Skip to content

Commit

Permalink
feat: show contract balance to admin if less than 2 NEAR (#973)
Browse files Browse the repository at this point in the history
* feat: show contract balance to admin if less than 2 NEAR

* test: fix

* test: fix
  • Loading branch information
Tguntenaar authored Oct 27, 2024
1 parent 1611dd6 commit fe9ecec
Show file tree
Hide file tree
Showing 6 changed files with 204 additions and 74 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
const { accountId, dark } = props;

const [remainingBalance, setRemainingBalance] = useState("2");

function stringToNear(yoctoString) {
const paddedYoctoString = yoctoString.padStart(25, "0");
const integerPart = paddedYoctoString.slice(0, -24) || "0";
const fractionalPart = paddedYoctoString.slice(-24, -18);
return parseFloat(`${integerPart}.${fractionalPart}`);
}

asyncFetch("${REPL_RPC_URL}", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
jsonrpc: "2.0",
id: "dontcare",
method: "query",
params: {
request_type: "view_account",
finality: "final",
account_id: accountId,
},
}),
})
.then(({ body: data }) => {
const storageUsage = data.result?.storage_usage || 0;
const yoctoString = data.result?.amount || "0";
const usageCostNear = storageUsage * 0.00001;
const nearAmount = stringToNear(yoctoString);
const remainingStorageCostNear = nearAmount - usageCostNear;
setRemainingBalance(remainingStorageCostNear.toFixed(2));
})
.catch((error) => console.error(error));

return (
<div
data-testid="contract-balance-wrapper"
className={dark ? "text-white" : "text-black"}
>
{parseFloat(remainingBalance) < 2 ? (
<span data-testid="contract-balance">
Remaining Balance: {remainingBalance}
</span>
) : null}
</div>
);
41 changes: 24 additions & 17 deletions instances/devhub.near/widget/devhub/components/organism/Navbar.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ const [showMenu, setShowMenu] = useState(false);

const { href: linkHref } = VM.require("${REPL_DEVHUB}/widget/core.lib.url");

const { hasModerator } = VM.require(
"${REPL_DEVHUB}/widget/core.adapter.devhub-contract"
);
const { hasModerator } =
VM.require("${REPL_DEVHUB}/widget/core.adapter.devhub-contract") ||
(() => {});

linkHref || (linkHref = () => {});

Expand Down Expand Up @@ -167,21 +167,19 @@ let links = [
},
];

if (hasModerator) {
const isDevHubModerator = hasModerator({
account_id: context.accountId,
});
const isDevHubModerator = hasModerator({
account_id: context.accountId,
});

if (isDevHubModerator) {
links = [
{
title: "/admin",
href: "admin",
links: [],
},
...links,
];
}
if (isDevHubModerator) {
links = [
{
title: "/admin",
href: "admin",
links: [],
},
...links,
];
}

const MobileNav = styled.div`
Expand Down Expand Up @@ -232,6 +230,15 @@ return (
<Navbar className="position-relative">
<Logo />
<div className="d-flex gap-3 align-items-center">
{isDevHubModerator ? (
<Widget
src="${REPL_DEVHUB}/widget/devhub.components.island.contract-balance"
props={{
accountId: "${REPL_DEVHUB}",
dark: false,
}}
/>
) : null}
<LinksContainer>
{links.map((link) => (
<Widget
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ const [showMenu, setShowMenu] = useState(false);

const { href: linkHref } = VM.require("${REPL_DEVHUB}/widget/core.lib.url");

const { hasModerator } = VM.require(
"${REPL_EVENTS}/widget/core.adapter.devhub-contract"
);
const { hasModerator } =
VM.require("${REPL_EVENTS}/widget/core.adapter.devhub-contract") ||
(() => {});

linkHref || (linkHref = () => {});

Expand Down Expand Up @@ -139,21 +139,19 @@ let links = [
},
];

if (hasModerator) {
const isDevHubModerator = hasModerator({
account_id: context.accountId,
});
const isAdmin = hasModerator({
account_id: context.accountId,
});

if (isDevHubModerator) {
links = [
{
title: "Admin",
href: "admin",
links: [],
},
...links,
];
}
if (isAdmin) {
links = [
{
title: "Admin",
href: "admin",
links: [],
},
...links,
];
}

const MobileNav = styled.div`
Expand Down Expand Up @@ -205,6 +203,15 @@ return (
<div className="d-flex justify-content-between container-xl">
<Logo />
<div className="d-flex gap-3 align-items-center">
{isAdmin ? (
<Widget
src="${REPL_DEVHUB}/widget/devhub.components.island.contract-balance"
props={{
accountId: "events-committee.near",
dark: true,
}}
/>
) : null}
<LinksContainer>
{links.map((link) => (
<Widget
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,15 @@ return (
<Navbar className="position-relative">
<Logo />
<div className="d-flex gap-3 align-items-center">
{isModerator ? (
<Widget
src="${REPL_DEVHUB}/widget/devhub.components.island.contract-balance"
props={{
accountId: "${REPL_INFRASTRUCTURE_COMMITTEE}",
dark: false,
}}
/>
) : null}
<LinksContainer>
{links.map((link) => (
<Widget
Expand Down
107 changes: 67 additions & 40 deletions playwright-tests/tests/other/admin.spec.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,38 @@
import { test, expect } from "@playwright/test";
import { mockMainnetRpcRequest } from "../../util/rpcmock";

const adminPageRoute = "/devhub.near/widget/app?page=admin";
const adminButtonSelector = `button[data-testid="preview-homepage"]`;

async function navigateToAdminPage(page) {
await page.goto(adminPageRoute);
await page.waitForSelector(adminButtonSelector, { state: "visible" });
}

async function fillCommunityHandle(page, handle) {
const communityHandleInputSelector = "input[placeholder='Community handle']";
await page.locator(communityHandleInputSelector).nth(4).click();
await page.locator(communityHandleInputSelector).nth(4).fill(handle);
}

async function mockNearBalance(page, balanceLeft) {
const storageUsage = (55 - balanceLeft) * 100000; // 0.00001 NEAR per byte
await mockMainnetRpcRequest({
page,
filterParams: {
request_type: "view_account",
},
mockedResult: {
amount: "55000000000000000000000000", // balance on account 55 NEAR
block_hash: "2KiPPwUK6MakeWt9cRgHdVKfsJcHX8HkhL9tGYCwHDDA",
block_height: 131251870,
code_hash: "AUHmE71SPjnq8S3EW7m8ompfVBJxwCt4YfVvFLHtvVnr",
locked: "0",
storage_paid_at: 0,
storage_usage: storageUsage,
},
});
}

test.describe("Wallet is connected", () => {
// sign in to wallet
Expand All @@ -10,20 +44,11 @@ test.describe("Wallet is connected", () => {
page,
}) => {
test.setTimeout(60000);
await page.goto("/devhub.near/widget/app?page=admin");

const buttonSelector = `button[data-testid="preview-homepage"]`;
// Wait for the preview homepage to appear
await page.waitForSelector(buttonSelector, {
state: "visible",
});
await page.goto(adminPageRoute);

await fillCommunityHandle(page, "thomasguntenaar");

// Click on Community handle input
await page.getByPlaceholder("Community handle").nth(4).click();
await page
.getByPlaceholder("Community handle")
.nth(4)
.fill("thomasguntenaar");
await page.getByTestId("add-to-list").click();
await page.getByRole("button", { name: " Submit" }).click();
await page.getByText("Close").click();
Expand All @@ -40,12 +65,7 @@ test.describe("Wallet is connected", () => {
});

test("should be able to manage moderators", async ({ page }) => {
await page.goto("/devhub.near/widget/app?page=admin");
const buttonSelector = `button[data-testid="preview-homepage"]`;
// Wait for the first post button to be visible
await page.waitForSelector(buttonSelector, {
state: "visible",
});
await navigateToAdminPage(page);
await page.getByRole("tab", { name: "Moderators" }).click();
await page.getByTestId("edit-members").click();
const inputElement = page.locator(
Expand All @@ -62,12 +82,7 @@ test.describe("Wallet is connected", () => {
});

test("should be able to manage restricted labels", async ({ page }) => {
await page.goto("/devhub.near/widget/app?page=admin");
const buttonSelector = `button[data-testid="preview-homepage"]`;
// Wait for the first post button to be visible
await page.waitForSelector(buttonSelector, {
state: "visible",
});
await navigateToAdminPage(page);
await page.getByRole("tab", { name: "Restricted labels" }).click();
await page.getByTestId("create-team").click();
await page.getByRole("button", { name: "Cancel" }).click();
Expand Down Expand Up @@ -158,20 +173,11 @@ test.describe("Wallet is connected", () => {
await page.getByLabel("Close").click();
});

test("shouldn't be able to add a none existing community handle without a warning", async ({
test("shouldn't be able to add a non-existing community handle without a warning", async ({
page,
}) => {
await page.goto("/devhub.near/widget/app?page=admin");
const buttonSelector = `button[data-testid="preview-homepage"]`;
// Wait for the first post button to be visible
await page.waitForSelector(buttonSelector, {
state: "visible",
});
await page.getByPlaceholder("Community handle").nth(4).click();
await page
.getByPlaceholder("Community handle")
.nth(4)
.fill("arandomnonsensehandlethatwouldnotexist");
await navigateToAdminPage(page);
await fillCommunityHandle(page, "arandomnonsensehandlethatwouldnotexist");
await page.getByTestId("add-to-list").click();
await page.getByTestId("add-to-list").click();
await page
Expand All @@ -180,19 +186,40 @@ test.describe("Wallet is connected", () => {
)
.click();
});

test("should be able to see contract balance if the balance is lower than 2 NEAR", async ({
page,
}) => {
await navigateToAdminPage(page);
await mockNearBalance(page, 5);

const contractBalanceWrapper = await page.getByTestId(
"contract-balance-wrapper"
);
expect(contractBalanceWrapper).toBeDefined();
await page.waitForTimeout(1000);
const balance = await page.getByTestId("contract-balance");
expect(await balance.isVisible()).toBe(false);

// Under 2 NEAR
await mockNearBalance(page, 1.9);
await navigateToAdminPage(page);

expect(contractBalanceWrapper).toBeDefined();
await page.waitForTimeout(1000);
expect(await balance.isVisible()).toBe(true);
});
});

test.describe("Wallet is not connect", () => {
test.describe("Wallet is not connected", () => {
test.use({
storageState: "playwright-tests/storage-states/wallet-not-connected.json",
});
test("should show banner that the user doesn't have access", async ({
page,
}) => {
await page.goto("/devhub.near/widget/app?page=admin");
await page.goto(adminPageRoute);
const buttonSelector = "h2.alert.alert-danger";
// Wait for the first post history button to be visible

const banner = await page.waitForSelector(buttonSelector, {
state: "visible",
});
Expand Down
33 changes: 33 additions & 0 deletions playwright-tests/util/rpcmock.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,38 @@
export const MOCK_RPC_URL = "http://127.0.0.1:8080/api/proxy-rpc";

export async function mockMainnetRpcRequest({
page,
filterParams = {},
mockedResult = {},
}) {
await page.route("https://rpc.mainnet.near.org", async (route, request) => {
const postData = request.postDataJSON();

const filterParamsKeys = Object.keys(filterParams);
if (
filterParamsKeys.filter(
(param) => postData.params[param] === filterParams[param]
).length === filterParamsKeys.length
) {
const mockedResponse = {
jsonrpc: "2.0",
id: "dontcare",
// This is different than the other mockRpcRequest because
// the mainnet RPC returns the result directly, not wrapped in a result.result
result: mockedResult,
};

route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify(mockedResponse),
});
} else {
route.fallback();
}
});
}

export async function mockRpcRequest({
page,
filterParams = {},
Expand Down

0 comments on commit fe9ecec

Please sign in to comment.