HANDS ON
Provenance chain

Build a provenance chain

This tutorial guides you through building a provenance chain. If the topics of provenance and provenance chain are new to you, start with strong provenance.

In this tutorial, you will:

  1. Construct a provenance chain that models “updates” to a text string
  2. Use the Irys Query SDK to extract the complete provenance chain
  3. Use GraphQL to extract the complete provenance chain

Setup

Start by installing the Irys SDK, and the Query package, setting up your imports, and creating a helper function that returns a reference to a configured Irys object.

import Irys from "@irys/sdk";
import fetch from "node-fetch";
import dotenv from "dotenv";
import Query from "@irys/query";
dotenv.config();
 
// Returns a reference to an Irys node
const getIrys = async () => {
	const url = "https://devnet.irys.xyz";
	// Devnet RPC URLs change often, use a recent one from https://chainlist.org/chain/80001
	const providerUrl = "";
	const token = "matic";
	const privateKey = process.env.PRIVATE_KEY;
 
	const irys = new Irys({
		url: url, // URL of the node you want to connect to
		token: token, // Token used for payment
		key: privateKey, // ETH or SOL private key
		config: { providerUrl: providerUrl }, // Optional provider URL, only required when using Devnet
	});
	return irys;
};

Storing the root asset

Create a function that stores your root transaction. In the interest of simplicity, we use text strings in this tutorial. We upload with the function irys.upload() which returns a signed receipt containing a timestamp accurate to the millisecond of when the upload happened.

// Stores the root transaction and returns the transaction id
const storeRoot = async (myData) => {
	const irys = await getIrys();
	const tags = [{ name: "Content-Type", value: "text/plain" }];
 
	const tx = await irys.upload(myData, { tags });
	return tx.id;
};

Storing updates

Next, create a function that stores “updates”. This is similar to the above function, the only difference is it adds a new tag called root-tx that ties back each update to the root transaction.

// Stores an "update" to the root transaction by creating
// a new transaction and tying it back to the original using
// the "root-id" metatag.
const storeUpdate = async (rootTxId, myData) => {
	const irys = await getIrys();
 
	const tags = [
		{ name: "Content-Type", value: "text/plain" },
		{ name: "root-tx", value: rootTxId },
	];
 
	const tx = await irys.upload(myData, { tags });
	return tx.id;
};

Provenance chains

We'll construct the provenance chain using two methods:

Both methods are equally performant, but the Query package significantly simplifies the process by abstracting the intricacies of GraphQL.

Query package provenance chain

Start by defining two functions. The first, printProveanceChainSDK() handles formatting the data and printing it using console.table()

// Print the full provenance chain in a table
const printProveanceChainSDK = async (rootTxId) => {
	const provenanceChainData = await getProveanceChainSDK(rootTxId);
	console.table(provenanceChainData);
};

and the second getProveanceChainSDK() does the bulk of the work as it interacts with the Query package.

The function starts by connecting to a new Query object and then searching for the transaction with our root transaction ID. Since we uploaded the provenance chain to the Irys Devnet node, we have to connect to the Devnet when setting up the Query object.

const myQuery = new Query({ url: "https://devnet.irys.xyz/graphql" });
 
// First, get the root TX
const rootTx = await myQuery
	.search("irys:transactions")
	.ids([rootTxId]);

After extracting the data, we again return to the same Query object and this time search for transactions with the metadata tag named root-tx that has the same transaction ID as our root transaction.

const chain = await myQuery
	.search("irys:transactions")
	.tags([{ name: "root-tx", values: [rootTxId] }]);

Here's the full function that includes all data parsing.

// Query for all transactions tagged as having a root-tx matching ours
// You could optionally expand on this by querying for the `owner` value
// and making sure it matches the wallet address used to upload
// the original transactions.
const getProveanceChainSDK = async (rootTxId) => {
	const provenanceChainData = [];
 
	// Connect to a Query object
	const myQuery = new Query({ url: "https://devnet.irys.xyz/graphql" });
 
	// First, get the root TX
	const rootTx = await myQuery
		.search("irys:transactions")
		.ids([rootTxId]);
 
	// Extract the id and timestamp and download the data payload
	if (rootTx) {
		const unixTimestamp = rootTx[0].timestamp;
		const date = new Date(unixTimestamp);
		const humanReadable = dateToHumanReadable(date);
 
		const url = `https://gateway.irys.xyz/${rootTx[0].id}`;
		const response = await fetch(url);
		const data = await response.text();
 
		const provenanceEntry = { Date: humanReadable, Data: data };
		provenanceChainData.push(provenanceEntry);
	}
 
	// Now, get the provenance chain
	const chain = await myQuery
		.search("irys:transactions")
		.tags([{ name: "root-tx", values: [rootTxId] }]);
 
	// Iterate over entries
	for (const item of chain) {
		const unixTimestamp = item.timestamp;
		const date = new Date(unixTimestamp);
		const humanReadable = dateToHumanReadable(date);
 
		const url = `https://gateway.irys.xyz/${item.id}`;
		const response = await fetch(url);
		const data = await response.text();
 
		const provenanceEntry = { Date: humanReadable, Data: data };
		provenanceChainData.push(provenanceEntry);
	}
	return provenanceChainData;
};

