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

feat(gclient): introduce gear general client #4016

Open
wants to merge 25 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
d00da46
feat(gclient): init the initial arch of the general client
clearloop Jun 17, 2024
640a890
chore(gclient): mock the the backend trait
clearloop Jun 20, 2024
582f99d
chore(gclient): try impl Backend for gtest
clearloop Jun 24, 2024
19b5b1a
feat(gclient): implement Backend for gclient
clearloop Jul 11, 2024
7376b16
feat(client): introduce TxResult
clearloop Jul 11, 2024
98d1438
Merge branch 'master' into cl/issue-3895
clearloop Jul 18, 2024
d5b0918
chore(gclient): get back gtest backend
clearloop Jul 18, 2024
242df80
chore(gclient): local thread for gtest
clearloop Jul 24, 2024
cfa12b5
feat(gclient): complete gtest for the integration
clearloop Jul 24, 2024
1906c29
feat(gclient): sugar methods for clients
clearloop Jul 24, 2024
7f1482f
feat(gclient): tokio runtime for gtest
clearloop Jul 24, 2024
627eb22
feat(gclient): capture block events
clearloop Jul 25, 2024
98d4229
feat(gclient): timeout handler for messages in gclient
clearloop Jul 25, 2024
f8d83ce
Merge branch 'master' into cl/issue-3895
clearloop Jul 25, 2024
4c24f24
Merge branch 'master' into cl/issue-3895
clearloop Jul 25, 2024
8249663
chore(gclient): address comments
clearloop Aug 6, 2024
db8d3f9
Merge branch 'master' into cl/issue-3895
clearloop Aug 6, 2024
bd6f184
feat(gclient): export Client instance only in general client
clearloop Aug 7, 2024
c4db2e9
chore(gclient): patch docs
clearloop Aug 8, 2024
d7d8515
chore(gclient): temp transfer
clearloop Aug 27, 2024
3056e7e
feat(gclient): introduce transfer for the client
clearloop Aug 27, 2024
f151b09
Merge branch 'master' into cl/issue-3895
clearloop Aug 28, 2024
a6eda16
feat(gnc): introduce gear next client
clearloop Aug 29, 2024
d4e93c6
chore(clippy): make clippy happy
clearloop Aug 29, 2024
128b3e3
Merge branch 'master' into cl/issue-3895
clearloop Aug 29, 2024
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
17 changes: 17 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ members = [
"utils/runtime-fuzzer/fuzz",
"utils/lazy-pages-fuzzer/fuzz",
"ethexe/*",
"ethexe/runtime/common",
"ethexe/runtime/common", "gnc",
]

[workspace.dependencies]
Expand Down
1 change: 1 addition & 0 deletions gclient/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ gsdk = { workspace = true, features = ["testing"] }
gear-node-wrapper.workspace = true
gear-core.workspace = true
gear-core-errors.workspace = true
gprimitives.workspace = true

futures.workspace = true
anyhow.workspace = true
Expand Down
26 changes: 23 additions & 3 deletions gclient/src/api/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ use gear_node_wrapper::{Node, NodeInstance};
use gsdk::{
ext::{sp_core::sr25519, sp_runtime::AccountId32},
signer::Signer,
Api,
Api, Blocks,
};
use std::{ffi::OsStr, sync::Arc};

Expand Down Expand Up @@ -127,8 +127,16 @@ impl GearApi {
/// Create an [`EventListener`] to subscribe and handle continuously
/// incoming events.
pub async fn subscribe(&self) -> Result<EventListener> {
let events = self.0.api().subscribe_finalized_blocks().await?;
Ok(EventListener(events))
Ok(EventListener(self.subscribe_blocks().await?))
}

/// Subscribe new blocks with events without [`EventListener`] as wrapper.
pub async fn subscribe_blocks(&self) -> Result<Blocks> {
self.0
.api()
.subscribe_finalized_blocks()
.await
.map_err(Into::into)
}

/// Set the number used once (`nonce`) that will be used while sending
Expand Down Expand Up @@ -174,6 +182,18 @@ impl GearApi {
pub fn account_id(&self) -> &AccountId32 {
self.0.account_id()
}

/// Change the singer of `GearApi`, see also [`GearApi::init_with`].
pub fn change_signer(&mut self, suri: impl AsRef<str>) -> Result<()> {
let mut suri = suri.as_ref().splitn(2, ':');
let new_signer = self
.0
.clone()
.change(suri.next().expect("Infallible"), suri.next())?;
self.0 = new_signer;

Ok(())
}
}

impl From<Signer> for GearApi {
Expand Down
1 change: 1 addition & 0 deletions gclient/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,7 @@
#![warn(missing_docs)]
#![doc(html_logo_url = "https://docs.gear.rs/logo.svg")]
#![doc(html_favicon_url = "https://gear-tech.io/favicons/favicon.ico")]
#![allow(async_fn_in_trait)]

mod api;
mod utils;
Expand Down
11 changes: 11 additions & 0 deletions gclient/src/ws.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ use anyhow::anyhow;
use std::{
fmt,
net::{AddrParseError, SocketAddrV4},
str::FromStr,
};
use url::Url;

Expand Down Expand Up @@ -184,6 +185,16 @@ impl From<SocketAddrV4> for WSAddress {
}
}

impl FromStr for WSAddress {
type Err = Error;

fn from_str(s: &str) -> Result<Self, Self::Err> {
s.parse()
.map_err(|e: AddrParseError| Error::Anyhow(e.into()))
.map(|addr: SocketAddrV4| addr.into())
}
}

impl TryInto<SocketAddrV4> for WSAddress {
type Error = Error;

Expand Down
24 changes: 24 additions & 0 deletions gnc/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
[package]
name = "gnc"
description = "Gear next client"
version.workspace = true
authors.workspace = true
edition.workspace = true
license.workspace = true
homepage.workspace = true
repository.workspace = true
rust-version.workspace = true

[dependencies]
gclient.workspace = true
gtest.workspace = true
tokio.workspace = true
tracing.workspace = true
gear-core.workspace = true
gprimitives.workspace = true
anyhow.workspace = true
parity-scale-codec.workspace = true
gsdk.workspace = true

[dev-dependencies]
demo-proxy = { workspace = true, features = ["std"] }
227 changes: 227 additions & 0 deletions gnc/src/backend/gclient.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,227 @@
// This file is part of Gear.

// Copyright (C) 2022-2024 Gear Technologies Inc.
// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0

// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.

// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.

// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.

use crate::{Backend, Code, Message, Program, TxResult, ALICE};
use anyhow::{anyhow, Result};
use gclient::{Event, GearApi, GearEvent};
use gear_core::{
ids::ProgramId,
message::{UserMessage, UserStoredMessage},
};
use gprimitives::{ActorId, MessageId, H256};
use gsdk::{
ext::sp_core::{sr25519, Pair},
metadata::runtime_types::gear_common::storage::primitives::Interval,
Blocks,
};
use std::{
collections::{BTreeMap, HashMap},
sync::Arc,
time::{Duration, SystemTime},
};
use tokio::sync::{Mutex, MutexGuard};

const MESSAGES_DEPTH: usize = 16;
const DEFAULT_TIMEOUT: u64 = 3000;

/// GClient instance
#[derive(Clone)]
pub struct GClient {
inner: Arc<Mutex<GearApi>>,
pairs: HashMap<ActorId, String>,
messages: Arc<Mutex<BTreeMap<H256, Vec<UserMessage>>>>,
timeout: Duration,
}

impl GClient {
/// Create new gclient instance
pub async fn new(api: GearApi) -> Result<Self> {
let messages = Arc::new(Mutex::new(BTreeMap::new()));
Self::spawn(api.subscribe_blocks().await?, messages.clone());

Ok(Self {
inner: Arc::new(Mutex::new(api)),
pairs: HashMap::from_iter(vec![(ALICE, "//Alice".to_string())].into_iter()),
timeout: Duration::from_millis(DEFAULT_TIMEOUT),
messages,
})
}

/// Switch to the provided pair
async fn switch_pair(&self, address: ActorId) -> Result<()> {
let pair = self
.pairs
.get(&address)
.ok_or(anyhow!("Could not find pair {address}"))?;

self.inner.lock().await.change_signer(pair)?;
Ok(())
}

/// Get [`GearApi`]
async fn api(&self) -> MutexGuard<'_, GearApi> {
self.inner.lock().await
}

/// Get user sent messages by block hash
async fn logs(&self, hash: H256) -> Result<Vec<UserMessage>> {
let now = SystemTime::now();
loop {
if now.elapsed()? > self.timeout {
return Ok(vec![]);
}

if let Some(messages) = self.messages.lock().await.remove(&hash) {
return Ok(messages);
}
}
}

/// Spawn gear messages
fn spawn(mut sub: Blocks, gmessages: Arc<Mutex<BTreeMap<H256, Vec<UserMessage>>>>) {
tokio::spawn(async move {
while let Ok(Some(bevs)) = sub.next_events().await {
let messages = bevs
.events()
.unwrap_or_default()
.into_iter()
.filter_map(|e| {
if let Event::Gear(GearEvent::UserMessageSent { message, .. }) = e {
Some(message.into())
} else {
None
}
})
.collect::<Vec<_>>();

if messages.is_empty() {
continue;
}

let mut map = gmessages.lock().await;
while map.len() > MESSAGES_DEPTH {
map.pop_first();
}

map.insert(bevs.block_hash(), messages);
}
});
}
}

impl Backend for GClient {
async fn program(&self, id: ProgramId) -> Result<Program<Self>> {
let _ = self.inner.lock().await.program_at(id, None).await?;

Ok(Program {
id,
backend: self.clone(),
})
}

async fn deploy<M>(&self, code: impl Code, message: M) -> Result<TxResult<Program<Self>>>
where
M: Into<Message> + Send,
{
let wasm = code.bytes()?;
let message = message.into();
self.switch_pair(message.signer).await?;

let api = self.api().await;
let (_, id, _) = api
.upload_program_bytes(
wasm,
message.salt,
message.payload,
message.gas_limit.unwrap_or(api.block_gas_limit()?),
message.value,
)
.await?;

Ok(TxResult {
result: Program {
id,
backend: self.clone(),
},
logs: vec![],
})
}

async fn send<M>(&self, id: ProgramId, message: M) -> Result<TxResult<MessageId>>
where
M: Into<Message> + Send,
{
let message = message.into();
self.switch_pair(message.signer).await?;

let api = self.api().await;
let (mid, hash) = api
.send_message_bytes(
id,
message.payload,
message.gas_limit.unwrap_or(api.block_gas_limit()?),
message.value,
)
.await?;

Ok(TxResult {
result: mid,
logs: self.logs(hash).await?,
})
}

async fn message(&self, mid: MessageId) -> Result<Option<(UserStoredMessage, Interval<u32>)>> {
self.inner
.lock()
.await
.get_mailbox_message(mid)
.await
.map_err(Into::into)
}

async fn transfer(&self, to: ActorId, value: u128) -> Result<TxResult<H256>> {
let hash = self
.inner
.lock()
.await
.transfer_keep_alive(to, value)
.await?;

Ok(TxResult {
result: hash,
logs: self.logs(hash).await?,
})
}

fn add_pair(&mut self, suri: impl AsRef<str>) -> Result<()> {
let mut patt = suri.as_ref().splitn(2, ':');
let pair = sr25519::Pair::from_string(
patt.next()
.ok_or(anyhow!("Invalid suri, failed to add pair"))?,
patt.next(),
)?;
self.pairs
.insert(pair.public().0.into(), suri.as_ref().to_string());

Ok(())
}

fn timeout(&mut self, timeout: Duration) {
self.timeout = timeout
}
}
Loading
Loading