Assigned: Chainsight BNT-1 - Asset Rating Oracle

Sorry for the inconvenience, and thanks for your advice.

canister wasm’s not being generated or not being recognised as such

The csx build will generate a .wasm deployment in the artifacts folder and a dfx.json with them.

If you do a deploy or exec immediately after build, there should be no problem…

By the way, do you know what action makes the wasm disappear (or dfx.json updated etc)?

canister controllers are not being registered properly

csx exec consumes cycles even when execution is unsuccessful

I thought I had resolved this issue with the upgrade I told you about the other day.

I will check this as well.

It certainly may not capture the consideration of cross sections due to cycles deficiency failures or changes in them.

csx deploy won’t run in full if some canisters are already deployed

Sorry, this is as you say.

We are aware of the lack of functionality regarding resume/suspend.

If you are concerned about failure of individual components, it would be helpful if you could mainly use individual operations using -c for both deploy and exec.

csx exec --only-execute-cmds won’t tolerate manual changes to cycle expenditure

Likewise, you are correct, and I apologize for the inconvenience.

Thank you.

That’s all fine, and I figure it’s all just part of the process at this stage. Not sure what happened with the wasm’s. I think they were in the artifacts folder but somehow not being saved and/or recognised properly on the IC (or so it seemed - I couldn’t test this). The CLI may have been updated between build and deploy/exec on my end, so perhaps that affected it.

1 Like

Excellent! Thanks for the report.
Also, since you put it up on Github, it’s easy for us to check too.
May I ask you a few questions and tell you what I would like to see corrected?

Questions

[1] What is the request url/host used in the snapshot_indexer_https datasource for algorithm_lens?
I did a little research and could not find out what services are offered by them.
Since it is http, it is originally in the web2 and has limited reliability, but if the service is too minor, it may be too unreliable.
Also, this host does not appear to be able to communicate over ipv6. It probably is not running on the icp network. Please check.
It would be better if snapshot_indexer_evm (& algorithm_lens) could be used to realize this, since the data that can be obtained from those paths would be similar to what can be obtained from UniswapV3 (or processed from them).

[2] What are the multiple algorithm_lens, but with slightly different logic?
There are multiple algorithm_lens for data with different data contents but the same format. I imagine that they use the same logic, but there are some subtle differences. What are the reasons for these differences?

diff src/logics/usdc_eth_0_3_algorithm_lens/src/lib.rs src/logics/vra_eth_0_3_algorithm_lens/src/lib.rs 
1c1
< use usdc_eth_0_3_algorithm_lens_accessors :: * ;
---
> use vra_eth_0_3_algorithm_lens_accessors :: * ;
25,27c25,27
<     let pool_fees_result = get_get_last_snapshot_value_in_usdc_eth_0_3_pool_fees (targets . get (0usize) . unwrap () . clone ()) . await ;
<     let tc28x6_result = get_get_last_snapshot_value_in_usdc_eth_0_3_tcumul_28x6hr (targets . get (1usize) . unwrap () . clone ()) . await ;
<     let v3pool_result = get_get_last_snapshot_value_in_usdc_eth_0_3_v3pool (targets . get (3usize) . unwrap () . clone ()) . await ;
---
>     let pool_fees_result = get_get_last_snapshot_value_in_vra_eth_0_3_pool_fees (targets . get (0usize) . unwrap () . clone ()) . await ;
>     let tc28x6_result = get_get_last_snapshot_value_in_vra_eth_0_3_tcumul_28x6hr (targets . get (1usize) . unwrap () . clone ()) . await ;
>     let v3pool_result = get_get_last_snapshot_value_in_vra_eth_0_3_v3pool (targets . get (3usize) . unwrap () . clone ()) . await ;
46c46
<     let current_price = current_price / f32::powf(10.0,12.0);
---
>     current_price = current_price / f32::powf(10.0,12.0);
102c102
<     let tvl_in_range = sum_liquidity as f32
---
>     let mut tvl_in_range = sum_liquidity as f32
105a106
> 	let tvl_in_range = tvl_in_range / current_price;
110a112,117
>     current_price = 1.0 / current_price;
>     let new_range_top = 1.0 / range_bottom;
>     let new_range_bottom = 1.0 / range_top;
>     range_top = new_range_top;
>     range_bottom = new_range_bottom;
> 