GraphQL provenance chain

For the GraphQL version, first we'll create and test the queries, and then look at how to call them from JavaScript. You can experiment with these queries by accessing our GraphQL sandbox.

We'll employ two GraphQL queries to identify the transaction IDs and timestamps for each component in our provenance chain. The initial query fetches the timestamp for the root transaction, while the subsequent query retrieves all transaction IDs and timestamps for the updates.

Root transaction

For the root transaction, we'll retrieve the timestamp from when it was uploaded. This provides a baseline for our provenance chain, as all subsequent updates will possess timestamps that follow this initial one.

query getByIds {
	transactions(ids: ["bVIWFJNYmw8cg9GkiPsP4VBI3eG9nok7kON1MOdMEt0"]) {
		edges {
			node {
				timestamp
			}
		}
	}
}

Update transactions

The second query retrieves all following transactions and arranges them chronologically according to their timestamps, thus establishing the correct time sequence.

query getProvenanceChain {
	transactions(tags: [{ name: "root-tx", values: ["bVIWFJNYmw8cg9GkiPsP4VBI3eG9nok7kON1MOdMEt0"] }], order: ASC) {
		edges {
			node {
				id
				timestamp
			}
		}
	}
}

Query functions

This tutorial uses the HTTP fetch library to interact with our GraphQL endpoint due to its simplicity. However, if your application demands a more robust solution, consider replacing it with a comprehensive GraphQL client like Apollo. (opens in a new tab)

Root transaction and data

Use GraphQL to query for the timestamp of the root transaction, then fetch the data payload. Create a JavaScript object holding both and return it embedded in an array.

// Gets the timestamp of the root transaction
const getRootTxGQL = async (rootTxId) => {
	// First query for the timestamp of the root transaction
	const query = `
	query getByIds {
		transactions(ids:["${rootTxId}"]) {
			edges {
				node {
					timestamp
				}
			}
		}
	}`;
 
	const url = "https://devnet.irys.xyz/graphql";
	const options = {
		method: "POST",
		headers: { "Content-Type": "application/json" },
		body: JSON.stringify({ query }),
	};
 
	// Send the request and return  the response
	const response = await fetch(url, options);
	const data = await response.json();
	const provenanceChain = data.data.transactions.edges;
	const provenanceChainData = [];
 
	for (const item of provenanceChain) {
		const id = item.node.id;
		const unixTimestamp = item.node.timestamp;
		const date = new Date(unixTimestamp);
		const humanReadable = dateToHumanReadable(date);
 
		const url = `https://gateway.irys.xyz/${rootTxId}`;
		const response = await fetch(url);
		const data = await response.text();
 
		const provenanceEntry = { Date: humanReadable, Data: data };
		provenanceChainData.push(provenanceEntry);
	}
 
	return provenanceChainData;
};

Update transaction and data

Then query for all transaction IDs and timestamps in the update chain, fetch the associated data payloads and return them as an array of objects.

// Query for all transactions tagged as having a root-tx matching ours
// You could optionally expand on this by querying for the `owner` value
// and making sure it matches the wallet address used to upload
// the original transactions.
const getProveanceChainGQL = async (rootTxId) => {
	const query = `
	query getProvenanceChain{
		transactions(tags: [{ name: "root-tx", values: ["${rootTxId}"] }], order: ASC) {
			edges {
				node {
					id
					timestamp
				}
			}
		}
	}`;
 
	// Define the URL of your GraphQL server
	const url = "https://devnet.irys.xyz/graphql";
 
	// Define the options for the fetch request
	const options = {
		method: "POST",
		headers: { "Content-Type": "application/json" },
		body: JSON.stringify({ query }),
	};
 
	// Send the request and return  the response
	const response = await fetch(url, options);
	const data = await response.json();
	const provenanceChain = data.data.transactions.edges;
	const provenanceChainData = [];
 
	for (const item of provenanceChain) {
		const id = item.node.id;
		const unixTimestamp = item.node.timestamp;
		const date = new Date(unixTimestamp);
		const humanReadable = dateToHumanReadable(date);
 
		const url = `https://gateway.irys.xyz/${id}`;
		const response = await fetch(url);
		const data = await response.text();
 
		const provenanceEntry = { Date: humanReadable, Data: data };
		provenanceChainData.push(provenanceEntry);
	}
	return provenanceChainData;
};

