diff --git a/generator/common.ts b/generator/common.ts index bad4f0d96..c7010334c 100644 --- a/generator/common.ts +++ b/generator/common.ts @@ -106,3 +106,7 @@ export const CHAIN_TO_CHAIN_OBJECT = { Bsc: bsc, Gnosis: gnosis, }; + +export function flagAsRequired(message: string, required?: boolean) { + return required ? `${message}*` : message; +} diff --git a/generator/features/__snapshots__/assetListing.spec.ts.snap b/generator/features/__snapshots__/assetListing.spec.ts.snap index fa297e7e4..d713c080a 100644 --- a/generator/features/__snapshots__/assetListing.spec.ts.snap +++ b/generator/features/__snapshots__/assetListing.spec.ts.snap @@ -50,23 +50,23 @@ Copyright and related rights waived via [CC0](https://creativecommons.org/public \\"liqBonus\\": \\"5_00\\", \\"debtCeiling\\": \\"100_000\\", \\"liqProtocolFee\\": \\"20_00\\", - \\"enabledToBorrow\\": \\"ENABLED\\", - \\"flashloanable\\": \\"ENABLED\\", - \\"stableRateModeEnabled\\": \\"DISABLED\\", - \\"borrowableInIsolation\\": \\"DISABLED\\", - \\"withSiloedBorrowing\\": \\"DISABLED\\", + \\"enabledToBorrow\\": \\"EngineFlags.ENABLED\\", + \\"flashloanable\\": \\"EngineFlags.ENABLED\\", + \\"stableRateModeEnabled\\": \\"EngineFlags.DISABLED\\", + \\"borrowableInIsolation\\": \\"EngineFlags.DISABLED\\", + \\"withSiloedBorrowing\\": \\"EngineFlags.DISABLED\\", \\"reserveFactor\\": \\"20_00\\", \\"supplyCap\\": \\"10_000\\", \\"borrowCap\\": \\"5_000\\", \\"rateStrategyParams\\": { \\"optimalUtilizationRate\\": \\"_bpsToRay(80_00)\\", - \\"baseVariableBorrowRate\\": \\"_bpsToRay(0_00)\\", + \\"baseVariableBorrowRate\\": \\"_bpsToRay(0)\\", \\"variableRateSlope1\\": \\"_bpsToRay(10_00)\\", \\"variableRateSlope2\\": \\"_bpsToRay(100_00)\\", \\"stableRateSlope1\\": \\"_bpsToRay(10_00)\\", \\"stableRateSlope2\\": \\"_bpsToRay(100_00)\\", \\"baseStableRateOffset\\": \\"_bpsToRay(1_00)\\", - \\"stableRateExcessOffset\\": \\"_bpsToRay()\\", + \\"stableRateExcessOffset\\": \\"_bpsToRay(0)\\", \\"optimalStableToTotalDebtRatio\\": \\"_bpsToRay(10_00)\\" }, \\"eModeCategory\\": \\"AaveV3EthereumEModes.NONE\\", @@ -88,6 +88,7 @@ pragma solidity ^0.8.0; import {AaveV3Ethereum, AaveV3EthereumEModes} from 'aave-address-book/AaveV3Ethereum.sol'; import {AaveV3PayloadEthereum} from 'aave-helpers/v3-config-engine/AaveV3PayloadEthereum.sol'; +import {EngineFlags} from 'aave-helpers/v3-config-engine/EngineFlags.sol'; import {IAaveV3ConfigEngine} from 'aave-helpers/v3-config-engine/IAaveV3ConfigEngine.sol'; import {IV3RateStrategyFactory} from 'aave-helpers/v3-config-engine/IV3RateStrategyFactory.sol'; import {IERC20} from 'solidity-utils/contracts/oz-common/interfaces/IERC20.sol'; @@ -118,11 +119,11 @@ contract AaveV3Ethereum_Test_20231023 is AaveV3PayloadEthereum { assetSymbol: 'PSP', priceFeed: 0x72AFAECF99C9d9C8215fF44C77B94B99C28741e8, eModeCategory: AaveV3EthereumEModes.NONE, - enabledToBorrow: ENABLED, - stableRateModeEnabled: DISABLED, - borrowableInIsolation: DISABLED, - withSiloedBorrowing: DISABLED, - flashloanable: ENABLED, + enabledToBorrow: EngineFlags.ENABLED, + stableRateModeEnabled: EngineFlags.DISABLED, + borrowableInIsolation: EngineFlags.DISABLED, + withSiloedBorrowing: EngineFlags.DISABLED, + flashloanable: EngineFlags.ENABLED, ltv: 40_00, liqThreshold: 50_00, liqBonus: 5_00, @@ -133,13 +134,13 @@ contract AaveV3Ethereum_Test_20231023 is AaveV3PayloadEthereum { liqProtocolFee: 20_00, rateStrategyParams: IV3RateStrategyFactory.RateStrategyParams({ optimalUsageRatio: _bpsToRay(80_00), - baseVariableBorrowRate: _bpsToRay(0_00), + baseVariableBorrowRate: _bpsToRay(0), variableRateSlope1: _bpsToRay(10_00), variableRateSlope2: _bpsToRay(100_00), stableRateSlope1: _bpsToRay(10_00), stableRateSlope2: _bpsToRay(100_00), baseStableRateOffset: _bpsToRay(1_00), - stableRateExcessOffset: _bpsToRay(), + stableRateExcessOffset: _bpsToRay(0), optimalStableToTotalDebtRatio: _bpsToRay(10_00) }) }); @@ -167,7 +168,7 @@ contract AaveV3Ethereum_Test_20231023_Test is ProtocolV3TestBase { AaveV3Ethereum_Test_20231023 internal proposal; function setUp() public { - vm.createSelectFork(vm.rpcUrl('mainnet'), 18484119); + vm.createSelectFork(vm.rpcUrl('mainnet'), 18487480); proposal = new AaveV3Ethereum_Test_20231023(); } @@ -262,11 +263,11 @@ exports[`feature: assetListing > should return reasonable code 1`] = ` assetSymbol: \\"PSP\\", priceFeed: 0x72AFAECF99C9d9C8215fF44C77B94B99C28741e8, eModeCategory: AaveV3EthereumEModes.NONE, - enabledToBorrow: ENABLED, - stableRateModeEnabled: DISABLED, - borrowableInIsolation: DISABLED, - withSiloedBorrowing: DISABLED, - flashloanable: ENABLED, + enabledToBorrow: EngineFlags.ENABLED, + stableRateModeEnabled: EngineFlags.DISABLED, + borrowableInIsolation: EngineFlags.DISABLED, + withSiloedBorrowing: EngineFlags.DISABLED, + flashloanable: EngineFlags.ENABLED, ltv: 40_00, liqThreshold: 50_00, liqBonus: 5_00, @@ -277,13 +278,13 @@ exports[`feature: assetListing > should return reasonable code 1`] = ` liqProtocolFee: 20_00, rateStrategyParams: IV3RateStrategyFactory.RateStrategyParams({ optimalUsageRatio: _bpsToRay(80_00), - baseVariableBorrowRate: _bpsToRay(0_00), + baseVariableBorrowRate: _bpsToRay(0), variableRateSlope1: _bpsToRay(10_00), variableRateSlope2: _bpsToRay(100_00), stableRateSlope1: _bpsToRay(10_00), stableRateSlope2: _bpsToRay(100_00), baseStableRateOffset: _bpsToRay(1_00), - stableRateExcessOffset: _bpsToRay(), + stableRateExcessOffset: _bpsToRay(0), optimalStableToTotalDebtRatio: _bpsToRay(10_00) }) }); diff --git a/generator/features/__snapshots__/priceFeedsUpdate.spec.ts.snap b/generator/features/__snapshots__/priceFeedsUpdate.spec.ts.snap new file mode 100644 index 000000000..7787856d3 --- /dev/null +++ b/generator/features/__snapshots__/priceFeedsUpdate.spec.ts.snap @@ -0,0 +1,192 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`feature: priceFeedsUpdates > should properly generate files 1`] = ` +{ + "aip": "--- +title: \\"test\\" +author: \\"test\\" +discussions: \\"test\\" +--- + +## Simple Summary + +## Motivation + +## Specification + +## References + +- Implementation: [AaveV3Ethereum](https://github.com/bgd-labs/aave-proposals-v3/blob/main/src/20231023_AaveV3Ethereum_Test/AaveV3Ethereum_Test_20231023.sol) +- Tests: [AaveV3Ethereum](https://github.com/bgd-labs/aave-proposals-v3/blob/main/src/20231023_AaveV3Ethereum_Test/AaveV3Ethereum_Test_20231023.t.sol) +- [Snapshot](test) +- [Discussion](test) + +## Copyright + +Copyright and related rights waived via [CC0](https://creativecommons.org/publicdomain/zero/1.0/). +", + "jsonConfig": "{ + \\"rootOptions\\": { + \\"pools\\": [ + \\"AaveV3Ethereum\\" + ], + \\"title\\": \\"test\\", + \\"shortName\\": \\"Test\\", + \\"date\\": \\"20231023\\", + \\"author\\": \\"test\\", + \\"discussion\\": \\"test\\", + \\"snapshot\\": \\"test\\" + }, + \\"poolOptions\\": { + \\"AaveV3Ethereum\\": { + \\"configs\\": { + \\"PRICE_FEEDS_UPDATE\\": [ + { + \\"asset\\": \\"DAI\\", + \\"priceFeed\\": \\"0xae7ab96520de3a18e5e111b5eaab095312d7fe84\\" + } + ] + }, + \\"features\\": [ + \\"PRICE_FEEDS_UPDATE\\" + ] + } + } +}", + "payloads": [ + { + "contractName": "AaveV3Ethereum_Test_20231023", + "payload": "// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import {AaveV3EthereumAssets} from 'aave-address-book/AaveV3Ethereum.sol'; +import {AaveV3PayloadEthereum} from 'aave-helpers/v3-config-engine/AaveV3PayloadEthereum.sol'; +import {IAaveV3ConfigEngine} from 'aave-helpers/v3-config-engine/IAaveV3ConfigEngine.sol'; + +/** + * @title test + * @author test + * - Snapshot: test + * - Discussion: test + */ +contract AaveV3Ethereum_Test_20231023 is AaveV3PayloadEthereum { + function priceFeedsUpdates() + public + pure + override + returns (IAaveV3ConfigEngine.PriceFeedUpdate[] memory) + { + IAaveV3ConfigEngine.PriceFeedUpdate[] + memory priceFeedUpdates = new IAaveV3ConfigEngine.PriceFeedUpdate[](1); + + priceFeedUpdates[0] = IAaveV3ConfigEngine.PriceFeedUpdate({ + asset: AaveV3EthereumAssets.DAI_UNDERLYING, + priceFeed: 0xae7ab96520DE3A18E5e111B5EaAb095312D7fE84 + }); + + return priceFeedUpdates; + } +} +", + "test": "// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import {AaveV3Ethereum} from 'aave-address-book/AaveV3Ethereum.sol'; + +import 'forge-std/Test.sol'; +import {ProtocolV3TestBase, ReserveConfig} from 'aave-helpers/ProtocolV3TestBase.sol'; +import {AaveV3Ethereum_Test_20231023} from './AaveV3Ethereum_Test_20231023.sol'; + +/** + * @dev Test for AaveV3Ethereum_Test_20231023 + * command: make test-contract filter=AaveV3Ethereum_Test_20231023 + */ +contract AaveV3Ethereum_Test_20231023_Test is ProtocolV3TestBase { + AaveV3Ethereum_Test_20231023 internal proposal; + + function setUp() public { + vm.createSelectFork(vm.rpcUrl('mainnet'), 18487480); + proposal = new AaveV3Ethereum_Test_20231023(); + } + + /** + * @dev executes the generic test suite including e2e and config snapshots + */ + function test_defaultProposalExecution() public { + defaultTest('AaveV3Ethereum_Test_20231023', AaveV3Ethereum.POOL, address(proposal)); + } +} +", + }, + ], + "script": "// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import {GovV3Helpers, IPayloadsControllerCore, PayloadsControllerUtils} from 'aave-helpers/GovV3Helpers.sol'; +import {EthereumScript} from 'aave-helpers/ScriptUtils.sol'; +import {AaveV3Ethereum_Test_20231023} from './AaveV3Ethereum_Test_20231023.sol'; + +/** + * @dev Deploy Ethereum + * command: make deploy-ledger contract=src/20231023_AaveV3Ethereum_Test/Test_20231023.s.sol:DeployEthereum chain=mainnet + */ +contract DeployEthereum is EthereumScript { + function run() external broadcast { + // deploy payloads + AaveV3Ethereum_Test_20231023 payload0 = new AaveV3Ethereum_Test_20231023(); + + // compose action + IPayloadsControllerCore.ExecutionAction[] + memory actions = new IPayloadsControllerCore.ExecutionAction[](1); + actions[0] = GovV3Helpers.buildAction(address(payload0)); + + // register action at payloadsController + GovV3Helpers.createPayload(actions); + } +} + +/** + * @dev Create Proposal + * command: make deploy-ledger contract=src/20231023_AaveV3Ethereum_Test/Test_20231023.s.sol:CreateProposal chain=mainnet + */ +contract CreateProposal is EthereumScript { + function run() external { + // create payloads + PayloadsControllerUtils.Payload[] memory payloads = new PayloadsControllerUtils.Payload[](1); + + // compose actions for validation + IPayloadsControllerCore.ExecutionAction[] + memory actionsEthereum = new IPayloadsControllerCore.ExecutionAction[](1); + actionsEthereum[0] = GovV3Helpers.buildAction(address(0)); + payloads[0] = GovV3Helpers.buildMainnetPayload(vm, actionsEthereum); + + // create proposal + vm.startBroadcast(); + GovV3Helpers.createProposal2_5( + payloads, + GovV3Helpers.ipfsHashFile(vm, 'src/20231023_AaveV3Ethereum_Test/Test.md') + ); + } +} +", +} +`; + +exports[`feature: priceFeedsUpdates > should return reasonable code 1`] = ` +{ + "code": { + "fn": [ + "function priceFeedsUpdates() public pure override returns (IAaveV3ConfigEngine.PriceFeedUpdate[] memory) { + IAaveV3ConfigEngine.PriceFeedUpdate[] memory priceFeedUpdates = new IAaveV3ConfigEngine.PriceFeedUpdate[](1); + + priceFeedUpdates[0] = IAaveV3ConfigEngine.PriceFeedUpdate({ + asset: AaveV3EthereumAssets.DAI_UNDERLYING, + priceFeed: 0xae7ab96520DE3A18E5e111B5EaAb095312D7fE84 + }); + + return priceFeedUpdates; + }", + ], + }, +} +`; diff --git a/generator/features/assetListing.ts b/generator/features/assetListing.ts index fbf352f34..960a72394 100644 --- a/generator/features/assetListing.ts +++ b/generator/features/assetListing.ts @@ -1,5 +1,5 @@ import {CodeArtifact, FEATURE, FeatureModule, PoolIdentifier} from '../types'; -import {addressInput, eModeSelect, stringInput} from '../prompts'; +import {eModeSelect} from '../prompts'; import {fetchBorrowUpdate} from './borrowsUpdates'; import {fetchRateStrategyParamsV3} from './rateUpdates'; import {fetchCollateralUpdate} from './collateralsUpdates'; @@ -9,11 +9,13 @@ import {CHAIN_TO_CHAIN_OBJECT, getPoolChain} from '../common'; import {createPublicClient, getContract, http} from 'viem'; import {confirm} from '@inquirer/prompts'; import {TEST_EXECUTE_PROPOSAL} from '../utils/constants'; +import {addressPrompt, translateJsAddressToSol} from '../prompts/addressPrompt'; +import {stringPrompt} from '../prompts/stringPrompt'; async function fetchListing(pool: PoolIdentifier): Promise { - const asset = await addressInput({ + const asset = await addressPrompt({ message: 'Enter the address of the asset you want to list', - disableKeepCurrent: true, + required: true, }); const chain = getPoolChain(pool); @@ -56,13 +58,13 @@ async function fetchListing(pool: PoolIdentifier): Promise { const decimals = await erc20.read.decimals(); return { - assetSymbol: await stringInput({ + assetSymbol: await stringPrompt({ message: 'Enter the asset symbol', - disableKeepCurrent: true, + required: true, defaultValue: symbol, }), decimals, - priceFeed: await addressInput({message: 'PriceFeed address', disableKeepCurrent: true}), + priceFeed: await addressPrompt({message: 'PriceFeed address', required: true}), ...(await fetchCollateralUpdate(pool, true)), ...(await fetchBorrowUpdate(true)), ...(await fetchCapsUpdate(true)), @@ -78,9 +80,9 @@ async function fetchListing(pool: PoolIdentifier): Promise { async function fetchCustomImpl(): Promise { return { - aToken: await addressInput({message: 'aToken implementation', disableKeepCurrent: true}), - vToken: await addressInput({message: 'vToken implementation', disableKeepCurrent: true}), - sToken: await addressInput({message: 'sToken implementation', disableKeepCurrent: true}), + aToken: await addressPrompt({message: 'aToken implementation', required: true}), + vToken: await addressPrompt({message: 'vToken implementation', required: true}), + sToken: await addressPrompt({message: 'sToken implementation', required: true}), }; } @@ -102,7 +104,7 @@ export const assetListing: FeatureModule = { code: { constants: cfg .map((cfg) => [ - `address public constant ${cfg.assetSymbol} = ${cfg.asset};`, + `address public constant ${cfg.assetSymbol} = ${translateJsAddressToSol(cfg.asset)};`, `uint256 internal constant ${cfg.assetSymbol}_SEED_AMOUNT = 10 ** ${cfg.decimals};`, ]) .flat(), @@ -122,7 +124,7 @@ export const assetListing: FeatureModule = { (cfg, ix) => `listings[${ix}] = IAaveV3ConfigEngine.Listing({ asset: ${cfg.assetSymbol}, assetSymbol: "${cfg.assetSymbol}", - priceFeed: ${cfg.priceFeed}, + priceFeed: ${translateJsAddressToSol(cfg.priceFeed)}, eModeCategory: ${cfg.eModeCategory}, enabledToBorrow: ${cfg.enabledToBorrow}, stableRateModeEnabled: ${cfg.stableRateModeEnabled}, @@ -146,7 +148,9 @@ export const assetListing: FeatureModule = { stableRateSlope2: ${cfg.rateStrategyParams.stableRateSlope2}, baseStableRateOffset: ${cfg.rateStrategyParams.baseStableRateOffset}, stableRateExcessOffset: ${cfg.rateStrategyParams.stableRateExcessOffset}, - optimalStableToTotalDebtRatio: ${cfg.rateStrategyParams.optimalStableToTotalDebtRatio} + optimalStableToTotalDebtRatio: ${ + cfg.rateStrategyParams.optimalStableToTotalDebtRatio + } }) });` ) @@ -186,7 +190,10 @@ export const assetListingCustom: FeatureModule = { const response: CodeArtifact = { code: { constants: cfg.map( - (cfg) => `address public constant ${cfg.base.assetSymbol} = ${cfg.base.asset};` + (cfg) => + `address public constant ${cfg.base.assetSymbol} = ${translateJsAddressToSol( + cfg.base.asset + )};` ), execute: cfg.map( (cfg) => @@ -205,7 +212,7 @@ export const assetListingCustom: FeatureModule = { IAaveV3ConfigEngine.Listing({ asset: ${cfg.base.assetSymbol}, assetSymbol: "${cfg.base.assetSymbol}", - priceFeed: ${cfg.base.priceFeed}, + priceFeed: ${translateJsAddressToSol(cfg.base.priceFeed)}, eModeCategory: ${cfg.base.eModeCategory}, enabledToBorrow: ${cfg.base.enabledToBorrow}, stableRateModeEnabled: ${cfg.base.stableRateModeEnabled}, @@ -229,13 +236,15 @@ export const assetListingCustom: FeatureModule = { stableRateSlope2: ${cfg.base.rateStrategyParams.stableRateSlope2}, baseStableRateOffset: ${cfg.base.rateStrategyParams.baseStableRateOffset}, stableRateExcessOffset: ${cfg.base.rateStrategyParams.stableRateExcessOffset}, - optimalStableToTotalDebtRatio: ${cfg.base.rateStrategyParams.optimalStableToTotalDebtRatio} + optimalStableToTotalDebtRatio: ${ + cfg.base.rateStrategyParams.optimalStableToTotalDebtRatio + } }) }), IAaveV3ConfigEngine.TokenImplementations({ - aToken: ${cfg.implementations.aToken}, - vToken: ${cfg.implementations.vToken}, - sToken: ${cfg.implementations.sToken} + aToken: ${translateJsAddressToSol(cfg.implementations.aToken)}, + vToken: ${translateJsAddressToSol(cfg.implementations.vToken)}, + sToken: ${translateJsAddressToSol(cfg.implementations.sToken)} }) );` ) diff --git a/generator/features/borrowsUpdates.ts b/generator/features/borrowsUpdates.ts index 029751679..cc7af61c8 100644 --- a/generator/features/borrowsUpdates.ts +++ b/generator/features/borrowsUpdates.ts @@ -1,6 +1,10 @@ import {CodeArtifact, ENGINE_FLAGS, FEATURE, FeatureModule} from '../types'; -import {assetsSelect, booleanSelect, percentInput} from '../prompts'; +import {booleanSelect, percentInput} from '../prompts'; import {BorrowUpdate} from './types'; +import { + assetsSelectPrompt, + translateAssetToAssetLibUnderlying, +} from '../prompts/assetsSelectPrompt'; export async function fetchBorrowUpdate(disableKeepCurrent?: T) { return { @@ -41,7 +45,7 @@ export const borrowsUpdates: FeatureModule = { description: 'BorrowsUpdates (enabledToBorrow, flashloanable, stableRateModeEnabled, borrowableInIsolation, withSiloedBorrowing, reserveFactor)', async cli(opt, pool) { - const assets = await assetsSelect({ + const assets = await assetsSelectPrompt({ message: 'Select the assets you want to amend', pool, }); @@ -64,7 +68,7 @@ export const borrowsUpdates: FeatureModule = { ${cfg .map( (cfg, ix) => `borrowUpdates[${ix}] = IAaveV3ConfigEngine.BorrowUpdate({ - asset: ${cfg.asset}, + asset: ${translateAssetToAssetLibUnderlying(cfg.asset, pool)}, enabledToBorrow: ${cfg.enabledToBorrow}, flashloanable: ${cfg.flashloanable}, stableRateModeEnabled: ${cfg.stableRateModeEnabled}, diff --git a/generator/features/capsUpdates.ts b/generator/features/capsUpdates.ts index f172f07f6..1b64a0c24 100644 --- a/generator/features/capsUpdates.ts +++ b/generator/features/capsUpdates.ts @@ -1,6 +1,10 @@ -import {CodeArtifact, FEATURE, FeatureModule, PoolIdentifier} from '../types'; -import {assetsSelect, numberInput} from '../prompts'; +import {CodeArtifact, FEATURE, FeatureModule} from '../types'; +import {numberInput} from '../prompts'; import {CapsUpdate, CapsUpdatePartial} from './types'; +import { + assetsSelectPrompt, + translateAssetToAssetLibUnderlying, +} from '../prompts/assetsSelectPrompt'; export async function fetchCapsUpdate(disableKeepCurrent?: boolean): Promise { return { @@ -22,7 +26,7 @@ export const capsUpdates: FeatureModule = { description: 'CapsUpdates (supplyCap, borrowCap)', async cli(opt, pool) { console.log(`Fetching information for CapsUpdates on ${pool}`); - const assets = await assetsSelect({ + const assets = await assetsSelectPrompt({ message: 'Select the assets you want to amend', pool, }); @@ -46,7 +50,7 @@ export const capsUpdates: FeatureModule = { ${cfg .map( (cfg, ix) => `capsUpdate[${ix}] = IAaveV3ConfigEngine.CapsUpdate({ - asset: ${cfg.asset}, + asset: ${translateAssetToAssetLibUnderlying(cfg.asset, pool)}, supplyCap: ${cfg.supplyCap}, borrowCap: ${cfg.borrowCap} });` diff --git a/generator/features/collateralsUpdates.ts b/generator/features/collateralsUpdates.ts index 6989d1343..f967c12a0 100644 --- a/generator/features/collateralsUpdates.ts +++ b/generator/features/collateralsUpdates.ts @@ -1,6 +1,10 @@ -import {CodeArtifact, ENGINE_FLAGS, FEATURE, FeatureModule, PoolIdentifier} from '../types'; -import {assetsSelect, eModeSelect, numberInput, percentInput} from '../prompts'; +import {CodeArtifact, FEATURE, FeatureModule, PoolIdentifier} from '../types'; +import {numberInput, percentInput} from '../prompts'; import {CollateralUpdate, CollateralUpdatePartial} from './types'; +import { + assetsSelectPrompt, + translateAssetToAssetLibUnderlying, +} from '../prompts/assetsSelectPrompt'; export async function fetchCollateralUpdate( pool: PoolIdentifier, @@ -39,7 +43,7 @@ export const collateralsUpdates: FeatureModule = { console.log(`Fetching information for Collateral Updates on ${pool}`); const response: CollateralUpdates = []; - const assets = await assetsSelect({ + const assets = await assetsSelectPrompt({ message: 'Select the assets you want to amend', pool, }); @@ -62,7 +66,7 @@ export const collateralsUpdates: FeatureModule = { ${cfg .map( (cfg, ix) => `collateralUpdate[${ix}] = IAaveV3ConfigEngine.CollateralUpdate({ - asset: ${cfg.asset}, + asset: ${translateAssetToAssetLibUnderlying(cfg.asset, pool)}, ltv: ${cfg.ltv}, liqThreshold: ${cfg.liqThreshold}, liqBonus: ${cfg.liqBonus}, diff --git a/generator/features/eModesAssets.ts b/generator/features/eModesAssets.ts index 6ec33d3d3..74be7fd90 100644 --- a/generator/features/eModesAssets.ts +++ b/generator/features/eModesAssets.ts @@ -1,10 +1,14 @@ import {CodeArtifact, FEATURE, FeatureModule, PoolIdentifier} from '../types'; -import {assetsSelect, eModeSelect} from '../prompts'; +import {eModeSelect} from '../prompts'; import {AssetEModeUpdate} from './types'; +import { + assetsSelectPrompt, + translateAssetToAssetLibUnderlying, +} from '../prompts/assetsSelectPrompt'; async function subCli(pool: PoolIdentifier) { console.log(`Fetching information for Emode assets on ${pool}`); - const assets = await assetsSelect({ + const assets = await assetsSelectPrompt({ message: 'Select the assets you want to amend eMode for', pool, }); @@ -44,7 +48,7 @@ export const eModeAssets: FeatureModule = { ${cfg .map( (cfg, ix) => `assetEModeUpdates[${ix}] = IAaveV3ConfigEngine.AssetEModeUpdate({ - asset: ${cfg.asset}, + asset: ${translateAssetToAssetLibUnderlying(cfg.asset, pool)}, eModeCategory: ${cfg.eModeCategory} });` ) diff --git a/generator/features/eModesUpdates.ts b/generator/features/eModesUpdates.ts index af37cc582..51e852a7d 100644 --- a/generator/features/eModesUpdates.ts +++ b/generator/features/eModesUpdates.ts @@ -1,15 +1,18 @@ import {CodeArtifact, FEATURE, FeatureModule, PoolIdentifier} from '../types'; -import {addressInput, eModesSelect, percentInput, stringInput} from '../prompts'; +import {eModesSelect, percentInput} from '../prompts'; import {EModeCategoryUpdate} from './types'; import {confirm} from '@inquirer/prompts'; +import {addressPrompt, translateJsAddressToSol} from '../prompts/addressPrompt'; +import {stringOrKeepCurrent, stringPrompt} from '../prompts/stringPrompt'; +import {zeroAddress} from 'viem'; +import {getEModes} from '../common'; async function fetchEmodeCategoryUpdate( - disableKeepCurrent?: T, - eModeCategory?: string + eModeCategory: string | number, + disableKeepCurrent?: T ): Promise { return { - eModeCategory: - eModeCategory ?? (await stringInput({message: 'eModeCategory', disableKeepCurrent})), + eModeCategory, ltv: await percentInput({ message: 'ltv', disableKeepCurrent, @@ -22,13 +25,14 @@ async function fetchEmodeCategoryUpdate( message: 'liqBonus', disableKeepCurrent, }), - priceSource: await addressInput({ + priceSource: await addressPrompt({ message: 'Price Source', - disableKeepCurrent, + required: disableKeepCurrent, + defaultValue: disableKeepCurrent ? zeroAddress : '', }), - label: await stringInput({ + label: await stringPrompt({ message: 'label', - disableKeepCurrent, + required: disableKeepCurrent, }), }; } @@ -42,8 +46,11 @@ async function subCli(pool: PoolIdentifier) { }); if (shouldAddNewCategory) { let more: boolean = true; + const eModes = getEModes(pool as any); + let highestEmode = Object.values(eModes).length > 0 ? Math.max(...Object.values(eModes)) : 0; + while (more) { - answers.push(await fetchEmodeCategoryUpdate(true)); + answers.push(await fetchEmodeCategoryUpdate(++highestEmode, true)); more = await confirm({message: 'Do you want to add another emode category?', default: false}); } } @@ -61,7 +68,7 @@ async function subCli(pool: PoolIdentifier) { if (eModeCategories) { for (const eModeCategory of eModeCategories) { console.log(`collecting info for ${eModeCategory}`); - answers.push(await fetchEmodeCategoryUpdate(false, eModeCategory)); + answers.push(await fetchEmodeCategoryUpdate(eModeCategory)); } } } @@ -94,10 +101,8 @@ export const eModeUpdates: FeatureModule = { ltv: ${cfg.ltv}, liqThreshold: ${cfg.liqThreshold}, liqBonus: ${cfg.liqBonus}, - priceSource: ${cfg.priceSource}, - label: ${ - cfg.label == 'EngineFlags.KEEP_CURRENT_STRING' ? cfg.label : `"${cfg.label}"` - } + priceSource: ${translateJsAddressToSol(cfg.priceSource)}, + label: ${stringOrKeepCurrent(cfg.label)} });` ) .join('\n')} diff --git a/generator/features/flashBorrower.ts b/generator/features/flashBorrower.ts index cbf837c32..75818a13f 100644 --- a/generator/features/flashBorrower.ts +++ b/generator/features/flashBorrower.ts @@ -1,7 +1,7 @@ import {CodeArtifact, FEATURE, FeatureModule} from '../types'; -import {addressInput} from '../prompts'; import {Hex} from 'viem'; import {TEST_EXECUTE_PROPOSAL} from '../utils/constants'; +import {addressPrompt, translateJsAddressToSol} from '../prompts/addressPrompt'; type FlashBorrower = { address: Hex; @@ -13,9 +13,9 @@ export const flashBorrower: FeatureModule = { async cli(opt, pool) { console.log(`Fetching information for FlashBorrower on ${pool}`); const response: FlashBorrower = { - address: await addressInput({ + address: await addressPrompt({ message: 'Who do you want to grant the flashBorrower role', - disableKeepCurrent: true, + required: true, }), }; return response; @@ -23,7 +23,9 @@ export const flashBorrower: FeatureModule = { build(opt, pool, cfg) { const response: CodeArtifact = { code: { - constants: [`address public constant NEW_FLASH_BORROWER = address(${cfg.address});`], + constants: [ + `address public constant NEW_FLASH_BORROWER = ${translateJsAddressToSol(cfg.address)};`, + ], execute: [`${pool}.ACL_MANAGER.addFlashBorrower(NEW_FLASH_BORROWER);`], }, test: { diff --git a/generator/features/mocks/configs.ts b/generator/features/mocks/configs.ts index eb0f1ccce..007a1658b 100644 --- a/generator/features/mocks/configs.ts +++ b/generator/features/mocks/configs.ts @@ -1,5 +1,5 @@ import {Options} from '../../types'; -import {CapsUpdate, Listing} from '../types'; +import {EModeCategoryUpdate, Listing, PriceFeedUpdate} from '../types'; export const MOCK_OPTIONS: Options = { pools: ['AaveV3Ethereum'], @@ -21,26 +21,52 @@ export const assetListingConfig: Listing[] = [ liqBonus: '5_00', debtCeiling: '100_000', liqProtocolFee: '20_00', - enabledToBorrow: 'ENABLED', - flashloanable: 'ENABLED', - stableRateModeEnabled: 'DISABLED', - borrowableInIsolation: 'DISABLED', - withSiloedBorrowing: 'DISABLED', + enabledToBorrow: 'EngineFlags.ENABLED', + flashloanable: 'EngineFlags.ENABLED', + stableRateModeEnabled: 'EngineFlags.DISABLED', + borrowableInIsolation: 'EngineFlags.DISABLED', + withSiloedBorrowing: 'EngineFlags.DISABLED', reserveFactor: '20_00', supplyCap: '10_000', borrowCap: '5_000', rateStrategyParams: { optimalUtilizationRate: '_bpsToRay(80_00)', - baseVariableBorrowRate: '_bpsToRay(0_00)', + baseVariableBorrowRate: '_bpsToRay(0)', variableRateSlope1: '_bpsToRay(10_00)', variableRateSlope2: '_bpsToRay(100_00)', stableRateSlope1: '_bpsToRay(10_00)', stableRateSlope2: '_bpsToRay(100_00)', baseStableRateOffset: '_bpsToRay(1_00)', - stableRateExcessOffset: '_bpsToRay()', + stableRateExcessOffset: '_bpsToRay(0)', optimalStableToTotalDebtRatio: '_bpsToRay(10_00)', }, eModeCategory: 'AaveV3EthereumEModes.NONE', asset: '0xcAfE001067cDEF266AfB7Eb5A286dCFD277f3dE5', }, ]; + +export const priceFeedsUpdateConfig: PriceFeedUpdate[] = [ + { + asset: 'DAI', + priceFeed: '0xae7ab96520de3a18e5e111b5eaab095312d7fe84', + }, +]; + +export const emodeUpdates: EModeCategoryUpdate[] = [ + { + eModeCategory: 2, + ltv: '20_00', + liqThreshold: '30_00', + liqBonus: '5_00', + priceSource: '0x0000000000000000000000000000000000000000', + label: 'label', + }, + { + eModeCategory: 'AaveV3EthereumEModes.ETH_CORRELATED', + ltv: 'EngineFlags.KEEP_CURRENT', + liqThreshold: '50_00', + liqBonus: 'EngineFlags.KEEP_CURRENT', + priceSource: '', + label: '', + }, +]; diff --git a/generator/features/priceFeedsUpdate.spec.ts b/generator/features/priceFeedsUpdate.spec.ts new file mode 100644 index 000000000..e18604920 --- /dev/null +++ b/generator/features/priceFeedsUpdate.spec.ts @@ -0,0 +1,28 @@ +// sum.test.js +import {expect, describe, it} from 'vitest'; +import {MOCK_OPTIONS, priceFeedsUpdateConfig} from './mocks/configs'; +import {generateFiles} from '../generator'; +import {FEATURE, PoolConfigs} from '../types'; +import {priceFeedsUpdates} from './priceFeedsUpdates'; + +describe('feature: priceFeedsUpdates', () => { + it('should return reasonable code', () => { + const output = priceFeedsUpdates.build(MOCK_OPTIONS, 'AaveV3Ethereum', priceFeedsUpdateConfig); + expect(output).toMatchSnapshot(); + }); + + it('should properly generate files', async () => { + const poolConfigs: PoolConfigs = { + [MOCK_OPTIONS.pools[0]]: { + pool: MOCK_OPTIONS.pools[0], + features: [FEATURE.PRICE_FEEDS_UPDATE], + artifacts: [ + priceFeedsUpdates.build(MOCK_OPTIONS, 'AaveV3Ethereum', priceFeedsUpdateConfig), + ], + configs: {[FEATURE.PRICE_FEEDS_UPDATE]: priceFeedsUpdateConfig}, + }, + }; + const files = await generateFiles(MOCK_OPTIONS, poolConfigs); + expect(files).toMatchSnapshot(); + }); +}); diff --git a/generator/features/priceFeedsUpdates.ts b/generator/features/priceFeedsUpdates.ts index 397c88d5d..2e9e6ec72 100644 --- a/generator/features/priceFeedsUpdates.ts +++ b/generator/features/priceFeedsUpdates.ts @@ -1,12 +1,16 @@ -import {CodeArtifact, FEATURE, FeatureModule, PoolIdentifier} from '../types'; -import {addressInput, assetsSelect} from '../prompts'; +import {CodeArtifact, FEATURE, FeatureModule} from '../types'; import {PriceFeedUpdate, PriceFeedUpdatePartial} from './types'; +import {addressPrompt, translateJsAddressToSol} from '../prompts/addressPrompt'; +import { + assetsSelectPrompt, + translateAssetToAssetLibUnderlying, +} from '../prompts/assetsSelectPrompt'; async function fetchPriceFeedUpdate(): Promise { return { - priceFeed: await addressInput({ + priceFeed: await addressPrompt({ message: 'New price feed address', - disableKeepCurrent: true, + required: true, }), }; } @@ -16,7 +20,7 @@ export const priceFeedsUpdates: FeatureModule = { description: 'PriceFeedsUpdates (replacing priceFeeds)', async cli(opt, pool) { const response: PriceFeedUpdate[] = []; - const assets = await assetsSelect({ + const assets = await assetsSelectPrompt({ message: 'Select the assets you want to amend', pool, }); @@ -38,8 +42,8 @@ export const priceFeedsUpdates: FeatureModule = { ${cfg .map( (cfg, ix) => `priceFeedUpdates[${ix}] = IAaveV3ConfigEngine.PriceFeedUpdate({ - asset: ${cfg.asset}, - priceFeed: ${cfg.priceFeed} + asset: ${translateAssetToAssetLibUnderlying(cfg.asset, pool)}, + priceFeed: ${translateJsAddressToSol(cfg.priceFeed)} });` ) .join('\n')} diff --git a/generator/features/rateUpdates.ts b/generator/features/rateUpdates.ts index a46717a3f..00e858fe0 100644 --- a/generator/features/rateUpdates.ts +++ b/generator/features/rateUpdates.ts @@ -1,6 +1,10 @@ -import {CodeArtifact, FEATURE, FeatureModule, PoolIdentifier} from '../types'; -import {assetsSelect, percentInput} from '../prompts'; +import {CodeArtifact, FEATURE, FeatureModule} from '../types'; +import {percentInput} from '../prompts'; import {RateStrategyParams, RateStrategyUpdate} from './types'; +import { + assetsSelectPrompt, + translateAssetToAssetLibUnderlying, +} from '../prompts/assetsSelectPrompt'; export async function fetchRateStrategyParamsV2( disableKeepCurrent?: boolean @@ -66,7 +70,7 @@ export const rateUpdatesV2: FeatureModule = { description: 'RateStrategiesUpdates', async cli(opt, pool) { console.log(`Fetching information for RatesUpdate on ${pool}`); - const assets = await assetsSelect({ + const assets = await assetsSelectPrompt({ message: 'Select the assets you want to amend', pool, }); @@ -93,7 +97,7 @@ export const rateUpdatesV2: FeatureModule = { ${cfg .map( (cfg, ix) => `rateStrategies[${ix}] = IAaveV2ConfigEngine.RateStrategyUpdate({ - asset: ${cfg.asset}, + asset: ${translateAssetToAssetLibUnderlying(cfg.asset, pool)}, params: IV2RateStrategyFactory.RateStrategyParams({ optimalUtilizationRate: ${cfg.params.optimalUtilizationRate}, baseVariableBorrowRate: ${cfg.params.baseVariableBorrowRate}, @@ -121,7 +125,7 @@ export const rateUpdatesV3: FeatureModule = { description: 'RateStrategiesUpdates', async cli(opt, pool) { console.log(`Fetching information for RatesUpdate on ${pool}`); - const assets = await assetsSelect({ + const assets = await assetsSelectPrompt({ message: 'Select the assets you want to amend', pool, }); @@ -148,7 +152,7 @@ export const rateUpdatesV3: FeatureModule = { ${cfg .map( (cfg, ix) => `rateStrategies[${ix}] = IAaveV3ConfigEngine.RateStrategyUpdate({ - asset: ${cfg.asset}, + asset: ${translateAssetToAssetLibUnderlying(cfg.asset, pool)}, params: IV3RateStrategyFactory.RateStrategyParams({ optimalUsageRatio: ${cfg.params.optimalUtilizationRate}, baseVariableBorrowRate: ${cfg.params.baseVariableBorrowRate}, diff --git a/generator/features/types.ts b/generator/features/types.ts index 94f565e23..caf89be92 100644 --- a/generator/features/types.ts +++ b/generator/features/types.ts @@ -1,10 +1,5 @@ import {Hex} from 'viem'; -import { - AddressInputValues, - BooleanSelectValues, - NumberInputValues, - PercentInputValues, -} from '../prompts'; +import {BooleanSelectValues, NumberInputValues, PercentInputValues} from '../prompts'; export interface AssetSelector { asset: string; @@ -57,11 +52,12 @@ export interface AssetEModeUpdatePartial { export interface AssetEModeUpdate extends AssetEModeUpdatePartial, AssetSelector {} export interface EModeCategoryUpdate { - eModeCategory: string; + // library accessor or new id + eModeCategory: string | number; ltv: NumberInputValues; liqThreshold: NumberInputValues; liqBonus: NumberInputValues; - priceSource: AddressInputValues; + priceSource?: Hex | ''; label: string; } diff --git a/generator/prompts.ts b/generator/prompts.ts index 129bed84c..97e002036 100644 --- a/generator/prompts.ts +++ b/generator/prompts.ts @@ -1,7 +1,6 @@ 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 @@ -14,11 +13,6 @@ function isNumberOrKeepCurrent(value: string) { return 'Must be number or KEEP_CURRENT'; } -function isAddressOrKeepCurrent(value: string) { - if (value == ENGINE_FLAGS.KEEP_CURRENT_ADDRESS || isAddress(value)) return true; - return 'Must be a valid address'; -} - // TRANSFORMS export function transformNumberToPercent(value: string) { if (value && isNumber(value)) { @@ -57,11 +51,6 @@ export function translateJsNumberToSol(value: string) { return String(value).replace(/\B(?=(\d{3})+(?!\d))/g, '_'); } -function translateJsAddressToSol(value: string) { - if (value === ENGINE_FLAGS.KEEP_CURRENT_ADDRESS) return `EngineFlags.KEEP_CURRENT_ADDRESS`; - return getAddress(value); -} - function translateJsBoolToSol(value: string) { switch (value) { case ENGINE_FLAGS.ENABLED: @@ -75,20 +64,11 @@ function translateJsBoolToSol(value: string) { } } -function translateJsStringToSol(value: string) { - if (value === ENGINE_FLAGS.KEEP_CURRENT_STRING) return `EngineFlags.KEEP_CURRENT_STRING`; - return value; -} - function translateEModeToEModeLib(value: string, pool: PoolIdentifier) { if (value === ENGINE_FLAGS.KEEP_CURRENT) return `EngineFlags.KEEP_CURRENT`; return `${pool}EModes.${value}`; } -function translateAssetToAssetLibUnderlying(value: string, pool: PoolIdentifier) { - return `${pool}Assets.${value}_UNDERLYING`; -} - // PROMPTS interface GenericPrompt { message: string; @@ -131,7 +111,7 @@ export type PercentInputValues = typeof ENGINE_FLAGS.KEEP_CURRENT | string; export async function percentInput( {message, disableKeepCurrent, toRay}: PercentInputPrompt, - opts + opts? ): Promise> { const value = await advancedInput( { @@ -149,7 +129,7 @@ export async function percentInput( export type NumberInputValues = typeof ENGINE_FLAGS.KEEP_CURRENT | string; -export async function numberInput({message, disableKeepCurrent}: GenericPrompt, opts) { +export async function numberInput({message, disableKeepCurrent}: GenericPrompt, opts?) { const value = await advancedInput( { message, @@ -164,37 +144,6 @@ export async function numberInput({message, disableKeepCurrent}: GenericPrompt, return translateJsNumberToSol(value); } -export type AddressInputValues = Hex | typeof ENGINE_FLAGS.KEEP_CURRENT_ADDRESS; - -export async function addressInput({ - message, - disableKeepCurrent, -}: GenericPrompt): Promise { - const value = await input({ - message, - validate: disableKeepCurrent ? isAddress : isAddressOrKeepCurrent, - ...(disableKeepCurrent ? {} : {default: ENGINE_FLAGS.KEEP_CURRENT_ADDRESS}), - }); - return translateJsAddressToSol(value) as T extends true ? Hex : AddressInputValues; -} - -interface AssetsSelectPrompt extends Exclude { - pool: PoolIdentifier; -} - -/** - * allows selecting multiple assets - * @param param0 - * @returns - */ -export async function assetsSelect({pool, message}: AssetsSelectPrompt) { - const values = await checkbox({ - message, - choices: getAssets(pool).map((asset) => ({name: asset, value: asset})), - }); - return values.map((v) => translateAssetToAssetLibUnderlying(v, pool)); -} - interface EModeSelectPrompt extends GenericPrompt { pool: PoolIdentifier; } @@ -236,16 +185,3 @@ export async function eModesSelect({message, pool}: EModeSele console.log('No e-mode category active on the current pool'); } } - -export async function stringInput({ - message, - defaultValue, - disableKeepCurrent, -}: GenericPrompt) { - const value = await input({ - message, - default: defaultValue, - ...(disableKeepCurrent ? {} : {default: ENGINE_FLAGS.KEEP_CURRENT_STRING}), - }); - return translateJsStringToSol(value); -} diff --git a/generator/prompts/addressPrompt.spec.ts b/generator/prompts/addressPrompt.spec.ts new file mode 100644 index 000000000..55e1bcf02 --- /dev/null +++ b/generator/prompts/addressPrompt.spec.ts @@ -0,0 +1,54 @@ +import {expect, describe, it} from 'vitest'; +import {render} from '@inquirer/testing'; +import {addressPrompt, translateJsAddressToSol} from './addressPrompt'; + +describe('addresses', () => { + describe('addressPrompt', () => { + it('handles "required"', async () => { + const {answer, events, getScreen} = await render(addressPrompt, { + message: 'Enter address?', + required: true, + }); + + expect(getScreen()).toMatchInlineSnapshot('"? Enter address?*"'); + + events.keypress('enter'); + await Promise.resolve(); + expect(getScreen()).toMatchInlineSnapshot( + '"? Enter address?*\n> You must provide a valid value"' + ); + + events.type('XX0xXXae7ab96520de3a18e5e111b5eaab095312d7fe84'); + expect(getScreen()).toMatchInlineSnapshot( + '"? Enter address?* 0xae7ab96520de3a18e5e111b5eaab095312d7fe84"' + ); + + events.keypress('enter'); + await expect(answer).resolves.toEqual('0xae7ab96520de3a18e5e111b5eaab095312d7fe84'); + }); + + it('handles "optional"', async () => { + const {answer, events, getScreen} = await render(addressPrompt, { + message: 'Enter address?', + }); + + expect(getScreen()).toMatchInlineSnapshot('"? Enter address?"'); + + events.keypress('enter'); + await Promise.resolve(); + await expect(answer).resolves.toEqual(''); + }); + }); + + /** + * Translates, translate the js input value to solidity + */ + describe('translate', () => { + it('translateJsAddressToSol: should properly translate values to addresses', () => { + expect(translateJsAddressToSol('')).toBe('EngineFlags.KEEP_CURRENT_ADDRESS'); + expect(translateJsAddressToSol('0xae7ab96520de3a18e5e111b5eaab095312d7fe84')).toBe( + '0xae7ab96520DE3A18E5e111B5EaAb095312D7fE84' + ); + }); + }); +}); diff --git a/generator/prompts/addressPrompt.ts b/generator/prompts/addressPrompt.ts new file mode 100644 index 000000000..3ec771dc0 --- /dev/null +++ b/generator/prompts/addressPrompt.ts @@ -0,0 +1,24 @@ +import {Hex, getAddress, isAddress} from 'viem'; +import {GenericPrompt} from './types'; +import {advancedInput} from './advancedInput'; +import {flagAsRequired} from '../common'; + +export async function addressPrompt( + {message, required}: GenericPrompt, + opts? +): Promise { + const value = await advancedInput( + { + message: flagAsRequired(message, required), + validate: (v) => (required ? isAddress(v) : isAddress(v) || v === ''), + pattern: /^(0|0x|0x[A-Fa-f0-9]{0,40})?$/, + }, + opts + ); + return value as Hex; +} + +export function translateJsAddressToSol(value?: string) { + if (!value) return `EngineFlags.KEEP_CURRENT_ADDRESS`; + return getAddress(value); +} diff --git a/generator/prompts/advancedInput.ts b/generator/prompts/advancedInput.ts index 70b242508..ed875706b 100644 --- a/generator/prompts/advancedInput.ts +++ b/generator/prompts/advancedInput.ts @@ -10,7 +10,7 @@ import { import type {} from '@inquirer/type'; import chalk from 'chalk'; -type InputConfig = PromptConfig<{ +export type InputConfig = PromptConfig<{ default?: string; transformer?: (value: string, {isFinal}: {isFinal: boolean}) => string; validate?: (value: string) => boolean | string | Promise; @@ -38,33 +38,31 @@ export const advancedInput = createPrompt((config, done) => 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); + if (isEnterKey(key)) { + const answer = value || defaultValue; + setStatus('loading'); + const isValid = await validate(answer); + if (isValid === true) { + setValue(answer); + setStatus('done'); + done(answer); } else { - setValue(rl.line); - setError(undefined); + // 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 if (!pattern || pattern?.test(rl.line)) { + setValue(rl.line); + setError(undefined); } else { const line = rl.line; rl.clearLine(0); diff --git a/generator/prompts/assetsSelectPrompt.ts b/generator/prompts/assetsSelectPrompt.ts new file mode 100644 index 000000000..01a2fcaf1 --- /dev/null +++ b/generator/prompts/assetsSelectPrompt.ts @@ -0,0 +1,21 @@ +import {checkbox} from '@inquirer/prompts'; +import {GenericPoolPrompt} from './types'; +import {getAssets} from '../common'; +import {PoolIdentifier} from '../types'; + +/** + * allows selecting multiple assets + * TODO: enforce selection of at least one asset (next version of inquirer ships with required) + * @param param0 + * @returns + */ +export async function assetsSelectPrompt({pool, message}: GenericPoolPrompt) { + return await checkbox({ + message, + choices: getAssets(pool).map((asset) => ({name: asset, value: asset})), + }); +} + +export function translateAssetToAssetLibUnderlying(value: string, pool: PoolIdentifier) { + return `${pool}Assets.${value}_UNDERLYING`; +} diff --git a/generator/prompts/stringPrompt.ts b/generator/prompts/stringPrompt.ts new file mode 100644 index 000000000..8ccdbd430 --- /dev/null +++ b/generator/prompts/stringPrompt.ts @@ -0,0 +1,22 @@ +import {flagAsRequired} from '../common'; +import {advancedInput} from './advancedInput'; +import {GenericPrompt} from './types'; + +export async function stringPrompt( + {message, defaultValue, required}: GenericPrompt, + opts? +) { + return advancedInput( + { + message: flagAsRequired(message, required), + default: defaultValue, + validate: (v) => (required ? v.trim().length != 0 : true), + }, + opts + ); +} + +export function stringOrKeepCurrent(value: string) { + if (!value) return `EngineFlags.KEEP_CURRENT_STRING`; + return `"${value}"`; +} diff --git a/generator/prompts/types.ts b/generator/prompts/types.ts new file mode 100644 index 000000000..bf96220a4 --- /dev/null +++ b/generator/prompts/types.ts @@ -0,0 +1,12 @@ +import {PoolIdentifier} from '../types'; + +export interface GenericPrompt { + message: string; + required?: T; + transform?: (value: string) => string; + defaultValue?: string; +} + +export interface GenericPoolPrompt extends GenericPrompt { + pool: PoolIdentifier; +} diff --git a/generator/templates/aip.template.ts b/generator/templates/aip.template.ts index 9401c82f8..3c1ac00a6 100644 --- a/generator/templates/aip.template.ts +++ b/generator/templates/aip.template.ts @@ -15,10 +15,7 @@ discussions: ${`"${options.discussion}"` || 'TODO'} ## Specification ${Object.keys(configs).map((pool) => { - let template = `On ${pool} the following steps are performed:\n`; - template += configs[pool].artifacts - .filter((artifact) => artifact.aip) - .map((artifact) => artifact.aip); + return configs[pool].artifacts.filter((artifact) => artifact.aip).map((artifact) => artifact.aip); })} ## References