From de318fe213972cfbace74770c77c44b30937141d Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 6 Sep 2026 12:15:25 +0000 Subject: [PATCH 1/2] Fix transfer-row non_transferrable_by_owner copying is_soulbound Transfer soft-delete rows in get_nft_v2_from_token_data wrote is_soulbound into non_transferrable_by_owner. Those flags diverge when Untransferable is present and allow_ungated_transfer is true. Co-authored-by: Young Yang Liauw --- .../token_v2_models/v2_token_ownerships.rs | 137 +++++++++++++++++- 1 file changed, 135 insertions(+), 2 deletions(-) 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..e44da6be 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 @@ -220,7 +220,7 @@ impl TokenOwnershipV2 { token_standard: TokenStandard::V2.to_string(), is_fungible_v2: None, transaction_timestamp: token_data.transaction_timestamp, - non_transferrable_by_owner: Some(is_soulbound), + non_transferrable_by_owner: Some(non_transferrable_by_owner), }); current_ownerships.insert( ( @@ -244,7 +244,7 @@ impl TokenOwnershipV2 { is_fungible_v2: None, last_transaction_version: token_data.transaction_version, last_transaction_timestamp: token_data.transaction_timestamp, - non_transferrable_by_owner: Some(is_soulbound), + non_transferrable_by_owner: Some(non_transferrable_by_owner), }, ); } @@ -835,3 +835,136 @@ impl From for PostgresCurrentTokenOwnershipV2 { } } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::processors::{ + objects::v2_object_utils::{ + ObjectAggregatedData, ObjectCore, ObjectWithMetadata, Untransferable, + }, + token_v2::token_v2_models::v2_token_utils::TransferEvent, + }; + + const TOKEN_ADDR: &str = "0xabc"; + const PREV_OWNER: &str = "0xaaa"; + const NEW_OWNER: &str = "0xdef"; + + fn txn_timestamp() -> chrono::NaiveDateTime { + chrono::NaiveDateTime::parse_from_str("2024-01-01 00:00:00", "%Y-%m-%d %H:%M:%S").unwrap() + } + + fn token_data(token_address: &str) -> TokenDataV2 { + TokenDataV2 { + transaction_version: 1, + write_set_change_index: 0, + token_data_id: standardize_address(token_address), + transaction_timestamp: txn_timestamp(), + ..Default::default() + } + } + + fn object_core(allow_ungated_transfer: bool, owner: &str) -> ObjectCore { + serde_json::from_str(&format!( + r#"{{"allow_ungated_transfer":{allow_ungated_transfer},"guid_creation_num":"0","owner":"{owner}"}}"# + )) + .unwrap() + } + + fn transfer_event(from: &str, to: &str, object: &str) -> TransferEvent { + serde_json::from_str(&format!( + r#"{{"from":"{from}","to":"{to}","object":"{object}"}}"# + )) + .unwrap() + } + + fn object_metadatas( + token_address: &str, + allow_ungated_transfer: bool, + owner: &str, + untransferable: Option, + transfers: Vec<(i64, TransferEvent)>, + ) -> ObjectAggregatedDataMapping { + let mut map = AHashMap::new(); + map.insert(standardize_address(token_address), ObjectAggregatedData { + object: ObjectWithMetadata { + object_core: object_core(allow_ungated_transfer, owner), + state_key_hash: String::new(), + }, + transfer_events: transfers, + untransferable, + ..ObjectAggregatedData::default() + }); + map + } + + fn previous_owner_rows<'a>( + ownerships: &'a [TokenOwnershipV2], + current: &'a AHashMap, + prev_owner: &str, + ) -> (&'a TokenOwnershipV2, &'a CurrentTokenOwnershipV2) { + let prev = standardize_address(prev_owner); + let history = ownerships + .iter() + .find(|row| row.owner_address.as_deref() == Some(prev.as_str()) && row.amount.is_zero()) + .expect("soft-delete transfer history row"); + let current_row = current + .values() + .find(|row| row.owner_address == prev && row.amount.is_zero()) + .expect("soft-delete current ownership row"); + (history, current_row) + } + + #[test] + fn transfer_rows_keep_owner_gated_flag_distinct_from_soulbound() { + // Untransferable present + allow_ungated_transfer=true is the only state + // where is_soulbound and non_transferrable_by_owner diverge. Transfer + // history used to copy is_soulbound into the owner-gated column. + let untransferable = + serde_json::from_str::(r#"{"dummy_field":false}"#).unwrap(); + let object_metadatas = + object_metadatas(TOKEN_ADDR, true, NEW_OWNER, Some(untransferable), vec![( + 1, + transfer_event(PREV_OWNER, NEW_OWNER, TOKEN_ADDR), + )]); + + let (ownerships, current) = TokenOwnershipV2::get_nft_v2_from_token_data( + &token_data(TOKEN_ADDR), + &object_metadatas, + ) + .unwrap(); + + let new_owner = standardize_address(NEW_OWNER); + let current_owner = current + .values() + .find(|row| row.owner_address == new_owner) + .expect("current owner row"); + assert_eq!(current_owner.is_soulbound_v2, Some(true)); + assert_eq!(current_owner.non_transferrable_by_owner, Some(false)); + + let (history, previous_current) = previous_owner_rows(&ownerships, ¤t, PREV_OWNER); + assert_eq!(history.is_soulbound_v2, Some(true)); + assert_eq!(history.non_transferrable_by_owner, Some(false)); + assert_eq!(previous_current.is_soulbound_v2, Some(true)); + assert_eq!(previous_current.non_transferrable_by_owner, Some(false)); + } + + #[test] + fn transfer_rows_mark_admin_gated_tokens_non_transferrable_by_owner() { + let object_metadatas = object_metadatas(TOKEN_ADDR, false, NEW_OWNER, None, vec![( + 1, + transfer_event(PREV_OWNER, NEW_OWNER, TOKEN_ADDR), + )]); + + let (ownerships, current) = TokenOwnershipV2::get_nft_v2_from_token_data( + &token_data(TOKEN_ADDR), + &object_metadatas, + ) + .unwrap(); + + let (history, previous_current) = previous_owner_rows(&ownerships, ¤t, PREV_OWNER); + assert_eq!(history.is_soulbound_v2, Some(true)); + assert_eq!(history.non_transferrable_by_owner, Some(true)); + assert_eq!(previous_current.non_transferrable_by_owner, Some(true)); + } +} From 8114feb8e67c0454fa7682c45e72b09e3a543572 Mon Sep 17 00:00:00 2001 From: Young Yang Liauw <7528420+sausagee@users.noreply.github.com> Date: Mon, 7 Sep 2026 15:33:54 -0700 Subject: [PATCH 2/2] ci: apply #70 unblocker files (stable xclippy + bookworm docker) Stack the CI unblocker files from PR #70 (stable cargo xclippy, bookworm Dockerfiles with make) onto this bugfix branch so CI can go green while #70 awaits review. Merge target remains main; this does not merge #70. --- Dockerfile | 22 +++++++++++++++++----- Dockerfile.address-reputation-api | 22 +++++++++++++++++----- scripts/rust_lint.sh | 5 ++++- 3 files changed, 38 insertions(+), 11 deletions(-) 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/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.