From 3faaf52a4c0347b65cb7acdb90b191c7ea313986 Mon Sep 17 00:00:00 2001 From: Mat Date: Mon, 8 Apr 2024 10:47:44 -0700 Subject: [PATCH] Add new currency render function --- .../src/mining/webhooks/webhookNotifier.ts | 8 +-- ironfish/src/primitives/rawTransaction.ts | 12 ++-- .../rpc/routes/wallet/createTransaction.ts | 9 ++- ironfish/src/utils/currency.test.ts | 59 +++++++++++++++++++ ironfish/src/utils/currency.ts | 49 +++++++++++++++ ironfish/src/wallet/errors.ts | 17 +++--- 6 files changed, 135 insertions(+), 19 deletions(-) diff --git a/ironfish/src/mining/webhooks/webhookNotifier.ts b/ironfish/src/mining/webhooks/webhookNotifier.ts index c6ab70e244..e636b1e54a 100644 --- a/ironfish/src/mining/webhooks/webhookNotifier.ts +++ b/ironfish/src/mining/webhooks/webhookNotifier.ts @@ -54,10 +54,11 @@ export abstract class WebhookNotifier { ): void { const total = outputs.reduce((m, c) => BigInt(c.amount) + m, BigInt(0)) + const renderedValue = CurrencyUtils.render(total, true) this.sendText( `Successfully created payout of ${shareCount} shares to ${ outputs.length - } users for ${CurrencyUtils.renderIron(total, true)} in transaction ${ + } users for ${renderedValue} in transaction ${ this.explorer?.getTransactionUrl(transactionHashHex) ?? `\`${transactionHashHex}\`` }. Transaction pending (${payoutPeriodId})`, ) @@ -76,10 +77,9 @@ export abstract class WebhookNotifier { ): void { const total = outputs.reduce((m, c) => BigInt(c.amount) + m, BigInt(0)) + const renderedValue = CurrencyUtils.render(total, true) this.sendText( - `Creating payout of ${shareCount} shares to ${ - outputs.length - } users for ${CurrencyUtils.renderIron(total, true)} (${payoutPeriodId})`, + `Creating payout of ${shareCount} shares to ${outputs.length} users for ${renderedValue} (${payoutPeriodId})`, ) } diff --git a/ironfish/src/primitives/rawTransaction.ts b/ironfish/src/primitives/rawTransaction.ts index 4343c3bd9c..9f24f5b8e7 100644 --- a/ironfish/src/primitives/rawTransaction.ts +++ b/ironfish/src/primitives/rawTransaction.ts @@ -132,10 +132,10 @@ export class RawTransaction { for (const mint of this.mints) { if (mint.value > MAX_MINT_OR_BURN_VALUE) { + const renderedValue = CurrencyUtils.renderOre(mint.value) + const renderedMax = CurrencyUtils.renderOre(MAX_MINT_OR_BURN_VALUE) throw new Error( - `Cannot post transaction. Mint value ${CurrencyUtils.renderIron( - mint.value, - )} exceededs maximum ${CurrencyUtils.renderIron(MAX_MINT_OR_BURN_VALUE)}. `, + `Cannot post transaction. Mint value ${renderedValue} exceededs maximum ${renderedMax}.`, ) } const asset = new Asset(mint.creator, mint.name, mint.metadata) @@ -145,10 +145,10 @@ export class RawTransaction { for (const burn of this.burns) { if (burn.value > MAX_MINT_OR_BURN_VALUE) { + const renderedValue = CurrencyUtils.renderOre(burn.value) + const renderedMax = CurrencyUtils.renderOre(MAX_MINT_OR_BURN_VALUE) throw new Error( - `Cannot post transaction. Burn value ${CurrencyUtils.renderIron( - burn.value, - )} exceededs maximum ${CurrencyUtils.renderIron(MAX_MINT_OR_BURN_VALUE)}`, + `Cannot post transaction. Burn value ${renderedValue} exceededs maximum ${renderedMax}`, ) } diff --git a/ironfish/src/rpc/routes/wallet/createTransaction.ts b/ironfish/src/rpc/routes/wallet/createTransaction.ts index d0f2ee1114..cd4082a243 100644 --- a/ironfish/src/rpc/routes/wallet/createTransaction.ts +++ b/ironfish/src/rpc/routes/wallet/createTransaction.ts @@ -112,7 +112,7 @@ routes.register => { - AssertHasRpcContext(request, node, 'wallet') + AssertHasRpcContext(request, node, 'wallet', 'assetsVerifier') const account = getAccount(node.wallet, request.data.account) @@ -227,7 +227,12 @@ routes.register { @@ -42,6 +43,64 @@ describe('CurrencyUtils', () => { expect(CurrencyUtils.decodeIron('0.00000999')).toBe(999n) }) + describe('render', () => { + // Randomly generated custom asset ID + const assetId = '1a75bf033c1c1925cfcd1a77461364e77c6e861c2a3acabaf9e398e980146651' + + it('should render iron with no extra parameters with 8 decimal places', () => { + expect(CurrencyUtils.render(0n)).toEqual('0.00000000') + expect(CurrencyUtils.render(1n)).toEqual('0.00000001') + expect(CurrencyUtils.render(100n)).toEqual('0.00000100') + expect(CurrencyUtils.render(10000n)).toEqual('0.00010000') + expect(CurrencyUtils.render(100000000n)).toEqual('1.00000000') + }) + + it('should include IRON for the iron asset ticker', () => { + expect(CurrencyUtils.render(1n, true)).toEqual('$IRON 0.00000001') + }) + + it('should still render iron with 8 decimals and $IRON symbol even if parameters are given', () => { + expect( + CurrencyUtils.render(1n, false, { + id: Asset.nativeId().toString('hex'), + decimals: 2, + symbol: 'FOO', + }), + ).toEqual('0.00000001') + expect( + CurrencyUtils.render(1n, true, { + id: Asset.nativeId().toString('hex'), + decimals: 2, + symbol: 'FOO', + }), + ).toEqual('$IRON 0.00000001') + }) + + it('should render an asset value with 0 decimals by default', () => { + expect(CurrencyUtils.render(1n, false, { id: assetId })).toEqual('1') + expect(CurrencyUtils.render(1n, true, { id: assetId })).toEqual(`${assetId} 1`) + }) + + it('should render an asset value with the given decimals and symbol', () => { + expect(CurrencyUtils.render(1n, false, { id: assetId, decimals: 2 })).toEqual('0.01') + expect(CurrencyUtils.render(1n, true, { id: assetId, decimals: 2 })).toEqual( + `${assetId} 0.01`, + ) + + expect(CurrencyUtils.render(100n, false, { id: assetId, decimals: 2 })).toEqual('1.00') + expect(CurrencyUtils.render(100n, true, { id: assetId, decimals: 2 })).toEqual( + `${assetId} 1.00`, + ) + + expect( + CurrencyUtils.render(100n, false, { id: assetId, decimals: 2, symbol: 'FOO' }), + ).toEqual('1.00') + expect( + CurrencyUtils.render(100n, true, { id: assetId, decimals: 2, symbol: 'FOO' }), + ).toEqual(`FOO 1.00`) + }) + }) + it('renderIron', () => { expect(CurrencyUtils.renderIron(0n)).toEqual('0.00000000') expect(CurrencyUtils.renderIron(1n)).toEqual('0.00000001') diff --git a/ironfish/src/utils/currency.ts b/ironfish/src/utils/currency.ts index e12f187e82..1e710008c6 100644 --- a/ironfish/src/utils/currency.ts +++ b/ironfish/src/utils/currency.ts @@ -58,6 +58,54 @@ export class CurrencyUtils { return amount.toString() } + /** + * Renders values for human-readable purposes: + * - Renders $IRON in the major denomination, with 8 decimal places + * - If a custom asset, and `decimals` is provided, it will render the custom + * asset in the major denomination with the decimal places + * - If a custom asset, and `decimals` is not provided, it will render the + * custom asset in the minor denomination with no decimal places + */ + static render( + amount: bigint | string, + includeSymbol: boolean = false, + asset?: { + id?: string + decimals?: number + symbol?: string + }, + ): string { + if (typeof amount === 'string') { + amount = this.decode(amount) + } + + // If an asset ID was provided, check if it is the native asset. Otherwise, + // we can only assume that the amount is in the native asset + const isNativeAsset = asset?.id ? isNativeIdentifier(asset?.id) : true + + // Default to displaying 0 decimal places for custom assets + let decimals = 0 + if (isNativeAsset) { + // Hard-code the amount of decimals in the native asset + decimals = IRON_DECIMAL_PLACES + } else if (asset?.decimals) { + decimals = asset.decimals + } + + const majorDenominationAmount = FixedNumberUtils.render(amount, decimals) + + if (includeSymbol) { + let symbol = '$IRON' + + if (asset?.id && !isNativeAsset) { + symbol = asset?.symbol || asset?.id + } + return `${symbol} ${majorDenominationAmount}` + } + + return majorDenominationAmount + } + /* * Renders ore as iron for human-readable purposes */ @@ -115,6 +163,7 @@ export function isParseFixedError(error: unknown): error is ParseFixedError { ) } +const IRON_DECIMAL_PLACES = 8 export const ORE_TO_IRON = 100000000 export const MINIMUM_ORE_AMOUNT = 0n export const MAXIMUM_ORE_AMOUNT = 2n ** 64n diff --git a/ironfish/src/wallet/errors.ts b/ironfish/src/wallet/errors.ts index 3fe3243ebe..fd0e559874 100644 --- a/ironfish/src/wallet/errors.ts +++ b/ironfish/src/wallet/errors.ts @@ -7,16 +7,19 @@ import { CurrencyUtils } from '../utils' export class NotEnoughFundsError extends Error { name = this.constructor.name + assetId: string + amount: bigint + amountNeeded: bigint constructor(assetId: Buffer, amount: bigint, amountNeeded: bigint) { super() - this.message = `Insufficient funds: Needed ${CurrencyUtils.renderIron( - amountNeeded, - true, - assetId.toString('hex'), - )} but have ${CurrencyUtils.renderIron( - amount, - )} available to spend. Please fund your account and/or wait for any pending transactions to be confirmed.'` + this.assetId = assetId.toString('hex') + this.amount = amount + this.amountNeeded = amountNeeded + + const renderedAmountNeeded = CurrencyUtils.render(amountNeeded, true, { id: this.assetId }) + const renderedAmount = CurrencyUtils.render(amount) + this.message = `Insufficient funds: Needed ${renderedAmountNeeded} but have ${renderedAmount} available to spend. Please fund your account and/or wait for any pending transactions to be confirmed.'` } }