-
Notifications
You must be signed in to change notification settings - Fork 15
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
Conversation
5fbcd05
to
f04027a
Compare
Codecov ReportAttention: Patch coverage is
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. |
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 |
Rest looks fine to me. Will let @AlastairHolmes approve as he's been working on these RPCs a lot recently |
state-chain/runtime/src/lib.rs
Outdated
@@ -1408,6 +1408,25 @@ impl_runtime_apis! { | |||
all_prewitnessed_swaps | |||
} | |||
|
|||
fn cf_scheduled_swaps(from: Asset, to: Asset) -> Vec<(Swap, BlockNumber)> { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
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)). |
a00b549
to
f142ef4
Compare
5593e95
to
e109324
Compare
There was a problem hiding this 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
state-chain/custom-rpc/src/lib.rs
Outdated
// 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)] |
There was a problem hiding this comment.
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, |
There was a problem hiding this comment.
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.
state-chain/custom-rpc/src/lib.rs
Outdated
sink, | ||
move |api, hash| { | ||
let swaps = api | ||
.cf_scheduled_swaps(hash, base_asset, quote_asset)? |
There was a problem hiding this comment.
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)>
?
There was a problem hiding this comment.
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)
.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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, |
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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)
There was a problem hiding this comment.
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}
.
state-chain/runtime/src/lib.rs
Outdated
|
||
debug_assert!(first_block < last_block); | ||
|
||
(first_block..=last_block).flat_map(|block| { |
There was a problem hiding this comment.
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.
state-chain/runtime/src/lib.rs
Outdated
|
||
Swapping::get_scheduled_swap_legs(swaps, base_asset).unwrap().into_iter().map(move |swap| (swap, block)) | ||
}).collect() | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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)] |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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).
There was a problem hiding this comment.
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, |
There was a problem hiding this comment.
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.
There was a problem hiding this 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))] |
There was a problem hiding this comment.
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> { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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
}
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). |
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. |
…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
…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)
Pull Request
Closes: PRO-1140
Checklist
Please conduct a thorough self-review before opening the PR.
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 whenpipe_from_try_stream
was failing to encodeBlockUpdate
internally (due toserde(flatten)
), and this wasn't closing the subscription either. I now added error handling aroundpipe_from_try_stream
to make it easier detect failures like this in the future.Example usage:
This would return a notification on every block, even when the list of swaps is empty:
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.