How to sign Transactions using multiple signers on Solana

Program execution on Solana Blockchain begins with a Transaction being submitted to the cluster. Transactions are a group of instructions that a client (typically a wallet) signs using one or more key pairs and then is executed atomically with only two possible outcomes: success or failure. The Solana runtime will execute a program to process each of the instructions contained in the transaction, in order, and atomically.

Each transaction contains a compact array of signatures, followed by a message. Each item in the signatures array is a digital signature of the given message. The Solana runtime verifies that each signature was signed by the private key corresponding to the public key at the same index in the message's account addresses array.

In some cases, Transactions require a single signature, and in other cases, they require more than one signature. A few APIs from SHYFT, such as the create_v2, and the update_v2 return encoded_transaction in their response, which in most cases require signatures from two or more wallets. In this blog, we will see how we can sign transactions that require more than one signer.

Before getting started

To get started, we will need a few things.

Phantom Wallet

We will need the Phantom wallet browser extension, you can download it from the link below.

Chrome/Brave.

Firefox.

Once done, set up your Phantom wallet account. On-screen tips are available, which will guide you through setting up and getting started. You can also find a detailed guide related to this here.

Authentication: Getting your Shyft API key

x-api-key is an authentication parameter, which gives you access to SHYFT APIs. You can get your own API Key from the SHYFT website. Just signup with your email id here and you can get it for free. This is not really required for signing transactions, but this finds its application when making the API call which returns the encoded_transaction.

Read SHYFT API Documentation here.

Signing Transactions: Two Signers

Let’s consider the following scenario, we are making a create_v2 API call which to mint a new NFT, but the NFT creator (creator_walletparameter) and the fee payer (fee_payer parameter) for the create operation are different wallets. Such situations arise, in cases such as where an application is allowing users to create NFTs in a gasless manner, where the application is paying the gas fees (fee_payer) and the end user is the NFT creator (creator_address).

Check out Minting Gasless NFTs with SHYFT APIs.

In such scenarios, the encoded_transaction received will need two signatures to be successfully executed by the Solana Runtime; first using the private_key of the fee_payer wallet, and next using the wallet of the end user. First, we attempt to sign the encoded_transaction using the private_key of the fee payer wallet, which in this case is the service provider, (Assuming the private_key of the service provided is available in the environment variable of the application or embedded somewhere in the application) using the partialSign() method . Once that is successful, we go on and sign the recoveredTransaction using the user’s wallet (creator_wallet).

The function for signing using both wallets is illustrated below

import {clusterApiUrl, Connection, Keypair, Transaction } from '@solana/web3.js';

import { decode } from 'bs58';
import { Buffer } from 'buffer';

export async function partialSignWithKeyAndWallet(connection,encodedTransaction,privateKey,wallet)
{
    const feePayer = Keypair.fromSecretKey(decode(privateKey));
    const recoveredTransaction = Transaction.from(
      Buffer.from(encodedTransaction, 'base64')
    );
    recoveredTransaction.partialSign(feePayer); //partially signing using private key of fee_payer wallet
    const signedTx = await wallet.signTransaction(recoveredTransaction); // signing the recovered transaction using the creator_wall
    const confirmTransaction = await connection.sendRawTransaction(
      signedTx.serialize()
    );
    return confirmTransaction;
  
}

This function accepts the connection to the user’s wallet, the encodedTransaction received in response of the API call, privateKey of the fee_payer, and the wallet object. We can get the connection and the wallet object for phantom in the following manner.

const phantom = new PhantomWalletAdapter();
await phantom.connect();
const rpcUrl = clusterApiUrl(network);
const connection = new Connection(rpcUrl, "confirmed");

This connection and wallet objects is then passed on to the partialSignWithKeyAndWallet() function, along with the encodedTransaction and the privateKey in the following manner.

const finalizedTransaction = await partialSignWithKeyAndWallet(connection, recoveredTransaction, phantom);

