This repository is part of the Tether WDK (Wallet Development Kit) ecosystem. It follows strict coding conventions and tooling standards to ensure consistency, reliability, and cross-platform compatibility (Node.js and Bare runtime).
- Architecture: Modular architecture with clear separation between Core, Wallet managers, and Protocols.
- Runtime: Supports both Node.js and Bare runtime.
- Language: JavaScript (ES2015+).
- Module System: ES Modules (
"type": "module"in package.json). - Type Checking: TypeScript is used purely for generating type declarations (
.d.ts). The source code remains JavaScript.- Command:
npm run build:types
- Command:
- Linting:
standard(JavaScript Standard Style).- Command:
npm run lint/npm run lint:fix
- Command:
- Testing:
jest(configured withexperimental-vm-modulesfor ESM support).- Command:
npm test
- Command:
- Dependencies:
cross-envis consistently used for environment variable management in scripts.
- File Naming: Kebab-case (e.g.,
wallet-manager.js). - Class Naming: PascalCase (e.g.,
WDK). - Private Members: Prefixed with
_(underscore) and explicitly documented with@private. - Imports: Explicit file extensions are mandatory (e.g.,
import ... from './file.js'). - Copyright: All source files must include the standard Tether copyright header.
Source code must be strictly typed using JSDoc comments to support the build:types process.
- Types: Use
@typedefto define or import types. - Methods: Use
@param,@returns,@throws. - Generics: Use
@template.
- Install:
npm install - Lint:
npm run lint - Test:
npm test - Build Types:
npm run build:types
index.js: Main entry point.bare.js: Entry point for Bare runtime optimization.src/: Core logic.types/: Generated type definitions (do not edit manually).
- Domain: Core Orchestrator.
- Role: Central entry point for the WDK. Manages lifecycle of multiple wallet instances, protocols, and transaction policies.
- Key Pattern: Dependency Injection (registerWallet, registerProtocol, registerPolicy).
- Architecture:
WDKclass manages a collection ofWalletManagerinstances and aPolicyEnginethat intercepts write-facing operations on every account returned fromgetAccount/getAccountByPath.
- Source lives under
src/policy/. Public surface is thePolicyViolationErrorandPolicyConfigurationErrorclasses plus thePolicy*/SimulationResulttypedefs re-exported fromindex.js. Everything else undersrc/policy/is internal. - The engine wraps account write methods and protocol getters at
getAccounttime. When any policy applies to an account ("governed"), the proxy wraps every method inOPERATIONSthat exists on the underlying account — not just the ones referenced by registered rules. Combined with the default-deny evaluator (below) this closes sibling-method bypasses (e.g. a "cap transfer" policy cannot be sidestepped bysendTransaction({ to: token, data: ERC-20-transfer-calldata }),approveto an attacker, off-chain Permit viasignTypedData, or ERC-7702delegate). - Default-deny on governed accounts.
policy-evaluator.evaluate()returns BLOCK withreason: 'no-applicable-rule'whenever an operation reaches it and no rule addresses that operation. To opt into permissive semantics on a specific account, register a wildcard ALLOW baseline (operation: '*', action: 'ALLOW') and layer specific DENYs on top. Accounts with no registered policies are not governed and skip the proxy entirely (zero cost). - Operation name = canonical IWalletAccount method name. The engine wraps
account[op]by string lookup, so every entry inOPERATIONS(src/policy/constants.js) must match the exact method name on@tetherto/wdk-wallet's baseIWalletAccountor a registered protocol class. A divergence silently no-ops the policy (registers fine, never fires) — which is worse than a hard error. When adding a new wallet method that emits a signature or moves value (e.g.,signTransaction,sign), add it toOPERATIONSand thePolicyOperationtypedef. Conversely, do not add aspirational entries that no wallet implements; the schema accepts them, but they produce silently-broken policies. - Two scopes only:
project(with optional wallet restriction declared via the policy'swalletfield) andaccount(with requiredwalletfield and a per-accountaccountslist of paths and/or integer indexes). Thewalletvalue is the same string passed toregisterWallet— an opaque consumer-chosen key, not a blockchain identifier.walletaccepts a single string or a non-empty array of strings; omitting it on a project-scope policy applies it across every registered wallet. DENY wins across scopes; account-scopeALLOWrules can short-circuit project-scope DENYs viaoverride_broader_scope: true. - Index-form
accountsentries match the index passed towdk.getAccount(wallet, index). They do not match accounts retrieved viagetAccountByPathbecause the wallet manager has no synchronous path → index resolver. Path-form entries match either retrieval style. - Nested-call escape works via a
Proxy(seesrc/policy/policy-account-proxy.js), not an async-context marker. The engine returns a Proxy that exposes enforced versions of write methods; the proxy'sgettrap binds non-enforced methods to the underlying account, so internalthis.method()calls and protocol code that holds a direct reference to the account naturally bypass enforcement. The policy engine itself does not mutate the account (the WDK's_registerProtocolsstep does installregisterProtocol/getXProtocolhelpers before the proxy is built — separate, pre-existing concern). The same code path runs on every JavaScript runtime that supportsProxy— noAsyncLocalStorage, no runtime detection, no Bare-specific fallback. - Enforcement applies to the surface of the proxy only. Underscore-prefixed fields (e.g.
protocol._account) reach the raw account and bypass enforcement by design — they are private by convention and not part of the supported surface. The same is true for protocol methods that internally call account operations via their storedthis._account(e.g.bridge.bridge(...)callingthis._account.sendTransaction(...)); this is the documented nested-call escape. proxy.registerProtocol(...)is special: the underlying helper returns the raw account, so thegettrap rewrites the return value to be the proxy itself. Without this rewrite,proxy.registerProtocol(...).sendTransaction(...)would skip enforcement.- Each condition is raced against
conditionTimeoutMs(default 30s, settable viaRegisterPolicyOptions). A condition that throws or times out is fail-closed for DENY rules (treated as matched → block) and fail-open-as-no-match for ALLOW rules. - Conditions are user-supplied functions in Phase 1. The engine accepts
stateandonSuccessrule fields for Phase 2 (engine-managed state + post-execution hooks) but ignores them at runtime;stateis deep-cloned at registration so callers can't mutate engine state post-registration. - Tests live in
tests/wdk-policy.test.jsand exercise the engine exclusively through the public WDK API.