Request for best practices re: wallet / account creation server side

Hello!

I’m looking to determine the best practice for creating flow accounts for users server side.

The backend of my app is built in node.js and deployed on Vercel. I need to create flow accounts / wallets for users when they sign up. I already have built out the auth using Auth0. I would just like to generate a flow account address / private key so I can manage a wallet on behalf of each user. I will be storing the user’s private keys for them.

I was able to dig through the different packages in the Flow Javascript SDK and put together the following solution.

const fcl = require('@onflow/fcl')
const t = require('@onflow/types')
const EC = require('elliptic').ec
const ec = new EC('p256')
const rlp = require('rlp')
const { SHA3 } = require('sha3')

const invariant = (fact, msg, ...rest) => {
  if (!fact) {
    const error = new Error(`INVARIANT ${msg}`)
    error.stack = error.stack
      .split('\n')
      .filter(d => !/at invariant/.test(d))
      .join('\n')
    console.error('\n\n---\n\n', error, '\n\n', ...rest, '\n\n---\n\n')
    throw error
  }
}

const get = (scope, path, fallback) => {
  if (typeof path === 'string') return get(scope, path.split('/'), fallback)
  if (!path.length) return scope
  try {
    const [head, ...rest] = path
    return get(scope[head], rest, fallback)
  } catch (_error) {
    return fallback
  }
}

const CONTRACT = `
  access(all) contract Noop {}
`

const PK = '55bdf531ce04af41caa0adc77b4274a4a024e6e0cb92c6c24e69c53c5df5ac47'

const SERVICE_ADDR = 'f8d6e0586b0a20c7'

// current cadded AuthAccount constructor (what you use to create an account on flow)
// requires a public key to be in a certain format. That format is an rlp encoded value
// that encodes the key itself, what curve it uses, how the signed values are hashed
// and the keys weight.
const encodePublicKeyForFlow = publicKey =>
  rlp
    .encode([
      Buffer.from(publicKey, 'hex'), // publicKey hex to binary
      2, // P256 per https://github.com/onflow/flow/blob/master/docs/accounts-and-keys.md#supported-signature--hash-algorithms
      3, // SHA3-256 per https://github.com/onflow/flow/blob/master/docs/accounts-and-keys.md#supported-signature--hash-algorithms
      1000, // give key full weight
    ])
    .toString('hex')

const signWithKey = (privateKey, msgHex) => {
  const key = ec.keyFromPrivate(Buffer.from(privateKey, 'hex'))
  const sig = key.sign(hashMsgHex(msgHex))
  const n = 32 // half of signature length?
  const r = sig.r.toArrayLike(Buffer, 'be', n)
  const s = sig.s.toArrayLike(Buffer, 'be', n)
  return Buffer.concat([r, s]).toString('hex')
}

const hashMsgHex = msgHex => {
  const sha = new SHA3(256)
  sha.update(Buffer.from(msgHex, 'hex'))
  return sha.digest()
}

const genKeys = () => {
  const keys = ec.genKeyPair()
  const privateKey = keys.getPrivate('hex')
  const publicKey = keys.getPublic('hex').replace(/^04/, '')
  return {
    publicKey,
    privateKey,
    flowKey: encodePublicKeyForFlow(publicKey),
  }
}

// Will be handled by fcl.user(addr).info()
const getAccount = async addr => {
  const { account } = await fcl.send([fcl.getAccount(addr)])
  return account
}

const authorization = async (account = {}) => {
  const user = await getAccount(SERVICE_ADDR)
  const key = user.keys[0]

  let sequenceNum
  if (account.role.proposer) sequenceNum = key.sequenceNumber

  const signingFunction = async data => {
    return {
      addr: user.address,
      keyId: key.index,
      signature: signWithKey(PK, data.message),
    }
  }

  return {
    ...account,
    addr: user.address,
    keyId: key.index,
    sequenceNum,
    signature: account.signature || null,
    signingFunction,
    resolve: null,
    roles: account.roles,
  }
}

const createFlowAccount = async (contract = CONTRACT) => {
  const keys = await genKeys()

  const response = await fcl.send([
    fcl.transaction`
      transaction {
        let payer: AuthAccount
        prepare(payer: AuthAccount) {
          self.payer = payer
        }
        execute {
          let account = AuthAccount(payer: self.payer)
          account.addPublicKey("${p => p.publicKey}".decodeHex())
          account.setCode("${p => p.code}".decodeHex())
        }
      }
    `,
    fcl.proposer(authorization),
    fcl.authorizations([authorization]),
    fcl.payer(authorization),
    fcl.params([
      fcl.param(keys.flowKey, t.Identity, 'publicKey'),
      fcl.param(
        Buffer.from(contract, 'utf8').toString('hex'),
        t.Identity,
        'code',
      ),
    ]),
  ])

  const { events } = await fcl.tx(response).onceSealed()
  const accountCreatedEvent = events.find(d => d.type === 'flow.AccountCreated')
  invariant(accountCreatedEvent, 'No flow.AccountCreated found', events)
  let addr = accountCreatedEvent.data.address
  // a standardized string format for addresses is coming soon
  // our aim is to make them as small as possible while making them unambiguous
  addr = addr.replace(/^0x/, '')
  invariant(addr, 'an address is required')

  const account = await getAccount(addr)
  const key = account.keys.find(d => d.publicKey === keys.publicKey)
  invariant(
    key,
    'could not find provided public key in on-chain flow account keys',
  )

  return {
    addr,
    publicKey: keys.publicKey,
    privateKey: keys.privateKey,
    keyId: key.index,
  }
}

