Skip to content

Commit

Permalink
fix: better number formatting (bgd-labs#25)
Browse files Browse the repository at this point in the history
* fix: better number formatting

* fix: improve generator

* fix: uncomment test

* Update src/20231029_AaveV3Ethereum_ACIPhaseII/ACIPhaseII_20231029.s.sol

* fix: add advanced input

* fix: add correct error
  • Loading branch information
sakulstra authored and Rozengarden committed Nov 2, 2023
1 parent c223665 commit 2ff5ecc
Show file tree
Hide file tree
Showing 8 changed files with 253 additions and 34 deletions.
2 changes: 1 addition & 1 deletion generator/features/__snapshots__/assetListing.spec.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,7 @@ contract AaveV3Ethereum_Test_20231023_Test is ProtocolV3TestBase {
AaveV3Ethereum_Test_20231023 internal proposal;
function setUp() public {
vm.createSelectFork(vm.rpcUrl('mainnet'), 18480118);
vm.createSelectFork(vm.rpcUrl('mainnet'), 18484119);
proposal = new AaveV3Ethereum_Test_20231023();
}
Expand Down
2 changes: 1 addition & 1 deletion generator/features/mocks/configs.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import {Options} from '../../types';
import {Listing} from '../types';
import {CapsUpdate, Listing} from '../types';

export const MOCK_OPTIONS: Options = {
pools: ['AaveV3Ethereum'],
Expand Down
80 changes: 80 additions & 0 deletions generator/prompts.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import {expect, describe, it} from 'vitest';
import {render} from '@inquirer/testing';
import {
numberInput,
percentInput,
transformNumberToHumanReadable,
transformNumberToPercent,
translateJsNumberToSol,
translateJsPercentToSol,
} from './prompts';

describe('prompts', () => {
describe('numberInput', () => {
it('handles "yes"', async () => {
const {answer, events, getScreen} = await render(numberInput, {
message: 'Enter number?',
});

expect(getScreen()).toMatchInlineSnapshot('"? Enter number? (KEEP_CURRENT)"');

events.type('yes112.3');
expect(getScreen()).toMatchInlineSnapshot('"? Enter number? 1,123"');

events.keypress('enter');
await expect(answer).resolves.toEqual('1_123');
});
});

describe('percentInput', () => {
it('handles "yes"', async () => {
const {answer, events, getScreen} = await render(percentInput, {
message: 'Enter number?',
});

expect(getScreen()).toMatchInlineSnapshot('"? Enter number? (KEEP_CURRENT)"');

events.type('yes12.3');
expect(getScreen()).toMatchInlineSnapshot('"? Enter number? 12.3 %"');

events.keypress('enter');
await expect(answer).resolves.toEqual('12_30');
});
});
/**
* Transformers are here to format the input based on a users input
* They do not change the users input value though, the effect is purely visual
*/
describe('transforms', () => {
it('transformNumberToHumanReadable: should return a human readable full number', () => {
expect(transformNumberToHumanReadable('1000')).toBe('1,000');
expect(transformNumberToHumanReadable('1000000')).toBe('1,000,000');
});

it('transformNumberToPercent: should return a human readable % number', () => {
expect(transformNumberToPercent('100')).toBe('100 %');
expect(transformNumberToPercent('3333.33')).toBe('3,333.33 %');
expect(transformNumberToPercent('0.33')).toBe('0.33 %');
expect(transformNumberToPercent('0.3')).toBe('0.3 %');
});
});

/**
* Translates, translate the js input value to solidity
*/
describe('translate', () => {
it('translateJsNumberToSol: should properly translate values', () => {
expect(translateJsNumberToSol('0')).toBe('0');
expect(translateJsNumberToSol('1000')).toBe('1_000');
expect(translateJsNumberToSol('1000000')).toBe('1_000_000');
});

it('translateJsPercentToSol: should properly translate % values', () => {
expect(translateJsPercentToSol('0')).toBe('0');
expect(translateJsPercentToSol('100')).toBe('100_00');
expect(translateJsPercentToSol('3333.33')).toBe('3_333_33');
expect(translateJsPercentToSol('0.33')).toBe('33');
expect(translateJsPercentToSol('0.3')).toBe('30');
});
});
});
80 changes: 49 additions & 31 deletions generator/prompts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import {checkbox, input, select} from '@inquirer/prompts';
import {ENGINE_FLAGS, PoolIdentifier} from './types';
import {getAssets, getEModes} from './common';
import {Hex, getAddress, isAddress} from 'viem';
import {advancedInput} from './prompts/advancedInput';

// VALIDATION
function isNumber(value: string) {
Expand All @@ -19,29 +20,39 @@ function isAddressOrKeepCurrent(value: string) {
}

// TRANSFORMS
function transformNumberToPercent(value: string) {
export function transformNumberToPercent(value: string) {
if (value && isNumber(value)) {
if (Number(value) <= 9) value = value.padStart(2, '0');
return value.replace(/(?=(\d{2}$)+(?!\d))/g, '.') + ' %';
return (
new Intl.NumberFormat('en-us', {
maximumFractionDigits: 2,
}).format(value as unknown as number) + ' %'
);
}
return value;
}

function transformNumberToHumanReadable(value: string) {
export function transformNumberToHumanReadable(value: string) {
if (value && isNumber(value)) {
return value.replace(/(?=(\d{3}$)+(?!\d))/g, '.');
return new Intl.NumberFormat('en-us').format(BigInt(value));
}
return value;
}

// TRANSLATIONS
function translateJsPercentToSol(value: string, bpsToRay?: boolean) {
export function translateJsPercentToSol(value: string, bpsToRay?: boolean) {
if (value === ENGINE_FLAGS.KEEP_CURRENT) return `EngineFlags.KEEP_CURRENT`;
if (bpsToRay) return `_bpsToRay(${value.replace(/(?=(\d{2}$))/g, '_')})`;
return value.replace(/(?=(\d{2}$)+(?!\d))/g, '_');
}

function translateJsNumberToSol(value: string) {
const formattedValue = new Intl.NumberFormat('en-us', {
maximumFractionDigits: 2,
minimumFractionDigits: 2,
}).format(value as unknown as number);
const _value = (
Number(value) >= 1 ? formattedValue : formattedValue.replace(/^0\.0*(?=[0-9])/, '')
).replace(/[\.,]/g, '_');
if (bpsToRay) return `_bpsToRay(${_value})`;
return _value;
}

export function translateJsNumberToSol(value: string) {
if (value === ENGINE_FLAGS.KEEP_CURRENT) return `EngineFlags.KEEP_CURRENT`;
return String(value).replace(/\B(?=(\d{3})+(?!\d))/g, '_');
}
Expand Down Expand Up @@ -118,31 +129,38 @@ interface PercentInputPrompt<T extends boolean> extends GenericPrompt<T> {

export type PercentInputValues = typeof ENGINE_FLAGS.KEEP_CURRENT | string;

export async function percentInput<T extends boolean>({
message,
disableKeepCurrent,
toRay,
}: PercentInputPrompt<T>): Promise<
T extends true ? PercentInputValues : Exclude<PercentInputValues, 'KEEP_CURRENT'>
> {
const value = await input({
message,
transformer: transformNumberToPercent,
validate: disableKeepCurrent ? isNumber : isNumberOrKeepCurrent,
...(disableKeepCurrent ? {} : {default: ENGINE_FLAGS.KEEP_CURRENT}),
});
export async function percentInput<T extends boolean>(
{message, disableKeepCurrent, toRay}: PercentInputPrompt<T>,
opts
): Promise<T extends true ? PercentInputValues : Exclude<PercentInputValues, 'KEEP_CURRENT'>> {
const value = await advancedInput(
{
message,
transformer: transformNumberToPercent,
validate: disableKeepCurrent ? isNumber : isNumberOrKeepCurrent,
...(disableKeepCurrent ? {} : {default: ENGINE_FLAGS.KEEP_CURRENT}),
pattern: /^[0-9]*\.?[0-9]*$/,
patternError: 'Only decimal numbers are allowed (e.g. 1.1)',
},
opts
);
return translateJsPercentToSol(value, toRay);
}

export type NumberInputValues = typeof ENGINE_FLAGS.KEEP_CURRENT | string;

export async function numberInput({message, disableKeepCurrent}: GenericPrompt) {
const value = await input({
message,
transformer: transformNumberToHumanReadable,
validate: disableKeepCurrent ? isNumber : isNumberOrKeepCurrent,
...(disableKeepCurrent ? {} : {default: ENGINE_FLAGS.KEEP_CURRENT}),
});
export async function numberInput({message, disableKeepCurrent}: GenericPrompt, opts) {
const value = await advancedInput(
{
message,
transformer: transformNumberToHumanReadable,
validate: disableKeepCurrent ? isNumber : isNumberOrKeepCurrent,
...(disableKeepCurrent ? {} : {default: ENGINE_FLAGS.KEEP_CURRENT}),
pattern: /^[0-9]*$/,
patternError: 'Only full numbers are allowed',
},
opts
);
return translateJsNumberToSol(value);
}

Expand Down
96 changes: 96 additions & 0 deletions generator/prompts/advancedInput.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import {
createPrompt,
useState,
useKeypress,
usePrefix,
isEnterKey,
isBackspaceKey,
type PromptConfig,
} from '@inquirer/core';
import type {} from '@inquirer/type';
import chalk from 'chalk';

type InputConfig = PromptConfig<{
default?: string;
transformer?: (value: string, {isFinal}: {isFinal: boolean}) => string;
validate?: (value: string) => boolean | string | Promise<string | boolean>;
pattern?: RegExp;
patternError?: string;
}>;

/**
* It's a modified input prompt allowing to specify a pattern
* The input will simply discard any non conform input and show an error
*/
export const advancedInput = createPrompt<string, InputConfig>((config, done) => {
const {validate = () => true, pattern, patternError} = config;
const [status, setStatus] = useState<string>('pending');
const [defaultValue = '', setDefaultValue] = useState<string | undefined>(config.default);
const [errorMsg, setError] = useState<string | undefined>(undefined);
const [value, setValue] = useState<string>('');

const isLoading = status === 'loading';
const prefix = usePrefix(isLoading);

useKeypress(async (key, rl) => {
// Ignore keypress while our prompt is doing other processing.
if (status !== 'pending') {
return;
}

if (!pattern || pattern?.test(rl.line)) {
if (isEnterKey(key)) {
const answer = value || defaultValue;
setStatus('loading');
const isValid = await validate(answer);
if (isValid === true) {
setValue(answer);
setStatus('done');
done(answer);
} else {
// Reset the readline line value to the previous value. On line event, the value
// get cleared, forcing the user to re-enter the value instead of fixing it.
rl.write(value);
setError(isValid || 'You must provide a valid value');
setStatus('pending');
}
} else if (isBackspaceKey(key) && !value) {
setDefaultValue(undefined);
} else if (key.name === 'tab' && !value) {
setDefaultValue(undefined);
rl.clearLine(0); // Remove the tab character.
rl.write(defaultValue);
setValue(defaultValue);
} else {
setValue(rl.line);
setError(undefined);
}
} else {
const line = rl.line;
rl.clearLine(0);
rl.write(line.slice(0, -1));
setError(patternError);
}
});

const message = chalk.bold(config.message);
let formattedValue = value;
if (typeof config.transformer === 'function') {
formattedValue = config.transformer(value, {isFinal: status === 'done'});
}
if (status === 'done') {
formattedValue = chalk.cyan(formattedValue);
}

let defaultStr = '';
if (defaultValue && status !== 'done' && !value) {
defaultStr = chalk.dim(` (${defaultValue})`);
}

let error = '';
if (errorMsg) {
error = chalk.red(`> ${errorMsg}`);
}

return [`${prefix} ${message}${defaultStr} ${formattedValue}`, error];
});
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
},
"homepage": "https://github.com/bgd-labs/aave-proposals-v3#readme",
"devDependencies": {
"@types/node": "^20.8.10",
"prettier": "2.8.7",
"prettier-plugin-solidity": "1.1.3",
"vitest": "^0.34.6"
Expand All @@ -32,6 +33,7 @@
"@bgd-labs/aave-address-book": "^2.8.0",
"@bgd-labs/aave-cli": "0.0.27-0a01f2a07efe0ec4c875cf479004d20350235f64.0",
"@inquirer/prompts": "^3.2.0",
"@inquirer/testing": "^2.1.8",
"commander": "^11.0.0",
"tsx": "^3.13.0",
"viem": "^1.16.6"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ contract DeployEthereum is EthereumScript {
IPayloadsControllerCore.ExecutionAction[]
memory actions = new IPayloadsControllerCore.ExecutionAction[](1);
actions[0] = GovV3Helpers.buildAction(address(payload0));

// register action at payloadsController
GovV3Helpers.createPayload(actions);
}
Expand Down
24 changes: 24 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -294,6 +294,18 @@
chalk "^4.1.2"
figures "^3.2.0"

"@inquirer/testing@^2.1.8":
version "2.1.8"
resolved "https://registry.yarnpkg.com/@inquirer/testing/-/testing-2.1.8.tgz#809dffe6a4891100c54eee02ae99e3cd26b8425e"
integrity sha512-PPCiS5wN/MDjeJRQuMZa3cUDwsVmmB9wwbWNSNoC3ciPUwhX8fHdC0Um/gKMlF7EnaZaPa+AOrA8zLo/zJLjPw==
dependencies:
"@inquirer/type" "^1.1.5"
"@types/mute-stream" "^0.0.2"
"@types/node" "^20.8.2"
ansi-escapes "^4.3.2"
mute-stream "^1.0.0"
strip-ansi "^6.0.1"

"@inquirer/type@^1.1.5":
version "1.1.5"
resolved "https://registry.yarnpkg.com/@inquirer/type/-/type-1.1.5.tgz#b8c171f755859c8159b10e41e1e3a88f0ca99d7f"
Expand Down Expand Up @@ -451,6 +463,13 @@
dependencies:
undici-types "~5.25.1"

"@types/node@^20.8.10":
version "20.8.10"
resolved "https://registry.yarnpkg.com/@types/node/-/node-20.8.10.tgz#a5448b895c753ae929c26ce85cab557c6d4a365e"
integrity sha512-TlgT8JntpcbmKUFzjhsyhGfP2fsiz1Mv56im6enJ905xG1DAYesxJaeSbGqQmAw8OWPdhyJGhGSQGKRNJ45u9w==
dependencies:
undici-types "~5.26.4"

"@types/normalize-package-data@^2.4.0":
version "2.4.2"
resolved "https://registry.yarnpkg.com/@types/normalize-package-data/-/normalize-package-data-2.4.2.tgz#9b0e3e8533fe5024ad32d6637eb9589988b6fdca"
Expand Down Expand Up @@ -1789,6 +1808,11 @@ undici-types@~5.25.1:
resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.25.3.tgz#e044115914c85f0bcbb229f346ab739f064998c3"
integrity sha512-Ga1jfYwRn7+cP9v8auvEXN1rX3sWqlayd4HP7OKk4mZWylEmu3KzXDUGrQUN6Ol7qo1gPvB2e5gX6udnyEPgdA==

undici-types@~5.26.4:
version "5.26.5"
resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.26.5.tgz#bcd539893d00b56e964fd2657a4866b221a65617"
integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==

util-deprecate@^1.0.1:
version "1.0.2"
resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"
Expand Down

0 comments on commit 2ff5ecc

Please sign in to comment.