# Developer Guide

### Getting Started

#### Installation and Setup

Install the required packages for Self Connect integration:

```bash
npm install @celo/identity @celo/abis viem
```

#### Required Packages

| Package          | Purpose                                             |
| ---------------- | --------------------------------------------------- |
| `@celo/identity` | ODIS integration and identifier utilities           |
| `@celo/abis`     | Contract ABIs for FederatedAttestations             |
| `viem`           | Modern Ethereum library for blockchain interactions |

#### Why Viem?

Self Connect uses **Viem** as the recommended library for blockchain interactions because:

* **Modern & Type-Safe**: Built with TypeScript for excellent type inference
* **Lightweight**: Smaller bundle size compared to legacy libraries
* **Modular**: Import only what you need
* **Better DX**: Cleaner API and better error messages
* **Active Maintenance**: Well-maintained with regular updates
* **EIP-1193 Support**: Native support for wallet providers

> **Note:** ContractKit is deprecated and no longer recommended for new projects.

#### Network Configuration

**Mainnet:**

```typescript
import { celo } from "viem/chains";

const RPC_URL = "https://forno.celo.org";
const ODIS_CONTEXT = OdisContextName.MAINNET;
```

**Alfajores Testnet:**

```typescript
import { celoAlfajores } from "viem/chains";

const RPC_URL = "https://alfajores-forno.celo-testnet.org";
const ODIS_CONTEXT = OdisContextName.ALFAJORES;
```

### Becoming an Issuer

#### Issuer Setup

An issuer needs:

1. A funded wallet account
2. ODIS quota for identifier obfuscation
3. Verification infrastructure
4. Optional: Data Encryption Key (DEK) for enhanced authentication

#### Step 1: Set Up Issuer Account

```typescript
import { createWalletClient, http, parseEther } from "viem";
import { privateKeyToAccount } from "viem/accounts";
import { celoAlfajores } from "viem/chains";

// Issuer private key - KEEP SECURE
const ISSUER_PRIVATE_KEY = process.env.ISSUER_PRIVATE_KEY;
const account = privateKeyToAccount(ISSUER_PRIVATE_KEY);

// Create Viem client
const walletClient = createWalletClient({
  account,
  transport: http(),
  chain: celoAlfajores
});

const issuerAddress = account.address;
console.log("Issuer Address:", issuerAddress);
```

#### Step 2: Authentication Methods

Self Connect supports multiple authentication methods for ODIS:

**Wallet Key Authentication**

```typescript
import { OdisUtils } from "@celo/identity";
import { AuthSigner } from "@celo/identity/lib/odis/query";

const authSigner: AuthSigner = {
  authenticationMethod: OdisUtils.Query.AuthenticationMethod.WALLET_KEY,
  sign191: ({ message, account }) => walletClient.signMessage({ 
    message, 
    account 
  })
};
```

**Encryption Key (DEK) Authentication**

```typescript
const authSigner: AuthSigner = {
  authenticationMethod: OdisUtils.Query.AuthenticationMethod.ENCRYPTION_KEY,
  rawKey: process.env.DEK_PRIVATE_KEY
};
```

#### Step 3: Configure ODIS Service Context

```typescript
import { OdisContextName } from "@celo/identity/lib/odis/query";

const serviceContext = OdisUtils.Query.getServiceContext(
  OdisContextName.ALFAJORES // or OdisContextName.MAINNET
);

console.log("ODIS Endpoint:", serviceContext.odisUrl);
console.log("ODIS Public Key:", serviceContext.odisPubKey);
```

#### ODIS Quota Management

**Check Current Quota**

```typescript
const { remainingQuota } = await OdisUtils.Quota.getPnpQuotaStatus(
  issuerAddress,
  authSigner,
  serviceContext
);

console.log("Remaining ODIS Quota:", remainingQuota);
```

**Purchase Quota**

