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 theHash 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 theupdate
method exposed bySHA3
, then return the result from thedigest
method:
const hashMessageHex = (msgHex) => {
const sha = new SHA3(256);
sha.update(Buffer.from(msgHex, "hex"));
return sha.digest();
};
- 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");
};
- 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 asigning 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
};
}
};
};
✨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"
});
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 });
};
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;
};
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();
})();
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"}
It wasn’t that hard in the end, right? 😉
Until next time! 👋
Resources
- Full Source Code - https://codesandbox.io/s/dev-to-14-mutate-with-pkey-d0jsj0
- Package - SHA3 - https://www.npmjs.com/package/sha3
- Package - elliptic - https://www.npmjs.com/package/elliptic
- FCL - Authorization Function - https://docs.onflow.org/fcl/reference/api/#authorization-function
Other resources you might find useful:
- Flow Docs Site - https://docs.onflow.org/ - More detailed information about Flow blockchain and how to interact with it
- Flow Portal - https://flow.com/ - your entry point to Flow
- FCL JS - https://github.com/onflow/fcl-js - Source code and ability to contribute to the FCL JS library
- Cadence - https://docs.onflow.org/cadence/ - Introduction to Cadence
- Codesandbox - https://codesandbox.io - An amazing in-browser IDE enabling quick prototyping
Top comments (0)