From 96a8f43b238d84eda4ddf9b41e4e59cc519e6969 Mon Sep 17 00:00:00 2001 From: Walledgarden Date: Mon, 24 Apr 2023 19:02:11 +0200 Subject: [PATCH 1/3] feat: Add option to format amount .toString() and value conversions for Amounts --- src/structures/Amount.ts | 69 +++++++++++++++++++++- src/structures/Currency.ts | 87 +++++++++++++++------------- src/structures/CurrencyCache.ts | 3 +- src/structures/CurrencyFormatting.ts | 4 +- src/structures/TipccClient.ts | 50 ++++++++++++++-- typedoc.json | 4 +- 6 files changed, 160 insertions(+), 57 deletions(-) diff --git a/src/structures/Amount.ts b/src/structures/Amount.ts index 4236bf6..aaaacd1 100644 --- a/src/structures/Amount.ts +++ b/src/structures/Amount.ts @@ -1,5 +1,7 @@ import BigNumber from 'bignumber.js'; import type { APICoin, APIMonetary } from '@tipccjs/tipcc-api-types'; +import { TipccClient } from './TipccClient'; +import { CryptoCurrency, FiatCurrency } from './Currency'; /** * A class for storing an API amount. This can be used for either fiats or cryptocurrencies. @@ -10,15 +12,76 @@ export class Amount { /** The raw API BigNumber */ public valueRaw: BigNumber; + /** The value */ + public get value(): BigNumber | null { + const currency = this.currency; + if (!currency) return null; + return currency.convertFromRaw(this.valueRaw); + } + /** The currency code */ - public currency: string; + public currencyCode: string; + + /** The currency */ + public get currency(): (FiatCurrency | CryptoCurrency) | null { + if (!this.client) return null; + const currency = + this.client.cryptos.get(this.currencyCode) ?? + this.client.fiats.get(this.currencyCode); + if (!currency) return null; + return currency; + } + + /** The USD value */ + public get usdValue(): BigNumber | null { + if (!this.currency) return null; + const exchangeRate = + this.client?.exchangeRates.get(this.currency.code) ?? null; + if (!exchangeRate) return null; + if (!this.value) return null; + return this.currency.convertByExchangeRate(this.value, exchangeRate); + } + + /** The currency emoji (Discord Formatted) */ + public get emoji(): string | null { + if (!this.client) return null; + const currencyEmoji = this.client.emojis.find( + (e) => e.name.toUpperCase() === this.currencyCode.toUpperCase(), + ); + if (!currencyEmoji) return null; + return `<:${currencyEmoji.name}:${currencyEmoji.id}>`; + } + + /** The client that instantiated this */ + public client: TipccClient | null = null; /** * Create an Amount. * @param payload An amount from the API */ - constructor(payload: APIMonetary | APICoin) { + constructor(payload: APIMonetary | APICoin, client?: TipccClient) { this.valueRaw = BigNumber(payload.value); - this.currency = payload.currency; + this.currencyCode = payload.currency; + if (client) this.client = client; + } + + public toString(includeUsd = true): string | null { + const emoji = this.emoji; + const value = this.valueRaw; + const currency = this.currency; + + if (!value || !currency) return null; + + const unit = + currency.format.units + .filter((u) => u.min) + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + .find((u) => BigNumber(u.min!).lte(this.valueRaw)) ?? + currency.format.units[0]; + + return `${emoji ? `${emoji} ` : ''} ${this.valueRaw + .shiftedBy(-unit.scale) + .toFixed(unit.optionalDecimals ?? unit.scale) + .replace(/\.?0+$/, '')} ${unit.singular} ${includeUsd ? 'USD' : ''}`; } } diff --git a/src/structures/Currency.ts b/src/structures/Currency.ts index 7ca3b66..e7ef97e 100644 --- a/src/structures/Currency.ts +++ b/src/structures/Currency.ts @@ -4,38 +4,27 @@ import type { APICryptoCurrency, APIFiatCurrency, } from '@tipccjs/tipcc-api-types'; +import { ExchangeRate } from './ExchangeRate'; /** - * A class for storing an API cryptocurrency. + * A class for storing an API currency. * * @category Currency */ -export class CryptoCurrency { +export class Currency { /** The currency code */ public code: string; /** The currency name */ public name: string; - /** The currency icon link */ - public icon: string; - - /** The currency explorer link */ - public explorer: string; - /** An instance of {@link CurrencyFormat} for this currency */ public format: CurrencyFormat; - /** - * Create a CryptoCurrency. - * @param payload The currency from the API - */ - constructor(payload: APICryptoCurrency) { + constructor(payload: { code: string; name: string; format: CurrencyFormat }) { this.code = payload.code; this.name = payload.name; - this.icon = payload.icon; - this.explorer = payload.explorer; - this.format = new CurrencyFormat(payload.format); + this.format = payload.format; } /** @@ -53,46 +42,62 @@ export class CryptoCurrency { public convertToRaw(value: BigNumber): BigNumber { return value.shiftedBy(this.format.scale); } + + /** + * Convert the value in this currency to USD as a BigNumber. + */ + public convertByExchangeRate( + value: BigNumber, + exchangeRate: ExchangeRate, + ): BigNumber | null { + if (!exchangeRate.usdValue?.value) return null; + return this.convertFromRaw(value).times(exchangeRate.usdValue?.value); + } } /** - * A class for storing an API fiat currency. + * A class for storing an API cryptocurrency. * * @category Currency */ -export class FiatCurrency { - /** The currency code */ - public code: string; - - /** The currency name */ - public name: string; +export class CryptoCurrency extends Currency { + /** The currency icon link */ + public icon: string; - /** An instance of {@link CurrencyFormat} for this currency */ - public format: CurrencyFormat; + /** The currency explorer link */ + public explorer: string; /** - * Create a FiatCurrency. + * Create a CryptoCurrency. * @param payload The currency from the API */ - constructor(payload: APIFiatCurrency) { - this.code = payload.code; - this.name = payload.name; - this.format = new CurrencyFormat(payload.format); - } + constructor(payload: APICryptoCurrency) { + super({ + code: payload.code, + name: payload.name, + format: new CurrencyFormat(payload.format), + }); - /** - * Convert a raw value to a BigNumber in human readable format. - * @param value The raw value - */ - public convertFromRaw(value: BigNumber): BigNumber { - return BigNumber(value).shiftedBy(this.format.scale * -1); + this.icon = payload.icon; + this.explorer = payload.explorer; } +} +/** + * A class for storing an API fiat currency. + * + * @category Currency + */ +export class FiatCurrency extends Currency { /** - * Convert a BigNumber value in human readable format to a raw API BigNumber. - * @param value The amount + * Create a FiatCurrency. + * @param payload The currency from the API */ - public convertToRaw(value: BigNumber): BigNumber { - return value.shiftedBy(this.format.scale); + constructor(payload: APIFiatCurrency) { + super({ + code: payload.code, + name: payload.name, + format: new CurrencyFormat(payload.format), + }); } } diff --git a/src/structures/CurrencyCache.ts b/src/structures/CurrencyCache.ts index 61aeb7a..3cee1a3 100644 --- a/src/structures/CurrencyCache.ts +++ b/src/structures/CurrencyCache.ts @@ -31,10 +31,9 @@ export class CurrencyCache extends Array { * A shortcut to find a currency by code. * @param code The code to search for */ - public async get(code: string): Promise { + public get(code: string): T | null { const found = this.find((i) => i.code === code); if (found) return found; - return null; } } diff --git a/src/structures/CurrencyFormatting.ts b/src/structures/CurrencyFormatting.ts index f782ab5..f4006b2 100644 --- a/src/structures/CurrencyFormatting.ts +++ b/src/structures/CurrencyFormatting.ts @@ -25,7 +25,7 @@ export class CurrencyUnit { public minDecimals: number | null; - public optionalDecimals: boolean | null; + public optionalDecimals: number | null; public min: number | null; @@ -41,7 +41,7 @@ export class CurrencyUnit { this.scale = payload.scale; this.aliases = payload.aliases ?? []; this.minDecimals = payload.minDecimals ?? null; - this.optionalDecimals = payload.optionalDecimals ?? null; + this.optionalDecimals = (payload.optionalDecimals as any as number) ?? null; this.min = payload.min ?? null; } } diff --git a/src/structures/TipccClient.ts b/src/structures/TipccClient.ts index 305ea55..ee5bd70 100644 --- a/src/structures/TipccClient.ts +++ b/src/structures/TipccClient.ts @@ -23,6 +23,11 @@ interface Events { ready: void; } +interface Emoji { + name: string; + id: string; +} + /** * A tip.cc client to interact with the API. * @@ -41,6 +46,11 @@ export class TipccClient extends EventEmitter { /** The {@link CurrencyCache} for fiat currencies */ public fiats = new CurrencyCache(this._refreshFiats); + /** The {@link CurrencyCache} for exchange rates */ + public exchangeRates = new CurrencyCache( + this._refreshExchangeRates, + ); + /** A boolean indicating whether the client is ready */ public isReady = false; @@ -50,6 +60,11 @@ export class TipccClient extends EventEmitter { /** The max number of retries to poll the API, after which an error will be thrown */ public maxRetries = 5; + /** The number of milliseconds between each exchange rate refresh (0 = no automatic refresh) */ + public exchangeRateRefreshInterval = 60 * 1000; + + private _emojis: Map = new Map(); + private polling = new Set(); private pollingTimeout: NodeJS.Timeout | null = null; @@ -87,12 +102,31 @@ export class TipccClient extends EventEmitter { if (options.pollingInterval) this.pollingInterval = options.pollingInterval; if (options.maxRetries) this.maxRetries = options.maxRetries; - Promise.all([this.cryptos.refresh(), this.fiats.refresh()]).then(() => { + Promise.all([ + this.cryptos.refresh(), + this.fiats.refresh(), + this.exchangeRates.refresh(), + ]).then(() => { + if (this.exchangeRateRefreshInterval > 0) + setInterval( + () => this.exchangeRates.refresh(), + this.exchangeRateRefreshInterval, + ); this.emit('ready'); this.isReady = true; }); } + /** A map for emojis which should be used for formatted amounts */ + public get emojis(): Emoji[] { + return [...this._emojis].map(([name, id]) => ({ name, id })); + } + + /** A map for emojis which should be used for formatted amounts */ + public set emojis(emojis: Emoji[]) { + this._emojis = new Map(emojis.map(({ name, id }) => [name, id])); + } + /** * Poll the tip.cc API for new data. */ @@ -152,9 +186,7 @@ export class TipccClient extends EventEmitter { Routes.currenciesCryptocurrencies(), )) as RESTGetAPICurrenciesCryptoCurrenciesResult; - const processed = cryptocurrencies.map((c) => new CryptoCurrency(c)); - - return processed; + return cryptocurrencies.map((c) => new CryptoCurrency(c)); } private async _refreshFiats(): Promise { @@ -163,9 +195,15 @@ export class TipccClient extends EventEmitter { Routes.currenciesFiats(), )) as RESTGetAPICurrenciesFiatsResult; - const processed = fiats.map((c) => new FiatCurrency(c)); + return fiats.map((c) => new FiatCurrency(c)); + } - return processed; + private async _refreshExchangeRates(): Promise { + const { rates } = (await this.REST.request( + 'GET', + Routes.currenciesRates(), + )) as RESTGetAPICurrenciesRatesResult; + return rates.map((r) => new ExchangeRate(r)); } public on(s: K, f: (arg: Events[K]) => void): this { diff --git a/typedoc.json b/typedoc.json index df4e0f9..3778cd4 100644 --- a/typedoc.json +++ b/typedoc.json @@ -10,9 +10,7 @@ }, "defaultCategory": "Classes", "categorizeByGroup": false, - "categoryOrder": [ - "Client", "Client utilities", "*", "Utilities" - ], + "categoryOrder": ["Client", "Client utilities", "*", "Utilities"], "customCss": "./docs.css", "githubPages": false } From ecbd10a5771bdf75dcec9c97ebe376c9bf4b7d6d Mon Sep 17 00:00:00 2001 From: Walledgarden Date: Mon, 24 Apr 2023 19:11:35 +0200 Subject: [PATCH 2/3] feat: Add option to format amount .toString() and value conversions for Amounts --- src/structures/Amount.ts | 8 +++++--- src/structures/Transaction.ts | 11 ++++++++--- 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/src/structures/Amount.ts b/src/structures/Amount.ts index aaaacd1..9195aeb 100644 --- a/src/structures/Amount.ts +++ b/src/structures/Amount.ts @@ -53,7 +53,7 @@ export class Amount { } /** The client that instantiated this */ - public client: TipccClient | null = null; + public client: TipccClient | undefined; /** * Create an Amount. @@ -79,9 +79,11 @@ export class Amount { .find((u) => BigNumber(u.min!).lte(this.valueRaw)) ?? currency.format.units[0]; - return `${emoji ? `${emoji} ` : ''} ${this.valueRaw + const usdValue = this.usdValue; + + return `**${emoji ? `${emoji} ` : ''} ${this.valueRaw .shiftedBy(-unit.scale) .toFixed(unit.optionalDecimals ?? unit.scale) - .replace(/\.?0+$/, '')} ${unit.singular} ${includeUsd ? 'USD' : ''}`; + .replace(/\.?0+$/, '')} ${unit.singular}**${includeUsd && usdValue ? ` (≈ $${usdValue.lt(0.01) ? usdValue.toFixed(4) : usdValue.toFixed(2)})` : ''}`; } } diff --git a/src/structures/Transaction.ts b/src/structures/Transaction.ts index 9fc91f1..fd99474 100644 --- a/src/structures/Transaction.ts +++ b/src/structures/Transaction.ts @@ -1,4 +1,5 @@ import { Amount } from './Amount'; +import { TipccClient } from './TipccClient'; import { User } from './User'; import type { APITransaction } from '@tipccjs/tipcc-api-types'; @@ -41,16 +42,20 @@ export class Transaction { /** The Date when this transaction was created */ public created: Date; + /** The client that instantiated this transaction */ + public client: TipccClient | undefined; + /** * Create a Transaction. * @param payload The transaction from the API */ - constructor(payload: APITransaction) { + constructor(payload: APITransaction, client?: TipccClient) { + if (client) this.client = client; this.id = payload.id; this.type = payload.type; - this.amount = new Amount(payload.amount); + this.amount = new Amount(payload.amount, this.client); this.fee = payload.fee ? new Amount(payload.fee) : null; - this.usdValue = payload.usd_value ? new Amount(payload.usd_value) : null; + this.usdValue = payload.usd_value ? new Amount(payload.usd_value, this.client) : null; this.service = payload.service; this.chatId = payload.chat_id; this.subchatId = payload.subchat_id; From d89eabead2d061057eaeff2b3724993b7993c41e Mon Sep 17 00:00:00 2001 From: ZeroWave022 <36341766+ZeroWave022@users.noreply.github.com> Date: Mon, 24 Apr 2023 20:08:19 +0200 Subject: [PATCH 3/3] fix: Provide Client to Amount whenever possible --- src/structures/ExchangeRate.ts | 10 ++++++++-- src/structures/TipccClient.ts | 13 +++++++------ src/structures/Transaction.ts | 6 ++++-- src/structures/Wallet.ts | 13 ++++++++++--- 4 files changed, 29 insertions(+), 13 deletions(-) diff --git a/src/structures/ExchangeRate.ts b/src/structures/ExchangeRate.ts index ce28b79..299acee 100644 --- a/src/structures/ExchangeRate.ts +++ b/src/structures/ExchangeRate.ts @@ -1,5 +1,6 @@ import { Amount } from './Amount'; import type { APIExchangeRate } from '@tipccjs/tipcc-api-types'; +import { TipccClient } from './TipccClient'; /** * A class for storing an API exchange rate for a cryptocurrency. @@ -16,13 +17,18 @@ export class ExchangeRate { /** The USD value of {@link ExchangeRate.code currency} */ public usdValue?: Amount; + /** The client that instantiated this */ + public client: TipccClient | undefined; + /** * Create an ExchangeRate. * @param payload The rate from the API */ - constructor(payload: APIExchangeRate) { + constructor(payload: APIExchangeRate, client?: TipccClient) { + if (client) this.client = client; this.code = payload.code; this.name = payload.name; - if (payload.usd_value) this.usdValue = new Amount(payload.usd_value); + if (payload.usd_value) + this.usdValue = new Amount(payload.usd_value, this.client); } } diff --git a/src/structures/TipccClient.ts b/src/structures/TipccClient.ts index ee5bd70..09b0dd3 100644 --- a/src/structures/TipccClient.ts +++ b/src/structures/TipccClient.ts @@ -162,7 +162,7 @@ export class TipccClient extends EventEmitter { for (const transaction of transactions) { if (!this.cryptos.get(transaction.amount.currency)) await this.cryptos.refresh(); - this.emit(transaction.type, new Transaction(transaction)); + this.emit(transaction.type, new Transaction(transaction, this)); } this.lastPoll = now; @@ -203,7 +203,7 @@ export class TipccClient extends EventEmitter { 'GET', Routes.currenciesRates(), )) as RESTGetAPICurrenciesRatesResult; - return rates.map((r) => new ExchangeRate(r)); + return rates.map((r) => new ExchangeRate(r, this)); } public on(s: K, f: (arg: Events[K]) => void): this { @@ -238,7 +238,7 @@ export class TipccClient extends EventEmitter { Routes.accountWalletTransactions(), options, )) as RESTGetAPIAccountTransactionsResult; - return transactions.map((t) => new Transaction(t)); + return transactions.map((t) => new Transaction(t, this)); } /** @@ -249,7 +249,7 @@ export class TipccClient extends EventEmitter { 'GET', Routes.currenciesRates(), )) as RESTGetAPICurrenciesRatesResult; - return rates.map((r) => new ExchangeRate(r)); + return rates.map((r) => new ExchangeRate(r, this)); } /** @@ -262,7 +262,7 @@ export class TipccClient extends EventEmitter { Routes.accountWalletTransaction(id), )) as RESTGetAPIAccountTransactionResult; if (!transaction) return null; - return new Transaction(transaction); + return new Transaction(transaction, this); } /** @@ -306,6 +306,7 @@ export class TipccClient extends EventEmitter { currency, }, }, + this, ); } @@ -317,6 +318,6 @@ export class TipccClient extends EventEmitter { 'GET', Routes.accountWallets(), )) as RESTGetAPIAccountWalletsResult; - return wallets.map((w) => new Wallet(w)); + return wallets.map((w) => new Wallet(w, this)); } } diff --git a/src/structures/Transaction.ts b/src/structures/Transaction.ts index fd99474..4387d82 100644 --- a/src/structures/Transaction.ts +++ b/src/structures/Transaction.ts @@ -54,8 +54,10 @@ export class Transaction { this.id = payload.id; this.type = payload.type; this.amount = new Amount(payload.amount, this.client); - this.fee = payload.fee ? new Amount(payload.fee) : null; - this.usdValue = payload.usd_value ? new Amount(payload.usd_value, this.client) : null; + this.fee = payload.fee ? new Amount(payload.fee, this.client) : null; + this.usdValue = payload.usd_value + ? new Amount(payload.usd_value, this.client) + : null; this.service = payload.service; this.chatId = payload.chat_id; this.subchatId = payload.subchat_id; diff --git a/src/structures/Wallet.ts b/src/structures/Wallet.ts index bc5f51a..9142d47 100644 --- a/src/structures/Wallet.ts +++ b/src/structures/Wallet.ts @@ -1,5 +1,6 @@ import { Amount } from './Amount'; import type { APIWallet } from '@tipccjs/tipcc-api-types'; +import { TipccClient } from './TipccClient'; /** * A class for storing an API wallet. @@ -19,14 +20,20 @@ export class Wallet { /** The USD value of this wallet's balance */ public usdValue: Amount | null = null; + /** The client that instantiated this */ + public client: TipccClient | undefined; + /** * Create a Wallet. * @param payload The wallet from the API */ - constructor(payload: APIWallet) { + constructor(payload: APIWallet, client?: TipccClient) { + if (client) this.client = client; this.code = payload.code; this.name = payload.name; - this.balance = new Amount(payload.balance); - this.usdValue = payload.usd_value ? new Amount(payload.usd_value) : null; + this.balance = new Amount(payload.balance, this.client); + this.usdValue = payload.usd_value + ? new Amount(payload.usd_value, this.client) + : null; } }