Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat: Scheduled Swaps Subscription #4525

Merged
merged 23 commits into from
Feb 27, 2024
Merged

Conversation

msgmaxim
Copy link
Contributor

Pull Request

Closes: PRO-1140

Checklist

Please conduct a thorough self-review before opening the PR.

  • I am confident that the code works.
  • I have updated documentation where appropriate.

Summary

Took me a bit longer than it should have... I initially thought we should need a stream of finalized stream, and I wrote some code to deal with that, only to realise that with a finalized stream a 2 block swap delay wouldn't be long enough. So here I'm exposing swaps from unfinalised blocks using existing new_subscription.

I also ran into an issue after merging recent changes in main. Took me some time to realise this, but depending on what we use as T in new_subscription, we could get a silent failure when pipe_from_try_stream was failing to encode BlockUpdate internally (due to serde(flatten)), and this wasn't closing the subscription either. I now added error handling around pipe_from_try_stream to make it easier detect failures like this in the future.

Example usage:

wscat -c ws://localhost:9944
> {"id":1, "jsonrpc":"2.0", "method": "cf_subscribe_scheduled_swaps", "params": ["FLIP", "BTC"]}

This would return a notification on every block, even when the list of swaps is empty:

{"jsonrpc":"2.0","method":"cf_subscribe_scheduled_swaps","params":{"subscription":"SgUMJq00PQ7tetsx","result":{"block_hash":"0xdb82a90439be5826f8dece231d35ee8b23c9baaf98839aa345fc188bdee64423","block_number":58,"swaps":[{"swap_id":1,"from":"BTC","to":"FLIP","amount":4949884,"swap_type":{"Swap":{"Eth":"0x41ad2bc63a2059f9b623533d87fe99887d794847"}},"stable_amount":null,"final_output":null,"fee_taken":false,"execute_at":60}]}}}
< {"jsonrpc":"2.0","method":"cf_subscribe_scheduled_swaps","params":{"subscription":"SgUMJq00PQ7tetsx","result":{"block_hash":"0x4f70862ca1544f5add9c61598a0e1875e9ca3dd9637e243f0cfb68cefdb7db3a","block_number":59,"swaps":[{"swap_id":1,"from":"BTC","to":"FLIP","amount":4949884,"swap_type":{"Swap":{"Eth":"0x41ad2bc63a2059f9b623533d87fe99887d794847"}},"stable_amount":null,"final_output":null,"fee_taken":false,"execute_at":60}]}}}
< {"jsonrpc":"2.0","method":"cf_subscribe_scheduled_swaps","params":{"subscription":"SgUMJq00PQ7tetsx","result":{"block_hash":"0xc5737b08cf7133c1fdb88f7932f1b8c67945e216c9e5101988323ad1e627f2af","block_number":60,"swaps":[]}}}

Note how the same swap is returned twice (i.e. in two notifications) in this case. This is becasue we return all swaps that are "pending", and it takes 2 blocks untils swaps get executed.

@msgmaxim msgmaxim requested a review from kylezs February 14, 2024 07:52
@msgmaxim msgmaxim requested a review from dandanlen as a code owner February 14, 2024 07:52
@msgmaxim msgmaxim force-pushed the feat/scheduled-swaps-subscription branch from 5fbcd05 to f04027a Compare February 14, 2024 07:58
Copy link

codecov bot commented Feb 14, 2024

Codecov Report

Attention: Patch coverage is 64.20455% with 63 lines in your changes are missing coverage. Please review.

Project coverage is 72%. Comparing base (307485c) to head (a2ef796).

Files Patch % Lines
state-chain/custom-rpc/src/lib.rs 0% 45 Missing ⚠️
state-chain/runtime/src/lib.rs 0% 0 Missing and 14 partials ⚠️
state-chain/pallets/cf-swapping/src/lib.rs 94% 1 Missing and 2 partials ⚠️
state-chain/runtime/src/runtime_apis.rs 67% 1 Missing ⚠️
Additional details and impacted files
@@          Coverage Diff           @@
##            main   #4525    +/-   ##
======================================
- Coverage     73%     72%    -0%     
======================================
  Files        401     401            
  Lines      66331   66367    +36     
  Branches   66331   66367    +36     
======================================
- Hits       48193   48101    -92     
- Misses     15814   15929   +115     
- Partials    2324    2337    +13     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

