diff --git a/crates/cli/src/commands/vault/deposit.rs b/crates/cli/src/commands/vault/deposit.rs index f59498bd1..474ea73be 100644 --- a/crates/cli/src/commands/vault/deposit.rs +++ b/crates/cli/src/commands/vault/deposit.rs @@ -17,18 +17,16 @@ impl Execute for Deposit { println!("----- Transaction (1/2): Approve ERC20 token spend -----"); deposit_args - .execute( - tx_args, - |status| { - display_write_transaction_status(status); - }, - |status| { - display_write_transaction_status(status); - }, - || { - println!("----- Transaction (2/2): Deposit tokens into Orderbook -----"); - }, - ) + .execute_approve(tx_args.clone(), |status| { + display_write_transaction_status(status); + }) + .await?; + + println!("----- Transaction (2/2): Deposit tokens into Orderbook -----"); + deposit_args + .execute_deposit(tx_args, |status| { + display_write_transaction_status(status); + }) .await?; Ok(()) } diff --git a/crates/common/src/deposit.rs b/crates/common/src/deposit.rs index f360036a9..c9e108c0f 100644 --- a/crates/common/src/deposit.rs +++ b/crates/common/src/deposit.rs @@ -4,9 +4,10 @@ use alloy_ethers_typecast::transaction::WriteTransaction; use alloy_ethers_typecast::{ethers_address_to_alloy, transaction::WriteTransactionStatus}; use alloy_primitives::{hex::FromHexError, Address, U256}; use rain_orderbook_bindings::{IOrderBookV3::depositCall, IERC20::approveCall}; +use serde::{Deserialize, Serialize}; use std::convert::TryInto; -#[derive(Clone)] +#[derive(Clone, Serialize, Deserialize)] pub struct DepositArgs { pub token: String, pub vault_id: U256, @@ -33,28 +34,8 @@ impl DepositArgs { } } - pub async fn execute< - A: Fn(WriteTransactionStatus), - D: Fn(WriteTransactionStatus), - S: Fn(), - >( - &self, - transaction_args: TransactionArgs, - approve_transaction_status_changed: A, - deposit_transaction_status_changed: D, - approve_transaction_success: S, - ) -> Result<(), WritableTransactionExecuteError> { - self.execute_approve(transaction_args.clone(), approve_transaction_status_changed) - .await?; - (approve_transaction_success)(); - self.execute_deposit(transaction_args, deposit_transaction_status_changed) - .await?; - - Ok(()) - } - /// Execute ERC20 approve call - async fn execute_approve)>( + pub async fn execute_approve)>( &self, transaction_args: TransactionArgs, transaction_status_changed: S, @@ -81,7 +62,7 @@ impl DepositArgs { } /// Execute OrderbookV3 deposit call - async fn execute_deposit)>( + pub async fn execute_deposit)>( &self, transaction_args: TransactionArgs, transaction_status_changed: S, diff --git a/crates/common/src/transaction.rs b/crates/common/src/transaction.rs index df21a2fee..149f64582 100644 --- a/crates/common/src/transaction.rs +++ b/crates/common/src/transaction.rs @@ -7,6 +7,7 @@ use alloy_ethers_typecast::{ }; use alloy_primitives::{hex::FromHexError, Address, U256}; use alloy_sol_types::SolCall; +use serde::{Deserialize, Serialize}; use thiserror::Error; #[derive(Error, Debug)] @@ -25,7 +26,7 @@ pub enum TransactionArgsError { LedgerClient(#[from] LedgerClientError), } -#[derive(Clone)] +#[derive(Clone, Serialize, Deserialize)] pub struct TransactionArgs { pub orderbook_address: String, pub derivation_index: Option, diff --git a/flake.lock b/flake.lock index 905653663..ded469b5d 100644 --- a/flake.lock +++ b/flake.lock @@ -230,11 +230,11 @@ "rust-overlay": "rust-overlay" }, "locked": { - "lastModified": 1706035670, - "narHash": "sha256-MK4o0vjQEreGJSoyl25X+ahHMo7ow702VN7ycBc2FrM=", + "lastModified": 1705721052, + "narHash": "sha256-bLXm81a98dujI3xwKsSQMB5/3d9fPJalIQyn/PzpcBo=", "owner": "rainprotocol", "repo": "rainix", - "rev": "246bf330f2bc8af10c121a329b4a909215b58e29", + "rev": "f0a66d3f0e12d8af1c1c0a1cb1e1704b77452f3e", "type": "github" }, "original": { diff --git a/flake.nix b/flake.nix index e0f6fe438..61d473829 100644 --- a/flake.nix +++ b/flake.nix @@ -27,6 +27,7 @@ typeshare crates/subgraph/src/types/order.rs --lang=typescript --output-file=tauri-app/src/lib/typeshare/order.ts; typeshare crates/subgraph/src/types/orders.rs --lang=typescript --output-file=tauri-app/src/lib/typeshare/orders.ts; typeshare tauri-app/src-tauri/src/toast.rs --lang=typescript --output-file=tauri-app/src/lib/typeshare/toast.ts; + typeshare tauri-app/src-tauri/src/transaction_status.rs --lang=typescript --output-file=tauri-app/src/lib/typeshare/transactionStatus.ts; ''; additionalBuildInputs = [ pkgs.typeshare diff --git a/tauri-app/src-tauri/Cargo.lock b/tauri-app/src-tauri/Cargo.lock index 8259c7437..c69e4f369 100644 --- a/tauri-app/src-tauri/Cargo.lock +++ b/tauri-app/src-tauri/Cargo.lock @@ -879,7 +879,7 @@ checksum = "d38f2da7a0a2c4ccf0065be06397cc26a81f4e528be095826eee9d4adbb8c60f" dependencies = [ "byteorder", "fnv", - "uuid 1.6.1", + "uuid 1.7.0", ] [[package]] @@ -909,9 +909,9 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "chrono" -version = "0.4.31" +version = "0.4.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f2c685bad3eb3d45a01354cedb7d5faa66194d1d58ba6e267a8de788f79db38" +checksum = "41daef31d7a747c5c847246f36de49ced6f7403b4cdabc807a97b5cc184cda7a" dependencies = [ "android-tzdata", "iana-time-zone", @@ -919,7 +919,7 @@ dependencies = [ "num-traits", "serde", "wasm-bindgen", - "windows-targets 0.48.5", + "windows-targets 0.52.0", ] [[package]] @@ -3475,7 +3475,7 @@ dependencies = [ "serde_json", "time", "url", - "uuid 1.6.1", + "uuid 1.7.0", ] [[package]] @@ -5277,7 +5277,7 @@ dependencies = [ "rkyv_derive", "seahash", "tinyvec", - "uuid 1.6.1", + "uuid 1.7.0", ] [[package]] @@ -6383,7 +6383,7 @@ dependencies = [ "serde", "tao-macros", "unicode-segmentation", - "uuid 1.6.1", + "uuid 1.7.0", "windows 0.39.0", "windows-implement", "x11-dl", @@ -6464,7 +6464,7 @@ dependencies = [ "thiserror", "tokio", "url", - "uuid 1.6.1", + "uuid 1.7.0", "webkit2gtk", "webview2-com", "windows 0.39.0", @@ -6476,6 +6476,8 @@ version = "0.0.0" dependencies = [ "alloy-ethers-typecast", "alloy-primitives", + "alloy-sol-types", + "chrono", "rain_orderbook_common", "rain_orderbook_subgraph_queries", "serde", @@ -6484,6 +6486,7 @@ dependencies = [ "tauri-build", "tauri-cli", "typeshare", + "uuid 1.7.0", ] [[package]] @@ -6539,7 +6542,7 @@ dependencies = [ "thiserror", "time", "ureq", - "uuid 1.6.1", + "uuid 1.7.0", "walkdir", "windows-sys 0.48.0", "winreg 0.51.0", @@ -6623,7 +6626,7 @@ dependencies = [ "tauri-utils", "thiserror", "time", - "uuid 1.6.1", + "uuid 1.7.0", "walkdir", ] @@ -6667,7 +6670,7 @@ dependencies = [ "tauri-utils", "thiserror", "url", - "uuid 1.6.1", + "uuid 1.7.0", "webview2-com", "windows 0.39.0", ] @@ -6685,7 +6688,7 @@ dependencies = [ "raw-window-handle", "tauri-runtime", "tauri-utils", - "uuid 1.6.1", + "uuid 1.7.0", "webkit2gtk", "webview2-com", "windows 0.39.0", @@ -7347,11 +7350,12 @@ dependencies = [ [[package]] name = "uuid" -version = "1.6.1" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e395fcf16a7a3d8127ec99782007af141946b4795001f876d54fb0d55978560" +checksum = "f00cc9702ca12d3c81455259621e676d0f7251cec66a21e98fe2e9a37db93b2a" dependencies = [ "getrandom 0.2.12", + "serde", "sha1_smol", ] diff --git a/tauri-app/src-tauri/Cargo.toml b/tauri-app/src-tauri/Cargo.toml index 0b4b766cd..46bbc2666 100644 --- a/tauri-app/src-tauri/Cargo.toml +++ b/tauri-app/src-tauri/Cargo.toml @@ -18,7 +18,10 @@ rain_orderbook_common = { path = "../../crates/common" } rain_orderbook_subgraph_queries = { path = "../../crates/subgraph" } alloy-ethers-typecast = { git = "https://github.com/rainlanguage/alloy-ethers-typecast", rev = "47b2d9f073ff891254af6ece6f0efc140566d3d5" } alloy-primitives = "0.5.4" +alloy-sol-types = "0.5.4" typeshare = "1.0.1" +chrono = { version = "0.4.32", features = ["serde"] } +uuid = { version = "1.7.0", features = ["serde"] } [features] # this feature is used for production builds or when `devPath` points to the filesystem diff --git a/tauri-app/src-tauri/src/commands/vault.rs b/tauri-app/src-tauri/src/commands/vault.rs index 7665b8dc4..cd85b186c 100644 --- a/tauri-app/src-tauri/src/commands/vault.rs +++ b/tauri-app/src-tauri/src/commands/vault.rs @@ -1,6 +1,11 @@ -use rain_orderbook_common::subgraph::SubgraphArgs; -use rain_orderbook_subgraph_queries::types::vault::TokenVault as VaultDetail; -use rain_orderbook_subgraph_queries::types::vaults::TokenVault as VaultsListItem; +use crate::transaction_status::{SeriesPosition, TransactionStatusNoticeRwLock}; +use rain_orderbook_common::{ + deposit::DepositArgs, subgraph::SubgraphArgs, transaction::TransactionArgs, +}; +use rain_orderbook_subgraph_queries::types::{ + vault::TokenVault as VaultDetail, vaults::TokenVault as VaultsListItem, +}; +use tauri::AppHandle; #[tauri::command] pub async fn vaults_list(subgraph_args: SubgraphArgs) -> Result, String> { @@ -23,3 +28,48 @@ pub async fn vault_detail(id: String, subgraph_args: SubgraphArgs) -> Result Result<(), String> { + let tx_status_notice = TransactionStatusNoticeRwLock::new( + "Approve ERC20 token transfer".into(), + Some(SeriesPosition { + position: 1, + total: 2, + }), + ); + deposit_args + .execute_approve(transaction_args.clone(), |status| { + tx_status_notice.update_status_and_emit(app_handle.clone(), status); + }) + .await + .map_err(|e| { + let text = format!("{}", e); + tx_status_notice.set_failed_status_and_emit(app_handle.clone(), text.clone()); + text + })?; + + let tx_status_notice = TransactionStatusNoticeRwLock::new( + "Deposit tokens into Orderbook".into(), + Some(SeriesPosition { + position: 2, + total: 2, + }), + ); + deposit_args + .execute_deposit(transaction_args.clone(), |status| { + tx_status_notice.update_status_and_emit(app_handle.clone(), status); + }) + .await + .map_err(|e| { + let text = format!("{}", e); + tx_status_notice.set_failed_status_and_emit(app_handle.clone(), text.clone()); + text + })?; + + Ok(()) +} diff --git a/tauri-app/src-tauri/src/main.rs b/tauri-app/src-tauri/src/main.rs index ad0252925..3018aac09 100644 --- a/tauri-app/src-tauri/src/main.rs +++ b/tauri-app/src-tauri/src/main.rs @@ -1,9 +1,12 @@ // Prevents additional console window on Windows in release, DO NOT REMOVE!! #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] +pub mod toast; +pub mod transaction_status; + mod commands; use commands::chain::get_chainid; -use commands::vault::{vault_detail, vaults_list}; +use commands::vault::{vault_deposit, vault_detail, vaults_list}; use commands::wallet::get_address_from_ledger; fn main() { @@ -11,6 +14,7 @@ fn main() { .invoke_handler(tauri::generate_handler![ vaults_list, vault_detail, + vault_deposit, get_address_from_ledger, get_chainid ]) diff --git a/tauri-app/src-tauri/src/transaction_status.rs b/tauri-app/src-tauri/src/transaction_status.rs new file mode 100644 index 000000000..10eed4a9d --- /dev/null +++ b/tauri-app/src-tauri/src/transaction_status.rs @@ -0,0 +1,101 @@ +use alloy_ethers_typecast::transaction::WriteTransactionStatus; +use alloy_sol_types::SolCall; +use chrono::{DateTime, Utc}; +use serde::{Deserialize, Serialize}; +use std::sync::RwLock; +use tauri::{AppHandle, Manager}; +use typeshare::typeshare; +use uuid::Uuid; + +#[typeshare] +#[derive(Serialize, Deserialize, Clone, Debug)] +#[serde(tag = "type", content = "payload")] +pub enum TransactionStatus { + Initialized, + PendingPrepare, + PendingSign, + PendingSend, + Confirmed(String), + Failed(String), +} + +impl From> for TransactionStatus { + fn from(val: WriteTransactionStatus) -> Self { + match val { + WriteTransactionStatus::PendingPrepare(_) => TransactionStatus::PendingPrepare, + WriteTransactionStatus::PendingSign(_) => TransactionStatus::PendingSign, + WriteTransactionStatus::PendingSend(_) => TransactionStatus::PendingSend, + WriteTransactionStatus::Confirmed(receipt) => { + TransactionStatus::Confirmed(format!("{:?}", receipt.transaction_hash)) + } + } + } +} + +/// Position and Total number in a 'series' of transactions +/// i.e. position: 1, total: 2 would be represent "Transaction 1 of 2" +#[typeshare] +#[derive(Serialize, Deserialize, Clone, Debug)] +pub struct SeriesPosition { + pub position: u8, + pub total: u8, +} + +#[typeshare] +#[derive(Serialize, Deserialize, Clone, Debug)] +pub struct TransactionStatusNotice { + pub id: Uuid, + pub status: TransactionStatus, + pub created_at: DateTime, + + /// Human-readable label to display in the UI, describing the transaction i.e. "Approving ERC20 Token Spend" + pub label: String, + pub series_position: Option, +} + +pub struct TransactionStatusNoticeRwLock(RwLock); + +impl TransactionStatusNoticeRwLock { + pub fn new(label: String, series_position: Option) -> Self { + let notice = TransactionStatusNotice { + id: Uuid::new_v4(), + status: TransactionStatus::Initialized, + created_at: Utc::now(), + label, + series_position, + }; + let new_self = Self(RwLock::new(notice)); + + new_self + } + + pub fn update_status_and_emit( + &self, + app_handle: AppHandle, + status: WriteTransactionStatus, + ) { + self.update_status(status); + self.emit(app_handle); + } + + pub fn set_failed_status_and_emit(&self, app_handle: AppHandle, message: String) { + self.set_failed_status(message); + self.emit(app_handle); + } + + fn update_status(&self, status: WriteTransactionStatus) { + let mut notice = self.0.write().unwrap(); + notice.status = status.into(); + } + + fn set_failed_status(&self, message: String) { + let mut notice = self.0.write().unwrap(); + notice.status = TransactionStatus::Failed(message); + } + + fn emit(&self, app_handle: AppHandle) { + app_handle + .emit_all("transaction_status_notice", self.0.read().unwrap().clone()) + .unwrap(); + } +} diff --git a/tauri-app/src/lib/ModalVaultDeposit.svelte b/tauri-app/src/lib/ModalVaultDeposit.svelte index 24a942d81..9a73eeed9 100644 --- a/tauri-app/src/lib/ModalVaultDeposit.svelte +++ b/tauri-app/src/lib/ModalVaultDeposit.svelte @@ -1,16 +1,29 @@ @@ -33,6 +46,15 @@

