Stellar Actions
Build, sign, and submit Stellar vault deposits and redeems with the SDK's XDR-based transaction flow.
Overview
Stellar vault interactions live on the Stellar adapter (sdk.stellar), not on the higher-level vaults module (which is read-only for Stellar). Unlike EVM — where a deposit is signed and broadcast in one call — Stellar write methods follow a build → sign → submit flow:
sdk.stellar.vaultDeposit()/sdk.stellar.vaultRedeem()build an unsigned transaction and return it as a base64-encoded XDR string.- You sign that XDR with the user's Stellar wallet (e.g. Freighter).
sdk.stellar.submitTransaction()broadcasts the signed XDR and resolves to the transaction hash once the network confirms it.
The SDK does not hardcode vault names like earnUSDC or earnXLM. You pass a vault's contract ID (the C… address); resolve it from the vault listing (see Get Vault Data below).
sdk.stellarruns against Stellar mainnet. For testnet, see Networks at the bottom of this page.
Setup
sdk.stellar is always available — no extra provider configuration is required, since Stellar vault metadata is served from the Upshift backend.
import AugustSDK from '@augustdigital/sdk';
const sdk = new AugustSDK({
appName: '<APP_NAME>', // required: stable kebab-case slug identifying your app
});
// Access the Stellar adapter
const stellar = sdk.stellar;
AugustSDKis the package's default export — import it asimport AugustSDK from '@augustdigital/sdk'(not a named import).appNameis required. Noprovidersentry is needed for Stellar, since its vault data is served from the Upshift backend; addprovidersonly if you also read EVM/Solana vaults.
Signing happens in your app via the user's wallet — there is no setWalletProvider step for Stellar. Whatever wallet you use must expose a "sign XDR" call (Freighter's signTransaction, for example).
Adapter API
Methods
| Method | Returns | Description |
|---|---|---|
vaultDeposit({ contractId, amount, senderAddress }) | Promise<string> (XDR) | Build an unsigned deposit transaction. |
vaultRedeem({ contractId, shares, receiverAddress }) | Promise<string> (XDR) | Build an unsigned redeem transaction. |
submitTransaction(signedXdr) | Promise<string> (tx hash) | Submit a signed XDR and poll until confirmed. |
getUserPosition(vaultAddress, walletAddress) | Promise<IStellarUserPosition | null> | Read the user's on-chain share balance. |
convertToShares(vaultAddress, rawAmount) | Promise<string | null> | Preview shares a deposit amount would yield. |
isStellarAddress(address) | boolean | Validate a Stellar address (C… or G…). |
getExplorerLink(id, type?) | string | Build a Stellar explorer URL. |
Get Vault Data
Stellar vaults appear in the standard listing with chain_type === 'stellar'. Use a vault's address as the contractId for deposit/redeem.
// List all vaults, then narrow to Stellar
const vaults = await sdk.getVaults();
const stellarVaults = vaults.filter((v) => v.chain_type === 'stellar');
// e.g. find "earnUSDC" by name/symbol
const earnUsdc = stellarVaults.find((v) => v.name?.includes('earnUSDC'));
const contractId = earnUsdc?.address; // "C…"Get User Positions
To include a user's Stellar balances in the unified positions call, pass stellarWallet (the user's G… address):
const positions = await sdk.getVaultPositions({
stellarWallet: 'G...',
});
positions.forEach((position) => {
console.log(`Vault: ${position.vault}`);
console.log(`Balance: ${position.walletBalance.normalized}`);
});For a direct on-chain read of a single vault, use the adapter (this is also how you obtain the shares value needed to redeem):
const position = await sdk.stellar.getUserPosition(contractId, 'G...');
// { shares: string, decimals: number, decimalsFromFallback?: boolean } | nullSizing a redeem from
getUserPosition: ifposition.decimalsFromFallback === true, the on-chaindecimals()read failed anddecimalsis a fabricated fallback (7). Do not trust it to size a redeem — against an offset vault (share decimals = asset + offset) this under-redeems by10^offset. Refuse and retry instead.
Vault Deposit
Build an unsigned deposit transaction. This is a self-deposit: internally the receiver, from, and operator roles are all set to senderAddress.
sdk.stellar.vaultDeposit({
contractId: string; // Stellar vault contract ("C…")
amount: string; // deposit amount in the token's smallest unit
senderAddress: string; // user's Stellar account ("G…")
}): Promise<string> // → unsigned base64 XDRParameters
| Parameter | Type | Required | Description |
|---|---|---|---|
contractId | string | Yes | Stellar vault contract address (C…). |
amount | string | Yes | Amount in the deposit token's smallest unit (e.g. 1 USDC @ 7 dp → "10000000"). |
senderAddress | string | Yes | User's Stellar account (G…); pays for and receives the deposit. |
Example
// 1. Build the unsigned transaction
const unsignedXdr = await sdk.stellar.vaultDeposit({
contractId,
amount: '10000000', // 1 USDC at 7 decimals
senderAddress: 'G...',
});
// 2. Sign with the user's wallet (Freighter shown here)
import { signTransaction } from '@stellar/freighter-api';
const { signedTxXdr } = await signTransaction(unsignedXdr, { networkPassphrase: '...' });
// 3. Submit and wait for confirmation
const txHash = await sdk.stellar.submitTransaction(signedTxXdr);
console.log('Deposit confirmed:', txHash);Optionally preview the shares a deposit would mint before building it:
const expectedShares = await sdk.stellar.convertToShares(contractId, '10000000');
// string | nullVault Redeem
Build an unsigned redeem transaction. Redeem burns shares (not asset amount), so read the user's position first. This is a self-redeem: receiver, owner, and operator roles are all set to receiverAddress.
sdk.stellar.vaultRedeem({
contractId: string; // Stellar vault contract ("C…")
shares: string; // amount of shares to redeem, in smallest unit
receiverAddress: string; // user's Stellar account ("G…")
}): Promise<string> // → unsigned base64 XDRParameters
| Parameter | Type | Required | Description |
|---|---|---|---|
contractId | string | Yes | Stellar vault contract address (C…). |
shares | string | Yes | Shares to redeem, in the share token's smallest unit. |
receiverAddress | string | Yes | User's Stellar account (G…); receives the redeemed assets. |
Example
// 1. Read the user's share balance
const position = await sdk.stellar.getUserPosition(contractId, 'G...');
if (!position || position.shares === '0') {
throw new Error('No balance to redeem');
}
if (position.decimalsFromFallback) {
throw new Error('Decimals unresolved — refusing to size redeem with fallback decimals');
}
// 2. Build the unsigned transaction (redeem all)
const unsignedXdr = await sdk.stellar.vaultRedeem({
contractId,
shares: position.shares,
receiverAddress: 'G...',
});
// 3. Sign with the user's wallet, then submit
const { signedTxXdr } = await signTransaction(unsignedXdr, { networkPassphrase: '...' });
const txHash = await sdk.stellar.submitTransaction(signedTxXdr);
console.log('Redeem confirmed:', txHash);Submit a Signed Transaction
submitTransaction broadcasts a signed XDR and polls until the network confirms it as successful, returning the transaction hash. The XDR must be signed for the same network the adapter is using.
const txHash = await sdk.stellar.submitTransaction(signedXdr);Error Handling
submitTransaction throws typed Upshift SDK errors:
AugustTimeoutError— the transaction was not confirmed within the poll budget.AugustSDKError— the RPC rejected the submission, or the transaction confirmed as failed. Inspecterr.contextfor{ network, status }and, when the result XDR decodes, aresultCodestring carrying the transaction-level reason (e.g."txBadSeq","txTooLate").resultCodeisundefinedwhen it cannot be decoded.
import { AugustSDKError } from '@augustdigital/sdk';
try {
const txHash = await sdk.stellar.submitTransaction(signedXdr);
} catch (err) {
if (err instanceof AugustSDKError && err.context?.resultCode === 'txBadSeq') {
// Stale sequence number — rebuild the transaction and resubmit.
} else {
throw err;
}
}Curator Notifications
Stellar vaults are instant-redeem only — there is no withdrawal queue — so a curator whose depositors cannot redeem has no other way to find out. When a redemption fails, the SDK reports it to the Upshift API (the same base URL it reads vault data from), which validates the report and forwards it to the vault curator's own Telegram group. Curators are onboarded per vault, so a vault nobody has opted in is never reported on.
Two cases are reported:
- the vault rejecting the redeem at build time — either a Soroban simulation failure or a vault whose ledger state has been archived and needs restoring; and
- a submitted redeem whose operation ran and failed (
resultCode === 'txFailed').
The body sent is exactly: an event name, the chain family ("stellar"), which of the two cases it was, the network, the vault contract ID, the redeeming account, the share amount (unscaled), an ISO timestamp, the failure reason (secrets scrubbed, capped at 1500 characters — on the submission path this is the decoded transaction result, not prose), your appName, and for case 2 the transaction hash and result code. Repeated attempts at the same failure on the same vault and account collapse to one report per 10 minutes.
Deliberately not reported, because none of them is a vault rejecting a redemption: an invalid address, an unfunded Stellar account, an RPC outage, a broadcast the RPC rejected, a confirmation timeout, and any transaction-level failure where the redeem never executed (txBadSeq, txInsufficientBalance, txTooLate, or an undecodable result code). Deposits are never reported. Note that a redeem which exhausts its Soroban resource budget is reported — the operation ran, so it arrives as txFailed.
Nothing is sent for networks other than mainnet — unless you set curatorAlerts.endpoint, which lifts that restriction so your own relay can receive testnet failures. Nothing is sent when monitoring.env is set to anything other than PROD, when NODE_ENV is development or test, or when the page is served from a loopback, private, or link-local host — so your CI and local dev runs stay silent. That last check covers:
| Kind | Covered |
|---|---|
| Reserved names | localhost, any *.localhost, any *.local |
| IPv4 loopback | 127.0.0.0/8 (not just 127.0.0.1), 0.0.0.0/8 |
| IPv4 private | 10.0.0.0/8, 172.16.0.0/12 (Docker's default bridge lives here), 192.168.0.0/16 |
| IPv4 link-local | 169.254.0.0/16 |
| IPv6 | [::1] and ::, unique-local fc00::/7, link-local fe80::/10, and IPv4-mapped forms of the above |
A public host that merely contains a reserved name — localhost.example.com — is treated as a real deployment and does report.
The hostname check is what covers browsers: bundlers strip process.env, so NODE_ENV is not readable there and cannot be relied on to keep a next dev tab quiet.
Note that monitoring.env defaults to PROD, so reporting is on unless you say otherwise; and code that drives the Stellar namespace without constructing an AugustSDK has no monitoring.env at all, where NODE_ENV and the hostname are the only gates.
Setting
curatorAlerts.enabled: truebypasses all three environment gates —monitoring.env,NODE_ENV, and the local-host check. It does not lift the mainnet-only restriction; onlyendpointdoes that. It is a force-on, not a "yes, use the defaults": aDEVor test integration that sets it and then fails a mainnet redeem will page a real curator. Leave it unset to get the safe defaults, and if you set it to exercise the path, pointendpointat a relay of your own. TheAUGUST_SDK_DISABLE_CURATOR_ALERTSenv var still overrides it.
Enabled by default. To turn it off:
const sdk = new AugustSDK({
appName: '<APP_NAME>',
keys: { august: 'YOUR_AUGUST_API_KEY' },
monitoring: { curatorAlerts: { enabled: false } },
});In Node you can also set AUGUST_SDK_DISABLE_CURATOR_ALERTS (to 1, true, yes, or on). If you call the Stellar namespace directly rather than constructing an AugustSDK (see Networks below), opt out with the top-level configureCuratorAlerts — in a browser this is the only way, since environment variables are unreadable there:
import { configureCuratorAlerts } from '@upshiftfinance/sdk';
configureCuratorAlerts({ enabled: false });Constructing an
AugustSDKafterwards resets this setting, so call it after any SDK construction, not before.
The reporting endpoint is chain-agnostic — the chain travels in the payload — but it only serves the families Upshift has enabled, which today means Stellar alone. Curators are onboarded on Upshift's side; there is nothing to configure in the SDK to route notifications to a particular channel. Ask your Upshift contact to enable a vault, and to allowlist the origin your users load the app from: delivery is a cross-origin browser request, so an origin that is not on the API's CORS allowlist loses these reports at the preflight. The
redeemcheck on the submit path reads the transaction's contract call rather than a list of Upshift vaults, so submitting an unrelated Soroban contract's failedredeemthroughsubmitTransactionalso sends a report; the API discards any vault it does not have an opted-in curator for.
Networks
sdk.stellar is hardwired to mainnet. To target testnet, use the Stellar namespace directly with an explicit network:
import { Stellar } from '@augustdigital/sdk';
// Build an unsigned deposit on testnet
const unsignedXdr = await Stellar.actions.handleStellarDeposit({
contractId,
amount: '10000000',
senderAddress: 'G...',
network: 'testnet',
});
// Submit on testnet
const txHash = await Stellar.submit.submitStellarTransaction(signedXdr, 'testnet');Notes & Caveats
- Self-deposit / self-redeem only. All role addresses are set to the single account you pass; there is no third-party/operator variant.
- Standard vault ABI assumed. Redeem assumes the deployed vault exposes
redeem(shares, receiver, owner, operator). A divergent ABI surfaces as a generic Soroban simulation error. - Read-only on the vaults module. Available redemptions and redemption history are not yet supported for Stellar vaults; deposit/redeem are available only on
sdk.stellar.