Stake Solana Tutorial
Requirements :
- Ledger Enteprise with the Solana Release
- LAM setup
- Admin access to the Ledger Enteprise Application
How to start :
- Create your API users
- Create a SOLANA Account as Reserve Cold Storage
- Create a SOLOANA Account to Perform Staking operations
- Add API user to the SOLANA Account
import requests
import json
CONSTANTES
LAM_URL = "https://split-merge-sol.minivault.ledger-sbx.com/lam"
API_USER_NAME = "bluestar"
HEADER = {'X-Ledger-API-User': API_USER_NAME}
Test LE-API connection
To see if your can reach your LAM URL.
You should get an API call code : 400, and the LAM should not know your user for now.
r = requests.get(LAM_URL + "/_health", headers=HEADER)
r.json()
MANDATORY - Create your LAM Operator
You will need to login as an Admin using the Ledger PSD to perfom this action since we will add a new operator on your HSM workspace.
This user can then be managed as any operator and can be added in your governance rules.
Login as Admin and go to the user section
Click Invite User
Select API and insert username and userId
Inside this section set the username as API_USER_NAME
for the constant section
Generate a User ID via API on your LAM via the /api_users endpoint
payload = {
"name": API_USER_NAME
}
r = requests.post(LAM_URL + "/api_users", json=payload, headers=HEADER)
r.json()
You should get this type of response :
{
'device_id': '6832c1a93806fca9',
'name': 'redstar',
'role': 'OPERATOR',
'type_': 'SOFT_PSD',
'workspace': 'your_workspace_name'
}
Use this your generated device Id to create the operator in the web application
For example 6832c1a93806fca9
need to be added under the User Id Section
Then register your operator via API
Copy past the UUID generated in by the web application 8436297b-962d-4e43-8a58-07b9ce84c1bc
Once done you should see {'success': True}
uuid = "97f90489-2b65-437c-9bf6-09ecd27e1723"
payload = {
"name": API_USER_NAME
}
r = requests.post(LAM_URL + "/api_users/" + API_USER_NAME + "/register/" + uuid , json=payload, headers=HEADER)
r.json()
Login as administrators to approve the API operator
Once approve by the quorum you can use the API operator and see that he is registered in your workspace
Extra security and Read only rights
You can check more information on the API operator setup the help center. You can directly ask your Technical Account Managers
for help on the topic.
You can create as many API operators as you need, and your can add custom permission like view all
for reporting purpose.
BASIC - Accounts & Transactions
In this section we will learn how to get accounts, and balances, how to perform send operations and how you can trace the status of a transaction.
First, create solana account and add user to the transaction and staking rules
To create an account please reffer to the help center.
Add your API user to the transaction rules for him to perform operations
Get your accounts
To test if everything is setup the right way, you can fetch your accounts.
r = requests.get(LAM_URL + "/accounts" , headers=HEADER)
r.json()
Send Assets to other account with /transaction
You can read more on the api documentation to learn everything about transactions.
You can find strange that we are using account_name
as unique identifier of the account, on LE-HSM we use a unique couple of ids and string to identify HSM objects, everything is recheck by the HSM to prevent any errors of miss management of funds.
recipient_address = "67Ge4jcScdZih5mi4NxpshxAptB2M6XzLD1i1vVAftgi"
payload = {
"speed": "NORMAL",
"amount": {
"value": 1,
"unit": "tSOL"
},
"max_fees": {
"value": 0.000101,
"unit": "tSOL"
},
"recipient": recipient_address,
"account_name": "Solana Reserve",
"coin_fields": {
"type": "SEND"
}
}
r = requests.post(LAM_URL + "/transactions", json=payload, headers=HEADER)
r.json()
Then you should received this type of response :
{'account_id': 5,
'account_index': 1,
'amount': '101000',
'broadcast_on': None,
'coin_fields': None,
'confirmations': 0,
'created_by': 15,
'created_on': '2022-10-03T07:39:01.489028+00:00',
'currency': 'solana_devnet',
'fees': None,
'id': 88,
'interaction_type': None,
'last_request': 13,
'max_fees': '101000',
'min_confirmations': 10,
'notes': [{'content': '', 'title': ''}],
'recipient': '8ua2Anm1ZM4Ngkz4qbeE2yyhhxi9bWqrZ2TfRhtkgRPp',
'speed': 'NORMAL',
'status': 'APPROVED',
'tx_hash': None,
'type': 'SEND',
'uid': None}
You can then get the transaction status
Using web hooks you can retreive when a transaction is broadcaste. On solana once a transaction was broadcasted there is no issue of having it fully confirmed. To verify that a Tx is fully confirmed you can get it's status with the following call.
# Little trick to Sync transaction history
account_id = "3"
r = requests.get(LAM_URL + "/accounts/" + account_id + "/sync" , headers=HEADER)
r.json()
# Id of the transaction that you received from your a /transaction or even from a staking operation.
transaction_id = "222"
r = requests.get(LAM_URL + "/transactions/" + transaction_id , json=payload, headers=HEADER)
r.json()
Example :
{'account_id': 5,
'account_index': 1,
'amount': '101000',
'broadcast_on': '2022-10-03T07:39:20.271966+00:00',
'coin_fields': None,
'confirmations': 0,
'created_by': 15,
'created_on': '2022-10-03T07:39:01.489028+00:00',
'currency': 'solana_devnet',
'fees': '5000',
'id': 88,
'interaction_type': None,
'last_request': 13,
'max_fees': '101000',
'min_confirmations': 10,
'notes': [{'content': '', 'title': ''}],
'recipient': '8ua2Anm1ZM4Ngkz4qbeE2yyhhxi9bWqrZ2TfRhtkgRPp',
'speed': 'NORMAL',
'status': 'SUBMITTED',
'tx_hash': '2cXN6VkU6tmCgdjUmLnFudSRQwQ91p98jRrK2xsHZpr1BnCUGjYZJeq5jN6f8X368T2LBuhi35j6qZjDenp2s43c',
'type': 'SEND',
'uid': None}
Since solana has a really fast finality time, even if you receive a 'status': 'SUBMITTED'
you can assume that your transaction is written forever on the solana blockchain.
If you need even deeper tracking of changes of states you can use the /history endpoint.
# Id of the transaction that you received from your a /transaction or even from a staking operation.
transaction_id = "222"
r = requests.get(LAM_URL + "/transactions/" + transaction_id + "/history", json=payload, headers=HEADER)
r.json()
Get Balance of an accounts
Let's get the blance of account that received the funds, which is in this case one LE-Account.
account_id = "3"
#Get Account balance and currency
r = requests.get(LAM_URL + "/accounts/" + account_id, headers=HEADER)
name = r.json()["name"]
balance = r.json()["balance"]
currency = r.json()["currency"]
#Get how to read solana currency
r = requests.get(LAM_URL + "/currencies/" + currency, headers=HEADER)
balance = int(balance) / 10**r.json()["units"][1]["magnitude"]
print(name , ":", balance, r.json()["units"][1]["code"])
ADVANCED - Solana Staking, all from one solana account
To start staking SOL, you need to have activated the staking rules on this account. To verify this you can go do it directly in the UI or via API with /accounts
.
FLOW - Operations
- POST - Stakes
- GET - Stakes
- GET - Stakes/{uid}
- POST /Stakes/{uid}/actions/deactivate
- POST /Stakes/{uid}/actions/withdraw
POST - Stake SOL
There is two way to stake SOL on LE :
- use the /transactions and craft manually the staking transactions. As you will do to perfom ETH operations with smart contracts.
- use the guided implementation, that enable staking without prior knowledge in the protocol.
In the following implementation we will use the guided implementation that you can find in the api documentation. (LINK)
# Convert the amount you want to stake in a human redable way
stake_account_id = "3"
#Get Account balance and currency
r = requests.get(LAM_URL + "/accounts/" + stake_account_id, headers=HEADER)
name = r.json()["name"]
balance = r.json()["balance"]
currency = r.json()["currency"]
#Get how to read solana currency
r = requests.get(LAM_URL + "/currencies/" + currency, headers=HEADER)
r = requests.get(LAM_URL + "/accounts/" + stake_account_id, headers=HEADER)
r.json()
# change the amount here to find out
amount_min_unit = 10000000
amount_min_unit / 10**r.json()["units"][1]["magnitude"]
stake_account_id = "3"
devnet_validator_address = "4QUZQ4c7bZuJ4o4L8tYAEGnePFV27SUFEVmC7BYfsXRp"
mainnet_validator_address = "26pV97Ce83ZQ6Kz9XT4td8tdoUFPTng8Fb8gPyc53dJx"
payload = {
"amount": 10000000,
"validator": mainnet_validator_address
}
r = requests.post(LAM_URL + "/solana/accounts/" + stake_account_id + "/stakes", json=payload, headers=HEADER)
r.json()
As response you will receive the Tx used to Create and Delegate this given amount of SOL, depending on your governance rules, you may need human approval, if you only use API the transaction will be directly Submited and the position open on chain.
Example of API initation and human approval required:
{'account_id': 2,
'account_index': 1,
'amount': '66600000',
'broadcast_on': None,
'coin_fields': None,
'confirmations': 0,
'created_by': 15,
'created_on': '2022-10-05T14:47:00.055438+00:00',
'currency': 'solana_devnet',
'fees': None,
'id': 90,
'interaction_type': None,
'last_request': 14,
'max_fees': '0',
'min_confirmations': 10,
'notes': [{'content': '', 'title': ''}],
'recipient': '4QUZQ4c7bZuJ4o4L8tYAEGnePFV27SUFEVmC7BYfsXRp',
'speed': 'NORMAL',
'status': 'APPROVED',
'tx_hash': None,
'type': 'STAKE_CREATE_DELEGATE',
'uid': None}
You can check the status of your transaction using the /transactions
or the /transaction/{id}/history
# Little trick to Sync transaction history
account_id = "2"
r = requests.get(LAM_URL + "/accounts/" + account_id + "/sync" , headers=HEADER)
r.json()
# Id of the transaction that you received from your a /transaction or even from a staking operation.
r = requests.get(LAM_URL + "/transactions/" + "224", json=payload, headers=HEADER)
r.json()
In this example we see that the Tx was approved, then signed by the HSM and SUBMITED to the solana chain.
Example of history:
{'history': [[{'created_by': 15,
'created_on': '2022-10-03T10:07:42.845231+00:00',
'expired_at': '2022-10-10T10:07:42.843803+00:00',
'id': 4,
'is_complete': True,
'status': 'PENDING_APPROVAL',
'type': 'CREATE_TRANSACTION'},
{'created_by': 11,
'created_on': '2022-10-03T10:08:09.451724+00:00',
'expired_at': '2022-10-10T10:07:42.843803+00:00',
'id': 3,
'is_complete': True,
'status': 'APPROVED',
'type': 'CREATE_TRANSACTION'},
{'created_by': 1,
'created_on': '2022-10-03T10:08:19.326571+00:00',
'expired_at': '2022-10-10T10:07:42.843803+00:00',
'id': 3,
'is_complete': True,
'status': 'SIGNED',
'type': 'CREATE_TRANSACTION'},
{'created_by': 1,
'created_on': '2022-10-03T10:08:20.118018+00:00',
'expired_at': '2022-10-10T10:07:42.843803+00:00',
'id': 2,
'is_complete': False,
'status': 'SUBMITTED',
'type': 'CREATE_TRANSACTION'}]]}
GET - Solana stakes, all open positions
Once the transaction submited you are able to get this stake
via api in a new object type.
stake_account_id = "2"
# Get all open staking positions
r = requests.get(LAM_URL + "/solana/accounts/" + stake_account_id + "/stakes", headers=HEADER)
stakes = r.json()
stakes
At the time of writing there is not yet pagination and search capabilities on this endpoints, so for now we will sort objects on our end.
# find the activating stakes (the one that we just created)
activating_stakes = []
deactivating_stakes = []
activated_stakes = []
deactivated_stakes = []
for stake in stakes:
if stake["blockchain_state"]["status"] == "activating":
activating_stakes.append(stake)
if stake["blockchain_state"]["status"] == "activated":
activated_stakes.append(stake)
if stake["blockchain_state"]["status"] == "deactivating":
deactivating_stakes.append(stake)
if stake["blockchain_state"]["status"] == "deactivated":
deactivated_stakes.append(stake)
# example of display of one obeject
activating_stakes
POST - Deactivate Stake
When you want to withdraw assets you first need to deactivate your stake. You can do so with only one operation via our API.
It will start an unlocking period of aproximately 2.5 days. At the start of the following epoch, you will have generated the last rewards and your account will be in status = "deactivated"
.
Note that you can only deactivate a stake that is in status = "activated"
or status = "activating"
stake_account_id = "2"
# Select one uid for activated_stakes
stake_uid_activated = 'd8b3d790-955c-47ea-856f-cf2569e1ded3'
# Get the stake object by ID
r = requests.get(LAM_URL + "/solana/accounts/" + stake_account_id + "/stakes/" + stake_uid_activated, headers=HEADER)
r.json()
# Select one stake account
stake_account_id = "2"
# Select one uid for activated_stakes
stake_uid_activated = 'd8b3d790-955c-47ea-856f-cf2569e1ded3'
#Deactivate stake
r = requests.post(LAM_URL + "/solana/accounts/" + stake_account_id + "/stakes/" + stake_uid_activated + "/actions/deactivate", headers=HEADER)
r.json()
In response you will get the transaction that will orchestrate this change:
{'account_id': 1,
'account_index': 0,
'amount': '0',
'broadcast_on': None,
'coin_fields': {'tx_parameters': {'stake_account': 'GzA7xqopHpV6oKcjZinttVbMpsPDMgpCmkhqVseVmrwS'},
'type': 'Solana'},
'confirmations': 0,
'created_by': 15,
'created_on': '2022-10-03T12:44:28.520832+00:00',
'currency': 'solana_devnet',
'fees': None,
'id': 278,
'interaction_type': None,
'last_request': 17,
'max_fees': '0',
'min_confirmations': 10,
'notes': [{'content': '', 'title': ''}],
'recipient': '',
'speed': 'NORMAL',
'status': 'PENDING_APPROVAL',
'tx_hash': None,
'type': 'STAKE_DEACTIVATE',
'uid': None}
# Little trick to Sync transaction history
account_id = "2"
r = requests.get(LAM_URL + "/accounts/" + account_id + "/sync" , headers=HEADER)
r.json()
# Select one uid for activated_stakes
stake_uid_deactactivating = '0f455f64-ce64-4e1c-89e4-a2fe7f19fc4d'
r = requests.get(LAM_URL + "/solana/accounts/" + stake_account_id + "/stakes/" + stake_uid_deactactivating, headers=HEADER)
r.json()
Example of response:
{'account_id': 1,
'account_name': 'devSol',
'blockchain_state': {'activation_epoch': 382,
'active_balance': 27750468,
'deactivation_epoch': 384,
'inactive_balance': 0,
'pub_key': 'GzA7xqopHpV6oKcjZinttVbMpsPDMgpCmkhqVseVmrwS',
'rent_exempt_reserve': 2282880,
'status': 'deactivating',
'total_balance': 30033348,
'validator_address': '4QUZQ4c7bZuJ4o4L8tYAEGnePFV27SUFEVmC7BYfsXRp'},
'created_on': '2022-10-03T12:56:43.924034',
'currency': 'solana_devnet',
'status': 'ACTIVE',
'total_reward': 0,
'uid': '7e01d033-4e8b-4e1a-94d9-be259236241a'}
POST - Withdraw Stake
# Array of all deactivated stakes
deactivated_stakes
# Select one stake account
stake_account_id = "2"
# Select one uid for deactivated_stakes
stake_uid_deactactivated= 'd8b3d790-955c-47ea-856f-cf2569e1ded3'
r = requests.post(LAM_URL + "/solana/accounts/" + stake_account_id + "/stakes/" + stake_uid_deactactivated + "/actions/withdraw", headers=HEADER)
r.json()
# Little trick to Sync transaction history
account_id = "2"
r = requests.get(LAM_URL + "/accounts/" + account_id + "/sync" , headers=HEADER)
r.json()
POST - Re-Delegate Object
This action creates a transaction to delegate a solana stake. The total_balance of this account will be delegated to the selected validator. You can use this transaction type if you did want to revert a /actions/deactivate
within the same epoch. Note that if your staking instance does't have actions/delegate
in its actions field, calling this endpoint will return a HTTP 400 error.
# Select one stake account
stake_account_id = "2"
devnet_validator_address = "4QUZQ4c7bZuJ4o4L8tYAEGnePFV27SUFEVmC7BYfsXRp"
# Select one uid for deactivated_stakes
stake_uid_deactactivated = 'd8b3d790-955c-47ea-856f-cf2569e1ded3'
payload = {
"validator": devnet_validator_address
}
r = requests.post(LAM_URL + "/solana/accounts/" + stake_account_id + "/stakes/" + stake_uid_deactactivated + "/actions/delegate", json=payload, headers=HEADER)
r.json()
EoD script on Solana Staking with 1-N stakes implementation
In end of day senarios you need yo orchestrate api calls in a given order.
On the exchange side you need to be able to calculate the net_staking
amount. (eg. + 100000 SOL, - 738734 SOL)
def reconciliate_stakes(account_id):
# Get / stakes on your staking account
return requests.get(LAM_URL + "/solana/accounts/" + account_id + "/stakes", json=payload, headers=HEADER)
def withdraw_deactivated_stakes(account_id, staking_instances):
deactivated_stakes = []
# Filter Deactivated
for stake in staking_instances:
if stake["blockchain_state"]["status"] == "deactivated":
deactivated_stakes.append(stake)
# POST /actions/withdraw
for deactivated_stake in deactivated_stakes:
requests.post(LAM_URL + "/solana/accounts/" + account_id + "/stakes/" + deactivated_stake["uid"] + "/actions/withdraw", json=payload, headers=HEADER)
def orchestrate_staking(account_id, net_staking, staking_instances):
if net_staking > 0 :
return split_and_stake(net_staking)
else:
return select_and_unstake(net_staking, staking_instances)
def split_and_stake(account_id, amount, staking_instances):
i = 0
objects = 15
devnet_validator_address = "4QUZQ4c7bZuJ4o4L8tYAEGnePFV27SUFEVmC7BYfsXRp"
# create and delegate equal amount
payload = {
"amount": amount* (1/objects),
"validator": devnet_validator_address
}
while i<objects:
# /stakes endpoint to be used
r = requests.post(LAM_URL + "/solana/accounts/" + account_id + "/stakes", json=payload, headers=HEADER)
i += 1
return 0
def select_and_unstake(amount, staking_instances):
#sort staking instances
staking_instances.sort(key=lambda x: x.balance)
#select x first object to unstake from
selected_amount = 0
ustaked_amount = 0
index = 0
while selected_amount < (- amount):
# anticipate the rewards generated during the undelegation period
selected_amount += staking_instances[index]['blockchain_state']['total_balance']*1.000479457
ustaked_amount += staking_instances[index]['blockchain_state']['total_balance']*1.000479457
requests.post(LAM_URL + "/solana/accounts/" + account_id + "/stakes/" + staking_instances[index]["uid"] + "/actions/deactivate", json=payload, headers=HEADER)
index +=1
return amount + ustaked_amount
Split
# Select one stake account
stake_account_id = "3"
# Select one uid for deactivated_stakes
stake_uid_deactactivated= 'd8b3d790-955c-47ea-856f-cf2569e1ded3'
r = requests.get(LAM_URL + "/solana/accounts/" + stake_account_id + "/stakes/", headers=HEADER)
r.json()
stake_uid= "a6b19443-48af-45f9-be44-037f354865ea"
payload = {
"amount": 5000000
}
r = requests.post(LAM_URL + "/solana/accounts/3/stakes/" + stake_uid + "/actions/split", json=payload, headers=HEADER)
r.json()
stake_uid= "a6b19443-48af-45f9-be44-037f354865ea"
payload = {
"amount": 5000000
}
r = requests.post(LAM_URL + "/solana/accounts/3/stakes/" + stake_uid + "/actions/split_deactivate", json=payload, headers=HEADER)
r.json()