Guides

Serializing, Deserializing, and sending Transactions

In this guide we're going to talk about:

  • Serializing and Deserializing Transaction
  • Noop Signer
  • Partially signed transactions
  • Passing transactions from different environments

Introduction

Transactions are usually serialized to facilitate movement across different environments. But the reasons could be different:

  • You may require signatures from different authorities stored in separate environments.
  • You may wish to create a transaction on the frontend, but then send and validate it on the backend before storing it to a database.

For example, when creating NFTs, you may need to sign the transaction with the collectionAutority keypair to authorize the NFT into the Collection. To safely sign it, without exposing your keypair, you could first create the transaction in the backend, partially sign the transaction with the collectionAutority without having to expose the keypair in a non secure environment, serialize the transaction and then send it. You can then safely deserialize the transactions and sign it with the Buyer wallet.

Note: When using the Candy Machine, you don't need the collectionAutority signature

Initial Setup

Required Packages and Imports

We're going to use the following packages:

To install them, use the following commands:

npm i @metaplex-foundation/umi
npm i @metaplex-foundation/umi-bundle-defaults
npm i @metaplex-foundation/mpl-core

These are all the imports that we're going to use for this guide.

import { generateSigner, signerIdentity, createNoopSigner } from '@metaplex-foundation/umi'
import { createUmi } from '@metaplex-foundation/umi-bundle-defaults'
import { fetchCollection, create, mplCore } from '@metaplex-foundation/mpl-core'
import { base64 } from '@metaplex-foundation/umi/serializers';

Setting up Umi

While setting up Umi you can use or generate keypairs/wallets from different sources. You create a new wallet for testing, import an existing wallet from the filesystem, or use walletAdapter if you are creating a website/dApp.

Serialization

Serialization of a transaction is the process of converting the transaction object into a series of bytes or string that saves the state of the transction in an easily transmittable form. This allows it to be passed through the likes of a http request.

Within the serialization example we're going to:

  • Use the NoopSigner to add the Payer as Signer in the instruction
  • Create a Versioned Transaction and sign it with the collectionAutority and the Asset
  • Serialize it so all the details are preserved and can be accurately reconstructed by the frontend
  • And send it as a String, instead of a u8, so it can be passed through a request

Noop Signer

Partially signing transactions to then serialize them, is only possible because of the NoopSigner.

Umi instructions can take Signer types by default that are often generated from local keypair files or walletAdapter signers. Sometimes you may not have access to a certain signer yet and will need to sign with said signer at a later point in time. This is where the Noop Signer comes into play.

The Noop Signer takes a publicKey and generates a special Signer type which allows Umi to build instructions without needing the Noop Signer to be present or to sign the transaction at the current point in time.

Instructions and transactions built with Noop Signers will be expecting them to sign at some point before sending off the transaction to the chain and will cause a "missing signature" error if not present.

The way to use it is:

createNoopSigner(publickey('11111111111111111111111111111111'))

Using Strings instead of Binary data

The decision to turn Serialized Transactions into Strings before passing them between environment is rooted in:

  • Formats like Base64 are universally recognized and can be safely transmitted over HTTP without the risk of data corruption or misinterpretation.
  • Using strings aligns with standard practices for web communication. Most APIs and web services expect data in JSON or other string-based formats

The way to do it is to use the base64 function present in the @metaplex-foundation/umi/serializers package.

Note: you don't need to install the package since it's included in the @metaplex-foundation/umi package

// Using the base64.deserialize and passing in a serializedTx 
const serializedTxAsString = base64.deserialize(serializedTx)[0];

// Using the base64.serialize and passing in the serializedTxAsString
const deserializedTxAsU8 = base64.serialize(serializedTxAsString);

Code Example

// Use the Collection Authority Keypair
const collectionAuthority = generateSigner(umi)
umi.use(signerIdentity(collectionAuthority))

// Create a noop signer that allows you to sign later
const frontendPubkey = publickey('11111111111111111111111111111111')
const frontEndSigner = createNoopSigner(frontendPubkey)

// Create the Asset Keypair
const asset = generateSigner(umi);

// Fetch the Collection
const collection = await fetchCollection(umi, publickey(`11111111111111111111111111111111`)); 

// Create the createAssetIx
const createAssetTx = await create(umi, {
  asset: asset,
  collection: collection,
  authority: collectionAuthority,
  payer: frontEndSigner,
  owner: frontendPubkey,
  name: 'My NFT',
  uri: 'https://example.com/my-nft.json',
})
  .useV0()
  .setBlockhash(await umi.rpc.getLatestBlockhash())
  .buildAndSign(umi);

// Serialize the Transaction
const serializedCreateAssetTx = umi.transactions.serialize(createAssetTx)

// Encode Uint8Array to String and Return the Transaction to the Frontend
const serializedCreateAssetTxAsString = base64.deserialize(serializedCreateAssetTx)[0];

return serializedCreateAssetTxAsString

Deserialization

In the deserialization example we're going to:

  • Transform the Transaction that we received through the request back to a Uint8Array
  • Deserialize it so we can operate on it from the point we left
  • Sign it with the Payer keypair since we used it through the NoopSigner in the other environment
  • Send it

Code Example

// Decode the String to Uint8Array to make it usable
const deserializedCreateAssetTxAsU8 = base64.serialize(serializedCreateAssetTxAsString);

// Deserialize the Transaction returned by the Backend
const deserializedCreateAssetTx = umi.transactions.deserialize(deserializedCreateAssetTxAsU8)

// Sign the Transaction with the Keypair that we got from the walletAdapter
const signedDeserializedCreateAssetTx = await umi.identity.signTransaction(deserializedCreateAssetTx)

// Send the Transaction
await umi.rpc.sendTransaction(signedDeserializedCreateAssetTx)

Full Code Example

Naturally, to have a fully reproducible example of the instructions in action, we need to include additional steps, such as handling the frontend signer and creating a collection.

Don't worry if everything isn't identical; the backend and frontend portions remain consistent.

import { generateSigner, createSignerFromKeypair, signerIdentity, sol, createNoopSigner, transactionBuilder } from '@metaplex-foundation/umi'
import { createUmi } from '@metaplex-foundation/umi-bundle-defaults'
import { base58 } from '@metaplex-foundation/umi/serializers';
import { createCollection, create, fetchCollection } from '@metaplex-foundation/mpl-core'

const umi = createUmi("https://api.devnet.solana.com", "finalized")

const collectionAuthority = generateSigner(umi);
umi.use(signerIdentity(collectionAuthority));

const frontEndSigner = generateSigner(umi);

(async () => {
  
  // Airdrop Tokens inside of the wallets
  await umi.rpc.airdrop(umi.identity.publicKey, sol(1));
  await umi.rpc.airdrop(frontEndSigner.publicKey, sol(1));

  // Generate the Collection KeyPair
  const collectionAddress = generateSigner(umi)
  console.log("\nCollection Address: ", collectionAddress.publicKey.toString())

  // Generate the collection
  let createCollectionTx = await createCollection(umi, {
    collection: collectionAddress,
    name: 'My Collection',
    uri: 'https://example.com/my-collection.json',
  }).sendAndConfirm(umi)

  const createCollectionSignature = base58.deserialize(createCollectionTx.signature)[0]
  console.log(`\nCollection Created: https://solana.fm/tx/${createCollectionSignature}?cluster=devnet-alpha`);

  // Serialize
  
  const asset = generateSigner(umi);
  console.log("\nAsset Address: ", asset.publicKey.toString());

  const collection = await fetchCollection(umi, collectionAddress.publicKey); 

  let createAssetIx = await create(umi, {
    asset: asset,
    collection: collection,
    authority: collectionAuthority,
    payer: createNoopSigner(frontEndSigner.publicKey),
    owner: frontEndSigner.publicKey,
    name: 'My NFT',
    uri: 'https://example.com/my-nft.json',
  })
    .useV0()
    .setBlockhash(await umi.rpc.getLatestBlockhash())
    .buildAndSign(umi);


  const serializedCreateAssetTx = umi.transactions.serialize(createAssetTx)
  const serializedCreateAssetTxAsString = base64.deserialize(serializedCreateAssetTx)[0];

  // Deserialize

  const deserializedCreateAssetTxAsU8 = base64.serialize(serializedCreateAssetTxAsString);
  const deserializedCreateAssetTx = umi.transactions.deserialize(deserializedCreateAssetTxAsU8)
  const signedDeserializedCreateAssetTx = await frontEndSigner.signTransaction(deserializedCreateAssetTx)

  const createAssetSignature = base58.deserialize(await umi.rpc.sendTransaction(signedDeserializedCreateAssetTx))[0]
  console.log(`\nAsset Created: https://solana.fm/tx/${createAssetSignature}}?cluster=devnet-alpha`);
})();