Build SUAPPs
This tutorial will show you how to build a SUAPP using suave-viem
, our typescript SDK, and two different SUAPP example contracts.
There are two different templates you can use for your SUAPP. One with minimal, typescript-only dependencies; and one which uses Next.
Set up suave-viem
β
Clone the repo:
git clone git@github.com:flashbots/suave-viem.git && cd suave-viem
Use bun to install the dependencies and build the package:
bun install && bun run build
Symlink your newly built package in the global directory:
cd src/ && bun link
Notesβ
- Confidential Compute Requests on SUAVE do not work with wallets that implement the EIP-1193 Javascript API. Therefore, we use the unsafe
eth_sign
method to sign CCRs, which does work, but requires that you enable this functionality in wallets like MetaMask.- To do so in MetaMask, go to "Settings" -> "Advanced" -> scroll to bottom -> switch Eth_sign requests on.
- Both templates assume that you are running SUAVE locally.
- No tests are included for the contracts, as it is not trivial to test new precompiles and different transaction types (i.e. CCRs) in
forge
at this time.
Typescript Templateβ
This template can be found directly in the suave-viem
repo under examples/suave-web-demo
. Continuing on from above, you can setup the template by running:
cd ../examples/suave-web-demo/ && bun install
This template use forge
to handle the contracts it interacts with. You will need to compile them, which can be done with:
bun run compile
Now you can start the frontend with:
bun run dev
This template uses the same MEV-Share example contract we worked with using the Golang SDK in the previous tutorial.
If you're struggling with any of the above, you can also find this pure typescript template as a standalone repo here.
Next Templateβ
This template comes with a more extensive frontend framework, which uses Next (in typescript) and therefore depends on React. You can get it running by first cloning the repo and installing its dependencies:
git clone git@github.com:andytudhope/build-a-suapp-next-ts.git \
&& cd build-a-suapp-next-ts \
&& yarn # make sure you have previously built and symlinked suave-viem for this to work
Setup forge
to compile your contracts:
cd packages/forge/ && forge install && forge build
Deploy the compiled contracts from the root directory (you need to have SUAVE running locally for this to work):
chmod +x packages/forge/deploy && yarn contracts:deploy
You can start the frontend with:
yarn fe:dev
Working with CCRsβ
These two templates illustrate different aspects of building on SUAVE.
The Next template uses a basic contract to demonstrate how CCRs work on SUAVE, and the sort of programming model you can expect as a developer building SUAPPs.
The Typescript template will lead you through how to sign a transaction on another domain (Goerli in this case), and then submit that as a CCR on SUAVE.
Let's therefore start in the Next template and look at how CCRs work and how to use them. OnChainState.sol
demonstrates that any CCR which tries to change state directly will revert. Rather, you need to use a callback to a different function that does change state in order to ensure that data sent in a CCR does remain confidential:
// nilExample is a function executed in a confidential request
// that CANNOT modify the state of the smart contract.
function nilExample() external payable returns (bytes memory) {
require(Suave.isConfidential());
state++;
return abi.encodeWithSelector(this.nilExampleCallback.selector);
}
function exampleCallback() external {
state++;
emit UpdatedState(state);
}
// example is a function executed in a confidential request that includes
// a callback that can modify the state.
function example() external view returns (bytes memory) {
require(Suave.isConfidential());
return bytes.concat(this.exampleCallback.selector);
}
If you try and call nilExample()
from the frontend, it will revert. And, in order to call example()
, we need to understand how to craft a CCR so that we can pass the require(Suave.isConfidential());
check. The code required for this is here:
const sendExample = async () => {
if (!provider || !suaveWallet) {
console.warn(`provider=${provider}\nsuaveWallet=${suaveWallet}`)
return
}
const nonce = await provider.getTransactionCount({ address: suaveWallet.account.address });
const ccr: TransactionRequestSuave = {
confidentialInputs: '0x',
kettleAddress: '0xB5fEAfbDD752ad52Afb7e1bD2E40432A485bBB7F', // Use 0x03493869959C866713C33669cA118E774A30A0E5 on Rigil.
to: deployedAddress,
gasPrice: 2000000000n,
gas: 100000n,
type: '0x43', // SuaveTxRequestTypes.ConfidentialRequest
chainId: 16813125, // chain id of local SUAVE devnet and Rigil
data: encodeFunctionData({
abi: OnChainState.abi,
functionName: 'example',
}),
nonce
};
const hash = await suaveWallet.sendTransaction(ccr);
console.log(`Transaction hash: ${hash}`);
setPendingReceipt(provider.waitForTransactionReceipt({ hash }));
}
There are a few points to understand here:
- You need to enable "Eth_sign" in the Advanced settings in your browser wallet for this to work.
- The code fetches the nonce manually, because we'll be using that newly-enabled
eth_sign
method. - We can leave the
confidentialInputs
field empty, as we're not actually sending any data along with this transaction: just demonstrating how CCRs work. - We specify a new type for the transaction. The 2
suave-viem
different TxRequest and Tx types are defined here. - The
suaveWallet.sendTransaction(ccr)
useseth_sign
under the hood, along with a few other steps required to serialize the transaction correctly, which you can see happening here.
CCRs with dataβ
What happens when we do actually want to send data in our CCR? The typescript template demonstrates how to do this by signing transactions on Goerli that you wish to be processed by kettles on SUAVE.
In this case, we need to:
- Craft a transaction on your chosen domain and sign it.
- Append relevant details like the
decryptionCondition
,kettleAddress
,contract
andchainId
. - Use a helper function like
toConfidentialRequest
to (i) place our signed transaction inconfidentialInputs
and (ii) replace thedata
field with a call to the appropriate method in the specified SUAVE contract.
The code which achieves this can be found here and looks like this:
const sendDataRecord = async (suaveWallet: any) => {
// create sample transaction; won't land onchain, but will pass payload validation
const sampleTx = {
type: "eip1559" as 'eip1559',
chainId: 5,
nonce: 0,
maxBaseFeePerGas: 0x3b9aca00n,
maxPriorityFeePerGas: 0x5208n,
to: '0x0000000000000000000000000000000000000000' as Address,
value: 0n,
data: '0xf00ba7' as Hex,
}
const signedTx = await goerliWallet.signTransaction(sampleTx)
console.log("signed goerli tx", signedTx)
// create bid & send ccr
try {
const bid = new MevShareRecord(
1n + await goerliProvider.getBlockNumber(),
signedTx,
KETTLE_ADDRESS,
MevShareContract.address as Address,
suaveRigil.id
)
console.log(bid)
const ccr = bid.toConfidentialRequest()
const txHash = await suaveWallet.sendTransaction(ccr)
console.log("sendResult", txHash)
// callback with result
onSendDataRecord(txHash)
} catch (e) {
return onSendDataRecord('0x', e)
}
}
Conclusionβ
You now have two different templates from which to begin building your own SUAPP π.
These templates demonstrate how to interact with SUAVE confidentially, both directly and with data from another domain.
Good luck and happy building β‘π€.