Writing reliably to the chain (push guarantee)

In this tutorial, we will demonstrate you how to setup eosjs to push a transaction through dfuse API Push Guaranteed endpoint.

The endpoint is 100% compatible with EOS push transaction endpoint. The single difference is that, when the required headers are present, you’ll get the response back only when your transaction has reach a block, passed 1, 2 or 3 handoffs or is irreversible (depending on value passed in header X-Eos-Push-Guarantee).

You can check the Push Guaranteed Reference for all details on the feature.

You are going to follow a quick guide to send tokens from one account to another one on the Jungle 2.0 Test Network using dfuse push_transaction with push guaranteed activated.

For that, you will need an account on the Jungle 2.0 Test Network with some tokens in it (see https://jungletestnet.io/ Get Started tutorial to create an account there and get some coins).

We are going to use TypeScript in this example, it should be quite easy to convert the code in here to pure JavaScript if it’s what you targets.

1. Project Set Up & Dependencies

Foremost, be sure to have a valid dfuse API key at hand.

Get an API key

  1. Create your account on https://app.dfuse.io
  2. Click “Create New Key” and give it a name, a category. In the case of a web key give it an “Origin” value.
See Authentication for further details

Let’s create the project and add the required dependencies.


mkdir -p example-push-guaranteed
cd example-push-guaranteed
npm init -y
npm install @dfuse/client eosjs node-fetch text-encoding
npm install --save-dev typescript ts-node @types/node @types/node-fetch @types/text-encoding

mkdir -p example-push-guaranteed
cd example-push-guaranteed
yarn init -y
yarn add @dfuse/client eosjs node-fetch text-encoding
yarn add --dev typescript ts-node @types/node @types/node-fetch @types/text-encoding

2. Configure & Create dfuse Client

All dfuse API calls must be authenticated using a an API token that is generated by using your API key. The easiest way to go for this is using the @dfuse/client that will make all the heavy lifting of managing the API token for us.

You will also see a readConfig function that reads the script configuration form the various environment variables, check at the end of this page to see the definition of this code.


import { createDfuseClient } from "@dfuse/client"
;(global as any).fetch = fetch
;(global as any).WebSocket = {}

const config = readConfig()
const client = createDfuseClient({ apiKey: config.dfuseApiKey, network: config.network })

Note

Explaining the @dfuse/client is out of scope for this tutorial. Refer to the JavaScript Quickstart for further details about the @dfuse/client library.

3. Define HTTP Override

When eosjs push a transaction (or make any /v1/chain calls in fact), it simply does it by doing an HTTP call. Since dfuse Push Guaranteed is a drop-in replacement of the native EOSIO push_transaction call, the most important part to perform in your application is overriding eosjs HTTP handling so that some extra headers are passed.

The headers that are required: - Authorization: Bearer $DFUSE_API_TOKEN - X-Eos-Push-Guarantee: in-block | irreversible | handoff:1 | handoffs:2 | handoffs:3

Important

Those two headers needs to be present on your push transaction request otherwise, the push guaranteed API will not kicked in and you will use the “normal endpoint” in those situations.

import fetch, { Request, RequestInit, Response } from "node-fetch"

const customizedFetch = async (input: string | Request, init: RequestInit): Promise<Response> => {
  if (init.headers === undefined) {
    init.headers = {}
  }

  // This is highly optimized and cached, so while the token is fresh, this is very fast
  const apiTokenInfo = await client.getTokenInfo()

  const headers = init.headers as { [name: string]: string }
  headers["Authorization"] = `Bearer ${apiTokenInfo.token}`
  headers["X-Eos-Push-Guarantee"] = config.guaranteed

  return fetch(input, init)
}

Note

This adds the headers to all HTTP queries and routes all traffic to dfuse endpoint. You could adapt the code above to only make the changes when the request is for the push transaction endpoint if you prefer and route all other requests to a different endpoint.

4. Transfer Transaction

Now, let’s define our main function that will create the transfer action, package it in a signed transaction and send it to the EOSIO network.

We go fast over it, but the code is simply creating an eosio.token:transfer action with the correct parameters and push all that through


import { Api, JsonRpc } from "eosjs"
import JsSignatureProvider from "eosjs/dist/eosjs-jssig"
import { TextDecoder, TextEncoder } from "text-encoding"

async function main() {
  const signatureProvider = new JsSignatureProvider([config.privateKey])
  const rpc = new JsonRpc(client.endpoints.restUrl, { fetch: customizedFetch })
  const api = new Api({
    rpc,
    signatureProvider,
    textDecoder: new TextDecoder(),
    textEncoder: new TextEncoder()
  })

  const transferAction = {
    account: "eosio.token",
    name: "transfer",
    authorization: [
      {
        actor: config.transferFrom,
        permission: "active"
      }
    ],
    data: {
      from: config.transferFrom,
      to: config.transferTo,
      quantity: config.transferQuantity,
      memo: `Transaction with push guaranteed '${
        config.guaranteed
      }' from dfuse (https://docs.dfuse.io/#rest-api-post-push_transaction)`
    }
  }

  console.log("Transfer action", prettyJson(transferAction))

  const startTime = new Date()
  const result = await api.transact(
    { actions: [transferAction] },
    {
      blocksBehind: 360,
      expireSeconds: 3600
    }
  )
  const endTime = new Date()

  printResult(result, startTime, endTime)
}

Security

We use an environment variable via JsSignatureProvider to store the private key for testing purposes. In a real production environment, always ensure security of your private keys.

Better yet, like stated directly in eosjs library, use a third-party signing provider:

The Signature Provider holds private keys and is responsible for signing transactions. Using the JsSignatureProvider in the browser is not secure and should only be used for development purposes. Use a secure vault outside of the context of the webpage to ensure security when signing transactions in production

5. Execute Code

Run the following commands from your terminal:

export DFUSE_API_KEY="<dfuse API key here>"
export TRANSFER_FROM_ACCOUNT="<account name that will send the token>"
export SIGNING_PRIVATE_KEY="<account private key here>"

The <dfuse API key here> should be replaced with your dfuse API key, <account name that will send the token> should be replaced with the account name that will transfer money to someone else and <account private key here> should be replaced with the private key controlling the Jungle test net account defined in TRANSFER_FROM_ACCOUNT.

Note The private key must be able to fulfill <from>@active where the <from> is actually the account name specified in TRANSFER_FROM_ACCOUNT.

Then launch the index.ts script to see the transaction being pushed to the network and waiting for the guaranteed condition to be met prior returning from the api.transac call:


./node_modules/bin/ts-node index.ts

yarn ts-node index.ts

Note

If you want to try on EOS Mainnet or Kylin instead, you can provide the following extra environment variables:

export DFUSE_API_NETWORK="<mainnet.eos.dfuse.io OR kylin.eos.dfuse.io>"
export TRANSFER_TO_ACCOUNT="<account name that will receive the token>"
export TRANSFER_QUANTITY="<quantity to transfer, defaults to 0.0001 EOS if unset>"

6. Support Code

And for sake of completion, here are the supporting code used throughout the tutorial to make reading easier:


function readConfig() {
  const network = process.env.DFUSE_API_NETWORK || "jungle.eos.dfuse.io"
  const guaranteed = process.env.PUSH_GUARANTEED || "in-block" // Or "irreversible", "handoff:1", "handoffs:2", "handoffs:3"
  const transferTo = process.env.TRANSFER_TO_ACCOUNT || "junglefaucet"
  const transferQuantity = process.env.TRANSFER_QUANTITY || "0.0001 EOS"

  const dfuseApiKey = process.env.DFUSE_API_KEY
  if (dfuseApiKey === undefined) {
    console.log(
      "You must have a 'process.env.DFUSE_API_KEY' environment variable containing your dfuse API key."
    )
    process.exit(1)
  }

  const privateKey = process.env.SIGNING_PRIVATE_KEY
  if (privateKey === undefined) {
    console.log(
      "You must have a 'SIGNING_PRIVATE_KEY' environment variable containing private used to sign."
    )
    process.exit(1)
  }

  const transferFrom = process.env.TRANSFER_FROM_ACCOUNT
  if (transferFrom === undefined) {
    console.log(
      "You must have a 'TRANSFER_FROM_ACCOUNT' environment variable containing account that is going to send token."
    )
    process.exit(1)
  }

  return {
    network,
    guaranteed,
    dfuseApiKey: dfuseApiKey!,
    privateKey: privateKey!,
    transferFrom: transferFrom!,
    transferTo,
    transferQuantity
  }
}

function printResult(result: any, startTime: Date, endTime: Date) {
  console.log("Transaction push result", prettyJson(result))
  console.log()

  const elapsed = (endTime.getTime() - startTime.getTime()) / 1000.0
  console.log(`Pushed with guarenteed '${config.guaranteed}' in '${elapsed}' seconds`)

  const networkMatch = client.endpoints.restUrl.match(
    /https:\/\/(mainnet|jungle|kylin).eos.dfuse.io/
  )
  if (networkMatch !== null && networkMatch[1] != null) {
    let network = networkMatch[1] + "."
    if (network === "mainnet") {
      network = ""
    }

    console.log(` - https://${network}eosq.app/tx/${result.transaction_id}`)
  }
}

function prettyJson(input: any): string {
  return JSON.stringify(input, null, 2)
}

main()
  .then(() => {
    process.exit(0)
  })
  .catch((error) => {
    console.log("An error occurred.", prettyJson(error))
    process.exit(1)
  })

7. Full Working Example


git clone https://github.com/dfuse-io/docs
cd docs/tutorials/eosio/push-guaranteed
yarn install

# Replace '<dfuse API key here>' with your own API key!
export DFUSE_API_KEY="<dfuse API key here>"
export TRANSFER_FROM_ACCOUNT="<account name that will send the token>"
export SIGNING_PRIVATE_KEY="<account private key here>"

yarn ts-node index.ts