Deploying Contracts and Signing Transaction from Node

Some of our friends are wanting to interact with the chain directly from node. This could be for various reasons but most of the time it seems its all about contract deployment or having a backend service do some sort of action on the applications behalf (minting, air drops etc). Going to go fairly deep here so you can understand whats actually going on when we send a transaction, so hopefully you can replace pieces as needed.

I am going to make a couple of assumptions, to make this easier for me to explain. They are as follows:

  • I already have a Flow Account
  • The Flow Account has a Public Key attached to it in which I have in my possession the corresponding Private Key
  • I will be using SHA3_256 as my hashing algorithm
  • My key is using the ECDSA_P256 curve
  • When I added my public key to the Flow Account I did so with a full weight (1000) and the above hash and curve

You can use what ever hashing and curves you want (as long as Flow Supports them, which is currently limited) but all of the coding examples below will work under the above assumption of a SHA3_256 hash and ECDSA_P256 curve.

Let’s get started.

A transaction is a bundle of data we send to the chain, I like to think of it like an onion, and to quote Shrek, onions have layers. At its core is the data about what we want to do. Sending just the core to the chain isn’t going to do anything, we first need to wrap it in its first layer of signatures, these signatures are for the authorizers (AuthAccounts in the prepare statement of a transaction) and the proposer (acts in a similar fashion to a nonce). Then we need to take that core, and the first layer of signatures and encode those, sign that encoded value to produce our outer most layer, if we send this to chain, and all our signatures are valid, and the proposers sequence number hasn’t changed, and the ref block is still within a certain range, the chain will probably accept our requested changes. This stuff is actually really complicated, but luckily for you in the JS-SDK we have abstracted this to the point where you really only need a single function to perform any and all variations of signatures required, we call this function an Authorization Function

Fun fact: FCLs fcl.currentUser().authorization is just an authorization function that while in the browser, knows how to discover and communicate with wallets. Sending the Signable to the wallet and receiving a CompositeSignature

In the following examples I will be using the address 0xe704fadec500fa15 and testnet. At the time of writing this that Flow Account has the properties that match the above criteria and assumptions, so you can use it as a reference. The registered public key that is ECDSA_P256 and SHA3_256 has a keyId of 0.

Since we know the account we and key we want to be using, I will be hard coding in the values, so you can see what they should look like, if you were to adopt some of this code, I would recommend being able to dynamically change these values or pull them from ENV variables.

Our first Authorizations Function

// File: ./authz.js
import {sign} from "./sign.js"

export async function authz (account) {
  return {
    ...account,                               // there is some stuff already in here, we need it
    addr: "0xe704fadec500fa15", // which flow account is going to be doing the signing
    keyId: 0,                                  // says which key we want to do the signing with
    // How to get a signature
    signingFunction: async (signable) => ({
      f_type: "CompositeSignature",
      f_vsn: "1.0.0",
      addr: "0xe704fadec500fa15", // In this case it should be the same as above
      keyId: 0,                                  // In this case it should be the same as above
      signature: sign(process.env.PRIVATE_KEY, signable.message)
    })
  }
}

The above authz function can now me passed into the JS-SDKs or FCLs proposer, payer or authorizations functions. But we need to talk about that imported sign function. It needs to be passed the private key that corresponds with public key with a keyId of 0 for the Flow Account 0xe704fadec500fa15, the second argument is going to be the encoded value that needs to be signed, it will be in a hex representation of the binary array.

Inside the sign function it will need to convert the signable.message to a binary array, hash it using the registered hashing algorithm, it then needs to sign that binary array with the process.env.PRIVATE_KEY and then convert the signature into a hex value.

Lets start with the hashing. Above we mentioned that we are using SHA3_256, the below function will do what we need:

// File: ./hash.js
import {SHA3} from "sha3"

export function hash(message) {
  const sha = snew SHA3(256)
  sha.update(Buffer.from(message, "hex"))
  return sha.digest()
}

Next we can sign the output of that hash function in the following function:

// File: ./sign.js
import {ec as EC} from "elliptic"
import {hash} from "./hash"

export const sign(privateKey, message) {
  const key = ex.keyFromPrivate(Buffer.from(privateKey, "hex"))
  const sig = key.sign(hashMsgHex(message))
  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")
}

When we want to interact with privately owned things on Flow we need access to the AuthAccount of the owners of those things. In a transaction these owners come from us authorizing the transaction using the above Authorization Function. If we want to deploy a contract we similarly need access to the accounts AuthAccount. Before we start talking about deploying contracts we should make sure we can submit valid transactions to Flow, next is a minimal viable transaction that requires a single AuthAccount, if we can successfully do this transaction we can deploy contracts and mess around with resources.

