Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
156 changes: 156 additions & 0 deletions sdk/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<Address>)
const allVaults = await simulateInvocation<string[]>({
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<Address> 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<boolean>({
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
Expand Down
27 changes: 15 additions & 12 deletions soroban-contracts/contracts/single_rwa_vault/src/events.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}
Expand All @@ -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));
Expand Down Expand Up @@ -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),
);
}

Expand Down
15 changes: 13 additions & 2 deletions soroban-contracts/contracts/single_rwa_vault/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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
}
Expand Down
45 changes: 45 additions & 0 deletions soroban-contracts/contracts/single_rwa_vault/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
// ─────────────────────────────────────────────────────────────────────────────
Expand Down
Loading