Skip to content

Major refactoring and compatibility with dynamic bundle#22

Merged
jonathunne merged 41 commits intotetherto:mainfrom
nulllpc:nampc/dev
Feb 8, 2026
Merged

Major refactoring and compatibility with dynamic bundle#22
jonathunne merged 41 commits intotetherto:mainfrom
nulllpc:nampc/dev

Conversation

@nulllpc
Copy link
Copy Markdown
Contributor

@nulllpc nulllpc commented Jan 21, 2026

No description provided.

gatteo and others added 27 commits January 13, 2026 15:05
Remove the tight coupling to pear-wrk-wdk by accepting bundleConfig
(containing bundle and HRPC) as explicit function parameters instead
of importing from the package directly.

Key changes:
- WorkletLifecycleService.startWorklet now accepts bundleConfig as 2nd param
- WdkAppProvider passes bundleConfig directly to startWorklet
- ensureWorkletStarted simplified to just validate worklet is started
- Remove pear-wrk-wdk from dependencies
- Update all tests to use local mocks instead of pear-wrk-wdk imports
- Define HRPC types locally in src/types/hrpc.ts

This allows consumers to provide their own bundle and HRPC implementation,
making the library more flexible and decoupled.
- Add Bundle Configuration section explaining two options:
  - Option A: Generate custom bundle with wdk-worklet-bundler CLI
  - Option B: Use pre-built pear-wrk-wdk package
- Update all examples to include the required bundle prop
- Update WdkAppProvider API reference with full props interface
- Add TypeScript configuration guidance for generated bundles
Set SecureStorage in WalletSetupService synchronously during render
instead of in a useEffect. This prevents race conditions where child
components' useEffect hooks run before the parent's effect, causing
"SecureStorage not initialized" errors.
Update the `pear-wrk-wdk` dependency to the `nampc/dynamic-worklet`
branch. This change also involves removing unused dependency entries
from `package-lock.json`.
This commit introduces several improvements:

- **Generics for `callAccountMethod`**: The
  `AccountService.callAccountMethod` function now supports generic type
  parameters (`TMethods`, `K`) to enable strict typing of method
  arguments and return values. Developers can define custom `MethodMap`
  types for their applications, improving type safety and developer
  experience.
- **New Wallet Generation Utilities**: `useWalletManager` now exposes
  `generateEntropyAndEncrypt`, `getMnemonicFromEntropy`, and
  `getSeedAndEntropyFromMnemonic` functions. These utilities facilitate
  custom wallet creation flows, such as displaying mnemonics before
  saving or importing existing wallets.
- **Type Definitions**: New type definitions for `MethodMap` and
  `LooseMethods` are added to `src/types/accountMethods.ts` to support
  the generic typing of account methods.
- **Provider Props**: The `WdkAppProviderProps` interface now accepts a
  generic type parameter `TConfig` to allow for more specific network
  configurations.
- **Type Casting**: Minor type casting adjustments in `useWallet` and
  `walletUtils` to align with the new generic types.
- **Dependency Updates**: Removed unused dependencies from
  `package-lock.json`.
@alexszolowicz-blockether
Copy link
Copy Markdown

alexszolowicz-blockether commented Jan 28, 2026

I don't know what has been changed in this MR, but would be nice to have:

  1. multiple networks support (not only evm)
  2. initializeFromMnemonic does not update "state.activeWalletId" on main branch
  3. I had problems with setSecureStorage race condition
  4. autoStartWorklet as a boolean (so start when needed/invoked)
my adapters:
export interface WdkAppContextValue extends UpstreamWdkAppContextValue {
  /** Start worklet manually. Idempotent (safe to call multiple times), fire-and-forget. */
  startWorklet: () => void
}

export interface WdkAppProviderProps
  extends Omit<UpstreamWdkAppProviderProps, 'networkConfigs'> {
  networkConfigs: NetworkConfigs
  autoStartWorklet?: boolean
}

const WdkAppContext = createContext<WdkAppContextValue | null>(null)

function WdkAppContextBridge({
  networkConfigs,
  autoStartWorklet,
  children,
}: {
  networkConfigs: NetworkConfigs
  autoStartWorklet: boolean
  children: React.ReactNode
}) {
  const upstreamContext = useUpstreamWdkApp()
  const { isLoading: isWorkletLoading, isWorkletStarted } = useWorklet()

  const startWorklet = useCallback(() => {
    if (isWorkletLoading || isWorkletStarted) return
    WorkletLifecycleService.startWorklet(
      networkConfigs as unknown as UpstreamNetworkConfigs
    ).catch(console.error)
  }, [networkConfigs, isWorkletLoading, isWorkletStarted])

  useEffect(() => {
    if (!autoStartWorklet) return
    if (isWorkletLoading || isWorkletStarted) return
    WorkletLifecycleService.startWorklet(
      networkConfigs as unknown as UpstreamNetworkConfigs
    ).catch(console.error)
  }, [autoStartWorklet, isWorkletLoading, isWorkletStarted, networkConfigs])

  const contextValue: WdkAppContextValue = useMemo(
    () => ({
      ...upstreamContext,
      startWorklet,
    }),
    [upstreamContext, startWorklet]
  )

  return (
    <WdkAppContext.Provider value={contextValue}>
      {children}
    </WdkAppContext.Provider>
  )
}

export function WdkAppProvider({
  autoStartWorklet = false,
  children,
  ...upstreamProps
}: WdkAppProviderProps) {
  // CRITICAL: Set secure storage synchronously before any children mount
  // This prevents "SecureStorage not initialized" race condition
  useMemo(() => {
    const storage = createSecureStorage()
    WalletSetupService.setSecureStorage(storage)
    return storage
  }, [])

  return (
    <UpstreamWdkAppProvider
      {...upstreamProps}
      networkConfigs={
        upstreamProps.networkConfigs as unknown as UpstreamNetworkConfigs
      }
    >
      <WdkAppContextBridge
        networkConfigs={upstreamProps.networkConfigs}
        autoStartWorklet={autoStartWorklet}
      >
        {children}
      </WdkAppContextBridge>
    </UpstreamWdkAppProvider>
  )
}

export function useWdkApp(): WdkAppContextValue {
  const context = useContext(WdkAppContext)
  if (!context) {
    throw new Error('useWdkApp must be used within WdkAppProvider')
  }
  return context
}

export { WdkAppContext }
export function useWalletManager(
  walletId?: string,
  networkConfigs?: Parameters<typeof useUpstreamWalletManager>[1]
): UseWalletManagerResult {
  const upstream = useUpstreamWalletManager(walletId, networkConfigs)
  const walletStore = getWalletStore()

  const initializeFromMnemonic = useCallback(
    async (mnemonic: string, walletIdParam?: string) => {
      const targetWalletId = walletIdParam ?? walletId ?? 'default'

      await upstream.initializeFromMnemonic(mnemonic, walletIdParam)

      walletStore.setState(prev =>
        produce(prev, (state: WalletStoreState) => {
          const existingIndex = state.walletList.findIndex(
            w => w.identifier === targetWalletId
          )

          if (existingIndex >= 0) {
            state.walletList[existingIndex] = {
              identifier: targetWalletId,
              exists: true,
              isActive: true,
            }
          } else {
            state.walletList.push({
              identifier: targetWalletId,
              exists: true,
              isActive: true,
            })
          }

          for (const wallet of state.walletList) {
            if (wallet.identifier !== targetWalletId) {
              wallet.isActive = false
            }
          }

          state.activeWalletId = targetWalletId
        })
      )
    },
    [upstream.initializeFromMnemonic, walletId, walletStore]
  )

  return useMemo(
    () => ({
      ...upstream,
      initializeFromMnemonic,
    }),
    [upstream, initializeFromMnemonic]
  )
}

export type { UseWalletManagerResult }

@alexszolowicz-blockether
Copy link
Copy Markdown

alexszolowicz-blockether commented Jan 28, 2026

btw please also take note on balance fetching, because right now N tokens = N rpc requests.

You can use ext-provider-multicall on evm and findAssociatedTokenPda + getMultipleAccounts on solana

my implementation
/**
   * @param {string[]} tokenAddresses
   * @returns {Promise<Record<string, bigint>>}
   */
  async getTokenBalances(tokenAddresses) {
    if (!this._provider) {
      throw new Error(
        'The wallet must be connected to a provider to retrieve token balances.'
      )
    }

    const address = await this.getAddress()
    const abi = ['function balanceOf(address owner) view returns (uint256)']

    const multicallProvider = new MulticallProvider(this._provider)

    const balances = await Promise.all(
      tokenAddresses.map(async tokenAddress => {
        const contract = new Contract(tokenAddress, abi, multicallProvider)
        return contract.balanceOf(address)
      })
    )

    return tokenAddresses.reduce((acc, tokenAddress, index) => {
      acc[tokenAddress] = balances[index]
      return acc
    }, {})
  }
  /**
   * Returns the account balances for a list of SPL tokens.
   *
   * @param {string[]} tokenAddresses - The smart contract addresses of the tokens.
   * @returns {Promise<Record<string, bigint>>} The token balances (in base unit).
   */
  async getTokenBalances(tokenAddresses) {
    if (!this._rpc) {
      throw new Error(
        'The wallet must be connected to a provider to retrieve token balances.'
      )
    }

    const addr = await this.getAddress()
    const ownerAddress = address(addr)

    // Deduplicate token addresses
    const uniqueTokenAddresses = [...new Set(tokenAddresses)]
    const mints = uniqueTokenAddresses.map(t => address(t))

    // Calculate ATAs
    const atas = await Promise.all(
      mints.map(mint =>
        findAssociatedTokenPda({
          mint,
          owner: ownerAddress,
          tokenProgram: TOKEN_PROGRAM_ADDRESS,
        }).then(([ata]) => ata)
      )
    )

    // Fetch accounts
    const { value: accounts } = await this._rpc
      .getMultipleAccounts(atas, {
        commitment: this._commitment,
        encoding: 'base64',
      })
      .send()

    const balances = {}
    const base64Encoder = getBase64Encoder()

    for (let i = 0; i < uniqueTokenAddresses.length; i++) {
      const tokenAddress = uniqueTokenAddresses[i]
      const account = accounts[i]

      if (!account) {
        balances[tokenAddress] = 0n
        continue
      }

      // Parse amount from account data
      // Account data is [base64_string, 'base64']
      const dataBase64 = account.data[0]
      const bytes = base64Encoder.encode(dataBase64)

      // Amount is at offset 64, 8 bytes, little endian
      const view = new DataView(
        bytes.buffer,
        bytes.byteOffset,
        bytes.byteLength
      )
      const amount = view.getBigUint64(64, true)
      balances[tokenAddress] = amount
    }

    return balances
  }

multi-argument methods
@alexszolowicz-blockether
Copy link
Copy Markdown

please update AddressService getAddress check, because now it supports only ERC-20 addresses

Comment thread src/utils/schemas.ts
*/
export const networkConfigsSchema = z.record(
export const wdkNetworkConfigsSchema = z.record(
z.string().regex(/^[a-zA-Z0-9_-]+$/, {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

please allow caip-2 specification

e.g. eip155:1 or eip155:ethereum

@jonathunne jonathunne merged commit ca0fa4a into tetherto:main Feb 8, 2026
1 check failed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants