diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..85b9293 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,144 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + +permissions: + contents: read + +jobs: + validate: + name: Validate (Node ${{ matrix.node }}) + runs-on: ubuntu-latest + timeout-minutes: 25 + strategy: + fail-fast: false + matrix: + node: ["22.22.2", "24.15.0"] + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v4 + + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v4 + with: + repository: TONresistor/teleton-agent + ref: dev + path: .teleton-agent + + - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v4 + with: + node-version: ${{ matrix.node }} + cache: npm + cache-dependency-path: | + package-lock.json + .teleton-agent/package-lock.json + plugins/*/package-lock.json + + - name: Install catalog tooling + run: npm ci --ignore-scripts --no-audit --no-fund + + - name: Install Teleton host dependencies + run: npm ci --ignore-scripts --no-audit --no-fund + working-directory: .teleton-agent + + - name: Build the public Teleton SDK contract + run: npm run build:sdk + working-directory: .teleton-agent + + - name: Install plugin dependencies + run: npm run install:plugins + + - name: Validate catalog policy + run: npm run validate + + - name: Run catalog tests + run: npm test + env: + TELETON_AGENT_DIR: ${{ github.workspace }}/.teleton-agent + + - name: Load every plugin against the current SDK + run: npm run validate:runtime + env: + TELETON_AGENT_DIR: ${{ github.workspace }}/.teleton-agent + + - name: Block vulnerable plugin dependencies + run: npm run audit:plugins + + notify: + needs: validate + if: always() && github.event_name == 'push' + runs-on: ubuntu-latest + timeout-minutes: 15 + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v4 + with: + fetch-depth: 0 + + - name: Notify Telegram + env: + TG_TOKEN: ${{ secrets.TELEGRAM_BOT_TOKEN }} + TG_CHAT: ${{ secrets.TELEGRAM_CHAT_ID }} + BRANCH: ${{ github.ref_name }} + BEFORE_SHA: ${{ github.event.before }} + AFTER_SHA: ${{ github.event.after }} + VALIDATE_RESULT: ${{ needs.validate.result }} + run: | + set -uo pipefail + NULL_SHA="0000000000000000000000000000000000000000" + + if [ -z "$TG_TOKEN" ] || [ -z "$TG_CHAT" ]; then + echo "Telegram secrets not configured, skipping" + exit 0 + fi + + send() { + local text="$1" attempt resp ra + for attempt in 1 2 3 4 5 6; do + resp=$(curl -sS -m 25 -X POST "https://api.telegram.org/bot${TG_TOKEN}/sendMessage" \ + -d chat_id="${TG_CHAT}" -d parse_mode=HTML -d disable_web_page_preview=true \ + --data-urlencode "text=${text}" 2>/dev/null || true) + case "$resp" in + *'"ok":true'*) return 0 ;; + esac + ra=$(printf '%s' "$resp" | grep -o '"retry_after":[0-9]*' | grep -o '[0-9]*' || true) + sleep "$(( ${ra:-2} + 1 ))" + done + echo "WARN: gave up sending after retries: ${text%%$'\n'*}" + return 1 + } + + REPO_URL="${{ github.server_url }}/${{ github.repository }}" + if [ "$BEFORE_SHA" = "$NULL_SHA" ] || ! git merge-base --is-ancestor "$BEFORE_SHA" "$AFTER_SHA" 2>/dev/null; then + COUNT=$(git rev-list --count "$AFTER_SHA") + RANGE="$AFTER_SHA" + URL="$REPO_URL/commits/$BRANCH" + else + COUNT=$(git rev-list --count "$BEFORE_SHA..$AFTER_SHA") + RANGE="$BEFORE_SHA..$AFTER_SHA" + URL="$REPO_URL/compare/$BEFORE_SHA...$AFTER_SHA" + fi + + NOUN=commits; [ "$COUNT" = "1" ] && NOUN=commit + REPO_NAME=${REPO_URL##*/} + LABEL=$(printf '%s' "$BRANCH" | tr '[:lower:]' '[:upper:]') + TEXT="[$LABEL] $REPO_NAME" + if [ "$VALIDATE_RESULT" = "success" ]; then + printf -v TEXT '%s\n%s %s pushed to %s' "$TEXT" "$COUNT" "$NOUN" "$BRANCH" + else + printf -v TEXT '%s\nCI failed on %s (%s %s)' "$TEXT" "$BRANCH" "$COUNT" "$NOUN" + fi + + MAX=50 + TITLES=$(git log --reverse --max-count="$MAX" --pretty=format:'%s' "$RANGE" \ + | sed -e 's/&/\&/g' -e 's//\>/g') + if [ "$COUNT" -gt 1 ]; then + if [ "$COUNT" -gt "$MAX" ]; then + printf -v TITLES '%s\nโ€ฆ and %s more' "$TITLES" "$((COUNT - MAX))" + fi + printf -v TEXT '%s\n\n
%s
' "$TEXT" "$TITLES" + elif [ -n "$TITLES" ]; then + printf -v TEXT '%s\n\n
%s
' "$TEXT" "$TITLES" + fi + printf -v TEXT '%s\n\nView commits' "$TEXT" "$URL" + send "$TEXT" diff --git a/.github/workflows/plugin-deps.yml b/.github/workflows/plugin-deps.yml deleted file mode 100644 index b9e5c3d..0000000 --- a/.github/workflows/plugin-deps.yml +++ /dev/null @@ -1,71 +0,0 @@ -name: Plugin Dependencies - -on: - pull_request: - paths: - - "plugins/*/package.json" - - "plugins/*/package-lock.json" - -jobs: - validate: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - - uses: actions/setup-node@v4 - with: - node-version: "20" - - - name: Find plugins with package.json - id: find - run: | - plugins="" - for pkg in plugins/*/package.json; do - dir=$(dirname "$pkg") - name=$(basename "$dir") - plugins="$plugins $name" - done - echo "plugins=$plugins" >> "$GITHUB_OUTPUT" - - - name: Verify lockfiles exist - run: | - exit_code=0 - for pkg in plugins/*/package.json; do - dir=$(dirname "$pkg") - if [ ! -f "$dir/package-lock.json" ]; then - echo "::error file=$pkg::Missing package-lock.json alongside package.json" - exit_code=1 - fi - done - exit $exit_code - - - name: Check for postinstall scripts - run: | - for pkg in plugins/*/package.json; do - dir=$(dirname "$pkg") - name=$(basename "$dir") - if [ -f "$dir/package-lock.json" ]; then - if grep -q '"postinstall"' "$dir/package-lock.json"; then - echo "::warning file=$pkg::Plugin '$name' has dependencies with postinstall scripts" - fi - fi - done - - - name: Install and audit deps - run: | - exit_code=0 - for pkg in plugins/*/package.json; do - dir=$(dirname "$pkg") - name=$(basename "$dir") - if [ ! -f "$dir/package-lock.json" ]; then - continue - fi - echo "--- $name ---" - if ! npm ci --ignore-scripts --no-audit --no-fund --prefix "$dir"; then - echo "::error file=$pkg::npm ci failed for plugin '$name'" - exit_code=1 - continue - fi - npm audit --prefix "$dir" || echo "::warning file=$pkg::Audit issues in plugin '$name'" - done - exit $exit_code diff --git a/.github/workflows/telegram-notify.yml b/.github/workflows/telegram-notify.yml deleted file mode 100644 index a05de86..0000000 --- a/.github/workflows/telegram-notify.yml +++ /dev/null @@ -1,52 +0,0 @@ -name: Telegram Notify - -on: - push: - branches: [main] - -jobs: - notify: - runs-on: ubuntu-latest - steps: - - name: Split commit message - id: msg - run: | - FULL="${{ github.event.head_commit.message }}" - TITLE=$(echo "$FULL" | head -1) - BODY=$(echo "$FULL" | tail -n +3) - echo "title=$TITLE" >> "$GITHUB_OUTPUT" - { - echo "body<> "$GITHUB_OUTPUT" - - - name: Build message - id: fmt - run: | - BODY="${{ steps.msg.outputs.body }}" - if [ -n "$BODY" ]; then - MSG="๐Ÿ“ฆ [plugins] ${{ steps.msg.outputs.title }} - - $BODY - - View commit" - else - MSG="๐Ÿ“ฆ [plugins] ${{ steps.msg.outputs.title }} - - View commit" - fi - { - echo "text<> "$GITHUB_OUTPUT" - - - name: Send to Telegram - uses: appleboy/telegram-action@master - with: - to: ${{ secrets.TELEGRAM_CHAT_ID }} - token: ${{ secrets.TELEGRAM_BOT_TOKEN }} - format: html - disable_web_page_preview: true - message: ${{ steps.fmt.outputs.text }} diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 439b805..753894f 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,702 +1,219 @@ -# Contributing +# Contributing to teleton-plugins -Plugins are single folders with an `index.js` that exports a `tools` array (or a function that returns one). Fork, add your plugin, open a PR. +This repository accepts plugins that comply with the Teleton SDK v2 capability model. A plugin is +not marketplace-ready merely because its JavaScript imports successfully: its manifest, tool +contract, dependency tree and security boundaries must all pass the repository validators. -## Steps +## Requirements -1. Fork this repo -2. Create `plugins/your-plugin/index.js` -3. Add a `manifest.json` in your plugin folder (see below) -4. Export a `tools` array or function (ESM โ€” `export const tools`) -5. Add a `README.md` in your plugin folder -6. Open a PR +- Node `^22.22.2`, `^24.15.0`, or `>=26.0.0`. +- ESM JavaScript only. +- `index.js`, `manifest.json` and `README.md` in each plugin directory. +- `package-lock.json` whenever `package.json` exists. +- Strict semver versions. +- `scope` and `category` on every tool. +- Public SDK capabilities only. -## Plugin structure +## Supported plugin structure +```text +plugins/my-plugin/ +โ”œโ”€โ”€ index.js +โ”œโ”€โ”€ manifest.json +โ”œโ”€โ”€ README.md +โ”œโ”€โ”€ package.json # optional +โ””โ”€โ”€ package-lock.json # required with package.json ``` -plugins/your-plugin/ -โ”œโ”€โ”€ index.js # Required โ€” exports tools[] or tools(sdk) -โ”œโ”€โ”€ manifest.json # Required โ€” plugin metadata -โ””โ”€โ”€ README.md # Required โ€” documentation + +### Runtime manifest + +SDK plugins must export a runtime manifest from `index.js`: + +```js +export const manifest = { + name: "my-plugin", + version: "1.0.0", + sdkVersion: "^2.0.0", + description: "One concise sentence", + defaultConfig: {}, + secrets: { + api_key: { + required: false, + description: "API key for the upstream service", + }, + }, +}; ``` -## manifest.json +The runtime and disk manifests must declare the same version and SDK range. A plugin that does not +use the SDK may omit `sdkVersion` in both places. -Every plugin must include a `manifest.json` at the root of its folder. This file describes the plugin to the registry and to the teleton runtime. +### Marketplace manifest ```json { - "id": "your-plugin", - "name": "Human-Readable Plugin Name", + "id": "my-plugin", + "name": "My Plugin", "version": "1.0.0", - "description": "One-line description of what the plugin does", - "author": { - "name": "your-name", - "url": "https://github.com/your-name" - }, + "description": "One concise sentence", + "author": { "name": "author", "url": "https://github.com/author" }, "license": "MIT", "entry": "index.js", - "teleton": ">=1.0.0", - "sdkVersion": ">=1.0.0", + "teleton": ">=0.9.0", + "sdkVersion": "^2.0.0", "tools": [ - { "name": "tool_name", "description": "What the tool does" } + { "name": "my_plugin_lookup", "description": "Look up an item" } ], "permissions": [], - "tags": ["category1", "category2"], - "repository": "https://github.com/TONresistor/teleton-plugins", - "funding": null + "tags": ["utility"] } ``` -### Field reference - -| Field | Type | Required | Description | -| ------------- | ------------ | -------- | ----------------------------------------------------------------------------------------- | -| `id` | string | **Yes** | Unique plugin identifier (lowercase, hyphens). Must match the folder name. | -| `name` | string | **Yes** | Human-readable display name shown in the registry. | -| `version` | string | **Yes** | Semver version string (e.g. `"1.0.0"`, `"2.3.1"`). | -| `description` | string | **Yes** | One-line description of what the plugin does. | -| `author` | object | **Yes** | Object with `name` (string) and `url` (string) fields. | -| `license` | string | **Yes** | SPDX license identifier (e.g. `"MIT"`, `"Apache-2.0"`). | -| `entry` | string | **Yes** | Entry point filename. Almost always `"index.js"`. | -| `teleton` | string | **Yes** | Minimum teleton version required (semver range, e.g. `">=1.0.0"`). | -| `sdkVersion` | string | No | Required SDK version (e.g. `">=1.0.0"`). Declare this if your plugin uses `tools(sdk)`. | -| `tools` | array | **Yes** | Array of objects, each with `name` and `description` for every tool the plugin exports. | -| `permissions` | array | **Yes** | Empty array `[]` by default. Add `"bridge"` if the plugin uses `context.bridge` directly. | -| `secrets` | object | No | Secret declarations โ€” `{ "key": { "required": bool, "description": string } }`. Validated at load time. | -| `tags` | array | No | Categories for discovery (e.g. `["defi", "ton", "trading"]`). | -| `bot` | object | No | Bot features: `{ inline?: bool, callbacks?: bool, rateLimits?: { inlinePerMinute?, callbackPerMinute? } }` | -| `hooks` | array | No | Hook declarations: `[{ name: string, priority?: number, description?: string }]` | -| `repository` | string | No | URL to the plugin's source repository. | -| `funding` | string\|null | No | Funding URL or `null`. | - -## Tool definition - -A tool is a plain object with a `name`, `description`, `parameters` (JSON Schema), and an async `execute` function. +The directory name, manifest `id`, runtime manifest `name`, compatibility entry and registry ID must +match. + +## Tool contract ```js -export const tools = [ +export const tools = (sdk) => [ { - name: "my_tool", - description: "What this tool does โ€” the LLM reads this to decide when to call it", + name: "my_plugin_lookup", + description: "Look up an item without changing external state", + scope: "always", + category: "data-bearing", parameters: { type: "object", properties: { - query: { type: "string", description: "Search query" } + query: { type: "string", description: "Item to look up" }, }, - required: ["query"] + required: ["query"], + }, + async execute(params) { + try { + const response = await fetch( + `https://api.example.com/items?q=${encodeURIComponent(params.query)}`, + { signal: AbortSignal.timeout(15_000) } + ); + if (!response.ok) return { success: false, error: `Upstream returned ${response.status}` }; + return { success: true, data: await response.json() }; + } catch (error) { + return { success: false, error: String(error?.message ?? error).slice(0, 500) }; + } }, - execute: async (params, context) => { - // params.query is what the LLM passed in - // context gives you Telegram, DB, user info - return { success: true, data: { result: "hello" } }; - } - } + }, ]; ``` -The `data` object is serialized to JSON and sent back to the LLM, which uses it to build its response. - -### Tool fields +Rules: -| Field | Type | Required | Description | -|-------|------|----------|-------------| -| `name` | string | **Yes** | Unique name across all plugins (e.g. `"weather_forecast"`) | -| `description` | string | **Yes** | LLM reads this to decide when to call your tool | -| `parameters` | object | No | JSON Schema for params. Defaults to empty object if omitted | -| `execute` | async function | **Yes** | `(params, context) => Promise` | -| `scope` | string | No | `"always"` (default), `"dm-only"`, `"group-only"`, or `"admin-only"` | -| `category` | string | No | `"data-bearing"` (read-only) or `"action"` (side-effect) โ€” helps the LLM reason about tool impact | +- Tool names match `^[a-z][a-z0-9_]{0,63}$` and are globally unique. +- `scope` is one of the SDK v2 values: `open`, `always`, `dm-only`, `group-only`, + `admin-only`, `allowlist`, or `disabled`. +- `category` is `data-bearing` for reads or `action` for side effects. +- Parameters use JSON Schema and bound arrays, strings, amounts and result counts. +- Network calls use explicit timeouts and bounded response handling. +- Returned errors are concise and do not include credentials or large upstream bodies. +- Actions must remain safe under retry and require Teleton's explicit approval flow. -### Return format +## SDK v2 boundaries -```js -// Success -return { success: true, data: { /* anything โ€” this is what the LLM sees */ } }; +Use only APIs declared by `@teleton-agent/sdk@^2`. The canonical reference is the +[SDK package README](https://github.com/TONresistor/teleton-agent/blob/dev/packages/sdk/README.md). -// Error -return { success: false, error: "What went wrong" }; -``` - -## Plugin SDK - -If your plugin needs TON blockchain or Telegram messaging features, export `tools` as a **function** instead of an array. The runtime passes a `sdk` object with high-level APIs: - -```js -export const tools = (sdk) => [ - { - name: "my_tool", - description: "Check balance and send a message", - parameters: { type: "object", properties: {} }, - execute: async (params, context) => { - const balance = await sdk.ton.getBalance(); - await sdk.telegram.sendMessage(context.chatId, `Balance: ${balance?.balance} TON`); - sdk.log.info("Balance checked"); - return { success: true, data: { balance: balance?.balance } }; - } - } -]; -``` +Forbidden: -The `context` object is still available in `execute` โ€” the SDK is an addition, not a replacement. - -### sdk.ton โ€” TON blockchain - -| Method | Returns | Throws | -|--------|---------|--------| -| `getAddress()` | `string \| null` โ€” bot's wallet address | โ€” | -| `getPublicKey()` | `string \| null` โ€” hex ed25519 public key, null if wallet not loaded | โ€” | -| `getWalletVersion()` | `string` โ€” always `"v5r1"` | โ€” | -| `getBalance(address?)` | `{ balance, balanceNano } \| null` โ€” defaults to bot's wallet | โ€” | -| `getPrice()` | `{ usd, source, timestamp } \| null` โ€” TON/USD price | โ€” | -| `sendTON(to, amount, comment?)` | `{ txRef, amount }` โ€” irreversible transfer | `WALLET_NOT_INITIALIZED`, `INVALID_ADDRESS`, `OPERATION_FAILED` | -| `getTransactions(address, limit?)` | `TonTransaction[]` โ€” max 50 | โ€” | -| `verifyPayment({ amount, memo, gameType, maxAgeMinutes? })` | `{ verified, txHash?, amount?, playerWallet?, date?, secondsAgo?, error? }` | `WALLET_NOT_INITIALIZED`, `OPERATION_FAILED` | -| `getJettonBalances(address?)` | `JettonBalance[]` โ€” all jetton balances | โ€” | -| `getJettonInfo(jettonAddress)` | `JettonInfo \| null` โ€” metadata, supply, holders | โ€” | -| `sendJetton(jettonAddress, to, amount, opts?)` | `{ success, seqno }` | `WALLET_NOT_INITIALIZED`, `INVALID_ADDRESS`, `OPERATION_FAILED` | -| `createJettonTransfer(jettonAddress, to, amount, opts?)` | `SignedTransfer` โ€” signed TEP-74 BOC without broadcasting | `WALLET_NOT_INITIALIZED`, `OPERATION_FAILED` | -| `getJettonWalletAddress(ownerAddress, jettonAddress)` | `string \| null` | โ€” | -| `getNftItems(address?)` | `NftItem[]` โ€” NFTs owned by address | โ€” | -| `getNftInfo(nftAddress)` | `NftItem \| null` โ€” NFT metadata and collection | โ€” | -| `toNano(amount)` | `bigint` โ€” converts TON to nanoTON | โ€” | -| `fromNano(amount)` | `string` โ€” converts nanoTON to TON | โ€” | -| `validateAddress(address)` | `boolean` โ€” checks if a TON address is valid | โ€” | -| `getJettonPrice(jettonAddress)` | `JettonPrice \| null` โ€” USD/TON price, 24h/7d/30d changes | โ€” | -| `getJettonHolders(jettonAddress, limit?)` | `JettonHolder[]` โ€” top holders by balance (max 100) | โ€” | -| `getJettonHistory(jettonAddress)` | `JettonHistory \| null` โ€” volume, FDV, market cap, holders | โ€” | - -Read methods return `null` or `[]` on failure. Write methods throw `PluginSDKError`. - -`SignedTransfer` shape: `{ signedBoc, walletPublicKey, walletAddress, seqno, validUntil }`. Deprecated aliases `boc`, `publicKey`, and `walletVersion` are also present for backwards compatibility. `opts` for `createJettonTransfer` accepts `{ comment?: string }`. - -### sdk.ton.dex โ€” DEX aggregator - -Compare and execute swaps across STON.fi and DeDust: - -| Method | Returns | Throws | -|--------|---------|--------| -| `dex.quote({ fromAsset, toAsset, amount, slippage? })` | `{ stonfi, dedust, recommended, savings }` | `OPERATION_FAILED` | -| `dex.quoteSTONfi(params)` | `DexSingleQuote \| null` | โ€” | -| `dex.quoteDeDust(params)` | `DexSingleQuote \| null` | โ€” | -| `dex.swap({ fromAsset, toAsset, amount, slippage?, dex? })` | `DexSwapResult` | `WALLET_NOT_INITIALIZED`, `OPERATION_FAILED` | -| `dex.swapSTONfi(params)` | `DexSwapResult` | `WALLET_NOT_INITIALIZED`, `OPERATION_FAILED` | -| `dex.swapDeDust(params)` | `DexSwapResult` | `WALLET_NOT_INITIALIZED`, `OPERATION_FAILED` | - -Assets are jetton master addresses (or `"TON"` for native TON). Slippage defaults to DEX-specific value. - -### sdk.ton.dns โ€” TON DNS domains - -Manage `.ton` domains โ€” check availability, auctions, linking, and TON Site records: - -| Method | Returns | Throws | -|--------|---------|--------| -| `dns.check(domain)` | `{ domain, available, owner?, nftAddress? }` | โ€” | -| `dns.resolve(domain)` | `{ domain, walletAddress, nftAddress, owner } \| null` | โ€” | -| `dns.getAuctions(limit?)` | `DnsAuction[]` (max 100) | โ€” | -| `dns.startAuction(domain)` | `{ domain, success, bidAmount }` | `WALLET_NOT_INITIALIZED`, `OPERATION_FAILED` | -| `dns.bid(domain, amount)` | `{ domain, bidAmount, success }` | `WALLET_NOT_INITIALIZED`, `OPERATION_FAILED` | -| `dns.link(domain, address)` | `void` | `WALLET_NOT_INITIALIZED`, `OPERATION_FAILED` | -| `dns.unlink(domain)` | `void` | `WALLET_NOT_INITIALIZED`, `OPERATION_FAILED` | -| `dns.setSiteRecord(domain, adnlAddress)` | `void` | `WALLET_NOT_INITIALIZED`, `OPERATION_FAILED` | - -### sdk.telegram โ€” Telegram messaging - -**Core messaging:** - -| Method | Returns | Throws | -| --------------------------------------------- | ---------------------------------------------- | ------------------------------------------ | -| `sendMessage(chatId, text, opts?)` | `number` โ€” message ID | `BRIDGE_NOT_CONNECTED`, `OPERATION_FAILED` | -| `editMessage(chatId, messageId, text, opts?)` | `number` โ€” message ID | `BRIDGE_NOT_CONNECTED`, `OPERATION_FAILED` | -| `deleteMessage(chatId, messageId, revoke?)` | `void` | `BRIDGE_NOT_CONNECTED`, `OPERATION_FAILED` | -| `forwardMessage(fromChat, toChat, messageId)` | `number` โ€” new message ID | `BRIDGE_NOT_CONNECTED`, `OPERATION_FAILED` | -| `pinMessage(chatId, messageId, opts?)` | `void` | `BRIDGE_NOT_CONNECTED`, `OPERATION_FAILED` | -| `sendDice(chatId, emoticon, replyToId?)` | `{ value, messageId }` | `BRIDGE_NOT_CONNECTED`, `OPERATION_FAILED` | -| `sendReaction(chatId, messageId, emoji)` | `void` | `BRIDGE_NOT_CONNECTED`, `OPERATION_FAILED` | -| `getMessages(chatId, limit?)` | `SimpleMessage[]` โ€” default 50 | โ€” | -| `searchMessages(chatId, query, limit?)` | `SimpleMessage[]` | โ€” | -| `getReplies(chatId, messageId, limit?)` | `SimpleMessage[]` | โ€” | -| `scheduleMessage(chatId, text, scheduleDate)` | `number` โ€” message ID | `BRIDGE_NOT_CONNECTED`, `OPERATION_FAILED` | -| `getScheduledMessages(chatId)` | `SimpleMessage[]` | โ€” | -| `deleteScheduledMessage(chatId, messageId)` | `void` | `BRIDGE_NOT_CONNECTED`, `OPERATION_FAILED` | -| `sendScheduledNow(chatId, messageId)` | `void` | `BRIDGE_NOT_CONNECTED`, `OPERATION_FAILED` | -| `getDialogs(limit?)` | `Dialog[]` โ€” conversations (max 100) | โ€” | -| `getHistory(chatId, limit?)` | `SimpleMessage[]` โ€” message history (max 100) | โ€” | -| `getMe()` | `{ id, username?, firstName?, isBot } \| null` | โ€” | -| `isAvailable()` | `boolean` | โ€” | -| `getRawClient()` | GramJS `TelegramClient \| null` โ€” escape hatch | โ€” | - -**Media:** - -| Method | Returns | Throws | -|--------|---------|--------| -| `sendPhoto(chatId, file, opts?)` | `number` โ€” message ID | `BRIDGE_NOT_CONNECTED`, `OPERATION_FAILED` | -| `sendVideo(chatId, file, opts?)` | `number` โ€” message ID | `BRIDGE_NOT_CONNECTED`, `OPERATION_FAILED` | -| `sendVoice(chatId, file, opts?)` | `number` โ€” message ID | `BRIDGE_NOT_CONNECTED`, `OPERATION_FAILED` | -| `sendFile(chatId, file, opts?)` | `number` โ€” message ID | `BRIDGE_NOT_CONNECTED`, `OPERATION_FAILED` | -| `sendGif(chatId, file, opts?)` | `number` โ€” message ID | `BRIDGE_NOT_CONNECTED`, `OPERATION_FAILED` | -| `sendSticker(chatId, file)` | `number` โ€” message ID | `BRIDGE_NOT_CONNECTED`, `OPERATION_FAILED` | -| `downloadMedia(chatId, messageId)` | `Buffer \| null` | `BRIDGE_NOT_CONNECTED`, `OPERATION_FAILED` | -| `setTyping(chatId)` | `void` | `BRIDGE_NOT_CONNECTED`, `OPERATION_FAILED` | - -**Social & moderation:** - -| Method | Returns | Throws | -|--------|---------|--------| -| `getChatInfo(chatId)` | `ChatInfo \| null` | โ€” | -| `getUserInfo(userId)` | `UserInfo \| null` | โ€” | -| `resolveUsername(username)` | `{ id, type } \| null` | โ€” | -| `getParticipants(chatId, limit?)` | `UserInfo[]` | โ€” | -| `createPoll(chatId, question, answers, opts?)` | `number` โ€” message ID | `BRIDGE_NOT_CONNECTED`, `OPERATION_FAILED` | -| `createQuiz(chatId, question, answers, correctIndex, explanation?)` | `number` โ€” message ID | `BRIDGE_NOT_CONNECTED`, `OPERATION_FAILED` | -| `banUser(chatId, userId)` | `void` | `BRIDGE_NOT_CONNECTED`, `OPERATION_FAILED` | -| `unbanUser(chatId, userId)` | `void` | `BRIDGE_NOT_CONNECTED`, `OPERATION_FAILED` | -| `muteUser(chatId, userId, untilDate)` | `void` | `BRIDGE_NOT_CONNECTED`, `OPERATION_FAILED` | -| `kickUser(chatId, userId)` | `void` โ€” ban + immediate unban | `BRIDGE_NOT_CONNECTED`, `OPERATION_FAILED` | - -**Stars & gifts:** - -| Method | Returns | Throws | -|--------|---------|--------| -| `getStarsBalance()` | `number` | `BRIDGE_NOT_CONNECTED`, `OPERATION_FAILED` | -| `sendGift(userId, giftId, opts?)` | `void` | `BRIDGE_NOT_CONNECTED`, `OPERATION_FAILED` | -| `getAvailableGifts()` | `StarGift[]` | `BRIDGE_NOT_CONNECTED`, `OPERATION_FAILED` | -| `getMyGifts(limit?)` | `ReceivedGift[]` | `BRIDGE_NOT_CONNECTED`, `OPERATION_FAILED` | -| `getResaleGifts(giftId, limit?)` | `StarGift[]` | `BRIDGE_NOT_CONNECTED`, `OPERATION_FAILED` | -| `buyResaleGift(giftId)` | `void` | `BRIDGE_NOT_CONNECTED`, `OPERATION_FAILED` | -| `getStarsTransactions(limit?)` | `StarsTransaction[]` | `BRIDGE_NOT_CONNECTED`, `OPERATION_FAILED` | -| `transferCollectible(msgId, toUserId)` | `TransferResult` โ€” `{ msgId, transferredTo, paidTransfer }` | `BRIDGE_NOT_CONNECTED`, `OPERATION_FAILED` | -| `setCollectiblePrice(msgId, price)` | `void` โ€” set/remove resale price | `BRIDGE_NOT_CONNECTED`, `OPERATION_FAILED` | -| `getCollectibleInfo(slug)` | `CollectibleInfo \| null` โ€” Fragment collectible info | โ€” | -| `getUniqueGift(slug)` | `UniqueGift \| null` โ€” NFT gift details | โ€” | -| `getUniqueGiftValue(slug)` | `GiftValue \| null` โ€” market valuation | โ€” | -| `sendGiftOffer(userId, giftSlug, price, opts?)` | `void` โ€” make buy offer | `BRIDGE_NOT_CONNECTED`, `OPERATION_FAILED` | - -**Stories:** - -| Method | Returns | Throws | -|--------|---------|--------| -| `sendStory(mediaPath, opts?)` | `number` โ€” story ID | `BRIDGE_NOT_CONNECTED`, `OPERATION_FAILED` | - -Options for `sendMessage`: -```js -await sdk.telegram.sendMessage(chatId, "Pick one:", { - replyToId: 123, - inlineKeyboard: [ - [{ text: "Option A", callback_data: "a" }, { text: "Option B", callback_data: "b" }] - ] -}); -``` +- `sdk.telegram.getRawClient()`; +- `context.bridge`, `ctx.bridge`, or private bridge/client traversal; +- direct imports of Teleton's private runtime modules; +- direct reads of `wallet.json`, wallet mnemonics or authentication stores; +- arbitrary filesystem access to Teleton state; +- secrets from global environment variables. -### sdk.bot โ€” Inline bot integration +Use instead: -If your plugin uses a Telegram bot for inline queries or button callbacks, declare `bot` in your manifest and use the Bot SDK: +- `sdk.telegram` for supported Telegram operations; +- `sdk.ton` for supported wallet and TON operations; +- `sdk.secrets` for declared credentials; +- `sdk.storage` or the isolated `sdk.db` for state; +- `sdk.pluginConfig` for plugin-specific configuration; +- `sdk.log` for structured logs. -```json -{ - "bot": { "inline": true, "callbacks": true, "rateLimits": { "inlinePerMinute": 30 } } -} -``` +If the public SDK lacks a required capability, do not bypass it. Mark the plugin quarantined in +`compatibility.json` and keep it out of `registry.json` until a safe capability exists. -| Member | Type | Description | -|--------|------|-------------| -| `isAvailable` | `boolean` (getter) | Whether the inline bot router is connected | -| `username` | `string` (getter) | Bot username from Grammy | -| `onInlineQuery(handler)` | `void` | Register inline query handler โ€” `handler(ctx) โ†’ InlineResult[]` | -| `onCallback(pattern, handler)` | `void` | Register callback handler for glob pattern โ€” `handler(ctx) โ†’ void` | -| `onChosenResult(handler)` | `void` | Register chosen result handler | -| `editInlineMessage(id, text, opts?)` | `void` | Edit an inline message (opts: `{ keyboard?, parseMode? }`) | -| `keyboard(rows: ButtonDef[][])` | `BotKeyboard` | Create keyboard with `.toGrammy()` and `.toTL()` serializers | +## Lifecycle -`sdk.bot` is `null` if no bot is configured or manifest doesn't declare `bot`. +Optional exports are `migrate`, `start`, `stop`, `onMessage`, and `onCallbackQuery`. -### sdk.on โ€” Plugin hooks +- `migrate(db)` receives only the plugin's isolated database. +- `start(ctx)` receives `sdk`, isolated `db`, sanitized `config`, `pluginConfig`, and `log`. +- Raw bridge access is not available. +- Cleanup timers and resources in `stop()`. -Register hooks to observe or intercept agent events. Declare hooks in your manifest: +## Secrets -```json -{ - "hooks": [{ "name": "tool:after", "priority": 50, "description": "Audit tool calls" }] -} -``` +Declare secrets in the runtime manifest and read them through `sdk.secrets`: ```js -export const tools = (sdk) => { - sdk.on("tool:after", async (event) => { - sdk.log.info(`Tool ${event.toolName} returned in ${event.durationMs}ms`); - }, { priority: 50 }); - return [/* tools */]; -}; +const token = sdk.secrets.get("api_key"); +if (!token) return { success: false, error: "API key is not configured" }; ``` -**13 hook types:** `tool:before`, `tool:after`, `tool:error`, `prompt:before`, `prompt:after`, `session:start`, `session:end`, `message:receive`, `response:before`, `response:after`, `response:error`, `agent:start`, `agent:stop` - -**Priority:** negative = security gates, 0 = default, 50+ = audit/logging, 100+ = reserved. - -### sdk.db โ€” Isolated database - -Each plugin gets its own SQLite database at `~/.teleton/plugins/data/{plugin-name}.db`. To enable it, export a `migrate` function: - -```js -export function migrate(db) { - db.exec(`CREATE TABLE IF NOT EXISTS scores ( - user_id TEXT PRIMARY KEY, - points INTEGER NOT NULL DEFAULT 0 - )`); -} - -export const tools = (sdk) => [{ - name: "my_tool", - execute: async (params, context) => { - // sdk.db is a full better-sqlite3 instance - sdk.db.prepare("INSERT INTO scores ...").run(...); - const row = sdk.db.prepare("SELECT * FROM scores WHERE user_id = ?").get(userId); - return { success: true, data: row }; - } -}]; -``` - -If you don't export `migrate`, `sdk.db` is `null`. - -### sdk.secrets โ€” Secret management - -3-tier resolution: **ENV variable** โ†’ **secrets store** (`~/.teleton/plugins/data/.secrets.json`) โ†’ **pluginConfig fallback**. - -| Method | Returns | Throws | -|--------|---------|--------| -| `get(key)` | `string \| undefined` โ€” resolved secret | โ€” | -| `require(key)` | `string` โ€” resolved secret (throws if missing) | `SECRET_NOT_FOUND` | -| `has(key)` | `boolean` โ€” checks if secret exists | โ€” | - -Declare secrets in `manifest.json` for validation at load time: - -```json -{ - "secrets": { - "api_key": { "required": true, "description": "API key for the service" }, - "webhook_url": { "required": false, "description": "Optional webhook endpoint" } - } -} -``` - -```js -// In your plugin: -const apiKey = sdk.secrets.require("api_key"); // throws if missing -const webhook = sdk.secrets.get("webhook_url"); // undefined if not set -``` - -Users set secrets via env vars (`YOURPLUGIN_API_KEY` โ€” plugin name uppercased, hyphens to underscores) or the secrets store (`/plugin set `). - -### sdk.storage โ€” Key-value store with TTL - -Auto-provisioned KV store โ€” no `migrate()` needed. Uses a `_kv` table in the plugin's SQLite database. - -| Method | Returns | Description | -|--------|---------|-------------| -| `get(key)` | `T \| undefined` | Get value (returns undefined if expired or missing) | -| `set(key, value, opts?)` | `void` | Set value with optional `{ ttl }` in milliseconds | -| `delete(key)` | `boolean` | Delete a key (returns true if key existed) | -| `has(key)` | `boolean` | Check if key exists and isn't expired | -| `clear()` | `void` | Delete all keys in this plugin's storage | - -```js -// Cache API responses for 1 hour (TTL in milliseconds) -sdk.storage.set("token_price", price, { ttl: 3_600_000 }); -const cached = sdk.storage.get("token_price"); -if (cached) return cached; // auto-deserialized from JSON -``` - -### sdk.config & sdk.pluginConfig - -- `sdk.config` โ€” sanitized application config (no API keys or secrets) -- `sdk.pluginConfig` โ€” plugin-specific config from `~/.teleton/config.yaml` - -Plugin config is merged with defaults from your manifest: - -```js -// In your plugin: -export const manifest = { - name: "my-plugin", - defaultConfig: { threshold: 50, mode: "auto" } -}; -``` - -```yaml -# In ~/.teleton/config.yaml (optional โ€” only if the user wants to override): -plugins: - my_plugin: - threshold: 100 -``` - -Result: `sdk.pluginConfig = { threshold: 100, mode: "auto" }`. The plugin works out of the box with `defaultConfig` โ€” users only touch config.yaml to override. - -### sdk.log โ€” Prefixed logger - -```js -sdk.log.info("started"); // [my-plugin] started -sdk.log.warn("low funds"); // โš ๏ธ [my-plugin] low funds -sdk.log.error("failed"); // โŒ [my-plugin] failed -sdk.log.debug("details"); // ๐Ÿ” [my-plugin] details (only if DEBUG or VERBOSE env) -``` - -### Error handling with SDK - -SDK write methods throw `PluginSDKError` with a `.code` property: - -```js -try { - await sdk.ton.sendTON(address, 1.0); -} catch (err) { - if (err.name === "PluginSDKError") { - switch (err.code) { - case "WALLET_NOT_INITIALIZED": // wallet not set up - case "INVALID_ADDRESS": // bad TON address - case "BRIDGE_NOT_CONNECTED": // Telegram not ready - case "SECRET_NOT_FOUND": // sdk.secrets.require() failed - case "OPERATION_FAILED": // generic failure - } - } - return { success: false, error: String(err.message).slice(0, 500) }; -} -``` +Never commit credentials, log them, include them in tool results, or read unrelated process +environment variables. -## Advanced lifecycle +## Dependencies -Beyond `tools`, plugins can export additional hooks for database, background tasks, and cleanup: - -```js -// Optional: the runtime reads this for sdkVersion, defaultConfig, etc. -// The manifest.json file is used by the registry for discovery. -export const manifest = { - name: "my-plugin", - version: "1.0.0", - sdkVersion: ">=1.0.0", - defaultConfig: { key: "value" }, -}; - -// Optional: database setup (enables sdk.db) -export function migrate(db) { - db.exec(`CREATE TABLE IF NOT EXISTS ...`); -} - -// Required: tools -export const tools = (sdk) => [{ ... }]; - -// Optional: runs after Telegram bridge connects -export async function start(ctx) { - // ctx.bridge โ€” TelegramBridge - // ctx.db โ€” plugin's isolated DB (null if no migrate) - // ctx.config โ€” sanitized app config - // ctx.pluginConfig โ€” plugin-specific config - // ctx.log โ€” prefixed logger function -} - -// Optional: runs on shutdown -export async function stop() { - // cleanup timers, connections, etc. -} -``` - -Execution order: `import` โ†’ manifest validation โ†’ `migrate(db)` โ†’ `tools(sdk)` โ†’ register โ†’ `start(ctx)` โ†’ ... โ†’ `stop()`. - -## Context object - -Your `execute` function receives `(params, context)`. The context contains: - -| Field | Type | Description | -|-------|------|-------------| -| `bridge` | TelegramBridge | Send messages, reactions, media via Telegram (low-level) | -| `db` | Database | SQLite instance (shared โ€” prefer `sdk.db` for isolation) | -| `chatId` | string | Current chat ID | -| `senderId` | number | Telegram user ID of who triggered the tool | -| `isGroup` | boolean | `true` if group chat, `false` if DM | -| `config` | Config? | Agent configuration (may be undefined) | - -When using the SDK, prefer `sdk.telegram` over `context.bridge` and `sdk.db` over `context.db`. - -## Best practices - -### Fetch timeouts - -Always use `AbortSignal.timeout()` on every `fetch()` call. This prevents tools from hanging indefinitely when an external API is slow or unreachable. - -```js -const res = await fetch(url, { signal: AbortSignal.timeout(15000) }); -``` - -### CJS dependencies - -The teleton runtime is ESM-only, but some Node.js packages (like `@ton/core`) only ship CommonJS. Use `createRequire` with `realpathSync` to load them: - -```js -import { createRequire } from "node:module"; -import { realpathSync } from "node:fs"; - -const _require = createRequire(realpathSync(process.argv[1])); -const { Address } = _require("@ton/core"); -``` - -### Per-plugin npm dependencies - -Plugins can declare their own npm dependencies by adding `package.json` and `package-lock.json` to the plugin folder. Teleton auto-installs them at startup โ€” no manual step needed. - -**Setup:** +Plugin dependencies are installed with lifecycle scripts disabled: ```bash -cd plugins/your-plugin -npm init -y -npm install some-package -# Commit both package.json and package-lock.json -``` - -**Dual-require pattern** โ€” use two `createRequire` instances to separate core and plugin-local deps: - -```js -import { createRequire } from "node:module"; -import { realpathSync } from "node:fs"; - -// Core deps (provided by teleton runtime: @ton/core, @ton/ton, @ton/crypto, telegram) -const _require = createRequire(realpathSync(process.argv[1])); -// Plugin-local deps (from your plugin's node_modules/) -const _pluginRequire = createRequire(import.meta.url); - -const { Address } = _require("@ton/core"); // core -const { getHttpEndpoint } = _pluginRequire("@orbs-network/ton-access"); // plugin-local -``` - -**Rules:** -- A `package-lock.json` is **required** alongside `package.json` (the loader skips install without it) -- Dependencies are installed with `npm ci --ignore-scripts` (no postinstall scripts run) -- `node_modules/` is gitignored โ€” it's created automatically at startup -- If install fails (e.g. no network), the plugin is skipped with a warning - -### Bridge access - -When your plugin needs direct Telegram MTProto access, you have two options: - -```js -// Option 1: SDK (recommended) -const client = sdk.telegram.getRawClient(); - -// Option 2: Context (legacy) -const client = context.bridge.getClient().getClient(); -``` - -If using `context.bridge` directly, declare `"permissions": ["bridge"]` in your `manifest.json`. - -### Tool scope - -Control where your tools are available: - -```js -{ - name: "send_payment", - scope: "dm-only", // Only in DMs (financial operations) - execute: async (params, context) => { ... } -} +npm ci --ignore-scripts --no-audit --no-fund ``` -- `"always"` (default) โ€” available in DMs and groups -- `"dm-only"` โ€” only in DMs (use for financial, private tools) -- `"group-only"` โ€” only in groups (use for moderation tools) -- `"admin-only"` โ€” only for admins (use for sensitive operations) - -### Error handling - -Always return the `{ success, data/error }` format. Slice long error messages to avoid flooding the LLM context: +Commit the generated lockfile. HIGH or CRITICAL audit findings block CI, including for quarantined +plugins retained in the repository. -```js -try { - const result = await doSomething(params); - return { success: true, data: result }; -} catch (err) { - return { success: false, error: String(err.message || err).slice(0, 500) }; -} -``` +## Compatibility and registry policy -## Rules +Every plugin has exactly one entry in [`compatibility.json`](compatibility.json): -- **ESM only** โ€” use `export const tools`, not `module.exports` -- **JS only at runtime** โ€” the loader only reads `.js` files. Write TypeScript if you want, but compile to `.js` first -- **`manifest.json` is required** โ€” plugins without it won't be listed in the registry -- Tool `name` must be globally unique โ€” if it collides with a built-in or another plugin, yours is silently skipped -- **Tool names must be prefixed** with the plugin name or a short unique prefix (e.g. `gas_`, `storm_`, `gift_`) -- **Defaults** โ€” use `??` (nullish coalescing), never `||` for default values -- **Per-plugin npm deps** โ€” plugins that need npm packages beyond the core runtime (`@ton/core`, `@ton/ton`, `@ton/crypto`, `telegram`) should add a `package.json` + `package-lock.json` in their plugin folder. Teleton auto-installs them at startup via `npm ci --ignore-scripts`. Use the dual-require pattern to load plugin-local deps (see below) -- **Use `AbortSignal.timeout()`** on all `fetch()` calls โ€” never let a network request hang without a timeout -- **GramJS** โ€” always use `createRequire(realpathSync(process.argv[1]))` to import CJS packages, never `import from "telegram"` -- **Declare `sdkVersion`** in `manifest.json` if your plugin uses `tools(sdk)` (e.g. `"sdkVersion": ">=1.0.0"`) +- `supported`: validated against SDK v2; +- `quarantined`: preserved but deliberately incompatible with SDK v2; +- `marketplace: true`: allowed only for supported production plugins. -## Local testing - -To test a plugin without restarting Teleton, verify it loads and exports the correct number of tools: +`registry.json` is the generated installable marketplace, not an archive. Examples and quarantined +plugins do not belong there. Never edit it or the generated README catalog sections manually; after +changing a manifest or `compatibility.json`, run: ```bash -# Simple array format -node -e "import('./plugins/your-plugin/index.js').then(m => console.log(m.tools.length, 'tools exported'))" - -# SDK function format (tools is a function, so check it exists) -node -e "import('./plugins/your-plugin/index.js').then(m => console.log(typeof m.tools, 'โ€” tools export type'))" +npm run generate ``` -To install it for live testing with Teleton: - -```bash -mkdir -p ~/.teleton/plugins -cp -r plugins/your-plugin ~/.teleton/plugins/ -``` +## Required local checks -Then restart Teleton and check the console output. +With `teleton-agent` checked out as `../teleton-agent`: -## Verify it works - -After installing your plugin and restarting Teleton, check the console output: - -``` -Plugin "example": 2 tools registered <- success -Plugin "my-plugin": no 'tools' array exported <- missing export -Plugin "my-plugin": tool "foo" missing 'execute' <- bad tool definition -Plugin "my-plugin" failed to load: <- syntax error or crash -``` - -If you see the registered line with your plugin name, it works. - -## Plugin README template - -Your plugin's `README.md` should include: - -- Plugin name and one-line description -- Table of tools (name + what each one does) -- Install command (`cp -r plugins/your-plugin ~/.teleton/plugins/`) -- Usage examples (natural language prompts the user can send) -- Parameter tables per tool (param name, type, required, default, description) - -Example structure: - -```markdown -# your-plugin - -One-line description of what the plugin does. - -| Tool | Description | -|------|-------------| -| `tool_name` | What it does | - -## Install - -mkdir -p ~/.teleton/plugins -cp -r plugins/your-plugin ~/.teleton/plugins/ - -## Usage examples - -- "Ask the AI to do X" -- "Ask the AI to do Y" - -## Tool schemas - -### tool_name - -| Param | Type | Required | Default | Description | -|-------|------|----------|---------|-------------| -| `param` | string | Yes | โ€” | What it is | -``` +```bash +npm ci --ignore-scripts +npm run install:plugins +npm run validate +npm test +npm --prefix ../teleton-agent run build:sdk +npm run validate:runtime +npm run audit:plugins +``` + +CI repeats these checks using the current `teleton-agent` `dev` branch on Node 22 and Node 24. + +## Pull request checklist + +- [ ] Plugin ID and tool names are globally unique. +- [ ] Runtime and disk manifests agree. +- [ ] Every tool has a valid scope, category and bounded schema. +- [ ] No raw Telegram client, bridge, wallet file or mnemonic access. +- [ ] Secrets are declared and use `sdk.secrets`. +- [ ] Dependency lockfile is committed and audit is clean. +- [ ] README documents configuration, tools and side effects. +- [ ] `compatibility.json` is updated. +- [ ] `npm run generate` updated the generated registry and README sections. +- [ ] All required local checks pass. diff --git a/README.md b/README.md index 51c1b48..2c775bc 100644 --- a/README.md +++ b/README.md @@ -2,763 +2,172 @@ # teleton-plugins -[![GitHub stars](https://img.shields.io/github/stars/TONresistor/teleton-plugins?style=flat&logo=github)](https://github.com/TONresistor/teleton-plugins/stargazers) -[![Plugins](https://img.shields.io/badge/plugins-25-8B5CF6.svg)](#available-plugins) -[![Tools](https://img.shields.io/badge/tools-183-E040FB.svg)](#available-plugins) -[![SDK](https://img.shields.io/badge/SDK-v1.0.0-00C896.svg)](#plugin-sdk) + +[![SDK](https://img.shields.io/badge/SDK-v2-00C896.svg)](https://www.npmjs.com/package/@teleton-agent/sdk) +[![Marketplace](https://img.shields.io/badge/marketplace-22-8B5CF6.svg)](#sdk-v2-marketplace) +[![Catalog](https://img.shields.io/badge/catalog-26_plugins-E040FB.svg)](compatibility.json) + [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE) -[![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](CONTRIBUTING.md) -[![SKILL.md](https://img.shields.io/badge/SKILL.md-AI%20prompt-F97316.svg)](https://github.com/TONresistor/teleton-plugins/blob/main/SKILL.md) -[![Telegram](https://img.shields.io/badge/Telegram-community-26A5E4.svg?logo=telegram)](https://t.me/ResistanceForum) -Community plugin directory for [Teleton](https://github.com/TONresistor/teleton-agent), the Telegram AI agent on TON.
-Drop a plugin in `~/.teleton/plugins/` and it's live. No build step, no config. +Official community plugin catalog for [Teleton Agent](https://github.com/TONresistor/teleton-agent). ---- - -
-Table of Contents - -- [How It Works](#how-it-works) -- [Quick Start](#quick-start) -- [Available Plugins](#available-plugins) -- [Build Your Own](#build-your-own) -- [Plugin SDK](#plugin-sdk) -- [Troubleshooting](#troubleshooting) -- [FAQ](#faq) -- [Community](#community) -- [Contributors](#contributors) -- [License](#license) - -
- -## How It Works - -``` -User message - โ†’ LLM reads tool descriptions - โ†’ picks and calls a tool - โ†’ execute(params, context) runs - โ†’ result JSON โ†’ LLM - โ†’ LLM responds to user -``` - -Teleton loads every folder from `~/.teleton/plugins/` at startup. Each plugin exports a `tools` array (or a function that receives the [Plugin SDK](#plugin-sdk)). The `execute` function receives the LLM's parameters and a `context` with Telegram bridge access. The returned `data` object is serialized to JSON and fed back to the LLM. - -**Plugin lifecycle:** `manifest.json` is read first, then `migrate(db)` runs if exported (for database setup), then `tools` are registered, `start(ctx)` is called if exported, and `stop()` on shutdown. - -## Quick Start - -### Option 1 โ€” WebUI Marketplace (recommended) +## Compatibility policy + +The target SDK version and plugin status are declared once in +[`compatibility.json`](compatibility.json). Only supported marketplace plugins are generated into +[`registry.json`](registry.json). + +Legacy source remains available for migration and audit history, but quarantined plugins are not +offered through the WebUI marketplace. This prevents SDK v2 from installing plugins that depend on +removed raw Telegram access or direct wallet mnemonic access. + + +| Status | Plugins | Meaning | +|---|---:|---| +| SDK v2 supported | 24 | Loads against SDK v2; 22 marketplace plugins plus 2 examples | +| Quarantined | 2 | Preserved in source, rejected by SDK v2 and excluded from the marketplace | +| Total | 26 | 193 tools | + + + +## SDK v2 marketplace + +| Plugin | Tools | Description | +|---|---:|---| +| [boards](plugins/boards/) | 9 | Browse and participate in the boards.ton decentralized forum using x402 TON micropayments | +| [casino](plugins/casino/) | 4 | Slot machine and dice games with TON payments and auto-payout | +| [crypto-prices](plugins/crypto-prices/) | 2 | Real-time cryptocurrency prices and comparison via CryptoCompare API | +| [dedust](plugins/dedust/) | 8 | Swap tokens, browse pools, and trade on DeDust -- TON's #2 DEX | +| [deezer](plugins/deezer/) | 1 | Search and send music tracks via Telegram's @DeezerMusicBot inline bot (Deezer) | +| [dyor](plugins/dyor/) | 11 | TON token analytics from DYOR.io -- search, price, trust score, metrics, DEX trades, holders, pools | +| [fragment](plugins/fragment/) | 6 | Search and browse Telegram's NFT marketplace โ€” usernames, numbers, collectible gifts, auction history | +| [geckoterminal](plugins/geckoterminal/) | 10 | TON DEX pool and token data -- trending, new, and top pools, trades, OHLCV, token info, batch prices | +| [giftindex](plugins/giftindex/) | 6 | GiftIndex ODROB trading with workflow guardrails - monitor, analyze and trade the Telegram Gifts index on TON. Owner-only, corridor-enforced, post-trade verified. | +| [giftstat](plugins/giftstat/) | 11 | Telegram gift market data -- collections, floor prices, models, stats, history | +| [multisend](plugins/multisend/) | 5 | Batch send TON and jettons to up to 254 recipients through Teleton's protected Highload Wallet v3 broker | +| [pic](plugins/pic/) | 1 | Search and send images via Telegram's @pic inline bot (Yandex Image Search) | +| [sbt](plugins/sbt/) | 2 | Deploy and mint Soulbound Tokens (TEP-85) on TON | +| [stonfi](plugins/stonfi/) | 8 | Swap tokens, browse pools, and farm on StonFi DEX -- the largest DEX on TON | +| [stormtrade](plugins/stormtrade/) | 13 | Trade perpetual futures on Storm Trade DEX โ€” crypto, stocks, forex, commodities | +| [swapcoffee](plugins/swapcoffee/) | 6 | Swap tokens on TON via swap.coffee aggregator - best rates across all DEXes | +| [tonapi](plugins/tonapi/) | 20 | TON blockchain data from TONAPI -- accounts, jettons, NFTs, prices, transactions, traces, DNS, staking | +| [twitter](plugins/twitter/) | 24 | X/Twitter API v2 โ€” read (search, lookup, trends) + write (post, like, retweet, follow) with OAuth 1.0a | +| [uranus](plugins/uranus/) | 13 | Inspect, quote, trade and launch Uranus Meme tokens on TON through the protected Teleton SDK transaction broker. | +| [vid](plugins/vid/) | 1 | Search and send YouTube videos via Telegram's @vid inline bot | +| [weather](plugins/weather/) | 2 | Current weather and 7-day forecast via Open-Meteo API | +| [webdom](plugins/webdom/) | 11 | Buy, sell, auction, and manage .ton domains and Telegram usernames on webdom.market | + + +Development examples are available in [`plugins/example`](plugins/example/) and +[`plugins/example-sdk`](plugins/example-sdk/), but are intentionally not marketplace entries. + +## Quarantined legacy plugins + + +| Plugin | Blocker | +|---|---| +| `gaspump` | Legacy SDK v1 plugin retained for older Teleton agents; raw bridge and wallet access are incompatible with SDK v2. | +| `voice-notes` | Legacy SDK v1 plugin retained for older Teleton agents; raw Telegram client access is incompatible with SDK v2. | + + +Plugins listed in this section declare an incompatible SDK range, so the runtime rejects them before registering tools. +They return to the marketplace only after migration to public SDK capabilities. + +## Install + +Use the Teleton WebUI marketplace for supported plugins: ```bash teleton start --webui ``` -Open the WebUI in your browser, go to **Plugins** > **Marketplace** tab, and install any community plugin with one click. No manual copy, no git clone โ€” browse, install, done. - -### Option 2 โ€” Manual install +For local development, copy a supported plugin into the agent data directory and restart Teleton: ```bash -# 1. Create the plugins directory -mkdir -p ~/.teleton/plugins - -# 2. Clone and copy any plugin -git clone https://github.com/TONresistor/teleton-plugins.git -cp -r teleton-plugins/plugins/example ~/.teleton/plugins/ - -# 3. Restart Teleton โ€” the plugin loads automatically +cp -R plugins/weather ~/.teleton/plugins/ +teleton start ``` -No build step. Just copy and go. Plugins with npm dependencies are auto-installed at startup. - -## Available Plugins - -> **25 plugins** ยท **183 tools** ยท [Browse the registry](registry.json) - -### DeFi & Trading - -| Plugin | Description | Tools | Author | -|--------|-------------|:-----:|--------| -| [gaspump](plugins/gaspump/) | Launch, trade, and manage meme tokens on Gas111/TON | 13 | teleton | -| [stormtrade](plugins/stormtrade/) | Perpetual futures โ€” crypto, stocks, forex, commodities | 13 | teleton | -| [evaa](plugins/evaa/) | EVAA Protocol โ€” supply, borrow, withdraw, repay, liquidate | 11 | teleton | -| [stonfi](plugins/stonfi/) | StonFi DEX โ€” tokens, pools, farms, swap | 8 | teleton | -| [dedust](plugins/dedust/) | DeDust DEX โ€” pools, assets, trades, on-chain swaps | 8 | teleton | -| [swapcoffee](plugins/swapcoffee/) | swap.coffee aggregator โ€” best rates across all DEXes | 6 | teleton | -| [giftindex](plugins/giftindex/) | GiftIndex ODROB โ€” trade Telegram Gifts index on TON | 6 | teleton | - -### Market Data & Analytics - -| Plugin | Description | Tools | Author | -|--------|-------------|:-----:|--------| -| [tonapi](plugins/tonapi/) | TON blockchain data โ€” accounts, jettons, NFTs, DNS, staking | 20 | teleton | -| [giftstat](plugins/giftstat/) | Telegram gift market data from Giftstat API | 11 | teleton | -| [dyor](plugins/dyor/) | DYOR.io โ€” trust score, price, metrics, holders, pools | 11 | teleton | -| [geckoterminal](plugins/geckoterminal/) | TON DEX pools โ€” trending, OHLCV, batch prices | 10 | teleton | -| [crypto-prices](plugins/crypto-prices/) | Real-time prices for 5000+ coins | 2 | walged | - -### Social & Messaging - -| Plugin | Description | Tools | Author | -|--------|-------------|:-----:|--------| -| [twitter](plugins/twitter/) | X/Twitter API v2 โ€” search, post, like, retweet, follow | 24 | teleton | -| [pic](plugins/pic/) | Image search via @pic inline bot | 1 | teleton | -| [vid](plugins/vid/) | YouTube search via @vid inline bot | 1 | teleton | -| [deezer](plugins/deezer/) | Music search via @DeezerMusicBot | 1 | teleton | -| [voice-notes](plugins/voice-notes/) | Transcribe voice messages (Premium STT) | 1 | walged | - -### TON Infrastructure - -| Plugin | Description | Tools | Author | -|--------|-------------|:-----:|--------| -| [multisend](plugins/multisend/) | Batch send TON/jettons to 254 recipients in one TX | 5 | teleton | -| [sbt](plugins/sbt/) | Deploy and mint Soulbound Tokens (TEP-85) | 2 | teleton | - -### Marketplace & NFTs +Do not manually install quarantined plugins into an SDK v2 runtime. -| Plugin | Description | Tools | Author | -|--------|-------------|:-----:|--------| -| [fragment](plugins/fragment/) | Fragment marketplace โ€” usernames, numbers, collectible gifts | 6 | teleton | -| [webdom](plugins/webdom/) | TON domain marketplace โ€” search, buy, sell, auction, DNS bid | 12 | teleton | +## Build a plugin -### Utilities & Games +A plugin is an ESM directory with three required files: -| Plugin | Description | Tools | Author | -|--------|-------------|:-----:|--------| -| [casino](plugins/casino/) | Slot machine and dice games with TON payments and auto-payout | 4 | teleton | -| [example](plugins/example/) | Dice roller and random picker | 2 | teleton | -| [example-sdk](plugins/example-sdk/) | SDK example โ€” greeting counter, balance check, announcements | 3 | teleton | -| [weather](plugins/weather/) | Weather and 7-day forecast via Open-Meteo | 2 | walged | - -## Build Your Own - -Three files. No build step. ESM only. - -``` -plugins/your-plugin/ -โ”œโ”€โ”€ index.js # exports tools[] or tools(sdk) -โ”œโ”€โ”€ manifest.json # registry metadata (marketplace, discovery) -โ””โ”€โ”€ README.md # documentation -``` - -### Pattern A โ€” Simple (static array) - -For plugins that only call external APIs and return data to the LLM. No TON, no Telegram messaging, no state. - -```js -// index.js -export const tools = [ - { - name: "myplugin_search", - description: "Search for something โ€” the LLM reads this to decide when to call the tool", - parameters: { - type: "object", - properties: { - query: { type: "string", description: "Search query" }, - }, - required: ["query"], - }, - execute: async (params, context) => { - try { - const res = await fetch(`https://api.example.com/search?q=${encodeURIComponent(params.query)}`, { - signal: AbortSignal.timeout(15_000), - }); - if (!res.ok) return { success: false, error: `API returned ${res.status}` }; - const data = await res.json(); - return { success: true, data }; - } catch (err) { - return { success: false, error: String(err.message || err).slice(0, 500) }; - } - }, - }, -]; +```text +plugins/my-plugin/ +โ”œโ”€โ”€ index.js +โ”œโ”€โ”€ manifest.json +โ””โ”€โ”€ README.md ``` -### Pattern B โ€” SDK plugin (function) - -For plugins that need TON blockchain, Telegram messaging, database, inline bot mode, or secrets. Export `tools` as a **function** that receives the SDK, and add an inline `manifest` for runtime config: +Use a static `tools` array for pure local logic or external API reads. Use `tools(sdk)` when the +plugin needs public TON, Telegram, storage, secrets or logging capabilities. ```js -// index.js export const manifest = { name: "my-plugin", version: "1.0.0", - sdkVersion: ">=1.0.0", - description: "What this plugin does", - defaultConfig: { threshold: 50 }, - // bot: { inline: true, callbacks: true }, // uncomment for inline mode + sdkVersion: "^2.0.0", + description: "Example SDK v2 plugin", }; -// Optional: enables sdk.db (isolated SQLite per plugin) -export function migrate(db) { - db.exec(`CREATE TABLE IF NOT EXISTS scores ( - user_id TEXT PRIMARY KEY, - points INTEGER NOT NULL DEFAULT 0 - )`); -} - export const tools = (sdk) => [ { - name: "myplugin_balance", - description: "Check TON wallet balance and current price", + name: "my_plugin_balance", + description: "Read the configured TON wallet balance", + scope: "admin-only", + category: "data-bearing", parameters: { type: "object", properties: {} }, - scope: "dm-only", // "always" | "dm-only" | "group-only" | "admin-only" - category: "data-bearing", // "data-bearing" (reads) | "action" (writes) - execute: async (params, context) => { - try { - const balance = await sdk.ton.getBalance(); - const price = await sdk.ton.getPrice(); - sdk.log.info(`Balance: ${balance?.balance ?? "unknown"} TON`); - return { - success: true, - data: { - balance: balance?.balance, - usd: price?.usd, - }, - }; - } catch (err) { - return { success: false, error: String(err.message || err).slice(0, 500) }; - } + async execute() { + const balance = await sdk.ton.getBalance(); + return { success: true, data: balance }; }, }, ]; - -// Optional lifecycle hooks -export async function start(ctx) { /* ctx.bridge, ctx.db, ctx.config, ctx.pluginConfig, ctx.log */ } -export async function stop() { /* cleanup timers, connections */ } -``` - -> See [`plugins/example/`](plugins/example/) for Pattern A and [`plugins/example-sdk/`](plugins/example-sdk/) for Pattern B. - -### Two manifests - -Plugins have **two** manifest sources with different roles: - -| File | Purpose | Required | -|------|---------|----------| -| `manifest.json` | Registry & marketplace (discovery, listing, metadata) | **Yes** | -| `export const manifest` in `index.js` | Runtime config (SDK version, defaults, secrets, bot) | Only for Pattern B | - -**manifest.json** (for registry): - -```json -{ - "id": "my-plugin", - "name": "My Plugin", - "version": "1.0.0", - "description": "One-line description", - "author": { "name": "your-name", "url": "https://github.com/your-name" }, - "license": "MIT", - "entry": "index.js", - "teleton": ">=1.0.0", - "tools": [{ "name": "myplugin_balance", "description": "Check TON balance" }], - "permissions": [], - "tags": ["defi", "ton"] -} -``` - -Add `"sdkVersion": ">=1.0.0"` for Pattern B plugins. Add `"secrets"` if your plugin needs API keys (see below). - -### Secrets - -Declare secrets in `manifest.json` so users know what to configure: - -```json -"secrets": { - "api_key": { "required": true, "description": "API key for the service" }, - "webhook_url": { "required": false, "description": "Optional webhook endpoint" } -} ``` -**Env var naming is automatic** โ€” derived from plugin name + key: -- Plugin `my-plugin`, key `api_key` โ†’ env var `MY_PLUGIN_API_KEY` -- Convention: plugin name uppercased, hyphens โ†’ underscores, then `_KEY_UPPERCASE` +The following are forbidden in SDK v2 plugins: -Users can set secrets via: -- **Environment variable**: `MY_PLUGIN_API_KEY=sk-xxx` (Docker, CI) -- **WebUI**: Plugins โ†’ Manage Secrets -- **Telegram**: `/plugin set my-plugin api_key sk-xxx` +- `sdk.telegram.getRawClient()`; +- `context.bridge` or `ctx.bridge`; +- reading `wallet.json` or a mnemonic; +- undeclared secrets or environment-variable scraping; +- dependencies without a committed lockfile. -In code: `sdk.secrets.require("api_key")` (throws if missing) or `sdk.secrets.get("api_key")` (returns `undefined`). +See [CONTRIBUTING.md](CONTRIBUTING.md) for the complete contribution contract and the +[SDK README](https://github.com/TONresistor/teleton-agent/blob/dev/packages/sdk/README.md) for the +public API. -### npm dependencies +## Validate the complete catalog -Plugins can have their own npm packages beyond what Teleton provides (`@ton/core`, `@ton/ton`, `@ton/crypto`, `telegram`): +Node `22.22.2`, `24.15.0`, or a supported newer release is required. ```bash -cd plugins/your-plugin -npm init -y -npm install some-package -# Commit BOTH package.json AND package-lock.json (lockfile is required) +npm ci --ignore-scripts +npm run install:plugins +npm run validate +npm test +npm --prefix ../teleton-agent run build:sdk +npm run validate:runtime +npm run audit:plugins ``` -Teleton auto-installs deps at startup via `npm ci --ignore-scripts`. Use the dual-require pattern for CJS packages: +Runtime validation imports every plugin against the sibling `../teleton-agent` SDK checkout, +compares runtime tools with manifests, and rejects duplicate or malformed tools. CI runs the same +checks on Node 22 and Node 24. -```js -import { createRequire } from "node:module"; -import { realpathSync } from "node:fs"; - -const _require = createRequire(realpathSync(process.argv[1])); // core deps -const _pluginRequire = createRequire(import.meta.url); // plugin-local deps - -const { Address } = _require("@ton/core"); // from teleton runtime -const { getHttpEndpoint } = _pluginRequire("@orbs-network/ton-access"); // from plugin node_modules -``` - -### Test locally - -```bash -# Pattern A โ€” verify tools load -node -e "import('./plugins/your-plugin/index.js').then(m => console.log(m.tools.length, 'tools'))" - -# Pattern B โ€” verify tools is a function -node -e "import('./plugins/your-plugin/index.js').then(m => console.log(typeof m.tools === 'function' ? 'SDK plugin OK' : m.tools.length + ' tools'))" - -# Live test โ€” copy and restart -cp -r plugins/your-plugin ~/.teleton/plugins/ -``` - -Check the console output after restart: -``` -Plugin "my-plugin": 3 tools registered โ† success -Plugin "my-plugin": no 'tools' exported โ† missing export -Plugin "my-plugin" failed to load: ... โ† syntax error -``` - -### Submission checklist - -- [ ] Three files: `index.js`, `manifest.json`, `README.md` -- [ ] `manifest.json` has `id`, `name`, `version`, `description`, `author`, `tools` -- [ ] Tool names are `snake_case`, prefixed with plugin name (e.g. `myplugin_action`) -- [ ] `sdkVersion` declared in both manifests if using Pattern B -- [ ] Secrets declared with `required` + `description` (no `env` field needed โ€” auto-derived) -- [ ] All `fetch()` calls use `AbortSignal.timeout(15_000)` -- [ ] All `execute` functions have try/catch and return `{ success, data/error }` -- [ ] Error messages sliced to 500 chars: `String(err.message || err).slice(0, 500)` -- [ ] Tested locally (see above) -- [ ] Added to `registry.json` - -### Submit - -1. Fork this repo -2. Create `plugins/your-plugin/` with the three files -3. Add your plugin to `registry.json` -4. Open a PR - -Full guide โ€” manifest fields, context API, lifecycle hooks, best practices: **[CONTRIBUTING.md](CONTRIBUTING.md)** - -## Plugin SDK - -> **[@teleton-agent/sdk](https://github.com/TONresistor/teleton-agent/tree/main/packages/sdk)** โ€” full TypeScript types, interfaces, and API reference - -The SDK gives your plugin access to TON blockchain, Telegram messaging, inline bot mode, secrets, storage, and more โ€” without touching any internals. Export `tools` as a function to receive it: - -```js -export const tools = (sdk) => [{ execute: async (params, context) => { /* sdk.* available here */ } }]; -``` - -### Namespaces +## Security model -| Namespace | What it does | -|-----------|-------------| -| [`sdk.ton`](#sdkton--ton-blockchain) | Wallet, balance, send TON/jettons, NFTs, payment verification, jetton analytics | -| [`sdk.ton.dex`](#sdktondex--dex-aggregator) | STON.fi + DeDust โ€” quotes, swaps, auto-select best DEX | -| [`sdk.ton.dns`](#sdktondns--ton-domains) | .ton domain check, auctions, bids, link/unlink, ADNL site records | -| [`sdk.telegram`](#sdktelegram--telegram-messaging) | Messages, media, scheduling, moderation, polls, stars, gifts, collectibles, stories | -| [`sdk.bot`](#sdkbot--inline-bot-mode) | Inline queries, callback buttons, colored styled keyboards | -| [`sdk.db`](#sdkdb--isolated-database) | Isolated SQLite per plugin (requires `migrate()` export) | -| [`sdk.storage`](#sdkstorage--key-value-store) | Key-value store with TTL โ€” no `migrate()` needed | -| [`sdk.secrets`](#sdksecrets--secret-management) | 3-tier resolution: ENV โ†’ secrets store โ†’ pluginConfig | -| `sdk.log` | Prefixed logger โ€” `info()`, `warn()`, `error()`, `debug()` | -| `sdk.config` | Sanitized app config (no secrets) | -| `sdk.pluginConfig` | Plugin-specific config merged with `manifest.defaultConfig` | - -### `sdk.ton` โ€” TON Blockchain - -```js -// Wallet -const address = sdk.ton.getAddress(); // string | null -const balance = await sdk.ton.getBalance(address?); // { balance, balanceNano } | null -const price = await sdk.ton.getPrice(); // { usd, source, timestamp } | null -const valid = sdk.ton.validateAddress("EQx..."); // boolean - -// Transfers (throw PluginSDKError on failure) -await sdk.ton.sendTON("EQx...", 1.5, "memo"); // { txRef, amount } -await sdk.ton.sendJetton(jettonAddr, toAddr, amount); // { success, seqno } - -// Payment verification -const result = await sdk.ton.verifyPayment({ - amount: 1.0, memo: "order-42", maxAgeMinutes: 30 -}); // { verified, txHash?, amount?, playerWallet?, error? } - -// Jettons & NFTs -const jettons = await sdk.ton.getJettonBalances(); // JettonBalance[] -const info = await sdk.ton.getJettonInfo(jettonAddr); // JettonInfo | null -const wallet = await sdk.ton.getJettonWalletAddress(owner, jettonAddr); // string | null -const nfts = await sdk.ton.getNftItems(); // NftItem[] -const nft = await sdk.ton.getNftInfo(nftAddr); // NftItem | null -const txs = await sdk.ton.getTransactions(addr, 50); // TonTransaction[] - -// Jetton analytics -const price = await sdk.ton.getJettonPrice(jettonAddr); // { usd, ton, change24h, change7d, change30d } | null -const holders = await sdk.ton.getJettonHolders(addr, 100); // JettonHolder[] -const history = await sdk.ton.getJettonHistory(addr); // { volume24h, fdv, marketCap } | null - -// Utilities -const nano = sdk.ton.toNano(1.5); // bigint -const ton = sdk.ton.fromNano(1500000000n); // "1.5" -``` - -### `sdk.ton.dex` โ€” DEX Aggregator - -Compares STON.fi and DeDust to find the best rate. - -```js -// Get quotes from both DEXes -const quote = await sdk.ton.dex.quote({ - fromAsset: "ton", - toAsset: jettonAddress, - amount: 10, - slippage: 0.01, -}); // DexQuoteResult โ€” includes recommendation - -// Execute swap (auto-selects best DEX) -const swap = await sdk.ton.dex.swap({ - fromAsset: "ton", - toAsset: jettonAddress, - amount: 10, - slippage: 0.01, -}); // DexSwapResult - -// Or target a specific DEX -const stonQuote = await sdk.ton.dex.quoteSTONfi(params); -const dedustQuote = await sdk.ton.dex.quoteDeDust(params); -await sdk.ton.dex.swapSTONfi(params); -await sdk.ton.dex.swapDeDust(params); -``` - -### `sdk.ton.dns` โ€” .ton Domains - -```js -const domain = await sdk.ton.dns.check("mybot.ton"); // { available, owner?, auction? } -const resolved = await sdk.ton.dns.resolve("mybot.ton"); // { address } | null -const auctions = await sdk.ton.dns.getAuctions(10); // DnsAuction[] - -// Domain management (throw PluginSDKError on failure) -await sdk.ton.dns.startAuction("mybot.ton"); // ~0.06 TON min bid -await sdk.ton.dns.bid("mybot.ton", 5.0); -await sdk.ton.dns.link("mybot.ton", walletAddress); -await sdk.ton.dns.unlink("mybot.ton"); -await sdk.ton.dns.setSiteRecord("mybot.ton", adnlAddress); // TON Site ADNL record -``` - -### `sdk.telegram` โ€” Telegram Messaging - -**Messages:** - -```js -const msgId = await sdk.telegram.sendMessage(chatId, "Hello!", { - replyToId: 123, - inlineKeyboard: [[{ text: "Click me", callback_data: "action" }]], -}); -await sdk.telegram.editMessage(chatId, msgId, "Updated!"); -await sdk.telegram.deleteMessage(chatId, msgId); -await sdk.telegram.forwardMessage(fromChat, toChat, msgId); -await sdk.telegram.pinMessage(chatId, msgId); -``` - -**Scheduling:** - -```js -await sdk.telegram.scheduleMessage(chatId, "Reminder!", unixTimestamp); -const scheduled = await sdk.telegram.getScheduledMessages(chatId); -await sdk.telegram.sendScheduledNow(chatId, msgId); -await sdk.telegram.deleteScheduledMessage(chatId, msgId); -``` - -**Media:** - -```js -await sdk.telegram.sendPhoto(chatId, filePath, { caption: "Check this!" }); -await sdk.telegram.sendVideo(chatId, filePath); -await sdk.telegram.sendVoice(chatId, filePath); -await sdk.telegram.sendFile(chatId, filePath, { caption: "Report" }); -await sdk.telegram.sendGif(chatId, filePath); -await sdk.telegram.sendSticker(chatId, filePath); -const buffer = await sdk.telegram.downloadMedia(chatId, msgId); // Buffer | null -await sdk.telegram.setTyping(chatId); -``` - -**Search & history:** - -```js -const messages = await sdk.telegram.getMessages(chatId, 50); -const results = await sdk.telegram.searchMessages(chatId, "keyword", 20); -const replies = await sdk.telegram.getReplies(chatId, msgId, 20); -const dialogs = await sdk.telegram.getDialogs(100); -const history = await sdk.telegram.getHistory(chatId, 100); -``` - -**Social & chat info:** - -```js -const chat = await sdk.telegram.getChatInfo(chatId); -const user = await sdk.telegram.getUserInfo(userId); -const resolved = await sdk.telegram.resolveUsername("username"); -const members = await sdk.telegram.getParticipants(chatId, 200); -const me = await sdk.telegram.getMe(); -``` - -**Interactive:** - -```js -await sdk.telegram.sendDice(chatId, "๐ŸŽฒ"); -await sdk.telegram.sendReaction(chatId, msgId, "๐Ÿ‘"); -await sdk.telegram.createPoll(chatId, "Best DEX?", ["STON.fi", "DeDust"]); -await sdk.telegram.createQuiz(chatId, "1+1=?", ["1", "2", "3"], 1, "It's 2!"); -``` - -**Moderation:** - -```js -await sdk.telegram.banUser(chatId, userId); -await sdk.telegram.unbanUser(chatId, userId); -await sdk.telegram.muteUser(chatId, userId, untilDate); // Unix timestamp, 0 = forever -await sdk.telegram.kickUser(chatId, userId); // ban + immediate unban -``` - -**Stars & gifts:** - -```js -const stars = await sdk.telegram.getStarsBalance(); -const gifts = await sdk.telegram.getAvailableGifts(); -const myGifts = await sdk.telegram.getMyGifts(50); -await sdk.telegram.sendGift(userId, giftId, { message: "For you!" }); -const resale = await sdk.telegram.getResaleGifts(giftId, 10); -await sdk.telegram.buyResaleGift(giftId); -const txs = await sdk.telegram.getStarsTransactions(50); -``` - -**Collectibles:** - -```js -await sdk.telegram.transferCollectible(msgId, toUserId); -await sdk.telegram.setCollectiblePrice(msgId, 100); // 0 to unlist -const info = await sdk.telegram.getCollectibleInfo(slug); -const unique = await sdk.telegram.getUniqueGift(slug); -const value = await sdk.telegram.getUniqueGiftValue(slug); -await sdk.telegram.sendGiftOffer(userId, giftSlug, price); -``` - -**Stories & raw client:** - -```js -await sdk.telegram.sendStory(mediaPath, { caption: "New!", pinned: true }); -const client = sdk.telegram.getRawClient(); // GramJS TelegramClient | null -``` - -### `sdk.bot` โ€” Inline Bot Mode - -Enables `@botname query` inline queries and callback button handling. Requires `bot` in manifest: - -```js -export const manifest = { - name: "my-bot", - version: "1.0.0", - sdkVersion: ">=1.0.0", - bot: { - inline: true, - callbacks: true, - rateLimits: { inlinePerMinute: 30, callbackPerMinute: 60 }, // optional - }, -}; - -export const tools = (sdk) => { - // Handle inline queries โ€” user types @botname - sdk.bot.onInlineQuery(async (ctx) => { - return [{ - id: "1", - type: "article", - title: `Result: ${ctx.query}`, - description: "Tap to send", - content: { text: `You searched: ${ctx.query}` }, - replyMarkup: sdk.bot.keyboard([ - [{ text: "โœ“ Yes", callback: "pick:yes", style: "success" }], // green - [{ text: "โœ— No", callback: "pick:no", style: "danger" }], // red - ]).toTL(), // .toTL() = GramJS colored buttons, .toGrammy() = Bot API - }]; - }); - - // Handle button presses โ€” glob pattern matching - sdk.bot.onCallback("pick:*", async (ctx) => { - await ctx.answer("Selected!"); // toast notification - await ctx.editMessage("Choice made."); // update the message - }); - - // Track which inline results users select - sdk.bot.onChosenResult(async (ctx) => { - sdk.log.info(`User ${ctx.userId} chose ${ctx.resultId}`); - }); - - return [/* regular tools alongside inline mode */]; -}; -``` - -Button styles: `"success"` (green), `"danger"` (red), `"primary"` (blue) โ€” colored via GramJS Layer 222, graceful fallback on Bot API. - -Properties: `sdk.bot.isAvailable` (boolean), `sdk.bot.username` (string). - -> **Important:** `sdk.bot` is `null` unless the manifest declares `bot` capabilities. - -### `sdk.db` โ€” Isolated Database - -Each plugin gets its own SQLite database. Export `migrate()` to enable it: - -```js -export function migrate(db) { - db.exec(`CREATE TABLE IF NOT EXISTS scores ( - user_id TEXT PRIMARY KEY, - points INTEGER NOT NULL DEFAULT 0 - )`); -} - -// sdk.db is a full better-sqlite3 instance -sdk.db.prepare("INSERT INTO scores ...").run(userId); -const row = sdk.db.prepare("SELECT * FROM scores WHERE user_id = ?").get(userId); -``` - -If you don't export `migrate`, `sdk.db` is `null`. - -### `sdk.storage` โ€” Key-Value Store - -No setup needed. Supports TTL (auto-expiry): - -```js -sdk.storage.set("price", 42.5, { ttl: 3_600_000 }); // expires in 1 hour -const val = sdk.storage.get("price"); // 42.5 or undefined if expired -sdk.storage.has("price"); // boolean -sdk.storage.delete("price"); // boolean -sdk.storage.clear(); // delete all keys -``` - -### `sdk.secrets` โ€” Secret Management - -3-tier resolution: **ENV variable** โ†’ **secrets store** โ†’ **pluginConfig fallback**. - -```js -const key = sdk.secrets.require("api_key"); // throws SECRET_NOT_FOUND if missing -const opt = sdk.secrets.get("webhook_url"); // undefined if not set -sdk.secrets.has("premium_key"); // boolean -``` - -Declare in `manifest.json` for validation at load time: - -```json -{ "secrets": { "api_key": { "required": true, "description": "API key for the service" } } } -``` - -### Plugin Lifecycle - -| Export | When | Purpose | -|--------|------|---------| -| `manifest` | Load time | Plugin metadata, `defaultConfig`, `sdkVersion`, `bot` config | -| `migrate(db)` | Before tools | Database schema setup (enables `sdk.db`) | -| `tools` / `tools(sdk)` | After migrate | Tool definitions | -| `start(ctx)` | After Telegram connects | Background tasks, intervals, initialization | -| `stop()` | On shutdown | Cleanup timers, connections | - -### Error Handling - -**Read methods** (`getBalance`, `getMessages`, etc.) return `null` or `[]` โ€” never throw. - -**Write methods** (`sendTON`, `sendMessage`, `banUser`, etc.) throw `PluginSDKError` with `.code`: - -| Code | Meaning | -|------|---------| -| `WALLET_NOT_INITIALIZED` | Wallet not set up | -| `INVALID_ADDRESS` | Bad TON address | -| `BRIDGE_NOT_CONNECTED` | Telegram not ready | -| `SECRET_NOT_FOUND` | `sdk.secrets.require()` failed | -| `OPERATION_FAILED` | Generic failure | - -```js -try { - await sdk.ton.sendTON(address, 1.0); -} catch (err) { - if (err.name === "PluginSDKError") { - return { success: false, error: `${err.code}: ${err.message}` }; - } - return { success: false, error: String(err.message || err).slice(0, 500) }; -} -``` - -> Complete SDK reference with TypeScript types and interfaces: **[@teleton-agent/sdk](https://github.com/TONresistor/teleton-agent/tree/main/packages/sdk)**
-> Contribution guide with best practices and testing: **[CONTRIBUTING.md](CONTRIBUTING.md)** - -## Troubleshooting - -**Plugin not loading?** - -- Check that `manifest.json` exists and has valid JSON -- Verify the plugin exports `tools` (array or function): `node -e "import('./plugins/name/index.js').then(m => console.log(m.tools))"` -- Look for errors in the Teleton console output at startup -- Make sure the plugin folder name matches the `id` in `manifest.json` - -**Common errors:** - -| Error | Cause | Fix | -|-------|-------|-----| -| `Cannot find module` | Missing dependency | Add a `package.json` โ€” deps are auto-installed at startup | -| `tools is not iterable` | `tools` export is not an array or function | Check your export: `export const tools = [...]` or `export const tools = (sdk) => [...]` | -| `Plugin name collision` | Two plugins share the same `id` | Rename one of the plugins in its `manifest.json` | -| `SDK not available` | Using `sdk.*` without the SDK pattern | Switch to Pattern B: `export const tools = (sdk) => [...]` | - -## FAQ - -**Can I use npm packages?** -Yes. Add a `package.json` (and `package-lock.json`) to your plugin folder. Teleton auto-installs dependencies at startup. - -**How do I store data?** -Use `sdk.db` for SQL (requires exporting a `migrate(db)` function) or `sdk.storage` for simple key-value pairs with optional TTL. - -**How do I access TON or Telegram?** -Use the SDK (Pattern B): `export const tools = (sdk) => [...]`. Then call `sdk.ton.*` for wallet/blockchain operations and `sdk.telegram.*` for messaging. - -**How do I manage API keys?** -Declare them in `manifest.json` with the `env` field so users know exactly what to set. In your code, use `sdk.secrets.require("key_name")`. Secrets resolve in order: environment variable โ†’ secrets store (`/plugin set`) โ†’ `pluginConfig` (config.yaml). - -**Why is my plugin not showing tools?** -Make sure your `tools` export is either an array of tool objects or a function that returns one. Each tool needs at least `name`, `description`, and `execute`. - -## Community - -- **[Telegram Group](https://t.me/ResistanceForum)**: questions, plugin ideas, support -- **[GitHub Issues](https://github.com/TONresistor/teleton-plugins/issues)**: bug reports, feature requests -- **[Contributing Guide](CONTRIBUTING.md)**: how to build and submit plugins - -## Contributors - -This project exists thanks to everyone who contributes. - - - - - -Want to see your name here? Check out the [Contributing Guide](CONTRIBUTING.md). +- Plugins receive sanitized configuration and an isolated database. +- Secrets must be declared and accessed through `sdk.secrets`. +- Marketplace actions are treated as external actions by Teleton and require explicit approval. +- SDK v2 capability boundaries are mandatory; manifest permissions cannot restore removed raw APIs. +- npm lifecycle scripts are disabled when plugin dependencies are installed. +- HIGH and CRITICAL dependency vulnerabilities fail CI. ## License -[MIT](LICENSE) โ€” use it, fork it, build on it. - ---- - -
- -**[teleton-plugins](https://github.com/TONresistor/teleton-plugins)** โ€” open source plugins for the TON ecosystem - -[Report Bug](https://github.com/TONresistor/teleton-plugins/issues) ยท [Request Plugin](https://github.com/TONresistor/teleton-plugins/issues/new) ยท [Contributing Guide](CONTRIBUTING.md) - -
+[MIT](LICENSE) diff --git a/SKILL.md b/SKILL.md index 01f2c1c..746d5a2 100644 --- a/SKILL.md +++ b/SKILL.md @@ -1,898 +1,127 @@ -# Teleton Plugin Builder - -You are building a plugin for **Teleton**, a Telegram AI agent on TON. Ask the user what plugin or tools they want to build, then follow this workflow. - -## Reference documentation - -Before building, read the relevant reference files from the teleton-plugins repo: - -- **Full rules & SDK reference**: `CONTRIBUTING.md` โ€” complete guide with tool definition, SDK API tables, error handling, lifecycle, best practices, testing -- **Simple plugin example**: `plugins/example/index.js` โ€” Pattern A (array of tools, no SDK) -- **SDK plugin example**: `plugins/example-sdk/index.js` โ€” Pattern B (tools(sdk) with database, TON balance, Telegram messaging) -- **Advanced SDK plugin**: `plugins/casino/index.js` โ€” real-world SDK plugin with TON payments, payment verification, isolated database, payout logic -- **Registry**: `registry.json` โ€” list of all existing plugins (check for name conflicts) -- **README.md** โ€” project overview, plugin list, SDK section - -Read at least `CONTRIBUTING.md` and the relevant example before building. - ---- - -## Workflow - -1. **Ask** the user what they want (plugin name, what it does, which API or bot) -2. **Decide** โ€” determine if the plugin needs the SDK (see decision tree below) -3. **Plan** โ€” present a structured plan and ask for validation -4. **Build** โ€” create all files once the user approves -5. **Install** โ€” copy to `~/.teleton/plugins/` and restart - ---- - -## Step 1 โ€” Understand the request - -Determine: - -- **Plugin name** โ€” short, lowercase folder name (e.g. `pic`, `deezer`, `weather`) -- **Plugin type**: - - **Inline bot** โ€” wraps a Telegram inline bot (@pic, @vid, @gif, @DeezerMusicBotโ€ฆ) - - **Public API** โ€” calls an external REST API, no auth - - **Auth API** โ€” external API with Telegram WebApp auth - - **Local logic** โ€” pure JavaScript, no external calls -- **Tools** โ€” list of tool names, what each does, parameters -- **Does it need GramJS?** โ€” yes for inline bots and WebApp auth -- **Does it need the SDK?** โ€” use the decision tree below - --- - -## SDK Decision Tree - -The Plugin SDK (`tools(sdk)`) gives high-level access to TON, Telegram, database, logging, and config. Use it **only when needed** โ€” simpler plugins should use Pattern A. - -**Use `tools(sdk)` (Pattern B) if ANY of these apply:** - -| Need | SDK namespace | Example | -|------|--------------|---------| -| Check TON balance or wallet address | `sdk.ton.getBalance()`, `sdk.ton.getAddress()` | Casino checking balance before payout | -| Send TON or verify payments | `sdk.ton.sendTON()`, `sdk.ton.verifyPayment()` | Casino auto-payout, paid services | -| Get TON price or transactions | `sdk.ton.getPrice()`, `sdk.ton.getTransactions()` | Portfolio tracker | -| Query jetton balances or metadata | `sdk.ton.getJettonBalances()`, `sdk.ton.getJettonInfo()` | Token portfolio, DEX tools | -| Send jettons (TEP-74 transfers) | `sdk.ton.sendJetton()` | Token payments, swaps | -| Query NFT items or metadata | `sdk.ton.getNftItems()`, `sdk.ton.getNftInfo()` | NFT gallery, collection tools | -| Convert TON units or validate addresses | `sdk.ton.toNano()`, `sdk.ton.fromNano()`, `sdk.ton.validateAddress()` | Any TON plugin | -| Send Telegram messages programmatically | `sdk.telegram.sendMessage()` | Announcements, notifications | -| Edit messages or send reactions | `sdk.telegram.editMessage()`, `sdk.telegram.sendReaction()` | Interactive UIs | -| Send dice/slot animations | `sdk.telegram.sendDice()` | Casino dice game | -| Send media (photos, videos, files, stickers) | `sdk.telegram.sendPhoto()`, `sdk.telegram.sendFile()` | Media bots, file sharing | -| Delete, forward, pin, or search messages | `sdk.telegram.deleteMessage()`, `sdk.telegram.pinMessage()` | Moderation, archival | -| Schedule messages | `sdk.telegram.scheduleMessage()` | Reminders, timed announcements | -| Create polls or quizzes | `sdk.telegram.createPoll()`, `sdk.telegram.createQuiz()` | Engagement, trivia bots | -| Moderate users (ban/mute/unban) | `sdk.telegram.banUser()`, `sdk.telegram.muteUser()` | Group moderation | -| Stars & gifts (balance, send, buy) | `sdk.telegram.getStarsBalance()`, `sdk.telegram.sendGift()` | Gift trading, rewards | -| Post stories | `sdk.telegram.sendStory()` | Promotional content | -| Lookup users, chats, or participants | `sdk.telegram.getUserInfo()`, `sdk.telegram.getChatInfo()` | Analytics, admin tools | -| Need an isolated database | `sdk.db` (requires `export function migrate(db)`) | Tracking user scores, history, state | -| Key-value storage with TTL | `sdk.storage` (auto-created, no migrate needed) | Caching, rate limits, temp state | -| Manage API keys or secrets | `sdk.secrets` | Plugins that call authenticated APIs | -| Plugin-specific config with defaults | `sdk.pluginConfig` + `manifest.defaultConfig` | Customizable thresholds, modes | -| Structured logging | `sdk.log.info()`, `sdk.log.error()` | Debug, monitoring | -| Swap tokens on DEX | `sdk.ton.dex.quote()`, `sdk.ton.dex.swap()` | DEX aggregator, trading bots | -| Compare DEX prices (STON.fi vs DeDust) | `sdk.ton.dex.quoteSTONfi()`, `sdk.ton.dex.quoteDeDust()` | Arbitrage, price comparison | -| Check/register .ton domains | `sdk.ton.dns.check()`, `sdk.ton.dns.resolve()` | Domain tools, DNS management | -| Auction .ton domains | `sdk.ton.dns.startAuction()`, `sdk.ton.dns.bid()` | Domain marketplace | -| Link domain to wallet or TON Site | `sdk.ton.dns.link()`, `sdk.ton.dns.setSiteRecord()` | Domain configuration | -| Jetton analytics (price, holders, history) | `sdk.ton.getJettonPrice()`, `sdk.ton.getJettonHolders()` | Token analytics, dashboards | -| Manage scheduled messages | `sdk.telegram.getScheduledMessages()`, `sdk.telegram.sendScheduledNow()` | Schedulers, reminders | -| Browse conversations or message history | `sdk.telegram.getDialogs()`, `sdk.telegram.getHistory()` | Analytics, search tools | -| Collectibles & unique gifts | `sdk.telegram.transferCollectible()`, `sdk.telegram.getUniqueGift()` | NFT gift trading, valuation | -| Inline bot features (queries, buttons) | `sdk.bot.onInlineQuery()`, `sdk.bot.keyboard()` | Inline bots, interactive UIs | -| Observe agent events (hooks) | `sdk.on("tool:after", handler)` | Audit, logging, enrichment | - -**Use `tools = [...]` (Pattern A) if ALL of these apply:** - -- Only calls external APIs (REST, GraphQL) โ€” no TON blockchain interaction -- Does not need to send Telegram messages from code (only returns data to LLM) -- Does not need persistent state (no database) -- Does not need plugin-specific config - -**Examples:** - -| Plugin | Pattern | Why | -|--------|---------|-----| -| `weather` | A (array) | Calls Open-Meteo API, returns data | -| `gaspump` | A (array) | Calls Gas111 API, uses WebApp auth | -| `pic` | B (SDK) | Uses `sdk.telegram.getRawClient()` for inline bot | -| `casino` | B (SDK) | Needs sdk.ton (payments), sdk.telegram (dice), sdk.db (history) | -| `example-sdk` | B (SDK) | Needs sdk.db (counters), sdk.ton (balance), sdk.telegram (messages) | - -**Note:** Inline bots and WebApp auth plugins can use either `context.bridge.getClient().getClient()` (Pattern A with `permissions: ["bridge"]`) or `sdk.telegram.getRawClient()` (Pattern B, preferred). - +name: teleton-plugin-builder +description: Build and maintain Teleton plugins that comply with the SDK v2 capability and marketplace policy. --- -## Step 2 โ€” Present the plan - -Show this to the user and **wait for approval**: +# Teleton Plugin Builder -``` -Plugin: [name] -Pattern: [A (simple) | B (SDK)] -Reason: [why SDK is/isn't needed] +Use this workflow when creating or updating a plugin in this repository. -Tools: -| Tool | Description | Params | -|-------------|--------------------------|-------------------------------------| -| `tool_name` | What it does | `query` (string, required), `index` (int, optional) | +## Read first -SDK features used: [none | sdk.ton, sdk.db, sdk.telegram, sdk.log, sdk.pluginConfig] +Before editing, read: -Files: -- plugins/[name]/index.js -- plugins/[name]/manifest.json -- plugins/[name]/README.md -- registry.json (update) -``` +1. [`CONTRIBUTING.md`](CONTRIBUTING.md) for the normative contract. +2. [`compatibility.json`](compatibility.json) for the current SDK policy. +3. [`plugins/example/index.js`](plugins/example/index.js) for a static tool. +4. [`plugins/example-sdk/index.js`](plugins/example-sdk/index.js) for an SDK v2 plugin. +5. [`references/patterns.md`](references/patterns.md) for safe implementation patterns. -Do NOT build until the user says go. +The public API is defined by `@teleton-agent/sdk@^2`. Do not infer APIs from old plugins. ---- - -## Step 3 โ€” Build - -Create all files in `plugins/[name]/` following the patterns below. +## Workflow -### index.js +1. Inspect the requested plugin, related manifests and existing tool names. +2. Choose a static `tools` array or `tools(sdk)`. +3. State the proposed tools, parameters, scopes, categories, secrets and side effects. +4. Implement `index.js`, `manifest.json` and `README.md`. +5. Add a `package.json` and lockfile only when external dependencies are necessary. +6. Add or update the plugin's `compatibility.json` entry. +7. Run `npm run generate`; supported production-ready plugins enter `registry.json` automatically. +8. Run the full repository validation commands. -**ESM only** โ€” always `export const tools`, never `module.exports`. +## Choose the plugin pattern -Choose the right pattern: +Use a static `tools` array when the plugin only performs local computation or calls an external API +without Teleton state, Telegram actions, wallet operations, secrets or persistence. ---- +Use `tools(sdk)` when the plugin needs any public SDK capability: -#### Pattern A: Simple tools (array) +- `sdk.telegram`; +- `sdk.ton`; +- `sdk.secrets`; +- `sdk.storage` or `sdk.db`; +- `sdk.pluginConfig`; +- `sdk.log`; +- lifecycle or bot capabilities declared by the SDK. -For plugins that don't need TON, Telegram messaging, or persistent database. +## Mandatory SDK v2 rules -Reference: `plugins/example/index.js` +- SDK plugins declare `sdkVersion: "^2.0.0"` in both runtime and disk manifests. +- Every tool declares `scope` and `category`. +- Action tools are safe under retry and compatible with explicit owner approval. +- Secrets are declared and accessed through `sdk.secrets`. +- State is stored only through the isolated plugin database or storage SDK. +- External requests have timeouts and bounded inputs and outputs. +- Tool errors never expose credentials, wallet data or unbounded upstream payloads. -```javascript -const myTool = { - name: "tool_name", - description: "The LLM reads this to decide when to call the tool. Be specific.", - parameters: { - type: "object", - properties: { - query: { type: "string", description: "Search query" }, - index: { type: "integer", description: "Which result (0 = first)", minimum: 0, maximum: 49 }, - }, - required: ["query"], - }, - execute: async (params, context) => { - try { - // logic here - return { success: true, data: { result: "..." } }; - } catch (err) { - return { success: false, error: String(err.message || err).slice(0, 500) }; - } - }, -}; +Never use: -export const tools = [myTool]; -``` +- `sdk.telegram.getRawClient()`; +- `context.bridge` or `ctx.bridge`; +- direct GramJS access through Teleton internals; +- `wallet.json`, mnemonic reads or direct signing; +- private Teleton modules as an undeclared API; +- global environment variables for secrets. ---- +If the SDK lacks a required capability, do not create an escape hatch. Preserve the plugin as +`quarantined`, set its range to `^1.0.0`, explain the blocker in `compatibility.json`, and keep it out +of the generated `registry.json`. -#### Pattern B: SDK tools (function) +## Minimal SDK v2 template -For plugins that need TON blockchain, Telegram messaging, isolated database, or config. - -Reference: `plugins/example-sdk/index.js` (basic), `plugins/casino/index.js` (advanced) - -```javascript +```js export const manifest = { name: "my-plugin", version: "1.0.0", - sdkVersion: ">=1.0.0", - description: "What this plugin does", - defaultConfig: { - some_setting: "default_value", - }, + sdkVersion: "^2.0.0", + description: "Describe the capability", }; -// Optional: export migrate() to get sdk.db (isolated SQLite per plugin) -export function migrate(db) { - db.exec(`CREATE TABLE IF NOT EXISTS my_table ( - id TEXT PRIMARY KEY, - value TEXT - )`); -} - export const tools = (sdk) => [ { - name: "my_tool", - description: "What this tool does", - parameters: { type: "object", properties: {}, }, - scope: "always", // "always" | "dm-only" | "group-only" | "admin-only" - category: "data-bearing", // "data-bearing" (reads/queries) | "action" (writes/modifies) - execute: async (params, context) => { + name: "my_plugin_lookup", + description: "Look up a value without changing external state", + scope: "always", + category: "data-bearing", + parameters: { + type: "object", + properties: { + query: { type: "string", minLength: 1, maxLength: 200 }, + }, + required: ["query"], + }, + async execute(params) { try { - // SDK namespaces: - // sdk.ton โ€” getAddress(), getBalance(), getPrice(), sendTON(), getTransactions(), verifyPayment() - // getPublicKey(), getWalletVersion(), createJettonTransfer() - // getJettonBalances(), getJettonInfo(), sendJetton(), getJettonWalletAddress() - // getNftItems(), getNftInfo(), toNano(), fromNano(), validateAddress() - // sdk.telegram โ€” sendMessage(), editMessage(), deleteMessage(), forwardMessage(), pinMessage() - // sendDice(), sendReaction(), getMessages(), searchMessages(), getReplies() - // sendPhoto(), sendVideo(), sendVoice(), sendFile(), sendGif(), sendSticker() - // downloadMedia(), setTyping(), getChatInfo(), getUserInfo(), resolveUsername() - // getParticipants(), createPoll(), createQuiz(), banUser(), unbanUser(), muteUser() - // getStarsBalance(), sendGift(), getAvailableGifts(), getMyGifts(), getResaleGifts() - // buyResaleGift(), sendStory(), scheduleMessage(), getMe(), isAvailable(), getRawClient() - // sdk.db โ€” better-sqlite3 instance (null if no migrate()) - // sdk.storage โ€” get(), set(key, val, {ttl?}), delete(), has(), clear() โ€” auto-created KV, no migrate needed - // sdk.secrets โ€” get(key), require(key), has(key) โ€” resolves ENV โ†’ secrets file โ†’ pluginConfig - // sdk.log โ€” info(), warn(), error(), debug() - // sdk.config โ€” sanitized app config (no secrets) - // sdk.pluginConfig โ€” plugin-specific config from config.yaml merged with defaultConfig - // sdk.bot โ€” isAvailable, username, onInlineQuery(), onCallback(), onChosenResult(), - // editInlineMessage(), keyboard() โ€” null if no bot manifest - // sdk.on โ€” on(hookName, handler, opts?) โ€” 13 hooks: tool:before/after/error, - // prompt:before/after, session:start/end, message:receive, etc. - - const balance = await sdk.ton.getBalance(); - sdk.log.info(`Balance: ${balance?.balance}`); - return { success: true, data: { balance: balance?.balance } }; - } catch (err) { - return { success: false, error: String(err.message || err).slice(0, 500) }; + const response = await fetch( + `https://api.example.com/search?q=${encodeURIComponent(params.query)}`, + { signal: AbortSignal.timeout(15_000) } + ); + if (!response.ok) return { success: false, error: `Upstream returned ${response.status}` }; + return { success: true, data: await response.json() }; + } catch (error) { + sdk.log.warn("Lookup failed"); + return { success: false, error: String(error?.message ?? error).slice(0, 500) }; } }, }, ]; - -// Optional: runs after Telegram bridge connects -export async function start(ctx) { - // ctx.bridge, ctx.db, ctx.config, ctx.pluginConfig, ctx.log -} - -// Optional: runs on shutdown -export async function stop() { - // cleanup -} -``` - -**SDK error handling:** -- Read methods (`getBalance`, `getPrice`, `getTransactions`, `getMessages`, `getJettonBalances`, `getNftItems`) return `null` or `[]` on failure โ€” never throw -- Write methods (`sendTON`, `sendJetton`, `createJettonTransfer`, `sendMessage`, `sendDice`, `sendPhoto`, `banUser`) throw `PluginSDKError` with `.code`: - - `WALLET_NOT_INITIALIZED` โ€” wallet not set up - - `INVALID_ADDRESS` โ€” bad TON address - - `BRIDGE_NOT_CONNECTED` โ€” Telegram not ready - - `SECRET_NOT_FOUND` โ€” `sdk.secrets.require()` called for missing secret - - `OPERATION_FAILED` โ€” generic failure - ---- - -#### GramJS import (only if plugin needs raw Telegram MTProto) - -```javascript -import { createRequire } from "node:module"; -import { realpathSync } from "node:fs"; - -const _require = createRequire(realpathSync(process.argv[1])); -const { Api } = _require("telegram"); -``` - -#### Per-plugin npm dependencies - -Plugins can have their own npm deps. Add `package.json` + `package-lock.json` to the plugin folder โ€” teleton auto-installs at startup via `npm ci --ignore-scripts`. - -Use two `createRequire` instances to separate core and plugin-local deps: - -```javascript -import { createRequire } from "node:module"; -import { realpathSync } from "node:fs"; - -// Core deps (from teleton runtime) -const _require = createRequire(realpathSync(process.argv[1])); -// Plugin-local deps (from plugin's node_modules/) -const _pluginRequire = createRequire(import.meta.url); - -const { Address } = _require("@ton/core"); // core -const { getHttpEndpoint } = _pluginRequire("@orbs-network/ton-access"); // plugin-local -``` - -Setup: `cd plugins/your-plugin && npm init -y && npm install `. Commit both `package.json` and `package-lock.json`. The `node_modules/` folder is auto-created at startup. - -With SDK plugins, prefer `sdk.telegram.getRawClient()` over `context.bridge.getClient().getClient()`. - -#### API fetch helper (for plugins calling external APIs) - -```javascript -const API_BASE = "https://api.example.com"; - -async function apiFetch(path, params = {}) { - const url = new URL(path, API_BASE); - for (const [key, value] of Object.entries(params)) { - if (value !== undefined && value !== null) { - url.searchParams.set(key, String(value)); - } - } - const res = await fetch(url, { signal: AbortSignal.timeout(15000) }); - if (!res.ok) { - throw new Error(`API error: ${res.status} ${await res.text().catch(() => "")}`); - } - return res.json(); -} -``` - -#### Inline bot pattern (@pic, @vid, @gif, @DeezerMusicBotโ€ฆ) - -**Preferred (SDK Pattern B)** โ€” define tools inside the `(sdk) => [...]` closure: - -```javascript -export const tools = (sdk) => [{ - name: "my_inline_bot", - description: "...", - parameters: { ... }, - execute: async (params, context) => { - const client = sdk.telegram.getRawClient(); - // ... same GramJS API calls - }, -}]; -``` - -**Legacy (Pattern A)** โ€” uses `context.bridge` directly: - -```javascript -execute: async (params, context) => { - try { - const client = context.bridge.getClient().getClient(); - const bot = await client.getEntity("BOT_USERNAME"); - const peer = await client.getInputEntity(context.chatId); - - const results = await client.invoke( - new Api.messages.GetInlineBotResults({ - bot, peer, query: params.query, offset: "", - }) - ); - - if (!results.results || results.results.length === 0) { - return { success: false, error: `No results found for "${params.query}"` }; - } - - const index = params.index ?? 0; - if (index >= results.results.length) { - return { success: false, error: `Only ${results.results.length} results, index ${index} out of range` }; - } - - const chosen = results.results[index]; - - await client.invoke( - new Api.messages.SendInlineBotResult({ - peer, - queryId: results.queryId, - id: chosen.id, - randomId: BigInt(Math.floor(Math.random() * 2 ** 62)), - }) - ); - - return { - success: true, - data: { - query: params.query, - sent_index: index, - total_results: results.results.length, - title: chosen.title || null, - description: chosen.description || null, - type: chosen.type || null, - }, - }; - } catch (err) { - return { success: false, error: String(err.message || err).slice(0, 500) }; - } -} -``` - -#### WebApp auth pattern (Telegram-authenticated APIs) - -```javascript -let cachedAuth = null; -let cachedAuthTime = 0; -const AUTH_TTL = 30 * 60 * 1000; - -async function getAuth(bridge, botUsername, webAppUrl) { - if (cachedAuth && Date.now() - cachedAuthTime < AUTH_TTL) return cachedAuth; - const client = bridge.getClient().getClient(); - const bot = await client.getEntity(botUsername); - const result = await client.invoke( - new Api.messages.RequestWebView({ peer: bot, bot, platform: "android", url: webAppUrl }) - ); - const fragment = new URL(result.url).hash.slice(1); - const initData = new URLSearchParams(fragment).get("tgWebAppData"); - if (!initData) throw new Error("Failed to extract tgWebAppData"); - cachedAuth = initData; - cachedAuthTime = Date.now(); - return cachedAuth; -} -``` - -#### Payment verification pattern (SDK) - -Reference: `plugins/casino/index.js` - -```javascript -export function migrate(db) { - db.exec(`CREATE TABLE IF NOT EXISTS used_transactions ( - tx_hash TEXT PRIMARY KEY, - user_id TEXT NOT NULL, - amount REAL NOT NULL, - game_type TEXT NOT NULL, - used_at INTEGER NOT NULL DEFAULT (unixepoch()) - )`); -} - -export const tools = (sdk) => [{ - name: "verify_and_process", - execute: async (params, context) => { - const payment = await sdk.ton.verifyPayment({ - amount: params.amount, - memo: params.username, - gameType: "my_service", - maxAgeMinutes: 10, - }); - - if (!payment.verified) { - const address = sdk.ton.getAddress(); - return { success: false, error: `Send ${params.amount} TON to ${address} with memo: ${params.username}` }; - } - - // Process the verified payment... - // payment.playerWallet โ€” sender's address (for refunds/payouts) - // payment.compositeKey โ€” unique tx identifier - // payment.amount โ€” verified amount - - return { success: true, data: { verified: true, from: payment.playerWallet } }; - } -}]; -``` - -### manifest.json - -```json -{ - "id": "PLUGIN_ID", - "name": "Display Name", - "version": "1.0.0", - "description": "One-line description", - "author": { "name": "teleton", "url": "https://github.com/TONresistor" }, - "license": "MIT", - "entry": "index.js", - "teleton": ">=1.0.0", - "tools": [ - { "name": "tool_name", "description": "Short description" } - ], - "permissions": [], - "tags": ["tag1", "tag2"], - "repository": "https://github.com/TONresistor/teleton-plugins", - "funding": null -} -``` - -Notes: -- Add `"sdkVersion": ">=1.0.0"` **only** if using `tools(sdk)` (Pattern B) -- Add `"permissions": ["bridge"]` only if using `context.bridge` directly (not needed with SDK) -- `permissions` is `[]` for most plugins -- Add `"secrets"` to declare required/optional secrets (see Secrets section below) - -#### Declaring secrets in manifest - -If your plugin needs API keys or other secrets, declare them in the manifest: - -```json -{ - "secrets": { - "api_key": { "required": true, "description": "API key for service X" }, - "webhook_url": { "required": false, "description": "Optional webhook endpoint" } - } -} -``` - -Then access them via `sdk.secrets.get("api_key")` or `sdk.secrets.require("api_key")` in your tools. - -### README.md - -```markdown -# Plugin Name - -One-line description. - -| Tool | Description | -|------|-------------| -| `tool_name` | What it does | - -## Install - -mkdir -p ~/.teleton/plugins -cp -r plugins/PLUGIN_ID ~/.teleton/plugins/ - -## Usage examples - -- "Natural language prompt the user would say" -- "Another example prompt" - -## Tool schema - -### tool_name - -| Param | Type | Required | Default | Description | -|-------|------|----------|---------|-------------| -| `query` | string | Yes | โ€” | Search query | -``` - -### registry.json - -Add to the `plugins` array: - -```json -{ - "id": "PLUGIN_ID", - "name": "Display Name", - "description": "One-line description", - "author": "teleton", - "tags": ["tag1", "tag2"], - "path": "plugins/PLUGIN_ID" -} -``` - ---- - -## Step 4 โ€” Install and commit - -1. Copy: `cp -r plugins/PLUGIN_ID ~/.teleton/plugins/` -2. Commit: `git add plugins/PLUGIN_ID/ registry.json && git commit -m "PLUGIN_NAME: short description"` -3. Ask user if they want to push. - ---- - -## Rules - -- **ESM only** โ€” `export const tools`, never `module.exports` -- **JS only** โ€” the plugin loader reads `.js` files only -- **Tool names** โ€” `snake_case`, globally unique across all plugins, prefixed with plugin name -- **Defaults** โ€” use `??` (nullish coalescing), never `||` -- **Errors** โ€” always try/catch in execute, return `{ success: false, error }`, slice to 500 chars -- **Timeouts** โ€” `AbortSignal.timeout(15000)` on all external fetch calls -- **Per-plugin npm deps** โ€” plugins needing external packages add `package.json` + `package-lock.json`; teleton auto-installs at startup. Use the dual-require pattern (see below) -- **GramJS** โ€” always `createRequire(realpathSync(process.argv[1]))`, never `import from "telegram"` -- **Client chain** โ€” `context.bridge.getClient().getClient()` OR `sdk.telegram.getRawClient()` for raw GramJS -- **SDK preferred** โ€” when using SDK, prefer `sdk.telegram` over `context.bridge`, `sdk.db` over `context.db` -- **Scope** โ€” add `scope: "dm-only"` on financial/private tools, `scope: "group-only"` on moderation tools, `scope: "admin-only"` for admin-only tools -- **Category** โ€” add `category: "data-bearing"` for read/query tools, `category: "action"` for write/modify tools -- **Secrets** โ€” use `sdk.secrets` instead of hardcoded keys; declare in `manifest.secrets` -- **Storage** โ€” prefer `sdk.storage` for caching and temp state over `sdk.db` (no `migrate()` needed) -- **SDK decision** โ€” only use Pattern B if the plugin actually needs TON, Telegram messaging, database, secrets, storage, or config (see decision tree) - -## Context object - -Available in `execute(params, context)` for **all** plugins (Pattern A and B): - -| Field | Type | Description | -|-------|------|-------------| -| `bridge` | TelegramBridge | Send messages, reactions, media (low-level) | -| `db` | Database | SQLite (shared โ€” prefer `sdk.db` for isolation) | -| `chatId` | string | Current chat ID | -| `senderId` | number | Telegram user ID of caller | -| `isGroup` | boolean | `true` = group, `false` = DM | -| `config` | Config? | Agent config (may be undefined) | - -## SDK object - -Available **only** in `tools(sdk)` function plugins (Pattern B): - -| Namespace | Methods | -|-----------|---------| -| `sdk.ton` | **Wallet:** `getAddress()`, `getBalance(addr?)`, `getPrice()`, `sendTON(to, amount, comment?)`, `getTransactions(addr, limit?)`, `verifyPayment(params)`, `getPublicKey()`, `getWalletVersion()` | -| | **Jettons:** `getJettonBalances(ownerAddr?)`, `getJettonInfo(jettonAddr)`, `sendJetton(jettonAddr, to, amount, opts?)`, `getJettonWalletAddress(ownerAddr, jettonAddr)`, `createJettonTransfer(jettonAddr, to, amount, opts?)` | -| | **NFT:** `getNftItems(ownerAddr?)`, `getNftInfo(nftAddr)` | -| | **Utilities:** `toNano(amount)`, `fromNano(nano)`, `validateAddress(addr)` | -| `sdk.telegram` | **Messages:** `sendMessage(chatId, text, opts?)`, `editMessage(chatId, msgId, text, opts?)`, `deleteMessage(chatId, msgId, revoke?)`, `forwardMessage(fromChat, toChat, msgId)`, `pinMessage(chatId, msgId, opts?)`, `searchMessages(chatId, query, limit?)`, `scheduleMessage(chatId, text, scheduleDate)`, `getReplies(chatId, msgId, limit?)`, `getMessages(chatId, limit?)` | -| | **Media:** `sendPhoto(chatId, photo, opts?)`, `sendVideo(chatId, video, opts?)`, `sendVoice(chatId, voice, opts?)`, `sendFile(chatId, file, opts?)`, `sendGif(chatId, gif, opts?)`, `sendSticker(chatId, sticker)`, `downloadMedia(chatId, msgId)`, `setTyping(chatId)` | -| | **Social:** `getChatInfo(chatId)`, `getUserInfo(userId)`, `resolveUsername(username)`, `getParticipants(chatId, limit?)` | -| | **Interactive:** `sendDice(chatId, emoticon, replyToId?)`, `sendReaction(chatId, msgId, emoji)`, `createPoll(chatId, question, answers, opts?)`, `createQuiz(chatId, question, answers, correctIdx, explanation?)` | -| | **Moderation:** `banUser(chatId, userId)`, `unbanUser(chatId, userId)`, `muteUser(chatId, userId, untilDate?)` | -| | **Stars & Gifts:** `getStarsBalance()`, `sendGift(userId, giftId, opts?)`, `getAvailableGifts()`, `getMyGifts(limit?)`, `getResaleGifts(limit?)`, `buyResaleGift(giftId)` | -| | **Stories:** `sendStory(mediaPath, opts?)` | -| | **Core:** `getMe()`, `isAvailable()`, `getRawClient()` | -| `sdk.db` | `better-sqlite3` instance (requires `export function migrate(db)`) | -| `sdk.storage` | `get(key)`, `set(key, value, opts?)`, `delete(key)`, `has(key)`, `clear()` โ€” auto-created KV store with optional TTL, no `migrate()` needed | -| `sdk.secrets` | `get(key)`, `require(key)`, `has(key)` โ€” resolves from: ENV var (`PLUGINNAME_KEY`) โ†’ secrets file โ†’ `pluginConfig` | -| `sdk.log` | `info()`, `warn()`, `error()`, `debug()` | -| `sdk.config` | Sanitized app config (no API keys) | -| `sdk.pluginConfig` | Plugin config from `config.yaml` merged with `manifest.defaultConfig` | -| `sdk.bot` | `isAvailable` (getter), `username` (getter), `onInlineQuery(handler)`, `onCallback(pattern, handler)`, `onChosenResult(handler)`, `editInlineMessage(id, text, opts?)`, `keyboard(rows)` โ€” `null` if no bot in manifest | -| `sdk.on` | `on(hookName, handler, opts?)` โ€” 13 hooks: `tool:before`, `tool:after`, `tool:error`, `prompt:before`, `prompt:after`, `session:start`, `session:end`, `message:receive`, `response:before`, `response:after`, `response:error`, `agent:start`, `agent:stop` | - -## sdk.secrets โ€” Secret management - -Resolves secrets from multiple sources in priority order: environment variable (`PLUGINNAME_KEY`) โ†’ secrets store (`~/.teleton/plugins/data/name.secrets.json`) โ†’ `pluginConfig`. - -```javascript -// Check if a secret is available -if (sdk.secrets.has("api_key")) { - const key = sdk.secrets.get("api_key"); // string | undefined -} - -// Require a secret (throws PluginSDKError with code SECRET_NOT_FOUND if missing) -const apiKey = sdk.secrets.require("api_key"); -``` - -Declare required secrets in `manifest.json` so users know what to configure: - -```json -{ - "secrets": { - "api_key": { "required": true, "description": "API key for service X" } - } -} -``` - -## sdk.storage โ€” Key-value storage with TTL - -Auto-created `_kv` table โ€” no `migrate()` export needed. Returns `null` if no database is available. - -```javascript -// Set a value with optional TTL (milliseconds) -sdk.storage.set("rate_limit:user123", { count: 1 }, { ttl: 60000 }); // expires in 60s - -// Get a value (returns undefined if missing or expired) -const data = sdk.storage.get("rate_limit:user123"); - -// Check existence (respects TTL) -if (sdk.storage.has("rate_limit:user123")) { /* ... */ } - -// Delete a key (returns true if it existed) -sdk.storage.delete("rate_limit:user123"); - -// Clear all keys for this plugin -sdk.storage.clear(); -``` - -## sdk.bot โ€” Inline bot features - -`null` if no `bot` field in `manifest.json`. Provides inline query handling, callback buttons, and keyboard building. - -```javascript -// Check if bot is available -if (sdk.bot.isAvailable) { - const botName = sdk.bot.username; // string โ€” bot username - - // Register inline query handler - sdk.bot.onInlineQuery(async (query) => { - // handle inline query - }); - - // Register callback handler for a pattern - sdk.bot.onCallback("action:*", async (data) => { - // handle callback - }); - - // Register chosen inline result handler - sdk.bot.onChosenResult(async (result) => { - // handle chosen result - }); - - // Edit an inline message - await sdk.bot.editInlineMessage(inlineMessageId, "Updated text", { parseMode: "HTML" }); - - // Build a keyboard - const kb = sdk.bot.keyboard([ - [{ text: "Button 1", callback: "action:1" }, { text: "Button 2", callback: "action:2" }], - ]); // BotKeyboard -} -``` - -## sdk.on โ€” Event hooks - -Register handlers for agent lifecycle and tool execution events. 13 hook types available. - -```javascript -// Tool hooks -sdk.on("tool:before", (event) => { /* before any tool executes */ }); -sdk.on("tool:after", (event) => { /* after any tool executes */ }); -sdk.on("tool:error", (event) => { /* when a tool throws */ }); - -// Prompt hooks -sdk.on("prompt:before", (event) => { /* before prompt is sent to LLM */ }); -sdk.on("prompt:after", (event) => { /* after LLM responds */ }); - -// Session hooks -sdk.on("session:start", (event) => { /* session begins */ }); -sdk.on("session:end", (event) => { /* session ends */ }); - -// Message hooks -sdk.on("message:receive", (event) => { /* incoming message */ }); - -// Response hooks -sdk.on("response:before", (event) => { /* before response is sent */ }); -sdk.on("response:after", (event) => { /* after response is sent */ }); -sdk.on("response:error", (event) => { /* response error */ }); - -// Agent hooks -sdk.on("agent:start", (event) => { /* agent starts */ }); -sdk.on("agent:stop", (event) => { /* agent stops */ }); - -// Optional: pass options -sdk.on("tool:after", handler, { priority: 10 }); -``` - -## sdk.ton โ€” Jetton & NFT methods - -#### Jettons (TEP-74) - -```javascript -// Get all jetton balances for the agent wallet (or a specific address) -const balances = await sdk.ton.getJettonBalances(); // JettonBalance[] -const otherBalances = await sdk.ton.getJettonBalances("UQ..."); - -// Get jetton metadata -const info = await sdk.ton.getJettonInfo("EQ...jetton_master"); // JettonInfo | null - -// Send jettons -const result = await sdk.ton.sendJetton( - "EQ...jetton_master", // jetton master address - "UQ...recipient", // destination - "1000000000", // amount in smallest units - { comment: "payment" } // optional -); // JettonSendResult - -// Get jetton wallet address -const wallet = await sdk.ton.getJettonWalletAddress("UQ...owner", "EQ...jetton"); // string | null ``` -#### NFTs +## Required validation -```javascript -// Get all NFTs for the agent wallet (or a specific address) -const nfts = await sdk.ton.getNftItems(); // NftItem[] +Run from the repository root with `../teleton-agent` available: -// Get NFT metadata -const nft = await sdk.ton.getNftInfo("EQ...nft_address"); // NftItem | null +```bash +npm ci --ignore-scripts +npm run install:plugins +npm run generate +npm run validate +npm test +npm --prefix ../teleton-agent run build:sdk +npm run validate:runtime +npm run audit:plugins ``` -#### Wallet info - -```javascript -const pubKey = sdk.ton.getPublicKey(); // string | null โ€” hex ed25519 public key -const version = sdk.ton.getWalletVersion(); // string โ€” always "v5r1" -``` - -#### Jetton transfer (sign without broadcast) - -```javascript -// Sign a jetton transfer (TEP-74) without broadcasting -const transfer = await sdk.ton.createJettonTransfer( - "EQ...jetton_master", // jetton master address - "UQ...recipient", // destination - "1000000000", // amount in smallest units - { comment: "payment" } // optional -); // SignedTransfer โ€” throws WALLET_NOT_INITIALIZED, OPERATION_FAILED -``` - -#### Utilities - -```javascript -const nano = sdk.ton.toNano(1.5); // bigint โ€” 1500000000n -const tons = sdk.ton.fromNano(1500000000n); // string โ€” "1.5" -const valid = sdk.ton.validateAddress("UQ..."); // boolean -``` - -## sdk.telegram โ€” Extended methods - -#### Media - -```javascript -// photo/video/voice/file/gif can be a file path (string) or Buffer -await sdk.telegram.sendPhoto(chatId, "/path/to/image.jpg", { caption: "Look!" }); -await sdk.telegram.sendVideo(chatId, videoBuffer, { caption: "Video" }); -await sdk.telegram.sendVoice(chatId, voiceBuffer, { caption: "Listen" }); -await sdk.telegram.sendFile(chatId, "/path/to/doc.pdf", { caption: "Document" }); -await sdk.telegram.sendGif(chatId, gifBuffer); -await sdk.telegram.sendSticker(chatId, stickerBuffer); - -// Download media from a message -const buffer = await sdk.telegram.downloadMedia(chatId, messageId); // Buffer | null - -// Show typing indicator -await sdk.telegram.setTyping(chatId); -``` - -#### Message management - -```javascript -await sdk.telegram.deleteMessage(chatId, messageId, true); // revoke=true deletes for everyone -await sdk.telegram.forwardMessage(fromChatId, toChatId, messageId); -await sdk.telegram.pinMessage(chatId, messageId, { silent: true }); -const results = await sdk.telegram.searchMessages(chatId, "query", 20); -const replies = await sdk.telegram.getReplies(chatId, messageId, 10); -await sdk.telegram.scheduleMessage(chatId, "Reminder!", new Date("2026-03-01T10:00:00Z")); -``` - -#### Social & lookup - -```javascript -const chat = await sdk.telegram.getChatInfo(chatId); // ChatInfo | null -const user = await sdk.telegram.getUserInfo(userId); // UserInfo | null -const peer = await sdk.telegram.resolveUsername("username"); // ResolvedPeer | null -const members = await sdk.telegram.getParticipants(chatId, 100); // UserInfo[] -``` - -#### Interactive - -```javascript -await sdk.telegram.createPoll(chatId, "Favorite color?", ["Red", "Blue", "Green"], { anonymous: false }); -await sdk.telegram.createQuiz(chatId, "2+2=?", ["3", "4", "5"], 1, "Basic math!"); -``` - -#### Moderation - -```javascript -await sdk.telegram.banUser(chatId, userId); -await sdk.telegram.unbanUser(chatId, userId); -await sdk.telegram.muteUser(chatId, userId, Math.floor(Date.now() / 1000) + 3600); // mute 1h -``` - -#### Stars & Gifts - -```javascript -const balance = await sdk.telegram.getStarsBalance(); // number -await sdk.telegram.sendGift(userId, giftId, { message: "Enjoy!" }); -const gifts = await sdk.telegram.getAvailableGifts(); // StarGift[] -const mine = await sdk.telegram.getMyGifts(10); // ReceivedGift[] -const resale = await sdk.telegram.getResaleGifts(10); // StarGift[] -await sdk.telegram.buyResaleGift(giftId); -``` - -#### Stories - -```javascript -await sdk.telegram.sendStory("/path/to/media.jpg", { caption: "Check this out!" }); -``` - -#### Raw GramJS client - -```javascript -const client = sdk.telegram.getRawClient(); // raw GramJS TelegramClient | null -// Use for inline bots, WebApp auth, custom MTProto calls -``` - -## SDK Types reference - -| Type | Key fields | -|------|------------| -| `JettonBalance` | `jettonAddress`, `balance`, `metadata` (name, symbol, decimals, image) | -| `JettonInfo` | `address`, `totalSupply`, `metadata` (name, symbol, decimals, image, description) | -| `JettonSendResult` | `hash`, `lt`, `success` | -| `SignedTransfer` | Signed transfer BOC (from `createJettonTransfer`) | -| `NftItem` | `address`, `collectionAddress`, `metadata` (name, description, image), `ownerAddress` | -| `ChatInfo` | `id`, `title`, `type`, `participantsCount`, `username` | -| `UserInfo` | `id`, `firstName`, `lastName`, `username`, `phone`, `bot` | -| `ResolvedPeer` | `id`, `type`, `username` | -| `MediaSendOptions` | `caption?`, `replyToId?`, `silent?` | -| `PollOptions` | `anonymous?`, `multipleChoice?`, `quizMode?` | -| `StarGift` | `id`, `name`, `price`, `availability`, `image` | -| `ReceivedGift` | `id`, `fromUser`, `gift`, `date`, `message` | -| `SecretDeclaration` | `required` (boolean), `description` (string) | -| `StorageSDK` | `get()`, `set()`, `delete()`, `has()`, `clear()` | -| `SecretsSDK` | `get()`, `require()`, `has()` | -| `ToolCategory` | `"data-bearing"` \| `"action"` | -| `StartContext` | `bridge`, `db`, `config`, `pluginConfig`, `log` | - -## Bridge methods (legacy) - -Only needed for Pattern A plugins that use `context.bridge` directly: - -```javascript -await context.bridge.sendMessage({ chatId, text, replyToId?, inlineKeyboard? }); -await context.bridge.sendReaction(chatId, messageId, emoji); -await context.bridge.editMessage({ chatId, messageId, text, inlineKeyboard? }); -await context.bridge.setTyping(chatId); -const msgs = await context.bridge.getMessages(chatId, limit); -const peer = context.bridge.getPeer(chatId); -const gramjs = context.bridge.getClient().getClient(); -``` +Do not claim completion if any command fails. Do not push, publish or install into a live agent unless +the user explicitly requests it. diff --git a/compatibility.json b/compatibility.json new file mode 100644 index 0000000..9a21d7f --- /dev/null +++ b/compatibility.json @@ -0,0 +1,138 @@ +{ + "schemaVersion": 1, + "targetSdkVersion": "2.1.0", + "plugins": { + "boards": { + "status": "supported", + "marketplace": true, + "sdkVersion": "^2.0.0" + }, + "casino": { + "status": "supported", + "marketplace": true, + "sdkVersion": "^2.0.0" + }, + "crypto-prices": { + "status": "supported", + "marketplace": true, + "sdkVersion": "^2.0.0" + }, + "dedust": { + "status": "supported", + "marketplace": true, + "sdkVersion": "^2.0.0" + }, + "deezer": { + "status": "supported", + "marketplace": true, + "sdkVersion": "^2.1.0" + }, + "dyor": { + "status": "supported", + "marketplace": true, + "sdkVersion": "^2.0.0" + }, + "example": { + "status": "supported", + "marketplace": false, + "sdkVersion": null + }, + "example-sdk": { + "status": "supported", + "marketplace": false, + "sdkVersion": "^2.0.0" + }, + "fragment": { + "status": "supported", + "marketplace": true, + "sdkVersion": "^2.0.0" + }, + "gaspump": { + "status": "quarantined", + "marketplace": false, + "sdkVersion": "^1.0.0", + "reason": "Legacy SDK v1 plugin retained for older Teleton agents; raw bridge and wallet access are incompatible with SDK v2." + }, + "geckoterminal": { + "status": "supported", + "marketplace": true, + "sdkVersion": "^2.0.0" + }, + "giftindex": { + "status": "supported", + "marketplace": true, + "sdkVersion": "^2.0.0" + }, + "giftstat": { + "status": "supported", + "marketplace": true, + "sdkVersion": "^2.0.0" + }, + "multisend": { + "status": "supported", + "marketplace": true, + "sdkVersion": "^2.1.0" + }, + "pic": { + "status": "supported", + "marketplace": true, + "sdkVersion": "^2.1.0" + }, + "sbt": { + "status": "supported", + "marketplace": true, + "sdkVersion": "^2.0.0" + }, + "stonfi": { + "status": "supported", + "marketplace": true, + "sdkVersion": "^2.0.0" + }, + "stormtrade": { + "status": "supported", + "marketplace": true, + "sdkVersion": "^2.0.0" + }, + "swapcoffee": { + "status": "supported", + "marketplace": true, + "sdkVersion": "^2.0.0" + }, + "tonapi": { + "status": "supported", + "marketplace": true, + "sdkVersion": "^2.0.0" + }, + "twitter": { + "status": "supported", + "marketplace": true, + "sdkVersion": "^2.0.0" + }, + "uranus": { + "status": "supported", + "marketplace": true, + "sdkVersion": "^2.1.0" + }, + "vid": { + "status": "supported", + "marketplace": true, + "sdkVersion": "^2.1.0" + }, + "voice-notes": { + "status": "quarantined", + "marketplace": false, + "sdkVersion": "^1.0.0", + "reason": "Legacy SDK v1 plugin retained for older Teleton agents; raw Telegram client access is incompatible with SDK v2." + }, + "weather": { + "status": "supported", + "marketplace": true, + "sdkVersion": "^2.0.0" + }, + "webdom": { + "status": "supported", + "marketplace": true, + "sdkVersion": "^2.0.0" + } + } +} diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..47703db --- /dev/null +++ b/package-lock.json @@ -0,0 +1,30 @@ +{ + "name": "teleton-plugins-catalog", + "version": "2.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "teleton-plugins-catalog", + "version": "2.0.0", + "dependencies": { + "semver": "^7.8.5" + }, + "engines": { + "node": "^22.22.2 || ^24.15.0 || >=26.0.0" + } + }, + "node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..c85a3bc --- /dev/null +++ b/package.json @@ -0,0 +1,21 @@ +{ + "name": "teleton-plugins-catalog", + "version": "2.0.0", + "private": true, + "type": "module", + "engines": { + "node": "^22.22.2 || ^24.15.0 || >=26.0.0" + }, + "scripts": { + "generate": "node scripts/generate-catalog.mjs", + "check:generated": "node scripts/generate-catalog.mjs --check", + "validate": "npm run check:generated && node scripts/validate-catalog.mjs", + "validate:runtime": "node scripts/validate-runtime.mjs", + "install:plugins": "node scripts/install-plugin-deps.mjs", + "audit:plugins": "node scripts/audit-plugin-deps.mjs", + "test": "node --test tests/*.test.mjs" + }, + "dependencies": { + "semver": "^7.8.5" + } +} diff --git a/plugins/boards/README.md b/plugins/boards/README.md index fde6dd7..a5c9522 100644 --- a/plugins/boards/README.md +++ b/plugins/boards/README.md @@ -19,7 +19,7 @@ ## Requirements - **TON proxy**: `tonutils-proxy` must be running on `127.0.0.1:8080` (routes requests to the `.ton` network) -- **Wallet**: `~/.teleton/wallet.json` for signing x402 payments +- **Wallet**: a configured Teleton wallet exposed through the public `sdk.ton` capability No external npm dependencies required. @@ -49,4 +49,4 @@ Write tools use the [x402 protocol](https://www.x402.org/) for micropayments: - `Content-Length` header set explicitly (required by tonutils proxy) - 6 read tools are free (no payment), 3 write tools require x402 micropayment - Read tools: `category: "data-bearing"` โ€” always kept in LLM context -- Write tools: `category: "action"`, `scope: "dm-only"` โ€” masked after 10 results, DM only +- Write tools: `category: "action"`, `scope: "dm-only"` โ€” DM-only and subject to Teleton's explicit approval policy diff --git a/plugins/boards/index.js b/plugins/boards/index.js index 0baa343..9735fba 100644 --- a/plugins/boards/index.js +++ b/plugins/boards/index.js @@ -13,7 +13,7 @@ import http from "node:http"; export const manifest = { name: "boards", version: "1.0.0", - sdkVersion: ">=1.0.0", + sdkVersion: "^2.0.0", description: "Browse and participate in the boards.ton decentralized forum using x402 TON payments", defaultConfig: {}, }; diff --git a/plugins/boards/manifest.json b/plugins/boards/manifest.json index 36fdaea..a81f1aa 100644 --- a/plugins/boards/manifest.json +++ b/plugins/boards/manifest.json @@ -9,8 +9,8 @@ }, "license": "MIT", "entry": "index.js", - "teleton": ">=1.0.0", - "sdkVersion": ">=1.0.0", + "teleton": ">=0.9.0", + "sdkVersion": "^2.0.0", "tools": [ { "name": "boards_list", "description": "List all boards on the forum with their thread/post counts and descriptions" }, { "name": "boards_catalog", "description": "Get the thread catalog for a specific board" }, diff --git a/plugins/casino/index.js b/plugins/casino/index.js index c56c0b9..bafd88f 100644 --- a/plugins/casino/index.js +++ b/plugins/casino/index.js @@ -12,7 +12,7 @@ export const manifest = { name: "casino", version: "1.0.0", - sdkVersion: ">=1.0.0", + sdkVersion: "^2.0.0", description: "Slot machine and dice games with TON payments and auto-payout", defaultConfig: { min_bet: 0.1, diff --git a/plugins/casino/manifest.json b/plugins/casino/manifest.json index e0dd5a1..e0610a8 100644 --- a/plugins/casino/manifest.json +++ b/plugins/casino/manifest.json @@ -6,8 +6,8 @@ "author": { "name": "teleton", "url": "https://github.com/TONresistor" }, "license": "MIT", "entry": "index.js", - "teleton": ">=1.0.0", - "sdkVersion": ">=1.0.0", + "teleton": ">=0.9.0", + "sdkVersion": "^2.0.0", "tools": [ { "name": "casino_balance", "description": "Check casino bankroll and betting limits" }, { "name": "casino_spin", "description": "Execute a slot machine spin" }, diff --git a/plugins/crypto-prices/index.js b/plugins/crypto-prices/index.js index f1463be..9b8fa98 100644 --- a/plugins/crypto-prices/index.js +++ b/plugins/crypto-prices/index.js @@ -45,7 +45,7 @@ function resolveSymbol(input) { export const manifest = { name: "crypto-prices", version: "1.0.0", - sdkVersion: ">=1.0.0", + sdkVersion: "^2.0.0", description: "Live cryptocurrency prices and comparisons via CryptoCompare.", }; diff --git a/plugins/crypto-prices/manifest.json b/plugins/crypto-prices/manifest.json index fc0f91e..be4aba5 100644 --- a/plugins/crypto-prices/manifest.json +++ b/plugins/crypto-prices/manifest.json @@ -9,8 +9,8 @@ }, "license": "MIT", "entry": "index.js", - "teleton": ">=1.0.0", - "sdkVersion": ">=1.0.0", + "teleton": ">=0.9.0", + "sdkVersion": "^2.0.0", "tools": [ { "name": "crypto_price", "description": "Get current price for any coin in USD and RUB" }, { "name": "crypto_compare", "description": "Compare up to 5 cryptocurrencies side by side" } diff --git a/plugins/dedust/README.md b/plugins/dedust/README.md index 075ec74..802140c 100644 --- a/plugins/dedust/README.md +++ b/plugins/dedust/README.md @@ -2,7 +2,7 @@ Swap tokens, browse pools, and trade on [DeDust](https://dedust.io) -- TON's #2 decentralized exchange. -Read tools use the DeDust REST API. Swap tools use the `@dedust/sdk` for on-chain estimation and transaction building, signed from the agent wallet. +Read tools use the DeDust REST API. Swap tools use the Teleton SDK v2 transaction broker; the plugin never reads wallet credentials. ## Tools @@ -14,8 +14,8 @@ Read tools use the DeDust REST API. Swap tools use the `@dedust/sdk` for on-chai | `dedust_pool_info` | Get detailed pool info including metadata, reserves, and fees | | `dedust_jetton_info` | Get jetton metadata, top holders, and top traders | | `dedust_prices` | Get prices and liquidity data from DeDust CoinGecko tickers | -| `dedust_swap_estimate` | Estimate swap output using on-chain pool get-methods | -| `dedust_swap` | Execute a swap from the agent wallet | +| `dedust_swap_estimate` | Estimate a swap through the SDK v2 DeDust broker | +| `dedust_swap` | Execute a brokered DeDust swap | ## Install @@ -46,15 +46,7 @@ Ask the AI: ## Dependencies -Requires at runtime (provided by teleton): -- `@ton/core` -- Address, beginCell, toNano, fromNano, SendMode -- `@ton/ton` -- WalletContractV5R1, TonClient -- `@ton/crypto` -- mnemonicToPrivateKey -- `@dedust/sdk` -- required for `dedust_swap_estimate` and `dedust_swap` (on-chain estimation and transaction building) - -Agent wallet at `~/.teleton/wallet.json` is used for signing all on-chain transactions. - -> **Note:** The `@dedust/sdk` package must be installed in the teleton runtime for swap tools to work. Install with: `npm install @dedust/sdk` +No plugin-local dependency is required. Teleton SDK v2 provides the DeDust quote and transaction broker capabilities. ## Schemas @@ -111,7 +103,7 @@ Get prices and liquidity data for tokens from DeDust CoinGecko tickers. Use "nat ### dedust_swap_estimate -Estimate swap output on DeDust using on-chain pool get-methods. Returns expected output amount and trade fee. Use "native" for TON or a jetton address. +Estimate swap output through the Teleton SDK v2 DeDust broker. Returns expected output, minimum output, rate, fee, and pool type. Use "native" for TON or a jetton address. | Param | Type | Required | Description | |-------|------|----------|-------------| diff --git a/plugins/dedust/index.js b/plugins/dedust/index.js index 83960ec..4e94195 100644 --- a/plugins/dedust/index.js +++ b/plugins/dedust/index.js @@ -2,41 +2,14 @@ * DeDust plugin -- DEX on TON * * Browse pools, search assets, view trades, get prices, estimate swaps, - * and execute on-chain swaps on the DeDust protocol. - * Agent wallet at ~/.teleton/wallet.json signs swap transactions. + * and execute on-chain swaps on the DeDust protocol through SDK v2. */ -import { createRequire } from "node:module"; -import { readFileSync, realpathSync } from "node:fs"; -import { join } from "node:path"; -import { homedir } from "node:os"; - -// --------------------------------------------------------------------------- -// CJS dependencies -// --------------------------------------------------------------------------- - -const _require = createRequire(realpathSync(process.argv[1])); // core: @ton/core, @ton/ton, @ton/crypto -const _pluginRequire = createRequire(import.meta.url); // local: plugin-specific deps - -const { Address, beginCell, toNano, fromNano, SendMode } = _require("@ton/core"); -const { WalletContractV5R1, TonClient, internal } = _require("@ton/ton"); -const { mnemonicToPrivateKey } = _require("@ton/crypto"); - -// DeDust SDK -- loaded from plugin's local node_modules -let DedustSDK = null; -try { - DedustSDK = _pluginRequire("@dedust/sdk"); -} catch { - // SDK not available; on-chain tools will throw a clear error -} - // --------------------------------------------------------------------------- // Constants // --------------------------------------------------------------------------- const API_BASE = "https://api.dedust.io"; -const WALLET_FILE = join(homedir(), ".teleton", "wallet.json"); -const FACTORY_ADDR = "EQBfBWT7X2BHg9tXAxzhz2aKiNTU1tpt5NsiK0uSDW_YAJ67"; // --------------------------------------------------------------------------- // Shared API helper @@ -101,51 +74,6 @@ function formatAmount(raw, decimals) { return fracPart ? `${intPart}.${fracPart}` : intPart; } -// --------------------------------------------------------------------------- -// Wallet helper -// --------------------------------------------------------------------------- - -async function getWalletAndClient() { - let walletData; - try { - walletData = JSON.parse(readFileSync(WALLET_FILE, "utf-8")); - } catch { - throw new Error("Agent wallet not found at " + WALLET_FILE); - } - if (!walletData.mnemonic || !Array.isArray(walletData.mnemonic)) { - throw new Error("Invalid wallet file: missing mnemonic array"); - } - - const keyPair = await mnemonicToPrivateKey(walletData.mnemonic); - const wallet = WalletContractV5R1.create({ - workchain: 0, - publicKey: keyPair.publicKey, - }); - - let endpoint; - try { - const { getHttpEndpoint } = _pluginRequire("@orbs-network/ton-access"); - endpoint = await getHttpEndpoint({ network: "mainnet" }); - } catch { - endpoint = "https://toncenter.com/api/v2/jsonRPC"; - } - - const client = new TonClient({ endpoint }); - const contract = client.open(wallet); - - return { wallet, keyPair, client, contract }; -} - -function requireSDK() { - if (!DedustSDK) { - throw new Error( - "@dedust/sdk is not installed in the teleton runtime. " + - "Install it with: npm install @dedust/sdk" - ); - } - return DedustSDK; -} - // --------------------------------------------------------------------------- // Tool 1: dedust_assets // --------------------------------------------------------------------------- @@ -709,7 +637,7 @@ const dedustPrices = { const dedustSwapEstimate = { name: "dedust_swap_estimate", description: - 'Estimate swap output on DeDust using on-chain pool get-methods. Returns expected output amount and trade fee. Use "native" for TON or a jetton address.', + 'Estimate a DeDust swap through the Teleton SDK transaction broker. Use "native" for TON or a jetton address.', category: "data-bearing", scope: "always", @@ -735,65 +663,17 @@ const dedustSwapEstimate = { execute: async (params) => { try { - const sdk = requireSDK(); - const { Factory, PoolType, Asset } = sdk; - - const inputAmount = Number(params.input_amount); - if (!Number.isFinite(inputAmount) || inputAmount <= 0) { + const amount = Number(params.input_amount); + if (!Number.isFinite(amount) || amount <= 0) { throw new Error("input_amount must be a positive number"); } - - _sdk?.log?.info(`Estimating swap: ${params.input_amount} ${params.input_token} -> ${params.output_token}`); - - // Resolve assets - const assets = await getAssets(); - const isInputNative = params.input_token === "native"; - const isOutputNative = params.output_token === "native"; - - const inputDecimals = isInputNative - ? 9 - : findAssetDecimals(assets, "jetton", params.input_token); - const outputDecimals = isOutputNative - ? 9 - : findAssetDecimals(assets, "jetton", params.output_token); - - const inputAsset = isInputNative - ? Asset.native() - : Asset.jetton(Address.parse(params.input_token)); - const outputAsset = isOutputNative - ? Asset.native() - : Asset.jetton(Address.parse(params.output_token)); - - // Convert to raw amount - const rawInput = BigInt( - Math.round(inputAmount * 10 ** inputDecimals) - ); - - // Connect to chain - const { client } = await getWalletAndClient(); - const factory = client.open( - Factory.createFromAddress(Address.parse(FACTORY_ADDR)) - ); - - // Resolve pool - const pool = client.open( - await factory.getPool(PoolType.VOLATILE, [inputAsset, outputAsset]) - ); - - // Check readiness - const { ReadinessStatus } = sdk; - const readiness = await pool.getReadinessStatus(); - if (readiness !== ReadinessStatus.READY) { - throw new Error( - "Pool is not ready. It may not exist for this pair or pool type." - ); - } - - // Estimate - const estimate = await pool.getEstimatedSwapOut({ - assetIn: inputAsset, - amountIn: rawInput, + const quote = await _sdk.ton.dex.quoteDeDust({ + fromAsset: params.input_token === "native" ? "ton" : params.input_token, + toAsset: params.output_token === "native" ? "ton" : params.output_token, + amount, + slippage: 0.05, }); + if (!quote) throw new Error("No ready DeDust pool for this pair"); return { success: true, @@ -801,23 +681,19 @@ const dedustSwapEstimate = { input_token: params.input_token, output_token: params.output_token, input_amount: params.input_amount, - estimated_output: formatAmount( - estimate.amountOut.toString(), - outputDecimals - ), - trade_fee: formatAmount( - estimate.tradeFee.toString(), - inputDecimals - ), - trade_fee_token: params.input_token, - pool_address: pool.address.toString(), + estimated_output: quote.expectedOutput, + min_output: quote.minOutput, + trade_fee: quote.fee, + rate: quote.rate, + price_impact: quote.priceImpact ?? null, + pool_type: quote.poolType ?? null, }, }; - } catch (err) { - _sdk?.log?.error(`Swap estimate failed: ${err.message}`); + } catch (error) { + _sdk.log.error(`Swap estimate failed: ${String(error?.message ?? error)}`); return { success: false, - error: String(err.message || err).slice(0, 500), + error: String(error?.message ?? error).slice(0, 500), }; } }, @@ -830,7 +706,7 @@ const dedustSwapEstimate = { const dedustSwap = { name: "dedust_swap", description: - 'Execute a swap on DeDust from the agent wallet. Supports TON->Jetton and Jetton->TON swaps. Use "native" for TON. Call dedust_swap_estimate first to preview the output.', + 'Execute a DeDust swap through the Teleton SDK transaction broker. Use "native" for TON and call dedust_swap_estimate first.', category: "action", scope: "admin-only", @@ -863,154 +739,35 @@ const dedustSwap = { execute: async (params) => { try { - const sdk = requireSDK(); - const { - Factory, - PoolType, - Asset, - VaultNative, - VaultJetton, - JettonRoot, - ReadinessStatus, - } = sdk; - - const slippage = params.slippage ?? 0.05; - const inputAmount = Number(params.input_amount); - if (!Number.isFinite(inputAmount) || inputAmount <= 0) { + const amount = Number(params.input_amount); + if (!Number.isFinite(amount) || amount <= 0) { throw new Error("input_amount must be a positive number"); } - - const isInputNative = params.input_token === "native"; - const isOutputNative = params.output_token === "native"; - - if (isInputNative && isOutputNative) { - throw new Error("Cannot swap TON to TON"); - } - - _sdk?.log?.info(`Executing swap: ${params.input_amount} ${params.input_token} -> ${params.output_token} (slippage: ${slippage})`); - - // Resolve assets and decimals - const allAssets = await getAssets(); - const inputDecimals = isInputNative - ? 9 - : findAssetDecimals(allAssets, "jetton", params.input_token); - - const inputAsset = isInputNative - ? Asset.native() - : Asset.jetton(Address.parse(params.input_token)); - const outputAsset = isOutputNative - ? Asset.native() - : Asset.jetton(Address.parse(params.output_token)); - - const rawInput = BigInt( - Math.round(inputAmount * 10 ** inputDecimals) - ); - - // Connect - const { wallet, keyPair, client, contract } = - await getWalletAndClient(); - const factory = client.open( - Factory.createFromAddress(Address.parse(FACTORY_ADDR)) - ); - - // Resolve pool - const pool = client.open( - await factory.getPool(PoolType.VOLATILE, [inputAsset, outputAsset]) - ); - - const readiness = await pool.getReadinessStatus(); - if (readiness !== ReadinessStatus.READY) { - throw new Error( - "Pool is not ready. It may not exist for this pair." - ); - } - - // Estimate to calculate min output with slippage - const estimate = await pool.getEstimatedSwapOut({ - assetIn: inputAsset, - amountIn: rawInput, + const result = await _sdk.ton.dex.swapDeDust({ + fromAsset: params.input_token === "native" ? "ton" : params.input_token, + toAsset: params.output_token === "native" ? "ton" : params.output_token, + amount, + slippage: params.slippage ?? 0.05, }); - const minOut = - (estimate.amountOut * BigInt(Math.round((1 - slippage) * 10000))) / - 10000n; - - const sender = contract.sender(keyPair.secretKey); - - if (isInputNative) { - // TON -> Jetton: use VaultNative - const tonVault = client.open(await factory.getNativeVault()); - - const vaultReadiness = await tonVault.getReadinessStatus(); - if (vaultReadiness !== ReadinessStatus.READY) { - throw new Error("Native vault is not ready"); - } - - await tonVault.sendSwap(sender, { - poolAddress: pool.address, - amount: rawInput, - gasAmount: toNano("0.25"), - limit: minOut, - }); - } else { - // Jetton -> TON or Jetton -> Jetton: use VaultJetton - const jettonVault = client.open( - await factory.getJettonVault(Address.parse(params.input_token)) - ); - - const vaultReadiness = await jettonVault.getReadinessStatus(); - if (vaultReadiness !== ReadinessStatus.READY) { - throw new Error("Jetton vault is not ready"); - } - - const jettonRoot = client.open( - JettonRoot.createFromAddress(Address.parse(params.input_token)) - ); - const jettonWallet = client.open( - await jettonRoot.getWallet(wallet.address) - ); - - const forwardPayload = VaultJetton.createSwapPayload({ - poolAddress: pool.address, - limit: minOut, - }); - - await jettonWallet.sendTransfer(sender, toNano("0.3"), { - amount: rawInput, - destination: jettonVault.address, - responseAddress: wallet.address, - forwardAmount: toNano("0.25"), - forwardPayload, - }); - } - - const outputDecimals = isOutputNative - ? 9 - : findAssetDecimals(allAssets, "jetton", params.output_token); - return { success: true, data: { input_token: params.input_token, output_token: params.output_token, - input_amount: params.input_amount, - estimated_output: formatAmount( - estimate.amountOut.toString(), - outputDecimals - ), - min_output: formatAmount(minOut.toString(), outputDecimals), - slippage, - pool_address: pool.address.toString(), - wallet_address: wallet.address.toString(), - message: - "Swap transaction sent. Allow ~30 seconds for on-chain confirmation.", + input_amount: result.amountIn, + estimated_output: result.expectedOutput, + min_output: result.minOutput, + slippage: result.slippage, + tx_ref: result.txRef ?? null, + message: "Swap transaction confirmed by the Teleton transaction broker.", }, }; - } catch (err) { - _sdk?.log?.error(`Swap failed: ${err.message}`); + } catch (error) { + _sdk.log.error(`Swap failed: ${String(error?.message ?? error)}`); return { success: false, - error: String(err.message || err).slice(0, 500), + error: String(error?.message ?? error).slice(0, 500), }; } }, @@ -1023,7 +780,7 @@ const dedustSwap = { export const manifest = { name: "dedust", version: "1.0.0", - sdkVersion: ">=1.0.0", + sdkVersion: "^2.0.0", description: "DeDust DEX on TON โ€” browse pools, search assets, view trades, get prices, and execute on-chain swaps.", }; diff --git a/plugins/dedust/manifest.json b/plugins/dedust/manifest.json index 8fa399b..87c0d36 100644 --- a/plugins/dedust/manifest.json +++ b/plugins/dedust/manifest.json @@ -6,8 +6,8 @@ "author": { "name": "teleton", "url": "https://github.com/TONresistor" }, "license": "MIT", "entry": "index.js", - "teleton": ">=1.0.0", - "sdkVersion": ">=1.0.0", + "teleton": ">=0.9.0", + "sdkVersion": "^2.0.0", "tools": [ { "name": "dedust_assets", "description": "Search or list tokens available on DeDust by symbol, name, or address" }, { "name": "dedust_pools", "description": "List top DeDust liquidity pools sorted by reserves, volume, or fees" }, diff --git a/plugins/dedust/package-lock.json b/plugins/dedust/package-lock.json deleted file mode 100644 index 34134e2..0000000 --- a/plugins/dedust/package-lock.json +++ /dev/null @@ -1,505 +0,0 @@ -{ - "name": "teleton-plugin-dedust", - "version": "1.0.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "teleton-plugin-dedust", - "version": "1.0.0", - "dependencies": { - "@dedust/sdk": "^0.8.7", - "@orbs-network/ton-access": "^2.3.3" - } - }, - "node_modules/@dedust/sdk": { - "version": "0.8.7", - "resolved": "https://registry.npmjs.org/@dedust/sdk/-/sdk-0.8.7.tgz", - "integrity": "sha512-xW7bcdIUD09t9h5iUA+jCR0qLOnjvloYXNgEJbajPmIdXrCal8R+UoLE26a/5as0h6B2DmuC4peSYMoiHDFSlg==", - "license": "Apache-2.0", - "peerDependencies": { - "@ton/core": ">=0.52.0", - "@tonconnect/sdk": ">=2.1.2", - "axios": ">=1.5.0" - } - }, - "node_modules/@orbs-network/ton-access": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/@orbs-network/ton-access/-/ton-access-2.3.3.tgz", - "integrity": "sha512-b1miCPts7wBG9JKYgzXIRZQm/LMy5Uk1mNK8NzlcXHL3HRHJkkFbuYJGuj3IkWCiIicW3Ipp4sYnn3Fwo4oB0g==", - "license": "MIT", - "dependencies": { - "isomorphic-fetch": "^3.0.0" - } - }, - "node_modules/@ton/core": { - "version": "0.63.1", - "resolved": "https://registry.npmjs.org/@ton/core/-/core-0.63.1.tgz", - "integrity": "sha512-hDWMjlKzc18W2E4OeV3hUP8ohRJNHPD4Wd1+AQJj8zshZyCRT0usrvnExgbNUTo/vntDqCGMzgYWbXxyaA+L4g==", - "license": "MIT", - "peer": true, - "peerDependencies": { - "@ton/crypto": ">=3.2.0" - } - }, - "node_modules/@ton/crypto": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/@ton/crypto/-/crypto-3.3.0.tgz", - "integrity": "sha512-/A6CYGgA/H36OZ9BbTaGerKtzWp50rg67ZCH2oIjV1NcrBaCK9Z343M+CxedvM7Haf3f/Ee9EhxyeTp0GKMUpA==", - "license": "MIT", - "peer": true, - "dependencies": { - "@ton/crypto-primitives": "2.1.0", - "jssha": "3.2.0", - "tweetnacl": "1.0.3" - } - }, - "node_modules/@ton/crypto-primitives": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@ton/crypto-primitives/-/crypto-primitives-2.1.0.tgz", - "integrity": "sha512-PQesoyPgqyI6vzYtCXw4/ZzevePc4VGcJtFwf08v10OevVJHVfW238KBdpj1kEDQkxWLeuNHEpTECNFKnP6tow==", - "license": "MIT", - "peer": true, - "dependencies": { - "jssha": "3.2.0" - } - }, - "node_modules/@tonconnect/isomorphic-eventsource": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/@tonconnect/isomorphic-eventsource/-/isomorphic-eventsource-0.0.2.tgz", - "integrity": "sha512-B4UoIjPi0QkvIzZH5fV3BQLWrqSYABdrzZQSI9sJA9aA+iC0ohOzFwVVGXanlxeDAy1bcvPbb29f6sVUk0UnnQ==", - "license": "Apache-2.0", - "peer": true, - "dependencies": { - "eventsource": "^2.0.2" - } - }, - "node_modules/@tonconnect/isomorphic-fetch": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/@tonconnect/isomorphic-fetch/-/isomorphic-fetch-0.0.3.tgz", - "integrity": "sha512-jIg5nTrDwnite4fXao3dD83eCpTvInTjZon/rZZrIftIegh4XxyVb5G2mpMqXrVGk1e8SVXm3Kj5OtfMplQs0w==", - "license": "Apache-2.0", - "peer": true, - "dependencies": { - "node-fetch": "^2.6.9" - } - }, - "node_modules/@tonconnect/protocol": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/@tonconnect/protocol/-/protocol-2.4.0.tgz", - "integrity": "sha512-3xg6sMWIrSgW7/f7iPgOb2BI3LaMScjDMqfu20fcEMbOVvNFk3TGAUKK5cJ3pfvUINsyLgPoylcIbPap37jXiA==", - "license": "Apache-2.0", - "peer": true, - "dependencies": { - "tweetnacl": "^1.0.3", - "tweetnacl-util": "^0.15.1" - } - }, - "node_modules/@tonconnect/sdk": { - "version": "3.4.1", - "resolved": "https://registry.npmjs.org/@tonconnect/sdk/-/sdk-3.4.1.tgz", - "integrity": "sha512-eVH8erAFout89gXYHXua/es+mmLPiW1r7ng9hgHKYQv85HLq8/zzQEkbJpbTlnIuQ6CJ/QKchjMsIgYz3BYCUQ==", - "license": "Apache-2.0", - "peer": true, - "dependencies": { - "@tonconnect/isomorphic-eventsource": "0.0.2", - "@tonconnect/isomorphic-fetch": "0.0.3", - "@tonconnect/protocol": "2.4.0" - } - }, - "node_modules/asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", - "license": "MIT", - "peer": true - }, - "node_modules/axios": { - "version": "1.13.5", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.5.tgz", - "integrity": "sha512-cz4ur7Vb0xS4/KUN0tPWe44eqxrIu31me+fbang3ijiNscE129POzipJJA6zniq2C/Z6sJCjMimjS8Lc/GAs8Q==", - "license": "MIT", - "peer": true, - "dependencies": { - "follow-redirects": "^1.15.11", - "form-data": "^4.0.5", - "proxy-from-env": "^1.1.0" - } - }, - "node_modules/call-bind-apply-helpers": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", - "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", - "license": "MIT", - "peer": true, - "dependencies": { - "es-errors": "^1.3.0", - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/combined-stream": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", - "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "license": "MIT", - "peer": true, - "dependencies": { - "delayed-stream": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", - "license": "MIT", - "peer": true, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/dunder-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", - "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", - "license": "MIT", - "peer": true, - "dependencies": { - "call-bind-apply-helpers": "^1.0.1", - "es-errors": "^1.3.0", - "gopd": "^1.2.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-define-property": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", - "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", - "license": "MIT", - "peer": true, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-errors": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", - "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", - "license": "MIT", - "peer": true, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-object-atoms": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", - "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", - "license": "MIT", - "peer": true, - "dependencies": { - "es-errors": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-set-tostringtag": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", - "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", - "license": "MIT", - "peer": true, - "dependencies": { - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.6", - "has-tostringtag": "^1.0.2", - "hasown": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/eventsource": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-2.0.2.tgz", - "integrity": "sha512-IzUmBGPR3+oUG9dUeXynyNmf91/3zUSJg1lCktzKw47OXuhco54U3r9B7O4XX+Rb1Itm9OZ2b0RkTs10bICOxA==", - "license": "MIT", - "peer": true, - "engines": { - "node": ">=12.0.0" - } - }, - "node_modules/follow-redirects": { - "version": "1.15.11", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", - "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/RubenVerborgh" - } - ], - "license": "MIT", - "peer": true, - "engines": { - "node": ">=4.0" - }, - "peerDependenciesMeta": { - "debug": { - "optional": true - } - } - }, - "node_modules/form-data": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", - "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", - "license": "MIT", - "peer": true, - "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "es-set-tostringtag": "^2.1.0", - "hasown": "^2.0.2", - "mime-types": "^2.1.12" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/function-bind": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "license": "MIT", - "peer": true, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-intrinsic": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", - "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", - "license": "MIT", - "peer": true, - "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "es-define-property": "^1.0.1", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.1.1", - "function-bind": "^1.1.2", - "get-proto": "^1.0.1", - "gopd": "^1.2.0", - "has-symbols": "^1.1.0", - "hasown": "^2.0.2", - "math-intrinsics": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", - "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", - "license": "MIT", - "peer": true, - "dependencies": { - "dunder-proto": "^1.0.1", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/gopd": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", - "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", - "license": "MIT", - "peer": true, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-symbols": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", - "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", - "license": "MIT", - "peer": true, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-tostringtag": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", - "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", - "license": "MIT", - "peer": true, - "dependencies": { - "has-symbols": "^1.0.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/hasown": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", - "license": "MIT", - "peer": true, - "dependencies": { - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/isomorphic-fetch": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/isomorphic-fetch/-/isomorphic-fetch-3.0.0.tgz", - "integrity": "sha512-qvUtwJ3j6qwsF3jLxkZ72qCgjMysPzDfeV240JHiGZsANBYd+EEuu35v7dfrJ9Up0Ak07D7GGSkGhCHTqg/5wA==", - "license": "MIT", - "dependencies": { - "node-fetch": "^2.6.1", - "whatwg-fetch": "^3.4.1" - } - }, - "node_modules/jssha": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/jssha/-/jssha-3.2.0.tgz", - "integrity": "sha512-QuruyBENDWdN4tZwJbQq7/eAK85FqrI4oDbXjy5IBhYD+2pTJyBUWZe8ctWaCkrV0gy6AaelgOZZBMeswEa/6Q==", - "license": "BSD-3-Clause", - "peer": true, - "engines": { - "node": "*" - } - }, - "node_modules/math-intrinsics": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", - "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", - "license": "MIT", - "peer": true, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "license": "MIT", - "peer": true, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "license": "MIT", - "peer": true, - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/node-fetch": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", - "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", - "license": "MIT", - "dependencies": { - "whatwg-url": "^5.0.0" - }, - "engines": { - "node": "4.x || >=6.0.0" - }, - "peerDependencies": { - "encoding": "^0.1.0" - }, - "peerDependenciesMeta": { - "encoding": { - "optional": true - } - } - }, - "node_modules/proxy-from-env": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", - "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", - "license": "MIT", - "peer": true - }, - "node_modules/tr46": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", - "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", - "license": "MIT" - }, - "node_modules/tweetnacl": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-1.0.3.tgz", - "integrity": "sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw==", - "license": "Unlicense", - "peer": true - }, - "node_modules/tweetnacl-util": { - "version": "0.15.1", - "resolved": "https://registry.npmjs.org/tweetnacl-util/-/tweetnacl-util-0.15.1.tgz", - "integrity": "sha512-RKJBIj8lySrShN4w6i/BonWp2Z/uxwC3h4y7xsRrpP59ZboCd0GpEVsOnMDYLMmKBpYhb5TgHzZXy7wTfYFBRw==", - "license": "Unlicense", - "peer": true - }, - "node_modules/webidl-conversions": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", - "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", - "license": "BSD-2-Clause" - }, - "node_modules/whatwg-fetch": { - "version": "3.6.20", - "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.6.20.tgz", - "integrity": "sha512-EqhiFU6daOA8kpjOWTL0olhVOF3i7OrFzSYiGsEMB8GcXS+RrzauAERX65xMeNWVqxA6HXH2m69Z9LaKKdisfg==", - "license": "MIT" - }, - "node_modules/whatwg-url": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", - "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", - "license": "MIT", - "dependencies": { - "tr46": "~0.0.3", - "webidl-conversions": "^3.0.0" - } - } - } -} diff --git a/plugins/dedust/package.json b/plugins/dedust/package.json deleted file mode 100644 index d1c7b38..0000000 --- a/plugins/dedust/package.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "name": "teleton-plugin-dedust", - "type": "module", - "version": "1.0.0", - "private": true, - "dependencies": { - "@dedust/sdk": "^0.8.7", - "@orbs-network/ton-access": "^2.3.3" - } -} diff --git a/plugins/deezer/index.js b/plugins/deezer/index.js index ad48352..31d6a6e 100644 --- a/plugins/deezer/index.js +++ b/plugins/deezer/index.js @@ -5,22 +5,14 @@ * Messages appear "via @DeezerMusicBot" just like typing @DeezerMusicBot in the Telegram input field. */ -import { createRequire } from "node:module"; -import { realpathSync } from "node:fs"; - -// Resolve "telegram" from teleton's own node_modules (not the plugin directory). -// realpathSync follows the symlink so createRequire looks in the right node_modules. -const _require = createRequire(realpathSync(process.argv[1])); -const { Api } = _require("telegram"); - // --------------------------------------------------------------------------- // Export // --------------------------------------------------------------------------- export const manifest = { name: "deezer", - version: "1.0.1", - sdkVersion: ">=1.0.0", + version: "2.0.0", + sdkVersion: "^2.1.0", description: "Search and send music tracks in chat via Telegram's @DeezerMusicBot inline bot.", }; @@ -53,51 +45,22 @@ export const tools = (sdk) => [ execute: async (params, context) => { try { - const client = sdk.telegram.getRawClient(); - const deezerBot = await client.getEntity("DeezerMusicBot"); - const peer = await client.getInputEntity(context.chatId); - - const results = await client.invoke( - new Api.messages.GetInlineBotResults({ - bot: deezerBot, - peer, - query: params.query, - offset: "", - }) - ); - - if (!results.results || results.results.length === 0) { - return { success: false, error: `No tracks found for "${params.query}"` }; - } - - const index = params.index ?? 0; - if (index >= results.results.length) { - return { - success: false, - error: `Only ${results.results.length} results available, index ${index} is out of range`, - }; - } - - const chosen = results.results[index]; - - await client.invoke( - new Api.messages.SendInlineBotResult({ - peer, - queryId: results.queryId, - id: chosen.id, - randomId: BigInt(Math.floor(Math.random() * 2 ** 62)), - }) + const result = await sdk.telegram.sendInlineBotResult( + context.chatId, + "DeezerMusicBot", + params.query, + params.index ); return { success: true, data: { - query: params.query, - sent_index: index, - total_results: results.results.length, - title: chosen.title || null, - description: chosen.description || null, - type: chosen.type || null, + query: result.query, + sent_index: result.sentIndex, + total_results: result.totalResults, + title: result.title, + description: result.description, + type: result.type, }, }; } catch (err) { diff --git a/plugins/deezer/manifest.json b/plugins/deezer/manifest.json index 17c4515..a8a8505 100644 --- a/plugins/deezer/manifest.json +++ b/plugins/deezer/manifest.json @@ -1,7 +1,7 @@ { "id": "deezer", "name": "@DeezerMusicBot โ€” Inline Music Search", - "version": "1.0.1", + "version": "2.0.0", "description": "Search and send music tracks via Telegram's @DeezerMusicBot inline bot (Deezer)", "author": { "name": "teleton", @@ -9,11 +9,11 @@ }, "license": "MIT", "entry": "index.js", - "teleton": ">=1.0.0", + "teleton": ">=0.9.0", "tools": [ { "name": "deezer", "description": "Search and send a music track in the current chat via @DeezerMusicBot" } ], - "sdkVersion": ">=1.0.0", + "sdkVersion": "^2.1.0", "permissions": [], "tags": ["music", "deezer", "search", "inline-bot"], "repository": "https://github.com/TONresistor/teleton-plugins", diff --git a/plugins/dyor/index.js b/plugins/dyor/index.js index e20ea60..e05b252 100644 --- a/plugins/dyor/index.js +++ b/plugins/dyor/index.js @@ -135,7 +135,7 @@ const TX_TYPE_MAP = { export const manifest = { name: "dyor", version: "1.0.0", - sdkVersion: ">=1.0.0", + sdkVersion: "^2.0.0", description: "TON token analytics from DYOR.io -- search, price, trust score, metrics, DEX trades, holders, pools", }; diff --git a/plugins/dyor/manifest.json b/plugins/dyor/manifest.json index 46e3feb..61bfe61 100644 --- a/plugins/dyor/manifest.json +++ b/plugins/dyor/manifest.json @@ -9,8 +9,8 @@ }, "license": "MIT", "entry": "index.js", - "teleton": ">=1.0.0", - "sdkVersion": ">=1.0.0", + "teleton": ">=0.9.0", + "sdkVersion": "^2.0.0", "tools": [ { "name": "dyor_search", "description": "Search TON jettons by name or symbol on DYOR.io" }, { "name": "dyor_details", "description": "Get full details for a TON jetton by contract address" }, diff --git a/plugins/evaa/README.md b/plugins/evaa/README.md deleted file mode 100644 index e26c242..0000000 --- a/plugins/evaa/README.md +++ /dev/null @@ -1,135 +0,0 @@ -# EVAA Protocol - -Lending and borrowing on TON via EVAA. Supply assets to earn interest, borrow against collateral, and liquidate undercollateralized positions across 4 pools (Main, LP, Alts, Stable). - -| Tool | Description | -|------|-------------| -| `evaa_markets` | Market data: supply/borrow APY, utilization, TVL per asset | -| `evaa_assets` | Asset configs: collateral factor, liquidation threshold, reserve factor | -| `evaa_prices` | Current oracle prices (Pyth/Classic) for all pool assets | -| `evaa_user_position` | User position: supply/borrow balances, health factor, limits | -| `evaa_predict` | Simulate health factor impact of supply/withdraw/borrow/repay | -| `evaa_liquidations` | Check if a position is liquidatable with amounts | -| `evaa_supply` | Supply TON or jetton to a lending pool | -| `evaa_withdraw` | Withdraw supplied assets from a pool | -| `evaa_borrow` | Borrow an asset against your collateral | -| `evaa_repay` | Repay borrowed assets to reduce debt | -| `evaa_liquidate` | Liquidate an undercollateralized position | - -## Pools - -| Pool | Assets | Oracle | -|------|--------|--------| -| main | TON, jUSDT, jUSDC, stTON, tsTON, USDT, USDe, tsUSDe | Pyth | -| lp | TON, USDT, TONUSDT_DEDUST, TON_STORM, USDT_STORM, TONUSDT_STONFI | Classic | -| alts | TON, USDT, CATI, NOT, DOGS | Classic | -| stable | USDT, USDe, tsUSDe, PT_tsUSDe_01Sep2025, PT_tsUSDe_18Dec2025 | Classic | - -## Install - -```bash -mkdir -p ~/.teleton/plugins -cp -r plugins/evaa ~/.teleton/plugins/ -``` - -Requires `@evaafi/sdk` and `crypto-js` installed in the teleton runtime's `node_modules/`: - -```bash -cd /path/to/teleton-agent && npm install @evaafi/sdk crypto-js --legacy-peer-deps -``` - -> **Note:** `crypto-js` is an undeclared dependency of `@evaafi/sdk` โ€” it must be installed manually. - -## Usage examples - -- "What are the current EVAA lending rates?" -- "Show my EVAA position" -- "Supply 10 TON to EVAA" -- "Borrow 500 USDT from EVAA" -- "What would happen to my health factor if I withdraw 5 TON?" -- "Check if this address is liquidatable on EVAA" -- "Withdraw all my USDT from EVAA" -- "Repay 200 USDT on EVAA" - -## Tool schemas - -### evaa_markets - -| Param | Type | Required | Default | Description | -|-------|------|----------|---------|-------------| -| `pool` | string | No | all | Pool: main, lp, alts, stable | - -### evaa_assets - -| Param | Type | Required | Default | Description | -|-------|------|----------|---------|-------------| -| `pool` | string | No | main | Pool to query | - -### evaa_prices - -| Param | Type | Required | Default | Description | -|-------|------|----------|---------|-------------| -| `pool` | string | No | main | Pool to query | - -### evaa_user_position - -| Param | Type | Required | Default | Description | -|-------|------|----------|---------|-------------| -| `address` | string | No | agent wallet | TON wallet address | -| `pool` | string | No | main | Pool to query | - -### evaa_predict - -| Param | Type | Required | Default | Description | -|-------|------|----------|---------|-------------| -| `action` | string | Yes | -- | supply, withdraw, borrow, repay | -| `asset` | string | Yes | -- | Asset name (TON, USDT, etc.) | -| `amount` | string | Yes | -- | Amount in human units | -| `address` | string | No | agent wallet | TON wallet address | -| `pool` | string | No | main | Pool to query | - -### evaa_liquidations - -| Param | Type | Required | Default | Description | -|-------|------|----------|---------|-------------| -| `address` | string | Yes | -- | TON wallet address to check | -| `pool` | string | No | main | Pool to query | - -### evaa_supply - -| Param | Type | Required | Default | Description | -|-------|------|----------|---------|-------------| -| `asset` | string | Yes | -- | Asset name (TON, USDT, stTON, etc.) | -| `amount` | string | Yes | -- | Amount in human units | -| `pool` | string | No | main | Pool to use | - -### evaa_withdraw - -| Param | Type | Required | Default | Description | -|-------|------|----------|---------|-------------| -| `asset` | string | Yes | -- | Asset name to withdraw | -| `amount` | string | Yes | -- | Amount or "max" for maximum | -| `pool` | string | No | main | Pool to use | - -### evaa_borrow - -| Param | Type | Required | Default | Description | -|-------|------|----------|---------|-------------| -| `asset` | string | Yes | -- | Asset name to borrow | -| `amount` | string | Yes | -- | Amount in human units | -| `pool` | string | No | main | Pool to use | - -### evaa_repay - -| Param | Type | Required | Default | Description | -|-------|------|----------|---------|-------------| -| `asset` | string | Yes | -- | Asset name to repay | -| `amount` | string | Yes | -- | Amount in human units | -| `pool` | string | No | main | Pool to use | - -### evaa_liquidate - -| Param | Type | Required | Default | Description | -|-------|------|----------|---------|-------------| -| `borrower_address` | string | Yes | -- | Address of undercollateralized borrower | -| `pool` | string | No | main | Pool to use | diff --git a/plugins/evaa/index.js b/plugins/evaa/index.js deleted file mode 100644 index b28497c..0000000 --- a/plugins/evaa/index.js +++ /dev/null @@ -1,1215 +0,0 @@ -/** - * EVAA Protocol plugin -- Lending & borrowing on TON - * - * Supply, borrow, withdraw, repay, and liquidate across 4 EVAA pools. - * Uses @evaafi/sdk for on-chain interactions and oracle prices. - * Agent wallet at ~/.teleton/wallet.json signs all write transactions. - */ - -import { createRequire } from "node:module"; -import { readFileSync, realpathSync } from "node:fs"; -import { join } from "node:path"; -import { homedir } from "node:os"; - -// --------------------------------------------------------------------------- -// CJS dependencies -// --------------------------------------------------------------------------- - -const _require = createRequire(realpathSync(process.argv[1])); // core: @ton/core, @ton/ton, @ton/crypto -const _pluginRequire = createRequire(import.meta.url); // local: plugin-specific deps - -const { Address, beginCell, toNano, Cell, SendMode, fromNano } = _require("@ton/core"); -const { WalletContractV5R1, TonClient, internal } = _require("@ton/ton"); -const { mnemonicToPrivateKey } = _require("@ton/crypto"); - -const evaa = _pluginRequire("@evaafi/sdk"); - -// --------------------------------------------------------------------------- -// Constants -// --------------------------------------------------------------------------- - -const WALLET_FILE = join(homedir(), ".teleton", "wallet.json"); -const MASTER_CACHE_TTL = 5 * 60 * 1000; // 5 minutes - -const POOL_MAP = { - main: { config: evaa.MAINNET_POOL_CONFIG, MasterClass: evaa.EvaaMasterPyth, label: "Main (Pyth)" }, - lp: { config: evaa.MAINNET_LP_POOL_CONFIG, MasterClass: evaa.EvaaMasterClassic, label: "LP" }, - alts: { config: evaa.MAINNET_ALTS_POOL_CONFIG, MasterClass: evaa.EvaaMasterClassic, label: "Alts" }, - stable: { config: evaa.MAINNET_STABLE_POOL_CONFIG, MasterClass: evaa.EvaaMasterClassic, label: "Stable" }, -}; - -// --------------------------------------------------------------------------- -// Shared helpers -// --------------------------------------------------------------------------- - -/** Cached TonClient + pool master instances */ -let _client = null; -const _masterCache = new Map(); // poolKey -> { master, syncTime } - -async function getTonClient() { - if (_client) return _client; - let endpoint; - try { - const { getHttpEndpoint } = _pluginRequire("@orbs-network/ton-access"); - endpoint = await getHttpEndpoint({ network: "mainnet" }); - } catch { - endpoint = "https://toncenter.com/api/v2/jsonRPC"; - } - _client = new TonClient({ endpoint }); - return _client; -} - -/** Get or create a synced master contract for a pool. Caches for 5 min. */ -async function getSyncedMaster(poolKey) { - const entry = POOL_MAP[poolKey]; - if (!entry) throw new Error("Unknown pool: " + poolKey + ". Use: main, lp, alts, stable."); - - const cached = _masterCache.get(poolKey); - if (cached && Date.now() - cached.syncTime < MASTER_CACHE_TTL) { - return { master: cached.master, poolConfig: entry.config }; - } - - const client = await getTonClient(); - const master = client.open(new entry.MasterClass({ poolConfig: entry.config })); - await master.getSync(); - - _masterCache.set(poolKey, { master, syncTime: Date.now() }); - return { master, poolConfig: entry.config }; -} - -/** Get prices using the pool's built-in collector */ -async function getPrices(poolConfig) { - return await poolConfig.collector.getPrices(); -} - -/** Resolve asset by name from a pool config. Case-insensitive. */ -function resolveAsset(poolConfig, assetName) { - const target = assetName.toUpperCase(); - const asset = poolConfig.poolAssetsConfig.find( - (a) => a.name.toUpperCase() === target - ); - if (!asset) { - const available = poolConfig.poolAssetsConfig.map((a) => a.name).join(", "); - throw new Error("Asset '" + assetName + "' not found in pool. Available: " + available); - } - return asset; -} - -/** Format bigint balance to human-readable string with decimals. */ -function formatBalance(amount, decimals) { - if (amount === 0n) return "0"; - const d = Number(decimals); - const s = amount.toString().padStart(d + 1, "0"); - const intPart = s.slice(0, s.length - d) || "0"; - const fracPart = s.slice(s.length - d).replace(/0+$/, ""); - return fracPart ? intPart + "." + fracPart : intPart; -} - -/** Format USD-scaled value (ASSET_PRICE_SCALE = 1e9). */ -function formatUSD(value) { - return "$" + formatBalance(value, 9n); -} - -/** Parse a human-readable amount string to asset units. */ -function parseAmount(amountStr, decimals) { - const parts = amountStr.split("."); - const intPart = parts[0] || "0"; - const fracPart = (parts[1] || "").padEnd(Number(decimals), "0").slice(0, Number(decimals)); - return BigInt(intPart + fracPart); -} - -/** Read agent wallet, create TonClient, open wallet contract. */ -async function getWalletAndClient() { - let walletData; - try { - walletData = JSON.parse(readFileSync(WALLET_FILE, "utf-8")); - } catch { - throw new Error("Agent wallet not found at " + WALLET_FILE); - } - if (!walletData.mnemonic || !Array.isArray(walletData.mnemonic)) { - throw new Error("Invalid wallet file: missing mnemonic array"); - } - - const keyPair = await mnemonicToPrivateKey(walletData.mnemonic); - const wallet = WalletContractV5R1.create({ workchain: 0, publicKey: keyPair.publicKey }); - - const client = await getTonClient(); - const contract = client.open(wallet); - - return { wallet, keyPair, client, contract }; -} - -/** Create a Sender adapter for the SDK. */ -function createSender(contract, keyPair) { - return { - address: contract.address, - async send(args) { - const seqno = await contract.getSeqno(); - await contract.sendTransfer({ - seqno, - secretKey: keyPair.secretKey, - sendMode: SendMode.PAY_GAS_SEPARATELY | SendMode.IGNORE_ERRORS, - messages: [ - internal({ - to: args.to, - value: args.value, - body: args.body, - bounce: args.bounce ?? true, - }), - ], - }); - return seqno; - }, - }; -} - -/** Resolve pool key from parameter, default "main". */ -function resolvePool(pool) { - return (pool ?? "main").toLowerCase(); -} - -// --------------------------------------------------------------------------- -// Export (SDK v1.0.0) -// --------------------------------------------------------------------------- - -export const manifest = { - name: "evaa", - version: "1.0.0", - sdkVersion: ">=1.0.0", - description: "EVAA Protocol lending and borrowing on TON โ€” supply, borrow, withdraw, repay, and liquidate across multiple pools.", -}; - -export const tools = (sdk) => { - const { log, ton } = sdk; - -// --------------------------------------------------------------------------- -// Tool 1: evaa_markets -// --------------------------------------------------------------------------- - -const evaaMarkets = { - name: "evaa_markets", - description: - "Get EVAA lending market data: supply/borrow APY, utilization, TVL per asset. " + - "Shows all pools or a specific pool (main, lp, alts, stable).", - category: "data-bearing", - scope: "always", - - parameters: { - type: "object", - properties: { - pool: { - type: "string", - enum: ["main", "lp", "alts", "stable"], - description: "Pool to query (default: all pools)", - }, - }, - }, - - execute: async (params) => { - try { - const poolKeys = params.pool ? [resolvePool(params.pool)] : ["main", "lp", "alts", "stable"]; - const results = []; - - for (const poolKey of poolKeys) { - const { master, poolConfig } = await getSyncedMaster(poolKey); - const data = master.data; - const mc = poolConfig.masterConstants; - const assets = []; - - for (const asset of poolConfig.poolAssetsConfig) { - const assetData = data.assetsData.get(asset.assetId); - const assetConfig = data.assetsConfig.get(asset.assetId); - const supplyApy = data.apy.supply.get(asset.assetId); - const borrowApy = data.apy.borrow.get(asset.assetId); - - const totalSupplyPV = (assetData.sRate * assetData.totalSupply) / mc.FACTOR_SCALE; - const totalBorrowPV = (assetData.bRate * assetData.totalBorrow) / mc.FACTOR_SCALE; - const utilization = - totalSupplyPV > 0n - ? Number((totalBorrowPV * 10000n) / totalSupplyPV) / 100 - : 0; - - assets.push({ - name: asset.name, - supply_apy: (supplyApy * 100).toFixed(2) + "%", - borrow_apy: (borrowApy * 100).toFixed(2) + "%", - utilization: utilization.toFixed(2) + "%", - total_supply: formatBalance(totalSupplyPV, assetConfig.decimals), - total_borrow: formatBalance(totalBorrowPV, assetConfig.decimals), - tvl_balance: formatBalance(assetData.balance, assetConfig.decimals), - decimals: Number(assetConfig.decimals), - }); - } - - results.push({ - pool: poolKey, - label: POOL_MAP[poolKey].label, - master_address: poolConfig.masterAddress.toString(), - assets, - }); - } - - return { success: true, data: results }; - } catch (err) { - return { success: false, error: String(err.message || err).slice(0, 500) }; - } - }, -}; - -// --------------------------------------------------------------------------- -// Tool 2: evaa_assets -// --------------------------------------------------------------------------- - -const evaaAssets = { - name: "evaa_assets", - description: - "List supported assets with their configurations: collateral factor, liquidation threshold, " + - "borrow cap, reserve factor, origination fee, and more.", - category: "data-bearing", - scope: "always", - - parameters: { - type: "object", - properties: { - pool: { - type: "string", - enum: ["main", "lp", "alts", "stable"], - description: "Pool to query (default: main)", - }, - }, - }, - - execute: async (params) => { - try { - const poolKey = resolvePool(params.pool); - const { master, poolConfig } = await getSyncedMaster(poolKey); - const data = master.data; - const mc = poolConfig.masterConstants; - const assets = []; - - for (const asset of poolConfig.poolAssetsConfig) { - const cfg = data.assetsConfig.get(asset.assetId); - assets.push({ - name: asset.name, - decimals: Number(cfg.decimals), - collateral_factor: (Number(cfg.collateralFactor) / Number(mc.ASSET_COEFFICIENT_SCALE) * 100).toFixed(1) + "%", - liquidation_threshold: (Number(cfg.liquidationThreshold) / Number(mc.ASSET_LIQUIDATION_THRESHOLD_SCALE) * 100).toFixed(1) + "%", - liquidation_bonus: (Number(cfg.liquidationBonus) / Number(mc.ASSET_LIQUIDATION_BONUS_SCALE) * 100).toFixed(1) + "%", - reserve_factor: (Number(cfg.reserveFactor) / Number(mc.ASSET_RESERVE_FACTOR_SCALE) * 100).toFixed(1) + "%", - origination_fee: (Number(cfg.originationFee) / Number(mc.ASSET_ORIGINATION_FEE_SCALE) * 100).toFixed(4) + "%", - borrow_cap: cfg.borrowCap === -1n ? "unlimited" : formatBalance(cfg.borrowCap < 0n ? -cfg.borrowCap : cfg.borrowCap, cfg.decimals), - max_total_supply: formatBalance(cfg.maxTotalSupply, cfg.decimals), - }); - } - - return { success: true, data: { pool: poolKey, assets } }; - } catch (err) { - return { success: false, error: String(err.message || err).slice(0, 500) }; - } - }, -}; - -// --------------------------------------------------------------------------- -// Tool 3: evaa_prices -// --------------------------------------------------------------------------- - -const evaaPrices = { - name: "evaa_prices", - description: - "Get current oracle prices for all assets in an EVAA pool. Prices in USD.", - category: "data-bearing", - scope: "always", - - parameters: { - type: "object", - properties: { - pool: { - type: "string", - enum: ["main", "lp", "alts", "stable"], - description: "Pool to query (default: main)", - }, - }, - }, - - execute: async (params) => { - try { - const poolKey = resolvePool(params.pool); - const { poolConfig } = await getSyncedMaster(poolKey); - const prices = await getPrices(poolConfig); - const result = []; - - for (const asset of poolConfig.poolAssetsConfig) { - const p = prices.dict.get(asset.assetId); - result.push({ - name: asset.name, - price_usd: p ? (Number(p) / 1e9).toFixed(6) : null, - price_raw: p?.toString() ?? null, - }); - } - - return { success: true, data: { pool: poolKey, prices: result } }; - } catch (err) { - return { success: false, error: String(err.message || err).slice(0, 500) }; - } - }, -}; - -// --------------------------------------------------------------------------- -// Tool 4: evaa_user_position -// --------------------------------------------------------------------------- - -const evaaUserPosition = { - name: "evaa_user_position", - description: - "Get a user's lending position: supply/borrow balances, health factor, " + - "available to borrow, withdrawal limits. Defaults to agent wallet.", - category: "data-bearing", - scope: "always", - - parameters: { - type: "object", - properties: { - address: { - type: "string", - description: "TON wallet address (default: agent wallet)", - }, - pool: { - type: "string", - enum: ["main", "lp", "alts", "stable"], - description: "Pool to query (default: main)", - }, - }, - }, - - execute: async (params) => { - try { - if (params.address && !ton.validateAddress(params.address)) { - return { success: false, error: `Invalid address: ${params.address}` }; - } - const poolKey = resolvePool(params.pool); - let userAddr; - if (params.address) { - userAddr = Address.parse(params.address); - } else { - const walletData = JSON.parse(readFileSync(WALLET_FILE, "utf-8")); - userAddr = Address.parse(walletData.address); - } - - const { master, poolConfig } = await getSyncedMaster(poolKey); - const data = master.data; - const client = await getTonClient(); - const prices = await getPrices(poolConfig); - - const userContract = master.getOpenedUserContract(client, userAddr); - await userContract.getSync(data.assetsData, data.assetsConfig, prices.dict); - - const ud = userContract.data; - if (!ud || ud.type === "inactive") { - return { - success: true, - data: { - pool: poolKey, - address: userAddr.toString(), - status: "inactive", - message: "No active position in this pool.", - }, - }; - } - - const balances = []; - for (const asset of poolConfig.poolAssetsConfig) { - const balance = ud.balances.get(asset.assetId); - const cfg = data.assetsConfig.get(asset.assetId); - if (!balance || balance.amount === 0n) continue; - - const withdrawLimit = ud.withdrawalLimits?.get(asset.assetId); - const borrowLimit = ud.borrowLimits?.get(asset.assetId); - - balances.push({ - asset: asset.name, - type: balance.type, - amount: formatBalance(balance.amount, cfg.decimals), - withdraw_limit: withdrawLimit !== undefined ? formatBalance(withdrawLimit, cfg.decimals) : null, - borrow_limit: borrowLimit !== undefined ? formatBalance(borrowLimit, cfg.decimals) : null, - }); - } - - return { - success: true, - data: { - pool: poolKey, - address: userAddr.toString(), - status: "active", - health_factor: ud.healthFactor?.toFixed(4), - supply_balance_usd: formatUSD(ud.supplyBalance), - borrow_balance_usd: formatUSD(ud.borrowBalance), - available_to_borrow_usd: formatUSD(ud.availableToBorrow), - limit_used_percent: ud.limitUsedPercent?.toFixed(2) + "%", - is_liquidatable: ud.liquidationData?.liquidable ?? false, - balances, - }, - }; - } catch (err) { - return { success: false, error: String(err.message || err).slice(0, 500) }; - } - }, -}; - -// --------------------------------------------------------------------------- -// Tool 5: evaa_predict -// --------------------------------------------------------------------------- - -const evaaPredict = { - name: "evaa_predict", - description: - "Simulate how supply/withdraw/borrow/repay would affect your health factor. " + - "Returns predicted health factor before and after the action.", - category: "data-bearing", - scope: "always", - - parameters: { - type: "object", - properties: { - action: { - type: "string", - enum: ["supply", "withdraw", "borrow", "repay"], - description: "Action to simulate", - }, - asset: { - type: "string", - description: "Asset name (e.g. 'TON', 'USDT')", - }, - amount: { - type: "string", - description: "Amount in human units (e.g. '100' for 100 USDT)", - }, - address: { - type: "string", - description: "TON wallet address (default: agent wallet)", - }, - pool: { - type: "string", - enum: ["main", "lp", "alts", "stable"], - description: "Pool (default: main)", - }, - }, - required: ["action", "asset", "amount"], - }, - - execute: async (params) => { - try { - if (params.address && !ton.validateAddress(params.address)) { - return { success: false, error: `Invalid address: ${params.address}` }; - } - const poolKey = resolvePool(params.pool); - let userAddr; - if (params.address) { - userAddr = Address.parse(params.address); - } else { - const walletData = JSON.parse(readFileSync(WALLET_FILE, "utf-8")); - userAddr = Address.parse(walletData.address); - } - - const { master, poolConfig } = await getSyncedMaster(poolKey); - const data = master.data; - const client = await getTonClient(); - const prices = await getPrices(poolConfig); - - const assetConfig = resolveAsset(poolConfig, params.asset); - const assetCfg = data.assetsConfig.get(assetConfig.assetId); - const assetData = data.assetsData.get(assetConfig.assetId); - const amount = parseAmount(params.amount, assetCfg.decimals); - - const userContract = master.getOpenedUserContract(client, userAddr); - await userContract.getSync(data.assetsData, data.assetsConfig, prices.dict); - - const ud = userContract.data; - const principals = ud && ud.type === "active" ? ud.principals : evaa.Dictionary?.empty?.() ?? new Map(); - - const actionMap = { - supply: evaa.BalanceChangeType.Supply, - withdraw: evaa.BalanceChangeType.Withdraw, - borrow: evaa.BalanceChangeType.Borrow, - repay: evaa.BalanceChangeType.Repay, - }; - - const currentHealth = ud && ud.type === "active" ? ud.healthFactor : 1; - const predictedHealth = evaa.predictHealthFactor({ - principals, - prices: prices.dict, - assetsData: data.assetsData, - assetsConfig: data.assetsConfig, - poolConfig, - asset: assetConfig, - amount, - balanceChangeType: actionMap[params.action], - }); - - const predictedApy = evaa.predictAPY({ - assetConfig: assetCfg, - assetData, - masterConstants: poolConfig.masterConstants, - amount, - balanceChangeType: actionMap[params.action], - }); - - return { - success: true, - data: { - pool: poolKey, - action: params.action, - asset: params.asset, - amount: params.amount, - current_health_factor: currentHealth.toFixed(4), - predicted_health_factor: predictedHealth.toFixed(4), - predicted_supply_apy: (predictedApy.supplyApy * 100).toFixed(2) + "%", - predicted_borrow_apy: (predictedApy.borrowApy * 100).toFixed(2) + "%", - }, - }; - } catch (err) { - return { success: false, error: String(err.message || err).slice(0, 500) }; - } - }, -}; - -// --------------------------------------------------------------------------- -// Tool 6: evaa_liquidations -// --------------------------------------------------------------------------- - -const evaaLiquidations = { - name: "evaa_liquidations", - description: - "Check if a position is liquidatable and compute liquidation parameters " + - "(greatest loan, greatest collateral, amounts).", - category: "data-bearing", - scope: "always", - - parameters: { - type: "object", - properties: { - address: { - type: "string", - description: "TON wallet address to check", - }, - pool: { - type: "string", - enum: ["main", "lp", "alts", "stable"], - description: "Pool to query (default: main)", - }, - }, - required: ["address"], - }, - - execute: async (params) => { - try { - if (!ton.validateAddress(params.address)) { - return { success: false, error: `Invalid address: ${params.address}` }; - } - const poolKey = resolvePool(params.pool); - const userAddr = Address.parse(params.address); - - const { master, poolConfig } = await getSyncedMaster(poolKey); - const data = master.data; - const client = await getTonClient(); - const prices = await getPrices(poolConfig); - - const userContract = master.getOpenedUserContract(client, userAddr); - await userContract.getSync(data.assetsData, data.assetsConfig, prices.dict); - - const ud = userContract.data; - if (!ud || ud.type === "inactive") { - return { - success: true, - data: { pool: poolKey, address: params.address, liquidatable: false, reason: "No active position." }, - }; - } - - const liqData = ud.liquidationData; - if (!liqData) { - return { - success: true, - data: { pool: poolKey, address: params.address, liquidatable: false, reason: "Could not compute liquidation data." }, - }; - } - - const result = { - pool: poolKey, - address: params.address, - liquidatable: liqData.liquidable, - health_factor: ud.healthFactor?.toFixed(4), - total_debt_usd: formatUSD(liqData.totalDebt), - total_limit_usd: formatUSD(liqData.totalLimit), - }; - - if (liqData.liquidable) { - result.greatest_loan_asset = liqData.greatestLoanAsset?.name; - result.greatest_loan_value_usd = formatUSD(liqData.greatestLoanValue); - result.greatest_collateral_asset = liqData.greatestCollateralAsset?.name; - result.greatest_collateral_value_usd = formatUSD(liqData.greatestCollateralValue); - if (liqData.liquidationAmount) { - const loanCfg = data.assetsConfig.get(liqData.greatestLoanAsset.assetId); - result.liquidation_amount = formatBalance(liqData.liquidationAmount, loanCfg.decimals); - } - if (liqData.minCollateralAmount) { - const colCfg = data.assetsConfig.get(liqData.greatestCollateralAsset.assetId); - result.min_collateral_amount = formatBalance(liqData.minCollateralAmount, colCfg.decimals); - } - } - - return { success: true, data: result }; - } catch (err) { - return { success: false, error: String(err.message || err).slice(0, 500) }; - } - }, -}; - -// --------------------------------------------------------------------------- -// Tool 7: evaa_supply -// --------------------------------------------------------------------------- - -const evaaSupply = { - name: "evaa_supply", - description: - "Supply TON or a jetton to an EVAA lending pool to earn interest.", - category: "action", - scope: "admin-only", - - parameters: { - type: "object", - properties: { - asset: { - type: "string", - description: "Asset name (e.g. 'TON', 'USDT', 'stTON')", - }, - amount: { - type: "string", - description: "Amount to supply in human units (e.g. '10' for 10 TON)", - }, - pool: { - type: "string", - enum: ["main", "lp", "alts", "stable"], - description: "Pool (default: main)", - }, - }, - required: ["asset", "amount"], - }, - - execute: async (params) => { - try { - const poolKey = resolvePool(params.pool); - const { master, poolConfig } = await getSyncedMaster(poolKey); - const data = master.data; - const { wallet, keyPair, client, contract } = await getWalletAndClient(); - - const assetPoolCfg = resolveAsset(poolConfig, params.asset); - const assetCfg = data.assetsConfig.get(assetPoolCfg.assetId); - const amount = parseAmount(params.amount, assetCfg.decimals); - const isTon = evaa.isTonAsset(assetPoolCfg); - - const sender = createSender(contract, keyPair); - const value = isTon - ? toNano("0.3") + BigInt(amount) - : toNano("0.3"); - - const openedMaster = client.open(new (POOL_MAP[poolKey].MasterClass)({ poolConfig })); - await openedMaster.getSync(); - - await openedMaster.sendSupply(client.provider(openedMaster.address), sender, value, { - queryID: 0n, - includeUserCode: true, - amount: BigInt(amount), - userAddress: wallet.address, - asset: assetPoolCfg, - payload: Cell.EMPTY, - returnRepayRemainingsFlag: false, - customPayloadRecipient: null, - customPayloadSaturationFlag: false, - }); - - log.info(`Supply ${params.amount} ${params.asset} to ${poolKey} pool`); - return { - success: true, - data: { - pool: poolKey, - asset: params.asset, - amount: params.amount, - wallet_address: wallet.address.toString(), - message: "Supply tx sent. Check position after ~15 seconds with evaa_user_position.", - }, - }; - } catch (err) { - return { success: false, error: String(err.message || err).slice(0, 500) }; - } - }, -}; - -// --------------------------------------------------------------------------- -// Tool 8: evaa_withdraw -// --------------------------------------------------------------------------- - -const evaaWithdraw = { - name: "evaa_withdraw", - description: - "Withdraw supplied assets from an EVAA lending pool.", - category: "action", - scope: "admin-only", - - parameters: { - type: "object", - properties: { - asset: { - type: "string", - description: "Asset name to withdraw (e.g. 'TON', 'USDT')", - }, - amount: { - type: "string", - description: "Amount to withdraw in human units (e.g. '5' for 5 TON). Use 'max' for maximum.", - }, - pool: { - type: "string", - enum: ["main", "lp", "alts", "stable"], - description: "Pool (default: main)", - }, - }, - required: ["asset", "amount"], - }, - - execute: async (params) => { - try { - const poolKey = resolvePool(params.pool); - const { master, poolConfig } = await getSyncedMaster(poolKey); - const data = master.data; - const { wallet, keyPair, client, contract } = await getWalletAndClient(); - - const assetPoolCfg = resolveAsset(poolConfig, params.asset); - const assetCfg = data.assetsConfig.get(assetPoolCfg.assetId); - const prices = await getPrices(poolConfig); - - // Get user data to determine amount and prices needed - const userContract = master.getOpenedUserContract(client, wallet.address); - await userContract.getSync(data.assetsData, data.assetsConfig, prices.dict); - const ud = userContract.data; - - let withdrawAmount; - if (params.amount === "max" && ud && ud.type === "active") { - const limit = ud.withdrawalLimits?.get(assetPoolCfg.assetId); - withdrawAmount = limit ?? 0n; - } else { - withdrawAmount = parseAmount(params.amount, assetCfg.decimals); - } - - if (withdrawAmount === 0n) { - return { success: false, error: "Nothing to withdraw." }; - } - - // Get price data for withdraw (needed if user has borrows) - const collector = poolConfig.collector; - const principals = ud && ud.type === "active" ? ud.realPrincipals : null; - - let pythData = undefined; - if (principals) { - try { - const withdrawPrices = await collector.getPricesForWithdraw(principals, assetPoolCfg); - if (withdrawPrices && withdrawPrices.dataCell) { - pythData = { - priceData: withdrawPrices.binaryUpdate ?? withdrawPrices.dataCell, - targetFeeds: withdrawPrices.targetFeeds ?? [], - refAssets: withdrawPrices.refAssets ?? [], - publishGap: 60n, - maxStaleness: 180n, - minPublishTime: withdrawPrices.minPublishTime ? BigInt(withdrawPrices.minPublishTime) : 0n, - maxPublishTime: withdrawPrices.maxPublishTime ? BigInt(withdrawPrices.maxPublishTime) : 0n, - pythAddress: evaa.PYTH_ORACLE_MAINNET, - }; - } - } catch (e) { - log.warn("Pyth oracle prices unavailable for withdraw:", e?.message ?? e); - } - } - - const sender = createSender(contract, keyPair); - const openedMaster = client.open(new (POOL_MAP[poolKey].MasterClass)({ poolConfig })); - await openedMaster.getSync(); - - await openedMaster.sendWithdraw(client.provider(openedMaster.address), sender, toNano("0.5"), { - queryID: 0n, - includeUserCode: true, - asset: assetPoolCfg, - amount: withdrawAmount, - userAddress: wallet.address, - payload: Cell.EMPTY, - subaccountId: 0, - customPayloadSaturationFlag: false, - returnRepayRemainingsFlag: false, - pyth: pythData, - }); - - log.info(`Withdraw ${params.amount} ${params.asset} from ${poolKey} pool`); - return { - success: true, - data: { - pool: poolKey, - asset: params.asset, - amount: params.amount === "max" ? formatBalance(withdrawAmount, assetCfg.decimals) : params.amount, - wallet_address: wallet.address.toString(), - message: "Withdraw tx sent. Check position after ~15 seconds with evaa_user_position.", - }, - }; - } catch (err) { - return { success: false, error: String(err.message || err).slice(0, 500) }; - } - }, -}; - -// --------------------------------------------------------------------------- -// Tool 9: evaa_borrow -// --------------------------------------------------------------------------- - -const evaaBorrow = { - name: "evaa_borrow", - description: - "Borrow an asset from an EVAA pool against your collateral. Requires a prior supply.", - category: "action", - scope: "admin-only", - - parameters: { - type: "object", - properties: { - asset: { - type: "string", - description: "Asset name to borrow (e.g. 'USDT', 'TON')", - }, - amount: { - type: "string", - description: "Amount to borrow in human units (e.g. '500' for 500 USDT)", - }, - pool: { - type: "string", - enum: ["main", "lp", "alts", "stable"], - description: "Pool (default: main)", - }, - }, - required: ["asset", "amount"], - }, - - execute: async (params) => { - try { - const poolKey = resolvePool(params.pool); - const { master, poolConfig } = await getSyncedMaster(poolKey); - const data = master.data; - const { wallet, keyPair, client, contract } = await getWalletAndClient(); - - const borrowAsset = resolveAsset(poolConfig, params.asset); - const assetCfg = data.assetsConfig.get(borrowAsset.assetId); - const borrowAmount = parseAmount(params.amount, assetCfg.decimals); - - // Borrow = supply 0 TON + withdraw borrowed asset - // We use sendSupplyWithdraw: supply 0 of TON, withdraw borrowAmount of target asset - const collector = poolConfig.collector; - const prices = await getPrices(poolConfig); - - const userContract = master.getOpenedUserContract(client, wallet.address); - await userContract.getSync(data.assetsData, data.assetsConfig, prices.dict); - const ud = userContract.data; - const principals = ud && ud.type === "active" ? ud.realPrincipals : null; - - // Need supply asset (TON with 0 amount) for the supplyWithdraw message - const supplyAsset = resolveAsset(poolConfig, "TON"); - - let pythData = undefined; - if (principals) { - try { - const swPrices = await collector.getPricesForSupplyWithdraw(principals, supplyAsset, borrowAsset, true); - if (swPrices && swPrices.dataCell) { - pythData = { - priceData: swPrices.binaryUpdate ?? swPrices.dataCell, - targetFeeds: swPrices.targetFeeds ?? [], - refAssets: swPrices.refAssets ?? [], - publishGap: 60n, - maxStaleness: 180n, - minPublishTime: swPrices.minPublishTime ? BigInt(swPrices.minPublishTime) : 0n, - maxPublishTime: swPrices.maxPublishTime ? BigInt(swPrices.maxPublishTime) : 0n, - pythAddress: evaa.PYTH_ORACLE_MAINNET, - }; - } - } catch (e) { - log.warn("Pyth oracle prices unavailable for borrow (primary):", e?.message ?? e); - } - } - - // If no prices from principals path, get fresh prices - if (!pythData) { - try { - const freshPrices = await collector.getPrices(); - if (freshPrices && freshPrices.dataCell) { - pythData = { - priceData: freshPrices.binaryUpdate ?? freshPrices.dataCell, - targetFeeds: freshPrices.targetFeeds ?? [], - refAssets: freshPrices.refAssets ?? [], - publishGap: 60n, - maxStaleness: 180n, - minPublishTime: freshPrices.minPublishTime ? BigInt(freshPrices.minPublishTime) : 0n, - maxPublishTime: freshPrices.maxPublishTime ? BigInt(freshPrices.maxPublishTime) : 0n, - pythAddress: evaa.PYTH_ORACLE_MAINNET, - }; - } - } catch (e) { - log.warn("Pyth oracle prices unavailable for borrow (fallback):", e?.message ?? e); - } - } - - const sender = createSender(contract, keyPair); - const openedMaster = client.open(new (POOL_MAP[poolKey].MasterClass)({ poolConfig })); - await openedMaster.getSync(); - - await openedMaster.sendSupplyWithdraw(client.provider(openedMaster.address), sender, toNano("0.5"), { - queryID: 0n, - includeUserCode: true, - supplyAsset, - supplyAmount: 0n, - withdrawAsset: borrowAsset, - withdrawAmount: borrowAmount, - withdrawRecipient: wallet.address, - userAddress: wallet.address, - payload: Cell.EMPTY, - subaccountId: 0, - forwardAmount: undefined, - customPayloadSaturationFlag: false, - returnRepayRemainingsFlag: false, - tonForRepayRemainings: 0n, - pyth: pythData, - }); - - log.info(`Borrow ${params.amount} ${params.asset} from ${poolKey} pool`); - return { - success: true, - data: { - pool: poolKey, - asset: params.asset, - amount: params.amount, - wallet_address: wallet.address.toString(), - message: "Borrow tx sent. Check position after ~15 seconds with evaa_user_position.", - }, - }; - } catch (err) { - return { success: false, error: String(err.message || err).slice(0, 500) }; - } - }, -}; - -// --------------------------------------------------------------------------- -// Tool 10: evaa_repay -// --------------------------------------------------------------------------- - -const evaaRepay = { - name: "evaa_repay", - description: - "Repay borrowed assets to an EVAA lending pool. Reduces your debt.", - category: "action", - scope: "admin-only", - - parameters: { - type: "object", - properties: { - asset: { - type: "string", - description: "Asset name to repay (e.g. 'USDT', 'TON')", - }, - amount: { - type: "string", - description: "Amount to repay in human units (e.g. '100' for 100 USDT)", - }, - pool: { - type: "string", - enum: ["main", "lp", "alts", "stable"], - description: "Pool (default: main)", - }, - }, - required: ["asset", "amount"], - }, - - execute: async (params) => { - try { - const poolKey = resolvePool(params.pool); - const { master, poolConfig } = await getSyncedMaster(poolKey); - const data = master.data; - const { wallet, keyPair, client, contract } = await getWalletAndClient(); - - const assetPoolCfg = resolveAsset(poolConfig, params.asset); - const assetCfg = data.assetsConfig.get(assetPoolCfg.assetId); - const amount = parseAmount(params.amount, assetCfg.decimals); - const isTon = evaa.isTonAsset(assetPoolCfg); - - // Repay is done via supply (supplying the borrowed asset reduces debt) - const sender = createSender(contract, keyPair); - const value = isTon - ? toNano("0.3") + BigInt(amount) - : toNano("0.3"); - - const openedMaster = client.open(new (POOL_MAP[poolKey].MasterClass)({ poolConfig })); - await openedMaster.getSync(); - - await openedMaster.sendSupply(client.provider(openedMaster.address), sender, value, { - queryID: 0n, - includeUserCode: true, - amount: BigInt(amount), - userAddress: wallet.address, - asset: assetPoolCfg, - payload: Cell.EMPTY, - returnRepayRemainingsFlag: true, - customPayloadRecipient: null, - customPayloadSaturationFlag: false, - }); - - log.info(`Repay ${params.amount} ${params.asset} to ${poolKey} pool`); - return { - success: true, - data: { - pool: poolKey, - asset: params.asset, - amount: params.amount, - wallet_address: wallet.address.toString(), - message: "Repay tx sent. Excess will be returned. Check position after ~15 seconds with evaa_user_position.", - }, - }; - } catch (err) { - return { success: false, error: String(err.message || err).slice(0, 500) }; - } - }, -}; - -// --------------------------------------------------------------------------- -// Tool 11: evaa_liquidate -// --------------------------------------------------------------------------- - -const evaaLiquidate = { - name: "evaa_liquidate", - description: - "Liquidate an undercollateralized position. Repay a portion of the borrower's debt " + - "and receive collateral at a discount. Use evaa_liquidations first to check eligibility.", - category: "action", - scope: "admin-only", - - parameters: { - type: "object", - properties: { - borrower_address: { - type: "string", - description: "Address of the undercollateralized borrower", - }, - pool: { - type: "string", - enum: ["main", "lp", "alts", "stable"], - description: "Pool (default: main)", - }, - }, - required: ["borrower_address"], - }, - - execute: async (params) => { - try { - if (!ton.validateAddress(params.borrower_address)) { - return { success: false, error: `Invalid borrower address: ${params.borrower_address}` }; - } - const poolKey = resolvePool(params.pool); - const { master, poolConfig } = await getSyncedMaster(poolKey); - const data = master.data; - const { wallet, keyPair, client, contract } = await getWalletAndClient(); - const borrowerAddr = Address.parse(params.borrower_address); - - const prices = await getPrices(poolConfig); - const userContract = master.getOpenedUserContract(client, borrowerAddr); - await userContract.getSync(data.assetsData, data.assetsConfig, prices.dict); - - const ud = userContract.data; - if (!ud || ud.type === "inactive") { - return { success: false, error: "Borrower has no active position." }; - } - if (!ud.liquidationData?.liquidable) { - return { success: false, error: "Position is not liquidatable. Health factor: " + (ud.healthFactor?.toFixed(4) ?? "N/A") }; - } - - const liqData = ud.liquidationData; - const loanAsset = liqData.greatestLoanAsset; - const collateralAsset = liqData.greatestCollateralAsset; - const loanCfg = data.assetsConfig.get(loanAsset.assetId); - const isTonLoan = evaa.isTonAsset(loanAsset); - - // Get prices for liquidation - const collector = poolConfig.collector; - const liqPrices = await collector.getPricesForLiquidate(ud.realPrincipals); - - let pythData = undefined; - if (liqPrices && liqPrices.dataCell) { - pythData = { - priceData: liqPrices.binaryUpdate ?? liqPrices.dataCell, - targetFeeds: liqPrices.targetFeeds ?? [], - refAssets: liqPrices.refAssets ?? [], - publishGap: 60n, - maxStaleness: 180n, - minPublishTime: liqPrices.minPublishTime ? BigInt(liqPrices.minPublishTime) : 0n, - maxPublishTime: liqPrices.maxPublishTime ? BigInt(liqPrices.maxPublishTime) : 0n, - pythAddress: evaa.PYTH_ORACLE_MAINNET, - }; - } - - const sender = createSender(contract, keyPair); - const liquidationAmount = liqData.liquidationAmount; - const minCollateralAmount = liqData.minCollateralAmount; - - const value = isTonLoan - ? evaa.FEES.LIQUIDATION + BigInt(liquidationAmount) - : evaa.FEES.LIQUIDATION_JETTON; - - const openedMaster = client.open(new (POOL_MAP[poolKey].MasterClass)({ poolConfig })); - await openedMaster.getSync(); - - await openedMaster.sendLiquidation(client.provider(openedMaster.address), sender, value, { - queryID: 0n, - includeUserCode: true, - borrowerAddress: borrowerAddr, - asset: loanAsset, - collateralAsset: collateralAsset.assetId, - liquidationAmount, - minCollateralAmount, - liquidatorAddress: wallet.address, - payload: Cell.EMPTY, - subaccountId: 0, - customPayloadRecipient: null, - customPayloadSaturationFlag: false, - pyth: pythData, - }); - - log.info(`Liquidate ${params.borrower_address} in ${poolKey} pool, loan ${loanAsset.name}`); - return { - success: true, - data: { - pool: poolKey, - borrower: params.borrower_address, - loan_asset: loanAsset.name, - collateral_asset: collateralAsset.name, - liquidation_amount: formatBalance(liquidationAmount, loanCfg.decimals), - wallet_address: wallet.address.toString(), - message: "Liquidation tx sent. Check result after ~15 seconds.", - }, - }; - } catch (err) { - return { success: false, error: String(err.message || err).slice(0, 500) }; - } - }, -}; - - return [ - evaaMarkets, - evaaAssets, - evaaPrices, - evaaUserPosition, - evaaPredict, - evaaLiquidations, - evaaSupply, - evaaWithdraw, - evaaBorrow, - evaaRepay, - evaaLiquidate, - ]; -}; // end tools(sdk) diff --git a/plugins/evaa/manifest.json b/plugins/evaa/manifest.json deleted file mode 100644 index 56b7db5..0000000 --- a/plugins/evaa/manifest.json +++ /dev/null @@ -1,28 +0,0 @@ -{ - "id": "evaa", - "name": "EVAA Protocol", - "version": "1.0.0", - "description": "Lending and borrowing on TON -- supply, borrow, withdraw, repay, liquidate across 4 pools", - "author": { "name": "teleton", "url": "https://github.com/TONresistor" }, - "license": "MIT", - "entry": "index.js", - "teleton": ">=1.0.0", - "sdkVersion": ">=1.0.0", - "tools": [ - { "name": "evaa_markets", "description": "Market data: APY, utilization, TVL per asset" }, - { "name": "evaa_assets", "description": "Asset configs: collateral factor, liquidation threshold" }, - { "name": "evaa_prices", "description": "Current oracle prices for all assets" }, - { "name": "evaa_user_position", "description": "User position: balances, health factor, limits" }, - { "name": "evaa_predict", "description": "Simulate health factor impact of supply/withdraw/borrow/repay" }, - { "name": "evaa_liquidations", "description": "Check if a position is liquidatable" }, - { "name": "evaa_supply", "description": "Supply TON or jetton to earn interest" }, - { "name": "evaa_withdraw", "description": "Withdraw supplied assets" }, - { "name": "evaa_borrow", "description": "Borrow against collateral" }, - { "name": "evaa_repay", "description": "Repay borrowed assets" }, - { "name": "evaa_liquidate", "description": "Liquidate undercollateralized position" } - ], - "permissions": [], - "tags": ["defi", "ton", "lending", "borrowing", "liquidation"], - "repository": "https://github.com/TONresistor/teleton-plugins", - "funding": null -} diff --git a/plugins/evaa/package-lock.json b/plugins/evaa/package-lock.json deleted file mode 100644 index 89dee3c..0000000 --- a/plugins/evaa/package-lock.json +++ /dev/null @@ -1,602 +0,0 @@ -{ - "name": "teleton-plugin-evaa", - "version": "1.0.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "teleton-plugin-evaa", - "version": "1.0.0", - "dependencies": { - "@evaafi/sdk": "^0.9.5", - "@orbs-network/ton-access": "^2.3.3" - } - }, - "node_modules/@evaafi/sdk": { - "version": "0.9.5", - "resolved": "https://registry.npmjs.org/@evaafi/sdk/-/sdk-0.9.5.tgz", - "integrity": "sha512-N5JOLO4LarWv/v9iKKhyRCwLemgPzbFU1v509bs9104LeWeGzwE19ElWWy7K5Sb0FuvDO3ahpkyxqp3NBkuLAw==", - "license": "MIT", - "dependencies": { - "@pythnetwork/hermes-client": "^2.0.0", - "@pythnetwork/pyth-ton-js": "^0.1.2", - "@ton/ton": "14.0.0", - "dotenv": "16.4.5" - }, - "peerDependencies": { - "@ton/core": ">=0.56.0", - "@tonconnect/sdk": ">=3.0.0", - "crypto-js": ">=4.2.0" - } - }, - "node_modules/@orbs-network/ton-access": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/@orbs-network/ton-access/-/ton-access-2.3.3.tgz", - "integrity": "sha512-b1miCPts7wBG9JKYgzXIRZQm/LMy5Uk1mNK8NzlcXHL3HRHJkkFbuYJGuj3IkWCiIicW3Ipp4sYnn3Fwo4oB0g==", - "license": "MIT", - "dependencies": { - "isomorphic-fetch": "^3.0.0" - } - }, - "node_modules/@pythnetwork/hermes-client": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@pythnetwork/hermes-client/-/hermes-client-2.1.0.tgz", - "integrity": "sha512-XOtP5dvHfKNl+uvFzXCMI9OL7VdJ8eXsv5ahy6ZB3ArZ+UMM4U4OrPYQPwLvJwlpkBXpEqsJKlMawcoyCB+yMA==", - "license": "Apache-2.0", - "dependencies": { - "@zodios/core": "^10.9.6", - "eventsource": "^3.0.5", - "zod": "^3.23.8" - }, - "engines": { - "node": ">=22.14.0" - } - }, - "node_modules/@pythnetwork/pyth-ton-js": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/@pythnetwork/pyth-ton-js/-/pyth-ton-js-0.1.2.tgz", - "integrity": "sha512-i7h2LD06PqgbUH3ueHHXu9e+4N4xRGyfTaI2eA7Mfkw9MNZiL5CDSj1AJZHPOaqcP92YJzuDa4yPOod34dXL7A==", - "license": "Apache-2.0" - }, - "node_modules/@ton/core": { - "version": "0.63.1", - "resolved": "https://registry.npmjs.org/@ton/core/-/core-0.63.1.tgz", - "integrity": "sha512-hDWMjlKzc18W2E4OeV3hUP8ohRJNHPD4Wd1+AQJj8zshZyCRT0usrvnExgbNUTo/vntDqCGMzgYWbXxyaA+L4g==", - "license": "MIT", - "peer": true, - "peerDependencies": { - "@ton/crypto": ">=3.2.0" - } - }, - "node_modules/@ton/crypto": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/@ton/crypto/-/crypto-3.3.0.tgz", - "integrity": "sha512-/A6CYGgA/H36OZ9BbTaGerKtzWp50rg67ZCH2oIjV1NcrBaCK9Z343M+CxedvM7Haf3f/Ee9EhxyeTp0GKMUpA==", - "license": "MIT", - "peer": true, - "dependencies": { - "@ton/crypto-primitives": "2.1.0", - "jssha": "3.2.0", - "tweetnacl": "1.0.3" - } - }, - "node_modules/@ton/crypto-primitives": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@ton/crypto-primitives/-/crypto-primitives-2.1.0.tgz", - "integrity": "sha512-PQesoyPgqyI6vzYtCXw4/ZzevePc4VGcJtFwf08v10OevVJHVfW238KBdpj1kEDQkxWLeuNHEpTECNFKnP6tow==", - "license": "MIT", - "peer": true, - "dependencies": { - "jssha": "3.2.0" - } - }, - "node_modules/@ton/ton": { - "version": "14.0.0", - "resolved": "https://registry.npmjs.org/@ton/ton/-/ton-14.0.0.tgz", - "integrity": "sha512-xb2CY6U0AlHUKc7DV7xK/K4Gqn6YoR253yUrM2E7L5WegVFsDF0CQRUIfpYACCuj1oUywQc5J2oMolYNu/uGkA==", - "license": "MIT", - "dependencies": { - "axios": "^1.6.7", - "dataloader": "^2.0.0", - "symbol.inspect": "1.0.1", - "teslabot": "^1.3.0", - "zod": "^3.21.4" - }, - "peerDependencies": { - "@ton/core": ">=0.56.0", - "@ton/crypto": ">=3.2.0" - } - }, - "node_modules/@tonconnect/isomorphic-eventsource": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/@tonconnect/isomorphic-eventsource/-/isomorphic-eventsource-0.0.2.tgz", - "integrity": "sha512-B4UoIjPi0QkvIzZH5fV3BQLWrqSYABdrzZQSI9sJA9aA+iC0ohOzFwVVGXanlxeDAy1bcvPbb29f6sVUk0UnnQ==", - "license": "Apache-2.0", - "peer": true, - "dependencies": { - "eventsource": "^2.0.2" - } - }, - "node_modules/@tonconnect/isomorphic-eventsource/node_modules/eventsource": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-2.0.2.tgz", - "integrity": "sha512-IzUmBGPR3+oUG9dUeXynyNmf91/3zUSJg1lCktzKw47OXuhco54U3r9B7O4XX+Rb1Itm9OZ2b0RkTs10bICOxA==", - "license": "MIT", - "peer": true, - "engines": { - "node": ">=12.0.0" - } - }, - "node_modules/@tonconnect/isomorphic-fetch": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/@tonconnect/isomorphic-fetch/-/isomorphic-fetch-0.0.3.tgz", - "integrity": "sha512-jIg5nTrDwnite4fXao3dD83eCpTvInTjZon/rZZrIftIegh4XxyVb5G2mpMqXrVGk1e8SVXm3Kj5OtfMplQs0w==", - "license": "Apache-2.0", - "peer": true, - "dependencies": { - "node-fetch": "^2.6.9" - } - }, - "node_modules/@tonconnect/protocol": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/@tonconnect/protocol/-/protocol-2.4.0.tgz", - "integrity": "sha512-3xg6sMWIrSgW7/f7iPgOb2BI3LaMScjDMqfu20fcEMbOVvNFk3TGAUKK5cJ3pfvUINsyLgPoylcIbPap37jXiA==", - "license": "Apache-2.0", - "peer": true, - "dependencies": { - "tweetnacl": "^1.0.3", - "tweetnacl-util": "^0.15.1" - } - }, - "node_modules/@tonconnect/sdk": { - "version": "3.4.1", - "resolved": "https://registry.npmjs.org/@tonconnect/sdk/-/sdk-3.4.1.tgz", - "integrity": "sha512-eVH8erAFout89gXYHXua/es+mmLPiW1r7ng9hgHKYQv85HLq8/zzQEkbJpbTlnIuQ6CJ/QKchjMsIgYz3BYCUQ==", - "license": "Apache-2.0", - "peer": true, - "dependencies": { - "@tonconnect/isomorphic-eventsource": "0.0.2", - "@tonconnect/isomorphic-fetch": "0.0.3", - "@tonconnect/protocol": "2.4.0" - } - }, - "node_modules/@zodios/core": { - "version": "10.9.6", - "resolved": "https://registry.npmjs.org/@zodios/core/-/core-10.9.6.tgz", - "integrity": "sha512-aH4rOdb3AcezN7ws8vDgBfGboZMk2JGGzEq/DtW65MhnRxyTGRuLJRWVQ/2KxDgWvV2F5oTkAS+5pnjKbl0n+A==", - "license": "MIT", - "peerDependencies": { - "axios": "^0.x || ^1.0.0", - "zod": "^3.x" - } - }, - "node_modules/asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", - "license": "MIT" - }, - "node_modules/axios": { - "version": "1.13.5", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.5.tgz", - "integrity": "sha512-cz4ur7Vb0xS4/KUN0tPWe44eqxrIu31me+fbang3ijiNscE129POzipJJA6zniq2C/Z6sJCjMimjS8Lc/GAs8Q==", - "license": "MIT", - "dependencies": { - "follow-redirects": "^1.15.11", - "form-data": "^4.0.5", - "proxy-from-env": "^1.1.0" - } - }, - "node_modules/call-bind-apply-helpers": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", - "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/combined-stream": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", - "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "license": "MIT", - "dependencies": { - "delayed-stream": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/crypto-js": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.2.0.tgz", - "integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==", - "license": "MIT", - "peer": true - }, - "node_modules/dataloader": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/dataloader/-/dataloader-2.2.3.tgz", - "integrity": "sha512-y2krtASINtPFS1rSDjacrFgn1dcUuoREVabwlOGOe4SdxenREqwjwjElAdwvbGM7kgZz9a3KVicWR7vcz8rnzA==", - "license": "MIT" - }, - "node_modules/delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", - "license": "MIT", - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/dotenv": { - "version": "16.4.5", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz", - "integrity": "sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==", - "license": "BSD-2-Clause", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://dotenvx.com" - } - }, - "node_modules/dunder-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", - "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.1", - "es-errors": "^1.3.0", - "gopd": "^1.2.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-define-property": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", - "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-errors": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", - "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-object-atoms": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", - "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-set-tostringtag": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", - "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.6", - "has-tostringtag": "^1.0.2", - "hasown": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/eventsource": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz", - "integrity": "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==", - "license": "MIT", - "dependencies": { - "eventsource-parser": "^3.0.1" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/eventsource-parser": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.6.tgz", - "integrity": "sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg==", - "license": "MIT", - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/follow-redirects": { - "version": "1.15.11", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", - "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/RubenVerborgh" - } - ], - "license": "MIT", - "engines": { - "node": ">=4.0" - }, - "peerDependenciesMeta": { - "debug": { - "optional": true - } - } - }, - "node_modules/form-data": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", - "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", - "license": "MIT", - "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "es-set-tostringtag": "^2.1.0", - "hasown": "^2.0.2", - "mime-types": "^2.1.12" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/function-bind": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-intrinsic": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", - "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "es-define-property": "^1.0.1", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.1.1", - "function-bind": "^1.1.2", - "get-proto": "^1.0.1", - "gopd": "^1.2.0", - "has-symbols": "^1.1.0", - "hasown": "^2.0.2", - "math-intrinsics": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", - "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", - "license": "MIT", - "dependencies": { - "dunder-proto": "^1.0.1", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/gopd": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", - "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-symbols": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", - "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-tostringtag": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", - "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", - "license": "MIT", - "dependencies": { - "has-symbols": "^1.0.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/hasown": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", - "license": "MIT", - "dependencies": { - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/isomorphic-fetch": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/isomorphic-fetch/-/isomorphic-fetch-3.0.0.tgz", - "integrity": "sha512-qvUtwJ3j6qwsF3jLxkZ72qCgjMysPzDfeV240JHiGZsANBYd+EEuu35v7dfrJ9Up0Ak07D7GGSkGhCHTqg/5wA==", - "license": "MIT", - "dependencies": { - "node-fetch": "^2.6.1", - "whatwg-fetch": "^3.4.1" - } - }, - "node_modules/jssha": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/jssha/-/jssha-3.2.0.tgz", - "integrity": "sha512-QuruyBENDWdN4tZwJbQq7/eAK85FqrI4oDbXjy5IBhYD+2pTJyBUWZe8ctWaCkrV0gy6AaelgOZZBMeswEa/6Q==", - "license": "BSD-3-Clause", - "peer": true, - "engines": { - "node": "*" - } - }, - "node_modules/math-intrinsics": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", - "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "license": "MIT", - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/node-fetch": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", - "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", - "license": "MIT", - "dependencies": { - "whatwg-url": "^5.0.0" - }, - "engines": { - "node": "4.x || >=6.0.0" - }, - "peerDependencies": { - "encoding": "^0.1.0" - }, - "peerDependenciesMeta": { - "encoding": { - "optional": true - } - } - }, - "node_modules/proxy-from-env": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", - "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", - "license": "MIT" - }, - "node_modules/symbol.inspect": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/symbol.inspect/-/symbol.inspect-1.0.1.tgz", - "integrity": "sha512-YQSL4duoHmLhsTD1Pw8RW6TZ5MaTX5rXJnqacJottr2P2LZBF/Yvrc3ku4NUpMOm8aM0KOCqM+UAkMA5HWQCzQ==", - "license": "ISC" - }, - "node_modules/teslabot": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/teslabot/-/teslabot-1.5.0.tgz", - "integrity": "sha512-e2MmELhCgrgZEGo7PQu/6bmYG36IDH+YrBI1iGm6jovXkeDIGa3pZ2WSqRjzkuw2vt1EqfkZoV5GpXgqL8QJVg==", - "license": "MIT" - }, - "node_modules/tr46": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", - "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", - "license": "MIT" - }, - "node_modules/tweetnacl": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-1.0.3.tgz", - "integrity": "sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw==", - "license": "Unlicense", - "peer": true - }, - "node_modules/tweetnacl-util": { - "version": "0.15.1", - "resolved": "https://registry.npmjs.org/tweetnacl-util/-/tweetnacl-util-0.15.1.tgz", - "integrity": "sha512-RKJBIj8lySrShN4w6i/BonWp2Z/uxwC3h4y7xsRrpP59ZboCd0GpEVsOnMDYLMmKBpYhb5TgHzZXy7wTfYFBRw==", - "license": "Unlicense", - "peer": true - }, - "node_modules/webidl-conversions": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", - "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", - "license": "BSD-2-Clause" - }, - "node_modules/whatwg-fetch": { - "version": "3.6.20", - "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.6.20.tgz", - "integrity": "sha512-EqhiFU6daOA8kpjOWTL0olhVOF3i7OrFzSYiGsEMB8GcXS+RrzauAERX65xMeNWVqxA6HXH2m69Z9LaKKdisfg==", - "license": "MIT" - }, - "node_modules/whatwg-url": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", - "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", - "license": "MIT", - "dependencies": { - "tr46": "~0.0.3", - "webidl-conversions": "^3.0.0" - } - }, - "node_modules/zod": { - "version": "3.25.76", - "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", - "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/colinhacks" - } - } - } -} diff --git a/plugins/evaa/package.json b/plugins/evaa/package.json deleted file mode 100644 index f002ebe..0000000 --- a/plugins/evaa/package.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "name": "teleton-plugin-evaa", - "type": "module", - "version": "1.0.0", - "private": true, - "dependencies": { - "@evaafi/sdk": "^0.9.5", - "@orbs-network/ton-access": "^2.3.3" - } -} diff --git a/plugins/example-sdk/index.js b/plugins/example-sdk/index.js index fcdb085..21df08d 100644 --- a/plugins/example-sdk/index.js +++ b/plugins/example-sdk/index.js @@ -16,7 +16,7 @@ export const manifest = { name: "example-sdk", version: "1.0.0", - sdkVersion: ">=1.0.0", + sdkVersion: "^2.0.0", description: "SDK example โ€” greeting counter with TON balance check", defaultConfig: { greeting: "Hello", diff --git a/plugins/example-sdk/manifest.json b/plugins/example-sdk/manifest.json index 0949bbb..b2b1671 100644 --- a/plugins/example-sdk/manifest.json +++ b/plugins/example-sdk/manifest.json @@ -9,8 +9,8 @@ }, "license": "MIT", "entry": "index.js", - "teleton": ">=1.0.0", - "sdkVersion": ">=1.0.0", + "teleton": ">=0.9.0", + "sdkVersion": "^2.0.0", "tools": [ { "name": "sdk_greet", "description": "Greet a user and track greeting count" }, { "name": "sdk_balance", "description": "Check TON wallet balance and price" }, diff --git a/plugins/example/index.js b/plugins/example/index.js index 34223cc..e4b38dd 100644 --- a/plugins/example/index.js +++ b/plugins/example/index.js @@ -56,7 +56,6 @@ const diceRoll = { // // params -- the arguments the LLM chose, matching the schema above // context -- Teleton runtime: - // context.bridge TelegramBridge (send messages, reactions, media) // context.db SQLite database instance // context.chatId current chat ID // context.senderId Telegram user ID of whoever triggered this diff --git a/plugins/example/manifest.json b/plugins/example/manifest.json index 3783627..a9edd45 100644 --- a/plugins/example/manifest.json +++ b/plugins/example/manifest.json @@ -9,7 +9,7 @@ }, "license": "MIT", "entry": "index.js", - "teleton": ">=1.0.0", + "teleton": ">=0.9.0", "tools": [ { "name": "dice_roll", "description": "Roll configurable dice with sides, count, and modifier" }, { "name": "random_pick", "description": "Randomly pick one item from a list of choices" } diff --git a/plugins/fragment/index.js b/plugins/fragment/index.js index 6ef9051..bb8262b 100644 --- a/plugins/fragment/index.js +++ b/plugins/fragment/index.js @@ -257,7 +257,7 @@ function parseCollections(html) { export const manifest = { name: "fragment", version: "1.0.0", - sdkVersion: ">=1.0.0", + sdkVersion: "^2.0.0", description: "Search and browse Telegram's NFT marketplace โ€” usernames, numbers, collectible gifts, auction history", }; diff --git a/plugins/fragment/manifest.json b/plugins/fragment/manifest.json index cb63a05..f690de9 100644 --- a/plugins/fragment/manifest.json +++ b/plugins/fragment/manifest.json @@ -6,8 +6,8 @@ "author": { "name": "teleton", "url": "https://github.com/TONresistor" }, "license": "MIT", "entry": "index.js", - "teleton": ">=1.0.0", - "sdkVersion": ">=1.0.0", + "teleton": ">=0.9.0", + "sdkVersion": "^2.0.0", "tools": [ { "name": "fragment_search", "description": "Search usernames, numbers, or gifts on Fragment marketplace" }, { "name": "fragment_item", "description": "Get detailed info for a specific Fragment item (username, number, or gift)" }, diff --git a/plugins/gaspump/README.md b/plugins/gaspump/README.md index c2f7d68..0087562 100644 --- a/plugins/gaspump/README.md +++ b/plugins/gaspump/README.md @@ -1,5 +1,8 @@ # gaspump +> [!WARNING] +> Legacy SDK v1 plugin. It is quarantined and not installable from the SDK v2 marketplace. + Token launcher and trader for [Gas111](https://gas111.com) on TON -- create, trade, and monitor meme tokens. Auth is handled automatically via Telegram WebApp -- no manual tokens needed. diff --git a/plugins/gaspump/index.js b/plugins/gaspump/index.js index a5404a5..b2c8f84 100644 --- a/plugins/gaspump/index.js +++ b/plugins/gaspump/index.js @@ -653,6 +653,15 @@ const gasKing = { // Export // --------------------------------------------------------------------------- +// This legacy plugin requires raw Telegram WebApp auth and direct wallet +// signing. The explicit SDK v1 range makes SDK v2 reject it before tools load. +export const manifest = { + name: "gaspump", + version: "2.0.0", + sdkVersion: "^1.0.0", + description: "Launch, trade, and manage meme tokens on Gas111/TON.", +}; + export const tools = [ gasLaunchToken, gasTokenInfo, diff --git a/plugins/gaspump/manifest.json b/plugins/gaspump/manifest.json index eb2333a..ef2fb60 100644 --- a/plugins/gaspump/manifest.json +++ b/plugins/gaspump/manifest.json @@ -9,8 +9,8 @@ }, "license": "MIT", "entry": "index.js", - "teleton": ">=1.0.0", - "sdkVersion": ">=1.0.0", + "teleton": ">=0.9.0", + "sdkVersion": "^1.0.0", "tools": [ { "name": "gas_launch_token", "description": "Launch a new token (upload, deploy, register โ€” all in one)" }, { "name": "gas_token_info", "description": "Get token details and status" }, diff --git a/plugins/geckoterminal/index.js b/plugins/geckoterminal/index.js index 0d53d27..c7f3ad6 100644 --- a/plugins/geckoterminal/index.js +++ b/plugins/geckoterminal/index.js @@ -122,7 +122,7 @@ function makePoolListTool(name, description, pathSuffix, sdk) { export const manifest = { name: "geckoterminal", version: "1.0.0", - sdkVersion: ">=1.0.0", + sdkVersion: "^2.0.0", description: "TON DEX pool and token data -- trending, new, and top pools, trades, OHLCV, token info, batch prices", }; diff --git a/plugins/geckoterminal/manifest.json b/plugins/geckoterminal/manifest.json index aa3b863..0353da7 100644 --- a/plugins/geckoterminal/manifest.json +++ b/plugins/geckoterminal/manifest.json @@ -9,8 +9,8 @@ }, "license": "MIT", "entry": "index.js", - "teleton": ">=1.0.0", - "sdkVersion": ">=1.0.0", + "teleton": ">=0.9.0", + "sdkVersion": "^2.0.0", "tools": [ { "name": "gecko_trending_pools", "description": "Get trending pools on TON by activity" }, { "name": "gecko_new_pools", "description": "Get newly created pools on TON (last 48h)" }, diff --git a/plugins/giftindex/README.md b/plugins/giftindex/README.md index 6b26f67..880f90c 100644 --- a/plugins/giftindex/README.md +++ b/plugins/giftindex/README.md @@ -84,7 +84,4 @@ No parameters. Uses the agent's wallet automatically. Requires at runtime (provided by teleton): - `@ton/core` -- Cell building, address computation -- `@ton/ton` -- Wallet contract, TonClient -- `@ton/crypto` -- Mnemonic to private key - -Agent wallet at `~/.teleton/wallet.json` is used for signing all on-chain transactions. +- Teleton Plugin SDK v2 -- wallet address, jetton resolution, protected transaction broker diff --git a/plugins/giftindex/index.js b/plugins/giftindex/index.js index 2a1f035..d72c024 100644 --- a/plugins/giftindex/index.js +++ b/plugins/giftindex/index.js @@ -42,13 +42,13 @@ import { } from './guards.js'; // --------------------------------------------------------------------------- -// Export (SDK v1.0.0) +// Export (SDK v2) // --------------------------------------------------------------------------- export const manifest = { name: "giftindex", version: "2.0.0", - sdkVersion: ">=1.0.0", + sdkVersion: "^2.0.0", description: "GiftIndex ODROB trading โ€” monitor and trade the Telegram Gifts index on TON with workflow guardrails.", }; diff --git a/plugins/giftindex/manifest.json b/plugins/giftindex/manifest.json index 5de68ed..a1907e8 100644 --- a/plugins/giftindex/manifest.json +++ b/plugins/giftindex/manifest.json @@ -9,8 +9,8 @@ }, "license": "MIT", "entry": "index.js", - "teleton": ">=1.0.0", - "sdkVersion": ">=1.0.0", + "teleton": ">=0.9.0", + "sdkVersion": "^2.0.0", "secrets": { "tonapi_key": { "required": false, "description": "TONAPI access key (optional โ€” improves rate limits)" } }, diff --git a/plugins/giftindex/package-lock.json b/plugins/giftindex/package-lock.json deleted file mode 100644 index 2aeb1ef..0000000 --- a/plugins/giftindex/package-lock.json +++ /dev/null @@ -1,82 +0,0 @@ -{ - "name": "teleton-plugin-giftindex", - "version": "1.0.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "teleton-plugin-giftindex", - "version": "1.0.0", - "dependencies": { - "@orbs-network/ton-access": "^2.3.3" - } - }, - "node_modules/@orbs-network/ton-access": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/@orbs-network/ton-access/-/ton-access-2.3.3.tgz", - "integrity": "sha512-b1miCPts7wBG9JKYgzXIRZQm/LMy5Uk1mNK8NzlcXHL3HRHJkkFbuYJGuj3IkWCiIicW3Ipp4sYnn3Fwo4oB0g==", - "license": "MIT", - "dependencies": { - "isomorphic-fetch": "^3.0.0" - } - }, - "node_modules/isomorphic-fetch": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/isomorphic-fetch/-/isomorphic-fetch-3.0.0.tgz", - "integrity": "sha512-qvUtwJ3j6qwsF3jLxkZ72qCgjMysPzDfeV240JHiGZsANBYd+EEuu35v7dfrJ9Up0Ak07D7GGSkGhCHTqg/5wA==", - "license": "MIT", - "dependencies": { - "node-fetch": "^2.6.1", - "whatwg-fetch": "^3.4.1" - } - }, - "node_modules/node-fetch": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", - "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", - "license": "MIT", - "dependencies": { - "whatwg-url": "^5.0.0" - }, - "engines": { - "node": "4.x || >=6.0.0" - }, - "peerDependencies": { - "encoding": "^0.1.0" - }, - "peerDependenciesMeta": { - "encoding": { - "optional": true - } - } - }, - "node_modules/tr46": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", - "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", - "license": "MIT" - }, - "node_modules/webidl-conversions": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", - "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", - "license": "BSD-2-Clause" - }, - "node_modules/whatwg-fetch": { - "version": "3.6.20", - "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.6.20.tgz", - "integrity": "sha512-EqhiFU6daOA8kpjOWTL0olhVOF3i7OrFzSYiGsEMB8GcXS+RrzauAERX65xMeNWVqxA6HXH2m69Z9LaKKdisfg==", - "license": "MIT" - }, - "node_modules/whatwg-url": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", - "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", - "license": "MIT", - "dependencies": { - "tr46": "~0.0.3", - "webidl-conversions": "^3.0.0" - } - } - } -} diff --git a/plugins/giftindex/package.json b/plugins/giftindex/package.json deleted file mode 100644 index dcde384..0000000 --- a/plugins/giftindex/package.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "name": "teleton-plugin-giftindex", - "type": "module", - "version": "2.0.0", - "private": true, - "dependencies": { - "@orbs-network/ton-access": "^2.3.3" - } -} diff --git a/plugins/giftindex/trade.js b/plugins/giftindex/trade.js index c6d4587..f775cdf 100644 --- a/plugins/giftindex/trade.js +++ b/plugins/giftindex/trade.js @@ -11,35 +11,30 @@ * * Price scaling: 10^4 (price=10000 means $1.0000 USDT). * - * Dependencies (provided by teleton runtime): - * @ton/core, @ton/ton, @ton/crypto, @orbs-network/ton-access + * Dependency provided by teleton runtime: @ton/core */ -import { readFileSync, realpathSync } from "fs"; +import { realpathSync } from "fs"; import { createRequire } from "module"; -import { homedir } from "os"; -import { join } from "path"; // --------------------------------------------------------------------------- // TON dependencies (CJS packages -- use createRequire for ESM compat) // --------------------------------------------------------------------------- const require = createRequire(realpathSync(process.argv[1])); -const _pluginRequire = createRequire(import.meta.url); - -const { beginCell, Address, SendMode } = require("@ton/core"); -const { WalletContractV5R1, TonClient, toNano, internal } = require("@ton/ton"); -const { mnemonicToPrivateKey } = require("@ton/crypto"); +const { beginCell, Address } = require("@ton/core"); // --------------------------------------------------------------------------- // SDK logger // --------------------------------------------------------------------------- let _log = { info() {}, warn() {}, error() {} }; +let _ton = null; /** Initialize trade module with SDK logger. */ export function initTrade(sdk) { _log = sdk.log; + _ton = sdk.ton; } // --------------------------------------------------------------------------- @@ -79,58 +74,9 @@ const BID_OP = 0x00845746; /** Cancel order op */ const CANCEL_OP = 0x3567; -const WALLET_FILE = join(homedir(), ".teleton", "wallet.json"); - -// --------------------------------------------------------------------------- -// Wallet + client setup (same pattern as gaspump/deploy.js) -// --------------------------------------------------------------------------- - -async function getWalletAndClient() { - let walletData; - try { - walletData = JSON.parse(readFileSync(WALLET_FILE, "utf-8")); - } catch { - throw new Error("Agent wallet not found at " + WALLET_FILE); - } - if (!walletData.mnemonic || !Array.isArray(walletData.mnemonic)) { - throw new Error("Invalid wallet file: missing mnemonic"); - } - - const keyPair = await mnemonicToPrivateKey(walletData.mnemonic); - const wallet = WalletContractV5R1.create({ - workchain: 0, - publicKey: keyPair.publicKey, - }); - - let endpoint; - try { - const { getHttpEndpoint } = _pluginRequire("@orbs-network/ton-access"); - endpoint = await getHttpEndpoint({ network: "mainnet" }); - } catch { - endpoint = "https://toncenter.com/api/v2/jsonRPC"; - } - - const client = new TonClient({ endpoint }); - const contract = client.open(wallet); - - return { wallet, keyPair, client, contract }; -} - -/** - * Resolve the jetton wallet address for a given owner on a jetton master. - * - * @param {object} client TonClient instance - * @param {string} jettonMaster Jetton master contract address (raw or friendly) - * @param {string} ownerAddress Owner wallet address - * @returns {Promise
} - */ -async function resolveJettonWallet(client, jettonMaster, ownerAddress) { - const result = await client.runMethod( - Address.parse(jettonMaster), - "get_wallet_address", - [{ type: "slice", cell: beginCell().storeAddress(Address.parse(ownerAddress)).endCell() }], - ); - return result.stack.readAddress(); +function requireTon() { + if (!_ton) throw new Error("GiftIndex SDK is not initialized"); + return _ton; } // --------------------------------------------------------------------------- @@ -172,7 +118,7 @@ export function buildJettonTransferBody(queryId, amount, destination, forwardPay .storeAddress(destination) .storeUint(0, 2) // response_destination = addr_none .storeUint(0, 1) // no custom_payload - .storeCoins(toNano("0.1")) // forward_ton_amount + .storeCoins(100000000n) // forward_ton_amount .storeBit(true) // forward_payload present as ref .storeRef(forwardPayload) .endCell(); @@ -221,12 +167,14 @@ export function buildCancelBody(queryId, priority, orderType, traderAddress) { * @returns {Promise<{seqno: number, walletAddress: string, jettonWalletAddress: string}>} */ export async function placeAskOrder(orderBook, amount, price) { - const { wallet, keyPair, client, contract } = await getWalletAndClient(); - const ownerAddress = wallet.address.toString(); + const ton = requireTon(); + const ownerAddress = ton.getAddress(); + if (!ownerAddress) throw new Error("Agent wallet is not initialized"); // Resolve the user's index token jetton wallet (GHOLD or FLOOR depending on OB) const indexMaster = getIndexMaster(orderBook); - const jettonWallet = await resolveJettonWallet(client, indexMaster, ownerAddress); + const jettonWallet = await ton.getJettonWalletAddress(ownerAddress, indexMaster); + if (!jettonWallet) throw new Error("Could not resolve index token wallet"); const forwardPayload = buildForwardPayload(ASK_OP, price); const body = buildJettonTransferBody( @@ -236,26 +184,13 @@ export async function placeAskOrder(orderBook, amount, price) { forwardPayload, ); - const seqno = await contract.getSeqno(); - _log.info(`ASK order: seqno=${seqno}, ob=${orderBook}, price=${price}`); - await contract.sendTransfer({ - seqno, - secretKey: keyPair.secretKey, - sendMode: SendMode.PAY_GAS_SEPARATELY | SendMode.IGNORE_ERRORS, - messages: [ - internal({ - to: jettonWallet, - value: toNano("0.15"), - body, - bounce: true, - }), - ], - }); + const sent = await ton.send(jettonWallet, 0.15, { body, bounce: true, sendMode: 3 }); + _log.info(`ASK order: seqno=${sent.seqno}, ob=${orderBook}, price=${price}`); return { - seqno, + seqno: sent.seqno, walletAddress: ownerAddress, - jettonWalletAddress: jettonWallet.toString(), + jettonWalletAddress: jettonWallet, }; } @@ -272,11 +207,13 @@ export async function placeAskOrder(orderBook, amount, price) { * @returns {Promise<{seqno: number, walletAddress: string, jettonWalletAddress: string}>} */ export async function placeBidOrder(orderBook, amount, price) { - const { wallet, keyPair, client, contract } = await getWalletAndClient(); - const ownerAddress = wallet.address.toString(); + const ton = requireTon(); + const ownerAddress = ton.getAddress(); + if (!ownerAddress) throw new Error("Agent wallet is not initialized"); // Resolve the user's USDT jetton wallet - const usdtJettonWallet = await resolveJettonWallet(client, USDT_MASTER, ownerAddress); + const usdtJettonWallet = await ton.getJettonWalletAddress(ownerAddress, USDT_MASTER); + if (!usdtJettonWallet) throw new Error("Could not resolve USDT wallet"); const forwardPayload = buildForwardPayload(BID_OP, price); const body = buildJettonTransferBody( @@ -286,26 +223,13 @@ export async function placeBidOrder(orderBook, amount, price) { forwardPayload, ); - const seqno = await contract.getSeqno(); - _log.info(`BID order: seqno=${seqno}, ob=${orderBook}, price=${price}`); - await contract.sendTransfer({ - seqno, - secretKey: keyPair.secretKey, - sendMode: SendMode.PAY_GAS_SEPARATELY | SendMode.IGNORE_ERRORS, - messages: [ - internal({ - to: usdtJettonWallet, - value: toNano("0.15"), - body, - bounce: true, - }), - ], - }); + const sent = await ton.send(usdtJettonWallet, 0.15, { body, bounce: true, sendMode: 3 }); + _log.info(`BID order: seqno=${sent.seqno}, ob=${orderBook}, price=${price}`); return { - seqno, + seqno: sent.seqno, walletAddress: ownerAddress, - jettonWalletAddress: usdtJettonWallet.toString(), + jettonWalletAddress: usdtJettonWallet, }; } @@ -322,29 +246,17 @@ export async function placeBidOrder(orderBook, amount, price) { * @returns {Promise<{seqno: number, walletAddress: string}>} */ export async function cancelOrder(orderBook, queryId, priority, orderType) { - const { wallet, keyPair, contract } = await getWalletAndClient(); - const ownerAddress = wallet.address.toString(); + const ton = requireTon(); + const ownerAddress = ton.getAddress(); + if (!ownerAddress) throw new Error("Agent wallet is not initialized"); const body = buildCancelBody(BigInt(queryId), priority, orderType, ownerAddress); - const seqno = await contract.getSeqno(); - _log.info(`CANCEL order: seqno=${seqno}, ob=${orderBook}, type=${orderType}`); - await contract.sendTransfer({ - seqno, - secretKey: keyPair.secretKey, - sendMode: SendMode.PAY_GAS_SEPARATELY | SendMode.IGNORE_ERRORS, - messages: [ - internal({ - to: Address.parse(orderBook), - value: toNano("0.1"), - body, - bounce: true, - }), - ], - }); + const sent = await ton.send(orderBook, 0.1, { body, bounce: true, sendMode: 3 }); + _log.info(`CANCEL order: seqno=${sent.seqno}, ob=${orderBook}, type=${orderType}`); return { - seqno, + seqno: sent.seqno, walletAddress: ownerAddress, }; } @@ -363,13 +275,13 @@ export async function cancelOrder(orderBook, queryId, priority, orderType) { * @returns {{ confirmed: boolean, newSeqno?: number, elapsed: number }} */ export async function verifySeqnoAdvanced(expectedSeqno, maxWaitMs = 25000, intervalMs = 3000) { - const { contract } = await getWalletAndClient(); + const ton = requireTon(); const start = Date.now(); while (Date.now() - start < maxWaitMs) { await new Promise((r) => setTimeout(r, intervalMs)); try { - const current = await contract.getSeqno(); + const current = await ton.getSeqno(); if (current > expectedSeqno) { return { confirmed: true, newSeqno: current, elapsed: Date.now() - start }; } diff --git a/plugins/giftstat/index.js b/plugins/giftstat/index.js index 54a9294..1f5ae36 100644 --- a/plugins/giftstat/index.js +++ b/plugins/giftstat/index.js @@ -75,7 +75,7 @@ function makePaginatedTool(name, description, path, sdk) { export const manifest = { name: "giftstat", version: "1.0.1", - sdkVersion: ">=1.0.0", + sdkVersion: "^2.0.0", description: "Telegram gift market data -- collections, floor prices, models, stats, history", }; diff --git a/plugins/giftstat/manifest.json b/plugins/giftstat/manifest.json index a82aa17..37872fa 100644 --- a/plugins/giftstat/manifest.json +++ b/plugins/giftstat/manifest.json @@ -9,8 +9,8 @@ }, "license": "MIT", "entry": "index.js", - "teleton": ">=1.0.0", - "sdkVersion": ">=1.0.0", + "teleton": ">=0.9.0", + "sdkVersion": "^2.0.0", "tools": [ { "name": "gift_collections", "description": "List all Telegram gift collections with supply and pricing data" }, { "name": "gift_floor_prices", "description": "Get current floor prices by marketplace" }, diff --git a/plugins/multisend/README.md b/plugins/multisend/README.md index 637da93..70f07b9 100644 --- a/plugins/multisend/README.md +++ b/plugins/multisend/README.md @@ -1,34 +1,25 @@ # Multisend -Batch send TON and jettons to up to 254 recipients in a single transaction via Highload Wallet v3. Ideal for airdrops, mass payments, and rewards distribution. +Batch send TON and jettons to up to 254 recipients through Highload Wallet v3. Ideal for airdrops, mass payments, and rewards distribution. | Tool | Description | |------|-------------| -| `multisend_info` | Multisend wallet address, balance, deployment status, sequence state | -| `multisend_fund` | Transfer TON from the agent wallet (V5R1) to fund the multisend wallet | +| `multisend_info` | Highload wallet address, balance, deployment, and query sequence | +| `multisend_fund` | Fund the Highload wallet from the agent's main wallet | | `multisend_batch_ton` | Send TON to up to 254 recipients in one transaction | | `multisend_batch_jetton` | Send jettons to up to 254 recipients in one transaction | -| `multisend_status` | On-chain wallet state: timeout, last cleanup, subwallet ID | +| `multisend_status` | Highload timeout, cleanup, subwallet, balance, and sequence state | ## Architecture -This plugin uses a **two-wallet system**: - -1. **Agent wallet** (WalletContractV5R1) -- your main wallet at `~/.teleton/wallet.json` -2. **Multisend wallet** (HighloadWalletV3) -- a separate contract derived from the same mnemonic, at a different address - -The multisend wallet can send up to 254 messages in a single transaction, making batch operations ~254x more efficient than sending individually. It auto-deploys on first use (just needs pre-funding). - -**Sequence persistence**: Query IDs are stored in `~/.teleton/multisend-sequence.json` to prevent replay collisions. +The agent's main Wallet V5 and the Highload Wallet v3 keep their original distinct addresses. Teleton core derives both from the configured wallet, owns signing, serializes broadcasts, and persists Highload query IDs in private global state. The plugin never receives the mnemonic, private key, or sequence store. ## First use -1. **Check address**: `multisend_info` shows the multisend wallet address and balance -2. **Fund it**: `multisend_fund` transfers TON from the agent wallet to the multisend address -3. **First batch**: `multisend_batch_ton` auto-deploys the contract on the first call -4. **Jetton batches**: Transfer jettons to the multisend wallet, then use `multisend_batch_jetton` - -The multisend wallet auto-deploys when the first batch is sent -- no separate deploy step needed. Forward fees are ~2x a normal wallet (external + internal self-message), but this is offset by batching up to 254 operations. +1. Use `multisend_info` to inspect the Highload address. +2. Fund it with `multisend_fund`. +3. Send a first TON batch to deploy the Highload contract. +4. For jetton batches, transfer jettons to that Highload address first. ## Install @@ -37,8 +28,6 @@ mkdir -p ~/.teleton/plugins cp -r plugins/multisend ~/.teleton/plugins/ ``` -Requires `@tonkite/highload-wallet-v3` installed in the teleton runtime's `node_modules/`. - ## Usage examples - "Show my multisend wallet info" @@ -55,9 +44,9 @@ No parameters. ### multisend_fund -| Param | Type | Required | Default | Description | -|-------|------|----------|---------|-------------| -| `amount` | string | Yes | -- | Amount in TON to transfer (e.g. "5" or "0.5") | +| Param | Type | Required | Description | +|-------|------|----------|-------------| +| `amount` | string | Yes | Amount in TON to fund the Highload wallet | ### multisend_batch_ton diff --git a/plugins/multisend/index.js b/plugins/multisend/index.js index dcc41f2..9f2aa29 100644 --- a/plugins/multisend/index.js +++ b/plugins/multisend/index.js @@ -1,565 +1,300 @@ -/** - * Multisend plugin -- batch TON & jetton transfers via Highload Wallet v3 - * - * Send TON or jettons to up to 254 recipients in a single transaction. - * Uses @tonkite/highload-wallet-v3 for on-chain batch operations. - * Agent wallet at ~/.teleton/wallet.json provides the signing key. - */ +/** Batch TON and jetton transfers through Teleton's core Highload Wallet v3 broker. */ import { createRequire } from "node:module"; -import { readFileSync, realpathSync } from "node:fs"; -import { join } from "node:path"; -import { homedir } from "node:os"; +import { realpathSync } from "node:fs"; -// --------------------------------------------------------------------------- -// CJS dependencies -// --------------------------------------------------------------------------- +const require = createRequire(realpathSync(process.argv[1])); +const { Address, beginCell } = require("@ton/core"); -const _require = createRequire(realpathSync(process.argv[1])); // core: @ton/core, @ton/ton, @ton/crypto -const _pluginRequire = createRequire(import.meta.url); // local: plugin-specific deps - -const { HighloadWalletV3 } = _pluginRequire("@tonkite/highload-wallet-v3"); -const { Address, SendMode, beginCell } = _require("@ton/core"); -const { WalletContractV5R1, TonClient, toNano, internal } = _require("@ton/ton"); -const { mnemonicToPrivateKey } = _require("@ton/crypto"); - -// --------------------------------------------------------------------------- -// Constants -// --------------------------------------------------------------------------- - -const WALLET_FILE = join(homedir(), ".teleton", "wallet.json"); - -// --------------------------------------------------------------------------- -// Database migration -// --------------------------------------------------------------------------- +const MAX_RECIPIENTS = 254; export function migrate(db) { db.exec(` CREATE TABLE IF NOT EXISTS multisend_sequence ( id INTEGER PRIMARY KEY CHECK (id = 1), last_query_id TEXT NOT NULL - ); + ) `); } -// --------------------------------------------------------------------------- -// Helpers -// --------------------------------------------------------------------------- - -/** Resolve TON RPC endpoint. */ -async function getEndpoint() { - try { - const { getHttpEndpoint } = _pluginRequire("@orbs-network/ton-access"); - return await getHttpEndpoint({ network: "mainnet" }); - } catch { - return "https://toncenter.com/api/v2/jsonRPC"; - } -} +export const manifest = { + name: "multisend", + version: "2.0.0", + sdkVersion: "^2.1.0", + description: "Batch TON and jetton transfers through Teleton's protected Highload Wallet v3 broker.", +}; -/** Create TonClient instance. */ -async function getClient() { - const endpoint = await getEndpoint(); - return new TonClient({ endpoint }); +function formatError(error) { + return { success: false, error: String(error?.message || error).slice(0, 500) }; } -/** Load agent mnemonic and derive keypair. */ -async function getWalletData() { - let raw; - try { - raw = JSON.parse(readFileSync(WALLET_FILE, "utf-8")); - } catch { - throw new Error("Agent wallet not found at " + WALLET_FILE); +function validateRecipients(ton, recipients) { + if (!Array.isArray(recipients) || recipients.length === 0) { + throw new Error("recipients must be a non-empty array"); } - if (!raw.mnemonic || !Array.isArray(raw.mnemonic)) { - throw new Error("Invalid wallet file: missing mnemonic array"); + if (recipients.length > MAX_RECIPIENTS) { + throw new Error(`Maximum ${MAX_RECIPIENTS} recipients per batch`); } - const keyPair = await mnemonicToPrivateKey(raw.mnemonic); - return { keyPair, mnemonic: raw.mnemonic }; -} - -/** Create HighloadWalletV3 instance with persisted sequence. */ -async function getMultisendWallet(db) { - const { keyPair } = await getWalletData(); - - let sequence; - const row = db.prepare("SELECT last_query_id FROM multisend_sequence WHERE id = 1").get(); - if (row) { - sequence = HighloadWalletV3.restoreSequence(row.last_query_id); - } else { - sequence = HighloadWalletV3.newSequence(); + for (const recipient of recipients) { + if (!ton.validateAddress(recipient.address)) { + throw new Error(`Invalid recipient address: ${recipient.address}`); + } } - - const wallet = new HighloadWalletV3(sequence, keyPair.publicKey); - return { wallet, keyPair, sequence }; } -/** Persist sequence state to database. */ -function saveSequence(db, sequence) { - db.prepare( - "INSERT INTO multisend_sequence (id, last_query_id) VALUES (1, ?) ON CONFLICT(id) DO UPDATE SET last_query_id = excluded.last_query_id" - ).run(sequence.current()); +function parsePositiveAmount(value, label) { + const amount = Number(value); + if (!Number.isFinite(amount) || amount <= 0) throw new Error(`Invalid ${label}: ${value}`); + return amount; } -/** Format nanotons to human-readable TON string. */ -function formatTON(nano) { - const n = typeof nano === "bigint" ? nano : BigInt(nano); - const whole = n / 1000000000n; - const frac = (n % 1000000000n).toString().padStart(9, "0").replace(/0+$/, ""); - return frac ? `${whole}.${frac}` : `${whole}`; +function toUnits(value, decimals) { + const input = String(value).trim(); + if (!/^\d+(?:\.\d+)?$/.test(input)) throw new Error(`Invalid jetton amount: ${value}`); + const [whole, fraction = ""] = input.split("."); + if (fraction.length > decimals) { + throw new Error(`Jetton amount ${value} has more than ${decimals} decimals`); + } + return BigInt(whole) * 10n ** BigInt(decimals) + BigInt(fraction.padEnd(decimals, "0") || "0"); } -// --------------------------------------------------------------------------- -// Export (SDK v1.0.0) -// --------------------------------------------------------------------------- - -export const manifest = { - name: "multisend", - version: "1.0.0", - sdkVersion: ">=1.0.0", - description: "Batch TON and jetton transfers via Highload Wallet v3 โ€” send to up to 254 recipients in a single transaction.", -}; - export const tools = (sdk) => { - const { db, log, ton } = sdk; - -// --------------------------------------------------------------------------- -// Tool 1: multisend_info -// --------------------------------------------------------------------------- - -const multisendInfo = { - name: "multisend_info", - description: - "Show the multisend wallet address derived from the agent mnemonic, its TON balance, deployment status, and query sequence state. Use to check if the multisend wallet exists and is funded.", - category: "data-bearing", - scope: "always", - - parameters: { - type: "object", - properties: {}, - required: [], - }, - - execute: async (_params, _context) => { - try { - const { wallet, sequence } = await getMultisendWallet(db); - const client = await getClient(); - const balance = await client.getBalance(wallet.address); - const state = await client.getContractState(wallet.address); - const deployed = state.state === "active"; - - const seqInfo = { lastQueryId: sequence.current(), hasNext: sequence.hasNext() }; - const row = db.prepare("SELECT last_query_id FROM multisend_sequence WHERE id = 1").get(); - if (row) seqInfo.savedQueryId = row.last_query_id; - - return { - success: true, - data: { - address: wallet.address.toString({ bounceable: !deployed }), - address_raw: wallet.address.toRawString(), - balance: formatTON(balance), - balance_nano: balance.toString(), - deployed, - sequence: seqInfo, - }, - }; - } catch (err) { - return { success: false, error: String(err.message || err).slice(0, 500) }; - } - }, -}; - -// --------------------------------------------------------------------------- -// Tool 2: multisend_fund -// --------------------------------------------------------------------------- - -const multisendFund = { - name: "multisend_fund", - description: - "Transfer TON from the agent's main wallet (V5R1) to the multisend wallet to fund it for batch operations.", - category: "action", - scope: "admin-only", - - parameters: { - type: "object", - properties: { - amount: { - type: "string", - description: "Amount in TON to transfer (e.g. '5' or '0.5')", - }, + const multisendInfo = { + name: "multisend_info", + description: "Show the Highload multisend wallet address, balance, deployment, and query sequence.", + category: "data-bearing", + scope: "always", + parameters: { type: "object", properties: {}, required: [] }, + execute: async () => { + try { + const info = await sdk.ton.highload.getInfo(); + return { + success: true, + data: { + address: info.address, + address_raw: info.rawAddress, + balance: info.balance, + balance_nano: info.balanceNano, + deployed: info.deployed, + sequence: { + lastQueryId: info.currentQueryId, + savedQueryId: info.currentQueryId, + hasNext: info.hasNext, + }, + }, + }; + } catch (error) { + return formatError(error); + } }, - required: ["amount"], - }, - - execute: async (params, _context) => { - try { - const { wallet: multisendWallet, keyPair } = await getMultisendWallet(db); - - const v5Wallet = WalletContractV5R1.create({ - workchain: 0, - publicKey: keyPair.publicKey, - }); - - const client = await getClient(); - const contract = client.open(v5Wallet); - const seqno = await contract.getSeqno(); - - // Check if multisend wallet is deployed to set bounce correctly - const state = await client.getContractState(multisendWallet.address); - const deployed = state.state === "active"; - - await contract.sendTransfer({ - seqno, - secretKey: keyPair.secretKey, - sendMode: SendMode.PAY_GAS_SEPARATELY | SendMode.IGNORE_ERRORS, - messages: [ - internal({ - to: multisendWallet.address, - value: toNano(params.amount), - bounce: deployed, - }), - ], - }); - - return { - success: true, - data: { - seqno, - from: v5Wallet.address.toString({ bounceable: false }), - to: multisendWallet.address.toString({ bounceable: !deployed }), - amount: params.amount, - bounce: deployed, - }, - }; - } catch (err) { - return { success: false, error: String(err.message || err).slice(0, 500) }; - } - }, -}; - -// --------------------------------------------------------------------------- -// Tool 3: multisend_batch_ton -// --------------------------------------------------------------------------- - -const multisendBatchTon = { - name: "multisend_batch_ton", - description: - "Send TON to up to 254 recipients in a single transaction via the multisend wallet. Ideal for airdrops, mass payments, and rewards distribution.", - category: "action", - scope: "admin-only", - - parameters: { - type: "object", - properties: { - recipients: { - type: "array", - description: "List of recipients (max 254)", - maxItems: 254, - items: { - type: "object", - properties: { - address: { type: "string", description: "Recipient TON address" }, - amount: { type: "string", description: "Amount in TON" }, - memo: { type: "string", description: "Comment to attach (optional)" }, + }; + + const multisendFund = { + name: "multisend_fund", + description: "Fund the Highload multisend wallet from the agent's main wallet.", + category: "action", + scope: "admin-only", + parameters: { + type: "object", + properties: { amount: { type: "string", description: "Amount in TON" } }, + required: ["amount"], + }, + execute: async (params) => { + try { + const amount = parsePositiveAmount(params.amount, "amount"); + const info = await sdk.ton.highload.getInfo(); + const result = await sdk.ton.highload.fund(amount); + return { + success: true, + data: { + from: sdk.ton.getAddress(), + to: info.address, + amount: params.amount, + bounce: info.deployed, + ...result, + }, + }; + } catch (error) { + return formatError(error); + } + }, + }; + + const multisendBatchTon = { + name: "multisend_batch_ton", + description: "Send TON to up to 254 recipients in one Highload Wallet v3 batch.", + category: "action", + scope: "admin-only", + parameters: { + type: "object", + properties: { + recipients: { + type: "array", + maxItems: MAX_RECIPIENTS, + items: { + type: "object", + properties: { + address: { type: "string" }, + amount: { type: "string", description: "Amount in TON" }, + memo: { type: "string" }, + }, + required: ["address", "amount"], }, - required: ["address", "amount"], }, }, + required: ["recipients"], }, - required: ["recipients"], - }, - - execute: async (params, _context) => { - try { - const recipients = params.recipients; - if (!Array.isArray(recipients) || recipients.length === 0) { - return { success: false, error: "recipients must be a non-empty array" }; - } - if (recipients.length > 254) { - return { success: false, error: "Maximum 254 recipients per batch" }; - } - - // Validate all recipient addresses - for (const r of recipients) { - if (!ton.validateAddress(r.address)) { - return { success: false, error: `Invalid recipient address: ${r.address}` }; + execute: async (params) => { + try { + validateRecipients(sdk.ton, params.recipients); + let total = 0; + const messages = params.recipients.map((recipient, index) => { + const amount = parsePositiveAmount(recipient.amount, `TON amount for recipient #${index + 1}`); + total += amount; + return { to: recipient.address, value: amount, body: recipient.memo, bounce: false }; + }); + const info = await sdk.ton.highload.getInfo(); + if (Number(info.balance) < total + 0.15) { + throw new Error(`Insufficient Highload balance: ${info.balance} TON, need ~${total + 0.15} TON`); } - } - - const { wallet, keyPair, sequence } = await getMultisendWallet(db); - const client = await getClient(); - - // Calculate total and verify balance (wallet auto-deploys on first sendBatch) - let totalNano = 0n; - for (const r of recipients) { - totalNano += toNano(r.amount); - } - const balance = await client.getBalance(wallet.address); - const gasBuffer = toNano("0.15"); - if (balance < totalNano + gasBuffer) { + const result = await sdk.ton.highload.sendMessages(messages, { valuePerBatch: 0.05 }); + sdk.log.info(`Highload TON batch sent to ${messages.length} recipients`); return { - success: false, - error: `Insufficient balance: ${formatTON(balance)} TON, need ~${formatTON(totalNano + gasBuffer)} TON (${formatTON(totalNano)} + gas)`, + success: true, + data: { + recipient_count: messages.length, + total_ton: total.toString(), + multisend_address: result.address, + query_id: result.nextQueryId, + submitted_query_id: result.queryId, + }, }; + } catch (error) { + return formatError(error); } - - // Build messages - const messages = recipients.map((r) => ({ - mode: SendMode.PAY_GAS_SEPARATELY, - message: internal({ - to: Address.parse(r.address), - value: toNano(r.amount), - body: r.memo - ? beginCell().storeUint(0, 32).storeStringTail(r.memo).endCell() - : undefined, - bounce: false, - }), - })); - - // Send batch (client.open auto-injects provider as first arg) - const opened = client.open(wallet); - await opened.sendBatch(keyPair.secretKey, { - messages, - createdAt: Math.floor(Date.now() / 1000) - 60, - valuePerBatch: toNano("0.05"), - }); - - // Advance sequence and persist - sequence.next(); - saveSequence(db, sequence); - log.info(`Batch TON sent to ${recipients.length} recipients, total ${formatTON(totalNano)} TON`); - - return { - success: true, - data: { - recipient_count: recipients.length, - total_ton: formatTON(totalNano), - multisend_address: wallet.address.toString({ bounceable: true }), - query_id: sequence.current(), - }, - }; - } catch (err) { - return { success: false, error: String(err.message || err).slice(0, 500) }; - } - }, -}; - -// --------------------------------------------------------------------------- -// Tool 4: multisend_batch_jetton -// --------------------------------------------------------------------------- - -const multisendBatchJetton = { - name: "multisend_batch_jetton", - description: - "Send a jetton (fungible token) to up to 254 recipients in a single transaction via the multisend wallet. All transfers go through the multisend wallet's jetton wallet contract.", - category: "action", - scope: "admin-only", - - parameters: { - type: "object", - properties: { - jetton_master: { - type: "string", - description: "Jetton master contract address", - }, - recipients: { - type: "array", - description: "List of recipients (max 254)", - maxItems: 254, - items: { - type: "object", - properties: { - address: { type: "string", description: "Recipient TON address" }, - amount: { type: "string", description: "Amount in human units" }, + }, + }; + + const multisendBatchJetton = { + name: "multisend_batch_jetton", + description: "Send one jetton to up to 254 recipients in one Highload Wallet v3 batch.", + category: "action", + scope: "admin-only", + parameters: { + type: "object", + properties: { + jetton_master: { type: "string" }, + recipients: { + type: "array", + maxItems: MAX_RECIPIENTS, + items: { + type: "object", + properties: { + address: { type: "string" }, + amount: { type: "string", description: "Amount in human units" }, + }, + required: ["address", "amount"], }, - required: ["address", "amount"], }, + decimals: { type: "integer", minimum: 0, maximum: 18 }, + forward_ton: { type: "string", description: "TON attached to each jetton transfer" }, }, - decimals: { - type: "integer", - description: "Jetton decimals (6 for USDT, 9 for most tokens)", - minimum: 0, - maximum: 18, - }, - forward_ton: { - type: "string", - description: "TON to attach per transfer for gas forwarding (default '0.05')", - }, + required: ["jetton_master", "recipients"], }, - required: ["jetton_master", "recipients"], - }, - - execute: async (params, _context) => { - try { - const recipients = params.recipients; - if (!Array.isArray(recipients) || recipients.length === 0) { - return { success: false, error: "recipients must be a non-empty array" }; - } - if (recipients.length > 254) { - return { success: false, error: "Maximum 254 recipients per batch" }; - } - - // Validate jetton master and all recipient addresses - if (!ton.validateAddress(params.jetton_master)) { - return { success: false, error: `Invalid jetton master address: ${params.jetton_master}` }; - } - for (const r of recipients) { - if (!ton.validateAddress(r.address)) { - return { success: false, error: `Invalid recipient address: ${r.address}` }; + execute: async (params) => { + try { + validateRecipients(sdk.ton, params.recipients); + if (!sdk.ton.validateAddress(params.jetton_master)) { + throw new Error(`Invalid jetton master address: ${params.jetton_master}`); } - } - - const { wallet, keyPair, sequence } = await getMultisendWallet(db); - const client = await getClient(); - - // Jetton batch requires the wallet to be deployed -- it must already hold - // jetton tokens, which means it was funded and deployed via a prior TON batch. - const walletState = await client.getContractState(wallet.address); - if (walletState.state !== "active") { - return { - success: false, - error: "Multisend wallet is not deployed. Fund it with multisend_fund, then send a TON batch (multisend_batch_ton) to trigger deployment. After that, transfer jettons to the multisend wallet before using this tool.", - }; - } - - // Resolve the multisend wallet's jetton wallet address - const jettonMaster = Address.parse(params.jetton_master); - const result = await client.runMethod(jettonMaster, "get_wallet_address", [ - { type: "slice", cell: beginCell().storeAddress(wallet.address).endCell() }, - ]); - const jettonWallet = result.stack.readAddress(); - - const decimals = params.decimals ?? 9; - const forwardTon = toNano(params.forward_ton ?? "0.05"); - - // Check TON balance covers gas (forwardTon per recipient + buffer) - const balance = await client.getBalance(wallet.address); - const gasNeeded = forwardTon * BigInt(recipients.length) + toNano("0.1"); - if (balance < gasNeeded) { + const info = await sdk.ton.highload.getInfo(); + if (!info.deployed) { + throw new Error("Highload wallet is not deployed. Fund it and send a TON batch first."); + } + const jettonWallet = await sdk.ton.getJettonWalletAddress(info.address, params.jetton_master); + if (!jettonWallet) throw new Error("Could not resolve the Highload jetton wallet"); + + const decimals = params.decimals ?? 9; + const forwardTon = parsePositiveAmount(params.forward_ton ?? "0.05", "forward_ton"); + const gasNeeded = forwardTon * params.recipients.length + 0.1; + if (Number(info.balance) < gasNeeded) { + throw new Error(`Insufficient Highload TON for gas: ${info.balance} TON, need ~${gasNeeded} TON`); + } + const responseAddress = Address.parse(info.address); + const queryBase = BigInt(Date.now()) * 1000n; + const messages = params.recipients.map((recipient, index) => { + const body = beginCell() + .storeUint(0x0f8a7ea5, 32) + .storeUint(queryBase + BigInt(index), 64) + .storeCoins(toUnits(recipient.amount, decimals)) + .storeAddress(Address.parse(recipient.address)) + .storeAddress(responseAddress) + .storeBit(false) + .storeCoins(1n) + .storeBit(false) + .endCell(); + return { to: jettonWallet, value: forwardTon, body, bounce: true }; + }); + const result = await sdk.ton.highload.sendMessages(messages, { valuePerBatch: 0.05 }); + sdk.log.info(`Highload jetton batch sent to ${messages.length} recipients`); return { - success: false, - error: `Insufficient TON for gas: ${formatTON(balance)} TON, need ~${formatTON(gasNeeded)} TON`, + success: true, + data: { + recipient_count: messages.length, + jetton_master: params.jetton_master, + jetton_wallet: jettonWallet, + multisend_address: result.address, + decimals, + query_id: result.nextQueryId, + submitted_query_id: result.queryId, + }, }; + } catch (error) { + return formatError(error); } - - // Build jetton transfer messages - const messages = recipients.map((r, i) => { - const parsedAmount = Number(r.amount); - if (!Number.isFinite(parsedAmount) || parsedAmount <= 0) { - throw new Error(`Invalid amount for recipient #${i + 1} (${r.address}): ${r.amount}`); - } - const jettonAmount = BigInt(Math.round(parsedAmount * 10 ** decimals)); - const body = beginCell() - .storeUint(0xf8a7ea5, 32) // op: jetton transfer - .storeUint(i, 64) // query_id - .storeCoins(jettonAmount) // jetton amount - .storeAddress(Address.parse(r.address)) // destination - .storeAddress(wallet.address) // response_destination (excess back to multisend) - .storeBit(false) // no custom payload - .storeCoins(1n) // forward_ton_amount (1 nanoton for notification) - .storeBit(false) // no forward payload - .endCell(); - + }, + }; + + const multisendStatus = { + name: "multisend_status", + description: "Check Highload wallet balance, deployment, timeout, cleanup, and subwallet state.", + category: "data-bearing", + scope: "always", + parameters: { type: "object", properties: {}, required: [] }, + execute: async () => { + try { + const info = await sdk.ton.highload.getInfo(); return { - mode: SendMode.PAY_GAS_SEPARATELY, - message: internal({ - to: jettonWallet, - value: forwardTon, - body, - bounce: true, - }), + success: true, + data: { + address: info.address, + address_raw: info.rawAddress, + balance: info.balance, + balance_nano: info.balanceNano, + deployed: info.deployed, + sequence: { + current_query_id: info.currentQueryId, + has_next: info.hasNext, + }, + timeout: info.timeout, + last_cleaned: info.lastCleaned, + last_cleaned_date: info.lastCleaned + ? new Date(info.lastCleaned * 1000).toISOString() + : undefined, + subwallet_id: info.subwalletId, + }, }; - }); - - // Send batch (client.open auto-injects provider as first arg) - const opened = client.open(wallet); - await opened.sendBatch(keyPair.secretKey, { - messages, - createdAt: Math.floor(Date.now() / 1000) - 60, - valuePerBatch: toNano("0.05"), - }); - - // Advance sequence and persist - sequence.next(); - saveSequence(db, sequence); - log.info(`Batch jetton sent to ${recipients.length} recipients, master ${jettonMaster.toString()}`); - - return { - success: true, - data: { - recipient_count: recipients.length, - jetton_master: jettonMaster.toString(), - jetton_wallet: jettonWallet.toString(), - multisend_address: wallet.address.toString({ bounceable: true }), - decimals, - query_id: sequence.current(), - }, - }; - } catch (err) { - return { success: false, error: String(err.message || err).slice(0, 500) }; - } - }, -}; - -// --------------------------------------------------------------------------- -// Tool 5: multisend_status -// --------------------------------------------------------------------------- - -const multisendStatus = { - name: "multisend_status", - description: - "Check the on-chain state of the multisend wallet: balance, timeout configuration, last cleanup timestamp, and subwallet ID.", - category: "data-bearing", - scope: "always", - - parameters: { - type: "object", - properties: {}, - required: [], - }, - - execute: async (_params, _context) => { - try { - const { wallet, sequence } = await getMultisendWallet(db); - const client = await getClient(); - const balance = await client.getBalance(wallet.address); - const contractState = await client.getContractState(wallet.address); - const deployed = contractState.state === "active"; - - const data = { - address: wallet.address.toString({ bounceable: !deployed }), - address_raw: wallet.address.toRawString(), - balance: formatTON(balance), - balance_nano: balance.toString(), - deployed, - sequence: { - current_query_id: sequence.current(), - has_next: sequence.hasNext(), - }, - }; - - // Read on-chain state if deployed - if (deployed) { - const opened = client.open(wallet); - try { - data.timeout = await opened.getTimeout(); - } catch { /* method may not exist on older versions */ } - try { - data.last_cleaned = await opened.getLastCleaned(); - if (data.last_cleaned > 0) { - data.last_cleaned_date = new Date(data.last_cleaned * 1000).toISOString(); - } - } catch { /* ignore */ } - try { - data.subwallet_id = await opened.getSubwalletId(); - } catch { /* ignore */ } + } catch (error) { + return formatError(error); } - - return { success: true, data }; - } catch (err) { - return { success: false, error: String(err.message || err).slice(0, 500) }; - } - }, + }, + }; + + return [ + multisendInfo, + multisendFund, + multisendBatchTon, + multisendBatchJetton, + multisendStatus, + ]; }; - - return [multisendInfo, multisendFund, multisendBatchTon, multisendBatchJetton, multisendStatus]; -}; // end tools(sdk) diff --git a/plugins/multisend/manifest.json b/plugins/multisend/manifest.json index d1e7cde..b56295a 100644 --- a/plugins/multisend/manifest.json +++ b/plugins/multisend/manifest.json @@ -1,19 +1,19 @@ { "id": "multisend", "name": "Multisend", - "version": "1.0.0", - "description": "Batch send TON and jettons to up to 254 recipients in a single transaction", + "version": "2.0.0", + "description": "Batch send TON and jettons to up to 254 recipients through Teleton's protected Highload Wallet v3 broker", "author": { "name": "teleton", "url": "https://github.com/TONresistor" }, "license": "MIT", "entry": "index.js", - "teleton": ">=1.0.0", - "sdkVersion": ">=1.0.0", + "teleton": ">=0.9.0", + "sdkVersion": "^2.1.0", "tools": [ - { "name": "multisend_info", "description": "Multisend wallet address, balance, deployment status" }, - { "name": "multisend_fund", "description": "Fund the multisend wallet from agent wallet" }, - { "name": "multisend_batch_ton", "description": "Send TON to up to 254 recipients in one TX" }, - { "name": "multisend_batch_jetton", "description": "Send jettons to up to 254 recipients in one TX" }, - { "name": "multisend_status", "description": "On-chain wallet state and sequence info" } + { "name": "multisend_info", "description": "Highload multisend wallet address, balance, deployment status, and sequence" }, + { "name": "multisend_fund", "description": "Fund the Highload multisend wallet from the agent wallet" }, + { "name": "multisend_batch_ton", "description": "Send TON to up to 254 recipients in one Highload Wallet v3 batch" }, + { "name": "multisend_batch_jetton", "description": "Send jettons to up to 254 recipients in one Highload Wallet v3 batch" }, + { "name": "multisend_status", "description": "Highload wallet state, timeout, cleanup, and subwallet info" } ], "permissions": [], "tags": ["batch", "ton", "airdrop", "wallet", "transfer"], diff --git a/plugins/multisend/package-lock.json b/plugins/multisend/package-lock.json deleted file mode 100644 index e34408c..0000000 --- a/plugins/multisend/package-lock.json +++ /dev/null @@ -1,152 +0,0 @@ -{ - "name": "teleton-plugin-multisend", - "version": "1.0.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "teleton-plugin-multisend", - "version": "1.0.0", - "dependencies": { - "@orbs-network/ton-access": "^2.3.3", - "@tonkite/highload-wallet-v3": "^2.0.11" - } - }, - "node_modules/@orbs-network/ton-access": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/@orbs-network/ton-access/-/ton-access-2.3.3.tgz", - "integrity": "sha512-b1miCPts7wBG9JKYgzXIRZQm/LMy5Uk1mNK8NzlcXHL3HRHJkkFbuYJGuj3IkWCiIicW3Ipp4sYnn3Fwo4oB0g==", - "license": "MIT", - "dependencies": { - "isomorphic-fetch": "^3.0.0" - } - }, - "node_modules/@ton/core": { - "version": "0.56.3", - "resolved": "https://registry.npmjs.org/@ton/core/-/core-0.56.3.tgz", - "integrity": "sha512-HVkalfqw8zqLLPehtq0CNhu5KjVzc7IrbDwDHPjGoOSXmnqSobiWj8a5F+YuWnZnEbQKtrnMGNOOjVw4LG37rg==", - "license": "MIT", - "peer": true, - "dependencies": { - "symbol.inspect": "1.0.1" - }, - "peerDependencies": { - "@ton/crypto": ">=3.2.0" - } - }, - "node_modules/@ton/crypto": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/@ton/crypto/-/crypto-3.3.0.tgz", - "integrity": "sha512-/A6CYGgA/H36OZ9BbTaGerKtzWp50rg67ZCH2oIjV1NcrBaCK9Z343M+CxedvM7Haf3f/Ee9EhxyeTp0GKMUpA==", - "license": "MIT", - "peer": true, - "dependencies": { - "@ton/crypto-primitives": "2.1.0", - "jssha": "3.2.0", - "tweetnacl": "1.0.3" - } - }, - "node_modules/@ton/crypto-primitives": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@ton/crypto-primitives/-/crypto-primitives-2.1.0.tgz", - "integrity": "sha512-PQesoyPgqyI6vzYtCXw4/ZzevePc4VGcJtFwf08v10OevVJHVfW238KBdpj1kEDQkxWLeuNHEpTECNFKnP6tow==", - "license": "MIT", - "peer": true, - "dependencies": { - "jssha": "3.2.0" - } - }, - "node_modules/@tonkite/highload-wallet-v3": { - "version": "2.0.11", - "resolved": "https://registry.npmjs.org/@tonkite/highload-wallet-v3/-/highload-wallet-v3-2.0.11.tgz", - "integrity": "sha512-XwXGBCIPHzq+KCmYXxLyFz9GMwyE8hLjegHYZqLOryJZ2xu5wQbj3EXT436CHEPnFYX5ZspA6YwdoE1VB+XgJw==", - "license": "Apache-2.0", - "peerDependencies": { - "@ton/core": "^0.56.3", - "@ton/crypto": "^3.3.0" - } - }, - "node_modules/isomorphic-fetch": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/isomorphic-fetch/-/isomorphic-fetch-3.0.0.tgz", - "integrity": "sha512-qvUtwJ3j6qwsF3jLxkZ72qCgjMysPzDfeV240JHiGZsANBYd+EEuu35v7dfrJ9Up0Ak07D7GGSkGhCHTqg/5wA==", - "license": "MIT", - "dependencies": { - "node-fetch": "^2.6.1", - "whatwg-fetch": "^3.4.1" - } - }, - "node_modules/jssha": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/jssha/-/jssha-3.2.0.tgz", - "integrity": "sha512-QuruyBENDWdN4tZwJbQq7/eAK85FqrI4oDbXjy5IBhYD+2pTJyBUWZe8ctWaCkrV0gy6AaelgOZZBMeswEa/6Q==", - "license": "BSD-3-Clause", - "peer": true, - "engines": { - "node": "*" - } - }, - "node_modules/node-fetch": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", - "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", - "license": "MIT", - "dependencies": { - "whatwg-url": "^5.0.0" - }, - "engines": { - "node": "4.x || >=6.0.0" - }, - "peerDependencies": { - "encoding": "^0.1.0" - }, - "peerDependenciesMeta": { - "encoding": { - "optional": true - } - } - }, - "node_modules/symbol.inspect": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/symbol.inspect/-/symbol.inspect-1.0.1.tgz", - "integrity": "sha512-YQSL4duoHmLhsTD1Pw8RW6TZ5MaTX5rXJnqacJottr2P2LZBF/Yvrc3ku4NUpMOm8aM0KOCqM+UAkMA5HWQCzQ==", - "license": "ISC", - "peer": true - }, - "node_modules/tr46": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", - "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", - "license": "MIT" - }, - "node_modules/tweetnacl": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-1.0.3.tgz", - "integrity": "sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw==", - "license": "Unlicense", - "peer": true - }, - "node_modules/webidl-conversions": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", - "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", - "license": "BSD-2-Clause" - }, - "node_modules/whatwg-fetch": { - "version": "3.6.20", - "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.6.20.tgz", - "integrity": "sha512-EqhiFU6daOA8kpjOWTL0olhVOF3i7OrFzSYiGsEMB8GcXS+RrzauAERX65xMeNWVqxA6HXH2m69Z9LaKKdisfg==", - "license": "MIT" - }, - "node_modules/whatwg-url": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", - "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", - "license": "MIT", - "dependencies": { - "tr46": "~0.0.3", - "webidl-conversions": "^3.0.0" - } - } - } -} diff --git a/plugins/multisend/package.json b/plugins/multisend/package.json deleted file mode 100644 index fb39d8d..0000000 --- a/plugins/multisend/package.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "name": "teleton-plugin-multisend", - "type": "module", - "version": "1.0.0", - "private": true, - "dependencies": { - "@tonkite/highload-wallet-v3": "^2.0.11", - "@orbs-network/ton-access": "^2.3.3" - } -} diff --git a/plugins/pic/index.js b/plugins/pic/index.js index 16f4126..ecf19ea 100644 --- a/plugins/pic/index.js +++ b/plugins/pic/index.js @@ -5,22 +5,14 @@ * Messages appear "via @pic" just like typing @pic in the Telegram input field. */ -import { createRequire } from "node:module"; -import { realpathSync } from "node:fs"; - -// Resolve "telegram" from teleton's own node_modules (not the plugin directory). -// realpathSync follows the symlink so createRequire looks in the right node_modules. -const _require = createRequire(realpathSync(process.argv[1])); -const { Api } = _require("telegram"); - // --------------------------------------------------------------------------- // Export // --------------------------------------------------------------------------- export const manifest = { name: "pic", - version: "1.0.1", - sdkVersion: ">=1.0.0", + version: "2.0.0", + sdkVersion: "^2.1.0", description: "Search and send images in chat via Telegram's @pic inline bot (Yandex Image Search).", }; @@ -52,51 +44,22 @@ export const tools = (sdk) => [ execute: async (params, context) => { try { - const client = sdk.telegram.getRawClient(); - const picBot = await client.getEntity("pic"); - const peer = await client.getInputEntity(context.chatId); - - const results = await client.invoke( - new Api.messages.GetInlineBotResults({ - bot: picBot, - peer, - query: params.query, - offset: "", - }) - ); - - if (!results.results || results.results.length === 0) { - return { success: false, error: `No images found for "${params.query}"` }; - } - - const index = params.index ?? 0; - if (index >= results.results.length) { - return { - success: false, - error: `Only ${results.results.length} results available, index ${index} is out of range`, - }; - } - - const chosen = results.results[index]; - - await client.invoke( - new Api.messages.SendInlineBotResult({ - peer, - queryId: results.queryId, - id: chosen.id, - randomId: BigInt(Math.floor(Math.random() * 2 ** 62)), - }) + const result = await sdk.telegram.sendInlineBotResult( + context.chatId, + "pic", + params.query, + params.index ); return { success: true, data: { - query: params.query, - sent_index: index, - total_results: results.results.length, - title: chosen.title || null, - description: chosen.description || null, - type: chosen.type || null, + query: result.query, + sent_index: result.sentIndex, + total_results: result.totalResults, + title: result.title, + description: result.description, + type: result.type, }, }; } catch (err) { diff --git a/plugins/pic/manifest.json b/plugins/pic/manifest.json index ad6ee20..ba77051 100644 --- a/plugins/pic/manifest.json +++ b/plugins/pic/manifest.json @@ -1,7 +1,7 @@ { "id": "pic", "name": "@pic โ€” Inline Image Search", - "version": "1.0.1", + "version": "2.0.0", "description": "Search and send images via Telegram's @pic inline bot (Yandex Image Search)", "author": { "name": "teleton", @@ -9,11 +9,11 @@ }, "license": "MIT", "entry": "index.js", - "teleton": ">=1.0.0", + "teleton": ">=0.9.0", "tools": [ { "name": "pic", "description": "Search and send an image in the current chat via @pic" } ], - "sdkVersion": ">=1.0.0", + "sdkVersion": "^2.1.0", "permissions": [], "tags": ["images", "search", "inline-bot", "yandex"], "repository": "https://github.com/TONresistor/teleton-plugins", diff --git a/plugins/sbt/README.md b/plugins/sbt/README.md index 34b0f29..ff825fb 100644 --- a/plugins/sbt/README.md +++ b/plugins/sbt/README.md @@ -33,7 +33,7 @@ Ask the AI: | `description` | string | Yes | -- | Collection description | | `image` | string | Yes | -- | URL to collection image | -Deploys from the agent wallet at `~/.teleton/wallet.json`. Cost: ~0.05 TON. +Deploys through Teleton's protected wallet broker. Cost: ~0.05 TON. ### sbt_mint diff --git a/plugins/sbt/index.js b/plugins/sbt/index.js index 00c0180..ecdec7a 100644 --- a/plugins/sbt/index.js +++ b/plugins/sbt/index.js @@ -1,11 +1,9 @@ /** * TON SBT plugin โ€” deploy and mint Soulbound Tokens (TEP-85) * - * Uses @ton/core for cell building and the agent wallet - * at ~/.teleton/wallet.json for signing transactions. + * Uses @ton/core for cell building and Teleton's SDK transaction broker. * - * Dependencies (provided by teleton runtime): - * @ton/core, @ton/ton, @ton/crypto, @orbs-network/ton-access + * Dependency provided by teleton runtime: @ton/core */ import { createHash } from "crypto"; @@ -13,25 +11,19 @@ import { readFileSync, realpathSync } from "fs"; import { fileURLToPath } from "url"; import { createRequire } from "module"; import { dirname, join } from "path"; -import { homedir } from "os"; // --------------------------------------------------------------------------- // TON dependencies (CJS packages โ€” use createRequire for ESM compat) // --------------------------------------------------------------------------- const require = createRequire(realpathSync(process.argv[1])); -const _pluginRequire = createRequire(import.meta.url); - -const { Cell, Address, beginCell, Dictionary, contractAddress, SendMode } = require("@ton/core"); -const { WalletContractV5R1, TonClient, toNano, internal } = require("@ton/ton"); -const { mnemonicToPrivateKey } = require("@ton/crypto"); +const { Cell, Address, beginCell, Dictionary, contractAddress, TupleReader } = require("@ton/core"); // --------------------------------------------------------------------------- // Constants // --------------------------------------------------------------------------- const __dirname = dirname(fileURLToPath(import.meta.url)); -const WALLET_FILE = join(homedir(), ".teleton", "wallet.json"); const SBT_ITEM_CODE = Cell.fromBoc( Buffer.from(readFileSync(join(__dirname, "sbt_item_code.boc.b64"), "utf-8").trim(), "base64"), @@ -100,41 +92,6 @@ function extractCollectionImage(metaCell) { } } -// --------------------------------------------------------------------------- -// Wallet setup -// --------------------------------------------------------------------------- - -async function getWalletAndClient() { - let walletData; - try { - walletData = JSON.parse(readFileSync(WALLET_FILE, "utf-8")); - } catch { - throw new Error("Agent wallet not found at " + WALLET_FILE); - } - if (!walletData.mnemonic || !Array.isArray(walletData.mnemonic)) { - throw new Error("Invalid wallet file: missing mnemonic"); - } - - const keyPair = await mnemonicToPrivateKey(walletData.mnemonic); - const wallet = WalletContractV5R1.create({ - workchain: 0, - publicKey: keyPair.publicKey, - }); - - let endpoint; - try { - const { getHttpEndpoint } = _pluginRequire("@orbs-network/ton-access"); - endpoint = await getHttpEndpoint({ network: "mainnet" }); - } catch { - endpoint = "https://toncenter.com/api/v2/jsonRPC"; - } - - const client = new TonClient({ endpoint }); - const contract = client.open(wallet); - - return { wallet, keyPair, client, contract }; -} - // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• // TOOLS // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• @@ -144,7 +101,7 @@ async function getWalletAndClient() { export const manifest = { name: "sbt", version: "2.0.0", - sdkVersion: ">=1.0.0", + sdkVersion: "^2.0.0", description: "Deploy and mint Soulbound Tokens (TEP-85) on TON โ€” non-transferable NFTs permanently bound to their owners.", }; @@ -169,8 +126,9 @@ const sbtDeployCollection = { }, execute: async (params) => { try { - const { wallet, keyPair, contract } = await getWalletAndClient(); - const seqno = await contract.getSeqno(); + const walletAddress = sdk.ton.getAddress(); + if (!walletAddress) throw new Error("Agent wallet is not initialized"); + const wallet = Address.parse(walletAddress); const collectionMetaCell = buildContentDict({ name: params.name, @@ -186,11 +144,11 @@ const sbtDeployCollection = { const royaltyCell = beginCell() .storeUint(0, 16) .storeUint(1000, 16) - .storeAddress(wallet.address) + .storeAddress(wallet) .endCell(); const data = beginCell() - .storeAddress(wallet.address) + .storeAddress(wallet) .storeUint(0, 64) .storeRef(contentCell) .storeRef(SBT_ITEM_CODE) @@ -200,20 +158,12 @@ const sbtDeployCollection = { const stateInit = { code: COLLECTION_CODE, data }; const address = contractAddress(0, stateInit); - sdk.log.info("sbt_deploy_collection: deploying collection", params.name, "from wallet", wallet.address.toString()); - - await contract.sendTransfer({ - seqno, - secretKey: keyPair.secretKey, - sendMode: SendMode.PAY_GAS_SEPARATELY | SendMode.IGNORE_ERRORS, - messages: [ - internal({ - to: address, - value: toNano("0.05"), - init: stateInit, - bounce: false, - }), - ], + sdk.log.info("sbt_deploy_collection: deploying collection", params.name, "from wallet", walletAddress); + + const sent = await sdk.ton.send(address.toString(), 0.05, { + stateInit, + bounce: false, + sendMode: 3, }); sdk.log.info("sbt_deploy_collection: deployed at", address.toString()); @@ -222,8 +172,9 @@ const sbtDeployCollection = { success: true, data: { collection_address: address.toString(), - seqno, - wallet_address: wallet.address.toString(), + seqno: sent.seqno, + hash: sent.hash, + wallet_address: walletAddress, explorer: "https://tonviewer.com/" + address.toString(), }, }; @@ -256,13 +207,15 @@ const sbtMint = { }, execute: async (params) => { try { - const { wallet, keyPair, client, contract } = await getWalletAndClient(); - const seqno = await contract.getSeqno(); + const walletAddress = sdk.ton.getAddress(); + if (!walletAddress) throw new Error("Agent wallet is not initialized"); + const wallet = Address.parse(walletAddress); const collectionAddr = Address.parse(params.collection_address); - const result = await client.runMethod(collectionAddr, "get_collection_data"); - const nextItemIndex = result.stack.readBigNumber(); - const collectionContent = result.stack.readCell(); + const result = await sdk.ton.runGetMethod(collectionAddr.toString(), "get_collection_data"); + const stack = new TupleReader(result.stack); + const nextItemIndex = stack.readBigNumber(); + const collectionContent = stack.readCell(); let image = params.image; if (!image) { @@ -277,7 +230,7 @@ const sbtMint = { const authority = params.authority_address ? Address.parse(params.authority_address) - : wallet.address; + : wallet; const itemPayloadCell = beginCell() .storeAddress(Address.parse(params.owner_address)) @@ -291,25 +244,17 @@ const sbtMint = { .storeUint(1, 32) .storeUint(0, 64) .storeUint(nextItemIndex, 64) - .storeCoins(toNano("0.05")) + .storeCoins(50000000n) .storeRef(itemPayloadCell) .endCell(); - await contract.sendTransfer({ - seqno, - secretKey: keyPair.secretKey, - sendMode: SendMode.PAY_GAS_SEPARATELY | SendMode.IGNORE_ERRORS, - messages: [ - internal({ - to: collectionAddr, - value: toNano("0.1"), - body: mintBody, - bounce: true, - }), - ], + const sent = await sdk.ton.send(collectionAddr.toString(), 0.1, { + body: mintBody, + bounce: true, + sendMode: 3, }); - sdk.log.info("sbt_mint: minted item #" + nextItemIndex.toString(), "seqno", seqno); + sdk.log.info("sbt_mint: minted item #" + nextItemIndex.toString(), "seqno", sent.seqno); return { success: true, @@ -319,8 +264,9 @@ const sbtMint = { owner: params.owner_address, authority: authority.toString(), image: image || null, - seqno, - wallet_address: wallet.address.toString(), + seqno: sent.seqno, + hash: sent.hash, + wallet_address: walletAddress, }, }; } catch (err) { diff --git a/plugins/sbt/manifest.json b/plugins/sbt/manifest.json index 1e5a9c3..2b19236 100644 --- a/plugins/sbt/manifest.json +++ b/plugins/sbt/manifest.json @@ -9,8 +9,8 @@ }, "license": "MIT", "entry": "index.js", - "teleton": ">=1.0.0", - "sdkVersion": ">=1.0.0", + "teleton": ">=0.9.0", + "sdkVersion": "^2.0.0", "tools": [ { "name": "sbt_deploy_collection", "description": "Deploy a new SBT collection on TON" }, { "name": "sbt_mint", "description": "Mint a soulbound token in a collection" } diff --git a/plugins/sbt/package-lock.json b/plugins/sbt/package-lock.json deleted file mode 100644 index bddd6ee..0000000 --- a/plugins/sbt/package-lock.json +++ /dev/null @@ -1,82 +0,0 @@ -{ - "name": "teleton-plugin-sbt", - "version": "1.0.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "teleton-plugin-sbt", - "version": "1.0.0", - "dependencies": { - "@orbs-network/ton-access": "^2.3.3" - } - }, - "node_modules/@orbs-network/ton-access": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/@orbs-network/ton-access/-/ton-access-2.3.3.tgz", - "integrity": "sha512-b1miCPts7wBG9JKYgzXIRZQm/LMy5Uk1mNK8NzlcXHL3HRHJkkFbuYJGuj3IkWCiIicW3Ipp4sYnn3Fwo4oB0g==", - "license": "MIT", - "dependencies": { - "isomorphic-fetch": "^3.0.0" - } - }, - "node_modules/isomorphic-fetch": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/isomorphic-fetch/-/isomorphic-fetch-3.0.0.tgz", - "integrity": "sha512-qvUtwJ3j6qwsF3jLxkZ72qCgjMysPzDfeV240JHiGZsANBYd+EEuu35v7dfrJ9Up0Ak07D7GGSkGhCHTqg/5wA==", - "license": "MIT", - "dependencies": { - "node-fetch": "^2.6.1", - "whatwg-fetch": "^3.4.1" - } - }, - "node_modules/node-fetch": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", - "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", - "license": "MIT", - "dependencies": { - "whatwg-url": "^5.0.0" - }, - "engines": { - "node": "4.x || >=6.0.0" - }, - "peerDependencies": { - "encoding": "^0.1.0" - }, - "peerDependenciesMeta": { - "encoding": { - "optional": true - } - } - }, - "node_modules/tr46": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", - "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", - "license": "MIT" - }, - "node_modules/webidl-conversions": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", - "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", - "license": "BSD-2-Clause" - }, - "node_modules/whatwg-fetch": { - "version": "3.6.20", - "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.6.20.tgz", - "integrity": "sha512-EqhiFU6daOA8kpjOWTL0olhVOF3i7OrFzSYiGsEMB8GcXS+RrzauAERX65xMeNWVqxA6HXH2m69Z9LaKKdisfg==", - "license": "MIT" - }, - "node_modules/whatwg-url": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", - "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", - "license": "MIT", - "dependencies": { - "tr46": "~0.0.3", - "webidl-conversions": "^3.0.0" - } - } - } -} diff --git a/plugins/sbt/package.json b/plugins/sbt/package.json deleted file mode 100644 index a19d0d3..0000000 --- a/plugins/sbt/package.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "name": "teleton-plugin-sbt", - "type": "module", - "version": "2.0.0", - "private": true, - "dependencies": { - "@orbs-network/ton-access": "^2.3.3" - } -} diff --git a/plugins/stonfi/README.md b/plugins/stonfi/README.md index e024761..b4f0870 100644 --- a/plugins/stonfi/README.md +++ b/plugins/stonfi/README.md @@ -2,7 +2,7 @@ Swap tokens, browse pools, and farm on [StonFi](https://ston.fi) DEX -- the largest decentralized exchange on TON. -Read tools use the StonFi REST API. Swap execution uses the `@ston-fi/sdk` to build transactions, signed from the agent wallet. +Read tools use the StonFi API. Swap execution uses the Teleton SDK v2 transaction broker; the plugin never reads wallet credentials. ## Tools @@ -15,7 +15,7 @@ Read tools use the StonFi REST API. Swap execution uses the `@ston-fi/sdk` to bu | `stonfi_farms` | List active farming opportunities, optionally filtered by pool | | `stonfi_dex_stats` | Get overall StonFi DEX statistics (TVL, volume, wallets, trades) | | `stonfi_swap_quote` | Simulate a swap and get expected output, price impact, and fees | -| `stonfi_swap` | Execute a token swap -- builds tx via SDK, signs with agent wallet | +| `stonfi_swap` | Execute a token swap through the SDK v2 transaction broker | ## Install @@ -46,13 +46,7 @@ Ask the AI: ## Dependencies -Requires at runtime (provided by teleton): -- `@ton/core` -- Address, SendMode -- `@ton/ton` -- WalletContractV5R1, TonClient -- `@ton/crypto` -- mnemonicToPrivateKey -- `@ston-fi/sdk` -- required for `stonfi_swap` (transaction building) - -Agent wallet at `~/.teleton/wallet.json` is used for signing all on-chain transactions. +`@ston-fi/api` is installed locally for read-only market data. Teleton SDK v2 provides the transaction broker used by `stonfi_swap`. ## Schemas @@ -118,7 +112,7 @@ Get a swap quote on StonFi -- simulates a swap between two tokens and returns ex ### stonfi_swap -Execute a token swap on StonFi DEX. Simulates the swap, builds the transaction via @ston-fi/sdk, and signs with the agent wallet. Call `stonfi_swap_quote` first to preview. +Execute a token swap on StonFi through the Teleton SDK v2 transaction broker. Call `stonfi_swap_quote` first to preview. | Param | Type | Required | Default | Description | |-------|------|----------|---------|-------------| diff --git a/plugins/stonfi/index.js b/plugins/stonfi/index.js index 8afb7b9..cb632a6 100644 --- a/plugins/stonfi/index.js +++ b/plugins/stonfi/index.js @@ -3,45 +3,21 @@ * * Search tokens, check prices, browse pools/farms, get swap quotes, * and execute swaps on StonFi DEX. Uses @ston-fi/api (StonApiClient) - * for all API calls. Agent wallet at ~/.teleton/wallet.json signs swaps. + * for market data and the Teleton SDK v2 transaction broker for swaps. */ import { createRequire } from "node:module"; -import { readFileSync, realpathSync } from "node:fs"; -import { join } from "node:path"; -import { homedir } from "node:os"; // --------------------------------------------------------------------------- // CJS dependencies // --------------------------------------------------------------------------- -const _require = createRequire(realpathSync(process.argv[1])); // core: @ton/core, @ton/ton, @ton/crypto -const _pluginRequire = createRequire(import.meta.url); // local: plugin-specific deps - -const { Address, SendMode } = _require("@ton/core"); -const { WalletContractV5R1, TonClient, internal } = _require("@ton/ton"); -const { mnemonicToPrivateKey } = _require("@ton/crypto"); +const _pluginRequire = createRequire(import.meta.url); // StonFi API client (from plugin's local node_modules) const { StonApiClient } = _pluginRequire("@ston-fi/api"); const stonApi = new StonApiClient(); -// StonFi SDK (for swap execution) -let dexFactory; -try { - const stonfi = _pluginRequire("@ston-fi/sdk"); - dexFactory = stonfi.dexFactory ?? stonfi.DEX; -} catch { - // SDK not available -- swap execution will fail with clear error -} - -// --------------------------------------------------------------------------- -// Constants -// --------------------------------------------------------------------------- - -const TON_ADDRESS = "EQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAM9c"; -const WALLET_FILE = join(homedir(), ".teleton", "wallet.json"); - // --------------------------------------------------------------------------- // Asset cache (5-minute TTL via sdk.storage) // queryAssets text search doesn't work, so we cache getAssets() and filter. @@ -82,41 +58,6 @@ function fmtAsset(a) { }; } -// --------------------------------------------------------------------------- -// Wallet helper -// --------------------------------------------------------------------------- - -async function getWalletAndClient() { - let walletData; - try { - walletData = JSON.parse(readFileSync(WALLET_FILE, "utf-8")); - } catch { - throw new Error("Agent wallet not found at " + WALLET_FILE); - } - if (!walletData.mnemonic || !Array.isArray(walletData.mnemonic)) { - throw new Error("Invalid wallet file: missing mnemonic array"); - } - - const keyPair = await mnemonicToPrivateKey(walletData.mnemonic); - const wallet = WalletContractV5R1.create({ - workchain: 0, - publicKey: keyPair.publicKey, - }); - - let endpoint; - try { - const { getHttpEndpoint } = _pluginRequire("@orbs-network/ton-access"); - endpoint = await getHttpEndpoint({ network: "mainnet" }); - } catch { - endpoint = "https://toncenter.com/api/v2/jsonRPC"; - } - - const client = new TonClient({ endpoint }); - const contract = client.open(wallet); - - return { wallet, keyPair, client, contract }; -} - // --------------------------------------------------------------------------- // Tool 1: stonfi_search // --------------------------------------------------------------------------- @@ -600,7 +541,7 @@ const stonfiSwapQuote = { const stonfiSwap = { name: "stonfi_swap", description: - "Execute a token swap on StonFi DEX. Simulates the swap via @ston-fi/api, builds the transaction via @ston-fi/sdk, and signs with the agent wallet. Call stonfi_swap_quote first to preview.", + "Execute a token swap on StonFi through the Teleton SDK transaction broker. Call stonfi_swap_quote first to preview.", category: "action", scope: "admin-only", @@ -632,118 +573,29 @@ const stonfiSwap = { execute: async (params) => { try { - if (!dexFactory) { - throw new Error( - "@ston-fi/sdk is not installed. Install it to execute swaps." - ); - } - const slippage = params.slippage ?? 0.01; - const inputAmount = Number(params.amount); - if (!Number.isFinite(inputAmount) || inputAmount <= 0) { + const amount = Number(params.amount); + if (!Number.isFinite(amount) || amount <= 0) { throw new Error("amount must be a positive number"); } - - // Step 1: Look up offer + ask asset decimals in parallel - const [offerAsset, askAsset] = await Promise.all([ - stonApi.getAsset(params.offer_address), - stonApi.getAsset(params.ask_address), - ]); - const offerDecimals = offerAsset.decimals ?? 9; - const askDecimals = askAsset.decimals ?? 9; - - _sdk?.log?.info(`Executing swap: ${params.amount} ${offerAsset.symbol} -> ${askAsset.symbol} (slippage: ${slippage})`); - - // Step 2: Simulate swap via StonApiClient - const units = toUnits(params.amount, offerDecimals); - const sim = await stonApi.simulateSwap({ - offerAddress: params.offer_address, - askAddress: params.ask_address, - offerUnits: units, - slippageTolerance: String(slippage), - }); - - if (!sim.router) { - throw new Error("Swap simulation did not return router info"); - } - - // Step 3: Get wallet - const { wallet, keyPair, client, contract } = - await getWalletAndClient(); - const walletAddr = wallet.address.toString(); - - // Step 4: Build transaction via SDK (dexFactory auto-detects version) - const dexContracts = dexFactory(sim.router); - const router = client.open( - dexContracts.Router.create(sim.router.address) - ); - const proxyTon = dexContracts.pTON.create( - sim.router.ptonMasterAddress - ); - - const offerIsTon = params.offer_address === TON_ADDRESS; - const askIsTon = params.ask_address === TON_ADDRESS; - - let txParams; - if (offerIsTon) { - txParams = await router.getSwapTonToJettonTxParams({ - userWalletAddress: walletAddr, - offerAmount: sim.offerUnits, - minAskAmount: sim.minAskUnits, - askJettonAddress: params.ask_address, - proxyTon, - }); - } else if (askIsTon) { - txParams = await router.getSwapJettonToTonTxParams({ - userWalletAddress: walletAddr, - offerJettonAddress: params.offer_address, - offerAmount: sim.offerUnits, - minAskAmount: sim.minAskUnits, - proxyTon, - }); - } else { - txParams = await router.getSwapJettonToJettonTxParams({ - userWalletAddress: walletAddr, - offerJettonAddress: params.offer_address, - askJettonAddress: params.ask_address, - offerAmount: sim.offerUnits, - minAskAmount: sim.minAskUnits, - }); - } - - // Step 5: Send transaction - const seqno = await contract.getSeqno(); - await contract.sendTransfer({ - seqno, - secretKey: keyPair.secretKey, - sendMode: SendMode.PAY_GAS_SEPARATELY | SendMode.IGNORE_ERRORS, - messages: [ - internal({ - to: txParams.to, - value: txParams.value, - body: txParams.body, - bounce: true, - }), - ], + const result = await _sdk.ton.dex.swapSTONfi({ + fromAsset: params.offer_address, + toAsset: params.ask_address, + amount, + slippage, }); return { success: true, data: { - offer_amount: params.amount, - offer_symbol: offerAsset.symbol ?? null, - expected_output: fromUnits(sim.askUnits, askDecimals), - min_output: fromUnits(sim.minAskUnits, askDecimals), - ask_symbol: askAsset.symbol ?? null, - swap_rate: sim.swapRate ?? null, - price_impact: sim.priceImpact ?? null, - slippage, - seqno, - wallet_address: walletAddr, - router_address: sim.routerAddress ?? null, - pool_address: sim.poolAddress ?? null, - message: - "Swap transaction sent. Confirmation typically takes ~30 seconds on TON.", + offer_address: result.fromAsset, + ask_address: result.toAsset, + offer_amount: result.amountIn, + expected_output: result.expectedOutput, + min_output: result.minOutput, + slippage: result.slippage, + tx_ref: result.txRef ?? null, + message: "Swap transaction confirmed by the Teleton transaction broker.", }, }; } catch (err) { @@ -763,7 +615,7 @@ const stonfiSwap = { export const manifest = { name: "stonfi", version: "1.0.0", - sdkVersion: ">=1.0.0", + sdkVersion: "^2.0.0", description: "StonFi DEX on TON โ€” search tokens, check prices, browse pools/farms, get swap quotes, and execute swaps.", }; diff --git a/plugins/stonfi/manifest.json b/plugins/stonfi/manifest.json index b9b1e80..640d612 100644 --- a/plugins/stonfi/manifest.json +++ b/plugins/stonfi/manifest.json @@ -6,8 +6,8 @@ "author": { "name": "teleton", "url": "https://github.com/TONresistor" }, "license": "MIT", "entry": "index.js", - "teleton": ">=1.0.0", - "sdkVersion": ">=1.0.0", + "teleton": ">=0.9.0", + "sdkVersion": "^2.0.0", "tools": [ { "name": "stonfi_search", "description": "Search tokens on StonFi DEX by name, symbol, or contract address" }, { "name": "stonfi_price", "description": "Get the current USD price for a token on StonFi" }, diff --git a/plugins/stonfi/package-lock.json b/plugins/stonfi/package-lock.json index b183b27..029c218 100644 --- a/plugins/stonfi/package-lock.json +++ b/plugins/stonfi/package-lock.json @@ -8,18 +8,7 @@ "name": "teleton-plugin-stonfi", "version": "1.0.0", "dependencies": { - "@orbs-network/ton-access": "^2.3.3", - "@ston-fi/api": "^0.30.0", - "@ston-fi/sdk": "^2.7.0" - } - }, - "node_modules/@orbs-network/ton-access": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/@orbs-network/ton-access/-/ton-access-2.3.3.tgz", - "integrity": "sha512-b1miCPts7wBG9JKYgzXIRZQm/LMy5Uk1mNK8NzlcXHL3HRHJkkFbuYJGuj3IkWCiIicW3Ipp4sYnn3Fwo4oB0g==", - "license": "MIT", - "dependencies": { - "isomorphic-fetch": "^3.0.0" + "@ston-fi/api": "^0.30.0" } }, "node_modules/@ston-fi/api": { @@ -34,97 +23,6 @@ "ofetch": "1.4.1" } }, - "node_modules/@ston-fi/sdk": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/@ston-fi/sdk/-/sdk-2.7.0.tgz", - "integrity": "sha512-dErhqyCy53t2p/tPe1KNVR7iX5f1d6v7wvSPltsFKy8uF+hU/mxL2/ElRZktDE0mkJQDljOjFlM9s4E0VTFzuw==", - "license": "MIT", - "peerDependencies": { - "@ston-fi/api": "^0", - "@ton/ton": "^13.9.0 || ^14.0.0 || ^15.0.0 || ^16.0.0" - } - }, - "node_modules/@ton/core": { - "version": "0.63.1", - "resolved": "https://registry.npmjs.org/@ton/core/-/core-0.63.1.tgz", - "integrity": "sha512-hDWMjlKzc18W2E4OeV3hUP8ohRJNHPD4Wd1+AQJj8zshZyCRT0usrvnExgbNUTo/vntDqCGMzgYWbXxyaA+L4g==", - "license": "MIT", - "peer": true, - "peerDependencies": { - "@ton/crypto": ">=3.2.0" - } - }, - "node_modules/@ton/crypto": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/@ton/crypto/-/crypto-3.3.0.tgz", - "integrity": "sha512-/A6CYGgA/H36OZ9BbTaGerKtzWp50rg67ZCH2oIjV1NcrBaCK9Z343M+CxedvM7Haf3f/Ee9EhxyeTp0GKMUpA==", - "license": "MIT", - "peer": true, - "dependencies": { - "@ton/crypto-primitives": "2.1.0", - "jssha": "3.2.0", - "tweetnacl": "1.0.3" - } - }, - "node_modules/@ton/crypto-primitives": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@ton/crypto-primitives/-/crypto-primitives-2.1.0.tgz", - "integrity": "sha512-PQesoyPgqyI6vzYtCXw4/ZzevePc4VGcJtFwf08v10OevVJHVfW238KBdpj1kEDQkxWLeuNHEpTECNFKnP6tow==", - "license": "MIT", - "peer": true, - "dependencies": { - "jssha": "3.2.0" - } - }, - "node_modules/@ton/ton": { - "version": "16.2.2", - "resolved": "https://registry.npmjs.org/@ton/ton/-/ton-16.2.2.tgz", - "integrity": "sha512-yEOw4IW3gpRZxJAcILMI4dQ1d5/eAAbD2VU/Iwc6z7f2jt1mLDWVED8yn2vLNucQfZr+1eaqYHLztYVFZ7PKmw==", - "license": "MIT", - "peer": true, - "dependencies": { - "axios": "^1.6.7", - "dataloader": "^2.0.0", - "zod": "^3.21.4" - }, - "peerDependencies": { - "@ton/core": ">=0.63.0 <1.0.0", - "@ton/crypto": ">=3.2.0" - } - }, - "node_modules/asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", - "license": "MIT", - "peer": true - }, - "node_modules/axios": { - "version": "1.13.5", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.5.tgz", - "integrity": "sha512-cz4ur7Vb0xS4/KUN0tPWe44eqxrIu31me+fbang3ijiNscE129POzipJJA6zniq2C/Z6sJCjMimjS8Lc/GAs8Q==", - "license": "MIT", - "peer": true, - "dependencies": { - "follow-redirects": "^1.15.11", - "form-data": "^4.0.5", - "proxy-from-env": "^1.1.0" - } - }, - "node_modules/call-bind-apply-helpers": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", - "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", - "license": "MIT", - "peer": true, - "dependencies": { - "es-errors": "^1.3.0", - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, "node_modules/camelcase": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-8.0.0.tgz", @@ -155,26 +53,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/combined-stream": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", - "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "license": "MIT", - "peer": true, - "dependencies": { - "delayed-stream": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/dataloader": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/dataloader/-/dataloader-2.2.3.tgz", - "integrity": "sha512-y2krtASINtPFS1rSDjacrFgn1dcUuoREVabwlOGOe4SdxenREqwjwjElAdwvbGM7kgZz9a3KVicWR7vcz8rnzA==", - "license": "MIT", - "peer": true - }, "node_modules/decamelize": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-6.0.1.tgz", @@ -229,248 +107,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", - "license": "MIT", - "peer": true, - "engines": { - "node": ">=0.4.0" - } - }, "node_modules/destr": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/destr/-/destr-2.0.5.tgz", "integrity": "sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==", "license": "MIT" }, - "node_modules/dunder-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", - "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", - "license": "MIT", - "peer": true, - "dependencies": { - "call-bind-apply-helpers": "^1.0.1", - "es-errors": "^1.3.0", - "gopd": "^1.2.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-define-property": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", - "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", - "license": "MIT", - "peer": true, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-errors": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", - "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", - "license": "MIT", - "peer": true, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-object-atoms": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", - "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", - "license": "MIT", - "peer": true, - "dependencies": { - "es-errors": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-set-tostringtag": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", - "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", - "license": "MIT", - "peer": true, - "dependencies": { - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.6", - "has-tostringtag": "^1.0.2", - "hasown": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/follow-redirects": { - "version": "1.15.11", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", - "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/RubenVerborgh" - } - ], - "license": "MIT", - "peer": true, - "engines": { - "node": ">=4.0" - }, - "peerDependenciesMeta": { - "debug": { - "optional": true - } - } - }, - "node_modules/form-data": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", - "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", - "license": "MIT", - "peer": true, - "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "es-set-tostringtag": "^2.1.0", - "hasown": "^2.0.2", - "mime-types": "^2.1.12" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/function-bind": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "license": "MIT", - "peer": true, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-intrinsic": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", - "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", - "license": "MIT", - "peer": true, - "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "es-define-property": "^1.0.1", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.1.1", - "function-bind": "^1.1.2", - "get-proto": "^1.0.1", - "gopd": "^1.2.0", - "has-symbols": "^1.1.0", - "hasown": "^2.0.2", - "math-intrinsics": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", - "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", - "license": "MIT", - "peer": true, - "dependencies": { - "dunder-proto": "^1.0.1", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/gopd": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", - "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", - "license": "MIT", - "peer": true, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-symbols": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", - "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", - "license": "MIT", - "peer": true, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-tostringtag": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", - "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", - "license": "MIT", - "peer": true, - "dependencies": { - "has-symbols": "^1.0.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/hasown": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", - "license": "MIT", - "peer": true, - "dependencies": { - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/isomorphic-fetch": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/isomorphic-fetch/-/isomorphic-fetch-3.0.0.tgz", - "integrity": "sha512-qvUtwJ3j6qwsF3jLxkZ72qCgjMysPzDfeV240JHiGZsANBYd+EEuu35v7dfrJ9Up0Ak07D7GGSkGhCHTqg/5wA==", - "license": "MIT", - "dependencies": { - "node-fetch": "^2.6.1", - "whatwg-fetch": "^3.4.1" - } - }, - "node_modules/jssha": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/jssha/-/jssha-3.2.0.tgz", - "integrity": "sha512-QuruyBENDWdN4tZwJbQq7/eAK85FqrI4oDbXjy5IBhYD+2pTJyBUWZe8ctWaCkrV0gy6AaelgOZZBMeswEa/6Q==", - "license": "BSD-3-Clause", - "peer": true, - "engines": { - "node": "*" - } - }, "node_modules/map-obj": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-5.0.0.tgz", @@ -483,59 +125,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/math-intrinsics": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", - "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", - "license": "MIT", - "peer": true, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "license": "MIT", - "peer": true, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "license": "MIT", - "peer": true, - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/node-fetch": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", - "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", - "license": "MIT", - "dependencies": { - "whatwg-url": "^5.0.0" - }, - "engines": { - "node": "4.x || >=6.0.0" - }, - "peerDependencies": { - "encoding": "^0.1.0" - }, - "peerDependenciesMeta": { - "encoding": { - "optional": true - } - } - }, "node_modules/node-fetch-native": { "version": "1.6.7", "resolved": "https://registry.npmjs.org/node-fetch-native/-/node-fetch-native-1.6.7.tgz", @@ -553,13 +142,6 @@ "ufo": "^1.5.4" } }, - "node_modules/proxy-from-env": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", - "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", - "license": "MIT", - "peer": true - }, "node_modules/quick-lru": { "version": "6.1.2", "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-6.1.2.tgz", @@ -572,19 +154,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/tr46": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", - "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", - "license": "MIT" - }, - "node_modules/tweetnacl": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-1.0.3.tgz", - "integrity": "sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw==", - "license": "Unlicense", - "peer": true - }, "node_modules/type-fest": { "version": "4.41.0", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", @@ -602,38 +171,6 @@ "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.3.tgz", "integrity": "sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q==", "license": "MIT" - }, - "node_modules/webidl-conversions": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", - "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", - "license": "BSD-2-Clause" - }, - "node_modules/whatwg-fetch": { - "version": "3.6.20", - "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.6.20.tgz", - "integrity": "sha512-EqhiFU6daOA8kpjOWTL0olhVOF3i7OrFzSYiGsEMB8GcXS+RrzauAERX65xMeNWVqxA6HXH2m69Z9LaKKdisfg==", - "license": "MIT" - }, - "node_modules/whatwg-url": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", - "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", - "license": "MIT", - "dependencies": { - "tr46": "~0.0.3", - "webidl-conversions": "^3.0.0" - } - }, - "node_modules/zod": { - "version": "3.25.76", - "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", - "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", - "license": "MIT", - "peer": true, - "funding": { - "url": "https://github.com/sponsors/colinhacks" - } } } } diff --git a/plugins/stonfi/package.json b/plugins/stonfi/package.json index be33e1b..b7bab34 100644 --- a/plugins/stonfi/package.json +++ b/plugins/stonfi/package.json @@ -4,8 +4,6 @@ "version": "1.0.0", "private": true, "dependencies": { - "@ston-fi/api": "^0.30.0", - "@ston-fi/sdk": "^2.7.0", - "@orbs-network/ton-access": "^2.3.3" + "@ston-fi/api": "^0.30.0" } } diff --git a/plugins/stormtrade/README.md b/plugins/stormtrade/README.md index 7658061..5b1d870 100644 --- a/plugins/stormtrade/README.md +++ b/plugins/stormtrade/README.md @@ -2,7 +2,7 @@ Perpetual futures trading on [Storm Trade](https://stormtrade.dev) DEX -- crypto, stocks, forex, and commodities on TON. -Read tools use the Storm Trade REST API. Write tools sign transactions from the agent wallet via the Storm Trade SDK. +Read tools use the Storm Trade REST API. Write tools build transactions with the Storm Trade SDK and send them through Teleton's protected wallet broker. ## Tools @@ -57,11 +57,9 @@ Ask the AI: Requires at runtime (provided by teleton): - `@ton/core` -- Cell building, address computation -- `@ton/ton` -- Wallet contract, TonClient -- `@ton/crypto` -- Mnemonic to private key +- `@ton/ton` -- read-only TonClient used by the protocol SDK - `@storm-trade/sdk` -- Position/order building, vault configs - -Agent wallet at `~/.teleton/wallet.json` is used for signing all on-chain transactions. +- Teleton Plugin SDK v2 -- protected transaction broker ## Schemas diff --git a/plugins/stormtrade/index.js b/plugins/stormtrade/index.js index a691a32..bb0e552 100644 --- a/plugins/stormtrade/index.js +++ b/plugins/stormtrade/index.js @@ -3,13 +3,11 @@ * * Trade crypto, stocks, forex, and commodities with up to 100x leverage. * Uses @storm-trade/sdk for on-chain writes and REST API for reads. - * Agent wallet at ~/.teleton/wallet.json signs all transactions. + * Teleton's SDK wallet broker signs all transactions. */ import { createRequire } from "node:module"; -import { readFileSync, realpathSync } from "node:fs"; -import { join } from "node:path"; -import { homedir } from "node:os"; +import { realpathSync } from "node:fs"; // --------------------------------------------------------------------------- // CJS dependencies @@ -18,9 +16,8 @@ import { homedir } from "node:os"; const _require = createRequire(realpathSync(process.argv[1])); // core: @ton/core, @ton/ton, @ton/crypto const _pluginRequire = createRequire(import.meta.url); // local: plugin-specific deps -const { Address, SendMode } = _require("@ton/core"); -const { WalletContractV5R1, TonClient, toNano, internal } = _require("@ton/ton"); -const { mnemonicToPrivateKey } = _require("@ton/crypto"); +const { Address } = _require("@ton/core"); +const { TonClient } = _require("@ton/ton"); const { StormSDK, Direction, @@ -35,7 +32,6 @@ const { // --------------------------------------------------------------------------- const API_BASE = "https://api5.storm.tg/api"; -const WALLET_FILE = join(homedir(), ".teleton", "wallet.json"); let _sdk = null; @@ -59,24 +55,8 @@ async function stormFetch(path, params = {}) { return res.json(); } -/** Read agent wallet, create TonClient, open wallet contract. */ -async function getWalletAndClient() { - let walletData; - try { - walletData = JSON.parse(readFileSync(WALLET_FILE, "utf-8")); - } catch { - throw new Error("Agent wallet not found at " + WALLET_FILE); - } - if (!walletData.mnemonic || !Array.isArray(walletData.mnemonic)) { - throw new Error("Invalid wallet file: missing mnemonic array"); - } - - const keyPair = await mnemonicToPrivateKey(walletData.mnemonic); - const wallet = WalletContractV5R1.create({ - workchain: 0, - publicKey: keyPair.publicKey, - }); - +/** Create the read-only TonClient required by the Storm protocol SDK. */ +async function getClient() { let endpoint; try { const { getHttpEndpoint } = _pluginRequire("@orbs-network/ton-access"); @@ -85,16 +65,23 @@ async function getWalletAndClient() { endpoint = "https://toncenter.com/api/v2/jsonRPC"; } - const client = new TonClient({ endpoint }); - const contract = client.open(wallet); - - return { wallet, keyPair, client, contract }; + return new TonClient({ endpoint }); } /** Get agent's friendly wallet address. */ function getAgentAddress() { - const data = JSON.parse(readFileSync(WALLET_FILE, "utf-8")); - return data.address; + const address = _sdk?.ton?.getAddress(); + if (!address) throw new Error("Agent wallet is not initialized"); + return address; +} + +async function sendTx(txParams) { + const value = Number(_sdk.ton.fromNano(txParams.value)); + return _sdk.ton.send(txParams.to.toString(), value, { + body: txParams.body, + bounce: true, + sendMode: 3, + }); } /** Return StormSDK instance for the given vault type. */ @@ -409,9 +396,9 @@ const stormOpenPosition = { execute: async (params) => { try { - const { wallet, keyPair, client, contract } = await getWalletAndClient(); + const client = await getClient(); const sdk = getSDK(params.vault, client); - const traderAddress = wallet.address; + const traderAddress = Address.parse(getAgentAddress()); const baseAsset = parseBaseAsset(params.market); const direction = parseDirection(params.direction); @@ -429,15 +416,7 @@ const stormOpenPosition = { const txParams = await sdk.increasePosition(increaseOpts); - const seqno = await contract.getSeqno(); - await contract.sendTransfer({ - seqno, - secretKey: keyPair.secretKey, - sendMode: SendMode.PAY_GAS_SEPARATELY | SendMode.IGNORE_ERRORS, - messages: [ - internal({ to: txParams.to, value: txParams.value, body: txParams.body, bounce: true }), - ], - }); + const sent = await sendTx(txParams); return { success: true, @@ -446,8 +425,8 @@ const stormOpenPosition = { direction: params.direction, amount: params.amount, leverage: params.leverage, - seqno, - walletAddress: wallet.address.toString(), + seqno: sent.seqno, + walletAddress: traderAddress.toString(), has_stop_loss: !!params.stop_loss, has_take_profit: !!params.take_profit, message: "Position open tx sent. Check status after ~15 seconds with storm_positions.", @@ -491,9 +470,9 @@ const stormClosePosition = { execute: async (params) => { try { - const { wallet, keyPair, client, contract } = await getWalletAndClient(); + const client = await getClient(); const sdk = getSDK(params.vault, client); - const traderAddress = wallet.address; + const traderAddress = Address.parse(getAgentAddress()); const baseAsset = parseBaseAsset(params.market); const direction = parseDirection(params.direction); @@ -517,15 +496,7 @@ const stormClosePosition = { size, }); - const seqno = await contract.getSeqno(); - await contract.sendTransfer({ - seqno, - secretKey: keyPair.secretKey, - sendMode: SendMode.PAY_GAS_SEPARATELY | SendMode.IGNORE_ERRORS, - messages: [ - internal({ to: txParams.to, value: txParams.value, body: txParams.body, bounce: true }), - ], - }); + const sent = await sendTx(txParams); return { success: true, @@ -533,8 +504,8 @@ const stormClosePosition = { market: params.market, direction: params.direction, size: params.size || "1", - seqno, - walletAddress: wallet.address.toString(), + seqno: sent.seqno, + walletAddress: traderAddress.toString(), message: "Close position tx sent. Check status after ~15 seconds.", }, }; @@ -572,9 +543,9 @@ const stormAddMargin = { execute: async (params) => { try { - const { wallet, keyPair, client, contract } = await getWalletAndClient(); + const client = await getClient(); const sdk = getSDK(params.vault, client); - const traderAddress = wallet.address; + const traderAddress = Address.parse(getAgentAddress()); const baseAsset = parseBaseAsset(params.market); const direction = parseDirection(params.direction); @@ -585,15 +556,7 @@ const stormAddMargin = { amount: parseAmount(params.amount, params.vault), }); - const seqno = await contract.getSeqno(); - await contract.sendTransfer({ - seqno, - secretKey: keyPair.secretKey, - sendMode: SendMode.PAY_GAS_SEPARATELY | SendMode.IGNORE_ERRORS, - messages: [ - internal({ to: txParams.to, value: txParams.value, body: txParams.body, bounce: true }), - ], - }); + const sent = await sendTx(txParams); return { success: true, @@ -601,8 +564,8 @@ const stormAddMargin = { market: params.market, direction: params.direction, amount: params.amount, - seqno, - walletAddress: wallet.address.toString(), + seqno: sent.seqno, + walletAddress: traderAddress.toString(), message: "Add margin tx sent. Check position after ~15 seconds.", }, }; @@ -639,9 +602,9 @@ const stormRemoveMargin = { execute: async (params) => { try { - const { wallet, keyPair, client, contract } = await getWalletAndClient(); + const client = await getClient(); const sdk = getSDK(params.vault, client); - const traderAddress = wallet.address; + const traderAddress = Address.parse(getAgentAddress()); const baseAsset = parseBaseAsset(params.market); const direction = parseDirection(params.direction); @@ -652,15 +615,7 @@ const stormRemoveMargin = { amount: numToNano(parseNum(params.amount, "amount")), }); - const seqno = await contract.getSeqno(); - await contract.sendTransfer({ - seqno, - secretKey: keyPair.secretKey, - sendMode: SendMode.PAY_GAS_SEPARATELY | SendMode.IGNORE_ERRORS, - messages: [ - internal({ to: txParams.to, value: txParams.value, body: txParams.body, bounce: true }), - ], - }); + const sent = await sendTx(txParams); return { success: true, @@ -668,8 +623,8 @@ const stormRemoveMargin = { market: params.market, direction: params.direction, amount: params.amount, - seqno, - walletAddress: wallet.address.toString(), + seqno: sent.seqno, + walletAddress: traderAddress.toString(), message: "Remove margin tx sent. Check position after ~15 seconds.", }, }; @@ -745,9 +700,9 @@ const stormCreateOrder = { execute: async (params) => { try { - const { wallet, keyPair, client, contract } = await getWalletAndClient(); + const client = await getClient(); const sdk = getSDK(params.vault, client); - const traderAddress = wallet.address; + const traderAddress = Address.parse(getAgentAddress()); const baseAsset = parseBaseAsset(params.market); const direction = parseDirection(params.direction); const expiration = params.expiration || 86400 * 30; @@ -788,15 +743,7 @@ const stormCreateOrder = { const txParams = await sdk.createOrder(orderOpts); - const seqno = await contract.getSeqno(); - await contract.sendTransfer({ - seqno, - secretKey: keyPair.secretKey, - sendMode: SendMode.PAY_GAS_SEPARATELY | SendMode.IGNORE_ERRORS, - messages: [ - internal({ to: txParams.to, value: txParams.value, body: txParams.body, bounce: true }), - ], - }); + const sent = await sendTx(txParams); return { success: true, @@ -804,8 +751,8 @@ const stormCreateOrder = { market: params.market, direction: params.direction, order_type: params.order_type, - seqno, - walletAddress: wallet.address.toString(), + seqno: sent.seqno, + walletAddress: traderAddress.toString(), message: "Order creation tx sent. Check orders after ~15 seconds.", }, }; @@ -851,9 +798,9 @@ const stormCancelOrder = { execute: async (params) => { try { - const { wallet, keyPair, client, contract } = await getWalletAndClient(); + const client = await getClient(); const sdk = getSDK(params.vault, client); - const traderAddress = wallet.address; + const traderAddress = Address.parse(getAgentAddress()); const baseAsset = parseBaseAsset(params.market); const direction = parseDirection(params.direction); @@ -865,15 +812,7 @@ const stormCancelOrder = { direction, }); - const seqno = await contract.getSeqno(); - await contract.sendTransfer({ - seqno, - secretKey: keyPair.secretKey, - sendMode: SendMode.PAY_GAS_SEPARATELY | SendMode.IGNORE_ERRORS, - messages: [ - internal({ to: txParams.to, value: txParams.value, body: txParams.body, bounce: true }), - ], - }); + const sent = await sendTx(txParams); return { success: true, @@ -882,8 +821,8 @@ const stormCancelOrder = { direction: params.direction, order_type: params.order_type, order_index: params.order_index ?? 0, - seqno, - walletAddress: wallet.address.toString(), + seqno: sent.seqno, + walletAddress: traderAddress.toString(), message: "Cancel order tx sent. Check orders after ~15 seconds.", }, }; @@ -918,32 +857,24 @@ const stormStake = { execute: async (params) => { try { - const { wallet, keyPair, client, contract } = await getWalletAndClient(); + const client = await getClient(); const sdk = getSDK(params.vault, client); - const userAddress = wallet.address; + const userAddress = Address.parse(getAgentAddress()); const txParams = await sdk.stake({ amount: parseAmount(params.amount, params.vault), userAddress, }); - const seqno = await contract.getSeqno(); - await contract.sendTransfer({ - seqno, - secretKey: keyPair.secretKey, - sendMode: SendMode.PAY_GAS_SEPARATELY | SendMode.IGNORE_ERRORS, - messages: [ - internal({ to: txParams.to, value: txParams.value, body: txParams.body, bounce: true }), - ], - }); + const sent = await sendTx(txParams); return { success: true, data: { amount: params.amount, vault: params.vault || "usdt", - seqno, - walletAddress: wallet.address.toString(), + seqno: sent.seqno, + walletAddress: userAddress.toString(), message: "Stake tx sent. Check vault balance after ~15 seconds.", }, }; @@ -977,31 +908,23 @@ const stormUnstake = { execute: async (params) => { try { - const { wallet, keyPair, client, contract } = await getWalletAndClient(); + const client = await getClient(); const sdk = getSDK(params.vault, client); - const userAddress = wallet.address; + const userAddress = Address.parse(getAgentAddress()); const unstakeOpts = { userAddress }; if (params.amount) unstakeOpts.amount = numToNano(parseNum(params.amount, "amount")); const txParams = await sdk.unstake(unstakeOpts); - const seqno = await contract.getSeqno(); - await contract.sendTransfer({ - seqno, - secretKey: keyPair.secretKey, - sendMode: SendMode.PAY_GAS_SEPARATELY | SendMode.IGNORE_ERRORS, - messages: [ - internal({ to: txParams.to, value: txParams.value, body: txParams.body, bounce: true }), - ], - }); + const sent = await sendTx(txParams); return { success: true, data: { amount: params.amount, vault: params.vault || "usdt", - seqno, - walletAddress: wallet.address.toString(), + seqno: sent.seqno, + walletAddress: userAddress.toString(), message: "Unstake tx sent. Check vault balance after ~15 seconds.", }, }; @@ -1017,8 +940,8 @@ const stormUnstake = { export const manifest = { name: "stormtrade", - version: "1.0.0", - sdkVersion: ">=1.0.0", + version: "2.0.0", + sdkVersion: "^2.0.0", description: "Storm Trade perpetual futures on TON โ€” trade crypto, stocks, forex, and commodities with up to 100x leverage.", }; diff --git a/plugins/stormtrade/manifest.json b/plugins/stormtrade/manifest.json index 753cbbf..ebafded 100644 --- a/plugins/stormtrade/manifest.json +++ b/plugins/stormtrade/manifest.json @@ -1,7 +1,7 @@ { "id": "stormtrade", "name": "Storm Trade Perpetual Futures", - "version": "1.0.0", + "version": "2.0.0", "description": "Trade perpetual futures on Storm Trade DEX โ€” crypto, stocks, forex, commodities", "author": { "name": "teleton", @@ -9,8 +9,8 @@ }, "license": "MIT", "entry": "index.js", - "teleton": ">=1.0.0", - "sdkVersion": ">=1.0.0", + "teleton": ">=0.9.0", + "sdkVersion": "^2.0.0", "tools": [ { "name": "storm_markets", "description": "List all available markets with prices, funding rates, and open interest" }, { "name": "storm_market_info", "description": "Get detailed info for a specific market" }, diff --git a/plugins/stormtrade/package-lock.json b/plugins/stormtrade/package-lock.json index 590ddab..456a76a 100644 --- a/plugins/stormtrade/package-lock.json +++ b/plugins/stormtrade/package-lock.json @@ -1,12 +1,12 @@ { "name": "teleton-plugin-stormtrade", - "version": "1.0.0", + "version": "2.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "teleton-plugin-stormtrade", - "version": "1.0.0", + "version": "2.0.0", "dependencies": { "@orbs-network/ton-access": "^2.3.3", "@storm-trade/sdk": "^1.0.0-rc.4" @@ -84,6 +84,18 @@ "@ton/crypto": ">=3.2.0" } }, + "node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "license": "MIT", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", @@ -91,14 +103,15 @@ "license": "MIT" }, "node_modules/axios": { - "version": "1.13.5", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.5.tgz", - "integrity": "sha512-cz4ur7Vb0xS4/KUN0tPWe44eqxrIu31me+fbang3ijiNscE129POzipJJA6zniq2C/Z6sJCjMimjS8Lc/GAs8Q==", + "version": "1.18.1", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.18.1.tgz", + "integrity": "sha512-3nTvFlvpn9Zu/RkHUqtc7/+al4UpRW5az71ap5zccp6e8RAYEzhMTecX8Dz1wWDYrPpUoB1HAQEGEAEvUr7S9g==", "license": "MIT", "dependencies": { - "follow-redirects": "^1.15.11", + "follow-redirects": "^1.16.0", "form-data": "^4.0.5", - "proxy-from-env": "^1.1.0" + "https-proxy-agent": "^5.0.1", + "proxy-from-env": "^2.1.0" } }, "node_modules/call-bind-apply-helpers": { @@ -133,6 +146,23 @@ "license": "MIT", "peer": true }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, "node_modules/delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", @@ -202,9 +232,9 @@ } }, "node_modules/follow-redirects": { - "version": "1.15.11", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", - "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz", + "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==", "funding": [ { "type": "individual", @@ -222,16 +252,16 @@ } }, "node_modules/form-data": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", - "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.6.tgz", + "integrity": "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==", "license": "MIT", "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", - "hasown": "^2.0.2", - "mime-types": "^2.1.12" + "hasown": "^2.0.4", + "mime-types": "^2.1.35" }, "engines": { "node": ">= 6" @@ -323,9 +353,9 @@ } }, "node_modules/hasown": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", "license": "MIT", "dependencies": { "function-bind": "^1.1.2" @@ -334,6 +364,19 @@ "node": ">= 0.4" } }, + "node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "license": "MIT", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/isomorphic-fetch": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/isomorphic-fetch/-/isomorphic-fetch-3.0.0.tgz", @@ -384,6 +427,12 @@ "node": ">= 0.6" } }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, "node_modules/node-fetch": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", @@ -405,10 +454,13 @@ } }, "node_modules/proxy-from-env": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", - "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", - "license": "MIT" + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz", + "integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==", + "license": "MIT", + "engines": { + "node": ">=10" + } }, "node_modules/tr46": { "version": "0.0.3", diff --git a/plugins/stormtrade/package.json b/plugins/stormtrade/package.json index 75b429c..e2b6be5 100644 --- a/plugins/stormtrade/package.json +++ b/plugins/stormtrade/package.json @@ -1,7 +1,7 @@ { "name": "teleton-plugin-stormtrade", "type": "module", - "version": "1.0.0", + "version": "2.0.0", "private": true, "dependencies": { "@storm-trade/sdk": "^1.0.0-rc.4", diff --git a/plugins/swapcoffee/README.md b/plugins/swapcoffee/README.md index 0a622c9..3d6a559 100644 --- a/plugins/swapcoffee/README.md +++ b/plugins/swapcoffee/README.md @@ -2,7 +2,7 @@ Swap tokens on TON via [swap.coffee](https://swap.coffee) DEX aggregator -- finds the best rate across STON.fi, DeDust, Tonco, Coffee DEX, and 15+ other DEXes. -Read tools use the swap.coffee REST API. Write tools sign transactions from the agent wallet. +Read tools use the swap.coffee REST API. Write tools send transactions through Teleton's protected wallet broker. ## Tools @@ -45,10 +45,7 @@ Ask the AI: Requires at runtime (provided by teleton): - `@ton/core` -- Cell parsing, Address -- `@ton/ton` -- WalletContractV5R1, TonClient -- `@ton/crypto` -- mnemonicToPrivateKey - -Agent wallet at `~/.teleton/wallet.json` is used for signing all on-chain transactions. +- Teleton Plugin SDK v2 -- protected transaction broker ## Schemas diff --git a/plugins/swapcoffee/index.js b/plugins/swapcoffee/index.js index 30fb3d5..6335e06 100644 --- a/plugins/swapcoffee/index.js +++ b/plugins/swapcoffee/index.js @@ -3,31 +3,24 @@ * * Find optimal swap routes, execute token swaps, browse pools, and check * token prices across all major TON DEXes (StonFi, DeDust, etc.). - * Agent wallet at ~/.teleton/wallet.json signs all swap transactions. + * Teleton's SDK wallet broker signs all swap transactions. */ import { createRequire } from "node:module"; -import { readFileSync, realpathSync } from "node:fs"; -import { join } from "node:path"; -import { homedir } from "node:os"; +import { realpathSync } from "node:fs"; // --------------------------------------------------------------------------- // CJS dependencies // --------------------------------------------------------------------------- const _require = createRequire(realpathSync(process.argv[1])); // core: @ton/core, @ton/ton, @ton/crypto -const _pluginRequire = createRequire(import.meta.url); // local: plugin-specific deps - -const { Cell, Address, SendMode } = _require("@ton/core"); -const { WalletContractV5R1, TonClient, internal } = _require("@ton/ton"); -const { mnemonicToPrivateKey } = _require("@ton/crypto"); +const { Cell } = _require("@ton/core"); // --------------------------------------------------------------------------- // Constants // --------------------------------------------------------------------------- const API_BASE = "https://backend.swap.coffee"; -const WALLET_FILE = join(homedir(), ".teleton", "wallet.json"); let _sdk = null; @@ -54,41 +47,6 @@ async function swapFetch(path, opts = {}) { return res.json(); } -// --------------------------------------------------------------------------- -// Wallet helper -// --------------------------------------------------------------------------- - -async function getWalletAndClient() { - let walletData; - try { - walletData = JSON.parse(readFileSync(WALLET_FILE, "utf-8")); - } catch { - throw new Error("Agent wallet not found at " + WALLET_FILE); - } - if (!walletData.mnemonic || !Array.isArray(walletData.mnemonic)) { - throw new Error("Invalid wallet file: missing mnemonic array"); - } - - const keyPair = await mnemonicToPrivateKey(walletData.mnemonic); - const wallet = WalletContractV5R1.create({ - workchain: 0, - publicKey: keyPair.publicKey, - }); - - let endpoint; - try { - const { getHttpEndpoint } = _pluginRequire("@orbs-network/ton-access"); - endpoint = await getHttpEndpoint({ network: "mainnet" }); - } catch { - endpoint = "https://toncenter.com/api/v2/jsonRPC"; - } - - const client = new TonClient({ endpoint }); - const contract = client.open(wallet); - - return { wallet, keyPair, client, contract }; -} - // --------------------------------------------------------------------------- // Tool 1: swap_quote // --------------------------------------------------------------------------- @@ -262,9 +220,9 @@ const swapExecute = { throw new Error("No swap route found for the given token pair"); } - // Step 2: Get wallet - const { wallet, keyPair, contract } = await getWalletAndClient(); - const senderAddress = wallet.address.toString(); + // Step 2: Get the protected core wallet address + const senderAddress = _sdk.ton.getAddress(); + if (!senderAddress) throw new Error("Agent wallet is not initialized"); // Step 3: Get transactions from the route const txBody = { @@ -289,22 +247,16 @@ const swapExecute = { // Step 4: Build messages const messages = txData.transactions.map((tx) => { const body = Cell.fromBoc(Buffer.from(tx.cell, "base64"))[0]; - return internal({ - to: Address.parse(tx.address), - value: BigInt(tx.value), + return { + to: tx.address, + value: Number(_sdk.ton.fromNano(BigInt(tx.value))), body, bounce: true, - }); + }; }); - // Step 5: Send transfer - const seqno = await contract.getSeqno(); - await contract.sendTransfer({ - seqno, - secretKey: keyPair.secretKey, - sendMode: SendMode.PAY_GAS_SEPARATELY | SendMode.IGNORE_ERRORS, - messages, - }); + // Step 5: Send through the core transaction broker + const sent = await _sdk.ton.sendMessages(messages, { sendMode: 3 }); return { success: true, @@ -314,7 +266,8 @@ const swapExecute = { price_impact: routeData.price_impact, slippage, transactions_sent: txData.transactions.length, - seqno, + seqno: sent.seqno, + hash: sent.hash, wallet_address: senderAddress, message: "Swap transaction sent. Use swap_status with route_id to check completion (~30 seconds).", @@ -689,8 +642,8 @@ const swapPools = { export const manifest = { name: "swapcoffee", - version: "1.0.0", - sdkVersion: ">=1.0.0", + version: "2.0.0", + sdkVersion: "^2.0.0", description: "swap.coffee DEX aggregator on TON โ€” find optimal swap routes, execute token swaps, browse pools, and check token prices.", }; diff --git a/plugins/swapcoffee/manifest.json b/plugins/swapcoffee/manifest.json index e009f77..77ef9e2 100644 --- a/plugins/swapcoffee/manifest.json +++ b/plugins/swapcoffee/manifest.json @@ -1,13 +1,13 @@ { "id": "swapcoffee", "name": "swap.coffee DEX Aggregator", - "version": "1.0.0", + "version": "2.0.0", "description": "Swap tokens on TON via swap.coffee aggregator - best rates across all DEXes", "author": { "name": "teleton", "url": "https://github.com/TONresistor" }, "license": "MIT", "entry": "index.js", - "teleton": ">=1.0.0", - "sdkVersion": ">=1.0.0", + "teleton": ">=0.9.0", + "sdkVersion": "^2.0.0", "tools": [ { "name": "swap_quote", "description": "Get optimal swap route with expected output and price impact" }, { "name": "swap_execute", "description": "Execute a token swap from the agent wallet" }, diff --git a/plugins/swapcoffee/package-lock.json b/plugins/swapcoffee/package-lock.json deleted file mode 100644 index be153c4..0000000 --- a/plugins/swapcoffee/package-lock.json +++ /dev/null @@ -1,82 +0,0 @@ -{ - "name": "teleton-plugin-swapcoffee", - "version": "1.0.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "teleton-plugin-swapcoffee", - "version": "1.0.0", - "dependencies": { - "@orbs-network/ton-access": "^2.3.3" - } - }, - "node_modules/@orbs-network/ton-access": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/@orbs-network/ton-access/-/ton-access-2.3.3.tgz", - "integrity": "sha512-b1miCPts7wBG9JKYgzXIRZQm/LMy5Uk1mNK8NzlcXHL3HRHJkkFbuYJGuj3IkWCiIicW3Ipp4sYnn3Fwo4oB0g==", - "license": "MIT", - "dependencies": { - "isomorphic-fetch": "^3.0.0" - } - }, - "node_modules/isomorphic-fetch": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/isomorphic-fetch/-/isomorphic-fetch-3.0.0.tgz", - "integrity": "sha512-qvUtwJ3j6qwsF3jLxkZ72qCgjMysPzDfeV240JHiGZsANBYd+EEuu35v7dfrJ9Up0Ak07D7GGSkGhCHTqg/5wA==", - "license": "MIT", - "dependencies": { - "node-fetch": "^2.6.1", - "whatwg-fetch": "^3.4.1" - } - }, - "node_modules/node-fetch": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", - "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", - "license": "MIT", - "dependencies": { - "whatwg-url": "^5.0.0" - }, - "engines": { - "node": "4.x || >=6.0.0" - }, - "peerDependencies": { - "encoding": "^0.1.0" - }, - "peerDependenciesMeta": { - "encoding": { - "optional": true - } - } - }, - "node_modules/tr46": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", - "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", - "license": "MIT" - }, - "node_modules/webidl-conversions": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", - "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", - "license": "BSD-2-Clause" - }, - "node_modules/whatwg-fetch": { - "version": "3.6.20", - "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.6.20.tgz", - "integrity": "sha512-EqhiFU6daOA8kpjOWTL0olhVOF3i7OrFzSYiGsEMB8GcXS+RrzauAERX65xMeNWVqxA6HXH2m69Z9LaKKdisfg==", - "license": "MIT" - }, - "node_modules/whatwg-url": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", - "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", - "license": "MIT", - "dependencies": { - "tr46": "~0.0.3", - "webidl-conversions": "^3.0.0" - } - } - } -} diff --git a/plugins/tonapi/index.js b/plugins/tonapi/index.js index 823e5bd..9bdba8e 100644 --- a/plugins/tonapi/index.js +++ b/plugins/tonapi/index.js @@ -16,7 +16,7 @@ const API_BASE = "https://tonapi.io"; export const manifest = { name: "tonapi", version: "1.0.0", - sdkVersion: ">=1.0.0", + sdkVersion: "^2.0.0", description: "TON blockchain data from TONAPI -- accounts, jettons, NFTs, prices, transactions, traces, DNS, staking", }; diff --git a/plugins/tonapi/manifest.json b/plugins/tonapi/manifest.json index 0eb0033..174dd8d 100644 --- a/plugins/tonapi/manifest.json +++ b/plugins/tonapi/manifest.json @@ -9,8 +9,8 @@ }, "license": "MIT", "entry": "index.js", - "sdkVersion": ">=1.0.0", - "teleton": ">=1.0.0", + "sdkVersion": "^2.0.0", + "teleton": ">=0.9.0", "secrets": { "api_key": { "required": false, "description": "TONAPI Bearer token for higher rate limits (optional, public API works without it)" } }, diff --git a/plugins/twitter/index.js b/plugins/twitter/index.js index 08d3240..9ec355c 100644 --- a/plugins/twitter/index.js +++ b/plugins/twitter/index.js @@ -101,7 +101,7 @@ function formatUser(u) { export const manifest = { name: "twitter", version: "3.0.0", - sdkVersion: ">=1.0.0", + sdkVersion: "^2.0.0", description: "X/Twitter API v2 โ€” read (search, lookup, trends) and write (post, like, retweet, follow) with OAuth 1.0a.", }; diff --git a/plugins/twitter/manifest.json b/plugins/twitter/manifest.json index 2a825f7..db66cb9 100644 --- a/plugins/twitter/manifest.json +++ b/plugins/twitter/manifest.json @@ -6,8 +6,8 @@ "author": { "name": "teleton", "url": "https://github.com/TONresistor" }, "license": "MIT", "entry": "index.js", - "sdkVersion": ">=1.0.0", - "teleton": ">=1.0.0", + "sdkVersion": "^2.0.0", + "teleton": ">=0.9.0", "secrets": { "bearer_token": { "required": true, "description": "Bearer Token for read-only API access (from X Developer Portal)" }, "consumer_key": { "required": false, "description": "Consumer Key (API Key) for OAuth 1.0a write access" }, diff --git a/plugins/uranus/README.md b/plugins/uranus/README.md new file mode 100644 index 0000000..a9acaf3 --- /dev/null +++ b/plugins/uranus/README.md @@ -0,0 +1,51 @@ +# Uranus Meme Launchpad + +Production-oriented Teleton SDK v2.1 plugin for the Uranus Meme lifecycle on TON mainnet. It verifies contract code before financial actions and delegates every signature to Teleton's protected transaction broker. + +## Tools + +| Tool | Access | Purpose | +|---|---|---| +| `uranus_protocol_info` | Public read | Deployments, hashes, presets and limits | +| `uranus_recent_launches` | Public read | Decode recent known-factory launches | +| `uranus_meme_info` | Public read | Meme, curve, partner and metadata state | +| `uranus_wallet_info` | Public read | Derived Meme Wallet and verified balance | +| `uranus_portfolio` | Admin read | Code-verified Uranus holdings | +| `uranus_quote` | Public read | Fresh curve or migrated DeDust quote | +| `uranus_recent_trades` | Public read | Decoded Buy/Sell events and fees | +| `uranus_buy` / `uranus_sell` | Admin + approval | Curve or DeDust trade | +| `uranus_launch_preset` / `uranus_launch_custom` | Admin + approval | v3.1 factory launch | +| `uranus_claim_creator_fee` / `uranus_claim_partner_fee` | Admin + approval | Claim to verified on-chain destination | + +## Configuration + +```yaml +plugins: + uranus: + maxActionTon: "50" + defaultSlippageBps: 500 + partnerId: null + partnerFeeBps: 0 + referrerId: null + referrerFeeBps: 0 + allowThirdPartyPartnerClaim: false +``` + +`toncenter_api_key` is an optional plugin secret. Public reads still work without it, subject to Toncenter rate limits. Affiliate IDs are trusted static configuration and cannot be supplied by a tool call. + +`allowThirdPartyPartnerClaim` remains fail-closed in v1: current mainnet evidence proves claims sent by the stored partner, but does not prove permissionless third-party triggering. Enabling the setting cannot redirect payouts or bypass this proof gate. + +## Safety model + +- Mainnet only; deployments always target the active Uranus v3.1 factory. +- Amounts are decimal strings and contract calculations use `bigint`. +- The plugin generates query IDs and minimum outputs internally. +- Code hash, wallet owner/master and payout authorization are checked before writes. +- A Teleton wallet commit is not treated as downstream completion: curve balances, fee state or factory output are polled for up to 30 seconds. +- A timeout returns `submitted_unsettled`, never a false confirmed result. +- Graduated and migrated tokens route only through DeDust CPMM. +- Metadata responses are capped at 2 MiB, time out after 10 seconds and reject local/private redirect targets. + +Observed conservative attached-value budgets are 0.10 TON for buys, 0.12 TON for sells, 0.50 TON for launches, 0.10 TON for creator claims and 0.30 TON for partner claims. These are safe message budgets, not Uranus protocol fees; unused value may be refunded by the contracts. + +Uranus reference documentation is currently marked under construction. The plugin combines documented layouts with verified mainnet code hashes and fails closed on unsupported identity or state. diff --git a/plugins/uranus/abi.js b/plugins/uranus/abi.js new file mode 100644 index 0000000..ed9e901 --- /dev/null +++ b/plugins/uranus/abi.js @@ -0,0 +1,233 @@ +import { Address, beginCell } from "@ton/core"; +import { OPCODES } from "./constants.js"; +import { UranusError } from "./errors.js"; +import { parseUint } from "./amounts.js"; + +function address(value, field = "address") { + try { + return value instanceof Address ? value : Address.parse(String(value)); + } catch { + throw new UranusError("INVALID_ADDRESS", `${field} is not a valid TON address`); + } +} + +function storeInlineAffiliate(builder, config, kind) { + if (!config) return builder.storeBit(false); + const idField = kind === "partner" ? "partnerId" : "referrerId"; + const feeField = kind === "partner" ? "partnerFeeBps" : "referrerFeeBps"; + builder.storeBit(true).storeUint(parseUint(config[idField], 256, idField), 256).storeUint(parseUint(config[feeField], 16, feeField), 16); + return builder; +} + +function affiliateCell(config, kind) { + if (!config) return null; + const idField = kind === "partner" ? "partnerId" : "referrerId"; + const feeField = kind === "partner" ? "partnerFeeBps" : "referrerFeeBps"; + return beginCell() + .storeUint(parseUint(config[idField], 256, idField), 256) + .storeUint(parseUint(config[feeField], 16, feeField), 16) + .endCell(); +} + +export function buildDeployMeme(args) { + const builder = beginCell() + .storeUint(OPCODES.DEPLOY_MEME, 32) + .storeUint(parseUint(args.queryId, 64, "queryId"), 64) + .storeUint(parseUint(args.presetId, 4, "presetId"), 4) + .storeStringRefTail(args.metadataUri) + .storeCoins(BigInt(args.initialBuy)); + storeInlineAffiliate(builder, args.partnerConfig, "partner"); + storeInlineAffiliate(builder, args.referrerConfig, "referrer"); + return builder.endCell(); +} + +export function buildDeployCustomizedMeme(args) { + return beginCell() + .storeUint(OPCODES.DEPLOY_CUSTOMIZED_MEME, 32) + .storeUint(parseUint(args.queryId, 64, "queryId"), 64) + .storeUint(parseUint(args.totalSupplyPresetId, 4, "totalSupplyPresetId"), 4) + .storeUint(parseUint(args.baseFeePresetId, 4, "baseFeePresetId"), 4) + .storeCoins(BigInt(args.raisingFunds)) + .storeUint(parseUint(args.onSellSupplyPercent, 8, "onSellSupplyPercent"), 8) + .storeAddress(address(args.partnerAddress, "partnerAddress")) + .storeUint(parseUint(args.partnerFeeBps, 16, "partnerFeeBps"), 16) + .storeUint(parseUint(args.poolPartnerFeeBps, 16, "poolPartnerFeeBps"), 16) + .storeUint(parseUint(args.poolBaseFeePresetId, 16, "poolBaseFeePresetId"), 16) + .storeAddress(args.poolLiquidityOwnerAddress ? address(args.poolLiquidityOwnerAddress, "poolLiquidityOwnerAddress") : null) + .storeStringRefTail(args.metadataUri) + .storeCoins(BigInt(args.initialBuy)) + .storeMaybeRef(affiliateCell(args.partnerConfig, "partner")) + .storeMaybeRef(affiliateCell(args.referrerConfig, "referrer")) + .endCell(); +} + +export function buildBuy(args) { + return beginCell() + .storeUint(OPCODES.BUY, 32) + .storeUint(parseUint(args.queryId, 64, "queryId"), 64) + .storeCoins(BigInt(args.amount)) + .storeCoins(BigInt(args.minimalAmountOut)) + .storeAddress(address(args.excessesTo, "excessesTo")) + .storeMaybeRef(affiliateCell(args.partnerConfig, "partner")) + .storeMaybeRef(affiliateCell(args.referrerConfig, "referrer")) + .endCell(); +} + +export function decodeBuy(cell) { + const slice = cell.beginParse(); + requireOpcode(slice, OPCODES.BUY, "Buy"); + return { + queryId: slice.loadUintBig(64), + amount: slice.loadCoins(), + minimalAmountOut: slice.loadCoins(), + excessesTo: slice.loadAddress(), + partnerConfig: loadAffiliateRef(slice, "partner"), + referrerConfig: loadAffiliateRef(slice, "referrer"), + }; +} + +export function buildSellTokens(args) { + return beginCell() + .storeUint(OPCODES.SELL_TOKENS, 32) + .storeUint(parseUint(args.queryId, 64, "queryId"), 64) + .storeCoins(BigInt(args.amount)) + .storeCoins(BigInt(args.minimalAmountOut)) + .storeAddress(args.excessesTo ? address(args.excessesTo, "excessesTo") : null) + .storeMaybeRef(affiliateCell(args.partnerConfig, "partner")) + .storeMaybeRef(affiliateCell(args.referrerConfig, "referrer")) + .endCell(); +} + +export function decodeSellTokens(cell) { + const slice = cell.beginParse(); + requireOpcode(slice, OPCODES.SELL_TOKENS, "SellTokens"); + return { + queryId: slice.loadUintBig(64), + amount: slice.loadCoins(), + minimalAmountOut: slice.loadCoins(), + excessesTo: slice.loadMaybeAddress(), + partnerConfig: loadAffiliateRef(slice, "partner"), + referrerConfig: loadAffiliateRef(slice, "referrer"), + }; +} + +function buildClaim(opcode, args) { + return beginCell() + .storeUint(opcode, 32) + .storeUint(parseUint(args.queryId, 64, "queryId"), 64) + .storeAddress(args.to ? address(args.to, "to") : null) + .storeAddress(args.excessesTo ? address(args.excessesTo, "excessesTo") : null) + .endCell(); +} + +export const buildClaimCreatorFee = (args) => buildClaim(OPCODES.CLAIM_CREATOR_FEE, args); +export const buildClaimPartnerFee = (args) => buildClaim(OPCODES.CLAIM_PARTNER_FEE, args); + +function requireOpcode(slice, expected, name) { + if (slice.remainingBits < 32 || slice.loadUint(32) !== expected) throw new UranusError("INVALID_RESPONSE", `Not a ${name} body`); +} + +function loadInlineAffiliate(slice, kind) { + if (!slice.loadBoolean()) return null; + return kind === "partner" + ? { partnerId: slice.loadUintBig(256), partnerFeeBps: slice.loadUint(16) } + : { referrerId: slice.loadUintBig(256), referrerFeeBps: slice.loadUint(16) }; +} + +export function decodeInitMeme(cell) { + const slice = cell.beginParse(); + requireOpcode(slice, OPCODES.INIT_MEME, "InitMeme"); + return { + queryId: slice.loadUintBig(64), + initialBuy: slice.loadCoins(), + partnerConfig: loadInlineAffiliate(slice, "partner"), + referrerConfig: loadInlineAffiliate(slice, "referrer"), + }; +} + +export function decodeDeployMeme(cell) { + const slice = cell.beginParse(); + requireOpcode(slice, OPCODES.DEPLOY_MEME, "DeployMeme"); + return { + kind: "preset", + queryId: slice.loadUintBig(64), + presetId: slice.loadUint(4), + metadataUri: slice.loadStringRefTail(), + initialBuy: slice.loadCoins(), + partnerConfig: loadInlineAffiliate(slice, "partner"), + referrerConfig: loadInlineAffiliate(slice, "referrer"), + }; +} + +function loadAffiliateRef(slice, kind) { + const ref = slice.loadMaybeRef(); + if (!ref) return null; + const data = ref.beginParse(); + return kind === "partner" + ? { partnerId: data.loadUintBig(256), partnerFeeBps: data.loadUint(16) } + : { referrerId: data.loadUintBig(256), referrerFeeBps: data.loadUint(16) }; +} + +export function decodeDeployCustomizedMeme(cell) { + const slice = cell.beginParse(); + requireOpcode(slice, OPCODES.DEPLOY_CUSTOMIZED_MEME, "DeployCustomizedMeme"); + return { + kind: "custom", + queryId: slice.loadUintBig(64), + totalSupplyPresetId: slice.loadUint(4), + baseFeePresetId: slice.loadUint(4), + raisingFunds: slice.loadCoins(), + onSellSupplyPercent: slice.loadUint(8), + partnerAddress: slice.loadAddress(), + partnerFeeBps: slice.loadUint(16), + poolPartnerFeeBps: slice.loadUint(16), + poolBaseFeePresetId: slice.loadUint(16), + poolLiquidityOwnerAddress: slice.loadMaybeAddress(), + metadataUri: slice.loadStringRefTail(), + initialBuy: slice.loadCoins(), + partnerConfig: loadAffiliateRef(slice, "partner"), + referrerConfig: loadAffiliateRef(slice, "referrer"), + }; +} + +function loadTradeFees(slice) { + return { + creatorFee: slice.loadCoins(), + protocolFee: slice.loadCoins(), + partnerFee: slice.loadCoins(), + referrerFee: slice.loadCoins(), + }; +} + +export function decodeTradeEvent(cell) { + const slice = cell.beginParse(); + if (slice.remainingBits < 32) return null; + const opcode = slice.preloadUint(32); + if (opcode !== OPCODES.BUY_EVENT && opcode !== OPCODES.SELL_EVENT) return null; + slice.loadUint(32); + const event = { + kind: opcode === OPCODES.BUY_EVENT ? "buy" : "sell", + traderAddress: slice.loadAddress(), + amountIn: slice.loadCoins(), + amountOut: slice.loadCoins(), + fees: loadTradeFees(slice), + currentSupply: slice.loadCoins(), + raisedFunds: slice.loadCoins(), + }; + if (opcode === OPCODES.BUY_EVENT) event.isGraduated = slice.loadBoolean(); + return event; +} + +export function decodeClaim(cell, expectedOpcode) { + const slice = cell.beginParse(); + requireOpcode(slice, expectedOpcode, "claim"); + return { + queryId: slice.loadUintBig(64), + to: slice.loadMaybeAddress(), + excessesTo: slice.loadMaybeAddress(), + }; +} + +export function normalizeAddress(value) { + return address(value).toString(); +} diff --git a/plugins/uranus/actions.js b/plugins/uranus/actions.js new file mode 100644 index 0000000..ea1dc50 --- /dev/null +++ b/plugins/uranus/actions.js @@ -0,0 +1,253 @@ +import { randomBytes } from "node:crypto"; +import { ACTIVE_FACTORY, GAS_NANO, SETTLEMENT } from "./constants.js"; +import { buildBuy, buildClaimCreatorFee, buildClaimPartnerFee, buildDeployCustomizedMeme, buildDeployMeme, buildSellTokens, normalizeAddress } from "./abi.js"; +import { formatUnits, nanoToTonNumber, parseUnits } from "./amounts.js"; +import { UranusError } from "./errors.js"; +import { normalizeMetadataUri } from "./http.js"; + +// The 100 most recent successful mainnet claims sampled on 2026-07-11 all had +// sender == stored partner == explicit payout. Partner authorization is proven; +// permissionless third-party triggering is not, so that path stays fail-closed. +const THIRD_PARTY_PARTNER_CLAIM_PROVEN = false; + +function queryId() { + return randomBytes(8).readBigUInt64BE(); +} + +function txFields(result) { + const txRef = result?.hash ?? result?.txRef ?? null; + return { tx_ref: txRef, explorer: txRef ? `https://tonviewer.com/transaction/${txRef}` : null }; +} + +function affiliateConfig(config, kind) { + const id = config[`${kind}Id`]; + const fee = Number(config[`${kind}FeeBps`] ?? 0); + if (id == null || id === "") { + if (fee !== 0) throw new UranusError("INVALID_CONFIG", `${kind}FeeBps requires ${kind}Id`); + return null; + } + if (!Number.isInteger(fee) || fee < 0 || fee > 10_000) throw new UranusError("INVALID_CONFIG", `${kind}FeeBps must be between 0 and 10000`); + return kind === "partner" ? { partnerId: id, partnerFeeBps: fee } : { referrerId: id, referrerFeeBps: fee }; +} + +function sleep(ms) { + return new Promise((resolve) => setTimeout(resolve, ms)); +} + +export function createActions(sdk, deps, options = {}) { + const attempts = options.attempts ?? SETTLEMENT.ATTEMPTS; + const intervalMs = options.intervalMs ?? SETTLEMENT.INTERVAL_MS; + const pause = options.sleep ?? sleep; + const config = deps.config; + const partnerConfig = affiliateConfig(config, "partner"); + const referrerConfig = affiliateConfig(config, "referrer"); + + function wallet() { + const value = sdk.ton.getAddress(); + if (!value) throw new UranusError("WALLET_NOT_INITIALIZED", "Teleton wallet is not initialized"); + return normalizeAddress(value); + } + + async function requireTon(requiredNano) { + const balance = await sdk.ton.getBalance(); + if (!balance) throw new UranusError("UPSTREAM_UNAVAILABLE", "Unable to read the Teleton wallet balance"); + if (BigInt(balance.balanceNano) < requiredNano + GAS_NANO.ACTION_HEADROOM) { + throw new UranusError("INSUFFICIENT_TON", `Wallet needs at least ${formatUnits(requiredNano + GAS_NANO.ACTION_HEADROOM)} TON`); + } + } + + function enforceSpend(amountNano) { + const cap = parseUnits(String(config.maxActionTon), 9, "maxActionTon"); + if (amountNano > cap) throw new UranusError("BALANCE_EXCEEDED", `Action exceeds configured ${config.maxActionTon} TON cap`); + } + + async function financialInfo(address) { + const info = await deps.state.memeInfo(address); + if (!info.verified) throw new UranusError("UNVERIFIED_CONTRACT", "Meme contract code hash is not a supported Uranus deployment"); + if (!info.writable_curve && !info.migrated) throw new UranusError("UNSUPPORTED_VERSION", "This Uranus version is read-only"); + return info; + } + + async function poll(check) { + for (let i = 0; i < attempts; i += 1) { + if (i > 0 || intervalMs > 0) await pause(intervalMs); + try { + const value = await check(); + if (value) return value; + } catch (error) { + sdk.log?.debug?.(`uranus: settlement poll ${i + 1} failed: ${String(error?.message ?? error)}`); + } + } + return null; + } + + async function buy(params) { + const owner = wallet(); + const info = await financialInfo(params.meme_address); + const amountNano = parseUnits(params.amount_ton, 9, "amount_ton"); + enforceSpend(amountNano); + if (info.migrated) { + await requireTon(amountNano + GAS_NANO.BUY); + const result = await sdk.ton.dex.swapDeDust({ fromAsset: "ton", toAsset: info.address, amount: Number(params.amount_ton), slippage: (params.slippage_bps ?? config.defaultSlippageBps) / 10_000 }); + return { status: "confirmed", route: "dedust", ...txFields(result), expected_output: result.expectedOutput, minimum_output: result.minOutput }; + } + const quote = await deps.quote({ meme_address: info.address, direction: "buy", amount: params.amount_ton, slippage_bps: params.slippage_bps }); + const before = await deps.state.walletInfo(info.address, owner); + await requireTon(amountNano + GAS_NANO.BUY); + const id = queryId(); + const body = buildBuy({ queryId: id, amount: amountNano, minimalAmountOut: BigInt(quote.minimum_output_raw), excessesTo: owner, partnerConfig, referrerConfig }); + const sent = await sdk.ton.send(info.address, nanoToTonNumber(amountNano + GAS_NANO.BUY), { body, bounce: true }); + const after = await poll(async () => { + const value = await deps.state.walletInfo(info.address, owner); + return value._rawBalance > before._rawBalance ? value : null; + }); + return { + status: after ? "confirmed" : "submitted_unsettled", + route: "uranus_curve", + query_id: id.toString(), + ...txFields(sent), + expected_output: quote.expected_output, + minimum_output: quote.minimum_output, + actual_token_delta: after ? formatUnits(after._rawBalance - before._rawBalance) : null, + }; + } + + async function sell(params) { + const owner = wallet(); + const info = await financialInfo(params.meme_address); + const amountNano = parseUnits(params.amount_tokens, info.decimals, "amount_tokens"); + if (info.migrated) { + const holdings = await deps.state.walletInfo(info.address, owner); + if (!holdings.verified || amountNano > holdings._rawBalance) { + throw new UranusError("INSUFFICIENT_JETTON", "Sell amount exceeds the verified Meme Wallet balance"); + } + await requireTon(GAS_NANO.SELL); + const result = await sdk.ton.dex.swapDeDust({ fromAsset: info.address, toAsset: "ton", amount: Number(params.amount_tokens), slippage: (params.slippage_bps ?? config.defaultSlippageBps) / 10_000 }); + return { status: "confirmed", route: "dedust", ...txFields(result), expected_output: result.expectedOutput, minimum_output: result.minOutput }; + } + const before = await deps.state.walletInfo(info.address, owner); + if (!before.verified) throw new UranusError("WALLET_NOT_INITIALIZED", "Uranus Meme Wallet is not active or failed owner/master verification"); + if (amountNano > before._rawBalance) throw new UranusError("INSUFFICIENT_JETTON", "Sell amount exceeds the Meme Wallet balance"); + const quote = await deps.quote({ meme_address: info.address, direction: "sell", amount: params.amount_tokens, slippage_bps: params.slippage_bps }); + await requireTon(GAS_NANO.SELL); + const id = queryId(); + const body = buildSellTokens({ queryId: id, amount: amountNano, minimalAmountOut: BigInt(quote.minimum_output_raw), excessesTo: owner, partnerConfig, referrerConfig }); + const sent = await sdk.ton.send(before.wallet_address, nanoToTonNumber(GAS_NANO.SELL), { body, bounce: true }); + const after = await poll(async () => { + const value = await deps.state.walletInfo(info.address, owner); + return value._rawBalance < before._rawBalance ? value : null; + }); + return { + status: after ? "confirmed" : "submitted_unsettled", + route: "uranus_curve", + query_id: id.toString(), + ...txFields(sent), + expected_output: quote.expected_output, + minimum_output: quote.minimum_output, + actual_token_delta: after ? formatUnits(before._rawBalance - after._rawBalance, info.decimals) : null, + }; + } + + async function confirmLaunch(id, creator) { + return poll(async () => { + const launches = await deps.history.recentLaunches({ limit: 50, factory_version: "v3.1" }); + const launch = launches.find((value) => value.query_id === id.toString()); + if (!launch || normalizeAddress(launch.creator_address) !== creator) return null; + const identity = await deps.state.inspectContract(launch.meme_address); + return identity.verified && identity.version === "v3.1" ? launch : null; + }); + } + + async function launchPreset(params) { + const creator = wallet(); + if (!Number.isInteger(params.preset_id) || params.preset_id < 3 || params.preset_id > 9) throw new UranusError("PRESET_NOT_FOUND", "preset_id must be between 3 and 9"); + normalizeMetadataUri(params.metadata_uri); + const initialBuy = parseUnits(params.initial_buy_ton ?? "0", 9, "initial_buy_ton", { allowZero: true }); + enforceSpend(initialBuy); + await requireTon(initialBuy + GAS_NANO.DEPLOY); + const id = queryId(); + const body = buildDeployMeme({ queryId: id, presetId: params.preset_id, metadataUri: params.metadata_uri, initialBuy, partnerConfig, referrerConfig }); + const sent = await sdk.ton.send(ACTIVE_FACTORY, nanoToTonNumber(initialBuy + GAS_NANO.DEPLOY), { body, bounce: true }); + const launch = await confirmLaunch(id, creator); + return { status: launch ? "confirmed" : "submitted_unsettled", query_id: id.toString(), ...txFields(sent), meme_address: launch?.meme_address ?? null }; + } + + async function launchCustom(params) { + const creator = wallet(); + const integer = (value, min, max, code, field) => { + if (!Number.isInteger(value) || value < min || value > max) throw new UranusError(code, `${field} must be between ${min} and ${max}`); + return value; + }; + integer(params.total_supply_preset_id, 0, 2, "SUPPLY_PRESET_NOT_FOUND", "total_supply_preset_id"); + integer(params.base_fee_preset_id, 0, 7, "FEE_PRESET_NOT_FOUND", "base_fee_preset_id"); + integer(params.pool_base_fee_preset_id, 0, 7, "FEE_PRESET_NOT_FOUND", "pool_base_fee_preset_id"); + integer(params.on_sell_supply_percent, 60, 80, "SELL_SUPPLY_OUT_OF_RANGE", "on_sell_supply_percent"); + integer(params.partner_fee_bps, 0, 6000, "PARTNER_FEE_OUT_OF_RANGE", "partner_fee_bps"); + integer(params.pool_partner_fee_bps, 0, 4000, "POOL_PARTNER_FEE_OUT_OF_RANGE", "pool_partner_fee_bps"); + normalizeMetadataUri(params.metadata_uri); + const raise = parseUnits(params.raising_funds_ton, 9, "raising_funds_ton"); + if (raise < 800n * 1_000_000_000n || raise > 1_000_000n * 1_000_000_000n) throw new UranusError("RAISE_OUT_OF_RANGE", "Raise target is outside 800โ€“1,000,000 TON"); + const initialBuy = parseUnits(params.initial_buy_ton ?? "0", 9, "initial_buy_ton", { allowZero: true }); + enforceSpend(initialBuy); + await requireTon(initialBuy + GAS_NANO.DEPLOY); + const id = queryId(); + const body = buildDeployCustomizedMeme({ + queryId: id, + totalSupplyPresetId: params.total_supply_preset_id, + baseFeePresetId: params.base_fee_preset_id, + raisingFunds: raise, + onSellSupplyPercent: params.on_sell_supply_percent, + partnerAddress: params.partner_address ?? creator, + partnerFeeBps: params.partner_fee_bps, + poolPartnerFeeBps: params.pool_partner_fee_bps, + poolBaseFeePresetId: params.pool_base_fee_preset_id, + poolLiquidityOwnerAddress: params.pool_liquidity_owner_address ?? null, + metadataUri: params.metadata_uri, + initialBuy, + partnerConfig, + referrerConfig, + }); + const sent = await sdk.ton.send(ACTIVE_FACTORY, nanoToTonNumber(initialBuy + GAS_NANO.DEPLOY), { body, bounce: true }); + const launch = await confirmLaunch(id, creator); + return { status: launch ? "confirmed" : "submitted_unsettled", query_id: id.toString(), ...txFields(sent), meme_address: launch?.meme_address ?? null }; + } + + async function claimCreatorFee({ meme_address }) { + const owner = wallet(); + const info = await financialInfo(meme_address); + if (normalizeAddress(info.creator_address) !== owner) throw new UranusError("SENDER_UNAUTHORIZED", "Teleton wallet is not the Meme creator"); + const before = BigInt(info.creator_fee_nano ?? 0); + if (before <= 0n) throw new UranusError("BALANCE_EXCEEDED", "No creator fee is currently claimable"); + await requireTon(GAS_NANO.CLAIM_CREATOR); + const id = queryId(); + const sent = await sdk.ton.send(info.address, nanoToTonNumber(GAS_NANO.CLAIM_CREATOR), { body: buildClaimCreatorFee({ queryId: id, to: owner, excessesTo: owner }), bounce: true }); + const after = await poll(async () => { + const value = await deps.state.memeInfo(info.address); + return BigInt(value.creator_fee_nano ?? before) < before ? value : null; + }); + return { status: after ? "confirmed" : "submitted_unsettled", query_id: id.toString(), ...txFields(sent), fee_before_ton: formatUnits(before), fee_after_ton: after?.creator_fee_ton ?? null }; + } + + async function claimPartnerFee({ meme_address }) { + const owner = wallet(); + const info = await financialInfo(meme_address); + if (!info.partner_address) throw new UranusError("INVALID_RESPONSE", "Meme does not expose partner state"); + const partner = normalizeAddress(info.partner_address); + if (partner !== owner && !config.allowThirdPartyPartnerClaim) throw new UranusError("SENDER_UNAUTHORIZED", "Teleton wallet is not the stored Uranus partner"); + if (partner !== owner && !THIRD_PARTY_PARTNER_CLAIM_PROVEN) { + throw new UranusError("PARTNER_CLAIM_AUTH_UNVERIFIED", "Third-party partner claim authorization is not proven on-chain"); + } + const before = BigInt(info.partner_fee_nano ?? 0); + if (before <= 0n) throw new UranusError("BALANCE_EXCEEDED", "No partner fee is currently claimable"); + await requireTon(GAS_NANO.CLAIM_PARTNER); + const id = queryId(); + const sent = await sdk.ton.send(info.address, nanoToTonNumber(GAS_NANO.CLAIM_PARTNER), { body: buildClaimPartnerFee({ queryId: id, to: partner, excessesTo: partner }), bounce: true }); + const after = await poll(async () => { + const value = await deps.state.memeInfo(info.address); + return BigInt(value.partner_fee_nano ?? before) < before ? value : null; + }); + return { status: after ? "confirmed" : "submitted_unsettled", query_id: id.toString(), ...txFields(sent), payout_address: partner, fee_before_ton: formatUnits(before), fee_after_ton: after?.partner_fee_ton ?? null }; + } + + return { buy, sell, launchPreset, launchCustom, claimCreatorFee, claimPartnerFee }; +} diff --git a/plugins/uranus/amounts.js b/plugins/uranus/amounts.js new file mode 100644 index 0000000..79b05aa --- /dev/null +++ b/plugins/uranus/amounts.js @@ -0,0 +1,51 @@ +import { UranusError } from "./errors.js"; + +const DECIMAL_RE = /^(0|[1-9]\d*)(?:\.(\d+))?$/; + +export function parseUnits(value, decimals = 9, field = "amount", { allowZero = false } = {}) { + if (typeof value !== "string") throw new UranusError("INVALID_AMOUNT", `${field} must be a decimal string`); + const input = value.trim(); + const match = DECIMAL_RE.exec(input); + if (!match || (match[1].length > 1 && match[1].startsWith("0"))) { + throw new UranusError("INVALID_AMOUNT", `${field} must be a plain positive decimal string`); + } + const fraction = match[2] ?? ""; + if (fraction.length > decimals) throw new UranusError("INVALID_AMOUNT", `${field} supports at most ${decimals} decimals`); + const units = BigInt(match[1]) * 10n ** BigInt(decimals) + BigInt((fraction + "0".repeat(decimals)).slice(0, decimals) || "0"); + if (allowZero ? units < 0n : units <= 0n) throw new UranusError("INVALID_AMOUNT", `${field} must be ${allowZero ? "non-negative" : "positive"}`); + return units; +} + +export function formatUnits(value, decimals = 9) { + const n = BigInt(value); + const sign = n < 0n ? "-" : ""; + const abs = n < 0n ? -n : n; + const scale = 10n ** BigInt(decimals); + const whole = abs / scale; + const fraction = (abs % scale).toString().padStart(decimals, "0").replace(/0+$/, ""); + return `${sign}${whole}${fraction ? `.${fraction}` : ""}`; +} + +export function nanoToTonNumber(value) { + const text = formatUnits(value, 9); + const number = Number(text); + if (!Number.isFinite(number) || number < 0) throw new UranusError("INVALID_AMOUNT", "TON value cannot be represented safely"); + return number; +} + +export function parseUint(value, bits, field) { + let n; + try { + n = typeof value === "bigint" ? value : BigInt(String(value)); + } catch { + throw new UranusError("INVALID_AMOUNT", `${field} must be an unsigned integer`); + } + if (n < 0n || n >= 1n << BigInt(bits)) throw new UranusError("INVALID_AMOUNT", `${field} must fit uint${bits}`); + return n; +} + +export function percentageString(numerator, denominator) { + if (BigInt(denominator) <= 0n) return null; + const hundredths = (BigInt(numerator) * 10_000n) / BigInt(denominator); + return formatUnits(hundredths, 2); +} diff --git a/plugins/uranus/constants.js b/plugins/uranus/constants.js new file mode 100644 index 0000000..1f71df7 --- /dev/null +++ b/plugins/uranus/constants.js @@ -0,0 +1,103 @@ +export const NETWORK = "mainnet"; + +export const FACTORIES = Object.freeze({ + "v3.1": "EQAmkd4Pd_xgUW4b9MLrygf0SOfR2EUVa_iCtVWGnYB2hItG", + "v3.0": "EQA6ivhIOBQJvqO1SY3IvutmluM1d817gZDeEjiaVNBLePd3", + v2: "EQBvhJBSxkmpAEkIoQfqztCPGRQNaCObIVuoy_Rz7oTbWBYi", + v1: "EQAO4cYqithwdltzmrlal1L5JKLK5Xk76feAJq0VoBC6Fy8T", +}); + +export const ACTIVE_FACTORY = FACTORIES["v3.1"]; + +export const MEME_CODE_HASHES = Object.freeze({ + "v3.1": "f4ba4ebd2678dde0c750564bd22ee0876aa2f39a1e8d3aacb606ba0c39a71d9a", + "v3.0": "1e4f8fcaabefdbbb9b397372d9f021511c272eda2b492ca2d33b817bc3251afe", + v2: "7369f9b9f570e1bf78b0dc9d52cbab7ce21a4f000ee858ea6e81d43232c1a2d6", + v1: "722d37be518ee0d4b6714077727ed60724bf379e8f1479c220b853d1af3cf00d", +}); + +export const MEME_LIBRARY_HASHES = Object.freeze({ + "v3.1": "4b33b12bd3acfccfebf1e5e104ccb13c48f3e55625b07e1a33ebb29abab7bdfc", + "v3.0": "a63e4a802c8b982cd888a357b2c9ee5a722644c2d7e7cdaed350f24fc96eca3d", + v2: "c91ed56f09598f54a56fd0585ea4ccc2013cd99554e0c2ed8bebed92031fdd85", + v1: "e35b0b2f2c1bab780b30934db9758d3575d453077e71d14f1565b435d505d698", +}); + +export const WALLET_CODE_HASHES = Object.freeze({ + v3: "e2da88070dcf9d1b2ed4ab9079b8c473625bf320f38423ed86ee03ef5c326858", + v2: "84cee104b9fdeb128b154ef61bd5ae6a4552f0bc691718fdf532836077f43732", + v1: "21097ae6d15af88099db08eb49b71df933905c86a09c3823fd5f0e1a8b6acdae", +}); + +export const WALLET_LIBRARY_HASHES = Object.freeze({ + v3: "891e5228e4300ae3ff22583163bbd50e3e1e8cd2304c0d57164f5318f9da1266", + v2: "889a53d5daf34255a992bf4eadf96bdf408604aa9785e7cf4133bb982034b6d7", + v1: "eefce5ce3d1e555a5010a309348c965dfaf72ebdf3233b4f691fa3d37bdb5605", +}); + +// Legacy deployments stay readable. Financial writes remain v3.1-only until +// each legacy ABI has its own version-specific fixture and live read proof. +export const WRITABLE_MEME_VERSIONS = new Set(["v3.1"]); + +export const OPCODES = Object.freeze({ + DEPLOY_MEME: 0x6ff416dc, + DEPLOY_CUSTOMIZED_MEME: 0x632f5d1c, + INIT_MEME: 0x796f5a0c, + BUY: 0x94826557, + SELL: 0x646ad424, + SELL_TOKENS: 0xb7459e2c, + CLAIM_CREATOR_FEE: 0xad7269a8, + CLAIM_PARTNER_FEE: 0x7f4bcbf4, + BUY_EVENT: 0xa0aa6bc2, + SELL_EVENT: 0x3ab0fccc, +}); + +export const GAS_NANO = Object.freeze({ + BUY: 100_000_000n, + SELL: 120_000_000n, + DEPLOY: 500_000_000n, + CLAIM_CREATOR: 100_000_000n, + CLAIM_PARTNER: 300_000_000n, + ACTION_HEADROOM: 20_000_000n, +}); + +export const SETTLEMENT = Object.freeze({ + ATTEMPTS: 20, + INTERVAL_MS: 1_500, +}); + +export const PRESET_DEPLOYS = Object.freeze([ + { id: 3, raisingFundsTon: "1000", tradeFeeBps: 100, migrationSupplyPercent: 20, migrationMarketCapTon: "5000" }, + { id: 4, raisingFundsTon: "1000", tradeFeeBps: 300, migrationSupplyPercent: 20, migrationMarketCapTon: "5000" }, + { id: 5, raisingFundsTon: "1000", tradeFeeBps: 500, migrationSupplyPercent: 20, migrationMarketCapTon: "5000" }, + { id: 6, raisingFundsTon: "1500", tradeFeeBps: 100, migrationSupplyPercent: 25, migrationMarketCapTon: "6000" }, + { id: 7, raisingFundsTon: "1500", tradeFeeBps: 300, migrationSupplyPercent: 25, migrationMarketCapTon: "6000" }, + { id: 8, raisingFundsTon: "2500", tradeFeeBps: 100, migrationSupplyPercent: 25, migrationMarketCapTon: "10000" }, + { id: 9, raisingFundsTon: "2500", tradeFeeBps: 300, migrationSupplyPercent: 25, migrationMarketCapTon: "10000" }, +]); + +export const TOTAL_SUPPLY_PRESETS = Object.freeze([ + { id: 0, tokens: "100000000" }, + { id: 1, tokens: "1000000000" }, + { id: 2, tokens: "10000000000" }, +]); + +export const FEE_PRESETS = Object.freeze([ + { id: 0, feeBps: 10 }, + { id: 1, feeBps: 25 }, + { id: 2, feeBps: 50 }, + { id: 3, feeBps: 100 }, + { id: 4, feeBps: 200 }, + { id: 5, feeBps: 300 }, + { id: 6, feeBps: 500 }, + { id: 7, feeBps: 1000 }, +]); + +export const DOCS = Object.freeze({ + overview: "https://hub-beta.dedust.io/docs/uranus/overview", + factory: "https://hub-beta.dedust.io/docs/uranus/reference/meme-factory", + meme: "https://hub-beta.dedust.io/docs/uranus/reference/meme", + wallet: "https://hub-beta.dedust.io/docs/uranus/reference/meme-wallet", + common: "https://hub-beta.dedust.io/docs/uranus/reference/common", + errors: "https://hub-beta.dedust.io/docs/uranus/reference/errors", +}); diff --git a/plugins/uranus/errors.js b/plugins/uranus/errors.js new file mode 100644 index 0000000..46e034a --- /dev/null +++ b/plugins/uranus/errors.js @@ -0,0 +1,66 @@ +const EXIT_ERRORS = new Map([ + [20, ["CURVE_PARAMS_MALFORMED", "Invalid bonding-curve parameters"]], + [21, ["SLIPPAGE_EXCEEDED", "Quote moved beyond allowed slippage"]], + [22, ["PRESET_NOT_FOUND", "Meme preset does not exist"]], + [23, ["MESSAGE_VALUE_TOO_LOW", "Attached TON is insufficient"]], + [24, ["ALREADY_INITIALIZED", "Meme is already initialized"]], + [25, ["SENDER_UNAUTHORIZED", "Wallet is not authorized"]], + [26, ["INITIAL_BUY_LIMIT", "Initial buy exceeds the contract limit"]], + [27, ["BALANCE_EXCEEDED", "Requested amount exceeds the available balance"]], + [28, ["NOT_INITIALIZED", "Contract is not initialized"]], + [29, ["ALREADY_GRADUATED", "Curve trading is closed after graduation"]], + [30, ["CROSS_WORKCHAIN", "Cross-workchain operations are unsupported"]], + [31, ["INCORRECT_ADDRESS", "Address is invalid for this operation"]], + [34, ["FEE_PRESET_NOT_FOUND", "Fee preset does not exist"]], + [35, ["SUPPLY_PRESET_NOT_FOUND", "Supply preset does not exist"]], + [36, ["RAISE_OUT_OF_RANGE", "Raise target is outside 800โ€“1,000,000 TON"]], + [37, ["PARTNER_FEE_OUT_OF_RANGE", "Partner fee exceeds protocol limits"]], + [38, ["SELL_SUPPLY_OUT_OF_RANGE", "On-sell supply must be between 60% and 80%"]], + [39, ["POOL_PARTNER_FEE_OUT_OF_RANGE", "Pool partner fee exceeds protocol limits"]], +]); + +export class UranusError extends Error { + constructor(code, message, cause) { + super(message, cause === undefined ? undefined : { cause }); + this.name = "UranusError"; + this.code = code; + } +} + +export function fail(code, message) { + throw new UranusError(code, message); +} + +export function exitError(exitCode) { + const known = EXIT_ERRORS.get(Number(exitCode)); + if (!known) return new UranusError("CONTRACT_ERROR", `Uranus contract exited with code ${exitCode}`); + return new UranusError(known[0], known[1]); +} + +function detectExitCode(message) { + const match = String(message).match(/(?:exit(?:_code)?|code)\s*[:=]?\s*(-?\d+)/i); + return match ? Number(match[1]) : undefined; +} + +export function normalizeError(error) { + if (error instanceof UranusError) return error; + const message = String(error?.message ?? error); + const exitCode = detectExitCode(message); + if (exitCode !== undefined && EXIT_ERRORS.has(exitCode)) return exitError(exitCode); + const sdkCode = typeof error?.code === "string" ? error.code : undefined; + if (sdkCode === "WALLET_NOT_INITIALIZED") return new UranusError(sdkCode, "Teleton wallet is not initialized", error); + if (sdkCode === "INVALID_ADDRESS") return new UranusError(sdkCode, "Invalid TON address", error); + if (/429|rate.?limit/i.test(message)) return new UranusError("UPSTREAM_RATE_LIMITED", "TON indexer rate limit reached; retry shortly", error); + if (/abort|timeout/i.test(message)) return new UranusError("UPSTREAM_UNAVAILABLE", "TON upstream timed out", error); + return new UranusError(sdkCode ?? "OPERATION_FAILED", message.slice(0, 500), error); +} + +export function errorResult(error, log, label = "uranus") { + const normalized = normalizeError(error); + log?.error?.(`${label}: ${normalized.code}: ${normalized.message}`); + return { success: false, error: normalized.message.slice(0, 500), code: normalized.code }; +} + +export const EXIT_ERROR_CODES = Object.freeze( + Object.fromEntries([...EXIT_ERRORS].map(([exit, [code, message]]) => [exit, { code, message }])) +); diff --git a/plugins/uranus/history.js b/plugins/uranus/history.js new file mode 100644 index 0000000..7879574 --- /dev/null +++ b/plugins/uranus/history.js @@ -0,0 +1,143 @@ +import { Address, Cell } from "@ton/core"; +import { FACTORIES, OPCODES } from "./constants.js"; +import { decodeDeployCustomizedMeme, decodeDeployMeme, decodeInitMeme, decodeTradeEvent, normalizeAddress } from "./abi.js"; +import { formatUnits } from "./amounts.js"; + +function transactions(payload) { + return Array.isArray(payload?.transactions) ? payload.transactions : []; +} + +function bodyCell(message) { + const body = message?.message_content?.body ?? message?.body; + if (typeof body !== "string" || !body) return null; + try { + return Cell.fromBase64(body); + } catch { + return null; + } +} + +function opcode(message) { + const value = message?.opcode; + if (typeof value === "number") return value >>> 0; + if (typeof value === "string" && /^0x[0-9a-f]+$/i.test(value)) return Number.parseInt(value.slice(2), 16) >>> 0; + const body = bodyCell(message); + if (!body || body.bits.length < 32) return null; + return body.beginParse().preloadUint(32) >>> 0; +} + +function txHashHex(value) { + if (!value) return null; + if (/^[0-9a-f]{64}$/i.test(value)) return value.toLowerCase(); + try { + const hex = Buffer.from(value, "base64").toString("hex"); + return hex.length === 64 ? hex : value; + } catch { + return value; + } +} + +function msgAddress(value) { + if (!value) return null; + try { + return Address.parse(value).toString(); + } catch { + return null; + } +} + +function decodeDeploy(message) { + const body = bodyCell(message); + if (!body) return null; + if (opcode(message) === OPCODES.DEPLOY_MEME) return decodeDeployMeme(body); + if (opcode(message) === OPCODES.DEPLOY_CUSTOMIZED_MEME) return decodeDeployCustomizedMeme(body); + return null; +} + +export function createHistory(sdk, http) { + async function recentLaunches({ limit = 10, factory_version = "v3.1" } = {}) { + const versions = factory_version === "all" ? ["v3.1", "v3.0", "v2"] : [factory_version]; + const launches = []; + const seen = new Set(); + for (const version of versions) { + const factory = FACTORIES[version]; + if (!factory) continue; + const payload = await http.transactions(factory, Math.min(100, limit * 4)); + for (const tx of transactions(payload)) { + try { + const deploy = decodeDeploy(tx.in_msg); + if (!deploy) continue; + const initMessage = (tx.out_msgs ?? []).find((message) => opcode(message) === OPCODES.INIT_MEME); + const initBody = bodyCell(initMessage); + if (!initMessage?.destination || !initBody) continue; + const init = decodeInitMeme(initBody); + if (init.queryId !== deploy.queryId) continue; + const memeAddress = normalizeAddress(initMessage.destination); + const raw = Address.parse(memeAddress).toRawString(); + if (seen.has(raw)) continue; + seen.add(raw); + const hash = txHashHex(tx.hash); + launches.push({ + meme_address: memeAddress, + factory_version: version, + factory_address: normalizeAddress(factory), + kind: deploy.kind, + query_id: deploy.queryId.toString(), + creator_address: msgAddress(tx.in_msg?.source), + initial_buy_nano: deploy.initialBuy.toString(), + initial_buy_ton: formatUnits(deploy.initialBuy), + metadata_uri: deploy.metadataUri, + preset_id: deploy.presetId ?? null, + transaction_hash: hash, + explorer: hash ? `https://tonviewer.com/transaction/${hash}` : null, + timestamp: Number(tx.now ?? tx.in_msg?.created_at ?? 0), + }); + } catch (error) { + sdk.log?.debug?.(`uranus: skipped malformed factory transaction: ${String(error?.message ?? error)}`); + } + } + } + launches.sort((a, b) => b.timestamp - a.timestamp); + return launches.slice(0, limit); + } + + async function recentTrades({ meme_address, limit = 10 }) { + const meme = normalizeAddress(meme_address); + const payload = await http.transactions(meme, Math.min(100, limit * 4)); + const records = []; + for (const tx of transactions(payload)) { + for (const message of tx.out_msgs ?? []) { + try { + const body = bodyCell(message); + if (!body) continue; + const event = decodeTradeEvent(body); + if (!event) continue; + const hash = txHashHex(tx.hash); + records.push({ + kind: event.kind, + trader_address: event.traderAddress.toString(), + amount_in_raw: event.amountIn.toString(), + amount_in: formatUnits(event.amountIn), + amount_out_raw: event.amountOut.toString(), + amount_out: formatUnits(event.amountOut), + fees: Object.fromEntries(Object.entries(event.fees).map(([key, value]) => [key, { raw: value.toString(), ton: formatUnits(value) }])), + current_supply_raw: event.currentSupply.toString(), + raised_funds_nano: event.raisedFunds.toString(), + graduated: event.isGraduated ?? null, + transaction_hash: hash, + explorer: hash ? `https://tonviewer.com/transaction/${hash}` : null, + timestamp: Number(tx.now ?? message.created_at ?? 0), + }); + } catch (error) { + sdk.log?.debug?.(`uranus: skipped malformed trade event: ${String(error?.message ?? error)}`); + } + } + } + records.sort((a, b) => a.timestamp - b.timestamp); + return records.slice(-limit); + } + + return { recentLaunches, recentTrades }; +} + +export const historyInternals = { bodyCell, opcode, txHashHex }; diff --git a/plugins/uranus/http.js b/plugins/uranus/http.js new file mode 100644 index 0000000..1e7eb2e --- /dev/null +++ b/plugins/uranus/http.js @@ -0,0 +1,234 @@ +import { lookup } from "node:dns/promises"; +import { request as httpRequest } from "node:http"; +import { request as httpsRequest } from "node:https"; +import { BlockList, isIP } from "node:net"; +import { Readable } from "node:stream"; +import { UranusError } from "./errors.js"; + +const TONCENTER_ORIGIN = "https://toncenter.com"; +const IPFS_ORIGIN = "https://ipfs.io"; +const TIMEOUT_MS = 10_000; +const MAX_BYTES = 2 * 1024 * 1024; +const MAX_REDIRECTS = 3; + +const privateIpv6 = new BlockList(); +privateIpv6.addAddress("::", "ipv6"); +privateIpv6.addAddress("::1", "ipv6"); +privateIpv6.addSubnet("fc00::", 7, "ipv6"); +privateIpv6.addSubnet("fe80::", 10, "ipv6"); +privateIpv6.addSubnet("ff00::", 8, "ipv6"); +privateIpv6.addSubnet("::ffff:0:0", 96, "ipv6"); + +function isPrivateIpv4(ip) { + const parts = ip.split(".").map(Number); + if (parts.length !== 4 || parts.some((part) => !Number.isInteger(part) || part < 0 || part > 255)) return true; + return parts[0] === 10 + || parts[0] === 127 + || parts[0] === 0 + || (parts[0] === 169 && parts[1] === 254) + || (parts[0] === 172 && parts[1] >= 16 && parts[1] <= 31) + || (parts[0] === 192 && parts[1] === 168) + || (parts[0] === 100 && parts[1] >= 64 && parts[1] <= 127) + || parts[0] >= 224; +} + +function isPrivateIp(ip) { + if (isIP(ip) === 4) return isPrivateIpv4(ip); + if (isIP(ip) !== 6) return true; + return privateIpv6.check(ip, "ipv6"); +} + +export async function resolvePublicMetadataTarget(input, lookupImpl = lookup) { + let url; + try { + url = new URL(input); + } catch { + throw new UranusError("INVALID_METADATA_URI", "Metadata URI is invalid"); + } + if (!['http:', 'https:'].includes(url.protocol)) throw new UranusError("INVALID_METADATA_URI", "Metadata URI must use IPFS, HTTP, or HTTPS"); + const host = url.hostname.toLowerCase().replace(/^\[|\]$/g, ""); + if (!host || host === "localhost" || host.endsWith(".localhost") || host.endsWith(".local")) { + throw new UranusError("METADATA_SSRF_BLOCKED", "Metadata host is local or private"); + } + if (isIP(host)) { + if (isPrivateIp(host)) throw new UranusError("METADATA_SSRF_BLOCKED", "Metadata host is local or private"); + return { url, address: host, family: isIP(host) }; + } else { + let records; + try { + records = await lookupImpl(host, { all: true, verbatim: true }); + } catch (error) { + throw new UranusError("UPSTREAM_UNAVAILABLE", "Metadata host could not be resolved", error); + } + if (!records.length || records.some(({ address }) => isPrivateIp(address))) { + throw new UranusError("METADATA_SSRF_BLOCKED", "Metadata host resolves to a local or private address"); + } + return { url, address: records[0].address, family: records[0].family }; + } +} + +export async function assertPublicMetadataUrl(input) { + return (await resolvePublicMetadataTarget(input)).url; +} + +export function normalizeMetadataUri(uri) { + if (typeof uri !== "string" || uri.length < 1 || uri.length > 1024) { + throw new UranusError("INVALID_METADATA_URI", "metadata_uri must be 1 to 1024 characters"); + } + if (uri.startsWith("ipfs://")) { + const path = uri.slice(7).replace(/^ipfs\//, ""); + if (!path || /[?#]/.test(path.split("/")[0])) throw new UranusError("INVALID_METADATA_URI", "Invalid IPFS metadata URI"); + return `${IPFS_ORIGIN}/ipfs/${path}`; + } + let url; + try { + url = new URL(uri); + } catch (error) { + throw new UranusError("INVALID_METADATA_URI", "Metadata URI is invalid", error); + } + if (!["https:", "http:"].includes(url.protocol)) throw new UranusError("INVALID_METADATA_URI", "Metadata URI must use IPFS, HTTP, or HTTPS"); + return url.toString(); +} + +async function readBounded(response) { + const declared = Number(response.headers.get("content-length") ?? 0); + if (declared > MAX_BYTES) throw new UranusError("RESPONSE_TOO_LARGE", "Upstream response exceeds 2 MiB"); + if (!response.body) return ""; + const reader = response.body.getReader(); + const chunks = []; + let size = 0; + try { + while (true) { + const { done, value } = await reader.read(); + if (done) break; + size += value.byteLength; + if (size > MAX_BYTES) throw new UranusError("RESPONSE_TOO_LARGE", "Upstream response exceeds 2 MiB"); + chunks.push(value); + } + } finally { + reader.releaseLock(); + } + const bytes = new Uint8Array(size); + let offset = 0; + for (const chunk of chunks) { + bytes.set(chunk, offset); + offset += chunk.byteLength; + } + return new TextDecoder().decode(bytes); +} + +function fetchPinned(target, options = {}) { + const { url, address, family } = target; + const request = url.protocol === "https:" ? httpsRequest : httpRequest; + const headers = { accept: "application/json", ...options.headers, host: url.host }; + const signal = AbortSignal.timeout(TIMEOUT_MS); + + return new Promise((resolve, reject) => { + const req = request({ + protocol: url.protocol, + hostname: address, + family, + port: url.port || undefined, + path: `${url.pathname}${url.search}`, + method: options.method ?? "GET", + headers, + signal, + ...(url.protocol === "https:" && !isIP(url.hostname.replace(/^\[|\]$/g, "")) + ? { servername: url.hostname } + : {}), + }, (response) => { + resolve(new Response(Readable.toWeb(response), { + status: response.statusCode, + statusText: response.statusMessage, + headers: response.headers, + })); + }); + req.once("error", reject); + if (options.body !== undefined) req.write(options.body); + req.end(); + }); +} + +async function fetchJsonOnce(url, options = {}, resolveTarget = null, pinnedFetch = fetchPinned) { + let current = new URL(url); + for (let redirects = 0; redirects <= MAX_REDIRECTS; redirects += 1) { + const response = resolveTarget + ? await pinnedFetch(await resolveTarget(current.toString()), options) + : await fetch(current, { + ...options, + redirect: "manual", + signal: AbortSignal.timeout(TIMEOUT_MS), + headers: { accept: "application/json", ...options.headers }, + }); + if ([301, 302, 303, 307, 308].includes(response.status)) { + if (redirects === MAX_REDIRECTS) throw new UranusError("UPSTREAM_UNAVAILABLE", "Too many upstream redirects"); + const location = response.headers.get("location"); + if (!location) throw new UranusError("INVALID_RESPONSE", "Upstream redirect is missing a location"); + await response.body?.cancel(); + current = new URL(location, current); + continue; + } + const body = await readBounded(response); + if (response.status === 429) throw new UranusError("UPSTREAM_RATE_LIMITED", "TON indexer rate limit reached; retry shortly"); + if (!response.ok) throw new UranusError("UPSTREAM_UNAVAILABLE", `Upstream request failed with HTTP ${response.status}`); + try { + return JSON.parse(body); + } catch (error) { + throw new UranusError("INVALID_RESPONSE", "Upstream returned malformed JSON", error); + } + } + throw new UranusError("UPSTREAM_UNAVAILABLE", "Upstream redirect limit reached"); +} + +async function fetchJson(url, options = {}, resolveTarget = null, pinnedFetch = fetchPinned) { + let lastError; + for (let attempt = 0; attempt < 2; attempt += 1) { + try { + return await fetchJsonOnce(url, options, resolveTarget, pinnedFetch); + } catch (error) { + lastError = error; + if (error instanceof UranusError && error.code !== "UPSTREAM_UNAVAILABLE") throw error; + } + } + if (lastError instanceof UranusError) throw lastError; + throw new UranusError("UPSTREAM_UNAVAILABLE", "Upstream request failed", lastError); +} + +function cached(sdk, key, ttl, loader) { + const hit = sdk.storage?.get?.(key); + if (hit !== undefined) return Promise.resolve(hit); + return loader().then((value) => { + sdk.storage?.set?.(key, value, { ttl }); + return value; + }); +} + +function toncenterHeaders(sdk) { + const key = sdk.secrets?.get?.("toncenter_api_key"); + return key ? { "X-API-Key": key } : {}; +} + +export function createHttp(sdk, dependencies = {}) { + const resolveMetadataTarget = dependencies.resolveMetadataTarget ?? resolvePublicMetadataTarget; + const fetchPinnedMetadata = dependencies.fetchPinnedMetadata ?? fetchPinned; + const toncenter = (path, params, ttl = 10_000) => { + const url = new URL(`/api/v3/${path}`, TONCENTER_ORIGIN); + for (const [key, value] of Object.entries(params)) if (value !== undefined) url.searchParams.set(key, String(value)); + return cached(sdk, `uranus:http:${url}`, ttl, () => fetchJson(url, { headers: toncenterHeaders(sdk) })); + }; + + return { + accountState(address) { + return toncenter("accountStates", { address, include_boc: "false" }, 600_000); + }, + transactions(address, limit = 50) { + return toncenter("transactions", { account: address, limit: Math.min(100, Math.max(1, limit)), sort: "desc" }, 10_000); + }, + async metadata(uri) { + const normalized = normalizeMetadataUri(uri); + return cached(sdk, `uranus:metadata:${normalized}`, 600_000, () => fetchJson(normalized, {}, resolveMetadataTarget, fetchPinnedMetadata)); + }, + }; +} + +export const HTTP_LIMITS = Object.freeze({ timeoutMs: TIMEOUT_MS, maxBytes: MAX_BYTES, maxRedirects: MAX_REDIRECTS }); diff --git a/plugins/uranus/index.js b/plugins/uranus/index.js new file mode 100644 index 0000000..647ddf3 --- /dev/null +++ b/plugins/uranus/index.js @@ -0,0 +1,162 @@ +import { ACTIVE_FACTORY, DOCS, FACTORIES, FEE_PRESETS, GAS_NANO, MEME_CODE_HASHES, MEME_LIBRARY_HASHES, NETWORK, PRESET_DEPLOYS, TOTAL_SUPPLY_PRESETS, WALLET_CODE_HASHES, WALLET_LIBRARY_HASHES } from "./constants.js"; +import { createHttp } from "./http.js"; +import { createState } from "./state.js"; +import { createQuote } from "./quote.js"; +import { createHistory } from "./history.js"; +import { createActions } from "./actions.js"; +import { errorResult, UranusError } from "./errors.js"; + +export const manifest = { + name: "uranus", + version: "1.0.0", + sdkVersion: "^2.1.0", + description: "Uranus Meme launchpad on TON โ€” inspect, quote, trade, deploy and claim fees.", + defaultConfig: { + maxActionTon: "50", + defaultSlippageBps: 500, + partnerId: null, + partnerFeeBps: 0, + referrerId: null, + referrerFeeBps: 0, + allowThirdPartyPartnerClaim: false, + }, + secrets: { + toncenter_api_key: { required: false, description: "Optional Toncenter v3 key for launch/trade history rate limits" }, + }, +}; + +const object = (properties, required = []) => ({ type: "object", properties, required, additionalProperties: false }); +const address = (description) => ({ type: "string", minLength: 48, maxLength: 80, description }); +const decimal = (description) => ({ type: "string", pattern: "^(0|[1-9][0-9]*)(\\.[0-9]+)?$", description }); +const slippage = { type: "integer", minimum: 1, maximum: 5000, default: 500, description: "Slippage tolerance in basis points" }; +const memeAddress = address("Uranus Meme master contract address"); + +function publicInfo(info) { + const { _raw, ...result } = info; + return result; +} + +function publicQuote(quote) { + const { _raw, _info, ...result } = quote; + return result; +} + +function safe(sdk, name, execute) { + return async (params = {}, context = {}) => { + try { + return { success: true, data: await execute(params, context) }; + } catch (error) { + return errorResult(error, sdk.log, name); + } + }; +} + +function isAdminContext(context) { + return Boolean(context?.config?.telegram?.admin_ids?.includes(context.senderId)); +} + +export const tools = (sdk) => { + const config = { ...manifest.defaultConfig, ...(sdk.pluginConfig ?? {}) }; + const http = createHttp(sdk); + const state = createState(sdk, http); + const quote = createQuote(sdk, state, config); + const history = createHistory(sdk, http); + const actions = createActions(sdk, { config, state, quote, history }); + return [ + { + name: "uranus_protocol_info", + description: "Return verified Uranus deployments, code hashes, presets, validation ranges, gas budgets and documentation links.", + scope: "always", category: "data-bearing", parameters: object({}), + execute: safe(sdk, "uranus_protocol_info", async () => ({ + network: NETWORK, + active_factory: ACTIVE_FACTORY, + factories: { ...FACTORIES }, + meme_code_hashes: { ...MEME_CODE_HASHES }, meme_library_hashes: { ...MEME_LIBRARY_HASHES }, + wallet_code_hashes: { ...WALLET_CODE_HASHES }, wallet_library_hashes: { ...WALLET_LIBRARY_HASHES }, + preset_deploys: PRESET_DEPLOYS.map((value) => ({ ...value })), + total_supply_presets: TOTAL_SUPPLY_PRESETS.map((value) => ({ ...value })), + fee_presets: FEE_PRESETS.map((value) => ({ ...value })), + custom_ranges: { raising_funds_ton: ["800", "1000000"], on_sell_supply_percent: [60, 80], partner_fee_bps: [0, 6000], pool_partner_fee_bps: [0, 4000] }, + observed_safe_budget_ton: Object.fromEntries(Object.entries(GAS_NANO).filter(([key]) => key !== "ACTION_HEADROOM").map(([key, value]) => [key.toLowerCase(), (Number(value) / 1e9).toString()])), + documentation: { ...DOCS }, + warning: "Uranus Developer Hub reference pages are marked under construction; actions also verify current on-chain identity and state.", + })), + }, + { + name: "uranus_recent_launches", description: "Discover and decode recent Uranus factory launches from Toncenter v3.", + scope: "always", category: "data-bearing", + parameters: object({ limit: { type: "integer", minimum: 1, maximum: 50, default: 10 }, factory_version: { type: "string", enum: ["v3.1", "v3.0", "v2", "all"], default: "v3.1" } }), + execute: safe(sdk, "uranus_recent_launches", (params) => history.recentLaunches(params)), + }, + { + name: "uranus_meme_info", description: "Inspect and verify Uranus Meme identity, jetton metadata, curve state, lifecycle and accrued fees.", + scope: "always", category: "data-bearing", parameters: object({ meme_address: memeAddress }, ["meme_address"]), + execute: safe(sdk, "uranus_meme_info", async ({ meme_address }) => publicInfo(await state.memeInfo(meme_address))), + }, + { + name: "uranus_wallet_info", description: "Resolve an owner's Uranus Meme Wallet and verify its owner, master and balance.", + scope: "always", category: "data-bearing", parameters: object({ meme_address: memeAddress, owner_address: address("Owner address; omit only in an admin context to inspect the Teleton wallet") }, ["meme_address"]), + execute: safe(sdk, "uranus_wallet_info", async (params, context) => { + if (!params.owner_address && !isAdminContext(context)) throw new UranusError("SENDER_UNAUTHORIZED", "owner_address is required outside an admin context"); + const { _rawBalance, ...result } = await state.walletInfo(params.meme_address, params.owner_address); + return result; + }), + }, + { + name: "uranus_portfolio", description: "List only code-verified Uranus holdings in the configured Teleton wallet.", + scope: "admin-only", category: "data-bearing", parameters: object({ include_zero: { type: "boolean", default: false }, limit: { type: "integer", minimum: 1, maximum: 100, default: 50 } }), + execute: safe(sdk, "uranus_portfolio", (params) => state.portfolio(params)), + }, + { + name: "uranus_quote", description: "Quote a Uranus curve trade from fresh on-chain state, or DeDust after migration, with internal slippage protection.", + scope: "always", category: "data-bearing", parameters: object({ meme_address: memeAddress, direction: { type: "string", enum: ["buy", "sell"] }, amount: decimal("TON for buy, token units for sell"), slippage_bps: slippage }, ["meme_address", "direction", "amount"]), + execute: safe(sdk, "uranus_quote", async (params) => publicQuote(await quote(params))), + }, + { + name: "uranus_recent_trades", description: "Decode recent Uranus BuyEvent and SellEvent records with their complete fee breakdown.", + scope: "always", category: "data-bearing", parameters: object({ meme_address: memeAddress, limit: { type: "integer", minimum: 1, maximum: 50, default: 10 } }, ["meme_address"]), + execute: safe(sdk, "uranus_recent_trades", (params) => history.recentTrades(params)), + }, + { + name: "uranus_buy", description: "Buy a verified Uranus Meme on its curve or DeDust after migration; quote and destination are derived internally.", + scope: "admin-only", category: "action", requiresApproval: true, + parameters: object({ meme_address: memeAddress, amount_ton: decimal("TON amount to spend"), slippage_bps: slippage }, ["meme_address", "amount_ton"]), + execute: safe(sdk, "uranus_buy", actions.buy), + }, + { + name: "uranus_sell", description: "Sell a verified Uranus Meme on its curve or DeDust after migration; minimum output is derived internally.", + scope: "admin-only", category: "action", requiresApproval: true, + parameters: object({ meme_address: memeAddress, amount_tokens: decimal("Human-readable token amount to sell"), slippage_bps: slippage }, ["meme_address", "amount_tokens"]), + execute: safe(sdk, "uranus_sell", actions.sell), + }, + { + name: "uranus_launch_preset", description: "Launch a Uranus v3.1 Meme with preset economics and an already-published metadata URI.", + scope: "admin-only", category: "action", requiresApproval: true, + parameters: object({ preset_id: { type: "integer", minimum: 3, maximum: 9 }, metadata_uri: { type: "string", minLength: 1, maxLength: 1024 }, initial_buy_ton: decimal("Optional initial TON buy; defaults to zero") }, ["preset_id", "metadata_uri"]), + execute: safe(sdk, "uranus_launch_preset", actions.launchPreset), + }, + { + name: "uranus_launch_custom", description: "Launch a customized Uranus v3.1 Meme with validated factory economics and fixed payout addresses.", + scope: "admin-only", category: "action", requiresApproval: true, + parameters: object({ + total_supply_preset_id: { type: "integer", minimum: 0, maximum: 2 }, base_fee_preset_id: { type: "integer", minimum: 0, maximum: 7 }, + raising_funds_ton: decimal("Raise target from 800 to 1,000,000 TON"), on_sell_supply_percent: { type: "integer", minimum: 60, maximum: 80 }, + partner_address: address("Partner payout address; defaults to the Teleton wallet"), partner_fee_bps: { type: "integer", minimum: 0, maximum: 6000 }, + pool_partner_fee_bps: { type: "integer", minimum: 0, maximum: 4000 }, pool_base_fee_preset_id: { type: "integer", minimum: 0, maximum: 7 }, + pool_liquidity_owner_address: address("Optional post-migration liquidity owner"), metadata_uri: { type: "string", minLength: 1, maxLength: 1024 }, + initial_buy_ton: decimal("Optional initial TON buy; defaults to zero"), + }, ["total_supply_preset_id", "base_fee_preset_id", "raising_funds_ton", "on_sell_supply_percent", "partner_fee_bps", "pool_partner_fee_bps", "pool_base_fee_preset_id", "metadata_uri"]), + execute: safe(sdk, "uranus_launch_custom", actions.launchCustom), + }, + { + name: "uranus_claim_creator_fee", description: "Claim accrued creator fees when the Teleton wallet matches the verified on-chain creator.", + scope: "admin-only", category: "action", requiresApproval: true, parameters: object({ meme_address: memeAddress }, ["meme_address"]), + execute: safe(sdk, "uranus_claim_creator_fee", actions.claimCreatorFee), + }, + { + name: "uranus_claim_partner_fee", description: "Claim accrued partner fees to the immutable on-chain partner destination when sender policy permits.", + scope: "admin-only", category: "action", requiresApproval: true, parameters: object({ meme_address: memeAddress }, ["meme_address"]), + execute: safe(sdk, "uranus_claim_partner_fee", actions.claimPartnerFee), + }, + ]; +}; diff --git a/plugins/uranus/manifest.json b/plugins/uranus/manifest.json new file mode 100644 index 0000000..4f4f13d --- /dev/null +++ b/plugins/uranus/manifest.json @@ -0,0 +1,33 @@ +{ + "id": "uranus", + "name": "Uranus Meme Launchpad", + "version": "1.0.0", + "description": "Inspect, quote, trade and launch Uranus Meme tokens on TON through the protected Teleton SDK transaction broker.", + "author": { "name": "teleton", "url": "https://github.com/TONresistor" }, + "license": "MIT", + "entry": "index.js", + "teleton": ">=0.9.0", + "sdkVersion": "^2.1.0", + "secrets": { + "toncenter_api_key": { "required": false, "description": "Optional Toncenter v3 key for launch/trade history rate limits" } + }, + "tools": [ + { "name": "uranus_protocol_info", "description": "Return Uranus deployments, hashes, presets and operational limits" }, + { "name": "uranus_recent_launches", "description": "Discover decoded launches from known Uranus factories" }, + { "name": "uranus_meme_info", "description": "Inspect verified Uranus Meme, curve, partner and metadata state" }, + { "name": "uranus_wallet_info", "description": "Resolve and verify an Uranus Meme Wallet and balance" }, + { "name": "uranus_portfolio", "description": "List code-verified Uranus holdings in the agent wallet" }, + { "name": "uranus_quote", "description": "Quote an Uranus curve or migrated DeDust trade" }, + { "name": "uranus_recent_trades", "description": "Decode recent Uranus buy and sell events" }, + { "name": "uranus_buy", "description": "Buy a verified Uranus Meme through its current route" }, + { "name": "uranus_sell", "description": "Sell a verified Uranus Meme through its current route" }, + { "name": "uranus_launch_preset", "description": "Launch an Uranus v3.1 Meme using preset economics" }, + { "name": "uranus_launch_custom", "description": "Launch an Uranus v3.1 Meme using custom economics" }, + { "name": "uranus_claim_creator_fee", "description": "Claim creator fees to the verified creator wallet" }, + { "name": "uranus_claim_partner_fee", "description": "Claim partner fees to the stored partner wallet" } + ], + "permissions": [], + "tags": ["ton", "defi", "meme", "launchpad", "bonding-curve", "trade"], + "repository": "https://github.com/TONresistor/teleton-plugins", + "funding": null +} diff --git a/plugins/uranus/package-lock.json b/plugins/uranus/package-lock.json new file mode 100644 index 0000000..0251104 --- /dev/null +++ b/plugins/uranus/package-lock.json @@ -0,0 +1,63 @@ +{ + "name": "@teleton/plugin-uranus", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@teleton/plugin-uranus", + "version": "1.0.0", + "dependencies": { + "@ton/core": "^0.63.1" + } + }, + "node_modules/@ton/core": { + "version": "0.63.1", + "resolved": "https://registry.npmjs.org/@ton/core/-/core-0.63.1.tgz", + "integrity": "sha512-hDWMjlKzc18W2E4OeV3hUP8ohRJNHPD4Wd1+AQJj8zshZyCRT0usrvnExgbNUTo/vntDqCGMzgYWbXxyaA+L4g==", + "license": "MIT", + "peerDependencies": { + "@ton/crypto": ">=3.2.0" + } + }, + "node_modules/@ton/crypto": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/@ton/crypto/-/crypto-3.3.0.tgz", + "integrity": "sha512-/A6CYGgA/H36OZ9BbTaGerKtzWp50rg67ZCH2oIjV1NcrBaCK9Z343M+CxedvM7Haf3f/Ee9EhxyeTp0GKMUpA==", + "license": "MIT", + "peer": true, + "dependencies": { + "@ton/crypto-primitives": "2.1.0", + "jssha": "3.2.0", + "tweetnacl": "1.0.3" + } + }, + "node_modules/@ton/crypto-primitives": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@ton/crypto-primitives/-/crypto-primitives-2.1.0.tgz", + "integrity": "sha512-PQesoyPgqyI6vzYtCXw4/ZzevePc4VGcJtFwf08v10OevVJHVfW238KBdpj1kEDQkxWLeuNHEpTECNFKnP6tow==", + "license": "MIT", + "peer": true, + "dependencies": { + "jssha": "3.2.0" + } + }, + "node_modules/jssha": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/jssha/-/jssha-3.2.0.tgz", + "integrity": "sha512-QuruyBENDWdN4tZwJbQq7/eAK85FqrI4oDbXjy5IBhYD+2pTJyBUWZe8ctWaCkrV0gy6AaelgOZZBMeswEa/6Q==", + "license": "BSD-3-Clause", + "peer": true, + "engines": { + "node": "*" + } + }, + "node_modules/tweetnacl": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-1.0.3.tgz", + "integrity": "sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw==", + "license": "Unlicense", + "peer": true + } + } +} diff --git a/plugins/swapcoffee/package.json b/plugins/uranus/package.json similarity index 52% rename from plugins/swapcoffee/package.json rename to plugins/uranus/package.json index 85bd919..a84a4ce 100644 --- a/plugins/swapcoffee/package.json +++ b/plugins/uranus/package.json @@ -1,9 +1,9 @@ { - "name": "teleton-plugin-swapcoffee", - "type": "module", + "name": "@teleton/plugin-uranus", "version": "1.0.0", "private": true, + "type": "module", "dependencies": { - "@orbs-network/ton-access": "^2.3.3" + "@ton/core": "^0.63.1" } } diff --git a/plugins/uranus/quote.js b/plugins/uranus/quote.js new file mode 100644 index 0000000..751aa5c --- /dev/null +++ b/plugins/uranus/quote.js @@ -0,0 +1,106 @@ +import { formatUnits, parseUnits } from "./amounts.js"; +import { UranusError } from "./errors.js"; + +const BPS = 10_000n; + +export function supplyAtRaise(alpha, beta, raise) { + if (alpha <= 0n || beta <= 0n || raise < 0n) throw new UranusError("CURVE_PARAMS_MALFORMED", "Invalid bonding-curve parameters"); + return (alpha * raise) / (beta + raise); +} + +export function raiseAtSupply(alpha, beta, supply) { + if (alpha <= 0n || beta <= 0n || supply < 0n || supply >= alpha) throw new UranusError("CURVE_PARAMS_MALFORMED", "Supply is outside the bonding curve"); + return (beta * supply) / (alpha - supply); +} + +function assertTradable(state) { + if (!state.initialized) throw new UranusError("NOT_INITIALIZED", "Contract is not initialized"); + if (state.migrated) throw new UranusError("ALREADY_GRADUATED", "Migrated tokens must use DeDust"); + if (state.isGraduated) throw new UranusError("MIGRATION_PENDING", "Token graduated but its DeDust migration is not complete"); + if (state.currentSupply >= state.alpha) throw new UranusError("CURVE_PARAMS_MALFORMED", "Curve supply reached its asymptote"); +} + +export function estimateBuyOut(state, tonIn) { + assertTradable(state); + if (tonIn <= 0n) throw new UranusError("INVALID_AMOUNT", "Buy amount must be positive"); + const output = supplyAtRaise(state.alpha, state.beta, state.raisedFunds + tonIn) - state.currentSupply; + return output > 0n ? output : 0n; +} + +export function estimateSellOut(state, tokensIn) { + assertTradable(state); + if (tokensIn <= 0n) throw new UranusError("INVALID_AMOUNT", "Sell amount must be positive"); + if (tokensIn > state.currentSupply) throw new UranusError("BALANCE_EXCEEDED", "Sell amount exceeds current curve supply"); + const gross = state.raisedFunds - raiseAtSupply(state.alpha, state.beta, state.currentSupply - tokensIn); + return gross > 0n ? (gross * BPS) / (BPS + BigInt(state.tradeFeeBps)) : 0n; +} + +export function applySlippage(output, slippageBps) { + if (!Number.isInteger(slippageBps) || slippageBps < 1 || slippageBps > 5000) { + throw new UranusError("INVALID_SLIPPAGE", "slippage_bps must be between 1 and 5000"); + } + return (output * (BPS - BigInt(slippageBps))) / BPS; +} + +export function quoteCurve(state, direction, amount, slippageBps) { + if (direction !== "buy" && direction !== "sell") throw new UranusError("INVALID_DIRECTION", "direction must be buy or sell"); + const expected = direction === "buy" ? estimateBuyOut(state, amount) : estimateSellOut(state, amount); + const minimum = applySlippage(expected, slippageBps); + if (expected <= 0n || minimum <= 0n) throw new UranusError("SLIPPAGE_EXCEEDED", "Quote output is too small for safe execution"); + const fee = direction === "sell" ? (expected * BigInt(state.tradeFeeBps)) / BPS : 0n; + return { expected, minimum, fee }; +} + +export function createQuote(sdk, stateReader, config) { + return async function quoteMeme({ meme_address, direction, amount, slippage_bps }) { + const info = await stateReader.memeInfo(meme_address); + if (!info.verified) throw new UranusError("UNVERIFIED_CONTRACT", "Meme contract code hash is not a supported Uranus deployment"); + const slippageBps = slippage_bps ?? config.defaultSlippageBps; + if (info.migrated) { + const dexAmount = Number(amount); + if (!Number.isFinite(dexAmount) || dexAmount <= 0) throw new UranusError("INVALID_AMOUNT", "amount must be a positive decimal string"); + const dex = await sdk.ton.dex.quoteDeDust({ + fromAsset: direction === "buy" ? "ton" : info.address, + toAsset: direction === "buy" ? info.address : "ton", + amount: dexAmount, + slippage: slippageBps / 10_000, + }); + if (!dex) throw new UranusError("UPSTREAM_UNAVAILABLE", "No ready DeDust pool was found for this migrated Meme"); + return { + route: "dedust", + direction, + amount_in: amount, + expected_output: dex.expectedOutput, + minimum_output: dex.minOutput, + estimated_fee: dex.fee, + price_impact: dex.priceImpact ?? null, + state_timestamp: info.state_timestamp, + }; + } + const raw = info._raw.meme; + if (!raw) throw new UranusError("UNSUPPORTED_VERSION", "This Uranus contract does not expose the required curve state"); + const amountRaw = parseUnits(amount, 9, "amount"); + const quote = quoteCurve(raw, direction, amountRaw, slippageBps); + return { + route: "uranus_curve", + direction, + amount_in: amount, + amount_in_raw: amountRaw.toString(), + expected_output: formatUnits(quote.expected, 9), + expected_output_raw: quote.expected.toString(), + minimum_output: formatUnits(quote.minimum, 9), + minimum_output_raw: quote.minimum.toString(), + estimated_fee: formatUnits(quote.fee, 9), + estimated_fee_raw: quote.fee.toString(), + price_impact: null, + slippage_bps: slippageBps, + state_timestamp: info.state_timestamp, + state: { + alpha: raw.alpha.toString(), beta: raw.beta.toString(), raised_funds: raw.raisedFunds.toString(), + current_supply: raw.currentSupply.toString(), trade_fee_bps: raw.tradeFeeBps, + }, + _info: info, + _raw: quote, + }; + }; +} diff --git a/plugins/uranus/state.js b/plugins/uranus/state.js new file mode 100644 index 0000000..cc9747d --- /dev/null +++ b/plugins/uranus/state.js @@ -0,0 +1,352 @@ +import { createHash } from "node:crypto"; +import { Address, beginCell, Dictionary } from "@ton/core"; +import { MEME_CODE_HASHES, WRITABLE_MEME_VERSIONS } from "./constants.js"; +import { formatUnits, percentageString } from "./amounts.js"; +import { UranusError, exitError } from "./errors.js"; +import { normalizeAddress } from "./abi.js"; + +function item(stack, index, type) { + const value = stack[index]; + if (!value || (type && value.type !== type)) throw new UranusError("INVALID_RESPONSE", `Unexpected getter stack item ${index}`); + return value; +} + +function int(stack, index) { + return item(stack, index, "int").value; +} + +function bool(stack, index) { + return int(stack, index) !== 0n; +} + +function addressFromItem(stack, index) { + const value = item(stack, index); + if (!["cell", "slice"].includes(value.type)) throw new UranusError("INVALID_RESPONSE", `Expected address at stack item ${index}`); + return value.cell.beginParse().loadAddress().toString(); +} + +function maybeAddressFromItem(stack, index) { + const value = item(stack, index); + if (!["cell", "slice"].includes(value.type)) throw new UranusError("INVALID_RESPONSE", `Expected address at stack item ${index}`); + return value.cell.beginParse().loadMaybeAddress()?.toString() ?? null; +} + +function cell(stack, index) { + const value = item(stack, index); + if (!["cell", "slice"].includes(value.type)) throw new UranusError("INVALID_RESPONSE", `Expected cell at stack item ${index}`); + return value.cell; +} + +function requireExit(result, method) { + if (!result || !Array.isArray(result.stack)) throw new UranusError("INVALID_RESPONSE", `${method} returned no stack`); + if (result.exitCode !== 0 && result.exitCode !== 1) throw exitError(result.exitCode); + return result.stack; +} + +async function optionalGetter(sdk, address, method, parser) { + try { + const result = await sdk.ton.runGetMethod(address, method); + return parser(requireExit(result, method)); + } catch (error) { + sdk.log?.debug?.(`uranus: optional ${method} unavailable: ${String(error?.message ?? error)}`); + return null; + } +} + +function base64HashToHex(value) { + if (!value) return null; + if (/^[0-9a-f]{64}$/i.test(value)) return value.toLowerCase(); + try { + const hex = Buffer.from(String(value), "base64").toString("hex"); + return hex.length === 64 ? hex : null; + } catch { + return null; + } +} + +function findAccount(payload, address) { + const list = payload?.account_states ?? payload?.accounts ?? payload?.result ?? []; + if (!Array.isArray(list)) return null; + const normalized = Address.parse(address).toRawString(); + return list.find((entry) => { + try { + return Address.parse(entry.address ?? entry.account ?? "").toRawString() === normalized; + } catch { + return false; + } + }) ?? null; +} + +function detectVersion(codeHash) { + return Object.entries(MEME_CODE_HASHES).find(([, hash]) => hash === codeHash)?.[0] ?? null; +} + +function parseMemeData(stack) { + if (stack.length < 13) throw new UranusError("INVALID_RESPONSE", "get_meme_data returned an incomplete stack"); + return { + initialized: bool(stack, 0), + migrated: bool(stack, 1), + controllerAddress: addressFromItem(stack, 2), + creatorAddress: addressFromItem(stack, 3), + creatorFee: int(stack, 4), + seed: int(stack, 5), + isGraduated: bool(stack, 6), + alpha: int(stack, 7), + beta: int(stack, 8), + onSellSupply: int(stack, 9), + tradeFeeBps: Number(int(stack, 10)), + raisedFunds: int(stack, 11), + currentSupply: int(stack, 12), + }; +} + +function parseJettonData(stack) { + if (stack.length < 5) throw new UranusError("INVALID_RESPONSE", "get_jetton_data returned an incomplete stack"); + return { + totalSupply: int(stack, 0), + mintable: bool(stack, 1), + adminAddress: maybeAddressFromItem(stack, 2), + metadataCell: cell(stack, 3), + walletCode: cell(stack, 4), + }; +} + +function parsePartnerData(stack) { + if (stack.length < 4) throw new UranusError("INVALID_RESPONSE", "get_partner_data returned an incomplete stack"); + return { + partnerFee: int(stack, 0), + partnerFeeBps: Number(int(stack, 1)), + partnerAddress: addressFromItem(stack, 2), + poolPartnerFeeBps: Number(int(stack, 3)), + }; +} + +function parseBondingCurveData(stack) { + if (stack.length < 10) throw new UranusError("INVALID_RESPONSE", "get_bonding_curve_data returned an incomplete stack"); + return { + isGraduated: bool(stack, 0), + maxSupply: int(stack, 1), + currentSupply: int(stack, 2), + bondingCurveSupply: int(stack, 3), + liquiditySupply: int(stack, 4), + raisingFunds: int(stack, 5), + migrationFee: int(stack, 6), + raisedFunds: int(stack, 7), + alpha: int(stack, 8), + beta: int(stack, 9), + }; +} + +function extractMetadataUri(account, jettonInfo) { + const candidates = [ + account?.metadata?.token_info?.[0]?.extra?.uri, + account?.metadata?.jetton?.content?.uri, + jettonInfo?.uri, + ]; + return candidates.find((value) => typeof value === "string" && value.length > 0) ?? null; +} + +function metadataKey(name) { + return BigInt(`0x${createHash("sha256").update(name).digest("hex")}`); +} + +function snakeValue(value) { + const slice = value.beginParse(); + try { + if (slice.remainingBits >= 8) slice.skip(8); + return slice.loadStringTail(); + } catch { + return value.beginParse().loadStringTail(); + } +} + +function parseContentCell(metadataCell) { + if (!metadataCell) return {}; + try { + const slice = metadataCell.beginParse(); + if (slice.remainingBits < 8) return {}; + const prefix = slice.loadUint(8); + if (prefix === 1) return { uri: slice.loadStringTail() || null }; + if (prefix !== 0) return {}; + const dict = slice.loadDict(Dictionary.Keys.BigUint(256), Dictionary.Values.Cell()); + const read = (name) => { + const value = dict.get(metadataKey(name)); + if (!value) return null; + try { return snakeValue(value); } catch { return null; } + }; + return { name: read("name"), symbol: read("symbol"), description: read("description"), image: read("image"), decimals: read("decimals"), uri: read("uri") }; + } catch { + return {}; + } +} + +function curveTarget(meme) { + if (!meme || meme.alpha <= 0n || meme.beta <= 0n || meme.onSellSupply < 0n || meme.onSellSupply >= meme.alpha) return null; + return (meme.beta * meme.onSellSupply) / (meme.alpha - meme.onSellSupply); +} + +function sanitizeMetadata(value) { + if (!value || typeof value !== "object" || Array.isArray(value)) return {}; + const text = (key, max = 4096) => typeof value[key] === "string" ? value[key].slice(0, max) : null; + return Object.fromEntries(Object.entries({ name: text("name", 256), symbol: text("symbol", 64), description: text("description"), image: text("image", 1024), decimals: text("decimals", 16) }).filter(([, value]) => value !== null)); +} + +export function createState(sdk, http) { + async function inspectContract(memeAddress) { + const address = normalizeAddress(memeAddress); + const payload = await http.accountState(address); + const account = findAccount(payload, address); + const status = account?.status ?? account?.account_status ?? "unknown"; + const codeHash = base64HashToHex(account?.code_hash ?? account?.codeHash); + const version = detectVersion(codeHash); + return { address, account, payload, accountStatus: status, codeHash, version, verified: Boolean(version) && status === "active" }; + } + + async function memeInfo(memeAddress) { + const identity = await inspectContract(memeAddress); + // Keep RPC getters sequential. Public endpoints commonly rate-limit bursts of + // parallel runMethod calls, while every getter is independently optional. + const meme = await optionalGetter(sdk, identity.address, "get_meme_data", parseMemeData); + const jetton = await optionalGetter(sdk, identity.address, "get_jetton_data", parseJettonData); + const bonding = await optionalGetter(sdk, identity.address, "get_bonding_curve_data", parseBondingCurveData); + const partner = await optionalGetter(sdk, identity.address, "get_partner_data", parsePartnerData); + const jettonInfo = await sdk.ton.getJettonInfo(identity.address).catch(() => null); + const raw = Address.parse(identity.address).toRawString(); + const indexedMetadata = identity.payload?.metadata?.[raw] + ?? identity.payload?.metadata?.[raw.toUpperCase()] + ?? identity.payload?.metadata?.[identity.account?.address]; + const contentMetadata = parseContentCell(jetton?.metadataCell); + const onchainMetadata = sanitizeMetadata(contentMetadata); + const uri = contentMetadata.uri ?? extractMetadataUri({ ...identity.account, metadata: indexedMetadata }, jettonInfo); + let metadata = onchainMetadata; + let metadataError = null; + if (uri) { + try { + metadata = { ...sanitizeMetadata(await http.metadata(uri)), ...onchainMetadata }; + } catch (error) { + metadataError = String(error?.message ?? error).slice(0, 500); + } + } + const decimals = Number(metadata.decimals ?? jettonInfo?.decimals ?? 9); + const safeDecimals = Number.isInteger(decimals) && decimals >= 0 && decimals <= 18 ? decimals : 9; + const graduated = meme?.isGraduated ?? bonding?.isGraduated ?? false; + const migrated = meme?.migrated ?? false; + const lifecycle = migrated ? "migrated" : graduated ? "migration_pending" : meme?.initialized ? "on_curve" : meme?.initialized === false ? "uninitialized" : "unknown"; + const curveTargetFunds = curveTarget(meme); + const fallbackMigrationFee = 50n * 1_000_000_000n; + const targetFunds = bonding?.raisingFunds + ?? (curveTargetFunds !== null && curveTargetFunds > fallbackMigrationFee ? curveTargetFunds - fallbackMigrationFee : null); + const migrationFee = bonding?.migrationFee ?? fallbackMigrationFee; + const raisedFunds = bonding?.raisedFunds ?? meme?.raisedFunds ?? null; + return { + address: identity.address, + version: identity.version, + code_hash: identity.codeHash, + verified: identity.verified, + account_status: identity.accountStatus, + writable_curve: Boolean(identity.version && WRITABLE_MEME_VERSIONS.has(identity.version)), + lifecycle, + name: metadata.name ?? jettonInfo?.name ?? null, + symbol: metadata.symbol ?? jettonInfo?.symbol ?? null, + description: metadata.description ?? jettonInfo?.description ?? null, + image: metadata.image ?? jettonInfo?.image ?? null, + metadata_uri: uri, + metadata_error: metadataError, + decimals: safeDecimals, + total_supply: jetton?.totalSupply?.toString() ?? jettonInfo?.totalSupply ?? null, + max_supply: bonding?.maxSupply?.toString() ?? jetton?.totalSupply?.toString() ?? null, + current_supply: bonding?.currentSupply?.toString() ?? meme?.currentSupply?.toString() ?? null, + bonding_supply: bonding?.bondingCurveSupply?.toString() ?? meme?.onSellSupply?.toString() ?? null, + liquidity_supply: bonding?.liquiditySupply?.toString() ?? (jetton && meme ? (jetton.totalSupply - meme.onSellSupply).toString() : null), + controller_address: meme?.controllerAddress ?? null, + creator_address: meme?.creatorAddress ?? null, + initialized: meme?.initialized ?? null, + graduated: meme?.isGraduated ?? bonding?.isGraduated ?? null, + migrated: meme?.migrated ?? null, + alpha: meme?.alpha?.toString() ?? bonding?.alpha?.toString() ?? null, + beta: meme?.beta?.toString() ?? bonding?.beta?.toString() ?? null, + raised_funds_nano: raisedFunds?.toString() ?? null, + raised_funds_ton: raisedFunds !== null ? formatUnits(raisedFunds) : null, + target_funds_nano: targetFunds?.toString() ?? null, + target_funds_ton: targetFunds !== null ? formatUnits(targetFunds) : null, + migration_fee_nano: migrationFee.toString(), + migration_fee_ton: formatUnits(migrationFee), + trade_fee_bps: meme?.tradeFeeBps ?? null, + creator_fee_nano: meme?.creatorFee?.toString() ?? null, + creator_fee_ton: meme ? formatUnits(meme.creatorFee) : null, + partner_address: partner?.partnerAddress ?? null, + partner_fee_nano: partner?.partnerFee?.toString() ?? null, + partner_fee_ton: partner ? formatUnits(partner.partnerFee) : null, + partner_fee_bps: partner?.partnerFeeBps ?? null, + pool_partner_fee_bps: partner?.poolPartnerFeeBps ?? null, + progress_percent: targetFunds !== null && raisedFunds !== null ? percentageString(raisedFunds, targetFunds) : null, + bonding_curve_data: bonding ? Object.fromEntries(Object.entries(bonding).map(([key, value]) => [key, typeof value === "bigint" ? value.toString() : value])) : null, + state_timestamp: new Date().toISOString(), + _raw: { meme, jetton, partner }, + }; + } + + async function walletInfo(memeAddress, ownerAddress) { + const meme = normalizeAddress(memeAddress); + const owner = normalizeAddress(ownerAddress ?? sdk.ton.getAddress()); + const derived = await sdk.ton.getJettonWalletAddress(owner, meme); + if (!derived) throw new UranusError("UPSTREAM_UNAVAILABLE", "Unable to derive the Uranus Meme Wallet address"); + const walletAddress = normalizeAddress(derived); + let walletData = null; + try { + const result = await sdk.ton.runGetMethod(walletAddress, "get_wallet_data"); + const stack = requireExit(result, "get_wallet_data"); + walletData = { + balance: int(stack, 0), + owner: addressFromItem(stack, 1), + master: addressFromItem(stack, 2), + }; + } catch (error) { + sdk.log?.debug?.(`uranus: wallet ${walletAddress} is not active: ${String(error?.message ?? error)}`); + } + const masterMatches = !walletData || normalizeAddress(walletData.master) === meme; + const ownerMatches = !walletData || normalizeAddress(walletData.owner) === owner; + return { + wallet_address: walletAddress, + owner_address: owner, + master_address: meme, + balance_raw: walletData?.balance?.toString() ?? "0", + balance: formatUnits(walletData?.balance ?? 0n, 9), + deployed: Boolean(walletData), + verified: Boolean(walletData && masterMatches && ownerMatches), + _rawBalance: walletData?.balance ?? 0n, + }; + } + + async function portfolio({ include_zero = false, limit = 50 } = {}) { + const balances = await sdk.ton.getJettonBalances(); + const output = []; + for (const balance of balances.slice(0, Math.min(100, limit * 3))) { + if (!include_zero && BigInt(balance.balance) === 0n) continue; + try { + const identity = await inspectContract(balance.jettonAddress); + if (!identity.verified) continue; + output.push({ + meme_address: identity.address, + version: identity.version, + wallet_address: balance.walletAddress, + balance_raw: balance.balance, + balance: balance.balanceFormatted, + symbol: balance.symbol, + name: balance.name, + decimals: balance.decimals, + }); + if (output.length >= limit) break; + } catch (error) { + sdk.log?.debug?.(`uranus: skipped portfolio candidate: ${String(error?.message ?? error)}`); + } + } + return { owner_address: normalizeAddress(sdk.ton.getAddress()), holdings: output }; + } + + function walletAddressStack(ownerAddress) { + return [{ type: "slice", cell: beginCell().storeAddress(Address.parse(ownerAddress)).endCell() }]; + } + + return { inspectContract, memeInfo, walletInfo, portfolio, walletAddressStack }; +} diff --git a/plugins/vid/index.js b/plugins/vid/index.js index 1b4e61b..615cc70 100644 --- a/plugins/vid/index.js +++ b/plugins/vid/index.js @@ -5,22 +5,14 @@ * Messages appear "via @vid" just like typing @vid in the Telegram input field. */ -import { createRequire } from "node:module"; -import { realpathSync } from "node:fs"; - -// Resolve "telegram" from teleton's own node_modules (not the plugin directory). -// realpathSync follows the symlink so createRequire looks in the right node_modules. -const _require = createRequire(realpathSync(process.argv[1])); -const { Api } = _require("telegram"); - // --------------------------------------------------------------------------- // Export // --------------------------------------------------------------------------- export const manifest = { name: "vid", - version: "1.0.1", - sdkVersion: ">=1.0.0", + version: "2.0.0", + sdkVersion: "^2.1.0", description: "Search and send YouTube videos in chat via Telegram's @vid inline bot.", }; @@ -52,51 +44,22 @@ export const tools = (sdk) => [ execute: async (params, context) => { try { - const client = sdk.telegram.getRawClient(); - const vidBot = await client.getEntity("vid"); - const peer = await client.getInputEntity(context.chatId); - - const results = await client.invoke( - new Api.messages.GetInlineBotResults({ - bot: vidBot, - peer, - query: params.query, - offset: "", - }) - ); - - if (!results.results || results.results.length === 0) { - return { success: false, error: `No YouTube videos found for "${params.query}"` }; - } - - const index = params.index ?? 0; - if (index >= results.results.length) { - return { - success: false, - error: `Only ${results.results.length} results available, index ${index} is out of range`, - }; - } - - const chosen = results.results[index]; - - await client.invoke( - new Api.messages.SendInlineBotResult({ - peer, - queryId: results.queryId, - id: chosen.id, - randomId: BigInt(Math.floor(Math.random() * 2 ** 62)), - }) + const result = await sdk.telegram.sendInlineBotResult( + context.chatId, + "vid", + params.query, + params.index ); return { success: true, data: { - query: params.query, - sent_index: index, - total_results: results.results.length, - title: chosen.title || null, - description: chosen.description || null, - type: chosen.type || null, + query: result.query, + sent_index: result.sentIndex, + total_results: result.totalResults, + title: result.title, + description: result.description, + type: result.type, }, }; } catch (err) { diff --git a/plugins/vid/manifest.json b/plugins/vid/manifest.json index e0e9794..d556bd3 100644 --- a/plugins/vid/manifest.json +++ b/plugins/vid/manifest.json @@ -1,7 +1,7 @@ { "id": "vid", "name": "@vid โ€” Inline YouTube Search", - "version": "1.0.1", + "version": "2.0.0", "description": "Search and send YouTube videos via Telegram's @vid inline bot", "author": { "name": "teleton", @@ -9,11 +9,11 @@ }, "license": "MIT", "entry": "index.js", - "teleton": ">=1.0.0", + "teleton": ">=0.9.0", "tools": [ { "name": "vid", "description": "Search and send a YouTube video in the current chat via @vid" } ], - "sdkVersion": ">=1.0.0", + "sdkVersion": "^2.1.0", "permissions": [], "tags": ["videos", "youtube", "search", "inline-bot"], "repository": "https://github.com/TONresistor/teleton-plugins", diff --git a/plugins/voice-notes/README.md b/plugins/voice-notes/README.md index fe72bc0..fbdf94d 100644 --- a/plugins/voice-notes/README.md +++ b/plugins/voice-notes/README.md @@ -1,5 +1,8 @@ # voice-notes +> [!WARNING] +> Legacy SDK v1 plugin. It is quarantined and not installable from the SDK v2 marketplace. + Transcribe voice messages and video notes using Telegram Premium built-in speech-to-text โ€” no external APIs or keys required. ## Tools diff --git a/plugins/voice-notes/index.js b/plugins/voice-notes/index.js index 3262989..39797af 100644 --- a/plugins/voice-notes/index.js +++ b/plugins/voice-notes/index.js @@ -11,7 +11,7 @@ function sleep(ms) { export const manifest = { name: "voice-notes", version: "1.0.0", - sdkVersion: ">=1.0.0", + sdkVersion: "^1.0.0", description: "Transcribe voice messages and video notes using Telegram Premium speech-to-text.", }; diff --git a/plugins/voice-notes/manifest.json b/plugins/voice-notes/manifest.json index cd6fe18..18b1fd1 100644 --- a/plugins/voice-notes/manifest.json +++ b/plugins/voice-notes/manifest.json @@ -9,11 +9,11 @@ }, "license": "MIT", "entry": "index.js", - "teleton": ">=1.0.0", + "teleton": ">=0.9.0", "tools": [ { "name": "voice_transcribe", "description": "Transcribe a voice message or video note to text" } ], - "sdkVersion": ">=1.0.0", + "sdkVersion": "^1.0.0", "permissions": [], "tags": ["telegram", "voice", "transcription"], "repository": "https://github.com/TONresistor/teleton-plugins", diff --git a/plugins/weather/index.js b/plugins/weather/index.js index 713a14a..d83c160 100644 --- a/plugins/weather/index.js +++ b/plugins/weather/index.js @@ -42,7 +42,7 @@ async function geocode(city) { export const manifest = { name: "weather", version: "1.0.0", - sdkVersion: ">=1.0.0", + sdkVersion: "^2.0.0", description: "Current weather and 7-day forecast via Open-Meteo (no API key required).", }; diff --git a/plugins/weather/manifest.json b/plugins/weather/manifest.json index 995d16a..c03a6e0 100644 --- a/plugins/weather/manifest.json +++ b/plugins/weather/manifest.json @@ -9,8 +9,8 @@ }, "license": "MIT", "entry": "index.js", - "teleton": ">=1.0.0", - "sdkVersion": ">=1.0.0", + "teleton": ">=0.9.0", + "sdkVersion": "^2.0.0", "tools": [ { "name": "weather_current", "description": "Get current weather for any city" }, { "name": "weather_forecast", "description": "Get 7-day forecast with daily min/max temperatures" } diff --git a/plugins/webdom/README.md b/plugins/webdom/README.md index b33ceeb..d0904c7 100644 --- a/plugins/webdom/README.md +++ b/plugins/webdom/README.md @@ -2,7 +2,7 @@ Buy, sell, auction, and manage **.ton domains** and **Telegram usernames** on [webdom.market](https://webdom.market) -- the first dedicated marketplace for TON DNS. -Read tools use the webdom REST API. Action tools sign on-chain transactions from the agent wallet, interacting with the webdom marketplace smart contracts or native TON DNS auctions. +Read tools use the webdom REST API. Action tools use Teleton's protected wallet broker to interact with webdom marketplace contracts or native TON DNS auctions. ## Tools @@ -61,10 +61,7 @@ Webdom marketplace auctions (listed by owners on webdom.market) use `webdom_plac Requires at runtime (provided by teleton): - `@ton/core` -- Address, beginCell, toNano, SendMode -- `@ton/ton` -- WalletContractV5R1, TonClient -- `@ton/crypto` -- mnemonicToPrivateKey - -Agent wallet at `~/.teleton/wallet.json` is used for signing all on-chain transactions. +- Teleton Plugin SDK v2 transaction broker for all on-chain writes ## Fee structure diff --git a/plugins/webdom/index.js b/plugins/webdom/index.js index 3e81bf1..0a2bc32 100644 --- a/plugins/webdom/index.js +++ b/plugins/webdom/index.js @@ -3,7 +3,7 @@ * * Buy, sell, auction, and manage domains on webdom.market. * Read-only tools use the public API; action tools sign on-chain - * transactions from the agent's wallet at ~/.teleton/wallet.json. + * transactions through Teleton's protected wallet broker. * * Uses Plugin SDK exclusively: * - sdk.storage for API response caching @@ -21,8 +21,8 @@ import { actionTools } from "./tools/actions.js"; export const manifest = { name: "webdom", - version: "1.0.0", - sdkVersion: ">=1.0.0", + version: "2.0.0", + sdkVersion: "^2.0.0", description: "Buy, sell, auction, and manage .ton domains and Telegram usernames on webdom.market", }; diff --git a/plugins/webdom/manifest.json b/plugins/webdom/manifest.json index 8389fda..ef6a140 100644 --- a/plugins/webdom/manifest.json +++ b/plugins/webdom/manifest.json @@ -1,13 +1,13 @@ { "id": "webdom", "name": "Webdom Domain Marketplace", - "version": "1.0.0", + "version": "2.0.0", "description": "Buy, sell, auction, and manage .ton domains and Telegram usernames on webdom.market", "author": { "name": "teleton", "url": "https://github.com/TONresistor" }, "license": "MIT", "entry": "index.js", - "sdkVersion": ">=1.0.0", - "teleton": ">=1.0.0", + "sdkVersion": "^2.0.0", + "teleton": ">=0.9.0", "secrets": {}, "tools": [ { "name": "webdom_search_domains", "description": "Search and filter .ton domains and Telegram usernames on the marketplace" }, diff --git a/plugins/webdom/tools/actions.js b/plugins/webdom/tools/actions.js index 42a0957..18d5ed3 100644 --- a/plugins/webdom/tools/actions.js +++ b/plugins/webdom/tools/actions.js @@ -4,9 +4,7 @@ */ import { createRequire } from "node:module"; -import { realpathSync, readFileSync } from "node:fs"; -import { join } from "node:path"; -import { homedir } from "node:os"; +import { realpathSync } from "node:fs"; import { WEBDOM_MARKETPLACE, @@ -18,15 +16,8 @@ import { // --------------------------------------------------------------------------- const _require = createRequire(realpathSync(process.argv[1])); -const { Address, beginCell, toNano, SendMode } = _require("@ton/core"); -const { WalletContractV5R1, TonClient, internal } = _require("@ton/ton"); -const { mnemonicToPrivateKey } = _require("@ton/crypto"); - -// --------------------------------------------------------------------------- -// Constants -// --------------------------------------------------------------------------- - -const WALLET_FILE = join(homedir(), ".teleton", "wallet.json"); +const { Address, beginCell, toNano } = _require("@ton/core"); +const { TonClient } = _require("@ton/ton"); // --------------------------------------------------------------------------- // RPC endpoint resolution (same logic as core endpoint.ts) @@ -97,20 +88,7 @@ async function getNftSaleInfo(nftAddress) { // --------------------------------------------------------------------------- let _log = null; - -function loadWalletKeyPair() { - let walletData; - try { - walletData = JSON.parse(readFileSync(WALLET_FILE, "utf-8")); - } catch (err) { - _log?.error("[webdom] wallet read failed:", err.message); - throw new Error("Agent wallet not found at " + WALLET_FILE); - } - if (!walletData.mnemonic || !Array.isArray(walletData.mnemonic)) { - throw new Error("Invalid wallet file: missing mnemonic array"); - } - return walletData.mnemonic; -} +let _ton = null; async function getSaleData(saleAddr) { const endpoints = await getAllEndpoints(); @@ -147,94 +125,23 @@ function buildDeployFeeBody(nftAddress, queryId) { async function sendMultiMessages(messages) { _log?.info("[webdom] sendMultiMessages, count:", messages.length); - - const mnemonic = loadWalletKeyPair(); - const keyPair = await mnemonicToPrivateKey(mnemonic); - const wallet = WalletContractV5R1.create({ workchain: 0, publicKey: keyPair.publicKey }); - _log?.info("[webdom] wallet:", wallet.address.toString({ bounceable: true })); - - const endpoints = await getAllEndpoints(); - let lastErr; - - for (const ep of endpoints) { - try { - _log?.info("[webdom] trying endpoint:", ep); - const client = new TonClient({ endpoint: ep }); - const contract = client.open(wallet); - - const seqno = await contract.getSeqno(); - _log?.info("[webdom] seqno:", seqno); - - if (ep.includes("toncenter.com")) await new Promise((r) => setTimeout(r, 3000)); - - await contract.sendTransfer({ - seqno, - secretKey: keyPair.secretKey, - sendMode: SendMode.PAY_GAS_SEPARATELY | SendMode.IGNORE_ERRORS, - messages, - }); - - _log?.info("[webdom] multi-message sent via", ep, "seqno:", seqno); - return { - tx_seqno: seqno, - wallet_address: wallet.address.toString({ bounceable: true }), - }; - } catch (err) { - _log?.warn("[webdom] endpoint failed:", ep, err.message); - lastErr = err; - await new Promise((r) => setTimeout(r, 1000)); - } - } - - throw new Error("All RPC endpoints failed: " + lastErr?.message); + const walletAddress = _ton.getAddress(); + if (!walletAddress) throw new Error("Agent wallet is not initialized"); + const result = await _ton.sendMessages(messages, { sendMode: 3 }); + return { tx_seqno: result.seqno, tx_hash: result.hash, wallet_address: walletAddress }; } async function sendTransaction(to, value, body) { _log?.info("[webdom] sendTransaction to:", to.toString(), "value:", value.toString()); - const mnemonic = loadWalletKeyPair(); - const keyPair = await mnemonicToPrivateKey(mnemonic); - const wallet = WalletContractV5R1.create({ workchain: 0, publicKey: keyPair.publicKey }); - _log?.info("[webdom] wallet:", wallet.address.toString({ bounceable: true })); - - const endpoints = await getAllEndpoints(); - let lastErr; - - for (const ep of endpoints) { - try { - _log?.info("[webdom] trying endpoint:", ep); - const client = new TonClient({ endpoint: ep }); - const contract = client.open(wallet); - - const seqno = await contract.getSeqno(); - _log?.info("[webdom] seqno:", seqno); - - // toncenter rate limit without API key โ€” need ~3s between requests - if (ep.includes("toncenter.com")) await new Promise((r) => setTimeout(r, 3000)); - - await contract.sendTransfer({ - seqno, - secretKey: keyPair.secretKey, - sendMode: SendMode.PAY_GAS_SEPARATELY | SendMode.IGNORE_ERRORS, - messages: [ - internal({ to, value, body, bounce: true }), - ], - }); - - _log?.info("[webdom] transaction sent via", ep, "seqno:", seqno); - return { - tx_seqno: seqno, - wallet_address: wallet.address.toString({ bounceable: true }), - }; - } catch (err) { - _log?.warn("[webdom] endpoint failed:", ep, err.message); - lastErr = err; - // wait 1s before trying next endpoint - await new Promise((r) => setTimeout(r, 1000)); - } - } - - throw new Error("All RPC endpoints failed: " + lastErr?.message); + const walletAddress = _ton.getAddress(); + if (!walletAddress) throw new Error("Agent wallet is not initialized"); + const result = await _ton.send(to.toString(), Number(_ton.fromNano(value)), { + body, + bounce: true, + sendMode: 3, + }); + return { tx_seqno: result.seqno, tx_hash: result.hash, wallet_address: walletAddress }; } // --------------------------------------------------------------------------- @@ -271,6 +178,7 @@ function buildNftTransferBody(newOwner, responseAddr, forwardAmount, forwardPayl export const actionTools = (sdk) => { _log = sdk.log; + _ton = sdk.ton; return [ @@ -414,10 +322,9 @@ export const actionTools = (sdk) => { .endCell(); // Need wallet address for response_destination in NFT transfer - const mnemonic = loadWalletKeyPair(); - const keyPair = await mnemonicToPrivateKey(mnemonic); - const wallet = WalletContractV5R1.create({ workchain: 0, publicKey: keyPair.publicKey }); - const senderAddr = wallet.address; + const senderAddress = _ton.getAddress(); + if (!senderAddress) throw new Error("Agent wallet is not initialized"); + const senderAddr = Address.parse(senderAddress); // Shared query_id (timestamp ms) โ€” marketplace matches fee to listing via this const queryId = Date.now(); @@ -428,18 +335,18 @@ export const actionTools = (sdk) => { // Send 2 messages in one tx: deploy fee to marketplace + NFT transfer to domain const result = await sendMultiMessages([ - internal({ - to: MARKETPLACE, - value: DEPLOY_FEE_VALUE, + { + to: MARKETPLACE.toString(), + value: Number(_ton.fromNano(DEPLOY_FEE_VALUE)), body: buildDeployFeeBody(domainAddr, queryId), bounce: true, - }), - internal({ - to: domainAddr, - value: FORWARD_NFT + toNano("0.15"), + }, + { + to: domainAddr.toString(), + value: Number(_ton.fromNano(FORWARD_NFT + toNano("0.15"))), body: nftBody, bounce: true, - }), + }, ]); // Poll TONAPI for the deployed sale contract address (takes ~10-15s on-chain) @@ -529,10 +436,9 @@ export const actionTools = (sdk) => { .endCell(); // Need wallet address for response_destination in NFT transfer - const mnemonic = loadWalletKeyPair(); - const keyPair = await mnemonicToPrivateKey(mnemonic); - const wallet = WalletContractV5R1.create({ workchain: 0, publicKey: keyPair.publicKey }); - const senderAddr = wallet.address; + const senderAddress = _ton.getAddress(); + if (!senderAddress) throw new Error("Agent wallet is not initialized"); + const senderAddr = Address.parse(senderAddress); // Shared query_id โ€” marketplace matches fee to listing via this const queryId = Date.now(); @@ -542,18 +448,18 @@ export const actionTools = (sdk) => { // Send 2 messages in one tx: deploy fee to marketplace + NFT transfer to domain const result = await sendMultiMessages([ - internal({ - to: MARKETPLACE, - value: DEPLOY_FEE_VALUE, + { + to: MARKETPLACE.toString(), + value: Number(_ton.fromNano(DEPLOY_FEE_VALUE)), body: buildDeployFeeBody(domainAddr, queryId), bounce: true, - }), - internal({ - to: domainAddr, - value: FORWARD_NFT + toNano("0.15"), + }, + { + to: domainAddr.toString(), + value: Number(_ton.fromNano(FORWARD_NFT + toNano("0.15"))), body: nftBody, bounce: true, - }), + }, ]); // Poll TONAPI for the deployed auction contract address diff --git a/references/patterns.md b/references/patterns.md index cdfa92f..753d364 100644 --- a/references/patterns.md +++ b/references/patterns.md @@ -1,515 +1,220 @@ -# Code Patterns & Best Practices +# SDK v2 implementation patterns -Ready-to-use code templates for common plugin scenarios. For full SDK method signatures and types, read the [SDK README](https://github.com/TONresistor/teleton-agent/blob/main/packages/sdk/README.md). +These examples use only public Teleton SDK v2 capabilities. The canonical method signatures live in +the [`@teleton-agent/sdk` README](https://github.com/TONresistor/teleton-agent/blob/dev/packages/sdk/README.md). -## Table of Contents +## External API read -- [GramJS / CJS Import](#gramjs--cjs-import) -- [Per-Plugin npm Dependencies](#per-plugin-npm-dependencies) -- [API Fetch with Timeout](#api-fetch-with-timeout) -- [Payment Verification Flow](#payment-verification-flow) -- [Inline Bot Mode](#inline-bot-mode) -- [Styled Keyboard Buttons](#styled-keyboard-buttons) -- [Database + Storage Patterns](#database--storage-patterns) -- [Secret Management](#secret-management) -- [Scheduled Messages](#scheduled-messages) -- [Media Handling](#media-handling) -- [Error Handling Patterns](#error-handling-patterns) -- [Anti-Patterns](#anti-patterns) - ---- - -## GramJS / CJS Import - -The teleton runtime is ESM-only, but some packages (GramJS, `@ton/core`) only ship CJS. Always use `createRequire` with `realpathSync`: - -```javascript -import { createRequire } from "node:module"; -import { realpathSync } from "node:fs"; - -const _require = createRequire(realpathSync(process.argv[1])); -const { Api } = _require("telegram"); -const { Address } = _require("@ton/core"); -``` - -**Never do this:** - -```javascript -// WRONG โ€” will fail at runtime -import { Api } from "telegram"; -import { Address } from "@ton/core"; -``` - -## Per-Plugin npm Dependencies - -Plugins can have their own `package.json` for dependencies beyond what teleton provides (`@ton/core`, `@ton/ton`, `@ton/crypto`, `telegram`). - -**Setup:** - -```bash -cd plugins/your-plugin -npm init -y -npm install some-package -# Commit BOTH package.json AND package-lock.json -``` - -**Dual-require pattern** โ€” separate core and plugin-local deps: - -```javascript -import { createRequire } from "node:module"; -import { realpathSync } from "node:fs"; - -// Core deps (provided by teleton runtime) -const _require = createRequire(realpathSync(process.argv[1])); -// Plugin-local deps (from your plugin's node_modules/) -const _pluginRequire = createRequire(import.meta.url); - -const { Address } = _require("@ton/core"); // core -const { getHttpEndpoint } = _pluginRequire("@orbs-network/ton-access"); // plugin-local -``` - -**Rules:** -- `package-lock.json` is **required** (loader skips install without it) -- Dependencies installed with `npm ci --ignore-scripts` (no postinstall scripts) -- `node_modules/` is gitignored โ€” created automatically at startup -- If install fails (no network), plugin is skipped with a warning - -## API Fetch with Timeout - -**Always** use `AbortSignal.timeout()` on every `fetch()` call: - -```javascript -execute: async (params, context) => { - try { - const res = await fetch(`https://api.example.com/data?q=${encodeURIComponent(params.query)}`, { - signal: AbortSignal.timeout(15_000), - headers: { "Accept": "application/json" }, - }); - if (!res.ok) { - return { success: false, error: `API returned ${res.status}` }; - } - const data = await res.json(); - return { success: true, data }; - } catch (err) { - return { success: false, error: String(err.message || err).slice(0, 500) }; - } +```js +async function fetchJson(url) { + const response = await fetch(url, { signal: AbortSignal.timeout(15_000) }); + if (!response.ok) throw new Error(`Upstream returned ${response.status}`); + return response.json(); } -``` -**With secrets for authenticated APIs:** - -```javascript -execute: async (params, context) => { - const apiKey = sdk.secrets.require("API_KEY"); // throws SECRET_NOT_FOUND if missing - try { - const res = await fetch("https://api.example.com/data", { - signal: AbortSignal.timeout(15_000), - headers: { - "Authorization": `Bearer ${apiKey}`, - "Accept": "application/json", - }, - }); - if (!res.ok) return { success: false, error: `API ${res.status}` }; - return { success: true, data: await res.json() }; - } catch (err) { - if (err.name === "PluginSDKError") { - return { success: false, error: `${err.code}: ${err.message}` }; - } - return { success: false, error: String(err.message || err).slice(0, 500) }; - } -} -``` - -## Payment Verification Flow - -Complete pattern for pay-to-use features: - -```javascript -export const tools = (sdk) => [ +export const tools = [ { - name: "myservice_pay", - description: "Pay 1 TON to use the service. Returns the wallet address and memo to include.", - scope: "dm-only", + name: "sample_lookup", + description: "Read a sample record", + scope: "always", category: "data-bearing", - parameters: { type: "object", properties: {} }, - execute: async (params, context) => { - const address = sdk.ton.getAddress(); - if (!address) return { success: false, error: "Wallet not initialized" }; - - const memo = `service-${context.senderId}-${Date.now()}`; - return { - success: true, - data: { - address, - amount: "1.0", - memo, - instructions: `Send exactly 1 TON to ${address} with memo: ${memo}`, - }, - }; - }, - }, - { - name: "myservice_verify", - description: "Verify payment and activate the service", - scope: "dm-only", - category: "action", parameters: { type: "object", - properties: { - memo: { type: "string", description: "Payment memo from myservice_pay" }, - }, - required: ["memo"], + properties: { id: { type: "string", minLength: 1, maxLength: 100 } }, + required: ["id"], }, - execute: async (params, context) => { + async execute({ id }) { try { - const result = await sdk.ton.verifyPayment({ - amount: 1.0, - memo: params.memo, - maxAgeMinutes: 30, - }); - if (!result.verified) { - return { success: false, error: result.error ?? "Payment not found" }; - } - // Payment confirmed โ€” activate service return { success: true, - data: { - verified: true, - txHash: result.txHash, - amount: result.amount, - }, + data: await fetchJson(`https://api.example.com/items/${encodeURIComponent(id)}`), }; - } catch (err) { - return { success: false, error: String(err.message || err).slice(0, 500) }; + } catch (error) { + return { success: false, error: String(error?.message ?? error).slice(0, 500) }; } }, }, ]; ``` -## Inline Bot Mode - -Full inline bot plugin with queries and callback buttons. Requires `bot` in manifest. +## Declared secret -```javascript +```js export const manifest = { - name: "my-inline", + name: "sample-auth", version: "1.0.0", - sdkVersion: ">=1.0.0", - description: "Inline search bot", - bot: { - inline: true, // enable onInlineQuery - callbacks: true, // enable onCallback - rateLimits: { // optional rate limits - inlinePerMinute: 30, - callbackPerMinute: 60, - }, + sdkVersion: "^2.0.0", + description: "Authenticated API example", + secrets: { + api_key: { required: true, description: "API key for the sample service" }, }, }; -export const tools = (sdk) => { - // Register inline query handler โ€” fires when user types @botname - sdk.bot.onInlineQuery(async (ctx) => { - // ctx.query โ€” the search text - // ctx.userId โ€” who is querying - // Return array of InlineResult objects - return [ - { - id: "1", - type: "article", - title: `Result for: ${ctx.query}`, - description: "Tap to send", - content: { text: `You searched for: ${ctx.query}` }, - replyMarkup: sdk.bot.keyboard([ - [ - { text: "Like", callback: "like:1", style: "success" }, - { text: "Dislike", callback: "dislike:1", style: "danger" }, - ], - ]).toTL(), // .toTL() for GramJS colored buttons, .toGrammy() for Bot API - }, - ]; - }); - - // Register callback handler โ€” fires on button presses matching the glob pattern - sdk.bot.onCallback("like:*", async (ctx) => { - // ctx.data โ€” full callback data (e.g. "like:1") - // ctx.userId โ€” who pressed - // ctx.match โ€” regex match groups (string[]) - await ctx.answer("Liked!"); // toast notification - await ctx.editMessage("You liked this result."); - }); - - sdk.bot.onCallback("dislike:*", async (ctx) => { - await ctx.answer("Disliked!"); - await ctx.editMessage("You disliked this result."); - }); - - // Optional: track which results users select - sdk.bot.onChosenResult(async (ctx) => { - sdk.log.info(`Chose result ${ctx.resultId} for query "${ctx.query}"`); - }); - - // Return tools array โ€” can be empty if plugin is purely inline-driven - return [ - { - name: "myinline_stats", - description: "Show inline bot usage stats", - parameters: { type: "object", properties: {} }, - category: "data-bearing", - execute: async (params, context) => { - return { success: true, data: { botUsername: sdk.bot.username } }; - }, +export const tools = (sdk) => [ + { + name: "sample_auth_profile", + description: "Read the authenticated profile", + scope: "admin-only", + category: "data-bearing", + parameters: { type: "object", properties: {} }, + async execute() { + const apiKey = sdk.secrets.get("api_key"); + if (!apiKey) return { success: false, error: "API key is not configured" }; + const response = await fetch("https://api.example.com/profile", { + headers: { Authorization: `Bearer ${apiKey}` }, + signal: AbortSignal.timeout(15_000), + }); + if (!response.ok) return { success: false, error: `Upstream returned ${response.status}` }; + return { success: true, data: await response.json() }; }, - ]; -}; + }, +]; ``` -**Key rules for inline mode:** -- `sdk.bot` is `null` unless manifest declares `bot` โ€” always check or declare it -- Callback patterns are **glob-matched** and auto-prefixed with plugin name (no conflicts) -- Button styles: `"success"` (green), `"danger"` (red), `"primary"` (blue) โ€” GramJS only, graceful fallback on Bot API -- `.toTL()` for GramJS styled buttons, `.toGrammy()` for standard Bot API keyboard - -## Styled Keyboard Buttons +## Isolated storage -```javascript -// Build a keyboard with colored buttons -const kb = sdk.bot.keyboard([ - [ - { text: "Buy", callback: "buy", style: "success" }, // green - { text: "Sell", callback: "sell", style: "danger" }, // red - ], - [ - { text: "Details", callback: "details", style: "primary" }, // blue - { text: "Cancel", callback: "cancel" }, // default - ], -]); - -// Use in inline results -return [{ id: "1", type: "article", title: "Trade", content: { text: "Choose action" }, replyMarkup: kb.toTL() }]; - -// Use in sendMessage (via inlineKeyboard option) -// Note: sendMessage uses raw button arrays, not sdk.bot.keyboard -await sdk.telegram.sendMessage(chatId, "Choose:", { - inlineKeyboard: [ - [{ text: "Option A", callback_data: "a" }, { text: "Option B", callback_data: "b" }], - ], -}); +```js +export const tools = (sdk) => [ + { + name: "sample_counter_increment", + description: "Increment the caller's isolated counter", + scope: "always", + category: "action", + parameters: { type: "object", properties: {} }, + async execute(_params, context) { + const key = `counter:${context.senderId}`; + const value = sdk.storage.get(key) ?? 0; + sdk.storage.set(key, value + 1); + return { success: true, data: { count: value + 1 } }; + }, + }, +]; ``` -## Database + Storage Patterns - -### When to use which - -| Need | Use | Why | -|------|-----|-----| -| Structured data, queries, relations | `sdk.db` + `migrate()` | Full SQL power | -| Simple cache, rate limits, TTL | `sdk.storage` | No migration needed | -| Counters, scores, leaderboards | `sdk.db` | Aggregation queries | -| API response caching | `sdk.storage` with TTL | Auto-expiry | - -### Database (sdk.db) +## Explicit database migration -```javascript +```js export function migrate(db) { - db.exec(`CREATE TABLE IF NOT EXISTS scores ( - user_id TEXT PRIMARY KEY, - points INTEGER NOT NULL DEFAULT 0, - updated_at INTEGER NOT NULL DEFAULT (unixepoch()) - )`); + db.exec(` + CREATE TABLE IF NOT EXISTS records ( + id TEXT PRIMARY KEY, + value TEXT NOT NULL, + created_at INTEGER NOT NULL + ) + `); } -export const tools = (sdk) => [{ - name: "myplugin_score", - execute: async (params, context) => { - const userId = String(context.senderId); - sdk.db.prepare( - `INSERT INTO scores (user_id, points) VALUES (?, 1) - ON CONFLICT(user_id) DO UPDATE SET points = points + 1, updated_at = unixepoch()` - ).run(userId); - const row = sdk.db.prepare("SELECT points FROM scores WHERE user_id = ?").get(userId); - return { success: true, data: { points: row.points } }; +export const tools = (sdk) => [ + { + name: "sample_record_get", + description: "Read one isolated plugin record", + scope: "admin-only", + category: "data-bearing", + parameters: { + type: "object", + properties: { id: { type: "string", minLength: 1, maxLength: 100 } }, + required: ["id"], + }, + async execute({ id }) { + const record = sdk.db.prepare("SELECT id, value, created_at FROM records WHERE id = ?").get(id); + return { success: true, data: record ?? null }; + }, }, -}]; -``` - -### Storage (sdk.storage) - -```javascript -// Cache with 1-hour TTL (milliseconds) -const cached = sdk.storage.get("price_data"); -if (cached) return { success: true, data: cached }; - -const fresh = await fetchPrice(); -sdk.storage.set("price_data", fresh, { ttl: 3_600_000 }); -return { success: true, data: fresh }; - -// Rate limiting -const key = `rate:${context.senderId}`; -if (sdk.storage.has(key)) { - return { success: false, error: "Rate limited. Try again in 1 minute." }; -} -sdk.storage.set(key, true, { ttl: 60_000 }); -``` - -## Secret Management - -**Declare secrets in manifest.json:** - -```json -{ - "secrets": { - "api_key": { "required": true, "description": "API key for the service" }, - "webhook_url": { "required": false, "description": "Optional webhook endpoint" } - } -} +]; ``` -**Use in code:** - -```javascript -// Throws SECRET_NOT_FOUND if missing โ€” fail fast -const apiKey = sdk.secrets.require("api_key"); +Use placeholders for every value. Plugin databases are isolated; never attach or open Teleton's main +database. -// Returns undefined if not set โ€” for optional secrets -const webhook = sdk.secrets.get("webhook_url"); +## Telegram message -// Check existence -if (sdk.secrets.has("premium_key")) { /* premium features */ } +```js +export const tools = (sdk) => [ + { + name: "sample_announce", + description: "Send an announcement to the current chat", + scope: "admin-only", + category: "action", + parameters: { + type: "object", + properties: { text: { type: "string", minLength: 1, maxLength: 2000 } }, + required: ["text"], + }, + async execute({ text }, context) { + const messageId = await sdk.telegram.sendMessage(context.chatId, text); + return { success: true, data: { messageId } }; + }, + }, +]; ``` -**Resolution order:** ENV variable (`YOURPLUGIN_API_KEY`) โ†’ secrets store (`~/.teleton/plugins/data/.secrets.json`) โ†’ pluginConfig fallback. - -## Scheduled Messages +## TON read and transfer -```javascript -// Schedule a message for later -const msgId = await sdk.telegram.scheduleMessage( - context.chatId, - "Reminder: meeting in 5 minutes!", - Math.floor(Date.now() / 1000) + 3600 // Unix timestamp, 1 hour from now -); - -// List scheduled messages -const scheduled = await sdk.telegram.getScheduledMessages(context.chatId); - -// Send a scheduled message immediately -await sdk.telegram.sendScheduledNow(context.chatId, msgId); - -// Delete a scheduled message -await sdk.telegram.deleteScheduledMessage(context.chatId, msgId); +```js +export const tools = (sdk) => [ + { + name: "sample_ton_balance", + description: "Read the configured wallet TON balance", + scope: "admin-only", + category: "data-bearing", + parameters: { type: "object", properties: {} }, + async execute() { + return { success: true, data: await sdk.ton.getBalance() }; + }, + }, + { + name: "sample_ton_send", + description: "Send a bounded TON transfer after explicit owner approval", + scope: "admin-only", + category: "action", + parameters: { + type: "object", + properties: { + to: { type: "string", minLength: 48, maxLength: 80 }, + amount: { type: "number", exclusiveMinimum: 0, maximum: 10 }, + }, + required: ["to", "amount"], + }, + async execute({ to, amount }) { + return { success: true, data: await sdk.ton.sendTON(to, amount) }; + }, + }, +]; ``` -## Media Handling +Never read or derive the mnemonic. If the public TON SDK cannot express the required transaction, +the plugin is not SDK v2 compatible yet. -```javascript -// Send photo with caption -await sdk.telegram.sendPhoto(context.chatId, "/path/to/image.jpg", { - caption: "Check this out!", -}); +## Lifecycle -// Send file -await sdk.telegram.sendFile(context.chatId, "/path/to/document.pdf", { - caption: "Here's the report", -}); +```js +let timer; -// Download media from a message -const buffer = await sdk.telegram.downloadMedia(context.chatId, messageId); -if (buffer) { - // Process the buffer... +export async function start(ctx) { + timer = setInterval(() => ctx.log.debug("background tick"), 60_000); + timer.unref?.(); } -// Send typing indicator -await sdk.telegram.setTyping(context.chatId); -``` - -## Error Handling Patterns - -### Standard pattern (all tools) - -```javascript -execute: async (params, context) => { - try { - const result = await doSomething(params); - return { success: true, data: result }; - } catch (err) { - return { success: false, error: String(err.message || err).slice(0, 500) }; - } +export async function stop() { + if (timer) clearInterval(timer); + timer = undefined; } ``` -### SDK write methods (ton.sendTON, telegram.sendMessage, etc.) - -```javascript -execute: async (params, context) => { - try { - await sdk.ton.sendTON(params.address, params.amount); - return { success: true, data: { sent: true } }; - } catch (err) { - if (err.name === "PluginSDKError") { - // Known error codes: WALLET_NOT_INITIALIZED, INVALID_ADDRESS, - // BRIDGE_NOT_CONNECTED, SECRET_NOT_FOUND, OPERATION_FAILED - return { success: false, error: `${err.code}: ${err.message}` }; - } - return { success: false, error: String(err.message || err).slice(0, 500) }; - } -} -``` +`start(ctx)` receives `sdk`, isolated `db`, sanitized `config`, `pluginConfig`, and `log`. It does not +receive a raw Telegram bridge. -### SDK read methods (getBalance, getMessages, etc.) +## Anti-patterns -Read methods **never throw** โ€” they return `null` or `[]`: +Do not: -```javascript -const balance = await sdk.ton.getBalance(); -if (!balance) { - return { success: false, error: "Could not fetch balance" }; -} -return { success: true, data: { balance: balance.balance } }; +```js +sdk.telegram.getRawClient(); +context.bridge.getClient(); +ctx.bridge.sendMessage(); +readFileSync("~/.teleton/wallet.json"); +process.env.UNRELATED_GLOBAL_SECRET; ``` -## Anti-Patterns - -**Do NOT:** - -```javascript -// WRONG: || for defaults (fails on 0, "", false) -const limit = params.limit || 10; -// CORRECT: ?? (nullish coalescing) -const limit = params.limit ?? 10; - -// WRONG: fetch without timeout -const res = await fetch(url); -// CORRECT: always use AbortSignal.timeout -const res = await fetch(url, { signal: AbortSignal.timeout(15_000) }); - -// WRONG: import GramJS as ESM -import { Api } from "telegram"; -// CORRECT: createRequire -const { Api } = _require("telegram"); - -// WRONG: module.exports (CJS) -module.exports = { tools }; -// CORRECT: ESM exports -export const tools = [...]; - -// WRONG: context.bridge when SDK available -await context.bridge.sendMessage(chatId, text); -// CORRECT: prefer SDK -await sdk.telegram.sendMessage(chatId, text); - -// WRONG: context.db (shared, no isolation) -context.db.prepare("INSERT INTO ...").run(); -// CORRECT: sdk.db (isolated per plugin) -sdk.db.prepare("INSERT INTO ...").run(); - -// WRONG: unsliced error messages (can flood LLM context) -return { success: false, error: err.message }; -// CORRECT: slice to 500 chars -return { success: false, error: String(err.message || err).slice(0, 500) }; - -// WRONG: using sdk.bot without declaring bot in manifest -sdk.bot.onInlineQuery(...); // sdk.bot is null! -// CORRECT: declare bot capabilities in manifest first -export const manifest = { bot: { inline: true, callbacks: true } }; -``` +Use public capabilities, declared secrets and isolated state. If a safe capability is unavailable, +quarantine the plugin instead of bypassing the SDK boundary. diff --git a/registry.json b/registry.json index c8b8ac6..515fbc4 100644 --- a/registry.json +++ b/registry.json @@ -1,189 +1,313 @@ { - "version": "1.0.0", + "version": "2.1.0", "plugins": [ { - "id": "giftstat", - "name": "Giftstat Market Data", - "description": "Telegram gift market data -- collections, floor prices, models, stats, history", + "id": "boards", + "name": "boards.ton Forum", + "description": "Browse and participate in the boards.ton decentralized forum using x402 TON micropayments", "author": "teleton", - "tags": ["market-data", "ton", "gifts", "trading", "api"], - "path": "plugins/giftstat" + "tags": [ + "forum", + "discussion", + "ton", + "decentralized", + "x402", + "boards" + ], + "path": "plugins/boards" }, { - "id": "gaspump", - "name": "Gas111 Token Launcher", - "description": "Launch, trade, and manage meme tokens on Gas111/TON", + "id": "casino", + "name": "Teleton Casino", + "description": "Slot machine and dice games with TON payments and auto-payout", "author": "teleton", - "tags": ["token-launch", "ton", "meme-tokens", "trading", "api"], - "path": "plugins/gaspump" + "tags": [ + "casino", + "games", + "ton", + "gambling" + ], + "path": "plugins/casino" }, { - "id": "pic", - "name": "@pic โ€” Inline Image Search", - "description": "Search and send images via Telegram's @pic inline bot (Yandex Image Search)", - "author": "teleton", - "tags": ["images", "search", "inline-bot"], - "path": "plugins/pic" + "id": "crypto-prices", + "name": "Crypto Prices", + "description": "Real-time cryptocurrency prices and comparison via CryptoCompare API", + "author": "walged", + "tags": [ + "crypto", + "prices", + "utility" + ], + "path": "plugins/crypto-prices" }, { - "id": "vid", - "name": "@vid โ€” Inline YouTube Search", - "description": "Search and send YouTube videos via Telegram's @vid inline bot", + "id": "dedust", + "name": "DeDust DEX", + "description": "Swap tokens, browse pools, and trade on DeDust -- TON's #2 DEX", "author": "teleton", - "tags": ["videos", "search", "inline-bot"], - "path": "plugins/vid" + "tags": [ + "defi", + "ton", + "dex", + "swap", + "liquidity" + ], + "path": "plugins/dedust" }, { "id": "deezer", "name": "@DeezerMusicBot โ€” Inline Music Search", "description": "Search and send music tracks via Telegram's @DeezerMusicBot inline bot (Deezer)", "author": "teleton", - "tags": ["music", "deezer", "search", "inline-bot"], + "tags": [ + "music", + "deezer", + "search", + "inline-bot" + ], "path": "plugins/deezer" }, { - "id": "giftindex", - "name": "GiftIndex ODROB Trading", - "description": "GiftIndex ODROB trading -- monitor, analyze and trade the Telegram Gifts index on TON", - "author": "teleton", - "tags": ["trading", "ton", "gifts", "defi"], - "path": "plugins/giftindex" - }, - { - "id": "stormtrade", - "name": "Storm Trade Perpetual Futures", - "description": "Trade perpetual futures on Storm Trade DEX โ€” crypto, stocks, forex, commodities", - "author": "teleton", - "tags": ["trading", "ton", "defi", "perpetual-futures", "derivatives"], - "path": "plugins/stormtrade" - }, - { - "id": "swapcoffee", - "name": "swap.coffee DEX Aggregator", - "description": "Swap tokens on TON via swap.coffee aggregator โ€” best rates across all DEXes", - "author": "teleton", - "tags": ["defi", "ton", "dex", "swap", "trading", "aggregator"], - "path": "plugins/swapcoffee" - }, - { - "id": "dedust", - "name": "DeDust DEX", - "description": "Swap tokens, browse pools, and trade on DeDust โ€” TON's #2 DEX", + "id": "dyor", + "name": "DYOR Token Analytics", + "description": "TON token analytics from DYOR.io -- search, price, trust score, metrics, DEX trades, holders, pools", "author": "teleton", - "tags": ["defi", "ton", "dex", "swap", "liquidity"], - "path": "plugins/dedust" + "tags": [ + "analytics", + "ton", + "market-data", + "trust-score", + "defi", + "dex" + ], + "path": "plugins/dyor" }, { - "id": "stonfi", - "name": "StonFi DEX", - "description": "Swap tokens, browse pools, and farm on StonFi DEX โ€” the largest DEX on TON", + "id": "fragment", + "name": "Fragment Marketplace", + "description": "Search and browse Telegram's NFT marketplace โ€” usernames, numbers, collectible gifts, auction history", "author": "teleton", - "tags": ["defi", "ton", "dex", "swap", "liquidity", "farming"], - "path": "plugins/stonfi" + "tags": [ + "marketplace", + "telegram", + "nft", + "ton", + "usernames", + "collectibles" + ], + "path": "plugins/fragment" }, { "id": "geckoterminal", "name": "GeckoTerminal", "description": "TON DEX pool and token data -- trending, new, and top pools, trades, OHLCV, token info, batch prices", "author": "teleton", - "tags": ["market-data", "dex", "analytics", "ton", "trading", "defi"], + "tags": [ + "market-data", + "dex", + "analytics", + "ton", + "trading", + "defi" + ], "path": "plugins/geckoterminal" }, { - "id": "dyor", - "name": "DYOR Token Analytics", - "description": "TON token analytics from DYOR.io -- search, price, trust score, metrics, DEX trades, holders, pools", + "id": "giftindex", + "name": "GiftIndex ODROB Trading", + "description": "GiftIndex ODROB trading with workflow guardrails - monitor, analyze and trade the Telegram Gifts index on TON. Owner-only, corridor-enforced, post-trade verified.", "author": "teleton", - "tags": ["analytics", "ton", "market-data", "trust-score", "defi", "dex"], - "path": "plugins/dyor" + "tags": [ + "giftindex", + "ton", + "trading", + "telegram-gifts", + "index" + ], + "path": "plugins/giftindex" }, { - "id": "tonapi", - "name": "TONAPI Explorer", - "description": "TON blockchain data from TONAPI -- accounts, jettons, NFTs, prices, transactions, traces, DNS, staking", + "id": "giftstat", + "name": "Giftstat Market Data", + "description": "Telegram gift market data -- collections, floor prices, models, stats, history", "author": "teleton", - "tags": ["blockchain", "ton", "explorer", "nft", "staking", "dns", "analytics"], - "path": "plugins/tonapi" + "tags": [ + "market-data", + "ton", + "gifts", + "trading", + "api" + ], + "path": "plugins/giftstat" }, { - "id": "evaa", - "name": "EVAA Protocol", - "description": "Lending and borrowing on TON -- supply, borrow, withdraw, repay, liquidate across 4 pools", + "id": "multisend", + "name": "Multisend", + "description": "Batch send TON and jettons to up to 254 recipients through Teleton's protected Highload Wallet v3 broker", "author": "teleton", - "tags": ["defi", "ton", "lending", "borrowing", "liquidation"], - "path": "plugins/evaa" + "tags": [ + "batch", + "ton", + "airdrop", + "wallet", + "transfer" + ], + "path": "plugins/multisend" }, { - "id": "weather", - "name": "Weather Forecast", - "description": "Current weather and 7-day forecast via Open-Meteo API", - "author": "walged", - "tags": ["weather", "utility"], - "path": "plugins/weather" + "id": "pic", + "name": "@pic โ€” Inline Image Search", + "description": "Search and send images via Telegram's @pic inline bot (Yandex Image Search)", + "author": "teleton", + "tags": [ + "images", + "search", + "inline-bot", + "yandex" + ], + "path": "plugins/pic" }, { - "id": "crypto-prices", - "name": "Crypto Prices", - "description": "Real-time cryptocurrency prices and comparison via CryptoCompare API", - "author": "walged", - "tags": ["crypto", "prices", "utility"], - "path": "plugins/crypto-prices" + "id": "sbt", + "name": "TON SBT", + "description": "Deploy and mint Soulbound Tokens (TEP-85) on TON", + "author": "teleton", + "tags": [ + "ton", + "nft", + "sbt" + ], + "path": "plugins/sbt" }, { - "id": "voice-notes", - "name": "Voice Notes Transcription", - "description": "Transcribe voice messages using Telegram Premium speech-to-text", - "author": "walged", - "tags": ["telegram", "voice", "transcription"], - "path": "plugins/voice-notes" + "id": "stonfi", + "name": "StonFi DEX", + "description": "Swap tokens, browse pools, and farm on StonFi DEX -- the largest DEX on TON", + "author": "teleton", + "tags": [ + "defi", + "ton", + "dex", + "swap", + "liquidity", + "farming" + ], + "path": "plugins/stonfi" }, { - "id": "fragment", - "name": "Fragment Marketplace", - "description": "Search and browse Telegram's NFT marketplace โ€” usernames, numbers, collectible gifts, auction history", + "id": "stormtrade", + "name": "Storm Trade Perpetual Futures", + "description": "Trade perpetual futures on Storm Trade DEX โ€” crypto, stocks, forex, commodities", "author": "teleton", - "tags": ["marketplace", "telegram", "nft", "ton", "usernames", "collectibles"], - "path": "plugins/fragment" + "tags": [ + "trading", + "ton", + "defi", + "perpetual-futures", + "derivatives" + ], + "path": "plugins/stormtrade" }, { - "id": "sbt", - "name": "TON SBT", - "description": "Deploy and mint Soulbound Tokens (TEP-85) on TON", + "id": "swapcoffee", + "name": "swap.coffee DEX Aggregator", + "description": "Swap tokens on TON via swap.coffee aggregator - best rates across all DEXes", "author": "teleton", - "tags": ["ton", "nft", "sbt"], - "path": "plugins/sbt" + "tags": [ + "defi", + "ton", + "dex", + "swap", + "trading", + "aggregator" + ], + "path": "plugins/swapcoffee" }, { - "id": "multisend", - "name": "Multisend", - "description": "Batch send TON and jettons to up to 254 recipients in a single transaction", + "id": "tonapi", + "name": "TONAPI Explorer", + "description": "TON blockchain data from TONAPI -- accounts, jettons, NFTs, prices, transactions, traces, DNS, staking", "author": "teleton", - "tags": ["batch", "ton", "airdrop", "wallet", "transfer"], - "path": "plugins/multisend" + "tags": [ + "blockchain", + "ton", + "explorer", + "nft", + "staking", + "dns", + "analytics" + ], + "path": "plugins/tonapi" }, { "id": "twitter", "name": "Twitter/X", "description": "X/Twitter API v2 โ€” read (search, lookup, trends) + write (post, like, retweet, follow) with OAuth 1.0a", "author": "teleton", - "tags": ["social", "twitter", "x", "search", "trends"], + "tags": [ + "social", + "twitter", + "x", + "search", + "trends", + "oauth" + ], "path": "plugins/twitter" }, { - "id": "casino", - "name": "Teleton Casino", - "description": "Slot machine and dice games with TON payments and auto-payout", + "id": "uranus", + "name": "Uranus Meme Launchpad", + "description": "Inspect, quote, trade and launch Uranus Meme tokens on TON through the protected Teleton SDK transaction broker.", "author": "teleton", - "tags": ["casino", "games", "ton", "gambling"], - "path": "plugins/casino" + "tags": [ + "ton", + "defi", + "meme", + "launchpad", + "bonding-curve", + "trade" + ], + "path": "plugins/uranus" }, { - "id": "boards", - "name": "boards.ton Forum", - "description": "Browse and participate in the boards.ton decentralized forum using x402 TON micropayments", + "id": "vid", + "name": "@vid โ€” Inline YouTube Search", + "description": "Search and send YouTube videos via Telegram's @vid inline bot", "author": "teleton", - "tags": ["forum", "discussion", "ton", "decentralized", "x402", "boards"], - "path": "plugins/boards" + "tags": [ + "videos", + "youtube", + "search", + "inline-bot" + ], + "path": "plugins/vid" + }, + { + "id": "weather", + "name": "Weather Forecast", + "description": "Current weather and 7-day forecast via Open-Meteo API", + "author": "walged", + "tags": [ + "weather", + "utility" + ], + "path": "plugins/weather" + }, + { + "id": "webdom", + "name": "Webdom Domain Marketplace", + "description": "Buy, sell, auction, and manage .ton domains and Telegram usernames on webdom.market", + "author": "teleton", + "tags": [ + "marketplace", + "domains", + "ton", + "dns", + "nft", + "auction" + ], + "path": "plugins/webdom" } ] } diff --git a/scripts/audit-plugin-deps.mjs b/scripts/audit-plugin-deps.mjs new file mode 100644 index 0000000..7c2adc9 --- /dev/null +++ b/scripts/audit-plugin-deps.mjs @@ -0,0 +1,48 @@ +import { existsSync, readdirSync } from "node:fs"; +import { join, resolve } from "node:path"; +import { spawnSync } from "node:child_process"; + +const pluginsDir = resolve("plugins"); +const packageDirs = readdirSync(pluginsDir) + .map((name) => join(pluginsDir, name)) + .filter((dir) => existsSync(join(dir, "package.json"))) + .sort(); + +let failed = false; +for (const dir of packageDirs) { + const name = dir.split("/").at(-1); + const result = spawnSync("npm", ["audit", "--omit=dev", "--json"], { + cwd: dir, + encoding: "utf8", + }); + + let audit; + try { + audit = JSON.parse(result.stdout); + } catch { + console.error(`ERROR: ${name}: npm audit did not return JSON`); + failed = true; + continue; + } + + if (!audit.metadata?.vulnerabilities || typeof audit.metadata.vulnerabilities.total !== "number") { + const detail = audit.error?.summary ?? audit.error?.detail ?? "missing vulnerability metadata"; + console.error(`ERROR: ${name}: npm audit failed: ${detail}`); + failed = true; + continue; + } + + const counts = audit.metadata?.vulnerabilities ?? {}; + console.log( + `${name}: critical=${counts.critical ?? 0} high=${counts.high ?? 0} ` + + `moderate=${counts.moderate ?? 0} low=${counts.low ?? 0}` + ); + if ((counts.critical ?? 0) > 0 || (counts.high ?? 0) > 0) failed = true; +} + +if (failed) { + console.error("ERROR: plugin dependency audit found HIGH or CRITICAL vulnerabilities"); + process.exit(1); +} + +console.log(`Dependency audit passed for ${packageDirs.length} plugins`); diff --git a/scripts/catalog-lib.mjs b/scripts/catalog-lib.mjs new file mode 100644 index 0000000..5d23b7e --- /dev/null +++ b/scripts/catalog-lib.mjs @@ -0,0 +1,162 @@ +import { existsSync, readFileSync, readdirSync } from "node:fs"; +import { join } from "node:path"; +import { satisfies } from "semver"; +import { pluginDirectories, readJson } from "./catalog-source.mjs"; + +export { pluginDirectories, readJson } from "./catalog-source.mjs"; + +const SEMVER_RE = /^\d+\.\d+\.\d+$/; +const PLUGIN_ID_RE = /^[a-z0-9][a-z0-9-]*$/; +const TOOL_NAME_RE = /^[a-z][a-z0-9_]{0,63}$/; +const SDK_RANGE_RE = /^\^\d+\.\d+\.\d+$/; + +export function validateCatalog(root = process.cwd()) { + const errors = []; + const compatibility = readJson(join(root, "compatibility.json")); + const directories = pluginDirectories(root); + const compatibilityIds = Object.keys(compatibility.plugins).sort(); + + if (compatibility.schemaVersion !== 1) errors.push("compatibility.schemaVersion must be 1"); + if (!SEMVER_RE.test(compatibility.targetSdkVersion)) { + errors.push("compatibility.targetSdkVersion must be strict semver"); + } + if (JSON.stringify(directories) !== JSON.stringify(compatibilityIds)) { + errors.push("compatibility.json must contain exactly one entry for every plugin directory"); + } + const manifestTools = new Map(); + const forbiddenSupportedPatterns = [ + ["sdk.telegram.getRawClient(", "removed sdk.telegram.getRawClient()"], + ["context.bridge", "removed context.bridge"], + ["ctx.bridge", "removed ctx.bridge"], + ["wallet.json", "direct wallet file access"], + [".mnemonic", "direct mnemonic access"], + ]; + + for (const id of directories) { + const pluginDir = join(root, "plugins", id); + const manifest = readJson(join(pluginDir, "manifest.json")); + const policy = compatibility.plugins[id]; + if (!policy || !["supported", "quarantined"].includes(policy.status)) { + errors.push(`${id}: compatibility status must be supported or quarantined`); + continue; + } + if (typeof policy.marketplace !== "boolean") { + errors.push(`${id}: compatibility marketplace flag must be boolean`); + } + if (policy.marketplace === true && policy.status !== "supported") { + errors.push(`${id}: only supported plugins may be listed in the marketplace`); + } + if (manifest.id !== id) errors.push(`${id}: manifest.id must match its directory`); + if (!PLUGIN_ID_RE.test(manifest.id ?? "")) errors.push(`${id}: invalid manifest.id`); + if (typeof manifest.name !== "string" || manifest.name.length === 0) { + errors.push(`${id}: manifest.name is required`); + } + if (!SEMVER_RE.test(manifest.version ?? "")) errors.push(`${id}: invalid manifest.version`); + if (typeof manifest.description !== "string" || manifest.description.length === 0) { + errors.push(`${id}: manifest.description is required`); + } + if (!manifest.author || typeof manifest.author !== "object") { + errors.push(`${id}: manifest.author must be an object`); + } + if (manifest.license !== "MIT") errors.push(`${id}: manifest.license must be MIT`); + if (manifest.teleton !== ">=0.9.0") errors.push(`${id}: manifest.teleton must be >=0.9.0`); + if (manifest.entry !== "index.js" || !existsSync(join(pluginDir, "index.js"))) { + errors.push(`${id}: manifest.entry must reference index.js`); + } + if (!existsSync(join(pluginDir, "README.md"))) errors.push(`${id}: README.md is required`); + if (!Array.isArray(manifest.permissions)) errors.push(`${id}: permissions must be an array`); + if (!Array.isArray(manifest.tags)) errors.push(`${id}: tags must be an array`); + if (existsSync(join(pluginDir, "package.json"))) { + if (!existsSync(join(pluginDir, "package-lock.json"))) { + errors.push(`${id}: package-lock.json is required with package.json`); + } + const packageJson = readJson(join(pluginDir, "package.json")); + if (packageJson.type !== "module") errors.push(`${id}: package.json type must be module`); + } + if (!Array.isArray(manifest.tools) || manifest.tools.length === 0) { + errors.push(`${id}: manifest.tools must be a non-empty array`); + continue; + } + + const expectedRange = policy.sdkVersion; + if (expectedRange === null) { + if (manifest.sdkVersion !== undefined) errors.push(`${id}: sdkVersion must be omitted`); + } else { + if (!SDK_RANGE_RE.test(expectedRange)) errors.push(`${id}: invalid compatibility sdkVersion`); + if (manifest.sdkVersion !== expectedRange) { + errors.push(`${id}: manifest.sdkVersion must be ${expectedRange}`); + } + } + + if ( + policy.status === "supported" && + expectedRange && + !satisfies(compatibility.targetSdkVersion, expectedRange) + ) { + errors.push( + `${id}: SDK range ${expectedRange} does not include target ${compatibility.targetSdkVersion}` + ); + } + if (policy.status === "quarantined") { + if (policy.marketplace !== false) errors.push(`${id}: quarantined plugins cannot be listed`); + if (expectedRange !== "^1.0.0") errors.push(`${id}: quarantined plugins must reject SDK v2`); + if (typeof policy.reason !== "string" || policy.reason.length < 20) { + errors.push(`${id}: quarantined plugins require a concrete reason`); + } + const readme = readFileSync(join(pluginDir, "README.md"), "utf8"); + if (!readme.includes("Legacy SDK v1 plugin") || !readme.includes("quarantined")) { + errors.push(`${id}: quarantined plugin README must carry the warning banner`); + } + } + + const localNames = new Set(); + for (const tool of manifest.tools) { + if (!TOOL_NAME_RE.test(tool?.name ?? "")) errors.push(`${id}: invalid tool name`); + if (typeof tool?.description !== "string" || tool.description.length === 0) { + errors.push(`${id}/${tool?.name ?? "?"}: missing description`); + } + if (localNames.has(tool.name)) errors.push(`${id}: duplicate tool ${tool.name}`); + localNames.add(tool.name); + const owner = manifestTools.get(tool.name); + if (owner) errors.push(`${id}: tool ${tool.name} duplicates ${owner}`); + else manifestTools.set(tool.name, id); + } + + if (policy.status === "supported") { + const sourceFiles = listJavaScriptFiles(pluginDir); + for (const sourceFile of sourceFiles) { + const source = readFileSync(sourceFile, "utf8"); + for (const [needle, label] of forbiddenSupportedPatterns) { + if (source.includes(needle)) errors.push(`${id}: ${label} in ${sourceFile.slice(root.length + 1)}`); + } + } + } + } + + const expectedRegistryIds = directories.filter( + (id) => compatibility.plugins[id]?.marketplace === true + ); + + return { + errors, + pluginCount: directories.length, + toolCount: manifestTools.size, + marketplaceCount: expectedRegistryIds.length, + supportedCount: directories.filter((id) => compatibility.plugins[id].status === "supported") + .length, + quarantinedCount: directories.filter( + (id) => compatibility.plugins[id].status === "quarantined" + ).length, + }; +} + +function listJavaScriptFiles(dir) { + const files = []; + for (const entry of readdirSync(dir, { withFileTypes: true })) { + if (entry.name === "node_modules") continue; + const path = join(dir, entry.name); + if (entry.isDirectory()) files.push(...listJavaScriptFiles(path)); + else if (entry.isFile() && entry.name.endsWith(".js")) files.push(path); + } + return files; +} diff --git a/scripts/catalog-source.mjs b/scripts/catalog-source.mjs new file mode 100644 index 0000000..da86f24 --- /dev/null +++ b/scripts/catalog-source.mjs @@ -0,0 +1,130 @@ +import { existsSync, readFileSync, readdirSync } from "node:fs"; +import { join } from "node:path"; + +const SECTION_PREFIX = "teleton-catalog"; + +export function readJson(path) { + return JSON.parse(readFileSync(path, "utf8")); +} + +export function pluginDirectories(root = process.cwd()) { + const pluginsDir = join(root, "plugins"); + return readdirSync(pluginsDir) + .filter((name) => existsSync(join(pluginsDir, name, "manifest.json"))) + .sort(); +} + +export function readCatalog(root = process.cwd()) { + const compatibility = readJson(join(root, "compatibility.json")); + const directories = pluginDirectories(root); + const manifests = new Map( + directories.map((id) => [id, readJson(join(root, "plugins", id, "manifest.json"))]) + ); + return { compatibility, directories, manifests }; +} + +export function buildRegistry(root = process.cwd()) { + const { compatibility, directories, manifests } = readCatalog(root); + return { + version: compatibility.targetSdkVersion, + plugins: directories + .filter((id) => compatibility.plugins[id]?.marketplace === true) + .map((id) => { + const manifest = manifests.get(id); + return { + id, + name: manifest.name ?? "", + description: manifest.description ?? "", + author: manifest.author?.name ?? "", + tags: Array.isArray(manifest.tags) ? manifest.tags : [], + path: `plugins/${id}`, + }; + }), + }; +} + +export function serializeRegistry(root = process.cwd()) { + return `${JSON.stringify(buildRegistry(root), null, 2)}\n`; +} + +export function renderReadme(root = process.cwd(), source = readFileSync(join(root, "README.md"), "utf8")) { + const { compatibility, directories, manifests } = readCatalog(root); + const marketplace = directories.filter((id) => compatibility.plugins[id]?.marketplace === true); + const supported = directories.filter((id) => compatibility.plugins[id]?.status === "supported"); + const quarantined = directories.filter((id) => compatibility.plugins[id]?.status === "quarantined"); + const sdkMajor = compatibility.targetSdkVersion.split(".")[0]; + const countTools = (id) => { + const tools = manifests.get(id).tools; + return Array.isArray(tools) ? tools.length : 0; + }; + const toolCount = directories.reduce((total, id) => total + countTools(id), 0); + const exampleCount = supported.length - marketplace.length; + + const badges = [ + `[![SDK](https://img.shields.io/badge/SDK-v${sdkMajor}-00C896.svg)](https://www.npmjs.com/package/@teleton-agent/sdk)`, + `[![Marketplace](https://img.shields.io/badge/marketplace-${marketplace.length}-8B5CF6.svg)](#sdk-v${sdkMajor}-marketplace)`, + `[![Catalog](https://img.shields.io/badge/catalog-${directories.length}_plugins-E040FB.svg)](compatibility.json)`, + ].join("\n"); + + const summary = [ + "| Status | Plugins | Meaning |", + "|---|---:|---|", + `| SDK v${sdkMajor} supported | ${supported.length} | Loads against SDK v${sdkMajor}; ${marketplace.length} marketplace plugins plus ${exampleCount} examples |`, + `| Quarantined | ${quarantined.length} | Preserved in source, rejected by SDK v2 and excluded from the marketplace |`, + `| Total | ${directories.length} | ${toolCount} tools |`, + ].join("\n"); + + const marketplaceTable = [ + `## SDK v${sdkMajor} marketplace`, + "", + "| Plugin | Tools | Description |", + "|---|---:|---|", + ...marketplace.map((id) => { + const manifest = manifests.get(id); + return `| [${escapeMarkdown(id)}](plugins/${id}/) | ${countTools(id)} | ${escapeMarkdown(manifest.description ?? "")} |`; + }), + ].join("\n"); + + const quarantineTable = [ + "| Plugin | Blocker |", + "|---|---|", + ...quarantined.map( + (id) => `| \`${escapeMarkdown(id)}\` | ${escapeMarkdown(compatibility.plugins[id]?.reason ?? "")} |` + ), + ].join("\n"); + + return replaceSection( + replaceSection( + replaceSection(replaceSection(source, "badges", badges), "summary", summary), + "marketplace", + marketplaceTable + ), + "quarantine", + quarantineTable + ); +} + +export function generatedArtifacts(root = process.cwd()) { + return new Map([ + [join(root, "registry.json"), serializeRegistry(root)], + [join(root, "README.md"), renderReadme(root)], + ]); +} + +function replaceSection(source, name, body) { + const start = ``; + const end = ``; + const startIndex = source.indexOf(start); + const endIndex = source.indexOf(end); + const duplicateStart = startIndex !== -1 && source.indexOf(start, startIndex + start.length) !== -1; + const duplicateEnd = endIndex !== -1 && source.indexOf(end, endIndex + end.length) !== -1; + if (startIndex === -1 || endIndex === -1 || endIndex < startIndex || duplicateStart || duplicateEnd) { + throw new Error(`README generated section is missing or malformed: ${name}`); + } + const contentStart = startIndex + start.length; + return `${source.slice(0, contentStart)}\n${body}\n${source.slice(endIndex)}`; +} + +function escapeMarkdown(value) { + return String(value).replaceAll("|", "\\|").replaceAll(/\s+/g, " ").trim(); +} diff --git a/scripts/generate-catalog.mjs b/scripts/generate-catalog.mjs new file mode 100644 index 0000000..a5969da --- /dev/null +++ b/scripts/generate-catalog.mjs @@ -0,0 +1,25 @@ +import { readFileSync, writeFileSync } from "node:fs"; +import { relative } from "node:path"; +import { generatedArtifacts } from "./catalog-source.mjs"; + +const check = process.argv.includes("--check"); +const root = process.cwd(); +const stale = []; + +for (const [path, expected] of generatedArtifacts(root)) { + const current = readFileSync(path, "utf8"); + if (current === expected) continue; + if (check) stale.push(relative(root, path)); + else { + writeFileSync(path, expected, "utf8"); + console.log(`Generated ${relative(root, path)}`); + } +} + +if (stale.length > 0) { + console.error(`Generated catalog files are stale: ${stale.join(", ")}`); + console.error("Run: npm run generate"); + process.exit(1); +} + +if (check) console.log("Generated catalog files are up to date"); diff --git a/scripts/install-plugin-deps.mjs b/scripts/install-plugin-deps.mjs new file mode 100644 index 0000000..4f06f5b --- /dev/null +++ b/scripts/install-plugin-deps.mjs @@ -0,0 +1,32 @@ +import { existsSync, readdirSync } from "node:fs"; +import { join, resolve } from "node:path"; +import { spawnSync } from "node:child_process"; + +const pluginsDir = resolve("plugins"); +const packageDirs = readdirSync(pluginsDir) + .map((name) => join(pluginsDir, name)) + .filter((dir) => existsSync(join(dir, "package.json"))) + .sort(); + +for (const dir of packageDirs) { + if (!existsSync(join(dir, "package-lock.json"))) { + console.error(`ERROR: ${dir} has package.json but no package-lock.json`); + process.exitCode = 1; + continue; + } + + const name = dir.split("/").at(-1); + console.log(`Installing ${name} dependencies...`); + const result = spawnSync( + "npm", + ["ci", "--ignore-scripts", "--no-audit", "--no-fund"], + { + cwd: dir, + env: { ...process.env, NODE_ENV: "production" }, + stdio: "inherit", + } + ); + if (result.status !== 0) process.exitCode = 1; +} + +if (!process.exitCode) console.log(`Installed dependencies for ${packageDirs.length} plugins`); diff --git a/scripts/validate-catalog.mjs b/scripts/validate-catalog.mjs new file mode 100644 index 0000000..666f93a --- /dev/null +++ b/scripts/validate-catalog.mjs @@ -0,0 +1,13 @@ +import { validateCatalog } from "./catalog-lib.mjs"; + +const result = validateCatalog(); +if (result.errors.length > 0) { + for (const error of result.errors) console.error(`ERROR: ${error}`); + process.exit(1); +} + +console.log( + `Catalog valid: ${result.pluginCount} plugins, ${result.toolCount} tools, ` + + `${result.supportedCount} SDK v2 supported, ${result.quarantinedCount} quarantined, ` + + `${result.marketplaceCount} listed` +); diff --git a/scripts/validate-runtime.mjs b/scripts/validate-runtime.mjs new file mode 100644 index 0000000..590ec06 --- /dev/null +++ b/scripts/validate-runtime.mjs @@ -0,0 +1,155 @@ +import { existsSync, readFileSync, realpathSync } from "node:fs"; +import { resolve } from "node:path"; +import { pathToFileURL } from "node:url"; +import { pluginDirectories, readJson, validateCatalog } from "./catalog-lib.mjs"; + +const root = process.cwd(); +const agentDir = resolve(process.env.TELETON_AGENT_DIR ?? "../teleton-agent"); +const agentBin = resolve(agentDir, "bin/teleton.js"); +const sdkEntry = resolve(agentDir, "packages/sdk/dist/index.js"); + +if (!existsSync(agentBin)) { + console.error(`ERROR: Teleton Agent checkout not found at ${agentDir}`); + process.exit(1); +} +if (!existsSync(sdkEntry)) { + console.error(`ERROR: Teleton SDK build not found at ${sdkEntry}`); + console.error(`Run: npm --prefix ${agentDir} run build:sdk`); + process.exit(1); +} + +const catalog = validateCatalog(root); +if (catalog.errors.length > 0) { + for (const error of catalog.errors) console.error(`ERROR: ${error}`); + process.exit(1); +} + +const { + SDK_VERSION: sdkVersion, + TOOL_CATEGORIES: sdkToolCategories, + TOOL_SCOPES: sdkToolScopes, +} = await import(pathToFileURL(sdkEntry).href); +const policy = readJson(resolve(root, "compatibility.json")); +if (sdkVersion !== policy.targetSdkVersion) { + console.error( + `ERROR: compatibility target SDK ${policy.targetSdkVersion} does not match agent SDK ${sdkVersion}` + ); + process.exit(1); +} + +// Legacy plugins resolve host-provided TON dependencies relative to argv[1]. +process.argv[1] = realpathSync(agentBin); + +const callable = new Proxy(function noop() {}, { + get: (_target, property) => (property === "then" ? undefined : callable), + apply: () => callable, +}); +const sdk = { + ton: callable, + telegram: callable, + db: callable, + storage: callable, + secrets: { get: () => undefined, require: () => "test", has: () => false }, + log: { info() {}, warn() {}, error() {}, debug() {} }, + pluginConfig: {}, + config: {}, + bot: null, + on() {}, +}; + +const scopes = new Set(sdkToolScopes); +const categories = new Set(sdkToolCategories); +const globalToolNames = new Map(); +let toolCount = 0; +let dataToolCount = 0; +let actionToolCount = 0; +const errors = []; + +for (const id of pluginDirectories(root)) { + const manifest = readJson(resolve(root, "plugins", id, "manifest.json")); + let module; + try { + module = await import(pathToFileURL(resolve(root, "plugins", id, "index.js")).href); + } catch (error) { + errors.push(`${id}: import failed: ${error instanceof Error ? error.message : String(error)}`); + continue; + } + + if (manifest.sdkVersion) { + if (module.manifest?.name !== id) errors.push(`${id}: runtime manifest name must match ID`); + if (module.manifest?.version !== manifest.version) { + errors.push(`${id}: runtime and disk versions differ`); + } + if (module.manifest?.sdkVersion !== manifest.sdkVersion) { + errors.push(`${id}: runtime and disk sdkVersion differ`); + } + } + + let tools; + try { + tools = typeof module.tools === "function" ? module.tools(sdk) : module.tools; + } catch (error) { + errors.push( + `${id}: tools initialization failed: ${error instanceof Error ? error.message : String(error)}` + ); + continue; + } + if (!Array.isArray(tools)) { + errors.push(`${id}: tools export must be an array or return one`); + continue; + } + + const runtimeNames = []; + for (const tool of tools) { + if (!tool || typeof tool !== "object") { + errors.push(`${id}: non-object tool`); + continue; + } + if (!/^[a-z][a-z0-9_]{0,63}$/.test(tool.name ?? "")) { + errors.push(`${id}: invalid runtime tool name ${String(tool.name)}`); + } + if ( + typeof tool.description !== "string" || + tool.description.length === 0 || + tool.description.length > 1024 + ) { + errors.push(`${id}/${tool.name}: invalid runtime description`); + } + if (typeof tool.execute !== "function") errors.push(`${id}/${tool.name}: missing execute()`); + if ( + tool.parameters !== undefined && + (!tool.parameters || typeof tool.parameters !== "object" || Array.isArray(tool.parameters)) + ) { + errors.push(`${id}/${tool.name}: invalid parameters schema`); + } + if (!scopes.has(tool.scope)) errors.push(`${id}/${tool.name}: invalid or missing scope`); + if (!categories.has(tool.category)) errors.push(`${id}/${tool.name}: invalid or missing category`); + if (tool.requiresApproval !== undefined && typeof tool.requiresApproval !== "boolean") { + errors.push(`${id}/${tool.name}: requiresApproval must be boolean`); + } + + const owner = globalToolNames.get(tool.name); + if (owner) errors.push(`${id}/${tool.name}: duplicates ${owner}`); + else globalToolNames.set(tool.name, id); + runtimeNames.push(tool.name); + toolCount++; + if (tool.category === "data-bearing") dataToolCount++; + else actionToolCount++; + } + + const manifestNames = manifest.tools.map((tool) => tool.name).sort(); + runtimeNames.sort(); + if (JSON.stringify(manifestNames) !== JSON.stringify(runtimeNames)) { + errors.push(`${id}: runtime tools and manifest tools differ`); + } +} + +if (errors.length > 0) { + for (const error of errors) console.error(`ERROR: ${error}`); + process.exit(1); +} + +console.log( + `Runtime validation passed against SDK ${sdkVersion}: ${catalog.pluginCount} plugins, ` + + `${toolCount} tools (${dataToolCount} data, ${actionToolCount} actions)` +); diff --git a/tests/catalog.test.mjs b/tests/catalog.test.mjs new file mode 100644 index 0000000..5fa3d99 --- /dev/null +++ b/tests/catalog.test.mjs @@ -0,0 +1,12 @@ +import assert from "node:assert/strict"; +import test from "node:test"; +import { validateCatalog } from "../scripts/catalog-lib.mjs"; + +test("catalog, manifests, compatibility policy and registry stay synchronized", () => { + const result = validateCatalog(); + assert.deepEqual(result.errors, []); + assert.equal(result.supportedCount + result.quarantinedCount, result.pluginCount); + assert.ok(result.pluginCount > 0); + assert.ok(result.toolCount > 0); + assert.ok(result.marketplaceCount <= result.supportedCount); +}); diff --git a/tests/sdk-v2-dex.test.mjs b/tests/sdk-v2-dex.test.mjs new file mode 100644 index 0000000..0e878ca --- /dev/null +++ b/tests/sdk-v2-dex.test.mjs @@ -0,0 +1,106 @@ +import assert from "node:assert/strict"; +import test from "node:test"; +import { tools as createDedustTools } from "../plugins/dedust/index.js"; +import { tools as createStonfiTools } from "../plugins/stonfi/index.js"; + +const log = { info() {}, warn() {}, error() {}, debug() {} }; +const storage = { get() {}, set() {} }; + +test("DeDust quote and swap use the SDK v2 transaction broker", async () => { + const calls = []; + const sdk = { + log, + storage, + ton: { + dex: { + async quoteDeDust(params) { + calls.push(["quote", params]); + return { + dex: "dedust", + expectedOutput: "2.500000", + minOutput: "2.375000", + rate: "0.250000", + fee: "0.010000", + poolType: "volatile", + }; + }, + async swapDeDust(params) { + calls.push(["swap", params]); + return { + dex: "dedust", + fromAsset: "ton", + toAsset: "EQJetton", + amountIn: "10", + expectedOutput: "2.500000", + minOutput: "2.375000", + slippage: "5.00%", + txRef: "dedust-hash", + }; + }, + }, + }, + }; + const tools = createDedustTools(sdk); + + const quote = await tools.find((tool) => tool.name === "dedust_swap_estimate").execute({ + input_token: "native", + output_token: "EQJetton", + input_amount: "10", + }); + const swap = await tools.find((tool) => tool.name === "dedust_swap").execute({ + input_token: "native", + output_token: "EQJetton", + input_amount: "10", + slippage: 0.05, + }); + + assert.deepEqual(calls, [ + ["quote", { fromAsset: "ton", toAsset: "EQJetton", amount: 10, slippage: 0.05 }], + ["swap", { fromAsset: "ton", toAsset: "EQJetton", amount: 10, slippage: 0.05 }], + ]); + assert.equal(quote.success, true); + assert.equal(quote.data.estimated_output, "2.500000"); + assert.equal(swap.success, true); + assert.equal(swap.data.tx_ref, "dedust-hash"); +}); + +test("STON.fi swap uses the SDK v2 transaction broker", async () => { + let received; + const sdk = { + log, + storage, + ton: { + dex: { + async swapSTONfi(params) { + received = params; + return { + dex: "stonfi", + fromAsset: "ton", + toAsset: "EQJetton", + amountIn: "3", + expectedOutput: "0.750000", + minOutput: "0.742500", + slippage: "1.00%", + txRef: "stonfi-hash", + }; + }, + }, + }, + }; + const tools = createStonfiTools(sdk); + const swap = await tools.find((tool) => tool.name === "stonfi_swap").execute({ + offer_address: "ton", + ask_address: "EQJetton", + amount: "3", + slippage: 0.01, + }); + + assert.deepEqual(received, { + fromAsset: "ton", + toAsset: "EQJetton", + amount: 3, + slippage: 0.01, + }); + assert.equal(swap.success, true); + assert.equal(swap.data.tx_ref, "stonfi-hash"); +}); diff --git a/tests/sdk-v2-migrations.test.mjs b/tests/sdk-v2-migrations.test.mjs new file mode 100644 index 0000000..83110c5 --- /dev/null +++ b/tests/sdk-v2-migrations.test.mjs @@ -0,0 +1,252 @@ +import assert from "node:assert/strict"; +import { createRequire } from "node:module"; +import { realpathSync } from "node:fs"; +import { resolve } from "node:path"; +import test from "node:test"; + +const agentDir = resolve(process.env.TELETON_AGENT_DIR ?? "../teleton-agent"); +process.argv[1] = realpathSync(resolve(agentDir, "bin/teleton.js")); + +const [{ tools: createDeezerTools }, { tools: createPicTools }, { tools: createVidTools }] = + await Promise.all([ + import("../plugins/deezer/index.js"), + import("../plugins/pic/index.js"), + import("../plugins/vid/index.js"), + ]); +const { tools: createMultisendTools } = await import("../plugins/multisend/index.js"); +const { tools: createSbtTools } = await import("../plugins/sbt/index.js"); +const { tools: createStormTools } = await import("../plugins/stormtrade/index.js"); +const { tools: createSwapCoffeeTools } = await import("../plugins/swapcoffee/index.js"); +const { actionTools: createWebdomActionTools } = await import("../plugins/webdom/tools/actions.js"); +const { initTrade, placeAskOrder } = await import("../plugins/giftindex/trade.js"); + +const ADDRESS_A = `0:${"1".repeat(64)}`; +const ADDRESS_B = `0:${"2".repeat(64)}`; +const log = { info() {}, warn() {}, error() {}, debug() {} }; +const { beginCell } = createRequire(process.argv[1])("@ton/core"); + +test("inline media plugins use the typed Telegram SDK capability", async () => { + const calls = []; + const sdk = { + telegram: { + async sendInlineBotResult(...args) { + calls.push(args); + return { + query: args[2], + sentIndex: args[3] ?? 0, + totalResults: 3, + title: "Result", + description: null, + type: "article", + }; + }, + }, + }; + + const cases = [ + [createDeezerTools, "DeezerMusicBot"], + [createPicTools, "pic"], + [createVidTools, "vid"], + ]; + for (const [factory, bot] of cases) { + const result = await factory(sdk)[0].execute({ query: "test", index: 1 }, { chatId: "42" }); + assert.equal(result.success, true); + assert.deepEqual(calls.at(-1), ["42", bot, "test", 1]); + } +}); + +test("multisend preserves the Highload wallet lifecycle through sdk.ton.highload", async () => { + const batches = []; + const highloadInfo = { + address: ADDRESS_A, + rawAddress: ADDRESS_A, + balance: "100", + balanceNano: "100000000000", + deployed: true, + currentQueryId: 4, + hasNext: true, + timeout: 86400, + subwalletId: 0x10ad, + }; + const sdk = { + log, + ton: { + validateAddress: () => true, + getAddress: () => ADDRESS_A, + getJettonWalletAddress: async () => ADDRESS_B, + highload: { + getInfo: async () => highloadInfo, + fund: async () => ({ hash: "fund-hash", seqno: 7 }), + sendMessages: async (messages) => { + batches.push(messages); + return { + address: ADDRESS_A, + queryId: 3 + batches.length, + nextQueryId: 4 + batches.length, + recipientCount: messages.length, + }; + }, + }, + }, + }; + const tools = createMultisendTools(sdk); + + assert.deepEqual( + (await tools.find((tool) => tool.name === "multisend_info").execute({})).data, + { + address: ADDRESS_A, + address_raw: ADDRESS_A, + balance: "100", + balance_nano: "100000000000", + deployed: true, + sequence: { lastQueryId: 4, savedQueryId: 4, hasNext: true }, + } + ); + assert.equal( + (await tools.find((tool) => tool.name === "multisend_fund").execute({ amount: "5" })).success, + true + ); + + const tonResult = await tools.find((tool) => tool.name === "multisend_batch_ton").execute({ + recipients: [{ address: ADDRESS_B, amount: "1.25", memo: "batch" }], + }); + assert.equal(tonResult.success, true); + assert.deepEqual(batches[0], [ + { to: ADDRESS_B, value: 1.25, body: "batch", bounce: false }, + ]); + + const jettonResult = await tools.find((tool) => tool.name === "multisend_batch_jetton").execute({ + jetton_master: ADDRESS_B, + decimals: 6, + recipients: [{ address: ADDRESS_B, amount: "12.345678" }], + }); + assert.equal(jettonResult.success, true); + assert.equal(batches[1][0].to, ADDRESS_B); + assert.equal(batches[1][0].value, 0.05); + assert.ok(batches[1][0].body); + assert.equal( + (await tools.find((tool) => tool.name === "multisend_status").execute({})).success, + true + ); +}); + +test("SBT deployment is sent through the protected TON broker", async () => { + let sent; + const sdk = { + log, + ton: { + getAddress: () => ADDRESS_A, + send: async (...args) => { + sent = args; + return { hash: "sbt-hash", seqno: 3 }; + }, + }, + }; + const deploy = createSbtTools(sdk).find((tool) => tool.name === "sbt_deploy_collection"); + const result = await deploy.execute({ + name: "Badges", + description: "Test badges", + image: "https://example.com/badge.png", + }); + + assert.equal(result.success, true); + assert.equal(sent[1], 0.05); + assert.equal(sent[2].bounce, false); + assert.ok(sent[2].stateInit.code); + assert.ok(sent[2].stateInit.data); +}); + +test("GiftIndex and Webdom writes no longer sign inside plugins", async () => { + const sends = []; + const ton = { + getAddress: () => ADDRESS_A, + getJettonWalletAddress: async () => ADDRESS_B, + send: async (...args) => { + sends.push(args); + return { hash: "broker-hash", seqno: 9 }; + }, + fromNano: (value) => (Number(value) / 1e9).toString(), + }; + initTrade({ log, ton }); + const gift = await placeAskOrder(ADDRESS_B, 1_000_000_000n, 10_000); + assert.equal(gift.seqno, 9); + assert.equal(sends[0][0], ADDRESS_B); + + const bid = createWebdomActionTools({ log, ton }).find( + (tool) => tool.name === "webdom_place_bid" + ); + const result = await bid.execute({ auction_address: ADDRESS_B, bid_ton: 2 }); + assert.equal(result.success, true); + assert.match(sends[1][0], /^EQ/); + assert.equal(sends[1][1], 2.11); +}); + +test("swap.coffee sends route transactions through sdk.ton.sendMessages", async () => { + const originalFetch = globalThis.fetch; + const responses = [ + { paths: [{ dex: "test" }], output_amount: 9, price_impact: 0.01 }, + { + route_id: 77, + transactions: [ + { + address: ADDRESS_B, + value: "100000000", + cell: beginCell().endCell().toBoc().toString("base64"), + }, + ], + }, + ]; + globalThis.fetch = async () => + new Response(JSON.stringify(responses.shift()), { + status: 200, + headers: { "content-type": "application/json" }, + }); + + try { + let messages; + const sdk = { + log, + ton: { + getAddress: () => ADDRESS_A, + fromNano: (value) => (Number(value) / 1e9).toString(), + sendMessages: async (value) => { + messages = value; + return { hash: "swap-hash", seqno: 11 }; + }, + }, + }; + const execute = createSwapCoffeeTools(sdk).find((tool) => tool.name === "swap_execute"); + const result = await execute.execute({ + input_token: "native", + output_token: ADDRESS_B, + input_amount: "1", + }); + + assert.equal(result.success, true); + assert.equal(result.data.seqno, 11); + assert.equal(messages[0].to, ADDRESS_B); + assert.equal(messages[0].value, 0.1); + } finally { + globalThis.fetch = originalFetch; + } +}); + +test("Storm read tools obtain the default trader from sdk.ton", async () => { + const originalFetch = globalThis.fetch; + let requestedUrl; + globalThis.fetch = async (url) => { + requestedUrl = String(url); + return new Response(JSON.stringify([{ position: "open" }]), { status: 200 }); + }; + try { + const sdk = { log, ton: { getAddress: () => ADDRESS_A } }; + const positions = createStormTools(sdk).find((tool) => tool.name === "storm_positions"); + const result = await positions.execute({}); + + assert.equal(result.success, true); + assert.equal(result.trader, ADDRESS_A); + assert.ok(requestedUrl.includes(encodeURIComponent(ADDRESS_A)) || requestedUrl.includes(ADDRESS_A)); + } finally { + globalThis.fetch = originalFetch; + } +}); diff --git a/tests/uranus-abi.test.mjs b/tests/uranus-abi.test.mjs new file mode 100644 index 0000000..8a42597 --- /dev/null +++ b/tests/uranus-abi.test.mjs @@ -0,0 +1,83 @@ +import assert from "node:assert/strict"; +import test from "node:test"; +import { Cell } from "../plugins/uranus/node_modules/@ton/core/dist/index.js"; +import { + buildBuy, buildClaimCreatorFee, buildClaimPartnerFee, buildDeployCustomizedMeme, buildDeployMeme, buildSellTokens, + decodeBuy, decodeClaim, decodeDeployCustomizedMeme, decodeInitMeme, decodeSellTokens, decodeTradeEvent, +} from "../plugins/uranus/abi.js"; +import { OPCODES } from "../plugins/uranus/constants.js"; + +const LIVE = { + buy: "te6cckEBAgEAYQABc5SCZVc67Aq5Y1ozO0stBeAHDV7bJg/nf4AKfRHTrlqjKhY56Yd1LyG0BzBJ8520mNcwHvmFs2m7dPQBAEShjmQstVt+A24+NNkEW58QpbiZjgcVZKzypMRzgSUBFAA8lckNNQ==", + sell: "te6cckEBAgEAYgABdbdFniw67Aq5jrYMJXHZ5ogW4dPFAhJ2SNaAHZZdG8WWCaTXMOZK5UZcusL/hyNT48jWSvREzbBGyQyUAQBEoY5kLLVbfgNuPjTZBFufEKW4mY4HFWSs8qTEc4ElARQAPLEb5yQ=", + custom: "te6cckEBAgEAiQABg2MvXRwAAAAAAAAAABVgJGE5yoAEuABmzJ8wJ8F/sm1iZmbF3rdSW/gWwCsjSsEV0ZrfC2AdaAAAAAAACigSoF8gAQEAhGlwZnM6Ly9iYWZrcmVpYWZzZGs0aWkzbW9wbGEzNjY0bGhlNG93am0zM2tzZnJpZWduZnMya2Jhejdra3h6YWhlYYHi4FQ=", + claim: "te6cckEBAQEAUQAAna1yaagAAAAAAAAAAIAdll0bxZYJpNcw5krlRly6wv+HI1PjyNZK9ETNsEbJDJADssujeLLBNJrmHMlcqMuXWF/w5Gp8eRrJXoiZtgjZIZKVtkGq", + init: "te6cckEBAQEAFAAAI3lvWgwAAAAAAAAAAFAlQL5AAt97yzU=", + buyEvent: "te6cckEBAQEATAAAk6Cqa8KAHZZdG8WWCaTXMOZK5UZcusL/hyNT48jWSvREzbBGyQyKBIVfKpDjs80QLcOniBvG5FqAbxuRYA47PNEC3Dp4oEhV8qkIDc92fQ==", + sellEvent: "te6cckEBAQEARQAAhTqw/MyAHZZdG8WWCaTXMOZK5UZcusL/hyNT48jWSvREzbBGyQyOOzzRAtw6eKBF0neSKBrPy56Aaz8uaAaz8ugAIDAGd0bY", +}; + +function sameCell(actual, encoded) { + assert.equal(encoded.hash().toString("hex"), actual.hash().toString("hex")); +} + +test("byte-exact live Buy and SellTokens fixtures re-encode", () => { + const buyCell = Cell.fromBase64(LIVE.buy); + const buy = decodeBuy(buyCell); + sameCell(buyCell, buildBuy({ ...buy, excessesTo: buy.excessesTo })); + assert.equal(buy.amount, 3_000_000_000n); + assert.equal(buy.partnerConfig.partnerFeeBps, 60); + + const sellCell = Cell.fromBase64(LIVE.sell); + const sell = decodeSellTokens(sellCell); + sameCell(sellCell, buildSellTokens({ ...sell, excessesTo: sell.excessesTo })); + assert.equal(sell.amount, 8_336_946_009_873_724n); +}); + +test("byte-exact successful mainnet customized deploy fixture re-encodes", () => { + const actual = Cell.fromBase64(LIVE.custom); + const decoded = decodeDeployCustomizedMeme(actual); + sameCell(actual, buildDeployCustomizedMeme(decoded)); + assert.equal(decoded.raisingFunds, 2_500_000_000_000n); + assert.equal(decoded.onSellSupplyPercent, 75); + assert.equal(decoded.metadataUri, "ipfs://bafkreiafsdk4ii3mopla3664lhe4owjm33ksfriegnfs2kbaz7kkxzahea"); +}); + +test("preset deploy matches the official generated-codec fixture", () => { + const expected = Cell.fromBase64("te6cckEBAgEAJgABI2/0FtwAAAAAAAAAe3QO5rKAIAEAHmlwZnM6Ly9iYWZ5dGVzdDR3ekk="); + const actual = buildDeployMeme({ queryId: 123n, presetId: 7, metadataUri: "ipfs://bafytest", initialBuy: 250_000_000n, partnerConfig: null, referrerConfig: null }); + sameCell(expected, actual); +}); + +test("claim and InitMeme live fixtures decode and claims re-encode", () => { + const claimCell = Cell.fromBase64(LIVE.claim); + const claim = decodeClaim(claimCell, OPCODES.CLAIM_CREATOR_FEE); + sameCell(claimCell, buildClaimCreatorFee(claim)); + const partner = buildClaimPartnerFee(claim); + assert.equal(partner.beginParse().loadUint(32), OPCODES.CLAIM_PARTNER_FEE); + + const init = decodeInitMeme(Cell.fromBase64(LIVE.init)); + assert.equal(init.queryId, 0n); + assert.equal(init.initialBuy, 10_000_000_000n); +}); + +test("live BuyEvent and SellEvent fixtures decode complete fee records", () => { + const buy = decodeTradeEvent(Cell.fromBase64(LIVE.buyEvent)); + assert.equal(buy.kind, "buy"); + assert.equal(buy.amountIn, 9_708_737_864n); + assert.deepEqual(buy.fees, { creatorFee: 233_009_709n, protocolFee: 58_252_427n, partnerFee: 0n, referrerFee: 0n }); + assert.equal(buy.isGraduated, false); + + const sell = decodeTradeEvent(Cell.fromBase64(LIVE.sellEvent)); + assert.equal(sell.kind, "sell"); + assert.equal(sell.raisedFunds, 1n); + assert.equal(sell.fees.partnerFee, 56_228_212n); +}); + +test("affiliate refs encode present and absent, malformed bodies fail closed", () => { + const absent = buildBuy({ queryId: 1n, amount: 1n, minimalAmountOut: 1n, excessesTo: "EQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAM9c", partnerConfig: null, referrerConfig: null }); + const present = buildBuy({ queryId: 1n, amount: 1n, minimalAmountOut: 1n, excessesTo: "EQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAM9c", partnerConfig: { partnerId: "2", partnerFeeBps: 50 }, referrerConfig: { referrerId: "3", referrerFeeBps: 25 } }); + assert.equal(decodeBuy(absent).partnerConfig, null); + assert.equal(decodeBuy(present).referrerConfig.referrerId, 3n); + assert.throws(() => decodeBuy(Cell.fromBase64("te6cckEBAQEABgAACAAAAAB7P4Q="))); +}); diff --git a/tests/uranus-quote.test.mjs b/tests/uranus-quote.test.mjs new file mode 100644 index 0000000..a558db6 --- /dev/null +++ b/tests/uranus-quote.test.mjs @@ -0,0 +1,48 @@ +import assert from "node:assert/strict"; +import test from "node:test"; +import { applySlippage, estimateBuyOut, estimateSellOut, quoteCurve, raiseAtSupply, supplyAtRaise } from "../plugins/uranus/quote.js"; + +const state = { + initialized: true, + migrated: false, + isGraduated: false, + alpha: 1_144_067_796_610_169_491n, + beta: 814_406_779_661n, + tradeFeeBps: 300, + raisedFunds: 23_926_336_271n, + currentSupply: 32_652_116_795_191_278n, +}; +const TON = 1_000_000_000n; + +test("curve half-saturation and inverse are exact within floor tolerance", () => { + assert.equal(supplyAtRaise(state.alpha, state.beta, state.beta), state.alpha / 2n); + const supply = supplyAtRaise(state.alpha, state.beta, state.raisedFunds); + const raise = raiseAtSupply(state.alpha, state.beta, supply); + assert.ok(state.raisedFunds - raise <= 1n); +}); + +test("verified tradingbot curve snapshot is monotonic and concave", () => { + const out5 = estimateBuyOut(state, 5n * TON); + const out10 = estimateBuyOut(state, 10n * TON); + assert.ok(out5 > 0n); + assert.ok(out10 > out5); + assert.ok(out10 < 2n * out5); + const sellBack = estimateSellOut(state, out5); + assert.ok(sellBack > 0n && sellBack < 5n * TON); +}); + +test("sell fee and slippage use exact integer floor division", () => { + const tokens = 1_000_000_000_000n; + const remaining = state.currentSupply - tokens; + const gross = state.raisedFunds - raiseAtSupply(state.alpha, state.beta, remaining); + assert.equal(estimateSellOut(state, tokens), (gross * 10_000n) / 10_300n); + assert.equal(applySlippage(1_234_567n, 500), (1_234_567n * 9_500n) / 10_000n); +}); + +test("quotes fail closed for invalid lifecycle, oversell and zero minimum", () => { + assert.throws(() => estimateBuyOut({ ...state, initialized: false }, TON), /not initialized/i); + assert.throws(() => estimateBuyOut({ ...state, isGraduated: true }, TON), /migration/i); + assert.throws(() => estimateBuyOut({ ...state, currentSupply: state.alpha }, TON), /asymptote/i); + assert.throws(() => estimateSellOut(state, state.currentSupply + 1n), /exceeds/i); + assert.throws(() => quoteCurve({ ...state, alpha: 2n, beta: 1n, raisedFunds: 0n, currentSupply: 0n }, "buy", 1n, 500), /too small/i); +}); diff --git a/tests/uranus-tools.test.mjs b/tests/uranus-tools.test.mjs new file mode 100644 index 0000000..501c0ae --- /dev/null +++ b/tests/uranus-tools.test.mjs @@ -0,0 +1,285 @@ +import assert from "node:assert/strict"; +import test from "node:test"; +import { readFile } from "node:fs/promises"; +import { Address, beginCell } from "../plugins/uranus/node_modules/@ton/core/dist/index.js"; +import { tools as createTools } from "../plugins/uranus/index.js"; +import { createActions } from "../plugins/uranus/actions.js"; +import { createHttp, assertPublicMetadataUrl, resolvePublicMetadataTarget } from "../plugins/uranus/http.js"; +import { createState } from "../plugins/uranus/state.js"; +import { createHistory } from "../plugins/uranus/history.js"; +import { buildDeployCustomizedMeme, buildDeployMeme, decodeBuy, decodeClaim, decodeSellTokens } from "../plugins/uranus/abi.js"; +import { ACTIVE_FACTORY, MEME_CODE_HASHES, OPCODES } from "../plugins/uranus/constants.js"; + +const ZERO = "EQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAM9c"; +const ONE = "EQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAd99"; +const MEME = "EQAm57u2jsX1XSzmYtXZLcg6-0F1DqTUJZgpm_fA-kqAVJ7d"; +const log = { info() {}, warn() {}, error() {}, debug() {} }; +const config = { maxActionTon: "50", defaultSlippageBps: 500, partnerId: null, partnerFeeBps: 0, referrerId: null, referrerFeeBps: 0, allowThirdPartyPartnerClaim: false }; + +function sdkBase(overrides = {}) { + return { + log, + pluginConfig: {}, + storage: { get() {}, set() {} }, + secrets: { get() {} }, + ton: { + getAddress: () => ZERO, + getBalance: async () => ({ balance: "100", balanceNano: "100000000000" }), + send: async () => ({ hash: "abc123", seqno: 1 }), + ...overrides, + }, + }; +} + +function curveInfo(extra = {}) { + return { + address: MEME, verified: true, version: "v3.1", writable_curve: true, migrated: false, + decimals: 9, creator_address: ZERO, creator_fee_nano: "1000000000", creator_fee_ton: "1", + partner_address: ZERO, partner_fee_nano: "2000000000", partner_fee_ton: "2", + ...extra, + }; +} + +test("runtime and disk manifests expose the same 13 tools and all actions are approval-gated", async () => { + const sdk = sdkBase({ dex: {} }); + const tools = createTools(sdk); + const disk = JSON.parse(await readFile(new URL("../plugins/uranus/manifest.json", import.meta.url))); + assert.equal(tools.length, 13); + assert.deepEqual(tools.map((tool) => tool.name).sort(), disk.tools.map((tool) => tool.name).sort()); + for (const tool of tools) { + assert.match(tool.name, /^uranus_/); + assert.equal(tool.parameters.type, "object"); + if (tool.category === "action") { + assert.equal(tool.scope, "admin-only"); + assert.equal(tool.requiresApproval, true); + } + } +}); + +test("documented Meme getter stack order maps to named public state", async () => { + const addressItem = (value) => ({ type: "cell", cell: beginCell().storeAddress(Address.parse(value)).endCell() }); + const integer = (value) => ({ type: "int", value: BigInt(value) }); + const methods = { + get_meme_data: [integer(-1), integer(0), addressItem(ONE), addressItem(ZERO), integer(7), integer(9), integer(0), integer(1_000), integer(100), integer(750), integer(300), integer(250), integer(200)], + get_jetton_data: [integer(1_000), integer(0), { type: "cell", cell: beginCell().storeAddress(null).endCell() }, { type: "cell", cell: beginCell().storeUint(0, 8).endCell() }, { type: "cell", cell: beginCell().endCell() }], + get_bonding_curve_data: [integer(0), integer(1_000), integer(200), integer(750), integer(250), integer(800), integer(50), integer(250), integer(1_000), integer(100)], + get_partner_data: [integer(33), integer(500), addressItem(ONE), integer(400)], + }; + const sdk = sdkBase({ + runGetMethod: async (_address, method) => ({ exitCode: 0, stack: methods[method] }), + getJettonInfo: async () => null, + }); + const codeHash = Buffer.from(MEME_CODE_HASHES["v3.1"], "hex").toString("base64"); + const http = { accountState: async () => ({ accounts: [{ address: Address.parse(MEME).toRawString(), status: "active", code_hash: codeHash }] }) }; + const info = await createState(sdk, http).memeInfo(MEME); + assert.equal(info.verified, true); + assert.equal(info.max_supply, "1000"); + assert.equal(info.bonding_supply, "750"); + assert.equal(info.liquidity_supply, "250"); + assert.equal(info.target_funds_nano, "800"); + assert.equal(info.migration_fee_nano, "50"); + assert.equal(info.partner_fee_nano, "33"); + assert.equal(info.partner_fee_bps, 500); + assert.equal(info.partner_address, ONE); + assert.equal(info.pool_partner_fee_bps, 400); +}); + +test("account identity fails closed when the response omits the requested address", async () => { + const codeHash = Buffer.from(MEME_CODE_HASHES["v3.1"], "hex").toString("base64"); + const http = { accountState: async () => ({ accounts: [{ address: Address.parse(ONE).toRawString(), status: "active", code_hash: codeHash }] }) }; + const identity = await createState(sdkBase(), http).inspectContract(MEME); + assert.equal(identity.account, null); + assert.equal(identity.codeHash, null); + assert.equal(identity.verified, false); +}); + +test("factory history decodes preset and custom launches and skips malformed transactions", async () => { + const init = (queryId, initialBuy) => beginCell().storeUint(OPCODES.INIT_MEME, 32).storeUint(queryId, 64).storeCoins(initialBuy).storeBit(false).storeBit(false).endCell(); + const simple = buildDeployMeme({ queryId: 11n, presetId: 3, metadataUri: "ipfs://simple", initialBuy: 1n, partnerConfig: null, referrerConfig: null }); + const custom = buildDeployCustomizedMeme({ queryId: 12n, totalSupplyPresetId: 1, baseFeePresetId: 3, raisingFunds: 1_000_000_000_000n, onSellSupplyPercent: 75, partnerAddress: ZERO, partnerFeeBps: 0, poolPartnerFeeBps: 0, poolBaseFeePresetId: 3, poolLiquidityOwnerAddress: null, metadataUri: "ipfs://custom", initialBuy: 2n, partnerConfig: null, referrerConfig: null }); + const message = (body, opcode, extra = {}) => ({ opcode, message_content: { body: body.toBoc().toString("base64") }, ...extra }); + const payload = { transactions: [ + { hash: Buffer.alloc(32, 1).toString("base64"), now: 2, in_msg: message(simple, "0x6ff416dc", { source: ZERO }), out_msgs: [message(init(11n, 1n), "0x796f5a0c", { destination: MEME })] }, + { hash: Buffer.alloc(32, 2).toString("base64"), now: 1, in_msg: message(custom, "0x632f5d1c", { source: ONE }), out_msgs: [message(init(12n, 2n), "0x796f5a0c", { destination: ONE })] }, + { hash: "bad", now: 3, in_msg: { opcode: "0x632f5d1c", message_content: { body: "not-a-boc" } }, out_msgs: [] }, + ] }; + const launches = await createHistory({ log }, { transactions: async () => payload }).recentLaunches({ limit: 10, factory_version: "v3.1" }); + assert.equal(launches.length, 2); + assert.deepEqual(launches.map((launch) => launch.kind), ["preset", "custom"]); + assert.deepEqual(launches.map((launch) => launch.query_id), ["11", "12"]); + assert.deepEqual(launches.map((launch) => launch.metadata_uri), ["ipfs://simple", "ipfs://custom"]); +}); + +test("counterfeit identity blocks address-based financial actions before send", async () => { + let sends = 0; + const sdk = sdkBase({ send: async () => { sends += 1; } }); + const deps = { + config, + state: { memeInfo: async () => curveInfo({ verified: false }) }, + quote: async () => { throw new Error("must not quote"); }, + history: {}, + }; + const actions = createActions(sdk, deps, { attempts: 1, intervalMs: 0 }); + await assert.rejects(actions.buy({ meme_address: MEME, amount_ton: "1" }), /code hash/i); + await assert.rejects(actions.sell({ meme_address: MEME, amount_tokens: "1" }), /code hash/i); + await assert.rejects(actions.claimCreatorFee({ meme_address: MEME }), /code hash/i); + await assert.rejects(actions.claimPartnerFee({ meme_address: MEME }), /code hash/i); + assert.equal(sends, 0); +}); + +test("curve buy and sell use fixed targets, safe budgets and internally-derived min output", async () => { + const sent = []; + const balances = [10_000n, 11_000n, 11_000n, 9_000n]; + const sdk = sdkBase({ send: async (...args) => { sent.push(args); return { hash: `hash${sent.length}`, seqno: sent.length }; } }); + const state = { + memeInfo: async () => curveInfo(), + walletInfo: async () => ({ wallet_address: ONE, verified: true, _rawBalance: balances.shift() }), + }; + const quote = async ({ direction }) => direction === "buy" + ? { expected_output: "2", minimum_output: "1.9", minimum_output_raw: "1900000000" } + : { expected_output: "0.9", minimum_output: "0.8", minimum_output_raw: "800000000" }; + const actions = createActions(sdk, { config, state, quote, history: {} }, { attempts: 1, intervalMs: 0, sleep: async () => {} }); + const buy = await actions.buy({ meme_address: MEME, amount_ton: "1" }); + assert.equal(sent[0][0], MEME); + assert.equal(sent[0][1], 1.1); + assert.equal(decodeBuy(sent[0][2].body).minimalAmountOut, 1_900_000_000n); + assert.equal(buy.status, "confirmed"); + + const sell = await actions.sell({ meme_address: MEME, amount_tokens: "0.000001" }); + assert.equal(sent[1][0], ONE); + assert.equal(sent[1][1], 0.12); + assert.equal(decodeSellTokens(sent[1][2].body).minimalAmountOut, 800_000_000n); + assert.equal(sell.status, "confirmed"); +}); + +test("settlement timeout returns submitted_unsettled, not confirmed", async () => { + const sdk = sdkBase(); + const state = { memeInfo: async () => curveInfo(), walletInfo: async () => ({ wallet_address: ONE, verified: true, _rawBalance: 10n }) }; + const quote = async () => ({ expected_output: "1", minimum_output: "0.9", minimum_output_raw: "900000000" }); + const actions = createActions(sdk, { config, state, quote, history: {} }, { attempts: 1, intervalMs: 0, sleep: async () => {} }); + const result = await actions.buy({ meme_address: MEME, amount_ton: "1" }); + assert.equal(result.status, "submitted_unsettled"); +}); + +test("migrated trades route exclusively through DeDust", async () => { + const calls = []; + const sdk = sdkBase({ dex: { swapDeDust: async (params) => { calls.push(params); return { txRef: "dexhash", expectedOutput: "2", minOutput: "1.8" }; } } }); + const state = { memeInfo: async () => curveInfo({ migrated: true }), walletInfo: async () => ({ verified: true, _rawBalance: 10_000_000_000n }) }; + const actions = createActions(sdk, { config, state, quote: async () => {}, history: {} }); + await actions.buy({ meme_address: MEME, amount_ton: "1", slippage_bps: 500 }); + await actions.sell({ meme_address: MEME, amount_tokens: "2", slippage_bps: 250 }); + assert.deepEqual(calls, [ + { fromAsset: "ton", toAsset: MEME, amount: 1, slippage: 0.05 }, + { fromAsset: MEME, toAsset: "ton", amount: 2, slippage: 0.025 }, + ]); +}); + +test("launches target only v3.1 and claims cannot redirect payouts", async () => { + const sent = []; + const sdk = sdkBase({ send: async (...args) => { sent.push(args); return { hash: "hash", seqno: 1 }; } }); + let creatorFee = 1_000_000_000n; + let partnerFee = 2_000_000_000n; + const state = { + memeInfo: async () => curveInfo({ creator_fee_nano: String(creatorFee), partner_fee_nano: String(partnerFee) }), + inspectContract: async () => ({ verified: true, version: "v3.1" }), + }; + const history = { recentLaunches: async () => [] }; + const actions = createActions(sdk, { config, state, quote: async () => {}, history }, { attempts: 1, intervalMs: 0, sleep: async () => { creatorFee = 0n; partnerFee = 0n; } }); + await actions.launchPreset({ preset_id: 3, metadata_uri: "ipfs://bafytest", initial_buy_ton: "0" }); + await actions.launchCustom({ total_supply_preset_id: 1, base_fee_preset_id: 3, raising_funds_ton: "1000", on_sell_supply_percent: 75, partner_fee_bps: 0, pool_partner_fee_bps: 0, pool_base_fee_preset_id: 3, metadata_uri: "ipfs://bafytest", initial_buy_ton: "0" }); + assert.equal(sent[0][0], ACTIVE_FACTORY); + assert.equal(sent[1][0], ACTIVE_FACTORY); + + await actions.claimCreatorFee({ meme_address: MEME }); + const creatorClaim = decodeClaim(sent[2][2].body, OPCODES.CLAIM_CREATOR_FEE); + assert.equal(creatorClaim.to.toString(), ZERO); + assert.equal(creatorClaim.excessesTo.toString(), ZERO); + await actions.claimPartnerFee({ meme_address: MEME }); + const partnerClaim = decodeClaim(sent[3][2].body, OPCODES.CLAIM_PARTNER_FEE); + assert.equal(partnerClaim.to.toString(), ZERO); + assert.equal(partnerClaim.excessesTo.toString(), ZERO); +}); + +test("custom deploy constraints return stable failures", async () => { + const actions = createActions(sdkBase(), { config, state: {}, quote: async () => {}, history: {} }); + const base = { total_supply_preset_id: 1, base_fee_preset_id: 3, raising_funds_ton: "1000", on_sell_supply_percent: 75, partner_fee_bps: 0, pool_partner_fee_bps: 0, pool_base_fee_preset_id: 3, metadata_uri: "ipfs://bafytest" }; + await assert.rejects(actions.launchCustom({ ...base, raising_funds_ton: "799" }), (error) => error.code === "RAISE_OUT_OF_RANGE"); + await assert.rejects(actions.launchCustom({ ...base, partner_fee_bps: 6001 }), (error) => error.code === "PARTNER_FEE_OUT_OF_RANGE"); + await assert.rejects(actions.launchCustom({ ...base, on_sell_supply_percent: 59 }), (error) => error.code === "SELL_SUPPLY_OUT_OF_RANGE"); +}); + +test("unproven third-party partner claims fail closed even when configured", async () => { + const thirdPartyConfig = { ...config, allowThirdPartyPartnerClaim: true }; + const state = { memeInfo: async () => curveInfo({ partner_address: ONE }) }; + const actions = createActions(sdkBase(), { config: thirdPartyConfig, state, quote: async () => {}, history: {} }); + await assert.rejects(actions.claimPartnerFee({ meme_address: MEME }), (error) => error.code === "PARTNER_CLAIM_AUTH_UNVERIFIED"); +}); + +test("metadata SSRF and Toncenter failures fail closed with bounded errors", async () => { + await assert.rejects(assertPublicMetadataUrl("http://127.0.0.1/a.json"), (error) => error.code === "METADATA_SSRF_BLOCKED"); + await assert.rejects(assertPublicMetadataUrl("http://localhost/a.json"), (error) => error.code === "METADATA_SSRF_BLOCKED"); + await assert.rejects(assertPublicMetadataUrl("http://[::1]/a.json"), (error) => error.code === "METADATA_SSRF_BLOCKED"); + await assert.rejects(assertPublicMetadataUrl("http://[0:0:0:0:0:0:0:1]/a.json"), (error) => error.code === "METADATA_SSRF_BLOCKED"); + const originalFetch = globalThis.fetch; + try { + globalThis.fetch = async () => new Response("{}", { status: 429 }); + await assert.rejects(createHttp(sdkBase()).transactions(ZERO), (error) => error.code === "UPSTREAM_RATE_LIMITED"); + globalThis.fetch = async () => new Response("not json", { status: 200, headers: { "content-type": "application/json" } }); + await assert.rejects(createHttp(sdkBase()).transactions(ZERO), (error) => error.code === "INVALID_RESPONSE"); + } finally { + globalThis.fetch = originalFetch; + } +}); + +test("metadata fetches stay pinned to the address validated for each redirect hop", async () => { + const resolved = []; + const fetched = []; + const responses = [ + new Response(null, { status: 302, headers: { location: "https://cdn.example/meta.json" } }), + new Response('{"name":"safe"}', { status: 200, headers: { "content-type": "application/json" } }), + ]; + const targets = new Map([ + ["metadata.example", "203.0.113.10"], + ["cdn.example", "203.0.113.11"], + ]); + const http = createHttp(sdkBase(), { + resolveMetadataTarget: async (input) => { + const url = new URL(input); + const target = { url, address: targets.get(url.hostname), family: 4 }; + resolved.push(target); + return target; + }, + fetchPinnedMetadata: async (target) => { + fetched.push(target); + return responses.shift(); + }, + }); + + assert.deepEqual(await http.metadata("https://metadata.example/meta.json"), { name: "safe" }); + assert.deepEqual(resolved.map(({ address }) => address), ["203.0.113.10", "203.0.113.11"]); + assert.deepEqual(fetched.map(({ address }) => address), ["203.0.113.10", "203.0.113.11"]); + assert.strictEqual(fetched[0], resolved[0]); + assert.strictEqual(fetched[1], resolved[1]); +}); + +test("metadata DNS is not resolved again between validation and connection", async () => { + let lookups = 0; + const fetched = []; + const lookupOnce = async () => { + lookups += 1; + return lookups === 1 + ? [{ address: "203.0.113.20", family: 4 }] + : [{ address: "127.0.0.1", family: 4 }]; + }; + const http = createHttp(sdkBase(), { + resolveMetadataTarget: (input) => resolvePublicMetadataTarget(input, lookupOnce), + fetchPinnedMetadata: async (target) => { + fetched.push(target.address); + return new Response('{"name":"safe"}', { status: 200 }); + }, + }); + + assert.deepEqual(await http.metadata("https://rebind.example/meta.json"), { name: "safe" }); + assert.equal(lookups, 1); + assert.deepEqual(fetched, ["203.0.113.20"]); +});