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
- 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.
- 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", })
- Backend TypeScript
- 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) - Send the attestation back to your backend. Use Complete User Registration with the
temporaryAuthenticationTokenas 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"])
- Backend TypeScript
- Authenticate the user with your system, then call Create Delegated Registration Challenge.
- 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.
- Backend TypeScript
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
delegateTofield. 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 })
- Backend TypeScript
- 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, }, )
delegateTofield 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. - Backend TypeScript
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) orDfnsDelegatedClient(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)
- Backend TypeScript
- 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.
- Init — get a challenge from IBM Digital Asset Haven.
- Backend TypeScript: Every write method has an Init variant, for example,
transferAssetInitandgenerateSignatureInit.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
_initvariant, for example,transfer_asset_initandgenerate_signature_init.challenge = delegated_client.wallets.transfer_asset_init( wallet_id, body={"kind": "Native", "to": "0xe5a2...", "amount": "1000000"}, )
- Backend TypeScript: Every write method has an Init variant, for example,
- 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
signedChallengeback to your backend.
- Triggers passkey prompt (Touch ID, Face ID, etc.).
- 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, }, )
- Backend TypeScript
This pattern applies to all write operations, such as
transferAssetInit/Complete,generateSignatureInit/CompleteandcreateWalletInit/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.