@t402/wdk
v2.9.0
Published
T402 Payment Protocol integration with Tether Wallet Development Kit (WDK)
Downloads
91
Maintainers
Readme
@t402/wdk
Multi-chain wallet abstraction layer for the T402 payment protocol
This package provides T402's own wallet abstraction: multi-chain account management, balance aggregation, seed phrase support, and ready-to-use T402-compatible signers. The interfaces are structurally typed so the wallet implementation can be swapped — including, at the user's option, Tether's open-source WDK (Apache 2.0). T402 has no formal affiliation with Tether Operations Limited.
Features
- Multi-chain Wallets - Manage wallets across Ethereum, Arbitrum, Base, and more
- T402-Compatible Signers - Ready-to-use signers for T402 HTTP payments
- Balance Aggregation - View balances across all configured chains
- Cross-chain Bridging - Bridge USDT0 between chains via LayerZero OFT
- Balance Caching - Reduce RPC calls with configurable TTL caching
- Comprehensive Error Handling - Typed errors with retry logic
Installation
npm install @t402/wdk
# or
pnpm add @t402/wdkYou'll also need the WDK packages:
npm install @tetherto/wdk @tetherto/wdk-wallet-evm
# Optional: for bridging support
npm install @tetherto/wdk-protocol-bridge-usdt0-evmQuick Start
1. Register WDK Modules
Before using T402WDK, register the WDK modules once at app startup:
import WDK from '@tetherto/wdk'
import WalletManagerEvm from '@tetherto/wdk-wallet-evm'
import BridgeUsdt0Evm from '@tetherto/wdk-protocol-bridge-usdt0-evm' // Optional
import { T402WDK } from '@t402/wdk'
// Register WDK modules (once at app startup)
T402WDK.registerWDK(WDK, WalletManagerEvm, BridgeUsdt0Evm)2. Create a Wallet
// Generate a new seed phrase
const seedPhrase = T402WDK.generateSeedPhrase()
// Or use an existing seed phrase
const seedPhrase = 'your twelve word seed phrase here ...'
// Create wallet with chain configurations
const wallet = new T402WDK(seedPhrase, {
arbitrum: 'https://arb1.arbitrum.io/rpc',
ethereum: 'https://eth.llamarpc.com',
base: 'https://mainnet.base.org',
})3. Get a Signer for T402 Payments
import { createT402HTTPClient } from '@t402/core'
// Get signer for Arbitrum
const signer = await wallet.getSigner('arbitrum')
// Use with T402 HTTP client
const client = createT402HTTPClient({
signers: [{ scheme: 'exact', signer }],
})
// Make paid requests
const response = await client.fetch('https://api.example.com/premium')API Reference
T402WDK Class
Static Methods
// Register WDK modules (required before creating instances)
T402WDK.registerWDK(WDK, WalletManagerEvm, BridgeUsdt0Evm?);
// Check registration status
T402WDK.isWDKRegistered(): boolean;
T402WDK.isWalletManagerRegistered(): boolean;
T402WDK.isBridgeRegistered(): boolean;
// Generate a new BIP-39 seed phrase
T402WDK.generateSeedPhrase(): string;Constructor
const wallet = new T402WDK(
seedPhrase: string, // BIP-39 mnemonic (12, 15, 18, 21, or 24 words)
config: T402WDKConfig, // Chain RPC configurations
options?: T402WDKOptions // Optional: cache settings
);Config Example:
const wallet = new T402WDK(
seedPhrase,
{
// String shorthand (uses default chain settings)
arbitrum: 'https://arb1.arbitrum.io/rpc',
// Full config object
ethereum: {
provider: 'https://eth.llamarpc.com',
chainId: 1,
network: 'eip155:1',
},
},
{
// Optional cache configuration
cache: {
enabled: true,
tokenBalanceTTL: 30000, // 30 seconds
nativeBalanceTTL: 15000, // 15 seconds
aggregatedBalanceTTL: 60000, // 60 seconds
},
},
)Instance Properties
wallet.isInitialized: boolean; // True if WDK is ready
wallet.initializationError: Error | null; // Error if initialization failed
wallet.isCacheEnabled: boolean; // True if balance caching is enabledChain Management
// Get all configured chain names
wallet.getConfiguredChains(): string[];
// Returns: ['arbitrum', 'ethereum', 'base']
// Get chain configuration
wallet.getChainConfig('arbitrum'): NormalizedChainConfig | undefined;
// Check if chain is configured
wallet.isChainConfigured('arbitrum'): boolean;
// Get chains that support USDT0
wallet.getUsdt0Chains(): string[];
// Get chains that support bridging
wallet.getBridgeableChains(): string[];Signers
// Get a T402-compatible signer for a chain
const signer = await wallet.getSigner('arbitrum', accountIndex?: number);
// Get wallet address
const address = await wallet.getAddress('arbitrum');
// Clear signer cache (forces re-initialization)
wallet.clearSignerCache();Balance Operations
// Get USDT0 balance on a chain
const usdt0Balance = await wallet.getUsdt0Balance('arbitrum')
// Get USDC balance on a chain
const usdcBalance = await wallet.getUsdcBalance('base')
// Get all balances for a chain
const chainBalance = await wallet.getChainBalances('arbitrum')
// Returns: { chain, network, native, tokens: [...] }
// Get aggregated balances across all chains
const allBalances = await wallet.getAggregatedBalances()
// Returns: { totalUsdt0, totalUsdc, chains: [...] }
// Find best chain for a payment amount
const best = await wallet.findBestChainForPayment(1000000n, 'USDT0')
// Returns: { chain: 'arbitrum', token: 'USDT0', balance: 5000000n } | nullBridging
// Check if bridging is possible
wallet.canBridge('arbitrum', 'ethereum'): boolean;
// Get available destinations from a chain
wallet.getBridgeDestinations('arbitrum'): string[];
// Bridge USDT0 between chains
const result = await wallet.bridgeUsdt0({
fromChain: 'ethereum',
toChain: 'arbitrum',
amount: 100_000000n, // 100 USDT0 (6 decimals)
recipient?: '0x...', // Optional, defaults to same wallet
});
// Returns: { txHash: '0x...', estimatedTime: 300 }Cache Management
// Get cache configuration
wallet.getCacheConfig(): BalanceCacheConfig;
// Get cache statistics
wallet.getCacheStats(): BalanceCacheStats;
// Invalidate all cached balances
wallet.invalidateBalanceCache();
// Invalidate cache for a specific chain
wallet.invalidateChainCache('arbitrum');
// Invalidate cache for a specific address
wallet.invalidateAddressCache('0x1234...');
// Dispose resources (call when done)
wallet.dispose();WDKSigner Class
The signer returned by wallet.getSigner() implements the T402 ClientEvmSigner interface:
interface WDKSigner {
readonly address: Address
// Sign EIP-712 typed data (used by T402 for EIP-3009)
signTypedData(message: {
domain: Record<string, unknown>
types: Record<string, unknown>
primaryType: string
message: Record<string, unknown>
}): Promise<`0x${string}`>
// Sign a personal message
signMessage(message: string | Uint8Array): Promise<`0x${string}`>
// Get native token balance
getBalance(): Promise<bigint>
// Get ERC20 token balance
getTokenBalance(tokenAddress: Address): Promise<bigint>
// Estimate gas for a transaction
estimateGas(params: { to: Address; value?: bigint; data?: string }): Promise<bigint>
// Send a transaction
sendTransaction(params: {
to: Address
value?: bigint
data?: string
}): Promise<{ hash: `0x${string}` }>
// Utility methods
getChain(): string
getChainId(): number
getAccountIndex(): number
isInitialized: boolean
}Error Handling
All errors are typed and include context information:
import {
WDKError,
WDKInitializationError,
ChainError,
SignerError,
SigningError,
BalanceError,
TransactionError,
BridgeError,
RPCError,
WDKErrorCode,
isWDKError,
hasErrorCode,
wrapError,
withRetry,
withTimeout,
} from '@t402/wdk'
try {
const signer = await wallet.getSigner('polygon')
} catch (error) {
if (isWDKError(error)) {
console.error(`Error code: ${error.code}`)
console.error(`Message: ${error.message}`)
console.error(`Context:`, error.context)
if (hasErrorCode(error, WDKErrorCode.CHAIN_NOT_CONFIGURED)) {
console.error('Chain is not configured')
}
if (error.isRetryable()) {
// Can retry this operation
}
}
}Error Codes
| Code Range | Category | | ---------- | -------------------------- | | 1xxx | Initialization errors | | 2xxx | Chain configuration errors | | 3xxx | Signer errors | | 4xxx | Signing errors | | 5xxx | Balance errors | | 6xxx | Transaction errors | | 7xxx | Bridge errors | | 8xxx | RPC errors |
Retry Utilities
import { withRetry, withTimeout } from '@t402/wdk'
// Retry an operation with exponential backoff
const balance = await withRetry(() => signer.getBalance(), { maxRetries: 3, baseDelay: 500 })
// Add timeout to a promise
const result = await withTimeout(
someAsyncOperation(),
30000, // 30 second timeout
'Operation description',
)Supported Chains
| Chain | Chain ID | USDT0 | Bridging | | --------- | -------- | ----- | -------- | | Ethereum | 1 | ✅ | ✅ | | Arbitrum | 42161 | ✅ | ✅ | | Base | 8453 | ✅ | ✅ | | Ink | 57073 | ✅ | ✅ | | Berachain | 80094 | ✅ | ✅ | | Unichain | 130 | ✅ | ✅ |
Examples
Complete Payment Flow
import WDK from '@tetherto/wdk'
import WalletManagerEvm from '@tetherto/wdk-wallet-evm'
import { T402WDK } from '@t402/wdk'
import { createT402HTTPClient } from '@t402/core'
// 1. Setup (once at app startup)
T402WDK.registerWDK(WDK, WalletManagerEvm)
// 2. Create wallet
const wallet = new T402WDK(seedPhrase, {
arbitrum: 'https://arb1.arbitrum.io/rpc',
})
// 3. Check balance before payment
const balance = await wallet.getUsdt0Balance('arbitrum')
console.log(`USDT0 Balance: ${balance / 1000000n} USDT0`)
// 4. Get signer and create client
const signer = await wallet.getSigner('arbitrum')
const client = createT402HTTPClient({
signers: [{ scheme: 'exact', signer }],
})
// 5. Make paid request
const response = await client.fetch('https://api.example.com/premium')
// 6. Invalidate cache after payment
wallet.invalidateChainCache('arbitrum')Multi-Chain Balance Check
const wallet = new T402WDK(seedPhrase, {
arbitrum: 'https://arb1.arbitrum.io/rpc',
ethereum: 'https://eth.llamarpc.com',
base: 'https://mainnet.base.org',
})
// Get all balances
const balances = await wallet.getAggregatedBalances()
console.log(`Total USDT0: ${balances.totalUsdt0 / 1000000n}`)
console.log(`Total USDC: ${balances.totalUsdc / 1000000n}`)
for (const chain of balances.chains) {
console.log(`\n${chain.chain}:`)
console.log(` Native: ${chain.native}`)
for (const token of chain.tokens) {
console.log(` ${token.symbol}: ${token.formatted}`)
}
}Auto-Select Best Chain
const amount = 50_000000n // 50 USDT0
// Find the best chain with sufficient balance
const best = await wallet.findBestChainForPayment(amount)
if (best) {
console.log(`Use ${best.token} on ${best.chain}`)
const signer = await wallet.getSigner(best.chain)
// Use signer for payment...
} else {
console.log('Insufficient balance on all chains')
}Cross-Chain Bridge
import BridgeUsdt0Evm from '@tetherto/wdk-protocol-bridge-usdt0-evm'
// Register with bridge support
T402WDK.registerWDK(WDK, WalletManagerEvm, BridgeUsdt0Evm)
const wallet = new T402WDK(seedPhrase, {
ethereum: 'https://eth.llamarpc.com',
arbitrum: 'https://arb1.arbitrum.io/rpc',
})
// Check if bridging is possible
if (wallet.canBridge('ethereum', 'arbitrum')) {
// Bridge 100 USDT0 from Ethereum to Arbitrum
const result = await wallet.bridgeUsdt0({
fromChain: 'ethereum',
toChain: 'arbitrum',
amount: 100_000000n,
})
console.log(`Bridge tx: ${result.txHash}`)
console.log(`Estimated time: ${result.estimatedTime}s`)
}Testing
For testing without actual WDK, use the MockWDKSigner:
import { MockWDKSigner } from '@t402/wdk';
const mockSigner = new MockWDKSigner(
'0x1234567890123456789012345678901234567890',
'0xprivatekey...'
);
// Use in tests
const signature = await mockSigner.signTypedData({...});TypeScript Support
Full TypeScript support with exported types:
import type {
T402WDKConfig,
T402WDKOptions,
T402BalanceCacheConfig,
NormalizedChainConfig,
TokenBalance,
ChainBalance,
AggregatedBalance,
BridgeParams,
BridgeResult,
T402WDKSigner,
WDKAccount,
WDKInstance,
WDKConstructor,
CacheConfig,
CacheStats,
BalanceCacheConfig,
BalanceCacheStats,
RetryConfig,
} from '@t402/wdk'Related Packages
@t402/core- Core protocol types and client@t402/evm- EVM mechanism implementation@t402/wdk-gasless- ERC-4337 gasless payments@t402/wdk-bridge- Cross-chain LayerZero bridging@t402/wdk-multisig- Safe multi-sig wallets
License
Apache 2.0
Trademark Notice
T402 is an independent open-source project. It is not affiliated with, endorsed by, sponsored by, or in any way officially connected with Tether Operations Limited, Tether Holdings, or any of their affiliates.
"Tether", "USDT", "USDT0", "USDC", and other token names and trademarks referenced are the property of their respective owners and are used nominatively to describe token interoperability or to reference public open-source products (Apache 2.0). Compatibility with Tether's open-source WDK is at the user's option and does not represent a formal partnership.
