Evaluating worker proposal support from all Bitshares blockchain accounts!

in BitShares23 days ago (edited)

image.png

Let's check out the Astro-UI's blockchain support!

The Astro-UI is a next-generation (evergreen) Bitshares user interface which directly integrates with both the Bitshares BEET multiwallet and the BeetEOS multiwallet for broadcasting to the bitshares blockchain.

It's developed using Astro, Electron, and React.

It's fully open source, using the MIT license, and quite full-featured, only a few features remain on the TODO list ahead of advanced functionality like account creation.

I created a worker proposal earlier this year, however it's not yet been activated and the current Bitshares voting system isn't tallying users votes at the moment.

image.png

So, in order to gauge how much support the Astro-UI has, we must fetch all Bitshares accounts from the blockchain!

const fs = require("fs");
const path = require("path");
const { Apis } = require("bitsharesjs-ws");

const node = "wss://node.xbts.io/ws";

const outputDir = path.join(__dirname, "fetchedData");
const outputFile = path.join(outputDir, "allAccounts.json");

const chunkSize = 2500; // Number of accounts to fetch per iteration

// Get the latest ID for an object in the blockchain
async function getLatestAccountID(node) {
  return new Promise(async (resolve, reject) => {
    try {
      await Apis.instance(node, true).init_promise;
    } catch (error) {
      console.error("Error initializing API:", error);
      reject(error);
      return;
    }

    if (!Apis.instance().db_api()) {
      console.error("No db_api available");
      reject(new Error("No db_api available"));
      return;
    }

    try {
      const nextObjectId = await Apis.instance().db_api().exec("get_next_object_id", [1, 2, false]);
      resolve(parseInt(nextObjectId.split(".")[2], 10) - 1);
    } catch (error) {
      console.error("Error fetching latest account ID:", error);
      reject(error);
    }
  });
}

function sliceIntoChunks(arr, size) {
  const chunks = [];
  for (let i = 0; i < arr.length; i += size) {
    chunks.push(arr.slice(i, i + size));
  }
  return chunks;
}

async function getObjects(node, object_ids) {
  return new Promise(async (resolve, reject) => {
    try {
      await Apis.instance(node, true).init_promise;
    } catch (error) {
      console.error("Error initializing API:", error);
      reject(error);
      return;
    }

    if (!Apis.instance().db_api()) {
      console.error("No db_api available");
      reject(new Error("No db_api available"));
      return;
    }

    let retrievedObjects = [];
    const chunksOfInputs = sliceIntoChunks(object_ids, 100);
    for (const [index, chunk] of chunksOfInputs.entries()) {
      try {
        console.log(`Fetching chunk ${index + 1}/${chunksOfInputs.length}`);
        const got_objects = await Apis.instance().db_api().exec("get_objects", [chunk, false]);
        if (got_objects && got_objects.length) {
          retrievedObjects = retrievedObjects.concat(got_objects.filter((x) => x !== null));
        }
      } catch (error) {
        console.error("Error fetching objects:", error);
        if (retrievedObjects.length > 0) {
          resolve(retrievedObjects);
        } else {
          reject(error);
        }
        return;
      }
    }

    resolve(retrievedObjects);
  });
}

function writeToFile(data) {
  try {
    console.log(`Writing to ${outputFile}`);
    fs.writeFileSync(outputFile, JSON.stringify(data, null, 2));
  } catch (error) {
    console.error(`Error writing to file: ${error.message}`);
    throw error;
  }
}

function readFromFile() {
  try {
    if (fs.existsSync(outputFile)) {
      const data = fs.readFileSync(outputFile);
      return JSON.parse(data);
    }
    return [];
  } catch (error) {
    console.error(`Error reading from file: ${error.message}`);
    return [];
  }
}

async function main() {
  const latestAccountID = await getLatestAccountID(node);

  let existingAccounts = readFromFile();

  const startID =
    existingAccounts.length > 0
      ? parseInt(existingAccounts[existingAccounts.length - 1].id.split(".")[2], 10) + 1
      : 0;

  const accountIDs = Array.from(
    { length: latestAccountID - startID + 1 },
    (_, i) => `1.2.${startID + i}`
  );

  const chunksOfAccountIDs = sliceIntoChunks(accountIDs, chunkSize);

  for (const chunk of chunksOfAccountIDs) {
    let holderAccountObjects;
    try {
      holderAccountObjects = await getObjects(node, chunk);
    } catch (error) {
      console.error("Error fetching account objects:", error);
      if (holderAccountObjects && holderAccountObjects.length > 0) {
        const filteredObjects = holderAccountObjects.map((account) => ({
          id: account.id,
          votes: account.options.votes,
          voting_account: account.options.voting_account,
        }));
        existingAccounts = existingAccounts.concat(filteredObjects);
        writeToFile(existingAccounts);
      }
      continue;
    }

    const filteredObjects = holderAccountObjects.map((account) => ({
      id: account.id,
      votes: account.options.votes,
      voting_account: account.options.voting_account,
    }));
    existingAccounts = existingAccounts.concat(filteredObjects);
    writeToFile(existingAccounts);
  }

  console.log("All accounts have been fetched and saved.");
  process.exit();
}