Creating the chain

To create the full chain, first request the root data, then the update data. Concatenate both arrays together and output using the console.table() function.

// Print the full provenance chain in a table
const printProveanceChainGQL = async (rootTxId) => {
	const provenanceChainData = await getRootTxGQL(rootTxId);
	provenanceChainData.push(...(await getProveanceChainGQL(rootTxId)));
	console.table(provenanceChainData);
};

Calling the functions

This is an example of calling the functions first with “Hello World” and then with updates that translate it into different languages.

// Create a provenance chain showing "Hello World" changing from one language to the next
const rootId = await storeRoot("Hello World");
await storeUpdate(rootId, "Hola Mundo"); // Spanish
await storeUpdate(rootId, "Olá Mundo"); // Portuguese
await storeUpdate(rootId, "こんにちは世界"); // Japanese
await storeUpdate(rootId, "สวัสดีชาวโลก"); // Thai
await storeUpdate(rootId, "GM"); // Web3
console.log(`Provenance Chain Stored: rootId=${rootId}`);
 
// And then print out the full provenance chain
await printProveanceChainGQL(rootId);
await printProveanceChainSDK(rootId);

When run, the code will output something similar to this.

Full code

Here's the full code:

 
import Irys from "@irys/sdk";
import fetch from "node-fetch";
import dotenv from "dotenv";
import Query from "@irys/query";
dotenv.config();
 
// Returns a reference to an Irys node
const getIrys = async () => {
	const url = "https://devnet.irys.xyz";
	// Devnet RPC URLs change often, use a recent one from https://chainlist.org/chain/80001
	const providerUrl = "";
	const token = "matic";
	const privateKey = process.env.PRIVATE_KEY;
 
	const irys = new Irys({
		url: url, // URL of the node you want to connect to
		token: token, // Token used for payment
		key: privateKey, // ETH or SOL private key
		config: { providerUrl }, // Optional provider URL, only required when using Devnet
	});
	return irys;
};
 
// Stores the root transaction and returns the transaction id
const storeRoot = async (myData) => {
	const irys = await getIrys();
	const tags = [{ name: "Content-Type", value: "text/plain" }];
 
	const tx = await irys.upload(myData, { tags });
	return tx.id;
};
 
// Stores an "update" to the root transaction by creating
// a new transaction and tying it back to the original using
// the "root-id" metatag.
const storeUpdate = async (rootTxId, myData) => {
	const irys = await getIrys();
 
	const tags = [
		{ name: "Content-Type", value: "text/plain" },
		{ name: "root-tx", value: rootTxId },
	];
 
	const tx = await irys.upload(myData, { tags });
	return tx.id;
};
 
// Helper function, takes a Date object and returns a human readable string
// showing date, month, year and time accurate to the millisecond
const dateToHumanReadable = (date) => {
	const options = {
		year: "numeric",
		month: "2-digit",
		day: "2-digit",
		hour: "2-digit",
		minute: "2-digit",
		second: "2-digit",
		fractionalSecondDigits: 3, // milliseconds
	};
 
	// Pass "undefined" to force the default local to be used for formatting
	return date.toLocaleString(undefined, options);
};
 
// Gets the timestamp of the root transaction
const getRootTxGQL = async (rootTxId) => {
	// First query for the timestamp of the root transaction
	const query = `
	query getByIds {
		transactions(ids:["${rootTxId}"]) {
			edges {
				node {
					timestamp
				}
			}
		}
	}`;
 
	const url = "https://devnet.irys.xyz/graphql";
	const options = {
		method: "POST",
		headers: { "Content-Type": "application/json" },
		body: JSON.stringify({ query }),
	};
 
	// Send the request and return  the response
	const response = await fetch(url, options);
	const data = await response.json();
	const provenanceChain = data.data.transactions.edges;
	const provenanceChainData = [];
 
	for (const item of provenanceChain) {
		const id = item.node.id;
		const unixTimestamp = item.node.timestamp;
		const date = new Date(unixTimestamp);
		const humanReadable = dateToHumanReadable(date);
 
		const url = `https://gateway.irys.xyz/${rootTxId}`;
		const response = await fetch(url);
		const data = await response.text();
 
		const provenanceEntry = { Date: humanReadable, Data: data };
		provenanceChainData.push(provenanceEntry);
	}
 
	return provenanceChainData;
};
 
