Skip to content

Commit

Permalink
🚀 Implement v3 call (callSync) (#85)
Browse files Browse the repository at this point in the history
  • Loading branch information
AlexV525 authored Oct 15, 2024
2 parents 545410b + 4935f54 commit 47b2844
Show file tree
Hide file tree
Showing 24 changed files with 202 additions and 132 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,12 @@ that can be found in the LICENSE file. -->

# Changelog

## 1.0.0-dev.28

- Implements v3 synchronized call API in agent and actor.
- `pollForResponse` can override the certificate result.
- v3 calls will return to v2 if 202/404 status is returned.

## 1.0.0-dev.27

- Support `flutter_rust_bridge` 2.5.
Expand Down
2 changes: 1 addition & 1 deletion packages/agent_dart/pubspec.yaml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
name: agent_dart
version: 1.0.0-dev.27
version: 1.0.0-dev.28

description: |
An agent library built for Internet Computer,
Expand Down
30 changes: 28 additions & 2 deletions packages/agent_dart_base/lib/agent/actor.dart
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
import 'dart:convert';
import 'dart:typed_data';

import 'package:typed_data/typed_data.dart';

import '../candid/idl.dart';
import '../principal/principal.dart';
import 'agent/api.dart';
import 'agent/http/types.dart';
import 'canisters/management.dart';
import 'cbor.dart' as cbor;
import 'errors.dart';
import 'polling/polling.dart';
import 'request_id.dart';
Expand Down Expand Up @@ -72,6 +76,7 @@ class CallConfig {
this.pollingStrategyFactory,
this.canisterId,
this.effectiveCanisterId,
this.callSync = true,
});

factory CallConfig.fromJson(Map<String, dynamic> map) {
Expand All @@ -80,6 +85,7 @@ class CallConfig {
pollingStrategyFactory: map['pollingStrategyFactory'],
canisterId: map['canisterId'],
effectiveCanisterId: map['effectiveCanisterId'],
callSync: map['callSync'] ?? true,
);
}

Expand All @@ -97,12 +103,16 @@ class CallConfig {
/// The effective canister ID. This should almost always be ignored.
final Principal? effectiveCanisterId;

/// Whether to call the endpoint synchronously.
final bool callSync;

Map<String, dynamic> toJson() {
return {
'agent': agent,
'pollingStrategyFactory': pollingStrategyFactory,
'canisterId': canisterId,
'effectiveCanisterId': effectiveCanisterId,
'callSync': callSync,
};
}
}
Expand All @@ -114,6 +124,7 @@ class ActorConfig extends CallConfig {
super.pollingStrategyFactory,
super.canisterId,
super.effectiveCanisterId,
super.callSync,
this.callTransform,
this.queryTransform,
});
Expand All @@ -126,6 +137,7 @@ class ActorConfig extends CallConfig {
pollingStrategyFactory: map['pollingStrategyFactory'],
canisterId: map['canisterId'],
effectiveCanisterId: map['effectiveCanisterId'],
callSync: map['callSync'] ?? true,
);
}

Expand Down Expand Up @@ -411,13 +423,15 @@ ActorMethod _createActorMethod(Actor actor, String methodName, Func func) {
final ecid = effectiveCanisterId != null
? Principal.from(effectiveCanisterId)
: cid;
// final { requestId, response } =
final result = await agent!.call(
final callSync = actor.metadata.config?.callSync ?? newOptions.callSync;

final result = await agent!.callRequest(
cid,
CallOptions(
methodName: methodName,
arg: arg,
effectiveCanisterId: ecid,
callSync: callSync,
),
null,
);
Expand All @@ -428,13 +442,25 @@ ActorMethod _createActorMethod(Actor actor, String methodName, Func func) {
throw UpdateCallRejectedError(cid, methodName, result, requestId);
}

BinaryBlob? certificate;
// Fall back to polling if we receive an "Accepted" response code,
// otherwise decode the certificate instantly.
if (result is CallResponseBody && result.response?.status != 202) {
final buffer = (result.response as HttpResponseBody).arrayBuffer!;
final decoded = cbor.cborDecode<Map>(buffer);
certificate = blobFromBuffer(
(decoded['certificate'] as Uint8Buffer).buffer,
);
}

final pollStrategy = pollingStrategyFactory();
final responseBytes = await pollForResponse(
agent,
ecid,
requestId,
pollStrategy,
methodName,
overrideCertificate: certificate,
);

if (responseBytes.isNotEmpty) {
Expand Down
6 changes: 5 additions & 1 deletion packages/agent_dart_base/lib/agent/agent/api.dart
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ class CallOptions {
required this.methodName,
required this.arg,
this.effectiveCanisterId,
this.callSync = true,
});

/// The method name to call.
Expand All @@ -109,6 +110,9 @@ class CallOptions {
/// An effective canister ID, used for routing. This should only be mentioned
/// if it's different from the canister ID.
final Principal? effectiveCanisterId;

/// Whether to call the endpoint synchronously.
final bool callSync;
}

@immutable
Expand Down Expand Up @@ -157,7 +161,7 @@ abstract class Agent {
Identity? identity,
);

Future<SubmitResponse> call(
Future<SubmitResponse> callRequest(
Principal canisterId,
CallOptions fields,
Identity? identity,
Expand Down
40 changes: 31 additions & 9 deletions packages/agent_dart_base/lib/agent/agent/http/index.dart
Original file line number Diff line number Diff line change
Expand Up @@ -210,7 +210,7 @@ class HttpAgent implements Agent {
}

@override
Future<SubmitResponse> call(
Future<CallResponseBody> callRequest(
Principal canisterId,
CallOptions fields,
Identity? identity,
Expand All @@ -220,6 +220,7 @@ class HttpAgent implements Agent {
final ecid = fields.effectiveCanisterId != null
? Principal.from(fields.effectiveCanisterId)
: canister;
final callSync = fields.callSync;
final sender = id != null ? id.getPrincipal() : Principal.anonymous();

final CallRequest submit = CallRequest(
Expand All @@ -241,17 +242,38 @@ class HttpAgent implements Agent {
body: submit,
);
final transformedRequest = await _transform(rsRequest);

final newTransformed = await id!.transformRequest(transformedRequest);
final body = cbor.cborEncode(newTransformed['body']);
final response = await withRetry(
() => _fetch!(
endpoint: '/api/v2/canister/${ecid.toText()}/call',

Future<Map<String, dynamic>> callV3() {
return _fetch!(
endpoint: '/api/v3/canister/${ecid.toText()}/call',
method: FetchMethod.post,
headers: newTransformed['request']['headers'],
body: body,
),
);
);
}

Future<Map<String, dynamic>> callV2() {
return withRetry(
() => _fetch!(
endpoint: '/api/v2/canister/${ecid.toText()}/call',
method: FetchMethod.post,
headers: newTransformed['request']['headers'],
body: body,
),
);
}

Map<String, dynamic> response;
if (callSync) {
response = await callV3();
if (response['statusCode'] == 404) {
response = await callV2();
}
} else {
response = await callV2();
}
final requestId = requestIdOf(submit.toJson());

if (!(response['ok'] as bool)) {
Expand Down Expand Up @@ -388,10 +410,10 @@ class HttpAgent implements Agent {
}

final buffer = response['arrayBuffer'] as Uint8List;

final decoded = cbor.cborDecode<Map>(buffer);
return ReadStateResponseResult(
certificate: blobFromBuffer(
(cbor.cborDecode<Map>(buffer)['certificate'] as Uint8Buffer).buffer,
(decoded['certificate'] as Uint8Buffer).buffer,
),
);
}
Expand Down
8 changes: 4 additions & 4 deletions packages/agent_dart_base/lib/agent/agent/proxy.dart
Original file line number Diff line number Diff line change
Expand Up @@ -280,7 +280,7 @@ class ProxyStubAgent {
final void Function(ProxyMessage msg) _frontend;
final Agent _agent;

void onmessage(ProxyMessage msg) {
void onMessage(ProxyMessage msg) {
switch (msg.type) {
case ProxyMessageKind.getPrincipal:
_agent.getPrincipal().then((response) {
Expand All @@ -305,7 +305,7 @@ class ProxyStubAgent {
});
break;
case ProxyMessageKind.call:
_agent.call(msg.args?[0], msg.args?[1], msg.args?[2]).then((response) {
_agent.callRequest(msg.args?[0], msg.args?[1], msg.args?[2]).then((response) {
_frontend(
ProxyMessageCallResponse.fromJson({
'id': msg.id,
Expand Down Expand Up @@ -356,7 +356,7 @@ class ProxyAgent implements Agent {
@override
BinaryBlob? rootKey;

void onmessage(ProxyMessage msg) {
void onMessage(ProxyMessage msg) {
final id = msg.id;

final maybePromise = _pendingCalls[id];
Expand Down Expand Up @@ -417,7 +417,7 @@ class ProxyAgent implements Agent {
}

@override
Future<SubmitResponse> call(
Future<SubmitResponse> callRequest(
Principal canisterId,
CallOptions fields,
Identity? identity,
Expand Down
10 changes: 6 additions & 4 deletions packages/agent_dart_base/lib/agent/certificate.dart
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,9 @@ class Cert {
factory Cert.fromJson(Map json) {
return Cert(
delegation: json['delegation'] != null
? CertDelegation.fromJson(Map<String, dynamic>.from(json['delegation']))
? CertDelegation.fromJson(
Map<String, dynamic>.from(json['delegation']),
)
: null,
signature: json['signature'] != null
? (json['signature'] as Uint8Buffer).buffer.asUint8List()
Expand Down Expand Up @@ -123,9 +125,9 @@ class CertDelegation extends ReadStateResponse {

class Certificate {
Certificate(
ReadStateResponse response,
BinaryBlob certificate,
this._agent,
) : cert = Cert.fromJson(cborDecode(response.certificate));
) : cert = Cert.fromJson(cborDecode(certificate));

final Agent _agent;
final Cert cert;
Expand Down Expand Up @@ -172,7 +174,7 @@ class Certificate {
}
return Future.value(_rootKey);
}
final Certificate cert = Certificate(d, _agent);
final Certificate cert = Certificate(d.certificate, _agent);
if (!(await cert.verify())) {
throw StateError('Fail to verify certificate.');
}
Expand Down
25 changes: 17 additions & 8 deletions packages/agent_dart_base/lib/agent/polling/polling.dart
Original file line number Diff line number Diff line change
Expand Up @@ -14,25 +14,33 @@ Future<BinaryBlob> pollForResponse(
Principal canisterId,
RequestId requestId,
PollStrategy strategy,
String method,
) async {
String method, {
BinaryBlob? overrideCertificate,
}) async {
final Principal? caller;
if (agent is HttpAgent) {
caller = agent.identity?.getPrincipal();
} else {
caller = null;
}

final path = [blobFromText('request_status'), requestId];
final state = await agent.readState(
canisterId,
ReadStateOptions(paths: [path]),
null,
);
final cert = Certificate(state, agent);
final Certificate cert;
if (overrideCertificate != null) {
cert = Certificate(overrideCertificate, agent);
} else {
final state = await agent.readState(
canisterId,
ReadStateOptions(paths: [path]),
null,
);
cert = Certificate(state.certificate, agent);
}
final verified = await cert.verify();
if (!verified) {
throw StateError('Fail to verify certificate.');
}

final maybeBuf = cert.lookup([...path, blobFromText('status').buffer]);
final RequestStatusResponseStatus status;
if (maybeBuf == null) {
Expand All @@ -50,6 +58,7 @@ Future<BinaryBlob> pollForResponse(
case RequestStatusResponseStatus.processing:
// Execute the polling strategy, then retry.
await strategy(canisterId, requestId, status);
// Passing the override certificate will cause infinite stacks.
return pollForResponse(agent, canisterId, requestId, strategy, method);
case RequestStatusResponseStatus.rejected:
final rejectCode = cert.lookup(
Expand Down
2 changes: 1 addition & 1 deletion packages/agent_dart_base/pubspec.yaml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
name: agent_dart_base
version: 1.0.0-dev.27
version: 1.0.0-dev.28

description: The Dart plugin that bridges Rust implementation for agent_dart.
repository: https://github.com/AstroxNetwork/agent_dart
Expand Down
33 changes: 32 additions & 1 deletion packages/agent_dart_base/test/agent/actor.dart
Original file line number Diff line number Diff line change
@@ -1,7 +1,38 @@
import 'package:agent_dart_base/agent_dart_base.dart';
import 'package:test/test.dart';

void main() {
actorTest();
}

void actorTest() {
/// skip, see https://github.com/dfinity/agent-js/blob/main/packages/agent/src/actor.test.ts
test('actor', () async {
final agent = HttpAgent(
defaultHost: 'icp-api.io',
defaultPort: 443,
options: const HttpAgentOptions(identity: AnonymousIdentity()),
);
final idl = IDL.Service({
'create_challenge': IDL.Func(
[],
[
IDL.Record({
'png_base64': IDL.Text,
'challenge_key': IDL.Text,
}),
],
[],
),
});
final actor = CanisterActor(
ActorConfig(
canisterId: Principal.fromText('rdmx6-jaaaa-aaaaa-aaadq-cai'),
agent: agent,
),
idl,
);
final result = await actor.getFunc('create_challenge')!.call([]);
expect(result, isA<Map>());
expect(result['challenge_key'], isA<String>());
});
}
Loading

0 comments on commit 47b2844

Please sign in to comment.