process.on("unhandledRejection", (reason, promise) => {
  console.error("Unhandled Rejection at:", promise, "reason:", reason);
  process.exit(1);
});

main();

.
This script will fetch the latest account ID, then it'll fetch each of the accounts details from the blockchain and store the relevant information we want in a JSON file.

Note that since we're fetching aronud 2 million accounts at 100 accounts per request, we'll be performing around 20,000 blockchain queries; please don't spam this script and if you must run this repeatedly do consider running your own local Bitshares node to speed up the data extraction process.

At this point we have a list of everyone's account id, their votes and their optional vote proxy status, we now need to filter this data!

const fs = require("fs");
const path = require("path");

// Config
const inputFile = path.join(__dirname, "fetchedData", "allAccounts.json");
const votersFile = path.join(__dirname, "fetchedData", "actual_voters.json");
const targetVotersFile = path.join(__dirname, "fetchedData", "target_voters.json");

function readAccountData() {
  console.log("Reading account data...");
  const data = fs.readFileSync(inputFile, "utf8");
  return JSON.parse(data);
}

function writeToFile(filename, data) {
  console.log(`Writing to ${filename}...`);
  fs.writeFileSync(filename, JSON.stringify(data, null, 2));
}

function filterActiveVoters(accounts) {
  console.log("Filtering for active voters...");
  return accounts.filter((account) => {
    const hasNoVotes = !account.votes || account.votes.length === 0;
    const isDefaultProxy = account.voting_account === "1.2.5";
    return !(hasNoVotes && isDefaultProxy);
  });
}

function findTargetVoters(accounts) {
  console.log("Finding target voters...");

  // Create map of accounts that directly voted for 2:841
  const directVoters = new Set(
    accounts
      .filter((account) => account.votes && account.votes.includes("2:841"))
      .map((account) => account.id)
  );

  // Find all accounts that either voted directly or proxy voted
  return accounts.filter(
    (account) =>
      (account.votes && account.votes.includes("2:841")) || // Direct vote
      directVoters.has(account.voting_account) // Proxy vote
  );
}

async function main() {
  try {
    // Read input file
    const allAccounts = readAccountData();
    console.log(`Total accounts: ${allAccounts.length}`);

    // First filter - active voters
    const activeVoters = filterActiveVoters(allAccounts);
    console.log(`Active voters: ${activeVoters.length}`);
    writeToFile(votersFile, activeVoters);

    // Second filter - target voters
    const targetVoters = findTargetVoters(activeVoters);
    console.log(`Target voters: ${targetVoters.length}`);
    writeToFile(targetVotersFile, targetVoters);
  } catch (error) {
    console.error("Error:", error);
    process.exit(1);
  }
}

main();

.
OK, so this has filtered out the inactive (non-voters), and those who both haven't voted for the worker proposal nor have a proxy who has voted for the worker proposal.

We now need to fetch their balances and names from the blockchain!

const fs = require("fs");
const path = require("path");
const { Apis } = require("bitsharesjs-ws");
const { humanReadableFloat } = require("./lib/common");

// Constants
const node = "wss://node.xbts.io/ws";
const fetchedDataDir = path.join(__dirname, "fetchedData");
const inputFile = path.join(fetchedDataDir, "target_voters.json");
const outputFile = path.join(fetchedDataDir, "voters_complete.json");
const chunkSize = 100;

// Ensure directory exists
if (!fs.existsSync(fetchedDataDir)) {
  fs.mkdirSync(fetchedDataDir, { recursive: true });
}

async function getBalancesAndNames(accountIDs) {
  const accounts = await Apis.instance().db_api().exec("get_accounts", [accountIDs]);
  const balancePromises = accounts.map((acc) =>
    Apis.instance()
      .db_api()
      .exec("get_account_balances", [acc.id, ["1.3.0"]])
  );
  const balances = await Promise.all(balancePromises);

  return accounts.map((acc, i) => ({
    id: acc.id,
    name: acc.name,
    bts_balance: humanReadableFloat(balances[i][0]?.amount || 0, 5),
  }));
}

function sliceIntoChunks(arr, size) {
  return Array.from({ length: Math.ceil(arr.length / size) }, (v, i) =>
    arr.slice(i * size, i * size + size)
  );
}

async function main() {
  try {
    // Read input file
    const voters = JSON.parse(fs.readFileSync(inputFile, "utf8"));
    console.log(`Processing ${voters.length} accounts...`);

    // Initialize API
    await Apis.instance(node, true).init_promise;

    let totalBTS = 0;
    let votersComplete = [];
    const chunks = sliceIntoChunks(
      voters.map((v) => v.id),
      chunkSize
    );

    // Process chunks
    for (const [index, chunk] of chunks.entries()) {
      console.log(`Processing chunk ${index + 1}/${chunks.length}...`);
      const accountData = await getBalancesAndNames(chunk);

      for (const voter of voters) {
        if (chunk.includes(voter.id)) {
          const accountInfo = accountData.find((a) => a.id === voter.id);
          if (accountInfo) {
            totalBTS += parseFloat(accountInfo.bts_balance);
            votersComplete.push({
              ...voter,
              name: accountInfo.name,
              bts_balance: accountInfo.bts_balance,
            });
          }
        }
      }
    }

    // Write results
    const result = {
      total_bts: totalBTS,
      voters: votersComplete,
    };

    fs.writeFileSync(outputFile, JSON.stringify(result, null, 2));
    console.log(`Results written to ${outputFile}`);
    console.log(`Total BTS: ${totalBTS}`);
  } catch (error) {
    console.error("Error:", error);
  } finally {
    Apis.close();
  }
}

