NestJS Module for Implementing a Token in Redis
Overview
We will create a NestJS module called TokenModule that implements a token system using Redis as the data store. The module will handle token balances, transfers, and burning of tokens. We will leverage Redis's advanced features such as atomic operations, Pub/Sub, and data structures to maximize performance and reliability.
Token Specifications
- Name: e.g.,
"Tarot Token" - Symbol: e.g.,
"TARO" - Decimals: e.g.,
6(common for tokens to allow fractional amounts) - Total Supply: e.g.,
100,000,000(fixed supply) - Burnable: Tokens can be burned (destroyed), reducing the circulating supply.
Data Storage in Redis
1. Token Information
- Key:
token:info - Type: Hash
- Fields:
namesymboldecimalstotalSupplycirculatingSupply(updates when tokens are burned)
2. User Balances
- Key Pattern:
balance:{userId} - Type: String (stores the balance as a string representing a number)
- Alternative: Use a Hash
balanceswithuserIdas the field and balance as the value.
3. Transaction Logs (Optional)
- Key:
transactions - Type: Stream
- Purpose: Logs all transfers and burn events for auditing and history.
4. Pub/Sub Channels
- Transfer Events:
token:transfers - Burn Events:
token:burns - Purpose: Notify subscribed services or clients about token events in real-time.
RedisService Assumption
We assume a RedisService is available in the application context that provides a Redis client instance connected to the Redis server.
@Injectable()
export class RedisService {
getClient(): Redis.RedisClient {
// Returns a Redis client instance
}
}
TokenModule Structure
-
Services:
TokenService: Core service handling all token operations.
-
Providers:
RedisService: Provides Redis client instances.
-
Exports:
TokenService: So it can be used by other modules if needed.
TokenService Interface and Methods
@Injectable()
export class TokenService {
constructor(private readonly redisService: RedisService) {}
async initializeToken(): Promise<void> {
// Initializes token information in Redis
}
async getTokenInfo(): Promise<TokenInfo> {
// Retrieves token information
}
async getBalance(userId: string): Promise<string> {
// Retrieves the balance of a user
}
async transfer(
fromUserId: string,
toUserId: string,
amount: string,
): Promise<void> {
// Transfers tokens from one user to another
}
async burn(userId: string, amount: string): Promise<void> {
// Burns tokens from a user's balance
}
async getTotalSupply(): Promise<string> {
// Retrieves the total supply of the token
}
async getCirculatingSupply(): Promise<string> {
// Retrieves the circulating supply of the token
}
async getTopHolders(
limit: number,
): Promise<{ userId: string; balance: string }[]> {
// Retrieves top token holders
}
}
Detailed Method Implementations
1. initializeToken()
- Purpose: Sets up the token information and initial balances in Redis.
- Process:
- Check if
token:infoalready exists. - If not, set the token information in
token:info. - Distribute the total supply to an initial account or accounts.
- Use
MULTIandEXECfor transaction-like behavior.
- Check if
2. getTokenInfo()
- Purpose: Retrieves token metadata.
- Process:
- Use
HGETALLontoken:info. - Return the token information as a
TokenInfoobject.
- Use
3. getBalance(userId: string)
- Purpose: Gets the balance of a specific user.
- Process:
- Use
GETonbalance:{userId}. - If the key doesn't exist, return
0.
- Use
4. transfer(fromUserId: string, toUserId: string, amount: string)
- Purpose: Transfers tokens between users.
- Process:
- Use Lua scripting to ensure atomicity of multiple operations:
- Check if
fromUserIdhas sufficient balance. - Decrease
fromUserIdbalance. - Increase
toUserIdbalance.
- Check if
- Publish a transfer event via Pub/Sub.
- Use Lua scripting to ensure atomicity of multiple operations:
- Redis Features Used:
- Atomic operations with Lua scripts.
- Pub/Sub for event notifications.
5. burn(userId: string, amount: string)
- Purpose: Burns tokens from a user's balance.
- Process:
- Check if the user has sufficient balance.
- Decrease the user's balance.
- Decrease
circulatingSupplyintoken:infousingHINCRBY. - Publish a burn event via Pub/Sub.
- Redis Features Used:
- Atomic operations.
- Pub/Sub.
6. getTotalSupply()
- Purpose: Retrieves the total token supply.
- Process:
- Use
HGETontoken:infofortotalSupply.
- Use
7. getCirculatingSupply()
- Purpose: Retrieves the circulating supply.
- Process:
- Use
HGETontoken:infoforcirculatingSupply.
- Use
8. getTopHolders(limit: number)
- Purpose: Retrieves the top token holders.
- Process:
- Use a Sorted Set
balances:zsetwhere:- Key:
balances:zset - Member:
userId - Score: Balance
- Key:
- Use
ZREVRANGEto get users with the highest balances.
- Use a Sorted Set
- Redis Features Used:
- Sorted Sets for ranking.
Implementing Redis Data Structures and Commands
1. Token Information (token:info)
HMSET token:info name "NovaToken" symbol "NVT" decimals "18" totalSupply "1000000000" circulatingSupply "1000000000"
2. User Balances (balance:{userId})
-
Set Balance:
SET balance:{userId} {balance} -
Increment/Decrement Balance Atomically:
INCRBY balance:{userId} {amount}
3. Using Lua Scripts for Atomic Transfers
-- Lua script for atomic transfer
local fromKey = "balance:" .. KEYS[1]
local toKey = "balance:" .. KEYS[2]
local amount = tonumber(ARGV[1])
local fromBalance = tonumber(redis.call("GET", fromKey) or "0")
if fromBalance < amount then
return redis.error_reply("Insufficient balance")
end
redis.call("DECRBY", fromKey, amount)
redis.call("INCRBY", toKey, amount)
-- Update sorted set for rankings
redis.call("ZADD", "balances:zset", fromBalance - amount, KEYS[1])
local toBalance = tonumber(redis.call("GET", toKey))
redis.call("ZADD", "balances:zset", toBalance, KEYS[2])
return "OK"
4. Pub/Sub for Event Notifications
-
Publishing an Event:
PUBLISH token:transfers "{ 'from': 'userId1', 'to': 'userId2', 'amount': '100' }" -
Subscribing to Events:
Services or clients can subscribe to
token:transfersandtoken:burnschannels to receive real-time updates.
Utilizing Redis Features Maximally
1. Atomic Operations
- Balances Updates:
- Use
INCRBYandDECRBYfor atomic balance modifications. - Lua scripts ensure multiple operations are executed atomically.
- Use
2. Pub/Sub Mechanism
- Real-Time Notifications:
- Notify other services or microservices about transfers and burns.
- Enables reactive programming and event-driven architectures.
3. Sorted Sets
- Ranking Users:
- Keep track of users' balances in a
Sorted Setfor leaderboards or top holders. - Efficient retrieval of top N users.
- Keep track of users' balances in a
4. Streams (Optional)
- Transaction Logs:
- Use Redis Streams to log all transactions for auditing.
- Can be processed asynchronously for analytics.
Example Interface Definitions
TokenInfo Interface
interface TokenInfo {
name: string;
symbol: string;
decimals: number;
totalSupply: string; // Use string to handle big numbers
circulatingSupply: string;
}
Considerations for Big Numbers
Handling Large Numbers
- Token amounts can be very large (especially with high decimals).
- Use strings to represent numbers in code to prevent precision loss.
- Utilize libraries like
BigNumber.jsfor arithmetic operations.
Sample Method Implementations
transfer(fromUserId: string, toUserId: string, amount: string)
async transfer(fromUserId: string, toUserId: string, amount: string): Promise<void> {
const redisClient = this.redisService.getClient();
const luaScript = `
local fromKey = "balance:" .. KEYS[1]
local toKey = "balance:" .. KEYS[2]
local amount = tonumber(ARGV[1])
local fromBalance = tonumber(redis.call("GET", fromKey) or "0")
if fromBalance < amount then
return redis.error_reply("Insufficient balance")
end
redis.call("DECRBY", fromKey, amount)
redis.call("INCRBY", toKey, amount)
-- Update sorted set for rankings
redis.call("ZADD", "balances:zset", fromBalance - amount, KEYS[1])
local toBalance = tonumber(redis.call("GET", toKey))
redis.call("ZADD", "balances:zset", toBalance, KEYS[2])
return "OK"
`;
try {
await redisClient.eval(luaScript, 2, fromUserId, toUserId, amount);
// Publish transfer event
const event = {
from: fromUserId,
to: toUserId,
amount,
timestamp: new Date().toISOString(),
};
redisClient.publish('token:transfers', JSON.stringify(event));
} catch (error) {
throw new Error(error.message);
}
}
Security and Validation
Input Validation
- Ensure
userIdandamountare validated before processing. - Prevent injection attacks or malformed data.
Error Handling
- Handle exceptions gracefully.
- Return meaningful error messages.
Authentication and Authorization
- Integrate with NestJS guards or middleware to authenticate users.
- Ensure that only authorized users can perform transfers or burns.
Module Registration in NestJS
@Module({
providers: [TokenService, RedisService],
exports: [TokenService],
})
export class TokenModule {}
Usage Example
@Controller('token')
export class TokenController {
constructor(private readonly tokenService: TokenService) {}
@Get('balance/:userId')
async getBalance(
@Param('userId') userId: string,
): Promise<{ balance: string }> {
const balance = await this.tokenService.getBalance(userId);
return { balance };
}
@Post('transfer')
async transfer(@Body() transferDto: TransferDto): Promise<void> {
await this.tokenService.transfer(
transferDto.fromUserId,
transferDto.toUserId,
transferDto.amount,
);
}
// Additional endpoints as needed
}
Maximizing Redis Features
Performance
- High-speed in-memory operations for low latency.
- Efficient data structures for quick data retrieval.
Atomicity
- Ensuring data consistency with atomic operations and Lua scripting.
Scalability
- Redis can be scaled horizontally using clustering.
- Suitable for applications with a large number of users and transactions.
Real-Time Capabilities
- Pub/Sub enables instant notifications and real-time updates.
Potential Limitations and Mitigations
Data Persistence
- Ensure Redis is configured with persistence options (
RDBsnapshots and/orAOFlogs) to prevent data loss. - Consider periodic backups.
Memory Usage
- Monitor Redis memory usage.
- Use efficient data structures and optimize key sizes.
Numeric Precision
- Be cautious with floating-point arithmetic.
- Use string representations or libraries that handle big numbers.
Concurrency
- Redis's single-threaded nature ensures operations are processed sequentially, but high concurrency might require scaling Redis instances.
Conclusion
By leveraging Redis's powerful features within a NestJS module, we can create an efficient and scalable token system that benefits from high-speed operations and real-time capabilities. This module can serve as the backbone for applications requiring quick and reliable token transactions, such as gaming platforms, reward systems, or financial services.
Note: The provided code snippets are for illustrative purposes. In a production environment, ensure to handle exceptions, edge cases, and security concerns appropriately. Testing and code reviews are essential to maintain the integrity and reliability of the token system.
