DEV Community

Max Daunarovich for Flow Blockchain

Posted on

Build on Flow | Learn FCL - 14. How to Mutate Chain State by Signing Transactions with a Private Key

Preface

Last time we covered how you can sign transactions with Lilico and Blocto wallets from the comfort of your browser. But it’s quite common that you might need to do the same task on a backend (i.e. server-side), where the browser is not available.

Fear not, my friend as this is exactly what we will teach you. After working through materials of this article you will know how to:

  • fill different roles for your transaction
  • get a basic understanding of the multisig process
  • sign a transaction with your private key

Step 1 - Installation

Add "@onflow/fcl", elliptic and sha3 packages as project dependencies.

Step 2 - Create a Signer

Let’s create a new file and call it signer.js. The file can have any name, we are just picking something with a semantic meaning here. Our signer will need to have 3 functions.

  • First one will be called hashMessageHex- it will be used to hash the transaction message using a SHA3 algorithm. The reason we are using SHA3 is because we’ve picked it as the Hash Algorithm during account creation on the Testnet Faucet outlined in one of the previous articles. The function itself is pretty straightforward. We will take the transaction message, specifically packed and represented as hex string and feed it into the update method exposed by SHA3 , then return the result from the digest method:
const hashMessageHex = (msgHex) => {
  const sha = new SHA3(256);
  sha.update(Buffer.from(msgHex, "hex"));
  return sha.digest();
};
Enter fullscreen mode Exit fullscreen mode
  • The next function will call signWithKey - we will use it to sign our hashed transaction message with a private key. Now this IS a really complicated topic and let’s say you will cut a lot of corners if you simply copy-paste this one and do your crypto research later, mkey? 😅 tldr: we will use a Elliptic Curve Digital Signature Algorithm in order to produce our signature:
const signWithKey = (privateKey, msgHex) => {
  const key = curve.keyFromPrivate(Buffer.from(privateKey, "hex"));
  const sig = key.sign(hashMessageHex(msgHex));

  const n = 32;
  const r = sig.r.toArrayLike(Buffer, "be", n);
  const s = sig.s.toArrayLike(Buffer, "be", n);

return Buffer.concat([r, s]).toString("hex");
};
Enter fullscreen mode Exit fullscreen mode
  • The last one will be a signer function, which will be used as Authorization Function - function which produce the information of the user that is going to sign and a signing function to use the information to produce a signature.
export const signer = async (account) => {
  // We are hard coding these values here, but you can pass those values from outside as well.
  // For example, you can create curried function: 
  // const signer = (keyId, accountAdddress, pkey) => (account) => {...}
  // and then create multiple signers for different key indices 

  const keyId = Number(0); // always ensure that your keyId is a number not a string
  const accountAddress = "0x5593df7d286bcdb8";
  const pkey =
    "248f1ea7b4a058c39dcc97d91e6a5d0aa7afbc931428561b6efbc7bd0f5e0875";

  // authorization function need to return an account
  return {
    ...account, // bunch of defaults in here, we want to overload some of them though
    tempId: `${accountAddress}-${keyId}`, // tempIds are more of an advanced topic, for 99% of the times where you know the address and keyId you will want it to be a unique string per that address and keyId
    addr: sansPrefix(accountAddress), // the address of the signatory, currently it needs to be without a prefix right now
    keyId // this is the keyId for the accounts registered key that will be used to sign, make extra sure this is a number and not a string

    // This is where magic happens! ✨
    signingFunction: async (signable) => {
      // Singing functions are passed a signable and need to return a composite signature
      // signable.message is a hex string of what needs to be signed.
      const signature = await signWithKey(pkey, signable.message);

      return {
        addr: withPrefix(accountAddress), // needs to be the same as the account.addr but this time with a prefix, eventually they will both be with a prefix
        keyId, // needs to be the same as account.keyId, once again make sure its a number and not a string
        signature // this needs to be a hex string of the signature, where signable.message is the hex value that needs to be signed
      };
    }
  };
};
Enter fullscreen mode Exit fullscreen mode

✨Please, note, that signingFunction is asynchronous, meaning that it can use Promises inside of its body to get a signature out of extension or remote server. Which is super handy, when you want to handle gas fees for your users 😉

Step 3 - FCL Setup

import { config, query, mutate, tx } from "@onflow/fcl";
import { signer } from "./signer"

// Contrary to our wallet signing example, we don't need most of it in our config now
// so we'll get back to simple version
config({
  "accessNode.api": "https://rest-testnet.onflow.org",
  "0xBasic": "0xafabe20e55e9ceb6"
});
Enter fullscreen mode Exit fullscreen mode

Step 4 - Implement readCounter

We will copy the same function from our previous article, because it will behave absolutely the same:

const readCounter = async () => {
  const cadence = `
    import Basic from 0xBasic

    pub fun main():UInt{
      return Basic.counter
    }
  `;
  const counter = await query({ cadence });
  console.log({ counter });
};
Enter fullscreen mode Exit fullscreen mode

Step 5 - Implement shiftCounter

shiftCounter function will be almost identical as well. The only difference is that this time we will fill all the roles, using our signer function. On top of that we will log how much time it took to seal the transaction, using console.time and console.timeEnd methods:

const shiftCounter = async (value) => {
  console.log("%cSigning Transaction", `color: teal`);

  // Our Cadence code. Notice the use of alias here
  const cadence = `
    import Basic from 0xBasic

    transaction(shift: UInt8){
      prepare(signer: AuthAccount){
        Basic.incrementCounterBy(shift)
      }
    }
  `;

  // List of arguments
  const args = (arg, t) => [arg(value.toString(), t.UInt8)];
  const proposer = signer;
  const payer = signer;
  const authorizations = [signer];

  // "mutate" method will return us transaction id
  const txId = await mutate({
    cadence,
    args,
    proposer,
    payer,
    authorizations,
    limit: 999
  });

  console.log(`Submitted transaction ${txId} to the network`);
  console.log("%cWaiting for transaction to be sealed...", `color: teal`);

  const label = "Transaction Sealing Time";
  console.time(label);

    // We will use transaction id in order to "subscribe" to it's state change and get the details
  // of the transaction
  const txDetails = await tx(txId).onceSealed();

  console.timeEnd(label);
  return txDetails;
};
Enter fullscreen mode Exit fullscreen mode

Finally

Let's add an IIFE at the end of the file and populate it with methods we have just defined:

(async () => {
  console.clear();
  await readCounter();

  const txDetails = await shiftCounter(12);
  console.log({ txDetails });

  // we will call "readCounter" function second time to ensure 
  // that value of counter has changed
  await readCounter();
})();
Enter fullscreen mode Exit fullscreen mode

After the dust settles your console should have similar output:

{counter: "655"}
Signing Transaction
Submitted transaction d88e98687dd98f7597aca9afaf3daaba788f644f90003c9b144bfa13440fd9ab to the network 
Waiting for transaction to be sealed...
Transaction Sealing Time: 14749.60000000149ms 
{txDetails: Object}
{counter: "667"}
Enter fullscreen mode Exit fullscreen mode

https://c.tenor.com/eIV3XiQyw4AAAAAC/you-did-it-willy-wonka-and-the-chocolate-factory.gif

It wasn’t that hard in the end, right? 😉

Until next time! 👋

Resources

Other resources you might find useful:

Top comments (0)