feat: add deposit instruction

This commit is contained in:
bartosz-lipinski 2020-11-18 15:24:03 -06:00
parent 682b21c9f9
commit 5c5fdd8d5c
11 changed files with 508 additions and 37 deletions

View File

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

145
src/actions/account.ts Normal file
View File

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

152
src/actions/deposit.tsx Normal file
View File

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

View File

@ -5,7 +5,7 @@ export class NumericInput extends React.Component<any, any> {
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<any, any> {
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<any, any> {
/>
);
}
}
}

View File

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

View File

@ -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<Buffer>) => {
}
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<Buffer>) => {
@ -64,5 +73,110 @@ export const LendingReserveParser = (pubKey: PublicKey, info: AccountInfo<Buffer
return details;
};
// TODO:
// create instructions for init, deposit and withdraw
export const initReserveInstruction = (
liquidityAmount: number | BN,
maxUtilizationRate: number,
from: PublicKey, // Liquidity input SPL Token account. $authority can transfer $liquidity_amount
to: PublicKey, // Collateral output SPL Token account,
reserveAccount: PublicKey,
liquidityMint: PublicKey,
liquiditySupply: PublicKey,
collateralMint: PublicKey,
collateralSupply: PublicKey,
lendingMarket: PublicKey,
lendingMarketAuthority: PublicKey,
dexMarket: PublicKey, // TODO: optional
): TransactionInstruction => {
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,
});
};

View File

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

View File

@ -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 <div style={{ display: 'flex', justifyContent: 'space-around' }}>
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 <div style={{ display: 'flex', flexDirection: 'column', justifyContent: 'space-around' }}>
<TokenIcon mintAddress={reserve?.liquidityMint} />
{name}
<div>{formatNumber.format(tokenBalance)} {name}</div>
<div>{formatNumber.format(collateralBalance)} {name}</div>
<div>--</div>
<Button>Deposit</Button>
<NumericInput value={value}
onChange={(val: any) => {
setValue(val);
}}
style={{
fontSize: 20,
boxShadow: "none",
borderColor: "transparent",
outline: "transpaernt",
}}
placeholder="0.00"
/>
<div>{name}</div>
<Button onClick={onDeposit} disabled={fromAccounts.length === 0}>Deposit</Button>
ADD: {id}
</div>;

View File

@ -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 <div style={{ display: 'flex', justifyContent: 'space-around' }}>
<span style={{ display: 'flex' }}><TokenIcon mintAddress={props.reserve.liquidityMint} />{name}</span>

View File

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

View File

@ -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 <div style={{ display: 'flex', justifyContent: 'space-around' }}>
<TokenIcon mintAddress={reserve?.liquidityMint} />