From d63da050ad18080626ee158e065ace309eb9c738 Mon Sep 17 00:00:00 2001 From: bartosz-lipinski <264380+bartosz-lipinski@users.noreply.github.com> Date: Thu, 19 Nov 2020 23:41:11 -0600 Subject: [PATCH] feat: add borrow instruction --- src/App.less | 2 +- src/actions/borrow.tsx | 121 +++++++++++++++++---------- src/components/BorrowInput/index.tsx | 38 ++++++--- src/contexts/lending.tsx | 1 + src/contexts/market.tsx | 5 -- src/models/index.ts | 1 + src/models/lending/borrow.ts | 89 ++++++++++++++++++++ src/models/lending/index.ts | 3 + src/models/lending/lending.ts | 8 ++ src/models/lending/obligation.ts | 45 +++++++++- src/models/lending/reserve.ts | 5 +- 11 files changed, 254 insertions(+), 64 deletions(-) create mode 100644 src/models/lending/borrow.ts create mode 100644 src/models/lending/lending.ts diff --git a/src/App.less b/src/App.less index f024fcd..f9e3c53 100644 --- a/src/App.less +++ b/src/App.less @@ -29,7 +29,7 @@ body { text-align: center; display: flex; align-self: center; - align-items: centerconn; + align-items: center; height: 100%; } diff --git a/src/actions/borrow.tsx b/src/actions/borrow.tsx index b0a4477..bb49277 100644 --- a/src/actions/borrow.tsx +++ b/src/actions/borrow.tsx @@ -1,33 +1,29 @@ import { Account, - AccountInfo, Connection, PublicKey, - sendAndConfirmRawTransaction, - SYSVAR_CLOCK_PUBKEY, TransactionInstruction, } from "@solana/web3.js"; -import BN from "bn.js"; -import * as BufferLayout from "buffer-layout"; import { sendTransaction } from "../contexts/connection"; import { notify } from "../utils/notifications"; -import * as Layout from "./../utils/layout"; -import { depositInstruction, initReserveInstruction, LendingReserve } from "./../models/lending/reserve"; -import { AccountLayout, MintInfo, Token } from "@solana/spl-token"; +import { LendingReserve } from "./../models/lending/reserve"; +import { AccountLayout, MintInfo, MintLayout, Token } from "@solana/spl-token"; import { LENDING_PROGRAM_ID, TOKEN_PROGRAM_ID } from "../constants/ids"; import { createUninitializedAccount, ensureSplAccount, findOrCreateAccountByMint } from "./account"; -import { cache, GenericAccountParser, MintParser, ParsedAccount } from "../contexts/accounts"; -import { TokenAccount } from "../models"; -import { isConstructorDeclaration } from "typescript"; -import { LendingMarketParser } from "../models/lending"; -import { sign } from "crypto"; -import { fromLamports, toLamports } from "../utils/utils"; +import { cache, MintParser, ParsedAccount } from "../contexts/accounts"; +import { TokenAccount, LendingObligationLayout, borrowInstruction, LendingMarket } from "../models"; +import { toLamports } from "../utils/utils"; export const borrow = async ( from: TokenAccount, amount: number, - reserve: LendingReserve, - reserveAddress: PublicKey, + + borrowReserve: LendingReserve, + borrowReserveAddress: PublicKey, + + depositReserve: LendingReserve, + depositReserveAddress: PublicKey, + connection: Connection, wallet: any) => { @@ -37,9 +33,6 @@ export const borrow = async ( type: "warn", }); - const isInitalized = true; // TODO: finish reserve init - - // user from account const signers: Account[] = []; const instructions: TransactionInstruction[] = []; const cleanupInstructions: TransactionInstruction[] = []; @@ -49,11 +42,11 @@ export const borrow = async ( ); const [authority] = await PublicKey.findProgramAddress( - [reserve.lendingMarket.toBuffer()], // which account should be authority + [depositReserve.lendingMarket.toBuffer()], // which account should be authority LENDING_PROGRAM_ID ); - const mint = (await cache.query(connection, reserve.liquidityMint, MintParser)) as ParsedAccount; + const mint = (await cache.query(connection, depositReserve.collateralMint, MintParser)) as ParsedAccount; const amountLamports = toLamports(amount, mint?.info); const fromAccount = ensureSplAccount( @@ -77,37 +70,77 @@ export const borrow = async ( ) ); - 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, - ); + let toAccount = await findOrCreateAccountByMint( + wallet.publicKey, + wallet.publicKey, + instructions, + cleanupInstructions, + accountRentExempt, + borrowReserve.liquidityMint, + signers + ); + + const obligation = createUninitializedAccount( + instructions, + wallet.publicKey, + await connection.getMinimumBalanceForRentExemption( + LendingObligationLayout.span + ), + signers, + ); + + const obligationMint = createUninitializedAccount( + instructions, + wallet.publicKey, + await connection.getMinimumBalanceForRentExemption( + MintLayout.span + ), + signers, + ); + + const obligationTokenOutput = createUninitializedAccount( + instructions, + wallet.publicKey, + accountRentExempt, + signers, + ); + + const market = cache.get(depositReserve.lendingMarket) as ParsedAccount; + + + const dexMarketAddress = market.info.quoteMint.equals(borrowReserve.liquidityMint) ? + borrowReserve.dexMarket : + depositReserve.dexMarket; + + const dexMarket = cache.get(dexMarketAddress); + if(!dexMarket) { + throw new Error(`Dex market doesn't exsists.`) } + const dexOrderBookSide = market.info.quoteMint.equals(borrowReserve.liquidityMint) ? + dexMarket.info.bids : + dexMarket.info.asks; + // deposit instructions.push( - depositInstruction( + borrowInstruction( amountLamports, fromAccount, toAccount, + depositReserveAddress, + depositReserve.liquiditySupply, + borrowReserveAddress, + borrowReserve.liquiditySupply, + + obligation, + obligationMint, + obligationTokenOutput, + wallet.publicKey, + authority, - reserveAddress, - reserve.liquiditySupply, - reserve.collateralMint, + + dexMarketAddress, + dexOrderBookSide, ) ); try { diff --git a/src/components/BorrowInput/index.tsx b/src/components/BorrowInput/index.tsx index c063191..0900de4 100644 --- a/src/components/BorrowInput/index.tsx +++ b/src/components/BorrowInput/index.tsx @@ -19,27 +19,43 @@ export const BorrowInput = (props: { className?: string, reserve: LendingReserve const { id } = useParams<{ id: string }>(); const [value, setValue] = useState(''); - const reserve = props.reserve; - const address = props.address; + const borrowReserve = props.reserve; + const borrowReserveAddress = props.address; - const name = useTokenName(reserve?.liquidityMint); - const { balance: tokenBalance, accounts: fromAccounts } = useUserBalance(reserve?.liquidityMint); + const [collateralReserve, setCollateralReserve] = useState(); + + const collateralReserveAddress = useMemo(() => { + return cache.byParser(LendingReserveParser) + .find(acc => cache.get(acc) === collateralReserve); + }, [collateralReserve]) + + const name = useTokenName(borrowReserve?.liquidityMint); + const { + balance: tokenBalance, + accounts: fromAccounts + } = useUserBalance(collateralReserve?.liquidityMint); // const collateralBalance = useUserBalance(reserve?.collateralMint); const onBorrow = useCallback(() => { + if(!collateralReserve || !collateralReserveAddress) { + return; + } + borrow( fromAccounts[0], parseFloat(value), - reserve, - address, + borrowReserve, + borrowReserveAddress, + collateralReserve, + new PublicKey(collateralReserveAddress), connection, wallet); - }, [value, reserve, fromAccounts, address]); + }, [value, borrowReserve, fromAccounts, borrowReserveAddress]); - const bodyStyle: React.CSSProperties = { - display: 'flex', + const bodyStyle: React.CSSProperties = { + display: 'flex', flex: 1, - justifyContent: 'center', + justifyContent: 'center', alignItems: 'center', height: '100%', }; @@ -51,7 +67,7 @@ export const BorrowInput = (props: { className?: string, reserve: LendingReserve How much would you like to borrow?
- + { setValue(val); diff --git a/src/contexts/lending.tsx b/src/contexts/lending.tsx index ed3054b..db892ca 100644 --- a/src/contexts/lending.tsx +++ b/src/contexts/lending.tsx @@ -55,6 +55,7 @@ export const useLending = () => { .map(acc => [ (acc?.info as LendingReserve).collateralMint.toBase58(), (acc?.info as LendingReserve).liquidityMint.toBase58(), + (acc?.info as LendingReserve).dexMarket.toBase58(), ]) ].flat().filter((p) => p) as string[]; diff --git a/src/contexts/market.tsx b/src/contexts/market.tsx index f43b620..08397be 100644 --- a/src/contexts/market.tsx +++ b/src/contexts/market.tsx @@ -15,11 +15,6 @@ import { AccountInfo, Connection, PublicKey } from "@solana/web3.js"; import { useMemo } from "react"; import { EventEmitter } from "./../utils/eventEmitter"; -interface RecentPoolData { - pool_identifier: string; - volume24hA: number; -} - export interface MarketsContextState { midPriceInUSD: (mint: string) => number; marketEmitter: EventEmitter; diff --git a/src/models/index.ts b/src/models/index.ts index ed4079f..e105516 100644 --- a/src/models/index.ts +++ b/src/models/index.ts @@ -1 +1,2 @@ export * from "./account"; +export * from './lending'; diff --git a/src/models/lending/borrow.ts b/src/models/lending/borrow.ts new file mode 100644 index 0000000..51dfa68 --- /dev/null +++ b/src/models/lending/borrow.ts @@ -0,0 +1,89 @@ +import { + PublicKey, + SYSVAR_CLOCK_PUBKEY, + SYSVAR_RENT_PUBKEY, + TransactionInstruction, +} from "@solana/web3.js"; +import BN from "bn.js"; +import * as BufferLayout from "buffer-layout"; +import { LENDING_PROGRAM_ID, TOKEN_PROGRAM_ID } from "../../constants/ids"; +import * as Layout from "./../../utils/layout"; +import { LendingInstruction } from './lending'; + +/// Borrow tokens from a reserve by depositing collateral tokens. The number of borrowed tokens +/// is calculated by market price. The debt obligation is tokenized. +/// +/// 0. `[writable]` Collateral input SPL Token account, $authority can transfer $collateral_amount +/// 1. `[writable]` Liquidity output SPL Token account +/// 2. `[writable]` Deposit reserve account. +/// 3. `[writable]` Deposit reserve collateral supply SPL Token account +/// 4. `[writable]` Borrow reserve account. +/// 5. `[writable]` Borrow reserve liquidity supply SPL Token account +/// 6. `[writable]` Obligation - uninitialized +/// 7. `[writable]` Obligation token mint - uninitialized +/// 8. `[writable]` Obligation token output - uninitialized +/// 9. `[]` Obligation token owner - uninitialized +/// 10 `[]` Derived lending market authority ($authority). +/// 11 `[]` Dex market +/// 12 `[]` Dex order book side // could be bid/ask +/// 13 `[]` Temporary memory +/// 14 `[]` Clock sysvar +/// 15 `[]` Rent sysvar +/// 16 '[]` Token program id +export const borrowInstruction = ( + collateralAmount: number | BN, + from: PublicKey, // Collateral input SPL Token account. $authority can transfer $collateralAmount + to: PublicKey, // Liquidity output SPL Token account, + depositReserve: PublicKey, + depositReserveCollateralSupply: PublicKey, + borrowReserve: PublicKey, + borrowReserveLiquiditySupply: PublicKey, + + obligation: PublicKey, + obligationMint: PublicKey, + obligationTokenOutput: PublicKey, + obligationTokenOwner: PublicKey, + + lendingMarketAuthority: PublicKey, + + dexMarket: PublicKey, + dexOrderBookSide: PublicKey, +): TransactionInstruction => { + const dataLayout = BufferLayout.struct([ + BufferLayout.u8("instruction"), + Layout.uint64("collateralAmount"), + ]); + + const data = Buffer.alloc(dataLayout.span); + dataLayout.encode( + { + instruction: LendingInstruction.BorrowReserveLiquidity, + collateralAmount: new BN(collateralAmount), + }, + data + ); + + const keys = [ + { pubkey: from, isSigner: false, isWritable: true }, + { pubkey: to, isSigner: false, isWritable: true }, + { pubkey: depositReserve, isSigner: false, isWritable: true }, + { pubkey: depositReserveCollateralSupply, isSigner: false, isWritable: true }, + { pubkey: borrowReserve, isSigner: false, isWritable: true }, + { pubkey: borrowReserveLiquiditySupply, isSigner: false, isWritable: false }, + { pubkey: obligation, isSigner: false, isWritable: true }, + { pubkey: obligationMint, isSigner: false, isWritable: true }, + { pubkey: obligationTokenOutput, isSigner: false, isWritable: true }, + { pubkey: obligationTokenOwner, isSigner: false, isWritable: false }, + { pubkey: lendingMarketAuthority, isSigner: false, isWritable: true }, + { pubkey: dexMarket, isSigner: false, isWritable: true }, + { pubkey: dexOrderBookSide, isSigner: false, isWritable: false }, + { pubkey: SYSVAR_CLOCK_PUBKEY, isSigner: false, isWritable: false }, + { pubkey: SYSVAR_RENT_PUBKEY, isSigner: false, isWritable: false }, + { pubkey: TOKEN_PROGRAM_ID, isSigner: false, isWritable: false }, + ]; + return new TransactionInstruction({ + keys, + programId: LENDING_PROGRAM_ID, + data, + }); +}; diff --git a/src/models/lending/index.ts b/src/models/lending/index.ts index 835966f..0dd4c6e 100644 --- a/src/models/lending/index.ts +++ b/src/models/lending/index.ts @@ -1,2 +1,5 @@ export * from './market'; export * from './reserve'; +export * from './obligation'; +export * from './lending'; +export * from './borrow'; diff --git a/src/models/lending/lending.ts b/src/models/lending/lending.ts new file mode 100644 index 0000000..6b281e1 --- /dev/null +++ b/src/models/lending/lending.ts @@ -0,0 +1,8 @@ +export enum LendingInstruction { + InitLendingMarket = 0, + InitReserve = 1, + DepositReserveLiquidity = 2, + WithdrawReserveLiquidity = 3, + BorrowReserveLiquidity = 4, + RepayReserveLiquidity = 5 +} \ No newline at end of file diff --git a/src/models/lending/obligation.ts b/src/models/lending/obligation.ts index ad1d380..19eef6b 100644 --- a/src/models/lending/obligation.ts +++ b/src/models/lending/obligation.ts @@ -1 +1,44 @@ -export const x = 1; +import { + AccountInfo, + Connection, + PublicKey, + sendAndConfirmRawTransaction, + SYSVAR_CLOCK_PUBKEY, + SYSVAR_RENT_PUBKEY, + TransactionInstruction, +} from "@solana/web3.js"; +import BN from "bn.js"; +import * as BufferLayout from "buffer-layout"; +import { LENDING_PROGRAM_ID, TOKEN_PROGRAM_ID } from "../../constants/ids"; +import { sendTransaction } from "../../contexts/connection"; +import * as Layout from "./../../utils/layout"; +import { LendingInstruction } from './lending'; + +export const LendingObligationLayout: typeof BufferLayout.Structure = BufferLayout.struct( + [ + /// Slot when obligation was updated. Used for calculating interest. + Layout.uint64("lastUpdateSlot"), + /// Amount of collateral tokens deposited for this obligation + Layout.uint64("collateralAmount"), + /// Reserve which collateral tokens were deposited into + Layout.publicKey("collateralSupply"), + /// Borrow rate used for calculating interest. + Layout.uint128("cumulativeBorrowRate"), + /// Amount of tokens borrowed for this obligation plus interest + Layout.uint128("borrowAmount"), + /// Reserve which tokens were borrowed from + Layout.publicKey("borrowReserve"), + /// Mint address of the tokens for this obligation + Layout.publicKey("tokenMint"), + ] +); + +export interface LendingObligation { + lastUpdateSlot: BN; + collateralAmount: BN; + collateralSupply: PublicKey; + cumulativeBorrowRate: BN; // decimals + borrowAmount: BN; // decimals + borrowReserve: PublicKey; + tokenMint: PublicKey; +} \ No newline at end of file diff --git a/src/models/lending/reserve.ts b/src/models/lending/reserve.ts index 2bf164b..1ff7ada 100644 --- a/src/models/lending/reserve.ts +++ b/src/models/lending/reserve.ts @@ -12,6 +12,7 @@ import * as BufferLayout from "buffer-layout"; import { LENDING_PROGRAM_ID, TOKEN_PROGRAM_ID } from "../../constants/ids"; import { sendTransaction } from "../../contexts/connection"; import * as Layout from "./../../utils/layout"; +import { LendingInstruction } from './lending'; export const LendingReserveLayout: typeof BufferLayout.Structure = BufferLayout.struct( [ @@ -108,7 +109,7 @@ export const initReserveInstruction = ( const data = Buffer.alloc(dataLayout.span); dataLayout.encode( { - instruction: 1, // Init reserve instruction + instruction: LendingInstruction.InitReserve, // Init reserve instruction liquidityAmount: new BN(liquidityAmount), maxUtilizationRate: maxUtilizationRate, }, @@ -169,7 +170,7 @@ export const depositInstruction = ( const data = Buffer.alloc(dataLayout.span); dataLayout.encode( { - instruction: 2, // Deposit instruction + instruction: LendingInstruction.DepositReserveLiquidity, // Deposit instruction liquidityAmount: new BN(liquidityAmount), }, data