Skip to content

Issue a Storage Request

A storage request is the instruction that tells DataHaven—through your chosen Main Storage Provider (MSP)—to persist a specific file in a bucket with the redundancy policy you select.

In this guide, you’ll go from a local file to a confirmed on-chain transaction: initialize a File Manager, derive the file’s fingerprint, fetch MSP details (including peer IDs), choose a replication level, and issue the storage request. When the transaction is finalized, you’ll have a transaction hash and an on-chain record of the request you can verify in the follow-up guide.

Prerequisites

  • 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 ts-node @types/node
      
      yarn add -D typescript ts-node @types/node
      
      npm install -D typescript 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
      
  • A bucket created with the ID handy

  • A file to upload to DataHaven (any file type is accepted; the current TestNet file size limit is 2 GB).

Install Dependencies

pnpm add @storagehub-sdk/core @storagehub-sdk/msp-client @storagehub/types-bundle @polkadot/api @storagehub/api-augment viem
yarn add @storagehub-sdk/core @storagehub-sdk/msp-client @storagehub/types-bundle @polkadot/api @storagehub/api-augment viem
npm install @storagehub-sdk/core @storagehub-sdk/msp-client @storagehub/types-bundle @polkadot/api @storagehub/api-augment viem

Initialize Clients

First, you'll need to set up the necessary clients to connect to the DataHaven network, which runs on a dual-protocol architecture (Substrate for core logic and EVM for compatibility).

If you’ve already followed the Create a Bucket guide, your clients may already be initialized. Review the placeholders at the bottom of the following snippet to see where you'll add logic in this guide, then skip ahead to Initialize File Manager.

Create an index.ts and add the following code:

Note

The code below uses DataHaven Testnet configuration values, which include the Chain ID, RPC URL, WSS URL, MSP URL, and token metadata. If you’re running a local devnet, make sure to replace these with your local configuration parameters. You can find all the relevant local devnet values in the Starter Kit.

index.ts
import '@storagehub/api-augment';
import { ApiPromise, WsProvider } from '@polkadot/api';
import { types } from '@storagehub/types-bundle';
import {
  FileManager,
  HttpClientConfig,
  ReplicationLevel,
  StorageHubClient,
  initWasm,
} from '@storagehub-sdk/core';
import { HealthStatus, MspClient } from '@storagehub-sdk/msp-client';
import { createReadStream, statSync } from 'node:fs';
import { Readable } from 'node:stream';
import {
  Chain,
  PublicClient,
  WalletClient,
  createPublicClient,
  createWalletClient,
  defineChain,
  http,
} from 'viem';
import { privateKeyToAccount } from 'viem/accounts';

async function run() {
  // For anything from @storagehub-sdk/core to work, initWasm() is required
  // on top of the file
  await initWasm();

  // --- viem setup ---
  // Define DataHaven chain, as expected by viem
  const chain: Chain = defineChain({
    id: 55931,
    name: 'DataHaven Testnet',
    nativeCurrency: { name: 'Mock', symbol: 'MOCK', decimals: 18 },
    rpcUrls: {
      default: { http: ['https://services.datahaven-testnet.network/testnet'] },
    },
  });

  // Define account from a private key
  const account = privateKeyToAccount('INSERT_PRIVATE_KEY' as `0x${string}`);
  const address = account.address;

  // Create a wallet client using the defined chain, account, and RPC URL
  const walletClient: WalletClient = createWalletClient({
    chain,
    account,
    transport: http('https://services.datahaven-testnet.network/testnet'),
  });

  // Create a public client using the defined chain and RPC URL
  const publicClient: PublicClient = createPublicClient({
    chain,
    transport: http('https://services.datahaven-testnet.network/testnet'),
  });

  // --- Polkadot.js API setup ---
  const provider = new WsProvider(
    'wss://services.datahaven-testnet.network/testnet'
  );
  const polkadotApi: ApiPromise = await ApiPromise.create({
    provider,
    typesBundle: types,
    noInitWarn: true,
  });

  // --- Initialize MSP client ---
  // Base URL of the MSP backend you want to interact with.
  const baseUrl = 'https://deo-dh-backend.testnet.datahaven-infra.network/';

  // Configuration for the HTTP client used by the SDK internally.
  const httpConfig: HttpClientConfig = { baseUrl: baseUrl };

  // A temporary authentication token obtained after Sign-In with Ethereum (SIWE).
  // If not yet authenticated, this will remain undefined and the client will operate in read-only mode.
  // Authentication is not required for issuing storage requests, but is needed for other operations like file uploads and bucket management.
  let sessionToken: string | undefined = undefined;

  // Provides the SDK with session data when available.
  // This callback is automatically invoked by the MSP Client whenever it needs to authenticate a request.
  const sessionProvider = async () =>
    sessionToken
      ? ({ token: sessionToken, user: { address: address } } as const)
      : undefined;

  // Create an instance of the MSP Client and establish connection with the backend.
  const mspClient = await MspClient.connect(httpConfig, sessionProvider);

  // Check MSP Health Status
  const mspHealth: HealthStatus = await mspClient.info.getHealth();
  console.log('MSP service health:', mspHealth);

  // --- Initialize StorageHub client ---
  const storageHubClient = new StorageHubClient({
    rpcUrl: 'https://services.datahaven-testnet.network/testnet',
    chain: chain,
    walletClient: walletClient,
    filesystemContractAddress:
      '0x0000000000000000000000000000000000000404' as `0x${string}`,
  });

  // --- Issue storage request logic ---
  // **PLACEHOLDER FOR STEP 1: INITIALIZE FILE MANAGER**
  // **PLACEHOLDER FOR STEP 2: CREATE FINGERPRINT**
  // **PLACEHOLDER FOR STEP 3: ISSUE STORAGE REQUEST**

  // Disconnect the Polkadot API at the very end
  await polkadotApi.disconnect();
}

