feat: query users obligations

This commit is contained in:
bartosz-lipinski 2020-11-21 23:11:36 -06:00
parent e2f8cafade
commit ab975de0b8
24 changed files with 564 additions and 75 deletions

View File

@ -2,7 +2,6 @@
Any content produced by Solana, or developer resources that Solana provides, are for educational and inspiration purposes only. Solana does not encourage, induce or sanction the deployment of any such applications in violation of applicable laws or regulations.
## TODO
- [] Calculate deposit APY and borrow APY for home page
@ -14,4 +13,4 @@ Any content produced by Solana, or developer resources that Solana provides, are
- [] Add liquidate view
- [] Borrow view calculate available to you and borrow APY
- [] Facuet add USDC that is using sol airdrop and USDC reserve to give user USDC
- [] Borrow - Convert target ccy to collateral on oposite side
- [] Borrow - Convert target ccy to collateral on oposite side

View File

@ -5,7 +5,11 @@ import {
SystemProgram,
TransactionInstruction,
} from "@solana/web3.js";
import { LENDING_PROGRAM_ID, TOKEN_PROGRAM_ID, WRAPPED_SOL_MINT } from "../constants/ids";
import {
LENDING_PROGRAM_ID,
TOKEN_PROGRAM_ID,
WRAPPED_SOL_MINT,
} from "../constants/ids";
import { LendingObligationLayout, TokenAccount } from "../models";
import { cache, TokenAccountParser } from "./../contexts/accounts";

View File

