Skip to content

Latest commit

 

History

History
341 lines (246 loc) · 18.2 KB

File metadata and controls

341 lines (246 loc) · 18.2 KB
keywords
advanced
chain fusion
integration
rust
sol
solana
solana integration
spl

Basic Solana

Overview

This tutorial will walk you through how to deploy a simple smart contract on the Internet Computer (known as canisters) that can control digital assets on the Solana blockchain:

  1. SOL, the native currency on Solana;
  2. any other token (known as SPL tokens).

🎥 Check out also this demo that runs through most parts of this example.

Architecture

This example internally leverages the threshold EdDSA and HTTPs outcalls features of the Internet Computer.

For a deeper understanding of the ICP <> SOL integration, see the chain fusion overview.

Prerequisites

  • Install the IC SDK v0.27.0. If the IC SDK is already installed with an old version, install 0.27.0 with dfxvm.
  • Confirm the IC SDK has been installed with the correct version with dfx --version.
  • On macOS, an llvm version that supports the wasm32-unknown-unknown target is required. This is because the zstd crate (used, for example, to decode base64+zstd-encoded responses from Solana's getAccountInfo) depends on LLVM during compilation. The default LLVM bundled with Xcode does not support wasm32-unknown-unknown. To fix this, install the Homebrew version, using brew install llvm.

Note

If you wish to use this example as a starting point for your own project, make sure your follow the instructions in the build requirements for the sol_rpc_client crate to ensure that your code compiles.

Step 1: Building and deploying sample code

Clone the smart contract

To clone and build the smart contract in Rust:

git clone https://github.com/dfinity/sol-rpc-canister
cd examples/basic_solana

If you are using macOS, you'll need to install Homebrew and run brew install llvm to be able to compile the example.

Acquire cycles to deploy

Deploying to the Internet Computer requires cycles (the equivalent of "gas" on other blockchains).

Deployment

Mainnet deployment

To deploy the Solana wallet smart contract to the ICP Mainnet, navigate to examples/basic_solana/mainnet and execute the following command:

dfx deploy --ic

This deploys a Solana wallet canister to the ICP Mainnet which is configured to interact with the Solana Devnet via the SOL RPC canister at tghme-zyaaa-aaaar-qarca-cai. Note that you will need to pay for your requests with cycles. If you do not have cycles available for testing, consider running this example locally as described in the next section.

Local deployment

To deploy the Solana wallet smart contract locally, navigate to examples/basic_solana/local and execute the following commands:

dfx start --clean --background 
dfx deploy

What this does:

  • dfx start --clean --background starts a local instance of the ICP blockchain.
  • dfx deploy deploys a Solana wallet canister as well as a SOL RPC canister, both locally. The Solana wallet canister interacts with the Solana Devnet via the local SOL RPC canister.

Tip

To target Solana Mainnet, you will need to change the init_arg for the basic_solana canister in the dfx.json file. To learn more about the initialization arguments, see the InitArg type in basic_solana.did.

Deploying from ICP Ninja

To deploy the Solana wallet smart contract using ICP Ninja, click on the following link:

Tip

If you downloaded the project from ICP Ninja to run it locally, you’ll need to update the init_arg for the basic_solana canister in dfx.json:

  • Change the key name from ed25519_key_name = opt variant { MainnetTestKey1 } to ed25519_key_name = opt variant { LocalDevelopment }.
  • To use the public Solana Labs RPC endpoint instead of the default RPC providers (which require a valid API key), replace solana_network = opt variant { Devnet } with solana_network = opt variant { Custom = record { url = "https://api.devnet.solana.com"; headers = null } }, or use the URL for the cluster you want, as documented here.

For more details, see the InitArg type in basic_solana.did.

Getting the canister ID

If the canister deployment is successful (whether on Mainnet or locally), you should see an output that looks like this:

Deploying: basic_solana
Building canisters...
...
Deployed canisters.
URLs:
Candid:
    basic_solana: https://bd3sg-teaaa-aaaaa-qaaba-cai.raw.icp0.io/?id=<YOUR-CANISTER-ID>

Your canister is live and ready to use! You can interact with it using either the command line or using the Candid UI, which is the link you see in the output above.

In the output above, to see the Candid Web UI for your Solana canister, you would use the URL https://bd3sg-teaaa-aaaaa-qaaba-cai.raw.icp0.io/?id=<YOUR-CANISTER-ID>. You should see the methods specified in the Candid file basic_solana.did.

Important

If running this example locally, you will need to skip the --ic flag in all subsequent dfx commands.

Step 2: Generating a Solana account

A Solana account can be derived from an EdDSA public key. To derive a user's specific account, identified on the IC by a principal, the canister uses its own threshold EdDSA public key to derive a new public key deterministically for each requested principal. To retrieve your Solana account, you can call the solana_account method on the previously deployed canister:

dfx canister --ic call basic_solana solana_account

This will return a Solana account such as ("2kqg1tEj59FNe3hSiLH88SySB9D7fUSArum6TP6iHFQY") that is tied to your principal. Your account will be different. You can view such accounts on any Solana explorer such as Solana Explorer.

If you want to send some SOL to someone else, you can also use the above method to enquire about their Solana account given their IC principal:

dfx canister --ic call basic_solana solana_account '(opt principal "hkroy-sm7vs-yyjs7-ekppe-qqnwx-hm4zf-n7ybs-titsi-k6e3k-ucuiu-uqe")'

This will return a different Solana address as the one above, such as ("8HNiduWaBanrBv8c2pgGXZWnpKBdEYuQNHnspqto4yyq").

Step 3: Receiving SOL

Now that you have your Solana account, let us send some Devnet SOL to it:

  1. Get some Devnet SOL if you don't have any. You can for example use this faucet.
  2. Send some Devnet SOL to the address you obtained in the previous step. You can use any Solana wallet to do so.

Once the transaction is confirmed, you'll be able to see it in your Solana account's balance, which should be visible in a Solana explorer, e.g. https://explorer.solana.com/address/2kqg1tEj59FNe3hSiLH88SySB9D7fUSArum6TP6iHFQY?cluster=devnet.

Step 4: Sending SOL

You can send SOL using the send_sol endpoint on your canister, specifying a Solana destination account and an amount in the smallest unit (Lamport). For example, to send 1 Lamport to 8HNiduWaBanrBv8c2pgGXZWnpKBdEYuQNHnspqto4yyq, run the following command:

Note

If no principal is provided, the caller's principal is used. In this basic_solana example, you could replace null with another principal to send SOL on their behalf. This is behaviour you would typically not want in production, as it allows anyone to send SOL from any account to any other account. In production, you would typically want to restrict the send_sol endpoint to only allow sending SOL from the caller's account.

dfx canister --ic call basic_solana send_sol '(null, "8HNiduWaBanrBv8c2pgGXZWnpKBdEYuQNHnspqto4yyq", 1)'

The send_sol endpoint sends SOL by executing the following steps:

  1. Retrieving a recent blockhash. This is necessary because all Solana transactions must include a blockhash within the 151 most recent stored hashes (which corresponds to about 60 to 90 seconds).
  2. Building a Solana transaction that includes a single instruction to transfer the specified amount from the sender's address to the given receiver's address, as well as the recent blockhash.
  3. Signing the Solana transaction using the threshold Ed25519 API.
  4. Sending the signed transaction to the Solana network using the sendTransaction method in the SOL RPC canister.

The send_sol endpoint returns the transaction ID of the transaction sent to the Solana network, which can for example be used to track the transaction on a Solana blockchain explorer.

Step 5: Sending SOL using durable nonces

Durable nonces can be used instead of a recent blockhash when constructing a Solana transaction. This can be useful for example when signing a transaction in advance before sending it out.

In order to use durable nonces, you must first create a nonce account controlled by your Solana account. The nonce account contains the current value of the durable nonce. To create a nonce account controlled by your Solana account, run the following command:

dfx canister --ic call basic_solana create_nonce_account

You can inspect the created nonce account and get the current durable nonce value in a Solana explorer. You can also fetch the current value of the durable nonce by running the following command:

dfx canister --ic call basic_solana get_nonce

To send some SOL using a durable nonce, you can run the following command:

Note

If no principal is provided, the caller's principal is used. In this basic_solana example, you could replace null with another principal to send SOL on their behalf. This is behaviour you would typically not want in production, as it allows anyone to send SOL from any account to any other account. In production, you would typically want to restrict the send_sol_with_durable_nonce endpoint to only allow sending SOL from the caller's account.

dfx canister --ic call basic_solana send_sol_with_durable_nonce '(null, "8HNiduWaBanrBv8c2pgGXZWnpKBdEYuQNHnspqto4yyq", 1)'

The send_sol_with_durable_nonce endpoint works similarly to the send_sol endpoint, however the instructions included in the transaction are different and the durable nonce is included in the transaction instead of a recent blockhash. The send_sol_with_durable_nonce endpoint sends SOL by executing the following steps:

  1. Retrieving the current durable nonce value from the nonce account.

  2. Building a Solana transaction that includes instructions to

    1. advance the nonce account (which is required so that the nonce value is used only once), and
    2. transfer the specified amount from the sender's address to the given receiver's address,

    as well as the durable nonce value instead of a recent blockhash.

  3. Signing the Solana transaction using the threshold Ed25519 API.

  4. Sending the signed transaction to the Solana network using the sendTransaction method in the SOL RPC canister.

The send_sol_with_durable_nonce endpoint returns the transaction ID of the transaction sent to the Solana network. You can also verify (either in a Solana explorer or using the get_nonce endpoint) that the nonce value stored in the account has changed after calling this endpoint.

Step 6: Sending Solana Program Library (SPL) tokens

We will now be sending some SPL tokens on Solana Devnet. The instructions below work for any SPL token. You may for example use the USDC token whose mint account on Devnet is 4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU.

You first need to create Associated Token Accounts (ATA) for the sender and recipient accounts if they do not exist yet. An ATA is a Program Derived Address (PDA) derived from a Solana account using the token mint account. An ATA is needed for each type of SPL token held by a Solana account.

We create two new identities, one for the sender and one for the recipient. You can do this by running the following commands:

dfx identity new sender
dfx identity new recipient

We have to make sure the Solana accounts belonging to the new identities created above actually hold SOL to pay for transaction fees. For this, follow the instructions outlined in Step 2 and Step 3 for each identity. You can switch between identities using the dfx identity use <IDENTITY_NAME> command or specify the identity to use by adding the --identity <IDENTITY_NAME> flag to the dfx commands.

To create the ATAs for the sender and recipient, you can run the following commands:

Note

If no principal is provided as the first argument, the caller's principal is used.

dfx identity use sender
dfx canister --ic call basic_solana create_associated_token_account '(null, "<TOKEN MINT ADDRESS>")'
dfx identity use recipient
dfx canister --ic call basic_solana create_associated_token_account '(null, "<TOKEN MINT ADDRESS>")'

This works by sending transactions that instruct the Solana Associated Token Account Program to create and initialize these accounts. You can now inspect the sender and recipient accounts on a Solana explorer and confirm that you can see a balance of 0 for the corresponding SPL token.

To send some tokens from the sender to the receiver, you will need to obtain some tokens on the sender account (using e.g. this faucet for USDC). To do this, you will need the ATA address of the sender. You can for example get it by running the following command:

Note

If no principal is provided as the first argument, the caller's principal is used.

dfx identity use sender
dfx canister --ic call basic_solana associated_token_account '(null, "<TOKEN MINT ADDRESS>")'

To transfer some tokens from the sender to the recipient, you can run the following command:

Note

If no principal is provided as the first argument, the caller's principal is used. Make sure to use the RECIPIENT SOLANA ADDRESS, not their ATA.

dfx identity use sender
dfx canister --ic call basic_solana send_spl_token '(null, "<TOKEN MINT ADDRESS>", "<RECIPIENT SOLANA ADDRESS>", <AMOUNT>)'

The send_spl_token endpoint works similarly to the send_sol endpoint, but creates a transaction with the sender and recipient ATAs instead of their account addresses. You can also inspect the resulting transaction on a Solana explorer, and verify that the associated token balances were updated accordingly. You can also check the updated token balances by running the following commands:

Note

If no ATA is provided, it is derived from the caller's principal.

dfx canister --ic call basic_solana get_spl_token_balance '(opt "<SENDER ATA>", "<TOKEN MINT ADDRESS>")'
dfx canister --ic call basic_solana get_spl_token_balance '(opt "<RECIPIENT ATA>", "<TOKEN MINT ADDRESS>")'

Conclusion

In this tutorial, you were able to:

  • Deploy a canister smart contract on the ICP blockchain that can receive and send SOL.
  • Acquire cycles to deploy the canister to the ICP Mainnet.
  • Connect the canister to the Solana Devnet.
  • Send the canister some Devnet SOL.
  • Use the canister to send SOL to another Solana account.
  • Create a Solana nonce account and use the canister to send some SOL to another Solana account using durable nonces.
  • Create an associated token account for an SPL token use the canister to send some tokens to another Solana account.

Additional examples regarding the ICP <> SOL integration can be found here.

Security considerations and best practices

If you base your application on this example, we recommend you familiarize yourself with and adhere to the security best practices for developing on the Internet Computer. This example may not implement all the best practices.

For example, the following aspects are particularly relevant for this app: