Skip to content

Commit 13e3bf8

Browse files
Introduce one smart contract without GraphQL and the corresponding end-to-end test. (#3577)
## Motivation GraphQL is used all over the Linera code. This led to the question whether we can program a Linera application without GraphQL. This PR demonstrates that it is possible. ## Proposal The following was done: * Introduce an application `counter-no-graphql` that works without GraphQL but still uses the `linera-sdk`. * The `ApplicationWrapper` has been restructured to separate between json queries and GraphQL queries. This PR led to some further questions to address in next PRs: * The `counter-no-graphql` application does not use GraphQL, but it still typed. We should also have a `counter_untyped` for which `execute_operation` takes a `Vec<u8>` and does not use the `linera-sdk` macros. * Different serializations are being used in the code: `bcs::to_bytes` and `serde_json::to_vec`. No effort was made to unify this. * The `raw_operation` function is returning some `CryptoHash` instead of a `Vec<u8>`. It was not clear how to obtain the result of the `execute_operation` from the resulting entry. * The EVM application are untyped. Moving to the `raw_application` seems premature. ## Test Plan The test running without GraphQL has been inserted. ## Release Plan This should be included in next TestNet release. ## Links None.
1 parent 707cbf4 commit 13e3bf8

File tree

13 files changed

+416
-22
lines changed

13 files changed

+416
-22
lines changed

Cargo.lock

Lines changed: 10 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -250,6 +250,7 @@ linera-witty-macros = { version = "0.14.0", path = "./linera-witty-macros" }
250250

251251
amm = { path = "./examples/amm" }
252252
counter = { path = "./examples/counter" }
253+
counter-no-graphql = { path = "./examples/counter-no-graphql" }
253254
crowd-funding = { path = "./examples/crowd-funding" }
254255
ethereum-tracker = { path = "./examples/ethereum-tracker" }
255256
fungible = { path = "./examples/fungible" }

examples/Cargo.lock

Lines changed: 11 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

examples/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ resolver = "2"
33
members = [
44
"amm",
55
"counter",
6+
"counter-no-graphql",
67
"crowd-funding",
78
"ethereum-tracker",
89
"fungible",
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
[package]
2+
name = "counter-no-graphql"
3+
version = "0.1.0"
4+
authors = ["Linera <[email protected]>"]
5+
edition = "2021"
6+
7+
[dependencies]
8+
futures.workspace = true
9+
linera-sdk.workspace = true
10+
serde.workspace = true
11+
12+
[target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies]
13+
linera-sdk = { workspace = true, features = ["test", "wasmer"] }
14+
tokio = { workspace = true, features = ["rt", "sync"] }
15+
16+
[dev-dependencies]
17+
assert_matches.workspace = true
18+
linera-sdk = { workspace = true, features = ["test"] }
19+
20+
[[bin]]
21+
name = "counter_no_graphql_contract"
22+
path = "src/contract.rs"
23+
24+
[[bin]]
25+
name = "counter_no_graphql_service"
26+
path = "src/service.rs"
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
# Counter No GraphQL Example Application
2+
3+
This example application implements a simple counter contract, it is initialized with an
4+
unsigned integer that can be increased by the `increment` operation. In contrast with the
5+
counter application, it works without GraphQL
6+
7+
## How It Works
8+
9+
It is a very basic Linera application, which is initialized by a `u64` which can be incremented
10+
by a `u64`.
11+
12+
For example, if the contract was initialized with 1, querying the contract would give us 1. Now if we want to
13+
`increment` it by 3, we will have to perform an operation with the parameter being 3. Now querying the
14+
application would give us 4 (1+3 = 4).
15+
16+
## Usage
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
// Copyright (c) Zefchain Labs, Inc.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
#![cfg_attr(target_arch = "wasm32", no_main)]
5+
6+
mod state;
7+
8+
use counter_no_graphql::{CounterNoGraphQlAbi, CounterOperation};
9+
use linera_sdk::{
10+
linera_base_types::WithContractAbi,
11+
views::{RootView, View},
12+
Contract, ContractRuntime,
13+
};
14+
15+
use self::state::CounterState;
16+
17+
pub struct CounterContract {
18+
state: CounterState,
19+
runtime: ContractRuntime<Self>,
20+
}
21+
22+
linera_sdk::contract!(CounterContract);
23+
24+
impl WithContractAbi for CounterContract {
25+
type Abi = CounterNoGraphQlAbi;
26+
}
27+
28+
impl Contract for CounterContract {
29+
type Message = ();
30+
type InstantiationArgument = u64;
31+
type Parameters = ();
32+
33+
async fn load(runtime: ContractRuntime<Self>) -> Self {
34+
let state = CounterState::load(runtime.root_view_storage_context())
35+
.await
36+
.expect("Failed to load state");
37+
CounterContract { state, runtime }
38+
}
39+
40+
async fn instantiate(&mut self, value: u64) {
41+
// Validate that the application parameters were configured correctly.
42+
self.runtime.application_parameters();
43+
44+
self.state.value.set(value);
45+
}
46+
47+
async fn execute_operation(&mut self, operation: CounterOperation) -> u64 {
48+
let previous_value = self.state.value.get();
49+
let CounterOperation::Increment(value) = operation;
50+
let new_value = previous_value + value;
51+
self.state.value.set(new_value);
52+
new_value
53+
}
54+
55+
async fn execute_message(&mut self, _message: ()) {
56+
panic!("Counter application doesn't support any cross-chain messages");
57+
}
58+
59+
async fn store(mut self) {
60+
self.state.save().await.expect("Failed to save state");
61+
}
62+
}
63+
64+
#[cfg(test)]
65+
mod tests {
66+
use futures::FutureExt as _;
67+
use linera_sdk::{util::BlockingWait, views::View, Contract, ContractRuntime};
68+
69+
use super::{CounterContract, CounterOperation, CounterState};
70+
71+
#[test]
72+
fn operation() {
73+
let initial_value = 72_u64;
74+
let mut counter = create_and_instantiate_counter(initial_value);
75+
76+
let increment = 42_308_u64;
77+
let operation = CounterOperation::Increment(increment);
78+
79+
let response = counter
80+
.execute_operation(operation)
81+
.now_or_never()
82+
.expect("Execution of counter operation should not await anything");
83+
84+
let expected_value = initial_value + increment;
85+
86+
assert_eq!(response, expected_value);
87+
assert_eq!(*counter.state.value.get(), initial_value + increment);
88+
}
89+
90+
#[test]
91+
#[should_panic(expected = "Counter application doesn't support any cross-chain messages")]
92+
fn message() {
93+
let initial_value = 72_u64;
94+
let mut counter = create_and_instantiate_counter(initial_value);
95+
96+
counter
97+
.execute_message(())
98+
.now_or_never()
99+
.expect("Execution of counter operation should not await anything");
100+
}
101+
102+
#[test]
103+
fn cross_application_call() {
104+
let initial_value = 2_845_u64;
105+
let mut counter = create_and_instantiate_counter(initial_value);
106+
107+
let increment = 8_u64;
108+
let operation = CounterOperation::Increment(increment);
109+
110+
let response = counter
111+
.execute_operation(operation)
112+
.now_or_never()
113+
.expect("Execution of counter operation should not await anything");
114+
115+
let expected_value = initial_value + increment;
116+
117+
assert_eq!(response, expected_value);
118+
assert_eq!(*counter.state.value.get(), expected_value);
119+
}
120+
121+
fn create_and_instantiate_counter(initial_value: u64) -> CounterContract {
122+
let runtime = ContractRuntime::new().with_application_parameters(());
123+
let mut contract = CounterContract {
124+
state: CounterState::load(runtime.root_view_storage_context())
125+
.blocking_wait()
126+
.expect("Failed to read from mock key value store"),
127+
runtime,
128+
};
129+
130+
contract
131+
.instantiate(initial_value)
132+
.now_or_never()
133+
.expect("Initialization of counter state should not await anything");
134+
135+
assert_eq!(*contract.state.value.get(), initial_value);
136+
137+
contract
138+
}
139+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
// Copyright (c) Zefchain Labs, Inc.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
/*! ABI of the Counter Example Application that does not use GraphQL */
5+
6+
use linera_sdk::linera_base_types::{ContractAbi, ServiceAbi};
7+
use serde::{Deserialize, Serialize};
8+
9+
pub struct CounterNoGraphQlAbi;
10+
11+
impl ContractAbi for CounterNoGraphQlAbi {
12+
type Operation = CounterOperation;
13+
type Response = u64;
14+
}
15+
16+
impl ServiceAbi for CounterNoGraphQlAbi {
17+
type Query = CounterRequest;
18+
type QueryResponse = u64;
19+
}
20+
21+
#[derive(Debug, Serialize, Deserialize)]
22+
pub enum CounterRequest {
23+
Query,
24+
Increment(u64),
25+
}
26+
27+
#[derive(Debug, Serialize, Deserialize)]
28+
pub enum CounterOperation {
29+
Increment(u64),
30+
}
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
// Copyright (c) Zefchain Labs, Inc.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
#![cfg_attr(target_arch = "wasm32", no_main)]
5+
6+
mod state;
7+
8+
use std::sync::Arc;
9+
10+
use counter_no_graphql::{CounterOperation, CounterRequest};
11+
use linera_sdk::{linera_base_types::WithServiceAbi, views::View, Service, ServiceRuntime};
12+
13+
use self::state::CounterState;
14+
15+
pub struct CounterService {
16+
state: CounterState,
17+
runtime: Arc<ServiceRuntime<Self>>,
18+
}
19+
20+
linera_sdk::service!(CounterService);
21+
22+
impl WithServiceAbi for CounterService {
23+
type Abi = counter_no_graphql::CounterNoGraphQlAbi;
24+
}
25+
26+
impl Service for CounterService {
27+
type Parameters = ();
28+
29+
async fn new(runtime: ServiceRuntime<Self>) -> Self {
30+
let state = CounterState::load(runtime.root_view_storage_context())
31+
.await
32+
.expect("Failed to load state");
33+
CounterService {
34+
state,
35+
runtime: Arc::new(runtime),
36+
}
37+
}
38+
39+
async fn handle_query(&self, request: CounterRequest) -> u64 {
40+
match request {
41+
CounterRequest::Query => *self.state.value.get(),
42+
CounterRequest::Increment(value) => {
43+
let operation = CounterOperation::Increment(value);
44+
self.runtime.schedule_operation(&operation);
45+
0
46+
}
47+
}
48+
}
49+
}
50+
51+
#[cfg(test)]
52+
mod tests {
53+
use std::sync::Arc;
54+
55+
use futures::FutureExt as _;
56+
use linera_sdk::{util::BlockingWait, views::View, Service, ServiceRuntime};
57+
58+
use super::{CounterRequest, CounterService, CounterState};
59+
60+
#[test]
61+
fn query() {
62+
let value = 61_098_721_u64;
63+
let runtime = Arc::new(ServiceRuntime::<CounterService>::new());
64+
let mut state = CounterState::load(runtime.root_view_storage_context())
65+
.blocking_wait()
66+
.expect("Failed to read from mock key value store");
67+
state.value.set(value);
68+
69+
let service = CounterService { state, runtime };
70+
let request = CounterRequest::Query;
71+
72+
let response = service
73+
.handle_query(request)
74+
.now_or_never()
75+
.expect("Query should not await anything");
76+
77+
assert_eq!(response, value)
78+
}
79+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
// Copyright (c) Zefchain Labs, Inc.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
use linera_sdk::views::{linera_views, RegisterView, RootView, ViewStorageContext};
5+
6+
/// The application state.
7+
#[derive(RootView)]
8+
#[view(context = "ViewStorageContext")]
9+
pub struct CounterState {
10+
pub value: RegisterView<u64>,
11+
}

0 commit comments

Comments
 (0)