Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
33 changes: 12 additions & 21 deletions payment_router/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -319,9 +319,7 @@ impl PaymentRouter {
.get(&DataKey::TimelockNonce)
.unwrap_or(0u64);
let next = current + 1;
env.storage()
.instance()
.set(&DataKey::TimelockNonce, &next);
env.storage().instance().set(&DataKey::TimelockNonce, &next);
next
}

Expand Down Expand Up @@ -509,9 +507,7 @@ impl PaymentRouter {
.set(&DataKey::MaxAmount, &max_amount);
env.storage().instance().set(&DataKey::Paused, &false);
env.storage().instance().set(&DataKey::Frozen, &false);
env.storage()
.instance()
.set(&DataKey::TimelockNonce, &0u64);
env.storage().instance().set(&DataKey::TimelockNonce, &0u64);
env.storage().instance().extend_ttl(
Self::INSTANCE_LIFETIME_THRESHOLD,
Self::INSTANCE_BUMP_AMOUNT,
Expand Down Expand Up @@ -548,7 +544,10 @@ impl PaymentRouter {
let nonce = Self::next_nonce(&env);
let queued_at = env.ledger().timestamp();

let entry = TimelockEntry { queued_at, action: action.clone() };
let entry = TimelockEntry {
queued_at,
action: action.clone(),
};

let key = DataKey::TimelockEntry(nonce);
env.storage().persistent().set(&key, &entry);
Expand Down Expand Up @@ -626,17 +625,13 @@ impl PaymentRouter {
env.storage().instance().set(&DataKey::FeeCap, &fee_cap);
}
ActionType::SetFeeBps(new_fee_bps) => {
env.storage()
.instance()
.set(&DataKey::FeeBps, &new_fee_bps);
env.storage().instance().set(&DataKey::FeeBps, &new_fee_bps);
}
ActionType::SetGovernance(gov) => {
env.storage().instance().set(&DataKey::Governance, &gov);
}
ActionType::SetMinLimit(min_limit) => {
env.storage()
.instance()
.set(&DataKey::MinLimit, &min_limit);
env.storage().instance().set(&DataKey::MinLimit, &min_limit);
}
ActionType::TransferAdmin(new_admin) => {
env.storage().instance().set(&DataKey::Admin, &new_admin);
Expand All @@ -651,10 +646,8 @@ impl PaymentRouter {
Self::INSTANCE_BUMP_AMOUNT,
);

env.events().publish(
(Symbol::new(&env, "action_executed"), admin),
nonce,
);
env.events()
.publish((Symbol::new(&env, "action_executed"), admin), nonce);

log!(&env, "Timelock action executed for nonce {}", nonce);
Ok(())
Expand All @@ -678,10 +671,8 @@ impl PaymentRouter {

env.storage().persistent().remove(&key);

env.events().publish(
(Symbol::new(&env, "action_cancelled"), admin),
nonce,
);
env.events()
.publish((Symbol::new(&env, "action_cancelled"), admin), nonce);

log!(&env, "Timelock action cancelled for nonce {}", nonce);
Ok(())
Expand Down
96 changes: 96 additions & 0 deletions stellar-payment-platform/tests/e2e/admin.e2e.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
'use strict';

process.env.ADMIN_API_KEY = 'e2e-admin-key';

jest.mock('../../src/cleanup-cron', () => ({ scheduleCleanupJob: jest.fn() }));
jest.mock('../../src/soft-delete-purge-cron', () => ({ scheduleSoftDeletePurgeJob: jest.fn() }));
jest.mock('@stellar/stellar-sdk', () => ({
Horizon: { Server: jest.fn() },
StrKey: { isValidEd25519PublicKey: jest.fn(() => true) },
}));
jest.mock('pdfkit', () => jest.fn());

const mockDbUsers = new Map();

jest.mock('../../prismaClient', () => {
const prisma = {
user: {
findFirst: jest.fn(async ({ where }) => {
let row = null;
if (where.address) {
for (const entry of mockDbUsers.values()) {
if (entry.address === where.address) {
row = entry;
break;
}
}
}
return row ? { ...row } : null;
}),
update: jest.fn(async ({ where, data }) => {
const entry = mockDbUsers.get(where.address);
if (!entry) throw new Error('Not found');
const updated = { ...entry, ...data };
mockDbUsers.set(where.address, updated);
return updated;
}),
findMany: jest.fn(async () => {
return Array.from(mockDbUsers.values());
}),
count: jest.fn(async () => {
return mockDbUsers.size;
})
},
$transaction: jest.fn(async (ops) => Promise.all(ops)),
$disconnect: jest.fn().mockResolvedValue(undefined),
};
return { prisma, isPrismaConnectionError: () => false };
});

const request = require('supertest');
const { app } = require('../../server');

describe('E2E: Admin Flow', () => {
beforeEach(() => {
mockDbUsers.clear();
mockDbUsers.set('GABC123XYZ456789ADMIN', {
username: 'admin_test_user',
address: 'GABC123XYZ456789ADMIN',
flaggedAt: null,
createdAt: new Date(),
});
});

it('should allow admin to block a user and export users', async () => {
// 1. Block User
let res = await request(app)
.post('/api/v1/admin/block')
.set('x-api-key', 'e2e-admin-key')
.send({ address: 'GABC123XYZ456789ADMIN' });

expect(res.status).toBe(200);
expect(res.body.address).toBe('GABC123XYZ456789ADMIN');
expect(res.body.flaggedAt).toBeDefined();

// 2. Export Users
res = await request(app)
.get('/api/v1/admin/export?format=json')
.set('x-api-key', 'e2e-admin-key');

expect(res.status).toBe(200);
expect(res.body.data).toBeDefined();
expect(Array.isArray(res.body.data)).toBe(true);
expect(res.body.data.length).toBe(1);
expect(res.body.data[0].address).toBe('GABC123XYZ456789ADMIN');
expect(res.body.data[0].flaggedAt).not.toBeNull();
});

it('should return 401 for unauthorized admin access', async () => {
const res = await request(app)
.post('/api/v1/admin/block')
.set('x-api-key', 'wrong-key')
.send({ address: 'GABC123XYZ456789ADMIN' });

expect(res.status).toBe(401);
});
});
102 changes: 102 additions & 0 deletions stellar-payment-platform/tests/e2e/federation.e2e.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
'use strict';

jest.mock('../../src/cleanup-cron', () => ({ scheduleCleanupJob: jest.fn() }));
jest.mock('../../src/soft-delete-purge-cron', () => ({ scheduleSoftDeletePurgeJob: jest.fn() }));
jest.mock('@stellar/stellar-sdk', () => ({
Horizon: { Server: jest.fn() },
StrKey: { isValidEd25519PublicKey: jest.fn(() => true) },
}));
jest.mock('pdfkit', () => jest.fn());

const mockDb = new Map();

jest.mock('../../prismaClient', () => {
const prisma = {
user: {
findFirst: jest.fn(async ({ where }) => {
let row = null;
if (where.username) {
const u = typeof where.username === 'string' ? where.username : where.username.equals;
for (const entry of mockDb.values()) {
if (entry.username === u) {
row = entry;
break;
}
}
} else if (where.address) {
const a = typeof where.address === 'string' ? where.address : where.address.equals;
for (const entry of mockDb.values()) {
if (entry.address.toLowerCase() === a.toLowerCase()) {
row = entry;
break;
}
}
}
return row ? { ...row } : null;
}),
create: jest.fn(async ({ data }) => {
const row = {
username: data.username,
address: data.address,
memoType: data.memoType || null,
memo: data.memo || null,
createdAt: new Date(),
};
mockDb.set(data.address, row);
return row;
}),
},
$transaction: jest.fn(async (ops) => Promise.all(ops)),
$disconnect: jest.fn().mockResolvedValue(undefined),
};
return { prisma, isPrismaConnectionError: () => false };
});

jest.mock('../../src/multisigner-verifier', () => ({
verifyMultiSignerThreshold: jest.fn().mockResolvedValue({ success: true }),
isSingleSignerAccount: jest.fn().mockReturnValue(true),
}));

jest.mock('bad-words', () => {
return jest.fn().mockImplementation(() => ({
isProfane: jest.fn(() => false),
}));
});

const request = require('supertest');
const { app } = require('../../server');
const { prisma } = require('../../prismaClient');

describe('E2E: Federation Flow', () => {
beforeEach(() => {
mockDb.clear();
});

it('should successfully lookup a user by name and ID', async () => {
const validUser = {
username: 'federation_test*localhost',
address: 'GABC123XYZ456789FEDERATION',
};
mockDb.set(validUser.address, validUser);

// 1. Lookup by Name (default behavior)
let res = await request(app)
.get(`/api/v1/federation?q=${encodeURIComponent(validUser.username)}&type=name`);

expect(res.status).toBe(200);
expect(res.body.account_id).toBe(validUser.address);

// 2. Lookup by ID
res = await request(app)
.get(`/api/v1/federation?q=${validUser.address}&type=id`);

expect(res.status).toBe(200);
expect(res.body.stellar_address).toBe(validUser.username);

// 3. Not Found
res = await request(app)
.get(`/api/v1/federation?q=nonexistent*localhost&type=name`);

expect(res.status).toBe(404);
});
});
Loading