Skip to content

Commit 99a2aef

Browse files
authored
Merge pull request #515 from samwel141/master
Add Pending Transfers Summary Endpoint
2 parents f24399c + 204850a commit 99a2aef

File tree

11 files changed

+384
-3
lines changed

11 files changed

+384
-3
lines changed
Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
require('dotenv').config();
2+
const request = require('supertest');
3+
const { expect } = require('chai');
4+
const server = require('../server/app');
5+
const seed = require('./seed');
6+
7+
const { apiKey } = seed;
8+
9+
describe('Wallet: Get pending transfers summary', () => {
10+
let bearerTokenA;
11+
let bearerTokenB;
12+
13+
before(async () => {
14+
await seed.clear();
15+
await seed.seed();
16+
17+
{
18+
const res = await request(server)
19+
.post('/auth')
20+
.set('treetracker-api-key', apiKey)
21+
.send({
22+
wallet: seed.wallet.name,
23+
password: seed.wallet.password,
24+
});
25+
26+
expect(res).to.have.property('statusCode', 200);
27+
bearerTokenA = res.body.token;
28+
expect(bearerTokenA).to.match(/\S+/);
29+
}
30+
31+
{
32+
const res = await request(server)
33+
.post('/auth')
34+
.set('treetracker-api-key', apiKey)
35+
.send({
36+
wallet: seed.walletB.name,
37+
password: seed.walletB.password,
38+
});
39+
expect(res).to.have.property('statusCode', 200);
40+
bearerTokenB = res.body.token;
41+
expect(bearerTokenB).to.match(/\S+/);
42+
}
43+
});
44+
45+
it('Should get pending transfers summary with no pending transfers', async () => {
46+
const res = await request(server)
47+
.get(`/wallets/${seed.wallet.id}/pending-transfers`)
48+
.set('treetracker-api-key', apiKey)
49+
.set('content-type', 'application/json')
50+
.set('Authorization', `Bearer ${bearerTokenA}`);
51+
52+
expect(res).property('statusCode').to.eq(200);
53+
expect(res.body).to.have.property('wallet_id').eq(seed.wallet.id);
54+
expect(res.body).to.have.property('wallet_name').eq(seed.wallet.name);
55+
expect(res.body).to.have.property('pending_outgoing');
56+
expect(res.body.pending_outgoing).to.have.property('total_amount').eq(0);
57+
expect(res.body.pending_outgoing).to.have.property('count').eq(0);
58+
expect(res.body).to.have.property('pending_incoming');
59+
expect(res.body.pending_incoming).to.have.property('total_amount').eq(0);
60+
expect(res.body.pending_incoming).to.have.property('count').eq(0);
61+
expect(res.body).to.have.property('net_pending').eq(0);
62+
});
63+
64+
it('Should create a pending token transfer and check summary', async () => {
65+
const transferRes = await request(server)
66+
.post('/transfers')
67+
.set('treetracker-api-key', apiKey)
68+
.set('Authorization', `Bearer ${bearerTokenA}`)
69+
.send({
70+
tokens: [seed.token.id],
71+
sender_wallet: seed.wallet.name,
72+
receiver_wallet: seed.walletB.name,
73+
});
74+
75+
expect(transferRes).property('statusCode').to.eq(202);
76+
77+
const summaryRes = await request(server)
78+
.get(`/wallets/${seed.wallet.id}/pending-transfers`)
79+
.set('treetracker-api-key', apiKey)
80+
.set('content-type', 'application/json')
81+
.set('Authorization', `Bearer ${bearerTokenA}`);
82+
83+
expect(summaryRes).property('statusCode').to.eq(200);
84+
expect(summaryRes.body.wallet_id).eq(seed.wallet.id);
85+
expect(summaryRes.body.wallet_name).eq(seed.wallet.name);
86+
expect(summaryRes.body.pending_outgoing.total_amount).eq(1);
87+
expect(summaryRes.body.pending_outgoing.count).eq(1);
88+
expect(summaryRes.body.pending_incoming.total_amount).eq(0);
89+
expect(summaryRes.body.pending_incoming.count).eq(0);
90+
expect(summaryRes.body.net_pending).eq(-1);
91+
92+
const summaryResB = await request(server)
93+
.get(`/wallets/${seed.walletB.id}/pending-transfers`)
94+
.set('treetracker-api-key', apiKey)
95+
.set('content-type', 'application/json')
96+
.set('Authorization', `Bearer ${bearerTokenB}`);
97+
98+
expect(summaryResB).property('statusCode').to.eq(200);
99+
expect(summaryResB.body.wallet_id).eq(seed.walletB.id);
100+
expect(summaryResB.body.wallet_name).eq(seed.walletB.name);
101+
102+
expect(summaryResB.body.pending_outgoing.total_amount).eq(0);
103+
expect(summaryResB.body.pending_outgoing.count).eq(0);
104+
105+
expect(summaryResB.body.pending_incoming.total_amount).eq(1);
106+
expect(summaryResB.body.pending_incoming.count).eq(1);
107+
108+
expect(summaryResB.body.net_pending).eq(1);
109+
});
110+
111+
it('Should create a pending bundle transfer and check summary', async () => {
112+
await seed.addTokenToWallet(seed.wallet.id);
113+
await seed.addTokenToWallet(seed.wallet.id);
114+
await seed.addTokenToWallet(seed.wallet.id);
115+
await seed.addTokenToWallet(seed.wallet.id);
116+
await seed.addTokenToWallet(seed.wallet.id);
117+
118+
const transferRes = await request(server)
119+
.post('/transfers')
120+
.set('treetracker-api-key', apiKey)
121+
.set('Authorization', `Bearer ${bearerTokenA}`)
122+
.send({
123+
bundle: {
124+
bundle_size: 5,
125+
},
126+
sender_wallet: seed.wallet.name,
127+
receiver_wallet: seed.walletB.name,
128+
});
129+
130+
expect(transferRes).property('statusCode').to.eq(202);
131+
132+
const summaryRes = await request(server)
133+
.get(`/wallets/${seed.wallet.id}/pending-transfers`)
134+
.set('treetracker-api-key', apiKey)
135+
.set('content-type', 'application/json')
136+
.set('Authorization', `Bearer ${bearerTokenA}`);
137+
138+
expect(summaryRes).property('statusCode').to.eq(200);
139+
expect(summaryRes.body.pending_outgoing.count).eq(2); // 2 transfers
140+
expect(summaryRes.body.pending_outgoing.total_amount).eq(6); // 1 + 5 tokens
141+
expect(summaryRes.body.net_pending).eq(-6);
142+
});
143+
144+
it('Should return 403 for unauthorized wallet access', async () => {
145+
146+
const res = await request(server)
147+
.get(`/wallets/${seed.walletC.id}/pending-transfers`)
148+
.set('treetracker-api-key', apiKey)
149+
.set('content-type', 'application/json')
150+
.set('Authorization', `Bearer ${bearerTokenA}`);
151+
152+
expect(res).property('statusCode').to.eq(403);
153+
});
154+
155+
it('Should allow managed wallet access', async () => {
156+
157+
const res = await request(server)
158+
.get(`/wallets/${seed.walletC.id}/pending-transfers`)
159+
.set('treetracker-api-key', apiKey)
160+
.set('content-type', 'application/json')
161+
.set('Authorization', `Bearer ${bearerTokenB}`);
162+
163+
expect(res).property('statusCode').to.eq(200);
164+
expect(res.body.wallet_id).eq(seed.walletC.id);
165+
expect(res.body.wallet_name).eq(seed.walletC.name);
166+
});
167+
168+
it('Should validate wallet_id parameter', async () => {
169+
const res = await request(server)
170+
.get('/wallets/invalid-uuid/pending-transfers')
171+
.set('treetracker-api-key', apiKey)
172+
.set('content-type', 'application/json')
173+
.set('Authorization', `Bearer ${bearerTokenA}`);
174+
175+
expect(res).property('statusCode').to.eq(422);
176+
});
177+
});

