Stellagate
@stellagate/core

Developer docs

One function, check(), tells you before signing whether a Stellar destination needs a memo, can hold the asset, and can be reached from the sending account.

Quick start

Stellagate runs in the browser and in Node.js 20 or later. It is built on @stellar/stellar-sdk, reads only public Stellar services, and never signs or submits anything. It is not on the npm registry yet, so install it from GitHub; npm builds it on install.

Terminal
npm install github:StellaGate/stellagate

Check an address

TypeScript
import { check } from "@stellagate/core";

const verdict = await check({
  to: "GDT7ARDYZRBXXYOCSQ3MUMISTITSSRWZI6KR2A5L5Q3KB4QIZHGYMTIH",
  network: "public",
});

if (verdict.status === "block") {
  const [reason] = verdict.reasons;
  console.error(reason.message, reason.fix);
}

Reading a verdict

A verdict has one status and a list of reasons. Each reason carries a stable code to switch on, a severity, a message written for the person sending, and usually a fix. The status is the most severe reason.

Status Check page says What it means
ok Looks fine. Nothing found that would lose or strand the payment.
warn Check first. It might go wrong. Show the reason and let the person decide.
block Stop. The network will reject the payment, or it will land and never be credited. Keep the send button disabled.

info reasons never change the status. They say what was not checked, for example when the directory could not be reached.

Address formats

Pass whatever the person pasted. Stellagate works out the format itself.

  • G... classic accounts, 56 characters.
  • M... muxed accounts (SEP-23), 69 characters: a G account plus a 64-bit customer ID, so no memo is needed.
  • C... contracts, including passkey smart wallets.
  • name*domain federation addresses (SEP-2), resolved to an account and, when the server gives one, a memo.

A pasted secret key (S...) is refused with destination_secret_key, and the check page clears the field.

Memo required

SEP-29 lets an account flag that it needs a memo, but few exchanges set it: of the 107 accounts StellarExpert's directory tags memo-required, 14 carry the flag. Stellagate reads both the on-chain flag and the directory, so an exchange that never set SEP-29 is still caught.

TypeScript
const verdict = await check({
  to: "GA5XIGA5C7QTPTWXQHY6MCJRMTRZDOSHR6EFIBNDQTCQHG262N4GGKTM", // Kraken
  network: "public",
});

// verdict.status          "block"
// verdict.reasons[0].code "memo_required"
// verdict.entry.name      "Kraken"

Smart wallets (C-addresses)

A contract account pays through Soroban, and Soroban transactions cannot carry a memo. So a smart wallet cannot pay an exchange that credits deposits by memo: the payment lands and is never credited.

Pass the sender as from and Stellagate catches this before the transaction is built. A memo-required destination gives contract_sender_cannot_memo, with the fix to send from a classic account or use the receiver's M-address. A muxed destination from a contract sender gives a muxed_from_contract warning, because not every exchange credits those yet.

Can it hold the asset?

Pass asset and, optionally, amount. A classic account needs a trustline the issuer has authorised, with room under its limit. A contract address holds classic assets through the asset's Stellar Asset Contract, so that contract must be deployed and the destination contract must exist.

TypeScript
const verdict = await check({
  to: "GARSCEEOGZ4MGOTZLQHOKJGOPK455N6SHD7SAEFFHIJDAV445GFWJGHD",
  asset: "USDC:GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN",
  amount: "100",
  network: "public",
});

// verdict.reasons[0].code "no_trustline"

Contract checks read Stellar RPC. On testnet the public endpoint is used by default. On mainnet, pass your own with contracts: rpcContractSource(url); without it, the verdict includes contract_checks_skipped.

Flagged accounts

Accounts the StellarExpert directory tags malicious or unsafe are blocked with destination_flagged. Some imitate an exchange's domain, so the verdict names the directory entry.

