feat: add borrow instruction

This commit is contained in:
bartosz-lipinski 2020-11-19 23:41:11 -06:00
parent 5cd9478317
commit d63da050ad
11 changed files with 254 additions and 64 deletions

View File

@ -29,7 +29,7 @@ body {
text-align: center; text-align: center;
display: flex; display: flex;
align-self: center; align-self: center;
align-items: centerconn; align-items: center;
height: 100%; height: 100%;
} }

View File

@ -1,33 +1,29 @@
import { import {
Account, Account,
AccountInfo,
Connection, Connection,
PublicKey, PublicKey,
sendAndConfirmRawTransaction,
SYSVAR_CLOCK_PUBKEY,
TransactionInstruction, TransactionInstruction,
} from "@solana/web3.js"; } from "@solana/web3.js";
import BN from "bn.js";
import * as BufferLayout from "buffer-layout";
import { sendTransaction } from "../contexts/connection"; import { sendTransaction } from "../contexts/connection";
import { notify } from "../utils/notifications"; import { notify } from "../utils/notifications";
import * as Layout from "./../utils/layout"; import { LendingReserve } from "./../models/lending/reserve";
import { depositInstruction, initReserveInstruction, LendingReserve } from "./../models/lending/reserve"; import { AccountLayout, MintInfo, MintLayout, Token } from "@solana/spl-token";
import { AccountLayout, MintInfo, Token } from "@solana/spl-token";
import { LENDING_PROGRAM_ID, TOKEN_PROGRAM_ID } from "../constants/ids"; import { LENDING_PROGRAM_ID, TOKEN_PROGRAM_ID } from "../constants/ids";
import { createUninitializedAccount, ensureSplAccount, findOrCreateAccountByMint } from "./account"; import { createUninitializedAccount, ensureSplAccount, findOrCreateAccountByMint } from "./account";
import { cache, GenericAccountParser, MintParser, ParsedAccount } from "../contexts/accounts"; import { cache, MintParser, ParsedAccount } from "../contexts/accounts";
import { TokenAccount } from "../models"; import { TokenAccount, LendingObligationLayout, borrowInstruction, LendingMarket } from "../models";
import { isConstructorDeclaration } from "typescript"; import { toLamports } from "../utils/utils";
import { LendingMarketParser } from "../models/lending";
import { sign } from "crypto";
import { fromLamports, toLamports } from "../utils/utils";
export const borrow = async ( export const borrow = async (
from: TokenAccount, from: TokenAccount,
amount: number, amount: number,
reserve: LendingReserve,
reserveAddress: PublicKey, borrowReserve: LendingReserve,
borrowReserveAddress: PublicKey,
depositReserve: LendingReserve,
depositReserveAddress: PublicKey,
connection: Connection, connection: Connection,
wallet: any) => { wallet: any) => {
@ -37,9 +33,6 @@ export const borrow = async (
type: "warn", type: "warn",
}); });
const isInitalized = true; // TODO: finish reserve init
// user from account
const signers: Account[] = []; const signers: Account[] = [];
const instructions: TransactionInstruction[] = []; const instructions: TransactionInstruction[] = [];
const cleanupInstructions: TransactionInstruction[] = []; const cleanupInstructions: TransactionInstruction[] = [];
@ -49,11 +42,11 @@ export const borrow = async (
); );
const [authority] = await PublicKey.findProgramAddress( const [authority] = await PublicKey.findProgramAddress(
[reserve.lendingMarket.toBuffer()], // which account should be authority [depositReserve.lendingMarket.toBuffer()], // which account should be authority
LENDING_PROGRAM_ID LENDING_PROGRAM_ID
); );
const mint = (await cache.query(connection, reserve.liquidityMint, MintParser)) as ParsedAccount<MintInfo>; const mint = (await cache.query(connection, depositReserve.collateralMint, MintParser)) as ParsedAccount<MintInfo>;
const amountLamports = toLamports(amount, mint?.info); const amountLamports = toLamports(amount, mint?.info);
const fromAccount = ensureSplAccount( const fromAccount = ensureSplAccount(
@ -77,37 +70,77 @@ export const borrow = async (
) )
); );
let toAccount: PublicKey; let toAccount = await findOrCreateAccountByMint(
if (isInitalized) {
// get destination account
toAccount = await findOrCreateAccountByMint(
wallet.publicKey, wallet.publicKey,
wallet.publicKey, wallet.publicKey,
instructions, instructions,
cleanupInstructions, cleanupInstructions,
accountRentExempt, accountRentExempt,
reserve.collateralMint, borrowReserve.liquidityMint,
signers signers
); );
} else {
toAccount = createUninitializedAccount( 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, instructions,
wallet.publicKey, wallet.publicKey,
accountRentExempt, accountRentExempt,
signers, signers,
); );
const market = cache.get(depositReserve.lendingMarket) as ParsedAccount<LendingMarket>;
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 // deposit
instructions.push( instructions.push(
depositInstruction( borrowInstruction(
amountLamports, amountLamports,
fromAccount, fromAccount,
toAccount, toAccount,
depositReserveAddress,
depositReserve.liquiditySupply,
borrowReserveAddress,
borrowReserve.liquiditySupply,
obligation,
obligationMint,
obligationTokenOutput,
wallet.publicKey,
authority, authority,
reserveAddress,
reserve.liquiditySupply, dexMarketAddress,
reserve.collateralMint, dexOrderBookSide,
) )
); );
try { try {

View File

@ -19,22 +19,38 @@ export const BorrowInput = (props: { className?: string, reserve: LendingReserve
const { id } = useParams<{ id: string }>(); const { id } = useParams<{ id: string }>();
const [value, setValue] = useState(''); const [value, setValue] = useState('');
const reserve = props.reserve; const borrowReserve = props.reserve;
const address = props.address; const borrowReserveAddress = props.address;
const name = useTokenName(reserve?.liquidityMint); const [collateralReserve, setCollateralReserve] = useState<LendingReserve>();
const { balance: tokenBalance, accounts: fromAccounts } = useUserBalance(reserve?.liquidityMint);
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 collateralBalance = useUserBalance(reserve?.collateralMint);
const onBorrow = useCallback(() => { const onBorrow = useCallback(() => {
if(!collateralReserve || !collateralReserveAddress) {
return;
}
borrow( borrow(
fromAccounts[0], fromAccounts[0],
parseFloat(value), parseFloat(value),
reserve, borrowReserve,
address, borrowReserveAddress,
collateralReserve,
new PublicKey(collateralReserveAddress),
connection, connection,
wallet); wallet);
}, [value, reserve, fromAccounts, address]); }, [value, borrowReserve, fromAccounts, borrowReserveAddress]);
const bodyStyle: React.CSSProperties = { const bodyStyle: React.CSSProperties = {
display: 'flex', display: 'flex',
@ -51,7 +67,7 @@ export const BorrowInput = (props: { className?: string, reserve: LendingReserve
How much would you like to borrow? How much would you like to borrow?
</div> </div>
<div className="token-input"> <div className="token-input">
<TokenIcon mintAddress={reserve?.liquidityMint} /> <TokenIcon mintAddress={borrowReserve?.liquidityMint} />
<NumericInput value={value} <NumericInput value={value}
onChange={(val: any) => { onChange={(val: any) => {
setValue(val); setValue(val);

View File

@ -55,6 +55,7 @@ export const useLending = () => {
.map(acc => [ .map(acc => [
(acc?.info as LendingReserve).collateralMint.toBase58(), (acc?.info as LendingReserve).collateralMint.toBase58(),
(acc?.info as LendingReserve).liquidityMint.toBase58(), (acc?.info as LendingReserve).liquidityMint.toBase58(),
(acc?.info as LendingReserve).dexMarket.toBase58(),
]) ])
].flat().filter((p) => p) as string[]; ].flat().filter((p) => p) as string[];

View File

@ -15,11 +15,6 @@ import { AccountInfo, Connection, PublicKey } from "@solana/web3.js";
import { useMemo } from "react"; import { useMemo } from "react";
import { EventEmitter } from "./../utils/eventEmitter"; import { EventEmitter } from "./../utils/eventEmitter";
interface RecentPoolData {
pool_identifier: string;
volume24hA: number;
}
export interface MarketsContextState { export interface MarketsContextState {
midPriceInUSD: (mint: string) => number; midPriceInUSD: (mint: string) => number;
marketEmitter: EventEmitter; marketEmitter: EventEmitter;

View File

@ -1 +1,2 @@
export * from "./account"; export * from "./account";
export * from './lending';

View File

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

View File

@ -1,2 +1,5 @@
export * from './market'; export * from './market';
export * from './reserve'; export * from './reserve';
export * from './obligation';
export * from './lending';
export * from './borrow';

View File

@ -0,0 +1,8 @@
export enum LendingInstruction {
InitLendingMarket = 0,
InitReserve = 1,
DepositReserveLiquidity = 2,
WithdrawReserveLiquidity = 3,
BorrowReserveLiquidity = 4,
RepayReserveLiquidity = 5
}

View File

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

View File

@ -12,6 +12,7 @@ import * as BufferLayout from "buffer-layout";
import { LENDING_PROGRAM_ID, TOKEN_PROGRAM_ID } from "../../constants/ids"; import { LENDING_PROGRAM_ID, TOKEN_PROGRAM_ID } from "../../constants/ids";
import { sendTransaction } from "../../contexts/connection"; import { sendTransaction } from "../../contexts/connection";
import * as Layout from "./../../utils/layout"; import * as Layout from "./../../utils/layout";
import { LendingInstruction } from './lending';
export const LendingReserveLayout: typeof BufferLayout.Structure = BufferLayout.struct( export const LendingReserveLayout: typeof BufferLayout.Structure = BufferLayout.struct(
[ [
@ -108,7 +109,7 @@ export const initReserveInstruction = (
const data = Buffer.alloc(dataLayout.span); const data = Buffer.alloc(dataLayout.span);
dataLayout.encode( dataLayout.encode(
{ {
instruction: 1, // Init reserve instruction instruction: LendingInstruction.InitReserve, // Init reserve instruction
liquidityAmount: new BN(liquidityAmount), liquidityAmount: new BN(liquidityAmount),
maxUtilizationRate: maxUtilizationRate, maxUtilizationRate: maxUtilizationRate,
}, },
@ -169,7 +170,7 @@ export const depositInstruction = (
const data = Buffer.alloc(dataLayout.span); const data = Buffer.alloc(dataLayout.span);
dataLayout.encode( dataLayout.encode(
{ {
instruction: 2, // Deposit instruction instruction: LendingInstruction.DepositReserveLiquidity, // Deposit instruction
liquidityAmount: new BN(liquidityAmount), liquidityAmount: new BN(liquidityAmount),
}, },
data data