diff --git a/.gitignore b/.gitignore index e1e8fc6b9ec..003a30116a8 100644 --- a/.gitignore +++ b/.gitignore @@ -4,7 +4,6 @@ npm-debug.log* .DS_Store .idea cache - coverage node_modules build @@ -13,8 +12,7 @@ public/static .env.development.local .env.test.local .env.production.local - -src/config.ts .vscode - dev.sh +*~ +*.marks diff --git a/README.md b/README.md index 4adf6cc61ab..41fa67267bc 100644 --- a/README.md +++ b/README.md @@ -58,6 +58,9 @@ Feel free to test it out and submit improvements and pull requests. When setting up another service like Ecency with Ecency-vision software: +- `HIVESIGNER_ID` - iff USE_PRIVATE is 0, set this to what account will handle the permissions for posting level operations. +- `HIVESIGNER_SECRET` - iff USE_PRIVATE is 0, set this to the "secret" field value in the [Hive Signer profile](https://hivesigner.com/profile) for the user named as your `HIVESIGNER_ID`. This should be a lengthy lowercase hex string. + 1. You may leave `HIVESIGNER_ID` and `HIVESIGNER_SECRET` environment variables unset and optionally set USE_PRIVATE=1 and leave "base" in the constants/defaults.json set to "https://ecency.com". Your new site will contain more features as it will use Ecency's private API. This is by far the easiest option. 2. You may change `base` to the URL of your own site, but you will have to set environment variables `HIVESIGNER_ID` and `HIVESIGNER_SECRET`; set USE_PRIVATE=0 as well as configure your the `HIVESIGNER_ID` account at the [Hivesigner website.](https://hivesigner.com/profile). Hivesigner will need a `secret`, in the form of a long lowercase hexadecimal number. The HIVESIGNER_SECRET should be set to this value. @@ -65,9 +68,11 @@ When setting up another service like Ecency with Ecency-vision software: In order to validate a login, and do posting level operations, this software relies on Hivesigner. A user @alice will use login credentials to login to the site via one of several methods, but the site will communicate with Hivesigner and ask it to do all posting operations on behalf of @alice. Hivesigner can and will do this because both @alice will have given posting authority to the `HIVESIGNER_ID` user and the `HIVESIGNER_ID` user will have given its posting authority to Hivesigner. -##### Edit "default" values +Also for URLs other than https://ecency.com\_... If you are setting up your own website other than Ecency.com, you can still leave the value `base` as "https://ecency.com". However, you should change `name`, `title` and `twitterHandle`. There are also a lot of static pages that are Ecency specific. +The 'appURL' member should be the URL of the front end you are running. If you are running this testing, it should be "http://localhost" so hiveSigner redirects you back to localhost. The default is 'https://ecency.com'. +The testnet member should be set to false, unless you want to use a Hive testnet ##### Start website in dev diff --git a/public/firebase-messaging-sw.js b/public/firebase-messaging-sw.js index 6e1024ce492..1aca7680db3 100644 --- a/public/firebase-messaging-sw.js +++ b/public/firebase-messaging-sw.js @@ -1,17 +1,17 @@ // Scripts for firebase and firebase messaging -importScripts('https://www.gstatic.com/firebasejs/8.2.0/firebase-app.js'); -importScripts('https://www.gstatic.com/firebasejs/8.2.0/firebase-messaging.js'); +importScripts("https://www.gstatic.com/firebasejs/8.2.0/firebase-app.js"); +importScripts("https://www.gstatic.com/firebasejs/8.2.0/firebase-messaging.js"); // Initialize the Firebase app in the service worker by passing the generated config var firebaseConfig = { - apiKey: 'AIzaSyDKF-JWDMmUs5ozjK7ZdgG4beHRsAMd2Yw', - authDomain: 'esteem-ded08.firebaseapp.com', - databaseURL: 'https://esteem-ded08.firebaseio.com', - projectId: 'esteem-ded08', - storageBucket: 'esteem-ded08.appspot.com', - messagingSenderId: '211285790917', - appId: '1:211285790917:web:c259d25ed1834c683760ac', - measurementId: 'G-TYQD1N3NR3' + apiKey: "AIzaSyDKF-JWDMmUs5ozjK7ZdgG4beHRsAMd2Yw", + authDomain: "esteem-ded08.firebaseapp.com", + databaseURL: "https://esteem-ded08.firebaseio.com", + projectId: "esteem-ded08", + storageBucket: "esteem-ded08.appspot.com", + messagingSenderId: "211285790917", + appId: "1:211285790917:web:c259d25ed1834c683760ac", + measurementId: "G-TYQD1N3NR3" }; firebase.initializeApp(firebaseConfig); @@ -21,28 +21,28 @@ const messaging = firebase.messaging(); messaging.onBackgroundMessage(function (payload) { //console.log('Received bg notification', payload); - const notificationTitle = payload.notification?.title || 'Ecency'; + const notificationTitle = payload.notification?.title || "Ecency"; self.registration.showNotification(notificationTitle, { body: payload.notification?.body, - icon: payload.notification?.image || 'https://ecency.com/static/media/logo-circle.2df6f251.svg', - data: payload.data, + icon: payload.notification?.image || "https://ecency.com/static/media/logo-circle.2df6f251.svg", + data: payload.data }); }); -self.addEventListener('notificationclick', function (event) { +self.addEventListener("notificationclick", function (event) { const data = event.notification.data; - let url = 'https://ecency.com'; + let url = "https://ecency.com"; const fullPermlink = data.permlink1 + data.permlink2 + data.permlink3; - if (['vote', 'unvote', 'spin', 'inactive'].includes(data.type)) { - url += '/@' + data.target; + if (["vote", "unvote", "spin", "inactive"].includes(data.type)) { + url += "/@" + data.target; } else { // delegation, mention, transfer, follow, unfollow, ignore, blacklist, reblog - url += '/@' + data.source; + url += "/@" + data.source; } if (fullPermlink) { - url += '/' + fullPermlink; + url += "/" + fullPermlink; } - clients.openWindow(url, '_blank'); -}); \ No newline at end of file + clients.openWindow(url, "_blank"); +}); diff --git a/razzle.config.js b/razzle.config.js index 40b45899785..8241ce73095 100644 --- a/razzle.config.js +++ b/razzle.config.js @@ -1,26 +1,26 @@ -'use strict'; -const LoadableWebpackPlugin = require('@loadable/webpack-plugin'); -const { loadableTransformer } = require('loadable-ts-transformer'); -const path = require('path'); +"use strict"; +const LoadableWebpackPlugin = require("@loadable/webpack-plugin"); +const { loadableTransformer } = require("loadable-ts-transformer"); +const path = require("path"); module.exports = { - plugins: ['typescript', 'scss'], + plugins: ["typescript", "scss"], options: { - buildType: 'iso' + buildType: "iso" }, modifyWebpackConfig({ env: { target, // the target 'node' or 'web' - dev, // is this a development build? true or false + dev // is this a development build? true or false }, webpackConfig, // the created webpack config webpackObject, // the imported webpack node module options: { pluginOptions, // the options passed to the plugin ({ name:'pluginname', options: { key: 'value'}}) razzleOptions, // the modified options passed to Razzle in the `options` key in `razzle.config.js` (options: { key: 'value'}) - webpackOptions, // the modified options that was used to configure webpack/ webpack loaders and plugins + webpackOptions // the modified options that was used to configure webpack/ webpack loaders and plugins }, - paths, // the modified paths that will be used by Razzle. + paths // the modified paths that will be used by Razzle. }) { // Do some stuff to webpackConfig if (target === "web") { @@ -31,16 +31,18 @@ module.exports = { webpackConfig.plugins.push( new LoadableWebpackPlugin({ outputAsset: true, - writeToDisk: { filename }, + writeToDisk: { filename } }) ); } // Enable SSR lazy-loading - const tsLoader = webpackConfig.module.rules.find(rule => !(rule.test instanceof Array) && rule.test && rule.test.test('.tsx')); + const tsLoader = webpackConfig.module.rules.find( + (rule) => !(rule.test instanceof Array) && rule.test && rule.test.test(".tsx") + ); tsLoader.use[0].options.getCustomTransformers = () => ({ before: [loadableTransformer] }); - webpackConfig.devtool = dev ? 'source-map' : false; + webpackConfig.devtool = dev ? "source-map" : false; return webpackConfig; } }; diff --git a/src/common/api/hive-engine.ts b/src/common/api/hive-engine.ts index 3d367a337b0..9204044eb8c 100644 --- a/src/common/api/hive-engine.ts +++ b/src/common/api/hive-engine.ts @@ -43,8 +43,47 @@ export interface TokenStatus { precision: number; } +export interface Unstake { + _id: number; + account: string; + symbol: string; + quantity: string; + quantityLeft: string; + nextTransactionTimestamp: number; + numberTransactionsLeft: string; + millisecPerPeriod: string; + txID: string; +} + +import { HECoarseTransaction, HEFineTransaction } from "../store/transactions/types"; + const HIVE_ENGINE_RPC_URL = engine.engineRpcUrl; +export const getPendingUnstakes = (account: string, tokenName: string): Promise> => { + const data = { + jsonrpc: "2.0", + method: "find", + params: { + contract: "tokens", + table: "pendingUnstakes", + query: { + account: account, + token: tokenName + } + }, + id: 1 + }; + + return axios + .post(HIVE_ENGINE_RPC_URL, data, { + headers: { "Content-type": "application/json" } + }) + .then((r) => r.data.result) + .catch((e) => { + return []; + }); +}; + export const getTokenBalances = (account: string): Promise => { const data = { jsonrpc: "2.0", @@ -93,6 +132,22 @@ const getTokens = (tokens: string[]): Promise => { }); }; +export const getHiveEngineTokenBalance = async ( + account: string, + tokenName: string +): Promise => { + // commented just to try removing the non-existing unknowing HiveEngineTokenBalance type + // ): Promise => { + let balances = await getTokenBalances(account); + const tokens = await getTokens([tokenName]); + + const balance = balances.find((balance) => balance.symbol == tokenName); + const token = tokens[0]; + const tokenMetadata = token && (JSON.parse(token!.metadata) as TokenMetadata); + + return new HiveEngineToken({ ...balance, ...token, ...tokenMetadata } as any); +}; + export const getHiveEngineTokenBalances = async (account: string): Promise => { // commented just to try removing the non-existing unknowing HiveEngineTokenBalance type // ): Promise => { @@ -151,6 +206,83 @@ export const stakeTokens = async ( return broadcastPostingJSON(account, "ssc-mainnet-hive", json); }; +export interface DelegationEntry { + _id: number; + from: string; + to: string; + symbol: string; + quantity: string; +} + +export async function getTokenDelegations(account: string): Promise> { + const data = { + jsonrpc: "2.0", + method: "find", + params: { + contract: "tokens", + table: "delegations", + query: { + $or: [{ from: account }, { to: account }] + } + }, + id: 3 + }; + return axios + .post(HIVE_ENGINE_RPC_URL, data, { + headers: { "Content-type": "application/json" } + }) + .then((r) => { + const list: Array = r.data.result; + return list; + }) + .catch((e) => { + console.log(e.message); + return []; + }); +} + +// Exclude author and curation reward details +export async function getCoarseTransactions( + account: string, + limit: number, + symbol: string, + offset: number = 0 +) { + const response = await axios({ + url: "https://accounts.hive-engine.com/accountHistory", + method: "GET", + params: { + account, + limit, + offset, + type: "user", + symbol + } + }); + return response.data; +} + +// Include virtual transactions like curation and author reward details. +export async function getFineTransactions( + symbol: string, + account: string, + limit: number, + offset: number +): Promise> { + return axios({ + url: `https://scot-api.hive-engine.com/get_account_history`, + method: "GET", + params: { + account, + token: symbol, + limit, + offset + } + }).then((response) => { + return response.data; + }); +} + export const getMetrics: any = async (symbol?: any, account?: any) => { const data = { jsonrpc: "2.0", @@ -166,11 +298,6 @@ export const getMetrics: any = async (symbol?: any, account?: any) => { id: 1 }; - // const result = await axios - // .post(HIVE_ENGINE_RPC_URL, data, { - // headers: { "Content-type": "application/json" } - // }) - // return result; return axios .post(HIVE_ENGINE_RPC_URL, data, { headers: { "Content-type": "application/json" } diff --git a/src/common/api/hive.ts b/src/common/api/hive.ts index 4267f94a1f9..7f81b8eaad5 100644 --- a/src/common/api/hive.ts +++ b/src/common/api/hive.ts @@ -1,4 +1,5 @@ import { Client, RCAPI, utils } from "@hiveio/dhive"; +import { DEFAULT_CHAIN_ID, DEFAULT_ADDRESS_PREFIX } from "@hiveio/dhive"; import { RCAccount } from "@hiveio/dhive/lib/chain/rc"; @@ -10,16 +11,29 @@ import parseAsset from "../helper/parse-asset"; import { vestsToRshares } from "../helper/vesting"; import isCommunity from "../helper/is-community"; -import SERVERS from "../constants/servers.json"; +import MAINNET_SERVERS from "../constants/servers.json"; import { dataLimit } from "./bridge"; import moment from "moment"; -export const client = new Client(SERVERS, { - timeout: 3000, - failoverThreshold: 3, - consoleOnFailover: true +export const CHAIN_ID = DEFAULT_CHAIN_ID.toString("hex"); +export const ADDRESS_PREFIX = DEFAULT_ADDRESS_PREFIX; +export const HIVE_API_NAME = "HIVE"; +export const DOLLAR_API_NAME = "HBD"; +export const HIVE_LANGUAGE_KEY = HIVE_API_NAME.toLowerCase(); +export const HIVE_HUMAN_NAME = "Hive"; +export const HIVE_HUMAN_NAME_UPPERCASE = "HIVE"; +export const DOLLAR_HUMAN_NAME = DOLLAR_API_NAME; +export const client = new Client(MAINNET_SERVERS, { + timeout: 4000, + failoverThreshold: 10, + consoleOnFailover: true, + addressPrefix: ADDRESS_PREFIX, + chainId: CHAIN_ID }); +export const HIVE_COLLATERALIZED_CONVERSION_FEE = 0.05; +export const HIVE_CONVERSION_COLLATERAL_RATIO = 2; + export interface Vote { percent: number; reputation: number; @@ -42,10 +56,7 @@ export interface DynamicGlobalProperties { } export interface FeedHistory { - current_median_history: { - base: string; - quote: string; - }; + current_median_history: Price; } export interface RewardFund { @@ -544,3 +555,89 @@ export interface BlogEntry { export const getBlogEntries = (username: string, limit: number = dataLimit): Promise => client.call("condenser_api", "get_blog_entries", [username, 0, limit]); + +export interface Price { + base: string; + quote: string; +} +/* group number and string */ +const gnas = (a: string) => { + const d = a.split(" "); + try { + const t = { n: parseFloat(d[0].replace(/,/g, "")), s: d[1] }; + return t; + } catch (e) { + return { n: 0, s: "" }; + } +}; +/** Translated from hive/hive/libraries/protocol/include/hive/protocol + Applies price to given asset in order to calculate its value in the second asset (like operator* ). + Additionally applies fee scale factor to specific asset in price. Used f.e. to apply fee to + collateralized conversions. Fee scale parameter in basis points. + */ +export function multiply_with_fee( + a: string, + p: Price, + fee: number, + apply_fee_to: string +): string | undefined { + if (a.indexOf(" ") == -1) return undefined; + let a_quantity: number; + let a_symbol: string; + { + const d = gnas(a); + a_quantity = d.n; + a_symbol = d.s; + } + const is_negative: boolean = a_quantity < 0; + let result: number = is_negative ? -a_quantity : a_quantity; + let scale_b = 1; + let scale_q = 1; + const { n: price_base_amount, s: price_base_symbol } = gnas(p.base); + const { n: price_quote_amount, s: price_quote_symbol } = gnas(p.quote); + if (apply_fee_to == price_base_symbol) { + scale_b += fee; + } else { + if (!(apply_fee_to == price_quote_symbol)) { + throw new Error(`Invalid fee symbol ${apply_fee_to} for price ${p.base}/${p.quote}`); + } + scale_q += fee; + } + if (a_symbol == price_base_symbol) { + result = (result * price_quote_amount * scale_q) / (price_base_amount * scale_b); + return `${is_negative ? -result : result} ${price_quote_symbol}`; + } else { + console.log({ + result, + price_base_amount, + scale_b, + price_quote_amount, + scale_q + }); + if (a_symbol !== price_quote_symbol) + throw new Error(`invalid ${a} != ${price_quote_symbol} nor ${price_base_symbol}`); + result = (result * price_base_amount * scale_b) / (price_quote_amount * scale_q); + return `${is_negative ? -result : result} ${price_base_symbol}`; + } +} + +export const estimateRequiredHiveCollateral = async ( + hbd_amount_to_get: number +): Promise => { + const fhistory = await getFeedHistory(); + if (fhistory.current_median_history === null) + throw new Error("Cannot estimate conversion collateral because there is no price feed."); + const needed_hive = multiply_with_fee( + `${hbd_amount_to_get} ${DOLLAR_API_NAME}`, + fhistory.current_median_history, + HIVE_COLLATERALIZED_CONVERSION_FEE, + HIVE_API_NAME + ); + if (!needed_hive) { + console.log({ needed_hive }); + return -1; + } + const { n: needed_hive_quantity, s: needed_hive_symbol } = gnas(needed_hive); + const _amount = needed_hive_quantity * HIVE_CONVERSION_COLLATERAL_RATIO; + return _amount; +}; diff --git a/src/common/api/operations.ts b/src/common/api/operations.ts index 50052ee3c68..eeae31c802c 100644 --- a/src/common/api/operations.ts +++ b/src/common/api/operations.ts @@ -1,4 +1,7 @@ import hs from "hivesigner"; +// so far using browser only front end code to determine the redirect URL hasn't broken any tests +// import { appURL } from "../constants/defaults.json"; +import { HIVE_API_NAME } from "./hive"; import { AccountUpdateOperation, @@ -319,7 +322,56 @@ export const claimRewardBalance = ( return broadcastPostingOperations(username, opArray); }; - +export const claimRewardBalanceHiveEngineAssetJSON = ( + from: string, + to: string, + amount: string +): string => { + const [quantity, token_name] = amount.split(/ /); + const json = JSON.stringify({ + symbol: token_name + }); + return json; +}; +interface ClaimTokenParams { + id: "scot_claim_token"; + json: string; + required_auths: []; + required_posting_auths: [string]; +} +export const claimHiveEngineRewardBalance = (from: string, to: string, amount: string) => { + const params: ClaimTokenParams = { + id: "scot_claim_token", + json: claimRewardBalanceHiveEngineAssetJSON(from, to, amount), + required_auths: [], + required_posting_auths: [from] + }; + const opArray: Operation[] = [["custom_json", params]]; + return broadcastPostingOperations(from, opArray); +}; +/* +export const HECustomJSONWithPostingKey = (key: PrivateKey, from:string, json: string): Promise => { + const op = { + id: "ssc-mainnet-hive", + json, + required_auths: [], + required_posting_auths: [from] + }; + return hiveClient.broadcast.json(op, key); +} +export const HECustomJSONPostingKc = (from:string, json: string, description: string): Promise => { + return keychain.customJson(from, "ssc-mainnet-hive", "Posting", json, description); +} +export const HECustomJSONPostingHot = (from:string, json: string, destination: string) => { + const params = { + authority: "posting", + required_auths: `[]`, + required_posting_auths: `["${from}"]`, + id: "ssc-mainnet-hive", + json + } + hotSign("custom-json", params, destination); +}*/ export const transfer = ( from: string, key: PrivateKey, @@ -348,7 +400,9 @@ export const transferHot = (from: string, to: string, amount: string, memo: stri } ]; - const params: Parameters = { callback: `https://ecency.com/@${from}/wallet` }; + const params: Parameters = { + callback: `${document.location.protocol}//${document.location.host}/@${from}/wallet` + }; return hs.sendOperation(op, params, () => {}); }; @@ -440,7 +494,9 @@ export const transferToSavingsHot = (from: string, to: string, amount: string, m } ]; - const params: Parameters = { callback: `https://ecency.com/@${from}/wallet` }; + const params: Parameters = { + callback: `${document.location.protocol}//${document.location.host}/@${from}/wallet` + }; return hs.sendOperation(op, params, () => {}); }; @@ -454,7 +510,6 @@ export const transferToSavingsKc = (from: string, to: string, amount: string, me memo } ]; - return keychain.broadcast(from, [op], "Active"); }; @@ -540,7 +595,9 @@ export const limitOrderCreateHot = ( ]; const params: Parameters = { - callback: `https://ecency.com/market${idPrefix === OrderIdPrefix.SWAP ? "#swap" : ""}` + callback: `${document.location.protocol}//${document.location.host}/market${ + idPrefix === OrderIdPrefix.SWAP ? "#swap" : "" + }` }; return hs.sendOperation(op, params, () => {}); }; @@ -554,7 +611,9 @@ export const limitOrderCancelHot = (owner: string, orderid: number) => { } ]; - const params: Parameters = { callback: `https://ecency.com/market` }; + const params: Parameters = { + callback: `${document.location.protocol}//${document.location.host}/market` + }; return hs.sendOperation(op, params, () => {}); }; @@ -630,7 +689,9 @@ export const convertHot = (owner: string, amount: string) => { } ]; - const params: Parameters = { callback: `https://ecency.com/@${owner}/wallet` }; + const params: Parameters = { + callback: `${document.location.protocol}//${document.location.host}/@${owner}/wallet` + }; return hs.sendOperation(op, params, () => {}); }; @@ -647,6 +708,75 @@ export const convertKc = (owner: string, amount: string) => { return keychain.broadcast(owner, [op], "Active"); }; +const transferHiveEngineAssetJSON = ( + from: string, + to: string, + amount: string, + memo: string +): string => { + const [quantity, token_name] = amount.replace(/,/g, "").split(/ /); + const json = JSON.stringify({ + // is it always 'tokens'? + contractName: "tokens", + contractAction: "transfer", + contractPayload: { + symbol: token_name, + to: to, + quantity, + memo: memo + } + }); + return json; +}; + +export const transferHiveEngineAsset = ( + from: string, + key: PrivateKey, + to: string, + amount: string, + memo: string +): Promise => { + const json = transferHiveEngineAssetJSON(from, to, amount, memo); + const op = { + id: "ssc-mainnet-hive", + json, + required_auths: [from], + required_posting_auths: [] + }; + return hiveClient.broadcast.json(op, key); +}; +export const transferHiveEngineAssetKc = ( + from: string, + to: string, + amount: string, + memo: string +) => { + const json = transferHiveEngineAssetJSON(from, to, amount, memo); + return keychain.customJson( + from, + "ssc-mainnet-hive", + "Active", + json, + "Hive Engine Asset Transfer" + ); +}; +export const transferHiveEngineAssetHot = ( + from: string, + to: string, + amount: string, + memo: string +) => { + const json = transferHiveEngineAssetJSON(from, to, amount, memo); + const params = { + authority: "active", + required_auths: `["${from}"]`, + required_posting_auths: "[]", + id: "ssc-mainnet-hive", + json + }; + hotSign("custom-json", params, `@${from}/wallet`); +}; + export const transferFromSavings = ( from: string, key: PrivateKey, @@ -680,7 +810,9 @@ export const transferFromSavingsHot = (from: string, to: string, amount: string, } ]; - const params: Parameters = { callback: `https://ecency.com/@${from}/wallet` }; + const params: Parameters = { + callback: `${document.location.protocol}//${document.location.host}/@${from}/wallet` + }; return hs.sendOperation(op, params, () => {}); }; @@ -748,7 +880,9 @@ export const claimInterestHot = (from: string, to: string, amount: string, memo: } ]; - const params: Parameters = { callback: `https://ecency.com/@${from}/wallet` }; + const params: Parameters = { + callback: `${document.location.protocol}//${document.location.host}/@${from}/wallet` + }; return hs.sendOperations([op, cop], params, () => {}); }; @@ -774,67 +908,158 @@ export const claimInterestKc = (from: string, to: string, amount: string, memo: return keychain.broadcast(from, [op, cop], "Active"); }; - -export const transferToVesting = ( - from: string, +export const collateralizedConvert = ( + owner: string, key: PrivateKey, - to: string, amount: string ): Promise => { const op: Operation = [ - "transfer_to_vesting", + "collateralized_convert", { - from, - to, - amount + owner, + amount, + requestid: new Date().getTime() >>> 0 } ]; - return hiveClient.broadcast.sendOperations([op], key); }; - -export const transferToVestingHot = (from: string, to: string, amount: string) => { +export const collateralizedConvertHot = (owner: string, amount: string): void => { const op: Operation = [ - "transfer_to_vesting", + "collateralized_convert", { - from, - to, - amount + owner, + amount, + requestid: new Date().getTime() >>> 0 } ]; - - const params: Parameters = { callback: `https://ecency.com/@${from}/wallet` }; - return hs.sendOperation(op, params, () => {}); + const params: Parameters = { + callback: document.location.toString() + }; + hs.sendOperation(op, params, () => {}); }; - -export const transferToVestingKc = (from: string, to: string, amount: string) => { +export const collateralizedConvertKc = (owner: string, amount: string) => { const op: Operation = [ - "transfer_to_vesting", + "collateralized_convert", { - from, - to, - amount + owner, + amount, + requestid: new Date().getTime() >>> 0 } ]; + return keychain.broadcast(owner, [op], "Active"); +}; +export const createTransferToVestingOp = (from: string, to: string, amount: string): Operation => { + const parts = amount.split(/ /); + const currency = parts[parts.length - 1]; + const quantity = parts[0].replace(/,/g, ""); + console.log(from, to, amount); + if (currency === HIVE_API_NAME) { + return [ + "transfer_to_vesting", + { + from, + to, + amount + } + ]; + } else { + return [ + "custom_json", + { + id: "ssc-mainnet-hive", + required_auths: [from], + required_posting_auths: [], + json: JSON.stringify({ + contractName: "tokens", + contractAction: "stake", + contractPayload: { + symbol: currency, + to: to, + quantity: quantity + } + }) + } + ]; + } +}; + +export const transferToVesting = ( + from: string, + key: PrivateKey, + to: string, + amount: string +): Promise => { + const op: Operation = createTransferToVestingOp(from, to, amount); + + return hiveClient.broadcast.sendOperations([op], key); +}; + +export const transferToVestingHot = (from: string, to: string, amount: string) => { + const op: Operation = createTransferToVestingOp(from, to, amount); + + const params: Parameters = { + callback: `${document.location.protocol}//${document.location.host}/@${from}/wallet` + }; + return hs.sendOperation(op, params, () => {}); +}; + +export const transferToVestingKc = (from: string, to: string, amount: string) => { + const op: Operation = createTransferToVestingOp(from, to, amount); return keychain.broadcast(from, [op], "Active"); }; +export const createDelegateVestingSharesOp = ( + delegator: string, + delegatee: string, + vestingShares: string +): Operation => { + if (!/[0-9]+(.[0-9]+)? [A-Z][A-Z0-9]+/.test(vestingShares)) { + throw new Error(`Invalid vestingShares Amount specified: "${vestingShares}"`); + } + const parts = vestingShares.split(/ /); + const currency = parts[parts.length - 1]; + const quantity = parts[0].replace(/,/g, ""); + if (currency === "HP" || currency === "HIVE") { + throw new Error(`Invalid parameter: ${currency} can be an HiveEngine asset or VESTS`); + } + if (currency === "VESTS") { + return [ + "delegate_vesting_shares", + { + delegator, + delegatee, + vesting_shares: vestingShares + } + ]; + } else { + return [ + "custom_json", + { + id: "ssc-mainnet-hive", + required_auths: [delegator], + required_posting_auths: [], + json: JSON.stringify({ + contractName: "tokens", + contractAction: "delegate", + contractPayload: { + symbol: currency, + to: delegatee, + quantity: quantity + } + }) + } + ]; + } +}; + export const delegateVestingShares = ( delegator: string, key: PrivateKey, delegatee: string, vestingShares: string ): Promise => { - const op: Operation = [ - "delegate_vesting_shares", - { - delegator, - delegatee, - vesting_shares: vestingShares - } - ]; - + const op: Operation = createDelegateVestingSharesOp(delegator, delegatee, vestingShares); return hiveClient.broadcast.sendOperations([op], key); }; @@ -843,16 +1068,13 @@ export const delegateVestingSharesHot = ( delegatee: string, vestingShares: string ) => { - const op: Operation = [ - "delegate_vesting_shares", - { - delegator, - delegatee, - vesting_shares: vestingShares - } - ]; - - const params: Parameters = { callback: `https://ecency.com/@${delegator}/wallet` }; + const op: Operation = createDelegateVestingSharesOp(delegator, delegatee, vestingShares); + const parts = vestingShares.split(/ /); + const currency = parts[parts.length - 1]; + const quantity = parts[0].replace(/,/g, ""); + const params: Parameters = { + callback: `${document.location.protocol}//${document.location.host}/@${delegator}/wallet` + }; return hs.sendOperation(op, params, () => {}); }; @@ -861,15 +1083,7 @@ export const delegateVestingSharesKc = ( delegatee: string, vestingShares: string ) => { - const op: Operation = [ - "delegate_vesting_shares", - { - delegator, - delegatee, - vesting_shares: vestingShares - } - ]; - + const op: Operation = createDelegateVestingSharesOp(delegator, delegatee, vestingShares); return keychain.broadcast(delegator, [op], "Active"); }; @@ -897,9 +1111,43 @@ export const withdrawVestingHot = (account: string, vestingShares: string) => { vesting_shares: vestingShares } ]; +}; - const params: Parameters = { callback: `https://ecency.com/@${account}/wallet` }; - return hs.sendOperation(op, params, () => {}); +export const cancelWithdrawVesting = ( + account: string, + key: PrivateKey, + txID: string +): Promise => { + const op: Operation = createCancelPowerDownOp(account, txID); + return hiveClient.broadcast.sendOperations([op], key); +}; +export const cancelWithdrawVestingHot = (account: string, txID: string) => { + const op: Operation = createCancelPowerDownOp(account, txID); + const params: Parameters = { + callback: document.location.toString() + }; + hs.sendOperation(op, params, () => {}); +}; +export const cancelWithdrawVestingKc = (account: string, txID: string) => { + const op: Operation = createCancelPowerDownOp(account, txID); + return keychain.broadcast(account, [op], "Active"); +}; +export const createCancelPowerDownOp = (account: string, txID: string): Operation => { + return [ + "custom_json", + { + id: "ssc-mainnet-hive", + required_auths: ["leprechaun"], + required_posting_auths: [], + json: JSON.stringify({ + contractName: "tokens", + contractAction: "cancelUnstake", + contractPayload: { + txID: txID + } + }) + } + ]; }; export const withdrawVestingKc = (account: string, vestingShares: string) => { @@ -950,7 +1198,9 @@ export const setWithdrawVestingRouteHot = ( } ]; - const params: Parameters = { callback: `https://ecency.com/@${from}/wallet` }; + const params: Parameters = { + callback: `${document.location.protocol}//${document.location.host}/@${from}/wallet` + }; return hs.sendOperation(op, params, () => {}); }; diff --git a/src/common/components/boost/index.tsx b/src/common/components/boost/index.tsx index 6ebabdd1f5b..b058f78eab1 100644 --- a/src/common/components/boost/index.tsx +++ b/src/common/components/boost/index.tsx @@ -29,6 +29,7 @@ import _c from "../../util/fix-class-names"; import formattedNumber from "../../util/formatted-number"; import { checkAllSvg } from "../../img/svg"; +import { base } from "../../constants/defaults.json"; interface Props { global: Global; diff --git a/src/common/components/buy-sell-hive/index.tsx b/src/common/components/buy-sell-hive/index.tsx index dcd236bdf63..6c2e5a551db 100644 --- a/src/common/components/buy-sell-hive/index.tsx +++ b/src/common/components/buy-sell-hive/index.tsx @@ -30,6 +30,7 @@ import { AnyAction, bindActionCreators, Dispatch } from "redux"; import { connect } from "react-redux"; import { AppState } from "../../store"; import { PrivateKey } from "@hiveio/dhive"; +import { base } from "../../constants/defaults.json"; export enum TransactionType { None = 0, diff --git a/src/common/components/community-rewards-registration/index.tsx b/src/common/components/community-rewards-registration/index.tsx index d2fa9b7bf6a..851dbf2dd78 100644 --- a/src/common/components/community-rewards-registration/index.tsx +++ b/src/common/components/community-rewards-registration/index.tsx @@ -22,6 +22,7 @@ import { import { getRewardedCommunities } from "../../api/private-api"; import { _t } from "../../i18n"; +import { base } from "../../constants/defaults.json"; interface Props { global: Global; diff --git a/src/common/components/delegated-vesting-hive-engine/index.tsx b/src/common/components/delegated-vesting-hive-engine/index.tsx new file mode 100644 index 00000000000..14ea96446c2 --- /dev/null +++ b/src/common/components/delegated-vesting-hive-engine/index.tsx @@ -0,0 +1,203 @@ +import React, { Component } from "react"; +import { History } from "history"; +import { Modal } from "react-bootstrap"; +import { Global } from "../../store/global/types"; +import { Account } from "../../store/accounts/types"; +import { DynamicProps } from "../../store/dynamic-props/types"; +import { ActiveUser } from "../../store/active-user/types"; +import BaseComponent from "../base"; +import ProfileLink from "../profile-link"; +import UserAvatar from "../user-avatar"; +import LinearProgress from "../linear-progress"; +import Tooltip from "../tooltip"; +import KeyOrHotDialog from "../key-or-hot-dialog"; +import { error } from "../feedback"; +import { + claimRewards, + getHiveEngineTokenBalances, + getUnclaimedRewards, + getTokenDelegations, + TokenStatus, + DelegationEntry, + Unstake, + getPendingUnstakes +} from "../../api/hive-engine"; +import { + undelegateHiveEngineKey, + undelegateHiveEngineHs, + undelegateHiveEngineKc, + formatError +} from "../../api/operations"; +import { _t } from "../../i18n"; +import { vestsToHp } from "../../helper/vesting"; +import parseAsset from "../../helper/parse-asset"; +import formattedNumber from "../../util/formatted-number"; +import _c from "../../util/fix-class-names"; +import HiveEngineToken, { HiveEngineTokenEntryDelta } from "../../helper/hive-engine-wallet"; + +interface Props { + history: History; + global: Global; + activeUser: ActiveUser | null; + account: Account; + dynamicProps: DynamicProps; + signingKey: string; + addAccount: (data: Account) => void; + setSigningKey: (key: string) => void; + onHide: () => void; + updateActiveUser: (data?: Account) => void; + modifyTokenValues: (delta: HiveEngineTokenEntryDelta) => void; + hiveEngineToken: HiveEngineToken; + delegationList: Array; +} + +interface State { + inProgress: boolean; + data: DelegationEntry[]; + hideList: boolean; +} + +export class ListHE extends BaseComponent { + state: State = { + inProgress: false, + data: [], + hideList: false + }; + componentDidMount() { + const { delegationList, hiveEngineToken, activeUser } = this.props; + const data = delegationList.filter( + (d) => d.symbol === hiveEngineToken.symbol && activeUser && d.from === activeUser.username + ); + this.setState({ data }); + } + render() { + const { data, hideList, inProgress } = this.state; + const { dynamicProps, activeUser, account, updateActiveUser, delegationList, hiveEngineToken } = + this.props; + const { hivePerMVests } = dynamicProps; + + return ( +
+
+
+ {data.length === 0 &&
{_t("g.empty-list")}
} + {data.map((x) => { + const { symbol, quantity, to: username } = x; + const deleteBtn = + activeUser && activeUser.username === account.name + ? KeyOrHotDialog({ + ...this.props, + activeUser: activeUser, + children: ( + + {_t("delegated-vesting.undelegate")} + + ), + onToggle: () => { + const { hideList } = this.state; + this.stateSet({ hideList: !hideList }); + }, + onKey: (key) => { + this.stateSet({ inProgress: true }); + undelegateHiveEngineKey( + activeUser.username, + key, + symbol, + username, + quantity + ) + .then((TxC) => { + const { modifyTokenValues } = this.props; + this.stateSet({ + data: data.filter((y) => y.to != x.to) + }); + modifyTokenValues({ symbol, delegationsOutDelta: -quantity }); + }) + .catch((err) => error(err.message)) + .finally(() => { + this.setState({ inProgress: false }); + updateActiveUser(activeUser.data); + }); + }, + onHot: () => { + undelegateHiveEngineHs(activeUser.username, username, symbol, quantity); + }, + onKc: () => { + this.stateSet({ inProgress: true }); + undelegateHiveEngineKc(activeUser.username, username, symbol, quantity) + .then(() => { + const { modifyTokenValues } = this.props; + this.stateSet({ + data: data.filter((y) => y.to !== x.to) + }); + modifyTokenValues({ symbol, delegationsOutDelta: -quantity }); + }) + .catch((err) => error(err.message)) + .finally(() => { + this.stateSet({ inProgress: false }); + updateActiveUser(activeUser.data); + }); + } + }) + : null; + return ( +
+
+ {ProfileLink({ + ...this.props, + username, + children: ( + <> + {UserAvatar({ + ...this.props, + username: x.to, + size: "small" + })} + + ) + })} +
+ {ProfileLink({ + ...this.props, + username, + children: {username} + })} +
+
+
+ + {formattedNumber(x.quantity, { suffix: x.symbol })} + + {deleteBtn} +
+
+ ); + })} +
+
+
+ ); + } +} + +export default class DelegatedVestingHE extends Component { + render() { + const { onHide, hiveEngineToken } = this.props; + return ( + <> + + + {_t("staked", hiveEngineToken)} + + + + + + + ); + } +} diff --git a/src/common/components/delegated-vesting/index.tsx b/src/common/components/delegated-vesting/index.tsx index 02e85d8b6ef..6dacb2ffeef 100644 --- a/src/common/components/delegated-vesting/index.tsx +++ b/src/common/components/delegated-vesting/index.tsx @@ -36,6 +36,7 @@ import formattedNumber from "../../util/formatted-number"; import _c from "../../util/fix-class-names"; import MyPagination from "../pagination"; +import { base } from "../../constants/defaults.json"; interface Props { history: History; diff --git a/src/common/components/entry-list-item/_index.scss b/src/common/components/entry-list-item/_index.scss index 14d97abfce2..8a25e6c914c 100644 --- a/src/common/components/entry-list-item/_index.scss +++ b/src/common/components/entry-list-item/_index.scss @@ -179,6 +179,15 @@ width: 20px; } +.tiny-image { + width: 20px; + max-height: 30px; +} + +.tiny-image:hover { + width: 25px; +} + .author-down-arrow { display: none; diff --git a/src/common/components/error-boundary.tsx b/src/common/components/error-boundary.tsx new file mode 100644 index 00000000000..499123669a8 --- /dev/null +++ b/src/common/components/error-boundary.tsx @@ -0,0 +1,33 @@ +import React, { Component } from "react"; + +interface State { + hasError: boolean; +} + +interface Props {} + +export default class ErrorBoundary extends React.Component { + constructor(props: Props) { + super(props); + this.state = { hasError: false }; + } + + static getDerivedStateFromError(error: unknown) { + // Update state so the next render will show the fallback UI. + return { hasError: true }; + } + + componentDidCatch(error: unknown, errorInfo: unknown) { + // You can also log the error to an error reporting service + console.error(error, errorInfo); + } + + render() { + if (this.state.hasError) { + // You can render any custom fallback UI + return

Something went wrong.

; + } + + return this.props.children; + } +} diff --git a/src/common/components/feedback/index.tsx b/src/common/components/feedback/index.tsx index b338f391660..353cec67dc5 100644 --- a/src/common/components/feedback/index.tsx +++ b/src/common/components/feedback/index.tsx @@ -8,15 +8,35 @@ import { ErrorTypes } from "../../enums"; import { ActiveUser } from "../../store/active-user/types"; import { _t } from "../../i18n"; -export const error = (message: string, errorType = ErrorTypes.COMMON) => { - const detail: ErrorFeedbackObject = { - id: random(), - type: "error", - message, - errorType - }; - const ev = new CustomEvent("feedback", { detail }); - window.dispatchEvent(ev); +function isString(x: any): x is string { + return typeof x === "string"; +} + +function isComposedErrorPair( + value: string | [string, ErrorTypes.COMMON | ErrorTypes.INSUFFICIENT_RESOURCE_CREDITS] +): value is [string, ErrorTypes.COMMON | ErrorTypes.INSUFFICIENT_RESOURCE_CREDITS] { + return ( + (value.length === 2 && typeof value[0] === "string" && value[1] === ErrorTypes.COMMON) || + value[1] === ErrorTypes.INSUFFICIENT_RESOURCE_CREDITS + ); +} + +export const error = ( + message: string | [string, ErrorTypes.COMMON | ErrorTypes.INSUFFICIENT_RESOURCE_CREDITS], + errorType = ErrorTypes.COMMON +) => { + if (isString(message)) { + const detail: ErrorFeedbackObject = { + id: random(), + type: "error", + message, + errorType + }; + const ev = new CustomEvent("feedback", { detail }); + window.dispatchEvent(ev); + } else { + error(message[0], message[1]); + } }; export const success = (message: string) => { diff --git a/src/common/components/key-or-hot-dialog/index.tsx b/src/common/components/key-or-hot-dialog/index.tsx index 21ba77d7cad..a646ec788fb 100644 --- a/src/common/components/key-or-hot-dialog/index.tsx +++ b/src/common/components/key-or-hot-dialog/index.tsx @@ -8,6 +8,7 @@ import { Global } from "../../store/global/types"; import { ActiveUser } from "../../store/active-user/types"; import KeyOrHot from "../key-or-hot"; +import { base } from "../../constants/defaults.json"; interface Props { global: Global; diff --git a/src/common/components/key-or-hot/index.tsx b/src/common/components/key-or-hot/index.tsx index 8485764a1e2..db7987919b1 100644 --- a/src/common/components/key-or-hot/index.tsx +++ b/src/common/components/key-or-hot/index.tsx @@ -13,6 +13,7 @@ import { error } from "../feedback"; import { _t } from "../../i18n"; import { keySvg } from "../../img/svg"; +import { base } from "../../constants/defaults.json"; interface Props { global: Global; diff --git a/src/common/components/login/index.tsx b/src/common/components/login/index.tsx index b2ec3005bc2..dde4b1c50e3 100644 --- a/src/common/components/login/index.tsx +++ b/src/common/components/login/index.tsx @@ -135,7 +135,7 @@ export class LoginKc extends BaseComponent { try { code = await makeHsCode(hsClientId, username, signer); } catch (err) { - error(...formatError(err)); + error(formatError(err)); this.stateSet({ inProgress: false }); return; } diff --git a/src/common/components/promote/index.tsx b/src/common/components/promote/index.tsx index a818c00a18d..251168e3de9 100644 --- a/src/common/components/promote/index.tsx +++ b/src/common/components/promote/index.tsx @@ -27,6 +27,7 @@ import { _t } from "../../i18n"; import _c from "../../util/fix-class-names"; import { checkAllSvg } from "../../img/svg"; +import { base } from "../../constants/defaults.json"; interface Props { global: Global; diff --git a/src/common/components/transactions/index.tsx b/src/common/components/transactions/index.tsx index 25f67ee2c8f..f083d20eb23 100644 --- a/src/common/components/transactions/index.tsx +++ b/src/common/components/transactions/index.tsx @@ -20,20 +20,25 @@ import { vestsToHp } from "../../helper/vesting"; import formattedNumber from "../../util/formatted-number"; import { - ticketSvg, - compareHorizontalSvg, + arrowLeftSvg, + arrowRightSvg, cashMultiple, - reOrderHorizontalSvg, - pickAxeSvg, + cashCoinSvg, + cashSvg, + chevronUpSvgForVote, + chevronDownSvgForSlider, closeSvg, + commentSvg, + compareHorizontalSvg, exchangeSvg, - cashCoinSvg, + gridSvg, + pickAxeSvg, powerDownSvg, powerUpSvg, + reOrderHorizontalSvg, + starSvg, starsSvg, - chevronUpSvgForVote, - chevronDownSvgForSlider, - starSvg + ticketSvg } from "../../img/svg"; import { _t } from "../../i18n"; @@ -127,6 +132,24 @@ export class TransactionRow extends Component { ) }); } + if (tr.type === "tokens_unstake") { + flag = true; + icon = cashMultiple; + numbers = ( + <> + {tr.amount} + + ); + } + if (tr.type === "tokens_issue") { + flag = true; + icon = cashMultiple; + numbers = ( + <> + {tr.amount} + + ); + } if (tr.type === "claim_reward_balance") { flag = true; @@ -260,6 +283,33 @@ export class TransactionRow extends Component { ); } + if (tr.type === "tokens_CancelUnstake") { + flag = true; + icon = closeSvg; + numbers = {tr.amount}; + } + if (tr.type === "tokens_unstakeStart") { + flag = true; + icon = cashSvg; + numbers = {tr.amount}; + } + if (tr.type === "tokens_unstakeDone") { + flag = true; + icon = cashSvg; + numbers = {tr.amount}; + } + if (tr.type === "market_placeOrder") { + flag = true; + icon = commentSvg; + numbers = {tr.quantityLocked}; + if (tr.price) + details = ( + <> + {tr.orderType} @ {tr.price} + + ); + else details = tr.orderType; + } if (tr.type === "withdraw_vesting") { flag = true; icon = powerDownSvg; @@ -324,6 +374,20 @@ export class TransactionRow extends Component { ); } + if (tr.type === "limit_order_cancel") { + flag = true; + icon = closeSvg; + numbers = ( + + # + {formattedNumber(tr.orderid, { + fractionDigits: 0, + maximumFractionDigits: 0 + })} + + ); + } + if (tr.type === "limit_order_create") { flag = true; icon = reOrderHorizontalSvg; @@ -377,6 +441,21 @@ export class TransactionRow extends Component { ); } + if (tr.type === "market_sell" && (icon = arrowRightSvg)) { + flag = true; + numbers = ( + + {_t("transactions.sold", { q: tr.quote })} ⟶ {tr.base} + + ); + details = {_t("transactions.for", { b: tr.base })}; + } + + if (tr.type === "market_buy" && (icon = arrowLeftSvg)) { + flag = true; + numbers = {_t("transactions.bought", { q: tr.quote })}; + details = {_t("transactions.for", { b: tr.base })}; + } if (tr.type === "fill_collateralized_convert_request") { flag = true; @@ -396,6 +475,63 @@ export class TransactionRow extends Component { ); } + if (tr.type === "market_cancel") { + flag = true; + icon = commentSvg; + details = {tr.orderType}; + numbers = {tr.amount}; + } + if (tr.type === "market_closeOrder") { + flag = true; + if (tr.orderType == "sell") { + icon = arrowRightSvg; + } else { + icon = arrowLeftSvg; + } + details = ( + <> + + {tr.orderType} + + + ); + } + if (tr.type === "market_expireOrder") { + flag = true; + icon = closeSvg; + numbers = <>{tr.amountUnlocked}; + details = ( + <> + + orderID: {tr.orderID} + {tr.orderType} + + + ); + } + + if (tr.type === "proposal_pay") { + flag = true; + icon = ticketSvg; + numbers = {tr.payment}; + } + if (tr.type === "tokens_undelegateDone") { + flag = true; + icon = arrowRightSvg; + details = trx_id {tr.trx_id}; + } + if (tr.type === "tokens_undelegateStart") { + flag = true; + icon = arrowRightSvg; + details = @{tr.from} ; + numbers = {tr.amount} ; + } + if (tr.type === "tokens_delegate") { + flag = true; + icon = arrowRightSvg; + details = @ {tr.to}; + numbers = {tr.amount}; + } if (tr.type === "return_vesting_delegation") { flag = true; diff --git a/src/common/components/transfer-he/__snapshots__/index.spec.tsx.snap b/src/common/components/transfer-he/__snapshots__/index.spec.tsx.snap index 5484edef7bc..bd49d05e826 100644 --- a/src/common/components/transfer-he/__snapshots__/index.spec.tsx.snap +++ b/src/common/components/transfer-he/__snapshots__/index.spec.tsx.snap @@ -136,12 +136,12 @@ exports[`(1) Hive Engine - Transfer (1) Step 1 1`] = ` type="text" value="0.001" /> - - - + + POB +
- 0 + 0.000 - + POB
@@ -323,7 +323,7 @@ exports[`(1) Hive Engine - Transfer (2) Step 2 1`] = ` > 0.001 - + POB @@ -513,7 +513,7 @@ exports[`(1) Hive Engine - Transfer (4) Step 4 1`] = ` className="success" dangerouslySetInnerHTML={ Object { - "__html": "0.001 transferred from foo to bar", + "__html": "0.001 POB transferred from foo to bar", } } /> @@ -681,12 +681,12 @@ exports[`(2) Hive Engine - Delegate (1) Step 1 1`] = ` type="text" value="0.001" /> - - - + + WEED +
- 0 + 0.000 - + WEED
@@ -835,7 +835,7 @@ exports[`(2) Hive Engine - Delegate (2) Step 2 1`] = ` > 0.001 - + WEED
0.001 delegated from foo to bar", + "__html": "0.001 WEED delegated from foo to bar", } } /> @@ -1045,12 +1045,12 @@ exports[`(3) Hive Engine - Undelegate (1) Step 1 1`] = ` type="text" value="0.001" /> - - -
+ + LOLZ +
- 0 + 0.000 - + LOLZ
@@ -1199,7 +1199,7 @@ exports[`(3) Hive Engine - Undelegate (2) Step 2 1`] = ` > 0.001 - + LOLZ @@ -1268,7 +1268,7 @@ exports[`(3) Hive Engine - Undelegate (4) Step 4 1`] = ` className="success" dangerouslySetInnerHTML={ Object { - "__html": "0.001 undelegated from foo to bar", + "__html": "0.001 LOLZ undelegated from foo to bar", } } /> @@ -1391,12 +1391,12 @@ exports[`(4) Hive Engine - Stake (1) Step 1 1`] = `
@@ -1436,12 +1436,12 @@ exports[`(4) Hive Engine - Stake (1) Step 1 1`] = ` type="text" value="0.001" /> - - -
+ + BST +
- 0 + 0.000 - + BST
@@ -1590,7 +1590,7 @@ exports[`(4) Hive Engine - Stake (2) Step 2 1`] = ` > 0.001 - + BST @@ -1659,7 +1659,7 @@ exports[`(4) Hive Engine - Stake (4) Step 4 1`] = ` className="success" dangerouslySetInnerHTML={ Object { - "__html": "0.001 staked from foo to bar", + "__html": "0.001 BST staked from foo to bar", } } /> @@ -1786,12 +1786,12 @@ exports[`(5) Hive Engine - Unstake (1) Step 1 1`] = ` type="text" value="0.001" /> - - - + + POB +
- 0 + 0.000 - + POB
@@ -1916,7 +1916,7 @@ exports[`(5) Hive Engine - Unstake (2) Step 2 1`] = ` > 0.001 - + POB @@ -1985,7 +1985,7 @@ exports[`(5) Hive Engine - Unstake (4) Step 4 1`] = ` className="success" dangerouslySetInnerHTML={ Object { - "__html": "0.001 unstaked from foo to bar", + "__html": "0.001 POB unstaked from foo to bar", } } /> diff --git a/src/common/components/transfer-he/index.spec.tsx b/src/common/components/transfer-he/index.spec.tsx index 52e764c2485..378f204f830 100644 --- a/src/common/components/transfer-he/index.spec.tsx +++ b/src/common/components/transfer-he/index.spec.tsx @@ -23,6 +23,7 @@ const defProps = { account: { name: "user1" }, + _to: "", activeUser: { username: "foo", data: { @@ -42,13 +43,15 @@ const defProps = { setSigningKey: () => {}, fetchPoints: () => {}, updateWalletValues: () => {}, - onHide: () => {} + onHide: () => {}, + tokens: [], + delegationList: [] }; describe("(1) Hive Engine - Transfer", () => { const mode: TransferMode = "transfer"; - const asset: string = ""; - const assetBalance: number = 0.0; + const asset: string = "POB"; + const assetBalance: number = 5000; const props = { ...defProps, @@ -82,7 +85,7 @@ describe("(1) Hive Engine - Transfer", () => { describe("(2) Hive Engine - Delegate", () => { const mode: TransferMode = "delegate"; - const asset: string = ""; + const asset: string = "WEED"; const assetBalance: number = 0.0; const props = { @@ -114,8 +117,8 @@ describe("(2) Hive Engine - Delegate", () => { describe("(3) Hive Engine - Undelegate", () => { const mode: TransferMode = "undelegate"; - const asset: string = ""; - const assetBalance: number = 0.0; + const asset: string = "LOLZ"; + const assetBalance: number = 4.0; const props = { ...defProps, @@ -144,8 +147,8 @@ describe("(3) Hive Engine - Undelegate", () => { describe("(4) Hive Engine - Stake", () => { const mode: TransferMode = "stake"; - const asset: string = ""; - const assetBalance: number = 0.0; + const asset: string = "BST"; + const assetBalance: number = 20; const props = { ...defProps, @@ -174,8 +177,8 @@ describe("(4) Hive Engine - Stake", () => { describe("(5) Hive Engine - Unstake", () => { const mode: TransferMode = "unstake"; - const asset: string = ""; - const assetBalance: number = 0.0; + const asset: string = "POB"; + const assetBalance: number = 10000; const props = { ...defProps, diff --git a/src/common/components/transfer-he/index.tsx b/src/common/components/transfer-he/index.tsx index 50f48ae7b40..5bf34a21d72 100644 --- a/src/common/components/transfer-he/index.tsx +++ b/src/common/components/transfer-he/index.tsx @@ -7,7 +7,7 @@ import numeral from "numeral"; import isEqual from "react-fast-compare"; import { Modal, Form, Row, Col, InputGroup, FormControl, Button } from "react-bootstrap"; - +import HiveEngineToken, { HiveEngineTokenEntryDelta } from "../../helper/hive-engine-wallet"; import badActors from "@hiveio/hivescript/bad-actors.json"; import { Global } from "../../store/global/types"; @@ -54,9 +54,72 @@ import { Tsx } from "../../i18n/helper"; import { arrowRightSvg } from "../../img/svg"; import formattedNumber from "../../util/formatted-number"; +import activeUser from "../../store/active-user"; import { dateToFullRelative } from "../../helper/parse-date"; export type TransferMode = "transfer" | "delegate" | "undelegate" | "stake" | "unstake"; +export type TransferAsset = string; +import { DelegationEntry } from "../../api/hive-engine"; +import { base } from "../../constants/defaults.json"; +interface AssetSwitchProps { + options: TransferAsset[]; + selected: TransferAsset; + onChange: (i: TransferAsset) => void; +} + +class AssetSwitch extends Component { + clicked = (i: TransferAsset) => { + this.setState({ selected: i }); + const { onChange } = this.props; + onChange(i); + }; + + selectSet = () => { + const el: HTMLSelectElement | null = document.getElementById("sel") as HTMLSelectElement | null; + + if (el) { + this.setState({ selected: el.value }); + const { onChange } = this.props; + onChange(el.value); + } else { + console.log("selectSet dne"); + } + }; + + render() { + const { options, selected, onChange } = this.props; + + if (options.length > 4) + return ( + + ); + + return ( + + ); + } +} class FormText extends Component<{ msg: string; @@ -92,6 +155,9 @@ interface Props { fetchPoints: (username: string, type?: number) => void; updateWalletValues: () => void; onHide: () => void; + tokens: HiveEngineToken[]; + modifyTokenValues?: (delta: HiveEngineTokenEntryDelta) => void; + delegationList: Array; } interface State { @@ -99,7 +165,7 @@ interface State { asset: string; assetBalance: number; precision: number; - delegationList: DelegateVestingShares[]; + delegationOutList: DelegationEntry[]; to: string; toData: Account | null; toError: string; @@ -107,6 +173,15 @@ interface State { toWarning: string; amount: string; amountError: string; + + stakingEnabled?: boolean; + delegationEnabled?: boolean; + balance: number; + stake: number; + stakedBalance: number; + delegationsIn: number; + delegationsOut: number; + // memo: string; inProgress: boolean; } @@ -115,26 +190,53 @@ const pureState = (props: Props): State => { let _to: string = ""; let _toData: Account | null = null; - // if ([ "delegate", "undelegate", "stake", "unstake"].includes(props.mode)) { - // _to = props.activeUser.username; - // _toData = props.activeUser.data - // } + if (["stake", "unstake"].includes(props.mode)) { + _to = props.activeUser.username; + _toData = props.activeUser.data; + } + + const { asset } = props; + let thisToken = props?.tokens?.find && props.tokens.find((t) => t.symbol === asset); + if (!thisToken) { + thisToken = new HiveEngineToken({ + name: asset, + icon: "", + balance: "0", + stake: "0", + + delegationsIn: "0", + delegationsOut: "0", + symbol: asset, + stakingEnabled: true, + delegationEnabled: true, + precision: asset === "VESTS" ? 6 : 3 + }); + } + const tokenPrecision = thisToken?.precision ?? 3; + const defaultAmount = + props.amount || + (tokenPrecision === 0 + ? "1" + : formattedNumber("0." + "0".repeat(tokenPrecision - 1) + "1", { + fractionDigits: tokenPrecision + })); return { step: 1, asset: props.asset, assetBalance: props.assetBalance, - precision: (props.assetBalance + "").split(".")[1]?.length || 3, to: props.to || _to, toData: props.to ? { name: props.to } : _toData, toError: "", memoError: "", toWarning: "", - amount: props.amount || "0.001", + amount: props.amount || defaultAmount, amountError: "", memo: props.memo || "", inProgress: false, - delegationList: [] + delegationOutList: props.delegationList.filter((e) => e.from === props?.activeUser?.username), + ...thisToken, + precision: tokenPrecision }; }; @@ -145,6 +247,7 @@ export class Transfer extends BaseComponent { componentDidMount() { this.checkAmount(); + const { updateActiveUser } = this.props; updateActiveUser(); } @@ -157,18 +260,41 @@ export class Transfer extends BaseComponent { formatNumber = (num: number | string, precision: number) => { const format = `0.${"0".repeat(precision)}`; - + if (typeof num === "string") { + const stripedNumber = num.replace(/,/g, ""); + const decimalLocation = stripedNumber.indexOf("."); + if (decimalLocation + 1 === 0) { + return stripedNumber + "." + "0".repeat(precision); + } else if (stripedNumber.length - decimalLocation - 1 < precision) { + return stripedNumber + "0".repeat(precision + 1 + decimalLocation - stripedNumber.length); + } else { + return stripedNumber.slice(0, decimalLocation + precision + 1); + } + } return numeral(num).format(format, Math.floor); // round to floor }; - assetChanged = (asset: string) => { - this.stateSet({ asset }, () => { + assetChanged = (asset: TransferAsset) => { + const { amount } = this.state; + const { tokens } = this.props; + let precision: number = (() => { + const tokenInformation = tokens?.find && tokens.find((i) => asset === i.symbol); + + if (tokenInformation) { + return tokenInformation.precision || 8; + } + + return 0; + })() as number; + const newAmount = formattedNumber(amount, { fractionDigits: precision }); + this.stateSet({ asset, amount: newAmount, precision }, () => { this.checkAmount(); }); }; toChanged = (e: React.ChangeEvent) => { const { value: to } = e.target; + const { mode } = this.props; this.stateSet({ to }, this.handleTo); }; @@ -252,14 +378,15 @@ export class Transfer extends BaseComponent { const dotParts = amount.split("."); if (dotParts.length > 1) { - const _precision = dotParts[1]; - if (_precision.length > precision) { + const fractionPart = dotParts[1].replace(/,/g, ""); + if (fractionPart.length > precision) { this.stateSet({ amountError: _t("transfer.amount-precision-error") }); return; } } - if (parseFloat(amount) > this.state.assetBalance) { + let balance = Number(this.formatBalance(this.getBalance())); + if (parseFloat(amount.replace(/,/g, "")) > balance) { this.stateSet({ amountError: _t("trx-common.insufficient-funds") }); return; } @@ -268,25 +395,105 @@ export class Transfer extends BaseComponent { }; copyBalance = () => { - const amount = this.formatBalance(this.state.assetBalance); + const amount = this.formatBalance(this.getBalance()); this.stateSet({ amount }, () => { this.checkAmount(); }); }; + getBalance = (): number => { + const { mode, activeUser, dynamicProps, tokens } = this.props; + const { asset, to, delegationOutList } = this.state; + + const { data: account } = activeUser; + + const tokenInformation = tokens?.find && tokens.find((i) => asset === i.symbol); + + if (tokenInformation) { + if (mode === "unstake" || mode == "delegate") { + return tokenInformation.stakedBalance; + } + if (mode === "undelegate") { + const delegation = delegationOutList.find((e) => e.symbol === asset && e.to === to); + if (delegation) { + const q = parseFloat(delegation.quantity); + return q; + } + return 0; + } + return tokenInformation.balance; + } + + return 0; + }; + formatBalance = (balance: number): string => { const { precision } = this.state; - return this.formatNumber(balance, precision); + return formattedNumber(balance, { fractionDigits: precision }); }; canSubmit = () => { - const { toData, toError, amountError, memoError, inProgress, amount } = this.state; - if (this.props.mode === "unstake") return parseFloat(amount) > 0; + const { toData, toError, amountError, memoError, inProgress, amount, precision } = this.state; + if (this.props.mode === "unstake") return parseFloat(amount.replace(/,/g, "")) > 0; return ( - toData && !toError && !amountError && !memoError && !inProgress && parseFloat(amount) > 0 + toData && + !toError && + !amountError && + !memoError && + !inProgress && + parseFloat(amount.replace(/,/g, "")) > 0 ); }; + modifyTokenValues() { + const { modifyTokenValues, mode } = this.props; + const { asset, delegationOutList, delegationsOut, to, precision, balance, stake } = this.state; + const amount = parseFloat(this.state.amount.replace(/,/g, "")); + if (modifyTokenValues) + switch (mode) { + case "transfer": { + modifyTokenValues({ symbol: asset, balanceDelta: -amount }); + this.setState({ balance: balance - amount }); + break; + } + case "stake": { + modifyTokenValues({ symbol: asset, balanceDelta: -amount, stakeDelta: amount }); + this.setState({ balance: balance - amount }); + break; + } + case "unstake": { + // balance wont go up until the power down period ends. + modifyTokenValues({ symbol: asset, stakeDelta: -amount }); + break; + } + case "delegate": { + modifyTokenValues({ symbol: asset, delegationsOutDelta: amount }); + break; + } + case "undelegate": { + for (let x of delegationOutList) { + if (x.to === to) { + const diff = parseFloat(x.quantity) - amount; + x.quantity = formattedNumber(diff, { fractionDigits: precision }); + if (diff === 0) { + this.setState({ + delegationsOut: delegationsOut - 1, + delegationOutList: delegationOutList.filter((x) => x.to !== to) + }); + } else { + this.setState({ delegationOutList: [...delegationOutList] }); + } + break; + } + } + modifyTokenValues({ symbol: asset, delegationsOutDelta: -amount }); + break; + } + default: + return; + } + } + next = () => { // make sure 3 decimals in amount const { amount, precision } = this.state; @@ -310,34 +517,34 @@ export class Transfer extends BaseComponent { sign = (key: PrivateKey) => { const { activeUser, mode } = this.props; const { to, amount, asset, memo } = this.state; - const fullAmount = `${amount}`; + const unformattedQuantity = amount.replace(/,/g, ""); const username = activeUser?.username!; let promise: Promise; switch (mode) { case "transfer": { // Perform HE operation - promise = transferHiveEngineKey(username, key, asset, to, fullAmount, memo); + promise = transferHiveEngineKey(username, key, asset, to, unformattedQuantity, memo); break; } case "delegate": { // Perform HE operation - promise = delegateHiveEngineKey(username, key, asset, to, fullAmount); + promise = delegateHiveEngineKey(username, key, asset, to, unformattedQuantity); break; } case "undelegate": { // Perform HE operation - promise = undelegateHiveEngineKey(username, key, asset, to, fullAmount); + promise = undelegateHiveEngineKey(username, key, asset, to, unformattedQuantity); break; } case "stake": { // Perform HE operation - promise = stakeHiveEngineKey(username, key, asset, to, fullAmount); + promise = stakeHiveEngineKey(username, key, asset, to, unformattedQuantity); break; } case "unstake": { // Perform HE operation - promise = unstakeHiveEngineKey(username, key, asset, to, fullAmount); + promise = unstakeHiveEngineKey(username, key, asset, to, unformattedQuantity); break; } default: @@ -350,10 +557,7 @@ export class Transfer extends BaseComponent { .then(() => getAccountFull(activeUser.username)) .then((a) => { const { addAccount, updateActiveUser } = this.props; - // refresh - addAccount(a); - // update active - updateActiveUser(a); + this.modifyTokenValues(); this.stateSet({ step: 4, inProgress: false }); }) .catch((err) => { @@ -365,30 +569,30 @@ export class Transfer extends BaseComponent { signHs = () => { const { activeUser, mode, onHide } = this.props; const { to, amount, asset, memo } = this.state; - const fullAmount = `${amount}`; + const unformattedQuantity = amount.replace(/,/g, ""); const username = activeUser?.username!; let promise: Promise; switch (mode) { case "transfer": { - promise = transferHiveEngineHs(username, to, asset, fullAmount, memo); + promise = transferHiveEngineHs(username, to, asset, unformattedQuantity, memo); break; } case "delegate": { - promise = delegateHiveEngineHs(username, to, asset, fullAmount); + promise = delegateHiveEngineHs(username, to, asset, unformattedQuantity); break; } case "undelegate": { - promise = undelegateHiveEngineHs(username, to, asset, fullAmount); + promise = undelegateHiveEngineHs(username, to, asset, unformattedQuantity); break; } case "stake": { - promise = stakeHiveEngineHs(username, to, asset, fullAmount); + promise = stakeHiveEngineHs(username, to, asset, unformattedQuantity); break; } case "unstake": { - promise = unstakeHiveEngineHs(username, to, asset, fullAmount); + promise = unstakeHiveEngineHs(username, to, asset, unformattedQuantity); break; } default: @@ -401,29 +605,29 @@ export class Transfer extends BaseComponent { signKs = () => { const { activeUser, mode } = this.props; const { to, amount, asset, memo } = this.state; - const fullAmount = `${amount}`; + const unformattedQuantity = amount.replace(/,/g, ""); const username = activeUser?.username!; let promise: Promise; switch (mode) { case "transfer": { - promise = transferHiveEngineKc(username, to, asset, fullAmount, memo); + promise = transferHiveEngineKc(username, to, asset, unformattedQuantity, memo); break; } case "delegate": { - promise = delegateHiveEngineKc(username, to, asset, fullAmount); + promise = delegateHiveEngineKc(username, to, asset, unformattedQuantity); break; } case "undelegate": { - promise = undelegateHiveEngineKc(username, to, asset, fullAmount); + promise = undelegateHiveEngineKc(username, to, asset, unformattedQuantity); break; } case "stake": { - promise = stakeHiveEngineKc(username, to, asset, fullAmount); + promise = stakeHiveEngineKc(username, to, asset, unformattedQuantity); break; } case "unstake": { - promise = unstakeHiveEngineKc(username, to, asset, fullAmount); + promise = unstakeHiveEngineKc(username, to, asset, unformattedQuantity); break; } default: @@ -435,10 +639,7 @@ export class Transfer extends BaseComponent { .then(() => getAccountFull(activeUser.username)) .then((a) => { const { addAccount, updateActiveUser } = this.props; - // refresh - addAccount(a); - // update active - updateActiveUser(a); + this.modifyTokenValues(); this.stateSet({ step: 4, inProgress: false }); }) .catch((err) => { @@ -462,7 +663,32 @@ export class Transfer extends BaseComponent { }; render() { - const { global, mode, activeUser, transactions, dynamicProps } = this.props; + const { tokens, mode, dynamicProps, activeUser, transactions } = this.props; + let assets: TransferAsset[] = []; + + for (const token of tokens) { + const { symbol, stakingEnabled, delegationEnabled } = token; + switch (mode) { + case "transfer": + { + assets = [...assets, symbol]; + } + break; + case "stake": + case "unstake": { + if (stakingEnabled) { + assets = [...assets, symbol]; + } + break; + } + case "delegate": { + if (delegationEnabled) { + assets = [...assets, symbol]; + } + } + } // switch + } + const { step, asset, @@ -476,32 +702,40 @@ export class Transfer extends BaseComponent { memo, inProgress, toData, - delegationList + delegationOutList } = this.state; const { hivePerMVests } = dynamicProps; - const recent = [ - ...new Set( - transactions.list - .filter( - (x) => - (x.type === "transfer" && x.from === activeUser.username) || - (x.type === "delegate_vesting_shares" && x.delegator === activeUser.username) - ) - .map((x) => - x.type === "transfer" ? x.to : x.type === "delegate_vesting_shares" ? x.delegatee : "" - ) - .filter((x) => { - if (to.trim() === "") { - return true; - } - - return x.indexOf(to) !== -1; - }) - .reverse() - .slice(0, 5) - ) - ]; + const shortenedList = delegationOutList.slice(-10); + const recent = + mode === "undelegate" + ? shortenedList.filter((e) => e.symbol === asset).map((e) => e.to) + : [ + ...new Set( + transactions.list + .filter( + (x) => + (x.type === "transfer" && x.from === activeUser.username) || + (x.type === "delegate_vesting_shares" && x.delegator === activeUser.username) + ) + .map((x) => + x.type === "transfer" + ? x.to + : x.type === "delegate_vesting_shares" + ? x.delegatee + : "" + ) + .filter((x) => { + if (to.trim() === "") { + return true; + } + + return x.indexOf(to) !== -1; + }) + .reverse() + .slice(0, 5) + ) + ]; const suggestionProps = { header: _t("transfer.recent-transfers"), @@ -519,27 +753,21 @@ export class Transfer extends BaseComponent { const showTo = ["transfer", "delegate", "undelegate", "stake"].includes(mode); const showMemo = ["transfer"].includes(mode); - const delegateAccount = - delegationList && - delegationList.length > 0 && - delegationList!.find( - (item) => - (item as DelegateVestingShares).delegatee === to && - (item as DelegateVestingShares).delegator === activeUser.username - ); - const previousAmount = delegateAccount - ? Number( - formattedNumber( - vestsToHp(Number(parseAsset(delegateAccount!.vesting_shares).amount), hivePerMVests) - ) - ) - : ""; - - let balance: string | number = this.props.assetBalance; - if (previousAmount) { - balance = Number(balance) + previousAmount; - balance = Number(balance).toFixed(precision); - } + const delegateAccount = delegationOutList.find(function (entry: DelegationEntry) { + return entry.to === to; + }); + const previousAmount = delegateAccount ? delegateAccount.quantity : ""; + + let balance: string = formattedNumber( + (() => { + const balance: number = this.getBalance(); + if (previousAmount) { + return balance + previousAmount; + } + return balance; + })(), + { fractionDigits: precision, separators: false } + ); const titleLngKey = mode === "transfer" ? `${mode}-title` : `${mode}-hive-engine-title`; const subTitleLngKey = @@ -685,11 +913,19 @@ export class Transfer extends BaseComponent { placeholder={_t("transfer.amount-placeholder")} value={amount} onChange={this.amountChanged} - className={amount > balance && amountError ? "is-invalid" : ""} + className={ + Number(amount.replace(/,/g, "")) > Number(balance) && amountError + ? "is-invalid" + : "" + } autoFocus={mode !== "transfer"} /> - {asset} + {assets.length > 1 ? ( + + ) : ( + {asset} + )} @@ -703,7 +939,11 @@ export class Transfer extends BaseComponent { {": "} - {this.props.assetBalance} {asset} + {formattedNumber(this.getBalance(), { + maximumFractionDigits: precision, + minimumFractionDigits: precision + })}{" "} + {asset} {asset === "HP" && (
{_t("transfer.available-hp-hint")}
diff --git a/src/common/components/transfer/__snapshots__/index.spec.tsx.snap b/src/common/components/transfer/__snapshots__/index.spec.tsx.snap index 396586f3f8e..5c7804202b2 100644 --- a/src/common/components/transfer/__snapshots__/index.spec.tsx.snap +++ b/src/common/components/transfer/__snapshots__/index.spec.tsx.snap @@ -2941,6 +2941,380 @@ exports[`(7) Power up (4) Step 4 1`] = ` `; +exports[`(7) Power up FOODIE (1) Step 1 1`] = ` +
+
+
+
+ 1 +
+
+
+ Stake Hive as Power +
+
+ Influence tokens which give you more control over post payouts and allow you to earn on curation rewards. +
+
+
+
+
+ +
+
+
+ + @ + +
+ +
+
+
+
+ +
+
+
+
+ + @ + +
+ +
+
+
+
+
+ + +
+ +
+
+
+ + # + +
+ +
+
+
+ +
+
+
+ + Balance + : + + + 1.751 + + HIVE + +
+
+
+
+
+ +
+
+ +
+
+`; + +exports[`(7) Power up FOODIE (2) Step 2 1`] = ` +
+
+
+
+ 2 +
+
+
+ Confirm your transaction +
+
+ Please, check if everything is correct +
+
+
+
+
+
+ Stake Hive as Power +
+
+
+ +
+
+ + + +
+
+ +
+
+
+ 0.001 + + HIVE +
+ +
+
+ + + +
+
+
+
+`; + +exports[`(7) Power up FOODIE (4) Step 4 1`] = ` +
+
+
+
+ 4 +
+
+
+ Success +
+
+ Transaction successful +
+
+
+
+
0.001 HIVE staked from foo to bar", + } + } + /> +
+ + + +
+
+
+
+`; + exports[`(8) Delegate (1) Step 1 1`] = `
() => ({ fromNow: () => "in 5 days" @@ -42,7 +43,9 @@ const defProps = { setSigningKey: () => {}, fetchPoints: () => {}, updateWalletValues: () => {}, - onHide: () => {} + onHide: () => {}, + tokens: [], + assetBalance: 0 }; describe("(1) Transfer HIVE", () => { @@ -248,6 +251,49 @@ describe("(7) Power up", () => { }); }); +describe("(7) Power up FOODIE", () => { + const mode: TransferMode = "power-up"; + const asset: TransferAsset = "HIVE"; + const tokens: HiveEngineToken[] = [ + new HiveEngineToken({ + symbol: "FOODIE", + name: "Foodies Bee Hive", + icon: "https://files.peakd.com/file/peakd-hive/hive-120586/MEe37llw-FBH_plate_icon-noUtil.png", + precision: 5, + stakingEnabled: true, + delegationEnabled: true, + balance: "0", + stake: "0.134", + delegationsIn: "0", + delegationsOut: "0" + }) + ]; + + const props = { + ...defProps, + mode, + asset, + tokens + }; + + const component = TestRenderer.create(); + const instance: any = component.getInstance(); + + it("(1) Step 1", () => { + expect(component.toJSON()).toMatchSnapshot(); + }); + + it("(2) Step 2", () => { + instance.setState({ step: 2, to: "bar" }); + expect(component.toJSON()).toMatchSnapshot(); + }); + + it("(4) Step 4", () => { + instance.setState({ step: 4 }); + expect(component.toJSON()).toMatchSnapshot(); + }); +}); + describe("(8) Delegate", () => { const mode: TransferMode = "delegate"; const asset: TransferAsset = "HP"; @@ -336,3 +382,189 @@ describe("(10) Powering down", () => { expect(component.toJSON()).toMatchSnapshot(); }); }); + +// const tokens = [ +// { +// "symbol": "FQX", +// "name": "Fucks", +// "icon": "https://files.steempeak.com/file/steempeak/madmagazine/ugnZJiIL-phuqxray2.gif", +// "precision": 8, +// "stakingEnabled": false, +// "delegationEnabled": false, +// "balance": "1090.62697017", +// "stake": "0", +// "delegationsIn": "0", +// "delegationsOut": "0", +// "stakedBalance": "0" +// }, +// { +// "symbol": "POB", +// "name": "Proof of Brain", +// "icon": "https://i.postimg.cc/02vXm7Rb/poblogo.png", +// "precision": 8, +// "stakingEnabled": true, +// "delegationEnabled": true, +// "balance": "485.9438739", +// "stake": "94782.30057459", +// "delegationsIn": "0", +// "delegationsOut": "4.00000001", +// "stakedBalance": "94778.30057458" +// }, +// { +// "symbol": "ENGAGE", +// "name": "Engagement Token", +// "icon": "https://images.hive.blog/0x0/https://files.peakd.com/file/peakd-hive/abh12345/H6EINbDA-1.png", +// "precision": 3, +// "stakingEnabled": false, +// "delegationEnabled": false, +// "balance": "115", +// "stake": "0", +// "delegationsIn": "0", +// "delegationsOut": "0", +// "stakedBalance": "0" +// }, +// { +// "symbol": "CCC", +// "name": "Creative Coin", +// "icon": "https://steemitimages.com/640x0/https://cdn.steemitimages.com/DQmfJtyqNLx8gU6UD1hcHjmCv4kYAyxS5ZogzpFecNBGtCS/CCdiscordLogo.png", +// "precision": 5, +// "stakingEnabled": true, +// "delegationEnabled": true, +// "balance": "15.75905", +// "stake": "15.75905", +// "delegationsIn": "0", +// "delegationsOut": "0", +// "stakedBalance": "15.75905" +// }, +// { +// "symbol": "BPC", +// "name": "bilpcoin", +// "icon": "https://i.imgur.com/L9dv7Vz.png[/img]", +// "precision": 6, +// "stakingEnabled": true, +// "delegationEnabled": false, +// "balance": "13.692491", +// "stake": "13.692491", +// "delegationsIn": "0", +// "delegationsOut": "0", +// "stakedBalance": "13.692491" +// }, +// { +// { +// "symbol": "CTP", +// "name": "CTP Token", +// "icon": "https://clicktrackprofit.com/v2/images/ctp_icon.png", +// "precision": 3, +// "stakingEnabled": true, +// "delegationEnabled": true, +// "balance": "12.777", +// "stake": "0", +// "delegationsIn": "0", +// "delegationsOut": "0", +// "stakedBalance": "0" +// }, +// { +// "symbol": "LOLZ", +// "name": "LOLz Token", +// "icon": "https://lolztoken.com/wp-content/uploads/2022/02/emoji-happy.gif", +// "precision": 8, +// "stakingEnabled": true, +// "delegationEnabled": false, +// "balance": "8", +// "stake": "0", +// "delegationsIn": "0", +// "delegationsOut": "0", +// "stakedBalance": "0" +// }, +// { +// "symbol": "LEO", +// "name": "Leo", +// "icon": "https://media.giphy.com/media/eLXRoZZkWMAyqEpic0/giphy.gif", +// "precision": 3, +// "stakingEnabled": true, +// "delegationEnabled": true, +// "balance": "3.558", +// "stake": "0", +// "delegationsIn": "0", +// "delegationsOut": "0", +// "stakedBalance": "0" +// }, +// { +// "symbol": "BST", +// "name": "BULLSHIT Token", +// "icon": "https://images.hive.blog/DQmdAYfg3L9PpWwReweDWoWoyarnj8J3idxsLRurUMH5nAU/Element%206normal.png", +// "precision": 0, +// "stakingEnabled": false, +// "delegationEnabled": false, +// "balance": "3", +// "stake": "0", +// "delegationsIn": "0", +// "delegationsOut": "0", +// "stakedBalance": "0" +// }, +// { +// "symbol": "PIZZA", +// "name": "PIZZA", +// "icon": "https://i.imgur.com/TE19kib.png", +// "precision": 2, +// "stakingEnabled": true, +// "delegationEnabled": false, +// "balance": "2", +// "stake": "0.8", +// "delegationsIn": "0", +// "delegationsOut": "0", +// "stakedBalance": "0.8" +// }, +// { +// "symbol": "LIST", +// "name": "Hivelist Token", +// "icon": "https://i.postimg.cc/3Jccv5Yb/hivelist-token-gear-new.png", +// "precision": 8, +// "stakingEnabled": true, +// "delegationEnabled": true, +// "balance": "0.98387449", +// "stake": "0.98387449", +// "delegationsIn": "0", +// "delegationsOut": "0", +// "stakedBalance": "0.98387449" +// }, +// { +// "symbol": "LERN", +// "name": "LERN Token", +// "icon": "https://img1.wsimg.com/isteam/ip/ce73b62b-4324-43ab-ab62-739e704b8e20/logo/59c9b77d-a32e-422f-880c-44d33a833d0a.jpg/:/rs=h:160,cg:true,m/qt=q:95", +// "precision": 8, +// "stakingEnabled": true, +// "delegationEnabled": true, +// "balance": "0.96811612", +// "stake": "0.96811612", +// "delegationsIn": "0", +// "delegationsOut": "0", +// "stakedBalance": "0.96811612" +// }, +// { +// "symbol": "GAMER", +// "name": "Krypto Gamers Token", +// "icon": "https://cdn.steemitimages.com/DQmc39SLQfnkzC9SE8uZXjV3optpu2GwzX1g7d7igSNW8Dn/logo1.png", +// "precision": 4, +// "stakingEnabled": true, +// "delegationEnabled": false, +// "balance": "0.625", +// "stake": "0", +// "delegationsIn": "0", +// "delegationsOut": "0", +// "stakedBalance": "0" +// }, +// { +// "symbol": "FOODIE", +// "name": "Foodies Bee Hive", +// "icon": "https://files.peakd.com/file/peakd-hive/hive-120586/MEe37llw-FBH_plate_icon-noUtil.png", +// "precision": 5, +// "stakingEnabled": true, +// "delegationEnabled": true, +// "balance": "0", +// "stake": "0.134", +// "delegationsIn": "0", +// "delegationsOut": "0", +// "stakedBalance": "0.134" +// } +// ]; diff --git a/src/common/components/transfer/index.tsx b/src/common/components/transfer/index.tsx index 847f865af1f..6fae0d07dcf 100644 --- a/src/common/components/transfer/index.tsx +++ b/src/common/components/transfer/index.tsx @@ -73,6 +73,7 @@ import { arrowRightSvg } from "../../img/svg"; import formattedNumber from "../../util/formatted-number"; import activeUser from "../../store/active-user"; import { dateToFullRelative } from "../../helper/parse-date"; +import { base } from "../../constants/defaults.json"; export type TransferMode = | "transfer" diff --git a/src/common/components/wallet-hive-engine-detail/_index.scss b/src/common/components/wallet-hive-engine-detail/_index.scss new file mode 100644 index 00000000000..b0733b51be5 --- /dev/null +++ b/src/common/components/wallet-hive-engine-detail/_index.scss @@ -0,0 +1,25 @@ +.wallet-hive { + @include wallet; + + .next-power-down { + align-items: center; + box-sizing: content-box; + display: flex; + padding: 10px; + justify-content: center; + text-align: center; + + @media (min-width: $lg-break) { + justify-content: flex-start; + text-align: initial; + } + + @include themify(day) { + color: $steel-grey; + } + + @include themify(night) { + color: $metallic-blue; + } + } +} diff --git a/src/common/components/wallet-hive-engine-detail/index.spec.tsx b/src/common/components/wallet-hive-engine-detail/index.spec.tsx new file mode 100644 index 00000000000..83048895ad4 --- /dev/null +++ b/src/common/components/wallet-hive-engine-detail/index.spec.tsx @@ -0,0 +1,71 @@ +import React from "react"; +import { act } from "react-dom/test-utils"; + +import WalletHiveEngineDetail from "./index"; + +import TestRenderer from "react-test-renderer"; + +import { initialState as transactionsInitialState } from "../../store/transactions/index"; +import { createBrowserHistory } from "history"; +import { + globalInstance, + dynamicPropsIntance1, + activeUserInstance, + allOver, + fullAccountInstance +} from "../../helper/test-helper"; +import { StaticRouter } from "react-router-dom"; +import HiveEngineToken, { + HiveEngineTokenEntryDelta, + isUndefined +} from "../../helper/hive-engine-wallet"; +import { FullAccount } from "../../store/accounts/types"; + +const account: FullAccount = { + ...fullAccountInstance, + name: "user1" +}; + +const defProps = { + history: createBrowserHistory(), + global: globalInstance, + dynamicProps: dynamicPropsIntance1, + activeUser: { ...activeUserInstance }, + account, + transactions: transactionsInitialState, + signingKey: "", + addAccount: () => {}, + updateActiveUser: () => {}, + setSigningKey: () => {}, + fetchPoints: () => {}, + updateWalletValues: () => {}, + clearToken: () => {}, + openTransferDialog: () => {}, + closeTransferDialog: () => {}, + modifyTokenValues: () => {}, + tokenName: "POB", + hiveEngineToken: new HiveEngineToken({ + symbol: "POB", + name: "Proof of Brain", + icon: "https://images.hive.blog/DQmebUEYTFmi2g4pqExAjaQrv9E9nzNtuEDbttRBRShkVYy/brain.png", + precision: 8, + stakingEnabled: true, + delegationEnabled: true, + balance: "0.0", + stake: "8.10882833", + delegationsIn: "0", + delegationsOut: "0" + }), + delegationList: [] +}; + +it("(1) Render an empty list when no tokens found", async () => { + const renderer = await TestRenderer.create( + + + + ); + await allOver(); + act(() => {}); + expect(renderer.toJSON()).toMatchSnapshot(); +}); diff --git a/src/common/components/wallet-hive-engine-detail/index.tsx b/src/common/components/wallet-hive-engine-detail/index.tsx new file mode 100644 index 00000000000..3bdd3849197 --- /dev/null +++ b/src/common/components/wallet-hive-engine-detail/index.tsx @@ -0,0 +1,573 @@ +import React from "react"; + +import { Global } from "../../store/global/types"; +import { Account } from "../../store/accounts/types"; +import { DynamicProps } from "../../store/dynamic-props/types"; +import { ActiveUser } from "../../store/active-user/types"; + +import BaseComponent from "../base"; +import HiveEngineToken, { + HiveEngineTokenEntryDelta, + isUndefined +} from "../../helper/hive-engine-wallet"; +import LinearProgress from "../linear-progress"; +import { OverlayTrigger, Tooltip } from "react-bootstrap"; +import WalletMenu from "../wallet-menu"; +import { + claimRewards, + getHiveEngineTokenBalances, + getUnclaimedRewards, + getTokenDelegations, + TokenStatus, + DelegationEntry, + Unstake, + getPendingUnstakes, + getFineTransactions, + getCoarseTransactions +} from "../../api/hive-engine"; +import { proxifyImageSrc } from "@ecency/render-helper"; +import { + informationVariantSvg, + plusCircle, + transferOutlineSvg, + lockOutlineSvg, + unlockOutlineSvg, + delegateOutlineSvg, + undelegateOutlineSvg, + hiveEngineSvg +} from "../../img/svg"; +import { error, success } from "../feedback"; +import { formatError } from "../../api/operations"; +import formattedNumber from "../../util/formatted-number"; +import DropDown from "../dropdown"; +import DelegatedVesting from "../delegated-vesting-hive-engine"; +import ReceivedVesting from "../received-vesting"; +import { + HEFineTransactionToHiveTransactions, + HEToHTransaction +} from "../../store/transactions/convert"; +import TransactionList from "../transactions"; +import { _t } from "../../i18n"; +import FormattedCurrency from "../formatted-currency"; +import { History, Location } from "history"; +import { TransferMode } from "../transfer-he"; +import { Modal } from "react-bootstrap"; +import { + OperationGroup, + Transactions, + Transaction, + HEFineTransaction, + HECoarseTransaction +} from "../../store/transactions/types"; +interface TokenProps { + symbol: string; + name: "Payment Token"; + icon: string; + precision: number; + stakingEnabled: true; + delegationEnabled: boolean; + balance: number; + stake: number; + delegationsIn: number; + delegationsOut: number; + stakedBalance: number; +} + +type TransferAsset = string; + +interface Props { + history: History; + global: Global; + dynamicProps: DynamicProps; + account: Account; + activeUser: ActiveUser | null; + transactions: Transactions; + signingKey: string; + addAccount: (data: Account) => void; + updateActiveUser: (data?: Account) => void; + setSigningKey: (key: string) => void; + fetchPoints: (username: string, type?: number) => void; + updateWalletValues: () => void; + clearToken: () => void; + openTransferDialog: (a: TransferMode, b: string, c: number) => void; + closeTransferDialog: () => void; + modifyTokenValues: (delta: HiveEngineTokenEntryDelta) => void; + tokenName: string; + hiveEngineToken: HiveEngineToken; + delegationList: Array; +} + +interface State { + loading: boolean; + nextPowerDown: number; + delegatedList: boolean; + receivedList: boolean; + transactions: Transactions; + converting: number; +} + +export class WalletHiveEngine extends BaseComponent { + state: State = { + loading: true, + nextPowerDown: 0, + delegatedList: false, + receivedList: false, + transactions: { list: [], loading: false, group: "" }, + converting: 0 + }; + componentDidMount() { + const { tokenName, account } = this.props; + this.fetchTokenUnstakes(); + this.fetchConvertingAmount(); + this.fetchHETransactions(tokenName, account.name, ""); + } + keepTransaction(group: OperationGroup | "", tx: Transaction): boolean { + switch (group) { + case "transfers": + return [ + "transfers", + "transfer_to_vesting", + "transfer_to_saving", + "withdraw_vesting", + "proposal-pay", + "cancel_transfer_from_savings", + "producer_reward", + "claim_reward_balance" + ].includes(tx.type); + case "interests": + return tx.type === "interest"; + case "rewards": + return tx.type.endsWith("_reward") || tx.type == "tokens_issue"; + case "market-orders": + return tx.type.startsWith("market_") || tx.type === "fill_order"; + case "stake-operations": + return ( + "transfer_to_saving,withdraw_vesting,tokens_unstakeDone,tokens_stake,withdraw_vesting," + + "return_vesting_delegation,tokens_unstakeStart,tokens_CancelUnstake," + + "tokens_unstake,tokens_undelegateDone,tokens_delegate," + + "tokens_undelegateStart" + ) + .split(/,/) + .includes(tx.type); + } + return true; + } + compareTransactions(a: Transaction, b: Transaction) { + return a.timestamp > b.timestamp ? -1 : 1; + } + handleFineTransactions(fts: Array) { + const { transactions } = this.state; + const { list } = transactions; + const ntxs = [...HEFineTransactionToHiveTransactions(fts), ...list] + .filter(this.keepTransaction.bind(this, transactions.group)) + .sort(this.compareTransactions); + this.stateSet({ + transactions: { list: ntxs, loading: false, group: transactions.group } + }); + } + handleCoarseTransactions(group: OperationGroup | "", cts: Array) { + const { transactions } = this.state; + try { + const txs: Array = cts + .map((t) => HEToHTransaction(t)) + .filter((x) => x != null) + // @ts-ignore + .filter(this.keepTransaction.bind(this, transactions.group)); + const { list } = transactions; + this.stateSet({ + transactions: { list: txs, loading: false, group: transactions.group } + }); + } catch (e) { + error("Unknown transaction type error."); + console.log(e); + this.stateSet({ + transactions: { + list: transactions.list, + loading: false, + group: transactions.group + } + }); + } + } + fetchHETransactions = (symbol: string, name: string, group?: OperationGroup | "") => { + const { transactions } = this.state; + const { list } = transactions; + this.stateSet({ + transactions: { list: [], loading: true, group: group || "" } + }); + if (group === "rewards") + getFineTransactions(symbol, name, group === "rewards" ? 200 : 10, 0) + .then(this.handleFineTransactions.bind(this)) + .catch((e) => { + console.log(e); + }); + else + getCoarseTransactions(name, 400, symbol, 0) + .then(this.handleCoarseTransactions.bind(this, group ? group : "")) + .catch(console.log); + }; + fetchConvertingAmount = () => { + // const {account} = this.props; + // getConversionRequests(account.name).then(r => { + // if (r.length === 0) { + // return; + // } + // let converting = 0; + // r.forEach(x => { + // converting += parseAsset(x.amount).amount; + // }); + // this.stateSet({converting}); + // }); + }; + toggleDelegatedList = () => { + const { delegatedList } = this.state; + this.stateSet({ delegatedList: !delegatedList }); + }; + toggleReceivedList = () => { + const { receivedList } = this.state; + this.stateSet({ receivedList: !receivedList }); + }; + fetchTokenUnstakes = async () => { + const { account, tokenName, hiveEngineToken } = this.props; + return getPendingUnstakes(account.name, tokenName).then((items) => { + items = items.filter((item) => { + const { quantityLeft } = item; + return quantityLeft != "0"; + }); + let nextPowerDown = 0; + if (hiveEngineToken && items.length > 0) { + const { precision } = hiveEngineToken; + const { numberTransactionsLeft, quantityLeft } = items[0]; + const quotient: number = + parseFloat(numberTransactionsLeft as string) / parseFloat(quantityLeft as string); + if (!isNaN(quotient)) { + nextPowerDown = quotient; + } + } + this.setState({ nextPowerDown, loading: false }); + }); + }; + + render() { + const { + global, + dynamicProps, + account, + activeUser, + tokenName, + openTransferDialog, + closeTransferDialog, + delegationList, + hiveEngineToken, + history + } = this.props; + const { loading, nextPowerDown, delegatedList, receivedList, transactions } = this.state; + const isMyPage = activeUser && activeUser.username === account.name; + + if (!account.__loaded) { + return null; + } + + if (isUndefined(hiveEngineToken)) { + return null; + } + + const estimatedValue: number | false = false; + const { precision, icon, stakingEnabled, delegationEnabled } = + hiveEngineToken as HiveEngineToken; + // ts-ignore + const [balance, stake, stakedBalance, delegationsIn, delegationsOut] = [ + hiveEngineToken.balance, + hiveEngineToken.stake, + hiveEngineToken.stakedBalance, + hiveEngineToken.delegationsIn, + hiveEngineToken.delegationsOut + ]; + + const fetchHETransactions = () => { + return []; + }; + + return loading ? ( + + ) : ( +
+
+
+
+
+
{tokenName}
+
+ {_t("wallet.token-description", { hiveEngineToken })} +
+
+
+
+ {(() => { + if (isMyPage) { + const dropDownConfig = { + history: null, + label: "", + items: [ + { + label: _t("wallet.transfer"), + onClick: () => { + openTransferDialog("transfer", tokenName, balance); + } + }, + ...(stakingEnabled + ? [ + { + label: _t("wallet.power-up"), + onClick: () => { + openTransferDialog("stake", tokenName, balance); + } + } + ] + : []), + { + label: "Trade at LeoDex", + onClick: () => { + window.open(`https://leodex.io/market/${tokenName}`, "leodex"); + } + }, + { + label: "Trade at TribalDex", + onClick: () => { + window.open("https://tribaldex.com/trade/" + tokenName, "tribaldex"); + } + }, + { + label: `Trade at HiveEngine`, + onClick: () => { + window.open( + `https://hive-engine.com/?p=market&t=${tokenName}`, + "hiveEngineDex" + ); + } + } + ] + }; + return ( +
+ +
+ ); + } + return null; + })()} + + {formattedNumber(balance, { + maximumFractionDigits: precision, + minimumFractionDigits: 0, + suffix: tokenName + })} + +
+
+
+ + {(stake || stakedBalance || stakingEnabled) && ( +
+
+
{_t("wallet.staked", hiveEngineToken)}
+
+ {_t("wallet.staked-description", hiveEngineToken)} +
+
+
+
+ {(() => { + if (isMyPage) { + const dropDownConfig = { + history: null, + label: "", + items: [ + { + label: _t("wallet.delegate"), + onClick: () => { + openTransferDialog("delegate", tokenName, stakedBalance); + } + }, + { + label: _t("wallet.power-down"), + onClick: () => { + openTransferDialog("unstake", tokenName, stakedBalance); + } + } + ] + }; + return ( +
+ +
+ ); + } + return null; + })()} + {formattedNumber(stake, { + suffix: tokenName, + fractionDigits: 0, + maximumFractionDigits: precision + })} +
+ {delegationsOut > 0 && ( +
+ + {_t("wallet.token-power-delegated", hiveEngineToken)} + + } + > + + {formattedNumber(delegationsOut, { + prefix: "-", + suffix: tokenName, + fractionDigits: precision + })} + + +
+ )} + {delegationEnabled && + (() => { + if (delegationsIn <= 0) { + return null; + } + const strReceived = formattedNumber(delegationsIn, { + prefix: "+", + suffix: tokenName, + fractionDigits: precision + }); + + return ( +
+ +
{_t("wallet.token-power-received", hiveEngineToken)}
+ + } + > + {strReceived} +
+
+ ); + })()} + {nextPowerDown !== 0 && ( +
+
{_t("wallet.next-power-down-amount")}
+ + {formattedNumber(nextPowerDown, { + prefix: "-", + suffix: tokenName, + fractionDigits: precision + })} + +
+ )} + {(delegationsOut > 0 || delegationsIn > 0) && ( +
+ + {_t("wallet.hive-power-total")} + + } + > + + {formattedNumber(stakedBalance, { + prefix: "=", + suffix: tokenName, + fractionDigits: precision + })} + + + + {_t("wallet.hive-power-total")} + + {formattedNumber(stakedBalance, { prefix: "=", suffix: tokenName })} + + +
+ )} +
+
+ )} + {TransactionList({ + global, + dynamicProps, + fetchTransactions: this.fetchHETransactions.bind(this, tokenName), + history: history, + transactions: transactions, + account: account + })} +
+
+ {this.state.delegatedList && ( + + )} + {this.state.receivedList && ( + + )} +
+ ); + } +} + +export default (p: Props) => { + const props = { + history: p.history, + global: p.global, + dynamicProps: p.dynamicProps, + account: p.account, + activeUser: p.activeUser, + transactions: p.transactions, + signingKey: p.signingKey, + addAccount: p.addAccount, + updateActiveUser: p.updateActiveUser, + setSigningKey: p.setSigningKey, + updateWalletValues: p.updateWalletValues, + fetchPoints: p.fetchPoints, + tokenName: p.tokenName, + clearToken: p.clearToken, + openTransferDialog: p.openTransferDialog, + closeTransferDialog: p.closeTransferDialog, + modifyTokenValues: p.modifyTokenValues, + hiveEngineToken: p.hiveEngineToken, + delegationList: p.delegationList + }; + + return ( + + + + + + + ); +}; diff --git a/src/common/components/wallet-hive-engine/__snapshots__/index.spec.tsx.snap b/src/common/components/wallet-hive-engine/__snapshots__/index.spec.tsx.snap index f51f1ce6ced..32dd3e588cd 100644 --- a/src/common/components/wallet-hive-engine/__snapshots__/index.spec.tsx.snap +++ b/src/common/components/wallet-hive-engine/__snapshots__/index.spec.tsx.snap @@ -264,7 +264,12 @@ exports[`(2) Render with some hive engine tokens 1`] = ` onError={[Function]} src="https://images.ecency.com/p/3W72119s5BjVs3HydwUQEFR8AX8qeAgo5bzGeBm1Z97PoTy3MZXBcU4dHbscesjnrTfYqRnK5SzfhSYmuC72yxZ4h1S8WRzei3xyiS6WMKQuEqhbYBH45L.png?format=match&mode=fit" /> - PIZZA +
- POB +
- 0.00883586 POB + 0.008,835,86 POB
@@ -586,7 +598,12 @@ exports[`(3) Render with an unclaimed rewards 1`] = ` onError={[Function]} src="https://images.ecency.com/p/3W72119s5BjVs3HydwUQEFR8AX8qeAgo5bzGeBm1Z97PoTy3MZXBcU4dHbscesjnrTfYqRnK5SzfhSYmuC72yxZ4h1S8WRzei3xyiS6WMKQuEqhbYBH45L.png?format=match&mode=fit" /> - PIZZA +
- POB +
({ + getTokenDelegations: () => + new Promise((resolve) => { + resolve([]); + }), getHiveEngineTokenBalances: () => new Promise((resolve) => { if (MOCK_MODE === 1) { @@ -120,6 +124,7 @@ const account: FullAccount = { }; const defProps = { + history: createBrowserHistory(), global: globalInstance, dynamicProps: dynamicPropsIntance1, activeUser: { ...activeUserInstance }, diff --git a/src/common/components/wallet-hive-engine/index.tsx b/src/common/components/wallet-hive-engine/index.tsx index 794a051d6b5..6ecf03140db 100644 --- a/src/common/components/wallet-hive-engine/index.tsx +++ b/src/common/components/wallet-hive-engine/index.tsx @@ -8,21 +8,23 @@ import { OperationGroup, Transactions } from "../../store/transactions/types"; import { ActiveUser } from "../../store/active-user/types"; import BaseComponent from "../base"; -import HiveEngineToken from "../../helper/hive-engine-wallet"; +import HiveEngineToken, { HiveEngineTokenEntryDelta } from "../../helper/hive-engine-wallet"; import LinearProgress from "../linear-progress"; import { Button, OverlayTrigger, Tooltip } from "react-bootstrap"; import WalletMenu from "../wallet-menu"; +import Transfer, { TransferMode } from "../transfer-he"; import { SortEngineTokens } from "../sort-hive-engine-tokens"; import { EngineTokensEstimated } from "../engine-tokens-estimated"; -import Transfer, { TransferMode } from "../transfer-he"; import { error, success } from "../feedback"; import { claimRewards, getHiveEngineTokenBalances, getUnclaimedRewards, - TokenStatus, - getMetrics + getTokenDelegations, + DelegationEntry, + getMetrics, + TokenStatus } from "../../api/hive-engine"; import { @@ -39,10 +41,28 @@ import { import { formatError } from "../../api/operations"; import formattedNumber from "../../util/formatted-number"; +import { History } from "history"; +import DropDown from "../dropdown"; +import WalletHiveEngineDetail from "../wallet-hive-engine-detail"; +interface TokenProps { + symbol: string; + name: "Payment Token"; + icon: string; + precision: number; + stakingEnabled: true; + delegationEnabled: boolean; + balance: number; + stake: number; + delegationsIn: number; + delegationsOut: number; + stakedBalance: number; +} + import { _t } from "../../i18n"; import { HiveEngineChart } from "../hive-engine-chart"; interface Props { + history: History; global: Global; dynamicProps: DynamicProps; account: Account; @@ -60,6 +80,7 @@ interface State { tokens: HiveEngineToken[]; utokens: HiveEngineToken[]; rewards: TokenStatus[]; + delegationList: Array; loading: boolean; claiming: boolean; claimed: boolean; @@ -80,6 +101,7 @@ export class WalletHiveEngine extends BaseComponent { claimed: false, transfer: false, transferMode: null, + delegationList: [], transferAsset: null, assetBalance: 0, allTokens: null @@ -91,12 +113,22 @@ export class WalletHiveEngine extends BaseComponent { this._isMounted && this.fetch(); this._isMounted && this.fetchUnclaimedRewards(); this._isMounted && this.priceChangePercent(); + this._isMounted && this.fetchDelegationList(); + this.modifyTokenValues = this.modifyTokenValues.bind(this); + this.clearToken = this.clearToken.bind(this); + this.setActiveToken = this.setActiveToken.bind(this); + this.openTransferDialog = this.openTransferDialog.bind(this); + this.closeTransferDialog = this.closeTransferDialog.bind(this); } componentWillUnmount() { this._isMounted = false; } + clearToken = () => { + this.stateSet({ transferAsset: null }); + }; + sortTokensInAscending: any = () => { const inAscending = this.state.tokens.sort((a: any, b: any) => { if (a.symbol > b.symbol) return 1; @@ -213,7 +245,7 @@ export class WalletHiveEngine extends BaseComponent { }; closeTransferDialog = () => { - this.stateSet({ transfer: false, transferMode: null, transferAsset: null }); + this.stateSet({ transfer: false, transferMode: null }); }; fetch = async () => { @@ -234,6 +266,17 @@ export class WalletHiveEngine extends BaseComponent { this.setState({ loading: false }); }; + fetchDelegationList = async () => { + const { account } = this.props; + const { name } = account; + // This causes an error message but it does exist. + return getTokenDelegations(name).then((items) => { + if (this._isMounted) { + this.setState({ delegationList: items }); + } + }); + }; + sort = (items: HiveEngineToken[]) => items.sort((a: HiveEngineToken, b: HiveEngineToken) => { if (a.balance !== b.balance) { @@ -286,9 +329,39 @@ export class WalletHiveEngine extends BaseComponent { }); }; + setActiveToken(newToken: string, e: any) { + if (e) { + // @ts-ignore + e.preventDefault(); + } + this.setState({ transferAsset: newToken }); + } + + modifyTokenValues(delta: HiveEngineTokenEntryDelta) { + const { tokens } = this.state; + const { symbol, balanceDelta, stakeDelta, delegationsInDelta, delegationsOutDelta } = delta; + let newTokens = [...tokens]; + for (const t of newTokens) { + if (t.symbol !== symbol) continue; + t.modify(delta); + this.setState({ tokens: newTokens }); + break; + } + } + render() { const { global, dynamicProps, account, activeUser } = this.props; - const { rewards, tokens, loading, claiming, claimed, utokens, allTokens } = this.state; + const { + rewards, + tokens, + loading, + claiming, + claimed, + utokens, + transferAsset, + delegationList, + allTokens + } = this.state; const hasUnclaimedRewards = rewards.length > 0; const hasMultipleUnclaimedRewards = rewards.length > 1; const isMyPage = activeUser && activeUser.username === account.name; @@ -299,6 +372,30 @@ export class WalletHiveEngine extends BaseComponent { return null; } + const tokenMenu = ( + <> + {tokens.map((b, i) => { + const fallbackImage = require("../../img/noimage.svg"); + const imageSrc = proxifyImageSrc(b.icon, 0, 0, global?.canUseWebp ? "webp" : "match"); + return ( + + {b.symbol} { + const target = e.target as HTMLImageElement; + target.src = fallbackImage; + }} + /> + + ); + })} + + ); + const hiveEngineToken: HiveEngineToken | undefined = tokens.find( + (t) => t.symbol === transferAsset + ); return (
@@ -396,13 +493,28 @@ export class WalletHiveEngine extends BaseComponent {
)} -
+
{loading ? (
) : tokens.length === 0 ? (
{_t("wallet-engine.no-results")}
+ ) : transferAsset ? ( +
+ {hiveEngineToken && ( + + )} +
) : (
{tokens.map((b, i) => { @@ -413,7 +525,7 @@ export class WalletHiveEngine extends BaseComponent { global?.canUseWebp ? "webp" : "match" ); const fallbackImage = require("../../img/noimage.svg"); - + const setThisActiveToken = this.setActiveToken.bind(this, b.symbol); return (
@@ -426,7 +538,9 @@ export class WalletHiveEngine extends BaseComponent { target.src = fallbackImage; }} /> - {b.symbol} +
{!global?.isMobile && ( @@ -471,7 +585,10 @@ export class WalletHiveEngine extends BaseComponent { } >
- + {informationVariantSvg}
@@ -680,6 +797,9 @@ export class WalletHiveEngine extends BaseComponent { asset={this.state.transferAsset!} onHide={this.closeTransferDialog} assetBalance={this.state.assetBalance} + tokens={tokens} + modifyTokenValues={this.modifyTokenValues} + delegationList={this.state.delegationList} /> )}
@@ -689,6 +809,7 @@ export class WalletHiveEngine extends BaseComponent { export default (p: Props) => { const props = { + history: p.history, global: p.global, dynamicProps: p.dynamicProps, account: p.account, diff --git a/src/common/components/withdraw-routes/index.tsx b/src/common/components/withdraw-routes/index.tsx index 4818428466c..47db90aaa8c 100644 --- a/src/common/components/withdraw-routes/index.tsx +++ b/src/common/components/withdraw-routes/index.tsx @@ -25,6 +25,7 @@ import { import { deleteForeverSvg } from "../../img/svg"; import { handleInvalid, handleOnInput } from "../../util/input-util"; +import { base } from "../../constants/defaults.json"; interface Props { global: Global; diff --git a/src/common/constants/defaults.json b/src/common/constants/defaults.json index be371d14b80..ef54b422d1d 100644 --- a/src/common/constants/defaults.json +++ b/src/common/constants/defaults.json @@ -9,6 +9,8 @@ }, "locale": "en-US", "base": "https://ecency.com", + "appURL": "https://ecency.com", + "testnet": false, "imageServer": "https://images.ecency.com", "nwsServer": "wss://enotify.esteem.app", "name": "Ecency", diff --git a/src/common/constants/servers-testnet.json b/src/common/constants/servers-testnet.json new file mode 100644 index 00000000000..57dfe107091 --- /dev/null +++ b/src/common/constants/servers-testnet.json @@ -0,0 +1 @@ +["https://testnet.openhive.network", "ws://testnet.openhive.network"] diff --git a/src/common/euphoria/index.ts b/src/common/euphoria/index.ts new file mode 100644 index 00000000000..a7e966915a2 --- /dev/null +++ b/src/common/euphoria/index.ts @@ -0,0 +1,21 @@ +export function isntString(v: any): boolean { + return typeof v !== "string"; +} + +export function isntNumber(v: any): boolean { + return typeof v !== "number" && !isNaN(v); +} + +export function isDefined(v: any): boolean { + return v !== undefined; +} + +export type TypeSpecification = string | number | boolean; + +export function typeCheck(expr: boolean, typeSpec: string, varName: string, value: any) { + if (expr) { + throw Error( + `Typecheck failure: ${varName} is not a/an ${typeSpec} it is ${JSON.stringify(value)}` + ); + } +} diff --git a/src/common/helper/amount-format-check.ts b/src/common/helper/amount-format-check.ts index 96b037ef256..bfae16096de 100644 --- a/src/common/helper/amount-format-check.ts +++ b/src/common/helper/amount-format-check.ts @@ -1 +1 @@ -export default (v: string) => /^\d+(\.\d+)?$/.test(v); +export default (v: string) => /^\d{1,3}(,?\d{3})*(\.(\d{3},?)*\d{1,3})?$/.test(v); diff --git a/src/common/helper/hive-engine-wallet.ts b/src/common/helper/hive-engine-wallet.ts index e81952accb2..187cf4d448e 100644 --- a/src/common/helper/hive-engine-wallet.ts +++ b/src/common/helper/hive-engine-wallet.ts @@ -13,6 +13,17 @@ interface Props { delegationsOut: string; } +export interface HiveEngineTokenDelta { + balanceDelta?: number; + stakeDelta?: number; + delegationsInDelta?: number; + delegationsOutDelta?: number; +} + +export interface HiveEngineTokenEntryDelta extends HiveEngineTokenDelta { + symbol: string; +} + export default class HiveEngineToken { symbol: string; name?: string; @@ -76,6 +87,15 @@ export default class HiveEngineToken { return formattedNumber(this.stakedBalance, { fractionDigits: this.precision }); }; + modify(delta: HiveEngineTokenDelta) { + const { balanceDelta, stakeDelta, delegationsInDelta, delegationsOutDelta } = delta; + this.balance += balanceDelta ?? 0; + this.stake += stakeDelta ?? 0; + this.delegationsIn += delegationsInDelta ?? 0; + this.delegationsOut += delegationsOutDelta ?? 0; + this.stakedBalance = this.stake + this.delegationsIn - this.delegationsOut; + } + balanced = (): string => { if (this.balance < 0.0001) { return this.balance.toString(); @@ -84,3 +104,7 @@ export default class HiveEngineToken { return formattedNumber(this.balance, { fractionDigits: this.precision }); }; } + +export function isUndefined(x: any): x is undefined { + return x === undefined; +} diff --git a/src/common/helper/hive-signer.ts b/src/common/helper/hive-signer.ts index 8f9d4efca40..bc6e5983fda 100644 --- a/src/common/helper/hive-signer.ts +++ b/src/common/helper/hive-signer.ts @@ -1,10 +1,14 @@ import { b64uEnc } from "../util/b64"; +import { appURL } from "../constants/defaults.json"; -export const getAuthUrl = (app: string, redir: string = `${window.location.origin}/auth`) => { +export const getAuthUrl = ( + hsClientId: string, + redir: string = `${window.location.origin}/auth` +) => { const scope = "vote,comment,delete_comment,comment_options,custom_json,claim_reward_balance,offline"; - return `https://hivesigner.com/oauth2/authorize?client_id=${app}&redirect_uri=${encodeURIComponent( + return `https://hivesigner.com/oauth2/authorize?client_id=${hsClientId}&redirect_uri=${encodeURIComponent( redir )}&response_type=code&scope=${encodeURIComponent(scope)}`; }; @@ -76,7 +80,7 @@ export const buildHotSignUrl = ( ): any => { const _params = { ...params, - redirect_uri: `https://ecency.com/${redirect}` + redirect_uri: `${appURL}/${redirect}` }; const queryString = new URLSearchParams(_params).toString(); diff --git a/src/common/i18n/locales/en-US.json b/src/common/i18n/locales/en-US.json index b2c67eb92e3..412b8bfbb9e 100644 --- a/src/common/i18n/locales/en-US.json +++ b/src/common/i18n/locales/en-US.json @@ -644,6 +644,12 @@ "updated": "Preference updated!" }, "wallet": { + "staked": "staked {{symbol}}", + "token-power-delegated": "staked {{symbol}} delegated to other users", + "token-power-received": "staked {{symbol}} delegated to you by other users", + "token-description": "liquid {{symbol}} tokens that can be transfered at any time", + "staked-description": "staked tokens that must be unstaked for transfers but allow influence over the token distribution", + "othertokens": "Other Hive Engine tokens", "unclaimed-rewards": "Unclaimed rewards", "claim-reward-balance": "Claim reward balance", "claim-reward-balance-ok": "Reward balance claimed", @@ -958,6 +964,7 @@ "success-sub-title": "Transaction successful" }, "transactions": { + "type-limit_order_cancel": "Limit Order Cancelled", "title": "History", "group-all": "All History", "group-transfers": "Transfers", @@ -973,6 +980,41 @@ "type-transfer_to_vesting": "Power up", "type-withdraw_vesting": "Power down", "type-set_withdraw_vesting_route": "Withdraw route changed", + "type-curation_reward": "Curation reward", + "type-collateralized_convert": "Collateralized convert", + "type-author_reward": "Author reward", + "type-comment_benefactor_reward": "Comment benefactor reward", + "type-claim_reward_balance": "Claim rewards", + "type-transfer": "Transfer", + "type-tokens_issue": "Tokens issued", + "type-transfer_to_vesting": "Transfer to vesting", + "type-withdraw_vesting": "Withdraw vesting", + "type-transfer_to_vesting": "Power up", + "type-withdraw_vesting": "Power down", + "type-market_sell": "Market Sell", + "type-market_buy": "Market Buy", + "type-tokens_unstake": "Unstaked tokens", + "type-tokens_CancelUnstake": "Cancel withdrawal of Stake", + "type-tokens_issue": "Tokens issued", + "type-tokens_unstakeStart": "Started withdrawal from vesting", + "type-tokens_unstakeDone": "Withdrawal from vesting completed", + "type-tokens_undelegateDone": "Staked tokens delegation removed", + "type-tokens_delegate": "Staked tokens delegated", + "type-market_placeOrder": "Order placed", + "type-market_closeOrder": "Order closed", + "type-market_expireOrder": "Order expired", + "type-market_cancel": "Order cancelled", + "type-curation_reward": "Curation reward", + "type-collateralized_convert": "Collateralized convert", + "type-author_reward": "Author reward", + "type-comment_benefactor_reward": "Comment benefactor reward", + "type-claim_reward_balance": "Claim rewards", + "type-transfer": "Transfer", + "type-tokens_issue": "Tokens issued", + "type-transfer_to_vesting": "Transfer to vesting", + "type-withdraw_vesting": "Withdraw vesting", + "type-transfer_to_vesting": "Power up", + "type-withdraw_vesting": "Power down", "type-fill_vesting_withdraw": "Partial Power down occurred", "type-fill_order": "Open trade filled", "type-producer_reward": "Producer reward", @@ -998,8 +1040,14 @@ "type-update_proposal_vote-detail": "Proposal id: {{pid}}", "type-limit_order_create": "New trade created", "type-limit_order_cancel": "Open trade cancelled", + "type-limit_order_create": "New trade", "type-effective_comment_vote": "Effective vote", - "type-account_witness_proxy": "Governance proxy update" + "type-account_witness_proxy": "Governance proxy update", + "type-tokens_undelegateDone": "Staked undelegated tokens returned", + "type-tokens_undelegateStart": "Undelegating tokens", + "bought": "bought {{q}}", + "sold": "sold {{q}}", + "for": "for {{b}}" }, "key-or-hot": { "sign": "Sign", diff --git a/src/common/pages/common.ts b/src/common/pages/common.ts index b7b16aa7055..77d5faa6f4b 100644 --- a/src/common/pages/common.ts +++ b/src/common/pages/common.ts @@ -168,6 +168,7 @@ export interface PageProps { deleteDeck: typeof deleteDeck; reorderDecks: typeof reorderDecks; savePageScroll: typeof savePageScroll; + hiveSignerApp: string; } export const pageMapStateToProps = (state: AppState) => ({ diff --git a/src/common/pages/community-create.tsx b/src/common/pages/community-create.tsx index 33e07f57b9a..075347cff5a 100644 --- a/src/common/pages/community-create.tsx +++ b/src/common/pages/community-create.tsx @@ -35,6 +35,7 @@ import { connect } from "react-redux"; import NavBar from "../components/navbar"; import LoginRequired from "../components/login-required"; import KeyOrHot from "../components/key-or-hot"; +import { base } from "../constants/defaults.json"; const namePattern = "^hive-[1]\\d{4,6}$"; interface CreateState { @@ -153,13 +154,15 @@ class CommunityCreatePage extends BaseComponent { postingKey: PrivateKey; }) => { const { ownerKey, activeKey, postingKey } = keys; + const { global } = this.props; + const { hsClientId } = global; return { ownerAuthority: Authority.from(ownerKey.createPublic()), activeAuthority: Authority.from(activeKey.createPublic()), postingAuthority: { ...Authority.from(postingKey.createPublic()), - account_auths: [["ecency.app", 1]] + account_auths: [[hsClientId, 1]] } as Authority }; }; @@ -188,8 +191,8 @@ class CommunityCreatePage extends BaseComponent { submit = async () => { const { activeUser, global } = this.props; - const { hsClientId } = global; const { username, creatorKey } = this.state; + const { hsClientId } = global; if (!activeUser || !creatorKey) return; this.stateSet({ inProgress: true, progress: _t("communities-create.progress-account") }); @@ -247,7 +250,7 @@ class CommunityCreatePage extends BaseComponent { }, posting: { weight_threshold: 1, - account_auths: [["ecency.app", 1]], + account_auths: [[hsClientId, 1]], key_auths: [[keys.postingKey.createPublic().toString(), 1]] }, memo_key: keys.memoKey.createPublic().toString(), @@ -348,9 +351,9 @@ class CommunityCreatePage extends BaseComponent { }; submitHot = async () => { + const { global } = this.props; + const { hsClientId } = global; const { username, title, about } = this.state; - const { hsClientId } = this.props.global; - const keys = this.makePrivateKeys(); const auths = this.makeAuthorities(keys); const operation = this.makeOperation(auths, keys.memoKey); diff --git a/src/common/pages/profile-functional.tsx b/src/common/pages/profile-functional.tsx index 1d5d38113b2..0e180e3bb5e 100644 --- a/src/common/pages/profile-functional.tsx +++ b/src/common/pages/profile-functional.tsx @@ -28,6 +28,7 @@ import ProfileReferrals from "../components/profile-referrals"; import WalletHive from "../components/wallet-hive"; import WalletHiveEngine from "../components/wallet-hive-engine"; import WalletEcency from "../components/wallet-ecency"; +import WalletHiveEngineDetail from "../components/wallet-hive-engine-detail"; import ScrollToTop from "../components/scroll-to-top"; import SearchListItem from "../components/search-list-item"; import SearchBox from "../components/search-box"; @@ -48,11 +49,11 @@ import { withPersistentScroll } from "../components/with-persistent-scroll"; import useAsyncEffect from "use-async-effect"; import { usePrevious } from "../util/use-previous"; import WalletSpk from "../components/wallet-spk"; - interface MatchParams { username: string; section?: string; search?: string; + token?: string; } interface Props extends PageProps { diff --git a/src/common/pages/static/index.tsx b/src/common/pages/static/index.tsx index a0931bf8f15..bfb5c9a727b 100644 --- a/src/common/pages/static/index.tsx +++ b/src/common/pages/static/index.tsx @@ -1,14 +1,14 @@ import React from "react"; import loadable from "@loadable/component"; -const AboutContainer = loadable(() => import("./about")); -const ContributeContainer = loadable(() => import("./contribute")); -const ContributorsContainer = loadable(() => import("./contributors")); -const FaqContainer = loadable(() => import("./faq")); -const GuestPostContainer = loadable(() => import("./guest-post")); -const PrivacyContainer = loadable(() => import("./privacy")); -const TosContainer = loadable(() => import("./tos")); -const WhitePaperContainer = loadable(() => import("./white-paper")); +export const AboutContainer = loadable(() => import("./about")); +export const ContributeContainer = loadable(() => import("./contribute")); +export const ContributorsContainer = loadable(() => import("./contributors")); +export const FaqContainer = loadable(() => import("./faq")); +export const GuestPostContainer = loadable(() => import("./guest-post")); +export const PrivacyContainer = loadable(() => import("./privacy")); +export const TosContainer = loadable(() => import("./tos")); +export const WhitePaperContainer = loadable(() => import("./white-paper")); export const AboutPage = (props: any) => ; export const ContributePage = (props: any) => ; diff --git a/src/common/store/transactions/convert.ts b/src/common/store/transactions/convert.ts new file mode 100644 index 00000000000..4c8a8d03b3b --- /dev/null +++ b/src/common/store/transactions/convert.ts @@ -0,0 +1,308 @@ +import { + HEUnstakeDone, + UnstakeDone, + HETokensUnstake, + TokensUnstake, + HEFineTransaction, + HEAuthorReward, + HECurationReward, + AuthorReward, + CurationReward, + TokensIssue, + HETokensIssue, + HECoarseTransaction, + HETokensTransfer, + Transfer, + Transaction, + HECoarseBaseTransaction, + MarketSell, + HEMarketSell, + TransferToVesting, + HETokensStake, + UnstakeStart, + HEUnstakeStart, + HECancelUnstake, + CancelUnstake, + HEMarketPlaceOrder, + MarketPlaceOrder, + HEMarketCancel, + MarketCancel, + validateOrderType, + HEMarketExpire, + MarketExpire +} from "./types"; +import FormattedNumber from "../../util/formatted-number"; +import { HIVE_HUMAN_NAME } from "../../api/hive"; +export const HEFineTransactionToHiveTransactions = (r: Array) => { + try { + const mapped: Transaction[] = r.map((x: HEFineTransaction): AuthorReward | CurationReward => { + const { id, type, timestamp, trx } = x; + if (x.type == "curation_reward") { + const y: HECurationReward = x as HECurationReward; + const z: CurationReward = { + type: "curation_reward", + num: id, + timestamp, + trx_id: "", + comment_author: x.author, + comment_permlink: x.permlink, + curator: x.account, + reward: FormattedNumber(x.int_amount * Math.pow(10, -x.precision), { + maximumFractionDigits: x.precision, + suffix: x.token + }) + }; + return z; + } else if (x.type === "author_reward") { + const y: HEAuthorReward = x as HEAuthorReward; + const z: AuthorReward = { + author: x.author, + num: id, + type: "author_reward", + timestamp, + trx_id: "", + permlink: x.permlink, + he_payout: FormattedNumber(x.int_amount * Math.pow(10, -x.precision), { + maximumFractionDigits: x.precision, + suffix: x.token + }), + hbd_payout: "0", + hive_payout: "0", + vesting_payout: "0 VESTS" + }; + return z; + } + return { + type: "curation_reward", + num: 9, + timestamp: "2070-01-01T00:00:00Z", + trx_id: "", + comment_author: "null", + comment_permlink: type, + curator: "null", + reward: "0 POB" + }; + }); + const transactions: Transaction[] = mapped + .filter((x) => x !== null) + .sort((a: any, b: any) => b.num - a.num); + return transactions; + } catch (e) { + console.log(e); + } + return []; +}; +export function HEB2B(t: HECoarseBaseTransaction): { + num: number; + trx_id: string; + timestamp: string; +} { + const { _id, symbol, transactionId, timestamp, operation } = t; + const d = new Date(1000 * t.timestamp); + const b = Buffer.from(_id, "hex"); + const ret = { + num: b.readInt32LE(0), + trx_id: transactionId, + timestamp: d.toISOString().split(".")[0] + }; + return ret; +} +export function HEToHTokensUnstake(x: HETokensUnstake): TokensUnstake { + const t: TokensUnstake = { + type: "tokens_unstake", + ...HEB2B(x), + amount: FormattedNumber(x.quantity, { suffix: x.symbol }), + account: x.account + }; + return t; +} +export function HEToHTransfer(x: HETokensTransfer): Transfer { + const t: Transfer = { + type: "transfer", + ...HEB2B(x), + amount: FormattedNumber(x.quantity, { suffix: x.symbol }), + from: x.from, + to: x.to, + memo: x.memo || "" + }; + return t; +} +export function HEtoHTokensIssue(het: HETokensIssue): TokensIssue { + const ht = { + type: "tokens_issue", + ...HEB2B(het), + to: het.to, + amount: FormattedNumber(het.quantity, { suffix: het.symbol }) + }; + // @ts-ignore + return ht; +} +export function HETokensStakeToTransferToVesting(het: HETokensStake): TransferToVesting { + const ht: TransferToVesting = { + ...HEB2B(het), + type: "transfer_to_vesting", + amount: FormattedNumber(het.quantity, { suffix: het.symbol }), + from: het.from, + to: het.to + }; + return ht; +} +export function HEToHMarketSell(het: HEMarketSell, fractionDigits: number = 8): MarketSell { + const { account, to, quantityHive, quantityTokens } = het; + return { + type: "market_sell", + ...HEB2B(het), + account, + to, + base: FormattedNumber(het.quantityHive, { + fractionDigits: 3, + suffix: HIVE_HUMAN_NAME + }), + quote: quantityTokens + ? FormattedNumber(quantityTokens, { + fractionDigits, + suffix: het.symbol + }) + : "0" + }; +} +export function HEToHUnstakeStart(t: HEUnstakeStart, fractionDigits = 8): UnstakeStart { + return { + type: "tokens_unstakeStart", + ...HEB2B(t), + account: t.account, + amount: FormattedNumber(t.quantity, { fractionDigits, suffix: t.symbol }) + }; +} +export function HEToHUnstakeDone(t: HEUnstakeDone, fractionDigits = 8): UnstakeDone { + return { + type: "tokens_unstakeDone", + ...HEB2B(t), + account: t.account, + amount: FormattedNumber(t.quantity, { fractionDigits, suffix: t.symbol }) + }; +} +export function HEToHCancelUnstake(t: HECancelUnstake): CancelUnstake { + return { + type: "tokens_CancelUnstake", + ...HEB2B(t), + unstakeTxID: t.unstakeTxID, + amount: FormattedNumber(t.quantityReturned, { + maximumFractionDigits: 8, + suffix: t.symbol + }) + }; +} +export function HEToHMarketPlaceOrder(t: HEMarketPlaceOrder): MarketPlaceOrder { + const { account, orderType, price, quantityLocked } = t; + validateOrderType(orderType); + const ret: MarketPlaceOrder = { + type: "market_placeOrder", + ...HEB2B(t), + account, + orderType: orderType as "buy" | "sell", + price: price + ? FormattedNumber(price, { + fractionDigits: 8, + suffix: "POB/" + HIVE_HUMAN_NAME + }) + : price, + quantityLocked: FormattedNumber(quantityLocked, { + fractionDigits: 8, + suffix: t.symbol + }) + }; + return ret; +} +function HEToHMarketCancel(t: HEMarketCancel): MarketCancel { + const { orderId, orderType, account, quantityReturned } = t; + validateOrderType(orderType); + return { + type: "market_cancel", + ...HEB2B(t), + account, + orderId, + orderType, + amount: FormattedNumber(quantityReturned, { suffix: t.symbol }) + }; +} +export function HEToHMarketExpire(t: HEMarketExpire): MarketExpire { + const { symbol, orderID, orderType, quantityUnlocked } = t; + validateOrderType(orderType); + return { + type: "market_expireOrder", + amountUnlocked: FormattedNumber(quantityUnlocked, { + suffix: symbol, + maximumFractionDigits: 8 + }), + orderID, + orderType, + ...HEB2B(t) + }; +} +export function HEToHTransaction(t: HECoarseTransaction): Transaction | null { + switch (t.operation) { + case "tokens_unstake": + return HEToHTokensUnstake(t); + case "tokens_transfer": + return HEToHTransfer(t); + case "tokens_issue": + return HEtoHTokensIssue(t); + case "tokens_stake": + return HETokensStakeToTransferToVesting(t); + case "market_sell": + return HEToHMarketSell(t); + case "tokens_unstakeStart": + return HEToHUnstakeStart(t); + case "tokens_unstakeDone": + return HEToHUnstakeDone(t); + case "tokens_cancelUnstake": + return HEToHCancelUnstake(t); + case "market_placeOrder": + return HEToHMarketPlaceOrder(t); + case "market_cancel": + return HEToHMarketCancel(t); + case "tokens_undelegateDone": + return { + type: "tokens_undelegateDone", + ...HEB2B(t), + account: t.account + }; + case "tokens_undelegateStart": + return { + type: "tokens_undelegateStart", + ...HEB2B(t), + account: t.account, + from: t.from, + amount: FormattedNumber(t.quantity, { suffix: t.symbol }) + }; + case "tokens_delegate": + return { + type: "tokens_delegate", + ...HEB2B(t), + account: t.account, + to: t.to, + amount: FormattedNumber(t.quantity, { suffix: t.symbol }) + }; + case "market_closeOrder": + return { + type: "market_closeOrder", + orderType: t.orderType, + ...HEB2B(t) + }; + case "market_buy": + return { + type: "market_buy", + ...HEB2B(t), + account: t.account, + from: t.from, + base: FormattedNumber(t.quantityHive, { suffix: HIVE_HUMAN_NAME }), + quote: FormattedNumber(t.quantityTokens, { suffix: t.symbol }) + }; + case "market_expire": + return HEToHMarketExpire(t); + } // switch + console.log("unhandled type:", t.operation, t); + //throw Error("Unhandled type:" + t.operation); + return null; +} diff --git a/src/common/store/transactions/types.ts b/src/common/store/transactions/types.ts index 9d49a8245b6..00cdd8dce3e 100644 --- a/src/common/store/transactions/types.ts +++ b/src/common/store/transactions/types.ts @@ -1,12 +1,81 @@ import { SMTAsset } from "@hiveio/dhive"; - +export type nAACRS = string; // number as a computer readable string: No commas. No units. +export type nAAHRS = string; // number as a human readable string: Commas, but no units. +export type aAAS = string; // amount as a string: commas and units. +export type orderTypeType = "buy" | "sell" | "marketSell" | "marketBuy"; +export function validateOrderType(s: string) { + if (!["buy", "sell", "marketSell", "marketBuy"].includes(s)) + throw new Error("Unexpected orderType value:" + JSON.stringify(s)); +} interface BaseTransaction { num: number; type: string; timestamp: string; trx_id: string; } - +export interface HEBaseFineTransaction { + account: string; + author: string; + id: number; + // in satoshis + int_amount: number; + permlink: string; + // For POB always 8 + precision: number; + // Iso string date + timestamp: string; + // POB + token: string; + trx: string | null; + type: string; +} +export interface HECoarseBaseTransaction { + _id: string; + blockNumber: number; + transactionId: string; + // seconds since 1970 + timestamp: number; + operation: string; + // POB for Proof of Brain + symbol: string; +} +export interface HEMarketExpire extends HECoarseBaseTransaction { + account: string; + operation: "market_expire"; + orderID: string; + orderType: orderTypeType; + quantityUnlocked: nAACRS; +} +export interface MarketExpire extends BaseTransaction { + type: "market_expireOrder"; + amountUnlocked: aAAS; + orderID: string; + orderType: orderTypeType; +} +export interface HEMarketCloseOrder extends HECoarseBaseTransaction { + operation: "market_closeOrder"; + orderType: orderTypeType; +} +export interface MarketCloseOrder extends BaseTransaction { + type: "market_closeOrder"; + orderType: orderTypeType; +} +export interface HETokensUnstake extends HECoarseBaseTransaction { + account: string; + operation: "tokens_unstake"; + quantity: nAACRS; +} +export interface TokensUnstake extends BaseTransaction { + type: "tokens_unstake"; + account: string; + amount: aAAS; +} +export interface CollateralizedConvert extends BaseTransaction { + type: "collateralized_convert"; + owner: string; + amount: string; + requestid: number; +} export interface CurationReward extends BaseTransaction { type: "curation_reward"; comment_author: string; @@ -22,6 +91,7 @@ export interface AuthorReward extends BaseTransaction { hbd_payout: string; hive_payout: string; vesting_payout: string; + he_payout?: string; } export interface CommentBenefactor extends BaseTransaction { @@ -50,6 +120,35 @@ export interface Transfer extends BaseTransaction { to: string; } +// This is only available as HE (tokens_issue) +export interface TokensIssue extends BaseTransaction { + type: "tokens_issue"; + to: string; + amount: string; +} +export interface TransferToVesting extends BaseTransaction { + type: "transfer_to_vesting"; + amount: string | aAAS; + memo?: string; + from: string; + to: string; +} +export interface HETokensStake extends HECoarseBaseTransaction { + operation: "tokens_stake"; + account: string; + from: string; + to: string; + quantity: nAACRS; +} + +export interface HETokensStake extends HECoarseBaseTransaction { + operation: "tokens_stake"; + account: string; + from: string; + to: string; + quantity: nAACRS; +} + export interface TransferToVesting extends BaseTransaction { type: "transfer_to_vesting"; amount: string; @@ -92,11 +191,254 @@ export interface FillOrder extends BaseTransaction { open_pays: string; } -export interface LimitOrderCancel extends BaseTransaction { - type: "limit_order_cancel"; +export interface ProducerReward extends BaseTransaction { + type: "producer_reward"; + vesting_shares: string; + producer: string; +} +export interface Interest extends BaseTransaction { + type: "interest"; owner: string; - orderid: number; + interest: string; +} + +export interface FillConvertUnstakeStartRequest extends BaseTransaction { + type: "fill_convertUnstakeStart_request"; + amount_in: string; + amount_out: string; +} +export interface ReturnVestingDelegation extends BaseTransaction { + type: "return_vesting_delegation"; + vesting_shares: string; +} +export interface ProposalPay extends BaseTransaction { + type: "proposal_pay"; + payment: string; +} +export interface MarketSell extends BaseTransaction { + type: "market_sell"; + account: string; + to: string; + base: aAAS; + quote: aAAS; +} +export interface HEMarketSell extends HECoarseBaseTransaction { + operation: "market_sell"; + quantityHive: nAACRS; + quantityTokens: null | nAACRS; + account: string; + to: string; +} +export interface HEAuthorReward extends HEBaseFineTransaction { + trx: null; + type: "author_reward"; +} +export interface HECurationReward extends HEBaseFineTransaction { + curator: string; + trx: null; + type: "curation_reward"; +} +export type HEFineTransaction = HECurationReward | HEAuthorReward; +export interface HETokensIssue extends HECoarseBaseTransaction { + operation: "tokens_issue"; + to: string; + quantity: nAACRS; +} +export interface HEUnstakeStart extends HECoarseBaseTransaction { + operation: "tokens_unstakeStart"; + account: string; + quantity: string; +} +export interface UnstakeStart extends BaseTransaction { + type: "tokens_unstakeStart"; + account: string; + amount: aAAS; +} +export interface UnstakeDone extends BaseTransaction { + type: "tokens_unstakeDone"; + account: string; + amount: aAAS; +} +export interface HEUnstakeDone extends HECoarseBaseTransaction { + operation: "tokens_unstakeDone"; + account: string; + quantity: nAACRS; + timestamp: number; + transactionId: string; +} +export interface HETokensTransfer extends HECoarseBaseTransaction { + operation: "tokens_transfer"; + from: string; + to: string; + quantity: nAACRS; + memo: string; +} +export interface HETokensCancelUnstake extends HECoarseBaseTransaction { + operation: "tokens_CancelUnstake"; + unstakeTxID: string; + quantityReturned: nAACRS; +} +export interface HEMarketPlaceOrder extends HECoarseBaseTransaction { + operation: "market_placeOrder"; + account: string; + orderType: orderTypeType; + price: null | nAACRS; // bare number + quantityLocked: nAACRS; // bare number +} +export interface MarketPlaceOrder extends BaseTransaction { + type: "market_placeOrder"; + account: string; + orderType: orderTypeType; + price: null | aAAS; // number with units + quantityLocked: aAAS; // number with units +} +export interface HEMarketCancel extends HECoarseBaseTransaction { + operation: "market_cancel"; + orderId: string; + orderType: orderTypeType; + account: string; + quantityReturned: string; +} +export interface MarketCancel extends BaseTransaction { + type: "market_cancel"; + orderId: string; + account: string; + orderType: orderTypeType; + amount: aAAS; +} + +export interface HETokensUndelegateDone extends HECoarseBaseTransaction { + operation: "tokens_undelegateDone"; num: number; + trx_id: string; + account: string; +} +export interface HETokensDelegate extends HECoarseBaseTransaction { + operation: "tokens_delegate"; + account: string; + quantity: nAACRS; + to: string; +} +export interface TokensDelegate extends BaseTransaction { + type: "tokens_delegate"; + account: string; + amount: aAAS; + to: string; +} +export interface HETokensUndelegateDone extends HECoarseBaseTransaction { + operation: "tokens_undelegateDone"; + account: string; +} +export interface TokensUndelegateDone extends BaseTransaction { + type: "tokens_undelegateDone"; + account: string; +} +export interface HEMarketBuy extends HECoarseBaseTransaction { + operation: "market_buy"; + account: string; + from: string; + quantityHive: nAACRS; + quantityTokens: nAACRS; +} +export interface MarketBuy extends BaseTransaction { + type: "market_buy"; + account: string; + from: string; + base: aAAS; + quote: aAAS; +} +export interface HETokensUndelegateStart extends HECoarseBaseTransaction { + operation: "tokens_undelegateStart"; + account: string; + from: string; + quantity: string; +} +export interface TokensUndelegateStart extends BaseTransaction { + type: "tokens_undelegateStart"; + account: string; + from: string; + amount: aAAS; +} +export interface HECancelUnstake { + _id: string; + blockNumber: number; + transactionId: string; + timestamp: number; + account: string; + operation: "tokens_cancelUnstake"; + unstakeTxID: string; + symbol: string; + quantityReturned: nAACRS; +} +export interface CancelUnstake extends BaseTransaction { + type: "tokens_CancelUnstake"; + amount: aAAS; + unstakeTxID: string; +} +export interface CurationReward extends BaseTransaction { + type: "curation_reward"; + comment_author: string; + comment_permlink: string; + curator: string; + reward: string; +} +export interface CommentBenefactor extends BaseTransaction { + type: "comment_benefactor_reward"; + benefactor: string; + author: string; + permlink: string; + hbd_payout: string; + hive_payout: string; + vesting_payout: string; +} +export interface ClaimRewardBalance extends BaseTransaction { + type: "claim_reward_balance"; + account: string; + reward_hbd: string; + reward_hive: string; + reward_vests: string; +} +export interface Transfer extends BaseTransaction { + type: "transfer"; + amount: string; + memo: string; + from: string; + to: string; +} +export interface TransferToVesting extends BaseTransaction { + type: "transfer_to_vesting"; + amount: string; + memo?: string; + from: string; + to: string; +} +export interface TransferToSavings extends BaseTransaction { + type: "transfer_to_savings"; + amount: string; + memo?: string; + from: string; + to: string; +} +export interface CancelTransferFromSavings extends BaseTransaction { + from: string; + request_id: number; + type: "cancel_transfer_from_savings"; +} +export interface WithdrawVesting extends BaseTransaction { + type: "withdraw_vesting"; + acc: string; + vesting_shares: string; +} +export interface FillOrder extends BaseTransaction { + type: "fill_order"; + current_pays: string; + open_pays: string; +} + +export interface CommentPayoutUpdate extends BaseTransaction { + type: "comment_payout_update"; + author: string; + permlink: string; } export interface ProducerReward extends BaseTransaction { @@ -189,6 +531,11 @@ export interface DelegateVestingShares extends BaseTransaction { vesting_shares: string; } +export interface LimitOrderCancel extends BaseTransaction { + type: "limit_order_cancel"; + owner: string; + orderid: string; +} export interface LimitOrderCreate extends BaseTransaction { type: "limit_order_create"; owner: string; @@ -223,7 +570,33 @@ export interface VoteProxy extends BaseTransaction { proxy: string; } +export type HECoarseTransaction = + | HEMarketCloseOrder + | HEMarketExpire + | HETokensUnstake + | HETokensIssue + | HEUnstakeStart + | HEUnstakeDone + | HETokensTransfer + | HETokensCancelUnstake + | HEMarketSell + | HEMarketBuy + | HEMarketPlaceOrder + | HEMarketCancel + | HETokensUndelegateDone + | HETokensDelegate + | HETokensUndelegateDone + | HEMarketBuy + | HEMarketPlaceOrder + | HETokensUndelegateStart + | HETokensStake + | HECancelUnstake + | HEMarketCancel; + export type Transaction = + | MarketExpire + | MarketCloseOrder + | TokensUnstake | CurationReward | AuthorReward | CommentBenefactor @@ -241,6 +614,7 @@ export type Transaction = | FillCollateralizedConvertRequest | ReturnVestingDelegation | ProposalPay + | MarketBuy | UpdateProposalVotes | CommentPayoutUpdate | CommentReward @@ -252,7 +626,17 @@ export type Transaction = | FillVestingWithdraw | EffectiveCommentVote | VoteProxy - | DelegateVestingShares; + | DelegateVestingShares + | TokensDelegate + | TokensUndelegateDone + | TokensUndelegateStart + | MarketCancel + | MarketPlaceOrder + | CancelUnstake + | UnstakeStart + | UnstakeDone + | MarketSell + | TokensIssue; export type OperationGroup = | "transfers" diff --git a/src/common/util/formatted-number.ts b/src/common/util/formatted-number.ts index 35a5e3eba73..3d0dbc03fd1 100644 --- a/src/common/util/formatted-number.ts +++ b/src/common/util/formatted-number.ts @@ -1,37 +1,159 @@ import numeral from "numeral"; +import { isntString, isntNumber, typeCheck } from "../euphoria"; interface Options { fractionDigits?: number; + maximumFractionDigits?: number; + minimumFractionDigits?: number; + separators?: boolean; prefix?: string; suffix?: string; + debug?: boolean; + truncate?: boolean; +} +function count0s(s: string) { + let counter = 0; + for (const c of s) { + if (c == "0") ++counter; + } + return counter; +} + +function max(a: number, b: number) { + if (a > b) return a; + return b; } export default (value: number | string, options: Options | undefined = undefined) => { + typeCheck(isntNumber(value) && isntString(value), "number|string", "value", value); + + let addNegativeSignFlag: boolean = false; let opts: Options = { - fractionDigits: 3, prefix: "", - suffix: "" + suffix: "", + debug: false, + separators: true, + truncate: false }; - if (options) { opts = { ...opts, ...options }; } + const debugLog = opts.debug; - const { fractionDigits, prefix, suffix } = opts; + if (debugLog) + console.log({ + value, + fractionDigits: options?.fractionDigits, + prefix: options?.prefix, + suffix: options?.suffix + }); + const { prefix, suffix, debug, separators, truncate } = opts; + const minFD: number = options?.minimumFractionDigits ?? options?.fractionDigits ?? 3; + const maxFD: number = options?.maximumFractionDigits ?? options?.fractionDigits ?? 3; + if (debugLog) { + console.log({ maxFD, minFD, value }); + } + if (minFD > maxFD) { + return "NaN"; + } + let out: string = ""; + if (typeof value == "number") { + // builtin format is buggy when using numbers smaller than 1e-6. + if (isNaN(value)) { + return "NaN"; + } + const unity = Math.pow(10, maxFD); + if (value < 0) { + addNegativeSignFlag = true; + } + let satoshis: number = + Math.floor(unity * value + (truncate ? 0 : 0.5)) * (addNegativeSignFlag ? -1 : 1); + if (satoshis == 0) { + addNegativeSignFlag = false; + } + if (debugLog) console.log({ satoshis, out }); + // virtual decimal digits + let vdecimalDigits = 0; + for (; satoshis % 10 == 0 && vdecimalDigits < maxFD - minFD; ++vdecimalDigits) { + satoshis /= 10; + } + while (vdecimalDigits + out.length < maxFD) { + out = (satoshis % 10) + out; + satoshis /= 10; + satoshis = Math.floor(satoshis); + if (debugLog) console.log({ satoshis, out }); + } + if (satoshis == 0) { + while (out.length < minFD) out = "0" + out; + if (debugLog) console.log({ satoshis, out }); + } + // out.length==opts.fracitionDigits + if (out !== "") { + out = "." + out; + if (debugLog) console.log({ satoshis, out }); + } + if (satoshis == 0) { + out = "0" + out; + if (debugLog) console.log({ satoshis, out }); + } - let format = "0,0"; + while (satoshis) { + out = (satoshis % 10) + out; + satoshis /= 10; + satoshis = Math.floor(satoshis); + if (debugLog) console.log({ satoshis, out }); + } + } else { + if (value === "NaN") { + return value; + } + value = value.replace(/,/g, ""); + if (debugLog) console.log(value); + const m = value.match(RegExp(/-?\d+(\.\d+)?/)); + out = m ? m[0] : "NaN"; + if (out === "NaN") { + if (debugLog) console.log("Value passed in was:", { value }); + } + if (out.charAt(0) == "-") { + addNegativeSignFlag = true; + out = out.slice(1); + } + } + let decimal_location: number = (out.indexOf(".") + out.length + 1) % (out.length + 1); + while (out.length - decimal_location - 1 > minFD && out.charAt(out.length - 1) === "0") { + out = out.slice(0, out.length - 1); + } - if (fractionDigits) { - format += "." + "0".repeat(fractionDigits); + if (debugLog) { + console.log(out); + } + if (separators) { + // add commas before and after the decimal point for readability + if (decimal_location + 3 <= out.length) { + for (let j = decimal_location + 4; j < out.length; j += 4) { + out = out.slice(0, j) + "," + out.slice(j); + } + } + for (let j = decimal_location - 3; j >= 0; j -= 3) { + if (j && out[j - 1] != ",") { + out = out.slice(0, j) + "," + out.slice(j); + ++decimal_location; + } + } + if (out.charAt(out.length - 1) === ",") { + out = out.slice(0, out.length - 1); + } } - let out = ""; + if (debugLog) { + console.log(out); + } - if (prefix) out += prefix + " "; - // turn too small values to zero. Bug: https://github.com/adamwdraper/Numeral-js/issues/563 - const av = Math.abs(parseFloat(value.toString())) < 0.0001 ? 0 : value; - out += numeral(av).format(format); + if (addNegativeSignFlag) { + out = "-" + out; + } + if (prefix) out = prefix + " " + out; if (suffix) out += " " + suffix; - + if (debugLog) console.log({ out }); return out; }; diff --git a/src/desktop/app/components/navbar/index.tsx b/src/desktop/app/components/navbar/index.tsx index 0fe03cbb54c..61cc109b710 100644 --- a/src/desktop/app/components/navbar/index.tsx +++ b/src/desktop/app/components/navbar/index.tsx @@ -402,6 +402,7 @@ export class NavBar extends Component { render() { const { global, activeUser, history, location, ui, step, match } = this.props; + const { hsClientId } = global; const themeText = global.theme == Theme.day ? _t("navbar.night-theme") : _t("navbar.day-theme"); const communityPage = match && match.params.name && isCommunity(match.params.name); const tagValue = global.tag ? `/${global.tag}` : ""; diff --git a/src/desktop/app/helper/hive-signer.ts b/src/desktop/app/helper/hive-signer.ts index 9eccc589c9f..4b4569b75ec 100644 --- a/src/desktop/app/helper/hive-signer.ts +++ b/src/desktop/app/helper/hive-signer.ts @@ -76,7 +76,7 @@ export const hsLogin = (hsClientId: string): Promise<{ code: string }> => const win = new window.remote.BrowserWindow(windowSettings); win.webContents.setUserAgent(`Chrome/77.0.3835.0`); - const authUrl = getAuthUrl(REDIR); + const authUrl = getAuthUrl(hsClientId, REDIR); win.loadURL(createWindowView(authUrl)); diff --git a/src/server/handlers/profile.tsx b/src/server/handlers/profile.tsx index b0b44ffc0a7..6d6ce5aeb44 100644 --- a/src/server/handlers/profile.tsx +++ b/src/server/handlers/profile.tsx @@ -54,7 +54,6 @@ export default async (req: express.Request, res: express.Response) => { const filter = ProfileFilter[section] || defaults.filter; const tag = ProfileFilter[section] ? address : ""; - const state = await makePreloadedState(req); const preLoadedState: AppState = { diff --git a/src/server/index.ts b/src/server/index.ts index 957ee3d7473..c4033d08e0a 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -10,6 +10,8 @@ import profileHandler from "./handlers/profile"; import entryHandler from "./handlers/entry"; import fallbackHandler, { healthCheck, iosURI, androidURI, nodeList } from "./handlers/fallback"; import { entryRssHandler, authorRssHandler } from "./handlers/rss"; +import * as http from "http"; +import * as net from "net"; import * as authApi from "./handlers/auth-api"; import { cleanURL, authCheck, stripLastSlash } from "./util"; import { coingeckoHandler } from "./handlers/coingecko.handler"; @@ -20,6 +22,8 @@ const server = express(); const entryFilters = Object.values(EntryFilter); const profileFilters = Object.values(ProfileFilter); +const configurationError = + "Set USE_PRIVATE=1 or define HIVESIGNER_ID and HIVESIGNER_SECRET ENV and make sure base in defaults is set to the base URL for this host"; server .disable("x-powered-by") @@ -103,7 +107,7 @@ if ( config.hsClientSecret.length === 0 || config.usePrivate === "1" ) { - console.error("configurationError:", { + console.error(configurationError, { base: defaults.base, hsClientId: config.hsClientId, hsClientSecret: config.hsClientSecret,