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
2 changes: 0 additions & 2 deletions packages/citizen-sdk/src/sdks/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,2 @@
export * from "./viem-identity-sdk"
export * from "./viem-claim-sdk"
export * from "./viem-custodial-claim-sdk"
export * from "./viem-custodial-identity-sdk"
123 changes: 90 additions & 33 deletions packages/citizen-sdk/src/sdks/viem-claim-sdk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,16 @@ import {
type Account,
type Address,
type Chain,
type Hash,
type LocalAccount,
type PublicClient,
type SimulateContractParameters,
type WalletClient,
ContractFunctionExecutionError,
encodeFunctionData,
TransactionReceipt,
} from "viem"

import { waitForTransactionReceipt } from "viem/actions"

import { IdentitySDK } from "./viem-identity-sdk"
import {
type contractEnv,
Expand All @@ -21,7 +22,8 @@ import { Envs, faucetABI, getGasPrice, ubiSchemeV2ABI } from "../constants"
import { resolveChainAndContract } from "../utils/chains"

export interface ClaimSDKOptions {
account: Address
account: Account | undefined
address: Address
publicClient: PublicClient
walletClient: WalletClient<any, Chain | undefined, Account | undefined>
identitySDK: IdentitySDK
Expand All @@ -48,13 +50,15 @@ export class ClaimSDK {
private readonly ubiSchemeAddress: Address
private readonly ubiSchemeAltAddress: Address
private readonly faucetAddress: Address
private readonly account: Address
private readonly account: Account
private readonly address: Address
private readonly altChain: SupportedChains
private readonly env: contractEnv
public readonly rdu: string

constructor({
account,
address,
publicClient,
walletClient,
identitySDK,
Expand All @@ -67,7 +71,8 @@ export class ClaimSDK {
this.publicClient = publicClient
this.walletClient = walletClient
this.identitySDK = identitySDK
this.account = account ?? walletClient.account.address
this.account = account ?? walletClient.account
this.address = address ?? this.account.address

this.rdu = rdu
this.env = env
Expand All @@ -86,10 +91,14 @@ export class ClaimSDK {
}

static async init(
props: Omit<ClaimSDKOptions, "account">,
props: Omit<ClaimSDKOptions, "account" | "address">,
): Promise<ClaimSDK> {
const [account] = await props.walletClient.getAddresses()
return new ClaimSDK({ account, ...props })
const [address] = await props.walletClient.getAddresses()
return new ClaimSDK({
Comment on lines +96 to +97
Copy link
Contributor

Choose a reason for hiding this comment

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

issue (bug_risk): The init method now passes both account and address, but account may be undefined.

Validate that walletClient.account is defined before creating the SDK to prevent downstream issues.

account: props.walletClient?.account,
address,
...props,
})
}

/**
Expand All @@ -114,7 +123,7 @@ export class ClaimSDK {
abi: params.abi,
functionName: params.functionName,
args: params.args || [],
account: this.account,
account: this.address,
})) as T
} catch (error: any) {
throw new Error(
Expand All @@ -132,28 +141,78 @@ export class ClaimSDK {
*/
async submitAndWait(
params: SimulateContractParameters,
onHash?: (hash: `0x${string}`) => void,
): Promise<TransactionReceipt> {
if (!this.account) {
onHash?: (hash: Hash) => void,
): Promise<TransactionReceipt | null> {
const account = this.walletClient.account
if (!account?.address) {
throw new Error("No active wallet address found.")
}

const { request } = await this.publicClient.simulateContract({
account: this.account,
account: account.address,
...params,
})

const hash = await this.walletClient.writeContract(request)
onHash?.(hash)
try {
let hash: Hash

if ("signMessage" in this.account) {
const callData = encodeFunctionData({
abi: params.abi,
functionName: params.functionName,
args: params.args,
})

// Local signing path (works on RPCs that block eth_sendTransaction)
hash = await this.walletClient.request({
// Pass the LocalAccount, not just the address
Comment on lines +159 to +168
Copy link
Contributor

Choose a reason for hiding this comment

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

issue (bug_risk): Type assertion to LocalAccount may be unsafe if account is not actually a LocalAccount.

Add a runtime check to ensure 'account' is a LocalAccount before casting to prevent potential errors.

account: account as LocalAccount,
to: request.address!,
data: callData,
value: request.value,
gas: request.gas,
...(request.maxFeePerGas
? {
maxFeePerGas: request.maxFeePerGas,
maxPriorityFeePerGas: request.maxPriorityFeePerGas,
}
: { gasPrice: request.gasPrice }),
} as any)
} else {
// RPC signing path (may fail on some Celo RPCs)
hash = await this.walletClient.writeContract(request)
}

// we wait one block
// to prevent waitFor... to immediately throw an error
await new Promise((res) => setTimeout(res, 5000))
onHash?.(hash)

return waitForTransactionReceipt(this.publicClient, {
hash,
retryDelay: 5000,
})
// Wait one block to prevent immediate errors
await new Promise((res) => setTimeout(res, 5000))

return this.publicClient.waitForTransactionReceipt({
hash,
pollingInterval: 5_000,
})
} catch (error: any) {
if (
error?.message?.includes("rpc method is not whitelisted") ||
error?.message?.includes("eth_sendTransaction") ||
error?.code === -32601
) {
throw new Error(
"Transaction failed: RPC does not support eth_sendTransaction. " +
"Attach a LocalAccount (privateKeyToAccount) or set submitStrategy: 'local'.",
)
}
if (error?.message?.toLowerCase().includes("insufficient funds")) {
throw new Error("Transaction failed: Insufficient funds for gas fees.")
}
if (error?.message?.toLowerCase().includes("nonce")) {
throw new Error("Transaction failed: Nonce error. Please try again.")
}
throw new Error(
`Transaction submission failed: ${error?.message ?? String(error)}`,
)
}
}

/**
Expand All @@ -170,7 +229,7 @@ export class ClaimSDK {
address: !pClient ? this.ubiSchemeAddress : this.ubiSchemeAltAddress,
abi: ubiSchemeV2ABI,
functionName: "checkEntitlement",
args: [this.account],
args: [this.address],
},
pClient,
)
Expand All @@ -183,11 +242,10 @@ export class ClaimSDK {
* @throws If unable to check wallet status or fetch required data.
*/
async getWalletClaimStatus(): Promise<WalletClaimStatus> {
const userAddress = this.account

// 1. Check whitelisting status
const { isWhitelisted } =
await this.identitySDK.getWhitelistedRoot(userAddress)
const { isWhitelisted } = await this.identitySDK.getWhitelistedRoot(
this.address,
)

if (!isWhitelisted) {
return {
Expand Down Expand Up @@ -227,11 +285,10 @@ export class ClaimSDK {
* @throws If the user is not whitelisted, not entitled to claim, balance check fails, or claim transaction fails.
*/
async claim(): Promise<TransactionReceipt | any> {
const userAddress = this.account

// 1. Check whitelisting status
const { isWhitelisted } =
await this.identitySDK.getWhitelistedRoot(userAddress)
const { isWhitelisted } = await this.identitySDK.getWhitelistedRoot(
this.address,
)
if (!isWhitelisted) {
await this.fvRedirect()
throw new Error("User requires identity verification.")
Expand Down Expand Up @@ -337,7 +394,7 @@ export class ClaimSDK {

const body = JSON.stringify({
chainId: this.walletClient.chain?.id,
account: this.account,
account: this.address,
})

const response = await fetch(`${backend}/verify/topWallet`, {
Expand Down Expand Up @@ -373,7 +430,7 @@ export class ClaimSDK {
(toppingAmount * (100n - BigInt(minTopping))) / 100n || minBalance

const balance = await this.publicClient.getBalance({
address: this.account,
address: this.address,
})

return balance >= minThreshold
Expand Down
132 changes: 0 additions & 132 deletions packages/citizen-sdk/src/sdks/viem-custodial-claim-sdk.ts

This file was deleted.

Loading