diff --git a/packages/ui-components/src/__tests__/DeploymentSteps.test.ts b/packages/ui-components/src/__tests__/DeploymentSteps.test.ts index 22ea3012e..dde93dbf2 100644 --- a/packages/ui-components/src/__tests__/DeploymentSteps.test.ts +++ b/packages/ui-components/src/__tests__/DeploymentSteps.test.ts @@ -1,12 +1,19 @@ import { describe, it, expect, vi, beforeEach, type Mock } from 'vitest'; -import { render, screen, fireEvent, waitFor } from '@testing-library/svelte'; +import { render, screen, waitFor } from '@testing-library/svelte'; import DeploymentSteps from '../lib/components/deployment/DeploymentSteps.svelte'; import { DotrainOrderGui } from '@rainlanguage/orderbook/js_api'; -import userEvent from '@testing-library/user-event'; + +import type { ComponentProps } from 'svelte'; +import { writable } from 'svelte/store'; +import type { AppKit } from '@reown/appkit'; +const { mockWagmiConfigStore, mockConnectedStore } = await vi.hoisted( + () => import('../lib/__mocks__/stores') +); + +export type DeploymentStepsProps = ComponentProps; vi.mock('@rainlanguage/orderbook/js_api', () => ({ DotrainOrderGui: { - getDeploymentKeys: vi.fn(), chooseDeployment: vi.fn() } })); @@ -566,115 +573,147 @@ describe('DeploymentSteps', () => { vi.clearAllMocks(); }); - it('renders strategy URL input and load button initially', () => { - render(DeploymentSteps); - expect(screen.getByPlaceholderText('Enter URL to .rain file')).toBeInTheDocument(); - const loadButton = screen.getByText('Load Strategy'); - expect(loadButton).toBeInTheDocument(); - expect(loadButton).toBeDisabled(); - }); + it('shows deployment details when provided', async () => { + (DotrainOrderGui.chooseDeployment as Mock).mockResolvedValue({ + getSelectTokens: () => [] + }); - it('enables load button when URL is entered', async () => { - render(DeploymentSteps); - const urlInput = screen.getByPlaceholderText('Enter URL to .rain file'); - const loadButton = screen.getByText('Load Strategy'); + const deploymentDetails = { + name: 'SFLR<>WFLR on Flare', + description: 'Rotate sFLR (Sceptre staked FLR) and WFLR on Flare.' + }; + + render(DeploymentSteps, { + props: { + dotrain, + deployment: 'flare-sflr-wflr', + deploymentDetails, + wagmiConfig: mockWagmiConfigStore, + wagmiConnected: mockConnectedStore, + appKitModal: writable({} as AppKit), + handleDeployModal: vi.fn() + } + }); - await userEvent.type(urlInput, 'https://example.com/strategy.rain'); await waitFor(() => { - expect(loadButton).not.toBeDisabled(); + expect(screen.getByText('SFLR<>WFLR on Flare')).toBeInTheDocument(); + expect( + screen.getByText('Rotate sFLR (Sceptre staked FLR) and WFLR on Flare.') + ).toBeInTheDocument(); }); }); - it('loads strategy from URL when button is clicked', async () => { - global.fetch = vi.fn().mockResolvedValue({ - ok: true, - text: () => Promise.resolve(JSON.stringify(dotrain)) + it('shows select tokens section when tokens need to be selected', async () => { + const mockSelectTokens = ['token1', 'token2']; + (DotrainOrderGui.chooseDeployment as Mock).mockResolvedValue({ + getSelectTokens: () => mockSelectTokens }); - render(DeploymentSteps); - const urlInput = screen.getByPlaceholderText('Enter URL to .rain file'); - const loadButton = screen.getByText('Load Strategy'); - await userEvent.clear(urlInput); - await fireEvent.input(urlInput, { target: { value: 'https://example.com/strategy.rain' } }); - await userEvent.click(loadButton); + render(DeploymentSteps, { + props: { + dotrain, + deployment: 'flare-sflr-wflr', + deploymentDetails: { name: 'Deployment 1', description: 'Description 1' }, + wagmiConfig: mockWagmiConfigStore, + wagmiConnected: mockConnectedStore, + appKitModal: writable({} as AppKit), + handleDeployModal: vi.fn() + } + }); await waitFor(() => { - expect(global.fetch).toHaveBeenCalledWith('https://example.com/strategy.rain'); + expect(screen.getByText('Select Tokens')).toBeInTheDocument(); + expect( + screen.getByText('Select the tokens that you want to use in your order.') + ).toBeInTheDocument(); }); }); - it('shows deployments dropdown after strategy is loaded', async () => { - global.fetch = vi.fn().mockResolvedValue({ - ok: true, - text: () => Promise.resolve(JSON.stringify(dotrain)) + it('shows error message when GUI initialization fails', async () => { + (DotrainOrderGui.chooseDeployment as Mock).mockRejectedValue( + new Error('Failed to initialize GUI') + ); + + render(DeploymentSteps, { + props: { + dotrain, + deployment: 'flare-sflr-wflr', + deploymentDetails: { name: 'Deployment 1', description: 'Description 1' }, + wagmiConfig: mockWagmiConfigStore, + wagmiConnected: mockConnectedStore, + appKitModal: writable({} as AppKit), + handleDeployModal: vi.fn() + } }); - const mockDeployments = [ - { key: 'deployment1', label: 'Deployment 1' }, - { key: 'deployment2', label: 'Deployment 2' } - ]; - - (DotrainOrderGui.getDeploymentKeys as Mock).mockResolvedValue(mockDeployments); - - render(DeploymentSteps); - const urlInput = screen.getByPlaceholderText('Enter URL to .rain file'); - const loadButton = screen.getByText('Load Strategy'); - - await userEvent.type(urlInput, 'https://example.com/strategy.rain'); - await userEvent.click(loadButton); - await waitFor(() => { - expect(screen.getByText('Select Deployment')).toBeInTheDocument(); - expect(screen.getByText('Select a deployment')).toBeInTheDocument(); + expect(screen.getByText('Error loading GUI')).toBeInTheDocument(); + expect(screen.getByText('Failed to initialize GUI')).toBeInTheDocument(); }); }); - it('handles URL fetch errors', async () => { - global.fetch = vi.fn().mockRejectedValue(new Error('Failed to fetch')); - - render(DeploymentSteps); - const urlInput = screen.getByPlaceholderText('Enter URL to .rain file'); - const loadButton = screen.getByText('Load Strategy'); + it('shows deploy strategy button when all required fields are filled', async () => { + mockConnectedStore.mockSetSubscribeValue(true); + (DotrainOrderGui.chooseDeployment as Mock).mockResolvedValue({ + getSelectTokens: () => [], + getCurrentDeployment: () => ({ + deployment: { + order: { + inputs: [], + outputs: [] + } + }, + deposits: [] + }), + getAllFieldDefinitions: () => [] + }); - await userEvent.type(urlInput, 'https://example.com/strategy.rain'); - await userEvent.click(loadButton); + render(DeploymentSteps, { + props: { + dotrain, + deployment: 'flare-sflr-wflr', + deploymentDetails: { name: 'Deployment 1', description: 'Description 1' }, + wagmiConfig: mockWagmiConfigStore, + wagmiConnected: mockConnectedStore, + appKitModal: writable({} as AppKit), + handleDeployModal: vi.fn() + } + }); await waitFor(() => { - expect(screen.getByText('No valid strategy exists at this URL')).toBeInTheDocument(); + expect(screen.getByText('Deploy Strategy')).toBeInTheDocument(); }); }); - - it('initializes GUI when deployment is selected', async () => { - global.fetch = vi.fn().mockResolvedValue({ - ok: true, - text: () => Promise.resolve(JSON.stringify(dotrain)) + it('shows connect wallet button when not connected', async () => { + mockConnectedStore.mockSetSubscribeValue(false); + (DotrainOrderGui.chooseDeployment as Mock).mockResolvedValue({ + getSelectTokens: () => [], + getCurrentDeployment: () => ({ + deployment: { + order: { + inputs: [], + outputs: [] + } + }, + deposits: [] + }), + getAllFieldDefinitions: () => [] }); - const mockDeployments: string[] = ['deployment1', 'deployment2']; - - (DotrainOrderGui.getDeploymentKeys as Mock).mockResolvedValue(mockDeployments); - - render(DeploymentSteps); - const urlInput = screen.getByPlaceholderText('Enter URL to .rain file'); - const loadButton = screen.getByText('Load Strategy'); - - await userEvent.type(urlInput, 'https://example.com/strategy.rain'); - await userEvent.click(loadButton); - - await waitFor(() => { - expect(screen.getByText('Select Deployment')).toBeInTheDocument(); - expect(screen.getByText('Select a deployment')).toBeInTheDocument(); + render(DeploymentSteps, { + props: { + dotrain, + deployment: 'flare-sflr-wflr', + deploymentDetails: { name: 'Deployment 1', description: 'Description 1' }, + wagmiConfig: mockWagmiConfigStore, + wagmiConnected: mockConnectedStore, + appKitModal: writable({} as AppKit), + handleDeployModal: vi.fn() + } }); - const dropdownButton = screen.getByTestId('dropdown-button'); - await userEvent.click(dropdownButton); - const dropdown = screen.getByTestId('dropdown'); - await userEvent.click(dropdown); - const deploymentOption = screen.getByText('deployment1'); - await userEvent.click(deploymentOption); - await waitFor(() => { - expect(DotrainOrderGui.chooseDeployment).toHaveBeenCalled(); + expect(screen.getByText('Connect Wallet')).toBeInTheDocument(); }); }); }); diff --git a/packages/ui-components/src/__tests__/DeploymentTile.test.ts b/packages/ui-components/src/__tests__/DeploymentTile.test.ts new file mode 100644 index 000000000..c871020ac --- /dev/null +++ b/packages/ui-components/src/__tests__/DeploymentTile.test.ts @@ -0,0 +1,34 @@ +import { render, screen } from '@testing-library/svelte'; +import { describe, it, expect, vi } from 'vitest'; +import { goto } from '$app/navigation'; +import DeploymentTile from '../lib/components/deployment/DeploymentTile.svelte'; + +// Mock the goto function +vi.mock('$app/navigation', () => ({ + goto: vi.fn() +})); + +describe('DeploymentTile', () => { + const mockProps = { + strategyName: 'test-strategy', + key: 'test-key', + name: 'Test Deployment', + description: 'This is a test deployment description' + }; + + it('renders the deployment name and description', () => { + render(DeploymentTile, mockProps); + + expect(screen.getByText('Test Deployment')).toBeInTheDocument(); + expect(screen.getByText('This is a test deployment description')).toBeInTheDocument(); + }); + + it('navigates to the correct URL when clicked', async () => { + const { getByRole } = render(DeploymentTile, mockProps); + + const button = getByRole('button'); + await button.click(); + + expect(goto).toHaveBeenCalledWith('/deploy/test-strategy/test-key'); + }); +}); diff --git a/packages/ui-components/src/__tests__/DeploymentsSection.test.ts b/packages/ui-components/src/__tests__/DeploymentsSection.test.ts new file mode 100644 index 000000000..e0b81e838 --- /dev/null +++ b/packages/ui-components/src/__tests__/DeploymentsSection.test.ts @@ -0,0 +1,71 @@ +import { render, screen } from '@testing-library/svelte'; +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import DeploymentsSection from '../lib/components/deployment/DeploymentsSection.svelte'; +import { DotrainOrderGui } from '@rainlanguage/orderbook/js_api'; + +// Mock the DotrainOrderGui +vi.mock('@rainlanguage/orderbook/js_api', () => ({ + DotrainOrderGui: { + getDeploymentDetails: vi.fn() + } +})); + +describe('DeploymentsSection', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it('should render deployments when data is available', async () => { + const mockDeployments = new Map([ + ['key1', { name: 'Deployment 1', description: 'Description 1' }], + ['key2', { name: 'Deployment 2', description: 'Description 2' }] + ]); + + vi.mocked(DotrainOrderGui.getDeploymentDetails).mockResolvedValue(mockDeployments); + + render(DeploymentsSection, { + props: { + dotrain: 'test-dotrain', + strategyName: 'Test Strategy' + } + }); + + // Wait for deployments to load + const deployment1 = await screen.findByText('Deployment 1'); + const deployment2 = await screen.findByText('Deployment 2'); + + expect(deployment1).toBeInTheDocument(); + expect(deployment2).toBeInTheDocument(); + }); + + it('should handle error when fetching deployments fails', async () => { + vi.mocked(DotrainOrderGui.getDeploymentDetails).mockRejectedValue(new Error('API Error')); + + render(DeploymentsSection, { + props: { + dotrain: 'test-dotrain', + strategyName: 'Test Strategy' + } + }); + + const errorMessage = await screen.findByText( + 'Error loading deployments: Error getting deployments.' + ); + expect(errorMessage).toBeInTheDocument(); + }); + + it('should fetch deployments when dotrain prop changes', async () => { + const { rerender } = render(DeploymentsSection, { + props: { + dotrain: '', + strategyName: 'Test Strategy' + } + }); + + expect(DotrainOrderGui.getDeploymentDetails).not.toHaveBeenCalled(); + + await rerender({ dotrain: 'new-dotrain', strategyName: 'Test Strategy' }); + + expect(DotrainOrderGui.getDeploymentDetails).toHaveBeenCalledWith('new-dotrain'); + }); +}); diff --git a/packages/ui-components/src/__tests__/StrategySection.test.ts b/packages/ui-components/src/__tests__/StrategySection.test.ts new file mode 100644 index 000000000..cf5476af0 --- /dev/null +++ b/packages/ui-components/src/__tests__/StrategySection.test.ts @@ -0,0 +1,94 @@ +import { render, screen, waitFor } from '@testing-library/svelte'; +import StrategySection from '../lib/components/deployment/StrategySection.svelte'; +import { DotrainOrderGui } from '@rainlanguage/orderbook/js_api'; +import { vi, describe, it, expect, beforeEach } from 'vitest'; + +// Mock fetch +const mockFetch = vi.fn(); +vi.stubGlobal('fetch', mockFetch); + +// Mock DotrainOrderGui +vi.mock('@rainlanguage/orderbook/js_api', () => ({ + DotrainOrderGui: { + getStrategyDetails: vi.fn(), + getDeploymentDetails: vi.fn() + } +})); + +describe('StrategySection', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it('renders strategy details successfully', async () => { + const mockDotrain = 'mock dotrain content'; + const mockStrategyDetails = { + name: 'Test Strategy', + description: 'Test Description' + }; + + // Mock fetch response + mockFetch.mockResolvedValueOnce({ + text: () => Promise.resolve(mockDotrain) + }); + + // Mock DotrainOrderGui methods + vi.mocked(DotrainOrderGui.getStrategyDetails).mockResolvedValueOnce(mockStrategyDetails); + + render(StrategySection, { + props: { + strategyUrl: 'http://example.com/strategy', + strategyName: 'TestStrategy' + } + }); + + await waitFor(() => { + expect(screen.getByText('Test Strategy')).toBeInTheDocument(); + expect(screen.getByText('Test Description')).toBeInTheDocument(); + }); + }); + + it('displays error message when strategy details fail', async () => { + const mockDotrain = 'mock dotrain content'; + const mockError = new Error('Failed to get strategy details'); + + // Mock fetch response + mockFetch.mockResolvedValueOnce({ + text: () => Promise.resolve(mockDotrain) + }); + + // Mock DotrainOrderGui methods + vi.mocked(DotrainOrderGui.getStrategyDetails).mockRejectedValueOnce(mockError); + + render(StrategySection, { + props: { + strategyUrl: 'http://example.com/strategy', + strategyName: 'TestStrategy' + } + }); + + await waitFor(() => { + expect(screen.getByText('Error getting strategy details')).toBeInTheDocument(); + expect(screen.getByText('Failed to get strategy details')).toBeInTheDocument(); + }); + }); + + it('handles fetch failure', async () => { + const mockError = new Error('Failed to fetch'); + + // Mock fetch to reject + mockFetch.mockRejectedValueOnce(mockError); + + render(StrategySection, { + props: { + strategyUrl: 'http://example.com/strategy', + strategyName: 'TestStrategy' + } + }); + + await waitFor(() => { + expect(screen.getByText('Error fetching strategy')).toBeInTheDocument(); + expect(screen.getByText('Failed to fetch')).toBeInTheDocument(); + }); + }); +}); diff --git a/packages/webapp/src/lib/__tests__/WalletConnect.test.ts b/packages/ui-components/src/__tests__/WalletConnect.test.ts similarity index 68% rename from packages/webapp/src/lib/__tests__/WalletConnect.test.ts rename to packages/ui-components/src/__tests__/WalletConnect.test.ts index 5466547cc..8e77f053a 100644 --- a/packages/webapp/src/lib/__tests__/WalletConnect.test.ts +++ b/packages/ui-components/src/__tests__/WalletConnect.test.ts @@ -1,16 +1,18 @@ import { render, screen } from '@testing-library/svelte'; -import WalletConnect from '../components/WalletConnect.svelte'; +import WalletConnect from '../lib/components/wallet/WalletConnect.svelte'; import { describe, it, vi, beforeEach, expect } from 'vitest'; +import { writable, type Writable } from 'svelte/store'; +import type { AppKit } from '@reown/appkit'; -const { mockSignerAddressStore, mockConnectedStore, mockAppKitModalStore } = await vi.hoisted( - () => import('../__mocks__/stores') +const { mockSignerAddressStore, mockConnectedStore } = await vi.hoisted( + () => import('$lib/__mocks__/stores') ); vi.mock('$lib/stores/wagmi', async (importOriginal) => { const original = (await importOriginal()) as object; return { ...original, - appKitModal: mockAppKitModalStore, + appKitModal: writable({} as AppKit), connected: mockConnectedStore }; }); @@ -35,7 +37,12 @@ describe('WalletConnect component', () => { mockSignerAddressStore.mockSetSubscribeValue('0x123'); mockConnectedStore.mockSetSubscribeValue(true); - render(WalletConnect); + render(WalletConnect, { + props: { + connected: mockConnectedStore as Writable, + appKitModal: writable({} as AppKit) + } + }); expect(screen.getByText('Connected')).toBeInTheDocument(); }); diff --git a/packages/ui-components/src/__tests__/transactionStore.test.ts b/packages/ui-components/src/__tests__/transactionStore.test.ts new file mode 100644 index 000000000..849424504 --- /dev/null +++ b/packages/ui-components/src/__tests__/transactionStore.test.ts @@ -0,0 +1,217 @@ +import { describe, it, expect, vi, beforeEach, afterAll, type Mock } from 'vitest'; +import { get } from 'svelte/store'; +import transactionStore, { + TransactionStatus, + TransactionErrorMessage +} from '../lib/stores/transactionStore'; +import { waitForTransactionReceipt, sendTransaction, switchChain, type Config } from '@wagmi/core'; + +vi.mock('@wagmi/core', () => ({ + waitForTransactionReceipt: vi.fn(), + sendTransaction: vi.fn(), + switchChain: vi.fn() +})); + +describe('transactionStore', () => { + const mockConfig = {} as Config; + const mockOrderbookAddress = '0xabcdef1234567890'; + + const { + reset, + checkingWalletAllowance, + handleDeploymentTransaction, + awaitWalletConfirmation, + awaitApprovalTx, + transactionSuccess, + transactionError + } = transactionStore; + + beforeEach(() => { + vi.resetAllMocks(); + reset(); + }); + + afterAll(() => { + vi.clearAllMocks(); + }); + + it('should initialize with the correct default state', () => { + expect(get(transactionStore)).toEqual({ + status: TransactionStatus.IDLE, + error: '', + hash: '', + data: null, + functionName: '', + message: '' + }); + }); + + it('should update status to CHECKING_ALLOWANCE', () => { + checkingWalletAllowance('Checking allowance...'); + expect(get(transactionStore).status).toBe(TransactionStatus.CHECKING_ALLOWANCE); + expect(get(transactionStore).message).toBe('Checking allowance...'); + }); + + it('should update status to PENDING_WALLET', () => { + awaitWalletConfirmation('Waiting for wallet...'); + expect(get(transactionStore).status).toBe(TransactionStatus.PENDING_WALLET); + expect(get(transactionStore).message).toBe('Waiting for wallet...'); + }); + + it('should update status to PENDING_APPROVAL', () => { + awaitApprovalTx('mockHash', 'TEST'); + expect(get(transactionStore).status).toBe(TransactionStatus.PENDING_APPROVAL); + expect(get(transactionStore).hash).toBe('mockHash'); + expect(get(transactionStore).message).toBe('Approving TEST spend...'); + }); + + it('should update status to SUCCESS', () => { + transactionSuccess('mockHash', 'Transaction successful'); + expect(get(transactionStore).status).toBe(TransactionStatus.SUCCESS); + expect(get(transactionStore).hash).toBe('mockHash'); + expect(get(transactionStore).message).toBe('Transaction successful'); + }); + + it('should update status to ERROR', () => { + transactionError(TransactionErrorMessage.DEPLOY_FAILED, 'mockHash'); + expect(get(transactionStore).status).toBe(TransactionStatus.ERROR); + expect(get(transactionStore).error).toBe(TransactionErrorMessage.DEPLOY_FAILED); + expect(get(transactionStore).hash).toBe('mockHash'); + }); + + it('should handle successful deployment transaction', async () => { + const mockApprovals = [ + { token: '0xtoken1', calldata: '0xapproval1' }, + { token: '0xtoken2', calldata: '0xapproval2' } + ]; + const mockDeploymentCalldata = '0xdeployment'; + + (sendTransaction as Mock).mockResolvedValueOnce('approvalHash1'); + (sendTransaction as Mock).mockResolvedValueOnce('approvalHash2'); + (sendTransaction as Mock).mockResolvedValueOnce('deployHash'); + (waitForTransactionReceipt as Mock).mockResolvedValue({}); + (switchChain as Mock).mockResolvedValue({}); + + await handleDeploymentTransaction({ + config: mockConfig, + approvals: mockApprovals, + deploymentCalldata: mockDeploymentCalldata, + orderbookAddress: mockOrderbookAddress as `0x${string}`, + chainId: 1 + }); + + expect(get(transactionStore).status).toBe(TransactionStatus.SUCCESS); + expect(get(transactionStore).hash).toBe('deployHash'); + }); + + it('should handle chain switch failure', async () => { + (switchChain as Mock).mockRejectedValue(new Error('Switch failed')); + + await handleDeploymentTransaction({ + config: mockConfig, + approvals: [], + deploymentCalldata: '0x', + orderbookAddress: mockOrderbookAddress as `0x${string}`, + chainId: 1 + }); + + expect(get(transactionStore).status).toBe(TransactionStatus.ERROR); + expect(get(transactionStore).error).toBe(TransactionErrorMessage.SWITCH_CHAIN_FAILED); + }); + + it('should handle user rejection of approval transaction', async () => { + const mockApprovals = [{ token: '0xtoken1', calldata: '0xapproval1' }]; + + (switchChain as Mock).mockResolvedValue({}); + (sendTransaction as Mock).mockRejectedValue(new Error('User rejected')); + + await handleDeploymentTransaction({ + config: mockConfig, + approvals: mockApprovals, + deploymentCalldata: '0x', + orderbookAddress: mockOrderbookAddress as `0x${string}`, + chainId: 1 + }); + + expect(get(transactionStore).status).toBe(TransactionStatus.ERROR); + expect(get(transactionStore).error).toBe(TransactionErrorMessage.USER_REJECTED_APPROVAL); + }); + + it('should handle approval transaction receipt failure', async () => { + const mockApprovals = [{ token: '0xtoken1', calldata: '0xapproval1' }]; + + (switchChain as Mock).mockResolvedValue({}); + (sendTransaction as Mock).mockResolvedValue('approvalHash'); + (waitForTransactionReceipt as Mock).mockRejectedValue(new Error('Receipt failed')); + + await handleDeploymentTransaction({ + config: mockConfig, + approvals: mockApprovals, + deploymentCalldata: '0x', + orderbookAddress: mockOrderbookAddress as `0x${string}`, + chainId: 1 + }); + + expect(get(transactionStore).status).toBe(TransactionStatus.ERROR); + expect(get(transactionStore).error).toBe(TransactionErrorMessage.APPROVAL_FAILED); + }); + + it('should handle user rejection of deployment transaction', async () => { + (switchChain as Mock).mockResolvedValue({}); + (sendTransaction as Mock).mockRejectedValue(new Error('User rejected')); + + await handleDeploymentTransaction({ + config: mockConfig, + approvals: [], + deploymentCalldata: '0x', + orderbookAddress: mockOrderbookAddress as `0x${string}`, + chainId: 1 + }); + + expect(get(transactionStore).status).toBe(TransactionStatus.ERROR); + expect(get(transactionStore).error).toBe(TransactionErrorMessage.USER_REJECTED_TRANSACTION); + }); + + it('should handle deployment transaction receipt failure', async () => { + (switchChain as Mock).mockResolvedValue({}); + (sendTransaction as Mock).mockResolvedValue('deployHash'); + (waitForTransactionReceipt as Mock).mockRejectedValue(new Error('Receipt failed')); + + await handleDeploymentTransaction({ + config: mockConfig, + approvals: [], + deploymentCalldata: '0x', + orderbookAddress: mockOrderbookAddress as `0x${string}`, + chainId: 1 + }); + + expect(get(transactionStore).status).toBe(TransactionStatus.ERROR); + expect(get(transactionStore).error).toBe(TransactionErrorMessage.DEPLOYMENT_FAILED); + }); + + it('should handle multiple approvals successfully', async () => { + const mockApprovals = [ + { token: '0xtoken1', calldata: '0xapproval1' }, + { token: '0xtoken2', calldata: '0xapproval2' } + ]; + + (switchChain as Mock).mockResolvedValue({}); + (sendTransaction as Mock) + .mockResolvedValueOnce('approvalHash1') + .mockResolvedValueOnce('approvalHash2') + .mockResolvedValueOnce('deployHash'); + (waitForTransactionReceipt as Mock).mockResolvedValue({}); + + await handleDeploymentTransaction({ + config: mockConfig, + approvals: mockApprovals, + deploymentCalldata: '0x', + orderbookAddress: mockOrderbookAddress as `0x${string}`, + chainId: 1 + }); + + expect(sendTransaction).toHaveBeenCalledTimes(3); // 2 approvals + 1 deployment + expect(get(transactionStore).status).toBe(TransactionStatus.SUCCESS); + expect(get(transactionStore).message).toBe('Strategy deployed successfully!'); + }); +}); diff --git a/packages/ui-components/src/lib/__mocks__/mockTransactionStore.ts b/packages/ui-components/src/lib/__mocks__/mockTransactionStore.ts new file mode 100644 index 000000000..ad9213895 --- /dev/null +++ b/packages/ui-components/src/lib/__mocks__/mockTransactionStore.ts @@ -0,0 +1,81 @@ +import { writable } from 'svelte/store'; +import { TransactionStatus, TransactionErrorMessage } from '../stores/transactionStore'; + +type MockTransactionStoreState = { + status: TransactionStatus; + error: string; + hash: string; + data: null; + functionName: string; + message: string; +}; + +const initialState: MockTransactionStoreState = { + status: TransactionStatus.IDLE, + error: '', + hash: '', + data: null, + functionName: '', + message: '' +}; + +const mockTransactionWritable = writable(initialState); + +export const mockTransactionStore = { + subscribe: mockTransactionWritable.subscribe, + set: mockTransactionWritable.set, + reset: () => mockTransactionWritable.set(initialState), + + handleDeploymentTransaction: async () => { + mockTransactionWritable.update((state) => ({ + ...state, + status: TransactionStatus.SUCCESS, + message: 'Strategy deployed successfully!', + hash: '0x123' + })); + }, + + checkingWalletAllowance: (message: string = '') => + mockTransactionWritable.update((state) => ({ + ...state, + status: TransactionStatus.CHECKING_ALLOWANCE, + message + })), + + awaitWalletConfirmation: (message: string = '') => + mockTransactionWritable.update((state) => ({ + ...state, + status: TransactionStatus.PENDING_WALLET, + message + })), + + awaitApprovalTx: (hash: string) => + mockTransactionWritable.update((state) => ({ + ...state, + hash, + status: TransactionStatus.PENDING_APPROVAL, + message: '' + })), + + transactionSuccess: (hash: string, message: string = '') => + mockTransactionWritable.update((state) => ({ + ...state, + status: TransactionStatus.SUCCESS, + hash, + message + })), + + transactionError: (error: TransactionErrorMessage, hash: string = '') => + mockTransactionWritable.update((state) => ({ + ...state, + status: TransactionStatus.ERROR, + error, + hash + })), + + mockSetSubscribeValue: (value: Partial) => + mockTransactionWritable.update((state) => ({ + ...state, + ...value + })) +}; diff --git a/packages/ui-components/src/lib/__mocks__/stores.ts b/packages/ui-components/src/lib/__mocks__/stores.ts index fa1b9b39c..dd4553101 100644 --- a/packages/ui-components/src/lib/__mocks__/stores.ts +++ b/packages/ui-components/src/lib/__mocks__/stores.ts @@ -112,11 +112,13 @@ export const mockChainIdStore = { export const mockConnectedStore = { subscribe: mockConnectedWritable.subscribe, set: mockConnectedWritable.set, + update: mockConnectedWritable.update, mockSetSubscribeValue: (value: boolean): void => mockConnectedWritable.set(value) }; export const mockWagmiConfigStore = { subscribe: mockWagmiConfigWritable.subscribe, set: mockWagmiConfigWritable.set, + update: mockWagmiConfigWritable.update, mockSetSubscribeValue: (value: Config): void => mockWagmiConfigWritable.set(value) }; diff --git a/packages/ui-components/src/lib/components/deployment/DeploymentPage.svelte b/packages/ui-components/src/lib/components/deployment/DeploymentPage.svelte new file mode 100644 index 000000000..0018c3c21 --- /dev/null +++ b/packages/ui-components/src/lib/components/deployment/DeploymentPage.svelte @@ -0,0 +1,35 @@ + + + diff --git a/packages/ui-components/src/lib/components/deployment/DeploymentSectionHeader.svelte b/packages/ui-components/src/lib/components/deployment/DeploymentSectionHeader.svelte index b94a6e94d..dbe0866e3 100644 --- a/packages/ui-components/src/lib/components/deployment/DeploymentSectionHeader.svelte +++ b/packages/ui-components/src/lib/components/deployment/DeploymentSectionHeader.svelte @@ -4,8 +4,8 @@
-

{title}

-

+

{title}

+

{description}

diff --git a/packages/ui-components/src/lib/components/deployment/DeploymentSteps.svelte b/packages/ui-components/src/lib/components/deployment/DeploymentSteps.svelte index a30638766..22456274d 100644 --- a/packages/ui-components/src/lib/components/deployment/DeploymentSteps.svelte +++ b/packages/ui-components/src/lib/components/deployment/DeploymentSteps.svelte @@ -3,24 +3,24 @@ import DepositInput from './DepositInput.svelte'; import SelectToken from './SelectToken.svelte'; import TokenInputOrOutput from './TokenInputOrOutput.svelte'; - import DropdownRadio from '../dropdown/DropdownRadio.svelte'; import DeploymentSectionHeader from './DeploymentSectionHeader.svelte'; + import WalletConnect from '../wallet/WalletConnect.svelte'; import { DotrainOrderGui, - type DeploymentKeys, - type ApprovalCalldataResult, - type DepositAndAddOrderCalldataResult, type GuiDeposit, type GuiFieldDefinition, type NameAndDescription, type GuiDeployment, - type OrderIO + type OrderIO, + type ApprovalCalldataResult, + type DepositAndAddOrderCalldataResult } from '@rainlanguage/orderbook/js_api'; - import { Button, Input, Spinner } from 'flowbite-svelte'; - import { type Chain } from 'viem'; - import { base, flare, arbitrum, polygon, bsc, mainnet, linea } from 'viem/chains'; - import { getAccount, sendTransaction, type Config } from '@wagmi/core'; + import { fade } from 'svelte/transition'; + import { Button } from 'flowbite-svelte'; + import { getAccount, type Config } from '@wagmi/core'; import { type Writable } from 'svelte/store'; + import type { AppKit } from '@reown/appkit'; + import type { Hex } from 'viem'; enum DeploymentStepErrors { NO_GUI = 'Error loading GUI', @@ -36,117 +36,57 @@ ADD_ORDER_FAILED = 'Failed to add order' } - const chains: Record = { - [base.id]: base, - [flare.id]: flare, - [arbitrum.id]: arbitrum, - [polygon.id]: polygon, - [bsc.id]: bsc, - [mainnet.id]: mainnet, - [linea.id]: linea - }; + export let dotrain: string; + export let deployment: string; + export let deploymentDetails: NameAndDescription; + export let handleDeployModal: (args: { + approvals: ApprovalCalldataResult; + deploymentCalldata: DepositAndAddOrderCalldataResult; + orderbookAddress: Hex; + chainId: number; + }) => void; - let dotrain = ''; - let isLoading = false; let error: DeploymentStepErrors | null = null; let errorDetails: string | null = null; - let strategyUrl = ''; let selectTokens: string[] | null = null; let allDepositFields: GuiDeposit[] = []; let allTokenOutputs: OrderIO[] = []; let allFieldDefinitions: GuiFieldDefinition[] = []; let allTokensSelected: boolean = false; - let guiDetails: NameAndDescription; let inputVaultIds: string[] = []; let outputVaultIds: string[] = []; - let addOrderError: string | null = null; - let addOrderErrorDetails: string | null = null; - - export let wagmiConfig: Writable | null = null; - export let wagmiConnected: Writable | null = null; - - async function loadStrategyFromUrl() { - isLoading = true; - error = null; - errorDetails = null; - - try { - const response = await fetch(strategyUrl); - if (!response.ok) { - error = DeploymentStepErrors.NO_STRATEGY; - errorDetails = `HTTP error - status: ${response.status}`; - } - dotrain = await response.text(); - } catch (e) { - error = DeploymentStepErrors.NO_STRATEGY; - errorDetails = e instanceof Error ? e.message : 'Unknown error'; - } finally { - isLoading = false; - } - } let gui: DotrainOrderGui | null = null; - let availableDeployments: Record = {}; + let addOrderError: DeploymentStepErrors | null = null; + let addOrderErrorDetails: string | null = null; + export let wagmiConfig: Writable; + export let wagmiConnected: Writable; + export let appKitModal: Writable; - async function initialize() { - try { - let deployments: DeploymentKeys = await DotrainOrderGui.getDeploymentKeys(dotrain); - availableDeployments = Object.fromEntries( - deployments.map((deployment) => [ - deployment, - { - label: deployment - } - ]) - ); - } catch (e: unknown) { - error = DeploymentStepErrors.NO_GUI; - errorDetails = e instanceof Error ? e.message : 'Unknown error'; - } + $: if (deployment) { + handleDeploymentChange(deployment); } - $: if (dotrain) { - isLoading = true; + async function handleDeploymentChange(deployment: string) { + if (!deployment || !dotrain) return; error = null; errorDetails = null; - gui = null; - initialize(); - isLoading = false; - } - - let selectedDeployment: string | undefined = undefined; - async function handleDeploymentChange(deployment: string) { - isLoading = true; - gui = null; - if (!deployment) return; try { gui = await DotrainOrderGui.chooseDeployment(dotrain, deployment); - try { - selectTokens = gui.getSelectTokens(); - getGuiDetails(); - } catch (e) { - error = DeploymentStepErrors.NO_SELECT_TOKENS; - errorDetails = e instanceof Error ? e.message : 'Unknown error'; + + if (gui) { + try { + selectTokens = await gui.getSelectTokens(); + return selectTokens; + } catch (e) { + error = DeploymentStepErrors.NO_SELECT_TOKENS; + return (errorDetails = e instanceof Error ? e.message : 'Unknown error'); + } } } catch (e) { error = DeploymentStepErrors.NO_GUI; - errorDetails = e instanceof Error ? e.message : 'Unknown error'; - } - isLoading = false; - } - - $: if (selectedDeployment) { - handleDeploymentChange(selectedDeployment as string); - } - - async function getGuiDetails() { - if (!gui) return; - try { - guiDetails = await DotrainOrderGui.getStrategyDetails(dotrain); - } catch (e) { - error = DeploymentStepErrors.NO_GUI_DETAILS; - errorDetails = e instanceof Error ? e.message : 'Unknown error'; + return (errorDetails = e instanceof Error ? e.message : 'Unknown error'); } } @@ -196,41 +136,51 @@ } $: if (selectTokens?.length === 0 || allTokensSelected) { - error = null; - initializeVaultIdArrays(); - getAllDepositFields(); - getAllFieldDefinitions(); - getAllTokenInputs(); - getAllTokenOutputs(); + updateFields(); } - export function getChainById(chainId: number): Chain { - const chain = chains[chainId]; - if (!chain) { - error = DeploymentStepErrors.NO_CHAIN; - errorDetails = `Unsupported chain ID: ${chainId}`; + async function updateFields() { + try { + error = null; + errorDetails = null; + initializeVaultIdArrays(); + getAllDepositFields(); + getAllFieldDefinitions(); + getAllTokenInputs(); + getAllTokenOutputs(); + } catch (e) { + error = DeploymentStepErrors.NO_GUI; + errorDetails = e instanceof Error ? e.message : 'Unknown error'; } - return chain; } - async function handleAddOrderWagmi() { + async function handleAddOrder() { try { if (!gui || !$wagmiConfig) return; const { address } = getAccount($wagmiConfig); if (!address) return; - const approvals: ApprovalCalldataResult = await gui.generateApprovalCalldatas(address); - for (const approval of approvals) { - await sendTransaction($wagmiConfig, { - to: approval.token as `0x${string}`, - data: approval.calldata as `0x${string}` - }); - } - const calldata: DepositAndAddOrderCalldataResult = - await gui.generateDepositAndAddOrderCalldatas(); - await sendTransaction($wagmiConfig, { - // @ts-expect-error orderbook is not typed - to: gui.getCurrentDeployment().deployment.order.orderbook.address as `0x${string}`, - data: calldata as `0x${string}` + let approvals = await gui.generateApprovalCalldatas(address); + const deploymentCalldata = await gui.generateDepositAndAddOrderCalldatas(); + const chainId = gui.getCurrentDeployment().deployment.order.network['chain-id'] as number; + // @ts-expect-error orderbook is not typed + const orderbookAddress = gui.getCurrentDeployment().deployment.order.orderbook.address; + const outputTokenInfos = await Promise.all( + allTokenOutputs.map((token) => gui?.getTokenInfo(token.token?.key as string)) + ); + + approvals = approvals.map((approval) => { + const token = outputTokenInfos.find((token) => token?.address === approval.token); + return { + ...approval, + symbol: token?.symbol + }; + }); + + handleDeployModal({ + approvals, + deploymentCalldata, + orderbookAddress, + chainId }); } catch (e) { addOrderError = DeploymentStepErrors.ADD_ORDER_FAILED; @@ -246,140 +196,105 @@ } -
-
-
- -
- -
-
- -{#if error} -

{error}

-{/if} -{#if errorDetails} -

{errorDetails}

-{/if} -{#if dotrain} - - - - {#if selectedRef === undefined} - Select a deployment - {:else if selectedOption?.label} - {selectedOption.label} - {:else} - {selectedRef} - {/if} - - - -
-
{option.label ? option.label : ref}
-
-
-
- - {#if isLoading} - +
+ {#if error} +

{error}

{/if} - {#if gui} -
- {#if guiDetails} -
-

- {guiDetails.name} -

-

- {guiDetails.description} -

-
- {/if} - - {#if selectTokens && selectTokens.length > 0} -
- -
- {#each selectTokens as tokenKey} - - {/each} -
-
- {/if} - - {#if allTokensSelected || selectTokens?.length === 0} - {#if allFieldDefinitions.length > 0} -
- {#each allFieldDefinitions as fieldDefinition} - - {/each} + {#if errorDetails} +

{errorDetails}

+ {/if} + {#if dotrain} + {#if gui} +
+ {#if deploymentDetails} +
+

+ {deploymentDetails.name} +

+

+ {deploymentDetails.description} +

{/if} - {#if allDepositFields.length > 0} -
- {#each allDepositFields as deposit} - - {/each} -
- {/if} - {#if allTokenInputs.length > 0 && allTokenOutputs.length > 0} + {#if selectTokens && selectTokens.length > 0}
- {#if allTokenInputs.length > 0} - {#each allTokenInputs as input, i} - - {/each} - {/if} - - {#if allTokenOutputs.length > 0} - {#each allTokenOutputs as output, i} - +
+ {#each selectTokens as tokenKey} + {/each} - {/if} +
{/if} -
- {#if $wagmiConnected} - - {:else} - + + {#if allTokensSelected || selectTokens?.length === 0} + {#if allFieldDefinitions.length > 0} +
+ {#each allFieldDefinitions as fieldDefinition} + + {/each} +
{/if} -
- {#if addOrderError} -

{addOrderError}

- {/if} - {#if addOrderErrorDetails} -

{addOrderErrorDetails}

+ + {#if allDepositFields.length > 0} +
+ {#each allDepositFields as deposit} + + {/each} +
+ {/if} + {#if allTokenInputs.length > 0 && allTokenOutputs.length > 0} +
+ + {#if allTokenInputs.length > 0} + {#each allTokenInputs as input, i} + + {/each} + {/if} + + {#if allTokenOutputs.length > 0} + {#each allTokenOutputs as output, i} + + {/each} + {/if} +
+ {/if} +
+ {#if $wagmiConnected} + + {:else} + {/if} +
+ {#if addOrderError} +

{addOrderError}

+ {/if} + {#if addOrderErrorDetails} +

{addOrderErrorDetails}

+ {/if} +
-
- {/if} -
+ {/if} +
+ {/if} {/if} -{/if} +
diff --git a/packages/ui-components/src/lib/components/deployment/DeploymentTile.svelte b/packages/ui-components/src/lib/components/deployment/DeploymentTile.svelte new file mode 100644 index 000000000..cbb0f32fc --- /dev/null +++ b/packages/ui-components/src/lib/components/deployment/DeploymentTile.svelte @@ -0,0 +1,15 @@ + + + diff --git a/packages/ui-components/src/lib/components/deployment/DeploymentsSection.svelte b/packages/ui-components/src/lib/components/deployment/DeploymentsSection.svelte new file mode 100644 index 000000000..25cf1cea3 --- /dev/null +++ b/packages/ui-components/src/lib/components/deployment/DeploymentsSection.svelte @@ -0,0 +1,41 @@ + + +{#if deployments.length > 0} +
+
+ {#each deployments as { key, name, description }} + + {/each} +
+
+{:else if error} +

Error loading deployments: {error}

+{:else} +

{errorDetails}

+{/if} diff --git a/packages/ui-components/src/lib/components/deployment/StrategySection.svelte b/packages/ui-components/src/lib/components/deployment/StrategySection.svelte new file mode 100644 index 000000000..c1759bfe5 --- /dev/null +++ b/packages/ui-components/src/lib/components/deployment/StrategySection.svelte @@ -0,0 +1,52 @@ + + +{#if dotrain && strategyDetails} +
+
+
+

+ {strategyDetails.name} +

+

+ {strategyDetails.description} +

+
+ +
+
+{:else if error} +
+

{error}

+

{errorDetails}

+
+{/if} diff --git a/packages/ui-components/src/lib/components/deployment/TokenInputOrOutput.svelte b/packages/ui-components/src/lib/components/deployment/TokenInputOrOutput.svelte index 1b14acfb8..8a2d1faad 100644 --- a/packages/ui-components/src/lib/components/deployment/TokenInputOrOutput.svelte +++ b/packages/ui-components/src/lib/components/deployment/TokenInputOrOutput.svelte @@ -1,7 +1,6 @@ + + diff --git a/packages/webapp/src/lib/components/Sidebar.svelte b/packages/webapp/src/lib/components/Sidebar.svelte index c436c9ad4..732c04bfb 100644 --- a/packages/webapp/src/lib/components/Sidebar.svelte +++ b/packages/webapp/src/lib/components/Sidebar.svelte @@ -20,13 +20,15 @@ IconTelegram, IconExternalLink, logoDark, - logoLight + logoLight, + WalletConnect } from '@rainlanguage/ui-components'; - import WalletConnect from './WalletConnect.svelte'; import { onMount } from 'svelte'; + import { connected, appKitModal } from '$lib/stores/wagmi'; export let colorTheme; export let page; + let sideBarHidden: boolean = false; let breakPoint: number = 1024; let width: number; @@ -102,7 +104,7 @@ - + + import { Modal, Spinner, Button } from 'flowbite-svelte'; + import { TransactionStatus } from '@rainlanguage/ui-components'; + import { transactionStore } from '@rainlanguage/ui-components'; + + export let open: boolean; + export let messages: { + success: string; + error: string; + pending: string; + }; + + function handleClose() { + open = false; + } + + $: if (!open) { + transactionStore.reset(); + } + + + + {#if $transactionStore.status !== TransactionStatus.IDLE} +
+ {#if $transactionStore.status === TransactionStatus.ERROR} +
+

+
+

+ {messages.error} +

+

+ {$transactionStore.error} +

+ + {:else if $transactionStore.status === TransactionStatus.SUCCESS} +
+

+
+
+

+ {messages.success} +

+ {#if $transactionStore.message} +

+ {$transactionStore.message} +

+ {/if} +
+ + {:else} +
+ +
+

+ {messages.pending} +

+

+ {$transactionStore.message} +

+ {/if} +
+ {/if} +
diff --git a/packages/webapp/src/lib/services/modal.ts b/packages/webapp/src/lib/services/modal.ts new file mode 100644 index 000000000..997ff73fb --- /dev/null +++ b/packages/webapp/src/lib/services/modal.ts @@ -0,0 +1,17 @@ +import DeployModal from '$lib/components/DeployModal.svelte'; +import type { + ApprovalCalldataResult, + DepositAndAddOrderCalldataResult +} from '@rainlanguage/orderbook/js_api'; +import type { Hex } from 'viem'; + +export type DeployModalProps = { + approvals: ApprovalCalldataResult; + deploymentCalldata: DepositAndAddOrderCalldataResult; + orderbookAddress: Hex; + chainId: number; +}; + +export const handleDeployModal = (args: DeployModalProps) => { + new DeployModal({ target: document.body, props: { open: true, ...args } }); +}; diff --git a/packages/webapp/src/routes/+layout.ts b/packages/webapp/src/routes/+layout.ts index 43a317565..6f1bb44bc 100644 --- a/packages/webapp/src/routes/+layout.ts +++ b/packages/webapp/src/routes/+layout.ts @@ -13,7 +13,7 @@ export interface LayoutData { stores: AppStoresInterface; } -export const load = async () => { +export const load = async ({ fetch }) => { const response = await fetch( 'https://raw.githubusercontent.com/rainlanguage/rain.strategies/refs/heads/main/settings.json' ); diff --git a/packages/webapp/src/routes/deploy/+layout.svelte b/packages/webapp/src/routes/deploy/+layout.svelte new file mode 100644 index 000000000..67a5aaa59 --- /dev/null +++ b/packages/webapp/src/routes/deploy/+layout.svelte @@ -0,0 +1,7 @@ + + + + diff --git a/packages/webapp/src/routes/deploy/+layout.ts b/packages/webapp/src/routes/deploy/+layout.ts new file mode 100644 index 000000000..1eaecf512 --- /dev/null +++ b/packages/webapp/src/routes/deploy/+layout.ts @@ -0,0 +1,26 @@ +import type { LayoutLoad } from './$types'; + +export const load: LayoutLoad = async ({ fetch }) => { + try { + const response = await fetch( + 'https://raw.githubusercontent.com/rainlanguage/rain.strategies/refs/heads/main/strategies/dev/registry' + ); + const files = await response.text(); + + const _files = files + .split('\n') + .filter((line: string) => line.trim()) + .map((line: string) => { + const [name, url] = line.split(' '); + return { name, url }; + }); + + return { + files: _files + }; + } catch { + return { + files: [] + }; + } +}; diff --git a/packages/webapp/src/routes/deploy/+page.svelte b/packages/webapp/src/routes/deploy/+page.svelte index 6dddc7b03..dcaf8debb 100644 --- a/packages/webapp/src/routes/deploy/+page.svelte +++ b/packages/webapp/src/routes/deploy/+page.svelte @@ -1,11 +1,65 @@ -
- - - +
+
+ {#if advancedMode} + + + {/if} + (advancedMode = !advancedMode)}> + {'Advanced Mode'} + +
+ + {#if loading} + + {:else if error} +

{error}

+

{errorDetails}

+ {:else if _files.length > 0} +
+ {#each _files as { name, url }} + + {/each} +
+ {/if}
diff --git a/packages/webapp/src/routes/deploy/[strategyName]/+page.ts b/packages/webapp/src/routes/deploy/[strategyName]/+page.ts new file mode 100644 index 000000000..f2323f5fd --- /dev/null +++ b/packages/webapp/src/routes/deploy/[strategyName]/+page.ts @@ -0,0 +1,5 @@ +import { redirect } from '@sveltejs/kit'; + +export const load = () => { + throw redirect(307, '/deploy'); +}; diff --git a/packages/webapp/src/routes/deploy/[strategyName]/[deploymentKey]/+page.svelte b/packages/webapp/src/routes/deploy/[strategyName]/[deploymentKey]/+page.svelte new file mode 100644 index 000000000..588138fda --- /dev/null +++ b/packages/webapp/src/routes/deploy/[strategyName]/[deploymentKey]/+page.svelte @@ -0,0 +1,29 @@ + + +{#if !dotrain || !key} +
Deployment not found. Redirecting to deployments page...
+{:else} + +{/if} diff --git a/packages/webapp/src/routes/deploy/[strategyName]/[deploymentKey]/+page.ts b/packages/webapp/src/routes/deploy/[strategyName]/[deploymentKey]/+page.ts new file mode 100644 index 000000000..6793faf70 --- /dev/null +++ b/packages/webapp/src/routes/deploy/[strategyName]/[deploymentKey]/+page.ts @@ -0,0 +1,68 @@ +import { DotrainOrderGui } from '@rainlanguage/orderbook/js_api'; + +export const load = async ({ + fetch, + params +}: { + fetch: typeof globalThis.fetch; + params: { strategyName: string; deploymentKey: string }; +}) => { + try { + const response = await fetch( + 'https://raw.githubusercontent.com/rainlanguage/rain.strategies/refs/heads/main/strategies/dev/registry' + ); + const files = await response.text(); + const { strategyName, deploymentKey } = params; + + const fileList = files + .split('\n') + .filter(Boolean) + .map((line: string) => { + const [name, url] = line.split(' '); + return { name, url }; + }); + + const strategy = fileList.find((file: { name: string }) => file.name === strategyName); + + if (strategy) { + const dotrainResponse = await fetch(strategy.url); + const dotrain = await dotrainResponse.text(); + + const deploymentWithDetails = await DotrainOrderGui.getDeploymentDetails(dotrain); + + const deployments = Array.from(deploymentWithDetails, ([key, details]) => ({ + key, + ...details + })); + const deployment = deployments.find( + (deployment: { key: string }) => deployment.key === deploymentKey + ); + + if (!deployment) { + throw new Error(`Deployment ${deploymentKey} not found`); + } + + const { key, name, description } = deployment; + + return { + dotrain, + strategyName, + key, + name, + description + }; + } else { + return { + dotrain: null, + strategyName: null, + deploymentKey: null + }; + } + } catch { + return { + dotrain: null, + strategyName: null, + deploymentKey: null + }; + } +};