stellaranchor

package
v1.0.2 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 14, 2026 License: AGPL-3.0 Imports: 20 Imported by: 0

Documentation

Overview

Package stellaranchor provides the reusable Stellar-anchor primitives that any SEP-24-compliant anchor implementation can compose on top of.

Protocol surface:

  • sep1.go — TOML fetch + validation of WEB_AUTH / TRANSFER_SERVER / SIGNING_KEY / NETWORK_PASSPHRASE / USDC issuer
  • sep9.go — KYC Customer payload + SplitFullName helper
  • sep10.go — challenge / cosign / token submit flow
  • sep24.go — interactive withdraw initiation, transaction lookup, and the Status enum that callers poll on

Supporting machinery:

  • client.go — top-level Client that wires Auth + Anchor + JWTCache from a single Config
  • jwt_cache.go — per-memo JWT cache so SEP-10 tokens are reused until near expiry
  • memo.go — ChildAccountMemo derivation (per-user 64-bit positive integer memo) for custodial-wallet anchors
  • iso.go — ISO-3166 alpha-2 to alpha-3 mapping for SEP-9 country fields
  • util.go — shared low-level helpers
  • errors.go — protocol-level sentinels and parsed error types

Anchor-specific packages (e.g. moneygram) embed *stellaranchor.Client and layer their own REST / OAuth / FX surface on top. Nothing here knows about any specific anchor, channel, database, or notification system.

Index

Constants

View Source
const RefundIDTypeStellar = "stellar"

RefundIDTypeStellar marks a refund payment whose ID is a Stellar transaction hash and can therefore be verified on-ledger.

Variables

View Source
var (
	// ErrInvalidConfig is returned by constructors when required configuration
	// is missing or malformed.
	ErrInvalidConfig = errors.New("invalid config")

	// ErrTOMLFetch is returned when the SEP-1 stellar.toml cannot be retrieved.
	ErrTOMLFetch = errors.New("toml fetch failed")

	// ErrTOMLValidation is returned when a fetched TOML is missing required
	// fields or fails sanity checks (network passphrase mismatch, signing key
	// rotation, etc.).
	ErrTOMLValidation = errors.New("toml validation failed")

	// ErrUnauthorized is returned for 401 responses from any authenticated
	// anchor endpoint. Callers should evict cached tokens and retry once.
	ErrUnauthorized = errors.New("unauthorized")

	// ErrInvalidAmount is returned when an anchor sends a decimal amount
	// string that cannot be parsed.
	ErrInvalidAmount = errors.New("invalid amount")
)

Sentinel errors. Wrap with fmt.Errorf("stellaranchor: ...: %w", err) at call sites. Anchor-specific consumers (e.g. moneygram, future SDKs) should re- wrap with their own prefix so log readers can tell which anchor failed.

Functions

func ChildAccountMemo

func ChildAccountMemo(treasuryPubkey string, accountIndex uint32) int64

ChildAccountMemo derives a deterministic positive int64 memo from the custodial treasury's public key and a user's BIP-44 Stellar derivation index (`accounts.account_index` — assigned by the existing user-account allocator; not minted here). The result is used as the SEP-10 memo so that a single treasury Stellar account can host many users without their JWTs or in-flight SEP-24 transactions cross-contaminating.

This is NOT the SEP-24 withdraw memo. The withdraw memo is supplied by the anchor on the SEP-24 transaction response (Transaction.WithdrawMemo) — that value is what we pass as the Stellar payment memo when sending USDC to the anchor, and what MG sends back on refund. The refund-ingest worker matches inbound payments against `loans.ramp_withdraw_memo`, not against this hash. See §13 of docs/moneygram-integration.md.

Properties:

  • Deterministic across runs and processes.
  • Treasury-scoped: testnet/preview/mainnet treasuries produce disjoint memo spaces.
  • Fits Stellar MEMO_ID and MoneyGram's "≤64-bit positive integer" constraint.

Collision probability at 10^9 users on a 63-bit space (the high bit is masked off to keep the value positive) is ≈ 5×10^-11. A collision causes JWT scope mis-routing — log loudly and resolve manually if observed.

func CountryISO3

func CountryISO3(iso2 string) string

CountryISO3 returns the ISO-3166-1 alpha-3 code for the given alpha-2 code, or "" if the alpha-2 is not in the map. Callers should omit the SEP-9 address_country_code field entirely when this returns "" — sending a wrong or guessed code is worse than sending none.

