Skip to content

refactor: split Token into Token + WrappedToken hierarchy [SDK-96] - #323

Merged
zama-cremaud merged 17 commits into
prereleasefrom
feature/sdk-96-feat-restructure-token-hierarchy-into-token-wrappedtoken
May 12, 2026
Merged

refactor: split Token into Token + WrappedToken hierarchy [SDK-96]#323
zama-cremaud merged 17 commits into
prereleasefrom
feature/sdk-96-feat-restructure-token-hierarchy-into-token-wrappedtoken

Conversation

@zama-cremaud

@zama-cremaud zama-cremaud commented May 6, 2026

Copy link
Copy Markdown
Contributor

Summary

Restructures the high-level token API to mirror the ERC-7984 contract architecture.

  • Token — base confidential token (reads + transfer + operator + delegated decrypt)
  • WrappedToken extends Token — adds wrapper-specific ops: shield, unshield, allowance, unwrap, finalizeUnwrap, etc.
  • ReadonlyToken is removed — reads live on Token. createToken(addr) is now single-arg; new createWrappedToken(addr) returns a WrappedToken (the wrapper IS the confidential token).
  • React hooks aligned: useToken(address) / useWrappedToken(address); useReadonlyToken removed; single-arg / { address, ... } shapes unified across all hooks (see commit body of b26cadb5 for the full list).
  • Mutation factories updated: shield/unshield/unwrap/finalize/approveUnderlying/resumeUnshield take a WrappedToken; delegateDecryption / revokeDelegation take (sdk, contractAddress) matching the SDK-95 SDK-level primitives.
  • Test harness pages/forms updated; wrapperAddress props dropped (defaults to tokenAddress).
  • Docs: ReadonlyToken.md / useReadonlyToken.md deleted; placeholder pages added for WrappedToken.md / useWrappedToken.md.
  • API reports regenerated.

Breaking changes

  • ReadonlyToken, createReadonlyToken, and the two-argument createToken(addr, wrapper) are removed.
  • useReadonlyToken is removed.
  • Several React hooks change shape (single-arg vs. { address, ... }); object-form callers should rename tokenAddressaddress and drop the now-unused wrapperAddress.

Migration: for wrapper operations, use createWrappedToken(wrapperAddr) / useWrappedToken(wrapperAddr).

Stack / context

