Skip to content

Commit f922e47

Browse files
committed
feat: implement extensible API provider architecture
- Add ApiProvider interface with multiple implementations - Support HoosatProxyProvider and HoosatNetworkProvider - Add MultiProvider with failover/fastest/round-robin strategies - Maintain 100% backward compatibility with existing HoosatWebClient - Update examples to use HoosatWebClient and calculateMinFee - Export factory functions for easy provider creation
1 parent 50368c3 commit f922e47

File tree

9 files changed

+516
-143
lines changed

9 files changed

+516
-143
lines changed

examples/example-wallet.html

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -227,10 +227,10 @@ <h2>📋 Activity Log</h2>
227227
</div>
228228

229229
<script type="module">
230-
import { HoosatCrypto, HoosatUtils, HoosatTxBuilder, HoosatBrowserClient } from '../dist/hoosat-sdk.es.js';
230+
import { HoosatCrypto, HoosatUtils, HoosatTxBuilder, HoosatWebClient } from '../dist/hoosat-sdk.es.js';
231231

232232
// Initialize API client
233-
const client = new HoosatBrowserClient({
233+
const client = new HoosatWebClient({
234234
baseUrl: 'https://proxy.hoosat.net/api/v1',
235235
timeout: 30000,
236236
debug: true,
@@ -359,7 +359,7 @@ <h2>📋 Activity Log</h2>
359359
builder.addOutput(recipient, amountSompi);
360360

361361
// Calculate fee
362-
const estimatedFee = HoosatCrypto.calculateFee(
362+
const estimatedFee = HoosatCrypto.calculateMinFee(
363363
utxosResult.utxos.length,
364364
2 // recipient + change
365365
);
@@ -391,20 +391,19 @@ <h2>📋 Activity Log</h2>
391391
logMessage('🌐 Fetching network info...', 'info');
392392

393393
const info = await client.getNetworkInfo();
394-
const tip = await client.getBlockTip();
395394

396395
document.getElementById('networkInfo').innerHTML = `
397396
<div class="wallet-info">
398-
<label>Network</label>
399-
<div class="value">${info.networkName}</div>
397+
<label>Server Version</label>
398+
<div class="value">${info.serverVersion}</div>
400399
</div>
401400
<div class="wallet-info">
402-
<label>Block Height</label>
403-
<div class="value">${info.blockCount}</div>
401+
<label>Mempool Size</label>
402+
<div class="value">${info.mempoolSize}</div>
404403
</div>
405404
<div class="wallet-info">
406-
<label>Latest Block Hash</label>
407-
<div class="value">${tip.hash}</div>
405+
<label>P2P ID</label>
406+
<div class="value">${info.p2pId}</div>
408407
</div>
409408
<div class="wallet-info">
410409
<label>Synced</label>

src/client/client-web.ts

Lines changed: 39 additions & 133 deletions
Original file line numberDiff line numberDiff line change
@@ -1,127 +1,71 @@
11
import type {
2-
ApiResponse,
32
AddressBalance,
43
AddressUtxos,
54
TransactionSubmission,
65
NetworkInfo,
76
FeeRecommendation,
87
BrowserClientConfig,
9-
RequestOptions,
108
} from './client-web.types';
119
import type { Transaction } from '@models/transaction.types';
10+
import type { ApiProvider } from './providers/api-provider.interface';
11+
import { HoosatProxyProvider } from './providers/hoosat-proxy-provider';
12+
import type { MultiProvider } from './providers/multi-provider';
1213

1314
/**
1415
* HoosatWebClient - REST API client for browser-based Hoosat applications
1516
*
16-
* Provides methods to interact with Hoosat blockchain via REST API proxy.
17+
* Now supports multiple API providers with automatic fallback and extensible architecture.
1718
* All methods return promises and handle errors gracefully.
1819
*
1920
* @example
2021
* ```typescript
22+
* // Using single provider (backward compatible)
2123
* const client = new HoosatWebClient({
2224
* baseUrl: 'https://proxy.hoosat.net/api/v1',
2325
* timeout: 30000
2426
* });
2527
*
26-
* // Get balance
27-
* const balance = await client.getBalance('hoosat:qz7ulu...');
28-
* console.log(`Balance: ${balance.balance} sompi`);
28+
* // Using custom provider
29+
* const customProvider = new HoosatProxyProvider({ baseUrl: 'https://proxy.hoosat.net/api/v1' });
30+
* const client = new HoosatWebClient({ provider: customProvider });
2931
*
30-
* // Get UTXOs for transaction
31-
* const utxos = await client.getUtxos('hoosat:qz7ulu...');
32+
* // Using multiple providers with fallback
33+
* const multiProvider = new MultiProvider({
34+
* providers: [proxyProvider, networkProvider],
35+
* strategy: 'failover'
36+
* });
37+
* const client = new HoosatWebClient({ provider: multiProvider });
3238
* ```
3339
*/
3440
export class HoosatWebClient {
35-
private readonly _baseUrl: string;
36-
private readonly _timeout: number;
37-
private readonly _headers: Record<string, string>;
38-
private readonly _debug: boolean;
41+
private readonly provider: ApiProvider;
3942

4043
/**
4144
* Creates a new HoosatWebClient instance
4245
*
4346
* @param config - Client configuration
44-
* @param config.baseUrl - Base URL of the API (e.g., 'https://proxy.hoosat.net/api/v1')
47+
* @param config.baseUrl - Base URL of the API (backward compatibility)
48+
* @param config.provider - Custom API provider instance
4549
* @param config.timeout - Request timeout in milliseconds (default: 30000)
4650
* @param config.headers - Additional headers to include in requests
4751
* @param config.debug - Enable debug logging (default: false)
4852
*/
49-
constructor(config: BrowserClientConfig) {
50-
this._baseUrl = config.baseUrl.replace(/\/$/, ''); // Remove trailing slash
51-
this._timeout = config.timeout || 30000;
52-
this._headers = {
53-
'Content-Type': 'application/json',
54-
...config.headers,
55-
};
56-
this._debug = config.debug || false;
57-
}
58-
59-
// ==================== PRIVATE HELPERS ====================
60-
61-
/**
62-
* Make HTTP request with timeout and error handling
63-
* @private
64-
*/
65-
private async request<T>(endpoint: string, options: RequestInit & RequestOptions = {}): Promise<T> {
66-
const url = `${this._baseUrl}${endpoint}`;
67-
const timeout = options.timeout || this._timeout;
68-
69-
if (this._debug) {
70-
console.log(`[HoosatWebClient] ${options.method || 'GET'} ${url}`);
71-
if (options.body) {
72-
console.log('[HoosatWebClient] Request body:', options.body);
73-
}
74-
}
75-
76-
// Create abort controller for timeout
77-
const controller = new AbortController();
78-
const timeoutId = setTimeout(() => controller.abort(), timeout);
79-
80-
try {
81-
const response = await fetch(url, {
82-
...options,
83-
headers: {
84-
...this._headers,
85-
...options.headers,
86-
},
87-
signal: controller.signal,
53+
constructor(config: BrowserClientConfig & { provider?: ApiProvider }) {
54+
if (config.provider) {
55+
this.provider = config.provider;
56+
} else if (config.baseUrl) {
57+
this.provider = new HoosatProxyProvider({
58+
baseUrl: config.baseUrl,
59+
timeout: config.timeout,
60+
headers: config.headers,
61+
debug: config.debug,
8862
});
89-
90-
clearTimeout(timeoutId);
91-
92-
// Parse response
93-
const data: ApiResponse<T> = (await response.json()) as ApiResponse<T>;
94-
95-
if (this._debug) {
96-
console.log('[HoosatWebClient] Response:', data);
97-
}
98-
99-
// Check API response format
100-
if (!data.success) {
101-
throw new Error(data.error || 'API request failed');
102-
}
103-
104-
if (!data.data) {
105-
throw new Error('API response missing data field');
106-
}
107-
108-
return data.data;
109-
} catch (error: any) {
110-
clearTimeout(timeoutId);
111-
112-
if (error.name === 'AbortError') {
113-
throw new Error(`Request timeout after ${timeout}ms`);
114-
}
115-
116-
if (this._debug) {
117-
console.error('[HoosatWebClient] Error:', error);
118-
}
119-
120-
throw error;
63+
} else {
64+
throw new Error('Either baseUrl or provider must be specified');
12165
}
12266
}
12367

124-
// ==================== ADDRESS METHODS ====================
68+
// ==================== PUBLIC API METHODS ====================
12569

12670
/**
12771
* Get balance for a Hoosat address
@@ -137,7 +81,7 @@ export class HoosatWebClient {
13781
* ```
13882
*/
13983
async getBalance(address: string): Promise<AddressBalance> {
140-
return this.request<AddressBalance>(`/address/${address}/balance`);
84+
return this.provider.getBalance(address);
14185
}
14286

14387
/**
@@ -160,32 +104,9 @@ export class HoosatWebClient {
160104
* ```
161105
*/
162106
async getUtxos(addresses: string[]): Promise<AddressUtxos> {
163-
const response = await this.request<any>('/address/utxos', {
164-
method: 'POST',
165-
body: JSON.stringify({ addresses }),
166-
});
167-
168-
// Adapt API response to TxBuilder format
169-
// API returns: scriptPublicKey.scriptPublicKey
170-
// TxBuilder expects: scriptPublicKey.script
171-
if (response.utxos) {
172-
response.utxos = response.utxos.map((utxo: any) => ({
173-
...utxo,
174-
utxoEntry: {
175-
...utxo.utxoEntry,
176-
scriptPublicKey: {
177-
version: utxo.utxoEntry.scriptPublicKey.version,
178-
script: utxo.utxoEntry.scriptPublicKey.scriptPublicKey, // Rename field
179-
},
180-
},
181-
}));
182-
}
183-
184-
return response;
107+
return this.provider.getUtxos(addresses);
185108
}
186109

187-
// ==================== TRANSACTION METHODS ====================
188-
189110
/**
190111
* Submit a signed transaction to the network
191112
*
@@ -205,14 +126,9 @@ export class HoosatWebClient {
205126
* ```
206127
*/
207128
async submitTransaction(transaction: Transaction): Promise<TransactionSubmission> {
208-
return this.request<TransactionSubmission>('/transaction/submit', {
209-
method: 'POST',
210-
body: JSON.stringify(transaction),
211-
});
129+
return this.provider.submitTransaction(transaction);
212130
}
213131

214-
// ==================== NETWORK METHODS ====================
215-
216132
/**
217133
* Get network information
218134
*
@@ -227,7 +143,7 @@ export class HoosatWebClient {
227143
* ```
228144
*/
229145
async getNetworkInfo(): Promise<NetworkInfo> {
230-
return this.request<NetworkInfo>('/node/info');
146+
return this.provider.getNetworkInfo();
231147
}
232148

233149
/**
@@ -245,7 +161,7 @@ export class HoosatWebClient {
245161
* ```
246162
*/
247163
async getFeeEstimate(): Promise<FeeRecommendation> {
248-
return this.request<FeeRecommendation>('/mempool/fee-estimate');
164+
return this.provider.getFeeEstimate();
249165
}
250166

251167
// ==================== UTILITY METHODS ====================
@@ -264,25 +180,15 @@ export class HoosatWebClient {
264180
* ```
265181
*/
266182
async ping(): Promise<boolean> {
267-
try {
268-
await this.getNetworkInfo();
269-
return true;
270-
} catch (error) {
271-
return false;
272-
}
183+
return this.provider.ping();
273184
}
274185

275186
/**
276-
* Get current configuration
187+
* Get the current API provider instance
277188
*
278-
* @returns Client configuration
189+
* @returns Current provider
279190
*/
280-
getConfig(): BrowserClientConfig {
281-
return {
282-
baseUrl: this._baseUrl,
283-
timeout: this._timeout,
284-
headers: { ...this._headers },
285-
debug: this._debug,
286-
};
191+
getProvider(): ApiProvider {
192+
return this.provider;
287193
}
288194
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import type {
2+
AddressBalance,
3+
AddressUtxos,
4+
TransactionSubmission,
5+
NetworkInfo,
6+
FeeRecommendation,
7+
} from '../client-web.types';
8+
import type { Transaction } from '@models/transaction.types';
9+
10+
export interface ApiProvider {
11+
getBalance(address: string): Promise<AddressBalance>;
12+
getUtxos(addresses: string[]): Promise<AddressUtxos>;
13+
submitTransaction(tx: Transaction): Promise<TransactionSubmission>;
14+
getNetworkInfo(): Promise<NetworkInfo>;
15+
getFeeEstimate(): Promise<FeeRecommendation>;
16+
ping(): Promise<boolean>;
17+
}
18+
19+
export interface ProviderConfig {
20+
baseUrl: string;
21+
timeout?: number;
22+
headers?: Record<string, string>;
23+
debug?: boolean;
24+
}
25+
26+
export interface EndpointConfig {
27+
balance: string;
28+
utxos: string;
29+
submitTransaction: string;
30+
networkInfo: string;
31+
feeEstimate: string;
32+
}

0 commit comments

Comments
 (0)