Skip to content

Upload a File

This guide covers the full path from a local file to a registered asset inside a bucket on DataHaven. This path can be divided into three major steps:

  1. Issue a Storage Request: Register your intent to store a file in your bucket and set its replication policy. Initialize FileManager, compute the file’s fingerprint, fetch MSP info (and extract peer IDs), choose a replication level and replica count, then call issueStorageRequest.
  2. Verify If Storage Request Is On-Chain: Derive the deterministic file key, query on-chain state, and confirm the request exists and matches your local fingerprint and bucket.
  3. Upload a File: Send the file bytes to the MSP, linked to your storage request. Confirm that the upload receipt indicates a successful upload.

These steps form the core workflow for any application that places data into DataHaven.

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
      
  • Dependencies installed

  • Clients initialized

  • 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 5 MB).

Add Method to Upload File

Because of the uploadFile method's complexity, you will be adding pieces of its logic step by step. Before that, you need to prepare the file and the method's imports, by following 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 fileOperations.ts.

  3. Add the following code:

    fileOperations.ts
    import { createReadStream, statSync } from 'node:fs';
    import { Readable } from 'node:stream';
    import { FileManager, ReplicationLevel } from '@storagehub-sdk/core';
    import { TypeRegistry } from '@polkadot/types';
    import { AccountId20, H256 } from '@polkadot/types/interfaces';
    import {
      storageHubClient,
      address,
      publicClient,
      polkadotApi,
      account,
    } from '../services/clientService.js';
    import {
      mspClient,
      getMspInfo,
      authenticateUser,
    } from '../services/mspService.js';
    
    export async function uploadFile(
      bucketId: string,
      filePath: string,
      fileName: string
    ) {
      // ISSUE STORAGE REQUEST
      // **PLACEHOLDER FOR STEP 1: INITIALIZE FILE MANAGER**
      // **PLACEHOLDER FOR STEP 2: CREATE FINGERPRINT**
      // **PLACEHOLDER FOR STEP 3: ISSUE STORAGE REQUEST**
      // VERIFY STORAGE REQUEST ON-CHAIN
      // **PLACEHOLDER FOR STEP 4: COMPUTE FILE KEY**
      // **PLACEHOLDER FOR STEP 5: RETRIEVE STORAGE REQUEST DATA**
      // **PLACEHOLDER FOR STEP 6: READ STORAGE REQUEST DATA**
      // UPLOAD FILE
      // **PLACEHOLDER FOR STEP 7: AUTHENTICATE**
      // **PLACEHOLDER FOR STEP 8: UPLOAD FILE TO MSP**
    
      return { fileKey, uploadReceipt };
    }
    

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 section of the guide, you’ll go from a local file to a confirmed on-chain transaction. You'll 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 next section of this guide.

Initialize File Manager

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

fileOperations.ts // **PLACEHOLDER FOR STEP 1: INITIALIZE FILE MANAGER**
// Set up FileManager
const fileSize = statSync(filePath).size;
const fileManager = new FileManager({
  size: fileSize,
  stream: () =>
    Readable.toWeb(createReadStream(filePath)) as ReadableStream<Uint8Array>,
});

Define Storage Request Parameters

To issue a storage request, you need to prepare the following:

  • fingerprint of your file (from FileManager)
  • fileSize in BigInt format
  • mspId of the target MSP
  • peerId extracted from the MSP’s multiaddresses
  • replicationLevel that defines how redundancy is applied
  • replicas indicating how many copies to request
  • bucketId created earlier (already passed as a parameter in uploadFile method)
  • fileName you plan to store (already passed as a parameter in uploadFile method)

Add the following code to gather these values:

fileOperations.ts // **PLACEHOLDER FOR STEP 2: CREATE FINGERPRINT**
// Get file details

const fingerprint = await fileManager.getFingerprint();
console.log(`Fingerprint: ${fingerprint.toHex()}`);

const fileSizeBigInt = BigInt(fileManager.getFileSize());
console.log(`File size: ${fileSize} bytes`);

// Get MSP details

