Skip to main content

Documentation Index

Fetch the complete documentation index at: https://www.dynamic.xyz/docs/llms.txt

Use this file to discover all available pages before exploring further.

Overview

Delegated access lets your server perform wallet operations (signing messages) on behalf of users with their permission. This is useful for automating workflows while maintaining security through user-approved delegation. This guide covers the server-side implementation using the Rust SDK. For conceptual background, see Delegated Access Overview.

Prerequisites

How Delegation Works

  1. User approves delegation request in your application (client-side).
  2. Dynamic sends an encrypted webhook to your server containing:
    • walletId — the user’s wallet ID
    • encryptedWalletApiKey — wallet-scoped API key for outbound signing requests, RSA-OAEP-wrapped + AES-GCM encrypted
    • encryptedShare — the customer-side MPC share, encrypted with the same envelope
  3. Your server decrypts both fields with your RSA private key.
  4. Your server stores the credentials securely (vault).
  5. Your server uses these credentials to sign on behalf of the user via DelegatedEvmWalletClient.

Decrypting the Webhook Payload

use dynamic_waas_sdk::decrypt_delegated_webhook_data;

// Your RSA private key (PEM-encoded PKCS#8). Load from a secrets manager — never hardcode.
let rsa_private_key_pem = std::env::var("DYNAMIC_DELEGATED_RSA_PRIVATE_KEY_PEM")?;

let decrypted = decrypt_delegated_webhook_data(
    &rsa_private_key_pem,
    &payload.encrypted_share,
    &payload.encrypted_wallet_api_key,
)?;

// decrypted.share         — Vec<u8>, the MPC server key share (sensitive)
// decrypted.wallet_api_key — String, the per-wallet API key (sensitive)
DecryptedWebhookData has a custom Debug impl that redacts both fields — safe to include in tracing spans. The envelope is RSA-OAEP-SHA256-wrapped AES-256-GCM (alg = "HYBRID-RSA-AES-256"). The SDK also accepts legacy "RSA-OAEP" for backward compatibility.

Creating the Delegated Client

use dynamic_waas_sdk::{DelegatedWalletClient, DelegatedWalletClientOpts};
use dynamic_waas_sdk_evm::DelegatedEvmWalletClient;

let delegated = DelegatedWalletClient::new(
    DelegatedWalletClientOpts::new(env_id, decrypted.wallet_api_key.clone()),
)?;
let evm = DelegatedEvmWalletClient::new(&delegated);

Configuration Options

let opts = DelegatedWalletClientOpts::new(env_id, wallet_api_key)
    .base_api_url("https://app.dynamic-preprod.xyz") // optional
    .base_mpc_relay_url("https://mpc.dynamic-preprod.xyz"); // optional

Signing Messages

The delegated client takes the same arguments as the org-token EVM client. Build a WalletProperties using identity from the webhook payload and the decrypted share:
use dynamic_waas_sdk::{ServerKeyShare, WalletProperties};

let wallet_properties = WalletProperties {
    chain_name: "EVM".to_string(),
    wallet_id: payload.wallet_id.clone(),
    account_address: payload.account_address.clone(),
    threshold_signature_scheme: Default::default(),
    derivation_path: None,
    address_type: None,
    external_server_key_shares_backup_info: None,
};

let external_server_key_shares = vec![ServerKeyShare {
    /* fields populated from decrypted.share — see docs.rs for the exact shape */
    ..Default::default()
}];

let signature = evm
    .sign_message(
        &wallet_properties,
        &external_server_key_shares,
        "Hello, World!",
    )
    .await?;

Revoking Delegation

When a user revokes consent (or your server detects misuse), call revoke_delegation on the base delegated client:
delegated.revoke_delegation(&wallet_id).await?;
After revocation, the wallet API key + share are no longer accepted by Dynamic — outbound sign_message calls will fail with Error::Auth.

Security Considerations

Credential Storage

  • Never log or expose delegation credentials (wallet API key, key share). The SDK redacts them in Debug output; mirror that in your own code.
  • Store credentials encrypted at rest in your database (envelope encryption via cloud KMS recommended — see Storage Best Practices).
  • Use secure environment variables for the RSA private key. Load it from AWS Secrets Manager / GCP Secret Manager / Vault, not .env.
  • Implement credential rotation policies — revoke wallet API keys when no longer needed.

Defense-in-Depth Skeleton

use dynamic_waas_sdk::{DelegatedWalletClient, DelegatedWalletClientOpts};
use dynamic_waas_sdk_evm::DelegatedEvmWalletClient;

pub struct DelegatedWalletService {
    env_id: String,
    rsa_private_key_pem: String,
    vault: VaultClient,
    audit: AuditLog,
}

impl DelegatedWalletService {
    pub async fn handle_webhook(&self, payload: WebhookPayload) -> anyhow::Result<()> {
        let decrypted = dynamic_waas_sdk::decrypt_delegated_webhook_data(
            &self.rsa_private_key_pem,
            &payload.encrypted_share,
            &payload.encrypted_wallet_api_key,
        )?;

        // Vault both pieces — encrypted at rest, KMS-wrapped.
        self.vault
            .write_envelope(
                &format!("delegated:{}", payload.wallet_id),
                &decrypted,
            )
            .await?;

        self.audit.log("delegation_granted", &payload.wallet_id).await?;
        Ok(())
    }

    pub async fn sign_message(
        &self,
        wallet_id: &str,
        message: &str,
    ) -> anyhow::Result<String> {
        let creds = self.vault.read_envelope(&format!("delegated:{wallet_id}")).await?;

        let delegated = DelegatedWalletClient::new(
            DelegatedWalletClientOpts::new(&self.env_id, creds.wallet_api_key),
        )?;
        let evm = DelegatedEvmWalletClient::new(&delegated);

        let signature = evm
            .sign_message(&creds.wallet_properties, &creds.shares, message)
            .await?;

        self.audit.log("sign_message", wallet_id).await?;
        Ok(signature)
    }
}