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

🚀 Implement v3 call (callSync) #85

Merged
merged 7 commits into from
Oct 15, 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
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
Loading