// Fetch MSP details from the backend (includes its on-chain ID and libp2p addresses)
const { mspId, multiaddresses } = await getMspInfo();
// Ensure the MSP exposes at least one multiaddress (required to reach it over libp2p)
if (!multiaddresses?.length) {
  throw new Error('MSP multiaddresses are missing');
}
// Extract the MSP’s libp2p peer IDs from the multiaddresses
// Each address should contain a `/p2p/<peerId>` segment
const peerIds: string[] = extractPeerIDs(multiaddresses);
// Validate that at least one valid peer ID was found
if (peerIds.length === 0) {
  throw new Error('MSP multiaddresses had no /p2p/<peerId> segment');
}

// Extracts libp2p peer IDs from a list of multiaddresses.
// A multiaddress commonly ends with `/p2p/<peerId>`, so this function
// splits on that delimiter and returns the trailing segment when present.
function extractPeerIDs(multiaddresses: string[]): string[] {
  return (multiaddresses ?? [])
    .map((addr) => addr.split('/p2p/').pop())
    .filter((id): id is string => !!id);
}

// Set the redundancy policy for this request.
// Custom replication allows the client to specify an exact replica count.
const replicationLevel = ReplicationLevel.Custom;
const replicas = 1;

Issue Storage Request

Issue the storage request by adding the following code:

fileOperations.ts // **PLACEHOLDER FOR STEP 3: ISSUE STORAGE REQUEST**
// 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
const receipt = await publicClient.waitForTransactionReceipt({
  hash: txHash,
});
if (receipt.status !== 'success') {
  throw new Error(`Storage request failed: ${txHash}`);
}
console.log('issueStorageRequest() txReceipt:', receipt);

Upon a successful storage request, the output will look something like this:

ts-node index.ts issueStorageRequest() txHash: 0x1cb9446510d9f204c93f1c348e0a13422adef91f1740ea0fdb1534e3ccb232ef
issueStorageRequest() txReceipt: {
  transactionHash: '0x1cb9446510d9f204c93f1c348e0a13422adef91f1740ea0fdb1534e3ccb232ef',
  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 fileOperations.ts up until this point
fileOperations.ts
import { createReadStream, statSync } from 'node:fs';
import { Readable } from 'node:stream';
import { FileManager, ReplicationLevel } from '@storagehub-sdk/core';
import { TypeRegistry } from '@polkadot/types';
import { AccountId20, H256 } from '@polkadot/types/interfaces';
import {
  storageHubClient,
  address,
  publicClient,
  polkadotApi,
  account,
} from '../services/clientService.js';
import {
  mspClient,
  getMspInfo,
  authenticateUser,
} from '../services/mspService.js';

export async function uploadFile(
  bucketId: string,
  filePath: string,
  fileName: string
) {

  // ISSUE STORAGE REQUEST
  // Set up FileManager
  const fileSize = statSync(filePath).size;
  const fileManager = new FileManager({
    size: fileSize,
    stream: () =>
      Readable.toWeb(createReadStream(filePath)) as ReadableStream<Uint8Array>,
  });
  // Get file details

  const fingerprint = await fileManager.getFingerprint();
  console.log(`Fingerprint: ${fingerprint.toHex()}`);

  const fileSizeBigInt = BigInt(fileManager.getFileSize());
  console.log(`File size: ${fileSize} bytes`);

  // Get MSP details

  // Fetch MSP details from the backend (includes its on-chain ID and libp2p addresses)
  const { mspId, multiaddresses } = await getMspInfo();
  // Ensure the MSP exposes at least one multiaddress (required to reach it over libp2p)
  if (!multiaddresses?.length) {
    throw new Error('MSP multiaddresses are missing');
  }
  // Extract the MSP’s libp2p peer IDs from the multiaddresses
  // Each address should contain a `/p2p/<peerId>` segment
  const peerIds: string[] = extractPeerIDs(multiaddresses);
  // Validate that at least one valid peer ID was found
  if (peerIds.length === 0) {
    throw new Error('MSP multiaddresses had no /p2p/<peerId> segment');
  }

  // Extracts libp2p peer IDs from a list of multiaddresses.
  // A multiaddress commonly ends with `/p2p/<peerId>`, so this function
  // splits on that delimiter and returns the trailing segment when present.
  function extractPeerIDs(multiaddresses: string[]): string[] {
    return (multiaddresses ?? [])
      .map((addr) => addr.split('/p2p/').pop())
      .filter((id): id is string => !!id);
  }

  // Set the redundancy policy for this request.
  // Custom replication allows the client to specify an exact replica count.
  const replicationLevel = ReplicationLevel.Custom;
  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
  const receipt = await publicClient.waitForTransactionReceipt({
    hash: txHash,
  });
  if (receipt.status !== 'success') {
    throw new Error(`Storage request failed: ${txHash}`);
  }
  console.log('issueStorageRequest() txReceipt:', receipt);

  // VERIFY STORAGE REQUEST ON-CHAIN
  // **PLACEHOLDER FOR STEP 4: COMPUTE FILE KEY **
  // **PLACEHOLDER FOR STEP 5: RETRIEVE STORAGE REQUEST DATA **
  // **PLACEHOLDER FOR STEP 6: READ STORAGE REQUEST DATA**
  // UPLOAD FILE
  // **PLACEHOLDER FOR STEP 7: AUTHENTICATE **
  // **PLACEHOLDER FOR STEP 8: UPLOAD FILE TO MSP **

  return { fileKey, uploadReceipt };
}

Verify Storage Request Registration

Use this section of the guide to confirm that a file's storage request has been successfully recorded on-chain. You'll learn how to derive the deterministic file key and query the on-chain storage requests via the Polkadot.js API. A successful check confirms that the request exists and that core fields, such as the bucket ID and content fingerprint, match your local values. If no record is found, the transaction may not have been finalized yet, or one of the inputs used to compute the file key may not exactly match what was used when the request was issued.

Compute the File Key

To compute the deterministic file key, derive it from the owner (AccountId20), bucket ID, and file name:

fileOperations.ts // **PLACEHOLDER FOR STEP 4: COMPUTE THE FILE KEY**
// Compute file key
const registry = new TypeRegistry();
const owner = registry.createType(
  'AccountId20',
  account.address
) as AccountId20;
const bucketIdH256 = registry.createType('H256', bucketId) as H256;
const fileKey = await fileManager.computeFileKey(
  owner,
  bucketIdH256,
  fileName
);

Retrieve Storage Request Data

To retrieve storage request data, query fileSystem.storageRequests and pass in the computed file key:

fileOperations.ts // **PLACEHOLDER FOR STEP 5: RETRIEVE STORAGE REQUEST DATA**
// Verify storage request on chain
const storageRequest = await polkadotApi.query.fileSystem.storageRequests(
  fileKey
);
if (!storageRequest.isSome) {
  throw new Error('Storage request not found on chain');
}

Read Storage Request Data

To read storage request data, it first must be unwrapped as follows:

fileOperations.ts // **PLACEHOLDER FOR STEP 6: READ STORAGE REQUEST DATA**
// Read the storage request data
const storageRequestData = storageRequest.unwrap().toHuman();
console.log('Storage request data:', storageRequestData);
console.log(
  'Storage request bucketId matches initial bucketId:',
  storageRequestData.bucketId === bucketId
);
console.log(
  'Storage request fingerprint matches initial fingerprint: ',
  storageRequestData.fingerprint === fingerprint.toString()
);

Upon successful storage request verification, you'll see a message like:

ts-node index.ts
 Storage request data: {
  requestedAt: '387,185',
  expiresAt: '387,295',
  owner: '0x00FA35D84a43db75467D2B2c1ed8974aCA57223e',
  bucketId: '0x8009cc4028ab4c8e333b13d38b840107f8467e27be11e9624e3b0d505314a5da',
  location: 'helloworld.txt',
  fingerprint: '0x1bc3a71173c16c1eee04f7e7cf2591678b0b6cdf08eb81c638ae60a38b706aad',
  size_: '18',
  msp: [
    '0x0000000000000000000000000000000000000000000000000000000000000001',
    false
  ],
  userPeerIds: [
    '12D3KooWNEor6iiEAbZhCXqJbXibdjethDY8oeDoieVVxpZhQcW1',
    '12D3KooWNEor6iiEAbZhCXqJbXibdjethDY8oeDoieVVxpZhQcW1',
    '12D3KooWNEor6iiEAbZhCXqJbXibdjethDY8oeDoieVVxpZhQcW1'
  ],
  bspsRequired: '1',
  bspsConfirmed: '0',
  bspsVolunteered: '0',
  depositPaid: '1,000,010,114,925,524,930'
}
'Storage request bucketId matches initial bucketId: true Storage request fingerprint matches initial fingerprint: true
View complete fileOperations.ts up until now
fileOperations.ts
import { createReadStream, statSync } from 'node:fs';
import { Readable } from 'node:stream';
import { FileManager, ReplicationLevel } from '@storagehub-sdk/core';
import { TypeRegistry } from '@polkadot/types';
import { AccountId20, H256 } from '@polkadot/types/interfaces';
import {
  storageHubClient,
  address,
  publicClient,
  polkadotApi,
  account,
} from '../services/clientService.js';
import {
  mspClient,
  getMspInfo,
  authenticateUser,
} from '../services/mspService.js';

export async function uploadFile(
  bucketId: string,
  filePath: string,
  fileName: string
) {

  // ISSUE STORAGE REQUEST
  // Set up FileManager
  const fileSize = statSync(filePath).size;
  const fileManager = new FileManager({
    size: fileSize,
    stream: () =>
      Readable.toWeb(createReadStream(filePath)) as ReadableStream<Uint8Array>,
  });
  // Get file details

  const fingerprint = await fileManager.getFingerprint();
  console.log(`Fingerprint: ${fingerprint.toHex()}`);

  const fileSizeBigInt = BigInt(fileManager.getFileSize());
  console.log(`File size: ${fileSize} bytes`);

  // Get MSP details

  // Fetch MSP details from the backend (includes its on-chain ID and libp2p addresses)
  const { mspId, multiaddresses } = await getMspInfo();
  // Ensure the MSP exposes at least one multiaddress (required to reach it over libp2p)
  if (!multiaddresses?.length) {
    throw new Error('MSP multiaddresses are missing');
  }
  // Extract the MSP’s libp2p peer IDs from the multiaddresses
  // Each address should contain a `/p2p/<peerId>` segment
  const peerIds: string[] = extractPeerIDs(multiaddresses);
  // Validate that at least one valid peer ID was found
  if (peerIds.length === 0) {
    throw new Error('MSP multiaddresses had no /p2p/<peerId> segment');
  }

  // Extracts libp2p peer IDs from a list of multiaddresses.
  // A multiaddress commonly ends with `/p2p/<peerId>`, so this function
  // splits on that delimiter and returns the trailing segment when present.
  function extractPeerIDs(multiaddresses: string[]): string[] {
    return (multiaddresses ?? [])
      .map((addr) => addr.split('/p2p/').pop())
      .filter((id): id is string => !!id);
  }

  // Set the redundancy policy for this request.
  // Custom replication allows the client to specify an exact replica count.
  const replicationLevel = ReplicationLevel.Custom;
  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
  const receipt = await publicClient.waitForTransactionReceipt({
    hash: txHash,
  });
  if (receipt.status !== 'success') {
    throw new Error(`Storage request failed: ${txHash}`);
  }
  console.log('issueStorageRequest() txReceipt:', receipt);

  // VERIFY STORAGE REQUEST ON-CHAIN
  // Compute file key
  const registry = new TypeRegistry();
  const owner = registry.createType(
    'AccountId20',
    account.address
  ) as AccountId20;
  const bucketIdH256 = registry.createType('H256', bucketId) as H256;
  const fileKey = await fileManager.computeFileKey(
    owner,
    bucketIdH256,
    fileName
  );
  // Verify storage request on chain
  const storageRequest = await polkadotApi.query.fileSystem.storageRequests(
    fileKey
  );
  if (!storageRequest.isSome) {
    throw new Error('Storage request not found on chain');
  }
  // Read the storage request data
  const storageRequestData = storageRequest.unwrap().toHuman();
  console.log('Storage request data:', storageRequestData);
  console.log(
    'Storage request bucketId matches initial bucketId:',
    storageRequestData.bucketId === bucketId
  );
  console.log(
    'Storage request fingerprint matches initial fingerprint: ',
    storageRequestData.fingerprint === fingerprint.toString()
  );

  // UPLOAD FILE
  // **PLACEHOLDER FOR STEP 7: AUTHENTICATE **
  // **PLACEHOLDER FOR STEP 8: UPLOAD FILE TO MSP **

  return { fileKey, uploadReceipt };
}

Upload a File

Once your bucket is ready and the storage request has been successfully recorded on-chain, it's time to upload your file's bytes to your selected Main Storage Provider (MSP), linking the data to your on-chain request.

This section walks you through preparing your local file for upload and confirming your MSP has successfully accepted it for ingestion and replication.

Authenticate

Before any file operations, authenticate with the MSP. The authenticateUser helper signs a SIWE message and returns a session token that authorizes your uploads, updates, and deletions. Add the following code to use the authenticateUser helper method you've already implemented in mspService.ts:

fileOperations.ts // **PLACEHOLDER FOR STEP 7: AUTHENTICATE**
// Authenticating the bucket owner address with MSP prior to file upload is required
const authProfile = await authenticateUser();
console.log('Authenticated user profile:', authProfile);

Upload File to MSP

Add the following code to trigger the file upload to the connected MSP and to verify if it was successful:

fileOperations.ts // **PLACEHOLDER FOR STEP 8: UPLOAD FILE TO MSP**
// Upload file to MSP
const uploadReceipt = await mspClient.files.uploadFile(
  bucketId,
  fileKey.toHex(),
  await fileManager.getFileBlob(),
  address,
  fileName
);
console.log('File upload receipt:', uploadReceipt);

if (uploadReceipt.status !== 'upload_successful') {
  throw new Error('File upload to MSP failed');
}

Note

To check your currently active payment streams (amount of fees you are being billed) within a certain MSP use the mspClient.info.getPaymentStreams method. Make sure you are authenticated prior to triggering this function.

Upon a successful file upload, the transaction receipt will look like this:

ts-node index.ts
File upload receipt: {
  status: 'upload_successful',
  fileKey: '0x8345bdd406fd9df119757b77c84e16a2e304276372dc21cb37a69a471ee093a6',
  bucketId: '0xdd2148ff63c15826ab42953a9d214770e6c2a73b22b83d28819a1777ab9d1322',
  fingerprint: '0x1bc3a71173c16c1eee04f7e7cf2591678b0b6cdf08eb81c638ae60a38b706aad',
  location: 'helloworld.txt'
}
View complete fileOperations.ts
fileOperations.ts
import { createReadStream, statSync } from 'node:fs';
import { Readable } from 'node:stream';
import { FileManager, ReplicationLevel } from '@storagehub-sdk/core';
import { TypeRegistry } from '@polkadot/types';
import { AccountId20, H256 } from '@polkadot/types/interfaces';
import {
  storageHubClient,
  address,
  publicClient,
  polkadotApi,
  account,
} from '../services/clientService.js';
import {
  mspClient,
  getMspInfo,
  authenticateUser,
} from '../services/mspService.js';

export async function uploadFile(
  bucketId: string,
  filePath: string,
  fileName: string
) {
  //   ISSUE STORAGE REQUEST

  // Set up FileManager
  const fileSize = statSync(filePath).size;
  const fileManager = new FileManager({
    size: fileSize,
    stream: () =>
      Readable.toWeb(createReadStream(filePath)) as ReadableStream<Uint8Array>,
  });

  // Get file details

  const fingerprint = await fileManager.getFingerprint();
  console.log(`Fingerprint: ${fingerprint.toHex()}`);

  const fileSizeBigInt = BigInt(fileManager.getFileSize());
  console.log(`File size: ${fileSize} bytes`);

  // Get MSP details

  // Fetch MSP details from the backend (includes its on-chain ID and libp2p addresses)
  const { mspId, multiaddresses } = await getMspInfo();
  // Ensure the MSP exposes at least one multiaddress (required to reach it over libp2p)
  if (!multiaddresses?.length) {
    throw new Error('MSP multiaddresses are missing');
  }
  // Extract the MSP’s libp2p peer IDs from the multiaddresses
  // Each address should contain a `/p2p/<peerId>` segment
  const peerIds: string[] = extractPeerIDs(multiaddresses);
  // Validate that at least one valid peer ID was found
  if (peerIds.length === 0) {
    throw new Error('MSP multiaddresses had no /p2p/<peerId> segment');
  }

  // Extracts libp2p peer IDs from a list of multiaddresses.
  // A multiaddress commonly ends with `/p2p/<peerId>`, so this function
  // splits on that delimiter and returns the trailing segment when present.
  function extractPeerIDs(multiaddresses: string[]): string[] {
    return (multiaddresses ?? [])
      .map((addr) => addr.split('/p2p/').pop())
      .filter((id): id is string => !!id);
  }

  // Set the redundancy policy for this request.
  // Custom replication allows the client to specify an exact replica count.
  const replicationLevel = ReplicationLevel.Custom;
  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
  const receipt = await publicClient.waitForTransactionReceipt({
    hash: txHash,
  });
  if (receipt.status !== 'success') {
    throw new Error(`Storage request failed: ${txHash}`);
  }
  console.log('issueStorageRequest() txReceipt:', receipt);

  //   VERIFY STORAGE REQUEST ON CHAIN

  // Compute file key
  const registry = new TypeRegistry();
  const owner = registry.createType(
    'AccountId20',
    account.address
  ) as AccountId20;
  const bucketIdH256 = registry.createType('H256', bucketId) as H256;
  const fileKey = await fileManager.computeFileKey(
    owner,
    bucketIdH256,
    fileName
  );

  // Verify storage request on chain
  const storageRequest = await polkadotApi.query.fileSystem.storageRequests(
    fileKey
  );
  if (!storageRequest.isSome) {
    throw new Error('Storage request not found on chain');
  }

  // Read the storage request data
  const storageRequestData = storageRequest.unwrap().toHuman();
  console.log('Storage request data:', storageRequestData);
  console.log(
    'Storage request bucketId matches initial bucketId:',
    storageRequestData.bucketId === bucketId
  );
  console.log(
    'Storage request fingerprint matches initial fingerprint: ',
    storageRequestData.fingerprint === fingerprint.toString()
  );

  //   UPLOAD FILE TO MSP

  // Authenticating the bucket owner address with MSP prior to file upload is required
  const authProfile = await authenticateUser();
  console.log('Authenticated user profile:', authProfile);

  // Upload file to MSP
  const uploadReceipt = await mspClient.files.uploadFile(
    bucketId,
    fileKey.toHex(),
    await fileManager.getFileBlob(),
    address,
    fileName
  );
  console.log('File upload receipt:', uploadReceipt);

  if (uploadReceipt.status !== 'upload_successful') {
    throw new Error('File upload to MSP failed');
  }

  return { fileKey, uploadReceipt };
}

Call the Upload File Helper Method

Create an index.ts file if you haven't already. Its run method will orchestrate all the logic in this guide. 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 fileOperations.ts, which you've created already through the previous sections in this guide.

Add the following code to your index.ts file:

index.ts
  import '@storagehub/api-augment';
  import { initWasm } from '@storagehub-sdk/core';
  import { polkadotApi } from './services/clientService.js';
  import { uploadFile } from './operations/fileOperations.js';

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

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


    // Specify the file name of the file to be uploaded
    const fileName = 'INSERT_FILE_NAME'; // Example: filename.jpeg
    const filePath = new URL(`../files/${fileName}`, import.meta.url).pathname;

    // Upload file
    const { fileKey, uploadReceipt } = await uploadFile(
      bucketId,
      filePath,
      fileName
    );
    console.log(`File uploaded: ${fileKey}`);
    console.log(`Status: ${uploadReceipt.status}`);

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

  run();

Run the script:

ts-node index.ts

Now that you have completed fileOperations.ts and index.ts, the final output when running the index.ts script should be:

ts-node index.ts File uploaded: 0x8345bdd406fd9df119757b77c84e16a2e304276372dc21cb37a69a471ee093a6 Status: upload_successful
View complete index.ts
index.ts
import '@storagehub/api-augment';
import { initWasm } from '@storagehub-sdk/core';
import { polkadotApi } from './services/clientService.js';
import { uploadFile } from './operations/fileOperations.js';

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

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


  // Specify the file name of the file to be uploaded
  const fileName = 'INSERT_FILE_NAME'; // Example: filename.jpeg
  const filePath = new URL(`../files/${fileName}`, import.meta.url).pathname;

  // Upload file
  const { fileKey, uploadReceipt } = await uploadFile(
    bucketId,
    filePath,
    fileName
  );
  console.log(`File uploaded: ${fileKey}`);
  console.log(`Status: ${uploadReceipt.status}`);

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

run();

Next Steps

Last update: December 5, 2025
| Created: November 6, 2025