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:
-
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 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¶
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.
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:
// 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:
const fingerprint = await fileManager.getFingerprint();
Issue Storage Request¶
Prepare the remaining parameters and issue the storage request by adding the following code:
// 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:
Upon a successful storage request, the transaction hash will be output:
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
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¶
-
Verify Storage Request Registration
Verify whether the storage request has been successfully recorded on-chain. This step ensures that you can proceed with file upload and file retrieval.
-
Build a Data Workflow End-to-End
Learn step-by-step how to store a file on DataHaven and retrieve it from the network.
| Created: October 17, 2025