About bug(?)

[1] Specify lens_target in algorithm_lens manifest
In the algorithm_lens logic, we are trying to make a cross canister call to 4 snapshot_indexer_https using the “targets” argument.
However, since lens_target is not specified in manifest, I think this “targets” is empty and the id of the calling canister is not passed.

[2] The caller snapshot_indexer_https is 4 in total, but tries to access it with index 4 (= len=5)
I think targets.get(4usize) is None.

pub async fn calculate (targets : Vec < String >) -> LensValue {
    let pool_fees_result = get_get_last_snapshot_value_in_rndr_eth_1_pool_fees (targets . get (0usize) . unwrap () . clone ()) . await ;
    let tc28x6_result = get_get_last_snapshot_value_in_rndr_eth_1_tcumul_28x6hr (targets . get (1usize) . unwrap () . clone ()) . await ;
    let v3pool_result = get_get_last_snapshot_value_in_rndr_eth_1_v3pool (targets . get (3usize) . unwrap () . clone ()) . await ;
    let eth_usdc_price_result = get_get_last_snapshot_value_in_eth_usdc_price (targets . get (4usize) . unwrap () . clone ()) . await ;
    ...

Improvements

[1] Tags is not set in accordance with the target currency.
Many seem to be in USDC, ETH.

[2] Common algorithm_lens
It is similar to the contents of Questions[2], but if the logic is the same, it seems that only one algorithm_lens is needed.
We can also change the parameters from relayer to select the data source.

Thank you.

Thanks for the feedback! Here are responses to the questions and issues you have raised:

Questions

[1] This is the Oku API, which I understand was developed under a Uniswap Foundation grant. See also https://blockworks.co/news/uniswap-v3-new-front-end for some background. It is not on the ICP network. How can I check if it is IPV6-compatible?

Getting the data that I have obtained from this API from on-chain data would be relatively straightforward for most of the variables used, but for a couple of them it is problematic:

  • fees_24h_usd - Calculating cumulative fees for a pool is very complex and probably requires the use of historical data or stored snapshots rather than data that can be queried from a contract, as the Uniswap pool contracts only seem to provide fee data in the form of current state variables.
  • ticks - I’ve used this variable for calculating the total value locked within a given range of ticks. This variable is readily available from the UniswapV3Pool contract but is difficult to implement for our purposes because the required ticks need to be individually specified in separate queries, which would consume huge resources and would not be workable if method values need to be hard-coded.

The remaining variables could be derived from 4 separate contract calls within UniswapV3Pool (slot0 for both the asset pair of interest and for UDSC/ETH, liquidity and tickSpacing) which would be easy to do but would start to get expensive in terms of cycles. I would have preferred to use on-chain data as much as possible, but for these limitations. Please correct me if my understanding of any this doesn’t seem right.

[2] The algorithm_lens all use the same logic except for the VRA/ETH pair. The reason is that in this pair, (W)ETH is designated as token0 but in the other pairs it is token1, so without this change the return rate would end up being somewhat different if the price ratio had fluctuated a great deal over the preceding week.

Bugs

[1] lens_targets: I couldn’t find this in the algorithm_lens schema but I see that it appears in the relayer schema, and is something that I had overlooked. To my mind this seems redundant as the information also appears under datasource in algorithm_lens (what am I missing?) but I’ve made this change for the each of the pairs. Does this look right?

[2] The indices in this section are 0, 1, 3, 4. Whoops! I must have deleted a line at some point and overlooked this detail. I didn’t understand how this part worked, but I gather that targets here are specified by lens_targets, which would answer my question in the last paragraph? I’ll fix this up now.

Improvements

[1] Thanks for spotting that! Something I overlooked while replicating the manifests. Now fixed.

[2] I’m not sure how this would work. Wouldn’t datasource: methods: - id: in each algorithm_lens.yaml still need to point to a specific set of snapshotters? By my understanding the contract address in each snapshotter manifest needs to be hard coded, so they are all unique. I can see how this would work for the relayers, but is there a way around this for the algorithm_lens?

Updating the project

Thanks again for such detailed feedback. This has really helped me get my head around how all this fits together! Now that I’ve corrected the two main bugs this should hopefully work as intended.

How should I now go about updating the deployed project? Should the following steps be sufficient to safely update and re-deploy the canisters?

csx generate
csx build --only-build
csx deploy --network ic

I’m cautious about using csx exec as it seems to use a lot of cycles (about $80 worth to execute the full project), unless there’s a way to refund the cycles I spent on the faulty version of the project similar to what happens with using dfx canister delete. If I’ve earnt the bounty then this won’t matter too much but I’m happy to be guided by your advice.

Thank you for checking it out.

Questions [1]

[1] This is the Oku API, which I understand was developed under a Uniswap Foundation grant. See also Uniswap v3 has a brand new front end - Blockworks for some background.

I see, I just didn’t know this. I would like to take a look.

It is not on the ICP network. How can I check if it is IPV6-compatible?

Currently on the ICP it must be able to communicate over ipv6.

https://internetcomputer.org/docs/current/developer-docs/integrations/https-outcalls/https-outcalls-how-it-works#ipv6-only-support

I’m sure there are other ways to check, e.g. ping6.

https://techhub.hpe.com/eginfolib/networking/docs/switches/YA-YB/16-01/5200-0136_yayb_2530_ipv6/content/ch07s02.html

Getting the data that I have obtained from this API from on-chain data would be relatively straightforward for most of the variables used, but for a couple of them it is problematic:

The remaining variables could be derived from 4 separate contract calls within UniswapV3Pool

I think your understanding is correct, I was just asking the question based on trust in https and its endpoints and if it is difficult to get from onchain (and endpoints can be trusted), then I think you can make a decision like this.

Questions [2]

In a nutshell, you want to do something like a reversal?

If you want to be able to choose between token0 and token1, I think you can control such a part by adding an argument when calling algorithm_lens to make it a single algorithm_lens logic.

Bugs? [1]

Sorry, this was about the relayer manifest.

On top of that, relayer seemed to be fine. Please let it go.

Improvements [2]

The address of the snapshot_indexer to which algorithm_lens is called is not hardcoded. The actual address is specified by the lens_target parameter in the manifest for relayer and snapshot_indexer_icp. (If you call algorithm_lens directly, it is the vec text argument.)

The datasource.methods.id of algorithm_lens will autoload .did only when the component id of the same project is specified.

The id (or func_name_alias) directly affects the function generation for cross canister calls in logics. If you are calling a canister outside of your project, make sure to specify the correct identifier (candid_file_path if you use a custom type), and make sure that the component (ex: relayer) calling the algorithm_lens has a lens_ target of the component (ex: relayer) calling algorithm_lens should be a data source such as snapshot_indexer.

Updating the project

Sorry, but there is currently no way to update a canister that has already been deployed.
if prod, you can delete the canister and its associated sidecar canisters and recover the cycles.
The canister id of a sidecar can be figured out with the following function
proxy: get_proxy: () → (principal) of the body canister
vault, db: get_component_info: () → (ComponentInfo) of the proxy
proxy: get_proxy: () → (principal) of the body canister
vault, db: get_component_info: () → (ComponentInfo) of the proxy

I’m cautious about using csx exec as it seems to use a lot of cycles (about $80 worth to execute the full project),

The whole thing is certainly large. But the cost of deploying main to the canister itself is the ICP default (it might be better to make this itself controllable via csx deploy).
To reduce the deployment cost of sidecar related to management-canister, I would appreciate it if you could modify the parameter of the script that I told you the other day.

That’s all good! :slight_smile:

I’ll write a bit more in the morning (Sydney time) in response to some of those points. For now, I’ve been redeploying and I’m up to csx exec but I keep getting this kind of error:

$ csx exec --only-execute-cmds --network ic -c rndr_eth_1_pool_fees
Dec 22 13:32:45.062 INFO Execute canister processing...
Dec 22 13:32:45.062 INFO Load env file: "./.env"
Dec 22 13:32:45.064 INFO Skip to generate commands to call components
Dec 22 13:32:45.064 INFO Start processing for commands execution...
Dec 22 13:32:45.065 INFO Run scripts to execute commands for deployed components
Dec 22 13:32:45.065 INFO Selected component is 'rndr_eth_1_pool_fees'
Dec 22 13:32:45.074 ERRO Failed: Executed './scripts/entrypoint.sh' by: Error: Cannot find canister id. Please issue 'dfx canister create rndr_eth_1_pool_fees'.
occured on /home/timk/ic/csx/edpr_full/uniswap-edpr/artifacts/scripts/components/rndr_eth_1_pool_fees.sh [Line 3]
command: dfx canister call rndr_eth_1_pool_fees init_in '(variant { "LocalDevelopment" }, record {
                refueling_interval = 86400: nat64;
                vault_intial_supply = 500000000000: nat;
                indexer = record {
                    initial_supply = 0: nat;
                    refueling_amount = 500000000000: nat;
                    refueling_threshold = 250000000000: nat;
                };
                db = record {
                    initial_supply = 750000000000: nat;
                    refueling_amount = 500000000000: nat;
                    refueling_threshold = 250000000000: nat;
                };
                proxy = record {
                    initial_supply = 150000000000: nat;
                    refueling_amount = 50000000000: nat;
                    refueling_threshold = 50000000000: nat;
                };
        })' --with-cycles 1400000000000 --wallet ${WALLET}

