Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
c15a5a9
feat: communication service for faucet
theblockstalk Dec 14, 2025
3f0b7b3
feat: faucet throttling
theblockstalk Dec 14, 2025
0a27fb8
feat: faucet fails on production
theblockstalk Dec 14, 2025
dc4518d
Merge branch 'development' into feature/faucet2
theblockstalk Dec 17, 2025
e3b32b9
style: fixed lint errors
theblockstalk Dec 17, 2025
fcacc2e
Merge branch 'feature/faucet2' of github.com:Tonomy-Foundation/Tonomy…
theblockstalk Dec 17, 2025
4af264c
fix: get base token address for env
theblockstalk Dec 17, 2025
e919c75
test: fixed test suite
theblockstalk Dec 17, 2025
b4f7987
test: skip veramo tests with a note
theblockstalk Dec 17, 2025
db1fd73
feat: added released sdk
theblockstalk Dec 17, 2025
5eff973
Merge pull request #180 from Tonomy-Foundation/feature/faucet2
theblockstalk Dec 17, 2025
ee25528
chore(release): 1.5.0-development.2 [skip ci]
semantic-release-bot Dec 17, 2025
67d2c29
Merge pull request #192 from Tonomy-Foundation/testnet
sadiabbasi Dec 21, 2025
b86b606
chore(release): 1.6.0 [skip ci]
semantic-release-bot Dec 21, 2025
167a4e3
feat: rebase from master
theblockstalk Dec 21, 2025
7ee9b11
feat: merge from testnet
theblockstalk Dec 21, 2025
5dfd333
Merge branch 'master' into feature/rebase-master
theblockstalk Dec 21, 2025
d8e69eb
feat: latest SDK development release
theblockstalk Dec 21, 2025
8a12016
Merge pull request #193 from Tonomy-Foundation/feature/rebase-master
theblockstalk Dec 21, 2025
6873af9
chore(release): 1.7.0-development.1 [skip ci]
semantic-release-bot Dec 21, 2025
8e7346a
feat: updated to testnet sdk
theblockstalk Dec 22, 2025
342f939
Merge pull request #195 from Tonomy-Foundation/feature/update-sdk
theblockstalk Dec 22, 2025
9769f8c
chore(release): 1.7.0-development.2 [skip ci]
semantic-release-bot Dec 22, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 31 additions & 30 deletions jest.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,39 +3,40 @@ import type { Config } from 'jest';
// https://www.jenchan.biz/blog/dissecting-the-hell-jest-setup-esm-typescript-setup
// https://kulshekhar.github.io/ts-jest/docs/guides/esm-support/
const baseConfig: Config = {
moduleFileExtensions: ['js', 'json', 'ts'],
collectCoverageFrom: ['**/*.(t|j)s'],
coverageDirectory: '../coverage',
testEnvironment: 'node',
transform: {
'^.+\\.m?tsx?$': [
'ts-jest',
{
useESM: true,
tsconfig: './tsconfig.json',
diagnostics: process.env.CI ? true : false,
},
],
},
transformIgnorePatterns: [],
extensionsToTreatAsEsm: ['.ts'],
moduleFileExtensions: ['js', 'json', 'ts'],
collectCoverageFrom: ['**/*.(t|j)s'],
coverageDirectory: '../coverage',
testEnvironment: 'node',
transform: {
'^.+\\.m?tsx?$': [
'ts-jest',
{
useESM: true,
tsconfig: './tsconfig.json',
diagnostics: process.env.CI ? true : false,
},
],
},
transformIgnorePatterns: [],
extensionsToTreatAsEsm: ['.ts'],
};

const config: Config = {
projects: [
{
...baseConfig,
displayName: 'Unit tests',
rootDir: './src',
testRegex: '.*\\.spec\\.ts$',
},
{
...baseConfig,
displayName: 'End-to-end tests',
rootDir: './test',
testRegex: '.e2e-spec.ts$',
},
],
projects: [
{
...baseConfig,
displayName: 'Unit tests',
rootDir: './src',
testRegex: '.*\\.spec\\.ts$',
},
{
...baseConfig,
displayName: 'End-to-end tests',
rootDir: './test',
testRegex: '.e2e-spec.ts$',
setupFilesAfterEnv: ['./setup.ts'],
},
],
};

export default config;
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "tonomy-communication",
"version": "1.6.0-rc.1",
"version": "1.7.0-development.2",
"description": "",
"author": "Rebal Alhaqash",
"private": true,
Expand Down Expand Up @@ -32,7 +32,7 @@
"@nestjs/swagger": "^7.1.10",
"@nestjs/websockets": "^10.2.3",
"@safe-global/sdk-starter-kit": "^3.0.2",
"@tonomy/tonomy-id-sdk": "0.36.0-rc.10",
"@tonomy/tonomy-id-sdk": "0.37.0-rc.2",
"axios": "1.9.0",
"class-transformer": "^0.5.1",
"class-validator": "^0.14.0",
Expand Down
27 changes: 26 additions & 1 deletion src/communication/communication.gateway.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ import { CommunicationGuard } from './communication.guard';
import { BodyDto } from './dto/body.dto';
import { Server } from 'socket.io';
import { SwapBodyDto } from './dto/swapBody.dto';
import { Transaction } from 'typeorm';
import { FaucetBodyDto } from './dto/faucetBody.dto';

