logo

Learn About Hastra

Hastra SOL Programmatic Integration Guide

This guide explains the programmatic lifecycle utilizing Hastra on Solana. For partners, this process converts liquid capital USDC into wYLDS, which can then be staked into PRIME.
The Hastra Solana protocol consists of two interconnected Anchor programs:
  • vault-mint (9WUyNREiPDMgwMh5Gt81Fd3JpiCKxpjZ5Dpq9Bo1RhMV) — accepts USDC and issues wYLDS at a 1:1 ratio.
  • vault-stake (97V7JsExNC6yFWu5KjK1FLfVkNVvtMpAFL5QkLWKEGxY) — accepts wYLDS and issues PRIME based on a Chainlink-sourced exchange rate.

1. The Asset Lifecycle: USDC → wYLDS

When a team "mints" on Hastra Solana, they are depositing USDC into the vault-mint program to receive wYLDS (Wrapped YLDS).
  • wYLDS is the base-layer wrapped token on Solana, acting as a custodial wrapper for YLDS, the underlying SEC-registered stablecoin. It acts as a 1:1 receipt for the USDC deposited into the vault token account.
  • Context: Unlike a simple swap, this "Deposit" moves USDC into a program-controlled vault token account (PDA-owned), and the program mints wYLDS to the user.

PDA Derivations (vault-mint)

TypeScript (Anchor)
// Derive the essential PDAs for the vault-mint program
const [configPda] = PublicKey.findProgramAddressSync(
[Buffer.from("config")],
vaultMintProgramId
);
const [vaultTokenAccountConfigPda] = PublicKey.findProgramAddressSync(
[Buffer.from("vault_token_account_config"), configPda.toBuffer()],
vaultMintProgramId
);
const [mintAuthorityPda] = PublicKey.findProgramAddressSync(
[Buffer.from("mint_authority")],
vaultMintProgramId
);

Programmatic Mint (USDC to wYLDS)

TypeScript (Anchor)
// Anchor program client for vault-mint
const vaultMint = new Program<HastraSolVaultMint>(idl, provider);
// Deposit USDC to receive wYLDS.
// The program transfers USDC from the user's ATA into the vault token account
// and mints wYLDS into the user's wYLDS ATA.
await vaultMint.methods
.deposit(new BN(amount))
.accountsStrict({
config: configPda,
vaultTokenAccount: vaultUsdcAta,          // program-owned USDC vault
vaultTokenAccountConfig: vaultTokenAccountConfigPda,
mint: wyldsMint,                           // wYLDS mint
mintAuthority: mintAuthorityPda,
signer: userPublicKey,
userVaultTokenAccount: userUsdcAta,        // user's USDC ATA
userMintTokenAccount: userWyldsAta,        // user's wYLDS ATA
tokenProgram: TOKEN_PROGRAM_ID,
})
.rpc();

2. The Staking Lifecycle: wYLDS → PRIME

PRIME represents a share-based position in the Democratized Prime pool. Unlike the ETH flow, on Solana yield does not require a manual claim — it accrues through an appreciating Chainlink-sourced exchange rate.
  • Context: Yield is generated off-chain on the Provenance side (via the Demo Prime HELOC pool) and bridged back to Solana as YLDS/wYLDS.
  • Mechanism: Periodically, the vault-stake program receives newly minted wYLDS via a CPI into vault-mint's external_program_mintinstruction (publish_rewards). This increases the wYLDS balance held in the stake vault (numerator) without changing the PRIME supply (denominator).
  • Result: The PRIME exchange rate appreciates over time. 1 PRIME becomes redeemable for more wYLDS. The on-chain PRIME/wYLDS exchange rate is maintained by Chainlink Data Streams and updated periodically by rewards administrators via verify_price.

PRIME Exchange Rate:

shares_to_mint  = deposit_wYLDS * price_scale / price
wYLDS_returned  = shares_burned * price / price_scale
where: price = (wYLDS per 1 PRIME) * price_scale  [from Chainlink Data Streams]