TypeScript
const verdict = await check({ to: "GD5CWQEKH22FMSJHCZ7W2IKPKZO3HGBT6O2LK3AILMGEQA656HLUBVAJ", network: "public" });

// verdict.reasons[0].code "destination_flagged"
// verdict.entry           { name: "Fraud", domain: "kraken.com.bz", ... }

check()

Parses the address, reads the account and the directory, and returns a verdict. An unreachable directory or RPC becomes an info reason and the remaining checks still run. If the account cannot be loaded, check() returns an account_unavailable warning: account existence, memo requirements and asset checks could not be completed. Earlier reasons are preserved, including any block.

function check(input: CheckInput): Promise<Verdict>;

CheckInput

A supplied memo is validated before any network request. Text is limited to 28 UTF-8 bytes, IDs use decimal digits from 0 to 18446744073709551615, and hashes use exactly 64 hexadecimal characters. Values are not trimmed. An invalid memo returns a memo_invalid block with an instruction to correct it. Omitting memoType treats the memo as text, so "-5" is valid text but an invalid ID.

interface CheckInput {
  /** What the person pasted: G, M, C or name*domain */
  to: string;
  /** The sending account, G or C. Without it, sender checks are skipped */
  from?: string;
  /** A memo the person already entered */
  memo?: string;
  /** Default: text. Hash values are 64 hexadecimal characters. */
  memoType?: "text" | "id" | "hash";
  /** "native" or CODE:ISSUER. Without it, holdability checks are skipped */
  asset?: string;
  /** Amount in units of the asset, up to 7 decimals */
  amount?: string;
  /** Default "public" */
  network?: "public" | "testnet";
  /** Default: public Horizon for the network */
  accounts?: AccountSource;
  /** Default: public RPC on testnet, none on mainnet. null skips */
  contracts?: ContractSource | null;
  /** Default: SEP-2 lookup over HTTPS */
  federation?: FederationResolver;
  /** Default: StellarExpert. null skips */
  directory?: DirectorySource | null;
}

Verdict and Reason

The verdict's memo and memoType describe what the federation server supplied, even when the caller entered a different memo. An empty or absent server memo leaves both fields absent. If the server supplies a memo without its type, memoType stays absent. This metadata survives an account_unavailable warning.

interface Verdict {
  status: "ok" | "warn" | "block";
  reasons: Reason[];
  /** The parsed destination */
  destination?: Destination;
  /** The account the payment reaches, after muxed or federation resolution */
  account?: string;
  /** A memo the federation server supplied */
  memo?: string;
  /** The server's memo type, when supplied with a nonempty memo */
  memoType?: "text" | "id" | "hash";
  /** The directory entry, when the account has one */
  entry?: DirectoryEntry;
}

interface Reason {
  code: string;
  severity: "info" | "warn" | "block";
  message: string;
  fix?: string;
}

Your own endpoints

Every network read goes through a source you can replace: your own Horizon, your own RPC, or a fake in tests.

Sources use an 8000 ms timeout per request, including response body reads. A full check can take longer because sources run in sequence; federation can read both a TOML file and a federation endpoint. Set timeoutMs on each source to change its limit. It must be an integer from 1 to 2147483647 milliseconds; invalid values throw a RangeError when the source is created. Custom sources control their own deadlines.

Horizon and directory factories also accept fetchImpl in the options object. Their existing second-argument fetch function still works with the default timeout. An injected fetch must honor the abort signal during the request and body read. The default sep2Federation object remains available; sep2FederationResolver creates a resolver with a custom timeout.

TypeScript
import { check, horizonAccountSource, stellarExpertDirectory, sep2FederationResolver, rpcContractSource } from "@stellagate/core";

const options = { timeoutMs: 5000 };
const verdict = await check({
  to,
  network: "public",
  accounts: horizonAccountSource("https://horizon.example.org", options),
  directory: stellarExpertDirectory("public", options),
  federation: sep2FederationResolver(options),
  contracts: rpcContractSource("https://rpc.example.org", options),
});