# Create address Source: https://docs.oumla.com/api-reference/addresses/create-address /api-reference/openapi.json post /api/v1/addresses/generate Generate a new address for the given network and reference. Requires wallet/address policy. # Generate wallet if not exists and create address (v2) Source: https://docs.oumla.com/api-reference/addresses/generate-wallet-if-not-exists-and-create-address-v2 /api-reference/openapi.json post /api/v2/addresses/generate V2: creates wallet if needed, then generates address for the given network and reference. # Get address by ID Source: https://docs.oumla.com/api-reference/addresses/get-address-by-id /api-reference/openapi.json get /api/v1/addresses Retrieve a single address by its UUID. # List addresses for organization Source: https://docs.oumla.com/api-reference/addresses/list-addresses-for-organization /api-reference/openapi.json get /api/v1/addresses/organization List addresses for the authenticated user's organization with pagination. # List addresses for profile Source: https://docs.oumla.com/api-reference/addresses/list-addresses-for-profile /api-reference/openapi.json get /api/v1/addresses/{reference} List addresses for a profile by reference with pagination. # Get Native balance for address Source: https://docs.oumla.com/api-reference/assets/get-native-balance-for-address /api-reference/openapi.json get /api/v1/portfolio/native-balance Get native (e.g. ETH/BTC) balance for a given address and optional network. # Retrieved assets for address Source: https://docs.oumla.com/api-reference/assets/retrieved-assets-for-address /api-reference/openapi.json get /api/v1/portfolio Get asset balances for a given address, optionally filtered by network. # Call read function Source: https://docs.oumla.com/api-reference/contract-interactions/call-read-function /api-reference/openapi.json post /api/v1/contract-interactions/network/{networkId}/contract-address/{contractAddress}/functions/read Execute a read-only contract call (no gas). # Call write function Source: https://docs.oumla.com/api-reference/contract-interactions/call-write-function /api-reference/openapi.json post /api/v1/contract-interactions/network/{networkId}/contract-address/{contractAddress}/functions/write Start a write/send transaction workflow. Requires interact contract policy. # Get deployed contract ABI/functions Source: https://docs.oumla.com/api-reference/contract-interactions/get-deployed-contract-abifunctions /api-reference/openapi.json get /api/v1/contract-interactions/network/{networkId}/contract-address/{contractAddress}/functions Get ABI and callable functions for a deployed contract. # Get transaction receipt Source: https://docs.oumla.com/api-reference/contract-interactions/get-transaction-receipt /api-reference/openapi.json get /api/v1/contract-interactions/network/{networkId}/tx/{txId} Get the receipt for a contract write transaction. # Create contract template Source: https://docs.oumla.com/api-reference/contract-templates/create-contract-template /api-reference/openapi.json post /api/v1/contract-templates Create a new contract template (ABI, bytecode, metadata). # Delete contract template Source: https://docs.oumla.com/api-reference/contract-templates/delete-contract-template /api-reference/openapi.json delete /api/v1/contract-templates/{templateId} Delete a contract template by ID. # Deploy Contract Source: https://docs.oumla.com/api-reference/contract-templates/deploy-contract /api-reference/openapi.json post /api/v1/contract-templates/{templateId}/deploy Deploy a contract from a template to the specified network. # Get contract constructor by template ID Source: https://docs.oumla.com/api-reference/contract-templates/get-contract-constructor-by-template-id /api-reference/openapi.json get /api/v1/contract-templates/{templateId}/getConstructorInfo Get constructor info (params, ABI) for a contract template. # Get contract function by template ID Source: https://docs.oumla.com/api-reference/contract-templates/get-contract-function-by-template-id /api-reference/openapi.json get /api/v1/contract-templates/{templateId}/function Get function metadata/ABI for a contract template. # Get contract template by ID Source: https://docs.oumla.com/api-reference/contract-templates/get-contract-template-by-id /api-reference/openapi.json get /api/v1/contract-templates/{templateId} Get a contract template by its ID. # List contract templates Source: https://docs.oumla.com/api-reference/contract-templates/list-contract-templates /api-reference/openapi.json get /api/v1/contract-templates List contract templates for the organization with pagination. # Add Contract ABI Source: https://docs.oumla.com/api-reference/deployed-contracts/add-contract-abi /api-reference/openapi.json post /api/v1/deployed-contracts/add-abi Add or update ABI for a deployed contract. # Fetch contract ABI Source: https://docs.oumla.com/api-reference/deployed-contracts/fetch-contract-abi /api-reference/openapi.json post /api/v1/deployed-contracts/fetch-abi Fetch contract ABI from chain/relayer by address and network. # Get deployed contract by address and networkId Source: https://docs.oumla.com/api-reference/deployed-contracts/get-deployed-contract-by-address-and-networkid /api-reference/openapi.json get /api/v1/deployed-contracts/{contractAddress}/{networkId} Get a deployed contract by contract address and networkId. # Get deployed contract by deployment ID Source: https://docs.oumla.com/api-reference/deployed-contracts/get-deployed-contract-by-deployment-id /api-reference/openapi.json get /api/v1/deployed-contracts/{deploymentId} Get a deployed contract by its deployment ID. # List Deployed contracts Source: https://docs.oumla.com/api-reference/deployed-contracts/list-deployed-contracts /api-reference/openapi.json get /api/v1/deployed-contracts List deployed contracts with optional filters and pagination. # Introduction Source: https://docs.oumla.com/api-reference/introduction Oumla API Reference - Blockchain wallet management, tokenization, and smart contracts Here, you will find all of the necessary documentation for our API, which will enable you to quickly get started. Before proceeding, it is important to note that you will need to obtain an API key from the dashboard as it is required for all routes. ## Welcome to the Oumla API The Oumla API is a comprehensive Cloud platform providing wallet management, tokenization, contract interactions, and blockchain services. This API reference is automatically generated from our OpenAPI specification. **Two Ways to Use Oumla**: You can interact with Oumla either through **direct REST API calls** (documented here) or through our **TypeScript SDK** (see [SDK Reference](/sdk-reference/introduction)). Both methods provide the same functionality - choose the one that best fits your project. Production endpoint: ## Authentication **API Key Required**: All API requests require authentication using your API key. Include it in the `x-api-key` header. ## Key Features Create and manage user profiles for different types: Users, Departments, and Merchants. Generate and manage wallets for profiles and organizations with support for multiple blockchain networks. Track transactions by address, wallet, profile, or organization with comprehensive history and monitoring. View and track assets across wallets and addresses with real-time balances and holdings. Create and execute withdrawal requests with multi-signature support. Create and manage reusable smart contract templates. Deploy contracts from templates and retrieve constructor and function information. Call read and write functions on deployed contracts. Fetch and manage contract ABIs with transaction receipt tracking. Create and manage NFT collections (ERC-721, ERC-1155), mint and burn tokens, issue new tokens, or link existing contracts. ## Supported Networks **tBTC** (testnet) - Available now\ **BTC** (mainnet) - Requires activation **tETH** (testnet - Sepolia) - Available now **ETH** (mainnet) - Requires activation **Mainnet Activation Required** To activate mainnet access for Bitcoin (BTC) and Ethereum (ETH), you need to contact our support team and await approval. We will whitelist your account and require more information about your business. Click to send a pre-filled email to our support team ## Response Format All successful API responses follow a consistent format for easy integration. ```json theme={null} { "message": "Operation successful", "data": { /* response data */ }, "success": true, "status": 200 } ``` Error responses include detailed field-level validation errors when applicable. ## Pagination List endpoints support pagination with the following query parameters: * **skip**: Number of records to skip (default: 0) * **take**: Number of records to retrieve (default: 10, max: 100) ## Getting Started New to Oumla? Follow our Quick Start guide to make your first API call in 5 minutes. Use the interactive playground on each endpoint page to test API calls with your API key before integrating into your application. # Get all enabled networks Source: https://docs.oumla.com/api-reference/networks/get-all-enabled-networks /api-reference/openapi.json get /api/v1/network/enabled Returns the enabled-networks catalog (ID, name, alias) from cache. Accessible to any authenticated user — exposes only non-sensitive metadata. On cache miss, rebuilds the index from the database. # Get networks available for your organization Source: https://docs.oumla.com/api-reference/networks/get-networks-available-for-your-organization /api-reference/openapi.json get /api/v1/network/organization Get networks available for your organization. # Create Profile Source: https://docs.oumla.com/api-reference/profiles/create-profile /api-reference/openapi.json post /api/v1/profiles Create a new profile for the organization. # List of profiles Source: https://docs.oumla.com/api-reference/profiles/list-of-profiles /api-reference/openapi.json get /api/v1/profiles List profiles for the authenticated user's organization with pagination. # Burn token Source: https://docs.oumla.com/api-reference/tokenization/burn-token /api-reference/openapi.json post /api/v1/tokenization/collections/{id}/tokens/burn Start burn workflow for tokens in a collection. Requires burn policy. # Create new Collection Source: https://docs.oumla.com/api-reference/tokenization/create-new-collection /api-reference/openapi.json post /api/v1/tokenization/collections Start the create-collection workflow. Returns workflow ID; collection is created asynchronously. # Delete collection by ID Source: https://docs.oumla.com/api-reference/tokenization/delete-collection-by-id /api-reference/openapi.json delete /api/v1/tokenization/collections/{id} Delete a collection and its associated data. # Get collection by ID Source: https://docs.oumla.com/api-reference/tokenization/get-collection-by-id /api-reference/openapi.json get /api/v1/tokenization/collections/{id} Get a single collection by its ID. # Get Collection token details Source: https://docs.oumla.com/api-reference/tokenization/get-collection-token-details /api-reference/openapi.json get /api/v1/tokenization/collections/{id}/tokens/{tokenId} Get details of a specific token within a collection. # Get collection tokens by ID or type Source: https://docs.oumla.com/api-reference/tokenization/get-collection-tokens-by-id-or-type /api-reference/openapi.json get /api/v1/tokenization/collection/tokens List tokens in a collection, optionally filtered by type, with pagination. # Get linked token by ID Source: https://docs.oumla.com/api-reference/tokenization/get-linked-token-by-id /api-reference/openapi.json get /api/v1/tokenization/tokens/{id} Get a linked token by its ID. # Issue Token Source: https://docs.oumla.com/api-reference/tokenization/issue-token /api-reference/openapi.json post /api/v1/tokenization/tokens Start the issue-token workflow. Returns the workflowId, tokenId, and status. # Link Contract Source: https://docs.oumla.com/api-reference/tokenization/link-contract /api-reference/openapi.json post /api/v1/tokenization/tokens/link Link an existing deployed contract to the organization for tokenization. # List Collections Source: https://docs.oumla.com/api-reference/tokenization/list-collections /api-reference/openapi.json get /api/v1/tokenization/collections List tokenization collections for the organization with pagination. # List Tokens Source: https://docs.oumla.com/api-reference/tokenization/list-tokens /api-reference/openapi.json get /api/v1/tokenization/tokens List issued tokens for the organization with pagination. # Mint token Source: https://docs.oumla.com/api-reference/tokenization/mint-token /api-reference/openapi.json post /api/v1/tokenization/collections/{id}/tokens/mint Start mint workflow for a collection. Requires mint policy. # Unlink Token Source: https://docs.oumla.com/api-reference/tokenization/unlink-token /api-reference/openapi.json delete /api/v1/tokenization/tokens/{id} Unlink a token from the organization. # List transactions by address Source: https://docs.oumla.com/api-reference/transactions/list-transactions-by-address /api-reference/openapi.json get /api/v1/transactions/address/{address} List transactions for a given address with pagination. # List Transactions by mini wallet Source: https://docs.oumla.com/api-reference/transactions/list-transactions-by-mini-wallet /api-reference/openapi.json get /api/v1/transactions/wallet/{miniWalletId} List transactions for a mini wallet with pagination. # List Transactions by organization Source: https://docs.oumla.com/api-reference/transactions/list-transactions-by-organization /api-reference/openapi.json get /api/v1/transactions/organization List transactions for the authenticated user's organization with pagination. # List Transactions by profile Source: https://docs.oumla.com/api-reference/transactions/list-transactions-by-profile /api-reference/openapi.json get /api/v1/transactions/profile/{reference} List transactions for a profile by reference with pagination. # Create wallet Source: https://docs.oumla.com/api-reference/wallets/create-wallet /api-reference/openapi.json post /api/v1/wallets/generate Generate a new wallet for the given reference and network. Requires wallet/address policy. # List wallets for the organization Source: https://docs.oumla.com/api-reference/wallets/list-wallets-for-the-organization /api-reference/openapi.json get /api/v1/wallets/organization List wallets for the authenticated user's organization with pagination. # List wallets for the profile Source: https://docs.oumla.com/api-reference/wallets/list-wallets-for-the-profile /api-reference/openapi.json get /api/v1/wallets/profile/{reference} List wallets for a profile by reference with pagination. # Withdraw Source: https://docs.oumla.com/api-reference/withdraw/withdraw /api-reference/openapi.json post /api/v1/withdraw Initiate a withdrawal. Returns pending status; supports BTC, tBTC, ETH, tETH. # Get Workflow status and result Source: https://docs.oumla.com/api-reference/workflows/get-workflow-status-and-result /api-reference/openapi.json get /api/v1/workflows/{workflowId}/status Get the status and result of a workflow by ID. # Changelog Source: https://docs.oumla.com/changelog Product updates and announcements for the Oumla API ## Added * **Get Address by ID**: New `client.addresses.getAddressById({ id })` method to fetch a single address by its UUID * New request type `GetAddressByIdRequest` and response type `GetAddressByIdResponseBodyDto` ## Changed * `client.wallets.createWallet(...)` response now includes an `id: string` (UUID) field on `CreateWalletDataDto`, allowing callers to reference the newly created wallet directly ## Added * **Enabled Networks Endpoint**: New `networks.getEnabledNetworks()` endpoint (`GET /api/v1/network/enabled`) returning the global catalog of enabled networks (`networkId`, `name`, `alias`) for any authenticated user * New types: `EnabledNetworkItemDto`, `GetEnabledNetworksResponseDto` * **Gasless Network Support**: `isGasless: boolean` added to network responses (`OrgNetworkItemDto`, network update payloads) — indicates whether the network sponsors gas fees ## Changed * **Token Issuance is now Asynchronous**: `tokenization.issueNewToken()` request body is unchanged; the response is now a workflow handle (`IssueTokenWorkflowResponseDto`) containing `workflowId`, `operationId`, `status`, and `message`, served with HTTP `202 Accepted`. Poll the workflow resource with the returned `workflowId` to observe completion and retrieve issued tokens * New 400 error code `ERR-146` ("Contract deployment is still in progress.") surfaced on `issueNewToken()` * **Removed `path` field from address and wallet responses**: the derivation `path` field has been dropped from `AddressDataDto`, `CreateWalletDataDto`, and `WalletDataDto` — request shapes are unchanged ## Removed * `IssueTokenResponseDto` (superseded by `IssueTokenWorkflowResponseDto`) ## Fixed * Improved field-level documentation and formats (`uuid`, descriptions, examples) on address and wallet DTOs — no runtime shape changes This is a major release with multiple breaking changes. Review the **Changed** section carefully before upgrading. ## Added * **Dynamic Network Support**: API responses now return dynamic network entities instead of hardcoded network strings. Networks are globally managed objects that can be assigned per organization * **Networks Resource**: New `networks` resource with `getNetworks()` endpoint for listing available networks * **Contract Deployment**: Improved contract deployment workflow with better parameter handling and execution reliability ## Changed * ⚠️ **Breaking — Resource Renames**: * `portfolio` resource renamed to `assets` — update all `client.portfolio.*` calls to `client.assets.*` * `withdrawals` resource renamed to `withdraw` — update all `client.withdrawals.*` calls to `client.withdraw.*` * ⚠️ **Breaking — Request / Response Type Renames**: All request and response types have been renamed to match the server-side DTO naming convention: | Old Name | New Name | | -------------------------- | -------------------------------- | | `CreateWalletRequest` | `CreateWalletRequestBodyDto` | | `CreateProfileRequest` | `CreateProfileRequestBodyDto` | | `CreateCollectionRequest` | `CreateCollectionBodyDto` | | `CreateContractRequest` | `CreateContractRequestBodyDto` | | `DeployContractRequest` | `DeployContractRequestBodyDto` | | `AddContractAbiRequest` | `AddContractAbiRequestBodyDto` | | `FetchContractAbiRequest` | `FetchContractAbiRequestBodyDto` | | `ReadCallFunctionRequest` | `ReadCallFunctionBody` | | `WriteCallFunctionRequest` | `WriteCallFunctionBody` | | `BurnTokenRequest` | `BurnTokenBodyDto` | | `IssueNewTokenRequest` | `IssueNewTokenRequestBody` | | `LinkContractRequest` | `LinkContractBodyDto` | | `MintTokenRequest` | `MintTokenBodyDto` | | `CreateWithdrawRequest` | `CreateWithdrawRequestBodyDto` | | `WalletData` | `WalletDataDto` | | `AssetData` | `AssetItemDto` | | `AddressData` | `AddressDataDto` | | `NativeBalanceData` | `NativeBalanceItemDto` | | `TokenData` | `TokenResponseDto` | | `TransactionData` | `TransactionItemDto` | | `CollectionData` | `CollectionItemDto` | | `CollectionTokenData` | `CollectionTokenDetailDto` | | `DeployedContractData` | `DeployedContractItemDto` | | `ContractTemplateData` | `ContractTemplateItemDto` | | `Pagination` | `AddressListPaginationDto` | * ⚠️ **Breaking — Network Architecture**: * Removed the `network` attribute from all database-backed models * API responses that previously returned a `network` string now return dynamic network entities * Any code reading the `network` string directly from API responses must be updated * ⚠️ **Breaking — Removed Error Classes**: the following error classes have been removed. Catch `OumlaSdkApiError` directly for these status codes if needed: * `BadGatewayError` (502) * `GatewayTimeoutError` (504) * `ServiceUnavailableError` (503) * `UnprocessableEntityError` (422) * **Error Handling**: error response bodies are now loosely typed (`unknown`) for forward compatibility * **Request Validation**: standardized Joi schema configuration and improved request validation across all API endpoints ## Fixed * Updated address generation endpoint to the correct API version with improved error handling * Added validation for `initializeParams` in the `issue-new-token` operation ## Added * **Enhanced Error Handling**: comprehensive error types for finer-grained error handling: * `BadGatewayError` (502) * `ConflictError` (409) * `ForbiddenError` (403) * `GatewayTimeoutError` (504) * `InternalServerError` (500) * `ServiceUnavailableError` (503) * `UnauthorizedError` (401) * `UnprocessableEntityError` (422) * All existing endpoints now include appropriate error codes in their documentation and error handling * **Portfolio Resource**: `getNativeBalance()` — get native balance for network, address, or wallet ## Changed * Renamed `assets` resource to `portfolio` for better semantic clarity * Enhanced error handling across all endpoints with appropriate HTTP status code mappings * Improved type definitions with new response types for all endpoints * `readFunction()` renamed to `readCallFunction()` in contract interactions * `writeFunction()` renamed to `writeCallFunction()` in contract interactions * `getTransactionsByWallet()` renamed to `getTransactionsByMiniWallet()` in transactions * `createWithdrawal()` renamed to `createWithdraw()` in withdrawals ## Changed * Use `OumlaSdkApiEnvironment.Mainnet` instead of `OumlaSdkApiEnvironment.Production` in examples * Updated README examples to use the Mainnet environment ## Changed * Renamed environment from `Production` to `Mainnet` for better clarity * Use `OumlaSdkApiEnvironment.Mainnet` instead of `OumlaSdkApiEnvironment.Production` ## Fixed * Updated README.md code examples to use correct `generateAddressV2` method name ## Fixed * Updated README.md to use correct `generateAddressV2` method name in code example ## Added * **Temporal Workflow Status**: Track async workflow status with `getTemporalWorkflowStatus()` * **Generate Address V2**: Enhanced address generation using V2 API endpoint * **Collection Management**: New endpoints for `getCollection()`, `deleteCollection()`, `getCollectionTokenDetails()`, and `getCollectionTokens()` * **Token Operations**: `unlinkToken()` for unlinking tokens from the platform * **Contract Interactions**: `getTransactionReceipt()` for retrieving transaction receipts ## Changed * Address generation now uses V2 API endpoint * Updated documentation and examples for all new features ## Initial Release * Complete TypeScript SDK with full API coverage * Support for profiles, wallets, addresses, transactions, assets, and withdrawals * Smart contract interaction capabilities * Tokenization and NFT management features * Multi-environment support with comprehensive error handling # Authentication Source: https://docs.oumla.com/guides/authentication Learn how to authenticate with the Oumla API Registering is straightforward. Visit our site, click the Signup button, and you'll be redirected to the dashboard. From there, you can easily view and manage API keys, as well as handle user, wallet, and transaction management. ## Two Ways to Authenticate Oumla supports authentication through **two methods**: 1. **TypeScript SDK** - Authentication is handled automatically when you initialize the client with your API key 2. **Direct REST API** - Include your API key in the `x-api-key` header for each request Both methods use the same API key and provide identical functionality. Choose the method that best fits your project. ## Request Headers All requests to the Oumla API require the `x-api-key` header containing your API key. Here's how to include your API key in requests: ```bash cURL theme={null} curl -X GET "https://sandbox.oumla.com/api/v1/profiles" \ -H "x-api-key: your-api-key-here" ``` ```javascript JavaScript theme={null} const response = await fetch('https://sandbox.oumla.com/api/v1/profiles', { headers: { 'x-api-key': 'your-api-key-here' } }); const data = await response.json(); console.log(data); ``` ```python Python theme={null} import requests headers = { 'x-api-key': 'your-api-key-here' } response = requests.get('https://sandbox.oumla.com/api/v1/profiles', headers=headers) print(response.json()) ``` ```go Go theme={null} package main import ( "fmt" "io" "net/http" ) func main() { url := "https://sandbox.oumla.com/api/v1/profiles" req, _ := http.NewRequest("GET", url, nil) req.Header.Set("x-api-key", "your-api-key-here") client := &http.Client{} resp, _ := client.Do(req) defer resp.Body.Close() body, _ := io.ReadAll(resp.Body) fmt.Println(string(body)) } ``` **New to Oumla?** Follow the [Quick Start](/guides/quickstart) guide to create your account and get your first API key. ## API Key Authentication ### Using the SDK When using the TypeScript SDK, authentication is handled automatically: ```javascript theme={null} import { OumlaSdkApiClient, OumlaSdkApiEnvironment } from '@oumla/sdk'; const client = new OumlaSdkApiClient({ environment: OumlaSdkApiEnvironment.Sandbox, apiKey: 'your-api-key-here' // Authentication handled automatically }); // All SDK methods automatically include the API key const profiles = await client.profiles.getProfiles(); ``` ### Using Direct REST API Calls When making direct HTTP requests, include your API key in the `x-api-key` header: ```javascript theme={null} // Direct REST API call const response = await fetch('https://sandbox.oumla.com/api/v1/profiles', { headers: { 'x-api-key': 'your-api-key-here' // Required for all REST API calls } }); const data = await response.json(); ``` ## API Key Management Best Practices **Never commit API keys to version control.** Always use environment variables or secure secret management systems. ### Environment Variables Store your API key securely: ```bash theme={null} # .env file (never commit this!) OUMLA_API_KEY=your-api-key-here OUMLA_ENVIRONMENT=sandbox ``` ```javascript theme={null} // In your application import { OumlaSdkApiClient, OumlaSdkApiEnvironment } from '@oumla/sdk'; const client = new OumlaSdkApiClient({ environment: process.env.OUMLA_ENVIRONMENT === 'production' ? OumlaSdkApiEnvironment.Production : OumlaSdkApiEnvironment.Sandbox, apiKey: process.env.OUMLA_API_KEY }); ``` ### Security Recommendations 1. **Use different API keys for different environments** (sandbox vs production) 2. **Rotate API keys regularly** for enhanced security 3. **Never share API keys** in client-side code or public repositories 4. **Use environment-specific keys** to prevent accidental production access 5. **Monitor API key usage** in the dashboard for suspicious activity ## Common Authentication Errors ### Invalid API Key (401 - ERR\_008) ```json theme={null} { "success": false, "message": "You need to sign in to continue", "code": "ERR_008", "category": "UNKNOWN_ERROR", "statusCode": 401, "path": "/api/v1/profiles", "timestamp": "2024-01-01T00:00:00.000Z", "requestId": "req_123456789" } ``` **Causes:** * API key is missing from request headers * API key is incorrect or invalid * API key has been revoked or expired **Solutions:** 1. Verify your API key in the dashboard 2. Check that the `x-api-key` header is included in your request 3. Ensure there are no extra spaces or characters in the API key 4. Generate a new API key if the current one is invalid ### Invalid API Key Format (401 - ERR\_405) ```json theme={null} { "success": false, "message": "Invalid API key", "code": "ERR_405", "category": "UNKNOWN_ERROR", "statusCode": 401 } ``` **Causes:** * API key format is incorrect * API key is malformed **Solutions:** 1. Verify the API key format matches the expected pattern 2. Regenerate the API key from the dashboard 3. Check for copy-paste errors ## Error Handling Always handle authentication errors gracefully in your application: ```javascript theme={null} import { OumlaSdkApiClient, OumlaSdkApiError } from '@oumla/sdk'; const client = new OumlaSdkApiClient({ environment: OumlaSdkApiEnvironment.Sandbox, apiKey: process.env.OUMLA_API_KEY }); try { const profile = await client.profiles.createProfile({ reference: 'user-123', type: 'User' }); } catch (error) { if (error instanceof OumlaSdkApiError) { if (error.statusCode === 401) { // Handle authentication error console.error('Authentication failed:', error.message); // Prompt user to check API key or re-authenticate } else { // Handle other errors console.error('API error:', error.message); } } else { // Handle unexpected errors console.error('Unexpected error:', error); } } ``` ## Rate Limiting Rate limiting information will be provided here once available. Check the dashboard or contact support for current rate limits. ## Mainnet Activation **Mainnet Activation Required** To activate mainnet access for Bitcoin (BTC) and Ethereum (ETH), you need to contact our support team and await approval. We will whitelist your account and require more information about your business. Click to send a pre-filled email to our support team # Error Reference Source: https://docs.oumla.com/guides/error-reference Complete reference for Oumla API error codes and troubleshooting This guide provides a comprehensive reference for all Oumla API error codes, their meanings, and solutions. ## Error Response Format All error responses follow a consistent format: ```json theme={null} { "success": false, "message": "Human-readable error message", "code": "ERR_XXX", "category": "ERROR_CATEGORY", "statusCode": 400, "path": "/api/v1/endpoint", "timestamp": "2024-01-01T00:00:00.000Z", "requestId": "req_123456789" } ``` ### Error Fields * **message**: User-friendly error message * **code**: Unique error code (ERR\_XXX format) * **category**: Error category (VALIDATION, RESOURCE, CONFLICT, etc.) * **statusCode**: HTTP status code * **path**: API endpoint where error occurred * **timestamp**: When the error occurred * **requestId**: Unique request identifier for support ## Error Categories Errors are organized into the following categories: * **VALIDATION**: Input validation errors * **RESOURCE**: Resource not found or invalid * **CONFLICT**: Resource conflicts (e.g., already exists) * **UNKNOWN\_ERROR**: General errors * **NETWORK**: Network-related errors * **AUTHENTICATION**: Authentication/authorization errors ## General Errors (0-99) ### ERR\_000 - Already Exists **Status Code:** 409\ **Category:** CONFLICT\ **Message:** "Already exists" **Causes:** * Attempting to create a resource that already exists **Solutions:** * Check if the resource already exists before creating * Use update endpoints if modifying existing resources ### ERR\_001 - Profile Already Exists **Status Code:** 409\ **Category:** CONFLICT\ **Message:** "Profile already exists" **Causes:** * Creating a profile with a reference that already exists **Solutions:** * Use a unique reference identifier * Check existing profiles before creating * Use the update endpoint if modifying an existing profile ### ERR\_006 - Not Found **Status Code:** 404\ **Category:** RESOURCE\ **Message:** "The requested resource was not found" **Causes:** * Requesting a resource that doesn't exist * Invalid resource ID or reference **Solutions:** * Verify the resource ID or reference is correct * Check that the resource exists before accessing it * Ensure you have permission to access the resource ### ERR\_008 - Unauthorized **Status Code:** 401\ **Category:** UNKNOWN\_ERROR\ **Message:** "You need to sign in to continue" **Causes:** * Missing or invalid API key * API key has been revoked or expired **Solutions:** * Verify your API key is correct and included in headers * Check API key in the dashboard * Generate a new API key if the current one is invalid * See [Authentication guide](/guides/authentication) for details ### ERR\_009 - Forbidden **Status Code:** 403\ **Category:** UNKNOWN\_ERROR\ **Message:** "You don't have permission to do that" **Causes:** * Insufficient permissions for the requested operation * Resource belongs to a different organization **Solutions:** * Verify you have the necessary permissions * Check that you're accessing resources from your organization * Contact support if permissions seem incorrect ### ERR\_010 - Bad Request **Status Code:** 400\ **Category:** VALIDATION\ **Message:** "Bad Request" **Causes:** * Invalid request format * Missing required fields * Invalid parameter values **Solutions:** * Review the API documentation for required fields * Validate request parameters before sending * Check request format matches API specification ### ERR\_014 - Master Wallet Already Generated **Status Code:** 409\ **Category:** RESOURCE\ **Message:** "Master wallet already generated" **Causes:** * Attempting to generate master wallet shares when they already exist **Solutions:** * Master wallet can only be generated once per organization * Use existing master wallet shares if already generated ### ERR\_015 - Master Wallet Not Generated **Status Code:** 400\ **Category:** RESOURCE\ **Message:** "Master wallet not generated" **Causes:** * Attempting to use master wallet before it's been generated **Solutions:** * Generate master wallet shares first * Ensure onboarding process is complete ### ERR\_022 - Invalid Pagination **Status Code:** 400\ **Category:** VALIDATION\ **Message:** "Invalid pagination parameters" **Causes:** * Invalid `skip` or `take` values * Negative pagination values * `take` exceeds maximum allowed (typically 100) **Solutions:** * Use valid pagination values (skip >= 0, take > 0) * Ensure `take` doesn't exceed maximum (100) * Use default values if not specified ## Validation Errors (300-399) ### ERR\_302 - Missing Required Field **Status Code:** 400\ **Category:** VALIDATION\ **Message:** "Required field is missing" **Causes:** * Required request field is missing * Required parameter not provided **Solutions:** * Review API documentation for required fields * Ensure all required fields are included in request * Check field names match API specification ### ERR\_303 - Invalid Format **Status Code:** 400\ **Category:** VALIDATION\ **Message:** "Invalid format provided" **Causes:** * Field value doesn't match expected format * Invalid data type * Format validation failed (e.g., email, address) **Solutions:** * Verify field format matches API specification * Check data types are correct * Validate formats before sending (e.g., valid blockchain address) ### ERR\_304 - Invalid Range **Status Code:** 400\ **Category:** VALIDATION\ **Message:** "Value out of valid range" **Causes:** * Numeric value outside allowed range * Amount exceeds limits **Solutions:** * Check value is within valid range * Review API documentation for value constraints * Validate ranges before sending request ## Contract & Blockchain Errors (100-199) ### ERR\_056 - Contract Template Not Found **Status Code:** 404\ **Category:** RESOURCE\ **Message:** "Contract template not found" **Causes:** * Contract template ID doesn't exist * Template was deleted **Solutions:** * Verify contract template ID is correct * List available templates to find correct ID * Ensure template hasn't been deleted ### ERR\_066 - Contract Not Found **Status Code:** 404\ **Category:** RESOURCE\ **Message:** "Contract not found" **Causes:** * Deployed contract doesn't exist * Invalid contract address or deployment ID **Solutions:** * Verify contract address or deployment ID * Check contract deployment status * Ensure contract belongs to your organization ### ERR\_068 - Collection Not Found **Status Code:** 404\ **Category:** RESOURCE\ **Message:** "Collection not found" **Causes:** * Collection ID doesn't exist * Collection was deleted **Solutions:** * Verify collection ID is correct * List available collections to find correct ID * Check collection deployment status ### ERR\_076 - Contract Function Not Found **Status Code:** 404\ **Category:** RESOURCE\ **Message:** "Contract function not found" **Causes:** * Function name doesn't exist in contract ABI * Function signature mismatch **Solutions:** * Verify function name matches contract ABI * Check function parameters match signature * Review contract ABI for available functions ### ERR\_096 - Address Not Found **Status Code:** 404\ **Category:** RESOURCE\ **Message:** "Address not found" **Causes:** * Address doesn't exist in system * Invalid address reference **Solutions:** * Verify address reference is correct * Ensure address belongs to your organization * Generate address if it doesn't exist ### ERR\_126 - Failed Deployment **Status Code:** 500\ **Category:** RESOURCE\ **Message:** "Contract deployment failed" **Causes:** * Contract deployment transaction failed * Insufficient gas or network issues * Invalid contract bytecode **Solutions:** * Check transaction status in workflow * Verify sufficient balance for gas fees * Review contract bytecode and constructor parameters * Check network status ### ERR\_146 - Contract Deployment Not Completed **Status Code:** 400\ **Category:** RESOURCE\ **Message:** "Contract deployment not completed" **Causes:** * Deployment workflow still in progress * Deployment failed but status not updated **Solutions:** * Check workflow status using Temporal endpoint * Wait for deployment to complete * Retry if workflow failed ### ERR\_176 - Invalid Contract Address **Status Code:** 400\ **Category:** VALIDATION\ **Message:** "Invalid contract address" **Causes:** * Contract address format is invalid * Address doesn't match network format **Solutions:** * Verify address format matches network (Ethereum vs Bitcoin) * Check address is a valid contract address * Ensure address is on the correct network ### ERR\_186 - Failed Issue Token **Status Code:** 500\ **Category:** RESOURCE\ **Message:** "Failed to issue token" **Causes:** * Token issuance transaction failed * Collection deployment incomplete * Network issues **Solutions:** * Check collection deployment status * Verify workflow completed successfully * Retry token issuance * Check network connectivity ### ERR\_196 - Token Not Found **Status Code:** 404\ **Category:** RESOURCE\ **Message:** "Token not found" **Causes:** * Token ID doesn't exist * Token was burned or removed **Solutions:** * Verify token ID is correct * Check token exists in collection * Ensure token hasn't been burned ## Network & External Service Errors (200-299) ### ERR\_200 - Network Error **Status Code:** 500\ **Category:** NETWORK\ **Message:** "Network error" **Causes:** * Connection to blockchain network failed * Network timeout * External service unavailable **Solutions:** * Retry the request * Check network connectivity * Verify blockchain network status * Contact support if issue persists ### ERR\_216 - Balance Not Found **Status Code:** 404\ **Category:** RESOURCE\ **Message:** "Balance not found" **Causes:** * Address has no balance * Address not indexed yet **Solutions:** * Verify address is correct * Wait for address indexing to complete * Check address has received funds ### ERR\_226 - No Unspent Transactions **Status Code:** 400\ **Category:** RESOURCE\ **Message:** "No unspent transactions" **Causes:** * Bitcoin address has no UTXOs * All funds already spent **Solutions:** * Ensure address has received funds * Wait for transaction confirmation * Check address balance ### ERR\_236 - Not Enough Balance **Status Code:** 400\ **Category:** RESOURCE\ **Message:** "Not enough balance" **Causes:** * Insufficient funds for transaction * Balance doesn't cover amount + fees **Solutions:** * Check available balance * Ensure balance covers amount and fees * Account for network fees in calculations ## Troubleshooting Guide ### Common Issues **Issue: Authentication errors persist** 1. Verify API key is correct in dashboard 2. Check API key is included in request headers 3. Ensure no extra spaces in API key 4. Try regenerating API key **Issue: Resource not found errors** 1. Verify resource IDs/references are correct 2. Check resource belongs to your organization 3. Ensure resource hasn't been deleted 4. List resources to find correct identifiers **Issue: Validation errors** 1. Review API documentation for required fields 2. Check field formats match specification 3. Validate data types are correct 4. Ensure all required fields are provided **Issue: Workflow/async operation errors** 1. Check workflow status using Temporal endpoint 2. Wait for async operation to complete 3. Verify workflow completed successfully 4. Retry if workflow failed ### Getting Help If you continue to experience issues: 1. **Check Error Details**: Review the full error response including `code`, `message`, and `requestId` 2. **Review Documentation**: Consult relevant guides for your use case 3. **Contact Support**: Email with: * Error code and message * Request ID from error response * Steps to reproduce * Request details (without sensitive data) Always include the `requestId` from error responses when contacting support. This helps us quickly locate and diagnose issues. # FAQs Source: https://docs.oumla.com/guides/faqs Frequently asked questions about Oumla API To perform transactions with tBTC or tETH, you need a balance in your wallet. Follow these steps to obtain testnet coins: Visit the [Ethereum Sepolia faucet](https://cloud.google.com/application/web3/faucet/ethereum/sepolia) and enter your wallet address. Visit the [Bitcoin testnet faucet](https://bitcoinfaucet.uo1.net/) and enter your wallet address. These faucets will provide you with the necessary testnet coins to create transactions on the respective networks. You will receive testnet coins, so after that you can check your balance through our Dashboard ### Example: Bitcoin Testnet Faucet For example, to get Bitcoin testnet coins, visit [https://bitcoinfaucet.uo1.net/](https://bitcoinfaucet.uo1.net/) and follow the steps to request testnet coins: How to fill the wallet with bitcoin testnet from Bitcoin Testnet Faucet A client share, also known as the "Oumla Share," is a key component of Oumla's hybrid key management model for enhanced blockchain security. ### How It Works When you register with Oumla, a cryptographic key is generated and split into three parts: * **Your Client Share** - You maintain control * **Oumla's Share** - Securely stored by Oumla * **Third-Party Share** - Held by a trusted third party This approach significantly reduces the risk of unauthorized access by ensuring no single entity has complete control over the keys. ### Your Responsibilities You'll receive your client share as a PDF document, which you must: * **Securely store** in an encrypted location * **Use for operations** like generating new addresses and signing transactions * **Integrate** into your own system or use directly in the Oumla dashboard ### Example Share Document How the Oumla Share looks like For any questions about client shares or assistance with key management, please contact our support team at . Getting started with Oumla API is simple. Visit the dashboard to create your account and generate your API key. For a complete walkthrough with code examples, see our [Quick Start](/guides/quickstart) guide. Currently available: * **Bitcoin Testnet (tBTC)** - For testing Bitcoin integrations * **Ethereum Testnet Sepolia (tETH)** - For testing Ethereum smart contracts **Mainnet Activation Required** To activate mainnet access for Bitcoin (BTC) and Ethereum (ETH), you need to contact our support team and await approval. We will whitelist your account and require more information about your business. Click to send a pre-filled email to our support team Additional networks including Polygon, Avalanche, Binance Chain, Cardano, ICP, and Solana are planned for the future. Our support team is here to help! You can reach us at: We're committed to providing timely assistance with your integration, security questions, and any other concerns. # Supported Networks Source: https://docs.oumla.com/guides/networks Explore the blockchain networks supported by Oumla At Oumla, we work with various blockchains and we're continuously adding support for more. Our platform is designed to provide seamless integration with multiple blockchain networks. ## Currently Supported Networks **tBTC** (testnet) - Available now\ **BTC** (mainnet) - Requires activation **tETH** (testnet - Sepolia) - Available now **ETH** (mainnet) - Requires activation ## Network Comparison Understanding the differences between networks helps you choose the right one for your use case: | Feature | Bitcoin (BTC/tBTC) | Ethereum (ETH/tETH) | | ---------------------------- | --------------------------------- | --------------------------- | | **Network Type** | UTXO-based | Account-based | | **Address Format** | Bech32 (bc1...) | Hex (0x...) | | **Transaction Model** | UTXO (Unspent Transaction Output) | Account balance | | **Multi-address Withdrawal** | ✅ Supported | ❌ Single address only | | **Gas/Fees** | Transaction-based fees | Gas price × gas limit | | **Smart Contracts** | ❌ Limited (Bitcoin Script) | ✅ Full EVM support | | **Token Standards** | ❌ Not supported | ✅ ERC-20, ERC-721, ERC-1155 | | **Confirmation Time** | \~10 minutes | \~15 seconds (varies) | ## Bitcoin vs Ethereum: Key Differences ### Transaction Formats **Bitcoin:** * Uses UTXO model - transactions consume and create outputs * Multiple inputs can be combined in one transaction * Addresses are derived from public keys * Supports multi-signature transactions natively **Ethereum:** * Uses account-based model - transactions modify account balances * Each transaction has one sender account * Addresses are derived from private keys * Supports smart contracts and complex logic ### Address Generation **Bitcoin Addresses:** * Bech32 format: `bc1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlh` * Testnet format: `tb1q...` * Generated from public key hash **Ethereum Addresses:** * Hex format: `0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb` * 20-byte addresses (40 hex characters + 0x prefix) * Generated from private key ### Withdrawal Differences **Bitcoin Withdrawals:** * Can withdraw from multiple addresses in one transaction * More efficient for consolidating funds * Returns withdrawal ID for tracking **Ethereum Withdrawals:** * Single address per withdrawal * Returns transaction hash immediately * Can check status via transaction receipt See the [Withdrawal Workflow](/guides/workflows/withdrawal) guide for detailed examples of network-specific withdrawal behavior. ## Network Selection Guidance ### Choose Bitcoin (BTC/tBTC) When: * You need simple value transfers * You want multi-address withdrawal support * You're building payment applications * You need maximum security and decentralization * You're working with Bitcoin-native applications ### Choose Ethereum (ETH/tETH) When: * You need smart contract functionality * You're building DeFi applications * You need tokenization (ERC-20, ERC-721, ERC-1155) * You want faster transaction confirmations * You're building dApps or complex blockchain logic ## Testnet vs Mainnet ### Testnets **Purpose:** * Development and testing * No real value at risk * Faster iteration and experimentation **Available Testnets:** * **tBTC**: Bitcoin testnet * **tETH**: Ethereum Sepolia testnet **When to Use:** * During development * Testing integrations * Learning and experimentation * Prototyping new features ### Mainnets **Purpose:** * Production applications * Real value transactions * Live user interactions **Available Mainnets:** * **BTC**: Bitcoin mainnet * **ETH**: Ethereum mainnet **When to Use:** * Production deployments * Real user transactions * After thorough testing **Always test on testnet first!** Mainnet transactions involve real value and cannot be reversed. Test thoroughly on testnet before deploying to mainnet. ## Gas and Fee Handling ### Bitcoin Fees * Fees are calculated based on transaction size * Paid in BTC * Deducted from transaction amount * Can be estimated before sending **Example:** ```javascript theme={null} // Bitcoin withdrawal - fees deducted automatically const withdrawal = await client.withdraw.createWithdraw({ networkId: 'your-btc-network-id', from: ['address1', 'address2'], to: 'recipient', amount: '0.001', // Amount after fees clientShare: 'your-share' }); ``` ### Ethereum Gas * Gas price × gas limit = total fee * Paid in ETH (or network native token) * Gas prices fluctuate with network congestion * Can be estimated before sending **Example:** ```javascript theme={null} // Ethereum withdrawal - gas deducted from balance const withdrawal = await client.withdraw.createWithdraw({ networkId: 'your-eth-network-id', from: ['0x...'], to: '0x...', amount: '0.1', // Amount before gas clientShare: 'your-share' }); ``` Gas prices vary significantly. During high network congestion, consider waiting or using gas price estimation tools to optimize costs. ## Network-Specific Examples ### Bitcoin Address Generation ```javascript theme={null} // Generate Bitcoin address const address = await client.addresses.createAddressV2({ reference: 'user-123', networkId: 'your-btc-network-id', // Get from getNetworks() clientShare: 'your-client-share' }); console.log('Bitcoin Address:', address.data.data?.address); // Output: bc1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlh ``` ### Ethereum Address Generation ```javascript theme={null} // Generate Ethereum address const address = await client.addresses.createAddressV2({ reference: 'user-123', networkId: 'your-eth-network-id', // Get from getNetworks() clientShare: 'your-client-share' }); console.log('Ethereum Address:', address.data.data?.address); // Output: 0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb ``` ## Mainnet Activation **Mainnet Activation Required** To activate mainnet access for Bitcoin (BTC) and Ethereum (ETH), you need to contact our support team and await approval. We will whitelist your account and require more information about your business. Click to send a pre-filled email to our support team ## Planned Networks In the coming months, we plan to include support for: **Polygon** MATIC **Avalanche** AVAX **Binance Chain** BNB **Cardano** ADA **Internet Computer Protocol** ICP **Solana** SOL Additional blockchains coming soon ## Request a Network If you want us to support a specific blockchain, whether it's public or private, click to send a pre-filled email. We're here to assist you! # Ethereum transaction nonce explained Source: https://docs.oumla.com/guides/nonce-management/ethereum-transaction-nonce-explained # Ethereum nonce management for SDK developers The Ethereum transaction nonce is a sequential counter per account that enforces strict transaction ordering and prevents replay attacks — and it is the single most common source of stuck transactions, race conditions, and silent failures in production systems that send transactions programmatically. For SDK builders using ethers.js, mastering nonce mechanics is non-negotiable: the built-in `NonceManager` handles simple cases, but any system sending concurrent transactions from the same wallet needs a custom nonce management layer backed by atomic operations. This report covers the full depth of the problem, from Yellow Paper fundamentals through production-grade queue architectures. *** ## How the EVM enforces nonces at the protocol level Every Ethereum account (EOA or contract) stores four fields in the world state trie: **nonce**, balance, storageRoot, and codeHash. The nonce for an EOA represents the total number of transactions sent; for a contract account, it counts contract creations via CREATE. In the Yellow Paper, the transaction nonce is denoted **Tₙ** and the account state nonce is **σ\[a]ₙ**. The nonce is a `uint64` value and occupies **the first field** in legacy (Type 0) RLP-encoded transactions: `(nonce, gasPrice, gasLimit, to, value, data)`. For EIP-1559 (Type 2) transactions, it sits second after chainId: `0x02 || rlp([chainId, nonce, maxPriorityFeePerGas, maxFeePerGas, gasLimit, to, value, data, accessList, ...signature])`. The EVM's validation rule is strict equality: **the transaction nonce must exactly match the sender's current account nonce** (Tₙ = σ\[S(T)]ₙ). Not less, not greater — exact match. Upon execution, the very first operation is incrementing the sender's nonce by 1, before any value transfer or contract interaction. This increment persists even if the transaction reverts — gas is consumed and the nonce advances regardless of execution outcome. This strict sequencing creates two critical properties. First, **same-chain replay protection**: once nonce N executes, the account nonce becomes N+1, and any replay of the same signed transaction fails validation. Second, **total ordering of transactions per account**: nonce N cannot be included before nonce N-1, making it impossible to reorder a sender's transactions within the protocol. Cross-chain replay protection is handled separately by EIP-155, which embeds the chain ID into the transaction signing hash, making signatures chain-specific. One often-overlooked consequence: the CREATE opcode uses the sender's nonce to compute the new contract's address via `keccak256(rlp([sender, nonce]))[12:]`. This means the sender's nonce at deployment time deterministically controls where a contract deploys. *** ## Pending vs confirmed: how nodes actually track nonces The distinction between confirmed and pending nonces is where most production bugs originate. The **confirmed nonce** (`eth_getTransactionCount(address, "latest")`) reflects the on-chain state at the latest mined block — the count of transactions already included in the blockchain. The **pending nonce** (`eth_getTransactionCount(address, "pending")`) attempts to account for transactions sitting in the node's mempool, returning confirmed nonce + the count of contiguous pending transactions from that account. The critical word is "attempts." **There is no single global mempool.** Each node maintains its own independent transaction pool with different contents. When using load-balanced RPC providers like Alchemy or Infura, consecutive requests may hit different backend nodes with entirely different mempool views. Node A may have seen your pending transaction; Node B may not. This inconsistency is the root cause of most production nonce issues. Different node implementations also behave differently: * **Geth** splits its txpool into two sub-pools: **pending** (executable, with contiguous nonces from confirmed state) and **queued** (future nonces with gaps). Default limits are **5,120 pending slots** globally, **16 per account**, and **1,024 queued slots** with a **3-hour expiry**. Replacement transactions require at least a **10% gas price increase** (`txpool.pricebump = 10`). * **Erigon** uses three sub-pools — pending, baseFee (valid nonce but gas too low for current base fee), and queued — with a larger default pool size of 30,000. * **Infura has blocked `txpool_*` methods since 2018**, so you cannot inspect the mempool directly. Some providers effectively return the `latest` count even when `pending` is requested. When a transaction arrives at a node, validation proceeds in two stages. If the nonce is **below** the confirmed nonce, the transaction is immediately rejected with "nonce too low." If the nonce is **above** the next expected pending nonce (creating a gap), the transaction enters the queued pool — it won't be propagated to peers, won't be included in blocks, and will expire after a timeout. Only when a gap-filling transaction arrives can queued transactions be promoted to the executable pending pool. *** ## The five nonce failure modes every SDK must handle ### Nonce gaps block everything downstream A nonce gap occurs when a transaction is dropped from the mempool (due to low gas during congestion, pool eviction, or node restarts) while subsequent nonces have already been submitted. If your account's confirmed nonce is 5 and you've submitted nonces 5, 6, 7, but nonce 5 gets evicted, nonces 6 and 7 become permanently stuck in the queued pool until nonce 5 is resubmitted. In Geth, those queued transactions expire after 3 hours by default. ### Race conditions in concurrent transaction submission This is **the core challenge for SDK development**. When multiple threads call `eth_getTransactionCount("pending")` simultaneously, they all receive the same nonce. All but one submission will fail with "nonce has already been used." Even serial calls can race: due to the eventually consistent nature of Ethereum nodes, two sequential calls to `getTransactionCount("pending")` may return the same value if a transaction was submitted between them but hasn't propagated. As ethers.js author ricmoo noted: "some backends do not honour the 'pending' block tag at all." ### Transaction replacement requires precise gas bumping To replace a pending transaction, you submit a new transaction with the **same nonce** but higher gas. Geth requires at least **10% increase in both `maxFeePerGas` and `maxPriorityFeePerGas`**. A subtle edge case exists: if the base fee drops significantly between the original and replacement, recalculating `maxFeePerGas = 2 * baseFee + tip` might produce a *lower* fee cap than the original despite a higher tip, causing rejection as "replacement transaction underpriced." Always ensure replacement values are ≥110% of originals regardless of current base fee: ```typescript theme={null} const bumpedMaxFee = (originalTx.maxFeePerGas * 112n) / 100n; const bumpedPriority = (originalTx.maxPriorityFeePerGas * 112n) / 100n; const maxFeePerGas = bumpedMaxFee > feeData.maxFeePerGas ? bumpedMaxFee : feeData.maxFeePerGas; ``` ### Load-balanced RPCs create phantom nonce conflicts When your SDK uses a provider behind a load balancer, transaction submission may hit Node A while a subsequent nonce query hits Node B, which hasn't received the pending transaction yet. This produces stale nonce values that cause collisions. Broadcasting the same signed raw transaction to multiple nodes is safe (duplicates return "already known"), but reading nonce state from inconsistent endpoints is not. ### Mempool eviction silently breaks nonce sequences Geth's per-account pending limit is just **16 slots** by default. Submitting more than 16 concurrent transactions from one account causes later ones to be dropped. The evicted transactions leave no record — your system thinks they're pending, but the network has forgotten them. This silently creates nonce gaps that block all subsequent transactions. *** ## ethers.js nonce handling across v5 and v6 ### Automatic nonce population differs by version When you call `signer.sendTransaction()` without specifying a nonce, ethers.js auto-fills it during the internal `populateTransaction()` step. The default behavior changed significantly between versions: * **ethers.js v5** populates the nonce via `signer.getTransactionCount("latest")` — using only confirmed transactions, ignoring the mempool entirely * **ethers.js v6** populates via `signer.getNonce("pending")` — attempting to include mempool transactions This means v5's default behavior is inherently unsafe for rapid sequential transactions (you'll get duplicate nonces), while v6's default is better but still unreliable when the provider doesn't properly support the `"pending"` block tag. ### NonceManager: from experimental to core In v5, the NonceManager lives in a **separate package** (`@ethersproject/experimental`) and must be installed independently. In v6, it's part of the **core `ethers` package**. **v5 usage:** ```typescript theme={null} import { ethers } from "ethers"; import { NonceManager } from "@ethersproject/experimental"; const provider = new ethers.providers.JsonRpcProvider("http://localhost:8545"); const wallet = new ethers.Wallet(PRIVATE_KEY, provider); const managed = new NonceManager(wallet); // Sequential nonces assigned automatically const tx1 = await managed.sendTransaction({ to: addr1, value: ethers.utils.parseEther("0.01") }); const tx2 = await managed.sendTransaction({ to: addr2, value: ethers.utils.parseEther("0.01") }); ``` **v6 usage:** ```typescript theme={null} import { ethers, NonceManager } from "ethers"; const provider = new ethers.JsonRpcProvider("http://localhost:8545"); const wallet = new ethers.Wallet(PRIVATE_KEY, provider); const managed = new NonceManager(wallet); const tx1 = await managed.sendTransaction({ to: addr1, value: ethers.parseEther("0.01") }); const tx2 = await managed.sendTransaction({ to: addr2, value: ethers.parseEther("0.01") }); // Reset forces reload from chain (new in v6) managed.reset(); ``` Both versions work the same way internally: fetch the initial nonce from the network once, then maintain a local **delta counter** that increments synchronously with each `sendTransaction()` call. The delta is captured before the async nonce promise resolves, ensuring concurrent calls get sequential values. However, neither version handles failure recovery — if a transaction fails after the delta increments, a nonce gap is created. ### Key API differences between v5 and v6 | Aspect | v5 | v6 | | -------------------- | ------------------------------------------- | --------------------------------- | | Package | `@ethersproject/experimental` | Core `ethers` | | Nonce method | `getTransactionCount(blockTag)` | `getNonce(blockTag)` | | Increment | `incrementTransactionCount(count?)` | `increment()` | | Reset | `setTransactionCount(value)` | `reset()` (clears & reloads) | | Default block tag | `"latest"` | `"pending"` | | Provider constructor | `new ethers.providers.JsonRpcProvider(url)` | `new ethers.JsonRpcProvider(url)` | | BigNumber | `ethers.BigNumber` | Native `bigint` | | Internals | Public-ish `_deltaCount` | Private `#delta` | ### Manual nonce control in ethers.js For production SDKs, you'll typically manage nonces externally and pass them explicitly: ```typescript theme={null} // Works identically in v5 and v6 (adjust imports accordingly) const nonce = await myNonceManager.acquireNonce(wallet.address); const tx = await wallet.sendTransaction({ to: recipient, value: ethers.parseEther("0.01"), nonce: nonce, // explicit nonce overrides auto-fill }); // For contract calls, nonce goes in the overrides object const contract = new ethers.Contract(address, abi, wallet); await contract.transfer(recipient, amount, { nonce: nonce }); ``` ### Provider configuration for your own RPC node When running your own node, v6's `JsonRpcProvider` supports implicit request batching, which reduces latency when populating multiple transaction fields simultaneously: ```typescript theme={null} const provider = new ethers.JsonRpcProvider("http://your-node:8545", undefined, { staticNetwork: true, // Skip network auto-detection on each call batchMaxCount: 10, // Batch up to 10 JSON-RPC calls per HTTP request batchStallTime: 10, // Wait 10ms to collect requests before sending }); ``` This batching is new in v6 — v5's `JsonRpcProvider` did not support it natively. *** ## Production nonce architecture: Redis-backed queue pattern The ethers.js `NonceManager` is adequate for simple sequential use cases but breaks down under concurrent load, failure scenarios, and distributed systems. Production SDKs need an external nonce management layer. The dominant pattern, used by thirdweb Engine and OpenZeppelin Defender, combines a **Redis-backed atomic nonce counter** with a **transaction queue**. ### Architecture overview ``` ┌────────────┐ ┌─────────────────┐ ┌─────────────────┐ │ API Server │────▶│ Transaction │────▶│ TX Workers │ │ (accepts │ │ Queue (Redis/ │ │ (per-wallet │ │ requests) │ │ BullMQ) │ │ sequential) │ └────────────┘ └─────────────────┘ └────────┬────────┘ │ ┌─────────────────┐ │ │ Redis Nonce │◀──────────────┘ │ Store │ INCR atomically │ (per-wallet │ before signing │ per-chain) │ └─────────────────┘ ``` The API server accepts transaction requests and persists them to a queue immediately (fast response to callers). Worker processes consume from the queue sequentially per wallet, acquiring nonces atomically from Redis, simulating transactions, signing, and submitting to the RPC node. ### Redis-backed nonce manager implementation The key insight is using **Redis Lua scripts** for atomic read-and-increment operations, eliminating race conditions without distributed locks: ```typescript theme={null} import Redis from 'ioredis'; import { ethers } from 'ethers'; class RedisNonceManager { private redis: Redis; private provider: ethers.JsonRpcProvider; constructor(redisUrl: string, rpcUrl: string) { this.redis = new Redis(redisUrl); this.provider = new ethers.JsonRpcProvider(rpcUrl); } /** Sync nonce from on-chain state. Called on startup or after drift detection. */ async syncNonce(walletAddress: string): Promise { const onchainNonce = await this.provider.getTransactionCount(walletAddress, 'latest'); const key = `nonce:${walletAddress.toLowerCase()}`; // Lua script: only update if on-chain nonce is higher than stored value const lua = ` local current = redis.call('GET', KEYS[1]) if current == false or tonumber(current) < tonumber(ARGV[1]) then redis.call('SET', KEYS[1], ARGV[1]) return ARGV[1] end return current `; const result = await this.redis.eval(lua, 1, key, onchainNonce); return Number(result); } /** Atomically acquire next nonce. Lock-free via Lua script. */ async acquireNonce(walletAddress: string): Promise { const key = `nonce:${walletAddress.toLowerCase()}`; const lua = ` local current = redis.call('GET', KEYS[1]) if current == false then return nil end redis.call('SET', KEYS[1], tonumber(current) + 1) return current `; const result = await this.redis.eval(lua, 1, key); if (result === null) { await this.syncNonce(walletAddress); return this.acquireNonce(walletAddress); } return Number(result); } /** Recycle nonce if tx failed before broadcast (only if it's the highest). */ async recycleNonce(walletAddress: string, nonce: number): Promise { const key = `nonce:${walletAddress.toLowerCase()}`; const lua = ` local current = tonumber(redis.call('GET', KEYS[1])) if current == tonumber(ARGV[1]) + 1 then redis.call('SET', KEYS[1], ARGV[1]) return 1 end return 0 `; await this.redis.eval(lua, 1, key, nonce); } } ``` ### Transaction worker with retry and recovery ```typescript theme={null} async function processTransaction( wallet: ethers.Wallet, nonceManager: RedisNonceManager, txReq: { to: string; data: string; value: bigint; id: string } ): Promise { const nonce = await nonceManager.acquireNonce(wallet.address); try { // Simulate first — failed simulation should NOT consume the nonce const tx: ethers.TransactionRequest = { to: txReq.to, data: txReq.data, value: txReq.value, nonce, }; await wallet.provider!.estimateGas({ ...tx, from: wallet.address }); // Sign and send const response = await wallet.sendTransaction(tx); return response.hash; } catch (err: any) { // Recycle nonce only if tx was never broadcast to the network if (!err.receipt && !err.transactionHash) { await nonceManager.recycleNonce(wallet.address, nonce); } throw err; } } ``` ### Recovering from stuck transactions and nonce gaps When monitoring detects a stuck transaction (pending longer than a threshold), the SDK should either speed it up or cancel it: ```typescript theme={null} async function cancelStuckTransaction( wallet: ethers.Wallet, stuckNonce: number, originalMaxFeePerGas: bigint, originalMaxPriorityFeePerGas: bigint ): Promise { // Bump by 12% (safely above Geth's 10% minimum) const maxFeePerGas = (originalMaxFeePerGas * 112n) / 100n; const maxPriorityFeePerGas = (originalMaxPriorityFeePerGas * 112n) / 100n; return wallet.sendTransaction({ to: wallet.address, // self-transfer = cancellation value: 0n, nonce: stuckNonce, gasLimit: 21000n, maxFeePerGas, maxPriorityFeePerGas, type: 2, }); } async function fillNonceGaps( wallet: ethers.Wallet, gapNonces: number[] ): Promise { const feeData = await wallet.provider!.getFeeData(); const hashes: string[] = []; for (const nonce of gapNonces) { const tx = await wallet.sendTransaction({ to: wallet.address, value: 0n, nonce, gasLimit: 21000n, maxFeePerGas: feeData.maxFeePerGas! * 2n, maxPriorityFeePerGas: feeData.maxPriorityFeePerGas! * 2n, }); hashes.push(tx.hash); } return hashes; } ``` For gap detection, periodically compare your local next nonce against the node's `getTransactionCount("latest")` and check pending transaction receipts. Any transaction pending longer than 5 minutes with a null receipt is likely dropped and may need resubmission or gap-filling. *** ## Scaling beyond one wallet: multi-user and high-throughput patterns For an SDK serving multiple customers, each wallet address has its own independent nonce sequence. Key the nonce store as `nonce:{chainId}:{walletAddress}` to ensure complete isolation. One user's stuck transaction cannot affect another's nonce sequence. For **high-throughput systems** that exceed the practical limit of one wallet, the proven pattern is a **hot wallet pool**. OpenZeppelin Defender recommends **no more than 50 transactions per minute per relayer wallet**, especially on fast L2 chains. For 250 tx/min, use 5 wallets with a load balancer distributing requests across them. thirdweb Engine v2 demonstrated that this architecture with Redis-backed nonces achieves hundreds of transactions per second. Key design principles for production: * **Never trust `eth_getTransactionCount("pending")`** for high-throughput — maintain your own atomic counter in Redis * **Always simulate before sending** via `estimateGas` — failed simulations should recycle the nonce, not consume it * **Separate fast path from durable storage** — Redis for atomic nonce operations, PostgreSQL for transaction history and audit trail * **Implement idempotency keys** — clients provide a unique key per request; the system checks for duplicates before processing, preventing double-submission on retries * **Sync on every startup** — initialize nonce from `getTransactionCount("latest")`, verify all pending transactions from your database against on-chain receipts, and reconcile gaps * **Monitor nonce health** — track drift between local and on-chain nonce, age of oldest pending transaction, gap count, wallet balance, and transaction throughput per wallet ## Conclusion The Ethereum nonce is deceptively simple in specification — a sequential counter with strict equality validation — but produces a combinatorial explosion of edge cases in production systems. The most important architectural insight is that **the RPC node's pending nonce is fundamentally unreliable** for concurrent transaction submission, whether due to load-balanced endpoint inconsistency, provider-specific behavior differences, or mempool propagation delays. Every production SDK should maintain its own nonce counter with atomic operations (Redis Lua scripts being the dominant solution), implement transaction simulation before nonce commitment, and build automated recovery for the inevitable stuck transactions and nonce gaps. The ethers.js `NonceManager` provides a reasonable starting point for single-threaded applications, but its lack of failure recovery, persistence, and distributed coordination means it should be replaced with a custom implementation for any SDK handling concurrent transactions or multiple wallets. # Solution Source: https://docs.oumla.com/guides/nonce-management/solution # Ethereum Nonce Management — Problem Analysis & Solution Proposals ## Current Architecture ``` ┌──────────────────────┐ HTTP (Basic Auth) ┌────────────────────────┐ │ apps/core │ ──────────────────────────▶ │ apps/relayer │ │ │ │ │ │ WithdrawService │ 1. getNonceForAddress() │ EvmController │ │ ContractInteractions│ 2. getCurrentFeeEVM() │ │ │ │ TokenizationService │ 3. sendRawEVMTransaction() │ ▼ │ │ │ │ EvmService │ │ Signs tx locally │ │ │ │ │ with ethers.Wallet │ │ ▼ │ └──────────────────────┘ │ EvmProviderPool │ │ │ │ │ ┌────▼─────┐ │ │ │ Redis │ (nonce) │ │ └──────────┘ │ │ │ │ │ ┌────▼─────┐ │ │ │ RPC Node│ │ │ └──────────┘ │ └────────────────────────┘ ``` **Transaction lifecycle:** 1. `core` calls relayer's `POST /evm/address/nonce` to get the next nonce 2. `core` calls relayer's `POST /evm/getCurrentFee` to get gas prices 3. `core` signs the transaction locally with `ethers.Wallet.signTransaction()` 4. `core` calls relayer's `POST /evm/transaction/sendRaw` with the signed tx 5. Relayer broadcasts via `eth_sendRawTransaction` and increments tracked nonce in Redis **Nonce tracking (relayer):** * Redis key: `relayer:evm:nonce:{networkId}:{address}` * `getNonce()`: `max(onChainNonce, redisTrackedNonce)` → persisted via `SET` * `sendRawTransaction()` success: `INCR` on the nonce key (atomic) * Error recovery: `DEL` the key on nonce errors, forcing re-fetch from chain *** ## Identified Problems ### Problem 1: TOCTOU Race in `getNonce()` (Critical) **File:** `apps/relayer/src/evm/evm.service.ts:49-119` The `getNonce` → sign → `sendRawTransaction` → `INCR` flow is **not atomic**. Two concurrent callers can get the same nonce: ``` Instance A: getNonce() → reads Redis=5, chain=5 → returns 5 Instance B: getNonce() → reads Redis=5, chain=5 → returns 5 (A hasn't sent yet) Instance A: signs tx with nonce 5, sends → INCR → Redis=6 Instance B: signs tx with nonce 5, sends → "nonce already used" ✗ ``` This is the **core bug**. The nonce is read and returned without reservation. Between the read and the subsequent `INCR` (which only happens after broadcast), another caller can obtain the same value. ### Problem 2: Split Read/Write Across Services `core` fetches nonce via HTTP, signs locally, then sends signed tx back. This means: * The nonce is "in flight" for the entire duration of HTTP round-trip + signing + another HTTP round-trip * The relayer has no knowledge that a nonce has been "claimed" until the signed tx arrives * No way to recycle a nonce if `core` crashes between getNonce and sendRaw ### Problem 3: No Nonce Recycling on Pre-Broadcast Failures If `core` gets nonce 5, signs a tx, but the signing fails (bad key, encoding error) or the HTTP call to `sendRaw` times out, nonce 5 is consumed in Redis (via `setTrackedNonce`) but never broadcast. This creates a **nonce gap** — all subsequent transactions queue behind a phantom nonce 5 that will never be mined. ### Problem 4: `setTrackedNonce()` Overwrites Without CAS **File:** `apps/relayer/src/evm/evm.provider-pool.service.ts:243-250` `setTrackedNonce` is a plain `SET`, not a compare-and-swap. Concurrent `getNonce()` calls can clobber each other: ``` A: reads chain=7, tracked=5 → calculates max=7 B: reads chain=7, tracked=5 → calculates max=7 A: SET nonce=7 B: SET nonce=7 (harmless here, but if timing differs, could SET a stale value) ``` ### Problem 5: Circuit Breaker Was Tripping on Nonce Errors (Fixed in ca79d480) The recent fix `ca79d480` corrected an issue where nonce errors (a tx-level problem) were being recorded as provider failures, tripping the circuit breaker and blocking **all** subsequent transactions on that network. This is now fixed — only connection errors trip the breaker. ### Problem 6: No Stuck Transaction Recovery There is no mechanism to detect or recover from transactions that are: * Stuck in mempool (gas too low for current conditions) * Dropped by the mempool (eviction after 3hr in Geth) * Creating nonce gaps that block subsequent transactions *** ## Solution Approaches ### Approach A: Atomic Nonce Reservation via Redis Lua Script (Recommended) **Summary:** Replace the read-then-write nonce flow with a single atomic `acquireNonce` operation using a Redis Lua script. The nonce is reserved at read time, not after broadcast. **Changes required:** 1. **Relayer — new Lua-based `acquireNonce()` method** in `evm.provider-pool.service.ts`: ```typescript theme={null} async acquireNonce(address: string, networkId: string): Promise { const key = this.nonceKey(address, networkId); const lua = ` local current = redis.call('GET', KEYS[1]) if current == false then return nil end redis.call('SET', KEYS[1], tonumber(current) + 1) return current `; const result = await this.redis.client.eval(lua, 1, key); if (result === null) { // First call or after reset — seed from chain await this.syncNonceFromChain(address, networkId); return this.acquireNonce(address, networkId); } return Number(result); } ``` 2. **Relayer — new `syncNonceFromChain()` method** to initialize/re-seed: ```typescript theme={null} async syncNonceFromChain(address: string, networkId: string): Promise { const provider = await this.getProvider(networkId, rpcUrl); const chainNonce = await provider.getTransactionCount(address); const key = this.nonceKey(address, networkId); // Only set if key doesn't exist or chain is higher (Lua for atomicity) const lua = ` local current = redis.call('GET', KEYS[1]) if current == false or tonumber(current) < tonumber(ARGV[1]) then redis.call('SET', KEYS[1], ARGV[1]) end `; await this.redis.client.eval(lua, 1, key, chainNonce); } ``` 3. **Relayer — new `recycleNonce()` method** for failed-before-broadcast: ```typescript theme={null} async recycleNonce(address: string, networkId: string, nonce: number): Promise { const key = this.nonceKey(address, networkId); // Only recycle if it's still the "next" nonce (no one else took a later one) const lua = ` local current = tonumber(redis.call('GET', KEYS[1])) if current == tonumber(ARGV[1]) + 1 then redis.call('SET', KEYS[1], ARGV[1]) return 1 end return 0 `; await this.redis.client.eval(lua, 1, key, nonce); } ``` 4. **Relayer — modify `getNonce()` in `evm.service.ts`** to call `acquireNonce` instead of separate get + set. 5. **Relayer — remove `incrementNonce()` call from `sendRawTransaction()`** — the nonce was already reserved at acquire time. 6. **Relayer — add `POST /evm/address/nonce/recycle` endpoint** so `core` can release a nonce if signing fails. 7. **Core — add nonce recycling on pre-broadcast failure** in `withdraw.service.ts` and `contract-interactions.service.ts`: ```typescript theme={null} const nonce = await this.relayerClient.getNonceForAddress(from, networkId); try { const tx = await signer.signTransaction({ ...params, nonce }); return await this.relayerClient.sendRawEVMTransaction(tx, networkId); } catch (err) { await this.relayerClient.recycleNonce(from, networkId, nonce); throw err; } ``` **Pros:** * Eliminates the TOCTOU race — nonce is atomically reserved via Lua * Lock-free — Redis Lua scripts are single-threaded, no distributed lock needed * Nonce recycling prevents gaps from pre-broadcast failures * Minimal architectural change — same HTTP API contract between core and relayer * Same pattern used by thirdweb Engine and OZ Defender in production **Cons:** * Still a window between acquire and broadcast where the nonce is "claimed but unused" — if core crashes, the nonce is burned (mitigated by recycling + periodic sync) * Lua scripts add operational complexity to Redis (need to monitor script load) * Doesn't solve the broader problem of stuck-in-mempool transactions **Effort:** Medium (2–3 days). Mostly relayer changes + one new endpoint + try/catch in core. *** ### Approach B: Relayer-Side Signing (Move Nonce + Sign + Send Into Single Atomic Operation) **Summary:** Move transaction signing into the relayer. Core sends *unsigned* transaction intent (to, value, data, gasLimit). Relayer acquires nonce, fetches fee, signs, and broadcasts — all in one atomic operation with no cross-service race window. **Changes required:** 1. **Relayer — new `POST /evm/transaction/send` endpoint** that accepts unsigned tx params: ```typescript theme={null} // Request: { from, to, value, data?, gasLimit?, networkId } // Relayer: acquireNonce → getFee → sign → broadcast (all local, atomic) ``` 2. **Relayer — key management integration.** The relayer needs access to signing keys. Options: * a) Relayer calls `core`'s decryption service to get the key for each tx (adds latency, key in memory briefly) * b) Relayer holds a hot wallet key for relayer-owned wallets only (not user wallets) * c) Core sends the private key along with the tx params (security risk — key transits over HTTP) 3. **Core — replace `getNonce + sign + sendRaw` flow** with single `sendTransaction(from, to, value, ...)` call. 4. **Core — remove ethers.Wallet signing logic** from withdraw and contract services. **Pros:** * Completely eliminates the cross-service nonce race — nonce is acquired and consumed in the same process * Simplifies core's transaction code significantly * Relayer can implement sophisticated retry with re-signing at new nonce/gas * Relayer can do `estimateGas` before committing the nonce (simulation-before-nonce pattern) * Natural place to add transaction queuing, stuck tx recovery, gas bumping **Cons:** * **Major security concern**: relayer needs access to signing keys. Current architecture deliberately isolates crypto operations in `signing-server` and `decryption-server`. Moving signing to relayer breaks this security boundary. * Large architectural change — touching withdraw, contract-interactions, tokenization services * Need to handle the decryption/threshold signature flow (clientShare + masterWallet share) somehow * Risk of introducing new bugs during migration * Doesn't work for user-initiated transactions that require client-side shares (the signer is derived from user-provided `clientShare` + server `encryptedShare`) **Effort:** Large (1–2 weeks). Requires careful security review and migration of all tx flows. **Verdict:** Not viable for user wallet transactions because the key derivation requires a client share that only exists during the user's request. Could work for system-owned hot wallets (e.g., gas station, contract deployer) but would require a parallel signing path. *** ### Approach C: Pessimistic Distributed Lock on getNonce **Summary:** Wrap the entire getNonce → sign → sendRaw flow in a per-address distributed lock (Redis Redlock or simpler `SET NX EX`). Only one transaction per address can be in flight at a time. **Changes required:** 1. **Relayer — add `POST /evm/address/nonce/lock` and `/unlock` endpoints:** ```typescript theme={null} // Lock: SET relayer:evm:lock:{networkId}:{address} {requestId} NX EX 30 // Returns: { nonce, lockId } // Unlock: DEL if lockId matches (Lua CAS) ``` 2. **Core — wrap tx flow in lock/unlock:** ```typescript theme={null} const { nonce, lockId } = await this.relayerClient.lockAndGetNonce(from, networkId); try { const tx = await signer.signTransaction({ ...params, nonce }); await this.relayerClient.sendRawEVMTransaction(tx, networkId); } finally { await this.relayerClient.unlockNonce(from, networkId, lockId); } ``` 3. **Relayer — auto-expire locks** after TTL to prevent deadlocks from crashed callers. **Pros:** * Simple mental model — only one tx per address at a time, no races possible * No Lua scripts needed beyond a simple CAS for unlock * Easy to reason about correctness **Cons:** * **Serializes all transactions per address** — massive throughput bottleneck. If a user has 3 pending withdrawals, they execute sequentially (getNonce → sign → send → wait → next). Each cycle is \~2-5 seconds minimum. * Lock expiry is a tradeoff: too short → lock expires during legitimate signing (especially with slow decryption); too long → stuck locks block subsequent txs * Distributed lock algorithms (Redlock) are complex and have known issues (see Martin Kleppmann's critique) * Adds two more HTTP round-trips per transaction (lock + unlock) * If core crashes between lock and unlock, the address is locked until TTL expires **Effort:** Medium (2–3 days). But the throughput cost is permanent. *** ### Approach D: Optimistic Nonce with Conflict Detection and Auto-Retry **Summary:** Keep the current architecture but add a nonce conflict detection layer. When `sendRawTransaction` fails with "nonce already used," the relayer automatically re-fetches the nonce from chain, and core retries the entire sign-and-send flow. **Changes required:** 1. **Relayer — enhance error responses** to return structured nonce-conflict errors: ```json theme={null} { "error": "NONCE_CONFLICT", "currentNonce": 7, "attemptedNonce": 5, "shouldRetry": true } ``` 2. **Core — add retry loop around the full tx flow** (not just sendRaw): ```typescript theme={null} for (let attempt = 0; attempt < MAX_NONCE_RETRIES; attempt++) { const nonce = await this.relayerClient.getNonceForAddress(from, networkId); const tx = await signer.signTransaction({ ...params, nonce }); try { return await this.relayerClient.sendRawEVMTransaction(tx, networkId); } catch (err) { if (isNonceConflict(err) && attempt < MAX_NONCE_RETRIES - 1) { this.logger.warn(`Nonce conflict (attempt ${attempt + 1}), retrying`); continue; } throw err; } } ``` 3. **Relayer — change `getNonce()` to always re-sync from chain on call** (not just `max(tracked, chain)`) to get the freshest value after a conflict. **Pros:** * Minimal changes to existing architecture * No Lua scripts, no distributed locks * Works with the existing security model (core holds keys, relayer broadcasts) * Handles the common case (low concurrency per address) efficiently **Cons:** * Optimistic approach means conflicts are detected *after* wasting a signing round-trip * Under high concurrency (multiple txs from same address), retry storms can occur — N concurrent txs means up to N-1 retries each * Each retry requires re-deriving the signer (decryption round-trip), which is expensive * Doesn't prevent nonce gaps from dropped mempool txs * Relies on the RPC node returning an accurate nonce, which is unreliable with load-balanced providers **Effort:** Small (1–2 days). But doesn't fully solve the problem — it papers over the race with retries. *** ### Approach E: Hybrid — Atomic Reservation + Stuck Transaction Monitor (Recommended for Production) **Summary:** Combine Approach A (atomic Lua reservation) with a background monitor that detects and resolves stuck transactions and nonce gaps. This is the production-grade solution used by thirdweb Engine and OZ Defender. **Changes required (in addition to Approach A):** 1. **Relayer — add transaction tracking table** (Postgres or Redis hash): ``` { txHash, from, networkId, nonce, gasPrice, submittedAt, status: 'pending' | 'confirmed' | 'dropped', lastCheckedAt } ``` 2. **Relayer — background nonce health monitor** (runs every 30s per active address): ```typescript theme={null} async monitorNonceHealth(address: string, networkId: string) { const chainNonce = await provider.getTransactionCount(address, 'latest'); const trackedNonce = await this.getTrackedNonce(address, networkId); const pendingTxs = await this.getPendingTxs(address, networkId); // Detect gaps: chain says nonce=5, but we have no pending tx for nonce 5 // Detect stuck: tx submitted > 5 min ago, receipt is null // Detect drift: tracked nonce diverged significantly from chain } ``` 3. **Relayer — stuck transaction recovery:** * If a pending tx is older than threshold (5 min): attempt gas bump (replace with +12% fee, same nonce) * If a pending tx is older than hard limit (30 min): cancel via self-transfer at same nonce with high gas * If nonce gap detected: fill with self-transfer 4. **Relayer — periodic nonce reconciliation** on startup and every N minutes: ```typescript theme={null} // On startup: syncNonceFromChain() for all known addresses // Periodic: compare tracked vs chain, log drift, auto-correct if safe ``` 5. **Relayer — nonce health metrics endpoint** for observability: ```json theme={null} GET /evm/nonce/health { "address": "0x...", "trackedNonce": 42, "chainNonce": 40, "pendingCount": 2, "oldestPendingAge": "45s", "gapCount": 0 } ``` **Pros:** * Solves both the race condition (Lua) AND the stuck transaction problem (monitor) * Self-healing — the system automatically recovers from nonce gaps and stuck txs * Observable — metrics and health endpoint for ops * Battle-tested pattern — this is what production relayer systems use * Incremental — can deploy Approach A first, add the monitor later **Cons:** * Most complex to implement — touches relayer storage, adds background jobs * Stuck tx recovery requires the relayer to sign self-transfer transactions (needs a key for the relayer's own address, not user addresses) * Gas bump for stuck user transactions requires re-signing, which needs the user's key (not available) * The monitor adds load on RPC nodes (getTransactionCount + getTransactionReceipt per address) **Effort:** Large (1–2 weeks for full implementation). But can be staged: Approach A first (2–3 days), monitor later (3–5 days). *** ## Comparison Matrix | Criteria | A: Lua Atomic | B: Relayer Signs | C: Dist Lock | D: Optimistic | E: Hybrid (A+Monitor) | | ------------------------ | :-----------: | :--------------: | :----------: | :-----------: | :-------------------: | | Eliminates nonce race | ✅ | ✅ | ✅ | ❌ (retries) | ✅ | | Prevents nonce gaps | Partial | ✅ | ✅ | ❌ | ✅ | | Handles stuck txs | ❌ | ✅ (can re-sign) | ❌ | ❌ | ✅ | | Preserves security model | ✅ | ❌ | ✅ | ✅ | ✅ | | Concurrent tx throughput | High | High | Low (serial) | Medium | High | | Implementation effort | Medium | Large | Medium | Small | Large | | Works for user wallets | ✅ | ❌ (needs key) | ✅ | ✅ | ✅ (monitor limited) | | Operational complexity | Low | High | Medium | Low | Medium | ## Recommendation **Start with Approach A (Atomic Nonce Reservation)** — it solves the critical race condition with moderate effort and zero architectural disruption. The key changes are: 1. Replace `getTrackedNonce + setTrackedNonce` with atomic `acquireNonce` (Lua) 2. Remove `incrementNonce` from `sendRawTransaction` (already reserved) 3. Add `recycleNonce` endpoint for pre-broadcast failures 4. Add try/catch with recycle in core's tx flows **Then incrementally add Approach E's monitor** for stuck transaction detection and nonce gap recovery. The monitor is valuable but not blocking — the atomic reservation alone fixes the most common failure mode (concurrent nonce collision). The per-address stuck transaction recovery (gas bumping, cancellation) is limited for user wallets since the relayer doesn't hold user keys. For user wallets, the practical recovery path is: detect the stuck nonce, alert the user or system, and have core re-derive the signer to submit a replacement. This can be a Temporal workflow that periodically checks pending txs. # Quick Start Source: https://docs.oumla.com/guides/quickstart Get started with Oumla API in 5 minutes Oumla enables developers to easily build on top of blockchains. You don't need to learn complex, low-level Blockchain-specific APIs. By using our API, you can accelerate your time to market and minimize security concerns. ## Two Ways to Use Oumla Oumla provides **two ways** to interact with the API: **Recommended for most developers**. Use our official TypeScript SDK for type-safe, easy-to-use methods. The SDK handles authentication, request formatting, and error handling automatically. **Direct HTTP calls**. Make direct REST API calls to our endpoints using any HTTP client. Perfect for non-TypeScript projects or when you need full control over requests. **Which should I use?** If you're building in TypeScript/JavaScript, we recommend starting with the SDK for a better developer experience. Both methods provide the same functionality and use the same API endpoints under the hood. ## Understanding the Oumla Model Before diving in, it's helpful to understand how Oumla organizes resources: A **Profile** represents a user, department, or merchant in your system. Profiles are the foundation for all other resources. Each profile has a unique `reference` identifier and a `type` (User, Department, or Merchant). A **Wallet** is associated with a profile and a specific blockchain network (BTC, ETH, etc.). Wallets manage addresses and enable transactions on that network. An **Address** belongs to a wallet and is used to send and receive funds on a specific blockchain. Each address is unique to a network (BTC addresses differ from ETH addresses). The relationship flows: **Profile → Wallet → Address → Transaction** **Address V2**: The V2 address generation endpoint (`/api/v2/addresses/generate`) automatically creates a wallet if one doesn't exist, simplifying the workflow. This is the recommended approach for most use cases. ## Prerequisites Before you begin, make sure you have: Sign up at the Oumla dashboard to get started Generate your API key from the dashboard All API requests require authentication via the `x-api-key` header. See the [Authentication](/guides/authentication) guide for details. ## Choose Your Integration Method ### Option 1: Use the TypeScript SDK (Recommended) The SDK provides a type-safe, easy-to-use interface: ```bash theme={null} npm install @oumla/sdk ``` ```javascript theme={null} import { OumlaSdkApiClient, OumlaSdkApiEnvironment } from '@oumla/sdk'; const client = new OumlaSdkApiClient({ environment: OumlaSdkApiEnvironment.Sandbox, apiKey: 'your-api-key-here' }); ``` ### Option 2: Use Direct REST API Calls Make HTTP requests directly to our API endpoints: ```bash theme={null} # No installation needed - use any HTTP client curl -X POST "https://sandbox.oumla.com/api/v1/profiles" \ -H "x-api-key: your-api-key-here" \ -H "Content-Type: application/json" \ -d '{"reference": "user-123", "type": "User"}' ``` ## Make Your First API Call Let's create a user profile - the foundation for wallets and transactions: ```bash cURL theme={null} curl -X POST "https://sandbox.oumla.com/api/v1/profiles" \ -H "x-api-key: your-api-key-here" \ -H "Content-Type: application/json" \ -d '{ "reference": "user-123", "type": "User" }' ``` ```javascript JavaScript (SDK) theme={null} // Using the TypeScript SDK import { OumlaSdkApiClient, OumlaSdkApiEnvironment } from '@oumla/sdk'; const client = new OumlaSdkApiClient({ environment: OumlaSdkApiEnvironment.Sandbox, apiKey: 'your-api-key-here' }); const profile = await client.profiles.createProfile({ reference: 'user-123', type: 'User' }); console.log(profile); ``` ```javascript JavaScript (REST API) theme={null} const response = await fetch('https://sandbox.oumla.com/api/v1/profiles', { method: 'POST', headers: { 'x-api-key': 'your-api-key-here', 'Content-Type': 'application/json' }, body: JSON.stringify({ reference: 'user-123', type: 'User' }) }); const data = await response.json(); console.log(data); ``` ```python Python theme={null} import requests headers = { 'x-api-key': 'your-api-key-here', 'Content-Type': 'application/json' } payload = { 'reference': 'user-123', 'type': 'User' } response = requests.post('https://sandbox.oumla.com/api/v1/profiles', headers=headers, json=payload) print(response.json()) ``` ```go Go theme={null} package main import ( "bytes" "encoding/json" "fmt" "io" "net/http" ) func main() { url := "https://sandbox.oumla.com/api/v1/profiles" payload := map[string]string{ "reference": "user-123", "type": "User", } jsonData, _ := json.Marshal(payload) req, _ := http.NewRequest("POST", url, bytes.NewBuffer(jsonData)) req.Header.Set("x-api-key", "your-api-key-here") req.Header.Set("Content-Type", "application/json") client := &http.Client{} resp, _ := client.Do(req) defer resp.Body.Close() body, _ := io.ReadAll(resp.Body) fmt.Println(string(body)) } ``` ```json theme={null} { "message": "Profile created", "data": { "reference": "user-123", "type": "User" }, "success": true, "status": 201 } ``` ## Complete Workflow Example Here's a complete example using the **SDK** that creates a profile, generates an address (which auto-creates a wallet), and checks the balance: The same workflow can be achieved using direct REST API calls. See the [API Reference](/api-reference/introduction) for endpoint details. ```javascript theme={null} import { OumlaSdkApiClient, OumlaSdkApiEnvironment } from '@oumla/sdk'; const client = new OumlaSdkApiClient({ environment: OumlaSdkApiEnvironment.Sandbox, apiKey: 'your-api-key-here' }); async function completeWorkflow() { try { // Step 1: Create a profile const profile = await client.profiles.createProfile({ reference: 'user-123', type: 'User' }); console.log('Profile created:', profile); // Step 2: Get available networks const networks = await client.networks.getNetworks({ skip: 0, take: 50, enabled: true }); const networkId = networks.data.networks[0].id; // Use appropriate network ID // Step 3: Generate an address (V2 auto-creates wallet if needed) const address = await client.addresses.createAddressV2({ reference: 'user-123', networkId, clientShare: 'your-client-share-here' }); const generatedAddress = address.data.data?.address; console.log('Address generated:', generatedAddress); // Step 4: Check native balance const balance = await client.assets.getNativeBalance({ address: generatedAddress!, skip: 0, take: 50 }); console.log('Balance:', balance.data.balance); } catch (error) { console.error('Error:', error); } } completeWorkflow(); ``` ## Profile Types Oumla supports three profile types: * **User**: Individual users in your system * **Department**: Organizational units or departments * **Merchant**: Business entities or merchants Choose the type that best fits your use case. The `reference` field should be a unique identifier within your system. ## Next Steps Discover wallets, transactions, smart contracts, and tokenization capabilities Browse the complete API reference with interactive playground Understand Oumla's hybrid key management model Get inspired by what you can build with Oumla **Pro Tip**: Use the interactive API playground in the [API Reference](/api-reference/introduction) to test endpoints with your API key before integrating into your application. # Security Source: https://docs.oumla.com/guides/security Learn about Oumla's hybrid key management model and security best practices At Oumla, we understand that key security is paramount in blockchain development. Our managed infrastructure solution employs cutting-edge security practices to protect your private and public keys, allowing you to focus on building your application with peace of mind. ## Hybrid Key Management Model Oumla utilizes a robust hybrid model for key management, enhancing security through key splitting: One part of the key remains with you, the client. We call this the "Oumla Share". Another part is securely stored by Oumla in our encrypted infrastructure. The final part is entrusted to a reliable third party for additional security. This three-way split ensures that no single entity has complete control over the keys, significantly reducing the risk of unauthorized access or compromise. ## Client Responsibilities As a client, you play a crucial role in maintaining the security of your Oumla Share: Upon registration, you'll receive a PDF document containing your Oumla Share. You must store this file in a secure location, such as an encrypted drive or a physical safe. Implement proper access controls to ensure only authorized personnel can access the Oumla Share. Regularly review and update your security measures to protect this critical information. ## Operational Requirements Your Oumla Share is essential for various blockchain operations: * **Generating new addresses** for receiving assets * **Signing transactions** to move funds or interact with contracts * **Other key-dependent functions** within the platform You'll need to provide your Oumla Share securely when performing these operations through our platform. ## Security Best Practices To further enhance the security of your Oumla Share: Use strong, unique passwords for any systems storing the share. Avoid using the same password across multiple platforms and consider using a password manager. Implement multi-factor authentication (MFA) wherever possible to add an extra layer of security to systems that store or access your Oumla Share. Regularly back up your Oumla Share, ensuring backups are also securely stored in encrypted locations. Test your backups periodically to ensure they can be restored when needed. Establish a clear protocol for share access within your organization. Document who has access, under what circumstances, and maintain an audit trail of all access attempts. ## Support and Assistance Our dedicated support team is always ready to assist you with any questions or concerns regarding key management and security. We're committed to your success in blockchain development and can provide guidance on best practices for securing your Oumla Share. Need help with security or key management? Reach out to our support team at Remember, while Oumla provides advanced security measures, the safety of your Oumla Share is a shared responsibility. By following these guidelines and maintaining vigilant security practices, you can ensure the highest level of protection for your blockchain assets. # Use Cases Source: https://docs.oumla.com/guides/use-cases Discover what you can build with Oumla API Oumla can be used in many different ways because its APIs and SDKs are designed to be flexible and adaptable, allowing developers to customize them according to their needs. For example: ### Tokenization Platform You have the option to create a tokenization platform using Oumla that leverages the Ethereum blockchain. This platform would enable users to securely store their assets on the blockchain and facilitate seamless asset trading. ### Investment Platform You can develop an investment or trading platform that provides users with a streamlined experience for buying and trading digital assets. The platform helps address securities-related considerations and simplifies the operational complexity of blockchain infrastructure. ### Gaming You have the opportunity to integrate your game with web3 and blockchain technology, allowing for exciting features such as pay-to-earn. By issuing tokens to players within your game, you can create a model where players can earn rewards that hold real-world value. This opens up new possibilities for engaging gameplay experiences and a thriving in-game economy. ### Wallets You have the ability to create a customized wallet that precisely fits your requirements effortlessly. This personalized wallet will offer you the flexibility to integrate the desired features and functionalities that perfectly align with your needs, all while ensuring robust security measures are in place, relieving you from the worry of potential security issues. ### And Way More! If you have a project or idea you'd like to build, please don't hesitate to reach out to us at . We'll be more than happy to assist you in designing and architecting the system to fulfill your requirements. The possibilities are **limitless**. # Contract Deployment Workflow Source: https://docs.oumla.com/guides/workflows/contract-deployment Complete guide to deploying smart contracts using templates This guide walks you through deploying smart contracts using Oumla's contract template system. **Two Ways to Use Oumla**: This guide uses the TypeScript SDK. You can also accomplish the same tasks using direct REST API calls - see the [API Reference](/api-reference/introduction) for endpoint details. ## Overview The contract deployment workflow involves: 1. **Create or Select Template** - Use an existing template or create a new one 2. **Get Constructor Info** - Understand required constructor parameters 3. **Deploy Contract** - Deploy with your parameters 4. **Check Status** - Monitor the async deployment workflow 5. **Interact with Contract** - Call functions on the deployed contract Contract deployment uses **async workflows** for async processing. You'll need to check workflow status to know when deployment completes. ## Prerequisites Before starting, ensure you have: * An initialized SDK client * A profile created * An address generated (for contract deployment) * Your client share (for signing operations) ## Step 1: Get Available Templates List available contract templates: ```javascript theme={null} import { OumlaSdkApiClient, OumlaSdkApiEnvironment } from '@oumla/sdk'; const client = new OumlaSdkApiClient({ environment: OumlaSdkApiEnvironment.Sandbox, apiKey: 'your-api-key-here' }); // Get all templates const templates = await client.contractTemplates.getContracts({ skip: 0, take: 25 }); console.log('Available templates:', templates.data); ``` ## Step 2: Get Template Details Retrieve details for a specific template: ```javascript theme={null} const template = await client.contractTemplates.getContractById('template-id-here'); console.log('Template:', template.data); console.log('Name:', template.data.name); console.log('Network:', template.data.network); ``` ## Step 3: Get Constructor Information Before deploying, understand what constructor parameters are required: ```javascript theme={null} const constructorInfo = await client.contractTemplates.getContractConstructorById('template-id-here'); // inputs describes the constructor's argument list console.log('Constructor inputs:', constructorInfo.data.contractConstructor?.inputs); console.log('Constructor type:', constructorInfo.data.contractConstructor?.type); ``` ### Understanding Constructor Parameters Constructor parameters are sent as an **array of objects**, where each object contains `name`, `type`, and `value`: ```javascript theme={null} // Example constructor parameters structure [ { name: 'name', // Parameter name type: 'string', // Parameter type value: 'MyToken' // Parameter value }, { name: 'symbol', type: 'string', value: 'MTK' }, { name: 'initialSupply', type: 'uint256', value: '1000000' } ] ``` The `getContractConstructorById()` method returns the constructor parameter definitions. Use those to build your `constructorParameters` array with the correct `name`, `type`, and `value` for each parameter. ## Step 4: Get Function Information Understand available functions on the contract: ```javascript theme={null} // Get all functions const functions = await client.contractTemplates.getContractFunctionById('template-id-here'); console.log('Available functions:', functions.data); // Functions include both read and write operations functions.data.forEach(func => { console.log(`Function: ${func.name}`); console.log(`Type: ${func.type}`); // 'read' or 'write' console.log(`Parameters:`, func.parameters); }); ``` ## Step 5: Deploy Contract Deploy the contract with your parameters: ```javascript theme={null} const deployment = await client.contractTemplates.deployContract('template-id-here', { networkId: 'your-network-id', addressId: 'your-address-id', clientShare: 'your-client-share', constructorParameters: [ { name: 'name', type: 'string', value: 'MyToken' }, { name: 'symbol', type: 'string', value: 'MTK' }, { name: 'initialSupply', type: 'uint256', value: '1000000' } ] }); console.log('Deployment Workflow ID:', deployment.data.workflowResult.workflowId); ``` Ensure constructor parameters match the types and order defined in the constructor info. Incorrect parameters will cause deployment to fail. ## Step 6: Check Deployment Status Deployment is async. Check workflow status: ```javascript theme={null} const status = await client.workflows.getWorkflowStatus(deployment.data.workflowResult.workflowId); console.log('Status:', status.data.status); // COMPLETED, RUNNING, FAILED if (status.data.status === 'COMPLETED') { console.log('Contract deployed at:', status.data.result.contractAddress); console.log('Transaction Hash:', status.data.result.transactionHash); } else if (status.data.status === 'FAILED') { console.error('Deployment failed:', status.data.error); } ``` ### Workflow Status Values * **RUNNING**: Deployment in progress * **COMPLETED**: Deployment successful * **FAILED**: Deployment failed (check error details) * **CANCELLED**: Deployment was cancelled ## Step 7: Get Deployed Contract Details Once deployment completes, retrieve contract details: ```javascript theme={null} const deployedContract = await client.deployedContracts.getDeployedContractById( deployment.data.workflowResult.operationId ); console.log('Contract Address:', deployedContract.data.deployedContract?.contractAddress); console.log('Network:', deployedContract.data.deployedContract?.network); console.log('Template ID:', deployedContract.data.deployedContract?.contractTemplateId); ``` ## Step 8: Interact with Contract After deployment, you can interact with the contract: ### Read Functions ```javascript theme={null} const result = await client.contractInteractions.readCallFunction( 'your-network-id', deployedContract.data.deployedContract?.contractAddress!, { abiFunction: { name: 'name', type: 'function', inputs: [], outputs: [{ name: '', type: 'string' }] } } ); console.log('Function result:', result.data.result?.data?.result); ``` ### Write Functions ```javascript theme={null} const writeResult = await client.contractInteractions.writeCallFunction( 'your-network-id', deployedContract.data.deployedContract?.contractAddress!, { addressId: 'your-address-id', clientShare: 'your-client-share', abiFunction: { name: 'transfer', type: 'function', inputs: [ { name: 'to', type: 'address' }, { name: 'amount', type: 'uint256' } ], outputs: [] }, amount: '0', feeLevel: 'standard' } ); console.log('Workflow ID:', writeResult.data.workflowResult.workflowId); // Poll the workflow until it completes to get the transaction hash from the result payload const writeStatus = await client.workflows.getWorkflowStatus(writeResult.data.workflowResult.workflowId); const transactionHash = writeStatus.data.result?.transactionHash; // Then fetch the transaction receipt const receipt = await client.contractInteractions.getTransactionReceipt('your-network-id', transactionHash); console.log('Transaction Status:', receipt.data.receipt?.status); ``` ## Complete Example Here's a complete example: ```javascript theme={null} import { OumlaSdkApiClient, OumlaSdkApiEnvironment } from '@oumla/sdk'; const client = new OumlaSdkApiClient({ environment: OumlaSdkApiEnvironment.Sandbox, apiKey: process.env.OUMLA_API_KEY }); async function deployContract() { try { // Step 1: Get templates const templates = await client.contractTemplates.getContracts({ skip: 0, take: 10 }); const template = templates.data.result?.data?.[0]; // Use first template console.log('Using template:', template?.name); // Step 2: Get constructor info const constructorInfo = await client.contractTemplates.getContractConstructorById(template!.id!); console.log('Constructor inputs:', constructorInfo.data.contractConstructor?.inputs); // Step 3: Deploy contract const deployment = await client.contractTemplates.deployContract(template!.id!, { addressId: process.env.ADDRESS_ID, clientShare: process.env.CLIENT_SHARE, constructorParameters: [ // Map parameters based on constructor info { name: 'name', type: 'string', value: 'MyToken' }, { name: 'symbol', type: 'string', value: 'MTK' } ] }); console.log('Deployment started. Workflow ID:', deployment.data.workflowResult.workflowId); // Step 4: Wait for deployment let status; do { await new Promise(resolve => setTimeout(resolve, 3000)); status = await client.workflows.getWorkflowStatus(deployment.data.workflowResult.workflowId); console.log('Status:', status.data.status); } while (status.data.status === 'RUNNING'); if (status.data.status === 'COMPLETED') { const contractAddress = status.data.result.contractAddress; console.log('Contract deployed at:', contractAddress); // Step 5: Interact with contract const name = await client.contractInteractions.readCallFunction( 'your-network-id', contractAddress, { abiFunction: { name: 'name', type: 'function', inputs: [], outputs: [{ name: '', type: 'string' }] } } ); console.log('Contract name:', name.data.result?.data?.result); } } catch (error) { console.error('Error:', error); } } deployContract(); ``` ## Managing Contract ABIs ### Fetch ABI from Contract Address ```javascript theme={null} const abi = await client.deployedContracts.fetchContractAbi({ networkId: 'your-network-id', contractAddress: '0x...' }); console.log('Contract ABI:', abi.data.contractAbi?.abi); ``` ### Add Custom ABI ```javascript theme={null} await client.deployedContracts.addContractAbi({ networkId: 'your-network-id', contractAddress: '0x...', name: 'MyContract', abi: [ // Your ABI JSON array ] }); ``` ## Common Issues ### Deployment Fails **Causes:** * Invalid constructor parameters * Insufficient gas balance * Network connectivity issues **Solutions:** * Verify constructor parameters match template requirements * Ensure address has sufficient balance for gas * Check network status and retry ### Constructor Parameter Errors **Causes:** * Wrong parameter types * Missing required parameters * Parameter order mismatch **Solutions:** * Review constructor info carefully * Ensure types match (string, uint256, address, etc.) * Provide all required parameters ### Function Call Fails **Causes:** * Function doesn't exist * Wrong parameter types * Insufficient gas **Solutions:** * Verify function exists in contract ABI * Check parameter types match function signature * Ensure sufficient gas balance ## Best Practices 1. **Always check constructor info** before deploying 2. **Validate parameters** before deployment 3. **Monitor workflow status** until completion 4. **Store deployment IDs** for future reference 5. **Handle errors gracefully** - deployments can fail 6. **Test on testnet** before mainnet deployment 7. **Verify contract address** after deployment ## Next Steps * Learn about [Tokenization Workflow](/guides/workflows/tokenization) * Explore [Withdrawal Workflow](/guides/workflows/withdrawal) * Review [Error Reference](/guides/error-reference) for troubleshooting # Tokenization Workflow Source: https://docs.oumla.com/guides/workflows/tokenization Complete guide to creating collections, minting tokens, and managing NFTs This guide walks you through the complete tokenization workflow using the Oumla SDK, from creating collections to minting and burning tokens. **Two Ways to Use Oumla**: This guide uses the TypeScript SDK. You can also accomplish the same tasks using direct REST API calls - see the [API Reference](/api-reference/introduction) for endpoint details. ## Overview The tokenization workflow involves several steps: 1. **Create Collection** - Deploy an NFT collection contract (ERC-721 or ERC-1155) 2. **Check Status** - Monitor the async deployment workflow 3. **Mint Tokens** - Mint NFTs to specific addresses 4. **View Tokens** - Query tokens in your collection 5. **Burn Tokens** - Remove tokens from circulation (optional) Collection creation and minting use **async workflows** for async processing. You'll need to check workflow status to know when operations complete. ## Prerequisites Before starting, ensure you have: * An initialized SDK client * A profile created * An address generated (for contract deployment) * Your client share (for signing operations) ## Step 1: Create a Collection Creating a collection deploys a new NFT contract. This is an **async operation** that returns a workflow ID. ```javascript theme={null} import { OumlaSdkApiClient, OumlaSdkApiEnvironment } from '@oumla/sdk'; const client = new OumlaSdkApiClient({ environment: OumlaSdkApiEnvironment.Sandbox, apiKey: 'your-api-key-here' }); // Create an ERC-721 collection (NON_FUNGIBLE_TOKEN) const collection = await client.tokenization.createCollection({ addressId: 'your-address-id', type: 'NON_FUNGIBLE_TOKEN', clientShare: 'your-client-share', network: 'ETHEREUM', displayName: 'My NFT Collection', useGasless: false, fee: '0', feeLevel: 'standard', description: 'Optional collection description', createParams: { initializeParams: [ { name: '_name', type: 'string', value: 'My Collection' }, { name: '_symbol', type: 'string', value: 'MYC' }, { name: 'defaultAdmin', type: 'address', value: '0x1111111111111111111111111111111111111111' }, { name: 'minter', type: 'address', value: '0x2222222222222222222222222222222222222222' }, { name: 'pauser', type: 'address', value: '0x3333333333333333333333333333333333333333' } ] } }); console.log('Workflow ID:', collection.data.workflowResult.workflowId); ``` ```javascript theme={null} // Create an ERC-1155 collection (SEMI_FUNGIBLE_TOKEN) const collection = await client.tokenization.createCollection({ addressId: 'your-address-id', type: 'SEMI_FUNGIBLE_TOKEN', clientShare: 'your-client-share', network: 'ETHEREUM', displayName: 'My Multi-Token Collection', createParams: { initializeParams: [ { name: '_name', type: 'string', value: 'My Multi-Token Collection' }, { name: '_symbol', type: 'string', value: 'MMTC' }, { name: 'defaultAdmin', type: 'address', value: '0x1111111111111111111111111111111111111111' }, { name: 'minter', type: 'address', value: '0x2222222222222222222222222222222222222222' }, { name: 'pauser', type: 'address', value: '0x3333333333333333333333333333333333333333' } ] } }); ``` ### Collection Types * **NON\_FUNGIBLE\_TOKEN** (ERC-721): Non-fungible tokens (NFTs) - each token is unique * **SEMI\_FUNGIBLE\_TOKEN** (ERC-1155): Multi-token standard - supports both fungible and non-fungible tokens Use `NON_FUNGIBLE_TOKEN` for ERC-721 collections and `SEMI_FUNGIBLE_TOKEN` for ERC-1155 collections. These are the actual API values, not `ERC721` or `ERC1155`. ## Step 2: Check Workflow Status Collection creation is async. Check the workflow status to know when deployment completes: ```javascript theme={null} // Check workflow status const status = await client.workflows.getWorkflowStatus(collection.data.workflowResult.workflowId); console.log('Status:', status.data.status); // COMPLETED, RUNNING, FAILED console.log('Result:', status.data.result); if (status.data.status === 'COMPLETED') { console.log('Collection deployed at:', status.data.result.contractAddress); } else if (status.data.status === 'FAILED') { console.error('Deployment failed:', status.data.error); } ``` ### Workflow Status Values * **RUNNING**: Operation in progress * **COMPLETED**: Operation successful * **FAILED**: Operation failed (check error details) * **CANCELLED**: Operation was cancelled Poll the workflow status endpoint every few seconds until status is COMPLETED or FAILED. Consider implementing exponential backoff for production applications. ## Step 3: Get Collection Details Once deployment completes, retrieve collection details: ```javascript theme={null} const collectionDetails = await client.tokenization.getCollectionById( collection.data.workflowResult.operationId ); console.log('Collection:', collectionDetails.data); console.log('Contract Address:', collectionDetails.data.contractAddress); console.log('Network:', collectionDetails.data.network); ``` ## Step 4: Mint Tokens Mint tokens to specific addresses. For ERC-721, mint unique tokens. For ERC-1155, specify token ID and amount. ### Minting ERC-721 Tokens ```javascript theme={null} const mintResult = await client.tokenization.mintToken(collection.data.workflowResult.operationId, { addressId: 'your-address-id', clientShare: 'your-client-share', to: '0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb', // Recipient address tokenId: '0', // Sequential token ID (start from 0) metadata: { name: 'My First NFT', description: 'This is my first NFT', image: 'https://example.com/image.png', attributes: [ { trait_type: 'Color', value: 'Blue' } ] } }); console.log('Mint Workflow ID:', mintResult.data.workflowResult.workflowId); ``` ### Minting ERC-1155 Tokens ```javascript theme={null} const mintResult = await client.tokenization.mintToken(collection.data.workflowResult.operationId, { addressId: 'your-address-id', clientShare: 'your-client-share', to: '0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb', tokenId: '1', // Token type ID amount: '100', // Amount to mint (for ERC-1155) metadataURI: 'https://api.example.com/metadata/1' }); console.log('Mint Workflow ID:', mintResult.data.workflowResult.workflowId); ``` **Token ID Sequencing**: For ERC-721 collections, token IDs must be sequential starting from 0. The system validates this to prevent conflicts. ## Step 5: Check Mint Status Minting is also async. Check the workflow status: ```javascript theme={null} const mintStatus = await client.workflows.getWorkflowStatus(mintResult.data.workflowResult.workflowId); if (mintStatus.data.status === 'COMPLETED') { console.log('Token minted successfully!'); console.log('Transaction Hash:', mintStatus.data.result.transactionHash); } ``` ## Step 6: View Collection Tokens List all tokens in your collection: ```javascript theme={null} const tokens = await client.tokenization.getCollectionTokens({ id: collection.data.workflowResult.operationId, skip: 0, take: 25 }); console.log('Total tokens:', tokens.data.tokens.length); tokens.data.tokens.forEach(token => { console.log(`Token ${token.tokenId}:`, token); }); ``` ## Step 7: Get Token Details Get details for a specific token: ```javascript theme={null} const tokenDetails = await client.tokenization.getCollectionTokenDetails( collection.data.workflowResult.operationId, '0' ); console.log('Token Details:', tokenDetails.data); console.log('Owner:', tokenDetails.data.owner); console.log('Metadata:', tokenDetails.data.metadata); ``` ## Step 8: Burn Tokens (Optional) Burn tokens to remove them from circulation: ```javascript theme={null} const burnResult = await client.tokenization.burnToken('collection-id', { addressId: 'your-address-id', clientShare: 'your-client-share', tokenId: '0' // Token ID to burn }); console.log('Burn Workflow ID:', burnResult.data.workflowResult.workflowId); // Check burn status const burnStatus = await client.workflows.getWorkflowStatus(burnResult.data.workflowResult.workflowId); ``` ## Complete Example Here's a complete example that ties everything together: ```javascript theme={null} import { OumlaSdkApiClient, OumlaSdkApiEnvironment } from '@oumla/sdk'; const client = new OumlaSdkApiClient({ environment: OumlaSdkApiEnvironment.Sandbox, apiKey: process.env.OUMLA_API_KEY }); async function tokenizationWorkflow() { try { // Step 1: Create collection const collection = await client.tokenization.createCollection({ addressId: process.env.ADDRESS_ID, type: 'NON_FUNGIBLE_TOKEN', // Use 'NON_FUNGIBLE_TOKEN' for ERC-721, 'SEMI_FUNGIBLE_TOKEN' for ERC-1155 clientShare: process.env.CLIENT_SHARE, network: 'ETHEREUM', displayName: 'My NFT Collection', createParams: { initializeParams: [ { name: '_name', type: 'string', value: 'My Collection' }, { name: '_symbol', type: 'string', value: 'MYC' }, { name: 'defaultAdmin', type: 'address', value: process.env.ADMIN_ADDRESS }, { name: 'minter', type: 'address', value: process.env.MINTER_ADDRESS }, { name: 'pauser', type: 'address', value: process.env.PAUSER_ADDRESS } ] } }); console.log('Collection creation started. Workflow ID:', collection.data.workflowResult.workflowId); // Step 2: Wait for collection deployment let collectionStatus; do { await new Promise(resolve => setTimeout(resolve, 3000)); // Wait 3 seconds collectionStatus = await client.workflows.getWorkflowStatus(collection.data.workflowResult.workflowId); console.log('Collection status:', collectionStatus.data.status); } while (collectionStatus.data.status === 'RUNNING'); if (collectionStatus.data.status === 'FAILED') { throw new Error('Collection deployment failed: ' + collectionStatus.data.error); } const contractAddress = collectionStatus.data.result.contractAddress; console.log('Collection deployed at:', contractAddress); // Step 3: Get collection ID (operationId from the createCollection workflow) const collectionId = collection.data.workflowResult.operationId; // Step 4: Mint a token const mintResult = await client.tokenization.mintToken(collectionId, { addressId: process.env.ADDRESS_ID, clientShare: process.env.CLIENT_SHARE, to: '0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb', metadata: { name: 'My First NFT', description: 'This is my first NFT', image: 'https://example.com/image.png' } }); console.log('Minting started. Workflow ID:', mintResult.data.workflowResult.workflowId); // Step 5: Wait for mint to complete let mintStatus; do { await new Promise(resolve => setTimeout(resolve, 3000)); mintStatus = await client.workflows.getWorkflowStatus(mintResult.data.workflowResult.workflowId); console.log('Mint status:', mintStatus.data.status); } while (mintStatus.data.status === 'RUNNING'); if (mintStatus.data.status === 'COMPLETED') { console.log('Token minted successfully!'); console.log('Transaction Hash:', mintStatus.data.result.transactionHash); } // Step 6: View tokens const tokens = await client.tokenization.getCollectionTokens({ id: collectionId, skip: 0, take: 10 }); console.log('Collection tokens:', tokens.data.tokens); } catch (error) { console.error('Error:', error); } } tokenizationWorkflow(); ``` ## Workflow Status Values When checking workflow status, you'll receive one of these values: * **RUNNING**: Operation is in progress * **COMPLETED**: Operation completed successfully * **FAILED**: Operation failed (check `error` field for details) * **CANCELLED**: Operation was cancelled ## Common Issues ### Collection Deployment Fails **Causes:** * Insufficient gas balance * Invalid constructor parameters * Network connectivity issues **Solutions:** * Ensure address has sufficient ETH for gas * Verify constructor parameters match contract requirements * Check network status and retry ### Minting Fails **Causes:** * Invalid token ID (not sequential for ERC-721) * Collection not deployed yet * Insufficient gas balance **Solutions:** * Ensure token IDs are sequential starting from 0 * Wait for collection deployment to complete * Verify sufficient gas balance ### Workflow Status Stuck **Causes:** * Network congestion * Transaction pending in mempool **Solutions:** * Wait longer (some operations take time) * Check blockchain explorer for transaction status * Contact support if stuck for extended period ## Best Practices 1. **Always check workflow status** before proceeding to next steps 2. **Handle errors gracefully** - workflows can fail 3. **Use sequential token IDs** for ERC-721 collections 4. **Store workflow IDs** for tracking and debugging 5. **Implement retry logic** for transient failures 6. **Monitor gas prices** for cost optimization 7. **Validate addresses** before minting tokens ## Next Steps * Learn about [Contract Deployment Workflow](/guides/workflows/contract-deployment) * Explore [Withdrawal Workflow](/guides/workflows/withdrawal) * Review [Error Reference](/guides/error-reference) for troubleshooting # Withdrawal Workflow Source: https://docs.oumla.com/guides/workflows/withdrawal Complete guide to withdrawing funds from Oumla addresses This guide explains how to withdraw funds from Oumla addresses. Withdrawal behavior differs significantly between Bitcoin and Ethereum networks. **Two Ways to Use Oumla**: This guide uses the TypeScript SDK. You can also accomplish the same tasks using direct REST API calls - see the [API Reference](/api-reference/introduction) for endpoint details. ## Overview Withdrawals work differently depending on the network: * **Bitcoin (BTC/tBTC)**: Multi-address withdrawal - can withdraw from multiple addresses in one transaction * **Ethereum (ETH/tETH)**: Single-address withdrawal - withdraws from one address at a time Withdrawals are async operations that return a status immediately. Check transaction status on the blockchain explorer for final confirmation. ## Prerequisites Before starting, ensure you have: * An initialized SDK client * Addresses with sufficient balance * Your client share (for signing transactions) * Recipient address ready ## Bitcoin Withdrawal (BTC/tBTC) Bitcoin withdrawals support **multi-address withdrawal**, meaning you can withdraw from multiple addresses in a single transaction. ### Step 1: Create Bitcoin Withdrawal ```javascript theme={null} import { OumlaSdkApiClient, OumlaSdkApiEnvironment } from '@oumla/sdk'; const client = new OumlaSdkApiClient({ environment: OumlaSdkApiEnvironment.Sandbox, apiKey: 'your-api-key-here' }); // Withdraw from multiple addresses const withdrawal = await client.withdraw.createWithdraw({ networkId: 'your-btc-network-id', // Get from getNetworks() from: [ 'bc1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlh', // Address 1 'bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4' // Address 2 ], to: 'bc1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlh', // Recipient amount: '0.001', // Total amount to withdraw clientShare: 'your-client-share' }); console.log('Withdrawal ID:', withdrawal.data.data?.id); console.log('Status:', withdrawal.data.data?.status); // 'pending' ``` ### Bitcoin Withdrawal Response Bitcoin withdrawals return: ```json theme={null} { "data": { "data": { "id": "withdrawal-id", "status": "pending" } } } ``` Bitcoin withdrawals return a withdrawal ID immediately. The transaction is processed asynchronously. Check the blockchain explorer or transaction status for final confirmation. ## Ethereum Withdrawal (ETH/tETH) Ethereum withdrawals work with **single addresses only**. To withdraw from multiple addresses, make separate withdrawal requests. ### Step 1: Create Ethereum Withdrawal ```javascript theme={null} // Withdraw from a single address const withdrawal = await client.withdraw.createWithdraw({ networkId: 'your-eth-network-id', // Get from getNetworks() from: [ '0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb' // Single address only ], to: '0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb', // Recipient amount: '0.1', // Amount in ETH clientShare: 'your-client-share' }); console.log('Transaction Hash:', withdrawal.data.data?.id); console.log('Status:', withdrawal.data.data?.status); // 'pending' ``` ### Ethereum Withdrawal Response Ethereum withdrawals return: ```json theme={null} { "data": { "data": { "id": "0x...", "status": "pending" } } } ``` For Ethereum withdrawals, `data.id` contains the transaction hash. **Ethereum Limitation**: You can only withdraw from **one address** per withdrawal request. If you need to withdraw from multiple addresses, make separate requests. ## Network-Specific Considerations ### Bitcoin Networks (BTC/tBTC) **Multi-Address Support:** * Can combine funds from multiple addresses * More efficient for consolidating funds * Uses UTXO (Unspent Transaction Output) model **Requirements:** * All addresses must belong to your organization * Sufficient balance across all addresses * Network fees deducted from total **Example:** ```javascript theme={null} // Withdraw 0.001 BTC from two addresses const withdrawal = await client.withdraw.createWithdraw({ networkId: 'your-btc-network-id', from: [ 'address1', // Has 0.0005 BTC 'address2' // Has 0.0006 BTC ], to: 'recipient-address', amount: '0.001', // Total from both addresses clientShare: 'your-client-share' }); ``` ### Ethereum Networks (ETH/tETH) **Single-Address Only:** * One address per withdrawal * Simpler transaction model * Account-based model **Requirements:** * Address must have sufficient balance * Gas fees deducted from balance * Recipient address must be valid Ethereum address **Example:** ```javascript theme={null} // Withdraw from single address const withdrawal = await client.withdraw.createWithdraw({ networkId: 'your-eth-network-id', from: ['0x...'], // Single address to: '0x...', amount: '0.1', clientShare: 'your-client-share' }); // For multiple addresses, make separate requests for (const address of addresses) { await client.withdraw.createWithdraw({ networkId: 'your-eth-network-id', from: [address], to: recipientAddress, amount: '0.1', clientShare: 'your-client-share' }); } ``` ## Complete Examples ### Bitcoin Withdrawal Example ```javascript theme={null} import { OumlaSdkApiClient, OumlaSdkApiEnvironment } from '@oumla/sdk'; const client = new OumlaSdkApiClient({ environment: OumlaSdkApiEnvironment.Sandbox, apiKey: process.env.OUMLA_API_KEY }); async function withdrawBitcoin() { try { const withdrawal = await client.withdraw.createWithdraw({ networkId: 'your-btc-network-id', // Use testnet network ID from: [ 'bc1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlh', 'bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4' ], to: 'bc1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlh', amount: '0.001', clientShare: process.env.CLIENT_SHARE }); console.log('Withdrawal ID:', withdrawal.data.data?.id); console.log('Status:', withdrawal.data.data?.status); // Note: Check blockchain explorer for transaction confirmation // Withdrawal ID can be used to track the transaction } catch (error) { console.error('Withdrawal failed:', error); } } withdrawBitcoin(); ``` ### Ethereum Withdrawal Example ```javascript theme={null} async function withdrawEthereum() { try { const withdrawal = await client.withdraw.createWithdraw({ networkId: 'your-eth-network-id', // Use testnet network ID from: ['0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb'], to: '0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb', amount: '0.1', clientShare: process.env.CLIENT_SHARE }); console.log('Transaction Hash:', withdrawal.data.data?.id); console.log('Status:', withdrawal.data.data?.status); // Check transaction on blockchain explorer console.log(`View on explorer: https://sepolia.etherscan.io/tx/${withdrawal.data.data?.id}`); } catch (error) { console.error('Withdrawal failed:', error); } } withdrawEthereum(); ``` ## Checking Withdrawal Status ### Bitcoin Withdrawals Bitcoin withdrawals return a withdrawal ID. Check status by: 1. **Blockchain Explorer**: Use the withdrawal ID or check addresses on blockchain explorer 2. **Transaction Monitoring**: Monitor the `from` addresses for outgoing transactions ### Ethereum Withdrawals Ethereum withdrawals return a transaction hash. Check status: ```javascript theme={null} // Use the transaction hash to check status const txHash = withdrawal.data.data?.id; // Check on blockchain explorer console.log(`View transaction: https://etherscan.io/tx/${txHash}`); // Or use contract interactions to check receipt (if applicable) const receipt = await client.contractInteractions.getTransactionReceipt('your-eth-network-id', txHash); console.log('Transaction Status:', receipt.data.receipt?.status); ``` ## Common Issues ### Insufficient Balance **Error:** `ERR_236 - Not enough balance` **Causes:** * Address doesn't have sufficient funds * Balance doesn't cover amount + fees **Solutions:** * Check address balance before withdrawal * Account for network fees in calculations * For Bitcoin: Ensure total across all addresses is sufficient ### Invalid Address Format **Error:** `ERR_176 - Invalid contract address` or validation error **Causes:** * Invalid recipient address format * Address doesn't match network **Solutions:** * Verify address format matches network (Bitcoin vs Ethereum) * Use valid blockchain addresses * Test addresses on testnet first ### Bitcoin: No Unspent Transactions **Error:** `ERR_226 - No unspent transactions` **Causes:** * Address has no UTXOs * All funds already spent **Solutions:** * Ensure address has received funds * Wait for transaction confirmations * Check address balance ### Ethereum: Single Address Limitation **Issue:** Trying to withdraw from multiple addresses **Solution:** * Make separate withdrawal requests for each address * Or consolidate funds to one address first ## Best Practices 1. **Check balances** before withdrawal 2. **Account for fees** - network fees are deducted automatically 3. **Use testnet first** to test withdrawal flow 4. **Verify addresses** before sending 5. **Monitor transactions** on blockchain explorer 6. **Handle errors gracefully** - withdrawals can fail 7. **For Bitcoin**: Leverage multi-address withdrawal for efficiency 8. **For Ethereum**: Make separate requests for multiple addresses ## Network Comparison | Feature | Bitcoin (BTC/tBTC) | Ethereum (ETH/tETH) | | ------------- | ------------------- | -------------------------- | | Multi-address | ✅ Yes | ❌ No (single address only) | | Response | Withdrawal ID | Transaction Hash | | Model | UTXO-based | Account-based | | Fee Model | Deducted from total | Deducted from balance | | Confirmation | Check blockchain | Check transaction hash | ## Next Steps * Learn about [Tokenization Workflow](/guides/workflows/tokenization) * Explore [Contract Deployment Workflow](/guides/workflows/contract-deployment) * Review [Networks Guide](/guides/networks) for network-specific details * Check [Error Reference](/guides/error-reference) for troubleshooting # Addresses Source: https://docs.oumla.com/sdk-reference/addresses SDK methods for generating and managing addresses Generate blockchain addresses for your profiles. Addresses belong to wallets and are used to send and receive funds. ## Methods ### `createAddress()` Generate a new address for a profile (V1 API). **Requires wallet to exist first.** **Signature:** ```typescript theme={null} createAddress( request: CreateAddressRequest, requestOptions?: RequestOptions ): Promise ``` **Parameters:** | Parameter | Type | Required | Description | | --------------------- | -------- | -------- | --------------------------------- | | `request.reference` | `string` | Yes | Profile reference | | `request.networkId` | `string` | Yes | Network ID (from `getNetworks()`) | | `request.clientShare` | `string` | Yes | Client share for key derivation | **Example:** ```typescript theme={null} const address = await client.addresses.createAddress({ reference: 'user-123', networkId: 'your-network-id', clientShare: 'your-client-share' }); console.log('Address:', address.data.data?.address); console.log('Address ID:', address.data.data?.id); ``` *** ### `createAddressV2()` Generate a new address (V2 API). **Automatically creates wallet if it doesn't exist.** This is the recommended method. **Signature:** ```typescript theme={null} createAddressV2( request: CreateAddressRequest, requestOptions?: RequestOptions ): Promise ``` **Parameters:** Same as `createAddress()`. **Example:** ```typescript theme={null} // V2 automatically creates wallet if needed const address = await client.addresses.createAddressV2({ reference: 'user-123', networkId: 'your-network-id', clientShare: 'your-client-share' }); console.log('Address:', address.data.data?.address); ``` **Use V2**: The `createAddressV2()` method is recommended as it automatically creates a wallet if one doesn't exist, simplifying your workflow. *** ### `getAddressById()` Retrieve a single address by its UUID. **Signature:** ```typescript theme={null} getAddressById( request: GetAddressByIdRequest, requestOptions?: RequestOptions ): Promise ``` **Parameters:** | Parameter | Type | Required | Description | | ------------ | -------- | -------- | ------------ | | `request.id` | `string` | Yes | Address UUID | **Example:** ```typescript theme={null} const address = await client.addresses.getAddressById({ id: '3fa85f64-5717-4562-b3fc-2c963f66afa6' }); console.log('Address:', address.data.data?.address); ``` *** ### `getAddressForProfile()` Get addresses for a profile by reference. **Signature:** ```typescript theme={null} getAddressForProfile( reference: string, request?: GetAddressForProfileRequest, requestOptions?: RequestOptions ): Promise ``` **Parameters:** | Parameter | Type | Required | Description | | -------------- | -------- | -------- | ----------------------------- | | `reference` | `string` | Yes | Profile reference | | `request.skip` | `number` | No | Number of records to skip | | `request.take` | `number` | No | Number of records to retrieve | **Example:** ```typescript theme={null} const addresses = await client.addresses.getAddressForProfile('user-123', { skip: 0, take: 50 }); console.log('Addresses:', addresses.data.data); ``` *** ### `getAddressForOrganization()` Get all addresses for an organization. **Signature:** ```typescript theme={null} getAddressForOrganization( request: GetAddressForOrganizationRequest, requestOptions?: RequestOptions ): Promise ``` **Parameters:** | Parameter | Type | Required | Description | | -------------- | -------- | -------- | ----------------------------- | | `request.skip` | `number` | No | Number of records to skip | | `request.take` | `number` | No | Number of records to retrieve | **Example:** ```typescript theme={null} const addresses = await client.addresses.getAddressForOrganization({ skip: 0, take: 50 }); console.log('Organization addresses:', addresses.data.data); ``` *** ## Complete Example ```typescript theme={null} import { OumlaSdkApiClient, OumlaSdkApiEnvironment } from '@oumla/sdk'; const client = new OumlaSdkApiClient({ environment: OumlaSdkApiEnvironment.Sandbox, apiKey: 'your-api-key' }); async function manageAddresses() { // First, get available networks const networks = await client.networks.getNetworks({ skip: 0, take: 50, enabled: true }); const networkId = networks.data.networks[0].id; // Use appropriate network ID // Generate address (V2 - auto-creates wallet) const address = await client.addresses.createAddressV2({ reference: 'user-123', networkId, clientShare: process.env.CLIENT_SHARE! }); console.log('Generated address:', address.data.data?.address); // Get addresses for profile const profileAddresses = await client.addresses.getAddressForProfile('user-123', { skip: 0, take: 50 }); console.log('Profile addresses:', profileAddresses.data.data); // List all organization addresses const orgAddresses = await client.addresses.getAddressForOrganization({ skip: 0, take: 100 }); console.log('Total addresses:', orgAddresses.data.data?.length ?? 0); } ``` # Contract Interactions Source: https://docs.oumla.com/sdk-reference/contract-interactions SDK methods for calling contract functions and reading transaction receipts Call read and write functions on deployed contracts and retrieve transaction receipts. ## Methods ### `readCallFunction()` Call a read-only function on a deployed contract. **Signature:** ```typescript theme={null} readCallFunction( networkId: string, contractAddress: string, request: ReadCallFunctionRequest, requestOptions?: RequestOptions ): Promise ``` **Parameters:** | Parameter | Type | Required | Description | | --------------------- | -------- | -------- | -------------------------------------- | | `networkId` | `string` | Yes | Network ID (UUID from `getNetworks()`) | | `contractAddress` | `string` | Yes | Contract address | | `request.abiFunction` | `object` | Yes | ABI function object | **Example Request:** ```typescript theme={null} const result = await client.contractInteractions.readCallFunction('your-network-id', '0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb', { abiFunction: { name: 'balanceOf', type: 'function', inputs: [ { name: 'owner', type: 'address' } ], outputs: [ { name: '', type: 'uint256' } ] } }); ``` **Example Response:** ```typescript theme={null} { data: { result: { data: { result: { // Decoded function result }, functionName: 'balanceOf', contractAddress: '0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb', network: 'ETH', encodedData: '0x1234567890abcdef...' }, success: true, status: 200 } } } ``` **Response Fields:** | Field | Type | Required | Description | | ---------------------------------- | --------- | -------- | ------------------------------- | | `data.result.data.result` | `object` | No | Decoded function result | | `data.result.data.functionName` | `string` | No | Function name that was called | | `data.result.data.contractAddress` | `string` | No | Contract address | | `data.result.data.network` | `string` | No | Network | | `data.result.data.encodedData` | `string` | No | Encoded function call data | | `data.result.success` | `boolean` | No | Whether the call was successful | | `data.result.status` | `number` | No | HTTP status code | ```typescript theme={null} console.log('Function result:', result.data.result?.data?.result); ``` *** ### `writeCallFunction()` Call a write function on a deployed contract. Returns a workflow ID for async processing. **Signature:** ```typescript theme={null} writeCallFunction( networkId: string, contractAddress: string, request: WriteCallFunctionRequest, requestOptions?: RequestOptions ): Promise ``` **Parameters:** | Parameter | Type | Required | Description | | --------------------- | -------- | -------- | -------------------------------------- | | `networkId` | `string` | Yes | Network ID (UUID from `getNetworks()`) | | `contractAddress` | `string` | Yes | Contract address | | `request.addressId` | `string` | Yes | Address ID for signing | | `request.clientShare` | `string` | Yes | Client share for signing | | `request.abiFunction` | `object` | Yes | ABI function object | | `request.amount` | `string` | No | Amount to send with transaction | | `request.feeLevel` | `string` | No | Transaction fee level | **Example Request:** ```typescript theme={null} const result = await client.contractInteractions.writeCallFunction('your-network-id', '0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb', { addressId: 'address-id', clientShare: 'your-client-share', abiFunction: { name: 'transfer', type: 'function', inputs: [ { name: 'to', type: 'address' }, { name: 'amount', type: 'uint256' } ], outputs: [] }, amount: '0', feeLevel: 'standard' }); ``` **Example Response:** ```typescript theme={null} { data: { workflowResult: { workflowId: 'workflow-id-here', operationId: 'write-call-id-here', status: 'started', message: 'Write call workflow started' } } } ``` **Response Fields:** | Field | Type | Required | Description | | --------------------------------- | -------------------------------- | -------- | -------------------------------------- | | `data.workflowResult.workflowId` | `string` | Yes | Workflow ID for tracking function call | | `data.workflowResult.operationId` | `string` | Yes | Operation ID (e.g. `writeCallId`) | | `data.workflowResult.status` | `'started' \| 'already_running'` | Yes | Workflow status | | `data.workflowResult.message` | `string` | Yes | Human-readable status message | ```typescript theme={null} console.log('Workflow ID:', result.data.workflowResult.workflowId); ``` *** ### `getTransactionReceipt()` Get transaction receipt for a transaction hash. **Signature:** ```typescript theme={null} getTransactionReceipt( networkId: string, txId: string, requestOptions?: RequestOptions ): Promise ``` **Parameters:** | Parameter | Type | Required | Description | | ----------- | -------- | -------- | -------------------------------------- | | `networkId` | `string` | Yes | Network ID (UUID from `getNetworks()`) | | `txId` | `string` | Yes | Transaction hash | **Example:** ```typescript theme={null} const receipt = await client.contractInteractions.getTransactionReceipt( 'your-network-id', '0x1234567890abcdef...' ); console.log('Status:', receipt.data.receipt?.status); console.log('Block Number:', receipt.data.receipt?.blockNumber); console.log('Gas Used:', receipt.data.receipt?.gasUsed); ``` *** ### `getDeployedContractAbi()` Get contract ABI for a deployed contract. **Signature:** ```typescript theme={null} getDeployedContractAbi( networkId: string, contractAddress: string, requestOptions?: RequestOptions ): Promise ``` **Example:** ```typescript theme={null} const abi = await client.contractInteractions.getDeployedContractAbi( 'your-network-id', '0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb' ); console.log('ABI:', abi.data.contractAbi?.abi); ``` # Contract Templates Source: https://docs.oumla.com/sdk-reference/contract-templates SDK methods for managing smart contract templates Manage smart contract templates and deploy contracts from templates. ## Methods ### `getContracts()` Get a paginated list of contract templates. **Signature:** ```typescript theme={null} getContracts( request?: GetContractsRequest, requestOptions?: RequestOptions ): Promise ``` **Example:** ```typescript theme={null} const templates = await client.contractTemplates.getContracts({ skip: 0, take: 25 }); console.log('Templates:', templates.data); ``` *** ### `getContractById()` Get details for a specific contract template. **Signature:** ```typescript theme={null} getContractById( templateId: string, requestOptions?: RequestOptions ): Promise ``` **Example:** ```typescript theme={null} const template = await client.contractTemplates.getContractById('template-id'); console.log('Template:', template.data.result); console.log('Name:', template.data.result?.name); console.log('Type:', template.data.result?.type); ``` *** ### `getContractConstructorById()` Get constructor information for a template. **Signature:** ```typescript theme={null} getContractConstructorById( templateId: string, requestOptions?: RequestOptions ): Promise ``` **Example:** ```typescript theme={null} const constructorInfo = await client.contractTemplates.getContractConstructorById('template-id'); console.log('Constructor inputs:', constructorInfo.data.contractConstructor?.inputs); ``` *** ### `getContractFunctionById()` Get function information for a template. **Signature:** ```typescript theme={null} getContractFunctionById( templateId: string, requestOptions?: RequestOptions ): Promise ``` **Example:** ```typescript theme={null} const functions = await client.contractTemplates.getContractFunctionById('template-id'); console.log('Available functions:', functions.data.contractFunction); ``` *** ### `deployContract()` Deploy a contract from a template. Returns a workflow ID for async processing. **Signature:** ```typescript theme={null} deployContract( templateId: string, request: DeployContractRequest, requestOptions?: RequestOptions ): Promise ``` **Parameters:** | Parameter | Type | Required | Description | | ------------------------------- | -------- | -------- | --------------------------------------------------------------------- | | `templateId` | `string` | Yes | Template ID | | `request.networkId` | `string` | Yes | Network ID (from `getNetworks()`) | | `request.addressId` | `string` | Yes | Address ID for deployment | | `request.clientShare` | `string` | Yes | Client share for signing | | `request.constructorParameters` | `array` | No | Array of constructor parameter objects `[{ name, type, value }, ...]` | **Example Request:** ```typescript theme={null} const deployment = await client.contractTemplates.deployContract('template-id', { networkId: 'your-network-id', addressId: 'address-id', clientShare: 'your-client-share', constructorParameters: [ { name: 'name', type: 'string', value: 'MyToken' }, { name: 'symbol', type: 'string', value: 'MTK' }, { name: 'initialSupply', type: 'uint256', value: '1000000' } ] }); ``` **Example Response:** ```typescript theme={null} { data: { workflowResult: { workflowId: 'workflow-id-here', operationId: 'deployment-id-here', status: 'started', message: 'Deploy contract workflow started' } } } ``` **Response Fields:** | Field | Type | Required | Description | | --------------------------------- | -------------------------------- | -------- | -------------------------------------------- | | `data.workflowResult.workflowId` | `string` | Yes | Workflow ID for tracking contract deployment | | `data.workflowResult.operationId` | `string` | Yes | Operation ID (e.g. `deploymentId`) | | `data.workflowResult.status` | `'started' \| 'already_running'` | Yes | Workflow status | | `data.workflowResult.message` | `string` | Yes | Human-readable status message | ```typescript theme={null} console.log('Deployment Workflow ID:', deployment.data.workflowResult.workflowId); ``` *** ### `createContract()` Create a new contract template. **Signature:** ```typescript theme={null} createContract( request: CreateContractRequest, requestOptions?: RequestOptions ): Promise ``` *** ### `deleteContract()` Delete a contract template. **Signature:** ```typescript theme={null} deleteContract( templateId: string, requestOptions?: RequestOptions ): Promise ``` # Deployed Contracts Source: https://docs.oumla.com/sdk-reference/deployed-contracts SDK methods for managing deployed contracts Manage deployed smart contracts and contract ABIs. ## Methods ### `getDeployedContracts()` Get a paginated list of deployed contracts. **Signature:** ```typescript theme={null} getDeployedContracts( request?: GetDeployedContractsRequest, requestOptions?: RequestOptions ): Promise ``` **Example:** ```typescript theme={null} const contracts = await client.deployedContracts.getDeployedContracts({ networkId: 'your-network-id', skip: 0, take: 50 }); console.log('Deployed contracts:', contracts.data); ``` *** ### `getDeployedContractById()` Get details for a specific deployed contract by deployment ID. **Signature:** ```typescript theme={null} getDeployedContractById( deploymentId: string, requestOptions?: RequestOptions ): Promise ``` **Example:** ```typescript theme={null} const contract = await client.deployedContracts.getDeployedContractById('deployment-id'); console.log('Contract Address:', contract.data.deployedContract?.contractAddress); console.log('Network:', contract.data.deployedContract?.network); ``` *** ### `getDeployedContractByAddress()` Get deployed contract details by contract address and network. **Signature:** ```typescript theme={null} getDeployedContractByAddress( contractAddress: string, networkId: string, requestOptions?: RequestOptions ): Promise ``` **Parameters:** | Parameter | Type | Required | Description | | ----------------- | -------- | -------- | -------------------------------------- | | `contractAddress` | `string` | Yes | Contract address | | `networkId` | `string` | Yes | Network ID (UUID from `getNetworks()`) | **Example:** ```typescript theme={null} const contract = await client.deployedContracts.getDeployedContractByAddress( '0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb', 'your-network-id' ); ``` *** ### `fetchContractAbi()` Fetch contract ABI from a blockchain explorer. **Signature:** ```typescript theme={null} fetchContractAbi( request: FetchContractAbiRequest, requestOptions?: RequestOptions ): Promise ``` **Example:** ```typescript theme={null} const abi = await client.deployedContracts.fetchContractAbi({ contractAddress: '0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb', networkId: 'your-network-id' }); console.log('ABI:', abi.data.contractAbi?.abi); ``` *** ### `addContractAbi()` Add or update contract ABI manually. **Signature:** ```typescript theme={null} addContractAbi( request: AddContractAbiRequest, requestOptions?: RequestOptions ): Promise ``` **Example:** ```typescript theme={null} await client.deployedContracts.addContractAbi({ contractAddress: '0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb', networkId: 'your-network-id', name: 'MyContract', abi: [ { "inputs": [], "name": "name", "outputs": [{"type": "string"}], "stateMutability": "view", "type": "function" } ] }); ``` # SDK Reference Source: https://docs.oumla.com/sdk-reference/introduction Complete TypeScript SDK reference for Oumla API The Oumla TypeScript SDK provides a type-safe, easy-to-use interface for interacting with the Oumla API. This reference documents all available SDK methods organized by resource. **Two Ways to Use Oumla**: You can interact with Oumla either through our **TypeScript SDK** (documented here) or through **direct REST API calls** (see [API Reference](/api-reference/introduction)). Both methods provide the same functionality - choose the one that best fits your project. The SDK is recommended for TypeScript/JavaScript projects as it provides type safety and better developer experience. ## Installation ```bash theme={null} npm install @oumla/sdk ``` or ```bash theme={null} yarn add @oumla/sdk ``` or ```bash theme={null} pnpm add @oumla/sdk ``` ## Quick Start ```typescript theme={null} import { OumlaSdkApiClient, OumlaSdkApiEnvironment } from '@oumla/sdk'; const client = new OumlaSdkApiClient({ environment: OumlaSdkApiEnvironment.Sandbox, apiKey: 'your-api-key-here' }); ``` Get your API key from the dashboard. ## SDK Resources The SDK is organized into the following resource modules: Create and manage user profiles (User, Department, Merchant) Generate and manage wallets for profiles and organizations Generate addresses for Bitcoin and Ethereum networks Query transaction history by address, wallet, profile, or organization View assets and native balances across addresses Create and execute withdrawal requests Create collections, mint tokens, and manage NFTs (ERC-721, ERC-1155) Manage smart contract templates and deploy contracts Manage deployed contracts and contract ABIs Call read and write functions on deployed contracts Track async workflow status for long-running operations ## Type Safety The SDK provides full TypeScript support with comprehensive type definitions: ```typescript theme={null} import type { CreateProfileRequest, CreateProfileResponse } from '@oumla/sdk'; const request: CreateProfileRequest = { reference: 'user-123', type: 'User' }; const response: CreateProfileResponse = await client.profiles.createProfile(request); ``` ## Error Handling All SDK methods throw typed errors that you can catch and handle: ```typescript theme={null} import { OumlaSdkApiError } from '@oumla/sdk'; try { const profile = await client.profiles.createProfile({ reference: 'user-123', type: 'User' }); } catch (error) { if (error instanceof OumlaSdkApiError) { console.error('API Error:', error.statusCode, error.message); // Handle specific error codes if (error.statusCode === 401) { // Authentication error } } } ``` ## Environments The SDK supports multiple environments: ```typescript theme={null} import { OumlaSdkApiEnvironment } from '@oumla/sdk'; // Sandbox environment (for testing) const sandboxClient = new OumlaSdkApiClient({ environment: OumlaSdkApiEnvironment.Sandbox, apiKey: 'your-sandbox-api-key' }); // Production environment const productionClient = new OumlaSdkApiClient({ environment: OumlaSdkApiEnvironment.Production, apiKey: 'your-production-api-key' }); // Custom base URL const customClient = new OumlaSdkApiClient({ baseUrl: 'https://custom-api.example.com', apiKey: 'your-api-key' }); ``` ## Request Options All SDK methods accept optional request options: ```typescript theme={null} const profile = await client.profiles.createProfile( { reference: 'user-123', type: 'User' }, { timeoutInSeconds: 30, maxRetries: 3, apiKey: 'override-api-key', // Override default API key headers: { 'Custom-Header': 'value' } } ); ``` ## Next Steps * Check out the [Quick Start Guide](/guides/quickstart) for a complete walkthrough * Explore [Workflow Guides](/guides/workflows/tokenization) for common use cases * Review the [API Reference](/api-reference/introduction) for REST API details # Portfolio Source: https://docs.oumla.com/sdk-reference/portfolio SDK methods for viewing assets and balances View assets and native balances across addresses. ## Methods ### `getAssets()` Get token assets for a specific address. **Signature:** ```typescript theme={null} getAssets( request: GetAssetsRequest, requestOptions?: RequestOptions ): Promise ``` **Parameters:** | Parameter | Type | Required | Description | | ------------------------- | -------- | -------- | ----------------------------- | | `request.address` | `string` | Yes | Blockchain address | | `request.skip` | `number` | Yes | Number of records to skip | | `request.take` | `number` | Yes | Number of records to retrieve | | `request.walletId` | `string` | No | Filter by wallet ID | | `request.contractAddress` | `string` | No | Filter by contract address | | `request.tokenizationId` | `string` | No | Filter by tokenization ID | | `request.network` | `string` | No | Filter by network | **Example:** ```typescript theme={null} const assets = await client.assets.getAssets({ address: '0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb', skip: 0, take: 50 }); console.log('Assets:', assets.data); ``` *** ### `getNativeBalance()` Get native balance (BTC/ETH) for a specific address. **Signature:** ```typescript theme={null} getNativeBalance( request: GetNativeBalanceRequest, requestOptions?: RequestOptions ): Promise ``` **Parameters:** | Parameter | Type | Required | Description | | ------------------- | -------- | -------- | ----------------------------- | | `request.address` | `string` | Yes | Blockchain address | | `request.skip` | `number` | Yes | Number of records to skip | | `request.take` | `number` | Yes | Number of records to retrieve | | `request.networkId` | `string` | No | Filter by network ID | | `request.walletId` | `string` | No | Filter by wallet ID | | `request.network` | `string` | No | Filter by network | **Example:** ```typescript theme={null} const balance = await client.assets.getNativeBalance({ address: '0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb', skip: 0, take: 50 }); console.log('Balance:', balance.data); ``` # Profiles Source: https://docs.oumla.com/sdk-reference/profiles SDK methods for managing user profiles Manage user profiles (User, Department, Merchant) in your organization. ## Methods ### `getProfiles()` Get a paginated list of profiles. **Signature:** ```typescript theme={null} getProfiles( request?: GetProfilesRequest, requestOptions?: RequestOptions ): Promise ``` **Parameters:** | Parameter | Type | Required | Description | | -------------- | -------- | -------- | ----------------------------------------------------- | | `request.skip` | `number` | No | Number of records to skip (default: 0) | | `request.take` | `number` | No | Number of records to retrieve (default: 10, max: 100) | **Example:** ```typescript theme={null} // Get first 10 profiles const profiles = await client.profiles.getProfiles(); // Get profiles with pagination const profiles = await client.profiles.getProfiles({ skip: 0, take: 25 }); console.log('Profiles:', profiles.data.data); console.log('Pagination:', profiles.data.pagination); ``` **Response:** ```typescript theme={null} { data: { data: Profile[], pagination: { skip: number, take: number, totalElements: number, totalPages: number } } } ``` *** ### `createProfile()` Create a new profile. **Signature:** ```typescript theme={null} createProfile( request: CreateProfileRequest, requestOptions?: RequestOptions ): Promise ``` **Parameters:** | Parameter | Type | Required | Description | | ------------------- | -------------------------------------- | -------- | ------------------------------------------------ | | `request.reference` | `string` | Yes | Unique identifier for the profile in your system | | `request.type` | `'User' \| 'Department' \| 'Merchant'` | Yes | Type of profile | **Example:** ```typescript theme={null} // Create a User profile const profile = await client.profiles.createProfile({ reference: 'user-123', type: 'User' }); console.log('Created profile:', profile.data.data); ``` **Response:** ```typescript theme={null} { data: { data: { reference: string, type: 'User' | 'Department' | 'Merchant' } } } ``` **Throws:** * `BadRequestError` - Invalid request parameters * `UnauthorizedError` - Missing or invalid API key * `InternalServerError` - Server error *** ## Profile Types * **User**: Individual users in your system * **Department**: Organizational units or departments * **Merchant**: Business entities or merchants ## Complete Example ```typescript theme={null} import { OumlaSdkApiClient, OumlaSdkApiEnvironment } from '@oumla/sdk'; const client = new OumlaSdkApiClient({ environment: OumlaSdkApiEnvironment.Sandbox, apiKey: 'your-api-key' }); async function manageProfiles() { // Create a profile const newProfile = await client.profiles.createProfile({ reference: 'merchant-001', type: 'Merchant' }); console.log('Created:', newProfile.data.data); // List all profiles const allProfiles = await client.profiles.getProfiles({ skip: 0, take: 100 }); console.log('Total profiles:', allProfiles.data.pagination?.totalElements); console.log('Profiles:', allProfiles.data.data); } ``` # Tokenization Source: https://docs.oumla.com/sdk-reference/tokenization SDK methods for creating collections, minting tokens, and managing NFTs Create NFT collections (ERC-721, ERC-1155), mint tokens, manage token operations, and link existing contracts. ## Collection Methods ### `createCollection()` Create a new NFT collection (ERC-721 or ERC-1155). Returns a workflow ID for async processing. **Signature:** ```typescript theme={null} createCollection( request: CreateCollectionRequest, requestOptions?: RequestOptions ): Promise ``` **Parameters:** | Parameter | Type | Required | Description | | --------------------------------------- | ----------------------------------------------- | -------- | ------------------------------------------------------------------------------- | | `request.addressId` | `string` | Yes | Address ID for deployment | | `request.type` | `'NON_FUNGIBLE_TOKEN' \| 'SEMI_FUNGIBLE_TOKEN'` | Yes | Collection type (ERC-721 or ERC-1155) | | `request.clientShare` | `string` | Yes | Client share for signing | | `request.createParams` | `object` | Yes | Constructor parameters | | `request.createParams.initializeParams` | `array` | Yes | Array of exactly 5 constructor parameter objects `[{ name, type, value }, ...]` | | `request.network` | `string` | No | Blockchain network | | `request.displayName` | `string` | No | Collection display name | | `request.useGasless` | `boolean` | No | Whether to use gasless transactions | | `request.fee` | `string` | No | Custom fee amount | | `request.feeLevel` | `string` | No | Transaction fee level | | `request.description` | `string` | No | Collection description | **Example Request:** ```typescript theme={null} const collection = await client.tokenization.createCollection({ addressId: 'your-address-id', type: 'NON_FUNGIBLE_TOKEN', clientShare: 'your-client-share', network: 'ETHEREUM', displayName: 'My Collection', useGasless: false, fee: '0', feeLevel: 'standard', description: 'Optional collection description', createParams: { initializeParams: [ { name: '_name', type: 'string', value: 'My Collection' }, { name: '_symbol', type: 'string', value: 'MYC' }, { name: 'defaultAdmin', type: 'address', value: '0x1111111111111111111111111111111111111111' }, { name: 'minter', type: 'address', value: '0x2222222222222222222222222222222222222222' }, { name: 'pauser', type: 'address', value: '0x3333333333333333333333333333333333333333' } ] } }); ``` **Example Response:** ```typescript theme={null} { data: { workflowResult: { workflowId: 'workflow-id-here', operationId: 'deployment-id-here', status: 'started', message: 'Create collection workflow started' } } } ``` **Response Fields:** | Field | Type | Required | Description | | --------------------------------- | -------------------------------- | -------- | -------------------------------------------- | | `data.workflowResult.workflowId` | `string` | Yes | Workflow ID for tracking collection creation | | `data.workflowResult.operationId` | `string` | Yes | Operation ID (e.g. `deploymentId`) | | `data.workflowResult.status` | `'started' \| 'already_running'` | Yes | Workflow status | | `data.workflowResult.message` | `string` | Yes | Human-readable status message | ```typescript theme={null} console.log('Workflow ID:', collection.data.workflowResult.workflowId); ``` *** ### `getCollections()` Get a paginated list of collections. **Signature:** ```typescript theme={null} getCollections( request?: GetCollectionsRequest, requestOptions?: RequestOptions ): Promise ``` **Example:** ```typescript theme={null} const collections = await client.tokenization.getCollections({ skip: 0, take: 25 }); console.log('Collections:', collections.data); ``` *** ### `getCollectionById()` Get details for a specific collection. **Signature:** ```typescript theme={null} getCollectionById( id: string, requestOptions?: RequestOptions ): Promise ``` **Example:** ```typescript theme={null} const collection = await client.tokenization.getCollectionById('collection-id'); console.log('Contract Address:', collection.data.contractAddress); console.log('Network:', collection.data.network); ``` *** ### `deleteCollection()` Delete a collection. **Signature:** ```typescript theme={null} deleteCollection( id: string, requestOptions?: RequestOptions ): Promise ``` **Example:** ```typescript theme={null} await client.tokenization.deleteCollection('collection-id'); ``` *** ## Token Methods ### `mintToken()` Mint a token in a collection. Returns a workflow ID for async processing. **Signature:** ```typescript theme={null} mintToken( id: string, request: MintTokenRequest, requestOptions?: RequestOptions ): Promise ``` **Parameters:** | Parameter | Type | Required | Description | | --------------------- | --------- | -------- | ----------------------------------- | | `id` | `string` | Yes | Collection ID | | `request.addressId` | `string` | Yes | Address ID for signing | | `request.clientShare` | `string` | Yes | Client share | | `request.to` | `string` | Yes | Recipient address | | `request.metadata` | `object` | No | Token metadata (JSON object) | | `request.useGasless` | `boolean` | No | Whether to use gasless transactions | | `request.fee` | `string` | No | Custom fee amount | | `request.feeLevel` | `string` | No | Transaction fee level | **Example Request:** ```typescript theme={null} const mintResult = await client.tokenization.mintToken('collection-id', { addressId: 'address-id', clientShare: 'your-client-share', to: '0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb', metadata: { name: 'My First NFT', image: 'https://example.com/image.png' }, useGasless: false, fee: '0', feeLevel: 'standard' }); ``` **Example Response:** ```typescript theme={null} { data: { workflowResult: { workflowId: 'workflow-id-here', operationId: 'mint-id-here', status: 'started', message: 'Mint token workflow started' } } } ``` **Response Fields:** | Field | Type | Required | Description | | --------------------------------- | -------------------------------- | -------- | -------------------------------------- | | `data.workflowResult.workflowId` | `string` | Yes | Workflow ID for tracking token minting | | `data.workflowResult.operationId` | `string` | Yes | Operation ID (e.g. `mintId`) | | `data.workflowResult.status` | `'started' \| 'already_running'` | Yes | Workflow status | | `data.workflowResult.message` | `string` | Yes | Human-readable status message | ```typescript theme={null} console.log('Mint Workflow ID:', mintResult.data.workflowResult.workflowId); ``` *** ### `burnToken()` Burn a token from a collection. Returns a workflow ID for async processing. **Signature:** ```typescript theme={null} burnToken( id: string, request: BurnTokenRequest, requestOptions?: RequestOptions ): Promise ``` **Parameters:** | Parameter | Type | Required | Description | | --------------------- | --------- | -------- | ----------------------------------- | | `id` | `string` | Yes | Collection ID | | `request.addressId` | `string` | Yes | Address ID for signing | | `request.clientShare` | `string` | Yes | Client share | | `request.tokenId` | `string` | Yes | Token ID to burn | | `request.useGasless` | `boolean` | No | Whether to use gasless transactions | | `request.fee` | `string` | No | Custom fee amount | | `request.feeLevel` | `string` | No | Transaction fee level | **Example Request:** ```typescript theme={null} const burnResult = await client.tokenization.burnToken('collection-id', { addressId: 'address-id', clientShare: 'your-client-share', tokenId: '0', useGasless: false, fee: '0', feeLevel: 'standard' }); ``` **Example Response:** ```typescript theme={null} { data: { workflowResult: { workflowId: 'workflow-id-here', operationId: 'burn-id-here', status: 'started', message: 'Burn token workflow started' } } } ``` **Response Fields:** | Field | Type | Required | Description | | --------------------------------- | -------------------------------- | -------- | -------------------------------------- | | `data.workflowResult.workflowId` | `string` | Yes | Workflow ID for tracking token burning | | `data.workflowResult.operationId` | `string` | Yes | Operation ID (e.g. `burnId`) | | `data.workflowResult.status` | `'started' \| 'already_running'` | Yes | Workflow status | | `data.workflowResult.message` | `string` | Yes | Human-readable status message | ```typescript theme={null} console.log('Burn Workflow ID:', burnResult.data.workflowResult.workflowId); ``` *** ### `getCollectionTokens()` Get all tokens in a collection. **Signature:** ```typescript theme={null} getCollectionTokens( request: GetCollectionTokensRequest, requestOptions?: RequestOptions ): Promise ``` **Example:** ```typescript theme={null} const tokens = await client.tokenization.getCollectionTokens({ id: 'collection-id', skip: 0, take: 50 }); console.log('Tokens:', tokens.data); ``` *** ### `getCollectionTokenDetails()` Get details for a specific token. **Signature:** ```typescript theme={null} getCollectionTokenDetails( id: string, tokenId: string, requestOptions?: RequestOptions ): Promise ``` **Example:** ```typescript theme={null} const token = await client.tokenization.getCollectionTokenDetails( 'collection-id', '0' ); console.log('Token:', token.data); ``` *** ## Token Management Methods ### `issueNewToken()` Start the issue-token workflow from a deployed contract template. Returns a workflow ID for async processing — the token is not ready until the workflow completes. **Signature:** ```typescript theme={null} issueNewToken( request: IssueNewTokenRequest, requestOptions?: RequestOptions ): Promise ``` **Parameters:** | Parameter | Type | Required | Description | | ---------------------- | ----------------------------- | -------- | -------------------------------------- | | `request.networkId` | `string` | Yes | Network ID (from `getNetworks()`) | | `request.addressId` | `string` | Yes | Address ID for signing | | `request.clientShare` | `string` | Yes | Client share for signing | | `request.createParams` | `object` | Yes | Constructor parameters | | `request.deploymentId` | `string` | Yes | Deployment ID of the contract template | | `request.displayName` | `string` | Yes | Token display name | | `request.useGasless` | `boolean` | Yes | Whether to use gasless transactions | | `request.fee` | `string` | Yes | Custom fee amount | | `request.feeLevel` | `'LOW' \| 'MEDIUM' \| 'HIGH'` | No | Transaction fee level | **Example Request:** ```typescript theme={null} const token = await client.tokenization.issueNewToken({ networkId: 'your-network-id', addressId: 'address-id', clientShare: 'your-client-share', deploymentId: 'deployment-id', displayName: 'My Token', useGasless: false, fee: '0', feeLevel: 'MEDIUM', createParams: { initializeParams: [ { name: '_name', type: 'string', value: 'My Token' }, { name: '_symbol', type: 'string', value: 'MTK' } ] } }); ``` **Example Response:** ```typescript theme={null} { data: { workflowResult: { workflowId: 'workflow-id-here', operationId: 'token-id-here', status: 'started', message: 'Issue token workflow started' } } } ``` **Response Fields:** | Field | Type | Required | Description | | --------------------------------- | -------------------------------- | -------- | ------------------------------------------------ | | `data.workflowResult.workflowId` | `string` | Yes | Workflow ID for tracking token issuance | | `data.workflowResult.operationId` | `string` | Yes | Operation ID (token ID assigned to the issuance) | | `data.workflowResult.status` | `'started' \| 'already_running'` | Yes | Workflow status | | `data.workflowResult.message` | `string` | Yes | Human-readable status message | The HTTP endpoint responds with `202 Accepted`. If the underlying contract is still deploying, expect error code `ERR-146` ("Contract deployment is still in progress.") — wait for the contract deployment workflow to complete before retrying. *** ### `getTokens()` List issued tokens for the organization with pagination. **Signature:** ```typescript theme={null} getTokens( request?: GetTokensRequest, requestOptions?: RequestOptions ): Promise ``` **Example:** ```typescript theme={null} const tokens = await client.tokenization.getTokens({ skip: 0, take: 50 }); console.log('Tokens:', tokens.data); ``` *** ### `getLinkedToken()` Get a linked token by its ID. **Signature:** ```typescript theme={null} getLinkedToken( id: string, requestOptions?: RequestOptions ): Promise ``` **Example:** ```typescript theme={null} const token = await client.tokenization.getLinkedToken('token-id'); console.log('Token:', token.data); ``` *** ### `linkContract()` Link an existing deployed contract to the organization for tokenization. **Signature:** ```typescript theme={null} linkContract( request: LinkContractRequest, requestOptions?: RequestOptions ): Promise ``` **Parameters:** | Parameter | Type | Required | Description | | ------------------------- | -------- | -------- | --------------------------------- | | `request.networkId` | `string` | Yes | Network ID (from `getNetworks()`) | | `request.contractAddress` | `string` | Yes | Contract address to link | | `request.type` | `string` | No | Token type | | `request.refId` | `string` | No | External reference ID | | `request.displayName` | `string` | No | Display name | **Example:** ```typescript theme={null} const linked = await client.tokenization.linkContract({ networkId: 'your-network-id', contractAddress: '0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb', displayName: 'My Linked Token' }); console.log('Linked:', linked.data); ``` *** ### `unlinkToken()` Unlink a token from the organization. **Signature:** ```typescript theme={null} unlinkToken( id: string, requestOptions?: RequestOptions ): Promise ``` **Example:** ```typescript theme={null} await client.tokenization.unlinkToken('token-id'); ``` *** ## Complete Example ```typescript theme={null} import { OumlaSdkApiClient, OumlaSdkApiEnvironment } from '@oumla/sdk'; const client = new OumlaSdkApiClient({ environment: OumlaSdkApiEnvironment.Sandbox, apiKey: 'your-api-key' }); async function tokenizationWorkflow() { // Create collection const collection = await client.tokenization.createCollection({ addressId: 'address-id', type: 'NON_FUNGIBLE_TOKEN', clientShare: process.env.CLIENT_SHARE!, network: 'ETHEREUM', displayName: 'My NFT Collection', createParams: { initializeParams: [ { name: '_name', type: 'string', value: 'My NFT Collection' }, { name: '_symbol', type: 'string', value: 'MNFT' }, { name: 'defaultAdmin', type: 'address', value: '0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb' }, { name: 'minter', type: 'address', value: '0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb' }, { name: 'pauser', type: 'address', value: '0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb' } ] } }); // Check workflow status let status; do { status = await client.workflows.getWorkflowStatus(collection.data.workflowResult.workflowId); await new Promise(resolve => setTimeout(resolve, 2000)); } while (status.data.status === 'RUNNING'); if (status.data.status === 'COMPLETED') { // The operationId returned from createCollection is the collection ID const collectionId = collection.data.workflowResult.operationId; // Mint token const mintResult = await client.tokenization.mintToken(collectionId, { addressId: 'address-id', clientShare: process.env.CLIENT_SHARE!, to: '0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb', metadata: { name: 'My First NFT', image: 'https://example.com/image.png' } }); console.log('Mint workflow started:', mintResult.data.workflowResult.workflowId); } // Link an existing contract const linked = await client.tokenization.linkContract({ networkId: 'your-network-id', contractAddress: '0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb', displayName: 'My Existing Token' }); // List all tokens const tokens = await client.tokenization.getTokens({ skip: 0, take: 50 }); console.log('Organization tokens:', tokens.data); } ``` # Transactions Source: https://docs.oumla.com/sdk-reference/transactions SDK methods for querying transaction history Query transaction history by address, wallet, profile, or organization. ## Methods ### `getTransactionsByAddress()` Get transactions for a specific address. **Signature:** ```typescript theme={null} getTransactionsByAddress( address: string, request?: GetTransactionsByAddressRequest, requestOptions?: RequestOptions ): Promise ``` **Parameters:** | Parameter | Type | Required | Description | | -------------- | -------- | -------- | ----------------------------- | | `address` | `string` | Yes | Blockchain address | | `request.skip` | `number` | No | Number of records to skip | | `request.take` | `number` | No | Number of records to retrieve | **Example:** ```typescript theme={null} const transactions = await client.transactions.getTransactionsByAddress( '0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb', { skip: 0, take: 50 } ); console.log('Transactions:', transactions.data); ``` *** ### `getTransactionsByMiniWallet()` Get transactions for a specific wallet. **Signature:** ```typescript theme={null} getTransactionsByMiniWallet( miniWalletId: string, request?: GetTransactionsByMiniWalletRequest, requestOptions?: RequestOptions ): Promise ``` **Example:** ```typescript theme={null} const transactions = await client.transactions.getTransactionsByMiniWallet( 'wallet-id', { skip: 0, take: 50 } ); ``` *** ### `getTransactionsByProfile()` Get transactions for a specific profile. **Signature:** ```typescript theme={null} getTransactionsByProfile( reference: string, request?: GetTransactionsByProfileRequest, requestOptions?: RequestOptions ): Promise ``` **Example:** ```typescript theme={null} const transactions = await client.transactions.getTransactionsByProfile( 'user-123', { skip: 0, take: 50 } ); ``` *** ### `getTransactionsByOrganization()` Get all transactions for the organization. **Signature:** ```typescript theme={null} getTransactionsByOrganization( request: GetTransactionsByOrganizationRequest, requestOptions?: RequestOptions ): Promise ``` **Example:** ```typescript theme={null} const transactions = await client.transactions.getTransactionsByOrganization({ skip: 0, take: 100 }); ``` # Wallets Source: https://docs.oumla.com/sdk-reference/wallets SDK methods for creating and managing wallets Create and manage blockchain wallets for profiles and organizations. Wallets are associated with specific blockchain networks. ## Methods ### `createWallet()` Create a new wallet for a profile. **Signature:** ```typescript theme={null} createWallet( request: CreateWalletRequest, requestOptions?: RequestOptions ): Promise ``` **Parameters:** | Parameter | Type | Required | Description | | ------------------- | -------- | -------- | --------------------------------- | | `request.reference` | `string` | Yes | Profile reference | | `request.networkId` | `string` | Yes | Network ID (from `getNetworks()`) | **Example:** ```typescript theme={null} const wallet = await client.wallets.createWallet({ reference: 'user-123', networkId: 'your-network-id' }); console.log('Wallet ID:', wallet.data.data?.id); console.log('Wallet reference:', wallet.data.data?.reference); console.log('Network ID:', wallet.data.data?.networkId); ``` *** ### `getWalletsByProfile()` Get all wallets for a specific profile. **Signature:** ```typescript theme={null} getWalletsByProfile( reference: string, request?: GetWalletsByProfileRequest, requestOptions?: RequestOptions ): Promise ``` **Parameters:** | Parameter | Type | Required | Description | | -------------- | -------- | -------- | ----------------------------- | | `reference` | `string` | Yes | Profile reference | | `request.skip` | `number` | No | Number of records to skip | | `request.take` | `number` | No | Number of records to retrieve | **Example:** ```typescript theme={null} const wallets = await client.wallets.getWalletsByProfile('user-123', { skip: 0, take: 50 }); console.log('Profile wallets:', wallets.data.data); ``` *** ### `getWalletsForOrganization()` Get all wallets for the organization. **Signature:** ```typescript theme={null} getWalletsForOrganization( request: GetWalletsForOrganizationRequest, requestOptions?: RequestOptions ): Promise ``` **Parameters:** | Parameter | Type | Required | Description | | -------------- | -------- | -------- | ----------------------------- | | `request.skip` | `number` | No | Number of records to skip | | `request.take` | `number` | No | Number of records to retrieve | **Example:** ```typescript theme={null} const wallets = await client.wallets.getWalletsForOrganization({ skip: 0, take: 100 }); console.log('Organization wallets:', wallets.data.data); ``` **Note**: When using `createAddressV2()`, wallets are automatically created if they don't exist, so you may not need to explicitly create wallets in most cases. ## Complete Example ```typescript theme={null} import { OumlaSdkApiClient, OumlaSdkApiEnvironment } from '@oumla/sdk'; const client = new OumlaSdkApiClient({ environment: OumlaSdkApiEnvironment.Sandbox, apiKey: 'your-api-key' }); async function manageWallets() { // Get available networks const networks = await client.networks.getNetworks({ skip: 0, take: 50, enabled: true }); const networkId = networks.data.networks[0].id; // Create a wallet const wallet = await client.wallets.createWallet({ reference: 'user-123', networkId }); console.log('Created wallet for reference:', wallet.data.data?.reference); // Get all wallets for a profile const profileWallets = await client.wallets.getWalletsByProfile('user-123'); console.log('Profile has', profileWallets.data.data?.length ?? 0, 'wallets'); // Get all organization wallets const orgWallets = await client.wallets.getWalletsForOrganization({ skip: 0, take: 100 }); console.log('Organization has', orgWallets.data.data?.length ?? 0, 'wallets'); } ``` # Withdrawals Source: https://docs.oumla.com/sdk-reference/withdrawals SDK methods for creating withdrawal requests Create and execute withdrawal requests. Supports both Bitcoin (multi-address) and Ethereum (single-address) networks. ## Methods ### `createWithdraw()` Create a withdrawal request. **Signature:** ```typescript theme={null} createWithdraw( request: CreateWithdrawRequest, requestOptions?: RequestOptions ): Promise ``` **Parameters:** | Parameter | Type | Required | Description | | --------------------- | ---------- | -------- | --------------------------------- | | `request.networkId` | `string` | Yes | Network ID (from `getNetworks()`) | | `request.from` | `string[]` | Yes | Array of source addresses | | `request.to` | `string` | Yes | Recipient address | | `request.amount` | `string` | Yes | Amount to withdraw | | `request.clientShare` | `string` | Yes | Client share for signing | **Example - Bitcoin (Multi-address):** ```typescript theme={null} const withdrawal = await client.withdraw.createWithdraw({ networkId: 'your-btc-network-id', from: [ 'bc1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlh', 'bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4' ], to: 'bc1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlh', amount: '0.001', clientShare: 'your-client-share' }); console.log('Withdrawal ID:', withdrawal.data.data?.id); console.log('Status:', withdrawal.data.data?.status); ``` **Example - Ethereum (Single-address):** ```typescript theme={null} const withdrawal = await client.withdraw.createWithdraw({ networkId: 'your-eth-network-id', from: [ '0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb' ], to: '0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb', amount: '0.1', clientShare: 'your-client-share' }); // For Ethereum, id is the transaction hash console.log('Transaction Hash:', withdrawal.data.data?.id); ``` **Network Differences**: Bitcoin supports multi-address withdrawals (withdraw from multiple addresses in one transaction), while Ethereum only supports single-address withdrawals. # Workflows Source: https://docs.oumla.com/sdk-reference/workflows SDK methods for tracking async workflow status Track the status of async operations like collection creation, token minting, and contract deployment. ## Methods ### `getWorkflowStatus()` Get the status and result of an async workflow. **Signature:** ```typescript theme={null} getWorkflowStatus( workflowId: string, requestOptions?: RequestOptions ): Promise ``` **Parameters:** | Parameter | Type | Required | Description | | ------------ | -------- | -------- | ----------- | | `workflowId` | `string` | Yes | Workflow ID | **Example:** ```typescript theme={null} const status = await client.workflows.getWorkflowStatus('workflow-id'); console.log('Status:', status.data.status); console.log('Result:', status.data.result); if (status.data.status === 'COMPLETED') { console.log('Workflow completed successfully!'); console.log('Result:', status.data.result); } else if (status.data.status === 'FAILED') { console.error('Workflow failed:', status.data.error); } ``` **Response:** ```typescript theme={null} { data: { status: 'RUNNING' | 'COMPLETED' | 'FAILED' | 'TERMINATED', result?: any, error?: string } } ``` ## Workflow Status Values * **RUNNING**: Operation is in progress * **COMPLETED**: Operation completed successfully * **FAILED**: Operation failed (check error details) * **TERMINATED**: Operation was terminated ## Complete Example ```typescript theme={null} import { OumlaSdkApiClient, OumlaSdkApiEnvironment } from '@oumla/sdk'; const client = new OumlaSdkApiClient({ environment: OumlaSdkApiEnvironment.Sandbox, apiKey: 'your-api-key' }); async function waitForWorkflow(workflowId: string) { let status; let attempts = 0; const maxAttempts = 60; // 2 minutes max (60 * 2 seconds) do { status = await client.workflows.getWorkflowStatus(workflowId); console.log(`Attempt ${attempts + 1}: Status = ${status.data.status}`); if (status.data.status === 'COMPLETED') { console.log('Workflow completed!', status.data.result); return status.data.result; } if (status.data.status === 'FAILED') { throw new Error(`Workflow failed: ${status.data.error}`); } if (status.data.status === 'TERMINATED') { throw new Error('Workflow was terminated'); } // Wait 2 seconds before checking again await new Promise(resolve => setTimeout(resolve, 2000)); attempts++; } while (attempts < maxAttempts && status.data.status === 'RUNNING'); if (status.data.status === 'RUNNING') { throw new Error('Workflow timeout - still running after max attempts'); } return status.data.result; } // Usage try { const result = await waitForWorkflow('workflow-id'); console.log('Final result:', result); } catch (error) { console.error('Error waiting for workflow:', error); } ``` **Polling Strategy**: Implement exponential backoff for production applications to avoid excessive API calls while waiting for workflows to complete.