// Query for all transactions tagged as having a root-tx matching ours
// You could optionally expand on this by querying for the `owner` value
// and making sure it matches the wallet address used to upload
// the original transactions.
const getProveanceChainGQL = async (rootTxId) => {
	const query = `
	query getProvenanceChain{
		transactions(tags: [{ name: "root-tx", values: ["${rootTxId}"] }], order: ASC) {
			edges {
				node {
					id
					timestamp
				}
			}
		}
	}`;
 
	// Define the URL of your GraphQL server
	const url = "https://devnet.irys.xyz/graphql";
 
	// Define the options for the fetch request
	const options = {
		method: "POST",
		headers: { "Content-Type": "application/json" },
		body: JSON.stringify({ query }),
	};
 
	// Send the request and return  the response
	const response = await fetch(url, options);
	const data = await response.json();
	const provenanceChain = data.data.transactions.edges;
	const provenanceChainData = [];
 
	for (const item of provenanceChain) {
		const id = item.node.id;
		const unixTimestamp = item.node.timestamp;
		const date = new Date(unixTimestamp);
		const humanReadable = dateToHumanReadable(date);
 
		const url = `https://gateway.irys.xyz/${id}`;
		const response = await fetch(url);
		const data = await response.text();
 
		const provenanceEntry = { Date: humanReadable, Data: data };
		provenanceChainData.push(provenanceEntry);
	}
	return provenanceChainData;
};
 
// Print the full provenance chain in a table
const printProveanceChainGQL = async (rootTxId) => {
	const provenanceChainData = await getRootTxGQL(rootTxId);
	provenanceChainData.push(...(await getProveanceChainGQL(rootTxId)));
	console.table(provenanceChainData);
};
 
// Query for all transactions tagged as having a root-tx matching ours
// You could optionally expand on this by querying for the `owner` value
// and making sure it matches the wallet address used to upload
// the original transactions.
const getProveanceChainSDK = async (rootTxId) => {
	const provenanceChainData = [];
 
	// Connect to a Query object
	const myQuery = new Query({ url: "https://devnet.irys.xyz/graphql" });
 
	// First, get the root TX
	const rootTx = await myQuery
		.search("irys:transactions"
		.ids([rootTxId]);
 
	// Extract the id and timestamp and download the data payload
	if (rootTx) {
		const unixTimestamp = rootTx[0].timestamp;
		const date = new Date(unixTimestamp);
		const humanReadable = dateToHumanReadable(date);
 
		const url = `https://gateway.irys.xyz/${rootTx[0].id}`;
		const response = await fetch(url);
		const data = await response.text();
 
		const provenanceEntry = { Date: humanReadable, Data: data };
		provenanceChainData.push(provenanceEntry);
	}
 
	// Now, get the provenance chain
	const chain = await myQuery
		.search("irys:transactions")
		.tags([{ name: "root-tx", values: [rootTxId] }]);
 
	// Iterate over entries
	for (const item of chain) {
		const unixTimestamp = item.timestamp;
		const date = new Date(unixTimestamp);
		const humanReadable = dateToHumanReadable(date);
 
		const url = `https://gateway.irys.xyz/${item.id}`;
		const response = await fetch(url);
		const data = await response.text();
 
		const provenanceEntry = { Date: humanReadable, Data: data };
		provenanceChainData.push(provenanceEntry);
	}
	return provenanceChainData;
};
 
// Print the full provenance chain in a table
const printProveanceChainSDK = async (rootTxId) => {
	const provenanceChainData = await getProveanceChainSDK(rootTxId);
	console.table(provenanceChainData);
};
 
// Create a provenance chain showing "Hello World" changing from one language to the next
const rootId = await storeRoot("Hello World");
await storeUpdate(rootId, "Hola Mundo"); // Spanish
await storeUpdate(rootId, "Olá Mundo"); // Portuguese
await storeUpdate(rootId, "こんにちは世界"); // Japanese
await storeUpdate(rootId, "สวัสดีชาวโลก"); // Thai
await storeUpdate(rootId, "GM"); // Web3
 
// And then print out the full provenance chain
await printProveanceChainGQL(rootId);
await printProveanceChainSDK(rootId);

Getting help

If you get stuck or need help, reach out in our Discord (opens in a new tab) and someone will get back to you quickly.

And, most importantly pleaase let us know how you're using Irys and what you're building. Let us know how we can best support you!