Builds on top of SDK-95 (#307), now squash-merged into prerelease. Reconciled with the SDK-134 credentials refactor that landed in parallel: signer.requireWalletAccount(op).address replaces signer.getAddress(); isFatalBatchError replaces isSessionError; confidentialBalanceQueryOptions keeps SDK-134's signer-scoped third arg.

Commits (ordered for review)

  1. feat(sdk)!: split Token into Token + WrappedToken hierarchy
  2. feat(react-sdk)!: align hooks with Token/WrappedToken split
  3. test(harness): align test pages/forms with single-address API
  4. docs(gitbook): replace ReadonlyToken with WrappedToken stubs
  5. chore(api-report): regenerate after Token/WrappedToken split

Test plan

  • pnpm typecheck — clean
  • pnpm lint — clean
  • pnpm format:check — clean
  • pnpm test:run1480 passed, 5 skipped, 0 failed
  • pnpm api-report — regenerated, committed
  • CI green
  • Manual smoke test of the test harness pages (test-nextjs / test-vite)

🤖 Generated with Claude Code

@cla-bot cla-bot Bot added the cla-signed label May 6, 2026
@github-actions

github-actions Bot commented May 6, 2026

Copy link
Copy Markdown

Public API Changes

react-sdk.api.md
--- a/react-sdk.api.md
+++ b/react-sdk.api.md
@@ -29,7 +29,6 @@
 import { PaginatedResult } from '@zama-fhe/sdk';
 import { PropsWithChildren } from 'react';
 import { PublicKeyData } from '@zama-fhe/sdk';
-import { ReadonlyToken } from '@zama-fhe/sdk';
 import { ResumeUnshieldParams } from '@zama-fhe/sdk/query';
 import { RevokeDelegationParams } from '@zama-fhe/sdk/query';
 import { ShieldParams } from '@zama-fhe/sdk/query';
@@ -45,6 +44,7 @@
 import { UseMutationResult } from '@tanstack/react-query';
 import { UseQueryOptions } from '@tanstack/react-query';
 import { UserDecryptQueryConfig } from '@zama-fhe/sdk/query';
+import { WrappedToken } from '@zama-fhe/sdk';
 import { ZamaConfig } from '@zama-fhe/sdk';
 import { ZamaSDK } from '@zama-fhe/sdk';
 import { ZKProofLike } from '@zama-fhe/sdk';
@@ -53,10 +53,10 @@
 export function useAllow(options?: UseMutationOptions<void, Error, Address[]>): _$_tanstack_react_query0.UseMutationResult<void, Error, `0x${string}`[], unknown>;
 
 // @public
-export function useApproveUnderlying(config: UseZamaConfig, options?: UseMutationOptions<TransactionResult, Error, ApproveUnderlyingParams, Address>): _$_tanstack_react_query0.UseMutationResult<TransactionResult, Error, ApproveUnderlyingParams, `0x${string}`>;
+export function useApproveUnderlying(address: Address, options?: UseMutationOptions<TransactionResult, Error, ApproveUnderlyingParams, Address>): _$_tanstack_react_query0.UseMutationResult<TransactionResult, Error, ApproveUnderlyingParams, `0x${string}`>;
 
 // @public
-export function useBatchDecryptBalancesAs(tokens: ReadonlyToken[], options?: UseMutationOptions<Map<Address, bigint>, Error, BatchDecryptBalancesAsParams>): _$_tanstack_react_query0.UseMutationResult<Map<`0x${string}`, bigint>, Error, _$_zama_fhe_sdk0.BatchDecryptAsOptions, unknown>;
+export function useBatchDecryptBalancesAs(tokens: Token[], options?: UseMutationOptions<Map<Address, bigint>, Error, BatchDecryptBalancesAsParams>): _$_tanstack_react_query0.UseMutationResult<Map<`0x${string}`, bigint>, Error, _$_zama_fhe_sdk0.BatchDecryptAsOptions, unknown>;
 
 // @public
 export function useClearCredentials(options?: UseMutationOptions<void>): _$_tanstack_react_query0.UseMutationResult<void, Error, void, unknown>;
@@ -67,7 +67,7 @@
 // @public (undocumented)
 export interface UseConfidentialBalanceConfig {
     account: Address | undefined;
-    tokenAddress: Address;
+    address: Address;
 }
 
 // @public (undocumented)
@@ -81,7 +81,7 @@
 // @public (undocumented)
 export interface UseConfidentialBalancesConfig {
     account: Address | undefined;
-    tokenAddresses: Address[];
+    addresses: Address[];
 }
 
 // @public (undocumented)
@@ -94,9 +94,9 @@
 
 // @public (undocumented)
 export interface UseConfidentialIsOperatorConfig {
+    address: Address | undefined;
     holder: Address | undefined;
     spender: Address | undefined;
-    tokenAddress: Address | undefined;
 }
 
 // @public
@@ -104,13 +104,13 @@
 
 // @public (undocumented)
 export interface UseConfidentialIsOperatorSuspenseConfig {
+    address: Address;
     holder: Address;
     spender: Address;
-    tokenAddress: Address;
 }
 
 // @public
-export function useConfidentialSetOperator(config: UseZamaConfig, options?: UseMutationOptions<TransactionResult, Error, ConfidentialSetOperatorParams, Address>): _$_tanstack_react_query0.UseMutationResult<TransactionResult, Error, ConfidentialSetOperatorParams, `0x${string}`>;
+export function useConfidentialSetOperator(address: Address, options?: UseMutationOptions<TransactionResult, Error, ConfidentialSetOperatorParams, Address>): _$_tanstack_react_query0.UseMutationResult<TransactionResult, Error, ConfidentialSetOperatorParams, `0x${string}`>;
 
 // @public
 export function useConfidentialTokenAddress(input: {
@@ -121,12 +121,13 @@
 export function useConfidentialTransfer<TContext = unknown>(config: UseConfidentialTransferConfig, options?: UseMutationOptions<TransactionResult, Error, ConfidentialTransferParams, TContext>): UseMutationResult<TransactionResult, Error, ConfidentialTransferParams, TContext>;
 
 // @public
-export interface UseConfidentialTransferConfig extends UseZamaConfig {
+export interface UseConfidentialTransferConfig {
+    address: Address;
     optimistic?: boolean;
 }
 
 // @public
-export function useConfidentialTransferFrom(config: UseZamaConfig, options?: UseMutationOptions<TransactionResult, Error, ConfidentialTransferFromParams, Address>): _$_tanstack_react_query0.UseMutationResult<TransactionResult, Error, ConfidentialTransferFromParams, `0x${string}`>;
+export function useConfidentialTransferFrom(address: Address, options?: UseMutationOptions<TransactionResult, Error, ConfidentialTransferFromParams, Address>): _$_tanstack_react_query0.UseMutationResult<TransactionResult, Error, ConfidentialTransferFromParams, `0x${string}`>;
 
 // @public
 export function useCreateDelegatedUserDecryptEIP712(): _$_tanstack_react_query0.UseMutationResult<Readonly<{
@@ -140,10 +141,10 @@
 export function useCreateEIP712(): _$_tanstack_react_query0.UseMutationResult<EIP712TypedData, Error, CreateEIP712Params, unknown>;
 
 // @public
-export function useDecryptBalanceAs(tokenAddress: Address, options?: UseMutationOptions<bigint, Error, DecryptBalanceAsParams>): _$_tanstack_react_query0.UseMutationResult<bigint, Error, DecryptBalanceAsParams, unknown>;
+export function useDecryptBalanceAs(address: Address, options?: UseMutationOptions<bigint, Error, DecryptBalanceAsParams>): _$_tanstack_react_query0.UseMutationResult<bigint, Error, DecryptBalanceAsParams, unknown>;
 
 // @public
-export function useDelegateDecryption(config: UseZamaConfig, options?: UseMutationOptions<TransactionResult, Error, DelegateDecryptionParams>): _$_tanstack_react_query0.UseMutationResult<TransactionResult, Error, DelegateDecryptionParams, unknown>;
+export function useDelegateDecryption(address: Address, options?: UseMutationOptions<TransactionResult, Error, DelegateDecryptionParams>): _$_tanstack_react_query0.UseMutationResult<TransactionResult, Error, DelegateDecryptionParams, unknown>;
 
 // @public
 export function useDelegatedUserDecrypt(): _$_tanstack_react_query0.UseMutationResult<Record<`0x${string}`, ClearValueType>, Error, DelegatedUserDecryptMutationParams, unknown>;
@@ -165,7 +166,7 @@
 }>, Error, EncryptParams, unknown>;
 
 // @public
-export function useFinalizeUnwrap(config: UseZamaConfig, options?: UseMutationOptions<TransactionResult, Error, FinalizeUnwrapParams, Address>): _$_tanstack_react_query0.UseMutationResult<TransactionResult, Error, FinalizeUnwrapParams, `0x${string}`>;
+export function useFinalizeUnwrap(address: Address, options?: UseMutationOptions<TransactionResult, Error, FinalizeUnwrapParams, Address>): _$_tanstack_react_query0.UseMutationResult<TransactionResult, Error, FinalizeUnwrapParams, `0x${string}`>;
 
 // @public
 export function useGenerateKeypair(): _$_tanstack_react_query0.UseMutationResult<_$_zama_fhe_sdk0.KeypairType<`0x${string}`>, Error, void, unknown>;
@@ -225,19 +226,16 @@
 } | null, Error>;
 
 // @public
-export function useReadonlyToken(address: Address): _$_zama_fhe_sdk0.ReadonlyToken;
-
-// @public
 export function useRequestZKProofVerification(): _$_tanstack_react_query0.UseMutationResult<Readonly<{
     handles: Uint8Array[];
     inputProof: Uint8Array;
 }>, Error, ZKProofLike, unknown>;
 
 // @public
-export function useResumeUnshield(config: UseZamaConfig, options?: UseMutationOptions<TransactionResult, Error, ResumeUnshieldParams, Address>): _$_tanstack_react_query0.UseMutationResult<TransactionResult, Error, ResumeUnshieldParams, `0x${string}`>;
+export function useResumeUnshield(address: Address, options?: UseMutationOptions<TransactionResult, Error, ResumeUnshieldParams, Address>): _$_tanstack_react_query0.UseMutationResult<TransactionResult, Error, ResumeUnshieldParams, `0x${string}`>;
 
 // @public
-export function useRevokeDelegation(config: UseZamaConfig, options?: UseMutationOptions<TransactionResult, Error, RevokeDelegationParams>): _$_tanstack_react_query0.UseMutationResult<TransactionResult, Error, RevokeDelegationParams, unknown>;
+export function useRevokeDelegation(address: Address, options?: UseMutationOptions<TransactionResult, Error, RevokeDelegationParams>): _$_tanstack_react_query0.UseMutationResult<TransactionResult, Error, RevokeDelegationParams, unknown>;
 
 // @public
 export function useRevokePermits(options?: UseMutationOptions<void, Error, Address[] | void>): _$_tanstack_react_query0.UseMutationResult<void, Error, void | `0x${string}`[], unknown>;
@@ -246,12 +244,13 @@
 export function useShield<TContext = unknown>(config: UseShieldConfig, options?: UseMutationOptions<TransactionResult, Error, ShieldParams, TContext>): UseMutationResult<TransactionResult, Error, ShieldParams, TContext>;
 
 // @public
-export interface UseShieldConfig extends UseZamaConfig {
+export interface UseShieldConfig {
+    address: Address;
     optimistic?: boolean;
 }
 
 // @public
-export function useToken(config: UseZamaConfig): Token;
+export function useToken(address: Address): Token;
 
 // @public
 export function useTokenAddress(input: {
@@ -286,9 +285,8 @@
 
 // @public (undocumented)
 export interface UseUnderlyingAllowanceConfig {
+    address: Address;
     owner: Address | undefined;
-    tokenAddress: Address;
-    wrapperAddress: Address;
 }
 
 // @public
@@ -296,22 +294,21 @@
 
 // @public (undocumented)
 export interface UseUnderlyingAllowanceSuspenseConfig {
+    address: Address;
     owner: Address;
-    tokenAddress: Address;
-    wrapperAddress: Address;
 }
 
 // @public
-export function useUnshield(config: UseZamaConfig, options?: UseMutationOptions<TransactionResult, Error, UnshieldParams, Address>): _$_tanstack_react_query0.UseMutationResult<TransactionResult, Error, UnshieldParams, `0x${string}`>;
+export function useUnshield(address: Address, options?: UseMutationOptions<TransactionResult, Error, UnshieldParams, Address>): _$_tanstack_react_query0.UseMutationResult<TransactionResult, Error, UnshieldParams, `0x${string}`>;
 
 // @public
-export function useUnshieldAll(config: UseZamaConfig, options?: UseMutationOptions<TransactionResult, Error, UnshieldAllParams | void, Address>): _$_tanstack_react_query0.UseMutationResult<TransactionResult, Error, void | UnshieldAllParams, `0x${string}`>;
+export function useUnshieldAll(address: Address, options?: UseMutationOptions<TransactionResult, Error, UnshieldAllParams | void, Address>): _$_tanstack_react_query0.UseMutationResult<TransactionResult, Error, void | UnshieldAllParams, `0x${string}`>;
 
 // @public
-export function useUnwrap(config: UseZamaConfig, options?: UseMutationOptions<TransactionResult, Error, UnwrapParams, Address>): _$_tanstack_react_query0.UseMutationResult<TransactionResult, Error, UnwrapParams, `0x${string}`>;
+export function useUnwrap(address: Address, options?: UseMutationOptions<TransactionResult, Error, UnwrapParams, Address>): _$_tanstack_react_query0.UseMutationResult<TransactionResult, Error, UnwrapParams, `0x${string}`>;
 
 // @public
-export function useUnwrapAll(config: UseZamaConfig, options?: UseMutationOptions<TransactionResult, Error, void, Address>): _$_tanstack_react_query0.UseMutationResult<TransactionResult, Error, void, `0x${string}`>;
+export function useUnwrapAll(address: Address, options?: UseMutationOptions<TransactionResult, Error, void, Address>): _$_tanstack_react_query0.UseMutationResult<TransactionResult, Error, void, `0x${string}`>;
 
 // @public
 export function useUserDecrypt(config: UserDecryptQueryConfig, options?: Omit<UseQueryOptions<DecryptResult>, "queryKey" | "queryFn">): _$_tanstack_react_query0.UseQueryResult<Readonly<Record<`0x${string}`, _$_zama_fhe_sdk0.ClearValueType>>, Error>;
@@ -320,6 +317,9 @@
 export type UseUserDecryptResult = ReturnType<typeof useUserDecrypt>;
 
 // @public
+export function useWrappedToken(address: Address): WrappedToken;
+
+// @public
 export function useWrapperDiscovery(config: UseWrapperDiscoveryConfig, options?: Omit<UseQueryOptions<Address | null>, "queryKey" | "queryFn">): _$_tanstack_react_query0.UseQueryResult<`0x${string}` | null, Error>;
 
 // @public
@@ -341,12 +341,6 @@
 export function useWrappersRegistryAddress(): Address | undefined;
 
 // @public
-export interface UseZamaConfig {
-    tokenAddress: Address;
-    wrapperAddress?: Address;
-}
-
-// @public
 export function useZamaSDK(): ZamaSDK;
 
 // @public
sdk-query.api.md
--- a/sdk-query.api.md
+++ b/sdk-query.api.md
@@ -35,7 +35,7 @@
 export type ApprovalStrategy = "max" | "exact" | "skip";
 
 // @public (undocumented)
-export function approveUnderlyingMutationOptions(token: Token): MutationFactoryOptions<readonly ["zama.approveUnderlying", Address], ApproveUnderlyingParams, TransactionResult>;
+export function approveUnderlyingMutationOptions(token: WrappedToken): MutationFactoryOptions<readonly ["zama.approveUnderlying", Address], ApproveUnderlyingParams, TransactionResult>;
 
 // @public
 export interface ApproveUnderlyingParams {
@@ -77,7 +77,7 @@
 }
 
 // @public (undocumented)
-export function batchDecryptBalancesAsMutationOptions(tokens: ReadonlyToken[]): MutationFactoryOptions<readonly ["zama.batchDecryptBalancesAs", ...Address[]], BatchDecryptBalancesAsParams, Map<Address, bigint>>;
+export function batchDecryptBalancesAsMutationOptions(tokens: Token[]): MutationFactoryOptions<readonly ["zama.batchDecryptBalancesAs", ...Address[]], BatchDecryptBalancesAsParams, Map<Address, bigint>>;
 
 // @public
 export type BatchDecryptBalancesAsParams = BatchDecryptAsOptions;
@@ -100,7 +100,7 @@
 // Warning: (ae-forgotten-export) The symbol "SignerQueryContext" needs to be exported by the entry point index.d.ts
 //
 // @public
-export function confidentialBalanceQueryOptions(token: ReadonlyToken, config: ConfidentialBalanceQueryConfig, signerContext?: SignerQueryContext): QueryFactoryOptions<bigint, Error, bigint, ReturnType<typeof zamaQueryKeys.confidentialBalance.owner>>;
+export function confidentialBalanceQueryOptions(token: Token, config: ConfidentialBalanceQueryConfig, signerContext?: SignerQueryContext): QueryFactoryOptions<bigint, Error, bigint, ReturnType<typeof zamaQueryKeys.confidentialBalance.owner>>;
 
 // @public (undocumented)
 export interface ConfidentialBalancesQueryConfig {
@@ -111,7 +111,7 @@
 }
 
 // @public (undocumented)
-export function confidentialBalancesQueryOptions(tokens: ReadonlyToken[], config?: ConfidentialBalancesQueryConfig, signerContext?: SignerQueryContext): QueryFactoryOptions<BatchBalancesResult, Error, BatchBalancesResult, ReturnType<typeof zamaQueryKeys.confidentialBalances.tokens>>;
+export function confidentialBalancesQueryOptions(tokens: Token[], config?: ConfidentialBalancesQueryConfig, signerContext?: SignerQueryContext): QueryFactoryOptions<BatchBalancesResult, Error, BatchBalancesResult, ReturnType<typeof zamaQueryKeys.confidentialBalances.tokens>>;
 
 // @public (undocumented)
 export interface ConfidentialIsOperatorQueryConfig {
@@ -208,7 +208,7 @@
 };
 
 // @public (undocumented)
-export function decryptBalanceAsMutationOptions(readonlyToken: ReadonlyToken): MutationFactoryOptions<readonly ["zama.decryptBalanceAs", Address], DecryptBalanceAsParams, bigint>;
+export function decryptBalanceAsMutationOptions(token: Token): MutationFactoryOptions<readonly ["zama.decryptBalanceAs", Address], DecryptBalanceAsParams, bigint>;
 
 // @public
 export interface DecryptBalanceAsParams {
@@ -257,7 +257,7 @@
 }
 
 // @public (undocumented)
-export function delegateDecryptionMutationOptions(token: Token): MutationFactoryOptions<readonly ["zama.delegateDecryption", Address], DelegateDecryptionParams, TransactionResult>;
+export function delegateDecryptionMutationOptions(sdk: ZamaSDK, contractAddress: Address): MutationFactoryOptions<readonly ["zama.delegateDecryption", Address], DelegateDecryptionParams, TransactionResult>;
 
 // @public
 export interface DelegateDecryptionParams {
@@ -390,7 +390,7 @@
 export function filterQueryOptions<TOptions extends Record<string, unknown>>(options: TOptions): Omit<TOptions, StrippedQueryOptionKeys>;
 
 // @public (undocumented)
-export function finalizeUnwrapMutationOptions(token: Token): MutationFactoryOptions<readonly ["zama.finalizeUnwrap", Address], FinalizeUnwrapParams, TransactionResult>;
+export function finalizeUnwrapMutationOptions(token: WrappedToken): MutationFactoryOptions<readonly ["zama.finalizeUnwrap", Address], FinalizeUnwrapParams, TransactionResult>;
 
 // @public
 export type FinalizeUnwrapParams = /** Preferred input from upgraded `UnwrapRequested` events. */{
@@ -602,47 +602,6 @@
     readonly topics: readonly Hex[];
 }
 
-// @public
-export class ReadonlyToken {
-    constructor(sdk: ZamaSDK, address: Address);
-    // (undocumented)
-    readonly address: Address;
-    allow(): Promise<void>;
-    static allow(...tokens: ReadonlyToken[]): Promise<void>;
-    allowance(wrapper: Address, owner: Address): Promise<bigint>;
-    balanceOf(owner: Address): Promise<bigint>;
-    static batchBalancesOf(tokens: ReadonlyToken[], owner: Address): Promise<BatchBalancesResult>;
-    static batchDecryptBalancesAs(tokens: ReadonlyToken[], options: BatchDecryptAsOptions): Promise<Map<Address, bigint>>;
-    confidentialBalanceOf(owner: Address): Promise<Handle>;
-    decimals(): Promise<number>;
-    decryptBalanceAs(input: {
-        delegatorAddress: Address;
-        accountAddress?: Address;
-    }): Promise<bigint>;
-    protected emit(input: ZamaSDKEventInput): void;
-    // (undocumented)
-    protected getAclAddress(): Promise<Address>;
-    getDelegationExpiry(input: {
-        delegatorAddress: Address;
-        delegateAddress: Address;
-    }): Promise<bigint>;
-    isAllowed(): Promise<boolean>;
-    isConfidential(): Promise<boolean>;
-    isDelegated(params: {
-        delegatorAddress: Address;
-        delegateAddress: Address;
-    }): Promise<boolean>;
-    isWrapper(): Promise<boolean>;
-    name(): Promise<string>;
-    // (undocumented)
-    protected readConfidentialBalanceOf(owner: Address): Promise<Handle>;
-    revoke(): Promise<void>;
-    // (undocumented)
-    readonly sdk: ZamaSDK;
-    symbol(): Promise<string>;
-    underlyingToken(): Promise<Address>;
-}
-
 // Warning: (ae-forgotten-export) The symbol "FheOperations" needs to be exported by the entry point index.d.ts
 //
 // @public
@@ -655,7 +614,7 @@
 export function requestZKProofVerificationMutationOptions(sdk: ZamaSDK): MutationFactoryOptions<readonly ["zama.requestZKProofVerification"], ZKProofLike, InputProofBytesType>;
 
 // @public (undocumented)
-export function resumeUnshieldMutationOptions(token: Token): MutationFactoryOptions<readonly ["zama.resumeUnshield", Address], ResumeUnshieldParams, TransactionResult>;
+export function resumeUnshieldMutationOptions(token: WrappedToken): MutationFactoryOptions<readonly ["zama.resumeUnshield", Address], ResumeUnshieldParams, TransactionResult>;
 
 // @public
 export interface ResumeUnshieldParams extends UnshieldCallbacks {
@@ -664,7 +623,7 @@
 }
 
 // @public (undocumented)
-export function revokeDelegationMutationOptions(token: Token): MutationFactoryOptions<readonly ["zama.revokeDelegation", Address], RevokeDelegationParams, TransactionResult>;
+export function revokeDelegationMutationOptions(sdk: ZamaSDK, contractAddress: Address): MutationFactoryOptions<readonly ["zama.revokeDelegation", Address], RevokeDelegationParams, TransactionResult>;
 
 // @public
 export interface RevokeDelegationParams {
@@ -698,7 +657,7 @@
 }
 
 // @public (undocumented)
-export function shieldMutationOptions(token: Token): MutationFactoryOptions<readonly ["zama.shield", Address], ShieldParams, TransactionResult>;
+export function shieldMutationOptions(token: WrappedToken): MutationFactoryOptions<readonly ["zama.shield", Address], ShieldParams, TransactionResult>;
 
 // @public
 export interface ShieldOptions extends ShieldCallbacks {
@@ -728,41 +687,35 @@
 export type StrippedQueryOptionKeys = "gcTime" | "staleTime" | "enabled" | "select" | "refetchInterval" | "refetchOnMount" | "refetchOnWindowFocus" | "refetchOnReconnect" | "retry" | "retryDelay" | "retryOnMount" | "queryFn" | "queryKey" | "queryKeyHashFn" | "initialData" | "initialDataUpdatedAt" | "placeholderData" | "structuralSharing" | "throwOnError" | "meta" | "query" | "pollingInterval";
 
 // @public
-export class Token extends ReadonlyToken {
-    constructor(sdk: ZamaSDK, address: Address, wrapper?: Address);
-    approveUnderlying(amount?: bigint): Promise<TransactionResult>;
-    static batchDelegateDecryption(input: {
-        tokens: Token[];
-        delegateAddress: Address;
-        expirationDate?: Date;
-    }): Promise<Map<Address, TransactionResult | ZamaError>>;
-    static batchRevokeDelegation(input: {
-        tokens: Token[];
-        delegateAddress: Address;
-    }): Promise<Map<Address, TransactionResult | ZamaError>>;
+export class Token {
+    constructor(sdk: ZamaSDK, address: Address);
+    // (undocumented)
+    readonly address: Address;
+    // @internal
+    protected assertConfidentialBalance(amount: bigint): Promise<void>;
+    balanceOf(owner: Address): Promise<bigint>;
+    static batchBalancesOf(tokens: Token[], owner: Address): Promise<BatchBalancesResult>;
+    static batchDecryptBalancesAs(tokens: Token[], options: BatchDecryptAsOptions): Promise<Map<Address, bigint>>;
+    confidentialBalanceOf(owner: Address): Promise<Handle>;
     confidentialTransfer(to: Address, amount: bigint, options?: TransferOptions): Promise<TransactionResult>;
     confidentialTransferFrom(from: Address, to: Address, amount: bigint, callbacks?: TransferCallbacks): Promise<TransactionResult>;
-    delegateDecryption(input: {
-        delegateAddress: Address;
-        expirationDate?: Date;
-    }): Promise<TransactionResult>;
-    finalizeUnwrap(unwrapRequestIdOrAmount: Handle): Promise<TransactionResult>;
+    decimals(): Promise<number>;
+    decryptBalanceAs(input: {
+        delegatorAddress: Address;
+        accountAddress?: Address;
+    }): Promise<bigint>;
+    // @internal
+    protected emit(input: ZamaSDKEventInput): void;
+    isConfidential(): Promise<boolean>;
     isOperator(holder: Address, spender: Address): Promise<boolean>;
-    isPayable(): Promise<boolean>;
-    resumeUnshield(unwrapTxHash: Hex, callbacks?: UnshieldCallbacks): Promise<TransactionResult>;
-    revokeDelegation(input: {
-        delegateAddress: Address;
-    }): Promise<TransactionResult>;
-    setOperator(operator: Address, until?: number): Promise<TransactionResult>;
-    shield(amount: bigint, options?: ShieldOptions): Promise<TransactionResult>;
-    unshield(amount: bigint, options?: UnshieldOptions): Promise<TransactionResult>;
-    unshieldAll(callbacks?: UnshieldCallbacks): Promise<TransactionResult>;
-    unwrap(amount: bigint): Promise<TransactionResult>;
-    unwrapAll(): Promise<TransactionResult>;
-    // (undocumented)
-    readonly wrapper: Address;
+    isWrapper(): Promise<boolean>;
+    name(): Promise<string>;
+    // @internal
+    protected readConfidentialBalanceOf(owner: Address): Promise<Handle>;
     // (undocumented)
-    static readonly ZERO_ADDRESS: Address;
+    readonly sdk: ZamaSDK;
+    setOperator(operator: Address, until?: number): Promise<TransactionResult>;
+    symbol(): Promise<string>;
 }
 
 // @public (undocumented)
@@ -883,15 +836,13 @@
     owner?: Address;
     // (undocumented)
     query?: Record<string, unknown>;
-    // (undocumented)
-    wrapperAddress?: Address;
 }
 
 // @public (undocumented)
 export function underlyingAllowanceQueryOptions(sdk: ZamaSDK, tokenAddress: Address, config: UnderlyingAllowanceQueryConfig): QueryFactoryOptions<bigint, Error, bigint, ReturnType<typeof zamaQueryKeys.underlyingAllowance.scope>>;
 
 // @public (undocumented)
-export function unshieldAllMutationOptions(token: Token): MutationFactoryOptions<readonly ["zama.unshieldAll", Address], UnshieldAllParams | void, TransactionResult>;
+export function unshieldAllMutationOptions(token: WrappedToken): MutationFactoryOptions<readonly ["zama.unshieldAll", Address], UnshieldAllParams | void, TransactionResult>;
 
 // @public
 export interface UnshieldAllParams extends UnshieldCallbacks {}
@@ -904,7 +855,7 @@
 }
 
 // @public (undocumented)
-export function unshieldMutationOptions(token: Token): MutationFactoryOptions<readonly ["zama.unshield", Address], UnshieldParams, TransactionResult>;
+export function unshieldMutationOptions(token: WrappedToken): MutationFactoryOptions<readonly ["zama.unshield", Address], UnshieldParams, TransactionResult>;
 
 // @public
 export interface UnshieldOptions extends UnshieldCallbacks {
@@ -940,7 +891,7 @@
 }
 
 // @public (undocumented)
-export function unwrapAllMutationOptions(token: Token): MutationFactoryOptions<readonly ["zama.unwrapAll", Address], void, TransactionResult>;
+export function unwrapAllMutationOptions(token: WrappedToken): MutationFactoryOptions<readonly ["zama.unwrapAll", Address], void, TransactionResult>;
 
 // @public
 export interface UnwrapFinalizedEvent {
@@ -953,7 +904,7 @@
 }
 
 // @public (undocumented)
-export function unwrapMutationOptions(token: Token): MutationFactoryOptions<readonly ["zama.unwrap", Address], UnwrapParams, TransactionResult>;
+export function unwrapMutationOptions(token: WrappedToken): MutationFactoryOptions<readonly ["zama.unwrap", Address], UnwrapParams, TransactionResult>;
 
 // @public
 export interface UnwrapParams {
@@ -1063,6 +1014,21 @@
     readonly to: Address;
 }
 
+// @public
+export class WrappedToken extends Token {
+    allowance(owner: Address): Promise<bigint>;
+    approveUnderlying(amount?: bigint): Promise<TransactionResult>;
+    finalizeUnwrap(unwrapRequestIdOrAmount: Handle): Promise<TransactionResult>;
+    isPayable(): Promise<boolean>;
+    resumeUnshield(unwrapTxHash: Hex, callbacks?: UnshieldCallbacks): Promise<TransactionResult>;
+    shield(amount: bigint, options?: ShieldOptions): Promise<TransactionResult>;
+    underlying(): Promise<Address>;
+    unshield(amount: bigint, options?: UnshieldOptions): Promise<TransactionResult>;
+    unshieldAll(callbacks?: UnshieldCallbacks): Promise<TransactionResult>;
+    unwrap(amount: bigint): Promise<TransactionResult>;
+    unwrapAll(): Promise<TransactionResult>;
+}
+
 // @public (undocumented)
 export interface WrapperDiscoveryQueryConfig {
     erc20Address?: Address;
@@ -1163,8 +1129,7 @@
         readonly token: (tokenAddress: Address) => readonly ["zama.underlyingAllowance", {
             readonly tokenAddress: `0x${string}`;
         }];
-        readonly scope: (tokenAddress: Address, owner?: Address, wrapperAddress?: Address) => readonly ["zama.underlyingAllowance", {
-            readonly wrapperAddress?: `0x${string}` | undefined;
+        readonly scope: (tokenAddress: Address, owner?: Address) => readonly ["zama.underlyingAllowance", {
             readonly owner?: `0x${string}` | undefined;
             readonly tokenAddress: `0x${string}`;
         }];
@@ -1303,8 +1268,8 @@
     allow(contracts: Address[]): Promise<void>;
     allowAs(delegator: Address, contracts: Address[]): Promise<void>;
     clearCredentials(): Promise<void>;
-    createReadonlyToken(address: Address): ReadonlyToken;
-    createToken(address: Address, wrapper?: Address): Token;
+    createToken(address: Address): Token;
+    createWrappedToken(address: Address): WrappedToken;
     createWrappersRegistry(registryAddresses?: Record<number, Address>): WrappersRegistry;
     // Warning: (ae-forgotten-export) The symbol "BatchDecryptHandlesResult" needs to be exported by the entry point index.d.ts
     //
@@ -1393,9 +1358,9 @@
 
 // Warnings were encountered during analysis:
 //
-// dist/esm/types-T0tr_-1R.d.ts:561:3 - (ae-forgotten-export) The symbol "FheChain" needs to be exported by the entry point index.d.ts
-// dist/esm/types-T0tr_-1R.d.ts:562:3 - (ae-forgotten-export) The symbol "RelayerDispatcher" needs to be exported by the entry point index.d.ts
-// dist/esm/types-T0tr_-1R.d.ts:563:3 - (ae-forgotten-export) The symbol "GenericProvider" needs to be exported by the entry point index.d.ts
+// dist/esm/types-57wtSyfb.d.ts:561:3 - (ae-forgotten-export) The symbol "FheChain" needs to be exported by the entry point index.d.ts
+// dist/esm/types-57wtSyfb.d.ts:562:3 - (ae-forgotten-export) The symbol "RelayerDispatcher" needs to be exported by the entry point index.d.ts
+// dist/esm/types-57wtSyfb.d.ts:563:3 - (ae-forgotten-export) The symbol "GenericProvider" needs to be exported by the entry point index.d.ts
 
 // (No @packageDocumentation comment for this package)
sdk.api.md
--- a/sdk.api.md
+++ b/sdk.api.md
@@ -13075,47 +13075,6 @@
 export type ReadFunctionName<TAbi extends ContractAbi = ContractAbi> = ContractFunctionName<TAbi, "pure" | "view">;
 
 // @public
-export class ReadonlyToken {
-    constructor(sdk: ZamaSDK, address: Address);
-    // (undocumented)
-    readonly address: Address;
-    allow(): Promise<void>;
-    static allow(...tokens: ReadonlyToken[]): Promise<void>;
-    allowance(wrapper: Address, owner: Address): Promise<bigint>;
-    balanceOf(owner: Address): Promise<bigint>;
-    static batchBalancesOf(tokens: ReadonlyToken[], owner: Address): Promise<BatchBalancesResult>;
-    static batchDecryptBalancesAs(tokens: ReadonlyToken[], options: BatchDecryptAsOptions): Promise<Map<Address, bigint>>;
-    confidentialBalanceOf(owner: Address): Promise<Handle>;
-    decimals(): Promise<number>;
-    decryptBalanceAs(input: {
-        delegatorAddress: Address;
-        accountAddress?: Address;
-    }): Promise<bigint>;
-    protected emit(input: ZamaSDKEventInput): void;
-    // (undocumented)
-    protected getAclAddress(): Promise<Address>;
-    getDelegationExpiry(input: {
-        delegatorAddress: Address;
-        delegateAddress: Address;
-    }): Promise<bigint>;
-    isAllowed(): Promise<boolean>;
-    isConfidential(): Promise<boolean>;
-    isDelegated(params: {
-        delegatorAddress: Address;
-        delegateAddress: Address;
-    }): Promise<boolean>;
-    isWrapper(): Promise<boolean>;
-    name(): Promise<string>;
-    // (undocumented)
-    protected readConfidentialBalanceOf(owner: Address): Promise<Handle>;
-    revoke(): Promise<void>;
-    // (undocumented)
-    readonly sdk: ZamaSDK;
-    symbol(): Promise<string>;
-    underlyingToken(): Promise<Address>;
-}
-
-// @public
 export interface RelayerConfig {
     readonly createRelayer: (chain: FheChain, worker: any) => RelayerSDK;
     readonly createWorker?: (chains: FheChain[]) => any;
@@ -14863,41 +14822,35 @@
 };
 
 // @public
-export class Token extends ReadonlyToken {
-    constructor(sdk: ZamaSDK, address: Address, wrapper?: Address);
-    approveUnderlying(amount?: bigint): Promise<TransactionResult>;
-    static batchDelegateDecryption(input: {
-        tokens: Token[];
-        delegateAddress: Address;
-        expirationDate?: Date;
-    }): Promise<Map<Address, TransactionResult | ZamaError>>;
-    static batchRevokeDelegation(input: {
-        tokens: Token[];
-        delegateAddress: Address;
-    }): Promise<Map<Address, TransactionResult | ZamaError>>;
+export class Token {
+    constructor(sdk: ZamaSDK, address: Address);
+    // (undocumented)
+    readonly address: Address;
+    // @internal
+    protected assertConfidentialBalance(amount: bigint): Promise<void>;
+    balanceOf(owner: Address): Promise<bigint>;
+    static batchBalancesOf(tokens: Token[], owner: Address): Promise<BatchBalancesResult>;
+    static batchDecryptBalancesAs(tokens: Token[], options: BatchDecryptAsOptions): Promise<Map<Address, bigint>>;
+    confidentialBalanceOf(owner: Address): Promise<Handle>;
     confidentialTransfer(to: Address, amount: bigint, options?: TransferOptions): Promise<TransactionResult>;
     confidentialTransferFrom(from: Address, to: Address, amount: bigint, callbacks?: TransferCallbacks): Promise<TransactionResult>;
-    delegateDecryption(input: {
-        delegateAddress: Address;
-        expirationDate?: Date;
-    }): Promise<TransactionResult>;
-    finalizeUnwrap(unwrapRequestIdOrAmount: Handle): Promise<TransactionResult>;
+    decimals(): Promise<number>;
+    decryptBalanceAs(input: {
+        delegatorAddress: Address;
+        accountAddress?: Address;
+    }): Promise<bigint>;
+    // @internal
+    protected emit(input: ZamaSDKEventInput): void;
+    isConfidential(): Promise<boolean>;
     isOperator(holder: Address, spender: Address): Promise<boolean>;
-    isPayable(): Promise<boolean>;
-    resumeUnshield(unwrapTxHash: Hex, callbacks?: UnshieldCallbacks): Promise<TransactionResult>;
-    revokeDelegation(input: {
-        delegateAddress: Address;
-    }): Promise<TransactionResult>;
-    setOperator(operator: Address, until?: number): Promise<TransactionResult>;
-    shield(amount: bigint, options?: ShieldOptions): Promise<TransactionResult>;
-    unshield(amount: bigint, options?: UnshieldOptions): Promise<TransactionResult>;
-    unshieldAll(callbacks?: UnshieldCallbacks): Promise<TransactionResult>;
-    unwrap(amount: bigint): Promise<TransactionResult>;
-    unwrapAll(): Promise<TransactionResult>;
-    // (undocumented)
-    readonly wrapper: Address;
+    isWrapper(): Promise<boolean>;
+    name(): Promise<string>;
+    // @internal
+    protected readConfidentialBalanceOf(owner: Address): Promise<Handle>;
     // (undocumented)
-    static readonly ZERO_ADDRESS: Address;
+    readonly sdk: ZamaSDK;
+    setOperator(operator: Address, until?: number): Promise<TransactionResult>;
+    symbol(): Promise<string>;
 }
 
 // @public
@@ -19897,6 +19850,21 @@
 }
 
 // @public
+export class WrappedToken extends Token {
+    allowance(owner: Address): Promise<bigint>;
+    approveUnderlying(amount?: bigint): Promise<TransactionResult>;
+    finalizeUnwrap(unwrapRequestIdOrAmount: Handle): Promise<TransactionResult>;
+    isPayable(): Promise<boolean>;
+    resumeUnshield(unwrapTxHash: Hex, callbacks?: UnshieldCallbacks): Promise<TransactionResult>;
+    shield(amount: bigint, options?: ShieldOptions): Promise<TransactionResult>;
+    underlying(): Promise<Address>;
+    unshield(amount: bigint, options?: UnshieldOptions): Promise<TransactionResult>;
+    unshieldAll(callbacks?: UnshieldCallbacks): Promise<TransactionResult>;
+    unwrap(amount: bigint): Promise<TransactionResult>;
+    unwrapAll(): Promise<TransactionResult>;
+}
+
+// @public
 export class WrappersRegistry {
     constructor(config: WrappersRegistryConfig);
     getAddress(chainId: number): Address | undefined;
@@ -20059,8 +20027,8 @@
     allow(contracts: Address[]): Promise<void>;
     allowAs(delegator: Address, contracts: Address[]): Promise<void>;
     clearCredentials(): Promise<void>;
-    createReadonlyToken(address: Address): ReadonlyToken;
-    createToken(address: Address, wrapper?: Address): Token;
+    createToken(address: Address): Token;
+    createWrappedToken(address: Address): WrappedToken;
     createWrappersRegistry(registryAddresses?: Record<number, Address>): WrappersRegistry;
     // @internal (undocumented)
     delegatedBatchDecryptHandlesAs(input: {

@github-actions

github-actions Bot commented May 6, 2026

Copy link
Copy Markdown

Coverage Report

Status Category Percentage Covered / Total
🔵 Lines 91.76% (🎯 80%) 3107 / 3386
🔵 Statements 91.82% 3202 / 3487
🔵 Functions 92.64% (🎯 80%) 1020 / 1101
🔵 Branches 84.63% (🎯 80%) 1212 / 1432
File Coverage
File Stmts Branches Functions Lines Uncovered Lines
Changed Files
packages/react-sdk/src/balance/use-confidential-balance.ts 100% 100% 100% 100%
packages/react-sdk/src/balance/use-confidential-balances.ts 100% 100% 100% 100%
packages/react-sdk/src/delegation/use-batch-decrypt-balances-as.ts 0% 100% 0% 0% 1-32
packages/react-sdk/src/delegation/use-decrypt-balance-as.ts 100% 100% 100% 100%
packages/react-sdk/src/delegation/use-delegate-decryption.ts 100% 100% 100% 100%
packages/react-sdk/src/delegation/use-revoke-delegation.ts 100% 100% 100% 100%
packages/react-sdk/src/operator/use-confidential-is-operator.ts 100% 75% 100% 100%
packages/react-sdk/src/operator/use-confidential-set-operator.ts 100% 100% 100% 100%
packages/react-sdk/src/shield/use-approve-underlying.ts 100% 100% 100% 100%
packages/react-sdk/src/shield/use-shield.ts 100% 100% 100% 100%
packages/react-sdk/src/shield/use-underlying-allowance.ts 100% 75% 100% 100%
packages/react-sdk/src/token/use-token.ts 100% 100% 100% 100%
packages/react-sdk/src/token/use-wrapped-token.ts 100% 100% 100% 100%
packages/react-sdk/src/transfer/use-confidential-transfer-from.ts 100% 100% 100% 100%
packages/react-sdk/src/transfer/use-confidential-transfer.ts 100% 100% 100% 100%
packages/react-sdk/src/unshield/use-resume-unshield.ts 100% 100% 100% 100%
packages/react-sdk/src/unshield/use-unshield-all.ts 100% 100% 100% 100%
packages/react-sdk/src/unshield/use-unshield.ts 100% 100% 100% 100%
packages/react-sdk/src/unwrap/use-finalize-unwrap.ts 100% 100% 100% 100%
packages/react-sdk/src/unwrap/use-unwrap-all.ts 100% 100% 100% 100%
packages/react-sdk/src/unwrap/use-unwrap.ts 100% 100% 100% 100%
packages/sdk/src/test-fixtures.ts 92.22% 77.1% 88.46% 93.25% 179-180, 406, 420-424
packages/sdk/src/zama-sdk.ts 85.71% 83.33% 87.87% 86.45% 75, 199, 256, 260, 283-286, 377-401, 584-588
packages/sdk/src/contracts/erc165.ts 100% 100% 100% 100%
packages/sdk/src/query/approve-underlying.ts 100% 100% 100% 100%
packages/sdk/src/query/batch-decrypt-balances-as.ts 0% 100% 0% 0% 15-17
packages/sdk/src/query/confidential-balance.ts 100% 100% 100% 100%
packages/sdk/src/query/confidential-balances.ts 100% 100% 100% 100%
packages/sdk/src/query/decrypt-balance-as.ts 100% 100% 100% 100%
packages/sdk/src/query/delegate-decryption.ts 100% 100% 100% 100%
packages/sdk/src/query/delegation-status.ts 100% 100% 100% 100%
packages/sdk/src/query/finalize-unwrap.ts 100% 100% 100% 100%
packages/sdk/src/query/query-keys.ts 95.83% 91.17% 96.96% 95.74% 156-157
packages/sdk/src/query/resume-unshield.ts 100% 100% 100% 100%
packages/sdk/src/query/revoke-delegation.ts 100% 100% 100% 100%
packages/sdk/src/query/shield.ts 100% 100% 100% 100%
packages/sdk/src/query/underlying-allowance.ts 100% 100% 100% 100%
packages/sdk/src/query/unshield-all.ts 100% 100% 100% 100%
packages/sdk/src/query/unshield.ts 100% 100% 100% 100%
packages/sdk/src/query/unwrap-all.ts 100% 100% 100% 100%
packages/sdk/src/query/unwrap.ts 100% 100% 100% 100%
packages/sdk/src/token/token.ts 88.38% 69.41% 100% 87.95% 161, 263, 281, 293-305, 311-312, 359-361, 371, 395, 403-409, 463, 473, 479-487, 607, 633, 766-768
packages/sdk/src/token/wrapped-token.ts 92.81% 80.95% 100% 92.61% 147, 200, 237, 295, 473-483, 528, 572-577, 641-643
packages/sdk/src/types/provider.ts 100% 100% 100% 100%
packages/sdk/src/types/token.ts 100% 100% 100% 100%
Generated in workflow #2247 for commit 4315c7a by the Vitest Coverage Report Action

Comment thread packages/sdk/src/utils/safe-callback.ts Outdated
@zama-cremaud
zama-cremaud force-pushed the feature/sdk-96-feat-restructure-token-hierarchy-into-token-wrappedtoken branch from a23faa9 to 1ad28cc Compare May 6, 2026 12:59
@enitrat enitrat changed the title feat!: split Token into Token + WrappedToken hierarchy [SDK-96] feat: split Token into Token + WrappedToken hierarchy [SDK-96] May 6, 2026
@enitrat enitrat changed the title feat: split Token into Token + WrappedToken hierarchy [SDK-96] refactor: split Token into Token + WrappedToken hierarchy [SDK-96] May 6, 2026
zama-cremaud and others added 5 commits May 6, 2026 15:13
Restructures the high-level token API to mirror the ERC-7984 contract
architecture: `Token` for the base confidential token, `WrappedToken`
extending it with wrapper-specific operations.

- `Token`: read + base writes (balance, transfer, set/isOperator,
  decryptBalanceAs, batchBalancesOf, batchDecryptBalancesAs)
- `WrappedToken extends Token`: shield, unshield, unshieldAll,
  resumeUnshield, unwrap, unwrapAll, finalizeUnwrap, allowance,
  approveUnderlying, underlying

`ReadonlyToken` is removed; reads live on `Token`. `createToken(addr)`
takes a single address (no wrapper param); new `createWrappedToken(addr)`
returns a `WrappedToken` — the wrapper IS the confidential token.

Mutation factories (shield, unshield, unwrap, finalize, approveUnderlying,
resumeUnshield) now take a `WrappedToken`. `delegateDecryption` /
`revokeDelegation` mutation factories now take `(sdk, contractAddress)`,
matching the SDK-level primitives added in SDK-95.

Tests updated in-place; `delegation.test.ts` and `readonly-token.test.ts`
are removed (coverage migrated to `zama-sdk.test.ts`, `token.test.ts`,
`batch-decrypt-as.test.ts`); new `wrapped-token.test.ts` covers the
wrapper-specific surface.

Migration: `ReadonlyToken`, `createReadonlyToken`, and the two-argument
`createToken(addr, wrapper)` are removed. Callers that need wrapper
operations should use `createWrappedToken(wrapperAddr)`.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Mirrors the SDK-side refactor in the React layer.

- `useToken(address)`: returns the base `Token` (single-arg)
- `useWrappedToken(address)`: new hook returning a `WrappedToken`
- `useReadonlyToken` is removed — reads live on `Token`

Hook signatures unified:
- single-arg: `useDelegateDecryption`, `useRevokeDelegation`,
  `useDecryptBalanceAs`, `useApproveUnderlying`, `useUnshield`,
  `useUnshieldAll`, `useResumeUnshield`, `useUnwrap`, `useUnwrapAll`,
  `useFinalizeUnwrap`, `useConfidentialTransferFrom`,
  `useConfidentialSetOperator`
- config object `{ address, ... }`: `useConfidentialBalance`,
  `useConfidentialBalances` (`addresses`), `useConfidentialTransfer`,
  `useShield`, `useConfidentialIsOperator`, `useUnderlyingAllowance`

`UseZamaConfig` is removed from the public barrel.

Migration: hook signatures change as listed above; `useReadonlyToken`
is removed. Migrate to `useToken` (reads) or `useWrappedToken` (wrapper
ops). Object-form callers should rename `tokenAddress` → `address` and
drop the now-unused `wrapperAddress` (the wrapper IS the token).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The wrapper IS the confidential token, so the harness pages no longer
need a separate `wrapperAddress` prop. Each page falls back to
`tokenAddress` and the forms drop the dual-address signature in favour
of the new single-arg / `{ address }` hook shapes.

Touches `test/test-components`, `test/test-nextjs`, and `test/test-vite`.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Removes the obsolete `ReadonlyToken` / `useReadonlyToken` reference
pages and adds placeholder pages for `WrappedToken` /
`useWrappedToken`. Full reference docs to follow once the API is
stabilised.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Move SDK-145's shield routing into WrappedToken (isPayable detection,
  #shieldViaTransferAndCall and #shieldViaApproveAndWrap branches).
- Update wrapped-token tests with the new isPayable read mocks.
- Switch shield.test.ts dynamic imports from Token to WrappedToken for
  the shieldPath event tests.
- Regenerate API reports + LLM corpus artifacts.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@zama-cremaud
zama-cremaud force-pushed the feature/sdk-96-feat-restructure-token-hierarchy-into-token-wrappedtoken branch from 1ad28cc to 0251d20 Compare May 6, 2026 13:20
zama-cremaud and others added 8 commits May 6, 2026 17:05
- wrapped-token: rewrite shield() JSDoc to document ERC-1363 routing and fix "Unshield"/"Unshield-all" error messages to "Unwrap"/"UnwrapAll"
- delegation-check, index.ts: update JSDoc references from ReadonlyToken to ZamaSDK/WrappedToken
- test/playwright/node: replace createReadonlyToken with createToken (regression fix lost in last rebase)
- docs sweep: AGENTS.md, jsdoc skill, agent docs, gitbook references/guides/tutorials, packages/sdk README — finalize ReadonlyToken → Token/WrappedToken cleanup
- regenerate llms.txt, llms-full.txt, corpus-manifest.json

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…warn, diagrams

- isPayable() no longer caches false on probe error; re-probes on next call,
  with a console.warn so transient RPC failures stay visible
- Architecture diagrams: ReadonlyToken → WrappedToken (extends Token)
- use-shield tests: drop dead wrapperAddress field
- 5 guides + 2 tutorials + 17 reference docs aligned with the new
  Token / WrappedToken hierarchy and positional / config-object hook signatures
- Regenerate llms-full.txt from the updated guide and reference content

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…hierarchy-into-token-wrappedtoken

Resolves conflicts after #325 (centralize Zod validation at public boundaries):

- ReadonlyToken.md, readonly-token.test.ts: kept SDK-96's deletion (class
  no longer exists in the new Token / WrappedToken hierarchy)
- query/index.ts: keep WrappedToken export, drop ZamaSDKConfig export (the
  type was inlined / removed by #325)
- token.test.ts: import the new ERC-20 / unshield validation tests added
  by #325 only where they apply to Token (confidentialTransfer cache and
  boundary cases). shield / unshield variants migrated to wrapped-token.test.ts
- wrapped-token.test.ts: add the new boundary + skipBalanceCheck-with-callbacks
  tests (preserves coverage from #325 on the right class)
- optional-signer.test.ts: drop the keypairTTL-rejection test — #325 moved
  that validation from the ZamaSDK constructor to buildZamaConfig; coverage
  remains in config/__tests__/schema.test.ts
- Token.md: reword create example so it doesn't reference Token.shield
  (now on WrappedToken)
- llms-full.txt: regenerated

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…hierarchy-into-token-wrappedtoken

Reconcile SDK-96 Token/WrappedToken split with prerelease's services
fanout (#325, #331, #333) and CVE-2026-31431 workaround. Adopt the new
DecryptionService / DelegationService / EncryptionService / CachingService
architecture in ZamaSDK while preserving SDK-96's typed createToken /
createWrappedToken API. Delete ReadonlyToken artifacts (class, tests,
docs) replaced by the Token/WrappedToken hierarchy, and migrate the
token-bound delegation tests to Token.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…hierarchy-into-token-wrappedtoken

Absorb latest prerelease commits (#322 docs terminology update, #332
react-viem upgrade, #336 next bump). Reconcile docs conflicts:
architecture.md adopts the KeypairVault + PermissionStore terminology
while keeping Token / WrappedToken hierarchy; Token.md merges the new
detailed reference with the SDK-96 split (WrappedToken-only methods
moved out); llms-full.txt regenerated from corpus.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Auto-fix formatting on three files flagged by `pnpm format:check`
in CI after the prerelease merge.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The previous merge commit regenerated llms-full.txt before oxfmt
normalized two source guides — pick up the post-format snapshot.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@zama-cremaud
zama-cremaud marked this pull request as ready for review May 11, 2026 13:02

@ghermet ghermet left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Automated multi-agent review (code-reviewer, pr-test-analyzer, silent-failure-hunter, type-design-analyzer, comment-analyzer). Findings are filtered to high-confidence items; please treat them as starting points, not verdicts.

PR #323 Review Summary — Token / WrappedToken Split

Stats: 172 files, +3474 / −5928


🔴 Critical (must fix before merge)

# Source Issue Location
C1 tests integration.test.ts is brokenTypeError: token.unwrap is not a function. The fixture still binds token = new Token(...), but shield/unwrap/unshield/finalizeUnwrap live on WrappedToken. Vitest unit config excludes it (CI hides this), but vitest.integration.config.ts includes it. packages/sdk/src/token/__tests__/integration.test.ts:14,104,110,153,177
C2 code-review ZamaSDK.cache made public so Token can reach this.sdk.cache.get(...). API extractor raises ae-forgotten-export: CachingService. Violates the "no handle leakage; SDK cache owns two-phase optimization" rule. packages/sdk/src/zama-sdk.ts:55-56,68; packages/sdk/src/token/token.ts:229,397; packages/sdk/etc/sdk.api.md:20029
C3 code-review useUnderlyingAllowance passes the same address twice as tokenAddress and wrapperAddress — stale two-address query-key shape under the new single-address hook surface (a "silent migration shim"). packages/react-sdk/src/shield/use-underlying-allowance.ts:41-46,73-77; packages/sdk/src/query/query-keys.ts:91-104
C4 silent-failures WrappedToken.isPayable() silently routes to approve+wrap on ANY error, including #getUnderlying() rejections. A transient RPC blip silently extracts an extra approve tx and wallet popup from the user for an ERC-1363 token that should have one-tx-shielded. packages/sdk/src/token/wrapped-token.ts:74-90
C5 silent-failures AccountService.#handleWalletAccountChange fire-and-forgets relayer chain switchvoid swallow("switch relayer chain", () => this.#relayer.switchChain(nextChainId)). If the switch fails, subsequent decrypts run against the previous chain's relayer and return stale plaintexts. Violates "RelayerNative parity". packages/sdk/src/services/account-service.ts:102-123
C6 silent-failures isFatalBatchError is too narrow — missing ChainMismatchError, WalletAccountNotReadyError, SignerNotConfiguredError. A session-level chain switch mid-batch produces "N of M tokens failed" results where every error is identical and meaningless. packages/sdk/src/errors/fatal-batch.ts:14-20
C7 comments Token.md documents a non-existent handle? parameter for decryptBalanceAs. Readers will pass it and silently lose the value. docs/gitbook/src/reference/sdk/Token.md:52
C8 comments unshield-tokens.md examples use sdk.createToken("0xEncryptedERC20").unshield(...) — a TypeScript error after the split (shield-tokens.md was updated, this guide was missed). Same applies to .unshieldAll() and .resumeUnshield(). docs/gitbook/src/guides/unshield-tokens.md:35-37,83,107

🟡 Important (should fix)

Architecture / types

  • Public constructors new Token(...) / new WrappedToken(...) bypass the SDK factories (zama-sdk.ts:248-250); no eager ERC-165 check inside createWrappedToken — plain ERC-20 addresses can be wrapped and only revert at first shield().
  • underlyingAllowanceQueryOptions(sdk, tokenAddress, config) is the only wrapper-related query that takes a bare address rather than (token: WrappedToken) — inconsistent with shieldMutationOptions, unshieldMutationOptions, etc. (packages/sdk/etc/sdk-query.api.md:844)
  • React hook surface re-erases wrapper-ness to a string: useShield/useUnshield/useUnwrap/etc. take address: Address, internally call useWrappedToken(address) — type discipline established in @zama-fhe/sdk/query doesn't survive the trip to react-sdk.

Error handling

  • WrappedToken.#waitAndFinalizeUnshield does not emit ZamaSDKEvents.TransactionError on failure (all other write paths do). Worst case: phase-1 receipt parse fails after on-chain unwrap succeeded — user loses the in-flight unshield with no event and a generic error message that doesn't mention resumeUnshield(hash). (wrapped-token.ts:557-596)
  • assertConfidentialBalance docstring promises "no surprise EIP-712 popup" but the implementation can trigger one when credentials aren't cached. (token.ts:686-689,693-717)
  • swallow() uses console.warn, not logError → invisible to Sentry. Used in 7+ load-bearing sites including chain switch and cache cleanup. (packages/sdk/src/utils/swallow.ts)
  • Token.batchBalancesOf total-failure aggregation throws only the first error; the rest are dropped, making debugging brutal. (token.ts:321-325)

Tests

  • No assertion that WrappedToken instanceof Token (or that inherited balanceOf works on a WrappedToken). Central design claim of the PR is unguarded against future refactors.
  • ZamaSDK.createWrappedToken has no direct factory test (matching the three createToken cases).
  • The three direct unit tests for Token.decryptBalanceAs from the deleted delegation.test.ts:117-164 (zero-handle short-circuit, happy path, "no value returned" error) were not migrated.
  • assertSameSdk cross-SDK guard tests deleted with no replacement.
  • New hooks useToken / useWrappedToken have no dedicated tests.

Code review

  • Token.confidentialTransferFrom uses requireChainAlignment, but confidentialTransfer uses requireAlignedWalletAccount — inconsistent pre-flight error story. (token.ts:502-503 vs :566-567)
  • Token class JSDoc says delegation lives on Token, but delegateDecryption/revokeDelegation/etc. were removed from the class surface and now live only on ZamaSDK. The PR description and token.ts:70-81 overstate the class's surface.

Comments / JSDoc

  • wrapped-token.ts:310,348Orchestrates: unshield → wait → ... finalize is self-referential (impl calls unwrap/unwrapAll).
  • @throws blocks dropped during migration across 8 methods (confidentialTransferFrom, setOperator, unshield, resumeUnshield, unwrap, unwrapAll, finalizeUnwrap, approveUnderlying).
  • use-confidential-transfer.ts:42 references "optional wrapper" leftover from the two-address model.

🟢 Suggestions

  • Resolve or remove the self-flagged TODO: code smell at token.ts:369 — refactor static batch methods to (sdk, tokens, options) so cache/sdk can be made private (folds in C2).
  • Consider protected on ZamaSDK.cache and Token.sdk (or narrow the published type via @internal).
  • Token.isWrapper uses Promise.all → rejects on first probe failure rather than returning false. Consider Promise.allSettled so it behaves like a probe.
  • unwrapAll zero-balance guard throws DecryptionFailedError — error type is misleading. Use InsufficientConfidentialBalanceError or new ZeroBalanceError. (wrapped-token.ts:460-462)
  • Section-banner comments (// METADATA, // BALANCES, // SHIELD (ERC-20 → confidential), …) violate "don't explain WHAT" guidance and add no info beyond the JSDoc on each method.
  • Remove the trivial restatement docstrings (/** Read the token name from the contract. */).

✅ Strengths

  • No legacy shims, no compat re-exports. ReadonlyToken / createReadonlyToken / useReadonlyToken removed cleanly — no ReadonlyToken references survive in .api.md.
  • Constructor consistency: both classes use (sdk, address); no separate wrapperAddress field anywhere; address normalization preserved in query keys.
  • Type-system-enforced separation in @zama-fhe/sdk/query: shieldMutationOptions, unshieldMutationOptions, finalizeUnwrapMutationOptions, … are typed token: WrappedToken. A plain Token instance fails at compile time.
  • Token and WrappedToken class-level JSDoc clearly states the split; "the wrapper IS the confidential token" kills the two-address mental model.
  • WrappedToken.isPayable JSDoc was updated to match the new "don't cache failures" behavior — textbook good behavior + comment migration.
  • Load-bearing rationale preserved: e.g. // ERC7984ERC20Wrapper.onTransferReceived decodes the recipient via 'address(bytes20(data))' correctly migrated to wrapped-token.ts:182-185; the sequential-write rationale on #batchDelegationOp preserved.
  • Test fixtures factorized well: mockToken and mockWrappedToken share createMockTokenInternal.
  • Shield routing rule honored: no per-call routing flag on ShieldOptions; path chosen via ERC-165 introspection only.
  • chain-alignment.test.ts migration is complete: shield/unwrap go through createWrappedToken, delegateDecryption moved onto SDK, batchBalancesOf stayed on Token.
  • Type-design ratings: 4 / 4 / 4 / 3 (Encapsulation, Invariants, Usefulness, Enforcement). Clear improvement over the prior optional-wrapper model.

Recommended Action Plan

  1. Block merge on C1–C8. C1 is a hard test regression; C2 is a CI-visible API contract break; C3 is a query-key shape bug; C4–C6 are silent-failure paths that mislead users; C7–C8 are docs that don't compile.
  2. Important issues in priority order: emit TransactionError from #waitAndFinalizeUnshield; fix assertConfidentialBalance doc-vs-impl drift; route swallow through logError; add the missing tests (instanceof Token, createWrappedToken factory, decryptBalanceAs direct, hook memoization).
  3. Re-run pnpm test:run and pnpm test:integration (the integration run is what catches C1) before re-requesting review.

@enitrat enitrat left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

some regressions observed, globally this PR should not be a lot more than taking existing functions and moving them around files

Comment thread packages/sdk/src/zama-sdk.ts Outdated
Comment thread packages/sdk/src/token/token.ts Outdated
Comment thread packages/sdk/src/token/token.ts
Comment thread packages/sdk/src/token/token.ts Outdated
Comment thread packages/sdk/src/token/wrapped-token.ts Outdated
…K public API

Apply changes from PR #323 review (enitrat + ghermet multi-agent review):

- Token.decryptBalanceAs now delegates entirely to sdk.delegatedUserDecrypt;
  the per-method cache lookup and #assertDelegationActive duplicated logic
  already handled by DecryptionService.
- Token.batchDecryptBalancesAs collapses to a single sdk.delegatedBatchDecryptHandlesAs
  call (instead of resolving handles, doing cache lookups, and looping per token).
- Remove #assertDelegationActive from Token (lives in DelegationService now).
- Repass ZamaSDK.cache to private (#cache); removes the api-extractor
  ae-forgotten-export warning on CachingService.
- WrappedToken.isPayable restores the negative-cache semantics from prerelease
  (probe failures cached as false, no permanent re-probing).
- useUnderlyingAllowance + zamaQueryKeys.underlyingAllowance.scope drop the
  duplicated wrapperAddress parameter (single-address model post-SDK-96).
- Fix Token.md (drop bogus handle? parameter from decryptBalanceAs signature)
  and unshield-tokens.md (use sdk.createWrappedToken).
- Fix integration.test.ts to use the wrappedToken fixture (and updated mock
  chains for the new shield path's ERC-20 balance check).
- Add tests: createWrappedToken factory (matching createToken trio),
  WrappedToken instanceof Token assertion, and three decryptBalanceAs direct
  unit tests (zero-handle short-circuit, happy path, no-value-returned error).
@zama-cremaud
zama-cremaud requested review from enitrat and ghermet May 11, 2026 18:17
Comment thread packages/sdk/src/token/token.ts
Comment thread packages/sdk/src/token/wrapped-token.ts
zama-cremaud and others added 2 commits May 12, 2026 10:21
…with prerelease

Address enitrat's review feedback on PR #323:

- `Token.batchDecryptBalancesAs`: rewrite to mirror prerelease's readonly-token
  implementation. Uses pLimit with maxConcurrency=10 default for parallel
  balance reads (was unbounded Promise.all), filters zero handles before
  contacting the relayer (skip-on-zero short-circuit), tracks balance-read
  failures in a Map<Address, ZamaError>, and always wraps errors in
  DecryptionFailedError with the underlying error as `.cause` (drops the
  size===1 special case).
- `WrappedToken.approveUnderlying`: use `sdk.requireAlignedWalletAccount`
  once at the top instead of split `requireSigner` + `requireChainAlignment`
  + `signer.requireWalletAccount`. Matches prerelease verbatim.
- Update 3 batch-decrypt-as tests to expect the wrapped DecryptionFailedError
  shape (with typed error preserved via `.cause`).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Resolve conflicts from prerelease's LifecycleService refactor (#337):

- `token.ts` / `wrapped-token.ts`: migrate from method-form
  `this.sdk.requireAlignedWalletAccount(op)` /
  `this.sdk.requireChainAlignment(op)` to standalone utility form
  `requireAlignedWalletAccount(op, signer, provider)` /
  `requireChainAlignment(op, signer, provider)` from `utils/alignment`,
  matching the rest of the SDK post-LifecycleService extraction.
- `zama-sdk.ts`: drop the inlined wallet-account listener registry
  (`#walletAccountListeners`) — LifecycleService owns it now.
- `test-fixtures.ts`: keep SDK-96's `createToken` / `createWrappedToken`
  fixtures, add prerelease's new `createLifecycleService` fixture; drop
  `createReadonlyToken` (ReadonlyToken removed in SDK-96).
- `zama-sdk.test.ts`: drop ReadonlyToken import and lifecycle test cases
  (now covered by the dedicated `lifecycle-service.test.ts` suite).
- `token/readonly-token.ts`: confirm deletion from SDK-96 (modify/delete
  conflict resolved in favor of the deletion).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@zama-cremaud
zama-cremaud requested a review from enitrat May 12, 2026 09:33
@enitrat

enitrat commented May 12, 2026

Copy link
Copy Markdown
Contributor

one last thing from the last merge commit from main is to use the new requireAlignedWalletAccount standalone function inside shield, unwrap, unwrapAll, #ensureAllowance, confidentialTransfer and confidentialTransferFrom

  • const account = await requireAlignedWalletAccount("shield", this.sdk.signer, this.sdk.provider);

and we will be good to merge

Address enitrat's review feedback on PR #323: use the standalone
`requireAlignedWalletAccount` from `utils/alignment` consistently across
signer-bound write paths, replacing the split
`requireChainAlignment` + `signer.requireWalletAccount` pattern.

- `WrappedToken.shield`: drop redundant `requireSigner` (private helpers
  re-acquire the signer) and source the user address from the aligned
  account.
- `WrappedToken.unwrap` / `WrappedToken.unwrapAll`: same consolidation,
  keeping `requireSigner` since those paths drive `signer.writeContract`
  directly.
- `WrappedToken.#ensureAllowance`: align the user-address lookup with the
  aligned account (chain mismatch was previously not surfaced here).
- `Token.confidentialTransferFrom`: use `requireAlignedWalletAccount` for
  consistency even though the operator's address is taken from the `from`
  argument.
- `Token.confidentialTransfer` already follows this pattern from the
  prerelease merge — no change needed.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@zama-cremaud
zama-cremaud merged commit 1683d9e into prerelease May 12, 2026
12 checks passed
@zama-cremaud
zama-cremaud deleted the feature/sdk-96-feat-restructure-token-hierarchy-into-token-wrappedtoken branch May 12, 2026 15:02
@zama-bot

Copy link
Copy Markdown
Collaborator

🎉 This PR is included in version 3.0.0-alpha.36 🎉

The release is available on GitHub release

Your semantic-release bot 📦🚀

@zama-bot

Copy link
Copy Markdown
Collaborator

🎉 This PR is included in version 3.1.0 🎉

The release is available on GitHub release

Your semantic-release bot 📦🚀

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants