Skip to content

Commit 47b2844

Browse files
authored
🚀 Implement v3 call (callSync) (#85)
dfinity/agent-js#906
2 parents 545410b + 4935f54 commit 47b2844

24 files changed

+202
-132
lines changed

CHANGELOG.md

+6
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,12 @@ that can be found in the LICENSE file. -->
44

55
# Changelog
66

7+
## 1.0.0-dev.28
8+
9+
- Implements v3 synchronized call API in agent and actor.
10+
- `pollForResponse` can override the certificate result.
11+
- v3 calls will return to v2 if 202/404 status is returned.
12+
713
## 1.0.0-dev.27
814

915
- Support `flutter_rust_bridge` 2.5.

packages/agent_dart/pubspec.yaml

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
name: agent_dart
2-
version: 1.0.0-dev.27
2+
version: 1.0.0-dev.28
33

44
description: |
55
An agent library built for Internet Computer,

packages/agent_dart_base/lib/agent/actor.dart

+28-2
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,14 @@
11
import 'dart:convert';
22
import 'dart:typed_data';
33

4+
import 'package:typed_data/typed_data.dart';
5+
46
import '../candid/idl.dart';
57
import '../principal/principal.dart';
68
import 'agent/api.dart';
9+
import 'agent/http/types.dart';
710
import 'canisters/management.dart';
11+
import 'cbor.dart' as cbor;
812
import 'errors.dart';
913
import 'polling/polling.dart';
1014
import 'request_id.dart';
@@ -72,6 +76,7 @@ class CallConfig {
7276
this.pollingStrategyFactory,
7377
this.canisterId,
7478
this.effectiveCanisterId,
79+
this.callSync = true,
7580
});
7681

7782
factory CallConfig.fromJson(Map<String, dynamic> map) {
@@ -80,6 +85,7 @@ class CallConfig {
8085
pollingStrategyFactory: map['pollingStrategyFactory'],
8186
canisterId: map['canisterId'],
8287
effectiveCanisterId: map['effectiveCanisterId'],
88+
callSync: map['callSync'] ?? true,
8389
);
8490
}
8591

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

106+
/// Whether to call the endpoint synchronously.
107+
final bool callSync;
108+
100109
Map<String, dynamic> toJson() {
101110
return {
102111
'agent': agent,
103112
'pollingStrategyFactory': pollingStrategyFactory,
104113
'canisterId': canisterId,
105114
'effectiveCanisterId': effectiveCanisterId,
115+
'callSync': callSync,
106116
};
107117
}
108118
}
@@ -114,6 +124,7 @@ class ActorConfig extends CallConfig {
114124
super.pollingStrategyFactory,
115125
super.canisterId,
116126
super.effectiveCanisterId,
127+
super.callSync,
117128
this.callTransform,
118129
this.queryTransform,
119130
});
@@ -126,6 +137,7 @@ class ActorConfig extends CallConfig {
126137
pollingStrategyFactory: map['pollingStrategyFactory'],
127138
canisterId: map['canisterId'],
128139
effectiveCanisterId: map['effectiveCanisterId'],
140+
callSync: map['callSync'] ?? true,
129141
);
130142
}
131143