func DefaultTOMLClient

func DefaultTOMLClient() *http.Client

DefaultTOMLClient returns an http.Client suitable for fetching SEP-1 TOMLs: short timeout, no redirects to other hosts. Use this if you don't already have an http.Client to inject.

func SplitFullName

func SplitFullName(fullName string) (first, last string)

SplitFullName splits a full-name string on the first whitespace into (first, last). One-token names become (token, ""); empty input yields ("", ""). Used to map a single stored full_name field onto SEP-9 first_name / last_name keys.

Types

type AnchorClient

type AnchorClient struct {
	// contains filtered or unexported fields
}

AnchorClient implements the subset of SEP-24 needed for cash-pickup off-ramp: /transactions/withdraw/interactive and /transaction.

func NewAnchorClient

func NewAnchorClient(cfg AnchorConfig, httpClient *http.Client, logger *slog.Logger) (*AnchorClient, error)

NewAnchorClient validates configuration. httpClient may be nil; logger may be nil.

func (*AnchorClient) GetTransaction

func (c *AnchorClient) GetTransaction(ctx context.Context, jwt, txID string) (*Transaction, error)

GetTransaction calls GET /transaction?id={txID} with the given JWT.

func (*AnchorClient) InitiateWithdrawal

func (c *AnchorClient) InitiateWithdrawal(ctx context.Context, jwt string, req WithdrawRequest) (*WithdrawResponse, error)

InitiateWithdrawal calls POST /transactions/withdraw/interactive with the given JWT (from AuthClient/JWTCache), the USDC amount, and any prefilled SEP-9 customer fields. Returns the interactive URL and MG transaction ID.

type AnchorConfig

type AnchorConfig struct {
	TransferServerURL string // from TOML's TRANSFER_SERVER_SEP0024
}

AnchorConfig configures the SEP-24 client.

type AuthClient

type AuthClient struct {
	// contains filtered or unexported fields
}

AuthClient implements the SEP-10 challenge/co-sign/token flow against a MoneyGram anchor in custodial mode (single treasury Stellar account, per- user int64 memo).

func NewAuthClient

func NewAuthClient(cfg AuthConfig, httpClient *http.Client, logger *slog.Logger) (*AuthClient, error)

NewAuthClient validates the config and constructs a client. httpClient may be nil; logger may be nil.

func (*AuthClient) Authenticate

func (c *AuthClient) Authenticate(ctx context.Context, childMemo int64) (AuthResult, error)

Authenticate runs the SEP-10 challenge/co-sign/submit flow for a given child-memo and returns the JWT plus its expiry. Callers should cache the result via JWTCache rather than calling this on every SEP-24 request.

func (*AuthClient) TreasuryAddress

func (c *AuthClient) TreasuryAddress() string

TreasuryAddress returns the custodial G... account ID derived from the configured treasury secret. Useful for diagnostics and Stellar payments.

type AuthConfig

type AuthConfig struct {
	WebAuthEndpoint   string // from TOML
	ServerSigningKey  string // from TOML — used to verify the challenge signature
	NetworkPassphrase string // from TOML, e.g. "Public Global Stellar Network ; September 2015"
	HomeDomain        string // e.g. "stellar.moneygram.com"
	WebAuthDomain     string // typically equal to HomeDomain unless MG runs the auth server on a different host

	// TreasurySecret is the S... seed of the custodial treasury account.
	// Held in memory only — never logged. The corresponding G... address
	// becomes the SEP-10 `account` parameter.
	TreasurySecret string

	// FallbackTokenTTL is used when the issued JWT has no `exp` claim.
	// Defaults to 23h to stay safely under MG's typical 24h validity.
	FallbackTokenTTL time.Duration

	// HTTPTimeout caps the duration of any single SEP-10 HTTP exchange.
	// Defaults to 15s.
	HTTPTimeout time.Duration
}

AuthConfig configures the SEP-10 client.

type AuthResult

type AuthResult struct {
	JWT       string
	ExpiresAt time.Time
}

AuthResult is the output of a successful SEP-10 round trip.

type Client

type Client struct {
	Auth     *AuthClient
	Anchor   *AnchorClient
	JWTCache *JWTCache
	// contains filtered or unexported fields
}