(stdout at run time)
Selected is 'rndr_eth_1_pool_fees'
Run script for rndr_eth_1_pool_fees

This doesn’t make much sense to me. I’ve been through all the steps and rndr_eth_1_pool_fees has been created, built and installed. You can see here which canisters have been deployed, and here for the Candid UI of rndr_eth_1_pool_fees.

Can you see anything that I might be missing?

Not too sure what happened there, but I’ve managed to work around it. I deleted all the canisters, cloned the project afresh from the repo, ran through all the steps to build, deploy and execute and it looks like it’s succeeded.

I checked the Oku API on a few different test sites and it looks like it’s IPV6-compatible. I totally agree with your thinking about on-chain data. On-chain data forms the ground truth for everything we’re looking at in this project and it would be ideal to eliminate the need to trust a centralised source, but I decided to stick with Oku due to the complexities involved. From the information at hand it seems to be highly reliable.

Sorry, this was about the relayer manifest.

No problem! I figured that was what you meant and it all made sense once I worked my way through it.

Reclaiming cycles from sidecar canisters by getting the details with get_proxy() worked well!

I worked on the two other main changes that you recommended - combining algorithm lenses and reducing cycle cost.

I was unable to get a successful outcome by combining lenses. I tried this:

# yaml-language-server: $schema=https://raw.githubusercontent.com/horizonx-tech/chainsight-cli/main/resources/schema/algorithm_lens.json
version: v1
metadata:
  label: pairs_algorithm_lens
  type: algorithm_lens
  description: 'Calculates EDPR for each trading pair'
  tags:
  - Ethereum
  - Account
