Web3 Integration Guide
This guide covers how to integrate the Arch Network TypeScript SDK (by Saturn) with Web3 applications, wallets, and dApps.
Important: The Arch TypeScript SDK is a low-level RPC client. It does not include wallet adapters, transaction builders, or other high-level abstractions. This guide shows how you could build these features on top of the SDK.
Understanding the Limitations
The current TypeScript SDK provides:
- Low-level RPC connection (
RpcConnection
) - Basic account creation with secp256k1 (
ArchConnection
) - Message/transaction serialization utilities
- Type definitions for Arch data structures
It does NOT provide:
- Wallet adapters or browser wallet integration
- High-level transaction builders
- React/Vue components or hooks
- State management solutions
Building Wallet Integration
Since the SDK doesn’t include wallet adapters, you’ll need to build your own. Here’s a conceptual approach:
Defining a Wallet Interface
import { RpcConnection } from '@saturnbtcio/arch-sdk';
import type { RuntimeTransaction, SanitizedMessage } from '@saturnbtcio/arch-sdk';
// Define what a wallet adapter might look like
interface ArchWallet {
publicKey: Uint8Array | null;
connected: boolean;
connect(): Promise<{ publicKey: string }>;
disconnect(): Promise<void>;
signMessage(message: Uint8Array): Promise<Uint8Array>;
signTransaction(tx: SanitizedMessage): Promise<Uint8Array>;
}
// Example implementation skeleton
class BrowserWalletAdapter implements ArchWallet {
publicKey: Uint8Array | null = null;
connected: boolean = false;
async connect(): Promise<{ publicKey: string }> {
// This would interface with actual browser wallet
// For now, this is just a placeholder
throw new Error('Wallet integration not implemented');
}
async disconnect(): Promise<void> {
this.publicKey = null;
this.connected = false;
}
async signMessage(message: Uint8Array): Promise<Uint8Array> {
// Would call wallet's signing method
throw new Error('Message signing not implemented');
}
async signTransaction(tx: SanitizedMessage): Promise<Uint8Array> {
// Would call wallet's transaction signing
throw new Error('Transaction signing not implemented');
}
}
Creating Transactions with External Signing
import { RpcConnection, SanitizedMessageUtil, PubkeyUtil } from '@saturnbtcio/arch-sdk';
import type { RuntimeTransaction, SanitizedMessage, SanitizedInstruction } from '@saturnbtcio/arch-sdk';
async function createAndSignTransaction(
connection: RpcConnection,
signer: Uint8Array,
signFunction: (message: Uint8Array) => Promise<Uint8Array>
) {
// Build a sanitized message
const message: SanitizedMessage = {
header: {
num_required_signatures: 1,
num_readonly_signed_accounts: 0,
num_readonly_unsigned_accounts: 1,
},
account_keys: [
signer, // Signer pubkey
PubkeyUtil.systemProgram(), // System program
],
recent_blockhash: new Uint8Array(32), // Need actual blockhash
instructions: [
{
program_id_index: 1,
accounts: [0],
data: new Uint8Array([0, 0, 0, 0]),
},
],
};
// Serialize message for signing
const serializedMessage = SanitizedMessageUtil.serialize(message);
// Sign with external wallet
const signature = await signFunction(serializedMessage);
// Create runtime transaction
const transaction: RuntimeTransaction = {
version: 0,
signatures: [signature],
message: message,
};
// Send transaction
const txId = await connection.sendTransaction(transaction);
return txId;
}
React Integration Pattern
Here’s how you might structure a React integration:
Basic Context Provider
import React, { createContext, useContext, useState } from 'react';
import { RpcConnection, ArchConnection } from '@saturnbtcio/arch-sdk';
interface ArchContextState {
connection: RpcConnection;
arch: ReturnType<typeof ArchConnection>;
// Add your wallet adapter here when implemented
}
const ArchContext = createContext<ArchContextState | null>(null);
export function ArchProvider({ children, endpoint }: { children: React.ReactNode, endpoint: string }) {
const connection = new RpcConnection(endpoint);
const arch = ArchConnection(connection);
const value = {
connection,
arch,
};
return (
<ArchContext.Provider value={value}>
{children}
</ArchContext.Provider>
);
}
export function useArch() {
const context = useContext(ArchContext);
if (!context) {
throw new Error('useArch must be used within ArchProvider');
}
return context;
}
Custom Hooks
import { useState, useEffect } from 'react';
import { RpcConnection } from '@saturnbtcio/arch-sdk';
// Hook for monitoring block count
export function useBlockCount(endpoint: string) {
const [blockCount, setBlockCount] = useState<number | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<Error | null>(null);
useEffect(() => {
const connection = new RpcConnection(endpoint);
let mounted = true;
const fetchBlockCount = async () => {
try {
const count = await connection.getBlockCount();
if (mounted) {
setBlockCount(count);
setError(null);
}
} catch (err) {
if (mounted) {
setError(err as Error);
}
} finally {
if (mounted) {
setLoading(false);
}
}
};
fetchBlockCount();
const interval = setInterval(fetchBlockCount, 10000);
return () => {
mounted = false;
clearInterval(interval);
};
}, [endpoint]);
return { blockCount, loading, error };
}
// Hook for account information
export function useAccountInfo(pubkey: Uint8Array | null) {
const { connection } = useArch();
const [accountInfo, setAccountInfo] = useState<any>(null);
const [loading, setLoading] = useState(false);
useEffect(() => {
if (!pubkey) return;
let cancelled = false;
const fetchAccount = async () => {
setLoading(true);
try {
const info = await connection.readAccountInfo(pubkey);
if (!cancelled) {
setAccountInfo(info);
}
} catch (error) {
console.error('Failed to fetch account:', error);
if (!cancelled) {
setAccountInfo(null);
}
} finally {
if (!cancelled) {
setLoading(false);
}
}
};
fetchAccount();
return () => {
cancelled = true;
};
}, [pubkey, connection]);
return { accountInfo, loading };
}
Building Transaction Helpers
Since the SDK doesn’t include transaction builders, here’s how you might create your own:
import { PubkeyUtil } from '@saturnbtcio/arch-sdk';
import type { SanitizedMessage, SanitizedInstruction } from '@saturnbtcio/arch-sdk';
class TransactionBuilder {
private accountKeys: Uint8Array[] = [];
private instructions: SanitizedInstruction[] = [];
private signerCount = 0;
addSigner(pubkey: Uint8Array): number {
const index = this.accountKeys.length;
this.accountKeys.push(pubkey);
this.signerCount++;
return index;
}
addAccount(pubkey: Uint8Array): number {
const index = this.accountKeys.length;
this.accountKeys.push(pubkey);
return index;
}
addInstruction(
programId: Uint8Array,
accounts: number[],
data: Uint8Array
): void {
// Ensure program ID is in account keys
let programIdIndex = this.accountKeys.findIndex(
key => this.arraysEqual(key, programId)
);
if (programIdIndex === -1) {
programIdIndex = this.addAccount(programId);
}
this.instructions.push({
program_id_index: programIdIndex,
accounts,
data,
});
}
build(recentBlockhash: Uint8Array): SanitizedMessage {
return {
header: {
num_required_signatures: this.signerCount,
num_readonly_signed_accounts: 0,
num_readonly_unsigned_accounts: this.accountKeys.length - this.signerCount,
},
account_keys: this.accountKeys,
recent_blockhash: recentBlockhash,
instructions: this.instructions,
};
}
private arraysEqual(a: Uint8Array, b: Uint8Array): boolean {
if (a.length !== b.length) return false;
for (let i = 0; i < a.length; i++) {
if (a[i] !== b[i]) return false;
}
return true;
}
}
// Usage example
function createTransferMessage(from: Uint8Array, to: Uint8Array, amount: bigint): SanitizedMessage {
const builder = new TransactionBuilder();
// Add accounts
const fromIndex = builder.addSigner(from);
const toIndex = builder.addAccount(to);
// Create transfer instruction data
// Note: This is a simplified example - actual encoding depends on the program
const data = new Uint8Array(8);
new DataView(data.buffer).setBigUint64(0, amount, true);
// Add instruction
builder.addInstruction(
PubkeyUtil.systemProgram(),
[fromIndex, toIndex],
data
);
// Build with a recent blockhash (you need to fetch this)
const recentBlockhash = new Uint8Array(32); // Placeholder
return builder.build(recentBlockhash);
}
Security Considerations
When building Web3 integrations with the low-level SDK:
- Key Management: Never handle private keys directly in browser code
- Message Validation: Always validate message contents before signing
- Error Handling: Implement robust error handling for RPC calls
- Type Safety: Use TypeScript strictly to catch errors at compile time
- Input Validation: Validate all user inputs, especially addresses and amounts
Example: Simple dApp Structure
// services/arch.ts
import { RpcConnection, ArchConnection } from '@saturnbtcio/arch-sdk';
export class ArchService {
private connection: RpcConnection;
private arch: ReturnType<typeof ArchConnection>;
constructor(endpoint: string) {
this.connection = new RpcConnection(endpoint);
this.arch = ArchConnection(this.connection);
}
async getNetworkStatus() {
const blockCount = await this.connection.getBlockCount();
const bestBlockHash = await this.connection.getBestBlockHash();
return { blockCount, bestBlockHash };
}
async createAccount() {
return await this.arch.createNewAccount();
}
async getAccountInfo(pubkey: Uint8Array) {
return await this.connection.readAccountInfo(pubkey);
}
}
// components/NetworkStatus.tsx
import React from 'react';
import { useBlockCount } from '../hooks/useBlockCount';
export function NetworkStatus() {
const { blockCount, loading, error } = useBlockCount('http://localhost:9002');
if (loading) return <div>Loading network status...</div>;
if (error) return <div>Error: {error.message}</div>;
return (
<div>
<h3>Network Status</h3>
<p>Current block: {blockCount}</p>
</div>
);
}
Next Steps
Since the TypeScript SDK is low-level, you’ll need to:
- Implement your own wallet integration layer
- Build transaction construction utilities
- Create state management solutions
- Develop UI components for common operations
For more information: