diff --git a/api/stake/admin.py b/api/stake/admin.py index 33a4b5cf6..9d6e07770 100644 --- a/api/stake/admin.py +++ b/api/stake/admin.py @@ -1,6 +1,6 @@ from django.contrib import admin from scorer.scorer_admin import ScorerModelAdmin -from stake.models import Stake, StakeEvent +from stake.models import Stake, StakeEvent, ReindexRequest @admin.register(Stake) @@ -51,3 +51,20 @@ class StakeEventAdmin(ScorerModelAdmin): "tx_hash", ] search_help_text = "Search by: " + ", ".join(search_fields) + + +@admin.register(ReindexRequest) +class ReindexRequestAdmin(admin.ModelAdmin): + list_display = [ + "pending", + "chain", + "start_block_number", + "created_at", + ] + + list_filter = [ + "chain", + "pending", + ] + + readonly_fields = ("pending",) diff --git a/api/stake/migrations/0006_reindexrequest_alter_stakeevent_block_number_and_more.py b/api/stake/migrations/0006_reindexrequest_alter_stakeevent_block_number_and_more.py new file mode 100644 index 000000000..2563e1f20 --- /dev/null +++ b/api/stake/migrations/0006_reindexrequest_alter_stakeevent_block_number_and_more.py @@ -0,0 +1,56 @@ +# Generated by Django 4.2.6 on 2024-07-02 21:30 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("stake", "0005_alter_stake_current_amount_alter_stakeevent_amount"), + ] + + operations = [ + migrations.CreateModel( + name="ReindexRequest", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("created_at", models.DateTimeField(auto_now_add=True)), + ( + "chain", + models.IntegerField( + db_index=True, + help_text="Decimal chain ID. Ethereum: 1, Optimism: 10, Arbitrum: 42161", + ), + ), + ( + "start_block_number", + models.DecimalField(decimal_places=0, max_digits=78), + ), + ("pending", models.BooleanField(db_index=True, default=True)), + ], + ), + migrations.AlterField( + model_name="stakeevent", + name="block_number", + field=models.DecimalField(db_index=True, decimal_places=0, max_digits=78), + ), + migrations.AlterUniqueTogether( + name="stakeevent", + unique_together={("tx_hash", "chain")}, + ), + migrations.AddConstraint( + model_name="reindexrequest", + constraint=models.UniqueConstraint( + condition=models.Q(("pending", True)), + fields=("chain",), + name="unique_only_one_pending_per_chain", + ), + ), + ] diff --git a/api/stake/models.py b/api/stake/models.py index 878fbb609..182222666 100644 --- a/api/stake/models.py +++ b/api/stake/models.py @@ -65,10 +65,40 @@ class StakeEventType(models.TextChoices): ) block_number = models.DecimalField( - decimal_places=0, null=False, blank=False, max_digits=78 + decimal_places=0, null=False, blank=False, max_digits=78, db_index=True ) tx_hash = models.CharField(max_length=66, null=False, blank=False) # Only applies to SelfStake and CommunityStake events unlock_time = models.DateTimeField(null=True, blank=True) + + class Meta: + unique_together = ["tx_hash", "chain"] + + +class ReindexRequest(models.Model): + created_at = models.DateTimeField(auto_now_add=True) + + chain = models.IntegerField( + null=False, + blank=False, + db_index=True, + help_text="Decimal chain ID. Ethereum: 1, Optimism: 10, Arbitrum: 42161", + ) + + start_block_number = models.DecimalField( + decimal_places=0, null=False, blank=False, max_digits=78 + ) + + pending = models.BooleanField(null=False, blank=False, default=True, db_index=True) + + class Meta: + # Only one reindex request can be pending at a time for a chain + constraints = [ + models.UniqueConstraint( + fields=["chain"], + name="unique_only_one_pending_per_chain", + condition=models.Q(pending=True), + ), + ] diff --git a/indexer/src/main.rs b/indexer/src/main.rs index ecc128ca7..1a0492b78 100644 --- a/indexer/src/main.rs +++ b/indexer/src/main.rs @@ -27,7 +27,7 @@ async fn main() -> Result<()> { let contract_address_op_mainnet = get_env("STAKING_CONTRACT_ADDRESS_OP_MAINNET") .parse::
() .unwrap(); - + let contract_address_op_sepolia = get_env("STAKING_CONTRACT_ADDRESS_OP_SEPOLIA") .parse::() .unwrap(); diff --git a/indexer/src/postgres.rs b/indexer/src/postgres.rs index 40459a67e..a384e62bd 100644 --- a/indexer/src/postgres.rs +++ b/indexer/src/postgres.rs @@ -36,9 +36,7 @@ impl PostgresClient { let pool = Pool::builder(mgr).max_size(16).build().unwrap(); - Ok(Self { - pool, - }) + Ok(Self { pool }) } // This function is for legacy staking contract events @@ -56,7 +54,7 @@ impl PostgresClient { let client = self.pool.get().await.unwrap(); client.execute("INSERT INTO registry_gtcstakeevent (event_type, round_id, staker, amount, staked, block_number, tx_hash) VALUES ($1, $2, $3, $4, $5, $6, $7)",&[&"SelfStake", &round_id, &staker, &decimal_amount, &staked, &block_number, &tx_hash]).await?; println!( - "Row inserted into registry_gtcstakeevent with type SelfStake for block {}!", + "Row inserted into registry_gtcstakeevent with type SelfStake for block {} for legacy contract!", block_number ); Ok(()) @@ -78,7 +76,7 @@ impl PostgresClient { let client = self.pool.get().await.unwrap(); client.execute("INSERT INTO registry_gtcstakeevent (event_type, round_id, staker, address, amount, staked, block_number, tx_hash) VALUES ($1, $2, $3, $4, $5, $6, $7, $8)", &[&"Xstake", &round_id, &staker, &user, &decimal_amount, &staked, &block_number, &tx_hash]).await?; println!( - "Row inserted into registry_gtcstakeevent with type Xstake for block {}!", + "Row inserted into registry_gtcstakeevent with type Xstake for block {} for legacy contract!", block_number ); Ok(()) @@ -111,28 +109,50 @@ impl PostgresClient { let client = self.pool.get().await.unwrap(); - // Log current stake state - client.execute( - concat!( - "INSERT INTO stake_stake as stake (chain, staker, stakee, unlock_time, lock_time, last_updated_in_block, current_amount)", - " VALUES ($1, $2, $3, $4, $5, $6, $7) ON CONFLICT (chain, staker, stakee) DO UPDATE", - " SET unlock_time = EXCLUDED.unlock_time,", - " lock_time = EXCLUDED.lock_time,", - " last_updated_in_block = EXCLUDED.last_updated_in_block,", - " current_amount = stake.current_amount + EXCLUDED.current_amount", - " WHERE EXCLUDED.last_updated_in_block >= stake.last_updated_in_block" - ), - &[&chain_id, &staker, &stakee, &unlock_time, &lock_time, &block_number, &increase_amount] - ).await?; - - // Log raw event - client.execute( - concat!( - "INSERT INTO stake_stakeevent (event_type, chain, staker, stakee, amount, unlock_time, block_number, tx_hash)", - " VALUES ($1, $2, $3, $4, $5, $6, $7, $8)" - ), - &[&get_code_for_stake_event_type(event_type), &chain_id, &staker, &stakee, &increase_amount, &unlock_time, &block_number, &tx_hash] - ).await?; + // begin transaction + client.execute("BEGIN", &[]).await?; + + let do_query = async { + // Log raw event + client.execute( + concat!( + "INSERT INTO stake_stakeevent (event_type, chain, staker, stakee, amount, unlock_time, block_number, tx_hash)", + " VALUES ($1, $2, $3, $4, $5, $6, $7, $8)" + ), + &[&get_code_for_stake_event_type(event_type), &chain_id, &staker, &stakee, &increase_amount, &unlock_time, &block_number, &tx_hash] + ).await?; + + // Log current stake state + client.execute( + concat!( + "INSERT INTO stake_stake as stake (chain, staker, stakee, unlock_time, lock_time, last_updated_in_block, current_amount)", + " VALUES ($1, $2, $3, $4, $5, $6, $7) ON CONFLICT (chain, staker, stakee) DO UPDATE", + " SET unlock_time = GREATEST(EXCLUDED.unlock_time, stake.unlock_time),", + " lock_time = GREATEST(EXCLUDED.lock_time, stake.lock_time),", + " last_updated_in_block = GREATEST(EXCLUDED.last_updated_in_block, stake.last_updated_in_block),", + " current_amount = stake.current_amount + EXCLUDED.current_amount", + ), + &[&chain_id, &staker, &stakee, &unlock_time, &lock_time, &block_number, &increase_amount] + ).await?; + + Ok::<(), Error>(()) + }; + + match do_query.await { + Ok(_) => { + // commit transaction + client.execute("COMMIT", &[]).await?; + } + Err(e) => { + // rollback transaction + client.execute("ROLLBACK", &[]).await?; + // continue if duplicate key error + if format!("{:?}", e).contains(&format!("Key (tx_hash, chain)=({}, {}) already exists.", tx_hash, chain_id)) { + return Ok(()); + } + return Err(e); + } + } println!( "Added or extended stake in block {} on chain {}!", @@ -166,55 +186,129 @@ impl PostgresClient { let client = self.pool.get().await.unwrap(); - // Log current stake state - client - .execute( - concat!( - "UPDATE stake_stake", - " SET current_amount = current_amount + $1", - " WHERE chain = $2 AND staker = $3 AND stakee = $4", - " AND last_updated_in_block <= $5" - ), - &[&amount, &chain_id, &staker, &stakee, &block_number], + // begin transaction + client.execute("BEGIN", &[]).await?; + + let do_query = async { + // Log raw event + client.execute( + concat!( + "INSERT INTO stake_stakeevent (event_type, chain, staker, stakee, amount, block_number, tx_hash)", + " VALUES ($1, $2, $3, $4, $5, $6, $7)" + ), + &[&get_code_for_stake_event_type(event_type), &chain_id, &staker, &stakee, &amount, &block_number, &tx_hash], + ) + .await?; + + // Log current stake state + client + .execute( + concat!( + "UPDATE stake_stake as stake", + " SET current_amount = current_amount + $1,", + " last_updated_in_block = GREATEST($5, stake.last_updated_in_block)", + " WHERE chain = $2 AND staker = $3 AND stakee = $4", + ), + &[&amount, &chain_id, &staker, &stakee, &block_number], + ) + .await?; + + Ok::<(), Error>(()) + }; + + match do_query.await { + Ok(_) => { + // commit transaction + client.execute("COMMIT", &[]).await?; + } + Err(e) => { + // rollback transaction + client.execute("ROLLBACK", &[]).await?; + // continue if duplicate key error + if format!("{:?}", e).contains(&format!("Key (tx_hash, chain)=({}, {}) already exists.", tx_hash, chain_id)) { + return Ok(()); + } + return Err(e); + } + } + + println!( + "Modified stake amount in block {} on chain {}!", + block_number, chain_id + ); + + Ok(()) + } + + pub async fn get_requested_start_block(&self, chain_id: u32) -> Result