|
| 1 | +import { getConnectionPool } from '../utils/connection'; |
| 2 | +import { getKeypair } from '../utils/keypair'; |
| 3 | +import { USDC_MINT } from '../utils/constants'; |
| 4 | +import Decimal from 'decimal.js/decimal'; |
| 5 | +import { |
| 6 | + AssetReserveConfig, |
| 7 | + getAssociatedTokenAddress, |
| 8 | + getDefaultConfigParams, |
| 9 | + getMedianSlotDurationInMsFromLastEpochs, |
| 10 | + KaminoManager, |
| 11 | + KaminoVault, |
| 12 | + KaminoVaultConfig, |
| 13 | + LendingMarket, |
| 14 | + Reserve, |
| 15 | + ReserveAllocationConfig, |
| 16 | + sleep, |
| 17 | +} from '@kamino-finance/klend-sdk'; |
| 18 | +import { address } from '@solana/kit'; |
| 19 | +import { TOKEN_PROGRAM_ADDRESS } from '@solana-program/token'; |
| 20 | +import { sendAndConfirmTx } from '../utils/tx'; |
| 21 | + |
| 22 | +/** |
| 23 | + * Option: Create your own lending market + add a reserve, then create a vault on top |
| 24 | + * - You are the market admin AND the vault admin. |
| 25 | + * - This is advanced: you must provide valid oracle configuration. |
| 26 | + * |
| 27 | + * Required env vars: |
| 28 | + * - `PYTH_PRICE` = Pyth price account for the asset you add (base58 address) |
| 29 | + * |
| 30 | + * Optional: |
| 31 | + * - `EXECUTE=true` to actually send transactions (default is dry-run). |
| 32 | + */ |
| 33 | +(async () => { |
| 34 | + const c = getConnectionPool(); |
| 35 | + const admin = await getKeypair(); |
| 36 | + |
| 37 | + const execute = (process.env.EXECUTE ?? 'false').toLowerCase() === 'true'; |
| 38 | + const pythPriceStr = process.env.PYTH_PRICE; |
| 39 | + if (!pythPriceStr) { |
| 40 | + throw new Error('Missing env var PYTH_PRICE (Pyth price account address)'); |
| 41 | + } |
| 42 | + |
| 43 | + const slotDuration = await getMedianSlotDurationInMsFromLastEpochs(); |
| 44 | + const kaminoManager = new KaminoManager(c.rpc, slotDuration); |
| 45 | + |
| 46 | + // 1) Create a new lending market |
| 47 | + const { market, ixs: createMarketIxs } = await kaminoManager.createMarketIxs({ admin }); |
| 48 | + console.log('New market:', market.address); |
| 49 | + |
| 50 | + if (execute) { |
| 51 | + await sendAndConfirmTx(c, admin, createMarketIxs, [market], [], 'OptionCreateMarketReserve-CreateMarket'); |
| 52 | + } else { |
| 53 | + console.log('[dry-run] Would send createMarket tx with', createMarketIxs.length, 'ixs'); |
| 54 | + } |
| 55 | + |
| 56 | + // Optionally, change default market params here . TODO to explore later |
| 57 | + // kaminoManager.updateLendingMarketIxs(market.address, market); |
| 58 | + |
| 59 | + // 2) Add an asset reserve (example uses USDC) |
| 60 | + // initReserve needs an `adminLiquiditySource` token account (ATA) for the mint. |
| 61 | + const adminUsdcAta = await getAssociatedTokenAddress(USDC_MINT, admin.address, TOKEN_PROGRAM_ADDRESS); |
| 62 | + |
| 63 | + const priceFeed = { |
| 64 | + pythPrice: address(pythPriceStr), |
| 65 | + }; |
| 66 | + |
| 67 | + const defaults = getDefaultConfigParams(); |
| 68 | + |
| 69 | + const assetConfig = new AssetReserveConfig({ |
| 70 | + mint: USDC_MINT, |
| 71 | + mintTokenProgram: TOKEN_PROGRAM_ADDRESS, |
| 72 | + tokenName: 'USDC', |
| 73 | + mintDecimals: 6, |
| 74 | + priceFeed, |
| 75 | + loanToValuePct: defaults.loanToValuePct, |
| 76 | + liquidationThresholdPct: defaults.liquidationThresholdPct, |
| 77 | + borrowRateCurve: defaults.borrowRateCurve, |
| 78 | + depositLimit: new Decimal(1_000_000), |
| 79 | + borrowLimit: new Decimal(1_000_000), |
| 80 | + }); |
| 81 | + |
| 82 | + const { reserve, txnIxs } = await kaminoManager.addAssetToMarketIxs({ |
| 83 | + admin, |
| 84 | + adminLiquiditySource: adminUsdcAta, |
| 85 | + marketAddress: market.address, |
| 86 | + assetConfig, |
| 87 | + }); |
| 88 | + |
| 89 | + console.log('New reserve:', reserve.address); |
| 90 | + |
| 91 | + if (execute) { |
| 92 | + // 2a) Create + init reserve (needs reserve signer) |
| 93 | + await sendAndConfirmTx(c, admin, txnIxs[0], [reserve], [], 'OptionCreateMarketReserve-CreateReserve'); |
| 94 | + |
| 95 | + // 2b) Update reserve config (oracle, limits, etc) |
| 96 | + await sendAndConfirmTx(c, admin, txnIxs[1], [], [], 'OptionCreateMarketReserve-UpdateReserveConfig'); |
| 97 | + } else { |
| 98 | + console.log('[dry-run] Would send reserve create tx with', txnIxs[0].length, 'ixs'); |
| 99 | + console.log('[dry-run] Would send reserve update tx with', txnIxs[1].length, 'ixs'); |
| 100 | + } |
| 101 | + |
| 102 | + // 3) Create a vault (base token = USDC) and allocate to the newly created reserve |
| 103 | + const vaultConfig = new KaminoVaultConfig({ |
| 104 | + admin, |
| 105 | + tokenMint: USDC_MINT, |
| 106 | + tokenMintProgramId: TOKEN_PROGRAM_ADDRESS, |
| 107 | + performanceFeeRatePercentage: new Decimal(1.0), |
| 108 | + managementFeeRatePercentage: new Decimal(2.0), |
| 109 | + name: 'example option create market + reserve', |
| 110 | + vaultTokenSymbol: 'USDC', |
| 111 | + vaultTokenName: 'OptionCreateMarketReserve', |
| 112 | + }); |
| 113 | + |
| 114 | + const { vault: vaultKp, initVaultIxs } = await kaminoManager.createVaultIxs(vaultConfig); |
| 115 | + const vault = new KaminoVault(c.rpc, vaultKp.address); |
| 116 | + console.log('New vault:', vault.address); |
| 117 | + |
| 118 | + if (execute) { |
| 119 | + await sendAndConfirmTx( |
| 120 | + c, |
| 121 | + admin, |
| 122 | + [...initVaultIxs.initVaultIxs, initVaultIxs.createLUTIx, initVaultIxs.initSharesMetadataIx], |
| 123 | + [vaultKp], |
| 124 | + [], |
| 125 | + 'OptionB2-InitVault' |
| 126 | + ); |
| 127 | + |
| 128 | + await sleep(2000); |
| 129 | + await sendAndConfirmTx(c, admin, initVaultIxs.populateLUTIxs, [], [], 'OptionCreateMarketReserve-PopulateLUT'); |
| 130 | + |
| 131 | + const reserveState = await Reserve.fetch(c.rpc, reserve.address); |
| 132 | + if (!reserveState) { |
| 133 | + throw new Error(`Reserve not found after creation: ${reserve.address}`); |
| 134 | + } |
| 135 | + |
| 136 | + const alloc = new ReserveAllocationConfig({ address: reserve.address, state: reserveState }, 100, new Decimal(1000)); |
| 137 | + const setAllocIxs = await kaminoManager.updateVaultReserveAllocationIxs(vault, alloc); |
| 138 | + |
| 139 | + await sendAndConfirmTx( |
| 140 | + c, |
| 141 | + admin, |
| 142 | + [setAllocIxs.updateReserveAllocationIx, ...setAllocIxs.updateLUTIxs], |
| 143 | + [], |
| 144 | + [], |
| 145 | + 'OptionB2-SetReserveAllocation' |
| 146 | + ); |
| 147 | + |
| 148 | + console.log('Allocated vault to new reserve:', reserve.address); |
| 149 | + } else { |
| 150 | + console.log('[dry-run] Vault + allocation steps are ready (set EXECUTE=true to run).'); |
| 151 | + } |
| 152 | +})().catch(async (e) => { |
| 153 | + console.error(e); |
| 154 | +}); |
0 commit comments