npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

micro-sol-signer

v0.4.2

Published

Create, sign & decode Solana transactions with minimum deps

Downloads

697

Readme

micro-sol-signer

Create, sign & decode Solana transactions with minimum deps.

  • 🔓 Secure: minimum deps, audited noble cryptography
  • 🔻 Tree-shaking-friendly: use only what's necessary, other code won't be included
  • 🌍 No network code for simplified auditing and offline usage
  • ✍️ Create, sign and decode transactions
  • 🪶 800 lines of code

Check out all web3 utility libraries: ETH, BTC, SOL, tx-tor-broadcaster

Usage

npm install micro-sol-signer

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

Method summary:

function formatPrivate(privateKey: Bytes, format: 'base58' | 'hex' | 'array' = 'base');
function getPublicKey(privateKey: Bytes);
function getAddress(privateKey: Bytes);
function getAddressFromPublicKey(publicKey: Bytes);
function signTx(privateKey: Bytes, data: TxData): Promise<[string, string]>;
function verifyTx(tx: TxData);
function createTx(from: string, to: string, amount: string, _fee: bigint, blockhash: string);
function createTxComplex(address: string, instructions: Instruction[], blockhash: string);
function defineProgram<T extends Record<string, MethodHint<any>>>;
function isOnCurve(bytes: Bytes | string);
function parseInstruction(instr: Instruction, tl: TokenList): any;
function programAddress(program: string, ...seeds: Bytes[]);
function tokenAddress(mint: string, owner: string, allowOffCurveOwner = false);
function tokenFromSymbol(symbol: string, tokens = COMMON_TOKENS);
function validateAddress(address: string);

There are other variables such as SYS_PROGRAM, which are also exported. Specific features:

Create and sign simple transaction

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

// Format private key
const privFormatted = 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] = 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 a big deal, since you will need to write API on top of raw ABI anyway. We have small DSL, 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.