diff --git a/.gitmodules b/.gitmodules
index b1c953dd35..510d75b006 100644
--- a/.gitmodules
+++ b/.gitmodules
@@ -11,3 +11,6 @@
[submodule "external/cli"]
path = external/cli
url = https://github.com/oasisprotocol/cli
+[submodule "external/sapphire-paratime"]
+ path = external/sapphire-paratime
+ url = https://github.com/oasisprotocol/sapphire-paratime
diff --git a/docs/dapp/images/sapphire/gasless-gsn-flow.jpg b/docs/dapp/images/sapphire/gasless-gsn-flow.jpg
deleted file mode 100644
index 4a3683560e..0000000000
Binary files a/docs/dapp/images/sapphire/gasless-gsn-flow.jpg and /dev/null differ
diff --git a/docs/dapp/images/sapphire/gasless-on-chain-signer.svg b/docs/dapp/images/sapphire/gasless-on-chain-signer.svg
deleted file mode 100644
index 6ca4a197c6..0000000000
--- a/docs/dapp/images/sapphire/gasless-on-chain-signer.svg
+++ /dev/null
@@ -1,4 +0,0 @@
-
-
-
-
\ No newline at end of file
diff --git a/docs/dapp/images/sapphire/hardhat-boilerplate-frontend1.png b/docs/dapp/images/sapphire/hardhat-boilerplate-frontend1.png
deleted file mode 100644
index 8d08559908..0000000000
Binary files a/docs/dapp/images/sapphire/hardhat-boilerplate-frontend1.png and /dev/null differ
diff --git a/docs/dapp/images/sapphire/hardhat-boilerplate-frontend2.png b/docs/dapp/images/sapphire/hardhat-boilerplate-frontend2.png
deleted file mode 100644
index 49a293bba2..0000000000
Binary files a/docs/dapp/images/sapphire/hardhat-boilerplate-frontend2.png and /dev/null differ
diff --git a/docs/dapp/images/sapphire/hardhat-boilerplate-frontend3.png b/docs/dapp/images/sapphire/hardhat-boilerplate-frontend3.png
deleted file mode 100644
index 16c4f944fb..0000000000
Binary files a/docs/dapp/images/sapphire/hardhat-boilerplate-frontend3.png and /dev/null differ
diff --git a/docs/dapp/images/sapphire/sourcify1.png b/docs/dapp/images/sapphire/sourcify1.png
deleted file mode 100644
index b6dba793af..0000000000
Binary files a/docs/dapp/images/sapphire/sourcify1.png and /dev/null differ
diff --git a/docs/dapp/images/sapphire/sourcify2.png b/docs/dapp/images/sapphire/sourcify2.png
deleted file mode 100644
index 42b201dfe2..0000000000
Binary files a/docs/dapp/images/sapphire/sourcify2.png and /dev/null differ
diff --git a/docs/dapp/images/sapphire/sourcify3.png b/docs/dapp/images/sapphire/sourcify3.png
deleted file mode 100644
index ac32713f68..0000000000
Binary files a/docs/dapp/images/sapphire/sourcify3.png and /dev/null differ
diff --git a/docs/dapp/sapphire/addresses.md b/docs/dapp/sapphire/addresses.md
deleted file mode 100644
index 6fd24eb2bf..0000000000
--- a/docs/dapp/sapphire/addresses.md
+++ /dev/null
@@ -1,18 +0,0 @@
----
-description: List of Standard Contract Addresses
----
-
-# Standard Contract Addresses
-
-| Name | Mainnet Address | Testnet Address | Verify | Source |
-|--------------|--------------------------------------------|--------------------------------------------|------------------------------------------------------------------|---------------------------------|
-| [Multicall V3][multicall] | `0xcA11bde05977b3631167028862bE2a173976CA11` | - | [Mainnet][multicall-verify-mainnet] | [Multicall3.sol][multicall-source] |
-| Wrapped ROSE | `0x8Bc2B030b299964eEfb5e1e0b36991352E56D2D3` | `0xB759a0fbc1dA517aF257D5Cf039aB4D86dFB3b94` | [Mainnet][wrose-verify-mainnet], [Testnet][wrose-verify-testnet] | [WrappedROSE.sol][wrose-source] |
-
-[multicall-source]: https://github.com/mds1/multicall/blob/main/src/Multicall3.sol
-[multicall-verify-mainnet]: https://sourcify.dev/#/lookup/0xcA11bde05977b3631167028862bE2a173976CA11
-[multicall]: https://multicall3.com/
-
-[wrose-source]: https://github.com/oasisprotocol/sapphire-paratime/blob/main/contracts/contracts/WrappedROSE.sol
-[wrose-verify-mainnet]: https://sourcify.dev/#/lookup/0x8Bc2B030b299964eEfb5e1e0b36991352E56D2D3
-[wrose-verify-testnet]: https://sourcify.dev/#/lookup/0xB759a0fbc1dA517aF257D5Cf039aB4D86dFB3b94
diff --git a/docs/dapp/sapphire/addresses.md b/docs/dapp/sapphire/addresses.md
new file mode 120000
index 0000000000..8ef4fd10cf
--- /dev/null
+++ b/docs/dapp/sapphire/addresses.md
@@ -0,0 +1 @@
+../../../external/sapphire-paratime/docs/addresses.md
\ No newline at end of file
diff --git a/docs/dapp/sapphire/authentication.md b/docs/dapp/sapphire/authentication.md
deleted file mode 100644
index 341f1b2b50..0000000000
--- a/docs/dapp/sapphire/authentication.md
+++ /dev/null
@@ -1,209 +0,0 @@
----
-description: Authenticate users with your confidential contracts
----
-
-# View-Call Authentication
-
-User impersonation on Ethereum and other "Transparent EVMs" isn't a problem
-because **everybody** can see **all** data however the Sapphire confidential
-EVM prevents contracts from revealing confidential information to the wrong
-party (account or contract) - for this reason we cannot allow arbitrary
-impersonation of any `msg.sender`.
-
-In Sapphire, there are four types of contract calls:
-
- 1. Contract to contract calls (also known as *internal calls*)
- 2. Unauthenticted view calls (queries using `eth_call`)
- 3. Authenticated view calls (signed queries)
- 4. Transactions (authenticated by signature)
-
-Intra-contract calls always set `msg.sender` appropriately, if a contract calls
-another contract in a way which could reveal sensitive information, the calling
-contract must implement access control or authentication.
-
-By default all `eth_call` queries used to invoke contract functions have the
-`msg.sender` parameter set to `address(0x0)`. In contrast, authenticated calls are
-signed by a keypair and will have the `msg.sender` parameter correctly initialized
-(more on that later). Also, when a transaction is
-submitted it is signed by a keypair (thus costs gas and can make state updates)
-and the `msg.sender` will be set to the signing account.
-
-## Sapphire Wrapper
-
-The [@oasisprotocol/sapphire-paratime][sp-npm] Ethereum provider wrapper
-`sapphire.wrap` function will **automatically end-to-end encrypt calldata** when
-interacting with contracts on Sapphire, this is an easy way to ensure the
-calldata of your dApp transactions remain confidential - although the `from`,
-`to`, and `gasprice` parameters are not encrypted.
-
-[sp-npm]: https://www.npmjs.com/package/@oasisprotocol/sapphire-paratime
-
-:::tip Unauthenticated calls and Encryption
-
-Although the calls may be unauthenticated, they can still be encrypted!
-
-:::
-
-However, if the Sapphire wrapper has been attached to a signer then subsequent
-view calls via `eth_call` will request that the user sign them (e.g. a
-MetaMask popup), these are called **signed queries** meaning `msg.sender` will be
-set to the signing account and can be used for authentication or to implement
-access control. This may add friction to the end-user experience and can result
-in frequent pop-ups requesting they sign queries which wouldn't normally require
-any interaction on Transparent EVMs.
-
-Let's see how Sapphire interprets different contract calls. Suppose the
-following solidity code:
-
-```solidity
-contract Example {
- address owner;
- constructor () {
- owner = msg.sender;
- }
- function isOwner () public view returns (bool) {
- return msg.sender == owner;
- }
-}
-```
-
-In the sample above, assuming we're calling from the same contract or account
-which created the contract, calling `isOwner` will return:
-
- * `false`, for `eth_call`
- * `false`, with `sapphire.wrap` but without an attached signer
- * `true`, with `sapphire.wrap` and an attached signer
- * `true`, if called via the contract which created it
-* `true`, if called via transaction
-
-## Caching Signed Queries
-
-When using signed queries the blockchain will be queried each time, however
-the Sapphire wrapper will cache signatures for signed queries with the same
-parameters to avoid asking the user to sign the same thing multiple times.
-
-Behind the scenes the signed queries use a "leash" to specify validity conditions
-so the query can only be performed within a block and account `nonce` range.
-These parameters are visible in the EIP-712 popup signed by the user. Queries
-with the same parameters will use the same leash.
-
-## Daily Sign-In with EIP-712
-
-One strategy which can be used to reduce the number of transaction signing
-prompts when a user interacts with contracts via a dApp is to use
-[EIP-712][eip-712] to "sign-in" once per day (or per-session), in combination
-with using two wrapped providers:
-
-[eip-712]: https://eips.ethereum.org/EIPS/eip-712
-
- 1. Provider to perform encrypted but unauthenticated view calls
- 2. Another provider to perform encrypted and authenticated transactions (or view calls)
- - The user will be prompted to sign each action.
-
-The two-provider pattern, in conjunction with a daily EIP-712 sign-in prompt
-ensures all transactions are end-to-end encrypted and the contract can
-authenticate users in view calls without frequent annoying popups.
-
-The code sample below uses an `authenticated` modifier to verify the sign-in:
-
-```solidity
-// SPDX-License-Identifier: UNLICENSED
-pragma solidity ^0.8.0;
-
-struct SignatureRSV {
- bytes32 r;
- bytes32 s;
- uint256 v;
-}
-
-contract SignInExample {
- bytes32 public constant EIP712_DOMAIN_TYPEHASH = keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)");
- string public constant SIGNIN_TYPE = "SignIn(address user,uint32 time)";
- bytes32 public constant SIGNIN_TYPEHASH = keccak256(bytes(SIGNIN_TYPE));
- bytes32 public immutable DOMAIN_SEPARATOR;
-
- constructor () {
- DOMAIN_SEPARATOR = keccak256(abi.encode(
- EIP712_DOMAIN_TYPEHASH,
- keccak256("SignInExample.SignIn"),
- keccak256("1"),
- block.chainid,
- address(this)
- ));
- }
-
- struct SignIn {
- address user;
- uint32 time;
- SignatureRSV rsv;
- }
-
- modifier authenticated(SignIn calldata auth)
- {
- // Must be signed within 24 hours ago.
- require( auth.time > (block.timestamp - (60*60*24)) );
-
- // Validate EIP-712 sign-in authentication.
- bytes32 authdataDigest = keccak256(abi.encodePacked(
- "\x19\x01",
- DOMAIN_SEPARATOR,
- keccak256(abi.encode(
- SIGNIN_TYPEHASH,
- auth.user,
- auth.time
- ))
- ));
-
- address recovered_address = ecrecover(
- authdataDigest, uint8(auth.rsv.v), auth.rsv.r, auth.rsv.s);
-
- require( auth.user == recovered_address, "Invalid Sign-In" );
-
- _;
- }
-
- function authenticatedViewCall(
- SignIn calldata auth,
- ... args
- )
- external view
- authenticated(auth)
- returns (bytes memory output)
- {
- // Use `auth.user` instead of `msg.sender`!
- }
-}
-```
-
-With the above contract code deployed, let's look at the frontend dApp and how
-it can request the user to sign-in using EIP-712. You may wish to add additional
-parameters which are authenticated such as the domain name. The following code
-example uses Ethers:
-
-```typescript
-const time = new Date().getTime();
-const user = await eth.signer.getAddress();
-
-// Ask user to "Sign-In" every 24 hours.
-const signature = await eth.signer._signTypedData({
- name: "SignInExample.SignIn",
- version: "1",
- chainId: import.meta.env.CHAINID,
- verifyingContract: contract.address
-}, {
- SignIn: [
- { name: 'user', type: "address" },
- { name: 'time', type: 'uint32' },
- ]
-}, {
- user,
- time: time
-});
-const rsv = ethers.utils.splitSignature(signature);
-const auth = {user, time, rsv};
-// The `auth` variable can then be cached.
-
-// Then in the future, authenticated view calls can be performed by
-// passing auth without further user interaction authenticated data.
-await contract.authenticatedViewCall(auth, ...args);
-```
diff --git a/docs/dapp/sapphire/authentication.md b/docs/dapp/sapphire/authentication.md
new file mode 120000
index 0000000000..912a6e5c27
--- /dev/null
+++ b/docs/dapp/sapphire/authentication.md
@@ -0,0 +1 @@
+../../../external/sapphire-paratime/docs/authentication.md
\ No newline at end of file
diff --git a/docs/dapp/sapphire/browser.md b/docs/dapp/sapphire/browser.md
deleted file mode 100644
index 20ab2f9a1a..0000000000
--- a/docs/dapp/sapphire/browser.md
+++ /dev/null
@@ -1,208 +0,0 @@
----
-description: Writing Sapphire dApp for browser and Metamask
----
-
-# Browser Support
-
-Confidential Sapphire dApps work in web browsers by wrapping the
-Ethereum provider such as Metamask to enable signing and encrypting
-calls and transactions.
-
-Let's begin with the [Hardhat boilerplate]. As mentioned on their website
-the boilerplate provides the following:
-
-- The Solidity contract implementing an ERC-20 token
-- Tests for the entire functionality of the contract
-- A minimal React front-end to interact with the contract using ethers.js
-
-Go ahead and clone the original [Hardhat boilerplate repo]. Move to the checked
-out folder and **apply the Sapphire-specific changes** to `hardhat.config.js`
-as [described in the quickstart][quickstart].
-
-Next, install dependencies. The boilerplate project uses
-[pnpm], but `yarn` and `npm` will also work with some modifications
-around workspaces:
-
-```shell npm2yarn
-npm install
-npm install -D @oasisprotocol/sapphire-paratime
-```
-
-Now, you can deploy the contract on the Testnet with the private key of the
-account holding some [TEST tokens]:
-
-```shell
-PRIVATE_KEY="0x..." npx hardhat run scripts/deploy.js --network sapphire-testnet
-```
-
-This will compile the contract and deploy it on the Testnet. In addition
-to the quickstart steps, **the contract address and ABI will also automatically
-be copied over to the `frontend/src/contracts` folder so that the frontend can
-access them!**
-
-:::warning
-
-The contract in the Hardhat boilerplate is ERC-20-compatible and emits the
-`transfer` event. If your wish to preserve confidentiality, you can comment
-out [line 66]. Read [the guide](guide.mdx#contract-logs) to learn more.
-
-:::
-
-## Signing Sapphire Calls and Transactions in Browser
-
-Now, let's explore the frontend of our dApp. Begin by moving into the
-`frontend` folder and install dependencies:
-
-```shell npm2yarn
-npm install
-npm install -D @oasisprotocol/sapphire-paratime
-```
-
-The main frontend logic is stored in `frontend/src/components/Dapp.js`. Apply
-the following changes:
-
-```diff title="frontend/src/components/Dapp.js"
---- a/hardhat-boilerplate/frontend/src/components/Dapp.js
-+++ b/hardhat-boilerplate/frontend/src/components/Dapp.js
-@@ -2,6 +2,7 @@
-
- // We'll use ethers to interact with the Ethereum network and our contract
- import { ethers } from "ethers";
-+import * as sapphire from '@oasisprotocol/sapphire-paratime';
-
- // We import the contract's artifacts and address here, as we are going to be
- // using them with ethers
-@@ -22,7 +23,7 @@
- // This is the Hardhat Network id that we set in our hardhat.config.js.
- // Here's a list of network ids https://docs.metamask.io/guide/ethereum-provider.html#properties
- // to use when deploying to other networks.
--const HARDHAT_NETWORK_ID = '1337';
-+const HARDHAT_NETWORK_ID = '23295'; // Sapphire Testnet
-
- // This is an error code that indicates that the user canceled a transaction
- const ERROR_CODE_TX_REJECTED_BY_USER = 4001;
-@@ -225,14 +226,20 @@
-
- async _initializeEthers() {
- // We first initialize ethers by creating a provider using window.ethereum
-- this._provider = new ethers.providers.Web3Provider(window.ethereum);
-+ this._provider = sapphire.wrap(new ethers.providers.Web3Provider(window.ethereum));
-
-- // Then, we initialize the contract using that provider and the token's
-- // artifact. You can do this same thing with your contracts.
-+ // Then, we initialize two contract instances:
-+ // - _token: Used for eth_calls (e.g. balanceOf, name, symbol)
-+ // - _tokenWrite: Used for on-chain transactions (e.g. transfer)
- this._token = new ethers.Contract(
- contractAddress.Token,
- TokenArtifact.abi,
-- this._provider.getSigner(0)
-+ this._provider,
-+ );
-+ this._tokenWrite = new ethers.Contract(
-+ contractAddress.Token,
-+ TokenArtifact.abi,
-+ sapphire.wrap(new ethers.providers.Web3Provider(window.ethereum).getSigner())
- );
- }
-
-@@ -294,7 +301,7 @@
-
- // We send the transaction, and save its hash in the Dapp's state. This
- // way we can indicate that we are waiting for it to be mined.
-- const tx = await this._token.transfer(to, amount);
-+ const tx = await this._tokenWrite.transfer(to, amount);
- this.setState({ txBeingSent: tx.hash });
-
- // We use .wait() to wait for the transaction to be mined. This method
-@@ -360,8 +367,8 @@
- return true;
- }
-
-- this.setState({
-- networkError: 'Please connect Metamask to Localhost:8545'
-+ this.setState({
-+ networkError: 'Please connect to Sapphire ParaTime Testnet'
- });
-
- return false;
-```
-
-Beside the obvious change to the chain ID and wrapping ethers.js objects with the
-Sapphire wrapper you can notice that we initialized **two** contract
-instances:
-
-- `this._token` object will be used for unsigned eth calls. This is the
- Hardhat method of [setting `from` transaction field to all zeros]
- [guide-transaction-calls] in order to avoid Metamask signature popups.
- Although the call is unsigned, **it is still encrypted** with the
- corresponding runtime key to preserve confidentiality.
-- `this._tokenWrite` object will be used for signed calls and transactions. The
- user will be prompted by Metamask for the signature. Both β calls and
- transactions β will be encrypted.
-
-## Trying it
-
-Start the frontend by typing:
-
-```shell npm2yarn
-npm run start
-```
-
-If all goes well the web server will spin up and your browser should
-automatically open `http://localhost:3000`.
-
-![Hardhat boilerplate frontend](../images/sapphire/hardhat-boilerplate-frontend1.png)
-
-Go ahead and connect the wallet. If you haven't done it yet, you will have
-to add the [Sapphire ParaTime Testnet network to your Metamask]
-[sapphire-testnet]. Once connected, the frontend will make an unsigned call to
-the `balanceOf` view and show you the amount of `MHT`s in your selected
-Metamask account.
-
-![MHT balance of your account](../images/sapphire/hardhat-boilerplate-frontend2.png)
-
-Next, let's transfer some `MHT`s. Fill in the amount, the address and hit the
-*Transfer* button. Metamask will show you the popup to sign and submit the
-transfer transaction. Once confirmed, Metamask will both **sign and encrypt** the transaction.
-
-![Sign and encrypt the transfer transaction](../images/sapphire/hardhat-boilerplate-frontend3.png)
-
-Once the transaction is processed, you will get a notification from Metamask
-and the balance in the dApp will be updated.
-
-:::tip
-
-If you commented out `emit Transfer(...)`, the transfer of `MHT`s would have
-been completely confidential. In the example above, the [following transaction]
-[block explorer] was generated. Go ahead and check your transaction on the
-block explorer too, to make sure no sensitive data was leaked!
-
-:::
-
-Congratulations, you successfully implemented your first truly confidential
-dApp which runs in the browser and wraps Metamask to both sign and encrypt the
-transactions!
-
-Should you have any questions or ideas to share, feel free to reach out to us
-on [discord and other social media channels][social-media].
-
-:::info Example
-
-You can download a full working example from the [Sapphire ParaTime examples]
-repository.
-
-:::
-
-[block explorer]: https://testnet.explorer.sapphire.oasis.dev/tx/0x3303dea5d48291d1564cad573f21fc71fcbdc2b862e17e056287fd9207e3bc53
-[guide-transaction-calls]: guide.mdx#transactions--calls
-[Hardhat boilerplate repo]: https://github.com/NomicFoundation/hardhat-boilerplate
-[Hardhat boilerplate]: https://hardhat.org/tutorial/boilerplate-project
-[Hardhat tutorial]: https://hardhat.org/tutorial
-[line 66]: https://github.com/NomicFoundation/hardhat-boilerplate/blob/13bd712c1285b2de572f14d20e6a750ae08565c0/contracts/Token.sol#L66
-[quickstart]: quickstart.mdx#add-the-sapphire-testnet-to-hardhat
-[sapphire-testnet]: ./README.mdx#testnet
-[Sapphire ParaTime examples]: https://github.com/oasisprotocol/sapphire-paratime/tree/main/examples/hardhat-boilerplate
-[social-media]: ../../get-involved/README.md#social-media-channels
-[pnpm]: https://pnpm.io
-[TEST tokens]: quickstart.mdx#get-some-sapphire-testnet-tokens
diff --git a/docs/dapp/sapphire/browser.md b/docs/dapp/sapphire/browser.md
new file mode 120000
index 0000000000..1b1dfca35c
--- /dev/null
+++ b/docs/dapp/sapphire/browser.md
@@ -0,0 +1 @@
+../../../external/sapphire-paratime/docs/browser.md
\ No newline at end of file
diff --git a/docs/dapp/sapphire/gasless.md b/docs/dapp/sapphire/gasless.md
deleted file mode 100644
index 775690d504..0000000000
--- a/docs/dapp/sapphire/gasless.md
+++ /dev/null
@@ -1,388 +0,0 @@
----
-description: Submitting transactions without paying for fees
----
-
-# Gasless Transactions
-
-When you submit a transaction to a blockchain, you need to pay certain fee
-(called *gas* in Ethereum jargon). Since only the transactions with the highest
-fee will be included in the block, this mechanism effectively prevents denial
-of service attacks on the network. On the other hand, paying for gas requires
-from the user that they have certain amount of blockchain-native tokens
-available in their wallet which may not be feasible.
-
-In this chapter we will learn how the user signs and sends their transaction to
-a *relayer*. The relayer then wraps the original signed transaction into a new
-*meta-transaction* (see [ERC-2771] for details), signs it and pays for the
-necessary transaction fees. When the transaction is submitted the on-chain
-recipient contract decodes the meta-transaction, verifies both signatures and
-executes the original transaction.
-
-Oasis Sapphire supports two transaction relaying methods: The **on-chain
-signer** exposes the Oasis-specific contract state encryption functionality
-while the **gas station network** method is a standardized approach known in
-other blockchains as well.
-
-:::caution
-
-The gas station network implementation on Sapphire is still in early beta. Some
-features such as the browser support are not fully implemented yet.
-
-:::
-
-[ERC-2771]: https://eips.ethereum.org/EIPS/eip-2771
-
-## On-Chain Signer
-
-The on-chain signer is a smart contract which receives the user's transaction,
-checks whether the transaction is valid, wraps it into a meta-transaction
-(which includes paying for the transaction fee) and returns it back to the user
-in [EIP-155] format. These steps are executed as a confidential call. Finally,
-the user submits the generated transaction to the network.
-
-![Diagram of the On-Chain Signing](../images/sapphire/gasless-on-chain-signer.svg)
-
-### EIP155Signer
-
-To sign a transaction, the Sapphire's `EIP155Signer` library bundled along the
-`@oasisprotocol/sapphire-contract` package comes with the following helper which
-returns a raw, RLP-encoded, signed transaction ready to be broadcast:
-
-```solidity
-function sign(address publicAddress, bytes32 secretKey, EthTx memory transaction) internal view returns (bytes memory);
-```
-
-`publicAddress` and `secretKey` are the signer's address and their private key
-used to sign a meta-transaction (and pay for the fees). We will store these
-sensitive data inside the encrypted smart contract state together with the
-signer's `nonce` field in the following struct:
-
-```solidity
-struct EthereumKeypair {
- address addr;
- bytes32 secret;
- uint64 nonce;
-}
-```
-
-The last `transaction` parameter in the `sign()` function is the transaction
-encoded in a format based on [EIP-155]. This can either be the original user's
-transaction or a meta-transaction.
-
-### Gasless Proxy Contract
-
-The following snippet is a complete *Gasless* contract for wrapping the user's
-transactions (`makeProxyTx()`) and executing them (`proxy()`). The signer's
-private key containing enough balance to cover transaction fees should be
-provided in the constructor.
-
-```solidity
-import {EIP155Signer} from "@oasisprotocol/sapphire-contracts/contracts/EIP155Signer.sol";
-
-struct EthereumKeypair {
- address addr;
- bytes32 secret;
- uint64 nonce;
-}
-
-struct EthTx {
- uint64 nonce;
- uint256 gasPrice;
- uint64 gasLimit;
- address to;
- uint256 value;
- bytes data;
- uint256 chainId;
-}
-
-// Proxy for gasless transaction.
-contract Gasless {
- EthereumKeypair private kp;
-
- function setKeypair(EthereumKeypair memory keypair) external payable {
- kp = keypair;
- }
-
- function makeProxyTx(address innercallAddr, bytes memory innercall)
- external
- view
- returns (bytes memory output)
- {
- bytes memory data = abi.encode(innercallAddr, innercall);
-
- // Call will invoke proxy().
- return
- EIP155Signer.sign(
- kp.addr,
- kp.secret,
- EIP155Signer.EthTx({
- nonce: kp.nonce,
- gasPrice: 100_000_000_000,
- gasLimit: 250000,
- to: address(this),
- value: 0,
- data: abi.encodeCall(this.proxy, data),
- chainId: block.chainid
- })
- );
- }
-
- function proxy(bytes memory data) external payable {
- (address addr, bytes memory subcallData) = abi.decode(
- data,
- (address, bytes)
- );
- (bool success, bytes memory outData) = addr.call{value: msg.value}(
- subcallData
- );
- if (!success) {
- // Add inner-transaction meaningful data in case of error.
- assembly {
- revert(add(outData, 32), mload(outData))
- }
- }
- kp.nonce += 1;
- }
-}
-```
-
-:::tip
-
-The snippet above only runs on Sapphire Mainnet, Testnet or Localnet.
-`EIP155Signer.sign()` is not supported on other EVM chains.
-
-:::
-
-### Simple Gasless Commenting dApp
-
-Let's see how we can implement on-chain signer for a gasless commenting dApp
-like this:
-
-```solidity
-contract CommentBox {
- string[] public comments;
-
- function comment(string memory commentText) external {
- comments.push(commentText);
- }
-}
-```
-
-Then, the TypeScript code on a client side for submitting a comment in a gasless
-fashion would look like this:
-
-```typescript
-const CommentBox = await ethers.getContractFactory("CommentBox");
-const commentBox = await CommentBox.deploy();
-const Gasless = await ethers.getContractFactory("Gasless");
-const gasless = await Gasless.deploy();
-
-// Set the keypair used to sign the meta-transaction.
-await gasless.setKeypair({
- addr: "70997970C51812dc3A010C7d01b50e0d17dc79C8",
- secret: Uint8Array.from(Buffer.from("59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d", 'hex')),
- nonce: 0,
-});
-
-const innercall = commentBox.interface.encodeFunctionData('comment', ['Hello, free world!']);
-const tx = await gasless.makeProxyTx(commentBox.address, innercall);
-
-const plainProvider = new ethers.providers.JsonRpcProvider(ethers.provider.connection);
-const plainResp = await plainProvider.sendTransaction(tx);
-
-const receipt = await ethers.provider.waitForTransaction(plainResp.hash);
-if (!receipt || receipt.status != 1) throw new Error('tx failed');
-```
-
-:::info Example
-
-You can download a complete on-chain signer example based on the above snippets
-from the [Sapphire ParaTime examples] repository.
-
-:::
-
-[Sapphire ParaTime examples]:
- https://github.com/oasisprotocol/sapphire-paratime/tree/main/examples/onchain-signer
-
-### Gasless Proxy in Production
-
-The snippets above have shown how the on-chain signer can generate and sign a
-meta-transaction for arbitrary transaction. In production environment however,
-you must consider the following:
-
-#### Confidentiality
-
-Both the inner- and the meta-transaction are stored on-chain unencrypted. Use
-`Sapphire.encrypt()` and `Sapphire.decrypt()` call on the inner-transaction with
-an encryption key generated and stored inside a confidential contract state.
-
-#### Gas Cost and Gas Limit
-
-The gas cost and the gas limit in our snippet were hardcoded inside the
-contract. Ideally the gas cost should be dynamically adjusted by an oracle and
-the gas limit determined based on the type of transactions. **Never let gas cost
-and limit to be freely defined by the user, since they can drain your relayer's
-account.**
-
-#### Allowed Transactions
-
-Your relayer will probably be used for transactions of a specific contract only.
-One approach is to store the allowed address of the target contract and **only
-allow calls to this contract address**.
-
-#### Access Control
-
-You can either whitelist specific addresses of the users in the relayer contract
-or implement the access control in the target contract. In the latter case, the
-relayer's `makeProxyTx()` should simulate the execution of the inner-transaction
-and generate the meta-transaction only if it inner-transaction succeeded.
-
-#### Multiple Signers
-
-Only one transaction per block can be relayed by the same signer since the order
-of the transactions is not deterministic and nonces could mismatch. To overcome
-this, relayer can randomly pick a signer from the **pool of signers**. When the
-transaction is relayed, don't forget to reimburse the signer of the transaction!
-
-:::info Example
-
-All the above points are considered in the [Demo Voting dApp][demo-voting].
-You can explore the code and also try out a deployed gasless version of the
-voting dApp on the [Oasis Playground site][demo-voting-playground]. The access
-control list is configured so that anyone can vote on any poll and only poll
-creators can close the poll.
-
-:::
-
-[demo-voting]: https://github.com/oasisprotocol/demo-voting
-[demo-voting-playground]: https://playground.oasis.io/demo-voting
-[dao-opl]: ../opl/host.md
-[EIP-155]: https://github.com/ethereum/EIPs/blob/master/EIPS/eip-155.md
-
-## Gas Station Network
-
-[Gas Station Network](https://docs.opengsn.org) (GSN) was adapted to work with
-Sapphire in a forked `@oasislabs/opengsn-cli` package. The diagram below
-illustrates a flow for signing a transaction by using a GSN[^1].
-
-![Diagram of the Gas Station Network Flow](../images/sapphire/gasless-gsn-flow.jpg)
-
-[^1]: The GSN flow diagram is courtesy of [OpenGSN documentation][opengsn-docs].
-
-[opengsn-docs]: https://github.com/opengsn/docs
-
-### Package Install
-
-Starting with an empty folder, let us install the
-[Oasis fork of the GSN command line tool](https://github.com/oasislabs/gsn) by
-using the following commands:
-
-```shell npm2yarn
-npm init
-npm install -D @oasislabs/opengsn-cli
-```
-
-Next, we will export our hex-encoded private key (**without** the leading `0x`)
-for deploying the gas station network as an environment variable:
-
-```shell
-export PRIVATE_KEY=...
-```
-
-### Deploy GSN
-
-Deploy GSN relaying contracts along with the test paymaster using a
-test token. Use the address of your account as `--burnAddress` and
-`--devAddress` parameters:
-
-```shell
-npx gsn deploy --network sapphire-testnet --burnAddress 0xfA3AC9f65C9D75EE3978ab76c6a1105f03156204 --devAddress 0xfA3AC9f65C9D75EE3978ab76c6a1105f03156204 --testToken true --testPaymaster true --yes --privateKeyHex $PRIVATE_KEY
-```
-
-After the command finishes successfully, you should find the addreses of
-deployed contracts at the end:
-
-```
- Deployed TestRecipient at address 0x594cd6354b23A5200a57355072E2A5B15354ee21
-
- RelayHub: 0xc4423AB6133B06e4e60D594Ac49abE53374124b3
- RelayRegistrar: 0x196036FBeC1dA841C60145Ce12b0c66078e141E6
- StakeManager: 0x6763c3fede9EBBCFbE4FEe6a4DE6C326ECCdacFc
- Penalizer: 0xA58A0D302e470490c064EEd5f752Df4095d3A002
- Forwarder: 0x59001d07a1Cd4836D22868fcc0dAf3732E93be81
- TestToken (test only): 0x6Ed21672c0c26Daa32943F7b1cA1f1d0ABdbac66
- Paymaster (Default): 0x8C06261f58a024C958d42df89be7195c8690008d
-```
-
-
-### Start GSN Relay Server
-
-Now we are ready to start our own relay server by using the following command.
-Use the newly deployed:
-
-- `RelayHub` address for `--relayHubAddress`,
-- `TestToken` address for `--managerStakeTokenAddress`,
-- address of your account for `--owner-address`
-
-```shell
-npx gsn relayer-run --relayHubAddress 0xc4423AB6133B06e4e60D594Ac49abE53374124b3 --managerStakeTokenAddress 0x6Ed21672c0c26Daa32943F7b1cA1f1d0ABdbac66 --ownerAddress '0xfA3AC9f65C9D75EE3978ab76c6a1105f03156204' --ethereumNodeUrl 'https://testnet.sapphire.oasis.dev' --workdir .
-```
-
-### Fund and Register GSN Relay Server
-
-The first thing is to fund your relay server so that it has enough native
-tokens to pay for others' transactions. Let's fund the paymaster with
-**5 tokens**. Use the `RelayHub` and `Paymaster` addresses for `--hub`
-and `--paymaster` values:
-
-```shell
-npx gsn paymaster-fund --network sapphire-testnet --hub 0xc4423AB6133B06e4e60D594Ac49abE53374124b3 --paymaster 0x8C06261f58a024C958d42df89be7195c8690008d --privateKeyHex $PRIVATE_KEY --amount 5000000000000000000
-```
-
-You can check the balance of the paymaster by running:
-
-```shell
-npx gsn paymaster-balance --network sapphire-testnet --hub 0xc4423AB6133B06e4e60D594Ac49abE53374124b3 --paymaster 0x8C06261f58a024C958d42df89be7195c8690008d
-```
-
-Next, we need to register the relay server with the your desired `relayUrl` by
-staking the `token` the relayHub requires.
-
-```shell
-npx gsn relayer-register --network sapphire-testnet --relayUrl 'http://localhost:8090' --token 0x6Ed21672c0c26Daa32943F7b1cA1f1d0ABdbac66 --wrap true --privateKeyHex $PRIVATE_KEY
-```
-
-After this step, your relay server should be ready to take incoming relay
-requests and forward them to the relay hub on Sapphire Testnet.
-
-### Send Testing Relayed Requests:
-
-We can test whether a relayed request can be forwarded and processed correctly.
-Scroll up to find the GSN deployment response and use the following parameters:
-- `Forwarder` as `--to`,
-- `Paymaster` as `--paymaster`,
-- your account address as `--from`
-
-Parameters matching our deployment would be:
-
-```shell
-npx gsn send-request --network sapphire-testnet --abiFile 'node_modules/@oasislabs/opengsn-cli/dist/compiled/TestRecipient.json' --method emitMessage --methodParams 'hello world!' --to 0x594cd6354b23A5200a57355072E2A5B15354ee21 --paymaster 0x8C06261f58a024C958d42df89be7195c8690008d --privateKeyHex $PRIVATE_KEY --from 0xfA3AC9f65C9D75EE3978ab76c6a1105f03156204 --gasLimit 150000 --gasPrice 100
-```
-
-:::info
-
-More detailed explanations of these GSN commands and parameters can be found on
-the [upstream OpenGSN website](https://docs.opengsn.org/javascript-client/gsn-helpers.html).
-
-:::
-
-### Writing a GSN-enabled Smart Contract
-
-First, install the OpenGSN contracts package:
-
-```shell npm2yarn
-npm install -D @opengsn/contracts@3.0.0-beta.2
-```
-
-Then follow the remainder of the steps from the
-[upstream OpenGSN docs](https://docs.opengsn.org/contracts/#receiving-a-relayed-call).
diff --git a/docs/dapp/sapphire/gasless.md b/docs/dapp/sapphire/gasless.md
new file mode 120000
index 0000000000..1ae10d0a76
--- /dev/null
+++ b/docs/dapp/sapphire/gasless.md
@@ -0,0 +1 @@
+../../../external/sapphire-paratime/docs/gasless.md
\ No newline at end of file
diff --git a/docs/dapp/sapphire/guide.mdx b/docs/dapp/sapphire/guide.mdx
deleted file mode 100644
index b7689b5aae..0000000000
--- a/docs/dapp/sapphire/guide.mdx
+++ /dev/null
@@ -1,390 +0,0 @@
----
-description: Guide to creating secure dApps on Sapphire
----
-
-import DocCard from '@theme/DocCard';
-import {findSidebarItem} from '@site/src/sidebarUtils';
-
-# Guide
-
-This page mainly describes the differences between Sapphire and Ethereum
-since there are a number of excellent tutorials on developing for Ethereum.
-If you don't know where to begin, the [Hardhat tutorial], [Solidity docs], and
-[Emerald dApp tutorial] are great places to start. You can continue following
-this guide once you've set up your development environment and have deployed
-your contract to a non-confidential EVM network (e.g., Ropsten, Emerald).
-
-
-[Hardhat tutorial]: https://hardhat.org/tutorial
-[Solidity docs]: https://docs.soliditylang.org/en/v0.8.15/solidity-by-example.html
-[Emerald dApp tutorial]: ../emerald/writing-dapps-on-emerald.mdx
-
-## Oasis Consensus Layer and Sapphire ParaTime
-
-The Oasis Network consists of the consensus layer and a number of ParaTimes.
-ParaTimes are independent replicated state machines that settle transactions
-using the consensus layer (to learn more, check the [Oasis Network Overview]
-[overview chapter]). Sapphire is a ParaTime which implements the Ethereum
-Virtual Machine (EVM).
-
-The minimum and also expected block time in Sapphire is **6 seconds**. Any
-Sapphire transaction will require at least this amount of time to be executed,
-and probably no more.
-
-ParaTimes, Sapphire included, are not allowed to directly access your tokens stored
-in consensus layer accounts. You will need to _deposit_ tokens from your consensus
-account to Sapphire. Consult the [How to transfer ROSE into an EVM ParaTime]
-[how-to-deposit-rose] chapter to learn more.
-
-
-[overview chapter]: ../../general/oasis-network/README.mdx
-[how-to-deposit-rose]: ../../general/manage-tokens/how-to-transfer-rose-into-paratime.mdx
-[Testnet faucet]: https://faucet.testnet.oasis.dev/
-
-## Testnet and Mainnet
-
-Sapphire is deployed on both Testnet and Mainnet. The Testnet should be
-considered unstable software and may have its state wiped at any time. As
-the name implies, only use the Testnet for testing unless you're testing how
-angry your users get when state is wiped.
-
-:::danger Never deploy production services on Testnet
-
-Because Testnet state can be wiped in the future, you should **never** deploy a
-production service on the Testnet! Just don't do it!
-
-Also note that while the Testnet does use actual TEEs, due to experimental
-software and different security parameters, **confidentiality of Sapphire on the
-Testnet is not guaranteed** -- all transactions and state published on the
-Sapphire Testnet should be considered public.
-
-:::
-
-:::tip
-
-For testing purposes, visit our [Testnet faucet] to obtain some TEST which you
-can then use on the Sapphire Testnet to pay for gas fees. The faucet supports
-sending TEST both to your consensus layer address or to your address inside the
-ParaTime.
-
-:::
-
-[network-parameters]: ../../node/mainnet/README.md
-[Testnet]: ../../node/testnet/README.md
-
-## Sapphire vs Ethereum
-
-Sapphire is generally compatible with Ethereum, the EVM, and all of the
-user and developer tooling that you already use. There are a few breaking changes,
-but we think that you'll like them:
-
-* Contract state is only visible to the contract that wrote it. With respect
- to the contract API, it's as if all state variables are declared as `private`, but
- with the further restriction that not even full nodes can read the values. Public or
- access-controlled values are provided instead through explicit getters.
-* Transactions and calls are end-to-end encrypted into the contract. Only the caller
- and the contract can see the data sent to/received from the ParaTime. This ends up
- defeating most of the utility of block explorers, however.
-* The `from` address using of calls is derived from a signature attached to the call.
- Unsigned calls have their sender set to the zero address. This allows contract authors
- to write getters that release secrets to authenticated callers, but without
- requiring a transaction to be posted on-chain.
-
-In addition to confidentiality, you get a few extra benefits including the ability to generate private
-entropy, and make signatures on-chain. An example of a dApp that uses both is a HSM contract
-that generates an Ethereum wallet and signs transactions sent to it via transactions.
-
-Otherwise Sapphire is like Emerald, which is like a fast, cheap Ethereum.
-
-## Integrating Sapphire
-
-Once ROSE tokens are [deposited into Sapphire], it should be painless for users to begin
-using dApps. To achieve this ideal user experience, we have to modify the dApp a little,
-but it's made simple by our compatibility library, [@oasisprotocol/sapphire-paratime].
-
-There are compatibility layers in other languages, which may be found in [the repo].
-
-
-[deposited into Sapphire]: ../../general/manage-tokens/how-to-transfer-rose-into-paratime.mdx
-[@oasisprotocol/sapphire-paratime]: https://www.npmjs.com/package/@oasisprotocol/sapphire-paratime
-[the repo]: https://github.com/oasisprotocol/sapphire-paratime/tree/main/clients
-
-## Writing Secure dApps
-
-### Wallets
-
-Sapphire is compatible with popular self-custodial wallets including MetaMask,
-Ledger, Brave, and so forth. You can also use libraries like Web3.js and Ethers
-to create programmatic wallets. In general, if it generates secp256k1 signatures,
-it'll work just fine.
-
-### Languages & Frameworks
-
-Sapphire is programmable using any language that targets the EVM, such as Solidity
-or Vyper. If you prefer to use an Ethereum framework like Hardhat or Foundry, you
-can also use those with Sapphire; all you need to do is set your Web3 gateway URL.
-You can find the details of the Oasis Sapphire Web3 gateway
-[here](/dapp/sapphire#web3-gateway).
-
-### Transactions & Calls
-
-![Client, Key Manager, Compute Node diagram](../../general/images/architecture/client-km-compute.svg)
-
-The figure above illustrates the flow of a confidential smart contract
-*transaction* executed on the Sapphire ParaTime.
-
-Transactions and calls must be encrypted and signed for maximum security.
-You can use the [@oasisprotocol/sapphire-paratime] JS package to make your life
-easy. It'll handle cryptography and signing for you.
-
-You should be aware that taking actions based on the value of private data may
-leak the private data through side channels like time spent and gas use. If you
-need to branch on private data, you should in most cases ensure that both
-branches exhibit similar time/gas and storage patterns.
-
-You can also make confidential smart contract *calls* on Sapphire. If you
-use `msg.sender` for access control in your contract, the call **must be
-signed**, otherwise `msg.sender` will be zeroed. On the other hand, set the
-`from` address to all zeros, if you want to avoid annoying signature popups in
-the user's wallet for calls that do not need to be signed. The JS library will
-do this for you.
-
-:::note
-
-Inside the smart contract code, there is no way of knowing whether the
-client's call data were originally encrypted or not.
-
-:::
-
-Detailed confidential smart contract transaction flow on Sapphire
- Detailed confidential smart contract call flow on Sapphire
-
- -
- -In this tutorial, you will build and deploy a unique dApp that requires -confidentiality to work. By the end of the tutorial, you should feel -comfortable setting up your Eth development environment to target Sapphire, -and know how and when to use confidentiality. - -The expected completion time of this tutorial is 15 minutes. - -:::info Sunsetting Truffle - -Per Consensys [announcement], Oasis will no longer support Truffle as of -2023-10-05 and encourage immediate [migration] to Hardhat. Please see our -repository for the archived Truffle [tutorial] and the deprecated [example]. - -::: - -[announcement]: https://consensys.io/blog/consensys-announces-the-sunset-of-truffle-and-ganache-and-new-hardhat -[migration]: https://trufflesuite.com/docs/truffle/how-to/migrate-to-hardhat/ -[tutorial]: https://github.com/oasisprotocol/docs/blob/2f4a1a3c217b82687ab9440bf051762ae369ed45/docs/dapp/sapphire/quickstart.mdx -[example]: https://github.com/oasisprotocol/sapphire-paratime/tree/3a85e42e6c1cc090c28a521cf7df6353aa8a30c8/examples/truffle - - -## Create a Sapphire-Native dApp - -Porting an existing Eth app is cool, and will provide benefits such as -protection against MEV. -However, starting from scratch with confidentiality in mind can unlock some -really novel dApps and provide a [higher level of security]. - -One simple-but-useful dApp that takes advantage of confidentiality is a -[dead person's switch] that reveals a secret (let's say the encryption key to a -data trove) if the operator fails to re-up before too long. -Let's make it happen! - -[higher level of security]: guide.mdx#writing-secure-dapps -[dead person's switch]: https://en.wikipedia.org/wiki/Dead_man%27s_switch - -### Init a new Hardhat project - -We're going to use Hardhat, but Sapphire should be compatible with your dev -environment of choice. Let us know if things are not as expected! - -1. Make & enter a new directory -2. `npx hardhat@~2.16.0 init` then create a TypeScript project. -3. Add [`@oasisprotocol/sapphire-hardhat`] as dependency: - - ```shell npm2yarn - npm install -D @oasisprotocol/sapphire-hardhat - ``` - -4. Install `@nomicfoundation/hardhat-toolbox`, TypeScript and other peer - dependencies required by HardHat. - -### Add the Sapphire Testnet to Hardhat - -Open up your `hardhat.config.ts` and drop in these lines. - -```diff -diff --git a/hardhat.config.ts b/hardhat.config.ts -index 414e974..49c95f9 100644 ---- a/hardhat.config.ts -+++ b/hardhat.config.ts -@@ -1,8 +1,19 @@ - import { HardhatUserConfig } from "hardhat/config"; -+import '@oasisprotocol/sapphire-hardhat'; - import "@nomicfoundation/hardhat-toolbox"; - - const config: HardhatUserConfig = { - solidity: "0.8.17", -+ networks: { -+ 'sapphire-testnet': { -+ // This is Testnet! If you want Mainnet, add a new network config item. -+ url: "https://testnet.sapphire.oasis.dev", -+ accounts: process.env.PRIVATE_KEY -+ ? [process.env.PRIVATE_KEY] -+ : [], -+ chainId: 0x5aff, -+ }, -+ }, - }; - - export default config; -``` - -By importing `@oasisprotocol/sapphire-hardhat` at the top of the config file, -**any network config entry corresponding to the Sapphire's chain ID will -automatically be wrapped with Sapphire specifics for encrypting and signing the -transactions**. - -### Get some Sapphire Testnet tokens - -Now for the fun part. We need to configure the Sapphire network and get some tokens. -Hit up the one and only [Oasis Testnet faucet] and select "Sapphire". -Submit the form and be on your way. - -[Oasis Testnet faucet]: https://faucet.testnet.oasis.dev - -### Get the Contract - -This is a Sapphire tutorial and you're already a Solidity expert, so let's not -bore you with explaining the gritty details of the contract. -Start by pasting [Vigil.sol] into `contracts/Vigil.sol`. - -While you're there, also place [run-vigil.ts] into `scripts/run-vigil.ts`. -We'll need that later. - -[Vigil.sol]: https://github.com/oasisprotocol/sapphire-paratime/blob/main/examples/hardhat/contracts/Vigil.sol -[run-vigil.ts]: https://github.com/oasisprotocol/sapphire-paratime/blob/main/examples/hardhat/scripts/run-vigil.ts - -#### Vigil.sol, the interesting parts - -The key state variables are: - -```solidity - SecretMetadata[] public _metas; - bytes[] private _secrets; -``` - -* `_metas` is marked with `public` visibility, so despite the state itself being - encrypted and not readable directly, Solidity will generate a getter that will - do the decryption for you. -* `_secrets` is `private` and therefore truly secret; only the contract can - access the data contained in this mapping. - -And the methods we'll care most about are - -* `createSecret`, which adds an entry to both `_metas` and `_secrets`. -* `revealSecret`, which acts as an access-controlled getter for the data - contained with `_secrets`. Due to trusted execution and confidentiality, the - only way that the secret will get revealed is if execution proceeds all the - way to the end of the function and does not revert. - -The rest of the methods are useful if you actually intended to use the contract, -but they demonstrate that developing for Sapphire is essentially the same as for -Ethereum. -You can even write tests against the Hardhat network and use Hardhat plugins. - -### Run the Contract - -And to wrap things up, we'll put `Vigil` through its paces. -First, let's see what's actually going on. - -After deploying the contract, we can create a secret, check that it's not -readable, wait a bit, and then check that it has become readable. -Pretty cool if you ask me! - -Anyway, make it happen by running - -```shell -PRIVATE_KEY="0x..." npx hardhat run scripts/run-vigil.ts --network sapphire-testnet -``` - -And if you see something like the following, you'll know you're well on the road -to deploying confidential dApps on Sapphire. - -``` -Vigil deployed to: 0x74dC4879B152FDD1DDe834E9ba187b3e14f462f1 -Storing a secret in 0x13125d868f5fb3cbc501466df26055ea063a90014b5ccc8dfd5164dc1dd67543 -Checking the secret -failed to fetch secret: reverted: not expired -Waiting... -Checking the secret again -The secret ingredient is brussels sprouts -``` - -## All done! - -Congratulations, you made it through the Sapphire tutorial! If you have any -questions, please check out the [guide] and join the discussion on the -[#sapphire-paratime Discord channel][social-media]. - -Best of luck on your future forays into confidentiality! - -:::info Example - -Visit the Sapphire ParaTime repository to download the [Hardhat][hardhat-example] -example of this quickstart. - -::: - -:::info Example - -If your project involves building a web frontend, we recommend that you check -out the official [Oasis starter] files. - -[Oasis starter]: https://github.com/oasisprotocol/demo-starter - -::: - -## See also - -