Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

First pass at DAP resolution #3

Merged
merged 3 commits into from
Jun 25, 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
165 changes: 158 additions & 7 deletions lib/src/dap_resolver.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import 'package:dap/dap.dart';
import 'dart:convert';

import 'package:dap/src/dap.dart';
import 'package:dap/src/registration_request.dart';
import 'package:dap/src/money_address.dart';
import 'package:http/http.dart' as http;
import 'package:web5/web5.dart';
import 'package:collection/collection.dart';

class DapResolver {
http.Client client;
Expand All @@ -9,11 +14,157 @@ class DapResolver {
DapResolver(this.didResolver, {http.Client? client})
: client = client ?? http.Client();

Future<void> getMoneyAddresses(Dap dap) async {}
Future<List<MoneyAddress>> getMoneyAddresses(Dap dap) async {
throw UnimplementedError();
}

Future<DapResolutionResult> resolve(Dap dap) async {
final registryDidResolution = await didResolver.resolveDid(dap.registryDid);
if (registryDidResolution.hasError()) {
throw DapResolutionException(
'Failed to resolve registry DID: ${registryDidResolution.didResolutionMetadata.error}');
}

final dapRegistryService = registryDidResolution.didDocument?.service
?.firstWhereOrNull((service) => service.type == 'DAPRegistry');

if (dapRegistryService == null) {
throw DapResolutionException(
'Registry DID does not have a DAPRegistry service');
}

final serviceEndpoint = dapRegistryService.serviceEndpoint.firstOrNull;

if (serviceEndpoint == null) {
throw DapResolutionException(
'DAPRegistry service does not have a service endpoint');
}

Uri registryEndpoint;

try {
registryEndpoint = Uri.parse(serviceEndpoint);
} on FormatException {
throw DapResolutionException(
'Invalid service endpoint in DAPRegistry service');
}

final dereferenceUrl =
registryEndpoint.replace(path: '/daps/${dap.handle}');

http.Response dereferenceResponse;
try {
dereferenceResponse = await client.get(dereferenceUrl);
} on Exception catch (e) {
throw DapResolutionException('Failed to dereference DAP handle: $e');
}

if (dereferenceResponse.statusCode != 200) {
throw DapResolutionException(
'Failed to dereference DAP handle: (${dereferenceResponse.statusCode}) ${dereferenceResponse.body}');
}

DereferencedHandle dereferencedDap;
try {
dereferencedDap = DereferencedHandle.fromJson(dereferenceResponse.body);
} on FormatException catch (e) {
throw DapResolutionException(
'Failed to parse dereferenced DAP handle: $e');
}

final dapDidResolution =
await didResolver.resolveDid(dereferencedDap.did.uri);

if (dapDidResolution.hasError()) {
throw DapResolutionException(
'Failed to resolve DAP DID: ${dapDidResolution.didResolutionMetadata.error}');
}

final dapDidServices = dapDidResolution.didDocument?.service ?? [];
List<MoneyAddress> moneyAddresses = [];

for (var service in dapDidServices) {
if (service.type == 'MoneyAddress') {
try {
var address = MoneyAddress.parse(service.serviceEndpoint.first);
moneyAddresses.add(address);
} catch (e) {
print(
'Error parsing MoneyAddress for service: ${service.serviceEndpoint.first}, error: $e');
}
}
}

return DapResolutionResult(
dap: dap,
dapDid: dereferencedDap.did,
dapDidDocument: dapDidResolution.didDocument ??
DidDocument(id: dereferencedDap.did.uri),
registryDidDocument: registryDidResolution.didDocument ??
DidDocument(id: dereferencedDap.did.uri),
registryEndpoint: registryEndpoint,
moneyAddresses: moneyAddresses,
);
}
}

class DapResolutionResult {
final Dap dap;
final Did dapDid;
final DidDocument dapDidDocument;
final DidDocument registryDidDocument;
final Uri registryEndpoint;
final List<MoneyAddress> moneyAddresses;

DapResolutionResult({
required this.dap,
required this.dapDid,
required this.dapDidDocument,
required this.registryDidDocument,
required this.registryEndpoint,
required this.moneyAddresses,
});
}

class DapResolutionException implements Exception {
final String message;

DapResolutionException(this.message);

@override
String toString() {
return "DapResolutionException: $message";
}
}

/// Response from dereferencing a DAP handle from a DAP registry.
/// More info [here](https://github.com/TBD54566975/dap?tab=readme-ov-file#dap-resolution)
class DereferencedHandle {
final Did did;
final RegistrationRequest proof;

DereferencedHandle({required this.did, required this.proof});

// Factory constructor to create an instance from a JSON string
factory DereferencedHandle.fromJson(String source) =>
DereferencedHandle.fromMap(jsonDecode(source));

// Factory constructor to create an instance from a map
factory DereferencedHandle.fromMap(Map<String, dynamic> map) {
return DereferencedHandle(
did: Did.parse(map['did']),
proof: RegistrationRequest.fromMap(map['proof']),
);
}

// Converts the instance into a map
Map<String, dynamic> toMap() {
return {
'did': did.uri,
'proof': proof.toMap(),
};
}

/// can construct yourself for mocking purposes etc.
/// getRegistryEndpoint() -> resolve registry did, find DAPRegistry service endpoint
/// dereferenceHandle() -> makes GET request to registry endpoint to get DAP's DID
/// resolveDid() -> resolve DAP's DID, get back did document
/// getMoneyAddresses(dap) -> getRegistryEndpoint() -> dereferenceHandle() -> resolveDid() -> parse out money addresses from did document -> return list of money addrrsses
// Converts the instance to a JSON string
String toJson() => jsonEncode(toMap());
}
47 changes: 47 additions & 0 deletions lib/src/registration_request.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import 'dart:convert';

class RegistrationRequest {
final String id;
final String handle;
final String did;
final String domain;
final String signature;

RegistrationRequest({
required this.id,
required this.handle,
required this.did,
required this.domain,
required this.signature,
});

factory RegistrationRequest.fromJson(String source) =>
RegistrationRequest.fromMap(jsonDecode(source));

factory RegistrationRequest.fromMap(Map<String, dynamic> map) {
return RegistrationRequest(
id: map['id'],
handle: map['handle'],
did: map['did'],
domain: map['domain'],
signature: map['signature'],
);
}

// Converts the instance into a map
Map<String, dynamic> toMap() {
return {
'id': id,
'handle': handle,
'did': did,
'domain': domain,
'signature': signature,
};
}

// Converts the instance to a JSON string
String toJson() => jsonEncode(toMap());

@override
String toString() => toJson();
}
1 change: 1 addition & 0 deletions pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ environment:

# Add regular dependencies here.
dependencies:
collection: ^1.18.0
http: ^1.2.0
typeid: ^1.0.1
web5: ^0.2.0
Expand Down
24 changes: 24 additions & 0 deletions test/dap_resolver_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import 'package:dap/dap.dart';
import 'package:dap/src/dap_resolver.dart';
import 'package:test/test.dart';
import 'package:web5/web5.dart';

void main() {
group('DapResolver.resolve', () {
test('should resolve DAP', () async {
final dap = Dap.parse('@moegrammer/didpay.me');

// TODO: use mocks
final resolver = DapResolver(DidResolver(
methodResolvers: [DidWebResolver()],
));

final result = await resolver.resolve(dap);

expect(result.dap.handle, 'moegrammer');
expect(result.dap.domain, 'didpay.me');
expect(result.dap.registryDid, 'did:web:didpay.me');
expect(result.moneyAddresses.length, 1);
});
});
}
Loading