docs/api/spec/treetracker-wallet-api.yaml

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -487,6 +487,51 @@ paths:
487487
code: 422
488488
message: '"wallet_id" must be a valid GUID'
489489
deprecated: false
490+
'/wallets/{wallet_uuid}/pending-transfers':
491+
get:
492+
tags:
493+
- Wallet operations
494+
operationId: get-wallets-walletId-pendingTransfers
495+
summary: Get pending transfers summary for a wallet
496+
description: 'Returns a summary of pending token transfers for the specified wallet, including both incoming and outgoing transfers with counts and total amounts.'
497+
parameters:
498+
- $ref: '#/components/parameters/treetrackerApiKeyParam'
499+
- name: wallet_uuid
500+
description: 'ID of wallet to get pending transfers summary for'
501+
in: path
502+
required: true
503+
schema:
504+
type: string
505+
format: uuid
506+
example: 496ffa8e-2fa2-488c-98e1-acf9b57c230b
507+
responses:
508+
'200':
509+
description: 'Pending transfers summary'
510+
content:
511+
application/json:
512+
schema:
513+
$ref: '#/components/schemas/pendingTransfersSummary'
514+
'401':
515+
$ref: '#/components/responses/UnauthorizedError'
516+
'403':
517+
description: 'Access denied - user does not have permission to access this wallet'
518+
content:
519+
application/json:
520+
schema:
521+
$ref: '#/components/schemas/errorResponse'
522+
example:
523+
code: 403
524+
message: 'Have no permission to access this wallet'
525+
'422':
526+
description: 'Invalid wallet UUID parameter'
527+
content:
528+
application/json:
529+
schema:
530+
$ref: '#/components/schemas/errorResponse'
531+
example:
532+
code: 422
533+
message: '"wallet_id" must be a valid GUID'
534+
deprecated: false
490535
/wallets/batch-create-wallet:
491536
post:
492537
tags:
@@ -1934,6 +1979,57 @@ components:
19341979
type: array
19351980
items:
19361981
$ref: '#/components/schemas/walletItem'
1982+
pendingTransfersSummary:
1983+
title: 'Pending Transfers Summary'
1984+
type: object
1985+
properties:
1986+
wallet_id:
1987+
type: string
1988+
format: uuid
1989+
description: 'ID of the wallet'
1990+
example: 482bf306-30c7-4cea-833a-1cdda3d96573
1991+
wallet_name:
1992+
type: string
1993+
description: 'Name of the wallet'
1994+
example: 'test_wallet'
1995+
pending_outgoing:
1996+
type: object
1997+
description: 'Summary of outgoing pending transfers'
1998+
properties:
1999+
total_amount:
2000+
type: integer
2001+
description: 'Total number of tokens in outgoing pending transfers'
2002+
example: 5
2003+
count:
2004+
type: integer
2005+
description: 'Number of outgoing pending transfers'
2006+
example: 2
2007+
pending_incoming:
2008+
type: object
2009+
description: 'Summary of incoming pending transfers'
2010+
properties:
2011+
total_amount:
2012+
type: integer
2013+
description: 'Total number of tokens in incoming pending transfers'
2014+
example: 3
2015+
count:
2016+
type: integer
2017+
description: 'Number of incoming pending transfers'
2018+
example: 1
2019+
net_pending:
2020+
type: integer
2021+
description: 'Net pending amount (incoming - outgoing)'
2022+
example: -2
2023+
example:
2024+
wallet_id: '482bf306-30c7-4cea-833a-1cdda3d96573'
2025+
wallet_name: 'test_wallet'
2026+
pending_outgoing:
2027+
total_amount: 5
2028+
count: 2
2029+
pending_incoming:
2030+
total_amount: 3
2031+
count: 1
2032+
net_pending: -2
19372033
responses:
19382034
InvalidQueryParametersLimit:
19392035
description: 'Invalid query parameters'

server/handlers/walletHandler.spec.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,8 @@ describe('walletRouter', () => {
124124
limit: 500,
125125
offset: 0,
126126
sort_by: 'created_at',
127-
order: 'desc'
127+
order: 'desc',
128+
exclude_managed: false
128129
}),
129130
).eql(true);
130131
});

server/handlers/walletHandler/index.js

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,7 @@ const walletGetTrustRelationships = async (req, res) => {
9393
sort_by,
9494
order,
9595
search,
96+
exclude_managed,
9697
} = validatedQuery;
9798

9899
const { wallets: managedWallets } = await walletService.getAllWallets(
@@ -124,11 +125,12 @@ const walletGetTrustRelationships = async (req, res) => {
124125
sort_by,
125126
order,
126127
search,
128+
exclude_managed,
127129
},
128130
);
129131
res.status(200).json({
130132
trust_relationships,
131-
query: { limit, offset, sort_by, order, state, type, request_type, search },
133+
query: { limit, offset, sort_by, order, state, type, request_type, search, exclude_managed },
132134
total,
133135
});
134136
};
@@ -235,6 +237,23 @@ const walletBatchTransfer = async (req, res) => {
235237
res.status(200).send(result);
236238
};
237239

240+
const walletGetPendingTransfersSummary = async (req, res) => {
241+
const validatedParams = await walletIdParamSchema.validateAsync(req.params, {
242+
abortEarly: false,
243+
});
244+
245+
const { wallet_id: requestedWalletId } = validatedParams;
246+
const { wallet_id: loggedInWalletId } = req;
247+
const walletService = new WalletService();
248+
249+
const summary = await walletService.getPendingTransfersSummary(
250+
loggedInWalletId,
251+
requestedWalletId,
252+
);
253+
254+
res.status(200).json(summary);
255+
};
256+
238257
module.exports = {
239258
walletPost,
240259
walletPatch,
@@ -243,4 +262,5 @@ module.exports = {
243262
walletSingleGet,
244263
walletBatchCreate,
245264
walletBatchTransfer,
265+
walletGetPendingTransfersSummary,
246266
};

server/handlers/walletHandler/schemas.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ const walletGetTrustRelationshipsSchema = Joi.object({
4141
sort_by: Joi.string().valid( 'state', 'created_at', 'updated_at').default('created_at'),
4242
order: Joi.string().valid('asc', 'desc').default('desc'),
4343
search: Joi.string().optional(),
44+
exclude_managed: Joi.boolean().default(false),
4445
});
4546

4647
const walletPostSchema = Joi.object({

server/models/Trust.js

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ class Trust {
5454
sort_by,
5555
search,
5656
order,
57+
exclude_managed,
5758
}) {
5859
const managedWalletIds = managedWallets.map((wallet) => wallet.id);
5960

@@ -93,7 +94,8 @@ class Trust {
9394
],
9495
});
9596
}
96-
return this._trustRepository.getByFilter(
97+
98+
const result = await this._trustRepository.getByFilter(
9799
filter,
98100
{
99101
offset,
@@ -104,6 +106,16 @@ class Trust {
104106
walletId,
105107
managedWalletIds,
106108
);
109+
110+
if (exclude_managed) {
111+
result.result = result.result.filter(relationship =>
112+
relationship.request_type !== 'manage' &&
113+
relationship.request_type !== 'yield'
114+
);
115+
result.count = result.result.length;
116+
}
117+
118+
return result;
107119
}
108120

109121
async getTrustRelationshipsCount({ walletId, state, type, request_type }) {

0 commit comments

Comments
 (0)