Generate a report
In this tutorial we present how to generate a report using the Ledger Enterprise API.
We explain how to retrieve information about your accounts including addresses, currencies and balances and how to generate a report from it.
Requirements
Before we start, you need to have access to your workspace's Ledger Authentication Module (LAM) and have the necessary credentials, i.e. an API account and API key (if applicable).
To generate a global report, your API account must also have a VIEW ALL
permission to be able to fetch data from all the accounts on the workspace.
Below we define the LAM address and headers we will use to send API requests:
LAM = '<your_LAM_address>'
headers = {'X-Ledger-API-User': '<your_api_username>', 'X-Ledger-API-Key': '<your_api_key>'}
Get list of active accounts
The first step will be to fetch the list of all the active accounts on your workspace. For that, we can execute a GET /accounts
request, which will return the list of accounts.
If the list of accounts does not fit in a single response page, one needs to iterate over the pages to get the full list. See the pagination section of the API documentation.
The function defined below automatically iterates over the pages of the GET /accounts
request, and builds the list of all ACTIVE
accounts in the current workspace:
import requests
def get_all_accounts(page_size=30):
active_accounts = []
page = 1
has_next_page = True
while has_next_page:
r = requests.get(LAM + '/accounts', headers=headers, params={'page': page,
'page_size': page_size,
'status': 'ACTIVE'})
accounts = r.json()['edges']
active_accounts += [a['node'] for a in accounts]
has_next_page = r.json()['page_info']['has_next_page']
page += 1
return active_accounts
Using this function, we fetch the list of all the accounts on our workspace:
accounts = get_all_accounts(page_size=30)
The list we obtain provides enough information to build a basic report which would include the account names, addresses and raw balances.
But it misses some information such as the corresponding token names, tickers and numbers of decimals.
In the next section, we build a dictionary of currencies and tokens to further refine our report content.
Build currencies and tokens dictionary
To get more information regarding the different currencies and tokens of our accounts, we can query the /currencies
and /currencies/tokens
endpoints, which will return the complete lists of currencies and tokens supported on the Ledger Enterprise platform.
Let's first get the list of all native currencies:
r = requests.get(LAM + '/currencies', headers=headers)
From the API response, we extract the currency names, tickers and corresponding numbers of decimals, and gather this data in a dictionary which we will use later to build our report. In that case, the dictionary keys are simply the currency/chain names:
currencies_list = r.json()['currencies']
currencies = {currency['name']: {'name': currency['name'].replace('_', ' ').capitalize(),
'ticker': currency['units'][-1]['code'],
'decimals': currency['units'][-1]['magnitude']}
for currency in currencies_list}
which returns a dictionary as below:
{'bitcoin': {'name': 'Bitcoin', 'ticker': 'BTC', 'decimals': 8},
'bitcoin_cash': {'name': 'Bitcoin cash', 'ticker': 'BCH', 'decimals': 8},
'bitcoin_gold': {'name': 'Bitcoin gold', 'ticker': 'BTG', 'decimals': 8},
'bitcoin_testnet': {'name': 'Bitcoin testnet', 'ticker': 'tBTC', 'decimals': 8},
...
'polygon': {'name': 'Polygon', 'ticker': 'MATIC', 'decimals': 18},
'ripple': {'name': 'Ripple', 'ticker': 'XRP', 'decimals': 6},
'solana': {'name': 'Solana', 'ticker': 'SOL', 'decimals': 9},
...
}
We now do the same for the list of ERC20/BEP20 tokens supported on the platform. This time, we build a dictionary of tokens where keys are defined as a tuples (token_address, token_chain)
:
r = requests.get(LAM + '/currencies/tokens', headers=headers)
tokens_list = r.json()['tokens']
tokens = {(token['contract_address'].lower(), token['parent_currency']):
{'name': token['name'],
'ticker': token['units'][0]['code'],
'decimals': token['units'][0]['magnitude']}
for token in tokens_list}
Finally, we merge the token dictionary with the previously built currency dictionary to get a single dictionary gathering all the useful information about native currencies and tokens:
currencies.update(tokens)
Create report
Finally, we combine the list of accounts we fetched in the first section with the currencies and tokens information we generated in the second section to build our report.
To build the report, we iterate over the list of accounts and apply the following process:
- First, we exclude accounts which we don't want to be added in the report. In this example, we exclude Ethereum PoW and Ethereum Ropsten accounts from the report.
-
For each UTXO-based account, we define the account
address
as its xpub. For other accounts, we simply use their blockchain addresses. - For each account, we fetch the corresponding currency or token information, including the currency/token's name, ticker and number of decimals.
- For each account, we adjust the account's raw balance based on the number of decimals to get the account balance expressed in the currency/token main unit.
-
Finally, for each account we record the following fields which we want to be part of the report:
-
chain
: the chain/network on which the corresponding account lives. -
currency
: the name of the account's currency/token. -
ticker
: the ticker/code of the account's currency/token. -
id
: the unique id of the account on the Ledger Enterprise platform. -
name
: the account's name on the Ledger Enterprise platform. -
balance
: the account's balance, expressed in the main currency/token unit. -
address
: the account's address or xpub.
-
To build the report, we use pandas
and create a DataFrame
out of the account fields we recorded in the loop:
import pandas as pd
report = []
for account in accounts:
account_chain = account['currency']
if account_chain not in ['ethereum_pow', 'ethereum_ropsten']:
if account['address'] is None: # If UTXO-based currency
account_address = account['xpub']
else:
account_address = account['address']
if account['contract_address'] is not None: # If token
currency_key = (account['contract_address'].lower(), account_chain)
else: # If native currency
currency_key = account_chain
currency_name = currencies[currency_key]['name']
currency_decimals = currencies[currency_key]['decimals']
currency_ticker = currencies[currency_key]['ticker']
balance = round(int(account['balance']) / pow(10, currency_decimals), 2)
report.append({'chain': account_chain,
'currency': currency_name,
'ticker': currency_ticker,
'id': account['id'],
'name': account['name'],
'balance': str(balance) + ' ' + currency_ticker,
'address': account_address})
report_df = pd.DataFrame(report)
report_df.sort_values(by=['id'], inplace=True, ignore_index=True)
The report can easily be saved as a csv or excel file using .to_csv()
or .to_excel()
methods.
report_df.to_csv('ledger-enterprise-report.csv')
The report can also be converted into markdown format using:
report_df.to_markdown(index=False)
which renders as follows on this tutorial page:
chain | currency | ticker | id | name | balance | address |
---|---|---|---|---|---|---|
bitcoin_testnet | Bitcoin testnet | tBTC | 6 | Reserve-BTC | 0.02 tBTC | tpubDCs9Krqrkuey5sZvF5yqpcVTSQWvqdgqf9wYodECWZ1W7Fo2Dm6wXYLMGJjzxTmxUtgeCkg7FTxtL6C6mbe6LEDhVX9aqy26nfNKVHVF9BA |
bitcoin_testnet | Bitcoin testnet | tBTC | 7 | Liquidity-BTC | 0.01 tBTC | tpubDDko6ESpH2GUQfBQ35Hrn3jVnmvrJk6AuBZZpdzZ63Ukgi3RPnbSd6cD9QiMHuyH6keLxa3GrPMtjVF5dEgnow8jXZKQzqw9V2buPGk8s5V |
ethereum | Ethereum | ETH | 10 | Ethereum-SCI-NFT | 0.07 ETH | 0xB6b171F2E253cD991eB98091ee8C258753083C5C |
ethereum | TrueUSD | TUSD | 11 | TUSD | 1.0 TUSD | 0xB6b171F2E253cD991eB98091ee8C258753083C5C |
tezos | Tezos | XTZ | 15 | Tezos1 | 0.72 XTZ | tz2CVMDVA16dD9A7kpWym2ptGDhs5zUhwWXr |
polkadot | Polkadot | DOT | 17 | DOT1 | 22.36 DOT | 14WUAWaLejoiWPurTgdhsQ7qLpQLdBqUtTfUFrnZ8BJG9eAn |
bitcoin | Bitcoin | BTC | 20 | ClientA-BTC | 0.0 BTC | xpub6DHkDqUp6Js1d4gs4gmnAP5uUsKV14nP1wodXfiga8JzTzkw6U3Lfq7moGsnJxGoR1XNP68oBWswvQ6d89oLEAZNpe81g5euWkFXUDX2jvW |
bitcoin_testnet | Bitcoin testnet | tBTC | 27 | bitcoin-testnet3 | 0.0 tBTC | tpubDCTf2NtMVx6N7s8nxQ9cVFSG7ByJpX6o12SBQUDFGfNzYefXDnZMHGxkViuBqE5xEsVE8QDQshuBGigxdiXyB29JQSoE7pAvrULLhqaZVpG |
bitcoin_testnet | Bitcoin testnet | tBTC | 28 | ClientA-BTC-Testnet | 0.0 tBTC | tpubDDTePbrCyqaTtxtQFkm8cwPmRPz1kEy5cMp5SK5hR48DnHr6Q7kJTcLoqN6AYHVxXFKATpyhin4UmH4qxWXsxLtsoJLE9cRKbAdc3A3p3pu |
ethereum | Ethereum | ETH | 29 | Liquidity-Ethereum | 0.0 ETH | 0x95dBFF72d215e4a0834Aed61A189882876116751 |
ethereum | Ethereum | ETH | 31 | Reserve-Ethereum | 0.0 ETH | 0x630D83b5Db5b8216bd2E13D5b3d2f89b845f304e |
ethereum | Tether USD | USDT | 32 | Reserve-USDT | 7.0 USDT | 0x630D83b5Db5b8216bd2E13D5b3d2f89b845f304e |
ethereum | Ethereum | ETH | 33 | Ethereum-Test | 0.0 ETH | 0xD5882DeCf61E23Db16bfbe036CE9d9707F1e7d40 |
bitcoin_testnet | Bitcoin testnet | tBTC | 34 | UFC-Athlete-BTC | 0.0 tBTC | tpubDCXWunXGeWzWSMu8xa7HqdUtgJbNsBsMVFrCGoLGkStyQLXVt7LuZj23zdFsTxrYxrdk1Ni44W8FRWbw3n6DEgVoYfaMNaHYST6f63S55LX |
bitcoin_testnet | Bitcoin testnet | tBTC | 35 | UFC-Agent-BTC | 0.0 tBTC | tpubDDCGcfmnL3P5Nhpqz5kLJD3BpGk7x9xyyPiVdaoxWxgoq8zYRYSCxwfGyF9BWURivFWs3bZQ1UZHtNTPp53xVgQi39NpM5UgtoYYJNqDEL9 |
bitcoin_testnet | Bitcoin testnet | tBTC | 36 | UFC-BlueChip-BTC | 0.0 tBTC | tpubDDYj7naXAg3foHuTxEk8Ugwsi3BRZ9kt8hFY3E2Q4M94nj74KJrToyXno34nc6FbV4Jtbym8ptRFacpAqghgyw6EU5G8DB3d1LhPPo1dW8R |
ethereum | Tether USD | USDT | 37 | Liquidity-USDT | 0.09 USDT | 0x95dBFF72d215e4a0834Aed61A189882876116751 |
polygon | Polygon | MATIC | 38 | Polygon-SCI-NFT | 0.0 MATIC | 0xB6b171F2E253cD991eB98091ee8C258753083C5C |
polygon | Polygon | MATIC | 39 | Liquidity-Polygon | 1.0 MATIC | 0x95dBFF72d215e4a0834Aed61A189882876116751 |
polygon | Matic Token | MATIC | 40 | Liquidity-MATIC | 1.0 MATIC | 0x95dBFF72d215e4a0834Aed61A189882876116751 |
polygon | Wrapped Ether | WETH | 41 | Liquidity-wETH | 0.0 WETH | 0x95dBFF72d215e4a0834Aed61A189882876116751 |
digibyte | Digibyte | DGB | 42 | DGB | 167.57 DGB | xpub6CAYMUHEKiJogZYC6Ssk3c8qt9ZpTP2LxTyPAXJatejanXrXJGnFw8kj2hjGySkjpcTygfTZXJDX4v7YX6bQ8z4QFX4ZNsFtjCK6nM4aw4f |
digibyte | Digibyte | DGB | 43 | DGB2 | 37.3 DGB | xpub6CA4kXVgZaY2YLt1gAeApqvW9HCcDKjG6JyZNbLVbjhryLw1pSdnbT5wvgGjvJV4mjKu5tDxm76FFHvDERXatQZz9eLfz2B9xNHiKEbc4jK |
ethereum_goerli | Ethereum goerli | gETH | 44 | ETH-RESERVE | 0.22 gETH | 0x2CE3895Fa3ad9a4EAcC37feD6ac8c35e5AE8c5cF |
ethereum_goerli | USDC | USDC | 46 | USDC-RESERVE | 0.9 USDC | 0x2CE3895Fa3ad9a4EAcC37feD6ac8c35e5AE8c5cF |
solana_devnet | Solana devnet | tSOL | 47 | SOL-DEV | 11.23 tSOL | fZ3im6BP7jpaqYBA4cBAtZrPKsT3PR3d8fwDyHnyh1e |
ethereum | Tether USD | USDT | 48 | SCI-USDT | 0.1 USDT | 0xB6b171F2E253cD991eB98091ee8C258753083C5C |
polygon | USD Coin (PoS) | USDC | 51 | Liquidity-USDC | 0.25 USDC | 0x95dBFF72d215e4a0834Aed61A189882876116751 |
solana | Solana | SOL | 55 | SOL1 | 0.15 SOL | fZ3im6BP7jpaqYBA4cBAtZrPKsT3PR3d8fwDyHnyh1e |
polygon | SAND | SAND | 56 | Liquidity-SAND | 0.0 SAND | 0x95dBFF72d215e4a0834Aed61A189882876116751 |
polygon | Polygon | MATIC | 58 | Reserve-Polygon | 0.0 MATIC | 0x630D83b5Db5b8216bd2E13D5b3d2f89b845f304e |
ethereum_goerli | Ethereum goerli | gETH | 61 | ETH-Goerli-Staking | 0.16 gETH | 0x128075552e4C6dC64Bca2Cf9ca46ee688629e4CD |
ethereum | Matic | MATIC | 64 | Ethereum-SCI-MATIC | 1.2 MATIC | 0xB6b171F2E253cD991eB98091ee8C258753083C5C |
bsc | Bsc | BNB | 65 | Liquidity-BSC | 0.0 BNB | 0x95dBFF72d215e4a0834Aed61A189882876116751 |
polygon | USD Coin (PoS) | USDC | 68 | account58_0x2791Bca | 0.01 USDC | 0x630D83b5Db5b8216bd2E13D5b3d2f89b845f304e |
Bonus - Send report by email
As a bonus, we show how to send the report by email.
To do that, one option is to convert the report DataFrame
to a HTML table which we can then include in an email.
from pretty_html_table import build_table
report_html = build_table(report_df, color='blue_light', text_align='center', escape=False)
In the example below, we send an email containing the report as a HTML table, using smtplib
and via a properly configured Gmail account.
The gmail account must be configured to authorize such operation, especially a specific email App password must be set up.
import smtplib, ssl
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
sender_email = '<sender_gmail_address>'
receiver_email = '<receiver_email_address>'
password = '<gmail_app_password>'
# Build email object
message = MIMEMultipart()
message['Subject'] = "Ledger Vault - Daily balances report"
message['From'] = sender_email
message['To'] = receiver_email
message.attach(MIMEText(report_html, 'html'))
# Create secure connection with server and send email
context = ssl.create_default_context()
with smtplib.SMTP_SSL('smtp.gmail.com', 465, context=context) as server:
server.login(sender_email, password)
server.sendmail(from_addr=sender_email, to_addrs=receiver_email, msg=message.as_string())