AR.IO LogoAR.IO Documentation

Using Turbo SDK with Vanilla HTML

Using Turbo SDK with Vanilla HTML

Firefox Compatibility: Some compatibility issues have been reported with the Turbo SDK in Firefox browsers. At this time the below framework examples may not behave as expected in Firefox.

Overview

This guide demonstrates how to integrate the @ardrive/turbo-sdk directly into vanilla HTML pages using CDN imports. No build tools, bundlers, or polyfills are required - just modern ES modules support in browsers.

Note: Vanilla HTML implementation is the simplest way to get started with the Turbo SDK. It's perfect for prototyping, simple applications, or when you want to avoid build complexity.

Prerequisites

  • Modern browser with ES modules support (Chrome 61+, Firefox 60+, Safari 10.1+, Edge 16+)
  • Basic understanding of HTML, CSS, and JavaScript
  • HTTPS hosting for production (required for browser wallet integrations)

Create a basic HTML file with Turbo SDK integration:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Turbo SDK Example</title>
    <style>
      body {
        font-family: Arial, sans-serif;
        max-width: 800px;
        margin: 0 auto;
        padding: 20px;
      }
      .section {
        margin: 20px 0;
        padding: 20px;
        border: 1px solid #ddd;
        border-radius: 8px;
      }
      .loading {
        color: #666;
        font-style: italic;
      }
      .error {
        color: red;
      }
      .success {
        color: green;
      }
      button {
        background: #007cba;
        color: white;
        border: none;
        padding: 10px 20px;
        border-radius: 4px;
        cursor: pointer;
        margin: 5px;
      }
      button:hover {
        background: #005a87;
      }
      button:disabled {
        background: #ccc;
        cursor: not-allowed;
      }
    </style>
  </head>
  <body>
    <h1>Turbo SDK - Vanilla HTML Demo</h1>

    <div class="section">
      <h2>Current Rates</h2>
      <div id="rates" class="loading">Loading rates...</div>
    </div>

    <div class="section">
      <h2>Upload File</h2>
      <form id="uploadForm">
        <input type="file" id="fileInput" accept="*/*" required />
        <br /><br />
        <button type="submit" id="uploadBtn">Upload File</button>
      </form>
      <div id="uploadStatus"></div>
    </div>

    <script type="module">
      import { TurboFactory } from "https://esm.sh/@ardrive/turbo-sdk";

      // Initialize Turbo client
      const turbo = TurboFactory.unauthenticated();

      // Fetch and display rates
      async function loadRates() {
        try {
          const rates = await turbo.getFiatRates();
          const ratesDiv = document.getElementById("rates");

          const ratesText = Object.entries(rates.fiat)
            .map(
              ([currency, rate]) =>
                `${currency.toUpperCase()}: $${rate} per GiB`
            )
            .join("<br>");

          ratesDiv.innerHTML = ratesText;
        } catch (error) {
          document.getElementById(
            "rates"
          ).innerHTML = `<span class="error">Error loading rates: ${error.message}</span>`;
        }
      }

      // Handle file upload
      document
        .getElementById("uploadForm")
        .addEventListener("submit", async (e) => {
          e.preventDefault();

          const fileInput = document.getElementById("fileInput");
          const uploadBtn = document.getElementById("uploadBtn");
          const statusDiv = document.getElementById("uploadStatus");

          if (!fileInput.files.length) {
            statusDiv.innerHTML =
              '<span class="error">Please select a file</span>';
            return;
          }

          const file = fileInput.files[0];
          uploadBtn.disabled = true;
          statusDiv.innerHTML =
            '<span class="loading">Preparing upload...</span>';

          try {
            // Show upload cost first
            const costs = await turbo.getUploadCosts({ bytes: [file.size] });
            const cost = costs[0];

            statusDiv.innerHTML = `
                    <p>Upload cost: ${cost.winc} winc</p>
                    <p>File size: ${file.size.toLocaleString()} bytes</p>
                    <p class="error">Note: This example cannot complete uploads without wallet authentication.</p>
                    <p>See wallet integration examples below for full upload functionality.</p>
                `;
          } catch (error) {
            statusDiv.innerHTML = `<span class="error">Error: ${error.message}</span>`;
          } finally {
            uploadBtn.disabled = false;
          }
        });

      // Load rates on page load
      loadRates();
    </script>
  </body>
</html>

Select the appropriate CDN import method for your needs:

Use esm.sh for best compatibility: The unpkg.com CDN has known issues with ES module exports for complex packages like Turbo SDK.

Latest Version (Recommended for Development)

import { TurboFactory } from "https://esm.sh/@ardrive/turbo-sdk";

Specific Version (Recommended for Production)

import { TurboFactory } from "https://esm.sh/@ardrive/turbo-sdk@1.20.0";

Alternative CDN Providers

// jsDelivr
import { TurboFactory } from "https://cdn.jsdelivr.net/npm/@ardrive/turbo-sdk@1.20.0/+esm";

// SkyPack
import { TurboFactory } from "https://cdn.skypack.dev/@ardrive/turbo-sdk@1.20.0";

// unpkg.com (not recommended - has ES module issues)
import { TurboFactory } from "https://unpkg.com/@ardrive/turbo-sdk@1.20.0";

Connect your browser wallet to enable file uploads:

Never expose private keys in browser applications! Always use browser wallet integrations.

Uploading with Wander

Deprecation Notice: The signature API used by ArConnect wallets is deprecated and will be removed. Visit Wander wallet documentation for alternatives.

Complete HTML page with Wander wallet integration:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Turbo SDK - Wander Wallet</title>
    <style>
      body {
        font-family: Arial, sans-serif;
        max-width: 600px;
        margin: 0 auto;
        padding: 20px;
        background: #f5f5f5;
      }
      .container {
        background: white;
        padding: 30px;
        border-radius: 10px;
        box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
      }
      .wallet-section {
        border: 2px solid #e0e0e0;
        border-radius: 8px;
        padding: 20px;
        margin: 20px 0;
      }
      .connected {
        border-color: #4caf50;
        background-color: #f9fff9;
      }
      button {
        background: #000;
        color: white;
        border: none;
        padding: 12px 24px;
        border-radius: 6px;
        cursor: pointer;
        font-size: 16px;
        margin: 5px;
      }
      button:hover {
        background: #333;
      }
      button:disabled {
        background: #ccc;
        cursor: not-allowed;
      }
      .status {
        margin: 10px 0;
        padding: 10px;
        border-radius: 4px;
      }
      .success {
        background: #d4edda;
        color: #155724;
        border: 1px solid #c3e6cb;
      }
      .error {
        background: #f8d7da;
        color: #721c24;
        border: 1px solid #f5c6cb;
      }
      .info {
        background: #d1ecf1;
        color: #0c5460;
        border: 1px solid #bee5eb;
      }
    </style>
  </head>
  <body>
    <div class="container">
      <h1>⚡ Turbo SDK + Wander Wallet</h1>

      <div id="walletSection" class="wallet-section">
        <h2>Wander Wallet Connection</h2>
        <p>
          Connect your Wander wallet to upload files to Arweave using your AR
          balance.
        </p>
        <button id="connectBtn" onclick="connectWanderWallet()">
          Connect Wander Wallet
        </button>
        <div id="walletStatus"></div>
      </div>

      <div id="uploadSection" style="display: none; margin-top: 20px;">
        <h3>📁 File Upload</h3>
        <input
          type="file"
          id="fileInput"
          accept="*/*"
          style="margin: 10px 0;"
        />
        <br />
        <button onclick="uploadFile()">Upload to Arweave</button>
        <div id="uploadStatus"></div>
      </div>
    </div>

    <script type="module">
      import {
        TurboFactory,
        ArconnectSigner,
      } from "https://esm.sh/@ardrive/turbo-sdk";

      let connectedAddress = null;
      let turboClient = null;

      // Connect to Wander wallet
      async function connectWanderWallet() {
        const statusDiv = document.getElementById("walletStatus");
        const connectBtn = document.getElementById("connectBtn");

        try {
          if (!window.arweaveWallet) {
            statusDiv.innerHTML = `
                        <div class="error">
                            Wander wallet is not installed!<br>
                            <a href="https://wander.arweave.dev/" target="_blank">Install Wander Wallet</a>
                        </div>
                    `;
            return;
          }

          connectBtn.disabled = true;
          statusDiv.innerHTML =
            '<div class="info">Connecting to Wander wallet...</div>';

          // Required permissions for Turbo SDK
          const permissions = [
            "ACCESS_ADDRESS",
            "ACCESS_PUBLIC_KEY",
            "SIGN_TRANSACTION",
            "SIGNATURE",
          ];

          // Connect to wallet
          await window.arweaveWallet.connect(permissions);

          // Get wallet address
          connectedAddress = await window.arweaveWallet.getActiveAddress();

          // Create authenticated Turbo client
          const signer = new ArconnectSigner(window.arweaveWallet);
          turboClient = TurboFactory.authenticated({ signer });

          // Update UI
          document.getElementById("walletSection").classList.add("connected");
          statusDiv.innerHTML = `
                    <div class="success">
                        ✅ Connected to Wander Wallet<br>
                        <strong>Address:</strong> ${connectedAddress.slice(
                          0,
                          8
                        )}...${connectedAddress.slice(-8)}
                    </div>
                `;
          connectBtn.style.display = "none";
          document.getElementById("uploadSection").style.display = "block";
        } catch (error) {
          console.error("Wander wallet connection failed:", error);
          statusDiv.innerHTML = `<div class="error">Connection failed: ${error.message}</div>`;
        } finally {
          connectBtn.disabled = false;
        }
      }

      // Upload file function
      async function uploadFile() {
        const fileInput = document.getElementById("fileInput");
        const statusDiv = document.getElementById("uploadStatus");

        if (!fileInput.files.length) {
          statusDiv.innerHTML =
            '<div class="error">Please select a file first</div>';
          return;
        }

        if (!turboClient) {
          statusDiv.innerHTML =
            '<div class="error">Please connect Wander wallet first</div>';
          return;
        }

        const file = fileInput.files[0];
        let uploadStartTime = Date.now();

        statusDiv.innerHTML = '<div class="info">Preparing upload...</div>';

        try {
          // Get upload cost first
          const costs = await turboClient.getUploadCosts({
            bytes: [file.size],
          });
          const cost = costs[0];

          statusDiv.innerHTML = `
                    <div class="info">
                        Upload cost: ${cost.winc} winc<br>
                        Starting upload...
                    </div>
                `;

          // Upload with comprehensive progress tracking
          const result = await turboClient.uploadFile({
            fileStreamFactory: () => file.stream(),
            fileSizeFactory: () => file.size,
            dataItemOpts: {
              tags: [
                {
                  name: "Content-Type",
                  value: file.type || "application/octet-stream",
                },
                { name: "App-Name", value: "Turbo-HTML-Wander-Demo" },
                { name: "File-Name", value: file.name },
                { name: "Upload-Timestamp", value: new Date().toISOString() },
              ],
            },
            events: {
              onProgress: ({ totalBytes, processedBytes, step }) => {
                const percent = Math.round((processedBytes / totalBytes) * 100);
                const elapsed = Math.round(
                  (Date.now() - uploadStartTime) / 1000
                );
                statusDiv.innerHTML = `
                                <div class="info">
                                    <strong>${step}:</strong> ${percent}%<br>
                                    Progress: ${processedBytes.toLocaleString()} / ${totalBytes.toLocaleString()} bytes<br>
                                    Elapsed: ${elapsed}s
                                </div>
                            `;
              },
              onError: ({ error, step }) => {
                console.error(`Error during ${step}:`, error);
                statusDiv.innerHTML = `<div class="error">Error during ${step}: ${error.message}</div>`;
              },
            },
          });

          const totalTime = Math.round((Date.now() - uploadStartTime) / 1000);
          // Use original file size for display (result object doesn't contain size info)
          const displayBytes = file.size;
          statusDiv.innerHTML = `
                    <div class="success">
                        <strong>🎉 Upload Successful!</strong><br>
                        <strong>Transaction ID:</strong> <code>${
                          result.id
                        }</code><br>
                        <strong>File Size:</strong> ${displayBytes.toLocaleString()} bytes<br>
                        <strong>Upload Time:</strong> ${totalTime}s<br>
                        <strong>Timestamp:</strong> ${new Date(
                          result.timestamp
                        ).toLocaleString()}<br>
                        <strong>View File:</strong> <a href="https://arweave.net/${
                          result.id
                        }" target="_blank">arweave.net/${result.id}</a><br>
                        <strong>Explorer:</strong> <a href="https://viewblock.io/arweave/tx/${
                          result.id
                        }" target="_blank">ViewBlock</a>
                    </div>
                `;
        } catch (error) {
          console.error("Upload failed:", error);
          statusDiv.innerHTML = `<div class="error">Upload failed: ${error.message}</div>`;
        }
      }

      // Make functions available globally for onclick handlers
      window.connectWanderWallet = connectWanderWallet;
      window.uploadFile = uploadFile;
    </script>
  </body>
</html>

Browser wallet integrations require HTTPS in production:

<!-- Production deployment checklist -->
<!-- ✅ HTTPS enabled (required for wallet connections) -->
<!-- ✅ CSP headers configured for external CDN imports -->
<!-- ✅ Proper error handling for network failures -->

Configure CSP headers to allow CDN imports:

<meta
  http-equiv="Content-Security-Policy"
  content="
    default-src 'self';
    script-src 'self' 'unsafe-inline' https://esm.sh https://unpkg.com https://cdn.jsdelivr.net;
    connect-src 'self' https://upload.ardrive.io https://payment.ardrive.io https://arweave.net;
    img-src 'self' data:;
"
/>

Implement comprehensive error handling:

// Network error handling
async function robustApiCall(apiFunction, retries = 3) {
  for (let i = 0; i < retries; i++) {
    try {
      return await apiFunction();
    } catch (error) {
      console.error(`Attempt ${i + 1} failed:`, error);
      if (i === retries - 1) throw error;
      await new Promise((resolve) =>
        setTimeout(resolve, 1000 * Math.pow(2, i))
      );
    }
  }
}

// Usage example
const rates = await robustApiCall(() => turbo.getFiatRates());

Optimize for production environments:

<!-- Preload CDN resources -->
<link rel="modulepreload" href="https://esm.sh/@ardrive/turbo-sdk@1.20.0" />

<!-- Use specific versions in production -->
<script type="module">
  import { TurboFactory } from "https://esm.sh/@ardrive/turbo-sdk@1.20.0";
  // Production code here
</script>

Best Practices

1. User Experience

  • Loading States: Always show loading indicators during API calls
  • Error Recovery: Provide clear error messages with recovery options
  • Progress Tracking: Show upload progress for large files
  • Wallet Detection: Guide users to install wallets if missing

2. Security

  • Never expose private keys in browser applications
  • Validate user inputs before API calls
  • Use HTTPS for all production deployments
  • Implement CSP headers to prevent XSS attacks

3. Performance

  • Cache API responses where appropriate (rates, costs)
  • Use specific CDN versions in production
  • Implement retry logic for network failures
  • Optimize file handling for large uploads

4. Development

  • Use development endpoints during testing
  • Test wallet integrations across different browsers
  • Validate upload functionality with small files first
  • Monitor API rate limits and implement backoff

Troubleshooting Common Issues

CDN Import Errors

If you encounter errors like:

  • The requested module does not provide an export named 'TurboFactory'
  • Module resolution failures

Solution: Use esm.sh instead of unpkg.com:

// ❌ Problematic
import { TurboFactory } from "https://unpkg.com/@ardrive/turbo-sdk";

// ✅ Working
import { TurboFactory } from "https://esm.sh/@ardrive/turbo-sdk";

Function Scope Issues

If onclick handlers throw ReferenceError: function is not defined:

Solution: Use explicit global assignment:

// ❌ Problematic
window.myFunction = async function() { ... }

// ✅ Working
async function myFunction() { ... }
window.myFunction = myFunction;

Upload Result Properties

If upload results have undefined properties:

Solution: Use original file size for display:

// ❌ Problematic - these properties don't exist in result object
const totalBytes = result.totalBytes || result.dataSizeBytes;

// ✅ Correct - use original file size
const displayBytes = originalFile.size;

// Available result properties: id, timestamp, winc, version,
// deadlineHeight, dataCaches, fastFinalityIndexes, public, signature, owner

Upload Cost Properties

If cost calculations fail:

Solution: Use correct cost object structure:

// ❌ Problematic - adjustedBytes doesn't exist in cost objects
const cost = costs[0];
console.log(`Adjusted: ${cost.adjustedBytes.toLocaleString()}`);

// ✅ Correct - use available properties
const cost = costs[0];
console.log(`Cost: ${cost.winc} winc`);
console.log(`File size: ${originalFile.size.toLocaleString()} bytes`);
// Available cost properties: winc (string), adjustments (array)

Testing Your Implementation

1. Basic Functionality Test

// Test CDN import
console.log("Testing Turbo SDK import...");
import { TurboFactory } from "https://esm.sh/@ardrive/turbo-sdk";
const turbo = TurboFactory.unauthenticated();
console.log("✅ SDK imported successfully");

// Test rate fetching
const rates = await turbo.getFiatRates();
console.log("✅ Rates fetched:", rates);

2. Wallet Integration Test

  • Connect to MetaMask/Wander wallet
  • Verify address display
  • Test small file upload (< 1MB)
  • Check transaction ID and Arweave confirmation

3. Production Readiness

  • Test with HTTPS hosting
  • Verify CSP headers don't block resources
  • Test error scenarios (network offline, wallet disconnected)
  • Validate cross-browser compatibility

API Object Structures

Understanding the actual structure of API responses helps prevent common errors:

Upload Cost Object

const costs = await turbo.getUploadCosts({ bytes: [file.size] });
const cost = costs[0];

// Actual structure:
{
  winc: "15763844",        // string - cost in Winston credits
  adjustments: []          // array - currently empty
}
// Note: adjustedBytes property does NOT exist

Upload Result Object

const result = await turboClient.uploadFile({...});

// Actual structure:
{
  id: "K2wbyN85FlZMBYyfn4fnk5A-fp-pmnrQWpgfuKI4UCc",
  timestamp: 1752094894060,
  winc: "0",
  version: "0.2.0",
  deadlineHeight: 1708455,
  dataCaches: ["arweave.net"],
  fastFinalityIndexes: ["arweave.net"],
  public: "wZgP9Rpfh0nXb5EdzBUg7y2LFMkBp2ADX3gdZhLXHYbz...",
  signature: "k31SCZwxerhuEojUZ7rnLMr0T6CwgVp1_dKDZc7UdvV...",
  owner: "cF0H0SKdnaDTqWKY9iJKBktTpdEWgb3GnlndE7ABv0Q"
}
// Note: totalBytes and dataSizeBytes properties do NOT exist

Fiat Rates Object

const rates = await turbo.getFiatRates();

// Actual structure:
{
  winc: "2257047178957",              // string - current winc rate
  fiat: {                            // object - fiat currency rates
    aud: 25.13395879439,             // number - AUD per GiB
    brl: 91.578363344626,            // number - BRL per GiB
    cad: 22.482075685955,            // number - CAD per GiB
    eur: 13.996049738963,            // number - EUR per GiB
    gbp: 12.080800827315,            // number - GBP per GiB
    hkd: 129.06624536489,            // number - HKD per GiB
    inr: 1409.435250458,             // number - INR per GiB
    jpy: 2406.336355749,             // number - JPY per GiB
    sgd: 21.058978043112,            // number - SGD per GiB
    usd: 16.428221816529             // number - USD per GiB
  },
  adjustments: []                    // array - currently empty
}

Progress Event Object

events: {
  onProgress: (progressEvent) => {
    // Actual structure:
    {
      processedBytes: 4326,          // number - bytes processed so far
      totalBytes: 8652,              // number - total bytes to process
      step: "signing"                // string - current step: "signing" or "upload"
    }
  }
}

Additional Resources


For more advanced implementations, see the Next.js and Vite framework guides, or explore the Turbo SDK examples repository.

How is this guide?