diff --git a/.gitignore b/.gitignore index 7fd9f58..93a8509 100644 --- a/.gitignore +++ b/.gitignore @@ -38,3 +38,7 @@ screenshot .eslintcache build + + +# pool +pool/ \ No newline at end of file diff --git a/README.md b/README.md index 8319617..b247337 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,84 @@ # Mining-Bot - Please Visit [Mining-Bot Documentation](https://daemon-technologies.github.io/docs/) - [WSL Tutorial Video](https://www.youtube.com/watch?v=FXifFx0Akzc) - [MacOS](https://www.youtube.com/watch?v=TCtCTttsSeI) + +## Pooling + +This pooling infrastructure assumes the following: + +- All inputs to your specified pooling address will be new contributors. +- All outputs to your specified pooling address will be btc spent on mining. + +### Formulation + +For cycle ![](https://latex.codecogs.com/png.latex?n): + +- Let ![](https://latex.codecogs.com/png.latex?X_{n-1}) be the total BTC contributed to the pool during cycle ![](https://latex.codecogs.com/png.latex?n-1). + +- Let ![](https://latex.codecogs.com/png.latex?Y_{n-1}) be the total BTC in the pool at the end of cycle ![](https://latex.codecogs.com/png.latex?n-1). + +- Let ![](https://latex.codecogs.com/png.latex?Z_{n-1}=Y_{n-1}-X_{n-1}), or the amount of BTC left in the pool after mining during cycle ![](https://latex.codecogs.com/png.latex?n-1) and not including the newest contributions in cycle ![](https://latex.codecogs.com/png.latex?n-1). + +- If you contributed ![](https://latex.codecogs.com/png.latex?c_{n-1}) BTC during cycle ![](https://latex.codecogs.com/png.latex?n-1), your reward percentage for cycle ![](https://latex.codecogs.com/png.latex?n) will be ![](https://latex.codecogs.com/png.latex?P_n=\frac{c_{n-1}}{X_{n-1}}*\frac{X_{n-1}}{Y_{n-1}}=\frac{c_{n-1}}{Y_{n-1}}). In other words, your reward percentage is based on how much you contributed in last cycle compared to all the BTC in the pool. + +- If you contributed ![](https://latex.codecogs.com/png.latex?c_{k}) BTC during cycle ![](https://latex.codecogs.com/png.latex?k) where ![](https://latex.codecogs.com/png.latex?k = () => { setNetworkName("Xenon"); message.info(`Switch to Xenon network`); changeNetworkDAO("Xenon"); + localStorage.removeItem("pooledBtcAddress") window.location.reload(); // switchPage('Xenon'); break; @@ -48,6 +49,7 @@ const SwitchNetwork: React.FC = () => { case "Mainnet": { setNetworkName("Mainnet"); message.info(`Switch to Mainnet network`); + localStorage.removeItem("pooledBtcAddress") changeNetworkDAO("Mainnet"); window.location.reload(); // switchPage('Xenon'); diff --git a/src/locales/en-US/menu.ts b/src/locales/en-US/menu.ts index dbf113f..f4d2895 100644 --- a/src/locales/en-US/menu.ts +++ b/src/locales/en-US/menu.ts @@ -3,6 +3,8 @@ export default { 'menu.wallet': 'Wallet', 'menu.client': 'Mining Client', 'menu.sysConf': 'System Configuration', + 'menu.managePool': 'Manage Pool', + 'menu.joinPool': 'Join Pool', 'menu.welcome': 'Welcome', 'menu.more-blocks': 'More Blocks', diff --git a/src/pages/joinPool/index.tsx b/src/pages/joinPool/index.tsx new file mode 100644 index 0000000..72715cf --- /dev/null +++ b/src/pages/joinPool/index.tsx @@ -0,0 +1,44 @@ +import React, { useEffect, useState } from "react"; +import { PageContainer } from "@ant-design/pro-layout"; +import { ConfigProvider } from "antd"; + +import { switchConfigProviderLocale } from "@/services/locale"; +import { + getCurrentCycle, + getCycleBlocks, + getPoolContributors, +} from "@/services/managePool/managePool"; + +const TableList: React.FC<{}> = () => { + const [currentCycle, setCurrentCycle] = useState(-1); + useEffect(() => { + getCurrentCycle().then(({ cycle }) => { + if (cycle) { + setCurrentCycle(cycle!); + } + }); + }, []); + + useEffect(() => { + const { startBlock, endBlock } = getCycleBlocks(currentCycle); + getPoolContributors(startBlock, endBlock).then((transactions) => { + if (transactions) { + console.log(transactions); + } + }); + }, [currentCycle]); + return ( + + +
joinPool
+
Cycle #{currentCycle}
+
+ From block {getCycleBlocks(currentCycle).startBlock} to + {getCycleBlocks(currentCycle).endBlock} +
+
+
+ ); +}; + +export default TableList; diff --git a/src/pages/managePool/component/PoolContributerTable.tsx b/src/pages/managePool/component/PoolContributerTable.tsx new file mode 100644 index 0000000..888598e --- /dev/null +++ b/src/pages/managePool/component/PoolContributerTable.tsx @@ -0,0 +1,222 @@ +import React, { useEffect, useState } from "react"; +import ProTable, { ProColumns } from "@ant-design/pro-table"; +import { FormattedMessage, useModel } from "umi"; +import { Card, InputNumber, Button, Table, Tooltip } from "antd"; +import { PoolContributerInfo, StxBalances } from "@/services/managePool/data"; +import { showMessage } from "@/services/locale"; +import { getNetworkFromStorage } from "@/utils/utils"; +import { + getBalanceAtBlock, + getCycleBlocks, + getCycleContributions, + getBtcHeight, +} from "@/services/managePool/managePool"; +import { getStxBalance } from "@/services/wallet/account"; +import { getCurrentCycle } from "@/services/managePool/managePool"; +const { bitcoinTestnet3, stxBalanceCoef } = require("@/services/constants"); +import { b58ToC32 } from "c32check"; + +const PoolContributerTable: React.FC<{}> = () => { + const { queryPoolContributerInfo } = useModel( + "managePool.poolContributerInfo" + ); + + const [currentCycle, setCurrentCycle] = useState(-1); + const [selectedCycle, setSelectedCycle] = useState(currentCycle); + const [stxBalance, setStxBalance] = useState(0); + const [currentBtcHeight, setBtcHeight] = useState(0); + + useEffect(() => { + let pooledBtcAddress = localStorage.getItem("pooledBtcAddress"); + if (pooledBtcAddress) { + getStxBalance(b58ToC32(pooledBtcAddress)).then((resp: StxBalances) => + setStxBalance(parseFloat(resp.stx.balance) / stxBalanceCoef) + ); + } + + getCurrentCycle().then(({ cycle }) => { + setCurrentCycle(cycle); + setSelectedCycle(cycle); + }); + + getBtcHeight().then((height) => setBtcHeight(height)); + }, []); + + const getDisabledReason = (): string => { + // TODO: if rewards were already sent out, disable button + const { endBlock } = getCycleBlocks(currentCycle - 1); + if (currentBtcHeight < endBlock + 100) { + return showMessage( + "TODO", + "Can only send rewards after 100 blocks after end of last cycle" + ); + } + return "Not implemented yet"; + }; + + const disabledReason = getDisabledReason(); + + const canSendRewards = (): boolean => { + if (selectedCycle != currentCycle - 1) { + return false; + } + return true; + }; + + const poolContributerColumns: ProColumns[] = [ + { + title: , + dataIndex: "address", + copyable: true, + ellipsis: true, + }, + { + title: ( + + ), + dataIndex: "stxAddress", + copyable: true, + ellipsis: true, + }, + { + title: ( + + ), + dataIndex: "contribution", + }, + { + title: ( + + ), + dataIndex: "cycleContribution", + }, + { + title: ( + + ), + dataIndex: "blockContribution", + }, + { + title: ( + + ), + dataIndex: "transactionHash", + render: (value) => { + let baseUrl = bitcoinTestnet3; + switch (getNetworkFromStorage()) { + case "Xenon": { + baseUrl = `https://live.blockcypher.com/btc-testnet/tx/${value}`; + break; + } + case "Mainnet": { + baseUrl = `https://live.blockcypher.com/btc/tx/${value}`; + break; + } + default: + break; + } + return ( + + ); + }, + }, + { + title: ( + + ), + dataIndex: "rewardPercentage", + }, + ]; + + return ( + <> + +
+ {showMessage("TODO", "View Contributors for Cycle:")} + +
+ + {/* {canSendRewards() && ( */} + {false && ( +
+
+ {showMessage("TODO", "STX to send: ")} +
+
+ +
+
+ + {/*TODO: add functionality for send many */} + + +
+
+ )} +
+ + headerTitle={ + + } + columns={poolContributerColumns} + request={() => queryPoolContributerInfo(selectedCycle)} + rowKey={"transactionHash"} + manualRequest={true} + params={{ selectedCycle }} + summary={(contributions) => { + let total = getCycleContributions(selectedCycle - 1); + + const { endBlock } = getCycleBlocks(selectedCycle - 1); + const balance = getBalanceAtBlock(endBlock); + return ( + <> + + + Total Contributed In Last Cycle + + + {total.toFixed(4)} + + + + + Total Remaining At End of Last Cycle + + + {balance.toFixed(4)} + + + + ); + }} + /> + + ); +}; + +export default PoolContributerTable; diff --git a/src/pages/managePool/index.tsx b/src/pages/managePool/index.tsx new file mode 100644 index 0000000..4d91076 --- /dev/null +++ b/src/pages/managePool/index.tsx @@ -0,0 +1,150 @@ +import React, { useEffect, useState } from "react"; +import { PageContainer } from "@ant-design/pro-layout"; +import { + Button, + Card, + ConfigProvider, + Divider, + Form, + Input, + InputNumber, + Select, + Switch, + message, +} from "antd"; + +import { queryAccount } from "@/services/wallet/account"; +import { Account } from "@/services/wallet/data"; +import { showMessage, switchConfigProviderLocale } from "@/services/locale"; +import FormItem from "antd/lib/form/FormItem"; +import { FormattedMessage } from "react-intl"; +import PoolContributerTable from "./component/PoolContributerTable"; +export interface FormValueType { + poolBtcAddress: string; + poolStartCycle: number; +} + +const TableList: React.FC<{}> = () => { + const { Option } = Select; + const [formVals, setFormVals] = useState({ + poolBtcAddress: localStorage.getItem("pooledBtcAddress") ?? "", + poolStartCycle: parseInt(localStorage.getItem("poolStartCycle") ?? "-1"), + }); + + const [accounts, setAccounts] = useState([]); + const [loadingAccounts, setLoadingAccounts] = useState(true); + + + const onSubmit = async () => { + const fieldsValue: FormValueType = await form.validateFields(); + setFormVals({ ...formVals, ...fieldsValue }); + localStorage.setItem("pooledBtcAddress", fieldsValue.poolBtcAddress); + localStorage.setItem( + "poolStartCycle", + fieldsValue.poolStartCycle.toString() + ); + message.success("Successfully saved!"); + }; + + const [isPooling, setIsPooling] = useState( + localStorage.getItem("isPooling") === "true" ?? false + ); + + useEffect(() => { + queryAccount(1).then(({ data }) => { + setAccounts(data); + setLoadingAccounts(false); + }); + + }, []); + + const renderForm = () => { + return ( + <> + + +
+ + + + + + + + + +
+
+ + ); + }; + + const [form] = Form.useForm(); + return ( + + + + { + setIsPooling(!isPooling); + localStorage.setItem("isPooling", (!isPooling).toString()); + }} + /> + {isPooling + ? showMessage("TODO", "Pooling is On") + : showMessage("TODO", "Pooling is Off")} + + {isPooling && renderForm()} + {isPooling && } + {isPooling && } + + + ); +}; + +export default TableList; diff --git a/src/pages/managePool/models/poolContributerInfo.ts b/src/pages/managePool/models/poolContributerInfo.ts new file mode 100644 index 0000000..9bd6d1d --- /dev/null +++ b/src/pages/managePool/models/poolContributerInfo.ts @@ -0,0 +1,199 @@ +import { PoolContributerInfo, Tx } from "@/services/managePool/data"; +import { + getCurrentCycle, + getCycleBlocks, + getCycleForBlock, + getPoolContributors, + PoolContributerInfoState, + LocalPoolContributors, + getLocalPoolContributorInfo, + setLocalPoolContributorInfo, + getBalanceAtBlock, + setLocalPoolBalances, + getLocalPoolBalance, + getPoolStartCycleBlocks, + getCycleContributions, +} from "@/services/managePool/managePool"; +import { useState } from "react"; +import { message } from "antd"; +import { getStxAddressFromPublicKey } from "@/services/wallet/key"; +import { getNetworkFromStorage } from "@/utils/utils"; +const { btcBalanceCoef } = require("@/services/constants"); +const bitcoinjs_lib_1 = require("bitcoinjs-lib"); +import { b58ToC32 } from "c32check"; + +// if transaction positive, this was an input / contribution, else output / spent on mining +const getTransactionValue = ( + pooledBtcAddress: string, + transaction: Tx +): number => { + let value = 0; + for (const input of transaction.inputs) { + if (input.addresses && input.addresses.includes(pooledBtcAddress)) { + value -= input.output_value; + } + } + for (const output of transaction.outputs) { + if (output.addresses && output.addresses.includes(pooledBtcAddress)) { + value += output.value; + } + } + // if this was an output, we also paid the fees + if (value < 0) { + value -= transaction.fees; + } + return value; +}; + +export default () => { + let [poolContributerInfoState, setPoolContributerInfoState] = + useState(); + const queryPoolContributerInfo = async (cycle: number) => { + let pooledBtcAddress = localStorage.getItem("pooledBtcAddress")!; + let res: PoolContributerInfo[] = getLocalPoolContributorInfo(); + let { endBlock } = getCycleBlocks(cycle - 1); + + // get highest height from local info + let highestHeight = Math.max(...res.map((o) => o.blockContribution)); + // if no saved transactions yet, set start block as the pool cycle start block + if (highestHeight < 0) { + highestHeight = getPoolStartCycleBlocks().startBlock; + } + let currentBalance = 0; + + if (endBlock > highestHeight) { + let { transactions, balance } = await getPoolContributors( + highestHeight, + endBlock + ); + + let txHashes = new Set(res.map((t) => t.transactionHash)); + transactions.map((transaction) => { + // if we already stored this transaction or its not confirmed yet, skip + if (txHashes.has(transaction.hash) || transaction.block_height == -1) { + return; + } + + let contribution = getTransactionValue(pooledBtcAddress, transaction); + if (contribution > 0) { + // sometimes the inputs can have multiple addresses, so we weigh contributions based on each address input + const totalInputvalue = transaction.inputs.reduce( + (prev, next) => prev + next.output_value, + 0 + ); + for (const input of transaction.inputs) { + let weightedContribution = + contribution * (input.output_value / totalInputvalue); + let address = input.addresses[0]; + let stxAddress = input.addresses[0]; + // BECH32 not supported + try { + stxAddress = b58ToC32(address); + } catch (err) { + stxAddress = "UNSUPPORTED"; + } + res.push({ + address: address, // TODO: deal with edge case where input has multiple addresses? + stxAddress: stxAddress, // b58ToC32(input.addresses[0]), + contribution: weightedContribution / btcBalanceCoef, + transactionHash: transaction.hash, + cycleContribution: getCycleForBlock(transaction.block_height), + blockContribution: transaction.block_height, + isContribution: true, + rewardPercentage: 0, + }); + } + } else { + res.push({ + address: "output", + stxAddress: "output", + contribution: contribution / btcBalanceCoef, + transactionHash: transaction.hash, + cycleContribution: getCycleForBlock(transaction.block_height), + blockContribution: transaction.block_height, + isContribution: false, + rewardPercentage: 0, + }); + } + }); + currentBalance = balance / btcBalanceCoef; + } else { + // currentBalance = await getBalance(); + } + + // sometimes API will return 0 tx for address, so only change local pool balance if we have a valid response + if (currentBalance > 0) { + setLocalPoolBalances(currentBalance); + } else { + currentBalance = getLocalPoolBalance(); + } + + let poolStartCycle = parseInt( + localStorage.getItem("poolStartCycle") ?? "-1" + ); + if (poolStartCycle == -1) { + message.error("poolStartCycle cannot be -1"); + } + + // cache of reward percentages per contribution + let cache = {}; + // sort from earlier contribution to later contribution + res = res + .filter( + (contribution) => contribution.cycleContribution >= poolStartCycle - 1 + ) + .sort((a, b) => (a.blockContribution > b.blockContribution ? 1 : -1)); + let currentCycle = poolStartCycle; + for (let contribution of res) { + if ( + !contribution.isContribution || + contribution.cycleContribution >= cycle + ) { + continue; + } + currentCycle = contribution.cycleContribution + 1; + for (currentCycle; currentCycle <= cycle; currentCycle += 1) { + const totalBtcContributedLastCycle = getCycleContributions( + currentCycle - 1 + ); //X + const { endBlock } = getCycleBlocks(currentCycle - 1); + const totalBtcAtEndOfLastCycle = getBalanceAtBlock(endBlock); // Y + const totalBtcRemainingInPool = + totalBtcAtEndOfLastCycle - totalBtcContributedLastCycle; // Z + if (contribution.transactionHash in cache) { + cache[contribution.transactionHash] = + (cache[contribution.transactionHash] * totalBtcRemainingInPool) / + totalBtcAtEndOfLastCycle; + } else { + cache[contribution.transactionHash] = + contribution.contribution / totalBtcAtEndOfLastCycle; + } + } + contribution.rewardPercentage = + cache[contribution.transactionHash].toFixed(4) * 100; + } + + res = res + .slice() + .sort((a, b) => (a.blockContribution > b.blockContribution ? 1 : -1)); + console.log(res); + setLocalPoolContributorInfo(res); + + const { startBlock } = getCycleBlocks(poolStartCycle - 1); + + res = res.filter( + (contribution) => + contribution.blockContribution >= startBlock && + contribution.blockContribution <= endBlock && + contribution.isContribution + ); + console.log(res.slice()); + + return { data: res, success: true }; + }; + + return { + poolContributerInfoState, + queryPoolContributerInfo, + }; +}; diff --git a/src/pages/publicData/locales/en-US.ts b/src/pages/publicData/locales/en-US.ts index 35aaa92..b36daaa 100644 --- a/src/pages/publicData/locales/en-US.ts +++ b/src/pages/publicData/locales/en-US.ts @@ -18,6 +18,11 @@ export default { 'block.info.status.pending': 'Pending', 'block.info.feeRate': 'Fee Rate', 'block.info.txType': 'TX Type', - - + 'pool.address': "Address", + 'pool.stxAddress': "STX Address", + 'pool.contribution': "Contribution", + 'pool.title': "Pool Contributors", + 'pool.transaction': "Transaction", + 'pool.cycleContribution': "Cycle", + 'pool.blockContribution': "Block", }; \ No newline at end of file diff --git a/src/pages/publicData/locales/zh-CN.ts b/src/pages/publicData/locales/zh-CN.ts index bb8327b..8279052 100644 --- a/src/pages/publicData/locales/zh-CN.ts +++ b/src/pages/publicData/locales/zh-CN.ts @@ -18,7 +18,12 @@ export default { 'block.info.status.pending': '待处理', 'block.info.feeRate': '交易费率', 'block.info.txType': '交易类型', - - - + // TODO + 'pool.address': "", + 'pool.stxAddress': "", + 'pool.contribution': "", + 'pool.title': "", + 'pool.transaction': "", + 'pool.cycleContribution': "", + 'pool.blockContribution': "", }; \ No newline at end of file diff --git a/src/pages/sysConf/index.tsx b/src/pages/sysConf/index.tsx index dad2487..f3ccfc9 100644 --- a/src/pages/sysConf/index.tsx +++ b/src/pages/sysConf/index.tsx @@ -187,7 +187,7 @@ const TableList: React.FC<{}> = () => { > {showMessage('这将清空你所有的账户信息,请慎重!' - , 'Attention plaese! This operation will clear all your account info.')} + , 'Attention please! This operation will clear all your account info.')} diff --git a/src/services/constants.ts b/src/services/constants.ts index e8453fc..8036d59 100644 --- a/src/services/constants.ts +++ b/src/services/constants.ts @@ -12,6 +12,7 @@ module.exports = { //Bitcoin endpoint bitcoinTestnet3: 'https://api.blockcypher.com/v1/btc/test3', bitcoinMainnet: 'https://blockchain.info', + bitcoinMainnet2: 'https://api.blockcypher.com/v1/btc/main', explorerURL: 'https://testnet-explorer.blockstack.org', binanceAPIURL: 'https://api.binance.com/api/v3', @@ -46,4 +47,11 @@ module.exports = { // address type btcType: 1, stxType: 2, + + // pooling + firstStackingBlock: 668050, + + btcBalanceCoef: 100000000, + stxBalanceCoef: 1000000 + } diff --git a/src/services/managePool/data.d.ts b/src/services/managePool/data.d.ts new file mode 100644 index 0000000..e3fbdc0 --- /dev/null +++ b/src/services/managePool/data.d.ts @@ -0,0 +1,144 @@ +// https://www.blockcypher.com/dev/bitcoin/#txref +export interface TXRef { + address?: string; + block_height: number; + tx_hash: string; + tx_input_n: number; + tx_output_n: number; + value: number; + prference: string; + spent: boolean; + double_spend: boolean; + confirmations: boolean; + script?: string; + ref_balance?: number; + confidence?: number; + confirmed?: time; + spent_by?: string; + received?: time; + receive_count?: number; + double_of?: string; +} + +export interface TxInput { + prev_hash: string; + output_index: number; + output_value: number; + script_type: string; + script: string; + addresses: string[]; + sequence: number; + age?: number; + wallet_name?: string; + wallet_token?: string; +} + +export interface TxOutput { + value: number; + script: string; + addresses: string[]; + script_type?: string; + data_hex?: string; + data_string?: string; +} + +export interface Tx { + block_height: number; + hash: string; + addresses: string[]; + total: number; + fees: number; + size: number; + vsize: number; + preference: string; + relayed_by: string; + received: string; + ver: number; + lock_time: number; + double_spend: boolean; + vin_sz: number; + vout_sz: number; + confirmations: number; + inputs: TxInput[]; + outputs: TxOutput[]; + opt_in_rbf?: boolean; + confidence?: number; + confirmed?: string; + receive_count?: number; + change_address?: string; + block_hash?: string; + block_index?: number; + double_of?: string; + data_protocol?: string; + hex?: string; + next_inputs?: string; + next_outputs?: string; +} + +export interface Wallet { + token: string; + name: string; + addresses: string[]; +} + +export interface Address { + address?: string; + wallet?: Wallet; + total_received: number; + total_sent: number; + balance: number; + unconfirmed_balance: number; + final_balance: number; + n_tx: number; + unconfirmed_n_tx: number; + final_n_tx: number; + tx_url?: string; + txrefs?: TxRef[]; + txs: Tx[]; + unconfirmed_txrefs?: Txref[]; + hasMore?: boolean; +} +export interface PoolContributerInfo { + address: string; + stxAddress: string; + contribution: number; + transactionHash: string; + cycleContribution: number; + blockContribution: number; + isContribution: boolean; + rewardPercentage: number; +} + +export interface StxBalance { + balance: string; + total_sent: string; + total_received: string; + total_fees_sent: string; + total_miner_rewards_received: string; + lock_tx_id: string; + locked: string; + lock_height: integer; + burnchain_lock_height: integer; + burnchain_unlock_height: integer; +} + +export interface StxBalances { + stx: StxBalance; +} + +export interface BtcInfo { + name: string; + height: number; + hash: string; + time: string; + latest_url: string; + previous_hash: string; + previous_url: string; + peer_count: number; + high_fee_per_kb: number; + medium_fee_per_kb: number; + low_fee_per_kb: number; + unconfirmed_count: number; + last_fork_height?: number; + last_fork_hash?: string; +} diff --git a/src/services/managePool/managePool.ts b/src/services/managePool/managePool.ts new file mode 100644 index 0000000..ea86a00 --- /dev/null +++ b/src/services/managePool/managePool.ts @@ -0,0 +1,276 @@ +import { getNetworkFromStorage } from "@/utils/utils"; +import request from "umi-request"; +import { Address, Tx, PoolContributerInfo, BtcInfo } from "./data"; +import { message } from "antd"; +const { + sidecarURLXenon, + sidecarURLMainnet, + bitcoinTestnet3, + bitcoinMainnet2, + firstStackingBlock, + btcBalanceCoef, +} = require("@/services/constants"); + +export interface PoolContributerInfoState { + poolContributerInfoList: PoolContributerInfo[]; +} + +// used for saving to local storage. btcAddress => PoolContributorInfo[] +export interface LocalPoolContributors { + [key: string]: PoolContributerInfo[]; +} + +export interface LocalPoolBalances { + [key: string]: number; +} + +export async function getCurrentCycle(): Promise<{ cycle: number }> { + let baseURL = sidecarURLXenon; + + switch (getNetworkFromStorage()) { + case "Xenon": + baseURL = bitcoinTestnet3; + break; + case "Mainnet": + baseURL = bitcoinMainnet2; + break; + default: + break; + } + + return request(`${baseURL}`, { method: "GET", timeout: 6000 }).then( + (resp) => { + let height: number = resp.height; + return { cycle: Math.ceil((height - firstStackingBlock) / 2100) }; + } + ); +} + +export function getCycleBlocks(cycle: number): { + startBlock: number; + endBlock: number; +} { + return { + startBlock: firstStackingBlock + (cycle - 1) * 2100, + endBlock: firstStackingBlock + cycle * 2100, + }; +} + +export function getPoolStartCycleBlocks(): { + startBlock: number; + endBlock: number; +} { + let poolStartCycle = localStorage.getItem("poolStartCycle"); + if (poolStartCycle) { + return getCycleBlocks(parseInt(poolStartCycle)); + } else { + return { + startBlock: firstStackingBlock, + endBlock: firstStackingBlock + 2100, + }; + } +} + +export function getCycleForBlock(blockHeight: number): number { + return Math.floor((blockHeight - firstStackingBlock) / 2100) + 1; +} + +// used to get pool balance from local storage +// cached so you don't have to requery +export const getLocalPoolBalance = (): number => { + let poolBalances = localStorage.getItem("poolBalances"); + let pooledBtcAddress = localStorage.getItem("pooledBtcAddress")!; + + if (poolBalances) { + let poolBalancesMap: LocalPoolBalances = JSON.parse(poolBalances); + if (pooledBtcAddress in poolBalancesMap) { + return poolBalancesMap[pooledBtcAddress]; + } + } + return 0; +}; + +// used to set pool balance from in local storage +// cached so you don't have to requery + +export const setLocalPoolBalances = (balance: number) => { + let poolBalances = localStorage.getItem("poolBalances"); + let pooledBtcAddress = localStorage.getItem("pooledBtcAddress")!; + let poolBalancesMap = {}; + if (poolBalances && pooledBtcAddress) { + poolBalancesMap = JSON.parse(poolBalances); + } + if (pooledBtcAddress) { + poolBalancesMap[pooledBtcAddress] = balance; + localStorage.setItem("poolBalances", JSON.stringify(poolBalancesMap)); + } +}; + +// used to get pool contributer info from local storage +// cached so you don't have to requery tx +export const getLocalPoolContributorInfo = (): PoolContributerInfo[] => { + let poolContributors = localStorage.getItem("poolContributors"); + let pooledBtcAddress = localStorage.getItem("pooledBtcAddress")!; + + if (poolContributors) { + let poolContributorsMap: LocalPoolContributors = + JSON.parse(poolContributors); + if (pooledBtcAddress in poolContributorsMap) { + return poolContributorsMap[pooledBtcAddress]; + } + } + return []; +}; + +// used to set pool contributor info in local storage +// cached so you don't have to requery tx +export const setLocalPoolContributorInfo = ( + contributions: PoolContributerInfo[] +) => { + let poolContributors = localStorage.getItem("poolContributors"); + let pooledBtcAddress = localStorage.getItem("pooledBtcAddress")!; + let poolContributorsMap = {}; + if (poolContributors && pooledBtcAddress) { + poolContributorsMap = JSON.parse(poolContributors); + } + if (pooledBtcAddress) { + poolContributorsMap[pooledBtcAddress] = contributions.sort((a, b) => + a.blockContribution < b.blockContribution ? 1 : -1 + ); + localStorage.setItem( + "poolContributors", + JSON.stringify(poolContributorsMap) + ); + } +}; + +// gets total BTC in the pool at a block +export function getBalanceAtBlock(blockHeight: number): number { + let balance = getLocalPoolBalance(); + let transactions = getLocalPoolContributorInfo(); + let index = 0; + let transaction = transactions[index]; + while ( + index < transactions.length && + transaction.blockContribution >= blockHeight + ) { + balance -= transaction.contribution; + index += 1; + transaction = transactions[index]; + } + return balance; +} + +export function getCycleContributions(cycle: number): number { + let transactions = getLocalPoolContributorInfo(); + const { startBlock, endBlock } = getCycleBlocks(cycle); + let totalContributions = 0; + for (const transaction of transactions) { + if ( + startBlock <= transaction.blockContribution && + transaction.blockContribution <= endBlock && + transaction.isContribution + ) { + totalContributions += transaction.contribution; + } + } + return totalContributions; +} + +export async function getBtcHeight(): Promise { + let baseURL = sidecarURLXenon; + + switch (getNetworkFromStorage()) { + case "Xenon": { + baseURL = `${bitcoinTestnet3}`; + break; + } + case "Mainnet": { + baseURL = `${bitcoinMainnet2}`; + break; + } + default: + break; + } + return request(`${baseURL}`, { method: "GET", timeout: 6000 }).then( + (resp: BtcInfo) => { + console.log(resp); + return resp.height; + } + ); +} + +// gets pool contributors between blocks +export async function getPoolContributorsHelper( + startBlock: number, + endBlock: number +): Promise
{ + let baseURL = sidecarURLXenon; + let pooledBtcAddress = localStorage.getItem("pooledBtcAddress")!; + // stx mainnet miner + + switch (getNetworkFromStorage()) { + case "Xenon": { + //TODO: remember to add before and after + // baseURL = `${bitcoinTestnet3}/addrs/${pooledBtcAddress}?before=${endBlock}&after=${startBlock}&limit=2000`; + baseURL = `${bitcoinTestnet3}/addrs/${pooledBtcAddress}/full?limit=50&before=${endBlock}&after=${startBlock}&confidence=99&token=18b85235d6544c67932698c98ed17317`; + // baseURL = `${bitcoinTestnet3}/addrs/${pooledBtcAddress}/full?limit=50`; + break; + } + case "Mainnet": { + baseURL = `${bitcoinMainnet2}/addrs/${pooledBtcAddress}/full?limit=50&before=${endBlock}&after=${startBlock}`; + break; + } + default: + break; + } + console.log(baseURL); + return request(`${baseURL}`, { method: "GET", timeout: 6000 }).then( + (resp: Address) => { + console.log(resp); + return resp; + } + ); +} + +export async function getPoolContributors( + startBlock: number, + endBlock: number +): Promise<{ transactions: Tx[]; balance: number }> { + let hasMore = true; + let transactions: Tx[] = []; + let balance = -1; + + // TODO: if you get rate limited halfway through, you'll have the most recent + // transactions cached but you'll be missing the transactions starting + // from startBlock. Then when you try again later, it'll only query from + // the highest cached transaction to end block, so you'll still be missing the + // middle transactions. either find a way to fix this, or return an empty array + // if any error occurs + while (hasMore) { + try { + const addressResult = await getPoolContributorsHelper( + startBlock, + endBlock + ); + + if (balance == -1) { + balance = addressResult.balance; + } + + if (addressResult.txs.length > 0) { + const [lastTx] = addressResult.txs.slice(-1); + endBlock = lastTx.block_height; + transactions = [...transactions, ...addressResult.txs]; + } + + hasMore = addressResult.hasMore!; + } catch (error) { + console.log(error); + message.error("Failed to get newest pool contributors"); + hasMore = false; + } + } + + return { transactions, balance }; +} diff --git a/src/services/sysConf/conf.ts b/src/services/sysConf/conf.ts index 73afc56..44dc707 100644 --- a/src/services/sysConf/conf.ts +++ b/src/services/sysConf/conf.ts @@ -115,6 +115,7 @@ export async function getNodeInfo() { } export function resetLockPassword() { + //TODO: reset pooling stuff localStorage.removeItem(MiningPasswordAuthorization); localStorage.removeItem('BTC'); localStorage.removeItem('STX'); diff --git a/src/services/wallet/account.ts b/src/services/wallet/account.ts index 5901490..1153df9 100644 --- a/src/services/wallet/account.ts +++ b/src/services/wallet/account.ts @@ -6,7 +6,7 @@ import { message } from 'antd'; import { aes256Encrypt, keyGen } from '@/utils/utils'; import { showMessage } from "@/services/locale"; -const { btcType, stxType } = require('@/services/constants'); +const { btcType, stxType, btcBalanceCoef } = require('@/services/constants'); const { sidecarURLXenon, sidecarURLMainnet, bitcoinTestnet3, bitcoinMainnet } = require('@/services/constants') @@ -53,40 +53,41 @@ export async function getStxBalance(stxAddress: string) { export async function getBtcBalance(btcAddress: string) { let baseURL = sidecarURLXenon; - let balanceCoef = 1; // https://api.blockcypher.com/v1/btc/test3/addrs/mzYBtAjNzuEvEMAp2ahx8oT9kWWvb5L2Rj/balance switch (getNetworkFromStorage()) { //{"balance":0} case "Xenon": { baseURL = `${bitcoinTestnet3}/addrs/${btcAddress}/balance` //`${sidecarURLXenon}/v1/faucets/btc/${btcAddress}`; - balanceCoef = 100000000 return request(`${baseURL}`, { method: "GET", timeout: 6000, }).then((resp) => { - return { 'balance': (resp.final_balance / balanceCoef).toString() }; + return { 'balance': (resp.final_balance / btcBalanceCoef).toString() }; }).catch(err => { console.log(err) + message.error("Failed to get Btc balance"); return { 'balance': 'NaN' }; }); } case "Mainnet": { baseURL = `${bitcoinMainnet}/balance?active=${btcAddress}&cors=true`; //`${sidecarURLXenon}/v1/faucets/btc/${btcAddress}`; - balanceCoef = 100000000 return request(`${baseURL}`, { method: "GET", timeout: 10000, }).then((resp) => { - return { 'balance': (resp[`${btcAddress}`].final_balance / balanceCoef).toString() }; + return { 'balance': (resp[`${btcAddress}`].final_balance / btcBalanceCoef).toString() }; }).catch(err => { console.log(err) + message.error("Failed to get Btc balance"); return { 'balance': 'NaN' }; }); } - default: break; + default: { + return {'balance': 'NaN'} + }; } diff --git a/src/utils/utils.ts b/src/utils/utils.ts index f4a028c..ed401d9 100644 --- a/src/utils/utils.ts +++ b/src/utils/utils.ts @@ -138,4 +138,9 @@ export function aes256Decrypt(data: string, key: Buffer, ivStr: string, authTagS export function getNetworkFromStorage() { let network = localStorage.getItem('network') return (network === null ? 'Xenon' : network) +} + +export function isPooling(): boolean { + return localStorage.getItem("isPooling") !== "true" || + !localStorage.getItem("pooledBtcAddress") } \ No newline at end of file diff --git a/tests/testPoolBalances.json b/tests/testPoolBalances.json new file mode 100644 index 0000000..57527b2 --- /dev/null +++ b/tests/testPoolBalances.json @@ -0,0 +1 @@ +{ "1NG4ZgJaGrvDLuyxgdjVWaeaZbvnS8EhQn": 2 } diff --git a/tests/testPoolContributors.json b/tests/testPoolContributors.json new file mode 100644 index 0000000..1b82f61 --- /dev/null +++ b/tests/testPoolContributors.json @@ -0,0 +1,74 @@ +{ + "1NG4ZgJaGrvDLuyxgdjVWaeaZbvnS8EhQn": [ + { + "address": "output", + "stxAddress": "output", + "contribution": -0.1, + "transactionHash": "6666666666666666666666666666666666666666666666666666666666666666", + "cycleContribution": 12, + "blockContribution": 691162, + "isContribution": false, + "rewardPercentage": 0 + }, + { + "address": "39jfi6BLYyTvBCFBKGF1bpEgixJgV7mVaM", + "stxAddress": "SM1C42R9QVG4B9VW5F1WGCF7QDQ8ZXBKS53ZZ0T0N", + "contribution": 0.4, + "transactionHash": "5555555555555555555555555555555555555555555555555555555555555555", + "cycleContribution": 12, + "blockContribution": 691155, + "isContribution": true, + "rewardPercentage": 0 + }, + { + "address": "output", + "stxAddress": "output", + "contribution": -0.1, + "transactionHash": "4444444444444444444444444444444444444444444444444444444444444444", + "cycleContribution": 11, + "blockContribution": 689051, + "isContribution": false, + "rewardPercentage": 0 + }, + { + "address": "38noJXruF8E5mNFL4q6FHrEVbnHwGu6sQn", + "stxAddress": "SM16Y1NT3WFQK1KB1VNJKB2MF2C1AYTSRH6PTMA5A", + "contribution": 0.9, + "transactionHash": "3333333333333333333333333333333333333333333333333333333333333333", + "cycleContribution": 10, + "blockContribution": 689048, + "isContribution": true, + "rewardPercentage": 0 + }, + { + "address": "output", + "stxAddress": "output", + "contribution": -0.1, + "transactionHash": "2222222222222222222222222222222222222222222222222222222222222222", + "cycleContribution": 10, + "blockContribution": 689040, + "isContribution": false, + "rewardPercentage": 0 + }, + { + "address": "38noJXruF8E5mNFL4q6FHrEVbnHwGu6sQn", + "stxAddress": "SM16Y1NT3WFQK1KB1VNJKB2MF2C1AYTSRH6PTMA5A", + "contribution": 0.9, + "transactionHash": "1111111111111111111111111111111111111111111111111111111111111111", + "cycleContribution": 9, + "blockContribution": 686910, + "isContribution": true, + "rewardPercentage": 0 + }, + { + "address": "39jfi6BLYyTvBCFBKGF1bpEgixJgV7mVaM", + "stxAddress": "SM1C42R9QVG4B9VW5F1WGCF7QDQ8ZXBKS53ZZ0T0N", + "contribution": 0.1, + "transactionHash": "0000000000000000000000000000000000000000000000000000000000000000", + "cycleContribution": 9, + "blockContribution": 686900, + "isContribution": true, + "rewardPercentage": 0 + } + ] +}