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

Commit

Permalink
feat: add cancel message (#48)
Browse files Browse the repository at this point in the history
  • Loading branch information
ethanwlee authored Jul 16, 2024
1 parent ae4c59d commit 65fa4bf
Show file tree
Hide file tree
Showing 13 changed files with 220 additions and 13 deletions.
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));
});
});
}

0 comments on commit 65fa4bf

Please sign in to comment.