feat: new @goodsdks/invite-sdk — InvitesV2 viem SDK + React/Wagmi hooks#44
feat: new @goodsdks/invite-sdk — InvitesV2 viem SDK + React/Wagmi hooks#44
@goodsdks/invite-sdk — InvitesV2 viem SDK + React/Wagmi hooks#44Conversation
Agent-Logs-Url: https://github.com/GoodDollar/GoodSDKs/sessions/2d043ce3-d453-4e3a-88a5-3b5ccd2e1842 Co-authored-by: L03TJ3 <6606028+L03TJ3@users.noreply.github.com>
…stant Agent-Logs-Url: https://github.com/GoodDollar/GoodSDKs/sessions/2d043ce3-d453-4e3a-88a5-3b5ccd2e1842 Co-authored-by: L03TJ3 <6606028+L03TJ3@users.noreply.github.com>
@goodsdks/invite-sdk — InvitesV2 viem SDK + React/Wagmi hooks
There was a problem hiding this comment.
Hey - I've found 3 issues, and left some high level feedback:
- In
formatBounty,const divisor = BigInt(10 ** decimals)will produce an imprecise divisor for larger decimals (e.g. 18) because10 ** decimalsis computed as a JSnumberfirst; consider usingconst divisor = 10n ** BigInt(decimals)to avoid precision issues. - In
useInviteStatus, when!sdk || !targetyou only clearuserbut leaveeligible,pendingBounties,pendingInvitees, andreverificationDueunchanged, which can leak stale state between account changes; reset these fields in that branch to keep the hook output consistent with the current target.
Prompt for AI Agents
Please address the comments from this code review:
## Overall Comments
- In `formatBounty`, `const divisor = BigInt(10 ** decimals)` will produce an imprecise divisor for larger decimals (e.g. 18) because `10 ** decimals` is computed as a JS `number` first; consider using `const divisor = 10n ** BigInt(decimals)` to avoid precision issues.
- In `useInviteStatus`, when `!sdk || !target` you only clear `user` but leave `eligible`, `pendingBounties`, `pendingInvitees`, and `reverificationDue` unchanged, which can leak stale state between account changes; reset these fields in that branch to keep the hook output consistent with the current target.
## Individual Comments
### Comment 1
<location path="packages/react-hooks/src/invite-sdk/wagmi-invite-hooks.ts" line_range="31-36" />
<code_context>
+ const [error, setError] = useState<string | null>(null)
+
+ useEffect(() => {
+ if (!publicClient || !walletClient) {
+ setSdk(null)
+ setError("Wallet or Public client not initialized")
+ return
+ }
</code_context>
<issue_to_address>
**suggestion:** Treat missing wallet/public client as a neutral state instead of an error string.
During initial wagmi setup or when the user isn’t connected, `publicClient`/`walletClient` can legitimately be undefined. Treating this as an `error` blurs the line between normal "not connected" and real failures (e.g. misconfiguration). Consider leaving `error` as `null` in this case and letting consumers infer state from `sdk === null` plus the account connection status instead.
```suggestion
useEffect(() => {
if (!publicClient || !walletClient) {
setSdk(null)
return
}
```
</issue_to_address>
### Comment 2
<location path="packages/invite-sdk/src/sdks/viem-invite-sdk.ts" line_range="189" />
<code_context>
+
+ // ─── Private helpers ───────────────────────────────────────────────────────
+
+ private async read<T = unknown>(
+ functionName: string,
+ args: unknown[] = [],
</code_context>
<issue_to_address>
**issue (complexity):** Consider introducing typed helper methods, shared pre-check utilities, and a small whitelist-status helper, plus a purely-BigInt divisor, to simplify the SDK’s control flow and improve type safety without changing behavior.
You can reduce the complexity without changing behavior by tightening a few patterns.
---
### 1. Avoid stringly‑typed `read` and ad‑hoc casting
The generic `read(functionName: string, args: unknown[])` forces string names and manual casting at every call site, which increases mental overhead and weakens type safety.
You can keep the helper but make it typed per function, or define small wrappers. For example:
```ts
// Narrow the helper
private readActive() {
return this.publicClient.readContract({
address: this.contractAddress,
abi: invitesV2ABI,
functionName: "active",
}) as Promise<boolean>
}
private readUser(address: Address) {
return this.publicClient.readContract({
address: this.contractAddress,
abi: invitesV2ABI,
functionName: "users",
args: [address],
}) as Promise<
readonly [Address, `0x${string}`, boolean, bigint, bigint, bigint, bigint, bigint, bigint]
>
}
```
Then the call sites become simpler and typed:
```ts
async getActive(): Promise<boolean> {
return this.readActive()
}
async getUser(address: Address): Promise<InviteUser> {
const result = await this.readUser(address)
// mapping unchanged...
}
```
You can incrementally replace the most frequently used `read(...)` calls with typed helpers like `readActive`, `readMinimums`, `readUser`, `readLevel`.
---
### 2. Factor out repeated “require active” / eligibility pre‑checks
`join`, `collectBounty`, and `collectAllBounties` all repeat `active` checks (and `canCollectBountyFor` in one place). A small shared helper keeps behavior but removes duplication:
```ts
private async ensureActive() {
const isActive = await this.read<boolean>("active")
if (!isActive) {
throw new InviteSDKError("contract is not active", "NOT_ACTIVE")
}
}
```
Then:
```ts
async join(myCode: `0x${string}`, inviterCode: `0x${string}`): Promise<`0x${string}`> {
await this.ensureActive()
const [existingOwner, callerUser] = await Promise.all([
this.read<Address>("codeToUser", [myCode]),
this.getUser(this.account),
])
// rest unchanged...
}
async collectAllBounties(): Promise<BountyResult[]> {
await this.ensureActive()
try {
const receipt = await this.submitAndWait("collectBounties")
// mapping unchanged...
} catch (err) {
throw mapContractError(err)
}
}
```
Similarly, you can extract the “ensure eligible or throw detailed InviteSDKError” logic from `collectBounty` into a reusable helper if you plan to have more bounty-related methods.
---
### 3. Simplify `checkEligibilityDetails` identity logic
The identity/whitelist orchestration is doing multiple reads and local mutations. Extracting the identity‑related part into its own helper makes the flow easier to follow without changing semantics:
```ts
private async getWhitelistStatus(
identityAddress: Address,
invitee: Address,
inviter: Address,
): Promise<{
inviteeWhitelisted: boolean
inviterWhitelisted: boolean | null
reverificationDue: boolean
}> {
if (identityAddress === zeroAddress) {
return { inviteeWhitelisted: false, inviterWhitelisted: null, reverificationDue: false }
}
let inviteeWhitelisted = false
let inviterWhitelisted: boolean | null = null
try {
inviteeWhitelisted = await this.publicClient.readContract({
address: identityAddress,
abi: identityABI,
functionName: "isWhitelisted",
args: [invitee],
}) as boolean
} catch {
// treat as not whitelisted
}
if (inviter !== zeroAddress) {
try {
inviterWhitelisted = await this.publicClient.readContract({
address: identityAddress,
abi: identityABI,
functionName: "isWhitelisted",
args: [inviter],
}) as boolean
} catch {
inviterWhitelisted = null
}
}
const reverificationDue =
!inviteeWhitelisted ||
(inviterWhitelisted !== null && !inviterWhitelisted)
return { inviteeWhitelisted, inviterWhitelisted, reverificationDue }
}
```
Then `checkEligibilityDetails` becomes:
```ts
async checkEligibilityDetails(invitee: Address) {
const identityAddress = await this.read<Address>("getIdentity")
const [isActive, eligible, minimumClaims, minimumDays, user] = await Promise.all([
this.read<boolean>("active"),
this.read<boolean>("canCollectBountyFor", [invitee]),
this.read<number>("minimumClaims"),
this.read<number>("minimumDays"),
this.getUser(invitee),
])
const { inviteeWhitelisted, inviterWhitelisted, reverificationDue } =
await this.getWhitelistStatus(identityAddress, invitee, user.invitedBy)
return {
eligible,
details: {
isActive,
inviteeWhitelisted,
inviterWhitelisted,
minimumClaims,
minimumDays,
reverificationDue,
},
}
}
```
This keeps all checks and edge cases but reduces branching and mutable locals in the main method.
---
### 4. Compute `formatBounty` divisor purely with `BigInt`
To avoid mixing `number` and `BigInt` (and avoid implicit precision pitfalls), you can keep the behavior but compute the divisor in `BigInt` only:
```ts
export function formatBounty(cents: bigint, chainId: SupportedChains): string {
const decimals = CHAIN_DECIMALS[chainId] ?? 2
let divisor = 1n
for (let i = 0; i < decimals; i++) {
divisor *= 10n
}
const whole = cents / divisor
const remainder = cents % divisor
const fractionStr = remainder.toString().padStart(decimals, "0")
const trimmed = fractionStr.replace(/0+$/, "").padEnd(2, "0")
return `${whole}.${trimmed}`
}
```
This keeps outputs identical while making the implementation more obviously correct for high‑decimals chains.
</issue_to_address>
### Comment 3
<location path="packages/react-hooks/src/invite-sdk/wagmi-invite-hooks.ts" line_range="151" />
<code_context>
+ * Pre-checks (contract must be active, code must be free, user must not have joined)
+ * are run before simulation to surface errors before signing.
+ */
+export const useJoin = (env: contractEnv = "production"): UseJoinResult => {
+ const { sdk } = useInviteSDK(env)
+
</code_context>
<issue_to_address>
**issue (complexity):** Consider extracting the shared async/error-handling logic into a reusable helper and simplifying the status hook state to remove duplication and opaque refetching.
You can keep all behaviour but trim a lot of boilerplate by extracting the repeated async pattern and error formatting, and slightly tightening `useInviteStatus`.
### 1) Centralise error formatting + async action pattern
The three action hooks are almost identical structurally. You can pull that into a small helper while preserving the current public API:
```ts
const formatInviteError = (err: unknown): string => {
if (err instanceof InviteSDKError) return `[${err.errorCode}] ${err.message}`
if (err instanceof Error) return err.message
return String(err)
}
interface InviteActionState<T> {
loading: boolean
error: string | null
result: T | null
}
const useInviteAction = <Args extends unknown[], Result>(
sdk: InviteSDK | null,
fn: (sdk: InviteSDK, ...args: Args) => Promise<Result>,
) => {
const [state, setState] = useState<InviteActionState<Result>>({
loading: false,
error: null,
result: null,
})
const run = useCallback(
async (...args: Args): Promise<Result> => {
if (!sdk) throw new Error("InviteSDK not initialized")
setState({ loading: true, error: null, result: null })
try {
const result = await fn(sdk, ...args)
setState({ loading: false, error: null, result })
return result
} catch (err) {
const msg = formatInviteError(err)
setState({ loading: false, error: msg, result: null })
throw err
}
},
[sdk, fn],
)
return { ...state, run }
}
```
Then each hook becomes much simpler without changing its outward behaviour:
```ts
export const useJoin = (env: contractEnv = "production"): UseJoinResult => {
const { sdk } = useInviteSDK(env)
const { run, loading, error, result } = useInviteAction<
[`0x${string}`, `0x${string}`],
`0x${string}`
>(sdk, (s, myCode, inviterCode) => s.join(myCode, inviterCode))
return {
join: run,
loading,
error,
txHash: result,
}
}
```
```ts
export const useCollectBounty = (
env: contractEnv = "production",
): UseCollectBountyResult => {
const { sdk } = useInviteSDK(env)
const { run, loading, error, result } = useInviteAction<[Address], BountyResult>(
sdk,
(s, invitee) => s.collectBounty(invitee),
)
return {
collectBounty: run,
loading,
error,
result,
}
}
```
```ts
export const useCollectAllBounties = (
env: contractEnv = "production",
): UseCollectAllBountiesResult => {
const { sdk } = useInviteSDK(env)
const { run, loading, error, result } = useInviteAction<[], BountyResult[]>(
sdk,
(s) => s.collectAllBounties(),
)
return {
collectAllBounties: run,
loading,
error,
results: result ?? [],
}
}
```
This removes the triplicated `try/catch/finally` blocks and guarantees consistent `InviteSDKError` formatting across all action hooks.
### 2) Simplify `useInviteStatus` state + refetch pattern
You can group the status pieces into a single object and replace the `tick` counter with a `fetchStatus` callback that is reused for both `useEffect` and `refetch`:
```ts
interface InviteStatusInternal {
user: InviteUser | null
eligible: boolean
pendingBounties: bigint
pendingInvitees: Address[]
reverificationDue: boolean
}
export const useInviteStatus = (invitee?: Address): InviteStatus => {
const { address: connectedAddress } = useAccount()
const { sdk, loading: sdkLoading, error: sdkError } = useInviteSDK()
const target = invitee ?? connectedAddress
const [status, setStatus] = useState<InviteStatusInternal>({
user: null,
eligible: false,
pendingBounties: 0n,
pendingInvitees: [],
reverificationDue: false,
})
const [loading, setLoading] = useState(false)
const [error, setError] = useState<string | null>(null)
const fetchStatus = useCallback(async () => {
if (!sdk || !target) {
setStatus((prev) => ({ ...prev, user: null }))
return
}
setLoading(true)
setError(null)
try {
const [u, eligibilityResult, pending, pendingInvs] = await Promise.all([
sdk.getUser(target),
sdk.checkEligibilityDetails(target),
sdk.getPendingBounties(target),
sdk.getPendingInvitees(target),
])
setStatus({
user: u,
eligible: eligibilityResult.eligible,
reverificationDue: eligibilityResult.details.reverificationDue,
pendingBounties: pending,
pendingInvitees: pendingInvs,
})
} catch (err) {
setError(formatInviteError(err)) // reuse same helper as above
} finally {
setLoading(false)
}
}, [sdk, target])
useEffect(() => {
void fetchStatus()
}, [fetchStatus])
return {
...status,
loading: sdkLoading || loading,
error: sdkError ?? error,
refetch: fetchStatus,
}
}
```
This keeps the exact behaviour but reduces the number of individual `useState` calls and removes the opaque `tick` refetch mechanism, making the flow easier to follow.
</issue_to_address>Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
| useEffect(() => { | ||
| if (!publicClient || !walletClient) { | ||
| setSdk(null) | ||
| setError("Wallet or Public client not initialized") | ||
| return | ||
| } |
There was a problem hiding this comment.
suggestion: Treat missing wallet/public client as a neutral state instead of an error string.
During initial wagmi setup or when the user isn’t connected, publicClient/walletClient can legitimately be undefined. Treating this as an error blurs the line between normal "not connected" and real failures (e.g. misconfiguration). Consider leaving error as null in this case and letting consumers infer state from sdk === null plus the account connection status instead.
| useEffect(() => { | |
| if (!publicClient || !walletClient) { | |
| setSdk(null) | |
| setError("Wallet or Public client not initialized") | |
| return | |
| } | |
| useEffect(() => { | |
| if (!publicClient || !walletClient) { | |
| setSdk(null) | |
| return | |
| } |
|
|
||
| // ─── Private helpers ─────────────────────────────────────────────────────── | ||
|
|
||
| private async read<T = unknown>( |
There was a problem hiding this comment.
issue (complexity): Consider introducing typed helper methods, shared pre-check utilities, and a small whitelist-status helper, plus a purely-BigInt divisor, to simplify the SDK’s control flow and improve type safety without changing behavior.
You can reduce the complexity without changing behavior by tightening a few patterns.
1. Avoid stringly‑typed read and ad‑hoc casting
The generic read(functionName: string, args: unknown[]) forces string names and manual casting at every call site, which increases mental overhead and weakens type safety.
You can keep the helper but make it typed per function, or define small wrappers. For example:
// Narrow the helper
private readActive() {
return this.publicClient.readContract({
address: this.contractAddress,
abi: invitesV2ABI,
functionName: "active",
}) as Promise<boolean>
}
private readUser(address: Address) {
return this.publicClient.readContract({
address: this.contractAddress,
abi: invitesV2ABI,
functionName: "users",
args: [address],
}) as Promise<
readonly [Address, `0x${string}`, boolean, bigint, bigint, bigint, bigint, bigint, bigint]
>
}Then the call sites become simpler and typed:
async getActive(): Promise<boolean> {
return this.readActive()
}
async getUser(address: Address): Promise<InviteUser> {
const result = await this.readUser(address)
// mapping unchanged...
}You can incrementally replace the most frequently used read(...) calls with typed helpers like readActive, readMinimums, readUser, readLevel.
2. Factor out repeated “require active” / eligibility pre‑checks
join, collectBounty, and collectAllBounties all repeat active checks (and canCollectBountyFor in one place). A small shared helper keeps behavior but removes duplication:
private async ensureActive() {
const isActive = await this.read<boolean>("active")
if (!isActive) {
throw new InviteSDKError("contract is not active", "NOT_ACTIVE")
}
}Then:
async join(myCode: `0x${string}`, inviterCode: `0x${string}`): Promise<`0x${string}`> {
await this.ensureActive()
const [existingOwner, callerUser] = await Promise.all([
this.read<Address>("codeToUser", [myCode]),
this.getUser(this.account),
])
// rest unchanged...
}
async collectAllBounties(): Promise<BountyResult[]> {
await this.ensureActive()
try {
const receipt = await this.submitAndWait("collectBounties")
// mapping unchanged...
} catch (err) {
throw mapContractError(err)
}
}Similarly, you can extract the “ensure eligible or throw detailed InviteSDKError” logic from collectBounty into a reusable helper if you plan to have more bounty-related methods.
3. Simplify checkEligibilityDetails identity logic
The identity/whitelist orchestration is doing multiple reads and local mutations. Extracting the identity‑related part into its own helper makes the flow easier to follow without changing semantics:
private async getWhitelistStatus(
identityAddress: Address,
invitee: Address,
inviter: Address,
): Promise<{
inviteeWhitelisted: boolean
inviterWhitelisted: boolean | null
reverificationDue: boolean
}> {
if (identityAddress === zeroAddress) {
return { inviteeWhitelisted: false, inviterWhitelisted: null, reverificationDue: false }
}
let inviteeWhitelisted = false
let inviterWhitelisted: boolean | null = null
try {
inviteeWhitelisted = await this.publicClient.readContract({
address: identityAddress,
abi: identityABI,
functionName: "isWhitelisted",
args: [invitee],
}) as boolean
} catch {
// treat as not whitelisted
}
if (inviter !== zeroAddress) {
try {
inviterWhitelisted = await this.publicClient.readContract({
address: identityAddress,
abi: identityABI,
functionName: "isWhitelisted",
args: [inviter],
}) as boolean
} catch {
inviterWhitelisted = null
}
}
const reverificationDue =
!inviteeWhitelisted ||
(inviterWhitelisted !== null && !inviterWhitelisted)
return { inviteeWhitelisted, inviterWhitelisted, reverificationDue }
}Then checkEligibilityDetails becomes:
async checkEligibilityDetails(invitee: Address) {
const identityAddress = await this.read<Address>("getIdentity")
const [isActive, eligible, minimumClaims, minimumDays, user] = await Promise.all([
this.read<boolean>("active"),
this.read<boolean>("canCollectBountyFor", [invitee]),
this.read<number>("minimumClaims"),
this.read<number>("minimumDays"),
this.getUser(invitee),
])
const { inviteeWhitelisted, inviterWhitelisted, reverificationDue } =
await this.getWhitelistStatus(identityAddress, invitee, user.invitedBy)
return {
eligible,
details: {
isActive,
inviteeWhitelisted,
inviterWhitelisted,
minimumClaims,
minimumDays,
reverificationDue,
},
}
}This keeps all checks and edge cases but reduces branching and mutable locals in the main method.
4. Compute formatBounty divisor purely with BigInt
To avoid mixing number and BigInt (and avoid implicit precision pitfalls), you can keep the behavior but compute the divisor in BigInt only:
export function formatBounty(cents: bigint, chainId: SupportedChains): string {
const decimals = CHAIN_DECIMALS[chainId] ?? 2
let divisor = 1n
for (let i = 0; i < decimals; i++) {
divisor *= 10n
}
const whole = cents / divisor
const remainder = cents % divisor
const fractionStr = remainder.toString().padStart(decimals, "0")
const trimmed = fractionStr.replace(/0+$/, "").padEnd(2, "0")
return `${whole}.${trimmed}`
}This keeps outputs identical while making the implementation more obviously correct for high‑decimals chains.
| * Pre-checks (contract must be active, code must be free, user must not have joined) | ||
| * are run before simulation to surface errors before signing. | ||
| */ | ||
| export const useJoin = (env: contractEnv = "production"): UseJoinResult => { |
There was a problem hiding this comment.
issue (complexity): Consider extracting the shared async/error-handling logic into a reusable helper and simplifying the status hook state to remove duplication and opaque refetching.
You can keep all behaviour but trim a lot of boilerplate by extracting the repeated async pattern and error formatting, and slightly tightening useInviteStatus.
1) Centralise error formatting + async action pattern
The three action hooks are almost identical structurally. You can pull that into a small helper while preserving the current public API:
const formatInviteError = (err: unknown): string => {
if (err instanceof InviteSDKError) return `[${err.errorCode}] ${err.message}`
if (err instanceof Error) return err.message
return String(err)
}
interface InviteActionState<T> {
loading: boolean
error: string | null
result: T | null
}
const useInviteAction = <Args extends unknown[], Result>(
sdk: InviteSDK | null,
fn: (sdk: InviteSDK, ...args: Args) => Promise<Result>,
) => {
const [state, setState] = useState<InviteActionState<Result>>({
loading: false,
error: null,
result: null,
})
const run = useCallback(
async (...args: Args): Promise<Result> => {
if (!sdk) throw new Error("InviteSDK not initialized")
setState({ loading: true, error: null, result: null })
try {
const result = await fn(sdk, ...args)
setState({ loading: false, error: null, result })
return result
} catch (err) {
const msg = formatInviteError(err)
setState({ loading: false, error: msg, result: null })
throw err
}
},
[sdk, fn],
)
return { ...state, run }
}Then each hook becomes much simpler without changing its outward behaviour:
export const useJoin = (env: contractEnv = "production"): UseJoinResult => {
const { sdk } = useInviteSDK(env)
const { run, loading, error, result } = useInviteAction<
[`0x${string}`, `0x${string}`],
`0x${string}`
>(sdk, (s, myCode, inviterCode) => s.join(myCode, inviterCode))
return {
join: run,
loading,
error,
txHash: result,
}
}export const useCollectBounty = (
env: contractEnv = "production",
): UseCollectBountyResult => {
const { sdk } = useInviteSDK(env)
const { run, loading, error, result } = useInviteAction<[Address], BountyResult>(
sdk,
(s, invitee) => s.collectBounty(invitee),
)
return {
collectBounty: run,
loading,
error,
result,
}
}export const useCollectAllBounties = (
env: contractEnv = "production",
): UseCollectAllBountiesResult => {
const { sdk } = useInviteSDK(env)
const { run, loading, error, result } = useInviteAction<[], BountyResult[]>(
sdk,
(s) => s.collectAllBounties(),
)
return {
collectAllBounties: run,
loading,
error,
results: result ?? [],
}
}This removes the triplicated try/catch/finally blocks and guarantees consistent InviteSDKError formatting across all action hooks.
2) Simplify useInviteStatus state + refetch pattern
You can group the status pieces into a single object and replace the tick counter with a fetchStatus callback that is reused for both useEffect and refetch:
interface InviteStatusInternal {
user: InviteUser | null
eligible: boolean
pendingBounties: bigint
pendingInvitees: Address[]
reverificationDue: boolean
}
export const useInviteStatus = (invitee?: Address): InviteStatus => {
const { address: connectedAddress } = useAccount()
const { sdk, loading: sdkLoading, error: sdkError } = useInviteSDK()
const target = invitee ?? connectedAddress
const [status, setStatus] = useState<InviteStatusInternal>({
user: null,
eligible: false,
pendingBounties: 0n,
pendingInvitees: [],
reverificationDue: false,
})
const [loading, setLoading] = useState(false)
const [error, setError] = useState<string | null>(null)
const fetchStatus = useCallback(async () => {
if (!sdk || !target) {
setStatus((prev) => ({ ...prev, user: null }))
return
}
setLoading(true)
setError(null)
try {
const [u, eligibilityResult, pending, pendingInvs] = await Promise.all([
sdk.getUser(target),
sdk.checkEligibilityDetails(target),
sdk.getPendingBounties(target),
sdk.getPendingInvitees(target),
])
setStatus({
user: u,
eligible: eligibilityResult.eligible,
reverificationDue: eligibilityResult.details.reverificationDue,
pendingBounties: pending,
pendingInvitees: pendingInvs,
})
} catch (err) {
setError(formatInviteError(err)) // reuse same helper as above
} finally {
setLoading(false)
}
}, [sdk, target])
useEffect(() => {
void fetchStatus()
}, [fetchStatus])
return {
...status,
loading: sdkLoading || loading,
error: sdkError ?? error,
refetch: fetchStatus,
}
}This keeps the exact behaviour but reduces the number of individual useState calls and removes the opaque tick refetch mechanism, making the flow easier to follow.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 6c3c01d87b
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| */ | ||
| export const useInviteStatus = (invitee?: Address): InviteStatus => { | ||
| const { address: connectedAddress } = useAccount() | ||
| const { sdk, loading: sdkLoading, error: sdkError } = useInviteSDK() |
There was a problem hiding this comment.
Thread env into useInviteStatus SDK initialization
useInviteStatus always builds its SDK with useInviteSDK() defaults, so it silently reads production contracts even when the app is configured for staging/development. In non-production deployments this can return wrong status data (or throw on env/chain combinations missing production addresses), while useJoin/useCollect* in the same module may be pointed at a different env, creating inconsistent behavior in one screen.
Useful? React with 👍 / 👎.
| reverificationDue = | ||
| !inviteeWhitelisted || | ||
| (inviterWhitelisted !== null && !inviterWhitelisted) |
There was a problem hiding this comment.
Compute reverificationDue only from historical identity state
This marks reverificationDue as true whenever inviteeWhitelisted is false, but the contract/docs distinction is “previously authenticated and now lapsed” vs “never whitelisted”. As written, first-time unverified users (and also transient identity read failures that default to false) are misclassified as reverification cases, so downstream UIs using this flag will show the wrong remediation path.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
reverificationDue should only be true when the user was previously verified and is now no longer whitelisted.
You can check lastAuthenticated(user) for prior verification evidence
| if (!sdk || !target) { | ||
| setUser(null) | ||
| return |
There was a problem hiding this comment.
Clear all status fields when invite target becomes unavailable
When sdk or target becomes falsy (disconnect/account switch), the effect only clears user and returns, leaving eligible, pendingBounties, pendingInvitees, and reverificationDue from the previous account in state. That leaks stale invite status into the next render and can show incorrect bounty/eligibility info until another successful fetch happens.
Useful? React with 👍 / 👎.
blueogin
left a comment
There was a problem hiding this comment.
no tests added for new invite SDK
| reverificationDue = | ||
| !inviteeWhitelisted || | ||
| (inviterWhitelisted !== null && !inviterWhitelisted) |
There was a problem hiding this comment.
reverificationDue should only be true when the user was previously verified and is now no longer whitelisted.
You can check lastAuthenticated(user) for prior verification evidence
Adds a new workspace package
@goodsdks/invite-sdkwrapping theInvitesV2contract (v2.4 UUPS) into a typed viem SDK, following the same class-based conventions ascitizen-sdk. Companion hooks are added toreact-hooks.packages/invite-sdksrc/abi.ts—invitesV2ABIviaparseAbi: view/write functions,InviteeJoined+InviterBountyevents, all five custom errorssrc/constants.ts—INVITES_V2_ADDRESSESfor Fuse + Celo across production/staging/development (+ XDC development), sourced fromdeployment.jsonsrc/types.ts—InviteUser,InviteLevel,InviteStats,BountyResult,BountyEligibilityDetails, typedInviteSDKErrorwitherrorCodesrc/sdks/viem-invite-sdk.ts—InviteSDKclass:getUser,getLevel,getStats,getInvitees,getPendingInvitees,getPendingBounties,canCollectBounty,resolveCode, …)join,collectBounty,collectAllBountiescheckEligibilityDetails— calls IdentityisWhitelistedon both invitee and inviter to surface the exact blocker (includingreverificationDueflag)formatBounty(cents, chainId)— normalises G¢ → G$ usingCHAIN_DECIMALSfromcitizen-sdkpackages/react-hooks/src/invite-sdkuseInviteSDK(env)InviteSDKfor the connected walletuseInviteStatus(invitee?)reverificationDueuseJoin(env)join(myCode, inviterCode)action + loading/error/txHashuseCollectBounty(env)collectBounty(invitee)action +BountyResultstateuseCollectAllBounties(env)BountyResult[]stateHooks re-export
InviteSDKErrorso callers can branch onerr.errorCode(NOT_ACTIVE,NOT_ELIGIBLE_BOUNTY, etc.).react-hooks/package.jsongains@goodsdks/invite-sdk: workspace:*; no circular dependency —invite-sdkdepends oncitizen-sdk, never the reverse.Warning
Firewall rules blocked me from connecting to one or more addresses (expand for details)
I tried to connect to the following addresses, but was blocked by firewall rules:
binaries.soliditylang.org/usr/local/bin/node /usr/local/bin/node /home/REDACTED/work/GoodSDKs/GoodSDKs/packages/engagement-contracts/node_modules/hardhat/internal/cli/bootstrap.js compile(dns block)If you need me to access, download, or install something from one of these locations, you can either:
Summary by Sourcery
Add a new InviteSDK workspace package for the InvitesV2 contract and expose React/Wagmi hooks for invite status and bounty flows.
New Features:
Enhancements:
Build: