How can I return the latest transaction where any asset of a given policy changed address/ownership?

Here is a query which returns me the most recent transaction which included an asset of a given policy in it's utxo's:

select encode(tx.hash::bytea, 'hex') as hash 
from ma_tx_out as matxo 
join multi_asset as ma on matxo.ident = ma.id 
join tx_out as txo on matxo.tx_out_id = txo.id 
join tx as tx on txo.tx_id = tx.id 
left join tx_in on tx_in.tx_out_id = txo.tx_id and tx_in.tx_out_index = txo.index 
where tx_in.id is null and ma.policy = '\x{policy_id}' 
order by txo.id desc limit 1;

What I would like is similar but also very much not the same, and every way I try to do it turns into a VERY long running query, so I am reaching out here for help.

I want only transactions where any asset of the given policy changed address - i.e. changed ownership, moved to another wallet, was sent to a script address, etc. So I am trying by differentiating on address of asset in an input utxo being on a different address in an output utxo but I think I may be misunderstanding how things are represented in the schema.

2 Answers 2


Here is a query that will detect whether a spacebudz nft has changed ownership. It uses a stake address if one exists and otherwise uses the address.

It runs in ~2 min for me when the time is set to 10 days ago.

tx_id_limit as (
  select tx.id as tx_id
  from block
    join tx on tx.block_id = block.id
  where time > '2024-03-10 00:00:00+00'
  order by tx_id asc
  limit 1

, stxo as (
  select  stxo.tx_out_id
     ,    stxo.tx_in_id
     ,    case
            when stake_address.view is not null then stake_address.view
            else stxo.address
          end as address
     ,    stxo.fingerprint
  from (
    select  tx_out.tx_id as tx_out_id
        ,   tx_in.tx_in_id as tx_in_id
        ,   tx_out.address
        ,   tx_out.stake_address_id
        ,   multi_asset.fingerprint
    from tx_out
    join tx_in on tx_in.tx_out_id = tx_out.tx_id and tx_in.tx_out_index = tx_out.index
    join ma_tx_out on ma_tx_out.tx_out_id = tx_out.id
    join multi_asset on multi_asset.id = ma_tx_out.ident
    where tx_out.tx_id > (select tx_id from tx_id_limit)
      and multi_asset.policy = '\x4523c5e21d409b81c95b45b0aea275b8ea1406e6cafea5583b9f8a5f'
      and tx_in.id is not null
  ) as stxo
  left join stake_address on stake_address.id = stake_address_id

select  stxo_1.fingerprint
    ,   stxo_1.address as from_address
    ,   stxo_2.address as to_address
    ,   encode(tx.hash, 'hex') as tx_hash
from stxo as stxo_1
join stxo as stxo_2 on stxo_2.tx_out_id = stxo_1.tx_in_id
join tx on tx.id = stxo_1.tx_in_id
where stxo_1.fingerprint = stxo_2.fingerprint
  and stxo_1.address <> stxo_2.address

This will probably not work as intended for fungible assets. For that you have to treat the input and output addresses as sets (since there can be multiple addresses for a single asset) and then check that the input set and output set are different.

  • Very cool - I am going to try it out now! Thank you. First question: I keep the last tx.id processed, and use that as my constraint, but you have used time. Is there a specific reason you chose to use time from the block table? Mar 20 at 1:08
  • Nah I just use time so I have a better sense of how far back in the chain I'm going. tx_id would be more efficient here but is less ergonomic imo
    – adjuric
    Mar 20 at 22:16

This is a non-trivial task. You can't quickly fetch the latest tx that meets this criteria. Postgres will first need to compute whether the criteria has been met across all txs for that asset. Then it will order them and return the latest one.

You have a few options:

  1. You can improve performance by selecting a timeframe (subset of latest txs). You then have to balance the risk returning of no results with the speed of the query ie the longer the timeframe the slower the query.
  2. You can write a postgres function that will iterate a day at time in reverse order until it finds a matching tx. This may still be quite slow especially if you are trying to load results for a web page.
  3. You can create a materialized view that maps assets to txs if they meet this criteria ie a table with columns asset_id, tx_id, time. This is probably your only option if you are doing this for a user-facing app. You then backfill the table with your relevant data and need a way to update the table with the new data coming in.
  • Agreed, and in fact I do have a tx id I use for limiting the scope in time: and tx_id > {X} this does reduce the amount of work required, agreed! The thing I need help with most though is what is it in the schema/data that would definitively identify a given transaction as one where any asset of the given policy has changed ownership. Mar 19 at 2:25
  • Thanks btw @adjuric. For your #1: I do constrain by time, typically only querying for records of the last hour, constrained by using: and tx.id > X It is not for a web UI per-se, it is for extracting the transactions I am interested in, and then storing/updating data in a separate DB. The part I need help on is determining for certain how to identify that a tx is one where an asset of the given policy changed ownership. There is the asset being on an input UTXO on one address, and then on another as an output ... Mar 19 at 4:54

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.