This will sign the transaction both via the fee_payer’s private key and the user's wallet. Once the user clicks approve, the transaction will be signed and the instruction will be executed.

We can verify if the instruction execution is complete or not, using the following method, where callback is a function, which is executed once the instruction has completed execution.

connection.onSignature(finalizedTransaction, callback, "finalized");

Now that we have seen how we can sign transactions using two different signer wallets, let’s see how we can sign transactions when there are more than two wallets.

Signing Transaction: More than Two Signers

In some instances, it may happen that, the fee_payer, creator_wallet and the collection owner( owner of the collection whose collection_address is used) are three different wallets, and the encoded_transaction generated needs 3 different signatures (or n different signatures). In such scenarios, we will use the fee_payer private key and the collection owner's private key to partially sign the transaction from the backend, and then we will use the end user’s wallet (creator_wallet) to sign the transaction finally.

We use a tweaked version of the same function to partially sign the transaction using the private keys of fee_payer and the collection owner. This function can be used in the previous case as well, as this function can partially sign transactions using n number of wallets as long as their private keys are available. Once that step is complete we go on and sign the recoveredTransaction using the user’s wallet (creator_wallet)

export async function partialSignWithKeysAndWallet(connection,encodedTransaction,privateKeys,wallet)
{
    const recoveredTransaction = Transaction.from(
      Buffer.from(encodedTransaction, 'base64')
    );
    const keys = privateKeys.map((k) => {
      return Keypair.fromSecretKey(decode(k));
    });
    
    recoveredTransaction.partialSign(...keys); //sign transaction with all private keys
   
    const signedTx = await wallet.signTransaction(recoveredTransaction);
    const confirmTransaction = await connection.sendRawTransaction(
      signedTx.serialize()
    );
    return confirmTransaction;
  
}

Similar to its previous version, this function accepts the connection to the user’s wallet, the encodedTransaction received in the response of the API call and the wallet object. The only difference is, along with these parameters, this function also takes in an array of private keys, which can contain all the private keys required to partially sign the transaction.

The wallet object and the connection to the user’s wallet can be obtained in the same manner as shown in the previous step.

This will also allow the user to sign the transaction via their wallet, once they click approve, the transaction will be signed and the instruction will be executed.

Signing multiple Transactions with one wallet

In certain situations, instead of one encoded_transaction SHYFT APIs return more than one, or an array of encoded_transactions, which will need a sign from the wallet in the front end. This happens in API endpoints such as transfer_many API, where the encoded_transactions returned is an array of encoded_transaction. In such cases, we will use the following function to sign the transaction from the front end:

export async function confirmTransactionsFromFrontend(connection, encodedTransactions, wallet) {
  
    const recoveredTransactions = encodedTransactions.map((tx) => {
      return Transaction.from(
        Buffer.from(tx, 'base64')
      );
    });
  
    const signedTx = await wallet.signAllTransactions(recoveredTransactions); //signs all the transactions in the recoveredTransactions array in one go
    
    var sentTxns = [];
    for await(const tx of signedTx)
    {
      const confirmTransaction = await connection.sendRawTransaction(
        tx.serialize()
      );
      sentTxns.push(confirmTransaction);
    }

    return sentTxns;
    
  }

This function accepts connection, the encoded_transactions (an array of encoded_transaction) from the SHYFT API response and the wallet object. Then all the transactions in the encoded_transactions array will be signed from the wallet in one go using the signAllTransactions() method. The wallet object and the connection to the user’s wallet can be obtained in the same manner as shown in the previous step.

That’s pretty much all about signing transactions using wallet and private keys on Solana. If you liked this blog, feel free to checkout our blog on adding NFTs to Collections on Solana or getting all NFT collections from a wallet. We hope you have a great time building dApps on Solana using SHYFT APIs. Happy Hacking. 😇

Resources

SHYFT API Documentation

Shyft Website

Get API Key

Github

Join our Discord

Try out our APIs on Swagger UI

Last updated