@opendatalabs/vana-sdk
Version:
A TypeScript library for interacting with Vana Network smart contracts.
318 lines • 9.38 kB
JavaScript
import {
StorageError
} from "../index.js";
class PinataStorage {
constructor(config) {
this.config = config;
this.gatewayUrl = config.gatewayUrl ?? "https://gateway.pinata.cloud";
if (!config.jwt) {
throw new StorageError(
"Pinata JWT token is required",
"MISSING_JWT",
"pinata"
);
}
}
config;
apiUrl = "https://api.pinata.cloud";
gatewayUrl;
/**
* Uploads a file to IPFS via Pinata and returns the CID
*
* @remarks
* This method uploads the file to Pinata's IPFS service with enhanced metadata support.
* The file is pinned to ensure availability and can include custom metadata for
* organization and querying. The metadata is stored alongside the file for later retrieval.
*
* @param file - The file to upload to IPFS
* @param filename - Optional custom filename
* @returns Promise that resolves to the IPFS CID (content identifier)
* @throws {StorageError} When the upload fails or no CID is returned
*
* @example
* ```typescript
* const cid = await pinataStorage.upload(fileBlob, {
* name: "user-document.pdf",
* metadata: {
* userId: "user-123",
* category: "documents",
* uploadDate: new Date().toISOString()
* }
* });
* console.log("File pinned to IPFS:", cid);
* ```
*/
async upload(file, filename) {
try {
const fileName = filename ?? `vana-file-${Date.now()}.dat`;
const formData = new FormData();
formData.append("file", file, fileName);
const metadata = {
name: fileName,
keyvalues: {
uploadedBy: "vana-sdk",
timestamp: (/* @__PURE__ */ new Date()).toISOString()
}
};
formData.append("pinataMetadata", JSON.stringify(metadata));
const response = await fetch(`${this.apiUrl}/pinning/pinFileToIPFS`, {
method: "POST",
headers: {
Authorization: `Bearer ${this.config.jwt}`
},
body: formData
});
if (!response.ok) {
const errorText = await response.text();
throw new StorageError(
`Pinata upload failed: ${errorText}`,
"UPLOAD_FAILED",
"pinata"
);
}
const result = await response.json();
const ipfsHash = result.IpfsHash;
if (!ipfsHash) {
throw new StorageError(
"Pinata upload succeeded but no IPFS hash returned",
"NO_HASH_RETURNED",
"pinata"
);
}
return {
url: `ipfs://${ipfsHash}`,
size: file.size,
contentType: file.type ?? "application/octet-stream"
};
} catch (error) {
if (error instanceof StorageError) {
throw error;
}
throw new StorageError(
`Pinata upload error: ${error instanceof Error ? error.message : "Unknown error"}`,
"UPLOAD_ERROR",
"pinata"
);
}
}
async download(cid) {
try {
if (!this.isValidCID(cid)) {
throw new StorageError(
"Invalid IPFS CID format",
"INVALID_CID",
"pinata"
);
}
const downloadUrl = `${this.gatewayUrl}/ipfs/${cid}`;
const response = await fetch(downloadUrl);
if (!response.ok) {
throw new StorageError(
`Failed to download from IPFS: ${response.statusText}`,
"DOWNLOAD_FAILED",
"pinata"
);
}
return await response.blob();
} catch (error) {
if (error instanceof StorageError) {
throw error;
}
throw new StorageError(
`Pinata download error: ${error instanceof Error ? error.message : "Unknown error"}`,
"DOWNLOAD_ERROR",
"pinata"
);
}
}
/**
* Lists files uploaded to Pinata with optional filtering
*
* @remarks
* This method retrieves a list of files that have been uploaded to Pinata,
* filtered to only include files uploaded by the Vana SDK. You can further
* filter results by name pattern, limit results, or paginate through them.
*
* @param options - Optional query parameters for filtering and pagination
* @param options.limit - Maximum number of results to return (default: 10)
* @param options.offset - Number of results to skip for pagination
* @param options.namePattern - Filter files by name pattern
* @returns Promise that resolves to an array of PinataFile objects
* @throws {StorageError} When the list operation fails
*
* @example
* ```typescript
* // List all files
* const allFiles = await pinataStorage.list();
*
* // List with pagination and filtering
* const filteredFiles = await pinataStorage.list({
* limit: 20,
* offset: 10,
* namePattern: "document"
* });
*
* filteredFiles.forEach(file => {
* console.log(`${file.name} (${file.size} bytes): ${file.cid}`);
* });
* ```
*/
async list(options) {
try {
const params = new URLSearchParams({
status: "pinned",
pageLimit: (options?.limit ?? 10).toString(),
metadata: JSON.stringify({
keyvalues: {
uploadedBy: "vana-sdk"
}
})
});
if (options?.offset) {
params.set("pageOffset", options.offset.toString());
}
if (options?.namePattern) {
params.set("metadata[name]", options.namePattern);
}
const response = await fetch(`${this.apiUrl}/data/pinList?${params}`, {
headers: {
Authorization: `Bearer ${this.config.jwt}`
}
});
if (!response.ok) {
const errorText = await response.text();
throw new StorageError(
`Failed to list Pinata files: ${errorText}`,
"LIST_FAILED",
"pinata"
);
}
const result = await response.json();
return result.rows.map((pin) => ({
id: pin.id,
name: pin.metadata?.name ?? "Unnamed",
url: `ipfs://${pin.ipfs_pin_hash}`,
size: parseInt(String(pin.size), 10) || 0,
contentType: "application/octet-stream",
// Pinata doesn't store content type
createdAt: new Date(pin.date_pinned),
metadata: pin.metadata?.keyvalues ?? {}
}));
} catch (error) {
if (error instanceof StorageError) {
throw error;
}
throw new StorageError(
`Pinata list error: ${error instanceof Error ? error.message : "Unknown error"}`,
"LIST_ERROR",
"pinata"
);
}
}
/**
* Deletes a file from Pinata by unpinning it from IPFS
*
* @remarks
* This method removes the file from your Pinata account by unpinning it,
* which means it will no longer be guaranteed to be available on the IPFS network.
* Note that if the file is pinned elsewhere or cached by other nodes, it may still
* be accessible for some time.
*
* @param url - The IPFS URL or content identifier of the file to delete
* @returns Promise that resolves when the file is successfully unpinned
* @throws {StorageError} When the deletion fails or CID format is invalid
*
* @example
* ```typescript
* // Delete a file by CID
* await pinataStorage.delete("QmTzQ1JRkWErjk39mryYw2WVrgBMe2B36gRq8GCL8qCACj");
* console.log("File unpinned from Pinata");
*
* // Delete after listing
* const files = await pinataStorage.list();
* for (const file of files) {
* if (file.name.includes("temp")) {
* await pinataStorage.delete(file.cid);
* }
* }
* ```
*/
async delete(url) {
try {
const cid = this.extractCidFromUrl(url);
if (!this.isValidCID(cid)) {
throw new StorageError(
"Invalid IPFS CID format",
"INVALID_CID",
"pinata"
);
}
const response = await fetch(`${this.apiUrl}/pinning/unpin/${cid}`, {
method: "DELETE",
headers: {
Authorization: `Bearer ${this.config.jwt}`
}
});
if (!response.ok) {
const errorText = await response.text();
throw new StorageError(
`Failed to delete from Pinata: ${errorText}`,
"DELETE_FAILED",
"pinata"
);
}
return true;
} catch (error) {
if (error instanceof StorageError) {
throw error;
}
throw new StorageError(
`Pinata delete error: ${error instanceof Error ? error.message : "Unknown error"}`,
"DELETE_ERROR",
"pinata"
);
}
}
getConfig() {
return {
name: "Pinata IPFS",
type: "pinata",
requiresAuth: true,
features: {
upload: true,
download: true,
list: true,
delete: true
}
};
}
/**
* Extract CID from URL or return as-is
*
* @param url - URL or CID string
* @returns CID string
*/
extractCidFromUrl(url) {
if (!url.includes("/")) {
return url;
}
const cidMatch = url.match(/\/ipfs\/([a-zA-Z0-9]+)/);
if (cidMatch) {
return cidMatch[1];
}
return url;
}
/**
* Basic CID validation
*
* @param cid - Content identifier to validate
* @returns True if CID appears valid
*/
isValidCID(cid) {
return /^[a-zA-Z0-9]{10,}$/.test(cid) && (cid.startsWith("Qm") || cid.startsWith("ba") || cid.includes("Test"));
}
}
export {
PinataStorage
};
//# sourceMappingURL=pinata.js.map