Solana

This example shows how to use the Builder Vault TSM as a simple Solana wallet using the Solana web3.js library.

It requires that you have access to a Builder Vault that is configured to allow signing with EdDSA keys, and that you have set up a project that can use the Builder Vault SDK as dependency. See one of our Getting Started guides for more on how to do this.

The code first tries to read a master key ID from a file. If the file does not exist, a new master key is generated in the Builder Vault, and the new master key ID is saved to the file for later use. The derived public key for the derivation path m/44/501 is then obtained from the Builder Vault and converted to a Solana base-58 account address.

Then, we initialize a web3.js client. This requires a URL to a Solana node. In the example, we use Blockdaemon’s Ubiquity Native API to get access to a Solana node in the Testnet test network:

// Initialize Solana client
const apiKey = process.env.API_KEY
if (!apiKey) {
  console.log('API_KEY environment variable not set')
  return
}
const solanaNodeUrl = `https://svc.blockdaemon.com/solana/testnet/native?apiKey=${apiKey}`
let connection = new web3.Connection(solanaNodeUrl, "confirmed")

If you use Ubiquity, you need to obtain a Ubiquity API key from Blockdaemon and make sure that it is available as the environment variable API_KEY, for example by running:

export API_KEY=put_your_ubiquity_api_key_here

Alternatively, you can modify the example so that it connects to a local Solana node that you host yourself or use another third-party Solana API provider instead of Blockdaemon Ubiquity.

Once connected to the Solana network, we use the web3.js client to get the balance of the account defined by the address m/44/501, as well as the latest block hash.

Then, we generate an unsigned transaction that sends 0.000001 SOL to a destination address. In the example, you can specify a different address or amount.

We create the payload to be signed, sign it using the Builder Vault, and construct the signed transaction. Finally, we use the web3.js client to publish the signed transaction to the Solana network.

The example uses the BIP32 derivation path m/44/501 . See our section about key derivation for more about this. See the section about key import if you want to migrate a key from an external wallet, such as Metamask, to the TSM.

📘

Note:

When you run this example the first time, a new random account will be created, and the balance will be 0 SOL. This will cause the program to print out the account address and stop. To actually transfer funds, you will need to first insert some test funds on the account address and then run the program again. Use a Testnet faucet such as https://solfaucet.com

For the full sample code, please see below:

const { TSMClient, Configuration, SessionConfig, curves } = require("@sepior/tsmsdkv2");
const fs = require("node:fs");
const web3 = require("@solana/web3.js");
const bs58 = require('bs58').default;

