npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

saksh-easy-wallet

v2.0.0

Published

Production-ready multi-currency wallet library with atomic transactions, idempotency, and race-condition protection.

Readme

saksh-easy-wallet

npm version License: MIT Node.js

Production-ready multi-currency wallet library for Node.js + MongoDB.
Atomic operations, idempotency, full ACID fund transfers, and zero race conditions.


👨‍💻 Author

Susheel Kumar[email protected]
Available for freelance and full-time remote work


✨ Features

| Feature | Detail | |---|---| | ⚛️ Atomic operations | $inc + conditional filter — no double-spend ever | | 🔄 ACID fund transfers | Full MongoDB session/transaction with automatic rollback | | 🔑 Idempotency | Duplicate requests within 24 h return cached result | | 🔁 Auto retry | Write-conflict errors retried up to 3× with backoff | | 💱 Multi-currency | Unlimited currencies per wallet, zero config | | 💸 Transaction fees | Built-in fee collection routed to admin wallet | | 📋 Audit log | Every operation persisted with console fallback | | 📡 Event emitter | credit / debit events for real-time hooks | | ♻️ Backward compatible | All legacy saksh* methods still work unchanged |


📋 Requirements

  • Node.js >= 14.0.0
  • MongoDB >= 4.0 with replica set (required for fund transfers)
  • Mongoose >= 6.0.0 (peer dependency)

One-time MongoDB replica set setup

# Start mongod as a replica set
mongod --replSet rs0

# In a new terminal, initialise (run once only)
mongosh --eval "rs.initiate()"

Single operations (credit, debit, getBalance) work without a replica set.
Only fundTransfer requires one.


🚀 Installation

npm install saksh-easy-wallet

⚙️ Environment Variables

| Variable | Required | Default | Description | |---|---|---|---| | ADMIN_EMAIL | Recommended | [email protected] | Receives collected transaction fees | | MONGODB_URI | Yes | — | Your MongoDB connection string |


🎯 Quick Start

const SakshWallet = require('saksh-easy-wallet');
const mongoose = require('mongoose');

async function main() {
    // Connect — include replicaSet param for fund transfers
    await mongoose.connect('mongodb://localhost:27017/wallet?replicaSet=rs0');

    const wallet = new SakshWallet();

    // Create indexes once at startup
    await wallet.ensureIndexes();

    // Credit
    await wallet.credit('[email protected]', 500, 'USD', 'txn_001', 'Initial deposit');

    // Check balance
    const bal = await wallet.getBalance('[email protected]', 'USD');
    console.log(bal); // { email: '[email protected]', currency: 'USD', balance: 500 }

    // Transfer (atomic — rolls back if anything fails)
    await wallet.fundTransfer(
        '[email protected]',
        '[email protected]',
        100, 'USD', 'txn_002', 'Payment for services',
        1    // $1 transaction fee → credited to ADMIN_EMAIL
    );
}

main();

📚 API Reference

new SakshWallet()

Creates a wallet instance. Reads ADMIN_EMAIL from process.env.

const wallet = new SakshWallet();
wallet.setAdmin('[email protected]'); // override at runtime

credit(email, amount, currency, referenceId, description, [transactionFee], [session])

Add funds to a wallet.

const result = await wallet.credit(
    '[email protected]',
    100,
    'USD',
    'ref_001',
    'Top-up'
);
// { message: 'Credited 100 USD.', balance: 100, transaction: {...} }

debit(email, amount, currency, referenceId, description, [transactionFee], [session])

Remove funds. Returns { message: 'Insufficient funds' } if balance is too low.

const result = await wallet.debit(
    '[email protected]',
    40,
    'USD',
    'ref_002',
    'Purchase',
    0.5   // $0.50 fee
);

fundTransfer(senderEmail, receiverEmail, amount, currency, referenceId, description, [transactionFee])

Atomic transfer between two wallets. Uses a MongoDB session — if either side fails, everything rolls back.

const result = await wallet.fundTransfer(
    '[email protected]',
    '[email protected]',
    75, 'USD', 'transfer_001', 'Invoice payment', 0.5
);
// {
//   message: 'Transferred 75 USD from alice to bob.',
//   senderBalance: 424.5,
//   receiverBalance: 75,
//   transaction: { senderTransaction: {...}, receiverTransaction: {...} }
// }

⚠️ Throws if sender has insufficient funds. Uses idempotency — retrying with the same referenceId, sender, receiver, amount, and currency returns the cached result.


getBalance(email, currency)

const { balance } = await wallet.getBalance('[email protected]', 'USD');

getBalanceSummary(email)

Returns all currency balances for a user.

const summary = await wallet.getBalanceSummary('[email protected]');
// { email: '...', balance: { USD: 460, EUR: 200 } }

transactionReport(email)

Transaction history, newest first.

const history = await wallet.transactionReport('[email protected]');
// [{ type, amount, currency, referenceId, description, transactionFee, date }, ...]

setAdmin(adminEmail)

Override the admin email that receives transaction fees.

wallet.setAdmin('[email protected]');

ensureIndexes()

Create all database indexes. Call once at app startup.

await wallet.ensureIndexes();

Events

SakshWallet extends EventEmitter. Listen to 'credit' and 'debit' events:

wallet.on('credit', ({ email, amount, currency, newBalance, referenceId }) => {
    console.log(`+${amount} ${currency} → ${email} (new balance: ${newBalance})`);
});

wallet.on('debit', ({ email, amount, currency, newBalance, referenceId }) => {
    console.log(`-${amount} ${currency} ← ${email} (new balance: ${newBalance})`);
});

🔑 Idempotency

fundTransfer is automatically idempotent. The idempotency key combines: referenceId + senderEmail + receiverEmail + amount + currency

If the exact same request is retried within 24 hours, the cached result is returned — the balance is not modified again.

// First call — executes the transfer
await wallet.fundTransfer('[email protected]', '[email protected]', 50, 'USD', 'pay_001', 'Work');

// Retry (e.g. after a network error) — returns same result, no double charge
await wallet.fundTransfer('[email protected]', '[email protected]', 50, 'USD', 'pay_001', 'Work');

🔧 Advanced Usage

Batch operations with a shared session

const session = await mongoose.startSession();
session.startTransaction();
try {
    await wallet.credit('[email protected]', 100, 'USD', 'batch_1', 'Salary', 0, session);
    await wallet.credit('[email protected]', 200, 'USD', 'batch_2', 'Salary', 0, session);
    await session.commitTransaction();
} catch (err) {
    await session.abortTransaction();
    throw err;
} finally {
    session.endSession();
}

Extending with custom fee logic

class MyWallet extends SakshWallet {
    async smartCredit(email, amount, currency, referenceId, description, userTier) {
        const fee = userTier === 'premium' ? 0 : amount * 0.01;
        return this.credit(email, amount, currency, referenceId, description, fee);
    }
}

🛡️ Error Handling

try {
    await wallet.debit('[email protected]', 9999, 'USD', 'ref_x', 'Big buy');
} catch (err) {
    switch (err.message) {
        case 'Insufficient funds':
            // debit returns this as a value, not a throw — but fundTransfer throws it
            break;
        case 'Invalid email address':
        case 'Amount must be a positive finite number':
        case 'Currency must be a non-empty string':
            // validation errors
            break;
        default:
            // DB or network error
            console.error(err);
    }
}

📊 Database Collections

| Collection | Purpose | |---|---| | walletusers | One doc per user — holds currency→balance map | | wallettransactions | Immutable ledger of every credit/debit | | logs | Operational logs with console fallback | | idempotencies | 24-hour TTL cache of completed operations |


🐛 Troubleshooting

MongoError: Transaction numbers are only allowed on replica sets
Start mongod with --replSet rs0 and run rs.initiate() once.

OverwriteModelError: Cannot overwrite model once compiled
All models are registered with mongoose.models.X || mongoose.model(...) guards. If you still see this, you likely have two different versions of mongoose installed.

High-concurrency write conflicts
The library retries automatically up to 3 times with exponential backoff (50ms, 100ms, 150ms). If conflicts persist under extreme load, check your MongoDB connection pool size.


🧪 Testing

Install mongodb-memory-server for in-process testing:

// tests/wallet.test.js
const { MongoMemoryReplSet } = require('mongodb-memory-server');
const mongoose = require('mongoose');
const SakshWallet = require('saksh-easy-wallet');

let replSet, wallet;

beforeAll(async () => {
    replSet = await MongoMemoryReplSet.create({ replSet: { count: 1 } });
    await mongoose.connect(replSet.getUri());
    wallet = new SakshWallet();
    await wallet.ensureIndexes();
});

afterAll(async () => {
    await mongoose.disconnect();
    await replSet.stop();
});

test('credit and debit', async () => {
    await wallet.credit('[email protected]', 100, 'USD', 'r1', 'Deposit');
    const { balance } = await wallet.getBalance('[email protected]', 'USD');
    expect(balance).toBe(100);

    const result = await wallet.debit('[email protected]', 40, 'USD', 'r2', 'Purchase');
    expect(result.balance).toBe(60);
});

test('insufficient funds', async () => {
    const result = await wallet.debit('[email protected]', 9999, 'USD', 'r3', 'Too much');
    expect(result.message).toBe('Insufficient funds');
});

test('atomic fund transfer', async () => {
    await wallet.credit('[email protected]', 200, 'USD', 'r4', 'Load');
    const transfer = await wallet.fundTransfer(
        '[email protected]', '[email protected]',
        50, 'USD', 'r5', 'Payment'
    );
    expect(transfer.senderBalance).toBe(150);
    expect(transfer.receiverBalance).toBe(50);
});

test('idempotent transfer', async () => {
    const first  = await wallet.fundTransfer('[email protected]', '[email protected]', 10, 'USD', 'r5', 'Payment');
    const second = await wallet.fundTransfer('[email protected]', '[email protected]', 10, 'USD', 'r5', 'Payment');
    // Balance unchanged on second call
    expect(first.senderBalance).toBe(second.senderBalance);
});

Run with:

npm test

📄 License

MIT — free for commercial use. See LICENSE.


🤝 Contact & Support

| | | |---|---| | Author | Susheel Kumar | | Email | [email protected] | | Availability | Open for freelance and full-time remote positions |


Built with care by Susheel Kumar — [email protected]