const run = async () => {
  const account = await createFlowAccount()
  console.log('account', account)
}

run()

I am able to run the code in a node environment and create accounts by sending transactions to the flow emulator.

Can I do the above in a production environment against the flow testnet or mainnet?

Should I be using the Flow Go SDK instead? I found a specific “creating an account” section in the readme (https://github.com/onflow/flow-go-sdk#creating-an-account) which there isn’t in the JS SDK readme.

I would prefer to use the JS SDK to do this as I already have my API built and deployed in Node.

To do this in Go I would need to create a separate standalone Go server to make calls to from the Node API, which I would prefer not to do.

Additionally, if I do use a JS solution as above. What would be the best way to generate the private key from a seed phrase like is done with the GO SDK?

Is there any other data I need to create a flow account for a user? I imagine it’s just address, public, and private key.

How can I get access to the testnet so I can test my code in a deployed environment (e.g. Vercel) where I cannot run the flow emulator.

Thanks!

Tagging @qvvg
There was also another discussion on Go SDK vs JS SDK Differences between js and go SDK

Hi @lukehamilton,

Compared to the GoSDK it looks like you have all the steps figured out except the key generation.

The keys are generated using the ECDSA signing algorithm, on one of this 2 curves: P256 or secp256k1. SHA3 is used to keep the data fixed-length. If the JS SDK does not have a way to create keys you’ll need to import a JS crypto library.

IMO if you find that, there is no need to use the GoSDK for account creation.

I’ll let someone else answer the testnet/mainnet access request but in general if it works on the emulator it will work on the testnet.

The data you need to create an account:

  • a public key to manage the new account. private key is not needed in the creation but the user should have it otherwise he won’t be able to access his account
  • the code that the user wants to deploy under his account, if any
  • the address, public key and private key of an account that will pay for the transaction, in examples this account is the service account. If you don’t want to provide a private key to your code, you’ll need to create the payload for this transaction and have that signed externally and provide the public key and the signature.

:wave: Hi @lukehamilton,

So in the following, please keep in mind, I am completely ignorant about the needs of your project. I do not know if the solution you have come to is because of a lack of us providing something or is in fact the ideal solution to your problem. That being said…

As the person who wrote most of the code you posted there, I see these sorts of approaches quite a bit. In our ideal situation, you as the dapp developer should never have to know or even care about keys/accounts, and unless you are building something like a wallet provider, your time and effort are probably best used focusing on that magic stuff that makes your dapp unique. So then where should your users get accounts, well, you should be able to build your application as if they already have them.

Using FCL you can do things like this:

// the callback will reactively be called anytime the users data updates
fcl.currentUser().subscribe(user => console.log("User Data Changed", user))

// This function will trigger the authentication process
fcl.authenticate()

// Then authorize a transaction as the current user
fcl.send([
  fcl.transaction`
    transaction {
      prepare(acct: AuthAccount) {
        log(acct.address)
      }
    }
  `,
  fcl.proposer(fcl.currentUser().authorization),
  fcl.authorizations([
    fcl.currentUser().authorization,
  ]),
  fcl.payer(fcl.currentUser().authorization),
])

The idea here is that during local development you point fcl at the dev-wallet. In the testnet, you point it at the testnet version of the wallet, and then in production you remove the config completely and it falls back on our chain powered discovery process. One of the principal design considerations we have when we are working on FCL is that “Users own and are in control of their information” and “Users can bring their identity with them using any FCL compatible wallet”. We are working with a bunch of wallet providers directly (Dapper, Magic/Formatic, TryCrypto, …).

When you actually need to create accounts.

Next comes the case where you actually do want to be creating accounts and having keys and all that. There are a couple things that you need to keep in mind with this, mainly FLOW ACCOUNTS ARE NOT FREE.

The Cadence transaction that creates an account looks like this:

transaction(flowKey: String, contract: String) {
  prepare(acct: AuthAccount) {
    newAccount = AuthAccount(payer: acct)
    newAccount.addPublicKey(flowKey.decodeHex())
    newAccount.setCode(contract.decodeHex())
  }
}

The important thing in there is the part newAccount = AuthAccount(payer: acct). Not only is it saying to create a new account that payer: acct bit is saying to take Flow Tokens from acct and put them as a storage deposit on the newAccount. This is an added expense on top of paying for the transaction.

If you are really needing to do this sort of thing can you reach out to me on discord in the flow-js-sdk channel. I am qvvg in there, we probably have to have a decent amount of communication and discussion we need to do. For example on testnet for a Flow account to even create accounts, their address needs to be added to an allow list. There are a myriad of things like this we need to help navigate through as its not well documented yet and still very susceptible to change.

Update: @lukehamilton and I had a chat in Discord. Their use case (Being largely a Native Mobile App first experience) seems to warrant the creation of accounts. While above I did stress that Accounts aren’t free, these costs should be quite minimal, and was mostly worried about them going in a direction they thought was free but then turned out to cost them.