Client is the top-level Stellar anchor handle: SEP-10 auth + SEP-24 anchor + JWT cache, glued together. Anchor-specific SDKs (e.g. moneygram) compose this client with their own REST/OAuth/FX extras.

func New

func New(cfg Config) (*Client, error)

New wires the sub-clients. Returns an error if AuthClient or AnchorClient validation fails.

func (*Client) GetTransaction

func (c *Client) GetTransaction(ctx context.Context, childMemo int64, txID string) (*Transaction, error)

GetTransaction wraps Anchor.GetTransaction with the same JWT cache and 401-retry semantics as InitiateWithdrawal.

func (*Client) InitiateWithdrawal

func (c *Client) InitiateWithdrawal(ctx context.Context, childMemo int64, req WithdrawRequest) (*WithdrawResponse, error)

InitiateWithdrawal fetches a JWT for childMemo and calls Anchor.InitiateWithdrawal. On a 401 it evicts the cached token and retries once.

func (*Client) Token

func (c *Client) Token(ctx context.Context, childMemo int64) (string, error)

Token returns a cached SEP-10 JWT for the given child memo, refreshing when stale.

func (*Client) TreasuryAddress

func (c *Client) TreasuryAddress() string

TreasuryAddress returns the custodial G... account ID.

func (*Client) USDCIssuer

func (c *Client) USDCIssuer() string

USDCIssuer returns the USDC issuer pubkey from Config.

type Config

type Config struct {
	HomeDomain        string // e.g. "stellar.moneygram.com"
	WebAuthEndpoint   string // SEP-1 WEB_AUTH_ENDPOINT
	TransferServerURL string // SEP-1 TRANSFER_SERVER_SEP0024
	ServerSigningKey  string // SEP-1 SIGNING_KEY
	NetworkPassphrase string // SEP-1 NETWORK_PASSPHRASE
	USDCIssuer        string // expected USDC issuer (validated against TOML upstream)
	TreasurySecret    string // S... seed of the custodial treasury account

	HTTPClient *http.Client
	Logger     *slog.Logger

	// JWTSafetyMargin tunes the JWT cache. Defaults to 60s.
	JWTSafetyMargin time.Duration
}

Config bundles the SEP-1 derived values plus the custodial treasury secret needed to talk to a Stellar anchor. Fetch the TOML at startup via FetchTOML, validate it via TOML.Validate, then pass the relevant fields here — surfacing rotation as an explicit re-Validate at boot rather than silent runtime drift.

type Currency

type Currency struct {
	Code            string `toml:"code"`
	Issuer          string `toml:"issuer"`
	IsAssetAnchored bool   `toml:"is_asset_anchored"`
	AnchorAssetType string `toml:"anchor_asset_type"`
	AnchorAsset     string `toml:"anchor_asset"`
	Name            string `toml:"name"`
	Desc            string `toml:"desc"`
}

Currency is one entry from the SEP-1 [[CURRENCIES]] array.

type Customer

type Customer struct {
	FirstName          string `json:"first_name,omitempty"`
	LastName           string `json:"last_name,omitempty"`
	MobileNumber       string `json:"mobile_number,omitempty"` // E.164, e.g. "+254712345678"
	BirthDate          string `json:"birth_date,omitempty"`    // YYYY-MM-DD
	Address            string `json:"address,omitempty"`       // line1[, line2]
	City               string `json:"city,omitempty"`
	PostalCode         string `json:"postal_code,omitempty"`
	AddressCountryCode string `json:"address_country_code,omitempty"` // ISO-3, e.g. "KEN"
}

Customer carries the subset of SEP-9 fields MoneyGram honours when passed at SEP-24 withdrawal initiation. MoneyGram silently ignores SEP-9 keys it does not support (email_address, id_*, occupation, etc.).

All fields are optional from MoneyGram's perspective; populate as much as you can to reduce friction in MoneyGram's interactive webview.

type JWTCache

type JWTCache struct {
	// contains filtered or unexported fields
}

JWTCache is an in-memory store of SEP-10 JWTs keyed by child-memo. Each entry is held until its parsed `exp` minus SafetyMargin. Safe for concurrent use.

func NewJWTCache

func NewJWTCache(auth *AuthClient, safetyMargin time.Duration) *JWTCache

