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

Sqlx #37

Merged
merged 19 commits into from
Mar 28, 2024
Merged
2 changes: 1 addition & 1 deletion Tiltfile
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ local_resource(

local_resource(
"auction-server",
serve_cmd="source ../tilt-resources.env; cargo run -- run --relayer-private-key $RELAYER_PRIVATE_KEY",
serve_cmd="source ../tilt-resources.env; source ./.env; cargo run -- run --database-url $DATABASE_URL --relayer-private-key $RELAYER_PRIVATE_KEY",
serve_dir="auction-server",
resource_deps=["create-configs"],
readiness_probe=probe(period_secs=5, http_get=http_get_action(port=9000)),
Expand Down
1 change: 1 addition & 0 deletions auction-server/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
DATABASE_URL=postgresql://postgres@localhost/postgres
1 change: 0 additions & 1 deletion auction-server/.prettierignore

This file was deleted.

This file was deleted.

This file was deleted.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

14 changes: 12 additions & 2 deletions auction-server/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ Each blockchain is configured in `config.yaml`.

This package uses Cargo for building and dependency management.
Simply run `cargo build` and `cargo test` to build and test the project.
We use `sqlx` for database operations, so you need to have a PostgreSQL server running locally.
Check the Migration section for more information on how to setup the database.

## Local Development

Expand All @@ -25,12 +27,16 @@ This command will start the webservice on `localhost:9000`.

You can check the documentation of the webservice by visiting `localhost:9000/docs`.

## Migrations
## DB & Migrations

sqlx checks the database schema at compile time, so you need to have the database schema up-to-date
before building the project. You can create a `.env` file similar
to the `.env.example` file and set `DATABASE_URL` to the URL of your PostgreSQL database. This file
will be picked up by sqlx-cli and cargo scripts when running the checks.

Install sqlx-cli by running `cargo install sqlx-cli`. Then, run the following command to apply migrations:
Copy link
Contributor

Choose a reason for hiding this comment

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

mind adding a note on which directory to run this from, just for clarity


```bash
export DATABASE_URL=postgres://postgres@localhost/postgres
sqlx migrate run
```

Expand All @@ -39,3 +45,7 @@ We use revertible migrations to manage the database schema. You can create a new
```bash
sqlx migrate add -r <migration-name>
```

Since we don't have a running db instance on CI, we use `cargo sqlx prepare` to generate the necessary
info offline. This command will update the `.sqlx` folder.
You need to commit the changes to this folder when adding or changing the queries.
4 changes: 2 additions & 2 deletions auction-server/migrations/20240320162754_init.up.sql
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ CREATE TABLE opportunity
creation_time TIMESTAMP NOT NULL,
permission_key BYTEA NOT NULL,
chain_id TEXT NOT NULL,
target_contract BYTEA NOT NULL,
target_call_value NUMERIC(80, 0) NOT NULL,
target_contract BYTEA NOT NULL CHECK (LENGTH(target_contract) = 20),
target_call_value NUMERIC(78, 0) NOT NULL,
target_calldata BYTEA NOT NULL,
sell_tokens JSONB NOT NULL,
buy_tokens JSONB NOT NULL,
Expand Down
1 change: 1 addition & 0 deletions auction-server/migrations/20240326063340_bids.down.sql
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
DROP TABLE bid;
DROP TYPE bid_status;
10 changes: 6 additions & 4 deletions auction-server/migrations/20240326063340_bids.up.sql
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
CREATE TYPE bid_status AS ENUM ('pending', 'lost', 'submitted');

CREATE TABLE bid
(
id UUID PRIMARY KEY,
creation_time TIMESTAMP NOT NULL,
permission_key BYTEA NOT NULL,
chain_id TEXT NOT NULL,
target_contract BYTEA NOT NULL,
target_contract BYTEA NOT NULL CHECK (LENGTH(target_contract) = 20),
target_calldata BYTEA NOT NULL,
bid_amount NUMERIC(80, 0) NOT NULL,
status TEXT NOT NULL, -- pending, lost, submitted
auction_id UUID, -- TODO: should be linked to the auction table in the future
bid_amount NUMERIC(78, 0) NOT NULL,
status bid_status NOT NULL,
auction_id UUID, -- TODO: should be linked to the auction table in the future
removal_time TIMESTAMP -- TODO: should be removed and read from the auction table in the future
);
10 changes: 6 additions & 4 deletions auction-server/src/auction.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ use {
BidAmount,
BidStatus,
BidStatusWithId,
PermissionKey,
SimulatedBid,
Store,
},
Expand Down Expand Up @@ -226,8 +227,10 @@ pub async fn run_submission_loop(store: Arc<Store>) -> Result<()> {
_ = submission_interval.tick() => {
for (chain_id, chain_store) in &store.chains {
let all_bids = store.get_bids_by_chain_id(chain_id).await;
let bid_by_permission_key = all_bids.into_iter().fold(HashMap::new(), |mut acc, bid| {
acc.entry(bid.permission_key.clone()).or_insert_with(Vec::new).push(bid);
let bid_by_permission_key:HashMap<PermissionKey,Vec<SimulatedBid>> =
all_bids.into_iter().fold(HashMap::new(),
|mut acc, bid| {
acc.entry(bid.permission_key.clone()).or_default().push(bid);
acc
});

Expand Down Expand Up @@ -263,8 +266,7 @@ pub async fn run_submission_loop(store: Arc<Store>) -> Result<()> {
true => BidStatus::Submitted(receipt.transaction_hash),
false => BidStatus::Lost
};
store.set_bid_status_and_broadcast(BidStatusWithId { id: bid.id, bid_status }).await?;
store.remove_bid(&bid.id).await?;
store.finalize_bid_status_and_broadcast(BidStatusWithId { id: bid.id, bid_status }).await?;
}
}
None => {
Expand Down
89 changes: 50 additions & 39 deletions auction-server/src/state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,18 @@ use {
Deserialize,
Serialize,
},
sqlx::types::{
time::{
OffsetDateTime,
PrimitiveDateTime,
sqlx::{
database::HasArguments,
encode::IsNull,
types::{
time::{
OffsetDateTime,
PrimitiveDateTime,
},
BigDecimal,
},
BigDecimal,
Postgres,
TypeInfo,
},
std::{
collections::HashMap,
Expand Down Expand Up @@ -171,13 +177,24 @@ pub enum BidStatus {
Lost,
}

impl BidStatus {
pub fn status_name(&self) -> String {
match self {
BidStatus::Pending => "pending".to_string(),
BidStatus::Submitted(_) => "submitted".to_string(),
BidStatus::Lost => "lost".to_string(),
}
impl sqlx::Encode<'_, sqlx::Postgres> for BidStatus {
fn encode_by_ref(&self, buf: &mut <Postgres as HasArguments<'_>>::ArgumentBuffer) -> IsNull {
let result = match self {
BidStatus::Pending => "pending",
BidStatus::Submitted(_) => "submitted",
BidStatus::Lost => "lost",
};
<&str as sqlx::Encode<sqlx::Postgres>>::encode(result, buf)
}
}

impl sqlx::Type<sqlx::Postgres> for BidStatus {
fn type_info() -> sqlx::postgres::PgTypeInfo {
sqlx::postgres::PgTypeInfo::with_name("bid_status")
}

fn compatible(ty: &sqlx::postgres::PgTypeInfo) -> bool {
ty.name() == "bid_status"
}
}

Expand Down Expand Up @@ -246,12 +263,16 @@ impl Store {
let key = match &opportunity.params {
OpportunityParams::V1(params) => params.permission_key.clone(),
};
self.opportunity_store
.opportunities
.write()
.await
.entry(key)
.and_modify(|opps| opps.retain(|o| o != opportunity));
let mut write_guard = self.opportunity_store.opportunities.write().await;
let entry = write_guard.entry(key.clone());
if entry
.and_modify(|opps| opps.retain(|o| o != opportunity))
.or_default()
.is_empty()
{
write_guard.remove(&key);
}
drop(write_guard);
let now = OffsetDateTime::now_utc();
sqlx::query!(
"UPDATE opportunity SET removal_time = $1 WHERE id = $2 AND removal_time IS NULL",
Expand All @@ -274,7 +295,7 @@ impl Store {
&bid.target_contract.to_fixed_bytes(),
bid.target_calldata.to_vec(),
BigDecimal::from_str(&bid.bid_amount.to_string()).unwrap(),
bid.status.status_name(),
bid.status as _,
)
.execute(&self.db)
.await.map_err(|e| {
Expand All @@ -290,22 +311,25 @@ impl Store {
Ok(())
}

pub async fn set_bid_status_and_broadcast(
pub async fn finalize_bid_status_and_broadcast(
Copy link
Contributor

Choose a reason for hiding this comment

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

a clearer function name might be broadcast_bid_status_and_remove since that is less ambiguous as to what finalize means. but i'm fine with current name if you prefer it

&self,
update: BidStatusWithId,
) -> anyhow::Result<()> {
if update.bid_status == BidStatus::Pending {
return Err(anyhow::anyhow!("Cannot finalize a pending bid"));
Copy link
Contributor

Choose a reason for hiding this comment

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

this error msg is a bit awkward, maybe just "Bid is still pending"

}

let now = OffsetDateTime::now_utc();
sqlx::query!(
"UPDATE bid SET status = $1 WHERE id = $2",
update.bid_status.status_name(),
"UPDATE bid SET status = $1, removal_time = $2 WHERE id = $3 AND removal_time IS NULL",
Copy link
Contributor

Choose a reason for hiding this comment

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

finalization_time or status_time?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

This field will be removed soon, when we add the auction table. Let's keep it as is for now.

update.bid_status as _,
PrimitiveDateTime::new(now.date(), now.time()),
update.id
)
.execute(&self.db)
.await?;


self.bids.write().await.get_mut(&update.id).map(|bid| {
bid.status = update.bid_status.clone();
});
self.bids.write().await.remove(&update.id);
self.broadcast_status_update(update);
Ok(())
}
Expand All @@ -326,17 +350,4 @@ impl Store {
.cloned()
.collect()
}

pub async fn remove_bid(&self, bid_id: &BidId) -> anyhow::Result<()> {
let now = OffsetDateTime::now_utc();
sqlx::query!(
"UPDATE bid SET removal_time = $1 WHERE id = $2 AND removal_time IS NULL",
PrimitiveDateTime::new(now.date(), now.time()),
bid_id
)
.execute(&self.db)
.await?;
self.bids.write().await.remove(bid_id);
Ok(())
}
}
Loading