From 5c5fdd8d5c5b36f8e1ef1654d15a1f542bea4dbe Mon Sep 17 00:00:00 2001 From: bartosz-lipinski <264380+bartosz-lipinski@users.noreply.github.com> Date: Wed, 18 Nov 2020 15:24:03 -0600 Subject: [PATCH] feat: add deposit instruction --- src/App.less | 2 +- src/actions/account.ts | 145 +++++++++++++++++++++++++++++ src/actions/deposit.tsx | 152 +++++++++++++++++++++++++++++++ src/components/Input/numeric.tsx | 7 +- src/hooks/useUserBalance.ts | 15 ++- src/models/lending/reserve.ts | 134 +++++++++++++++++++++++++-- src/utils/layout.ts | 3 +- src/views/deposit/add/index.tsx | 75 ++++++++++++--- src/views/deposit/view/item.tsx | 4 +- src/views/home/item.tsx | 4 +- src/views/reserve/index.tsx | 4 +- 11 files changed, 508 insertions(+), 37 deletions(-) create mode 100644 src/actions/account.ts create mode 100644 src/actions/deposit.tsx diff --git a/src/App.less b/src/App.less index c90a791..9f3941d 100644 --- a/src/App.less +++ b/src/App.less @@ -29,7 +29,7 @@ body { text-align: center; display: flex; align-self: center; - align-items: center; + align-items: centerconn; height: 100%; } diff --git a/src/actions/account.ts b/src/actions/account.ts new file mode 100644 index 0000000..302873e --- /dev/null +++ b/src/actions/account.ts @@ -0,0 +1,145 @@ +import { AccountLayout, Token } from "@solana/spl-token"; +import { Account, PublicKey, SystemProgram, TransactionInstruction } from "@solana/web3.js"; +import { TOKEN_PROGRAM_ID, WRAPPED_SOL_MINT } from "../constants/ids"; +import { TokenAccount } from "../models"; +import { cache, TokenAccountParser } from './../contexts/accounts'; + +export function ensureSplAccount( + instructions: TransactionInstruction[], + cleanupInstructions: TransactionInstruction[], + toCheck: TokenAccount, + payer: PublicKey, + amount: number, + signers: Account[] +) { + if (!toCheck.info.isNative) { + return toCheck.pubkey; + } + + const account = createUninitializedAccount( + instructions, + payer, + amount, + signers); + + instructions.push( + Token.createInitAccountInstruction( + TOKEN_PROGRAM_ID, + WRAPPED_SOL_MINT, + account, + payer + ) + ); + + cleanupInstructions.push( + Token.createCloseAccountInstruction( + TOKEN_PROGRAM_ID, + account, + payer, + payer, + [] + ) + ); + + return account; +} + +export function createUninitializedAccount( + instructions: TransactionInstruction[], + payer: PublicKey, + amount: number, + signers: Account[]) { + const account = new Account(); + instructions.push( + SystemProgram.createAccount({ + fromPubkey: payer, + newAccountPubkey: account.publicKey, + lamports: amount, + space: AccountLayout.span, + programId: TOKEN_PROGRAM_ID, + }) + ); + + signers.push(account); + + return account.publicKey; +} + +export function createTokenAccount( + instructions: TransactionInstruction[], + payer: PublicKey, + accountRentExempt: number, + mint: PublicKey, + owner: PublicKey, + signers: Account[], +) { + const account = createUninitializedAccount( + instructions, + payer, + accountRentExempt, + signers); + + instructions.push( + Token.createInitAccountInstruction( + TOKEN_PROGRAM_ID, + mint, + account, + owner + ) + ); + + return account; +} + +// TODO: check if one of to accounts needs to be native sol ... if yes unwrap it ... +export function findOrCreateAccountByMint( + payer: PublicKey, + owner: PublicKey, + instructions: TransactionInstruction[], + cleanupInstructions: TransactionInstruction[], + accountRentExempt: number, + mint: PublicKey, // use to identify same type + signers: Account[], + excluded?: Set +): PublicKey { + const accountToFind = mint.toBase58(); + const account = cache.byParser(TokenAccountParser) + .map(id => cache.get(id)) + .find( + (acc) => + acc !== undefined && + acc.info.mint.toBase58() === accountToFind && + acc.info.owner.toBase58() === owner.toBase58() && + (excluded === undefined || !excluded.has(acc.pubkey.toBase58())) + ); + const isWrappedSol = accountToFind === WRAPPED_SOL_MINT.toBase58(); + + let toAccount: PublicKey; + if (account && !isWrappedSol) { + toAccount = account.pubkey; + } else { + // creating depositor pool account + toAccount = createTokenAccount( + instructions, + payer, + accountRentExempt, + mint, + owner, + signers, + ); + + if (isWrappedSol) { + cleanupInstructions.push( + Token.createCloseAccountInstruction( + TOKEN_PROGRAM_ID, + toAccount, + payer, + payer, + [] + ) + ); + } + } + + return toAccount; +} diff --git a/src/actions/deposit.tsx b/src/actions/deposit.tsx new file mode 100644 index 0000000..9a5b7f2 --- /dev/null +++ b/src/actions/deposit.tsx @@ -0,0 +1,152 @@ +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, Token } from "@solana/spl-token"; +import { LENDING_PROGRAM_ID, TOKEN_PROGRAM_ID } from "../constants/ids"; +import { createUninitializedAccount, ensureSplAccount, findOrCreateAccountByMint } from "./account"; +import { cache, GenericAccountParser } from "../contexts/accounts"; +import { TokenAccount } from "../models"; +import { isConstructorDeclaration } from "typescript"; +import { LendingMarketParser } from "../models/lending"; +import { sign } from "crypto"; + +export const deposit = async ( + from: TokenAccount, + amount: number, + reserve: LendingReserve, + reserveAddress: PublicKey, + connection: Connection, + wallet: any) => { + + // TODO: customize ? + const MAX_UTILIZATION_RATE = 80; + + 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 + ); + + // TODO: ... + const amountLamports = amount; + + const fromAccount = ensureSplAccount( + instructions, + cleanupInstructions, + from, + wallet.publicKey, + amountLamports + accountRentExempt, + signers + ); + + // create approval for transfer transactions + instructions.push( + Token.createApproveInstruction( + TOKEN_PROGRAM_ID, + fromAccount, + authority, + wallet.publicKey, + [], + amountLamports, + ) + ); + + let toAccount: PublicKey; + if(isInitalized) { + // get destination account + toAccount = await findOrCreateAccountByMint( + wallet.publicKey, + wallet.publicKey, + instructions, + cleanupInstructions, + accountRentExempt, + reserve.liquidityMint, + signers + ); + } else { + toAccount = createUninitializedAccount( + instructions, + wallet.publicKey, + accountRentExempt, + signers, + ); + } + + if (isInitalized) { + // deposit + instructions.push( + depositInstruction( + amountLamports, + fromAccount, + toAccount, + authority, + reserveAddress, + reserve.liquiditySupply, + reserve.collateralMint, + ) + ); + } else { + // TODO: finish reserve init + + instructions.push(initReserveInstruction( + amountLamports, + MAX_UTILIZATION_RATE, + fromAccount, + toAccount, + reserveAddress, + reserve.liquidityMint, + reserve.liquiditySupply, + reserve.collateralMint, + reserve.collateralSupply, + reserve.lendingMarket, + authority, + reserve.dexMarket, + )) + } + + try { + let tx = await sendTransaction( + connection, + wallet, + instructions.concat(cleanupInstructions), + signers, + true + ); + + notify({ + message: "Funds deposited.", + type: "success", + description: `Transaction - ${tx}`, + }); +} catch { + // TODO: +} +} diff --git a/src/components/Input/numeric.tsx b/src/components/Input/numeric.tsx index ed3d55a..fb3cd55 100644 --- a/src/components/Input/numeric.tsx +++ b/src/components/Input/numeric.tsx @@ -5,7 +5,7 @@ export class NumericInput extends React.Component { onChange = (e: any) => { const { value } = e.target; const reg = /^-?\d*(\.\d*)?$/; - if ((!isNaN(value) && reg.test(value)) || value === "" || value === "-") { + if (reg.test(value) || value === "" || value === "-") { this.props.onChange(value); } }; @@ -17,6 +17,9 @@ export class NumericInput extends React.Component { if (value.charAt(value.length - 1) === "." || value === "-") { valueTemp = value.slice(0, -1); } + if (value.startsWith(".") || value.startsWith("-.")) { + valueTemp = valueTemp.replace(".", "0."); + } onChange(valueTemp.replace(/0*(\d+)/, "$1")); if (onBlur) { onBlur(); @@ -33,4 +36,4 @@ export class NumericInput extends React.Component { /> ); } -} +} \ No newline at end of file diff --git a/src/hooks/useUserBalance.ts b/src/hooks/useUserBalance.ts index b4b5cd5..a324d50 100644 --- a/src/hooks/useUserBalance.ts +++ b/src/hooks/useUserBalance.ts @@ -6,13 +6,18 @@ import { useUserAccounts } from "./useUserAccounts"; export function useUserBalance(mint?: PublicKey) { const { userAccounts } = useUserAccounts(); - const mintInfo = useMint(mint); - - return useMemo(() => - convert(userAccounts + const accounts = useMemo(() => { + return userAccounts .filter(acc => mint?.equals(acc.info.mint)) + .sort((a, b) => b.info.amount.sub(a.info.amount).toNumber()); + }, [userAccounts]); + + const balance = useMemo(() => + convert(accounts .reduce((res, item) => res += item.info.amount.toNumber(), 0) , mintInfo), - [userAccounts]); + [accounts, mintInfo]); + + return { balance, accounts }; } \ No newline at end of file diff --git a/src/models/lending/reserve.ts b/src/models/lending/reserve.ts index f6f37c7..f2653a8 100644 --- a/src/models/lending/reserve.ts +++ b/src/models/lending/reserve.ts @@ -1,28 +1,36 @@ 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"; export const LendingReserveLayout: typeof BufferLayout.Structure = BufferLayout.struct( [ - BufferLayout.u8("isInitialized"), + Layout.uint64("lastUpdateSlot"), Layout.publicKey("lendingMarket"), - Layout.publicKey("liquiditySupply"), Layout.publicKey("liquidityMint"), - Layout.publicKey("collateralSupply"), + Layout.publicKey("liquiditySupply"), Layout.publicKey("collateralMint"), + Layout.publicKey("collateralSupply"), // TODO: replace u32 option with generic quivalent BufferLayout.u32('dexMarketOption'), Layout.publicKey("dexMarket"), - Layout.uint64("dexMarketPrice"), - Layout.uint64("dexMarketPriceUpdatedSlot"), + BufferLayout.u8("maxUtilizationRate"), Layout.uint128("cumulative_borrow_rate"), Layout.uint128("total_borrows"), - Layout.uint64("borrow_state_update_slot"), + + Layout.uint64("totalLiquidity"), + Layout.uint64("collateralMintSupply"), ] ); @@ -31,7 +39,8 @@ export const isLendingReserve = (info: AccountInfo) => { } export interface LendingReserve { - isInitialized: boolean, + lastUpdateSlot: BN; + lendingMarket: PublicKey; liquiditySupply: PublicKey; liquidityMint: PublicKey; @@ -42,11 +51,11 @@ export interface LendingReserve { dexMarket: PublicKey; dexMarketPrice: BN; // what is precision on the price? + maxUtilizationRate: number; dexMarketPriceUpdatedSlot: BN; // Layout.uint128("cumulative_borrow_rate"), // Layout.uint128("total_borrows"), - borrow_state_update_slot: BN; } export const LendingReserveParser = (pubKey: PublicKey, info: AccountInfo) => { @@ -64,5 +73,110 @@ export const LendingReserveParser = (pubKey: PublicKey, info: AccountInfo { + const dataLayout = BufferLayout.struct([ + BufferLayout.u8("instruction"), + Layout.uint64("liquidityAmount"), + BufferLayout.u8("maxUtilizationRate") + ]); + + const data = Buffer.alloc(dataLayout.span); + dataLayout.encode( + { + instruction: 1, // Init reserve instruction + liquidityAmount: new BN(liquidityAmount), + maxUtilizationRate: maxUtilizationRate, + }, + data + ); + + const keys = [ + { pubkey: from, isSigner: false, isWritable: true }, + { pubkey: to, isSigner: false, isWritable: true }, + { pubkey: reserveAccount, isSigner: false, isWritable: true }, + { pubkey: liquidityMint, isSigner: false, isWritable: false }, + { pubkey: liquiditySupply, isSigner: false, isWritable: true }, + { pubkey: collateralMint, isSigner: false, isWritable: true }, + { pubkey: collateralSupply, isSigner: false, isWritable: true }, + { pubkey: lendingMarket, isSigner: false, isWritable: true }, + { pubkey: lendingMarketAuthority, 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 }, + + // optionals + { pubkey: dexMarket, isSigner: false, isWritable: false }, + ]; + return new TransactionInstruction({ + keys, + programId: LENDING_PROGRAM_ID, + data, + }); +}; + +/// Deposit liquidity into a reserve. The output is a collateral token representing ownership +/// of the reserve liquidity pool. +/// +/// 0. `[writable]` Liquidity input SPL Token account. $authority can transfer $liquidity_amount +/// 1. `[writable]` Collateral output SPL Token account, +/// 2. `[writable]` Reserve account. +/// 3. `[writable]` Reserve liquidity supply SPL Token account. +/// 4. `[writable]` Reserve collateral SPL Token mint. +/// 5. `[]` Derived lending market authority ($authority). +/// 6. `[]` Clock sysvar +/// 7. '[]` Token program id +export const depositInstruction = ( + liquidityAmount: number | BN, + from: PublicKey, // Liquidity input SPL Token account. $authority can transfer $liquidity_amount + to: PublicKey, // Collateral output SPL Token account, + reserveAuthority: PublicKey, + reserveAccount: PublicKey, + reserveSupply: PublicKey, + collateralMint: PublicKey, +): TransactionInstruction => { + const dataLayout = BufferLayout.struct([ + BufferLayout.u8("instruction"), + Layout.uint64("liquidityAmount"), + ]); + + const data = Buffer.alloc(dataLayout.span); + dataLayout.encode( + { + instruction: 2, // Deposit instruction + liquidityAmount: new BN(liquidityAmount), + }, + data + ); + + const keys = [ + { pubkey: from, isSigner: false, isWritable: true }, + { pubkey: to, isSigner: false, isWritable: true }, + { pubkey: reserveAccount, isSigner: false, isWritable: true }, + { pubkey: reserveSupply, isSigner: false, isWritable: true }, + { pubkey: collateralMint, isSigner: false, isWritable: true }, + { pubkey: reserveAuthority, isSigner: false, isWritable: false }, + { pubkey: SYSVAR_CLOCK_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/utils/layout.ts b/src/utils/layout.ts index 6ab1e6b..a99313f 100644 --- a/src/utils/layout.ts +++ b/src/utils/layout.ts @@ -46,10 +46,11 @@ export const uint64 = (property = "uint64"): unknown => { layout.encode = (num: BN, buffer: Buffer, offset: number) => { const a = num.toArray().reverse(); - const b = Buffer.from(a); + let b = Buffer.from(a); if (b.length !== 8) { const zeroPad = Buffer.alloc(8); b.copy(zeroPad); + b = zeroPad; } return _encode(b, buffer, offset); diff --git a/src/views/deposit/add/index.tsx b/src/views/deposit/add/index.tsx index 4bf35d3..2c1b18c 100644 --- a/src/views/deposit/add/index.tsx +++ b/src/views/deposit/add/index.tsx @@ -1,28 +1,79 @@ -import React, { useMemo } from "react"; +import React, { useCallback, useEffect, useMemo, useState } from "react"; import { useLendingReserve, useTokenName, useUserAccounts, useUserBalance } from './../../../hooks'; import { LendingReserve, LendingReserveParser } from "../../../models/lending"; import { TokenIcon } from "../../../components/TokenIcon"; import { formatNumber } from "../../../utils/utils"; import { Button } from "antd"; import { useParams } from "react-router-dom"; -import { useAccount } from "../../../contexts/accounts"; +import { cache, useAccount } from "../../../contexts/accounts"; +import { NumericInput } from "../../../components/Input/numeric"; +import { useConnection } from "../../../contexts/connection"; +import { useWallet } from "../../../contexts/wallet"; +import { deposit } from './../../../actions/deposit'; export const DepositAddView = () => { + const connection = useConnection(); + const { wallet } = useWallet(); const { id } = useParams<{ id: string }>(); + const [value, setValue] = useState(''); const lendingReserve = useLendingReserve(id); const reserve = lendingReserve?.info; - - const name = useTokenName(reserve?.liquidityMint); - const tokenBalance = useUserBalance(reserve?.liquidityMint); - const collateralBalance = useUserBalance(reserve?.collateralMint); - return
+ const name = useTokenName(reserve?.liquidityMint); + const { balance: tokenBalance, accounts: fromAccounts } = useUserBalance(reserve?.liquidityMint); + // const collateralBalance = useUserBalance(reserve?.collateralMint); + + useEffect(() => { + (async () => { + const reserve = lendingReserve?.info; + if(!reserve) { + return; + } + + console.log(`utlization: ${reserve.maxUtilizationRate}`) + console.log(`lendingMarket: ${reserve.lendingMarket.toBase58()}`); + + const lendingMarket = await cache.get(reserve.lendingMarket); + console.log(`lendingMarket quote: ${lendingMarket?.info.quoteMint.toBase58()}`); + + console.log(`liquiditySupply: ${reserve.liquiditySupply.toBase58()}`); + console.log(`liquidityMint: ${reserve.liquidityMint.toBase58()}`); + console.log(`collateralSupply: ${reserve.collateralSupply.toBase58()}`); + console.log(`collateralMint: ${reserve.collateralMint.toBase58()}`); + })(); + }, [lendingReserve]) + + const onDeposit = useCallback(() => { + if(!lendingReserve || !reserve) { + return; + } + + deposit( + fromAccounts[0], + parseFloat(value), + reserve, + lendingReserve.pubkey, + connection, + wallet); + }, [value, reserve, fromAccounts]); + + return
- {name} -
{formatNumber.format(tokenBalance)} {name}
-
{formatNumber.format(collateralBalance)} {name}
-
--
- + { + setValue(val); + }} + style={{ + fontSize: 20, + boxShadow: "none", + borderColor: "transparent", + outline: "transpaernt", + }} + placeholder="0.00" + /> +
{name}
+ + ADD: {id}
; diff --git a/src/views/deposit/view/item.tsx b/src/views/deposit/view/item.tsx index d7969a4..8d9d1f0 100644 --- a/src/views/deposit/view/item.tsx +++ b/src/views/deposit/view/item.tsx @@ -9,8 +9,8 @@ import { PublicKey } from "@solana/web3.js"; export const ReserveItem = (props: { reserve: LendingReserve, address: PublicKey }) => { const name = useTokenName(props.reserve.liquidityMint); - const tokenBalance = useUserBalance(props.reserve.liquidityMint); - const collateralBalance = useUserBalance(props.reserve.collateralMint); + const { balance: tokenBalance } = useUserBalance(props.reserve.liquidityMint); + const { balance: collateralBalance } = useUserBalance(props.reserve.collateralMint); return
{name} diff --git a/src/views/home/item.tsx b/src/views/home/item.tsx index e9480bc..66de219 100644 --- a/src/views/home/item.tsx +++ b/src/views/home/item.tsx @@ -10,8 +10,8 @@ import { useAccount, useMint } from "../../contexts/accounts"; export const LendingReserveItem = (props: { reserve: LendingReserve, address: PublicKey }) => { const name = useTokenName(props.reserve.liquidityMint); - const tokenBalance = useUserBalance(props.reserve.liquidityMint); - const collateralBalance = useUserBalance(props.reserve.collateralMint); + const { balance: tokenBalance } = useUserBalance(props.reserve.liquidityMint); + const { balance: collateralBalance } = useUserBalance(props.reserve.collateralMint); const collateralSupply = useAccount(props.reserve.collateralSupply); const liquiditySupply = useAccount(props.reserve.liquiditySupply); diff --git a/src/views/reserve/index.tsx b/src/views/reserve/index.tsx index 878e4a7..6c37771 100644 --- a/src/views/reserve/index.tsx +++ b/src/views/reserve/index.tsx @@ -14,8 +14,8 @@ export const ReserveView = () => { const reserve = lendingReserve?.info; const name = useTokenName(reserve?.liquidityMint); - const tokenBalance = useUserBalance(reserve?.liquidityMint); - const collateralBalance = useUserBalance(reserve?.collateralMint); + const { balance: tokenBalance } = useUserBalance(reserve?.liquidityMint); + const { balance: collateralBalance } = useUserBalance(reserve?.collateralMint); return