```typescript
import { getContract } from "viem";
import { stableTokenABI, odisPaymentsABI } from "@celo/abis";

// Contract addresses (Alfajores testnet)
const STABLE_TOKEN_ADDRESS = "0x874069Fa1Eb16D44d622F2e0Ca25eeA172369bC1"; // cUSD
const ODIS_PAYMENTS_ADDRESS = "0x645170cdB6B5c1bc80847bb728dBa56C50a20a49";

// Amount to pay (0.01 cUSD = 100 queries)
const ONE_CENT_CUSD = parseEther("0.01");

// Approve ODIS Payments to spend cUSD
const stableToken = getContract({
  address: STABLE_TOKEN_ADDRESS,
  abi: stableTokenABI,
  client: walletClient
});

const approveHash = await stableToken.write.approve([
  ODIS_PAYMENTS_ADDRESS,
  ONE_CENT_CUSD
]);

await walletClient.waitForTransactionReceipt({ hash: approveHash });

// Pay for quota
const odisPayments = getContract({
  address: ODIS_PAYMENTS_ADDRESS,
  abi: odisPaymentsABI,
  client: walletClient
});

const paymentHash = await odisPayments.write.payInCUSD([
  issuerAddress,
  ONE_CENT_CUSD
]);

await walletClient.waitForTransactionReceipt({ hash: paymentHash });

console.log("ODIS quota purchased successfully");
```

#### Verification Responsibilities

As an issuer, you must verify user ownership of identifiers. Implementation depends on identifier type:

**Phone Number Verification**

```typescript
// Example using Twilio
import twilio from "twilio";

const client = twilio(TWILIO_ACCOUNT_SID, TWILIO_AUTH_TOKEN);

async function verifyPhoneNumber(phoneNumber: string): Promise<boolean> {
  // Send verification code
  await client.verify.v2
    .services(TWILIO_VERIFY_SERVICE_SID)
    .verifications.create({ to: phoneNumber, channel: "sms" });
  
  // User enters code (from your UI)
  const verificationCode = await getUserInput();
  
  // Check verification
  const verification = await client.verify.v2
    .services(TWILIO_VERIFY_SERVICE_SID)
    .verificationChecks.create({ to: phoneNumber, code: verificationCode });
  
  return verification.status === "approved";
}
```

**Twitter Verification**

```typescript
// Example using Twitter OAuth
async function verifyTwitterHandle(handle: string, userAddress: string): Promise<boolean> {
  // Implement OAuth flow
  const oauth = await initiateTwitterOAuth(userAddress);
  
  // User authenticates with Twitter
  const twitterUser = await completeOAuthFlow(oauth);
  
  // Verify handle matches
  return twitterUser.username === handle;
}
```

**Email Verification**

```typescript
// Example verification flow
async function verifyEmail(email: string, userAddress: string): Promise<boolean> {
  // Generate verification token
  const token = generateSecureToken();
  
  // Store token with expiry
  await storeVerificationToken(email, userAddress, token);
  
  // Send verification email
  await sendEmail(email, {
    subject: "Verify your email",
    body: `Click here to verify: ${BASE_URL}/verify/${token}`
  });
  
  // User clicks link, returns true if token is valid
  return await checkTokenVerified(token);
}
```

### SDK Integration with Viem

#### Complete Registration Example