await run();

Warning

It is assumed that private keys are securely stored and managed in accordance with standard security practices.

With the above code in place, you now have the following:

  • walletClient: Used for signing and broadcasting transactions using the derived private key.
  • publicClient: Used for reading general public data from the chain, such as checking transaction receipts or block status.
  • polkadotApi: Used for reading code chain logic and state data from the underlying DataHaven Substrate node.

  • mspClient: Used to connect to the MSP client.

  • storageHubClient: Used to connect to the StorageHub client.

Initialize File Manager

To initialize the File Manager, add the following code to your file:

// **PLACEHOLDER FOR STEP 1: INITIALIZE FILE MANAGER**
// Specify the file name of the file to be uploaded
const fileName = 'INSERT_FILE_NAME'; // Example: filename.jpeg

// Specify the file path of the file to be uploaded relative to the location of your index.ts file
const filePath = new URL(`./files/${fileName}`, import.meta.url).pathname;
const fileSize = statSync(filePath).size;

// Initialize a FileManager instance with file metadata and a readable stream.
// The stream converts the local file into a Web-compatible ReadableStream,
// which the SDK uses to handle file uploads to the network
const fileManager = new FileManager({
  size: fileSize,
  stream: () =>
    Readable.toWeb(createReadStream(filePath)) as ReadableStream<Uint8Array>,
});

Create Fingerprint

To create the fingerprint of your file from the File Manager:

// **PLACEHOLDER FOR STEP 2: CREATE FINGERPRINT**
const fingerprint = await fileManager.getFingerprint();

Issue Storage Request

Prepare the remaining parameters and issue the storage request by adding the following code:

// **PLACEHOLDER FOR STEP 3: ISSUE STORAGE REQUEST**
// Add your bucket ID here from the bucket you created earlier
// Example (32byte hash): 0xdd2148ff63c15826ab42953a9d214770e6c8a73b22b83d28819a1777ab9d1322
const bucketId = 'INSERT_BUCKET_ID';

// Get file details
const fileSizeBigInt = BigInt(fileManager.getFileSize());
console.log(`File size: ${fileSizeBigInt} bytes`);
console.log(`Fingerprint: ${fingerprint.toHex()}`);

// Get MSP info
const { mspId, multiaddresses } = await mspClient.info.getInfo();
if (!multiaddresses?.length) {
  throw new Error('MSP multiaddresses are missing');
}

const peerIds: string[] = extractPeerIDs(multiaddresses);
if (peerIds.length === 0) {
  throw new Error('MSP multiaddresses had no /p2p/<peerId> segment');
}

function extractPeerIDs(multiaddresses: string[]): string[] {
  return (multiaddresses ?? [])
    .map((addr) => addr.split('/p2p/').pop())
    .filter((id): id is string => !!id);
}

// Choose replication level - defines the redundancy policy for the storage request.
// Custom level allows specifying exact number of replicas
const replicationLevel = ReplicationLevel.Custom;

// Choose number of replicas - how many additional replicas to request beyond original copy
const replicas = 1;

// Issue storage request
const txHash: `0x${string}` | undefined =
  await storageHubClient.issueStorageRequest(
    bucketId as `0x${string}`,
    fileName,
    fingerprint.toHex() as `0x${string}`,
    fileSizeBigInt,
    mspId as `0x${string}`,
    peerIds,
    replicationLevel,
    replicas
  );
