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 Kylin Test Network using dfuse push_transaction
with push guaranteed
activated.
For that, you will need an account on the Kylin Test Network with some tokens in it (check cryptokylin.io 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
- Create your account on dfuse.eosnation.io
-
Click “Create New Key” and give it a name, a category. In the case of a
web
key give it an “Origin” value.
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
Quick Start: JavaScript 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 === undefined) {
init = {}
}
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 </span><span style="color:#e6db74">${</span><span style="color:#a6e22e">apiTokenInfo</span>.<span style="color:#a6e22e">token</span><span style="color:#e6db74">}</span><span style="color:#e6db74">
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 as any })
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 '</span><span style="color:#e6db74">${</span> <span style="color:#a6e22e">config</span>.<span style="color:#a6e22e">guaranteed</span> <span style="color:#e6db74">}</span><span style="color:#e6db74">' from dfuse (https://docs.dfuse.eosnation.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 Kylin 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="<eos.dfuse.eosnation.io OR kylin.dfuse.eosnation.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 || "kylin.dfuse.eosnation.io"
const guaranteed = process.env.PUSH_GUARANTEED || "in-block" // Or "irreversible", "handoff:1", "handoffs:2", "handoffs:3"
const transferTo = process.env.TRANSFER_TO_ACCOUNT || "eoscanadacom"
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 '</span><span style="color:#e6db74">${</span><span style="color:#a6e22e">config</span>.<span style="color:#a6e22e">guaranteed</span><span style="color:#e6db74">}</span><span style="color:#e6db74">' in '</span><span style="color:#e6db74">${</span><span style="color:#a6e22e">elapsed</span><span style="color:#e6db74">}</span><span style="color:#e6db74">' seconds
)
const networkMatch = client.endpoints.restUrl.match(
/https://(eos|wax|testnet|kylin|jungle4).dfuse.eosnation.io/
)
if (networkMatch !== null && networkMatch[1] != null) {
let network = networkMatch[1]
console.log( - https://</span><span style="color:#e6db74">${</span><span style="color:#a6e22e">network</span><span style="color:#e6db74">}</span><span style="color:#e6db74">.eosq.eosnation.io/tx/</span><span style="color:#e6db74">${</span><span style="color:#a6e22e">result</span>.<span style="color:#a6e22e">transaction_id</span><span style="color:#e6db74">}</span><span style="color:#e6db74">
)
}
}
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/EOS-Nation/dfuse-docs
cd dfuse-docs/tutorials/eos/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