PDA Derivations (vault-stake)

TypeScript (Anchor)
const [stakeConfigPda] = PublicKey.findProgramAddressSync(
[Buffer.from("stake_config")],
vaultStakeProgramId
);
const [stakeVaultTokenAccountConfigPda] = PublicKey.findProgramAddressSync(
[Buffer.from("stake_vault_token_account_config"), stakeConfigPda.toBuffer()],
vaultStakeProgramId
);
const [vaultAuthorityPda] = PublicKey.findProgramAddressSync(
[Buffer.from("vault_authority")],
vaultStakeProgramId
);
const [mintAuthorityPda] = PublicKey.findProgramAddressSync(
[Buffer.from("mint_authority")],
vaultStakeProgramId
);
const [stakePriceConfigPda] = PublicKey.findProgramAddressSync(
[Buffer.from("stake_price_config"), stakeConfigPda.toBuffer()],
vaultStakeProgramId
);

Programmatic Stake (wYLDS to PRIME)

TypeScript (Anchor)
const vaultStake = new Program<HastraSolVaultStake>(idl, provider);
// Deposit wYLDS to receive PRIME at the current Chainlink exchange rate.
await vaultStake.methods
.deposit(new BN(amount))
.accountsStrict({
stakeConfig: stakeConfigPda,
vaultTokenAccount: stakeVaultWyldsAta,    // program-owned wYLDS vault
stakeVaultTokenAccountConfig: stakeVaultTokenAccountConfigPda,
vaultAuthority: vaultAuthorityPda,
mint: primeMint,                           // PRIME mint
vaultMint: wyldsMint,                      // wYLDS mint
mintAuthority: mintAuthorityPda,
signer: userPublicKey,
userVaultTokenAccount: userWyldsAta,       // user's wYLDS ATA
userMintTokenAccount: userPrimeAta,        // user's PRIME ATA
stakePriceConfig: stakePriceConfigPda,
tokenProgram: TOKEN_PROGRAM_ID,
})
.rpc();

Optional: Merkle-Based Reward Claims(vault-mint)

In addition to exchange-rate appreciation, the protocol supports discrete reward epochs distributed via merkle proofs (used for incentive programs and supplemental allocations).
Important: These claims live on the vault-mint program and mint wYLDS (not PRIME) directly to the user. If a partner wants PRIME exposure on the claimed wYLDS, they must subsequently stake it via vault-stake.
  • Context: Each epoch contains a merkle root summarizing user rewards for the period. Rewards are minted as wYLDS directly to the user's wYLDS ATA.
  • Leaf format: sha256(user_pubkey || reward_amount_le_bytes || epoch_index_le_bytes)
// PDA derivations for claim
const [epochPda] = PublicKey.findProgramAddressSync(
[Buffer.from("epoch"), epochIndexBuffer /* u64 little-endian */],
vaultMintProgramId
);
const [claimRecordPda] = PublicKey.findProgramAddressSync(
[Buffer.from("claim"), epochPda.toBuffer(), userPublicKey.toBuffer()],
vaultMintProgramId
);
// Claim rewards for a specific epoch with a merkle proof.
// Note: this is called on vaultMint, not vaultStake.
await vaultMint.methods
.claimRewards(new BN(rewardAmount), merkleProof)
.accountsStrict({
config: configPda,                         // vault-mint config PDA
user: userPublicKey,
epoch: epochPda,
claimRecord: claimRecordPda,               // PDA: ["claim", epoch, user] — prevents double-claim
mintAuthority: mintAuthorityPda,           // vault-mint mint authority
mint: wyldsMint,                           // wYLDS mint (not PRIME)
userMintTokenAccount: userWyldsAta,        // user's wYLDS ATA
systemProgram: SystemProgram.programId,
tokenProgram: TOKEN_PROGRAM_ID,
})
.rpc();

3. The Redemption Lifecycle: PRIME → wYLDS → USDC

Returning to USDC is a multi-step process.

