Intermediary step where ive gotten everything at least green and moved to common and linked

This commit is contained in:
Dummy Tester 123 2021-02-06 16:08:34 -06:00
parent 27c62ce9d3
commit 7e2749b9bc
118 changed files with 2387 additions and 3400 deletions

7
.gitignore vendored
View File

@ -2,7 +2,12 @@
# dependencies # dependencies
*/node_modules */node_modules
.yarn/ .yarn/*
!.yarn/releases
!.yarn/plugins
!.yarn/sdks
!.yarn/versions
.pnp.*
/.pnp /.pnp
.pnp.js .pnp.js

View File

@ -5,5 +5,6 @@
"**/.pnp.*": true "**/.pnp.*": true
}, },
"typescript.tsdk": ".yarn/sdks/typescript/lib", "typescript.tsdk": ".yarn/sdks/typescript/lib",
"typescript.enablePromptUseWorkspaceTsdk": true "typescript.enablePromptUseWorkspaceTsdk": true,
"prettier.prettierPath": ".vscode/pnpify/prettier/index.js"
} }

File diff suppressed because one or more lines are too long

55
.yarn/releases/yarn-berry.cjs vendored Executable file

File diff suppressed because one or more lines are too long

5
.yarn/sdks/integrations.yml vendored Normal file
View File

@ -0,0 +1,5 @@
# This file is automatically generated by PnPify.
# Manual changes will be lost!
integrations:
- vscode

20
.yarn/sdks/typescript/bin/tsc vendored Executable file
View File

@ -0,0 +1,20 @@
#!/usr/bin/env node
const {existsSync} = require(`fs`);
const {createRequire, createRequireFromPath} = require(`module`);
const {resolve} = require(`path`);
const relPnpApiPath = "../../../../.pnp.js";
const absPnpApiPath = resolve(__dirname, relPnpApiPath);
const absRequire = (createRequire || createRequireFromPath)(absPnpApiPath);
if (existsSync(absPnpApiPath)) {
if (!process.versions.pnp) {
// Setup the environment to be able to require typescript/bin/tsc
require(absPnpApiPath).setup();
}
}
// Defer to the real typescript/bin/tsc your application uses
module.exports = absRequire(`typescript/bin/tsc`);

20
.yarn/sdks/typescript/bin/tsserver vendored Executable file
View File

@ -0,0 +1,20 @@
#!/usr/bin/env node
const {existsSync} = require(`fs`);
const {createRequire, createRequireFromPath} = require(`module`);
const {resolve} = require(`path`);
const relPnpApiPath = "../../../../.pnp.js";
const absPnpApiPath = resolve(__dirname, relPnpApiPath);
const absRequire = (createRequire || createRequireFromPath)(absPnpApiPath);
if (existsSync(absPnpApiPath)) {
if (!process.versions.pnp) {
// Setup the environment to be able to require typescript/bin/tsserver
require(absPnpApiPath).setup();
}
}
// Defer to the real typescript/bin/tsserver your application uses
module.exports = absRequire(`typescript/bin/tsserver`);

20
.yarn/sdks/typescript/lib/tsc.js vendored Normal file
View File

@ -0,0 +1,20 @@
#!/usr/bin/env node
const {existsSync} = require(`fs`);
const {createRequire, createRequireFromPath} = require(`module`);
const {resolve} = require(`path`);
const relPnpApiPath = "../../../../.pnp.js";
const absPnpApiPath = resolve(__dirname, relPnpApiPath);
const absRequire = (createRequire || createRequireFromPath)(absPnpApiPath);
if (existsSync(absPnpApiPath)) {
if (!process.versions.pnp) {
// Setup the environment to be able to require typescript/lib/tsc.js
require(absPnpApiPath).setup();
}
}
// Defer to the real typescript/lib/tsc.js your application uses
module.exports = absRequire(`typescript/lib/tsc.js`);

111
.yarn/sdks/typescript/lib/tsserver.js vendored Normal file
View File

