From 42af776ee842ce780b4e82d4d4ff67453330b608 Mon Sep 17 00:00:00 2001 From: bartosz-lipinski <264380+bartosz-lipinski@users.noreply.github.com> Date: Fri, 20 Nov 2020 14:14:06 -0600 Subject: [PATCH] feat: borrow instruction --- src/actions/account.ts | 25 ++++++ src/actions/borrow.tsx | 122 ++++++++++++++++---------- src/components/BorrowInput/index.tsx | 18 +--- src/components/DepositInput/index.tsx | 1 + src/contexts/accounts.tsx | 24 +++-- src/contexts/connection.tsx | 8 +- src/contexts/lending.tsx | 24 +++-- src/contexts/market.tsx | 37 ++------ src/hooks/useUserAccounts.ts | 1 - src/hooks/useUserBalance.ts | 4 +- src/models/dex/index.ts | 1 + src/models/dex/market.ts | 44 ++++++++++ src/models/lending/borrow.ts | 4 + src/models/lending/reserve.ts | 16 +++- 14 files changed, 211 insertions(+), 118 deletions(-) create mode 100644 src/models/dex/index.ts create mode 100644 src/models/dex/market.ts diff --git a/src/actions/account.ts b/src/actions/account.ts index 302873e..41c5c6a 100644 --- a/src/actions/account.ts +++ b/src/actions/account.ts @@ -44,6 +44,31 @@ export function ensureSplAccount( return account; } +export const DEFAULT_TEMP_MEM_SPACE = 65528; + +export function createTempMemoryAccount( + instructions: TransactionInstruction[], + payer: PublicKey, + signers: Account[], + space = DEFAULT_TEMP_MEM_SPACE) { + const account = new Account(); + instructions.push( + SystemProgram.createAccount({ + fromPubkey: payer, + newAccountPubkey: account.publicKey, + // 0 will evict/clost account since it cannot pay rent + lamports: 0, + space: space, + programId: TOKEN_PROGRAM_ID, + }) + ); + + signers.push(account); + + return account.publicKey; +} + + export function createUninitializedAccount( instructions: TransactionInstruction[], payer: PublicKey, diff --git a/src/actions/borrow.tsx b/src/actions/borrow.tsx index bb49277..2809d98 100644 --- a/src/actions/borrow.tsx +++ b/src/actions/borrow.tsx @@ -9,10 +9,11 @@ import { notify } from "../utils/notifications"; 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 { createTempMemoryAccount, createUninitializedAccount, ensureSplAccount, findOrCreateAccountByMint } from "./account"; import { cache, MintParser, ParsedAccount } from "../contexts/accounts"; import { TokenAccount, LendingObligationLayout, borrowInstruction, LendingMarket } from "../models"; import { toLamports } from "../utils/utils"; +import { DexMarketParser } from "../models/dex"; export const borrow = async ( from: TokenAccount, @@ -33,14 +34,68 @@ export const borrow = async ( type: "warn", }); - const signers: Account[] = []; - const instructions: TransactionInstruction[] = []; - const cleanupInstructions: TransactionInstruction[] = []; + let signers: Account[] = []; + let instructions: TransactionInstruction[] = []; + let cleanupInstructions: TransactionInstruction[] = []; const accountRentExempt = await connection.getMinimumBalanceForRentExemption( AccountLayout.span ); + 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, + ); + + let toAccount = await findOrCreateAccountByMint( + wallet.publicKey, + wallet.publicKey, + instructions, + cleanupInstructions, + accountRentExempt, + borrowReserve.liquidityMint, + signers + ); + + // create all accounts in one transaction + let tx = await sendTransaction(connection, wallet, instructions, [...signers]); + + notify({ + message: "Obligation accounts created", + description: `Transaction ${tx}`, + type: "success", + }); + + notify({ + message: "Adding Liquidity...", + description: "Please review transactions to approve.", + type: "warn", + }); + + signers = []; + instructions = []; + cleanupInstructions = []; + const [authority] = await PublicKey.findProgramAddress( [depositReserve.lendingMarket.toBuffer()], // which account should be authority LENDING_PROGRAM_ID @@ -70,56 +125,25 @@ export const borrow = async ( ) ); - 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 dexMarketAddress = borrowReserve.dexMarketOption ? borrowReserve.dexMarket : depositReserve.dexMarket; const dexMarket = cache.get(dexMarketAddress); + debugger; + if(!dexMarket) { throw new Error(`Dex market doesn't exsists.`) } - const dexOrderBookSide = market.info.quoteMint.equals(borrowReserve.liquidityMint) ? - dexMarket.info.bids : - dexMarket.info.asks; + const dexOrderBookSide = market.info.quoteMint.equals(depositReserve.liquidityMint) ? + dexMarket?.info.bids : + dexMarket?.info.asks + + const memory = createTempMemoryAccount( + instructions, + wallet.publicKey, + signers, + ); // deposit instructions.push( @@ -128,7 +152,7 @@ export const borrow = async ( fromAccount, toAccount, depositReserveAddress, - depositReserve.liquiditySupply, + depositReserve.collateralSupply, borrowReserveAddress, borrowReserve.liquiditySupply, @@ -141,6 +165,8 @@ export const borrow = async ( dexMarketAddress, dexOrderBookSide, + + memory, ) ); try { diff --git a/src/components/BorrowInput/index.tsx b/src/components/BorrowInput/index.tsx index bb7da04..0f4c376 100644 --- a/src/components/BorrowInput/index.tsx +++ b/src/components/BorrowInput/index.tsx @@ -1,18 +1,17 @@ -import React, { useCallback, useEffect, useMemo, useState } from "react"; -import { useCollateralBalance, useLendingReserve, useLendingReserves, useTokenName, useUserAccounts, useUserBalance } from '../../hooks'; +import React, { useCallback, useMemo, useState } from "react"; +import { useCollateralBalance, useLendingReserves, useTokenName, useUserBalance } from '../../hooks'; import { LendingReserve, LendingReserveParser } from "../../models"; import { TokenIcon } from "../TokenIcon"; -import { formatNumber, getTokenName } from "../../utils/utils"; +import { getTokenName } from "../../utils/utils"; import { Button, Card, Select } from "antd"; import { useParams } from "react-router-dom"; -import { cache, ParsedAccount, useAccount } from "../../contexts/accounts"; +import { cache, ParsedAccount } from "../../contexts/accounts"; import { NumericInput } from "../Input/numeric"; import { useConnection, useConnectionConfig } from "../../contexts/connection"; import { useWallet } from "../../contexts/wallet"; import { borrow } from '../../actions'; import { PublicKey } from "@solana/web3.js"; import './style.less'; -import { Token } from "@solana/spl-token"; const { Option } = Select; @@ -55,7 +54,6 @@ const CollateralSelector = (props: { export const BorrowInput = (props: { className?: string, reserve: LendingReserve, address: PublicKey }) => { const connection = useConnection(); const { wallet } = useWallet(); - const { id } = useParams<{ id: string }>(); const [value, setValue] = useState(''); const borrowReserve = props.reserve; @@ -71,26 +69,18 @@ export const BorrowInput = (props: { className?: string, reserve: LendingReserve return cache.get(id) as ParsedAccount; }, [collateralReserveMint]) - const collateral = useCollateralBalance(collateralReserve?.info); const name = useTokenName(borrowReserve?.liquidityMint); const { - balance: tokenBalance, accounts: fromAccounts } = useUserBalance(collateralReserve?.info.collateralMint); // const collateralBalance = useUserBalance(reserve?.collateralMint); - if(collateral) { - debugger; - } - const onBorrow = useCallback(() => { if (!collateralReserve) { return; } - debugger; - borrow( fromAccounts[0], parseFloat(value), diff --git a/src/components/DepositInput/index.tsx b/src/components/DepositInput/index.tsx index dd663fd..9d410fa 100644 --- a/src/components/DepositInput/index.tsx +++ b/src/components/DepositInput/index.tsx @@ -42,6 +42,7 @@ export const DepositInput = (props: { className?: string, reserve: LendingReserv console.log(`liquidityMint: ${reserve.liquidityMint.toBase58()}`); console.log(`collateralSupply: ${reserve.collateralSupply.toBase58()}`); console.log(`collateralMint: ${reserve.collateralMint.toBase58()}`); + console.log(`dexMarket: ${reserve.dexMarket.toBase58()}`); })(); }, [reserve]) diff --git a/src/contexts/accounts.tsx b/src/contexts/accounts.tsx index 828583f..f897023 100644 --- a/src/contexts/accounts.tsx +++ b/src/contexts/accounts.tsx @@ -24,7 +24,7 @@ export interface ParsedAccountBase { export type AccountParser = ( pubkey: PublicKey, data: AccountInfo -) => ParsedAccountBase; +) => ParsedAccountBase | undefined; export interface ParsedAccount extends ParsedAccountBase { info: T; @@ -47,7 +47,7 @@ export const MintParser = (pubKey: PublicKey, info: AccountInfo) => { }; export const TokenAccountParser = (pubKey: PublicKey, info: AccountInfo) => { - const buffer = Buffer.from(info.data); + const buffer = Buffer.from(info.data); const data = deserializeAccount(buffer); const details = { @@ -130,6 +130,10 @@ export const cache = { cache.registerParser(id, deserialize); pendingCalls.delete(address); const account = deserialize(new PublicKey(address), obj); + if (!account) { + return; + } + genericCache.set(address, account); cache.emitter.raiseCacheUpdated(address, deserialize); return account; @@ -146,8 +150,8 @@ export const cache = { }, byParser: (parser: AccountParser) => { const result: string[] = []; - for(const id of keyToAccountParser.keys()) { - if(keyToAccountParser.get(id) === parser) { + for (const id of keyToAccountParser.keys()) { + if (keyToAccountParser.get(id) === parser) { result.push(id); } } @@ -155,8 +159,12 @@ export const cache = { return result; }, registerParser: (pubkey: PublicKey | string, parser: AccountParser) => { - const address = typeof pubkey === 'string' ? pubkey : pubkey?.toBase58(); - keyToAccountParser.set(address, parser); + if (pubkey) { + const address = typeof pubkey === 'string' ? pubkey : pubkey?.toBase58(); + keyToAccountParser.set(address, parser); + } + + return pubkey; }, }; @@ -200,7 +208,7 @@ const UseNativeAccount = () => { const updateCache = useCallback((account) => { const wrapped = wrapNativeAccount(wallet.publicKey, account); - if(wrapped !== undefined && wallet) { + if (wrapped !== undefined && wallet) { cache.registerParser(wallet.publicKey.toBase58(), TokenAccountParser); genericCache.set(wallet.publicKey.toBase58(), wrapped as TokenAccount); } @@ -352,7 +360,7 @@ export const getMultipleAccounts = async ( .map( (a) => a.array.map((acc) => { - if(!acc) { + if (!acc) { return; } diff --git a/src/contexts/connection.tsx b/src/contexts/connection.tsx index f858f82..88e46b0 100644 --- a/src/contexts/connection.tsx +++ b/src/contexts/connection.tsx @@ -12,16 +12,20 @@ import { notify } from "./../utils/notifications"; import { ExplorerLink } from "../components/ExplorerLink"; import LocalTokens from '../config/tokens.json'; -export type ENV = "mainnet-beta" | "testnet" | "devnet" | "localnet"; +export type ENV = "mainnet-beta" | "testnet" | "devnet" | "localnet" | "lending"; export const ENDPOINTS = [ + { + name: 'lending' as ENV, + endpoint: "https://tln.solana.com", + }, { name: "mainnet-beta" as ENV, endpoint: "https://solana-api.projectserum.com/", }, { name: "testnet" as ENV, endpoint: clusterApiUrl("testnet") }, { name: "devnet" as ENV, endpoint: clusterApiUrl("devnet") }, - { name: "localnet" as ENV, endpoint: "http://35.206.228.142:8899" }, + { name: "localnet" as ENV, endpoint: "http://127.0.0.1:8899" }, ]; const DEFAULT = ENDPOINTS[0].endpoint; diff --git a/src/contexts/lending.tsx b/src/contexts/lending.tsx index e7ce90e..ca45574 100644 --- a/src/contexts/lending.tsx +++ b/src/contexts/lending.tsx @@ -2,8 +2,9 @@ import React, { useCallback, useEffect, useState } from "react"; import { useConnection } from "./connection"; import { LENDING_PROGRAM_ID } from "./../constants/ids"; import { LendingMarketParser, isLendingReserve, isLendingMarket, LendingReserveParser, LendingReserve } from "./../models/lending"; -import { cache, getMultipleAccounts } from "./accounts"; +import { cache, getMultipleAccounts, MintParser, ParsedAccount } from "./accounts"; import { PublicKey } from "@solana/web3.js"; +import { DexMarketParser } from "../models/dex"; export interface LendingContextState { @@ -51,20 +52,25 @@ export const useLending = () => { const toQuery = [ ...accounts.filter(acc => (acc?.info as LendingReserve).lendingMarket !== undefined) - .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[]; + .map(acc => acc as ParsedAccount) + .map(acc => { + const result = [ + cache.registerParser(acc?.info.collateralMint.toBase58(), MintParser), + cache.registerParser(acc?.info.liquidityMint.toBase58(), MintParser), + // ignore dex if its not set + cache.registerParser(acc?.info.dexMarketOption ? acc?.info.dexMarket.toBase58() : '', DexMarketParser), + ].filter(_ => _); + return result; + }) + ].flat() as string[]; // This will pre-cache all accounts used by pools // All those accounts are updated whenever there is a change await getMultipleAccounts(connection, toQuery, "single").then( ({ keys, array }) => { return array.map((obj, index) => { - // TODO: add to cache - + const address = keys[index]; + cache.add(address, obj); return obj; }) as any[]; } diff --git a/src/contexts/market.tsx b/src/contexts/market.tsx index 08397be..e4cb861 100644 --- a/src/contexts/market.tsx +++ b/src/contexts/market.tsx @@ -15,6 +15,8 @@ import { AccountInfo, Connection, PublicKey } from "@solana/web3.js"; import { useMemo } from "react"; import { EventEmitter } from "./../utils/eventEmitter"; +import { DexMarketParser } from "./../models/dex"; + export interface MarketsContextState { midPriceInUSD: (mint: string) => number; marketEmitter: EventEmitter; @@ -39,7 +41,9 @@ export function MarketProvider({ children = null as any }) { ]); // TODO: identify which markets to query ... - const mints = useMemo(() => [] as PublicKey[], []); + const mints = useMemo(() => [ + + ] as PublicKey[], []); const marketByMint = useMemo(() => { return [ @@ -107,24 +111,7 @@ export function MarketProvider({ children = null as any }) { if (market) { const programId = market.marketInfo.programId; const id = market.marketInfo.address; - cache.add(id, item, (id, acc) => { - const decoded = Market.getLayout(programId).decode(acc.data); - - const details = { - pubkey: id, - account: { - ...acc, - }, - info: decoded, - } as ParsedAccountBase; - - cache.registerParser(details.info.baseMint, MintParser); - cache.registerParser(details.info.quoteMint, MintParser); - cache.registerParser(details.info.bids, OrderBookParser); - cache.registerParser(details.info.asks, OrderBookParser); - - return details; - }); + cache.add(id, item, DexMarketParser); } } @@ -256,19 +243,7 @@ export const useMidPriceInUSD = (mint: string) => { return { price, isBase: price === 1.0 }; }; -const OrderBookParser = (id: PublicKey, acc: AccountInfo) => { - const decoded = Orderbook.LAYOUT.decode(acc.data); - const details = { - pubkey: id, - account: { - ...acc, - }, - info: decoded, - } as ParsedAccountBase; - - return details; -}; const getMidPrice = (marketAddress?: string, mintAddress?: string) => { const SERUM_TOKEN = TOKEN_MINTS.find( diff --git a/src/hooks/useUserAccounts.ts b/src/hooks/useUserAccounts.ts index f63dc6b..38fe752 100644 --- a/src/hooks/useUserAccounts.ts +++ b/src/hooks/useUserAccounts.ts @@ -1,4 +1,3 @@ -import { useContext } from "react"; import { TokenAccount } from "../models"; import { useAccountsContext } from './../contexts/accounts'; diff --git a/src/hooks/useUserBalance.ts b/src/hooks/useUserBalance.ts index af30b92..ed4d43f 100644 --- a/src/hooks/useUserBalance.ts +++ b/src/hooks/useUserBalance.ts @@ -11,12 +11,12 @@ export function useUserBalance(mint?: PublicKey) { return userAccounts .filter(acc => mint?.equals(acc.info.mint)) .sort((a, b) => b.info.amount.sub(a.info.amount).toNumber()); - }, [userAccounts]); + }, [userAccounts, mint]); const balanceLamports = useMemo(() => { return accounts .reduce((res, item) => res += item.info.amount.toNumber(), 0); - },[accounts, mintInfo]); + },[accounts]); return { balance: fromLamports(balanceLamports, mintInfo), diff --git a/src/models/dex/index.ts b/src/models/dex/index.ts new file mode 100644 index 0000000..fab1886 --- /dev/null +++ b/src/models/dex/index.ts @@ -0,0 +1 @@ +export * from "./market"; \ No newline at end of file diff --git a/src/models/dex/market.ts b/src/models/dex/market.ts new file mode 100644 index 0000000..bf9952e --- /dev/null +++ b/src/models/dex/market.ts @@ -0,0 +1,44 @@ +import { Market, MARKETS, Orderbook } from "@project-serum/serum"; +import { AccountInfo, PublicKey } from "@solana/web3.js"; +import { + MintParser, + ParsedAccountBase, + cache, +} from "./../../contexts/accounts"; + +export const OrderBookParser = (id: PublicKey, acc: AccountInfo) => { + const decoded = Orderbook.LAYOUT.decode(acc.data); + + const details = { + pubkey: id, + account: { + ...acc, + }, + info: decoded, + } as ParsedAccountBase; + + return details; +}; + +const DEFAULT_DEX_ID = new PublicKey('EUqojwWA2rd19FZrzeBncJsm38Jm1hEhE3zsmX3bRc2o'); + +export const DexMarketParser = (pubkey: PublicKey, acc: AccountInfo) => { + const market = MARKETS.find(m => m.address.equals(pubkey)); + const decoded = Market.getLayout(market?.programId || DEFAULT_DEX_ID) + .decode(acc.data); + + const details = { + pubkey, + account: { + ...acc, + }, + info: decoded, + } as ParsedAccountBase; + + cache.registerParser(details.info.baseMint, MintParser); + cache.registerParser(details.info.quoteMint, MintParser); + cache.registerParser(details.info.bids, OrderBookParser); + cache.registerParser(details.info.asks, OrderBookParser); + + return details; +} \ No newline at end of file diff --git a/src/models/lending/borrow.ts b/src/models/lending/borrow.ts index 51dfa68..3a8ea2f 100644 --- a/src/models/lending/borrow.ts +++ b/src/models/lending/borrow.ts @@ -48,12 +48,15 @@ export const borrowInstruction = ( dexMarket: PublicKey, dexOrderBookSide: PublicKey, + + memory: PublicKey, ): TransactionInstruction => { const dataLayout = BufferLayout.struct([ BufferLayout.u8("instruction"), Layout.uint64("collateralAmount"), ]); + debugger; const data = Buffer.alloc(dataLayout.span); dataLayout.encode( { @@ -77,6 +80,7 @@ export const borrowInstruction = ( { pubkey: lendingMarketAuthority, isSigner: false, isWritable: true }, { pubkey: dexMarket, isSigner: false, isWritable: true }, { pubkey: dexOrderBookSide, isSigner: false, isWritable: false }, + { pubkey: memory, isSigner: false, isWritable: true }, { pubkey: SYSVAR_CLOCK_PUBKEY, isSigner: false, isWritable: false }, { pubkey: SYSVAR_RENT_PUBKEY, isSigner: false, isWritable: false }, { pubkey: TOKEN_PROGRAM_ID, isSigner: false, isWritable: false }, diff --git a/src/models/lending/reserve.ts b/src/models/lending/reserve.ts index 12476d4..0843be6 100644 --- a/src/models/lending/reserve.ts +++ b/src/models/lending/reserve.ts @@ -16,6 +16,7 @@ export const LendingReserveLayout: typeof BufferLayout.Structure = BufferLayout. Layout.uint64("lastUpdateSlot"), Layout.publicKey("lendingMarket"), Layout.publicKey("liquidityMint"), + BufferLayout.u8("liquidityMintDecimals"), Layout.publicKey("liquiditySupply"), Layout.publicKey("collateralMint"), Layout.publicKey("collateralSupply"), @@ -23,9 +24,16 @@ export const LendingReserveLayout: typeof BufferLayout.Structure = BufferLayout. BufferLayout.u32('dexMarketOption'), Layout.publicKey("dexMarket"), - BufferLayout.u8("maxUtilizationRate"), - - BufferLayout.u8("collateralFactor"), + BufferLayout.struct([ + /// Max utilization rate as a percent + BufferLayout.u8("maxUtilizationRate"), + /// The ratio of the loan to the value of the collateral as a percent + BufferLayout.u8("loanToValueRatio"), + /// The percent discount the liquidator gets when buying collateral for an unhealthy obligation + BufferLayout.u8("liquidationBonus"), + /// The percent at which an obligation is considered unhealthy + BufferLayout.u8("liquidationThreshold"), + ], "config"), Layout.uint128("cumulativeBorrowRate"), Layout.uint128("totalBorrows"), @@ -36,6 +44,8 @@ export const LendingReserveLayout: typeof BufferLayout.Structure = BufferLayout. ); export const isLendingReserve = (info: AccountInfo) => { + console.log(LendingReserveLayout.span); + console.log(info.data.length); return info.data.length === LendingReserveLayout.span; }