console.log('issueStorageRequest() txHash:', txHash);
if (!txHash) {
  throw new Error('issueStorageRequest() did not return a transaction hash');
}

// Wait for storage request transaction receipt
const receipt = await publicClient.waitForTransactionReceipt({
  hash: txHash,
});
console.log('Storage request receipt:', receipt);
if (receipt.status !== 'success') {
  throw new Error(`Storage request failed: ${txHash}`);
}

Run the script:

ts-node index.ts

Upon a successful storage request, the transaction hash will be output:

issueStorageRequest() txHash: 0x1cb9446510d9f204c93f1c348e0a13422adef91f1740ea0fdb1534e3ccb232ef

And upon a successful storage request, the transaction receipt will be output:

Storage request receipt: {
  transactionHash: '0xfb344dc05359ee4d13189e65fc3230a1998a1802d3a0cf929ffb80a0670d7ce0',
  transactionIndex: 0,
  blockHash: '0x0cd98b5d6050b926e6876a5b09124d1840e2c94d95faffdd6668a659e3c5c6a7',
  from: '0x00fa35d84a43db75467d2b2c1ed8974aca57223e',
  to: '0x0000000000000000000000000000000000000404',
  blockNumber: 98684n,
  cumulativeGasUsed: 239712n,
  gasUsed: 239712n,
  contractAddress: null,
  logs: [
    {
      address: '0x0000000000000000000000000000000000000404',
      topics: [Array],
      data: '0x',
      blockHash: '0x0cd98b5d6050b926e6876a5b09124d1840e2c94d95faffdd6668a659e3c5c6a7',
      blockNumber: 98684n,
      transactionHash: '0xfb344dc05359ee4d13189e65fc3230a1998a1802d3a0cf929ffb80a0670d7ce0',
      transactionIndex: 0,
      logIndex: 0,
      transactionLogIndex: '0x0',
      removed: false
    }
  ],
  logsBloom: '0x00000000000000040000000000000000000000000000000000000000000000040000000000000000000000000001000000000000000000000000080000000000000000040000000000000000000000000000000000000140000000000000000000000000000000000000000000000400000000100000000000000000000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000800800000000000000000000000000200000000000000000000000010000000000000000000000000000080000',
  status: 'success',
  effectiveGasPrice: 1000000000n,
  type: 'legacy'
}
View complete script
index.ts
import '@storagehub/api-augment';
import { ApiPromise, WsProvider } from '@polkadot/api';
import { types } from '@storagehub/types-bundle';
import {
  FileManager,
  HttpClientConfig,
  ReplicationLevel,
  StorageHubClient,
  initWasm,
} from '@storagehub-sdk/core';
import { HealthStatus, MspClient } from '@storagehub-sdk/msp-client';
import { createReadStream, statSync } from 'node:fs';
import { Readable } from 'node:stream';
import {
  Chain,
  PublicClient,
  WalletClient,
  createPublicClient,
  createWalletClient,
  defineChain,
  http,
} from 'viem';
import { privateKeyToAccount } from 'viem/accounts';