```typescript
import { 
  createWalletClient, 
  createPublicClient, 
  http,
  parseEther,
  type Address,
  type Hex 
} from "viem";
import { privateKeyToAccount } from "viem/accounts";
import { celoAlfajores } from "viem/chains";
import { OdisUtils } from "@celo/identity";
import { OdisContextName } from "@celo/identity/lib/odis/query";
import type { AuthSigner } from "@celo/identity/lib/odis/query";
import { getContract } from "viem";
import { federatedAttestationsABI, odisPaymentsABI, stableTokenABI } from "@celo/abis";

// Configuration
const ISSUER_PRIVATE_KEY = process.env.ISSUER_PRIVATE_KEY as Hex;
const FEDERATED_ATTESTATIONS_ADDRESS = "0x70F9314aF173c246669cFb0EEe79F9Cfd9C34ee3" as Address;
const ODIS_PAYMENTS_ADDRESS = "0x645170cdB6B5c1bc80847bb728dBa56C50a20a49" as Address;
const STABLE_TOKEN_ADDRESS = "0x874069Fa1Eb16D44d622F2e0Ca25eeA172369bC1" as Address;

// Setup
const account = privateKeyToAccount(ISSUER_PRIVATE_KEY);
const walletClient = createWalletClient({
  account,
  transport: http(),
  chain: celoAlfajores
});

const publicClient = createPublicClient({
  transport: http(),
  chain: celoAlfajores
});

const issuerAddress = account.address;

// User information (provided by user after verification)
const userPlaintextIdentifier = "+12345678910";
const userAccountAddress = "0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb" as Address;
const attestationVerifiedTime = BigInt(Math.floor(Date.now() / 1000));

async function registerAttestation() {
  // 1. Setup authentication
  const authSigner: AuthSigner = {
    authenticationMethod: OdisUtils.Query.AuthenticationMethod.WALLET_KEY,
    sign191: ({ message, account }) => 
      walletClient.signMessage({ message, account })
  };

  const serviceContext = OdisUtils.Query.getServiceContext(
    OdisContextName.ALFAJORES
  );

  // 2. Check and top up ODIS quota if needed
  const { remainingQuota } = await OdisUtils.Quota.getPnpQuotaStatus(
    issuerAddress,
    authSigner,
    serviceContext
  );

  console.log("Remaining quota:", remainingQuota);

  if (remainingQuota < 1) {
    console.log("Purchasing ODIS quota...");
    
    // Get contract instances
    const stableToken = getContract({
      address: STABLE_TOKEN_ADDRESS,
      abi: stableTokenABI,
      client: { public: publicClient, wallet: walletClient }
    });
    
    const odisPayments = getContract({
      address: ODIS_PAYMENTS_ADDRESS,
      abi: odisPaymentsABI,
      client: { public: publicClient, wallet: walletClient }
    });
    
    const ONE_CENT_CUSD = parseEther("0.01");
    
    // Approve ODIS Payments to spend cUSD
    const approveHash = await stableToken.write.approve([
      ODIS_PAYMENTS_ADDRESS,
      ONE_CENT_CUSD
    ]);
    await publicClient.waitForTransactionReceipt({ hash: approveHash });
    
    // Pay for quota
    const paymentHash = await odisPayments.write.payInCUSD([
      issuerAddress,
      ONE_CENT_CUSD
    ]);
    await publicClient.waitForTransactionReceipt({ hash: paymentHash });
    
    console.log("ODIS quota purchased successfully");
  }

  // 3. Get obfuscated identifier from ODIS
  console.log("Getting obfuscated identifier...");
  const { obfuscatedIdentifier } = await OdisUtils.Identifier.getObfuscatedIdentifier(
    userPlaintextIdentifier,
    OdisUtils.Identifier.IdentifierPrefix.PHONE_NUMBER,
    issuerAddress,
    authSigner,
    serviceContext
  );

  console.log("Obfuscated Identifier:", obfuscatedIdentifier);

  // 4. Register attestation on-chain
  console.log("Registering attestation...");
  const federatedAttestations = getContract({
    address: FEDERATED_ATTESTATIONS_ADDRESS,
    abi: federatedAttestationsABI,
    client: { public: publicClient, wallet: walletClient }
  });

  const hash = await federatedAttestations.write.registerAttestationAsIssuer([
    obfuscatedIdentifier as Hex,
    userAccountAddress,
    attestationVerifiedTime
  ]);

  const receipt = await publicClient.waitForTransactionReceipt({ hash });
  
  console.log("Attestation registered!");
  console.log("Transaction:", receipt.transactionHash);
  
  return {
    obfuscatedIdentifier,
    transactionHash: receipt.transactionHash
  };
}

// Execute
registerAttestation().catch(console.error);
```

#### Complete Lookup Example

