Solana Account History

Introduction

When using our Solana API endpoints, it's important to understand how Solana token accounts work and how this affects fetching and parsing an account's transaction history.

In Solana, an account can own multiple sub-accounts (referred to as "token accounts" from here on), each associated with a particular token held by the main account.

To obtain a complete transaction history for a Solana account, you need to consider not only the main account but also all its associated token accounts, both active and closed. This guide explains how to achieve this using our API endpoints.

Understanding Token Accounts

The /splAccounts endpoint returns all active token accounts owned by the main account you are querying. However, due to limitations in Solana's underlying RPC infrastructure, it does not return token accounts that have been closed by the time of your query. This means that if your main account previously held tokens that it no longer holds, those closed token accounts will not be listed.

Fetching a Complete Transaction History

To fetch a fully complete list of transactions for a given account (including all associated token accounts, current and past), you need to follow these steps:

1. Retrieve Active Token Accounts

Call the /splAccounts endpoint with the main account address. This will return a list of all active associated token account addresses.

2. Initialize the Accounts List

Create a set that includes the main account address and all the associated token account addresses obtained from the previous step.

3. Iteratively Fetch Transactions

For each account in the set:

  • a. Call the /txs endpoint for that account and retrieve all transactions.
  • b. Keep a running tally of all transactions returned (you'll probably need to de-duplicate transactions later).
  • c. In each transaction, inspect the transfers array. For each transfer, examine the owner field of the from and to addresses.
  • d. If the owner address is one of the accounts in your set, add this new address to your set of accounts to fetch transactions for.

Example:

{
  "from": {  
    "name": null,  
    "address": "Gwk6eDbPWpw6YEfDvQ9KodGWghWnGdVMbkEdvWxRBtd",  
    "owner": {  
      "name": null,  
      "address": "2VoJ7iR1KSJ9dUTsN6Mt1EEVhbP13Zjk2EfVpJXTcLRN"  
    }  
}

In this example, if the owner address (2VoJ7...) is one of the addresses in your current set, you should add the associated address (Gwk6...) to your set of accounts to fetch transactions for.

4. Repeat the Process

Continue this process recursively until no new associated accounts are found. Once you've called txs for every account in the running tally, and have de-duped the list, you'll effectively have all relevant transactions in the history of your main account.

This will give you a result that reconciles, for accounting purposes.

Sample Code

Here's a sample Python script that demonstrates how to implement this logic:

import json
import requests


def get_spl_accounts(main_account, api_key):
    url = f"https://translate.noves.fi/svm/solana/splAccounts/{main_account}"
    headers = {"apiKey": api_key}
    response = requests.get(url, headers=headers)
    return response.json().get("accounts", [])


def get_transactions(account, api_key):
    url = f"https://translate.noves.fi/svm/solana/txs/{account}"
    headers = {"apiKey": api_key}
    transactions = []
    next_url = url
    while next_url:
        response = requests.get(next_url, headers=headers)
        response.raise_for_status()
        data = response.json()
        transactions.extend(data.get("items", []))
        next_url = data.get("nextPageUrl", "")
    return transactions


def find_associated_accounts(transactions, known_accounts):
    new_accounts = set()
    for tx in transactions:
        for transfer in tx.get("transfers", []):
            from_info = transfer.get("from", {})
            from_address = from_info.get("address")
            from_owner = from_info.get("owner", {}).get("address")

            to_info = transfer.get("to", {})
            to_address = to_info.get("address")
            to_owner = to_info.get("owner", {}).get("address")

            for address in [from_address, to_address, from_owner, to_owner]:
                if (
                    address
                    and address not in known_accounts
                    and any(known in [from_address, to_address, from_owner, to_owner] for known in known_accounts)
                ):
                    new_accounts.add(address)

    return new_accounts


def fetch_complete_history(main_account, api_key):
    known_accounts = set(get_spl_accounts(main_account, api_key))
    known_accounts.add(main_account)
    all_transactions = []
    accounts_to_process = known_accounts.copy()

    while accounts_to_process:
        account = accounts_to_process.pop()
        transactions = get_transactions(account, api_key)
        all_transactions.extend(transactions)
        new_accounts = find_associated_accounts(transactions, known_accounts)
        accounts_to_process.update(new_accounts - known_accounts)
        known_accounts.update(new_accounts)

    seen_tx_ids = set()
    unique_transactions = []
    for tx in all_transactions:
        tx_id = tx.get("rawTransactionData", {}).get("signature")
        if tx_id and tx_id not in seen_tx_ids:
            seen_tx_ids.add(tx_id)
            unique_transactions.append(tx)

    return unique_transactions


if __name__ == "__main__":
    api_key = ""  # ENTER API KEY HERE
    main_account = ""  # ENTER MAIN SOLANA ACCOUNT HERE
    transactions = fetch_complete_history(main_account, api_key)

    with open("transactions.json", "w") as f:
        json.dump(transactions, f)