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
81 changes: 81 additions & 0 deletions docs/event-types.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand Down Expand Up @@ -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 | ✓ | ✓ | ✓ |
141 changes: 141 additions & 0 deletions docs/insurance-pool-design.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
52 changes: 52 additions & 0 deletions scripts/verify-deployment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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];
Expand All @@ -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;
}
Expand Down Expand Up @@ -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 });
}

Expand Down
2 changes: 2 additions & 0 deletions sdk/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,8 @@ export {
enrollInsurancePool,
depositInsurancePremium,
claimInsurance,
isInsuranceEnrolled,
getInsurancePremiums,
InsuranceContractError,
} from "./methods/insurance.js";
export type { InsurancePoolInfo } from "@invoice-liquidity/types";
Expand Down
40 changes: 40 additions & 0 deletions sdk/src/methods/insurance.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import {
enrollInsurancePool,
depositInsurancePremium,
claimInsurance,
isInsuranceEnrolled,
getInsurancePremiums,
InsuranceContractError,
} from "./insurance.js";
import { SorobanRpc, Keypair, Address, Account } from "@stellar/stellar-sdk";
Expand Down Expand Up @@ -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);
});
});
Loading
Loading