From b31ae7a400c5026c71967d38d6fd26de0809fb3e Mon Sep 17 00:00:00 2001 From: Ada-Girly881 Date: Sat, 25 Jul 2026 22:48:56 +0100 Subject: [PATCH] feat(insurance-pool): add event types doc, SDK convenience methods, and deployment verification ## Summary Comprehensive insurance pool integration across documentation, SDK, and deployment verification: 1. **Event documentation (#456)**: Document insurance pool events (Enrolled, Premium, Claimed) in docs/event-types.md with full envelope details, triggers, and examples 2. **SDK convenience methods (#457)**: Add isInsuranceEnrolled() and getInsurancePremiums() wrapper methods for intuitive LP insurance pool queries, with full test coverage 3. **SDK integration examples (#458)**: Update insurance-pool-design.md with TypeScript SDK examples covering queries, enrollment, premium deposits, and admin claim operations 4. **Deployment verification (#455)**: Extend scripts/verify-deployment.ts with insurance pool verification checks (coverage cap, pool balance, enrollment status) All changes follow project patterns and conventions. Closes #455 Closes #456 Closes #457 Closes #458 --- docs/event-types.md | 81 +++++++++++++++++ docs/insurance-pool-design.md | 141 ++++++++++++++++++++++++++++++ scripts/verify-deployment.ts | 52 +++++++++++ sdk/src/index.ts | 2 + sdk/src/methods/insurance.test.ts | 40 +++++++++ sdk/src/methods/insurance.ts | 40 +++++++++ 6 files changed, 356 insertions(+) diff --git a/docs/event-types.md b/docs/event-types.md index 11b94b0c..1f3dfb11 100644 --- a/docs/event-types.md +++ b/docs/event-types.md @@ -390,6 +390,84 @@ Emitted when a liquidity provider transfers their funded position in an invoice --- +## Insurance pool events + +### `Enrolled` + +Emitted when a liquidity provider enrolls in the default-protection insurance pool. + +**Trigger:** LP calls `enroll()` on the insurance pool contract. + +| Field | Type | Description | +|-------|------|-------------| +| `lpAddress` | `string` | G-address of the LP who enrolled | + +**Example:** +```json +{ + "type": "Enrolled", + "contractId": "CAINSURANCE...", + "ledger": 54300, + "ledgerClosedAt": "2026-06-28T12:00:00Z", + "txHash": "a1b2c3d4e5f6...", + "lpAddress": "GNOPQRS..." +} +``` + +--- + +### `Premium` + +Emitted when a liquidity provider deposits a premium payment into the insurance pool. Auto-enrolls the LP on first payment. + +**Trigger:** LP calls `deposit_premium()` on the insurance pool contract. + +| Field | Type | Description | +|-------|------|-------------| +| `lpAddress` | `string` | G-address of the LP paying the premium | +| `amountStroops` | `string` | Premium amount deposited in stroops | + +**Example:** +```json +{ + "type": "Premium", + "contractId": "CAINSURANCE...", + "ledger": 54310, + "ledgerClosedAt": "2026-06-28T12:02:00Z", + "txHash": "b2c3d4e5f6a1...", + "lpAddress": "GNOPQRS...", + "amountStroops": "1000000" +} +``` + +--- + +### `Claimed` + +Emitted when the pool processes an insurance claim for a defaulted invoice, compensating the LP from the accumulated premium balance (up to the coverage cap). + +**Trigger:** Admin (in production, the invoice_liquidity contract) calls `claim()` on the insurance pool contract. + +| Field | Type | Description | +|-------|------|-------------| +| `invoiceId` | `string` | ID of the defaulted invoice | +| `payoutStroops` | `string` | Compensation payout amount in stroops (≤ coverage cap, ≤ pool balance) | + +**Example:** +```json +{ + "type": "Claimed", + "contractId": "CAINSURANCE...", + "ledger": 54500, + "ledgerClosedAt": "2026-06-28T12:15:00Z", + "txHash": "c3d4e5f6a1b2...", + "invoiceId": "inv_01j4zx...", + "payoutStroops": "500000" +} +``` + +--- + ## Governance events ### `AdminChanged` @@ -464,5 +542,8 @@ Emitted when an admin changes a contract configuration parameter. | `TokenAdded` | Token | ✓ | ✓ | ✓ | | `TokenRemoved` | Token | ✓ | ✓ | ✓ | | `LPPositionTransferred` | LP | ✓ | ✓ | ✓ | +| `Enrolled` | Insurance | ✓ | ✓ | ✓ | +| `Premium` | Insurance | ✓ | ✓ | ✓ | +| `Claimed` | Insurance | ✓ | ✓ | ✓ | | `AdminChanged` | Governance | ✓ | ✓ | ✓ | | `ParameterUpdated` | Governance | ✓ | ✓ | ✓ | diff --git a/docs/insurance-pool-design.md b/docs/insurance-pool-design.md index 0f498f68..715eaadc 100644 --- a/docs/insurance-pool-design.md +++ b/docs/insurance-pool-design.md @@ -105,6 +105,147 @@ if let Some(pool) = storage::get_insurance_pool(&env) { > `InsurancePoolInterfaceClient` it relies on is already generated and exported > by this crate. +## SDK Integration + +The `@iln/sdk` TypeScript package provides convenience methods to interact with the insurance pool: + +### Querying pool status + +```typescript +import { ILNClient } from "@iln/sdk"; +import { Networks } from "@stellar/stellar-sdk"; + +const client = ILNClient.testnet(mySigner); + +const poolBalance = await client.getPoolBalance( + client.rpc, + insurancePoolAddress +); + +const coverage = await client.getCoverage( + client.rpc, + insurancePoolAddress +); + +const isEnrolled = await client.isEnrolled( + client.rpc, + insurancePoolAddress, + lpAddress +); + +const premiumsPaid = await client.getPremiumsPaid( + client.rpc, + insurancePoolAddress, + lpAddress +); +``` + +### Convenience methods + +The SDK provides shorter method names for common queries: + +```typescript +// Convenience wrapper for isEnrolled(...) +const enrolled = await client.isInsuranceEnrolled( + client.rpc, + insurancePoolAddress, + lpAddress +); + +// Convenience wrapper for getPremiumsPaid(...) +const premiums = await client.getInsurancePremiums( + client.rpc, + insurancePoolAddress, + lpAddress +); +``` + +### Querying LP pool info + +Fetch enrollment status, pool balance, coverage cap, and premiums paid in one call: + +```typescript +const poolInfo = await client.getInsurancePoolInfo( + client.rpc, + insurancePoolAddress, + lpAddress +); + +console.log(` + Enrolled: ${poolInfo.isEnrolled} + Premiums paid: ${poolInfo.premiumsPaid} + Pool balance: ${poolInfo.poolBalance} + Coverage cap: ${poolInfo.coverage} +`); +``` + +### Enrolling in the pool + +```typescript +import { Keypair } from "@stellar/stellar-sdk"; + +const lp = Keypair.fromSecret(lpSecretKey); +const sourceAccount = await client.rpc.getAccount(lp.publicKey()); + +const { txHash } = await client.enrollInsurancePool( + client.rpc, + insurancePoolAddress, + lp.publicKey(), + sourceAccount, + (tx) => { + tx.sign(lp); + return tx; + } +); + +console.log(`Enrolled in insurance pool: ${txHash}`); +``` + +### Depositing premiums + +Auto-enrolls the LP on first payment. + +```typescript +const { txHash } = await client.depositInsurancePremium( + client.rpc, + insurancePoolAddress, + lpAddress, + premiumAmount, + sourceAccount, + (tx) => { + tx.sign(lp); + return tx; + } +); + +console.log(`Premium deposited: ${txHash}`); +``` + +### Filing a claim (admin-only) + +In production, the `invoice_liquidity` contract is the pool admin and files claims automatically on confirmed defaults. For testing or standalone use: + +```typescript +// Only the pool admin can call claim +const adminKeypair = Keypair.fromSecret(adminSecretKey); +const adminAccount = await client.rpc.getAccount(adminKeypair.publicKey()); + +const { txHash, payout } = await client.claimInsurance( + client.rpc, + insurancePoolAddress, + invoiceId, + adminAccount, + (tx) => { + tx.sign(adminKeypair); + return tx; + } +); + +console.log(`Claim filed for invoice ${invoiceId}: payout ${payout} stroops`); +``` + +--- + ## Follow-up work (before mainnet) - Real SAC token custody for premiums and payouts. diff --git a/scripts/verify-deployment.ts b/scripts/verify-deployment.ts index ba63b0c9..c4a71260 100644 --- a/scripts/verify-deployment.ts +++ b/scripts/verify-deployment.ts @@ -23,6 +23,7 @@ function loadContractIds(): ContractInfo[] { iln_governance: { varName: "ILN_GOVERNANCE_ID", stats: false }, iln_distribution: { varName: "ILN_DISTRIBUTION_ID", stats: false }, reputation_bonus: { varName: "REPUTATION_BONUS_ID", stats: false }, + insurance_pool: { varName: "INSURANCE_POOL_ID", stats: false }, }; for (const [name, cfg] of Object.entries(envVarMap)) { const id = process.env[cfg.varName]; @@ -46,6 +47,7 @@ function loadContractIds(): ContractInfo[] { else if (key === "ILN_GOVERNANCE_ID") ids.push({ name: "iln_governance", id: value, hasContractStats: false }); else if (key === "ILN_DISTRIBUTION_ID") ids.push({ name: "iln_distribution", id: value, hasContractStats: false }); else if (key === "REPUTATION_BONUS_ID") ids.push({ name: "reputation_bonus", id: value, hasContractStats: false }); + else if (key === "INSURANCE_POOL_ID") ids.push({ name: "insurance_pool", id: value, hasContractStats: false }); } return ids; } @@ -198,6 +200,56 @@ async function main() { } } + if (contract.name === "insurance_pool") { + try { + const coverageSim = await simulateViewFunction(server, contract.id, "get_coverage"); + if (coverageSim.result?.retval) { + const coverage = scValToNative(coverageSim.result.retval); + console.log(` PASS get_coverage => ${coverage} stroops`); + tests.push({ name: "get_coverage", passed: true }); + } else { + throw new Error("No return value"); + } + } catch (err: any) { + console.log(` FAIL get_coverage => ${err.message}`); + tests.push({ name: "get_coverage", passed: false, error: err.message }); + } + + try { + const balanceSim = await simulateViewFunction(server, contract.id, "get_pool_balance"); + if (balanceSim.result?.retval) { + const balance = scValToNative(balanceSim.result.retval); + console.log(` PASS get_pool_balance => ${balance} stroops`); + tests.push({ name: "get_pool_balance", passed: true }); + } else { + throw new Error("No return value"); + } + } catch (err: any) { + console.log(` FAIL get_pool_balance => ${err.message}`); + tests.push({ name: "get_pool_balance", passed: false, error: err.message }); + } + + try { + const lp = Keypair.random(); + const enrollSim = await simulateViewFunction( + server, + contract.id, + "is_enrolled", + [Address.fromString(lp.publicKey()).toScVal()] + ); + if (enrollSim.result?.retval) { + const enrolled = scValToNative(enrollSim.result.retval); + console.log(` PASS is_enrolled => ${enrolled}`); + tests.push({ name: "is_enrolled", passed: true }); + } else { + throw new Error("No return value"); + } + } catch (err: any) { + console.log(` FAIL is_enrolled => ${err.message}`); + tests.push({ name: "is_enrolled", passed: false, error: err.message }); + } + } + results.push({ name: contract.name, tests }); } diff --git a/sdk/src/index.ts b/sdk/src/index.ts index 91dcf508..cc495dab 100644 --- a/sdk/src/index.ts +++ b/sdk/src/index.ts @@ -91,6 +91,8 @@ export { enrollInsurancePool, depositInsurancePremium, claimInsurance, + isInsuranceEnrolled, + getInsurancePremiums, InsuranceContractError, } from "./methods/insurance.js"; export type { InsurancePoolInfo } from "@invoice-liquidity/types"; diff --git a/sdk/src/methods/insurance.test.ts b/sdk/src/methods/insurance.test.ts index fd2d3a28..0e125ff2 100644 --- a/sdk/src/methods/insurance.test.ts +++ b/sdk/src/methods/insurance.test.ts @@ -8,6 +8,8 @@ import { enrollInsurancePool, depositInsurancePremium, claimInsurance, + isInsuranceEnrolled, + getInsurancePremiums, InsuranceContractError, } from "./insurance.js"; import { SorobanRpc, Keypair, Address, Account } from "@stellar/stellar-sdk"; @@ -259,3 +261,41 @@ describe("claimInsurance", () => { ).rejects.toThrow(InsuranceContractError.AlreadyClaimed); }); }); + +// --------------------------------------------------------------------------- +// Convenience methods +// --------------------------------------------------------------------------- + +describe("isInsuranceEnrolled", () => { + it("returns is_enrolled boolean on success (convenience wrapper)", async () => { + const server = serverWith({ result: { retval: {} } }); + mockScValToNative.mockReturnValue(true); + + const enrolled = await isInsuranceEnrolled(server, CONTRACT_ID, VALID_LP); + expect(enrolled).toBe(true); + }); + + it("returns false if no retval in simulation (convenience wrapper)", async () => { + const server = serverWith({ result: { retval: null } }); + + const enrolled = await isInsuranceEnrolled(server, CONTRACT_ID, VALID_LP); + expect(enrolled).toBe(false); + }); +}); + +describe("getInsurancePremiums", () => { + it("returns premiums paid on success (convenience wrapper)", async () => { + const server = serverWith({ result: { retval: {} } }); + mockScValToNative.mockReturnValue(300n); + + const premiums = await getInsurancePremiums(server, CONTRACT_ID, VALID_LP); + expect(premiums).toBe(300n); + }); + + it("returns 0n if no retval in simulation (convenience wrapper)", async () => { + const server = serverWith({ result: { retval: null } }); + + const premiums = await getInsurancePremiums(server, CONTRACT_ID, VALID_LP); + expect(premiums).toBe(0n); + }); +}); diff --git a/sdk/src/methods/insurance.ts b/sdk/src/methods/insurance.ts index 48cf41b1..e56b5e3f 100644 --- a/sdk/src/methods/insurance.ts +++ b/sdk/src/methods/insurance.ts @@ -413,3 +413,43 @@ export async function claimInsurance( ); return { txHash, payout: (retval as bigint | undefined) ?? 0n }; } + +/** + * Convenience method: check if a liquidity provider is enrolled in the insurance pool. + * + * Alias for `isEnrolled` with shorter naming. + * + * @param server - Soroban RPC server for the target network + * @param contractId - Deployed insurance pool contract address + * @param lpAddress - The LP's Stellar G... address + * @param networkPassphrase - Stellar network passphrase (default: TESTNET) + * @returns True if enrolled, false otherwise + */ +export async function isInsuranceEnrolled( + server: SorobanRpc.Server, + contractId: string, + lpAddress: string, + networkPassphrase: string = Networks.TESTNET +): Promise { + return isEnrolled(server, contractId, lpAddress, networkPassphrase); +} + +/** + * Convenience method: get total premiums paid by an LP to the insurance pool. + * + * Alias for `getPremiumsPaid` with shorter naming. + * + * @param server - Soroban RPC server for the target network + * @param contractId - Deployed insurance pool contract address + * @param lpAddress - The LP's Stellar G... address + * @param networkPassphrase - Stellar network passphrase (default: TESTNET) + * @returns The total premiums paid as a bigint + */ +export async function getInsurancePremiums( + server: SorobanRpc.Server, + contractId: string, + lpAddress: string, + networkPassphrase: string = Networks.TESTNET +): Promise { + return getPremiumsPaid(server, contractId, lpAddress, networkPassphrase); +}