Implementing Delegated Wallets

This topic explains how to implement delegated wallets: registering end users, creating wallets they control, and enabling them to perform actions with their passkeys.

Setting up a service account client

All backend operations use your service account. Set up the client once; it is used for registration, login, and wallet creation.
  • Backend TypeScript
    import { DfnsApiClient } from '@dfns/sdk'
    import { AsymmetricKeySigner } from '@dfns/sdk-keysigner'
    
    const signer = new AsymmetricKeySigner({
      credId: process.env.DFNS_SERVICE_ACCOUNT_CRED_ID,
      privateKey: process.env.DFNS_SERVICE_ACCOUNT_PRIVATE_KEY,
    })
    
    const dfns = new DfnsApiClient({
      baseUrl: process.env.DFNS_API_URL,
      authToken: process.env.DFNS_SERVICE_ACCOUNT_TOKEN,
      signer,
    })
  • Backend Python
    import os
    from dfns_sdk import DfnsClient, DfnsClientConfig, KeySigner
    
    signer = KeySigner(
        credential_id=os.environ["DFNS_SERVICE_ACCOUNT_CRED_ID"],
        private_key=os.environ["DFNS_SERVICE_ACCOUNT_PRIVATE_KEY"],
    )
    
    config = DfnsClientConfig(
        auth_token=os.environ["IBM Digital Asset Haven_SERVICE_ACCOUNT_TOKEN"],
        base_url=os.environ.get("IBM Digital Asset Haven_API_URL", "https://api.digitalassets.ibm.com"),
        signer=signer,
    )

Register and log in end users

Registration (new users)
Follow the Delegated Registration flow. Your service account creates the user, and your frontend collects the passkey.

  1. Authenticate the user with your system, then call Create Delegated Registration Challenge.
    • Backend TypeScript
      const challenge = await dfns.auth.createDelegatedRegistrationChallenge({
        body: { email: user.email, kind: 'EndUser' },
      })
    • Backend Python
      with DfnsClient(config) as dfns:
          challenge = dfns.auth.create_delegated_registration_challenge({
              "email": user["email"],
              "kind": "EndUser",
          })
  2. Send the challenge to your frontend. The browser prompts the user to create a passkey.
    import { WebAuthn } from '@dfns/sdk-browser'
    
    const webauthn = new WebAuthn({ rpId: 'your-domain.com' })
    const attestation = await webauthn.create(challenge)
  3. Send the attestation back to your backend. Use Complete User Registration with the temporaryAuthenticationToken as the auth token.
    • Backend TypeScript
      const dfnsWithTempToken = new DfnsApiClient({
        baseUrl: process.env.DFNS_API_URL,
        authToken: challenge.temporaryAuthenticationToken,
        signer,
      })
      
      const newUser = await dfnsWithTempToken.auth.register({
        body: { firstFactorCredential: attestation },
      })
      
      // Store mapping: your user ID -> Dfns user ID
      await db.users.update({
        where: { id: yourUserId },
        data: { dfnsUserId: newUser.id },
      })
    • Backend Python
      temp_config = DfnsClientConfig(
          auth_token=challenge["temporaryAuthenticationToken"],
          base_url=os.environ.get("DFNS_API_URL", "https://api.digitalassets.ibm.com"),
          signer=signer,
      )
      
      with DfnsClient(temp_config) as dfns_temp:
          new_user = dfns_temp.auth.complete_user_registration({
              "firstFactorCredential": attestation,
          })
      
      # Store mapping: your user ID -> Dfns user ID
      db.users.update(user_id=your_user_id, dfns_user_id=new_user["id"])
Login (returning users)
Use Delegated Login. Authenticate the user with your system, then retrieve their IBM Digital Asset Haven auth token.
  • Backend TypeScript
    const { token } = await dfns.auth.delegatedLogin({
      body: { username: user.email },
    })
  • Backend Python
    with DfnsClient(config) as dfns:
        result = dfns.auth.delegated_login({"username": user["email"]})
        token = result["token"]

The returned token is the end user’s auth token, which is required for wallet operations.

Create delegated wallets

It is recommended to create wallets after registration rather than during it. This gives you control over when and which networks to provision. Two approaches are available for creating delegated wallets.

Through your service account
Your service account creates a wallet and delegates it to a user via the delegateTo field. The service account’s signer handles action signing automatically.
  • Backend TypeScript
    const wallet = await dfns.wallets.createWallet({
      body: {
        network: 'EthereumSepolia',
        delegateTo: userId, // end user ID from registration
      },
    })
  • Backend Python
    with DfnsClient(config) as dfns:
        wallet = dfns.wallets.create_wallet({
            "network": "EthereumSepolia",
            "delegateTo": user_id,  # end user ID from registration
        })
