Skip to content
Open
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
22 changes: 1 addition & 21 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions e2e/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ categories = ["cryptography::cryptocurrencies"]
tokio = { version = "1.0", features = ["full"] }
soroban-sdk = { version = "22.0.11", features = ["testutils"] }
bc-forge-token = { path = "../contracts/token", features = ["testutils"] }
bc-forge-wrapper = { path = "../contracts/wrapper", features = ["testutils"] }

[dev-dependencies]
tokio = { version = "1.0", features = ["test-util"] }
Expand Down
68 changes: 68 additions & 0 deletions e2e/integration_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
#[cfg(test)]
use bc_forge_token::{BcForgeToken, BcForgeTokenClient};
#[cfg(test)]
use bc_forge_wrapper::{WrapperContract, WrapperContractClient};
#[cfg(test)]
use soroban_sdk::testutils::Address as _;
#[cfg(test)]
use soroban_sdk::{Address, Env, String};
Expand Down Expand Up @@ -68,6 +70,72 @@ async fn test_complete_lifecycle() {
println!("✅ Complete lifecycle test passed!");
}

/// E2E: Token -> Vault -> Compound flow lifecycle test (#740)
///
/// Flow: Mint -> Vault Deposit -> Fee Generation -> Compound -> Vault Withdraw
#[tokio::test]
async fn test_token_vault_compound_lifecycle() {
let env = Env::default();
env.mock_all_auths();

let admin = Address::generate(&env);
let user = Address::generate(&env);
let fee_generator = Address::generate(&env);

// 1. Deploy & Initialize Underlying Token
let token_id = env.register(BcForgeToken, ());
let token_client = BcForgeTokenClient::new(&env, &token_id);
let token_name = String::from_str(&env, "Underlying Token");
let token_symbol = String::from_str(&env, "UND");
token_client.initialize(&admin, &7, &token_name, &token_symbol);

// 2. Deploy & Initialize Vault Contract
let vault_id = env.register(WrapperContract, ());
let vault_client = WrapperContractClient::new(&env, &vault_id);
let vault_name = String::from_str(&env, "Yield Vault Share");
let vault_symbol = String::from_str(&env, "yvUND");
vault_client.initialize(&admin, &token_id, &7, &vault_name, &vault_symbol);

// 3. MINT: Mint tokens to User (1,000,000) and Fee Generator (500,000)
token_client.mint(&admin, &user, &1_000_000);
token_client.mint(&admin, &fee_generator, &500_000);
assert_eq!(token_client.balance(&user), 1_000_000);
assert_eq!(token_client.balance(&fee_generator), 500_000);

// 4. VAULT DEPOSIT: User approves and deposits 1,000,000 tokens
token_client.approve(&user, &vault_id, &1_000_000, &u32::MAX);
let shares_minted = vault_client.deposit(&user, &1_000_000);
assert_eq!(shares_minted, 1_000_000);
assert_eq!(vault_client.balance(&user), 1_000_000);
assert_eq!(vault_client.total_assets(), 1_000_000);
assert_eq!(vault_client.supply(), 1_000_000);
assert_eq!(token_client.balance(&user), 0);

// 5. FEE GENERATION: Protocol generates 500,000 fees and distributes to vault
token_client.approve(&fee_generator, &vault_id, &500_000, &u32::MAX);
vault_client.distribute_rewards(&fee_generator, &500_000);
assert_eq!(token_client.balance(&fee_generator), 0);
assert_eq!(vault_client.pending_rewards(), 500_000);
assert_eq!(vault_client.total_assets(), 1_500_000);
assert_eq!(vault_client.supply(), 1_000_000); // shares unchanged

// 6. COMPOUND & PRO-RATA ENTITLEMENT: Verify share price appreciation
let entitlement = vault_client.calculate_rewards(&1_000_000);
assert_eq!(entitlement, 1_500_000);

// 7. VAULT WITHDRAW: User withdraws all 1,000,000 shares
let tokens_returned = vault_client.withdraw(&user, &1_000_000);
assert_eq!(tokens_returned, 1_500_000); // 1,000,000 principal + 500,000 yield

// 8. VERIFY FINAL BALANCES
assert_eq!(token_client.balance(&user), 1_500_000);
assert_eq!(vault_client.balance(&user), 0);
assert_eq!(vault_client.supply(), 0);
assert_eq!(vault_client.total_assets(), 0);

println!("✅ Token -> Vault -> Compound lifecycle test passed!");
}

/// Test parallel execution of multiple operations
#[tokio::test]
async fn test_parallel_execution() {
Expand Down
31 changes: 31 additions & 0 deletions sdk/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -489,6 +489,37 @@ When a `walletAdapter` is configured and connected, write methods may be invoked
| `simulateMint(to, amount, sourcePublicKey)` | `any` | Simulate mint operation |
| `simulateTransfer(from, to, amount, sourcePublicKey)` | `any` | Simulate transfer operation |

## Vault Client (`VaultClient`) (#744)

The SDK provides `VaultClient` for interacting with yield-bearing fee vault contracts and wrapper contracts.

```typescript
import { VaultClient } from '@bc-forge/sdk';
import { Keypair } from '@stellar/stellar-sdk';

const vault = new VaultClient({
rpcUrl: 'https://soroban-testnet.stellar.org',
networkPassphrase: 'Test SDF Network ; September 2015',
contractId: 'CVAULT...XYZ',
});

// Deposit underlying tokens to receive vault shares
await vault.deposit('GUSER...', BigInt(1000_0000000), userKeypair);

// Check share balance & underlying value
const shares = await vault.getShareBalance('GUSER...');
const totalAssets = await vault.getTotalAssets();
const sharePrice = await vault.calculateSharePrice();
const rewards = await vault.calculateRewards(shares);

// Compound protocol fees into vault assets
await vault.compound('GADMIN...', adminKeypair);

// Withdraw shares and receive underlying tokens + accrued yield
await vault.withdraw('GUSER...', shares, userKeypair);
```

## License

MIT

6 changes: 4 additions & 2 deletions sdk/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,10 @@
"build": "tsc",
"dev": "tsc --watch",
"test": "node --experimental-vm-modules ../node_modules/jest/bin/jest.js --passWithNoTests",
"lint": "eslint 'src/**/*.ts'",
"format": "prettier --write 'src/**/*.ts'",
"lint": "eslint src",
"format": "prettier --write src",


"clean": "rm -rf dist"
},
"keywords": [
Expand Down
10 changes: 8 additions & 2 deletions sdk/src/apy.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,14 @@ function makeSimError(): object {
// network calls; instead we mock the server constructor inline via jest.
import { rpc as SorobanRpc } from '@stellar/stellar-sdk';

const mockSimulateTransaction = jest.spyOn(SorobanRpc.Server.prototype, 'simulateTransaction') as unknown as jest.Mock;
const mockGetLatestLedger = jest.spyOn(SorobanRpc.Server.prototype, 'getLatestLedger') as unknown as jest.Mock;
const mockSimulateTransaction = jest.spyOn(
SorobanRpc.Server.prototype,
'simulateTransaction',
) as unknown as jest.Mock;
const mockGetLatestLedger = jest.spyOn(
SorobanRpc.Server.prototype,
'getLatestLedger',
) as unknown as jest.Mock;

// ─── Import subject after mock setup ─────────────────────────────────────────

Expand Down
13 changes: 4 additions & 9 deletions sdk/src/apy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,10 +109,7 @@ function buildSimTx(
method: string,
...args: xdr.ScVal[]
): ReturnType<TransactionBuilder['build']> {
const dummyAccount = new Account(
'GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF',
'0',
);
const dummyAccount = new Account('GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF', '0');
return new TransactionBuilder(dummyAccount, {
fee: '100',
networkPassphrase,
Expand Down Expand Up @@ -163,7 +160,7 @@ async function simulateI128(
if (i64 !== undefined && i64 !== null) return BigInt(i64.toString());

return null;
} catch (err) {
} catch {
return null;
}
}
Expand All @@ -186,8 +183,7 @@ async function readSnapshot(
const assets = totalAssets ?? 0n;
const shares = totalShares ?? 0n;

const sharePrice =
shares > 0n ? Number(assets) / Number(shares) : null;
const sharePrice = shares > 0n ? Number(assets) / Number(shares) : null;

return {
ledger: ledgerSequence,
Expand Down Expand Up @@ -259,8 +255,7 @@ export async function calculateApy(options: ApyOptions): Promise<ApyResult | nul
// growth = (P1 - P0) / P0
// periods = LEDGERS_PER_YEAR / windowLedgers
// APY = (1 + growth) ^ periods - 1
const growth =
(current.sharePrice - historical.sharePrice) / historical.sharePrice;
const growth = (current.sharePrice - historical.sharePrice) / historical.sharePrice;

const periods = LEDGERS_PER_YEAR / actualWindow;
const apy = Math.pow(1 + growth, periods) - 1;
Expand Down
7 changes: 6 additions & 1 deletion sdk/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,12 @@ export { FreighterAdapter } from './adapters/freighterAdapter';
export { AlbedoAdapter } from './adapters/albedoAdapter';
export { WalletConnectAdapter } from './adapters/walletConnectAdapter';

// ─── Vault and Wrapper Clients (#744) ────────────────────────────────────────
export { VaultClient } from './vaultClient';
export type { VaultClientConfig } from './vaultClient';
export { WrapperClient } from './wrapperClient';
export type { WrapperClientConfig } from './wrapperClient';

// ─── APY helpers (#745) ──────────────────────────────────────────────────────
export { calculateApy } from './apy';
export type { ApyOptions, ApyResult, ApySnapshot } from './apy';

Loading
Loading