Skip to content
Merged
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
15 changes: 14 additions & 1 deletion docs/api-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,21 @@ You can also import everything from the root `@guildpass/sdk` (the adapter subpa

## GuildPassClient

The main constructor.
The main constructor. You can initialize the client by passing a config object directly, or by using the `GuildPassClientBuilder` for a fluent interface.

### Initialization

**Using the Builder (Recommended):**
```typescript
import { GuildPassClientBuilder } from '@guildpass/sdk';

const client = new GuildPassClientBuilder('https://api.guildpass.xyz')
.withApiKey('secret-key')
.withTimeout(5000)
.build();
```

**Using the Constructor:**
```typescript
new GuildPassClient(config: GuildPassClientConfig)
```
Expand Down
17 changes: 17 additions & 0 deletions docs/sdk-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,23 @@

This guide covers advanced usage and patterns for the GuildPass SDK.

## Client Configuration Builder

As the SDK gains support for more options, configuring the `GuildPassClient` directly with an object can become complex. The `GuildPassClientBuilder` provides a fluent, strongly typed way to build and validate your client configuration before runtime.

```typescript
import { GuildPassClientBuilder } from "@guildpass/sdk";

// Validate and build fluently
const client = new GuildPassClientBuilder('https://api.guildpass.xyz')
.withApiKey(process.env.GUILDPASS_API_KEY)
.withTimeout(5000)
.withRetry({ maxRetries: 3 })
.build();
```

The builder validates your configuration when `.build()` is called, immediately throwing a `GuildPassConfigError` if any settings are invalid. Existing configuration methods (passing an object directly to the constructor) continue to work exactly as before.

## Error Handling

The SDK uses a custom `GuildPassError` class. You should always wrap SDK calls in try-catch blocks.
Expand Down
142 changes: 142 additions & 0 deletions src/client/GuildPassClientBuilder.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
import { GuildPassClientConfig, validateConfig } from '../config/sdkConfig';
import { GuildPassClient } from './GuildPassClient';
import type { HttpTransport } from '../network/transport.types';
import type { CacheAdapter } from '../cache/cache.types';
import type { ContractProvider } from '../contracts/providers/provider.types';
import type { FetchLike, HttpHooks, Middleware, RateLimitConfig, RetryConfig } from '../http/http.types';
import type { ChainConfig, ContractReadConsensus } from '../contracts/contract.types';

export class GuildPassClientBuilder {
private config: Partial<GuildPassClientConfig>;

constructor(apiUrl?: string) {
this.config = {};
if (apiUrl) {
this.config.apiUrl = apiUrl;
}
}

public withApiUrl(apiUrl: string): this {
this.config.apiUrl = apiUrl;
return this;
}

public withApiKey(apiKey: string): this {
this.config.apiKey = apiKey;
return this;
}

public withTimeout(timeoutMs: number): this {
this.config.defaultTimeoutMs = timeoutMs;
// Keep timeoutMs in sync for backward compatibility, although defaultTimeoutMs takes precedence
this.config.timeoutMs = timeoutMs;
return this;
}

public withRetry(retry: RetryConfig): this {
this.config.retry = retry;
return this;
}

public withRateLimit(rateLimit: RateLimitConfig): this {
this.config.rateLimit = rateLimit;
return this;
}

public withTransport(transport: HttpTransport): this {
this.config.transport = transport;
return this;
}

public withFetch(fetchFn: FetchLike): this {
this.config.fetch = fetchFn;
return this;
}

public withHooks(hooks: HttpHooks): this {
this.config.hooks = hooks;
return this;
}

public withMiddleware(middleware: Middleware[]): this {
this.config.middleware = middleware;
return this;
}

public withCache(cache: CacheAdapter, ttlMs?: number): this {
this.config.cache = cache;
if (ttlMs !== undefined) {
this.config.cacheTtl = ttlMs;
}
return this;
}

public withContractProvider(provider: ContractProvider): this {
this.config.contractProvider = provider;
return this;
}

public withRpcUrl(rpcUrl: string): this {
this.config.rpcUrl = rpcUrl;
return this;
}

public withRpcUrls(rpcUrls: string[]): this {
this.config.rpcUrls = rpcUrls;
return this;
}

public withChain(chainId: number, chainConfig: ChainConfig): this {
if (!this.config.chains) {
this.config.chains = {};
}
this.config.chains[chainId] = chainConfig;
return this;
}

public withBatchStrategy(strategy: 'jsonrpc' | 'multicall3'): this {
this.config.batchStrategy = strategy;
return this;
}

public withContractReadConsensus(consensus: ContractReadConsensus): this {
this.config.contractReadConsensus = consensus;
return this;
}

public withDeduplication(enabled: boolean): this {
this.config.deduplication = enabled;
return this;
}

public withStrictAddressChecksum(enabled: boolean): this {
this.config.strictAddressChecksum = enabled;
return this;
}

public withStrictInterfaceChecking(enabled: boolean): this {
this.config.strictInterfaceChecking = enabled;
return this;
}

public withSignedResponses(enabled: boolean, trustedSignerAddress?: string): this {
this.config.verifySignedResponses = enabled;
if (trustedSignerAddress) {
this.config.trustedSignerAddress = trustedSignerAddress;
}
return this;
}

public withClientMetadata(name: string, version: string): this {
this.config.sendClientMetadata = true;
this.config.clientName = name;
this.config.clientVersion = version;
return this;
}

public build(): GuildPassClient {
// Validate config before constructing the client
validateConfig(this.config as GuildPassClientConfig);
return new GuildPassClient(this.config as GuildPassClientConfig);
}
}
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
// Main Client
export * from './client/GuildPassClient';
export * from './client/GuildPassClientBuilder';

// Cache
export * from './cache/cache.types';
Expand Down
75 changes: 75 additions & 0 deletions tests/GuildPassClientBuilder.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import { describe, it, expect, vi } from 'vitest';
import { GuildPassClientBuilder } from '../src/client/GuildPassClientBuilder';
import { GuildPassClient } from '../src/client/GuildPassClient';
import { GuildPassConfigError } from '../src/errors/errorTypes';

describe('GuildPassClientBuilder', () => {
it('should build a client with only apiUrl', () => {
const builder = new GuildPassClientBuilder('https://api.test.com');

// We mock global fetch just for validation if we are in node where fetch might not exist
const fetchSpy = vi.fn();
vi.stubGlobal('fetch', fetchSpy);

const client = builder.build();

expect(client).toBeInstanceOf(GuildPassClient);
expect(client.getConfig().apiUrl).toBe('https://api.test.com');

vi.unstubAllGlobals();
});

it('should allow building config fluently', () => {
const fetchSpy = vi.fn();
vi.stubGlobal('fetch', fetchSpy);

const client = new GuildPassClientBuilder()
.withApiUrl('https://api.test.com')
.withApiKey('secret-key')
.withTimeout(5000)
.withRpcUrl('https://rpc.test.com')
.withStrictAddressChecksum(true)
.build();

const config = client.getConfig();
expect(config.apiUrl).toBe('https://api.test.com');
// apiKey is redacted from getConfig(), so we can't assert it easily via getConfig
// but we can check other public config values
expect(config.defaultTimeoutMs).toBe(5000);
expect(config.rpcUrl).toBe('https://rpc.test.com');
expect(config.strictAddressChecksum).toBe(true);

vi.unstubAllGlobals();
});

it('should validate configuration before building', () => {
const builder = new GuildPassClientBuilder('not-a-valid-url');

expect(() => builder.build()).toThrow(GuildPassConfigError);
});

it('should support advanced options like retry and cache', () => {
const fetchSpy = vi.fn();
vi.stubGlobal('fetch', fetchSpy);

const mockCache = {
get: async () => null,
set: async () => {},
delete: async () => {},
clear: async () => {},
};

const client = new GuildPassClientBuilder('https://api.test.com')
.withRetry({ maxRetries: 3, baseDelayMs: 100 })
.withCache(mockCache, 60000)
.withBatchStrategy('multicall3')
.build();

const config = client.getConfig();
expect(config.retry?.maxRetries).toBe(3);
expect(config.cacheTtl).toBe(60000);
expect(config.batchStrategy).toBe('multicall3');

vi.unstubAllGlobals();
});
});