diff --git a/crates/js_api/src/gui/select_tokens.rs b/crates/js_api/src/gui/select_tokens.rs index 67a920764..beafda022 100644 --- a/crates/js_api/src/gui/select_tokens.rs +++ b/crates/js_api/src/gui/select_tokens.rs @@ -47,6 +47,17 @@ impl DotrainOrderGui { Ok(()) } + #[wasm_bindgen(js_name = "getNetworkKey")] + pub fn get_network_key(&self) -> Result { + let order_key = Deployment::parse_order_key( + self.dotrain_order.dotrain_yaml().documents, + &self.selected_deployment, + )?; + let network_key = + Order::parse_network_key(self.dotrain_order.dotrain_yaml().documents, &order_key)?; + Ok(network_key) + } + #[wasm_bindgen(js_name = "saveSelectToken")] pub async fn save_select_token( &mut self, diff --git a/crates/js_api/src/subgraph/mod.rs b/crates/js_api/src/subgraph/mod.rs index 3241b9c16..7df2bad7f 100644 --- a/crates/js_api/src/subgraph/mod.rs +++ b/crates/js_api/src/subgraph/mod.rs @@ -8,6 +8,7 @@ use thiserror::Error; use wasm_bindgen::{JsError, JsValue}; pub mod order; +pub mod transaction; pub mod vault; #[derive(Error, Debug)] diff --git a/crates/js_api/src/subgraph/transaction.rs b/crates/js_api/src/subgraph/transaction.rs new file mode 100644 index 000000000..cb173586d --- /dev/null +++ b/crates/js_api/src/subgraph/transaction.rs @@ -0,0 +1,16 @@ +use cynic::Id; +use rain_orderbook_bindings::wasm_traits::prelude::*; +use rain_orderbook_subgraph_client::{OrderbookSubgraphClient, OrderbookSubgraphClientError}; +use reqwest::Url; + +/// Internal function to fetch a single transaction +/// Returns the Transaction struct +#[wasm_bindgen(js_name = "getTransaction")] +pub async fn get_transaction( + url: &str, + tx_hash: &str, +) -> Result { + let client = OrderbookSubgraphClient::new(Url::parse(url)?); + let transaction = client.transaction_detail(Id::new(tx_hash)).await?; + Ok(to_value(&transaction)?) +} diff --git a/crates/settings/src/gui.rs b/crates/settings/src/gui.rs index 773f88809..bd1ac4a48 100644 --- a/crates/settings/src/gui.rs +++ b/crates/settings/src/gui.rs @@ -263,7 +263,9 @@ impl_all_wasm_traits!(Gui); pub struct NameAndDescription { pub name: String, pub description: String, + pub short_description: Option, } + #[cfg(target_family = "wasm")] impl_all_wasm_traits!(NameAndDescription); @@ -376,7 +378,17 @@ impl Gui { Some("gui".to_string()), )?; - return Ok(NameAndDescription { name, description }); + let short_description = require_string( + get_hash_value(gui, "short-description", Some("gui".to_string()))?, + None, + Some("gui".to_string()), + )?; + + return Ok(NameAndDescription { + name, + description, + short_description: Some(short_description), + }); } } Err(YamlError::Field { @@ -422,8 +434,14 @@ impl Gui { Some(location.clone()), )?; - deployment_details - .insert(deployment_key, NameAndDescription { name, description }); + deployment_details.insert( + deployment_key, + NameAndDescription { + name, + description, + short_description: None, + }, + ); } } } @@ -1474,8 +1492,8 @@ gui: - binding: test name: test presets: - - value: - - test + - value: + wrong: map "#; let error = Gui::parse_from_yaml_optional( vec![get_document(&format!("{yaml_prefix}{yaml}"))], diff --git a/crates/settings/src/yaml/dotrain.rs b/crates/settings/src/yaml/dotrain.rs index 0181a7dd1..89404ec08 100644 --- a/crates/settings/src/yaml/dotrain.rs +++ b/crates/settings/src/yaml/dotrain.rs @@ -204,6 +204,7 @@ mod tests { gui: name: Test gui description: Test description + short-description: Test short description deployments: deployment1: name: Test deployment diff --git a/crates/subgraph/src/orderbook_client.rs b/crates/subgraph/src/orderbook_client.rs index fbcb74ac5..aa902c629 100644 --- a/crates/subgraph/src/orderbook_client.rs +++ b/crates/subgraph/src/orderbook_client.rs @@ -8,6 +8,7 @@ use crate::types::order::{ OrdersListQuery, }; use crate::types::order_trade::{OrderTradeDetailQuery, OrderTradesListQuery}; +use crate::types::transaction::TransactionDetailQuery; use crate::types::vault::{VaultDetailQuery, VaultsListQuery}; use crate::vault_balance_changes_query::VaultBalanceChangesListPageQueryClient; use cynic::Id; @@ -25,6 +26,8 @@ pub enum OrderbookSubgraphClientError { CynicClientError(#[from] CynicClientError), #[error("Subgraph query returned no data")] Empty, + #[error("Request timed out")] + RequestTimedOut, #[error(transparent)] PaginationClientError(#[from] PaginationClientError), #[error(transparent)] @@ -379,4 +382,17 @@ impl OrderbookSubgraphClient { } Ok(all_pages_merged) } + + pub async fn transaction_detail( + &self, + id: Id, + ) -> Result { + let data = self + .query::(IdQueryVariables { id: &id }) + .await?; + let transaction = data + .transaction + .ok_or(OrderbookSubgraphClientError::Empty)?; + Ok(transaction) + } } diff --git a/crates/subgraph/src/types/mod.rs b/crates/subgraph/src/types/mod.rs index b653da9b9..aa590dc15 100644 --- a/crates/subgraph/src/types/mod.rs +++ b/crates/subgraph/src/types/mod.rs @@ -3,6 +3,7 @@ mod impls; pub mod order; pub mod order_detail_traits; pub mod order_trade; +pub mod transaction; pub mod vault; pub use cynic::Id; diff --git a/crates/subgraph/src/types/transaction.rs b/crates/subgraph/src/types/transaction.rs new file mode 100644 index 000000000..7344b5f23 --- /dev/null +++ b/crates/subgraph/src/types/transaction.rs @@ -0,0 +1,12 @@ +use super::common::*; +use crate::schema; +use typeshare::typeshare; + +#[derive(cynic::QueryFragment, Debug)] +#[cynic(graphql_type = "Query", variables = "IdQueryVariables")] +#[typeshare] +pub struct TransactionDetailQuery { + #[arguments(id: $id)] + #[typeshare(typescript(type = "TransactionSubgraph"))] + pub transaction: Option, +} diff --git a/package-lock.json b/package-lock.json index 58ffb73bd..b7b44e132 100644 --- a/package-lock.json +++ b/package-lock.json @@ -21,6 +21,7 @@ "flowbite": "^2.2.1", "flowbite-svelte": "^0.44.21", "flowbite-svelte-icons": "^0.4.5", + "svelte-markdown": "^0.4.1", "wagmi": "^2.14.7" }, "devDependencies": { @@ -28,6 +29,7 @@ "@sveltejs/adapter-auto": "^3.0.0", "@sveltejs/adapter-static": "^3.0.1", "@sveltejs/vite-plugin-svelte": "^3.0.0", + "@tailwindcss/typography": "^0.5.16", "@tanstack/svelte-query": "^5.59.20", "@testing-library/jest-dom": "^6.4.2", "@testing-library/svelte": "^5.1.0", @@ -49,7 +51,7 @@ "jsdom": "^24.0.0", "lodash": "^4.17.21", "mockttp": "^3.15.1", - "postcss": "^8.4.32", + "postcss": "^8.5.2", "prettier": "^3.3.2", "prettier-plugin-svelte": "^3.2.6", "prettier-plugin-tailwindcss": "^0.6.5", @@ -57,7 +59,7 @@ "svelte": "^4.2.7", "svelte-check": "^3.6.0", "tailwind-merge": "^2.5.4", - "tailwindcss": "^3.4.9", + "tailwindcss": "^3.4.17", "ts-node": "^10.9.1", "typescript": "^5.0.0", "typescript-eslint": "^8.0.0", @@ -4676,6 +4678,36 @@ "vite": "^5.0.0" } }, + "node_modules/@tailwindcss/typography": { + "version": "0.5.16", + "resolved": "https://registry.npmjs.org/@tailwindcss/typography/-/typography-0.5.16.tgz", + "integrity": "sha512-0wDLwCVF5V3x3b1SGXPCDcdsbDHMBe+lkFzBRaHeLvNi+nrrnZ1lA18u+OTWO8iSWU2GxUOCvlXtDuqftc1oiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "lodash.castarray": "^4.4.0", + "lodash.isplainobject": "^4.0.6", + "lodash.merge": "^4.6.2", + "postcss-selector-parser": "6.0.10" + }, + "peerDependencies": { + "tailwindcss": ">=3.0.0 || insiders || >=4.0.0-alpha.20 || >=4.0.0-beta.1" + } + }, + "node_modules/@tailwindcss/typography/node_modules/postcss-selector-parser": { + "version": "6.0.10", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.10.tgz", + "integrity": "sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/@tanstack/query-core": { "version": "5.59.20", "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.59.20.tgz", @@ -12172,6 +12204,7 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz", "integrity": "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==", + "dev": true, "license": "MIT", "engines": { "node": ">=10" @@ -12289,12 +12322,26 @@ "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", "license": "MIT" }, + "node_modules/lodash.castarray": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.castarray/-/lodash.castarray-4.4.0.tgz", + "integrity": "sha512-aVx8ztPv7/2ULbArGJ2Y42bG1mEQ5mGjpdvrbJcJFU3TbYybe+QlLS4pst9zV52ymy2in1KpFPiZnAOATxD4+Q==", + "dev": true, + "license": "MIT" + }, "node_modules/lodash.isequal": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", "integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==", "license": "MIT" }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "dev": true, + "license": "MIT" + }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", @@ -12807,9 +12854,9 @@ } }, "node_modules/nanoid": { - "version": "3.3.7", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", - "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", + "version": "3.3.8", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz", + "integrity": "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==", "funding": [ { "type": "github", @@ -16599,9 +16646,9 @@ } }, "node_modules/postcss": { - "version": "8.4.49", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.49.tgz", - "integrity": "sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA==", + "version": "8.5.2", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.2.tgz", + "integrity": "sha512-MjOadfU3Ys9KYoX0AdkBlFEF1Vx37uCCeN4ZHnmwm9FfpbsGWMZeBLMmmpY+6Ocqod7mkdZ0DT31OlbsFrLlkA==", "funding": [ { "type": "opencollective", @@ -16618,7 +16665,7 @@ ], "license": "MIT", "dependencies": { - "nanoid": "^3.3.7", + "nanoid": "^3.3.8", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" }, @@ -18650,33 +18697,33 @@ } }, "node_modules/tailwindcss": { - "version": "3.4.14", - "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.14.tgz", - "integrity": "sha512-IcSvOcTRcUtQQ7ILQL5quRDg7Xs93PdJEk1ZLbhhvJc7uj/OAhYOnruEiwnGgBvUtaUAJ8/mhSw1o8L2jCiENA==", + "version": "3.4.17", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.17.tgz", + "integrity": "sha512-w33E2aCvSDP0tW9RZuNXadXlkHXqFzSkQew/aIa2i/Sj8fThxwovwlXHSPXTbAHwEIhBFXAedUhP2tueAKP8Og==", "license": "MIT", "dependencies": { "@alloc/quick-lru": "^5.2.0", "arg": "^5.0.2", - "chokidar": "^3.5.3", + "chokidar": "^3.6.0", "didyoumean": "^1.2.2", "dlv": "^1.1.3", - "fast-glob": "^3.3.0", + "fast-glob": "^3.3.2", "glob-parent": "^6.0.2", "is-glob": "^4.0.3", - "jiti": "^1.21.0", - "lilconfig": "^2.1.0", - "micromatch": "^4.0.5", + "jiti": "^1.21.6", + "lilconfig": "^3.1.3", + "micromatch": "^4.0.8", "normalize-path": "^3.0.0", "object-hash": "^3.0.0", - "picocolors": "^1.0.0", - "postcss": "^8.4.23", + "picocolors": "^1.1.1", + "postcss": "^8.4.47", "postcss-import": "^15.1.0", "postcss-js": "^4.0.1", - "postcss-load-config": "^4.0.1", - "postcss-nested": "^6.0.1", - "postcss-selector-parser": "^6.0.11", - "resolve": "^1.22.2", - "sucrase": "^3.32.0" + "postcss-load-config": "^4.0.2", + "postcss-nested": "^6.2.0", + "postcss-selector-parser": "^6.1.2", + "resolve": "^1.22.8", + "sucrase": "^3.35.0" }, "bin": { "tailwind": "lib/cli.js", @@ -18722,6 +18769,18 @@ "node": ">= 6" } }, + "node_modules/tailwindcss/node_modules/lilconfig": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", + "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" + } + }, "node_modules/tailwindcss/node_modules/picomatch": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", @@ -18769,18 +18828,6 @@ } } }, - "node_modules/tailwindcss/node_modules/postcss-load-config/node_modules/lilconfig": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.2.tgz", - "integrity": "sha512-eop+wDAvpItUys0FWkHIKeC9ybYrTGbU41U5K7+bttZZeohvnY7M9dZ5kB21GNWiFT2q1OoPTvncPCgSOVO5ow==", - "license": "MIT", - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/antonk52" - } - }, "node_modules/tailwindcss/node_modules/readdirp": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", diff --git a/package.json b/package.json index d97f8df23..b7f7b38a2 100644 --- a/package.json +++ b/package.json @@ -31,6 +31,7 @@ "@sveltejs/adapter-auto": "^3.0.0", "@sveltejs/adapter-static": "^3.0.1", "@sveltejs/vite-plugin-svelte": "^3.0.0", + "@tailwindcss/typography": "^0.5.16", "@tanstack/svelte-query": "^5.59.20", "@testing-library/jest-dom": "^6.4.2", "@testing-library/svelte": "^5.1.0", @@ -52,7 +53,7 @@ "jsdom": "^24.0.0", "lodash": "^4.17.21", "mockttp": "^3.15.1", - "postcss": "^8.4.32", + "postcss": "^8.5.2", "prettier": "^3.3.2", "prettier-plugin-svelte": "^3.2.6", "prettier-plugin-tailwindcss": "^0.6.5", @@ -60,7 +61,7 @@ "svelte": "^4.2.7", "svelte-check": "^3.6.0", "tailwind-merge": "^2.5.4", - "tailwindcss": "^3.4.9", + "tailwindcss": "^3.4.17", "ts-node": "^10.9.1", "typescript": "^5.0.0", "typescript-eslint": "^8.0.0", @@ -83,6 +84,7 @@ "flowbite": "^2.2.1", "flowbite-svelte": "^0.44.21", "flowbite-svelte-icons": "^0.4.5", + "svelte-markdown": "^0.4.1", "wagmi": "^2.14.7" } } diff --git a/packages/orderbook/test/js_api/gui.test.ts b/packages/orderbook/test/js_api/gui.test.ts index 11ed141c1..dac15e4eb 100644 --- a/packages/orderbook/test/js_api/gui.test.ts +++ b/packages/orderbook/test/js_api/gui.test.ts @@ -23,10 +23,12 @@ const guiConfig = ` gui: name: Fixed limit description: Fixed limit order strategy + short-description: Buy WETH with USDC on Base. deployments: some-deployment: name: Buy WETH with USDC on Base. description: Buy WETH with USDC for fixed price on Base network. + short-description: Buy WETH with USDC on Base. deposits: - token: token1 min: 0 @@ -374,6 +376,7 @@ describe('Rain Orderbook JS API Package Bindgen Tests - Gui', async function () await DotrainOrderGui.getStrategyDetails(dotrainWithGui); assert.equal(strategyDetails.name, 'Fixed limit'); assert.equal(strategyDetails.description, 'Fixed limit order strategy'); + assert.equal(strategyDetails.short_description, 'Buy WETH with USDC on Base.'); }); it('should get deployment details', async () => { @@ -1393,5 +1396,10 @@ ${dotrainWithoutVaultIds}`; "Missing required field 'tokens' in root" ); }); + + it('should get network key', async () => { + const networkKey = gui.getNetworkKey(); + assert.equal(networkKey, 'some-network'); + }); }); }); diff --git a/packages/orderbook/test/js_api/transaction.test.ts b/packages/orderbook/test/js_api/transaction.test.ts new file mode 100644 index 000000000..809db66bb --- /dev/null +++ b/packages/orderbook/test/js_api/transaction.test.ts @@ -0,0 +1,31 @@ +import assert from 'assert'; +import { getLocal } from 'mockttp'; +import { describe, it, beforeEach, afterEach } from 'vitest'; +import { Transaction } from '../../dist/types/js_api.js'; +import { getTransaction } from '../../dist/cjs/js_api.js'; + +const transaction1 = { + id: 'tx1', + from: '0x1', + blockNumber: '1', + timestamp: '1' +} as unknown as Transaction; + +describe('Rain Orderbook JS API Package Bindgen Tests - Order', async function () { + const mockServer = getLocal(); + beforeEach(() => mockServer.start(8090)); + afterEach(() => mockServer.stop()); + + it('should fetch a single transaction', async () => { + await mockServer + .forPost('/sg1') + .thenReply(200, JSON.stringify({ data: { transaction: transaction1 } })); + + try { + const result: Transaction = await getTransaction(mockServer.url + '/sg1', transaction1.id); + assert.equal(result.id, transaction1.id); + } catch (e) { + assert.fail('expected to resolve, but failed' + (e instanceof Error ? e.message : String(e))); + } + }); +}); diff --git a/packages/ui-components/package.json b/packages/ui-components/package.json index b37deba33..9225134a3 100644 --- a/packages/ui-components/package.json +++ b/packages/ui-components/package.json @@ -12,6 +12,12 @@ "import": "./dist/index.js", "require": "./dist/index.js", "svelte": "./dist/index.js" + }, + "./services": { + "types": "./dist/services/index.d.ts", + "import": "./dist/services/index.js", + "require": "./dist/services/index.js", + "svelte": "./dist/services/index.js" } }, "scripts": { diff --git a/packages/ui-components/src/__tests__/DeploymentSteps.test.ts b/packages/ui-components/src/__tests__/DeploymentSteps.test.ts index 464e815a0..46566e7f3 100644 --- a/packages/ui-components/src/__tests__/DeploymentSteps.test.ts +++ b/packages/ui-components/src/__tests__/DeploymentSteps.test.ts @@ -1,11 +1,12 @@ import { describe, it, expect, vi, beforeEach, type Mock } from 'vitest'; import { render, screen, waitFor } from '@testing-library/svelte'; import DeploymentSteps from '../lib/components/deployment/DeploymentSteps.svelte'; -import { DotrainOrderGui } from '@rainlanguage/orderbook/js_api'; +import { DotrainOrderGui, type Scenario } from '@rainlanguage/orderbook/js_api'; import type { ComponentProps } from 'svelte'; import { writable } from 'svelte/store'; import type { AppKit } from '@reown/appkit'; +import type { ConfigSource } from '../lib/typeshare/config'; const { mockWagmiConfigStore, mockConnectedStore } = await vi.hoisted( () => import('../lib/__mocks__/stores') ); @@ -14,7 +15,8 @@ export type DeploymentStepsProps = ComponentProps; vi.mock('@rainlanguage/orderbook/js_api', () => ({ DotrainOrderGui: { - chooseDeployment: vi.fn() + chooseDeployment: vi.fn(), + getStrategyDetails: vi.fn() } })); @@ -288,7 +290,8 @@ subgraphs: orderbooks: flare: - address: 0xCEe8Cd002F151A536394E564b84076c41bBBcD4d + id: 'flare', + address: '0x0' deployers: flare: @@ -493,7 +496,6 @@ val: multiplier multiplier multiplier - multiplier ); #set-last-trade @@ -569,6 +571,51 @@ min-trade-amount: mul(min-amount 0.9), :call<'set-cost-basis-io-ratio>();`; describe('DeploymentSteps', () => { + const mockDeployment = { + key: 'flare-sflr-wflr', + name: 'SFLR<>WFLR on Flare', + description: 'Rotate sFLR (Sceptre staked FLR) and WFLR on Flare.', + deposits: [], + fields: [], + select_tokens: [], + deployment: { + key: 'flare-sflr-wflr', + scenario: { + key: 'flare', + bindings: {} + } as Scenario, + order: { + key: 'flare-sflr-wflr', + network: { + key: 'flare', + 'chain-id': 14, + 'network-id': 14, + rpc: 'https://rpc.ankr.com/flare', + label: 'Flare', + currency: 'FLR' + }, + deployer: { + key: 'flare', + network: { + key: 'flare', + 'chain-id': 14, + 'network-id': 14, + rpc: 'https://rpc.ankr.com/flare', + label: 'Flare', + currency: 'FLR' + }, + address: '0x0' + }, + orderbook: { + id: 'flare', + address: '0x0' + }, + inputs: [], + outputs: [] + } + } + }; + beforeEach(() => { vi.clearAllMocks(); }); @@ -576,32 +623,25 @@ describe('DeploymentSteps', () => { it('shows deployment details when provided', async () => { (DotrainOrderGui.chooseDeployment as Mock).mockResolvedValue({ getSelectTokens: () => [], - getTokenInfo: vi.fn() + getTokenInfo: vi.fn(), + getNetworkKey: vi.fn() }); - 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, + deployment: mockDeployment, wagmiConfig: mockWagmiConfigStore, wagmiConnected: mockConnectedStore, appKitModal: writable({} as AppKit), handleDeployModal: vi.fn(), + settings: writable({} as ConfigSource), handleUpdateGuiState: vi.fn() } }); await waitFor(() => { expect(screen.getByText('SFLR<>WFLR on Flare')).toBeInTheDocument(); - expect( - screen.getByText('Rotate sFLR (Sceptre staked FLR) and WFLR on Flare.') - ).toBeInTheDocument(); }); }); @@ -609,18 +649,19 @@ describe('DeploymentSteps', () => { const mockSelectTokens = ['token1', 'token2']; (DotrainOrderGui.chooseDeployment as Mock).mockResolvedValue({ getSelectTokens: () => mockSelectTokens, - getTokenInfo: vi.fn() + getTokenInfo: vi.fn(), + getNetworkKey: vi.fn() }); render(DeploymentSteps, { props: { dotrain, - deployment: 'flare-sflr-wflr', - deploymentDetails: { name: 'Deployment 1', description: 'Description 1' }, + deployment: mockDeployment, wagmiConfig: mockWagmiConfigStore, wagmiConnected: mockConnectedStore, appKitModal: writable({} as AppKit), handleDeployModal: vi.fn(), + settings: writable({} as ConfigSource), handleUpdateGuiState: vi.fn() } }); @@ -641,12 +682,12 @@ describe('DeploymentSteps', () => { render(DeploymentSteps, { props: { dotrain, - deployment: 'flare-sflr-wflr', - deploymentDetails: { name: 'Deployment 1', description: 'Description 1' }, + deployment: mockDeployment, wagmiConfig: mockWagmiConfigStore, wagmiConnected: mockConnectedStore, appKitModal: writable({} as AppKit), handleDeployModal: vi.fn(), + settings: writable({} as ConfigSource), handleUpdateGuiState: vi.fn() } }); @@ -671,18 +712,19 @@ describe('DeploymentSteps', () => { deposits: [] }), getAllFieldDefinitions: () => [], - getTokenInfo: vi.fn() + getTokenInfo: vi.fn(), + getNetworkKey: vi.fn() }); render(DeploymentSteps, { props: { dotrain, - deployment: 'flare-sflr-wflr', - deploymentDetails: { name: 'Deployment 1', description: 'Description 1' }, + deployment: mockDeployment, wagmiConfig: mockWagmiConfigStore, wagmiConnected: mockConnectedStore, appKitModal: writable({} as AppKit), handleDeployModal: vi.fn(), + settings: writable({} as ConfigSource), handleUpdateGuiState: vi.fn() } }); @@ -705,18 +747,19 @@ describe('DeploymentSteps', () => { deposits: [] }), getAllFieldDefinitions: () => [], - getTokenInfo: vi.fn() + getTokenInfo: vi.fn(), + getNetworkKey: vi.fn() }); render(DeploymentSteps, { props: { dotrain, - deployment: 'flare-sflr-wflr', - deploymentDetails: { name: 'Deployment 1', description: 'Description 1' }, + deployment: mockDeployment, wagmiConfig: mockWagmiConfigStore, wagmiConnected: mockConnectedStore, appKitModal: writable({} as AppKit), handleDeployModal: vi.fn(), + settings: writable({} as ConfigSource), handleUpdateGuiState: vi.fn() } }); diff --git a/packages/ui-components/src/__tests__/DeploymentsSection.test.ts b/packages/ui-components/src/__tests__/DeploymentsSection.test.ts index e0b81e838..7a0145a43 100644 --- a/packages/ui-components/src/__tests__/DeploymentsSection.test.ts +++ b/packages/ui-components/src/__tests__/DeploymentsSection.test.ts @@ -17,8 +17,11 @@ describe('DeploymentsSection', () => { 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' }] + [ + 'key1', + { name: 'Deployment 1', description: 'Description 1', short_description: 'Short 1' } + ], + ['key2', { name: 'Deployment 2', description: 'Description 2', short_description: 'Short 2' }] ]); vi.mocked(DotrainOrderGui.getDeploymentDetails).mockResolvedValue(mockDeployments); diff --git a/packages/ui-components/src/__tests__/LightweightChart.test.ts b/packages/ui-components/src/__tests__/LightweightChart.test.ts index 2911a5df4..e400b2058 100644 --- a/packages/ui-components/src/__tests__/LightweightChart.test.ts +++ b/packages/ui-components/src/__tests__/LightweightChart.test.ts @@ -8,12 +8,14 @@ const setDataMock = vi.fn(); const applyOptionsMock = vi.fn(); const setVisibleRangeMock = vi.fn(); const removeMock = vi.fn(); +const updateMock = vi.fn(); vi.mock('lightweight-charts', async (importOriginal) => ({ ...((await importOriginal()) as object), createChart: vi.fn(() => ({ addLineSeries: vi.fn(() => ({ - setData: setDataMock + setData: setDataMock, + update: updateMock })), remove: removeMock, applyOptions: applyOptionsMock, @@ -96,23 +98,18 @@ test('renders with data correctly', async () => { }); }); -test('updates data correctly when props change', async () => { +test('updates data correctly when new data points are added', async () => { const title = 'test title'; const emptyMessage = 'empty message'; const loading = false; const priceSymbol = '$'; const createSeries = (chart: IChartApi) => chart.addLineSeries(); - const initialData: { value: number; time: UTCTimestamp; color?: string }[] = [ + const initialData = [ { value: 10, time: 1529899200 as UTCTimestamp }, { value: 20, time: 1529899300 as UTCTimestamp } ]; - const newData: { value: number; time: UTCTimestamp; color?: string }[] = [ - { value: 15, time: 1529900000 as UTCTimestamp }, - { value: 25, time: 1529900300 as UTCTimestamp } - ]; - const { component } = render(LightweightChart, { title, emptyMessage, @@ -123,15 +120,32 @@ test('updates data correctly when props change', async () => { lightweightChartsTheme: readable({ test: 'test' }) }); + // First render should call setData with initial data await waitFor(() => { expect(setDataMock).toHaveBeenCalledWith(initialData); }); - // Update data prop - await act(() => component.$set({ data: newData })); + // Add new data points + const newDataPoints = [ + ...initialData, + { value: 30, time: 1529899400 as UTCTimestamp }, + { value: 40, time: 1529899500 as UTCTimestamp } + ]; + + // Update with new data that includes additional points + await act(() => component.$set({ data: newDataPoints })); + // Should call update for each new point await waitFor(() => { - expect(setDataMock).toHaveBeenCalledWith(newData); + expect(updateMock).toHaveBeenCalledTimes(2); + expect(updateMock).toHaveBeenCalledWith({ + value: 30, + time: 1529899400 as UTCTimestamp + }); + expect(updateMock).toHaveBeenCalledWith({ + value: 40, + time: 1529899500 as UTCTimestamp + }); }); }); diff --git a/packages/ui-components/src/__tests__/OrderDetail.test.svelte b/packages/ui-components/src/__tests__/OrderDetail.test.svelte index 284198db7..a1b46623b 100644 --- a/packages/ui-components/src/__tests__/OrderDetail.test.svelte +++ b/packages/ui-components/src/__tests__/OrderDetail.test.svelte @@ -58,6 +58,7 @@ rpcUrl="https://example.com" query={orderDetailQuery} handleDepositOrWithdrawModal={() => {}} + {subgraphUrl} /> diff --git a/packages/ui-components/src/__tests__/StrategyPage.test.ts b/packages/ui-components/src/__tests__/StrategyPage.test.ts new file mode 100644 index 000000000..40a43d68d --- /dev/null +++ b/packages/ui-components/src/__tests__/StrategyPage.test.ts @@ -0,0 +1,196 @@ +import { render, screen, waitFor } from '@testing-library/svelte'; +import StrategyPage from '../lib/components/deployment/StrategyPage.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() + } +})); + +vi.mock('svelte-markdown', async () => { + const mockSvelteMarkdown = (await import('../lib/__mocks__/MockComponent.svelte')).default; + return { default: mockSvelteMarkdown }; +}); + +describe('StrategySection', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it('renders strategy details successfully with rawDotrain', async () => { + const mockDotrain = 'mock dotrain content'; + const mockStrategyDetails = { + name: 'Test Strategy', + description: 'Test Description', + short_description: 'Test Short Description' + }; + vi.mocked(DotrainOrderGui.getStrategyDetails).mockResolvedValueOnce(mockStrategyDetails); + + render(StrategyPage, { + props: { + dotrain: mockDotrain + } + }); + + await waitFor(() => { + expect(screen.getByText('Test Strategy')).toBeInTheDocument(); + expect(screen.getByText('Test Description')).toBeInTheDocument(); + }); + }); + + it('renders strategy details successfully from fetch', async () => { + const mockDotrain = 'mock dotrain content'; + const mockStrategyDetails = { + name: 'Test Strategy', + description: 'Test Description', + short_description: 'Test Short Description' + }; + + // Mock fetch response + mockFetch.mockResolvedValueOnce({ + text: () => Promise.resolve(mockDotrain) + }); + + // Mock DotrainOrderGui methods + vi.mocked(DotrainOrderGui.getStrategyDetails).mockResolvedValueOnce(mockStrategyDetails); + + render(StrategyPage, { + props: { + strategyName: 'TestStrategy', + dotrain: mockDotrain + } + }); + + 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(StrategyPage, { + props: { + strategyName: 'TestStrategy', + dotrain: mockDotrain + } + }); + + 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(StrategyPage, { + props: { + strategyName: 'TestStrategy' + } + }); + + await waitFor(() => { + expect(screen.getByText('Error getting strategy details')).toBeInTheDocument(); + expect( + screen.getByText("Cannot read properties of undefined (reading 'description')") + ).toBeInTheDocument(); + }); + }); + + it('renders markdown if description is a markdown url', async () => { + const mockDotrain = 'mock dotrain content'; + const mockStrategyDetails = { + name: 'Test Strategy', + description: 'https://example.com/description.md', + short_description: 'Test Short Description' + }; + const mockMarkdownContent = '# Mock Markdown Content'; + + // First fetch for dotrain + mockFetch.mockResolvedValueOnce({ + ok: true, + text: () => Promise.resolve(mockDotrain) + }); + + // Second fetch for markdown content + mockFetch.mockResolvedValueOnce({ + ok: true, + text: () => Promise.resolve(mockMarkdownContent) + }); + + vi.mocked(DotrainOrderGui.getStrategyDetails).mockResolvedValueOnce(mockStrategyDetails); + + render(StrategyPage, { + props: { + strategyName: 'TestStrategy', + dotrain: mockDotrain + } + }); + + await waitFor(() => { + expect(screen.getByText('Test Strategy')).toBeInTheDocument(); + expect(screen.getByTestId('plain-description')).toHaveTextContent( + 'https://example.com/description.md' + ); + expect(mockFetch).toHaveBeenCalledWith('https://example.com/description.md'); + }); + }); + + it('falls back to plain text when markdown fetch fails', async () => { + const mockDotrain = 'mock dotrain content'; + const mockStrategyDetails = { + name: 'Test Strategy', + description: 'https://example.com/description.md', + short_description: 'Test Short Description' + }; + + mockFetch + .mockResolvedValueOnce({ + ok: true, + text: () => Promise.resolve(mockDotrain) + }) + .mockResolvedValueOnce({ + ok: false, + statusText: 'Not Found' + }); + + vi.mocked(DotrainOrderGui.getStrategyDetails).mockResolvedValueOnce(mockStrategyDetails); + + render(StrategyPage, { + props: { + strategyName: 'TestStrategy', + dotrain: mockDotrain + } + }); + + await waitFor(() => { + expect(screen.getByText('Test Strategy')).toBeInTheDocument(); + expect(screen.getByTestId('plain-description')).toHaveTextContent( + 'https://example.com/description.md' + ); + }); + }); +}); diff --git a/packages/ui-components/src/__tests__/StrategySection.test.ts b/packages/ui-components/src/__tests__/StrategySection.test.ts deleted file mode 100644 index 9d9a1efdf..000000000 --- a/packages/ui-components/src/__tests__/StrategySection.test.ts +++ /dev/null @@ -1,114 +0,0 @@ -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 with rawDotrain', async () => { - const mockDotrain = 'mock dotrain content'; - const mockStrategyDetails = { - name: 'Test Strategy', - description: 'Test Description' - }; - vi.mocked(DotrainOrderGui.getStrategyDetails).mockResolvedValueOnce(mockStrategyDetails); - - render(StrategySection, { - props: { - rawDotrain: mockDotrain - } - }); - - await waitFor(() => { - expect(screen.getByText('Test Strategy')).toBeInTheDocument(); - expect(screen.getByText('Test Description')).toBeInTheDocument(); - }); - }); - - it('renders strategy details successfully from fetch', 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/ui-components/src/__tests__/StrategyShortTile.test.ts b/packages/ui-components/src/__tests__/StrategyShortTile.test.ts new file mode 100644 index 000000000..917f168b6 --- /dev/null +++ b/packages/ui-components/src/__tests__/StrategyShortTile.test.ts @@ -0,0 +1,113 @@ +import { vi } from 'vitest'; +import { render, screen } from '@testing-library/svelte'; +import { describe, it, expect, beforeEach } from 'vitest'; +import StrategyShortTile from '../lib/components/deployment/StrategyShortTile.svelte'; +import { mockPageStore } from '$lib/__mocks__/stores'; + +// Mock using the hoisted store +vi.mock('$app/stores', async () => { + const { mockPageStore } = await import('$lib/__mocks__/stores'); + return { + page: mockPageStore + }; +}); + +describe('StrategyShortTile', () => { + const mockStrategyDetails = { + name: 'Test Strategy', + description: 'A test strategy full description', + short_description: 'A test strategy description' + }; + + beforeEach(() => { + // Reset page URL params before each test + mockPageStore.mockSetSubscribeValue({ + url: new URL('http://localhost:3000'), + params: {}, + route: { id: '' }, + status: 200, + error: null, + data: {}, + form: undefined, + state: { + page: 1, + perPage: 10, + total: 100 + } + }); + }); + + it('renders strategy name and description', () => { + render(StrategyShortTile, { + props: { + strategyDetails: mockStrategyDetails, + registryName: 'test-registry' + } + }); + + expect(screen.getByText('Test Strategy')).toBeInTheDocument(); + expect(screen.getByText('A test strategy description')).toBeInTheDocument(); + }); + + it('generates correct href without registry parameter', () => { + render(StrategyShortTile, { + props: { + strategyDetails: mockStrategyDetails, + registryName: 'test-registry' + } + }); + + const link = screen.getByRole('link'); + expect(link.getAttribute('href')).toBe('/deploy/test-registry'); + }); + + it('generates correct href with registry parameter', () => { + mockPageStore.mockSetSubscribeValue({ + url: new URL('http://localhost:3000?registry=custom-registry'), + params: {}, + route: { id: '' }, + status: 200, + error: null, + data: {}, + form: undefined, + state: { + page: 1, + perPage: 10, + total: 100 + } + }); + + render(StrategyShortTile, { + props: { + strategyDetails: mockStrategyDetails, + registryName: 'test-registry' + } + }); + + const link = screen.getByRole('link'); + expect(link.getAttribute('href')).toBe('/deploy/test-registry?registry=custom-registry'); + }); + + it('applies correct styling classes', () => { + render(StrategyShortTile, { + props: { + strategyDetails: mockStrategyDetails, + registryName: 'test-registry' + } + }); + + const link = screen.getByRole('link'); + expect(link).toHaveClass( + 'flex', + 'flex-col', + 'gap-y-2', + 'rounded-xl', + 'border', + 'border-gray-200', + 'p-4', + 'hover:bg-gray-50', + 'dark:border-gray-800', + 'dark:hover:bg-gray-900' + ); + }); +}); diff --git a/packages/ui-components/src/__tests__/VaultBalanceChart.test.ts b/packages/ui-components/src/__tests__/VaultBalanceChart.test.ts index 4304faa75..49337235f 100644 --- a/packages/ui-components/src/__tests__/VaultBalanceChart.test.ts +++ b/packages/ui-components/src/__tests__/VaultBalanceChart.test.ts @@ -5,6 +5,9 @@ import VaultBalanceChart from '../lib/components/charts/VaultBalanceChart.svelte import type { Vault } from '@rainlanguage/orderbook/js_api'; import { getVaultBalanceChanges } from '@rainlanguage/orderbook/js_api'; import { writable } from 'svelte/store'; +import type { ComponentProps } from 'svelte'; + +type VaultBalanceChartProps = ComponentProps; vi.mock('@rainlanguage/orderbook/js_api', () => ({ getVaultBalanceChanges: vi.fn() @@ -42,8 +45,9 @@ test('calls getVaultBalanceChanges with correct arguments', async () => { props: { vault: mockVault, subgraphUrl: 'https://example.com', - lightweightChartsTheme: writable({}) - }, + lightweightChartsTheme: writable({}), + id: 'vault1' + } as VaultBalanceChartProps, context: new Map([['$$_queryClient', queryClient]]) }); diff --git a/packages/ui-components/src/__tests__/handleShareChoices.test.ts b/packages/ui-components/src/__tests__/handleShareChoices.test.ts new file mode 100644 index 000000000..98b4343ef --- /dev/null +++ b/packages/ui-components/src/__tests__/handleShareChoices.test.ts @@ -0,0 +1,52 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { handleShareChoices } from '../lib/services/handleShareChoices'; +import type { DotrainOrderGui } from '@rainlanguage/orderbook/js_api'; + +describe('handleShareChoices', () => { + beforeEach(() => { + // Mock clipboard API + Object.assign(navigator, { + clipboard: { + writeText: vi.fn() + } + }); + + // Mock Svelte's page store + vi.mock('$app/stores', () => ({ + page: { + subscribe: vi.fn((fn) => { + fn({ url: new URL('http://example.com') }); + return () => {}; + }) + } + })); + }); + + it('should share the choices with state', async () => { + const mockGui = { + serializeState: vi.fn().mockReturnValue('mockState123') + }; + + await handleShareChoices(mockGui as unknown as DotrainOrderGui); + + expect(navigator.clipboard.writeText).toHaveBeenCalledWith( + 'http://example.com/?state=mockState123' + ); + }); + + it('should handle null state', async () => { + const mockGui = { + serializeState: vi.fn().mockReturnValue(null) + }; + + await handleShareChoices(mockGui as unknown as DotrainOrderGui); + + expect(navigator.clipboard.writeText).toHaveBeenCalledWith('http://example.com/?state='); + }); + + it('should handle undefined gui', async () => { + await handleShareChoices(undefined as unknown as DotrainOrderGui); + + expect(navigator.clipboard.writeText).toHaveBeenCalledWith('http://example.com/?state='); + }); +}); diff --git a/packages/ui-components/src/__tests__/transactionStore.test.ts b/packages/ui-components/src/__tests__/transactionStore.test.ts index 95ab04e64..10630ccb6 100644 --- a/packages/ui-components/src/__tests__/transactionStore.test.ts +++ b/packages/ui-components/src/__tests__/transactionStore.test.ts @@ -5,6 +5,8 @@ import transactionStore, { TransactionErrorMessage } from '../lib/stores/transactionStore'; import { waitForTransactionReceipt, sendTransaction, switchChain, type Config } from '@wagmi/core'; +import { getTransaction } from '@rainlanguage/orderbook/js_api'; +import { waitFor } from '@testing-library/svelte'; vi.mock('@wagmi/core', () => ({ waitForTransactionReceipt: vi.fn(), @@ -12,6 +14,10 @@ vi.mock('@wagmi/core', () => ({ switchChain: vi.fn() })); +vi.mock('@rainlanguage/orderbook/js_api', () => ({ + getTransaction: vi.fn() +})); + describe('transactionStore', () => { const mockConfig = {} as Config; const mockOrderbookAddress = '0xabcdef1234567890'; @@ -23,7 +29,8 @@ describe('transactionStore', () => { awaitWalletConfirmation, awaitApprovalTx, transactionSuccess, - transactionError + transactionError, + awaitTransactionIndexing } = transactionStore; beforeEach(() => { @@ -89,6 +96,7 @@ describe('transactionStore', () => { (sendTransaction as Mock).mockResolvedValueOnce('approvalHash1'); (sendTransaction as Mock).mockResolvedValueOnce('approvalHash2'); (sendTransaction as Mock).mockResolvedValueOnce('deployHash'); + (getTransaction as Mock).mockReturnValue({ id: 'mockHash' }); (waitForTransactionReceipt as Mock).mockResolvedValue({}); (switchChain as Mock).mockResolvedValue({}); @@ -97,10 +105,11 @@ describe('transactionStore', () => { approvals: mockApprovals, deploymentCalldata: mockDeploymentCalldata, orderbookAddress: mockOrderbookAddress as `0x${string}`, - chainId: 1 + chainId: 1, + subgraphUrl: 'test.com' }); - expect(get(transactionStore).status).toBe(TransactionStatus.SUCCESS); + expect(get(transactionStore).status).toBe(TransactionStatus.PENDING_SUBGRAPH); expect(get(transactionStore).hash).toBe('deployHash'); }); @@ -112,7 +121,8 @@ describe('transactionStore', () => { approvals: [], deploymentCalldata: '0x', orderbookAddress: mockOrderbookAddress as `0x${string}`, - chainId: 1 + chainId: 1, + subgraphUrl: 'test.com' }); expect(get(transactionStore).status).toBe(TransactionStatus.ERROR); @@ -130,7 +140,8 @@ describe('transactionStore', () => { approvals: mockApprovals, deploymentCalldata: '0x', orderbookAddress: mockOrderbookAddress as `0x${string}`, - chainId: 1 + chainId: 1, + subgraphUrl: 'test.com' }); expect(get(transactionStore).status).toBe(TransactionStatus.ERROR); @@ -149,7 +160,8 @@ describe('transactionStore', () => { approvals: mockApprovals, deploymentCalldata: '0x', orderbookAddress: mockOrderbookAddress as `0x${string}`, - chainId: 1 + chainId: 1, + subgraphUrl: 'test.com' }); expect(get(transactionStore).status).toBe(TransactionStatus.ERROR); @@ -165,7 +177,8 @@ describe('transactionStore', () => { approvals: [], deploymentCalldata: '0x', orderbookAddress: mockOrderbookAddress as `0x${string}`, - chainId: 1 + chainId: 1, + subgraphUrl: 'test.com' }); expect(get(transactionStore).status).toBe(TransactionStatus.ERROR); @@ -182,7 +195,8 @@ describe('transactionStore', () => { approvals: [], deploymentCalldata: '0x', orderbookAddress: mockOrderbookAddress as `0x${string}`, - chainId: 1 + chainId: 1, + subgraphUrl: 'test.com' }); expect(get(transactionStore).status).toBe(TransactionStatus.ERROR); @@ -207,11 +221,58 @@ describe('transactionStore', () => { approvals: mockApprovals, deploymentCalldata: '0x', orderbookAddress: mockOrderbookAddress as `0x${string}`, - chainId: 1 + chainId: 1, + subgraphUrl: 'test.com' }); expect(sendTransaction).toHaveBeenCalledTimes(3); // 2 approvals + 1 deployment - expect(get(transactionStore).status).toBe(TransactionStatus.SUCCESS); - expect(get(transactionStore).message).toBe('Strategy deployed successfully.'); + expect(get(transactionStore).status).toBe(TransactionStatus.PENDING_SUBGRAPH); + }); + + it('should handle waiting for subgraph indexing', async () => { + const mockSubgraphUrl = 'test.com'; + const mockTxHash = 'mockHash'; + const mockSuccessMessage = 'Success! Transaction confirmed'; + + (getTransaction as Mock).mockResolvedValue({ id: mockTxHash }); + + vi.useFakeTimers({ shouldAdvanceTime: true }); + + await awaitTransactionIndexing(mockSubgraphUrl, mockTxHash, mockSuccessMessage); + + vi.runOnlyPendingTimers(); + + await waitFor(() => { + expect(get(transactionStore).status).toBe(TransactionStatus.SUCCESS); + expect(get(transactionStore).message).toBe(mockSuccessMessage); + expect(get(transactionStore).hash).toBe(mockTxHash); + }); + }); + + it('should handle subgraph indexing timeout', async () => { + vi.useFakeTimers(); + const mockSubgraphUrl = 'test.com'; + const mockTxHash = 'mockHash'; + const mockSuccessMessage = 'Success message'; + + (getTransaction as Mock).mockResolvedValue(null); + + const indexingPromise = awaitTransactionIndexing( + mockSubgraphUrl, + mockTxHash, + mockSuccessMessage + ); + + expect(get(transactionStore).status).toBe(TransactionStatus.PENDING_SUBGRAPH); + expect(get(transactionStore).message).toBe('Checking for transaction indexing...'); + + await vi.advanceTimersByTime(10000); + await indexingPromise; + + expect(get(transactionStore).message).toBe( + 'The subgraph took too long to respond. Please check again later.' + ); + + vi.useRealTimers(); }); }); diff --git a/packages/ui-components/src/lib/__mocks__/stores.ts b/packages/ui-components/src/lib/__mocks__/stores.ts index 933a322f9..f447d0628 100644 --- a/packages/ui-components/src/lib/__mocks__/stores.ts +++ b/packages/ui-components/src/lib/__mocks__/stores.ts @@ -4,6 +4,7 @@ import settingsFixture from '../__fixtures__/settings-12-11-24.json'; import { type Config } from '@wagmi/core'; import { mockWeb3Config } from './mockWeb3Config'; +import type { Page } from '@sveltejs/kit'; const mockSettingsWritable = writable(settingsFixture); const mockActiveSubgraphsWritable = writable>({}); @@ -22,6 +23,7 @@ const mockChainIdWritable = writable(0); const mockConnectedWritable = writable(false); const mockWagmiConfigWritable = writable(mockWeb3Config); const mockShowMyItemsOnlyWritable = writable(false); +const mockPageWritable = writable(); export const mockWalletAddressMatchesOrBlankStore = { subscribe: mockWalletAddressMatchesOrBlankWritable.subscribe, @@ -129,3 +131,9 @@ export const mockShowMyItemsOnlyStore = { set: mockShowMyItemsOnlyWritable.set, mockSetSubscribeValue: (value: boolean): void => mockShowMyItemsOnlyWritable.set(value) }; + +export const mockPageStore = { + subscribe: mockPageWritable.subscribe, + set: mockPageWritable.set, + mockSetSubscribeValue: (value: Page): void => mockPageWritable.set(value) +}; diff --git a/packages/ui-components/src/lib/components/charts/LightweightChart.svelte b/packages/ui-components/src/lib/components/charts/LightweightChart.svelte index 599d63698..8713fd332 100644 --- a/packages/ui-components/src/lib/components/charts/LightweightChart.svelte +++ b/packages/ui-components/src/lib/components/charts/LightweightChart.svelte @@ -45,6 +45,7 @@ let timeDelta: number; let timeFrom: UTCTimestamp; let timeTo: UTCTimestamp; + let previousDataLength = 0; function setTimeScale() { if (chart === undefined) return; @@ -70,9 +71,22 @@ }); } - function setData() { + function updateNewDataPoints() { if (series === undefined || data.length === 0) return; - series.setData(data); + + // If this is the first data set, set all the data + if (previousDataLength === 0) { + series.setData(data); + } + // If we have new data points, only update the new ones + else if (data.length > previousDataLength) { + const newPoints = data.slice(previousDataLength); + newPoints.forEach((point) => { + series?.update(point); + }); + } + + previousDataLength = data.length; setTimeScale(); } @@ -91,7 +105,7 @@ setOptions(); } - $: if (data || series) setData(); + $: if (data || series) updateNewDataPoints(); $: if (timeDelta) setTimeScale(); $: if ($lightweightChartsTheme) setOptions(); diff --git a/packages/ui-components/src/lib/components/charts/VaultBalanceChart.svelte b/packages/ui-components/src/lib/components/charts/VaultBalanceChart.svelte index 93a7ab5f5..6c71fa818 100644 --- a/packages/ui-components/src/lib/components/charts/VaultBalanceChart.svelte +++ b/packages/ui-components/src/lib/components/charts/VaultBalanceChart.svelte @@ -14,11 +14,12 @@ import { QKEY_VAULT_CHANGES } from '../../queries/keys'; export let vault: Vault; + export let id: string; export let subgraphUrl: string; export let lightweightChartsTheme; $: query = createQuery({ - queryKey: [QKEY_VAULT_CHANGES, vault], + queryKey: [id, QKEY_VAULT_CHANGES + id, QKEY_VAULT_CHANGES], queryFn: () => { return getVaultBalanceChanges(subgraphUrl || '', vault.id, { page: 1, diff --git a/packages/ui-components/src/lib/components/deployment/DeploymentPage.svelte b/packages/ui-components/src/lib/components/deployment/DeploymentPage.svelte deleted file mode 100644 index 4ee92aee3..000000000 --- a/packages/ui-components/src/lib/components/deployment/DeploymentPage.svelte +++ /dev/null @@ -1,40 +0,0 @@ - - - diff --git a/packages/ui-components/src/lib/components/deployment/DeploymentSteps.svelte b/packages/ui-components/src/lib/components/deployment/DeploymentSteps.svelte index c8e2454ba..84e5124e5 100644 --- a/packages/ui-components/src/lib/components/deployment/DeploymentSteps.svelte +++ b/packages/ui-components/src/lib/components/deployment/DeploymentSteps.svelte @@ -4,12 +4,13 @@ import SelectTokensSection from './SelectTokensSection.svelte'; import ComposedRainlangModal from './ComposedRainlangModal.svelte'; import FieldDefinitionsSection from './FieldDefinitionsSection.svelte'; + import { type ConfigSource } from '../../typeshare/config'; + import WalletConnect from '../wallet/WalletConnect.svelte'; import { DotrainOrderGui, type GuiDeposit, type GuiFieldDefinition, - type NameAndDescription, type GuiDeployment, type OrderIO, type ApprovalCalldataResult, @@ -25,6 +26,7 @@ import { page } from '$app/stores'; import { onMount } from 'svelte'; import ShareChoicesButton from './ShareChoicesButton.svelte'; + import { handleShareChoices } from '$lib/services/handleShareChoices'; enum DeploymentStepErrors { NO_GUI = 'Error loading GUI', NO_STRATEGY = 'No valid strategy exists at this URL', @@ -39,17 +41,18 @@ SERIALIZE_ERROR = 'Error serializing state', ADD_ORDER_FAILED = 'Failed to add order' } - + export let settings: Writable; export let dotrain: string; - export let deployment: string; - export let deploymentDetails: NameAndDescription; + export let deployment: GuiDeployment; export let handleDeployModal: (args: { approvals: ApprovalCalldataResult; deploymentCalldata: DepositAndAddOrderCalldataResult; orderbookAddress: Hex; chainId: number; + subgraphUrl: string; }) => void; export let handleUpdateGuiState: (gui: DotrainOrderGui) => void; + let selectTokens: SelectTokens | null = null; let allDepositFields: GuiDeposit[] = []; let allTokenOutputs: OrderIO[] = []; @@ -59,6 +62,8 @@ let gui: DotrainOrderGui | null = null; let error: DeploymentStepErrors | null = null; let errorDetails: string | null = null; + let networkKey: string | null = null; + let subgraphUrl: string = ''; export let wagmiConfig: Writable; export let wagmiConnected: Writable; @@ -66,7 +71,7 @@ export let stateFromUrl: string | null = null; $: if (deployment) { - handleDeploymentChange(deployment); + handleDeploymentChange(deployment.key); } async function handleDeploymentChange(deployment: string) { @@ -78,8 +83,10 @@ gui = await DotrainOrderGui.chooseDeployment(dotrain, deployment); if (gui) { + networkKey = await gui.getNetworkKey(); + subgraphUrl = $settings?.subgraphs?.[networkKey] ?? ''; try { - selectTokens = await gui.getSelectTokens(); + selectTokens = gui.getSelectTokens(); return selectTokens; } catch (e) { error = DeploymentStepErrors.NO_SELECT_TOKENS; @@ -181,7 +188,8 @@ approvals, deploymentCalldata, orderbookAddress, - chainId + chainId, + subgraphUrl }); } catch (e) { error = DeploymentStepErrors.ADD_ORDER_FAILED; @@ -189,9 +197,9 @@ } } - async function handleShareChoices() { - // copy the current url to the clipboard - navigator.clipboard.writeText($page.url.toString()); + async function _handleShareChoices() { + if (!gui) return; + await handleShareChoices(gui); } onMount(async () => { @@ -220,6 +228,8 @@ if (gui) { try { allTokensSelected = gui?.areAllTokensSelected(); + if (!allTokensSelected) return; + const vaultIds = gui?.getVaultIds(); const inputVaultIds = vaultIds?.get('input'); const outputVaultIds = vaultIds?.get('output'); @@ -246,13 +256,13 @@ {#if dotrain} {#if gui}
- {#if deploymentDetails} + {#if deployment}

- {deploymentDetails.name} + {deployment.name}

- {deploymentDetails.description} + {deployment.description}

{/if} @@ -283,7 +293,7 @@ {:else} {/if} - +
{#if error} diff --git a/packages/ui-components/src/lib/components/deployment/DeploymentTile.svelte b/packages/ui-components/src/lib/components/deployment/DeploymentTile.svelte index a55243b72..e12661aff 100644 --- a/packages/ui-components/src/lib/components/deployment/DeploymentTile.svelte +++ b/packages/ui-components/src/lib/components/deployment/DeploymentTile.svelte @@ -1,12 +1,24 @@

{name}

diff --git a/packages/ui-components/src/lib/components/deployment/SelectToken.svelte b/packages/ui-components/src/lib/components/deployment/SelectToken.svelte index 9c1e961e6..581681989 100644 --- a/packages/ui-components/src/lib/components/deployment/SelectToken.svelte +++ b/packages/ui-components/src/lib/components/deployment/SelectToken.svelte @@ -14,9 +14,13 @@ let checking = false; onMount(async () => { - tokenInfo = await gui?.getTokenInfo(token.key); - if (tokenInfo?.address) { - inputValue = tokenInfo.address; + try { + tokenInfo = await gui?.getTokenInfo(token.key); + if (tokenInfo?.address) { + inputValue = tokenInfo.address; + } + } catch { + // do nothing } }); diff --git a/packages/ui-components/src/lib/components/deployment/StrategyPage.svelte b/packages/ui-components/src/lib/components/deployment/StrategyPage.svelte new file mode 100644 index 000000000..cc566f1f3 --- /dev/null +++ b/packages/ui-components/src/lib/components/deployment/StrategyPage.svelte @@ -0,0 +1,80 @@ + + +{#if dotrain && strategyDetails} +
+
+
+

+ {strategyDetails.name} +

+ {#if isMarkdownUrl(strategyDetails.description) && markdownContent} +
+ +
+ {:else} +

+ {strategyDetails.description} +

+ {/if} +
+
+

Deployments

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

{error}

+

{errorDetails}

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

- {strategyDetails.name} -

-

- {strategyDetails.description} -

-
- -
-{:else if error} -
-

{error}

-

{errorDetails}

-
-{/if} diff --git a/packages/ui-components/src/lib/components/deployment/StrategyShortTile.svelte b/packages/ui-components/src/lib/components/deployment/StrategyShortTile.svelte new file mode 100644 index 000000000..92a2575f9 --- /dev/null +++ b/packages/ui-components/src/lib/components/deployment/StrategyShortTile.svelte @@ -0,0 +1,24 @@ + + +
+

{strategyDetails?.name}

+

{strategyDetails?.short_description}

+
diff --git a/packages/ui-components/src/lib/components/detail/DepositOrWithdrawButtons.svelte b/packages/ui-components/src/lib/components/detail/DepositOrWithdrawButtons.svelte index 6ec384d48..d72fe8732 100644 --- a/packages/ui-components/src/lib/components/detail/DepositOrWithdrawButtons.svelte +++ b/packages/ui-components/src/lib/components/detail/DepositOrWithdrawButtons.svelte @@ -10,12 +10,14 @@ action: 'deposit' | 'withdraw'; chainId: number; rpcUrl: string; + subgraphUrl: string; }) => void; export let vault: Vault; export let chainId: number; export let rpcUrl: string; export let query: CreateQueryResult; + export let subgraphUrl: string; diff --git a/packages/ui-components/src/lib/components/detail/OrderDetail.svelte b/packages/ui-components/src/lib/components/detail/OrderDetail.svelte index d5241bd0c..c1d3ce749 100644 --- a/packages/ui-components/src/lib/components/detail/OrderDetail.svelte +++ b/packages/ui-components/src/lib/components/detail/OrderDetail.svelte @@ -34,6 +34,7 @@ action: 'deposit' | 'withdraw'; chainId: number; rpcUrl: string; + subgraphUrl: string; }) => void) | undefined = undefined; export let handleOrderRemoveModal: @@ -73,7 +74,9 @@ $: orderDetailQuery = createQuery({ queryKey: [id, QKEY_ORDER + id], - queryFn: () => getOrder(subgraphUrl, id), + queryFn: () => { + return getOrder(subgraphUrl, id); + }, enabled: !!subgraphUrl }); @@ -162,6 +165,7 @@ {rpcUrl} query={orderDetailQuery} {handleDepositOrWithdrawModal} + {subgraphUrl} /> {/if} @@ -188,7 +192,7 @@
diff --git a/packages/ui-components/src/lib/components/detail/TanstackOrderQuote.svelte b/packages/ui-components/src/lib/components/detail/TanstackOrderQuote.svelte index 265bd9067..2d63397bd 100644 --- a/packages/ui-components/src/lib/components/detail/TanstackOrderQuote.svelte +++ b/packages/ui-components/src/lib/components/detail/TanstackOrderQuote.svelte @@ -40,7 +40,7 @@ }; $: orderQuoteQuery = createQuery({ - queryKey: [QKEY_ORDER_QUOTE + id], + queryKey: [QKEY_ORDER_QUOTE + id, id], queryFn: () => getOrderQuote([order], rpcUrl), enabled: !!id && enabled, refetchInterval: 10000 diff --git a/packages/ui-components/src/lib/components/detail/VaultDetail.svelte b/packages/ui-components/src/lib/components/detail/VaultDetail.svelte index 4ba56f815..63bdca277 100644 --- a/packages/ui-components/src/lib/components/detail/VaultDetail.svelte +++ b/packages/ui-components/src/lib/components/detail/VaultDetail.svelte @@ -30,6 +30,7 @@ action: 'deposit' | 'withdraw'; chainId: number; rpcUrl: string; + subgraphUrl: string; }) => void) | undefined = undefined; export let id: string; @@ -97,6 +98,7 @@ {rpcUrl} query={vaultDetailQuery} {handleDepositOrWithdrawModal} + {subgraphUrl} /> {:else if handleDepositModal && handleWithdrawModal && $walletAddressMatchesOrBlank?.(data.owner)} -
-
-