mirror of
https://github.com/fluencelabs/examples
synced 2024-12-04 19:20:17 +00:00
change dir name
This commit is contained in:
parent
304ed57f0a
commit
108d3feb01
569
aqua-examples/decentralized-blockchain-gateway/README _copy.md
Normal file
569
aqua-examples/decentralized-blockchain-gateway/README _copy.md
Normal file
@ -0,0 +1,569 @@
|
||||
# EVM Integration Examples
|
||||
|
||||
## Overview
|
||||
|
||||
The [Ethereum virtual machine](https://ethereum.org/en/developers/docs/evm/) (EVM) is available across a variety of L1, L2 and sidechains with implementations ranging from straight clones to [compatible or equivalent](https://metisdao.medium.com/evm-equivalence-vs-evm-compatibility-199bd66f455d) incarnations. [Ethereum JSON RPC](https://ethereum.github.io/execution-apis/api-documentation/) is the base API to access EVM functionality and the underlying request model for the various web3 libraries such as [ethers](https://docs.ethers.io/v5/) or [ethers-rs](https://docs.rs/ethers/0.1.3/ethers/). In theory, and sometimes even practice, this allows developers to reuse their contracts and Web3 code across different chains with no changes but a few parameters.
|
||||
|
||||
While many of the various EVM chains provide the Web3 benefits desired, running (blockchain) clients tends to be a resource intensive adventure. Not surprisingly, a myriad of hosted node providers, such as [Infura](https://infura.io/), [Alchemy](https://www.alchemy.com/) and many more, provide relatively cheap access to a variety of chains. Alas, the benefits of convenience and low-to-no cost, the typical siren call of Web2 SaaS, comes at the price of introducing a single point of failure, trust in not finding your (personal) data on some blackhat site right beside the data from all the other "secure" SaaS vendors, and high exit barriers when adopting a hosting provider's specific API, thereby introducing a nontrivial chokepoint in your DApp. Morallis has a very strong [PoV](https://moralis.io/ethereum-rpc-nodes-what-they-are-and-why-you-shouldnt-use-them/) trying to convince developers to abandon open and common standards and lock into their custom APIs with high exit barriers. Of course, we'll ignore this rather self-serving piece of advice and embark on a journey in line with our Web3 ethos.
|
||||
|
||||
In this tutorial, we illustrate how Fluence and Aqua can help DApp developers minimize and even eliminate centralized points of failure undermining the Web3 benefits of their DApp.
|
||||
|
||||
|
||||
## Setting Up
|
||||
|
||||
If you haven't set up your Fluence and Aqua development environment, head over to the [setup docs](https://doc.fluence.dev/docs/tutorials_tutorials/recipes_setting_up); if you're not familiar with Fluence and Aqua, give the [developer docs](https://doc.fluence.dev) a gander.
|
||||
|
||||
In addition to the Fluence setup, you ought to sign up for a few hosted (EVM) node providers with JSON-RPC access as discussed below.
|
||||
|
||||
### Selecting Providers
|
||||
|
||||
A large number of EVM nodes are hosted by centralized providers such as [Alchemy](https://www.alchemy.com/), which isn't all that surprising how resource intensive it is to self-host nodes. In addition to hosting nodes, many hosters provide their custom API, REST, GraphQL, etc., to provide a Web2 experience and don't even offer native EVM JSON-RPC access. But with a bit of searching, you ought to be able to find a few vendors that suit our needs. For our tutorial purposes, we choose the following three hosting providers for the hosted Ethereum mainnet clients but feel free to substitute as you see fit:
|
||||
|
||||
* [Infura](https://infura.io/)
|
||||
* need to sign up for a free account and get API lkey
|
||||
* for Ethereum mainnet, our uri is:
|
||||
|
||||
* [Alchemy](https://www.alchemy.com/)
|
||||
* need to sign up for a free account and get API lkey
|
||||
* for Ethereum mainnet, our uri is:
|
||||
|
||||
* [Linkpool](https://linkpool.io/)
|
||||
* no need to sign up or need for an API key
|
||||
* is a light client limiting historic queries
|
||||
* for Ethereum mainnet, our uri is: https://main-light.eth.linkpool.io
|
||||
* does not work with "Accept: application/json" but only "Content-Type: application/json" header
|
||||
|
||||
## Oracleizing Hosting Provider Responses
|
||||
|
||||
Centralized hosted nodes introduce at best a single point of failure and at worst, a nefarious actor creating havoc in your DApp. Regardless, a centralized source of truth easily negates the benefits of a decentralized backend. Without giving up all of the convenience and cost savings, who wants to run a 3 TB node on AWS anyway, we can route identical requests to multiple hosted providers and determine, against some subjective metric, the acceptability of the responses. That is, we [oracleize](https://en.wikipedia.org/wiki/Blockchain_oracle) hosted provider responses. See Figure 1.
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
title: Figure 1: Stylize Oracle Over Multiple Host Providers
|
||||
|
||||
participant C as Client
|
||||
participant R as Relay node
|
||||
participant Pi as P2P Network
|
||||
|
||||
loop for some nodes
|
||||
C -> Pi: Deploy provider adapter
|
||||
Pi -> C: Deployed provider service metadata
|
||||
end
|
||||
|
||||
C -> Pi: Deploy quorum service to one node
|
||||
Pi -> C: Deployed quorum service metadata
|
||||
|
||||
|
||||
par for all provider urls
|
||||
R -> Pi: JSON-RPC Request
|
||||
Pi -> R: JSON-RPC Response
|
||||
end
|
||||
|
||||
R -> Pi: Evaluate JSON-RPC responses
|
||||
Pi -> R: Accept or Reject provider(s) or provider responses
|
||||
|
||||
opt data to client peer
|
||||
R -> C: provider responses, quorum, etc.
|
||||
end
|
||||
```
|
||||
|
||||
### Provider Adapter
|
||||
|
||||
In order to interact with the hosted EVMs, we choose the lowest common but open and interoperable API denominator, i.e., Ethereum JSON-RPC, which provides us with flexibility and great reusability.
|
||||
|
||||
For illustrative purposes, let's say we want to make sure the provider is returning the `latest` block, which, while simple, is a pretty good indicator of provider "liveness." The method we want to call is [`eth_blockNumber`](https://ethereum.github.io/execution-apis/api-documentation/) which formats to the following request:
|
||||
|
||||
```json
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"method": "eth_blockNumber",
|
||||
"params": [],
|
||||
"id": 0
|
||||
}
|
||||
```
|
||||
|
||||
where `id` is the nonce. If we were to use `curl` on the command line:
|
||||
|
||||
```bash
|
||||
curl --data '{"method":"eth_blockNumber","params":[],"id":1,"jsonrpc":"2.0"}' -H "Content-Type: application/json" -X POST <node or provider url>
|
||||
```
|
||||
|
||||
we expect the following result:
|
||||
|
||||
```json
|
||||
{
|
||||
"id": 1,
|
||||
"jsonrpc": "2.0",
|
||||
"result": "0x4b7" // hex encoded latest block
|
||||
}
|
||||
```
|
||||
|
||||
Ok, let's create an Wasm service we can use to query multiple providers. Keep the curl command in mind as we'll need to use curl from our Wasm to make the provider call.
|
||||
|
||||
`cd` into the `multi-provider-query` directory and have a look at `src/main.rs`:
|
||||
|
||||
```rust
|
||||
// src/main.rs
|
||||
#[marine]
|
||||
// see https://eth.wiki/json-rpc/API#eth_blocknumbers
|
||||
fn get_block_number(provider: ProviderInfo) -> EVMResult {
|
||||
let method = "eth_blockNumber";
|
||||
let params: Vec<String> = vec![];
|
||||
let url = provider.url;
|
||||
|
||||
let data = RpcData::new(method.to_owned(), params);
|
||||
let data = serde_json::to_string(&data).unwrap();
|
||||
|
||||
let curl_cmd = curl_cmd_builder(url, data);
|
||||
let response = get_curl_response(curl_cmd);
|
||||
|
||||
if response.error.is_none() {
|
||||
let raw_response = response.result.unwrap();
|
||||
let block_height = u64::from_str_radix(raw_response.trim_start_matches("0x"), 16);
|
||||
|
||||
let result = match block_height {
|
||||
Ok(r) => {
|
||||
let j_res = serde_json::json!({ "block-heigh": r });
|
||||
EVMResult {
|
||||
stdout: j_res.to_string(),
|
||||
stderr: "".to_owned(),
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
let err = format!("unable to convert {} to u64 with error {}", raw_response, e);
|
||||
EVMResult {
|
||||
stdout: "".to_owned(),
|
||||
stderr: err,
|
||||
}
|
||||
}
|
||||
};
|
||||
return result;
|
||||
}
|
||||
|
||||
EVMResult {
|
||||
stdout: "".to_owned(),
|
||||
stderr: serde_json::to_string(&response.error).unwrap(),
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The `get_block_number` function implements a wrapper around the `eth_blockNumber` method and decodes the `hex` response to a json string. We could have implemented a more general function, say, `fn eth_rpc_wrapper(provider: ProviderInfo, method: String, parameters: Vec<String>) -> EVMResult` and either returned the raw json rpc result or added per-method decoding match arms, which **you are encouraged** to implement for the methods of your choosing.
|
||||
|
||||
Note that not all providers follow the JSON-RPC style when it comes to error handling. For example, submitting an invalid API key to Alchemy, results in an JSON-RPC error:
|
||||
|
||||
```json
|
||||
{ jsonrpc: "2.0", error: Some(RpcResponseError { code: -32000, message: "Must be authenticated!" }), result: None }
|
||||
```
|
||||
|
||||
Infura, on the other hand, does not follow the JSON-RPC route and instead returns a string in stdout with no other error codes or indicators provided:
|
||||
|
||||
```json
|
||||
"invalid project id\n"
|
||||
```
|
||||
|
||||
Infura-ating but such is life. We compensate for this idiosyncrasy with the following adjustment to an otherwise straight-forward curl response processor:
|
||||
|
||||
```rust
|
||||
fn get_curl_response(curl_cmd: Vec<String>) -> RpcResponse {
|
||||
let response = curl_request(curl_cmd);
|
||||
let response = String::from_utf8(response.stdout).unwrap();
|
||||
|
||||
let response: Result<RpcResponse, _> = serde_json::from_str(&response);
|
||||
match response {
|
||||
Ok(r) => r,
|
||||
Err(e) => RpcResponse {
|
||||
jsonrpc: "".to_owned(),
|
||||
error: Some(RpcResponseError {
|
||||
code: -1, // we know it's not an EVM error
|
||||
message: e.to_string(),
|
||||
}),
|
||||
result: None,
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
Of course, other providers may provide even other response patterns and it is up to you to make the necessary adjustments. You may think the convenience of vendor lock-in doesn't look too bad right about now but trust yourself, it is a big risk and cost. Recall that the `#[marine]` macro brings the Fluence `marine-rust-sdk` into play to compile to the Wasi target and expose the appropriate interfaces. Moreover, note that we link the [`curl` module](./curl-adapter/) to enable our http calls.
|
||||
|
||||
At this point we're good to go and compile our code to Wasm:
|
||||
|
||||
```bash
|
||||
./scripts/build.sh
|
||||
```
|
||||
|
||||
which should put `curl_adapter.wasm` and `multi_provider_query.wasm` in the `artifacts` directory. Before we deploy or service to one or more peers, let's check it out locally using the `marine` REPL:
|
||||
|
||||
```bash
|
||||
cd ../ # assuming your are still in the multi-provider-query directory
|
||||
marine repl configs/Config.toml
|
||||
|
||||
Welcome to the Marine REPL (version 0.16.2)
|
||||
Minimal supported versions
|
||||
sdk: 0.6.0
|
||||
interface-types: 0.20.0
|
||||
|
||||
app service was created with service id = 2429b9fb-c04a-4ed9-9062-400c39e86f72
|
||||
elapsed time 130.289671ms
|
||||
```
|
||||
|
||||
Let's have a look at what we loaded:
|
||||
|
||||
```python
|
||||
1> interface
|
||||
Loaded modules interface:
|
||||
exported data types (combined from all modules):
|
||||
data ProviderInfo:
|
||||
url: string
|
||||
name: string
|
||||
data EVMResult:
|
||||
provider: string
|
||||
stdout: string
|
||||
stderr: string
|
||||
data MountedBinaryResult:
|
||||
ret_code: i32
|
||||
error: string
|
||||
stdout: []u8
|
||||
stderr: []u8
|
||||
|
||||
exported functions:
|
||||
curl_adapter:
|
||||
fn curl_request(cmd: []string) -> MountedBinaryResult
|
||||
multi_provider_query:
|
||||
fn get_block_number(provider: ProviderInfo) -> EVMResult
|
||||
|
||||
2>
|
||||
```
|
||||
|
||||
If you go back to the source files, you'll see that all the interfaces marked up with the `#[marine]` macro are exposed and available in the REPL. Moreover, note that both the `curl_adapter` and `multi_provider_query` WASM modules are available as the eponymous namespaces with the corresponding (exposed) functions.
|
||||
|
||||
Without further ado, let's try to get a the latest block with a couple of the provider urls:
|
||||
|
||||
```python
|
||||
2> call multi_provider_query get_block_number [{"name":"infura", "url":"https://mainnet.infura.io/v3/<YOUR-API_KEY>"}]
|
||||
result: Object({"provider": String("infura"), "stderr": String(""), "stdout": String("{\"block-height\":15016773}")})
|
||||
elapsed time: 909.277016ms
|
||||
|
||||
3> call multi_provider_query get_block_number [{"url":"https://main-light.eth.linkpool.io/", "name":"linkpool"}]
|
||||
result: Object({"provider": String("linkpool"), "stderr": String(""), "stdout": String("{\"block-height\":15016773}")})
|
||||
elapsed time: 621.172593ms
|
||||
|
||||
4>
|
||||
```
|
||||
|
||||
Ok, so we called both Alchemy and Infura for the latest block height and got the same result, which is somewhat confidence inspiring. Let's check with a bad API key and keep in mind that we had to "coerce" the Infura response into the JSON-RPC format:
|
||||
|
||||
```python
|
||||
|
||||
4>call multi_provider_query get_block_number [{"name":"infura", "url":"https://mainnet.infura.io/v3/<YOUR-BAD-API-KEY"}]
|
||||
result: Object({"provider": String("infura"), "stderr": String("{\"code\":-1,\"message\":\"expected value at line 1 column 1\"}"), "stdout": String("")})
|
||||
elapsed time: 419.089663ms
|
||||
```
|
||||
|
||||
While we have been using hosted Ethereum mainnet endpoints, you can easily use other supported (EVM) networks such as [Polygn PoS on Alchemy](https://docs.polygon.technology/docs/develop/alchemy/) or [Polygon PoS on Infura](https://docs.infura.io/infura/networks/polygon-pos/how-to).
|
||||
|
||||
All looks well and we are ready to deploy!
|
||||
|
||||
## Multi-Provider Queries With Aqua
|
||||
|
||||
Now that we have our adapter and providers, let's think about how we can use a multi-provider query approach to ensure a high likelihood of truth and reliability in our query results. Keep in mind that in addition to our lack of trust in each of the providers, we may also not trust the Fluence peers hosting our services, which we'll ignore for the moment. Moreover, we need to parallelize our requests to make sure we don't inadvertently straddle different block times legitimately leading to different responses. As you may recall, Wasm modules are single threaded and concurrency of service execution is managed at the Aqua level. Parallel execution of a service may be accomplished on one or multiple nodes but either way, you need to deploy multiple service instances, e.g., for three providers, we want three service instances.
|
||||
|
||||
In the "trust the Fluence nodes" case, then, we deploy our adapter service to three (3) different nodes -- one for each provider. Let's get to it, deploy our services and create our Aqua script.
|
||||
|
||||
### Service Deployment
|
||||
|
||||
We deploy our service with the `aqua cli` tool to the `stage` testnet. To see the Fluence default peers for `stage`:
|
||||
|
||||
```bash
|
||||
aqua config default_peers stage
|
||||
|
||||
/dns4/stage.fluence.dev/tcp/19001/wss/p2p/12D3KooWHCJbJKGDfCgHSoCuK9q4STyRnVveqLoXAPBbXHTZx9Cv
|
||||
/dns4/stage.fluence.dev/tcp/19002/wss/p2p/12D3KooWMigkP4jkVyufq5JnDJL6nXvyjeaDNpRfEZqQhsG3sYCU
|
||||
/dns4/stage.fluence.dev/tcp/19003/wss/p2p/12D3KooWMMGdfVEJ1rWe1nH1nehYDzNEHhg5ogdfiGk88AupCMnf
|
||||
/dns4/stage.fluence.dev/tcp/19004/wss/p2p/12D3KooWJ4bTHirdTFNZpCS72TAzwtdmavTBkkEXtzo6wHL25CtE
|
||||
/dns4/stage.fluence.dev/tcp/19005/wss/p2p/12D3KooWAKNos2KogexTXhrkMZzFYpLHuWJ4PgoAhurSAv7o5CWA
|
||||
/dns4/stage.fluence.dev/tcp/19990/wss/p2p/12D3KooWDcpWuyrMTDinqNgmXAuRdfd2mTdY9VoXZSAet2pDzh6r
|
||||
```
|
||||
|
||||
Also, if you are deploying your own service instances, you need one or more keypairs to deploy, authenticate and eventually delete your service. You can use `aqua-cli to create keys:
|
||||
|
||||
```bash
|
||||
aqua key create
|
||||
{
|
||||
"peerId": "12D3KooWDAM4YDz7WdAWVzxQspeZudL4rqyGmvWUePEvuERNYfLC",
|
||||
"secretKey": "SOME-SECRET-KEY",
|
||||
"publicKey": "SOME-PUB-KEY"
|
||||
}
|
||||
```
|
||||
|
||||
For tutorial purposes, we'll just stick with one keypair but feel free to use one pair for each deployed service.
|
||||
|
||||
Time to deploy:
|
||||
|
||||
```bash
|
||||
aqua remote deploy_service \
|
||||
--addr /dns4/stage.fluence.dev/tcp/19004/wss/p2p/12D3KooWJ4bTHirdTFNZpCS72TAzwtdmavTBkkEXtzo6wHL25CtE \
|
||||
--config-path configs/deployment_cfg.json \
|
||||
--service multi-provider-query \
|
||||
--sk <SECRET KEY> \
|
||||
--log-level off
|
||||
Going to upload a module...
|
||||
Going to upload a module...
|
||||
Now time to make a blueprint...
|
||||
Blueprint id:
|
||||
3bd81487cb43b76a5a67d5ebb417dd4afacb29f56d77492d8b21ad469acdfb8b
|
||||
And your service id is:
|
||||
"d9124884-3c42-43d6-9a1f-1b645d073c3f"
|
||||
|
||||
```
|
||||
|
||||
Ok, one down, two to go:
|
||||
|
||||
```bash
|
||||
--addr /dns4/stage.fluence.dev/tcp/19005/wss/p2p/12D3KooWAKNos2KogexTXhrkMZzFYpLHuWJ4PgoAhurSAv7o5CWA \
|
||||
--config-path configs/deployment_cfg.json \
|
||||
--service multi-provider-query \
|
||||
--sk <SECRET KEY> \
|
||||
--log-level off
|
||||
Going to upload a module...
|
||||
Going to upload a module...
|
||||
Now time to make a blueprint...
|
||||
Blueprint id:
|
||||
3bd81487cb43b76a5a67d5ebb417dd4afacb29f56d77492d8b21ad469acdfb8b
|
||||
And your service id is:
|
||||
"3c321110-b069-42c6-b5e0-aed73d976a60"
|
||||
```
|
||||
|
||||
and
|
||||
|
||||
```bash
|
||||
aqua remote deploy_service \
|
||||
--addr /dns4/stage.fluence.dev/tcp/19003/wss/p2p/12D3KooWMMGdfVEJ1rWe1nH1nehYDzNEHhg5ogdfiGk88AupCMnf \
|
||||
--config-path configs/deployment_cfg.json \
|
||||
--service multi-provider-query \
|
||||
--sk <SECRET KEY> \
|
||||
--log-level off
|
||||
Going to upload a module...
|
||||
Going to upload a module...
|
||||
Now time to make a blueprint...
|
||||
Blueprint id:
|
||||
3bd81487cb43b76a5a67d5ebb417dd4afacb29f56d77492d8b21ad469acdfb8b
|
||||
And your service id is:
|
||||
"84d4d018-0c13-4d6d-8c11-599a3919911c"
|
||||
```
|
||||
|
||||
Now that we have our services deployed, it's time to create our Aqua script.
|
||||
|
||||
### Aqua
|
||||
|
||||
The Fluence protocol uses function addressability, i.e., (peer id, service id) tuples, to resolve services. In our model, function addresses are input parameters just like the provider urls. So we can capture our function addresses with a struct:
|
||||
|
||||
```aqua
|
||||
data FunctionAddress:
|
||||
peer_id: string
|
||||
service_id: string
|
||||
```
|
||||
|
||||
which allows us to create a fairly compact function signature:
|
||||
|
||||
```aqua
|
||||
func get_block_heights(providers: []string, addrs: []FunctionAddress):
|
||||
```
|
||||
|
||||
However, we want to be able to track the providers' responses and it seems easier to provide the provider name with the url, which we with much foresight already used in our Wasm code:
|
||||
|
||||
```aqua
|
||||
data ProviderInfo:
|
||||
name: string
|
||||
url: string
|
||||
```
|
||||
|
||||
Let's put it all together:
|
||||
|
||||
```aqua
|
||||
|
||||
data FunctionAddress:
|
||||
peer_id: string
|
||||
service_id: string
|
||||
|
||||
data ProviderInfo:
|
||||
name: string
|
||||
url: string
|
||||
|
||||
-- from our WASM code
|
||||
data EVMResult:
|
||||
provider: string
|
||||
stdout: string
|
||||
stderr: string
|
||||
|
||||
-- helper function to get length of []Provider
|
||||
service MyOp("op"):
|
||||
array_length(providers: []ProviderInfo) -> i64
|
||||
|
||||
service MyOp2("op"):
|
||||
array_length(providers: []FunctionAddress) -> i64
|
||||
|
||||
-- from our WASM code
|
||||
service MultiProviderQuery("service-id"):
|
||||
get_block_number(provider: ProviderInfo) -> EVMResult
|
||||
|
||||
-- finally, our Aqua function
|
||||
func get_block_heights(providers: []ProviderInfo, addrs: []FunctionAddress) -> []EVMResult:
|
||||
result: *EVMResult
|
||||
result2: *string
|
||||
n <- MyOp.array_length(providers)
|
||||
n2 <- MyOp2.array_length(addrs)
|
||||
|
||||
if n > 0:
|
||||
for addr <- addrs par:
|
||||
on addr.peer_id:
|
||||
MultiProviderQuery addr.service_id
|
||||
for provider <- providers:
|
||||
result <- MultiProviderQuery.get_block_number(provider)
|
||||
result2 <<- provider.name
|
||||
-- join result[n2-1]
|
||||
join result[n*n2-1]
|
||||
<- result
|
||||
```
|
||||
|
||||
The script we created is going to get us halfway to where we want to go. So let's try it from the command-line with `aqua-cli`, which is a bit unwieldy and we'll see in a bit how we can create a local client with Fluence JS. For now, quick and easy:
|
||||
|
||||
```bash
|
||||
aqua run \
|
||||
--addr /dns4/stage.fluence.dev/tcp/19004/wss/p2p/12D3KooWJ4bTHirdTFNZpCS72TAzwtdmavTBkkEXtzo6wHL25CtE \
|
||||
-i aqua \
|
||||
-f 'get_block_heights(arg1, arg2)' \
|
||||
--data '{"arg1": [{"name":"infura", "url":"https://mainnet.infura.io/v3/0cc023286cae4ab886598ecd14e256fd"},
|
||||
{"name":"alchemy","url":"https://eth-mainnet.alchemyapi.io/v2/2FLlm9t-xOm0CbGx-ORr81li1yD_cKP6"},
|
||||
{"name":"link", "url":"https://main-light.eth.linkpool.io"}],
|
||||
"arg2": [{"peer_id":"12D3KooWJ4bTHirdTFNZpCS72TAzwtdmavTBkkEXtzo6wHL25CtE", "service_id":"d9124884-3c42-43d6-9a1f-1b645d073c3f"},
|
||||
{"peer_id":"12D3KooWAKNos2KogexTXhrkMZzFYpLHuWJ4PgoAhurSAv7o5CWA", "service_id":"3c321110-b069-42c6-b5e0-aed73d976a60"},
|
||||
{"peer_id":"12D3KooWMMGdfVEJ1rWe1nH1nehYDzNEHhg5ogdfiGk88AupCMnf", "service_id":"84d4d018-0c13-4d6d-8c11-599a3919911c"}]}'
|
||||
```
|
||||
|
||||
which results it:
|
||||
|
||||
```bash
|
||||
[
|
||||
{
|
||||
"provider": "infura",
|
||||
"stderr": "",
|
||||
"stdout": "{\"block-height\":15016806}"
|
||||
},
|
||||
{
|
||||
"provider": "alchemy",
|
||||
"stderr": "",
|
||||
"stdout": "{\"block-height\":15016806}"
|
||||
},
|
||||
{
|
||||
"provider": "link",
|
||||
"stderr": "",
|
||||
"stdout": "{\"block-height\":15016806}"
|
||||
},
|
||||
{
|
||||
"provider": "infura",
|
||||
"stderr": "",
|
||||
"stdout": "{\"block-height\":15016806}"
|
||||
},
|
||||
{
|
||||
"provider": "alchemy",
|
||||
"stderr": "",
|
||||
"stdout": "{\"block-height\":15016806}"
|
||||
},
|
||||
{
|
||||
"provider": "link",
|
||||
"stderr": "",
|
||||
"stdout": "{\"block-height\":15016806}"
|
||||
},
|
||||
{
|
||||
"provider": "infura",
|
||||
"stderr": "",
|
||||
"stdout": "{\"block-height\":15016806}"
|
||||
},
|
||||
{
|
||||
"provider": "alchemy",
|
||||
"stderr": "",
|
||||
"stdout": "{\"block-height\":15016806}"
|
||||
},
|
||||
{
|
||||
"provider": "link",
|
||||
"stderr": "",
|
||||
"stdout": "{\"block-height\":15016806}"
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
A quick scan suggests that all responses are equal across providers. That's a good start! However, we don't want to rely on manual inspection of our data but want to programmatically determine what a quorum. A possible quorum rule is the two-third rule but we need consider our response categories:
|
||||
|
||||
* blocknumber response -- "correct"
|
||||
* blocknumber response -- "incorrect"
|
||||
* no response
|
||||
|
||||
In order to be able to apply a threshold decision like the 2/3 rule, we first need to determine the frequency distribution of our result set. A naive implementation might look like this:
|
||||
|
||||
```rust
|
||||
|
||||
```
|
||||
|
||||
Alright, so we got results ...
|
||||
|
||||
|
||||
|
||||
|
||||
### Persisting Results
|
||||
|
||||
aqua run -i aqua \
|
||||
--addr /dns4/stage.fluence.dev/tcp/19004/wss/p2p/12D3KooWJ4bTHirdTFNZpCS72TAzwtdmavTBkkEXtzo6wHL25CtE \
|
||||
-f'create(arg, "12D3KooWJ4bTHirdTFNZpCS72TAzwtdmavTBkkEXtzo6wHL25CtE", "e9fbfb09-c8b8-447a-b405-5de579b8db6c")' \
|
||||
-d '{"arg": [
|
||||
{
|
||||
"provider": "infura",
|
||||
"stderr": "",
|
||||
"stdout": "{\"block-height\":15016806}"
|
||||
},
|
||||
{
|
||||
"provider": "alchemy",
|
||||
"stderr": "",
|
||||
"stdout": "{\"block-height\":15016806}"
|
||||
},
|
||||
{
|
||||
"provider": "link",
|
||||
"stderr": "",
|
||||
"stdout": "{\"block-height\":15016806}"
|
||||
},
|
||||
{
|
||||
"provider": "infura",
|
||||
"stderr": "",
|
||||
"stdout": "{\"block-height\":15016806}"
|
||||
},
|
||||
{
|
||||
"provider": "alchemy",
|
||||
"stderr": "",
|
||||
"stdout": "{\"block-height\":15016806}"
|
||||
},
|
||||
{
|
||||
"provider": "link",
|
||||
"stderr": "",
|
||||
"stdout": "{\"block-height\":15016806}"
|
||||
},
|
||||
{
|
||||
"provider": "infura",
|
||||
"stderr": "",
|
||||
"stdout": "{\"block-height\":15016806}"
|
||||
},
|
||||
{
|
||||
"provider": "alchemy",
|
||||
"stderr": "",
|
||||
"stdout": "{\"block-height\":15016806}"
|
||||
},
|
||||
{
|
||||
"provider": "link",
|
||||
"stderr": "",
|
||||
"stdout": "{\"block-height\":15016806}"
|
||||
}]}'
|
||||
|
||||
|
||||
|
||||
## Summary
|
||||
|
||||
We developed a model to decentralize EVM hosting providers for our DApps and implemented a stylized solution with Fluence and Aqua. Specifically, we queried multiple centralized hosted EVM providers with the open Ethereum JSON-RPC API and settled on pulling the `latest` block as an indicator of reliability and "liveness", as opposed to (stale) caches, of each provider.
|
||||
|
||||
Along the our journey, we pretty much touched on every possible chokepoint and discussed what a feasible approach to quroum, or consensus, might look like and what ??? diviaions actually might mean. All in all, it became clear rather quickly that ... wide decisions. Hopefully making us more aware of all the decisons oracle providers, on- or off-chain, may make and how such decisions may not be in our best interest.
|
425
aqua-examples/decentralized-blockchain-gateway/README.md.bk2
Normal file
425
aqua-examples/decentralized-blockchain-gateway/README.md.bk2
Normal file
@ -0,0 +1,425 @@
|
||||
# EVM Integration Examples
|
||||
|
||||
## Overview
|
||||
|
||||
The [Ethereum virtual machine](https://ethereum.org/en/developers/docs/evm/) (EVM) is available across a variety of L1, L2 and sidechains with implementations ranging from straight clones to [compatible or equivalent](https://metisdao.medium.com/evm-equivalence-vs-evm-compatibility-199bd66f455d) implementation. [Ethereum JSON RPC](https://ethereum.github.io/execution-apis/api-documentation/) is the base API to access EVM functionality and the underlying request model for the various web3 libraries such as [ethers](https://docs.ethers.io/v5/) or [ethers-rs](https://docs.rs/ethers/0.1.3/ethers/). In theory, and sometimes even practice, this allows developers to reuse their contracts and Web3 code across different chains with no changes but a few parameters.
|
||||
|
||||
While many of the various EVM chains provide the Web3 benefits desired, running (blockchain) clients tends to be a resource intensive adventure. Not surprisingly, a myriad of hosted node providers, such as [Infura](https://infura.io/), [Alchemy](https://www.alchemy.com/) and many more, provide relatively cheap access and convenience APIs to a variety of chain clients. Alas, the benefits of convenience, possibly performance and low-to-no cost, comes at the price of introducing a single point of failure and a high exit barriers when adopting a hosting provider's specific API, thereby introducing a nontrivial chokepoint in your DApp. Morallis has a [PoV](https://moralis.io/ethereum-rpc-nodes-what-they-are-and-why-you-shouldnt-use-them/) suggesting developers abandon open and common standards and to lock into custom APIs with high exit barriers. Of course, we'll ignore this rather self-serving piece of advice and embark on a journey in line with our Web3 ethos.
|
||||
|
||||
In this tutorial, we illustrate how Fluence and Aqua can help DApp developers minimize and even eliminate centralized points of failure undermining the Web3 benefits of their DApp.
|
||||
|
||||
|
||||
## Setting Up
|
||||
|
||||
If you haven't set up your Fluence and Aqua development environment, head over to the [setup docs](https://doc.fluence.dev/docs/tutorials_tutorials/recipes_setting_up); if you're not familiar with Fluence and Aqua, give the [developer docs](https://doc.fluence.dev) a gander.
|
||||
|
||||
In oder to actively participate, you ought to [sign up](##Selecting-Providers) for a few hosted (EVM) node providers with JSON-RPC access.
|
||||
|
||||
## Oracleizing Hosting Provider Responses
|
||||
|
||||
Centralized hosted nodes introduce at best a single point of failure and at worst, a nefarious actor creating havoc in your DApp. Regardless, a centralized source of truth easily negates the benefits of a decentralized backend. Without giving up all of the convenience and cost savings, who wants to run a 3 TB node on AWS anyway, we can route identical requests to multiple hosted providers and determine, against some quorum, the acceptability of the responses. That is, we [oracleize](https://en.wikipedia.org/wiki/Blockchain_oracle) hosted provider responses. See Figure 1.
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
title: Figure 1: Stylize Oracle Over Multiple Host Providers
|
||||
|
||||
participant C as Client
|
||||
participant R as Relay node
|
||||
participant Pi as P2P Network
|
||||
|
||||
loop for some nodes
|
||||
C -> Pi: Deploy provider adapter
|
||||
Pi -> C: Deployed provider service metadata
|
||||
end
|
||||
|
||||
C -> Pi: Deploy quorum service to one node
|
||||
Pi -> C: Deployed quorum service metadata
|
||||
|
||||
|
||||
par for all provider urls
|
||||
R -> Pi: JSON-RPC Request
|
||||
Pi -> R: JSON-RPC Response
|
||||
end
|
||||
|
||||
R -> Pi: Evaluate JSON-RPC responses
|
||||
Pi -> R: Accept or Reject provider(s) or provider responses
|
||||
|
||||
opt data to client peer
|
||||
R -> C: provider responses, quorum, etc.
|
||||
end
|
||||
```
|
||||
|
||||
### Provider Adapter
|
||||
|
||||
In order to interact with the hosted EVms, we choose the lowest common API denominator, i.e., Ethereum JSON-RPC, which provides us with flexibility and great reuse.
|
||||
|
||||
For illustrative purposes, let's say we want to make sure the provider is returning the `latest` block, which, while simple, is a pretty good indicator of provider "liveness." The JSON-RPC method we want to call is [`eth_blockNumber`](https://ethereum.github.io/execution-apis/api-documentation/) which formats to the following request:
|
||||
|
||||
```json
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"method": "eth_blockNumber",
|
||||
"params": [],
|
||||
"id": 0
|
||||
}
|
||||
```
|
||||
|
||||
where `id` is the nonce. If we were to use `curl` on the command line:
|
||||
|
||||
```bash
|
||||
curl --data '{"method":"eth_blockNumber","params":[],"id":1,"jsonrpc":"2.0"}' -H "Content-Type: application/json" -X POST <node or provider url>
|
||||
```
|
||||
|
||||
we expect the following result:
|
||||
|
||||
```json
|
||||
{
|
||||
"id": 1,
|
||||
"jsonrpc": "2.0",
|
||||
"result": "0x4b7" // hex encoded latest block
|
||||
}
|
||||
```
|
||||
|
||||
Ok, let's create an Wasm service we can use to query providers. Keep the curl command in mind as we'll need to use curl from our Wasm module to make the provider call.
|
||||
|
||||
`cd` into the `multi-provider-query` directory and have a look at `src/main.rs`:
|
||||
|
||||
```rust
|
||||
// src/main.rs
|
||||
#[marine]
|
||||
// see https://eth.wiki/json-rpc/API#eth_blocknumbers
|
||||
fn get_block_number(provider: ProviderInfo) -> EVMResult {
|
||||
let method = "eth_blockNumber";
|
||||
let params: Vec<String> = vec![];
|
||||
let url = provider.url;
|
||||
|
||||
let data = RpcData::new(method.to_owned(), params);
|
||||
let data = serde_json::to_string(&data).unwrap();
|
||||
|
||||
let curl_cmd = curl_cmd_builder(url, data);
|
||||
let response = get_curl_response(curl_cmd);
|
||||
|
||||
if response.error.is_none() {
|
||||
let raw_response = response.result.unwrap();
|
||||
let block_height = u64::from_str_radix(raw_response.trim_start_matches("0x"), 16);
|
||||
|
||||
let result = match block_height {
|
||||
Ok(r) => {
|
||||
let j_res = serde_json::json!({ "block-heigh": r });
|
||||
EVMResult {
|
||||
stdout: j_res.to_string(),
|
||||
stderr: "".to_owned(),
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
let err = format!("unable to convert {} to u64 with error {}", raw_response, e);
|
||||
EVMResult {
|
||||
stdout: "".to_owned(),
|
||||
stderr: err,
|
||||
}
|
||||
}
|
||||
};
|
||||
return result;
|
||||
}
|
||||
|
||||
EVMResult {
|
||||
stdout: "".to_owned(),
|
||||
stderr: serde_json::to_string(&response.error).unwrap(),
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The `get_block_number` function implements a wrapper around the `eth_blockNumber` method and decodes the hex response to a json string. We could have implemented a more general function, say, `fn eth_rpc_wrapper(provider: ProviderInfo, method: String, parameters: Vec<String>) -> EVMResult` and either returned the raw json rpc result or added per-method decoding match arms, which you are encouraged to implement for the methods of your choosing.
|
||||
|
||||
Note that not all provides follow the JSON-RPC style when it comes to error handling. For example, submitting an invalid API key to Alchemy, results in JSON-RPC error:
|
||||
|
||||
```json
|
||||
{ jsonrpc: "2.0", error: Some(RpcResponseError { code: -32000, message: "Must be authenticated!" }), result: None }
|
||||
```
|
||||
|
||||
Infura, on the other hand, does not follow the JSON-RPC route and instead returns a string in stdout with no other error codes or indicators available:
|
||||
|
||||
```json
|
||||
"invalid project id\n"
|
||||
```
|
||||
|
||||
Infura-ating but such is life. We compensate for this idiosyncrasy with the following curl response processor:
|
||||
|
||||
```rust
|
||||
fn get_curl_response(curl_cmd: Vec<String>) -> RpcResponse {
|
||||
let response = curl_request(curl_cmd);
|
||||
let response = String::from_utf8(response.stdout).unwrap();
|
||||
|
||||
let response: Result<RpcResponse, _> = serde_json::from_str(&response);
|
||||
match response {
|
||||
Ok(r) => r,
|
||||
Err(e) => RpcResponse {
|
||||
jsonrpc: "".to_owned(),
|
||||
error: Some(RpcResponseError {
|
||||
code: -1,
|
||||
message: e.to_string(),
|
||||
}),
|
||||
result: None,
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
Of course, other providers may provide even other response patters and it is up to you to make the necessary adjustments. You may think the convenience of vendor lock-in doesn't look that bad right about now but trust yourself, it is not.
|
||||
|
||||
Recall that the `#[marine]` macro brings the Fluence `marine-rust-sdk` into play to compile to the Wasi target and expose the appropriate interfaces. Moreover, note that we link the [`curl` module](./curl-adapter/) to enable our http calls.
|
||||
|
||||
At this point we're good to go and compile our code to Wasm:
|
||||
|
||||
```bash
|
||||
./scripts/build.sh
|
||||
```
|
||||
|
||||
which should put `curl_adapter.wasm` and `multi_provider_query.wasm` in the `artifacts` directory. Before we deploy or service to one or more peers, let's check it out locally using the `marine` REPL:
|
||||
|
||||
```bash
|
||||
cd ../ # assuming your are still in the multi-provider-query directory
|
||||
marine repl configs/Config.toml
|
||||
|
||||
Welcome to the Marine REPL (version 0.16.2)
|
||||
Minimal supported versions
|
||||
sdk: 0.6.0
|
||||
interface-types: 0.20.0
|
||||
|
||||
app service was created with service id = 2429b9fb-c04a-4ed9-9062-400c39e86f72
|
||||
elapsed time 130.289671ms
|
||||
```
|
||||
|
||||
Let's have a look at what we loaded:
|
||||
|
||||
```python
|
||||
1> interface
|
||||
Loaded modules interface:
|
||||
exported data types (combined from all modules):
|
||||
data ProviderInfo:
|
||||
url: string
|
||||
name: string
|
||||
data EVMResult:
|
||||
stdout: string
|
||||
stderr: string
|
||||
data MountedBinaryResult:
|
||||
ret_code: i32
|
||||
error: string
|
||||
stdout: []u8
|
||||
stderr: []u8
|
||||
|
||||
exported functions:
|
||||
curl_adapter:
|
||||
fn curl_request(cmd: []string) -> MountedBinaryResult
|
||||
multi_provider_query:
|
||||
fn get_block_number(provider: ProviderInfo) -> EVMResult
|
||||
|
||||
2>
|
||||
```
|
||||
|
||||
If you go back to the source files, you'll see that all the interfaces marked up with the `#[marine]` macro are exposed and available in the REPL. Moreover, note that both the `curl_adapter` and `multi_provider_query` WASM modules are available as the eponymous namespaces with the corresponding (exposed) functions.
|
||||
|
||||
Without further ado, let's try to get a the latest block with a couple of the provider urls:
|
||||
|
||||
```python
|
||||
2> call multi_provider_query get_block_number [{"url":"https://eth-mainnet.alchemyapi.io/v2/<YOUR-API_KEY>", "name":"alchemy"}]
|
||||
result: Object({"stderr": String(""), "stdout": String("{\"block-height\":14992116}")})
|
||||
elapsed time: 492.973202ms
|
||||
|
||||
3> call multi_provider_query get_block_number [{"url":"https://mainnet.infura.io/v3/0cc023286cae4ab886598ecd14e256fd", "name":"infura"}]
|
||||
result: Object({"stderr": String(""), "stdout": String("{\"block-height\":14992116}")})
|
||||
elapsed time: 589.44887ms
|
||||
|
||||
4>
|
||||
```
|
||||
|
||||
Ok, so we called both Alchemy and Infura for the latest block height and got the same result, which is somewhat confidence inspiring. Let's check with a bad API key and keep in mind that we had to "coerce" the Infura response into the JSON-RPC format:
|
||||
|
||||
```python
|
||||
4> call multi_provider_query get_block_number [{"url":"https://eth-mainnet.alchemyapi.io/v2/YOUR-BAD-API_KEY", "name":"alchemy"}]
|
||||
result: Object({"stderr": String("{\"code\":-32000,\"message\":\"Must be authenticated!\"}"), "stdout": String("")})
|
||||
elapsed time: 402.875003ms
|
||||
|
||||
5> call multi_provider_query get_block_number [{"url":"https://mainnet.infura.io/v3/YOUR-BAD-API_KEY", "name":"infura"}]
|
||||
result: Object({"stderr": String("{\"code\":-1,\"message\":\"expected value at line 1 column 1\"}"), "stdout": String("")})
|
||||
elapsed time: 399.970977ms
|
||||
```
|
||||
|
||||
While we have been using hosted Ethereum mainnet endpoints, you can easily use other supported (EVM) networks such as [Polygn PoS on Alchemy](https://docs.polygon.technology/docs/develop/alchemy/) or [Polygon PoS on Infura](https://docs.infura.io/infura/networks/polygon-pos/how-to).
|
||||
|
||||
Since all looks well, let's deploy our service with the `aqua cli` tool to the `stage` testnet. To see the Fluence default peers for `stage`:
|
||||
|
||||
```bash
|
||||
aqua config default_peers stage
|
||||
/dns4/stage.fluence.dev/tcp/19001/wss/p2p/12D3KooWHCJbJKGDfCgHSoCuK9q4STyRnVveqLoXAPBbXHTZx9Cv
|
||||
/dns4/stage.fluence.dev/tcp/19002/wss/p2p/12D3KooWMigkP4jkVyufq5JnDJL6nXvyjeaDNpRfEZqQhsG3sYCU
|
||||
/dns4/stage.fluence.dev/tcp/19003/wss/p2p/12D3KooWMMGdfVEJ1rWe1nH1nehYDzNEHhg5ogdfiGk88AupCMnf
|
||||
/dns4/stage.fluence.dev/tcp/19004/wss/p2p/12D3KooWJ4bTHirdTFNZpCS72TAzwtdmavTBkkEXtzo6wHL25CtE
|
||||
/dns4/stage.fluence.dev/tcp/19005/wss/p2p/12D3KooWAKNos2KogexTXhrkMZzFYpLHuWJ4PgoAhurSAv7o5CWA
|
||||
/dns4/stage.fluence.dev/tcp/19990/wss/p2p/12D3KooWDcpWuyrMTDinqNgmXAuRdfd2mTdY9VoXZSAet2pDzh6r
|
||||
```
|
||||
|
||||
Also, if you are deploying your own service instances, you need one, or more, keypairs to deploy, authenticate and eventually delete your service. You can use `aqua-cli to create keys:
|
||||
|
||||
```bash
|
||||
aqua key create
|
||||
{
|
||||
"peerId": "12D3KooWDAM4YDz7WdAWVzxQspeZudL4rqyGmvWUePEvuERNYfLC",
|
||||
"secretKey": "SOME-SECRET-KEY",
|
||||
"publicKey": "SOME-PUB-KEY"
|
||||
}
|
||||
```
|
||||
|
||||
Let's deploy to, say, /dns4/stage.fluence.dev/tcp/19005/wss/p2p/12D3KooWAKNos2KogexTXhrkMZzFYpLHuWJ4PgoAhurSAv7o5CWA:
|
||||
|
||||
```bash
|
||||
aqua remote deploy_service \
|
||||
--addr /dns4/stage.fluence.dev/tcp/19005/wss/p2p/12D3KooWAKNos2KogexTXhrkMZzFYpLHuWJ4PgoAhurSAv7o5CWA \
|
||||
--config-path configs/deployment_cfg.json \
|
||||
--service multi-provider-query \
|
||||
--sk Bl/RLGRT4SafZyzYVXK4QufvRKTTVn+3kpNsAzDeEQU=
|
||||
```
|
||||
|
||||
Which gives up back our service id and then some:
|
||||
|
||||
```bash
|
||||
Going to upload a module...
|
||||
2022.06.22 14:53:33 [INFO] created ipfs client to /ip4/134.209.186.43/tcp/5005
|
||||
2022.06.22 14:53:33 [INFO] connected to ipfs
|
||||
2022.06.22 14:53:34 [INFO] file uploaded
|
||||
Going to upload a module...
|
||||
2022.06.22 14:53:34 [INFO] created ipfs client to /ip4/134.209.186.43/tcp/5005
|
||||
2022.06.22 14:53:34 [INFO] connected to ipfs
|
||||
2022.06.22 14:53:37 [INFO] file uploaded
|
||||
Now time to make a blueprint...
|
||||
Blueprint id:
|
||||
a046d3de2d9093df854ab97130f4d1b2dbbb6d9bbe4c445e433deb402b9a8fd3
|
||||
And your service id is:
|
||||
"b3094b93-31c5-475b-9919-3b6997461b9d"
|
||||
```
|
||||
|
||||
## Selecting Providers
|
||||
|
||||
A large number of EVM nodes are hosted by centralized providers such as [Alchemy](https://www.alchemy.com/), which isn't all that surprising how resource intensive it is to self-host nodes. In addition to hosting nodes, many hosters provide their custom API, REST, GraphQL, etc., to provide a Web2 experience and don't even offer native EVM JSON-RPC access. Alas, that provides a double pain-point for DApp developers: the introduction of a single point of failure with a high exit/migration barrier due to vendor-specific API lock-in.
|
||||
|
||||
For our purposes, we choose the following three hosting providers as they provide free EVM JSON-RPC access with a common http header approach:
|
||||
|
||||
* [Infura](https://infura.io/)
|
||||
* need to sign up for a free account and get API lkey
|
||||
* for Ethereum mainnet, our uri is:
|
||||
|
||||
* [Alchemy](https://www.alchemy.com/)
|
||||
* need to sign up for a free account and get API lkey
|
||||
* for Ethereum mainnet, our uri is:
|
||||
|
||||
* [Linkpool](https://linkpool.io/)
|
||||
* no need to sign up or need for an API key
|
||||
* is a light client limiting historic queries
|
||||
* for Ethereum mainnet, our uri is: https://main-light.eth.linkpool.io
|
||||
* does not work with "Accept: application/json" but only "Content-Type: application/json" header
|
||||
|
||||
|
||||
## Multi-Provider Queries With Aqua
|
||||
|
||||
Now that we have our adapter and providers, let's think about how we can use a multi-provider query approach to ensure a high likelihood of truth and reliability in our queries. Keep in mind that in addition to our lack of trust in each of the providers, we may also not trust the Fluence peers hosting a service. In that case, we should run every provider request on, say, three or more Fluence nodes. Moreover, we need to parallelize our requests to make sure we don't inadvertently straddle different block times and legitimately providing different responses.
|
||||
|
||||
As you may recall, Wasm modules are single threaded and concurrency of service execution is managed at the Aqua level. Parallel execution of a service may be accomplished on one or multiple nodes but either way, you need to deploy multiple service instances, e.g., for k parallel arms, you want k service instances. So in our case, we can deploy our service to, say, three (3) nodes and then execute in parallel each provider request on each of the Fluence nodes, which gives us optimal "coverage": for each request block on each node we expect the same response set. Luckily, Aqua was created exactly to easily implement complex patterns over a variety of topologies. Note that for true parallelization
|
||||
|
||||
```aqua
|
||||
-- aqua/...aqua
|
||||
|
||||
data FunctionAddress:
|
||||
peer_id: string
|
||||
service_id: string
|
||||
|
||||
data Provider:
|
||||
name: string
|
||||
url: string
|
||||
|
||||
data EVMResult:
|
||||
stdout: string
|
||||
stderr: string
|
||||
|
||||
service MultiProviderQuery("service-id"): --< interface to our Wasm service
|
||||
get_block_number(provider: ProviderInfo) -> EVMResult
|
||||
|
||||
-- utility function ot map service id to peer id since we don't have hashmaps in Aqua
|
||||
func get_service_id(node: string, services: []FunctionAddress) -> string:
|
||||
for service in services try:
|
||||
if service.peer_id == node:
|
||||
res <- service.service_id
|
||||
<- res
|
||||
|
||||
func pseudo_caller(providers: []Provider, nodes: []string, services: []FunctionAddress) -> []string:
|
||||
result: *string
|
||||
for node <- nodes par:
|
||||
on node:
|
||||
for provider <- providers par:
|
||||
MultiProviderQuery
|
||||
res <- get_block_number(provider.url)
|
||||
result <- (provider.name, node, res)
|
||||
```
|
||||
|
||||
|
||||
Time to turn to Aqua to get things done.
|
||||
|
||||
```aqua
|
||||
-- aqua/multi_provider.aqua
|
||||
|
||||
data EVMResult:
|
||||
stdout: string
|
||||
stderr: string
|
||||
|
||||
data ProviderInfo:
|
||||
url: string
|
||||
name: string
|
||||
|
||||
data Providers:
|
||||
urls: []ProviderInfo
|
||||
|
||||
data
|
||||
|
||||
service MyOp("op"):
|
||||
identity(x:string) -> string
|
||||
array_length(providers: []ProviderInfo) -> i64
|
||||
|
||||
service MultiProviderQuery("service-id"):
|
||||
get_balance(provider: ProviderInfo, account: string, block_height: string) -> EVMResult
|
||||
get_block_number(provider: ProviderInfo) -> EVMResult
|
||||
|
||||
func get_block_heights(providers: Providers, service_id: string, node: string) -> []EVMResult, i64:
|
||||
block_heights: *EVMResult
|
||||
-- n <- n_urls(providers)
|
||||
n <- MyOp.array_length(providers.urls)
|
||||
|
||||
if n > 0:
|
||||
on node:
|
||||
MultiProviderQuery service_id
|
||||
for provider <- providers.urls par:
|
||||
block_heights <- MultiProviderQuery.get_block_number(provider)
|
||||
join block_heights[n-1]
|
||||
|
||||
<- block_heights, n
|
||||
```
|
||||
|
||||
|
||||
Par
|
||||
```
|
||||
func get_block_heights(providers: Providers, service_id: string, node: string) -> []EVMResult, i64:
|
||||
block_heights: *EVMResult
|
||||
-- n <- n_urls(providers)
|
||||
n <- MyOp.array_length(providers.urls)
|
||||
|
||||
if n > 0:
|
||||
on node:
|
||||
MultiProviderQuery service_id
|
||||
for provider <- providers.urls par:
|
||||
block_heights <- MultiProviderQuery.get_block_number(provider)
|
||||
join block_heights[n-1]
|
||||
|
||||
<- block_heights, n
|
||||
```
|
||||
|
||||
|
||||
## Let's Persist Stuff
|
||||
|
||||
Coming soon.
|
73
aqua-examples/decentralized-blockchain-gateway/aqua/all.aqua
Normal file
73
aqua-examples/decentralized-blockchain-gateway/aqua/all.aqua
Normal file
@ -0,0 +1,73 @@
|
||||
data FunctionAddress:
|
||||
peer_id: string
|
||||
service_id: string
|
||||
|
||||
data Provider:
|
||||
name: string
|
||||
url: string
|
||||
|
||||
data EVMResult:
|
||||
stdout: string
|
||||
stderr: string
|
||||
|
||||
service MyOp("op"):
|
||||
-- identity(x:string) -> string
|
||||
array_length(providers: []Provider) -> i64
|
||||
-- array_length(nodes: []string) -> i64
|
||||
|
||||
service MultiProviderQuery("service-id"):
|
||||
get_block_number(provider: Provider) -> EVMResult
|
||||
|
||||
func get_service_id_no_try(node: string, services: []FunctionAddress) -> string:
|
||||
res: *string
|
||||
for service <- services:
|
||||
if service.peer_id == node:
|
||||
res <<- service.service_id
|
||||
<- res[0]
|
||||
|
||||
--func custom_result(out_string: string, err_string: string) -> EVMResult:
|
||||
-- r: *EVMResult
|
||||
-- r.stdout <- out_string
|
||||
-- r.stderr <- err_string
|
||||
-- <- r
|
||||
|
||||
--func custom_num(number: i64) -> i64:
|
||||
-- num: *i64
|
||||
-- num <<- number
|
||||
-- <- num[0]
|
||||
|
||||
|
||||
func get_service_id_try(node: string, services: []FunctionAddress) -> string:
|
||||
res: *string
|
||||
for service <- services try:
|
||||
if service.peer_id == node:
|
||||
res <<- service.service_id
|
||||
<- res[0]
|
||||
|
||||
func get_service_id_par(node: string, services: []FunctionAddress) -> string:
|
||||
res: *string
|
||||
for service <- services par:
|
||||
if service.peer_id == node:
|
||||
res <<- service.service_id
|
||||
<- res[0]
|
||||
|
||||
-- func get_block_number(providers: []Provider, nodes: []string, services: []FunctionAddress) -> []string:
|
||||
func get_block_number(providers: []Provider, nodes: []string, services: []FunctionAddress) -> []EVMResult:
|
||||
-- result: *string
|
||||
result: *EVMResult
|
||||
for node <- nodes par:
|
||||
for provider <- providers par:
|
||||
on node:
|
||||
service_id <- get_service_id_no_try(node, services)
|
||||
MultiProviderQuery service_id
|
||||
try:
|
||||
-- res <- get_block_number(provider.url)
|
||||
-- result <<- (provider.name, node, res)
|
||||
result <- MultiProviderQuery.get_block_number(provider)
|
||||
-- catch e:
|
||||
-- result <- custom_result(e)
|
||||
<- result
|
||||
|
||||
|
||||
|
||||
|
@ -0,0 +1,25 @@
|
||||
data FunctionAddress:
|
||||
peer_id: string
|
||||
service_id: string
|
||||
|
||||
func get_service_id_no_try(node: string, services: []FunctionAddress) -> string:
|
||||
res: *string
|
||||
for service <- services:
|
||||
if service.peer_id == node:
|
||||
res <<- service.service_id
|
||||
<- res[0]
|
||||
|
||||
func get_service_id_try(node: string, services: []FunctionAddress) -> string:
|
||||
res: *string
|
||||
for service <- services try:
|
||||
if service.peer_id == node:
|
||||
res <<- service.service_id
|
||||
<- res[0]
|
||||
|
||||
func get_service_id_par(node: string, services: []FunctionAddress) -> string:
|
||||
res: *string
|
||||
for service <- services par:
|
||||
if service.peer_id == node:
|
||||
res <<- service.service_id
|
||||
<- res[0]
|
||||
|
@ -0,0 +1,12 @@
|
||||
{
|
||||
"multi-provider-query": {
|
||||
"modules": [
|
||||
{
|
||||
"name": "simple-quorum",
|
||||
"path": "./artifacts/simple_quorum.wasm",
|
||||
"max_heap_size": "128 MiB",
|
||||
"logger_enabled": [true]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
170
aqua-examples/decentralized-blockchain-gateway/deployments.txt
Normal file
170
aqua-examples/decentralized-blockchain-gateway/deployments.txt
Normal file
@ -0,0 +1,170 @@
|
||||
aqua remote deploy_service \
|
||||
--addr /dns4/stage.fluence.dev/tcp/19004/wss/p2p/12D3KooWJ4bTHirdTFNZpCS72TAzwtdmavTBkkEXtzo6wHL25CtE \
|
||||
--config-path configs/deployment_cfg.json \
|
||||
--service multi-provider-query \
|
||||
--sk Bl/RLGRT4SafZyzYVXK4QufvRKTTVn+3kpNsAzDeEQU= --log-level off
|
||||
Going to upload a module...
|
||||
Going to upload a module...
|
||||
Now time to make a blueprint...
|
||||
Blueprint id:
|
||||
3bd81487cb43b76a5a67d5ebb417dd4afacb29f56d77492d8b21ad469acdfb8b
|
||||
And your service id is:
|
||||
"d9124884-3c42-43d6-9a1f-1b645d073c3f"
|
||||
|
||||
|
||||
aqua remote deploy_service \
|
||||
--addr /dns4/stage.fluence.dev/tcp/19005/wss/p2p/12D3KooWAKNos2KogexTXhrkMZzFYpLHuWJ4PgoAhurSAv7o5CWA \
|
||||
--config-path configs/deployment_cfg.json \
|
||||
--service multi-provider-query \
|
||||
--sk Bl/RLGRT4SafZyzYVXK4QufvRKTTVn+3kpNsAzDeEQU= \
|
||||
--log-level off
|
||||
Going to upload a module...
|
||||
Going to upload a module...
|
||||
Now time to make a blueprint...
|
||||
Blueprint id:
|
||||
3bd81487cb43b76a5a67d5ebb417dd4afacb29f56d77492d8b21ad469acdfb8b
|
||||
And your service id is:
|
||||
"3c321110-b069-42c6-b5e0-aed73d976a60"
|
||||
|
||||
|
||||
aqua remote deploy_service \
|
||||
--addr /dns4/stage.fluence.dev/tcp/19003/wss/p2p/12D3KooWMMGdfVEJ1rWe1nH1nehYDzNEHhg5ogdfiGk88AupCMnf \
|
||||
--config-path configs/deployment_cfg.json \
|
||||
--service multi-provider-query \
|
||||
--sk Bl/RLGRT4SafZyzYVXK4QufvRKTTVn+3kpNsAzDeEQU= \
|
||||
--log-level off
|
||||
Going to upload a module...
|
||||
Going to upload a module...
|
||||
Now time to make a blueprint...
|
||||
Blueprint id:
|
||||
3bd81487cb43b76a5a67d5ebb417dd4afacb29f56d77492d8b21ad469acdfb8b
|
||||
And your service id is:
|
||||
"84d4d018-0c13-4d6d-8c11-599a3919911c"
|
||||
|
||||
|
||||
|
||||
aqua run \
|
||||
--addr /dns4/stage.fluence.dev/tcp/19004/wss/p2p/12D3KooWJ4bTHirdTFNZpCS72TAzwtdmavTBkkEXtzo6wHL25CtE \
|
||||
-i aqua \
|
||||
-f 'get_block_heights(arg1, arg2)' \
|
||||
--data '{"arg1": [{"name":"infura", "url":"https://mainnet.infura.io/v3/0cc023286cae4ab886598ecd14e256fd"},
|
||||
{"name":"alchemy","url":"https://eth-mainnet.alchemyapi.io/v2/2FLlm9t-xOm0CbGx-ORr81li1yD_cKP6"},
|
||||
{"name":"link", "url":"https://main-light.eth.linkpool.io"}],
|
||||
"arg2": [{"peer_id":"12D3KooWJ4bTHirdTFNZpCS72TAzwtdmavTBkkEXtzo6wHL25CtE", "service_id":"d9124884-3c42-43d6-9a1f-1b645d073c3f"},
|
||||
{"peer_id":"12D3KooWAKNos2KogexTXhrkMZzFYpLHuWJ4PgoAhurSAv7o5CWA", "service_id":"3c321110-b069-42c6-b5e0-aed73d976a60"},
|
||||
{"peer_id":"12D3KooWMMGdfVEJ1rWe1nH1nehYDzNEHhg5ogdfiGk88AupCMnf", "service_id":"84d4d018-0c13-4d6d-8c11-599a3919911c"}]}' --verbose
|
||||
Particle id: c27cbca5-9e60-45d4-a78b-cff45db2444f
|
||||
[
|
||||
[
|
||||
{
|
||||
"provider": "infura",
|
||||
"stderr": "",
|
||||
"stdout": "{\"block-height\":15016714}"
|
||||
},
|
||||
{
|
||||
"provider": "alchemy",
|
||||
"stderr": "",
|
||||
"stdout": "{\"block-height\":15016714}"
|
||||
},
|
||||
{
|
||||
"provider": "link",
|
||||
"stderr": "",
|
||||
"stdout": "{\"block-height\":15016714}"
|
||||
},
|
||||
{
|
||||
"provider": "infura",
|
||||
"stderr": "",
|
||||
"stdout": "{\"block-height\":15016714}"
|
||||
},
|
||||
{
|
||||
"provider": "alchemy",
|
||||
"stderr": "",
|
||||
"stdout": "{\"block-height\":15016714}"
|
||||
},
|
||||
{
|
||||
"provider": "link",
|
||||
"stderr": "",
|
||||
"stdout": "{\"block-height\":15016702}"
|
||||
},
|
||||
{
|
||||
"provider": "infura",
|
||||
"stderr": "",
|
||||
"stdout": "{\"block-height\":15016714}"
|
||||
},
|
||||
{
|
||||
"provider": "alchemy",
|
||||
"stderr": "",
|
||||
"stdout": "{\"block-height\":15016714}"
|
||||
},
|
||||
{
|
||||
"provider": "link",
|
||||
"stderr": "",
|
||||
"stdout": "{\"block-height\":15016702}"
|
||||
}
|
||||
],
|
||||
[
|
||||
"infura",
|
||||
"alchemy",
|
||||
"link",
|
||||
"infura",
|
||||
"alchemy",
|
||||
"link",
|
||||
"infura",
|
||||
"alchemy",
|
||||
"link"
|
||||
]
|
||||
]
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
aqua remote deploy_service \
|
||||
--addr /dns4/stage.fluence.dev/tcp/19005/wss/p2p/12D3KooWAKNos2KogexTXhrkMZzFYpLHuWJ4PgoAhurSAv7o5CWA \
|
||||
--config-path configs/deployment_cfg.json \
|
||||
--service simple-quorum \
|
||||
--sk Bl/RLGRT4SafZyzYVXK4QufvRKTTVn+3kpNsAzDeEQU= \
|
||||
--log-level off
|
||||
Going to upload a module...
|
||||
Now time to make a blueprint...
|
||||
Blueprint id:
|
||||
90bd4e39ae16c7a8407d3b770952bf708d7f3f9f98ab815252f81adc45d2ca63
|
||||
And your service id is:
|
||||
"b348351e-485c-4424-b0a9-3e4304ce9431"
|
||||
|
||||
|
||||
aqua run \
|
||||
--addr /dns4/stage.fluence.dev/tcp/19004/wss/p2p/12D3KooWJ4bTHirdTFNZpCS72TAzwtdmavTBkkEXtzo6wHL25CtE \
|
||||
-i aqua \
|
||||
-f 'get_block_height_oracle(arg1, arg2, arg3)' \
|
||||
--data '{"arg1": [{"name":"infura", "url":"https://mainnet.infura.io/v3/0cc023286cae4ab886598ecd14e256fd"},
|
||||
{"name":"alchemy","url":"https://eth-mainnet.alchemyapi.io/v2/2FLlm9t-xOm0CbGx-ORr81li1yD_cKP6"},
|
||||
{"name":"link", "url":"https://main-light.eth.linkpool.io"}],
|
||||
"arg2": [{"peer_id":"12D3KooWJ4bTHirdTFNZpCS72TAzwtdmavTBkkEXtzo6wHL25CtE", "service_id":"d9124884-3c42-43d6-9a1f-1b645d073c3f"},
|
||||
{"peer_id":"12D3KooWAKNos2KogexTXhrkMZzFYpLHuWJ4PgoAhurSAv7o5CWA", "service_id":"3c321110-b069-42c6-b5e0-aed73d976a60"},
|
||||
{"peer_id":"12D3KooWMMGdfVEJ1rWe1nH1nehYDzNEHhg5ogdfiGk88AupCMnf", "service_id":"84d4d018-0c13-4d6d-8c11-599a3919911c"}],
|
||||
"arg3": {"peer_id":"12D3KooWAKNos2KogexTXhrkMZzFYpLHuWJ4PgoAhurSAv7o5CWA", "service_id":"8e2c0464-b066-4a53-95d5-8dc116b92909"}}'
|
||||
|
||||
|
||||
aqua run \
|
||||
--addr /dns4/stage.fluence.dev/tcp/19004/wss/p2p/12D3KooWJ4bTHirdTFNZpCS72TAzwtdmavTBkkEXtzo6wHL25CtE \
|
||||
-i aqua \
|
||||
-f 'get_block_height_quorum(arg1, arg2, arg3, 0.66)' \
|
||||
--data '{"arg1": [{"name":"infura", "url":"https://mainnet.infura.io/v3/0cc023286cae4ab886598ecd14e256fd"},
|
||||
{"name":"alchemy","url":"https://eth-mainnet.alchemyapi.io/v2/2FLlm9t-xOm0CbGx-ORr81li1yD_cKP6"},
|
||||
{"name":"link", "url":"https://main-light.eth.linkpool.io"}],
|
||||
"arg2": [{"peer_id":"12D3KooWJ4bTHirdTFNZpCS72TAzwtdmavTBkkEXtzo6wHL25CtE", "service_id":"d9124884-3c42-43d6-9a1f-1b645d073c3f"},
|
||||
{"peer_id":"12D3KooWAKNos2KogexTXhrkMZzFYpLHuWJ4PgoAhurSAv7o5CWA", "service_id":"3c321110-b069-42c6-b5e0-aed73d976a60"},
|
||||
{"peer_id":"12D3KooWMMGdfVEJ1rWe1nH1nehYDzNEHhg5ogdfiGk88AupCMnf", "service_id":"84d4d018-0c13-4d6d-8c11-599a3919911c"}],
|
||||
"arg3": {"peer_id":"12D3KooWAKNos2KogexTXhrkMZzFYpLHuWJ4PgoAhurSAv7o5CWA", "service_id":"b348351e-485c-4424-b0a9-3e4304ce9431"}}'
|
||||
[
|
||||
{
|
||||
"err_str": "",
|
||||
"freq": 9,
|
||||
"mode": 15031112,
|
||||
"n": 9
|
||||
},
|
||||
true
|
||||
]
|
||||
|
||||
|
@ -0,0 +1,11 @@
|
||||
|
||||
https://mainnet.infura.io/v3/0cc023286cae4ab886598ecd14e256fd
|
||||
|
||||
https://eth-mainnet.alchemyapi.io/v2/2FLlm9t-xOm0CbGx-ORr81li1yD_cKP6
|
||||
|
||||
https://main-light.eth.linkpool.io
|
||||
|
||||
|
||||
|
||||
|
||||
curl https://main-light.eth.linkpool.io -X POST -d '{"method":"eth_blockNumber","params":[],"id":1,"jsonrpc":"2.0"}' -H "Content-Type: application/json"
|
@ -0,0 +1,13 @@
|
||||
aqua remote deploy_service \
|
||||
--addr krasnodar-05 \
|
||||
--config-path configs/deployment_cfg.json \
|
||||
--service multi-provider-query \
|
||||
--sk Bl/RLGRT4SafZyzYVXK4QufvRKTTVn+3kpNsAzDeEQU= \
|
||||
--log-level off
|
||||
Going to upload a module...
|
||||
Going to upload a module...
|
||||
Now time to make a blueprint...
|
||||
Blueprint id:
|
||||
2d09fa0c29ccf8e00bac853554e9edbbd94ac13efbb5349982449c95381f76c4
|
||||
And your service id is:
|
||||
"ba0da956-294e-496f-bf47-893ab3224b13"
|
@ -0,0 +1,24 @@
|
||||
[package]
|
||||
name = "multi_provider_query"
|
||||
version = "0.1.5"
|
||||
authors = ["boneyard93501 <4523011+boneyard93501@users.noreply.github.com>"]
|
||||
edition = "2018"
|
||||
description = "multi-provider-query, a Marine wasi module"
|
||||
license = "Apache-2.0"
|
||||
|
||||
[[bin]]
|
||||
name = "multi_provider_query"
|
||||
path = "src/main.rs"
|
||||
|
||||
[dependencies]
|
||||
marine-rs-sdk = { version = "0.6.15", features = ["logger"] }
|
||||
log = "0.4.14"
|
||||
serde_json = "1.0.81"
|
||||
serde = "1.0.137"
|
||||
|
||||
[dev-dependencies]
|
||||
marine-rs-sdk-test = "0.6.0"
|
||||
|
||||
[dev]
|
||||
[profile.release]
|
||||
opt-level = "s"
|
@ -0,0 +1,40 @@
|
||||
/*
|
||||
* Copyright 2022 Fluence Labs Limited
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
use marine_rs_sdk::{marine, module_manifest, MountedBinaryResult};
|
||||
|
||||
module_manifest!();
|
||||
|
||||
fn main() {}
|
||||
|
||||
#[marine]
|
||||
pub struct ProviderInfo {
|
||||
pub url: String,
|
||||
}
|
||||
|
||||
#[marine]
|
||||
pub struct EVMResult {
|
||||
pub stdout: String,
|
||||
pub stderr: String,
|
||||
}
|
||||
|
||||
#[marine]
|
||||
fn count_providers(providers: Vec<ProviderInfo>) -> u64 {
|
||||
providers.len() as u64
|
||||
}
|
||||
|
||||
#[marine]
|
||||
fn provider_results(results: Vec<EVMResult>) {}
|
5
aqua-examples/decentralized-blockchain-gateway/utilities/.gitignore
vendored
Normal file
5
aqua-examples/decentralized-blockchain-gateway/utilities/.gitignore
vendored
Normal file
@ -0,0 +1,5 @@
|
||||
debug/
|
||||
target/
|
||||
Cargo.lock
|
||||
**/*.bk
|
||||
**/*.bak
|
@ -0,0 +1,22 @@
|
||||
[package]
|
||||
name = "utilities"
|
||||
version = "0.1.5"
|
||||
authors = ["boneyard93501 <4523011+boneyard93501@users.noreply.github.com>"]
|
||||
edition = "2018"
|
||||
description = "utilities, a Marine wasi module"
|
||||
license = "Apache-2.0"
|
||||
|
||||
[[bin]]
|
||||
name = "utilities"
|
||||
path = "src/main.rs"
|
||||
|
||||
[dependencies]
|
||||
marine-rs-sdk = { version = "0.6.15", features = ["logger"] }
|
||||
log = "0.4.14"
|
||||
|
||||
[dev-dependencies]
|
||||
marine-rs-sdk-test = "0.6.0"
|
||||
|
||||
[dev]
|
||||
[profile.release]
|
||||
opt-level = "s"
|
@ -0,0 +1,6 @@
|
||||
modules_dir = "artifacts/"
|
||||
|
||||
[[module]]
|
||||
name = "utilities"
|
||||
max_heap_size = "10 KiB"
|
||||
logger_enabled = true
|
@ -0,0 +1,18 @@
|
||||
# Marine Starter Template
|
||||
|
||||
Get started quickly with your Wasm project with this template.
|
||||
To use this template you should have the following components available:
|
||||
|
||||
* Rust [installation instructions](https://www.rust-lang.org/tools/install)
|
||||
* Cargo Generate [installation instructions](https://github.com/cargo-generate/cargo-generate#installation)
|
||||
* Marine [installation instructions](https://github.com/cargo-generate/cargo-generate#installation)
|
||||
|
||||
Use the template with `cargo generate`:
|
||||
|
||||
```bash
|
||||
cargo generate cargo generate --git https://github.com/boneyard93501/fluence-template.git
|
||||
```
|
||||
|
||||
Which will prompt you for the project name and then generate the project, which will be your directory name. `cd` into your new directory and run `./scripts/build.sh` to compile the supplied __greeting__ function to a Wasm file ready for deployment.
|
||||
|
||||
Check out the [Fluence documentation](https://doc.fluence.dev/docs/) for more details on Marine and Aqua.
|
@ -0,0 +1,11 @@
|
||||
{
|
||||
"utilities": {
|
||||
"name": "utilities",
|
||||
"modules": [
|
||||
{
|
||||
"name": "utilities",
|
||||
"path": "./artifacts/utilities.wasm"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
@ -0,0 +1,7 @@
|
||||
#!/usr/bin/env bash -o errexit -o nounset -o pipefail
|
||||
cargo update --aggressive
|
||||
|
||||
mkdir -p artifacts
|
||||
rm -f artifacts/*.wasm
|
||||
marine build --release
|
||||
cp target/wasm32-wasi/release/utilities.wasm artifacts/
|
@ -0,0 +1,34 @@
|
||||
/*
|
||||
* Copyright 2022 Fluence Labs Limited
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
use marine_rs_sdk::{marine, module_manifest, WasmLoggerBuilder};
|
||||
|
||||
module_manifest!();
|
||||
|
||||
pub fn main() {
|
||||
WasmLoggerBuilder::new().build().unwrap();
|
||||
}
|
||||
|
||||
#[marine]
|
||||
pub struct CustomResult {
|
||||
stdout: String,
|
||||
stderr: String,
|
||||
provider_name: String,
|
||||
error: bool,
|
||||
}
|
||||
|
||||
#[marine]
|
||||
pub fn to_struct(name: String, stdout: String, stderr: String) -> String {}
|
Loading…
Reference in New Issue
Block a user