mirror of https://github.com/certusone/oyster.git
feat: water wave chart
This commit is contained in:
parent
d02ea83f1d
commit
2bd278fd41
|
@ -16,7 +16,7 @@ import { PublicKey } from "@solana/web3.js";
|
|||
import "./style.less";
|
||||
import { LoadingOutlined } from "@ant-design/icons";
|
||||
import { ActionConfirmation } from "./../ActionConfirmation";
|
||||
import { marks } from "../../constants";
|
||||
import { LABELS, marks } from "../../constants";
|
||||
|
||||
const antIcon = <LoadingOutlined style={{ fontSize: 24 }} spin />;
|
||||
|
||||
|
@ -37,7 +37,6 @@ export const DepositInput = (props: {
|
|||
const { accounts: fromAccounts, balance, balanceLamports } = useUserBalance(
|
||||
reserve?.liquidityMint
|
||||
);
|
||||
// const collateralBalance = useUserBalance(reserve?.collateralMint);
|
||||
|
||||
const convert = useCallback(
|
||||
(val: string | number) => {
|
||||
|
@ -111,15 +110,13 @@ export const DepositInput = (props: {
|
|||
}}
|
||||
>
|
||||
<div className="deposit-input-title">
|
||||
How much would you like to deposit?
|
||||
{LABELS.DEPOSIT_QUESTION}
|
||||
</div>
|
||||
<div className="token-input">
|
||||
<TokenIcon mintAddress={reserve?.liquidityMint} />
|
||||
<NumericInput
|
||||
value={value}
|
||||
onChange={(val: any) => {
|
||||
setValue(val);
|
||||
}}
|
||||
onChange={setValue}
|
||||
autoFocus={true}
|
||||
style={{
|
||||
fontSize: 20,
|
||||
|
@ -139,7 +136,7 @@ export const DepositInput = (props: {
|
|||
onClick={onDeposit}
|
||||
disabled={fromAccounts.length === 0 || pendingTx}
|
||||
>
|
||||
Deposit
|
||||
{LABELS.DEPOSIT_ACTION}
|
||||
{pendingTx && (
|
||||
<Spin indicator={antIcon} className="action-spinner" />
|
||||
)}
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
import React, { useCallback, useMemo, useState } from "react";
|
||||
import { useAccountByMint, useTokenName, useUserBalance } from "../../hooks";
|
||||
import { InputType, useAccountByMint, useSliderInput, useTokenName, useUserBalance } from "../../hooks";
|
||||
import {
|
||||
LendingObligation,
|
||||
LendingReserve,
|
||||
LendingReserveParser,
|
||||
} from "../../models";
|
||||
import { TokenIcon } from "../TokenIcon";
|
||||
import { Button, Card, Spin } from "antd";
|
||||
import { Button, Card, Slider, Spin } from "antd";
|
||||
import { cache, ParsedAccount, useMint } from "../../contexts/accounts";
|
||||
import { NumericInput } from "../Input/numeric";
|
||||
import { useConnection } from "../../contexts/connection";
|
||||
|
@ -14,8 +14,7 @@ import { useWallet } from "../../contexts/wallet";
|
|||
import { repay } from "../../actions";
|
||||
import { CollateralSelector } from "./../CollateralSelector";
|
||||
import "./style.less";
|
||||
import { formatNumber, toLamports } from "../../utils/utils";
|
||||
import { LABELS } from "../../constants";
|
||||
import { LABELS, marks } from "../../constants";
|
||||
import { LoadingOutlined } from "@ant-design/icons";
|
||||
import { ActionConfirmation } from "./../ActionConfirmation";
|
||||
|
||||
|
@ -28,7 +27,6 @@ export const RepayInput = (props: {
|
|||
}) => {
|
||||
const connection = useConnection();
|
||||
const { wallet } = useWallet();
|
||||
const [value, setValue] = useState("");
|
||||
const [pendingTx, setPendingTx] = useState(false);
|
||||
const [showConfirmation, setShowConfirmation] = useState(false);
|
||||
|
||||
|
@ -47,7 +45,7 @@ export const RepayInput = (props: {
|
|||
}, [collateralReserveMint]);
|
||||
|
||||
const name = useTokenName(repayReserve?.info.liquidityMint);
|
||||
const { accounts: fromAccounts, balance } = useUserBalance(
|
||||
const { accounts: fromAccounts, balance, balanceLamports } = useUserBalance(
|
||||
repayReserve.info.liquidityMint
|
||||
);
|
||||
|
||||
|
@ -56,11 +54,19 @@ export const RepayInput = (props: {
|
|||
|
||||
const obligationAccount = useAccountByMint(obligation?.info.tokenMint);
|
||||
|
||||
const lamports = useMemo(
|
||||
() => toLamports(parseFloat(value), repayLiquidityMint),
|
||||
[value, repayLiquidityMint]
|
||||
const convert = useCallback(
|
||||
(val: string | number) => {
|
||||
if (typeof val === "string") {
|
||||
return (parseFloat(val) / balance) * 100;
|
||||
} else {
|
||||
return ((val * balance) / 100).toFixed(2);
|
||||
}
|
||||
},
|
||||
[balance]
|
||||
);
|
||||
|
||||
const { value, setValue, mark, setMark, type } = useSliderInput(convert);
|
||||
|
||||
const onRepay = useCallback(() => {
|
||||
if (
|
||||
!collateralReserve ||
|
||||
|
@ -77,7 +83,9 @@ export const RepayInput = (props: {
|
|||
try {
|
||||
await repay(
|
||||
fromAccounts[0],
|
||||
lamports,
|
||||
type === InputType.Slider
|
||||
? (mark * balanceLamports) / 100
|
||||
: Math.ceil(balanceLamports * (parseFloat(value) / balance)),
|
||||
obligation,
|
||||
obligationAccount,
|
||||
repayReserve,
|
||||
|
@ -95,7 +103,11 @@ export const RepayInput = (props: {
|
|||
}
|
||||
})();
|
||||
}, [
|
||||
lamports,
|
||||
mark,
|
||||
value,
|
||||
balance,
|
||||
balanceLamports,
|
||||
type,
|
||||
connection,
|
||||
wallet,
|
||||
obligation,
|
||||
|
@ -126,16 +138,13 @@ export const RepayInput = (props: {
|
|||
}}
|
||||
>
|
||||
<div className="repay-input-title">
|
||||
{LABELS.REPAY_QUESTION} (Currently: ){formatNumber.format(balance)}{" "}
|
||||
{name})
|
||||
{LABELS.REPAY_QUESTION}
|
||||
</div>
|
||||
<div className="token-input">
|
||||
<TokenIcon mintAddress={repayReserve?.info.liquidityMint} />
|
||||
<NumericInput
|
||||
value={value}
|
||||
onChange={(val: any) => {
|
||||
setValue(val);
|
||||
}}
|
||||
onChange={setValue}
|
||||
autoFocus={true}
|
||||
style={{
|
||||
fontSize: 20,
|
||||
|
@ -147,23 +156,11 @@ export const RepayInput = (props: {
|
|||
/>
|
||||
<div>{name}</div>
|
||||
</div>
|
||||
{/* TODO: finish slider implementation */}
|
||||
{/* <Slider
|
||||
marks={marks}
|
||||
value={mark}
|
||||
onChange={(val: number) =>
|
||||
setValue(
|
||||
(
|
||||
(fromLamports(
|
||||
wadToLamports(obligation?.info.borrowAmountWad).toNumber(),
|
||||
repayLiquidityMint
|
||||
) *
|
||||
val) /
|
||||
100
|
||||
).toFixed(2)
|
||||
)
|
||||
}
|
||||
/> */}
|
||||
<Slider
|
||||
marks={marks}
|
||||
value={mark}
|
||||
onChange={setMark}
|
||||
/>
|
||||
<div className="repay-input-title">{LABELS.SELECT_COLLATERAL}</div>
|
||||
<CollateralSelector
|
||||
reserve={repayReserve.info}
|
||||
|
|
|
@ -1,26 +1,13 @@
|
|||
import React, { useEffect, useMemo, useRef } from "react";
|
||||
import React, { useMemo } from "react";
|
||||
import { LendingReserve } from "../../models/lending";
|
||||
import echarts from "echarts";
|
||||
import {
|
||||
formatNumber,
|
||||
formatUSD,
|
||||
fromLamports,
|
||||
wadToLamports,
|
||||
} from "../../utils/utils";
|
||||
import { useMint } from "../../contexts/accounts";
|
||||
import { WaterWave } from "./../WaterWave";
|
||||
|
||||
export const ReserveUtilizationChart = (props: { reserve: LendingReserve }) => {
|
||||
const chartDiv = useRef<HTMLDivElement>(null);
|
||||
|
||||
// dispose chart
|
||||
useEffect(() => {
|
||||
const div = chartDiv.current;
|
||||
return () => {
|
||||
let instance = div && echarts.getInstanceByDom(div);
|
||||
instance && instance.dispose();
|
||||
};
|
||||
}, []);
|
||||
|
||||
const liquidityMint = useMint(props.reserve.liquidityMint);
|
||||
const availableLiquidity = fromLamports(
|
||||
props.reserve.availableLiquidity.toNumber(),
|
||||
|
@ -36,79 +23,7 @@ export const ReserveUtilizationChart = (props: { reserve: LendingReserve }) => {
|
|||
[props.reserve, liquidityMint]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!chartDiv.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
let instance = echarts.getInstanceByDom(chartDiv.current);
|
||||
if (!instance) {
|
||||
instance = echarts.init(chartDiv.current as any);
|
||||
}
|
||||
|
||||
const data = [
|
||||
{
|
||||
name: "Available Liquidity",
|
||||
value: availableLiquidity,
|
||||
tokens: availableLiquidity,
|
||||
},
|
||||
{
|
||||
name: "Total Borrowed",
|
||||
value: totalBorrows,
|
||||
tokens: totalBorrows,
|
||||
},
|
||||
];
|
||||
|
||||
instance.setOption({
|
||||
tooltip: {
|
||||
trigger: "item",
|
||||
formatter: function (params: any) {
|
||||
var val = formatUSD.format(params.value);
|
||||
var tokenAmount = formatNumber.format(params.data.tokens);
|
||||
return `${params.name}: \n${val}\n(${tokenAmount})`;
|
||||
},
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: "Liquidity",
|
||||
type: "pie",
|
||||
radius: ["50%", "70%"],
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
animation: false,
|
||||
label: {
|
||||
fontSize: 14,
|
||||
show: true,
|
||||
formatter: function (params: any) {
|
||||
var val = formatUSD.format(params.value);
|
||||
var tokenAmount = formatNumber.format(params.data.tokens);
|
||||
return `{c|${params.name}}\n{r|${tokenAmount}}\n{r|${val}}`;
|
||||
},
|
||||
rich: {
|
||||
c: {
|
||||
color: "#999",
|
||||
lineHeight: 22,
|
||||
align: "center",
|
||||
},
|
||||
r: {
|
||||
color: "#999",
|
||||
align: "right",
|
||||
},
|
||||
},
|
||||
color: "rgba(255, 255, 255, 0.5)",
|
||||
},
|
||||
itemStyle: {
|
||||
normal: {
|
||||
borderColor: "#000",
|
||||
},
|
||||
},
|
||||
data,
|
||||
},
|
||||
],
|
||||
});
|
||||
}, [totalBorrows, availableLiquidity]);
|
||||
|
||||
return <div ref={chartDiv} style={{ height: 300, width: 400 }} />;
|
||||
return <WaterWave
|
||||
style={{ height: 300 }}
|
||||
percent={availableLiquidity * 100 / (availableLiquidity + totalBorrows)} />;
|
||||
};
|
||||
|
|
|
@ -0,0 +1,29 @@
|
|||
@import '~antd/lib/style/themes/default.less';
|
||||
|
||||
.waterWave {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
transform-origin: left;
|
||||
.text {
|
||||
position: absolute;
|
||||
left: 5px;
|
||||
top: calc(50% - 15px);
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
span {
|
||||
color: @text-color-secondary;
|
||||
font-size: 14px;
|
||||
line-height: 22px;
|
||||
}
|
||||
h4 {
|
||||
color: @heading-color;
|
||||
line-height: 32px;
|
||||
font-size: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
.waterWaveCanvasWrapper {
|
||||
transform: scale(0.5);
|
||||
transform-origin: 0 0;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,184 @@
|
|||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import "./index.less";
|
||||
|
||||
export const WaterWave = (props: any) => {
|
||||
const node = useRef<HTMLCanvasElement>();
|
||||
const root = useRef<HTMLDivElement>();
|
||||
const [radio, setRadio] = useState(1);
|
||||
const { percent, title, style, color } = props;
|
||||
const { height } = style;
|
||||
|
||||
const resize = useCallback(() => {
|
||||
const { offsetWidth } = root.current?.parentNode as HTMLElement;
|
||||
setRadio(offsetWidth < height ? offsetWidth / height : 1);
|
||||
}, [height]);
|
||||
|
||||
// resize
|
||||
useEffect(() => {
|
||||
resize();
|
||||
|
||||
window.addEventListener('resize', resize);
|
||||
return () => {
|
||||
window.removeEventListener('resize', resize);
|
||||
};
|
||||
}, [resize]);
|
||||
|
||||
useEffect(() => {
|
||||
let timer = 0;
|
||||
renderChart(
|
||||
node.current,
|
||||
percent,
|
||||
(val) => {
|
||||
timer = val;
|
||||
},
|
||||
color);
|
||||
|
||||
return () => {
|
||||
cancelAnimationFrame(timer);
|
||||
}
|
||||
}, [percent, color]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className="waterWave"
|
||||
ref={root as any}
|
||||
style={{ transform: `scale(${radio})` }}
|
||||
>
|
||||
<div style={{ width: height, height, overflow: 'hidden' }}>
|
||||
<canvas
|
||||
className="waterWaveCanvasWrapper"
|
||||
ref={node as any}
|
||||
width={height * 2}
|
||||
height={height * 2}
|
||||
/>
|
||||
</div>
|
||||
<div className="text" style={{ width: height }}>
|
||||
{title && <span>{title}</span>}
|
||||
<h4>{percent.toFixed(2)}%</h4>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
const renderChart = (canvas: HTMLCanvasElement | undefined, percent: number, setTimer: (timer: number) => void, color = '#1890FF',) => {
|
||||
const data = percent / 100;
|
||||
if (!canvas || !data) {
|
||||
return;
|
||||
}
|
||||
|
||||
const ctx = canvas.getContext('2d');
|
||||
|
||||
if (!ctx) {
|
||||
return;
|
||||
}
|
||||
|
||||
const canvasWidth = canvas.width;
|
||||
const canvasHeight = canvas.height;
|
||||
const radius = canvasWidth / 2;
|
||||
const lineWidth = 2;
|
||||
const cR = radius - lineWidth;
|
||||
|
||||
ctx.beginPath();
|
||||
ctx.lineWidth = lineWidth * 2;
|
||||
|
||||
const axisLength = canvasWidth - lineWidth;
|
||||
const unit = axisLength / 8;
|
||||
const range = 0.2;
|
||||
let currRange = range;
|
||||
const xOffset = lineWidth;
|
||||
let sp = 0;
|
||||
let currData = 0;
|
||||
const waveupsp = 0.005;
|
||||
|
||||
const bR = radius - lineWidth;
|
||||
const circleOffset = -(Math.PI / 2);
|
||||
let circleLock = true;
|
||||
|
||||
const cStartPoint = [radius + bR * Math.cos(circleOffset), radius + bR * Math.sin(circleOffset)];
|
||||
ctx.strokeStyle = color;
|
||||
ctx.moveTo(cStartPoint[0], cStartPoint[1]);
|
||||
|
||||
const drawSin = () => {
|
||||
ctx.beginPath();
|
||||
ctx.save();
|
||||
|
||||
const sinStack = [];
|
||||
for (let i = xOffset; i <= xOffset + axisLength; i += 20 / axisLength) {
|
||||
const x = sp + (xOffset + i) / unit;
|
||||
const y = Math.sin(x) * currRange;
|
||||
const dx = i;
|
||||
const dy = 2 * cR * (1 - currData) + (radius - cR) - unit * y;
|
||||
|
||||
ctx.lineTo(dx, dy);
|
||||
sinStack.push([dx, dy]);
|
||||
}
|
||||
|
||||
const startPoint = sinStack.shift();
|
||||
|
||||
if (!startPoint) {
|
||||
return;
|
||||
}
|
||||
|
||||
ctx.lineTo(xOffset + axisLength, canvasHeight);
|
||||
ctx.lineTo(xOffset, canvasHeight);
|
||||
ctx.lineTo(startPoint[0], startPoint[1]);
|
||||
|
||||
const gradient = ctx.createLinearGradient(0, 0, 0, canvasHeight);
|
||||
gradient.addColorStop(0, '#ffffff');
|
||||
gradient.addColorStop(1, '#1890FF');
|
||||
ctx.fillStyle = gradient;
|
||||
ctx.fill();
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
const render = () => {
|
||||
ctx.clearRect(0, 0, canvasWidth, canvasHeight);
|
||||
if (circleLock) {
|
||||
circleLock = false;
|
||||
|
||||
ctx.globalCompositeOperation = 'destination-over';
|
||||
|
||||
ctx.beginPath();
|
||||
ctx.save();
|
||||
ctx.arc(radius, radius, radius - 3 * lineWidth, 0, 2 * Math.PI, true);
|
||||
ctx.stroke();
|
||||
ctx.restore();
|
||||
ctx.clip();
|
||||
ctx.fillStyle = '#1890FF';
|
||||
} else {
|
||||
if (data >= 0.85) {
|
||||
if (currRange > range / 4) {
|
||||
const t = range * 0.01;
|
||||
currRange -= t;
|
||||
}
|
||||
} else if (data <= 0.1) {
|
||||
if (currRange < range * 1.5) {
|
||||
const t = range * 0.01;
|
||||
currRange += t;
|
||||
}
|
||||
} else {
|
||||
if (currRange <= range) {
|
||||
const t = range * 0.01;
|
||||
currRange += t;
|
||||
}
|
||||
if (currRange >= range) {
|
||||
const t = range * 0.01;
|
||||
currRange -= t;
|
||||
}
|
||||
}
|
||||
if (data - currData > 0) {
|
||||
currData += waveupsp;
|
||||
}
|
||||
if (data - currData < 0) {
|
||||
currData -= waveupsp;
|
||||
}
|
||||
|
||||
sp += 0.07;
|
||||
drawSin();
|
||||
}
|
||||
setTimer(requestAnimationFrame(render));
|
||||
}
|
||||
|
||||
render();
|
||||
}
|
|
@ -37,6 +37,7 @@ export const LABELS = {
|
|||
TABLE_TITLE_MAX_BORROW: "Available for you",
|
||||
DASHBOARD_TITLE_LOANS: "Loans",
|
||||
DASHBOARD_TITLE_DEPOSITS: "Deposits",
|
||||
DEPOSIT_QUESTION: "How much would you like to deposit?",
|
||||
WITHDRAW_ACTION: "Withdraw",
|
||||
WITHDRAW_QUESTION: "How much would you like to withdraw?",
|
||||
DASHBOARD_ACTION: "Go to dashboard",
|
||||
|
|
Loading…
Reference in New Issue