Skip to content
This repository has been archived by the owner on Sep 26, 2024. It is now read-only.

feat(frontend): display detailed balance information on AccountDetails page #447

Merged
merged 19 commits into from
Oct 20, 2020
Merged
Show file tree
Hide file tree
Changes from 17 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,9 @@
"homepage": "https://github.com/near/near-explorer#readme",
"dependencies": {
"autobahn": "19.4.1",
"bn.js": "^5.1.1",
"geoip-lite": "^1.4.2",
"js-sha256": "^0.9.0",
"moment": "^2.24.0",
"near-api-js": "^0.27.0",
"pg": "^8.3.0",
Expand Down
3 changes: 3 additions & 0 deletions backend/src/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,3 +47,6 @@ exports.wampNearExplorerBackendSecret =
process.env.WAMP_NEAR_EXPLORER_BACKEND_SECRET || "back";

exports.genesisRecordsUrl = process.env.NEAR_GENESIS_RECORDS_URL;

exports.nearLockupAccountIdSuffix =
process.env.NEAR_LOCKUP_ACCOUNT_SUFFIX || "lockup.near";
12 changes: 9 additions & 3 deletions backend/src/near.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,14 @@
const nearlib = require("near-api-js");
const nearApi = require("near-api-js");

const { nearRpcUrl, wampNearNetworkName } = require("./config");

const nearRpc = new nearlib.providers.JsonRpcProvider(nearRpcUrl);
const nearRpc = new nearApi.providers.JsonRpcProvider(nearRpcUrl);

// TODO: Provide an equivalent method in near-api-js, so we don't need to hack it around.
nearRpc.callViewMethod = async function (contractName, methodName, args) {
const account = new nearApi.Account({ provider: this });
return await account.viewFunction(contractName, methodName, args);
};

const queryFinalTimestamp = async () => {
const finalBlock = await nearRpc.sendJsonRpc("block", { finality: "final" });
Expand Down Expand Up @@ -34,7 +40,7 @@ const getCurrentNodes = (nodes) => {
const {
newValidators,
removedValidators,
} = nearlib.validators.diffEpochValidators(currentValidators, nextValidators);
} = nearApi.validators.diffEpochValidators(currentValidators, nextValidators);
signNewValidators(newValidators);
signRemovedValidators(removedValidators);
currentValidators = currentValidators.concat(newValidators);
Expand Down
57 changes: 57 additions & 0 deletions backend/src/wamp.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
const autobahn = require("autobahn");
const BN = require("bn.js");
const geoip = require("geoip-lite");
const { sha256 } = require("js-sha256");

const models = require("../models");

const {
wampNearNetworkName,
wampNearExplorerUrl,
wampNearExplorerBackendSecret,
nearLockupAccountIdSuffix,
} = require("./config");

const { nearRpc } = require("./near");

const wampHandlers = {};
Expand Down Expand Up @@ -98,6 +102,59 @@ wampHandlers["nearcore-validators"] = async () => {
return await nearRpc.sendJsonRpc("validators", [null]);
};

wampHandlers["get-account-details"] = async ([accountId]) => {
function generateLockupAccountIdFromAccountId(accountId) {
// copied from https://github.com/near/near-wallet/blob/f52a3b1a72b901d87ab2c9cee79770d697be2bd9/src/utils/wallet.js#L601
return (
sha256(Buffer.from(accountId)).substring(0, 40) +
"." +
nearLockupAccountIdSuffix
);
}

const lockupAccountId = generateLockupAccountIdFromAccountId(accountId);

const [accountInfo, lockupTotalBalance, genesisConfig] = await Promise.all([
nearRpc.sendJsonRpc("query", {
request_type: "view_account",
finality: "final",
account_id: accountId,
}),
nearRpc
.callViewMethod(lockupAccountId, "get_balance", {})
.then((balance) => new BN(balance)),
nearRpc.sendJsonRpc("EXPERIMENTAL_genesis_config", {}),
]);

const storageUsage = new BN(accountInfo.storage_usage);
const storageAmountPerByte = new BN(
genesisConfig.runtime_config.storage_amount_per_byte
);
const stakedBalance = new BN(accountInfo.locked);
const nonStakedBalance = new BN(accountInfo.amount);
const minimumBalance = storageAmountPerByte.mul(storageUsage);
const availableBalance = nonStakedBalance.sub(
BN.max(stakedBalance, minimumBalance)
);
let totalBalance = stakedBalance.add(nonStakedBalance);
if (lockupTotalBalance) {
totalBalance = totalBalance.add(lockupTotalBalance);
}

return {
storageUsage: storageUsage.toString(),
stakedBalance: stakedBalance.toString(),
nonStakedBalance: nonStakedBalance.toString(),
minimumBalance: minimumBalance.toString(),
availableBalance: availableBalance.toString(),
totalBalance: totalBalance.toString(),
lockupTotalBalance: lockupTotalBalance
? lockupTotalBalance.toString()
: undefined,
lockupAccountId: lockupTotalBalance ? lockupAccountId : undefined,
};
};

function setupWamp() {
const wamp = new autobahn.Connection({
realm: "near-explorer",
Expand Down
6 changes: 3 additions & 3 deletions frontend/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@
"hexy": "^0.3.0",
"lodash": "^4.17.15",
"moment": "^2.26.0",
"near-api-js": "^0.27.0",
"near-api-js": "^0.30.0",
"next": "^9.4.1",
"react": "^16.13.1",
"react-bootstrap": "^1.0.1",
Expand Down
134 changes: 119 additions & 15 deletions frontend/src/components/accounts/AccountDetails.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import Balance from "../utils/Balance";
import CardCell from "../utils/CardCell";
import TransactionLink from "../utils/TransactionLink";
import Term from "../utils/Term";
import AccountLink from "../utils/AccountLink";

export interface Props {
account: A.Account;
Expand All @@ -18,36 +19,55 @@ export interface Props {
export default class extends React.Component<Props> {
render() {
const { account } = this.props;
console.log(account);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please remove the log

return (
<div className="account-info-container">
<Row noGutters>
<Col md="3">
<CardCell
title={
<Term title={"Ⓝ Balance"}>
{"NEAR token that is spendable all the time. "}
<a href={"https://docs.near.org/docs/concepts/account"}>
docs
</a>
<Term title={"Ⓝ Available Balance"}>
{
"This is your spendable NEAR balance, and can be used or transferred immediately. This will be lower than your Total Balance."
}
</Term>
}
text={<Balance amount={account.amount} />}
text={<Balance amount={account.availableBalance} />}
className="border-0"
/>
</Col>
<Col md="3">
<CardCell
title={
<Term title={"Ⓝ Locked"}>
<Term title={"Minimum Balance"}>
{
"NEAR token balance that is currently staked, and thus not immediately spendable. "
"This is the minimum NEAR balance your account must maintain to remain active. This balance represents the storage space your account is using on the NEAR blockchain, and will go up or down as you use more or less space. "
}
<a href={"https://docs.near.org/docs/concepts/account"}>
<a
href={
"https://docs.near.org/docs/roles/integrator/faq#is-there-a-minimum-account-balance"
}
>
docs
</a>
</Term>
}
text={<Balance amount={account.locked} />}
imgLink="/static/images/icon-storage.svg"
text={<Balance amount={account.minimumBalance} />}
/>
</Col>
<Col md="3">
<CardCell
title={
<Term title={"Storage Used"}>
{"Total blockchain storage (in bytes) used by this account. "}
<a href={"https://docs.near.org/docs/concepts/storage"}>
docs
</a>
</Term>
}
imgLink="/static/images/icon-storage.svg"
text={`${account.storageUsage.toLocaleString()} B`}
/>
</Col>
<Col md="3">
Expand All @@ -74,18 +94,102 @@ export default class extends React.Component<Props> {
}
/>
</Col>
<Col md="3">
</Row>
<Row>
<Col md="4">
<CardCell
title={
<Term title={"Storage Used"}>
{"Total blockchain storage (in bytes) used by this account. "}
<Term title={"Ⓝ Total Balance"}>
{
"Your total balance represents all NEAR tokens under your control. In many cases, you will not have immediate access to this entire balance (e.g. if it is locked, delegated, or staked). Check your Available Balance for the NEAR you can actively use, transfer, delegate, and stake."
}
</Term>
}
imgLink="/static/images/icon-storage.svg"
text={`${account.storageUsage.toLocaleString()} B`}
text={<Balance amount={account.totalBalance} />}
className="border-0"
/>
</Col>
<Col md="4">
<CardCell
title={
<Term title={"Ⓝ Staked Balance"}>
{
"This NEAR is actively being used to back a validator and secure the network. When you decide to unstake this NEAR, it will take some time to be shown in your Available Balance, as NEAR takes 3 epochs (~36 hours) to unstake. "
}
<a
href={
"https://docs.near.org/docs/validator/economics#1-near-tokens-to-stake"
}
>
docs
</a>
</Term>
}
text={<Balance amount={account.stakedBalance} />}
/>
</Col>
<Col md="4">
<CardCell
title={
<Term title={"Ⓝ Non Staked Balance"}>
{
"The amount of NEAR Token that is stored as amount in your account."
}
</Term>
}
text={<Balance amount={account.nonStakedBalance} />}
/>
</Col>
</Row>
{account.lockupTotalBalance && (
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems that we will miss the total balance information if there is no information about the lockup balance. This should not be the case

<Row noGutters>
<Col md="6">
<CardCell
title={
<Term title={"Ⓝ Total Lockup Balance"}>
{
"This NEAR is locked in a lockup contract, and cannot be withdrawn. You may still delegate or stake this NEAR. Once the NEAR is unlocked, you can view it in your Unlocked Balance, and chose to withdraw it (moving to your Available Balance). "
}
<a
href={
"https://docs.near.org/docs/tokens/lockup#lockup-basics"
}
>
docs
</a>
</Term>
}
text={<Balance amount={account.lockupTotalBalance} />}
/>
</Col>
<Col md="6">
<CardCell
title={
<Term title={"Lockup Account"}>
{
"Lockup is a special smart contract that ensures that the full amount or even a partial amount is not transferable until it is supposed to be. "
}
<a
href={
"https://docs.near.org/docs/tokens/lockup#the-lockup-contract"
}
>
docs
</a>
</Term>
}
imgLink="/static/images/icon-m-transaction.svg"
text={
account.lockupAccountId ? (
<AccountLink accountId={account.lockupAccountId} />
) : (
""
)
}
/>
</Col>
</Row>
)}
<Row noGutters className="border-0">
<Col md="4">
<CardCell
Expand Down
4 changes: 2 additions & 2 deletions frontend/src/components/accounts/Accounts.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,8 @@ class Accounts extends React.Component<InnerProps> {
{items &&
items.map((account) => (
<AccountRow
key={account.id}
accountId={account.id}
key={account.accountId}
accountId={account.accountId}
createdAt={account.createdAtBlockTimestamp}
/>
))}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,22 +3,50 @@ import renderer from "react-test-renderer";
import AccountDetails from "../AccountDetails";

describe("<AccountDetails />", () => {
it("renders", () => {
it("renders with lockup account", () => {
expect(
renderer.create(
<AccountDetails
account={{
id: "account",
amount: "1",
locked: "1",
storageUsage: 0,
storagePaidAt: 1,
inTransactionsCount: 0,
outTransactionsCount: 0,
accountId: "megan.near",
inTransactionsCount: 10,
outTransactionsCount: 7,
createdAtBlockTimestamp: Number(new Date(2019, 1, 1)),
createdByTransactionHash:
"EVvWW1S9BFaEjY1JBNSdstb7ZTtTFjQ6cygkbw1KY4tL",
accountIndex: 1234567890,
stakedBalance: "0",
nonStakedBalance: "654345665432345678765",
minimumBalance: "87600000000000",
availableBalance: "43454321345678765432345678",
totalBalance: "9876545678765432134567876543",
storageUsage: "876",
lockupTotalBalance: "765654565324543",
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's add a test for an account without lockup account information

lockupAccountId: "gjturigjgkjnfidsjffjsa.lockup.near",
}}
/>
)
).toMatchSnapshot();
});

it("renders without lockup account", () => {
expect(
renderer.create(
<AccountDetails
account={{
accountId: "near.near",
inTransactionsCount: 10,
outTransactionsCount: 7,
createdAtBlockTimestamp: Number(new Date(2019, 1, 1)),
createdByTransactionHash:
"EVvWW1S9BFlkjkmnjmkb7ZTtTFjQ6cygkbw1KY4tL",
accountIndex: 1234567890,
stakedBalance: "0",
nonStakedBalance: "6987876845678765",
minimumBalance: "187600000000000",
availableBalance: "434345678765432345678",
totalBalance: "98765445654565134567876543",
storageUsage: "1876",
}}
/>
)
Expand Down
Loading