CounterpartyXCP / counterparty-core

Counterparty Protocol Reference Implementation
http://counterparty.io
MIT License
287 stars 206 forks source link

Get UTXO attached assets by address #2705

Open warrenpuffett opened 2 days ago

warrenpuffett commented 2 days ago

@ouziel-slama Currently the only way to do this is with GET /addresses/balances?address={address}. This gives back an inconvenient shape that incurs a compute performance penalty when filtering O(n*m). Additionally it is bandwidth inefficient as many of the assets will not be UTXO attached.

Maybe an endpoint like GET /addresses/{address}/utxo-assets?

droplister commented 2 days ago

Here's how I approached this problem: {FC2C6236-45B2-447B-87A8-6237F092F82F} {73CC1EEE-88E9-4218-ADF5-DEBD77D5294F}

I've made it so you have to select an asset before you can compose a message, where you're shown the balance as the sum of utxo and non-utxo amounts. And then it you click into the send experience, you're only shown what's non-utxo. And if you want to interact with the utxo you click into that. This works pretty well.

And then in my application, I have:

/**
 * Fetches the balance of a specific token for a given address.
 *
 * @param address - The Bitcoin address to fetch the token balance for.
 * @param asset - The asset (token) name to fetch the balance of.
 * @param excludeUtxos - Whether to exclude balances associated with UTXOs (default is false).
 * @returns A promise that resolves to a TokenBalance object or null if not found.
 */
export async function fetchTokenBalance(
  address: string,
  asset: string,
  excludeUtxos: boolean = false
): Promise<TokenBalance | null> {
  try {
    const response = await axios.get(
      `/v2/addresses/${address}/balances/${asset}?verbose=true`
    );
    const data = response.data;

    if (!data.result) {
      return null;
    }

    if (Array.isArray(data.result) && data.result.length === 0) {
      // Return a zero balance if the asset is not held by the address.
      return {
        asset: asset,
        quantity: 0,
        quantity_normalized: '0',
        asset_info: {
          asset_longname: null,
          description: '',
          issuer: '',
          divisible: true,
          locked: false,
        },
      };
    }

    let balances = data.result;

    if (excludeUtxos) {
      // Filter out balances that have utxo information
      balances = balances.filter((balance: TokenBalance) => !balance.utxo);
    }

    if (balances.length === 0) {
      // After filtering, if no balances are left, return zero balance
      return {
        asset: asset,
        quantity: 0,
        quantity_normalized: '0',
        asset_info: {
          asset_longname: null,
          description: '',
          issuer: '',
          divisible: true,
          locked: false,
        },
      };
    }

    // Aggregate the quantities
    const totalQuantity = balances.reduce(
      (sum: number, entry: TokenBalance) => sum + (entry.quantity || 0),
      0
    );
    const totalQuantityNormalized = balances
      .reduce(
        (sum: number, entry: TokenBalance) =>
          sum + parseFloat(entry.quantity_normalized),
        0
      )
      .toString();

    const firstEntry = balances[0];
    return {
      asset: asset,
      quantity: totalQuantity,
      asset_info: firstEntry.asset_info,
      quantity_normalized: totalQuantityNormalized,
    };
  } catch (error) {
    console.error('Error fetching token balance:', error);
    return null;
  }
}
/**
 * Fetches token balances that are locked in UTXOs for a given address and asset.
 * 
 * @param address - The Bitcoin address to fetch UTXO balances for
 * @param asset - The asset name to fetch UTXOs for
 * @returns A promise that resolves to an array of TokenBalance objects with UTXO information
 */
export async function fetchTokenUtxos(
  address: string,
  asset: string
): Promise<TokenBalance[]> {
  try {
    const response = await axios.get(
      `/v2/addresses/${address}/balances/${asset}?verbose=true`
    );
    const data = response.data;

    if (!data.result || !Array.isArray(data.result)) {
      return [];
    }

    // Filter to only include balances with UTXO information
    return data.result.filter((balance: TokenBalance) => balance.utxo !== null);
  } catch (error) {
    console.error('Error fetching token UTXOs:', error);
    return [];
  }
}
adamkrellenstein commented 2 days ago

(I'd name the route /addresses/<address>/balances?type=utxo)