diff --git a/sdk/README.md b/sdk/README.md index 3fb434c..cd1fd1b 100644 --- a/sdk/README.md +++ b/sdk/README.md @@ -199,6 +199,162 @@ const tx = buildUnsignedTransaction({ --- +## Example 5 — Redeem shares at maturity + +Once the vault reaches the `Matured` state the operator calls `mature_vault`, after which investors can redeem their full principal plus any unclaimed yield in a single transaction. + +```typescript +import { Networks, rpc } from "@stellar/stellar-sdk"; +import { + SingleRwaVaultClient, + buildUnsignedTransaction, +} from "@stellaryield/sdk"; + +const server = new rpc.Server("https://soroban-testnet.stellar.org"); +const user = "G..."; +const vaultId = "C..."; + +const account = await server.getAccount(user); +const vault = new SingleRwaVaultClient(vaultId); + +// Check current share balance +const sharesOp = vault.balance(user); +// simulate to read balance, then redeem the full amount +const shares = 5_000_000n; // stroops of share tokens + +const op = vault.redeemAtMaturity(user, shares, user, user); +const tx = buildUnsignedTransaction({ + account, + networkPassphrase: Networks.TESTNET, + operation: op, +}); + +const sim = await server.simulateTransaction(tx); +if (rpc.Api.isSimulationError(sim)) throw new Error(sim.error); +// assemble with sorobanData from sim, sign, submit +``` + +**Early redemption (while vault is Active):** + +```typescript +// Request an early exit — shares are escrowed until the operator processes it +const requestOp = vault.requestEarlyRedemption(user, shares); +const requestTx = buildUnsignedTransaction({ + account, + networkPassphrase: Networks.TESTNET, + operation: requestOp, +}); +const requestSim = await server.simulateTransaction(requestTx); +// ... sign & submit; the response includes the queue position hint in events +``` + +--- + +## Example 6 — List vaults from the factory + +Use the `VaultFactoryClient` to discover all deployed vaults or filter by asset. + +```typescript +import { Networks, rpc } from "@stellar/stellar-sdk"; +import { + VaultFactoryClient, + simulateInvocation, +} from "@stellaryield/sdk"; + +const server = new rpc.Server("https://soroban-testnet.stellar.org"); +const factoryId = "C..."; // VaultFactory contract address +const factory = new VaultFactoryClient(factoryId); + +const account = await server.getAccount("G..."); + +// All registered vaults (returns Vec
) +const allVaults = await simulateInvocation({ + server, + account, + networkPassphrase: Networks.TESTNET, + contractId: factory.contractId, + method: "get_single_rwa_vaults", + args: [], +}); +console.log("Vault addresses:", allVaults); + +// Paginated list — useful for large registries +const page = factory.getVaultsPaginated(/* offset */ 0, /* limit */ 10); +const pageTx = buildUnsignedTransaction({ + account, + networkPassphrase: Networks.TESTNET, + operation: page, +}); +const pageSim = await server.simulateTransaction(pageTx); +// parse pageSim.result?.retval for the Vec
value + +// Vaults backed by a specific asset (e.g. USDC) +const usdcVaultsOp = factory.getVaultsByAsset("C...USDC..."); +``` + +--- + +## Example 7 — Status and config checks + +Read vault state, configuration, and a user's position without any on-chain writes. + +```typescript +import { Networks, rpc } from "@stellar/stellar-sdk"; +import { + SingleRwaVaultClient, + simulateInvocation, +} from "@stellaryield/sdk"; + +const server = new rpc.Server("https://soroban-testnet.stellar.org"); +const user = "G..."; +const vaultId = "C..."; +const vault = new SingleRwaVaultClient(vaultId); +const account = await server.getAccount(user); + +// One-call vault overview (state, total assets, epoch, maturity date …) +const overview = await simulateInvocation({ + server, + account, + networkPassphrase: Networks.TESTNET, + contractId: vault.contractId, + method: "get_vault_overview", + args: [], +}); +console.log("Vault overview:", overview); + +// Consolidated config snapshot — cache and refresh only on relevant events +// (dep_lim, fee_set, zkme_upd, coop_upd) +const config = await simulateInvocation({ + server, + account, + networkPassphrase: Networks.TESTNET, + contractId: vault.contractId, + method: "get_config_snapshot", + args: [], +}); +console.log("Fee bps:", config.early_redemption_fee_bps); +console.log("Min deposit:", config.min_deposit); + +// Per-user summary (balance, pending yield, KYC status …) +const userOp = vault.invoke("get_user_overview", /* scAddress(user) */ ); +// or use simulateInvocation with method "get_user_overview" + +// KYC check before attempting a deposit +const kyc = await simulateInvocation({ + server, + account, + networkPassphrase: Networks.TESTNET, + contractId: vault.contractId, + method: "is_kyc_verified", + args: [/* scAddress(user) */], +}); +if (!kyc) { + console.warn("User has not passed KYC — deposit will be rejected."); +} +``` + +--- + ## Read-only simulation (preview / views) ```typescript diff --git a/soroban-contracts/contracts/single_rwa_vault/src/events.rs b/soroban-contracts/contracts/single_rwa_vault/src/events.rs index cfbff75..b1487f8 100644 --- a/soroban-contracts/contracts/single_rwa_vault/src/events.rs +++ b/soroban-contracts/contracts/single_rwa_vault/src/events.rs @@ -172,7 +172,6 @@ pub fn emit_redeem_at_maturity( /// Which early-redemption user event to emit (same topics/data layout for all variants). #[derive(Copy, Clone)] enum EarlyRedemptionUserEventKind { - Requested, Processed, Cancelled, } @@ -196,10 +195,6 @@ fn publish_early_redemption_user_event( amount: i128, ) { match kind { - EarlyRedemptionUserEventKind::Requested => { - e.events() - .publish((symbol_short!("erq_req"), user), (request_id, amount)); - } EarlyRedemptionUserEventKind::Processed => { e.events() .publish((symbol_short!("erq_done"), user), (request_id, amount)); @@ -238,13 +233,21 @@ fn publish_early_redemption_non_success_event_v2( } /// Emitted by `request_early_redemption`. -pub fn emit_early_redemption_requested(e: &Env, user: Address, request_id: u32, shares: i128) { - publish_early_redemption_user_event( - e, - EarlyRedemptionUserEventKind::Requested, - user, - request_id, - shares, +/// +/// `queue_position` is an approximate 1-based position in the pending queue at +/// the moment of submission (i.e. how many unprocessed requests preceded this +/// one, plus one). It is computed with a best-effort scan and may not reflect +/// concurrent submissions; integrators should treat it as a UI hint only. +pub fn emit_early_redemption_requested( + e: &Env, + user: Address, + request_id: u32, + shares: i128, + queue_position: u32, +) { + e.events().publish( + (symbol_short!("erq_req"), user), + (request_id, shares, queue_position), ); } diff --git a/soroban-contracts/contracts/single_rwa_vault/src/lib.rs b/soroban-contracts/contracts/single_rwa_vault/src/lib.rs index 61c1e6f..711246d 100644 --- a/soroban-contracts/contracts/single_rwa_vault/src/lib.rs +++ b/soroban-contracts/contracts/single_rwa_vault/src/lib.rs @@ -2532,7 +2532,18 @@ impl SingleRWAVault { put_escrowed_shares(e, &caller, escrowed); bump_balance(e, &caller); - let id = get_redemption_counter(e) + 1; + // Compute approximate 1-based queue position before inserting the new + // request — count unprocessed entries that precede it. + let prev_total = get_redemption_counter(e); + let mut pending_before: u32 = 0; + for i in 1..=prev_total { + if !get_redemption_request(e, i).processed { + pending_before += 1; + } + } + let queue_position = pending_before + 1; + + let id = prev_total + 1; put_redemption_counter(e, id); let user = caller.clone(); put_redemption_request( @@ -2547,7 +2558,7 @@ impl SingleRWAVault { }, ); - emit_early_redemption_requested(e, user, id, shares); + emit_early_redemption_requested(e, user, id, shares, queue_position); bump_instance(e); id } diff --git a/soroban-contracts/contracts/single_rwa_vault/src/types.rs b/soroban-contracts/contracts/single_rwa_vault/src/types.rs index b952edd..323dfe8 100644 --- a/soroban-contracts/contracts/single_rwa_vault/src/types.rs +++ b/soroban-contracts/contracts/single_rwa_vault/src/types.rs @@ -616,6 +616,51 @@ pub struct EmergencyProposal { pub executed: bool, } +// ───────────────────────────────────────────────────────────────────────────── +// Config snapshot (issue-265) +// ───────────────────────────────────────────────────────────────────────────── + +/// Immutable-ish consolidated view of frequently-read vault configuration +/// parameters. Integrators can cache this struct and only refresh it on +/// relevant admin events rather than issuing separate RPC calls per field. +#[contracttype] +#[derive(Clone, Debug)] +pub struct ConfigSnapshot { + /// Early redemption fee in basis points (0–1_000; divide by 10_000 for %). + pub early_redemption_fee_bps: u32, + /// Minimum deposit amount in underlying asset units (0 = no minimum). + pub min_deposit: i128, + /// Maximum deposit per user in underlying asset units (0 = uncapped). + pub max_deposit_per_user: i128, + /// Address of the zkMe KYC verifier contract. + pub zkme_verifier: Address, + /// Cooperator address used when calling the zkMe verifier. + pub cooperator: Address, +} + +// ───────────────────────────────────────────────────────────────────────────── +// Pending redemption pagination (issue-282) +// ───────────────────────────────────────────────────────────────────────────── + +/// A single entry in the paginated pending-redemption list returned by +/// `list_pending_redemptions`. Contains only the fields useful for operator +/// review dashboards; the full `RedemptionRequest` is available via +/// `redemption_request(id)`. +#[contracttype] +#[derive(Clone, Debug)] +pub struct PendingRedemptionEntry { + /// Monotonically increasing redemption ID (1-based). + pub id: u32, + /// Address that submitted the redemption request. + pub user: Address, + /// Number of shares locked in escrow for this request. + pub shares: i128, + /// Asset value snapshotted at request time (before fee). + pub locked_asset_value: i128, + /// Unix timestamp when the request was submitted. + pub request_time: u64, +} + // ───────────────────────────────────────────────────────────────────────────── // Interface IDs for supports_interface (#299) // ─────────────────────────────────────────────────────────────────────────────