Create, sign & decode Solana transactions with minimum deps

Overview

micro-sol-signer

Create, sign & decode Solana transactions with minimum deps.

  • Tiny: 674 LOC, 3K LOC with all deps bundled
  • No network code in main package: allows simpler audits and offline usage
  • Can be used for transaction decoding in offline wallet. Solana web node could decode transactions, but can we trust it?

Check out all web3 utility libraries:

Usage

npm install micro-sol-signer

import * as sol from 'micro-sol-signer';

Specific features:

Create and sign simple transaction

// 11111111... private key
const privKey = new Uint8Array(32).fill(0x01);
// Get address of private key
const address = await sol.getAddress(privKey);
// AKnL4NNf3DGWZJS6cPknBuEGnVsV4A4m5tgebLHaRSZ9

// Format private key
const privFormatted = await sol.formatPrivate(privKey);
// 2AXDGYSE4f2sz7tvMMzyHvUfcoJmxudvdhBcmiUSo6iuCXagjUCKEQF21awZnUGxmwD4m9vGXuC3qieHXJQHAcT

// Simple tx (transfer some sol's from one account to another)
const toAddress = 'FDwkzWGxx6LfCfzcmVVLEk3QUMxNhuFuKEMRwzR4Dtys';
const blockhash = 'J2BjKU6L83eehHVgoze6uTXGCBu6nbxsqEro9QvWpU52';
const amount = '10.1';
const fee = '1.2';
const tx0 = sol.createTx(address, toAddress, amount, fee, blockhash);
// AQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAEDiojj3XQJ8ZX9UtstPLpdcspnCb8dlBIb83SIAbQPb1zTVICVf7+to6zQ/+XautpF+KSSoZ7ESTxv3rg8xPqyXgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/ORj/WtXHGLCh9wC0eGkf26qTFR5x3nCqwXXmoVtZb0BAgIAAQwCAAAAAMUBWgIAAAA=
const [txHash0, signedTx0] = await sol.signTx(privKey, tx0);
/*
{
  txHash0: '3wL4PdgBr3J2r4uuUrf4MU7HhgrJg6re2YRBeAwsZpYZRHgSgUAJymLRu6GcnKk7ZuR3F5UgPRTNj1mbzv966PTy',
  signedTx0: 'AZLiibj05SPRhNU/o3ntK/7+aNQ8H/3HGLdh6zjxZKdyrKJEhjUjWDlMnEF2x0U/8JsKYsMMiYvQShkuNfpYdAwBAAEDiojj3XQJ8ZX9UtstPLpdcspnCb8dlBIb83SIAbQPb1zTVICVf7+to6zQ/+XautpF+KSSoZ7ESTxv3rg8xPqyXgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/ORj/WtXHGLCh9wC0eGkf26qTFR5x3nCqwXXmoVtZb0BAgIAAQwCAAAAAMUBWgIAAAA='
}
  */

Decode transaction

const { base64 } = require('@scure/base');
const sol = require('../lib');

// Random USDT tx from explorer:
// https://explorer.solana.com/tx/5Nnhjv1GVB8T1k8MguUGHQw5zQQQsWET1f1zzj8azRhnVoYQPoZPtkscPCKy6FisP2eVWehjU1EYV8zywqKm5if4
const tx =
  'Atrba9P4rJ4tA3fMXioF+LBR5Y397TCaCC7o/JsViIFxDQ+FOpW2/I+DGMtapWPmrRJ3KDEaYa21YbpUcXaygQPKXDfudpRNZKsMsjhhH018U2YKTAJoqu6Jr1jASfnV98/65boYyPzPujo4YMKnIaCjrt1EsvnPNCuoBMXUEzYAAgEECc20MANIMI92j1eVfOiH5WQ691HznE9ZeQfjeXpDNm0eH5z5eohWokD+6H+jjnZ/KFqkCmlEdPrk6HCx+mOgjTAJUM/3r5vR1DjJnZhT6PQK3Z32pIe8MzDmPxe8Ttzy2CTxiTfFaNQeAkRJCefcB5JJGeb/Qxrj4dpxv8Kv9gClJ544V5wdVgmhBbCFO1kSIv6OaEUizyYdqhTUiO8w8XsGp9UXGSxWjuCKhF9z0peIzwNcMUWyGrNE2AYuqUAAAM4BDmCv7bInF71jGS9UFFo/llozu4LSxwKess4eIIJkAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAG3fbh12Whk9nL4UbO63msHLSF7V9bN5E6jPWFfv8AqYUpqsC9KfFD7lsris1C7YZkNRdSH5qix9nMo2igoP0yAgcDAgUBBAQAAAAIBAMGBAAKDJSDxgMPAAAABg==';
const decodedTx = sol.Transaction.decode(base64.decode(tx));

console.log(decodedTx);
/*
{
  msg: {
    feePayer: 'EqywLUZcm73PSWri93X3M5TN62iFMsUPMjvWYUq89dKB',
    blockhash: '9xp5Jz2v7ZsE3Xn5SVGkRisjo7h16vzF1ducwhWnc5n9',
    instructions: [ [Object], [Object] ]
  },
  signatures: {
    EqywLUZcm73PSWri93X3M5TN62iFMsUPMjvWYUq89dKB: Uint8Array(64),
    '38QU8LKVK1Ew5uzsqttamNTTFxvnfzgi2ACQvj3ekuom': Uint8Array(64),
  }
}
*/

console.log(
  decodedTx.msg.instructions.map((i) => {
    return sol.parseInstruction(i, {
      ...sol.COMMON_TOKENS,
      // You can add custom tokens here if needed
    });
  })
);
/*
[
  {
    type: 'advanceNonce',
    info: {
      nonceAccount: 'dN98UQCp6Hq9kedJKEczHt5B53tsC8eENv9cGEVwvuD',
      nonceAuthority: '38QU8LKVK1Ew5uzsqttamNTTFxvnfzgi2ACQvj3ekuom'
    },
    hint: 'Consume nonce in nonce account=dN98UQCp6Hq9kedJKEczHt5B53tsC8eENv9cGEVwvuD (owner: 38QU8LKVK1Ew5uzsqttamNTTFxvnfzgi2ACQvj3ekuom)'
  },
  {
    type: 'transferChecked',
    info: {
      amount: 64487850900n,
      decimals: 6,
      source: '3VDHywae15vgbG2euNPpwoHTEr2eyGLuS6EoF74kDkp4',
      mint: 'Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB',
      destination: '3feqC1fmo5YHMh2iw7X9kGE9F8P147hiiDQqC5xtSbpN',
      owner: 'EqywLUZcm73PSWri93X3M5TN62iFMsUPMjvWYUq89dKB'
    },
    hint: 'Transfer 64487.8509 USDT from token account=3VDHywae15vgbG2euNPpwoHTEr2eyGLuS6EoF74kDkp4 of owner=EqywLUZcm73PSWri93X3M5TN62iFMsUPMjvWYUq89dKB to 3feqC1fmo5YHMh2iw7X9kGE9F8P147hiiDQqC5xtSbpN'
  }
]
*/

Create complex transactions and send tokens

Solana is very flexible and has awesome architecture, but it also means there is no 'right' way to send tokens:

  • Basic account (ex. EqywLUZcm73PSWri93X3M5TN62iFMsUPMjvWYUq89dKB) cannot have any tokens
  • For every token contract you need to create separate token account which will be controlled by token contract
  • If a user gives some address to send tokens, it can be:
    • Solana account: which means we need to derive token account address (sol.tokenAddress)
    • Token account: it is possible the token account has not yet been created, in this case we can create it for user; but it is not free, and will cost some fee.
    • token account which was derived in a different way from main solana account

The basic token sending example is:

// Current blockhash
const blockhash = '9xp5Jz2v7ZsE3Xn5SVGkRisjo7h16vzF1ducwhWnc5n9';
// Sol account which is owner of tokenAccount
const fromAccount = 'EqywLUZcm73PSWri93X3M5TN62iFMsUPMjvWYUq89dKB';

const USDT = sol.tokenFromSymbol('USDT', {
  ...sol.COMMON_TOKENS,
  // You can add custom tokens here
});
// Deriving token account address from solana account address
const fromTokenAccount = sol.tokenAddress(USDT.contract, fromAccount);

// Should be valid token account, not solana account
const toTokenAddress = 'FDwkzWGxx6LfCfzcmVVLEk3QUMxNhuFuKEMRwzR4Dtys';

const amount = '64487.8509';
const tokenSimple = sol.createTxComplex(
  fromAccount, // owner of source token account (solana account)
  [
    sol.token.transferChecked({
      source: fromTokenAccount, // Source token account (not solana account)
      amount: sol.parseDecimal(amount, USDT.decimals),
      decimals: USDT.decimals, // decimals of value
      mint: USDT.contract, // token contract address
      owner: fromAccount, // owner of source token account (solana account)
      destination: toTokenAddress,
    }),
  ],
  blockhash
);
console.log(tokenSimple);
/*
  AQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAIFzbQwA0gwj3aPV5V86IflZDr3UfOcT1l5B+N5ekM2bR4k8Yk3xWjUHgJESQnn3AeSSRnm/0Ma4+Hacb/Cr/YApdNUgJV/v62jrND/5dq62kX4pJKhnsRJPG/euDzE+rJezgEOYK/tsicXvWMZL1QUWj+WWjO7gtLHAp6yzh4ggmQG3fbh12Whk9nL4UbO63msHLSF7V9bN5E6jPWFfv8AqYUpqsC9KfFD7lsris1C7YZkNRdSH5qix9nMo2igoP0yAQQEAQMCAAoMlIPGAw8AAAAG
  */

However, in real world you may need more complex logic, like:

// Check if account is valid token account
function isValidTokenAccount(mint, info, owner) {
  if (!info) return false;
  if (info.owner !== sol.TOKEN_PROGRAM) return false;
  try {
    const data = sol.TokenAccount.decode(info.data);
    if (data.mint !== mint) return false;
    if (data.state !== 'initialized') return false;
    if (owner && data.owner !== owner) return false;
    return true;
  } catch (e) {
    return false;
  }
}

async function solAccountInfo(address) {
  // Network code, outside of scope of this package

  // Call rpc with getAccountInfo
  const res = await rpcCall('getAccountInfo', address, {
    encoding: 'base64',
    commitment: 'confirmed',
  });
  if (res.value === null) return undefined;
  const [data, encoding] = res.value.data;
  if (encoding !== 'base64') throw new Error(`SOL: invalid data encoding=${encoding}`);
  return {
    lamports: BigInt(res.value.lamports),
    owner: res.value.owner,
    rentEpoch: res.value.rentEpoch,
    data: base64.decode(data),
    exec: !!res.value.executable,
  };
}

async function createTx(
  fromAddress, // our address (solana account), from which we will send tokens
  toAddress, // user provided address to send tokens in.
  tokenContract, // token contract address
  decimals, // decimals of contract
  amount, // string with amount of tokens to send
  blockhash // current block hash
) {
  // Derive token account from 'from' address
  const fromTokenAccount = sol.tokenAddress(tokenContract, fromAccount);
  // Common token options
  const tokenOpt = {
    source: fromTokenAccount,
    amount: sol.parseDecimal(amount, decimals),
    decimals,
    mint: tokenContract,
    owner: fromAddress, // owner of source
  };
  // If address is on curve, it is probably not 'associated token contract'
  if (sol.isOnCurve(toAddress)) {
    // Derive token account from solana account
    const toTokenAddress = sol.tokenAddress(tokenContract, toAddress);
    const [addrInfo, assocInfo] = await Promise.all([
      solAccountInfo(toAddress),
      solAccountInfo(toTokenAddress),
    ]);
    // toTokenAddress -- is valid token account, we can send here
    if (isValidTokenAccount(tokenContract, assocInfo, toAddress)) {
      // Associted account is ok, send to toTokenAddress
      return sol.createTxComplex(
        fromAddress,
        [sol.token.transferChecked({ ...tokenOpt, destination: toTokenAddress })],
        blockhash
      );
      // toTokenAddress is not valid token account, but toAddress is (even if it is on-curve)
    } else if (isValidTokenAccount(tokenContract, addrInfo)) {
      // account is actually token account, send to address. But since we don't know
      return sol.createTxComplex(
        fromAddress,
        [sol.token.transferChecked({ ...tokenOpt, destination: toAddress })],
        blockhash
      );
      // There is no valid token accounts, but toAddress is basic solana account and we can create
      // token account for it
    } else if (addrInfo && addrInfo.owner === sol.SYS_PROGRAM) {
      // try to create assoc address and send tokens to it
      return sol.createTxComplex(
        fromAddress,
        [
          sol.associatedToken.create({
            source: fromAddress,
            account: toTokenAddress,
            wallet: toAddress,
            mint: tokenContract,
          }),
          sol.token.transferChecked({ ...tokenOpt, destination: toTokenAddress }),
        ],
        blockhash
      );
    } else {
      // We probably can create associated account here, even if account doesn't exists, but it is probably typo and funds will be lost
      throw new Error(
        `SOL.createTx: invalid token destination address, account=${toAddress} doesn't exists, associated=${assocAddress} doesn't exists`
      );
    }
  } else {
    // Address is off-curve: which means it should be associated token account
    const info = await solAccountInfo(toAddress);
    // We cannot create associated address here, since given address is already off-curve
    if (!isValidTokenAccount(tokenContract, info))
      throw new Error(
        `SOL.createTx: invalid token destination address=${toAddress}, off-curve and invalid`
      );
    // Valid token addr, send to it. Send to address
    return sol.createTxComplex(
      fromAddress,
      [sol.token.transferChecked({ ...tokenOpt, destination: toAddress })],
      blockhash
    );
  }
  throw new Error('SOL.createTx: unexpected case');
}

ABI/API

There is no official ABI for Solana (in comparison with ethereum), but it is actually not big deal, since you will need to write API on top of raw ABI anyway. However, we have small DSL here (based on micro-packed), which allows to define ABI easier (look at sol.token definition in index.ts)

License

MIT (c) Paul Miller (https://paulmillr.com), see LICENSE file.

You might also like...

A library with different methods to encode and decode data.

encryption_lib for Deno A library with different methods to encode and decode data. Usage Example: Caesar Cipher import { Caesar } from "https://deno.

Feb 3, 2022

A pure JavaScript QRCode encode and decode library.

QRCode A pure JavaScript QRCode encode and decode library. QRCode guide and demo QRCode guide QRCode example QRCode example use worker Modify from kaz

Nov 28, 2022

An interceptor to validate and decode Pub/Sub push messages for endpoints

NestJS GCP Pub/Sub Interceptor Provides an Interceptor for NestJS to automagically validate and unwrap HTTP push messages from Google Cloud Platform's

Dec 15, 2022

Sample code for ETH Sign In

Sign in with Ethereum Sample code for ETH Sign In at https://acik-kaynak.org/oauth-guzel-peki-ethereumu-denediniz-mi/ The related code for Sign in wit

Jan 2, 2023

Demodal is a browser extension that automatically removes content blocking modals including paywalls, discount offers, promts to sign up or enter your email address and more.

Demodal Demodal is a browser extension that automatically removes content blocking modals including paywalls, discount offers, promts to sign up or en

Jan 4, 2023

Firebase SDK 9 + Google Sign In + Chrome Extension Manifest Version 3 + Webpack

Firebase SDK 9 + Google Sign In + Chrome Extension Manifest Version 3 + Webpack Demo Find this Chrome Extension Setup and working demo here or on Yout

Dec 28, 2022

Development of a Sign Up page form in HTML

Project: Trybewarts Project developed studying in Trybe. Technologies and tools used HTML CSS FlexBox JavaScript Kanban / Scrum Project objective Deve

Jun 14, 2022

An easy and simple way to manage your financial transactions.

An easy and simple way to manage your financial transactions.

MyWallet An easy and simple way to manage your financial transactions. With MyWallet you can track your incomes and expenses and always keep track of

Nov 16, 2022

Front-end for FireNearby service. View recent fires and sign up to receive alerts: caseymm.github.io/fire-nearby

fire-nearby (firenearby service front-end) This application is composed of three pages: Map of recent fires Sign up form to receive alerts About this

Mar 30, 2022
Comments
  • Are there any plans to implement a micro-xmr-signer

    Are there any plans to implement a micro-xmr-signer

    Sorry, I shouldn't have said that here. Since Monero transactions involve Bulletproofs, Ring Confidential Transactions and ring signatures, etc., there is currently no simple library to support transfers, I found a library with only one file https://github.com/CoinSpace/cs-monero-wallet, but it still contains a lot of cpp code dependencies https://github.com/mymonero/monero-core-custom, Do you plan to study monero ring signatures?

    opened by vae520283995 2
Releases(0.1.3)
Owner
Paul Miller
Paul Miller
A plugin for Strapi Headless CMS that provides ability to sign-in/sign-up to an application by link had sent to email.

Strapi PasswordLess Plugin A plugin for Strapi Headless CMS that provides ability to sign-in/sign-up to an application by link had sent to email. A pl

Andrey Kucherenko 51 Dec 12, 2022
A comprehensive user sign-up/sign-in system

Nodejs Login System You want a comprehensive user sign-up/sign-in system I strongly suggest you take a look at this repo. The System Includes Welcome

Furkan Gulsen 8 Aug 1, 2022
🛠 Solana Web3 Tools - A set of tools to improve the user experience on Web3 Solana Frontends.

?? Solana Web3 Tools - A set of tools to improve the user experience on Web3 Solana Frontends.

Holaplex 30 May 21, 2022
solana-base-app is a base level, including most of the common features and wallet connectivity, try using `npx solana-base-app react my-app`

solana-base-app solana-base-app is for Solana beginners to get them up and running fast. To start run : run npx solana-base-app react my-app change th

UjjwalGupta49 33 Dec 27, 2022
✨ Properly credit deps and assets in your projects in a pretty way.

Acknowledgements ✨ Properly credit deps and assets in your projects in a pretty way. About Acknowledgments is a CLI tool that generates a JSON file wi

Clembs 5 Sep 1, 2022
Minimum project with Hono.

Hono minimum project This is a minimum project with Hono[ç‚Ž]. Features Minimum TypeScript Esbuild to build miniflare 2.x to develop Wrangler 2.0 Beta t

Yusuke Wada 60 Dec 25, 2022
💀 A bare-minimum solution to solve CORS problem via proxy API

?? CORS Hijacker A bare-minimum solution to solve CORS problem via proxy API Base URL https://cors-hijacker.vercel.app/ Get Get Basic HTML Endpoint: $

Irfan Maulana 35 Nov 4, 2022
Benefit cards API, create and store card data and log transactions

Valex ?? Benefit cards for companies and employees! ?? Tech used Overview An API to store benefit cards from companies to employees and log transactio

Felipe Ventura 2 Apr 25, 2022
Lucid is a library, which allows you to create Cardano transactions and off-chain code for your Plutus contracts in JavaScript and Node.js.

Lucid is a library, which allows you to create Cardano transactions and off-chain code for your Plutus contracts in JavaScript and Node.js.

Berry 243 Jan 8, 2023
Encode/Decode Bot Protections Payload

decode.antibot.to Open source tools to help you decode/encode sensor data. Features Browser decoding/encoding API decoding/encoding Usage PerimeterX E

null 15 Dec 25, 2022