```typescript
import { createPublicClient, http, type Address, type Hex } from "viem";
import { celoAlfajores } from "viem/chains";
import { OdisUtils } from "@celo/identity";
import { OdisContextName } from "@celo/identity/lib/odis/query";
import { getContract } from "viem";
import { federatedAttestationsABI } from "@celo/abis";

const FEDERATED_ATTESTATIONS_ADDRESS = "0x70F9314aF173c246669cFb0EEe79F9Cfd9C34ee3" as Address;

const publicClient = createPublicClient({
  transport: http(),
  chain: celoAlfajores
});

async function lookupIdentifier(
  plaintextIdentifier: string,
  identifierType: string,
  trustedIssuers: Address[]
): Promise<Address[]> {
  // 1. Setup authentication for lookup
  // For read-only operations, use a zero address
  const lookupAddress = "0x0000000000000000000000000000000000000000" as Address;
  
  const authSigner = {
    authenticationMethod: OdisUtils.Query.AuthenticationMethod.WALLET_KEY,
    sign191: async () => "0x" as Hex
  };

  const serviceContext = OdisUtils.Query.getServiceContext(
    OdisContextName.ALFAJORES
  );

  // 2. Get obfuscated identifier
  const { obfuscatedIdentifier } = await OdisUtils.Identifier.getObfuscatedIdentifier(
    plaintextIdentifier,
    identifierType,
    lookupAddress,
    authSigner,
    serviceContext
  );

  console.log("Looking up:", obfuscatedIdentifier);

  // 3. Query FederatedAttestations
  const federatedAttestations = getContract({
    address: FEDERATED_ATTESTATIONS_ADDRESS,
    abi: federatedAttestationsABI,
    client: publicClient
  });

  const attestations = await federatedAttestations.read.lookupAttestations([
    obfuscatedIdentifier as Hex,
    trustedIssuers
  ]);

  const [countsPerIssuer, accounts, signers, issuedOns, publishedOns] = attestations;

  // 4. Process results
  console.log("Found attestations:");
  
  let accountIndex = 0;
  for (let i = 0; i < trustedIssuers.length; i++) {
    const count = Number(countsPerIssuer[i]);
    console.log(`\nIssuer: ${trustedIssuers[i]}`);
    console.log(`Attestation count: ${count}`);
    
    for (let j = 0; j < count; j++) {
      console.log(`  Account: ${accounts[accountIndex]}`);
      console.log(`  Signer: ${signers[accountIndex]}`);
      console.log(`  Issued: ${new Date(Number(issuedOns[accountIndex]) * 1000).toISOString()}`);
      console.log(`  Published: ${new Date(Number(publishedOns[accountIndex]) * 1000).toISOString()}`);
      accountIndex++;
    }
  }

  return accounts as Address[];
}

// Example usage
const trustedIssuers: Address[] = [
  "0x6549aF2688e07907C1b821cA44d6d65872737f05", // Kaala
  "0x388612590F8cC6577F19c9b61811475Aa432CB44"  // Libera
];

lookupIdentifier(
  "+12345678910",
  OdisUtils.Identifier.IdentifierPrefix.PHONE_NUMBER,
  trustedIssuers
).catch(console.error);
```

#### Viem Best Practices

**Use Type-Safe Contract Interactions**

```typescript
import { getContract, type Address } from "viem";
import { federatedAttestationsABI } from "@celo/abis";

// Type-safe contract instance
const contract = getContract({
  address: FEDERATED_ATTESTATIONS_ADDRESS,
  abi: federatedAttestationsABI,
  client: { public: publicClient, wallet: walletClient }
});

// TypeScript knows the exact function signatures
const hash = await contract.write.registerAttestationAsIssuer([
  obfuscatedIdentifier as `0x${string}`,
  userAddress as `0x${string}`,
  timestamp
]);
```

**Handle Hex Types Properly**

Viem uses strict `Hex` types for type safety:

```typescript
import type { Hex, Address } from "viem";

// Correct
const privateKey: Hex = process.env.PRIVATE_KEY as Hex;
const address: Address = "0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb" as Address;

// Type assertion for obfuscated identifiers
const obfuscatedIdentifier: Hex = result.obfuscatedIdentifier as Hex;
```

**Use Proper Error Handling**

```typescript
import { 
  ContractFunctionExecutionError,
  TransactionExecutionError 
} from "viem";

try {
  const hash = await contract.write.registerAttestationAsIssuer([...]);
} catch (error) {
  if (error instanceof ContractFunctionExecutionError) {
    console.error("Contract error:", error.shortMessage);
  } else if (error instanceof TransactionExecutionError) {
    console.error("Transaction failed:", error.message);
  } else {
    console.error("Unknown error:", error);
  }
}
```

