From 4b6b17bdc30f697079a8de00db152766237a45b4 Mon Sep 17 00:00:00 2001 From: Bonjour Internet Date: Wed, 27 Sep 2023 16:52:12 +0200 Subject: [PATCH] Revert "Revert "Feature/token indexer flow"" --- .../form/AddIndexerBlockchainNetworks/cmp.tsx | 126 +++++++++++++ .../AddIndexerBlockchainNetworks/index.ts | 2 + .../AddIndexerBlockchainNetworks/types.ts | 13 ++ .../form/AddIndexerTokenAccounts/cmp.tsx | 153 ++++++++++++++++ .../form/AddIndexerTokenAccounts/index.ts | 2 + .../form/AddIndexerTokenAccounts/types.ts | 16 ++ src/components/form/AddNameAndTags/types.ts | 2 +- src/components/pages/HomePage/cmp.tsx | 35 ++-- .../pages/dashboard/NewIndexerPage/cmp.tsx | 103 +++++++++++ .../pages/dashboard/NewIndexerPage/index.ts | 1 + .../dashboard/manage/ManageFunction/cmp.tsx | 2 +- src/domain/executable.ts | 10 +- src/domain/indexer.ts | 169 +++++++++++++++++ src/domain/program.ts | 34 ++-- src/domain/runtime.ts | 8 + src/domain/volume.ts | 2 + src/helpers/constants.ts | 11 ++ src/helpers/schemas.ts | 167 +++++++++++++---- src/helpers/store.ts | 6 + src/helpers/utils.ts | 12 ++ .../common/useManager/useEntityManager.ts | 3 + .../common/useManager/useIndexerManager.ts | 9 + src/hooks/form/useAddFunctionCode.ts | 19 +- .../form/useAddIndexerBlockchainNetworks.ts | 126 +++++++++++++ src/hooks/form/useAddIndexerTokenAccounts.ts | 170 ++++++++++++++++++ src/hooks/form/useAddNameAndTags.ts | 2 +- src/hooks/form/useSelectFunctionRuntime.ts | 1 + .../pages/dashboard/useNewFunctionPage.ts | 2 +- .../pages/dashboard/useNewIndexerPage.ts | 112 ++++++++++++ src/hooks/pages/useHomePage.ts | 12 ++ src/pages/dashboard/indexer.tsx | 3 + 31 files changed, 1261 insertions(+), 72 deletions(-) create mode 100644 src/components/form/AddIndexerBlockchainNetworks/cmp.tsx create mode 100644 src/components/form/AddIndexerBlockchainNetworks/index.ts create mode 100644 src/components/form/AddIndexerBlockchainNetworks/types.ts create mode 100644 src/components/form/AddIndexerTokenAccounts/cmp.tsx create mode 100644 src/components/form/AddIndexerTokenAccounts/index.ts create mode 100644 src/components/form/AddIndexerTokenAccounts/types.ts create mode 100644 src/components/pages/dashboard/NewIndexerPage/cmp.tsx create mode 100644 src/components/pages/dashboard/NewIndexerPage/index.ts create mode 100644 src/domain/indexer.ts create mode 100644 src/hooks/common/useManager/useIndexerManager.ts create mode 100644 src/hooks/form/useAddIndexerBlockchainNetworks.ts create mode 100644 src/hooks/form/useAddIndexerTokenAccounts.ts create mode 100644 src/hooks/pages/dashboard/useNewIndexerPage.ts create mode 100644 src/pages/dashboard/indexer.tsx diff --git a/src/components/form/AddIndexerBlockchainNetworks/cmp.tsx b/src/components/form/AddIndexerBlockchainNetworks/cmp.tsx new file mode 100644 index 00000000..9d6b8bed --- /dev/null +++ b/src/components/form/AddIndexerBlockchainNetworks/cmp.tsx @@ -0,0 +1,126 @@ +import React from 'react' +import { + Icon, + TextInput, + Button, + Dropdown, + DropdownOption, +} from '@aleph-front/aleph-core' +import { + useAddIndexerBlockchainNetworks, + useIndexerBlockchainNetworkItem, +} from '@/hooks/form/useAddIndexerBlockchainNetworks' +import { + IndexerBlockchainNetworkItemProps, + AddIndexerBlockchainNetworksProps, +} from './types' +import NoisyContainer from '@/components/common/NoisyContainer' + +const IndexerBlockchainNetworkItem = React.memo( + (props: IndexerBlockchainNetworkItemProps) => { + const { idCtrl, blockchainCtrl, rpcUrlCtrl, networks, handleRemove } = + useIndexerBlockchainNetworkItem(props) + + return ( + <> +
+ +
+
+ + {networks.map((value) => ( + + {value} + + ))} + +
+ +
+ +
+ {/*
+ +
*/} +
+ +
+ + ) + }, +) +IndexerBlockchainNetworkItem.displayName = 'IndexerBlockchainNetworkItem' + +export const AddIndexerBlockchainNetworks = React.memo( + (props: AddIndexerBlockchainNetworksProps) => { + const { name, control, fields, handleAdd, handleRemove } = + useAddIndexerBlockchainNetworks(props) + + return ( + <> + {fields.length > 0 && ( + +
+ {fields.map((field, index) => ( + + ))} +
+
+ )} +
+ +
+ + ) + }, +) +AddIndexerBlockchainNetworks.displayName = 'AddIndexerBlockchainNetworks' + +export default AddIndexerBlockchainNetworks diff --git a/src/components/form/AddIndexerBlockchainNetworks/index.ts b/src/components/form/AddIndexerBlockchainNetworks/index.ts new file mode 100644 index 00000000..584e84f6 --- /dev/null +++ b/src/components/form/AddIndexerBlockchainNetworks/index.ts @@ -0,0 +1,2 @@ +export { default } from './cmp' +export type { AddIndexerBlockchainNetworksProps } from './types' diff --git a/src/components/form/AddIndexerBlockchainNetworks/types.ts b/src/components/form/AddIndexerBlockchainNetworks/types.ts new file mode 100644 index 00000000..50efbfdd --- /dev/null +++ b/src/components/form/AddIndexerBlockchainNetworks/types.ts @@ -0,0 +1,13 @@ +import { Control } from 'react-hook-form' + +export type IndexerBlockchainNetworkItemProps = { + name?: string + index: number + control: Control + onRemove: (index?: number) => void +} + +export type AddIndexerBlockchainNetworksProps = { + name: string + control: Control +} diff --git a/src/components/form/AddIndexerTokenAccounts/cmp.tsx b/src/components/form/AddIndexerTokenAccounts/cmp.tsx new file mode 100644 index 00000000..d71cccd7 --- /dev/null +++ b/src/components/form/AddIndexerTokenAccounts/cmp.tsx @@ -0,0 +1,153 @@ +import React from 'react' +import { + Icon, + TextInput, + Button, + Dropdown, + DropdownOption, +} from '@aleph-front/aleph-core' +import { + useAddIndexerTokenAccounts, + useIndexerTokenAccountItem, +} from '@/hooks/form/useAddIndexerTokenAccounts' +import { + IndexerTokenAccountItemProps, + AddIndexerTokenAccountsProps, +} from './types' +import NoisyContainer from '@/components/common/NoisyContainer' + +const IndexerTokenAccountItem = React.memo( + (props: IndexerTokenAccountItemProps) => { + const { + networkCtrl, + contractCtrl, + deployerCtrl, + supplyCtrl, + decimalsCtrl, + decimalsValue, + networks, + supplyPreview, + decimalsHandleChange, + handleRemove, + } = useIndexerTokenAccountItem(props) + + return ( + <> +
+ + {networks.map(({ id }) => ( + + {id} + + ))} + +
+
+ +
+
+ +
+
+ +
+
+ +
+ {supplyPreview && ( +
+ Supply: {supplyPreview} +
+ )} +
+ +
+ + ) + }, +) +IndexerTokenAccountItem.displayName = 'IndexerTokenAccountItem' + +export const AddIndexerTokenAccounts = React.memo( + (props: AddIndexerTokenAccountsProps) => { + const { name, control, fields, networks, handleAdd, handleRemove } = + useAddIndexerTokenAccounts(props) + + return ( + <> + {fields.length > 0 && ( + +
+ {fields.map((field, index) => ( + + ))} +
+
+ )} +
+ +
+ + ) + }, +) +AddIndexerTokenAccounts.displayName = 'AddIndexerTokenAccounts' + +export default AddIndexerTokenAccounts diff --git a/src/components/form/AddIndexerTokenAccounts/index.ts b/src/components/form/AddIndexerTokenAccounts/index.ts new file mode 100644 index 00000000..93954a1e --- /dev/null +++ b/src/components/form/AddIndexerTokenAccounts/index.ts @@ -0,0 +1,2 @@ +export { default } from './cmp' +export type { AddIndexerTokenAccountsProps } from './types' diff --git a/src/components/form/AddIndexerTokenAccounts/types.ts b/src/components/form/AddIndexerTokenAccounts/types.ts new file mode 100644 index 00000000..cee1e172 --- /dev/null +++ b/src/components/form/AddIndexerTokenAccounts/types.ts @@ -0,0 +1,16 @@ +import { IndexerBlockchainNetworkField } from '@/hooks/form/useAddIndexerBlockchainNetworks' +import { Control } from 'react-hook-form' + +export type IndexerTokenAccountItemProps = { + name?: string + index: number + control: Control + networks?: IndexerBlockchainNetworkField[] + onRemove: (index?: number) => void +} + +export type AddIndexerTokenAccountsProps = { + name: string + control: Control + networks: IndexerBlockchainNetworkField[] +} diff --git a/src/components/form/AddNameAndTags/types.ts b/src/components/form/AddNameAndTags/types.ts index 84bdc764..911b1b0d 100644 --- a/src/components/form/AddNameAndTags/types.ts +++ b/src/components/form/AddNameAndTags/types.ts @@ -2,7 +2,7 @@ import { EntityType } from '@/helpers/constants' import { Control } from 'react-hook-form' export type AddNameAndTagsProps = { - entityType: EntityType.Instance | EntityType.Program + entityType: EntityType.Instance | EntityType.Program | EntityType.Indexer name?: string control: Control } diff --git a/src/components/pages/HomePage/cmp.tsx b/src/components/pages/HomePage/cmp.tsx index e2d1b3b6..b64ef9ab 100644 --- a/src/components/pages/HomePage/cmp.tsx +++ b/src/components/pages/HomePage/cmp.tsx @@ -132,18 +132,29 @@ export default function HomePage() { decentralized indexers on{' '} Aleph.im's infrastructure.

- +
+ + +
diff --git a/src/components/pages/dashboard/NewIndexerPage/cmp.tsx b/src/components/pages/dashboard/NewIndexerPage/cmp.tsx new file mode 100644 index 00000000..d75199db --- /dev/null +++ b/src/components/pages/dashboard/NewIndexerPage/cmp.tsx @@ -0,0 +1,103 @@ +import { EntityType } from '@/helpers/constants' +import { useNewIndexerPage } from '@/hooks/pages/dashboard/useNewIndexerPage' +import { Button } from '@aleph-front/aleph-core' +import HoldingRequirements from '@/components/common/HoldingRequirements' +import Container from '@/components/common/CenteredContainer' +import { AddIndexerBlockchainNetworks } from '@/components/form/AddIndexerBlockchainNetworks/cmp' +import CompositeTitle from '@/components/common/CompositeTitle' +import { Form } from '@/components/form/Form/cmp' +import AddIndexerTokenAccounts from '@/components/form/AddIndexerTokenAccounts/cmp' +import AddNameAndTags from '@/components/form/AddNameAndTags' + +export default function NewIndexerPage() { + const { + control, + values, + address, + accountBalance, + isCreateButtonDisabled, + errors, + holdingRequirementsProps, + handleSubmit, + } = useNewIndexerPage() + + return ( +
+
+ + + Define Your Blockchain Network + +

+ Specify the blockchain network where you aim to attach accounts. You + can index unique accounts tailored to each environment. This + flexible approach allows for nuanced tracking across multiple + networks, whether on a main net or testnet. Please provide the + necessary details including the network's ID, RPC URL, and + blockchain type. +

+ +
+
+
+ + + Token accounts + +

+ Define the core parameters associated with the token, like the + contract address, deployer's details, and initial supply. This + foundational data assists in bootstrapping the state of your token, + guaranteeing precise and consistent tracking over time. Ensure you + detail the blockchain type, token's contract address, deployer + address, initial supply, and token decimal places. +

+ +
+
+
+ + + Configure Your Token Account + +

+ Organize and identify your indexers more effectively by assigning a + unique name, obtaining a hash reference, and defining multiple tags. + This helps streamline your development process and makes it easier + to manage your web3 indexers. +

+ +
+
+ + This amount needs to be present in your wallet until the indexer is + removed. Tokens won't be locked nor consumed. The indexer will + be garbage collected once funds are removed from the wallet. + + } + button={ + + } + /> + + ) +} diff --git a/src/components/pages/dashboard/NewIndexerPage/index.ts b/src/components/pages/dashboard/NewIndexerPage/index.ts new file mode 100644 index 00000000..7a9f83f3 --- /dev/null +++ b/src/components/pages/dashboard/NewIndexerPage/index.ts @@ -0,0 +1 @@ +export { default } from './cmp' diff --git a/src/components/pages/dashboard/manage/ManageFunction/cmp.tsx b/src/components/pages/dashboard/manage/ManageFunction/cmp.tsx index 6aaab552..9463f36d 100644 --- a/src/components/pages/dashboard/manage/ManageFunction/cmp.tsx +++ b/src/components/pages/dashboard/manage/ManageFunction/cmp.tsx @@ -98,7 +98,7 @@ export default function ManageFunction() {
- FUNCTION LINE + API ENTRYPOINT
, ): Record { - const metadata: Record = { name } + const out: Record = { name } if (tags && tags.length > 0) { - metadata.tags = tags + out.tags = tags } - return metadata + return { + ...metadata, + ...out, + } } } diff --git a/src/domain/indexer.ts b/src/domain/indexer.ts new file mode 100644 index 00000000..3b7b5697 --- /dev/null +++ b/src/domain/indexer.ts @@ -0,0 +1,169 @@ +import { Account } from 'aleph-sdk-ts/dist/accounts/account' +import { EntityManager } from './types' +import { + AddProgram, + Program, + ProgramCost, + ProgramCostProps, + ProgramManager, +} from './program' +import { indexerSchema } from '@/helpers/schemas' +import { NameAndTagsField } from '@/hooks/form/useAddNameAndTags' +import { FunctionRuntimeId } from './runtime' +import { getDefaultSpecsOptions } from '@/hooks/form/useSelectInstanceSpecs' +import { Encoding } from 'aleph-sdk-ts/dist/messages/program/programModel' +import { FunctionCodeField } from '@/hooks/form/useAddFunctionCode' +import { EnvVarField } from '@/hooks/form/useAddEnvVars' +import { toKebabCase, toSnakeCase } from '@/helpers/utils' +import { VolumeField } from '@/hooks/form/useAddVolume' +import { VolumeType } from './volume' +import { IndexerBlockchainNetworkField } from '@/hooks/form/useAddIndexerBlockchainNetworks' +import { IndexerTokenAccountField } from '@/hooks/form/useAddIndexerTokenAccounts' +import { BlockchainDefaultABIUrl } from '@/helpers/constants' + +export type AddIndexer = NameAndTagsField & { + networks: IndexerBlockchainNetworkField[] + accounts: IndexerTokenAccountField[] +} + +export type Indexer = Program + +export type IndexerCostProps = ProgramCostProps +export type IndexerCost = ProgramCost + +export class IndexerManager implements EntityManager { + static addSchema = indexerSchema + + static getCost = (props: IndexerCostProps): IndexerCost => { + const { specs } = this.getStaticProgramConfig() + + return ProgramManager.getCost({ + ...props, + isPersistent: true, + specs, + }) + } + + static getStaticProgramConfig() { + const isPersistent = true + const metadata = { indexer: true } + const specs = { ...getDefaultSpecsOptions(true)[1] } + const runtime = { id: FunctionRuntimeId.Runtime3 } // @note: Nodejs + nvm + const code: FunctionCodeField = { + type: 'ref', + lang: 'javascript', + encoding: Encoding.squashfs, + entrypoint: 'dist/run.js', + programRef: + 'd4a9f4abb451edb361504cc093e78e2d507fb3bb9244ffd746ababaf4c8537a9', // @note: token program + } + const volumes: VolumeField[] = [ + { + volumeType: VolumeType.Persistent, + name: 'data', + mountPath: '/data', + size: 40960, + }, + ] + + return { + isPersistent, + metadata, + specs, + runtime, + code, + volumes, + } + } + + constructor( + protected account: Account, + protected programManager: ProgramManager, + ) {} + + async getAll(): Promise { + try { + const programs = await this.programManager.getAll() + return programs.filter((p) => !!p.metadata?.indexer) + } catch (err) { + return [] + } + } + + async get(id: string): Promise { + const program = await this.programManager.get(id) + if (!program?.metadata?.indexer) return + return program + } + + async add(newIndexer: AddIndexer): Promise { + const newProgram = await this.parseIndexers(newIndexer) + return await this.programManager.add(newProgram) + } + + async del(indexerOrId: string | Indexer): Promise { + await this.programManager.del(indexerOrId) + } + + protected parseEnvVars(newIndexer: AddIndexer): EnvVarField[] { + const INDEXER_BLOCKCHAINS = newIndexer.networks + .map((network) => { + const { id, blockchain } = network + return blockchain === id ? id : `${blockchain}:${id}` + }) + .join(',') + + const INDEXER_NAMESPACE = toKebabCase(newIndexer.name) + + const NETWORKS_CONFIG = newIndexer.networks.reduce((ac, cu) => { + ac[this.getBlockchainEnvName(cu.id, 'INDEX_LOGS')] = 'true' + ac[this.getBlockchainEnvName(cu.id, 'INDEX_BLOCKS')] = 'false' + ac[this.getBlockchainEnvName(cu.id, 'INDEX_TRANSACTIONS')] = 'false' + ac[this.getBlockchainEnvName(cu.id, 'RPC')] = cu.rpcUrl + ac[this.getBlockchainEnvName(cu.id, 'EXPLORER_URL')] = cu.abiUrl + ? cu.abiUrl + : BlockchainDefaultABIUrl[cu.blockchain] + return ac + }, {} as Record) + + const INDEXER_ACCOUNTS = newIndexer.accounts + .map( + ({ network, contract, deployer, supply, decimals }) => + `${network}:${contract}:${deployer}:${supply}:${decimals}`, + ) + .join(',') + + return Object.entries({ + ...NETWORKS_CONFIG, + INDEXER_ACCOUNTS, + INDEXER_NAMESPACE, + INDEXER_BLOCKCHAINS, + INDEXER_DATA_PATH: '/data', + }).map(([name, value]) => ({ name, value })) + } + + protected async parseIndexers(newIndexer: AddIndexer): Promise { + newIndexer = IndexerManager.addSchema.parse(newIndexer) + + const { name, tags } = newIndexer + const envVars = this.parseEnvVars(newIndexer) + const { code, specs, runtime, metadata, isPersistent, volumes } = + IndexerManager.getStaticProgramConfig() + + return { + code, + name, + tags, + specs, + runtime, + envVars, + volumes, + metadata, + isPersistent, + } + } + + protected getBlockchainEnvName(blockchainId: string, name: string): string { + return toSnakeCase(`${blockchainId}_${name}`).toUpperCase() + } +} diff --git a/src/domain/program.ts b/src/domain/program.ts index 5091b158..34f79cbc 100644 --- a/src/domain/program.ts +++ b/src/domain/program.ts @@ -72,6 +72,7 @@ export type AddProgram = Omit< | 'account' | 'channel' | 'file' + | 'programRef' | 'vcpus' | 'memory' | 'runtime' @@ -108,8 +109,16 @@ export type ProgramCost = ExecutableCost export type ParsedCodeType = { encoding: Encoding entrypoint: string - file: any -} +} & ( + | { + file?: undefined + programRef: string + } + | { + file: Blob | Buffer + programRef?: undefined + } +) export class ProgramManager extends Executable @@ -167,8 +176,6 @@ export class ProgramManager try { const programMessage = await this.parseProgram(newProgram) - console.log('programMessage', programMessage) - const response = await program.publish(programMessage) const [entity] = await this.parseMessages([response]) @@ -211,13 +218,15 @@ export class ProgramManager if (code.type === 'text') { return { entrypoint: 'main:app', - file: new Blob([code.text], { type: 'text/plain' }), + file: new Blob([code.text], { type: 'text/plain' }) as File, encoding: Encoding.plain, } } else if (code.type === 'file') { if (!code.file) throw new Error('Invalid function code file') const fileName = code.file.name + let encoding: Encoding + if (fileName.endsWith('.zip')) { encoding = Encoding.zip } else if (fileName.endsWith('.sqsh')) { @@ -225,11 +234,18 @@ export class ProgramManager } else { throw new Error('Invalid function code file') } + return { entrypoint: code.entrypoint, file: code.file, encoding, } + } else if (code.type === 'ref') { + return { + entrypoint: code.entrypoint, + encoding: code.encoding, + programRef: code.programRef, + } } else throw new Error('Invalid function code type') } @@ -244,24 +260,22 @@ export class ProgramManager const variables = this.parseEnvVars(envVars) const { memory, vcpus } = this.parseSpecs(specs) - const metadata = this.parseMetadata(name, tags) + const metadata = this.parseMetadata(name, tags, newProgram.metadata) const runtime = this.parseRuntime(newProgram.runtime) const volumes = await this.parseVolumes(newProgram.volumes) - const { file, entrypoint, encoding } = await this.parseCode(newProgram.code) + const code = await this.parseCode(newProgram.code) return { account, channel, runtime, isPersistent, - entrypoint, - file, variables, memory, vcpus, volumes, + ...code, metadata, - encoding, } } diff --git a/src/domain/runtime.ts b/src/domain/runtime.ts index 241488d0..7fb42f8b 100644 --- a/src/domain/runtime.ts +++ b/src/domain/runtime.ts @@ -2,6 +2,9 @@ export enum FunctionRuntimeId { Runtime1 = 'bd79839bf96e595a06da5ac0b6ba51dea6f7e2591bb913deccded04d831d29f4', // @note: Added trailing blank spaces for generating different unique ids (it will be safely .trim() before sending the request) until the right hashes are provided Runtime2 = 'bd79839bf96e595a06da5ac0b6ba51dea6f7e2591bb913deccded04d831d29f4 ', + // @note: old nvm runtime + // Runtime3 = 'a14560e617f24338517902599a019890fc265425f3311d29b56c7e7603defc32', + Runtime3 = '3c238dd3ffba73ab9b2cccb90a11e40e78aff396152de922a6d794a0a65a305e', Custom = 'custom', } @@ -22,6 +25,11 @@ export const FunctionRuntimes: Record = { name: 'Official min. runtime for binaries x86_64 (Rust, Go, ...)', dist: 'debian', }, + [FunctionRuntimeId.Runtime3]: { + id: FunctionRuntimeId.Runtime3, + name: 'Official Node.js LTS runtime (with nvm support)', + dist: 'debian', + }, [FunctionRuntimeId.Custom]: { id: FunctionRuntimeId.Custom, name: 'Custom runtime', diff --git a/src/domain/volume.ts b/src/domain/volume.ts index 6e8a1d10..2ed93df3 100644 --- a/src/domain/volume.ts +++ b/src/domain/volume.ts @@ -243,6 +243,8 @@ export class VolumeManager implements EntityManager { account, channel, fileObject, + // fileHash: 'IPFS_HASH', + // storageEngine: ItemType.ipfs, }), ), ) diff --git a/src/helpers/constants.ts b/src/helpers/constants.ts index 5bf407fe..089e02d6 100644 --- a/src/helpers/constants.ts +++ b/src/helpers/constants.ts @@ -43,6 +43,7 @@ export enum EntityType { Instance = 'instance', SSHKey = 'sshKey', Domain = 'domain', + Indexer = 'indexer', } export enum AddDomainTarget { @@ -63,4 +64,14 @@ export const EntityTypeName: Record = { [EntityType.Instance]: 'Instance', [EntityType.SSHKey]: 'SSH Key', [EntityType.Domain]: 'Domain', + [EntityType.Indexer]: 'Indexer', +} + +export enum IndexerBlockchain { + Ethereum = 'ethereum', +} + +export const BlockchainDefaultABIUrl: Record = { + [IndexerBlockchain.Ethereum]: + 'https://api.etherscan.io/api?module=contract&action=getabi&address=$ADDRESS', } diff --git a/src/helpers/schemas.ts b/src/helpers/schemas.ts index 14b4e063..56cbd925 100644 --- a/src/helpers/schemas.ts +++ b/src/helpers/schemas.ts @@ -1,16 +1,22 @@ import { z } from 'zod' -import { AddDomainTarget, EntityType, VolumeType } from './constants' +import { + AddDomainTarget, + EntityType, + IndexerBlockchain, + VolumeType, +} from './constants' import { convertByteUnits } from './utils' +import { Encoding } from 'aleph-sdk-ts/dist/messages/program/programModel' -export const requiredString = z +export const requiredStringSchema = z .string() .trim() .min(1, { message: 'Required field' }) -export const optionalString = z.string().trim().optional() +export const optionalStringSchema = z.string().trim().optional() -export const messageHash = requiredString.regex(/^[0-9a-f]{64}$/, { - message: 'Invalid hash', +export const messageHashSchema = requiredStringSchema.regex(/^[0-9a-f]{64}$/, { + message: 'Invalid hash format', }) // @note: Different options to validate a domain @@ -18,17 +24,31 @@ export const messageHash = requiredString.regex(/^[0-9a-f]{64}$/, { // /^((?!-))(xn--)?[a-z0-9][a-z0-9-_]{0,61}[a-z0-9]{0,1}\.(xn--)?([a-z0-9\-]{1,61}|[a-z0-9-]{1,30}\.[a-z]{2,})$/ // /^((?!-)[A-Za-z0-9-]{1, 63}(?((val) => val instanceof File, 'Required file') .refine( (file) => { @@ -44,7 +64,7 @@ const volumeFile = z message: 'File size should be greater than 0', }) -const codeFile = z +export const codeFileSchema = z .custom((val) => val instanceof File, 'Required file') .refine( (file) => { @@ -61,13 +81,20 @@ const codeFile = z message: 'File size should be greater than 0', }) -const programType = z.enum([EntityType.Instance, EntityType.Program]) +export const programTypeSchema = z.enum([ + EntityType.Instance, + EntityType.Program, +]) + +export const indexerBlockchainSchema = z.enum([IndexerBlockchain.Ethereum]) + +export const metadataSchema = z.record(requiredStringSchema, z.unknown()) // SSH KEYS export const sshKeySchema = z.object({ - key: requiredString, - label: optionalString, + key: requiredStringSchema, + label: optionalStringSchema, }) export const sshKeysSchema = z.array(sshKeySchema) @@ -75,14 +102,14 @@ export const sshKeysSchema = z.array(sshKeySchema) // DOMAINS export const domainSchema = z.object({ - name: domainName, + name: domainNameSchema, target: z.enum([ AddDomainTarget.IPFS, AddDomainTarget.Program, AddDomainTarget.Instance, ]), - programType, - ref: messageHash, + programType: programTypeSchema, + ref: messageHashSchema, }) export const domainsSchema = z.array(domainSchema) @@ -91,7 +118,7 @@ export const domainsSchema = z.array(domainSchema) export const newIsolatedVolumeSchema = z.object({ volumeType: z.literal(VolumeType.New), - file: volumeFile, + file: volumeFileSchema, }) export const newIsolatedVolumesSchema = z.array(newIsolatedVolumeSchema) @@ -99,7 +126,7 @@ export const newIsolatedVolumesSchema = z.array(newIsolatedVolumeSchema) // VOLUMES export const newVolumeSchema = newIsolatedVolumeSchema.extend({ - mountPath: linuxPath, + mountPath: linuxPathSchema, useLatest: z.coerce.boolean(), }) @@ -107,15 +134,15 @@ export const newVolumeSchema = newIsolatedVolumeSchema.extend({ export const existingVolumeSchema = z.object({ volumeType: z.literal(VolumeType.Existing), - refHash: messageHash, - mountPath: linuxPath, + refHash: messageHashSchema, + mountPath: linuxPathSchema, useLatest: z.coerce.boolean(), }) export const persistentVolumeSchema = z.object({ volumeType: z.literal(VolumeType.Persistent), - name: requiredString, - mountPath: linuxPath, + name: requiredStringSchema, + mountPath: linuxPathSchema, size: z.number().gt(0), }) @@ -129,14 +156,14 @@ export const addVolumesSchema = z.array(addVolumeSchema) export const addDomainSchema = domainSchema.extend({ // @note: This is calculated after publishing the instance - ref: optionalString, + ref: optionalStringSchema, }) export const addDomainsSchema = z.array(addDomainSchema) export const addEnvVarSchema = z.object({ - name: requiredString, - value: requiredString, + name: requiredStringSchema, + value: requiredStringSchema, }) export const addEnvVarsSchema = z.array(addEnvVarSchema) @@ -148,17 +175,23 @@ export const defaultCode = z.object({ export const addCodeSchema = z.discriminatedUnion('type', [ defaultCode.extend({ type: z.literal('file'), - file: codeFile, - entrypoint: requiredString, + file: codeFileSchema, + entrypoint: requiredStringSchema, }), defaultCode.extend({ type: z.literal('text'), - text: requiredString, + text: requiredStringSchema, + }), + defaultCode.extend({ + type: z.literal('ref'), + encoding: z.enum([Encoding.squashfs, Encoding.zip, Encoding.plain]), + programRef: messageHashSchema, + entrypoint: requiredStringSchema, }), ]) export const addNameAndTagsSchema = z.object({ - name: requiredString, + name: requiredStringSchema, tags: z.array(z.string().trim()).optional(), }) @@ -178,19 +211,19 @@ export const isPersistentSchema = z.coerce.boolean() export const functionRuntimeSchema = z .object({ - id: z.union([messageHash, z.literal('custom')]), - custom: optionalString, + id: z.union([messageHashSchema, z.literal('custom')]), + custom: optionalStringSchema, }) .superRefine(({ id, custom }, { addIssue }) => { if (id !== 'custom') return true - const result = messageHash.safeParse(custom, { path: ['custom'] }) + const result = messageHashSchema.safeParse(custom, { path: ['custom'] }) if (!result.success) { result.error.issues.forEach((issue) => addIssue(issue)) } }) -export const instanceImageSchema = messageHash +export const instanceImageSchema = messageHashSchema export const addSpecsSchema = z .object({ @@ -224,10 +257,11 @@ export const functionSchema = z code: addCodeSchema, runtime: functionRuntimeSchema, isPersistent: isPersistentSchema, - volumes: addVolumesSchema, specs: addSpecsSchema, - envVars: addEnvVarsSchema, - domains: addDomainsSchema, + volumes: addVolumesSchema.optional(), + envVars: addEnvVarsSchema.optional(), + domains: addDomainsSchema.optional(), + metadata: metadataSchema.optional(), }) .merge(addNameAndTagsSchema) @@ -236,10 +270,65 @@ export const functionSchema = z export const instanceSchema = z .object({ image: instanceImageSchema, - volumes: addVolumesSchema, specs: addSpecsSchema, - envVars: addEnvVarsSchema, sshKeys: addSSHKeysSchema, - domains: addDomainsSchema, + volumes: addVolumesSchema.optional(), + envVars: addEnvVarsSchema.optional(), + domains: addDomainsSchema.optional(), + metadata: metadataSchema.optional(), }) .merge(addNameAndTagsSchema) + +// INDEXER + +export const indexerNetworkIdSchema = requiredStringSchema.regex( + /^[0-9a-z-]+$/, + { + message: 'Network id should be provided in kebab-case-format', + }, +) + +export const abiUrlSchema = urlSchema.includes('$ADDRESS', { + message: + 'The url must contain the token "$ADDRESS" that will be replaced in runtime with token contract addresses', +}) + +export const indexerNetworkSchema = z.object({ + id: indexerNetworkIdSchema, + blockchain: indexerBlockchainSchema, + rpcUrl: urlSchema, + abiUrl: abiUrlSchema.optional(), +}) + +export const indexerNetworksSchema = z.array(indexerNetworkSchema) + +export const IndexerTokenAccountSchema = z.object({ + network: requiredStringSchema, + contract: ethereumAddressSchema, + deployer: ethereumAddressSchema, + supply: tokenSupplySchema, + decimals: z.number().gte(0), +}) + +export const IndexerTokenAccountsSchema = z.array(IndexerTokenAccountSchema) + +export const indexerSchema = z + .object({ + networks: indexerNetworksSchema.min(1), + accounts: IndexerTokenAccountsSchema.min(1), + }) + .merge(addNameAndTagsSchema) + .superRefine(({ networks, accounts }, ctx) => + accounts.every((account, i) => { + const ok = networks.some((network) => network.id === account.network) + if (ok) return true + + ctx.addIssue({ + fatal: true, + code: z.ZodIssueCode.invalid_intersection_types, + message: + 'Invalid network. It should be one of the defined blockchain networks ids', + path: [`accounts.${i}.network`], + }) + }), + ) diff --git a/src/helpers/store.ts b/src/helpers/store.ts index b1d388c7..11eb7a39 100644 --- a/src/helpers/store.ts +++ b/src/helpers/store.ts @@ -6,6 +6,7 @@ import { Program, ProgramManager } from '@/domain/program' import { AccountFilesResponse, FileManager } from '@/domain/file' import { MessageManager } from '@/domain/message' import { Domain, DomainManager } from '@/domain/domain' +import { IndexerManager } from '@/domain/indexer' export enum ActionTypes { connect, @@ -46,6 +47,7 @@ export type State = { volumeManager?: VolumeManager programManager?: ProgramManager instanceManager?: InstanceManager + indexerManager?: IndexerManager } export type Action = { @@ -71,6 +73,7 @@ export const initialState: State = { volumeManager: undefined, programManager: undefined, instanceManager: undefined, + indexerManager: undefined, } function addEntitiesToCollection( @@ -137,6 +140,8 @@ export const reducer = ( fileManager, ) + const indexerManager = new IndexerManager(account, programManager) + return { ...state, account, @@ -148,6 +153,7 @@ export const reducer = ( volumeManager, programManager, instanceManager, + indexerManager, } } diff --git a/src/helpers/utils.ts b/src/helpers/utils.ts index bc8a4e1d..6f755487 100644 --- a/src/helpers/utils.ts +++ b/src/helpers/utils.ts @@ -350,3 +350,15 @@ export class Mutex { export function sleep(ms: number): Promise { return new Promise((resolve) => setTimeout(resolve, ms)) } + +export function toKebabCase(input: string): string { + return input + .trim() + .toLowerCase() + .replace(/[^a-z0-9_\\-]/g, '') + .replace(/_/g, '-') +} + +export function toSnakeCase(input: string): string { + return toKebabCase(input).replace(/-/g, '_') +} diff --git a/src/hooks/common/useManager/useEntityManager.ts b/src/hooks/common/useManager/useEntityManager.ts index 6fe0b3ee..a4691bc8 100644 --- a/src/hooks/common/useManager/useEntityManager.ts +++ b/src/hooks/common/useManager/useEntityManager.ts @@ -13,6 +13,7 @@ export function useEntityManager( volumeManager, programManager, instanceManager, + indexerManager, } = appState const entityMap: Record< @@ -25,6 +26,7 @@ export function useEntityManager( [EntityType.Volume]: volumeManager, [EntityType.Instance]: instanceManager, [EntityType.Program]: programManager, + [EntityType.Indexer]: indexerManager, } }, [ domainManager, @@ -32,6 +34,7 @@ export function useEntityManager( programManager, sshKeyManager, volumeManager, + indexerManager, ]) if (!type) return diff --git a/src/hooks/common/useManager/useIndexerManager.ts b/src/hooks/common/useManager/useIndexerManager.ts new file mode 100644 index 00000000..432b2e0a --- /dev/null +++ b/src/hooks/common/useManager/useIndexerManager.ts @@ -0,0 +1,9 @@ +import { useAppState } from '@/contexts/appState' +import { IndexerManager } from '@/domain/indexer' + +export function useIndexerManager(): IndexerManager | undefined { + const [appState] = useAppState() + const { indexerManager } = appState + + return indexerManager +} diff --git a/src/hooks/form/useAddFunctionCode.ts b/src/hooks/form/useAddFunctionCode.ts index 0ec2fa2f..85e2c120 100644 --- a/src/hooks/form/useAddFunctionCode.ts +++ b/src/hooks/form/useAddFunctionCode.ts @@ -1,3 +1,4 @@ +import { Encoding } from 'aleph-sdk-ts/dist/messages/program/programModel' import { Control, UseControllerReturn, useController } from 'react-hook-form' const defaultText = `from fastapi import FastAPI @@ -20,15 +21,25 @@ export type FunctionCodeField = { } & ( | { type: 'text' - text: string - file?: File entrypoint?: string + text: string + ref?: undefined + file?: undefined + } + | { + type: 'ref' + encoding: Encoding + entrypoint: string + programRef: string + text?: undefined + file?: undefined } | { type: 'file' - file: File - text?: string entrypoint: string + file: File + text?: undefined + ref?: undefined } ) diff --git a/src/hooks/form/useAddIndexerBlockchainNetworks.ts b/src/hooks/form/useAddIndexerBlockchainNetworks.ts new file mode 100644 index 00000000..60014e1b --- /dev/null +++ b/src/hooks/form/useAddIndexerBlockchainNetworks.ts @@ -0,0 +1,126 @@ +import { IndexerBlockchain } from '@/helpers/constants' +import { useCallback } from 'react' +import { + Control, + FieldArrayWithId, + UseControllerReturn, + useController, + useFieldArray, +} from 'react-hook-form' + +export type IndexerBlockchainNetworkField = { + id: string + rpcUrl: string + abiUrl?: string + blockchain: IndexerBlockchain +} + +export const defaultValues: Partial = { + id: '', + blockchain: IndexerBlockchain.Ethereum, + rpcUrl: '', + // abiUrl: '', +} + +export type UseIndexerBlockchainNetworkItemProps = { + name?: string + index: number + control: Control + defaultValue?: IndexerBlockchainNetworkField + onRemove: (index?: number) => void +} + +export type UseIndexerBlockchainNetworkItemReturn = { + idCtrl: UseControllerReturn + blockchainCtrl: UseControllerReturn + rpcUrlCtrl: UseControllerReturn + // abiUrlCtrl: UseControllerReturn + networks: IndexerBlockchain[] + handleRemove: () => void +} + +export function useIndexerBlockchainNetworkItem({ + name = 'networks', + index, + control, + defaultValue, + onRemove, +}: UseIndexerBlockchainNetworkItemProps): UseIndexerBlockchainNetworkItemReturn { + const idCtrl = useController({ + control, + name: `${name}.${index}.id`, + defaultValue: defaultValue?.id, + }) + + const blockchainCtrl = useController({ + control, + name: `${name}.${index}.blockchain`, + defaultValue: defaultValue?.blockchain, + }) + + const rpcUrlCtrl = useController({ + control, + name: `${name}.${index}.rpcUrl`, + defaultValue: defaultValue?.rpcUrl, + }) + + // const abiUrlCtrl = useController({ + // control, + // name: `${name}.${index}.abiUrl`, + // defaultValue: defaultValue?.abiUrl, + // }) + + const networks = Object.values(IndexerBlockchain) + + const handleRemove = useCallback(() => { + onRemove(index) + }, [index, onRemove]) + + return { + idCtrl, + blockchainCtrl, + rpcUrlCtrl, + // abiUrlCtrl, + networks, + handleRemove, + } +} + +// -------------------- + +export type UseBlockchainNetworksProps = { + name?: string + control: Control +} + +export type UseBlockchainNetworksReturn = { + name: string + control: Control + fields: FieldArrayWithId[] + handleAdd: () => void + handleRemove: (index?: number) => void +} + +export function useAddIndexerBlockchainNetworks({ + name = 'networks', + control, +}: UseBlockchainNetworksProps): UseBlockchainNetworksReturn { + const networksCtrl = useFieldArray({ + control, + name, + }) + + const { fields, remove: handleRemove, append } = networksCtrl + + const handleAdd = useCallback(() => { + append({ ...defaultValues }) + }, [append]) + + return { + name, + control, + fields, + handleAdd, + handleRemove, + } +} diff --git a/src/hooks/form/useAddIndexerTokenAccounts.ts b/src/hooks/form/useAddIndexerTokenAccounts.ts new file mode 100644 index 00000000..9634635a --- /dev/null +++ b/src/hooks/form/useAddIndexerTokenAccounts.ts @@ -0,0 +1,170 @@ +import { ChangeEvent, useCallback, useMemo } from 'react' +import { + Control, + FieldArrayWithId, + UseControllerReturn, + useController, + useFieldArray, +} from 'react-hook-form' +import { IndexerBlockchainNetworkField } from './useAddIndexerBlockchainNetworks' + +export type IndexerTokenAccountField = { + network: string + contract: string + deployer: string + supply: string + decimals: number +} + +export const defaultValues: Partial = { + network: '', + contract: '', + deployer: '', + supply: '', +} + +export type UseIndexerTokenAccountItemProps = { + name?: string + index: number + control: Control + defaultValue?: IndexerTokenAccountField + networks?: IndexerBlockchainNetworkField[] + onRemove: (index?: number) => void +} + +export type UseIndexerTokenAccountItemReturn = { + networkCtrl: UseControllerReturn + contractCtrl: UseControllerReturn + deployerCtrl: UseControllerReturn + supplyCtrl: UseControllerReturn + decimalsCtrl: UseControllerReturn + decimalsValue: number | undefined + networks: IndexerBlockchainNetworkField[] + supplyPreview: string + decimalsHandleChange: (e: ChangeEvent) => void + handleRemove: () => void +} + +export function useIndexerTokenAccountItem({ + name = 'tokenAccounts', + index, + control, + defaultValue, + networks = [], + onRemove, +}: UseIndexerTokenAccountItemProps): UseIndexerTokenAccountItemReturn { + const networkCtrl = useController({ + control, + name: `${name}.${index}.network`, + defaultValue: defaultValue?.network, + }) + + const contractCtrl = useController({ + control, + name: `${name}.${index}.contract`, + defaultValue: defaultValue?.contract, + }) + + const deployerCtrl = useController({ + control, + name: `${name}.${index}.deployer`, + defaultValue: defaultValue?.deployer, + }) + + const supplyCtrl = useController({ + control, + name: `${name}.${index}.supply`, + defaultValue: defaultValue?.supply, + }) + + const decimalsCtrl = useController({ + control, + name: `${name}.${index}.decimals`, + defaultValue: defaultValue?.decimals, + }) + + const decimalsValue = useMemo(() => { + return decimalsCtrl.field.value || undefined + }, [decimalsCtrl.field]) + + const dec = decimalsCtrl.field.value + const sup = supplyCtrl.field.value + const supLen = supplyCtrl.field.value.length + + const supplyPreview = + sup && dec !== undefined + ? `${ + dec >= supLen + ? Array.from({ length: dec - supLen + 2 }).join('0') + : sup.substring(0, supLen - dec) + }${dec > 0 ? '.' : ''}${sup.substring(supLen - dec)}` + : '' + + const decimalsHandleChange = useCallback( + (e: ChangeEvent) => { + const val = Number(e.target.value) + decimalsCtrl.field.onChange(val) + }, + [decimalsCtrl.field], + ) + + const handleRemove = useCallback(() => { + onRemove(index) + }, [index, onRemove]) + + return { + networkCtrl, + contractCtrl, + deployerCtrl, + supplyCtrl, + decimalsCtrl, + decimalsValue, + networks, + supplyPreview, + decimalsHandleChange, + handleRemove, + } +} + +// -------------------- + +export type UseBlockchainNetworksProps = { + name?: string + control: Control + networks: IndexerBlockchainNetworkField[] +} + +export type UseBlockchainNetworksReturn = { + name: string + control: Control + fields: FieldArrayWithId[] + networks: IndexerBlockchainNetworkField[] + handleAdd: () => void + handleRemove: (index?: number) => void +} + +export function useAddIndexerTokenAccounts({ + name = 'tokenAccounts', + control, + networks, +}: UseBlockchainNetworksProps): UseBlockchainNetworksReturn { + const networksCtrl = useFieldArray({ + control, + name, + }) + + const { fields, remove: handleRemove, append } = networksCtrl + + const handleAdd = useCallback(() => { + append({ ...defaultValues }) + }, [append]) + + return { + name, + control, + fields, + networks, + handleAdd, + handleRemove, + } +} diff --git a/src/hooks/form/useAddNameAndTags.ts b/src/hooks/form/useAddNameAndTags.ts index 8d500c73..77bc0442 100644 --- a/src/hooks/form/useAddNameAndTags.ts +++ b/src/hooks/form/useAddNameAndTags.ts @@ -15,7 +15,7 @@ export type UseNameAndTagsProps = { name?: string control: Control defaultValue?: NameAndTagsField - entityType: EntityType.Instance | EntityType.Program + entityType: EntityType.Instance | EntityType.Program | EntityType.Indexer } export type UseNameAndTagsReturn = { diff --git a/src/hooks/form/useSelectFunctionRuntime.ts b/src/hooks/form/useSelectFunctionRuntime.ts index 7d46ad5a..1c811676 100644 --- a/src/hooks/form/useSelectFunctionRuntime.ts +++ b/src/hooks/form/useSelectFunctionRuntime.ts @@ -17,6 +17,7 @@ export const defaultFunctionRuntime: FunctionRuntimeField = { export const defaultFunctionRuntimeOptions = [ FunctionRuntimes[FunctionRuntimeId.Runtime1], FunctionRuntimes[FunctionRuntimeId.Runtime2], + FunctionRuntimes[FunctionRuntimeId.Runtime3], FunctionRuntimes[FunctionRuntimeId.Custom], ] diff --git a/src/hooks/pages/dashboard/useNewFunctionPage.ts b/src/hooks/pages/dashboard/useNewFunctionPage.ts index 346c102c..8dfa1238 100644 --- a/src/hooks/pages/dashboard/useNewFunctionPage.ts +++ b/src/hooks/pages/dashboard/useNewFunctionPage.ts @@ -35,7 +35,7 @@ export type NewFunctionFormState = NameAndTagsField & { domains?: DomainField[] } -const defaultValues: Partial = { +export const defaultValues: Partial = { ...defaultNameAndTags, code: { ...defaultCode }, runtime: { ...defaultFunctionRuntime }, diff --git a/src/hooks/pages/dashboard/useNewIndexerPage.ts b/src/hooks/pages/dashboard/useNewIndexerPage.ts new file mode 100644 index 00000000..18a2e970 --- /dev/null +++ b/src/hooks/pages/dashboard/useNewIndexerPage.ts @@ -0,0 +1,112 @@ +import { useAppState } from '@/contexts/appState' +import { FormEvent, useCallback, useMemo } from 'react' +import { useRouter } from 'next/router' +import { NameAndTagsField } from '../../form/useAddNameAndTags' +import useConnectedWard from '@/hooks/common/useConnectedWard' +import { useForm } from '@/hooks/common/useForm' +import { useIndexerManager } from '@/hooks/common/useManager/useIndexerManager' +import { ActionTypes } from '@/helpers/store' +import { IndexerManager } from '@/domain/indexer' +import { Control, FieldErrors, useWatch } from 'react-hook-form' +import { zodResolver } from '@hookform/resolvers/zod' +import { IndexerBlockchainNetworkField } from '@/hooks/form/useAddIndexerBlockchainNetworks' +import { IndexerTokenAccountField } from '@/hooks/form/useAddIndexerTokenAccounts' + +export type NewIndexerFormState = NameAndTagsField & { + networks: IndexerBlockchainNetworkField[] + accounts: IndexerTokenAccountField[] +} + +const defaultValues: Partial = {} + +// @todo: Split this into reusable hooks by composition + +export type UseNewIndexerPage = { + address: string + accountBalance: number + isCreateButtonDisabled: boolean + values: any + control: Control + errors: FieldErrors + holdingRequirementsProps: Record + handleSubmit: (e: FormEvent) => Promise + handleChangeEntityTab: (tabId: string) => void +} + +export function useNewIndexerPage(): UseNewIndexerPage { + useConnectedWard() + + const router = useRouter() + const [appState, dispatch] = useAppState() + const { account, accountBalance } = appState + + const manager = useIndexerManager() + + const onSubmit = useCallback( + async (state: NewIndexerFormState) => { + if (!manager) throw new Error('Manager not ready') + + const accountIndexer = await manager.add(state) + + // @todo: Change this + dispatch({ + type: ActionTypes.addAccountFunction, + payload: { accountFunction: accountIndexer }, + }) + + // @todo: Check new volumes and domains being created to add them to the store + + router.replace('/dashboard') + }, + [dispatch, manager, router], + ) + + const { + control, + handleSubmit, + formState: { errors }, + } = useForm({ + defaultValues, + onSubmit, + resolver: zodResolver(IndexerManager.addSchema), + }) + // @note: dont use watch, use useWatch instead: https://github.com/react-hook-form/react-hook-form/issues/10753 + const values = useWatch({ control }) as NewIndexerFormState + + const holdingRequirementsProps = IndexerManager.getStaticProgramConfig() + const { specs, isPersistent, volumes } = holdingRequirementsProps + + const { totalCost } = useMemo( + () => + IndexerManager.getCost({ + specs, + isPersistent, + volumes, + capabilities: {}, + }), + [isPersistent, specs, volumes], + ) + + const canAfford = (accountBalance || 0) > totalCost + let isCreateButtonDisabled = !canAfford + if (process.env.NEXT_PUBLIC_OVERRIDE_ALEPH_BALANCE === 'true') { + isCreateButtonDisabled = false + } + + const handleChangeEntityTab = useCallback( + (id: string) => router.push(`/dashboard/${id}`), + [router], + ) + + return { + address: account?.address || '', + accountBalance: accountBalance || 0, + isCreateButtonDisabled, + values, + control, + errors, + holdingRequirementsProps, + handleSubmit, + handleChangeEntityTab, + } +} diff --git a/src/hooks/pages/useHomePage.ts b/src/hooks/pages/useHomePage.ts index 598f6bf2..58c38418 100644 --- a/src/hooks/pages/useHomePage.ts +++ b/src/hooks/pages/useHomePage.ts @@ -10,6 +10,7 @@ export type HomePage = { function: () => void instance: () => void volume: () => void + indexer: () => void } scroll: { function: { ref: RefObject; handle: () => void } @@ -57,12 +58,23 @@ export function useHomePage(): HomePage { router.push('/dashboard/volume') }, [connect, isConnected, router]) + // @note: wait till account is connected and redirect + const navigateIndexer = useCallback(async () => { + if (!isConnected) { + const acc = await connect() + if (!acc) return + } + + router.push('/dashboard/indexer') + }, [connect, isConnected, router]) + return { featureSectionBg, navigate: { function: navigateFunction, instance: navigateInstance, volume: navigateVolume, + indexer: navigateIndexer, }, scroll: { function: { ref: scroll1[0], handle: scroll1[1] }, diff --git a/src/pages/dashboard/indexer.tsx b/src/pages/dashboard/indexer.tsx new file mode 100644 index 00000000..4e1f998e --- /dev/null +++ b/src/pages/dashboard/indexer.tsx @@ -0,0 +1,3 @@ +import NewIndexerPage from '@/components/pages/dashboard/NewIndexerPage' + +export default NewIndexerPage