Skip to content

Commit

Permalink
First pass at DAP resolution (#3)
Browse files Browse the repository at this point in the history
* first pass at dap resolution

* add `collection` dep to use `firstWhereOrNull()`

* remove all force unwrapping

---------

Co-authored-by: Ethan Lee <[email protected]>
  • Loading branch information
mistermoe and ethan-tbd authored Jun 25, 2024
1 parent dbdfd35 commit 87ef17d
Show file tree
Hide file tree
Showing 4 changed files with 230 additions and 7 deletions.
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);
});
});
}

0 comments on commit 87ef17d

Please sign in to comment.