Skip to content
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
5bb6618
fix(citizen-sdk): resolve whitelisted root for connected accounts
Ryjen1 Feb 1, 2026
e480b45
docs(citizen-sdk): add connected accounts claiming flow guide
Ryjen1 Feb 1, 2026
e5ac108
Adding a standalone script to verify connected account logic off-chain
Ryjen1 Feb 1, 2026
9bd79df
Adding documentation for the new connected accounts test script
Ryjen1 Feb 1, 2026
c1df3a9
Adding a simple shell script to make running the connected account te…
Ryjen1 Feb 1, 2026
e448c83
Updating test script defaults to use a real verified whitelisted address
Ryjen1 Feb 1, 2026
d7f059e
chore(citizen-sdk): remove hardcoded test addresses in favor of envir…
Ryjen1 Feb 1, 2026
902eddd
chore(citizen-sdk): use zero-address placeholders in test scripts for…
Ryjen1 Feb 1, 2026
ccae930
Address PR review feedback
Ryjen1 Feb 1, 2026
8d179ce
Implement comprehensive error handling and address all review feedback
Ryjen1 Feb 1, 2026
3f0642f
refactor(citizen-sdk): convert connected accounts test to TypeScript …
Ryjen1 Feb 2, 2026
edc8094
chore(citizen-sdk): update test runner and docs for TypeScript migration
Ryjen1 Feb 2, 2026
efd20d6
chore(citizen-sdk): bump version and add tsx devDependency
Ryjen1 Feb 2, 2026
e218df6
Move test files to test directory
Feb 4, 2026
0955bef
Chore: Update yarn.lock and cleanup test script
Feb 4, 2026
a1f19d2
move tests to main test folder
Feb 9, 2026
7e52bd0
add support for connected accounts to claim SDK
Feb 9, 2026
0fe046d
add env file for test accounts
Feb 9, 2026
a927b9a
chore: final cleanup for connected accounts
Feb 9, 2026
547951f
docs: simplify test README
Feb 9, 2026
18bde41
chore: fix pre-existing linting errors and add missing config
Feb 9, 2026
aca253c
Fix lint issues in engagement app hooks
Feb 12, 2026
1cfd097
Refine SDK logic and remove caching
Feb 12, 2026
0344180
Migrate tests to Vitest and cleanup old test suite
Feb 12, 2026
5096bf4
refactor: use identitySDK directly for root address resolution
Feb 16, 2026
f6eb039
build: remove unused tsx dependency and update test script
Feb 16, 2026
7442ed9
test: relocate connected accounts tests to package root
Feb 16, 2026
6132ef6
feat(demo): support connected accounts in IdentityCard via getWhiteli…
Feb 17, 2026
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
55 changes: 50 additions & 5 deletions packages/citizen-sdk/README-ClaimSDK.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,11 @@ The Claim SDK ships with `@goodsdks/citizen-sdk` and builds on the Identity SDK
## Claim Flow at a Glance

1. **Verify identity** – confirm the wallet is whitelisted through the Identity SDK.
2. **Check entitlement** – determine the claimable amount on the active chain and look for fallbacks (Fuse ⇄ Celo ⇄ XDC).
3. **Trigger faucet (optional)** – tops up the claim contract if required.
4. **Submit claim** – send the `claim()` transaction and wait for confirmation.
2. **Resolve whitelisted root** – for connected accounts, resolve the main whitelisted address.
3. **Check entitlement** – determine the claimable amount on the active chain using the whitelisted root address.
4. **Look for fallbacks** – if no entitlement on the current chain, check alternatives (Fuse ⇄ Celo ⇄ XDC).
5. **Trigger faucet (optional)** – tops up the claim contract if required.
6. **Submit claim** – send the `claim()` transaction and wait for confirmation.

```
User connects wallet
Expand All @@ -16,9 +18,11 @@ IdentitySDK.getWhitelistedRoot
Whitelisted? ── no ──▶ Face verification
yes
yes (returns root address)
ClaimSDK uses root address
ClaimSDK.checkEntitlement
ClaimSDK.checkEntitlement(root)
Can claim? ── no ──▶ nextClaimTime timer
Expand Down Expand Up @@ -90,6 +94,46 @@ if (amount > 0n) {
> switch networks and then re-run `ClaimSDK.init` so the SDK binds to that
> environment before calling `claimSDK.claim()` again.

## Connected Accounts

Users can connect secondary wallets to their main whitelisted account. When a connected account attempts to claim:

1. **`getWhitelistedRoot(connectedAddress)`** returns the main whitelisted address
2. **`checkEntitlement`** is automatically called with the whitelisted root address (not the connected wallet)
3. **Claiming proceeds** as if the main account is claiming

This allows users to claim from any connected wallet without re-verification.

### How it Works

The `IdentityV2.getWhitelistedRoot(address)` contract method returns:
- `0x0` = address is neither whitelisted nor connected
- `input address` = address is the main whitelisted account
- `different address` = input is a connected account, returns the main whitelisted address

The ClaimSDK automatically resolves the whitelisted root and uses it for all entitlement checks, making connected accounts work transparently.

### Example

```ts
// User connects with a secondary wallet (Account B)
// Account B is connected to main whitelisted Account A

