diff --git a/src/app/application-layout.tsx b/src/app/application-layout.tsx index 5f920bd..e278dd9 100644 --- a/src/app/application-layout.tsx +++ b/src/app/application-layout.tsx @@ -1,3 +1,19 @@ +/* + * Copyright 2025 by Ideal Labs, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + 'use client' import { Avatar } from '@/components/avatar' import { BlockHeaders } from '@/components/etf/latestBlocks' diff --git a/src/app/client-layout.tsx b/src/app/client-layout.tsx new file mode 100644 index 0000000..8521b70 --- /dev/null +++ b/src/app/client-layout.tsx @@ -0,0 +1,26 @@ +'use client'; + +import '@/styles/tailwind.css'; +import { Inter } from 'next/font/google'; +import { ConnectedWalletProvider } from '@/components/etf/connectedWalletContext'; +import { PolkadotProvider } from '@/contexts/PolkadotContext'; +import { ApplicationLayout } from './application-layout'; +import { DIProvider } from '@/components/providers/di-provider'; + +const inter = Inter({ subsets: ['latin'] }); + +export function ClientLayout({ children }: { children: React.ReactNode }) { + return ( + + + + + + {children} + + + + + + ); +} diff --git a/src/app/compose/[...id]/page.tsx b/src/app/compose/[...id]/page.tsx index 19ba398..d4926de 100644 --- a/src/app/compose/[...id]/page.tsx +++ b/src/app/compose/[...id]/page.tsx @@ -1,3 +1,19 @@ +/* + * Copyright 2025 by Ideal Labs, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + 'use client' import { Badge } from '@/components/badge' @@ -87,4 +103,4 @@ export default function ExecutedTransaction({ params }: { readonly params: { rea :
Loading...
); -} \ No newline at end of file +} diff --git a/src/app/compose/page.tsx b/src/app/compose/page.tsx index f62e5d9..54226ad 100644 --- a/src/app/compose/page.tsx +++ b/src/app/compose/page.tsx @@ -1,3 +1,19 @@ +/* + * Copyright 2025 by Ideal Labs, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + 'use client' import { Badge } from '@/components/badge' @@ -13,7 +29,7 @@ import { NUMBER_BLOCKS_EXECUTED, useConnectedWallet } from '@/components/etf/con import { ConnectWallet } from '@/components/etf/connectWallet' import { useState } from 'react' import { ExclamationTriangleIcon, XCircleIcon } from '@heroicons/react/20/solid' -import { explorerClient } from '../explorerClient' +import { explorerClient } from '@/lib/explorer-client'; interface LatestError { index: number; diff --git a/src/app/compose/schedule/page.tsx b/src/app/compose/schedule/page.tsx index 53196fd..603886e 100644 --- a/src/app/compose/schedule/page.tsx +++ b/src/app/compose/schedule/page.tsx @@ -1,3 +1,19 @@ +/* + * Copyright 2025 by Ideal Labs, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + 'use client' import { Button } from '@/components/button' @@ -15,7 +31,7 @@ import { useState } from 'react' import { DelayedTransactionDetails } from '@/domain/DelayedTransactionDetails' import { XCircleIcon } from '@heroicons/react/20/solid' import { useRouter } from 'next/navigation' -import { explorerClient } from '@/app/explorerClient' +import { explorerClient } from '@/lib/explorer-client' const FUTURE_BLOCK_DEFAULT_START: number = 100; diff --git a/src/app/explorerClient.ts b/src/app/explorerClient.ts deleted file mode 100644 index 69ea5f4..0000000 --- a/src/app/explorerClient.ts +++ /dev/null @@ -1,18 +0,0 @@ -import "reflect-metadata"; -import { ExplorerService } from "@/services/ExplorerService"; -import type { IExplorerService } from "@/services/IExplorerService"; -import { container, delay, inject, injectable, registry } from "tsyringe"; - -@injectable() -@registry([ - { - token: "ExplorerServiceImplementation", - useToken: delay(() => ExplorerService) - } -]) -class ExplorerClient { - constructor(@inject("ExplorerServiceImplementation") public explorerServiceInstance: IExplorerService - ) { } -} - -export const explorerClient: IExplorerService = container.resolve(ExplorerClient).explorerServiceInstance; \ No newline at end of file diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 3dff73d..ee8befd 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -1,7 +1,23 @@ +/* + * Copyright 2025 by Ideal Labs, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + import '@/styles/tailwind.css' import type { Metadata } from 'next' import type React from 'react' -import { ApplicationLayout } from './application-layout' +import { ClientLayout } from './client-layout' export const metadata: Metadata = { title: { @@ -23,9 +39,7 @@ export default async function RootLayout({ children }: { children: React.ReactNo - - {children} - + {children} ) diff --git a/src/app/page.tsx b/src/app/page.tsx index 70761c6..3fe8fef 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,3 +1,19 @@ +/* + * Copyright 2025 by Ideal Labs, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + 'use client' import { Badge } from '@/components/badge' diff --git a/src/components/alert.tsx b/src/components/alert.tsx index 4bf149a..a0f82a8 100644 --- a/src/components/alert.tsx +++ b/src/components/alert.tsx @@ -1,3 +1,19 @@ +/* + * Copyright 2025 by Ideal Labs, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + import * as Headless from '@headlessui/react' import clsx from 'clsx' import type React from 'react' diff --git a/src/components/avatar.tsx b/src/components/avatar.tsx index 03bfadd..c3b3f1b 100644 --- a/src/components/avatar.tsx +++ b/src/components/avatar.tsx @@ -1,3 +1,19 @@ +/* + * Copyright 2025 by Ideal Labs, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + import * as Headless from '@headlessui/react' import clsx from 'clsx' import React from 'react' diff --git a/src/components/badge.tsx b/src/components/badge.tsx index 516c3ba..e2ced93 100644 --- a/src/components/badge.tsx +++ b/src/components/badge.tsx @@ -1,3 +1,19 @@ +/* + * Copyright 2025 by Ideal Labs, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + import * as Headless from '@headlessui/react' import clsx from 'clsx' import React from 'react' diff --git a/src/components/button.tsx b/src/components/button.tsx index b569992..f4772e2 100644 --- a/src/components/button.tsx +++ b/src/components/button.tsx @@ -1,3 +1,19 @@ +/* + * Copyright 2025 by Ideal Labs, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + import * as Headless from '@headlessui/react' import { clsx } from 'clsx' import React from 'react' diff --git a/src/components/checkbox.tsx b/src/components/checkbox.tsx index 36e411a..57984a1 100644 --- a/src/components/checkbox.tsx +++ b/src/components/checkbox.tsx @@ -1,3 +1,19 @@ +/* + * Copyright 2025 by Ideal Labs, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + import * as Headless from '@headlessui/react' import { clsx } from 'clsx' import type React from 'react' diff --git a/src/components/description-list.tsx b/src/components/description-list.tsx index 657f8da..dbf13fd 100644 --- a/src/components/description-list.tsx +++ b/src/components/description-list.tsx @@ -1,3 +1,19 @@ +/* + * Copyright 2025 by Ideal Labs, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + import clsx from 'clsx' export function DescriptionList({ className, ...props }: React.ComponentPropsWithoutRef<'dl'>) { diff --git a/src/components/dialog.tsx b/src/components/dialog.tsx index 53979f0..ffe1d5a 100644 --- a/src/components/dialog.tsx +++ b/src/components/dialog.tsx @@ -1,3 +1,19 @@ +/* + * Copyright 2025 by Ideal Labs, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + import * as Headless from '@headlessui/react' import clsx from 'clsx' import type React from 'react' diff --git a/src/components/divider.tsx b/src/components/divider.tsx index dd84b10..b63a3c4 100644 --- a/src/components/divider.tsx +++ b/src/components/divider.tsx @@ -1,3 +1,19 @@ +/* + * Copyright 2025 by Ideal Labs, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + import clsx from 'clsx' export function Divider({ diff --git a/src/components/dropdown.tsx b/src/components/dropdown.tsx index ff8408e..8d25e08 100644 --- a/src/components/dropdown.tsx +++ b/src/components/dropdown.tsx @@ -1,3 +1,19 @@ +/* + * Copyright 2025 by Ideal Labs, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + 'use client' import * as Headless from '@headlessui/react' diff --git a/src/components/etf/connectWallet.tsx b/src/components/etf/connectWallet.tsx index 1b4276a..7c6940c 100644 --- a/src/components/etf/connectWallet.tsx +++ b/src/components/etf/connectWallet.tsx @@ -1,3 +1,19 @@ +/* + * Copyright 2025 by Ideal Labs, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + 'use client' import { Avatar } from '@/components/avatar' import { @@ -22,7 +38,7 @@ import { Text } from "@/components/text"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/table"; import { useConnectedWallet } from "@/components/etf/connectedWalletContext"; import { ArrowRightStartOnRectangleIcon } from "@heroicons/react/20/solid"; -import { explorerClient } from "@/app/explorerClient"; +import { explorerClient } from '@/lib/explorer-client'; export function AccountDropdownMenu({ anchor }: { readonly anchor: 'top start' | 'bottom end' }) { const { setSignerAddress, setSigner, setIsConnected } = useConnectedWallet(); diff --git a/src/components/etf/connectedWalletContext.tsx b/src/components/etf/connectedWalletContext.tsx index 6243a74..c544d25 100644 --- a/src/components/etf/connectedWalletContext.tsx +++ b/src/components/etf/connectedWalletContext.tsx @@ -1,9 +1,13 @@ -import { explorerClient } from '@/app/explorerClient'; +'use client'; + +import { explorerClient } from '@/lib/explorer-client'; import { DelayedTransaction } from '@/domain/DelayedTransaction'; import { ExecutedTransaction } from '@/domain/ExecutedTransaction'; import { Randomness } from '@/domain/Randomness'; -import { ApiPromise, WsProvider } from '@polkadot/api'; import React, { createContext, useContext, useState, ReactNode, useEffect } from 'react'; +import { usePolkadot } from '@/contexts/PolkadotContext'; +import { container } from 'tsyringe'; +import { IChainStateService } from '@/services/IChainStateService'; // Define the shape of the context interface ConnectedWalletContextType { @@ -48,10 +52,13 @@ interface ConnectedWalletContextType { // Create the context with default values const ConnectedWalletContext = createContext(undefined); -export const NUMBER_BLOCKS_EXECUTED = 250; +export const NUMBER_BLOCKS_EXECUTED = 50; export const RAMDOMNESS_SAMPLE = 33; // Create a provider component export const ConnectedWalletProvider: React.FC<{ children: ReactNode }> = ({ children }) => { + const { polkadotApiService } = usePolkadot(); + const chainStateService = container.resolve('IChainStateService'); + const [isReady, setIsReady] = useState(false); const [signer, setSigner] = useState(undefined); // The state variable const [isConnected, setIsConnected] = useState(false); const [signerAddress, setSignerAddress] = useState(""); @@ -72,42 +79,71 @@ export const ConnectedWalletProvider: React.FC<{ children: ReactNode }> = ({ chi const [searchTermScheduled, setSearchTermScheduled] = useState(""); useEffect(() => { + const checkApiReady = async () => { + const ready = await polkadotApiService.isReady(); + setIsReady(ready); + }; + checkApiReady(); + }, [polkadotApiService]); - async function subscribeToLatestBlock() { - const wsProvider = new WsProvider(process.env.NEXT_PUBLIC_NODE_WS || 'wss://rpc.polkadot.io'); - const api = await ApiPromise.create({ provider: wsProvider }); - await api.isReady; + useEffect(() => { + if (!isReady) return; - // Subscribe to new block headers - await api.rpc.chain.subscribeNewHeads(async (lastHeader) => { + let unsubscribe: (() => void) | undefined; - // Get the current epoch index - const epochInfo = await api.query.babe.epochIndex(); - const progress = await api.derive.session.progress(); - // Get session and era progress - setSessionProgress(progress.sessionProgress.toNumber()); - setSessionLength(progress.sessionLength.toNumber()); - setEraProgress(progress.eraProgress.toNumber()); - setSessionsPerEra(progress.sessionsPerEra.toNumber()); - setEpochIndex((epochInfo as any).toNumber()); - const blockNumber = lastHeader.number.toNumber(); - const blockHash = lastHeader.hash.toHex(); - setLatestBlock(blockNumber); - explorerClient.getRandomness(blockNumber, RAMDOMNESS_SAMPLE).then((result) => { - setGeneratedRandomness(result); - }); - explorerClient.queryHistoricalEvents(blockNumber > NUMBER_BLOCKS_EXECUTED ? blockNumber - NUMBER_BLOCKS_EXECUTED : 0, blockNumber).then((result) => { - setExecutedTransactions(result); - }); - explorerClient.getScheduledTransactions().then((result) => { - setScheduledTransactions(result); + const subscribeToUpdates = async () => { + try { + unsubscribe = await chainStateService.subscribeToBlocks((blockNumber) => { + setLatestBlock(blockNumber); + console.log("BLOCK", blockNumber); + // Update other state based on new blocks + if (signerAddress) { + chainStateService.getBalance(signerAddress) + .then(balance => setSignerBalance(balance)) + .catch(console.error); + } + + // Get session and era progress + Promise.all([ + chainStateService.getSessionInfo(), + chainStateService.getEpochIndex(), + explorerClient.getScheduledTransactions(), + explorerClient.queryHistoricalEvents( + blockNumber > NUMBER_BLOCKS_EXECUTED ? + blockNumber - NUMBER_BLOCKS_EXECUTED : 0, + blockNumber + ), + explorerClient.getRandomness(blockNumber, RAMDOMNESS_SAMPLE) + ]).then(([ + sessionInfo, + epochIndex, + scheduled, + executed, + randomness + ]) => { + setSessionProgress(sessionInfo.sessionProgress); + setSessionLength(sessionInfo.sessionLength); + setEraProgress(sessionInfo.eraProgress); + setSessionsPerEra(sessionInfo.sessionsPerEra); + setEpochIndex(epochIndex); + setScheduledTransactions(scheduled); + setExecutedTransactions(executed); + setGeneratedRandomness(randomness); + }).catch(console.error); }); - }); - } + } catch (error) { + console.error('Failed to subscribe to updates:', error); + } + }; - subscribeToLatestBlock(); + subscribeToUpdates(); - }, []); + return () => { + if (unsubscribe) { + unsubscribe(); + } + }; + }, [isReady, signerAddress]); const contextValue = React.useMemo(() => ({ signer, diff --git a/src/components/etf/dynamicExtrinsicForm.tsx b/src/components/etf/dynamicExtrinsicForm.tsx index 678f53d..73383b2 100644 --- a/src/components/etf/dynamicExtrinsicForm.tsx +++ b/src/components/etf/dynamicExtrinsicForm.tsx @@ -1,10 +1,26 @@ -'use client' -import { Field, Label } from '@/components/fieldset' -import { Select } from '@/components/select' -import { Input } from '@/components/input' -import { ApiPromise, WsProvider } from '@polkadot/api' -import React, { useEffect, useState } from 'react' -import { DelayedTransactionDetails } from '@/domain/DelayedTransactionDetails' +/* + * Copyright 2025 by Ideal Labs, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Field, Label } from '@/components/fieldset'; +import { Select } from '@/components/select'; +import { Input } from '@/components/input'; +import React, { useEffect, useState } from 'react'; +import { DelayedTransactionDetails } from '@/domain/DelayedTransactionDetails'; +import { container } from 'tsyringe'; +import { ChainStateService } from '@/services/ChainStateService'; interface PalletOption { text: string; @@ -12,72 +28,44 @@ interface PalletOption { } interface MethodArgument { - argType: string; - argTypeName: string; name: string; + type: string; + typeName: string; value: any; } export const DynamicExtrinsicForm: React.FC<{ block: number, setExtrinsicData: React.Dispatch> }> = ({ block, setExtrinsicData }) => { - const [api, setApi] = useState(null) - const [pallets, setPallets] = useState([]) - const [extrinsics, setExtrinsics] = useState([]) - const [parameters, setParameters] = useState([]) - const [selectedPallet, setSelectedPallet] = useState("") - const [selectedExtrinsic, setSelectedExtrinsic] = useState("") + const chainStateService = container.resolve(ChainStateService); + const [pallets, setPallets] = useState([]); + const [extrinsics, setExtrinsics] = useState([]); + const [parameters, setParameters] = useState([]); + const [selectedPallet, setSelectedPallet] = useState(""); + const [selectedExtrinsic, setSelectedExtrinsic] = useState(""); useEffect(() => { - async function connect() { - const wsProvider = new WsProvider(process.env.NEXT_PUBLIC_NODE_WS || 'wss://rpc.polkadot.io') - const api = await ApiPromise.create({ provider: wsProvider }) - setApi(api) - - const availablePallets = Object - .keys(api.tx) - .filter((s) => - !s.startsWith('$') - ) - .sort() - .filter((name): number => Object.keys(api.tx[name]).length) - .map((name): { text: string; value: string } => ({ - text: name, - value: name - })); - setPallets(availablePallets) + async function loadPallets() { + const availablePallets = await chainStateService.getPallets(); + setPallets(availablePallets); } - connect(); - }, []) + loadPallets(); + }, []); - function handlePalletChange(selectedPallet: string) { + async function handlePalletChange(selectedPallet: string) { setSelectedPallet(selectedPallet); setSelectedExtrinsic(""); - if (api && selectedPallet) { - // Safely access the pallet - const pallet = api.tx[selectedPallet as keyof typeof api.tx]; - if (pallet) { - const palletExtrinsics = Object.keys(pallet); - setExtrinsics(palletExtrinsics); - setParameters([]); // Reset parameters when a new extrinsic is selected - } else { - console.error(`Pallet ${selectedPallet} does not exist or has no extrinsics.`); - setExtrinsics([]); - } + if (selectedPallet) { + const palletExtrinsics = await chainStateService.getExtrinsics(selectedPallet); + setExtrinsics(palletExtrinsics); + setParameters([]); // Reset parameters when a new pallet is selected } } - - function handleExtrinsicChange(selectedExtrinsic: string) { + async function handleExtrinsicChange(selectedExtrinsic: string) { setSelectedExtrinsic(selectedExtrinsic); - if (api && selectedPallet && selectedExtrinsic) { - const extrinsicMeta = api.tx[selectedPallet][selectedExtrinsic].meta - const paramTypes = extrinsicMeta.args.map((arg): MethodArgument => ({ - argType: arg.type.toString(), - argTypeName: arg.typeName.unwrapOrDefault().toString(), - name: arg.name.toString(), - value: null - })); - setParameters(paramTypes); + if (selectedPallet && selectedExtrinsic) { + const paramTypes = await chainStateService.getExtrinsicParameters(selectedPallet, selectedExtrinsic); + setParameters(paramTypes.map(param => ({ ...param, value: null }))); } } @@ -96,7 +84,7 @@ export const DynamicExtrinsicForm: React.FC<{ block: number, setExtrinsicData: R selectedExtrinsic, parameters.map((param) => ({ name: param.name, - type: param.argType, + type: param.type, value: param.value })) )) @@ -106,7 +94,7 @@ export const DynamicExtrinsicForm: React.FC<{ block: number, setExtrinsicData: R } isReady(); - }, [selectedPallet, selectedExtrinsic, parameters, block]) + }, [selectedPallet, selectedExtrinsic, parameters, block]); return (
@@ -135,10 +123,10 @@ export const DynamicExtrinsicForm: React.FC<{ block: number, setExtrinsicData: R )} {parameters.length > 0 && parameters.map((param, index) => ( - - handleParameterChange(index, e.target.value)} autoFocus /> + + handleParameterChange(index, e.target.value)} autoFocus /> ))}
- ) -}; \ No newline at end of file + ); +}; diff --git a/src/components/etf/latestBlocks.tsx b/src/components/etf/latestBlocks.tsx index e105888..3e9a384 100644 --- a/src/components/etf/latestBlocks.tsx +++ b/src/components/etf/latestBlocks.tsx @@ -1,40 +1,37 @@ +/* + * Copyright 2025 by Ideal Labs, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + import React, { useEffect, useState } from 'react'; -import { ApiPromise, WsProvider } from '@polkadot/api'; -import { Badge } from '@/components/badge' -import { - SidebarItem, - SidebarLabel -} from '@/components/sidebar' +import { Badge } from '@/components/badge'; +import { SidebarItem, SidebarLabel } from '@/components/sidebar'; import { formatNumber } from '@polkadot/util'; - -interface BlockHeader { - blockNumber: number; - blockHash: string; - parentHash: string; - stateRoot: string; - extrinsicsRoot: string; -} +import { container } from 'tsyringe'; +import { ChainStateService } from '@/services/ChainStateService'; +import type { BlockHeader } from '@/services/IChainStateService'; export const BlockHeaders: React.FC = () => { const [headers, setHeaders] = useState([]); + const chainStateService = container.resolve(ChainStateService); useEffect(() => { let unsubscribe: () => void; async function subscribeHeaders() { - const wsProvider = new WsProvider(process.env.NEXT_PUBLIC_NODE_WS || 'wss://rpc.polkadot.io'); - const api = await ApiPromise.create({ provider: wsProvider }); - - unsubscribe = await api.rpc.chain.subscribeNewHeads((lastHeader) => { + unsubscribe = await chainStateService.subscribeToNewHeaders((newHeader) => { setHeaders((prevHeaders) => { - const newHeader: BlockHeader = { - blockNumber: lastHeader.number.toNumber(), - blockHash: lastHeader.hash.toHex(), - parentHash: lastHeader.parentHash.toHex(), - stateRoot: lastHeader.stateRoot.toHex(), - extrinsicsRoot: lastHeader.extrinsicsRoot.toHex(), - }; - // Filter out duplicates based on block number const filteredHeaders = prevHeaders.filter( (header) => header.blockNumber !== newHeader.blockNumber diff --git a/src/components/etf/recentEvents.tsx b/src/components/etf/recentEvents.tsx index bcf6f52..7e0d0ea 100644 --- a/src/components/etf/recentEvents.tsx +++ b/src/components/etf/recentEvents.tsx @@ -1,49 +1,64 @@ -import React, { useEffect, useState } from 'react'; -import { ApiPromise, WsProvider } from '@polkadot/api'; -import { Badge } from '@/components/badge' -import { - SidebarItem, - SidebarLabel -} from '@/components/sidebar' -import { explorerClient } from '@/app/explorerClient'; -import { ExecutedTransaction } from '@/domain/ExecutedTransaction'; - -export const LatestEvents: React.FC = () => { - const [events, setEvents] = useState([]); - - useEffect(() => { - - async function subscribeEvents() { - const wsProvider = new WsProvider(process.env.NEXT_PUBLIC_NODE_WS || 'wss://rpc.polkadot.io'); - const api = await ApiPromise.create({ provider: wsProvider }); - await api.isReady; - - // Subscribe to new block headers - await api.rpc.chain.subscribeNewHeads(async (lastHeader) => { - const blockNumber = lastHeader.number.toNumber(); - explorerClient.queryHistoricalEvents(blockNumber, blockNumber).then((result) => { - setEvents(result); - }); - }); - } - - subscribeEvents(); - - return () => { - }; - }, []); - - return ( - <> - {!events?.length && No events found} - {events.map((event, index) => ( - - {event.id} {event.operation} -

- {event?.metadata[0]}

-
-
- ))} - - ); -}; +/* + * Copyright 2025 by Ideal Labs, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React, { useEffect, useState } from 'react'; +import { Badge } from '@/components/badge' +import { + SidebarItem, + SidebarLabel +} from '@/components/sidebar' +import { ExecutedTransaction } from '@/domain/ExecutedTransaction'; +import { container } from 'tsyringe'; +import { IChainStateService } from '@/services/IChainStateService'; +import { explorerClient } from '@/lib/explorer-client'; + +export default function LatestEvents() { + const [events, setEvents] = useState([]); + const chainStateService = container.resolve('IChainStateService'); + + useEffect(() => { + let unsubscribe: (() => void) | undefined; + + const subscribeToBlocks = async () => { + unsubscribe = await chainStateService.subscribeToBlocks(async (blockNumber) => { + const result = await explorerClient.queryHistoricalEvents(blockNumber, blockNumber); + setEvents(result); + }); + }; + + subscribeToBlocks().catch(console.error); + + return () => { + if (unsubscribe) { + unsubscribe(); + } + }; + }, [chainStateService]); + + return ( + <> + {!events?.length && No events found} + {events.map((event, index) => ( + + {event.id} {event.operation} +

+ {event?.metadata[0]}

+
+
+ ))} + + ); +}; diff --git a/src/components/fieldset.tsx b/src/components/fieldset.tsx index ef8c77c..1dc0312 100644 --- a/src/components/fieldset.tsx +++ b/src/components/fieldset.tsx @@ -1,3 +1,19 @@ +/* + * Copyright 2025 by Ideal Labs, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + import * as Headless from '@headlessui/react' import clsx from 'clsx' import type React from 'react' diff --git a/src/components/heading.tsx b/src/components/heading.tsx index 7dd787e..5e28ed3 100644 --- a/src/components/heading.tsx +++ b/src/components/heading.tsx @@ -1,3 +1,19 @@ +/* + * Copyright 2025 by Ideal Labs, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + import { clsx } from 'clsx' type HeadingProps = { level?: 1 | 2 | 3 | 4 | 5 | 6 } & React.ComponentPropsWithoutRef< diff --git a/src/components/input.tsx b/src/components/input.tsx index 3bc4d5b..4d74003 100644 --- a/src/components/input.tsx +++ b/src/components/input.tsx @@ -1,3 +1,19 @@ +/* + * Copyright 2025 by Ideal Labs, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + import * as Headless from '@headlessui/react' import { clsx } from 'clsx' import { forwardRef } from 'react' diff --git a/src/components/link.tsx b/src/components/link.tsx index 7764b81..ca90e8f 100644 --- a/src/components/link.tsx +++ b/src/components/link.tsx @@ -1,3 +1,19 @@ +/* + * Copyright 2025 by Ideal Labs, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + import * as Headless from '@headlessui/react' import NextLink, { type LinkProps } from 'next/link' import React from 'react' diff --git a/src/components/listbox.tsx b/src/components/listbox.tsx index ecf9c6d..34d9d3c 100644 --- a/src/components/listbox.tsx +++ b/src/components/listbox.tsx @@ -1,3 +1,19 @@ +/* + * Copyright 2025 by Ideal Labs, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + 'use client' import * as Headless from '@headlessui/react' diff --git a/src/components/navbar.tsx b/src/components/navbar.tsx index 14bdd41..67637c0 100644 --- a/src/components/navbar.tsx +++ b/src/components/navbar.tsx @@ -1,3 +1,19 @@ +/* + * Copyright 2025 by Ideal Labs, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + 'use client' import * as Headless from '@headlessui/react' diff --git a/src/components/pagination.tsx b/src/components/pagination.tsx index 98a94ca..af8463a 100644 --- a/src/components/pagination.tsx +++ b/src/components/pagination.tsx @@ -1,3 +1,19 @@ +/* + * Copyright 2025 by Ideal Labs, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + import clsx from 'clsx' import type React from 'react' import { Button } from './button' diff --git a/src/components/providers/di-provider.tsx b/src/components/providers/di-provider.tsx new file mode 100644 index 0000000..f7443dd --- /dev/null +++ b/src/components/providers/di-provider.tsx @@ -0,0 +1,19 @@ +'use client'; + +import { container } from '@/lib/di-container'; +import { useEffect, useState } from 'react'; + +export function DIProvider({ children }: { children: React.ReactNode }) { + const [isReady, setIsReady] = useState(false); + + useEffect(() => { + // Container is already initialized by the import + setIsReady(true); + }, []); + + if (!isReady) { + return null; + } + + return <>{children}; +} diff --git a/src/components/radio.tsx b/src/components/radio.tsx index 464fb3b..f665198 100644 --- a/src/components/radio.tsx +++ b/src/components/radio.tsx @@ -1,3 +1,19 @@ +/* + * Copyright 2025 by Ideal Labs, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + import * as Headless from '@headlessui/react' import { clsx } from 'clsx' diff --git a/src/components/select.tsx b/src/components/select.tsx index 305e1f6..cb301f5 100644 --- a/src/components/select.tsx +++ b/src/components/select.tsx @@ -1,3 +1,19 @@ +/* + * Copyright 2025 by Ideal Labs, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + import * as Headless from '@headlessui/react' import { clsx } from 'clsx' import { forwardRef } from 'react' diff --git a/src/components/sidebar-layout.tsx b/src/components/sidebar-layout.tsx index 4f2ad5f..295a351 100644 --- a/src/components/sidebar-layout.tsx +++ b/src/components/sidebar-layout.tsx @@ -1,3 +1,19 @@ +/* + * Copyright 2025 by Ideal Labs, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + 'use client' import * as Headless from '@headlessui/react' diff --git a/src/components/sidebar.tsx b/src/components/sidebar.tsx index cd2bc1e..62f60ea 100644 --- a/src/components/sidebar.tsx +++ b/src/components/sidebar.tsx @@ -1,3 +1,19 @@ +/* + * Copyright 2025 by Ideal Labs, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + 'use client' import * as Headless from '@headlessui/react' diff --git a/src/components/stacked-layout.tsx b/src/components/stacked-layout.tsx index 43c99fd..2e6bc14 100644 --- a/src/components/stacked-layout.tsx +++ b/src/components/stacked-layout.tsx @@ -1,3 +1,19 @@ +/* + * Copyright 2025 by Ideal Labs, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + 'use client' import * as Headless from '@headlessui/react' diff --git a/src/components/switch.tsx b/src/components/switch.tsx index 9c3d07a..93ddeb2 100644 --- a/src/components/switch.tsx +++ b/src/components/switch.tsx @@ -1,3 +1,19 @@ +/* + * Copyright 2025 by Ideal Labs, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + import * as Headless from '@headlessui/react' import { clsx } from 'clsx' import type React from 'react' diff --git a/src/components/table.tsx b/src/components/table.tsx index b746e71..cd71d2e 100644 --- a/src/components/table.tsx +++ b/src/components/table.tsx @@ -1,3 +1,19 @@ +/* + * Copyright 2025 by Ideal Labs, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + 'use client' import { clsx } from 'clsx' diff --git a/src/components/text.tsx b/src/components/text.tsx index 18eaf6c..4a88eca 100644 --- a/src/components/text.tsx +++ b/src/components/text.tsx @@ -1,3 +1,19 @@ +/* + * Copyright 2025 by Ideal Labs, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + import { clsx } from 'clsx' import { Link } from './link' diff --git a/src/components/textarea.tsx b/src/components/textarea.tsx index 4552d30..89f6ce3 100644 --- a/src/components/textarea.tsx +++ b/src/components/textarea.tsx @@ -1,3 +1,19 @@ +/* + * Copyright 2025 by Ideal Labs, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + import * as Headless from '@headlessui/react' import { clsx } from 'clsx' import { forwardRef } from 'react' diff --git a/src/contexts/PolkadotContext.tsx b/src/contexts/PolkadotContext.tsx new file mode 100644 index 0000000..800ad6a --- /dev/null +++ b/src/contexts/PolkadotContext.tsx @@ -0,0 +1,45 @@ +'use client'; + +import { container } from 'tsyringe'; +import React, { createContext, useContext, useState, useEffect } from 'react'; +import { IPolkadotApiService } from '@/services/IPolkadotApiService'; +import { IChainStateService } from '@/services/IChainStateService'; + +interface PolkadotContextType { + polkadotApiService: IPolkadotApiService; + chainStateService: IChainStateService; +} + +const PolkadotContext = createContext(null); + +export const PolkadotProvider = ({ children }: { children: React.ReactNode }) => { + const [services, setServices] = useState(null); + + useEffect(() => { + const polkadotApiService = container.resolve('IPolkadotApiService'); + const chainStateService = container.resolve('IChainStateService'); + + setServices({ + polkadotApiService, + chainStateService, + }); + }, []); + + if (!services) { + return null; + } + + return ( + + {children} + + ); +} + +export function usePolkadot() { + const context = useContext(PolkadotContext); + if (!context) { + throw new Error('usePolkadot must be used within a PolkadotProvider'); + } + return context; +} diff --git a/src/data.ts b/src/data.ts index 7cab841..865e9c1 100644 --- a/src/data.ts +++ b/src/data.ts @@ -1,3 +1,19 @@ +/* + * Copyright 2025 by Ideal Labs, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + export function getCountries() { return [ { diff --git a/src/domain/DelayedTransaction.ts b/src/domain/DelayedTransaction.ts index c14b7d8..17cf9f0 100644 --- a/src/domain/DelayedTransaction.ts +++ b/src/domain/DelayedTransaction.ts @@ -1,3 +1,19 @@ +/* + * Copyright 2025 by Ideal Labs, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + /** * This is the DelayedTransaction domain class. */ @@ -21,4 +37,4 @@ export class DelayedTransaction { this.operation = operation; this.deadlineBlock = deadlineBlock } -} \ No newline at end of file +} diff --git a/src/domain/DelayedTransactionDetails.ts b/src/domain/DelayedTransactionDetails.ts index 8030cf4..bbac16a 100644 --- a/src/domain/DelayedTransactionDetails.ts +++ b/src/domain/DelayedTransactionDetails.ts @@ -1,3 +1,19 @@ +/* + * Copyright 2025 by Ideal Labs, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + /** * This is the DelayedTransactionDetails domain class. */ @@ -25,4 +41,4 @@ export class DelayedTransactionDetails { this.extrinsic = extrinsic; this.params = params; } -} \ No newline at end of file +} diff --git a/src/domain/ExecutedTransaction.ts b/src/domain/ExecutedTransaction.ts index 74aff45..fe536c4 100644 --- a/src/domain/ExecutedTransaction.ts +++ b/src/domain/ExecutedTransaction.ts @@ -1,3 +1,19 @@ +/* + * Copyright 2025 by Ideal Labs, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + /** * This is the DelayedTransaction domain class. */ @@ -30,4 +46,4 @@ export class ExecutedTransaction { this.delayedTx = delayedTx; } -} \ No newline at end of file +} diff --git a/src/domain/Randomness.ts b/src/domain/Randomness.ts index e0834e3..07f5b75 100644 --- a/src/domain/Randomness.ts +++ b/src/domain/Randomness.ts @@ -1,3 +1,19 @@ +/* + * Copyright 2025 by Ideal Labs, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + /** * This is the Randomness domain class. It is used to represent a generated random value. */ @@ -16,4 +32,4 @@ export class Randomness { this.randomness = randomness; this.signature = signature; } -} \ No newline at end of file +} diff --git a/src/domain/Subscription.ts b/src/domain/Subscription.ts index f7f7758..e1d95f7 100644 --- a/src/domain/Subscription.ts +++ b/src/domain/Subscription.ts @@ -1,3 +1,19 @@ +/* + * Copyright 2025 by Ideal Labs, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + /** * This module contains the domain entities for handling randomness subscriptions. * It mirrors the structure defined in the idn-manager pallet and provides diff --git a/src/lib/di-container.ts b/src/lib/di-container.ts new file mode 100644 index 0000000..f11f8bd --- /dev/null +++ b/src/lib/di-container.ts @@ -0,0 +1,42 @@ +/* + * Copyright 2025 by Ideal Labs, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import 'reflect-metadata'; +import { container } from 'tsyringe'; +import { IPolkadotApiService } from '../services/IPolkadotApiService'; +import { PolkadotApiService } from '../services/PolkadotApiService'; +import { IChainStateService } from '../services/IChainStateService'; +import { ChainStateService } from '../services/ChainStateService'; +import { IExplorerService } from '../services/IExplorerService'; +import { ExplorerService } from '../services/ExplorerService'; + +// Only register services if we're in a browser environment +if (typeof window !== 'undefined') { + // Register services + container.register('IPolkadotApiService', { + useClass: PolkadotApiService + }); + + container.register('IChainStateService', { + useClass: ChainStateService + }); + + container.register('IExplorerService', { + useClass: ExplorerService + }); +} + +export { container }; diff --git a/src/lib/explorer-client.ts b/src/lib/explorer-client.ts new file mode 100644 index 0000000..803c289 --- /dev/null +++ b/src/lib/explorer-client.ts @@ -0,0 +1,28 @@ +/* + * Copyright 2025 by Ideal Labs, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { ExplorerService } from "@/services/ExplorerService"; +import type { IExplorerService } from "@/services/IExplorerService"; +import { container } from './di-container'; + +// Only create the client in browser environment +let explorerClient: IExplorerService | null = null; + +if (typeof window !== 'undefined') { + explorerClient = container.resolve('IExplorerService'); +} + +export { explorerClient }; diff --git a/src/services/ChainStateService.ts b/src/services/ChainStateService.ts new file mode 100644 index 0000000..f36fba8 --- /dev/null +++ b/src/services/ChainStateService.ts @@ -0,0 +1,120 @@ +/* + * Copyright 2025 by Ideal Labs, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { formatBalance } from '@polkadot/util'; +import { inject, singleton } from 'tsyringe'; +import type { IChainStateService, SessionInfo, BlockHeader } from './IChainStateService'; +import type { IPolkadotApiService } from './IPolkadotApiService'; + +@singleton() +export class ChainStateService implements IChainStateService { + constructor( + @inject('IPolkadotApiService') private polkadotApiService: IPolkadotApiService + ) {} + + async getBalance(address: string): Promise { + try { + const api = await this.polkadotApiService.getApi(); + const { data: balance } = await api.query.system.account(address); + return formatBalance(balance.free, { withUnit: true }); + } catch (error) { + console.error('Error fetching balance:', error); + throw error; + } + } + + async subscribeToBlocks(callback: (blockNumber: number) => void): Promise<() => void> { + return this.subscribeToNewHeaders((header) => callback(header.blockNumber)); + } + + async subscribeToNewHeaders(callback: (header: BlockHeader) => void): Promise<() => void> { + const api = await this.polkadotApiService.getApi(); + const unsubscribe = await api.rpc.chain.subscribeNewHeads((lastHeader) => { + const header: BlockHeader = { + blockNumber: lastHeader.number.toNumber(), + blockHash: lastHeader.hash.toHex(), + parentHash: lastHeader.parentHash.toHex(), + stateRoot: lastHeader.stateRoot.toHex(), + extrinsicsRoot: lastHeader.extrinsicsRoot.toHex(), + }; + callback(header); + }); + return unsubscribe; + } + + async getSessionInfo(): Promise { + const api = await this.polkadotApiService.getApi(); + const progress = await api.derive.session.progress(); + + return { + sessionProgress: progress.sessionProgress.toNumber(), + sessionLength: progress.sessionLength.toNumber(), + eraProgress: progress.eraProgress.toNumber(), + sessionsPerEra: progress.sessionsPerEra.toNumber() + }; + } + + async getEpochIndex(): Promise { + const api = await this.polkadotApiService.getApi(); + const epochInfo = await api.query.babe.epochIndex(); + return epochInfo.toNumber(); + } + + async subscribeToBalanceChanges( + address: string, + callback: (balance: string) => void + ): Promise<() => void> { + const api = await this.polkadotApiService.getApi(); + const unsubscribe = await api.query.system.account( + address, + ({ data: balance }) => { + callback(formatBalance(balance.free, { withUnit: true })); + } + ); + return unsubscribe; + } + + async getPallets(): Promise<{ text: string; value: string; }[]> { + const api = await this.polkadotApiService.getApi(); + return Object.keys(api.tx) + .filter((s) => !s.startsWith('$')) + .sort() + .filter((name): number => Object.keys(api.tx[name]).length) + .map((name) => ({ + text: name, + value: name + })); + } + + async getExtrinsics(pallet: string): Promise { + const api = await this.polkadotApiService.getApi(); + const palletApi = api.tx[pallet as keyof typeof api.tx]; + if (palletApi) { + return Object.keys(palletApi); + } + return []; + } + + async getExtrinsicParameters(pallet: string, extrinsic: string): Promise<{ name: string; type: string; typeName: string; }[]> { + const api = await this.polkadotApiService.getApi(); + const extrinsicMeta = api.tx[pallet][extrinsic].meta; + return extrinsicMeta.args.map((arg) => ({ + name: arg.name.toString(), + type: arg.type.toString(), + typeName: arg.typeName.unwrapOrDefault().toString() + })); + } +} diff --git a/src/services/ExplorerService.ts b/src/services/ExplorerService.ts index 51cc24b..2b6ac03 100644 --- a/src/services/ExplorerService.ts +++ b/src/services/ExplorerService.ts @@ -1,5 +1,22 @@ -import { singleton } from "tsyringe"; -import { IExplorerService } from "./IExplorerService"; +/* + * Copyright 2025 by Ideal Labs, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { inject, singleton } from "tsyringe"; +import type { IExplorerService } from "./IExplorerService"; +import type { IPolkadotApiService } from "./IPolkadotApiService"; import { cryptoWaitReady } from '@polkadot/util-crypto'; import chainSpec from "../etf_spec/dev/etf_spec.json" import { Etf } from "@ideallabs/etf.js"; @@ -11,101 +28,126 @@ import { EventRecord, SignedBlock } from '@polkadot/types/interfaces'; @singleton() export class ExplorerService implements IExplorerService { + private etfApi: Etf | null = null; - api: any; - node_dev: string = "ws://127.0.0.1:9944"; - - constructor() { - this.getEtfApi().then(() => { + constructor( + @inject('IPolkadotApiService') private polkadotApiService: IPolkadotApiService + ) { + this.initializeEtf().then(() => { console.log("ETF.js API is ready."); }); - }; + } - async getEtfApi(signer = undefined): Promise { + private async initializeEtf(): Promise { - if (!this.api) { - // ensure params are defined - if (process.env.NEXT_PUBLIC_NODE_WS === undefined) { - console.error("Provide a valid value for NEXT_PUBLIC_NODE_WS. Using fallback"); - process.env.NEXT_PUBLIC_NODE_WS = this.node_dev; - } + if (!process.env.NEXT_PUBLIC_NODE_WS) { + console.error("NEXT_PUBLIC_NODE_WS environment variable is not defined."); + this.etfApi = null; + return; + } + if (!this.etfApi) { try { await cryptoWaitReady(); - let api = new Etf(process.env.NEXT_PUBLIC_NODE_WS, false); + this.etfApi = new Etf(process.env.NEXT_PUBLIC_NODE_WS, false); console.log("Connecting to ETF chain"); - await api.init(JSON.stringify(chainSpec)); - this.api = api; - console.log("api initialized") - } catch (_e) { - // TODO: next will try to fetch the wasm blob but it doesn't need to - // since the transitive dependency is built with the desired wasm already - // so we can ignore this error for now (no impact to functionality) - // but shall be addressed in the future + await this.etfApi.init(JSON.stringify(chainSpec)); + console.log("ETF API initialized"); + } catch (e) { + console.error("Failed to initialize ETF API:", e); + this.etfApi = null; + throw e; } } + } + + async getEtfApi(signer = undefined): Promise { + if (!this.etfApi) { + await this.initializeEtf(); + } + if (signer) { - this.api.api.setSigner(signer); + // Only set signer on ETF API for timelock operations + this.etfApi!.api.setSigner(signer); } - return Promise.resolve(this.api); - }; + return this.etfApi; + } async getRandomness(blockNumber: number, size: number = 10): Promise { - const api = await this.getEtfApi(); + const polkadotApi = await this.polkadotApiService.getApi(); let listOfGeneratedRandomness: Randomness[] = []; + + if (!polkadotApi?.query?.randomnessBeacon?.pulses) { + console.log("Network Without Randomness Beacon"); + return listOfGeneratedRandomness; + } + let i: number = 0; while (i < size && (blockNumber - i >= 0)) { let nextBlock: number = blockNumber - i; - let result = await api.api.query.randomnessBeacon.pulses(nextBlock).then((pulse: any) => { - return new Randomness( - nextBlock, - pulse.toHuman() != null ? pulse?.toHuman()['body']?.randomness as string : "", - pulse.toHuman() != null ? pulse?.toHuman()['body']?.signature as string : "" - ); - }); - if (result.randomness != "") { - listOfGeneratedRandomness.push(result); + try { + const pulse = await polkadotApi.query.randomnessBeacon.pulses(nextBlock); + const pulseData = pulse.toHuman(); + if (pulseData) { + const result = new Randomness( + nextBlock, + pulseData['body']?.randomness || "", + pulseData['body']?.signature || "" + ); + if (result.randomness !== "") { + listOfGeneratedRandomness.push(result); + } + } + } catch (e) { + console.error(`Error fetching randomness for block ${nextBlock}:`, e); } i++; } - return Promise.resolve(listOfGeneratedRandomness); + return listOfGeneratedRandomness; } async scheduleTransaction(signer: any, transactionDetails: DelayedTransactionDetails): Promise { - const api = await this.getEtfApi(signer.signer); - let extrinsicPath = `api.api.tx.${transactionDetails.pallet}.${transactionDetails.extrinsic}`; - let parametersPath = "("; - if (transactionDetails.params.length > 0) { - transactionDetails.params.forEach((param: any) => { - if (isNaN(param.value) && param.value != "true" && param.value != "false") { - parametersPath += `"${param.value}", `; - } - else { - parametersPath += `${param.value}, `; - } - }); - parametersPath = parametersPath.slice(0, -2); + const polkadotApi = await this.polkadotApiService.getApi(); + const etfApi = await this.getEtfApi(); + + // Get the inner call using Polkadot API + const tx = polkadotApi.tx[transactionDetails.pallet][transactionDetails.extrinsic]; + if (!tx) { + throw new Error(`Invalid extrinsic: ${transactionDetails.pallet}.${transactionDetails.extrinsic}`); } - parametersPath += ")"; - extrinsicPath += parametersPath; - let innerCall = eval(extrinsicPath); - let deadline = transactionDetails.block; - let outerCall = await api.delay(innerCall, 127, deadline); - await outerCall.signAndSend(signer.address, (result: any) => { + + // Parse parameters + const params = transactionDetails.params.map(param => { + if (param.value === "true") return true; + if (param.value === "false") return false; + if (!isNaN(param.value)) return Number(param.value); + return param.value; + }); + + // Create the inner call + const innerCall = tx(...params); + + // Use ETF's delay function with our Polkadot API call + const outerCall = await etfApi.delay(innerCall, 127, transactionDetails.block); + + // Sign and send using Polkadot API + await outerCall.signAndSend(signer.address, { signer: signer.signer }, (result: any) => { if (result.status.isInBlock) { - console.log('in block') + console.log('Transaction in block:', result.status.asInBlock.toHex()); } }); - return Promise.resolve(); } async getScheduledTransactions(): Promise { - const api = await this.getEtfApi(); + const polkadotApi = await this.polkadotApiService.getApi(); let listOfTransactions: DelayedTransaction[] = []; - let entries = await api.api.query.scheduler.agenda.entries(); + if (!polkadotApi?.query?.scheduler?.agenda) { + console.log("Network Without Randomness Beacon"); + return listOfTransactions; + } + const entries = await polkadotApi.query.scheduler.agenda.entries(); entries.forEach(([key, value]: [any, any]) => { for (const humanValue of value.map((v: any) => v.toHuman())) { - //we are only interested on those txs scheduled to be executed in the future if (humanValue.maybeCiphertext) { const delayedTx = new DelayedTransaction( "NA", @@ -118,125 +160,134 @@ export class ExplorerService implements IExplorerService { } } }); - return Promise.resolve(listOfTransactions); + return listOfTransactions; } async queryHistoricalEvents(startBlock: number, endBlock: number): Promise { - let api = await this.getEtfApi(); + const polkadotApi = await this.polkadotApiService.getApi(); let listOfEvents: ExecutedTransaction[] = []; - // Connect to the node + for (let blockNumber = startBlock; blockNumber <= endBlock; blockNumber++) { - // Get the block hash - const blockHash = await api.api.rpc.chain.getBlockHash(blockNumber); - const signedBlock: SignedBlock = await api.api.rpc.chain.getBlock(blockHash); - // Get the block to fetch extrinsics - const block = await api.api.rpc.chain.getBlock(blockHash); - // Get the events for the block - const events = await api.api.query.system.events.at(blockHash); - // Loop through the extrinsics to get the signer (owner) of each transaction - signedBlock.block.extrinsics.forEach((extrinsic, index) => { - const { method, signer } = extrinsic; - // Find all the events associated with this extrinsic - const relatedEvents = events.filter(({ phase }: { phase: any }) => - phase.isApplyExtrinsic && phase.asApplyExtrinsic.eq(index) - ); - // Check for success or failure in the related events - let status = 'Pending'; // Default status - relatedEvents.forEach((record: EventRecord) => { - const { event } = record; - if (event.section === 'system' && event.method === 'ExtrinsicSuccess') { - status = 'Confirmed'; // Set status as Success if the ExtrinsicSuccess event is present - } else if (event.section === 'system' && event.method === 'ExtrinsicFailed') { - status = 'Failed'; // Set status as Failed if the ExtrinsicFailed event is present - } - }); - // For each event related to this extrinsic - relatedEvents.forEach((record: EventRecord) => { - const { event } = record; - const types = event.typeDef; - const operation = `${event.section}.${event.method}`; - // Create a new ExecutedTransaction instance - const executedTransaction = new ExecutedTransaction( - blockNumber, // block - `${blockNumber}-${index}`, // id (unique per block and event index) - signer?.toString() || 'Unsigned', // signer (owner) from the extrinsic - operation, // operation (e.g., balances.Transfer) - status, // actual status based on the events - event.data.map((data, i) => ({ // eventData (raw event data) - type: types[i].type, - value: data.toString() - })), - event?.meta?.docs?.map(meta => meta.toString().trim()), - operation === "scheduler.Scheduled" + try { + // Get the block hash and block + const blockHash = await polkadotApi.rpc.chain.getBlockHash(blockNumber); + const signedBlock = await polkadotApi.rpc.chain.getBlock(blockHash); + const events = await polkadotApi.query.system.events.at(blockHash); + + // Process extrinsics and their events + signedBlock.block.extrinsics.forEach((extrinsic, index) => { + const { method, signer } = extrinsic; + + // Find events for this extrinsic + const relatedEvents = events.filter(({ phase }: EventRecord) => + phase.isApplyExtrinsic && phase.asApplyExtrinsic.eq(index) ); - // Add the transaction to the array - listOfEvents.push(executedTransaction); + + // Determine transaction status + let status = 'Pending'; + relatedEvents.forEach((record: EventRecord) => { + const { event } = record; + if (event.section === 'system') { + if (event.method === 'ExtrinsicSuccess') status = 'Confirmed'; + else if (event.method === 'ExtrinsicFailed') status = 'Failed'; + } + }); + + // Process each event + relatedEvents.forEach((record: EventRecord) => { + const { event } = record; + const operation = `${event.section}.${event.method}`; + + const executedTransaction = new ExecutedTransaction( + blockNumber, + `${blockNumber}-${index}`, + signer?.toString() || 'Unsigned', + operation, + status, + event.data.map((data, i) => ({ + type: event.typeDef[i].type, + value: data.toString() + })), + event.meta.docs.map(d => d.toString().trim()), + operation === "scheduler.Scheduled" + ); + + listOfEvents.push(executedTransaction); + }); }); - }); - // Handle system events that are not tied to extrinsics - const systemEvents = events.filter(({ phase }: { phase: any }) => phase.isFinalization || phase.isInitialization); - systemEvents.forEach((record: EventRecord, index: number) => { - const { event } = record; - const types = event.typeDef; - // Helper function to determine if a value is a valid hex address - function looksLikeAddress(value: string): boolean { - return value?.startsWith('0x') || value?.length >= 48; - } - const eventData = event.data.map((data, i) => ({ - type: types[i].type, - value: data.toString(), - })); - // Create a new ExecutedTransaction instance for system events - const operation = `${event.section}.${event.method}`; - const executedTransaction = new ExecutedTransaction( - blockNumber, // block - `${blockNumber}-sys-${index}`, // id (unique per block and system event index) - looksLikeAddress(eventData[0]?.value) ? eventData[0]?.value : "System", - operation, // operation (e.g., system.Finalized) - 'Confirmed', // Status for system events is usually successful - eventData, - event?.meta?.docs?.map(meta => meta.toString().trim()), - operation === "scheduler.Dispatched" || looksLikeAddress(eventData[0]?.value) - ); - // Add the transaction to the array - listOfEvents.push(executedTransaction); - }); + + // Handle system events + events + .filter(({ phase }) => phase.isFinalization || phase.isInitialization) + .forEach((record: EventRecord, index: number) => { + const { event } = record; + const eventData = event.data.map((data, i) => ({ + type: event.typeDef[i].type, + value: data.toString() + })); + + const operation = `${event.section}.${event.method}`; + const executedTransaction = new ExecutedTransaction( + blockNumber, + `${blockNumber}-sys-${index}`, + this.looksLikeAddress(eventData[0]?.value) ? eventData[0].value : "System", + operation, + 'Confirmed', + eventData, + event.meta.docs.map(d => d.toString().trim()), + operation === "scheduler.Dispatched" || this.looksLikeAddress(eventData[0]?.value) + ); + + listOfEvents.push(executedTransaction); + }); + } catch (e) { + console.error(`Error processing block ${blockNumber}:`, e); + } } - listOfEvents.reverse() - return Promise.resolve(listOfEvents); + listOfEvents.reverse(); + return listOfEvents; } async getFreeBalance(signer: any): Promise { - const api = await this.getEtfApi(signer); - const accountInfo = await api.api.query.system.account(signer.address); - return Promise.resolve(accountInfo.data.free.toHuman()); + const polkadotApi = await this.polkadotApiService.getApi(); + const { data: balance } = await polkadotApi.query.system.account(signer.address); + return balance.free.toHuman(); } async cancelTransaction(signer: any, blockNumber: number, index: number): Promise { - console.log('canceling transaction', blockNumber, index, signer); - const api = await this.getEtfApi(signer); - // Initialize keyring and add the account using the string value - await api.api.tx.scheduler.cancel(blockNumber, index).signAndSend(signer.address, { signer: signer.signer }, (result: any) => { - if (result.status.isInBlock) { - console.log('in block'); - } - // Check if there is a dispatch error - if (result.dispatchError) { - if (result.dispatchError.isModule) { - // Decode the module error - const decoded = api.api.registry.findMetaError(result.dispatchError.asModule); - console.error(`Error: ${JSON.stringify(decoded)}`); - } else { - // Handle other errors (non-module errors) - console.error(`Error: ${result.dispatchError.toString()}`); - } - } else { - console.log('Extrinsic executed successfully'); - } - }); + const polkadotApi = await this.polkadotApiService.getApi(); + console.log('Canceling transaction', blockNumber, index, signer); + + return new Promise((resolve, reject) => { + polkadotApi.tx.scheduler.cancel(blockNumber, index) + .signAndSend(signer.address, { signer: signer.signer }, (result: any) => { + if (result.status.isInBlock) { + console.log('Transaction included in block:', result.status.asInBlock.toHex()); + resolve(); + } - return Promise.resolve(); + if (result.dispatchError) { + if (result.dispatchError.isModule) { + const decoded = polkadotApi.registry.findMetaError(result.dispatchError.asModule); + const decodedError = `Module Error: ${decoded.section}.${decoded.method}: ${decoded.docs}`; + console.error(decodedError); + reject(new Error(decodedError)); + } else { + const error = result.dispatchError.toString(); + console.error('Dispatch error:', error); + reject(new Error(error)); + } + } + }) + .catch((e: any) => { + console.error('Error canceling transaction:', e); + reject(e); + }); + }); } -} \ No newline at end of file + private looksLikeAddress(value: string): boolean { + return value?.startsWith('0x') || value?.length >= 48; + } +} diff --git a/src/services/IChainStateService.ts b/src/services/IChainStateService.ts new file mode 100644 index 0000000..64d85f6 --- /dev/null +++ b/src/services/IChainStateService.ts @@ -0,0 +1,42 @@ +/* + * Copyright 2025 by Ideal Labs, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export interface SessionInfo { + sessionProgress: number; + sessionLength: number; + eraProgress: number; + sessionsPerEra: number; +} + +export interface BlockHeader { + blockNumber: number; + blockHash: string; + parentHash: string; + stateRoot: string; + extrinsicsRoot: string; +} + +export interface IChainStateService { + getBalance(address: string): Promise; + subscribeToBlocks(callback: (blockNumber: number) => void): Promise<() => void>; + getSessionInfo(): Promise; + getEpochIndex(): Promise; + subscribeToBalanceChanges(address: string, callback: (balance: string) => void): Promise<() => void>; + subscribeToNewHeaders(callback: (header: BlockHeader) => void): Promise<() => void>; + getPallets(): Promise<{ text: string; value: string; }[]>; + getExtrinsics(pallet: string): Promise; + getExtrinsicParameters(pallet: string, extrinsic: string): Promise<{ name: string; type: string; typeName: string; }[]>; +} diff --git a/src/services/IExplorerService.ts b/src/services/IExplorerService.ts index fe423f7..0ef06f1 100644 --- a/src/services/IExplorerService.ts +++ b/src/services/IExplorerService.ts @@ -1,3 +1,19 @@ +/* + * Copyright 2025 by Ideal Labs, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + import { DelayedTransaction } from "@/domain/DelayedTransaction"; import { DelayedTransactionDetails } from "@/domain/DelayedTransactionDetails"; import { ExecutedTransaction } from "@/domain/ExecutedTransaction"; diff --git a/src/services/IPolkadotApiService.ts b/src/services/IPolkadotApiService.ts new file mode 100644 index 0000000..2599054 --- /dev/null +++ b/src/services/IPolkadotApiService.ts @@ -0,0 +1,26 @@ +/* + * Copyright 2025 by Ideal Labs, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { ApiPromise } from '@polkadot/api'; + +export interface IPolkadotApiService { + getApi(): Promise; + isReady(): Promise; + disconnect(): Promise; + onReady(callback: () => void): void; + onDisconnect(callback: () => void): void; + onError(callback: (error: Error) => void): void; +} diff --git a/src/services/ISubscriptionService.ts b/src/services/ISubscriptionService.ts index 35b737c..a25335d 100644 --- a/src/services/ISubscriptionService.ts +++ b/src/services/ISubscriptionService.ts @@ -1,3 +1,19 @@ +/* + * Copyright 2025 by Ideal Labs, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + import { Subscription, SubscriptionDetails } from '../domain/Subscription'; /** diff --git a/src/services/MockSubscriptionService.ts b/src/services/MockSubscriptionService.ts index 0b0a2a1..dec3f0d 100644 --- a/src/services/MockSubscriptionService.ts +++ b/src/services/MockSubscriptionService.ts @@ -1,3 +1,19 @@ +/* + * Copyright 2025 by Ideal Labs, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + import { ISubscriptionService } from './ISubscriptionService'; import { Subscription, SubscriptionState, SubscriptionDetails } from '../domain/Subscription'; diff --git a/src/services/PolkadotApiService.ts b/src/services/PolkadotApiService.ts new file mode 100644 index 0000000..e3378ad --- /dev/null +++ b/src/services/PolkadotApiService.ts @@ -0,0 +1,149 @@ +/* + * Copyright 2025 by Ideal Labs, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { ApiPromise, WsProvider } from '@polkadot/api'; +import { singleton } from 'tsyringe'; +import { IPolkadotApiService } from './IPolkadotApiService'; + +@singleton() +export class PolkadotApiService implements IPolkadotApiService { + private api: ApiPromise | null = null; + private wsProvider: WsProvider | null = null; + private connectionPromise: Promise | null = null; + private readyCallbacks: (() => void)[] = []; + private disconnectCallbacks: (() => void)[] = []; + private errorCallbacks: ((error: Error) => void)[] = []; + + constructor() { + // Don't initialize in constructor, wait for first getApi call + } + + private getNodeUrl(): string { + return process.env.NEXT_PUBLIC_NODE_WS || 'wss://rpc.polkadot.io' + } + + private async initApi(): Promise { + try { + if (!this.wsProvider) { + this.wsProvider = new WsProvider(this.getNodeUrl()); + + // Set up event handlers before creating API + this.wsProvider.on('connected', () => { + console.log('Connected to node:', this.getNodeUrl()); + this.notifyReady(); + }); + + this.wsProvider.on('disconnected', () => { + console.log('Disconnected from node'); + // Clear the API instance on disconnect so we can reconnect fresh + this.api = null; + this.connectionPromise = null; + this.notifyDisconnect(); + }); + + this.wsProvider.on('error', (error: Error) => { + console.error('WebSocket error:', error); + // Clear connection state on error + this.api = null; + this.connectionPromise = null; + this.notifyError(error); + }); + } + + this.api = await ApiPromise.create({ + provider: this.wsProvider, + throwOnConnect: true // Make connection errors more explicit + }); + + await this.api.isReady; + return this.api; + } catch (error) { + // Clear connection state on error + this.api = null; + this.connectionPromise = null; + console.error('Failed to initialize Polkadot API:', error); + const apiError = error instanceof Error ? error : new Error('Failed to initialize API'); + this.notifyError(apiError); + throw apiError; + } + } + + async getApi(): Promise { + // If we already have a working API instance, return it + if (this.api?.isConnected) { + return this.api; + } + + // If we're in the process of connecting, wait for that to finish + if (this.connectionPromise) { + return this.connectionPromise; + } + + // Start a new connection + this.connectionPromise = this.initApi(); + return this.connectionPromise; + } + + async isReady(): Promise { + try { + const api = await this.getApi(); + return api.isConnected; + } catch { + return false; + } + } + + async disconnect(): Promise { + if (this.api) { + await this.api.disconnect(); + this.api = null; + } + if (this.wsProvider) { + await this.wsProvider.disconnect(); + this.wsProvider = null; + } + this.connectionPromise = null; + this.notifyDisconnect(); + } + + onReady(callback: () => void): void { + this.readyCallbacks.push(callback); + // If already ready, call immediately + if (this.api?.isConnected) { + callback(); + } + } + + onDisconnect(callback: () => void): void { + this.disconnectCallbacks.push(callback); + } + + onError(callback: (error: Error) => void): void { + this.errorCallbacks.push(callback); + } + + private notifyReady(): void { + this.readyCallbacks.forEach(callback => callback()); + } + + private notifyDisconnect(): void { + this.disconnectCallbacks.forEach(callback => callback()); + } + + private notifyError(error: Error): void { + this.errorCallbacks.forEach(callback => callback(error)); + } +}