datasource:
  methods:
  - id: pool_fees
    identifier: 'get_last_snapshot_value : () -> (SnapshotValue)'
  - id: tcumul_28x6hr
    identifier: 'get_last_snapshot_value : () -> (record { vec int64; vec text })'
  - id: v3pool
    identifier: 'get_last_snapshot_value : () -> (SnapshotValue)'
  - id: eth_usdc_price
    identifier: 'get_last_snapshot_value : () -> (SnapshotValue)'

This gave me an error: ERRO Not compilable IDLProg. It looked the method ids had to match the specific components targeted, which needed to separate as they are making calls to different contracts. I may have missed something here but I decided to go back to having a separate algorithm lens for each trading pair.

On the other hand, reducing cycle cost was successful. This ended up working once I halved all the cycles values in each .sh file, instead of just the initial_supply (x3) and --with-cycles, like so:

#!/bin/bash
# init
dfx canister --network ic call rndr_eth_1_algorithm_lens init_in '(variant { "Production" }, record {
                refueling_interval = 86400: nat64;
                vault_intial_supply = 500000000000: nat;
                indexer = record { 
                    initial_supply = 0: nat;
                    refueling_amount = 500000000000: nat;
                    refueling_threshold = 250000000000: nat;
                };
                db = record { 
                    initial_supply = 750000000000: nat;
                    refueling_amount = 500000000000: nat;
                    refueling_threshold = 250000000000: nat;
                };
                proxy = record { 
                    initial_supply = 150000000000: nat;
                    refueling_amount = 50000000000: nat;
                    refueling_threshold = 50000000000: nat;
                };
        })' --with-cycles 1400000000000 --wallet $(dfx identity get-wallet --network ic)

I now have the entire project deployed, hopefully in full working order although I haven’t managed to test it out.

