From fe9ececc862883fbd13ebdb88330dd64d431d229 Mon Sep 17 00:00:00 2001 From: Thomas Date: Sun, 27 Oct 2024 00:17:08 -0500 Subject: [PATCH] feat: show contract balance to admin if less than 2 NEAR (#973) * feat: show contract balance to admin if less than 2 NEAR * test: fix * test: fix --- .../components/island/contract-balance.jsx | 47 ++++++++ .../devhub/components/organism/Navbar.jsx | 41 ++++--- .../devhub/components/organism/Navbar.jsx | 41 ++++--- .../widget/components/organism/Navbar.jsx | 9 ++ playwright-tests/tests/other/admin.spec.js | 107 +++++++++++------- playwright-tests/util/rpcmock.js | 33 ++++++ 6 files changed, 204 insertions(+), 74 deletions(-) create mode 100644 instances/devhub.near/widget/devhub/components/island/contract-balance.jsx diff --git a/instances/devhub.near/widget/devhub/components/island/contract-balance.jsx b/instances/devhub.near/widget/devhub/components/island/contract-balance.jsx new file mode 100644 index 000000000..e5f6337fc --- /dev/null +++ b/instances/devhub.near/widget/devhub/components/island/contract-balance.jsx @@ -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 ( +
+ {parseFloat(remainingBalance) < 2 ? ( + + Remaining Balance: {remainingBalance} + + ) : null} +
+); diff --git a/instances/devhub.near/widget/devhub/components/organism/Navbar.jsx b/instances/devhub.near/widget/devhub/components/organism/Navbar.jsx index 087c4a46c..a5ed91d5a 100644 --- a/instances/devhub.near/widget/devhub/components/organism/Navbar.jsx +++ b/instances/devhub.near/widget/devhub/components/organism/Navbar.jsx @@ -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 = () => {}); @@ -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` @@ -232,6 +230,15 @@ return (
+ {isDevHubModerator ? ( + + ) : null} {links.map((link) => ( {}); linkHref || (linkHref = () => {}); @@ -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` @@ -205,6 +203,15 @@ return (
+ {isAdmin ? ( + + ) : null} {links.map((link) => (
+ {isModerator ? ( + + ) : null} {links.map((link) => ( { // sign in to wallet @@ -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(); @@ -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( @@ -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(); @@ -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 @@ -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", }); diff --git a/playwright-tests/util/rpcmock.js b/playwright-tests/util/rpcmock.js index 5a84cdb37..03e5724f5 100644 --- a/playwright-tests/util/rpcmock.js +++ b/playwright-tests/util/rpcmock.js @@ -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 = {},