**Optimize with Public Actions**

For read-only operations, use public client directly:

```typescript
import { createPublicClient, http } from "viem";
import { celo } from "viem/chains";

const publicClient = createPublicClient({
  chain: celo,
  transport: http()
});

// No wallet needed for reads
const attestations = await publicClient.readContract({
  address: FEDERATED_ATTESTATIONS_ADDRESS,
  abi: federatedAttestationsABI,
  functionName: "lookupAttestations",
  args: [obfuscatedIdentifier, trustedIssuers]
});
```

#### Web/Browser

Browser environments require careful handling of wallet connections:

```typescript
// Check for wallet
if (window.ethereum) {
  const accounts = await window.ethereum.request({
    method: "eth_requestAccounts"
  });
  
  // Create client with injected provider
  const walletClient = createWalletClient({
    account: accounts[0],
    transport: custom(window.ethereum),
    chain: celoAlfajores
  });
}
```

**Next.js Example:**

```typescript
// pages/api/register-attestation.ts
import type { NextApiRequest, NextApiResponse } from "next";
import { OdisUtils } from "@celo/identity";

export default async function handler(
  req: NextApiRequest,
  res: NextApiResponse
) {
  const { phoneNumber, userAddress } = req.body;

  try {
    // Verify phone number (your verification logic)
    const isVerified = await verifyPhoneNumber(phoneNumber);
    
    if (!isVerified) {
      return res.status(400).json({ error: "Verification failed" });
    }

    // Register attestation
    const result = await registerAttestation(phoneNumber, userAddress);
    
    res.status(200).json(result);
  } catch (error) {
    res.status(500).json({ error: error.message });
  }
}
```

#### Custom Identifier Types

Create custom identifier types for your use case:

```typescript
// Define custom prefix
const CUSTOM_PREFIX = "custom-app";

async function registerCustomIdentifier(
  customId: string,
  userAddress: string
) {
  const { obfuscatedIdentifier } = await OdisUtils.Identifier.getObfuscatedIdentifier(
    customId,
    CUSTOM_PREFIX, // Custom prefix
    issuerAddress,
    authSigner,
    serviceContext
  );

  // Register as usual
  await registerAttestation(obfuscatedIdentifier, userAddress);
}
```

**Best Practices for Custom Identifiers:**

* Use descriptive prefixes (e.g., `myapp://` not `ma://`)
* Document your prefix for ecosystem adoption
* Consider standardization if widely applicable
* Ensure identifiers are unique and verifiable

### Reference & Resources

#### Contract Addresses

**Mainnet (Celo)**

| Contract              | Address                                      |
| --------------------- | -------------------------------------------- |
| FederatedAttestations | `0x0aD5b1d0C25ecF6266Dd951403723B2687d6aff2` |
| OdisPayments          | `0x645170cdB6B5c1bc80847bb728dBa56C50a20a49` |
| StableToken (cUSD)    | `0x765DE816845861e75A25fCA122bb6898B8B1282a` |

**Alfajores Testnet**

| Contract              | Address                                      |
| --------------------- | -------------------------------------------- |
| FederatedAttestations | `0x70F9314aF173c246669cFb0EEe79F9Cfd9C34ee3` |
| OdisPayments          | `0x645170cdB6B5c1bc80847bb728dBa56C50a20a49` |
| StableToken (cUSD)    | `0x874069Fa1Eb16D44d622F2e0Ca25eeA172369bC1` |

#### Example Repositories

