Skip to content

Instantly share code, notes, and snippets.

@anonymoussprocket
Last active March 16, 2021 06:31
Show Gist options
  • 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": [ "." ]
}
@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