diff --git a/README.md b/README.md index 4e5636a78..8d516f3da 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,9 @@ + [![All Contributors](https://img.shields.io/badge/all_contributors-88-orange.svg?style=flat-square)](#contributors-) + ## Description @@ -71,7 +73,6 @@ pnpm studio create-group [app-id] ```bash pnpm studio create-token-fetcher [app-id] pnpm studio create-contract-position-fetcher [app-id] -pnpm studio create-balance-fetcher [app-id] ``` ## Clearing the cache diff --git a/cli/commands/create-app.ts b/cli/commands/create-app.ts index fb1bb67ff..594d80889 100644 --- a/cli/commands/create-app.ts +++ b/cli/commands/create-app.ts @@ -1,10 +1,9 @@ import { Command } from '@oclif/core'; -import fse from 'fs-extra'; +import { ensureDir } from 'fs-extra'; import { zipObject } from 'lodash'; import { AppAction } from '../../src/app/app.interface'; import { generateAppDefinition } from '../generators/generate-app-definition'; -import { generateAppIndex } from '../generators/generate-app-index'; import { generateAppModule } from '../generators/generate-app-module'; import { promptAppDescription, @@ -32,22 +31,22 @@ export default class CreateApp extends Command { const networks = await promptAppNetworks(); const tags = await promptAppTags(); - fse.ensureDir(`./src/apps/${appId}`); - fse.ensureDir(`./src/apps/${appId}/assets`); - fse.ensureDir(`./src/apps/${appId}/contracts`); - fse.ensureDir(`./src/apps/${appId}/contracts/abis`); + await ensureDir(`./src/apps/${appId}`); + await ensureDir(`./src/apps/${appId}/assets`); + await ensureDir(`./src/apps/${appId}/contracts`); + await ensureDir(`./src/apps/${appId}/contracts/abis`); for (const network of networks) { - fse.ensureDir(`./src/apps/${appId}/${network}`); + await ensureDir(`./src/apps/${appId}/${network}`); } await generateAppModule(appId); - await generateAppIndex(appId); await generateAppDefinition({ id: appId, name: appName, description: appDescription, url: appUrl, tags, + links: {}, supportedNetworks: zipObject( networks, networks.map(() => [AppAction.VIEW]), diff --git a/cli/commands/create-balance-fetcher.ts b/cli/commands/create-balance-fetcher.ts deleted file mode 100644 index eb98cdcee..000000000 --- a/cli/commands/create-balance-fetcher.ts +++ /dev/null @@ -1,28 +0,0 @@ -/* eslint no-console: 0 */ - -import { Command } from '@oclif/core'; - -import { addBalanceFetcherToAppModule } from '../generators/generate-app-module'; -import { generateBalanceFetcher } from '../generators/generate-balance-fetcher'; -import { loadAppDefinition } from '../generators/utils'; -import { promptAppNetwork } from '../prompts'; - -export default class CreateBalanceFetcher extends Command { - static description = 'Creates a balance fetcher in a given app'; - static examples = [`$ ./studio create-balance-fetcher appId`]; - static args = [{ name: 'appId', description: 'The application id ', required: true }]; - static flags = {}; - - async run(): Promise { - const { args } = await this.parse(CreateBalanceFetcher); - const appId = args.appId; - - const definition = await loadAppDefinition(appId); - const networks = Object.keys(definition.supportedNetworks); - - const network = await promptAppNetwork(networks); - - await generateBalanceFetcher(appId, network); - await addBalanceFetcherToAppModule({ appId, network }); - } -} diff --git a/cli/commands/create-contract-position-fetcher.ts b/cli/commands/create-contract-position-fetcher.ts index 8c526ff9e..61e473323 100644 --- a/cli/commands/create-contract-position-fetcher.ts +++ b/cli/commands/create-contract-position-fetcher.ts @@ -7,13 +7,7 @@ import { addGroupToAppDefinition } from '../generators/generate-app-definition'; import { addContractPositionFetcherToAppModule } from '../generators/generate-app-module'; import { generateContractPositionFetcher } from '../generators/generate-contract-position-fetcher'; import { loadAppDefinition } from '../generators/utils'; -import { - promptAppGroupId, - promptAppNetwork, - promptNewGroupId, - promptNewGroupLabel, - promptNewGroupType, -} from '../prompts'; +import { promptAppGroupId, promptAppNetwork, promptNewGroupId, promptNewGroupLabel } from '../prompts'; export default class CreateContractPositionFetcher extends Command { static description = 'Creates a contract position fetcher in a given app'; @@ -26,15 +20,14 @@ export default class CreateContractPositionFetcher extends Command { const appId = args.appId; const definition = await loadAppDefinition(appId); - const groupIds = Object.values(definition.groups).map(v => v.id); - const networks = Object.keys(definition.supportedNetworks); + const groupIds = Object.values(definition.groups ?? {}).map(v => v.id); + const networks = Object.keys(definition.supportedNetworks ?? {}); let groupId = await promptAppGroupId(groupIds); let group: AppGroup | null = null; if (!groupId) { const newGroupId = await promptNewGroupId(groupIds); const newGroupLabel = await promptNewGroupLabel(); - const newGroupType = await promptNewGroupType(); group = { id: newGroupId, label: newGroupLabel, type: GroupType.POSITION }; groupId = newGroupId; } diff --git a/cli/commands/create-group.ts b/cli/commands/create-group.ts index 6b1221278..2f1e42979 100644 --- a/cli/commands/create-group.ts +++ b/cli/commands/create-group.ts @@ -17,7 +17,7 @@ export default class CreateGroup extends Command { const appId = args.appId; const definition = await loadAppDefinition(appId); - const groupIds = Object.values(definition.groups).map(v => v.id); + const groupIds = Object.values(definition.groups ?? {}).map(v => v.id); const id = await promptNewGroupId(groupIds); const label = await promptNewGroupLabel(); diff --git a/cli/commands/create-token-fetcher.ts b/cli/commands/create-token-fetcher.ts index 5e8d90f13..348f71f9b 100644 --- a/cli/commands/create-token-fetcher.ts +++ b/cli/commands/create-token-fetcher.ts @@ -15,21 +15,13 @@ export default class CreateTokenFetcher extends Command { static args = [{ name: 'appId', description: 'The application id ', required: true }]; static flags = {}; - private async loadDefinition(appId: string) { - const modPath = `../src/apps/${appId}/${appId}.definition`; - const mod = require(modPath); - const key = Object.keys(mod).find(v => /_DEFINITION/.test(v)); - if (!key) throw new Error(`No matched export found in ${modPath}`); - return mod[key]; - } - async run(): Promise { const { args } = await this.parse(CreateTokenFetcher); const appId = args.appId; const definition = await loadAppDefinition(appId); - const groupIds = Object.values(definition.groups).map(v => v.id); - const networks = Object.keys(definition.supportedNetworks); + const groupIds = Object.values(definition.groups ?? {}).map(v => v.id); + const networks = Object.keys(definition.supportedNetworks ?? {}); let groupId = await promptAppGroupId(groupIds); let group: AppGroup | null = null; diff --git a/cli/commands/index.ts b/cli/commands/index.ts index 131251c01..c4143cf36 100644 --- a/cli/commands/index.ts +++ b/cli/commands/index.ts @@ -2,7 +2,6 @@ import { Command } from '@oclif/core'; import ClearCache from './clear-cache'; import CreateApp from './create-app'; -import CreateBalanceFetcher from './create-balance-fetcher'; import CreateContractPositionFetcher from './create-contract-position-fetcher'; import CreateGroup from './create-group'; import CreateTokenFetcher from './create-token-fetcher'; @@ -19,7 +18,6 @@ export const commands: Record = { 'create-app': CreateApp, 'create-group': CreateGroup, 'create-token-fetcher': CreateTokenFetcher, - 'create-balance-fetcher': CreateBalanceFetcher, 'create-contract-position-fetcher': CreateContractPositionFetcher, 'clear-cache': ClearCache, 'set-network-provider': SetNetworkProvider, diff --git a/cli/commands/set-network-provider.ts b/cli/commands/set-network-provider.ts index 99316f785..9bda5c287 100644 --- a/cli/commands/set-network-provider.ts +++ b/cli/commands/set-network-provider.ts @@ -66,16 +66,16 @@ export default class SetNetworkProvider extends Command { } } - private deleteLineEnv(network: Network) { + private async deleteLineEnv(network: Network) { const envVarKey = NetworkProviderService.getEnvVarKey(network); // Create .env file if not exist - this.upsertFile(this.envFileName); + await this.upsertFile(this.envFileName); // Delete the line related to the specified network if exists const envFileLines = fs.readFileSync(this.envFileName).toString().trim().split('\n'); const envFileLine = envFileLines.findIndex(line => line.startsWith(`${envVarKey}=`)); - if (envFileLine >= 0) envFileLines[envFileLine] = null; + if (envFileLine >= 0) envFileLines[envFileLine] = ''; // Rewrite file without the line const newEnvFile = compact(envFileLines).join('\n'); @@ -96,11 +96,11 @@ export default class SetNetworkProvider extends Command { const network = await this.promptNetwork(); const action = await this.promptAction(); - this.deleteLineEnv(network); + await this.deleteLineEnv(network); if (action === 'custom') { const url = await this.promptNetworkProviderUrl(); - this.setCustomProvider(network, url); + await this.setCustomProvider(network, url); } } } diff --git a/cli/generators/generate-app-definition.ts b/cli/generators/generate-app-definition.ts index 16c7a619d..9d5d375ef 100644 --- a/cli/generators/generate-app-definition.ts +++ b/cli/generators/generate-app-definition.ts @@ -11,7 +11,7 @@ import { formatAndWrite } from './utils'; import t = recast.types.namedTypes; -export async function generateAppDefinition(appDefinition: Partial) { +export async function generateAppDefinition(appDefinition: AppDefinitionObject) { const appDefinitionName = `${strings.upperCase(appDefinition.id)}_DEFINITION`; const appClassName = strings.titleCase(appDefinition.id); @@ -39,10 +39,9 @@ export async function generateAppDefinition(appDefinition: Partial `[Network.${networkToKey[nk]}]: [${n.map(v => `AppAction.${actionToKey[v]}`).join(',')}]`) + .map(([nk, n]) => `[Network.${networkToKey[nk]}]: [${n!.map(v => `AppAction.${actionToKey[v]}`).join(',')}]`) .join(',')} }, - primaryColor: '${appDefinition.primaryColor ?? '#fff'}', }); @Register.AppDefinition(${appDefinitionName}.id) @@ -71,11 +70,11 @@ export const addGroupToAppDefinition = async ({ appId, group }: { appId: string; const imports = value.body.filter((v): v is t.ImportDeclaration => v.type === 'ImportDeclaration'); const appInterfaceImport = imports.find(v => v.source.value === '~app/app.interface'); - const appInterfaceImportedNames = appInterfaceImport.specifiers.map(v => v as t.ImportSpecifier); + const appInterfaceImportedNames = appInterfaceImport!.specifiers!.map(v => v as t.ImportSpecifier); const hasGroupTypeImport = appInterfaceImportedNames.find(t => t.imported.name === 'GroupType'); if (!hasGroupTypeImport) { - appInterfaceImport.specifiers.push(b.importSpecifier(b.identifier('GroupType'))); + appInterfaceImport!.specifiers!.push(b.importSpecifier(b.identifier('GroupType'))); } this.traverse(path); @@ -101,7 +100,7 @@ export const addGroupToAppDefinition = async ({ appId, group }: { appId: string; ]), ); - (groups.value as t.ObjectExpression).properties.push(newGroupProperty); + (groups!.value as t.ObjectExpression).properties.push(newGroupProperty); } this.traverse(path); diff --git a/cli/generators/generate-app-index.ts b/cli/generators/generate-app-index.ts deleted file mode 100644 index a3dcd66ad..000000000 --- a/cli/generators/generate-app-index.ts +++ /dev/null @@ -1,21 +0,0 @@ -import dedent from 'dedent'; -import * as recast from 'recast'; - -import { strings } from '../strings'; - -import { formatAndWrite } from './utils'; - -export async function generateAppIndex(appId: string) { - const appDefinitionName = `${strings.upperCase(appId)}_DEFINITION`; - const appClassName = strings.titleCase(appId); - - const content = dedent` - export { ${appDefinitionName}, ${appClassName}AppDefinition } from './${appId}.definition'; - export { ${appClassName}AppModule } from './${appId}.module'; - export { ${appClassName}ContractFactory } from './contracts'; - `; - - const ast = recast.parse(content, { parser: require('recast/parsers/typescript') }); - const prettyContent = recast.prettyPrint(ast).code; - await formatAndWrite(`./src/apps/${appId}/index.ts`, prettyContent); -} diff --git a/cli/generators/generate-balance-fetcher.ts b/cli/generators/generate-balance-fetcher.ts deleted file mode 100644 index 10853510f..000000000 --- a/cli/generators/generate-balance-fetcher.ts +++ /dev/null @@ -1,40 +0,0 @@ -import dedent from 'dedent'; - -import { Network } from '../../src/types/network.interface'; -import { strings } from '../strings'; - -import { formatAndWrite } from './utils'; - -export async function generateBalanceFetcher(appId: string, network: Network) { - const appDefinitionName = `${strings.upperCase(appId)}_DEFINITION`; - const appCamelCase = strings.camelCase(appId); - const appTitleCase = strings.titleCase(appCamelCase); - - const networkKey = Object.keys(Network).find(k => network.includes(Network[k])); - const networkTitleCase = strings.titleCase(network); - - const content = dedent` - import { Inject } from '@nestjs/common'; - - import { IAppToolkit, APP_TOOLKIT } from '~app-toolkit/app-toolkit.interface'; - import { Register } from '~app-toolkit/decorators'; - import { presentBalanceFetcherResponse } from '~app-toolkit/helpers/presentation/balance-fetcher-response.present'; - import { BalanceFetcher } from '~balance/balance-fetcher.interface'; - import { Network } from '~types/network.interface'; - - import { ${appDefinitionName} } from '../${appId}.definition'; - - const network = Network.${networkKey}; - - @Register.BalanceFetcher(${appDefinitionName}.id, network) - export class ${networkTitleCase}${appTitleCase}BalanceFetcher implements BalanceFetcher { - constructor(@Inject(APP_TOOLKIT) private readonly appToolkit: IAppToolkit) {} - - async getBalances(address: string) { - return presentBalanceFetcherResponse([]); - } - } - `; - - await formatAndWrite(`./src/apps/${appId}/${network}/${appId}.balance-fetcher.ts`, content); -} diff --git a/cli/generators/generate-contract-position-fetcher.ts b/cli/generators/generate-contract-position-fetcher.ts index 1976be947..48e0e6169 100644 --- a/cli/generators/generate-contract-position-fetcher.ts +++ b/cli/generators/generate-contract-position-fetcher.ts @@ -6,40 +6,64 @@ import { strings } from '../strings'; import { formatAndWrite } from './utils'; export async function generateContractPositionFetcher(appId: string, groupId: string, network: Network) { - const appDefinitionName = `${strings.upperCase(appId)}_DEFINITION`; const appCamelCase = strings.camelCase(appId); const appTitleCase = strings.titleCase(appCamelCase); const groupKey = strings.camelCase(groupId); const groupTitleCase = strings.titleCase(groupKey); - - const networkKey = Object.keys(Network).filter(k => network.includes(Network[k])); const networkTitleCase = strings.titleCase(network); const content = dedent` import { Inject } from '@nestjs/common'; - - import { IAppToolkit, APP_TOOLKIT } from '~app-toolkit/app-toolkit.interface'; - import { Register } from '~app-toolkit/decorators'; - import { PositionFetcher } from '~position/position-fetcher.interface'; - import { ContractPosition } from '~position/position.interface'; - import { Network } from '~types/network.interface'; + import { BigNumberish, Contract } from 'ethers'; - import { ${appTitleCase}ContractFactory } from '../contracts'; - import { ${appDefinitionName} } from '../${appId}.definition'; + import { APP_TOOLKIT, IAppToolkit } from '~app-toolkit/app-toolkit.interface'; + import { PositionTemplate } from '~app-toolkit/decorators/position-template.decorator'; + import { DefaultDataProps } from '~position/display.interface'; + import { ContractPositionTemplatePositionFetcher } from '~position/template/contract-position.template.position-fetcher'; + import { + GetDefinitionsParams, + DefaultContractPositionDefinition, + GetTokenDefinitionsParams, + UnderlyingTokenDefinition, + GetDisplayPropsParams, + GetTokenBalancesParams, + } from '~position/template/contract-position.template.types'; - const appId = ${appDefinitionName}.id; - const groupId = ${appDefinitionName}.groups.${groupKey}.id; - const network = Network.${networkKey}; + import { ${appTitleCase}ContractFactory } from '../contracts'; - @Register.ContractPositionFetcher({ appId, groupId, network }) - export class ${networkTitleCase}${appTitleCase}${groupTitleCase}ContractPositionFetcher implements PositionFetcher { + @PositionTemplate() + export class ${networkTitleCase}${appTitleCase}${groupTitleCase}ContractPositionFetcher extends ContractPositionTemplatePositionFetcher { + groupLabel: string; + constructor( - @Inject(APP_TOOLKIT) private readonly appToolkit: IAppToolkit, - @Inject(${appTitleCase}ContractFactory) private readonly ${appCamelCase}ContractFactory: ${appTitleCase}ContractFactory, - ) {} - - async getPositions() { - return []; + @Inject(APP_TOOLKIT) protected readonly appToolkit: IAppToolkit, + @Inject(${appTitleCase}ContractFactory) protected readonly ${appCamelCase}ContractFactory: ${appTitleCase}ContractFactory, + ) { + super(appToolkit); + } + + getContract(_address: string): Contract { + throw new Error('Method not implemented.'); + } + + getDefinitions(_params: GetDefinitionsParams): Promise { + throw new Error('Method not implemented.'); + } + + getTokenDefinitions( + _params: GetTokenDefinitionsParams, + ): Promise { + throw new Error('Method not implemented.'); + } + + getLabel( + _params: GetDisplayPropsParams, + ): Promise { + throw new Error('Method not implemented.'); + } + + getTokenBalancesPerPosition(_params: GetTokenBalancesParams): Promise { + throw new Error('Method not implemented.'); } } `; diff --git a/cli/generators/generate-token-fetcher.ts b/cli/generators/generate-token-fetcher.ts index 735d5aeb9..d280098f3 100644 --- a/cli/generators/generate-token-fetcher.ts +++ b/cli/generators/generate-token-fetcher.ts @@ -6,40 +6,59 @@ import { strings } from '../strings'; import { formatAndWrite } from './utils'; export async function generateTokenFetcher(appId: string, groupId: string, network: Network) { - const appDefinitionName = `${strings.upperCase(appId)}_DEFINITION`; const appCamelCase = strings.camelCase(appId); const appTitleCase = strings.titleCase(appCamelCase); const groupKey = strings.camelCase(groupId); const groupTitleCase = strings.titleCase(groupKey); - - const networkKey = Object.keys(Network).filter(k => network.includes(Network[k])); const networkTitleCase = strings.titleCase(network); const content = dedent` import { Inject } from '@nestjs/common'; - import { IAppToolkit, APP_TOOLKIT } from '~app-toolkit/app-toolkit.interface'; - import { Register } from '~app-toolkit/decorators'; - import { PositionFetcher } from '~position/position-fetcher.interface'; - import { AppTokenPosition } from '~position/position.interface'; - import { Network } from '~types/network.interface'; + import { APP_TOOLKIT, IAppToolkit } from '~app-toolkit/app-toolkit.interface'; + import { PositionTemplate } from '~app-toolkit/decorators/position-template.decorator'; + import { Erc20 } from '~contract/contracts'; + import { AppTokenTemplatePositionFetcher } from '~position/template/app-token.template.position-fetcher'; + import { + GetAddressesParams, + DefaultAppTokenDefinition, + GetUnderlyingTokensParams, + UnderlyingTokenDefinition, + GetPricePerShareParams, + DefaultAppTokenDataProps, + } from '~position/template/app-token.template.types'; import { ${appTitleCase}ContractFactory } from '../contracts'; - import { ${appDefinitionName} } from '../${appId}.definition'; - - const appId = ${appDefinitionName}.id; - const groupId = ${appDefinitionName}.groups.${groupKey}.id; - const network = Network.${networkKey}; - @Register.TokenPositionFetcher({ appId, groupId, network }) - export class ${networkTitleCase}${appTitleCase}${groupTitleCase}TokenFetcher implements PositionFetcher { + @PositionTemplate() + export class ${networkTitleCase}${appTitleCase}${groupTitleCase}TokenFetcher extends AppTokenTemplatePositionFetcher { + groupLabel: string; + constructor( @Inject(APP_TOOLKIT) private readonly appToolkit: IAppToolkit, @Inject(${appTitleCase}ContractFactory) private readonly ${appCamelCase}ContractFactory: ${appTitleCase}ContractFactory, - ) {} + ) { + super(appToolkit); + } + + getContract(_address: string): Erc20 { + throw new Error('Method not implemented.'); + } + + async getAddresses(_params: GetAddressesParams): Promise { + throw new Error('Method not implemented.'); + } + + async getUnderlyingTokenDefinitions( + _params: GetUnderlyingTokensParams, + ): Promise { + throw new Error('Method not implemented.'); + } - async getPositions() { - return []; + async getPricePerShare( + _params: GetPricePerShareParams, + ): Promise { + throw new Error('Method not implemented.'); } } `; diff --git a/cli/generators/utils.ts b/cli/generators/utils.ts index b516535c9..dd21f6f21 100644 --- a/cli/generators/utils.ts +++ b/cli/generators/utils.ts @@ -1,8 +1,8 @@ import { dirname } from 'path'; import { ESLint } from 'eslint'; -import fse from 'fs-extra'; -import prettier from 'prettier'; +import fse, { ensureDirSync } from 'fs-extra'; +import { resolveConfig, format } from 'prettier'; import { AppDefinitionObject } from '../../src/app/app.interface'; @@ -15,11 +15,11 @@ export const loadAppDefinition = async (appId: string) => { }; export const formatAndWrite = async (filename: string, content: string) => { - fse.ensureDirSync(dirname(filename)); + ensureDirSync(dirname(filename)); const eslint = new ESLint({ fix: true }); - const config = await prettier.resolveConfig(process.cwd()); - const formatted = prettier.format(content, { ...config, parser: 'typescript' }); + const config = await resolveConfig(process.cwd()); + const formatted = format(content, { ...config, parser: 'typescript' }); fse.writeFileSync(filename, formatted); const results = await eslint.lintFiles(filename); diff --git a/cli/prompts/index.ts b/cli/prompts/index.ts index f8aacb7aa..678d04922 100644 --- a/cli/prompts/index.ts +++ b/cli/prompts/index.ts @@ -1,5 +1,7 @@ import inquirer from 'inquirer'; +import { ArrayOfOneOrMore } from '~types/utils'; + import { AppTag, GroupType } from '../../src/app/app.interface'; import { Network } from '../../src/types/network.interface'; @@ -97,7 +99,7 @@ export const promptAppTags = async () => { return true; }, }) - .then(v => v.tags); + .then(v => v.tags as ArrayOfOneOrMore); }; export const promptAppGroupId = async (groupIds: string[]) => { diff --git a/tsconfig.json b/tsconfig.json index 3b8e6f50a..f4e46eadd 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -25,5 +25,5 @@ }, "typeRoots": ["types", "node_modules/@types"] }, - "include": ["./src/**/*"] + "include": ["./src/**/*", "./cli/**/*"] }