Wallet-as-a-ServiceAdvancedNon-Custodial Provider

Web3NonCustodialProvider

The Web3NonCustodialProvider is a low-level class for building custom authentication and wallet management flows. It provides direct access to OAuth sign-in, email OTP, wallet creation with Shamir’s Secret Sharing, and wallet recovery.

⚠️

This is an advanced API. For most use cases, use Web3Wallet.enable() instead, which handles authentication and wallet management automatically.

When to Use This

Use Web3NonCustodialProvider when you need:

  • Custom OAuth redirect handling
  • Custom UI for authentication flows
  • Direct control over wallet creation and recovery
  • Chrome extension development (with chrome.storage support)

Installation

npm install @utxos/sdk

Initialization

import { Web3NonCustodialProvider } from "@utxos/sdk";
 
const provider = new Web3NonCustodialProvider({
  projectId: process.env.NEXT_PUBLIC_UTXOS_PROJECT_ID,
  googleOauth2ClientId: "your-google-client-id",
  twitterOauth2ClientId: "your-twitter-client-id",
  discordOauth2ClientId: "your-discord-client-id",
  appleOauth2ClientId: "your-apple-client-id",
  storageLocation: "local_storage", // "local_storage" | "chrome_local" | "chrome_sync"
  appOrigin: "https://utxos.dev", // optional, defaults to utxos.dev
});

Configuration Options

OptionTypeRequiredDescription
projectIdstringYesYour UTXOS project ID
googleOauth2ClientIdstringYesGoogle OAuth 2.0 client ID
twitterOauth2ClientIdstringYesTwitter/X OAuth 2.0 client ID
discordOauth2ClientIdstringYesDiscord OAuth 2.0 client ID
appleOauth2ClientIdstringYesApple OAuth client ID
storageLocationStorageLocationNoWhere to store credentials (default: "local_storage")
appOriginstringNoAPI origin (default: "https://utxos.dev")

Authentication

OAuth Sign-In

Initiate OAuth sign-in with a social provider:

provider.signInWithProvider(
  "google", // "google" | "twitter" | "discord" | "apple"
  "https://yourapp.com/callback", // redirect URL after auth
  (authorizationUrl) => {
    // Redirect user to the authorization URL
    window.location.href = authorizationUrl;
  }
);

Handle OAuth Callback

On your callback page, handle the authentication response:

// Call this on your /auth/mesh callback page
const result = await provider.handleAuthenticationRoute();
 
if (result?.error) {
  console.error("Authentication failed:", result.error);
}
// On success, user is redirected to your specified redirect URL

Email OTP Authentication

Send and verify email OTP codes:

// Step 1: Send OTP to email
const { error: sendError } = await provider.sendEmailOtp("user@example.com");
 
if (sendError) {
  console.error("Failed to send OTP:", sendError);
  return;
}
 
// Step 2: Verify OTP code
const { data: user, error: verifyError } = await provider.verifyEmailOtp(
  "user@example.com",
  "123456" // 6-digit OTP code
);
 
if (verifyError) {
  console.error("Invalid OTP:", verifyError);
  return;
}
 
console.log("Authenticated user:", user);

Get Current User

Check if user is authenticated and get their info:

const { data: user, error } = await provider.getUser();
 
if (error) {
  if (error instanceof NotAuthenticatedError) {
    console.log("User not logged in");
  } else if (error instanceof SessionExpiredError) {
    console.log("Session expired, please log in again");
  }
  return;
}
 
console.log("User ID:", user.id);
console.log("Provider:", user.provider); // "google", "twitter", etc.
console.log("Email:", user.email);
console.log("Username:", user.username);
console.log("Avatar:", user.avatarUrl);

Wallet Management

Create Wallet

Create a new wallet with Shamir’s Secret Sharing:

// Generate encryption keys for device and recovery shards
const deviceKey = await crypto.subtle.generateKey(
  { name: "AES-GCM", length: 256 },
  true,
  ["encrypt", "decrypt"]
);
 
const recoveryKey = await crypto.subtle.generateKey(
  { name: "AES-GCM", length: 256 },
  true,
  ["encrypt", "decrypt"]
);
 
const { data, error } = await provider.createWallet(
  deviceKey,
  recoveryKey,
  "What is your pet's name?", // optional recovery question
  undefined // optional WebAuthn credential ID for passkey auth
);
 
if (error) {
  console.error("Wallet creation failed:", error);
  return;
}
 
console.log("Wallet ID:", data.walletId);
console.log("Device ID:", data.deviceId);

The wallet uses 2-of-3 Shamir’s Secret Sharing. The three shards are:

  • Device shard: Encrypted and stored locally on this device
  • Auth shard: Stored on UTXOS servers, requires authentication to access
  • Recovery shard: Encrypted and stored on UTXOS servers for wallet recovery

Get Wallets

Retrieve all wallets for the authenticated user on this device:

const { data: wallets, error } = await provider.getWallets();
 
if (error) {
  console.error("Failed to get wallets:", error);
  return;
}
 
wallets.forEach((wallet) => {
  console.log("Wallet ID:", wallet.walletId);
  console.log("Device ID:", wallet.deviceId);
  console.log("Auth Shard:", wallet.authShard);
  console.log("Local Shard:", wallet.localShard);
});

Check Server Wallets

Check all wallets registered on the server (without local shard):

const { data: serverWallets, error } = await provider.checkNonCustodialWalletsOnServer();
 
if (error) {
  console.error("Failed to check server wallets:", error);
  return;
}
 
console.log("Wallets on server:", serverWallets);

Recover Wallet

Recover access to a wallet on a new device:

const { data, error } = await provider.performRecovery(
  "wallet-id-to-recover",
  recoveryShardEncryptionKey, // key to decrypt recovery shard
  newDeviceShardEncryptionKey // key for new device shard
);
 
if (error) {
  console.error("Recovery failed:", error);
  return;
}
 
console.log("Wallet recovered, full key available");

Error Types

The provider exports specific error classes for handling different failure scenarios:

import {
  NotAuthenticatedError,
  SessionExpiredError,
  WalletServerRetrievalError,
  WalletServerCreationError,
  AuthRouteError,
  StorageRetrievalError,
  StorageInsertError,
} from "@utxos/sdk";
 
// Example error handling
const { data, error } = await provider.getUser();
 
if (error instanceof NotAuthenticatedError) {
  // Redirect to login
} else if (error instanceof SessionExpiredError) {
  // Refresh authentication
} else if (error instanceof WalletServerRetrievalError) {
  // Handle server communication failure
}
Error ClassWhen Thrown
NotAuthenticatedErrorUser is not logged in
SessionExpiredErrorJWT has expired
WalletServerRetrievalErrorFailed to fetch wallet data from server
WalletServerCreationErrorFailed to create wallet on server
AuthRouteErrorOAuth callback handling failed
StorageRetrievalErrorFailed to read from local storage
StorageInsertErrorFailed to write to local storage

TypeScript Types

import type {
  Web3NonCustodialProviderParams,
  Web3NonCustodialProviderUser,
  Web3NonCustodialWallet,
  StorageLocation,
  CreateWalletBody,
  GetWalletBody,
  WalletDevice,
} from "@utxos/sdk";

Key Types

type StorageLocation = "local_storage" | "chrome_local" | "chrome_sync";
 
type Web3NonCustodialProviderUser = {
  id: string;
  scopes: string[];
  provider: string;
  providerId: string;
  avatarUrl: string | null;
  email: string | null;
  username: string | null;
  token: string;
};
 
type Web3NonCustodialWallet = {
  deviceId: string;
  walletId: string;
  authShard: string;
  localShard: string;
  userAgent: string | null;
  webauthnCredentialId: string | null;
};

Complete Example

Here’s a complete example of a custom authentication flow:

import { Web3NonCustodialProvider, NotAuthenticatedError } from "@utxos/sdk";
 
const provider = new Web3NonCustodialProvider({
  projectId: process.env.NEXT_PUBLIC_UTXOS_PROJECT_ID,
  googleOauth2ClientId: process.env.GOOGLE_CLIENT_ID,
  twitterOauth2ClientId: process.env.TWITTER_CLIENT_ID,
  discordOauth2ClientId: process.env.DISCORD_CLIENT_ID,
  appleOauth2ClientId: process.env.APPLE_CLIENT_ID,
});
 
async function checkAuthStatus() {
  const { data: user, error } = await provider.getUser();
 
  if (error instanceof NotAuthenticatedError) {
    return { authenticated: false, user: null };
  }
 
  return { authenticated: true, user };
}
 
function loginWithGoogle() {
  provider.signInWithProvider(
    "google",
    window.location.origin + "/dashboard",
    (url) => { window.location.href = url; }
  );
}
 
async function loginWithEmail(email: string, otp: string) {
  // First call sendEmailOtp, then verify
  const { data: user, error } = await provider.verifyEmailOtp(email, otp);
 
  if (error) {
    throw error;
  }
 
  return user;
}
 
async function createNewWallet() {
  const deviceKey = await crypto.subtle.generateKey(
    { name: "AES-GCM", length: 256 },
    true,
    ["encrypt", "decrypt"]
  );
 
  const recoveryKey = await crypto.subtle.generateKey(
    { name: "AES-GCM", length: 256 },
    true,
    ["encrypt", "decrypt"]
  );
 
  const { data, error } = await provider.createWallet(
    deviceKey,
    recoveryKey,
    "Security question here"
  );
 
  if (error) {
    throw error;
  }
 
  // Store recoveryKey securely - user needs this for recovery!
  return data;
}

Chrome Extension Support

For Chrome extensions, use chrome_local or chrome_sync storage:

const provider = new Web3NonCustodialProvider({
  projectId: "your-project-id",
  storageLocation: "chrome_local", // Uses chrome.storage.local
  // ... oauth client IDs
});

This stores credentials in Chrome’s extension storage instead of localStorage, allowing the wallet to persist across browser sessions and sync across devices (with chrome_sync).