const identitySDK = await IdentitySDK.init({ publicClient, walletClient, env: "production" })
const claimSDK = await ClaimSDK.init({ publicClient, walletClient, identitySDK, env: "production" })

// Behind the scenes:
// 1. getWhitelistedRoot(Account B) → returns Account A
// 2. checkEntitlement(Account A) → returns entitlement for Account A
// 3. User can claim on behalf of Account A

const { amount } = await claimSDK.checkEntitlement()
if (amount > 0n) {
const receipt = await claimSDK.claim()
console.log("Claimed successfully from connected account!", receipt.transactionHash)
}
```

## Using with React

For Wagmi-based React projects, use the hooks exposed from `@goodsdks/react-hooks`. They wrap these Viem clients with loading/error state and should be the default integration layer for UI code. See `packages/react-hooks/README.md` for full guidance and examples.
Expand Down Expand Up @@ -118,6 +162,7 @@ Refer to the generated TypeScript declarations in `packages/citizen-sdk/dist/` f
## Best Practices

- Check `identitySDK.getWhitelistedRoot` before instantiating the Claim SDK UI.
- **Handle connected accounts**: The SDK automatically resolves the whitelisted root, but you may want to display which account is being claimed for in your UI.
- Surface `altClaimAvailable` hints so users can switch to chains with available allocations.
- Provide loading and error states around every async interaction.
- Log transaction hashes and explorer links after a successful claim for supportability.
Expand Down
5 changes: 3 additions & 2 deletions packages/citizen-sdk/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@goodsdks/citizen-sdk",
"version": "1.2.3",
"version": "1.2.4",
"type": "module",
"scripts": {
"build": "tsup --clean",
Expand All @@ -26,6 +26,7 @@
],
"devDependencies": {
"@repo/typescript-config": "workspace:*",
"tsx": "^4.19.2",
"typescript": "latest",
"viem": "latest",
"wagmi": "latest"
Expand All @@ -42,4 +43,4 @@
"author": "",
"license": "ISC",
"description": ""
}
}
30 changes: 30 additions & 0 deletions packages/citizen-sdk/run-test.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
#!/bin/bash
# Helper script to run connected accounts tests with example addresses
#
# Usage:
# ./run-test.sh
#
# Or with custom addresses:
# MAIN_ACCOUNT=0x... CONNECTED_ACCOUNT=0x... ./run-test.sh

# Example test addresses (replace with real ones)
# These are example addresses - you need to replace them with actual test accounts
MAIN_ACCOUNT=${MAIN_ACCOUNT:-"0x0000000000000000000000000000000000000000"}
CONNECTED_ACCOUNT=${CONNECTED_ACCOUNT:-"0x0000000000000000000000000000000000000000"}
NON_WHITELISTED_ACCOUNT=${NON_WHITELISTED_ACCOUNT:-"0x0000000000000000000000000000000000000000"}
ENV=${ENV:-"development"}

echo "🧪 Running Connected Accounts SDK Test (TypeScript)"
echo ""
echo "Configuration:"
echo " Main Account: $MAIN_ACCOUNT"
echo " Connected Account: $CONNECTED_ACCOUNT"
echo " Non-Whitelisted: $NON_WHITELISTED_ACCOUNT"
echo " Environment: $ENV"
echo ""

MAIN_ACCOUNT=$MAIN_ACCOUNT \
CONNECTED_ACCOUNT=$CONNECTED_ACCOUNT \
NON_WHITELISTED_ACCOUNT=$NON_WHITELISTED_ACCOUNT \
ENV=$ENV \
npx tsx test-connected-accounts.ts
52 changes: 51 additions & 1 deletion packages/citizen-sdk/src/sdks/viem-claim-sdk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ export class ClaimSDK {
private readonly account: Address
private readonly env: contractEnv
public readonly rdu: string
private whitelistedRootCache: Address | null = null

constructor({
account,
Expand Down Expand Up @@ -164,6 +165,51 @@ export class ClaimSDK {
return this.chainId
}

/**
* Resolves the whitelisted root address for the connected account.
* This enables connected accounts to claim on behalf of their main whitelisted account.
*
* Failure modes are normalized so callers see predictable behavior:
* - Throws when no whitelisted root exists for the connected account.
* - Throws when the SDK cannot resolve a whitelisted root (network / domain errors).
*
* @returns The whitelisted root address to use for entitlement checks.
* @throws Error if no whitelisted root exists or resolution fails for any reason.
*/
private async getWhitelistedRootAddress(): Promise<Address> {
// Return cached value if available
if (this.whitelistedRootCache) {
return this.whitelistedRootCache
}

try {
// Resolve the whitelisted root for this account
const { root } = await this.identitySDK.getWhitelistedRoot(this.account)

// Normalize "no root" / "not whitelisted" cases
if (!root) {
throw new Error(
"No whitelisted root address found for connected account; the user may not be whitelisted.",
)
}

// Cache the result
this.whitelistedRootCache = root

return root
} catch (error) {
// Normalize SDK and transport errors into a predictable failure mode
const message =
error instanceof Error && error.message
? error.message
: String(error)

throw new Error(
`Unable to resolve whitelisted root address for connected account: ${message}`,
)
}
}

private async readChainEntitlement(
chainId: SupportedChains,
client?: PublicClient,
Expand All @@ -184,12 +230,16 @@ export class ClaimSDK {
altClient = resolvedClient
}

// Use whitelisted root address for entitlement check
// This enables connected accounts to claim on behalf of their main account
const rootAddress = await this.getWhitelistedRootAddress()

return this.readContract<bigint>(
{
address: contracts.ubiContract as Address,
abi: ubiSchemeV2ABI,
functionName: "checkEntitlement",
args: [this.account],
args: [rootAddress],
},
altClient,
chainId,
Expand Down
99 changes: 99 additions & 0 deletions packages/citizen-sdk/test-connected-accounts.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
# Connected Accounts Test Script

This directory contains a standalone test script to verify the connected accounts claiming flow works correctly.

## Usage

### Basic Usage

```bash
npx tsx test-connected-accounts.ts
```

### With Environment Variables

```bash
MAIN_ACCOUNT=0x1234... \
CONNECTED_ACCOUNT=0x5678... \
NON_WHITELISTED_ACCOUNT=0x9abc... \
ENV=development \
npx tsx test-connected-accounts.ts
```

## Environment Variables

| Variable | Description | Required | Default |
|----------|-------------|----------|---------|
| `MAIN_ACCOUNT` | Main whitelisted account address | No | 0x0...0 |
| `CONNECTED_ACCOUNT` | Account connected to main account | No | 0x0...0 |
| `NON_WHITELISTED_ACCOUNT` | Non-whitelisted account | No | 0x0...0 |
| `ENV` | Environment (development/staging/production) | No | development |
| `RPC_URL` | Custom RPC URL | No | https://forno.celo.org |

## What It Tests

1. **Main Account Resolution** - Verifies main whitelisted account returns itself as root
2. **Connected Account Resolution** - Verifies connected account returns main account as root
3. **Non-Whitelisted Account** - Verifies non-whitelisted account returns 0x0
4. **Main Account Entitlement** - Verifies entitlement check works for main account
5. **Connected Account Entitlement** - Verifies entitlement check uses root address (simulating SDK behavior)

## Example Output

```
🧪 Testing Connected Accounts Claiming Flow

Environment: development
RPC: https://forno.celo.org
Identity Contract: 0xF25fA0D4896271228193E782831F6f3CFCcF169C
UBI Contract: 0x6B86F82293552C3B9FE380FC038A89e0328C7C5f

Test 1: Main whitelisted account
✓ Main account is whitelisted
Root: 0x1234...

Test 2: Connected account resolution
✓ Connected account resolves to main
Root: 0x1234...

Test 3: Non-whitelisted account
✓ Non-whitelisted account returns 0x0
Root: 0x0000000000000000000000000000000000000000

Test 4: Entitlement check (main account)
✓ Main account entitlement retrieved
Entitlement: 1000000000000000000

Test 5: Entitlement check (connected account → root)
✓ Connected account entitlement via root
Root: 0x1234..., Entitlement: 1000000000000000000

==================================================

Test Results: 5/5 passed

✅ All tests passed! Connected accounts flow is working correctly.
```

## Integration with CI/CD

Add to your CI pipeline:

```yaml
# .github/workflows/test.yml
- name: Test Connected Accounts
run: |
cd packages/citizen-sdk
MAIN_ACCOUNT=${{ secrets.TEST_MAIN_ACCOUNT }} \
CONNECTED_ACCOUNT=${{ secrets.TEST_CONNECTED_ACCOUNT }} \
NON_WHITELISTED_ACCOUNT=${{ secrets.TEST_NON_WHITELISTED }} \
ENV=development \
node test-connected-accounts.js
```

## Notes

- This script only tests contract interactions, not the full SDK
- Requires valid test accounts to be set via environment variables
- Uses read-only contract calls (no transactions)
- No private key needed (read-only operations)
Loading