Skip to content

Commit

Permalink
test and add disclaimer modal
Browse files Browse the repository at this point in the history
  • Loading branch information
hardingjam committed Feb 14, 2025
1 parent 671bc63 commit a1bdaa1
Show file tree
Hide file tree
Showing 5 changed files with 501 additions and 29 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
import { describe, it, expect, vi, beforeEach } from 'vitest';
import {
getDeploymentTransactionArgs,
AddOrderErrors
} from '../lib/components/deployment/getDeploymentTransactionArgs';
import { getAccount } from '@wagmi/core';
import type { Config } from '@wagmi/core';
import type { DotrainOrderGui, OrderIO } from '@rainlanguage/orderbook/js_api';

// Mock wagmi/core
vi.mock('@wagmi/core', () => ({
getAccount: vi.fn()
}));

describe('getDeploymentTransactionArgs', () => {
let mockGui: DotrainOrderGui;
let mockWagmiConfig: Config;
let mockTokenOutputs: OrderIO[];

beforeEach(() => {
vi.clearAllMocks();

// Mock GUI with successful responses
mockGui = {
generateApprovalCalldatas: vi.fn().mockResolvedValue([{ token: '0x123', amount: '1000' }]),
generateDepositAndAddOrderCalldatas: vi.fn().mockResolvedValue({
deposit: '0xdeposit',
addOrder: '0xaddOrder'
}),
getCurrentDeployment: vi.fn().mockReturnValue({
deployment: {
order: {
network: { 'chain-id': 1 },
orderbook: { address: '0xorderbook' }
}
}
}),
getTokenInfo: vi.fn().mockResolvedValue({
address: '0x123',
symbol: 'TEST'
})
} as unknown as DotrainOrderGui;

mockWagmiConfig = {} as Config;
(getAccount as any).mockReturnValue({ address: '0xuser' });

mockTokenOutputs = [{ token: { key: 'token1' } }] as OrderIO[];
});

describe('successful cases', () => {
it('should successfully return deployment transaction args', async () => {
const result = await getDeploymentTransactionArgs(mockGui, mockWagmiConfig, mockTokenOutputs);

expect(result).toEqual({
approvals: [{ token: '0x123', amount: '1000', symbol: 'TEST' }],
deploymentCalldata: {
deposit: '0xdeposit',
addOrder: '0xaddOrder'
},
orderbookAddress: '0xorderbook',
chainId: 1
});

expect(mockGui.generateApprovalCalldatas).toHaveBeenCalledWith('0xuser');
expect(mockGui.generateDepositAndAddOrderCalldatas).toHaveBeenCalled();
});
});

describe('input validation errors', () => {
it('should throw MISSING_GUI when GUI is null', async () => {
await expect(
getDeploymentTransactionArgs(null, mockWagmiConfig, mockTokenOutputs)
).rejects.toThrow(AddOrderErrors.MISSING_GUI);
});

it('should throw MISSING_CONFIG when wagmiConfig is undefined', async () => {
await expect(
getDeploymentTransactionArgs(mockGui, undefined, mockTokenOutputs)
).rejects.toThrow(AddOrderErrors.MISSING_CONFIG);
});

it('should throw NO_WALLET when wallet address is not found', async () => {
(getAccount as any).mockReturnValue({ address: null });
await expect(
getDeploymentTransactionArgs(mockGui, mockWagmiConfig, mockTokenOutputs)
).rejects.toThrow(AddOrderErrors.NO_WALLET);
});
});

describe('deployment errors', () => {
it('should throw INVALID_CHAIN_ID when chain ID is missing', async () => {
mockGui.getCurrentDeployment = vi.fn().mockReturnValue({
deployment: {
order: {
network: {},
orderbook: { address: '0xorderbook' }
}
}
});

await expect(
getDeploymentTransactionArgs(mockGui, mockWagmiConfig, mockTokenOutputs)
).rejects.toThrow(AddOrderErrors.INVALID_CHAIN_ID);
});

it('should throw MISSING_ORDERBOOK when orderbook address is missing', async () => {
mockGui.getCurrentDeployment = vi.fn().mockReturnValue({
deployment: {
order: {
network: { 'chain-id': 1 },
orderbook: {}
}
}
});

await expect(
getDeploymentTransactionArgs(mockGui, mockWagmiConfig, mockTokenOutputs)
).rejects.toThrow(AddOrderErrors.MISSING_ORDERBOOK);
});
});

describe('approval and calldata errors', () => {
it('should throw APPROVAL_FAILED when generateApprovalCalldatas fails', async () => {
mockGui.generateApprovalCalldatas = vi.fn().mockRejectedValue(new Error('Approval error'));

await expect(
getDeploymentTransactionArgs(mockGui, mockWagmiConfig, mockTokenOutputs)
).rejects.toThrow(`${AddOrderErrors.APPROVAL_FAILED}: Approval error`);
});

it('should throw DEPLOYMENT_FAILED when generateDepositAndAddOrderCalldatas fails', async () => {
mockGui.generateDepositAndAddOrderCalldatas = vi
.fn()
.mockRejectedValue(new Error('Deployment error'));

await expect(
getDeploymentTransactionArgs(mockGui, mockWagmiConfig, mockTokenOutputs)
).rejects.toThrow(`${AddOrderErrors.DEPLOYMENT_FAILED}: Deployment error`);
});
});

describe('token info errors', () => {
it('should throw TOKEN_INFO_FAILED when token key is missing', async () => {
const invalidTokenOutputs = [{ token: {} }] as OrderIO[];

await expect(
getDeploymentTransactionArgs(mockGui, mockWagmiConfig, invalidTokenOutputs)
).rejects.toThrow(`${AddOrderErrors.TOKEN_INFO_FAILED}: Token key is missing`);
});

it('should throw TOKEN_INFO_FAILED when getTokenInfo fails', async () => {
mockGui.getTokenInfo = vi.fn().mockRejectedValue(new Error('Token info error'));

await expect(
getDeploymentTransactionArgs(mockGui, mockWagmiConfig, mockTokenOutputs)
).rejects.toThrow(`${AddOrderErrors.TOKEN_INFO_FAILED}: Token info error`);
});

it('should throw TOKEN_INFO_FAILED when token info is not found for approval', async () => {
mockGui.getTokenInfo = vi.fn().mockResolvedValue({
address: '0x456', // Different address than the approval token
symbol: 'TEST'
});

await expect(
getDeploymentTransactionArgs(mockGui, mockWagmiConfig, mockTokenOutputs)
).rejects.toThrow(
`${AddOrderErrors.TOKEN_INFO_FAILED}: Token info not found for address: 0x123`
);
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,16 @@
} from '@rainlanguage/orderbook/js_api';
import { fade } from 'svelte/transition';
import { Button, Toggle } from 'flowbite-svelte';
import { getAccount, type Config } from '@wagmi/core';
import { type Config } from '@wagmi/core';
import { type Writable } from 'svelte/store';
import type { AppKit } from '@reown/appkit';
import type { Hex } from 'viem';
import { page } from '$app/stores';
import { onMount } from 'svelte';
import ShareChoicesButton from './ShareChoicesButton.svelte';
import { getDeploymentTransactionArgs } from './getDeploymentTransactionArgs';
import DisclaimerModal from './DisclaimerModal.svelte';
enum DeploymentStepErrors {
NO_GUI = 'Error loading GUI',
NO_STRATEGY = 'No valid strategy exists at this URL',
Expand Down Expand Up @@ -59,6 +62,7 @@
let gui: DotrainOrderGui | null = null;
let error: DeploymentStepErrors | null = null;
let errorDetails: string | null = null;
let showDisclaimerModal = false;
export let wagmiConfig: Writable<Config | undefined>;
export let wagmiConnected: Writable<boolean>;
Expand Down Expand Up @@ -155,34 +159,15 @@
}
}
async function handleAddOrder() {
try {
if (!gui || !$wagmiConfig) return;
const { address } = getAccount($wagmiConfig);
if (!address) return;
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
};
});
async function handleAddOrderClick() {
showDisclaimerModal = true;
}
handleDeployModal({
approvals,
deploymentCalldata,
orderbookAddress,
chainId
});
async function handleAcceptDisclaimer() {
try {
showDisclaimerModal = false;
const result = await getDeploymentTransactionArgs(gui, $wagmiConfig, allTokenOutputs);
handleDeployModal(result);
} catch (e) {
error = DeploymentStepErrors.ADD_ORDER_FAILED;
errorDetails = e instanceof Error ? e.message : 'Unknown error';
Expand Down Expand Up @@ -278,7 +263,7 @@

<div class="flex gap-2">
{#if $wagmiConnected}
<Button size="lg" on:click={handleAddOrder}>Deploy Strategy</Button>
<Button size="lg" on:click={handleAddOrderClick}>Deploy Strategy</Button>
<ComposedRainlangModal {gui} />
{:else}
<WalletConnect {appKitModal} connected={wagmiConnected} />
Expand All @@ -299,3 +284,13 @@
{/if}
{/if}
</div>

{#if showDisclaimerModal && gui && allTokenOutputs && wagmiConfig}
<DisclaimerModal
bind:open={showDisclaimerModal}
{gui}
{allTokenOutputs}
{wagmiConfig}
{handleDeployModal}
/>
{/if}
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
<script lang="ts">
import { Alert, Modal, Button } from 'flowbite-svelte';
import { ExclamationCircleSolid } from 'flowbite-svelte-icons';
import { getDeploymentTransactionArgs } from './getDeploymentTransactionArgs';
import type { Config } from 'wagmi';
import type { Writable } from 'svelte/store';
import type {
ApprovalCalldataResult,
DepositAndAddOrderCalldataResult,
DotrainOrderGui,
OrderIO
} from '@rainlanguage/orderbook/js_api';
import type { Hex } from 'viem';
import type { HandleAddOrderResult } from './getDeploymentTransactionArgs';
export let open: boolean;
export let gui: DotrainOrderGui;
export let allTokenOutputs: OrderIO[];
export let wagmiConfig: Writable<Config | undefined>;
export let handleDeployModal: (args: {
approvals: ApprovalCalldataResult;
deploymentCalldata: DepositAndAddOrderCalldataResult;
orderbookAddress: Hex;
chainId: number;
}) => void;
let result: HandleAddOrderResult | null = null;
let error: string | null = null;
let errorDetails: string | null = null;
let deployButtonText: 'Loading...' | 'Deploy' | 'Error' = 'Loading...';
const handleOpenModal = async () => {
try {
result = await getDeploymentTransactionArgs(gui, $wagmiConfig, allTokenOutputs);
deployButtonText = 'Deploy';
} catch (e) {
deployButtonText = 'Error';
error = 'Error getting deployment transaction data:';
errorDetails = e instanceof Error ? e.message : 'Unknown error';
}
};
$: if (open === true) {
handleOpenModal();
}
async function handleAcceptDisclaimer() {
if (!result) {
error = 'No result found';
return;
} else {
open = false;
handleDeployModal(result);
}
}
</script>

<Modal bind:open>
<div class="flex flex-col items-start gap-y-4">
<div class="space-y-4">
<Alert color="red" class="text-base">
<div class="flex items-center justify-center">
<ExclamationCircleSolid class="h-6 w-6 text-red-500" />
<span class="ml-2">
Before you deploy your strategy, make sure you understand the following...
</span>
</div>
</Alert>
<ul class="list-outside list-disc space-y-2 text-gray-700">
<li class="ml-4">
This front end is provided as a tool to interact with the Raindex smart contracts.
</li>
<li class="ml-4">
You are deploying your own strategy and depositing funds to an immutable smart contract
using your own wallet and private keys.
</li>
<li class="ml-4">
Nobody is custodying your funds, there is no recourse for recovery of funds if lost.
</li>
<li class="ml-4">There is no endorsement or guarantee provided with these strategies.</li>
<li class="ml-4">
Do not proceed if you do not understand the strategy you are deploying.
</li>
<li class="ml-4">Do not invest unless you are prepared to lose all funds.</li>
</ul>
</div>
<div class="flex gap-2">
<Button
size="lg"
class="w-32"
color="green"
disabled={!result}
on:click={handleAcceptDisclaimer}
>
{deployButtonText}
</Button>
<Button size="lg" class="w-32" color="red" on:click={() => (open = false)}>Cancel</Button>
</div>
<div class="flex flex-col">
{#if error}
<span class="ml-2 text-red-500">{error}</span>
{/if}
{#if errorDetails}
<span class="ml-2 text-red-500">{errorDetails}</span>
{/if}
</div>
</div>
</Modal>
Loading

0 comments on commit a1bdaa1

Please sign in to comment.