@ -0,0 +1,111 @@
#!/usr/bin/env node
const {existsSync} = require(`fs`);
const {createRequire, createRequireFromPath} = require(`module`);
const {resolve} = require(`path`);
const relPnpApiPath = "../../../../.pnp.js";
const absPnpApiPath = resolve(__dirname, relPnpApiPath);
const absRequire = (createRequire || createRequireFromPath)(absPnpApiPath);
const moduleWrapper = tsserver => {
const {isAbsolute} = require(`path`);
const pnpApi = require(`pnpapi`);
const dependencyTreeRoots = new Set(pnpApi.getDependencyTreeRoots().map(locator => {
return `${locator.name}@${locator.reference}`;
}));
// VSCode sends the zip paths to TS using the "zip://" prefix, that TS
// doesn't understand. This layer makes sure to remove the protocol
// before forwarding it to TS, and to add it back on all returned paths.
function toEditorPath(str) {
// We add the `zip:` prefix to both `.zip/` paths and virtual paths
if (isAbsolute(str) && !str.match(/^\^zip:/) && (str.match(/\.zip\//) || str.match(/\$\$virtual\//))) {
// We also take the opportunity to turn virtual paths into physical ones;
// this makes is much easier to work with workspaces that list peer
// dependencies, since otherwise Ctrl+Click would bring us to the virtual
// file instances instead of the real ones.
//
// We only do this to modules owned by the the dependency tree roots.
// This avoids breaking the resolution when jumping inside a vendor
// with peer dep (otherwise jumping into react-dom would show resolution
// errors on react).
//
const resolved = pnpApi.resolveVirtual(str);
if (resolved) {
const locator = pnpApi.findPackageLocator(resolved);
if (locator && dependencyTreeRoots.has(`${locator.name}@${locator.reference}`)) {
str = resolved;
}
}
str = str.replace(/\\/g, `/`)
str = str.replace(/^\/?/, `/`);
// Absolute VSCode `Uri.fsPath`s need to start with a slash.
// VSCode only adds it automatically for supported schemes,
// so we have to do it manually for the `zip` scheme.
// The path needs to start with a caret otherwise VSCode doesn't handle the protocol
//
// Ref: https://github.com/microsoft/vscode/issues/105014#issuecomment-686760910
//
if (str.match(/\.zip\//)) {
str = `${isVSCode ? `^` : ``}zip:${str}`;
}
}
return str;
}
function fromEditorPath(str) {
return process.platform === `win32`
? str.replace(/^\^?zip:\//, ``)
: str.replace(/^\^?zip:/, ``);
}
// And here is the point where we hijack the VSCode <-> TS communications
// by adding ourselves in the middle. We locate everything that looks
// like an absolute path of ours and normalize it.
const Session = tsserver.server.Session;
const {onMessage: originalOnMessage, send: originalSend} = Session.prototype;
let isVSCode = false;
return Object.assign(Session.prototype, {
onMessage(/** @type {string} */ message) {
const parsedMessage = JSON.parse(message)
if (
parsedMessage != null &&
typeof parsedMessage === `object` &&
parsedMessage.arguments &&
parsedMessage.arguments.hostInfo === `vscode`
) {
isVSCode = true;
}
return originalOnMessage.call(this, JSON.stringify(parsedMessage, (key, value) => {
return typeof value === `string` ? fromEditorPath(value) : value;
}));
},
send(/** @type {any} */ msg) {
return originalSend.call(this, JSON.parse(JSON.stringify(msg, (key, value) => {
return typeof value === `string` ? toEditorPath(value) : value;
})));
}
});
};
if (existsSync(absPnpApiPath)) {
if (!process.versions.pnp) {
// Setup the environment to be able to require typescript/lib/tsserver.js
require(absPnpApiPath).setup();
}
}
// Defer to the real typescript/lib/tsserver.js your application uses
module.exports = moduleWrapper(absRequire(`typescript/lib/tsserver.js`));

20
.yarn/sdks/typescript/lib/typescript.js vendored Normal file
View File

@ -0,0 +1,20 @@
#!/usr/bin/env node
const {existsSync} = require(`fs`);
const {createRequire, createRequireFromPath} = require(`module`);
const {resolve} = require(`path`);
const relPnpApiPath = "../../../../.pnp.js";
const absPnpApiPath = resolve(__dirname, relPnpApiPath);
const absRequire = (createRequire || createRequireFromPath)(absPnpApiPath);
if (existsSync(absPnpApiPath)) {
if (!process.versions.pnp) {
// Setup the environment to be able to require typescript/lib/typescript.js
require(absPnpApiPath).setup();
}
}
// Defer to the real typescript/lib/typescript.js your application uses
module.exports = absRequire(`typescript/lib/typescript.js`);

6
.yarn/sdks/typescript/package.json vendored Normal file
View File

@ -0,0 +1,6 @@
{
"name": "typescript",
"version": "4.1.3-pnpify",
"main": "./lib/typescript.js",
"type": "commonjs"
}

View File

@ -32,8 +32,7 @@
"react-github-btn": "^1.2.0", "react-github-btn": "^1.2.0",
"react-intl": "^5.10.2", "react-intl": "^5.10.2",
"react-router-dom": "^5.2.0", "react-router-dom": "^5.2.0",
"react-scripts": "3.4.3", "react-scripts": "3.4.3"
"typescript": "^4.0.0"
}, },
"eslintConfig": { "eslintConfig": {
"extends": "react-app" "extends": "react-app"

View File

@ -1,8 +1,8 @@
import { AccountLayout, MintLayout, Token } from '@solana/spl-token'; import { AccountLayout, MintLayout, Token } from '@solana/spl-token';
import { Account, PublicKey, SystemProgram, TransactionInstruction } from '@solana/web3.js'; import { Account, PublicKey, SystemProgram, TransactionInstruction } from '@solana/web3.js';
import { LENDING_PROGRAM_ID, TOKEN_PROGRAM_ID, WRAPPED_SOL_MINT } from '../utils/ids'; import { TOKEN_PROGRAM_ID, WRAPPED_SOL_MINT } from '../utils/ids';
import { LendingObligationLayout, TokenAccount } from '../models'; import { TokenAccount } from '../models/account';
import { cache, TokenAccountParser } from './../contexts/accounts'; import { cache, TokenAccountParser } from '../contexts/accounts';
export function ensureSplAccount( export function ensureSplAccount(
instructions: TransactionInstruction[], instructions: TransactionInstruction[],
@ -51,28 +51,6 @@ export function createTempMemoryAccount(
return account.publicKey; return account.publicKey;
} }
export function createUninitializedObligation(
instructions: TransactionInstruction[],
payer: PublicKey,
amount: number,
signers: Account[]
) {
const account = new Account();
instructions.push(
SystemProgram.createAccount({
fromPubkey: payer,
newAccountPubkey: account.publicKey,
lamports: amount,
space: LendingObligationLayout.span,
programId: LENDING_PROGRAM_ID,
})
);
signers.push(account);
return account.publicKey;
}
export function createUninitializedMint( export function createUninitializedMint(
instructions: TransactionInstruction[], instructions: TransactionInstruction[],
payer: PublicKey, payer: PublicKey,

View File

@ -0,0 +1 @@
export * from './account';

View File

@ -1,7 +1,7 @@
import React from "react"; import React from 'react';
import { Typography } from "antd"; import { Typography } from 'antd';
import { shortenAddress } from "../../utils/utils"; import { shortenAddress } from '../../utils/utils';
import { PublicKey } from "@solana/web3.js"; import { PublicKey } from '@solana/web3.js';
export const ExplorerLink = (props: { export const ExplorerLink = (props: {
address: string | PublicKey; address: string | PublicKey;
@ -12,10 +12,7 @@ export const ExplorerLink = (props: {
}) => { }) => {
const { type, code } = props; const { type, code } = props;
const address = const address = typeof props.address === 'string' ? props.address : props.address?.toBase58();
typeof props.address === "string"
? props.address
: props.address?.toBase58();
if (!address) { if (!address) {
return null; return null;
@ -27,7 +24,7 @@ export const ExplorerLink = (props: {
<a <a
href={`https://explorer.solana.com/${type}/${address}`} href={`https://explorer.solana.com/${type}/${address}`}
// eslint-disable-next-line react/jsx-no-target-blank // eslint-disable-next-line react/jsx-no-target-blank
target="_blank" target='_blank'
title={address} title={address}
style={props.style} style={props.style}
> >

View File

@ -0,0 +1 @@
export * from './math';

View File

@ -1,23 +1,13 @@
import React, { import React, { useCallback, useContext, useEffect, useMemo, useState } from 'react';
useCallback, import { useConnection } from '../contexts/connection';
useContext, import { useWallet } from '../contexts/wallet';
useEffect, import { AccountInfo, Connection, PublicKey } from '@solana/web3.js';
useMemo, import { AccountLayout, u64, MintInfo, MintLayout } from '@solana/spl-token';
useState, import { TokenAccount } from '../models';
} from "react"; import { chunks } from '../utils/utils';
import { useConnection } from "./connection"; import { EventEmitter } from '../utils/eventEmitter';
import { useWallet } from "./wallet"; import { useUserAccounts } from '../hooks/useUserAccounts';
import { AccountInfo, Connection, PublicKey } from "@solana/web3.js"; import { WRAPPED_SOL_MINT, programIds, LEND_HOST_FEE_ADDRESS } from 'common/src/utils/ids';
import { AccountLayout, u64, MintInfo, MintLayout } from "@solana/spl-token";
import { PoolInfo, TokenAccount } from "./../models";
import { chunks } from "./../utils/utils";
import { EventEmitter } from "./../utils/eventEmitter";
import { useUserAccounts } from "../hooks/useUserAccounts";
import {
WRAPPED_SOL_MINT,
programIds,
LEND_HOST_FEE_ADDRESS,
} from "../utils/ids";
const AccountsContext = React.createContext<any>(null); const AccountsContext = React.createContext<any>(null);
@ -32,10 +22,7 @@ export interface ParsedAccountBase {
info: any; // TODO: change to unkown info: any; // TODO: change to unkown
} }
export type AccountParser = ( export type AccountParser = (pubkey: PublicKey, data: AccountInfo<Buffer>) => ParsedAccountBase | undefined;
pubkey: PublicKey,
data: AccountInfo<Buffer>
) => ParsedAccountBase | undefined;
export interface ParsedAccount<T> extends ParsedAccountBase { export interface ParsedAccount<T> extends ParsedAccountBase {
info: T; info: T;
@ -44,7 +31,7 @@ export interface ParsedAccount<T> extends ParsedAccountBase {
const getMintInfo = async (connection: Connection, pubKey: PublicKey) => { const getMintInfo = async (connection: Connection, pubKey: PublicKey) => {
const info = await connection.getAccountInfo(pubKey); const info = await connection.getAccountInfo(pubKey);
if (info === null) { if (info === null) {
throw new Error("Failed to find mint account"); throw new Error('Failed to find mint account');
} }
const data = Buffer.from(info.data); const data = Buffer.from(info.data);
@ -68,10 +55,7 @@ export const MintParser = (pubKey: PublicKey, info: AccountInfo<Buffer>) => {
return details; return details;
}; };
export const TokenAccountParser = ( export const TokenAccountParser = (pubKey: PublicKey, info: AccountInfo<Buffer>) => {
pubKey: PublicKey,
info: AccountInfo<Buffer>
) => {
const buffer = Buffer.from(info.data); const buffer = Buffer.from(info.data);
const data = deserializeAccount(buffer); const data = deserializeAccount(buffer);
@ -86,10 +70,7 @@ export const TokenAccountParser = (
return details; return details;
}; };
export const GenericAccountParser = ( export const GenericAccountParser = (pubKey: PublicKey, info: AccountInfo<Buffer>) => {
pubKey: PublicKey,
info: AccountInfo<Buffer>
) => {
const buffer = Buffer.from(info.data); const buffer = Buffer.from(info.data);
const details = { const details = {
@ -107,13 +88,9 @@ export const keyToAccountParser = new Map<string, AccountParser>();
export const cache = { export const cache = {
emitter: new EventEmitter(), emitter: new EventEmitter(),
query: async ( query: async (connection: Connection, pubKey: string | PublicKey, parser?: AccountParser) => {
connection: Connection,
pubKey: string | PublicKey,
parser?: AccountParser
) => {
let id: PublicKey; let id: PublicKey;
if (typeof pubKey === "string") { if (typeof pubKey === 'string') {
id = new PublicKey(pubKey); id = new PublicKey(pubKey);
} else { } else {
id = pubKey; id = pubKey;
@ -134,7 +111,7 @@ export const cache = {
// TODO: refactor to use multiple accounts query with flush like behavior // TODO: refactor to use multiple accounts query with flush like behavior
query = connection.getAccountInfo(id).then((data) => { query = connection.getAccountInfo(id).then((data) => {
if (!data) { if (!data) {
throw new Error("Account not found"); throw new Error('Account not found');
} }
return cache.add(id, data, parser); return cache.add(id, data, parser);
@ -143,21 +120,15 @@ export const cache = {
return query; return query;
}, },
add: ( add: (id: PublicKey | string, obj: AccountInfo<Buffer>, parser?: AccountParser) => {
id: PublicKey | string,
obj: AccountInfo<Buffer>,
parser?: AccountParser
) => {
if (obj.data.length === 0) { if (obj.data.length === 0) {
return; return;
} }
const address = typeof id === "string" ? id : id?.toBase58(); const address = typeof id === 'string' ? id : id?.toBase58();
const deserialize = parser ? parser : keyToAccountParser.get(address); const deserialize = parser ? parser : keyToAccountParser.get(address);
if (!deserialize) { if (!deserialize) {
throw new Error( throw new Error('Deserializer needs to be registered or passed as a parameter');
"Deserializer needs to be registered or passed as a parameter"
);
} }
cache.registerParser(id, deserialize); cache.registerParser(id, deserialize);
@ -175,7 +146,7 @@ export const cache = {
}, },
get: (pubKey: string | PublicKey) => { get: (pubKey: string | PublicKey) => {
let key: string; let key: string;
if (typeof pubKey !== "string") { if (typeof pubKey !== 'string') {
key = pubKey.toBase58(); key = pubKey.toBase58();
} else { } else {
key = pubKey; key = pubKey;
@ -185,7 +156,7 @@ export const cache = {
}, },
delete: (pubKey: string | PublicKey) => { delete: (pubKey: string | PublicKey) => {
let key: string; let key: string;
if (typeof pubKey !== "string") { if (typeof pubKey !== 'string') {
key = pubKey.toBase58(); key = pubKey.toBase58();
} else { } else {
key = pubKey; key = pubKey;
@ -211,7 +182,7 @@ export const cache = {
}, },
registerParser: (pubkey: PublicKey | string, parser: AccountParser) => { registerParser: (pubkey: PublicKey | string, parser: AccountParser) => {
if (pubkey) { if (pubkey) {
const address = typeof pubkey === "string" ? pubkey : pubkey?.toBase58(); const address = typeof pubkey === 'string' ? pubkey : pubkey?.toBase58();
keyToAccountParser.set(address, parser); keyToAccountParser.set(address, parser);
} }
@ -219,7 +190,7 @@ export const cache = {
}, },
queryMint: async (connection: Connection, pubKey: string | PublicKey) => { queryMint: async (connection: Connection, pubKey: string | PublicKey) => {
let id: PublicKey; let id: PublicKey;
if (typeof pubKey === "string") { if (typeof pubKey === 'string') {
id = new PublicKey(pubKey); id = new PublicKey(pubKey);
} else { } else {
id = pubKey; id = pubKey;
@ -248,7 +219,7 @@ export const cache = {
}, },
getMint: (pubKey: string | PublicKey) => { getMint: (pubKey: string | PublicKey) => {
let key: string; let key: string;
if (typeof pubKey !== "string") { if (typeof pubKey !== 'string') {
key = pubKey.toBase58(); key = pubKey.toBase58();
} else { } else {
key = pubKey; key = pubKey;
@ -270,10 +241,7 @@ export const useAccountsContext = () => {
return context; return context;
}; };
function wrapNativeAccount( function wrapNativeAccount(pubkey: PublicKey, account?: AccountInfo<Buffer>): TokenAccount | undefined {
pubkey: PublicKey,
account?: AccountInfo<Buffer>
): TokenAccount | undefined {
if (!account) { if (!account) {
return undefined; return undefined;
} }
@ -296,22 +264,7 @@ function wrapNativeAccount(
}; };
} }
export function useCachedPool(legacy = false) { export const getCachedAccount = (predicate: (account: TokenAccount) => boolean) => {
const context = useContext(AccountsContext);
const allPools = context.pools as PoolInfo[];
const pools = useMemo(() => {
return allPools.filter((p) => p.legacy === legacy);
}, [allPools, legacy]);
return {
pools,
};
}
export const getCachedAccount = (
predicate: (account: TokenAccount) => boolean
) => {
for (const account of genericCache.values()) { for (const account of genericCache.values()) {
if (predicate(account)) { if (predicate(account)) {
return account as TokenAccount; return account as TokenAccount;
@ -361,10 +314,7 @@ const UseNativeAccount = () => {
}; };
const PRECACHED_OWNERS = new Set<string>(); const PRECACHED_OWNERS = new Set<string>();
const precacheUserTokenAccounts = async ( const precacheUserTokenAccounts = async (connection: Connection, owner?: PublicKey) => {
connection: Connection,
owner?: PublicKey
) => {
if (!owner) { if (!owner) {
return; return;
} }
@ -392,16 +342,12 @@ export function AccountsProvider({ children = null as any }) {
return cache return cache
.byParser(TokenAccountParser) .byParser(TokenAccountParser)
.map((id) => cache.get(id)) .map((id) => cache.get(id))
.filter( .filter((a) => a && a.info.owner.toBase58() === wallet.publicKey?.toBase58())
(a) => a && a.info.owner.toBase58() === wallet.publicKey?.toBase58()
)
.map((a) => a as TokenAccount); .map((a) => a as TokenAccount);
}, [wallet]); }, [wallet]);
useEffect(() => { useEffect(() => {
const accounts = selectUserAccounts().filter( const accounts = selectUserAccounts().filter((a) => a !== undefined) as TokenAccount[];
(a) => a !== undefined
) as TokenAccount[];
setUserAccounts(accounts); setUserAccounts(accounts);
}, [nativeAccount, wallet, tokenAccounts, selectUserAccounts]); }, [nativeAccount, wallet, tokenAccounts, selectUserAccounts]);
@ -451,7 +397,7 @@ export function AccountsProvider({ children = null as any }) {
} }
} }
}, },
"singleGossip" 'singleGossip'
); );
return () => { return () => {
@ -479,15 +425,9 @@ export function useNativeAccount() {
}; };
} }
export const getMultipleAccounts = async ( export const getMultipleAccounts = async (connection: any, keys: string[], commitment: string) => {
connection: any,
keys: string[],
commitment: string
) => {
const result = await Promise.all( const result = await Promise.all(
chunks(keys, 99).map((chunk) => chunks(keys, 99).map((chunk) => getMultipleAccountsCore(connection, chunk, commitment))
getMultipleAccountsCore(connection, chunk, commitment)
)
); );
const array = result const array = result
@ -502,7 +442,7 @@ export const getMultipleAccounts = async (
const { data, ...rest } = acc; const { data, ...rest } = acc;
const obj = { const obj = {
...rest, ...rest,
data: Buffer.from(data[0], "base64"), data: Buffer.from(data[0], 'base64'),
} as AccountInfo<Buffer>; } as AccountInfo<Buffer>;
return obj; return obj;
}) })
@ -512,18 +452,12 @@ export const getMultipleAccounts = async (
return { keys, array }; return { keys, array };
}; };
const getMultipleAccountsCore = async ( const getMultipleAccountsCore = async (connection: any, keys: string[], commitment: string) => {
connection: any, const args = connection._buildArgs([keys], commitment, 'base64');
keys: string[],
commitment: string
) => {
const args = connection._buildArgs([keys], commitment, "base64");
const unsafeRes = await connection._rpcRequest("getMultipleAccounts", args); const unsafeRes = await connection._rpcRequest('getMultipleAccounts', args);
if (unsafeRes.error) { if (unsafeRes.error) {
throw new Error( throw new Error('failed to get info about account ' + unsafeRes.error.message);
"failed to get info about account " + unsafeRes.error.message
);
} }
if (unsafeRes.result.value) { if (unsafeRes.result.value) {
@ -539,7 +473,7 @@ export function useMint(key?: string | PublicKey) {
const connection = useConnection(); const connection = useConnection();
const [mint, setMint] = useState<MintInfo>(); const [mint, setMint] = useState<MintInfo>();
const id = typeof key === "string" ? key : key?.toBase58(); const id = typeof key === 'string' ? key : key?.toBase58();
useEffect(() => { useEffect(() => {
if (!id) { if (!id) {
@ -554,9 +488,7 @@ export function useMint(key?: string | PublicKey) {
const dispose = cache.emitter.onCache((e) => { const dispose = cache.emitter.onCache((e) => {
const event = e; const event = e;
if (event.id === id) { if (event.id === id) {
cache cache.query(connection, id, MintParser).then((mint) => setMint(mint.info as any));
.query(connection, id, MintParser)
.then((mint) => setMint(mint.info as any));
} }
}); });
return () => { return () => {
@ -569,9 +501,7 @@ export function useMint(key?: string | PublicKey) {
export const useAccountByMint = (mint: string) => { export const useAccountByMint = (mint: string) => {
const { userAccounts } = useUserAccounts(); const { userAccounts } = useUserAccounts();
const index = userAccounts.findIndex( const index = userAccounts.findIndex((acc) => acc.info.mint.toBase58() === mint);
(acc) => acc.info.mint.toBase58() === mint
);
if (index !== -1) { if (index !== -1) {
return userAccounts[index]; return userAccounts[index];
@ -592,9 +522,7 @@ export function useAccount(pubKey?: PublicKey) {
return; return;
} }
const acc = await cache const acc = await cache.query(connection, key, TokenAccountParser).catch((err) => console.log(err));
.query(connection, key, TokenAccountParser)
.catch((err) => console.log(err));
if (acc) { if (acc) {
setAccount(acc); setAccount(acc);
} }
@ -657,7 +585,7 @@ const deserializeAccount = (data: Buffer) => {
// TODO: expose in spl package // TODO: expose in spl package
const deserializeMint = (data: Buffer) => { const deserializeMint = (data: Buffer) => {
if (data.length !== MintLayout.span) { if (data.length !== MintLayout.span) {
throw new Error("Not a valid Mint"); throw new Error('Not a valid Mint');
} }
const mintInfo = MintLayout.decode(data); const mintInfo = MintLayout.decode(data);

View File

@ -1,40 +1,29 @@
import { KnownToken, useLocalStorageState } from "./../utils/utils"; import { KnownToken, useLocalStorageState } from '../utils/utils';
import { import { Account, clusterApiUrl, Connection, Transaction, TransactionInstruction } from '@solana/web3.js';
Account, import React, { useContext, useEffect, useMemo, useState } from 'react';
clusterApiUrl, import { notify } from '../utils/notifications';
Connection, import { ExplorerLink } from '../components/ExplorerLink';
Transaction, import LocalTokens from '../config/tokens.json';
TransactionInstruction, import { setProgramIds } from 'common/src/utils/ids';
} from "@solana/web3.js";
import React, { useContext, useEffect, useMemo, useState } from "react";
import { notify } from "./../utils/notifications";
import { ExplorerLink } from "../components/ExplorerLink";
import LocalTokens from "../config/tokens.json";
import { setProgramIds } from "../utils/ids";
export type ENV = export type ENV = 'mainnet-beta' | 'testnet' | 'devnet' | 'localnet' | 'lending';
| "mainnet-beta"
| "testnet"
| "devnet"
| "localnet"
| "lending";
export const ENDPOINTS = [ export const ENDPOINTS = [
{ {
name: "mainnet-beta" as ENV, name: 'mainnet-beta' as ENV,
endpoint: "https://solana-api.projectserum.com/", endpoint: 'https://solana-api.projectserum.com/',
}, },
{ {
name: "Oyster Dev" as ENV, name: 'Oyster Dev' as ENV,
endpoint: "http://oyster-dev.solana.com/", endpoint: 'http://oyster-dev.solana.com/',
}, },
{ {
name: "Lending" as ENV, name: 'Lending' as ENV,
endpoint: "https://tln.solana.com/", endpoint: 'https://tln.solana.com/',
}, },
{ name: "testnet" as ENV, endpoint: clusterApiUrl("testnet") }, { name: 'testnet' as ENV, endpoint: clusterApiUrl('testnet') },
{ name: "devnet" as ENV, endpoint: clusterApiUrl("devnet") }, { name: 'devnet' as ENV, endpoint: clusterApiUrl('devnet') },
{ name: "localnet" as ENV, endpoint: "http://127.0.0.1:8899" }, { name: 'localnet' as ENV, endpoint: 'http://127.0.0.1:8899' },
]; ];
const DEFAULT = ENDPOINTS[0].endpoint; const DEFAULT = ENDPOINTS[0].endpoint;
@ -57,43 +46,29 @@ const ConnectionContext = React.createContext<ConnectionConfig>({
setEndpoint: () => {}, setEndpoint: () => {},
slippage: DEFAULT_SLIPPAGE, slippage: DEFAULT_SLIPPAGE,
setSlippage: (val: number) => {}, setSlippage: (val: number) => {},
connection: new Connection(DEFAULT, "recent"), connection: new Connection(DEFAULT, 'recent'),
sendConnection: new Connection(DEFAULT, "recent"), sendConnection: new Connection(DEFAULT, 'recent'),
env: ENDPOINTS[0].name, env: ENDPOINTS[0].name,
tokens: [], tokens: [],
tokenMap: new Map<string, KnownToken>(), tokenMap: new Map<string, KnownToken>(),
}); });
export function ConnectionProvider({ children = undefined as any }) { export function ConnectionProvider({ children = undefined as any }) {
const [endpoint, setEndpoint] = useLocalStorageState( const [endpoint, setEndpoint] = useLocalStorageState('connectionEndpts', ENDPOINTS[0].endpoint);
"connectionEndpts",
ENDPOINTS[0].endpoint
);
const [slippage, setSlippage] = useLocalStorageState( const [slippage, setSlippage] = useLocalStorageState('slippage', DEFAULT_SLIPPAGE.toString());
"slippage",
DEFAULT_SLIPPAGE.toString()
);
const connection = useMemo(() => new Connection(endpoint, "recent"), [ const connection = useMemo(() => new Connection(endpoint, 'recent'), [endpoint]);
endpoint, const sendConnection = useMemo(() => new Connection(endpoint, 'recent'), [endpoint]);
]);
const sendConnection = useMemo(() => new Connection(endpoint, "recent"), [
endpoint,
]);
const env = const env = ENDPOINTS.find((end) => end.endpoint === endpoint)?.name || ENDPOINTS[0].name;
ENDPOINTS.find((end) => end.endpoint === endpoint)?.name ||
ENDPOINTS[0].name;
const [tokens, setTokens] = useState<KnownToken[]>([]); const [tokens, setTokens] = useState<KnownToken[]>([]);
const [tokenMap, setTokenMap] = useState<Map<string, KnownToken>>(new Map()); const [tokenMap, setTokenMap] = useState<Map<string, KnownToken>>(new Map());
useEffect(() => { useEffect(() => {
// fetch token files // fetch token files
window window
.fetch( .fetch(`https://raw.githubusercontent.com/solana-labs/token-list/main/src/tokens/${env}.json`)
`https://raw.githubusercontent.com/solana-labs/token-list/main/src/tokens/${env}.json`
)
.then((res) => { .then((res) => {
return res.json(); return res.json();
}) })
@ -129,10 +104,7 @@ export function ConnectionProvider({ children = undefined as any }) {
}, [connection]); }, [connection]);
useEffect(() => { useEffect(() => {
const id = sendConnection.onAccountChange( const id = sendConnection.onAccountChange(new Account().publicKey, () => {});
new Account().publicKey,
() => {}
);
return () => { return () => {
sendConnection.removeAccountChangeListener(id); sendConnection.removeAccountChangeListener(id);
}; };
@ -190,7 +162,7 @@ export function useSlippageConfig() {
const getErrorForTransaction = async (connection: Connection, txid: string) => { const getErrorForTransaction = async (connection: Connection, txid: string) => {
// wait for all confirmation before geting transaction // wait for all confirmation before geting transaction
await connection.confirmTransaction(txid, "max"); await connection.confirmTransaction(txid, 'max');
const tx = await connection.getParsedConfirmedTransaction(txid); const tx = await connection.getParsedConfirmedTransaction(txid);
@ -224,9 +196,7 @@ export const sendTransaction = async (
) => { ) => {
let transaction = new Transaction(); let transaction = new Transaction();
instructions.forEach((instruction) => transaction.add(instruction)); instructions.forEach((instruction) => transaction.add(instruction));
transaction.recentBlockhash = ( transaction.recentBlockhash = (await connection.getRecentBlockhash('max')).blockhash;
await connection.getRecentBlockhash("max")
).blockhash;
transaction.setSigners( transaction.setSigners(
// fee payied by the wallet owner // fee payied by the wallet owner
wallet.publicKey, wallet.publicKey,
@ -239,37 +209,30 @@ export const sendTransaction = async (
const rawTransaction = transaction.serialize(); const rawTransaction = transaction.serialize();
let options = { let options = {
skipPreflight: true, skipPreflight: true,
commitment: "singleGossip", commitment: 'singleGossip',
}; };
const txid = await connection.sendRawTransaction(rawTransaction, options); const txid = await connection.sendRawTransaction(rawTransaction, options);
if (awaitConfirmation) { if (awaitConfirmation) {
const status = ( const status = (await connection.confirmTransaction(txid, options && (options.commitment as any))).value;
await connection.confirmTransaction(
txid,
options && (options.commitment as any)
)
).value;
if (status?.err) { if (status?.err) {
const errors = await getErrorForTransaction(connection, txid); const errors = await getErrorForTransaction(connection, txid);
notify({ notify({
message: "Transaction failed...", message: 'Transaction failed...',
description: ( description: (
<> <>
{errors.map((err) => ( {errors.map((err) => (
<div>{err}</div> <div>{err}</div>
))} ))}
<ExplorerLink address={txid} type="transaction" /> <ExplorerLink address={txid} type='transaction' />
</> </>
), ),
type: "error", type: 'error',
}); });
throw new Error( throw new Error(`Raw transaction ${txid} failed (${JSON.stringify(status)})`);
`Raw transaction ${txid} failed (${JSON.stringify(status)})`
);
} }
} }

View File

@ -1,27 +1,24 @@
import React, { useContext, useEffect, useMemo, useState } from "react"; import React, { useContext, useEffect, useMemo, useState } from 'react';
import Wallet from "@project-serum/sol-wallet-adapter"; import Wallet from '@project-serum/sol-wallet-adapter';
import { notify } from "./../utils/notifications"; import { notify } from '../utils/notifications';
import { useConnectionConfig } from "./connection"; import { useConnectionConfig } from 'common/src/contexts/connection';
import { useLocalStorageState } from "./../utils/utils"; import { useLocalStorageState } from '../utils/utils';
import { SolongAdapter } from "./../wallet-adapters/solong_adapter"; import { SolongAdapter } from '../wallet-adapters/solong_adapter';
export const WALLET_PROVIDERS = [ export const WALLET_PROVIDERS = [
{ name: "sollet.io", url: "https://www.sollet.io" }, { name: 'sollet.io', url: 'https://www.sollet.io' },
{ name: "solongwallet.com", url: "http://solongwallet.com" }, { name: 'solongwallet.com', url: 'http://solongwallet.com' },
{ name: "solflare.com", url: "https://solflare.com/access-wallet" }, { name: 'solflare.com', url: 'https://solflare.com/access-wallet' },
{ name: "mathwallet.org", url: "https://www.mathwallet.org" }, { name: 'mathwallet.org', url: 'https://www.mathwallet.org' },
]; ];
const WalletContext = React.createContext<any>(null); const WalletContext = React.createContext<any>(null);
export function WalletProvider({ children = null as any }) { export function WalletProvider({ children = null as any }) {
const { endpoint } = useConnectionConfig(); const { endpoint } = useConnectionConfig();
const [providerUrl, setProviderUrl] = useLocalStorageState( const [providerUrl, setProviderUrl] = useLocalStorageState('walletProvider', 'https://www.sollet.io');
"walletProvider",
"https://www.sollet.io"
);
const wallet = useMemo(() => { const wallet = useMemo(() => {
if (providerUrl === "http://solongwallet.com") { if (providerUrl === 'http://solongwallet.com') {
return new SolongAdapter(providerUrl, endpoint); return new SolongAdapter(providerUrl, endpoint);
} else { } else {
return new Wallet(providerUrl, endpoint); return new Wallet(providerUrl, endpoint);
@ -30,7 +27,7 @@ export function WalletProvider({ children = null as any }) {
const [connected, setConnected] = useState(false); const [connected, setConnected] = useState(false);
useEffect(() => { useEffect(() => {
wallet.on("connect", () => { wallet.on('connect', () => {
setConnected(true); setConnected(true);
let walletPublicKey = wallet.publicKey.toBase58(); let walletPublicKey = wallet.publicKey.toBase58();
let keyToDisplay = let keyToDisplay =
@ -42,15 +39,15 @@ export function WalletProvider({ children = null as any }) {
: walletPublicKey; : walletPublicKey;
notify({ notify({
message: "Wallet update", message: 'Wallet update',
description: "Connected to wallet " + keyToDisplay, description: 'Connected to wallet ' + keyToDisplay,
}); });
}); });
wallet.on("disconnect", () => { wallet.on('disconnect', () => {
setConnected(false); setConnected(false);
notify({ notify({
message: "Wallet update", message: 'Wallet update',
description: "Disconnected from wallet", description: 'Disconnected from wallet',
}); });
}); });
return () => { return () => {
@ -65,9 +62,7 @@ export function WalletProvider({ children = null as any }) {
connected, connected,
providerUrl, providerUrl,
setProviderUrl, setProviderUrl,
providerName: providerName: WALLET_PROVIDERS.find(({ url }) => url === providerUrl)?.name ?? providerUrl,
WALLET_PROVIDERS.find(({ url }) => url === providerUrl)?.name ??
providerUrl,
}} }}
> >
{children} {children}

View File

@ -0,0 +1,3 @@
export * from './useUserAccounts';
export * from './useAccountByMint';
export * from './useTokenName';

View File

@ -0,0 +1,15 @@
import { PublicKey } from '@solana/web3.js';
import { useUserAccounts } from '../hooks/useUserAccounts';
export const useAccountByMint = (mint?: string | PublicKey) => {
const { userAccounts } = useUserAccounts();
const mintAddress = typeof mint === 'string' ? mint : mint?.toBase58();
const index = userAccounts.findIndex((acc) => acc.info.mint.toBase58() === mintAddress);
if (index !== -1) {
return userAccounts[index];
}
return;
};

View File

@ -0,0 +1,9 @@
import { PublicKey } from '@solana/web3.js';
import { useConnectionConfig } from 'common/src/contexts/connection';
import { getTokenName } from '../utils/utils';
export function useTokenName(mintAddress?: string | PublicKey) {
const { tokenMap } = useConnectionConfig();
const address = typeof mintAddress === 'string' ? mintAddress : mintAddress?.toBase58();
return getTokenName(tokenMap, address);
}

View File

@ -1,5 +1,5 @@
import { TokenAccount } from "../models"; import { TokenAccount } from '../models';
import { useAccountsContext } from "./../contexts/accounts"; import { useAccountsContext } from 'common/src/contexts/accounts';
export function useUserAccounts() { export function useUserAccounts() {
const context = useAccountsContext(); const context = useAccountsContext();

View File

@ -0,0 +1,2 @@
export * from './account';
export * from './tokenSwap';

View File

@ -0,0 +1,68 @@
import * as BufferLayout from 'buffer-layout';
import { publicKey, uint64 } from '../utils/layout';
export { TokenSwap } from '@solana/spl-token-swap';
const FEE_LAYOUT = BufferLayout.struct(
[
BufferLayout.nu64('tradeFeeNumerator'),
BufferLayout.nu64('tradeFeeDenominator'),
BufferLayout.nu64('ownerTradeFeeNumerator'),
BufferLayout.nu64('ownerTradeFeeDenominator'),
BufferLayout.nu64('ownerWithdrawFeeNumerator'),
BufferLayout.nu64('ownerWithdrawFeeDenominator'),
BufferLayout.nu64('hostFeeNumerator'),
BufferLayout.nu64('hostFeeDenominator'),
],
'fees'
);
export const TokenSwapLayoutLegacyV0 = BufferLayout.struct([
BufferLayout.u8('isInitialized'),
BufferLayout.u8('nonce'),
publicKey('tokenAccountA'),
publicKey('tokenAccountB'),
publicKey('tokenPool'),
uint64('feesNumerator'),
uint64('feesDenominator'),
]);
export const TokenSwapLayoutV1: typeof BufferLayout.Structure = BufferLayout.struct([
BufferLayout.u8('isInitialized'),
BufferLayout.u8('nonce'),
publicKey('tokenProgramId'),
publicKey('tokenAccountA'),
publicKey('tokenAccountB'),
publicKey('tokenPool'),
publicKey('mintA'),
publicKey('mintB'),
publicKey('feeAccount'),
BufferLayout.u8('curveType'),
uint64('tradeFeeNumerator'),
uint64('tradeFeeDenominator'),
uint64('ownerTradeFeeNumerator'),
uint64('ownerTradeFeeDenominator'),
uint64('ownerWithdrawFeeNumerator'),
uint64('ownerWithdrawFeeDenominator'),
BufferLayout.blob(16, 'padding'),
]);
const CURVE_NODE = BufferLayout.union(BufferLayout.u8(), BufferLayout.blob(32), 'curve');
CURVE_NODE.addVariant(0, BufferLayout.struct([]), 'constantProduct');
CURVE_NODE.addVariant(1, BufferLayout.struct([BufferLayout.nu64('token_b_price')]), 'constantPrice');
CURVE_NODE.addVariant(2, BufferLayout.struct([]), 'stable');
CURVE_NODE.addVariant(3, BufferLayout.struct([BufferLayout.nu64('token_b_offset')]), 'offset');
export const TokenSwapLayout: typeof BufferLayout.Structure = BufferLayout.struct([
BufferLayout.u8('isInitialized'),
BufferLayout.u8('nonce'),
publicKey('tokenProgramId'),
publicKey('tokenAccountA'),
publicKey('tokenAccountB'),
publicKey('tokenPool'),
publicKey('mintA'),
publicKey('mintB'),
publicKey('feeAccount'),
FEE_LAYOUT,
CURVE_NODE,
]);

View File

@ -1,16 +1,10 @@
import { PublicKey } from "@solana/web3.js"; import { PublicKey } from '@solana/web3.js';
import { TokenSwapLayout, TokenSwapLayoutV1 } from "../models"; import { TokenSwapLayout, TokenSwapLayoutV1 } from '../models/tokenSwap';
export const WRAPPED_SOL_MINT = new PublicKey( export const WRAPPED_SOL_MINT = new PublicKey('So11111111111111111111111111111111111111112');
"So11111111111111111111111111111111111111112" export let TOKEN_PROGRAM_ID = new PublicKey('TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA');
);
export let TOKEN_PROGRAM_ID = new PublicKey(
"TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA"
);
export let LENDING_PROGRAM_ID = new PublicKey( export let LENDING_PROGRAM_ID = new PublicKey('TokenLending1111111111111111111111111111111');
"TokenLending1111111111111111111111111111111"
);
let SWAP_PROGRAM_ID: PublicKey; let SWAP_PROGRAM_ID: PublicKey;
let SWAP_PROGRAM_LEGACY_IDS: PublicKey[]; let SWAP_PROGRAM_LEGACY_IDS: PublicKey[];
@ -27,10 +21,10 @@ export const ENABLE_FEES_INPUT = false;
// legacy pools are used to show users contributions in those pools to allow for withdrawals of funds // legacy pools are used to show users contributions in those pools to allow for withdrawals of funds
export const PROGRAM_IDS = [ export const PROGRAM_IDS = [
{ {
name: "mainnet-beta", name: 'mainnet-beta',
swap: () => ({ swap: () => ({
current: { current: {
pubkey: new PublicKey("9qvG1zUp8xF1Bi4m6UdRNby1BAAuaDrUxSpv4CmRRMjL"), pubkey: new PublicKey('9qvG1zUp8xF1Bi4m6UdRNby1BAAuaDrUxSpv4CmRRMjL'),
layout: TokenSwapLayoutV1, layout: TokenSwapLayoutV1,
}, },
legacy: [ legacy: [
@ -40,30 +34,30 @@ export const PROGRAM_IDS = [
}), }),
}, },
{ {
name: "testnet", name: 'testnet',
swap: () => ({ swap: () => ({
current: { current: {
pubkey: new PublicKey("2n2dsFSgmPcZ8jkmBZLGUM2nzuFqcBGQ3JEEj6RJJcEg"), pubkey: new PublicKey('2n2dsFSgmPcZ8jkmBZLGUM2nzuFqcBGQ3JEEj6RJJcEg'),
layout: TokenSwapLayoutV1, layout: TokenSwapLayoutV1,
}, },
legacy: [], legacy: [],
}), }),
}, },
{ {
name: "devnet", name: 'devnet',
swap: () => ({ swap: () => ({
current: { current: {
pubkey: new PublicKey("6Cust2JhvweKLh4CVo1dt21s2PJ86uNGkziudpkNPaCj"), pubkey: new PublicKey('6Cust2JhvweKLh4CVo1dt21s2PJ86uNGkziudpkNPaCj'),
layout: TokenSwapLayout, layout: TokenSwapLayout,
}, },
legacy: [new PublicKey("BSfTAcBdqmvX5iE2PW88WFNNp2DHhLUaBKk5WrnxVkcJ")], legacy: [new PublicKey('BSfTAcBdqmvX5iE2PW88WFNNp2DHhLUaBKk5WrnxVkcJ')],
}), }),
}, },
{ {
name: "localnet", name: 'localnet',
swap: () => ({ swap: () => ({
current: { current: {
pubkey: new PublicKey("369YmCWHGxznT7GGBhcLZDRcRoGWmGKFWdmtiPy78yj7"), pubkey: new PublicKey('369YmCWHGxznT7GGBhcLZDRcRoGWmGKFWdmtiPy78yj7'),
layout: TokenSwapLayoutV1, layout: TokenSwapLayoutV1,
}, },
legacy: [], legacy: [],
@ -83,10 +77,8 @@ export const setProgramIds = (envName: string) => {
SWAP_PROGRAM_LAYOUT = swap.current.layout; SWAP_PROGRAM_LAYOUT = swap.current.layout;
SWAP_PROGRAM_LEGACY_IDS = swap.legacy; SWAP_PROGRAM_LEGACY_IDS = swap.legacy;
if (envName === "mainnet-beta") { if (envName === 'mainnet-beta') {
LENDING_PROGRAM_ID = new PublicKey( LENDING_PROGRAM_ID = new PublicKey('LendZqTs7gn5CTSJU1jWKhKuVpjJGom45nnwPb2AMTi');
"LendZqTs7gn5CTSJU1jWKhKuVpjJGom45nnwPb2AMTi"
);
} }
}; };

View File

@ -1,11 +1,11 @@
import { PublicKey } from "@solana/web3.js"; import { PublicKey } from '@solana/web3.js';
import BN from "bn.js"; import BN from 'bn.js';
import * as BufferLayout from "buffer-layout"; import * as BufferLayout from 'buffer-layout';
/** /**
* Layout for a public key * Layout for a public key
*/ */
export const publicKey = (property = "publicKey"): unknown => { export const publicKey = (property = 'publicKey'): unknown => {
const publicKeyLayout = BufferLayout.blob(32, property); const publicKeyLayout = BufferLayout.blob(32, property);
const _decode = publicKeyLayout.decode.bind(publicKeyLayout); const _decode = publicKeyLayout.decode.bind(publicKeyLayout);
@ -26,7 +26,7 @@ export const publicKey = (property = "publicKey"): unknown => {
/** /**
* Layout for a 64bit unsigned value * Layout for a 64bit unsigned value
*/ */
export const uint64 = (property = "uint64"): unknown => { export const uint64 = (property = 'uint64'): unknown => {
const layout = BufferLayout.blob(8, property); const layout = BufferLayout.blob(8, property);
const _decode = layout.decode.bind(layout); const _decode = layout.decode.bind(layout);
@ -38,7 +38,7 @@ export const uint64 = (property = "uint64"): unknown => {
[...data] [...data]
.reverse() .reverse()
.map((i) => `00${i.toString(16)}`.slice(-2)) .map((i) => `00${i.toString(16)}`.slice(-2))
.join(""), .join(''),
16 16
); );
}; };
@ -58,7 +58,7 @@ export const uint64 = (property = "uint64"): unknown => {
}; };
// TODO: wrap in BN (what about decimals?) // TODO: wrap in BN (what about decimals?)
export const uint128 = (property = "uint128"): unknown => { export const uint128 = (property = 'uint128'): unknown => {
const layout = BufferLayout.blob(16, property); const layout = BufferLayout.blob(16, property);
const _decode = layout.decode.bind(layout); const _decode = layout.decode.bind(layout);
@ -70,7 +70,7 @@ export const uint128 = (property = "uint128"): unknown => {
[...data] [...data]
.reverse() .reverse()
.map((i) => `00${i.toString(16)}`.slice(-2)) .map((i) => `00${i.toString(16)}`.slice(-2))
.join(""), .join(''),
16 16
); );
}; };
@ -93,12 +93,12 @@ export const uint128 = (property = "uint128"): unknown => {
/** /**
* Layout for a Rust String type * Layout for a Rust String type
*/ */
export const rustString = (property = "string"): unknown => { export const rustString = (property = 'string'): unknown => {
const rsl = BufferLayout.struct( const rsl = BufferLayout.struct(
[ [
BufferLayout.u32("length"), BufferLayout.u32('length'),
BufferLayout.u32("lengthPadding"), BufferLayout.u32('lengthPadding'),
BufferLayout.blob(BufferLayout.offset(BufferLayout.u32(), -8), "chars"), BufferLayout.blob(BufferLayout.offset(BufferLayout.u32(), -8), 'chars'),
], ],
property property
); );
@ -107,12 +107,12 @@ export const rustString = (property = "string"): unknown => {
rsl.decode = (buffer: Buffer, offset: number) => { rsl.decode = (buffer: Buffer, offset: number) => {
const data = _decode(buffer, offset); const data = _decode(buffer, offset);
return data.chars.toString("utf8"); return data.chars.toString('utf8');
}; };
rsl.encode = (str: string, buffer: Buffer, offset: number) => { rsl.encode = (str: string, buffer: Buffer, offset: number) => {
const data = { const data = {
chars: Buffer.from(str, "utf8"), chars: Buffer.from(str, 'utf8'),
}; };
return _encode(data, buffer, offset); return _encode(data, buffer, offset);
}; };

204
common/src/utils/utils.ts Normal file
View File

@ -0,0 +1,204 @@
import { useCallback, useState } from 'react';
import { MintInfo } from '@solana/spl-token';
import { TokenAccount } from './../models';
import { PublicKey } from '@solana/web3.js';
import BN from 'bn.js';
import { WAD, ZERO } from '../constants';
export interface KnownToken {
tokenSymbol: string;
tokenName: string;
icon: string;
mintAddress: string;
}
export type KnownTokenMap = Map<string, KnownToken>;
export const formatPriceNumber = new Intl.NumberFormat('en-US', {
style: 'decimal',
minimumFractionDigits: 2,
maximumFractionDigits: 8,
});
export function useLocalStorageState(key: string, defaultState?: string) {
const [state, setState] = useState(() => {
// NOTE: Not sure if this is ok
const storedState = localStorage.getItem(key);
if (storedState) {
return JSON.parse(storedState);
}
return defaultState;
});
const setLocalStorageState = useCallback(
(newState) => {
const changed = state !== newState;
if (!changed) {
return;
}
setState(newState);
if (newState === null) {
localStorage.removeItem(key);
} else {
localStorage.setItem(key, JSON.stringify(newState));
}
},
[state, key]
);
return [state, setLocalStorageState];
}
// shorten the checksummed version of the input address to have 4 characters at start and end
export function shortenAddress(address: string, chars = 4): string {
return `${address.slice(0, chars)}...${address.slice(-chars)}`;
}
export function getTokenName(map: KnownTokenMap, mint?: string | PublicKey, shorten = true): string {
const mintAddress = typeof mint === 'string' ? mint : mint?.toBase58();
if (!mintAddress) {
return 'N/A';
}
const knownSymbol = map.get(mintAddress)?.tokenSymbol;
if (knownSymbol) {
return knownSymbol;
}
return shorten ? `${mintAddress.substring(0, 5)}...` : mintAddress;
}
export function getTokenByName(tokenMap: KnownTokenMap, name: string) {
let token: KnownToken | null = null;
for (const val of tokenMap.values()) {
if (val.tokenSymbol === name) {
token = val;
break;
}
}
return token;
}
export function getTokenIcon(map: KnownTokenMap, mintAddress?: string | PublicKey): string | undefined {
const address = typeof mintAddress === 'string' ? mintAddress : mintAddress?.toBase58();
if (!address) {
return;
}
return map.get(address)?.icon;
}
export function isKnownMint(map: KnownTokenMap, mintAddress: string) {
return !!map.get(mintAddress);
}
export const STABLE_COINS = new Set(['USDC', 'wUSDC', 'USDT']);
export function chunks<T>(array: T[], size: number): T[][] {
return Array.apply<number, T[], T[][]>(0, new Array(Math.ceil(array.length / size))).map((_, index) =>
array.slice(index * size, (index + 1) * size)
);
}
export function toLamports(account?: TokenAccount | number, mint?: MintInfo): number {
if (!account) {
return 0;
}
const amount = typeof account === 'number' ? account : account.info.amount?.toNumber();
const precision = Math.pow(10, mint?.decimals || 0);
return Math.floor(amount * precision);
}
export function wadToLamports(amount?: BN): BN {
return amount?.div(WAD) || ZERO;
}
export function fromLamports(account?: TokenAccount | number | BN, mint?: MintInfo, rate: number = 1.0): number {
if (!account) {
return 0;
}
const amount = Math.floor(
typeof account === 'number' ? account : BN.isBN(account) ? account.toNumber() : account.info.amount.toNumber()
);
const precision = Math.pow(10, mint?.decimals || 0);
return (amount / precision) * rate;
}
var SI_SYMBOL = ['', 'k', 'M', 'G', 'T', 'P', 'E'];
const abbreviateNumber = (number: number, precision: number) => {
let tier = (Math.log10(number) / 3) | 0;
let scaled = number;
let suffix = SI_SYMBOL[tier];
if (tier !== 0) {
let scale = Math.pow(10, tier * 3);
scaled = number / scale;
}
return scaled.toFixed(precision) + suffix;
};
export const formatAmount = (val: number, precision: number = 6, abbr: boolean = true) =>
abbr ? abbreviateNumber(val, precision) : val.toFixed(precision);
export function formatTokenAmount(
account?: TokenAccount,
mint?: MintInfo,
rate: number = 1.0,
prefix = '',
suffix = '',
precision = 6,
abbr = false
): string {
if (!account) {
return '';
}
return `${[prefix]}${formatAmount(fromLamports(account, mint, rate), precision, abbr)}${suffix}`;
}
export const formatUSD = new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD',
});
const numberFormater = new Intl.NumberFormat('en-US', {
style: 'decimal',
minimumFractionDigits: 2,
maximumFractionDigits: 2,
});
export const formatNumber = {
format: (val?: number) => {
if (!val) {
return '--';
}
return numberFormater.format(val);
},
};
export const formatPct = new Intl.NumberFormat('en-US', {
style: 'percent',
minimumFractionDigits: 2,
maximumFractionDigits: 2,
});
export function convert(account?: TokenAccount | number, mint?: MintInfo, rate: number = 1.0): number {
if (!account) {
return 0;
}
const amount = typeof account === 'number' ? account : account.info.amount?.toNumber();
const precision = Math.pow(10, mint?.decimals || 0);
let result = (amount / precision) * rate;
return result;
}

5
common/tsconfig.json Normal file
View File

@ -0,0 +1,5 @@
{
"extends": "../tsconfig.json",
"compilerOptions": {},
"include": ["src"]
}

Binary file not shown.

View File

@ -2,6 +2,7 @@
"name": "lending", "name": "lending",
"version": "0.1.0", "version": "0.1.0",
"dependencies": { "dependencies": {
"@ant-design/icons": "^4.4.0",
"@ant-design/pro-layout": "^6.7.0", "@ant-design/pro-layout": "^6.7.0",
"@craco/craco": "^5.7.0", "@craco/craco": "^5.7.0",
"@project-serum/serum": "^0.13.11", "@project-serum/serum": "^0.13.11",
@ -21,6 +22,7 @@
"bs58": "^4.0.1", "bs58": "^4.0.1",
"buffer-layout": "^1.2.0", "buffer-layout": "^1.2.0",
"chart.js": "^2.9.4", "chart.js": "^2.9.4",
"common": "workspace:common",
"craco-less": "^1.17.0", "craco-less": "^1.17.0",
"echarts": "^4.9.0", "echarts": "^4.9.0",
"eventemitter3": "^4.0.7", "eventemitter3": "^4.0.7",

View File

@ -1,37 +1,28 @@
import { import { Account, Connection, PublicKey, TransactionInstruction } from '@solana/web3.js';
Account, import { sendTransaction } from 'common/src/contexts/connection';
Connection, import { notify } from 'common/src/utils/notifications';
PublicKey, import { accrueInterestInstruction, LendingReserve } from './../models/lending/reserve';
TransactionInstruction, import { AccountLayout, MintInfo, MintLayout } from '@solana/spl-token';
} from "@solana/web3.js"; import { LENDING_PROGRAM_ID, LEND_HOST_FEE_ADDRESS } from 'common/src/utils/ids';
import { sendTransaction } from "../contexts/connection";
import { notify } from "../utils/notifications";
import {
accrueInterestInstruction,
LendingReserve,
} from "./../models/lending/reserve";
import { AccountLayout, MintInfo, MintLayout } from "@solana/spl-token";
import { LENDING_PROGRAM_ID, LEND_HOST_FEE_ADDRESS } from "../utils/ids";
import { import {
createTempMemoryAccount, createTempMemoryAccount,
createUninitializedAccount, createUninitializedAccount,
createUninitializedMint, createUninitializedMint,
createUninitializedObligation,
ensureSplAccount, ensureSplAccount,
findOrCreateAccountByMint, findOrCreateAccountByMint,
} from "./account"; } from 'common/src/actions/account';
import { cache, MintParser, ParsedAccount } from "../contexts/accounts"; import { createUninitializedObligation } from './obligation';
import { cache, MintParser, ParsedAccount } from 'common/src/contexts/accounts';
import { import {
TokenAccount,
LendingObligationLayout, LendingObligationLayout,
borrowInstruction, borrowInstruction,
LendingMarket, LendingMarket,
BorrowAmountType, BorrowAmountType,
LendingObligation, LendingObligation,
approve,
initObligationInstruction, initObligationInstruction,
} from "../models"; } from '../models';
import { toLamports } from "../utils/utils"; import { TokenAccount, approve } from 'common/src/models';
import { toLamports } from 'common/src/utils/utils';
export const borrow = async ( export const borrow = async (
connection: Connection, connection: Connection,
@ -50,9 +41,9 @@ export const borrow = async (
obligationAccount?: PublicKey obligationAccount?: PublicKey
) => { ) => {
notify({ notify({
message: "Borrowing funds...", message: 'Borrowing funds...',
description: "Please review transactions to approve.", description: 'Please review transactions to approve.',
type: "warn", type: 'warn',
}); });
let signers: Account[] = []; let signers: Account[] = [];
@ -65,18 +56,14 @@ export const borrow = async (
LENDING_PROGRAM_ID LENDING_PROGRAM_ID
); );
const accountRentExempt = await connection.getMinimumBalanceForRentExemption( const accountRentExempt = await connection.getMinimumBalanceForRentExemption(AccountLayout.span);
AccountLayout.span
);
const obligation = existingObligation const obligation = existingObligation
? existingObligation.pubkey ? existingObligation.pubkey
: createUninitializedObligation( : createUninitializedObligation(
instructions, instructions,
wallet.publicKey, wallet.publicKey,
await connection.getMinimumBalanceForRentExemption( await connection.getMinimumBalanceForRentExemption(LendingObligationLayout.span),
LendingObligationLayout.span
),
signers signers
); );
@ -91,12 +78,7 @@ export const borrow = async (
const obligationTokenOutput = obligationAccount const obligationTokenOutput = obligationAccount
? obligationAccount ? obligationAccount
: createUninitializedAccount( : createUninitializedAccount(instructions, wallet.publicKey, accountRentExempt, signers);
instructions,
wallet.publicKey,
accountRentExempt,
signers
);
if (!obligationAccount) { if (!obligationAccount) {
instructions.push( instructions.push(
@ -173,21 +155,19 @@ export const borrow = async (
if (instructions.length > 0) { if (instructions.length > 0) {
// create all accounts in one transaction // create all accounts in one transaction
let tx = await sendTransaction(connection, wallet, instructions, [ let tx = await sendTransaction(connection, wallet, instructions, [...signers]);
...signers,
]);
notify({ notify({
message: "Obligation accounts created", message: 'Obligation accounts created',
description: `Transaction ${tx}`, description: `Transaction ${tx}`,
type: "success", type: 'success',
}); });
} }
notify({ notify({
message: "Borrowing funds...", message: 'Borrowing funds...',
description: "Please review transactions to approve.", description: 'Please review transactions to approve.',
type: "warn", type: 'warn',
}); });
signers = []; signers = [];
@ -214,25 +194,14 @@ export const borrow = async (
throw new Error(`Dex market doesn't exist.`); throw new Error(`Dex market doesn't exist.`);
} }
const market = cache.get(depositReserve.info.lendingMarket) as ParsedAccount< const market = cache.get(depositReserve.info.lendingMarket) as ParsedAccount<LendingMarket>;
LendingMarket const dexOrderBookSide = market.info.quoteMint.equals(depositReserve.info.liquidityMint)
>;
const dexOrderBookSide = market.info.quoteMint.equals(
depositReserve.info.liquidityMint
)
? dexMarket?.info.asks ? dexMarket?.info.asks
: dexMarket?.info.bids; : dexMarket?.info.bids;
const memory = createTempMemoryAccount( const memory = createTempMemoryAccount(instructions, wallet.publicKey, signers, LENDING_PROGRAM_ID);
instructions,
wallet.publicKey,
signers,
LENDING_PROGRAM_ID
);
instructions.push( instructions.push(accrueInterestInstruction(depositReserve.pubkey, borrowReserve.pubkey));
accrueInterestInstruction(depositReserve.pubkey, borrowReserve.pubkey)
);
// borrow // borrow
instructions.push( instructions.push(
borrowInstruction( borrowInstruction(
@ -264,17 +233,11 @@ export const borrow = async (
) )
); );
try { try {
let tx = await sendTransaction( let tx = await sendTransaction(connection, wallet, instructions.concat(cleanupInstructions), signers, true);
connection,
wallet,
instructions.concat(cleanupInstructions),
signers,
true
);
notify({ notify({
message: "Funds borrowed.", message: 'Funds borrowed.',
type: "success", type: 'success',
description: `Transaction - ${tx}`, description: `Transaction - ${tx}`,
}); });
} catch (ex) { } catch (ex) {

View File

@ -1,25 +1,16 @@
import { import { Account, Connection, PublicKey, TransactionInstruction } from '@solana/web3.js';
Account, import { sendTransaction } from 'common/src/contexts/connection';
Connection, import { notify } from 'common/src/utils/notifications';
PublicKey,
TransactionInstruction,
} from "@solana/web3.js";
import { sendTransaction } from "../contexts/connection";
import { notify } from "../utils/notifications";
import { import {
accrueInterestInstruction, accrueInterestInstruction,
depositInstruction, depositInstruction,
initReserveInstruction, initReserveInstruction,
LendingReserve, LendingReserve,
} from "./../models/lending"; } from './../models/lending';
import { AccountLayout } from "@solana/spl-token"; import { AccountLayout } from '@solana/spl-token';
import { LENDING_PROGRAM_ID } from "../utils/ids"; import { LENDING_PROGRAM_ID } from 'common/src/utils/ids';
import { import { createUninitializedAccount, ensureSplAccount, findOrCreateAccountByMint } from 'common/src/actions/account';
createUninitializedAccount, import { approve, TokenAccount } from 'common/src/models';
ensureSplAccount,
findOrCreateAccountByMint,
} from "./account";
import { approve, TokenAccount } from "../models";
export const deposit = async ( export const deposit = async (
from: TokenAccount, from: TokenAccount,
@ -30,9 +21,9 @@ export const deposit = async (
wallet: any wallet: any
) => { ) => {
notify({ notify({
message: "Depositing funds...", message: 'Depositing funds...',
description: "Please review transactions to approve.", description: 'Please review transactions to approve.',
type: "warn", type: 'warn',
}); });
const isInitalized = true; // TODO: finish reserve init const isInitalized = true; // TODO: finish reserve init
@ -42,9 +33,7 @@ export const deposit = async (
const instructions: TransactionInstruction[] = []; const instructions: TransactionInstruction[] = [];
const cleanupInstructions: TransactionInstruction[] = []; const cleanupInstructions: TransactionInstruction[] = [];
const accountRentExempt = await connection.getMinimumBalanceForRentExemption( const accountRentExempt = await connection.getMinimumBalanceForRentExemption(AccountLayout.span);
AccountLayout.span
);
const [authority] = await PublicKey.findProgramAddress( const [authority] = await PublicKey.findProgramAddress(
[reserve.lendingMarket.toBuffer()], // which account should be authority [reserve.lendingMarket.toBuffer()], // which account should be authority
@ -61,13 +50,7 @@ export const deposit = async (
); );
// create approval for transfer transactions // create approval for transfer transactions
const transferAuthority = approve( const transferAuthority = approve(instructions, cleanupInstructions, fromAccount, wallet.publicKey, amountLamports);
instructions,
cleanupInstructions,
fromAccount,
wallet.publicKey,
amountLamports
);
signers.push(transferAuthority); signers.push(transferAuthority);
@ -84,12 +67,7 @@ export const deposit = async (
signers signers
); );
} else { } else {
toAccount = createUninitializedAccount( toAccount = createUninitializedAccount(instructions, wallet.publicKey, accountRentExempt, signers);
instructions,
wallet.publicKey,
accountRentExempt,
signers
);
} }
if (isInitalized) { if (isInitalized) {
@ -132,17 +110,11 @@ export const deposit = async (
} }
try { try {
let tx = await sendTransaction( let tx = await sendTransaction(connection, wallet, instructions.concat(cleanupInstructions), signers, true);
connection,
wallet,
instructions.concat(cleanupInstructions),
signers,
true
);
notify({ notify({
message: "Funds deposited.", message: 'Funds deposited.',
type: "success", type: 'success',
description: `Transaction - ${tx}`, description: `Transaction - ${tx}`,
}); });
} catch { } catch {

View File

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

View File

@ -1,30 +1,14 @@
import { import { Account, Connection, PublicKey, TransactionInstruction } from '@solana/web3.js';
Account, import { sendTransaction } from 'common/src/contexts/connection';
Connection, import { notify } from 'common/src/utils/notifications';
PublicKey, import { accrueInterestInstruction, LendingReserve } from './../models/lending/reserve';
TransactionInstruction, import { liquidateInstruction } from './../models/lending/liquidate';
} from "@solana/web3.js"; import { AccountLayout } from '@solana/spl-token';
import { sendTransaction } from "../contexts/connection"; import { LENDING_PROGRAM_ID } from 'common/src/utils/ids';
import { notify } from "../utils/notifications"; import { createTempMemoryAccount, ensureSplAccount, findOrCreateAccountByMint } from 'common/src/actions/account';
import { import { LendingMarket, LendingObligation } from '../models';
accrueInterestInstruction, import { approve, TokenAccount } from 'common/src/models';
LendingReserve, import { cache, ParsedAccount } from 'common/src/contexts/accounts';
} from "./../models/lending/reserve";
import { liquidateInstruction } from "./../models/lending/liquidate";
import { AccountLayout } from "@solana/spl-token";
import { LENDING_PROGRAM_ID } from "../utils/ids";
import {
createTempMemoryAccount,
ensureSplAccount,
findOrCreateAccountByMint,
} from "./account";
import {
approve,
LendingMarket,
LendingObligation,
TokenAccount,
} from "../models";
import { cache, ParsedAccount } from "../contexts/accounts";
export const liquidate = async ( export const liquidate = async (
connection: Connection, connection: Connection,
@ -40,9 +24,9 @@ export const liquidate = async (
withdrawReserve: ParsedAccount<LendingReserve> withdrawReserve: ParsedAccount<LendingReserve>
) => { ) => {
notify({ notify({
message: "Repaying funds...", message: 'Repaying funds...',
description: "Please review transactions to approve.", description: 'Please review transactions to approve.',
type: "warn", type: 'warn',
}); });
// user from account // user from account
@ -50,9 +34,7 @@ export const liquidate = async (
const instructions: TransactionInstruction[] = []; const instructions: TransactionInstruction[] = [];
const cleanupInstructions: TransactionInstruction[] = []; const cleanupInstructions: TransactionInstruction[] = [];
const accountRentExempt = await connection.getMinimumBalanceForRentExemption( const accountRentExempt = await connection.getMinimumBalanceForRentExemption(AccountLayout.span);
AccountLayout.span
);
const [authority] = await PublicKey.findProgramAddress( const [authority] = await PublicKey.findProgramAddress(
[repayReserve.info.lendingMarket.toBuffer()], [repayReserve.info.lendingMarket.toBuffer()],
@ -69,13 +51,7 @@ export const liquidate = async (
); );
// create approval for transfer transactions // create approval for transfer transactions
const transferAuthority = approve( const transferAuthority = approve(instructions, cleanupInstructions, fromAccount, wallet.publicKey, amountLamports);
instructions,
cleanupInstructions,
fromAccount,
wallet.publicKey,
amountLamports
);
signers.push(transferAuthority); signers.push(transferAuthority);
// get destination account // get destination account
@ -98,26 +74,15 @@ export const liquidate = async (
throw new Error(`Dex market doesn't exist.`); throw new Error(`Dex market doesn't exist.`);
} }
const market = cache.get(withdrawReserve.info.lendingMarket) as ParsedAccount< const market = cache.get(withdrawReserve.info.lendingMarket) as ParsedAccount<LendingMarket>;
LendingMarket
>;
const dexOrderBookSide = market.info.quoteMint.equals( const dexOrderBookSide = market.info.quoteMint.equals(repayReserve.info.liquidityMint)
repayReserve.info.liquidityMint
)
? dexMarket?.info.asks ? dexMarket?.info.asks
: dexMarket?.info.bids; : dexMarket?.info.bids;
const memory = createTempMemoryAccount( const memory = createTempMemoryAccount(instructions, wallet.publicKey, signers, LENDING_PROGRAM_ID);
instructions,
wallet.publicKey,
signers,
LENDING_PROGRAM_ID
);
instructions.push( instructions.push(accrueInterestInstruction(repayReserve.pubkey, withdrawReserve.pubkey));
accrueInterestInstruction(repayReserve.pubkey, withdrawReserve.pubkey)
);
instructions.push( instructions.push(
liquidateInstruction( liquidateInstruction(
@ -138,17 +103,11 @@ export const liquidate = async (
) )
); );
let tx = await sendTransaction( let tx = await sendTransaction(connection, wallet, instructions.concat(cleanupInstructions), signers, true);
connection,
wallet,
instructions.concat(cleanupInstructions),
signers,
true
);
notify({ notify({
message: "Funds liquidated.", message: 'Funds liquidated.',
type: "success", type: 'success',
description: `Transaction - ${tx}`, description: `Transaction - ${tx}`,
}); });
}; };

View File

@ -0,0 +1,25 @@
import { Account, PublicKey, SystemProgram, TransactionInstruction } from '@solana/web3.js';
import { LENDING_PROGRAM_ID } from 'common/src/utils/ids';
import { LendingObligationLayout } from '../models';
export function createUninitializedObligation(
instructions: TransactionInstruction[],
payer: PublicKey,
amount: number,
signers: Account[]
) {
const account = new Account();
instructions.push(
SystemProgram.createAccount({
fromPubkey: payer,
newAccountPubkey: account.publicKey,
lamports: amount,
space: LendingObligationLayout.span,
programId: LENDING_PROGRAM_ID,
})
);
signers.push(account);
return account.publicKey;
}

View File

@ -1,21 +1,14 @@
import { import { Account, Connection, PublicKey, TransactionInstruction } from '@solana/web3.js';
Account, import { sendTransaction } from 'common/src/contexts/connection';
Connection, import { notify } from 'common/src/utils/notifications';
PublicKey, import { accrueInterestInstruction, LendingReserve } from './../models/lending/reserve';
TransactionInstruction, import { repayInstruction } from './../models/lending/repay';
} from "@solana/web3.js"; import { AccountLayout, Token, NATIVE_MINT } from '@solana/spl-token';
import { sendTransaction } from "../contexts/connection"; import { LENDING_PROGRAM_ID, TOKEN_PROGRAM_ID } from 'common/src/utils/ids';
import { notify } from "../utils/notifications"; import { createTokenAccount, findOrCreateAccountByMint } from 'common/src/actions/account';
import { import { LendingObligation } from '../models';
accrueInterestInstruction, import { approve, TokenAccount } from 'common/src/models';
LendingReserve, import { ParsedAccount } from 'common/src/contexts/accounts';
} from "./../models/lending/reserve";
import { repayInstruction } from "./../models/lending/repay";
import { AccountLayout, Token, NATIVE_MINT } from "@solana/spl-token";
import { LENDING_PROGRAM_ID, TOKEN_PROGRAM_ID } from "../utils/ids";
import { createTokenAccount, findOrCreateAccountByMint } from "./account";
import { approve, LendingObligation, TokenAccount } from "../models";
import { ParsedAccount } from "../contexts/accounts";
export const repay = async ( export const repay = async (
from: TokenAccount, from: TokenAccount,
@ -34,9 +27,9 @@ export const repay = async (
wallet: any wallet: any
) => { ) => {
notify({ notify({
message: "Repaying funds...", message: 'Repaying funds...',
description: "Please review transactions to approve.", description: 'Please review transactions to approve.',
type: "warn", type: 'warn',
}); });
// user from account // user from account
@ -44,9 +37,7 @@ export const repay = async (
const instructions: TransactionInstruction[] = []; const instructions: TransactionInstruction[] = [];
const cleanupInstructions: TransactionInstruction[] = []; const cleanupInstructions: TransactionInstruction[] = [];
const accountRentExempt = await connection.getMinimumBalanceForRentExemption( const accountRentExempt = await connection.getMinimumBalanceForRentExemption(AccountLayout.span);
AccountLayout.span
);
const [authority] = await PublicKey.findProgramAddress( const [authority] = await PublicKey.findProgramAddress(
[repayReserve.info.lendingMarket.toBuffer()], [repayReserve.info.lendingMarket.toBuffer()],
@ -54,10 +45,7 @@ export const repay = async (
); );
let fromAccount = from.pubkey; let fromAccount = from.pubkey;
if ( if (wallet.publicKey.equals(fromAccount) && repayReserve.info.liquidityMint.equals(NATIVE_MINT)) {
wallet.publicKey.equals(fromAccount) &&
repayReserve.info.liquidityMint.equals(NATIVE_MINT)
) {
fromAccount = createTokenAccount( fromAccount = createTokenAccount(
instructions, instructions,
wallet.publicKey, wallet.publicKey,
@ -67,24 +55,12 @@ export const repay = async (
signers signers
); );
cleanupInstructions.push( cleanupInstructions.push(
Token.createCloseAccountInstruction( Token.createCloseAccountInstruction(TOKEN_PROGRAM_ID, fromAccount, wallet.publicKey, wallet.publicKey, [])
TOKEN_PROGRAM_ID,
fromAccount,
wallet.publicKey,
wallet.publicKey,
[]
)
); );
} }
// create approval for transfer transactions // create approval for transfer transactions
const transferAuthority = approve( const transferAuthority = approve(instructions, cleanupInstructions, fromAccount, wallet.publicKey, repayAmount);
instructions,
cleanupInstructions,
fromAccount,
wallet.publicKey,
repayAmount
);
signers.push(transferAuthority); signers.push(transferAuthority);
// get destination account // get destination account
@ -110,9 +86,7 @@ export const repay = async (
transferAuthority.publicKey transferAuthority.publicKey
); );
instructions.push( instructions.push(accrueInterestInstruction(repayReserve.pubkey, withdrawReserve.pubkey));
accrueInterestInstruction(repayReserve.pubkey, withdrawReserve.pubkey)
);
instructions.push( instructions.push(
repayInstruction( repayInstruction(
@ -132,17 +106,11 @@ export const repay = async (
) )
); );
let tx = await sendTransaction( let tx = await sendTransaction(connection, wallet, instructions.concat(cleanupInstructions), signers, true);
connection,
wallet,
instructions.concat(cleanupInstructions),
signers,
true
);
notify({ notify({
message: "Funds repaid.", message: 'Funds repaid.',
type: "success", type: 'success',
description: `Transaction - ${tx}`, description: `Transaction - ${tx}`,
}); });
}; };

View File

@ -1,20 +1,11 @@
import { import { Account, Connection, PublicKey, TransactionInstruction } from '@solana/web3.js';
Account, import { sendTransaction } from 'common/src/contexts/connection';
Connection, import { notify } from 'common/src/utils/notifications';
PublicKey, import { accrueInterestInstruction, LendingReserve, withdrawInstruction } from './../models/lending';
TransactionInstruction, import { AccountLayout } from '@solana/spl-token';
} from "@solana/web3.js"; import { LENDING_PROGRAM_ID } from 'common/src/utils/ids';
import { sendTransaction } from "../contexts/connection"; import { findOrCreateAccountByMint } from 'common/src/actions/account';
import { notify } from "../utils/notifications"; import { approve, TokenAccount } from 'common/src/models';
import {
accrueInterestInstruction,
LendingReserve,
withdrawInstruction,
} from "./../models/lending";
import { AccountLayout } from "@solana/spl-token";
import { LENDING_PROGRAM_ID } from "../utils/ids";
import { findOrCreateAccountByMint } from "./account";
import { approve, TokenAccount } from "../models";
export const withdraw = async ( export const withdraw = async (
from: TokenAccount, // CollateralAccount from: TokenAccount, // CollateralAccount
@ -25,9 +16,9 @@ export const withdraw = async (
wallet: any wallet: any
) => { ) => {
notify({ notify({
message: "Withdrawing funds...", message: 'Withdrawing funds...',
description: "Please review transactions to approve.", description: 'Please review transactions to approve.',
type: "warn", type: 'warn',
}); });
// user from account // user from account
@ -35,25 +26,14 @@ export const withdraw = async (
const instructions: TransactionInstruction[] = []; const instructions: TransactionInstruction[] = [];
const cleanupInstructions: TransactionInstruction[] = []; const cleanupInstructions: TransactionInstruction[] = [];
const accountRentExempt = await connection.getMinimumBalanceForRentExemption( const accountRentExempt = await connection.getMinimumBalanceForRentExemption(AccountLayout.span);
AccountLayout.span
);
const [authority] = await PublicKey.findProgramAddress( const [authority] = await PublicKey.findProgramAddress([reserve.lendingMarket.toBuffer()], LENDING_PROGRAM_ID);
[reserve.lendingMarket.toBuffer()],
LENDING_PROGRAM_ID
);
const fromAccount = from.pubkey; const fromAccount = from.pubkey;
// create approval for transfer transactions // create approval for transfer transactions
const transferAuthority = approve( const transferAuthority = approve(instructions, cleanupInstructions, fromAccount, wallet.publicKey, amountLamports);
instructions,
cleanupInstructions,
fromAccount,
wallet.publicKey,
amountLamports
);
signers.push(transferAuthority); signers.push(transferAuthority);
@ -85,17 +65,11 @@ export const withdraw = async (
); );
try { try {
let tx = await sendTransaction( let tx = await sendTransaction(connection, wallet, instructions.concat(cleanupInstructions), signers, true);
connection,
wallet,
instructions.concat(cleanupInstructions),
signers,
true
);
notify({ notify({
message: "Funds deposited.", message: 'Funds deposited.',
type: "success", type: 'success',
description: `Transaction - ${tx}`, description: `Transaction - ${tx}`,
}); });
} catch { } catch {

View File

@ -1,48 +1,32 @@
import React from "react"; import React from 'react';
import { Button, Popover } from "antd"; import { Button, Popover } from 'antd';
import { useWallet } from "../../contexts/wallet"; import { useWallet } from 'common/src/contexts/wallet';
import { CurrentUserBadge } from "../CurrentUserBadge"; import { CurrentUserBadge } from '../CurrentUserBadge';
import { SettingOutlined } from "@ant-design/icons"; import { SettingOutlined } from '@ant-design/icons';
import { Settings } from "../Settings"; import { Settings } from '../Settings';
import { LABELS } from "../../constants"; import { LABELS } from '../../constants';
export const AppBar = (props: { left?: JSX.Element; right?: JSX.Element }) => { export const AppBar = (props: { left?: JSX.Element; right?: JSX.Element }) => {
const { connected, wallet } = useWallet(); const { connected, wallet } = useWallet();
const TopBar = ( const TopBar = (
<div className="App-Bar-right"> <div className='App-Bar-right'>
<CurrentUserBadge /> <CurrentUserBadge />
<div> <div>
{!connected && ( {!connected && (
<Button <Button
type="text" type='text'
size="large" size='large'
onClick={connected ? wallet.disconnect : wallet.connect} onClick={connected ? wallet.disconnect : wallet.connect}
style={{ color: "#2abdd2" }} style={{ color: '#2abdd2' }}
> >
{LABELS.CONNECT_BUTTON} {LABELS.CONNECT_BUTTON}
</Button> </Button>
)} )}
{connected && ( {connected && <Popover placement='bottomRight' title={LABELS.WALLET_TOOLTIP} trigger='click'></Popover>}
<Popover
placement="bottomRight"
title={LABELS.WALLET_TOOLTIP}
trigger="click"
></Popover>
)}
</div> </div>
<Popover <Popover placement='topRight' title={LABELS.SETTINGS_TOOLTIP} content={<Settings />} trigger='click'>
placement="topRight" <Button shape='circle' size='large' type='text' icon={<SettingOutlined />} />
title={LABELS.SETTINGS_TOOLTIP}
content={<Settings />}
trigger="click"
>
<Button
shape="circle"
size="large"
type="text"
icon={<SettingOutlined />}
/>
</Popover> </Popover>
{props.right} {props.right}
</div> </div>

View File

@ -1,37 +1,25 @@
import React, { useCallback, useEffect, useMemo, useState } from "react"; import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { import { useSliderInput, useUserBalance, useUserDeposits, useUserObligationByReserve } from '../../hooks';
useSliderInput, import { BorrowAmountType, LendingReserve, LendingReserveParser } from '../../models';
useUserBalance, import { Card } from 'antd';
useUserDeposits, import { cache, ParsedAccount } from 'common/src/contexts/accounts';
useUserObligationByReserve, import { useConnection } from 'common/src/contexts/connection';
} from "../../hooks"; import { useWallet } from 'common/src/contexts/wallet';
import { import { borrow } from '../../actions';
BorrowAmountType, import './style.less';
LendingReserve, import { LABELS } from '../../constants';
LendingReserveParser, import { ActionConfirmation } from './../ActionConfirmation';
} from "../../models"; import { BackButton } from './../BackButton';
import { Card } from "antd"; import { ConnectButton } from '../ConnectButton';
import { cache, ParsedAccount } from "../../contexts/accounts"; import CollateralInput from '../CollateralInput';
import { useConnection } from "../../contexts/connection"; import { useMidPriceInUSD } from '../../contexts/market';
import { useWallet } from "../../contexts/wallet"; import { RiskSlider } from '../RiskSlider';
import { borrow } from "../../actions";
import "./style.less";
import { LABELS } from "../../constants";
import { ActionConfirmation } from "./../ActionConfirmation";
import { BackButton } from "./../BackButton";
import { ConnectButton } from "../ConnectButton";
import CollateralInput from "../CollateralInput";
import { useMidPriceInUSD } from "../../contexts/market";
import { RiskSlider } from "../RiskSlider";
export const BorrowInput = (props: { export const BorrowInput = (props: { className?: string; reserve: ParsedAccount<LendingReserve> }) => {
className?: string;
reserve: ParsedAccount<LendingReserve>;
}) => {
const connection = useConnection(); const connection = useConnection();
const { wallet } = useWallet(); const { wallet } = useWallet();
const [collateralValue, setCollateralValue] = useState(""); const [collateralValue, setCollateralValue] = useState('');
const [lastTyped, setLastTyped] = useState("collateral"); const [lastTyped, setLastTyped] = useState('collateral');
const [pendingTx, setPendingTx] = useState(false); const [pendingTx, setPendingTx] = useState(false);
const [showConfirmation, setShowConfirmation] = useState(false); const [showConfirmation, setShowConfirmation] = useState(false);
@ -40,24 +28,14 @@ export const BorrowInput = (props: {
const [collateralReserveKey, setCollateralReserveKey] = useState<string>(); const [collateralReserveKey, setCollateralReserveKey] = useState<string>();
const collateralReserve = useMemo(() => { const collateralReserve = useMemo(() => {
const id: string = const id: string = cache.byParser(LendingReserveParser).find((acc) => acc === collateralReserveKey) || '';
cache
.byParser(LendingReserveParser)
.find((acc) => acc === collateralReserveKey) || "";
return cache.get(id) as ParsedAccount<LendingReserve>; return cache.get(id) as ParsedAccount<LendingReserve>;
}, [collateralReserveKey]); }, [collateralReserveKey]);
const borrowPrice = useMidPriceInUSD( const borrowPrice = useMidPriceInUSD(borrowReserve.info.liquidityMint.toBase58()).price;
borrowReserve.info.liquidityMint.toBase58() const collateralPrice = useMidPriceInUSD(collateralReserve?.info.liquidityMint.toBase58())?.price;
).price;
const collateralPrice = useMidPriceInUSD(
collateralReserve?.info.liquidityMint.toBase58()
)?.price;
const include = useMemo( const include = useMemo(() => new Set([collateralReserve?.pubkey.toBase58()]), [collateralReserve]);
() => new Set([collateralReserve?.pubkey.toBase58()]),
[collateralReserve]
);
const exclude = useMemo(() => new Set([]), []); const exclude = useMemo(() => new Set([]), []);
@ -67,7 +45,7 @@ export const BorrowInput = (props: {
const convert = useCallback( const convert = useCallback(
(val: string | number) => { (val: string | number) => {
const minAmount = Math.min(tokenBalance, Infinity); const minAmount = Math.min(tokenBalance, Infinity);
if (typeof val === "string") { if (typeof val === 'string') {
return (parseFloat(val) / minAmount) * 100; return (parseFloat(val) / minAmount) * 100;
} else { } else {
return (val * minAmount) / 100; return (val * minAmount) / 100;
@ -79,7 +57,7 @@ export const BorrowInput = (props: {
const { value, setValue, pct } = useSliderInput(convert); const { value, setValue, pct } = useSliderInput(convert);
useEffect(() => { useEffect(() => {
if (collateralReserve && lastTyped === "collateral") { if (collateralReserve && lastTyped === 'collateral') {
const ltv = borrowReserve.info.config.loanToValueRatio / 100; const ltv = borrowReserve.info.config.loanToValueRatio / 100;
if (collateralValue) { if (collateralValue) {
@ -88,21 +66,13 @@ export const BorrowInput = (props: {
const borrowAmount = borrowInUSD / borrowPrice; const borrowAmount = borrowInUSD / borrowPrice;
setValue(borrowAmount.toString()); setValue(borrowAmount.toString());
} else { } else {
setValue(""); setValue('');
} }
} }
}, [ }, [lastTyped, collateralReserve, collateralPrice, borrowPrice, borrowReserve, collateralValue, setValue]);
lastTyped,
collateralReserve,
collateralPrice,
borrowPrice,
borrowReserve,
collateralValue,
setValue,
]);
useEffect(() => { useEffect(() => {
if (collateralReserve && lastTyped === "borrow") { if (collateralReserve && lastTyped === 'borrow') {
const ltv = borrowReserve.info.config.loanToValueRatio / 100; const ltv = borrowReserve.info.config.loanToValueRatio / 100;
if (value) { if (value) {
@ -111,25 +81,13 @@ export const BorrowInput = (props: {
const collateralAmount = borrowInUSD / ltv / collateralPrice; const collateralAmount = borrowInUSD / ltv / collateralPrice;
setCollateralValue(collateralAmount.toString()); setCollateralValue(collateralAmount.toString());
} else { } else {
setCollateralValue(""); setCollateralValue('');
} }
} }
}, [ }, [lastTyped, collateralReserve, collateralPrice, borrowPrice, borrowReserve, value]);
lastTyped,
collateralReserve,
collateralPrice,
borrowPrice,
borrowReserve,
value,
]);
const { userObligationsByReserve } = useUserObligationByReserve( const { userObligationsByReserve } = useUserObligationByReserve(borrowReserve?.pubkey, collateralReserve?.pubkey);
borrowReserve?.pubkey, const { accounts: fromAccounts } = useUserBalance(collateralReserve?.info.collateralMint);
collateralReserve?.pubkey
);
const { accounts: fromAccounts } = useUserBalance(
collateralReserve?.info.collateralMint
);
const onBorrow = useCallback(() => { const onBorrow = useCallback(() => {
if (!collateralReserve) { if (!collateralReserve) {
return; return;
@ -151,17 +109,13 @@ export const BorrowInput = (props: {
collateralReserve, collateralReserve,
// TODO: select exsisting obligations by collateral reserve // TODO: select exsisting obligations by collateral reserve
userObligationsByReserve.length > 0 userObligationsByReserve.length > 0 ? userObligationsByReserve[0].obligation.account : undefined,
? userObligationsByReserve[0].obligation.account
: undefined,
userObligationsByReserve.length > 0 userObligationsByReserve.length > 0 ? userObligationsByReserve[0].userAccounts[0].pubkey : undefined
? userObligationsByReserve[0].userAccounts[0].pubkey
: undefined
); );
setValue(""); setValue('');
setCollateralValue(""); setCollateralValue('');
setShowConfirmation(true); setShowConfirmation(true);
} catch { } catch {
// TODO: // TODO:
@ -183,11 +137,11 @@ export const BorrowInput = (props: {
]); ]);
const bodyStyle: React.CSSProperties = { const bodyStyle: React.CSSProperties = {
display: "flex", display: 'flex',
flex: 1, flex: 1,
justifyContent: "center", justifyContent: 'center',
alignItems: "center", alignItems: 'center',
height: "100%", height: '100%',
}; };
return ( return (
@ -197,27 +151,27 @@ export const BorrowInput = (props: {
) : ( ) : (
<div <div
style={{ style={{
display: "flex", display: 'flex',
flexDirection: "column", flexDirection: 'column',
justifyContent: "space-around", justifyContent: 'space-around',
}} }}
> >
<div className="borrow-input-title">{LABELS.BORROW_QUESTION}</div> <div className='borrow-input-title'>{LABELS.BORROW_QUESTION}</div>
<div <div
style={{ style={{
display: "flex", display: 'flex',
flexDirection: "row", flexDirection: 'row',
justifyContent: "space-evenly", justifyContent: 'space-evenly',
alignItems: "center", alignItems: 'center',
}} }}
> >
<CollateralInput <CollateralInput
title="Collateral (estimated)" title='Collateral (estimated)'
reserve={borrowReserve.info} reserve={borrowReserve.info}
amount={parseFloat(collateralValue) || 0} amount={parseFloat(collateralValue) || 0}
onInputChange={(val: number | null) => { onInputChange={(val: number | null) => {
setCollateralValue(val?.toString() || ""); setCollateralValue(val?.toString() || '');
setLastTyped("collateral"); setLastTyped('collateral');
}} }}
onCollateralReserve={(key) => { onCollateralReserve={(key) => {
setCollateralReserveKey(key); setCollateralReserveKey(key);
@ -228,35 +182,33 @@ export const BorrowInput = (props: {
<RiskSlider value={pct} /> <RiskSlider value={pct} />
<div <div
style={{ style={{
display: "flex", display: 'flex',
flexDirection: "row", flexDirection: 'row',
justifyContent: "space-evenly", justifyContent: 'space-evenly',
alignItems: "center", alignItems: 'center',
marginBottom: 20, marginBottom: 20,
}} }}
> >
<CollateralInput <CollateralInput
title="Borrow Amount" title='Borrow Amount'
reserve={borrowReserve.info} reserve={borrowReserve.info}
amount={parseFloat(value) || 0} amount={parseFloat(value) || 0}
onInputChange={(val: number | null) => { onInputChange={(val: number | null) => {
setValue(val?.toString() || ""); setValue(val?.toString() || '');
setLastTyped("borrow"); setLastTyped('borrow');
}} }}
disabled={true} disabled={true}
hideBalance={true} hideBalance={true}
/> />
</div> </div>
<ConnectButton <ConnectButton
size="large" size='large'
type="primary" type='primary'
onClick={onBorrow} onClick={onBorrow}
loading={pendingTx} loading={pendingTx}
disabled={fromAccounts.length === 0} disabled={fromAccounts.length === 0}
> >
{fromAccounts.length === 0 {fromAccounts.length === 0 ? LABELS.NO_COLLATERAL : LABELS.BORROW_ACTION}
? LABELS.NO_COLLATERAL
: LABELS.BORROW_ACTION}
</ConnectButton> </ConnectButton>
<BackButton /> <BackButton />
</div> </div>

View File

@ -1,22 +1,14 @@
import React, { useEffect, useState } from "react"; import React, { useEffect, useState } from 'react';
import { cache, ParsedAccount } from "../../contexts/accounts"; import { cache, ParsedAccount } from 'common/src/contexts/accounts';
import { useConnectionConfig } from "../../contexts/connection"; import { useConnectionConfig } from 'common/src/contexts/connection';
import { import { useLendingReserves, useUserBalance, useUserDeposits } from '../../hooks';
useLendingReserves, import { LendingReserve, LendingMarket, LendingReserveParser } from '../../models';
useUserBalance, import { getTokenName } from 'common/src/utils/utils';
useUserDeposits, import { Card, Select } from 'antd';
} from "../../hooks"; import { TokenIcon } from '../TokenIcon';
import { import { NumericInput } from '../Input/numeric';
LendingReserve, import './style.less';
LendingMarket, import { TokenDisplay } from '../TokenDisplay';
LendingReserveParser,
} from "../../models";
import { getTokenName } from "../../utils/utils";
import { Card, Select } from "antd";
import { TokenIcon } from "../TokenIcon";
import { NumericInput } from "../Input/numeric";
import "./style.less";
import { TokenDisplay } from "../TokenDisplay";
const { Option } = Select; const { Option } = Select;
@ -41,24 +33,19 @@ export default function CollateralInput(props: {
const { tokenMap } = useConnectionConfig(); const { tokenMap } = useConnectionConfig();
const [collateralReserve, setCollateralReserve] = useState<string>(); const [collateralReserve, setCollateralReserve] = useState<string>();
const [balance, setBalance] = useState<number>(0); const [balance, setBalance] = useState<number>(0);
const [lastAmount, setLastAmount] = useState<string>(""); const [lastAmount, setLastAmount] = useState<string>('');
const userDeposits = useUserDeposits(); const userDeposits = useUserDeposits();
useEffect(() => { useEffect(() => {
if (props.useWalletBalance) { if (props.useWalletBalance) {
setBalance(tokenBalance); setBalance(tokenBalance);
} else { } else {
const id: string = const id: string = cache.byParser(LendingReserveParser).find((acc) => acc === collateralReserve) || '';
cache
.byParser(LendingReserveParser)
.find((acc) => acc === collateralReserve) || "";
const parser = cache.get(id) as ParsedAccount<LendingReserve>; const parser = cache.get(id) as ParsedAccount<LendingReserve>;
if (parser) { if (parser) {
const collateralDeposit = userDeposits.userDeposits.find( const collateralDeposit = userDeposits.userDeposits.find(
(u) => (u) => u.reserve.info.liquidityMint.toBase58() === parser.info.liquidityMint.toBase58()
u.reserve.info.liquidityMint.toBase58() ===
parser.info.liquidityMint.toBase58()
); );
if (collateralDeposit) setBalance(collateralDeposit.info.amount); if (collateralDeposit) setBalance(collateralDeposit.info.amount);
else setBalance(0); else setBalance(0);
@ -66,28 +53,16 @@ export default function CollateralInput(props: {
} }
}, [collateralReserve, userDeposits, tokenBalance, props.useWalletBalance]); }, [collateralReserve, userDeposits, tokenBalance, props.useWalletBalance]);
const market = cache.get(props.reserve.lendingMarket) as ParsedAccount< const market = cache.get(props.reserve.lendingMarket) as ParsedAccount<LendingMarket>;
LendingMarket
>;
if (!market) return null; if (!market) return null;
const onlyQuoteAllowed = !props.reserve?.liquidityMint?.equals( const onlyQuoteAllowed = !props.reserve?.liquidityMint?.equals(market?.info?.quoteMint);
market?.info?.quoteMint
);
const filteredReserveAccounts = reserveAccounts const filteredReserveAccounts = reserveAccounts
.filter((reserve) => reserve.info !== props.reserve) .filter((reserve) => reserve.info !== props.reserve)
.filter( .filter((reserve) => !onlyQuoteAllowed || reserve.info.liquidityMint.equals(market.info.quoteMint));
(reserve) =>
!onlyQuoteAllowed ||
reserve.info.liquidityMint.equals(market.info.quoteMint)
);
if ( if (!collateralReserve && props.useFirstReserve && filteredReserveAccounts.length) {
!collateralReserve &&
props.useFirstReserve &&
filteredReserveAccounts.length
) {
const address = filteredReserveAccounts[0].pubkey.toBase58(); const address = filteredReserveAccounts[0].pubkey.toBase58();
setCollateralReserve(address); setCollateralReserve(address);
} }
@ -97,7 +72,7 @@ export default function CollateralInput(props: {
const name = getTokenName(tokenMap, mint); const name = getTokenName(tokenMap, mint);
return ( return (
<Option key={address} value={address} name={name} title={address}> <Option key={address} value={address} name={name} title={address}>
<div key={address} style={{ display: "flex", alignItems: "center" }}> <div key={address} style={{ display: 'flex', alignItems: 'center' }}>
<TokenIcon mintAddress={mint} /> <TokenIcon mintAddress={mint} />
{name} {name}
</div> </div>
@ -106,30 +81,19 @@ export default function CollateralInput(props: {
}); });
return ( return (
<Card <Card className='ccy-input' style={{ borderRadius: 20 }} bodyStyle={{ padding: 0 }}>
className="ccy-input" <div className='ccy-input-header'>
style={{ borderRadius: 20 }} <div className='ccy-input-header-left'>{props.title}</div>
bodyStyle={{ padding: 0 }}
>
<div className="ccy-input-header">
<div className="ccy-input-header-left">{props.title}</div>
{!props.hideBalance && ( {!props.hideBalance && (
<div <div className='ccy-input-header-right' onClick={(e) => props.onInputChange && props.onInputChange(balance)}>
className="ccy-input-header-right"
onClick={(e) => props.onInputChange && props.onInputChange(balance)}
>
Balance: {balance.toFixed(6)} Balance: {balance.toFixed(6)}
</div> </div>
)} )}
</div> </div>
<div className="ccy-input-header" style={{ padding: "0px 10px 5px 7px" }}> <div className='ccy-input-header' style={{ padding: '0px 10px 5px 7px' }}>
<NumericInput <NumericInput
value={ value={parseFloat(lastAmount || '0.00') === props.amount ? lastAmount : props.amount?.toFixed(6)?.toString()}
parseFloat(lastAmount || "0.00") === props.amount
? lastAmount
: props.amount?.toFixed(6)?.toString()
}
onChange={(val: string) => { onChange={(val: string) => {
if (props.onInputChange && parseFloat(val) !== props.amount) { if (props.onInputChange && parseFloat(val) !== props.amount) {
if (!val || !parseFloat(val)) props.onInputChange(null); if (!val || !parseFloat(val)) props.onInputChange(null);
@ -139,19 +103,19 @@ export default function CollateralInput(props: {
}} }}
style={{ style={{
fontSize: 20, fontSize: 20,
boxShadow: "none", boxShadow: 'none',
borderColor: "transparent", borderColor: 'transparent',
outline: "transparent", outline: 'transparent',
}} }}
placeholder="0.00" placeholder='0.00'
/> />
<div className="ccy-input-header-right" style={{ display: "flex" }}> <div className='ccy-input-header-right' style={{ display: 'flex' }}>
{props.showLeverageSelector && ( {props.showLeverageSelector && (
<Select <Select
size="large" size='large'
showSearch showSearch
style={{ width: 80 }} style={{ width: 80 }}
placeholder="CCY" placeholder='CCY'
value={props.leverage} value={props.leverage}
onChange={(item: number) => { onChange={(item: number) => {
if (props.onLeverage) props.onLeverage(item); if (props.onLeverage) props.onLeverage(item);
@ -162,22 +126,12 @@ export default function CollateralInput(props: {
props.onLeverage(parseFloat(item)); props.onLeverage(parseFloat(item));
} }
}} }}
filterOption={(input, option) => filterOption={(input, option) => option?.name?.toLowerCase().indexOf(input.toLowerCase()) >= 0}
option?.name?.toLowerCase().indexOf(input.toLowerCase()) >= 0
}
> >
{[1, 2, 3, 4, 5].map((val) => ( {[1, 2, 3, 4, 5].map((val) => (
<Option <Option key={val} value={val} name={val + 'x'} title={val + 'x'}>
key={val} <div key={val} style={{ display: 'flex', alignItems: 'center' }}>
value={val} {val + 'x'}
name={val + "x"}
title={val + "x"}
>
<div
key={val}
style={{ display: "flex", alignItems: "center" }}
>
{val + "x"}
</div> </div>
</Option> </Option>
))} ))}
@ -185,28 +139,23 @@ export default function CollateralInput(props: {
)} )}
{!props.disabled ? ( {!props.disabled ? (
<Select <Select
size="large" size='large'
showSearch showSearch
style={{ minWidth: 150 }} style={{ minWidth: 150 }}
placeholder="CCY" placeholder='CCY'
value={collateralReserve} value={collateralReserve}
onChange={(item) => { onChange={(item) => {
if (props.onCollateralReserve) props.onCollateralReserve(item); if (props.onCollateralReserve) props.onCollateralReserve(item);
setCollateralReserve(item); setCollateralReserve(item);
}} }}
filterOption={(input, option) => filterOption={(input, option) => option?.name?.toLowerCase().indexOf(input.toLowerCase()) >= 0}
option?.name?.toLowerCase().indexOf(input.toLowerCase()) >= 0
}
> >
{renderReserveAccounts} {renderReserveAccounts}
</Select> </Select>
) : ( ) : (
<TokenDisplay <TokenDisplay
key={props.reserve.liquidityMint.toBase58()} key={props.reserve.liquidityMint.toBase58()}
name={getTokenName( name={getTokenName(tokenMap, props.reserve.liquidityMint.toBase58())}
tokenMap,
props.reserve.liquidityMint.toBase58()
)}
mintAddress={props.reserve.liquidityMint.toBase58()} mintAddress={props.reserve.liquidityMint.toBase58()}
showBalance={false} showBalance={false}
/> />

View File

@ -1,11 +1,11 @@
import React from "react"; import React from 'react';
import { useLendingReserves, UserDeposit, useUserDeposits } from "../../hooks"; import { useLendingReserves, UserDeposit, useUserDeposits } from '../../hooks';
import { LendingMarket, LendingReserve } from "../../models"; import { LendingMarket, LendingReserve } from '../../models';
import { TokenIcon } from "../TokenIcon"; import { TokenIcon } from '../TokenIcon';
import { formatAmount, getTokenName } from "../../utils/utils"; import { formatAmount, getTokenName } from 'common/src/utils/utils';
import { Select } from "antd"; import { Select } from 'antd';
import { useConnectionConfig } from "../../contexts/connection"; import { useConnectionConfig } from 'common/src/contexts/connection';
import { cache, ParsedAccount } from "../../contexts/accounts"; import { cache, ParsedAccount } from 'common/src/contexts/accounts';
const { Option } = Select; const { Option } = Select;
@ -19,12 +19,10 @@ export const CollateralItem = (props: {
return ( return (
<> <>
<div style={{ display: "flex", alignItems: "center" }}> <div style={{ display: 'flex', alignItems: 'center' }}>
<TokenIcon mintAddress={mint} /> <TokenIcon mintAddress={mint} />
{name} {name}
<span className="token-balance"> <span className='token-balance'>&nbsp; {userDeposit ? formatAmount(userDeposit.info.amount) : '--'}</span>
&nbsp; {userDeposit ? formatAmount(userDeposit.info.amount) : "--"}
</span>
</div> </div>
</> </>
); );
@ -40,22 +38,19 @@ export const CollateralSelector = (props: {
const { tokenMap } = useConnectionConfig(); const { tokenMap } = useConnectionConfig();
const { userDeposits } = useUserDeposits(); const { userDeposits } = useUserDeposits();
const market = cache.get(props.reserve?.lendingMarket) as ParsedAccount< const market = cache.get(props.reserve?.lendingMarket) as ParsedAccount<LendingMarket>;
LendingMarket
>;
if (!market) return null; if (!market) return null;
const quoteMintAddress = market?.info?.quoteMint?.toBase58(); const quoteMintAddress = market?.info?.quoteMint?.toBase58();
const onlyQuoteAllowed = const onlyQuoteAllowed = props.reserve?.liquidityMint?.toBase58() !== quoteMintAddress;
props.reserve?.liquidityMint?.toBase58() !== quoteMintAddress;
return ( return (
<Select <Select
size="large" size='large'
showSearch showSearch
style={{ minWidth: 300, margin: "5px 0px" }} style={{ minWidth: 300, margin: '5px 0px' }}
placeholder="Collateral" placeholder='Collateral'
value={props.collateralReserve} value={props.collateralReserve}
disabled={props.disabled} disabled={props.disabled}
defaultValue={props.collateralReserve} defaultValue={props.collateralReserve}
@ -64,17 +59,11 @@ export const CollateralSelector = (props: {
props.onCollateralReserve(item); props.onCollateralReserve(item);
} }
}} }}
filterOption={(input, option) => filterOption={(input, option) => option?.name?.toLowerCase().indexOf(input.toLowerCase()) >= 0}
option?.name?.toLowerCase().indexOf(input.toLowerCase()) >= 0
}
> >
{reserveAccounts {reserveAccounts
.filter((reserve) => reserve.info !== props.reserve) .filter((reserve) => reserve.info !== props.reserve)
.filter( .filter((reserve) => !onlyQuoteAllowed || reserve.info.liquidityMint.equals(market.info.quoteMint))
(reserve) =>
!onlyQuoteAllowed ||
reserve.info.liquidityMint.equals(market.info.quoteMint)
)
.map((reserve) => { .map((reserve) => {
const mint = reserve.info.liquidityMint.toBase58(); const mint = reserve.info.liquidityMint.toBase58();
const address = reserve.pubkey.toBase58(); const address = reserve.pubkey.toBase58();
@ -84,9 +73,7 @@ export const CollateralSelector = (props: {
<Option key={address} value={address} name={name} title={address}> <Option key={address} value={address} name={name} title={address}>
<CollateralItem <CollateralItem
reserve={reserve} reserve={reserve}
userDeposit={userDeposits.find( userDeposit={userDeposits.find((dep) => dep.reserve.pubkey.toBase58() === address)}
(dep) => dep.reserve.pubkey.toBase58() === address
)}
mint={mint} mint={mint}
name={name} name={name}
/> />

View File

@ -1,20 +1,14 @@
import { Button } from "antd"; import { Button } from 'antd';
import { ButtonProps } from "antd/lib/button"; import { ButtonProps } from 'antd/lib/button';
import React from "react"; import React from 'react';
import { useWallet } from "../../contexts/wallet"; import { useWallet } from 'common/src/contexts/wallet';
import { LABELS } from "./../../constants"; import { LABELS } from './../../constants';
export const ConnectButton = ( export const ConnectButton = (props: ButtonProps & React.RefAttributes<HTMLElement>) => {
props: ButtonProps & React.RefAttributes<HTMLElement>
) => {
const { wallet, connected } = useWallet(); const { wallet, connected } = useWallet();
const { onClick, children, disabled, ...rest } = props; const { onClick, children, disabled, ...rest } = props;
return ( return (
<Button <Button {...rest} onClick={connected ? onClick : wallet.connect} disabled={connected && disabled}>
{...rest}
onClick={connected ? onClick : wallet.connect}
disabled={connected && disabled}
>
{connected ? props.children : LABELS.CONNECT_LABEL} {connected ? props.children : LABELS.CONNECT_LABEL}
</Button> </Button>
); );

View File

@ -1,9 +1,9 @@
import React from "react"; import React from 'react';
import { useWallet } from "../../contexts/wallet"; import { useWallet } from 'common/src/contexts/wallet';
import { formatNumber, shortenAddress } from "../../utils/utils"; import { formatNumber, shortenAddress } from 'common/src/utils/utils';
import { Identicon } from "../Identicon"; import { Identicon } from '../Identicon';
import { useNativeAccount } from "../../contexts/accounts"; import { useNativeAccount } from 'common/src/contexts/accounts';
import { LAMPORTS_PER_SOL } from "@solana/web3.js"; import { LAMPORTS_PER_SOL } from '@solana/web3.js';
export const CurrentUserBadge = (props: {}) => { export const CurrentUserBadge = (props: {}) => {
const { wallet } = useWallet(); const { wallet } = useWallet();
@ -16,16 +16,11 @@ export const CurrentUserBadge = (props: {}) => {
// should use SOL ◎ ? // should use SOL ◎ ?
return ( return (
<div className="wallet-wrapper"> <div className='wallet-wrapper'>
<span> <span>{formatNumber.format((account?.lamports || 0) / LAMPORTS_PER_SOL)} SOL</span>
{formatNumber.format((account?.lamports || 0) / LAMPORTS_PER_SOL)} SOL <div className='wallet-key'>
</span>
<div className="wallet-key">
{shortenAddress(`${wallet.publicKey}`)} {shortenAddress(`${wallet.publicKey}`)}
<Identicon <Identicon address={wallet.publicKey?.toBase58()} style={{ marginLeft: '0.5rem', display: 'flex' }} />
address={wallet.publicKey?.toBase58()}
style={{ marginLeft: "0.5rem", display: "flex" }}
/>
</div> </div>
</div> </div>
); );

View File

@ -1,58 +1,39 @@
import React, { useMemo } from "react"; import React, { useMemo } from 'react';
import { import { useUserBalance, useUserCollateralBalance } from './../../hooks';
useTokenName, import { useTokenName } from 'common/src/hooks';
useUserBalance, import { calculateDepositAPY, LendingReserve } from '../../models/lending';
useUserCollateralBalance, import { formatNumber, formatPct } from 'common/src/utils/utils';
} from "./../../hooks"; import { Card, Col, Row, Statistic } from 'antd';
import { calculateDepositAPY, LendingReserve } from "../../models/lending"; import './style.less';
import { formatNumber, formatPct } from "../../utils/utils"; import { PublicKey } from '@solana/web3.js';
import { Card, Col, Row, Statistic } from "antd"; import { GUTTER } from '../../constants';
import "./style.less";
import { PublicKey } from "@solana/web3.js";
import { GUTTER } from "../../constants";
export const DepositInfoLine = (props: { export const DepositInfoLine = (props: { className?: string; reserve: LendingReserve; address: PublicKey }) => {
className?: string;
reserve: LendingReserve;
address: PublicKey;
}) => {
const name = useTokenName(props.reserve.liquidityMint); const name = useTokenName(props.reserve.liquidityMint);
const { balance: tokenBalance } = useUserBalance(props.reserve.liquidityMint); const { balance: tokenBalance } = useUserBalance(props.reserve.liquidityMint);
const { balance: collateralBalance } = useUserCollateralBalance( const { balance: collateralBalance } = useUserCollateralBalance(props.reserve);
props.reserve const depositAPY = useMemo(() => calculateDepositAPY(props.reserve), [props.reserve]);
);
const depositAPY = useMemo(() => calculateDepositAPY(props.reserve), [
props.reserve,
]);
return ( return (
<Row gutter={GUTTER}> <Row gutter={GUTTER}>
<Col xs={24} xl={5}> <Col xs={24} xl={5}>
<Card className={props.className}> <Card className={props.className}>
<Statistic <Statistic title='Your balance in Oyster' value={formatNumber.format(collateralBalance)} suffix={name} />
title="Your balance in Oyster"
value={formatNumber.format(collateralBalance)}
suffix={name}
/>
</Card> </Card>
</Col> </Col>
<Col xs={24} xl={5}> <Col xs={24} xl={5}>
<Card className={props.className}> <Card className={props.className}>
<Statistic <Statistic title='Your wallet balance' value={formatNumber.format(tokenBalance)} suffix={name} />
title="Your wallet balance"
value={formatNumber.format(tokenBalance)}
suffix={name}
/>
</Card> </Card>
</Col> </Col>
<Col xs={24} xl={5}> <Col xs={24} xl={5}>
<Card className={props.className}> <Card className={props.className}>
<Statistic title="Health Factor" value="--" /> <Statistic title='Health Factor' value='--' />
</Card> </Card>
</Col> </Col>
<Col xs={24} xl={9}> <Col xs={24} xl={9}>
<Card className={props.className}> <Card className={props.className}>
<Statistic title="APY" value={formatPct.format(depositAPY)} /> <Statistic title='APY' value={formatPct.format(depositAPY)} />
</Card> </Card>
</Col> </Col>
</Row> </Row>

View File

@ -1,22 +1,18 @@
import React, { useCallback, useState } from "react"; import React, { useCallback, useState } from 'react';
import { InputType, useSliderInput, useUserBalance } from "../../hooks"; import { InputType, useSliderInput, useUserBalance } from '../../hooks';
import { LendingReserve } from "../../models/lending"; import { LendingReserve } from '../../models/lending';
import { Card, Slider } from "antd"; import { Card, Slider } from 'antd';
import { useConnection } from "../../contexts/connection"; import { useConnection } from 'common/src/contexts/connection';
import { useWallet } from "../../contexts/wallet"; import { useWallet } from 'common/src/contexts/wallet';
import { deposit } from "../../actions/deposit"; import { deposit } from '../../actions/deposit';
import { PublicKey } from "@solana/web3.js"; import { PublicKey } from '@solana/web3.js';
import "./style.less"; import './style.less';
import { ActionConfirmation } from "./../ActionConfirmation"; import { ActionConfirmation } from './../ActionConfirmation';
import { LABELS, marks } from "../../constants"; import { LABELS, marks } from '../../constants';
import { ConnectButton } from "../ConnectButton"; import { ConnectButton } from '../ConnectButton';
import CollateralInput from "../CollateralInput"; import CollateralInput from '../CollateralInput';
export const DepositInput = (props: { export const DepositInput = (props: { className?: string; reserve: LendingReserve; address: PublicKey }) => {
className?: string;
reserve: LendingReserve;
address: PublicKey;
}) => {
const connection = useConnection(); const connection = useConnection();
const { wallet } = useWallet(); const { wallet } = useWallet();
const [pendingTx, setPendingTx] = useState(false); const [pendingTx, setPendingTx] = useState(false);
@ -25,13 +21,11 @@ export const DepositInput = (props: {
const reserve = props.reserve; const reserve = props.reserve;
const address = props.address; const address = props.address;
const { accounts: fromAccounts, balance, balanceLamports } = useUserBalance( const { accounts: fromAccounts, balance, balanceLamports } = useUserBalance(reserve?.liquidityMint);
reserve?.liquidityMint
);
const convert = useCallback( const convert = useCallback(
(val: string | number) => { (val: string | number) => {
if (typeof val === "string") { if (typeof val === 'string') {
return (parseFloat(val) / balance) * 100; return (parseFloat(val) / balance) * 100;
} else { } else {
return (val * balance) / 100; return (val * balance) / 100;
@ -58,7 +52,7 @@ export const DepositInput = (props: {
wallet wallet
); );
setValue(""); setValue('');
setShowConfirmation(true); setShowConfirmation(true);
} catch { } catch {
// TODO: // TODO:
@ -66,26 +60,14 @@ export const DepositInput = (props: {
setPendingTx(false); setPendingTx(false);
} }
})(); })();
}, [ }, [connection, setValue, balanceLamports, balance, wallet, value, pct, type, reserve, fromAccounts, address]);
connection,
setValue,
balanceLamports,
balance,
wallet,
value,
pct,
type,
reserve,
fromAccounts,
address,
]);
const bodyStyle: React.CSSProperties = { const bodyStyle: React.CSSProperties = {
display: "flex", display: 'flex',
flex: 1, flex: 1,
justifyContent: "center", justifyContent: 'center',
alignItems: "center", alignItems: 'center',
height: "100%", height: '100%',
}; };
return ( return (
@ -95,26 +77,26 @@ export const DepositInput = (props: {
) : ( ) : (
<div <div
style={{ style={{
display: "flex", display: 'flex',
flexDirection: "column", flexDirection: 'column',
justifyContent: "space-around", justifyContent: 'space-around',
}} }}
> >
<div className="deposit-input-title">{LABELS.DEPOSIT_QUESTION}</div> <div className='deposit-input-title'>{LABELS.DEPOSIT_QUESTION}</div>
<div <div
style={{ style={{
display: "flex", display: 'flex',
flexDirection: "row", flexDirection: 'row',
justifyContent: "space-evenly", justifyContent: 'space-evenly',
alignItems: "center", alignItems: 'center',
}} }}
> >
<CollateralInput <CollateralInput
title="Amount" title='Amount'
reserve={reserve} reserve={reserve}
amount={parseFloat(value) || 0} amount={parseFloat(value) || 0}
onInputChange={(val: number | null) => { onInputChange={(val: number | null) => {
setValue(val?.toString() || ""); setValue(val?.toString() || '');
}} }}
disabled={true} disabled={true}
hideBalance={true} hideBalance={true}
@ -124,8 +106,8 @@ export const DepositInput = (props: {
<Slider marks={marks} value={pct} onChange={setPct} /> <Slider marks={marks} value={pct} onChange={setPct} />
<ConnectButton <ConnectButton
size="large" size='large'
type="primary" type='primary'
onClick={onDeposit} onClick={onDeposit}
loading={pendingTx} loading={pendingTx}
disabled={fromAccounts.length === 0} disabled={fromAccounts.length === 0}

View File

@ -1,6 +1,6 @@
import React from "react"; import React from 'react';
import "./../../App.less"; import './../../App.less';
import { Menu } from "antd"; import { Menu } from 'antd';
import { import {
PieChartOutlined, PieChartOutlined,
GithubOutlined, GithubOutlined,
@ -11,103 +11,97 @@ import {
RocketOutlined, RocketOutlined,
ForkOutlined, ForkOutlined,
// LineChartOutlined // LineChartOutlined
} from "@ant-design/icons"; } from '@ant-design/icons';
import BasicLayout from "@ant-design/pro-layout"; import BasicLayout from '@ant-design/pro-layout';
import { AppBar } from "./../AppBar"; import { AppBar } from './../AppBar';
import { Link, useLocation } from "react-router-dom"; import { Link, useLocation } from 'react-router-dom';
import { useConnectionConfig } from "../../contexts/connection"; import { useConnectionConfig } from 'common/src/contexts/connection';
import { LABELS } from "../../constants"; import { LABELS } from '../../constants';
import config from "./../../../package.json"; import config from './../../../package.json';
export const AppLayout = React.memo((props: any) => { export const AppLayout = React.memo((props: any) => {
const { env } = useConnectionConfig(); const { env } = useConnectionConfig();
const location = useLocation(); const location = useLocation();
const paths: { [key: string]: string } = { const paths: { [key: string]: string } = {
"/dashboard": "2", '/dashboard': '2',
"/deposit": "3", '/deposit': '3',
"/borrow": "4", '/borrow': '4',
"/liquidate": "5", '/liquidate': '5',
"/margin": "6", '/margin': '6',
"/faucet": "7", '/faucet': '7',
}; };
const current = const current = [...Object.keys(paths)].find((key) => location.pathname.startsWith(key)) || '';
[...Object.keys(paths)].find((key) => location.pathname.startsWith(key)) || const defaultKey = paths[current] || '1';
""; const theme = 'dark';
const defaultKey = paths[current] || "1";
const theme = "dark";
return ( return (
<div className="App"> <div className='App'>
<div className="Banner"> <div className='Banner'>
<div className="Banner-description">{LABELS.AUDIT_WARNING}</div> <div className='Banner-description'>{LABELS.AUDIT_WARNING}</div>
</div> </div>
<BasicLayout <BasicLayout
title={LABELS.APP_TITLE} title={LABELS.APP_TITLE}
footerRender={() => ( footerRender={() => (
<div className="footer" title={LABELS.FOOTER}> <div className='footer' title={LABELS.FOOTER}>
{LABELS.FOOTER} {LABELS.FOOTER}
</div> </div>
)} )}
navTheme={theme} navTheme={theme}
headerTheme={theme} headerTheme={theme}
theme={theme} theme={theme}
layout="mix" layout='mix'
fixSiderbar={true} fixSiderbar={true}
primaryColor="#d83aeb" primaryColor='#d83aeb'
logo={<div className="App-logo" />} logo={<div className='App-logo' />}
rightContentRender={() => <AppBar />} rightContentRender={() => <AppBar />}
links={[]} links={[]}
menuContentRender={() => { menuContentRender={() => {
return ( return (
<div className="links"> <div className='links'>
<Menu <Menu theme={theme} defaultSelectedKeys={[defaultKey]} mode='inline'>
theme={theme} <Menu.Item key='1' icon={<HomeOutlined />}>
defaultSelectedKeys={[defaultKey]}
mode="inline"
>
<Menu.Item key="1" icon={<HomeOutlined />}>
<Link <Link
to={{ to={{
pathname: "/", pathname: '/',
}} }}
> >
{LABELS.MENU_HOME} {LABELS.MENU_HOME}
</Link> </Link>
</Menu.Item> </Menu.Item>
<Menu.Item key="2" icon={<PieChartOutlined />}> <Menu.Item key='2' icon={<PieChartOutlined />}>
<Link <Link
to={{ to={{
pathname: "/dashboard", pathname: '/dashboard',
}} }}
> >
{LABELS.MENU_DASHBOARD} {LABELS.MENU_DASHBOARD}
</Link> </Link>
</Menu.Item> </Menu.Item>
<Menu.Item key="3" icon={<BankOutlined />}> <Menu.Item key='3' icon={<BankOutlined />}>
<Link <Link
to={{ to={{
pathname: "/deposit", pathname: '/deposit',
}} }}
> >
{LABELS.MENU_DEPOSIT} {LABELS.MENU_DEPOSIT}
</Link> </Link>
</Menu.Item> </Menu.Item>
<Menu.Item key="4" icon={<LogoutOutlined />}> <Menu.Item key='4' icon={<LogoutOutlined />}>
<Link <Link
to={{ to={{
pathname: "/borrow", pathname: '/borrow',
}} }}
> >
{LABELS.MENU_BORROW} {LABELS.MENU_BORROW}
</Link> </Link>
</Menu.Item> </Menu.Item>
<Menu.Item key="5" icon={<ShoppingOutlined />}> <Menu.Item key='5' icon={<ShoppingOutlined />}>
<Link <Link
to={{ to={{
pathname: "/liquidate", pathname: '/liquidate',
}} }}
> >
{LABELS.MENU_LIQUIDATE} {LABELS.MENU_LIQUIDATE}
@ -123,11 +117,11 @@ export const AppLayout = React.memo((props: any) => {
{LABELS.MARGIN_TRADING} {LABELS.MARGIN_TRADING}
</Link> </Link>
</Menu.Item> */} </Menu.Item> */}
{env !== "mainnet-beta" && ( {env !== 'mainnet-beta' && (
<Menu.Item key="7" icon={<RocketOutlined />}> <Menu.Item key='7' icon={<RocketOutlined />}>
<Link <Link
to={{ to={{
pathname: "/faucet", pathname: '/faucet',
}} }}
> >
{LABELS.MENU_FAUCET} {LABELS.MENU_FAUCET}
@ -139,27 +133,17 @@ export const AppLayout = React.memo((props: any) => {
theme={theme} theme={theme}
defaultSelectedKeys={[defaultKey]} defaultSelectedKeys={[defaultKey]}
selectable={false} selectable={false}
mode="inline" mode='inline'
className="bottom-links" className='bottom-links'
> >
<Menu.Item key="16" icon={<ForkOutlined />}> <Menu.Item key='16' icon={<ForkOutlined />}>
<a <a title='Fork' href={`${config.repository.url}/fork`} target='_blank' rel='noopener noreferrer'>
title="Fork"
href={`${config.repository.url}/fork`}
target="_blank"
rel="noopener noreferrer"
>
Fork Fork
</a> </a>
</Menu.Item> </Menu.Item>
, ,
<Menu.Item key="15" icon={<GithubOutlined />}> <Menu.Item key='15' icon={<GithubOutlined />}>
<a <a title='Gtihub' href={config.repository.url} target='_blank' rel='noopener noreferrer'>
title="Gtihub"
href={config.repository.url}
target="_blank"
rel="noopener noreferrer"
>
Github Github
</a> </a>
</Menu.Item> </Menu.Item>

View File

@ -1,26 +1,21 @@
import { Slider } from "antd"; import { Slider } from 'antd';
import Card from "antd/lib/card"; import Card from 'antd/lib/card';
import React, { useCallback, useEffect } from "react"; import React, { useCallback, useEffect } from 'react';
import { useState } from "react"; import { useState } from 'react';
import { LABELS, marks } from "../../constants"; import { LABELS, marks } from '../../constants';
import { ParsedAccount, useMint } from "../../contexts/accounts"; import { ParsedAccount, useMint } from 'common/src/contexts/accounts';
import { import { EnrichedLendingObligation, InputType, useSliderInput, useUserBalance } from '../../hooks';
EnrichedLendingObligation, import { LendingReserve } from '../../models';
InputType, import { ActionConfirmation } from '../ActionConfirmation';
useSliderInput, import { liquidate } from '../../actions';
useUserBalance, import './style.less';
} from "../../hooks"; import { useConnection } from 'common/src/contexts/connection';
import { LendingReserve } from "../../models"; import { useWallet } from 'common/src/contexts/wallet';
import { ActionConfirmation } from "../ActionConfirmation"; import { fromLamports, wadToLamports } from 'common/src/utils/utils';
import { liquidate } from "../../actions"; import CollateralInput from '../CollateralInput';
import "./style.less"; import { notify } from 'common/src/utils/notifications';
import { useConnection } from "../../contexts/connection"; import { ConnectButton } from '../ConnectButton';
import { useWallet } from "../../contexts/wallet"; import { useMidPriceInUSD } from '../../contexts/market';
import { fromLamports, wadToLamports } from "../../utils/utils";
import CollateralInput from "../CollateralInput";
import { notify } from "../../utils/notifications";
import { ConnectButton } from "../ConnectButton";
import { useMidPriceInUSD } from "../../contexts/market";
export const LiquidateInput = (props: { export const LiquidateInput = (props: {
className?: string; className?: string;
@ -31,26 +26,22 @@ export const LiquidateInput = (props: {
const connection = useConnection(); const connection = useConnection();
const { wallet } = useWallet(); const { wallet } = useWallet();
const { repayReserve, withdrawReserve, obligation } = props; const { repayReserve, withdrawReserve, obligation } = props;
const [lastTyped, setLastTyped] = useState("liquidate"); const [lastTyped, setLastTyped] = useState('liquidate');
const [pendingTx, setPendingTx] = useState(false); const [pendingTx, setPendingTx] = useState(false);
const [showConfirmation, setShowConfirmation] = useState(false); const [showConfirmation, setShowConfirmation] = useState(false);
const [collateralValue, setCollateralValue] = useState(""); const [collateralValue, setCollateralValue] = useState('');
const liquidityMint = useMint(repayReserve.info.liquidityMint); const liquidityMint = useMint(repayReserve.info.liquidityMint);
const { accounts: fromAccounts, balance: tokenBalance } = useUserBalance( const { accounts: fromAccounts, balance: tokenBalance } = useUserBalance(repayReserve?.info.liquidityMint);
repayReserve?.info.liquidityMint const borrowAmountLamports = wadToLamports(obligation.info.borrowAmountWad).toNumber();
);
const borrowAmountLamports = wadToLamports(
obligation.info.borrowAmountWad
).toNumber();
const borrowAmount = fromLamports(borrowAmountLamports, liquidityMint); const borrowAmount = fromLamports(borrowAmountLamports, liquidityMint);
const convert = useCallback( const convert = useCallback(
(val: string | number) => { (val: string | number) => {
const minAmount = Math.min(tokenBalance || Infinity, borrowAmount); const minAmount = Math.min(tokenBalance || Infinity, borrowAmount);
setLastTyped("liquidate"); setLastTyped('liquidate');
if (typeof val === "string") { if (typeof val === 'string') {
return (parseFloat(val) / minAmount) * 100; return (parseFloat(val) / minAmount) * 100;
} else { } else {
return (val * minAmount) / 100; return (val * minAmount) / 100;
@ -73,9 +64,7 @@ export const LiquidateInput = (props: {
const toLiquidateLamports = const toLiquidateLamports =
type === InputType.Percent && tokenBalance >= borrowAmount type === InputType.Percent && tokenBalance >= borrowAmount
? (pct * borrowAmountLamports) / 100 ? (pct * borrowAmountLamports) / 100
: Math.ceil( : Math.ceil(borrowAmountLamports * (parseFloat(value) / borrowAmount));
borrowAmountLamports * (parseFloat(value) / borrowAmount)
);
await liquidate( await liquidate(
connection, connection,
wallet, wallet,
@ -87,14 +76,14 @@ export const LiquidateInput = (props: {
withdrawReserve withdrawReserve
); );
setValue(""); setValue('');
setCollateralValue(""); setCollateralValue('');
setShowConfirmation(true); setShowConfirmation(true);
} catch (error) { } catch (error) {
// TODO: // TODO:
notify({ notify({
message: "Unable to liquidate loan.", message: 'Unable to liquidate loan.',
type: "error", type: 'error',
description: error.message, description: error.message,
}); });
} finally { } finally {
@ -117,12 +106,10 @@ export const LiquidateInput = (props: {
type, type,
]); ]);
const collateralPrice = useMidPriceInUSD( const collateralPrice = useMidPriceInUSD(withdrawReserve?.info.liquidityMint.toBase58())?.price;
withdrawReserve?.info.liquidityMint.toBase58()
)?.price;
useEffect(() => { useEffect(() => {
if (withdrawReserve && lastTyped === "liquidate") { if (withdrawReserve && lastTyped === 'liquidate') {
const collateralInQuote = obligation.info.collateralInQuote; const collateralInQuote = obligation.info.collateralInQuote;
const collateral = collateralInQuote / collateralPrice; const collateral = collateralInQuote / collateralPrice;
if (value) { if (value) {
@ -130,29 +117,21 @@ export const LiquidateInput = (props: {
const collateralAmount = (borrowRatio * collateral) / 100; const collateralAmount = (borrowRatio * collateral) / 100;
setCollateralValue(collateralAmount.toString()); setCollateralValue(collateralAmount.toString());
} else { } else {
setCollateralValue(""); setCollateralValue('');
} }
} }
}, [ }, [borrowAmount, collateralPrice, withdrawReserve, lastTyped, obligation.info.collateralInQuote, value]);
borrowAmount,
collateralPrice,
withdrawReserve,
lastTyped,
obligation.info.collateralInQuote,
value,
]);
useEffect(() => { useEffect(() => {
if (withdrawReserve && lastTyped === "collateral") { if (withdrawReserve && lastTyped === 'collateral') {
const collateralInQuote = obligation.info.collateralInQuote; const collateralInQuote = obligation.info.collateralInQuote;
const collateral = collateralInQuote / collateralPrice; const collateral = collateralInQuote / collateralPrice;
if (collateralValue) { if (collateralValue) {
const collateralRatio = const collateralRatio = (parseFloat(collateralValue) / collateral) * 100;
(parseFloat(collateralValue) / collateral) * 100;
const borrowValue = (collateralRatio * borrowAmount) / 100; const borrowValue = (collateralRatio * borrowAmount) / 100;
setValue(borrowValue.toString()); setValue(borrowValue.toString());
} else { } else {
setValue(""); setValue('');
} }
} }
}, [ }, [
@ -167,11 +146,11 @@ export const LiquidateInput = (props: {
if (!withdrawReserve) return null; if (!withdrawReserve) return null;
const bodyStyle: React.CSSProperties = { const bodyStyle: React.CSSProperties = {
display: "flex", display: 'flex',
flex: 1, flex: 1,
justifyContent: "center", justifyContent: 'center',
alignItems: "center", alignItems: 'center',
height: "100%", height: '100%',
}; };
return ( return (
@ -181,27 +160,27 @@ export const LiquidateInput = (props: {
) : ( ) : (
<div <div
style={{ style={{
display: "flex", display: 'flex',
flexDirection: "column", flexDirection: 'column',
justifyContent: "space-around", justifyContent: 'space-around',
}} }}
> >
<div className="repay-input-title">{LABELS.LIQUIDATE_QUESTION}</div> <div className='repay-input-title'>{LABELS.LIQUIDATE_QUESTION}</div>
<div <div
style={{ style={{
display: "flex", display: 'flex',
flexDirection: "row", flexDirection: 'row',
justifyContent: "space-evenly", justifyContent: 'space-evenly',
alignItems: "center", alignItems: 'center',
}} }}
> >
<CollateralInput <CollateralInput
title="Liquidate Amount" title='Liquidate Amount'
reserve={repayReserve.info} reserve={repayReserve.info}
amount={parseFloat(value) || 0} amount={parseFloat(value) || 0}
onInputChange={(val: number | null) => { onInputChange={(val: number | null) => {
setValue(val?.toString() || ""); setValue(val?.toString() || '');
setLastTyped("liquidate"); setLastTyped('liquidate');
}} }}
disabled={true} disabled={true}
useWalletBalance={true} useWalletBalance={true}
@ -210,28 +189,28 @@ export const LiquidateInput = (props: {
<Slider marks={marks} value={pct} onChange={setPct} /> <Slider marks={marks} value={pct} onChange={setPct} />
<div <div
style={{ style={{
display: "flex", display: 'flex',
flexDirection: "row", flexDirection: 'row',
justifyContent: "space-evenly", justifyContent: 'space-evenly',
alignItems: "center", alignItems: 'center',
marginBottom: 20, marginBottom: 20,
}} }}
> >
<CollateralInput <CollateralInput
title="Collateral Amount (estimated)" title='Collateral Amount (estimated)'
reserve={withdrawReserve?.info} reserve={withdrawReserve?.info}
amount={parseFloat(collateralValue) || 0} amount={parseFloat(collateralValue) || 0}
onInputChange={(val: number | null) => { onInputChange={(val: number | null) => {
setCollateralValue(val?.toString() || ""); setCollateralValue(val?.toString() || '');
setLastTyped("collateral"); setLastTyped('collateral');
}} }}
disabled={true} disabled={true}
hideBalance={true} hideBalance={true}
/> />
</div> </div>
<ConnectButton <ConnectButton
type="primary" type='primary'
size="large" size='large'
onClick={onLiquidate} onClick={onLiquidate}
loading={pendingTx} loading={pendingTx}
disabled={fromAccounts.length === 0} disabled={fromAccounts.length === 0}

View File

@ -1,24 +1,13 @@
import { Card, Col, Row, Statistic } from "antd"; import { Card, Col, Row, Statistic } from 'antd';
import { import { formatNumber, formatPct, fromLamports, wadToLamports } from 'common/src/utils/utils';
formatNumber, import React, { useMemo } from 'react';
formatPct, import { EnrichedLendingObligation, useLendingReserve } from '../../hooks';
fromLamports, import { useTokenName } from 'common/src/hooks';
wadToLamports, import { useMint } from 'common/src/contexts/accounts';
} from "../../utils/utils"; import { calculateBorrowAPY, collateralToLiquidity } from '../../models';
import React, { useMemo } from "react"; import { GUTTER } from '../../constants';
import {
EnrichedLendingObligation,
useLendingReserve,
useTokenName,
} from "../../hooks";
import { useMint } from "../../contexts/accounts";
import { calculateBorrowAPY, collateralToLiquidity } from "../../models";
import { GUTTER } from "../../constants";
export const LoanInfoLine = (props: { export const LoanInfoLine = (props: { className?: string; obligation: EnrichedLendingObligation }) => {
className?: string;
obligation: EnrichedLendingObligation;
}) => {
const obligation = props.obligation; const obligation = props.obligation;
const repayReserve = useLendingReserve(obligation?.info.borrowReserve); const repayReserve = useLendingReserve(obligation?.info.borrowReserve);
@ -29,21 +18,12 @@ export const LoanInfoLine = (props: {
const repayName = useTokenName(repayReserve?.info.liquidityMint); const repayName = useTokenName(repayReserve?.info.liquidityMint);
const withdrawName = useTokenName(withdrawReserve?.info.liquidityMint); const withdrawName = useTokenName(withdrawReserve?.info.liquidityMint);
const borrowAPY = useMemo( const borrowAPY = useMemo(() => (repayReserve ? calculateBorrowAPY(repayReserve?.info) : 0), [repayReserve]);
() => (repayReserve ? calculateBorrowAPY(repayReserve?.info) : 0),
[repayReserve]
);
if (!obligation || !repayReserve) { if (!obligation || !repayReserve) {
return null; return null;
} }
const borrowAmount = fromLamports( const borrowAmount = fromLamports(wadToLamports(obligation?.info.borrowAmountWad), liquidityMint);
wadToLamports(obligation?.info.borrowAmountWad), const collateralLamports = collateralToLiquidity(obligation?.info.depositedCollateral, repayReserve.info);
liquidityMint
);
const collateralLamports = collateralToLiquidity(
obligation?.info.depositedCollateral,
repayReserve.info
);
const collateral = fromLamports(collateralLamports, collateralMint); const collateral = fromLamports(collateralLamports, collateralMint);
return ( return (
@ -51,16 +31,14 @@ export const LoanInfoLine = (props: {
<Col xs={24} xl={5}> <Col xs={24} xl={5}>
<Card className={props.className}> <Card className={props.className}>
<Statistic <Statistic
title="Loan Balance" title='Loan Balance'
value={obligation.info.borrowedInQuote} value={obligation.info.borrowedInQuote}
formatter={(val) => ( formatter={(val) => (
<div> <div>
<div> <div>
<em>{formatNumber.format(borrowAmount)}</em> {repayName} <em>{formatNumber.format(borrowAmount)}</em> {repayName}
</div> </div>
<div className="dashboard-amount-quote"> <div className='dashboard-amount-quote'>${formatNumber.format(parseFloat(val.toString()))}</div>
${formatNumber.format(parseFloat(val.toString()))}
</div>
</div> </div>
)} )}
/> />
@ -69,16 +47,14 @@ export const LoanInfoLine = (props: {
<Col xs={24} xl={5}> <Col xs={24} xl={5}>
<Card className={props.className}> <Card className={props.className}>
<Statistic <Statistic
title="Collateral" title='Collateral'
value={obligation.info.collateralInQuote} value={obligation.info.collateralInQuote}
formatter={(val) => ( formatter={(val) => (
<div> <div>
<div> <div>
<em>{formatNumber.format(collateral)}</em> {withdrawName} <em>{formatNumber.format(collateral)}</em> {withdrawName}
</div> </div>
<div className="dashboard-amount-quote"> <div className='dashboard-amount-quote'>${formatNumber.format(parseFloat(val.toString()))}</div>
${formatNumber.format(parseFloat(val.toString()))}
</div>
</div> </div>
)} )}
/> />
@ -86,15 +62,12 @@ export const LoanInfoLine = (props: {
</Col> </Col>
<Col xs={24} xl={5}> <Col xs={24} xl={5}>
<Card className={props.className}> <Card className={props.className}>
<Statistic title="APY" value={formatPct.format(borrowAPY)} /> <Statistic title='APY' value={formatPct.format(borrowAPY)} />
</Card> </Card>
</Col> </Col>
<Col xs={24} xl={9}> <Col xs={24} xl={9}>
<Card className={props.className}> <Card className={props.className}>
<Statistic <Statistic title='Health Factor' value={obligation.info.health.toFixed(2)} />
title="Health Factor"
value={obligation.info.health.toFixed(2)}
/>
</Card> </Card>
</Col> </Col>
</Row> </Row>

View File

@ -1,16 +1,14 @@
import { Card, Row, Col } from "antd"; import { Card, Row, Col } from 'antd';
import React, { useMemo } from "react"; import React, { useMemo } from 'react';
import { useMint } from "../../contexts/accounts"; import { useMint } from 'common/src/contexts/accounts';
import { useEnrichedPools } from "../../contexts/market"; import { useEnrichedPools } from '../../contexts/market';
import { useUserAccounts } from "../../hooks"; import { useUserAccounts } from 'common/src/hooks';
import { PoolInfo } from "../../models"; import { PoolInfo } from '../../models';
import { formatPriceNumber } from "../../utils/utils"; import { formatPriceNumber } from 'common/src/utils/utils';
export const PoolPrice = (props: { pool: PoolInfo }) => { export const PoolPrice = (props: { pool: PoolInfo }) => {
const pool = props.pool; const pool = props.pool;
const pools = useMemo(() => [props.pool].filter((p) => p) as PoolInfo[], [ const pools = useMemo(() => [props.pool].filter((p) => p) as PoolInfo[], [props.pool]);
props.pool,
]);
const enriched = useEnrichedPools(pools)[0]; const enriched = useEnrichedPools(pools)[0];
const { userAccounts } = useUserAccounts(); const { userAccounts } = useUserAccounts();
@ -19,37 +17,32 @@ export const PoolPrice = (props: { pool: PoolInfo }) => {
const ratio = const ratio =
userAccounts userAccounts
.filter((f) => pool.pubkeys.mint.equals(f.info.mint)) .filter((f) => pool.pubkeys.mint.equals(f.info.mint))
.reduce((acc, item) => item.info.amount.toNumber() + acc, 0) / .reduce((acc, item) => item.info.amount.toNumber() + acc, 0) / (lpMint?.supply.toNumber() || 0);
(lpMint?.supply.toNumber() || 0);
if (!enriched) { if (!enriched) {
return null; return null;
} }
return ( return (
<Card <Card
className="ccy-input" className='ccy-input'
style={{ borderRadius: 20, width: "100%" }} style={{ borderRadius: 20, width: '100%' }}
bodyStyle={{ padding: "7px" }} bodyStyle={{ padding: '7px' }}
size="small" size='small'
title="Prices and pool share" title='Prices and pool share'
> >
<Row style={{ width: "100%" }}> <Row style={{ width: '100%' }}>
<Col span={8}> <Col span={8}>
{formatPriceNumber.format( {formatPriceNumber.format(parseFloat(enriched.liquidityA) / parseFloat(enriched.liquidityB))}
parseFloat(enriched.liquidityA) / parseFloat(enriched.liquidityB)
)}
</Col> </Col>
<Col span={8}> <Col span={8}>
{formatPriceNumber.format( {formatPriceNumber.format(parseFloat(enriched.liquidityB) / parseFloat(enriched.liquidityA))}
parseFloat(enriched.liquidityB) / parseFloat(enriched.liquidityA)
)}
</Col> </Col>
<Col span={8}> <Col span={8}>
{ratio * 100 < 0.001 && ratio > 0 ? "<" : ""} {ratio * 100 < 0.001 && ratio > 0 ? '<' : ''}
&nbsp;{formatPriceNumber.format(ratio * 100)}% &nbsp;{formatPriceNumber.format(ratio * 100)}%
</Col> </Col>
</Row> </Row>
<Row style={{ width: "100%" }}> <Row style={{ width: '100%' }}>
<Col span={8}> <Col span={8}>
{enriched.names[0]} per {enriched.names[1]} {enriched.names[0]} per {enriched.names[1]}
</Col> </Col>

View File

@ -1,25 +1,20 @@
import React, { useCallback, useEffect, useState } from "react"; import React, { useCallback, useEffect, useState } from 'react';
import { import { EnrichedLendingObligation, InputType, useSliderInput, useUserBalance } from '../../hooks';
EnrichedLendingObligation, import { useAccountByMint } from 'common/src/hooks';
InputType, import { LendingReserve } from '../../models';
useAccountByMint, import { Card, Slider } from 'antd';
useSliderInput, import { ParsedAccount, useMint } from 'common/src/contexts/accounts';
useUserBalance, import { useConnection } from 'common/src/contexts/connection';
} from "../../hooks"; import { useWallet } from 'common/src/contexts/wallet';
import { LendingReserve } from "../../models"; import { repay } from '../../actions';
import { Card, Slider } from "antd"; import './style.less';
import { ParsedAccount, useMint } from "../../contexts/accounts"; import { LABELS, marks } from '../../constants';
import { useConnection } from "../../contexts/connection"; import { ActionConfirmation } from './../ActionConfirmation';
import { useWallet } from "../../contexts/wallet"; import { fromLamports, wadToLamports } from 'common/src/utils/utils';
import { repay } from "../../actions"; import { notify } from 'common/src/utils/notifications';
import "./style.less"; import { ConnectButton } from '../ConnectButton';
import { LABELS, marks } from "../../constants"; import CollateralInput from '../CollateralInput';
import { ActionConfirmation } from "./../ActionConfirmation"; import { useMidPriceInUSD } from '../../contexts/market';
import { fromLamports, wadToLamports } from "../../utils/utils";
import { notify } from "../../utils/notifications";
import { ConnectButton } from "../ConnectButton";
import CollateralInput from "../CollateralInput";
import { useMidPriceInUSD } from "../../contexts/market";
export const RepayInput = (props: { export const RepayInput = (props: {
className?: string; className?: string;
@ -29,36 +24,30 @@ export const RepayInput = (props: {
}) => { }) => {
const connection = useConnection(); const connection = useConnection();
const { wallet } = useWallet(); const { wallet } = useWallet();
const [lastTyped, setLastTyped] = useState("repay"); const [lastTyped, setLastTyped] = useState('repay');
const [pendingTx, setPendingTx] = useState(false); const [pendingTx, setPendingTx] = useState(false);
const [showConfirmation, setShowConfirmation] = useState(false); const [showConfirmation, setShowConfirmation] = useState(false);
const [collateralValue, setCollateralValue] = useState(""); const [collateralValue, setCollateralValue] = useState('');
const repayReserve = props.borrowReserve; const repayReserve = props.borrowReserve;
const obligation = props.obligation; const obligation = props.obligation;
const liquidityMint = useMint(repayReserve.info.liquidityMint); const liquidityMint = useMint(repayReserve.info.liquidityMint);
const { balance: tokenBalance } = useUserBalance( const { balance: tokenBalance } = useUserBalance(repayReserve.info.liquidityMint);
repayReserve.info.liquidityMint
);
const borrowAmountLamports = wadToLamports( const borrowAmountLamports = wadToLamports(obligation.info.borrowAmountWad).toNumber();
obligation.info.borrowAmountWad
).toNumber();
const borrowAmount = fromLamports(borrowAmountLamports, liquidityMint); const borrowAmount = fromLamports(borrowAmountLamports, liquidityMint);
const collateralReserve = props.collateralReserve; const collateralReserve = props.collateralReserve;
const { accounts: fromAccounts } = useUserBalance( const { accounts: fromAccounts } = useUserBalance(repayReserve.info.liquidityMint);
repayReserve.info.liquidityMint
);
const obligationAccount = useAccountByMint(obligation?.info.tokenMint); const obligationAccount = useAccountByMint(obligation?.info.tokenMint);
const convert = useCallback( const convert = useCallback(
(val: string | number) => { (val: string | number) => {
const minAmount = Math.min(tokenBalance || Infinity, borrowAmount); const minAmount = Math.min(tokenBalance || Infinity, borrowAmount);
setLastTyped("repay"); setLastTyped('repay');
if (typeof val === "string") { if (typeof val === 'string') {
return (parseFloat(val) / minAmount) * 100; return (parseFloat(val) / minAmount) * 100;
} else { } else {
return (val * minAmount) / 100; return (val * minAmount) / 100;
@ -70,12 +59,7 @@ export const RepayInput = (props: {
const { value, setValue, pct, setPct, type } = useSliderInput(convert); const { value, setValue, pct, setPct, type } = useSliderInput(convert);
const onRepay = useCallback(() => { const onRepay = useCallback(() => {
if ( if (!collateralReserve || !obligation || !repayReserve || !obligationAccount) {
!collateralReserve ||
!obligation ||
!repayReserve ||
!obligationAccount
) {
return; return;
} }
@ -86,9 +70,7 @@ export const RepayInput = (props: {
const toRepayLamports = const toRepayLamports =
type === InputType.Percent type === InputType.Percent
? (pct * borrowAmountLamports) / 100 ? (pct * borrowAmountLamports) / 100
: Math.ceil( : Math.ceil(borrowAmountLamports * (parseFloat(value) / borrowAmount));
borrowAmountLamports * (parseFloat(value) / borrowAmount)
);
await repay( await repay(
fromAccounts[0], fromAccounts[0],
toRepayLamports, toRepayLamports,
@ -100,13 +82,13 @@ export const RepayInput = (props: {
wallet wallet
); );
setValue(""); setValue('');
setCollateralValue(""); setCollateralValue('');
setShowConfirmation(true); setShowConfirmation(true);
} catch (error) { } catch (error) {
notify({ notify({
message: "Unable to repay loan.", message: 'Unable to repay loan.',
type: "error", type: 'error',
description: error.message, description: error.message,
}); });
} finally { } finally {
@ -129,12 +111,10 @@ export const RepayInput = (props: {
wallet, wallet,
]); ]);
const collateralPrice = useMidPriceInUSD( const collateralPrice = useMidPriceInUSD(collateralReserve?.info.liquidityMint.toBase58())?.price;
collateralReserve?.info.liquidityMint.toBase58()
)?.price;
useEffect(() => { useEffect(() => {
if (collateralReserve && lastTyped === "repay") { if (collateralReserve && lastTyped === 'repay') {
const collateralInQuote = obligation.info.collateralInQuote; const collateralInQuote = obligation.info.collateralInQuote;
const collateral = collateralInQuote / collateralPrice; const collateral = collateralInQuote / collateralPrice;
if (value) { if (value) {
@ -142,29 +122,21 @@ export const RepayInput = (props: {
const collateralAmount = (borrowRatio * collateral) / 100; const collateralAmount = (borrowRatio * collateral) / 100;
setCollateralValue(collateralAmount.toString()); setCollateralValue(collateralAmount.toString());
} else { } else {
setCollateralValue(""); setCollateralValue('');
} }
} }
}, [ }, [borrowAmount, collateralPrice, collateralReserve, lastTyped, obligation.info.collateralInQuote, value]);
borrowAmount,
collateralPrice,
collateralReserve,
lastTyped,
obligation.info.collateralInQuote,
value,
]);
useEffect(() => { useEffect(() => {
if (collateralReserve && lastTyped === "collateral") { if (collateralReserve && lastTyped === 'collateral') {
const collateralInQuote = obligation.info.collateralInQuote; const collateralInQuote = obligation.info.collateralInQuote;
const collateral = collateralInQuote / collateralPrice; const collateral = collateralInQuote / collateralPrice;
if (collateralValue) { if (collateralValue) {
const collateralRatio = const collateralRatio = (parseFloat(collateralValue) / collateral) * 100;
(parseFloat(collateralValue) / collateral) * 100;
const borrowValue = (collateralRatio * borrowAmount) / 100; const borrowValue = (collateralRatio * borrowAmount) / 100;
setValue(borrowValue.toString()); setValue(borrowValue.toString());
} else { } else {
setValue(""); setValue('');
} }
} }
}, [ }, [
@ -178,11 +150,11 @@ export const RepayInput = (props: {
]); ]);
const bodyStyle: React.CSSProperties = { const bodyStyle: React.CSSProperties = {
display: "flex", display: 'flex',
flex: 1, flex: 1,
justifyContent: "center", justifyContent: 'center',
alignItems: "center", alignItems: 'center',
height: "100%", height: '100%',
}; };
return ( return (
@ -192,27 +164,27 @@ export const RepayInput = (props: {
) : ( ) : (
<div <div
style={{ style={{
display: "flex", display: 'flex',
flexDirection: "column", flexDirection: 'column',
justifyContent: "space-around", justifyContent: 'space-around',
}} }}
> >
<div className="repay-input-title">{LABELS.REPAY_QUESTION}</div> <div className='repay-input-title'>{LABELS.REPAY_QUESTION}</div>
<div <div
style={{ style={{
display: "flex", display: 'flex',
flexDirection: "row", flexDirection: 'row',
justifyContent: "space-evenly", justifyContent: 'space-evenly',
alignItems: "center", alignItems: 'center',
}} }}
> >
<CollateralInput <CollateralInput
title="Repay Amount" title='Repay Amount'
reserve={repayReserve.info} reserve={repayReserve.info}
amount={parseFloat(value) || 0} amount={parseFloat(value) || 0}
onInputChange={(val: number | null) => { onInputChange={(val: number | null) => {
setValue(val?.toString() || ""); setValue(val?.toString() || '');
setLastTyped("repay"); setLastTyped('repay');
}} }}
disabled={true} disabled={true}
useWalletBalance={true} useWalletBalance={true}
@ -221,28 +193,28 @@ export const RepayInput = (props: {
<Slider marks={marks} value={pct} onChange={setPct} /> <Slider marks={marks} value={pct} onChange={setPct} />
<div <div
style={{ style={{
display: "flex", display: 'flex',
flexDirection: "row", flexDirection: 'row',
justifyContent: "space-evenly", justifyContent: 'space-evenly',
alignItems: "center", alignItems: 'center',
marginBottom: 20, marginBottom: 20,
}} }}
> >
<CollateralInput <CollateralInput
title="Collateral Amount (estimated)" title='Collateral Amount (estimated)'
reserve={collateralReserve?.info} reserve={collateralReserve?.info}
amount={parseFloat(collateralValue) || 0} amount={parseFloat(collateralValue) || 0}
onInputChange={(val: number | null) => { onInputChange={(val: number | null) => {
setCollateralValue(val?.toString() || ""); setCollateralValue(val?.toString() || '');
setLastTyped("collateral"); setLastTyped('collateral');
}} }}
disabled={true} disabled={true}
hideBalance={true} hideBalance={true}
/> />
</div> </div>
<ConnectButton <ConnectButton
type="primary" type='primary'
size="large" size='large'
onClick={onRepay} onClick={onRepay}
loading={pendingTx} loading={pendingTx}
disabled={fromAccounts.length === 0} disabled={fromAccounts.length === 0}

View File

@ -1,51 +1,38 @@
import React from "react"; import React from 'react';
import { calculateDepositAPY, LendingReserve } from "../../models/lending"; import { calculateDepositAPY, LendingReserve } from '../../models/lending';
import { Card, Col, Row, Statistic } from "antd"; import { Card, Col, Row, Statistic } from 'antd';
import { PublicKey } from "@solana/web3.js"; import { PublicKey } from '@solana/web3.js';
import "./style.less"; import './style.less';
import { GUTTER, LABELS } from "../../constants"; import { GUTTER, LABELS } from '../../constants';
import { ReserveUtilizationChart } from "./../../components/ReserveUtilizationChart"; import { ReserveUtilizationChart } from './../../components/ReserveUtilizationChart';
import { useMemo } from "react"; import { useMemo } from 'react';
import { formatNumber, fromLamports, wadToLamports } from "../../utils/utils"; import { formatNumber, fromLamports, wadToLamports } from 'common/src/utils/utils';
import { useMint } from "../../contexts/accounts"; import { useMint } from 'common/src/contexts/accounts';
import { useMidPriceInUSD } from "../../contexts/market"; import { useMidPriceInUSD } from '../../contexts/market';
import { TokenIcon } from "../TokenIcon"; import { TokenIcon } from '../TokenIcon';
export const ReserveStatus = (props: { export const ReserveStatus = (props: { className?: string; reserve: LendingReserve; address: PublicKey }) => {
className?: string;
reserve: LendingReserve;
address: PublicKey;
}) => {
const bodyStyle: React.CSSProperties = { const bodyStyle: React.CSSProperties = {
display: "flex", display: 'flex',
justifyContent: "center", justifyContent: 'center',
alignItems: "center", alignItems: 'center',
}; };
const mintAddress = props.reserve.liquidityMint?.toBase58(); const mintAddress = props.reserve.liquidityMint?.toBase58();
const liquidityMint = useMint(mintAddress); const liquidityMint = useMint(mintAddress);
const { price } = useMidPriceInUSD(mintAddress); const { price } = useMidPriceInUSD(mintAddress);
const availableLiquidity = fromLamports( const availableLiquidity = fromLamports(props.reserve.state.availableLiquidity, liquidityMint);
props.reserve.state.availableLiquidity,
liquidityMint
);
const availableLiquidityInUSD = price * availableLiquidity; const availableLiquidityInUSD = price * availableLiquidity;
const totalBorrows = useMemo( const totalBorrows = useMemo(
() => () => fromLamports(wadToLamports(props.reserve.state.borrowedLiquidityWad), liquidityMint),
fromLamports(
wadToLamports(props.reserve.state.borrowedLiquidityWad),
liquidityMint
),
[props.reserve, liquidityMint] [props.reserve, liquidityMint]
); );
const totalBorrowsInUSD = price * totalBorrows; const totalBorrowsInUSD = price * totalBorrows;
const depositAPY = useMemo(() => calculateDepositAPY(props.reserve), [ const depositAPY = useMemo(() => calculateDepositAPY(props.reserve), [props.reserve]);
props.reserve,
]);
const liquidationThreshold = props.reserve.config.liquidationThreshold; const liquidationThreshold = props.reserve.config.liquidationThreshold;
const liquidationPenalty = props.reserve.config.liquidationBonus; const liquidationPenalty = props.reserve.config.liquidationBonus;
@ -60,7 +47,7 @@ export const ReserveStatus = (props: {
style={{ style={{
marginRight: 0, marginRight: 0,
marginTop: 0, marginTop: 0,
position: "absolute", position: 'absolute',
left: 15, left: 15,
}} }}
mintAddress={mintAddress} mintAddress={mintAddress}
@ -71,18 +58,16 @@ export const ReserveStatus = (props: {
} }
bodyStyle={bodyStyle} bodyStyle={bodyStyle}
> >
<div className="flexColumn"> <div className='flexColumn'>
<Row gutter={GUTTER}> <Row gutter={GUTTER}>
<Col span={12}> <Col span={12}>
<Statistic <Statistic
title="Available Liquidity" title='Available Liquidity'
value={availableLiquidity} value={availableLiquidity}
valueRender={(node) => ( valueRender={(node) => (
<div> <div>
{node} {node}
<div className="dashboard-amount-quote-stat"> <div className='dashboard-amount-quote-stat'>${formatNumber.format(availableLiquidityInUSD)}</div>
${formatNumber.format(availableLiquidityInUSD)}
</div>
</div> </div>
)} )}
precision={2} precision={2}
@ -90,14 +75,12 @@ export const ReserveStatus = (props: {
</Col> </Col>
<Col span={12}> <Col span={12}>
<Statistic <Statistic
title="Total Borrowed" title='Total Borrowed'
value={totalBorrows} value={totalBorrows}
valueRender={(node) => ( valueRender={(node) => (
<div> <div>
{node} {node}
<div className="dashboard-amount-quote-stat"> <div className='dashboard-amount-quote-stat'>${formatNumber.format(totalBorrowsInUSD)}</div>
${formatNumber.format(totalBorrowsInUSD)}
</div>
</div> </div>
)} )}
precision={2} precision={2}
@ -108,9 +91,9 @@ export const ReserveStatus = (props: {
<Col <Col
span={24} span={24}
style={{ style={{
display: "flex", display: 'flex',
flexDirection: "column", flexDirection: 'column',
justifyContent: "space-around", justifyContent: 'space-around',
}} }}
> >
<ReserveUtilizationChart reserve={props.reserve} /> <ReserveUtilizationChart reserve={props.reserve} />
@ -118,39 +101,33 @@ export const ReserveStatus = (props: {
</Row> </Row>
<Row gutter={GUTTER}> <Row gutter={GUTTER}>
<Col span={6}> <Col span={6}>
<Statistic <Statistic title={LABELS.MAX_LTV} className='small-statisitc' value={maxLTV} precision={2} suffix='%' />
title={LABELS.MAX_LTV}
className="small-statisitc"
value={maxLTV}
precision={2}
suffix="%"
/>
</Col> </Col>
<Col span={6}> <Col span={6}>
<Statistic <Statistic
title={LABELS.LIQUIDATION_THRESHOLD} title={LABELS.LIQUIDATION_THRESHOLD}
className="small-statisitc" className='small-statisitc'
value={liquidationThreshold} value={liquidationThreshold}
precision={2} precision={2}
suffix="%" suffix='%'
/> />
</Col> </Col>
<Col span={6}> <Col span={6}>
<Statistic <Statistic
title={LABELS.LIQUIDATION_PENALTY} title={LABELS.LIQUIDATION_PENALTY}
className="small-statisitc" className='small-statisitc'
value={liquidationPenalty} value={liquidationPenalty}
precision={2} precision={2}
suffix="%" suffix='%'
/> />
</Col> </Col>
<Col span={6}> <Col span={6}>
<Statistic <Statistic
title={LABELS.TABLE_TITLE_DEPOSIT_APY} title={LABELS.TABLE_TITLE_DEPOSIT_APY}
className="small-statisitc" className='small-statisitc'
value={depositAPY * 100} value={depositAPY * 100}
precision={2} precision={2}
suffix="%" suffix='%'
/> />
</Col> </Col>
</Row> </Row>

View File

@ -1,24 +1,17 @@
import React, { useMemo } from "react"; import React, { useMemo } from 'react';
import { LendingReserve } from "../../models/lending"; import { LendingReserve } from '../../models/lending';
import { fromLamports, wadToLamports } from "../../utils/utils"; import { fromLamports, wadToLamports } from 'common/src/utils/utils';
import { useMint } from "../../contexts/accounts"; import { useMint } from 'common/src/contexts/accounts';
import { WaterWave } from "./../WaterWave"; import { WaterWave } from './../WaterWave';
import { Statistic } from "antd"; import { Statistic } from 'antd';
export const ReserveUtilizationChart = (props: { reserve: LendingReserve }) => { export const ReserveUtilizationChart = (props: { reserve: LendingReserve }) => {
const mintAddress = props.reserve.liquidityMint?.toBase58(); const mintAddress = props.reserve.liquidityMint?.toBase58();
const liquidityMint = useMint(mintAddress); const liquidityMint = useMint(mintAddress);
const availableLiquidity = fromLamports( const availableLiquidity = fromLamports(props.reserve.state.availableLiquidity, liquidityMint);
props.reserve.state.availableLiquidity,
liquidityMint
);
const totalBorrows = useMemo( const totalBorrows = useMemo(
() => () => fromLamports(wadToLamports(props.reserve.state.borrowedLiquidityWad), liquidityMint),
fromLamports(
wadToLamports(props.reserve.state.borrowedLiquidityWad),
liquidityMint
),
[props.reserve, liquidityMint] [props.reserve, liquidityMint]
); );
@ -29,14 +22,7 @@ export const ReserveUtilizationChart = (props: { reserve: LendingReserve }) => {
<WaterWave <WaterWave
style={{ height: 300 }} style={{ height: 300 }}
showPercent={false} showPercent={false}
title={ title={<Statistic title='Utilization' value={percent} suffix='%' precision={2} />}
<Statistic
title="Utilization"
value={percent}
suffix="%"
precision={2}
/>
}
percent={percent} percent={percent}
/> />
); );

View File

@ -1,7 +1,7 @@
import React from "react"; import React from 'react';
import { Select } from "antd"; import { Select } from 'antd';
import { ENDPOINTS, useConnectionConfig } from "../../contexts/connection"; import { ENDPOINTS, useConnectionConfig } from 'common/src/contexts/connection';
import { useWallet, WALLET_PROVIDERS } from "../../contexts/wallet"; import { useWallet, WALLET_PROVIDERS } from 'common/src/contexts/wallet';
export const Settings = () => { export const Settings = () => {
const { providerUrl, setProvider } = useWallet(); const { providerUrl, setProvider } = useWallet();
@ -9,13 +9,9 @@ export const Settings = () => {
return ( return (
<> <>
<div style={{ display: "grid" }}> <div style={{ display: 'grid' }}>
Network:{" "} Network:{' '}
<Select <Select onSelect={setEndpoint} value={endpoint} style={{ marginRight: 8 }}>
onSelect={setEndpoint}
value={endpoint}
style={{ marginRight: 8 }}
>
{ENDPOINTS.map(({ name, endpoint }) => ( {ENDPOINTS.map(({ name, endpoint }) => (
<Select.Option value={endpoint} key={endpoint}> <Select.Option value={endpoint} key={endpoint}>
{name} {name}
@ -23,8 +19,8 @@ export const Settings = () => {
))} ))}
</Select> </Select>
</div> </div>
<div style={{ display: "grid" }}> <div style={{ display: 'grid' }}>
Wallet:{" "} Wallet:{' '}
<Select onSelect={setProvider} value={providerUrl}> <Select onSelect={setProvider} value={providerUrl}>
{WALLET_PROVIDERS.map(({ name, url }) => ( {WALLET_PROVIDERS.map(({ name, url }) => (
<Select.Option value={url} key={url}> <Select.Option value={url} key={url}>

View File

@ -1,23 +1,23 @@
import React from "react"; import React from 'react';
import { useTokenName } from "./../../hooks"; import { useTokenName } from 'common/src/hooks';
import { import {
calculateBorrowAPY, calculateBorrowAPY,
calculateDepositAPY, calculateDepositAPY,
calculateUtilizationRatio, calculateUtilizationRatio,
LendingReserve, LendingReserve,
} from "../../models/lending"; } from '../../models/lending';
import { TokenIcon } from "../../components/TokenIcon"; import { TokenIcon } from '../../components/TokenIcon';
import { formatNumber, formatPct, fromLamports } from "../../utils/utils"; import { formatNumber, formatPct, fromLamports } from 'common/src/utils/utils';
import { Card, Typography } from "antd"; import { Card, Typography } from 'antd';
import { ParsedAccount, useMint } from "../../contexts/accounts"; import { ParsedAccount, useMint } from 'common/src/contexts/accounts';
import { Link } from "react-router-dom"; import { Link } from 'react-router-dom';
import { LABELS } from "../../constants"; import { LABELS } from '../../constants';
const { Text } = Typography; const { Text } = Typography;
export enum SideReserveOverviewMode { export enum SideReserveOverviewMode {
Deposit = "deposit", Deposit = 'deposit',
Borrow = "borrow", Borrow = 'borrow',
} }
export const SideReserveOverview = (props: { export const SideReserveOverview = (props: {
@ -30,10 +30,7 @@ export const SideReserveOverview = (props: {
const name = useTokenName(reserve?.liquidityMint); const name = useTokenName(reserve?.liquidityMint);
const liquidityMint = useMint(reserve.liquidityMint); const liquidityMint = useMint(reserve.liquidityMint);
const availableLiquidity = fromLamports( const availableLiquidity = fromLamports(reserve.state.availableLiquidity, liquidityMint);
reserve.state.availableLiquidity,
liquidityMint
);
const depositApy = calculateDepositAPY(reserve); const depositApy = calculateDepositAPY(reserve);
const borrowApr = calculateBorrowAPY(reserve); const borrowApr = calculateBorrowAPY(reserve);
@ -47,47 +44,43 @@ export const SideReserveOverview = (props: {
if (mode === SideReserveOverviewMode.Deposit) { if (mode === SideReserveOverviewMode.Deposit) {
extraInfo = ( extraInfo = (
<> <>
<div className="card-row"> <div className='card-row'>
<Text type="secondary" className="card-cell "> <Text type='secondary' className='card-cell '>
{LABELS.TABLE_TITLE_DEPOSIT_APY}: {LABELS.TABLE_TITLE_DEPOSIT_APY}:
</Text> </Text>
<div className="card-cell ">{formatPct.format(depositApy)}</div> <div className='card-cell '>{formatPct.format(depositApy)}</div>
</div> </div>
<div className="card-row"> <div className='card-row'>
<Text type="secondary" className="card-cell "> <Text type='secondary' className='card-cell '>
Maximum LTV: Maximum LTV:
</Text> </Text>
<div className="card-cell ">{formatPct.format(maxLTV)}</div> <div className='card-cell '>{formatPct.format(maxLTV)}</div>
</div> </div>
<div className="card-row"> <div className='card-row'>
<Text type="secondary" className="card-cell "> <Text type='secondary' className='card-cell '>
Liquidation threshold: Liquidation threshold:
</Text> </Text>
<div className="card-cell "> <div className='card-cell '>{formatPct.format(liquidationThreshold)}</div>
{formatPct.format(liquidationThreshold)}
</div>
</div> </div>
<div className="card-row"> <div className='card-row'>
<Text type="secondary" className="card-cell "> <Text type='secondary' className='card-cell '>
Liquidation penalty: Liquidation penalty:
</Text> </Text>
<div className="card-cell "> <div className='card-cell '>{formatPct.format(liquidationPenalty)}</div>
{formatPct.format(liquidationPenalty)}
</div>
</div> </div>
</> </>
); );
} else if (mode === SideReserveOverviewMode.Borrow) { } else if (mode === SideReserveOverviewMode.Borrow) {
extraInfo = ( extraInfo = (
<> <>
<div className="card-row"> <div className='card-row'>
<Text type="secondary" className="card-cell "> <Text type='secondary' className='card-cell '>
{LABELS.TABLE_TITLE_BORROW_APY}: {LABELS.TABLE_TITLE_BORROW_APY}:
</Text> </Text>
<div className="card-cell ">{formatPct.format(borrowApr)}</div> <div className='card-cell '>{formatPct.format(borrowApr)}</div>
</div> </div>
</> </>
); );
@ -99,34 +92,30 @@ export const SideReserveOverview = (props: {
title={ title={
<div <div
style={{ style={{
display: "flex", display: 'flex',
alignItems: "center", alignItems: 'center',
fontSize: "1.2rem", fontSize: '1.2rem',
justifyContent: "center", justifyContent: 'center',
}} }}
> >
<Link to={`/reserve/${props.reserve.pubkey}`}> <Link to={`/reserve/${props.reserve.pubkey}`}>
<TokenIcon <TokenIcon mintAddress={reserve?.liquidityMint} style={{ width: 30, height: 30 }} /> {name} Reserve Overview
mintAddress={reserve?.liquidityMint}
style={{ width: 30, height: 30 }}
/>{" "}
{name} Reserve Overview
</Link> </Link>
</div> </div>
} }
> >
<div className="card-row"> <div className='card-row'>
<Text type="secondary" className="card-cell "> <Text type='secondary' className='card-cell '>
Utilization rate: Utilization rate:
</Text> </Text>
<div className="card-cell ">{formatPct.format(utilizationRate)}</div> <div className='card-cell '>{formatPct.format(utilizationRate)}</div>
</div> </div>
<div className="card-row"> <div className='card-row'>
<Text type="secondary" className="card-cell "> <Text type='secondary' className='card-cell '>
Available liquidity: Available liquidity:
</Text> </Text>
<div className="card-cell "> <div className='card-cell '>
{formatNumber.format(availableLiquidity)} {name} {formatNumber.format(availableLiquidity)} {name}
</div> </div>
</div> </div>

View File

@ -1,8 +1,8 @@
import React, { useEffect, useMemo, useRef } from "react"; import React, { useEffect, useMemo, useRef } from 'react';
import { PoolInfo } from "../../models"; import { PoolInfo } from '../../models';
import echarts from "echarts"; import echarts from 'echarts';
import { formatNumber, formatUSD } from "../../utils/utils"; import { formatNumber, formatUSD } from 'common/src/utils/utils';
import { useEnrichedPools } from "../../contexts/market"; import { useEnrichedPools } from '../../contexts/market';
export const SupplyOverview = (props: { pool?: PoolInfo }) => { export const SupplyOverview = (props: { pool?: PoolInfo }) => {
const { pool } = props; const { pool } = props;
@ -44,7 +44,7 @@ export const SupplyOverview = (props: { pool?: PoolInfo }) => {
instance.setOption({ instance.setOption({
tooltip: { tooltip: {
trigger: "item", trigger: 'item',
formatter: function (params: any) { formatter: function (params: any) {
var val = formatUSD.format(params.value); var val = formatUSD.format(params.value);
var tokenAmount = formatNumber.format(params.data.tokens); var tokenAmount = formatNumber.format(params.data.tokens);
@ -53,8 +53,8 @@ export const SupplyOverview = (props: { pool?: PoolInfo }) => {
}, },
series: [ series: [
{ {
name: "Liquidity", name: 'Liquidity',
type: "pie", type: 'pie',
top: 0, top: 0,
bottom: 0, bottom: 0,
left: 0, left: 0,
@ -70,20 +70,20 @@ export const SupplyOverview = (props: { pool?: PoolInfo }) => {
}, },
rich: { rich: {
c: { c: {
color: "black", color: 'black',
lineHeight: 22, lineHeight: 22,
align: "center", align: 'center',
}, },
r: { r: {
color: "black", color: 'black',
align: "right", align: 'right',
}, },
}, },
color: "rgba(255, 255, 255, 0.5)", color: 'rgba(255, 255, 255, 0.5)',
}, },
itemStyle: { itemStyle: {
normal: { normal: {
borderColor: "#000", borderColor: '#000',
}, },
}, },
data, data,
@ -96,5 +96,5 @@ export const SupplyOverview = (props: { pool?: PoolInfo }) => {
return null; return null;
} }
return <div ref={chartDiv} style={{ height: 150, width: "100%" }} />; return <div ref={chartDiv} style={{ height: 150, width: '100%' }} />;
}; };

View File

@ -1,6 +1,6 @@
import React from "react"; import React from 'react';
import { useMint, useAccountByMint } from "../../contexts/accounts"; import { useMint, useAccountByMint } from 'common/src/contexts/accounts';
import { TokenIcon } from "../TokenIcon"; import { TokenIcon } from '../TokenIcon';
export const TokenDisplay = (props: { export const TokenDisplay = (props: {
name: string; name: string;
@ -16,8 +16,7 @@ export const TokenDisplay = (props: {
let hasBalance: boolean = false; let hasBalance: boolean = false;
if (showBalance) { if (showBalance) {
if (tokenAccount && tokenMint) { if (tokenAccount && tokenMint) {
balance = balance = tokenAccount.info.amount.toNumber() / Math.pow(10, tokenMint.decimals);
tokenAccount.info.amount.toNumber() / Math.pow(10, tokenMint.decimals);
hasBalance = balance > 0; hasBalance = balance > 0;
} }
} }
@ -28,27 +27,18 @@ export const TokenDisplay = (props: {
title={mintAddress} title={mintAddress}
key={mintAddress} key={mintAddress}
style={{ style={{
display: "flex", display: 'flex',
alignItems: "center", alignItems: 'center',
justifyContent: "space-between", justifyContent: 'space-between',
}} }}
> >
<div style={{ display: "flex", alignItems: "center" }}> <div style={{ display: 'flex', alignItems: 'center' }}>
{icon || <TokenIcon mintAddress={mintAddress} />} {icon || <TokenIcon mintAddress={mintAddress} />}
{name} {name}
</div> </div>
{showBalance ? ( {showBalance ? (
<span <span title={balance.toString()} key={mintAddress} className='token-balance'>
title={balance.toString()} &nbsp; {hasBalance ? (balance < 0.001 ? '<0.001' : balance.toFixed(3)) : '-'}
key={mintAddress}
className="token-balance"
>
&nbsp;{" "}
{hasBalance
? balance < 0.001
? "<0.001"
: balance.toFixed(3)
: "-"}
</span> </span>
) : null} ) : null}
</div> </div>

View File

@ -1,8 +1,8 @@
import { Identicon } from "../Identicon"; import { Identicon } from '../Identicon';
import React from "react"; import React from 'react';
import { getTokenIcon } from "../../utils/utils"; import { getTokenIcon } from 'common/src/utils/utils';
import { useConnectionConfig } from "../../contexts/connection"; import { useConnectionConfig } from 'common/src/contexts/connection';
import { PublicKey } from "@solana/web3.js"; import { PublicKey } from '@solana/web3.js';
export const TokenIcon = (props: { export const TokenIcon = (props: {
mintAddress?: string | PublicKey; mintAddress?: string | PublicKey;
@ -18,18 +18,18 @@ export const TokenIcon = (props: {
if (icon) { if (icon) {
return ( return (
<img <img
alt="Token icon" alt='Token icon'
className={props.className} className={props.className}
key={icon} key={icon}
width={props.style?.width || size.toString()} width={props.style?.width || size.toString()}
height={props.style?.height || size.toString()} height={props.style?.height || size.toString()}
src={icon} src={icon}
style={{ style={{
marginRight: "0.5rem", marginRight: '0.5rem',
marginTop: "0.11rem", marginTop: '0.11rem',
borderRadius: "10rem", borderRadius: '10rem',
backgroundColor: "white", backgroundColor: 'white',
backgroundClip: "padding-box", backgroundClip: 'padding-box',
...props.style, ...props.style,
}} }}
/> />
@ -40,7 +40,7 @@ export const TokenIcon = (props: {
<Identicon <Identicon
address={props.mintAddress} address={props.mintAddress}
style={{ style={{
marginRight: "0.5rem", marginRight: '0.5rem',
width: size, width: size,
height: size, height: size,
marginTop: 2, marginTop: 2,
@ -50,18 +50,10 @@ export const TokenIcon = (props: {
); );
}; };
export const PoolIcon = (props: { export const PoolIcon = (props: { mintA: string; mintB: string; style?: React.CSSProperties; className?: string }) => {
mintA: string;
mintB: string;
style?: React.CSSProperties;
className?: string;
}) => {
return ( return (
<div className={props.className} style={{ display: "flex" }}> <div className={props.className} style={{ display: 'flex' }}>
<TokenIcon <TokenIcon mintAddress={props.mintA} style={{ marginRight: '-0.5rem', ...props.style }} />
mintAddress={props.mintA}
style={{ marginRight: "-0.5rem", ...props.style }}
/>
<TokenIcon mintAddress={props.mintB} /> <TokenIcon mintAddress={props.mintB} />
</div> </div>
); );

View File

@ -1,50 +1,32 @@
import React from "react"; import React from 'react';
import { import {
useUserCollateralBalance, useUserCollateralBalance,
useTokenName,
useUserBalance, useUserBalance,
useBorrowedAmount, useBorrowedAmount,
useBorrowingPower, useBorrowingPower,
useUserObligationByReserve, useUserObligationByReserve,
} from "./../../hooks"; } from './../../hooks';
import { LendingReserve } from "../../models/lending"; import { useTokenName } from 'common/src/hooks';
import { formatNumber } from "../../utils/utils"; import { LendingReserve } from '../../models/lending';
import { Button, Card, Typography } from "antd"; import { formatNumber } from 'common/src/utils/utils';
import { Link } from "react-router-dom"; import { Button, Card, Typography } from 'antd';
import { PublicKey } from "@solana/web3.js"; import { Link } from 'react-router-dom';
import { LABELS } from "../../constants"; import { PublicKey } from '@solana/web3.js';
import { LABELS } from '../../constants';
const { Text } = Typography; const { Text } = Typography;
export const UserLendingCard = (props: { export const UserLendingCard = (props: { className?: string; reserve: LendingReserve; address: PublicKey }) => {
className?: string;
reserve: LendingReserve;
address: PublicKey;
}) => {
const reserve = props.reserve; const reserve = props.reserve;
const address = props.address; const address = props.address;
const name = useTokenName(reserve?.liquidityMint); const name = useTokenName(reserve?.liquidityMint);
const { const { balance: tokenBalance, balanceInUSD: tokenBalanceInUSD } = useUserBalance(props.reserve.liquidityMint);
balance: tokenBalance, const { balance: collateralBalance, balanceInUSD: collateralBalanceInUSD } = useUserCollateralBalance(props.reserve);
balanceInUSD: tokenBalanceInUSD,
} = useUserBalance(props.reserve.liquidityMint);
const {
balance: collateralBalance,
balanceInUSD: collateralBalanceInUSD,
} = useUserCollateralBalance(props.reserve);
const { const { borrowed: totalBorrowed, borrowedInUSD, ltv, health } = useBorrowedAmount(address);
borrowed: totalBorrowed, const { totalInQuote: borrowingPowerInUSD, borrowingPower } = useBorrowingPower(address);
borrowedInUSD,
ltv,
health,
} = useBorrowedAmount(address);
const {
totalInQuote: borrowingPowerInUSD,
borrowingPower,
} = useBorrowingPower(address);
const { userObligationsByReserve } = useUserObligationByReserve(address); const { userObligationsByReserve } = useUserObligationByReserve(address);
return ( return (
@ -53,10 +35,10 @@ export const UserLendingCard = (props: {
title={ title={
<div <div
style={{ style={{
display: "flex", display: 'flex',
alignItems: "center", alignItems: 'center',
fontSize: "1.2rem", fontSize: '1.2rem',
justifyContent: "center", justifyContent: 'center',
}} }}
> >
Your Information Your Information
@ -65,90 +47,79 @@ export const UserLendingCard = (props: {
> >
<h3>{LABELS.BORROWS}</h3> <h3>{LABELS.BORROWS}</h3>
<div className="card-row"> <div className='card-row'>
<Text type="secondary" className="card-cell "> <Text type='secondary' className='card-cell '>
Borrowed Borrowed
</Text> </Text>
<div className="card-cell "> <div className='card-cell '>
<div> <div>
<div> <div>
<em>{formatNumber.format(totalBorrowed)}</em> {name} <em>{formatNumber.format(totalBorrowed)}</em> {name}
</div> </div>
<div className="dashboard-amount-quote"> <div className='dashboard-amount-quote'>${formatNumber.format(borrowedInUSD)}</div>
${formatNumber.format(borrowedInUSD)}
</div>
</div> </div>
</div> </div>
</div> </div>
<div className="card-row"> <div className='card-row'>
<Text type="secondary" className="card-cell "> <Text type='secondary' className='card-cell '>
{LABELS.TABLE_TITLE_HEALTH}: {LABELS.TABLE_TITLE_HEALTH}:
</Text> </Text>
<div className="card-cell ">{health.toFixed(2)}</div> <div className='card-cell '>{health.toFixed(2)}</div>
</div> </div>
<div className="card-row"> <div className='card-row'>
<Text type="secondary" className="card-cell "> <Text type='secondary' className='card-cell '>
{LABELS.LOAN_TO_VALUE}: {LABELS.LOAN_TO_VALUE}:
</Text> </Text>
<div className="card-cell ">{formatNumber.format(ltv)}</div> <div className='card-cell '>{formatNumber.format(ltv)}</div>
</div> </div>
<div className="card-row"> <div className='card-row'>
<Text type="secondary" className="card-cell "> <Text type='secondary' className='card-cell '>
Available to you: Available to you:
</Text> </Text>
<div className="card-cell "> <div className='card-cell '>
<div> <div>
<div> <div>
<em>{formatNumber.format(borrowingPower)}</em> {name} <em>{formatNumber.format(borrowingPower)}</em> {name}
</div> </div>
<div className="dashboard-amount-quote"> <div className='dashboard-amount-quote'>${formatNumber.format(borrowingPowerInUSD)}</div>
${formatNumber.format(borrowingPowerInUSD)}
</div>
</div> </div>
</div> </div>
</div> </div>
<h3>{LABELS.DEPOSITS}</h3> <h3>{LABELS.DEPOSITS}</h3>
<div className="card-row"> <div className='card-row'>
<Text type="secondary" className="card-cell "> <Text type='secondary' className='card-cell '>
{LABELS.WALLET_BALANCE}: {LABELS.WALLET_BALANCE}:
</Text> </Text>
<div className="card-cell "> <div className='card-cell '>
<div> <div>
<div> <div>
<em>{formatNumber.format(tokenBalance)}</em> {name} <em>{formatNumber.format(tokenBalance)}</em> {name}
</div> </div>
<div className="dashboard-amount-quote"> <div className='dashboard-amount-quote'>${formatNumber.format(tokenBalanceInUSD)}</div>
${formatNumber.format(tokenBalanceInUSD)}
</div>
</div> </div>
</div> </div>
</div> </div>
<div className="card-row"> <div className='card-row'>
<Text type="secondary" className="card-cell "> <Text type='secondary' className='card-cell '>
You already deposited: You already deposited:
</Text> </Text>
<div className="card-cell "> <div className='card-cell '>
<div> <div>
<div> <div>
<em>{formatNumber.format(collateralBalance)}</em> {name} <em>{formatNumber.format(collateralBalance)}</em> {name}
</div> </div>
<div className="dashboard-amount-quote"> <div className='dashboard-amount-quote'>${formatNumber.format(collateralBalanceInUSD)}</div>
${formatNumber.format(collateralBalanceInUSD)}
</div>
</div> </div>
</div> </div>
</div> </div>
<div <div className='card-row' style={{ marginTop: 20, justifyContent: 'space-evenly' }}>
className="card-row"
style={{ marginTop: 20, justifyContent: "space-evenly" }}
>
<Link to={`/deposit/${address}`}> <Link to={`/deposit/${address}`}>
<Button>{LABELS.DEPOSIT_ACTION}</Button> <Button>{LABELS.DEPOSIT_ACTION}</Button>
</Link> </Link>
@ -159,9 +130,7 @@ export const UserLendingCard = (props: {
<Button>{LABELS.WITHDRAW_ACTION}</Button> <Button>{LABELS.WITHDRAW_ACTION}</Button>
</Link> </Link>
{!!userObligationsByReserve.length && ( {!!userObligationsByReserve.length && (
<Link <Link to={`/repay/loan/${userObligationsByReserve[0].obligation.account.pubkey.toBase58()}`}>
to={`/repay/loan/${userObligationsByReserve[0].obligation.account.pubkey.toBase58()}`}
>
<Button>{LABELS.REPAY_ACTION}</Button> <Button>{LABELS.REPAY_ACTION}</Button>
</Link> </Link>
)} )}

View File

@ -1,27 +1,18 @@
import React, { useCallback, useState } from "react"; import React, { useCallback, useState } from 'react';
import { import { InputType, useUserCollateralBalance, useSliderInput, useUserBalance } from '../../hooks';
InputType, import { LendingReserve } from '../../models/lending';
useUserCollateralBalance, import { Card, Slider } from 'antd';
useSliderInput, import { useConnection } from 'common/src/contexts/connection';
useUserBalance, import { useWallet } from 'common/src/contexts/wallet';
} from "../../hooks"; import { withdraw } from '../../actions';
import { LendingReserve } from "../../models/lending"; import { PublicKey } from '@solana/web3.js';
import { Card, Slider } from "antd"; import './style.less';
import { useConnection } from "../../contexts/connection"; import { LABELS, marks } from '../../constants';
import { useWallet } from "../../contexts/wallet"; import { ActionConfirmation } from './../ActionConfirmation';
import { withdraw } from "../../actions"; import { ConnectButton } from '../ConnectButton';
import { PublicKey } from "@solana/web3.js"; import CollateralInput from '../CollateralInput';
import "./style.less";
import { LABELS, marks } from "../../constants";
import { ActionConfirmation } from "./../ActionConfirmation";
import { ConnectButton } from "../ConnectButton";
import CollateralInput from "../CollateralInput";
export const WithdrawInput = (props: { export const WithdrawInput = (props: { className?: string; reserve: LendingReserve; address: PublicKey }) => {
className?: string;
reserve: LendingReserve;
address: PublicKey;
}) => {
const connection = useConnection(); const connection = useConnection();
const { wallet } = useWallet(); const { wallet } = useWallet();
const [pendingTx, setPendingTx] = useState(false); const [pendingTx, setPendingTx] = useState(false);
@ -30,17 +21,14 @@ export const WithdrawInput = (props: {
const reserve = props.reserve; const reserve = props.reserve;
const address = props.address; const address = props.address;
const { const { balanceLamports: collateralBalanceLamports, accounts: fromAccounts } = useUserBalance(
balanceLamports: collateralBalanceLamports, reserve?.collateralMint
accounts: fromAccounts,
} = useUserBalance(reserve?.collateralMint);
const { balance: collateralBalanceInLiquidity } = useUserCollateralBalance(
reserve
); );
const { balance: collateralBalanceInLiquidity } = useUserCollateralBalance(reserve);
const convert = useCallback( const convert = useCallback(
(val: string | number) => { (val: string | number) => {
if (typeof val === "string") { if (typeof val === 'string') {
return (parseFloat(val) / collateralBalanceInLiquidity) * 100; return (parseFloat(val) / collateralBalanceInLiquidity) * 100;
} else { } else {
return (val * collateralBalanceInLiquidity) / 100; return (val * collateralBalanceInLiquidity) / 100;
@ -59,22 +47,12 @@ export const WithdrawInput = (props: {
const withdrawAmount = Math.min( const withdrawAmount = Math.min(
type === InputType.Percent type === InputType.Percent
? (pct * collateralBalanceLamports) / 100 ? (pct * collateralBalanceLamports) / 100
: Math.ceil( : Math.ceil(collateralBalanceLamports * (parseFloat(value) / collateralBalanceInLiquidity)),
collateralBalanceLamports *
(parseFloat(value) / collateralBalanceInLiquidity)
),
collateralBalanceLamports collateralBalanceLamports
); );
await withdraw( await withdraw(fromAccounts[0], withdrawAmount, reserve, address, connection, wallet);
fromAccounts[0],
withdrawAmount,
reserve,
address,
connection,
wallet
);
setValue(""); setValue('');
setShowConfirmation(true); setShowConfirmation(true);
} catch { } catch {
// TODO: // TODO:
@ -97,11 +75,11 @@ export const WithdrawInput = (props: {
]); ]);
const bodyStyle: React.CSSProperties = { const bodyStyle: React.CSSProperties = {
display: "flex", display: 'flex',
flex: 1, flex: 1,
justifyContent: "center", justifyContent: 'center',
alignItems: "center", alignItems: 'center',
height: "100%", height: '100%',
}; };
return ( return (
@ -111,26 +89,26 @@ export const WithdrawInput = (props: {
) : ( ) : (
<div <div
style={{ style={{
display: "flex", display: 'flex',
flexDirection: "column", flexDirection: 'column',
justifyContent: "space-around", justifyContent: 'space-around',
}} }}
> >
<div className="withdraw-input-title">{LABELS.WITHDRAW_QUESTION}</div> <div className='withdraw-input-title'>{LABELS.WITHDRAW_QUESTION}</div>
<div <div
style={{ style={{
display: "flex", display: 'flex',
flexDirection: "row", flexDirection: 'row',
justifyContent: "space-evenly", justifyContent: 'space-evenly',
alignItems: "center", alignItems: 'center',
}} }}
> >
<CollateralInput <CollateralInput
title="Amount" title='Amount'
reserve={reserve} reserve={reserve}
amount={parseFloat(value) || 0} amount={parseFloat(value) || 0}
onInputChange={(val: number | null) => { onInputChange={(val: number | null) => {
setValue(val?.toString() || ""); setValue(val?.toString() || '');
}} }}
disabled={true} disabled={true}
hideBalance={true} hideBalance={true}
@ -140,15 +118,13 @@ export const WithdrawInput = (props: {
<Slider marks={marks} value={pct} onChange={setPct} /> <Slider marks={marks} value={pct} onChange={setPct} />
<ConnectButton <ConnectButton
size="large" size='large'
type="primary" type='primary'
onClick={onWithdraw} onClick={onWithdraw}
loading={pendingTx} loading={pendingTx}
disabled={fromAccounts.length === 0} disabled={fromAccounts.length === 0}
> >
{fromAccounts.length === 0 {fromAccounts.length === 0 ? LABELS.NO_COLLATERAL : LABELS.WITHDRAW_ACTION}
? LABELS.NO_COLLATERAL
: LABELS.WITHDRAW_ACTION}
</ConnectButton> </ConnectButton>
</div> </div>
)} )}

View File

@ -1,4 +1,3 @@
export * from "./labels"; export * from './labels';
export * from "./math"; export * from './marks';
export * from "./marks"; export * from './style';
export * from "./style";

View File

@ -1,6 +1,6 @@
import React, { useCallback, useEffect, useState } from "react"; import React, { useCallback, useEffect, useState } from 'react';
import { useConnection } from "./connection"; import { useConnection } from '@common/contexts/connection';
import { LENDING_PROGRAM_ID } from "./../utils/ids"; import { LENDING_PROGRAM_ID } from 'common/src/utils/ids';
import { import {
LendingMarketParser, LendingMarketParser,
isLendingReserve, isLendingReserve,
@ -9,17 +9,12 @@ import {
LendingReserve, LendingReserve,
isLendingObligation, isLendingObligation,
LendingObligationParser, LendingObligationParser,
} from "./../models/lending"; } from './../models/lending';
import { import { cache, getMultipleAccounts, MintParser, ParsedAccount } from 'common/src/contexts/accounts';
cache, import { PublicKey, AccountInfo } from '@solana/web3.js';
getMultipleAccounts, import { DexMarketParser } from '../models/dex';
MintParser, import { usePrecacheMarket } from './market';
ParsedAccount, import { useLendingReserves } from '../hooks';
} from "./accounts";
import { PublicKey, AccountInfo } from "@solana/web3.js";
import { DexMarketParser } from "../models/dex";
import { usePrecacheMarket } from "./market";
import { useLendingReserves } from "../hooks";
export interface LendingContextState {} export interface LendingContextState {}
@ -46,38 +41,21 @@ export const useLending = () => {
// TODO: query for all the dex from reserves // TODO: query for all the dex from reserves
const processAccount = useCallback( const processAccount = useCallback((item: { pubkey: PublicKey; account: AccountInfo<Buffer> }) => {
(item: { pubkey: PublicKey; account: AccountInfo<Buffer> }) => { if (isLendingReserve(item.account)) {
if (isLendingReserve(item.account)) { const reserve = cache.add(item.pubkey.toBase58(), item.account, LendingReserveParser);
const reserve = cache.add(
item.pubkey.toBase58(),
item.account,
LendingReserveParser
);
return reserve; return reserve;
} else if (isLendingMarket(item.account)) { } else if (isLendingMarket(item.account)) {
return cache.add( return cache.add(item.pubkey.toBase58(), item.account, LendingMarketParser);
item.pubkey.toBase58(), } else if (isLendingObligation(item.account)) {
item.account, return cache.add(item.pubkey.toBase58(), item.account, LendingObligationParser);
LendingMarketParser }
); }, []);
} else if (isLendingObligation(item.account)) {
return cache.add(
item.pubkey.toBase58(),
item.account,
LendingObligationParser
);
}
},
[]
);
useEffect(() => { useEffect(() => {
if (reserveAccounts.length > 0) { if (reserveAccounts.length > 0) {
precacheMarkets( precacheMarkets(reserveAccounts.map((reserve) => reserve.info.liquidityMint.toBase58()));
reserveAccounts.map((reserve) => reserve.info.liquidityMint.toBase58())
);
} }
}, [reserveAccounts, precacheMarkets]); }, [reserveAccounts, precacheMarkets]);
@ -86,36 +64,21 @@ export const useLending = () => {
setLendingAccounts([]); setLendingAccounts([]);
const queryLendingAccounts = async () => { const queryLendingAccounts = async () => {
const programAccounts = await connection.getProgramAccounts( const programAccounts = await connection.getProgramAccounts(LENDING_PROGRAM_ID);
LENDING_PROGRAM_ID
);
const accounts = programAccounts const accounts = programAccounts.map(processAccount).filter((item) => item !== undefined);
.map(processAccount)
.filter((item) => item !== undefined);
const lendingReserves = accounts const lendingReserves = accounts
.filter( .filter((acc) => (acc?.info as LendingReserve).lendingMarket !== undefined)
(acc) => (acc?.info as LendingReserve).lendingMarket !== undefined
)
.map((acc) => acc as ParsedAccount<LendingReserve>); .map((acc) => acc as ParsedAccount<LendingReserve>);
const toQuery = [ const toQuery = [
...lendingReserves.map((acc) => { ...lendingReserves.map((acc) => {
const result = [ const result = [
cache.registerParser( cache.registerParser(acc?.info.collateralMint.toBase58(), MintParser),
acc?.info.collateralMint.toBase58(), cache.registerParser(acc?.info.liquidityMint.toBase58(), MintParser),
MintParser
),
cache.registerParser(
acc?.info.liquidityMint.toBase58(),
MintParser
),
// ignore dex if its not set // ignore dex if its not set
cache.registerParser( cache.registerParser(acc?.info.dexMarketOption ? acc?.info.dexMarket.toBase58() : '', DexMarketParser),
acc?.info.dexMarketOption ? acc?.info.dexMarket.toBase58() : "",
DexMarketParser
),
].filter((_) => _); ].filter((_) => _);
return result; return result;
}), }),
@ -123,15 +86,13 @@ export const useLending = () => {
// This will pre-cache all accounts used by pools // This will pre-cache all accounts used by pools
// All those accounts are updated whenever there is a change // All those accounts are updated whenever there is a change
await getMultipleAccounts(connection, toQuery, "single").then( await getMultipleAccounts(connection, toQuery, 'single').then(({ keys, array }) => {
({ keys, array }) => { return array.map((obj, index) => {
return array.map((obj, index) => { const address = keys[index];
const address = keys[index]; cache.add(address, obj);
cache.add(address, obj); return obj;
return obj; }) as any[];
}) as any[]; });
}
);
// HACK: fix, force account refresh // HACK: fix, force account refresh
programAccounts.map(processAccount).filter((item) => item !== undefined); programAccounts.map(processAccount).filter((item) => item !== undefined);
@ -155,7 +116,7 @@ export const useLending = () => {
}; };
processAccount(item); processAccount(item);
}, },
"singleGossip" 'singleGossip'
); );
return () => { return () => {

View File

@ -1,26 +1,20 @@
import React, { useCallback, useContext, useEffect, useState } from "react"; import React, { useCallback, useContext, useEffect, useState } from 'react';
import { MINT_TO_MARKET } from "./../models/marketOverrides"; import { MINT_TO_MARKET } from './../models/marketOverrides';
import { POOLS_WITH_AIRDROP } from "./../models/airdrops"; import { POOLS_WITH_AIRDROP } from './../models/airdrops';
import { import { convert, fromLamports, getTokenName, KnownTokenMap, STABLE_COINS } from 'common/src/utils/utils';
convert, import { getPoolName } from '../utils/utils';
fromLamports, import { useConnectionConfig } from 'common/src/contexts/connection';
getPoolName, import { cache, getMultipleAccounts, ParsedAccount } from 'common/src/contexts/accounts';
getTokenName, import { Market, MARKETS, Orderbook, TOKEN_MINTS } from '@project-serum/serum';
KnownTokenMap, import { AccountInfo, Connection, PublicKey } from '@solana/web3.js';
STABLE_COINS, import { useMemo } from 'react';
} from "./../utils/utils"; import { EventEmitter } from 'common/src/utils/eventEmitter';
import { useConnectionConfig } from "./connection";
import { cache, getMultipleAccounts, ParsedAccount } from "./accounts";
import { Market, MARKETS, Orderbook, TOKEN_MINTS } from "@project-serum/serum";
import { AccountInfo, Connection, PublicKey } from "@solana/web3.js";
import { useMemo } from "react";
import { EventEmitter } from "./../utils/eventEmitter";
import { DexMarketParser } from "./../models/dex"; import { DexMarketParser } from './../models/dex';
import { LendingMarket, LendingReserve, PoolInfo } from "../models"; import { LendingMarket, LendingReserve, PoolInfo } from '../models';
import { LIQUIDITY_PROVIDER_FEE, SERUM_FEE } from "../utils/pools"; import { LIQUIDITY_PROVIDER_FEE, SERUM_FEE } from '../utils/pools';
const INITAL_LIQUIDITY_DATE = new Date("2020-10-27"); const INITAL_LIQUIDITY_DATE = new Date('2020-10-27');
export const BONFIDA_POOL_INTERVAL = 30 * 60_000; // 30 min export const BONFIDA_POOL_INTERVAL = 30 * 60_000; // 30 min
interface RecentPoolData { interface RecentPoolData {
@ -50,27 +44,19 @@ export function MarketProvider({ children = null as any }) {
const { endpoint } = useConnectionConfig(); const { endpoint } = useConnectionConfig();
const accountsToObserve = useMemo(() => new Map<string, number>(), []); const accountsToObserve = useMemo(() => new Map<string, number>(), []);
const [marketMints, setMarketMints] = useState<string[]>([]); const [marketMints, setMarketMints] = useState<string[]>([]);
const [dailyVolume, setDailyVolume] = useState<Map<string, RecentPoolData>>( const [dailyVolume, setDailyVolume] = useState<Map<string, RecentPoolData>>(new Map());
new Map()
);
const connection = useMemo(() => new Connection(endpoint, "recent"), [ const connection = useMemo(() => new Connection(endpoint, 'recent'), [endpoint]);
endpoint,
]);
const marketByMint = useMemo(() => { const marketByMint = useMemo(() => {
return [...new Set(marketMints).values()].reduce((acc, key) => { return [...new Set(marketMints).values()].reduce((acc, key) => {
const mintAddress = key; const mintAddress = key;
const SERUM_TOKEN = TOKEN_MINTS.find( const SERUM_TOKEN = TOKEN_MINTS.find((a) => a.address.toBase58() === mintAddress);
(a) => a.address.toBase58() === mintAddress
);
const marketAddress = MINT_TO_MARKET[mintAddress]; const marketAddress = MINT_TO_MARKET[mintAddress];
const marketName = `${SERUM_TOKEN?.name}/USDC`; const marketName = `${SERUM_TOKEN?.name}/USDC`;
const marketInfo = MARKETS.find( const marketInfo = MARKETS.find((m) => m.name === marketName || m.address.toBase58() === marketAddress);
(m) => m.name === marketName || m.address.toBase58() === marketAddress
);
if (marketInfo) { if (marketInfo) {
acc.set(mintAddress, { acc.set(mintAddress, {
@ -109,7 +95,7 @@ export function MarketProvider({ children = null as any }) {
connection, connection,
// only query for markets that are not in cahce // only query for markets that are not in cahce
allMarkets.filter((a) => cache.get(a) === undefined), allMarkets.filter((a) => cache.get(a) === undefined),
"single" 'single'
).then(({ keys, array }) => { ).then(({ keys, array }) => {
allMarkets.forEach(() => {}); allMarkets.forEach(() => {});
@ -167,10 +153,7 @@ export function MarketProvider({ children = null as any }) {
const midPriceInUSD = useCallback( const midPriceInUSD = useCallback(
(mintAddress: string) => { (mintAddress: string) => {
return getMidPrice( return getMidPrice(marketByMint.get(mintAddress)?.marketInfo.address.toBase58(), mintAddress);
marketByMint.get(mintAddress)?.marketInfo.address.toBase58(),
mintAddress
);
}, },
[marketByMint] [marketByMint]
); );
@ -178,7 +161,7 @@ export function MarketProvider({ children = null as any }) {
const subscribeToMarket = useCallback( const subscribeToMarket = useCallback(
(mintAddress: string) => { (mintAddress: string) => {
const info = marketByMint.get(mintAddress); const info = marketByMint.get(mintAddress);
const market = cache.get(info?.marketInfo.address.toBase58() || ""); const market = cache.get(info?.marketInfo.address.toBase58() || '');
if (!market) { if (!market) {
return () => {}; return () => {};
} }
@ -248,7 +231,7 @@ export const useEnrichedPools = (pools: PoolInfo[]) => {
const marketEmitter = context?.marketEmitter; const marketEmitter = context?.marketEmitter;
const marketsByMint = context?.marketByMint; const marketsByMint = context?.marketByMint;
const dailyVolume = context?.dailyVolume; const dailyVolume = context?.dailyVolume;
const poolKeys = pools.map((p) => p.pubkeys.account.toBase58()).join(","); const poolKeys = pools.map((p) => p.pubkeys.account.toBase58()).join(',');
useEffect(() => { useEffect(() => {
if (!marketEmitter || !subscribeToMarket || pools.length === 0) { if (!marketEmitter || !subscribeToMarket || pools.length === 0) {
@ -260,9 +243,7 @@ export const useEnrichedPools = (pools: PoolInfo[]) => {
const subscriptions = mints.map((m) => subscribeToMarket(m)); const subscriptions = mints.map((m) => subscribeToMarket(m));
const update = () => { const update = () => {
setEnriched( setEnriched(createEnrichedPools(pools, marketsByMint, dailyVolume, tokenMap));
createEnrichedPools(pools, marketsByMint, dailyVolume, tokenMap)
);
}; };
const dispose = marketEmitter.onMarket(update); const dispose = marketEmitter.onMarket(update);
@ -274,15 +255,7 @@ export const useEnrichedPools = (pools: PoolInfo[]) => {
subscriptions.forEach((dispose) => dispose && dispose()); subscriptions.forEach((dispose) => dispose && dispose());
}; };
// Do not add pools here, causes a really bad infinite rendering loop. Use poolKeys instead. // Do not add pools here, causes a really bad infinite rendering loop. Use poolKeys instead.
}, [ }, [pools, tokenMap, dailyVolume, poolKeys, subscribeToMarket, marketEmitter, marketsByMint]);
pools,
tokenMap,
dailyVolume,
poolKeys,
subscribeToMarket,
marketEmitter,
marketsByMint,
]);
return enriched; return enriched;
}; };
@ -307,30 +280,20 @@ function createEnrichedPools(
const result = pools const result = pools
.filter((p) => p.pubkeys.holdingMints && p.pubkeys.holdingMints.length > 1) .filter((p) => p.pubkeys.holdingMints && p.pubkeys.holdingMints.length > 1)
.map((p, index) => { .map((p, index) => {
const mints = (p.pubkeys.holdingMints || []) const mints = (p.pubkeys.holdingMints || []).map((a) => a.toBase58()).sort();
.map((a) => a.toBase58())
.sort();
const mintA = cache.getMint(mints[0]); const mintA = cache.getMint(mints[0]);
const mintB = cache.getMint(mints[1]); const mintB = cache.getMint(mints[1]);
const account0 = cache.get(p.pubkeys.holdingAccounts[0]); const account0 = cache.get(p.pubkeys.holdingAccounts[0]);
const account1 = cache.get(p.pubkeys.holdingAccounts[1]); const account1 = cache.get(p.pubkeys.holdingAccounts[1]);
const accountA = const accountA = account0?.info.mint.toBase58() === mints[0] ? account0 : account1;
account0?.info.mint.toBase58() === mints[0] ? account0 : account1; const accountB = account1?.info.mint.toBase58() === mints[1] ? account1 : account0;
const accountB =
account1?.info.mint.toBase58() === mints[1] ? account1 : account0;
const baseMid = getMidPrice( const baseMid = getMidPrice(marketByMint.get(mints[0])?.marketInfo.address.toBase58() || '', mints[0]);
marketByMint.get(mints[0])?.marketInfo.address.toBase58() || "",
mints[0]
);
const baseReserveUSD = baseMid * convert(accountA, mintA); const baseReserveUSD = baseMid * convert(accountA, mintA);
const quote = getMidPrice( const quote = getMidPrice(marketByMint.get(mints[1])?.marketInfo.address.toBase58() || '', mints[1]);
marketByMint.get(mints[1])?.marketInfo.address.toBase58() || "",
mints[1]
);
const quoteReserveUSD = quote * convert(accountB, mintB); const quoteReserveUSD = quote * convert(accountB, mintB);
const poolMint = cache.getMint(p.pubkeys.mint); const poolMint = cache.getMint(p.pubkeys.mint);
@ -338,16 +301,10 @@ function createEnrichedPools(
return undefined; return undefined;
} }
let airdropYield = calculateAirdropYield( let airdropYield = calculateAirdropYield(p, marketByMint, baseReserveUSD, quoteReserveUSD);
p,
marketByMint,
baseReserveUSD,
quoteReserveUSD
);
let volume = 0; let volume = 0;
let volume24h = let volume24h = baseMid * (poolData?.get(p.pubkeys.mint.toBase58())?.volume24hA || 0);
baseMid * (poolData?.get(p.pubkeys.mint.toBase58())?.volume24hA || 0);
let fees24h = volume24h * (LIQUIDITY_PROVIDER_FEE - SERUM_FEE); let fees24h = volume24h * (LIQUIDITY_PROVIDER_FEE - SERUM_FEE);
let fees = 0; let fees = 0;
let apy = airdropYield; let apy = airdropYield;
@ -355,18 +312,13 @@ function createEnrichedPools(
if (p.pubkeys.feeAccount) { if (p.pubkeys.feeAccount) {
const feeAccount = cache.get(p.pubkeys.feeAccount); const feeAccount = cache.get(p.pubkeys.feeAccount);
if ( if (poolMint && feeAccount && feeAccount.info.mint.toBase58() === p.pubkeys.mint.toBase58()) {
poolMint &&
feeAccount &&
feeAccount.info.mint.toBase58() === p.pubkeys.mint.toBase58()
) {
const feeBalance = feeAccount?.info.amount.toNumber(); const feeBalance = feeAccount?.info.amount.toNumber();
const supply = poolMint?.supply.toNumber(); const supply = poolMint?.supply.toNumber();
const ownedPct = feeBalance / supply; const ownedPct = feeBalance / supply;
const poolOwnerFees = const poolOwnerFees = ownedPct * baseReserveUSD + ownedPct * quoteReserveUSD;
ownedPct * baseReserveUSD + ownedPct * quoteReserveUSD;
volume = poolOwnerFees / 0.0004; volume = poolOwnerFees / 0.0004;
fees = volume * LIQUIDITY_PROVIDER_FEE; fees = volume * LIQUIDITY_PROVIDER_FEE;
@ -376,27 +328,16 @@ function createEnrichedPools(
// Aproximation not true for all pools we need to fine a better way // Aproximation not true for all pools we need to fine a better way
const daysSinceInception = Math.floor( const daysSinceInception = Math.floor(
(TODAY.getTime() - INITAL_LIQUIDITY_DATE.getTime()) / (TODAY.getTime() - INITAL_LIQUIDITY_DATE.getTime()) / (24 * 3600 * 1000)
(24 * 3600 * 1000)
); );
const apy0 = const apy0 =
parseFloat( parseFloat(((baseVolume / daysSinceInception) * LIQUIDITY_PROVIDER_FEE * 356) as any) / baseReserveUSD;
((baseVolume / daysSinceInception) *
LIQUIDITY_PROVIDER_FEE *
356) as any
) / baseReserveUSD;
const apy1 = const apy1 =
parseFloat( parseFloat(((quoteVolume / daysSinceInception) * LIQUIDITY_PROVIDER_FEE * 356) as any) / quoteReserveUSD;
((quoteVolume / daysSinceInception) *
LIQUIDITY_PROVIDER_FEE *
356) as any
) / quoteReserveUSD;
apy = apy + Math.max(apy0, apy1); apy = apy + Math.max(apy0, apy1);
const apy24h0 = const apy24h0 = parseFloat((volume24h * LIQUIDITY_PROVIDER_FEE * 356) as any) / baseReserveUSD;
parseFloat((volume24h * LIQUIDITY_PROVIDER_FEE * 356) as any) /
baseReserveUSD;
apy24h = apy24h + apy24h0; apy24h = apy24h + apy24h0;
} }
} }
@ -405,10 +346,7 @@ function createEnrichedPools(
const lpMint = cache.getMint(p.pubkeys.mint); const lpMint = cache.getMint(p.pubkeys.mint);
const name = getPoolName(tokenMap, p); const name = getPoolName(tokenMap, p);
const link = `#/?pair=${getPoolName(tokenMap, p, false).replace( const link = `#/?pair=${getPoolName(tokenMap, p, false).replace('/', '-')}`;
"/",
"-"
)}`;
return { return {
key: p.pubkeys.account.toBase58(), key: p.pubkeys.account.toBase58(),
@ -423,11 +361,7 @@ function createEnrichedPools(
liquidityAinUsd: baseReserveUSD, liquidityAinUsd: baseReserveUSD,
liquidityB: convert(accountB, mintB), liquidityB: convert(accountB, mintB),
liquidityBinUsd: quoteReserveUSD, liquidityBinUsd: quoteReserveUSD,
supply: supply: lpMint && (lpMint?.supply.toNumber() / Math.pow(10, lpMint?.decimals || 0)).toFixed(9),
lpMint &&
(
lpMint?.supply.toNumber() / Math.pow(10, lpMint?.decimals || 0)
).toFixed(9),
fees, fees,
fees24h, fees24h,
liquidity: baseReserveUSD + quoteReserveUSD, liquidity: baseReserveUSD + quoteReserveUSD,
@ -451,9 +385,7 @@ function calculateAirdropYield(
quoteReserveUSD: number quoteReserveUSD: number
) { ) {
let airdropYield = 0; let airdropYield = 0;
let poolWithAirdrop = POOLS_WITH_AIRDROP.find((drop) => let poolWithAirdrop = POOLS_WITH_AIRDROP.find((drop) => drop.pool.equals(p.pubkeys.mint));
drop.pool.equals(p.pubkeys.mint)
);
if (poolWithAirdrop) { if (poolWithAirdrop) {
airdropYield = poolWithAirdrop.airdrops.reduce((acc, item) => { airdropYield = poolWithAirdrop.airdrops.reduce((acc, item) => {
const market = marketByMint.get(item.mint.toBase58())?.marketInfo.address; const market = marketByMint.get(item.mint.toBase58())?.marketInfo.address;
@ -463,8 +395,7 @@ function calculateAirdropYield(
acc = acc =
acc + acc +
// airdrop yield // airdrop yield
((item.amount * midPrice) / (baseReserveUSD + quoteReserveUSD)) * ((item.amount * midPrice) / (baseReserveUSD + quoteReserveUSD)) * (365 / 30);
(365 / 30);
} }
return acc; return acc;
@ -474,9 +405,7 @@ function calculateAirdropYield(
} }
export const useMidPriceInUSD = (mint: string) => { export const useMidPriceInUSD = (mint: string) => {
const { midPriceInUSD, subscribeToMarket, marketEmitter } = useContext( const { midPriceInUSD, subscribeToMarket, marketEmitter } = useContext(MarketsContext) as MarketsContextState;
MarketsContext
) as MarketsContextState;
const [price, setPrice] = useState<number>(0); const [price, setPrice] = useState<number>(0);
useEffect(() => { useEffect(() => {
@ -504,12 +433,7 @@ export const usePrecacheMarket = () => {
return context.precacheMarkets; return context.precacheMarkets;
}; };
export const simulateMarketOrderFill = ( export const simulateMarketOrderFill = (amount: number, reserve: LendingReserve, dex: PublicKey, useBBO = false) => {
amount: number,
reserve: LendingReserve,
dex: PublicKey,
useBBO = false
) => {
const liquidityMint = cache.get(reserve.liquidityMint); const liquidityMint = cache.get(reserve.liquidityMint);
const collateralMint = cache.get(reserve.collateralMint); const collateralMint = cache.get(reserve.collateralMint);
if (!liquidityMint || !collateralMint) { if (!liquidityMint || !collateralMint) {
@ -522,22 +446,12 @@ export const simulateMarketOrderFill = (
} }
const decodedMarket = marketInfo.info; const decodedMarket = marketInfo.info;
const baseMintDecimals = const baseMintDecimals = cache.get(decodedMarket.baseMint)?.info.decimals || 0;
cache.get(decodedMarket.baseMint)?.info.decimals || 0; const quoteMintDecimals = cache.get(decodedMarket.quoteMint)?.info.decimals || 0;
const quoteMintDecimals =
cache.get(decodedMarket.quoteMint)?.info.decimals || 0;
const lendingMarket = cache.get(reserve.lendingMarket) as ParsedAccount< const lendingMarket = cache.get(reserve.lendingMarket) as ParsedAccount<LendingMarket>;
LendingMarket
>;
const dexMarket = new Market( const dexMarket = new Market(decodedMarket, baseMintDecimals, quoteMintDecimals, undefined, decodedMarket.programId);
decodedMarket,
baseMintDecimals,
quoteMintDecimals,
undefined,
decodedMarket.programId
);
const bidInfo = cache.get(decodedMarket?.bids)?.info; const bidInfo = cache.get(decodedMarket?.bids)?.info;
const askInfo = cache.get(decodedMarket?.asks)?.info; const askInfo = cache.get(decodedMarket?.asks)?.info;
@ -548,9 +462,7 @@ export const simulateMarketOrderFill = (
const bids = new Orderbook(dexMarket, bidInfo.accountFlags, bidInfo.slab); const bids = new Orderbook(dexMarket, bidInfo.accountFlags, bidInfo.slab);
const asks = new Orderbook(dexMarket, askInfo.accountFlags, askInfo.slab); const asks = new Orderbook(dexMarket, askInfo.accountFlags, askInfo.slab);
const book = lendingMarket.info.quoteMint.equals(reserve.liquidityMint) const book = lendingMarket.info.quoteMint.equals(reserve.liquidityMint) ? bids : asks;
? bids
: asks;
let cost = 0; let cost = 0;
let remaining = fromLamports(amount, liquidityMint.info); let remaining = fromLamports(amount, liquidityMint.info);
@ -593,11 +505,9 @@ const bbo = (bidsBook: Orderbook, asksBook: Orderbook) => {
}; };
const getMidPrice = (marketAddress?: string, mintAddress?: string) => { const getMidPrice = (marketAddress?: string, mintAddress?: string) => {
const SERUM_TOKEN = TOKEN_MINTS.find( const SERUM_TOKEN = TOKEN_MINTS.find((a) => a.address.toBase58() === mintAddress);
(a) => a.address.toBase58() === mintAddress
);
if (STABLE_COINS.has(SERUM_TOKEN?.name || "")) { if (STABLE_COINS.has(SERUM_TOKEN?.name || '')) {
return 1.0; return 1.0;
} }
@ -612,18 +522,10 @@ const getMidPrice = (marketAddress?: string, mintAddress?: string) => {
const decodedMarket = marketInfo.info; const decodedMarket = marketInfo.info;
const baseMintDecimals = const baseMintDecimals = cache.get(decodedMarket.baseMint)?.info.decimals || 0;
cache.get(decodedMarket.baseMint)?.info.decimals || 0; const quoteMintDecimals = cache.get(decodedMarket.quoteMint)?.info.decimals || 0;
const quoteMintDecimals =
cache.get(decodedMarket.quoteMint)?.info.decimals || 0;
const market = new Market( const market = new Market(decodedMarket, baseMintDecimals, quoteMintDecimals, undefined, decodedMarket.programId);
decodedMarket,
baseMintDecimals,
quoteMintDecimals,
undefined,
decodedMarket.programId
);
const bids = cache.get(decodedMarket.bids)?.info; const bids = cache.get(decodedMarket.bids)?.info;
const asks = cache.get(decodedMarket.asks)?.info; const asks = cache.get(decodedMarket.asks)?.info;
@ -643,14 +545,12 @@ const refreshAccounts = async (connection: Connection, keys: string[]) => {
return []; return [];
} }
return getMultipleAccounts(connection, keys, "single").then( return getMultipleAccounts(connection, keys, 'single').then(({ keys, array }) => {
({ keys, array }) => { return array.map((item, index) => {
return array.map((item, index) => { const address = keys[index];
const address = keys[index]; return cache.add(new PublicKey(address), item);
return cache.add(new PublicKey(address), item); });
}); });
}
);
}; };
interface SerumMarket { interface SerumMarket {

View File

@ -0,0 +1,18 @@
import React, { useContext, useMemo } from 'react';
import { PoolInfo } from '../models';
const AccountsContext = React.createContext<any>(null);
export function useCachedPool(legacy = false) {
const context = useContext(AccountsContext);
const allPools = context.pools as PoolInfo[];
const pools = useMemo(() => {
return allPools.filter((p) => p.legacy === legacy);
}, [allPools, legacy]);
return {
pools,
};
}

View File

@ -1,15 +1,12 @@
export * from "./useUserAccounts"; export * from './useLendingReserves';
export * from "./useAccountByMint"; export * from './useCollateralBalance';
export * from "./useLendingReserves"; export * from './useLendingObligations';
export * from "./useTokenName"; export * from './useUserObligations';
export * from "./useUserBalance"; export * from './useUserObligationByReserve';
export * from "./useCollateralBalance"; export * from './useBorrowedAmount';
export * from "./useLendingObligations"; export * from './useUserDeposits';
export * from "./useUserObligations"; export * from './useSliderInput';
export * from "./useUserObligationByReserve"; export * from './useUserBalance';
export * from "./useBorrowedAmount"; export * from './useEnrichedLendingObligations';
export * from "./useUserDeposits"; export * from './useBorrowingPower';
export * from "./useSliderInput"; export * from './useLendingMarket';
export * from "./useEnrichedLendingObligations";
export * from "./useBorrowingPower";
export * from "./useLendingMarket";

View File

@ -1,17 +0,0 @@
import { PublicKey } from "@solana/web3.js";
import { useUserAccounts } from "./useUserAccounts";
export const useAccountByMint = (mint?: string | PublicKey) => {
const { userAccounts } = useUserAccounts();
const mintAddress = typeof mint === "string" ? mint : mint?.toBase58();
const index = userAccounts.findIndex(
(acc) => acc.info.mint.toBase58() === mintAddress
);
if (index !== -1) {
return userAccounts[index];
}
return;
};

View File

@ -1,17 +1,11 @@
import { useEffect, useState } from "react"; import { useEffect, useState } from 'react';
import { PublicKey } from "@solana/web3.js"; import { PublicKey } from '@solana/web3.js';
import { useUserObligationByReserve } from "./useUserObligationByReserve"; import { useUserObligationByReserve } from './useUserObligationByReserve';
import { fromLamports, wadToLamports } from "../utils/utils"; import { fromLamports, wadToLamports } from 'common/src/utils/utils';
import { import { cache, getMultipleAccounts, MintParser, ParsedAccount, useMint } from 'common/src/contexts/accounts';
cache, import { useConnection } from 'common/src/contexts/connection';
getMultipleAccounts, import { MintInfo } from '@solana/spl-token';
MintParser, import { useLendingReserve } from './useLendingReserves';
ParsedAccount,
useMint,
} from "../contexts/accounts";
import { useConnection } from "../contexts/connection";
import { MintInfo } from "@solana/spl-token";
import { useLendingReserve } from "./useLendingReserves";
export function useBorrowedAmount(address?: string | PublicKey) { export function useBorrowedAmount(address?: string | PublicKey) {
const connection = useConnection(); const connection = useConnection();
@ -39,10 +33,8 @@ export function useBorrowedAmount(address?: string | PublicKey) {
// precache obligation mints // precache obligation mints
const { keys, array } = await getMultipleAccounts( const { keys, array } = await getMultipleAccounts(
connection, connection,
userObligationsByReserve.map((item) => userObligationsByReserve.map((item) => item.obligation.info.tokenMint.toBase58()),
item.obligation.info.tokenMint.toBase58() 'single'
),
"single"
); );
array.forEach((item, index) => { array.forEach((item, index) => {
@ -61,20 +53,12 @@ export function useBorrowedAmount(address?: string | PublicKey) {
let liquidationThreshold = 0; let liquidationThreshold = 0;
userObligationsByReserve.forEach((item) => { userObligationsByReserve.forEach((item) => {
const borrowed = wadToLamports( const borrowed = wadToLamports(item.obligation.info.borrowAmountWad).toNumber();
item.obligation.info.borrowAmountWad
).toNumber();
const owned = item.userAccounts.reduce( const owned = item.userAccounts.reduce((amount, acc) => (amount += acc.info.amount.toNumber()), 0);
(amount, acc) => (amount += acc.info.amount.toNumber()), const obligationMint = cache.get(item.obligation.info.tokenMint) as ParsedAccount<MintInfo>;
0
);
const obligationMint = cache.get(
item.obligation.info.tokenMint
) as ParsedAccount<MintInfo>;
result.borrowedLamports += result.borrowedLamports += borrowed * (owned / obligationMint?.info.supply.toNumber());
borrowed * (owned / obligationMint?.info.supply.toNumber());
result.borrowedInUSD += item.obligation.info.borrowedInQuote; result.borrowedInUSD += item.obligation.info.borrowedInQuote;
result.colateralInUSD += item.obligation.info.collateralInQuote; result.colateralInUSD += item.obligation.info.collateralInQuote;
liquidationThreshold = item.obligation.info.liquidationThreshold; liquidationThreshold = item.obligation.info.liquidationThreshold;
@ -85,10 +69,7 @@ export function useBorrowedAmount(address?: string | PublicKey) {
result.health = userObligationsByReserve[0].obligation.info.health; result.health = userObligationsByReserve[0].obligation.info.health;
} else { } else {
result.ltv = (100 * result.borrowedInUSD) / result.colateralInUSD; result.ltv = (100 * result.borrowedInUSD) / result.colateralInUSD;
result.health = result.health = (result.colateralInUSD * liquidationThreshold) / 100 / result.borrowedInUSD;
(result.colateralInUSD * liquidationThreshold) /
100 /
result.borrowedInUSD;
result.health = Number.isFinite(result.health) ? result.health : 0; result.health = Number.isFinite(result.health) ? result.health : 0;
} }

View File

@ -1,43 +1,32 @@
import { PublicKey } from "@solana/web3.js"; import { PublicKey } from '@solana/web3.js';
import { useEffect, useMemo, useState } from "react"; import { useEffect, useMemo, useState } from 'react';
import { useMint } from "../contexts/accounts"; import { useMint } from 'common/src/contexts/accounts';
import { useMarkets } from "../contexts/market"; import { useMarkets } from '../contexts/market';
import { LendingReserve, reserveMarketCap } from "../models/lending"; import { LendingReserve, reserveMarketCap } from '../models/lending';
import { fromLamports } from "../utils/utils"; import { fromLamports } from 'common/src/utils/utils';
import { useUserBalance } from "./useUserBalance"; import { useUserBalance } from './useUserBalance';
export function useUserCollateralBalance( export function useUserCollateralBalance(reserve?: LendingReserve, account?: PublicKey) {
reserve?: LendingReserve,
account?: PublicKey
) {
const mint = useMint(reserve?.collateralMint); const mint = useMint(reserve?.collateralMint);
const { balanceLamports: userBalance, accounts } = useUserBalance( const { balanceLamports: userBalance, accounts } = useUserBalance(reserve?.collateralMint, account);
reserve?.collateralMint,
account
);
const [balanceInUSD, setBalanceInUSD] = useState(0); const [balanceInUSD, setBalanceInUSD] = useState(0);
const { marketEmitter, midPriceInUSD } = useMarkets(); const { marketEmitter, midPriceInUSD } = useMarkets();
const balanceLamports = useMemo( const balanceLamports = useMemo(() => reserve && calculateCollateralBalance(reserve, userBalance), [
() => reserve && calculateCollateralBalance(reserve, userBalance), userBalance,
[userBalance, reserve] reserve,
);
const balance = useMemo(() => fromLamports(balanceLamports, mint), [
balanceLamports,
mint,
]); ]);
const balance = useMemo(() => fromLamports(balanceLamports, mint), [balanceLamports, mint]);
useEffect(() => { useEffect(() => {
const updateBalance = () => { const updateBalance = () => {
setBalanceInUSD( setBalanceInUSD(balance * midPriceInUSD(reserve?.liquidityMint?.toBase58() || ''));
balance * midPriceInUSD(reserve?.liquidityMint?.toBase58() || "")
);
}; };
const dispose = marketEmitter.onMarket((args) => { const dispose = marketEmitter.onMarket((args) => {
if (args.ids.has(reserve?.dexMarket.toBase58() || "")) { if (args.ids.has(reserve?.dexMarket.toBase58() || '')) {
updateBalance(); updateBalance();
} }
}); });
@ -58,12 +47,6 @@ export function useUserCollateralBalance(
hasBalance: accounts.length > 0 && balance > 0, hasBalance: accounts.length > 0 && balance > 0,
}; };
} }
export function calculateCollateralBalance( export function calculateCollateralBalance(reserve: LendingReserve, balanceLamports: number) {
reserve: LendingReserve, return reserveMarketCap(reserve) * (balanceLamports / (reserve?.state.collateralMintSupply.toNumber() || 1));
balanceLamports: number
) {
return (
reserveMarketCap(reserve) *
(balanceLamports / (reserve?.state.collateralMintSupply.toNumber() || 1))
);
} }

View File

@ -1,17 +1,13 @@
import { PublicKey } from "@solana/web3.js"; import { PublicKey } from '@solana/web3.js';
import { useCallback, useEffect, useMemo, useState } from "react"; import { useCallback, useEffect, useMemo, useState } from 'react';
import { cache, ParsedAccount } from "./../contexts/accounts"; import { cache, ParsedAccount } from 'common/src/contexts/accounts';
import { useLendingObligations } from "./useLendingObligations"; import { useLendingObligations } from './useLendingObligations';
import { import { collateralToLiquidity, LendingObligation, LendingReserve } from '../models/lending';
collateralToLiquidity, import { useLendingReserves } from './useLendingReserves';
LendingObligation, import { fromLamports, getTokenName, wadToLamports } from 'common/src/utils/utils';
LendingReserve, import { MintInfo } from '@solana/spl-token';
} from "../models/lending"; import { simulateMarketOrderFill, useMarkets } from '../contexts/market';
import { useLendingReserves } from "./useLendingReserves"; import { useConnectionConfig } from 'common/src/contexts/connection';
import { fromLamports, getTokenName, wadToLamports } from "../utils/utils";
import { MintInfo } from "@solana/spl-token";
import { simulateMarketOrderFill, useMarkets } from "../contexts/market";
import { useConnectionConfig } from "../contexts/connection";
interface EnrichedLendingObligationInfo extends LendingObligation { interface EnrichedLendingObligationInfo extends LendingObligation {
ltv: number; ltv: number;
@ -50,9 +46,7 @@ export function useEnrichedLendingObligations() {
obligations obligations
.map((obligation) => ({ .map((obligation) => ({
obligation, obligation,
reserve: availableReserves.get( reserve: availableReserves.get(obligation.info.borrowReserve.toBase58()) as ParsedAccount<LendingReserve>,
obligation.info.borrowReserve.toBase58()
) as ParsedAccount<LendingReserve>,
collateralReserve: availableReserves.get( collateralReserve: availableReserves.get(
obligation.info.collateralReserve.toBase58() obligation.info.collateralReserve.toBase58()
) as ParsedAccount<LendingReserve>, ) as ParsedAccount<LendingReserve>,
@ -60,63 +54,42 @@ export function useEnrichedLendingObligations() {
// use obligations with reserves available // use obligations with reserves available
.filter((item) => item.reserve) .filter((item) => item.reserve)
// use reserves with borrow amount greater than zero // use reserves with borrow amount greater than zero
.filter( .filter((item) => wadToLamports(item.obligation.info.borrowAmountWad).toNumber() > 0)
(item) =>
wadToLamports(item.obligation.info.borrowAmountWad).toNumber() > 0
)
.map((item) => { .map((item) => {
const obligation = item.obligation; const obligation = item.obligation;
const reserve = item.reserve.info; const reserve = item.reserve.info;
const collateralReserve = item.reserve.info; const collateralReserve = item.reserve.info;
const liquidityMint = cache.get( const liquidityMint = cache.get(reserve.liquidityMint) as ParsedAccount<MintInfo>;
reserve.liquidityMint
) as ParsedAccount<MintInfo>;
let ltv = 0; let ltv = 0;
let health = 0; let health = 0;
let borrowedInQuote = 0; let borrowedInQuote = 0;
let collateralInQuote = 0; let collateralInQuote = 0;
if (liquidityMint) { if (liquidityMint) {
const collateralMint = cache.get( const collateralMint = cache.get(item.collateralReserve.info.liquidityMint);
item.collateralReserve.info.liquidityMint
);
const collateral = fromLamports( const collateral = fromLamports(
collateralToLiquidity( collateralToLiquidity(obligation.info.depositedCollateral, item.reserve.info),
obligation.info.depositedCollateral,
item.reserve.info
),
collateralMint?.info collateralMint?.info
); );
const borrowed = wadToLamports( const borrowed = wadToLamports(obligation.info.borrowAmountWad).toNumber();
obligation.info.borrowAmountWad
).toNumber();
const borrowedAmount = simulateMarketOrderFill( const borrowedAmount = simulateMarketOrderFill(
borrowed, borrowed,
item.reserve.info, item.reserve.info,
item.reserve.info.dexMarketOption item.reserve.info.dexMarketOption ? item.reserve.info.dexMarket : item.collateralReserve.info.dexMarket,
? item.reserve.info.dexMarket
: item.collateralReserve.info.dexMarket,
true true
); );
const liquidityMintAddress = item.reserve.info.liquidityMint.toBase58(); const liquidityMintAddress = item.reserve.info.liquidityMint.toBase58();
const liquidityMint = cache.get( const liquidityMint = cache.get(liquidityMintAddress) as ParsedAccount<MintInfo>;
liquidityMintAddress borrowedInQuote = fromLamports(borrowed, liquidityMint.info) * midPriceInUSD(liquidityMintAddress);
) as ParsedAccount<MintInfo>; collateralInQuote = collateral * midPriceInUSD(collateralMint?.pubkey.toBase58() || '');
borrowedInQuote =
fromLamports(borrowed, liquidityMint.info) *
midPriceInUSD(liquidityMintAddress);
collateralInQuote =
collateral *
midPriceInUSD(collateralMint?.pubkey.toBase58() || "");
ltv = (100 * borrowedAmount) / collateral; ltv = (100 * borrowedAmount) / collateral;
const liquidationThreshold = const liquidationThreshold = item.reserve.info.config.liquidationThreshold;
item.reserve.info.config.liquidationThreshold;
health = (collateral * liquidationThreshold) / 100 / borrowedAmount; health = (collateral * liquidationThreshold) / 100 / borrowedAmount;
} }
@ -128,13 +101,9 @@ export function useEnrichedLendingObligations() {
health, health,
borrowedInQuote, borrowedInQuote,
collateralInQuote, collateralInQuote,
liquidationThreshold: liquidationThreshold: item.reserve.info.config.liquidationThreshold,
item.reserve.info.config.liquidationThreshold,
repayName: getTokenName(tokenMap, reserve.liquidityMint), repayName: getTokenName(tokenMap, reserve.liquidityMint),
collateralName: getTokenName( collateralName: getTokenName(tokenMap, collateralReserve.liquidityMint),
tokenMap,
collateralReserve.liquidityMint
),
}, },
} as EnrichedLendingObligation; } as EnrichedLendingObligation;
}) })
@ -142,9 +111,7 @@ export function useEnrichedLendingObligations() {
); );
}, [obligations, availableReserves, midPriceInUSD, tokenMap]); }, [obligations, availableReserves, midPriceInUSD, tokenMap]);
const [enriched, setEnriched] = useState<EnrichedLendingObligation[]>( const [enriched, setEnriched] = useState<EnrichedLendingObligation[]>(enrichedFactory());
enrichedFactory()
);
useEffect(() => { useEffect(() => {
const dispose = marketEmitter.onMarket(() => { const dispose = marketEmitter.onMarket(() => {
@ -162,7 +129,7 @@ export function useEnrichedLendingObligations() {
} }
export function useEnrichedLendingObligation(address?: string | PublicKey) { export function useEnrichedLendingObligation(address?: string | PublicKey) {
const id = typeof address === "string" ? address : address?.toBase58(); const id = typeof address === 'string' ? address : address?.toBase58();
const { obligations } = useEnrichedLendingObligations(); const { obligations } = useEnrichedLendingObligations();
const obligation = useMemo(() => { const obligation = useMemo(() => {

View File

@ -1,7 +1,7 @@
import { PublicKey } from "@solana/web3.js"; import { PublicKey } from '@solana/web3.js';
import { useEffect, useState } from "react"; import { useEffect, useState } from 'react';
import { LendingMarketParser, LendingMarket } from "../models/lending"; import { LendingMarketParser, LendingMarket } from '../models/lending';
import { cache, ParsedAccount } from "./../contexts/accounts"; import { cache, ParsedAccount } from 'common/src/contexts/accounts';
const getLendingMarkets = () => { const getLendingMarkets = () => {
return cache return cache
@ -11,9 +11,7 @@ const getLendingMarkets = () => {
}; };
export function useLendingMarkets() { export function useLendingMarkets() {
const [lendingMarkets, setLendingMarket] = useState< const [lendingMarkets, setLendingMarket] = useState<ParsedAccount<LendingMarket>[]>(getLendingMarkets());
ParsedAccount<LendingMarket>[]
>(getLendingMarkets());
useEffect(() => { useEffect(() => {
const dispose = cache.emitter.onCache((args) => { const dispose = cache.emitter.onCache((args) => {
@ -33,10 +31,10 @@ export function useLendingMarkets() {
} }
export function useLendingMarket(address?: string | PublicKey) { export function useLendingMarket(address?: string | PublicKey) {
const id = typeof address === "string" ? address : address?.toBase58(); const id = typeof address === 'string' ? address : address?.toBase58();
const [lendingMarket, setLendingMarket] = useState< const [lendingMarket, setLendingMarket] = useState<ParsedAccount<LendingMarket>>(
ParsedAccount<LendingMarket> cache.get(id || '') as ParsedAccount<LendingMarket>
>(cache.get(id || "") as ParsedAccount<LendingMarket>); );
useEffect(() => { useEffect(() => {
const dispose = cache.emitter.onCache((args) => { const dispose = cache.emitter.onCache((args) => {

View File

@ -1,7 +1,7 @@
import { PublicKey } from "@solana/web3.js"; import { PublicKey } from '@solana/web3.js';
import { useEffect, useState } from "react"; import { useEffect, useState } from 'react';
import { LendingObligation, LendingObligationParser } from "../models/lending"; import { LendingObligation, LendingObligationParser } from '../models/lending';
import { cache, ParsedAccount } from "./../contexts/accounts"; import { cache, ParsedAccount } from 'common/src/contexts/accounts';
const getLendingObligations = () => { const getLendingObligations = () => {
return cache return cache
@ -31,10 +31,8 @@ export function useLendingObligations() {
} }
export function useLendingObligation(address?: string | PublicKey) { export function useLendingObligation(address?: string | PublicKey) {
const id = typeof address === "string" ? address : address?.toBase58(); const id = typeof address === 'string' ? address : address?.toBase58();
const [obligationAccount, setObligationAccount] = useState( const [obligationAccount, setObligationAccount] = useState(cache.get(id || '') as ParsedAccount<LendingObligation>);
cache.get(id || "") as ParsedAccount<LendingObligation>
);
useEffect(() => { useEffect(() => {
const dispose = cache.emitter.onCache((args) => { const dispose = cache.emitter.onCache((args) => {

View File

@ -1,9 +1,9 @@
import { PublicKey } from "@solana/web3.js"; import { PublicKey } from '@solana/web3.js';
import { useEffect, useMemo, useState } from "react"; import { useEffect, useMemo, useState } from 'react';
import { LendingReserve, LendingReserveParser } from "../models/lending"; import { LendingReserve, LendingReserveParser } from '../models/lending';
import { cache, ParsedAccount } from "./../contexts/accounts"; import { cache, ParsedAccount } from 'common/src/contexts/accounts';
import { useConnectionConfig } from "./../contexts/connection"; import { useConnectionConfig } from 'common/src/contexts/connection';
import { getTokenByName, KnownToken } from "../utils/utils"; import { getTokenByName, KnownToken } from 'common/src/utils/utils';
export const getLendingReserves = () => { export const getLendingReserves = () => {
return cache return cache
@ -13,9 +13,7 @@ export const getLendingReserves = () => {
}; };
export function useLendingReserves() { export function useLendingReserves() {
const [reserveAccounts, setReserveAccounts] = useState< const [reserveAccounts, setReserveAccounts] = useState<ParsedAccount<LendingReserve>[]>(getLendingReserves());
ParsedAccount<LendingReserve>[]
>(getLendingReserves());
useEffect(() => { useEffect(() => {
const dispose = cache.emitter.onCache((args) => { const dispose = cache.emitter.onCache((args) => {
@ -38,26 +36,20 @@ export function useLendingReserve(address?: string | PublicKey) {
const { tokenMap } = useConnectionConfig(); const { tokenMap } = useConnectionConfig();
const { reserveAccounts } = useLendingReserves(); const { reserveAccounts } = useLendingReserves();
let addressName = address; let addressName = address;
if (typeof address === "string") { if (typeof address === 'string') {
const token: KnownToken | null = getTokenByName(tokenMap, address); const token: KnownToken | null = getTokenByName(tokenMap, address);
if (token) { if (token) {
const account = reserveAccounts.filter( const account = reserveAccounts.filter((acc) => acc.info.liquidityMint.toBase58() === token.mintAddress)[0];
(acc) => acc.info.liquidityMint.toBase58() === token.mintAddress
)[0];
if (account) { if (account) {
addressName = account.pubkey; addressName = account.pubkey;
} }
} }
} }
const id = useMemo( const id = useMemo(() => (typeof addressName === 'string' ? addressName : addressName?.toBase58()), [addressName]);
() =>
typeof addressName === "string" ? addressName : addressName?.toBase58(),
[addressName]
);
const [reserveAccount, setReserveAccount] = useState< const [reserveAccount, setReserveAccount] = useState<ParsedAccount<LendingReserve>>(
ParsedAccount<LendingReserve> cache.get(id || '') as ParsedAccount<LendingReserve>
>(cache.get(id || "") as ParsedAccount<LendingReserve>); );
useEffect(() => { useEffect(() => {
const dispose = cache.emitter.onCache((args) => { const dispose = cache.emitter.onCache((args) => {

View File

@ -1,10 +0,0 @@
import { PublicKey } from "@solana/web3.js";
import { useConnectionConfig } from "../contexts/connection";
import { getTokenName } from "../utils/utils";
export function useTokenName(mintAddress?: string | PublicKey) {
const { tokenMap } = useConnectionConfig();
const address =
typeof mintAddress === "string" ? mintAddress : mintAddress?.toBase58();
return getTokenName(tokenMap, address);
}

View File

@ -1,19 +1,12 @@
import { PublicKey } from "@solana/web3.js"; import { PublicKey } from '@solana/web3.js';
import { useEffect, useMemo, useState } from "react"; import { useEffect, useMemo, useState } from 'react';
import { useMint } from "../contexts/accounts"; import { useMint } from 'common/src/contexts/accounts';
import { useMarkets } from "../contexts/market"; import { useMarkets } from '../contexts/market';
import { fromLamports } from "../utils/utils"; import { fromLamports } from 'common/src/utils/utils';
import { useUserAccounts } from "./useUserAccounts"; import { useUserAccounts } from 'common/src/hooks/useUserAccounts';
export function useUserBalance( export function useUserBalance(mintAddress?: PublicKey | string, account?: PublicKey) {
mintAddress?: PublicKey | string, const mint = useMemo(() => (typeof mintAddress === 'string' ? mintAddress : mintAddress?.toBase58()), [mintAddress]);
account?: PublicKey
) {
const mint = useMemo(
() =>
typeof mintAddress === "string" ? mintAddress : mintAddress?.toBase58(),
[mintAddress]
);
const { userAccounts } = useUserAccounts(); const { userAccounts } = useUserAccounts();
const [balanceInUSD, setBalanceInUSD] = useState(0); const [balanceInUSD, setBalanceInUSD] = useState(0);
const { marketEmitter, midPriceInUSD } = useMarkets(); const { marketEmitter, midPriceInUSD } = useMarkets();
@ -21,29 +14,19 @@ export function useUserBalance(
const mintInfo = useMint(mint); const mintInfo = useMint(mint);
const accounts = useMemo(() => { const accounts = useMemo(() => {
return userAccounts return userAccounts
.filter( .filter((acc) => mint === acc.info.mint.toBase58() && (!account || account.equals(acc.pubkey)))
(acc) =>
mint === acc.info.mint.toBase58() &&
(!account || account.equals(acc.pubkey))
)
.sort((a, b) => b.info.amount.sub(a.info.amount).toNumber()); .sort((a, b) => b.info.amount.sub(a.info.amount).toNumber());
}, [userAccounts, mint, account]); }, [userAccounts, mint, account]);
const balanceLamports = useMemo(() => { const balanceLamports = useMemo(() => {
return accounts.reduce( return accounts.reduce((res, item) => (res += item.info.amount.toNumber()), 0);
(res, item) => (res += item.info.amount.toNumber()),
0
);
}, [accounts]); }, [accounts]);
const balance = useMemo(() => fromLamports(balanceLamports, mintInfo), [ const balance = useMemo(() => fromLamports(balanceLamports, mintInfo), [mintInfo, balanceLamports]);
mintInfo,
balanceLamports,
]);
useEffect(() => { useEffect(() => {
const updateBalance = () => { const updateBalance = () => {
setBalanceInUSD(balance * midPriceInUSD(mint || "")); setBalanceInUSD(balance * midPriceInUSD(mint || ''));
}; };
const dispose = marketEmitter.onMarket((args) => { const dispose = marketEmitter.onMarket((args) => {

View File

@ -1,14 +1,14 @@
import { cache, ParsedAccount } from "../contexts/accounts"; import { cache, ParsedAccount } from 'common/src/contexts/accounts';
import { calculateDepositAPY, LendingReserve } from "../models/lending"; import { calculateDepositAPY, LendingReserve } from '../models/lending';
import { useUserAccounts } from "./useUserAccounts"; import { useUserAccounts } from 'common/src/hooks/useUserAccounts';
import { useLendingReserves } from "./useLendingReserves"; import { useLendingReserves } from './useLendingReserves';
import { useEffect, useMemo, useState } from "react"; import { useEffect, useMemo, useState } from 'react';
import { TokenAccount } from "../models"; import { TokenAccount } from 'common/src/models';
import { useMarkets } from "../contexts/market"; import { useMarkets } from '../contexts/market';
import { fromLamports, getTokenName } from "../utils/utils"; import { fromLamports, getTokenName } from 'common/src/utils/utils';
import { useConnectionConfig } from "../contexts/connection"; import { useConnectionConfig } from 'common/src/contexts/connection';
import { calculateCollateralBalance } from "./useCollateralBalance"; import { calculateCollateralBalance } from './useCollateralBalance';
import { MintInfo } from "@solana/spl-token"; import { MintInfo } from '@solana/spl-token';
export interface UserDeposit { export interface UserDeposit {
account: TokenAccount; account: TokenAccount;
@ -45,28 +45,17 @@ export function useUserDeposits(exclude?: Set<string>, include?: Set<string>) {
}, [reserveAccounts, exclude, include]); }, [reserveAccounts, exclude, include]);
useEffect(() => { useEffect(() => {
const activeMarkets = new Set( const activeMarkets = new Set(reserveAccounts.map((r) => r.info.dexMarket.toBase58()));
reserveAccounts.map((r) => r.info.dexMarket.toBase58())
);
const userDepositsFactory = () => { const userDepositsFactory = () => {
return userAccounts return userAccounts
.filter((acc) => .filter((acc) => reservesByCollateralMint.has(acc?.info.mint.toBase58()))
reservesByCollateralMint.has(acc?.info.mint.toBase58())
)
.map((item) => { .map((item) => {
const reserve = reservesByCollateralMint.get( const reserve = reservesByCollateralMint.get(item?.info.mint.toBase58()) as ParsedAccount<LendingReserve>;
item?.info.mint.toBase58()
) as ParsedAccount<LendingReserve>;
let collateralMint = cache.get( let collateralMint = cache.get(reserve.info.collateralMint) as ParsedAccount<MintInfo>;
reserve.info.collateralMint
) as ParsedAccount<MintInfo>;
const amountLamports = calculateCollateralBalance( const amountLamports = calculateCollateralBalance(reserve.info, item?.info.amount.toNumber());
reserve.info,
item?.info.amount.toNumber()
);
const amount = fromLamports(amountLamports, collateralMint?.info); const amount = fromLamports(amountLamports, collateralMint?.info);
const price = midPriceInUSD(reserve.info.liquidityMint.toBase58()); const price = midPriceInUSD(reserve.info.liquidityMint.toBase58());
const amountInQuote = price * amount; const amountInQuote = price * amount;
@ -99,20 +88,10 @@ export function useUserDeposits(exclude?: Set<string>, include?: Set<string>) {
return () => { return () => {
dispose(); dispose();
}; };
}, [ }, [userAccounts, reserveAccounts, reservesByCollateralMint, tokenMap, midPriceInUSD, marketEmitter]);
userAccounts,
reserveAccounts,
reservesByCollateralMint,
tokenMap,
midPriceInUSD,
marketEmitter,
]);
return { return {
userDeposits, userDeposits,
totalInQuote: userDeposits.reduce( totalInQuote: userDeposits.reduce((res, item) => res + item.info.amountInQuote, 0),
(res, item) => res + item.info.amountInQuote,
0
),
}; };
} }

View File

@ -1,7 +1,7 @@
import { useMemo } from "react"; import { useMemo } from 'react';
import { useUserAccounts } from "./useUserAccounts"; import { useUserAccounts } from 'common/src/hooks/useUserAccounts';
import { useEnrichedLendingObligations } from "./useEnrichedLendingObligations"; import { useEnrichedLendingObligations } from './useEnrichedLendingObligations';
import { TokenAccount } from "../models"; import { TokenAccount } from 'common/src/models';
export function useUserObligations() { export function useUserObligations() {
const { userAccounts } = useUserAccounts(); const { userAccounts } = useUserAccounts();
@ -21,26 +21,18 @@ export function useUserObligations() {
} }
return obligations return obligations
.filter( .filter((acc) => accountsByMint.get(acc.info.tokenMint.toBase58()) !== undefined)
(acc) => accountsByMint.get(acc.info.tokenMint.toBase58()) !== undefined
)
.map((ob) => { .map((ob) => {
return { return {
obligation: ob, obligation: ob,
userAccounts: [...accountsByMint.get(ob.info.tokenMint.toBase58())], userAccounts: [...accountsByMint.get(ob.info.tokenMint.toBase58())],
}; };
}) })
.sort( .sort((a, b) => b.obligation.info.borrowedInQuote - a.obligation.info.borrowedInQuote);
(a, b) =>
b.obligation.info.borrowedInQuote - a.obligation.info.borrowedInQuote
);
}, [accountsByMint, obligations]); }, [accountsByMint, obligations]);
return { return {
userObligations, userObligations,
totalInQuote: userObligations.reduce( totalInQuote: userObligations.reduce((result, item) => result + item.obligation.info.borrowedInQuote, 0),
(result, item) => result + item.obligation.info.borrowedInQuote,
0
),
}; };
} }

View File

@ -1,10 +1,6 @@
import { Market, MARKETS, Orderbook } from "@project-serum/serum"; import { Market, MARKETS, Orderbook } from '@project-serum/serum';
import { AccountInfo, PublicKey } from "@solana/web3.js"; import { AccountInfo, PublicKey } from '@solana/web3.js';
import { import { MintParser, ParsedAccountBase, cache } from 'common/src/contexts/accounts';
MintParser,
ParsedAccountBase,
cache,
} from "./../../contexts/accounts";
export const OrderBookParser = (id: PublicKey, acc: AccountInfo<Buffer>) => { export const OrderBookParser = (id: PublicKey, acc: AccountInfo<Buffer>) => {
const decoded = Orderbook.LAYOUT.decode(acc.data); const decoded = Orderbook.LAYOUT.decode(acc.data);
@ -20,18 +16,11 @@ export const OrderBookParser = (id: PublicKey, acc: AccountInfo<Buffer>) => {
return details; return details;
}; };
const DEFAULT_DEX_ID = new PublicKey( const DEFAULT_DEX_ID = new PublicKey('EUqojwWA2rd19FZrzeBncJsm38Jm1hEhE3zsmX3bRc2o');
"EUqojwWA2rd19FZrzeBncJsm38Jm1hEhE3zsmX3bRc2o"
);
export const DexMarketParser = ( export const DexMarketParser = (pubkey: PublicKey, acc: AccountInfo<Buffer>) => {
pubkey: PublicKey,
acc: AccountInfo<Buffer>
) => {
const market = MARKETS.find((m) => m.address.equals(pubkey)); const market = MARKETS.find((m) => m.address.equals(pubkey));
const decoded = Market.getLayout(market?.programId || DEFAULT_DEX_ID).decode( const decoded = Market.getLayout(market?.programId || DEFAULT_DEX_ID).decode(acc.data);
acc.data
);
const details = { const details = {
pubkey, pubkey,

View File

@ -1,5 +1,3 @@
export * from "./account"; export * from './lending';
export * from "./lending"; export * from './pool';
export * from "./tokenSwap"; export * from './totals';
export * from "./pool";
export * from "./totals";

View File

@ -1,14 +1,10 @@
import { import { PublicKey, SYSVAR_CLOCK_PUBKEY, TransactionInstruction } from '@solana/web3.js';
PublicKey, import BN from 'bn.js';
SYSVAR_CLOCK_PUBKEY, import * as BufferLayout from 'buffer-layout';
TransactionInstruction, import { TOKEN_PROGRAM_ID, LENDING_PROGRAM_ID } from 'common/src/utils/ids';
} from "@solana/web3.js"; import * as Layout from 'common/src/utils/layout';
import BN from "bn.js"; import { LendingInstruction } from './lending';
import * as BufferLayout from "buffer-layout"; import { calculateUtilizationRatio, LendingReserve } from './reserve';
import { TOKEN_PROGRAM_ID, LENDING_PROGRAM_ID } from "../../utils/ids";
import * as Layout from "./../../utils/layout";
import { LendingInstruction } from "./lending";
import { calculateUtilizationRatio, LendingReserve } from "./reserve";
export enum BorrowAmountType { export enum BorrowAmountType {
LiquidityBorrowAmount = 0, LiquidityBorrowAmount = 0,
@ -64,9 +60,9 @@ export const borrowInstruction = (
hostFeeReceiver?: PublicKey hostFeeReceiver?: PublicKey
): TransactionInstruction => { ): TransactionInstruction => {
const dataLayout = BufferLayout.struct([ const dataLayout = BufferLayout.struct([
BufferLayout.u8("instruction"), BufferLayout.u8('instruction'),
Layout.uint64("amount"), Layout.uint64('amount'),
BufferLayout.u8("amountType"), BufferLayout.u8('amountType'),
]); ]);
const data = Buffer.alloc(dataLayout.span); const data = Buffer.alloc(dataLayout.span);
@ -133,16 +129,12 @@ export const calculateBorrowAPY = (reserve: LendingReserve) => {
const normalizedFactor = currentUtilization / optimalUtilization; const normalizedFactor = currentUtilization / optimalUtilization;
const optimalBorrowRate = reserve.config.optimalBorrowRate / 100; const optimalBorrowRate = reserve.config.optimalBorrowRate / 100;
const minBorrowRate = reserve.config.minBorrowRate / 100; const minBorrowRate = reserve.config.minBorrowRate / 100;
borrowAPY = borrowAPY = normalizedFactor * (optimalBorrowRate - minBorrowRate) + minBorrowRate;
normalizedFactor * (optimalBorrowRate - minBorrowRate) + minBorrowRate;
} else { } else {
const normalizedFactor = const normalizedFactor = (currentUtilization - optimalUtilization) / (1 - optimalUtilization);
(currentUtilization - optimalUtilization) / (1 - optimalUtilization);
const optimalBorrowRate = reserve.config.optimalBorrowRate / 100; const optimalBorrowRate = reserve.config.optimalBorrowRate / 100;
const maxBorrowRate = reserve.config.maxBorrowRate / 100; const maxBorrowRate = reserve.config.maxBorrowRate / 100;
borrowAPY = borrowAPY = normalizedFactor * (maxBorrowRate - optimalBorrowRate) + optimalBorrowRate;
normalizedFactor * (maxBorrowRate - optimalBorrowRate) +
optimalBorrowRate;
} }
return borrowAPY; return borrowAPY;

View File

@ -1,15 +1,11 @@
import { import { PublicKey, SYSVAR_CLOCK_PUBKEY, TransactionInstruction } from '@solana/web3.js';
PublicKey, import BN from 'bn.js';
SYSVAR_CLOCK_PUBKEY, import * as BufferLayout from 'buffer-layout';
TransactionInstruction, import { TOKEN_PROGRAM_ID, LENDING_PROGRAM_ID } from 'common/src/utils/ids';
} from "@solana/web3.js"; import * as Layout from 'common/src/utils/layout';
import BN from "bn.js"; import { calculateBorrowAPY } from './borrow';
import * as BufferLayout from "buffer-layout"; import { LendingInstruction } from './lending';
import { TOKEN_PROGRAM_ID, LENDING_PROGRAM_ID } from "../../utils/ids"; import { calculateUtilizationRatio, LendingReserve } from './reserve';
import * as Layout from "./../../utils/layout";
import { calculateBorrowAPY } from "./borrow";
import { LendingInstruction } from "./lending";
import { calculateUtilizationRatio, LendingReserve } from "./reserve";
/// Deposit liquidity into a reserve. The output is a collateral token representing ownership /// Deposit liquidity into a reserve. The output is a collateral token representing ownership
/// of the reserve liquidity pool. /// of the reserve liquidity pool.
@ -35,10 +31,7 @@ export const depositInstruction = (
reserveSupply: PublicKey, reserveSupply: PublicKey,
collateralMint: PublicKey collateralMint: PublicKey
): TransactionInstruction => { ): TransactionInstruction => {
const dataLayout = BufferLayout.struct([ const dataLayout = BufferLayout.struct([BufferLayout.u8('instruction'), Layout.uint64('liquidityAmount')]);
BufferLayout.u8("instruction"),
Layout.uint64("liquidityAmount"),
]);
const data = Buffer.alloc(dataLayout.span); const data = Buffer.alloc(dataLayout.span);
dataLayout.encode( dataLayout.encode(

View File

@ -1,13 +1,9 @@
import { import { PublicKey, SYSVAR_CLOCK_PUBKEY, TransactionInstruction } from '@solana/web3.js';
PublicKey, import BN from 'bn.js';
SYSVAR_CLOCK_PUBKEY, import { LendingInstruction } from './lending';
TransactionInstruction, import * as BufferLayout from 'buffer-layout';
} from "@solana/web3.js"; import * as Layout from 'common/src/utils/layout';
import BN from "bn.js"; import { TOKEN_PROGRAM_ID, LENDING_PROGRAM_ID } from 'common/src/utils/ids';
import { LendingInstruction } from "./lending";
import * as BufferLayout from "buffer-layout";
import * as Layout from "./../../utils/layout";
import { TOKEN_PROGRAM_ID, LENDING_PROGRAM_ID } from "../../utils/ids";
/// Purchase collateral tokens at a discount rate if the chosen obligation is unhealthy. /// Purchase collateral tokens at a discount rate if the chosen obligation is unhealthy.
/// ///
@ -43,10 +39,7 @@ export const liquidateInstruction = (
dexOrderBookSide: PublicKey, dexOrderBookSide: PublicKey,
memory: PublicKey memory: PublicKey
): TransactionInstruction => { ): TransactionInstruction => {
const dataLayout = BufferLayout.struct([ const dataLayout = BufferLayout.struct([BufferLayout.u8('instruction'), Layout.uint64('liquidityAmount')]);
BufferLayout.u8("instruction"),
Layout.uint64("liquidityAmount"),
]);
const data = Buffer.alloc(dataLayout.span); const data = Buffer.alloc(dataLayout.span);
dataLayout.encode( dataLayout.encode(

View File

@ -1,19 +1,17 @@
import { AccountInfo, PublicKey } from "@solana/web3.js"; import { AccountInfo, PublicKey } from '@solana/web3.js';
import * as BufferLayout from "buffer-layout"; import * as BufferLayout from 'buffer-layout';
import * as Layout from "./../../utils/layout"; import * as Layout from 'common/src/utils/layout';
export const LendingMarketLayout: typeof BufferLayout.Structure = BufferLayout.struct( export const LendingMarketLayout: typeof BufferLayout.Structure = BufferLayout.struct([
[ BufferLayout.u8('version'),
BufferLayout.u8("version"), BufferLayout.u8('bumpSeed'),
BufferLayout.u8("bumpSeed"), Layout.publicKey('owner'),
Layout.publicKey("owner"), Layout.publicKey('quoteMint'),
Layout.publicKey("quoteMint"), Layout.publicKey('tokenProgramId'),
Layout.publicKey("tokenProgramId"),
// extra space for future contract changes // extra space for future contract changes
BufferLayout.blob(62, "padding"), BufferLayout.blob(62, 'padding'),
] ]);
);
export interface LendingMarket { export interface LendingMarket {
version: number; version: number;
@ -27,10 +25,7 @@ export const isLendingMarket = (info: AccountInfo<Buffer>) => {
return info.data.length === LendingMarketLayout.span; return info.data.length === LendingMarketLayout.span;
}; };
export const LendingMarketParser = ( export const LendingMarketParser = (pubKey: PublicKey, info: AccountInfo<Buffer>) => {
pubKey: PublicKey,
info: AccountInfo<Buffer>
) => {
const buffer = Buffer.from(info.data); const buffer = Buffer.from(info.data);
const data = LendingMarketLayout.decode(buffer); const data = LendingMarketLayout.decode(buffer);

View File

@ -4,33 +4,31 @@ import {
SYSVAR_CLOCK_PUBKEY, SYSVAR_CLOCK_PUBKEY,
SYSVAR_RENT_PUBKEY, SYSVAR_RENT_PUBKEY,
TransactionInstruction, TransactionInstruction,
} from "@solana/web3.js"; } from '@solana/web3.js';
import BN from "bn.js"; import BN from 'bn.js';
import * as BufferLayout from "buffer-layout"; import * as BufferLayout from 'buffer-layout';
import { LendingInstruction } from "."; import { LendingInstruction } from '.';
import { LENDING_PROGRAM_ID, TOKEN_PROGRAM_ID } from "../../utils/ids"; import { LENDING_PROGRAM_ID, TOKEN_PROGRAM_ID } from 'common/src/utils/ids';
import * as Layout from "./../../utils/layout"; import * as Layout from 'common/src/utils/layout';
export const LendingObligationLayout: typeof BufferLayout.Structure = BufferLayout.struct( export const LendingObligationLayout: typeof BufferLayout.Structure = BufferLayout.struct([
[ BufferLayout.u8('version'),
BufferLayout.u8("version"), /// Amount of collateral tokens deposited for this obligation
/// Amount of collateral tokens deposited for this obligation Layout.uint64('depositedCollateral'),
Layout.uint64("depositedCollateral"), /// Reserve which collateral tokens were deposited into
/// Reserve which collateral tokens were deposited into Layout.publicKey('collateralReserve'),
Layout.publicKey("collateralReserve"), /// Borrow rate used for calculating interest.
/// Borrow rate used for calculating interest. Layout.uint128('cumulativeBorrowRateWad'),
Layout.uint128("cumulativeBorrowRateWad"), /// Amount of tokens borrowed for this obligation plus interest
/// Amount of tokens borrowed for this obligation plus interest Layout.uint128('borrowAmountWad'),
Layout.uint128("borrowAmountWad"), /// Reserve which tokens were borrowed from
/// Reserve which tokens were borrowed from Layout.publicKey('borrowReserve'),
Layout.publicKey("borrowReserve"), /// Mint address of the tokens for this obligation
/// Mint address of the tokens for this obligation Layout.publicKey('tokenMint'),
Layout.publicKey("tokenMint"),
// extra space for future contract changes // extra space for future contract changes
BufferLayout.blob(128, "padding"), BufferLayout.blob(128, 'padding'),
] ]);
);
export const isLendingObligation = (info: AccountInfo<Buffer>) => { export const isLendingObligation = (info: AccountInfo<Buffer>) => {
return info.data.length === LendingObligationLayout.span; return info.data.length === LendingObligationLayout.span;
@ -47,10 +45,7 @@ export interface LendingObligation {
tokenMint: PublicKey; tokenMint: PublicKey;
} }
export const LendingObligationParser = ( export const LendingObligationParser = (pubKey: PublicKey, info: AccountInfo<Buffer>) => {
pubKey: PublicKey,
info: AccountInfo<Buffer>
) => {
const buffer = Buffer.from(info.data); const buffer = Buffer.from(info.data);
const data = LendingObligationLayout.decode(buffer); const data = LendingObligationLayout.decode(buffer);
@ -66,7 +61,7 @@ export const LendingObligationParser = (
}; };
export const healthFactorToRiskColor = (health: number) => { export const healthFactorToRiskColor = (health: number) => {
return ""; return '';
}; };
export const initObligationInstruction = ( export const initObligationInstruction = (
@ -79,7 +74,7 @@ export const initObligationInstruction = (
lendingMarket: PublicKey, lendingMarket: PublicKey,
lendingMarketAuthority: PublicKey lendingMarketAuthority: PublicKey
): TransactionInstruction => { ): TransactionInstruction => {
const dataLayout = BufferLayout.struct([BufferLayout.u8("instruction")]); const dataLayout = BufferLayout.struct([BufferLayout.u8('instruction')]);
const data = Buffer.alloc(dataLayout.span); const data = Buffer.alloc(dataLayout.span);
dataLayout.encode( dataLayout.encode(

View File

@ -1,13 +1,9 @@
import { import { PublicKey, SYSVAR_CLOCK_PUBKEY, TransactionInstruction } from '@solana/web3.js';
PublicKey, import BN from 'bn.js';
SYSVAR_CLOCK_PUBKEY, import { LendingInstruction } from './lending';
TransactionInstruction, import * as BufferLayout from 'buffer-layout';
} from "@solana/web3.js"; import * as Layout from 'common/src/utils/layout';
import BN from "bn.js"; import { TOKEN_PROGRAM_ID, LENDING_PROGRAM_ID } from 'common/src/utils/ids';
import { LendingInstruction } from "./lending";
import * as BufferLayout from "buffer-layout";
import * as Layout from "./../../utils/layout";
import { TOKEN_PROGRAM_ID, LENDING_PROGRAM_ID } from "../../utils/ids";
/// Repay loaned tokens to a reserve and receive collateral tokens. The obligation balance /// Repay loaned tokens to a reserve and receive collateral tokens. The obligation balance
/// will be recalculated for interest. /// will be recalculated for interest.
@ -42,10 +38,7 @@ export const repayInstruction = (
authority: PublicKey, authority: PublicKey,
transferAuthority: PublicKey transferAuthority: PublicKey
): TransactionInstruction => { ): TransactionInstruction => {
const dataLayout = BufferLayout.struct([ const dataLayout = BufferLayout.struct([BufferLayout.u8('instruction'), Layout.uint64('liquidityAmount')]);
BufferLayout.u8("instruction"),
Layout.uint64("liquidityAmount"),
]);
const data = Buffer.alloc(dataLayout.span); const data = Buffer.alloc(dataLayout.span);
dataLayout.encode( dataLayout.encode(

View File

@ -4,82 +4,80 @@ import {
SYSVAR_CLOCK_PUBKEY, SYSVAR_CLOCK_PUBKEY,
SYSVAR_RENT_PUBKEY, SYSVAR_RENT_PUBKEY,
TransactionInstruction, TransactionInstruction,
} from "@solana/web3.js"; } from '@solana/web3.js';
import BN from "bn.js"; import BN from 'bn.js';
import * as BufferLayout from "buffer-layout"; import * as BufferLayout from 'buffer-layout';
import { TOKEN_PROGRAM_ID, LENDING_PROGRAM_ID } from "../../utils/ids"; import { TOKEN_PROGRAM_ID, LENDING_PROGRAM_ID } from 'common/src/utils/ids';
import { wadToLamports } from "../../utils/utils"; import { wadToLamports } from 'common/src/utils/utils';
import * as Layout from "./../../utils/layout"; import * as Layout from 'common/src/utils/layout';
import { LendingInstruction } from "./lending"; import { LendingInstruction } from './lending';
export const LendingReserveLayout: typeof BufferLayout.Structure = BufferLayout.struct( export const LendingReserveLayout: typeof BufferLayout.Structure = BufferLayout.struct([
[ BufferLayout.u8('version'),
BufferLayout.u8("version"), Layout.uint64('lastUpdateSlot'),
Layout.uint64("lastUpdateSlot"),
Layout.publicKey("lendingMarket"), Layout.publicKey('lendingMarket'),
Layout.publicKey("liquidityMint"), Layout.publicKey('liquidityMint'),
BufferLayout.u8("liquidityMintDecimals"), BufferLayout.u8('liquidityMintDecimals'),
Layout.publicKey("liquiditySupply"), Layout.publicKey('liquiditySupply'),
Layout.publicKey("collateralMint"), Layout.publicKey('collateralMint'),
Layout.publicKey("collateralSupply"), Layout.publicKey('collateralSupply'),
Layout.publicKey("collateralFeesReceiver"), Layout.publicKey('collateralFeesReceiver'),
// TODO: replace u32 option with generic quivalent // TODO: replace u32 option with generic quivalent
BufferLayout.u32("dexMarketOption"), BufferLayout.u32('dexMarketOption'),
Layout.publicKey("dexMarket"), Layout.publicKey('dexMarket'),
BufferLayout.struct( BufferLayout.struct(
[ [
/// Optimal utilization rate as a percent /// Optimal utilization rate as a percent
BufferLayout.u8("optimalUtilizationRate"), BufferLayout.u8('optimalUtilizationRate'),
/// The ratio of the loan to the value of the collateral as a percent /// The ratio of the loan to the value of the collateral as a percent
BufferLayout.u8("loanToValueRatio"), BufferLayout.u8('loanToValueRatio'),
/// The percent discount the liquidator gets when buying collateral for an unhealthy obligation /// The percent discount the liquidator gets when buying collateral for an unhealthy obligation
BufferLayout.u8("liquidationBonus"), BufferLayout.u8('liquidationBonus'),
/// The percent at which an obligation is considered unhealthy /// The percent at which an obligation is considered unhealthy
BufferLayout.u8("liquidationThreshold"), BufferLayout.u8('liquidationThreshold'),
/// Min borrow APY /// Min borrow APY
BufferLayout.u8("minBorrowRate"), BufferLayout.u8('minBorrowRate'),
/// Optimal (utilization) borrow APY /// Optimal (utilization) borrow APY
BufferLayout.u8("optimalBorrowRate"), BufferLayout.u8('optimalBorrowRate'),
/// Max borrow APY /// Max borrow APY
BufferLayout.u8("maxBorrowRate"), BufferLayout.u8('maxBorrowRate'),
BufferLayout.struct( BufferLayout.struct(
[ [
/// Fee assessed on `BorrowReserveLiquidity`, expressed as a Wad. /// Fee assessed on `BorrowReserveLiquidity`, expressed as a Wad.
/// Must be between 0 and 10^18, such that 10^18 = 1. A few examples for /// Must be between 0 and 10^18, such that 10^18 = 1. A few examples for
/// clarity: /// clarity:
/// 1% = 10_000_000_000_000_000 /// 1% = 10_000_000_000_000_000
/// 0.01% (1 basis point) = 100_000_000_000_000 /// 0.01% (1 basis point) = 100_000_000_000_000
/// 0.00001% (Aave borrow fee) = 100_000_000_000 /// 0.00001% (Aave borrow fee) = 100_000_000_000
Layout.uint64("borrowFeeWad"), Layout.uint64('borrowFeeWad'),
/// Amount of fee going to host account, if provided in liquidate and repay /// Amount of fee going to host account, if provided in liquidate and repay
BufferLayout.u8("hostFeePercentage"), BufferLayout.u8('hostFeePercentage'),
], ],
"fees" 'fees'
), ),
], ],
"config" 'config'
), ),
BufferLayout.struct( BufferLayout.struct(
[ [
Layout.uint128("cumulativeBorrowRateWad"), Layout.uint128('cumulativeBorrowRateWad'),
Layout.uint128("borrowedLiquidityWad"), Layout.uint128('borrowedLiquidityWad'),
Layout.uint64("availableLiquidity"), Layout.uint64('availableLiquidity'),
Layout.uint64("collateralMintSupply"), Layout.uint64('collateralMintSupply'),
], ],
"state" 'state'
), ),
// extra space for future contract changes // extra space for future contract changes
BufferLayout.blob(300, "padding"), BufferLayout.blob(300, 'padding'),
] ]);
);
export const isLendingReserve = (info: AccountInfo<Buffer>) => { export const isLendingReserve = (info: AccountInfo<Buffer>) => {
return info.data.length === LendingReserveLayout.span; return info.data.length === LendingReserveLayout.span;
@ -124,10 +122,7 @@ export interface LendingReserve {
}; };
} }
export const LendingReserveParser = ( export const LendingReserveParser = (pubKey: PublicKey, info: AccountInfo<Buffer>) => {
pubKey: PublicKey,
info: AccountInfo<Buffer>
) => {
const buffer = Buffer.from(info.data); const buffer = Buffer.from(info.data);
const data = LendingReserveLayout.decode(buffer) as LendingReserve; const data = LendingReserveLayout.decode(buffer) as LendingReserve;
@ -165,9 +160,9 @@ export const initReserveInstruction = (
dexMarket: PublicKey // TODO: optional dexMarket: PublicKey // TODO: optional
): TransactionInstruction => { ): TransactionInstruction => {
const dataLayout = BufferLayout.struct([ const dataLayout = BufferLayout.struct([
BufferLayout.u8("instruction"), BufferLayout.u8('instruction'),
Layout.uint64("liquidityAmount"), Layout.uint64('liquidityAmount'),
BufferLayout.u8("maxUtilizationRate"), BufferLayout.u8('maxUtilizationRate'),
]); ]);
const data = Buffer.alloc(dataLayout.span); const data = Buffer.alloc(dataLayout.span);
@ -207,10 +202,8 @@ export const initReserveInstruction = (
}); });
}; };
export const accrueInterestInstruction = ( export const accrueInterestInstruction = (...reserveAccount: PublicKey[]): TransactionInstruction => {
...reserveAccount: PublicKey[] const dataLayout = BufferLayout.struct([BufferLayout.u8('instruction')]);
): TransactionInstruction => {
const dataLayout = BufferLayout.struct([BufferLayout.u8("instruction")]);
const data = Buffer.alloc(dataLayout.span); const data = Buffer.alloc(dataLayout.span);
dataLayout.encode( dataLayout.encode(
@ -236,50 +229,30 @@ export const accrueInterestInstruction = (
}; };
export const calculateUtilizationRatio = (reserve: LendingReserve) => { export const calculateUtilizationRatio = (reserve: LendingReserve) => {
const totalBorrows = wadToLamports( const totalBorrows = wadToLamports(reserve.state.borrowedLiquidityWad).toNumber();
reserve.state.borrowedLiquidityWad const currentUtilization = totalBorrows / (reserve.state.availableLiquidity.toNumber() + totalBorrows);
).toNumber();
const currentUtilization =
totalBorrows / (reserve.state.availableLiquidity.toNumber() + totalBorrows);
return currentUtilization; return currentUtilization;
}; };
export const reserveMarketCap = (reserve?: LendingReserve) => { export const reserveMarketCap = (reserve?: LendingReserve) => {
const available = reserve?.state.availableLiquidity.toNumber() || 0; const available = reserve?.state.availableLiquidity.toNumber() || 0;
const borrowed = wadToLamports( const borrowed = wadToLamports(reserve?.state.borrowedLiquidityWad).toNumber();
reserve?.state.borrowedLiquidityWad
).toNumber();
const total = available + borrowed; const total = available + borrowed;
return total; return total;
}; };
export const collateralExchangeRate = (reserve?: LendingReserve) => { export const collateralExchangeRate = (reserve?: LendingReserve) => {
return ( return (reserve?.state.collateralMintSupply.toNumber() || 1) / reserveMarketCap(reserve);
(reserve?.state.collateralMintSupply.toNumber() || 1) /
reserveMarketCap(reserve)
);
}; };
export const collateralToLiquidity = ( export const collateralToLiquidity = (collateralAmount: BN | number, reserve?: LendingReserve) => {
collateralAmount: BN | number, const amount = typeof collateralAmount === 'number' ? collateralAmount : collateralAmount.toNumber();
reserve?: LendingReserve
) => {
const amount =
typeof collateralAmount === "number"
? collateralAmount
: collateralAmount.toNumber();
return Math.floor(amount / collateralExchangeRate(reserve)); return Math.floor(amount / collateralExchangeRate(reserve));
}; };
export const liquidityToCollateral = ( export const liquidityToCollateral = (liquidityAmount: BN | number, reserve?: LendingReserve) => {
liquidityAmount: BN | number, const amount = typeof liquidityAmount === 'number' ? liquidityAmount : liquidityAmount.toNumber();
reserve?: LendingReserve
) => {
const amount =
typeof liquidityAmount === "number"
? liquidityAmount
: liquidityAmount.toNumber();
return Math.floor(amount * collateralExchangeRate(reserve)); return Math.floor(amount * collateralExchangeRate(reserve));
}; };

View File

@ -1,13 +1,9 @@
import { import { PublicKey, SYSVAR_CLOCK_PUBKEY, TransactionInstruction } from '@solana/web3.js';
PublicKey, import BN from 'bn.js';
SYSVAR_CLOCK_PUBKEY, import * as BufferLayout from 'buffer-layout';
TransactionInstruction, import { TOKEN_PROGRAM_ID, LENDING_PROGRAM_ID } from 'common/src/utils/ids';
} from "@solana/web3.js"; import * as Layout from 'common/src/utils/layout';
import BN from "bn.js"; import { LendingInstruction } from './lending';
import * as BufferLayout from "buffer-layout";
import { TOKEN_PROGRAM_ID, LENDING_PROGRAM_ID } from "../../utils/ids";
import * as Layout from "./../../utils/layout";
import { LendingInstruction } from "./lending";
export const withdrawInstruction = ( export const withdrawInstruction = (
collateralAmount: number | BN, collateralAmount: number | BN,
@ -20,10 +16,7 @@ export const withdrawInstruction = (
authority: PublicKey, authority: PublicKey,
transferAuthority: PublicKey transferAuthority: PublicKey
): TransactionInstruction => { ): TransactionInstruction => {
const dataLayout = BufferLayout.struct([ const dataLayout = BufferLayout.struct([BufferLayout.u8('instruction'), Layout.uint64('collateralAmount')]);
BufferLayout.u8("instruction"),
Layout.uint64("collateralAmount"),
]);
const data = Buffer.alloc(dataLayout.span); const data = Buffer.alloc(dataLayout.span);
dataLayout.encode( dataLayout.encode(

View File

@ -1,5 +1,5 @@
import { PublicKey } from "@solana/web3.js"; import { PublicKey } from '@solana/web3.js';
import { TokenAccount } from "./account"; import { TokenAccount } from 'common/src/models/account';
export const DEFAULT_DENOMINATOR = 10_000; export const DEFAULT_DENOMINATOR = 10_000;

View File

@ -1,84 +0,0 @@
import * as BufferLayout from "buffer-layout";
import { publicKey, uint64 } from "../utils/layout";
export { TokenSwap } from "@solana/spl-token-swap";
const FEE_LAYOUT = BufferLayout.struct(
[
BufferLayout.nu64("tradeFeeNumerator"),
BufferLayout.nu64("tradeFeeDenominator"),
BufferLayout.nu64("ownerTradeFeeNumerator"),
BufferLayout.nu64("ownerTradeFeeDenominator"),
BufferLayout.nu64("ownerWithdrawFeeNumerator"),
BufferLayout.nu64("ownerWithdrawFeeDenominator"),
BufferLayout.nu64("hostFeeNumerator"),
BufferLayout.nu64("hostFeeDenominator"),
],
"fees"
);
export const TokenSwapLayoutLegacyV0 = BufferLayout.struct([
BufferLayout.u8("isInitialized"),
BufferLayout.u8("nonce"),
publicKey("tokenAccountA"),
publicKey("tokenAccountB"),
publicKey("tokenPool"),
uint64("feesNumerator"),
uint64("feesDenominator"),
]);
export const TokenSwapLayoutV1: typeof BufferLayout.Structure = BufferLayout.struct(
[
BufferLayout.u8("isInitialized"),
BufferLayout.u8("nonce"),
publicKey("tokenProgramId"),
publicKey("tokenAccountA"),
publicKey("tokenAccountB"),
publicKey("tokenPool"),
publicKey("mintA"),
publicKey("mintB"),
publicKey("feeAccount"),
BufferLayout.u8("curveType"),
uint64("tradeFeeNumerator"),
uint64("tradeFeeDenominator"),
uint64("ownerTradeFeeNumerator"),
uint64("ownerTradeFeeDenominator"),
uint64("ownerWithdrawFeeNumerator"),
uint64("ownerWithdrawFeeDenominator"),
BufferLayout.blob(16, "padding"),
]
);
const CURVE_NODE = BufferLayout.union(
BufferLayout.u8(),
BufferLayout.blob(32),
"curve"
);
CURVE_NODE.addVariant(0, BufferLayout.struct([]), "constantProduct");
CURVE_NODE.addVariant(
1,
BufferLayout.struct([BufferLayout.nu64("token_b_price")]),
"constantPrice"
);
CURVE_NODE.addVariant(2, BufferLayout.struct([]), "stable");
CURVE_NODE.addVariant(
3,
BufferLayout.struct([BufferLayout.nu64("token_b_offset")]),
"offset"
);
export const TokenSwapLayout: typeof BufferLayout.Structure = BufferLayout.struct(
[
BufferLayout.u8("isInitialized"),
BufferLayout.u8("nonce"),
publicKey("tokenProgramId"),
publicKey("tokenAccountA"),
publicKey("tokenAccountB"),
publicKey("tokenPool"),
publicKey("mintA"),
publicKey("mintB"),
publicKey("feeAccount"),
FEE_LAYOUT,
CURVE_NODE,
]
);

View File

@ -1,11 +1,11 @@
import { HashRouter, Route, Switch } from "react-router-dom"; import { HashRouter, Route, Switch } from 'react-router-dom';
import React from "react"; import React from 'react';
import { WalletProvider } from "./contexts/wallet"; import { WalletProvider } from 'common/src/contexts/wallet';
import { ConnectionProvider } from "./contexts/connection"; import { ConnectionProvider } from 'common/src/contexts/connection';
import { AccountsProvider } from "./contexts/accounts"; import { AccountsProvider } from 'common/src/contexts/accounts';
import { MarketProvider } from "./contexts/market"; import { MarketProvider } from './contexts/market';
import { LendingProvider } from "./contexts/lending"; import { LendingProvider } from './contexts/lending';
import { AppLayout } from "./components/Layout"; import { AppLayout } from './components/Layout';
import { import {
BorrowReserveView, BorrowReserveView,
@ -21,13 +21,13 @@ import {
LiquidateView, LiquidateView,
LiquidateReserveView, LiquidateReserveView,
MarginTrading, MarginTrading,
} from "./views"; } from './views';
import { NewPosition } from "./views/margin/newPosition"; import { NewPosition } from './views/margin/newPosition';
export function Routes() { export function Routes() {
return ( return (
<> <>
<HashRouter basename={"/"}> <HashRouter basename={'/'}>
<ConnectionProvider> <ConnectionProvider>
<WalletProvider> <WalletProvider>
<AccountsProvider> <AccountsProvider>
@ -35,53 +35,22 @@ export function Routes() {
<LendingProvider> <LendingProvider>
<AppLayout> <AppLayout>
<Switch> <Switch>
<Route exact path="/" component={() => <HomeView />} /> <Route exact path='/' component={() => <HomeView />} />
<Route <Route exact path='/dashboard' children={<DashboardView />} />
exact <Route path='/reserve/:id' children={<ReserveView />} />
path="/dashboard" <Route exact path='/deposit' component={() => <DepositView />} />
children={<DashboardView />} <Route path='/deposit/:id' children={<DepositReserveView />} />
/> <Route path='/withdraw/:id' children={<WithdrawView />} />
<Route path="/reserve/:id" children={<ReserveView />} /> <Route exact path='/borrow' children={<BorrowView />} />
<Route <Route path='/borrow/:id' children={<BorrowReserveView />} />
exact <Route path='/repay/loan/:obligation' children={<RepayReserveView />} />
path="/deposit" <Route path='/repay/:reserve' children={<RepayReserveView />} />
component={() => <DepositView />} <Route exact path='/liquidate' children={<LiquidateView />} />
/> <Route path='/liquidate/:id' children={<LiquidateReserveView />} />
<Route <Route exact path='/margin' children={<MarginTrading />} />
path="/deposit/:id"
children={<DepositReserveView />}
/>
<Route path="/withdraw/:id" children={<WithdrawView />} />
<Route exact path="/borrow" children={<BorrowView />} />
<Route
path="/borrow/:id"
children={<BorrowReserveView />}
/>
<Route
path="/repay/loan/:obligation"
children={<RepayReserveView />}
/>
<Route
path="/repay/:reserve"
children={<RepayReserveView />}
/>
<Route
exact
path="/liquidate"
children={<LiquidateView />}
/>
<Route
path="/liquidate/:id"
children={<LiquidateReserveView />}
/>
<Route
exact
path="/margin"
children={<MarginTrading />}
/>
<Route path="/margin/:id" children={<NewPosition />} /> <Route path='/margin/:id' children={<NewPosition />} />
<Route exact path="/faucet" children={<FaucetView />} /> <Route exact path='/faucet' children={<FaucetView />} />
</Switch> </Switch>
</AppLayout> </AppLayout>
</LendingProvider> </LendingProvider>

View File

@ -1,19 +1,11 @@
import { Connection, PublicKey } from "@solana/web3.js"; import { Connection, PublicKey } from '@solana/web3.js';
import { useEffect, useMemo, useState } from "react"; import { useEffect, useMemo, useState } from 'react';
import { MintLayout, AccountLayout } from "@solana/spl-token"; import { MintLayout, AccountLayout } from '@solana/spl-token';
import { programIds } from "./ids"; import { programIds } from 'common/src/utils/ids';
import { import { TokenSwapLayout, TokenSwapLayoutLegacyV0 as TokenSwapLayoutV0, TokenSwapLayoutV1 } from 'common/src/models';
PoolInfo, import { PoolInfo } from '../models';
TokenSwapLayout, import { useConnection } from 'common/src/contexts/connection';
TokenSwapLayoutLegacyV0 as TokenSwapLayoutV0, import { cache, getMultipleAccounts, TokenAccountParser } from 'common/src/contexts/accounts';
TokenSwapLayoutV1,
} from "./../models";
import { useConnection } from "../contexts/connection";
import {
cache,
getMultipleAccounts,
TokenAccountParser,
} from "../contexts/accounts";
export const LIQUIDITY_PROVIDER_FEE = 0.003; export const LIQUIDITY_PROVIDER_FEE = 0.003;
export const SERUM_FEE = 0.0005; export const SERUM_FEE = 0.0005;
@ -80,16 +72,10 @@ export const usePools = () => {
// TODO: this is not great // TODO: this is not great
// Ideally SwapLayout stores hash of all the mints to make finding of pool for a pair easier // Ideally SwapLayout stores hash of all the mints to make finding of pool for a pair easier
const holdings = await Promise.all( const holdings = await Promise.all(
getHoldings(connection, [ getHoldings(connection, [result.data.tokenAccountA, result.data.tokenAccountB])
result.data.tokenAccountA,
result.data.tokenAccountB,
])
); );
pool.pubkeys.holdingMints = [ pool.pubkeys.holdingMints = [holdings[0].info.mint, holdings[1].info.mint] as PublicKey[];
holdings[0].info.mint,
holdings[1].info.mint,
] as PublicKey[];
} catch (err) { } catch (err) {
console.log(err); console.log(err);
} }
@ -100,10 +86,7 @@ export const usePools = () => {
let pool = toPoolInfo(result, swapId); let pool = toPoolInfo(result, swapId);
pool.legacy = isLegacy; pool.legacy = isLegacy;
pool.pubkeys.feeAccount = result.data.feeAccount; pool.pubkeys.feeAccount = result.data.feeAccount;
pool.pubkeys.holdingMints = [ pool.pubkeys.holdingMints = [result.data.mintA, result.data.mintB] as PublicKey[];
result.data.mintA,
result.data.mintB,
] as PublicKey[];
poolsArray.push(pool as PoolInfo); poolsArray.push(pool as PoolInfo);
} }
@ -125,31 +108,28 @@ export const usePools = () => {
// This will pre-cache all accounts used by pools // This will pre-cache all accounts used by pools
// All those accounts are updated whenever there is a change // All those accounts are updated whenever there is a change
await getMultipleAccounts(connection, toQuery, "single").then( await getMultipleAccounts(connection, toQuery, 'single').then(({ keys, array }) => {
({ keys, array }) => { return array.map((obj, index) => {
return array.map((obj, index) => { const pubKey = keys[index];
const pubKey = keys[index]; if (obj.data.length === AccountLayout.span) {
if (obj.data.length === AccountLayout.span) { return cache.add(pubKey, obj, TokenAccountParser);
return cache.add(pubKey, obj, TokenAccountParser); } else if (obj.data.length === MintLayout.span) {
} else if (obj.data.length === MintLayout.span) { if (!cache.getMint(pubKey)) {
if (!cache.getMint(pubKey)) { return cache.addMint(new PublicKey(pubKey), obj);
return cache.addMint(new PublicKey(pubKey), obj);
}
} }
}
return obj; return obj;
}) as any[]; }) as any[];
} });
);
return poolsArray; return poolsArray;
}; };
Promise.all([ Promise.all([queryPools(programIds().swap), ...programIds().swap_legacy.map((leg) => queryPools(leg, true))]).then(
queryPools(programIds().swap), (all) => {
...programIds().swap_legacy.map((leg) => queryPools(leg, true)), setPools(all.flat());
]).then((all) => { }
setPools(all.flat()); );
});
}, [connection]); }, [connection]);
useEffect(() => { useEffect(() => {
@ -165,9 +145,7 @@ export const usePools = () => {
pubkey: new PublicKey(id), pubkey: new PublicKey(id),
}; };
const index = const index = pools && pools.findIndex((p) => p.pubkeys.account.toBase58() === id);
pools &&
pools.findIndex((p) => p.pubkeys.account.toBase58() === id);
if (index && index >= 0 && pools) { if (index && index >= 0 && pools) {
// TODO: check if account is empty? // TODO: check if account is empty?
@ -177,16 +155,13 @@ export const usePools = () => {
let pool = toPoolInfo(updated, programIds().swap); let pool = toPoolInfo(updated, programIds().swap);
pool.pubkeys.feeAccount = updated.data.feeAccount; pool.pubkeys.feeAccount = updated.data.feeAccount;
pool.pubkeys.holdingMints = [ pool.pubkeys.holdingMints = [updated.data.mintA, updated.data.mintB] as PublicKey[];
updated.data.mintA,
updated.data.mintB,
] as PublicKey[];
setPools([...pools, pool]); setPools([...pools, pool]);
} }
} }
}, },
"singleGossip" 'singleGossip'
); );
return () => { return () => {
@ -218,10 +193,7 @@ export const usePoolForBasket = (mints: (string | undefined)[]) => {
for (let i = 0; i < matchingPool.length; i++) { for (let i = 0; i < matchingPool.length; i++) {
const p = matchingPool[i]; const p = matchingPool[i];
const account = await cache.query( const account = await cache.query(connection, p.pubkeys.holdingAccounts[0]);
connection,
p.pubkeys.holdingAccounts[0]
);
if (!account.info.amount.eqn(0)) { if (!account.info.amount.eqn(0)) {
setPool(p); setPool(p);
@ -239,9 +211,7 @@ function estimateProceedsFromInput(
proceedsQuantityInPool: number, proceedsQuantityInPool: number,
inputAmount: number inputAmount: number
): number { ): number {
return ( return (proceedsQuantityInPool * inputAmount) / (inputQuantityInPool + inputAmount);
(proceedsQuantityInPool * inputAmount) / (inputQuantityInPool + inputAmount)
);
} }
function estimateInputFromProceeds( function estimateInputFromProceeds(
@ -250,13 +220,10 @@ function estimateInputFromProceeds(
proceedsAmount: number proceedsAmount: number
): number | string { ): number | string {
if (proceedsAmount >= proceedsQuantityInPool) { if (proceedsAmount >= proceedsQuantityInPool) {
return "Not possible"; return 'Not possible';
} }
return ( return (inputQuantityInPool * proceedsAmount) / (proceedsQuantityInPool - proceedsAmount);
(inputQuantityInPool * proceedsAmount) /
(proceedsQuantityInPool - proceedsAmount)
);
} }
export enum PoolOperation { export enum PoolOperation {
@ -273,20 +240,14 @@ export async function calculateDependentAmount(
op: PoolOperation op: PoolOperation
): Promise<number | string | undefined> { ): Promise<number | string | undefined> {
const poolMint = await cache.queryMint(connection, pool.pubkeys.mint); const poolMint = await cache.queryMint(connection, pool.pubkeys.mint);
const accountA = await cache.query( const accountA = await cache.query(connection, pool.pubkeys.holdingAccounts[0]);
connection,
pool.pubkeys.holdingAccounts[0]
);
const amountA = accountA.info.amount.toNumber(); const amountA = accountA.info.amount.toNumber();
const accountB = await cache.query( const accountB = await cache.query(connection, pool.pubkeys.holdingAccounts[1]);
connection,
pool.pubkeys.holdingAccounts[1]
);
let amountB = accountB.info.amount.toNumber(); let amountB = accountB.info.amount.toNumber();
if (!poolMint.mintAuthority) { if (!poolMint.mintAuthority) {
throw new Error("Mint doesnt have authority"); throw new Error('Mint doesnt have authority');
} }
if (poolMint.supply.eqn(0)) { if (poolMint.supply.eqn(0)) {
@ -308,14 +269,8 @@ export async function calculateDependentAmount(
} }
const isFirstIndependent = accountA.info.mint.toBase58() === independent; const isFirstIndependent = accountA.info.mint.toBase58() === independent;
const depPrecision = Math.pow( const depPrecision = Math.pow(10, isFirstIndependent ? mintB.decimals : mintA.decimals);
10, const indPrecision = Math.pow(10, isFirstIndependent ? mintA.decimals : mintB.decimals);
isFirstIndependent ? mintB.decimals : mintA.decimals
);
const indPrecision = Math.pow(
10,
isFirstIndependent ? mintA.decimals : mintB.decimals
);
const indAdjustedAmount = amount * indPrecision; const indAdjustedAmount = amount * indPrecision;
let indBasketQuantity = isFirstIndependent ? amountA : amountB; let indBasketQuantity = isFirstIndependent ? amountA : amountB;
@ -330,27 +285,18 @@ export async function calculateDependentAmount(
} else { } else {
switch (+op) { switch (+op) {
case PoolOperation.Add: case PoolOperation.Add:
depAdjustedAmount = depAdjustedAmount = (depBasketQuantity / indBasketQuantity) * indAdjustedAmount;
(depBasketQuantity / indBasketQuantity) * indAdjustedAmount;
break; break;
case PoolOperation.SwapGivenProceeds: case PoolOperation.SwapGivenProceeds:
depAdjustedAmount = estimateInputFromProceeds( depAdjustedAmount = estimateInputFromProceeds(depBasketQuantity, indBasketQuantity, indAdjustedAmount);
depBasketQuantity,
indBasketQuantity,
indAdjustedAmount
);
break; break;
case PoolOperation.SwapGivenInput: case PoolOperation.SwapGivenInput:
depAdjustedAmount = estimateProceedsFromInput( depAdjustedAmount = estimateProceedsFromInput(indBasketQuantity, depBasketQuantity, indAdjustedAmount);
indBasketQuantity,
depBasketQuantity,
indAdjustedAmount
);
break; break;
} }
} }
if (typeof depAdjustedAmount === "string") { if (typeof depAdjustedAmount === 'string') {
return depAdjustedAmount; return depAdjustedAmount;
} }
if (depAdjustedAmount === undefined) { if (depAdjustedAmount === undefined) {

View File

@ -1,246 +1,7 @@
import { useCallback, useState } from "react"; import { getTokenName, KnownTokenMap } from 'common/src/utils/utils';
import { MintInfo } from "@solana/spl-token"; import { PoolInfo } from '../models';
import { PoolInfo, TokenAccount } from "./../models"; export function getPoolName(map: KnownTokenMap, pool: PoolInfo, shorten = true) {
import { PublicKey } from "@solana/web3.js";
import BN from "bn.js";
import { WAD, ZERO } from "../constants";
export interface KnownToken {
tokenSymbol: string;
tokenName: string;
icon: string;
mintAddress: string;
}
export type KnownTokenMap = Map<string, KnownToken>;
export const formatPriceNumber = new Intl.NumberFormat("en-US", {
style: "decimal",
minimumFractionDigits: 2,
maximumFractionDigits: 8,
});
export function useLocalStorageState(key: string, defaultState?: string) {
const [state, setState] = useState(() => {
// NOTE: Not sure if this is ok
const storedState = localStorage.getItem(key);
if (storedState) {
return JSON.parse(storedState);
}
return defaultState;
});
const setLocalStorageState = useCallback(
(newState) => {
const changed = state !== newState;
if (!changed) {
return;
}
setState(newState);
if (newState === null) {
localStorage.removeItem(key);
} else {
localStorage.setItem(key, JSON.stringify(newState));
}
},
[state, key]
);
return [state, setLocalStorageState];
}
// shorten the checksummed version of the input address to have 4 characters at start and end
export function shortenAddress(address: string, chars = 4): string {
return `${address.slice(0, chars)}...${address.slice(-chars)}`;
}
export function getTokenName(
map: KnownTokenMap,
mint?: string | PublicKey,
shorten = true
): string {
const mintAddress = typeof mint === "string" ? mint : mint?.toBase58();
if (!mintAddress) {
return "N/A";
}
const knownSymbol = map.get(mintAddress)?.tokenSymbol;
if (knownSymbol) {
return knownSymbol;
}
return shorten ? `${mintAddress.substring(0, 5)}...` : mintAddress;
}
export function getTokenByName(tokenMap: KnownTokenMap, name: string) {
let token: KnownToken | null = null;
for (const val of tokenMap.values()) {
if (val.tokenSymbol === name) {
token = val;
break;
}
}
return token;
}
export function getTokenIcon(
map: KnownTokenMap,
mintAddress?: string | PublicKey
): string | undefined {
const address =
typeof mintAddress === "string" ? mintAddress : mintAddress?.toBase58();
if (!address) {
return;
}
return map.get(address)?.icon;
}
export function isKnownMint(map: KnownTokenMap, mintAddress: string) {
return !!map.get(mintAddress);
}
export const STABLE_COINS = new Set(["USDC", "wUSDC", "USDT"]);
export function chunks<T>(array: T[], size: number): T[][] {
return Array.apply<number, T[], T[][]>(
0,
new Array(Math.ceil(array.length / size))
).map((_, index) => array.slice(index * size, (index + 1) * size));
}
export function toLamports(
account?: TokenAccount | number,
mint?: MintInfo
): number {
if (!account) {
return 0;
}
const amount =
typeof account === "number" ? account : account.info.amount?.toNumber();
const precision = Math.pow(10, mint?.decimals || 0);
return Math.floor(amount * precision);
}
export function wadToLamports(amount?: BN): BN {
return amount?.div(WAD) || ZERO;
}
export function fromLamports(
account?: TokenAccount | number | BN,
mint?: MintInfo,
rate: number = 1.0
): number {
if (!account) {
return 0;
}
const amount = Math.floor(
typeof account === "number"
? account
: BN.isBN(account)
? account.toNumber()
: account.info.amount.toNumber()
);
const precision = Math.pow(10, mint?.decimals || 0);
return (amount / precision) * rate;
}
var SI_SYMBOL = ["", "k", "M", "G", "T", "P", "E"];
const abbreviateNumber = (number: number, precision: number) => {
let tier = (Math.log10(number) / 3) | 0;
let scaled = number;
let suffix = SI_SYMBOL[tier];
if (tier !== 0) {
let scale = Math.pow(10, tier * 3);
scaled = number / scale;
}
return scaled.toFixed(precision) + suffix;
};
export const formatAmount = (
val: number,
precision: number = 6,
abbr: boolean = true
) => (abbr ? abbreviateNumber(val, precision) : val.toFixed(precision));
export function formatTokenAmount(
account?: TokenAccount,
mint?: MintInfo,
rate: number = 1.0,
prefix = "",
suffix = "",
precision = 6,
abbr = false
): string {
if (!account) {
return "";
}
return `${[prefix]}${formatAmount(
fromLamports(account, mint, rate),
precision,
abbr
)}${suffix}`;
}
export const formatUSD = new Intl.NumberFormat("en-US", {
style: "currency",
currency: "USD",
});
const numberFormater = new Intl.NumberFormat("en-US", {
style: "decimal",
minimumFractionDigits: 2,
maximumFractionDigits: 2,
});
export const formatNumber = {
format: (val?: number) => {
if (!val) {
return "--";
}
return numberFormater.format(val);
},
};
export const formatPct = new Intl.NumberFormat("en-US", {
style: "percent",
minimumFractionDigits: 2,
maximumFractionDigits: 2,
});
export function convert(
account?: TokenAccount | number,
mint?: MintInfo,
rate: number = 1.0
): number {
if (!account) {
return 0;
}
const amount =
typeof account === "number" ? account : account.info.amount?.toNumber();
const precision = Math.pow(10, mint?.decimals || 0);
let result = (amount / precision) * rate;
return result;
}
export function getPoolName(
map: KnownTokenMap,
pool: PoolInfo,
shorten = true
) {
const sorted = pool.pubkeys.holdingMints.map((a) => a.toBase58()).sort(); const sorted = pool.pubkeys.holdingMints.map((a) => a.toBase58()).sort();
return sorted.map((item) => getTokenName(map, item, shorten)).join("/"); return sorted.map((item) => getTokenName(map, item, shorten)).join('/');
} }

View File

@ -1,18 +1,16 @@
import React from "react"; import React from 'react';
import { useTokenName, useBorrowingPower } from "../../hooks"; import { useTokenName } from 'common/src/hooks';
import { calculateBorrowAPY, LendingReserve } from "../../models/lending"; import { useBorrowingPower } from '../../hooks';
import { TokenIcon } from "../../components/TokenIcon"; import { calculateBorrowAPY, LendingReserve } from '../../models/lending';
import { formatNumber, formatPct } from "../../utils/utils"; import { TokenIcon } from '../../components/TokenIcon';
import { Button } from "antd"; import { formatNumber, formatPct } from 'common/src/utils/utils';
import { Link } from "react-router-dom"; import { Button } from 'antd';
import { PublicKey } from "@solana/web3.js"; import { Link } from 'react-router-dom';
import { LABELS } from "../../constants"; import { PublicKey } from '@solana/web3.js';
import { useMidPriceInUSD } from "../../contexts/market"; import { LABELS } from '../../constants';
import { useMidPriceInUSD } from '../../contexts/market';
export const BorrowItem = (props: { export const BorrowItem = (props: { reserve: LendingReserve; address: PublicKey }) => {
reserve: LendingReserve;
address: PublicKey;
}) => {
const name = useTokenName(props.reserve.liquidityMint); const name = useTokenName(props.reserve.liquidityMint);
const price = useMidPriceInUSD(props.reserve.liquidityMint.toBase58()).price; const price = useMidPriceInUSD(props.reserve.liquidityMint.toBase58()).price;
@ -22,8 +20,8 @@ export const BorrowItem = (props: {
return ( return (
<Link to={`/borrow/${props.address.toBase58()}`}> <Link to={`/borrow/${props.address.toBase58()}`}>
<div className="borrow-item"> <div className='borrow-item'>
<span style={{ display: "flex" }}> <span style={{ display: 'flex' }}>
<TokenIcon mintAddress={props.reserve.liquidityMint} /> <TokenIcon mintAddress={props.reserve.liquidityMint} />
{name} {name}
</span> </span>
@ -33,14 +31,12 @@ export const BorrowItem = (props: {
<div> <div>
<em>{formatNumber.format(borrowingPower)}</em> {name} <em>{formatNumber.format(borrowingPower)}</em> {name}
</div> </div>
<div className="dashboard-amount-quote"> <div className='dashboard-amount-quote'>${formatNumber.format(totalInQuote)}</div>
${formatNumber.format(totalInQuote)}
</div>
</div> </div>
</div> </div>
<div>{formatPct.format(apr)}</div> <div>{formatPct.format(apr)}</div>
<div> <div>
<Button type="primary"> <Button type='primary'>
<span>{LABELS.BORROW_ACTION}</span> <span>{LABELS.BORROW_ACTION}</span>
</Button> </Button>
</div> </div>

View File

@ -1,10 +1,10 @@
import { Card } from "antd"; import { Card } from 'antd';
import React from "react"; import React from 'react';
import { BarChartStatistic } from "../../../components/BarChartStatistic"; import { BarChartStatistic } from '../../../components/BarChartStatistic';
import { LABELS } from "../../../constants"; import { LABELS } from '../../../constants';
import { formatNumber } from "../../../utils/utils"; import { formatNumber } from 'common/src/utils/utils';
import { useUserDeposits } from "./../../../hooks"; import { useUserDeposits } from './../../../hooks';
import { DepositItem } from "./item"; import { DepositItem } from './item';
export const DashboardDeposits = () => { export const DashboardDeposits = () => {
const { userDeposits, totalInQuote } = useUserDeposits(); const { userDeposits, totalInQuote } = useUserDeposits();
@ -12,11 +12,10 @@ export const DashboardDeposits = () => {
return ( return (
<Card <Card
title={ title={
<div className="dashboard-title"> <div className='dashboard-title'>
<div>{LABELS.DASHBOARD_TITLE_DEPOSITS}</div> <div>{LABELS.DASHBOARD_TITLE_DEPOSITS}</div>
<div> <div>
<span>{LABELS.TOTAL_TITLE}: </span>$ <span>{LABELS.TOTAL_TITLE}: </span>${formatNumber.format(totalInQuote)}
{formatNumber.format(totalInQuote)}
</div> </div>
</div> </div>
} }
@ -26,17 +25,14 @@ export const DashboardDeposits = () => {
getPct={(item) => item.info.amountInQuote / totalInQuote} getPct={(item) => item.info.amountInQuote / totalInQuote}
name={(item) => item.info.name} name={(item) => item.info.name}
/> />
<div className="dashboard-item dashboard-header"> <div className='dashboard-item dashboard-header'>
<div>{LABELS.TABLE_TITLE_ASSET}</div> <div>{LABELS.TABLE_TITLE_ASSET}</div>
<div>{LABELS.TABLE_TITLE_DEPOSIT_BALANCE}</div> <div>{LABELS.TABLE_TITLE_DEPOSIT_BALANCE}</div>
<div>{LABELS.TABLE_TITLE_APY}</div> <div>{LABELS.TABLE_TITLE_APY}</div>
<div></div> <div></div>
</div> </div>
{userDeposits.map((deposit) => ( {userDeposits.map((deposit) => (
<DepositItem <DepositItem key={deposit.account.pubkey.toBase58()} userDeposit={deposit} />
key={deposit.account.pubkey.toBase58()}
userDeposit={deposit}
/>
))} ))}
</Card> </Card>
); );

View File

@ -1,24 +1,23 @@
import React, { useMemo } from "react"; import React, { useMemo } from 'react';
import { useTokenName, UserDeposit } from "../../../hooks"; import { useTokenName } from 'common/src/hooks';
import { calculateDepositAPY } from "../../../models/lending"; import { UserDeposit } from '../../../hooks';
import { TokenIcon } from "../../../components/TokenIcon"; import { calculateDepositAPY } from '../../../models/lending';
import { formatNumber, formatPct } from "../../../utils/utils"; import { TokenIcon } from '../../../components/TokenIcon';
import { Button } from "antd"; import { formatNumber, formatPct } from 'common/src/utils/utils';
import { Link } from "react-router-dom"; import { Button } from 'antd';
import { LABELS } from "../../../constants"; import { Link } from 'react-router-dom';
import { LABELS } from '../../../constants';
export const DepositItem = (props: { userDeposit: UserDeposit }) => { export const DepositItem = (props: { userDeposit: UserDeposit }) => {
const { reserve, info } = props.userDeposit; const { reserve, info } = props.userDeposit;
const mintAddress = reserve.info.liquidityMint; const mintAddress = reserve.info.liquidityMint;
const name = useTokenName(mintAddress); const name = useTokenName(mintAddress);
const depositAPY = useMemo(() => calculateDepositAPY(reserve.info), [ const depositAPY = useMemo(() => calculateDepositAPY(reserve.info), [reserve]);
reserve,
]);
return ( return (
<div className="dashboard-item"> <div className='dashboard-item'>
<span style={{ display: "flex" }}> <span style={{ display: 'flex' }}>
<TokenIcon mintAddress={mintAddress} /> <TokenIcon mintAddress={mintAddress} />
{name} {name}
</span> </span>
@ -27,20 +26,18 @@ export const DepositItem = (props: { userDeposit: UserDeposit }) => {
<div> <div>
<em>{formatNumber.format(info.amount)}</em> {name} <em>{formatNumber.format(info.amount)}</em> {name}
</div> </div>
<div className="dashboard-amount-quote"> <div className='dashboard-amount-quote'>${formatNumber.format(info.amountInQuote)}</div>
${formatNumber.format(info.amountInQuote)}
</div>
</div> </div>
</div> </div>
<div>{formatPct.format(depositAPY)}</div> <div>{formatPct.format(depositAPY)}</div>
<div style={{ display: "flex", justifyContent: "flex-end" }}> <div style={{ display: 'flex', justifyContent: 'flex-end' }}>
<Link to={`/deposit/${reserve.pubkey.toBase58()}`}> <Link to={`/deposit/${reserve.pubkey.toBase58()}`}>
<Button type="primary"> <Button type='primary'>
<span>{LABELS.DEPOSIT_ACTION}</span> <span>{LABELS.DEPOSIT_ACTION}</span>
</Button> </Button>
</Link> </Link>
<Link to={`/withdraw/${reserve.pubkey.toBase58()}`}> <Link to={`/withdraw/${reserve.pubkey.toBase58()}`}>
<Button type="text"> <Button type='text'>
<span>{LABELS.WITHDRAW_ACTION}</span> <span>{LABELS.WITHDRAW_ACTION}</span>
</Button> </Button>
</Link> </Link>

Some files were not shown because too many files have changed in this diff Show More