async function main() {
  
  // destination address
  const destAddressHex = "4ETf86tK7b4W72f27kNLJLgRWi9UfJjgH4koHGUXMFtn"  // SOL testnet Faucet
  const amountLamports = 1000 // default 0.000001 SOL

  // Set buildervault endpoints
  const config0 = await new Configuration("http://localhost:8500")
  await config0.withAPIKeyAuthentication("apikey0")

  const config1 = await new Configuration("http://localhost:8501")
  await config1.withAPIKeyAuthentication("apikey1")

  // Create clients for two MPC nodes
  const clients = [
    await TSMClient.withConfiguration(config0),
    await TSMClient.withConfiguration(config1),
  ]

  const threshold = 1 // The security threshold for this key

  const masterKeyId = await getKeyId(clients, threshold, "key.txt")

  // Get the public key for the derived key m/44/501

  const derivationPath = new Uint32Array([44, 501])

  const publickeys = []

  for (const [_, client] of clients.entries()) {
    const eddsaApi = client.Schnorr()

    publickeys.push(
      await eddsaApi.publicKey(masterKeyId, derivationPath)
    )
  }

  // Validate public keys

  for (let i = 1; i < publickeys.length; i++) {
      if (Buffer.compare(publickeys[0], publickeys[i]) !== 0) {
        throw Error("public keys do not match")
      }
    }

  // Convert PublicKey to Base58 Solana address

  const compressedPublicKey = await clients[0].Utils().pkixPublicKeyToCompressedPoint(publickeys[0])
  const address = new web3.PublicKey(bs58.encode(compressedPublicKey))
  console.log(`Solana address of derived key m/44/501: ${address}`)

  // Initialize Solana client
  const apiKey = process.env.API_KEY

  if (!apiKey) {
      console.log('API_KEY environment variable not set')
      return
  }

  //const solanaNodeUrl = `https://svc.blockdaemon.com/solana/testnet/native?apiKey=${apiKey}`
  const solanaNodeUrl = `http://svc.blockdaemon.com/`  
 
  let connection = new web3.Connection(solanaNodeUrl, "confirmed")
  
  const balance = await connection.getBalance(address);

  console.log(`Balance at account m/44/501 ${address}: ${balance}`)

  if (balance <= 0) {
      console.log(`
          Insufficient funds
          Insert additional funds at address ${address} e.g. by visiting https://solfaucet.com
          Then run this program again. 
      `)
      return
  }

  let toAccount = new web3.PublicKey(destAddressHex)
      
  // Send and confirm transaction

  let latestBlockhash = await connection.getLatestBlockhash()
  let transaction = new web3.Transaction({
      recentBlockhash: latestBlockhash.blockhash,
      feePayer: address,
  })
  transaction.add(
      web3.SystemProgram.transfer({
          fromPubkey: address,
          toPubkey: toAccount,
          lamports: amountLamports,
      }),
  )

  const messageToSign = transaction.serializeMessage()

  // Use the TSM to sign via the derived key m/44/501

  console.log(`Signing transaction using Builder Vault: ${messageToSign.toString('hex')}`)
  
  const partialSignatures = []

  const sessionConfig = await SessionConfig.newStaticSessionConfig(
    await SessionConfig.GenerateSessionID(),
    clients.length
  )

  const partialSignaturePromises = []

  for (const [_, client] of clients.entries()) {
    const func = async () => {
      const eddsaApi = client.Schnorr()

      const partialSignResult = await eddsaApi.sign(
        sessionConfig,
        masterKeyId,
        chainPath,
        messageToSign
      )

      partialSignatures.push(partialSignResult)
    }

    partialSignaturePromises.push(func())
  }

  await Promise.all(partialSignaturePromises)

  const eddsaApi = clients[0].Schnorr()

  const signature = await eddsaApi.finalizeSignature(
    messageToSign,
    partialSignatures
  )
  
  transaction.addSignature(address, signature.signature)
  console.log(`The signatures were verified: ${transaction.verifySignatures()}`)

  // Send the transaction

  console.log("Raw signed message base64:", transaction.serialize().toString("base64"))
  const txid = await web3.sendAndConfirmRawTransaction(connection, transaction.serialize(),
  {
    signature: bs58.encode(signature.signature),
    blockhash: latestBlockhash.blockhash,
    lastValidBlockHeight: latestBlockhash.lastValidBlockHeight,
  })

  console.log(`Confirmed transaction: https://explorer.solana.com/tx/${txid}/?cluster=testnet`)
}

async function getKeyId(clients, threshold, keyfile) {
  if (fs.existsSync(keyfile)) {
    const data = fs.readFileSync(keyfile).toString().trim()

    console.log(`Read key with ID ${data} from file ${keyfile}`)

    return data
  }

  const sessionConfig = await SessionConfig.newStaticSessionConfig(
    await SessionConfig.GenerateSessionID(),
    clients.length
  )

  const masterKeyIds = []

  clients.forEach(() => masterKeyIds.push(""))

  const promises = []

  for (const [i, client] of clients.entries()) {
    const func = async () => {
      const eddsaApi = client.Schnorr()

      masterKeyIds[i] = await eddsaApi.generateKey(sessionConfig, threshold,curves.ED25519)
    }

    promises.push(func())
  }

  await Promise.all(promises)

  for (let i = 1; i < masterKeyIds.length; i++) {
    if (masterKeyIds[0] !== masterKeyIds[i]) {
      throw Error("Key ids do not match")
    }
  }

  const keyID = masterKeyIds[0]

  console.log(`Generated master key (m) with ID ${keyID}  saving to file ${keyfile}`)

  fs.writeFileSync(keyfile, `${keyID}\n`)

  return keyID
}

main()