Skip to content

Conversation

@cameronvoell
Copy link
Contributor

@cameronvoell cameronvoell commented Oct 17, 2025

Add gatewayUrl support across JS, Android, and iOS client creation paths and update XMTP dependencies for D14z client prerelease in index.ts and native modules

This pull request introduces a gatewayUrl parameter to the JS APIs and native client configuration on Android and iOS, and updates XMTP dependencies to prerelease versions. It extends auth parameter parsing to normalize empty strings and propagates gatewayUrl into ClientOptions.Api for environment-specific clients.

📍Where to Start

Start with the JS entry points that introduce gatewayUrl in index.createRandom, index.create, index.build, and index.ffiCreateClient in index.ts, then follow propagation through Client factories in Client.ts into Android XMTPModule.apiEnvironments and iOS XMTPModule.createApiClient.


📊 Macroscope summarized ab41c70. 6 files reviewed, 23 issues evaluated, 22 issues filtered, 0 comments posted

🗂️ Filtered Issues

android/src/main/java/expo/modules/xmtpreactnativesdk/XMTPModule.kt — 0 comments posted, 4 evaluated, 3 filtered
  • line 207: Race and inconsistent state due to shared preAuthenticateToInboxCallbackDeferred being overwritten without synchronization or per-instance scoping. If clientOptions(...) is called multiple times with hasPreAuthenticateToInboxCallback == true before prior callback flows complete, preAuthenticateToInboxCallbackDeferred is reassigned, causing any ongoing await() in the previously created callback to now wait on the new deferred. The old deferred becomes orphaned and will never be completed, and the first flow can deadlock or wait on the wrong completion signal. This violates at-most-once callback consumption, atomicity, and no-leak/no-double-apply guarantees. [ Low confidence ]
  • line 207: Stale preAuthenticateToInboxCallbackDeferred when hasPreAuthenticateToInboxCallback is false: The function sets preAuthenticateToInboxCallbackDeferred only when hasPreAuthenticateToInboxCallback == true and never clears it when false. If a previous call left a non-null deferred, a subsequent call without a callback leaves the stale deferred in memory and accessible, violating the 'no leaks' requirement and potentially causing confusion or incorrect behavior if other parts of the module read it. [ Invalidated by documentation search ]
  • line 211: Unhandled exceptions from AuthParamsWrapper.authParamsFromJson(authParams) can cause clientOptions(...) to fail catastrophically without a graceful outcome. If authParams is malformed JSON or missing required fields (e.g., environment), authParamsFromJson can throw, and clientOptions does not catch this. This violates the requirement that all inputs (including empty or invalid) must reach a defined terminal state with a visible outcome. In addition, the failure can leave earlier side effects (see related issues) in an inconsistent state. [ Invalidated by documentation search ]
android/src/main/java/expo/modules/xmtpreactnativesdk/wrappers/AuthParamsWrapper.kt — 0 comments posted, 4 evaluated, 4 filtered
  • line 18: JsonParser.parseString(authParams).asJsonObject assumes authParams is valid JSON and a JSON object. If authParams is malformed JSON or a JSON array/primitive, asJsonObject will throw at runtime (e.g., IllegalStateException). There is no guard or graceful fallback, so any bad input crashes the call path. [ Low confidence ]
  • line 20: jsonOptions.get("environment").asString dereferences the environment member without checking existence or nullability. If the environment key is missing or is JsonNull/non-string, this will throw at runtime (e.g., NullPointerException or IllegalStateException). There is no fallback for mandatory environment. [ Low confidence ]
  • line 21: Optional string fields use jsonOptions.has(key) to decide whether to read asString, but has(key) returns true even when the value is JsonNull. In such cases, jsonOptions.get(key).asString will throw IllegalStateException. This affects dbDirectory, historySyncUrl, customLocalUrl, appVersion, and gatewayUrl. [ Invalidated by documentation search ]
  • line 24: Boolean fields deviceSyncEnabled and debugEventsEnabled read asBoolean when the key exists. If the value is JsonNull or a non-boolean type, asBoolean will throw. JsonObject.has(key) returns true for keys with JsonNull, so explicit nulls cause runtime exceptions. There is no type/nullable guard before asBoolean. [ Invalidated by documentation search ]
example/src/tests/clientTests.ts — 0 comments posted, 4 evaluated, 4 filtered
  • line 318: Persistent filesystem side effects without cleanup can lead to test pollution. The test creates a database directory at RNFS.DocumentDirectoryPath + '/xmtp_db' if it doesn't exist and leaves it in place. Subsequent test runs may operate on residual state, potentially affecting reproducibility or causing false positives/negatives. To preserve invariants across runs, consider cleaning up the directory after the test or using a uniquely-scoped temporary directory. [ Code style ]
  • line 323: Unsafe property access on Config may throw before the intended validation. The code checks if (!Config.GATEWAY_URL) to validate the environment variable, but if the react-native-config module isn't properly initialized or Config is undefined at runtime, accessing Config.GATEWAY_URL will throw a TypeError (cannot read properties of undefined) before reaching the guard, causing the test to crash. Add a preceding check (e.g., if (!Config || !Config.GATEWAY_URL)) to ensure a defined object before property access. [ Test / Mock code ]
  • line 338: Contract parity inconsistency with gatewayUrl: The test configures the client using a custom gatewayUrl (options.gatewayUrl), but Client.getOrCreateInboxId only accepts identity and env and does not take or use gatewayUrl. If the XMTP stack uses the gatewayUrl to route requests, calling getOrCreateInboxId without it may hit a different gateway than the one used to create the client, potentially yielding a different inboxId and causing the inboxIds should match assertion to fail. This breaks parity between client creation/build paths and this lookup path. [ Test / Mock code ]
  • line 340: Environment mismatch risk: Client.getOrCreateInboxId is called with a hardcoded 'dev' value (env: 'dev' as XMTPEnvironment) instead of reusing options.env. If options.env differs from 'dev' (now or in future refactors), the test will derive the inboxId from a different environment than the one used to create the client, potentially causing the subsequent equality assertion to fail and violating contract parity across code paths. [ Test / Mock code ]
ios/Wrappers/AuthParamsWrapper.swift — 0 comments posted, 2 evaluated, 2 filtered
  • line 49: environment is not normalized using stringOrNil like other string fields, so an empty string value from JSON (e.g., "environment":"") will be preserved as "" instead of defaulting to "dev" or nil. This creates an inconsistency in normalization compared to dbDirectory, historySyncUrl, customLocalUrl, appVersion, and gatewayUrl, and can propagate an invalid/empty environment value through the system, violating the canonicalization applied to other fields. [ Low confidence ]
  • line 54: Strict casting of deviceSyncEnabled and debugEventsEnabled using as? Bool can silently misinterpret valid but non-boolean JSON inputs (e.g., 0/1 numbers or string values like "false"). In such cases, the code defaults to true for deviceSyncEnabled and false for debugEventsEnabled, potentially enabling device sync when it was intended to be disabled or vice versa. Since inputs are unconstrained and JSONSerialization may yield NSNumber for 0/1, this creates a runtime misconfiguration risk without error reporting or normalization. [ Low confidence ]
src/index.ts — 0 comments posted, 5 evaluated, 5 filtered
  • line 145: hasPreAuthenticateToInboxCallback is optional and forwarded directly to XMTPModule.create/XMTPModule.createRandom. If the native layer expects a strict boolean, passing undefined can cause runtime errors or unintended behavior. Consider defaulting to false or explicitly mapping undefined to a well-defined value per API contract. [ Invalidated by documentation search ]
  • line 179: chainId and blockNumber are only checked with typeof === 'number', which allows NaN, Infinity, and -Infinity to pass through into signerParams. These values are not valid chain IDs or block numbers and can cause downstream runtime failures in the native XMTPModule.create handler or any numeric validation expecting finite positive integers. Consider validating with Number.isFinite(...) and domain checks (e.g., > 0) before serialization. [ Low confidence ]
  • line 184: Same as above: hasPreAuthenticateToInboxCallback is optional and forwarded directly to XMTPModule.create in the create(...) path. If undefined is passed where a strict boolean is expected on the native side, it can cause runtime errors or misbehavior. Normalize to a boolean or explicitly handle absence. [ Invalidated by documentation search ]
  • line 216: inboxId is optional and passed directly to XMTPModule.build without normalization or guarding. If the native bridge expects a non-null string, passing undefined can lead to runtime exceptions or misinterpretation (e.g., the string 'undefined'). Consider explicitly passing null or omitting the parameter per native contract, or validate and throw/reject early when inboxId is absent. [ Invalidated by documentation search ]
  • line 1777: There is a naming mismatch between option properties in different layers: Client uses options.env, while AuthParams defines environment. This asymmetry can lead to runtime confusion or misconfiguration if a caller constructs options according to AuthParams and expects environment to be used; Client will read env instead, potentially yielding undefined and misconfiguring the environment. This issue is pre-existing and not introduced by the gatewayUrl change, but it presents a runtime contract mismatch risk. [ Out of scope ]
src/lib/Client.ts — 0 comments posted, 4 evaluated, 4 filtered
  • line 111: The call to XMTPModule.createRandom now forwards options.gatewayUrl without any guard or normalization. If options is present but options.gatewayUrl is undefined, null, or an invalid type/value for the native bridge, this can cause a runtime failure inside the module or yield misconfigured client behavior. There is no fallback or default value. Since this is an external-effect call (creating a client), the lack of validation means an invalid or missing gatewayUrl can lead to errors after side-effects begin, with no defined terminal outcome. [ Invalidated by documentation search ]
  • line 241: The call to XMTPModule.build now forwards options.gatewayUrl as a new trailing argument (options.appVersion, options.gatewayUrl). Because gatewayUrl is optional in the options object, it can be undefined at runtime. If XMTPModule.build expects a non-empty string and uses it to construct a URL or perform network configuration, passing undefined is a reachable input that can cause malformed configuration or runtime failures (e.g., URL parsing errors, HTTP client misconfiguration) without any guard or fallback. There is no validation or defaulting of options.gatewayUrl before passing it to the lower-level builder. [ Invalidated by documentation search ]
  • line 241: Adding a new trailing positional argument (options.gatewayUrl) to XMTPModule.build can cause runtime misbinding if the callee relies on argument count to select behavior (e.g., overload-like branching on arguments.length) or expects a specific arity. In JavaScript, extra arguments are generally ignored, but native/bridged modules or code that inspects arguments.length or positional offsets could misinterpret the parameters. Without visible confirmation that XMTPModule.build accepts the new parameter at that position, there is a risk of mismatched contract at runtime. [ Low confidence ]
  • line 288: The call to XMTPModule.ffiCreateClient now includes a new trailing argument options.gatewayUrl. If the native bridge or FFI binding expects a fixed arity and does not tolerate extra arguments, this can cause a runtime error or misinterpretation of parameters on the native side. JavaScript functions ignore extra arguments, but many native module bridges (e.g., React Native/FFI) map all passed arguments by position; an unexpected extra argument can break the call at runtime. [ Low confidence ]

@changeset-bot
Copy link

changeset-bot bot commented Oct 17, 2025

⚠️ No Changeset found

Latest commit: ab41c70

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@claude
Copy link

claude bot commented Oct 17, 2025

Claude finished @cameronvoell's task —— View job


Pull Request Review ✅

Summary

This PR adds support for an optional gatewayUrl parameter in XMTP client creation functions, enabling connection to decentralized backend gateways. The implementation is well-structured and includes comprehensive test coverage, though there are areas that could benefit from minor improvements.

Code Quality ✅

  • Good consistency: The gatewayUrl parameter is properly added across all client creation methods (create, createRandom, build, ffiCreateClient) maintaining API consistency
  • Proper null handling: Both iOS and Android wrappers correctly handle empty strings by converting them to null
  • Backward compatibility: The parameter is optional with proper defaults, ensuring no breaking changes

Potential Issues ⚠️

  1. Missing URL validation: The gatewayUrl parameter accepts any string without validation. Consider adding checks to ensure:

    • Valid URL format
    • HTTPS protocol enforcement for production environments
    • src/lib/Client.ts:111, 201, 241, 288
  2. Test configuration issue: The test includes a hardcoded gateway URL for the staging testnet:

    gatewayUrl: 'https://payer.testnet-staging.xmtp.network:443'

    This should either be extracted to a test configuration file or better documented - example/src/tests/clientTests.ts:329

  3. Missing JSDoc documentation: The gatewayUrl parameter lacks documentation comments in several locations:

    • src/index.ts:132, 165, 202, 232 - Missing parameter documentation
    • The ClientOptions interface properly documents it at src/lib/Client.ts:1218-1221 ✅

Performance Considerations 🚀

  • No performance concerns: The implementation only adds an optional parameter that's passed through to native SDKs
  • Native SDK cache fix (mentioned in commit message) should improve performance

