Skip to content
This repository has been archived by the owner on Dec 12, 2024. It is now read-only.

feat: add Balance #43

Merged
merged 6 commits into from
Jun 10, 2024
Merged
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
41 changes: 41 additions & 0 deletions lib/src/http_client/tbdex_http_client.dart
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import 'package:tbdex/src/http_client/models/exchange.dart';
import 'package:tbdex/src/http_client/models/get_offerings_filter.dart';
import 'package:tbdex/src/http_client/models/submit_close_request.dart';
import 'package:tbdex/src/http_client/models/submit_order_request.dart';
import 'package:tbdex/src/protocol/models/balance.dart';
import 'package:tbdex/src/protocol/models/close.dart';
import 'package:tbdex/src/protocol/models/offering.dart';
import 'package:tbdex/src/protocol/models/order.dart';
Expand Down Expand Up @@ -162,6 +163,46 @@ class TbdexHttpClient {
return offerings;
}

static Future<List<Balance>> listBalances(
String pfiDid,
) async {
final pfiServiceEndpoint = await _getPfiServiceEndpoint(pfiDid);
final url = Uri.parse('$pfiServiceEndpoint/balances/');

http.Response response;
try {
response = await _client.get(url);

if (response.statusCode != 200) {
throw ResponseError(
message: 'failed to list balances',
status: response.statusCode,
body: response.body,
);
}
} on Exception catch (e) {
if (e is ResponseError) rethrow;

throw RequestError(
message: 'failed to send list balances request',
url: url.toString(),
cause: e,
);
}

List<Balance> balances;
try {
balances = Parser.parseBalances(response.body);
} on Exception catch (e) {
throw ValidationError(
message: 'failed to parse balances',
cause: e,
);
}

return balances;
}

static Future<void> createExchange(
Rfq rfq, {
String? replyTo,
Expand Down
60 changes: 60 additions & 0 deletions lib/src/protocol/models/balance.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import 'package:tbdex/tbdex.dart';

class Balance extends Resource {
@override
final ResourceMetadata metadata;
@override
final BalanceData data;

Balance._({
required this.metadata,
required this.data,
String? signature,
}) : super() {
this.signature = signature;
}

static Balance create(
String from,
BalanceData data, {
String? externalId,
String protocol = '1.0',
}) {
final now = DateTime.now().toUtc().toIso8601String();
final metadata = ResourceMetadata(
kind: ResourceKind.balance,
from: from,
id: Resource.generateId(ResourceKind.balance),
protocol: protocol,
createdAt: now,
updatedAt: now,
);

return Balance._(
metadata: metadata,
data: data,
);
}

static Future<Balance> parse(String rawResource) async {
final balance = Parser.parseResource(rawResource) as Balance;
await balance.verify();
return balance;
}

factory Balance.fromJson(Map<String, dynamic> json) {
return Balance._(
metadata: ResourceMetadata.fromJson(json['metadata']),
data: BalanceData.fromJson(json['data']),
signature: json['signature'],
);
}

Map<String, dynamic> toJson() {
return {
'metadata': metadata.toJson(),
'data': data.toJson(),
'signature': signature,
};
}
}
25 changes: 25 additions & 0 deletions lib/src/protocol/models/resource_data.dart
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,31 @@ abstract class Data {

abstract class ResourceData extends Data {}

class BalanceData extends ResourceData {
final String currencyCode;
final String available;

BalanceData({
required this.currencyCode,
required this.available,
});

factory BalanceData.fromJson(Map<String, dynamic> json) {
return BalanceData(
currencyCode: json['currencyCode'],
available: json['available'],
);
}

@override
Map<String, dynamic> toJson() {
return {
'currencyCode': currencyCode,
'available': available,
};
}
}

class OfferingData extends ResourceData {
final String description;
final String payoutUnitsPerPayinUnit;
Expand Down
24 changes: 24 additions & 0 deletions lib/src/protocol/parser.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import 'dart:convert';

import 'package:tbdex/src/http_client/models/exchange.dart';
import 'package:tbdex/src/protocol/exceptions.dart';
import 'package:tbdex/src/protocol/models/balance.dart';
import 'package:tbdex/src/protocol/models/close.dart';
import 'package:tbdex/src/protocol/models/message.dart';
import 'package:tbdex/src/protocol/models/offering.dart';
Expand Down Expand Up @@ -100,6 +101,28 @@ abstract class Parser {
return parsedOfferings;
}

static List<Balance> parseBalances(String rawBalances) {
final jsonObject = jsonDecode(rawBalances);

if (jsonObject is! Map<String, dynamic>) {
throw Exception('balances must be a json object');
}

final balances = jsonObject['data'];

if (balances is! List<dynamic> || balances.isEmpty) {
throw Exception('balances data is malformed or empty');
}

final parsedBalances = <Balance>[];
for (final balanceJson in balances) {
final balance = _parseResourceJson(balanceJson) as Balance;
parsedBalances.add(balance);
}

return parsedBalances;
}

static Message _parseMessageJson(Map<String, dynamic> jsonObject) {
final messageKind = _getKindFromJson(jsonObject);
final matchedKind = MessageKind.values.firstWhere(
Expand Down Expand Up @@ -138,6 +161,7 @@ abstract class Parser {
case ResourceKind.offering:
return Offering.fromJson(jsonObject);
case ResourceKind.balance:
return Balance.fromJson(jsonObject);
case ResourceKind.reputation:
throw UnimplementedError();
}
Expand Down
17 changes: 17 additions & 0 deletions test/helpers/test_data.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import 'package:json_schema/json_schema.dart';
import 'package:tbdex/src/http_client/models/create_exchange_request.dart';
import 'package:tbdex/src/http_client/models/submit_close_request.dart';
import 'package:tbdex/src/http_client/models/submit_order_request.dart';
import 'package:tbdex/src/protocol/models/balance.dart';
import 'package:tbdex/src/protocol/models/close.dart';
import 'package:tbdex/src/protocol/models/message.dart';
import 'package:tbdex/src/protocol/models/message_data.dart';
Expand Down Expand Up @@ -66,6 +67,16 @@ class TestData {
);
}

static Balance getBalance() {
return Balance.create(
pfiDid.uri,
BalanceData(
currencyCode: 'USD',
available: '100.00',
),
);
}

static PresentationDefinition getRequiredClaims() {
// From web5-spec test vectors
const json = r'''
Expand Down Expand Up @@ -192,6 +203,12 @@ class TestData {
return jsonEncode({'data': mockOfferings.map((e) => e.toJson()).toList()});
}

static String listBalancesResponse() {
final balance = TestData.getBalance();
final mockBalances = [balance];
return jsonEncode({'data': mockBalances.map((e) => e.toJson()).toList()});
}

static String getExchangeResponse() {
final offering = TestData.getOffering();
final rfq = TestData.getRfq(offeringId: offering.metadata.id);
Expand Down
30 changes: 29 additions & 1 deletion test/http_client/tbdex_http_client_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,35 @@ void main() async {
);

expect(
() async => await TbdexHttpClient.listOfferings(pfiDid),
() async => TbdexHttpClient.listOfferings(pfiDid),
throwsA(isA<ResponseError>()),
);
});

test('can list balances', () async {
when(
() => mockHttpClient.get(Uri.parse('$pfiServiceEndpoint/balances/')),
).thenAnswer(
(_) async => http.Response(TestData.listBalancesResponse(), 200),
);

final response = await TbdexHttpClient.listBalances(pfiDid);
expect(response.length, 1);

verify(
() => mockHttpClient.get(Uri.parse('$pfiServiceEndpoint/balances/')),
).called(1);
});

test('list balances throws ResponseError', () async {
when(
() => mockHttpClient.get(Uri.parse('$pfiServiceEndpoint/balances/')),
).thenAnswer(
(_) async => http.Response('Error', 400),
);

expect(
() async => TbdexHttpClient.listBalances(pfiDid),
throwsA(isA<ResponseError>()),
);
});
Expand Down
39 changes: 39 additions & 0 deletions test/protocol/models/balance_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import 'dart:convert';

import 'package:tbdex/src/protocol/models/balance.dart';
import 'package:tbdex/tbdex.dart';
import 'package:test/test.dart';

import '../../helpers/test_data.dart';

void main() async {
await TestData.initializeDids();

group('Balance', () {
test('can create a new balance', () {
final offering = Balance.create(
TestData.pfi,
BalanceData(
currencyCode: 'USD',
available: '100.00',
),
);

expect(offering.metadata.id, startsWith(ResourceKind.balance.name));
expect(offering.metadata.kind, equals(ResourceKind.balance));
expect(offering.metadata.protocol, equals('1.0'));
expect(offering.data.currencyCode, equals('USD'));
expect(offering.data.available, equals('100.00'));
});

test('can parse and verify balance from a json string', () async {
final balance = TestData.getBalance();
await balance.sign(TestData.pfiDid);
final json = jsonEncode(balance.toJson());
final parsed = await Balance.parse(json);

expect(parsed, isA<Balance>());
expect(parsed.toString(), equals(json));
});
});
}
17 changes: 17 additions & 0 deletions test/protocol/parser_test.dart
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import 'dart:convert';

import 'package:tbdex/src/protocol/models/balance.dart';
import 'package:tbdex/src/protocol/models/message.dart';
import 'package:tbdex/src/protocol/models/offering.dart';
import 'package:tbdex/src/protocol/models/quote.dart';
Expand Down Expand Up @@ -82,6 +83,22 @@ void main() async {
expect(offerings.last.toString(), equals(offeringJson));
});

test('can parse a list of balances', () async {
final balance = TestData.getBalance();
final balanceJson = jsonEncode(balance.toJson());

final balances = Parser.parseBalances(
jsonEncode({
'data': [balance, balance],
}),
);

expect(balances.first, isA<Balance>());
expect(balances.last, isA<Balance>());
expect(balances.first.toString(), equals(balanceJson));
expect(balances.last.toString(), equals(balanceJson));
});

test('parse throws error if json is null', () {
expect(
() => Parser.parseMessage(jsonEncode(null)),
Expand Down
Loading