Phase 1: Redeem PRIME for wYLDS (vault-stake)

  • What happens: The vault-stake program supports a single-step direct redemption. There is no unbonding period or queuing — redeem atomically burns the specified PRIME and transfers the proportional wYLDS from the stake vault to the user.
  • Context: The amount of wYLDS returned is calculated using the live Chainlink price at the time of the call: wYLDS_returned = prime_amount * price / price_scale. The call will fail if the stored Chainlink price is stale (past price_max_staleness) or uninitialised.
TypeScript (Anchor)
// Atomically burn PRIME and receive wYLDS in a single call.
await vaultStake.methods
.redeem(new BN(primeAmount))
.accountsStrict({
stakeConfig: stakeConfigPda,
vaultTokenAccount: stakeVaultWyldsAta,    // program-owned wYLDS vault
stakeVaultTokenAccountConfig: stakeVaultTokenAccountConfigPda,
vaultAuthority: vaultAuthorityPda,
signer: userPublicKey,
ticket: vaultStakeProgramId,              // pass program ID if no legacy v1 ticket exists
userVaultTokenAccount: userWyldsAta,       // user's wYLDS ATA (destination)
userMintTokenAccount: userPrimeAta,        // user's PRIME ATA (source to burn)
mint: primeMint,                           // PRIME mint
vaultMint: wyldsMint,                      // wYLDS mint
stakePriceConfig: stakePriceConfigPda,
tokenProgram: TOKEN_PROGRAM_ID,
})
.rpc();
Legacy note: An optional ticket account exists for users who have an UnbondingTicket from the deprecated v1 two-step flow. If no ticket exists, pass the vault-stake program ID for that account; Anchor treats it as None and skips all ticket-related constraints. The on-chain unbonding_period field is permanently set to 0 and the unbond instruction no longer exists.

Phase 2: wYLDS → USDC (Operator-Mediated, vault-mint)

To convert wYLDS back to USDC, the vault-mint program uses an admin-mediated two-step request_redeem / complete_redeem flow. This leg is operator-managed and includes a batching minimum (currently $2k) and may be subject to banking-hours delays during the YLDS → USDC conversion via Circle's CCTP.
For most institutional integrators, the recommended path is to stop at wYLDS unless USDC settlement is strictly required.

Phase 2a: User submits a redemption request

const [redemptionRequestPda] = PublicKey.findProgramAddressSync(
[Buffer.from("redemption_request"), userPublicKey.toBuffer()],
vaultMintProgramId
);
const [redeemVaultAuthorityPda] = PublicKey.findProgramAddressSync(
[Buffer.from("redeem_vault_authority")],
vaultMintProgramId
);
// Burns wYLDS from the user and creates a RedemptionRequest PDA.
await vaultMint.methods
.requestRedeem(new BN(wyldsAmount))
.accountsStrict({
signer: userPublicKey,
userMintTokenAccount: userWyldsAta,        // user's wYLDS ATA (to burn from)
redemptionRequest: redemptionRequestPda,
mint: wyldsMint,
config: configPda,
redeemVaultAuthority: redeemVaultAuthorityPda,
systemProgram: SystemProgram.programId,
tokenProgram: TOKEN_PROGRAM_ID,
})
.rpc();

Phase 2b: Operator fulfills the redemption

The operator calls complete_redeem once USDC has been bridged via Circle's CCTP. The RedemptionRequest PDA is closed and rent is returned to the user.
// Operator-only: transfers USDC from the redeem vault to the user's wallet.
await vaultMint.methods
.completeRedeem()
.accountsStrict({
admin: operatorPublicKey,
user: userPublicKey,
userMintTokenAccount: userWyldsAta,        // wYLDS ATA (for constraint validation)
userVaultTokenAccount: userUsdcAta,        // user's USDC ATA (destination)
redemptionRequest: redemptionRequestPda,
redeemVaultTokenAccount: redeemVaultUsdcAta,
redeemVaultAuthority: redeemVaultAuthorityPda,
mint: wyldsMint,
config: configPda,
tokenProgram: TOKEN_PROGRAM_ID,
})
.signers([operatorKeypair])
.rpc();

