Skip to content

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:

    1. Create a new project folder by executing the following command in the terminal:

      mkdir datahaven-project && cd datahaven-project
      
    2. Initialize a package.json file using the correct command for your package manager:

      pnpm init
      
      yarn init
      
      npm init --y
      
    3. Add the TypeScript and Node type definitions to your projects using the correct command for your package manager:

      pnpm add -D typescript tsx ts-node @types/node
      
      yarn add -D typescript tsx ts-node @types/node
      
      npm install -D typescript tsx ts-node @types/node
      
    4. Create a tsconfig.json file in the root of your project and paste the following configuration:

      tsconfig.json
      {
          "compilerOptions": {
              "target": "ES2022",
              "module": "nodenext",
              "moduleResolution": "NodeNext",
              "esModuleInterop": true,
              "strict": true,
              "skipLibCheck": true,
              "outDir": "dist",
              "declaration": true,
              "sourceMap": true
          },
          "include": ["src/**/*.ts"]
      }
      
    5. Initialize the src directory:

      mkdir src && touch src/index.ts
      
  • Dependencies installed

  • Clients initialized

  • 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:

index.ts
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:

index.ts // **PLACEHOLDER FOR STEP 1: AUTHENTICATE**
// Authenticate
const authProfile = await authenticateUser();
console.log('Authenticated user profile:', authProfile);
View complete index.ts up until this point
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);
  // **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:

mspService.ts
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:

index.ts // **PLACEHOLDER FOR STEP 2: GET PAYMENT STREAMS**
// Get payment streams from MSP
const paymentStreams = await getPaymentStreams();
console.log('Payment Streams:', paymentStreams);
View complete index.ts up until this point
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);
  // **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:

ts-node index.ts
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:

  1. Create a new folder called operations within the src folder (at the same level as the services folder), like so:

    mkdir operations
    
  2. Create a new file within the operations folder called costOperations.ts.

  3. Add the following code:

    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 };
    

Call Calculate Remaining Time Helper Method

Replace the placeholder // **PLACEHOLDER FOR STEP 3: CALCULATE TIME REMAINING** with the following code:

index.ts // **PLACEHOLDER FOR STEP 3: CALCULATE TIME REMAINING**
// 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
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:

ts-node index.ts
Balance: 9968298036095017577707761 (wei)
Balance: 9968298.036095018 (MOCK)
Time remaining: 533863 years, 2 months
View complete mspService.ts
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
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

Last update: January 29, 2026
| Created: January 29, 2026