Security Concerns 🔒

  1. URL validation needed: Without proper validation, malformed or insecure URLs could be passed to the native SDKs
  2. Consider enforcing HTTPS: For production environments, enforce HTTPS protocol to prevent potential MITM attacks
  3. Test URL exposure: The staging testnet URL in tests might need rotation if it's not meant to be public

Test Coverage ✅

  • Good coverage: Tests verify the feature works with both createRandom and build methods
  • Integration test: Tests actual connection to a staging gateway
  • Suggestion: Add negative test cases for invalid gateway URLs

Recommendations

  1. Add URL validation utility function to validate gateway URLs before passing to native code
  2. Document the expected format and constraints for the gatewayUrl parameter
  3. Consider adding environment-specific validation (e.g., require HTTPS for production)
  4. Add error handling for gateway connection failures
  5. Consider extracting test gateway URLs to environment configuration

Native SDK Updates

  • iOS: XMTP SDK updated to 4.6.0-dev.4a8ee5d
  • Android: XMTP SDK updated to 4.6.0-dev.c78f91f
  • Both updates appear to be development versions with API client cache fixes

Overall Assessment

The implementation is solid and follows the existing codebase patterns well. The main concerns are around input validation and documentation. With the suggested improvements, this feature would be production-ready.


✅ Review completed

if (jsonOptions.has("dbDirectory")) jsonOptions.get("dbDirectory").asString else null,
if (jsonOptions.has("historySyncUrl")) jsonOptions.get("historySyncUrl").asString else null,
if (jsonOptions.has("customLocalUrl")) jsonOptions.get("customLocalUrl").asString else null,
if (jsonOptions.has("dbDirectory")) stringOrNull(jsonOptions.get("dbDirectory").asString) else null,
Copy link

Choose a reason for hiding this comment

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

jsonOptions.has(key) returns true even if the value is JsonNull. Calling asString on such values throws IllegalStateException. This can crash when dbDirectory, historySyncUrl, customLocalUrl, appVersion, or gatewayUrl are present but explicitly null in the JSON.

Consider guarding these reads with an isJsonNull check (e.g., only call asString when the value exists and is not JsonNull), and otherwise treat them as null so they flow through stringOrNull(...) or default to null.

-                if (jsonOptions.has("dbDirectory")) stringOrNull(jsonOptions.get("dbDirectory").asString) else null,
-                if (jsonOptions.has("historySyncUrl")) stringOrNull(jsonOptions.get("historySyncUrl").asString) else null,
-                if (jsonOptions.has("customLocalUrl")) stringOrNull(jsonOptions.get("customLocalUrl").asString) else null,
+                if (jsonOptions.has("dbDirectory") && !jsonOptions.get("dbDirectory").isJsonNull) stringOrNull(jsonOptions.get("dbDirectory").asString) else null,
+                if (jsonOptions.has("historySyncUrl") && !jsonOptions.get("historySyncUrl").isJsonNull) stringOrNull(jsonOptions.get("historySyncUrl").asString) else null,
+                if (jsonOptions.has("customLocalUrl") && !jsonOptions.get("customLocalUrl").isJsonNull) stringOrNull(jsonOptions.get("customLocalUrl").asString) else null,
                 if (jsonOptions.has("deviceSyncEnabled")) jsonOptions.get("deviceSyncEnabled").asBoolean else true,
                 if (jsonOptions.has("debugEventsEnabled")) jsonOptions.get("debugEventsEnabled").asBoolean else false,
-                if (jsonOptions.has("appVersion")) stringOrNull(jsonOptions.get("appVersion").asString) else null,
-                if (jsonOptions.has("gatewayUrl")) stringOrNull(jsonOptions.get("gatewayUrl").asString) else null,
+                if (jsonOptions.has("appVersion") && !jsonOptions.get("appVersion").isJsonNull) stringOrNull(jsonOptions.get("appVersion").asString) else null,
+                if (jsonOptions.has("gatewayUrl") && !jsonOptions.get("gatewayUrl").isJsonNull) stringOrNull(jsonOptions.get("gatewayUrl").asString) else null,
                 )

🚀 Reply to ask Macroscope to explain or update this suggestion.

👍 Helpful? React to give us feedback.

@cameronvoell cameronvoell force-pushed the d14z-client-prerelease branch from 6d1265e to ab41c70 Compare October 17, 2025 22:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants