diff --git a/crates/common/src/dotrain_order/calldata.rs b/crates/common/src/dotrain_order/calldata.rs index 72ad88686..ed48a0c05 100644 --- a/crates/common/src/dotrain_order/calldata.rs +++ b/crates/common/src/dotrain_order/calldata.rs @@ -15,9 +15,9 @@ use std::{collections::HashMap, str::FromStr, sync::Arc}; #[cfg_attr(target_family = "wasm", derive(Tsify))] pub struct ApprovalCalldata { #[cfg_attr(target_family = "wasm", tsify(type = "string"))] - token: Address, + pub token: Address, #[cfg_attr(target_family = "wasm", tsify(type = "string"))] - calldata: Bytes, + pub calldata: Bytes, } #[cfg(target_family = "wasm")] impl_all_wasm_traits!(ApprovalCalldata); diff --git a/crates/js_api/src/gui/order_operations.rs b/crates/js_api/src/gui/order_operations.rs index 4b6438234..b92344932 100644 --- a/crates/js_api/src/gui/order_operations.rs +++ b/crates/js_api/src/gui/order_operations.rs @@ -9,6 +9,14 @@ use rain_orderbook_bindings::OrderBook::multicallCall; use rain_orderbook_common::{deposit::DepositArgs, dotrain_order, transaction::TransactionArgs}; use std::{collections::HashMap, str::FromStr, sync::Arc}; +pub enum CalldataFunction { + Allowance, + Approval, + Deposit, + AddOrder, + DepositAndAddOrder, +} + #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Tsify)] pub struct TokenAllowance { @@ -53,6 +61,24 @@ impl_all_wasm_traits!(DepositAndAddOrderCalldataResult); pub struct IOVaultIds(HashMap>>); impl_all_wasm_traits!(IOVaultIds); +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Tsify)] +pub struct ExtendedApprovalCalldata { + pub token: Address, + pub calldata: Bytes, + pub symbol: String, +} +impl_all_wasm_traits!(ExtendedApprovalCalldata); + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Tsify)] +#[serde(rename_all = "camelCase")] +pub struct DeploymentTransactionArgs { + approvals: Vec, + deployment_calldata: Bytes, + orderbook_address: Address, + chain_id: u64, +} +impl_all_wasm_traits!(DeploymentTransactionArgs); + #[wasm_bindgen] impl DotrainOrderGui { fn get_orderbook(&self) -> Result, GuiError> { @@ -129,13 +155,32 @@ impl DotrainOrderGui { }) } + fn prepare_calldata_generation( + &mut self, + calldata_function: CalldataFunction, + ) -> Result { + let deployment = self.get_current_deployment()?; + self.check_select_tokens()?; + match calldata_function { + CalldataFunction::Deposit => { + self.populate_vault_ids(&deployment)?; + } + CalldataFunction::AddOrder | CalldataFunction::DepositAndAddOrder => { + self.check_field_values()?; + self.populate_vault_ids(&deployment)?; + self.update_bindings(&deployment)?; + } + _ => {} + } + Ok(self.get_current_deployment()?) + } + /// Check allowances for all inputs and outputs of the order /// /// Returns a vector of [`TokenAllowance`] objects #[wasm_bindgen(js_name = "checkAllowances")] - pub async fn check_allowances(&self, owner: String) -> Result { - let deployment = self.get_current_deployment()?; - self.check_select_tokens()?; + pub async fn check_allowances(&mut self, owner: String) -> Result { + let deployment = self.prepare_calldata_generation(CalldataFunction::Allowance)?; let orderbook = self.get_orderbook()?; let vaults_and_deposits = self.get_vaults_and_deposits(&deployment).await?; @@ -166,11 +211,10 @@ impl DotrainOrderGui { /// Returns a vector of [`ApprovalCalldata`] objects #[wasm_bindgen(js_name = "generateApprovalCalldatas")] pub async fn generate_approval_calldatas( - &self, + &mut self, owner: String, ) -> Result { - let deployment = self.get_current_deployment()?; - self.check_select_tokens()?; + let deployment = self.prepare_calldata_generation(CalldataFunction::Approval)?; let deposits_map = self.get_deposits_as_map().await?; if deposits_map.is_empty() { @@ -210,10 +254,7 @@ impl DotrainOrderGui { /// Returns a vector of bytes #[wasm_bindgen(js_name = "generateDepositCalldatas")] pub async fn generate_deposit_calldatas(&mut self) -> Result { - let deployment = self.get_current_deployment()?; - self.check_select_tokens()?; - self.populate_vault_ids(&deployment)?; - let deployment = self.get_current_deployment()?; + let deployment = self.prepare_calldata_generation(CalldataFunction::Deposit)?; let token_deposits = self .get_vaults_and_deposits(&deployment) @@ -251,13 +292,7 @@ impl DotrainOrderGui { pub async fn generate_add_order_calldata( &mut self, ) -> Result { - let deployment = self.get_current_deployment()?; - self.check_select_tokens()?; - self.check_field_values()?; - self.populate_vault_ids(&deployment)?; - self.update_bindings(&deployment)?; - let deployment = self.get_current_deployment()?; - + let deployment = self.prepare_calldata_generation(CalldataFunction::AddOrder)?; let calldata = self .dotrain_order .generate_add_order_calldata(&deployment.key) @@ -269,12 +304,7 @@ impl DotrainOrderGui { pub async fn generate_deposit_and_add_order_calldatas( &mut self, ) -> Result { - let deployment = self.get_current_deployment()?; - self.check_select_tokens()?; - self.check_field_values()?; - self.populate_vault_ids(&deployment)?; - self.update_bindings(&deployment)?; - let deployment = self.get_current_deployment()?; + let deployment = self.prepare_calldata_generation(CalldataFunction::DepositAndAddOrder)?; let mut calls = Vec::new(); @@ -356,4 +386,56 @@ impl DotrainOrderGui { self.update_bindings(&deployment)?; Ok(()) } + + #[wasm_bindgen(js_name = "getDeploymentTransactionArgs")] + pub async fn get_deployment_transaction_args( + &mut self, + owner: String, + ) -> Result { + let deployment = self.prepare_calldata_generation(CalldataFunction::DepositAndAddOrder)?; + + let mut approvals = Vec::new(); + let approval_calldata = self.generate_approval_calldatas(owner).await?; + match approval_calldata { + ApprovalCalldataResult::Calldatas(calldatas) => { + let mut output_token_infos = HashMap::new(); + for output in deployment.deployment.order.outputs.clone() { + if output.token.is_none() { + return Err(GuiError::SelectTokensNotSet); + } + let token = output.token.as_ref().unwrap(); + let token_info = self.get_token_info(token.key.clone()).await?; + output_token_infos.insert(token.address.clone(), token_info); + } + + for calldata in calldatas.iter() { + let token_info = output_token_infos + .get(&calldata.token) + .ok_or(GuiError::TokenNotFound(calldata.token.to_string()))?; + approvals.push(ExtendedApprovalCalldata { + token: calldata.token, + calldata: calldata.calldata.clone(), + symbol: token_info.symbol.clone(), + }); + } + } + _ => {} + } + + let deposit_and_add_order_calldata = + self.generate_deposit_and_add_order_calldatas().await?; + + Ok(DeploymentTransactionArgs { + approvals, + deployment_calldata: deposit_and_add_order_calldata.0, + orderbook_address: deployment + .deployment + .order + .orderbook + .as_ref() + .ok_or(GuiError::OrderbookNotFound)? + .address, + chain_id: deployment.deployment.order.network.chain_id, + }) + } } diff --git a/packages/orderbook/test/js_api/gui.test.ts b/packages/orderbook/test/js_api/gui.test.ts index d270a594a..88b1b19f7 100644 --- a/packages/orderbook/test/js_api/gui.test.ts +++ b/packages/orderbook/test/js_api/gui.test.ts @@ -7,6 +7,7 @@ import { AllowancesResult, DeploymentDetails, DeploymentKeys, + DeploymentTransactionArgs, DepositAndAddOrderCalldataResult, Gui, GuiDeployment, @@ -1235,6 +1236,60 @@ ${dotrainWithoutVaultIds}`; const calldatas = await gui.generateDepositCalldatas(); assert.equal(calldatas.Calldatas.length, 0); }); + + it('should generate deployment transaction args', async () => { + await mockServer + .forPost('/rpc-url') + .thenSendJsonRpcResult( + '0x00000000000000000000000000000000000000000000003635C9ADC5DEA00000' + ); + await mockServer + .forPost('/rpc-url') + .withBodyIncluding('0xf0cfdd37') + .thenSendJsonRpcResult(`0x${'0'.repeat(24) + '1'.repeat(40)}`); + await mockServer + .forPost('/rpc-url') + .withBodyIncluding('0xc19423bc') + .thenSendJsonRpcResult(`0x${'0'.repeat(24) + '2'.repeat(40)}`); + await mockServer + .forPost('/rpc-url') + .withBodyIncluding('0x24376855') + .thenSendJsonRpcResult(`0x${'0'.repeat(24) + '3'.repeat(40)}`); + await mockServer + .forPost('/rpc-url') + .withBodyIncluding('0xa3869e14') + .thenSendJsonRpcResult( + '0x000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000021234000000000000000000000000000000000000000000000000000000000000' + ); + + gui.saveDeposit('token2', '5000'); + gui.saveFieldValue('test-binding', { + isPreset: false, + value: '10' + }); + + let result: DeploymentTransactionArgs = await gui.getDeploymentTransactionArgs( + '0x1234567890abcdef1234567890abcdef12345678' + ); + + assert.equal(result.approvals.length, 1); + assert.equal( + result.approvals[0].calldata, + '0x095ea7b3000000000000000000000000c95a5f8efe14d7a20bd2e5bafec4e71f8ce0b9a600000000000000000000000000000000000000000000010f0cf064dd59200000' + ); + assert.equal(result.approvals[0].symbol, 'T2'); + assert.equal(result.deploymentCalldata.length, 3146); + assert.equal(result.orderbookAddress, '0xc95a5f8efe14d7a20bd2e5bafec4e71f8ce0b9a6'); + assert.equal(result.chainId, 123); + + gui.removeDeposit('token2'); + result = await gui.getDeploymentTransactionArgs('0x1234567890abcdef1234567890abcdef12345678'); + + assert.equal(result.approvals.length, 0); + assert.equal(result.deploymentCalldata.length, 2634); + assert.equal(result.orderbookAddress, '0xc95a5f8efe14d7a20bd2e5bafec4e71f8ce0b9a6'); + assert.equal(result.chainId, 123); + }); }); describe('select tokens tests', async () => { diff --git a/packages/ui-components/src/__tests__/getDeploymentTransactionArgs.test.ts b/packages/ui-components/src/__tests__/getDeploymentTransactionArgs.test.ts index a85760ebe..29144a8a8 100644 --- a/packages/ui-components/src/__tests__/getDeploymentTransactionArgs.test.ts +++ b/packages/ui-components/src/__tests__/getDeploymentTransactionArgs.test.ts @@ -5,7 +5,7 @@ import { } 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'; +import type { DotrainOrderGui } from '@rainlanguage/orderbook/js_api'; // Mock wagmi/core vi.mock('@wagmi/core', () => ({ @@ -15,36 +15,22 @@ vi.mock('@wagmi/core', () => ({ 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' + getDeploymentTransactionArgs: vi.fn().mockResolvedValue({ + chainId: 1, + orderbookAddress: '0xorderbook', + approvals: [{ token: '0x123', calldata: '0x1', symbol: 'TEST' }], + deploymentCalldata: '0x1' }) } as unknown as DotrainOrderGui; mockWagmiConfig = {} as Config; (getAccount as Mock).mockReturnValue({ address: '0xuser' }); - - mockTokenOutputs = [{ token: { key: 'token1' } }] as OrderIO[]; }); describe('successful cases', () => { @@ -52,127 +38,34 @@ describe('getDeploymentTransactionArgs', () => { mockGui.generateApprovalCalldatas = vi.fn().mockResolvedValue({ Calldatas: [{ token: '0x123', amount: '1000' }] }); - const result = await getDeploymentTransactionArgs(mockGui, mockWagmiConfig, mockTokenOutputs); + const result = await getDeploymentTransactionArgs(mockGui, mockWagmiConfig); expect(result).toEqual({ - approvals: [{ token: '0x123', amount: '1000', symbol: 'TEST' }], - deploymentCalldata: { - deposit: '0xdeposit', - addOrder: '0xaddOrder' - }, + approvals: [{ token: '0x123', calldata: '0x1', symbol: 'TEST' }], + deploymentCalldata: '0x1', 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); + await expect(getDeploymentTransactionArgs(null, mockWagmiConfig)).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); + await expect(getDeploymentTransactionArgs(mockGui, undefined)).rejects.toThrow( + AddOrderErrors.MISSING_CONFIG + ); }); it('should throw NO_WALLET when wallet address is not found', async () => { (getAccount as Mock).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' - }); - - mockGui.generateApprovalCalldatas = vi.fn().mockResolvedValue({ - Calldatas: [{ token: '0x123', amount: '1000' }] - }); - - await expect( - getDeploymentTransactionArgs(mockGui, mockWagmiConfig, mockTokenOutputs) - ).rejects.toThrow( - `${AddOrderErrors.TOKEN_INFO_FAILED}: Token info not found for address: 0x123` + await expect(getDeploymentTransactionArgs(mockGui, mockWagmiConfig)).rejects.toThrow( + AddOrderErrors.NO_WALLET ); }); }); diff --git a/packages/ui-components/src/lib/components/deployment/DeploymentSteps.svelte b/packages/ui-components/src/lib/components/deployment/DeploymentSteps.svelte index 8d069344d..d9540ad5b 100644 --- a/packages/ui-components/src/lib/components/deployment/DeploymentSteps.svelte +++ b/packages/ui-components/src/lib/components/deployment/DeploymentSteps.svelte @@ -214,7 +214,7 @@ checkingDeployment = true; try { - result = await getDeploymentTransactionArgs(gui, $wagmiConfig, allTokenOutputs); + result = await getDeploymentTransactionArgs(gui, $wagmiConfig); } catch (e) { checkingDeployment = false; error = DeploymentStepErrors.ADD_ORDER_FAILED; diff --git a/packages/ui-components/src/lib/components/deployment/getDeploymentTransactionArgs.ts b/packages/ui-components/src/lib/components/deployment/getDeploymentTransactionArgs.ts index 4070187ce..004198a85 100644 --- a/packages/ui-components/src/lib/components/deployment/getDeploymentTransactionArgs.ts +++ b/packages/ui-components/src/lib/components/deployment/getDeploymentTransactionArgs.ts @@ -4,7 +4,6 @@ import type { DepositAndAddOrderCalldataResult, DotrainOrderGui } from '@rainlanguage/orderbook/js_api'; -import type { OrderIO } from '@rainlanguage/orderbook/js_api'; import type { Hex } from 'viem'; import type { ExtendedApprovalCalldata } from '$lib/stores/transactionStore'; @@ -12,12 +11,7 @@ export enum AddOrderErrors { ADD_ORDER_FAILED = 'Failed to add order', MISSING_GUI = 'Order GUI is required', MISSING_CONFIG = 'Wagmi config is required', - NO_WALLET = 'No wallet address found', - INVALID_CHAIN_ID = 'Invalid chain ID in deployment', - MISSING_ORDERBOOK = 'Orderbook address not found', - TOKEN_INFO_FAILED = 'Failed to fetch token information', - APPROVAL_FAILED = 'Failed to generate approval calldata', - DEPLOYMENT_FAILED = 'Failed to generate deployment calldata' + NO_WALLET = 'No wallet address found' } export interface HandleAddOrderResult { @@ -29,8 +23,7 @@ export interface HandleAddOrderResult { export async function getDeploymentTransactionArgs( gui: DotrainOrderGui | null, - wagmiConfig: Config | undefined, - allTokenOutputs: OrderIO[] + wagmiConfig: Config | undefined ): Promise { if (!gui) { throw new Error(AddOrderErrors.MISSING_GUI); @@ -45,64 +38,8 @@ export async function getDeploymentTransactionArgs( throw new Error(AddOrderErrors.NO_WALLET); } - let approvalResults; - try { - approvalResults = await gui.generateApprovalCalldatas(address); - } catch (error) { - throw new Error( - `${AddOrderErrors.APPROVAL_FAILED}: ${error instanceof Error ? error.message : 'Unknown error'}` - ); - } - - let deploymentCalldata; - try { - deploymentCalldata = await gui.generateDepositAndAddOrderCalldatas(); - } catch (error) { - throw new Error( - `${AddOrderErrors.DEPLOYMENT_FAILED}: ${error instanceof Error ? error.message : 'Unknown error'}` - ); - } - - const currentDeployment = gui.getCurrentDeployment(); - const chainId = currentDeployment?.deployment?.order?.network?.['chain-id'] as number; - if (!chainId) { - throw new Error(AddOrderErrors.INVALID_CHAIN_ID); - } - - // @ts-expect-error orderbook is not typed - const orderbookAddress = currentDeployment?.deployment?.order?.orderbook?.address; - if (!orderbookAddress) { - throw new Error(AddOrderErrors.MISSING_ORDERBOOK); - } - - let approvals: ExtendedApprovalCalldata[] = []; - - try { - const outputTokenInfos = await Promise.all( - allTokenOutputs.map((token) => { - const key = token.token?.key; - if (!key) throw new Error('Token key is missing'); - return gui.getTokenInfo(key); - }) - ); - - if (approvalResults !== 'NoDeposits') { - approvals = approvalResults.Calldatas.map((approval) => { - const token = outputTokenInfos.find((token) => token?.address === approval.token); - if (!token) { - throw new Error(`Token info not found for address: ${approval.token}`); - } - return { - ...approval, - symbol: token.symbol - }; - }); - } - } catch (error) { - throw new Error( - `${AddOrderErrors.TOKEN_INFO_FAILED}: ${error instanceof Error ? error.message : 'Unknown error'}` - ); - } + const { approvals, deploymentCalldata, orderbookAddress, chainId } = + await gui.getDeploymentTransactionArgs(address); return { approvals,