Skip to content

Commit

Permalink
Add new currency render function (#4879)
Browse files Browse the repository at this point in the history
* Add new currency render function

* Use new render function signature based off RpcAsset changes

* code review changes
  • Loading branch information
mat-if committed Apr 9, 2024
1 parent c0d0777 commit d6856bc
Show file tree
Hide file tree
Showing 7 changed files with 148 additions and 21 deletions.
8 changes: 4 additions & 4 deletions ironfish/src/mining/webhooks/webhookNotifier.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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})`,
)
Expand All @@ -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})`,
)
}

Expand Down
12 changes: 6 additions & 6 deletions ironfish/src/primitives/rawTransaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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}`,
)
}

Expand Down
14 changes: 12 additions & 2 deletions ironfish/src/rpc/routes/wallet/createTransaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ routes.register<typeof CreateTransactionRequestSchema, CreateTransactionResponse
`${ApiNamespace.wallet}/createTransaction`,
CreateTransactionRequestSchema,
async (request, node): Promise<void> => {
AssertHasRpcContext(request, node, 'wallet')
AssertHasRpcContext(request, node, 'wallet', 'assetsVerifier')

const account = getAccount(node.wallet, request.data.account)

Expand Down Expand Up @@ -227,7 +227,17 @@ routes.register<typeof CreateTransactionRequestSchema, CreateTransactionResponse
})
} catch (e) {
if (e instanceof NotEnoughFundsError) {
throw new RpcValidationError(e.message, 400, RPC_ERROR_CODES.INSUFFICIENT_BALANCE)
// We are overriding the error message from the node to include verified assets in their appropriate denomination.
const assetData = node.assetsVerifier.getAssetData(e.assetId)
const renderedAmountNeeded = CurrencyUtils.render(
e.amountNeeded,
true,
e.assetId,
assetData,
)
const renderedAmount = CurrencyUtils.render(e.amount, false, e.assetId, assetData)
const 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.'`
throw new RpcValidationError(message, 400, RPC_ERROR_CODES.INSUFFICIENT_BALANCE)
}
throw e
}
Expand Down
14 changes: 12 additions & 2 deletions ironfish/src/rpc/routes/wallet/sendTransaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ routes.register<typeof SendTransactionRequestSchema, SendTransactionResponse>(
`${ApiNamespace.wallet}/sendTransaction`,
SendTransactionRequestSchema,
async (request, context): Promise<void> => {
AssertHasRpcContext(request, context, 'wallet')
AssertHasRpcContext(request, context, 'wallet', 'assetsVerifier')

Assert.isNotNull(context.wallet.nodeClient)
const account = getAccount(context.wallet, request.data.account)
Expand Down Expand Up @@ -165,7 +165,17 @@ routes.register<typeof SendTransactionRequestSchema, SendTransactionResponse>(
})
} catch (e) {
if (e instanceof NotEnoughFundsError) {
throw new RpcValidationError(e.message, 400, RPC_ERROR_CODES.INSUFFICIENT_BALANCE)
// We are overriding the error message from the node to include verified assets in their appropriate denomination.
const assetData = context.assetsVerifier.getAssetData(e.assetId)
const renderedAmountNeeded = CurrencyUtils.render(
e.amountNeeded,
true,
e.assetId,
assetData,
)
const renderedAmount = CurrencyUtils.render(e.amount, false, e.assetId, assetData)
const 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.'`
throw new RpcValidationError(message, 400, RPC_ERROR_CODES.INSUFFICIENT_BALANCE)
}
throw e
}
Expand Down
57 changes: 57 additions & 0 deletions ironfish/src/utils/currency.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
import { Asset } from '@ironfish/rust-nodejs'
import { CurrencyUtils } from './currency'

describe('CurrencyUtils', () => {
Expand Down Expand Up @@ -42,6 +43,62 @@ 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, Asset.nativeId().toString('hex'), {
decimals: 2,
symbol: 'FOO',
}),
).toEqual('0.00000001')
expect(
CurrencyUtils.render(1n, true, 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, assetId)).toEqual('1')
expect(CurrencyUtils.render(1n, true, assetId)).toEqual(`${assetId} 1`)
})

it('should render an asset value with the given decimals and symbol', () => {
expect(CurrencyUtils.render(1n, false, assetId, { decimals: 2 })).toEqual('0.01')
expect(CurrencyUtils.render(1n, true, assetId, { decimals: 2 })).toEqual(
`${assetId} 0.01`,
)

expect(CurrencyUtils.render(100n, false, assetId, { decimals: 2 })).toEqual('1.00')
expect(CurrencyUtils.render(100n, true, assetId, { decimals: 2 })).toEqual(
`${assetId} 1.00`,
)

expect(
CurrencyUtils.render(100n, false, assetId, { decimals: 2, symbol: 'FOO' }),
).toEqual('1.00')
expect(CurrencyUtils.render(100n, true, 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')
Expand Down
47 changes: 47 additions & 0 deletions ironfish/src/utils/currency.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,51 @@ 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,
assetId?: string,
verifiedAssetMetadata?: {
decimals?: number
symbol?: string
},
): string {
if (typeof amount === 'string') {
amount = this.decode(amount)
}

let decimals: number
let symbol: string
// 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 = !assetId || isNativeIdentifier(assetId)
if (isNativeAsset) {
// Hard-code the amount of decimals in the native asset
decimals = IRON_DECIMAL_PLACES
symbol = IRON_SYMBOL
} else {
// Default to displaying 0 decimal places for custom assets
decimals = verifiedAssetMetadata?.decimals || 0
symbol = verifiedAssetMetadata?.symbol || assetId
}

const majorDenominationAmount = FixedNumberUtils.render(amount, decimals)

if (includeSymbol) {
return `${symbol} ${majorDenominationAmount}`
}

return majorDenominationAmount
}

/*
* Renders ore as iron for human-readable purposes
*/
Expand Down Expand Up @@ -115,6 +160,8 @@ export function isParseFixedError(error: unknown): error is ParseFixedError {
)
}

const IRON_DECIMAL_PLACES = 8
const IRON_SYMBOL = '$IRON'
export const ORE_TO_IRON = 100000000
export const MINIMUM_ORE_AMOUNT = 0n
export const MAXIMUM_ORE_AMOUNT = 2n ** 64n
Expand Down
17 changes: 10 additions & 7 deletions ironfish/src/wallet/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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, 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.'`
}
}

Expand Down

0 comments on commit d6856bc

Please sign in to comment.