Skip to content

Commit

Permalink
Merge pull request #309 from tonlabs/1.5.2-rc
Browse files Browse the repository at this point in the history
Version 1.5.2
  • Loading branch information
d3p authored Dec 30, 2020
2 parents e16d682 + c4a6188 commit 44d7c39
Show file tree
Hide file tree
Showing 11 changed files with 238 additions and 31 deletions.
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,15 @@
# Release Notes
All notable changes to this project will be documented in this file.


## 1.5.2 Dec 30, 2020

### Fixed
- `net` module functions wait for `net.resume` call instead of returning error if called while the module is suspended

### Documentation
- How to work with `Application Objects` [specification](docs/app_objects.md) added

## 1.5.1 Dec 28, 2020

### Fixed
Expand Down
160 changes: 160 additions & 0 deletions docs/app_objects.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
# How to work with Application Objects in binding generators

Binding generator must detect functions that accept application objects.

Application object interaction protocol and functions that accept app objects have the following signatures in core:

```rust
/// Methods of the Foo interface with parameters
enum ParamsOfAppFoo {
CallWithParamsAndResult { a: String, b: String },
CallWithParams { a: String, b: String },
CallWithResult,
CallWithoutParamsAndResult,
NotifyWithParams { a: String, b: String },
NotifyWithoutParams
}

/// Method results of the Foo interface
enum ResultOfAppFoo {
CallWithParamsAndResult { c: String },
CallWithParams,
CallWithResult { c: String },
CallWithoutParamsAndResult,
}

/// API function that accepts an application object as a parameter
async fn foo(
context: Arc<ClientContext>,
params: ParamsOfFoo,
obj: AppObject<ParamsOfAppFoo, ResultOfAppFoo>,
) -> ClientResult<()> {}
```

It means when a function accepts an application-implemented object, from that point library will have access to the methods of this object.

### How to detect

Function contains parameter with generic type `AppObject<foo_module.ParamsOfAppFoo, foo_module.ResultOfAppFoo>`.

### Generated code

Generator must produce:

- interface declaration;
- interface dispatcher;
- function that accept application object.

### Interface declaration

```tsx
export type ParamsOfAppFooCallWithParamsAndResult {
a: string,
b: string,
}

export type ResultOfAppFooCallWithParamsAndResult {
c: string,
}

export type ParamsOfAppFooCallWithParams {
a: string,
b: string,
}

export type ResultOfAppFooCallWithResult {
c: string,
}

export type ParamsOfAppFooNotifyWithParams {
a: string,
b: string,
}

export interface AppFoo {
call_with_params_and_result(params: ParamsOfFooWithParamsAndResult): Promise<ResultOfFooWithParamsAndResult>,
call_with_params(params: ParamsOfFooWithParams): Promise<void>,
call_with_result(): Promise<ResultOfFooWithResult>,
notify_with_params(params: ParamsoOfFooNotifyWithParams),
notify_without_params(),
}
```

- Interface `Foo` is extracted from the name of the first generic arg of the `AppObject<foo_module.ParamsOfAppFoo, foo_module.ResultOfAppFoo>`. Note that a generic arg name is a fully qualified name so you must remove the module name first. In the example above the first arg name is `foo_module.ParamsOfAppFoo.` After removing module name we have `ParamsOfAppFoo`. Then we must remove the prefix `ParamsOf`. The rest of the name contains the interface name `Foo`.
- To collect a list of interface methods we must collect variant names from enum `foo_module.ParamsOfAppFoo`. Each variant of `ParamsOfAppFoo` represents the interface method. Respectively each variant of `ResultOfAppFoo` represents the result of the interface method. If interface method has no `ResultOfAppFoo` then such method is a notify method – no waiting for the response is needed.
- The name of the interface method is constructed from the name of the variant by using a simple rule `PascalStyleName``snake_style_name`. So the variant `CallWithParamsAndResult` will be converted to `call_with_params_and_result`.
- If a function has a result then this function must return `Promise` and perform asynchronous execution.

### Interface dispatcher

The implementation of the wrapper method is more difficult than regular. It must pass dispatching `responseHandler` to the library. Library will call this handler every time when it requires to call the application object. Two response types are used for calling application objects: `3` for calling methods which return result and `4` for notifiyng without awaiting any result. When response type `4` is passed, data contains enum `ParamsOfAppFoo`. When `3` is passed, data contains struct `ParamsOfAppRequest` where `request_data` field contains `ParamsOfAppFoo`

```tsx
type ParamsOfAppRequest {
app_request_id: number,
request_data: any,
}
```

Generator must define special dispatch helper for application object invocation:

```tsx
async function dispatchFoo(obj: Foo, params: ParamsOfAppFoo, app_request_id: number | null, client: TonClient) {
try {
let result = undefined;
switch (params.type) {
case 'CallWithParamsAndResult':
result = await obj.call_with_params_and_result(params);
break;
case 'CallWithParams':
await obj.call_with_params(params);
break;
case 'CallWithResult':
result = await obj.call_with_result();
break;
case 'CallWithoutParamsAndResult':
await obj.call_with_result();
break;
case 'NotifyWithParams':
obj.notify_with_params(params);
break;
case 'NotifyWithoutParams':
obj.notify_without_params();
break;
}
if (app_request_id) {
client.resolve_app_request({ app_request_id, result: { type: 'Ok', result: { type: params.type, ...result }}});
}
}
catch (error) {
if (app_request_id) {
client.resolve_app_request({ app_request_id, result: { type: 'Error', text: error.message }});
}
}
}

```

### Functions with application object

The `obj` parameter must be declared instead of the source `obj: AppObject<ParamsOfAppFoo, ResultOfAppFoo>`.

Wrapper implementation with dispatcher must be generated as:

```tsx
type ParamsOfFoo {
...
}

export class AppFoo {
foo(params: ParamsOfFoo, obj: Foo): Promise<ResultOfFoo> {
return this.client.request('foo', params, (params, responseType) => {
if (responseType === 3) {
dispatchFoo(obj, params.request_data, params.app_request_id, this);
} else if (responseType === 4) {
dispatchFoo(obj, params, null, this);
}
}
}
}
```
6 changes: 5 additions & 1 deletion docs/json_interface.md
Original file line number Diff line number Diff line change
Expand Up @@ -292,4 +292,8 @@ type ParamsOfAppRequest {

Here `request_data` is some data describing the request and `app_request_id` is ID of the request, which should be used for request result resolving. After the request is processed application should call `client.resolve_app_request` function passing `app_request_id` used in the request and result of processing.

In case if response type is `4`, `params_json` contains serialized notification data without any wrappers. Application processes notification in the way it needs. No response is needed for SDK.
In case if response type is `4`, `params_json` contains serialized notification data without any wrappers. Application processes notification in the way it needs. No response is needed for SDK.

### How to work with Application Objects in binding generators

Find out how to work with Application Objects in binding generators in this [specification](app_objects.md).
3 changes: 2 additions & 1 deletion docs/mod_processing.md
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,8 @@ timeout: SDK recreates the message, sends it and processes it again.
The intermediate events, such as `WillFetchFirstBlock`, `WillSend`, `DidSend`,
`WillFetchNextBlock`, etc - are switched on/off by `send_events` flag
and logged into the supplied callback function.
The retry configuration parameters are defined in client's `NetworkConfig`.

The retry configuration parameters are defined in the client's `NetworkConfig` and `AbiConfig`.

If contract's ABI does not include "expire" header
then, if no transaction is found within the network timeout (see config parameter ), exits with error.
Expand Down
2 changes: 1 addition & 1 deletion ton_client/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "ton_client"
version = "1.5.1"
version = "1.5.2"
authors = ["Michael Vlasov"]
edition = "2018"
license = "Apache-2.0"
Expand Down
22 changes: 9 additions & 13 deletions ton_client/src/net/server_link.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,9 @@ use crate::net::websocket_link::WebsocketLink;
use crate::net::{Error, NetworkConfig};
use futures::{Future, Stream, StreamExt};
use serde_json::Value;
use tokio::sync::watch;
use std::collections::HashMap;
use std::pin::Pin;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, Mutex};

pub const MAX_TIMEOUT: u32 = std::i32::MAX as u32;
Expand All @@ -36,10 +36,8 @@ pub(crate) struct ServerLink {
config: NetworkConfig,
endpoints: tokio::sync::RwLock<Vec<String>>,
client_env: Arc<ClientEnv>,
suspended: AtomicBool,
suspended: (watch::Sender<bool>, watch::Receiver<bool>),
server_info: tokio::sync::RwLock<Option<ServerInfo>>,
// TODO: use tokio::sync:RwLock when SDK core is fully async
query_url: std::sync::RwLock<Option<String>>,
websocket_link: WebsocketLink,
}

Expand All @@ -58,8 +56,7 @@ impl ServerLink {
config: config.clone(),
endpoints: tokio::sync::RwLock::new(endpoints),
client_env: client_env.clone(),
suspended: AtomicBool::new(false),
query_url: std::sync::RwLock::new(None),
suspended: watch::channel(false),
server_info: tokio::sync::RwLock::new(None),
websocket_link: WebsocketLink::new(client_env.clone()),
})
Expand Down Expand Up @@ -136,9 +133,9 @@ impl ServerLink {
}

async fn ensure_info(&self) -> ClientResult<()> {
if self.suspended.load(Ordering::Relaxed) {
return Err(Error::network_module_suspended());
}
// wait for resume
let mut suspended = self.suspended.1.clone();
while Some(true) == suspended.recv().await {}

if self.server_info.read().await.is_some() {
return Ok(());
Expand All @@ -159,7 +156,6 @@ impl ServerLink {
})
.await;

*self.query_url.write().unwrap() = Some(inited_data.query_url.clone());
*data = Some(inited_data);

Ok(())
Expand Down Expand Up @@ -308,7 +304,7 @@ impl ServerLink {
let client_lock = self.server_info.read().await;
let address = &client_lock.as_ref().unwrap().query_url;

let result = self.fetch_operation(address, query, timeout).await?;
let result = self.fetch_operation(address, query, None).await?;

// try to extract the record value from the answer
let records_array = &result["data"][&table];
Expand Down Expand Up @@ -391,12 +387,12 @@ impl ServerLink {
}

pub async fn suspend(&self) {
self.suspended.store(true, Ordering::Relaxed);
let _ = self.suspended.0.broadcast(true);
self.websocket_link.suspend().await;
}

pub async fn resume(&self) {
self.suspended.store(false, Ordering::Relaxed);
let _ = self.suspended.0.broadcast(false);
self.websocket_link.resume().await;
}

Expand Down
51 changes: 40 additions & 11 deletions ton_client/src/net/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -367,23 +367,52 @@ async fn find_last_shard_block() {
println!("{}", block.block_id);
}

#[tokio::test(core_threads = 2)]
async fn test_endpoints() {
return;
// #[tokio::test(core_threads = 2)]
// async fn test_endpoints() {
// let client = TestClient::new_with_config(json!({
// "network": {
// "endpoints": ["cinet.tonlabs.io", "cinet2.tonlabs.io/"],
// }
// }));

// let endpoints: EndpointsSet = client
// .request_async("net.fetch_endpoints", ())
// .await
// .unwrap();

// let _: () = client
// .request_async("net.set_endpoints", endpoints)
// .await
// .unwrap();
// }

let client = TestClient::new_with_config(json!({
"network": {
"endpoints": ["cinet.tonlabs.io", "cinet2.tonlabs.io/"],
}
}));
#[tokio::test(core_threads = 2)]
async fn test_wait_resume() {
let client = std::sync::Arc::new(TestClient::new());
let client_copy = client.clone();

let endpoints: EndpointsSet = client
.request_async("net.fetch_endpoints", ())
let _: () = client
.request_async("net.suspend", ())
.await
.unwrap();

let start = std::time::Instant::now();

let duration = tokio::spawn(async move {
client_copy
.fetch_account(&TestClient::network_giver())
.await;

start.elapsed().as_millis()
});

let timeout = 5000;
tokio::time::delay_for(tokio::time::Duration::from_millis(timeout)).await;

let _: () = client
.request_async("net.set_endpoints", endpoints)
.request_async("net.resume", ())
.await
.unwrap();

assert!(duration.await.unwrap() > timeout as u128);
}
8 changes: 8 additions & 0 deletions ton_client/src/tests/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -263,6 +263,14 @@ impl TestClient {
"0:2bb4a0e8391e7ea8877f4825064924bd41ce110fce97e939d3323999e1efbb13".into()
}

pub fn network_giver() -> String {
if Self::node_se() {
Self::giver_address()
} else {
Self::wallet_address()
}
}

pub fn wallet_keys() -> Option<KeyPair> {
if Self::node_se() {
return None;
Expand Down
2 changes: 1 addition & 1 deletion ton_sdk/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "ton_sdk"
version = "1.5.1"
version = "1.5.2"
edition = "2018"
license = "Apache-2.0"

Expand Down
2 changes: 1 addition & 1 deletion toncli/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "toncli"
version = "1.5.1"
version = "1.5.2"
description = "TON CLient Command Line Tool"
authors = ["TON DEV SOLUTIONS LTD <[email protected]>"]
repository = "https://github.com/tonlabs/TON-SDK"
Expand Down
4 changes: 2 additions & 2 deletions tools/api.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"version": "1.5.0",
"version": "1.5.2",
"modules": [
{
"name": "client",
Expand Down Expand Up @@ -6037,7 +6037,7 @@
{
"name": "process_message",
"summary": "Creates message, sends it to the network and monitors its processing.",
"description": "Creates ABI-compatible message,\nsends it to the network and monitors for the result transaction.\nDecodes the output messages' bodies.\n\nIf contract's ABI includes \"expire\" header, then\nSDK implements retries in case of unsuccessful message delivery within the expiration\ntimeout: SDK recreates the message, sends it and processes it again.\n\nThe intermediate events, such as `WillFetchFirstBlock`, `WillSend`, `DidSend`,\n`WillFetchNextBlock`, etc - are switched on/off by `send_events` flag\nand logged into the supplied callback function.\nThe retry configuration parameters are defined in client's `NetworkConfig`.\n\nIf contract's ABI does not include \"expire\" header\nthen, if no transaction is found within the network timeout (see config parameter ), exits with error.",
"description": "Creates ABI-compatible message,\nsends it to the network and monitors for the result transaction.\nDecodes the output messages' bodies.\n\nIf contract's ABI includes \"expire\" header, then\nSDK implements retries in case of unsuccessful message delivery within the expiration\ntimeout: SDK recreates the message, sends it and processes it again.\n\nThe intermediate events, such as `WillFetchFirstBlock`, `WillSend`, `DidSend`,\n`WillFetchNextBlock`, etc - are switched on/off by `send_events` flag\nand logged into the supplied callback function.\n\nThe retry configuration parameters are defined in the client's `NetworkConfig` and `AbiConfig`.\n\nIf contract's ABI does not include \"expire\" header\nthen, if no transaction is found within the network timeout (see config parameter ), exits with error.",
"params": [
{
"name": "context",
Expand Down

0 comments on commit 44d7c39

Please sign in to comment.