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/integration-tests/sdk_expected_db_output_files/token_v2_processor/test_token_v2_burn_event_v1/current_token_ownerships_v2.json b/integration-tests/sdk_expected_db_output_files/token_v2_processor/test_token_v2_burn_event_v1/current_token_ownerships_v2.json index 484bea33..1298bd34 100644 --- a/integration-tests/sdk_expected_db_output_files/token_v2_processor/test_token_v2_burn_event_v1/current_token_ownerships_v2.json +++ b/integration-tests/sdk_expected_db_output_files/token_v2_processor/test_token_v2_burn_event_v1/current_token_ownerships_v2.json @@ -7,7 +7,7 @@ "amount": "0", "table_type_v1": null, "token_properties_mutated_v1": null, - "is_soulbound_v2": true, + "is_soulbound_v2": false, "token_standard": "v2", "is_fungible_v2": false, "last_transaction_version": 1080786089, 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..b5a83c85 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 @@ -275,9 +275,10 @@ impl TokenOwnershipV2 { // is_soulbound currently means if an object is completely untransferrable // OR if only admin can transfer. Only the former is true soulbound but // people might already be using it with the latter meaning so let's include both. + // and_then (not map): object present with untransferable=None is not soulbound. let is_soulbound = if object_metadatas .get(&token_data_id) - .map(|obj| obj.untransferable.as_ref()) + .and_then(|obj| obj.untransferable.as_ref()) .is_some() { true @@ -835,3 +836,151 @@ impl From for PostgresCurrentTokenOwnershipV2 { } } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::processors::objects::v2_object_utils::{ObjectAggregatedData, Untransferable}; + use aptos_indexer_processor_sdk::aptos_protos::transaction::v1::MoveStructTag; + + const TOKEN_ADDR: &str = "0xabc"; + const OWNER_ADDR: &str = "0xdef"; + + fn object_core_write( + address: &str, + allow_ungated_transfer: bool, + owner: &str, + ) -> WriteResource { + WriteResource { + address: address.to_string(), + state_key_hash: vec![], + r#type: Some(MoveStructTag { + address: "0x1".to_string(), + module: "object".to_string(), + name: "ObjectCore".to_string(), + generic_type_params: vec![], + }), + type_str: "0x1::object::ObjectCore".to_string(), + data: format!( + r#"{{"allow_ungated_transfer":{allow_ungated_transfer},"guid_creation_num":"0","owner":"{owner}"}}"# + ), + } + } + + fn burned_map(token_address: &str) -> TokenV2Burned { + let mut tokens_burned = AHashMap::new(); + let token_data_id = standardize_address(token_address); + tokens_burned.insert( + token_data_id.clone(), + crate::processors::token_v2::token_v2_models::v2_token_utils::Burn::new( + standardize_address("0x1"), + BigDecimal::zero(), + token_data_id, + standardize_address(OWNER_ADDR), + ), + ); + tokens_burned + } + + fn object_metadata( + token_address: &str, + untransferable: Option, + ) -> ObjectAggregatedDataMapping { + let mut map = AHashMap::new(); + map.insert(standardize_address(token_address), ObjectAggregatedData { + untransferable, + ..ObjectAggregatedData::default() + }); + map + } + + fn txn_timestamp() -> chrono::NaiveDateTime { + chrono::NaiveDateTime::parse_from_str("2024-01-01 00:00:00", "%Y-%m-%d %H:%M:%S").unwrap() + } + + #[tokio::test] + async fn burned_transferable_nft_is_not_soulbound() { + // Production burn path: ObjectCore is still written and object_metadatas has + // the token with untransferable=None. The old map().is_some() treated + // Some(None) as soulbound. + let write_resource = object_core_write(TOKEN_ADDR, true, OWNER_ADDR); + let tokens_burned = burned_map(TOKEN_ADDR); + let object_metadatas = object_metadata(TOKEN_ADDR, None); + let prior = AHashMap::new(); + let mut db_context = None; + + let result = TokenOwnershipV2::get_burned_nft_v2_from_write_resource( + &write_resource, + 1, + 0, + txn_timestamp(), + &prior, + &tokens_burned, + &object_metadatas, + &mut db_context, + ) + .await + .unwrap() + .expect("burned NFT ownership row"); + + assert_eq!(result.0.is_soulbound_v2, Some(false)); + assert_eq!(result.1.is_soulbound_v2, Some(false)); + assert_eq!(result.0.non_transferrable_by_owner, Some(false)); + assert_eq!(result.1.non_transferrable_by_owner, Some(false)); + } + + #[tokio::test] + async fn burned_admin_gated_nft_is_soulbound() { + let write_resource = object_core_write(TOKEN_ADDR, false, OWNER_ADDR); + let tokens_burned = burned_map(TOKEN_ADDR); + let object_metadatas = object_metadata(TOKEN_ADDR, None); + let prior = AHashMap::new(); + let mut db_context = None; + + let result = TokenOwnershipV2::get_burned_nft_v2_from_write_resource( + &write_resource, + 1, + 0, + txn_timestamp(), + &prior, + &tokens_burned, + &object_metadatas, + &mut db_context, + ) + .await + .unwrap() + .expect("burned NFT ownership row"); + + assert_eq!(result.0.is_soulbound_v2, Some(true)); + assert_eq!(result.0.non_transferrable_by_owner, Some(true)); + } + + #[tokio::test] + async fn burned_untransferable_nft_is_soulbound() { + let write_resource = object_core_write(TOKEN_ADDR, true, OWNER_ADDR); + let tokens_burned = burned_map(TOKEN_ADDR); + let untransferable = + serde_json::from_str::(r#"{"dummy_field":false}"#).unwrap(); + let object_metadatas = object_metadata(TOKEN_ADDR, Some(untransferable)); + let prior = AHashMap::new(); + let mut db_context = None; + + let result = TokenOwnershipV2::get_burned_nft_v2_from_write_resource( + &write_resource, + 1, + 0, + txn_timestamp(), + &prior, + &tokens_burned, + &object_metadatas, + &mut db_context, + ) + .await + .unwrap() + .expect("burned NFT ownership row"); + + assert_eq!(result.0.is_soulbound_v2, Some(true)); + // Owner-gated flag still follows ObjectCore, not Untransferable. + assert_eq!(result.0.non_transferrable_by_owner, Some(false)); + } +} 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.