// File: ./mvt.tx.js
import {send} from "@onflow/sdk-send"
import {decode} from "@onflow/sdk-decode"
import {proposer} from "@onflow/sdk-build-proposer"
import {payer} from "@onflow/sdk-build-payer"
import {authorizations} from "@onflow/sdk-build-authorizations"

import {authz} from "./authz"

var txId = await send([
  transaction`
    transaction {
      prepare(acct: AuthAccount) {
        log(acct)
      }
    }
  `,
  proposer(authz), // used as a nonce
  payer(authz),    // means your account 0xe704fadec500fa15 will be paying for the transaction
  authorizations([
    authz, // means the first AuthAccount passed into the prepare will be for 0xe704fadec500fa15
  ]),
]).then(decode)

console.log(`tx[${txId}]: https://flow-view-source.com/testnet/tx/${txId}`) // see the status of the transaction

Once we have that transaction working let’s deploy a contract, because deploying a contract is just another transaction like any other transaction.

// File: ./deply-contract.tx.js

// new imports
import {args, arg} from "@onflow/sdk-build-arguments"
import * as t from "@onflow/types"

var contract = `
pub contract Foo {
  pub fun woot(rawr: String) {
    log(rawr)
  }
}
`

var CODE = Buffer.from(contract, "utf8").toString("hex")

var txId = await send([
  transaction`
    transaction(code: String) {
      prepare(acct: AuthAccount) {
        acct.contracts.add(name: "Foo", code: code.decodeHex())
      }
    }
  `,
  proposer(authz),
  payer(authz),
  authorizations([authz]),
  args([
    arg(CODE, t.String) // the `code` in `transaction(code: String) {`
  ]),
])

console.log(`tx[${txId}]: https://flow-view-source.com/testnet/tx/${txId}`) // see the status of the transaction

To update the contract we need to change: acct.contracts.add(name: "Foo", code: code.decodeHex()) to acct.contracts.update__experimental(name: "Foo", code: code.decodeHex()) in the above code.

// File: ./deply-contract.tx.js

// new imports
import {args, arg} from "@onflow/sdk-build-arguments"
import * as t from "@onflow/types"

var contract = `
pub contract Foo {
  pub fun woot(rawr: String) {
    log(rawr)
  }
}
`

var CODE = Buffer.from(contract, "utf8").toString("hex")

var txId = await send([
  transaction`
    transaction(code: String) {
      prepare(acct: AuthAccount) {
        acct.contracts.update_experimental(name: "Foo", code: code.decodeHex())
      }
    }
  `,
  proposer(authz),
  payer(authz),
  authorizations([authz]),
  args([
    arg(CODE, t.String) // the `code` in `transaction(code: String) {`
  ]),
])

console.log(`tx[${txId}]: https://flow-view-source.com/testnet/tx/${txId}`) // see the status of the transaction

And now that we have a contract deployed (and can update it), we can interact with it using another transaction.

var txId = await send([
  transaction`
    import Foo from 0xe704fadec500fa15
    transaction(value: String) {
      prepare(acct: AuthAccount) {
        Foo.woot(value)
      }
    }
  `,
  proposer(authz),
  payer(authz),
  authorizations([authz]),
  args([
    arg("I will be logged", t.String) // the `value` in `transaction(value: String) {`
  ]),
])

console.log(`tx[${txId}]: https://flow-view-source.com/testnet/tx/${txId}`) // see the status of the transaction

We should now have a contract deployed and be able to interact with it from any node application that has access to our Private Key.

5 Likes

In the above the following code:

// File: ./sign.js
import {ec as EC} from "elliptic"
import {hash} from "./hash"

export const sign(privateKey, message) {
  const key = ex.keyFromPrivate(Buffer.from(privateKey, "hex"))
  const sig = key.sign(hashMsgHex(message))
  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")
}

Should in fact be:

// File: ./sign.js
import {ex as EC} from "elliptic" // ec -> ex
import {hash} from "./hash"

export const sign(privateKey, message) {
  const key = ex.keyFromPrivate(Buffer.from(privateKey, "hex"))
  const sig = key.sign(hash(message)) // hashMsgHex -> hash
  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 transaction function referenced in a couple of the code snippets in the original post is from import {transaction} from "@onflow/sdk-build-transaction"