@@ -411,13 +423,15 @@ ActorMethod _createActorMethod(Actor actor, String methodName, Func func) {
411423
final ecid = effectiveCanisterId != null
412424
? Principal.from(effectiveCanisterId)
413425
: cid;
414-
// final { requestId, response } =
415-
final result = await agent!.call(
426+
final callSync = actor.metadata.config?.callSync ?? newOptions.callSync;
427+
428+
final result = await agent!.callRequest(
416429
cid,
417430
CallOptions(
418431
methodName: methodName,
419432
arg: arg,
420433
effectiveCanisterId: ecid,
434+
callSync: callSync,
421435
),
422436
null,
423437
);
@@ -428,13 +442,25 @@ ActorMethod _createActorMethod(Actor actor, String methodName, Func func) {
428442
throw UpdateCallRejectedError(cid, methodName, result, requestId);
429443
}
430444

445+
BinaryBlob? certificate;
446+
// Fall back to polling if we receive an "Accepted" response code,
447+
// otherwise decode the certificate instantly.
448+
if (result is CallResponseBody && result.response?.status != 202) {
449+
final buffer = (result.response as HttpResponseBody).arrayBuffer!;
450+
final decoded = cbor.cborDecode<Map>(buffer);
451+
certificate = blobFromBuffer(
452+
(decoded['certificate'] as Uint8Buffer).buffer,
453+
);
454+
}
455+
431456
final pollStrategy = pollingStrategyFactory();
432457
final responseBytes = await pollForResponse(
433458
agent,
434459
ecid,
435460
requestId,
436461
pollStrategy,
437462
methodName,
463+
overrideCertificate: certificate,
438464
);
439465

440466
if (responseBytes.isNotEmpty) {

packages/agent_dart_base/lib/agent/agent/api.dart

+5-1
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@ class CallOptions {
9898
required this.methodName,
9999
required this.arg,
100100
this.effectiveCanisterId,
101+
this.callSync = true,
101102
});
102103

103104
/// The method name to call.
@@ -109,6 +110,9 @@ class CallOptions {
109110
/// An effective canister ID, used for routing. This should only be mentioned
110111
/// if it's different from the canister ID.
111112
final Principal? effectiveCanisterId;
113+
114+
/// Whether to call the endpoint synchronously.
115+
final bool callSync;
112116
}
113117

114118
@immutable
@@ -157,7 +161,7 @@ abstract class Agent {
157161
Identity? identity,
158162
);
159163

160-
Future<SubmitResponse> call(
164+
Future<SubmitResponse> callRequest(
161165
Principal canisterId,
162166
CallOptions fields,
163167
Identity? identity,

packages/agent_dart_base/lib/agent/agent/http/index.dart

+31-9
Original file line numberDiff line numberDiff line change
@@ -210,7 +210,7 @@ class HttpAgent implements Agent {
210210
}
211211

212212
@override
213-
Future<SubmitResponse> call(
213+
Future<CallResponseBody> callRequest(
214214
Principal canisterId,
215215
CallOptions fields,
216216
Identity? identity,
@@ -220,6 +220,7 @@ class HttpAgent implements Agent {
220220
final ecid = fields.effectiveCanisterId != null
221221
? Principal.from(fields.effectiveCanisterId)
222222
: canister;
223+
final callSync = fields.callSync;
223224
final sender = id != null ? id.getPrincipal() : Principal.anonymous();
224225

225226
final CallRequest submit = CallRequest(
@@ -241,17 +242,38 @@ class HttpAgent implements Agent {
241242
body: submit,
242243
);
243244
final transformedRequest = await _transform(rsRequest);
244-
245245
final newTransformed = await id!.transformRequest(transformedRequest);
246246
final body = cbor.cborEncode(newTransformed['body']);
247-
final response = await withRetry(
248-
() => _fetch!(
249-
endpoint: '/api/v2/canister/${ecid.toText()}/call',
247+
248+
Future<Map<String, dynamic>> callV3() {
249+
return _fetch!(
250+
endpoint: '/api/v3/canister/${ecid.toText()}/call',
250251
method: FetchMethod.post,
251252
headers: newTransformed['request']['headers'],
252253
body: body,
253-
),
254-
);
254+
);
255+
}
256+
257+
Future<Map<String, dynamic>> callV2() {
258+
return withRetry(
259+
() => _fetch!(
260+
endpoint: '/api/v2/canister/${ecid.toText()}/call',
261+
method: FetchMethod.post,
262+
headers: newTransformed['request']['headers'],
263+
body: body,
264+
),
265+
);
266+
}
267+
268+
Map<String, dynamic> response;
269+
if (callSync) {
270+
response = await callV3();
271+
if (response['statusCode'] == 404) {
272+
response = await callV2();
273+
}
274+
} else {
275+
response = await callV2();
276+
}
255277
final requestId = requestIdOf(submit.toJson());
256278

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

390412
final buffer = response['arrayBuffer'] as Uint8List;
391-
413+
final decoded = cbor.cborDecode<Map>(buffer);
392414
return ReadStateResponseResult(
393415
certificate: blobFromBuffer(
394-
(cbor.cborDecode<Map>(buffer)['certificate'] as Uint8Buffer).buffer,
416+
(decoded['certificate'] as Uint8Buffer).buffer,
395417
),
396418
);
397419
}

packages/agent_dart_base/lib/agent/agent/proxy.dart

+4-4
Original file line numberDiff line numberDiff line change
@@ -280,7 +280,7 @@ class ProxyStubAgent {
280280
final void Function(ProxyMessage msg) _frontend;
281281
final Agent _agent;
282282

283-
void onmessage(ProxyMessage msg) {
283+
void onMessage(ProxyMessage msg) {
284284
switch (msg.type) {
285285
case ProxyMessageKind.getPrincipal:
286286
_agent.getPrincipal().then((response) {
@@ -305,7 +305,7 @@ class ProxyStubAgent {
305305
});
306306
break;
307307
case ProxyMessageKind.call:
308-
_agent.call(msg.args?[0], msg.args?[1], msg.args?[2]).then((response) {
308+
_agent.callRequest(msg.args?[0], msg.args?[1], msg.args?[2]).then((response) {
309309
_frontend(
310310
ProxyMessageCallResponse.fromJson({
311311
'id': msg.id,
@@ -356,7 +356,7 @@ class ProxyAgent implements Agent {
356356
@override
357357
BinaryBlob? rootKey;
358358

359-
void onmessage(ProxyMessage msg) {
359+
void onMessage(ProxyMessage msg) {
360360
final id = msg.id;
361361

362362
final maybePromise = _pendingCalls[id];
@@ -417,7 +417,7 @@ class ProxyAgent implements Agent {
417417
}
418418

419419
@override
420-
Future<SubmitResponse> call(
420+
Future<SubmitResponse> callRequest(
421421
Principal canisterId,
422422
CallOptions fields,
423423
Identity? identity,

packages/agent_dart_base/lib/agent/certificate.dart

+6-4
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,9 @@ class Cert {
5252
factory Cert.fromJson(Map json) {
5353
return Cert(
5454
delegation: json['delegation'] != null
55-
? CertDelegation.fromJson(Map<String, dynamic>.from(json['delegation']))
55+
? CertDelegation.fromJson(
56+
Map<String, dynamic>.from(json['delegation']),
57+
)
5658
: null,
5759
signature: json['signature'] != null
5860
? (json['signature'] as Uint8Buffer).buffer.asUint8List()
@@ -123,9 +125,9 @@ class CertDelegation extends ReadStateResponse {
123125

124126
class Certificate {
125127
Certificate(
126-
ReadStateResponse response,
128+
BinaryBlob certificate,
127129
this._agent,
128-
) : cert = Cert.fromJson(cborDecode(response.certificate));
130+
) : cert = Cert.fromJson(cborDecode(certificate));
129131

130132
final Agent _agent;
131133
final Cert cert;
@@ -172,7 +174,7 @@ class Certificate {
172174
}
173175
return Future.value(_rootKey);
174176
}
175-
final Certificate cert = Certificate(d, _agent);
177+
final Certificate cert = Certificate(d.certificate, _agent);
176178
if (!(await cert.verify())) {
177179
throw StateError('Fail to verify certificate.');
178180
}

packages/agent_dart_base/lib/agent/polling/polling.dart

+17-8
Original file line numberDiff line numberDiff line change
@@ -14,25 +14,33 @@ Future<BinaryBlob> pollForResponse(
1414
Principal canisterId,
1515
RequestId requestId,
1616
PollStrategy strategy,
17-
String method,
18-
) async {
17+
String method, {
18+
BinaryBlob? overrideCertificate,
19+
}) async {
1920
final Principal? caller;
2021
if (agent is HttpAgent) {
2122
caller = agent.identity?.getPrincipal();
2223
} else {
2324
caller = null;
2425
}
26+
2527
final path = [blobFromText('request_status'), requestId];
26-
final state = await agent.readState(
27-
canisterId,
28-
ReadStateOptions(paths: [path]),
29-
null,
30-
);
31-
final cert = Certificate(state, agent);
28+
final Certificate cert;
29+
if (overrideCertificate != null) {
30+
cert = Certificate(overrideCertificate, agent);
31+
} else {
32+
final state = await agent.readState(
33+
canisterId,
34+
ReadStateOptions(paths: [path]),
35+
null,
36+
);
37+
cert = Certificate(state.certificate, agent);
38+
}
3239
final verified = await cert.verify();
3340
if (!verified) {
3441
throw StateError('Fail to verify certificate.');
3542
}
43+
3644
final maybeBuf = cert.lookup([...path, blobFromText('status').buffer]);
3745
final RequestStatusResponseStatus status;
3846
if (maybeBuf == null) {
@@ -50,6 +58,7 @@ Future<BinaryBlob> pollForResponse(
5058
case RequestStatusResponseStatus.processing:
5159
// Execute the polling strategy, then retry.
5260
await strategy(canisterId, requestId, status);
61+
// Passing the override certificate will cause infinite stacks.
5362
return pollForResponse(agent, canisterId, requestId, strategy, method);
5463
case RequestStatusResponseStatus.rejected:
5564
final rejectCode = cert.lookup(

packages/agent_dart_base/pubspec.yaml

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
name: agent_dart_base
2-
version: 1.0.0-dev.27
2+
version: 1.0.0-dev.28
33

44
description: The Dart plugin that bridges Rust implementation for agent_dart.
55
repository: https://github.com/AstroxNetwork/agent_dart
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,38 @@
1+
import 'package:agent_dart_base/agent_dart_base.dart';
2+
import 'package:test/test.dart';
3+
14
void main() {
25
actorTest();
36
}
47

58
void actorTest() {
6-
/// skip, see https://github.com/dfinity/agent-js/blob/main/packages/agent/src/actor.test.ts
9+
test('actor', () async {
10+
final agent = HttpAgent(
11+
defaultHost: 'icp-api.io',
12+
defaultPort: 443,
13+
options: const HttpAgentOptions(identity: AnonymousIdentity()),
14+
);
15+
final idl = IDL.Service({
16+
'create_challenge': IDL.Func(
17+
[],
18+
[
19+
IDL.Record({
20+
'png_base64': IDL.Text,
21+
'challenge_key': IDL.Text,
22+
}),
23+
],
24+
[],
25+
),
26+
});
27+
final actor = CanisterActor(
28+
ActorConfig(
29+
canisterId: Principal.fromText('rdmx6-jaaaa-aaaaa-aaadq-cai'),
30+
agent: agent,
31+
),
32+
idl,
33+
);
34+
final result = await actor.getFunc('create_challenge')!.call([]);
35+
expect(result, isA<Map>());
36+
expect(result['challenge_key'], isA<String>());
37+
});
738
}

0 commit comments

Comments
 (0)