main();

.
This results in the following data:

{
  "total_bts": 4864187.05407,
  "voters": [
    {
      "id": "1.2.159",
      "votes": ["2:841"],
      "voting_account": "1.2.5",
      "name": "ebit",
      "bts_balance": 3322165.71886
    },
    {
      "id": "1.2.12917",
      "votes": [],
      "voting_account": "1.2.159",
      "name": "nie",
      "bts_balance": 9.07864
    },
    {
      "id": "1.2.18769",
      "votes": [],
      "voting_account": "1.2.159",
      "name": "x.ebit",
      "bts_balance": 0.00007
    },
    {
      "id": "1.2.19791",
      "votes": [],
      "voting_account": "1.2.159",
      "name": "rose.ebit",
      "bts_balance": 0
    },
    {
      "id": "1.2.36604",
      "votes": [],
      "voting_account": "1.2.159",
      "name": "ebitags",
      "bts_balance": 82.0943
    },
    {
      "id": "1.2.98546",
      "votes": ["2:841"],
      "voting_account": "1.2.5",
      "name": "brekyrse1f3",
      "bts_balance": 400221.5137
    },
    {
      "id": "1.2.99396",
      "votes": [],
      "voting_account": "1.2.159",
      "name": "bts.tips",
      "bts_balance": 89.22596
    },
    {
      "id": "1.2.102858",
      "votes": [],
      "voting_account": "1.2.159",
      "name": "phuketcoinshow-com",
      "bts_balance": 276.1813
    },
    {
      "id": "1.2.102889",
      "votes": [],
      "voting_account": "1.2.159",
      "name": "openler-com",
      "bts_balance": 29.13133
    },
    {
      "id": "1.2.125906",
      "votes": [],
      "voting_account": "1.2.159",
      "name": "tianan",
      "bts_balance": 80.0077
    },
    {
      "id": "1.2.128942",
      "votes": [],
      "voting_account": "1.2.159",
      "name": "sep",
      "bts_balance": 69.21949
    },
    {
      "id": "1.2.188053",
      "votes": [],
      "voting_account": "1.2.159",
      "name": "tim-zhang",
      "bts_balance": 0.01462
    },
    {
      "id": "1.2.271466",
      "votes": ["0:112"],
      "voting_account": "1.2.159",
      "name": "qile",
      "bts_balance": 0.67165
    },
    {
      "id": "1.2.444065",
      "votes": ["1:328"],
      "voting_account": "1.2.159",
      "name": "alpha-zero",
      "bts_balance": 0.52794
    },
    {
      "id": "1.2.626369",
      "votes": [],
      "voting_account": "1.2.159",
      "name": "ades1981",
      "bts_balance": 7.16294
    },
    {
      "id": "1.2.1014025",
      "votes": ["2:841"],
      "voting_account": "1.2.5",
      "name": "johnr",
      "bts_balance": 9.73584
    },
    {
      "id": "1.2.1803677",
      "votes": ["2:841"],
      "voting_account": "1.2.5",
      "name": "nftprofessional1",
      "bts_balance": 722811.24658
    },
    {
      "id": "1.2.1804072",
      "votes": ["2:841"],
      "voting_account": "1.2.5",
      "name": "nft.artist",
      "bts_balance": 418335.52315
    }
  ]
}

.
Which can be summarized to:

Processing 18 accounts with a total of 4,864,187.05407 BTS

Direct supporters: ebit, brekyrse1f3, johnr
Proxy based supporters: nie, x.ebit, rose.ebit, ebitags, bts.tips, phuketcoinshow-com, openler-com, tianan, sep, tim-zhang, qile, alpha-zero, ades1981

Thanks to the above supporters of the Bitshares Astro-UI project!


With the worker proposal payment eligibility begining on 1st April 2024 and ending on 1st April 2026 we're roughly 30% through the worker proposal period already.

Something else to bear in mind, the above is liquid BTS, not locked BTS held in vote lock tickets, so there isn't actual vote weight above, it's just an indication of bare minimal blockchain support.

Thus far no reserve funding has been allocated to the project, as the worker proposal lacks sufficient network support.


Thanks for reading this far, I hope you're able to use some of the code above to perform your own blockchain data analysis, just mind to not spam the network with too many requests!

Sort:  


The rewards earned on this comment will go directly to the people sharing the post on Reddit as long as they are registered with @poshtoken. Sign up at https://hiveposh.com. Otherwise, rewards go to the author of the blog post.