4. Troubleshooting for "programmatically" Teams

Without a GUI, teams need to know exactly why a transaction failed.
Scenario
Programmatic Check
Logic / Context
Deposit / Stake Fails
stakeConfig.paused or config.paused on the relevant program
Both vault-mint and vault-stake can be paused by the program authority, blocking deposits and redeems.
Stake Deposit / Redeem Fails (Price)
stakePriceConfig.priceTimestamp and stakePriceConfig.priceMaxStaleness
vault-stake requires a live Chainlink price. The call fails if the stored price is uninitialised (price_timestamp == 0) or stale (now - price_timestamp > price_max_staleness). Rewards admins must call verify_price to refresh it.
Account Frozen
Token account frozen by freeze authority
wYLDS and PRIME mints have program-controlled freeze authorities. TRM sanctions rules can auto-freeze addresses violating ToS.
Redeem Fails (Liquidity)
wYLDS balance of the stake vault
If the stake vault is briefly under-funded (large redemption), the call will fail until the next yield CPI (publish_rewards) lands.
request_redeem Fails (Duplicate)
RedemptionRequest PDA already exists
Only one pending redemption request per user is allowed. Complete or wait for the existing one before opening another.
complete_redeem Fails (Liquidity)
USDC balance of the redeem vault
The off-ramp vault may not yet hold sufficient USDC; the operator controls funding timing.
Claim Fails (Double Claim)
ClaimRecord PDA already exists
The ClaimRecord PDA at seeds ["claim", epoch, user] is permanent. A given user can only claim each epoch once.
Claim Fails (Proof)
Merkle proof verification
Verify the leaf is reconstructed as sha256(user_pubkey || amount_le_bytes || epoch_index_le_bytes) and the proof matches the published epoch root.

5. Key Events / Logs to Monitor

For a full-cycle automated system, your indexer should track instruction logs and account state changes for:
  • Deposit (vault-mint): Confirmation that USDC is now wYLDS.
  • Deposit (vault-stake): Confirmation that wYLDS is now PRIME.
  • publish_rewards / External Program Mint (CPI): Signal that yield was distributed into the stake vault — rewards admins will subsequently call verify_price to advance the exchange rate.
  • verify_price (vault-stake): Signal that the on-chain Chainlink price has been updated and the PRIME/wYLDS exchange rate has advanced.
  • Redeem (vault-stake): Confirmation that PRIME has been burned and wYLDS returned.
  • request_redeem (vault-mint): Confirmation that the wYLDS → USDC exit clock has started; track the resulting RedemptionRequest PDA.
  • complete_redeem (vault-mint): Confirmation that USDC has been bridged back via CCTP and delivered to the user.
  • create_rewards_epoch (vault-mint): Signal that a new merkle-based wYLDS claim is available for the previous epoch.

Key Differences vs. the ETH Integration

Concept
ETH (hastra-eth-vault)
Solana (hastra-sol-vault)
Yield delivery
Manual claimRewards per epoch with merkle proof
Automatic via Chainlink-tracked PRIME/wYLDS exchange rate (CPI mint into stake vault + verify_price); merkle epochs are supplemental wYLDS rewards
Token model
Single vault token (wYLDS)
Two-layer: wYLDS (base) + PRIME (share)
Redemption gate
wYLDS → USDC unbonding via requestRedeem / completeRedeem
PRIME → wYLDS via single-step redeem; wYLDS → USDC is a separate operator-mediated step (request_redeem / complete_redeem)
Unbonding period
Present
Removed in v0.0.5; redeem is immediate
Allowlist / KYC
Whitelist on the vault
Permissionless on-chain; sanctions-based freezing only
Account model
EVM contract calls + ERC-20 approvals
Anchor instructions + PDAs; no approve step (token transfers are signed directly)
Price oracle
N/A
Chainlink Data Streams — must be periodically refreshed by rewards admins via verify_price before deposits/redeems can proceed
Powered by Notaku