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 (
+
+ )
+}
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