@kylezs
Copy link
Contributor

kylezs commented Feb 14, 2024

Once this is included we should add docs to the docs repo, I added a sub-issue on linear so we don't forget: PRO-1203

@kylezs
Copy link
Contributor

kylezs commented Feb 14, 2024

Rest looks fine to me. Will let @AlastairHolmes approve as he's been working on these RPCs a lot recently

@@ -1408,6 +1408,25 @@ impl_runtime_apis! {
all_prewitnessed_swaps
}

fn cf_scheduled_swaps(from: Asset, to: Asset) -> Vec<(Swap, BlockNumber)> {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These should be consistent with the other interfaces, and therefore called base_asset and quote_asset.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And the ordering should be enforced to match the pool.

Copy link
Contributor

@AlastairHolmes AlastairHolmes Feb 14, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, I see a problem. I believe what we want from this sub is per pool scheduled swaps, otherwise the sub isn't very useful. Where as my (somewhat brief) reading of this, is this implementation is a sub of end to end scheduled swaps. The problem with that is LPs would have to do not insignificant amount of work to work out what the per pool scheduled swaps are.

@AlastairHolmes
Copy link
Contributor

Ignore my comments, I've realised there's been some miss-communication about the intended behaviour. I will write on the ticket (Shortly, not immediately (Lunch)).

@msgmaxim msgmaxim marked this pull request as draft February 16, 2024 11:11
@msgmaxim msgmaxim force-pushed the feat/scheduled-swaps-subscription branch from a00b549 to f142ef4 Compare February 19, 2024 00:39
@msgmaxim msgmaxim force-pushed the feat/scheduled-swaps-subscription branch from 5593e95 to e109324 Compare February 19, 2024 05:01
@msgmaxim msgmaxim marked this pull request as ready for review February 19, 2024 05:02
Copy link
Contributor

@kylezs kylezs left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good to me, will let @AlastairHolmes hit approve

// Subscribe to a stream that on every block produces a list of all scheduled/pending
// swaps in the base_asset/quote_asset pool, including any "implicit" half-swaps (as a
// part of a swap involving two pools)
#[subscription(name = "subscribe_scheduled_swaps", item = SwapResponse)]
Copy link
Contributor

@AlastairHolmes AlastairHolmes Feb 20, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be BlockUpdate<SwapResponse> right?

#[cfg_attr(feature = "std", derive(serde::Serialize, serde::Deserialize))]
pub struct SwapLegInfo {
pub swap_id: SwapId,
pub from: Asset,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of from and to, we should use base_asset, quote_asset, and side: Order (This type is being changed to be called Side fyi, by Jan).

I'd like to do this for consistency with the new rpcs.

sink,
move |api, hash| {
let swaps = api
.cf_scheduled_swaps(hash, base_asset, quote_asset)?
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not just have this fucntion return Vec<ScheduledSwap> instead of Vec<(SwapLegInfo, BlockNumber)>?

Copy link
Contributor

@AlastairHolmes AlastairHolmes Feb 20, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also then you could just delete SwapLegInfo and inline it into the ScheduledSwap structure, i.e. avoiding the serde(flatten).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I changed cf_scheduled_swaps to return Vec<ScheduledSwap> now, and simplified the code a bit.

However, I'm not sure merging the two structs is obviously justified. The reason I have separate structs is bacause get_scheduled_swap_legs doesn't deal with block numbers, so we'd need to pass the block number through that function just so it can construct ScheduledSwap on our behalf, which doesn't seem particularly nice. I would aslo need to add a generic parameter (for the block number) to ScheduledSwap to make it work. I don't think having this extra struct is much worse than the alternative. I can make this change if you have a strong preference though.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You should just inline get_scheduled_swap_legs, the fact that it causes you to need two struct's here (that aren't used separate other than in this one function), imo is clear indication the split isn't a good idea.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The advantage of having a separate get_scheduled_swap_legs is that we can write unit tests against it. It is possible to re-write tests with it inlined, but that wouldn't be as nice. I don't think having a separate struct for what the RPC call returns is such a bad thing, particularly since we wrap the internal type (with serde(flatten)), and so we don't even have any duplication.

// swaps in the base_asset/quote_asset pool, including any "implicit" half-swaps (as a
// part of a swap involving two pools)
#[subscription(name = "subscribe_scheduled_swaps", item = SwapResponse)]
fn cf_subscribe_scheduled_swaps(&self, base_asset: Asset, quote_asset: Asset);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suggest we add a cf_scheduled_swaps(base_asset, quote_asset, at) rpc too

Copy link
Contributor

@AlastairHolmes AlastairHolmes Feb 20, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Was a request of LPs, helps them know what as happened in the past

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

added

pub from: Asset,
pub to: Asset,
pub amount: AssetAmount,
pub swap_type: SwapType,
Copy link
Contributor

@AlastairHolmes AlastairHolmes Feb 20, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Had a discussion with Kyle and Dan, we agreed that we don't want to expose the SwapType.

Also this means you don't need the serde derives on the SwapType enum, and can remove this member here

// swaps in the base_asset/quote_asset pool, including any "implicit" half-swaps (as a
// part of a swap involving two pools)
#[subscription(name = "subscribe_scheduled_swaps", item = SwapResponse)]
fn cf_subscribe_scheduled_swaps(&self, base_asset: Asset, quote_asset: Asset);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This rpc call doesn't seem to have any checks on quote_asset? It should atleast fail, if it is not USDC, ideally I'd suggest just checking if the pool exists in cf_scheduled_swaps, and returning an error if it doesn't (In the pools pallet there is an error for this)

Copy link
Contributor Author

@msgmaxim msgmaxim Feb 21, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I reused cf_pool_info for this (creating a separate method returning true/false didn't seem worth it), however it doesn't seem trivial to return anything other than SubscriptionEmptyError for #[subscription] methods, so I'm just returning that. In practice this results in: {"jsonrpc":"2.0","error":{"code":-32602,"message":"Invalid params"},"id":1}.


debug_assert!(first_block < last_block);

(first_block..=last_block).flat_map(|block| {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You could just read the entire SwapQueue map into memory (Instead of one element at a time), then you can make this code much simpler.


Swapping::get_scheduled_swap_legs(swaps, base_asset).unwrap().into_iter().map(move |swap| (swap, block))
}).collect()

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change

pub(crate) type SwapQueue<T: Config> =
StorageMap<_, Twox64Concat, BlockNumberFor<T>, Vec<Swap>, ValueQuery>;

/// The first block for which swaps haven't yet been processed
#[pallet::storage]
#[pallet::getter(fn first_unprocessed_block)]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure there is much point in using these getters really, particularly as you cannot update through a similarly named setter?

Copy link
Contributor Author

@msgmaxim msgmaxim Feb 21, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IMO it is a bit cleaner than writing pallet_cf_swapping::FirstUnprocessedBlock::<Runtime>::get(). I also don't think every getter needs a setter (in fact, it is a common incapsulation pattern to expose a value through a getter only so that it can only be modified internally).

Copy link
Contributor

@AlastairHolmes AlastairHolmes Feb 21, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, I see your point. I hadn't seen that we do follow this pattern consistently elsewhere.

But if we do what I suggest inregards to the code that uses this getter, we wouldn't need to read this storage item anyway (Which isn't an exactly a nice concept (FirstUnprocessedBlock) to expose externally). And therefore the getter would have no purpose.

swap_id: swap.swap_id,
from: swap.from,
// All swaps from `base_asset` have to go through Usdc:
to: Asset::Usdc,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you use the STABLE_ASSET constant? Same below.

Copy link
Contributor

@AlastairHolmes AlastairHolmes left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Once all these are addressed we can merge.

pub enum SwapType {
Swap(ForeignChainAddress),
CcmPrincipal(SwapId),
CcmGas(SwapId),
}
#[derive(Clone, Debug, PartialEq, Eq, Encode, Decode, TypeInfo, MaxEncodedLen)]
#[cfg_attr(feature = "std", derive(serde::Serialize, serde::Deserialize))]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry forgot to mention this. This serde derive is not used, and so can be removed.

sink: SubscriptionSink,
base_asset: Asset,
quote_asset: Asset,
) -> Result<(), SubscriptionEmptyError> {
Copy link
Contributor

@AlastairHolmes AlastairHolmes Feb 21, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've realised there is something else we need to deal with, swaps can fail will limited liquidity, so the simulation of the first leg can fail. I think what we should do is if the simulation fails, don't output the second leg as scheduled, but do output the first leg as being scheduled.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In this case if the liquidity gets added for the first leg, since the LPs can see that it requires liquidity, then it'll be swapped, but then the no one has accounted for the second leg? that seems bad? Would it not be better to use some kind approximate price for the first leg, to the estimate the second leg input - and perhaps making it clear through the API that this second amount is an estimate or something

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This start to get a little messy with us mixing actual swaps with approximated swaps and now occasionally not including certain swaps. @dandanlen raised a concern about this to me, have you had a chance to chat about this in the office with him?

Copy link
Contributor

@AlastairHolmes AlastairHolmes Feb 23, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So I had a chat with Dan, I think the right way to deal with this is to instead of just having the estimate_amount via simulation is to also list the source_asset and source_amount, i.e. for multiple leg swaps what is the input asset and amount. And if the first leg simulation fails we can just set the estimated amount to zero?

Copy link
Contributor Author

@msgmaxim msgmaxim Feb 26, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure, I now include optional source_asset and source_amount for simulated swaps. Here is what it looks like for a BTC -> FLIP swap:

{
    "swap_id":1,
    "base_asset":{"chain":"Ethereum","asset":"FLIP"},
    "quote_asset":{"chain":"Ethereum","asset":"USDC"},
    "side":"buy",
    "amount":492047747,
    "source_asset":{"chain":"Bitcoin","asset":"BTC"},
    "source_amount":4949884,
    "execute_at":69
}

@msgmaxim
Copy link
Contributor Author

Addressed/responded to all ouf your comments now @AlastairHolmes. Let me know is we can merge this (and whether your other comment re failing due to limited liquidity can be addressed separately).

@AlastairHolmes
Copy link
Contributor

There is also another bug, the subscription can list swaps as being scheduled in past blocks, we can just bound the scheduled block to after the current block. Then I think this can be merged. This could happen when swaps fail, as swaps aren't rescheduled so their scheduled block doesn't change.

@msgmaxim msgmaxim enabled auto-merge (squash) February 27, 2024 03:26
@msgmaxim msgmaxim merged commit 64f7b99 into main Feb 27, 2024
43 checks passed
@msgmaxim msgmaxim deleted the feat/scheduled-swaps-subscription branch February 27, 2024 06:00
syan095 added a commit that referenced this pull request Feb 28, 2024
…ero-liquidity

* origin/main:
  fix: disable try-state checks (#4576)
  chore: debug solana in CI 🐛 (#4580)
  refactor: pass tx_ref as an extrinsic parameter (#4579)
  fix: remove bounded balance check (#4575)
  Solana: update image to latest tag (#4574)
  feat: add boost lp account to bouncer and fund it on setup_swaps (#4552)
  feat: Expose tx_hash on BroadcastSuccess event (#4561)
  feat: Relative Slippage Limits (PRO-1207) (#4547)
  chore: disable localnet solana in CI ⏱️ (#4569)
  feat: store prewitnessed deposits with id (#4496)
  Feat: Scheduled Swaps Subscription (#4525)

# Conflicts:
#	state-chain/pallets/cf-pools/src/tests.rs
syan095 added a commit that referenced this pull request Feb 29, 2024
…utxo

* origin/main:
  feat: expose command for broker fee withdrawal (#4581)
  Chore/fix arbitrum deployment (#4570)
  Added Range order pool price to the pool_price_v2 rpc call (#4548)
  fix: just check that the balance after is greater than before (#4587)
  chore: add origin to ccm failed (#4586)
  fix: runtime upgrade state check uses AllPalletsWithoutSystem (#4583)
  chore: add zellic audit to repo (#4585)
  fix: disable try-state checks (#4576)
  chore: debug solana in CI 🐛 (#4580)
  refactor: pass tx_ref as an extrinsic parameter (#4579)
  fix: remove bounded balance check (#4575)
  Solana: update image to latest tag (#4574)
  feat: add boost lp account to bouncer and fund it on setup_swaps (#4552)
  feat: Expose tx_hash on BroadcastSuccess event (#4561)
  feat: Relative Slippage Limits (PRO-1207) (#4547)
  chore: disable localnet solana in CI ⏱️ (#4569)
  feat: store prewitnessed deposits with id (#4496)
  Feat: Scheduled Swaps Subscription (#4525)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants