feat: borrow instruction

This commit is contained in:
bartosz-lipinski 2020-11-20 14:14:06 -06:00
parent 3554e8ce86
commit 42af776ee8
14 changed files with 211 additions and 118 deletions

View File

@ -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,

View File

@ -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<LendingMarket>;
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 {

View File

@ -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<LendingReserve>;
}, [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),

View File

@ -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])

View File

@ -24,7 +24,7 @@ export interface ParsedAccountBase {
export type AccountParser = (
pubkey: PublicKey,
data: AccountInfo<Buffer>
) => ParsedAccountBase;
) => ParsedAccountBase | undefined;
export interface ParsedAccount<T> extends ParsedAccountBase {
info: T;
@ -47,7 +47,7 @@ export const MintParser = (pubKey: PublicKey, info: AccountInfo<Buffer>) => {
};
export const TokenAccountParser = (pubKey: PublicKey, info: AccountInfo<Buffer>) => {
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;
}

View File

@ -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;

View File

@ -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<LendingReserve>)
.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[];
}

View File

@ -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<Buffer>) => {
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(

View File

@ -1,4 +1,3 @@
import { useContext } from "react";
import { TokenAccount } from "../models";
import { useAccountsContext } from './../contexts/accounts';

View File

@ -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),

1
src/models/dex/index.ts Normal file
View File

@ -0,0 +1 @@
export * from "./market";

44
src/models/dex/market.ts Normal file
View File

@ -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<Buffer>) => {
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<Buffer>) => {
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;
}

View File

@ -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 },

View File

@ -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<Buffer>) => {
console.log(LendingReserveLayout.span);
console.log(info.data.length);
return info.data.length === LendingReserveLayout.span;
}