diff --git a/src-tauri/migrations/2024-04-25-073069_create_tapplets/down.sql b/src-tauri/migrations/2024-04-25-073069_create_tapplets/down.sql index 989a1cb..b62e819 100644 --- a/src-tauri/migrations/2024-04-25-073069_create_tapplets/down.sql +++ b/src-tauri/migrations/2024-04-25-073069_create_tapplets/down.sql @@ -4,3 +4,4 @@ DROP TABLE tapplet_version; DROP TABLE tapplet_audit; DROP TABLE installed_tapplet; DROP TABLE dev_tapplet; +DROP TABLE tapplet_asset; diff --git a/src-tauri/migrations/2024-04-25-073069_create_tapplets/up.sql b/src-tauri/migrations/2024-04-25-073069_create_tapplets/up.sql index 1b38641..343961e 100644 --- a/src-tauri/migrations/2024-04-25-073069_create_tapplets/up.sql +++ b/src-tauri/migrations/2024-04-25-073069_create_tapplets/up.sql @@ -51,3 +51,11 @@ CREATE TABLE dev_tapplet ( display_name TEXT NOT NULL, UNIQUE(endpoint) ); + +CREATE TABLE tapplet_asset ( + id INTEGER PRIMARY KEY, + tapplet_id INTEGER, + icon_url TEXT NOT NULL, + background_url TEXT NOT NULL, + FOREIGN KEY (tapplet_id) REFERENCES tapplet(id) +); diff --git a/src-tauri/src/commands.rs b/src-tauri/src/commands.rs index 25a6018..cd49e50 100644 --- a/src-tauri/src/commands.rs +++ b/src-tauri/src/commands.rs @@ -1,13 +1,15 @@ use tari_wallet_daemon_client::types::AccountsGetBalancesResponse; -use tauri::{ self, State }; +use tauri::{ self, AppHandle, State }; use std::path::PathBuf; use crate::{ + constants::REGISTRY_URL, database::{ models::{ CreateDevTapplet, CreateInstalledTapplet, CreateTapplet, + CreateTappletAsset, CreateTappletAudit, CreateTappletVersion, DevTapplet, @@ -26,8 +28,16 @@ use crate::{ hash_calculator::calculate_checksum, interface::{ DevTappletResponse, InstalledTappletWithName, RegisteredTappletWithVersion, RegisteredTapplets }, rpc::{ balances, free_coins, make_request }, - tapplet_installer::{ check_extracted_files, delete_tapplet, download_file, extract_tar, get_tapp_download_path }, + tapplet_installer::{ + check_extracted_files, + delete_tapplet, + download_asset, + download_file_and_archive, + extract_tar, + get_tapp_download_path, + }, tapplet_server::start, + AssetServer, DatabaseConnection, ShutdownTokens, Tokens, @@ -128,6 +138,11 @@ pub async fn close_tapplet(installed_tapplet_id: i32, shutdown_tokens: State<'_, Ok(()) } +#[tauri::command] +pub fn get_assets_server_addr(state: tauri::State<'_, AssetServer>) -> Result { + Ok(format!("http://{}", state.addr)) +} + #[tauri::command] pub async fn download_and_extract_tapp( tapplet_id: i32, @@ -143,7 +158,7 @@ pub async fn download_and_extract_tapp( // download tarball let url = tapp_version.registry_url.clone(); let download_path = tapplet_path.clone(); - let handle = tauri::async_runtime::spawn(async move { download_file(&url, download_path).await }); + let handle = tauri::async_runtime::spawn(async move { download_file_and_archive(&url, download_path).await }); handle.await?.map_err(|_| Error::RequestError(FailedToDownload { url: tapp_version.registry_url }))?; //extract tarball @@ -196,10 +211,8 @@ pub fn read_tapp_registry_db(db_connection: State<'_, DatabaseConnection>) -> Re * REGISTERED TAPPLETS - FETCH DATA FROM MANIFEST JSON */ #[tauri::command] -pub async fn fetch_tapplets(db_connection: State<'_, DatabaseConnection>) -> Result<(), Error> { - let manifest_endpoint = String::from( - "https://raw.githubusercontent.com/karczuRF/tapp-registry/main/dist/tapplets-registry.manifest.json" - ); +pub async fn fetch_tapplets(app_handle: AppHandle, db_connection: State<'_, DatabaseConnection>) -> Result<(), Error> { + let manifest_endpoint = format!("{}/dist/tapplets-registry.manifest.json", REGISTRY_URL); let manifest_res = reqwest ::get(&manifest_endpoint).await .map_err(|_| RequestError(FetchManifestError { endpoint: manifest_endpoint.clone() }))? @@ -212,12 +225,11 @@ pub async fn fetch_tapplets(db_connection: State<'_, DatabaseConnection>) -> Res for tapplet_manifest in tapplets.registered_tapplets.values() { let inserted_tapplet = store.create(&CreateTapplet::from(tapplet_manifest))?; - let tapplet_db_id = inserted_tapplet.id; // for audit_data in tapplet_manifest.metadata.audits.iter() { // store.create( // &(CreateTappletAudit { - // tapplet_id: tapplet_db_id, + // tapplet_id: inserted_tapplet.id, // auditor: &audit_data.auditor, // report_url: &audit_data.report_url, // }) @@ -227,13 +239,27 @@ pub async fn fetch_tapplets(db_connection: State<'_, DatabaseConnection>) -> Res for (version, version_data) in tapplet_manifest.versions.iter() { store.create( &(CreateTappletVersion { - tapplet_id: tapplet_db_id, + tapplet_id: inserted_tapplet.id, version: &version, integrity: &version_data.integrity, registry_url: &version_data.registry_url, }) )?; } + + match store.get_tapplet_assets_by_tapplet_id(inserted_tapplet.id.unwrap())? { + Some(_) => {} + None => { + let tapplet_assets = download_asset(app_handle.clone(), inserted_tapplet.registry_id).await?; + store.create( + &(CreateTappletAsset { + tapplet_id: inserted_tapplet.id, + icon_url: &tapplet_assets.icon_url, + background_url: &tapplet_assets.background_url, + }) + )?; + } + } } Ok(()) } diff --git a/src-tauri/src/constants.rs b/src-tauri/src/constants.rs index 3607a03..242e4c3 100644 --- a/src-tauri/src/constants.rs +++ b/src-tauri/src/constants.rs @@ -1,2 +1,4 @@ pub const TAPPLETS_INSTALLED_DIR: &'static str = "tapplets_installed"; +pub const TAPPLETS_ASSETS_DIR: &'static str = "assets"; pub const DB_FILE_NAME: &'static str = "tari_universe.sqlite3"; +pub const REGISTRY_URL: &'static str = "https://raw.githubusercontent.com/karczuRF/tapp-registry/main"; diff --git a/src-tauri/src/database/models.rs b/src-tauri/src/database/models.rs index b204394..bd666e6 100644 --- a/src-tauri/src/database/models.rs +++ b/src-tauri/src/database/models.rs @@ -227,3 +227,39 @@ pub struct UpdateTappletAudit { pub auditor: String, pub report_url: String, } + +#[derive(Queryable, Selectable, Debug, Serialize)] +#[diesel(table_name = tapplet_asset)] +#[diesel(check_for_backend(diesel::sqlite::Sqlite))] +pub struct TappletAsset { + pub id: Option, + pub tapplet_id: Option, + pub icon_url: String, + pub background_url: String, +} + +#[derive(Insertable, Debug)] +#[diesel(table_name = tapplet_asset)] +pub struct CreateTappletAsset<'a> { + pub tapplet_id: Option, + pub icon_url: &'a str, + pub background_url: &'a str, +} + +impl<'a> From<&CreateTappletAsset<'a>> for UpdateTappletAsset { + fn from(create_tapplet_asset: &CreateTappletAsset) -> Self { + UpdateTappletAsset { + tapplet_id: create_tapplet_asset.tapplet_id, + icon_url: create_tapplet_asset.icon_url.to_string(), + background_url: create_tapplet_asset.background_url.to_string(), + } + } +} + +#[derive(Debug, AsChangeset)] +#[diesel(table_name = tapplet_asset)] +pub struct UpdateTappletAsset { + pub tapplet_id: Option, + pub icon_url: String, + pub background_url: String, +} diff --git a/src-tauri/src/database/schema.rs b/src-tauri/src/database/schema.rs index 3e5c558..68618be 100644 --- a/src-tauri/src/database/schema.rs +++ b/src-tauri/src/database/schema.rs @@ -33,6 +33,15 @@ diesel::table! { } } +diesel::table! { + tapplet_asset (id) { + id -> Nullable, + tapplet_id -> Nullable, + icon_url -> Text, + background_url -> Text, + } +} + diesel::table! { tapplet_audit (id) { id -> Nullable, @@ -54,6 +63,7 @@ diesel::table! { diesel::joinable!(installed_tapplet -> tapplet (tapplet_id)); diesel::joinable!(installed_tapplet -> tapplet_version (tapplet_version_id)); +diesel::joinable!(tapplet_asset -> tapplet (tapplet_id)); diesel::joinable!(tapplet_audit -> tapplet (tapplet_id)); diesel::joinable!(tapplet_version -> tapplet (tapplet_id)); @@ -61,6 +71,7 @@ diesel::allow_tables_to_appear_in_same_query!( dev_tapplet, installed_tapplet, tapplet, + tapplet_asset, tapplet_audit, tapplet_version, ); diff --git a/src-tauri/src/database/store.rs b/src-tauri/src/database/store.rs index 792521b..9bc5f8e 100644 --- a/src-tauri/src/database/store.rs +++ b/src-tauri/src/database/store.rs @@ -13,11 +13,14 @@ use crate::interface::InstalledTappletWithName; use crate::interface::TappletSemver; use super::models::CreateDevTapplet; +use super::models::CreateTappletAsset; use super::models::CreateTappletVersion; use super::models::CreateTappletAudit; use super::models::DevTapplet; +use super::models::TappletAsset; use super::models::UpdateDevTapplet; use super::models::UpdateInstalledTapplet; +use super::models::UpdateTappletAsset; use super::models::UpdateTappletVersion; use super::models::UpdateTappletAudit; @@ -121,6 +124,16 @@ impl SqliteStore { return Ok((boxed_tapplet, latest_version.tapplet_version)); } + + pub fn get_tapplet_assets_by_tapplet_id(&mut self, tapp_id: i32) -> Result, Error> { + use crate::database::schema::tapplet_asset::dsl::*; + + tapplet_asset + .filter(tapplet_id.eq(tapp_id)) + .first::(self.get_connection().deref_mut()) + .optional() + .map_err(|_| DatabaseError(FailedToRetrieveData { entity_name: "tapplet asset".to_string() })) + } } impl<'a> Store, UpdateTapplet> for SqliteStore { @@ -391,3 +404,55 @@ impl<'a> Store, UpdateDevTapplet> for SqliteSto .map_err(|_| DatabaseError(FailedToDelete { entity_name: "Dev Tapplet".to_string() })) } } + +impl<'a> Store, UpdateTappletAsset> for SqliteStore { + fn get_all(&mut self) -> Result, Error> { + use crate::database::schema::tapplet_asset::dsl::*; + + tapplet_asset + .load::(self.get_connection().deref_mut()) + .map_err(|_| DatabaseError(FailedToRetrieveData { entity_name: "Tapplet asset".to_string() })) + } + + fn get_by_id(&mut self, tapplet_asset_id: i32) -> Result { + use crate::database::schema::tapplet_asset::dsl::*; + + tapplet_asset + .filter(id.eq(tapplet_asset_id)) + .first::(self.get_connection().deref_mut()) + .map_err(|_| DatabaseError(FailedToRetrieveData { entity_name: "Tapplet asset".to_string() })) + } + + fn create(&mut self, item: &CreateTappletAsset) -> Result { + use crate::database::schema::tapplet_asset; + + diesel + ::insert_into(tapplet_asset::table) + .values(item) + .get_result(self.get_connection().deref_mut()) + .map_err(|_| + DatabaseError(FailedToCreate { + entity_name: "Tapplet asset".to_string(), + }) + ) + } + + fn update(&mut self, old: TappletAsset, new: &UpdateTappletAsset) -> Result { + use crate::database::schema::tapplet_asset::dsl::*; + + diesel + ::update(tapplet_asset.filter(id.eq(old.id))) + .set(new) + .execute(self.get_connection().deref_mut()) + .map_err(|_| DatabaseError(FailedToUpdate { entity_name: "Tapplet asset".to_string() })) + } + + fn delete(&mut self, entity: TappletAsset) -> Result { + use crate::database::schema::tapplet_asset::dsl::*; + + diesel + ::delete(tapplet_asset.filter(id.eq(entity.id))) + .execute(self.get_connection().deref_mut()) + .map_err(|_| DatabaseError(FailedToDelete { entity_name: "Tapplet asset".to_string() })) + } +} diff --git a/src-tauri/src/interface/tapplet.rs b/src-tauri/src/interface/tapplet.rs index 9d39539..91765c3 100644 --- a/src-tauri/src/interface/tapplet.rs +++ b/src-tauri/src/interface/tapplet.rs @@ -28,3 +28,9 @@ pub struct RegisteredTappletWithVersion { pub registered_tapp: Tapplet, pub tapp_version: TappletVersion, } + +#[derive(Serialize)] +pub struct TappletAssets { + pub icon_url: String, + pub background_url: String, +} diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 8fad62c..863d11f 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -1,5 +1,7 @@ +use constants::TAPPLETS_ASSETS_DIR; use diesel::SqliteConnection; use fs::{ get_config_file, get_data_dir, get_log_dir }; +use tapplet_server::start; use std::{ collections::HashMap, sync::{ Arc, Mutex }, thread::sleep, time::Duration }; use tauri::{ self, Manager }; use tokio_util::sync::CancellationToken; @@ -21,6 +23,7 @@ use commands::{ calculate_and_validate_tapp_checksum, launch_tapplet, close_tapplet, + get_assets_server_addr, download_and_extract_tapp, get_balances, get_free_coins, @@ -49,6 +52,10 @@ pub struct Tokens { #[derive(Default)] pub struct ShutdownTokens(Arc>>); pub struct DatabaseConnection(Arc>); +pub struct AssetServer { + pub addr: String, + pub cancel_token: CancellationToken, +} async fn try_get_tokens() -> (String, String) { loop { @@ -88,6 +95,12 @@ fn setup_tari_universe(app: &mut tauri::App) -> Result<(), Box Result<(), Error> { fs::remove_dir_all(tapplet_path).map_err(|_| IOError(FailedToDeleteTapplet { path })) } -pub async fn download_file(url: &str, tapplet_path: PathBuf) -> Result<(), anyhow::Error> { +pub async fn download_file_and_archive(url: &str, tapplet_path: PathBuf) -> Result<(), Error> { // Download the file let client = reqwest::Client::new(); let mut response = client @@ -102,3 +106,66 @@ pub fn get_tapp_download_path( Ok(tapplet_path) } + +async fn download_file(url: &str, dest: PathBuf) -> Result<(), Error> { + let client = reqwest::Client::new(); + let mut response = client + .get(url) + .send().await + .map_err(|_| RequestError(FailedToDownload { url: url.to_string() }))?; + + if response.status().is_success() { + let dest_parent = dest.parent().unwrap(); + let path = dest + .clone() + .into_os_string() + .into_string() + .map_err(|_| IOError(FailedToGetFilePath))?; + fs + ::create_dir_all(&dest_parent) + .map_err(|_| IOError(FailedToCreateDir { path: dest_parent.to_str().unwrap().to_owned() }))?; + + let mut file = fs::File::create(dest).map_err(|_| IOError(FailedToCreateFile { path: path.clone() }))?; + + while + let Some(chunk) = response.chunk().await.map_err(|_| RequestError(FailedToDownload { url: url.to_string() }))? + { + file.write_all(&chunk).map_err(|_| IOError(FailedToWriteFile { path: path.clone() }))?; + } + } else if response.status().is_server_error() { + println!("Download server error! Status: {:?}", response.status()); + } else { + println!("Download failed. Something else happened. Status: {:?}", response); + } + + Ok(()) +} + +fn get_or_create_tapp_asset_dir(tapp_root_dir: PathBuf, tapplet_name: &str) -> Result { + let tapp_asset_dir = tapp_root_dir.join(TAPPLETS_ASSETS_DIR).join(tapplet_name); + let path = tapp_asset_dir + .clone() + .into_os_string() + .into_string() + .map_err(|_| IOError(FailedToGetFilePath))?; + fs::create_dir_all(path.clone()).map_err(|_| IOError(FailedToCreateDir { path }))?; + return Ok(tapp_asset_dir); +} + +pub async fn download_asset(app_handle: tauri::AppHandle, tapplet_name: String) -> Result { + let tapp_root_dir: PathBuf = app_handle.path().app_data_dir().unwrap().to_path_buf(); + let tapp_asset_dir = get_or_create_tapp_asset_dir(tapp_root_dir, &tapplet_name)?; + let icon_url = format!("{}/src/{}/images/logo.svg", REGISTRY_URL, tapplet_name); + let background_url = format!("{}/src/{}/images/background.svg", REGISTRY_URL, tapplet_name); + + let icon_dest = tapp_asset_dir.join("logo.svg"); + let background_dest = tapp_asset_dir.join("background.svg"); + + download_file(&icon_url, icon_dest.clone()).await?; + download_file(&background_url, background_dest.clone()).await?; + + Ok(TappletAssets { + icon_url: icon_dest.into_os_string().into_string().unwrap(), + background_url: background_dest.into_os_string().into_string().unwrap(), + }) +} diff --git a/src/components/TappletsInstalled.tsx b/src/components/TappletsInstalled.tsx index c8605fa..a8537fd 100644 --- a/src/components/TappletsInstalled.tsx +++ b/src/components/TappletsInstalled.tsx @@ -5,16 +5,16 @@ import tariLogo from "../assets/tari.svg" import { NavLink } from "react-router-dom" import { TabKey } from "../views/Tabs" import { useDispatch, useSelector } from "react-redux" -import { installedTappletsSelectors } from "../store/installedTapplets/installedTapplets.selector" import { devTappletsSelectors } from "../store/devTapplets/devTapplets.selector" import { installedTappletsActions } from "../store/installedTapplets/installedTapplets.slice" import { devTappletsActions } from "../store/devTapplets/devTapplets.slice" import { DevTapplet, InstalledTappletWithName } from "@type/tapplet" import { useTranslation } from "react-i18next" +import { installedTappletsListSelector } from "../store/installedTapplets/installedTapplets.selector" export const TappletsInstalled: React.FC = () => { const { t } = useTranslation("components") - const installedTapplets = useSelector(installedTappletsSelectors.selectAll) + const installedTapplets = useSelector(installedTappletsListSelector) const devTapplets = useSelector(devTappletsSelectors.selectAll) const dispatch = useDispatch() @@ -39,7 +39,7 @@ export const TappletsInstalled: React.FC = () => { {installedTapplets.map((item, index) => ( - + diff --git a/src/components/TappletsRegistered.tsx b/src/components/TappletsRegistered.tsx index 5f76649..a672575 100644 --- a/src/components/TappletsRegistered.tsx +++ b/src/components/TappletsRegistered.tsx @@ -10,7 +10,6 @@ import { Typography, } from "@mui/material" import { InstallDesktop } from "@mui/icons-material" -import tariLogo from "../assets/tari.svg" import AddDevTappletDialog from "./AddDevTappletDialog" import { useDispatch, useSelector } from "react-redux" import { registeredTappletsSelectors } from "../store/registeredTapplets/registeredTapplets.selector" @@ -42,7 +41,7 @@ export const TappletsRegistered: React.FC = () => { {registeredTapplets.map((item) => ( - + handleInstall(item.id)} sx={{ marginLeft: 8 }}> diff --git a/src/store/installedTapplets/installedTapplets.selector.ts b/src/store/installedTapplets/installedTapplets.selector.ts index eb01f7b..d3d1d1e 100644 --- a/src/store/installedTapplets/installedTapplets.selector.ts +++ b/src/store/installedTapplets/installedTapplets.selector.ts @@ -1,16 +1,27 @@ import { createSelector } from "@reduxjs/toolkit" import { RootState } from "../store" import { installedTappletAdapter } from "./installedTapplets.slice" +import { registeredTappletsSelectors } from "../registeredTapplets/registeredTapplets.selector" const installedTappletsStateSelector = (state: RootState) => state.installedTapplets export const installedTappletsSelectors = installedTappletAdapter.getSelectors( (state) => state.installedTapplets.installedTapplets ) -const getAllInstalledTapplets = createSelector([installedTappletsStateSelector], (state) => state.installedTapplets) +export const installedTappletsListSelector = createSelector( + [installedTappletsSelectors.selectAll, registeredTappletsSelectors.selectAll], + (installedTapplets, registeredTapplets) => { + return installedTapplets.map((installedTapplet) => { + const registeredTapplet = registeredTapplets.find( + (tapplet) => tapplet.id === installedTapplet.installed_tapplet.tapplet_id + ) + return { ...installedTapplet, ...registeredTapplet } + }) + } +) + const isInitialized = createSelector([installedTappletsStateSelector], (state) => state.isInitialized) export const installedTappletsSelector = { - getAllInstalledTapplets, isInitialized, } diff --git a/src/store/registeredTapplets/registeredTapplets.action.ts b/src/store/registeredTapplets/registeredTapplets.action.ts index 6bf7fb1..c9d0476 100644 --- a/src/store/registeredTapplets/registeredTapplets.action.ts +++ b/src/store/registeredTapplets/registeredTapplets.action.ts @@ -12,7 +12,13 @@ export const initializeAction = () => ({ try { await invoke("fetch_tapplets") const registeredTapplets = await invoke("read_tapp_registry_db") - listenerApi.dispatch(registeredTappletsActions.initializeSuccess({ registeredTapplets })) + const assetsServerAddr = await invoke("get_assets_server_addr") + const tappletsWithAssets = registeredTapplets.map((tapp) => ({ + ...tapp, + logoAddr: `${assetsServerAddr}/${tapp.package_name}/logo.svg`, + backgroundAddr: `${assetsServerAddr}/${tapp.package_name}/background.svg`, + })) + listenerApi.dispatch(registeredTappletsActions.initializeSuccess({ registeredTapplets: tappletsWithAssets })) } catch (error) { listenerApi.dispatch(registeredTappletsActions.initializeFailure({ errorMsg: error as string })) } diff --git a/src/store/registeredTapplets/registeredTapplets.slice.ts b/src/store/registeredTapplets/registeredTapplets.slice.ts index e045a19..23f4ce6 100644 --- a/src/store/registeredTapplets/registeredTapplets.slice.ts +++ b/src/store/registeredTapplets/registeredTapplets.slice.ts @@ -1,5 +1,5 @@ import { PayloadAction, createEntityAdapter, createSlice } from "@reduxjs/toolkit" -import { RegisteredTapplet } from "@type/tapplet" +import { RegisteredTappletWithAssets } from "@type/tapplet" import { listenerMiddleware } from "../store.listener" import { initializeAction } from "./registeredTapplets.action" import { @@ -8,7 +8,7 @@ import { InitRegisteredTappletsFailurePayload, } from "./registeredTapplets.types" -export const registeredTappletAdapter = createEntityAdapter() +export const registeredTappletAdapter = createEntityAdapter() const registeredTappletsSlice = createSlice({ name: "registeredTapplets", diff --git a/src/store/registeredTapplets/registeredTapplets.types.ts b/src/store/registeredTapplets/registeredTapplets.types.ts index 45f6ce1..290911f 100644 --- a/src/store/registeredTapplets/registeredTapplets.types.ts +++ b/src/store/registeredTapplets/registeredTapplets.types.ts @@ -1,4 +1,4 @@ -import { RegisteredTapplet } from "../../types/tapplet" +import { RegisteredTapplet, RegisteredTappletWithAssets } from "../../types/tapplet" import { EntityState } from "@reduxjs/toolkit" export type TappletStoreState = { @@ -9,7 +9,7 @@ export type TappletStoreState = { export type InitRegisteredTappletsReqPayload = {} export type InitRegisteredTappletsSuccessPayload = { - registeredTapplets: RegisteredTapplet[] + registeredTapplets: RegisteredTappletWithAssets[] } export type InitRegisteredTappletsFailurePayload = { errorMsg: string diff --git a/src/types/invoke.ts b/src/types/invoke.ts index 05a1373..ac7b07f 100644 --- a/src/types/invoke.ts +++ b/src/types/invoke.ts @@ -11,4 +11,5 @@ declare module "@tauri-apps/api/core" { payload: { tappletId: string; installedTappletId: string } ): Promise function invoke(param: "get_balances", payload: {}): Promise // TODO use AccountsGetBalancesResponse from typescript-bindings packages after it's fixed + function invoke(param: "get_assets_server_addr"): Promise } diff --git a/src/types/tapplet.ts b/src/types/tapplet.ts index df73a92..bc03072 100644 --- a/src/types/tapplet.ts +++ b/src/types/tapplet.ts @@ -11,6 +11,11 @@ export type RegisteredTapplet = { category: string } +export type RegisteredTappletWithAssets = RegisteredTapplet & { + logoAddr: string + backgroundAddr: string +} + export type InstalledTapplet = { id: string tapplet_id: string