@ -108,7 +108,7 @@ export const borrow = async (
cleanupInstructions = [];
const [authority] = await PublicKey.findProgramAddress(
[depositReserve.lendingMarket.toBuffer()], // which account should be authority
[depositReserve.lendingMarket.toBuffer()],
LENDING_PROGRAM_ID
);

View File

@ -1,4 +1,5 @@
export { borrow } from "./borrow";
export { deposit } from "./deposit";
export { repay } from "./repay";
export { withdraw } from "./withdraw";
export * from "./account";

112
src/actions/repay.tsx Normal file
View File

@ -0,0 +1,112 @@
import {
Account,
Connection,
PublicKey,
TransactionInstruction,
} from "@solana/web3.js";
import { sendTransaction } from "../contexts/connection";
import { notify } from "../utils/notifications";
import {
LendingReserve,
} from "./../models/lending/reserve";
import {
repayInstruction,
} from "./../models/lending/repay";
import { AccountLayout, Token } from "@solana/spl-token";
import { LENDING_PROGRAM_ID, TOKEN_PROGRAM_ID } from "../constants/ids";
import { findOrCreateAccountByMint } from "./account";
import { LendingObligation, TokenAccount } from "../models";
export const repay = async (
from: TokenAccount, // CollateralAccount
amountLamports: number, // in collateral token (lamports)
// which loan to repay
obligation: LendingObligation,
repayReserve: LendingReserve,
repayReserveAddress: PublicKey,
withdrawReserve: LendingReserve,
withdrawReserveAddress: PublicKey,
connection: Connection,
wallet: any
) => {
notify({
message: "Repaing funds...",
description: "Please review transactions to approve.",
type: "warn",
});
// user from account
const signers: Account[] = [];
const instructions: TransactionInstruction[] = [];
const cleanupInstructions: TransactionInstruction[] = [];
const accountRentExempt = await connection.getMinimumBalanceForRentExemption(
AccountLayout.span
);
const [authority] = await PublicKey.findProgramAddress(
[repayReserve.lendingMarket.toBuffer()],
LENDING_PROGRAM_ID
);
const fromAccount = from.pubkey;
// create approval for transfer transactions
instructions.push(
Token.createApproveInstruction(
TOKEN_PROGRAM_ID,
fromAccount,
authority,
wallet.publicKey,
[],
amountLamports
)
);
// get destination account
const toAccount = await findOrCreateAccountByMint(
wallet.publicKey,
wallet.publicKey,
instructions,
cleanupInstructions,
accountRentExempt,
withdrawReserve.liquidityMint,
signers
);
// TODO: add obligation
// instructions.push(
// repayInstruction(
// amountLamports,
// fromAccount,
// toAccount,
// reserveAddress,
// reserve.collateralMint,
// reserve.liquiditySupply,
// authority
// )
// );
try {
let tx = await sendTransaction(
connection,
wallet,
instructions.concat(cleanupInstructions),
signers,
true
);
notify({
message: "Funds repaid.",
type: "success",
description: `Transaction - ${tx}`,
});
} catch {
// TODO:
}
};

View File

@ -39,7 +39,7 @@ export const withdraw = async (
);
const [authority] = await PublicKey.findProgramAddress(
[reserve.lendingMarket.toBuffer()], // which account should be authority
[reserve.lendingMarket.toBuffer()],
LENDING_PROGRAM_ID
);

View File

@ -1,65 +1,17 @@
import React, { useCallback, useMemo, useState } from "react";
import { useLendingReserves, useTokenName, useUserBalance } from "../../hooks";
import { useTokenName, useUserBalance } from "../../hooks";
import { LendingReserve, LendingReserveParser } from "../../models";
import { TokenIcon } from "../TokenIcon";
import { getTokenName } from "../../utils/utils";
import { Button, Card, Select } from "antd";
import { Button, Card } from "antd";
import { cache, ParsedAccount } from "../../contexts/accounts";
import { NumericInput } from "../Input/numeric";
import { useConnection, useConnectionConfig } from "../../contexts/connection";
import { useConnection } from "../../contexts/connection";
import { useWallet } from "../../contexts/wallet";
import { borrow } from "../../actions";
import { PublicKey } from "@solana/web3.js";
import { CollateralSelector } from "./../CollateralSelector";
import "./style.less";
const { Option } = Select;
const CollateralSelector = (props: {
reserve: LendingReserve;
mint?: string;
onMintChange: (id: string) => void;
}) => {
const { reserveAccounts } = useLendingReserves();
const { tokenMap } = useConnectionConfig();
return (
<Select
size="large"
showSearch
style={{ minWidth: 120 }}
placeholder="Collateral"
value={props.mint}
onChange={(item) => {
if (props.onMintChange) {
props.onMintChange(item);
}
}}
filterOption={(input, option) =>
option?.name?.toLowerCase().indexOf(input.toLowerCase()) >= 0
}
>
{reserveAccounts
.filter((reserve) => reserve.info !== props.reserve)
.map((reserve) => {
const mint = reserve.info.liquidityMint.toBase58();
const address = reserve.pubkey.toBase58();
const name = getTokenName(tokenMap, mint);
return (
<Option key={address} value={address} name={name} title={address}>
<div
key={address}
style={{ display: "flex", alignItems: "center" }}
>
<TokenIcon mintAddress={mint} />
{name}
</div>
</Option>
);
})}
</Select>
);
};
export const BorrowInput = (props: {
className?: string;
reserve: LendingReserve;

View File

@ -0,0 +1,55 @@
import React from "react";
import { useLendingReserves } from "../../hooks";
import { LendingReserve } from "../../models";
import { TokenIcon } from "../TokenIcon";
import { getTokenName } from "../../utils/utils";
import { Select } from "antd";
import { useConnectionConfig } from "../../contexts/connection";
const { Option } = Select;
export const CollateralSelector = (props: {
reserve: LendingReserve;
mint?: string;
onMintChange: (id: string) => void;
}) => {
const { reserveAccounts } = useLendingReserves();
const { tokenMap } = useConnectionConfig();
return (
<Select
size="large"
showSearch
style={{ minWidth: 120 }}
placeholder="Collateral"
value={props.mint}
onChange={(item) => {
if (props.onMintChange) {
props.onMintChange(item);
}
}}
filterOption={(input, option) =>
option?.name?.toLowerCase().indexOf(input.toLowerCase()) >= 0
}
>
{reserveAccounts
.filter((reserve) => reserve.info !== props.reserve)
.map((reserve) => {
const mint = reserve.info.liquidityMint.toBase58();
const address = reserve.pubkey.toBase58();
const name = getTokenName(tokenMap, mint);
return (
<Option key={address} value={address} name={name} title={address}>
<div
key={address}
style={{ display: "flex", alignItems: "center" }}
>
<TokenIcon mintAddress={mint} />
{name}
</div>
</Option>
);
})}
</Select>
);
};

View File

@ -0,0 +1,128 @@
import React, { useCallback, useMemo, useState } from "react";
import { useTokenName, useUserBalance } from "../../hooks";
import { LendingObligation, LendingReserve, LendingReserveParser } from "../../models";
import { TokenIcon } from "../TokenIcon";
import { Button, Card } from "antd";
import { cache, ParsedAccount } from "../../contexts/accounts";
import { NumericInput } from "../Input/numeric";
import { useConnection } from "../../contexts/connection";
import { useWallet } from "../../contexts/wallet";
import { repay } from "../../actions";
import { PublicKey } from "@solana/web3.js";
import { CollateralSelector } from "./../CollateralSelector";
import "./style.less";
export const RepayInput = (props: {
className?: string;
reserve: LendingReserve;
obligation: LendingObligation;
address: PublicKey;
}) => {
const connection = useConnection();
const { wallet } = useWallet();
const [value, setValue] = useState("");
const repayReserve = props.reserve;
const repayReserveAddress = props.address;
const obligation = props.obligation;
const [collateralReserveMint, setCollateralReserveMint] = useState<string>();
const collateralReserve = useMemo(() => {
const id: string =
cache
.byParser(LendingReserveParser)
.find((acc) => acc === collateralReserveMint) || "";
return cache.get(id) as ParsedAccount<LendingReserve>;
}, [collateralReserveMint]);
const name = useTokenName(repayReserve?.liquidityMint);
const { accounts: fromAccounts } = useUserBalance(
collateralReserve?.info.collateralMint
);
// const collateralBalance = useUserBalance(reserve?.collateralMint);
const onReoay = useCallback(() => {
if (!collateralReserve) {
return;
}
repay(
fromAccounts[0],
parseFloat(value),
obligation,
repayReserve,
repayReserveAddress,
collateralReserve.info,
collateralReserve.pubkey,
connection,
wallet
);
}, [
connection,
wallet,
value,
obligation,
collateralReserve,
repayReserve,
fromAccounts,
repayReserveAddress,
]);
const bodyStyle: React.CSSProperties = {
display: "flex",
flex: 1,
justifyContent: "center",
alignItems: "center",
height: "100%",
};
return (
<Card className={props.className} bodyStyle={bodyStyle}>
<div
style={{
display: "flex",
flexDirection: "column",
justifyContent: "space-around",
}}
>
<div className="repay-input-title">
How much would you like to repay?
</div>
<div className="token-input">
<TokenIcon mintAddress={repayReserve?.liquidityMint} />
<NumericInput
value={value}
onChange={(val: any) => {
setValue(val);
}}
autoFocus={true}
style={{
fontSize: 20,
boxShadow: "none",
borderColor: "transparent",
outline: "transpaernt",
}}
placeholder="0.00"
/>
<div>{name}</div>
</div>
<div className="repay-input-title">Select collateral account?</div>
<CollateralSelector
reserve={repayReserve}
mint={collateralReserveMint}
onMintChange={setCollateralReserveMint}
/>
<Button
type="primary"
onClick={onReoay}
disabled={fromAccounts.length === 0}
>
Repay
</Button>
</div>
</Card>
);
};

View File

@ -0,0 +1,3 @@
.borrow-input-title {
font-size: 1.05rem;
}

View File

@ -115,7 +115,7 @@ export const UserLendingCard = (props: {
<Button>Withdraw</Button>
</Link>
<Link to={`/repay/${address}`}>
<Button disabled={true}>Repay</Button>
<Button>Repay</Button>
</Link>
</div>
</Card>

View File

@ -7,6 +7,8 @@ import {
isLendingMarket,
LendingReserveParser,
LendingReserve,
isLendingObligation,
LendingObligationParser,
} from "./../models/lending";
import {
cache,
@ -51,6 +53,12 @@ export const useLending = () => {
item.account,
LendingMarketParser
);
}else if (isLendingObligation(item.account)) {
return cache.add(
item.pubkey.toBase58(),
item.account,
LendingObligationParser,
);
}
}, []);

View File

@ -4,3 +4,5 @@ export * from "./useLendingReserves";
export * from "./useTokenName";
export * from "./useUserBalance";
export * from "./useCollateralBalance";
export * from "./useLendingObligations";
export * from "./useUserObligations";

View File

@ -0,0 +1,58 @@
import { PublicKey } from "@solana/web3.js";
import { useEffect, useState } from "react";
import { LendingObligation, LendingObligationParser } from "../models/lending";
import { cache, ParsedAccount } from "./../contexts/accounts";
const getLendingObligations = () => {
return cache
.byParser(LendingObligationParser)
.map((id) => cache.get(id))
.filter((acc) => acc !== undefined) as any[];
};
export function useLendingObligations() {
const [obligations, setObligations] = useState<
ParsedAccount<LendingObligation>[]
>([]);
useEffect(() => {
setObligations(getLendingObligations());
const dispose = cache.emitter.onCache((args) => {
if (args.parser === LendingObligationParser) {
setObligations(getLendingObligations());
}
});
return () => {
dispose();
};
}, [setObligations]);
return {
obligations,
};
}
export function useLendingObligation(address: string | PublicKey) {
const id = typeof address === "string" ? address : address?.toBase58();
const [obligationAccount, setObligationAccount] = useState<
ParsedAccount<LendingObligation>
>();
useEffect(() => {
setObligationAccount(cache.get(id));
const dispose = cache.emitter.onCache((args) => {
if (args.id === id) {
setObligationAccount(cache.get(id));
}
});
return () => {
dispose();
};
}, [id, setObligationAccount]);
return obligationAccount;
}

View File

@ -0,0 +1,40 @@
import { useMemo } from "react";
import { useUserAccounts } from "./useUserAccounts";
import { useLendingObligations } from "./useLendingObligations";
import { TokenAccount } from "../models";
export function useUserObligations() {
const { userAccounts } = useUserAccounts();
const { obligations } = useLendingObligations();
const accountsByMint = useMemo(() => {
return userAccounts.reduce((res, acc) => {
const id = acc.info.mint.toBase58();
res.set(id, [...(res.get(id) || []), acc]);
return res;
}, new Map<string, TokenAccount[]>())
},
[userAccounts]);
const userObligations = useMemo(() => {
if(accountsByMint.size === 0) {
return [];
}
return obligations
.filter((acc) => accountsByMint.get(acc.info.tokenMint.toBase58()) !== undefined)
.map(ob => {
return {
oblication: ob,
userAccounts: [...accountsByMint.get(ob.info.tokenMint.toBase58())],
// TODO: add total borrowed amount?
}
});
}, [accountsByMint, obligations]);
return {
userObligations
};
}

View File

@ -1,4 +1,8 @@
import { PublicKey, SYSVAR_CLOCK_PUBKEY, TransactionInstruction } from "@solana/web3.js";
import {
PublicKey,
SYSVAR_CLOCK_PUBKEY,
TransactionInstruction,
} from "@solana/web3.js";
import BN from "bn.js";
import { LENDING_PROGRAM_ID, TOKEN_PROGRAM_ID } from "../../constants/ids";
import { LendingInstruction } from "./lending";
@ -32,7 +36,7 @@ export const liquidateInstruction = (
authority: PublicKey,
dexMarket: PublicKey,
dexOrderBookSide: PublicKey,
memory: PublicKey,
memory: PublicKey
): TransactionInstruction => {
const dataLayout = BufferLayout.struct([
BufferLayout.u8("instruction"),
@ -56,7 +60,11 @@ export const liquidateInstruction = (
{ pubkey: repayReserveLiquiditySupply, isSigner: false, isWritable: true },
{ pubkey: withdrawReserve, isSigner: false, isWritable: true },
{ pubkey: withdrawReserveCollateralSupply, isSigner: false, isWritable: true },
{
pubkey: withdrawReserveCollateralSupply,
isSigner: false,
isWritable: true,
},
{ pubkey: obligation, isSigner: false, isWritable: true },
@ -75,4 +83,4 @@ export const liquidateInstruction = (
programId: LENDING_PROGRAM_ID,
data,
});
};
};

View File

@ -1,4 +1,4 @@
import { PublicKey } from "@solana/web3.js";
import { AccountInfo, PublicKey } from "@solana/web3.js";
import BN from "bn.js";
import * as BufferLayout from "buffer-layout";
import * as Layout from "./../../utils/layout";
@ -22,6 +22,10 @@ export const LendingObligationLayout: typeof BufferLayout.Structure = BufferLayo
]
);
export const isLendingObligation = (info: AccountInfo<Buffer>) => {
return info.data.length === LendingObligationLayout.span;
};
export interface LendingObligation {
lastUpdateSlot: BN;
collateralAmount: BN;
@ -31,3 +35,23 @@ export interface LendingObligation {
borrowReserve: PublicKey;
tokenMint: PublicKey;
}
export const LendingObligationParser = (
pubKey: PublicKey,
info: AccountInfo<Buffer>
) => {
const buffer = Buffer.from(info.data);
const data = LendingObligationLayout.decode(buffer);
console.log(data);
const details = {
pubkey: pubKey,
account: {
...info,
},
info: data,
};
return details;
};

View File

@ -1,4 +1,8 @@
import { PublicKey, SYSVAR_CLOCK_PUBKEY, TransactionInstruction } from "@solana/web3.js";
import {
PublicKey,
SYSVAR_CLOCK_PUBKEY,
TransactionInstruction,
} from "@solana/web3.js";
import BN from "bn.js";
import { LENDING_PROGRAM_ID, TOKEN_PROGRAM_ID } from "../../constants/ids";
import { LendingInstruction } from "./lending";
@ -55,12 +59,16 @@ export const repayInstruction = (
{ pubkey: repayReserveLiquiditySupply, isSigner: false, isWritable: true },
{ pubkey: withdrawReserve, isSigner: false, isWritable: false },
{ pubkey: withdrawReserveCollateralSupply, isSigner: false, isWritable: true },
{
pubkey: withdrawReserveCollateralSupply,
isSigner: false,
isWritable: true,
},
{ pubkey: obligation, isSigner: false, isWritable: true },
{ pubkey: obligationMint, isSigner: false, isWritable: true },
{ pubkey: obligationInput, isSigner: false, isWritable: true },
{ pubkey: authority, isSigner: false, isWritable: false },
{ pubkey: SYSVAR_CLOCK_PUBKEY, isSigner: false, isWritable: false },
{ pubkey: TOKEN_PROGRAM_ID, isSigner: false, isWritable: false },
@ -70,4 +78,4 @@ export const repayInstruction = (
programId: LENDING_PROGRAM_ID,
data,
});
};
};

View File

@ -8,15 +8,16 @@ import { LendingProvider } from "./contexts/lending";
import { AppLayout } from "./components/Layout";
import {
HomeView,
DepositView,
DepositReserveView,
BorrowView,
ReserveView,
DashboardView,
BorrowReserveView,
WithdrawView,
BorrowView,
DashboardView,
DepositReserveView,
DepositView,
FaucetView,
HomeView,
RepayReserveView,
ReserveView,
WithdrawView,
} from "./views";
export function Routes() {
@ -52,6 +53,10 @@ export function Routes() {
path="/borrow/:id"
children={<BorrowReserveView />}
/>
<Route
path="/repay/:id"
children={<RepayReserveView />}
/>
<Route exact path="/faucet" children={<FaucetView />} />
</Switch>
</AppLayout>

View File

@ -1,9 +1,16 @@
import React from "react";
import { useUserObligations } from "./../../hooks";
export const DashboardView = () => {
const { userObligations } = useUserObligations();
return (
<div className="flexColumn">
DASHBOARD: TODO: 1. Add deposits 2. Add obligations
{userObligations.map(item => {
return <div>{item?.oblication.info.borrowAmount.toString()}</div>;
})}
</div>
);
};

View File

@ -20,7 +20,7 @@ export const LendingReserveItem = (props: {
props.reserve.totalLiquidity.toNumber(),
liquidityMint
);
const totalBorrows = props.reserve.totalBorrows.toString();
console.log(liquidityMint);

View File

@ -7,3 +7,4 @@ export { DepositReserveView } from "./depositReserve";
export { ReserveView } from "./reserve";
export { WithdrawView } from "./withdraw";
export { FaucetView } from "./faucet";
export { RepayReserveView } from "./repayReserve";

View File

@ -0,0 +1,44 @@
import React from "react";
import { useLendingReserve } from "../../hooks";
import { useParams } from "react-router-dom";
import { RepayInput } from "../../components/RepayInput";
import {
SideReserveOverview,
SideReserveOverviewMode,
} from "../../components/SideReserveOverview";
import "./style.less";
import { LendingObligation } from "../../models";
export const RepayReserveView = () => {
const { id } = useParams<{ id: string }>();
const lendingReserve = useLendingReserve(id);
const reserve = lendingReserve?.info;
// TODO: query for lending obligation
const ob: LendingObligation = {} as any;
if (!reserve || !lendingReserve) {
return null;
}
return (
<div className="repay-reserve">
<div className="repay-reserve-container">
<RepayInput
className="repay-reserve-item repay-reserve-item-left"
reserve={reserve}
obligation={ob}
address={lendingReserve.pubkey}
/>
<SideReserveOverview
className="repay-reserve-item repay-reserve-item-right"
reserve={reserve}
address={lendingReserve.pubkey}
mode={SideReserveOverviewMode.Borrow}
/>
</div>
</div>
);
};

View File

@ -0,0 +1,30 @@
.repay-reserve {
display: flex;
flex-direction: column;
flex: 1;
}
.repay-reserve-item {
margin: 4px;
}
.repay-reserve-container {
display: flex;
flex-wrap: wrap;
flex: 1;
}
.repay-reserve-item-left {
flex: 60%;
}
.repay-reserve-item-right {
flex: 30%;
}
/* Responsive layout - makes a one column layout instead of a two-column layout */
@media (max-width: 600px) {
.repay-reserve-item-right, .repay-reserve-item-left {
flex: 100%;
}
}