Through the end user
The end user can create wallets using the delegated client. This follows the Init/Complete pattern, requiring the user to sign with their passkey.
  • Backend TypeScript
    const challenge = await delegatedClient.wallets.createWalletInit({
      body: { network: 'EthereumSepolia' },
    })
    
    // ... user signs challenge with passkey on frontend ...
    
    const wallet = await delegatedClient.wallets.createWalletComplete(
      { body: { network: 'EthereumSepolia' } },
      signedChallenge
    )
  • Backend Python
    challenge = delegated_client.wallets.create_wallet_init(
        body={"network": "EthereumSepolia"},
    )
    
    # ... user signs challenge with passkey on frontend ...
    
    wallet = delegated_client.wallets.create_wallet_complete(
        body={"network": "EthereumSepolia"},
        signed_challenge={
            "challengeIdentifier": challenge["challengeIdentifier"],
            "firstFactor": signed_challenge,
        },
    )
Wallets created by the end user are automatically delegated to them; delegateTo field is not required.
Warning: Policies do not apply to delegated wallets. By design, delegated wallets bypass the policy engine, giving end users full control without organizational approval requirements.

Delegated actions

Once logged in, users can perform actions on their wallets (such as transfers or signatures). These operations require the user to sign with their passkey.

Delegated client
Use DfnsDelegatedApiClient (TypeScript) or DfnsDelegatedClient (Python) with the end user’s auth token. Unlike the service account client, this client does not include a signer—signing occurs on the user’s device.
  • Backend TypeScript
    import { DfnsDelegatedApiClient } from '@dfns/sdk'
    
    const delegatedClient = new DfnsDelegatedApiClient({
      baseUrl: process.env.DFNS_API_URL,
      authToken: endUserToken, // token from delegated login
    })
  • Backend Python
    from dfns_sdk import DfnsDelegatedClient, DfnsDelegatedClientConfig
    
    delegated_config = DfnsDelegatedClientConfig(
        auth_token=end_user_token,  # token from delegated login
        base_url="https://api.digitalassets.ibm.com",
    )
    
    delegated_client = DfnsDelegatedClient(delegated_config)
Init/Complete pattern
Every write method on the delegated client is split into two calls:
  • methodInit(): sends the request payload to IBM Digital Asset Haven and returns a challenge.
  • methodComplete(): sends the same payload plus the user’s signed challenge, then executes the action.
This split exists because the passkey resides on the user’s device, not on your server.
  1. Init — get a challenge from IBM Digital Asset Haven.
    • Backend TypeScript: Every write method has an Init variant, for example, transferAssetInit and generateSignatureInit.
      generateSignatureInit, etc.
      const challenge = await delegatedClient.wallets.transferAssetInit({
        walletId,
        body: { kind: 'Native', to: '0xe5a2...', amount: '1000000' },
      })
      
      return { challenge }
    • Backend Python: Every write method has an _init variant, for example, transfer_asset_init and generate_signature_init.
      challenge = delegated_client.wallets.transfer_asset_init(
          wallet_id,
          body={"kind": "Native", "to": "0xe5a2...", "amount": "1000000"},
      )
  2. Sign — user approves with passkey (frontend).
    import { WebAuthnSigner } from '@dfns/sdk-browser'
    
    const webauthn = new WebAuthnSigner({
      relyingParty: { id: 'your-domain.com', name: 'Your App' },
    })
    • Triggers passkey prompt (Touch ID, Face ID, etc.).
      const signedChallenge = await webauthn.sign(challenge)
    • Sends signedChallenge back to your backend.
  3. Complete — execute the action.
    • Backend TypeScript
      const transfer = await delegatedClient.wallets.transferAssetComplete(
        { walletId, body: { kind: 'Native', to: '0xe5a2...', amount: '1000000' } },
        signedChallenge
      )
    • Backend Python
      transfer = delegated_client.wallets.transfer_asset_complete(
          wallet_id,
          body={"kind": "Native", "to": "0xe5a2...", "amount": "1000000"},
          signed_challenge={
              "challengeIdentifier": challenge["challengeIdentifier"],
              "firstFactor": signed_challenge,
          },
      )

This pattern applies to all write operations, such as transferAssetInit/Complete, generateSignatureInit/Complete and createWalletInit/Complete. For more details, including direct API calls without the SDK, see the User Action Signing flows.

Social registration

Instead of delegated registration, users can authenticate directly with an identity provider (such as Google). No service account is required.

Security considerations

  • Never expose your service account credentials to the frontend.
  • Validate tokens on your backend before creating IBM Digital Asset Haven users.
  • Store the mapping between your user IDs and IBM Digital Asset Haven user IDs securely.
  • Implement rate limiting on registration endpoints.