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
- Variables
- func ChildAccountMemo(treasuryPubkey string, accountIndex uint32) int64
- func CountryISO3(iso2 string) string
- func DefaultTOMLClient() *http.Client
- func SplitFullName(fullName string) (first, last string)
- type AnchorClient
- func (c *AnchorClient) GetTransaction(ctx context.Context, jwt, txID string) (*Transaction, error)
- func (c *AnchorClient) InitiateDeposit(ctx context.Context, jwt string, req DepositRequest) (*DepositResponse, error)
- func (c *AnchorClient) InitiateWithdrawal(ctx context.Context, jwt string, req WithdrawRequest) (*WithdrawResponse, error)
- type AnchorConfig
- type AuthClient
- type AuthConfig
- type AuthResult
- type Client
- func (c *Client) GetTransaction(ctx context.Context, childMemo int64, txID string) (*Transaction, error)
- func (c *Client) InitiateDeposit(ctx context.Context, childMemo int64, req DepositRequest) (*DepositResponse, error)
- func (c *Client) InitiateWithdrawal(ctx context.Context, childMemo int64, req WithdrawRequest) (*WithdrawResponse, error)
- func (c *Client) Token(ctx context.Context, childMemo int64) (string, error)
- func (c *Client) TreasuryAddress() string
- func (c *Client) USDCIssuer() string
- type Config
- type Currency
- type Customer
- type DepositRequest
- type DepositResponse
- type FeeDetail
- type FeeDetails
- type JWTCache
- type RefundPayment
- type Refunds
- type Status
- type TOML
- type Transaction
- type ValidateOptions
- type WithdrawRequest
- type WithdrawResponse
Constants ¶
const RefundIDTypeStellar = "stellar"
RefundIDTypeStellar marks a refund payment whose ID is a Stellar transaction hash and can therefore be verified on-ledger.
Variables ¶
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") // 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 these with an oops builder at the call site — see tomlErr, authErr and anchorErr — so the error carries a domain, a code and the attributes needed to diagnose it, while errors.Is against the sentinel keeps working for callers that branch on the kind of failure.
Functions ¶
func ChildAccountMemo ¶
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 ¶
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 ¶
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 ¶
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) InitiateDeposit ¶ added in v1.1.2
func (c *AnchorClient) InitiateDeposit(ctx context.Context, jwt string, req DepositRequest) (*DepositResponse, error)
InitiateDeposit calls POST /transactions/deposit/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.
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 ¶
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 ¶
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) InitiateDeposit ¶ added in v1.1.2
func (c *Client) InitiateDeposit(ctx context.Context, childMemo int64, req DepositRequest) (*DepositResponse, error)
InitiateDeposit fetches a JWT for childMemo and calls Anchor.InitiateDeposit, with the same 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 ¶
Token returns a cached SEP-10 JWT for the given child memo, refreshing when stale.
func (*Client) TreasuryAddress ¶
TreasuryAddress returns the custodial G... account ID.
func (*Client) USDCIssuer ¶
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.).
type DepositRequest ¶ added in v1.1.2
type DepositRequest struct {
AssetCode string // "USDC"
Amount string // decimal USDC, e.g. "50.00" — required for custodial wallets
Lang string // ISO-639-1, defaults to "en"
Account string // destination wallet G... address the anchor credits
Customer Customer // optional SEP-9 prefill
// Memo asks the anchor to attach a memo to the Stellar payment it makes to
// Account. SEP-24 offers it precisely so a client can match inbound
// payments to its own records, and it is independent of the SEP-10 memo:
// that one identifies the borrower, this one can identify the loan.
//
// Optional in the specification, so an anchor may ignore it. MoneyGram
// does not: the sandbox echoes it on deposit_memo from the first poll, at
// status incomplete, well before the borrower finishes the webview.
//
// Every borrower's deposit lands on the same Account, so without this the
// payments are distinguishable on-chain only by amount and timing.
Memo string
MemoType string // "text", "id" or "hash"; defaults to "text" when Memo is set
}
DepositRequest is the body of a SEP-24 /transactions/deposit/interactive call. Customer fields are SEP-9 — see Customer.
type DepositResponse ¶ added in v1.1.2
type DepositResponse 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
}
DepositResponse is what MoneyGram returns from /transactions/deposit/interactive.
Same shape as WithdrawResponse, but the user's obligation is reversed: they must complete the webview to pick an agent and commit before any cash can be paid in, and nothing on our side compels them to.
type FeeDetail ¶ added in v1.1.2
type FeeDetail struct {
Name string `json:"name"`
Description string `json:"description,omitempty"`
Amount string `json:"amount"`
}
FeeDetail is one named line of a fee breakdown.
type FeeDetails ¶ added in v1.1.2
type FeeDetails struct {
Total string `json:"total"`
Asset string `json:"asset"`
Details []FeeDetail `json:"details,omitempty"`
}
FeeDetails is the SEP-24 breakdown of what an anchor charges.
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 ¶
Get returns a valid JWT for the given child-memo, calling AuthClient only when the cache is empty or stale.
func (*JWTCache) Invalidate ¶
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.
func (*Refunds) NetRefundedStroops ¶
NetRefundedStroops is the anchor's stated refund total in stroops, taken as AmountRefunded less AmountFee.
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.
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 ¶
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).
func (*TOML) AssetIssuer ¶
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"`
// Deposit-side fields. DepositMemo is the memo the anchor will use to
// transfer the asset to To, and MoneyGram populates it with whatever
// DepositRequest.Memo asked for — verified against the sandbox, which
// echoed "LR-1A03C985D8A-61ce" at status incomplete.
//
// It is a statement of intent until a payment actually exists: the field
// says what the anchor means to attach, not what landed on the ledger.
//
// This is never ChildAccountMemo. That one scopes the SEP-10 session and
// identifies the borrower; this one is per-transaction and is what makes
// concurrent deposits to a shared account distinguishable on-chain.
To string `json:"to,omitempty"` // account the anchor credits
DepositMemo string `json:"deposit_memo,omitempty"`
DepositMemoType string `json:"deposit_memo_type,omitempty"`
// UserActionRequiredBy is MoneyGram's own deadline for the borrower to act.
// It is theirs, not ours: a deposit lapses on this timestamp whatever our
// configured window says, so it is the figure to schedule and quote against.
UserActionRequiredBy string `json:"user_action_required_by,omitempty"`
// FeeDetails is what the borrower pays on top of the amount. SEP-24 carries
// it separately from amount_in, so a quote that ignores it understates what
// they hand over at the counter.
FeeDetails *FeeDetails `json:"fee_details,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.