NewJWTCache wraps an AuthClient with caching. SafetyMargin is how long before nominal expiry the cache treats a token as stale and refreshes; defaults to 60s when zero.

func (*JWTCache) Get

func (c *JWTCache) Get(ctx context.Context, childMemo int64) (string, error)

Get returns a valid JWT for the given child-memo, calling AuthClient only when the cache is empty or stale.

func (*JWTCache) Invalidate

func (c *JWTCache) Invalidate(childMemo int64)

Invalidate evicts the cached token for childMemo. Use after a 401 from a SEP-24 endpoint — the next Get call will fetch a fresh token.

func (*JWTCache) InvalidateAll

func (c *JWTCache) InvalidateAll()

InvalidateAll clears the entire cache. Useful when the SEP-1 SIGNING_KEY has rotated and every cached token is now invalid.

type RefundPayment

type RefundPayment struct {
	// ID is the Stellar transaction hash when IDType is "stellar", or an
	// anchor-internal identifier when it is "external".
	ID     string `json:"id"`
	IDType string `json:"id_type"`
	Amount string `json:"amount"`
	Fee    string `json:"fee"`
}

RefundPayment is one individual refund payment within a Refunds object.

type Refunds

type Refunds struct {
	AmountRefunded string          `json:"amount_refunded"`
	AmountFee      string          `json:"amount_fee"`
	Payments       []RefundPayment `json:"payments"`
}

Refunds describes money returned to the user for a transaction. For a withdrawal this is the anchor sending our USDC back on Stellar.

Amounts are denominated in amount_in_asset (USDC for our withdrawals) and are the anchor's own account of what it did. They are not reliable enough to settle against — read NetRefundedStroops before using them.

func (*Refunds) NetRefundedStroops

func (r *Refunds) NetRefundedStroops() (int64, error)

NetRefundedStroops is the anchor's stated refund total in stroops, taken as AmountRefunded less AmountFee.

Treat this as a cross-check, never as the amount to settle against. It is only as good as the anchor's bookkeeping, and MoneyGram's does not hold: it reports AmountRefunded already net of the withdrawal fee and then repeats that fee in AmountFee, so this subtracts it twice and understates what MG actually sent. Take the figure that moves money from the ledger.

Parsing is exact — SEP-24 amounts are decimal strings and float conversion would lose stroops on values like "49.9999999".

func (*Refunds) StellarPayments

func (r *Refunds) StellarPayments() []RefundPayment

StellarPayments returns the refund payments settled on Stellar, whose IDs are transaction hashes.

An empty IDType counts as Stellar: the field is required by SEP-24 but anchors omit it in practice, and for a withdrawal refund the funds can only come back over Stellar.

type Status

type Status string

Status is the SEP-24 transaction status.

const (
	StatusIncomplete                  Status = "incomplete"
	StatusPendingUserTransferStart    Status = "pending_user_transfer_start"
	StatusPendingUserTransferComplete Status = "pending_user_transfer_complete"
	StatusPendingExternal             Status = "pending_external"
	StatusPendingAnchor               Status = "pending_anchor"
	StatusOnHold                      Status = "on_hold"
	StatusPendingStellar              Status = "pending_stellar"
	StatusPendingTrust                Status = "pending_trust"
	StatusPendingUser                 Status = "pending_user"
	StatusCompleted                   Status = "completed"
	StatusRefunded                    Status = "refunded"
	StatusExpired                     Status = "expired"
	StatusNoMarket                    Status = "no_market"
	StatusTooSmall                    Status = "too_small"
	StatusTooLarge                    Status = "too_large"
	StatusError                       Status = "error"
)

SEP-24 transaction states. The full set documented by [SEP-0024 protocol](https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0024.md) in the "Shared fields for both deposits and withdrawals" section.

func (Status) Terminal

func (s Status) Terminal() bool

Terminal returns true when no further status transitions are possible — pollers should stop polling on terminal statuses.

type TOML

type TOML struct {
	Version             string     `toml:"VERSION"`
	NetworkPassphrase   string     `toml:"NETWORK_PASSPHRASE"`
	SigningKey          string     `toml:"SIGNING_KEY"`
	WebAuthEndpoint     string     `toml:"WEB_AUTH_ENDPOINT"`
	TransferServerSEP24 string     `toml:"TRANSFER_SERVER_SEP0024"`
	Accounts            []string   `toml:"ACCOUNTS"`
	Currencies          []Currency `toml:"CURRENCIES"`
}

