Upload a large binary (SOCIAL_BLOB)
Binary payloads > 16 KB (images, audio, files, large text) are stored as chunked SOCIAL_BLOB records. Each chunk is a separate transaction. Once finalized, the blobId can be referenced by IMAGE_MINT, AUDIO_MINT, SOCIAL_DM_SEND, SOCIAL_POST_CREATE, CHAT_TURN, etc.
Constants
MAX_CHUNK_BYTES = 400_000 // ~400 KB per SOCIAL_BLOB_CHUNK tx (after base64 encoding)
A 5 MB image = ~13 chunks = 13 transactions plus INIT + FINALIZE = 15 total.
Flow
1. Compute blobId + contentHash from full payload
2. SOCIAL_BLOB_INIT — declare blobId, totalBytes, totalChunks, contentHash
3. SOCIAL_BLOB_CHUNK × N — upload chunks in order
4. SOCIAL_BLOB_FINALIZE — chain verifies reassembled hash matches
5. Reference blobId from your target tx (IMAGE_MINT etc.)
Full code
import {
OmbraClient, Wallet,
buildSocialBlobInitTx, buildSocialBlobChunkTx, buildSocialBlobFinalizeTx,
} from "@ombrachain/sdk";
import { sha256 } from "@noble/hashes/sha256";
import { readFileSync } from "fs";
const client = new OmbraClient({ endpoint: "https://api.ombra-net.com" });
const wallet = Wallet.fromMnemonic("...");
async function uploadBlob(filePath: string, contentType: string): Promise<string> {
const data = readFileSync(filePath);
const totalBytes = data.length;
const contentHash = bytesToHex(sha256(data));
const blobId = contentHash; // content-addressed
const CHUNK_SIZE = 400_000;
const chunks: Buffer[] = [];
for (let i = 0; i < data.length; i += CHUNK_SIZE) {
chunks.push(data.subarray(i, i + CHUNK_SIZE));
}
const totalChunks = chunks.length;
let acct = await client.chain.getAccount(wallet.address);
let nonce = acct.nonce;
// 1. INIT
const initTx = buildSocialBlobInitTx(
wallet.address, blobId, contentType, totalBytes, totalChunks, contentHash,
nonce++, wallet.privateKey,
);
await client.chain.submitTx(initTx);
console.log("init:", blobId);
// 2. CHUNKS
for (let i = 0; i < chunks.length; i++) {
const chunkTx = buildSocialBlobChunkTx(
wallet.address, blobId, i, chunks[i].toString("base64"),
nonce++, wallet.privateKey,
);
await client.chain.submitTx(chunkTx);
console.log(` chunk ${i + 1}/${totalChunks}`);
// Rate limit: wait 100ms between chunks to avoid mempool flood
await new Promise((r) => setTimeout(r, 100));
}
// 3. FINALIZE
const finalizeTx = buildSocialBlobFinalizeTx(
wallet.address, blobId,
nonce++, wallet.privateKey,
);
await client.chain.submitTx(finalizeTx);
console.log("finalized:", blobId);
return blobId;
}
function bytesToHex(b: Uint8Array): string {
return Array.from(b).map(x => x.toString(16).padStart(2, "0")).join("");
}
// Usage
const blobId = await uploadBlob("./photo.png", "image/png");
// Now you can use blobId in an IMAGE_MINT tx
Cost
Each tx has implicit chain costs (proposer fees, mempool inclusion). For a 5 MB blob (15 txs), expect:
- ~15 ×
MIN_BURN_PER_BLOCK = 10micro = 150 micro burned (negligible) - No explicit fee on
SOCIAL_BLOB_*txs
But your account must have enough balance to fund the wallet ops if your custom client charges them.
Verification
After finalize, query the blob:
# Metadata (size, hash, chunks)
curl https://api.ombra-net.com/api/social/blob/BLOB_ID
# Reassembled binary
curl https://api.ombra-net.com/api/social/blob/BLOB_ID/file > restored.png
Compare sha256(restored.png) with the original contentHash — they must match. If not, FINALIZE would have failed.
Race conditions
If two chunks arrive at the chain out of order (e.g., one is delayed in P2P), the chain accepts them by chunkIndex. FINALIZE only succeeds if all [0, totalChunks) are present.
For best reliability:
- Use
nonce++strictly sequentially (don't parallelize chunk submission from same wallet) - After all chunks submitted, wait ~3 blocks (~45s) before FINALIZE to ensure propagation
Re-uploading
Blobs are content-addressed by blobId = sha256(payload). If INIT succeeds for a blobId that already exists, the chain returns 409 Conflict. You can skip the upload and reference the existing blobId directly.