# Stake ETH using P2P.ORG and Ledger Enterprise APIs In this tutorial, we present how you can implement a fully automated and secure Ethereum staking workflow using P2P.ORG and Ledger Enterprise APIs. For this tutorial, we will run these operations on the Goerli test network. ## Requirements You need to have access and valid API keys to both [P2P.ORG](https://docs.p2p.org/docs/overview-eth) and Ledger Enterprise APIs. On P2P.ORG side: * You need an API key to be able to send requests. If you don't have a P2P.ORG API key, or your workspace is not properly configured, please contact your P2P.ORG account manager. Click [here](https://docs.p2p.org/docs/authentication) to see details. 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. * 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 P2P.ORG staking contract address 0x2E0743aAAB3118945564b715598B7DF10e083dC1 (Goerli contract) on the corresponding Ledger Enterprise Ethereum account. Below, we define the LAM address and 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_p2p = {'Authorization': 'Bearer <your_p2p_api_key>', 'accept: application/json', 'content-type: application/json'} ``` ## Process The staking process here is twofold: * The user must first initiate a staking intent using the [P2P.ORG API](https://docs.p2p.org) 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. ## Provision validators via P2P.ORG API To begin the staking flow, you need to provision validators by calling the following POST endpoint `https://api.p2p.org/api/v1/eth/staking/direct/nodes-request/create` on [p2p.org API](https://docs.p2p.org/docs/staking-eth) > Use corresponding endpoints: `api.p2p.org` for mainnet stake or `api-test.p2p.org` for Goerli. > You need to specify the number of validators that you wish to stake on (max. 400 per request), as well as the withdrawal credentials (your Ledger Enteprise ETH address to which the initial deposit and rewards will be sent to during withdrawals) Get provisioning status by calling `/api/v1/eth/staking/direct/nodes-request/status/` which returns for each validator: * `pubkey` — validator public key * `signature` — validator signature * `depositDataRoot` — hash of the deposit data ## Get staking intent data from P2P.ORG API To create a staking intent, we can query the endpoint `/api/v1/eth/staking/direct/tx/deposit` from the [P2P.ORG API](https://https://docs.p2p.org/docs/staking-eth) The staking intent request must include: `withdrawalAddress` — withdrawal address for all the validators And for each validator: `pubkey` — validator public key `signature` — validator signature `depositDataRoot` — hash of the deposit data ``` data = { 'withdrawalAddress': '<your_ledger_eth_address>', "depositData": [ { "pubkey": "0xac1e9969d7b87f3102549ab41558136674a7306b85b9f73cfbd7d9fdb7db85724569da3ebd4d7de9689f6ac058d7e2a3", "signature": "0xb656f9c771166c82a7891b930e6a920878d9736eb3f9f241753a15ea69d8e2f20a3740dfaf546c70e31bd323e14b341205d04e3227dd4cf2923644a375f6792875ac02c5f256f7a17c96b09bafcbce7e4443e1862356b1e90d78875d78e9a742", "depositDataRoot": "0xba013b4950b9aff0c3c19017ec5b6e0ed5b957b36f6ff03a545e5cc5605baff8" } ] } ``` The result is `serializeTx` — serialized unsigned transaction. For more details and more options, please refer to the official [P2P.ORG documentation](https://docs.p2p.org). 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 batch of 32 ETH, and with withdrawal_address set as our Ledger Enterprise Ethereum account address: ``` import requests import uuid url = "https://api-test.p2p.org/api/v1/eth/staking/direct/nodes-request/create" flow_id = str(uuid.uuid4()) data = { "id": flow_id, "validatorsCount": 1, "withdrawalAddress": ledger_eth_account_address, "feeRecipientAddress": ledger_eth_account_address, } response = requests.post(url, headers=headers_p2p, json=data) print(response.json()) ``` which returns the following response: ``` { "result": true } ``` This means that validators deployment process is started. Spinning up validators requires some time, so we need to check the status of the infrastructure provisioning until the validators are ready. We can do this by sending a GET request to /api/v1/eth/staking/direct/nodes-request/status/{id}. ``` url = f"https://api.p2p.org/api/v1/eth/staking/direct/nodes-request/status/{flow_id}" response = requests.get(url, headers=p2p_headers) print(response.json()) ``` which returns the following response: ``` { "result": { "id": "3611b95c-e1b3-40c0-9086-3de0a4379943", "status": "ready", "validatorsCount": 1, "withdrawalAddress": "0x128075552e4C6dC64Bca2Cf9ca46ee688629e4CD", "controllerAddress": "0x128075552e4C6dC64Bca2Cf9ca46ee688629e4CD", "feeRecipientAddress": "0x128075552e4C6dC64Bca2Cf9ca46ee688629e4CD", "depositData": [ { "pubkey": "0xac1e9969d7b87f3102549ab41558136674a7306b85b9f73cfbd7d9fdb7db85724569da3ebd4d7de9689f6ac058d7e2a3", "signature": "0xb656f9c771166c82a7891b930e6a920878d9736eb3f9f241753a15ea69d8e2f20a3740dfaf546c70e31bd323e14b341205d04e3227dd4cf2923644a375f6792875ac02c5f256f7a17c96b09bafcbce7e4443e1862356b1e90d78875d78e9a742", "depositDataRoot": "0xba013b4950b9aff0c3c19017ec5b6e0ed5b957b36f6ff03a545e5cc5605baff8" } ] } } ``` This reflects the status and data for each validator, such as `pubkey`, `signature`, and `depositDataRoot`, which is a hash of the deposit data. ### Prepare for signing First, we need to request the serialized transaction data for the specific validators by calling the /api/v1/eth/staking/direct/tx/deposit method. The data should be signed after it is obtained. ``` url = "https://api-test.p2p.org/api/v1/eth/staking/direct/tx/deposit" data = { "withdrawalAddress": ledger_eth_account_address, "depositData": [ { "pubkey": "0xac1e9969d7b87f3102549ab41558136674a7306b85b9f73cfbd7d9fdb7db85724569da3ebd4d7de9689f6ac058d7e2a3", "signature": "0xb656f9c771166c82a7891b930e6a920878d9736eb3f9f241753a15ea69d8e2f20a3740dfaf546c70e31bd323e14b341205d04e3227dd4cf2923644a375f6792875ac02c5f256f7a17c96b09bafcbce7e4443e1862356b1e90d78875d78e9a742", "depositDataRoot": "0xba013b4950b9aff0c3c19017ec5b6e0ed5b957b36f6ff03a545e5cc5605baff8" } ] } response = requests.post(url, headers=headers_p2p, json=data) staking_intent = response.json() print(staking_intent.json()) ``` which returns: ``` { "result": { "serializeTx": "0x02f902d705808301674e8508530af16e830186a094681a1b3441c6bfb12f91651efd9f02c83c0702938901bc16d674ec800000b902a44f498c730000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000012000000000000000000000000000000000000000000000000000000000000001a00000000000000000000000000000000000000000000000000000000000000260000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000030aa5f27070a21d79455c4a9b73c0aa4a8b1a65a1fb530d7fd8e6cd23aa16660679ac43ee4861098f6d9166aed3a4d8abb0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000002001000000000000000000000028c84612d37de9209018ad96167f12169b653e9a000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000060978c565cd915f4e885b4201093d1501697610eb9ee99b9b60b70434dc330e98d5b42927725304ded48483a8b8f39506d09bcb22ee18d4f6b50257946ac5ee360385308d95c0e2bc963902d42e985c29ee489aa3c989ac1561c952a6424f107a800000000000000000000000000000000000000000000000000000000000000014cb452f6e3f10ba2175c86a0284f53fcb61404b458393391abc3d5622e3e55cdc0", "to": "0x128075552e4C6dC64Bca2Cf9ca46ee688629e4CD", "gasLimit": "", "data": "", "value": "", "chainId": "", "type": "", "maxFeePerGas": "", "maxPriorityFeePerGas" : "" } } ``` P2P.ORG API returns the staking intent data corresponding to our request. Especially, it returns the `serializeTx` data 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['result']['serializeTx']['to'] # Staking contract data_to_sign = staking_intent['result']['serializeTx'][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. ``` {'account_id': 61, 'account_index': 7, 'amount': '32000000000000000000', 'block': None, 'broadcast_on': None, 'coin_fields': {'gas_limit': '121431', 'gas_price': '2282039876', 'contract_interaction': {'contract_data': '4f498c730000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000012000000000000000000000000000000000000000000000000000000000000001a00000000000000000000000000000000000000000000000000000000000000260000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000030956dfb1ce263163eeefc67d9aa4e7d4957a1e807a97b128a35ce68ccded88c61b7f6a7214cd4301285e83d6bdb2466b100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000020010000000000000000000000128075552e4c6dc64bca2cf9ca46ee688629e4cd000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000060aa641438bd72c2a0adb6f0762e2149c2f3ef4e4e4d4e0813494d5c032144646feb421feb574f35e769e2e4f67a0e98150285d45043c63d467c2c7c76526e8ffae21c5fa8be6ed5520e14e220652715d9d10d94f9146b3f37e180148e59cc79930000000000000000000000000000000000000000000000000000000000000001fdd510180769ce959fe7c38a0b606577cb9b17f497a4cc722080526447fa2860', 'contract_name': None, 'dapp': None, 'function_arguments': {'deposit_data_roots': ['fdd510180769ce959fe7c38a0b606577cb9b17f497a4cc722080526447fa2860'], 'pubkeys': ['956dfb1ce263163eeefc67d9aa4e7d4957a1e807a97b128a35ce68ccded88c61b7f6a7214cd4301285e83d6bdb2466b1'], 'signatures': ['aa641438bd72c2a0adb6f0762e2149c2f3ef4e4e4d4e0813494d5c032144646feb421feb574f35e769e2e4f67a0e98150285d45043c63d467c2c7c76526e8ffae21c5fa8be6ed5520e14e220652715d9d10d94f9146b3f37e180148e59cc7993'], 'withdrawal_credentials': ['010000000000000000000000128075552e4c6dc64bca2cf9ca46ee688629e4cd']}, 'function_name': 'deposit', 'smart_contract_interaction_type': 'UNKNOWN'}, 'type': 'EthereumAndEvm'}, 'confirmations': 0, 'created_by': 18, 'created_on': '2023-04-17T03:21:31.315245+00:00', 'currency': 'ethereum_goerli', 'fees': None, 'id': 1076, 'interaction_type': 'UNKNOWN', 'last_request': 881, 'max_fees': '277184355333378', 'metadata': None, 'min_confirmations': 30, 'notes': [{'content': '', 'title': ''}], 'recipient': '0x94a2da805867c962148d3832d9afc512034a62c5', 'senders': None, 'speed': 'NORMAL', 'status': 'APPROVED', 'tx_hash': None, 'type': 'SEND', 'uid': None} ``` 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}') ```