export type WebsocketReturnType = {
status: HttpStatus;
Expand Down Expand Up @@ -141,6 +141,31 @@ export class CommunicationGateway
}
}

/**
* Requests testnet tokens from the faucet
* @param {FaucetBodyDto} body - The faucet token message VC or an error from the transformer
* @param client user socket
*/
@SubscribeMessage('v1/faucet/token/tono')
@UseGuards(CommunicationGuard)
async requestFaucetToken(
@MessageBody() body: FaucetBodyDto,
@ConnectedSocket() client: Client,
) {
try {
if (body.error) throw body.error;
if (!body.value) throw new Error('Body not found');
const message = body.value;

return {
status: HttpStatus.OK,
details: await this.usersService.requestFaucetToken(client, message),
};
} catch (e) {
return this.usersService.handleError(e);
}
}

/**
* safely disconnect the user from the server when user disconnects
* @param socket user socket
Expand Down
156 changes: 155 additions & 1 deletion src/communication/communication.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ import {
waitForTonomyTrxFinalization,
prepareSafeWalletTransfer,
createAntelopeDid,
FaucetTokenMessage,
assetToDecimal,
getSigner,
} from '@tonomy/tonomy-id-sdk';
import { tonomySigner } from '../signer';
Expand All @@ -38,6 +40,12 @@ export class CommunicationService {

private readonly loggedInUsers = new Map<string, Socket['id']>();
private readonly userSockets = new Map<string, Client>();
private readonly faucetRequestTracker = new Map<
string,
{ amount: Decimal; timestamp: number }[]
>();
private readonly FAUCET_LIMIT_24H = new Decimal('20000');
private readonly THROTTLE_WINDOW_MS = 24 * 60 * 60 * 1000; // 24 hours in milliseconds

private server!: Server;

Expand Down Expand Up @@ -234,6 +242,152 @@ export class CommunicationService {
return true;
}

/**
* Requests testnet tokens from the faucet service
* @param {Client} socket user socket
* @param {SwapTokenMessage} message signed VC containing faucet request
* @returns boolean if tokens were transferred successfully
*/
async requestFaucetToken(
socket: Client,
message: FaucetTokenMessage,
): Promise<boolean> {
const loggerId = randomString(6);
const payload = message.getPayload();
const issuer = message.getIssuer();

this.logger.debug(
`[Faucet: ${loggerId}]: requestFaucetToken() from ${issuer}`,
);

if (settings.env === 'production') {
throw new HttpException(
`Faucet service is not available in production`,
HttpStatus.BAD_REQUEST,
);
}

await checkIssuerFromTonomyPlatform(
issuer,
payload._testOnly_tonomyAppsWebsiteUsername,
);

const tonomyAccount = getAccountNameFromDid(issuer);
const asset = payload.asset;

if (!asset || asset.trim().length === 0 || !asset.endsWith(' TONO')) {
throw new HttpException(
`Invalid asset format ${asset}`,
HttpStatus.BAD_REQUEST,
);
}

const assetAmount = assetToDecimal(asset);

if (assetAmount.lessThanOrEqualTo(0)) {
throw new HttpException(
`Invalid asset amount ${asset}`,
HttpStatus.BAD_REQUEST,
);
} else if (assetAmount.greaterThan(1000)) {
throw new HttpException(
`Requested asset amount exceeds faucet limit: ${asset}`,
HttpStatus.BAD_REQUEST,
);
}

// Check 24-hour throttling
this.checkFaucetThrottle(issuer, assetAmount, loggerId);

this.logger.log(
`[Faucet: ${loggerId}]: Transferring ${asset} to Tonomy account ${tonomyAccount}`,
);

const trx = await getTokenContract().transfer(
'ops.tmy',
tonomyAccount,
asset,
`Faucet token request ${loggerId}`,
tonomySigner,
);

this.logger.debug(
`[Faucet: ${loggerId}]: Faucet transaction submitted with ID ${trx.transaction_id}`,
);

if (settings.env === 'production' || settings.env === 'testnet') {
// Depends on Hyperion API which is only available on testnet and mainnet
await waitForTonomyTrxFinalization(trx.transaction_id);
this.logger.debug(
`[Faucet: ${loggerId}]: Tonomy transaction ${trx.transaction_id} finalized`,
);
}

// Track this request for throttling purposes
this.trackFaucetRequest(issuer, assetAmount);

this.logger.log(
`[Faucet: ${loggerId}]: Faucet transfer completed successfully`,
);
return true;
}

