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
16 changes: 16 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,19 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
## [Unreleased]

### Added
- **Batch partial-failure entries now carry a `GuildPassErrorCode`** — resolves [#390](https://github.com/Adamantine-Guild/guildpass-sdk/issues/390). `BatchItemResult` gains an optional `code` field, populated on every `{ status: 'error' }` entry returned by `batchEthCall`, `getMembershipTokenBalancesBatch` and `getGuildOwnersBatch`. Callers debugging a partial failure at scale can finally branch on *why* an individual item failed — a contract revert, an unusable node response, a consensus disagreement — instead of pattern-matching a human-readable string that was never a stable API.
- Codes are consistent across all three batch strategies (JSON-RPC batch, Multicall3 `aggregate3`, and the adaptive provider's sequential fallback): a contract-level revert is `HTTP_ERROR` wherever it occurs, matching the code the single-call `eth_call` path already throws for the identical JSON-RPC error envelope; a missing, empty, wrong-typed, oversized or short response is `INVALID_RESPONSE`; a per-item quorum failure is `CONSENSUS_MISMATCH`. Which strategy the adaptive provider happened to pick can no longer change an item's classification.
- Three sites previously discarded the `GuildPassError` they caught, flattening a precise code into a bare string: the adaptive provider's sequential fallback, and the balance and guild-owner decode steps (both a bare `catch {}`). All three now propagate the original `code`, falling back to `UNKNOWN_ERROR` and `INVALID_RESPONSE` respectively. A code raised by a custom `contractProvider` survives to the caller unchanged.
- **Every `error` string is byte-for-byte unchanged**, including `'execution reverted'`, `'Reverted: <reason>'`, `'Missing Multicall3 result'` and `'Failed to decode balance result'`; existing assertions and log scrapers keep working. `code` is added alongside them, never in place of them.
- Purely additive: `code` is optional on the existing flat `BatchItemResult` rather than turning it into a discriminated union, so code that reads `item.result` without narrowing still compiles. Success entries never carry a `code`.
- The field is optional only because `ContractProvider` is a public interface a third party can implement. Results from a user-supplied `contractProvider` are normalised as they enter `ContractClient`: a missing `code`, or one that is not a member of this build's `GuildPassErrorCode`, becomes `UNKNOWN_ERROR`. An error entry from any of the three batch methods therefore always carries a real enum member — `code: undefined` is only observable if you call a `ContractProvider` directly instead of going through the client.
- Pre-flight validation is unchanged: an invalid wallet address, an empty array, or a batch exceeding `maxBatchSize` still **throws** before any RPC call rather than becoming a per-item entry. `code` classifies failures that happen *during* execution. Documented with a branching example in [`docs/api-reference.md`](docs/api-reference.md).
- `GuildPassErrorCode` is a string enum, so compare with `GuildPassErrorCode.HTTP_ERROR` rather than the `'HTTP_ERROR'` literal.
- Per this project's pre-1.0 versioning policy this ships as a **minor** bump.
- **`GuildPassErrorCode.WS_CONNECTION_ERROR` now exists** — found while implementing [#390](https://github.com/Adamantine-Guild/guildpass-sdk/issues/390). `WebSocketContractProvider` referenced this member at six sites but it was never declared on the enum, so every WebSocket failure was constructed with `code: undefined`.
- The consequences were invisible in-process because `isGuildPassError` short-circuits on `instanceof`, but a code of `undefined` fails the structural branch of that guard — so a WebSocket error that crossed a realm boundary, arrived from a duplicated copy of the SDK, or was serialised via `toJSON()` was no longer recognisable as a GuildPass error at all, and carried nothing to branch on.
- Two of the provider's tests asserted `code: GuildPassErrorCode.WS_CONNECTION_ERROR`, which itself evaluated to `undefined`; they compared `undefined` to `undefined` and passed while proving nothing. They now assert a real value.
- Additive: no existing code is affected, and the enum's other 29 members are unchanged.
- **`client.guilds.getGuildConfigBatch(params, options?)`** — resolves [#389](https://github.com/Adamantine-Guild/guildpass-sdk/issues/389). Fetches configuration for several guilds in one call, returning `BatchItemResult<GuildConfig>[]` in input order with per-guild failure isolation, matching the contract of `checkAccessBatch` and `getGuildOwnersBatch`.
- **`BatchItemResult` is now generic**, `BatchItemResult<T = string>`. The default preserves the existing meaning (raw hex from the contract batch methods) so every current call site and consumer type stays source-compatible; batch methods resolving richer values parameterise it instead.
- Client-side fan-out over the existing `GET /guilds/:id/config` endpoint — no batch endpoint is assumed — through a bounded worker pool. `concurrency` defaults to `5` and is capped at `50`, matching `checkAccessBatch`.
Expand All @@ -28,6 +41,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
- `InMemoryCacheAdapterOptions` is exported for consumers who want to type their options.

### Fixed
- **`toErrorCode` no longer returns a code that is not a real enum member** — hardening found while implementing [#390](https://github.com/Adamantine-Guild/guildpass-sdk/issues/390). Because `isGuildPassError` short-circuits on `instanceof`, an error carrying a code that does not exist at runtime passed the guard and its `undefined` was returned verbatim, defeating the `fallback` argument and breaking the function's own return type.
- This is reachable from user code: a custom `contractProvider` compiled against a different SDK version can throw a `GuildPassError` whose code has since been renamed or removed. Such an item now falls back to `UNKNOWN_ERROR` (or the caller's chosen fallback) instead of surfacing a `BatchItemResult` with `code: undefined`, which is exactly the state [#390](https://github.com/Adamantine-Guild/guildpass-sdk/issues/390) exists to eliminate.
- New `tests/error-code-integrity.test.ts` locks the invariant repo-wide: it scans every `GuildPassErrorCode.X` reference in `src/` (ignoring comments) and fails if the member is not declared, asserts each member's value equals its own name, and checks that an error built from any member survives both a realm boundary and a JSON round-trip. It reproduces the `WS_CONNECTION_ERROR` bug when run against the previous code.
- **`chains` entries are now resolved as overrides instead of replacements** — resolves [#393](https://github.com/Adamantine-Guild/guildpass-sdk/issues/393). `resolveChainConfig` returned the matching `chains[chainId]` entry verbatim, so a partial entry such as `chains: { 8453: { contractAddress: '0x…' } }` silently discarded the top-level `rpcUrl` and the call failed with a generic error that never mentioned the chain. Every field a `chains` entry does not declare is now inherited from the top level, matching what the README has always documented.
- Fields are merged individually rather than by spreading, so a field an entry declares as `undefined` no longer clobbers a usable top-level value.
- Whether an RPC endpoint is present is decided through `mergeRpcUrls`, so a config supplying only `rpcUrls` is no longer misreported as missing `rpcUrl`.
Expand Down
37 changes: 35 additions & 2 deletions api-report/guildpass-sdk.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,7 @@ export type BatchItemResult<T = string> = {
status: 'success' | 'error';
result?: T;
error?: string;
code?: GuildPassErrorCode;
};

// @public
Expand Down Expand Up @@ -836,7 +837,8 @@ export enum GuildPassErrorCode {
// (undocumented)
UNKNOWN_ERROR = "UNKNOWN_ERROR",
// (undocumented)
UNVERIFIABLE_RESPONSE = "UNVERIFIABLE_RESPONSE"
UNVERIFIABLE_RESPONSE = "UNVERIFIABLE_RESPONSE",
WS_CONNECTION_ERROR = "WS_CONNECTION_ERROR"
}

// @public
Expand Down Expand Up @@ -946,9 +948,11 @@ export class HealthTracker {
get multicallPreferenceThreshold(): number;
recordFailure(url: string, now?: number): void;
recordSuccess(url: string, latencyMs: number): void;
recordTimeout(url: string, now?: number): void;
snapshot(url: string): Readonly<UrlHealth> | undefined;
// (undocumented)
snapshotAll(): Record<string, Readonly<UrlHealth>>;
timeoutCount(url: string): number;
}

// @public (undocumented)
Expand Down Expand Up @@ -1502,6 +1506,12 @@ export interface SiweVerifyResult {
success: boolean;
}

// @public
export interface SubscribableContractProvider extends ContractProvider {
destroy(): void;
subscribe(contractAddress: string, callback: TransferCallback): Promise<() => void>;
}

// @public (undocumented)
export const SUPPORTED_NETWORKS: Record<number, NetworkConfig>;

Expand Down Expand Up @@ -1540,6 +1550,18 @@ export type TokenBalancesBatchParams = {
chunkConcurrency?: number;
};

// @public
export type TransferCallback = (event: TransferEvent) => void;

// @public
export type TransferEvent = {
from: string;
to: string;
value: bigint;
transactionHash: string;
blockNumber: number;
};

// @public (undocumented)
export interface TransportRequest {
// (undocumented)
Expand Down Expand Up @@ -1583,6 +1605,7 @@ export type UrlHealth = {
latencyEmaMs: number;
circuitOpen: boolean;
openUntil: number;
timeoutCount?: number;
};

// @public
Expand Down Expand Up @@ -1662,9 +1685,19 @@ export function verifySiweSignatureWithReplayProtection(params: SiweVerifyAsyncP
// @public
export function verifyTypedDataSignature(domain: EIP712Domain, types: EIP712Types, primaryType: string, message: EIP712Message, signature: string, expectedSigner: string): EIP712VerifyResult;

// @public
export type WebSocketProviderConfig = {
wssUrl: string;
maxReconnects?: number;
baseDelayMs?: number;
maxDelayMs?: number;
subscribeTimeoutMs?: number;
requestTimeoutMs?: number;
};

// Warnings were encountered during analysis:
//
// dist/common-_RgT9NFP.d.ts:239:5 - (ae-forgotten-export) The symbol "ClientMetadata" needs to be exported by the entry point index.d.ts
// dist/errorCodes-DuQ7Uugs.d.ts:239:5 - (ae-forgotten-export) The symbol "ClientMetadata" needs to be exported by the entry point index.d.ts

// (No @packageDocumentation comment for this package)

Expand Down
Loading
Loading