Module Integration
About
This guide explains how to create and register a signer-side module in the Extra Wallet Signer codebase.
Signer modules:
- encapsulate view logic, storage/state logic, or transaction signing,
- are wired into the build system via a small
module.config.tsfile, - are exposed to the consumer layer through a generated
loadSafeServiceMethodfunction.
If you’ve read the consumer-side module guide, think of signer modules as the simpler, logic-focused counterpart: no React components, only functions.
File Structure
Modules exposed to the consumer are organized by functional area:
src/services/safeView/– View logic (e.g., form rendering, prompts).src/services/safeStorage/– State and storage logic (e.g., saving wallets, renaming portfolios).src/services/safeSigner/– Transaction signing logic.src/services/core/– Core logic not tied to a specific domain (e.g., service worker, theme setup).
Each of these directories has a modules/ subdirectory. The example structure:
src/services/safeView/
modules/
qrEncrypt/
submitQrEncryptForm.ts
showQrEncryptForm.ts
module.config.ts
You create a folder per logical module under modules/, place your action files there, and add a module.config.ts alongside them.
Module Structure
Each signer module directory typically contains:
1. Action Files (Functions)
Each file represents a single “action” or capability, for example:
submitQrEncryptForm.tsshowQrEncryptForm.tssignTransaction.tsaddAccounts.ts
Each file should export its function as a named export:
export async function submitQrEncryptForm(/* args */) {
// ...
}
2. Module Config (module.config.ts)
A module.config.ts file must live next to your action files.
It must export a named moduleConfig object:
import type { ModuleConfig } from "@/services/types/moduleConfig";
export const moduleConfig: ModuleConfig = {
doesSupportOfflineMode: true,
paths: ["./submitQrEncryptForm", "./showQrEncryptForm"],
names: ["submitQrEncryptForm", "showQrEncryptForm"],
hasExports: true,
// extraPaths: ['./workers/qrWorker'], // optional
};
Each module folder should have one module.config.ts that lists all exported functions and their files in that folder.
Module Config Reference
Top-Level Properties
| Property | Description |
|---|---|
names | Required. Array of exported function names from your module files. These names are used as keys when calling loadSafeServiceMethod(moduleName). |
paths | Required. Array of relative paths (from module.config.ts) to the files that contain these functions. Usually extensionless (e.g. './submitQrEncryptForm'); the build script resolves .ts. |
doesSupportOfflineMode | Required. Whether this module can be used in offline mode. Set to true only if the logic is needed in offline mode. |
hasExports | Required. Controls how the module is loaded: true → functions are dynamically imported and re-exported via loadSafeServiceMethod; false → the module is auto-executed on import. |
extraPaths | Optional. Extra file paths (e.g., workers, helper scripts) that the build script cannot detect automatically but must be included in the bundle. |
names ↔ paths AlignmentEvery names[i] must refer to a named export in one of the files listed in paths. If a name doesn’t exist in those files, the generated loader will fail at runtime.
Module Loading and loadSafeServiceMethod
The build system uses your moduleConfig to generate two kinds of loaders:
1. Auto-Executed Modules (hasExports: false)
These modules are imported for their side effects only (no exported functions needed). Example use cases: service worker setup, theme bootstrap, one-time initialization code.
Example output:
// src/services/safeService.ts:
import("./core/serviceWorker/setupServiceWorker");
For such modules:
hasExportsis set tofalse.- The module is emitted and imported directly; no
loadSafeServiceMethodintegration is needed.
2. Function Modules (hasExports: true)
For modules with hasExports: true, the build script generates a typed dispatcher:
export async function loadSafeServiceMethod(moduleName: string) {
switch (moduleName) {
case "signTransaction": {
return import("./safeSigner/signTransaction");
}
case "addAccounts": {
return import("./safeStorage/modules/addAccounts/addAccounts");
}
case "createWallet": {
return import("./safeStorage/modules/createWallet/createWallet");
}
// ...
default: {
throw new Error(`Module ${moduleName} not found`);
}
}
}
The link between:
- the string key (e.g.
'signTransaction'), - and the actual file (
'./safeSigner/signTransaction')
is defined in your moduleConfig via names and paths.
The moduleName string is effectively a public API between consumer and signer.
Changing or removing an entry in names without updating the consumer will break calls at runtime.
Checklist for Custom Signer Modules
Before you commit a new signer module, verify:
-
✅
namesis set:- Contains all function names you want to expose.
- Each name matches a real named export in one of the module files.
-
✅
pathsis set:- Paths are relative to
module.config.ts. - They point to the files that contain those exports.
- Paths are relative to
-
✅
doesSupportOfflineModeis set correctly:trueonly if the module must be present offline.falseif it is a purely online module.
-
✅
hasExportsmatches the intended behavior:truefor modules that export functions and are called vialoadSafeServiceMethod.falsefor modules that run once on import and don’t expose functions.
-
✅
extraPathsincludes any workers or dynamically referenced files that the build tool can’t see automatically. -
✅ Module is placed in the correct service area:
safeView,safeStorage,safeSigner, orcore.
Common Pitfalls
1. Mismatched Names and Exports
Symptom: ❌ Iframe encountered error: Error: Module X not found at loadSafeServiceMethod at runtime.
- Check that
namesincludes the exact function names. - Ensure those functions are named exports, not default exports.
2. Incorrect Paths
Symptom: Build fails or dynamic imports throw.
- Confirm
pathsare relative tomodule.config.ts. - Don’t forget nested folders, e.g.
'./addAccounts/addAccounts'.
3. Wrong hasExports Setting
hasExports: falseon a module that you expect to call vialoadSafeServiceMethod→ your function will never be reachable.hasExports: trueon a pure side-effect module → may cause unused code to be included but never executed.
Final Notes
- Signer modules are discovered and wired up at build time.
- Missing required fields or invalid configuration will prevent your module from being loaded or cause runtime errors when the consumer calls it.
- Keep signer logic minimal and focused: perform sensitive operations inside signer modules and expose small, explicit entry points to the consumer.
For questions, issues, or contributions, please contact the core Extra Wallet team or open a PR in the signer repository.