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:
- Construct a provenance chain that models “updates” to a text string
- Use the Irys Query SDK to extract the complete provenance chain
- 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:
- The Query package
- GraphQL
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!