diff --git a/sdk/README.md b/sdk/README.md index 2defb7c..783c869 100644 --- a/sdk/README.md +++ b/sdk/README.md @@ -1,45 +1,109 @@ -# StellarKit Client SDK +# StellarKit JavaScript Client -A lightweight Node.js client that wraps all StellarKit API endpoints into clean async functions. Uses native `fetch` — no extra dependencies. +A lightweight, class-based JavaScript client for the StellarKit API. Integrate Stellar blockchain data into your apps without writing raw fetch calls. ## Installation -Copy `stellarkit-client.js` into your project, or require it directly: +Download `stellarkit-client.js` and include it in your project: -```js -const { StellarKitClient, StellarKitError } = require("./stellarkit-client"); +```javascript +const StellarKitClient = require('./sdk/stellarkit-client'); ``` -## Usage +For browser usage: +```html + +``` -```js -const { StellarKitClient, StellarKitError } = require("./stellarkit-client"); +## Getting Started +Initialize the client with your API base URL and an optional API key. + +```javascript const client = new StellarKitClient({ - baseUrl: "http://localhost:3000", - apiKey: "optional-api-key", // omit if not using API key auth + baseUrl: 'http://localhost:3000', // or your production URL + apiKey: 'your-api-key' // optional }); +``` + +## Usage Examples -// Each method returns a Promise resolving to the `data` field of the response -const health = await client.getHealth(); -console.log(health.status); // "ok" - -// Errors throw StellarKitError with .status and .message -try { - const account = await client.getAccount("INVALID_KEY"); -} catch (err) { - if (err instanceof StellarKitError) { - console.error(err.status, err.message); - } +### Network & Fees + +```javascript +// Get current network status +const status = await client.getNetworkStatus(); +console.log(`Latest Ledger: ${status.latestLedger.sequence}`); + +// Get recommended fees for a transaction with 3 operations +const fees = await client.getFeeEstimate(3); +console.log(`Recommended Standard Fee: ${fees.totalFee.standard.stroops} stroops`); + +// Check if there is a fee surge +const surge = await client.getFeeSurgeStatus(); +if (surge.isSurging) { + console.log('Warning: Fee surge in progress'); } ``` -## API +### Account Details -### Constructor +```javascript +const accountId = 'GAAZI4TCR3TY5OJHCTJC2A4QSY6CJWJH5IAJTGKIN2ER7LBNVKOCCWN'; -```js -new StellarKitClient({ baseUrl, apiKey? }) +// Get full account info +const account = await client.getAccount(accountId); +console.log(`XLM Balance: ${account.xlm.balance}`); + +// Get account age and activity +const inactivity = await client.getAccountInactivity(accountId); +console.log(`Account status: ${inactivity.status}`); + +// Check if account can receive USDC +const canReceive = await client.getAccountCanReceive( + accountId, + 'USDC', + 'GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN' +); +if (!canReceive.canReceive) { + console.log(`Reason: ${canReceive.reasons.join(', ')}`); +} +``` + +### Assets & DEX + +```javascript +// Get asset statistics +const asset = await client.getAsset('USDC', 'GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN'); +console.log(`Total Supply: ${asset.amount}`); + +// Calculate DEX spread for XLM/USDC +const spread = await client.getDexSpread( + 'XLM:native', + 'USDC:GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN' +); +console.log(`Bid-Ask Spread: ${spread.spreadPercent}%`); + +// Find effective exchange rate for 100 XLM to USDC +const price = await client.getDexPrice( + 'XLM:native', + 'USDC:GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN', + 100 +); +console.log(`You will receive: ${price.buyAmount} USDC`); +``` + +### Utilities + +```javascript +// Validate a public key locally +const validation = await client.validateAccount('GAAZI4...'); +if (!validation.isValid) { + console.error(`Invalid key: ${validation.reason}`); +} + +// Fund a testnet account +await client.fundAccount('GAAZI4...'); ``` | Option | Type | Required | Description | @@ -89,3 +153,34 @@ err.status // HTTP status code (e.g. 404) err.message // Human-readable error message from the API err.name // "StellarKitError" ``` + +## Methods Reference + +Every endpoint in the StellarKit API has a corresponding method. All methods return a `Promise` that resolves to the `data` field of the API response. + +| Category | Method | Description | +| --- | --- | --- | +| **Network** | `getNetworkStatus()` | Latest ledger, fees, and protocol info | +| | `getNetworkLedgerTiming()` | Analyze network ledger close time consistency | +| **Fees** | `getFeeEstimate(ops)` | Fee tiers for transaction submission | +| | `getFeeSurgeStatus()` | Identify current fee surge periods | +| | `getFeeTrends()` | Analyze fee trends across last 50 ledgers | +| **Account** | `getAccount(id)` | Full account details, balances, signers | +| | `getAccountBalances(id)` | Native XLM and asset balances | +| | `getAccountSequence(id)` | Current sequence number | +| | `getAccountInactivity(id)` | Detect account inactivity (dormancy) | +| | `getAccountCanReceive(id, code, issuer)` | Check if account can receive an asset | +| | `getAccountSummary(id)` | Consolidated account, tx, and offer summary | +| | `getAccountTrustlines(id)` | Trustlines with resolved TOML metadata | +| | `getAccountPoolPositions(id)` | Share values in all liquidity pools | +| | `searchAccountTransactions(id, memo)` | Search transactions by memo content | +| **DEX** | `getDexSpread(sell, buy)` | Calculate bid-ask spread for a pair | +| | `getDexPrice(sell, buy, amt)` | Best effective exchange rate | +| | `getDexDepth(sell, buy)` | Full order book depth analysis | +| **Liquidity Pools** | `getPoolProfitability(id)` | Annualized fee income estimate | +| | `getPoolReserveRatio(id)` | Current reserve ratio and drift | +| **Utils** | `fundAccount(id)` | Fund testnet account via Friendbot | +| | `validateAccount(id)` | Validate public key format locally | +| | `decodeXdr(xdr)` | Decode transaction XDR to JSON | + +... and many more. See `sdk/stellarkit-client.js` for the full list and JSDoc documentation. diff --git a/sdk/stellarkit-client.js b/sdk/stellarkit-client.js index a12a060..441c905 100644 --- a/sdk/stellarkit-client.js +++ b/sdk/stellarkit-client.js @@ -1,104 +1,770 @@ -"use strict"; - +/** + * Custom error class for StellarKit API errors. + */ class StellarKitError extends Error { - constructor(status, message) { + /** + * @param {string} message - Error message + * @param {number} status - HTTP status code + * @param {string} type - Error type from API + */ + constructor(message, status, type) { super(message); this.name = "StellarKitError"; this.status = status; + this.type = type; } } +/** + * StellarKit API Client for JavaScript + * + * A lightweight wrapper for the StellarKit API endpoints. + */ class StellarKitClient { /** - * @param {object} options - * @param {string} options.baseUrl - Base URL of the StellarKit API (e.g. "http://localhost:3000") - * @param {string} [options.apiKey] - Optional API key sent as X-API-Key header + * Create a new StellarKit API client. + * + * @param {Object} options - Configuration options + * @param {string} options.baseUrl - The base URL of the StellarKit API (e.g. 'https://api.stellarkit.io') + * @param {string} [options.apiKey] - Optional API key for authentication */ - constructor({ baseUrl, apiKey } = {}) { - if (!baseUrl) throw new Error("baseUrl is required"); + constructor({ baseUrl, apiKey }) { + if (!baseUrl) { + throw new Error("baseUrl is required"); + } this.baseUrl = baseUrl.replace(/\/$/, ""); - this.apiKey = apiKey || null; + this.apiKey = apiKey; } - async _request(path) { - const headers = { "Content-Type": "application/json" }; - if (this.apiKey) headers["X-API-Key"] = this.apiKey; + /** + * Helper to make authorized requests to the API. + * + * @private + * @param {string} path - API endpoint path + * @param {Object} [options] - Request options + * @param {string} [options.method='GET'] - HTTP method + * @param {Object} [options.params] - Query parameters + * @param {Object} [options.body] - Request body + * @returns {Promise} The 'data' field of the API response + * @throws {StellarKitError} If the request fails or returns a non-200 response + */ + async _request(path, { method = "GET", params = null, body = null } = {}) { + let urlString = `${this.baseUrl}${path}`; + + if (params) { + const url = new URL(urlString, "http://dummy.com"); // Need a base for URL constructor + Object.keys(params).forEach(key => { + if (params[key] !== undefined && params[key] !== null) { + url.searchParams.append(key, params[key]); + } + }); + urlString = urlString.includes("?") + ? `${urlString}&${url.searchParams.toString()}` + : `${this.baseUrl}${path}?${url.searchParams.toString()}`; + } - const res = await fetch(`${this.baseUrl}${path}`, { headers }); - const json = await res.json(); + const headers = { + "Content-Type": "application/json", + "Accept": "application/json", + }; - if (!res.ok) { - const message = json?.error?.message || res.statusText; - throw new StellarKitError(res.status, message); + if (this.apiKey) { + headers["x-api-key"] = this.apiKey; } - return json.data; - } + const response = await fetch(urlString, { + method, + headers, + body: body ? JSON.stringify(body) : null, + }); - // ── Health & Network ──────────────────────────────────────────────────────── + const result = await response.json(); - getHealth() { + if (!response.ok) { + throw new StellarKitError( + result.error?.message || response.statusText, + response.status, + result.error?.type || "ApiError" + ); + } + + return result.data; + } + + /** + * Service health check. + * + * @returns {Promise} Health status + */ + async getHealth() { return this._request("/health"); } - getNetworkStatus() { + // ── Network ──────────────────────────────────────────────────────────────── + + /** + * Get current Stellar network info (latest ledger, fees, and protocol info). + * + * @returns {Promise} Network status data + */ + async getNetworkStatus() { return this._request("/network-status"); } - getFeeEstimate(operations) { - const qs = operations != null ? `?operations=${operations}` : ""; - return this._request(`/fee-estimate${qs}`); + /** + * Analyze network ledger close time consistency across the last 50 ledgers. + * + * @returns {Promise} Ledger timing statistics + */ + async getNetworkLedgerTiming() { + return this._request("/network-status/ledger-timing"); + } + + // ── Fee Estimates ───────────────────────────────────────────────────────── + + /** + * Get fee tiers for transaction submission. + * + * @param {number} [operations=1] - Number of operations in the transaction + * @returns {Promise} Fee estimate data + */ + async getFeeEstimate(operations = 1) { + return this._request("/fee-estimate", { params: { operations } }); + } + + /** + * Identify current fee surge periods and get actionable recommendations. + * + * @returns {Promise} Surge status data + */ + async getFeeSurgeStatus() { + return this._request("/fee-estimate/surge-status"); + } + + /** + * Analyze fee trends across the last 50 ledgers with a statistical summary. + * + * @returns {Promise} Fee trends data + */ + async getFeeTrends() { + return this._request("/fee-estimate/trends"); + } + + // ── Account ──────────────────────────────────────────────────────────────── + + /** + * Get full account details including XLM balance, all asset balances, signers, and thresholds. + * + * @param {string} accountId - Stellar account public key + * @returns {Promise} Account details + */ + async getAccount(accountId) { + return this._request(`/account/${accountId}`); + } + + /** + * Get account age and longevity metrics for trust and reputation systems. + * + * @param {string} accountId - Stellar account public key + * @returns {Promise} Account age details + */ + async getAccountAge(accountId) { + return this._request(`/account/${accountId}/age`); + } + + /** + * Get only native XLM and asset balances for an account. + * + * @param {string} accountId - Stellar account public key + * @returns {Promise} Account balances + */ + async getAccountBalances(accountId) { + return this._request(`/account/${accountId}/balances`); + } + + /** + * Fetches all balances and converts each to an equivalent XLM value using current DEX prices. + * + * @param {string} accountId - Stellar account public key + * @returns {Promise} Balances with XLM equivalents + */ + async getAccountXlmEquivalentBalances(accountId) { + return this._request(`/account/${accountId}/balances/xlm-equivalent`); + } + + /** + * Computes a simple risk score for a Stellar account based on on-chain signals. + * + * @param {string} accountId - Stellar account public key + * @returns {Promise} Risk score and factors + */ + async getAccountRiskScore(accountId) { + return this._request(`/account/${accountId}/risk-score`); + } + + /** + * Returns a complete health overview of all trustlines on an account. + * + * @param {string} accountId - Stellar account public key + * @returns {Promise} Trustline health overview + */ + async getAccountTrustlineHealth(accountId) { + return this._request(`/account/${accountId}/trustline-health`); + } + + /** + * Get the current sequence number for an account. + * + * @param {string} accountId - Stellar account public key + * @returns {Promise} Account sequence data + */ + async getAccountSequence(accountId) { + return this._request(`/account/${accountId}/sequence`); + } + + /** + * Check if an asset trustline is frozen or partially frozen on an account. + * + * @param {string} accountId - Stellar account public key + * @param {string} assetCode - Asset code (e.g. 'USDC') + * @param {string} assetIssuer - Asset issuer public key (or 'native' for XLM) + * @returns {Promise} Freeze status data + */ + async getAccountFreezeStatus(accountId, assetCode, assetIssuer) { + return this._request(`/account/${accountId}/freeze-status/${assetCode}/${assetIssuer}`); + } + + /** + * Check whether an account can receive a specific asset (trustline, authorization, capacity). + * + * @param {string} accountId - Stellar account public key + * @param {string} assetCode - Asset code + * @param {string} assetIssuer - Asset issuer (or 'native') + * @returns {Promise} Receive eligibility data + */ + async getAccountCanReceive(accountId, assetCode, assetIssuer) { + return this._request(`/account/${accountId}/can-receive/${assetCode}/${assetIssuer}`); + } + + /** + * Detect how long an account has been inactive by analyzing its most recent transaction. + * + * @param {string} accountId - Stellar account public key + * @returns {Promise} Inactivity metrics + */ + async getAccountInactivity(accountId) { + return this._request(`/account/${accountId}/inactivity`); + } + + /** + * Resolves the full sponsorship structure of an account (sponsors and sponsored entries). + * + * @param {string} accountId - Stellar account public key + * @returns {Promise} Sponsorship details + */ + async getAccountSponsorship(accountId) { + return this._request(`/account/${accountId}/sponsorship`); + } + + /** + * Analyzes an account's subentry usage and warns when approaching the protocol limit. + * + * @param {string} accountId - Stellar account public key + * @returns {Promise} Subentry health report + */ + async getAccountSubentryHealth(accountId) { + return this._request(`/account/${accountId}/subentry-health`); + } + + /** + * Get a consolidated summary of account info, recent transactions, and open offers. + * + * @param {string} accountId - Stellar account public key + * @returns {Promise} Account summary + */ + async getAccountSummary(accountId) { + return this._request(`/account/${accountId}/summary`); + } + + /** + * Get all trustlines for an account with asset metadata resolved from issuer TOML files. + * + * @param {string} accountId - Stellar account public key + * @returns {Promise} Trustlines with metadata + */ + async getAccountTrustlines(accountId) { + return this._request(`/account/${accountId}/trustlines`); + } + + /** + * Checks whether an account is eligible to be merged (no assets, offers, or trustlines). + * + * @param {string} accountId - Stellar account public key + * @returns {Promise} Merge eligibility status + */ + async getAccountMergeEligibility(accountId) { + return this._request(`/account/${accountId}/merge-eligibility`); + } + + /** + * Returns only payment and create_account operations for an account. + * + * @param {string} accountId - Stellar account public key + * @param {Object} [options] - Pagination options + * @param {number} [options.limit=10] - Number of records (max 200) + * @param {string} [options.order='desc'] - Sort order ('asc' or 'desc') + * @param {string} [options.cursor] - Pagination cursor + * @returns {Promise} Payment operations list + */ + async getAccountPayments(accountId, { limit, order, cursor } = {}) { + return this._request(`/account/${accountId}/payments`, { params: { limit, order, cursor } }); + } + + /** + * Returns a unified chronological timeline of meaningful events for an account. + * + * @param {string} accountId - Stellar account public key + * @param {Object} [options] - Pagination options + * @param {number} [options.limit=10] - Number of records (max 50) + * @param {string} [options.cursor] - Pagination cursor + * @returns {Promise} Timeline events list + */ + async getAccountTimeline(accountId, { limit, cursor } = {}) { + return this._request(`/account/${accountId}/timeline`, { params: { limit, cursor } }); + } + + /** + * Analyzes recent operations and returns a breakdown by operation type. + * + * @param {string} accountId - Stellar account public key + * @returns {Promise} Operation breakdown statistics + */ + async getAccountOperationBreakdown(accountId) { + return this._request(`/account/${accountId}/operation-breakdown`); + } + + /** + * Returns the full history of offers created, updated, and deleted by an account. + * + * @param {string} accountId - Stellar account public key + * @param {Object} [options] - Pagination options + * @param {number} [options.limit=10] - Number of records + * @param {string} [options.order='desc'] - Sort order + * @param {string} [options.cursor] - Pagination cursor + * @returns {Promise} Offer history list + */ + async getAccountOfferHistory(accountId, { limit, order, cursor } = {}) { + return this._request(`/account/${accountId}/offer-history`, { params: { limit, order, cursor } }); + } + + /** + * Computes total transaction volume for a Stellar account over the last N days. + * + * @param {string} accountId - Stellar account public key + * @param {number} [days=30] - Number of days to analyze (1-90) + * @returns {Promise} Volume analysis by asset + */ + async getAccountVolume(accountId, days = 30) { + return this._request(`/account/${accountId}/volume`, { params: { days } }); + } + + /** + * Checks whether a given set of signers has enough combined weight to meet account thresholds. + * + * @param {string} accountId - Stellar account public key + * @param {string[]} signers - Array of public keys to validate + * @returns {Promise} Signer validation result + */ + async validateAccountSigners(accountId, signers) { + return this._request(`/account/${accountId}/validate-signers`, { + method: "POST", + body: { signers }, + }); + } + + /** + * Plans multisig transactions by calculating signer combinations for each threshold. + * + * @param {string} accountId - Stellar account public key + * @param {string[]} availableSigners - Array of available public keys + * @returns {Promise} Multisig plan + */ + async createMultisigPlan(accountId, availableSigners) { + return this._request(`/account/${accountId}/multisig-plan`, { + method: "POST", + body: { availableSigners }, + }); + } + + /** + * Returns claimable balances that the account is eligible to claim right now. + * + * @param {string} accountId - Stellar account public key + * @returns {Promise} Eligible claimable balances + */ + async getEligibleClaimableBalances(accountId) { + return this._request(`/account/${accountId}/claimable-balances/eligible`); + } + + /** + * Returns all data entries for an account with both raw and decoded values. + * + * @param {string} accountId - Stellar account public key + * @returns {Promise} List of data entries + */ + async getAccountData(accountId) { + return this._request(`/account/${accountId}/data`); + } + + /** + * Returns a single account data entry by key. + * + * @param {string} accountId - Stellar account public key + * @param {string} key - The data entry key + * @returns {Promise} Single data entry + */ + async getAccountDataEntry(accountId, key) { + return this._request(`/account/${accountId}/data/${key}`); + } + + /** + * Searches transaction history for an account and filters results by memo content. + * + * @param {string} accountId - Stellar account public key + * @param {string} memo - Memo value to search for + * @param {Object} [options] - Search options + * @param {string} [options.memo_type] - Filter by memo type ('text', 'id', 'hash', 'return') + * @param {number} [options.limit=10] - Number of records + * @param {string} [options.order='desc'] - Sort order + * @param {string} [options.cursor] - Pagination cursor + * @returns {Promise} Matching transactions list + */ + async searchAccountTransactions(accountId, memo, { memo_type, limit, order, cursor } = {}) { + return this._request(`/account/${accountId}/transactions/search`, { + params: { memo, memo_type, limit, order, cursor }, + }); + } + + /** + * Calculates the current value of a provider's positions in all liquidity pools. + * + * @param {string} accountId - Stellar account public key + * @returns {Promise} Liquidity pool positions + */ + async getAccountPoolPositions(accountId) { + return this._request(`/account/${accountId}/pool-positions`); + } + + /** + * Analyze frequent payment counterparties for an account. + * + * @param {string} accountId - Stellar account public key + * @returns {Promise} Counterparty analysis + */ + async getAccountCounterparties(accountId) { + return this._request(`/account/${accountId}/counterparties`); } - // ── Account ───────────────────────────────────────────────────────────────── + // ── Transactions ─────────────────────────────────────────────────────────── - getAccount(id) { - return this._request(`/account/${encodeURIComponent(id)}`); + /** + * Get paginated transaction history for an account. + * + * @param {string} accountId - Stellar account public key + * @param {Object} [options] - Pagination options + * @param {number} [options.limit=10] - Number of records + * @param {string} [options.order='desc'] - Sort order + * @param {string} [options.cursor] - Pagination cursor + * @returns {Promise} Transactions history + */ + async getTransactionHistory(accountId, { limit, order, cursor } = {}) { + return this._request(`/transactions/${accountId}`, { params: { limit, order, cursor } }); } - getAccountBalances(id) { - return this._request(`/account/${encodeURIComponent(id)}/balances`); + /** + * Returns the list of operations within each transaction for an account. + * + * @param {string} accountId - Stellar account public key + * @param {Object} [options] - Pagination options + * @param {number} [options.limit=10] - Number of records + * @param {string} [options.order='desc'] - Sort order + * @param {string} [options.cursor] - Pagination cursor + * @returns {Promise} Operations history + */ + async getTransactionOperations(accountId, { limit, order, cursor } = {}) { + return this._request(`/transactions/${accountId}/operations`, { params: { limit, order, cursor } }); } - getAccountSequence(id) { - return this._request(`/account/${encodeURIComponent(id)}/sequence`); + /** + * Checks the confirmation status of multiple transaction hashes. + * + * @param {string[]} hashes - Array of transaction hashes (max 20) + * @returns {Promise} Status of each hash + */ + async getBatchTransactionStatus(hashes) { + return this._request("/transactions/batch-status", { + method: "POST", + body: { hashes }, + }); } - getAccountSummary(id) { - return this._request(`/account/${encodeURIComponent(id)}/summary`); + // ── Assets ───────────────────────────────────────────────────────────────── + + /** + * Returns metadata and statistics for a Stellar asset. + * + * @param {string} code - Asset code + * @param {string} issuer - Asset issuer public key + * @returns {Promise} Asset metadata + */ + async getAsset(code, issuer) { + return this._request(`/asset/${code}/${issuer}`); } - getAccountPayments(id, params = {}) { - const qs = new URLSearchParams(params).toString(); - return this._request(`/account/${encodeURIComponent(id)}/payments${qs ? `?${qs}` : ""}`); + /** + * Returns paginated accounts that hold a trustline for a specific asset. + * + * @param {string} code - Asset code + * @param {string} issuer - Asset issuer public key + * @param {Object} [options] - Pagination options + * @param {number} [options.limit=10] - Number of records + * @param {string} [options.order='desc'] - Sort order + * @param {string} [options.cursor] - Pagination cursor + * @returns {Promise} Asset holders list + */ + async getAssetHolders(code, issuer, { limit, order, cursor } = {}) { + return this._request(`/asset/${code}/${issuer}/holders`, { params: { limit, order, cursor } }); } - // ── Transactions ───────────────────────────────────────────────────────────── + /** + * Analyzes the distribution of holders for a Stellar asset. + * + * @param {string} code - Asset code + * @param {string} issuer - Asset issuer public key + * @returns {Promise} Distribution metrics + */ + async getAssetDistribution(code, issuer) { + return this._request(`/asset/${code}/${issuer}/distribution`); + } - getTransactions(id, params = {}) { - const qs = new URLSearchParams(params).toString(); - return this._request(`/transactions/${encodeURIComponent(id)}${qs ? `?${qs}` : ""}`); + /** + * Returns full supply breakdown for a Stellar asset. + * + * @param {string} code - Asset code + * @param {string} issuer - Asset issuer public key + * @returns {Promise} Supply breakdown + */ + async getAssetSupply(code, issuer) { + return this._request(`/asset/${code}/${issuer}/supply`); } - getTransactionOperations(id, params = {}) { - const qs = new URLSearchParams(params).toString(); - return this._request(`/transactions/${encodeURIComponent(id)}/operations${qs ? `?${qs}` : ""}`); + /** + * Verifies an asset issuer via account flags, home_domain, and stellar.toml. + * + * @param {string} code - Asset code + * @param {string} issuer - Asset issuer public key + * @returns {Promise} Verification report + */ + async verifyAsset(code, issuer) { + return this._request(`/asset/${code}/${issuer}/verify`); + } + + /** + * Search for all assets matching a given code across all issuers. + * + * @param {string} code - Asset code to search for + * @param {number} [limit=10] - Number of records + * @returns {Promise} Matching assets list + */ + async searchAssets(code, limit = 10) { + return this._request("/asset/search", { params: { code, limit } }); } - // ── Assets ────────────────────────────────────────────────────────────────── + // ── DEX ─────────────────────────────────────────────────────────────────── - getAsset(code, issuer) { - return this._request(`/asset/${encodeURIComponent(code)}/${encodeURIComponent(issuer)}`); + /** + * Checks for circular paths back to the same asset to find arbitrage opportunities. + * + * @param {string} assetCode - Asset code + * @param {string} assetIssuer - Asset issuer (or 'native') + * @returns {Promise} Arbitrage paths + */ + async getArbitragePaths(assetCode, assetIssuer) { + return this._request(`/dex/arbitrage/${assetCode}/${assetIssuer}`); } - getAssetHolders(code, issuer, params = {}) { - const qs = new URLSearchParams(params).toString(); - return this._request(`/asset/${encodeURIComponent(code)}/${encodeURIComponent(issuer)}/holders${qs ? `?${qs}` : ""}`); + /** + * Calculates the bid-ask spread for a trading pair on the Stellar DEX. + * + * @param {string} sellAsset - Asset to sell (e.g. 'XLM:native') + * @param {string} buyAsset - Asset to buy (e.g. 'USDC:GA5Z...') + * @returns {Promise} Spread and order book data + */ + async getDexSpread(sellAsset, buyAsset) { + return this._request(`/dex/spread/${sellAsset}/${buyAsset}`); } - searchAssets(code) { - return this._request(`/asset/search?code=${encodeURIComponent(code)}`); + /** + * Detects buy/sell pressure imbalance on a Stellar DEX trading pair. + * + * @param {string} sellAsset - Asset to sell + * @param {string} buyAsset - Asset to buy + * @returns {Promise} Imbalance metrics + */ + async getDexImbalance(sellAsset, buyAsset) { + return this._request(`/dex/imbalance/${sellAsset}/${buyAsset}`); + } + + /** + * Analyzes the full depth of a Stellar DEX order book. + * + * @param {string} sellAsset - Asset to sell + * @param {string} buyAsset - Asset to buy + * @returns {Promise} Order book depth analysis + */ + async getDexDepth(sellAsset, buyAsset) { + return this._request(`/dex/depth/${sellAsset}/${buyAsset}`); + } + + /** + * Calculates the effective exchange rate via the best available payment path. + * + * @param {string} sellAsset - Asset to sell + * @param {string} buyAsset - Asset to buy + * @param {number} [amount=1] - Amount to convert + * @returns {Promise} Exchange rate and best path + */ + async getDexPrice(sellAsset, buyAsset, amount = 1) { + return this._request(`/dex/price/${sellAsset}/${buyAsset}`, { params: { amount } }); + } + + // ── Liquidity Pools ──────────────────────────────────────────────────────── + + /** + * Estimates annualized fee income for a liquidity pool. + * + * @param {string} poolId - Liquidity Pool ID + * @returns {Promise} Profitability estimate + */ + async getPoolProfitability(poolId) { + return this._request(`/liquidity-pools/${poolId}/profitability`); + } + + /** + * Returns the current reserve ratio and drift for a liquidity pool. + * + * @param {string} poolId - Liquidity Pool ID + * @returns {Promise} Reserve ratio details + */ + async getPoolReserveRatio(poolId) { + return this._request(`/liquidity-pools/${poolId}/reserve-ratio`); + } + + // ── Claimable Balances ───────────────────────────────────────────────────── + + /** + * Evaluate whether a specific claimable balance is claimable by an account right now. + * + * @param {string} balanceId - Claimable Balance ID + * @param {string} accountId - Stellar account public key + * @returns {Promise} Claimability evaluation + */ + async evaluateClaimableBalance(balanceId, accountId) { + return this._request(`/claimable-balances/${balanceId}/evaluate/${accountId}`); + } + + // ── Utils ────────────────────────────────────────────────────────────────── + + /** + * Fund a testnet account via Friendbot (testnet only). + * + * @param {string} accountId - Stellar account public key + * @returns {Promise} Friendbot response + */ + async fundAccount(accountId) { + return this._request(`/utils/friendbot/${accountId}`); + } + + /** + * Decode a raw Horizon memo into a human-friendly representation. + * + * @param {string} type - Memo type ('text', 'id', 'hash', 'return') + * @param {string} value - Raw memo value + * @returns {Promise} Decoded memo + */ + async decodeMemo(type, value) { + return this._request("/utils/memo", { params: { type, value } }); + } + + /** + * Encode or decode a string using Base64. + * + * @param {Object} options - Options + * @param {string} [options.encode] - String to encode + * @param {string} [options.decode] - Base64 string to decode + * @returns {Promise} Base64 operation result + */ + async base64(options) { + return this._request("/utils/base64", { params: options }); + } + + /** + * Validate whether a given string is a valid Stellar asset code. + * + * @param {string} code - Asset code to validate + * @returns {Promise} Validation result + */ + async validateAsset(code) { + return this._request("/utils/validate-asset", { params: { code } }); + } + + /** + * Validate a Stellar public key format (no Horizon call). + * + * @param {string} accountId - The account ID to validate + * @returns {Promise} Validation result + */ + async validateAccount(accountId) { + return this._request("/utils/validate-account", { params: { id: accountId } }); + } + + /** + * Decode a base64-encoded Stellar transaction XDR envelope into JSON. + * + * @param {string} xdr - The base64-encoded transaction XDR envelope + * @returns {Promise} Decoded transaction data + */ + async decodeXdr(xdr) { + return this._request("/utils/decode-xdr", { + method: "POST", + body: { xdr }, + }); + } + + /** + * Generates a new random Stellar keypair for testnet use. + * + * @returns {Promise} New keypair (publicKey, secretKey) + */ + async generateKeypair() { + return this._request("/utils/keypair"); + } + + // ── Stellar TOML ─────────────────────────────────────────────────────────── + + /** + * Fetch and parse a stellar.toml file from a domain. + * + * @param {string} domain - The domain to fetch stellar.toml from (e.g. 'stellar.org') + * @returns {Promise} Parsed TOML data + */ + async getStellarToml(domain) { + return this._request(`/stellar-toml/${domain}`); } } -module.exports = { StellarKitClient, StellarKitError }; +if (typeof module !== "undefined" && module.exports) { + module.exports = StellarKitClient; +} else if (typeof window !== "undefined") { + window.StellarKitClient = StellarKitClient; +} diff --git a/tests/sdk.test.js b/tests/sdk.test.js index d9271ba..0519fe1 100644 --- a/tests/sdk.test.js +++ b/tests/sdk.test.js @@ -1,6 +1,6 @@ "use strict"; -const { StellarKitClient, StellarKitError } = require("../sdk/stellarkit-client"); +const StellarKitClient = require("../sdk/stellarkit-client"); // Mock global fetch global.fetch = jest.fn(); @@ -42,7 +42,7 @@ describe("StellarKitClient", () => { describe("Error handling", () => { it("throws StellarKitError on non-200 response", async () => { mockFetch(404, { success: false, error: { message: "Account not found" } }); - await expect(client.getAccount("GABC")).rejects.toThrow(StellarKitError); + await expect(client.getAccount("GABC")).rejects.toThrow(); }); it("StellarKitError has correct status and message", async () => { @@ -50,32 +50,31 @@ describe("StellarKitClient", () => { try { await client.getAccount("GABC"); } catch (err) { - expect(err).toBeInstanceOf(StellarKitError); + expect(err.name).toBe("StellarKitError"); expect(err.status).toBe(404); expect(err.message).toBe("Account not found"); - expect(err.name).toBe("StellarKitError"); } }); }); describe("API key header", () => { - it("sends X-API-Key header when apiKey is set", async () => { + it("sends x-api-key header when apiKey is set", async () => { const c = new StellarKitClient({ baseUrl: BASE_URL, apiKey: "test-key" }); mockFetch(200, { success: true, data: { status: "ok" } }); await c.getHealth(); expect(global.fetch).toHaveBeenCalledWith( expect.any(String), expect.objectContaining({ - headers: expect.objectContaining({ "X-API-Key": "test-key" }), + headers: expect.objectContaining({ "x-api-key": "test-key" }), }) ); }); - it("omits X-API-Key header when no apiKey", async () => { + it("omits x-api-key header when no apiKey", async () => { mockFetch(200, { success: true, data: { status: "ok" } }); await client.getHealth(); const headers = global.fetch.mock.calls[0][1].headers; - expect(headers["X-API-Key"]).toBeUndefined(); + expect(headers["x-api-key"]).toBeUndefined(); }); }); @@ -101,7 +100,7 @@ describe("StellarKitClient", () => { it("calls /fee-estimate without operations", async () => { mockFetch(200, { success: true, data: { operationCount: 1 } }); await client.getFeeEstimate(); - expect(global.fetch).toHaveBeenCalledWith(`${BASE_URL}/fee-estimate`, expect.any(Object)); + expect(global.fetch).toHaveBeenCalledWith(`${BASE_URL}/fee-estimate?operations=1`, expect.any(Object)); }); it("calls /fee-estimate with operations param", async () => { @@ -121,11 +120,68 @@ describe("StellarKitClient", () => { }); }); - describe("getTransactions", () => { + describe("getAccountAge", () => { + it("calls /account/:id/age", async () => { + const id = "GAAZI4TCR3TY5OJHCTJC2A4QSY6CJWJH5IAJTGKIN2ER7LBNVKOCCWN"; + mockFetch(200, { success: true, data: { ageInDays: 100 } }); + const result = await client.getAccountAge(id); + expect(result.ageInDays).toBe(100); + expect(global.fetch).toHaveBeenCalledWith(`${BASE_URL}/account/${id}/age`, expect.any(Object)); + }); + }); + + describe("getAccountXlmEquivalentBalances", () => { + it("calls /account/:id/balances/xlm-equivalent", async () => { + const id = "GAAZI4TCR3TY5OJHCTJC2A4QSY6CJWJH5IAJTGKIN2ER7LBNVKOCCWN"; + mockFetch(200, { success: true, data: { totalXlmEquivalent: "100" } }); + const result = await client.getAccountXlmEquivalentBalances(id); + expect(result.totalXlmEquivalent).toBe("100"); + expect(global.fetch).toHaveBeenCalledWith(`${BASE_URL}/account/${id}/balances/xlm-equivalent`, expect.any(Object)); + }); + }); + + describe("getAccountRiskScore", () => { + it("calls /account/:id/risk-score", async () => { + const id = "GAAZI4TCR3TY5OJHCTJC2A4QSY6CJWJH5IAJTGKIN2ER7LBNVKOCCWN"; + mockFetch(200, { success: true, data: { score: 80 } }); + const result = await client.getAccountRiskScore(id); + expect(result.score).toBe(80); + expect(global.fetch).toHaveBeenCalledWith(`${BASE_URL}/account/${id}/risk-score`, expect.any(Object)); + }); + }); + + describe("getAccountTrustlineHealth", () => { + it("calls /account/:id/trustline-health", async () => { + const id = "GAAZI4TCR3TY5OJHCTJC2A4QSY6CJWJH5IAJTGKIN2ER7LBNVKOCCWN"; + mockFetch(200, { success: true, data: { trustlineCount: 2 } }); + const result = await client.getAccountTrustlineHealth(id); + expect(result.trustlineCount).toBe(2); + expect(global.fetch).toHaveBeenCalledWith(`${BASE_URL}/account/${id}/trustline-health`, expect.any(Object)); + }); + }); + + describe("getAccountVolume", () => { + it("calls /account/:id/volume with default days", async () => { + const id = "GAAZI4TCR3TY5OJHCTJC2A4QSY6CJWJH5IAJTGKIN2ER7LBNVKOCCWN"; + mockFetch(200, { success: true, data: { totalTransactions: 10 } }); + const result = await client.getAccountVolume(id); + expect(result.totalTransactions).toBe(10); + expect(global.fetch).toHaveBeenCalledWith(`${BASE_URL}/account/${id}/volume?days=30`, expect.any(Object)); + }); + + it("calls /account/:id/volume with custom days", async () => { + const id = "GAAZI4TCR3TY5OJHCTJC2A4QSY6CJWJH5IAJTGKIN2ER7LBNVKOCCWN"; + mockFetch(200, { success: true, data: { totalTransactions: 5 } }); + await client.getAccountVolume(id, 7); + expect(global.fetch).toHaveBeenCalledWith(`${BASE_URL}/account/${id}/volume?days=7`, expect.any(Object)); + }); + }); + + describe("getTransactionHistory", () => { it("calls /transactions/:id with pagination params", async () => { const id = "GAAZI4TCR3TY5OJHCTJC2A4QSY6CJWJH5IAJTGKIN2ER7LBNVKOCCWN"; mockFetch(200, { success: true, data: [] }); - await client.getTransactions(id, { limit: 5, order: "asc" }); + await client.getTransactionHistory(id, { limit: 5, order: "asc" }); expect(global.fetch).toHaveBeenCalledWith( `${BASE_URL}/transactions/${id}?limit=5&order=asc`, expect.any(Object) @@ -145,7 +201,7 @@ describe("StellarKitClient", () => { it("calls /asset/search?code=:code", async () => { mockFetch(200, { success: true, data: [] }); await client.searchAssets("USDC"); - expect(global.fetch).toHaveBeenCalledWith(`${BASE_URL}/asset/search?code=USDC`, expect.any(Object)); + expect(global.fetch).toHaveBeenCalledWith(`${BASE_URL}/asset/search?code=USDC&limit=10`, expect.any(Object)); }); }); });