├── .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 | | ![](https://i.imgur.com/mVNh6Nh.png) | ![](https://i.imgur.com/kZEquyA.png) | 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 | ![](https://i.imgur.com/ytxwnnJ.png) 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 | ![](https://i.imgur.com/1vMK0Lz.png) 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 | ![](https://i.imgur.com/Ak6kquP.png) 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 | ![](https://i.imgur.com/sFuPV2U.png) 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 | ![](https://i.imgur.com/mFG9WpX.png) 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 | ![](https://i.imgur.com/giRp7Wj.png) 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 | ![](https://i.imgur.com/hVPNLDm.png) 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 | ![](https://i.imgur.com/QWAS09a.png) 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 | ![](https://i.imgur.com/HIMYIlY.png) 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 | ![](https://i.imgur.com/Vae03il.png) 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 | ![](https://i.imgur.com/4nOeFmb.png) 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 | ![](https://i.imgur.com/4qHcvvd.png) 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>, 24 | } 25 | 26 | // This is safe as long there is no threads in wasm32. 27 | unsafe impl ::core::marker::Sync for LocalKey {} 28 | 29 | impl LocalKey { 30 | pub fn with(&'static self, f: F) -> R 31 | where F: FnOnce(&T) -> R 32 | { 33 | if self.inner.borrow().is_none() { 34 | let v = (self.init)(); 35 | *self.inner.borrow_mut() = Some(v); 36 | } 37 | // This code can't panic because: 38 | // 1. `inner` can be borrowed mutably only once at the initialization time. 39 | // 2. After the initialization `inner` is always `Some`. 40 | f(&*self.inner.borrow().as_ref().unwrap()) 41 | } 42 | } 43 | 44 | /// Initialize [`LocalKey`]. 45 | #[macro_export] 46 | macro_rules! local_key_init { 47 | ( 48 | $init:ident 49 | ) => { 50 | $crate::LocalKey { 51 | init: $init, 52 | inner: $crate::RefCell::new(None), 53 | } 54 | } 55 | } 56 | --------------------------------------------------------------------------------