Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Address endpoints (simplified) #43

Merged
merged 7 commits into from
Aug 7, 2023
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
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,5 @@ out.log
exchanges.json
carousel-data.json
circulating-supply-data.json
total-supply-data.json
total-supply-data.json
addresses-data.json
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
"cors": "^2.8.5",
"cross-fetch": "^3.0.6",
"dotenv": "^8.2.0",
"ejs": "^3.1.9",
"express": "^4.17.1",
"inquirer": "^7.3.3",
"locks": "^0.2.2",
Expand Down
38 changes: 38 additions & 0 deletions public/address_ui.ejs
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Joystream Address UI</title>
<link rel="icon" href="https://www.joystream.org/favicon-32x32.png">
<script src="https://cdn.tailwindcss.com"></script>
</head>
<body class="min-h-screen">
<main class="min-h-screen flex flex-col items-center justify-center">
<div class="w-full max-w-lg">
<form class="bg-white shadow-md rounded px-8 pt-6 pb-8 mb-4">
<div class="mb-6">
<label class="block text-gray-700 text-sm font-bold mb-2" for="address">
Address
</label>
<input class="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline" type="text" placeholder="Address" name="address" value="<%= locals?.address %>">
</div>
<div>
<p class="text-gray-700 text-md font-bold mb-2">Recorded at block: <span class="font-normal"><%= locals?.recorded_at_block %></span></p>
<p class="text-gray-700 text-md font-bold mb-2">Recorded at time: <span class="font-normal"><%= locals?.recorded_at_time %></span></p>
<p class="text-gray-700 text-md font-bold mb-2">Locked balance: <span class="font-normal"><%= locals?.locked_balance %></span></p>
<p class="text-gray-700 text-md font-bold mb-2">Total balance: <span class="font-normal"><%= locals?.total_balance %></span></p>
<p class="text-gray-700 text-md font-bold mb-2">Transferrable balance: <span class="font-normal"><%= locals?.transferrable_balance %></span></p>
<p class="text-gray-700 text-md font-bold mb-2">Vesting lock: <span class="font-normal"><%= locals?.vesting_lock %></span></p>
</div>
<div class="flex items-center justify-center">
<button class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded focus:outline-none focus:shadow-outline" type="submit">
Search
</button>
</div>
</form>
</div>
</main>
</body>
</html>
80 changes: 79 additions & 1 deletion src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,16 +12,20 @@ import getPrice from "./get-price";
import getCirculatingSupply from "./get-circulating-supply";
import { calculateSecondsUntilNext5MinuteInterval } from "./utils";
import getTotalSupply from "./get-total-supply";
import getAddresses from "./get-addresses";

const app = express();
const cache = apicache.middleware;
const port = process.env.PORT || 8081;
const CAROUSEL_DATA_PATH = path.join(__dirname, "../carousel-data.json");
const CIRCULATING_SUPPLY_DATA_PATH = path.join(__dirname, "../circulating-supply-data.json");
const TOTAL_SUPPLY_DATA_PATH = path.join(__dirname, "../total-supply-data.json");
const ADDRESSES_DATA_PATH = path.join(__dirname, "../addresses-data.json");
const ADDRESS_UI_HTML = path.join(__dirname, "../public/address_ui.ejs");

app.use(cors());
app.use(express.json());
app.set("view engine", "ejs");

