-
Notifications
You must be signed in to change notification settings - Fork 22
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
Let users withdraw funds from auctions #1068
Conversation
761d989
to
2ebf2d7
Compare
packages/ui/components/ui/tx/view/action-dutch-auction-withdraw-view.tsx
Show resolved
Hide resolved
ec8bd27
to
b5c5a51
Compare
packages/wasm/crate/src/storage.rs
Outdated
.map(|auction| { | ||
serde_wasm_bindgen::from_value::<AuctionRecord>(auction)? | ||
.outstanding_reserves | ||
.ok_or(WasmError::Anyhow(Error::msg("could not find reserves"))) |
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.
Is there a clean way to not have to repeat this error line below on line 376?
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 are technically different errors - i would put 'could not find auction' here, and 'could not find reserves' is appropriate at the other.
consider also rewriting it in the pattern of other fns in this file, which seem a bit clearer.
ActionView::ActionDutchAuctionWithdraw(ActionDutchAuctionWithdrawView { | ||
reserves, | ||
.. | ||
}) => reserves.iter().for_each(|reserve| { | ||
asset_ids.insert(reserve.asset_id()); | ||
}), |
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 doesn't do anything yet, since core currently creates a stubbed withdraw view with an empty reserves array.
This is a known issue and will eventually be fixed. In the meantime, though, I'm writing the code here that will automatically populate the necessary metadata once core is updated.
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.
flagging that I'll also be inspecting these views closely per #1040, possibly changing what we display in our TxP / TxV
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.
Yeah it's hardcoded for now, my sense is that we can get to it a bit later, not next week but the one after, let me know if you think this should be prioritized and i'll get to it sooner
@@ -452,6 +445,7 @@ export class BlockProcessor implements BlockProcessorInterface { | |||
this.indexedDb.saveAssetsMetadata(metadata), | |||
this.indexedDb.upsertAuction(auctionId, { | |||
seqNum: action.value.seq, | |||
outstandingReserves: undefined, |
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 is in a couple places. i understand what it's doing here in idb,
web/packages/storage/src/indexed-db/index.ts
Lines 714 to 721 in 670034a
const outstandingReserves = value.outstandingReserves | |
? { | |
input: value.outstandingReserves.input.toJson() as Jsonified<Value>, | |
output: value.outstandingReserves.output.toJson() as Jsonified<Value>, | |
} | |
: 'outstandingReserves' in value | |
? undefined | |
: existingRecord?.outstandingReserves; |
but i think it would be more appropriate to use some other mechanism, or use a value like null
rather than exploiting the subtle distinction between an undefined value and a value that is undefined.
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.
Hmmm... I tend to agree. But as I started to implement your suggestion of using null
, I realized I'd either have to:
- Make
outstandingReserves
required and type it asoutstandingReserves?: { input: ..., output: ... } | null
— meaning it can be undefined OR null, depending on whetheroutstandingReserves
has previously been set and then cleared, OR - Pass
outstandingReserves: null
everywhere thatupsertAuction
is called, which sort of defeats the purpose of an upsert. (I'd designed the method so that you can only pass the properties you want to update.)
Alternatively, instead of using null
or undefined
, I could create a separate OUTSTANDING_AUCTION_RESERVES
table that we add to / delete from at the appropriate times. I don't love it, since outstanding reserves are essentially a child of auction records and thus should probably be in the auctions table. But... 🤷🏻♂️
Thoughts? Suggestions?
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.
Make outstandingReserves required and type it as outstandingReserves?: { input: ..., output: ... } | null — meaning it can be undefined OR null, depending on whether outstandingReserves has previously been set and then cleared
In storage, it's okay to use a simple optional on the stored type, as you'll never need to distinguish 'was never defined' from 'was previously defined but then cleared'
but your upsert parameter definitely needs three possible values, as you're taking three possible actions:
- preserve existing data
- write new data
- delete data
and these deserve clarity.
null and undefined are distinct values - loosely equal but not strictly equal, so something like
outstandingReserves === null
can be a useful conditional.
notably some of this is converted to json - strictly, json has no undefined
type, but does have null
. i don't think null
ever comes out of the bufbuild serializer, but it is valid input as part of a JsonValue
or JsonObject
. the de-serializer throws if you pass it a bare null
but would successfully deserialize a null field as a missing field.
part of the issue also is you're essentially doing a deep object merge.
some options:
- never delete data on upsert, and require a separate delete
- break out outstandingReserves into multiple values to avoid deep comparison. the null/undefined problem remains, but is simpler
- use null value to clear outstandingReserves, and undefined to preserve it
- use null value to clear fields inside outstandingReserves, and undefined to preserve them
- use an optional object for outstandingReserves, optional fields inside outstandingReserves, and to clear the fields within outstaindgReserves use
- a present but empty object
- an object with null values
- an object with defined empty object values
- an object with defined
new Value()
values
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 think keeping a table of active auctions is reasonable, as the table of all auctions may eventually be large.
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 think we should be careful to not assign meaning to null
vs undefined
from an API perspective. This has been a long debated thing in javascript land and there isn't a plain-meaning consensus on it. I'm personally in the camp that including null
in the language was a mistake (yet another win for Rust).
Think explicit indicators for update versus deletion would be nice. Perhaps something like:
type UpdateField<T> = { type: 'UPDATE'; value: T } | { type: 'DELETE' };
outstandingReserves?: UpdateField<{ input: Value; output: Value }>;
or perhaps a new unset field:
upsertAuction<T extends DutchAuctionDescription>(
auctionId: AuctionId,
value: UpdateFields<T>,
unset?: (keyof UpdateFields<T>)[],
): Promise<void>;
or even an explicit method:
deleteAuctionFields<T extends DutchAuctionDescription>(
auctionId: AuctionId,
fields: (keyof UpdateFields<T>)[],
): Promise<void>;
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 amount of discussion generated here makes me think we should not provide any merging upsert, but just expect the caller to construct the object they intend to store, and then clobber whatever's there
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 see this commit.
I like keeping upsertAuction
as a merge function since there are three fields that need to be updated separately. So I broke out outstanding reserves into a separate table, and simply added/deleted records as the user ends/withdraws from an auction, respectively.
(I added a commit afterward to upgrade to the latest buf build/etc. packages, but you can safely ignore that.)
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.
(cc @grod220 / @Valentine1898 / @turbocrime / @TalDerei )
@@ -121,7 +122,8 @@ export const createDutchAuctionSlice = (): SliceCreator<DutchAuctionSlice> => (s | |||
state.dutchAuction.auctionInfos = []; | |||
}); | |||
|
|||
for await (const response of viewClient.auctions({})) { | |||
/** @todo: Sort by... 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.
what's the motivation here?
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.
Eventually, I'll probably want to sort by e.g., most recent auction descending, or something.
this.indexedDb.saveAssetsMetadata(metadata), | ||
this.indexedDb.upsertAuction(action.value.auctionId, { seqNum }), | ||
]); | ||
await processActionDutchAuctionEnd(action.value, this.querier.auction, this.indexedDb); | ||
} else if (action.case === 'actionDutchAuctionWithdraw') { |
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 think helper functions for decluttering the block processor is great. For continuity, can we make processActionDutchAuctionSchedule
and processActionDutchAuctionWithdraw
helpers as well?
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.
Good call — done!
.ok_or_else(|| anyhow!("missing auction ID in Dutch auction withdraw action"))? | ||
.try_into()?; | ||
|
||
save_auction_nft_metadata_if_needed(auction_id, &storage, seq).await?; |
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 remind me again we make this call in the planner to save the metadata, if we're already doing this in the block processor?
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.
Huh, I actually didn't think of this. But now that you raise it, I think we'll still need to do it this way for these two use cases:
1. The user creates the auction via pcli
, then views it via a Prax-connected dapp like minifront.
In this case, we need to save the metadata in the block processor, so it won't yet exist in IndexedDB.
2. The user creates the auction via a Prax-connected dapp like minifront.
In this case, we need to save the metadata during the planning request, or else the auction NFT will show up as "Unknown Asset" in the "Output" action in the txn approval dialog.
Fortunately, these are keyed by ID, so there won't be duplicate assets in the database.
705c53d
to
011452e
Compare
5b74b19
to
c6ec8f8
Compare
🦋 Changeset detectedLatest commit: 27cba71 The changes in this PR will be included in the next version bump. This PR includes changesets to release 10 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
c6ec8f8
to
182654c
Compare
182654c
to
8bc5a92
Compare
@@ -446,15 +446,15 @@ pub async fn plan_transaction( | |||
.try_into()?; | |||
|
|||
save_auction_nft_metadata_if_needed(auction_id, &storage, seq).await?; | |||
let oustanding_reserves: OutstandingReserves = | |||
let outstanding_reserves: OutstandingReserves = |
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.
Typo :)
2075cbd
to
81a49be
Compare
screencap.mov
In this PR
<DutchAuctionComponent />
to takebuttonType
andonClickButton
props, rather thanendButton
andonClickEndButton
props (since now the button may be a withdraw or end button).outstandingReserves
property on theAUCTIONS
table for storing outstanding reserves for an auction that has ended but not been withdrawn from.byte_array_to_base64
helper and use it throughoutstorage.rs
.processActionDutchAuctionEnd()
helper from the block processor.Closes #1020