Skip to content

Commit

Permalink
Stake delegation (#60)
Browse files Browse the repository at this point in the history
This adds the stake delegation functionality to the treasury dashboard.

The recorded video from the automated playwright test shows the
"happy-path" scenario of staking max available NEAR tokens.


https://github.com/user-attachments/assets/35866bc1-315e-40ba-94ba-1528866418f9

---------

Co-authored-by: Peter Salomonsen <[email protected]>
  • Loading branch information
Megha-Dev-19 and petersalomonsen authored Oct 15, 2024
1 parent 27d7cdd commit e221939
Show file tree
Hide file tree
Showing 32 changed files with 2,581 additions and 138 deletions.
1 change: 0 additions & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,6 @@ jobs:
- name: Install dependencies
run: |
npm ci
curl --proto '=https' --tlsv1.2 -LsSf https://github.com/mpeterdev/bos-loader/releases/download/v0.7.1/bos-loader-v0.7.1-installer.sh | sh
npx playwright install-deps
npx playwright install
- name: Run tests
Expand Down
22 changes: 22 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,30 @@ NEAR DevHub trustees dashboard BOS components

This is the repository of the BOS components for the NEAR DevHub trustees dashboard.

# Contributing

Please refer to [NEAR DevHub](https://github.com/NEAR-DevHub/neardevhub-bos/blob/main/CONTRIBUTING.md) for general contribution guidelines. Details specific for this repository will be added later.

## Creating test videos

Creating videos of your automated tests is a great way of showcasing the changes in your contribution. Video recording of your test can be enabled in the [playwright.config.js](./playwright.config.js) under the `use` section where there is the `video` property that you should set to `on` ( Remember to NOT commit it with the `on` setting, as we don't want to waste github action resources on recording videos).

It might also be smart to add a pause in the end of your test, just to ensure that the last frame is also showing, by adding the following line:

```javascript
await page.waitForTimeout(500);
```

After running the test with video recording on, you should convert it to mp4. This can be easitly done using a command like this:

```bash
ffmpeg -i test-results/stake-delegation-stake-delegation-admin-connected-Should-create-stake-delegation-request-treasury-testing/video.webm stakedelegation.mp4
```

(replace with the paths of the video of the actual test you want to show).

The resulting mp4 file can be directly dragged into the Pull Request description.

# Web4 gateway

In the [web4](./web4) folder there is a setup for a web4 gateway. The [public_html](./web4/public_html/) contains the gateway static index.html file that is served on the web4 page, and there is also the [treasury-web4](./web4/treasury-web4/) that contains the web4 contract that is written in Rust.
Expand Down
4 changes: 3 additions & 1 deletion instances/treasury-devdao.near/aliases.mainnet.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,7 @@
"REPL_PROPOSAL_FEED_INDEXER_QUERY_NAME": "polyprogrammist_near_devhub_prod_v1_proposals_with_latest_snapshot",
"REPL_X_HASURA_ROLE": "polyprogrammist_near",
"REPL_NEAR_TOKEN_ICON": "https://ipfs.near.social/ipfs/bafkreifnyxk6cssapw7j5vc6zuzwl7vt6o5ddspoo5lcmbvtdrcmfozqyu",
"REPL_PIKESPEAK_KEY": "36f2b87a-7ee6-40d8-80b9-5e68e587a5b5"
"REPL_PIKESPEAK_KEY": "36f2b87a-7ee6-40d8-80b9-5e68e587a5b5",
"REPL_STAKE_ICON": "https://ipfs.near.social/ipfs/bafkreicjylnnugawkpvhtwjqegc2y6rhdmaqrzdo3cixr2epj5wlxz6ts4",
"REPL_UNSTAKE_ICON": "https://ipfs.near.social/ipfs/bafkreiepl6mvtyvd4hxiy3s4jpij4ep6e5fea324bvbujitmjup63iexea"
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
const status = props.status;
const isPaymentsPage = props.isPaymentsPage;
const Container = styled.div`
text-align: center;
.reject {
Expand All @@ -25,7 +26,9 @@ const Container = styled.div`
return (
<Container>
{status === "Approved" ? (
<div className="approve rounded-2 p-2 bold">Funded</div>
<div className="approve rounded-2 p-2 bold">
{isPaymentsPage ? "Funded" : "Executed"}
</div>
) : status === "Rejected" ? (
<div className="reject rounded-2 p-2 bold">Rejected</div>
) : status === "Failed" ? (
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
const instance = props.instance;
if (!instance) {
return <></>;
}

const { treasuryDaoID } = VM.require(`${instance}/widget/config.data`);

const setNearStakedTokens = props.setNearStakedTokens || (() => {});
const setPoolWithBalance = props.setPoolWithBalance || (() => {});

const code = `
<!doctype html>
<html>
<body>
<script>
const archiveNodeUrl = "https://archival-rpc.mainnet.near.org";
const treasuryDaoID = "${treasuryDaoID}"
async function getAccountBalance(stakingpool_id, account_id) {
return await fetch(archiveNodeUrl, {
method: "POST",
headers: {
"content-type": "application/json",
},
body: JSON.stringify({
jsonrpc: "2.0",
id: "dontcare",
method: "query",
params: {
request_type: "call_function",
finality: 'final',
account_id: stakingpool_id,
method_name: "get_account_total_balance",
args_base64: btoa(
JSON.stringify({
account_id: account_id,
})
),
},
}),
})
.then((r) => r.json())
.then((r) =>
parseInt(
r.result.result
.map((c) => String.fromCharCode(c))
.join("")
.replace(/\"/g, "")
)
);
}
async function getStakingPools() {
return await fetch("https://api.fastnear.com/v1/account/" + treasuryDaoID + "/staking").then(r => r.json())
}
window.onload = async () => {
const poolResp = await getStakingPools();
const pools = await Promise.all(poolResp.pools.map(async (i) => {
const balance = await getAccountBalance(i.pool_id, poolResp.account_id);
return {pool: i.pool_id, balance};
}));
window.parent.postMessage({ handler: "stakedNearPool", pools }, "*");
};
</script>
</body>
</html>
`;

const iframe = (
<iframe
style={{
display: "none",
}}
srcDoc={code}
onMessage={(e) => {
switch (e.handler) {
case "stakedNearPool":
const pools = e.pools;
let sum = new Big(0);
pools.forEach((pool) => {
let bigNum = new Big(pool.balance).div(1e24);
sum = sum.plus(bigNum);
});
setNearStakedTokens(sum.toFixed() ?? "0");
setPoolWithBalance(pools);
break;
}
}}
/>
);

return iframe;
37 changes: 20 additions & 17 deletions instances/treasury-devdao.near/widget/components/TokensDropdown.jsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
const { getNearBalances } = VM.require(
"${REPL_BASE_DEPLOYMENT_ACCOUNT}/widget/lib.common"
);
const instance = props.instance;
if (!instance) {
return <></>;
Expand All @@ -14,27 +17,18 @@ const ftTokensResp = fetch(
`https://api3.nearblocks.io/v1/account/${treasuryDaoID}/inventory`
);

const nearBalanceResp = fetch(
`https://api3.nearblocks.io/v1/account/${treasuryDaoID}`
);
const nearBalances = getNearBalances(treasuryDaoID);

if (
!ftTokensResp ||
!Array.isArray(ftTokensResp?.body?.inventory?.fts) ||
!nearBalanceResp
typeof getNearBalances !== "function"
) {
return <></>;
}

const nearBalance = Big(nearBalanceResp?.body?.account?.[0]?.amount ?? "0")
.div(Big(10).pow(24))
.toFixed(4);
const lockedStorageAmt = Big(
nearBalanceResp?.body?.account?.[0]?.storage_usage ?? "0"
)
.div(Big(10).pow(5))
.toFixed(5);
const [options, setOptions] = useState([]);
const [nearStakedTokens, setNearStakedTokens] = useState(null);

const tokensWithBalance =
ftTokensResp?.body?.inventory?.fts.filter((i) => parseFloat(i.amount) > 0) ??
Expand All @@ -46,7 +40,7 @@ useEffect(() => {
icon: nearTokenIcon,
title: "NEAR",
value: "NEAR",
tokenBalance: nearBalance,
tokenBalance: nearBalances.totalParsed,
},
];

Expand All @@ -71,8 +65,9 @@ const [isOpen, setIsOpen] = useState(false);
const [selectedOptionValue, setSelectedValue] = useState(selectedValue);

function getNearAvailableBalance(tokenBalance) {
return Big(tokenBalance ?? "0")
.minus(lockedStorageAmt ?? "0")
return Big(tokenBalance)
.minus(nearBalances.lockedParsed ?? "0")
.minus(nearStakedTokens ?? "0")
.toFixed(4);
}
const toggleDropdown = () => {
Expand Down Expand Up @@ -171,8 +166,9 @@ const Item = ({ option }) => {
<div className="d-flex flex-column gap-1 w-100 text-wrap">
<div className="h6 mb-0"> {option.title}</div>
{option.value === "NEAR" && (
<div className="text-sm text-muted w-100 text-wrap">
Tokens locked for storage: {lockedStorageAmt}
<div className="d-flex flex-column gap-1 w-100 text-wrap text-sm text-muted">
<div>Tokens locked for storage: {nearBalances.lockedParsed}</div>
{nearStakedTokens && <div>Tokens staked: {nearStakedTokens}</div>}
</div>
)}
<div className="text-sm text-muted w-100 text-wrap">
Expand All @@ -191,6 +187,13 @@ const selectedOption =

return (
<Container>
<Widget
src={`${REPL_BASE_DEPLOYMENT_ACCOUNT}/widget/components.StakedNearIframe`}
props={{
instance,
setNearStakedTokens: (v) => setNearStakedTokens(Big(v).toFixed(4)),
}}
/>
<div
className="custom-select w-100"
tabIndex="0"
Expand Down
17 changes: 10 additions & 7 deletions instances/treasury-devdao.near/widget/components/VoteActions.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ const showRejectToast = props.showRejectToast ?? (() => {});
const currentAmount = props.currentAmount ?? "0";
const currentContract = props.currentContract ?? "";
const setVoteProposalId = props.setVoteProposalId ?? (() => {});
const avoidCheckForBalance = props.avoidCheckForBalance;

const alreadyVoted = Object.keys(votes).includes(accountId);
const userVote = votes[accountId];
Expand All @@ -32,12 +33,14 @@ const [showWarning, setShowWarning] = useState(false);
const [showConfirmModal, setConfirmModal] = useState(null);

useEffect(() => {
setInsufficientBal(
Big(
tokensBalance.find((i) => i.contract === currentContract)?.amount ?? "0"
).lt(Big(currentAmount ?? "0"))
);
}, [tokensBalance, currentAmount, currentContract]);
if (!avoidCheckForBalance) {
setInsufficientBal(
Big(
tokensBalance.find((i) => i.contract === currentContract)?.amount ?? "0"
).lt(Big(currentAmount ?? "0"))
);
}
}, [tokensBalance, currentAmount, currentContract, avoidCheckForBalance]);

function actProposal() {
setTxnCreated(true);
Expand All @@ -48,7 +51,7 @@ function actProposal() {
id: proposalId,
action: vote,
},
gas: 200000000000000,
gas: 300000000000000,
});
}

Expand Down
Loading

0 comments on commit e221939

Please sign in to comment.