const scheduleCronJob = async () => {
console.log("Scheduling cron job...");
Expand All @@ -35,14 +39,31 @@ const scheduleCronJob = async () => {
const fetchAndWriteSupplyData = async () => {
const circulatingSupplyData = await getCirculatingSupply();
const totalSupplyData = await getTotalSupply();
const { addresses } = await getAddresses();

fs.writeFileSync(CIRCULATING_SUPPLY_DATA_PATH, JSON.stringify(circulatingSupplyData, null, 2));
fs.writeFileSync(TOTAL_SUPPLY_DATA_PATH, JSON.stringify(totalSupplyData, null, 2));
fs.writeFileSync(
ADDRESSES_DATA_PATH,
JSON.stringify(
{
total_supply: totalSupplyData.totalSupply,
circulating_supply: circulatingSupplyData.circulatingSupply,
addresses,
},
null,
2
)
);
};

// Fetch data initially such that we have something to serve. There will at most
// be a buffer of 5 minutes from this running until the first cron execution.
if (!fs.existsSync(CIRCULATING_SUPPLY_DATA_PATH) || !fs.existsSync(TOTAL_SUPPLY_DATA_PATH))
if (
!fs.existsSync(CIRCULATING_SUPPLY_DATA_PATH) ||
!fs.existsSync(TOTAL_SUPPLY_DATA_PATH) ||
!fs.existsSync(ADDRESSES_DATA_PATH)
)
await fetchAndWriteSupplyData();
if (!fs.existsSync(CAROUSEL_DATA_PATH)) await fetchAndWriteCarouselData();

Expand Down Expand Up @@ -109,6 +130,63 @@ app.get("/total-supply", async (req, res) => {
res.status(503).send();
});

app.get("/addresses", async (req, res) => {
if (fs.existsSync(ADDRESSES_DATA_PATH)) {
const addressesFileData = fs.readFileSync(ADDRESSES_DATA_PATH);
res.setHeader("Content-Type", "application/json");
const addressesData = JSON.parse(addressesFileData.toString());
res.send(addressesData);

return;
}

res.setHeader("Retry-After", calculateSecondsUntilNext5MinuteInterval());
res.status(503).send();
});

app.get("/address", async (req, res) => {
if (fs.existsSync(ADDRESSES_DATA_PATH)) {
res.setHeader("Content-Type", "application/json");

if (!req.query.address) {
res.send({});
return;
}

const addressesFileData = fs.readFileSync(ADDRESSES_DATA_PATH);
const { addresses } = JSON.parse(addressesFileData.toString());
const receivedAddress = req.query.address as string;

res.send({ [receivedAddress]: addresses[receivedAddress] });

return;
}

res.setHeader("Retry-After", calculateSecondsUntilNext5MinuteInterval());
res.status(503).send();
});

app.get("/address_ui", async (req, res) => {
if (!fs.existsSync(ADDRESSES_DATA_PATH)) {
res.setHeader("Retry-After", calculateSecondsUntilNext5MinuteInterval());
res.status(503).send();
return;
}

if (!req.query.address) {
res.render(ADDRESS_UI_HTML);
return;
}

const addressesFileData = fs.readFileSync(ADDRESSES_DATA_PATH);
const { addresses } = JSON.parse(addressesFileData.toString());

res.render(ADDRESS_UI_HTML, {
address: req.query.address,
...addresses[req.query.address as string],
});
});

scheduleCronJob().then(() => {
app.listen(port, () => {
log(`server started at http://localhost:${port}`);
Expand Down
13 changes: 13 additions & 0 deletions src/get-addresses.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { JoyApi } from "./joyApi";

const api = new JoyApi();

const getAddresses = async () => {
await api.init;

const addresses = await api.getAddresses();

return { addresses };
};

export default getAddresses;
95 changes: 92 additions & 3 deletions src/joyApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import {
PalletWorkingGroupGroupWorker as Worker,
PalletReferendumReferendumStage as ReferendumStage,
PalletCouncilCouncilStageUpdate as CouncilStageUpdate,
PalletVestingVestingInfo
PalletVestingVestingInfo,
} from '@polkadot/types/lookup'
import { Vec } from '@polkadot/types';

Expand All @@ -21,6 +21,7 @@ if(process.env.QUERY_NODE === undefined){
throw new Error("Missing QUERY_NODE in .env!");
}
const QUERY_NODE = process.env.QUERY_NODE;
const VESTING_STRING_HEX = "0x76657374696e6720";

type SystemData = {
chain: string
Expand Down Expand Up @@ -81,6 +82,15 @@ type NetworkStatus = {
runtimeData: RuntimeData
}

type Address = {
recorded_at_block: number;
recorded_at_time: string;
total_balance: number
transferrable_balance: number
locked_balance: number
vesting_lock: number
}

export class JoyApi {
endpoint: string;
tokenDecimals!: number;
Expand Down Expand Up @@ -373,8 +383,6 @@ export class JoyApi {


async calculateCirculatingSupply() {
const VESTING_STRING_HEX = "0x76657374696e6720";

const accounts = [];
const amounts: BN[] = [];
const lockData = await this.api.query.balances.locks.entries();
Expand Down Expand Up @@ -407,6 +415,87 @@ export class JoyApi {
return totalSupply - this.toJOY(total);
}

async getAddresses() {
const finalizedHeadHash = await this.finalizedHash();
const { number: blockNumber } = await this.api.rpc.chain.getHeader(`${finalizedHeadHash}`);
const timestamp = await this.api.query.timestamp.now.at(finalizedHeadHash);
const finalizedApi = await this.api.at(finalizedHeadHash);
const currentBlock = blockNumber.toBn();
const currentTime = (new Date(timestamp.toNumber())).toISOString();

const lockData = await finalizedApi.query.balances.locks.entries();
const systemAccounts = await finalizedApi.query.system.account.entries();
const resultData = systemAccounts.reduce((acc, [key, account]) => {
const address = key.args[0].toString();

acc[address] = {
tempAmount: new BN(0),
tempAmount2: new BN(0),
recorded_at_block: currentBlock.toNumber(),
recorded_at_time: currentTime,
total_balance: 0,
transferrable_balance: 0,
locked_balance: 0,
vesting_lock: 0,
};

return acc;
}, {} as {
[key: string]: {
tempAmount: BN;
tempAmount2: BN;
} & Address;
});

for (let [storageKey, palletBalances] of lockData) {
let biggestLock = new BN(0);
let biggestVestingLock = new BN(0);
const address = storageKey.args[0].toString();

for (let palletBalance of palletBalances) {
if(palletBalance.amount.toBn().gt(biggestLock)) {
biggestLock = palletBalance.amount.toBn();
}

if (
palletBalance.id.toString() === VESTING_STRING_HEX &&
palletBalance.amount.toBn().gt(biggestVestingLock)
) {
biggestVestingLock = palletBalance.amount.toBn();
}
}

if(biggestLock.gt(new BN(0))) {
resultData[address].tempAmount = biggestLock;
}

if (biggestVestingLock.gt(new BN(0))) {
resultData[address].vesting_lock = this.toJOY(biggestVestingLock);
resultData[address].tempAmount2 = biggestVestingLock;
}
}

systemAccounts.forEach(([key, account]) => {
const address = key.args[0].toString();

const totalBalance = this.toJOY(account.data.free);
const lockedBalance = this.toJOY(
BN.min(resultData[address].tempAmount, BN.min(account.data.free, account.data.miscFrozen))
);
resultData[address].total_balance = totalBalance;
resultData[address].transferrable_balance = totalBalance - lockedBalance;
resultData[address].locked_balance = lockedBalance;
resultData[address].vesting_lock = this.toJOY(
BN.min(resultData[address].tempAmount2, BN.min(account.data.free, account.data.miscFrozen))
);
});

Object.keys(resultData).forEach((address) => { delete (resultData[address] as any).tempAmount; });
Object.keys(resultData).forEach((address) => { delete (resultData[address] as any).tempAmount2; });

return resultData as { [key: string]: Address };
}

protected async fetchNetworkStatus(): Promise<NetworkStatus> {
const [
[
Expand Down
Loading