| Example                    | Description                                        | Link                                                                                             |
| -------------------------- | -------------------------------------------------- | ------------------------------------------------------------------------------------------------ |
| **Phone Number (Next.js)** | Web app demonstrating phone verification with Viem | [emisianto](https://github.com/celo-org/emisianto)                                               |
| **Template**               | Next.js template for Self Connect                  | [socialconnect-template](https://github.com/celo-org/socialconnect-template)                     |
| **React Native**           | Mobile app demo                                    | [SelfConnect-ReactNative-Demo](https://github.com/celo-org/SocialConnect-ReactNative-Demo)       |
| **Twitter (Client-side)**  | Twitter handle verification                        | [SelfConnect-Twitter](https://github.com/celo-org/SocialConnect-Twitter)                         |
| **Twitter (Server-side)**  | Server-side Twitter verification                   | [SelfConnect-Twitter-Server-Side](https://github.com/celo-org/SocialConnect-Twitter-Server-Side) |
| **MiniPay Nexus**          | MiniPay integration template                       | [nexus](https://github.com/celo-org/nexus)                                                       |

#### Troubleshooting

**ODIS Quota Issues**

**Problem:** "Insufficient quota" error

**Solution:**

```typescript
// Check quota
const { remainingQuota } = await OdisUtils.Quota.getPnpQuotaStatus(
  issuerAddress,
  authSigner,
  serviceContext
);

console.log("Remaining:", remainingQuota);

// Purchase more if needed (10 cUSD = 10,000 queries)
```

**Rate Limiting**

**Problem:** Too many requests to ODIS

**Solution:**

* Implement request queuing
* Cache obfuscated identifiers
* Use batch operations when possible
* Monitor quota usage

**Transaction Failures**

**Problem:** "Transaction reverted" or gas estimation failed

**Solution:**

```typescript
// Ensure sufficient gas
const hash = await contract.write.registerAttestationAsIssuer(
  [identifier, address, timestamp],
  {
    gas: 200000n // Explicit gas limit
  }
);

// Check if attestation already exists
const existing = await contract.read.lookupAttestations([
  identifier,
  [issuerAddress]
]);

if (existing.accounts.length > 0) {
  console.log("Attestation already exists");
}
```

**Network Issues**

**Problem:** RPC connection failures

**Solution:**

```typescript
// Use fallback RPCs
const transport = fallback([
  http("https://forno.celo.org"),
  http("https://rpc.ankr.com/celo"),
  http("https://1rpc.io/celo")
]);

const client = createPublicClient({
  chain: celo,
  transport
});
```

#### FAQ

**Q: Do I need to pay gas for lookups?**

A: No, lookups are read-only operations that don't require gas. You only need ODIS quota.

**Q: Can users register themselves?**

A: Users can submit the registration transaction if the issuer provides a signed attestation, but the issuer must still verify ownership and provide the obfuscated identifier.

**Q: How much does it cost to register a user?**

A: With 10 cUSD of ODIS quota, you can register 10,000 users. Gas costs for on-chain registration are typically <0.01 cUSD per transaction.

**Q: Can I trust attestations from any issuer?**

A: No, you should only trust issuers whose verification standards you trust. Each issuer is responsible for their own verification quality.

**Q: What happens if an issuer goes offline?**

A: Existing attestations remain on-chain and accessible. Users can register with other issuers for redundancy.

**Q: Can I verify multiple identifier types for the same user?**

A: Yes, you can register multiple attestations (phone + Twitter + email) for the same address, each with its own prefix.

**Q: Is E.164 format required for phone numbers?**

A: Yes, the SDK's `getObfuscatedIdentifier` function only accepts E.164 formatted phone numbers (e.g., +12345678901).

**Q: How do I map attestation results to issuers?**

A: The `lookupAttestations` return arrays are ordered by the `trustedIssuers` input array. Use `countsPerIssuer` to determine how many attestations belong to each issuer.

**Q: Can I use Self Connect on other EVM chains?**

A: Self Connect is currently designed for Celo. The contracts and ODIS infrastructure are Celo-specific.

**Q: Why use Viem instead of Web3.js or Ethers.js?**

A: Viem offers better TypeScript support, smaller bundle size, modular architecture, and is actively maintained. It's the recommended library for modern Web3 development on Celo.

**Q: Do I need to use different RPC endpoints for mainnet vs testnet?**

A: Yes, Viem's chain configurations handle this automatically:

* Mainnet: Use `celo` chain from `viem/chains`
* Testnet: Use `celoAlfajores` chain from `viem/chains`


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://docs.self.xyz/self-connect/developer-guide.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
