diff --git a/8-yield-derivatives/SRWA-Yield-Derivatives/README.md b/8-yield-derivatives/SRWA-Yield-Derivatives/README.md new file mode 100644 index 000000000..f3c5c9a83 --- /dev/null +++ b/8-yield-derivatives/SRWA-Yield-Derivatives/README.md @@ -0,0 +1,310 @@ +# SRWA Yield Derivatives + +### About + +This is a simple flexible staking blueprint designed to yield rewards for staking for a specific time. For the account staking the XRD (or other approved asset, like xUSDC), the rewards are taken from the ytXRD dedicated pool (or ytxUSDC). + +There is no locking, and the account can unstake at any time, while the rewards would be available only after a predefined time has elapsed. + +The package contains component with a simple frontend to demonstrate the functionality and end-user experience. The demo is also available here: https://yield-srwa.netlify.app/ + +### Usage + +#### dApp + +``` +`cd dApp` - go into the dApp folder +`cp .env-example .env` - change the name of the .env file +`npm run dev`: Starts the development server using Vite. +`npm run build`: Builds the project for production using TypeScript and Vite. +`npm run lint`: Lints the source code using ESLint. +`npm run preview`: Previews the production build using Vite. + +There is a parameter in the .env file named VITE_COMPONENT_ADDRESS, frontend is already connected with the testnet version, if you want to release your own component version, you should just update the component address. +``` + +#### Backend + +# Yield-Derivatives-Demo + +## Resim + +resim is a command line tool that is used to interact with local Radix Engine simulator during development. +All of the interactions with the backend are done with resim for now. + +On every start or whenever you want to try something new, first command should be `resim reset`. It deletes everything from the simulator. + +## Creating a new account + +Running this command: +`resim new-account ` +creates a new account on the simulator. When done the first time it will automatically set the account as default. Response will be something like this: + +`A new account has been created! +Account component address: account_sim1qdu23xcp4jcvurxvnap5e7994xzfza8e0myjaez0s73qd2wye3 +Public key: 0208beddb4a109910b5fc9ddfe8b370351bf3e6430874d2ae9e65e3a863b8b6bd6 +Private key: 4edf45bf7b6da8ac4d06fec8512c9b6d37a8288c9b39e5ebbb738e60e028a297 +NonFungibleGlobalId: resource_sim1qpugpy08q9mp8v9vzcs0y6yyzw5003ratjue48ag2j4s73casc:#1# +Account configuration in complete. Will use the above account as default.` + +Save the Account component address, Private and Public keys and NonFungibleGlobalId as you'll need it later. +When you make the account, it will have 1000 XRD on it by default. +If you want to see the details of the account you can use: + +`resim show ` + +## Publishing the package + +To publish the package run this command: + +`resim publish .` + +At the bottom of the response you'll get the package address (something like this `package_sim1pk3cmat8st4ja2ms8mjqy2e9ptk8y6cx40v4qnfrkgnxcp2krkpr92`), +save it as you'll need it for later. + +## Component Instantiation + +To instantiate the YieldDerivatives component run this command: + +`resim call-function YieldDerivatives instantiate` + +You'll get the component and resource addresses in the response, something like this: + +`└─ Component: component_sim1cq4kl9qul5nsd49u99x25fs8gclv2yd28a9g242l2x0zx4hh6kqfkn +└─ Resource: resource_sim1t4h3kupr5l95w6ufpuysl0afun0gfzzw7ltmk7y68ks5ekqh4cpx9w` + +Component address is the address of the instantiated component and it will be used for all of the transactions later on. +Resource address is the Admin Badge that will be used for creating the Proof for using the admin specific methods. +You can also get it with `resim show `. + +## Interacting with the app + +There are several transactions you can use in order to do things on the app. + +##### add_new_asset + +First transaction must be add_new_asset, it's creating the vault with assets that can be used for deposit and redeem. +When the asset is added, corresponding ytToken token is created (with initial amount of 1 000 000). +It is something that only admin can do so it requires admin badge. + +You can add new asset like this: + +`resim call-method add_new_asset("", ) --proofs ` + +Examle: +`resim call-method component_sim1cq4kl9qul5nsd49u99x25fs8gclv2yd28a9g242l2x0zx4hh6kqfkn add_new_asset("resource_sim1tknxxxxxxxxxradxrdxxxxxxxxx009923554798xxxxxxxxxakj8n3", 0.1) --proofs resource_sim1t4h3kupr5l95w6ufpuysl0afun0gfzzw7ltmk7y68ks5ekqh4cpx9w:1` + +or run it with this command: + +`resim run "./src/transactions/add_new_asset.rtm"` + +`CALL_METHOD +Address("component_sim1cptxxxxxxxxxfaucetxxxxxxxxx000527798379xxxxxxxxxhkrefh") +"lock_fee" +Decimal("5000"); + +CALL_METHOD +Address("account_tdx_2_128qp5d27yepw44ftmq9wa9jzps7tf3y7r20xw0v6xz20wt54s7muwp") +"create_proof_of_amount" +Address("resource_tdx_2_1t5v99d0s7njg7qe65fggvhsj69n4qfukhnhd3egj3j9s0rg0yupp6e") +Decimal("1"); + +CALL_METHOD +Address("component_tdx_2_1cq9kx6zhxwrnme6g08gkxnngq6hhsetv4u7z2y9t6pmp5t4xenlplg") +"add_new_asset" +Address("resource_sim1tknxxxxxxxxxradxrdxxxxxxxxx009923554798xxxxxxxxxakj8n3") +Decimal("0.1"); + +CALL_METHOD +Address("account_tdx_2_128qp5d27yepw44ftmq9wa9jzps7tf3y7r20xw0v6xz20wt54s7muwp") +"try_deposit_batch_or_refund" +Expression("ENTIRE_WORKTOP") +Enum<0u8>(); ` + +If XRD is the asset then the asset resource address will be resource_sim1tknxxxxxxxxxradxrdxxxxxxxxx009923554798xxxxxxxxxakj8n3. + +##### create_user_and_deposit_principal + +Next step is to create a new user and deposit principal token (which is added in previous step). + +It can be done with this ccommand: +`resim call-method create_user_and_deposit_principal :)` + +Example: +`resim call-method component_sim1cq4kl9qul5nsd49u99x25fs8gclv2yd28a9g242l2x0zx4hh6kqfkn create_user_and_deposit_principal resource_sim1tknxxxxxxxxxradxrdxxxxxxxxx009923554798xxxxxxxxxakj8n3:10` + +or run it with this command: + +`resim run "./src/transactions/create_user_and_deposit_principal.rtm"` + +`CALL_METHOD +Address("component_sim1cptxxxxxxxxxfaucetxxxxxxxxx000527798379xxxxxxxxxhkrefh") +"lock_fee" +Decimal("5000"); + +CALL_METHOD +Address("account_sim1c956qr3kxlgypxwst89j9yf24tjc7zxd4up38x37zr6q4jxdx9rhma") +"withdraw" +Address("resource_sim1tknxxxxxxxxxradxrdxxxxxxxxx009923554798xxxxxxxxxakj8n3") +Decimal("10"); + +TAKE_FROM_WORKTOP +Address("resource_sim1tknxxxxxxxxxradxrdxxxxxxxxx009923554798xxxxxxxxxakj8n3") +Decimal("10") +Bucket("DepositingBucket"); + +CALL_METHOD +Address("component_sim1cq4kl9qul5nsd49u99x25fs8gclv2yd28a9g242l2x0zx4hh6kqfkn") +"create_user_and_deposit_principal" +Bucket("DepositingBucket"); + +CALL_METHOD +Address("account_sim1c956qr3kxlgypxwst89j9yf24tjc7zxd4up38x37zr6q4jxdx9rhma") +"try_deposit_batch_or_refund" +Expression("ENTIRE_WORKTOP") +Enum<0u8>(); ` + +After running this command, user is created and stored in the app and his principal and yield balances are updated. + +Also you should see that you have the User Badge in resources if you run the command `resim show ` , something like this: +`{ amount: 1, resource address: resource_sim1qp2ahm386cw0hcxmyj88r4w249wqrgnyh7ncu66v3lqq2rrts3, name: "User Badge" }` + +##### deposit_principal + +Principal can be deposited with this function, but depositing is available if the user does not have an active staking deposit. +So, if you have already staked some tokens, you must redeem them first, than deposit new tokens. + +It can be done with this ccommand: +`resim call-method deposit_principal(:, )` + +Example: +`resim call-method component_sim1cq4kl9qul5nsd49u99x25fs8gclv2yd28a9g242l2x0zx4hh6kqfkn deposit_principal("resource_sim1tknxxxxxxxxxradxrdxxxxxxxxx009923554798xxxxxxxxxakj8n3:10", "resource_sim1nfmyl590n6dttggacglnqx98yu8y2k4vqxjakqeyueqvqtxcqf433s:#1#")` + +or run it with this command: + +`resim run "./src/transactions/deposit_principal.rtm"` + +`CALL_METHOD +Address("component_sim1cptxxxxxxxxxfaucetxxxxxxxxx000527798379xxxxxxxxxhkrefh") +"lock_fee" +Decimal("5000"); + +CALL_METHOD +Address("account_sim1c956qr3kxlgypxwst89j9yf24tjc7zxd4up38x37zr6q4jxdx9rhma") +"withdraw" +Address("resource_sim1tknxxxxxxxxxradxrdxxxxxxxxx009923554798xxxxxxxxxakj8n3") +Decimal("10"); + +TAKE_FROM_WORKTOP +Address("resource_sim1tknxxxxxxxxxradxrdxxxxxxxxx009923554798xxxxxxxxxakj8n3") +Decimal("10") +Bucket("DepositingBucket"); + +CALL_METHOD +Address("account_sim1c956qr3kxlgypxwst89j9yf24tjc7zxd4up38x37zr6q4jxdx9rhma") +"create_proof_of_non_fungibles" +Address("resource_sim1nfmyl590n6dttggacglnqx98yu8y2k4vqxjakqeyueqvqtxcqf433s") +Array( +NonFungibleLocalId("#1#") +); + +POP_FROM_AUTH_ZONE +Proof("UserBadge"); + +CALL_METHOD +Address("component_sim1cq4kl9qul5nsd49u99x25fs8gclv2yd28a9g242l2x0zx4hh6kqfkn") +"deposit_principal" +Bucket("DepositingBucket") +Proof("UserBadge"); + +CALL_METHOD +Address("account_sim1c956qr3kxlgypxwst89j9yf24tjc7zxd4up38x37zr6q4jxdx9rhma") +"try_deposit_batch_or_refund" +Expression("ENTIRE_WORKTOP") +Enum<0u8>(); ` + +User sends bucket that he wants to deposit. +When the deposit is made, principal and yield balance are updated. + +##### redeem + +User can redeem the deposit at any time. If redeem is activated before maturity date, user will not receive yield token award. + +It can be done with this ccommand: +`resim call-method redeem(, )` + +Example: +`resim call-method component_sim1cq4kl9qul5nsd49u99x25fs8gclv2yd28a9g242l2x0zx4hh6kqfkn redeem("resource_sim1tknxxxxxxxxxradxrdxxxxxxxxx009923554798xxxxxxxxxakj8n3", "resource_sim1nfmyl590n6dttggacglnqx98yu8y2k4vqxjakqeyueqvqtxcqf433s:#1#")` + +or run it with this command: + +`resim run "./src/transactions/redeem.rtm"` + +`CALL_METHOD +Address("component_sim1cptxxxxxxxxxfaucetxxxxxxxxx000527798379xxxxxxxxxhkrefh") +"lock_fee" +Decimal("5000"); + +CALL_METHOD +Address("account_sim1c956qr3kxlgypxwst89j9yf24tjc7zxd4up38x37zr6q4jxdx9rhma") +"create_proof_of_non_fungibles" +Address("resource_sim1ngzq4h9deqr8vmwrenzv2ajagf2nggc2kysshsxsphhxes4cn83ymk") +Array( +NonFungibleLocalId("#1#") +); + +POP_FROM_AUTH_ZONE +Proof("UserBadge"); + +CALL_METHOD +Address("component_sim1cq4kl9qul5nsd49u99x25fs8gclv2yd28a9g242l2x0zx4hh6kqfkn") +"redeem" +Address("resource_sim1tknxxxxxxxxxradxrdxxxxxxxxx009923554798xxxxxxxxxakj8n3") +Proof("UserBadge"); + +CALL_METHOD +Address("account_sim1c956qr3kxlgypxwst89j9yf24tjc7zxd4up38x37zr6q4jxdx9rhma") +"try_deposit_batch_or_refund" +Expression("ENTIRE_WORKTOP") +Enum<0u8>(); ` + +After this transaction user will receive his deposited tokens and reward if all conditions are met. + +##### get_users_deposit_balance + +You can check deposit balance with this command: +`resim call-method get_users_deposit_balance(, )` + +Example: +`resim call-method component_sim1cq4kl9qul5nsd49u99x25fs8gclv2yd28a9g242l2x0zx4hh6kqfkn get_users_deposit_balance("resource_sim1tknxxxxxxxxxradxrdxxxxxxxxx009923554798xxxxxxxxxakj8n3", "resource_sim1nfmyl590n6dttggacglnqx98yu8y2k4vqxjakqeyueqvqtxcqf433s:#1#")` + +or run it with this command: +`resim run "./src/transactions/get_users_deposit_balance.rtm"` + +`CALL_METHOD +Address("component_sim1cptxxxxxxxxxfaucetxxxxxxxxx000527798379xxxxxxxxxhkrefh") +"lock_fee" +Decimal("5000"); + +CALL_METHOD +Address("account_sim1c956qr3kxlgypxwst89j9yf24tjc7zxd4up38x37zr6q4jxdx9rhma") +"create_proof_of_non_fungibles" +Address("resource_sim1nfmyl590n6dttggacglnqx98yu8y2k4vqxjakqeyueqvqtxcqf433s") +Array( +NonFungibleLocalId("#1#") +); + +POP_FROM_AUTH_ZONE +Proof("UserBadge"); + +CALL_METHOD +Address("component_sim1cq4kl9qul5nsd49u99x25fs8gclv2yd28a9g242l2x0zx4hh6kqfkn") +"get_users_deposit_balance" +Address("resource_sim1tknxxxxxxxxxradxrdxxxxxxxxx009923554798xxxxxxxxxakj8n3") +Proof("UserBadge"); + +CALL_METHOD +Address("account_sim1c956qr3kxlgypxwst89j9yf24tjc7zxd4up38x37zr6q4jxdx9rhma") +"try_deposit_batch_or_refund" +Expression("ENTIRE_WORKTOP") +Enum<0u8>(); ` diff --git a/8-yield-derivatives/SRWA-Yield-Derivatives/Scrypto/.gitignore b/8-yield-derivatives/SRWA-Yield-Derivatives/Scrypto/.gitignore new file mode 100644 index 000000000..13120f9ed --- /dev/null +++ b/8-yield-derivatives/SRWA-Yield-Derivatives/Scrypto/.gitignore @@ -0,0 +1,2 @@ +/target +/coverage diff --git a/8-yield-derivatives/SRWA-Yield-Derivatives/Scrypto/Cargo.toml b/8-yield-derivatives/SRWA-Yield-Derivatives/Scrypto/Cargo.toml new file mode 100644 index 000000000..af198878a --- /dev/null +++ b/8-yield-derivatives/SRWA-Yield-Derivatives/Scrypto/Cargo.toml @@ -0,0 +1,36 @@ +[package] +name = "srwa-yield-derivatives" +version = "1.0.0" +edition = "2021" +resolver = "2" + +[dependencies] +sbor = { git = "https://github.com/radixdlt/radixdlt-scrypto", tag = "v1.1.1" } +scrypto = { git = "https://github.com/radixdlt/radixdlt-scrypto", tag = "v1.1.1" } + +[dev-dependencies] +transaction = { git = "https://github.com/radixdlt/radixdlt-scrypto", tag = "v1.1.1" } +radix-engine = { git = "https://github.com/radixdlt/radixdlt-scrypto", tag = "v1.1.1" } +scrypto-unit = { git = "https://github.com/radixdlt/radixdlt-scrypto", tag = "v1.1.1" } +scrypto-test = { git = "https://github.com/radixdlt/radixdlt-scrypto", tag = "v1.1.1" } +radix-engine-interface = { git = "https://github.com/radixdlt/radixdlt-scrypto", tag = "v1.1.1" } +srwa-yield-derivatives = { path = ".", features = ["test"] } + +[profile.release] +opt-level = 'z' # Optimize for size. +lto = true # Enable Link Time Optimization. +codegen-units = 1 # Reduce number of codegen units to increase optimizations. +panic = 'abort' # Abort on panic. +strip = true # Strip the symbols. +overflow-checks = true # Panic in the case of an overflow. + +[features] +default = [] +test = [] + +[lib] +crate-type = ["cdylib", "lib"] + +[workspace] +# Set the package crate as its own empty workspace, to hide it from any potential ancestor workspace +# Remove this [workspace] section if you intend the package to be part of a Cargo workspace \ No newline at end of file diff --git a/8-yield-derivatives/SRWA-Yield-Derivatives/Scrypto/src/lib.rs b/8-yield-derivatives/SRWA-Yield-Derivatives/Scrypto/src/lib.rs new file mode 100644 index 000000000..572168f8b --- /dev/null +++ b/8-yield-derivatives/SRWA-Yield-Derivatives/Scrypto/src/lib.rs @@ -0,0 +1,389 @@ +use scrypto::prelude::*; +mod user; + +#[blueprint] +mod yield_derivatives { + + enable_method_auth! { + roles { + admin => updatable_by: [admin]; + }, + methods { + add_new_asset => restrict_to: [admin]; + create_user_and_deposit_principal => PUBLIC; + deposit_principal => PUBLIC; + redeem => PUBLIC; + get_users_deposit_balance => PUBLIC; + } + } + struct YieldDerivatives { + principal_liquidity_pools: HashMap, + yield_liquidity_pools: HashMap, + principal_tokens_symbols: HashMap, + yield_tokens_symbols: HashMap, + yield_tokens: HashMap, + yield_rates: HashMap, + total_balances: HashMap, + users: HashMap, + } + + impl YieldDerivatives { + // Implement the functions and methods which will manage those resources and data + + // This is a function, and can be called directly on the blueprint once deployed + pub fn instantiate() -> (ComponentAddress, FungibleBucket) { + // Create the admin badges + let admin_badge: FungibleBucket = ResourceBuilder::new_fungible(OwnerRole::None) + .divisibility(DIVISIBILITY_NONE) + .metadata(metadata! ( + init { + "name" => "Yield Admin Badge".to_string(), locked; + } + )) + .divisibility(DIVISIBILITY_NONE) + .mint_initial_supply(1) + .into(); + + // Instantiate a Hello component, populating its vault with our supply of 1000 HelloToken + let component = Self { + principal_liquidity_pools: HashMap::new(), + yield_liquidity_pools: HashMap::new(), + principal_tokens_symbols: HashMap::new(), + yield_tokens_symbols: HashMap::new(), + yield_tokens: HashMap::new(), + yield_rates: HashMap::new(), + total_balances: HashMap::new(), + users: HashMap::new(), + } + .instantiate() + .prepare_to_globalize(OwnerRole::Fixed(rule!(require( + admin_badge.resource_address() + )))) + .roles(roles!( + admin => rule!(require(admin_badge.resource_address())); + )) + .metadata(metadata! ( + roles { + metadata_setter => rule!(allow_all); + metadata_setter_updater => rule!(allow_all); + metadata_locker => rule!(allow_all); + metadata_locker_updater => rule!(allow_all); + }, + init { + "name" => "SRWA Yield Derivatives component".to_string(), locked; + } + )) + .globalize(); + (component.address(), admin_badge) + } + + // Adding the new asset + pub fn add_new_asset(&mut self, asset_address: ResourceAddress, yield_rate: Decimal) { + match self.yield_rates.get(&asset_address) { + Some(..) => { + warn!("Asset `{:?}` already exists.", asset_address); + } + None => { + self.yield_rates.insert(asset_address, yield_rate); + info!( + "Added new asset `{:?}` with yield_rate of {} to the lending pool.", + asset_address, yield_rate + ); + + // Creates corresponding LP token - commented out for now, will be updated later + self.create_yield_token(asset_address); + info!("Created yield token."); + } + }; + } + + fn create_new_user(&mut self) -> NonFungibleBucket { + info!("create_new_user initiated."); + let global_address = Runtime::global_address(); + let component_address = Runtime::bech32_encode_address(global_address); + let user_badge: NonFungibleBucket = + ResourceBuilder::new_integer_non_fungible(OwnerRole::None) + .metadata(metadata! ( + init { + "name" => "Yield User Badge".to_string(), locked; + "component" => component_address, locked; + } + )) + .mint_initial_supply([( + IntegerNonFungibleLocalId::new(1u64), + self::UserNft::new(), + )]); + + info!( + "new_user resource address `{:?}`.", + user_badge.resource_address() + ); + + let user_id = user_badge.resource_address(); + + let user = user::User { + user_badge_resource_address: user_id, + deposit_balances: HashMap::new(), + }; + + self.users.insert(user_id, user); + user_badge + } + + pub fn create_user_and_deposit_principal( + &mut self, + principal: Bucket, + ) -> NonFungibleBucket { + let principal_address = principal.resource_address(); + let user_badge = self.create_new_user(); + let user_badge_resource_address = user_badge.resource_address(); + + let yield_rate = match self.yield_rates.get(&principal_address) { + Some(&x) => x, + None => Decimal::from(0), + }; + if yield_rate == Decimal::ZERO { + panic!("The deposited resource is not the accepted principal token."); + } + let principal_amount = principal.amount(); + let yield_amount = yield_rate * principal_amount; + let mut user = self.get_user(user_badge_resource_address); + user.on_deposit(principal_address, principal_amount, yield_amount); + self.users.insert(user_badge_resource_address, user); + + //update total principal and yield balance + self.update_balances(principal_address, principal_amount, yield_amount); + + //put principal tokens to vault + match self.principal_liquidity_pools.get_mut(&principal_address) { + Some(x) => x.put(principal), + None => { + let vault = Vault::with_bucket(principal); + self.principal_liquidity_pools + .insert(principal_address, vault); + } + }; + user_badge + } + + pub fn deposit_principal(&mut self, principal: Bucket, user_badge: Proof) { + let principal_address = principal.resource_address(); + let yield_rate = match self.yield_rates.get(&principal_address) { + Some(&x) => x, + None => Decimal::from(0), + }; + if yield_rate == Decimal::ZERO { + panic!("The deposited resource is not the accepted principal token."); + } + + let user_badge_resource_address = user_badge.resource_address(); + let mut user = self.get_user(user_badge_resource_address); + let user_principal_balance = match user.deposit_balances.get_mut(&principal_address) { + Some(current_balance) => current_balance.principal_balance, + None => Decimal::ZERO, + }; + if user_principal_balance != Decimal::ZERO { + panic!("Stake is available if the user does not have an active staking deposit."); + } + let principal_amount = principal.amount(); + + let yield_amount = yield_rate * principal_amount; + user.on_deposit(principal_address, principal_amount, yield_amount); + self.users.insert(user_badge_resource_address, user); + + //update total principal and yield balance + self.update_balances(principal_address, principal_amount, yield_amount); + + match self.principal_liquidity_pools.get_mut(&principal_address) { + Some(x) => x.put(principal), + None => { + let vault = Vault::with_bucket(principal); + self.principal_liquidity_pools + .insert(principal_address, vault); + } + }; + } + + pub fn redeem( + &mut self, + principal_address: ResourceAddress, + user_badge: Proof, + ) -> (Bucket, Bucket) { + let user_badge_resource_address = user_badge.resource_address(); + let mut user = self.get_user(user_badge_resource_address); + let principal_balance = match user.deposit_balances.get_mut(&principal_address) { + Some(current_balance) => current_balance.principal_balance, + None => Decimal::ZERO, + }; + if principal_balance == Decimal::ZERO { + panic!("Nothing to redeem."); + } + let now = Clock::current_time_rounded_to_minutes(); + + let yield_balance = match user.deposit_balances.get_mut(&principal_address) { + Some(current_balance) => current_balance.yield_balance, + None => Decimal::ZERO, + }; + + let deposited_at = match user.deposit_balances.get_mut(&principal_address) { + Some(current_balance) => current_balance.deposited_at, + None => now, + }; + let maturity_date = deposited_at.add_days(30).unwrap(); + let principal_bucket = match self.principal_liquidity_pools.get_mut(&principal_address) + { + Some(x) => x.take(principal_balance), + None => Bucket::new(principal_address), + }; + let yield_address = self.yield_tokens.get(&principal_address).unwrap().clone(); + + let yield_bucket; + user.on_redeem(principal_address); + self.users.insert(user_badge_resource_address, user); + if now.compare(maturity_date, TimeComparisonOperator::Gt) { + yield_bucket = match self.yield_liquidity_pools.get_mut(&yield_address) { + Some(x) => x.take(yield_balance), + None => Bucket::new(yield_address), + }; + info!("Matured."); + } else { + yield_bucket = Bucket::new(yield_address); + info!("Not Matured."); + } + //update total principal and yield balance + self.update_balances(principal_address, -principal_balance, -yield_balance); + (principal_bucket, yield_bucket) + } + + fn get_user(&self, user_badge_resource_address: ResourceAddress) -> user::User { + let user = self + .users + .get(&user_badge_resource_address) + .unwrap() + .clone(); + user + } + + fn update_balances( + &mut self, + principal_address: ResourceAddress, + principal_amount: Decimal, + yield_amount: Decimal, + ) { + let yield_address = self.yield_tokens.get(&principal_address).unwrap().clone(); + //update principal balance + let mut total_principal_deposit_balance = + match self.total_balances.get(&principal_address) { + Some(&x) => x, + None => Decimal::from(0), + }; + total_principal_deposit_balance += principal_amount; + self.total_balances + .insert(principal_address, total_principal_deposit_balance); + + //update yield balance + let mut total_yield_deposit_balance = match self.total_balances.get(&yield_address) { + Some(&x) => x, + None => Decimal::from(0), + }; + total_yield_deposit_balance += yield_amount; + self.total_balances + .insert(yield_address, total_yield_deposit_balance); + } + + fn create_yield_token(&mut self, asset_address: ResourceAddress) { + let manager = ResourceManager::from(asset_address); + + // check resource manager for get_metadata + let asset_name_option: Option = manager.get_metadata("symbol").unwrap(); + let asset_name: String = asset_name_option.unwrap_or_default().to_owned(); + let mut yield_token_symbol = "yt".to_owned(); + + yield_token_symbol.push_str(&asset_name); + let yt_symbol = yield_token_symbol.clone(); + + let yield_token = ResourceBuilder::new_fungible(OwnerRole::None) + .metadata(metadata! { + roles { + metadata_locker => rule!(allow_all); + metadata_locker_updater => rule!(allow_all); + metadata_setter => rule!(allow_all); + metadata_setter_updater => rule!(deny_all); + }, + init { + + "name" => "Yield Token", locked; + "symbol" => yield_token_symbol, locked; + } + }) + .mint_roles(mint_roles!( + minter => rule!(allow_all); + minter_updater => rule!(deny_all); + )) + .burn_roles(burn_roles!( + burner => rule!(allow_all); + burner_updater => rule!(deny_all); + )) + .recall_roles(recall_roles!( + recaller => rule!(allow_all); + recaller_updater => rule!(deny_all); + )) + .mint_initial_supply(1000000); + let yield_address = yield_token.resource_address(); + match self.yield_liquidity_pools.get_mut(&yield_address) { + Some(x) => x.put(yield_token.into()), + None => { + let vault = Vault::with_bucket(yield_token.into()); + self.yield_liquidity_pools.insert(yield_address, vault); + } + }; + + self.yield_tokens.insert(asset_address, yield_address); + self.yield_tokens_symbols.insert(asset_address, yt_symbol); + self.principal_tokens_symbols + .insert(asset_address, asset_name); + } + + // Retrieve a user's current deposit balance for a specific asset + pub fn get_users_deposit_balance( + &self, + asset_address: ResourceAddress, + user_badge: Proof, + ) -> (Decimal, Decimal) { + info!("get_resource_deposit_balance initiated."); + + // Retrieve user data + let user_badge_resource_address = user_badge.resource_address(); + let user = self.users.get(&user_badge_resource_address).unwrap(); + + // Update User Borrowed Balance + let current_principal_balance = match user.deposit_balances.get(&asset_address) { + Some(current_balance) => current_balance.principal_balance, + None => Decimal::from(0), + }; + let current_yield_balance = match user.deposit_balances.get(&asset_address) { + Some(current_balance) => current_balance.yield_balance, + None => Decimal::from(0), + }; + info!( + " Principal balance for asset {:?} and yield balance is {:?}.", + current_principal_balance, current_yield_balance + ); + (current_principal_balance, current_yield_balance) + } + } +} + +#[derive(ScryptoSbor, NonFungibleData)] +pub struct UserNft { + pub name: String, + // #[mutable] + // pub flag: bool, +} + +impl UserNft { + pub fn new() { + ResourceBuilder::new_ruid_non_fungible::(OwnerRole::None) + .create_with_no_initial_supply(); + } +} diff --git a/8-yield-derivatives/SRWA-Yield-Derivatives/Scrypto/src/transactions/add_new_asset.rtm b/8-yield-derivatives/SRWA-Yield-Derivatives/Scrypto/src/transactions/add_new_asset.rtm new file mode 100644 index 000000000..3753ed58f --- /dev/null +++ b/8-yield-derivatives/SRWA-Yield-Derivatives/Scrypto/src/transactions/add_new_asset.rtm @@ -0,0 +1,23 @@ +CALL_METHOD + Address("component_sim1cptxxxxxxxxxfaucetxxxxxxxxx000527798379xxxxxxxxxhkrefh") + "lock_fee" + Decimal("5000") +; +CALL_METHOD + Address("account_tdx_2_128qp5d27yepw44ftmq9wa9jzps7tf3y7r20xw0v6xz20wt54s7muwp") + "create_proof_of_amount" + Address("resource_tdx_2_1t5v99d0s7njg7qe65fggvhsj69n4qfukhnhd3egj3j9s0rg0yupp6e") + Decimal("1") +; +CALL_METHOD + Address("component_tdx_2_1cq9kx6zhxwrnme6g08gkxnngq6hhsetv4u7z2y9t6pmp5t4xenlplg") + "add_new_asset" + Address("resource_sim1tknxxxxxxxxxradxrdxxxxxxxxx009923554798xxxxxxxxxakj8n3") + Decimal("0.1") +; +CALL_METHOD + Address("account_tdx_2_128qp5d27yepw44ftmq9wa9jzps7tf3y7r20xw0v6xz20wt54s7muwp") + "try_deposit_batch_or_refund" + Expression("ENTIRE_WORKTOP") + Enum<0u8>() +; diff --git a/8-yield-derivatives/SRWA-Yield-Derivatives/Scrypto/src/transactions/create_user_and_deposit_principal.rtm b/8-yield-derivatives/SRWA-Yield-Derivatives/Scrypto/src/transactions/create_user_and_deposit_principal.rtm new file mode 100644 index 000000000..4908a838e --- /dev/null +++ b/8-yield-derivatives/SRWA-Yield-Derivatives/Scrypto/src/transactions/create_user_and_deposit_principal.rtm @@ -0,0 +1,27 @@ +CALL_METHOD + Address("component_sim1cptxxxxxxxxxfaucetxxxxxxxxx000527798379xxxxxxxxxhkrefh") + "lock_fee" + Decimal("5000") +; +CALL_METHOD + Address("account_sim1c956qr3kxlgypxwst89j9yf24tjc7zxd4up38x37zr6q4jxdx9rhma") + "withdraw" + Address("resource_sim1tknxxxxxxxxxradxrdxxxxxxxxx009923554798xxxxxxxxxakj8n3") + Decimal("10") +; +TAKE_FROM_WORKTOP + Address("resource_sim1tknxxxxxxxxxradxrdxxxxxxxxx009923554798xxxxxxxxxakj8n3") + Decimal("10") + Bucket("bucket1") +; +CALL_METHOD + Address("component_sim1cq4kl9qul5nsd49u99x25fs8gclv2yd28a9g242l2x0zx4hh6kqfkn") + "create_user_and_deposit_principal" + Bucket("bucket1") +; +CALL_METHOD + Address("account_sim1c956qr3kxlgypxwst89j9yf24tjc7zxd4up38x37zr6q4jxdx9rhma") + "try_deposit_batch_or_refund" + Expression("ENTIRE_WORKTOP") + Enum<0u8>() +; diff --git a/8-yield-derivatives/SRWA-Yield-Derivatives/Scrypto/src/transactions/deposit_principal.rtm b/8-yield-derivatives/SRWA-Yield-Derivatives/Scrypto/src/transactions/deposit_principal.rtm new file mode 100644 index 000000000..14d0f3729 --- /dev/null +++ b/8-yield-derivatives/SRWA-Yield-Derivatives/Scrypto/src/transactions/deposit_principal.rtm @@ -0,0 +1,39 @@ +CALL_METHOD + Address("component_sim1cptxxxxxxxxxfaucetxxxxxxxxx000527798379xxxxxxxxxhkrefh") + "lock_fee" + Decimal("5000") +; +CALL_METHOD + Address("account_sim1c956qr3kxlgypxwst89j9yf24tjc7zxd4up38x37zr6q4jxdx9rhma") + "withdraw" + Address("resource_sim1tknxxxxxxxxxradxrdxxxxxxxxx009923554798xxxxxxxxxakj8n3") + Decimal("10") +; +TAKE_FROM_WORKTOP + Address("resource_sim1tknxxxxxxxxxradxrdxxxxxxxxx009923554798xxxxxxxxxakj8n3") + Decimal("10") + Bucket("bucket1") +; +CALL_METHOD + Address("account_sim1c956qr3kxlgypxwst89j9yf24tjc7zxd4up38x37zr6q4jxdx9rhma") + "create_proof_of_non_fungibles" + Address("resource_sim1nfmyl590n6dttggacglnqx98yu8y2k4vqxjakqeyueqvqtxcqf433s") + Array( + NonFungibleLocalId("#1#") + ) +; +POP_FROM_AUTH_ZONE + Proof("proof1") +; +CALL_METHOD + Address("component_sim1cq4kl9qul5nsd49u99x25fs8gclv2yd28a9g242l2x0zx4hh6kqfkn") + "deposit_principal" + Bucket("bucket1") + Proof("proof1") +; +CALL_METHOD + Address("account_sim1c956qr3kxlgypxwst89j9yf24tjc7zxd4up38x37zr6q4jxdx9rhma") + "try_deposit_batch_or_refund" + Expression("ENTIRE_WORKTOP") + Enum<0u8>() +; diff --git a/8-yield-derivatives/SRWA-Yield-Derivatives/Scrypto/src/transactions/instantiate.rtm b/8-yield-derivatives/SRWA-Yield-Derivatives/Scrypto/src/transactions/instantiate.rtm new file mode 100644 index 000000000..56d686659 --- /dev/null +++ b/8-yield-derivatives/SRWA-Yield-Derivatives/Scrypto/src/transactions/instantiate.rtm @@ -0,0 +1,16 @@ +CALL_METHOD + Address("component_sim1cptxxxxxxxxxfaucetxxxxxxxxx000527798379xxxxxxxxxhkrefh") + "lock_fee" + Decimal("5000") +; +CALL_FUNCTION + Address("package_sim1pk3cmat8st4ja2ms8mjqy2e9ptk8y6cx40v4qnfrkgnxcp2krkpr92") + "YieldDerivatives" + "instantiate" +; +CALL_METHOD + Address("account_sim1c956qr3kxlgypxwst89j9yf24tjc7zxd4up38x37zr6q4jxdx9rhma") + "try_deposit_batch_or_refund" + Expression("ENTIRE_WORKTOP") + Enum<0u8>() +; diff --git a/8-yield-derivatives/SRWA-Yield-Derivatives/Scrypto/src/transactions/redeem.rtm b/8-yield-derivatives/SRWA-Yield-Derivatives/Scrypto/src/transactions/redeem.rtm new file mode 100644 index 000000000..4747c95f7 --- /dev/null +++ b/8-yield-derivatives/SRWA-Yield-Derivatives/Scrypto/src/transactions/redeem.rtm @@ -0,0 +1,28 @@ +CALL_METHOD + Address("component_sim1cptxxxxxxxxxfaucetxxxxxxxxx000527798379xxxxxxxxxhkrefh") + "lock_fee" + Decimal("5000") +; +CALL_METHOD + Address("account_sim1c956qr3kxlgypxwst89j9yf24tjc7zxd4up38x37zr6q4jxdx9rhma") + "create_proof_of_non_fungibles" + Address("resource_sim1ngzq4h9deqr8vmwrenzv2ajagf2nggc2kysshsxsphhxes4cn83ymk") + Array( + NonFungibleLocalId("#1#") + ) +; +POP_FROM_AUTH_ZONE + Proof("proof1") +; +CALL_METHOD + Address("component_sim1cq4kl9qul5nsd49u99x25fs8gclv2yd28a9g242l2x0zx4hh6kqfkn") + "redeem" + Address("resource_sim1tknxxxxxxxxxradxrdxxxxxxxxx009923554798xxxxxxxxxakj8n3") + Proof("proof1") +; +CALL_METHOD + Address("account_sim1c956qr3kxlgypxwst89j9yf24tjc7zxd4up38x37zr6q4jxdx9rhma") + "try_deposit_batch_or_refund" + Expression("ENTIRE_WORKTOP") + Enum<0u8>() +; diff --git a/8-yield-derivatives/SRWA-Yield-Derivatives/Scrypto/src/user.rs b/8-yield-derivatives/SRWA-Yield-Derivatives/Scrypto/src/user.rs new file mode 100644 index 000000000..9d528589b --- /dev/null +++ b/8-yield-derivatives/SRWA-Yield-Derivatives/Scrypto/src/user.rs @@ -0,0 +1,66 @@ +use scrypto::prelude::*; + +#[derive(Debug, ScryptoSbor, PartialEq, Eq, Clone)] + +pub struct User { + pub user_badge_resource_address: ResourceAddress, + pub deposit_balances: HashMap, +} +impl User { + pub fn on_deposit( + &mut self, + resource_address: ResourceAddress, + amount: Decimal, + yield_amount: Decimal, + ) { + let now = Clock::current_time_rounded_to_minutes(); + match self.deposit_balances.get_mut(&resource_address) { + Some(_current_balance) => { + let deposit = Deposit { + principal_balance: amount, + yield_balance: yield_amount, + deposited_at: now, + }; + + self.deposit_balances.insert(resource_address, deposit); + } + None => { + let deposit = Deposit { + principal_balance: amount, + yield_balance: yield_amount, + deposited_at: now, + }; + self.deposit_balances.insert(resource_address, deposit); + } + }; + } + + pub fn on_redeem(&mut self, resource_address: ResourceAddress) { + let now = Clock::current_time_rounded_to_minutes(); + match self.deposit_balances.get_mut(&resource_address) { + Some(_current_balance) => { + let deposit = Deposit { + principal_balance: Decimal::ZERO, + yield_balance: Decimal::ZERO, + deposited_at: now, + }; + + self.deposit_balances.insert(resource_address, deposit); + } + None => { + let deposit = Deposit { + principal_balance: Decimal::ZERO, + yield_balance: Decimal::ZERO, + deposited_at: now, + }; + self.deposit_balances.insert(resource_address, deposit); + } + }; + } +} +#[derive(Debug, ScryptoSbor, PartialEq, Eq, Clone)] +pub struct Deposit { + pub principal_balance: Decimal, + pub yield_balance: Decimal, + pub deposited_at: Instant, +} diff --git a/8-yield-derivatives/SRWA-Yield-Derivatives/Scrypto/tests/lib.rs b/8-yield-derivatives/SRWA-Yield-Derivatives/Scrypto/tests/lib.rs new file mode 100644 index 000000000..d00208447 --- /dev/null +++ b/8-yield-derivatives/SRWA-Yield-Derivatives/Scrypto/tests/lib.rs @@ -0,0 +1,61 @@ +use radix_engine_interface::prelude::*; +use scrypto::this_package; +use scrypto_test::prelude::*; +use scrypto_unit::*; + +use srwa_yield_derivatives::test_bindings::*; + +#[test] +fn test_hello() { + // Setup the environment + let mut test_runner = TestRunnerBuilder::new().build(); + + // Create an account + let (public_key, _private_key, account) = test_runner.new_allocated_account(); + + // Publish package + let package_address = test_runner.compile_and_publish(this_package!()); + + // Test the `instantiate_hello` function. + let manifest = ManifestBuilder::new() + .call_function( + package_address, + "Hello", + "instantiate_hello", + manifest_args!(), + ) + .build(); + let receipt = test_runner.execute_manifest_ignoring_fee( + manifest, + vec![NonFungibleGlobalId::from_public_key(&public_key)], + ); + println!("{:?}\n", receipt); + let component = receipt.expect_commit(true).new_component_addresses()[0]; + + // Test the `free_token` method. + let manifest = ManifestBuilder::new() + .call_method(component, "free_token", manifest_args!()) + .call_method( + account, + "deposit_batch", + manifest_args!(ManifestExpression::EntireWorktop), + ) + .build(); + let receipt = test_runner.execute_manifest_ignoring_fee( + manifest, + vec![NonFungibleGlobalId::from_public_key(&public_key)], + ); + println!("{:?}\n", receipt); + receipt.expect_commit_success(); +} + +#[test] +fn test_hello_with_test_environment() -> Result<(), RuntimeError> { + // Arrange + let mut env = TestEnvironment::new(); + let package_address = Package::compile_and_publish(this_package!(), &mut env)?; + + let mut hello = YieldDerivatives::instantiate(package_address, &mut env)?; + + Ok(()) +} diff --git a/8-yield-derivatives/SRWA-Yield-Derivatives/dApp/.env-example b/8-yield-derivatives/SRWA-Yield-Derivatives/dApp/.env-example new file mode 100644 index 000000000..f7f6e483b --- /dev/null +++ b/8-yield-derivatives/SRWA-Yield-Derivatives/dApp/.env-example @@ -0,0 +1 @@ +VITE_COMPONENT_ADDRESS=component_tdx_2_1crav0g79lp4efxvwuvsmsj26l0c3n4jq022cjtrx7t84r65t7x57z9 diff --git a/8-yield-derivatives/SRWA-Yield-Derivatives/dApp/.eslintignore b/8-yield-derivatives/SRWA-Yield-Derivatives/dApp/.eslintignore new file mode 100644 index 000000000..ba682b91f --- /dev/null +++ b/8-yield-derivatives/SRWA-Yield-Derivatives/dApp/.eslintignore @@ -0,0 +1,3 @@ +node_modules/ +dist/ +env.d.ts diff --git a/8-yield-derivatives/SRWA-Yield-Derivatives/dApp/.eslintrc b/8-yield-derivatives/SRWA-Yield-Derivatives/dApp/.eslintrc new file mode 100644 index 000000000..febccf1d6 --- /dev/null +++ b/8-yield-derivatives/SRWA-Yield-Derivatives/dApp/.eslintrc @@ -0,0 +1,41 @@ +{ + "extends": [ + "eslint:recommended", + "plugin:react/recommended", + "plugin:react-hooks/recommended", + "plugin:import/recommended", + "plugin:jsx-a11y/recommended", + "plugin:@typescript-eslint/recommended", + "eslint-config-prettier" + ], + "settings": { + "react": { + "version": "detect" + }, + "import/resolver": { + "node": { + "paths": [ + "src" + ], + "extensions": [ + ".js", + ".jsx", + ".ts", + ".tsx" + ] + } + } + }, + "rules": { + "no-unused-vars": [ + "error", + { + "vars": "all", + "args": "after-used", + "ignoreRestSiblings": true, + "argsIgnorePattern": "^_" + } + ], + "react/react-in-jsx-scope": "off" + } +} diff --git a/8-yield-derivatives/SRWA-Yield-Derivatives/dApp/.eslintrc.cjs b/8-yield-derivatives/SRWA-Yield-Derivatives/dApp/.eslintrc.cjs new file mode 100644 index 000000000..f1020b945 --- /dev/null +++ b/8-yield-derivatives/SRWA-Yield-Derivatives/dApp/.eslintrc.cjs @@ -0,0 +1,37 @@ +module.exports = { + env: { browser: true, es2020: true }, + extends: [ + 'eslint:recommended', + 'plugin:@typescript-eslint/recommended', + 'plugin:react-hooks/recommended', + ], + parser: '@typescript-eslint/parser', + parserOptions: { ecmaVersion: 'latest', sourceType: 'module' }, + plugins: ['react-refresh', 'import'], + rules: { + 'react-refresh/only-export-components': 'warn', + '@typescript-eslint/no-non-null-assertion': 'off', + '@typescript-eslint/no-explicit-any': 'off', + 'sort-imports': [ + 'error', + { + ignoreCase: false, + ignoreDeclarationSort: true, + ignoreMemberSort: false, + memberSyntaxSortOrder: ['none', 'all', 'multiple', 'single'], + allowSeparatedGroups: true, + }, + ], + 'import/order': [ + 'error', + { + groups: [['builtin', 'external'], ['internal', 'parent', 'sibling', 'index']], + 'newlines-between': 'always', + alphabetize: { + order: 'asc', + caseInsensitive: false, + }, + }, + ], + }, +}; diff --git a/8-yield-derivatives/SRWA-Yield-Derivatives/dApp/.gitignore b/8-yield-derivatives/SRWA-Yield-Derivatives/dApp/.gitignore new file mode 100644 index 000000000..5fb3fbc06 --- /dev/null +++ b/8-yield-derivatives/SRWA-Yield-Derivatives/dApp/.gitignore @@ -0,0 +1,27 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local +.env +.env.development +package-lock.json + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/8-yield-derivatives/SRWA-Yield-Derivatives/dApp/.prettierignore b/8-yield-derivatives/SRWA-Yield-Derivatives/dApp/.prettierignore new file mode 100644 index 000000000..b6e731457 --- /dev/null +++ b/8-yield-derivatives/SRWA-Yield-Derivatives/dApp/.prettierignore @@ -0,0 +1,2 @@ +node_modules/ +dist/ diff --git a/8-yield-derivatives/SRWA-Yield-Derivatives/dApp/.prettierrc b/8-yield-derivatives/SRWA-Yield-Derivatives/dApp/.prettierrc new file mode 100644 index 000000000..c4dc5b181 --- /dev/null +++ b/8-yield-derivatives/SRWA-Yield-Derivatives/dApp/.prettierrc @@ -0,0 +1,9 @@ +{ + "trailingComma": "all", + "tabWidth": 2, + "semi": true, + "singleQuote": true, + "printWidth": 120, + "bracketSpacing": true, + "endOfLine": "lf" +} diff --git a/8-yield-derivatives/SRWA-Yield-Derivatives/dApp/README.md b/8-yield-derivatives/SRWA-Yield-Derivatives/dApp/README.md new file mode 100644 index 000000000..8b7d322d2 --- /dev/null +++ b/8-yield-derivatives/SRWA-Yield-Derivatives/dApp/README.md @@ -0,0 +1,50 @@ +# srwa-yield-derivatives + +### Installation +``` +1. Clone the repository: `git clone https://github.com/SRWAio/srwa.git` +2. Navigate to the project directory: `cd srwa` +3. Install the dependencies: `npm install` +``` + +### Usage +``` +`npm run dev`: Starts the development server using Vite. +`npm run build`: Builds the project for production using TypeScript and Vite. +`npm run lint`: Lints the source code using ESLint. +`npm run preview`: Previews the production build using Vite. +``` + +### Customize configuration +See [Configuration Reference](https://vitejs.dev/). + +# React + TypeScript + Vite + +This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. + +Currently, two official plugins are available: + +- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh +- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh + +## Expanding the ESLint configuration + +If you are developing a production application, we recommend updating the configuration to enable type aware lint rules: + +- Configure the top-level `parserOptions` property like this: + +```js +export default { + // other rules... + parserOptions: { + ecmaVersion: 'latest', + sourceType: 'module', + project: ['./tsconfig.json', './tsconfig.node.json'], + tsconfigRootDir: __dirname, + }, +} +``` + +- Replace `plugin:@typescript-eslint/recommended` to `plugin:@typescript-eslint/recommended-type-checked` or `plugin:@typescript-eslint/strict-type-checked` +- Optionally add `plugin:@typescript-eslint/stylistic-type-checked` +- Install [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react) and add `plugin:react/recommended` & `plugin:react/jsx-runtime` to the `extends` list diff --git a/8-yield-derivatives/SRWA-Yield-Derivatives/dApp/index.html b/8-yield-derivatives/SRWA-Yield-Derivatives/dApp/index.html new file mode 100644 index 000000000..e04551be4 --- /dev/null +++ b/8-yield-derivatives/SRWA-Yield-Derivatives/dApp/index.html @@ -0,0 +1,13 @@ + + + + + + + SRWA - Yield Derivatives + + +
+ + + diff --git a/8-yield-derivatives/SRWA-Yield-Derivatives/dApp/package.json b/8-yield-derivatives/SRWA-Yield-Derivatives/dApp/package.json new file mode 100644 index 000000000..4c9ddd665 --- /dev/null +++ b/8-yield-derivatives/SRWA-Yield-Derivatives/dApp/package.json @@ -0,0 +1,42 @@ +{ + "name": "srwa-yield-derivatives", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc && vite build", + "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0", + "preview": "vite preview" + }, + "dependencies": { + "react": "^18.2.0", + "react-dom": "^18.2.0" + }, + "devDependencies": { + "@radixdlt/babylon-gateway-api-sdk": "^1.4.1", + "@radixdlt/radix-dapp-toolkit": "^1.4.4", + "@types/react": "^18.2.66", + "@types/react-dom": "^18.2.22", + "@typescript-eslint/eslint-plugin": "^7.2.0", + "@typescript-eslint/parser": "^7.2.0", + "@vitejs/plugin-react-swc": "^3.5.0", + "autoprefixer": "^10.4.19", + "buffer": "^6.0.3", + "eslint": "^8.57.0", + "eslint-config-prettier": "^9.1.0", + "eslint-plugin-import": "^2.29.1", + "eslint-plugin-jsx-a11y": "^6.8.0", + "eslint-plugin-react": "^7.34.1", + "eslint-plugin-react-hooks": "^4.6.0", + "eslint-plugin-react-refresh": "^0.4.6", + "postcss": "^8.4.38", + "prettier": "^3.2.5", + "react-hook-form": "^7.51.3", + "react-toastify": "^10.0.5", + "tailwindcss": "^3.4.3", + "typescript": "^5.2.2", + "vite": "^5.2.0", + "zustand": "^4.5.2" + } +} diff --git a/8-yield-derivatives/SRWA-Yield-Derivatives/dApp/postcss.config.js b/8-yield-derivatives/SRWA-Yield-Derivatives/dApp/postcss.config.js new file mode 100644 index 000000000..2aa7205d4 --- /dev/null +++ b/8-yield-derivatives/SRWA-Yield-Derivatives/dApp/postcss.config.js @@ -0,0 +1,6 @@ +export default { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +}; diff --git a/8-yield-derivatives/SRWA-Yield-Derivatives/dApp/src/App.tsx b/8-yield-derivatives/SRWA-Yield-Derivatives/dApp/src/App.tsx new file mode 100644 index 000000000..357551a55 --- /dev/null +++ b/8-yield-derivatives/SRWA-Yield-Derivatives/dApp/src/App.tsx @@ -0,0 +1,13 @@ +import AppHeader from './components/AppHeader'; +import Home from './pages/Home'; + +function App() { + return ( + <> + + + + ); +} + +export default App; diff --git a/8-yield-derivatives/SRWA-Yield-Derivatives/dApp/src/account/state.ts b/8-yield-derivatives/SRWA-Yield-Derivatives/dApp/src/account/state.ts new file mode 100644 index 000000000..95c52904e --- /dev/null +++ b/8-yield-derivatives/SRWA-Yield-Derivatives/dApp/src/account/state.ts @@ -0,0 +1,11 @@ +import { Account } from '@radixdlt/wallet-sdk'; +import { BehaviorSubject } from 'rxjs'; + +import { addEntities } from '../entity/state'; + +const accounts = new BehaviorSubject([]); + +export const setAccounts = (input: Account[]) => { + accounts.next(input); + addEntities(input.map((item) => ({ address: item.address, type: 'account' }))); +}; diff --git a/8-yield-derivatives/SRWA-Yield-Derivatives/dApp/src/app/shared/models/index.model.ts b/8-yield-derivatives/SRWA-Yield-Derivatives/dApp/src/app/shared/models/index.model.ts new file mode 100644 index 000000000..f8b801b1f --- /dev/null +++ b/8-yield-derivatives/SRWA-Yield-Derivatives/dApp/src/app/shared/models/index.model.ts @@ -0,0 +1,10 @@ +export type AssetsItemModel = { + amount: number | null; + as_string: string; + address: string; + total_balances: number; + yield_rates: number; + principal_balance: number; + yield_balance: number; + deposited_at: number; +}; diff --git a/8-yield-derivatives/SRWA-Yield-Derivatives/dApp/src/components/AppHeader/index.tsx b/8-yield-derivatives/SRWA-Yield-Derivatives/dApp/src/components/AppHeader/index.tsx new file mode 100644 index 000000000..b4ebd4902 --- /dev/null +++ b/8-yield-derivatives/SRWA-Yield-Derivatives/dApp/src/components/AppHeader/index.tsx @@ -0,0 +1,16 @@ +function AppHeader() { + return ( +
+ +
+ ); +} + +export default AppHeader; diff --git a/8-yield-derivatives/SRWA-Yield-Derivatives/dApp/src/entity/state.ts b/8-yield-derivatives/SRWA-Yield-Derivatives/dApp/src/entity/state.ts new file mode 100644 index 000000000..926b2a69e --- /dev/null +++ b/8-yield-derivatives/SRWA-Yield-Derivatives/dApp/src/entity/state.ts @@ -0,0 +1,293 @@ +import { + EntityMetadataItem, + FungibleResourcesCollectionItemVaultAggregated, + NonFungibleResourcesCollectionItemVaultAggregated, + StateEntityDetailsResponseComponentDetails, +} from '@radixdlt/babylon-gateway-api-sdk'; +import { ResultAsync, errAsync, okAsync } from 'neverthrow'; +import { BehaviorSubject } from 'rxjs'; + +import { shortenAddress } from './../helpers/shorten-address'; +import { getStringMetadata } from '../helpers/find-metadata'; +import { gatewayApi } from '../rdt/rdt'; + +const entityType = { + account: 'account', + identity: 'identity', + fungibleToken: 'fungibleToken', + nftCollection: 'nftCollection', + nft: 'nft', + component: 'component', +} as const; + +type Entity = { + [entityType.account]: { + entityType: typeof entityType.account; + address: string; + fungibleTokens: Omit[]; + nftCollections: Omit[]; + metadata: EntityMetadataItem[]; + }; + [entityType.identity]: { + entityType: typeof entityType.identity; + address: string; + metadata: EntityMetadataItem[]; + }; + [entityType.fungibleToken]: { + entityType: typeof entityType.fungibleToken; + address: string; + value: number; + displayLabel?: string; + metadata: EntityMetadataItem[]; + }; + [entityType.nftCollection]: { + entityType: typeof entityType.nftCollection; + address: string; + vaultAddress: string; + totalCount: number; + metadata: EntityMetadataItem[]; + }; + [entityType.nft]: { + entityType: typeof entityType.nft; + address: string; + nftId: string; + nftCollectionAddress: string; + ownerAddress: string; + }; + [entityType.component]: { + entityType: typeof entityType.component; + address: string; + metadata: EntityMetadataItem[]; + fungibleTokens: Omit[]; + nftCollections: Omit[]; + details: { + type: 'Component'; + } & StateEntityDetailsResponseComponentDetails; + }; +}; + +// type EntityKind = Entity[keyof typeof entityType]; + +type EntityCollections = { + [EntityType in keyof typeof entityType]: Record; +}; + +type AddEntityToCollectionInputKinds = { + [entityType.account]: { + address: string; + type: (typeof entityType)['account']; + }; + [entityType.identity]: { + address: string; + type: (typeof entityType)['identity']; + }; + [entityType.fungibleToken]: { + address: string; + value: number; + type: (typeof entityType)['fungibleToken']; + }; + [entityType.nftCollection]: { + address: string; + ownerAddress: string; + vaultAddress: string; + totalCount: number; + type: (typeof entityType)['nftCollection']; + }; + [entityType.nft]: { + nftId: string; + address: string; + nftCollectionsAddress: string; + ownerAddress: string; + type: (typeof entityType)['nft']; + }; + [entityType.component]: { + address: string; + type: (typeof entityType)['component']; + }; +}; + +type AddEntityToCollectionInput = AddEntityToCollectionInputKinds[keyof AddEntityToCollectionInputKinds]; + +const transformFungibleResourceItemResponse = (item: FungibleResourcesCollectionItemVaultAggregated) => ({ + address: item.resource_address, + value: item.vaults.items.reduce((acc: any, curr: any) => acc + Number(curr.amount), 0), +}); + +const transformNftResourceItemResponse = (item: NonFungibleResourcesCollectionItemVaultAggregated) => ({ + address: item.resource_address, + vaultAddress: item.vaults.items[0].vault_address, + totalCount: item.vaults.items[0].total_count, +}); + +const defaultEntitiesState = { + account: {}, + identity: {}, + fungibleToken: {}, + nftCollection: {}, + nft: {}, + component: {}, +} satisfies EntityCollections; + +const entitiesState = new BehaviorSubject(defaultEntitiesState); + +const setEntities = (entities: EntityCollections) => { + entitiesState.next(entities); +}; + +const fetchEntities = (requestedEntities: AddEntityToCollectionInput[]) => { + const requestedEntitiesMap = requestedEntities.reduce((prev: any, next) => { + prev[next.address] = next; + return prev; + }, {}); + return gatewayApi + .getEntitiesDetails(requestedEntities.map((item) => item.address)) + .andThen((items: any) => + ResultAsync.combine( + items.map((item: any) => { + const entity = requestedEntitiesMap[item.address]; + if (!entity) { + console.warn('didnt found matching entity!'); + return okAsync([]); + } + + const fungibleTokens = item.fungible_resources.items.map(transformFungibleResourceItemResponse); + + const nftCollections = + entity.type === 'account' + ? item.non_fungible_resources.items.map((item: NonFungibleResourcesCollectionItemVaultAggregated) => ({ + ...transformNftResourceItemResponse(item), + ownerAddress: entity.address, + })) + : []; + + switch (entity.type) { + case entityType.account: + return okAsync([ + { + entityType: entity.type, + address: entity.address, + metadata: item.metadata.items, + fungibleTokens, + nftCollections, + } satisfies Entity['account'], + ]); + + case entityType.component: + return okAsync([ + { + entityType: entity.type, + address: entity.address, + metadata: item.metadata.items, + fungibleTokens, + nftCollections, + details: item.details as Entity['component']['details'], + } satisfies Entity['component'], + ]); + + case entityType.identity: + return okAsync([ + { + entityType: entity.type, + address: entity.address, + metadata: item.metadata.items, + } satisfies Entity['identity'], + ]); + + case entityType.fungibleToken: { + const symbol = getStringMetadata('symbol', { + metadata: item.metadata.items, + }); + const name = getStringMetadata('name', { + metadata: item.metadata.items, + }); + const displayLabel = [symbol, name].filter(Boolean).join(' - ') || shortenAddress(entity.address); + return okAsync([ + { + entityType: entity.type, + address: entity.address, + value: entity.value, + metadata: item.metadata.items, + displayLabel, + } satisfies Entity['fungibleToken'], + ]); + } + + case entityType.nftCollection: + return gatewayApi + .getEntityNonFungibleIds({ + accountAddress: entity.ownerAddress, + nftAddress: entity.address, + vaultAddress: entity.vaultAddress, + }) + .map((response: any) => + response.items.map( + (item: any) => + ({ + entityType: entityType.nft, + nftId: item, + address: `${entity.address}:${item}`, + nftCollectionAddress: entity.address, + ownerAddress: entity.ownerAddress, + }) satisfies Entity['nft'], + ), + ) + .map((items) => [ + { + entityType: entityType.nftCollection, + address: entity.address, + metadata: item.metadata.items, + vaultAddress: entity.vaultAddress, + totalCount: entity.totalCount, + } satisfies Entity['nftCollection'], + ...items, + ]); + + default: { + return errAsync(new Error('Invalid entity type')); + } + } + }), + ), + ) + .map((items) => items.flat()); +}; + +export const addEntities = (input: AddEntityToCollectionInput[], forceReload?: boolean) => { + const storedEntities = entitiesState.value; + const entitiesToFetch = input.filter((item) => !storedEntities[item.type][item.address]); + + if (entitiesToFetch.length === 0 || forceReload) return; + + fetchEntities(entitiesToFetch).map((items) => { + const entities = items.reduce( + (acc: EntityCollections, curr: any) => ({ + ...acc, + [curr.entityType]: { ...acc[curr.entityType], [curr.address]: curr }, + }), + entitiesState.value, + ); + + setEntities(entities); + + const childEntities = items.reduce((acc, curr: any) => { + if (!([entityType.account, entityType.component] as (keyof typeof entityType)[]).includes(curr.entityType)) + return acc; + + const item = curr as Entity['account'] | Entity['component']; + + const fungibleTokens = item.fungibleTokens.map((fungibleToken) => ({ + type: entityType.fungibleToken, + ...fungibleToken, + })) satisfies AddEntityToCollectionInput[]; + + const nftCollections = item.nftCollections.map((nftCollection) => ({ + type: entityType.nftCollection, + ...nftCollection, + ownerAddress: item.address, + })); + return [...acc, ...fungibleTokens, ...nftCollections]; + }, []) satisfies AddEntityToCollectionInput[]; + + return addEntities(childEntities, forceReload); + }); +}; diff --git a/8-yield-derivatives/SRWA-Yield-Derivatives/dApp/src/features/helpers/index.ts b/8-yield-derivatives/SRWA-Yield-Derivatives/dApp/src/features/helpers/index.ts new file mode 100644 index 000000000..da65e8cf3 --- /dev/null +++ b/8-yield-derivatives/SRWA-Yield-Derivatives/dApp/src/features/helpers/index.ts @@ -0,0 +1,63 @@ +import { StateEntityNonFungiblesPageResponse } from '@radixdlt/babylon-gateway-api-sdk'; +import { + FungibleResourcesCollectionItem, + StateEntityDetailsResponse, + StateEntityFungiblesPageResponse, +} from '@radixdlt/babylon-gateway-api-sdk'; +import { StateEntityDetailsResponseItem } from '@radixdlt/babylon-gateway-api-sdk/dist/generated/models/StateEntityDetailsResponseItem'; + +import { AssetsItemModel } from '../../app/shared/models/index.model'; +import { COMPONENT_ADDRESS, stateApi } from '../../helpers'; + +export const findUserBadgeAddress = async (data: StateEntityNonFungiblesPageResponse) => { + for (const e of data.items) { + const metadata = await stateApi.entityMetadataPage({ + stateEntityMetadataPageRequest: { + address: e.resource_address, + }, + }); + + for (const item of metadata.items) { + // @ts-expect-error: Property 'value' does not exist on type 'MetadataTypedValue'. + if (item.key === 'component' && item.value?.typed?.value === COMPONENT_ADDRESS) { + return metadata.address; + } + } + } +}; + +export const setInitialAssets = ( + entityData: StateEntityDetailsResponse, + fungiblesData: StateEntityFungiblesPageResponse, +) => { + return entityData.items.flatMap((entity: StateEntityDetailsResponseItem) => { + if (entity.metadata && entity.metadata.items) { + return entity.metadata.items + .filter((item: StateEntityDetailsResponseItem) => item.key === 'symbol') + .map((item: StateEntityDetailsResponseItem) => { + const fungibleItem = fungiblesData.items.find((fungible: FungibleResourcesCollectionItem) => { + return fungible.resource_address === entity.address; + }) as FungibleResourcesCollectionItem & { amount: number }; + + return { + as_string: item.value.typed.value, + amount: fungibleItem ? Number(fungibleItem?.amount) : null, + address: entity.address, + }; + }); + } + return []; + }); +}; + +export const updateAssetsFields = (assets: AssetsItemModel[], fields: any[], fieldName: string): AssetsItemModel[] => + assets.map((asset) => { + const field = fields.find((field) => field.field_name === fieldName); + const matchingItem = field?.entries.find((entry: any) => entry.key.value === asset.address); + const updatedValue = matchingItem ? parseFloat(matchingItem.value.value) : 0; + + return { + ...asset, + [fieldName]: updatedValue, + }; + }); diff --git a/8-yield-derivatives/SRWA-Yield-Derivatives/dApp/src/features/index.ts b/8-yield-derivatives/SRWA-Yield-Derivatives/dApp/src/features/index.ts new file mode 100644 index 000000000..a5b007675 --- /dev/null +++ b/8-yield-derivatives/SRWA-Yield-Derivatives/dApp/src/features/index.ts @@ -0,0 +1,225 @@ +import { ProgrammaticScryptoSborValue } from '@radixdlt/babylon-gateway-api-sdk'; +import { StateEntityDetailsResponseItem } from '@radixdlt/babylon-gateway-api-sdk/dist/generated/models/StateEntityDetailsResponseItem'; +import { SendTransaction } from '@radixdlt/radix-dapp-toolkit'; +import { toast } from 'react-toastify'; + +import { findUserBadgeAddress, setInitialAssets, updateAssetsFields } from './helpers'; +// import {AssetsItemModel} from '../app/shared/models/index.model'; +import { stateApi } from '../helpers'; +import { getTransactionStatus } from '../helpers/get-transaction-status'; +import { depositAndCreateUserManifest, depositAssetManifest, withdrawAssetManifest } from '../manifests'; +import { handleErrorManifest } from '../manifests/helpers'; +import { useBearStore } from '../store'; + +const fetchUserBadge = async (accountAddress: string) => { + const data = await stateApi.entityNonFungiblesPage({ + stateEntityNonFungiblesPageRequest: { + address: accountAddress, + }, + }); + + useBearStore.setState({ userBadge: data.items.length > 0 ? await findUserBadgeAddress(data) : undefined }); +}; + +const depositAndCreateUser = async ( + sendTx: SendTransaction, + tokenAmount: number, + address: string, + tokenAddress: string, + componentAddress: string, +) => { + const result = await sendTx(depositAndCreateUserManifest(tokenAmount, address, tokenAddress, componentAddress)); + + handleErrorManifest(result); + + if (result.value) { + const status = await getTransactionStatus(result.value.transactionIntentHash); + + if (status.status === 'CommittedSuccess') { + toast.success('Transaction successful.'); + } + + return { + result, + }; + } +}; + +const depositAsset = async ( + sendTx: SendTransaction, + tokenAmount: number, + userBadge: string | null, + address: string, + tokenAddress: string, + componentAddress: string, +) => { + const result = await sendTx(depositAssetManifest(tokenAmount, userBadge, address, tokenAddress, componentAddress)); + + handleErrorManifest(result); + + if (result.value) { + const status = await getTransactionStatus(result.value.transactionIntentHash); + + if (status.status === 'CommittedSuccess') { + toast.success('Transaction successful.'); + } + + return { + result, + }; + } +}; + +const withdrawAsset = async ( + sendTx: SendTransaction, + userBadge: string | undefined, + address: string, + tokenAddress: string, + componentAddress: string, +) => { + const result = await sendTx(withdrawAssetManifest(userBadge, address, tokenAddress, componentAddress)); + + handleErrorManifest(result); + + if (result.value) { + const status = await getTransactionStatus(result.value.transactionIntentHash); + + if (status.status === 'CommittedSuccess') { + toast.success('Transaction successful.'); + } + + return { + result, + }; + } +}; + +const fetchAssets = async (address: string, addressType: string) => { + const fungiblesData = await stateApi.entityFungiblesPage({ + stateEntityFungiblesPageRequest: { + address: address, + }, + }); + + const resourceAddresses = fungiblesData.items.map((item) => item.resource_address); + + if (resourceAddresses.length === 0) { + if (addressType === 'account') { + useBearStore.setState({ accountAssets: [] }); + } + return []; + } + + const entityData: StateEntityDetailsResponseItem = { items: [] }; + + const chunkSize: number = 20; + const addressChunks: string[][] = []; + for (let i = 0; i < resourceAddresses.length; i += chunkSize) { + addressChunks.push(resourceAddresses.slice(i, i + chunkSize)); + } + + for (const chunk of addressChunks) { + const data = await stateApi.stateEntityDetails({ + stateEntityDetailsRequest: { + addresses: chunk, + }, + }); + entityData.items.push(...data.items); + } + + if (addressType === 'component') { + useBearStore.setState({ yieldAssets: setInitialAssets(entityData, fungiblesData) }); + } else { + const accountAssets = setInitialAssets(entityData, fungiblesData); + const yieldAssets = useBearStore.getState().yieldAssets; + const filteredAccountAssets = accountAssets.filter((item) => + yieldAssets.some((subItem) => subItem.address === item.address), + ); + + useBearStore.setState({ accountAssets: filteredAccountAssets }); + } +}; + +const fetchComponentAssets = async (componentAddress: string) => { + if (!componentAddress) { + return; + } + + const accountAssets = useBearStore.getState().accountAssets; + const yieldAssets = useBearStore.getState().yieldAssets; + + const data = await stateApi.stateEntityDetails({ + stateEntityDetailsRequest: { + addresses: [componentAddress], + }, + }); + + // @ts-expect-error: Property 'state' does not exist on type 'StateEntityDetailsResponseItemDetails'. + const fields = data?.items[0]?.details?.state?.fields; + + const principalTokensSymbolsField = fields.find((field) => field.field_name === 'principal_tokens_symbols'); + const addressesPrincipalTokensSymbolsField = + principalTokensSymbolsField?.entries.map((entry) => entry.key.value) || []; + const filteredAssets = yieldAssets.filter((resource) => + addressesPrincipalTokensSymbolsField.includes(resource.address), + ); + + // COMPONENT ASSETS + const updatedPrincipalTotalBalances = updateAssetsFields(filteredAssets, fields, 'total_balances'); + const updatedPrincipalYieldRates = updateAssetsFields(updatedPrincipalTotalBalances, fields, 'yield_rates'); + useBearStore.setState({ componentAssets: updatedPrincipalYieldRates }); + + // ACCOUNT ASSETS + const userBadge = useBearStore.getState().userBadge; + const usersField = fields.find((field: ProgrammaticScryptoSborValue) => field.field_name === 'users'); + const connectedUser = usersField.entries.find((user: StateEntityDetailsResponseItem) => user.key.value === userBadge); + + if (connectedUser) { + const principalTokensDepositBalancesField = connectedUser.value.fields.find( + (field) => field.field_name === 'deposit_balances', + ); + + const mergedArray = accountAssets.map((item) => { + const additionalFieldsItem = principalTokensDepositBalancesField.entries.find( + (field) => field.key.value === item.address, + ); + if (additionalFieldsItem) { + const fields = additionalFieldsItem.value.fields; + const principalBalance = fields.find((field) => field.field_name === 'principal_balance'); + const yieldBalance = fields.find((field) => field.field_name === 'yield_balance'); + const depositedAt = fields.find((field) => field.field_name === 'deposited_at'); + return { + ...item, + principal_balance: Number(principalBalance?.value) || 0, + yield_balance: Number(yieldBalance?.value) || 0, + deposited_at: Number(depositedAt?.value) || 0, + }; + } else { + return item; + } + }); + + useBearStore.setState({ accountAssets: mergedArray }); + } else { + useBearStore.setState({ accountAssets: accountAssets }); + } + + // YIELD ASSETS + const yieldTokensField = fields.find((field) => field.field_name === 'yield_tokens'); + const addressesYieldTokensField = yieldTokensField?.entries.map((entry) => entry.value.value) || []; + const filteredYieldAssets = yieldAssets.filter((resource) => addressesYieldTokensField.includes(resource.address)); + const updatedYieldTotalBalances = updateAssetsFields(filteredYieldAssets, fields, 'total_balances'); + + useBearStore.setState({ yieldAssets: updatedYieldTotalBalances }); +}; + +const featuresService = { + fetchUserBadge, + depositAndCreateUser, + depositAsset, + withdrawAsset, + fetchAssets, + fetchComponentAssets, +}; + +export default featuresService; diff --git a/8-yield-derivatives/SRWA-Yield-Derivatives/dApp/src/gateway/gateway-api.ts b/8-yield-derivatives/SRWA-Yield-Derivatives/dApp/src/gateway/gateway-api.ts new file mode 100644 index 000000000..633f3578c --- /dev/null +++ b/8-yield-derivatives/SRWA-Yield-Derivatives/dApp/src/gateway/gateway-api.ts @@ -0,0 +1,59 @@ +import { GatewayApiClient as BabylonGatewayApiClient } from '@radixdlt/babylon-gateway-api-sdk'; +import { ResultAsync } from 'neverthrow'; + +import { errorIdentity } from '../helpers/error-identity'; + +export type GatewayApiClient = ReturnType; + +export const createGatewayApiClient = ({ basePath }: { basePath: string; dAppDefinitionAddress?: string }) => { + const { transaction, state, status } = BabylonGatewayApiClient.initialize({ + basePath, + applicationName: 'SRWA Yield Derivatives', + }); + + const getTransactionStatus = (transactionIntentHashHex: string) => + ResultAsync.fromPromise(transaction.getStatus(transactionIntentHashHex), errorIdentity); + + const getTransactionDetails = (transactionIntentHashHex: string) => + ResultAsync.fromPromise(transaction.getCommittedDetails(transactionIntentHashHex), errorIdentity); + + const getEntityDetails = (address: string) => + ResultAsync.fromPromise(state.getEntityDetailsVaultAggregated(address), errorIdentity); + + const getEntitiesDetails = (addresses: string[]) => + ResultAsync.fromPromise(state.getEntityDetailsVaultAggregated(addresses), errorIdentity); + + const getEntityNonFungibleIds = ({ + accountAddress, + nftAddress, + vaultAddress, + }: { + accountAddress: string; + nftAddress: string; + vaultAddress: string; + }) => + ResultAsync.fromPromise( + state.innerClient.entityNonFungibleIdsPage({ + stateEntityNonFungibleIdsPageRequest: { + address: accountAddress, + vault_address: vaultAddress, + resource_address: nftAddress, + }, + }), + errorIdentity, + ); + + const getNetworkConfiguration = () => ResultAsync.fromPromise(status.getNetworkConfiguration(), errorIdentity); + + return { + getTransactionStatus, + getTransactionDetails, + getEntityDetails, + getEntitiesDetails, + getEntityNonFungibleIds, + getNetworkConfiguration, + transactionApi: transaction, + stateApi: state, + statusApi: status, + }; +}; diff --git a/8-yield-derivatives/SRWA-Yield-Derivatives/dApp/src/helpers/create-challenge.ts b/8-yield-derivatives/SRWA-Yield-Derivatives/dApp/src/helpers/create-challenge.ts new file mode 100644 index 000000000..d6ba9f37f --- /dev/null +++ b/8-yield-derivatives/SRWA-Yield-Derivatives/dApp/src/helpers/create-challenge.ts @@ -0,0 +1,3 @@ +import { Buffer } from 'buffer'; + +export const createChallenge = () => Buffer.from(crypto.getRandomValues(new Uint8Array(32))).toString('hex'); diff --git a/8-yield-derivatives/SRWA-Yield-Derivatives/dApp/src/helpers/error-identity.ts b/8-yield-derivatives/SRWA-Yield-Derivatives/dApp/src/helpers/error-identity.ts new file mode 100644 index 000000000..7b9a31e41 --- /dev/null +++ b/8-yield-derivatives/SRWA-Yield-Derivatives/dApp/src/helpers/error-identity.ts @@ -0,0 +1 @@ +export const errorIdentity = (e: unknown) => e as Error; diff --git a/8-yield-derivatives/SRWA-Yield-Derivatives/dApp/src/helpers/find-metadata.ts b/8-yield-derivatives/SRWA-Yield-Derivatives/dApp/src/helpers/find-metadata.ts new file mode 100644 index 000000000..355067f8e --- /dev/null +++ b/8-yield-derivatives/SRWA-Yield-Derivatives/dApp/src/helpers/find-metadata.ts @@ -0,0 +1,4 @@ +export const getStringMetadata = (key: string, object: { metadata: any[] }) => { + const metadata = object.metadata.find((m) => m.key === key); + return metadata ? metadata.value.typed.value : undefined; +}; diff --git a/8-yield-derivatives/SRWA-Yield-Derivatives/dApp/src/helpers/get-network-id.ts b/8-yield-derivatives/SRWA-Yield-Derivatives/dApp/src/helpers/get-network-id.ts new file mode 100644 index 000000000..b362b72e4 --- /dev/null +++ b/8-yield-derivatives/SRWA-Yield-Derivatives/dApp/src/helpers/get-network-id.ts @@ -0,0 +1,5 @@ +import { RadixNetwork, RadixNetworkConfig } from '@radixdlt/babylon-gateway-api-sdk'; + +const networkId = RadixNetworkConfig?.['Stokenet']?.networkId; + +export const DEFAULT_NETWORK_ID = networkId ? String(networkId) : RadixNetwork.Stokenet.toString(); diff --git a/8-yield-derivatives/SRWA-Yield-Derivatives/dApp/src/helpers/get-transaction-status.ts b/8-yield-derivatives/SRWA-Yield-Derivatives/dApp/src/helpers/get-transaction-status.ts new file mode 100644 index 000000000..11c59934e --- /dev/null +++ b/8-yield-derivatives/SRWA-Yield-Derivatives/dApp/src/helpers/get-transaction-status.ts @@ -0,0 +1,15 @@ +import { transactionApi } from './index'; + +// ************ Fetch the transaction status from the Gateway API ************ +export async function getTransactionStatus(transactionIntentHash: string) { + try { + return await transactionApi.transactionStatus({ + transactionStatusRequest: { + intent_hash: transactionIntentHash, + }, + }); + } catch (error) { + console.error('Error getting transaction status:', error); + throw error; + } +} diff --git a/8-yield-derivatives/SRWA-Yield-Derivatives/dApp/src/helpers/index.ts b/8-yield-derivatives/SRWA-Yield-Derivatives/dApp/src/helpers/index.ts new file mode 100644 index 000000000..69da3556b --- /dev/null +++ b/8-yield-derivatives/SRWA-Yield-Derivatives/dApp/src/helpers/index.ts @@ -0,0 +1,49 @@ +import { Configuration, RadixNetworkConfigById, StateApi, TransactionApi } from '@radixdlt/babylon-gateway-api-sdk'; + +import { networkId as networkIdSubject } from '../network/state'; + +const networkId = networkIdSubject.value; + +export const COMPONENT_ADDRESS = import.meta.env.VITE_COMPONENT_ADDRESS; +export const TOTAL_YT_SUPPLY = 1000000; + +const gatewayApiConfig = new Configuration({ basePath: RadixNetworkConfigById[networkId].gatewayUrl }); + +export const stateApi = new StateApi(gatewayApiConfig); + +export const transactionApi = new TransactionApi(gatewayApiConfig); + +export const formattedNumber = (number: number | null, max = 4): string => { + if (number === null) return ''; + + const formatter: Intl.NumberFormat = new Intl.NumberFormat('en-US', { + minimumFractionDigits: max === 4 ? 1 : 2, + maximumFractionDigits: max, + notation: 'compact', + }); + return formatter.format(number); +}; + +export const roundDownNumber = (amount: number, decimal = 2): number => { + decimal = +decimal; + const value = +(1 + new Array(decimal + 1).join('0').slice(-decimal)); + return Math.floor(+amount * value) / value; +}; + +export const calculateDaysLeft = (timestamp: number, daysLeft: number): number => { + if (isNaN(timestamp)) { + return 0; + } + + // Convert Unix timestamp to milliseconds and add 30 days (in milliseconds) + const futureDate = new Date((timestamp + daysLeft * 24 * 60 * 60) * 1000); + + // Get today's date + const today = new Date(); + + // Calculate the difference in milliseconds between the future date and today's date + const differenceInMilliseconds = futureDate.getTime() - today.getTime(); + + // Convert milliseconds to days + return Math.ceil(differenceInMilliseconds / (1000 * 60 * 60 * 24)); +}; diff --git a/8-yield-derivatives/SRWA-Yield-Derivatives/dApp/src/helpers/shorten-address.ts b/8-yield-derivatives/SRWA-Yield-Derivatives/dApp/src/helpers/shorten-address.ts new file mode 100644 index 000000000..cfcc84ce8 --- /dev/null +++ b/8-yield-derivatives/SRWA-Yield-Derivatives/dApp/src/helpers/shorten-address.ts @@ -0,0 +1,8 @@ +export const shortenAddress = (address?: string) => { + if (!address) { + console.warn('Address is undefined'); + return ''; + } + + return `${address.slice(0, 4)}...${address.slice(address.length - 6, address.length)}`; +}; diff --git a/8-yield-derivatives/SRWA-Yield-Derivatives/dApp/src/index.css b/8-yield-derivatives/SRWA-Yield-Derivatives/dApp/src/index.css new file mode 100644 index 000000000..5efe62636 --- /dev/null +++ b/8-yield-derivatives/SRWA-Yield-Derivatives/dApp/src/index.css @@ -0,0 +1,46 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +@keyframes spinAround { + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } +} + +.btn-is-loading { + color: transparent !important; + pointer-events: none; + user-select: none; + + &.with-text { + color: #ffffff !important; + font-size: 14px; + + &.right { + text-align: right; + } + + &::after { + left: 12.5px; + } + } + + &::after { + animation: spinAround 500ms infinite linear; + border: 2px solid #ffffff; + border-radius: 100%; + border-right-color: transparent; + border-top-color: transparent; + content: ''; + display: block; + height: 25px; + width: 25px; + left: calc(50% - (25px / 2)); + top: calc(50% - (25px / 2)); + position: absolute !important; + } +} diff --git a/8-yield-derivatives/SRWA-Yield-Derivatives/dApp/src/main.tsx b/8-yield-derivatives/SRWA-Yield-Derivatives/dApp/src/main.tsx new file mode 100644 index 000000000..5b78b4768 --- /dev/null +++ b/8-yield-derivatives/SRWA-Yield-Derivatives/dApp/src/main.tsx @@ -0,0 +1,18 @@ +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import { ToastContainer } from 'react-toastify'; + +import App from './App.tsx'; +import { rdt } from './rdt/rdt'; +import { RdtProvider } from './rdt/rdt-provider'; +import 'react-toastify/dist/ReactToastify.css'; +import './index.css'; + +ReactDOM.createRoot(document.getElementById('root')!).render( + + + + + + , +); diff --git a/8-yield-derivatives/SRWA-Yield-Derivatives/dApp/src/manifests/helpers/index.ts b/8-yield-derivatives/SRWA-Yield-Derivatives/dApp/src/manifests/helpers/index.ts new file mode 100644 index 000000000..62ccd2172 --- /dev/null +++ b/8-yield-derivatives/SRWA-Yield-Derivatives/dApp/src/manifests/helpers/index.ts @@ -0,0 +1,16 @@ +import { toast } from 'react-toastify'; + +type ErrorType = { + interactionId: string; + error: string; +}; + +type ResultType = { + error?: ErrorType; +}; + +export const handleErrorManifest = (result: ResultType): void => { + if (result.error) { + toast.error(result.error.error); + } +}; diff --git a/8-yield-derivatives/SRWA-Yield-Derivatives/dApp/src/manifests/index.ts b/8-yield-derivatives/SRWA-Yield-Derivatives/dApp/src/manifests/index.ts new file mode 100644 index 000000000..debd8d540 --- /dev/null +++ b/8-yield-derivatives/SRWA-Yield-Derivatives/dApp/src/manifests/index.ts @@ -0,0 +1,86 @@ +export const depositAndCreateUserManifest = ( + amount: number, + accountAddress: string, + tokenAddress: string, + componentAddress: string, +) => ` +CALL_METHOD + Address("${accountAddress}") + "withdraw" + Address("${tokenAddress}") + Decimal("${amount}"); +TAKE_FROM_WORKTOP + Address("${tokenAddress}") + Decimal("${amount}") + Bucket("bucket1"); +CALL_METHOD + Address("${componentAddress}") + "create_user_and_deposit_principal" + Bucket("bucket1"); +CALL_METHOD + Address("${accountAddress}") + "deposit_batch" + Expression("ENTIRE_WORKTOP"); +`; + +export const depositAssetManifest = ( + amount: number, + userBadge: string | null, + accountAddress: string, + tokenAddress: string, + componentAddress: string, +) => ` +CALL_METHOD + Address("${accountAddress}") + "create_proof_of_non_fungibles" + Address("${userBadge}") + Array(NonFungibleLocalId("#1#")); +CREATE_PROOF_FROM_AUTH_ZONE_OF_NON_FUNGIBLES + Address("${userBadge}") + Array(NonFungibleLocalId("#1#")) + Proof("proof1"); +CALL_METHOD + Address("${accountAddress}") + "withdraw" + Address("${tokenAddress}") + Decimal("${amount}"); +TAKE_FROM_WORKTOP + Address("${tokenAddress}") + Decimal("${amount}") + Bucket("bucket1"); +CALL_METHOD + Address("${componentAddress}") + "deposit_principal" + Bucket("bucket1") + Proof("proof1"); +CALL_METHOD + Address("${accountAddress}") + "deposit_batch" + Expression("ENTIRE_WORKTOP"); +`; + +export const withdrawAssetManifest = ( + userBadge: string | undefined, + accountAddress: string, + tokenAddress: string, + componentAddress: string, +) => ` +CALL_METHOD + Address("${accountAddress}") + "create_proof_of_non_fungibles" + Address("${userBadge}") + Array(NonFungibleLocalId("#1#")); +CREATE_PROOF_FROM_AUTH_ZONE_OF_NON_FUNGIBLES + Address("${userBadge}") + Array(NonFungibleLocalId("#1#")) + Proof("UserBadge"); +CALL_METHOD + Address("${componentAddress}") + "redeem" + Address("${tokenAddress}") + Proof("UserBadge"); +CALL_METHOD + Address("${accountAddress}") + "deposit_batch" + Expression("ENTIRE_WORKTOP"); +`; diff --git a/8-yield-derivatives/SRWA-Yield-Derivatives/dApp/src/network/state.ts b/8-yield-derivatives/SRWA-Yield-Derivatives/dApp/src/network/state.ts new file mode 100644 index 000000000..757eaac69 --- /dev/null +++ b/8-yield-derivatives/SRWA-Yield-Derivatives/dApp/src/network/state.ts @@ -0,0 +1,23 @@ +import { GatewayApiClient, RadixNetworkConfigById } from '@radixdlt/babylon-gateway-api-sdk'; +import { BehaviorSubject } from 'rxjs'; + +import { DEFAULT_NETWORK_ID } from '../helpers/get-network-id'; + +export const bootstrapNetwork = (networkId: number) => { + const gatewayApi = GatewayApiClient.initialize({ + basePath: RadixNetworkConfigById[networkId].gatewayUrl, + applicationName: 'SRWA Yield Derivatives', + }); + gatewayApi.status.getNetworkConfiguration().then((response: any) => { + return xrdAddress.next(response.well_known_addresses.xrd); + }); +}; + +const xrdAddress = new BehaviorSubject(undefined); + +const getNetworkIdDefault = () => { + const urlParams = new URLSearchParams(window.location.search); + return parseInt(urlParams.get('networkId') || localStorage.getItem('networkId') || DEFAULT_NETWORK_ID, 10); +}; + +export const networkId = new BehaviorSubject(getNetworkIdDefault()); diff --git a/8-yield-derivatives/SRWA-Yield-Derivatives/dApp/src/pages/Home/Deposit/index.tsx b/8-yield-derivatives/SRWA-Yield-Derivatives/dApp/src/pages/Home/Deposit/index.tsx new file mode 100644 index 000000000..a13895e85 --- /dev/null +++ b/8-yield-derivatives/SRWA-Yield-Derivatives/dApp/src/pages/Home/Deposit/index.tsx @@ -0,0 +1,175 @@ +import { ChangeEvent, useState } from 'react'; +import { SubmitHandler, useForm } from 'react-hook-form'; + +import { AssetsItemModel } from '../../../app/shared/models/index.model'; +import featuresService from '../../../features'; +import { COMPONENT_ADDRESS, formattedNumber, roundDownNumber } from '../../../helpers'; +import { useSendTransaction } from '../../../rdt/hooks/useSendTransaction'; + +type DepositProps = { + onActionChange: (action: string) => void; + userBadge: string | undefined; + accountAddress: string; + accountAssets: AssetsItemModel[]; +}; +type FormData = { + amount: string; + asset: string; +}; + +function Deposit({ onActionChange, userBadge, accountAddress, accountAssets }: DepositProps) { + const sendTransaction = useSendTransaction(); + const [selectedAsset, setSelectedAsset] = useState(accountAssets[0].address); + + const { + register, + reset, + formState: { errors }, + handleSubmit, + setValue, + } = useForm(); + const [isLoading, setIsLoading] = useState(false); + + const handleActionChange = (action: string) => { + onActionChange(action); + }; + const onSubmit: SubmitHandler = async (data) => { + setIsLoading(true); + + if (!userBadge) { + const response = await featuresService.depositAndCreateUser( + sendTransaction, + Number(data.amount), + accountAddress, + data.asset, + COMPONENT_ADDRESS, + ); + + if (response) { + await featuresService.fetchUserBadge(accountAddress); + await onSuccessSubmit(); + } + setIsLoading(false); + } else { + const response = await featuresService.depositAsset( + sendTransaction, + Number(data.amount), + userBadge, + accountAddress, + data.asset, + COMPONENT_ADDRESS, + ); + + if (response) { + await onSuccessSubmit(); + } + + setIsLoading(false); + } + }; + const onSuccessSubmit = async () => { + reset(); + await featuresService.fetchAssets(COMPONENT_ADDRESS, 'component'); + await featuresService.fetchAssets(accountAddress, 'account'); + await featuresService.fetchComponentAssets(COMPONENT_ADDRESS); + }; + const handleAssetChange = (event: ChangeEvent) => { + setSelectedAsset(event.target.value); + setValue('amount', ''); + }; + const handleSetMaxAmount = () => { + setValue('amount', handleMaxAmount().toString()); + }; + const handleMaxAmount = () => { + return Number(activeAccountAsset?.amount ?? 0); + }; + + const activeAccountAsset = accountAssets.find((asset) => asset.address === selectedAsset); + + return ( +
+

DEPOSIT

+ +
+ +
+ + + Wallet Balance: {formattedNumber(roundDownNumber(activeAccountAsset?.amount ?? 0), 2)}{' '} + {activeAccountAsset?.as_string} + + {errors.asset && ( +

+ {errors.asset.message} +

+ )} +
+
+
+ +
+ + Number(value) <= handleMaxAmount() || `Value cannot be greater than ${handleMaxAmount()}`, + }, + })} + id="amount" + type="text" + className="w-10/12 rounded-md border-0 p-1.5 text-gray-900 ring-1 ring-gray-300 placeholder:text-gray-400 sm:text-sm sm:leading-6" + /> + +
+ {errors.amount && ( +

+ {errors.amount.message} +

+ )} +
+
+ + +
+
+ ); +} + +export default Deposit; diff --git a/8-yield-derivatives/SRWA-Yield-Derivatives/dApp/src/pages/Home/Statistics/index.tsx b/8-yield-derivatives/SRWA-Yield-Derivatives/dApp/src/pages/Home/Statistics/index.tsx new file mode 100644 index 000000000..fe479b529 --- /dev/null +++ b/8-yield-derivatives/SRWA-Yield-Derivatives/dApp/src/pages/Home/Statistics/index.tsx @@ -0,0 +1,120 @@ +import { AssetsItemModel } from '../../../app/shared/models/index.model'; +import { TOTAL_YT_SUPPLY, calculateDaysLeft, formattedNumber } from '../../../helpers'; + +type StatisticsProps = { + onActionChange: (action: string) => void; + accountAssets: AssetsItemModel[]; + yieldAssets: AssetsItemModel[]; + componentAssets: AssetsItemModel[]; + userBadge: string; +}; + +function Statistics({ onActionChange, accountAssets, yieldAssets, componentAssets, userBadge }: StatisticsProps) { + const handleActionChange = (action: string) => { + onActionChange(action); + }; + + return ( +
+
+

REWARDS

+ + + + + + + + + + + + {yieldAssets.map((asset: AssetsItemModel) => ( + + + + + + + + ))} + +
ASSETTOTALREALIZEDPENDINGAVAILABLE
{asset.as_string}{formattedNumber(TOTAL_YT_SUPPLY, 2)}{formattedNumber(TOTAL_YT_SUPPLY - (asset.amount ?? 0), 2)}{formattedNumber(asset.total_balances, 2)}{formattedNumber((asset.amount ?? 0) - (asset.total_balances ?? 0), 2)}
+ +

DEPOSITS

+ + + + + + + + + + {componentAssets.map((item: AssetsItemModel) => ( + + + + + + ))} + +
ASSETTOTALAVAILABLE
{item.as_string}{formattedNumber(item.total_balances, 2)}{formattedNumber(TOTAL_YT_SUPPLY / item.yield_rates - item.total_balances || 0, 2)}
+
+
1 ? 'border-b border-slate-300' : ''}`} + > + {userBadge ? ( + + + + + + + + + + + {accountAssets + .filter( + (item: AssetsItemModel) => + 'deposited_at' in item && 'principal_balance' in item && 'yield_balance' in item, + ) + .map((item: AssetsItemModel) => ( + + + + + + + ))} + +
ASSETMY DEPOSITMY YIELDUNLOCKING IN
{item.as_string}{formattedNumber(item.principal_balance, 2)}{formattedNumber(item.yield_balance, 2)} YT{calculateDaysLeft(item.deposited_at, 30)} days
+ ) : null} + + {accountAssets.length > 0 && ( +
+ + {userBadge ? ( + + ) : null} +
+ )} +
+
+ ); +} + +export default Statistics; diff --git a/8-yield-derivatives/SRWA-Yield-Derivatives/dApp/src/pages/Home/Withdrawal/index.tsx b/8-yield-derivatives/SRWA-Yield-Derivatives/dApp/src/pages/Home/Withdrawal/index.tsx new file mode 100644 index 000000000..3052115e5 --- /dev/null +++ b/8-yield-derivatives/SRWA-Yield-Derivatives/dApp/src/pages/Home/Withdrawal/index.tsx @@ -0,0 +1,108 @@ +import { useState } from 'react'; +import { SubmitHandler, useForm } from 'react-hook-form'; + +import { AssetsItemModel } from '../../../app/shared/models/index.model'; +import featuresService from '../../../features'; +import { COMPONENT_ADDRESS } from '../../../helpers'; +import { useSendTransaction } from '../../../rdt/hooks/useSendTransaction'; + +type DepositProps = { + onActionChange: (action: string) => void; + userBadge: string | undefined; + accountAddress: string; + accountAssets: AssetsItemModel[]; +}; +type FormData = { + asset: string; +}; + +function Withdrawal({ onActionChange, userBadge, accountAddress, accountAssets }: DepositProps) { + const sendTransaction = useSendTransaction(); + + const { + register, + reset, + formState: { errors }, + handleSubmit, + } = useForm(); + const [isLoading, setIsLoading] = useState(false); + + const handleActionChange = (action: string) => { + onActionChange(action); + }; + const onSubmit: SubmitHandler = async (data) => { + setIsLoading(true); + + const response = await featuresService.withdrawAsset( + sendTransaction, + userBadge, + accountAddress, + data.asset, + COMPONENT_ADDRESS, + ); + + if (response) { + reset(); + await featuresService.fetchAssets(COMPONENT_ADDRESS, 'component'); + await featuresService.fetchAssets(accountAddress, 'account'); + await featuresService.fetchComponentAssets(COMPONENT_ADDRESS); + } + + setIsLoading(false); + }; + + return ( +
+

WITHDRAWAL

+ +

YT IS UNLOCKED

+

+ YT IS LOCKED(UNLOCKING PERIOD HAS NOT EXPIRED) +

+
+ +
+ + {errors.asset && ( +

+ {errors.asset.message} +

+ )} +
+
+
+ + +
+
+ ); +} + +export default Withdrawal; diff --git a/8-yield-derivatives/SRWA-Yield-Derivatives/dApp/src/pages/Home/index.tsx b/8-yield-derivatives/SRWA-Yield-Derivatives/dApp/src/pages/Home/index.tsx new file mode 100644 index 000000000..9055eb7ea --- /dev/null +++ b/8-yield-derivatives/SRWA-Yield-Derivatives/dApp/src/pages/Home/index.tsx @@ -0,0 +1,110 @@ +import { useEffect, useState } from 'react'; + +import Deposit from './Deposit'; +import Statistics from './Statistics'; +import Withdrawal from './Withdrawal'; +import featuresService from '../../features'; +import { COMPONENT_ADDRESS } from '../../helpers'; +import { useBearStore } from '../../store'; + +function Home() { + const accountAddress = useBearStore((state) => state.accountAddress); + const accountAssets = useBearStore((state) => state.accountAssets); + const yieldAssets = useBearStore((state) => state.yieldAssets); + const componentAssets = useBearStore((state) => state.componentAssets); + const userBadge = useBearStore((state) => state.userBadge); + + const [activeAction, setActiveAction] = useState(''); + + const handleActionChange = (action: string) => { + setActiveAction(action); + }; + + useEffect(() => { + (async () => { + if (!accountAddress) return; + await featuresService.fetchUserBadge(accountAddress); + await featuresService.fetchAssets(COMPONENT_ADDRESS, 'component'); + await featuresService.fetchAssets(accountAddress, 'account'); + await featuresService.fetchComponentAssets(COMPONENT_ADDRESS); + })(); + }, [accountAddress]); + + console.log('accountAssets: ', accountAssets); + console.log('yieldAssets: ', yieldAssets); + console.log('componentAssets: ', componentAssets); + // console.log('userBadge: ', userBadge); + + return ( +
+