Thanks for confirming.

This gave me an error: ERRO Not compilable IDLProg .

This is because it uses a custom type called SnapshotValue but does not understand its type definition,
In this case, you must specify candid_file_path to specify the .did where the SnapshotValue is located.
If the id is the name of another component in the same project, the .did will be automatically retrieved and interpreted.

Would this solve it?

rndr_eth_1_relayer.yaml: [and similar changes for other pairs]

# yaml-language-server: $schema=https://raw.githubusercontent.com/horizonx-tech/chainsight-cli/main/resources/schema/relayer.json
version: v1
metadata:
  label: rndr_eth_1_relayer
  type: relayer
  description: 'This canister relays the Estimated Daily Percentage Return (EDPR) of the RNDR/ETH 1% pair on UniSwap V3, using a price range based on the previous week''s prices, to Sepolia'
  tags:
  - Oracle
  - snapshot
datasource:
  type: canister
  location:
    id: shared_algorithm_lens
    args:
      id_type: canister_name
  method:
    identifier: 'get_last_snapshot_value : () -> (text)'
    candid_file_path: src/canisters/rndr_eth_1_algorithm_lens/rndr_eth_1_algorithm_lens.did
    interface: null
    args: []
destination:
  network_id: 11155111
  type: uint256
  oracle_address: "0xB5Ef491939A6dBf17287666768C903F03602c550"
  rpc_url: https://ethereum-sepolia.blockpi.network/v1/rpc/public
interval: ${INTERVAL}
lens_targets:
  identifiers:
    - rndr_eth_1_pool_fees
    - rndr_eth_1_tcumul_28x6hr
    - rndr_eth_1_v3pool
    - eth_usdc_price

shared_algorithm_lens:

# yaml-language-server: $schema=https://raw.githubusercontent.com/horizonx-tech/chainsight-cli/main/resources/schema/algorithm_lens.json
version: v1
metadata:
  label: shared_algorithm_lens
  type: algorithm_lens
  description: ''
  tags:
  - Ethereum
  - Account
datasource:
  methods:
  - id: pool_fees
    identifier: 'get_last_snapshot_value : () -> (SnapshotValue)'
  - id: tcumul_28x6hr
    identifier: 'get_last_snapshot_value : () -> (record { vec int64; vec text })'
  - id: v3pool
    identifier: 'get_last_snapshot_value : () -> (SnapshotValue)'
  - id: eth_usdc_price
    identifier: 'get_last_snapshot_value : () -> (SnapshotValue)'

The bit I’m still unsure about is datasource in the shared algorithm_lens. There is no specific component called only pool_fees, tcumul_28x6hr or v3pool. These snapshotters are all linked to specific pairs (rndr_eth_1_pool_fees etc) and to a separate Ethereum contract for each pair, so there’s no way I can see to narrow it down further. What do you think?

That is the image I had in mind.
algorithm_lens does not specify the data source itself, and the interface of the data source must be determined.
Therefore, id does not specify a specific component, but rather a candid_file representing its interface.

However, as I mentioned in my previous comment, it is a matter of which currency is used for token0 and token1, so I think it would be better to use algrotihm_lens with an argument and call this algorithm_lens by snapshot_indexer_icp or relayer with an argument. I think you can combine these two algorithm_lens into one by changing the parameters.

I think this showcase has everything you might want to do.

  1. algorithm_lens with argument
    chainsight-showcase/implied_volatility_spx/components/lens_iv_calculator.yaml at 3f4fac8389e9990f3ae045f2ad80532b826a3179 · horizonx-tech/chainsight-showcase · GitHub
    chainsight-showcase/implied_volatility_spx/src/logics/lens_iv_calculator/src/lib.rs at 3f4fac8389e9990f3ae045f2ad80532b826a3179 · horizonx-tech/chainsight-showcase · GitHub
  2. snapshot_indexer_icp calling its algorithm_lens
    chainsight-showcase/implied_volatility_spx/components/snapshots_iv_for_spx_4500_call.yaml at 3f4fac8389e9990f3ae045f2ad80532b826a3179 · horizonx-tech/chainsight-showcase · GitHub
    chainsight-showcase/implied_volatility_spx/src/logics/snapshots_iv_for_spx_4500_call/src/lib.rs at 3f4fac8389e9990f3ae045f2ad80532b826a3179 · horizonx-tech/chainsight-showcase · GitHub
    chainsight-showcase/implied_volatility_spx/components/snapshots_iv_for_spx_4500_put.yaml at 3f4fac8389e9990f3ae045f2ad80532b826a3179 · horizonx-tech/chainsight-showcase · GitHub
    chainsight-showcase/implied_volatility_spx/src/logics/snapshots_iv_for_spx_4500_put/src/lib.rs at 3f4fac8389e9990f3ae045f2ad80532b826a3179 · horizonx-tech/chainsight-showcase · GitHub

