Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ import { LinkPreviewModalProvider } from '@/components/post/link-preview'

const basePath = process.env.BASE_PATH || ''

// CDN URL for the evo-sdk - loaded dynamically for better performance
const EVO_SDK_CDN_URL = 'https://cdn.jsdelivr.net/npm/@dashevo/evo-sdk@3.0.0/dist/evo-sdk.module.js'

export const metadata: Metadata = {
title: 'Yappr - Share Your Voice',
description: 'A modern social platform for sharing thoughts and connecting with others',
Expand All @@ -26,6 +29,14 @@ export default function RootLayout({
}) {
return (
<html lang="en" className="h-full" suppressHydrationWarning>
<head>
{/* Preload the evo-sdk from CDN for faster initial load */}
<link
rel="modulepreload"
href={EVO_SDK_CDN_URL}
crossOrigin="anonymous"
/>
Comment on lines +32 to +38

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Suggestion: Avoid preloading the SDK on every page

Placing this modulepreload in the root layout makes every initial route fetch and prepare the SDK, whether or not the user invokes a Platform-dependent workflow. The referenced module is 8,030,052 bytes, and the current static export contains this preload in all 36 generated HTML files. This negates the refactor's on-demand transfer benefit; remove the global hint or initiate preloading only when the user approaches a workflow that requires the SDK.

source: ['codex']

</head>
<body className="font-sans h-full bg-white dark:bg-neutral-900">
<ErrorBoundary level="app">
<Providers>
Expand Down
204 changes: 204 additions & 0 deletions lib/services/cdn-loader.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,204 @@
/**
* CDN Loader for @dashevo/evo-sdk
*
* This module loads the evo-sdk from a CDN instead of bundling it with the application.
* This significantly reduces the initial bundle size and improves load times by leveraging
* CDN caching.
*
* The SDK is loaded once and cached for subsequent use.
*/

// CDN URLs for the SDK
const EVO_SDK_VERSION = '3.0.0';
const CDN_BASE_URL = 'https://cdn.jsdelivr.net/npm';
const EVO_SDK_URL = `${CDN_BASE_URL}/@dashevo/evo-sdk@${EVO_SDK_VERSION}/dist/evo-sdk.module.js`;

// Type definitions for the SDK exports we use
// These match the actual exports from @dashevo/evo-sdk
export interface EvoSdkModule {
EvoSDK: EvoSDKClass;
IdentitySigner: IdentitySignerClass;
PrivateKey: PrivateKeyClass;
IdentityPublicKey: IdentityPublicKeyClass;
Document: DocumentClass;
wallet: WalletModule;
}

// EvoSDK class type
export interface EvoSDKClass {
testnetTrusted(config?: { settings?: { timeoutMs?: number } }): EvoSDKInstance;
mainnetTrusted(config?: { settings?: { timeoutMs?: number } }): EvoSDKInstance;
}

export interface EvoSDKInstance {
connect(): Promise<void>;
documents: {
query(query: unknown): Promise<Map<unknown, unknown>>;
get(contractId: string, docType: string, docId: string): Promise<unknown>;
create(params: { document: unknown; identityKey: unknown; signer: unknown }): Promise<unknown>;
replace(params: { document: unknown; identityKey: unknown; signer: unknown }): Promise<unknown>;
delete(params: { document: unknown; identityKey: unknown; signer: unknown }): Promise<unknown>;
};
identities: {
fetch(identityId: string): Promise<Identity | null>;
balance(identityId: string): Promise<unknown>;
update(params: unknown): Promise<unknown>;
creditTransfer(params: unknown): Promise<unknown>;
};
contracts: {
fetch(contractId: string): Promise<unknown>;
};
dpns: {
usernames(params: { identityId: string; limit?: number }): Promise<string[]>;
resolve(name: string): Promise<unknown>;
resolveName(name: string): Promise<string | null>;
register(params: unknown): Promise<unknown>;
registerName(params: unknown): Promise<unknown>;
isContestedUsername(label: string): Promise<boolean>;
isNameAvailable(name: string): Promise<boolean>;
isValidUsername(label: string): Promise<boolean>;
convertToHomographSafe(input: string): Promise<string>;
};
wasm: {
waitForStateTransitionResult(hash: string): Promise<unknown>;
};
}

// Identity type returned by identities.fetch
export interface Identity {
toJSON(): Record<string, unknown>;
getPublicKeys(): WasmPublicKey[];
}

// WASM public key type
export interface WasmPublicKey {
keyId: number;
keyTypeNumber: number;
purposeNumber: number;
securityLevelNumber: number;
securityLevel: number;
purpose: string;
keyType: string;
data: string;
disabledAt?: number;
}

// IdentitySigner class type
export interface IdentitySignerClass {
new (): IdentitySignerInstance;
}

export interface IdentitySignerInstance {
addKeyFromWif(wif: string): void;
addKey(key: unknown): void;
}

// PrivateKey class type
export interface PrivateKeyClass {
fromHex(hex: string, network: 'testnet' | 'mainnet'): unknown;
}

// IdentityPublicKey class type
export interface IdentityPublicKeyClass {
fromJSON(data: unknown): unknown;
}

// Document class type
export interface DocumentClass {
new (
data: Record<string, unknown>,
documentTypeName: string,
revision: bigint,
contractId: string,
ownerId: string,
documentId?: string
): DocumentInstance;
}

export interface DocumentInstance {
id: unknown;
toJSON(): Record<string, unknown>;
}

// Wallet module type
export interface WalletModule {
keyPairFromWif(wif: string): Promise<{ publicKey: string; privateKey: string } | null>;
}

// Cached SDK module
let cachedModule: EvoSdkModule | null = null;
let loadPromise: Promise<EvoSdkModule> | null = null;

/**
* Load the evo-sdk from CDN
*
* This function dynamically imports the SDK from jsdelivr CDN.
* The module is cached after the first load.
*
* @returns Promise resolving to the SDK module exports
*/
export async function loadEvoSdk(): Promise<EvoSdkModule> {
// Return cached module if available
if (cachedModule) {
return cachedModule;
}

// Return existing load promise if one is in progress
if (loadPromise) {
return loadPromise;
}

// Start loading
loadPromise = (async () => {
try {
console.log('CDN Loader: Loading evo-sdk from CDN...');
const startTime = performance.now();

// Dynamic import from CDN
// Note: This works because the SDK is published as an ES module
const sdkModule = await import(/* webpackIgnore: true */ EVO_SDK_URL);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Blocking: Do not execute CDN code in private-key signing flows

This dynamic import executes the response served by jsDelivr without verifying it against the dependency lockfile or another cryptographic integrity value. The imported IdentitySigner is passed users' WIF private keys by signer-service.ts, and the same remote module participates in identity updates and credit transfers. A compromised or incorrectly served response could therefore read and exfiltrate private keys or alter signed transitions. Pinning the package version prevents ordinary version drift but does not remove the new runtime trust in the CDN; bundle the lockfile-pinned dependency or serve an integrity-controlled first-party artifact instead.

Suggested change
const sdkModule = await import(/* webpackIgnore: true */ EVO_SDK_URL);
const sdkModule = await import('@dashevo/evo-sdk');

source: ['codex']


const loadTime = Math.round(performance.now() - startTime);
console.log(`CDN Loader: evo-sdk loaded from CDN in ${loadTime}ms`);

// Cache the module
cachedModule = sdkModule as EvoSdkModule;
return cachedModule;
} catch (error) {
console.error('CDN Loader: Failed to load evo-sdk from CDN:', error);
loadPromise = null; // Reset so we can retry
throw error;
}
})();

return loadPromise;
}

/**
* Check if the SDK is already loaded
*/
export function isEvoSdkLoaded(): boolean {
return cachedModule !== null;
}

/**
* Get the cached SDK module (returns null if not loaded)
*/
export function getCachedEvoSdk(): EvoSdkModule | null {
return cachedModule;
}

/**
* Preload the SDK without waiting for it
* Useful for starting the load early in the application lifecycle
*/
export function preloadEvoSdk(): void {
if (!cachedModule && !loadPromise) {
loadEvoSdk().catch(error => {
console.error('CDN Loader: Preload failed:', error);
});
}
}

// Re-export the CDN URL for use in preload hints
export const EVO_SDK_CDN_URL = EVO_SDK_URL;
25 changes: 15 additions & 10 deletions lib/services/document-builder-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,11 @@
*
* The new API requires Document WASM objects instead of plain data objects.
*
* IMPORTANT: We import the Document class from @dashevo/evo-sdk which re-exports
* from the shared @dashevo/wasm-sdk module. By calling getEvoSdk() first, we ensure
* IMPORTANT: The SDK is loaded from CDN. By calling loadEvoSdk() first, we ensure
* the WASM module is initialized before creating any Document objects.
*/
import { getEvoSdk } from './evo-sdk-service';
import { Document } from '@dashevo/evo-sdk';
import { loadEvoSdk, type DocumentInstance } from './cdn-loader';

/**
* Ensure WASM module is initialized by connecting SDK
Expand Down Expand Up @@ -39,10 +38,13 @@ class DocumentBuilderService {
documentTypeName: string,
ownerId: string,
data: Record<string, unknown>
): Promise<InstanceType<typeof Document>> {
): Promise<DocumentInstance> {
// Ensure WASM is initialized before creating objects
await ensureWasmReady();

// Load SDK from CDN and get Document class
const { Document } = await loadEvoSdk();

// Create document with revision 1 for new documents
// Document ID is undefined to let the SDK generate it based on entropy
// Note: TypeScript types are stricter than the actual WASM API - undefined is valid
Expand Down Expand Up @@ -80,10 +82,13 @@ class DocumentBuilderService {
ownerId: string,
data: Record<string, unknown>,
newRevision: number
): Promise<InstanceType<typeof Document>> {
): Promise<DocumentInstance> {
// Ensure WASM is initialized before creating objects
await ensureWasmReady();

// Load SDK from CDN and get Document class
const { Document } = await loadEvoSdk();

// Create document with the incremented revision
const document = new Document(
data, // Updated document data fields
Expand Down Expand Up @@ -137,10 +142,10 @@ class DocumentBuilderService {
* @param document - A WASM Document or document-like object
* @returns Normalized document data with $ prefixed fields
*/
normalizeDocumentResponse(document: Document | Record<string, unknown>): Record<string, unknown> {
normalizeDocumentResponse(document: DocumentInstance | Record<string, unknown>): Record<string, unknown> {
// Check if it's a WASM Document with toJSON method
if (document && typeof (document as Document).toJSON === 'function') {
return (document as Document).toJSON();
if (document && typeof (document as DocumentInstance).toJSON === 'function') {
return (document as DocumentInstance).toJSON();
}

// Handle raw objects - normalize field names
Expand Down Expand Up @@ -172,7 +177,7 @@ class DocumentBuilderService {
* @param document - The WASM Document after creation
* @returns The document ID as a string
*/
getDocumentId(document: Document): string {
getDocumentId(document: DocumentInstance): string {
// The document.id property returns an Identifier which can be converted to string
const id = document.id;
if (typeof id === 'string') {
Expand All @@ -183,7 +188,7 @@ class DocumentBuilderService {
}
// Fallback: try to get from JSON
const json = document.toJSON();
return json.$id || json.id || '';
return (json.$id as string) || (json.id as string) || '';
}
}

Expand Down
3 changes: 2 additions & 1 deletion lib/services/document-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,8 @@ export abstract class BaseDocumentService<T> {
}

// Document has toJSON method
const docData = typeof response.toJSON === 'function' ? response.toJSON() : response;
const doc = response as { toJSON?: () => Record<string, unknown> };
const docData = typeof doc.toJSON === 'function' ? doc.toJSON() : response as Record<string, unknown>;
const transformed = this.transformDocument(docData);

// Cache the result
Expand Down
3 changes: 1 addition & 2 deletions lib/services/dpns-service.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
import { getEvoSdk } from './evo-sdk-service';
import { SecurityLevel, KeyPurpose, signerService } from './signer-service';
import { SecurityLevel, KeyPurpose, signerService, type WasmIdentityPublicKey } from './signer-service';
import { DPNS_CONTRACT_ID, DPNS_DOCUMENT_TYPE } from '../constants';
import { identifierToBase58 } from './sdk-helpers';
import { findMatchingKeyIndex, getSecurityLevelName, type IdentityPublicKeyInfo } from '@/lib/crypto/keys';
import type { UsernameCheckResult, UsernameRegistrationResult } from '../types';
import type { IdentityPublicKey as WasmIdentityPublicKey } from '@dashevo/wasm-sdk/compressed';

/**
* Extract documents array from SDK response (handles Map, Array, and object formats)
Expand Down
19 changes: 11 additions & 8 deletions lib/services/evo-sdk-service.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
import { EvoSDK } from '@dashevo/evo-sdk';
import { loadEvoSdk, type EvoSDKInstance } from './cdn-loader';
import { DPNS_CONTRACT_ID, YAPPR_DM_CONTRACT_ID, YAPPR_PROFILE_CONTRACT_ID } from '../constants';

export interface EvoSdkConfig {
network: 'testnet' | 'mainnet';
contractId: string;
}

// Re-export the EvoSDK instance type for convenience
export type EvoSDK = EvoSDKInstance;

class EvoSdkService {
private sdk: EvoSDK | null = null;
private sdk: EvoSDKInstance | null = null;
private initPromise: Promise<void> | null = null;
private config: EvoSdkConfig | null = null;
private _isInitialized = false;
Expand Down Expand Up @@ -54,7 +57,10 @@ class EvoSdkService {
}

try {
console.log('EvoSdkService: Creating EvoSDK instance...');
console.log('EvoSdkService: Loading EvoSDK from CDN...');

// Load SDK from CDN
const { EvoSDK } = await loadEvoSdk();

// Create SDK with trusted mode based on network
if (this.config.network === 'testnet') {
Expand Down Expand Up @@ -140,7 +146,7 @@ class EvoSdkService {
/**
* Get the SDK instance, initializing if necessary
*/
async getSdk(): Promise<EvoSDK> {
async getSdk(): Promise<EvoSDKInstance> {
if (!this._isInitialized || !this.sdk) {
if (!this.config) {
throw new Error('SDK not configured. Call initialize() first.');
Expand Down Expand Up @@ -226,9 +232,6 @@ class EvoSdkService {
export const evoSdkService = new EvoSdkService();

// Export helper to ensure SDK is initialized
export async function getEvoSdk(): Promise<EvoSDK> {
export async function getEvoSdk(): Promise<EvoSDKInstance> {
return evoSdkService.getSdk();
}

// Re-export EvoSDK type for convenience
export type { EvoSDK };
Loading