Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
22 changes: 16 additions & 6 deletions src/LiNEAR/Account.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,16 @@ function formatDate(timestamp) {
].join("/");
}

function formatDateTime(timestamp) {
const d = new Date(timestamp);
const time = [
("0" + d.getHours()).slice(-2),
("0" + d.getMinutes()).slice(-2),
("0" + d.getSeconds()).slice(-2),
].join(":");
return formatDate(timestamp) + " " + time;
}

const data = state.data || {};
const stakingRewards = data.stakingRewards
? formatAmount(
Expand Down Expand Up @@ -263,9 +273,9 @@ return (
<div style={{ display: "flex", justifyContent: "space-between" }}>
<div>
<TokenValue>
{!endTime.remainingHours && <div>Unstake is Ready</div>}
{endTime.remainingHours && (
<div>~${endTime.remainingHours} hours</div>
{endTime.ready && <div>Unstake is Ready</div>}
{!endTime.ready && endTime.remainingHours && (
<div>~{endTime.remainingHours} hours</div>
)}
</TokenValue>
<GrayContent>Remaining</GrayContent>
Expand All @@ -277,21 +287,21 @@ return (
onClick: () => {
Near.call(config.contractId, "withdraw_all", {});
},
disabled: !!endTime.remainingHours,
disabled: !endTime.ready,
text: "Withdraw",
size: "base",
full: "full",
}}
/>
</div>
</div>
{endTime.remainingHours && (
{endTime.timestamp && (
<>
<HorizontalLine />
<div>
<div>
<TokenValue>
<div>{endTime.time}</div>
<div>{formatDateTime(endTime.timestamp)}</div>
</TokenValue>
<GrayContent>Withdrawal will be available</GrayContent>
</div>
Expand Down
183 changes: 64 additions & 119 deletions src/LiNEAR/Data/Unstake.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,11 @@ if (!config) {
// one hour in ms
const HOUR_MS = 60 * 60 * 1000;

function isValid(a) {
if (!a) return false;
if (isNaN(Number(a))) return false;
if (a === "") return false;
return true;
function nsToMs(ns) {
return Math.round((ns ?? 0) / 1e6);
}

function getValidators() {
function callNearRpc(method, params) {
const options = {
method: "POST",
headers: {
Expand All @@ -24,141 +21,89 @@ function getValidators() {
body: JSON.stringify({
jsonrpc: "2.0",
id: "dontcare",
method: "validators",
params: [null],
method,
params,
}),
};
return fetch(config.nodeUrl, options).body.result;
}

function getBlock(blockId) {
const options = {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
jsonrpc: "2.0",
id: "dontcare",
method: "block",
params: !!blockId
? {
block_id: blockId,
}
: {
finality: "final",
},
}),
};
return fetch(config.nodeUrl, options).body.result;
function getValidators() {
return callNearRpc("validators", [null]);
}

function latestBlock() {
const lastBlock = getBlock();
const startBlock = getBlock(lastBlock.header.next_epoch_id);
const prevBlock = getBlock(lastBlock.header.epoch_id);
const startBlockHeight = startBlock.header.height;
let prevBlockTimestamp = Math.round((prevBlock.header.timestamp ?? 0) / 1e6);
let startBlockTimestamp = Math.round(
(startBlock.header.timestamp ?? 0) / 1e6
function getBlock(blockId) {
return callNearRpc(
"block",
!!blockId
? {
block_id: blockId,
}
: {
finality: "final",
}
);
let lastBlockTimestamp = Math.round((lastBlock.header.timestamp ?? 0) / 1e6);

if (startBlockTimestamp < new Date().getTime() - 48 * HOUR_MS) {
//genesis or hard-fork
startBlockTimestamp = new Date().getTime() - 6 * HOUR_MS;
}
if (prevBlockTimestamp < new Date().getTime() - 48 * HOUR_MS) {
//genesis or hard-fork
prevBlockTimestamp = new Date().getTime() - 12 * HOUR_MS;
}
}

//const noPrevBloc = startBlock.header.height == prevBlock.header.height;
let length = startBlock.header.height - prevBlock.header.height,
duration_ms = 0,
advance;
let start_dtm, ends_dtm, duration_till_now_ms;
if (length === 0) {
//!prevBlock, genesis or hard-fork
length = 43200;
duration_ms = 12 * HOUR_MS;
//estimated start & prev timestamps
advance =
Math.round(
Number(
((BigInt(lastBlock.header.height) - BigInt(this.start_block_height)) *
BigInt(1000000)) /
BigInt(this.length)
)
) / 1000000;
startBlockTimestamp = lastBlockTimestamp - duration_ms * advance;
prevBlockTimestamp = startBlockTimestamp - duration_ms;
} else {
duration_ms = startBlockTimestamp - prevBlockTimestamp;
function getEpochInfo() {
const now = new Date().getTime();
const latestBlock = getBlock();
const latestBlockTimestamp = nsToMs(latestBlock.header.timestamp);
const latestBlockHeight = latestBlock.header.height;

// last epoch
const epochEndBlock = getBlock(latestBlock.header.next_epoch_id);
const epochStartBlock = getBlock(latestBlock.header.epoch_id);
const epochEndBlockHeight = epochEndBlock.header.height;
const epochStartBlockHeight = epochStartBlockHeight;

let epochStartTimestamp = nsToMs(epochStartBlock.header.timestamp);
let epochEndTimestamp = nsToMs(epochEndBlock.header.timestamp);

let epochBlockNum = epochEndBlockHeight - epochStartBlockHeight;
let epochLengthMs = epochEndTimestamp - epochStartTimestamp;
if (epochBlockNum === 0) {
epochBlockNum = 43200;
epochLengthMs = 14 * HOUR_MS;
const scale = Big(latestBlockHeight)
.sub(epochEndBlockHeight)
.div(epochBlockNum)
.toNumber();
epochEndTimestamp = latestBlockTimestamp - epochLengthMs * scale;
epochStartTimestamp = epochEndTimestamp - epochLengthMs;
}

start_dtm = new Date(startBlockTimestamp);
ends_dtm = new Date(startBlockTimestamp + duration_ms);
duration_till_now_ms = lastBlockTimestamp - startBlockTimestamp;

// update function
if (isValid(lastBlock.header.height) && isValid(startBlockHeight)) {
advance =
Math.round(
Big(lastBlock.header.height)
.minus(startBlockHeight)
.times(1000000)
.div(length)
.toNumber()
) / 1000000;
if (advance > 0.1) {
ends_dtm = new Date(
startBlockTimestamp +
duration_till_now_ms +
duration_till_now_ms * (1 - advance)
);
}
}
return {
lastEpochDurationHours: duration_ms / HOUR_MS,
hoursToEnd:
Math.round(
((startBlockTimestamp + duration_ms - new Date().getTime()) / HOUR_MS) *
100
) / 100,
lastEpochLengthHours: epochLengthMs / HOUR_MS,
hoursTillEpochEnd: (epochEndTimestamp + epochLengthMs - now) / HOUR_MS,
};
}

function padNumber(n) {
if (n < 10) return `0${n}`;
return n.toString();
}

function getUnstakeEndTime(epochHeight) {
const nowValidator = getValidators();
let currentEpochHeight = nowValidator.epoch_height;
function estimateUnstakeEndTime(endEpochHeight) {
const validators = getValidators();
const currentEpochHeight = validators.epoch_height;

if (currentEpochHeight >= epochHeight) return {};
if (currentEpochHeight >= endEpochHeight) {
return {
ready: true,
};
}

const { hoursToEnd, lastEpochDurationHours } = latestBlock();
const BUFFER = 3; // 3 HOURs buffer
const { hoursTillEpochEnd, lastEpochLengthHours } = getEpochInfo();
const EXTRA_HOURS = 3;
const remainingHours =
hoursToEnd +
(epochHeight - currentEpochHeight - 1) * lastEpochDurationHours +
BUFFER;
(endEpochHeight - currentEpochHeight - 1) * lastEpochLengthHours +
hoursTillEpochEnd +
EXTRA_HOURS;

if (remainingHours) {
const endTime = new Date(new Date().getTime() + remainingHours * HOUR_MS);
if (remainingHours && remainingHours > 0) {
return {
time: `${endTime.getFullYear()}/${padNumber(
endTime.getMonth() + 1
)}/${padNumber(endTime.getDate())} ${padNumber(
endTime.getHours()
)}:${padNumber(endTime.getMinutes())}:${padNumber(endTime.getSeconds())}`,
ready: false,
timestamp: Date.now() + remainingHours * HOUR_MS,
remainingHours: Math.floor(remainingHours).toString(),
};
} else {
return {};
return null;
}
}

Expand All @@ -168,7 +113,7 @@ if (onLoad) {
});
const endTime =
accountDetails && accountDetails.unstaked_available_epoch_height
? getUnstakeEndTime(accountDetails.unstaked_available_epoch_height)
? estimateUnstakeEndTime(accountDetails.unstaked_available_epoch_height)
: {};

if (accountDetails) {
Expand Down