+
+
+ Owner +
+

+ {vault.owner.id} +

+
+
Balance @@ -61,10 +83,13 @@
- - + +
diff --git a/tauri-app/src/lib/ModalVaultWithdraw.svelte b/tauri-app/src/lib/ModalVaultWithdraw.svelte index b37c7162e..fe8369b14 100644 --- a/tauri-app/src/lib/ModalVaultWithdraw.svelte +++ b/tauri-app/src/lib/ModalVaultWithdraw.svelte @@ -40,6 +40,15 @@

+
+
+ Owner +
+

+ {vault.owner.id} +

+
+
Balance @@ -73,7 +82,7 @@
- + + {/if} +
+ {:else if transactionStatusNotice.status.type === 'Failed'} + +
+
Transaction Failed
+
+ {transactionStatusNotice.status.payload} +
+
+ {/if} +
+ diff --git a/tauri-app/src/lib/stores/chain.ts b/tauri-app/src/lib/stores/chain.ts index 7d7fdfe19..e9f15009b 100644 --- a/tauri-app/src/lib/stores/chain.ts +++ b/tauri-app/src/lib/stores/chain.ts @@ -26,4 +26,15 @@ export async function updateChainId() { export const activeChain = derived(chainId, (val) => { return find(Object.values(chains), (c) => c.id === val); -}) \ No newline at end of file +}); + +export const activeChainHasBlockExplorer = derived(activeChain, (val) => { + return val && val.blockExplorers?.default !== undefined; +}) + +export function formatBlockExplorerTxUrl(txHash: string) { + const c = get(activeChain); + if(!c || !c.blockExplorers?.default) return; + + return `${c.blockExplorers?.default.url}/tx/${txHash}`; +} diff --git a/tauri-app/src/lib/stores/toasts.ts b/tauri-app/src/lib/stores/toasts.ts index 95c5d3afd..d9838adaa 100644 --- a/tauri-app/src/lib/stores/toasts.ts +++ b/tauri-app/src/lib/stores/toasts.ts @@ -37,4 +37,4 @@ function useToastsStore(autohideMs = 5000) { export const toasts = useToastsStore(); -export const toastsList = derived(toasts, (toasts) => sortBy(Object.values(toasts), [(val) => val.timestamp, (val) => val.id])) \ No newline at end of file +export const toastsList = derived(toasts, (toasts) => sortBy(Object.values(toasts), [(val) => val.timestamp, (val) => val.id])) diff --git a/tauri-app/src/lib/stores/transactionStatusNotice.ts b/tauri-app/src/lib/stores/transactionStatusNotice.ts new file mode 100644 index 000000000..b70bd5afc --- /dev/null +++ b/tauri-app/src/lib/stores/transactionStatusNotice.ts @@ -0,0 +1,39 @@ +import { derived, writable } from 'svelte/store'; +import { listen } from '@tauri-apps/api/event'; +import sortBy from 'lodash/sortBy'; + +import type { TransactionStatusNotice } from "$lib/typeshare/transactionStatus"; + +export type TransactionStatusNoticeStore = { [id: string]: TransactionStatusNotice }; + +function useTransactionStatusNoticeStore(autoCloseMs = 5000) { + const { subscribe, update } = writable({}); + + listen('transaction_status_notice', (event) => handleNotice(event.payload)); + + function handleNotice(payload: TransactionStatusNotice) { + update((val) => { + val[payload.id] = { ...payload }; + return val; + }); + + // Auto remove transaction status notice once transaction is failed or complete + if(payload.status.type === 'Failed' || payload.status.type === 'Confirmed') { + setTimeout(() => { + update((val) => { + const newVal = {...val}; + delete newVal[payload.id]; + return newVal; + }); + }, autoCloseMs); + } + } + + return { + subscribe + } +} + +export const transactionStatusNotices = useTransactionStatusNoticeStore(); + +export const transactionStatusNoticesList = derived(transactionStatusNotices, (obj) => sortBy(Object.values(obj), [(val) => new Date(val.created_at), (val) => val.id])) diff --git a/tauri-app/src/lib/stores/vaultDeposit.ts b/tauri-app/src/lib/stores/vaultDeposit.ts new file mode 100644 index 000000000..4534ffee3 --- /dev/null +++ b/tauri-app/src/lib/stores/vaultDeposit.ts @@ -0,0 +1,30 @@ +import { get } from 'svelte/store'; +import { invoke } from '@tauri-apps/api'; +import { rpcUrl, orderbookAddress, walletDerivationIndex } from './settings'; +import { chainId } from '$lib/stores/chain'; + +function useVaultDepositStore() { + async function call(vaultId: bigint, token: string, amount: bigint) { + await invoke("vault_deposit", { + depositArgs: { + vault_id: vaultId.toString(), + token, + amount: amount.toString(), + }, + transactionArgs: { + rpc_url: get(rpcUrl), + orderbook_address: get(orderbookAddress), + derivation_index: get(walletDerivationIndex), + chain_id: get(chainId), + max_priority_fee_per_gas: '400000000000', + max_fee_per_gas: '400000000000', + } + }); + } + + return { + call + } +} + +export const vaultDeposit = useVaultDepositStore(); \ No newline at end of file diff --git a/tauri-app/src/routes/+layout.svelte b/tauri-app/src/routes/+layout.svelte index bff84af63..7dc71384a 100644 --- a/tauri-app/src/routes/+layout.svelte +++ b/tauri-app/src/routes/+layout.svelte @@ -6,6 +6,8 @@ import Sidebar from '$lib/Sidebar.svelte'; import { toastsList } from '$lib/stores/toasts'; import AppToast from '$lib/AppToast.svelte'; + import { transactionStatusNoticesList } from '$lib/stores/transactionStatusNotice'; + import TransactionStatusNotice from '$lib/TransactionStatusNotice.svelte';
@@ -15,7 +17,10 @@ -
+
+ {#each $transactionStatusNoticesList as transactionStatusNotice} + + {/each} {#each $toastsList as toast} {/each} diff --git a/typeshare.toml b/typeshare.toml index 423a1bb56..fcd7fd97d 100644 --- a/typeshare.toml +++ b/typeshare.toml @@ -3,3 +3,5 @@ "BigInt" = "bigint" "BigDecimal" = "string" "Bytes" = "string" +"Uuid" = "string" +"DateTime" = "string"