Stake ETH using Blockdaemon and Ledger Enterprise APIs

In this tutorial, we present how you can implement a fully automated and secure Ethereum staking workflow using Blockdaemon and Ledger Enterprise APIs.

For this tutorial, we will run these operations on the Goerli test network.

Requirements

You need to have access to both Blockdaemon and Ledger Enterprise APIs, especially you need to have API keys for both.

On Blockdaemon side:

  • Your workspace must be configured so that you can create staking intent via the Blockdaemon API.
  • You need an API key to be able to send requests. If you don't have a Blockdaemon API key, or your workspace is not properly configured, please contact your Blockdaemon account manager.

On Ledger Enterprise side:

  • You need to have access to your workspace's Ledger Authentication Module (LAM).
  • You need to have the necessary credentials, i.e. an API user account and corresponding API key (if API key is required).
  • The Ethereum account from which you will stake must have Smart Contract Interaction enabled.
  • Your API user must have authorization to create a smart contract interaction from this particular account.
  • For maximum security, you also want to whitelist the Blockdaemon staking contract address 0x6D144323aED2326255e9cE36a429ad737a1ccE37 (Goerli contract) on the corresponding Ledger Enterprise Ethereum account.

Below, we define the LAM address and API headers we will use to send API requests:

Copy
Copied
LAM = '<your_LAM_address>'

headers_ledger = {'X-Ledger-API-User': '<your_ledger_api_username>', 'X-Ledger-API-Key': '<your_ledger_api_key>'}
headers_blockdaemon = {'X-API-Key': '<your_blockdaemon_api_key>'}

Process

The staking process here is twofold:

  • The user must first create a staking intent using the Blockdaemon API which will return the unsigned staking data to be signed by the user wallet to fund the corresponding validator.
  • The user must then sign the staking transaction using the Ledger Enterprise API to fund the validator and complete the staking operation.

Get staking intent data from Blockdaemon API

To create a staking intent, we can query the endpoint /ethereum/prater/stake-intents from the Blockdaemon API.

The staking intent request must include, at least:

  • the amount of ETH to be staked (in Gwei).
  • the withdrawal_address : an execution layer address to which the initial deposit and rewards will be sent to during withdrawals.
    • For maximum security, you want this address to be one of your Ledger Enterprise Ethereum addresses, so that your withdrawal authority is always protected by Ledger's security and governance.

For more details and more options, please refer to the official Blockdaemon documentation.

In this tutorial, we will use the following Ledger Enterprise Ethereum account/address as both our funding address and withdrawal authority:

Copy
Copied
ledger_eth_account = 'ETH-Goerli-Staking'
ledger_eth_account_address = '0x128075552e4C6dC64Bca2Cf9ca46ee688629e4CD'

Let's first check that the account holds at least 32 ETH, and enough funds to pay for the staking transaction gas fees:

Copy
Copied
import requests

r = requests.get(LAM + '/accounts', headers=headers_ledger, params={'name': ledger_eth_account})

balance = int(r.json()['edges'][0]['node']['balance']) / 1e18
print(f'balance: {balance}')

which in this case returns:

balance: 33.16732738779311.

In this example, the account holds more than 33 ETH, which is more than enough for our staking transaction.

In the example below, we create a staking intent for a single block of 32 ETH, and with withdrawal_address set as our Ledger Enterprise Ethereum account address:

Copy
Copied
staking_request = {
    'stakes': [
        {
            'amount': '32000000000',  # 32 ETH in Gwei
            'withdrawal_address': ledger_eth_account_address,
        }
    ]
}

r = requests.post('https://svc.blockdaemon.com/boss/v1/ethereum/prater/stake-intents', 
                  headers=headers_blockdaemon, 
                  json=staking_request)

staking_intent = r.json()
print(staking_intent)

which returns the following response:

Copy
Copied
{'customer_id': 'LedgerStakingTest-39PlRRxPtr',
 'ethereum': {'contract_address': '0x6D144323aED2326255e9cE36a429ad737a1ccE37',
  'stakes': [{'amount': '32000000000',
    'stake_id': 'c2731fa6-be26-41d7-9ad1-b0bfad7b2007',
    'validator_public_key': '0xb0e519a2125c56171ab5c4a482a2eb2ead1f5889baec205671c304ef3a4e3f988d6ab35ef876a0adb7e75fcd63312a23',
    'withdrawal_credentials': '0x010000000000000000000000128075552e4C6dC64Bca2Cf9ca46ee688629e4CD'}],
  'unsigned_transaction': '0x592c0b7d0000000000000000000000000000000000000000000000000000000064b0ca3d000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000d0b0e519a2125c56171ab5c4a482a2eb2ead1f5889baec205671c304ef3a4e3f988d6ab35ef876a0adb7e75fcd63312a23010000000000000000000000128075552e4c6dc64bca2cf9ca46ee688629e4cdb03500acf3081bb3ab966f60e6980da63e780f27c35b5360e2a81a747beb2898f6fc2331cbc945a7adad5418801510e90272d59337ba4389b5ff96d543b82c60e55fb52f19a12943a94415e71e9d5244eff4487a2255d575d1f53a54977b1ce3ba5e9295836bf8e85d4d7725390b9d793f774f6afd5d78d8f89cb6b40effe99a00000000000000000000000000000000'},
 'network': 'prater',
 'protocol': 'ethereum',
 'stake_intent_id': '2990f94f-8b15-42c6-8911-938a015c9d5a'}

Blockdaemon API returns the staking intent data corresponding to our request.

Especially, it returns the raw unsigned transaction data unsigned_transaction which must be signed by the funding wallet.

Sign staking transaction with Ledger Enterprise API

Second step is to sign the previously generated staking transaction data using the Ledger Enterprise API.

We first prepare the transaction to be signed, which includes the staking intent unsigned data from above:

Copy
Copied
import eth_utils

contract_address = staking_intent['ethereum']['contract_address']  # Staking contract
data_to_sign = staking_intent['ethereum']['unsigned_transaction'][2:]  # Staking data to be signed

tx_data = {
    'account_name': ledger_eth_account,
    'amount': int(32 * 1e18),  # 32 ETH (expressed in wei)
    'coin_fields': {
        'contract_interaction': {
            'contract_data': data_to_sign
        }
    },
    'recipient': eth_utils.to_checksum_address(contract_address),
    'speed': 'NORMAL'
}

Before creating the transaction, we can estimate the transaction fees for that particular smart contract interaction using the /transactions/fees endpoint:

Copy
Copied
r = requests.post(LAM + '/transactions/fees', headers=headers_ledger, json=tx_data)

The max_fees estimated by the previous request can be attached to the transaction object before being sent to the LAM:

Copy
Copied
tx_data['max_fees'] = r.json()['max_fees']

We then execute a POST /transactions request to sign the staking transaction and fund the validator. The response will include the corresponding transaction id from the Vault.

Copy
Copied
tx = requests.post(LAM + '/transactions', headers=headers_ledger, json=tx_data)

tx_id = tx.json()['id']

If the account governance requires multiple approvals, the transaction will need to be approved by the right quorum before it is signed and broadcasted.

In this example, the account governance is set so that the transaction does not require additional approvals to be signed and broadcasted.

Once the transaction is signed, we can get the transaction confirmation and hash from the Vault:

Copy
Copied
tx_confirmation = requests.get(LAM + f'/transactions/{tx_id}', headers=headers_ledger)

print(tx_confirmation.json())

which returns the following response:

Copy
Copied
{'account_id': 61,
 'account_index': 7,
 'amount': '32000000000000000000',
 'block': {'hash': '0x6817e4ef65c3d78377d8998cf0a33d0fa56dd5dc09b05e0ca04a575b2bf1c57f',
  'height': 8826828,
  'time': '2023-04-14T04:09:36+00:00'},
 'broadcast_on': '2023-04-14T04:09:27.025867+00:00',
 'coin_fields': {'gas_limit': '106106',
  'gas_price': '18578275606',
  'contract_interaction': {'contract_data': '592c0b7d0000000000000000000000000000000000000000000000000000000064b0ca3d000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000d0b0e519a2125c56171ab5c4a482a2eb2ead1f5889baec205671c304ef3a4e3f988d6ab35ef876a0adb7e75fcd63312a23010000000000000000000000128075552e4c6dc64bca2cf9ca46ee688629e4cdb03500acf3081bb3ab966f60e6980da63e780f27c35b5360e2a81a747beb2898f6fc2331cbc945a7adad5418801510e90272d59337ba4389b5ff96d543b82c60e55fb52f19a12943a94415e71e9d5244eff4487a2255d575d1f53a54977b1ce3ba5e9295836bf8e85d4d7725390b9d793f774f6afd5d78d8f89cb6b40effe99a00000000000000000000000000000000',
   'contract_name': None,
   'dapp': None,
   'function_arguments': {'args': 'b0e519a2125c56171ab5c4a482a2eb2ead1f5889baec205671c304ef3a4e3f988d6ab35ef876a0adb7e75fcd63312a23010000000000000000000000128075552e4c6dc64bca2cf9ca46ee688629e4cdb03500acf3081bb3ab966f60e6980da63e780f27c35b5360e2a81a747beb2898f6fc2331cbc945a7adad5418801510e90272d59337ba4389b5ff96d543b82c60e55fb52f19a12943a94415e71e9d5244eff4487a2255d575d1f53a54977b1ce3ba5e9295836bf8e85d4d7725390b9d793f774f6afd5d78d8f89cb6b40effe99a',
    'validUntil': 1689307709},
   'function_name': 'batchDeposit',
   'smart_contract_interaction_type': 'UNKNOWN'},
  'type': 'EthereumAndEvm'},
 'confirmations': 20,
 'created_by': 18,
 'created_on': '2023-04-14T04:09:11.016157+00:00',
 'currency': 'ethereum_goerli',
 'fees': '1154788455117748',
 'id': 1065,
 'interaction_type': 'UNKNOWN',
 'last_request': 870,
 'max_fees': '1994995193162972',
 'metadata': None,
 'min_confirmations': 30,
 'notes': [{'content': '', 'title': ''}],
 'recipient': '0x6D144323aED2326255e9cE36a429ad737a1ccE37',
 'senders': ['0x128075552e4C6dC64Bca2Cf9ca46ee688629e4CD'],
 'speed': 'NORMAL',
 'status': 'SUBMITTED',
 'tx_hash': '0x6854b456f04d88db6cf96fcfab13633397371b91d7d24aaf175366dcdc1a64dc',
 'type': 'SEND',
 'uid': 'f6859e5b4a06063201609bafa23a048215189d216f90d4924c1fa44ac0d073d2'}

We can double check the transaction on chain using the transaction hash:

Copy
Copied
tx_hash = tx_confirmation.json()['tx_hash']

print(f'Tx on explorer: https://goerli.etherscan.io/tx/{tx_hash}')

Tx on explorer: https://goerli.etherscan.io/tx/0x6854b456f04d88db6cf96fcfab13633397371b91d7d24aaf175366dcdc1a64dc

Exiting

To exit and withdraw your staking deposit, you can simply call the /ethereum/prater/voluntary-exit endpoint on the Blockdaemon API and provide your validator public key.

For more information, check the official Blockdaemon API documentation.

Copy
Copied
exit_request = {
    'validator_identifier': {
        'validator_public_key': '0xaa5dd7ba7aa23bbfcb033667a383c6a50608a52439d8ef30f039af35469426517ec057a1c45af63aa1c596a722220cbe',
    }
}

r = requests.post('https://svc.blockdaemon.com/boss/v1/ethereum/prater/voluntary-exit', 
                  headers=headers_blockdaemon, 
                  json=exit_request)

exit = r.json()
print(exit)
Copyright © Ledger Enterprise Platform 2023. All right reserved.