Skip to content

Instantly share code, notes, and snippets.

@anonymoussprocket
Last active March 16, 2021 06:31
Show Gist options
  • Star 4 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save anonymoussprocket/148d82fc9bf6c413be04155a90d57be6 to your computer and use it in GitHub Desktop.
Save anonymoussprocket/148d82fc9bf6c413be04155a90d57be6 to your computer and use it in GitHub Desktop.
Tezos User Workflows with ConseilJS

Purpose

Single script to test common Tezos user workflows. Additionally this sample illustrates the use of ConseilJS for interactions with a Tezos blockchain node.

Environment

This tutorial requires nodejs 12.21+ & npm. It's highly recommended to install those via nvm.

Steps

Create a new directory for the files included in this gist. Run the following commands and observe the console output.

  • Create an account with Nautilus Cloud and fill in the missing data for tezosNode and conseilServer variables.
  • Get a faucet account save it in the same directory. The file name is expected to be in the format "tz1*.json"
  • nvm use 12.21
  • npm install
  • npm start

Code walk-through

tezosNode & conseilServer variables need to be populated with valid values from Nautilus Cloud. You can of course provide any other instance of a Tezos node or Conseil Indexer.

clearRPCOperationGroupHash

Operation group hash comes poorly formatted from the node, this function cleans it up for use with Conseil.

initConseil

initAccount

activateAccount

revealAccount

sendTransaction

delegateAccount

deployMichelineContract & deployMichelsonContract

invokeContract

parseContract

dumpMempool

More functionality in ConseilJS

  • dApp interaction with Beacon
  • FA1.2 token deployment & interactions
  • Complex, indexed data extraction
  • FA2 token deployment & interactions
  • Batched blockchain operations
  • Direct & indexed big_map access
  • Multi-baker delegation
  • Improved security for softsigner
  • HD keys in softsigner
  • Ledger operation signing
  • Complex fee estimation
  • dApp interaction Galleon

Other Links

Confirmed Environment Configurations

We use precise dependency versions to avoid unintended package upgrades that can lead to broken software.

  • macOS 10.15.7, nodejs v12.21.0, conseiljs 5.0.7-2, typescript 3.8.3
import * as fs from 'fs';
import * as glob from 'glob';
import fetch from 'node-fetch';
import * as log from 'loglevel';
import { BigNumber } from 'bignumber.js';
import { registerFetch, registerLogger } from 'conseiljs';
import { TezosConstants, TezosConseilClient, TezosNodeWriter, TezosParameterFormat, KeyStore, TezosNodeReader, TezosContractIntrospector, Signer, TezosMessageUtils } from 'conseiljs';
import { KeyStoreUtils, SoftSigner } from 'conseiljs-softsigner';
const tezosNode = '<tezos-node-url>';
const conseilServer = { url: '<conseil-indexer-url>', apiKey: '<APIKEY_from_nautuilus.cloud>', network: 'edonet' };
const networkBlockTime = 30 + 1;
function clearRPCOperationGroupHash(hash: string) {
return hash.replace(/\"/g, '').replace(/\n/, '');
}
function initConseil() {
const logger = log.getLogger('conseiljs');
logger.setLevel('debug', false);
registerLogger(logger);
registerFetch(fetch);
}
async function initAccount(): Promise<KeyStore> {
console.log('~~ initAccount');
let faucetFiles: string[] = glob.sync('tz1*.json');
if (faucetFiles.length === 0) {
throw new Error('Did not find any faucet files, please go to faucet.tzalpha.net to get one');
}
console.log(`loading ${faucetFiles[0]} faucet file`);
let faucetAccount = JSON.parse(fs.readFileSync(faucetFiles[0], 'utf8'));
let keyStore: KeyStore;
if (faucetAccount['secretKey']) {
keyStore = await KeyStoreUtils.restoreIdentityFromSecretKey(faucetAccount['secretKey']);
} else {
keyStore = await KeyStoreUtils.restoreIdentityFromFundraiser(faucetAccount['mnemonic'].join(' '), faucetAccount['email'], faucetAccount['password'], faucetAccount['pkh']);
faucetAccount['secretKey'] = keyStore.secretKey;
fs.writeFileSync(faucetFiles[0], JSON.stringify(faucetAccount));
}
console.log(`public key: ${keyStore.publicKey}`);
console.log(`secret key: ${keyStore.secretKey}`);
console.log(`account hash: ${keyStore.publicKeyHash}`);
console.log('~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~');
return keyStore;
}
async function activateAccount(signer: Signer, keyStore: KeyStore): Promise<string> {
console.log(`~~ activateAccount`);
const accountRecord = await TezosConseilClient.getAccount(conseilServer, conseilServer.network, keyStore.publicKeyHash);
if (accountRecord !== undefined) { return accountRecord['account_id']; }
const faucetAccount = JSON.parse(fs.readFileSync(`${keyStore.publicKeyHash}.json`, 'utf8'));
const nodeResult = await TezosNodeWriter.sendIdentityActivationOperation(tezosNode, signer, keyStore, faucetAccount['secret']);
const groupid = clearRPCOperationGroupHash(nodeResult.operationGroupID);
console.log(`Injected activation operation with ${groupid}`);
const conseilResult = await TezosConseilClient.awaitOperationConfirmation(conseilServer, conseilServer.network, groupid, 5, networkBlockTime);
console.log(`Activated account at ${conseilResult.pkh}`);
console.log('~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~');
return conseilResult.pkh;
}
async function revealAccount(signer: Signer, keyStore: KeyStore): Promise<string> {
console.log(`~~ revealAccount`);
if (await TezosNodeReader.isManagerKeyRevealedForAccount(tezosNode, keyStore.publicKeyHash)) {
return keyStore.publicKeyHash;
}
const nodeResult = await TezosNodeWriter.sendKeyRevealOperation(tezosNode, signer, keyStore);
const groupid = clearRPCOperationGroupHash(nodeResult.operationGroupID);
console.log(`Injected reveal operation with ${groupid}`);
const conseilResult = await TezosConseilClient.awaitOperationConfirmation(conseilServer, conseilServer.network, groupid, 5, networkBlockTime);
console.log(`Revealed account at ${conseilResult.source}`);
console.log('~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~');
return conseilResult.source;
}
async function sendTransaction(signer: Signer, keyStore: KeyStore, destination: string, amount: number) {
console.log(`~~ sendTransaction: ${amount}µtz from ${keyStore.publicKeyHash} to ${destination}`);
const nodeResult = await TezosNodeWriter.sendTransactionOperation(tezosNode, signer, keyStore, destination, amount, 100_000, TezosConstants.HeadBranchOffset, true);
const groupid = clearRPCOperationGroupHash(nodeResult.operationGroupID);
console.log(`Injected transaction operation with ${groupid}`);
const conseilResult = await TezosConseilClient.awaitOperationConfirmation(conseilServer, conseilServer.network, groupid, 5, networkBlockTime);
console.log(`Completed transfer of ${conseilResult.amount}µtz`);
console.log('~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~');
}
async function delegateAccount(signer: Signer, keyStore: KeyStore, baker: string) {
console.log(`~~ delegatePrimaryAccount`);
const nodeResult = await TezosNodeWriter.sendDelegationOperation(tezosNode, signer, keyStore, baker, 400, 54, true);
const groupid = clearRPCOperationGroupHash(nodeResult.operationGroupID);
console.log(`Injected delegation operation with ${groupid}`);
await TezosConseilClient.awaitOperationConfirmation(conseilServer, conseilServer.network, groupid, 5, networkBlockTime);
console.log('~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~');
}
async function deployMichelineContract(signer: Signer, keyStore: KeyStore, amount: number, baker: string | undefined): Promise<string> {
console.log(`~~ deployMichelineContract`);
const contract = `[
{ "prim":"parameter", "args":[ { "prim":"string" } ] },
{ "prim":"storage", "args":[ { "prim":"string" } ] },
{
"prim":"code",
"args":[
[
{ "prim":"CAR" },
{ "prim":"NIL", "args":[ { "prim":"operation" } ] },
{ "prim":"PAIR" }
]
]
}
]`;
const storage = '{"string": "Sample"}';
const nodeResult = await TezosNodeWriter.sendContractOriginationOperation(tezosNode, signer, keyStore, amount, baker, 1000, 1000, 100000, contract, storage, TezosParameterFormat.Micheline, 54, true);
const groupid = clearRPCOperationGroupHash(nodeResult['operationGroupID']);
console.log(`Injected origination operation with ${groupid}`);
const conseilResult = await TezosConseilClient.awaitOperationConfirmation(conseilServer, conseilServer.network, groupid, 5, networkBlockTime);
console.log(`Originated contract at ${conseilResult.originated_contracts}`);
console.log('~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~');
return conseilResult.originated_contracts;
}
async function deployMichelsonContract(signer: Signer, keyStore: KeyStore, amount: number, baker: string | undefined): Promise<string> {
console.log(`~~ deployMichelsonContract`);
const contract = `parameter string;
storage string;
code { DUP;
DIP { CDR ; NIL string ; SWAP ; CONS } ;
CAR ; CONS ;
CONCAT;
NIL operation; PAIR}`;
const storage = '"Sample"';
const nodeResult = await TezosNodeWriter.sendContractOriginationOperation(tezosNode, signer, keyStore, amount, baker, 1000, 1000, 100000, contract, storage, TezosParameterFormat.Michelson, 54, true);
const groupid = clearRPCOperationGroupHash(nodeResult['operationGroupID']);
console.log(`Injected origination operation with ${groupid}`);
const conseilResult = await TezosConseilClient.awaitOperationConfirmation(conseilServer, conseilServer.network, groupid, 5, networkBlockTime);
console.log(`Originated contract at ${conseilResult.originated_contracts}`);
console.log('~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~');
return conseilResult.originated_contracts;
}
async function invokeContract(signer: Signer, keyStore: KeyStore, address: string, amount: number, parameter: string, entrypoint: string = '') {
console.log(`~~ invokeContract`);
let storageResult = await TezosNodeReader.getContractStorage(tezosNode, address);
console.log(`initial storage: ${JSON.stringify(storageResult)}`);
const { gas, storageCost: freight, estimatedFee, } = await TezosNodeWriter.testContractInvocationOperation(tezosNode, 'main', keyStore, address, amount, 0, 1000, 100000, entrypoint, parameter, TezosParameterFormat.Michelson);
const nodeResult = await TezosNodeWriter.sendContractInvocationOperation(tezosNode, signer, keyStore, address, amount, estimatedFee, freight, gas, entrypoint, parameter, TezosParameterFormat.Michelson);
const groupid = clearRPCOperationGroupHash(nodeResult.operationGroupID);
console.log(`Injected transaction(invocation) operation with ${groupid}`);
const conseilResult = await TezosConseilClient.awaitOperationConfirmation(conseilServer, conseilServer.network, groupid, 5, networkBlockTime);
console.log(`Completed invocation of ${conseilResult.destination}`);
storageResult = await TezosNodeReader.getContractStorage(tezosNode, address);
console.log(`modified storage: ${JSON.stringify(storageResult)}`);
console.log('~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~');
}
async function parseContract(address: string) {
const entryPoints = await TezosContractIntrospector.generateEntryPointsFromAddress(conseilServer, conseilServer.network, address);
for (const entryPoint of entryPoints) {
console.log(`${entryPoint.name}(${entryPoint.parameters.map(p => (p.name ? p.name + ': ' : '') + p.type + (p.optional ? '?' : '')).join(', ')})`)
console.log(entryPoint.structure)
}
}
async function dumpMempool(account: string) {
const rr = await TezosNodeReader.getMempoolOperationsForAccount(tezosNode, account);
await Promise.all(
rr.map(async (r) => {
const ttl = await TezosNodeReader.estimateBranchTimeout(tezosNode, r['branch']);
const t = r['contents'][0];
console.log(`operation ${r['hash']} for ${new BigNumber(t.amount || 0).toNumber()}xtz expires in ${ttl} blocks`)
})
);
}
async function run() {
initConseil()
// Account initialization
const keyStore = await initAccount();
const signer = await SoftSigner.createSigner(TezosMessageUtils.writeKeyWithHint(keyStore.secretKey, 'edsk'));
await activateAccount(signer, keyStore);
await revealAccount(signer, keyStore);
// Basic operations
const baker = 'tz1VpvtSaSxKvykrqajFJTZqCXgoVJ5cKaM1'; // random baker from https://arronax.io/tezos/edonet/blocks
const recipient = 'tz1aWXP237BLwNHJcCD4b3DutCevhqq2T1Z9' // random account from https://arronax.io/tezos/edonet/accounts
await sendTransaction(signer, keyStore, recipient, 500_000);
await delegateAccount(signer, keyStore, baker);
// Basic contract operations
// const contractAddress = await deployMichelineContract(signer, keyStore, 0, undefined);
const contractAddress = await deployMichelsonContract(signer, keyStore, 0, undefined);
await invokeContract(signer, keyStore, contractAddress, 0, '"new text"');
await parseContract(contractAddress);
await dumpMempool(keyStore.publicKeyHash);
}
run();
{
"name": "tezos-workflow-test",
"version": "8.0.0",
"description": "Integrated Tezos Workflow Test",
"main": "index.ts",
"scripts": {
"postinstall": "npm run build",
"build": "tsc index.ts",
"start": "node index.js"
},
"author": "anonymoussprocket",
"license": "Apache2",
"engines": {
"node": "12.21.0",
"npm": "6.14.11"
},
"homepage": "https://gist.github.com/anonymoussprocket",
"dependencies": {
"@types/node": "14.14.35",
"bignumber.js": "9.0.1",
"conseiljs": "5.0.7-2",
"conseiljs-ledgersigner": "5.0.4",
"conseiljs-softsigner": "5.0.4-1",
"glob": "7.1.6",
"loglevel": "1.7.1",
"node-fetch": "2.6.1"
},
"devDependencies": {
"typescript": "3.8.3"
}
}
{
"compilerOptions": {
"target": "es2016",
"module": "commonjs",
"declaration": true,
"sourceMap": true,
"outDir": "dist",
"removeComments": true,
"strict": true,
"noImplicitAny": false,
"baseUrl": ".",
"esModuleInterop": true,
"resolveJsonModule": true
},
"exclude": [ "node_modules" ],
"include": [ "." ]
}
@ltfschoen
Copy link

If you're getting error 404 not found, then try changing your Tezos Bablylonnet Node address from https://tezos-dev.cryptonomic-infra.tech/ to https://conseil-dev.cryptonomic-infra.tech:443 in index.ts

@ltfschoen
Copy link

@TriplEight
Copy link

  1. Copy addresses from https://nautilus.cloud/ they are provided with trailing slash /.
  2. Put these adresses in https://gist.github.com/anonymoussprocket/148d82fc9bf6c413be04155a90d57be6#file-index-ts-L9-L10 and
  3. run npm install or npm run build
  4. npm start
    You'll get
ConseilDataClient.executeEntityQuery request: https://tezos-dev.cryptonomic-infra.tech//v2/data/tezos/babylonnet/accounts, {"fields":[],"predicates":[{"field":"account_id","operation":"eq","set":["tz1SKm3uu43HL1jqnedWtxsbzvwq4UGZ9JF5"],"inverse":false}],"orderBy":[],"aggregation":[],"limit":1}
ConseilDataClient.executeEntityQuery request: https://tezos-dev.cryptonomic-infra.tech//v2/data/tezos/babylonnet/accounts, {"fields":[],"predicates":[{"field":"account_id","operation":"eq","set":["tz1SKm3uu43HL1jqnedWtxsbzvwq4UGZ9JF5"],"inverse":false}],"orderBy":[],"aggregation":[],"limit":1}, failed with Not Found(404)
(node:6233) UnhandledPromiseRejectionWarning: Error
    at /home/tripleight/code/practice/tezos_workshop/node_modules/conseiljs/dist/reporting/ConseilDataClient.js:34:27
    at processTicksAndRejections (internal/process/task_queues.js:94:5)
(node:6233) UnhandledPromiseRejectionWarning: Unhandled promise rejection. This error originated either by throwing inside of an async function without a catch block, or by rejecting a promise which was not handled with .catch(). (rejection id: 1)
(node:6233) [DEP0018] DeprecationWarning: Unhandled promise rejections are deprecated. In the future, promise rejections that are not handled will terminate the Node.js process with a non-zero exit code.

Which fails simply because of the double slash in the address.

@ltfschoen
Copy link

@TriplEight, see my previous comment, add :443 to the end too

@TriplEight
Copy link

@TriplEight, see my previous comment, add :443 to the end too

cheers, posted my report and then saw yours.

@lijianl
Copy link

lijianl commented Mar 29, 2020

faucetAccount activate failed? why
-->
https://tezos-dev.cryptonomic-infra.tech//injection/operation?chain=main
Injected activation operation with [{kind:temporary,id:failure,msg:Error while applying operation onyhAXdQsbnHqu4Q7S4mUi3nGLZxGWKS7b2HzWEiFZXV3cijLzY:\nrefused (Error:\n Invalid activation. The public key tz1SamNpDf5WU6cXs8zWhAWzkWn9omqyJM2z does not match any commitment.\n)}]

@lijianl
Copy link

lijianl commented Mar 29, 2020

where could I find a activate code for an empty Tezos account?

@lijianl
Copy link

lijianl commented Mar 29, 2020

async function createAccount() {
const mnemonic = conseiljs.TezosWalletUtil.generateMnemonic();
console.log(mnemonic: ${mnemonic});
const keystore = await conseiljs.TezosWalletUtil.unlockIdentityWithMnemonic(mnemonic, '');
console.log(account id: ${keystore.publicKeyHash});
console.log(public key: ${keystore.publicKey});
console.log(secret key: ${keystore.privateKey});
}

how to activate a new empty account, what is the activate code?

@anonymoussprocket
Copy link
Author

get a faucet file from faucet.tzalpha.net

@lijianl
Copy link

lijianl commented Mar 30, 2020

where to get a fundraiser account on the main net, send a transaction from a Mnemonic account is always failed??
could you help with this

@anonymoussprocket
Copy link
Author

Please join our developer room on Riot

@lijianl
Copy link

lijianl commented Mar 30, 2020

i use TezosWalletUtil.getKeysFromMnemonicAndPassphrase to get a fundraiser account, but how to get an activate code

@lijianl
Copy link

lijianl commented Mar 30, 2020

developer room on Riot has an error, "Invalid homeserver discovery response" could you help to solve above issue

@lijianl
Copy link

lijianl commented Mar 30, 2020

Please join our developer room on Riot

the URL is ERROR:Invalid homeserver discovery response

@lijianl
Copy link

lijianl commented Mar 30, 2020

Please join our developer room on Riot

the URL is ERROR:Invalid homeserver discovery response

could you give me any help with this?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment