Track Costs¶
This guide covers the process of fetching payment stream data via the StorageHub SDK as an authorized user and using that data to calculate how much longer the DataHaven network will continue storing your files, based on your current balance and costs.
Prerequisites¶
Before you begin, ensure you have the following:
- Node.js v22+ installed
-
A TypeScript project
Need a starter project?
If you don't have an existing project, follow these steps to create a TypeScript project you can use to follow the guides in this section:
-
Create a new project folder by executing the following command in the terminal:
-
Initialize a
package.jsonfile using the correct command for your package manager: -
Add the TypeScript and Node type definitions to your projects using the correct command for your package manager:
-
Create a
tsconfig.jsonfile in the root of your project and paste the following configuration: -
Initialize the
srcdirectory:
-
-
A file uploaded to DataHaven
Initialize the Script Entry Point¶
First, create an index.ts file if you haven't already. Its run method will orchestrate all the logic in this guide, and you’ll replace the labelled placeholders with real code step by step. By now, your services folder (including the MSP and client helper services) should already be created. If not, see the Get Started guide.
The index.ts snippet below also imports costOperations.ts, which is not in your project yet—that's expected, as you'll create it later in this guide.
Add the following code to your index.ts file:
async function run() {
// --- Cost tracking logic ---
// **PLACEHOLDER FOR STEP 1: AUTHENTICATE**
// **PLACEHOLDER FOR STEP 2: GET PAYMENT STREAMS**
// **PLACEHOLDER FOR STEP 3: CALCULATE TIME REMAINING**
// Disconnect the Polkadot API at the very end
await polkadotApi.disconnect();
}
await run();
Authenticate¶
Before accessing payment stream information or balance data, authenticate with the MSP. The authenticateUser helper signs a SIWE message and returns a session token that authorizes your cost-tracking requests. Add the following code to use the authenticateUser helper method you've already implemented in mspService.ts:
// Authenticate
const authProfile = await authenticateUser();
console.log('Authenticated user profile:', authProfile);
View complete index.ts up until this point
import '@storagehub/api-augment';
import { initWasm } from '@storagehub-sdk/core';
import { address, polkadotApi } from './services/clientService.js';
import { authenticateUser, getPaymentStreams } from './services/mspService.js';
import {
calculateTimeRemaining,
getBalance,
} from './operations/costOperations.js';
async function run() {
// --- Cost tracking logic ---
// Authenticate
const authProfile = await authenticateUser();
console.log('Authenticated user profile:', authProfile);
// **PLACEHOLDER FOR STEP 2: GET PAYMENT STREAMS**
// **PLACEHOLDER FOR STEP 3: CALCULATE TIME REMAINING**
// Disconnect the Polkadot API at the very end
await polkadotApi.disconnect();
}
await run();
Get Payment Streams¶
For each created bucket and uploaded file, the MSP and corresponding BSPs storing your data bill your account a costPerTick. A tick corresponds to one block on the DataHaven network. With a 6-second block time, one tick = 6 seconds. Storage costs are calculated and deducted on a per-tick basis.
To see what this recurring cost is for you specifically, in this section, you'll implement the getPaymentStreams helper method and then call it in your index.ts file.
Add Method to Get Payment Streams¶
To implement the getPaymentStreams helper method, add the following code to the mspService.ts file:
const getPaymentStreams = async (): Promise<PaymentStreamsResponse> => {
// Fetch payment streams associated with the authenticated user
const paymentStreams = await mspClient.info.getPaymentStreams();
return paymentStreams;
};
// Make sure to add this method to the export statement at the bottom of the file too
Call Get Payment Streams Helper Method¶
Replace the placeholder // **PLACEHOLDER FOR STEP 2: GET PAYMENT STREAMS** with the following code:
// Get payment streams from MSP
const paymentStreams = await getPaymentStreams();
console.log('Payment Streams:', paymentStreams);
View complete index.ts up until this point
import '@storagehub/api-augment';
import { initWasm } from '@storagehub-sdk/core';
import { address, polkadotApi } from './services/clientService.js';
import { authenticateUser, getPaymentStreams } from './services/mspService.js';
import {
calculateTimeRemaining,
getBalance,
} from './operations/costOperations.js';
async function run() {
// --- Cost tracking logic ---
// Authenticate
const authProfile = await authenticateUser();
console.log('Authenticated user profile:', authProfile);
// Get payment streams from MSP
const paymentStreams = await getPaymentStreams();
console.log('Payment Streams:', paymentStreams);
// **PLACEHOLDER FOR STEP 3: CALCULATE TIME REMAINING**
// Disconnect the Polkadot API at the very end
await polkadotApi.disconnect();
}
await run();
If you run the script with the code above, the response should look something like this:
Payment Streams: {
streams: [
{
provider: '0x0000000000000000000000000000000000000000000000000000000000000010',
providerType: 'bsp',
totalAmountPaid: '4043906586769',
costPerTick: '10125385'
},
{
provider: '0x0000000000000000000000000000000000000000000000000000000000000010',
providerType: 'bsp',
totalAmountPaid: '115084472242',
costPerTick: '554183'
},
{
provider: '0x0000000000000000000000000000000000000000000000000000000000000001',
providerType: 'msp',
totalAmountPaid: '2060982766347038397',
costPerTick: '3450008424335'
},
{
provider: '0xe4effe4a3890ae69ab5236729c55542f9540fc5e3bfaf2e44212731ba8d3cb1a',
providerType: 'bsp',
totalAmountPaid: '25334302',
costPerTick: '2514'
},
{
provider: '0x0000000000000000000000000000000000000000000000000000000000000020',
providerType: 'bsp',
totalAmountPaid: '4775995222447',
costPerTick: '11974060'
}
]
}
Calculate Remaining Time¶
In this section, you'll learn how to use the balance of your account, along with data gathered from all of your payment streams, to calculate how much time remains before your account runs out of funds due to the DataHaven network's recurring costs.
Warning
If your balance reaches zero, all buckets and files will be permanently deleted. Keep funds topped up to avoid losing data.
Add Method to Calculate Remaining Time¶
To implement the calculateRemainingTime helper method along with a few other methods to get your account's balance and format the remaining time in a human-readable way, follow these steps:
-
Create a new folder called
operationswithin thesrcfolder (at the same level as theservicesfolder), like so: -
Create a new file within the
operationsfolder calledcostOperations.ts. -
Add the following code:
costOperations.tsimport { formatEther } from 'viem'; import { publicClient } from '../services/clientService.js'; import { PaymentStreamInfo, PaymentStreamsResponse, } from '@storagehub-sdk/msp-client'; interface TimeRemaining { ticks: bigint; seconds: bigint; formatted: string; } const TICK_DURATION_SECONDS = 6n; // 6 seconds per block const DECIMALS = 18; const calculateTimeRemaining = ( balanceInWei: bigint, streams: PaymentStreamsResponse, ): TimeRemaining => { // Sum all costPerTick values const totalCostPerTick = streams.streams.reduce( (sum: bigint, stream: PaymentStreamInfo) => sum + BigInt(stream.costPerTick), 0n, ); if (totalCostPerTick === 0n) { return { ticks: 0n, seconds: 0n, formatted: 'No active streams' }; } // Ticks remaining = balance / total cost per tick const ticksRemaining = balanceInWei / totalCostPerTick; const secondsRemaining = ticksRemaining * TICK_DURATION_SECONDS; return { ticks: ticksRemaining, seconds: secondsRemaining, formatted: formatDuration(secondsRemaining), }; }; const formatDuration = (totalSeconds: bigint): string => { const SECONDS_PER_MINUTE = 60n; const SECONDS_PER_HOUR = 3600n; const SECONDS_PER_DAY = 86400n; const SECONDS_PER_MONTH = 2629746n; // ~30.44 days const SECONDS_PER_YEAR = 31556952n; // ~365.25 days const years = totalSeconds / SECONDS_PER_YEAR; const months = (totalSeconds % SECONDS_PER_YEAR) / SECONDS_PER_MONTH; if (years > 0n) { return `${years} years, ${months} months`; } const days = totalSeconds / SECONDS_PER_DAY; const hoursWithinDay = (totalSeconds % SECONDS_PER_DAY) / SECONDS_PER_HOUR; if (days > 0n) { return `${days} days, ${hoursWithinDay} hours`; } const hours = totalSeconds / SECONDS_PER_HOUR; const minutes = (totalSeconds % SECONDS_PER_HOUR) / SECONDS_PER_MINUTE; if (hours > 0n) { return `${hours} hours, ${minutes} minutes`; } return `${minutes} minutes`; }; const getBalance = async (address: `0x${string}`): Promise<bigint> => { // Query balance // const balance = parseFloat(formatEther(await publicClient.getBalance({ address }))); const balanceWei = await publicClient.getBalance({ address }); console.log(`Address: ${address}`); console.log(`Balance: ${balanceWei} (wei)`); console.log(`Balance: ${Number(formatEther(balanceWei))} (MOCK)`); return balanceWei; }; export { calculateTimeRemaining, formatDuration, getBalance };
Call Calculate Remaining Time Helper Method¶
Replace the placeholder // **PLACEHOLDER FOR STEP 3: CALCULATE TIME REMAINING** with the following code:
// Calculate time remaining based on balance and payment streams
const balance = await getBalance(address); // MOCK tokens in wei
const result = calculateTimeRemaining(balance, paymentStreams);
console.log(`Time remaining: ${result.formatted}`);
View complete index.ts
import '@storagehub/api-augment';
import { initWasm } from '@storagehub-sdk/core';
import { address, polkadotApi } from './services/clientService.js';
import { authenticateUser, getPaymentStreams } from './services/mspService.js';
import {
calculateTimeRemaining,
getBalance,
} from './operations/costOperations.js';
async function run() {
// --- Cost tracking logic ---
// Authenticate
const authProfile = await authenticateUser();
console.log('Authenticated user profile:', authProfile);
// Get payment streams from MSP
const paymentStreams = await getPaymentStreams();
console.log('Payment Streams:', paymentStreams);
// Calculate time remaining based on balance and payment streams
const balance = await getBalance(address); // MOCK tokens in wei
const result = calculateTimeRemaining(balance, paymentStreams);
console.log(`Time remaining: ${result.formatted}`);
// Disconnect the Polkadot API at the very end
await polkadotApi.disconnect();
}
await run();
If you run the script with the code above, the response should look something like this:
Balance: 9968298036095017577707761 (wei) Balance: 9968298.036095018 (MOCK) Time remaining: 533863 years, 2 months
View complete mspService.ts
import {
HealthStatus,
InfoResponse,
MspClient,
UserInfo,
PaymentStreamsResponse,
} from '@storagehub-sdk/msp-client';
import { HttpClientConfig } from '@storagehub-sdk/core';
import { address, walletClient } from './clientService.js';
const NETWORKS = {
devnet: {
id: 181222,
name: 'DataHaven Local Devnet',
rpcUrl: 'http://127.0.0.1:9666',
wsUrl: 'wss://127.0.0.1:9666',
mspUrl: 'http://127.0.0.1:8080/',
nativeCurrency: { name: 'StorageHub', symbol: 'SH', decimals: 18 },
},
testnet: {
id: 55931,
name: 'DataHaven Testnet',
rpcUrl: 'https://services.datahaven-testnet.network/testnet',
wsUrl: 'wss://services.datahaven-testnet.network/testnet',
mspUrl: 'https://deo-dh-backend.testnet.datahaven-infra.network/',
nativeCurrency: { name: 'Mock', symbol: 'MOCK', decimals: 18 },
},
};
// Configure the HTTP client to point to the MSP backend
const httpCfg: HttpClientConfig = { baseUrl: NETWORKS.testnet.mspUrl };
// Initialize a session token for authenticated requests (updated after authentication through SIWE)
let sessionToken: string | undefined = undefined;
// Provide session information to the MSP client whenever available
// Returns a token and user address if authenticated, otherwise undefined
const sessionProvider = async () =>
sessionToken
? ({ token: sessionToken, user: { address: address } } as const)
: undefined;
// Establish a connection to the Main Storage Provider (MSP) backend
const mspClient = await MspClient.connect(httpCfg, sessionProvider);
// Retrieve MSP metadata, including its unique ID and version, and log it to the console
const getMspInfo = async (): Promise<InfoResponse> => {
const mspInfo = await mspClient.info.getInfo();
console.log(`MSP ID: ${mspInfo.mspId}`);
return mspInfo;
};
// Retrieve and log the MSP’s current health status
const getMspHealth = async (): Promise<HealthStatus> => {
const mspHealth = await mspClient.info.getHealth();
console.log(`MSP Health: ${mspHealth}`);
return mspHealth;
};
// Authenticate the user via SIWE (Sign-In With Ethereum) using the connected wallet
// Once authenticated, store the returned session token and retrieve the user’s profile
const authenticateUser = async (): Promise<UserInfo> => {
console.log('Authenticating user with MSP via SIWE...');
// In development domain and uri can be arbitrary placeholders,
// but in production they must match your actual frontend origin.
const domain = 'localhost';
const uri = 'http://localhost';
const siweSession = await mspClient.auth.SIWE(walletClient, domain, uri);
console.log('SIWE Session:', siweSession);
sessionToken = (siweSession as { token: string }).token;
const profile: UserInfo = await mspClient.auth.getProfile();
return profile;
};
const getPaymentStreams = async (): Promise<PaymentStreamsResponse> => {
// Fetch payment streams associated with the authenticated user
const paymentStreams = await mspClient.info.getPaymentStreams();
return paymentStreams;
};
// Make sure to add this method to the export statement at the bottom of the file too
// Export initialized client and helper functions for use in other modules
export {
mspClient,
getMspInfo,
getMspHealth,
authenticateUser,
getPaymentStreams,
};
View complete costOperations.ts
import { formatEther } from 'viem';
import { publicClient } from '../services/clientService.js';
import {
PaymentStreamInfo,
PaymentStreamsResponse,
} from '@storagehub-sdk/msp-client';
interface TimeRemaining {
ticks: bigint;
seconds: bigint;
formatted: string;
}
const TICK_DURATION_SECONDS = 6n; // 6 seconds per block
const DECIMALS = 18;
const calculateTimeRemaining = (
balanceInWei: bigint,
streams: PaymentStreamsResponse,
): TimeRemaining => {
// Sum all costPerTick values
const totalCostPerTick = streams.streams.reduce(
(sum: bigint, stream: PaymentStreamInfo) =>
sum + BigInt(stream.costPerTick),
0n,
);
if (totalCostPerTick === 0n) {
return { ticks: 0n, seconds: 0n, formatted: 'No active streams' };
}
// Ticks remaining = balance / total cost per tick
const ticksRemaining = balanceInWei / totalCostPerTick;
const secondsRemaining = ticksRemaining * TICK_DURATION_SECONDS;
return {
ticks: ticksRemaining,
seconds: secondsRemaining,
formatted: formatDuration(secondsRemaining),
};
};
const formatDuration = (totalSeconds: bigint): string => {
const SECONDS_PER_MINUTE = 60n;
const SECONDS_PER_HOUR = 3600n;
const SECONDS_PER_DAY = 86400n;
const SECONDS_PER_MONTH = 2629746n; // ~30.44 days
const SECONDS_PER_YEAR = 31556952n; // ~365.25 days
const years = totalSeconds / SECONDS_PER_YEAR;
const months = (totalSeconds % SECONDS_PER_YEAR) / SECONDS_PER_MONTH;
if (years > 0n) {
return `${years} years, ${months} months`;
}
const days = totalSeconds / SECONDS_PER_DAY;
const hoursWithinDay = (totalSeconds % SECONDS_PER_DAY) / SECONDS_PER_HOUR;
if (days > 0n) {
return `${days} days, ${hoursWithinDay} hours`;
}
const hours = totalSeconds / SECONDS_PER_HOUR;
const minutes = (totalSeconds % SECONDS_PER_HOUR) / SECONDS_PER_MINUTE;
if (hours > 0n) {
return `${hours} hours, ${minutes} minutes`;
}
return `${minutes} minutes`;
};
const getBalance = async (address: `0x${string}`): Promise<bigint> => {
// Query balance
// const balance = parseFloat(formatEther(await publicClient.getBalance({ address })));
const balanceWei = await publicClient.getBalance({ address });
console.log(`Address: ${address}`);
console.log(`Balance: ${balanceWei} (wei)`);
console.log(`Balance: ${Number(formatEther(balanceWei))} (MOCK)`);
return balanceWei;
};
export { calculateTimeRemaining, formatDuration, getBalance };
Next Steps¶
| Created: January 29, 2026