/**
* Check if account has exceeded 24-hour faucet limit
* @param issuer the DID of the requester
* @param requestAmount the amount being requested
* @param loggerId logging identifier
* @throws HttpException if throttle limit exceeded
*/
private checkFaucetThrottle(
issuer: string,
requestAmount: Decimal,
loggerId: string,
): void {
const now = Date.now();
const requests = this.faucetRequestTracker.get(issuer) || [];

// Remove requests older than 24 hours
const recentRequests = requests.filter(
(req) => now - req.timestamp < this.THROTTLE_WINDOW_MS,
);

// Calculate total requested in last 24 hours
const totalRequested = recentRequests.reduce(
(sum, req) => sum.plus(req.amount),
new Decimal(0),
);

const newTotal = totalRequested.plus(requestAmount);

if (newTotal.greaterThan(this.FAUCET_LIMIT_24H)) {
const remainingAllowance = this.FAUCET_LIMIT_24H.minus(totalRequested);

this.logger.warn(
`[Faucet: ${loggerId}]: Account ${issuer} exceeded 24-hour limit. Requested: ${requestAmount.toString()}, Remaining allowance: ${remainingAllowance.toString()}`,
);
throw new HttpException(
`Daily faucet limit exceeded. You have requested ${totalRequested.toString()} TONO in the last 24 hours. Maximum allowed: ${this.FAUCET_LIMIT_24H.toString()} TONO. Remaining allowance: ${remainingAllowance.toString()} TONO`,
HttpStatus.BAD_REQUEST,
);
}
}

/**
* Track a faucet request for throttling purposes
* @param issuer the DID of the requester
* @param amount the amount requested
*/
private trackFaucetRequest(issuer: string, amount: Decimal): void {
const requests = this.faucetRequestTracker.get(issuer) || [];

requests.push({
amount,
timestamp: Date.now(),
});
this.faucetRequestTracker.set(issuer, requests);
}

/**
* Send a 'veriff' event to a specific user by DID
* @param did recipient DID
Expand Down Expand Up @@ -300,7 +454,7 @@ async function getAccountNameForTonomyAppsPlatform(
} else {
if (_testOnly_tonomyAppsWebsiteUsername) {
throw new Error(
'tonomyAppsWebsiteUsername can only be used in non-production environments',
'tonomyAppsWebsiteUsername can only be used in testing/development environments',
);
}

Expand Down
7 changes: 7 additions & 0 deletions src/communication/dto/faucetBody.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { FaucetTokenMessage } from '@tonomy/tonomy-id-sdk';

export class FaucetBodyDto {
value?: FaucetTokenMessage;
error?: Error | any;
asset?: string; // Format: "1.000000 TONO"
}
7 changes: 6 additions & 1 deletion src/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,12 @@ if (process.env.ETHEREUM_PRIVATE_KEY) {
settings.secrets.basePrivateKey = process.env.ETHEREUM_PRIVATE_KEY;
}

console.log(
if (process.env.BASE_TOKEN_ADDRESS) {
logger.log('Using BASE_TOKEN_ADDRESS from env');
settings.config.baseTokenAddress = process.env.BASE_TOKEN_ADDRESS;
}

logger.log(
`Ethereum signing address: ${new ethers.Wallet(settings.secrets.basePrivateKey).address}`,
);

Expand Down
11 changes: 4 additions & 7 deletions test/communication.e2e-spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,9 @@ import { Socket } from 'socket.io-client';
import {
AuthenticationMessage,
generateRandomKeyPair,
setSettings,
util,
} from '@tonomy/tonomy-id-sdk';

setSettings({
blockchainUrl: 'http://localhost:8888',
});

describe('CommunicationGateway (e2e)', () => {
let app: INestApplication;
let socket: Socket;
Expand All @@ -24,9 +19,11 @@ describe('CommunicationGateway (e2e)', () => {
}).compile();

app = moduleFixture.createNestApplication();
app.listen(5000);
await app.listen(0);
const server: any = app.getHttpServer();
const port: number = server.address().port;

socket = await connectSocket();
socket = await connectSocket(port);
});

afterEach(async () => {
Expand Down
15 changes: 15 additions & 0 deletions test/setup.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { setSettings } from '@tonomy/tonomy-id-sdk';
import commSettings from '../src/settings';

// Initialize SDK settings before all e2e tests run
setSettings({
environment: commSettings.env,
blockchainUrl: commSettings.config.blockchainUrl,
accountSuffix: commSettings.config.accountSuffix,
currencySymbol: commSettings.config.currencySymbol,
basePrivateKey: commSettings.secrets.basePrivateKey,
baseNetwork: commSettings.config.baseNetwork,
baseRpcUrl: commSettings.config.baseRpcUrl,
baseTokenAddress:
process.env.BASE_TOKEN_ADDRESS || commSettings.config.baseTokenAddress,
});
Loading