diff --git a/.gitignore b/.gitignore index 2835c3a..d212119 100644 --- a/.gitignore +++ b/.gitignore @@ -4,4 +4,5 @@ build dist .DS_Store .env -.env.development \ No newline at end of file +.env.development +packages \ No newline at end of file diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..7915798 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "packages/zutils"] + path = packages/zutils + url = git@git.kingsome.cn:zhanghongliang/zutils.git diff --git a/docs/api.md b/docs/api.md index c2041e9..11fb237 100644 --- a/docs/api.md +++ b/docs/api.md @@ -341,7 +341,7 @@ body: #### Request -- URL:`/api/user/checkin/list?tag=` +- URL:`/api/user/checkin/list/:tag` - 方法:`GET` - 头部: - Authorization: Bearer JWT_token diff --git a/package.json b/package.json index 85276ec..bcb0bdc 100644 --- a/package.json +++ b/package.json @@ -40,7 +40,8 @@ "redis": "^3.1.2", "siwe": "^2.1.4", "tracer": "^1.1.6", - "web3": "^1.7.4" + "web3": "^1.7.4", + "zutils": "link:packages/zutils" }, "devDependencies": { "@types/dotenv": "^8.2.0", @@ -57,6 +58,6 @@ "ts-node-dev": "^2.0.0", "tsconfig-paths": "^3.9.0", "tslint": "^6.1.1", - "typescript": "^4.1.3" + "typescript": "^5.3.3" } } diff --git a/packages/zutils b/packages/zutils new file mode 160000 index 0000000..5aa4ffe --- /dev/null +++ b/packages/zutils @@ -0,0 +1 @@ +Subproject commit 5aa4ffe02480098342c0deca0f9e641c56ce84f0 diff --git a/src/common/LotteryCache.ts b/src/common/LotteryCache.ts index 219d244..8cb1f1a 100644 --- a/src/common/LotteryCache.ts +++ b/src/common/LotteryCache.ts @@ -1,6 +1,6 @@ import { singleton } from 'decorators/singleton' import { LotteryStats } from 'models/LotteryStats' -import { formatDate } from 'utils/date.util' +import { formatDate } from 'zutils/utils/date.util' const EXPIRE_TIME = 1000 * 60 * 60 @singleton diff --git a/src/controllers/activity.controller.ts b/src/controllers/activity.controller.ts index f9f0998..340a959 100644 --- a/src/controllers/activity.controller.ts +++ b/src/controllers/activity.controller.ts @@ -1,12 +1,12 @@ import { SyncLocker } from 'common/SyncLocker' -import { ZError } from 'common/ZError' import BaseController, { ROLE_ANON } from 'common/base.controller' import { role, router } from 'decorators/router' import { ActivityInfo } from 'models/ActivityInfo' import { ActivityUser } from 'models/ActivityUser' import { RedisClient } from 'redis/RedisClient' import { rankKey } from 'services/rank.svr' -import { yesterday } from 'utils/date.util' +import { yesterday } from 'zutils/utils/date.util' +import { ZError } from 'zutils' const MAX_LIMIT = 50 export default class ActivityController extends BaseController { diff --git a/src/controllers/chain.controller.ts b/src/controllers/chain.controller.ts index 7c483b2..dd26b12 100644 --- a/src/controllers/chain.controller.ts +++ b/src/controllers/chain.controller.ts @@ -6,7 +6,7 @@ import { FastifyRequest } from 'fastify' import { ActivityItem } from 'models/ActivityItem' import { TokenClaimHistory } from 'models/TokenClaimHistory' import { queryStakeList } from 'services/chain.svr' -import { sign } from 'utils/chain.util' +import { sign } from 'zutils/utils/chain.util' export default class ChainController extends BaseController { @router('get /api/stake/list') diff --git a/src/controllers/lottery.controller.ts b/src/controllers/lottery.controller.ts index db7f343..049b8ce 100644 --- a/src/controllers/lottery.controller.ts +++ b/src/controllers/lottery.controller.ts @@ -1,4 +1,4 @@ -import { EMPTY_REWARD, ITEM_FRAME } from 'common/Constants' +import { EMPTY_REWARD } from 'common/Constants' import { LotteryCache } from 'common/LotteryCache' import { SyncLocker } from 'common/SyncLocker' import { ZError } from 'common/ZError' @@ -7,11 +7,10 @@ import { FUSION_CFG } from 'configs/fusion' import { ALL_ITEMS } from 'configs/items' import { LOTTERY_CFG } from 'configs/lottery' import { router } from 'decorators/router' -import { FastifyRequest } from 'fastify' import { ActivityItem } from 'models/ActivityItem' import { LotteryRecord } from 'models/LotteryRecord' import { updateRankScore } from 'services/rank.svr' -import { formatDate } from 'utils/date.util' +import { formatDate } from 'zutils/utils/date.util' const ROUND = 1000000 diff --git a/src/controllers/sign.controller.ts b/src/controllers/sign.controller.ts index 846ed5b..b7326ef 100644 --- a/src/controllers/sign.controller.ts +++ b/src/controllers/sign.controller.ts @@ -1,6 +1,6 @@ import BaseController, { ROLE_ANON } from 'common/base.controller' import { SyncLocker } from 'common/SyncLocker' -import { ZError } from 'common/ZError' +import { ZError } from 'zutils' import { BOOST_CFG } from 'configs/boost' import { role, router } from 'decorators/router' import logger from 'logger/logger' @@ -12,9 +12,9 @@ import { RedisClient } from 'redis/RedisClient' import { queryCheckInList } from 'services/chain.svr' import { rankKey } from 'services/rank.svr' import { SiweMessage } from 'siwe' -import { nextday, yesterday } from 'utils/date.util' -import { checkParamsNeeded } from 'utils/net.util' -import { aesDecrypt, base58ToHex } from 'utils/security.util' +import { nextday } from 'zutils/utils/date.util' +import { checkParamsNeeded } from 'zutils/utils/net.util' +import { aesDecrypt, base58ToHex } from 'zutils/utils/security.util' const LOGIN_TIP = 'This signature is just to verify your identity' diff --git a/src/controllers/tasks.controller.ts b/src/controllers/tasks.controller.ts index 771a5af..75aa1da 100644 --- a/src/controllers/tasks.controller.ts +++ b/src/controllers/tasks.controller.ts @@ -5,7 +5,7 @@ import { router } from 'decorators/router' import { TaskCfg, TaskTypeEnum } from 'models/ActivityInfo' import { TaskStatus, TaskStatusEnum } from 'models/ActivityUser' import { join } from 'path' -import { formatDate } from 'utils/date.util' +import { formatDate } from 'zutils/utils/date.util' const fs = require('fs') const prod = process.env.NODE_ENV === 'production' diff --git a/src/models/Account.ts b/src/models/Account.ts index dfce5a8..dd4624e 100644 --- a/src/models/Account.ts +++ b/src/models/Account.ts @@ -2,7 +2,7 @@ import { getModelForClass, index, modelOptions, mongoose, prop, ReturnModelType, import { dbconn } from 'decorators/dbconn' import { Base, TimeStamps } from '@typegoose/typegoose/lib/defaultClasses' import { BaseModule } from './Base' -import { genRandomString, sha512 } from 'utils/security.util' +import { genRandomString, sha512 } from 'zutils/utils/security.util' import { PlatEnum } from '../enums/PlatEnum' /** * copy from wallet-svr diff --git a/src/models/ActivityUser.ts b/src/models/ActivityUser.ts index 01e43cd..93a52a5 100644 --- a/src/models/ActivityUser.ts +++ b/src/models/ActivityUser.ts @@ -13,7 +13,7 @@ import { dbconn } from 'decorators/dbconn' import findOrCreate from 'mongoose-findorcreate' import { Base, TimeStamps } from '@typegoose/typegoose/lib/defaultClasses' import { BaseModule } from './Base' -import { convert } from 'utils/number.util' +import { convert } from 'zutils/utils/number.util' const alphabet = '3fBCM8j17XNA9xYun4wmLWep2oHFlhPcgyEJskqOz6GK0UtV5ZRaDSvrTbidQI' diff --git a/src/models/Base.ts b/src/models/Base.ts index 8d58374..225cc7c 100644 --- a/src/models/Base.ts +++ b/src/models/Base.ts @@ -6,7 +6,7 @@ import { plugin, ReturnModelType } from '@typegoose/typegoose' import findOrCreate from 'mongoose-findorcreate' import { Connection } from 'mongoose' import { ObjectId } from 'bson' -import { isTrue } from '../utils/string.util' +import { isTrue } from 'zutils/utils/string.util' import { AnyParamConstructor } from '@typegoose/typegoose/lib/types' const jsonExcludeKeys = ['updatedAt', '__v'] diff --git a/src/models/chain/CheckIn.ts b/src/models/chain/CheckIn.ts index ab9c639..55bed47 100644 --- a/src/models/chain/CheckIn.ts +++ b/src/models/chain/CheckIn.ts @@ -1,7 +1,7 @@ import { getModelForClass, index, modelOptions, prop } from '@typegoose/typegoose' import { dbconn } from 'decorators/dbconn' import { BaseModule } from '../Base' -import { formatDate, yesterday } from 'utils/date.util' +import { formatDate, yesterday } from 'zutils/utils/date.util' @dbconn('chain') @index({ from: 1 }, { unique: false }) diff --git a/src/services/oauth.svr.ts b/src/services/oauth.svr.ts index 50f4314..1806f8d 100644 --- a/src/services/oauth.svr.ts +++ b/src/services/oauth.svr.ts @@ -1,4 +1,4 @@ -import { hmacSha256 } from 'utils/security.util' +import { hmacSha256 } from 'zutils/utils/security.util' import axios from 'axios' const SECRET_KEY = process.env.HASH_SALT diff --git a/src/services/rank.svr.ts b/src/services/rank.svr.ts index fbbc49a..f70f8d9 100644 --- a/src/services/rank.svr.ts +++ b/src/services/rank.svr.ts @@ -1,6 +1,6 @@ import { ScoreRecord } from 'models/ScoreRecord' import { RedisClient } from 'redis/RedisClient' -import { formatDate } from 'utils/date.util' +import { formatDate } from 'zutils/utils/date.util' /** * 更新排行榜 diff --git a/src/utils/bn.util.ts b/src/utils/bn.util.ts deleted file mode 100644 index cd5733f..0000000 --- a/src/utils/bn.util.ts +++ /dev/null @@ -1,218 +0,0 @@ -export declare type HexString = string -export declare type Numbers = number | bigint | string | HexString - -const isHexStrict = hex => typeof hex === 'string' && /^((-)?0x[0-9a-f]+|(0x))$/i.test(hex) -export declare type ValidInputTypes = Uint8Array | bigint | string | number | boolean -export const isHex = (hex: ValidInputTypes): boolean => - typeof hex === 'number' || - typeof hex === 'bigint' || - (typeof hex === 'string' && /^((-0x|0x|-)?[0-9a-f]+|(0x))$/i.test(hex)) -const base = BigInt(10) -const expo10 = (expo: number) => base ** BigInt(expo) - -export const ethUnitMap = { - noether: BigInt('0'), - wei: BigInt(1), - kwei: expo10(3), - Kwei: expo10(3), - babbage: expo10(3), - femtoether: expo10(3), - mwei: expo10(6), - Mwei: expo10(6), - lovelace: expo10(6), - picoether: expo10(6), - gwei: expo10(9), - Gwei: expo10(9), - shannon: expo10(9), - nanoether: expo10(9), - nano: expo10(9), - szabo: expo10(12), - microether: expo10(12), - micro: expo10(12), - finney: expo10(15), - milliether: expo10(15), - milli: expo10(15), - ether: expo10(18), - kether: expo10(21), - grand: expo10(21), - mether: expo10(24), - gether: expo10(27), - tether: expo10(30), -} - -export type EtherUnits = keyof typeof ethUnitMap - -/** - * Converts value to it's number representation - */ -export const hexToNumber = (value: string): bigint | number => { - if (!isHexStrict(value)) { - throw new Error('Invalid hex string') - } - - const [negative, hexValue] = value.startsWith('-') ? [true, value.slice(1)] : [false, value] - const num = BigInt(hexValue) - - if (num > Number.MAX_SAFE_INTEGER) { - return negative ? -num : num - } - - if (num < Number.MIN_SAFE_INTEGER) { - return num - } - - return negative ? -1 * Number(num) : Number(num) -} - -export const toNumber = (value: Numbers): number | bigint => { - if (typeof value === 'number') { - return value - } - - if (typeof value === 'bigint') { - return value >= Number.MIN_SAFE_INTEGER && value <= Number.MAX_SAFE_INTEGER ? Number(value) : value - } - - if (typeof value === 'string' && isHexStrict(value)) { - return hexToNumber(value) - } - - try { - return toNumber(BigInt(value)) - } catch { - throw new Error('ivalid number: ' + value) - } -} - -/** - * Auto converts any given value into it's bigint representation - * - * @param value - The value to convert - * @returns - Returns the value in bigint representation - - * @example - * ```ts - * console.log(web3.utils.toBigInt(1)); - * > 1n - * ``` - */ -export const toBigInt = (value: unknown): bigint => { - if (typeof value === 'number') { - return BigInt(value) - } - - if (typeof value === 'bigint') { - return value - } - - // isHex passes for dec, too - if (typeof value === 'string' && isHex(value)) { - return BigInt(value) - } - - if (typeof value === 'string' && value.indexOf(',') >= 0) { - return BigInt(value.replace(/,/g, '')) - } - - throw new Error('invalid number' + value) -} - -export const toBigWei = (number: Numbers, unit: EtherUnits = 'ether'): bigint => { - return toBigInt(toWei(number, unit)) -} - -export const toWei = (number: Numbers, unit: EtherUnits = 'ether'): string => { - const denomination = ethUnitMap[unit] - - if (!denomination) { - throw new Error('error unit: ' + unit) - } - - // if value is decimal e.g. 24.56 extract `integer` and `fraction` part - // to avoid `fraction` to be null use `concat` with empty string - typeof number === 'string' && number.indexOf(',') >= 0 && (number = number.replace(/,/g, '')) - const [integer, fraction] = String(typeof number === 'string' && !isHexStrict(number) ? number : toNumber(number)) - .split('.') - .concat('') - - // join the value removing `.` from - // 24.56 -> 2456 - const value = BigInt(`${integer}${fraction}`) - - // multiply value with denomination - // 2456 * 1000000 -> 2456000000 - const updatedValue = value * denomination - - // count number of zeros in denomination - const numberOfZerosInDenomination = denomination.toString().length - 1 - - // check which either `fraction` or `denomination` have lower number of zeros - const decimals = Math.min(fraction.length, numberOfZerosInDenomination) - - if (decimals === 0) { - return updatedValue.toString() - } - - // Add zeros to make length equal to required decimal points - // If string is larger than decimal points required then remove last zeros - return updatedValue.toString().padStart(decimals, '0').slice(0, -decimals) -} - -/** - * Takes a number of wei and converts it to any other ether unit. - * @param number - The value in wei - * @param unit - The unit to convert to - * @returns - Returns the converted value in the given unit - * - * @example - * ```ts - * console.log(web3.utils.fromWei("1", "ether")); - * > 0.000000000000000001 - * - * console.log(web3.utils.fromWei("1", "shannon")); - * > 0.000000001 - * ``` - */ -export const fromWei = (number: Numbers, unit: EtherUnits = 'ether'): string => { - const denomination = ethUnitMap[unit] - - if (!denomination) { - throw new Error('invalid unit: ' + unit) - } - - // value in wei would always be integer - // 13456789, 1234 - const value = String(toNumber(number)) - - // count number of zeros in denomination - // 1000000 -> 6 - const numberOfZerosInDenomination = denomination.toString().length - 1 - - if (numberOfZerosInDenomination <= 0) { - return value.toString() - } - - // pad the value with required zeros - // 13456789 -> 13456789, 1234 -> 001234 - const zeroPaddedValue = value.padStart(numberOfZerosInDenomination, '0') - - // get the integer part of value by counting number of zeros from start - // 13456789 -> '13' - // 001234 -> '' - const integer = zeroPaddedValue.slice(0, -numberOfZerosInDenomination) - - // get the fraction part of value by counting number of zeros backward - // 13456789 -> '456789' - // 001234 -> '001234' - const fraction = zeroPaddedValue.slice(-numberOfZerosInDenomination).replace(/\.?0+$/, '') - - if (integer === '') { - return `0.${fraction}` - } - - if (fraction === '') { - return integer - } - - return `${integer}.${fraction}` -} diff --git a/src/utils/chain.util.ts b/src/utils/chain.util.ts deleted file mode 100644 index 1eafbd7..0000000 --- a/src/utils/chain.util.ts +++ /dev/null @@ -1,74 +0,0 @@ -import { recoverTypedSignature, SignTypedDataVersion } from '@metamask/eth-sig-util' -import { soliditySha3, toWei } from 'web3-utils' -import Web3 from 'web3' - -export function recoverTypedSignatureV4(signObj: any, signature: string) { - return recoverTypedSignature({ - data: signObj, - signature, - version: SignTypedDataVersion.V4, - }) -} - -export function formatAddress(address: string) { - if (address.length >= 10) { - return address.substring(0, 8) + '...' + address.substring(address.length - 8) - } else if (address.length > 0 && address.length < 10) { - return address - } else { - return '' - } -} - -export function buildLoginSignMsg(nonce: string, tips: string) { - const signMsg = { - tips, - nonce, - } - const signObj = { - types: { - EIP712Domain: [ - { name: 'name', type: 'string' }, - { name: 'version', type: 'string' }, - ], - set: [ - { name: 'tips', type: 'string' }, - { name: 'nonce', type: 'string' }, - ], - }, - primaryType: 'set', - domain: { - name: 'Auth', - version: '1', - }, - message: signMsg, - } - return signObj -} - -export const sign = async ({ - user, - token, - amount, - saltNonce, -}: { - user: string - token: string - amount: number | string - saltNonce?: string -}) => { - const web3 = new Web3() - let privateKey = process.env.SIGN_PRIVATE_KEY - const acc = web3.eth.accounts.privateKeyToAccount(privateKey) - const account = web3.eth.accounts.wallet.add(acc) - const executor = account.address - const amountBn = toWei(amount + '') - const chainId = process.env.CHAIN - const claimContract = process.env.CLAIM_CONTRACT - const startTime = (Date.now() / 1000) | 0 - saltNonce = saltNonce || ((Math.random() * 1000) | 0) + '' - let signStr = soliditySha3.apply(this, [user, token, claimContract, chainId, amountBn, startTime, saltNonce]) - let signature = await web3.eth.sign(signStr, executor) - signature = signature.replace(/00$/, '1b').replace(/01$/, '1c') - return { token, amount: amountBn, startTime, saltNonce, signature } -} diff --git a/src/utils/date.util.ts b/src/utils/date.util.ts deleted file mode 100644 index 5c57243..0000000 --- a/src/utils/date.util.ts +++ /dev/null @@ -1,20 +0,0 @@ -// format the date to the format we want -export const formatDate = (date: Date): string => { - const year = date.getFullYear() - const month = (date.getMonth() + 1 + '').padStart(2, '0') - const day = (date.getDate() + '').padStart(2, '0') - return `${year}${month}${day}` -} - -// get formated datestring of yesterday -export const yesterday = (date?: Date) => { - date = date || new Date() - date.setDate(date.getDate() - 1) - return date -} - -export const nextday = (date?: Date) => { - date = date || new Date() - date.setDate(date.getDate() + 1) - return date -} diff --git a/src/utils/net.util.ts b/src/utils/net.util.ts deleted file mode 100644 index aa83981..0000000 --- a/src/utils/net.util.ts +++ /dev/null @@ -1,148 +0,0 @@ -import { ZError } from 'common/ZError' - -const TIMEOUT_ERROR = new Error('timeout') - -const hexRe = /^[0-9A-Fa-f]+$/gu - -/** - * Execute fetch and verify that the response was successful. - * - * @param request - Request information. - * @param options - Fetch options. - * @returns The fetch response. - */ -export async function successfulFetch(request: string, options?: RequestInit) { - const response = await fetch(request, options) - if (!response.ok) { - throw new Error(`Fetch failed with status '${response.status}' for request '${request}'`) - } - return response -} - -/** - * Execute fetch and return object response. - * - * @param request - The request information. - * @param options - The fetch options. - * @returns The fetch response JSON data. - */ -export async function handleFetch(request: string, options?: RequestInit) { - const response = await successfulFetch(request, options) - const object = await response.json() - return object -} - -/** - * Execute fetch and return object response, log if known error thrown, otherwise rethrow error. - * - * @param request - the request options object - * @param request.url - The request url to query. - * @param request.options - The fetch options. - * @param request.timeout - Timeout to fail request - * @param request.errorCodesToCatch - array of error codes for errors we want to catch in a particular context - * @returns The fetch response JSON data or undefined (if error occurs). - */ -export async function fetchWithErrorHandling({ - url, - options, - timeout, - errorCodesToCatch, -}: { - url: string - options?: RequestInit - timeout?: number - errorCodesToCatch?: number[] -}) { - let result - try { - if (timeout) { - result = Promise.race([ - await handleFetch(url, options), - new Promise((_, reject) => - setTimeout(() => { - reject(TIMEOUT_ERROR) - }, timeout), - ), - ]) - } else { - result = await handleFetch(url, options) - } - } catch (e) { - logOrRethrowError(e, errorCodesToCatch) - } - return result -} - -/** - * Fetch that fails after timeout. - * - * @param url - Url to fetch. - * @param options - Options to send with the request. - * @param timeout - Timeout to fail request. - * @returns Promise resolving the request. - */ -export async function timeoutFetch(url: string, options?: RequestInit, timeout = 500): Promise { - return Promise.race([ - successfulFetch(url, options), - new Promise((_, reject) => - setTimeout(() => { - reject(TIMEOUT_ERROR) - }, timeout), - ), - ]) -} - -/** - * Utility method to log if error is a common fetch error and otherwise rethrow it. - * - * @param error - Caught error that we should either rethrow or log to console - * @param codesToCatch - array of error codes for errors we want to catch and log in a particular context - */ -function logOrRethrowError(error: any, codesToCatch: number[] = []) { - if (!error) { - return - } - - const includesErrorCodeToCatch = codesToCatch.some(code => - error.message.includes(`Fetch failed with status '${code}'`), - ) - - if ( - error instanceof Error && - (includesErrorCodeToCatch || error.message.includes('Failed to fetch') || error === TIMEOUT_ERROR) - ) { - console.error(error) - } else { - throw error - } -} - -export function generateHeader() { - let random = function (start, end) { - return (Math.random() * (end - start) + start) | 0 - } - let getIp = function () { - return `${random(1, 254)}.${random(1, 254)}.${random(1, 254)}.${random(1, 254)}` - } - let time = Date.now() - let useragent = `Mozilla/5.0 (Macintosh; Intel Mac OS X 10_0_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/${ - (70 + Math.random() * 10) | 0 - }.0.4324.${(Math.random() * 100) | 0} Safari/537.36` - const ip = getIp() - return { - 'Refresh-Token': (time -= 5000), - 'Cache-Control': 'no-cache', - 'User-Agent': useragent, - 'X-Forwarded-For': ip, - 'X-Real-IP': ip, - 'Content-Type': 'application/json', - } -} - -export const checkParamsNeeded = (...args) => { - args.forEach(arg => { - if (!arg) { - throw new ZError(10, 'params mismatch') - } - }) -} diff --git a/src/utils/nft.util.ts b/src/utils/nft.util.ts deleted file mode 100644 index e244872..0000000 --- a/src/utils/nft.util.ts +++ /dev/null @@ -1,21 +0,0 @@ -export const ONE_DAY = 24 * 60 * 60 * 1000 -export const NFT_BEGIN_DAY = new Date(2023, 4, 8) - -export const NFT_TYPE = { - badge1: 100, //2022NFT购买用户奖励徽章 - badge2: 101, - badge3: 102, - badge4: 103, -} - -export const MINT_CHANNEL = { - claim: '01', // 2022购买用户claim -} - -// calc days between two Date -export function daysBetween(date1: Date, date2: Date) { - // hours*minutes*seconds*milliseconds - const diffInMs = Math.abs(date1.getTime() - date2.getTime()) - const diffInDays = Math.round(diffInMs / ONE_DAY) - return diffInDays -} diff --git a/src/utils/number.util.ts b/src/utils/number.util.ts deleted file mode 100644 index 87a90b0..0000000 --- a/src/utils/number.util.ts +++ /dev/null @@ -1,278 +0,0 @@ -import Web3 from 'web3' -import { BN } from 'ethereumjs-util' - -/** - * Converts some token minimal unit to render format string, showing 5 decimals - * - * @param {Number|String|BN} tokenValue - Token value to convert - * @param {Number} decimals - Token decimals to convert - * @param {Number} decimalsToShow - Decimals to 5 - * @returns {String} - Number of token minimal unit, in render format - * If value is less than 5 precision decimals will show '< 0.00001' - */ -export function renderFromTokenMinimalUnit(tokenValue, decimals, decimalsToShow = 5) { - const minimalUnit = fromTokenMinimalUnit(tokenValue || 0, decimals) - const minimalUnitNumber = parseFloat(minimalUnit) - let renderMinimalUnit - if (minimalUnitNumber < 0.00001 && minimalUnitNumber > 0) { - renderMinimalUnit = '< 0.00001' - } else { - const base = Math.pow(10, decimalsToShow) - renderMinimalUnit = (Math.round(minimalUnitNumber * base) / base).toString() - } - return renderMinimalUnit -} -/** - * Converts token minimal unit to readable string value - * - * @param {number|string|Object} minimalInput - Token minimal unit to convert - * @param {string} decimals - Token decimals to convert - * @returns {string} - String containing the new number - */ -export function fromTokenMinimalUnit(minimalInput, decimals) { - minimalInput = addHexPrefix(Number(minimalInput).toString(16)) - let minimal = safeNumberToBN(minimalInput) - const negative = minimal.lt(new BN(0)) - const base = Web3.utils.toBN(Math.pow(10, decimals).toString()) - - if (negative) { - minimal = minimal.mul(new BN(-1)) - } - let fraction = minimal.mod(base).toString(10) - while (fraction.length < decimals) { - fraction = '0' + fraction - } - fraction = fraction.match(/^([0-9]*[1-9]|0)(0*)/)[1] - const whole = minimal.div(base).toString(10) - let value = '' + whole + (fraction === '0' ? '' : '.' + fraction) - if (negative) { - value = '-' + value - } - return value -} - -/** - * Converts wei to render format string, showing 5 decimals - * - * @param {Number|String|BN} value - Wei to convert - * @param {Number} decimalsToShow - Decimals to 5 - * @returns {String} - Number of token minimal unit, in render format - * If value is less than 5 precision decimals will show '< 0.00001' - */ -export function renderFromWei(value, decimalsToShow = 5) { - let renderWei = '0' - // avoid undefined - if (value) { - const wei = Web3.utils.fromWei(value) - const weiNumber = parseFloat(wei) - if (weiNumber < 0.00001 && weiNumber > 0) { - renderWei = '< 0.00001' - } else { - const base = Math.pow(10, decimalsToShow) - renderWei = (Math.round(weiNumber * base) / base).toString() - } - } - return renderWei -} - -/** - * Converts token BN value to hex string number to be sent - * - * @param {Object} value - BN instance to convert - * @param {number} decimals - Decimals to be considered on the conversion - * @returns {string} - String of the hex token value - */ -export function calcTokenValueToSend(value, decimals) { - return value ? (value * Math.pow(10, decimals)).toString(16) : 0 -} - -/** - * Determines if a string is a valid decimal - * - * @param {string} value - String to check - * @returns {boolean} - True if the string is a valid decimal - */ -export function isDecimal(value) { - return Number.isFinite(parseFloat(value)) && !Number.isNaN(parseFloat(value)) && !isNaN(+value) -} - -/** - * Creates a BN object from a string - * - * @param {string} value - Some numeric value represented as a string - * @returns {Object} - BN instance - */ -export function toBN(value) { - return Web3.utils.toBN(value) -} - -/** - * Prefixes a hex string with '0x' or '-0x' and returns it. Idempotent. - * - * @param {string} str - The string to prefix. - * @returns {string} The prefixed string. - */ -export const addHexPrefix = (str: string) => { - if (typeof str !== 'string' || str.match(/^-?0x/u)) { - return str - } - - if (str.match(/^-?0X/u)) { - return str.replace('0X', '0x') - } - - if (str.startsWith('-')) { - return str.replace('-', '-0x') - } - - return `0x${str}` -} - -/** - * Wraps 'numberToBN' method to avoid potential undefined and decimal values - * - * @param {number|string} value - number - * @returns {Object} - The converted value as BN instance - */ -export function safeNumberToBN(value: number | string) { - const safeValue = fastSplit(value.toString()) || '0' - return numberToBN(safeValue) -} - -/** - * Performs a fast string split and returns the first item of the string based on the divider provided - * - * @param {number|string} value - number/string to be splitted - * @param {string} divider - string value to use to split the string (default '.') - * @returns {string} - the selected splitted element - */ - -export function fastSplit(value, divider = '.') { - value += '' - const [from, to] = [value.indexOf(divider), 0] - return value.substring(from, to) || value -} - -export function stripHexPrefix(str: string) { - if (typeof str !== 'string') { - return str - } - - return str.slice(0, 2) === '0x' ? str.slice(2) : str -} - -export function numberToBN(arg) { - if (typeof arg === 'string' || typeof arg === 'number') { - var multiplier = Web3.utils.toBN(1); // eslint-disable-line - var formattedString = String(arg).toLowerCase().trim() - var isHexPrefixed = formattedString.substr(0, 2) === '0x' || formattedString.substr(0, 3) === '-0x' - var stringArg = stripHexPrefix(formattedString); // eslint-disable-line - if (stringArg.substr(0, 1) === '-') { - stringArg = stripHexPrefix(stringArg.slice(1)) - multiplier = Web3.utils.toBN(-1) - } - stringArg = stringArg === '' ? '0' : stringArg - - if ( - (!stringArg.match(/^-?[0-9]+$/) && stringArg.match(/^[0-9A-Fa-f]+$/)) || - stringArg.match(/^[a-fA-F]+$/) || - (isHexPrefixed === true && stringArg.match(/^[0-9A-Fa-f]+$/)) - ) { - return Web3.utils.toBN(stringArg).mul(multiplier) - } - - if ((stringArg.match(/^-?[0-9]+$/) || stringArg === '') && isHexPrefixed === false) { - return Web3.utils.toBN(stringArg).mul(multiplier) - } - } else if (typeof arg === 'object' && arg.toString && !arg.pop && !arg.push) { - if (arg.toString(10).match(/^-?[0-9]+$/) && (arg.mul || arg.dividedToIntegerBy)) { - return Web3.utils.toBN(arg.toString(10)) - } - } - - throw new Error( - '[number-to-bn] while converting number ' + - JSON.stringify(arg) + - ' to BN.js instance, error: invalid number value. Value must be an integer, hex string, BN or BigNumber instance. Note, decimals are not supported.', - ) -} - -function checkRadixLegal(radix) { - return radix >= 2 && radix <= 62 -} - -/** - * 将letter转为纯数字 - * @param {string} letter - * @returns {number} - */ -function transformCharToNum(letter, base) { - if (base <= 36) { - letter = letter.toLowerCase() - } - if (letter >= '0' && letter <= '9') { - return parseInt(letter) - } - if (letter >= 'a' && letter <= 'z') { - return letter.charCodeAt(0) - 'a'.charCodeAt(0) + 10 - } - if (letter >= 'A' && letter <= 'Z') { - return letter.charCodeAt(0) - 'A'.charCodeAt(0) + 36 - } - return 0 -} - -/** - * 将数字转为进制里的字母 - * @param {number} num - * @return {string} - */ -function transformNumToChar(num, alphabet) { - alphabet = alphabet || '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ' - return alphabet.charAt(num) -} - -/** - * 将num从base进制转为to指定的进制 - * @param {string} numStr 要转换的数字字符串 - * @param {number} base num的进制 - * @param {number} to 转换后的进制 - * @return {string} - */ -export function convert({ - numStr, - base, - to, - alphabet, -}: { - numStr: string - base: number - to: number - alphabet?: string -}): string { - // 当base和to相等 或 base和to超出转换范围,则原样返回 - if (base === to || !checkRadixLegal(base) || !checkRadixLegal(to)) { - return numStr - } - - // 先转成10进制 - let p = 0 - let number10 = 0 - while (p < numStr.length) { - number10 *= base - number10 += transformCharToNum(numStr.charAt(p), base) - p++ - } - // 若要转换的正好是进制,则直接返回 - if (to === 10) { - return number10.toString() - } - let result = '' - let cur - while (number10) { - cur = number10 % to - result = transformNumToChar(cur, alphabet) + result - number10 = Math.floor(number10 / to) - } - return result -} diff --git a/src/utils/promise.util.ts b/src/utils/promise.util.ts deleted file mode 100644 index 80512f9..0000000 --- a/src/utils/promise.util.ts +++ /dev/null @@ -1,138 +0,0 @@ -type RetryOptions = { - maxRetries: number - whitelistErrors: Error[] -} -/** - * 使用: - * retry(() => fetch("https://example.com"), { maxRetries: 3, whitelistErrors: [] }) - * .then((response) => console.log(response)) - * .catch((error) => console.error(error)); - * @param promiseFn - * @param options - * @returns - */ -export function retry(promiseFn: () => Promise, options: RetryOptions): Promise { - let retries = 0 - let defaultOptions = { - maxRetries: 3, - whitelistErrors: [], - } - Object.assign(defaultOptions, options) - const { maxRetries, whitelistErrors } = options - - const retryPromise = async (): Promise => { - try { - return await promiseFn() - } catch (err) { - if ( - retries < maxRetries && - whitelistErrors.some(whitelistedError => err instanceof whitelistedError.constructor) - ) { - retries++ - return retryPromise() - } - throw err - } - } - - return retryPromise() -} -/** - * 构建一个promise, 在 - * usage: - * function delay(ms: number): Promise { - const deferred = new Deferred(); - - setTimeout(() => { - deferred.resolve(); - }, ms); - - return deferred.promise; - } - - console.log("start"); - - delay(1000).then(() => { - console.log("after 1 second"); - }); - - console.log("end"); - */ -export class Deferred { - private _resolve!: (value: T | PromiseLike) => void - private _reject!: (reason?: any) => void - - public readonly promise: Promise - - constructor() { - this.promise = new Promise((resolve, reject) => { - this._resolve = resolve - this._reject = reject - }) - } - - public resolve(value: T | PromiseLike): void { - this._resolve(value) - } - - public reject(reason?: any): void { - this._reject(reason) - } - - public then( - onfulfilled?: ((value: T) => TResult1 | PromiseLike) | null | undefined, - onrejected?: ((reason: any) => TResult2 | PromiseLike) | null | undefined, - ): Promise { - return this.promise.then(onfulfilled, onrejected) - } - - public catch( - onrejected?: ((reason: any) => TResult | PromiseLike) | null | undefined, - ): Promise { - return this.promise.catch(onrejected) - } -} - -/** - * 简单限流的 Promise 队列 - * usage: - const q = new PromiseQueue(); - [1, 2, 3, 4, 5, 6, 7, 8, 9, 10].forEach((v) => { - q.add( - () => - new Promise((resolve) => { - setTimeout(() => { - console.log(v); - resolve(); - }, 1000); - }) - ); -}); - */ -export class PromiseQueue { - private readonly concurrency: number - private _current: number = 0 - private _list: (() => Promise)[] = [] - - constructor({ concurrency = 2 }: { concurrency: number }) { - this.concurrency = concurrency - } - - add(promiseFn: () => Promise) { - this._list.push(promiseFn) - this.loadNext() - } - - loadNext() { - if (this._list.length === 0 || this.concurrency === this._current) return - this._current++ - const fn = this._list.shift()! - const promise = fn.call(this) - promise.then(this.onLoaded.bind(this)).catch(this.onLoaded.bind(this)) - } - - onLoaded() { - this._current-- - this.loadNext() - } -} diff --git a/src/utils/security.util.ts b/src/utils/security.util.ts deleted file mode 100644 index 9be14dd..0000000 --- a/src/utils/security.util.ts +++ /dev/null @@ -1,153 +0,0 @@ -import crypto from 'crypto' -import CryptoJS from 'crypto-js' - -export function hmac(input, key, out) { - return out - ? crypto.createHmac('sha1', key).update(input).digest(out) - : crypto.createHmac('sha1', key).update(input).digest('hex') -} - -export function genRandomString(length) { - return crypto - .randomBytes(Math.ceil(length / 2)) - .toString('hex') - .slice(0, length) -} - -export function sha512(password, salt) { - let hash = crypto.createHmac('sha512', salt) - hash.update(password) - let value = hash.digest('hex') - return { - salt: salt, - passwordHash: value, - } -} - -export function sha1(str) { - const md5sum = crypto.createHash('sha1') - md5sum.update(str) - str = md5sum.digest('hex') - return str -} - -export function hmacSha256(str: string, key: any) { - const md5sum = crypto.createHmac('sha256', key) - md5sum.update(str) - const data = md5sum.digest('hex') - console.log(`HmacSHA256 rawContent is [${str}], key is [${key}], hash result is [${data}]`) - return data -} - -export function md5(str) { - const md5sum = crypto.createHash('md5') - md5sum.update(str) - str = md5sum.digest('hex') - return str -} - -export function createSign(secretKey, paramStr, timestamp) { - paramStr = `${paramStr}:${timestamp}:${secretKey}` - return sha1(paramStr) -} - -export function checkSign({ - secretKey, - data, - sign, - signKeys, -}: { - secretKey: string - data: {} - sign: string - signKeys: string[] -}) { - signKeys.sort() - let signStr = '' - for (let key of signKeys) { - if (signStr.length > 0) { - signStr += '&' - } - signStr += `${key}=${data[key]}` - } - console.log(signStr) - let sign1 = hmacSha256(signStr, secretKey) - return sign1 === sign -} - -// export function aesDecrypt(encrypted: string, key: string) { -// let bytes = aes.decrypt(encrypted, key) -// var originalText = bytes.toString(enc.Utf8); -// return originalText -// } - -export const aesEncrypt = (plaintText, key) => { - key = CryptoJS.SHA1(key).toString().substring(0, 16) - key = CryptoJS.enc.Base64.parse(key) - let encryptedData = CryptoJS.AES.encrypt(plaintText, key, { - mode: CryptoJS.mode.ECB, - padding: CryptoJS.pad.Pkcs7, - }) - - return encryptedData.toString(CryptoJS.format.Hex) -} - -export const aesDecrypt = (encryptedDataHexStr, key) => { - key = CryptoJS.SHA1(key).toString().substring(0, 16) - key = CryptoJS.enc.Base64.parse(key) - let encryptedHex = CryptoJS.enc.Hex.parse(encryptedDataHexStr) - let encryptedBase64 = CryptoJS.enc.Base64.stringify(encryptedHex) - - var decryptedData = CryptoJS.AES.decrypt(encryptedBase64, key, { - mode: CryptoJS.mode.ECB, - padding: CryptoJS.pad.Pkcs7, - }) - - return decryptedData.toString(CryptoJS.enc.Utf8) -} - -const base58Alphabet = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz' - -export const hexToBase58 = (hexString: string) => { - const bytes = hexString.match(/.{1,2}/g).map(byte => parseInt(byte, 16)) - let base58String = '' - - let num = BigInt('0x' + hexString) - while (num > BigInt(0)) { - const remainder = num % BigInt(58) - num = num / BigInt(58) - base58String = base58Alphabet[Number(remainder)] + base58String - } - - return base58String -} - -export const base58ToHex = (base58String: string) => { - const base58Length = base58String.length - let num = BigInt(0) - let leadingZeros = 0 - for (let i = 0; i < base58Length; i++) { - const charIndex = base58Alphabet.indexOf(base58String[i]) - if (charIndex === -1) { - throw new Error('Invalid Base58 string') - } - - num = num * BigInt(58) + BigInt(charIndex) - } - return num.toString(16) -} - -export const hexToBase32 = (hexString: string) => { - const bytes = hexString.match(/.{1,2}/g).map(byte => parseInt(byte, 16)) - const base32Alphabet = 'qpzry9x8gf2tvdw0s3jn54khce6mua7l' - let base32String = '' - - let num = BigInt('0x' + hexString) - while (num > BigInt(0)) { - const remainder = num % BigInt(32) - num = num / BigInt(32) - base32String = base32Alphabet[Number(remainder)] + base32String - } - - return base32String -} diff --git a/src/utils/string.util.ts b/src/utils/string.util.ts deleted file mode 100644 index 62dba1b..0000000 --- a/src/utils/string.util.ts +++ /dev/null @@ -1,106 +0,0 @@ -/** - * 根据key升序生成 key1=val1&key2=val2的字符串 - * @param {object} data 需要处理的对象 - * @param {boolean} ignoreNull 是否过滤空值(空格或者null值不参与拼接) - * @param splitChar 连接的字符, 默认是& - * @param equalChar = - */ -export function generateKeyValStr(data: {}, ignoreNull = true, splitChar: string = '&', equalChar = '=') { - const keys = Object.keys(data) - keys.sort() - let result = '' - let i = 0 - for (let key of keys) { - if (ignoreNull && !data[key]) { - return - } - if (i++ > 0) result += splitChar - result += `${key}${equalChar}${data[key]}` - } - return result -} - -/** - * 将key1=val&key2=val的字符串组装成对象 - * @param str key1=val&key2=val的字符串 - * @param splitChar 连接的字符, 默认是& - * @param equalChar = - */ -export function keyValToObject(str: string, splitChar: string = '&', equalChar = '='): {} { - let result = {} - if (!str) { - return result - } - let arrs = str.split(splitChar) - for (let sub of arrs) { - let subArr = sub.split(equalChar) - result[subArr[0]] = subArr[1] - } - return result -} - -/** - * 判断传入的值是否为true - * @param {Object} obj 传入值为'true','TRUE',1,'1','on','ON','YES','yes'时,返回true,其他值均返回false - * @return {boolean} - */ -export function isTrue(obj) { - return ( - obj === 'true' || - obj === 'TRUE' || - obj === 'True' || - obj === 'on' || - obj === 'ON' || - obj === true || - obj === 1 || - obj === '1' || - obj === 'YES' || - obj === 'yes' - ) -} - -/** - * 验证ObjectId格式是否正确 - * @param {string} id - * @return {boolean} - */ -export function isObjectId(id: string): boolean { - //mongoose.Types.ObjectId.isValid(id) - return /^[a-fA-F0-9]{24}$/.test(id) -} - -/** - * 10进制 -> 62进制 - * @param {string | number} number - * @return {string} - */ -export function string10to62(number: string | number) { - const chars = '0123456789abcdefghigklmnopqrstuvwxyzABCDEFGHIGKLMNOPQRSTUVWXYZ'.split('') - const radix = chars.length - let qutient = +number - const arr = [] - do { - const mod = qutient % radix - qutient = (qutient - mod) / radix - arr.unshift(chars[mod]) - } while (qutient) - return arr.join('') -} - -/** - * 62进制 -> 10 进制 - * @param {string} numberCode - * @return {number} - */ -export function string62to10(numberCode: string) { - const chars = '0123456789abcdefghigklmnopqrstuvwxyzABCDEFGHIGKLMNOPQRSTUVWXYZ' - const radix = chars.length - numberCode = numberCode + '' - const len = numberCode.length - let i = 0 - let originNumber = 0 - while (i < len) { - originNumber += Math.pow(radix, i++) * (chars.indexOf(numberCode.charAt(len - i)) || 0) - } - return originNumber -} diff --git a/src/utils/wallet.util.ts b/src/utils/wallet.util.ts deleted file mode 100644 index b30cf5a..0000000 --- a/src/utils/wallet.util.ts +++ /dev/null @@ -1,125 +0,0 @@ -import { renderFromTokenMinimalUnit } from './number.util' -import { asciiToHex } from 'web3-utils' - -/** - * Removes IPFS protocol prefix from input string. - * - * @param ipfsUrl - An IPFS url (e.g. ipfs://{content id}) - * @returns IPFS content identifier and (possibly) path in a string - * @throws Will throw if the url passed is not IPFS. - */ -export function removeIpfsProtocolPrefix(ipfsUrl: string) { - if (ipfsUrl.startsWith('ipfs://ipfs/')) { - return ipfsUrl.replace('ipfs://ipfs/', '') - } else if (ipfsUrl.startsWith('ipfs://')) { - return ipfsUrl.replace('ipfs://', '') - } - // this method should not be used with non-ipfs urls (i.e. startsWith('ipfs://') === true) - throw new Error('this method should not be used with non ipfs urls') -} - -/** - * Extracts content identifier and path from an input string. - * - * @param ipfsUrl - An IPFS URL minus the IPFS protocol prefix - * @returns IFPS content identifier (cid) and sub path as string. - * @throws Will throw if the url passed is not ipfs. - */ -export function getIpfsCIDv1AndPath(ipfsUrl: string): { - cid: string - path?: string -} { - const url = removeIpfsProtocolPrefix(ipfsUrl) - - // check if there is a path - // (CID is everything preceding first forward slash, path is everything after) - const index = url.indexOf('/') - const cid = index !== -1 ? url.substring(0, index) : url - const path = index !== -1 ? url.substring(index) : undefined - //TODO: - // We want to ensure that the CID is v1 (https://docs.ipfs.io/concepts/content-addressing/#identifier-formats) - // because most cid v0s appear to be incompatible with IPFS subdomains - // return { - // cid: CID.parse(cid).toV1().toString(), - // path, - // }; - return { - cid, - path, - } -} - -/** - * Adds URL protocol prefix to input URL string if missing. - * - * @param urlString - An IPFS URL. - * @returns A URL with a https:// prepended. - */ -export function addUrlProtocolPrefix(urlString: string): string { - if (!urlString.match(/(^http:\/\/)|(^https:\/\/)/u)) { - return `https://${urlString}` - } - return urlString -} - -/** - * Formats URL correctly for use retrieving assets hosted on IPFS. - * - * @param ipfsGateway - The users preferred IPFS gateway (full URL or just host). - * @param ipfsUrl - The IFPS URL pointed at the asset. - * @param subdomainSupported - Boolean indicating whether the URL should be formatted with subdomains or not. - * @returns A formatted URL, with the user's preferred IPFS gateway and format (subdomain or not), pointing to an asset hosted on IPFS. - */ -export function getFormattedIpfsUrl(ipfsGateway: string, ipfsUrl: string, subdomainSupported: boolean): string { - const { host, protocol, origin } = new URL(addUrlProtocolPrefix(ipfsGateway)) - if (subdomainSupported) { - const { cid, path } = getIpfsCIDv1AndPath(ipfsUrl) - return `${protocol}//${cid}.ipfs.${host}${path || ''}` - } - const cidAndPath = removeIpfsProtocolPrefix(ipfsUrl) - return `${origin}/ipfs/${cidAndPath}` -} - -/** - * Returns whether the given code corresponds to a smart contract. - * - * @param code - The potential smart contract code. - * @returns Whether the code was smart contract code or not. - */ -export function isSmartContractCode(code: string) { - /* istanbul ignore if */ - if (!code) { - return false - } - // Geth will return '0x', and ganache-core v2.2.1 will return '0x0' - const smartContractCode = code !== '0x' && code !== '0x0' - return smartContractCode -} - -export function formatAddress(address: string) { - if (address.length >= 10) { - return address.substring(0, 6) + '...' + address.substring(address.length - 4) - } else if (address.length > 0 && address.length < 10) { - return address - } else { - return '' - } -} - -export function formatMoney(balance: number | string, symbol: string) { - if (balance === '-') { - return `- ${symbol}` - } - let money = renderFromTokenMinimalUnit(balance, 18, 4) - return `${money} ${symbol}` -} - -/** - * 生成随机的bytes32的字符串 - * @returns - */ -export function generateRandomBytes32() { - const v1 = (Math.random() * 9000000 + 1000000) | 0 - const v2 = (Math.random() * 900000 + 100000) | 0 - return asciiToHex(v1 + '' + v2) -} diff --git a/yarn.lock b/yarn.lock index 701095b..a3110ca 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4590,10 +4590,10 @@ typedarray-to-buffer@^3.1.5: dependencies: is-typedarray "^1.0.0" -typescript@^4.1.3: - version "4.9.5" - resolved "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz" - integrity sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g== +typescript@^5.3.3: + version "5.3.3" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.3.3.tgz#b3ce6ba258e72e6305ba66f5c9b452aaee3ffe37" + integrity sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw== ultron@~1.1.0: version "1.1.1" @@ -5050,3 +5050,7 @@ yn@3.1.1: version "3.1.1" resolved "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz" integrity sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q== + +"zutils@link:packages/zutils": + version "0.0.0" + uid ""