diff --git a/Dockerfile b/Dockerfile index a7e266e6..57df1080 100644 --- a/Dockerfile +++ b/Dockerfile @@ -2,13 +2,25 @@ # Stage 1: Build the binary -FROM rust:slim-bullseye as builder +FROM rust:slim-bookworm AS builder WORKDIR /app COPY --link . /app -RUN for i in 1 2 3; do apt-get update && apt-get install --fix-missing -y cmake curl clang git pkg-config libssl-dev libdw-dev libpq-dev lld && break || sleep 10; done +RUN apt-get update \ + && apt-get install --no-install-recommends --fix-missing -y \ + cmake \ + curl \ + clang \ + make \ + git \ + pkg-config \ + libssl-dev \ + libdw-dev \ + libpq-dev \ + lld \ + && rm -rf /var/lib/apt/lists/* ENV CARGO_NET_GIT_FETCH_WITH_CLI true # TODO: Fix this with real processors. RUN cargo build --locked --release -p processor && ls -lah target/release/ @@ -24,19 +36,19 @@ ENV GIT_SHA ${GIT_SHA} # Stage 2: Create the final image -FROM debian:bullseye-slim +FROM debian:bookworm-slim COPY --from=builder /usr/local/bin/processor /usr/local/bin RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \ --mount=type=cache,target=/var/lib/apt,sharing=locked \ apt-get update && apt-get install --no-install-recommends --fix-missing -y \ - libssl1.1 \ + libssl3 \ ca-certificates \ net-tools \ tcpdump \ iproute2 \ - netcat \ + netcat-openbsd \ libdw-dev \ libpq-dev \ curl diff --git a/Dockerfile.address-reputation-api b/Dockerfile.address-reputation-api index 5be4c111..488d8ed5 100644 --- a/Dockerfile.address-reputation-api +++ b/Dockerfile.address-reputation-api @@ -2,13 +2,25 @@ # Stage 1: Build the binary -FROM rust:slim-bullseye as builder +FROM rust:slim-bookworm AS builder WORKDIR /app COPY --link . /app -RUN for i in 1 2 3; do apt-get update && apt-get install --fix-missing -y cmake curl clang git pkg-config libssl-dev libdw-dev libpq-dev lld && break || sleep 10; done +RUN apt-get update \ + && apt-get install --no-install-recommends --fix-missing -y \ + cmake \ + curl \ + clang \ + make \ + git \ + pkg-config \ + libssl-dev \ + libdw-dev \ + libpq-dev \ + lld \ + && rm -rf /var/lib/apt/lists/* ENV CARGO_NET_GIT_FETCH_WITH_CLI true RUN cargo build --locked --release -p address-reputation-api && ls -lah target/release/ RUN cp target/release/address-reputation-api /usr/local/bin @@ -23,19 +35,19 @@ ENV GIT_SHA ${GIT_SHA} # Stage 2: Create the final image -FROM debian:bullseye-slim +FROM debian:bookworm-slim COPY --from=builder /usr/local/bin/address-reputation-api /usr/local/bin RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \ --mount=type=cache,target=/var/lib/apt,sharing=locked \ apt-get update && apt-get install --no-install-recommends --fix-missing -y \ - libssl1.1 \ + libssl3 \ ca-certificates \ net-tools \ tcpdump \ iproute2 \ - netcat \ + netcat-openbsd \ libdw-dev \ libpq-dev \ curl diff --git a/processor/src/processors/token_v2/token_models/tokens.rs b/processor/src/processors/token_v2/token_models/tokens.rs index 0f462325..dbe05391 100644 --- a/processor/src/processors/token_v2/token_models/tokens.rs +++ b/processor/src/processors/token_v2/token_models/tokens.rs @@ -39,8 +39,11 @@ pub type CurrentTokenPendingClaimPK = (TokenDataIdHash, BigDecimal, Address, Add // PK of tokens table, used to dedupe tokens pub type TokenPK = (TokenDataIdHash, BigDecimal); // Map to keep track of token withdraw and deposit module events for token v1. -pub type TokenV1WithdrawModuleEvents = AHashMap; -pub type TokenV1DepositModuleEvents = AHashMap; +// Keyed by (token_data_id, property_version) so two PVs of the same named token +// in one txn do not share an owner fallback (TokenStore not rewritten). +pub type TokenV1ModuleEventPK = (TokenDataIdHash, BigDecimal); +pub type TokenV1WithdrawModuleEvents = AHashMap; +pub type TokenV1DepositModuleEvents = AHashMap; #[derive(Clone, Debug, Deserialize, FieldCount, Identifiable, Insertable, Serialize)] #[diesel(primary_key(token_data_id_hash, property_version, transaction_version))] diff --git a/processor/src/processors/token_v2/token_v2_models/v2_token_activities.rs b/processor/src/processors/token_v2/token_v2_models/v2_token_activities.rs index 5ea0d503..e293421a 100644 --- a/processor/src/processors/token_v2/token_v2_models/v2_token_activities.rs +++ b/processor/src/processors/token_v2/token_v2_models/v2_token_activities.rs @@ -279,7 +279,13 @@ impl TokenActivityV2 { to_address: None, token_amount: inner.amount.clone(), }; - tokens_withdrawn.insert(token_data_id_struct.to_id(), helper.clone()); + tokens_withdrawn.insert( + ( + token_data_id_struct.to_id(), + inner.id.property_version.clone(), + ), + helper.clone(), + ); helper }, TokenEvent::DepositTokenEvent(inner) => TokenActivityHelperV1 { @@ -298,7 +304,13 @@ impl TokenActivityV2 { to_address: Some(inner.get_account()), token_amount: inner.amount.clone(), }; - tokens_deposited.insert(token_data_id_struct.to_id(), helper.clone()); + tokens_deposited.insert( + ( + token_data_id_struct.to_id(), + inner.id.property_version.clone(), + ), + helper.clone(), + ); helper }, TokenEvent::OfferTokenEvent(inner) => TokenActivityHelperV1 { diff --git a/processor/src/processors/token_v2/token_v2_models/v2_token_ownerships.rs b/processor/src/processors/token_v2/token_v2_models/v2_token_ownerships.rs index a95159c4..7f985422 100644 --- a/processor/src/processors/token_v2/token_v2_models/v2_token_ownerships.rs +++ b/processor/src/processors/token_v2/token_v2_models/v2_token_ownerships.rs @@ -492,7 +492,10 @@ impl TokenOwnershipV2 { // If table_handle_to_owner doesn’t have the table metadata, it might be because // that module events were emitted, which means the resource won’t appear in the transaction. // Try getting the token metadata from the deposit module event instead. - let maybe_token_metadata = tokens_deposited.get(&token_data_id); + let maybe_token_metadata = tokens_deposited.get(&( + token_data_id.clone(), + token_id_struct.property_version.clone(), + )); let owner_address = if let Some(token_metadata) = maybe_token_metadata { token_metadata.to_address.clone().unwrap() } else { @@ -572,7 +575,10 @@ impl TokenOwnershipV2 { let owner_address = tm.get_owner_address(); (owner_address, Some(tm.table_type.clone())) } else { - let maybe_token_metadata = tokens_withdrawn.get(&token_data_id); + let maybe_token_metadata = tokens_withdrawn.get(&( + token_data_id.clone(), + token_id_struct.property_version.clone(), + )); let owner_address = if let Some(token_metadata) = maybe_token_metadata { token_metadata.from_address.clone().unwrap() } else { @@ -835,3 +841,203 @@ impl From for PostgresCurrentTokenOwnershipV2 { } } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::processors::token_v2::{ + token_models::token_claims::{TokenV1Canceled, TokenV1Claimed}, + token_v2_models::v2_token_activities::TokenActivityV2, + }; + use aptos_indexer_processor_sdk::aptos_protos::transaction::v1::{ + DeleteTableData, DeleteTableItem, Event, EventKey, WriteTableData, WriteTableItem, + }; + + const CREATOR: &str = "0x1"; + const COLLECTION: &str = "C"; + const NAME: &str = "T"; + const ALICE: &str = "0xa11ce"; + const BOB: &str = "0xb0b"; + + fn ts() -> chrono::NaiveDateTime { + chrono::NaiveDateTime::default() + } + + fn token_id_json(property_version: &str) -> String { + format!( + r#"{{"token_data_id":{{"creator":"{CREATOR}","collection":"{COLLECTION}","name":"{NAME}"}},"property_version":"{property_version}"}}"# + ) + } + + fn token_deposit_event(account: &str, property_version: &str) -> Event { + Event { + key: Some(EventKey { + creation_number: 0, + account_address: "0x0".to_string(), + }), + sequence_number: 0, + type_str: "0x3::token::TokenDeposit".to_string(), + data: format!( + r#"{{"account":"{account}","amount":"1","id":{}}}"#, + token_id_json(property_version) + ), + ..Default::default() + } + } + + fn token_withdraw_event(account: &str, property_version: &str) -> Event { + Event { + key: Some(EventKey { + creation_number: 0, + account_address: "0x0".to_string(), + }), + sequence_number: 0, + type_str: "0x3::token::TokenWithdraw".to_string(), + data: format!( + r#"{{"account":"{account}","amount":"1","id":{}}}"#, + token_id_json(property_version) + ), + ..Default::default() + } + } + + fn write_token_table_item(handle: &str, property_version: &str) -> WriteTableItem { + let token_json = format!( + r#"{{"amount":"1","id":{},"token_properties":"0x00"}}"#, + token_id_json(property_version) + ); + WriteTableItem { + handle: handle.to_string(), + key: token_id_json(property_version), + data: Some(WriteTableData { + key: token_id_json(property_version), + key_type: "0x3::token::TokenId".to_string(), + value: token_json, + value_type: "0x3::token::Token".to_string(), + }), + ..Default::default() + } + } + + fn delete_token_table_item(handle: &str, property_version: &str) -> DeleteTableItem { + DeleteTableItem { + handle: handle.to_string(), + key: token_id_json(property_version), + data: Some(DeleteTableData { + key: token_id_json(property_version), + key_type: "0x3::token::TokenId".to_string(), + }), + ..Default::default() + } + } + + fn ingest_v1_events( + events: &[Event], + ) -> (TokenV1DepositModuleEvents, TokenV1WithdrawModuleEvents) { + let mut tokens_claimed: TokenV1Claimed = AHashMap::new(); + let mut tokens_canceled: TokenV1Canceled = AHashMap::new(); + let mut tokens_withdrawn = AHashMap::new(); + let mut tokens_deposited = AHashMap::new(); + for (i, event) in events.iter().enumerate() { + TokenActivityV2::get_v1_from_parsed_event( + event, + 1, + ts(), + i as i64, + &None, + &mut tokens_claimed, + &mut tokens_canceled, + &mut tokens_withdrawn, + &mut tokens_deposited, + ) + .unwrap(); + } + (tokens_deposited, tokens_withdrawn) + } + + #[test] + fn deposit_fallback_does_not_reuse_owner_across_property_versions() { + let (tokens_deposited, _) = ingest_v1_events(&[ + token_deposit_event(ALICE, "0"), + token_deposit_event(BOB, "1"), + ]); + let empty_owners = AHashMap::new(); + + let alice_row = TokenOwnershipV2::get_v1_from_write_table_item( + &write_token_table_item("0xaaa", "0"), + 1, + 0, + ts(), + &empty_owners, + &tokens_deposited, + ) + .unwrap() + .expect("pv0 deposit fallback") + .1 + .expect("current ownership"); + let bob_row = TokenOwnershipV2::get_v1_from_write_table_item( + &write_token_table_item("0xbbb", "1"), + 1, + 1, + ts(), + &empty_owners, + &tokens_deposited, + ) + .unwrap() + .expect("pv1 deposit fallback") + .1 + .expect("current ownership"); + + assert_eq!( + alice_row.owner_address, + standardize_address(ALICE), + "pv0 must keep Alice, not the later Bob deposit" + ); + assert_eq!(bob_row.owner_address, standardize_address(BOB)); + assert_eq!(alice_row.property_version_v1, BigDecimal::from(0)); + assert_eq!(bob_row.property_version_v1, BigDecimal::from(1)); + } + + #[test] + fn withdraw_fallback_does_not_reuse_owner_across_property_versions() { + let (_, tokens_withdrawn) = ingest_v1_events(&[ + token_withdraw_event(ALICE, "0"), + token_withdraw_event(BOB, "1"), + ]); + let empty_owners = AHashMap::new(); + + let alice_row = TokenOwnershipV2::get_v1_from_delete_table_item( + &delete_token_table_item("0xaaa", "0"), + 1, + 0, + ts(), + &empty_owners, + &tokens_withdrawn, + ) + .unwrap() + .expect("pv0 withdraw fallback") + .1 + .expect("current ownership"); + let bob_row = TokenOwnershipV2::get_v1_from_delete_table_item( + &delete_token_table_item("0xbbb", "1"), + 1, + 1, + ts(), + &empty_owners, + &tokens_withdrawn, + ) + .unwrap() + .expect("pv1 withdraw fallback") + .1 + .expect("current ownership"); + + assert_eq!( + alice_row.owner_address, + standardize_address(ALICE), + "pv0 must keep Alice, not the later Bob withdraw" + ); + assert_eq!(bob_row.owner_address, standardize_address(BOB)); + assert_eq!(alice_row.amount, BigDecimal::zero()); + assert_eq!(bob_row.amount, BigDecimal::zero()); + } +} diff --git a/scripts/rust_lint.sh b/scripts/rust_lint.sh index 261c1aa9..32a48f2b 100755 --- a/scripts/rust_lint.sh +++ b/scripts/rust_lint.sh @@ -25,7 +25,10 @@ fi set -e set -x -cargo +nightly xclippy +# Run clippy on the pinned STABLE toolchain (from rust-toolchain.toml), NOT +# nightly. Latest nightly makes Infallible an alias of !, which breaks +# allocative (impl Allocative for both). Stable clippy matches the build toolchain. +cargo xclippy # We require the nightly build of cargo fmt # to provide stricter rust formatting.