Stake Solana Tutorial

Requirements :

  • Ledger Enteprise with the Solana Release
  • LAM setup
  • Admin access to the Ledger Enteprise Application

How to start :

  1. Create your API users
  2. Create a SOLANA Account as Reserve Cold Storage
  3. Create a SOLOANA Account to Perform Staking operations
  4. Add API user to the SOLANA Account
Copy
Copied
import requests
import json

CONSTANTES

Copy
Copied
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.

Copy
Copied
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

Add_User_screen.png

Click Invite User

image.png

Select API and insert username and userId

Screenshot 2022-09-29 at 14.33.30.png

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

Copy
Copied
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 :

Copy
Copied
{
 '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

image.png

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}

Copy
Copied
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

Screenshot 2022-09-29 at 14.57.42.png

Once approve by the quorum you can use the API operator and see that he is registered in your workspace

image.png

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.

image.png

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.

Copy
Copied
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.

Copy
Copied
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 :

Copy
Copied
{'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.

image.png

colab={"base_uri": "https://localhost:8080/"} id="5pTsccT_BRL9" outputId="ab4fc9e5-6e39-47cc-a58e-8a95b480465e"colab={"base_uri": "https://localhost:8080/"} id="5eYDonlAyckU" outputId="2c0e7a23-516a-43bf-f0bd-aaedf6817a6d"
Copy
Copied
# Little trick to Sync transaction history
account_id = "3"
r = requests.get(LAM_URL + "/accounts/" + account_id + "/sync" , headers=HEADER)
r.json()
Copy
Copied
# 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 :

Copy
Copied
{'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.

Copy
Copied
# 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.

Copy
Copied
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.

image.png

FLOW - Operations

  • POST - Stakes
  • GET - Stakes
  • GET - Stakes/{uid}
  • POST /Stakes/{uid}/actions/deactivate
  • POST /Stakes/{uid}/actions/withdraw

solana_flow.png

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)

id="WCa6QQg9Sc6Y"colab={"base_uri": "https://localhost:8080/"} id="c1y81Mvf1UZ3" outputId="08aa484a-87f3-4586-fb39-08f405343f4e"colab={"base_uri": "https://localhost:8080/"} id="sJ2BGhnmSwMn" outputId="fa17f056-c406-4f6d-803a-c68b0b2d28a7"colab={"base_uri": "https://localhost:8080/"} id="jJDVd6NVEPBC" outputId="27758e12-0993-4415-9f15-0ed2add66142"
Copy
Copied
# 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)
Copy
Copied
r = requests.get(LAM_URL + "/accounts/" + stake_account_id, headers=HEADER)

r.json()
Copy
Copied
# change the amount here to find out
amount_min_unit = 10000000
amount_min_unit / 10**r.json()["units"][1]["magnitude"]
Copy
Copied
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:

Copy
Copied
{'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 /transactionsor the /transaction/{id}/history

colab={"base_uri": "https://localhost:8080/"} id="pV5JRgUknbNJ" outputId="f3842fe5-dfd5-4fb6-d4eb-9be5e1b682b3"colab={"base_uri": "https://localhost:8080/"} id="sSM8eLJfSKo3" outputId="2f6f9f12-6382-43a7-9d70-7f1bc9cfae81"
Copy
Copied
# Little trick to Sync transaction history
account_id = "2"
r = requests.get(LAM_URL + "/accounts/" + account_id + "/sync" , headers=HEADER)
r.json()
Copy
Copied
# 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:

Copy
Copied
{'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.

id="RLy9P8I6VYbV"colab={"base_uri": "https://localhost:8080/"} id="J645io2tog2M" outputId="67eec9fc-d781-463c-b72e-59b40f825ea0"
Copy
Copied
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()
Copy
Copied
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.

id="5-6vndvNWAtM"colab={"base_uri": "https://localhost:8080/"} id="PKT26wteynEH" outputId="f8712436-df76-44b7-984e-dab126a611c2"
Copy
Copied
# 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)
Copy
Copied
# 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"

colab={"base_uri": "https://localhost:8080/"} id="mmAeHZyWWLIC" outputId="e34cef11-2b2c-41ce-cd7d-65fff8d44188"colab={"base_uri": "https://localhost:8080/"} id="KpJN2CRs2m1H" outputId="27373cd7-bb04-48e0-a55a-ade5b177d2f0"
Copy
Copied
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()
Copy
Copied
# 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:

Copy
Copied
{'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}
colab={"base_uri": "https://localhost:8080/"} id="RfodFp7WrOTr" outputId="e77bacaa-6b61-42eb-a331-47df859a99b6"colab={"base_uri": "https://localhost:8080/"} id="qj6sN-QR5eEM" outputId="ca9cee88-1b19-457a-ca2b-c765fd8d5c31"
Copy
Copied
# Little trick to Sync transaction history
account_id = "2"
r = requests.get(LAM_URL + "/accounts/" + account_id + "/sync" , headers=HEADER)
r.json()
Copy
Copied
# 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:

Copy
Copied
{'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

colab={"base_uri": "https://localhost:8080/"} id="OoCxgaoHBWKC" outputId="9bd141f1-ff7d-42b8-d355-dc7cff0929af"colab={"base_uri": "https://localhost:8080/"} id="kZR6uoLGAzGx" outputId="4a2ab187-3861-4733-b657-3cecf71d465c"colab={"base_uri": "https://localhost:8080/"} id="tKSmwaJxrshy" outputId="a50277e0-df84-44a8-952a-9c9da3de59f2"
Copy
Copied
# Array of all deactivated stakes
deactivated_stakes
Copy
Copied
# 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()
Copy
Copied
# 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.

Copy
Copied
# 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)

Copy
Copied
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

colab={"base_uri": "https://localhost:8080/"} id="knDAlbmkGsj6" outputId="fd22573d-c8cd-4e0b-fcfa-4a80b050109d"colab={"base_uri": "https://localhost:8080/"} id="0zsxblVGG1oP" outputId="0e589768-d9fc-4594-db48-76c5579156f6"id="suT4ZeImHDkC"
Copy
Copied
# 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()
Copy
Copied
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()
Copy
Copied
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()
Copyright © Ledger Enterprise Platform 2023. All right reserved.