Now I’m confused. Can you clarify further?

I see there’s also the logic which will need to be combined. With this line as an example:

    let pool_fees_result = get_get_last_snapshot_value_in_rndr_eth_1_pool_fees (targets . get (0usize) . unwrap () . clone ()) . await ;

would this become

    let pool_fees_result = get_get_last_snapshot_value_in_pool_fees (targets . get (0usize) . unwrap () . clone ()) . await ;

?

Not sure exactly where the get_get_... variable name is derived from, but I would have imagined this not to work.

The function name is automatically generated from datasource.methods.id (or datasource.methods.id.func_name_alias).
The function is identified by which datasource.methods[n] it was generated by.
On top of that, this function does not determine which canister to call, the caller is one of the principals in the targets passed in the argument (the targets are specified by the snapshot_ indexer_icp or relayer that calls this algorithm_lens).

If you look at the link in the previous comment, you will see examples of two different snapshot_indexer_icp’s calling the same algorithm_lens with different lens_targets (and non-target arguments).

  1. algorithm_lens with argument
  2. snapshot_indexer_icp calling its algorithm_lens

Got it. So this line should work, and combing the USDC, RNDR and WBTC lenses should be straightforward.

Would it be acceptable to have two separate lenses? One for these three (xxx/ETH) and the other for VRA (ETH/xxx)? I believe this would make it easy for any other pairs to be added, and simple for any other users to understand. Non-ETH pairs would probably have their returns denominated in one of the pair tokens (e.g. xxx/DAI, DAI/xxx) so this solution would have broad utility.

I am commenting on this at Github.

Hi everyone.

I want to express my gratitude for your interest in this bounty. It has been three months since we opened it to the public. I would like to inform you that we have contacted the participants individually and the final review is currently in progress on GitHub and Telegram. Therefore, I would like to close this thread soon.

It’s great to see @timk11 using Chainsight CLI to create an EDPR Oracle with UniswapV3 on this bounty. I respect his hard work. I also want to thank all the participants who used the CLI and gave the comments here.

We will be opening a second bounty program in the near future and we would love for you to apply. We appreciate your feedback and will continue to make improvements based on it.

Thank you.

3 Likes

Thanks @hawk ! Completing this project has been a tremendously valuable learning experience and I’m most grateful for the opportunity to have taken part.

Final deliverables are in place as follows:

Further notes:

  • All requested deliverables are now in place pending final review.
  • Results for all 4 pairs can be obtained by calling the _indexer component (see canister_ids) via the IC dashboard, e.g. dai_eth_0_3_indexer: https://dashboard.internetcomputer.org/canister/2stcd-6qaaa-aaaag-qc3vq-cai using get_last_snapshot_value().
  • The same results can also be viewed on the Chainsight UI, as seen here for the DAI/ETH pair.
  • Results for the DAI/ETH, SHIB/ETH and wstETH/ETH pairs have been verified as accurate. Results for the RPL/ETH pair are erroneous due to an identified error that has now been fixed and pushed to the repo, but as per the team’s request I have not yet re-deployed it to the IC as a fresh component.
  • Relayers for each pair can be seen on the Chainsight UI, e.g. dai_eth_0_3_relayer - https://beta.chainsight.network/explore/components/ztrg6-wyaaa-aaaag-qc4dq-cai. So far the Relayer is not giving an output (which would correspond to edpr obtained from this call to dai_eth_0_3_indexer). I’m informed that this is due to a known Relayer issue unrelated to my own code.
1 Like

Thanks @hawk for the further discussion regarding the final review. All criteria have now been completed as per details in Telegram. I look forward to your confirmation that the bounty has been completed.

@hawk - Confirming that the final bounty payment has now been received, with thanks.

1 Like