TOML is the parsed subset of a Stellar SEP-1 stellar.toml that matters for the MoneyGram Ramps integration. Fields not used by this SDK are not captured — extend if your wallet needs more.

func FetchTOML

func FetchTOML(ctx context.Context, httpClient *http.Client, homeDomain string) (*TOML, error)

FetchTOML retrieves and parses the SEP-1 stellar.toml served at https://{homeDomain}/.well-known/stellar.toml. It does NOT validate semantically — call TOML.Validate to enforce environment-specific expectations (passphrase, signing key, USDC issuer).

httpClient may be nil; http.DefaultClient is used in that case. The response is capped at 64 KiB — well above the size of any well-formed stellar.toml — to avoid pathological responses exhausting memory.

func (*TOML) AssetIssuer

func (t *TOML) AssetIssuer(code string) string

AssetIssuer returns the issuer pubkey for the named currency code, or "" if the code is not in the [[CURRENCIES]] list.

func (*TOML) Validate

func (t *TOML) Validate(opts ValidateOptions) error

Validate returns nil if the TOML carries every field needed to drive a SEP-10 + SEP-24 withdrawal flow against MoneyGram, and matches any expectations supplied in opts.

type Transaction

type Transaction struct {
	ID                    string `json:"id"`
	Kind                  string `json:"kind"` // "deposit" | "withdrawal"
	Status                Status `json:"status"`
	StatusEta             int64  `json:"status_eta,omitempty"`
	AmountIn              string `json:"amount_in,omitempty"`
	AmountInAsset         string `json:"amount_in_asset,omitempty"`
	AmountOut             string `json:"amount_out,omitempty"`
	AmountOutAsset        string `json:"amount_out_asset,omitempty"`
	AmountFee             string `json:"amount_fee,omitempty"`
	AmountFeeAsset        string `json:"amount_fee_asset,omitempty"`
	StartedAt             string `json:"started_at,omitempty"`
	CompletedAt           string `json:"completed_at,omitempty"`
	StellarTransactionID  string `json:"stellar_transaction_id,omitempty"`
	ExternalTransactionID string `json:"external_transaction_id,omitempty"` // cash-pickup reference
	WithdrawAnchorAccount string `json:"withdraw_anchor_account,omitempty"`
	WithdrawMemo          string `json:"withdraw_memo,omitempty"`
	WithdrawMemoType      string `json:"withdraw_memo_type,omitempty"` // typically "id"
	MoreInfoURL           string `json:"more_info_url,omitempty"`
	Message               string `json:"message,omitempty"`

	// Refunded is the deprecated SEP-24 boolean, kept because some anchors
	// still emit it. Refunds is the authoritative field.
	Refunded bool     `json:"refunded,omitempty"`
	Refunds  *Refunds `json:"refunds,omitempty"`
}

Transaction is the SEP-24 transaction object returned from /transaction. Field set is intentionally a superset of what we need to drive the off- ramp state machine — some fields are populated only at certain statuses.

type ValidateOptions

type ValidateOptions struct {
	ExpectedNetworkPassphrase string
	ExpectedSigningKey        string // optional; "" disables the check
	ExpectedUSDCIssuer        string // optional; "" disables the check
}

ValidateOptions configures TOML.Validate. Set ExpectedSigningKey when you have committed to a specific MG signing key for the environment — a mismatch should be a hard fail rather than silent drift.

type WithdrawRequest

type WithdrawRequest struct {
	AssetCode string   // "USDC"
	Amount    string   // decimal USD, e.g. "50.00" — required for custodial wallets
	Lang      string   // ISO-639-1, defaults to "en"
	Account   string   // funds wallet G... address — the withdrawal payment source
	Customer  Customer // optional SEP-9 prefill
}

WithdrawRequest is the body of a SEP-24 /transactions/withdraw/interactive call. Customer fields are SEP-9 — see Customer.

type WithdrawResponse

type WithdrawResponse struct {
	Type string `json:"type"` // typically "interactive_customer_info_needed"
	URL  string `json:"url"`  // interactive webview URL — SMS this to the user
	ID   string `json:"id"`   // MoneyGram's transaction ID — persist and poll
}

WithdrawResponse is what MoneyGram returns from /transactions/withdraw/interactive.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL