diff --git a/.vscode/settings.json b/.vscode/settings.json
index 43a153a..2fced71 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -8,6 +8,7 @@
"KRISTWALLET",
"KRISTWALLETEXTENSION",
"Lngs",
+ "Mutex",
"Popconfirm",
"Sider",
"Syncable",
diff --git a/package.json b/package.json
index 7477ebc..4edc3db 100644
--- a/package.json
+++ b/package.json
@@ -20,6 +20,7 @@
"@testing-library/react": "^11.2.5",
"@testing-library/user-event": "^12.7.1",
"antd": "^4.12.3",
+ "async-mutex": "^0.3.0",
"base64-arraybuffer": "^0.2.0",
"csv-stringify": "^5.6.1",
"dayjs": "^1.10.4",
@@ -28,7 +29,7 @@
"i18next": "^19.7.0",
"i18next-browser-languagedetector": "^6.0.1",
"i18next-http-backend": "^1.0.20",
- "lodash.throttle": "^4.1.1",
+ "lodash-es": "^4.17.21",
"react": "^17.0.1",
"react-dom": "^17.0.1",
"react-i18next": "^11.8.6",
@@ -71,7 +72,7 @@
"@types/debug": "^4.1.5",
"@types/file-saver": "^2.0.1",
"@types/jest": "^26.0.20",
- "@types/lodash.throttle": "^4.1.6",
+ "@types/lodash-es": "^4.17.4",
"@types/node": "^12.19.16",
"@types/react": "^17.0.1",
"@types/react-dom": "^17.0.0",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index b47f8e6..e34048c 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -4,6 +4,7 @@
'@testing-library/react': 11.2.5_react-dom@17.0.1+react@17.0.1
'@testing-library/user-event': 12.7.1
antd: 4.12.3_89622fd8e4ec221151a62783d49305af
+ async-mutex: 0.3.0
base64-arraybuffer: 0.2.0
csv-stringify: 5.6.1
dayjs: 1.10.4
@@ -12,7 +13,7 @@
i18next: 19.8.7
i18next-browser-languagedetector: 6.0.1
i18next-http-backend: 1.1.0
- lodash.throttle: 4.1.1
+ lodash-es: 4.17.21
react: 17.0.1
react-dom: 17.0.1_react@17.0.1
react-i18next: 11.8.6_i18next@19.8.7+react@17.0.1
@@ -32,7 +33,7 @@
'@types/debug': 4.1.5
'@types/file-saver': 2.0.1
'@types/jest': 26.0.20
- '@types/lodash.throttle': 4.1.6
+ '@types/lodash-es': 4.17.4
'@types/node': 12.20.0
'@types/react': 17.0.2
'@types/react-dom': 17.0.1
@@ -2083,12 +2084,12 @@
dev: true
resolution:
integrity: sha1-7ihweulOEdK4J7y+UnC86n8+ce4=
- /@types/lodash.throttle/4.1.6:
+ /@types/lodash-es/4.17.4:
dependencies:
'@types/lodash': 4.14.168
dev: true
resolution:
- integrity: sha512-/UIH96i/sIRYGC60NoY72jGkCJtFN5KVPhEMMMTjol65effe1gPn0tycJqV5tlSwMTzX8FqzB5yAj0rfGHTPNg==
+ integrity: sha512-BBz79DCJbD2CVYZH67MBeHZRX++HF+5p8Mo5MzjZi64Wac39S3diedJYHZtScbRVf4DjZyN6LzA0SB0zy+HSSQ==
/@types/lodash/4.14.168:
dev: true
resolution:
@@ -2950,6 +2951,12 @@
dev: true
resolution:
integrity: sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ==
+ /async-mutex/0.3.0:
+ dependencies:
+ tslib: 2.1.0
+ dev: false
+ resolution:
+ integrity: sha512-6VIpUM7s37EMXvnO3TvujgaS6gx4yJby13BhxovMYSap7nrbS0gJ1UzGcjD+HElNSdTz/+IlAIqj7H48N0ZlyQ==
/async-validator/3.5.1:
dev: false
resolution:
@@ -7890,6 +7897,10 @@
node: '>=8'
resolution:
integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==
+ /lodash-es/4.17.21:
+ dev: false
+ resolution:
+ integrity: sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==
/lodash._reinterpolate/3.0.0:
dev: true
resolution:
@@ -7915,10 +7926,6 @@
dev: true
resolution:
integrity: sha512-stgLz+i3Aa9mZgnjr/O+v9ruKZsPsndy7qPZOchbqk2cnTU1ZaldKK+v7m54WoKIyxiuMZTKT2H81F8BeAc3ZQ==
- /lodash.throttle/4.1.1:
- dev: false
- resolution:
- integrity: sha1-wj6RtxAkKscMN/HhzaknTMOb8vQ=
/lodash.uniq/4.5.0:
dev: true
resolution:
@@ -12524,7 +12531,6 @@
resolution:
integrity: sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==
/tslib/2.1.0:
- dev: true
resolution:
integrity: sha512-hcVC3wYEziELGGmEEXue7D75zbwIIVUMWAVbHItGPx0ziyXxrOMQx4rQEVEV45Ut/1IotuEvwqPopzIOkDMf0A==
/tsutils/3.20.0_typescript@4.1.5:
@@ -13542,7 +13548,7 @@
'@types/debug': ^4.1.5
'@types/file-saver': ^2.0.1
'@types/jest': ^26.0.20
- '@types/lodash.throttle': ^4.1.6
+ '@types/lodash-es': ^4.17.4
'@types/node': ^12.19.16
'@types/react': ^17.0.1
'@types/react-dom': ^17.0.0
@@ -13555,6 +13561,7 @@
'@typescript-eslint/eslint-plugin': ^4.15.0
'@typescript-eslint/parser': ^4.15.0
antd: ^4.12.3
+ async-mutex: ^0.3.0
base64-arraybuffer: ^0.2.0
craco-less: ^1.17.1
csv-stringify: ^5.6.1
@@ -13568,7 +13575,7 @@
i18next: ^19.7.0
i18next-browser-languagedetector: ^6.0.1
i18next-http-backend: ^1.0.20
- lodash.throttle: ^4.1.1
+ lodash-es: ^4.17.21
prettier: ^2.2.1
react: ^17.0.1
react-dom: ^17.0.1
diff --git a/src/App.tsx b/src/App.tsx
index 1c58bea..6f0a3bb 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -20,6 +20,7 @@
import { ForcedAuth } from "./components/auth/ForcedAuth";
import { WebsocketService } from "./components/ws/WebsocketService";
import { SyncWork } from "./components/ws/SyncWork";
+import { SyncMOTD } from "./components/ws/SyncMOTD";
export const store = createStore(
rootReducer,
@@ -42,6 +43,7 @@
{/* Services, etc. */}
+
diff --git a/src/components/ContextualAddress.tsx b/src/components/ContextualAddress.tsx
index 260941b..bd6573b 100644
--- a/src/components/ContextualAddress.tsx
+++ b/src/components/ContextualAddress.tsx
@@ -1,12 +1,15 @@
import React from "react";
import { Tooltip } from "antd";
+import { useSelector } from "react-redux";
+import { RootState } from "../store";
import { useTranslation } from "react-i18next";
import { Link } from "react-router-dom";
import { KristAddress } from "../krist/api/types";
import { Wallet } from "../krist/wallets/Wallet";
import { parseCommonMeta, CommonMeta } from "../utils/commonmeta";
+import { stripNameSuffix } from "../utils/currency";
import { KristName } from "./KristName";
@@ -21,19 +24,18 @@
}
interface AddressMetanameProps {
+ nameSuffix: string;
address: string;
commonMeta: CommonMeta;
source: boolean;
hideNameAddress: boolean;
}
-export function AddressMetaname({ address, commonMeta, source, hideNameAddress }: AddressMetanameProps): JSX.Element {
+export function AddressMetaname({ nameSuffix, address, commonMeta, source, hideNameAddress }: AddressMetanameProps): JSX.Element {
const rawMetaname = (source ? commonMeta?.return : commonMeta?.recipient) || undefined;
const metaname = (source ? commonMeta?.returnMetaname : commonMeta?.metaname) || undefined;
const name = (source ? commonMeta?.returnName : commonMeta?.name) || undefined;
-
- // TODO: support custom suffixes
- const nameWithoutSuffix = name ? name.replace(/\.kst$/, "") : undefined;
+ const nameWithoutSuffix = name ? stripNameSuffix(nameSuffix, name) : undefined;
return name
? <>
@@ -60,13 +62,14 @@
export function ContextualAddress({ address: origAddress, wallet, metadata, source, hideNameAddress }: Props): JSX.Element {
const { t } = useTranslation();
+ const nameSuffix = useSelector((s: RootState) => s.node.currency.name_suffix);
if (!origAddress) return (
{t("contextualAddressUnknown")}
);
const address = typeof origAddress === "object" ? origAddress.address : origAddress;
- const commonMeta = parseCommonMeta(metadata);
+ const commonMeta = parseCommonMeta(nameSuffix, metadata);
const hasMetaname = source ? !!commonMeta?.returnRecipient : !!commonMeta?.recipient;
return
@@ -74,6 +77,7 @@
? (
// Display the metaname and link to the name if possible
s.node.currency.name_suffix);
+
return
- {/* TODO: support other name suffixes */}
- {name}.kst
+ {name}.{nameSuffix}
;
}
diff --git a/src/components/KristValue.tsx b/src/components/KristValue.tsx
index b9527b6..cdaf960 100644
--- a/src/components/KristValue.tsx
+++ b/src/components/KristValue.tsx
@@ -1,5 +1,8 @@
import React from "react";
+import { useSelector } from "react-redux";
+import { RootState } from "../store";
+
import { KristSymbol } from "./KristSymbol";
import "./KristValue.less";
@@ -13,13 +16,16 @@
type Props = React.HTMLProps & OwnProps;
-export const KristValue = ({ value, long, hideNullish, green, ...props }: Props): JSX.Element | null =>
- hideNullish && (value === undefined || value === null)
- ? null
- : (
-
-
- {(value || 0).toLocaleString()}
- {long && KST}
-
- );
+export const KristValue = ({ value, long, hideNullish, green, ...props }: Props): JSX.Element | null => {
+ const currencySymbol = useSelector((s: RootState) => s.node.currency.currency_symbol);
+
+ if (hideNullish && (value === undefined || value === null)) return null;
+
+ return (
+
+ {(currencySymbol || "KST") === "KST" && }
+ {(value || 0).toLocaleString()}
+ {long && {currencySymbol || "KST"}}
+
+ );
+};
diff --git a/src/components/ws/SyncMOTD.tsx b/src/components/ws/SyncMOTD.tsx
new file mode 100644
index 0000000..a6e6bd2
--- /dev/null
+++ b/src/components/ws/SyncMOTD.tsx
@@ -0,0 +1,62 @@
+import { useEffect } from "react";
+
+import { useDispatch, useSelector, shallowEqual } from "react-redux";
+import { RootState } from "../../store";
+import * as nodeActions from "../../store/actions/NodeActions";
+
+import { AppDispatch } from "../../App";
+import { APIResponse, KristMOTD } from "../../krist/api/types";
+
+import { recalculateWallets } from "../../krist/wallets/Wallet";
+
+import Debug from "debug";
+const debug = Debug("kristweb:sync-motd");
+
+export async function updateMOTD(dispatch: AppDispatch, syncNode: string): Promise {
+ debug("updating motd");
+
+ const res = await fetch(syncNode + "/motd");
+ if (!res.ok || res.status !== 200) // TODO: handle API errors
+ throw new Error("error fetching motd");
+
+ const data: APIResponse = await res.json();
+ if (!data?.ok) throw new Error("error fetching motd");
+
+ debug("motd: %s", data.motd);
+ dispatch(nodeActions.setCurrency(data.currency));
+}
+
+/** Sync the MOTD with the Krist node on startup. */
+export function SyncMOTD(): JSX.Element | null {
+ const syncNode = useSelector((s: RootState) => s.node.syncNode);
+ const connectionState = useSelector((s: RootState) => s.websocket.connectionState);
+
+ // All these are used to determine if we need to recalculate the addresses
+ const addressPrefix = useSelector((s: RootState) => s.node.currency.address_prefix);
+ const masterPassword = useSelector((s: RootState) => s.walletManager.masterPassword);
+ const wallets = useSelector((s: RootState) => s.wallets.wallets, shallowEqual);
+
+ const dispatch = useDispatch();
+
+ // Update the MOTD when the sync node changes, and on startup
+ useEffect(() => {
+ // TODO: show errors to the user?
+ updateMOTD(dispatch, syncNode).catch(console.error);
+ }, [syncNode]);
+
+ // Update the MOTD when the sync node reconnects, in case it changes in
+ // realtime (basically only used for development)
+ useEffect(() => {
+ if (connectionState !== "connected") return;
+ updateMOTD(dispatch, syncNode).catch(console.error);
+ }, [connectionState]);
+
+ // When the currency's address prefix changes, or our master password appears,
+ // recalculate the addresses if necessary
+ useEffect(() => {
+ if (!addressPrefix || !masterPassword) return;
+ recalculateWallets(dispatch, masterPassword, wallets, addressPrefix).catch(console.error);
+ }, [addressPrefix, masterPassword, wallets]);
+
+ return null;
+}
diff --git a/src/components/ws/WebsocketService.tsx b/src/components/ws/WebsocketService.tsx
index a7271eb..a920a9a 100644
--- a/src/components/ws/WebsocketService.tsx
+++ b/src/components/ws/WebsocketService.tsx
@@ -11,7 +11,7 @@
import { findWalletByAddress, syncWallet, syncWalletUpdate } from "../../krist/wallets/Wallet";
import WebSocketAsPromised from "websocket-as-promised";
-import throttle from "lodash.throttle";
+import { throttle } from "lodash-es";
import Debug from "debug";
const debug = Debug("kristweb:ws");
diff --git a/src/krist/AddressAlgo.ts b/src/krist/AddressAlgo.ts
index 0775d0b..dfbb5c1 100644
--- a/src/krist/AddressAlgo.ts
+++ b/src/krist/AddressAlgo.ts
@@ -5,9 +5,9 @@
return String.fromCharCode(byte + 39 > 122 ? 101 : byte > 57 ? byte + 39 : byte);
};
-export const makeV2Address = async (key: string): Promise => {
+export const makeV2Address = async (addressPrefix: string, key: string): Promise => {
const chars = ["", "", "", "", "", "", "", "", ""];
- let chain = "k"; // TODO: custom prefixes
+ let chain = addressPrefix;
let hash = await doubleSHA256(key);
for (let i = 0; i <= 8; i++) {
diff --git a/src/krist/api/types.ts b/src/krist/api/types.ts
index da112e8..9c2603a 100644
--- a/src/krist/api/types.ts
+++ b/src/krist/api/types.ts
@@ -44,6 +44,38 @@
};
}
+export interface KristCurrency {
+ address_prefix: string;
+ name_suffix: string;
+ currency_name: string;
+ currency_symbol: string;
+}
+export const DEFAULT_CURRENCY: KristCurrency = {
+ address_prefix: "k", name_suffix: "kst",
+ currency_name: "Krist", currency_symbol: "KST"
+};
+
+export interface KristMOTD {
+ motd: string;
+ set: string;
+
+ public_url: string;
+ mining_enabled: boolean;
+ debug_enabled: boolean;
+
+ constants: {
+ wallet_version: number;
+ nonce_max_size: number;
+ name_cost: number;
+ min_work: number;
+ max_work: number;
+ work_factor: number;
+ seconds_per_block: number;
+ };
+
+ currency: KristCurrency;
+}
+
export type APIResponse> = T & {
ok: boolean;
error?: string;
diff --git a/src/krist/wallets/Wallet.ts b/src/krist/wallets/Wallet.ts
index f2afb2f..46ae46d 100644
--- a/src/krist/wallets/Wallet.ts
+++ b/src/krist/wallets/Wallet.ts
@@ -11,6 +11,11 @@
import * as actions from "../../store/actions/WalletsActions";
import { WalletMap } from "../../store/reducers/WalletsReducer";
+import { Mutex } from "async-mutex";
+
+import Debug from "debug";
+const debug = Debug("kristweb:wallet");
+
export interface Wallet {
// UUID for this wallet
id: string;
@@ -184,6 +189,7 @@
* @param dispatch - The AppDispatch instance used to dispatch the new wallet to
* the Redux store.
* @param syncNode - The Krist sync node to fetch the wallet data from.
+ * @param addressPrefix - The prefixes of addresses on this node.
* @param masterPassword - The master password used to encrypt the wallet
* password and privatekey.
* @param wallet - The information for the new wallet.
@@ -193,6 +199,7 @@
export async function addWallet(
dispatch: AppDispatch,
syncNode: string,
+ addressPrefix: string,
masterPassword: string,
wallet: WalletNew,
password: string,
@@ -200,7 +207,7 @@
): Promise {
// Calculate the privatekey for the given wallet format
const privatekey = await applyWalletFormat(wallet.format || "kristwallet", password, wallet.username);
- const address = await makeV2Address(privatekey);
+ const address = await makeV2Address(addressPrefix, privatekey);
const id = uuid();
@@ -238,6 +245,7 @@
* @param dispatch - The AppDispatch instance used to dispatch the new wallet to
* the Redux store.
* @param syncNode - The Krist sync node to fetch the wallet data from.
+ * @param addressPrefix - The prefixes of addresses on this node.
* @param masterPassword - The master password used to encrypt the wallet
* password and privatekey.
* @param wallet - The old wallet information.
@@ -247,6 +255,7 @@
export async function editWallet(
dispatch: AppDispatch,
syncNode: string,
+ addressPrefix: string,
masterPassword: string,
wallet: Wallet,
updated: WalletNew,
@@ -254,7 +263,7 @@
): Promise {
// Calculate the privatekey for the given wallet format
const privatekey = await applyWalletFormat(updated.format || "kristwallet", password, updated.username);
- const address = await makeV2Address(privatekey);
+ const address = await makeV2Address(addressPrefix, privatekey);
// Encrypt the password and privatekey. These will be decrypted on-demand.
const encPassword = await aesGcmEncrypt(password, masterPassword);
@@ -317,3 +326,60 @@
return null;
}
+
+const recalculationMutex = new Mutex();
+/** If the address prefix changes (e.g. swapping sync node), and we are
+ * decrypted, recalculate all the addresses with the new prefix. If the prefix
+ * is unchanged, this does nothing. The changes will be dispatched to the
+ * Redux store. */
+export async function recalculateWallets(dispatch: AppDispatch, masterPassword: string, wallets: WalletMap, addressPrefix: string): Promise {
+ const lastPrefix = localStorage.getItem("lastAddressPrefix") || "k";
+ if (addressPrefix === lastPrefix) return;
+ debug("address prefix changed from %s to %s, waiting for mutex...", lastPrefix, addressPrefix);
+
+ // Don't allow more than one recalculation at a time
+ await recalculationMutex.runExclusive(async () => {
+ const lastPrefix = localStorage.getItem("lastAddressPrefix") || "k";
+ if (addressPrefix === lastPrefix) {
+ debug("prefix was already reset while we were calculating");
+ return;
+ }
+
+ debug("recalculating all wallets", lastPrefix, addressPrefix);
+
+ // Map of wallet IDs -> new addresses
+ const updatedWallets: Record = {};
+
+ // Recalculate all the wallets
+ for (const id in wallets) {
+ // Prepare the wallet for recalculation
+ const wallet = wallets[id];
+ const decrypted = await decryptWallet(masterPassword, wallet);
+ if (!decrypted)
+ throw new Error(`couldn't decrypt wallet ${wallet.id}!`);
+
+ // Calculate the new address
+ const privatekey = await applyWalletFormat(wallet.format || "kristwallet", decrypted.password, wallet.username);
+ const address = await makeV2Address(addressPrefix, privatekey);
+
+ if (wallet.address === address) continue;
+
+ // Prepare the change to be applied
+ debug("old address: %s - new address: %s", wallet.address, address);
+ updatedWallets[wallet.id] = address;
+ }
+
+ // Now that we know everything converted successfully, save the updated
+ // wallets to local storage
+ for (const id in wallets) {
+ const wallet = wallets[id];
+ saveWallet({ ...wallet, address: updatedWallets[wallet.id] });
+ }
+
+ // Apply all the changes to the Redux store
+ dispatch(actions.recalculateWallets(updatedWallets));
+
+ debug("recalculation done, saving prefix");
+ localStorage.setItem("lastAddressPrefix", addressPrefix);
+ });
+}
diff --git a/src/pages/wallets/AddWalletModal.tsx b/src/pages/wallets/AddWalletModal.tsx
index 1c173a2..3bc3997 100644
--- a/src/pages/wallets/AddWalletModal.tsx
+++ b/src/pages/wallets/AddWalletModal.tsx
@@ -51,6 +51,7 @@
// Required to check for existing wallets
const { wallets } = useSelector((s: RootState) => s.wallets, shallowEqual);
const syncNode = useSelector((s: RootState) => s.node.syncNode);
+ const addressPrefix = useSelector((s: RootState) => s.node.currency.address_prefix);
const dispatch = useDispatch();
const { t } = useTranslation();
@@ -100,7 +101,7 @@
});
}
- await editWallet(dispatch, syncNode, masterPassword, editing, values, values.password);
+ await editWallet(dispatch, syncNode, addressPrefix, masterPassword, editing, values, values.password);
message.success(t("addWallet.messageSuccessEdit"));
closeModal();
@@ -121,7 +122,7 @@
});
}
- await addWallet(dispatch, syncNode, masterPassword, values, values.password, values.save ?? true);
+ await addWallet(dispatch, syncNode, addressPrefix, masterPassword, values, values.password, values.save ?? true);
message.success(create ? t("addWallet.messageSuccessCreate") : t("addWallet.messageSuccessAdd"));
closeModal();
@@ -147,7 +148,7 @@
/** Update the 'Wallet address' field */
async function updateCalculatedAddress(format: WalletFormatName | undefined, password: string, username?: string) {
const privatekey = await applyWalletFormat(format || "kristwallet", password, username);
- const address = await makeV2Address(privatekey);
+ const address = await makeV2Address(addressPrefix, privatekey);
setCalculatedAddress(address);
}
diff --git a/src/store/actions/NodeActions.ts b/src/store/actions/NodeActions.ts
index fc78f6c..b36aa6a 100644
--- a/src/store/actions/NodeActions.ts
+++ b/src/store/actions/NodeActions.ts
@@ -1,8 +1,9 @@
import { createAction } from "typesafe-actions";
-import { KristWorkDetailed } from "../../krist/api/types";
+import { KristWorkDetailed, KristCurrency } from "../../krist/api/types";
import * as constants from "../constants";
export const setSyncNode = createAction(constants.SYNC_NODE)();
export const setLastBlockID = createAction(constants.LAST_BLOCK_ID)();
export const setDetailedWork = createAction(constants.DETAILED_WORK)();
+export const setCurrency = createAction(constants.DETAILED_WORK)();
diff --git a/src/store/actions/WalletsActions.ts b/src/store/actions/WalletsActions.ts
index 93e8676..2bb47df 100644
--- a/src/store/actions/WalletsActions.ts
+++ b/src/store/actions/WalletsActions.ts
@@ -28,3 +28,7 @@
export interface SyncWalletsPayload { wallets: Record };
export const syncWallets = createAction(constants.SYNC_WALLETS,
(wallets): SyncWalletsPayload => ({ wallets }))();
+
+export interface RecalculateWalletsPayload { wallets: Record };
+export const recalculateWallets = createAction(constants.RECALCULATE_WALLETS,
+ (wallets): RecalculateWalletsPayload => ({ wallets }))();
diff --git a/src/store/constants.ts b/src/store/constants.ts
index 8b3fbd2..04ae167 100644
--- a/src/store/constants.ts
+++ b/src/store/constants.ts
@@ -11,6 +11,7 @@
export const UPDATE_WALLET = "UPDATE_WALLET";
export const SYNC_WALLET = "SYNC_WALLET";
export const SYNC_WALLETS = "SYNC_WALLETS";
+export const RECALCULATE_WALLETS = "RECALCULATE_WALLETS";
// Settings
// ---
@@ -24,3 +25,4 @@
export const SYNC_NODE = "SYNC_NODE";
export const LAST_BLOCK_ID = "LAST_BLOCK_ID";
export const DETAILED_WORK = "DETAILED_WORK";
+export const CURRENCY = "CURRENCY";
diff --git a/src/store/reducers/NodeReducer.ts b/src/store/reducers/NodeReducer.ts
index 9bab7c0..b7f956e 100644
--- a/src/store/reducers/NodeReducer.ts
+++ b/src/store/reducers/NodeReducer.ts
@@ -1,6 +1,6 @@
import { createReducer, ActionType } from "typesafe-actions";
-import { KristWorkDetailed } from "../../krist/api/types";
-import { setSyncNode, setLastBlockID, setDetailedWork } from "../actions/NodeActions";
+import { KristWorkDetailed, KristCurrency, DEFAULT_CURRENCY } from "../../krist/api/types";
+import { setSyncNode, setLastBlockID, setDetailedWork, setCurrency } from "../actions/NodeActions";
import packageJson from "../../../package.json";
@@ -8,12 +8,14 @@
readonly lastBlockID: number;
readonly detailedWork?: KristWorkDetailed;
readonly syncNode: string;
+ readonly currency: KristCurrency;
}
export function getInitialNodeState(): State {
return {
lastBlockID: 0,
- syncNode: localStorage.getItem("syncNode") || packageJson.defaultSyncNode
+ syncNode: localStorage.getItem("syncNode") || packageJson.defaultSyncNode,
+ currency: DEFAULT_CURRENCY
};
}
@@ -29,5 +31,9 @@
.handleAction(setDetailedWork, (state: State, action: ActionType) => ({
...state,
detailedWork: action.payload
+ }))
+ .handleAction(setCurrency, (state: State, action: ActionType) => ({
+ ...state,
+ currency: action.payload
}));
diff --git a/src/store/reducers/WalletsReducer.ts b/src/store/reducers/WalletsReducer.ts
index 8747f7d..f53fb3a 100644
--- a/src/store/reducers/WalletsReducer.ts
+++ b/src/store/reducers/WalletsReducer.ts
@@ -75,4 +75,16 @@
.reduce((o, wallet) => ({ ...o, [wallet.id]: wallet }), {});
return { ...state, wallets: { ...state.wallets, ...updatedWallets }};
+ })
+ // Recalculate wallets
+ .handleAction(actions.recalculateWallets, (state: State, { payload }: ActionType) => {
+ const updatedWallets = Object.entries(payload.wallets)
+ .map(([id, newData]) => ({ // merge in the new data
+ ...(state.wallets[id]), // old data
+ address: newData // recalculated address
+ })) // convert back to a WalletMap
+ .reduce((o, wallet) => ({ ...o, [wallet.id]: wallet }), {});
+
+ return { ...state, wallets: { ...state.wallets, ...updatedWallets }};
});
+
diff --git a/src/utils/commonmeta.ts b/src/utils/commonmeta.ts
index a486cff..91c54c0 100644
--- a/src/utils/commonmeta.ts
+++ b/src/utils/commonmeta.ts
@@ -1,3 +1,5 @@
+import { getNameRegex } from "./currency";
+
export interface CommonMeta {
metaname?: string;
name?: string;
@@ -10,7 +12,7 @@
[key: string]: string | undefined;
}
-export function parseCommonMeta(metadata: string | undefined | null): CommonMeta | null {
+export function parseCommonMeta(nameSuffix: string, metadata: string | undefined | null): CommonMeta | null {
if (!metadata) return null;
const parts: CommonMeta = {};
@@ -18,7 +20,7 @@
const metaParts = metadata.split(";");
if (metaParts.length <= 0) return null;
- const nameMatches = /^(?:([a-z0-9-_]{1,32})@)?([a-z0-9]{1,64}\.kst)$/.exec(metaParts[0]);
+ const nameMatches = getNameRegex(nameSuffix).exec(metaParts[0]);
if (nameMatches) {
if (nameMatches[1]) parts.metaname = nameMatches[1];
if (nameMatches[2]) parts.name = nameMatches[2];
@@ -40,7 +42,7 @@
}
if (parts.return) {
- const returnMatches = /^(?:([a-z0-9-_]{1,32})@)?([a-z0-9]{1,64}\.kst)$/.exec(parts.return);
+ const returnMatches = getNameRegex(nameSuffix).exec(parts.return);
if (returnMatches) {
if (returnMatches[1]) parts.returnMetaname = returnMatches[1];
if (returnMatches[2]) parts.returnName = returnMatches[2];
diff --git a/src/utils/currency.ts b/src/utils/currency.ts
new file mode 100644
index 0000000..d402048
--- /dev/null
+++ b/src/utils/currency.ts
@@ -0,0 +1,23 @@
+import { memoize, escapeRegExp, truncate, toString } from "lodash-es";
+
+const _cleanNameSuffix = (nameSuffix: string | undefined | null): string => {
+ // Ensure the name suffix is safe to put into a RegExp
+ const stringSuffix = toString(nameSuffix);
+ const shortSuffix = truncate(stringSuffix, { length: MAX_NAME_SUFFIX_LENGTH, omission: "" });
+ const escaped = escapeRegExp(shortSuffix);
+ return escaped;
+};
+export const cleanNameSuffix = memoize(_cleanNameSuffix);
+
+// Cheap way to avoid RegExp DoS
+const MAX_NAME_SUFFIX_LENGTH = 6;
+const _getNameRegex = (nameSuffix: string | undefined | null): RegExp =>
+ new RegExp(`^(?:([a-z0-9-_]{1,32})@)?([a-z0-9]{1,64}\\.${cleanNameSuffix(nameSuffix)})$`);
+export const getNameRegex = memoize(_getNameRegex);
+
+const _stripNameSuffixRegExp = (nameSuffix: string | undefined | null): RegExp =>
+ new RegExp(`\\.${cleanNameSuffix(nameSuffix)}$`);
+export const stripNameSuffixRegExp = memoize(_stripNameSuffixRegExp);
+
+export const stripNameSuffix = (nameSuffix: string | undefined | null, inp: string): string =>
+ inp.replace(stripNameSuffixRegExp(nameSuffix), "");