├── .gitignore
├── .gitmodules
├── Cargo.lock
├── Cargo.toml
├── README.md
├── advanced_judger
├── Cargo.toml
└── lib.rs
├── bin
├── advanced_judger
│ ├── advanced_judger.contract
│ ├── advanced_judger.wasm
│ └── metadata.json
├── easy_oracle
│ ├── easy_oracle.contract
│ ├── easy_oracle.wasm
│ └── metadata.json
└── fat_badges
│ ├── fat_badges.contract
│ ├── fat_badges.wasm
│ └── metadata.json
├── easy_oracle
├── Cargo.toml
└── lib.rs
├── fat_badges
├── Cargo.toml
└── lib.rs
├── rust-toolchain.toml
├── scripts
├── build.sh
├── collect-bin.sh
└── js
│ ├── .gitignore
│ ├── package.json
│ ├── src
│ ├── common.js
│ ├── deploy.js
│ ├── e2e.js
│ └── utils.js
│ └── yarn.lock
└── utils
├── Cargo.toml
└── environmental
├── .gitignore
├── Cargo.toml
├── LICENSE
├── README.adoc
└── src
├── lib.rs
└── local_key.rs
/.gitignore:
--------------------------------------------------------------------------------
1 | # Ignore build artifacts from the local tests sub-crate.
2 | /target/
3 |
4 | # Ignore backup files creates by cargo fmt.
5 | **/*.rs.bk
6 |
7 | /tmp/
--------------------------------------------------------------------------------
/.gitmodules:
--------------------------------------------------------------------------------
1 | [submodule "vendor/openbrush-contracts"]
2 | path = vendor/openbrush-contracts
3 | url = https://github.com/Phala-Network/openbrush-contracts.git
4 | branch = cross-contract
--------------------------------------------------------------------------------
/Cargo.toml:
--------------------------------------------------------------------------------
1 | [workspace]
2 | resolver = "2"
3 |
4 | members = [
5 | "fat_badges",
6 | "easy_oracle",
7 | "advanced_judger",
8 | "utils/environmental",
9 | ]
10 |
11 | exclude = [
12 | "vendor/openbrush-contracts",
13 | ]
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Deprecated
2 |
3 | This workshop is no longer maintained. Please check our updated workshop: [Sub0 2022 Oracle Workshop](https://github.com/Phala-Network/phat-offchain-rollup/blob/main/phat/Sub0-Workshop.md)
4 |
5 | ----
6 |
7 | # Fat Contract Oracle Workshop
8 |
9 | _First created for Polkadot Decoded 2022 with the subtitle "The Web3 infrastructure beyond smart contracts: Build an oracle in 15 mins with ink!"_ ([Slides](https://docs.google.com/presentation/d/1HjmQCSvwpc7gwaCU2W_5yHA3gTIrg49SpSUZ4gLZwD8/edit?usp=sharing))
10 |
11 | ## What you can learn
12 |
13 | There are the beginner challenge and the advanced challenge in the workshop. In the beginner challenge, you are going to play with the oracle built on Phala Fat Contract. In the advanced challenge, you are going to learn how to build an oracle that:
14 |
15 | 1. links off-chain identity to blockchain
16 | 2. sends HTTP requests to verify off-chain data
17 | 3. gives out [POAP NFT](https://poap.xyz/) rewards
18 | 4. is written in [ink!](https://ink.substrate.io/)
19 | 5. (and can be built in 15 mins)
20 |
21 | ## Bonus
22 |
23 | When you have solved a challenge successfully, you can earn a beautiful POAP.
24 |
25 | | Beginner POAP | Advanced PAOP |
26 | | -------- | -------- |
27 | |  |  |
28 |
29 | ---
30 |
31 | # Beginner challenge
32 |
33 | Before a deep dive into how an oracle is made, let's try out a very simple oracle demo first. In this demo, you will be asked to post a message to Github to prove your ownership of the account. The Fat Contract will verify your post from Github. Once the verification is passed, you will win a nice POAP as a bonus!
34 |
35 |
36 |
37 | ## Step-by-step
38 |
39 | In this challenge, you will interact with the workshop DApp in your browser. Before starting, please make sure you have:
40 |
41 | 1. [Polkadot.js Browser Extension](https://polkadot.js.org/extension/)
42 | 3. [Github](https://github.com/) account
43 |
44 | > Some Polkadot.js Extension compatible extensions may also work, but this tutorial is only tested with the official extension.
45 |
46 | If you haven't done it yet, please [generate an account](https://wiki.polkadot.network/docs/learn-account-generation#polkadotjs-browser-extension) in your Polkadot.js Extension. Otherwise, we are ready to go!
47 |
48 | Open the [Workshop DApp](https://phala-decoded-2022.netlify.app), and enter the _Easy Challenge_ page.
49 |
50 | 
51 |
52 | On the _Easy Challenge_ page, the browser will immediately pop up an _Authorize_ window. Click _Yes_ to allow the authorization. Then you can click the _Select Account_ drop-down to connect to an account.
53 |
54 | On this page, you can request the faucet to get some test tokens by _Get 10 PHA_ button (under the drop-down). Please do if you haven't done it yet. The operations below require tokens as the transaction fee.
55 |
56 | 
57 |
58 | Now, let's click _Sign Certificate_. It will ask you to sign a _certificate_ that is used to interact with the contracts. Once it's finished, it will show you the DApp UI like below.
59 |
60 | 
61 |
62 | The DApp asks you to create a Github Gist with the given text. You can follow the Github link on the page to create a gist. You should paste the text it gives you as the content of the gist, and submit it. The filename and the title don't matter. Both public and private gist work.
63 |
64 | 
65 |
66 | Once the gist is created, open it in the _raw format_ (as highlighted in the screenshot below). Then copy the URL of the raw gist, and paste it to section 2 in the DApp. Please note that the raw gist URL should match the following pattern:
67 |
68 | ```
69 | https://gist.githubusercontent.com//.../raw/...
70 | ```
71 |
72 | Then, click _Verify_ to submit the URL as proof. If the verification is passed, you will be asked to sign a transaction to redeem the PAOP code. The transaction may take up to half a minute to complete. When you get the prompt saying the transaction is finalized, you can follow the _FatBadges_ link in section 3 to check your redeem code.
73 |
74 | On this page, you will need to sign the certificate again. Then click _Load_ button, and it will show you your PAOP redeem code as well as the basic stats of the challenges.
75 |
76 | 
77 |
78 | Congratulations! Now you should be able to use the redeem code to get your shining NFT!
79 |
80 | Want to know how it works? We will cover this in the next section.
81 |
82 | ## Build an oracle in Fat Contract
83 |
84 | ### Prerequests
85 |
86 | To read this section, it's suggested to have a basic understanding of the following concepts:
87 |
88 | 1. Smart contracts
89 | 2. Oracles
90 | 3. Blockchain Consensus
91 |
92 | ### The way to scale oracles
93 |
94 | Existing oracles don't scale. For instance, ChainLink is the most commonly used oracle. It supports only 12 EVM blockchains, and they struggle to add long-tail support. On the other hand, existing oracles often serve very limited data sources and functions (price feed and VRF). Though there are rich data available on the internet, none of the oracle is flexible enough to process this data and make it useful to blockchain apps.
95 |
96 | The key to scale oracle is the ability to make its logic programmable. Thinking about building a customized oracle as easy as writing smart contracts, it would be easy to support all the long-tail blockchain and use cases.
97 |
98 | Unfortunately, traditional oracles are not good at this because of their technical limitation. Oracle brings outside information to a blockchain. It must run off-chain because a blockchain can never access the data beyond its consensus system by itself. Without the consensus systems, the security guarantee disappears of a sudden. As a result, a decentralized off-chain project needs to take care to design the mechanism to ensure the correctness and the liveness of the network. Often, we cannot always find the mechanism that applies to all kinds of logic. Not to mention we may need to spend from months to years to implement it.
99 |
100 | Is it possible to build a scalable oracle efficiently at all? It turns out possible, but we need an off-chain execution environment with:
101 |
102 | 1. Internet access: it enables access to all the data available around the world
103 | 2. Easy integration: can easily expand to long-tail blockchains in hours
104 | 3. Off-chain security: running off-chain, but still as secure as on-chain apps
105 |
106 | Fat Contract is designed to meet all these requirements! As the decentralized cloud for Web3, Phala allows you to deploy the customized program to access the internet and report data to any blockchain.
107 |
108 | ## Fat Contract introduction
109 |
110 | Fat Contract is the programming model designed for Phala cloud computation. It has some similarities to smart contracts but is fundamentally different from smart contracts.
111 |
112 | To help understand the programming model, let's first learn how Phala works. Phala Network is a network with thousands of secure off-chain workers. The workers are available to deploy apps. Unlike the fully redundant nodes in blockchain, Phala workers run their app in parallel. The developer can pay one or more workers to deploy their app, just like how they deploy apps on the traditional serverless cloud.
113 |
114 | This is possible because the workers are secure enough to run apps separately without involving blockchain consensus. In other words, Fat Contract is fully off-chain computation. This gives us the following advantages:
115 |
116 | 1. Support internet access: Fat Contract provides API to send HTTP requests.
117 | 2. Low latency and CPU-intensive computation
118 | 3. Keep secrets: states and communications are protected by default
119 |
120 | > Wanna know why Phala Network's workers are secure and confidentiality preserving? Please check out [wiki](https://wiki.phala.network/en-us/learn/phala-blockchain/blockchain-detail/).
121 |
122 | With the above features, we can build decentralized oracles as easily as writing a backend app. In fact, in the advanced challenge, we are going to show you how to build and deploy a customized oracle in 15 mins.
123 |
124 | ### Basics
125 |
126 | Fat Contract is based on [Parity ink!](https://ink.substrate.io/) and fully compatible with ink!. It has some special extensions and differences in usage to better support the unique features. Most of the time, developing Fat Contract is the same as writing ink! smart contract. So we strongly suggest learning ink! with [the official documentation](https://ink.substrate.io/) first. In this section, we will only cover the basic structure of a contract.
127 |
128 | Let's look into the similar part first. In a typical ink! contract, you are going to define the storage and the methods of a smart contract:
129 |
130 | ```rust
131 | #[ink(storage)]
132 | pub struct EasyOracle {
133 | admin: AccountId,
134 | linked_users: Mapping,
135 | }
136 |
137 |
138 | impl EasyOracle {
139 | #[ink(constructor)]
140 | pub fn new() -> Self {
141 | let admin = Self::env().caller();
142 | ink_lang::utils::initialize_contract(|this: &mut Self| {
143 | this.admin = admin;
144 | })
145 | }
146 |
147 | #[ink(message)]
148 | fn admin(&self) -> AccountId {
149 | self.admin.clone()
150 | }
151 |
152 | #[ink(message)]
153 | pub fn redeem(&mut self, attestation: Attestation) -> Result<()> {
154 | // ...
155 | }
156 | }
157 | ```
158 |
159 | This example is taken from the [`EasyOracle`](https://github.com/Phala-Network/oracle-workshop/blob/3fe330fcdfef8f088896c3fba07c9bc79ccecea5/easy_oracle/lib.rs) contract. In the code above, we have defined a contract with two storage items accessible in the contract methods. It has three methods. The first one, `new()` is a **constructor** to instantiate a contract. In the constructor, we save the caller as the admin of the contract in the storage. The second method `admin()` is a **query** to return the admin account. Queries can read the storage, but cannot write to the storage (notice the immutable `&self` reference). The third method `redeem()` is a **transaction** method (or called "command" in Fat Contract). Transaction methods can read and write the storage.
160 |
161 | It's important to understand the types of methods. **Constructors** and **transaction** methods are only triggered by on-chain transactions. Although the benefit is you can write to the storage, they are slow and expensive, because you always need to send a transaction. Additionally, advanced features like HTTP requests are not available in these methods.
162 |
163 | The most interesting part of Fat Contract is the **queries**. Despite the limitation that you can only read the storage, queries give you a lot of benefits:
164 |
165 | 1. Access to HTTP requests
166 | 2. Low latency: the queries are sent to the worker directly and you can get the response immediately, without waiting for blocks
167 | 3. Free to call: queries doesn't charge any gas fee
168 |
169 | We are going to combine queries and transactions to build the oracle.
170 |
171 | ### The primitives to build an oracle
172 |
173 | The [`EasyOracle`](https://github.com/Phala-Network/oracle-workshop/blob/3fe330fcdfef8f088896c3fba07c9bc79ccecea5/easy_oracle/lib.rs) Fat Contract asks you to post a special statement on Github to verify your ownership of the Github account. By sending a statement like the below, we can ensure the Github account is controlled by yourself:
174 |
175 | ```
176 | This gist is owned by address: 0xd43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d
177 | ```
178 |
179 | #### HTTP request
180 |
181 | To verify the ownership, the Fat Contract needs to send an HTTP request to the Github Gist server, and check if the content of the gist matches the caller. This can be done in a query like this:
182 |
183 | ```rust
184 | // Verify the URL
185 | let gist_url = parse_gist_url(&url)?;
186 | // Fetch the gist content
187 | let response = http_get!(url);
188 | if response.status_code != 200 {
189 | return Err(Error::RequestFailed);
190 | }
191 | let body = resposne.body;
192 | ```
193 |
194 | The `http_get!` macro is provided by the Fat Contract API [`pink_extension::http_get`](https://docs.rs/pink-extension/0.1.9/pink_extension/macro.http_get.html). It allows the developer to send a `GET` or `POST` request in queries. If it's not in a query, the execution will fail because it violates the determinism rule.
195 |
196 | #### Attestation
197 |
198 | We can send the HTTP request and verify the response in a query. However, it's not allowed to mutate the contract storage. How can we bring the verification result back to the blockchain to trigger the next step logic?
199 |
200 | The answer is [**off-chain attestation**](https://ethereum.org/en/decentralized-identity/#off-chain-attestations). This is a useful pattern that allows users to submit data from queries to the blockchain (to a transaction method, or even an external independent blockchain).
201 |
202 | Fat Contract provides the `attestation` utils to easily generate and verify the attestation.
203 |
204 | ```rust
205 | // Generate a key pair
206 | let (generator, verifier) = attestation::create(b"salt");
207 |
208 | // In a query
209 | let payload = SomeData { ... };
210 | let cert = generator.sign(payload)?;
211 |
212 | // In an on-chain transaction
213 | if let Some(payload) = verifier.verify_as(cert)? {
214 | // Verification passed
215 | }
216 | ```
217 |
218 | Under the hood, an attestation is just a payload signed with a private key. The private key is usually generated by the contract constructor. As only the Fat Contract holds the private key, the signature proves that the data in the payload is created by the contract, and the integrity is guaranteed. When we want to verify the attestation, we simply verify the signature.
219 |
220 |
221 | #### Access control
222 |
223 | Access control is a special feature of Fat Contract. In Fat Contract, the states and the communication are confidentiality-preserving by default. Users can only read encrypted data from the blockchain but cannot guess the plain text. The only way to reveal data to the user is by queries.
224 |
225 | In Fat Contract, queries are signed by the user's wallet. This makes it possible to check the role of the user before responding to the query. We can write an easy access control logic like this:
226 |
227 | ```rust
228 | if self.env().caller() != self.admin {
229 | return Err(Error::BadOrigin);
230 | }
231 | return self.actual_data;
232 | ```
233 |
234 | We are going to leverage this feature to store some POAP redeem codes on the blockchain, and distribute the code to the challenge winners only.
235 |
236 | ### Put everything together
237 |
238 | With the HTTP request, off-chain attestation, and access control, we can finally build a full oracle that can check your ownership of a Github account, and produce proof to redeem a POAP code on the blockchain.
239 |
240 | To learn more about the implementation, we suggest reading the following Fat Contracts:
241 |
242 | 1. [`EasyOracle`](https://github.com/Phala-Network/oracle-workshop/blob/3fe330fcdfef8f088896c3fba07c9bc79ccecea5/easy_oracle/lib.rs): The oracle to attest your Github ownership
243 | 2. [`FatBadges`](https://github.com/Phala-Network/oracle-workshop/blob/master/fat_badges/lib.rs): The contract to distribute POAP NFT "badges" to challenge winners.
244 |
245 | To get started, please check the tutorial for the Advanced Challenge.
246 |
247 | ## Resources
248 |
249 | - Decoded Workshop DApp:
250 | - Github Repo:
251 | - Understand the certificate: _TODO_
252 | - Support
253 | - Discord (dev group):
254 | - Polkadot StackExchange (tag with `phala` and `fat-contract`):
255 |
256 | ---
257 |
258 | # Advanced Challenge
259 |
260 | At this point, you should be already familiar with the basics of Fat Contract. If not, please go back to the Beginner Challenge section.
261 |
262 | In the advanced challenge, you are going to learn:
263 |
264 | - How to build your oracle in Fat Contract
265 | - Deploy the oracle to the Phala Testnet
266 | - Use Fat Contract UI to play with your contracts
267 |
268 | And finally, once your on-chain submission is successful, you are going to earn a nice Advanced Challenge Winner PAOP!
269 |
270 |
271 |
272 | ## Step-by-step
273 |
274 | ### Prerequets
275 |
276 | You need the Polkadot.js browser extension as required in the Beginner Challenge. Additionally, this challenge requires you to install the development environment.
277 |
278 | An operating system of macOS or Linux systems like Ubuntu 20.04/22.04 is recommended for the workshop.
279 |
280 | - For macOS users, we recommend using the *Homebrew* package manager to install the dependencies
281 | - For other Linux distribution users, use the package manager with the system like Apt/Yum
282 |
283 | The following toolchains are needed:
284 |
285 | - Rust toolchain
286 | - Install rustup, rustup is the "package manager" of different versions of Rust compilers: `curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh`
287 | - This will install `rustup` and `cargo`
288 | - Ink! Contract toolchain
289 | - Install [binaryen](https://github.com/WebAssembly/binaryen) with
290 | - Homebrew for macOS: `brew install binaryen`
291 | - For Linux / Unix, download the latest version from [the Github release page](https://github.com/WebAssembly/binaryen/releases) and put it under your `$PATH`
292 | - **Note**: Linux package managers may download legacy binaryen. We strongly suggest installing the latest binary from the Github release page listed above.
293 | - Install dylint-link toolchain: `cargo install cargo-dylint dylint-link`
294 | - Install contract toolchain: `cargo install cargo-contract --force`
295 | - For macOS M1 chip users: `rustup component add rust-src --toolchain nightly-aarch64-apple-darwin`
296 | - Install the frontend toolchain (if you want to hack the frontend as well)
297 | - Node.js (>=v16), follow the [official tutorial](https://nodejs.org/en/download/package-manager/)
298 | - Yarn (v1): `npm install --global yarn`
299 |
300 | Check your installation with
301 |
302 | ```bash
303 | $ rustup toolchain list
304 | # stable-x86_64-unknown-linux-gnu (default)
305 | # nightly-x86_64-unknown-linux-gnu
306 |
307 | $ cargo --version
308 | # cargo 1.58.0 (f01b232bc 2022-01-19)
309 |
310 | $ cargo contract --version
311 | # cargo-contract 0.17.0-unknown-x86_64-linux-gnu
312 |
313 | $ node --version
314 | # v17.5.0
315 |
316 | $ yarn --version
317 | # 1.22.17
318 | ```
319 |
320 | ### Compile a contract
321 |
322 | Clone and initialize the workshop git repo:
323 |
324 | ```bash
325 | git clone https://github.com/Phala-Network/oracle-workshop.git
326 | cd oracle-workshop
327 | git submodule update --init
328 | ```
329 |
330 | Build the `EasyOracle` contract:
331 |
332 | ```bash
333 | cd easy_oracle
334 | cargo contract build
335 | ```
336 |
337 | A successful run should output a similar log in the console:
338 |
339 | ```
340 | Original wasm size: 83.2K, Optimized: 43.9K
341 |
342 | The contract was built in DEBUG mode.
343 |
344 | Your contract artifacts are ready. You can find them in:
345 | /home/workshop/oracle-workshop/target/ink/easy_oracle
346 |
347 | - easy_oracle.contract (code + metadata)
348 | - easy_oracle.wasm (the contract's code)
349 | - metadata.json (the contract's metadata)
350 | ```
351 |
352 | Once the contract is built, you can find the contract artifacts under `target/ink/easy_oracle`. It will produce three files:
353 |
354 | - `easy_oracle.wasm`: The wasm binary
355 | - `metadata.json`: The generated ABI file, useful to work with the js-sdk
356 | - `easy_oracle.contract`: The JSON bundle with the content of the above two files, useful to work with Fat Contract UI
357 |
358 | There are three contracts in the workshop repo. In this workshop, you only need to work with `EasyOracle`. However, if you want to build the other two contracts, you need to `cd` to their directory and run `cargo contract build` separately.
359 |
360 | ### Hack the contract
361 |
362 | The `EasyOracle` is ready to hack. A simple idea is to change it from verifying the Github account to a Twitter account, where you can use Twitter's [lookup api](https://developer.twitter.com/apitools/api?endpoint=%2F2%2Ftweets%2F%7Bid%7D&method=get) to get a JSON response of the tweet content and the author username.
363 |
364 | > Note that Twitter API requires authentication. You can [generate a bearer token](https://developer.twitter.com/en/docs/authentication/oauth-2-0) and seal it in the contract.
365 |
366 | > You may also want to use a JSON deserializer in your Fat Contract. However due to [some limitation of ink!](https://substrate.stackexchange.com/a/3325/544), you may want to use [serde-json-core](https://docs.rs/serde-json-core/latest/serde_json_core/) to bypass the float point problem.
367 |
368 | To pass the Advanced Challenge, you need to make sure:
369 |
370 | - The contract implements the`SubmittableOracle` trait (already done in `EasyOracle`)
371 | - The contract returns the owner account in method `admin()`
372 | - The contract returns the attestation verifier in method `verifier()`
373 | - The contract can generate a valid attestation in method `attest()`
374 |
375 | #### Running tests
376 |
377 | Once you started to hack, unit tests are your best friend to test your contract. Running a unit test is a little bit different from ink! in this workshop:
378 |
379 | ```bash
380 | cargo test --features mockable
381 | ```
382 |
383 | When you are in trouble, consider enabling stack backtrace by tweaking the env var:
384 |
385 | ```bash
386 | RUST_BACKTRACE=1 cargo test --features mockable
387 | ```
388 |
389 | And sometimes when you want to use Fat Contract's logger in the unit test:
390 |
391 | ```bash
392 | RUST_BACKTRACE=1 cargo test --features mockable -- --nocapture
393 | ```
394 |
395 | #### Test the HTTP requests
396 |
397 | Fat Contract supports HTTP requests, but it may not be a good idea to trigger a request in a unit test. It's suggested to mock the response in a unit test like below. Then all the requests in the contract will get the mock response from your function.
398 |
399 | ```rust
400 | mock::mock_http_request(|_| {
401 | HttpResponse::ok(b"This gist is owned by address: 0x0101010101010101010101010101010101010101010101010101010101010101".to_vec())
402 | });
403 | ```
404 |
405 | #### Openbrush library and "trait_definition"
406 |
407 | Openbrush is the "OpenZeppelin" in ink! ecosystem. It has a lot of useful utilities and macros to help you build the contract. Openbrush is used in `EasyOracle` and some other contracts to facilitate the cross-contract call and unit tests.
408 |
409 | > TODO: Need a comprehensive explanation.
410 |
411 | `trait_definition` is a powerful tool to define a common interface for cross-contract invocation in ink!. For now, you can check the following topics:
412 |
413 | - [[trait_definition] in the Official Docs](https://ink.substrate.io/basics/trait-definitions)
414 | - [Openbrush](https://github.com/Supercolony-net/openbrush-contracts/)
415 | - [Discussion about cross-contract call in unit tests](https://github.com/Supercolony-net/openbrush-contracts/pull/136)
416 |
417 | ### Deploy and configure the contract
418 |
419 | Once you have finished the test and want to run it in the real testnet, you can start to deploy the contract.
420 |
421 | First, compile the contract by `cargo contract build`. Then you can save the `.contract` file for deployment.
422 |
423 | Open the [Contracts UI](https://phat.phala.network/). It will show a popup to connect to Polkdot.js extension. Please accept the request, and connect to your wallet. In the connect popup, make sure to connect to the testnet RPC endpoint, and select an account with some test token:
424 |
425 | ```
426 | wss://poc5.phala.network/ws
427 | ```
428 |
429 | 
430 |
431 | Now you can drag-n-drop your `.contract` file to the upload area. Please leave the _Cluster ID_ the default value.
432 |
433 | 
434 |
435 | When the contract is loaded, it will show the constructor selector. If you haven't especially changed it, just use the default constructor (`new`).
436 |
437 | 
438 |
439 | Then click _Submit_. The Contract UI will upload your contract to the blockchain and instantiate it. The process may take half a minute to complete. Once it's ready, go back to the homepage, and your contract will show up.
440 |
441 | 
442 |
443 | Click on your contract to enter the contract console page. You will see the important contract information like the **CONTRACT ID** (the address of your contract instance).
444 |
445 | 
446 |
447 | In the body of the page, you can interact with the contract transaction and query methods.
448 |
449 | To invoke a transaction method (with a `TX` tag), you are going to submit a transaction with your wallet, but you don't know the outcome of the transaction. Usually, you also need to query the contract (with a `QUERY` tag) to check its status. The query response will show in the output panel as shown below.
450 |
451 | 
452 |
453 | #### Submit your solution
454 |
455 | Before the submission, please make sure your contract can meet the submission criteria described in the "Hack the contract" section, and that it's deployed on the public testnet.
456 |
457 | Open the [Decoded Workshop Dapp]() and switch to the _Advanced Challenge_ page. Fill in the contract id and a valid argument for your `attest()` method, and click the _Verify_ button. The judger will call the `attest()` method with the given arg in your oracle, and check if your submission meets the criteria.
458 |
459 | 
460 |
461 | If it turns out your submission passed the verification, congratulations, you will win an Advanced Challenge Winner POAP! Get your code on the FatBadges page, and redeem it!
462 |
463 | #### (Optional) Issue badge from your oracle
464 |
465 | If you want to enable your oracle to issue POAP like the Easy Challenge, you will need to config your contract in the following steps:
466 |
467 | 1. Config the `FatBadges` contract (ID: `0x083872054018c5b1890b8a901fc4213a385e3e4df5ddcc71405e4000e4244c6c`)
468 | - Create a new badge by `tx.new_badge`. The caller will be the owner of the badge.
469 | - Grant the permission to issue badges to your oracle by `tx.add_issuer`
470 | - Add enough POAP redeem code to your badge by `tx.add_code`. Not that you will need to give a JSON string array in the arg textbox, because the input type is `Vec`
471 | - Note that all of the above operations are owner-only
472 | - To check if your badge is configured correctly, call `query.get_total_badges` and `query.get_badge_info`. Each created badge will have a self-incremental id. Usually your newly created badge id is `get_total_badges() - 1`
473 | 2. Config your `EasyOracle` contract
474 | - Set the badges contract and badge id by `tx.config_issuer`. The badge contract should be that of `FatBadges`. The id should be the one you just created.
475 |
476 | A more accurate process is described in the [end-to-end test](https://github.com/Phala-Network/oracle-workshop/blob/3fe330fcdfef8f088896c3fba07c9bc79ccecea5/scripts/js/src/e2e.js#L180-L262).
477 |
478 | #### (Optional) Interact with the contract programmatically
479 |
480 | For contract interaction in node.js, please check the [end-to-end test](https://github.com/Phala-Network/oracle-workshop/blob/3fe330fcdfef8f088896c3fba07c9bc79ccecea5/scripts/js/src/e2e.js#L180-L262) as an example.
481 |
482 | For contract interaction in the browser, please check the [Decoded Workshop DApp source](https://github.com/Phala-Network/js-sdk/blob/6c26c1aef0b6ea6eb85b5d75f1492f120233047c/packages/example/pages/easy-challenge.tsx).
483 |
484 | You can find basic usage from the [`@phala/sdk` readme](https://github.com/Phala-Network/js-sdk/tree/decoded-2022/packages/sdk).
485 |
486 | ### Resources
487 |
488 | - Fat Contract UI:
489 | - Testnet Endpoint: `wss://poc5.phala.network/ws`
490 | - FatBadges contract id: `0x083872054018c5b1890b8a901fc4213a385e3e4df5ddcc71405e4000e4244c6c`
491 | - [End-to-end test](https://github.com/Phala-Network/oracle-workshop/blob/3fe330fcdfef8f088896c3fba07c9bc79ccecea5/scripts/js/src/e2e.js#L180-L262)
492 | - [Decoded Workshop DApp source](https://github.com/Phala-Network/js-sdk/blob/6c26c1aef0b6ea6eb85b5d75f1492f120233047c/packages/example/pages/easy-challenge.tsx)
493 | - [`@phala/sdk` readme](https://github.com/Phala-Network/js-sdk/tree/decoded-2022/packages/sdk)
494 |
495 | ## Troubleshooting
496 |
497 | ### Failed to compile with edition2021 error
498 |
499 | > "ERROR: Error invoking cargo metadata", "feature `edition2021` is required"
500 |
501 | Please make sure your rust toolchain is at the latest version by running:
502 |
503 | ```bash
504 | rustup update
505 | ```
506 |
507 | ### Failed to compile with rustlib error
508 |
509 | > error: ".../.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/Cargo.lock" does not exist, unable to build with the standard library
510 |
511 | Try to add the `rust-src` component:
512 |
513 | ```bash
514 | rustup component add rust-src --toolchain nightly
515 | ```
516 |
517 | ### Too old binaryen (wasm-opt)
518 |
519 | > ERROR: Your wasm-opt version is 91, but we require a version >= 99
520 |
521 | Please uninstall your current `binaryen` and reinstall the latest version from [the official repo](https://github.com/WebAssembly/binaryen/releases).
522 |
--------------------------------------------------------------------------------
/advanced_judger/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "advanced_judger"
3 | version = "0.1.0"
4 | authors = ["Hang Yin "]
5 | edition = "2021"
6 |
7 | [dependencies]
8 | ink_prelude = { version = "3", default-features = false }
9 | ink_primitives = { version = "3", default-features = false }
10 | ink_metadata = { version = "3", default-features = false, features = ["derive"], optional = true }
11 | ink_env = { version = "3", default-features = false }
12 | ink_storage = { version = "3", default-features = false }
13 | ink_lang = { version = "3", default-features = false }
14 |
15 | scale = { package = "parity-scale-codec", version = "3", default-features = false, features = ["derive"] }
16 | scale-info = { version = "2", default-features = false, features = ["derive"], optional = true }
17 |
18 | openbrush = { path = "../vendor/openbrush-contracts", version = "~2.1.0", default-features = false }
19 | pink-extension = { version = "0.1.17", default-features = false }
20 | pink-utils = { version = "0.1", default-features = false }
21 |
22 | fat_badges = { path = "../fat_badges", default-features = false, features = ["ink-as-dependency"] }
23 |
24 | [dev-dependencies]
25 | environmental = { path = "../utils/environmental", default-features = false }
26 | hex = "0.4.3"
27 | pink-extension-runtime = "0.1.3"
28 |
29 | [lib]
30 | name = "advanced_judger"
31 | path = "lib.rs"
32 | crate-type = [
33 | # Used for normal contract Wasm blobs.
34 | "cdylib",
35 | ]
36 |
37 | [features]
38 | default = ["std"]
39 | std = [
40 | "ink_metadata/std",
41 | "ink_env/std",
42 | "ink_storage/std",
43 | "ink_primitives/std",
44 | "openbrush/std",
45 | "scale/std",
46 | "scale-info/std",
47 | "pink-extension/std",
48 | "pink-utils/std",
49 | "fat_badges/std",
50 | ]
51 | ink-as-dependency = []
52 | mockable = [
53 | "fat_badges/mockable",
54 | "openbrush/mockable",
55 | ]
--------------------------------------------------------------------------------
/advanced_judger/lib.rs:
--------------------------------------------------------------------------------
1 | #![cfg_attr(not(feature = "std"), no_std)]
2 |
3 | use pink_extension as pink;
4 |
5 | mod submittable {
6 | use pink_utils::attestation::{Attestation, Verifier};
7 | use ink_env::AccountId;
8 | use ink_lang as ink;
9 | use ink_prelude::string::String;
10 | use ink_prelude::vec::Vec;
11 |
12 | #[openbrush::trait_definition(mock = mock_oracle::MockOracle)]
13 | pub trait SubmittableOracle {
14 | #[ink(message)]
15 | fn admin(&self) -> AccountId;
16 |
17 | #[ink(message)]
18 | fn verifier(&self) -> Verifier;
19 |
20 | #[ink(message)]
21 | fn attest(&self, arg: String) -> Result>;
22 | }
23 |
24 | #[openbrush::wrapper]
25 | pub type SubmittableOracleRef = dyn SubmittableOracle;
26 |
27 | // Only used for test, but we have to define it outside `mod tests`
28 | pub mod mock_oracle {
29 | use super::*;
30 | use pink_utils::attestation::{self, Generator};
31 |
32 | pub struct MockOracle {
33 | admin: AccountId,
34 | generator: Generator,
35 | verifier: Verifier,
36 | should_return_err: bool,
37 | }
38 |
39 | impl MockOracle {
40 | pub fn new(admin: AccountId, err: bool) -> Self {
41 | let (generator, verifier) = attestation::create(b"test");
42 | MockOracle {
43 | admin,
44 | generator,
45 | verifier,
46 | should_return_err: err,
47 | }
48 | }
49 |
50 | pub fn admin(&self) -> AccountId {
51 | self.admin.clone()
52 | }
53 |
54 | pub fn verifier(&self) -> Verifier {
55 | self.verifier.clone()
56 | }
57 |
58 | pub fn attest(&self, _arg: String) -> Result> {
59 | if self.should_return_err {
60 | Err(Default::default())
61 | } else {
62 | Ok(self.generator.sign(()))
63 | }
64 | }
65 | }
66 | }
67 | }
68 |
69 | #[pink::contract(env=PinkEnvironment)]
70 | mod advanced_judger {
71 | use super::pink::PinkEnvironment;
72 | use pink_utils::attestation;
73 | use ink_lang as ink;
74 | use ink_prelude::string::String;
75 | use ink_storage::traits::SpreadAllocate;
76 | use ink_storage::Mapping;
77 | use scale::{Decode, Encode};
78 |
79 | #[ink(storage)]
80 | #[derive(SpreadAllocate)]
81 | #[cfg_attr(feature = "std", derive(scale_info::TypeInfo))]
82 | pub struct AdvancedJudger {
83 | admin: AccountId,
84 | badge_contract_options: Option<(AccountId, u32)>,
85 | attestation_verifier: attestation::Verifier,
86 | attestation_generator: attestation::Generator,
87 | passed_contracts: Mapping,
88 | }
89 |
90 | /// Errors that can occur upon calling this contract.
91 | #[derive(Debug, PartialEq, Eq, Encode, Decode)]
92 | #[cfg_attr(feature = "std", derive(scale_info::TypeInfo))]
93 | pub enum Error {
94 | BadOrigin,
95 | BadgeContractNotSetUp,
96 | FailedToIssueBadge,
97 | FailedToVerify,
98 | InvalidParameter,
99 | AlreadySubmitted,
100 | }
101 |
102 | /// Type alias for the contract's result type.
103 | pub type Result = core::result::Result;
104 |
105 | impl AdvancedJudger {
106 | #[ink(constructor)]
107 | pub fn new() -> Self {
108 | // Create the attestation helpers
109 | let (generator, verifier) = attestation::create(b"adv-challenge-attestation-key");
110 | // Save sender as the contract admin
111 | let admin = Self::env().caller();
112 |
113 | ink_lang::utils::initialize_contract(|this: &mut Self| {
114 | this.admin = admin;
115 | this.badge_contract_options = None;
116 | this.attestation_generator = generator;
117 | this.attestation_verifier = verifier;
118 | })
119 | }
120 |
121 | // Commands
122 |
123 | /// Sets the downstream badge contract
124 | ///
125 | /// Only the admin can call it.
126 | #[ink(message)]
127 | pub fn config_issuer(&mut self, contract: AccountId, badge_id: u32) -> Result<()> {
128 | let caller = self.env().caller();
129 | if caller != self.admin {
130 | return Err(Error::BadOrigin);
131 | }
132 | // Create a reference to the already deployed FatBadges contract
133 | self.badge_contract_options = Some((contract, badge_id));
134 | Ok(())
135 | }
136 |
137 | /// Redeems a POAP with a signed `attestation`. (callable)
138 | ///
139 | /// The attestation must be created by [attest_gist] function. After the verification of
140 | /// the attestation, the the sender account will the linked to a Github username. Then a
141 | /// POAP redemption code will be allocated to the sender.
142 | ///
143 | /// Each blockchain account and github account can only be linked once.
144 | #[ink(message)]
145 | pub fn redeem(&mut self, attestation: attestation::Attestation) -> Result<()> {
146 | // Verify the attestation
147 | let data: GoodSubmission = self
148 | .attestation_verifier
149 | .verify_as(&attestation)
150 | .ok_or(Error::FailedToVerify)?;
151 | // The caller must be the attested account
152 | if data.admin != self.env().caller() {
153 | return Err(Error::BadOrigin);
154 | }
155 | // The contract is not submitted twice
156 | if self.passed_contracts.contains(data.contract) {
157 | return Err(Error::AlreadySubmitted);
158 | }
159 | self.passed_contracts.insert(data.contract, &());
160 |
161 | // Issue the badge
162 | let (contract, id) = self
163 | .badge_contract_options
164 | .ok_or(Error::BadgeContractNotSetUp)?;
165 |
166 | use fat_badges::issuable::IssuableRef;
167 | let badges: &IssuableRef = &contract;
168 | let r = badges.issue(id, data.admin);
169 | r.or(Err(Error::FailedToIssueBadge))
170 | }
171 |
172 | // Queries
173 |
174 | /// Attests a contract submission has passed the check (Query only)
175 | ///
176 | /// Call the submitted contract with an URL, and check that it can produce a valid offchain
177 | /// attestation. Once the check is passed, it returns an attestation that can be used
178 | /// to redeem a badge by `Self::redeem` by the admin of the submitted contract.
179 | #[ink(message)]
180 | pub fn check_contract(
181 | &self,
182 | contract: AccountId,
183 | url: String,
184 | ) -> Result {
185 | use crate::submittable::SubmittableOracleRef;
186 | let oracle: &SubmittableOracleRef = &contract;
187 |
188 | // The attestation should be at least `Ok(attestation)`
189 | let attestation = oracle.attest(url).or(Err(Error::FailedToVerify))?;
190 |
191 | // The attestation can be verified successfully
192 | let verifier = oracle.verifier();
193 | if !verifier.verify(&attestation) {
194 | return Err(Error::FailedToVerify);
195 | }
196 |
197 | // Ok. Now we can produce the attestation to redeem
198 | let admin = oracle.admin();
199 | let quote = GoodSubmission { admin, contract };
200 | let result = self.attestation_generator.sign(quote);
201 | Ok(result)
202 | }
203 | }
204 |
205 | #[derive(Clone, Encode, Decode, Debug)]
206 | #[cfg_attr(feature = "std", derive(scale_info::TypeInfo))]
207 | struct GoodSubmission {
208 | admin: AccountId,
209 | contract: AccountId,
210 | }
211 |
212 | #[cfg(test)]
213 | mod tests {
214 | use super::*;
215 | use crate::submittable::{mock_oracle::MockOracle, mock_submittableoracle};
216 |
217 | use ink_lang as ink;
218 |
219 | fn default_accounts() -> ink_env::test::DefaultAccounts {
220 | ink_env::test::default_accounts::()
221 | }
222 |
223 | #[ink::test]
224 | fn end_to_end() {
225 | pink_extension_runtime::mock_ext::mock_all_ext();
226 |
227 | // Test accounts
228 | let accounts = default_accounts();
229 |
230 | use fat_badges::issuable::mock_issuable;
231 | use openbrush::traits::mock::{Addressable, SharedCallStack};
232 |
233 | let stack = SharedCallStack::new(accounts.alice);
234 | mock_issuable::using(stack.clone(), || {
235 | mock_submittableoracle::using(stack.clone(), || {
236 | // let badges = Addressable::create_native(1, fat_badges::FatBadges::new(), stack.clone());
237 |
238 | let badges = mock_issuable::deploy(fat_badges::FatBadges::new());
239 | // Deploy the mock oracle on behalf of Bob
240 | let good_oracle =
241 | mock_submittableoracle::deploy(MockOracle::new(accounts.bob, false));
242 | let bad_oracle =
243 | mock_submittableoracle::deploy(MockOracle::new(accounts.bob, true));
244 | let contract =
245 | Addressable::create_native(1, AdvancedJudger::new(), stack.clone());
246 |
247 | // Create a badge and set the oracle as its issuer
248 | let id = badges
249 | .call_mut()
250 | .new_badge("test-badge".to_string())
251 | .unwrap();
252 | badges
253 | .call_mut()
254 | .add_code(id, vec!["code1".to_string(), "code2".to_string()])
255 | .unwrap();
256 | badges.call_mut().add_issuer(id, contract.id()).unwrap();
257 | contract.call_mut().config_issuer(badges.id(), id).unwrap();
258 |
259 | // Test the happy path
260 | stack.switch_account(accounts.bob).unwrap();
261 | let att = contract
262 | .call()
263 | .check_contract(good_oracle.id(), "some-url".to_string())
264 | .expect("good contract must pass the check");
265 |
266 | let data: GoodSubmission = contract
267 | .call()
268 | .attestation_verifier
269 | .verify_as(&att)
270 | .expect("should pass verification");
271 | assert_eq!(data.admin, accounts.bob);
272 | // Bob can redeem the code
273 | contract.call_mut().redeem(att).unwrap();
274 | // Bob has received the POAP
275 | assert_eq!(badges.call().get(id), Ok("code1".to_string()));
276 |
277 | // Test the bad path
278 | assert_eq!(
279 | contract
280 | .call()
281 | .check_contract(bad_oracle.id(), "some-url".to_string()),
282 | Err(Error::FailedToVerify)
283 | );
284 | });
285 | });
286 | }
287 | }
288 | }
289 |
--------------------------------------------------------------------------------
/bin/advanced_judger/advanced_judger.wasm:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Phala-Network/oracle-workshop/a9361b1722e7680040873688f41ebc7adfd77e95/bin/advanced_judger/advanced_judger.wasm
--------------------------------------------------------------------------------
/bin/advanced_judger/metadata.json:
--------------------------------------------------------------------------------
1 | {
2 | "source": {
3 | "hash": "0x6d63e947342160ebf1b01f3916444deb04c0ebcc3e2a3a689887743e182831c1",
4 | "language": "ink! 3.3.1",
5 | "compiler": "rustc 1.62.0-nightly"
6 | },
7 | "contract": {
8 | "name": "advanced_judger",
9 | "version": "0.1.0",
10 | "authors": [
11 | "Hang Yin "
12 | ]
13 | },
14 | "V3": {
15 | "spec": {
16 | "constructors": [
17 | {
18 | "args": [],
19 | "docs": [],
20 | "label": "new",
21 | "payable": false,
22 | "selector": "0x9bae9d5e"
23 | }
24 | ],
25 | "docs": [],
26 | "events": [],
27 | "messages": [
28 | {
29 | "args": [
30 | {
31 | "label": "contract",
32 | "type": {
33 | "displayName": [
34 | "AccountId"
35 | ],
36 | "type": 0
37 | }
38 | },
39 | {
40 | "label": "badge_id",
41 | "type": {
42 | "displayName": [
43 | "u32"
44 | ],
45 | "type": 3
46 | }
47 | }
48 | ],
49 | "docs": [
50 | " Sets the downstream badge contract",
51 | "",
52 | " Only the admin can call it."
53 | ],
54 | "label": "config_issuer",
55 | "mutates": true,
56 | "payable": false,
57 | "returnType": {
58 | "displayName": [
59 | "Result"
60 | ],
61 | "type": 8
62 | },
63 | "selector": "0x5acd8e33"
64 | },
65 | {
66 | "args": [
67 | {
68 | "label": "attestation",
69 | "type": {
70 | "displayName": [
71 | "attestation",
72 | "Attestation"
73 | ],
74 | "type": 10
75 | }
76 | }
77 | ],
78 | "docs": [
79 | " Redeems a POAP with a signed `attestation`. (callable)",
80 | "",
81 | " The attestation must be created by [attest_gist] function. After the verification of",
82 | " the attestation, the the sender account will the linked to a Github username. Then a",
83 | " POAP redemption code will be allocated to the sender.",
84 | "",
85 | " Each blockchain account and github account can only be linked once."
86 | ],
87 | "label": "redeem",
88 | "mutates": true,
89 | "payable": false,
90 | "returnType": {
91 | "displayName": [
92 | "Result"
93 | ],
94 | "type": 8
95 | },
96 | "selector": "0xec3e9290"
97 | },
98 | {
99 | "args": [
100 | {
101 | "label": "contract",
102 | "type": {
103 | "displayName": [
104 | "AccountId"
105 | ],
106 | "type": 0
107 | }
108 | },
109 | {
110 | "label": "url",
111 | "type": {
112 | "displayName": [
113 | "String"
114 | ],
115 | "type": 11
116 | }
117 | }
118 | ],
119 | "docs": [
120 | " Attests a contract submission has passed the check (Query only)",
121 | "",
122 | " Call the submitted contract with an URL, and check that it can produce a valid offchain",
123 | " attestation. Once the check is passed, it returns an attestation that can be used",
124 | " to redeem a badge by `Self::redeem` by the admin of the submitted contract."
125 | ],
126 | "label": "check_contract",
127 | "mutates": false,
128 | "payable": false,
129 | "returnType": {
130 | "displayName": [
131 | "Result"
132 | ],
133 | "type": 12
134 | },
135 | "selector": "0xba26db0d"
136 | }
137 | ]
138 | },
139 | "storage": {
140 | "struct": {
141 | "fields": [
142 | {
143 | "layout": {
144 | "cell": {
145 | "key": "0x0000000000000000000000000000000000000000000000000000000000000000",
146 | "ty": 0
147 | }
148 | },
149 | "name": "admin"
150 | },
151 | {
152 | "layout": {
153 | "enum": {
154 | "dispatchKey": "0x0100000000000000000000000000000000000000000000000000000000000000",
155 | "variants": {
156 | "0": {
157 | "fields": [
158 | {
159 | "layout": {
160 | "struct": {
161 | "fields": [
162 | {
163 | "layout": {
164 | "cell": {
165 | "key": "0x0200000000000000000000000000000000000000000000000000000000000000",
166 | "ty": 0
167 | }
168 | },
169 | "name": null
170 | },
171 | {
172 | "layout": {
173 | "cell": {
174 | "key": "0x0300000000000000000000000000000000000000000000000000000000000000",
175 | "ty": 3
176 | }
177 | },
178 | "name": null
179 | }
180 | ]
181 | }
182 | },
183 | "name": null
184 | }
185 | ]
186 | },
187 | "1": {
188 | "fields": []
189 | }
190 | }
191 | }
192 | },
193 | "name": "badge_contract_options"
194 | },
195 | {
196 | "layout": {
197 | "struct": {
198 | "fields": [
199 | {
200 | "layout": {
201 | "cell": {
202 | "key": "0x0200000000000000000000000000000000000000000000000000000000000000",
203 | "ty": 4
204 | }
205 | },
206 | "name": "pubkey"
207 | }
208 | ]
209 | }
210 | },
211 | "name": "attestation_verifier"
212 | },
213 | {
214 | "layout": {
215 | "struct": {
216 | "fields": [
217 | {
218 | "layout": {
219 | "cell": {
220 | "key": "0x0300000000000000000000000000000000000000000000000000000000000000",
221 | "ty": 4
222 | }
223 | },
224 | "name": "privkey"
225 | }
226 | ]
227 | }
228 | },
229 | "name": "attestation_generator"
230 | },
231 | {
232 | "layout": {
233 | "cell": {
234 | "key": "0x0400000000000000000000000000000000000000000000000000000000000000",
235 | "ty": 5
236 | }
237 | },
238 | "name": "passed_contracts"
239 | }
240 | ]
241 | }
242 | },
243 | "types": [
244 | {
245 | "id": 0,
246 | "type": {
247 | "def": {
248 | "composite": {
249 | "fields": [
250 | {
251 | "type": 1,
252 | "typeName": "[u8; 32]"
253 | }
254 | ]
255 | }
256 | },
257 | "path": [
258 | "ink_env",
259 | "types",
260 | "AccountId"
261 | ]
262 | }
263 | },
264 | {
265 | "id": 1,
266 | "type": {
267 | "def": {
268 | "array": {
269 | "len": 32,
270 | "type": 2
271 | }
272 | }
273 | }
274 | },
275 | {
276 | "id": 2,
277 | "type": {
278 | "def": {
279 | "primitive": "u8"
280 | }
281 | }
282 | },
283 | {
284 | "id": 3,
285 | "type": {
286 | "def": {
287 | "primitive": "u32"
288 | }
289 | }
290 | },
291 | {
292 | "id": 4,
293 | "type": {
294 | "def": {
295 | "sequence": {
296 | "type": 2
297 | }
298 | }
299 | }
300 | },
301 | {
302 | "id": 5,
303 | "type": {
304 | "def": {
305 | "composite": {
306 | "fields": [
307 | {
308 | "name": "offset_key",
309 | "type": 7,
310 | "typeName": "Key"
311 | }
312 | ]
313 | }
314 | },
315 | "params": [
316 | {
317 | "name": "K",
318 | "type": 0
319 | },
320 | {
321 | "name": "V",
322 | "type": 6
323 | }
324 | ],
325 | "path": [
326 | "ink_storage",
327 | "lazy",
328 | "mapping",
329 | "Mapping"
330 | ]
331 | }
332 | },
333 | {
334 | "id": 6,
335 | "type": {
336 | "def": {
337 | "tuple": []
338 | }
339 | }
340 | },
341 | {
342 | "id": 7,
343 | "type": {
344 | "def": {
345 | "composite": {
346 | "fields": [
347 | {
348 | "type": 1,
349 | "typeName": "[u8; 32]"
350 | }
351 | ]
352 | }
353 | },
354 | "path": [
355 | "ink_primitives",
356 | "Key"
357 | ]
358 | }
359 | },
360 | {
361 | "id": 8,
362 | "type": {
363 | "def": {
364 | "variant": {
365 | "variants": [
366 | {
367 | "fields": [
368 | {
369 | "type": 6
370 | }
371 | ],
372 | "index": 0,
373 | "name": "Ok"
374 | },
375 | {
376 | "fields": [
377 | {
378 | "type": 9
379 | }
380 | ],
381 | "index": 1,
382 | "name": "Err"
383 | }
384 | ]
385 | }
386 | },
387 | "params": [
388 | {
389 | "name": "T",
390 | "type": 6
391 | },
392 | {
393 | "name": "E",
394 | "type": 9
395 | }
396 | ],
397 | "path": [
398 | "Result"
399 | ]
400 | }
401 | },
402 | {
403 | "id": 9,
404 | "type": {
405 | "def": {
406 | "variant": {
407 | "variants": [
408 | {
409 | "index": 0,
410 | "name": "BadOrigin"
411 | },
412 | {
413 | "index": 1,
414 | "name": "BadgeContractNotSetUp"
415 | },
416 | {
417 | "index": 2,
418 | "name": "FailedToIssueBadge"
419 | },
420 | {
421 | "index": 3,
422 | "name": "FailedToVerify"
423 | },
424 | {
425 | "index": 4,
426 | "name": "InvalidParameter"
427 | },
428 | {
429 | "index": 5,
430 | "name": "AlreadySubmitted"
431 | }
432 | ]
433 | }
434 | },
435 | "path": [
436 | "advanced_judger",
437 | "advanced_judger",
438 | "Error"
439 | ]
440 | }
441 | },
442 | {
443 | "id": 10,
444 | "type": {
445 | "def": {
446 | "composite": {
447 | "fields": [
448 | {
449 | "name": "data",
450 | "type": 4,
451 | "typeName": "Vec"
452 | },
453 | {
454 | "name": "signature",
455 | "type": 4,
456 | "typeName": "Vec"
457 | }
458 | ]
459 | }
460 | },
461 | "path": [
462 | "pink_utils",
463 | "attestation",
464 | "Attestation"
465 | ]
466 | }
467 | },
468 | {
469 | "id": 11,
470 | "type": {
471 | "def": {
472 | "primitive": "str"
473 | }
474 | }
475 | },
476 | {
477 | "id": 12,
478 | "type": {
479 | "def": {
480 | "variant": {
481 | "variants": [
482 | {
483 | "fields": [
484 | {
485 | "type": 10
486 | }
487 | ],
488 | "index": 0,
489 | "name": "Ok"
490 | },
491 | {
492 | "fields": [
493 | {
494 | "type": 9
495 | }
496 | ],
497 | "index": 1,
498 | "name": "Err"
499 | }
500 | ]
501 | }
502 | },
503 | "params": [
504 | {
505 | "name": "T",
506 | "type": 10
507 | },
508 | {
509 | "name": "E",
510 | "type": 9
511 | }
512 | ],
513 | "path": [
514 | "Result"
515 | ]
516 | }
517 | }
518 | ]
519 | }
520 | }
--------------------------------------------------------------------------------
/bin/easy_oracle/easy_oracle.wasm:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Phala-Network/oracle-workshop/a9361b1722e7680040873688f41ebc7adfd77e95/bin/easy_oracle/easy_oracle.wasm
--------------------------------------------------------------------------------
/bin/easy_oracle/metadata.json:
--------------------------------------------------------------------------------
1 | {
2 | "source": {
3 | "hash": "0xfd1044483430f0050793e0190a8d153e88383d033c38df1f89def54dc20c92c3",
4 | "language": "ink! 3.3.1",
5 | "compiler": "rustc 1.62.0-nightly"
6 | },
7 | "contract": {
8 | "name": "easy_oracle",
9 | "version": "0.1.0",
10 | "authors": [
11 | "Hang Yin "
12 | ]
13 | },
14 | "V3": {
15 | "spec": {
16 | "constructors": [
17 | {
18 | "args": [],
19 | "docs": [],
20 | "label": "new",
21 | "payable": false,
22 | "selector": "0x9bae9d5e"
23 | }
24 | ],
25 | "docs": [],
26 | "events": [],
27 | "messages": [
28 | {
29 | "args": [
30 | {
31 | "label": "contract",
32 | "type": {
33 | "displayName": [
34 | "AccountId"
35 | ],
36 | "type": 0
37 | }
38 | },
39 | {
40 | "label": "badge_id",
41 | "type": {
42 | "displayName": [
43 | "u32"
44 | ],
45 | "type": 3
46 | }
47 | }
48 | ],
49 | "docs": [
50 | " Sets the downstream badge contract",
51 | "",
52 | " Only the admin can call it."
53 | ],
54 | "label": "config_issuer",
55 | "mutates": true,
56 | "payable": false,
57 | "returnType": {
58 | "displayName": [
59 | "Result"
60 | ],
61 | "type": 9
62 | },
63 | "selector": "0x5acd8e33"
64 | },
65 | {
66 | "args": [
67 | {
68 | "label": "attestation",
69 | "type": {
70 | "displayName": [
71 | "attestation",
72 | "Attestation"
73 | ],
74 | "type": 11
75 | }
76 | }
77 | ],
78 | "docs": [
79 | " Redeems a POAP with a signed `attestation`. (callable)",
80 | "",
81 | " The attestation must be created by [`attest_gist`] function. After the verification of",
82 | " the attestation, the the sender account will the linked to a Github username. Then a",
83 | " POAP redemption code will be allocated to the sender.",
84 | "",
85 | " Each blockchain account and github account can only be linked once."
86 | ],
87 | "label": "redeem",
88 | "mutates": true,
89 | "payable": false,
90 | "returnType": {
91 | "displayName": [
92 | "Result"
93 | ],
94 | "type": 9
95 | },
96 | "selector": "0xec3e9290"
97 | },
98 | {
99 | "args": [
100 | {
101 | "label": "url",
102 | "type": {
103 | "displayName": [
104 | "String"
105 | ],
106 | "type": 6
107 | }
108 | }
109 | ],
110 | "docs": [
111 | " Attests a Github Gist by the raw file url. (Query only)",
112 | "",
113 | " It sends a HTTPS request to the url and extract an address from the claim (\"This gist",
114 | " is owned by address: 0x...\"). Once the claim is verified, it returns a signed",
115 | " attestation with the data `(username, account_id)`.",
116 | "",
117 | " The `Err` variant of the result is an encoded `Error` to simplify cross-contract calls.",
118 | " Particularly, when another contract wants to call us, they may not want to depend on",
119 | " any special type defined by us (`Error` in this case). So we only return generic types."
120 | ],
121 | "label": "SubmittableOracle::attest",
122 | "mutates": false,
123 | "payable": false,
124 | "returnType": {
125 | "displayName": [
126 | "core",
127 | "result",
128 | "Result"
129 | ],
130 | "type": 12
131 | },
132 | "selector": "0x2b03dda0"
133 | },
134 | {
135 | "args": [],
136 | "docs": [],
137 | "label": "SubmittableOracle::admin",
138 | "mutates": false,
139 | "payable": false,
140 | "returnType": {
141 | "displayName": [
142 | "AccountId"
143 | ],
144 | "type": 0
145 | },
146 | "selector": "0x395849c6"
147 | },
148 | {
149 | "args": [],
150 | "docs": [
151 | " The attestation verifier"
152 | ],
153 | "label": "SubmittableOracle::verifier",
154 | "mutates": false,
155 | "payable": false,
156 | "returnType": {
157 | "displayName": [
158 | "attestation",
159 | "Verifier"
160 | ],
161 | "type": 13
162 | },
163 | "selector": "0x05dc0314"
164 | }
165 | ]
166 | },
167 | "storage": {
168 | "struct": {
169 | "fields": [
170 | {
171 | "layout": {
172 | "cell": {
173 | "key": "0x0000000000000000000000000000000000000000000000000000000000000000",
174 | "ty": 0
175 | }
176 | },
177 | "name": "admin"
178 | },
179 | {
180 | "layout": {
181 | "enum": {
182 | "dispatchKey": "0x0100000000000000000000000000000000000000000000000000000000000000",
183 | "variants": {
184 | "0": {
185 | "fields": [
186 | {
187 | "layout": {
188 | "struct": {
189 | "fields": [
190 | {
191 | "layout": {
192 | "cell": {
193 | "key": "0x0200000000000000000000000000000000000000000000000000000000000000",
194 | "ty": 0
195 | }
196 | },
197 | "name": null
198 | },
199 | {
200 | "layout": {
201 | "cell": {
202 | "key": "0x0300000000000000000000000000000000000000000000000000000000000000",
203 | "ty": 3
204 | }
205 | },
206 | "name": null
207 | }
208 | ]
209 | }
210 | },
211 | "name": null
212 | }
213 | ]
214 | },
215 | "1": {
216 | "fields": []
217 | }
218 | }
219 | }
220 | },
221 | "name": "badge_contract_options"
222 | },
223 | {
224 | "layout": {
225 | "struct": {
226 | "fields": [
227 | {
228 | "layout": {
229 | "cell": {
230 | "key": "0x0200000000000000000000000000000000000000000000000000000000000000",
231 | "ty": 4
232 | }
233 | },
234 | "name": "pubkey"
235 | }
236 | ]
237 | }
238 | },
239 | "name": "attestation_verifier"
240 | },
241 | {
242 | "layout": {
243 | "struct": {
244 | "fields": [
245 | {
246 | "layout": {
247 | "cell": {
248 | "key": "0x0300000000000000000000000000000000000000000000000000000000000000",
249 | "ty": 4
250 | }
251 | },
252 | "name": "privkey"
253 | }
254 | ]
255 | }
256 | },
257 | "name": "attestation_generator"
258 | },
259 | {
260 | "layout": {
261 | "cell": {
262 | "key": "0x0400000000000000000000000000000000000000000000000000000000000000",
263 | "ty": 5
264 | }
265 | },
266 | "name": "linked_users"
267 | }
268 | ]
269 | }
270 | },
271 | "types": [
272 | {
273 | "id": 0,
274 | "type": {
275 | "def": {
276 | "composite": {
277 | "fields": [
278 | {
279 | "type": 1,
280 | "typeName": "[u8; 32]"
281 | }
282 | ]
283 | }
284 | },
285 | "path": [
286 | "ink_env",
287 | "types",
288 | "AccountId"
289 | ]
290 | }
291 | },
292 | {
293 | "id": 1,
294 | "type": {
295 | "def": {
296 | "array": {
297 | "len": 32,
298 | "type": 2
299 | }
300 | }
301 | }
302 | },
303 | {
304 | "id": 2,
305 | "type": {
306 | "def": {
307 | "primitive": "u8"
308 | }
309 | }
310 | },
311 | {
312 | "id": 3,
313 | "type": {
314 | "def": {
315 | "primitive": "u32"
316 | }
317 | }
318 | },
319 | {
320 | "id": 4,
321 | "type": {
322 | "def": {
323 | "sequence": {
324 | "type": 2
325 | }
326 | }
327 | }
328 | },
329 | {
330 | "id": 5,
331 | "type": {
332 | "def": {
333 | "composite": {
334 | "fields": [
335 | {
336 | "name": "offset_key",
337 | "type": 8,
338 | "typeName": "Key"
339 | }
340 | ]
341 | }
342 | },
343 | "params": [
344 | {
345 | "name": "K",
346 | "type": 6
347 | },
348 | {
349 | "name": "V",
350 | "type": 7
351 | }
352 | ],
353 | "path": [
354 | "ink_storage",
355 | "lazy",
356 | "mapping",
357 | "Mapping"
358 | ]
359 | }
360 | },
361 | {
362 | "id": 6,
363 | "type": {
364 | "def": {
365 | "primitive": "str"
366 | }
367 | }
368 | },
369 | {
370 | "id": 7,
371 | "type": {
372 | "def": {
373 | "tuple": []
374 | }
375 | }
376 | },
377 | {
378 | "id": 8,
379 | "type": {
380 | "def": {
381 | "composite": {
382 | "fields": [
383 | {
384 | "type": 1,
385 | "typeName": "[u8; 32]"
386 | }
387 | ]
388 | }
389 | },
390 | "path": [
391 | "ink_primitives",
392 | "Key"
393 | ]
394 | }
395 | },
396 | {
397 | "id": 9,
398 | "type": {
399 | "def": {
400 | "variant": {
401 | "variants": [
402 | {
403 | "fields": [
404 | {
405 | "type": 7
406 | }
407 | ],
408 | "index": 0,
409 | "name": "Ok"
410 | },
411 | {
412 | "fields": [
413 | {
414 | "type": 10
415 | }
416 | ],
417 | "index": 1,
418 | "name": "Err"
419 | }
420 | ]
421 | }
422 | },
423 | "params": [
424 | {
425 | "name": "T",
426 | "type": 7
427 | },
428 | {
429 | "name": "E",
430 | "type": 10
431 | }
432 | ],
433 | "path": [
434 | "Result"
435 | ]
436 | }
437 | },
438 | {
439 | "id": 10,
440 | "type": {
441 | "def": {
442 | "variant": {
443 | "variants": [
444 | {
445 | "index": 0,
446 | "name": "BadOrigin"
447 | },
448 | {
449 | "index": 1,
450 | "name": "BadgeContractNotSetUp"
451 | },
452 | {
453 | "index": 2,
454 | "name": "InvalidUrl"
455 | },
456 | {
457 | "index": 3,
458 | "name": "RequestFailed"
459 | },
460 | {
461 | "index": 4,
462 | "name": "NoClaimFound"
463 | },
464 | {
465 | "index": 5,
466 | "name": "InvalidAddressLength"
467 | },
468 | {
469 | "index": 6,
470 | "name": "InvalidAddress"
471 | },
472 | {
473 | "index": 7,
474 | "name": "NoPermission"
475 | },
476 | {
477 | "index": 8,
478 | "name": "InvalidSignature"
479 | },
480 | {
481 | "index": 9,
482 | "name": "UsernameAlreadyInUse"
483 | },
484 | {
485 | "index": 10,
486 | "name": "AccountAlreadyInUse"
487 | },
488 | {
489 | "index": 11,
490 | "name": "FailedToIssueBadge"
491 | }
492 | ]
493 | }
494 | },
495 | "path": [
496 | "easy_oracle",
497 | "easy_oracle",
498 | "Error"
499 | ]
500 | }
501 | },
502 | {
503 | "id": 11,
504 | "type": {
505 | "def": {
506 | "composite": {
507 | "fields": [
508 | {
509 | "name": "data",
510 | "type": 4,
511 | "typeName": "Vec"
512 | },
513 | {
514 | "name": "signature",
515 | "type": 4,
516 | "typeName": "Vec"
517 | }
518 | ]
519 | }
520 | },
521 | "path": [
522 | "pink_utils",
523 | "attestation",
524 | "Attestation"
525 | ]
526 | }
527 | },
528 | {
529 | "id": 12,
530 | "type": {
531 | "def": {
532 | "variant": {
533 | "variants": [
534 | {
535 | "fields": [
536 | {
537 | "type": 11
538 | }
539 | ],
540 | "index": 0,
541 | "name": "Ok"
542 | },
543 | {
544 | "fields": [
545 | {
546 | "type": 4
547 | }
548 | ],
549 | "index": 1,
550 | "name": "Err"
551 | }
552 | ]
553 | }
554 | },
555 | "params": [
556 | {
557 | "name": "T",
558 | "type": 11
559 | },
560 | {
561 | "name": "E",
562 | "type": 4
563 | }
564 | ],
565 | "path": [
566 | "Result"
567 | ]
568 | }
569 | },
570 | {
571 | "id": 13,
572 | "type": {
573 | "def": {
574 | "composite": {
575 | "fields": [
576 | {
577 | "name": "pubkey",
578 | "type": 4,
579 | "typeName": "Vec"
580 | }
581 | ]
582 | }
583 | },
584 | "path": [
585 | "pink_utils",
586 | "attestation",
587 | "Verifier"
588 | ]
589 | }
590 | }
591 | ]
592 | }
593 | }
--------------------------------------------------------------------------------
/bin/fat_badges/fat_badges.wasm:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Phala-Network/oracle-workshop/a9361b1722e7680040873688f41ebc7adfd77e95/bin/fat_badges/fat_badges.wasm
--------------------------------------------------------------------------------
/bin/fat_badges/metadata.json:
--------------------------------------------------------------------------------
1 | {
2 | "source": {
3 | "hash": "0x1892e204a098f81150db8154afd96eba0f8a476a0ecad776eca5c05c47c25855",
4 | "language": "ink! 3.3.1",
5 | "compiler": "rustc 1.62.0-nightly"
6 | },
7 | "contract": {
8 | "name": "fat_badges",
9 | "version": "0.1.0",
10 | "authors": [
11 | "Hang Yin "
12 | ]
13 | },
14 | "V3": {
15 | "spec": {
16 | "constructors": [
17 | {
18 | "args": [],
19 | "docs": [],
20 | "label": "new",
21 | "payable": false,
22 | "selector": "0x9bae9d5e"
23 | }
24 | ],
25 | "docs": [],
26 | "events": [],
27 | "messages": [
28 | {
29 | "args": [
30 | {
31 | "label": "name",
32 | "type": {
33 | "displayName": [
34 | "String"
35 | ],
36 | "type": 6
37 | }
38 | }
39 | ],
40 | "docs": [
41 | " Creates a new badge and become the admin of the badge",
42 | "",
43 | " Return the id of the badge."
44 | ],
45 | "label": "new_badge",
46 | "mutates": true,
47 | "payable": false,
48 | "returnType": {
49 | "displayName": [
50 | "Result"
51 | ],
52 | "type": 14
53 | },
54 | "selector": "0x0df6030d"
55 | },
56 | {
57 | "args": [
58 | {
59 | "label": "id",
60 | "type": {
61 | "displayName": [
62 | "u32"
63 | ],
64 | "type": 3
65 | }
66 | },
67 | {
68 | "label": "issuer",
69 | "type": {
70 | "displayName": [
71 | "AccountId"
72 | ],
73 | "type": 0
74 | }
75 | }
76 | ],
77 | "docs": [
78 | " Adds a badge issuer",
79 | "",
80 | " The caller must be the badge admin."
81 | ],
82 | "label": "add_issuer",
83 | "mutates": true,
84 | "payable": false,
85 | "returnType": {
86 | "displayName": [
87 | "Result"
88 | ],
89 | "type": 16
90 | },
91 | "selector": "0x04d438b0"
92 | },
93 | {
94 | "args": [
95 | {
96 | "label": "id",
97 | "type": {
98 | "displayName": [
99 | "u32"
100 | ],
101 | "type": 3
102 | }
103 | },
104 | {
105 | "label": "issuer",
106 | "type": {
107 | "displayName": [
108 | "AccountId"
109 | ],
110 | "type": 0
111 | }
112 | }
113 | ],
114 | "docs": [
115 | " Removes a badge issuer",
116 | "",
117 | " The caller must be the badge admin."
118 | ],
119 | "label": "remove_issuer",
120 | "mutates": true,
121 | "payable": false,
122 | "returnType": {
123 | "displayName": [
124 | "Result"
125 | ],
126 | "type": 16
127 | },
128 | "selector": "0x5765dc58"
129 | },
130 | {
131 | "args": [
132 | {
133 | "label": "id",
134 | "type": {
135 | "displayName": [
136 | "u32"
137 | ],
138 | "type": 3
139 | }
140 | },
141 | {
142 | "label": "code",
143 | "type": {
144 | "displayName": [
145 | "Vec"
146 | ],
147 | "type": 17
148 | }
149 | }
150 | ],
151 | "docs": [
152 | " Appends a list of redeem code to a badge",
153 | "",
154 | " The caller must be the badge admin."
155 | ],
156 | "label": "add_code",
157 | "mutates": true,
158 | "payable": false,
159 | "returnType": {
160 | "displayName": [
161 | "Result"
162 | ],
163 | "type": 16
164 | },
165 | "selector": "0xbe785fe9"
166 | },
167 | {
168 | "args": [],
169 | "docs": [
170 | " Returns the number of all the badges"
171 | ],
172 | "label": "get_total_badges",
173 | "mutates": false,
174 | "payable": false,
175 | "returnType": {
176 | "displayName": [
177 | "u32"
178 | ],
179 | "type": 3
180 | },
181 | "selector": "0x5883ec64"
182 | },
183 | {
184 | "args": [
185 | {
186 | "label": "id",
187 | "type": {
188 | "displayName": [
189 | "u32"
190 | ],
191 | "type": 3
192 | }
193 | }
194 | ],
195 | "docs": [
196 | " Returns the detailed information of a badge"
197 | ],
198 | "label": "get_badge_info",
199 | "mutates": false,
200 | "payable": false,
201 | "returnType": {
202 | "displayName": [
203 | "Result"
204 | ],
205 | "type": 18
206 | },
207 | "selector": "0xf4c2ab36"
208 | },
209 | {
210 | "args": [
211 | {
212 | "label": "id",
213 | "type": {
214 | "displayName": [
215 | "u32"
216 | ],
217 | "type": 3
218 | }
219 | },
220 | {
221 | "label": "issuer",
222 | "type": {
223 | "displayName": [
224 | "AccountId"
225 | ],
226 | "type": 0
227 | }
228 | }
229 | ],
230 | "docs": [
231 | " Checks if an account is a badge issuer"
232 | ],
233 | "label": "is_badge_issuer",
234 | "mutates": false,
235 | "payable": false,
236 | "returnType": {
237 | "displayName": [
238 | "bool"
239 | ],
240 | "type": 19
241 | },
242 | "selector": "0xfac3bedd"
243 | },
244 | {
245 | "args": [
246 | {
247 | "label": "id",
248 | "type": {
249 | "displayName": [
250 | "u32"
251 | ],
252 | "type": 3
253 | }
254 | }
255 | ],
256 | "docs": [
257 | " Reads the badge code assigned to the caller if exists"
258 | ],
259 | "label": "get",
260 | "mutates": false,
261 | "payable": false,
262 | "returnType": {
263 | "displayName": [
264 | "Result"
265 | ],
266 | "type": 20
267 | },
268 | "selector": "0x2f865bd9"
269 | },
270 | {
271 | "args": [
272 | {
273 | "label": "id",
274 | "type": {
275 | "displayName": [
276 | "issuable_external",
277 | "IssueInput1"
278 | ],
279 | "type": 3
280 | }
281 | },
282 | {
283 | "label": "dest",
284 | "type": {
285 | "displayName": [
286 | "issuable_external",
287 | "IssueInput2"
288 | ],
289 | "type": 0
290 | }
291 | }
292 | ],
293 | "docs": [
294 | " Issues a badge to the `dest` account",
295 | "",
296 | " The caller must be the badge admin or a badge issuer. Return a `RunOutOfCode` error",
297 | " when there's no enough redeem code to issue."
298 | ],
299 | "label": "Issuable::issue",
300 | "mutates": true,
301 | "payable": false,
302 | "returnType": {
303 | "displayName": [
304 | "issuable_external",
305 | "IssueOutput"
306 | ],
307 | "type": 16
308 | },
309 | "selector": "0x445c0488"
310 | }
311 | ]
312 | },
313 | "storage": {
314 | "struct": {
315 | "fields": [
316 | {
317 | "layout": {
318 | "cell": {
319 | "key": "0x0000000000000000000000000000000000000000000000000000000000000000",
320 | "ty": 0
321 | }
322 | },
323 | "name": "admin"
324 | },
325 | {
326 | "layout": {
327 | "cell": {
328 | "key": "0x0100000000000000000000000000000000000000000000000000000000000000",
329 | "ty": 3
330 | }
331 | },
332 | "name": "total_badges"
333 | },
334 | {
335 | "layout": {
336 | "cell": {
337 | "key": "0x0200000000000000000000000000000000000000000000000000000000000000",
338 | "ty": 4
339 | }
340 | },
341 | "name": "badge_info"
342 | },
343 | {
344 | "layout": {
345 | "cell": {
346 | "key": "0x0300000000000000000000000000000000000000000000000000000000000000",
347 | "ty": 8
348 | }
349 | },
350 | "name": "badge_issuers"
351 | },
352 | {
353 | "layout": {
354 | "cell": {
355 | "key": "0x0400000000000000000000000000000000000000000000000000000000000000",
356 | "ty": 11
357 | }
358 | },
359 | "name": "badge_code"
360 | },
361 | {
362 | "layout": {
363 | "cell": {
364 | "key": "0x0500000000000000000000000000000000000000000000000000000000000000",
365 | "ty": 13
366 | }
367 | },
368 | "name": "badge_assignments"
369 | }
370 | ]
371 | }
372 | },
373 | "types": [
374 | {
375 | "id": 0,
376 | "type": {
377 | "def": {
378 | "composite": {
379 | "fields": [
380 | {
381 | "type": 1,
382 | "typeName": "[u8; 32]"
383 | }
384 | ]
385 | }
386 | },
387 | "path": [
388 | "ink_env",
389 | "types",
390 | "AccountId"
391 | ]
392 | }
393 | },
394 | {
395 | "id": 1,
396 | "type": {
397 | "def": {
398 | "array": {
399 | "len": 32,
400 | "type": 2
401 | }
402 | }
403 | }
404 | },
405 | {
406 | "id": 2,
407 | "type": {
408 | "def": {
409 | "primitive": "u8"
410 | }
411 | }
412 | },
413 | {
414 | "id": 3,
415 | "type": {
416 | "def": {
417 | "primitive": "u32"
418 | }
419 | }
420 | },
421 | {
422 | "id": 4,
423 | "type": {
424 | "def": {
425 | "composite": {
426 | "fields": [
427 | {
428 | "name": "offset_key",
429 | "type": 7,
430 | "typeName": "Key"
431 | }
432 | ]
433 | }
434 | },
435 | "params": [
436 | {
437 | "name": "K",
438 | "type": 3
439 | },
440 | {
441 | "name": "V",
442 | "type": 5
443 | }
444 | ],
445 | "path": [
446 | "ink_storage",
447 | "lazy",
448 | "mapping",
449 | "Mapping"
450 | ]
451 | }
452 | },
453 | {
454 | "id": 5,
455 | "type": {
456 | "def": {
457 | "composite": {
458 | "fields": [
459 | {
460 | "name": "id",
461 | "type": 3,
462 | "typeName": "u32"
463 | },
464 | {
465 | "name": "admin",
466 | "type": 0,
467 | "typeName": "AccountId"
468 | },
469 | {
470 | "name": "name",
471 | "type": 6,
472 | "typeName": "String"
473 | },
474 | {
475 | "name": "num_code",
476 | "type": 3,
477 | "typeName": "u32"
478 | },
479 | {
480 | "name": "num_issued",
481 | "type": 3,
482 | "typeName": "u32"
483 | }
484 | ]
485 | }
486 | },
487 | "path": [
488 | "fat_badges",
489 | "fat_badges",
490 | "BadgeInfo"
491 | ]
492 | }
493 | },
494 | {
495 | "id": 6,
496 | "type": {
497 | "def": {
498 | "primitive": "str"
499 | }
500 | }
501 | },
502 | {
503 | "id": 7,
504 | "type": {
505 | "def": {
506 | "composite": {
507 | "fields": [
508 | {
509 | "type": 1,
510 | "typeName": "[u8; 32]"
511 | }
512 | ]
513 | }
514 | },
515 | "path": [
516 | "ink_primitives",
517 | "Key"
518 | ]
519 | }
520 | },
521 | {
522 | "id": 8,
523 | "type": {
524 | "def": {
525 | "composite": {
526 | "fields": [
527 | {
528 | "name": "offset_key",
529 | "type": 7,
530 | "typeName": "Key"
531 | }
532 | ]
533 | }
534 | },
535 | "params": [
536 | {
537 | "name": "K",
538 | "type": 9
539 | },
540 | {
541 | "name": "V",
542 | "type": 10
543 | }
544 | ],
545 | "path": [
546 | "ink_storage",
547 | "lazy",
548 | "mapping",
549 | "Mapping"
550 | ]
551 | }
552 | },
553 | {
554 | "id": 9,
555 | "type": {
556 | "def": {
557 | "tuple": [
558 | 3,
559 | 0
560 | ]
561 | }
562 | }
563 | },
564 | {
565 | "id": 10,
566 | "type": {
567 | "def": {
568 | "tuple": []
569 | }
570 | }
571 | },
572 | {
573 | "id": 11,
574 | "type": {
575 | "def": {
576 | "composite": {
577 | "fields": [
578 | {
579 | "name": "offset_key",
580 | "type": 7,
581 | "typeName": "Key"
582 | }
583 | ]
584 | }
585 | },
586 | "params": [
587 | {
588 | "name": "K",
589 | "type": 12
590 | },
591 | {
592 | "name": "V",
593 | "type": 6
594 | }
595 | ],
596 | "path": [
597 | "ink_storage",
598 | "lazy",
599 | "mapping",
600 | "Mapping"
601 | ]
602 | }
603 | },
604 | {
605 | "id": 12,
606 | "type": {
607 | "def": {
608 | "tuple": [
609 | 3,
610 | 3
611 | ]
612 | }
613 | }
614 | },
615 | {
616 | "id": 13,
617 | "type": {
618 | "def": {
619 | "composite": {
620 | "fields": [
621 | {
622 | "name": "offset_key",
623 | "type": 7,
624 | "typeName": "Key"
625 | }
626 | ]
627 | }
628 | },
629 | "params": [
630 | {
631 | "name": "K",
632 | "type": 9
633 | },
634 | {
635 | "name": "V",
636 | "type": 3
637 | }
638 | ],
639 | "path": [
640 | "ink_storage",
641 | "lazy",
642 | "mapping",
643 | "Mapping"
644 | ]
645 | }
646 | },
647 | {
648 | "id": 14,
649 | "type": {
650 | "def": {
651 | "variant": {
652 | "variants": [
653 | {
654 | "fields": [
655 | {
656 | "type": 3
657 | }
658 | ],
659 | "index": 0,
660 | "name": "Ok"
661 | },
662 | {
663 | "fields": [
664 | {
665 | "type": 15
666 | }
667 | ],
668 | "index": 1,
669 | "name": "Err"
670 | }
671 | ]
672 | }
673 | },
674 | "params": [
675 | {
676 | "name": "T",
677 | "type": 3
678 | },
679 | {
680 | "name": "E",
681 | "type": 15
682 | }
683 | ],
684 | "path": [
685 | "Result"
686 | ]
687 | }
688 | },
689 | {
690 | "id": 15,
691 | "type": {
692 | "def": {
693 | "variant": {
694 | "variants": [
695 | {
696 | "index": 0,
697 | "name": "BadOrigin"
698 | },
699 | {
700 | "index": 1,
701 | "name": "BadgeNotFound"
702 | },
703 | {
704 | "index": 2,
705 | "name": "NotAnIssuer"
706 | },
707 | {
708 | "index": 3,
709 | "name": "NotFound"
710 | },
711 | {
712 | "index": 4,
713 | "name": "RunOutOfCode"
714 | },
715 | {
716 | "index": 5,
717 | "name": "Duplicated"
718 | }
719 | ]
720 | }
721 | },
722 | "path": [
723 | "fat_badges",
724 | "fat_badges",
725 | "Error"
726 | ]
727 | }
728 | },
729 | {
730 | "id": 16,
731 | "type": {
732 | "def": {
733 | "variant": {
734 | "variants": [
735 | {
736 | "fields": [
737 | {
738 | "type": 10
739 | }
740 | ],
741 | "index": 0,
742 | "name": "Ok"
743 | },
744 | {
745 | "fields": [
746 | {
747 | "type": 15
748 | }
749 | ],
750 | "index": 1,
751 | "name": "Err"
752 | }
753 | ]
754 | }
755 | },
756 | "params": [
757 | {
758 | "name": "T",
759 | "type": 10
760 | },
761 | {
762 | "name": "E",
763 | "type": 15
764 | }
765 | ],
766 | "path": [
767 | "Result"
768 | ]
769 | }
770 | },
771 | {
772 | "id": 17,
773 | "type": {
774 | "def": {
775 | "sequence": {
776 | "type": 6
777 | }
778 | }
779 | }
780 | },
781 | {
782 | "id": 18,
783 | "type": {
784 | "def": {
785 | "variant": {
786 | "variants": [
787 | {
788 | "fields": [
789 | {
790 | "type": 5
791 | }
792 | ],
793 | "index": 0,
794 | "name": "Ok"
795 | },
796 | {
797 | "fields": [
798 | {
799 | "type": 15
800 | }
801 | ],
802 | "index": 1,
803 | "name": "Err"
804 | }
805 | ]
806 | }
807 | },
808 | "params": [
809 | {
810 | "name": "T",
811 | "type": 5
812 | },
813 | {
814 | "name": "E",
815 | "type": 15
816 | }
817 | ],
818 | "path": [
819 | "Result"
820 | ]
821 | }
822 | },
823 | {
824 | "id": 19,
825 | "type": {
826 | "def": {
827 | "primitive": "bool"
828 | }
829 | }
830 | },
831 | {
832 | "id": 20,
833 | "type": {
834 | "def": {
835 | "variant": {
836 | "variants": [
837 | {
838 | "fields": [
839 | {
840 | "type": 6
841 | }
842 | ],
843 | "index": 0,
844 | "name": "Ok"
845 | },
846 | {
847 | "fields": [
848 | {
849 | "type": 15
850 | }
851 | ],
852 | "index": 1,
853 | "name": "Err"
854 | }
855 | ]
856 | }
857 | },
858 | "params": [
859 | {
860 | "name": "T",
861 | "type": 6
862 | },
863 | {
864 | "name": "E",
865 | "type": 15
866 | }
867 | ],
868 | "path": [
869 | "Result"
870 | ]
871 | }
872 | }
873 | ]
874 | }
875 | }
--------------------------------------------------------------------------------
/easy_oracle/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "easy_oracle"
3 | version = "0.1.0"
4 | authors = ["Hang Yin "]
5 | edition = "2021"
6 |
7 | [dependencies]
8 | ink_prelude = { version = "3", default-features = false }
9 | ink_primitives = { version = "3", default-features = false }
10 | ink_metadata = { version = "3", default-features = false, features = ["derive"], optional = true }
11 | ink_env = { version = "3", default-features = false }
12 | ink_storage = { version = "3", default-features = false }
13 | ink_lang = { version = "3", default-features = false }
14 |
15 | scale = { package = "parity-scale-codec", version = "3", default-features = false, features = ["derive"] }
16 | scale-info = { version = "2", default-features = false, features = ["derive"], optional = true }
17 | hex = { version = "0.4.3", default-features = false, features = ["alloc"] }
18 |
19 | openbrush = { path = "../vendor/openbrush-contracts", version = "~2.1.0", default-features = false }
20 | pink-extension = { version = "0.1.17", default-features = false }
21 | pink-utils = { version = "0.1", default-features = false }
22 |
23 | fat_badges = { path = "../fat_badges", default-features = false, features = ["ink-as-dependency"] }
24 |
25 | [dev-dependencies]
26 | environmental = { path = "../utils/environmental", default-features = false }
27 | pink-extension-runtime = "0.1.3"
28 |
29 | [lib]
30 | name = "easy_oracle"
31 | path = "lib.rs"
32 | crate-type = [
33 | # Used for normal contract Wasm blobs.
34 | "cdylib",
35 | ]
36 |
37 | [features]
38 | default = ["std"]
39 | std = [
40 | "ink_metadata/std",
41 | "ink_env/std",
42 | "ink_storage/std",
43 | "ink_primitives/std",
44 | "openbrush/std",
45 | "scale/std",
46 | "scale-info/std",
47 | "pink-extension/std",
48 | "pink-utils/std",
49 | "fat_badges/std",
50 | ]
51 | ink-as-dependency = []
52 | mockable = [
53 | "fat_badges/mockable",
54 | "openbrush/mockable",
55 | ]
56 |
--------------------------------------------------------------------------------
/easy_oracle/lib.rs:
--------------------------------------------------------------------------------
1 | #![cfg_attr(not(feature = "std"), no_std)]
2 | #![feature(trace_macros)]
3 |
4 | use ink_env::AccountId;
5 | use ink_lang as ink;
6 | use ink_prelude::{string::String, vec::Vec};
7 | use pink_extension as pink;
8 | use pink_utils::attestation;
9 |
10 | #[ink::trait_definition]
11 | pub trait SubmittableOracle {
12 | #[ink(message)]
13 | fn admin(&self) -> AccountId;
14 |
15 | #[ink(message)]
16 | fn verifier(&self) -> attestation::Verifier;
17 |
18 | #[ink(message)]
19 | fn attest(&self, arg: String) -> Result>;
20 | }
21 |
22 | #[pink::contract(env=PinkEnvironment)]
23 | mod easy_oracle {
24 | use super::pink;
25 | use super::SubmittableOracle;
26 | use pink::{http_get, PinkEnvironment};
27 |
28 | use ink_prelude::{
29 | string::{String, ToString},
30 | vec::Vec,
31 | };
32 | use ink_storage::traits::SpreadAllocate;
33 | use ink_storage::Mapping;
34 | use pink_utils::attestation;
35 | use scale::{Decode, Encode};
36 |
37 | use fat_badges::issuable::IssuableRef;
38 |
39 | #[ink(storage)]
40 | #[derive(SpreadAllocate)]
41 | #[cfg_attr(feature = "std", derive(scale_info::TypeInfo))]
42 | pub struct EasyOracle {
43 | admin: AccountId,
44 | badge_contract_options: Option<(AccountId, u32)>,
45 | attestation_verifier: attestation::Verifier,
46 | attestation_generator: attestation::Generator,
47 | linked_users: Mapping,
48 | }
49 |
50 | /// Errors that can occur upon calling this contract.
51 | #[derive(Debug, PartialEq, Eq, Encode, Decode)]
52 | #[cfg_attr(feature = "std", derive(scale_info::TypeInfo))]
53 | pub enum Error {
54 | BadOrigin,
55 | BadgeContractNotSetUp,
56 | InvalidUrl,
57 | RequestFailed,
58 | NoClaimFound,
59 | InvalidAddressLength,
60 | InvalidAddress,
61 | NoPermission,
62 | InvalidSignature,
63 | UsernameAlreadyInUse,
64 | AccountAlreadyInUse,
65 | FailedToIssueBadge,
66 | }
67 |
68 | /// Type alias for the contract's result type.
69 | pub type Result = core::result::Result;
70 |
71 | impl EasyOracle {
72 | #[ink(constructor)]
73 | pub fn new() -> Self {
74 | // Create the attestation helpers
75 | let (generator, verifier) = attestation::create(b"gist-attestation-key");
76 | // Save sender as the contract admin
77 | let admin = Self::env().caller();
78 |
79 | ink_lang::utils::initialize_contract(|this: &mut Self| {
80 | this.admin = admin;
81 | this.badge_contract_options = None;
82 | this.attestation_generator = generator;
83 | this.attestation_verifier = verifier;
84 | })
85 | }
86 |
87 | /// Sets the downstream badge contract
88 | ///
89 | /// Only the admin can call it.
90 | #[ink(message)]
91 | pub fn config_issuer(&mut self, contract: AccountId, badge_id: u32) -> Result<()> {
92 | let caller = self.env().caller();
93 | if caller != self.admin {
94 | return Err(Error::BadOrigin);
95 | }
96 | // Create a reference to the already deployed FatBadges contract
97 | self.badge_contract_options = Some((contract, badge_id));
98 | Ok(())
99 | }
100 |
101 | /// Redeems a POAP with a signed `attestation`. (callable)
102 | ///
103 | /// The attestation must be created by [`attest_gist`] function. After the verification of
104 | /// the attestation, the the sender account will the linked to a Github username. Then a
105 | /// POAP redemption code will be allocated to the sender.
106 | ///
107 | /// Each blockchain account and github account can only be linked once.
108 | #[ink(message)]
109 | pub fn redeem(&mut self, attestation: attestation::Attestation) -> Result<()> {
110 | // Verify the attestation
111 | let data: GistQuote = self
112 | .attestation_verifier
113 | .verify_as(&attestation)
114 | .ok_or(Error::InvalidSignature)?;
115 | // The caller must be the attested account
116 | if data.account_id != self.env().caller() {
117 | pink::warn!("No permission.");
118 | return Err(Error::NoPermission);
119 | }
120 | // The github username can only link to one account
121 | if self.linked_users.contains(&data.username) {
122 | pink::warn!("Username alreay in use.");
123 | return Err(Error::UsernameAlreadyInUse);
124 | }
125 | self.linked_users.insert(&data.username, &());
126 | // Call the badges contract to issue the NFT
127 | let (contract, id) = self
128 | .badge_contract_options
129 | .as_mut()
130 | .ok_or(Error::BadgeContractNotSetUp)?;
131 |
132 | let badges: &IssuableRef = contract;
133 | let result = badges.issue(*id, data.account_id);
134 | pink::warn!("Badges.issue() result = {:?}", result);
135 | result.or(Err(Error::FailedToIssueBadge))
136 | }
137 | }
138 |
139 | impl SubmittableOracle for EasyOracle {
140 | // Queries
141 |
142 | /// Attests a Github Gist by the raw file url. (Query only)
143 | ///
144 | /// It sends a HTTPS request to the url and extract an address from the claim ("This gist
145 | /// is owned by address: 0x..."). Once the claim is verified, it returns a signed
146 | /// attestation with the data `(username, account_id)`.
147 | ///
148 | /// The `Err` variant of the result is an encoded `Error` to simplify cross-contract calls.
149 | /// Particularly, when another contract wants to call us, they may not want to depend on
150 | /// any special type defined by us (`Error` in this case). So we only return generic types.
151 | #[ink(message)]
152 | fn attest(&self, url: String) -> core::result::Result> {
153 | // Verify the URL
154 | let gist_url = parse_gist_url(&url).map_err(|e| e.encode())?;
155 | // Fetch the gist content
156 | let resposne = http_get!(url);
157 | if resposne.status_code != 200 {
158 | return Err(Error::RequestFailed.encode());
159 | }
160 | let body = resposne.body;
161 | // Verify the claim and extract the account id
162 | let account_id = extract_claim(&body).map_err(|e| e.encode())?;
163 | let quote = GistQuote {
164 | username: gist_url.username,
165 | account_id,
166 | };
167 | let result = self.attestation_generator.sign(quote);
168 | Ok(result)
169 | }
170 |
171 | #[ink(message)]
172 | fn admin(&self) -> AccountId {
173 | self.admin.clone()
174 | }
175 |
176 | /// The attestation verifier
177 | #[ink(message)]
178 | fn verifier(&self) -> attestation::Verifier {
179 | self.attestation_verifier.clone()
180 | }
181 | }
182 |
183 | #[derive(PartialEq, Eq, Debug)]
184 | struct GistUrl {
185 | username: String,
186 | gist_id: String,
187 | filename: String,
188 | }
189 |
190 | #[derive(Clone, Encode, Decode, Debug)]
191 | #[cfg_attr(feature = "std", derive(scale_info::TypeInfo))]
192 | pub struct GistQuote {
193 | username: String,
194 | account_id: AccountId,
195 | }
196 |
197 | /// Parses a Github Gist url.
198 | ///
199 | /// - Returns a parsed [GistUrl] struct if the input is a valid url;
200 | /// - Otherwise returns an [Error].
201 | fn parse_gist_url(url: &str) -> Result {
202 | let path = url
203 | .strip_prefix("https://gist.githubusercontent.com/")
204 | .ok_or(Error::InvalidUrl)?;
205 | let components: Vec<_> = path.split('/').collect();
206 | if components.len() < 5 {
207 | return Err(Error::InvalidUrl);
208 | }
209 | Ok(GistUrl {
210 | username: components[0].to_string(),
211 | gist_id: components[1].to_string(),
212 | filename: components[4].to_string(),
213 | })
214 | }
215 |
216 | const CLAIM_PREFIX: &str = "This gist is owned by address: 0x";
217 | const ADDRESS_LEN: usize = 64;
218 |
219 | /// Extracts the ownerhip of the gist from a claim in the gist body.
220 | ///
221 | /// A valid claim must have the statement "This gist is owned by address: 0x..." in `body`. The
222 | /// address must be the 256 bits public key of the Substrate account in hex.
223 | ///
224 | /// - Returns a 256-bit `AccountId` representing the owner account if the claim is valid;
225 | /// - otherwise returns an [Error].
226 | fn extract_claim(body: &[u8]) -> Result {
227 | let body = String::from_utf8_lossy(body);
228 | let pos = body.find(CLAIM_PREFIX).ok_or(Error::NoClaimFound)?;
229 | let addr: String = body
230 | .chars()
231 | .skip(pos)
232 | .skip(CLAIM_PREFIX.len())
233 | .take(ADDRESS_LEN)
234 | .collect();
235 | let addr = addr.as_bytes();
236 | let account_id = decode_accountid_256(addr)?;
237 | Ok(account_id)
238 | }
239 |
240 | /// Decodes a hex string as an 256-bit AccountId32
241 | fn decode_accountid_256(addr: &[u8]) -> Result {
242 | use hex::FromHex;
243 | if addr.len() != ADDRESS_LEN {
244 | return Err(Error::InvalidAddressLength);
245 | }
246 | let bytes = <[u8; 32]>::from_hex(addr).or(Err(Error::InvalidAddress))?;
247 | Ok(AccountId::from(bytes))
248 | }
249 |
250 | #[cfg(test)]
251 | mod tests {
252 | use super::*;
253 | use ink_lang as ink;
254 |
255 | fn default_accounts() -> ink_env::test::DefaultAccounts {
256 | ink_env::test::default_accounts::()
257 | }
258 |
259 | #[ink::test]
260 | fn can_parse_gist_url() {
261 | let result = parse_gist_url("https://gist.githubusercontent.com/h4x3rotab/0cabeb528bdaf30e4cf741e26b714e04/raw/620f958fb92baba585a77c1854d68dc986803b4e/test%2520gist");
262 | assert_eq!(
263 | result,
264 | Ok(GistUrl {
265 | username: "h4x3rotab".to_string(),
266 | gist_id: "0cabeb528bdaf30e4cf741e26b714e04".to_string(),
267 | filename: "test%2520gist".to_string(),
268 | })
269 | );
270 | let err = parse_gist_url("http://example.com");
271 | assert_eq!(err, Err(Error::InvalidUrl));
272 | }
273 |
274 | #[ink::test]
275 | fn can_decode_claim() {
276 | let ok = extract_claim(b"...This gist is owned by address: 0x0123456789012345678901234567890123456789012345678901234567890123...");
277 | assert_eq!(
278 | ok,
279 | decode_accountid_256(
280 | b"0123456789012345678901234567890123456789012345678901234567890123"
281 | )
282 | );
283 | // Bad cases
284 | assert_eq!(
285 | extract_claim(b"This gist is owned by"),
286 | Err(Error::NoClaimFound),
287 | );
288 | assert_eq!(
289 | extract_claim(b"This gist is owned by address: 0xAB"),
290 | Err(Error::InvalidAddressLength),
291 | );
292 | assert_eq!(
293 | extract_claim(b"This gist is owned by address: 0xXX23456789012345678901234567890123456789012345678901234567890123"),
294 | Err(Error::InvalidAddress),
295 | );
296 | }
297 |
298 | #[ink::test]
299 | fn end_to_end() {
300 | use pink_extension::chain_extension::{mock, HttpResponse};
301 | pink_extension_runtime::mock_ext::mock_all_ext();
302 |
303 | // Test accounts
304 | let accounts = default_accounts();
305 |
306 | use fat_badges::issuable::mock_issuable;
307 | use openbrush::traits::mock::{Addressable, SharedCallStack};
308 |
309 | let stack = SharedCallStack::new(accounts.alice);
310 | mock_issuable::using(stack.clone(), || {
311 | // Deploy a FatBadges contract
312 | let badges = mock_issuable::deploy(fat_badges::FatBadges::new());
313 |
314 | // Construct our contract (deployed by `accounts.alice` by default)
315 | let contract = Addressable::create_native(1, EasyOracle::new(), stack);
316 |
317 | // Create a badge and add the oracle contract as its issuer
318 | let id = badges
319 | .call_mut()
320 | .new_badge("test-badge".to_string())
321 | .unwrap();
322 | assert!(badges
323 | .call_mut()
324 | .add_code(id, vec!["code1".to_string(), "code2".to_string()])
325 | .is_ok());
326 | assert!(badges.call_mut().add_issuer(id, contract.id()).is_ok());
327 | // Tell the oracle the badges are ready to issue
328 | assert!(contract.call_mut().config_issuer(badges.id(), id).is_ok());
329 |
330 | // Generate an attestation
331 | //
332 | // Mock a http request first (the 256 bits account id is the pubkey of Alice)
333 | mock::mock_http_request(|_| {
334 | HttpResponse::ok(b"This gist is owned by address: 0x0101010101010101010101010101010101010101010101010101010101010101".to_vec())
335 | });
336 | let result = contract.call().attest("https://gist.githubusercontent.com/h4x3rotab/0cabeb528bdaf30e4cf741e26b714e04/raw/620f958fb92baba585a77c1854d68dc986803b4e/test%2520gist".to_string());
337 | assert!(result.is_ok());
338 |
339 | let attestation = result.unwrap();
340 | let data: GistQuote = Decode::decode(&mut &attestation.data[..]).unwrap();
341 | assert_eq!(data.username, "h4x3rotab");
342 | assert_eq!(data.account_id, accounts.alice);
343 |
344 | // Before redeem
345 | assert!(badges.call().get(id).is_err());
346 |
347 | // Redeem and check if the contract as the code distributed
348 | contract
349 | .call_mut()
350 | .redeem(attestation)
351 | .expect("Should be able to issue badge");
352 | assert_eq!(badges.call().get(id), Ok("code1".to_string()));
353 | });
354 | }
355 | }
356 | }
357 |
--------------------------------------------------------------------------------
/fat_badges/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "fat_badges"
3 | version = "0.1.0"
4 | authors = ["Hang Yin "]
5 | edition = "2021"
6 |
7 | [dependencies]
8 | ink_prelude = { version = "3", default-features = false }
9 | ink_primitives = { version = "3", default-features = false }
10 | ink_metadata = { version = "3", default-features = false, features = ["derive"], optional = true }
11 | ink_env = { version = "3", default-features = false }
12 | ink_storage = { version = "3", default-features = false }
13 | ink_lang = { version = "3", default-features = false }
14 |
15 | scale = { package = "parity-scale-codec", version = "3", default-features = false, features = ["derive"] }
16 | scale-info = { version = "2", default-features = false, features = ["derive"], optional = true }
17 |
18 | openbrush = { path = "../vendor/openbrush-contracts", version = "~2.1.0", default-features = false }
19 | environmental = { path = "../utils/environmental", default-features = false, optional = true }
20 |
21 | [lib]
22 | name = "fat_badges"
23 | path = "lib.rs"
24 | crate-type = [
25 | # Used for normal contract Wasm blobs.
26 | "cdylib",
27 | # Used for ABI generation.
28 | "rlib",
29 | ]
30 |
31 | [features]
32 | default = ["std"]
33 | std = [
34 | "ink_metadata/std",
35 | "ink_env/std",
36 | "ink_storage/std",
37 | "ink_primitives/std",
38 | "scale/std",
39 | "scale-info/std",
40 | ]
41 | ink-as-dependency = []
42 | mockable = [
43 | "environmental",
44 | "openbrush/mockable",
45 | ]
--------------------------------------------------------------------------------
/fat_badges/lib.rs:
--------------------------------------------------------------------------------
1 | #![cfg_attr(not(feature = "std"), no_std)]
2 |
3 | use ink_lang as ink;
4 |
5 | pub use crate::fat_badges::{FatBadges, Result};
6 |
7 | // Define a trait for cross-contract call. Necessary to enable it in unit tests.
8 | pub mod issuable {
9 | use ink_env::AccountId;
10 | use ink_lang as ink;
11 |
12 | #[openbrush::trait_definition(mock = crate::FatBadges)]
13 | pub trait Issuable {
14 | #[ink(message)]
15 | fn issue(&mut self, id: u32, dest: AccountId) -> crate::Result<()>;
16 | }
17 |
18 | #[openbrush::wrapper]
19 | pub type IssuableRef = dyn Issuable;
20 | }
21 |
22 | #[openbrush::contract]
23 | mod fat_badges {
24 | use super::issuable::*;
25 | use ink_lang::codegen::Env;
26 | use ink_prelude::{string::String, vec::Vec};
27 | use ink_storage::traits::{PackedLayout, SpreadAllocate, SpreadLayout};
28 | use ink_storage::Mapping;
29 | use scale::{Decode, Encode};
30 |
31 | #[ink(storage)]
32 | #[derive(SpreadAllocate)]
33 | #[cfg_attr(feature = "std", derive(scale_info::TypeInfo))]
34 | pub struct FatBadges {
35 | admin: AccountId,
36 | total_badges: u32,
37 | badge_info: Mapping,
38 | badge_issuers: Mapping<(u32, AccountId), ()>,
39 | badge_code: Mapping<(u32, u32), String>,
40 | badge_assignments: Mapping<(u32, AccountId), u32>,
41 | }
42 |
43 | /// Errors that can occur upon calling this contract.
44 | #[derive(Debug, PartialEq, Eq, Encode, Decode)]
45 | #[cfg_attr(feature = "std", derive(scale_info::TypeInfo))]
46 | pub enum Error {
47 | BadOrigin,
48 | BadgeNotFound,
49 | NotAnIssuer,
50 | NotFound,
51 | RunOutOfCode,
52 | Duplicated,
53 | }
54 |
55 | /// Type alias for the contract's result type.
56 | pub type Result = core::result::Result;
57 |
58 | /// The basic information of a badge
59 | #[derive(
60 | Debug, PartialEq, Encode, Decode, Clone, SpreadLayout, PackedLayout, SpreadAllocate,
61 | )]
62 | #[cfg_attr(
63 | feature = "std",
64 | derive(scale_info::TypeInfo, ink_storage::traits::StorageLayout,)
65 | )]
66 | pub struct BadgeInfo {
67 | /// Badge ID
68 | id: u32,
69 | /// The admin to manage the badge
70 | admin: AccountId,
71 | /// Name of the badge
72 | name: String,
73 | /// Total available redeem code
74 | num_code: u32,
75 | /// The number of issued badges
76 | num_issued: u32,
77 | }
78 |
79 | impl FatBadges {
80 | #[ink(constructor)]
81 | pub fn new() -> Self {
82 | ink_lang::utils::initialize_contract(|this: &mut Self| {
83 | this.admin = Self::env().caller();
84 | this.total_badges = 0;
85 | })
86 | }
87 |
88 | // Commands
89 |
90 | /// Creates a new badge and become the admin of the badge
91 | ///
92 | /// Return the id of the badge.
93 | #[ink(message)]
94 | pub fn new_badge(&mut self, name: String) -> Result {
95 | let caller = self.env().caller();
96 | let id = self.total_badges;
97 | let badge = BadgeInfo {
98 | id,
99 | admin: caller,
100 | name,
101 | num_code: 0,
102 | num_issued: 0,
103 | };
104 | self.badge_info.insert(id, &badge);
105 | self.total_badges += 1;
106 | Ok(id)
107 | }
108 |
109 | /// Adds a badge issuer
110 | ///
111 | /// The caller must be the badge admin.
112 | #[ink(message)]
113 | pub fn add_issuer(&mut self, id: u32, issuer: AccountId) -> Result<()> {
114 | self.ensure_badge_admin(id)?;
115 | self.badge_issuers.insert((id, issuer), &());
116 | Ok(())
117 | }
118 |
119 | /// Removes a badge issuer
120 | ///
121 | /// The caller must be the badge admin.
122 | #[ink(message)]
123 | pub fn remove_issuer(&mut self, id: u32, issuer: AccountId) -> Result<()> {
124 | self.ensure_badge_admin(id)?;
125 | self.badge_issuers.remove((id, issuer));
126 | Ok(())
127 | }
128 |
129 | /// Appends a list of redeem code to a badge
130 | ///
131 | /// The caller must be the badge admin.
132 | #[ink(message)]
133 | pub fn add_code(&mut self, id: u32, code: Vec) -> Result<()> {
134 | let mut badge = self.ensure_badge_admin(id)?;
135 | let start = badge.num_code;
136 | badge.num_code += code.len() as u32;
137 | for (i, entry) in code.iter().enumerate() {
138 | let idx = (i as u32) + start;
139 | self.badge_code.insert((id, idx), entry);
140 | }
141 | self.badge_info.insert(id, &badge);
142 | Ok(())
143 | }
144 |
145 | // Queries
146 |
147 | /// Returns the number of all the badges
148 | #[ink(message)]
149 | pub fn get_total_badges(&self) -> u32 {
150 | self.total_badges
151 | }
152 |
153 | /// Returns the detailed information of a badge
154 | #[ink(message)]
155 | pub fn get_badge_info(&self, id: u32) -> Result {
156 | self.badge_info.get(id).ok_or(Error::BadgeNotFound)
157 | }
158 |
159 | /// Checks if an account is a badge issuer
160 | #[ink(message)]
161 | pub fn is_badge_issuer(&self, id: u32, issuer: AccountId) -> bool {
162 | self.badge_issuers.contains((id, issuer))
163 | }
164 |
165 | /// Reads the badge code assigned to the caller if exists
166 | #[ink(message)]
167 | pub fn get(&self, id: u32) -> Result {
168 | let caller = self.env().caller();
169 | let code_idx = self
170 | .badge_assignments
171 | .get((id, caller))
172 | .ok_or(Error::NotFound)?;
173 | let code = self
174 | .badge_code
175 | .get((id, code_idx))
176 | .expect("Assigned code exists; qed.");
177 | Ok(code)
178 | }
179 |
180 | // Helper functions
181 |
182 | /// Returns the badge info if it exists
183 | fn ensure_badge(&self, id: u32) -> Result {
184 | self.badge_info.get(id).ok_or(Error::BadgeNotFound)
185 | }
186 |
187 | /// Returns the badge if the it exists and the caller is the admin
188 | fn ensure_badge_admin(&self, id: u32) -> Result {
189 | let caller = self.env().caller();
190 | let badge = self.badge_info.get(id).ok_or(Error::BadgeNotFound)?;
191 | if badge.admin != caller {
192 | return Err(Error::BadOrigin);
193 | }
194 | Ok(badge)
195 | }
196 | }
197 |
198 | impl Issuable for FatBadges {
199 | /// Issues a badge to the `dest` account
200 | ///
201 | /// The caller must be the badge admin or a badge issuer. Return a `RunOutOfCode` error
202 | /// when there's no enough redeem code to issue.
203 | #[ink(message)]
204 | fn issue(&mut self, id: u32, dest: AccountId) -> Result<()> {
205 | let caller = self.env().caller();
206 | let mut badge = self.ensure_badge(id)?;
207 | // Allow the badge issuers or the badge admin
208 | if !self.badge_issuers.contains((id, caller)) && caller != badge.admin {
209 | return Err(Error::NotAnIssuer);
210 | }
211 | // Make sure we don't issue more than what we have
212 | if badge.num_issued >= badge.num_code {
213 | return Err(Error::RunOutOfCode);
214 | }
215 | // No duplication
216 | if self.badge_assignments.contains((id, dest)) {
217 | return Err(Error::Duplicated);
218 | }
219 | // Update assignment and issued count
220 | let idx = badge.num_issued;
221 | self.badge_assignments.insert((id, dest), &idx);
222 | badge.num_issued += 1;
223 | self.badge_info.insert(id, &badge);
224 | Ok(())
225 | }
226 | }
227 |
228 | #[cfg(test)]
229 | mod tests {
230 | use super::*;
231 | use ink_lang as ink;
232 | use openbrush::traits::mock::{Addressable, SharedCallStack};
233 |
234 | fn default_accounts() -> ink_env::test::DefaultAccounts {
235 | ink_env::test::default_accounts::()
236 | }
237 |
238 | #[ink::test]
239 | fn issue_badges() {
240 | let accounts = default_accounts();
241 |
242 | let stack = SharedCallStack::new(accounts.alice);
243 | let fat_badges = Addressable::create_native(1, FatBadges::new(), stack.clone());
244 | assert_eq!(fat_badges.call().admin, accounts.alice);
245 |
246 | // Alice can create a badge
247 | let id = fat_badges
248 | .call_mut()
249 | .new_badge("Phala Workshop: Easy".to_string())
250 | .expect("Should be able to create badges");
251 |
252 | // Can add an issuer
253 | assert!(fat_badges.call_mut().add_issuer(id, accounts.bob).is_ok());
254 |
255 | // Bob can create another badge
256 | stack.switch_account(accounts.bob).unwrap();
257 | let id_adv = fat_badges
258 | .call_mut()
259 | .new_badge("Phala Workshop: Advanced".to_string())
260 | .expect("Should be able to create badges");
261 | stack.switch_account(accounts.alice).unwrap();
262 | assert_eq!(
263 | fat_badges.call_mut().add_issuer(id_adv, accounts.bob),
264 | Err(Error::BadOrigin),
265 | "Only the badge owner can add issuers"
266 | );
267 | assert_eq!(
268 | fat_badges.call_mut().add_issuer(999, accounts.bob),
269 | Err(Error::BadgeNotFound),
270 | "Non-existing badge"
271 | );
272 |
273 | // Can remove an issuer
274 | assert!(fat_badges
275 | .call_mut()
276 | .add_issuer(id, accounts.charlie)
277 | .is_ok());
278 | assert!(fat_badges
279 | .call_mut()
280 | .remove_issuer(id, accounts.charlie)
281 | .is_ok());
282 | assert!(!fat_badges.call().is_badge_issuer(id, accounts.charlie));
283 |
284 | // Can add code
285 | assert!(fat_badges
286 | .call_mut()
287 | .add_code(id, vec!["code1".to_string(), "code2".to_string()])
288 | .is_ok());
289 | stack.switch_account(accounts.bob).unwrap();
290 | assert_eq!(
291 | fat_badges.call_mut().add_code(id, vec![]),
292 | Err(Error::BadOrigin),
293 | "Only the badge owner can add code"
294 | );
295 |
296 | // Check the badge stats
297 | let badge = fat_badges.call().get_badge_info(id).unwrap();
298 | assert_eq!(badge.num_code, 2);
299 | assert_eq!(badge.num_issued, 0);
300 |
301 | // Can issue badges to Django and Eve
302 | stack.switch_account(accounts.alice).unwrap();
303 | assert!(fat_badges.call_mut().issue(id, accounts.django).is_ok());
304 | assert_eq!(
305 | fat_badges.call_mut().issue(id, accounts.django),
306 | Err(Error::Duplicated),
307 | "Cannot issue duplicated badges"
308 | );
309 | stack.switch_account(accounts.bob).unwrap();
310 | assert!(fat_badges.call_mut().issue(id, accounts.eve).is_ok());
311 | assert_eq!(
312 | fat_badges.call_mut().issue(id, accounts.frank),
313 | Err(Error::RunOutOfCode),
314 | "No code available to issue badges"
315 | );
316 |
317 | // Adding a new code solves the problem
318 | stack.switch_account(accounts.alice).unwrap();
319 | assert!(fat_badges
320 | .call_mut()
321 | .add_code(id, vec!["code3".to_string()])
322 | .is_ok());
323 | assert!(fat_badges.call_mut().issue(id, accounts.frank).is_ok());
324 |
325 | // Code can be revealed
326 | stack.switch_account(accounts.django).unwrap();
327 | assert_eq!(fat_badges.call().get(id), Ok("code1".to_string()));
328 | stack.switch_account(accounts.eve).unwrap();
329 | assert_eq!(fat_badges.call().get(id), Ok("code2".to_string()));
330 | stack.switch_account(accounts.frank).unwrap();
331 | assert_eq!(fat_badges.call().get(id), Ok("code3".to_string()));
332 | stack.switch_account(accounts.alice).unwrap();
333 | assert_eq!(fat_badges.call().get(id), Err(Error::NotFound));
334 |
335 | // Final checks
336 | let badge = fat_badges.call().get_badge_info(id).unwrap();
337 | assert_eq!(badge.num_code, 3);
338 | assert_eq!(badge.num_issued, 3);
339 | assert_eq!(fat_badges.call().get_total_badges(), 2);
340 | }
341 | }
342 | }
343 |
--------------------------------------------------------------------------------
/rust-toolchain.toml:
--------------------------------------------------------------------------------
1 | [toolchain]
2 | channel = "nightly"
3 | components = [ "rustfmt" ]
4 | targets = [ "wasm32-unknown-unknown" ]
--------------------------------------------------------------------------------
/scripts/build.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | (cd fat_badges; cargo contract build) && \
4 | (cd easy_oracle; cargo contract build) && \
5 | (cd advanced_judger; cargo contract build)
6 |
--------------------------------------------------------------------------------
/scripts/collect-bin.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | for contract in $(ls target/ink); do
4 | mkdir -p "./bin/$contract"
5 | cp "target/ink/$contract/"*.{wasm,contract,json} "./bin/$contract"
6 | done
7 |
--------------------------------------------------------------------------------
/scripts/js/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | tmp/
3 | .env
--------------------------------------------------------------------------------
/scripts/js/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "oracle-workshop-scripts",
3 | "version": "1.0.0",
4 | "main": "index.js",
5 | "license": "MIT",
6 | "private": true,
7 | "dependencies": {
8 | "@phala/sdk": "^0.3.3",
9 | "@polkadot/api": "^9.4.1",
10 | "@polkadot/api-contract": "^9.4.1",
11 | "dotenv": "^16.0.1"
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/scripts/js/src/common.js:
--------------------------------------------------------------------------------
1 | const fs = require('fs');
2 | const crypto = require('crypto');
3 | const { checkUntil, checkUntilEq, hex } = require('./utils');
4 |
5 | const CONTRACT_NAMES = [
6 | ['fat_badges', 'FatBadges'],
7 | ['easy_oracle', 'EasyOracle'],
8 | ['advanced_judger', 'AdvancedJudger'],
9 | ]
10 |
11 | function loadContract(name) {
12 | const wasmPath = `../../target/ink/${name}/${name}.wasm`;
13 | const metadataPath = `../../target/ink/${name}/metadata.json`;
14 | const wasm = hex(fs.readFileSync(wasmPath, 'hex'));
15 | const metadata = JSON.parse(fs.readFileSync(metadataPath));
16 | const constructor = metadata.V3.spec.constructors.find(c => c.label == 'new').selector;
17 | return {wasm, metadata, constructor};
18 | }
19 |
20 | function loadArtifacts() {
21 | return Object.assign(
22 | {}, ...CONTRACT_NAMES.map(
23 | ([filename, name]) => ({[name]: loadContract(filename)})
24 | )
25 | );
26 | }
27 |
28 | async function deployContracts(api, txqueue, pair, artifacts, clusterId) {
29 | console.log('Contracts: uploading');
30 | // upload contracts
31 | const contractNames = Object.keys(artifacts);
32 | const { events: deployEvents } = await txqueue.submit(
33 | api.tx.utility.batchAll(
34 | Object.entries(artifacts).flatMap(([_k, v]) => [
35 | api.tx.phalaFatContracts.clusterUploadResource(clusterId, 'InkCode', v.wasm),
36 | api.tx.phalaFatContracts.instantiateContract(
37 | { WasmCode: v.metadata.source.hash },
38 | v.constructor,
39 | hex(crypto.randomBytes(4).toString('hex')), // salt
40 | clusterId,
41 | )
42 | ])
43 | ),
44 | pair
45 | );
46 | const contractIds = deployEvents
47 | .filter(ev => ev.event.section == 'phalaFatContracts' && ev.event.method == 'Instantiating')
48 | .map(ev => ev.event.data[0].toString());
49 | const numContracts = contractNames.length;
50 | console.assert(contractIds.length == numContracts, 'Incorrect length:', `${contractIds.length} vs ${numContracts}`);
51 | for (const [i, id] of contractIds.entries()) {
52 | artifacts[contractNames[i]].address = id;
53 | }
54 | await checkUntilEq(
55 | async () => (await api.query.phalaFatContracts.clusterContracts(clusterId))
56 | .filter(c => contractIds.includes(c.toString()) )
57 | .length,
58 | numContracts,
59 | 4 * 6000
60 | );
61 | console.log('Contracts: uploaded');
62 | for (const [name, contract] of Object.entries(artifacts)) {
63 | await checkUntil(
64 | async () => (await api.query.phalaRegistry.contractKeys(contract.address)).isSome,
65 | 4 * 6000
66 | );
67 | console.log('Contracts:', contract.address, name, 'key ready');
68 | }
69 | console.log('Contracts: deployed');
70 | }
71 |
72 | module.exports = {
73 | loadArtifacts,
74 | deployContracts,
75 | }
76 |
--------------------------------------------------------------------------------
/scripts/js/src/deploy.js:
--------------------------------------------------------------------------------
1 | require('dotenv').config();
2 | const fs = require('fs');
3 |
4 | const {ApiPromise, WsProvider, Keyring} = require('@polkadot/api');
5 | const {ContractPromise} = require('@polkadot/api-contract');
6 | const Phala = require('@phala/sdk');
7 |
8 | const { TxQueue, blockBarrier, hex } = require('./utils');
9 | const { loadArtifacts, deployContracts } = require('./common');
10 |
11 | function loadCode(path) {
12 | const content = fs.readFileSync(path, {encoding: 'utf-8'});
13 | return content.split('\n').map(x => x.trim()).filter(x => !!x);
14 | }
15 |
16 | async function main() {
17 | const clusterId = process.env.CLUSTER_ID || '0x0000000000000000000000000000000000000000000000000000000000000000';
18 | const privkey = process.env.PRIVKEY || '//Alice';
19 | const chainUrl = process.env.CHAIN || 'wss://poc5.phala.network/ws';
20 | const pruntimeUrl = process.env.PRUNTIME || 'https://poc5.phala.network/tee-api-1';
21 | const codeEasyCsv = process.env.CODE_EASY_CSV || './tmp/code-easy.csv';
22 | const codeAdvCsv = process.env.CODE_EASY_CSV || './tmp/code-adv.csv';
23 |
24 | const artifacts = loadArtifacts();
25 | const codeEasy = loadCode(codeEasyCsv);
26 | const codeAdv = loadCode(codeAdvCsv);
27 |
28 | // connect to the chain
29 | const wsProvider = new WsProvider(chainUrl);
30 | const api = await ApiPromise.create({
31 | provider: wsProvider,
32 | types: {
33 | ...Phala.types,
34 | 'GistQuote': {
35 | username: 'String',
36 | accountId: 'AccountId',
37 | },
38 | }
39 | });
40 | const txqueue = new TxQueue(api);
41 |
42 | // prepare accounts
43 | const keyring = new Keyring({type: 'sr25519'})
44 | const pair = keyring.addFromUri(privkey);
45 | console.log('Using account', pair.address);
46 | const cert = await Phala.signCertificate({api, pair});
47 |
48 | // connect to pruntime
49 | const prpc = Phala.createPruntimeApi(pruntimeUrl);
50 | const connectedWorker = hex((await prpc.getInfo({})).publicKey);
51 | console.log('Connected worker:', connectedWorker);
52 |
53 | // contracts
54 | await deployContracts(api, txqueue, pair, artifacts, clusterId);
55 |
56 | // create Fat Contract objects
57 | const contracts = {};
58 | for (const [name, contract] of Object.entries(artifacts)) {
59 | const contractId = contract.address;
60 | const newApi = await api.clone().isReady;
61 | contracts[name] = new ContractPromise(
62 | await Phala.create({api: newApi, baseURL: pruntimeUrl, contractId}),
63 | contract.metadata,
64 | contractId
65 | );
66 | }
67 | console.log('Fat Contract: connected');
68 | const { FatBadges, EasyOracle, AdvancedJudger } = contracts;
69 |
70 |
71 | // set up the contracts
72 | const easyBadgeId = 0;
73 | const advBadgeId = 1;
74 | await txqueue.submit(
75 | api.tx.utility.batchAll([
76 | // set up the badges; assume the ids are 0 and 1.
77 | FatBadges.tx.newBadge({}, 'fat-easy-challenge'),
78 | FatBadges.tx.newBadge({}, 'fat-adv-challenge'),
79 | // fill with code
80 | FatBadges.tx.addCode({}, easyBadgeId, codeEasy),
81 | FatBadges.tx.addCode({}, advBadgeId, codeAdv),
82 | // set the issuers
83 | FatBadges.tx.addIssuer({}, easyBadgeId, artifacts.EasyOracle.address),
84 | FatBadges.tx.addIssuer({}, advBadgeId, artifacts.AdvancedJudger.address),
85 | // config the issuers
86 | EasyOracle.tx.configIssuer({}, artifacts.FatBadges.address, easyBadgeId),
87 | AdvancedJudger.tx.configIssuer({}, artifacts.FatBadges.address, advBadgeId),
88 | ]),
89 | pair,
90 | true,
91 | );
92 |
93 | // wait for the worker to sync to the bockchain
94 | await blockBarrier(api, prpc);
95 |
96 | // basic checks
97 | console.log('Fat Contract: basic checks');
98 | console.assert(
99 | (await FatBadges.query.getTotalBadges(cert, {})).output.toNumber() == 2,
100 | 'Should have two badges created'
101 | );
102 |
103 | const easyInfo = await FatBadges.query.getBadgeInfo(cert, {}, easyBadgeId);
104 | console.log('Easy badge:', easyInfo.output.toHuman());
105 |
106 | const advInfo = await FatBadges.query.getBadgeInfo(cert, {}, advBadgeId);
107 | console.log('Adv badge:', advInfo.output.toHuman());
108 |
109 | console.log('Deployment finished');
110 | }
111 |
112 | main().then(process.exit).catch(err => console.error('Crashed', err)).finally(() => process.exit(-1));
--------------------------------------------------------------------------------
/scripts/js/src/e2e.js:
--------------------------------------------------------------------------------
1 | const fs = require('fs');
2 | const crypto = require('crypto');
3 |
4 | const {ApiPromise, WsProvider, Keyring} = require('@polkadot/api');
5 | const {ContractPromise} = require('@polkadot/api-contract');
6 | const Phala = require('@phala/sdk');
7 |
8 | const { TxQueue, checkUntil, blockBarrier, hex } = require('./utils');
9 | const { loadArtifacts, deployContracts } = require('./common');
10 |
11 | async function getWorkerPubkey(api) {
12 | const workers = await api.query.phalaRegistry.workers.entries();
13 | const worker = workers[0][0].args[0].toString();
14 | return worker;
15 | }
16 |
17 | async function setupGatekeeper(api, txpool, pair, worker) {
18 | if ((await api.query.phalaRegistry.gatekeeper()).length > 0) {
19 | return;
20 | }
21 | console.log('Gatekeeper: registering');
22 | await txpool.submit(
23 | api.tx.sudo.sudo(
24 | api.tx.phalaRegistry.registerGatekeeper(worker)
25 | ),
26 | pair,
27 | );
28 | await checkUntil(
29 | async () => (await api.query.phalaRegistry.gatekeeper()).length == 1,
30 | 4 * 6000
31 | );
32 | console.log('Gatekeeper: added');
33 | await checkUntil(
34 | async () => (await api.query.phalaRegistry.gatekeeperMasterPubkey()).isSome,
35 | 4 * 6000
36 | );
37 | console.log('Gatekeeper: master key ready');
38 | }
39 |
40 | async function deployCluster(api, txqueue, pair, worker, defaultCluster = '0x0000000000000000000000000000000000000000000000000000000000000000') {
41 | if ((await api.query.phalaRegistry.clusterKeys(defaultCluster)).isSome) {
42 | return defaultCluster;
43 | }
44 | console.log('Cluster: creating');
45 | // crete contract cluster and wait for the setup
46 | const { events } = await txqueue.submit(
47 | api.tx.phalaFatContracts.addCluster(
48 | 'Public', // can be {'OnlyOwner': accountId}
49 | [worker]
50 | ),
51 | pair
52 | );
53 | const ev = events[1].event;
54 | console.assert(ev.section == 'phalaFatContracts' && ev.method == 'ClusterCreated');
55 | const clusterId = ev.data[0].toString();
56 | console.log('Cluster: created', clusterId)
57 | await checkUntil(
58 | async () => (await api.query.phalaRegistry.clusterKeys(clusterId)).isSome,
59 | 4 * 6000
60 | );
61 | return clusterId;
62 | }
63 |
64 | async function main() {
65 | const artifacts = loadArtifacts();
66 |
67 | // connect to the chain
68 | const wsProvider = new WsProvider('ws://localhost:19944');
69 | const api = await ApiPromise.create({
70 | provider: wsProvider,
71 | types: {
72 | ...Phala.types,
73 | 'GistQuote': {
74 | username: 'String',
75 | accountId: 'AccountId',
76 | },
77 | }
78 | });
79 | const txqueue = new TxQueue(api);
80 |
81 | // prepare accounts
82 | const keyring = new Keyring({type: 'sr25519'})
83 | const alice = keyring.addFromUri('//Alice')
84 | const bob = keyring.addFromUri('//Bob')
85 | const certAlice = await Phala.signCertificate({api, pair: alice});
86 | const certBob = await Phala.signCertificate({api, pair: bob});
87 |
88 | // connect to pruntime
89 | const pruntimeURL = 'http://localhost:18000';
90 | const prpc = Phala.createPruntimeApi(pruntimeURL);
91 | const worker = await getWorkerPubkey(api);
92 | const connectedWorker = hex((await prpc.getInfo({})).publicKey);
93 | console.log('Worker:', worker);
94 | console.log('Connected worker:', connectedWorker);
95 |
96 | // basic phala network setup
97 | await setupGatekeeper(api, txqueue, alice, worker);
98 | const clusterId = await deployCluster(api, txqueue, alice, worker);
99 |
100 | // contracts
101 | await deployContracts(api, txqueue, bob, artifacts, clusterId);
102 |
103 | // create Fat Contract objects
104 | const contracts = {};
105 | for (const [name, contract] of Object.entries(artifacts)) {
106 | const contractId = contract.address;
107 | const newApi = await api.clone().isReady;
108 | contracts[name] = new ContractPromise(
109 | await Phala.create({api: newApi, baseURL: pruntimeURL, contractId}),
110 | contract.metadata,
111 | contractId
112 | );
113 | }
114 | console.log('Fat Contract: connected');
115 | const { FatBadges, EasyOracle, AdvancedJudger } = contracts;
116 |
117 | // set up the contracts
118 | const easyBadgeId = 0;
119 | const advBadgeId = 1;
120 | await txqueue.submit(
121 | api.tx.utility.batchAll([
122 | // set up the badges; assume the ids are 0 and 1.
123 | FatBadges.tx.newBadge({}, 'fat-easy-challenge'),
124 | FatBadges.tx.newBadge({}, 'fat-adv-challenge'),
125 | // fill with code
126 | FatBadges.tx.addCode({}, easyBadgeId, ['easy1', 'easy2']),
127 | FatBadges.tx.addCode({}, advBadgeId, ['adv1', 'adv2']),
128 | // set the issuers
129 | FatBadges.tx.addIssuer({}, easyBadgeId, artifacts.EasyOracle.address),
130 | FatBadges.tx.addIssuer({}, advBadgeId, artifacts.AdvancedJudger.address),
131 | // config the issuers
132 | EasyOracle.tx.configIssuer({}, artifacts.FatBadges.address, easyBadgeId),
133 | AdvancedJudger.tx.configIssuer({}, artifacts.FatBadges.address, advBadgeId),
134 | ]),
135 | bob,
136 | true,
137 | );
138 |
139 | // wait for the worker to sync to the bockchain
140 | await blockBarrier(api, prpc);
141 |
142 | // basic checks
143 | console.log('Fat Contract: basic checks');
144 | console.assert(
145 | (await FatBadges.query.getTotalBadges(certAlice, {})).output.toNumber() == 2,
146 | 'Should have two badges created'
147 | );
148 |
149 | const easyInfo = await FatBadges.query.getBadgeInfo(certAlice, {}, easyBadgeId);
150 | console.log('Easy badge:', easyInfo.output.toHuman());
151 |
152 | const advInfo = await FatBadges.query.getBadgeInfo(certAlice, {}, advBadgeId);
153 | console.log('Adv badge:', advInfo.output.toHuman());
154 |
155 | // create an attestation
156 | const attest = await EasyOracle.query['submittableOracle::attest'](
157 | certAlice, {},
158 | 'https://gist.githubusercontent.com/h4x3rotab/4b6bb4aa8dc9956af9c976a906daaa2a/raw/80da37a6e9e91b9e3929ba284c826631644f7d1a/test'
159 | );
160 | console.log(
161 | 'Easy attestation:',
162 | attest.result.isOk ? attest.output.toHuman() : attest.result.toHuman()
163 | );
164 | console.log(EasyOracle.registry.createType('GistQuote', attest.output.asOk.data.toHex()).toHuman());
165 | const attestObj = attest.output.asOk;
166 |
167 | // submit attestation
168 | await txqueue.submit(
169 | EasyOracle.tx.redeem({}, attestObj),
170 | alice,
171 | true,
172 | );
173 | await blockBarrier(api, prpc);
174 |
175 | const aliceBadge = await FatBadges.query.get(certAlice, {}, easyBadgeId);
176 | console.log('Alice won:', aliceBadge.output.toHuman());
177 |
178 | // test the advanced challenge judger
179 | const advAttest = await AdvancedJudger.query.checkContract(
180 | certAlice, {},
181 | artifacts.EasyOracle.address,
182 | 'https://gist.githubusercontent.com/h4x3rotab/4b6bb4aa8dc9956af9c976a906daaa2a/raw/80da37a6e9e91b9e3929ba284c826631644f7d1a/test'
183 | );
184 | console.log(
185 | 'Advanced attestation:',
186 | advAttest.result.isOk ? advAttest.output.toHuman() : advAttest.result.toHuman()
187 | );
188 | const advAttestObj = advAttest.output.asOk;
189 |
190 | // submit attestation
191 | await txqueue.submit(
192 | AdvancedJudger.tx.redeem({}, advAttestObj),
193 | bob,
194 | true,
195 | );
196 | await blockBarrier(api, prpc);
197 |
198 | const aliceAdvBadge = await FatBadges.query.get(certBob, {}, advBadgeId);
199 | console.log('Bob won adv:', aliceAdvBadge.output.toHuman());
200 | }
201 |
202 | main().then(process.exit).catch(err => console.error('Crashed', err)).finally(() => process.exit(-1));
--------------------------------------------------------------------------------
/scripts/js/src/utils.js:
--------------------------------------------------------------------------------
1 | class TxQueue {
2 | constructor(api) {
3 | this.nonceTracker = {};
4 | this.api = api;
5 | }
6 | async nextNonce(address) {
7 | const byCache = this.nonceTracker[address] || 0;
8 | const byRpc = (await this.api.rpc.system.accountNextIndex(address)).toNumber();
9 | return Math.max(byCache, byRpc);
10 | }
11 | markNonceFailed(address, nonce) {
12 | if (!this.nonceTracker[address]) {
13 | return;
14 | }
15 | if (nonce < this.nonceTracker[address]) {
16 | this.nonceTracker[address] = nonce;
17 | }
18 | }
19 | async submit(txBuilder, signer, waitForFinalization=false) {
20 | const address = signer.address;
21 | const nonce = await this.nextNonce(address);
22 | this.nonceTracker[address] = nonce + 1;
23 | let hash;
24 | return new Promise(async (resolve, reject) => {
25 | const unsub = await txBuilder.signAndSend(signer, {nonce}, (result) => {
26 | if (result.status.isInBlock) {
27 | for (const e of result.events) {
28 | const { event: { data, method, section } } = e;
29 | if (section === 'system' && method === 'ExtrinsicFailed') {
30 | unsub();
31 | reject(data[0].toHuman())
32 | }
33 | }
34 | if (!waitForFinalization) {
35 | unsub();
36 | resolve({
37 | hash: result.status.asInBlock,
38 | events: result.events,
39 | });
40 | } else {
41 | hash = result.status.asInBlock;
42 | }
43 | } else if (result.status.isFinalized) {
44 | resolve({
45 | hash,
46 | events: result.events,
47 | })
48 | } else if (result.status.isInvalid) {
49 | unsub();
50 | this.markNonceFailed(address, nonce);
51 | reject('Invalid transaction');
52 | }
53 | });
54 | });
55 | }
56 | }
57 |
58 | async function sleep(t) {
59 | await new Promise(resolve => {
60 | setTimeout(resolve, t);
61 | });
62 | }
63 |
64 | async function checkUntil(async_fn, timeout) {
65 | const t0 = new Date().getTime();
66 | while (true) {
67 | if (await async_fn()) {
68 | return;
69 | }
70 | const t = new Date().getTime();
71 | if (t - t0 >= timeout) {
72 | throw new Error('timeout');
73 | }
74 | await sleep(100);
75 | }
76 | }
77 |
78 | async function checkUntilEq(async_fn, expected, timeout, verbose=true) {
79 | const t0 = new Date().getTime();
80 | let lastActual = undefined;
81 | while (true) {
82 | const actual = await async_fn();
83 | if (actual == expected) {
84 | return;
85 | }
86 | if (actual != lastActual && verbose) {
87 | console.log(`Waiting... (current = ${actual}, expected = ${expected})`)
88 | lastActual = actual;
89 | }
90 | const t = new Date().getTime();
91 | if (t - t0 >= timeout) {
92 | throw new Error('timeout');
93 | }
94 | await sleep(100);
95 | }
96 | }
97 |
98 | async function blockBarrier(api, prpc, finalized=false, timeout=4*6000) {
99 | const head = await (finalized
100 | ? api.rpc.chain.getFinalizedHead()
101 | : api.rpc.chain.getHeader()
102 | );
103 | let chainHeight = head.number.toNumber();
104 | await checkUntil(
105 | async() => (await prpc.getInfo({})).blocknum > chainHeight,
106 | timeout,
107 | );
108 | }
109 |
110 | function hex(b) {
111 | if (!b.startsWith('0x')) {
112 | return '0x' + b;
113 | } else {
114 | return b;
115 | }
116 | }
117 |
118 | module.exports = {
119 | TxQueue,
120 | sleep,
121 | checkUntil,
122 | checkUntilEq,
123 | blockBarrier,
124 | hex,
125 | }
--------------------------------------------------------------------------------
/utils/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "fat_utils"
3 | version = "0.1.0"
4 | edition = "2021"
5 |
6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
7 |
8 | [dependencies]
9 | ink_prelude = { version = "3", default-features = false }
10 | ink_primitives = { version = "3", default-features = false }
11 | ink_metadata = { version = "3", default-features = false, features = ["derive"], optional = true }
12 | ink_storage = { version = "3", default-features = false }
13 | ink_lang = { version = "3", default-features = false }
14 | ink_env = { version = "3", default-features = false }
15 |
16 | scale = { package = "parity-scale-codec", version = "3", default-features = false, features = ["derive"] }
17 | scale-info = { version = "2", default-features = false, features = ["derive"], optional = true }
18 |
19 | pink-extension = { version = "0.1.17", default-features = false }
20 | hex = { version = "0.4.3", default-features = false, features = ["alloc"] }
21 |
22 | [dev-dependencies]
23 | pink-extension-runtime = "0.1.3"
24 |
25 | [lib]
26 | name = "fat_utils"
27 | path = "src/lib.rs"
28 |
29 | [features]
30 | default = ["std"]
31 | std = [
32 | "ink_primitives/std",
33 | "ink_metadata/std",
34 | "ink_storage/std",
35 | "ink_env/std",
36 | "scale/std",
37 | "scale-info/std",
38 | "pink-extension/std",
39 | ]
40 |
--------------------------------------------------------------------------------
/utils/environmental/.gitignore:
--------------------------------------------------------------------------------
1 | target/
2 | .DS*
3 |
--------------------------------------------------------------------------------
/utils/environmental/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "environmental"
3 | description = "Set scope-limited values can can be accessed statically"
4 | version = "1.1.3"
5 | authors = ["Parity Technologies "]
6 | license = "Apache-2.0"
7 | edition = "2018"
8 |
9 | [features]
10 | default = ["std"]
11 | std = []
12 |
13 | [lib]
14 | name = "environmental"
15 | path = "src/lib.rs"
16 |
--------------------------------------------------------------------------------
/utils/environmental/LICENSE:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction,
10 | and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by
13 | the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all
16 | other entities that control, are controlled by, or are under common
17 | control with that entity. For the purposes of this definition,
18 | "control" means (i) the power, direct or indirect, to cause the
19 | direction or management of such entity, whether by contract or
20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 | outstanding shares, or (iii) beneficial ownership of such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity
24 | exercising permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation
28 | source, and configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical
31 | transformation or translation of a Source form, including but
32 | not limited to compiled object code, generated documentation,
33 | and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
176 | END OF TERMS AND CONDITIONS
177 |
178 | APPENDIX: How to apply the Apache License to your work.
179 |
180 | To apply the Apache License to your work, attach the following
181 | boilerplate notice, with the fields enclosed by brackets "[]"
182 | replaced with your own identifying information. (Don't include
183 | the brackets!) The text should be enclosed in the appropriate
184 | comment syntax for the file format. We also recommend that a
185 | file or class name and description of purpose be included on the
186 | same "printed page" as the copyright notice for easier
187 | identification within third-party archives.
188 |
189 | Copyright [yyyy] [name of copyright owner]
190 |
191 | Licensed under the Apache License, Version 2.0 (the "License");
192 | you may not use this file except in compliance with the License.
193 | You may obtain a copy of the License at
194 |
195 | http://www.apache.org/licenses/LICENSE-2.0
196 |
197 | Unless required by applicable law or agreed to in writing, software
198 | distributed under the License is distributed on an "AS IS" BASIS,
199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200 | See the License for the specific language governing permissions and
201 | limitations under the License.
202 |
--------------------------------------------------------------------------------
/utils/environmental/README.adoc:
--------------------------------------------------------------------------------
1 |
2 | = Environmental
3 |
4 | .Summary
5 | [source, toml]
6 | ----
7 | include::Cargo.toml[lines=2..5]
8 | ----
9 |
10 | .Description
11 | ----
12 | include::src/lib.rs[tag=description]
13 | ----
14 |
--------------------------------------------------------------------------------
/utils/environmental/src/lib.rs:
--------------------------------------------------------------------------------
1 | // Copyright 2017-2020 Parity Technologies
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | //! Safe global references to stack variables.
16 | //!
17 | //! Set up a global reference with environmental! macro giving it a name and type.
18 | //! Use the `using` function scoped under its name to name a reference and call a function that
19 | //! takes no parameters yet can access said reference through the similarly placed `with` function.
20 | //!
21 | //! # Examples
22 | //!
23 | //! ```
24 | //! #[macro_use] extern crate environmental;
25 | //! // create a place for the global reference to exist.
26 | //! environmental!(counter: u32);
27 | //! fn stuff() {
28 | //! // do some stuff, accessing the named reference as desired.
29 | //! counter::with(|i| *i += 1);
30 | //! }
31 | //! fn main() {
32 | //! // declare a stack variable of the same type as our global declaration.
33 | //! let mut counter_value = 41u32;
34 | //! // call stuff, setting up our `counter` environment as a reference to our counter_value var.
35 | //! counter::using(&mut counter_value, stuff);
36 | //! println!("The answer is {:?}", counter_value); // will print 42!
37 | //! stuff(); // safe! doesn't do anything.
38 | //! }
39 | //! ```
40 |
41 | #![cfg_attr(not(feature = "std"), no_std)]
42 |
43 | extern crate alloc;
44 |
45 | #[doc(hidden)]
46 | pub use core::{cell::RefCell, mem::{transmute, replace}, marker::PhantomData};
47 |
48 | #[doc(hidden)]
49 | pub use alloc::{rc::Rc, vec::Vec};
50 |
51 | #[cfg(not(feature = "std"))]
52 | #[macro_export]
53 | mod local_key;
54 |
55 | #[doc(hidden)]
56 | #[cfg(not(feature = "std"))]
57 | pub use local_key::LocalKey;
58 |
59 | #[doc(hidden)]
60 | #[cfg(feature = "std")]
61 | pub use std::thread::LocalKey;
62 |
63 | #[doc(hidden)]
64 | #[cfg(feature = "std")]
65 | #[macro_export]
66 | macro_rules! thread_local_impl {
67 | ($(#[$attr:meta])* static $name:ident: $t:ty = $init:expr) => (
68 | thread_local!($(#[$attr])* static $name: $t = $init);
69 | );
70 | }
71 |
72 | #[doc(hidden)]
73 | #[cfg(not(feature = "std"))]
74 | #[macro_export]
75 | macro_rules! thread_local_impl {
76 | ($(#[$attr:meta])* static $name:ident: $t:ty = $init:expr) => (
77 | $(#[$attr])*
78 | static $name: $crate::LocalKey<$t> = {
79 | fn __init() -> $t { $init }
80 |
81 | $crate::local_key_init!(__init)
82 | };
83 | );
84 | }
85 |
86 | /// The global inner that stores the stack of globals.
87 | #[doc(hidden)]
88 | pub type GlobalInner = RefCell>>>;
89 |
90 | /// The global type.
91 | type Global = LocalKey>;
92 |
93 | #[doc(hidden)]
94 | pub fn using R>(
95 | global: &'static Global,
96 | protected: &mut T,
97 | f: F,
98 | ) -> R {
99 | // store the `protected` reference as a pointer so we can provide it to logic running within
100 | // `f`.
101 | // while we record this pointer (while it's non-zero) we guarantee:
102 | // - it will only be used once at any time (no reentrancy);
103 | // - that no other thread will use it; and
104 | // - that we do not use the original mutating reference while the pointer.
105 | // exists.
106 | global.with(|r| {
107 | // Push the new global to the end of the stack.
108 | r.borrow_mut().push(
109 | Rc::new(RefCell::new(protected as _)),
110 | );
111 |
112 | // Even if `f` panics the added global will be popped.
113 | struct PopGlobal<'a, T: 'a + ?Sized> {
114 | global_stack: &'a GlobalInner,
115 | }
116 |
117 | impl<'a, T: 'a + ?Sized> Drop for PopGlobal<'a, T> {
118 | fn drop(&mut self) {
119 | self.global_stack.borrow_mut().pop();
120 | }
121 | }
122 |
123 | let _guard = PopGlobal { global_stack: r };
124 |
125 | f()
126 | })
127 | }
128 |
129 | #[doc(hidden)]
130 | pub fn with R>(
131 | global: &'static Global,
132 | mutator: F,
133 | ) -> Option {
134 | global.with(|r| {
135 | // We always use the `last` element when we want to access the
136 | // currently set global.
137 | let last = r.borrow().last().cloned();
138 | match last {
139 | Some(ptr) => unsafe {
140 | // safe because it's only non-zero when it's being called from using, which
141 | // is holding on to the underlying reference (and not using it itself) safely.
142 | Some(mutator(&mut **ptr.borrow_mut()))
143 | }
144 | None => None,
145 | }
146 | })
147 | }
148 |
149 | /// Declare a new global reference module whose underlying value does not contain references.
150 | ///
151 | /// Will create a module of a given name that contains two functions:
152 | ///
153 | /// * `pub fn using R>(protected: &mut $t, f: F) -> R`
154 | /// This executes `f`, returning its value. During the call, the module's reference is set to
155 | /// be equal to `protected`.
156 | /// * `pub fn with R>(f: F) -> Option`
157 | /// This executes `f`, returning `Some` of its value if called from code that is being executed
158 | /// as part of a `using` call. If not, it returns `None`. `f` is provided with one argument: the
159 | /// same reference as provided to the most recent `using` call.
160 | ///
161 | /// # Examples
162 | ///
163 | /// Initializing the global context with a given value.
164 | ///
165 | /// ```rust
166 | /// #[macro_use] extern crate environmental;
167 | /// environmental!(counter: u32);
168 | /// fn main() {
169 | /// let mut counter_value = 41u32;
170 | /// counter::using(&mut counter_value, || {
171 | /// let odd = counter::with(|value|
172 | /// if *value % 2 == 1 {
173 | /// *value += 1; true
174 | /// } else {
175 | /// *value -= 3; false
176 | /// }).unwrap(); // safe because we're inside a counter::using
177 | /// println!("counter was {}", match odd { true => "odd", _ => "even" });
178 | /// });
179 | ///
180 | /// println!("The answer is {:?}", counter_value); // 42
181 | /// }
182 | /// ```
183 | ///
184 | /// Roughly the same, but with a trait object:
185 | ///
186 | /// ```rust
187 | /// #[macro_use] extern crate environmental;
188 | ///
189 | /// trait Increment { fn increment(&mut self); }
190 | ///
191 | /// impl Increment for i32 {
192 | /// fn increment(&mut self) { *self += 1 }
193 | /// }
194 | ///
195 | /// environmental!(val: Increment + 'static);
196 | ///
197 | /// fn main() {
198 | /// let mut local = 0i32;
199 | /// val::using(&mut local, || {
200 | /// val::with(|v| for _ in 0..5 { v.increment() });
201 | /// });
202 | ///
203 | /// assert_eq!(local, 5);
204 | /// }
205 | /// ```
206 | #[macro_export]
207 | macro_rules! environmental {
208 | ($vis:vis $name:ident : $t:ty) => {
209 | #[allow(non_camel_case_types)]
210 | $vis struct $name { __private_field: () }
211 |
212 | $crate::thread_local_impl! {
213 | static GLOBAL: $crate::GlobalInner<$t> = Default::default()
214 | }
215 |
216 | impl $name {
217 | #[allow(unused_imports)]
218 |
219 | pub fn using R>(
220 | protected: &mut $t,
221 | f: F
222 | ) -> R {
223 | $crate::using(&GLOBAL, protected, f)
224 | }
225 |
226 | pub fn with R>(
227 | f: F
228 | ) -> Option {
229 | $crate::with(&GLOBAL, |x| f(x))
230 | }
231 | }
232 | };
233 | ($vis:vis $name:ident : trait @$t:ident [$($args:ty,)*]) => {
234 | #[allow(non_camel_case_types, dead_code)]
235 | $vis struct $name { __private_field: () }
236 |
237 | $crate::thread_local_impl! {
238 | static GLOBAL: $crate::GlobalInner<(dyn $t<$($args),*> + 'static)>
239 | = Default::default()
240 | }
241 |
242 | impl $name {
243 | #[allow(unused_imports)]
244 |
245 | pub fn using R>(
246 | protected: &mut dyn $t<$($args),*>,
247 | f: F
248 | ) -> R {
249 | let lifetime_extended = unsafe {
250 | $crate::transmute::<&mut dyn $t<$($args),*>, &mut (dyn $t<$($args),*> + 'static)>(protected)
251 | };
252 | $crate::using(&GLOBAL, lifetime_extended, f)
253 | }
254 |
255 | pub fn with FnOnce(&'a mut (dyn $t<$($args),*> + 'a)) -> R>(
256 | f: F
257 | ) -> Option {
258 | $crate::with(&GLOBAL, |x| f(x))
259 | }
260 | }
261 | };
262 | ($vis:vis $name:ident<$traittype:ident> : trait $t:ident <$concretetype:ty>) => {
263 | #[allow(non_camel_case_types, dead_code)]
264 | $vis struct $name { _private_field: $crate::PhantomData }
265 |
266 | $crate::thread_local_impl! {
267 | static GLOBAL: $crate::GlobalInner<(dyn $t<$concretetype> + 'static)>
268 | = Default::default()
269 | }
270 |
271 | impl $name {
272 | #[allow(unused_imports)]
273 | pub fn using R>(
274 | protected: &mut dyn $t,
275 | f: F
276 | ) -> R {
277 | let lifetime_extended = unsafe {
278 | $crate::transmute::<&mut dyn $t, &mut (dyn $t<$concretetype> + 'static)>(protected)
279 | };
280 | $crate::using(&GLOBAL, lifetime_extended, f)
281 | }
282 |
283 | pub fn with FnOnce(&'a mut (dyn $t<$concretetype> + 'a)) -> R>(
284 | f: F
285 | ) -> Option {
286 | $crate::with(&GLOBAL, |x| f(x))
287 | }
288 | }
289 | };
290 | ($vis:vis $name:ident : trait $t:ident <>) => { $crate::environmental! {$vis $name : trait @$t [] } };
291 | ($vis:vis $name:ident : trait $t:ident < $($args:ty),* $(,)* >) => {
292 | $crate::environmental! { $vis $name : trait @$t [$($args,)*] }
293 | };
294 | ($vis:vis $name:ident : trait $t:ident) => { $crate::environmental! { $vis $name : trait @$t [] } };
295 | }
296 |
297 | #[cfg(test)]
298 | mod tests {
299 | // Test trait in item position
300 | #[allow(dead_code)]
301 | mod trait_test {
302 | trait Test {}
303 |
304 | environmental!(item_positon_trait: trait Test);
305 | }
306 |
307 | // Test type in item position
308 | #[allow(dead_code)]
309 | mod type_test {
310 | environmental!(item_position_type: u32);
311 | }
312 |
313 | #[test]
314 | fn simple_works() {
315 | environmental!(counter: u32);
316 |
317 | fn stuff() { counter::with(|value| *value += 1); }
318 |
319 | // declare a stack variable of the same type as our global declaration.
320 | let mut local = 41u32;
321 |
322 | // call stuff, setting up our `counter` environment as a reference to our local counter var.
323 | counter::using(&mut local, stuff);
324 | assert_eq!(local, 42);
325 | stuff(); // safe! doesn't do anything.
326 | assert_eq!(local, 42);
327 | }
328 |
329 | #[test]
330 | fn overwrite_with_lesser_lifetime() {
331 | environmental!(items: Vec);
332 |
333 | let mut local_items = vec![1, 2, 3];
334 | items::using(&mut local_items, || {
335 | let dies_at_end = vec![4, 5, 6];
336 | items::with(|items| *items = dies_at_end);
337 | });
338 |
339 | assert_eq!(local_items, vec![4, 5, 6]);
340 | }
341 |
342 | #[test]
343 | fn declare_with_trait_object() {
344 | trait Foo {
345 | fn get(&self) -> i32;
346 | fn set(&mut self, x: i32);
347 | }
348 |
349 | impl Foo for i32 {
350 | fn get(&self) -> i32 { *self }
351 | fn set(&mut self, x: i32) { *self = x }
352 | }
353 |
354 | environmental!(foo: dyn Foo + 'static);
355 |
356 | fn stuff() {
357 | foo::with(|value| {
358 | let new_val = value.get() + 1;
359 | value.set(new_val);
360 | });
361 | }
362 |
363 | let mut local = 41i32;
364 | foo::using(&mut local, stuff);
365 |
366 | assert_eq!(local, 42);
367 |
368 | stuff(); // doesn't do anything.
369 |
370 | assert_eq!(local, 42);
371 | }
372 |
373 | #[test]
374 | fn unwind_recursive() {
375 | use std::panic;
376 |
377 | environmental!(items: Vec);
378 |
379 | let panicked = panic::catch_unwind(|| {
380 | let mut local_outer = vec![1, 2, 3];
381 |
382 | items::using(&mut local_outer, || {
383 | let mut local_inner = vec![4, 5, 6];
384 | items::using(&mut local_inner, || {
385 | panic!("are you unsafe?");
386 | })
387 | });
388 | }).is_err();
389 |
390 | assert!(panicked);
391 |
392 | let mut was_cleared = true;
393 | items::with(|_items| was_cleared = false);
394 |
395 | assert!(was_cleared);
396 | }
397 |
398 | #[test]
399 | fn use_non_static_trait() {
400 | trait Sum { fn sum(&self) -> usize; }
401 | impl Sum for &[usize] {
402 | fn sum(&self) -> usize {
403 | self.iter().fold(0, |a, c| a + c)
404 | }
405 | }
406 |
407 | environmental!(sum: trait Sum);
408 | let numbers = vec![1, 2, 3, 4, 5];
409 | let mut numbers = &numbers[..];
410 | let got_sum = sum::using(&mut numbers, || {
411 | sum::with(|x| x.sum())
412 | }).unwrap();
413 |
414 | assert_eq!(got_sum, 15);
415 | }
416 |
417 | #[test]
418 | fn stacking_globals() {
419 | trait Sum { fn sum(&self) -> usize; }
420 | impl Sum for &[usize] {
421 | fn sum(&self) -> usize {
422 | self.iter().fold(0, |a, c| a + c)
423 | }
424 | }
425 |
426 | environmental!(sum: trait Sum);
427 | let numbers = vec![1, 2, 3, 4, 5];
428 | let mut numbers = &numbers[..];
429 | let got_sum = sum::using(&mut numbers, || {
430 | sum::with(|_| {
431 | let numbers2 = vec![1, 2, 3, 4, 5, 6];
432 | let mut numbers2 = &numbers2[..];
433 | sum::using(&mut numbers2, || {
434 | sum::with(|x| x.sum())
435 | })
436 | })
437 | }).unwrap().unwrap();
438 |
439 | assert_eq!(got_sum, 21);
440 |
441 | assert!(sum::with(|_| ()).is_none());
442 | }
443 |
444 | #[test]
445 | fn use_generic_trait() {
446 | trait Plus { fn plus42() -> usize; }
447 | struct ConcretePlus;
448 | impl Plus for ConcretePlus {
449 | fn plus42() -> usize { 42 }
450 | }
451 | trait Multiplier { fn mul_and_add(&self) -> usize; }
452 | impl<'a, P: Plus> Multiplier
for &'a [usize] {
453 | fn mul_and_add(&self) -> usize {
454 | self.iter().fold(1, |a, c| a * c) + P::plus42()
455 | }
456 | }
457 |
458 | let numbers = vec![1, 2, 3];
459 | let mut numbers = &numbers[..];
460 | let out = foo::::using(&mut numbers, || {
461 | foo::::with(|x| x.mul_and_add() )
462 | }).unwrap();
463 |
464 | assert_eq!(out, 6 + 42);
465 | environmental!(foo: trait Multiplier);
466 | }
467 | }
468 |
--------------------------------------------------------------------------------
/utils/environmental/src/local_key.rs:
--------------------------------------------------------------------------------
1 | // Copyright 2017-2020 Parity Technologies
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | use super::*;
16 |
17 | // This code is a simplified version of [`LocalKey`] and it's wasm32 specialization: [`statik::Key`].
18 | // [`LocalKey`]: https://github.com/alexcrichton/rust/blob/98931165a23a1c2860d99759385f45d6807c8982/src/libstd/thread/local.rs#L89
19 | // [`statik::Key`]: https://github.com/alexcrichton/rust/blob/98931165a23a1c2860d99759385f45d6807c8982/src/libstd/thread/local.rs#L310-L312
20 |
21 | pub struct LocalKey {
22 | pub init: fn() -> T,
23 | pub inner: RefCell