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:
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:
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:
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:
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:
{'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:
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:
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:
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.
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:
tx_confirmation = requests.get(LAM + f'/transactions/{tx_id}', headers=headers_ledger)
print(tx_confirmation.json())
which returns the following response:
{'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:
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.
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)