diff --git a/CHANGELOG.md b/CHANGELOG.md index be68587..e5feb8c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,18 @@ All notable changes to this project will be documented in this file. +## [0.2.7] - 2024-11-18 + +### ๐Ÿš€ Features + +- Asset Hub transactions with fee currency + - Autofill tip with asset + - Pass asset id into transaction constructor to properly select fee currency + +### ๐Ÿงช Testing + +- Test cases to cover partial withdrawal and Asset Gub transfers + ## [0.2.6] - 2024-11-01 ### ๐Ÿš€ Features diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 10ddf6a..0066569 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,13 +1,30 @@ ## Preparing development environment It's possible to mimic to spawn chopsticks instances in parallel for development purposes. -Chopsticks Dockerfile exposes 4 ports (8000, 8500, 9000, 9500), so you can spawn 4 instances of chopsticks and each one of them will look at different RPC. +Chopsticks Dockerfile exposes 4 ports (8000, 8500, 9000, 9500), so you can spawn up to 4 instances of chopsticks and each one of them will look at different RPC (note that those will be different chains). Note that the RPCs are not real, so the changes made on one chopsticks instance will not affect the others. 1. `cd chopsticks` 2. `docker compose up`, in case you want to just 2 instances edit the docker-compose.yml file 3. start the app with `KALATORI_CONFIG` environment variable pointing to `configs/chopsticks.toml` +## Running tests locally + +While having the kalatori app running. You can run the tests locally by running the following command: + +```bash +cd tests/kalatori-api-test-suite +yarn +yarn test +``` + +You can run specific test similarly to the following command: + +```bash +cd tests/kalatori-api-test-suite +yarn test -t "should create, repay, and automatically withdraw an order in USDC" +``` + ## Version Bumping and Release Process When you make changes that require a new version of the project, follow these steps to bump the version: diff --git a/Cargo.lock b/Cargo.lock index 1ffe6a8..a1f0bf9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -902,9 +902,9 @@ dependencies = [ [[package]] name = "frame-metadata" -version = "16.0.0" +version = "17.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87cf1549fba25a6fcac22785b61698317d958e96cac72a59102ea45b9ae64692" +checksum = "701bac17e9b55e0f95067c428ebcb46496587f08e8cf4ccc0fe5903bea10dbb8" dependencies = [ "cfg-if", "parity-scale-codec", @@ -1275,9 +1275,9 @@ dependencies = [ [[package]] name = "impl-codec" -version = "0.6.0" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba6a270039626615617f3f36d15fc827041df3b78c439da2cadfa47455a77f2f" +checksum = "b67aa010c1e3da95bf151bd8b4c059b2ed7e75387cdb969b4f8f2723a43f9941" dependencies = [ "parity-scale-codec", ] @@ -1473,7 +1473,7 @@ dependencies = [ [[package]] name = "kalatori" -version = "0.2.6" +version = "0.2.7" dependencies = [ "ahash", "async-lock", @@ -1975,9 +1975,9 @@ dependencies = [ [[package]] name = "primitive-types" -version = "0.12.2" +version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b34d9fd68ae0b74a41b21c03c2f62847aa0ffea044eee893b4c140b37e244e2" +checksum = "d15600a7d856470b7d278b3fe0e311fe28c2526348549f8ef2ff7db3299c87f5" dependencies = [ "fixed-hash", "impl-codec", @@ -2716,7 +2716,7 @@ checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" [[package]] name = "substrate-constructor" version = "0.1.0" -source = "git+https://github.com/Alzymologist/substrate-constructor#540559207e640bfa158358cdbf736eb488c57100" +source = "git+https://github.com/Alzymologist/substrate-constructor#09c877f9ef228508e9d1fbeb3f66bcb782b70bb6" dependencies = [ "bitvec", "external-memory-tools", @@ -2730,13 +2730,12 @@ dependencies = [ "sp-crypto-hashing", "substrate-crypto-light", "substrate_parser", - "thiserror", ] [[package]] name = "substrate-crypto-light" version = "0.1.0" -source = "git+https://github.com/Alzymologist/substrate-crypto-light#8b5d43144a622f4fdbd8f4147a302e6ff4af894c" +source = "git+https://github.com/Alzymologist/substrate-crypto-light#e1c07e19ccb19862accc2771d3b7ec0f86c80a36" dependencies = [ "base58", "blake2b_simd", @@ -2756,7 +2755,7 @@ dependencies = [ [[package]] name = "substrate_parser" version = "0.6.1" -source = "git+https://github.com/Alzymologist/substrate-parser#09f98462f5179bf3c5b6c323c53f7438caf03f06" +source = "git+https://github.com/Alzymologist/substrate-parser#ed388d969b803b4713622378bb716b2032b2c7fe" dependencies = [ "bitvec", "external-memory-tools", @@ -3187,9 +3186,9 @@ checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" [[package]] name = "uint" -version = "0.9.5" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76f64bba2c53b04fcab63c01a7d7427eadc821e3bc48c34dc9ba29c501164b52" +checksum = "909988d098b2f738727b161a106cfc7cab00c539c2687a8836f8e565976fb53e" dependencies = [ "byteorder", "crunchy", diff --git a/Cargo.toml b/Cargo.toml index d2eac2b..7b67f27 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "kalatori" authors = ["Alzymologist Oy "] -version = "0.2.6" +version = "0.2.7" edition = "2021" description = "A gateway daemon for Kalatori." license = "GPL-3.0-or-later" @@ -27,10 +27,10 @@ serde = { version = "1", features = ["derive", "rc"] } tracing = "0.1" scale-info = "2" axum-macros = "0.4" -primitive-types = { version = "0.12", features = ["codec"] } +primitive-types = { version = "0.13", features = ["codec"] } jsonrpsee = { version = "0.24", features = ["ws-client"] } thiserror = "1" -frame-metadata = "16" +frame-metadata = "17" const-hex = "1" codec = { package = "parity-scale-codec", version = "3", features = [ "chain-error", diff --git a/src/chain/payout.rs b/src/chain/payout.rs index 07e89d0..ea255c9 100644 --- a/src/chain/payout.rs +++ b/src/chain/payout.rs @@ -118,6 +118,7 @@ pub async fn payout( block, block_number, 0, + currency.asset_id, )?; let sign_this = batch_transaction diff --git a/src/chain/tracker.rs b/src/chain/tracker.rs index 316b3dc..3d483c2 100644 --- a/src/chain/tracker.rs +++ b/src/chain/tracker.rs @@ -116,52 +116,39 @@ pub fn start_chain_watch( break; } - match transfer_events( - &client, - &block, - &watcher.metadata, - ) - .await { - Ok(events) => { - let mut id_remove_list = Vec::new(); - let now = SystemTime::now().duration_since(SystemTime::UNIX_EPOCH).unwrap().as_millis() as u64; + let mut id_remove_list = Vec::new(); + let now = SystemTime::now().duration_since(SystemTime::UNIX_EPOCH).unwrap().as_millis() as u64; - for (id, invoice) in &watched_accounts { - if events.iter().any(|event| was_balance_received_at_account(&invoice.address, &event.0.fields)) { - match invoice.check(&client, &watcher, &block).await { - Ok(true) => { - state.order_paid(id.clone()).await; - id_remove_list.push(id.to_owned()); - } - Ok(false) => (), - Err(e) => { - tracing::warn!("account fetch error: {0:?}", e); - } - } - } else if invoice.death.0 >= now { - match invoice.check(&client, &watcher, &block).await { - Ok(paid) => { - if paid { - state.order_paid(id.clone()).await; - } - - id_remove_list.push(id.to_owned()); - } - Err(e) => { - tracing::warn!("account fetch error: {0:?}", e); - } - } + // Important! There used to be a significant oprimisation that + // watched events and checked only accounts that have tranfers into + // them in given block; this was found to be unreliable: there are + // ways to transfer funds without emitting a transfer event (one + // notable example is through asset exchange procedure directed + // straight into invoice account), and probably even without any + // reliably expected event (through XCM). Thus we just scan all + // accounts, every time. Please submit a PR or an issue if you + // figure out a reliable optimization for this. + for (id, invoice) in &watched_accounts { + match invoice.check(&client, &watcher, &block).await { + Ok(true) => { + state.order_paid(id.clone()).await; + id_remove_list.push(id.to_owned()); + }, + Ok(false) => { + if invoice.death.0 <= now { + id_remove_list.push(id.to_owned()); } + }, + Err(e) => { + tracing::warn!("account fetch error: {0:?}", e); } - for id in id_remove_list { - watched_accounts.remove(&id); - } - }, - Err(e) => { - tracing::warn!("Events fetch error {e} at {}", chain.name); - break; - }, } + } + + for id in id_remove_list { + watched_accounts.remove(&id); + }; + tracing::debug!("Block {} from {} processed successfully", block.to_string(), chain.name); } ChainTrackerRequest::WatchAccount(request) => { diff --git a/src/chain/utils.rs b/src/chain/utils.rs index cee4e54..38f8cbd 100644 --- a/src/chain/utils.rs +++ b/src/chain/utils.rs @@ -214,6 +214,7 @@ pub fn construct_batch_transaction( block: BlockHash, block_number: u32, nonce: u32, + asset: Option, ) -> Result { let mut transaction_to_fill = TransactionToFill::init(&mut (), metadata, genesis_hash.0)?; @@ -296,6 +297,9 @@ pub fn construct_batch_transaction( transaction_to_fill.populate_block_info(Some(block.0), Some(block_number.into())); transaction_to_fill.populate_nonce(nonce); + if let Some(asset) = asset { + transaction_to_fill.try_default_tip_assets_in_given_asset(&mut (), metadata, asset); + } for ext in &mut transaction_to_fill.extensions { if ext.finalize().is_none() { diff --git a/tests/kalatori-api-test-suite/tests/order.test.ts b/tests/kalatori-api-test-suite/tests/order.test.ts index 2644e8f..68a222e 100644 --- a/tests/kalatori-api-test-suite/tests/order.test.ts +++ b/tests/kalatori-api-test-suite/tests/order.test.ts @@ -233,7 +233,7 @@ describe('Order Endpoint Blackbox Tests', () => { expect(repaidOrderDetails.withdrawal_status).toBe('completed'); }, 100000); - it.skip('should create, repay, and automatically withdraw an order in USDC', async () => { + it('should create, repay, and automatically withdraw an order in USDC', async () => { const orderId = generateRandomOrderId(); await createOrder(orderId, usdcOrderData); const orderDetails = await getOrderDetails(orderId); @@ -255,7 +255,46 @@ describe('Order Endpoint Blackbox Tests', () => { expect(repaidOrderDetails.withdrawal_status).toBe('completed'); }, 50000); - it.skip('should not automatically withdraw an order until fully repaid', async () => { + it('should not automatically withdraw DOT order until fully repaid', async () => { + const orderId = generateRandomOrderId(); + await createOrder(orderId, dotOrderData); + const orderDetails = await getOrderDetails(orderId); + const paymentAccount = orderDetails.payment_account; + expect(paymentAccount).toBeDefined(); + + const halfAmount = orderDetails.amount/2; + + // Partial repayment + await transferFunds( + orderDetails.currency.rpc_url, + paymentAccount, + halfAmount, + orderDetails.currency.asset_id + ); + // lets wait for the changes to get propagated on chain and app to catch them + await new Promise(resolve => setTimeout(resolve, 15000)); + + let repaidOrderDetails = await getOrderDetails(orderId); + expect(repaidOrderDetails.payment_status).toBe('pending'); + expect(repaidOrderDetails.withdrawal_status).toBe('waiting'); + + // Full repayment + await transferFunds( + orderDetails.currency.rpc_url, + paymentAccount, + halfAmount+5, + orderDetails.currency.asset_id + ); + + // lets wait for the changes to get propagated on chain and app to catch them + await new Promise(resolve => setTimeout(resolve, 15000)); + + repaidOrderDetails = await getOrderDetails(orderId); + expect(repaidOrderDetails.payment_status).toBe('paid'); + expect(repaidOrderDetails.withdrawal_status).toBe('completed'); + }, 100000); + + it('should not automatically withdraw USDC order until fully repaid', async () => { const orderId = generateRandomOrderId(); await createOrder(orderId, usdcOrderData); const orderDetails = await getOrderDetails(orderId); @@ -292,9 +331,9 @@ describe('Order Endpoint Blackbox Tests', () => { repaidOrderDetails = await getOrderDetails(orderId); expect(repaidOrderDetails.payment_status).toBe('paid'); expect(repaidOrderDetails.withdrawal_status).toBe('completed'); - }, 50000); + }, 100000); - it.skip('should not update order if received payment in wrong currency', async () => { + it('should not update order if received payment in wrong currency', async () => { const orderId = generateRandomOrderId(); await createOrder(orderId, usdcOrderData); const orderDetails = await getOrderDetails(orderId); @@ -317,7 +356,7 @@ describe('Order Endpoint Blackbox Tests', () => { expect(repaidOrderDetails.withdrawal_status).toBe('waiting'); }, 50000); - it('should be able to force withdraw partially repayed order', async () => { + it('should be able to force withdraw partially repayed DOT order', async () => { const orderId = generateRandomOrderId(); await createOrder(orderId, dotOrderData); const orderDetails = await getOrderDetails(orderId); @@ -342,6 +381,31 @@ describe('Order Endpoint Blackbox Tests', () => { expect(forcedOrderDetails.withdrawal_status).toBe('forced'); }, 100000); + it('should be able to force withdraw partially repayed USDC order', async () => { + const orderId = generateRandomOrderId(); + await createOrder(orderId, usdcOrderData); + const orderDetails = await getOrderDetails(orderId); + const paymentAccount = orderDetails.payment_account; + expect(paymentAccount).toBeDefined(); + + await transferFunds(orderDetails.currency.rpc_url, paymentAccount, usdcOrderData.amount/2); + + // lets wait for the changes to get propagated on chain and app to catch them + await new Promise(resolve => setTimeout(resolve, 15000)); + + const partiallyRepaidOrderDetails = await getOrderDetails(orderId); + expect(partiallyRepaidOrderDetails.payment_status).toBe('pending'); + expect(partiallyRepaidOrderDetails.withdrawal_status).toBe('waiting'); + + const response = await request(baseUrl) + .post(`/v2/order/${orderId}/forceWithdrawal`); + expect(response.status).toBe(201); + + let forcedOrderDetails = await getOrderDetails(orderId); + expect(forcedOrderDetails.payment_status).toBe('pending'); + expect(forcedOrderDetails.withdrawal_status).toBe('forced'); + }, 100000); + it('should return 404 for non-existing order on force withdrawal', async () => { const nonExistingOrderId = 'nonExistingOrder123'; const response = await request(baseUrl)