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

feat: add cancel message #48

Merged
merged 4 commits into from
Jul 16, 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
14 changes: 14 additions & 0 deletions lib/src/protocol/json_schemas/cancel_schema.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
class CancelSchema {
static const String json = r'''
{
"$schema": "http://json-schema.org/draft-07/schema#",
"$id": "https://tbdex.dev/cancel.schema.json",
"type": "object",
"additionalProperties": false,
"properties": {
"reason": {
"type": "string"
}
}
}''';
}
2 changes: 1 addition & 1 deletion lib/src/protocol/json_schemas/message_schema.dart
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ class MessageSchema {
},
"kind": {
"type": "string",
"enum": ["rfq", "quote", "order", "orderstatus", "close"],
"enum": ["rfq", "quote", "order", "orderstatus", "close", "cancel"],
"description": "The message kind (e.g. rfq, quote)"
},
"id": {
Expand Down
21 changes: 20 additions & 1 deletion lib/src/protocol/json_schemas/offering_schema.dart
Original file line number Diff line number Diff line change
Expand Up @@ -144,13 +144,32 @@ class OfferingSchema {
"requiredClaims": {
"type": "object",
"description": "PresentationDefinition that describes the credential(s) the PFI requires in order to provide a quote."
},
"cancellation": {
"type": "object",
"properties": {
"enabled": {
"type": "boolean",
"description": "Whether cancellation is enabled for this offering"
},
"termsUrl": {
"type": "string",
"description": "A link to a page that describes the terms of cancellation"
},
"terms": {
"type": "string",
"description": "A human-readable description of the terms of cancellation in plaintext"
}
},
"required": ["enabled"]
}
},
"required": [
"description",
"payin",
"payout",
"payoutUnitsPerPayinUnit"
"payoutUnitsPerPayinUnit",
"cancellation"
]
}
''';
Expand Down
26 changes: 21 additions & 5 deletions lib/src/protocol/json_schemas/orderstatus_schema.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,30 @@ class OrderstatusSchema {
"$schema": "http://json-schema.org/draft-07/schema#",
"$id": "https://tbdex.dev/orderstatus.schema.json",
"type": "object",
"required": [
"orderStatus"
],
"additionalProperties": false,
"properties": {
"orderStatus": {
"status": {
"type":"string",
"enum": [
"PAYIN_PENDING",
"PAYIN_INITIATED",
"PAYIN_SETTLED",
"PAYIN_FAILED",
"PAYIN_EXPIRED",
"PAYOUT_PENDING",
"PAYOUT_INITIATED",
"PAYOUT_SETTLED",
"PAYOUT_FAILED",
"REFUND_PENDING",
"REFUND_INITIATED",
"REFUND_SETTLED",
"REFUND_FAILED"
]
},
"details": {
"type":"string"
}
}
},
"required": ["status"]
}''';
}
18 changes: 13 additions & 5 deletions lib/src/protocol/json_schemas/quote_schema.dart
Original file line number Diff line number Diff line change
Expand Up @@ -12,19 +12,23 @@ class QuoteSchema {
"type": "string",
"description": "ISO 4217 currency code string"
},
"amount": {
"subtotal": {
"$ref": "definitions.json#/definitions/decimalString",
"description": "The amount of currency expressed in the smallest respective unit"
"description": "The amount of currency paid for the exchange, excluding fees"
},
"fee": {
"$ref": "definitions.json#/definitions/decimalString",
"description": "The amount paid in fees"
"description": "The amount of currency paid in fees"
},
"total": {
"$ref": "definitions.json#/definitions/decimalString",
"description": "The total amount of currency to be paid in or paid out. It is always a sum of subtotal and fee"
},
"paymentInstruction": {
"$ref": "#/definitions/PaymentInstruction"
}
},
"required": ["currencyCode", "amount"]
"required": ["currencyCode", "subtotal", "total"]
},
"PaymentInstruction": {
"type": "object",
Expand All @@ -48,14 +52,18 @@ class QuoteSchema {
"type": "string",
"description": "When this quote expires. Expressed as ISO8601"
},
"payoutUnitsPerPayinUnit": {
"type": "string",
"description": "The exchange rate to convert from payin currency to payout currency. Expressed as an unrounded decimal string."
},
"payin": {
"$ref": "#/definitions/QuoteDetails"
},
"payout": {
"$ref": "#/definitions/QuoteDetails"
}
},
"required": ["expiresAt", "payin", "payout"]
"required": ["expiresAt", "payoutUnitsPerPayinUnit", "payin", "payout"]
}
''';
}
69 changes: 69 additions & 0 deletions lib/src/protocol/models/cancel.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import 'package:tbdex/src/protocol/models/message.dart';
import 'package:tbdex/src/protocol/models/message_data.dart';
import 'package:tbdex/src/protocol/parser.dart';

class Cancel extends Message {
@override
final MessageMetadata metadata;
@override
final CancelData data;

@override
Set<MessageKind> get validNext => {};

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

static Cancel create(
String to,
String from,
String exchangeId,
CancelData data, {
String? externalId,
String protocol = '1.0',
}) {
final now = DateTime.now().toUtc().toIso8601String();
final metadata = MessageMetadata(
kind: MessageKind.cancel,
to: to,
from: from,
id: Message.generateId(MessageKind.cancel),
exchangeId: exchangeId,
createdAt: now,
protocol: protocol,
externalId: externalId,
);

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

static Future<Cancel> parse(String rawMessage) async {
final cancel = Parser.parseMessage(rawMessage) as Cancel;
await cancel.verify();
return cancel;
}

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

Map<String, dynamic> toJson() {
return {
'metadata': metadata.toJson(),
'data': data.toJson(),
'signature': signature,
};
}
}
1 change: 1 addition & 0 deletions lib/src/protocol/models/message.dart
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ enum MessageKind {
rfq,
quote,
close,
cancel,
order,
orderstatus,
}
Expand Down
21 changes: 21 additions & 0 deletions lib/src/protocol/models/message_data.dart
Original file line number Diff line number Diff line change
Expand Up @@ -292,6 +292,27 @@ class CloseData extends MessageData {
}
}

class CancelData extends MessageData {
final String? reason;

CancelData({
this.reason,
});

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

@override
Map<String, dynamic> toJson() {
return {
if (reason != null) 'reason': reason,
};
}
}

class OrderData extends MessageData {
@override
Map<String, dynamic> toJson() {
Expand Down
3 changes: 3 additions & 0 deletions lib/src/protocol/parser.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,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/cancel.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 @@ -140,6 +141,8 @@ abstract class Parser {
return Quote.fromJson(jsonObject);
case MessageKind.close:
return Close.fromJson(jsonObject);
case MessageKind.cancel:
return Cancel.fromJson(jsonObject);
case MessageKind.order:
return Order.fromJson(jsonObject);
case MessageKind.orderstatus:
Expand Down
6 changes: 6 additions & 0 deletions lib/src/protocol/validator.dart
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import 'package:tbdex/src/protocol/json_schemas/quote_schema.dart';
import 'package:tbdex/src/protocol/json_schemas/resource_schema.dart';
import 'package:tbdex/src/protocol/json_schemas/rfq_private_schema.dart';
import 'package:tbdex/src/protocol/json_schemas/rfq_schema.dart';
import 'package:tbdex/src/protocol/models/cancel.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 @@ -83,6 +84,11 @@ class Validator {
_instance._validate(close.toJson(), 'message');
_instance._validate(close.data.toJson(), close.metadata.kind.name);
break;
case MessageKind.cancel:
final cancel = message as Cancel;
_instance._validate(cancel.toJson(), 'message');
_instance._validate(cancel.data.toJson(), cancel.metadata.kind.name);
break;
case MessageKind.order:
final order = message as Order;
_instance._validate(order.toJson(), 'message');
Expand Down
10 changes: 10 additions & 0 deletions test/helpers/test_data.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ 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/cancel.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 @@ -200,6 +201,15 @@ class TestData {
);
}

static Cancel getCancel({String? to}) {
return Cancel.create(
to ?? pfiDid.uri,
aliceDid.uri,
TypeId.generate(MessageKind.cancel.name),
CancelData(reason: 'reason'),
);
}

static String getOfferingResponse() {
final offering = TestData.getOffering();
final mockOfferings = [offering];
Expand Down
40 changes: 40 additions & 0 deletions test/protocol/models/cancel_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import 'dart:convert';

import 'package:tbdex/src/protocol/models/cancel.dart';
import 'package:tbdex/src/protocol/models/message.dart';
import 'package:tbdex/src/protocol/models/message_data.dart';
import 'package:tbdex/tbdex.dart';
import 'package:test/test.dart';
import 'package:typeid/typeid.dart';

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

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

group('Cancel', () {
test('can create a new cancel', () {
final cancel = Cancel.create(
TestData.pfi,
TestData.alice,
TypeId.generate(MessageKind.cancel.name),
CancelData(reason: 'my reason'),
);

expect(cancel.metadata.id, startsWith(MessageKind.cancel.name));
expect(cancel.metadata.kind, equals(MessageKind.cancel));
expect(cancel.metadata.protocol, equals('1.0'));
expect(cancel.data.reason, equals('my reason'));
});

test('can parse and verify cancel from a json string', () async {
final cancel = TestData.getCancel();
await cancel.sign(TestData.aliceDid);
final json = jsonEncode(cancel.toJson());
final parsed = await Cancel.parse(json);

expect(parsed, isA<Cancel>());
expect(parsed.toString(), equals(json));
});
});
}
Loading