diff --git a/packages/common/package.json b/packages/common/package.json index 2f90825f..57312d44 100644 --- a/packages/common/package.json +++ b/packages/common/package.json @@ -25,7 +25,7 @@ "prepare": "run-s clean build" }, "dependencies": { - "@project-serum/serum": "^0.13.11", + "@project-serum/serum": "^0.13.38", "@project-serum/sol-wallet-adapter": "^0.1.4", "@solana/spl-token": "0.0.13", "@solana/spl-token-swap": "0.1.0", @@ -44,7 +44,7 @@ "bn.js": "^5.1.3", "borsh": "^0.3.1", "bs58": "^4.0.1", - "buffer-layout": "^1.2.0", + "buffer-layout": "^1.2.1", "eventemitter3": "^4.0.7", "identicon.js": "^2.3.3", "jazzicon": "^1.5.0", diff --git a/packages/common/src/contexts/connection.tsx b/packages/common/src/contexts/connection.tsx index c1cf5c71..2aa1cc9a 100644 --- a/packages/common/src/contexts/connection.tsx +++ b/packages/common/src/contexts/connection.tsx @@ -1,4 +1,9 @@ import { sleep, useLocalStorageState } from '../utils/utils'; +import { + ENV as ChainId, + TokenInfo, + TokenListProvider, +} from '@solana/spl-token-registry'; import { Account, BlockhashAndFeeCalculator, @@ -13,24 +18,17 @@ import { TransactionSignature, } from '@solana/web3.js'; import React, { useContext, useEffect, useMemo, useState } from 'react'; -import { notify } from '../utils/notifications'; import { ExplorerLink } from '../components/ExplorerLink'; import { setProgramIds } from '../utils/ids'; -import { - TokenInfo, - TokenListProvider, - ENV as ChainId, -} from '@solana/spl-token-registry'; +import { notify } from '../utils/notifications'; import { SendTransactionError, SignTransactionError } from '../utils/errors'; export type ENV = | 'mainnet-beta (Serum)' | 'mainnet-beta' - | 'mainnet-beta (Serum)' | 'testnet' | 'devnet' - | 'localnet' - | 'lending'; + | 'localnet'; export const ENDPOINTS = [ { @@ -58,16 +56,6 @@ export const ENDPOINTS = [ endpoint: 'http://127.0.0.1:8899', ChainId: ChainId.Devnet, }, - { - name: 'Oyster Dev' as ENV, - endpoint: 'http://oyster-dev.solana.com/', - ChainId: ChainId.Devnet, - }, - { - name: 'Lending' as ENV, - endpoint: 'https://tln.solana.com/', - ChainId: ChainId.Devnet, - }, ]; const DEFAULT = ENDPOINTS[0].endpoint; @@ -108,14 +96,12 @@ export function ConnectionProvider({ children = undefined as any }) { DEFAULT_SLIPPAGE.toString(), ); - const connection = useMemo( - () => new Connection(endpoint, 'recent'), - [endpoint], - ); - const sendConnection = useMemo( - () => new Connection(endpoint, 'recent'), - [endpoint], - ); + const connection = useMemo(() => new Connection(endpoint, 'recent'), [ + endpoint, + ]); + const sendConnection = useMemo(() => new Connection(endpoint, 'recent'), [ + endpoint, + ]); const env = ENDPOINTS.find(end => end.endpoint === endpoint)?.name || ENDPOINTS[0].name; @@ -125,7 +111,7 @@ export function ConnectionProvider({ children = undefined as any }) { useEffect(() => { // fetch token files new TokenListProvider().resolve().then(container => { - const list = container + const tokens = container .excludeByTag('nft') .filterByChainId( ENDPOINTS.find(end => end.endpoint === endpoint)?.ChainId || @@ -133,13 +119,45 @@ export function ConnectionProvider({ children = undefined as any }) { ) .getList(); - const knownMints = [...list].reduce((map, item) => { + // @FIXME: remove hardcoded values + if (endpoint.ChainId === ChainId.Devnet) { + tokens.push( + { + chainId: 103, + address: '9FbAMDvXqNjPqZSYt4EWTguJuDrGkfvwr3gSFpiSbX9S', + symbol: 'SRM', + name: 'Serum', + decimals: 6, + logoURI: + 'https://cdn.jsdelivr.net/gh/trustwallet/assets@master/blockchains/ethereum/assets/0x476c5E26a75bd202a9683ffD34359C0CC15be0fF/logo.png', + tags: [], + extensions: { + website: 'https://projectserum.com/', + }, + }, + { + chainId: 103, + address: '7KBVenLz5WNH4PA5MdGkJNpDDyNKnBQTwnz1UqJv9GUm', + symbol: 'USDT', + name: 'USDT', + decimals: 6, + logoURI: + 'https://cdn.jsdelivr.net/gh/solana-labs/explorer/public/tokens/usdt.svg', + tags: ['stablecoin'], + extensions: { + website: 'https://tether.to/', + }, + }, + ); + } + + const tokenMap = tokens.reduce((map, item) => { map.set(item.address, item); return map; }, new Map()); - setTokenMap(knownMints); - setTokens(list); + setTokenMap(tokenMap); + setTokens(tokens); }); }, [env]); diff --git a/packages/common/src/utils/ids.ts b/packages/common/src/utils/ids.ts index 72b98c84..f9d34b33 100644 --- a/packages/common/src/utils/ids.ts +++ b/packages/common/src/utils/ids.ts @@ -1,17 +1,16 @@ import { PublicKey } from '@solana/web3.js'; import { TokenSwapLayout, TokenSwapLayoutV1 } from '../models/tokenSwap'; +export let SYSTEM = new PublicKey('11111111111111111111111111111111'); export const WRAPPED_SOL_MINT = new PublicKey( 'So11111111111111111111111111111111111111112', ); export let TOKEN_PROGRAM_ID = new PublicKey( 'TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA', ); - export let LENDING_PROGRAM_ID = new PublicKey( 'LendZqTs7gn5CTSJU1jWKhKuVpjJGom45nnwPb2AMTi', ); - export let SPL_ASSOCIATED_TOKEN_ACCOUNT_PROGRAM_ID = new PublicKey( 'ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL', ); @@ -21,24 +20,21 @@ export let BPF_UPGRADE_LOADER_ID = new PublicKey( export let METADATA_PROGRAM_ID = new PublicKey( 'metaTA73sFPqA8whreUbBsbn3SLJH2vhrW9fP5dmfdC', ); - export const MEMO_ID = new PublicKey( 'MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr', ); - export const VAULT_ID = new PublicKey( '94wRaYAQdC2gYF76AUTYSugNJ3rAC4EimjAMPwM7uYry', ); - export const AUCTION_ID = new PublicKey( 'C9nHkL6BfGx9M9MyYrJqAD5hPsGJd1fHpp1uAJA6vTCn', ); - export const METAPLEX_ID = new PublicKey( 'EPtpKdKW8qciGVd1UFyGjgbBHTbSAyvbY61h9uQGVgeu', ); - -export let SYSTEM = new PublicKey('11111111111111111111111111111111'); +export const PYTH_PROGRAM_ID = new PublicKey( + 'BmA9Z6FjioHJPpjT39QazZyhDRUdZy2ezwx4GiDdE2u2', +); let WORMHOLE_BRIDGE: { pubkey: PublicKey; @@ -84,6 +80,7 @@ export const PROGRAM_IDS = [ // new PublicKey("9qvG1zUp8xF1Bi4m6UdRNby1BAAuaDrUxSpv4CmRRMjL"), ], }), + lending: () => new PublicKey('LendZqTs7gn5CTSJU1jWKhKuVpjJGom45nnwPb2AMTi'), }, { name: 'testnet', @@ -102,8 +99,8 @@ export const PROGRAM_IDS = [ }, legacy: [], }), + lending: () => new PublicKey('6TvznH3B2e3p2mbhufNBpgSrLx6UkgvxtVQvopEZ2kuH'), }, - { name: 'devnet', governance: () => ({ @@ -121,6 +118,7 @@ export const PROGRAM_IDS = [ }, legacy: [new PublicKey('BSfTAcBdqmvX5iE2PW88WFNNp2DHhLUaBKk5WrnxVkcJ')], }), + lending: () => new PublicKey('6TvznH3B2e3p2mbhufNBpgSrLx6UkgvxtVQvopEZ2kuH'), }, { name: 'localnet', @@ -139,6 +137,7 @@ export const PROGRAM_IDS = [ }, legacy: [], }), + lending: () => new PublicKey('6TvznH3B2e3p2mbhufNBpgSrLx6UkgvxtVQvopEZ2kuH'), }, ]; @@ -158,11 +157,7 @@ export const setProgramIds = (envName: string) => { GOVERNANCE = instance.governance(); - if (envName === 'mainnet-beta') { - LENDING_PROGRAM_ID = new PublicKey( - 'LendZqTs7gn5CTSJU1jWKhKuVpjJGom45nnwPb2AMTi', - ); - } + LENDING_PROGRAM_ID = instance.lending(); }; export const programIds = () => { diff --git a/packages/lending/.prettierignore b/packages/lending/.prettierignore new file mode 100644 index 00000000..378eac25 --- /dev/null +++ b/packages/lending/.prettierignore @@ -0,0 +1 @@ +build diff --git a/packages/lending/package.json b/packages/lending/package.json index 4b3af8b3..8d730f06 100644 --- a/packages/lending/package.json +++ b/packages/lending/package.json @@ -2,18 +2,20 @@ "name": "lending", "version": "0.1.0", "dependencies": { - "@solana/wallet-base": "0.0.1", - "@solana/wallet-ledger": "0.0.1", "@ant-design/icons": "^4.4.0", "@ant-design/pro-layout": "^6.7.0", "@babel/preset-typescript": "^7.12.13", "@craco/craco": "^5.7.0", "@oyster/common": "0.0.1", - "@project-serum/serum": "^0.13.11", + "@project-serum/serum": "^0.13.38", "@project-serum/sol-wallet-adapter": "^0.1.4", - "@solana/spl-token": "0.0.13", - "@solana/spl-token-swap": "0.1.0", - "@solana/web3.js": "^1.5.0", + "@pythnetwork/client": "^2.0.0", + "@solana/spl-token": "^0.1.6", + "@solana/spl-token-lending": "^0.3.3", + "@solana/spl-token-swap": "^0.1.0", + "@solana/wallet-base": "0.0.1", + "@solana/wallet-ledger": "0.0.1", + "@solana/web3.js": "^1.20.0", "@testing-library/jest-dom": "^4.2.4", "@testing-library/react": "^9.5.0", "@testing-library/user-event": "^7.2.1", @@ -23,9 +25,9 @@ "@types/testing-library__react": "^10.2.0", "@welldone-software/why-did-you-render": "^6.0.5", "antd": "^4.6.6", + "bignumber.js": "^9.0.1", "bn.js": "^5.1.3", "bs58": "^4.0.1", - "buffer-layout": "^1.2.0", "chart.js": "^2.9.4", "craco-alias": "^2.1.1", "craco-babel-loader": "^0.1.4", @@ -46,7 +48,6 @@ "scripts": { "prestart": "npm-link-shared ../common/node_modules/ . react", "start": "craco start --verbose", - "start:lending": "craco start --verbose", "build": "craco build", "test": "craco test", "eject": "react-scripts eject", @@ -87,8 +88,8 @@ "@types/node": "^12.12.62", "arweave-deploy": "^1.9.1", "gh-pages": "^3.1.0", - "prettier": "^2.1.2", - "npm-link-shared": "0.5.6" + "npm-link-shared": "0.5.6", + "prettier": "^2.1.2" }, "peerDependencies": { "react": "*", @@ -96,6 +97,7 @@ }, "resolutions": { "react": "16.13.1", - "react-dom": "16.13.1" + "react-dom": "16.13.1", + "@oyster/common": "file:../common/dist/lib" } } diff --git a/packages/lending/src/App.tsx b/packages/lending/src/App.tsx index 21da6499..d2220c74 100644 --- a/packages/lending/src/App.tsx +++ b/packages/lending/src/App.tsx @@ -1,6 +1,6 @@ -import React from "react"; -import "./App.less"; -import { Routes } from "./routes"; +import React from 'react'; +import './App.less'; +import { Routes } from './routes'; function App() { return ; diff --git a/packages/lending/src/actions/borrow.tsx b/packages/lending/src/actions/borrow.tsx deleted file mode 100644 index a37cf2f6..00000000 --- a/packages/lending/src/actions/borrow.tsx +++ /dev/null @@ -1,293 +0,0 @@ -import { - Account, - Connection, - PublicKey, - TransactionInstruction, -} from '@solana/web3.js'; -import { - contexts, - utils, - actions, - models, - TokenAccount, - ParsedAccount, -} from '@oyster/common'; - -import { - accrueInterestInstruction, - LendingReserve, -} from './../models/lending/reserve'; -import { AccountLayout, MintInfo, MintLayout } from '@solana/spl-token'; - -import { createUninitializedObligation } from './obligation'; - -import { - LendingObligationLayout, - borrowInstruction, - LendingMarket, - BorrowAmountType, - LendingObligation, - initObligationInstruction, -} from '../models'; - -const { approve } = models; -const { toLamports, LENDING_PROGRAM_ID, LEND_HOST_FEE_ADDRESS, notify } = utils; -const { cache, MintParser } = contexts.Accounts; -const { sendTransaction } = contexts.Connection; -const { - createTempMemoryAccount, - createUninitializedAccount, - createUninitializedMint, - ensureSplAccount, - findOrCreateAccountByMint, -} = actions; - -export const borrow = async ( - connection: Connection, - wallet: any, - - from: TokenAccount, - amount: number, - amountType: BorrowAmountType, - - borrowReserve: ParsedAccount, - - depositReserve: ParsedAccount, - - existingObligation?: ParsedAccount, - - obligationAccount?: PublicKey, -) => { - notify({ - message: 'Borrowing funds...', - description: 'Please review transactions to approve.', - type: 'warn', - }); - - let signers: Account[] = []; - let instructions: TransactionInstruction[] = []; - let cleanupInstructions: TransactionInstruction[] = []; - let finalCleanupInstructions: TransactionInstruction[] = []; - - const [authority] = await PublicKey.findProgramAddress( - [depositReserve.info.lendingMarket.toBuffer()], - LENDING_PROGRAM_ID, - ); - - const accountRentExempt = await connection.getMinimumBalanceForRentExemption( - AccountLayout.span, - ); - - const obligation = existingObligation - ? existingObligation.pubkey - : createUninitializedObligation( - instructions, - wallet.publicKey, - await connection.getMinimumBalanceForRentExemption( - LendingObligationLayout.span, - ), - signers, - ); - - const obligationMint = existingObligation - ? existingObligation.info.tokenMint - : createUninitializedMint( - instructions, - wallet.publicKey, - await connection.getMinimumBalanceForRentExemption(MintLayout.span), - signers, - ); - - const obligationTokenOutput = obligationAccount - ? obligationAccount - : createUninitializedAccount( - instructions, - wallet.publicKey, - accountRentExempt, - signers, - ); - - if (!obligationAccount) { - instructions.push( - initObligationInstruction( - depositReserve.pubkey, - borrowReserve.pubkey, - obligation, - obligationMint, - obligationTokenOutput, - wallet.publicKey, - depositReserve.info.lendingMarket, - authority, - ), - ); - } - - // Creates host fee account if it doesn't exsist - let hostFeeReceiver = LEND_HOST_FEE_ADDRESS - ? findOrCreateAccountByMint( - wallet.publicKey, - LEND_HOST_FEE_ADDRESS, - instructions, - [], - accountRentExempt, - depositReserve.info.collateralMint, - signers, - ) - : undefined; - - let amountLamports: number = 0; - let fromLamports: number = 0; - if (amountType === BorrowAmountType.LiquidityBorrowAmount) { - // approve max transfer - // TODO: improve contrain by using dex market data - const approvedAmount = from.info.amount.toNumber(); - - fromLamports = approvedAmount - accountRentExempt; - - const mint = (await cache.query( - connection, - borrowReserve.info.liquidityMint, - MintParser, - )) as ParsedAccount; - - amountLamports = toLamports(amount, mint?.info); - } else if (amountType === BorrowAmountType.CollateralDepositAmount) { - const mint = (await cache.query( - connection, - depositReserve.info.collateralMint, - MintParser, - )) as ParsedAccount; - amountLamports = toLamports(amount, mint?.info); - fromLamports = amountLamports; - } - - const fromAccount = ensureSplAccount( - instructions, - finalCleanupInstructions, - from, - wallet.publicKey, - fromLamports + accountRentExempt, - signers, - ); - - let toAccount = await findOrCreateAccountByMint( - wallet.publicKey, - wallet.publicKey, - instructions, - finalCleanupInstructions, - accountRentExempt, - borrowReserve.info.liquidityMint, - signers, - ); - - if (instructions.length > 0) { - // create all accounts in one transaction - let { txid } = await sendTransaction(connection, wallet, instructions, [ - ...signers, - ]); - - notify({ - message: 'Obligation accounts created', - description: `Transaction ${txid}`, - type: 'success', - }); - } - - notify({ - message: 'Borrowing funds...', - description: 'Please review transactions to approve.', - type: 'warn', - }); - - signers = []; - instructions = []; - cleanupInstructions = [...finalCleanupInstructions]; - - // create approval for transfer transactions - const transferAuthority = approve( - instructions, - cleanupInstructions, - fromAccount, - wallet.publicKey, - fromLamports, - false, - ); - signers.push(transferAuthority); - - const dexMarketAddress = borrowReserve.info.dexMarketOption - ? borrowReserve.info.dexMarket - : depositReserve.info.dexMarket; - const dexMarket = cache.get(dexMarketAddress); - - if (!dexMarket) { - throw new Error(`Dex market doesn't exist.`); - } - - const market = cache.get( - depositReserve.info.lendingMarket, - ) as ParsedAccount; - const dexOrderBookSide = market.info.quoteMint.equals( - depositReserve.info.liquidityMint, - ) - ? dexMarket?.info.asks - : dexMarket?.info.bids; - - const memory = createTempMemoryAccount( - instructions, - wallet.publicKey, - signers, - LENDING_PROGRAM_ID, - ); - - instructions.push( - accrueInterestInstruction(depositReserve.pubkey, borrowReserve.pubkey), - ); - // borrow - instructions.push( - borrowInstruction( - amountLamports, - amountType, - fromAccount, - toAccount, - depositReserve.pubkey, - depositReserve.info.collateralSupply, - depositReserve.info.collateralFeesReceiver, - - borrowReserve.pubkey, - borrowReserve.info.liquiditySupply, - - obligation, - obligationMint, - obligationTokenOutput, - - depositReserve.info.lendingMarket, - authority, - transferAuthority.publicKey, - - dexMarketAddress, - dexOrderBookSide, - - memory, - - hostFeeReceiver, - ), - ); - try { - let { txid } = await sendTransaction( - connection, - wallet, - instructions.concat(cleanupInstructions), - signers, - true, - ); - - notify({ - message: 'Funds borrowed.', - type: 'success', - description: `Transaction - ${txid}`, - }); - } catch (ex) { - console.error(ex); - throw new Error(); - } -}; diff --git a/packages/lending/src/actions/borrowObligationLiquidity.tsx b/packages/lending/src/actions/borrowObligationLiquidity.tsx new file mode 100644 index 00000000..9c480ab8 --- /dev/null +++ b/packages/lending/src/actions/borrowObligationLiquidity.tsx @@ -0,0 +1,144 @@ +import { + contexts, + findOrCreateAccountByMint, + LEND_HOST_FEE_ADDRESS, + LENDING_PROGRAM_ID, + notify, + ParsedAccount, + toLamports, +} from '@oyster/common'; +import { AccountLayout, MintInfo } from '@solana/spl-token'; +import { + Account, + Connection, + PublicKey, + TransactionInstruction, +} from '@solana/web3.js'; +import { + borrowObligationLiquidityInstruction, + Obligation, + Reserve, +} from '@solana/spl-token-lending'; +import { refreshObligationAndReserves } from './refreshObligationAndReserves'; + +const { cache, MintParser } = contexts.Accounts; +const { sendTransaction } = contexts.Connection; + +export const borrowObligationLiquidity = async ( + connection: Connection, + wallet: any, + liquidityAmount: number, + borrowReserve: ParsedAccount, + obligation: ParsedAccount, +) => { + notify({ + message: 'Borrowing liquidity...', + description: 'Please review transactions to approve.', + type: 'warn', + }); + + let signers: Account[] = []; + let instructions: TransactionInstruction[] = []; + let cleanupInstructions: TransactionInstruction[] = []; + let finalCleanupInstructions: TransactionInstruction[] = []; + + const [lendingMarketAuthority] = await PublicKey.findProgramAddress( + [borrowReserve.info.lendingMarket.toBuffer()], + LENDING_PROGRAM_ID, + ); + + const accountRentExempt = await connection.getMinimumBalanceForRentExemption( + AccountLayout.span, + ); + + // Creates host fee account if it doesn't exist + let hostFeeReceiver = LEND_HOST_FEE_ADDRESS + ? findOrCreateAccountByMint( + wallet.publicKey, + LEND_HOST_FEE_ADDRESS, + instructions, + [], + accountRentExempt, + borrowReserve.info.liquidity.mintPubkey, + signers, + ) + : undefined; + + const mint = (await cache.query( + connection, + borrowReserve.info.liquidity.mintPubkey, + MintParser, + )) as ParsedAccount; + + // @TODO: handle 100% -> u64::MAX + const amountLamports = toLamports(liquidityAmount, mint?.info); + + let destinationLiquidity = await findOrCreateAccountByMint( + wallet.publicKey, + wallet.publicKey, + instructions, + finalCleanupInstructions, + accountRentExempt, + borrowReserve.info.liquidity.mintPubkey, + signers, + ); + + if (instructions.length > 0) { + // create all accounts in one transaction + let { txid } = await sendTransaction(connection, wallet, instructions, [ + ...signers, + ]); + + notify({ + // @TODO: change message + message: 'Obligation accounts created', + description: `Transaction ${txid}`, + type: 'success', + }); + } + + notify({ + message: 'Borrowing funds...', + description: 'Please review transactions to approve.', + type: 'warn', + }); + + signers = []; + instructions = []; + cleanupInstructions = [...finalCleanupInstructions]; + + instructions.push( + ...await refreshObligationAndReserves(connection, obligation), + borrowObligationLiquidityInstruction( + amountLamports, + borrowReserve.info.liquidity.supplyPubkey, + destinationLiquidity, + borrowReserve.pubkey, + borrowReserve.info.liquidity.feeReceiver, + obligation.pubkey, + borrowReserve.info.lendingMarket, + lendingMarketAuthority, + obligation.info.owner, + hostFeeReceiver, + ), + ); + + try { + let { txid } = await sendTransaction( + connection, + wallet, + instructions.concat(cleanupInstructions), + signers, + true, + ); + + notify({ + message: 'Liquidity borrowed.', + type: 'success', + description: `Transaction - ${txid}`, + }); + } catch (ex) { + console.error(ex); + throw new Error(); + } +}; diff --git a/packages/lending/src/actions/obligation.tsx b/packages/lending/src/actions/createAccount.tsx similarity index 69% rename from packages/lending/src/actions/obligation.tsx rename to packages/lending/src/actions/createAccount.tsx index 4543ff37..6ef61e93 100644 --- a/packages/lending/src/actions/obligation.tsx +++ b/packages/lending/src/actions/createAccount.tsx @@ -1,25 +1,26 @@ +import { LENDING_PROGRAM_ID } from '@oyster/common'; import { Account, PublicKey, SystemProgram, TransactionInstruction, } from '@solana/web3.js'; -import { utils } from '@oyster/common'; -import { LendingObligationLayout } from '../models'; -const { LENDING_PROGRAM_ID } = utils; -export function createUninitializedObligation( + +export function createAccount( instructions: TransactionInstruction[], payer: PublicKey, amount: number, signers: Account[], + space: number, ) { const account = new Account(); + instructions.push( SystemProgram.createAccount({ fromPubkey: payer, newAccountPubkey: account.publicKey, lamports: amount, - space: LendingObligationLayout.span, + space, programId: LENDING_PROGRAM_ID, }), ); diff --git a/packages/lending/src/actions/createObligation.tsx b/packages/lending/src/actions/createObligation.tsx new file mode 100644 index 00000000..eb587009 --- /dev/null +++ b/packages/lending/src/actions/createObligation.tsx @@ -0,0 +1,18 @@ +import { OBLIGATION_SIZE } from '@solana/spl-token-lending'; +import { Account, PublicKey, TransactionInstruction } from '@solana/web3.js'; +import { createAccount } from './createAccount'; + +export function createObligation( + instructions: TransactionInstruction[], + payer: PublicKey, + amount: number, + signers: Account[], +) { + return createAccount( + instructions, + payer, + amount, + signers, + OBLIGATION_SIZE, + ); +} diff --git a/packages/lending/src/actions/createReserve.tsx b/packages/lending/src/actions/createReserve.tsx new file mode 100644 index 00000000..170a8360 --- /dev/null +++ b/packages/lending/src/actions/createReserve.tsx @@ -0,0 +1,18 @@ +import { RESERVE_SIZE } from '@solana/spl-token-lending'; +import { Account, PublicKey, TransactionInstruction } from '@solana/web3.js'; +import { createAccount } from './createAccount'; + +export function createReserve( + instructions: TransactionInstruction[], + payer: PublicKey, + amount: number, + signers: Account[], +) { + return createAccount( + instructions, + payer, + amount, + signers, + RESERVE_SIZE, + ); +} diff --git a/packages/lending/src/actions/deposit.tsx b/packages/lending/src/actions/deposit.tsx deleted file mode 100644 index eb60ae99..00000000 --- a/packages/lending/src/actions/deposit.tsx +++ /dev/null @@ -1,153 +0,0 @@ -import { - Account, - Connection, - PublicKey, - TransactionInstruction, -} from '@solana/web3.js'; -import { contexts, utils, models, actions, TokenAccount } from '@oyster/common'; -import { - accrueInterestInstruction, - depositInstruction, - initReserveInstruction, - LendingReserve, -} from './../models/lending'; -import { AccountLayout } from '@solana/spl-token'; - -const { sendTransaction } = contexts.Connection; -const { LENDING_PROGRAM_ID, notify } = utils; -const { - createUninitializedAccount, - ensureSplAccount, - findOrCreateAccountByMint, -} = actions; -const { approve } = models; - -export const deposit = async ( - from: TokenAccount, - amountLamports: number, - reserve: LendingReserve, - reserveAddress: PublicKey, - connection: Connection, - wallet: any, -) => { - notify({ - message: 'Depositing funds...', - description: 'Please review transactions to approve.', - type: 'warn', - }); - - const isInitalized = true; // TODO: finish reserve init - - // user from account - const signers: Account[] = []; - const instructions: TransactionInstruction[] = []; - const cleanupInstructions: TransactionInstruction[] = []; - - const accountRentExempt = await connection.getMinimumBalanceForRentExemption( - AccountLayout.span, - ); - - const [authority] = await PublicKey.findProgramAddress( - [reserve.lendingMarket.toBuffer()], // which account should be authority - LENDING_PROGRAM_ID, - ); - - const fromAccount = ensureSplAccount( - instructions, - cleanupInstructions, - from, - wallet.publicKey, - amountLamports + accountRentExempt, - signers, - ); - - // create approval for transfer transactions - const transferAuthority = approve( - instructions, - cleanupInstructions, - fromAccount, - wallet.publicKey, - amountLamports, - ); - - signers.push(transferAuthority); - - let toAccount: PublicKey; - if (isInitalized) { - // get destination account - toAccount = await findOrCreateAccountByMint( - wallet.publicKey, - wallet.publicKey, - instructions, - cleanupInstructions, - accountRentExempt, - reserve.collateralMint, - signers, - ); - } else { - toAccount = createUninitializedAccount( - instructions, - wallet.publicKey, - accountRentExempt, - signers, - ); - } - - if (isInitalized) { - instructions.push(accrueInterestInstruction(reserveAddress)); - - // deposit - instructions.push( - depositInstruction( - amountLamports, - fromAccount, - toAccount, - reserve.lendingMarket, - authority, - transferAuthority.publicKey, - reserveAddress, - reserve.liquiditySupply, - reserve.collateralMint, - ), - ); - } else { - // TODO: finish reserve init - const MAX_UTILIZATION_RATE = 80; - instructions.push( - initReserveInstruction( - amountLamports, - MAX_UTILIZATION_RATE, - fromAccount, - toAccount, - reserveAddress, - reserve.liquidityMint, - reserve.liquiditySupply, - reserve.collateralMint, - reserve.collateralSupply, - reserve.lendingMarket, - authority, - transferAuthority.publicKey, - reserve.dexMarket, - ), - ); - } - - try { - let { txid } = await sendTransaction( - connection, - wallet, - instructions.concat(cleanupInstructions), - signers, - true, - ); - - notify({ - message: 'Funds deposited.', - type: 'success', - description: `Transaction - ${txid}`, - }); - } catch { - // TODO: - throw new Error(); - } -}; diff --git a/packages/lending/src/actions/depositObligationCollateral.tsx b/packages/lending/src/actions/depositObligationCollateral.tsx new file mode 100644 index 00000000..e30a2931 --- /dev/null +++ b/packages/lending/src/actions/depositObligationCollateral.tsx @@ -0,0 +1,90 @@ +import { + sendTransaction, + LENDING_PROGRAM_ID, + models, + notify, + TokenAccount, +} from '@oyster/common'; +import { + Account, + Connection, + PublicKey, + TransactionInstruction, +} from '@solana/web3.js'; +import { + depositObligationCollateralInstruction, + Reserve, +} from '@solana/spl-token-lending'; + +const { approve } = models; + +export const depositObligationCollateral = async ( + connection: Connection, + wallet: any, + collateralAmount: number, + source: TokenAccount, + reserve: Reserve, + reserveAddress: PublicKey, + obligationAddress: PublicKey +) => { + notify({ + message: 'Depositing collateral...', + description: 'Please review transactions to approve.', + type: 'warn', + }); + + // user from account + const signers: Account[] = []; + const instructions: TransactionInstruction[] = []; + const cleanupInstructions: TransactionInstruction[] = []; + + const [lendingMarketAuthority] = await PublicKey.findProgramAddress( + [reserve.lendingMarket.toBuffer()], + LENDING_PROGRAM_ID, + ); + + const sourceCollateral = source.pubkey; + + // create approval for transfer transactions + const transferAuthority = approve( + instructions, + cleanupInstructions, + sourceCollateral, + wallet.publicKey, + collateralAmount, + ); + + signers.push(transferAuthority); + + instructions.push( + depositObligationCollateralInstruction( + collateralAmount, + sourceCollateral, + reserve.collateral.mintPubkey, + reserveAddress, + obligationAddress, + reserve.lendingMarket, + lendingMarketAuthority, + wallet.publicKey, + transferAuthority.publicKey, + ), + ); + + try { + let { txid } = await sendTransaction( + connection, + wallet, + instructions.concat(cleanupInstructions), + signers, + true, + ); + + notify({ + message: 'Collateral deposited.', + type: 'success', + description: `Transaction - ${txid}`, + }); + } catch { + // TODO: + } +}; diff --git a/packages/lending/src/actions/depositReserveLiquidity.tsx b/packages/lending/src/actions/depositReserveLiquidity.tsx new file mode 100644 index 00000000..1526ed19 --- /dev/null +++ b/packages/lending/src/actions/depositReserveLiquidity.tsx @@ -0,0 +1,119 @@ +import { + ensureSplAccount, + findOrCreateAccountByMint, + LENDING_PROGRAM_ID, + models, + notify, + sendTransaction, + TokenAccount, +} from '@oyster/common'; +import { AccountLayout } from '@solana/spl-token'; +import { + Account, + Connection, + PublicKey, + TransactionInstruction, +} from '@solana/web3.js'; +import { + depositReserveLiquidityInstruction, + refreshReserveInstruction, + Reserve, +} from '@solana/spl-token-lending'; + +const { approve } = models; + +export const depositReserveLiquidity = async ( + connection: Connection, + wallet: any, + liquidityAmount: number, + source: TokenAccount, + reserve: Reserve, + reserveAddress: PublicKey, +) => { + notify({ + message: 'Depositing liquidity...', + description: 'Please review transactions to approve.', + type: 'warn', + }); + + // user from account + const signers: Account[] = []; + const instructions: TransactionInstruction[] = []; + const cleanupInstructions: TransactionInstruction[] = []; + + const accountRentExempt = await connection.getMinimumBalanceForRentExemption( + AccountLayout.span, + ); + + const [lendingMarketAuthority] = await PublicKey.findProgramAddress( + [reserve.lendingMarket.toBuffer()], // which account should be authority + LENDING_PROGRAM_ID, + ); + + const sourceLiquidityAccount = ensureSplAccount( + instructions, + cleanupInstructions, + source, + wallet.publicKey, + liquidityAmount + accountRentExempt, + signers, + ); + + // create approval for transfer transactions + const transferAuthority = approve( + instructions, + cleanupInstructions, + sourceLiquidityAccount, + wallet.publicKey, + liquidityAmount, + ); + + signers.push(transferAuthority); + + let destinationCollateralAccount: PublicKey = await findOrCreateAccountByMint( + wallet.publicKey, + wallet.publicKey, + instructions, + cleanupInstructions, + accountRentExempt, + reserve.collateral.mintPubkey, + signers, + ); + + instructions.push( + refreshReserveInstruction( + reserveAddress, + reserve.liquidity.oraclePubkey, + ), + depositReserveLiquidityInstruction( + liquidityAmount, + sourceLiquidityAccount, + destinationCollateralAccount, + reserveAddress, + reserve.liquidity.supplyPubkey, + reserve.collateral.mintPubkey, + reserve.lendingMarket, + lendingMarketAuthority, + transferAuthority.publicKey, + ), + ); + + try { + let { txid } = await sendTransaction( + connection, + wallet, + instructions.concat(cleanupInstructions), + signers, + true, + ); + + notify({ + message: 'Liquidity deposited.', + type: 'success', + description: `Transaction - ${txid}`, + }); + } catch { + // TODO: + throw new Error(); + } +}; diff --git a/packages/lending/src/actions/index.ts b/packages/lending/src/actions/index.ts index 8301458d..b340a257 100644 --- a/packages/lending/src/actions/index.ts +++ b/packages/lending/src/actions/index.ts @@ -1,5 +1,15 @@ -export { borrow } from './borrow'; -export { deposit } from './deposit'; -export { repay } from './repay'; -export { withdraw } from './withdraw'; -export { liquidate } from './liquidate'; +export { borrowObligationLiquidity } from './borrowObligationLiquidity'; +export { createAccount } from './createAccount'; +export { createObligation } from './createObligation'; +export { createReserve } from './createReserve'; +export { depositObligationCollateral } from './depositObligationCollateral'; +export { depositReserveLiquidity } from './depositReserveLiquidity'; +export { initObligation } from './initObligation'; +export { initReserve } from './initReserve'; +export { liquidateObligation } from './liquidateObligation'; +export { redeemReserveCollateral } from './redeemReserveCollateral'; +export { refreshObligationAndReserves } from './refreshObligationAndReserves'; +export { repayObligationLiquidity } from './repayObligationLiquidity'; +export { withdrawObligationCollateral } from './withdrawObligationCollateral'; + +// @TODO: add actions for other instructions diff --git a/packages/lending/src/actions/initObligation.tsx b/packages/lending/src/actions/initObligation.tsx new file mode 100644 index 00000000..46229cb7 --- /dev/null +++ b/packages/lending/src/actions/initObligation.tsx @@ -0,0 +1,63 @@ +import { notify, sendTransaction } from '@oyster/common'; +import { + Account, + Connection, + PublicKey, + TransactionInstruction, +} from '@solana/web3.js'; +import { initObligationInstruction, OBLIGATION_SIZE } from '@solana/spl-token-lending'; +import { createObligation } from './createObligation'; + +export const initObligation = async ( + connection: Connection, + wallet: any, + lendingMarket: PublicKey, +) => { + notify({ + message: 'Initializing obligation...', + description: 'Please review transactions to approve.', + type: 'warn', + }); + + const signers: Account[] = []; + const instructions: TransactionInstruction[] = []; + const cleanupInstructions: TransactionInstruction[] = []; + + const obligationRentExempt = await connection.getMinimumBalanceForRentExemption( + OBLIGATION_SIZE, + ); + + const obligationAddress = createObligation( + instructions, + wallet.publicKey, + obligationRentExempt, + signers, + ); + + instructions.push( + initObligationInstruction( + obligationAddress, + lendingMarket, + wallet.publicKey, + ), + ); + + try { + let { txid } = await sendTransaction( + connection, + wallet, + instructions.concat(cleanupInstructions), + signers, + true, + ); + + notify({ + message: 'Obligation initialized.', + type: 'success', + description: `Transaction - ${txid}`, + }); + } catch { + // TODO: + throw new Error(); + } +}; diff --git a/packages/lending/src/actions/initReserve.tsx b/packages/lending/src/actions/initReserve.tsx new file mode 100644 index 00000000..b383d6ab --- /dev/null +++ b/packages/lending/src/actions/initReserve.tsx @@ -0,0 +1,117 @@ +import { + createUninitializedAccount, + ensureSplAccount, + sendTransaction, + LENDING_PROGRAM_ID, + models, + notify, + TokenAccount, +} from '@oyster/common'; +import { AccountLayout } from '@solana/spl-token'; +import { + Account, + Connection, + PublicKey, + TransactionInstruction, +} from '@solana/web3.js'; +import { initReserveInstruction, Reserve } from '@solana/spl-token-lending'; + +const { approve } = models; + +export const initReserve = async ( + connection: Connection, + wallet: any, + liquidityAmount: number, + source: TokenAccount, + reserve: Reserve, + reserveAddress: PublicKey, + pythProduct: PublicKey, +) => { + notify({ + message: 'Initializing reserve...', + description: 'Please review transactions to approve.', + type: 'warn', + }); + + // user from account + const signers: Account[] = []; + const instructions: TransactionInstruction[] = []; + const cleanupInstructions: TransactionInstruction[] = []; + + const accountRentExempt = await connection.getMinimumBalanceForRentExemption( + AccountLayout.span, + ); + + const [lendingMarketAuthority] = await PublicKey.findProgramAddress( + [reserve.lendingMarket.toBuffer()], // which account should be authority + LENDING_PROGRAM_ID, + ); + + const sourceLiquidityAccount = ensureSplAccount( + instructions, + cleanupInstructions, + source, + wallet.publicKey, + liquidityAmount + accountRentExempt, + signers, + ); + + // create approval for transfer transactions + const transferAuthority = approve( + instructions, + cleanupInstructions, + sourceLiquidityAccount, + wallet.publicKey, + liquidityAmount, + ); + + signers.push(transferAuthority); + + let destinationCollateralAccount: PublicKey = createUninitializedAccount( + instructions, + wallet.publicKey, + accountRentExempt, + signers, + ); + + instructions.push( + initReserveInstruction( + liquidityAmount, + reserve.config, + sourceLiquidityAccount, + destinationCollateralAccount, + reserveAddress, + reserve.liquidity.mintPubkey, + reserve.liquidity.supplyPubkey, + reserve.liquidity.feeReceiver, + pythProduct, + reserve.liquidity.oraclePubkey, + reserve.collateral.mintPubkey, + reserve.collateral.supplyPubkey, + reserve.lendingMarket, + lendingMarketAuthority, + // @TODO: must provide owner as arg if wallet isn't lending market owner + wallet.publicKey, + transferAuthority.publicKey, + ), + ); + + try { + let { txid } = await sendTransaction( + connection, + wallet, + instructions.concat(cleanupInstructions), + signers, + true, + ); + + notify({ + message: 'Reserve initialized.', + type: 'success', + description: `Transaction - ${txid}`, + }); + } catch { + // TODO: + throw new Error(); + } +}; diff --git a/packages/lending/src/actions/liquidate.tsx b/packages/lending/src/actions/liquidate.tsx deleted file mode 100644 index 4758ac98..00000000 --- a/packages/lending/src/actions/liquidate.tsx +++ /dev/null @@ -1,157 +0,0 @@ -import { - Account, - Connection, - PublicKey, - TransactionInstruction, -} from '@solana/web3.js'; -import { - contexts, - utils, - actions, - models, - ParsedAccount, - TokenAccount, -} from '@oyster/common'; -import { - accrueInterestInstruction, - LendingReserve, -} from './../models/lending/reserve'; -import { liquidateInstruction } from './../models/lending/liquidate'; -import { AccountLayout } from '@solana/spl-token'; -import { LendingMarket, LendingObligation } from '../models'; -const { cache } = contexts.Accounts; -const { approve } = models; -const { - createTempMemoryAccount, - ensureSplAccount, - findOrCreateAccountByMint, -} = actions; -const { sendTransaction } = contexts.Connection; -const { LENDING_PROGRAM_ID, notify } = utils; - -export const liquidate = async ( - connection: Connection, - wallet: any, - from: TokenAccount, // liquidity account - amountLamports: number, // in liquidty token (lamports) - - // which loan to repay - obligation: ParsedAccount, - - repayReserve: ParsedAccount, - - withdrawReserve: ParsedAccount, -) => { - notify({ - message: 'Repaying funds...', - description: 'Please review transactions to approve.', - type: 'warn', - }); - - // user from account - const signers: Account[] = []; - const instructions: TransactionInstruction[] = []; - const cleanupInstructions: TransactionInstruction[] = []; - - const accountRentExempt = await connection.getMinimumBalanceForRentExemption( - AccountLayout.span, - ); - - const [authority] = await PublicKey.findProgramAddress( - [repayReserve.info.lendingMarket.toBuffer()], - LENDING_PROGRAM_ID, - ); - - const fromAccount = ensureSplAccount( - instructions, - cleanupInstructions, - from, - wallet.publicKey, - amountLamports + accountRentExempt, - signers, - ); - - // create approval for transfer transactions - const transferAuthority = approve( - instructions, - cleanupInstructions, - fromAccount, - wallet.publicKey, - amountLamports, - ); - signers.push(transferAuthority); - - // get destination account - const toAccount = await findOrCreateAccountByMint( - wallet.publicKey, - wallet.publicKey, - instructions, - cleanupInstructions, - accountRentExempt, - withdrawReserve.info.collateralMint, - signers, - ); - - const dexMarketAddress = repayReserve.info.dexMarketOption - ? repayReserve.info.dexMarket - : withdrawReserve.info.dexMarket; - const dexMarket = cache.get(dexMarketAddress); - - if (!dexMarket) { - throw new Error(`Dex market doesn't exist.`); - } - - const market = cache.get( - withdrawReserve.info.lendingMarket, - ) as ParsedAccount; - - const dexOrderBookSide = market.info.quoteMint.equals( - repayReserve.info.liquidityMint, - ) - ? dexMarket?.info.asks - : dexMarket?.info.bids; - - const memory = createTempMemoryAccount( - instructions, - wallet.publicKey, - signers, - LENDING_PROGRAM_ID, - ); - - instructions.push( - accrueInterestInstruction(repayReserve.pubkey, withdrawReserve.pubkey), - ); - - instructions.push( - liquidateInstruction( - amountLamports, - fromAccount, - toAccount, - repayReserve.pubkey, - repayReserve.info.liquiditySupply, - withdrawReserve.pubkey, - withdrawReserve.info.collateralSupply, - obligation.pubkey, - repayReserve.info.lendingMarket, - authority, - transferAuthority.publicKey, - dexMarketAddress, - dexOrderBookSide, - memory, - ), - ); - - let { txid } = await sendTransaction( - connection, - wallet, - instructions.concat(cleanupInstructions), - signers, - true, - ); - - notify({ - message: 'Funds liquidated.', - type: 'success', - description: `Transaction - ${txid}`, - }); -}; diff --git a/packages/lending/src/actions/liquidateObligation.tsx b/packages/lending/src/actions/liquidateObligation.tsx new file mode 100644 index 00000000..6ee50648 --- /dev/null +++ b/packages/lending/src/actions/liquidateObligation.tsx @@ -0,0 +1,112 @@ +import { + ensureSplAccount, + findOrCreateAccountByMint, + LENDING_PROGRAM_ID, + models, + notify, + ParsedAccount, + sendTransaction, + TokenAccount, +} from '@oyster/common'; +import { AccountLayout } from '@solana/spl-token'; +import { + Account, + Connection, + PublicKey, + TransactionInstruction, +} from '@solana/web3.js'; +import { liquidateObligationInstruction, Obligation, Reserve } from '@solana/spl-token-lending'; +import { refreshObligationAndReserves } from './refreshObligationAndReserves'; + +const { approve } = models; + +export const liquidateObligation = async ( + connection: Connection, + wallet: any, + liquidityAmount: number, + source: TokenAccount, + repayReserve: ParsedAccount, + withdrawReserve: ParsedAccount, + obligation: ParsedAccount, +) => { + notify({ + message: 'Liquidating obligation...', + description: 'Please review transactions to approve.', + type: 'warn', + }); + + // user from account + const signers: Account[] = []; + const instructions: TransactionInstruction[] = []; + const cleanupInstructions: TransactionInstruction[] = []; + + const accountRentExempt = await connection.getMinimumBalanceForRentExemption( + AccountLayout.span, + ); + + const [lendingMarketAuthority] = await PublicKey.findProgramAddress( + [repayReserve.info.lendingMarket.toBuffer()], + LENDING_PROGRAM_ID, + ); + + const sourceAccount = ensureSplAccount( + instructions, + cleanupInstructions, + source, + wallet.publicKey, + liquidityAmount + accountRentExempt, + signers, + ); + + // create approval for transfer transactions + const transferAuthority = approve( + instructions, + cleanupInstructions, + sourceAccount, + wallet.publicKey, + liquidityAmount, + ); + signers.push(transferAuthority); + + // get destination account + const toAccount = await findOrCreateAccountByMint( + wallet.publicKey, + wallet.publicKey, + instructions, + cleanupInstructions, + accountRentExempt, + withdrawReserve.info.collateral.mintPubkey, + signers, + ); + + instructions.push( + ...await refreshObligationAndReserves(connection, obligation), + liquidateObligationInstruction( + liquidityAmount, + sourceAccount, + toAccount, + repayReserve.pubkey, + repayReserve.info.liquidity.supplyPubkey, + withdrawReserve.pubkey, + withdrawReserve.info.collateral.supplyPubkey, + obligation.pubkey, + repayReserve.info.lendingMarket, + lendingMarketAuthority, + transferAuthority.publicKey, + ), + ); + + let { txid } = await sendTransaction( + connection, + wallet, + instructions.concat(cleanupInstructions), + signers, + true, + ); + + notify({ + message: 'Obligation liquidated.', + type: 'success', + description: `Transaction - ${txid}`, + }); +}; diff --git a/packages/lending/src/actions/withdraw.tsx b/packages/lending/src/actions/redeemReserveCollateral.tsx similarity index 57% rename from packages/lending/src/actions/withdraw.tsx rename to packages/lending/src/actions/redeemReserveCollateral.tsx index 9852e8a4..8b84ea60 100644 --- a/packages/lending/src/actions/withdraw.tsx +++ b/packages/lending/src/actions/redeemReserveCollateral.tsx @@ -1,31 +1,36 @@ +import { + findOrCreateAccountByMint, + LENDING_PROGRAM_ID, + models, + notify, + sendTransaction, + TokenAccount, +} from '@oyster/common'; +import { AccountLayout } from '@solana/spl-token'; import { Account, Connection, PublicKey, TransactionInstruction, } from '@solana/web3.js'; -import { contexts, utils, actions, models, TokenAccount } from '@oyster/common'; import { - accrueInterestInstruction, - LendingReserve, - withdrawInstruction, -} from './../models/lending'; -import { AccountLayout } from '@solana/spl-token'; + redeemReserveCollateralInstruction, + refreshReserveInstruction, + Reserve, +} from '@solana/spl-token-lending'; + const { approve } = models; -const { findOrCreateAccountByMint } = actions; -const { sendTransaction } = contexts.Connection; -const { LENDING_PROGRAM_ID, notify } = utils; -export const withdraw = async ( - from: TokenAccount, // CollateralAccount - amountLamports: number, // in collateral token (lamports) - reserve: LendingReserve, - reserveAddress: PublicKey, +export const redeemReserveCollateral = async ( connection: Connection, wallet: any, + collateralAmount: number, + source: TokenAccount, + reserve: Reserve, + reserveAddress: PublicKey, ) => { notify({ - message: 'Withdrawing funds...', + message: 'Redeeming collateral...', description: 'Please review transactions to approve.', type: 'warn', }); @@ -39,53 +44,55 @@ export const withdraw = async ( AccountLayout.span, ); - const [authority] = await PublicKey.findProgramAddress( + const [lendingMarketAuthority] = await PublicKey.findProgramAddress( [reserve.lendingMarket.toBuffer()], LENDING_PROGRAM_ID, ); - const fromAccount = from.pubkey; + const sourceCollateral = source.pubkey; // create approval for transfer transactions const transferAuthority = approve( instructions, cleanupInstructions, - fromAccount, + sourceCollateral, wallet.publicKey, - amountLamports, + collateralAmount, ); signers.push(transferAuthority); // get destination account - const toAccount = await findOrCreateAccountByMint( + const destinationLiquidity = await findOrCreateAccountByMint( wallet.publicKey, wallet.publicKey, instructions, cleanupInstructions, accountRentExempt, - reserve.liquidityMint, + reserve.liquidity.mintPubkey, signers, ); - instructions.push(accrueInterestInstruction(reserveAddress)); - instructions.push( - withdrawInstruction( - amountLamports, - fromAccount, - toAccount, + refreshReserveInstruction( + reserveAddress, + reserve.liquidity.oraclePubkey, + ), + redeemReserveCollateralInstruction( + collateralAmount, + sourceCollateral, + destinationLiquidity, reserveAddress, - reserve.collateralMint, - reserve.liquiditySupply, + reserve.collateral.mintPubkey, + reserve.liquidity.supplyPubkey, reserve.lendingMarket, - authority, + lendingMarketAuthority, transferAuthority.publicKey, ), ); try { - let { txid } = await sendTransaction( + let { txid } = await sendTransaction( connection, wallet, instructions.concat(cleanupInstructions), @@ -94,7 +101,7 @@ export const withdraw = async ( ); notify({ - message: 'Funds deposited.', + message: 'Collateral redeemed.', type: 'success', description: `Transaction - ${txid}`, }); diff --git a/packages/lending/src/actions/refreshObligationAndReserves.tsx b/packages/lending/src/actions/refreshObligationAndReserves.tsx new file mode 100644 index 00000000..17120921 --- /dev/null +++ b/packages/lending/src/actions/refreshObligationAndReserves.tsx @@ -0,0 +1,50 @@ +import { contexts, ParsedAccount } from '@oyster/common'; +import { + Obligation, + refreshObligationInstruction, + refreshReserveInstruction, + Reserve, +} from '@solana/spl-token-lending'; +import { Connection, PublicKey, TransactionInstruction } from '@solana/web3.js'; +import { ReserveParser } from '../models'; + +const { cache } = contexts.Accounts; + +export const refreshObligationAndReserves = async ( + connection: Connection, + obligation: ParsedAccount, +) => { + const instructions = [] as TransactionInstruction[]; + const reserves = {} as Record; + + for (const collateral of obligation.info.deposits) { + reserves[collateral.depositReserve.toBase58()] = collateral.depositReserve; + } + for (const liquidity of obligation.info.borrows) { + reserves[liquidity.borrowReserve.toBase58()] = liquidity.borrowReserve; + } + + await Promise.all( + Object.values(reserves).map(async pubkey => { + const reserve = (await cache.query( + connection, + pubkey, + ReserveParser, + )) as ParsedAccount; + + instructions.push( + refreshReserveInstruction(pubkey, reserve.info.liquidity.oraclePubkey), + ); + }), + ); + + instructions.push( + refreshObligationInstruction( + obligation.pubkey, + obligation.info.deposits.map(collateral => collateral.depositReserve), + obligation.info.borrows.map(liquidity => liquidity.borrowReserve), + ), + ); + + return instructions; +}; diff --git a/packages/lending/src/actions/repay.tsx b/packages/lending/src/actions/repay.tsx deleted file mode 100644 index 7e8ac77a..00000000 --- a/packages/lending/src/actions/repay.tsx +++ /dev/null @@ -1,157 +0,0 @@ -import { - Account, - Connection, - PublicKey, - TransactionInstruction, -} from '@solana/web3.js'; -import { - contexts, - utils, - actions, - models, - ParsedAccount, - TokenAccount, -} from '@oyster/common'; - -import { - accrueInterestInstruction, - LendingReserve, -} from './../models/lending/reserve'; -import { repayInstruction } from './../models/lending/repay'; -import { AccountLayout, Token, NATIVE_MINT } from '@solana/spl-token'; - -import { LendingObligation } from '../models'; -const { approve } = models; -const { createTokenAccount, findOrCreateAccountByMint } = actions; -const { sendTransaction } = contexts.Connection; -const { LENDING_PROGRAM_ID, TOKEN_PROGRAM_ID, notify } = utils; - -export const repay = async ( - from: TokenAccount, - repayAmount: number, - - // which loan to repay - obligation: ParsedAccount, - - obligationToken: TokenAccount, - - repayReserve: ParsedAccount, - - withdrawReserve: ParsedAccount, - - connection: Connection, - wallet: any, -) => { - notify({ - message: 'Repaying funds...', - description: 'Please review transactions to approve.', - type: 'warn', - }); - - // user from account - const signers: Account[] = []; - const instructions: TransactionInstruction[] = []; - const cleanupInstructions: TransactionInstruction[] = []; - - const accountRentExempt = await connection.getMinimumBalanceForRentExemption( - AccountLayout.span, - ); - - const [authority] = await PublicKey.findProgramAddress( - [repayReserve.info.lendingMarket.toBuffer()], - LENDING_PROGRAM_ID, - ); - - let fromAccount = from.pubkey; - if ( - wallet.publicKey.equals(fromAccount) && - repayReserve.info.liquidityMint.equals(NATIVE_MINT) - ) { - fromAccount = createTokenAccount( - instructions, - wallet.publicKey, - accountRentExempt + repayAmount, - NATIVE_MINT, - wallet.publicKey, - signers, - ); - cleanupInstructions.push( - Token.createCloseAccountInstruction( - TOKEN_PROGRAM_ID, - fromAccount, - wallet.publicKey, - wallet.publicKey, - [], - ), - ); - } - - // create approval for transfer transactions - const transferAuthority = approve( - instructions, - cleanupInstructions, - fromAccount, - wallet.publicKey, - repayAmount, - ); - signers.push(transferAuthority); - - // get destination account - const toAccount = await findOrCreateAccountByMint( - wallet.publicKey, - wallet.publicKey, - instructions, - cleanupInstructions, - accountRentExempt, - withdrawReserve.info.collateralMint, - signers, - ); - - // create approval for transfer transactions - approve( - instructions, - cleanupInstructions, - obligationToken.pubkey, - wallet.publicKey, - obligationToken.info.amount.toNumber(), - true, - // reuse transfer authority - transferAuthority.publicKey, - ); - - instructions.push( - accrueInterestInstruction(repayReserve.pubkey, withdrawReserve.pubkey), - ); - - instructions.push( - repayInstruction( - repayAmount, - fromAccount, - toAccount, - repayReserve.pubkey, - repayReserve.info.liquiditySupply, - withdrawReserve.pubkey, - withdrawReserve.info.collateralSupply, - obligation.pubkey, - obligation.info.tokenMint, - obligationToken.pubkey, - repayReserve.info.lendingMarket, - authority, - transferAuthority.publicKey, - ), - ); - - let { txid } = await sendTransaction( - connection, - wallet, - instructions.concat(cleanupInstructions), - signers, - true, - ); - - notify({ - message: 'Funds repaid.', - type: 'success', - description: `Transaction - ${txid}`, - }); -}; diff --git a/packages/lending/src/actions/repayObligationLiquidity.tsx b/packages/lending/src/actions/repayObligationLiquidity.tsx new file mode 100644 index 00000000..ef9f8f58 --- /dev/null +++ b/packages/lending/src/actions/repayObligationLiquidity.tsx @@ -0,0 +1,105 @@ +import { + createTokenAccount, + models, + notify, + ParsedAccount, + sendTransaction, + TOKEN_PROGRAM_ID, + TokenAccount, +} from '@oyster/common'; +import { AccountLayout, NATIVE_MINT, Token } from '@solana/spl-token'; +import { Account, Connection, TransactionInstruction } from '@solana/web3.js'; +import { + Obligation, + repayObligationLiquidityInstruction, + Reserve, +} from '@solana/spl-token-lending'; +import { refreshObligationAndReserves } from './refreshObligationAndReserves'; + +const { approve } = models; + +export const repayObligationLiquidity = async ( + connection: Connection, + wallet: any, + liquidityAmount: number, + source: TokenAccount, + repayReserve: ParsedAccount, + obligation: ParsedAccount, +) => { + notify({ + message: 'Repaying liquidity...', + description: 'Please review transactions to approve.', + type: 'warn', + }); + + // user from account + const signers: Account[] = []; + const instructions: TransactionInstruction[] = []; + const cleanupInstructions: TransactionInstruction[] = []; + + const accountRentExempt = await connection.getMinimumBalanceForRentExemption( + AccountLayout.span, + ); + + let sourceLiquidity = source.pubkey; + if ( + wallet.publicKey.equals(sourceLiquidity) && + repayReserve.info.liquidity.mintPubkey.equals(NATIVE_MINT) + ) { + sourceLiquidity = createTokenAccount( + instructions, + wallet.publicKey, + accountRentExempt + liquidityAmount, + NATIVE_MINT, + wallet.publicKey, + signers, + ); + cleanupInstructions.push( + Token.createCloseAccountInstruction( + TOKEN_PROGRAM_ID, + sourceLiquidity, + wallet.publicKey, + wallet.publicKey, + [], + ), + ); + } + + // create approval for transfer transactions + const transferAuthority = approve( + instructions, + cleanupInstructions, + sourceLiquidity, + wallet.publicKey, + liquidityAmount, + ); + + signers.push(transferAuthority); + + instructions.push( + ...await refreshObligationAndReserves(connection, obligation), + repayObligationLiquidityInstruction( + liquidityAmount, + sourceLiquidity, + repayReserve.info.liquidity.mintPubkey, + repayReserve.pubkey, + obligation.pubkey, + repayReserve.info.lendingMarket, + transferAuthority.publicKey, + ), + ); + + let { txid } = await sendTransaction( + connection, + wallet, + instructions.concat(cleanupInstructions), + signers, + true, + ); + + notify({ + message: 'Liquidity repaid.', + type: 'success', + description: `Transaction - ${txid}`, + }); +}; diff --git a/packages/lending/src/actions/withdrawObligationCollateral.tsx b/packages/lending/src/actions/withdrawObligationCollateral.tsx new file mode 100644 index 00000000..2ca5b3e2 --- /dev/null +++ b/packages/lending/src/actions/withdrawObligationCollateral.tsx @@ -0,0 +1,94 @@ +import { + findOrCreateAccountByMint, + LENDING_PROGRAM_ID, + notify, + ParsedAccount, + sendTransaction, + TokenAccount, +} from '@oyster/common'; +import { AccountLayout } from '@solana/spl-token'; +import { + Account, + Connection, + PublicKey, + TransactionInstruction, +} from '@solana/web3.js'; +import { + Obligation, + Reserve, + withdrawObligationCollateralInstruction, +} from '@solana/spl-token-lending'; +import { refreshObligationAndReserves } from './refreshObligationAndReserves'; + +export const withdrawObligationCollateral = async ( + connection: Connection, + wallet: any, + collateralAmount: number, + source: TokenAccount, + reserve: Reserve, + reserveAddress: PublicKey, + obligation: ParsedAccount, +) => { + notify({ + message: 'Withdrawing collateral...', + description: 'Please review transactions to approve.', + type: 'warn', + }); + + // user from account + const signers: Account[] = []; + const instructions: TransactionInstruction[] = []; + const cleanupInstructions: TransactionInstruction[] = []; + + const accountRentExempt = await connection.getMinimumBalanceForRentExemption( + AccountLayout.span, + ); + + const [lendingMarketAuthority] = await PublicKey.findProgramAddress( + [reserve.lendingMarket.toBuffer()], + LENDING_PROGRAM_ID, + ); + + // get destination account + const destinationCollateral = await findOrCreateAccountByMint( + wallet.publicKey, + wallet.publicKey, + instructions, + cleanupInstructions, + accountRentExempt, + reserve.collateral.mintPubkey, + signers, + ); + + instructions.push( + ...(await refreshObligationAndReserves(connection, obligation)), + withdrawObligationCollateralInstruction( + collateralAmount, + reserve.collateral.supplyPubkey, + destinationCollateral, + reserveAddress, + obligation.pubkey, + reserve.lendingMarket, + lendingMarketAuthority, + wallet.publicKey, + ), + ); + + try { + let { txid } = await sendTransaction( + connection, + wallet, + instructions.concat(cleanupInstructions), + signers, + true, + ); + + notify({ + message: 'Collateral withdrawn.', + type: 'success', + description: `Transaction - ${txid}`, + }); + } catch { + // TODO: + } +}; diff --git a/packages/lending/src/components/BarChartStatistic/index.tsx b/packages/lending/src/components/BarChartStatistic/index.tsx index fd7ab6b0..ad0bed61 100644 --- a/packages/lending/src/components/BarChartStatistic/index.tsx +++ b/packages/lending/src/components/BarChartStatistic/index.tsx @@ -1,5 +1,5 @@ -import { Statistic } from "antd"; -import React from "react"; +import { Statistic } from 'antd'; +import React from 'react'; export const BarChartStatistic = (props: { items: T[]; @@ -9,14 +9,14 @@ export const BarChartStatistic = (props: { getPct: (item: T) => number; }) => { const colors = [ - "#003f5c", - "#2f4b7c", - "#665191", - "#a05195", - "#d45087", - "#f95d6a", - "#ff7c43", - "#ffa600", + '#003f5c', + '#2f4b7c', + '#665191', + '#a05195', + '#d45087', + '#f95d6a', + '#ff7c43', + '#ffa600', ].reverse(); return ( @@ -25,12 +25,12 @@ export const BarChartStatistic = (props: { valueRender={() => (
{props.items.map((item, i) => ( @@ -38,7 +38,7 @@ export const BarChartStatistic = (props: { key={props.name(item)} title={props.name(item)} style={{ - overflow: "hidden", + overflow: 'hidden', width: `${100 * props.getPct(item)}%`, backgroundColor: (props.color && props.color(item)) || diff --git a/packages/lending/src/components/BorrowInput/index.tsx b/packages/lending/src/components/BorrowInput/index.tsx deleted file mode 100644 index c1202833..00000000 --- a/packages/lending/src/components/BorrowInput/index.tsx +++ /dev/null @@ -1,271 +0,0 @@ -import React, { useCallback, useEffect, useMemo, useState } from 'react'; -import { - useSliderInput, - useUserBalance, - useUserDeposits, - useUserObligationByReserve, -} from '../../hooks'; -import { - BorrowAmountType, - LendingReserve, - LendingReserveParser, -} from '../../models'; -import { Card } from 'antd'; -import { - contexts, - ParsedAccount, - ConnectButton, - BackButton, - ActionConfirmation, -} from '@oyster/common'; - -import { borrow } from '../../actions'; -import './style.less'; -import { LABELS } from '../../constants'; -import CollateralInput from '../CollateralInput'; -import { useMidPriceInUSD } from '../../contexts/market'; -import { RiskSlider } from '../RiskSlider'; -const { useWallet } = contexts.Wallet; -const { useConnection } = contexts.Connection; -const { cache } = contexts.Accounts; - -export const BorrowInput = (props: { - className?: string; - reserve: ParsedAccount; -}) => { - const connection = useConnection(); - const { wallet } = useWallet(); - const [collateralValue, setCollateralValue] = useState(''); - const [lastTyped, setLastTyped] = useState('collateral'); - const [pendingTx, setPendingTx] = useState(false); - const [showConfirmation, setShowConfirmation] = useState(false); - - const borrowReserve = props.reserve; - - const [collateralReserveKey, setCollateralReserveKey] = useState(); - - const collateralReserve = useMemo(() => { - const id: string = - cache - .byParser(LendingReserveParser) - .find(acc => acc === collateralReserveKey) || ''; - - return cache.get(id) as ParsedAccount; - }, [collateralReserveKey]); - const borrowPrice = useMidPriceInUSD( - borrowReserve.info.liquidityMint.toBase58(), - ).price; - const collateralPrice = useMidPriceInUSD( - collateralReserve?.info.liquidityMint.toBase58(), - )?.price; - - const include = useMemo( - () => new Set([collateralReserve?.pubkey.toBase58()]), - [collateralReserve], - ); - - const exclude = useMemo(() => new Set([]), []); - - const { userDeposits: accountBalance } = useUserDeposits(exclude, include); - const tokenBalance = accountBalance[0]?.info.amount || 0; - - const convert = useCallback( - (val: string | number) => { - const minAmount = Math.min(tokenBalance, Infinity); - if (typeof val === 'string') { - return (parseFloat(val) / minAmount) * 100; - } else { - return (val * minAmount) / 100; - } - }, - [tokenBalance], - ); - - const { value, setValue, pct } = useSliderInput(convert); - - useEffect(() => { - if (collateralReserve && lastTyped === 'collateral') { - const ltv = borrowReserve.info.config.loanToValueRatio / 100; - - if (collateralValue) { - const nCollateralValue = parseFloat(collateralValue); - const borrowInUSD = nCollateralValue * collateralPrice * ltv; - const borrowAmount = borrowInUSD / borrowPrice; - setValue(borrowAmount.toString()); - } else { - setValue(''); - } - } - }, [ - lastTyped, - collateralReserve, - collateralPrice, - borrowPrice, - borrowReserve, - collateralValue, - setValue, - ]); - - useEffect(() => { - if (collateralReserve && lastTyped === 'borrow') { - const ltv = borrowReserve.info.config.loanToValueRatio / 100; - - if (value) { - const nValue = parseFloat(value); - const borrowInUSD = nValue * borrowPrice; - const collateralAmount = borrowInUSD / ltv / collateralPrice; - setCollateralValue(collateralAmount.toString()); - } else { - setCollateralValue(''); - } - } - }, [ - lastTyped, - collateralReserve, - collateralPrice, - borrowPrice, - borrowReserve, - value, - ]); - - const { userObligationsByReserve } = useUserObligationByReserve( - borrowReserve?.pubkey, - collateralReserve?.pubkey, - ); - const { accounts: fromAccounts } = useUserBalance( - collateralReserve?.info.collateralMint, - ); - const onBorrow = useCallback(() => { - if (!collateralReserve) { - return; - } - - setPendingTx(true); - - (async () => { - try { - await borrow( - connection, - wallet, - - fromAccounts[0], - parseFloat(value), - // TODO: switch to collateral when user is using slider - BorrowAmountType.LiquidityBorrowAmount, - borrowReserve, - collateralReserve, - - // TODO: select exsisting obligations by collateral reserve - userObligationsByReserve.length > 0 - ? userObligationsByReserve[0].obligation.account - : undefined, - - userObligationsByReserve.length > 0 - ? userObligationsByReserve[0].userAccounts[0].pubkey - : undefined, - ); - - setValue(''); - setCollateralValue(''); - setShowConfirmation(true); - } catch { - // TODO: - } finally { - setPendingTx(false); - } - })(); - }, [ - connection, - wallet, - value, - setValue, - collateralReserve, - borrowReserve, - fromAccounts, - userObligationsByReserve, - setPendingTx, - setShowConfirmation, - ]); - - const bodyStyle: React.CSSProperties = { - display: 'flex', - flex: 1, - justifyContent: 'center', - alignItems: 'center', - height: '100%', - }; - - return ( - - {showConfirmation ? ( - setShowConfirmation(false)} /> - ) : ( -
-
{LABELS.BORROW_QUESTION}
-
- { - setCollateralValue(val?.toString() || ''); - setLastTyped('collateral'); - }} - onCollateralReserve={key => { - setCollateralReserveKey(key); - }} - useFirstReserve={true} - /> -
- -
- { - setValue(val?.toString() || ''); - setLastTyped('borrow'); - }} - disabled={true} - hideBalance={true} - /> -
- - {fromAccounts.length === 0 - ? LABELS.NO_COLLATERAL - : LABELS.BORROW_ACTION} - - -
- )} -
- ); -}; diff --git a/packages/lending/src/components/BorrowInput/style.less b/packages/lending/src/components/BorrowInput/style.less deleted file mode 100644 index 43b1f23e..00000000 --- a/packages/lending/src/components/BorrowInput/style.less +++ /dev/null @@ -1,8 +0,0 @@ -@import "~antd/dist/antd.dark.less"; - -.borrow-input-title { - font-size: 1.05rem; - margin-top: 15px; - margin-bottom: 15px; - color: @text-color-secondary; -} \ No newline at end of file diff --git a/packages/lending/src/components/CollateralInput/index.tsx b/packages/lending/src/components/CollateralInput/index.tsx index aaefe70a..d3e14472 100644 --- a/packages/lending/src/components/CollateralInput/index.tsx +++ b/packages/lending/src/components/CollateralInput/index.tsx @@ -1,18 +1,22 @@ +import { + contexts, + getTokenName, + NumericInput, + ParsedAccount, + TokenDisplay, + TokenIcon, +} from '@oyster/common'; +import { LendingMarket, Reserve } from '@solana/spl-token-lending'; +import { Card, Select } from 'antd'; import React, { useEffect, useState } from 'react'; -import { contexts, utils, ParsedAccount, NumericInput, TokenIcon, TokenDisplay } from '@oyster/common'; import { - useLendingReserves, + useReserves, useUserBalance, useUserDeposits, } from '../../hooks'; -import { - LendingReserve, - LendingMarket, - LendingReserveParser, -} from '../../models'; -import { Card, Select } from 'antd'; +import { ReserveParser } from '../../models'; import './style.less'; -const { getTokenName } = utils; + const { cache } = contexts.Accounts; const { useConnectionConfig } = contexts.Connection; @@ -23,7 +27,7 @@ const { Option } = Select; export default function CollateralInput(props: { title: string; amount?: number | null; - reserve: LendingReserve; + reserve: Reserve; disabled?: boolean; onCollateralReserve?: (id: string) => void; onLeverage?: (leverage: number) => void; @@ -34,10 +38,12 @@ export default function CollateralInput(props: { showLeverageSelector?: boolean; leverage?: number; }) { - const { balance: tokenBalance } = useUserBalance(props.reserve.liquidityMint); - const { reserveAccounts } = useLendingReserves(); + const { balance: tokenBalance } = useUserBalance( + props.reserve.liquidity.mintPubkey, + ); + const { reserveAccounts } = useReserves(); const { tokenMap } = useConnectionConfig(); - const [collateralReserve, setCollateralReserve] = useState(); + const [depositReserve, setCollateralReserve] = useState(); const [balance, setBalance] = useState(0); const [lastAmount, setLastAmount] = useState(''); const userDeposits = useUserDeposits(); @@ -47,42 +53,31 @@ export default function CollateralInput(props: { setBalance(tokenBalance); } else { const id: string = - cache - .byParser(LendingReserveParser) - .find(acc => acc === collateralReserve) || ''; - const parser = cache.get(id) as ParsedAccount; + cache.byParser(ReserveParser).find(acc => acc === depositReserve) || ''; + const parser = cache.get(id) as ParsedAccount; if (parser) { const collateralDeposit = userDeposits.userDeposits.find( u => - u.reserve.info.liquidityMint.toBase58() === - parser.info.liquidityMint.toBase58(), + u.reserve.info.liquidity.mintPubkey.toBase58() === + parser.info.liquidity.mintPubkey.toBase58(), ); if (collateralDeposit) setBalance(collateralDeposit.info.amount); else setBalance(0); } } - }, [collateralReserve, userDeposits, tokenBalance, props.useWalletBalance]); + }, [depositReserve, userDeposits, tokenBalance, props.useWalletBalance]); const market = cache.get( props.reserve.lendingMarket, ) as ParsedAccount; if (!market) return null; - const onlyQuoteAllowed = !props.reserve?.liquidityMint?.equals( - market?.info?.quoteMint, - ); - const filteredReserveAccounts = reserveAccounts - .filter(reserve => reserve.info !== props.reserve) - .filter( - reserve => - !onlyQuoteAllowed || - reserve.info.liquidityMint.equals(market.info.quoteMint), - ); + .filter(reserve => reserve.info !== props.reserve); if ( - !collateralReserve && + !depositReserve && props.useFirstReserve && filteredReserveAccounts.length ) { @@ -90,7 +85,7 @@ export default function CollateralInput(props: { setCollateralReserve(address); } const renderReserveAccounts = filteredReserveAccounts.map(reserve => { - const mint = reserve.info.liquidityMint.toBase58(); + const mint = reserve.info.liquidity.mintPubkey.toBase58(); const address = reserve.pubkey.toBase58(); const name = getTokenName(tokenMap, mint); return ( @@ -187,7 +182,7 @@ export default function CollateralInput(props: { showSearch style={{ minWidth: 150 }} placeholder="CCY" - value={collateralReserve} + value={depositReserve} onChange={item => { if (props.onCollateralReserve) props.onCollateralReserve(item); setCollateralReserve(item); @@ -200,12 +195,12 @@ export default function CollateralInput(props: { ) : ( )} diff --git a/packages/lending/src/components/CollateralSelector/index.tsx b/packages/lending/src/components/CollateralSelector/index.tsx index 12842ce8..9b7bbc07 100644 --- a/packages/lending/src/components/CollateralSelector/index.tsx +++ b/packages/lending/src/components/CollateralSelector/index.tsx @@ -1,10 +1,15 @@ -import React from 'react'; -import { useLendingReserves, UserDeposit, useUserDeposits } from '../../hooks'; -import { LendingMarket, LendingReserve } from '../../models'; +import { + contexts, + formatAmount, + getTokenName, + ParsedAccount, + TokenIcon, +} from '@oyster/common'; +import { LendingMarket, Reserve } from '@solana/spl-token-lending'; import { Select } from 'antd'; -import { contexts, utils, ParsedAccount, TokenIcon } from '@oyster/common'; +import React from 'react'; +import { useReserves, UserDeposit, useUserDeposits } from '../../hooks'; -const { getTokenName, formatAmount } = utils; const { cache } = contexts.Accounts; const { useConnectionConfig } = contexts.Connection; @@ -12,7 +17,7 @@ const { Option } = Select; export const CollateralItem = (props: { mint: string; - reserve: ParsedAccount; + reserve: ParsedAccount; userDeposit?: UserDeposit; name: string; }) => { @@ -32,12 +37,12 @@ export const CollateralItem = (props: { }; export const CollateralSelector = (props: { - reserve: LendingReserve; - collateralReserve?: string; + reserve: Reserve; + depositReserve?: string; disabled?: boolean; onCollateralReserve?: (id: string) => void; }) => { - const { reserveAccounts } = useLendingReserves(); + const { reserveAccounts } = useReserves(); const { tokenMap } = useConnectionConfig(); const { userDeposits } = useUserDeposits(); @@ -46,20 +51,15 @@ export const CollateralSelector = (props: { ) as ParsedAccount; if (!market) return null; - const quoteMintAddress = market?.info?.quoteMint?.toBase58(); - - const onlyQuoteAllowed = - props.reserve?.liquidityMint?.toBase58() !== quoteMintAddress; - return (