async function run() {
  // For anything from @storagehub-sdk/core to work, initWasm() is required
  // on top of the file
  await initWasm();

  // --- viem setup ---
  // Define DataHaven chain, as expected by viem
  const chain: Chain = defineChain({
    id: 55931,
    name: 'DataHaven Testnet',
    nativeCurrency: { name: 'Mock', symbol: 'MOCK', decimals: 18 },
    rpcUrls: {
      default: { http: ['https://services.datahaven-testnet.network/testnet'] },
    },
  });

  // Define account from a private key
  const account = privateKeyToAccount('INSERT_PRIVATE_KEY' as `0x${string}`);
  const address = account.address;

  // Create a wallet client using the defined chain, account, and RPC URL
  const walletClient: WalletClient = createWalletClient({
    chain,
    account,
    transport: http('https://services.datahaven-testnet.network/testnet'),
  });

  // Create a public client using the defined chain and RPC URL
  const publicClient: PublicClient = createPublicClient({
    chain,
    transport: http('https://services.datahaven-testnet.network/testnet'),
  });

  // --- Polkadot.js API setup ---
  const provider = new WsProvider(
    'wss://services.datahaven-testnet.network/testnet'
  );
  const polkadotApi: ApiPromise = await ApiPromise.create({
    provider,
    typesBundle: types,
    noInitWarn: true,
  });

  // --- Initialize MSP client ---
  // Base URL of the MSP backend you want to interact with.
  const baseUrl = 'https://deo-dh-backend.testnet.datahaven-infra.network/';

  // Configuration for the HTTP client used by the SDK internally.
  const httpConfig: HttpClientConfig = { baseUrl: baseUrl };

  // A temporary authentication token obtained after Sign-In with Ethereum (SIWE).
  // If not yet authenticated, this will remain undefined and the client will operate in read-only mode.
  // Authentication is not required for issuing storage requests, but is needed for other operations like file uploads and bucket management.
  let sessionToken: string | undefined = undefined;

  // Provides the SDK with session data when available.
  // This callback is automatically invoked by the MSP Client whenever it needs to authenticate a request.
  const sessionProvider = async () =>
    sessionToken
      ? ({ token: sessionToken, user: { address: address } } as const)
      : undefined;

  // Create an instance of the MSP Client and establish connection with the backend.
  const mspClient = await MspClient.connect(httpConfig, sessionProvider);

  // Check MSP Health Status
  const mspHealth: HealthStatus = await mspClient.info.getHealth();
  console.log('MSP service health:', mspHealth);

  // --- Initialize StorageHub client ---
  const storageHubClient = new StorageHubClient({
    rpcUrl: 'https://services.datahaven-testnet.network/testnet',
    chain: chain,
    walletClient: walletClient,
    filesystemContractAddress:
      '0x0000000000000000000000000000000000000404' as `0x${string}`,
  });

  // --- Issue storage request logic ---
  // Specify the file name of the file to be uploaded
  const fileName = 'INSERT_FILE_NAME'; // Example: filename.jpeg

  // Specify the file path of the file to be uploaded relative to the location of your index.ts file
  const filePath = new URL(`./files/${fileName}`, import.meta.url).pathname;
  const fileSize = statSync(filePath).size;

  // Initialize a FileManager instance with file metadata and a readable stream.
  // The stream converts the local file into a Web-compatible ReadableStream,
  // which the SDK uses to handle file uploads to the network
  const fileManager = new FileManager({
    size: fileSize,
    stream: () =>
      Readable.toWeb(createReadStream(filePath)) as ReadableStream<Uint8Array>,
  });

  const fingerprint = await fileManager.getFingerprint();

  // Add your bucket ID here from the bucket you created earlier
  // Example (32byte hash): 0xdd2148ff63c15826ab42953a9d214770e6c8a73b22b83d28819a1777ab9d1322
  const bucketId = 'INSERT_BUCKET_ID';

  // Get file details
  const fileSizeBigInt = BigInt(fileManager.getFileSize());
  console.log(`File size: ${fileSizeBigInt} bytes`);
  console.log(`Fingerprint: ${fingerprint.toHex()}`);

  // Get MSP info
  const { mspId, multiaddresses } = await mspClient.info.getInfo();
  if (!multiaddresses?.length) {
    throw new Error('MSP multiaddresses are missing');
  }

  const peerIds: string[] = extractPeerIDs(multiaddresses);
  if (peerIds.length === 0) {
    throw new Error('MSP multiaddresses had no /p2p/<peerId> segment');
  }

  function extractPeerIDs(multiaddresses: string[]): string[] {
    return (multiaddresses ?? [])
      .map((addr) => addr.split('/p2p/').pop())
      .filter((id): id is string => !!id);
  }

  // Choose replication level - defines the redundancy policy for the storage request.
  // Custom level allows specifying exact number of replicas
  const replicationLevel = ReplicationLevel.Custom;

  // Choose number of replicas - how many additional replicas to request beyond original copy
  const replicas = 1;

  // Issue storage request
  const txHash: `0x${string}` | undefined =
    await storageHubClient.issueStorageRequest(
      bucketId as `0x${string}`,
      fileName,
      fingerprint.toHex() as `0x${string}`,
      fileSizeBigInt,
      mspId as `0x${string}`,
      peerIds,
      replicationLevel,
      replicas
    );
  console.log('issueStorageRequest() txHash:', txHash);
  if (!txHash) {
    throw new Error('issueStorageRequest() did not return a transaction hash');
  }

  // Wait for storage request transaction receipt
  const receipt = await publicClient.waitForTransactionReceipt({
    hash: txHash,
  });
  console.log('Storage request receipt:', receipt);
  if (receipt.status !== 'success') {
    throw new Error(`Storage request failed: ${txHash}`);
  }

  // Disconnect the Polkadot API at the very end
  await polkadotApi.disconnect();
}

await run();

Next Steps

Last update: November 11, 2025
| Created: October 17, 2025