Oasis introduced the framework for runtime off-chain logic ( ROFL) to help build and run apps off-chain while ensuring privacy and maintaining trust with on-chainOasis introduced the framework for runtime off-chain logic ( ROFL) to help build and run apps off-chain while ensuring privacy and maintaining trust with on-chain

Guide To Cross-Chain Key Generation (EVM / Base) With Oasis ROFL

2026/02/20 21:16
10 min read

Oasis introduced the framework for runtime off-chain logic ( ROFL) to help build and run apps off-chain while ensuring privacy and maintaining trust with on-chain verifiability. There are many moving parts to building with ROFL.
In this tutorial, I will demonstrate how to build a tiny TypeScript app, generating a secp256k1 key inside ROFL. It will be using the @oasisprotocol/rofl-client TypeScript SDK, which talks to the appd REST API under the hood. The TypeScript app will also:

There will be a simple smoke test that prints to logs.

Prerequisites

To do the steps described in this guide, you will need:

  • Node.js 20+ and Docker (or Podman)
  • Oasis CLI and a minimum of 120 TEST tokens in your wallet (Oasis Testnet faucet)
  • Some Base Sepiola test ETH (Base Sepiola faucet)

For the setup details, please refer to the documentation on Quickstart Prerequisites.

Init App

The first step is to initialize a new app using the Oasis CLI.

oasis rofl init rofl-keygen
cd rofl-keygen

Create App

At the time of creating the app on the Testnet, you will be required to deposit tokens. Assign 100 TEST tokens at this point.

oasis rofl create --network testnet

As output, the CLI will produce the App ID, denoted by rofl1….

Init a Hardhat (TypeScript) project

Now, you are ready to kickstart the project.

npx hardhat init

Since we are showcasing a TypeScript app, choose TypeScript when prompted, and then accept the defaults.
The nextstep would be to add the small runtime deps for use outside of Hardhat.

npm i @oasisprotocol/rofl-client ethers dotenv @types/node
npm i -D tsx

Hardhat’s TypeScript template automatically creates a tsconfig.json. We need to add a small script so that the app code can compile to dist/.

// tsconfig.json
{
"compilerOptions": {
"rootDir": "./src",
"outDir": "./dist"
},
"include": ["src"]
}

App structure

In this section, we will add a few small TS files and one Solidity contract.

src/
├── appd.ts # thin wrapper over @oasisprotocol/rofl-client
├── evm.ts # ethers helpers (provider, wallet, tx, deploy)
├── keys.ts # tiny helpers (checksum)
└── scripts/
├── deploy-contract.ts # generic deploy script for compiled artifacts
└── smoke-test.ts # end-to-end demo (logs)
contracts/
└── Counter.sol # sample contract

  1. src/appd.ts — thin wrapper over the SDK Here, you will need to use the official client to talk to appd (UNIX socket). We will also need to keep an explicit local‑dev fallback when running outside ROFL.

src/appd.ts

import {existsSync} from 'node:fs';
import {
RoflClient,
KeyKind,
ROFL_SOCKET_PATH
} from '@oasisprotocol/rofl-client';
const client = new RoflClient(); // UDS: /run/rofl-appd.sock
export async function getAppId(): Promise<string> {
return client.getAppId();
}
/**
* Generates (or deterministically re-derives) a secp256k1 key inside ROFL and
* returns it as a 0x-prefixed hex string (for ethers.js Wallet).
*
* Local development ONLY (outside ROFL): If the socket is missing and you set
* ALLOW_LOCAL_DEV=true and LOCAL_DEV_SK=0x<64-hex>, that value is used.
*/
export async function getEvmSecretKey(keyId: string): Promise<string> {
if (existsSync(ROFL_SOCKET_PATH)) {
const hex = await client.generateKey(keyId, KeyKind.SECP256K1);
return hex.startsWith('0x') ? hex : `0x${hex}`;
}
const allow = process.env.ALLOW_LOCAL_DEV === 'true';
const pk = process.env.LOCAL_DEV_SK;
if (allow && pk && /^0x[0-9a-fA-F]{64}$/.test(pk)) return pk;
throw new Error(
'rofl-appd socket not found and no LOCAL_DEV_SK provided (dev only).'
);
}

2. src/evm.ts — ethers helpers

import {
JsonRpcProvider,
Wallet,
parseEther,
type TransactionReceipt,
ContractFactory
} from "ethers";
export function makeProvider(rpcUrl: string, chainId: number) {
return new JsonRpcProvider(rpcUrl, chainId);
}
export function connectWallet(
skHex: string,
rpcUrl: string,
chainId: number
): Wallet {
const w = new Wallet(skHex);
return w.connect(makeProvider(rpcUrl, chainId));
}
export async function signPersonalMessage(wallet: Wallet, msg: string) {
return wallet.signMessage(msg);
}
export async function sendEth(
wallet: Wallet,
to: string,
amountEth: string
): Promise<TransactionReceipt> {
const tx = await wallet.sendTransaction({
to,
value: parseEther(amountEth)
});
const receipt = await tx.wait();
if (receipt == null) {
throw new Error("Transaction dropped or replaced before confirmation");
}
return receipt;
}
export async function deployContract(
wallet: Wallet,
abi: any[],
bytecode: string,
args: unknown[] = []
): Promise<{ address: string; receipt: TransactionReceipt }> {
const factory = new ContractFactory(abi, bytecode, wallet);
const contract = await factory.deploy(...args);
const deployTx = contract.deploymentTransaction();
const receipt = await deployTx?.wait();
await contract.waitForDeployment();
if (!receipt) {
throw new Error("Deployment TX not mined");
}
return { address: contract.target as string, receipt };
}

3. src/keys.ts — tiny helpers

import { Wallet, getAddress } from "ethers";
export function secretKeyToWallet(skHex: string): Wallet {
return new Wallet(skHex);
}
export function checksumAddress(addr: string): string {
return getAddress(addr);
}

4. src/scripts/smoke-test.ts — single end‑to‑end flow

This is an important step as this script has multiple functions:

  • print the App ID (inside ROFL), address, and a signed message
  • wait for funding
  • deploy the counter contract

import "dotenv/config";
import { readFileSync } from "node:fs";
import { join } from "node:path";
import { getAppId, getEvmSecretKey } from "../appd.js";
import { secretKeyToWallet, checksumAddress } from "../keys.js";
import { makeProvider, signPersonalMessage, sendEth, deployContract } from "../evm.js";
import { formatEther, JsonRpcProvider } from "ethers";
const RPC_URL = process.env.BASE_RPC_URL ?? "https://sepolia.base.org";
const CHAIN_ID = Number(process.env.BASE_CHAIN_ID ?? "84532");
const KEY_ID = process.env.KEY_ID ?? "evm:base:sepolia";
function sleep(ms: number): Promise<void> {
return new Promise((r) => setTimeout(r, ms));
}
async function waitForFunding(
provider: JsonRpcProvider,
addr: string,
minWei: bigint = 1n,
timeoutMs = 15 * 60 * 1000,
pollMs = 5_000
): Promise<bigint> {
const start = Date.now();
while (Date.now() - start < timeoutMs) {
const bal = await provider.getBalance(addr);
if (bal >= minWei) return bal;
console.log(`Waiting for funding... current balance=${formatEther(bal)} ETH`);
await sleep(pollMs);
}
throw new Error("Timed out waiting for funding.");
}
async function main() {
const appId = await getAppId().catch(() => null);
console.log(`ROFL App ID: ${appId ?? "(unavailable outside ROFL)"}`);
const sk = await getEvmSecretKey(KEY_ID);
// NOTE: This demo trusts the configured RPC provider. For production, prefer a
// light client (for example, Helios) so you can verify remote chain state.
const wallet = secretKeyToWallet(sk).connect(makeProvider(RPC_URL, CHAIN_ID));
const addr = checksumAddress(await wallet.getAddress());
console.log(`EVM address (Base Sepolia): ${addr}`);
const msg = "hello from rofl";
const sig = await signPersonalMessage(wallet, msg);
console.log(`Signed message: "${msg}"`);
console.log(`Signature: ${sig}`);
const provider = wallet.provider as JsonRpcProvider;
let bal = await provider.getBalance(addr);
if (bal === 0n) {
console.log("Please fund the above address with Base Sepolia ETH to continue.");
bal = await waitForFunding(provider, addr);
}
console.log(`Balance detected: ${formatEther(bal)} ETH`);
const artifactPath = join(process.cwd(), "artifacts", "contracts", "Counter.sol", "Counter.json");
const artifact = JSON.parse(readFileSync(artifactPath, "utf8"));
if (!artifact?.abi || !artifact?.bytecode) {
throw new Error("Counter artifact missing abi/bytecode");
}
const { address: contractAddress, receipt: deployRcpt } =
await deployContract(wallet, artifact.abi, artifact.bytecode, []);
console.log(`Deployed Counter at ${contractAddress} (tx=${deployRcpt.hash})`);
console.log("Smoke test completed successfully!");
}
main().catch((e) => {
console.error(e);
process.exit(1);
});

5. contracts/Counter.sol — minimal sample

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
contract Counter {
uint256 private _value;
event Incremented(uint256 v);
event Set(uint256 v);
function current() external view returns (uint256) { return _value; }
function inc() external { unchecked { _value += 1; } emit Incremented(_value); }
function set(uint256 v) external { _value = v; emit Set(v); }
}

6. src/scripts/deploy-contract.ts — generic deployer

import "dotenv/config";
import { readFileSync } from "node:fs";
import { getEvmSecretKey } from "../appd.js";
import { secretKeyToWallet } from "../keys.js";
import { makeProvider, deployContract } from "../evm.js";
const KEY_ID = process.env.KEY_ID ?? "evm:base:sepolia";
const RPC_URL = process.env.BASE_RPC_URL ?? "https://sepolia.base.org";
const CHAIN_ID = Number(process.env.BASE_CHAIN_ID ?? "84532");
/**
* Usage:
* npm run deploy-contract -- ./artifacts/MyContract.json '[arg0, arg1]'
* The artifact must contain { abi, bytecode }.
*/
async function main() {
const [artifactPath, ctorJson = "[]"] = process.argv.slice(2);
if (!artifactPath) {
console.error("Usage: npm run deploy-contract -- <artifact.json> '[constructorArgsJson]'");
process.exit(2);
}
const artifactRaw = readFileSync(artifactPath, "utf8");
const artifact = JSON.parse(artifactRaw);
const { abi, bytecode } = artifact ?? {};
if (!abi || !bytecode) {
throw new Error("Artifact must contain { abi, bytecode }");
}
let args: unknown[];
try {
args = JSON.parse(ctorJson);
if (!Array.isArray(args)) throw new Error("constructor args must be a JSON array");
} catch (e) {
throw new Error(`Failed to parse constructor args JSON: ${String(e)}`);
}
const sk = await getEvmSecretKey(KEY_ID);
// NOTE: This demo trusts the configured RPC provider. For production, prefer a
// light client (for example, Helios) so you can verify remote chain state.
const wallet = secretKeyToWallet(sk).connect(makeProvider(RPC_URL, CHAIN_ID));
const { address, receipt } = await deployContract(wallet, abi, bytecode, args);
console.log(JSON.stringify({ contractAddress: address, txHash: receipt.hash, status: receipt.status }, null, 2));
}
main().catch((e) => {
console.error(e);
process.exit(1);
});

Hardhat (contracts only)

At this stage, we will need minimal config to compile Counter.sol

hardhat.config.ts

import type { HardhatUserConfig } from "hardhat/config";
const config: HardhatUserConfig = {
solidity: {
version: "0.8.24",
settings: {
optimizer: { enabled: true, runs: 200 }
}
},
paths: {
sources: "./contracts",
artifacts: "./artifacts",
cache: "./cache"
}
};
export default config;

Point to note is that local compilation is optional, so you can skip it if you want. Next step is a choice — either delete the existing contracts/Lock.sol file or you can update it to Solidity version 0.8.24.

npx hardhat compile

Containerize

This is an essential step. Here, you need to a Dockerfile that builds TS and compiles the contract. The file will also run the smoke test once, and then stand idle while you inspect logs.

Dockerfile

FROM node:20-alpine
WORKDIR /app
COPY package.json package-lock.json* ./
RUN npm ci
COPY tsconfig.json ./
COPY src ./src
COPY contracts ./contracts
COPY hardhat.config.ts ./
RUN npm run build && npx hardhat compile && npm prune --omit=dev
ENV NODE_ENV=production
CMD ["sh", "-c", "node dist/scripts/smoke-test.js || true; tail -f /dev/null"]

Next, you must mount appd socket provided by ROFL. Rest assured that no public ports are exposed in the process.

compose.yaml

services:
demo:
image: docker.io/YOURUSER/rofl-keygen:0.1.0
platform: linux/amd64
environment:
- KEY_ID=${KEY_ID:-evm:base:sepolia}
- BASE_RPC_URL=${BASE_RPC_URL:-https://sepolia.base.org}
- BASE_CHAIN_ID=${BASE_CHAIN_ID:-84532}
volumes:
- /run/rofl-appd.sock:/run/rofl-appd.sock

Build the image

It is important to remember that ROFL only runs on Intel TDX-enabled hardware. So, if you’re compiling images on a different host, such as macOS, then passing the — platform linux/amd64 parameter is an essential extra step.

docker buildx build --platform linux/amd64 \
-t docker.io/YOURUSER/rofl-keygen:0.1.0 --push .

An interesting point to note here is that you can opt for extra security and verifiability. You just need to pin the digest and use image: …@sha256:… in compose.yaml.

Build ROFL bundle

There is a step that you must take before running the oasis rofl build command. Since building the image segment comes after containerization, you will need to update the services.demo.image in compose.yaml to the image you built.
For simple TypeScript projects, like this one, there is sometimes a possibility that the image size is larger than anticipated. It is thus advisable to update the rofl.yaml resources section to at least: memory: 1024 and storage.size: 4096.
Now, you are ready.

oasis rofl build

You can next publish the enclave identities and config.

oasis rofl update

Deploy

This is an easy enough step where you deploy to a Testnet provider.

oasis rofl deploy

End‑to‑end (Base Sepolia)

This is a 2-step process, although the second step is optional.
First, you view smoke‑test logs.

oasis rofl machine logs

If you have completed all the steps till now correctly, you will see in the output:

  • App ID
  • EVM address and a signed message
  • A prompt to fund the address
  • Once funding is done, a Counter.sol deployment

Next, local dev. Here, you need to run npm run build:all to compile the TypeScript code and the Solidity contract. Skip this step if not needed.

export ALLOW_LOCAL_DEV=true
export LOCAL_DEV_SK=0x<64-hex-dev-secret-key> # DO NOT USE IN PROD
npm run smoke-test

Security & notes to remember

  • Provider logs are not encrypted at rest. So, never log secret keys.
  • The appd socket /run/rofl-appd.sock exists only inside ROFL.
  • There may be rate limits in public RPCs. So, it is advisable to opt for a dedicated Base RPC URL.

There is a key generation demo in the Oasis GitHub, which you can refer to as an example of this tutorial. https://github.com/oasisprotocol/demo-rofl-keygen

Now that you have successfully generated a key in ROFL with appd, signed messages, deployed a contract, and moved ETH on Base Sepolia, let us know in the comments section your feedback. For a quick chat with the Oasis engineering team for help with specific issues, you can drop your comments in the dev-central channel in the official Discord.

Originally published at https://dev.to on February 20, 2026.


Guide To Cross-Chain Key Generation (EVM / Base) With Oasis ROFL was originally published in Coinmonks on Medium, where people are continuing the conversation by highlighting and responding to this story.

Market Opportunity
CROSS Logo
CROSS Price(CROSS)
$0.10838
$0.10838$0.10838
+0.19%
USD
CROSS (CROSS) Live Price Chart
Disclaimer: The articles reposted on this site are sourced from public platforms and are provided for informational purposes only. They do not necessarily reflect the views of MEXC. All rights remain with the original authors. If you believe any content infringes on third-party rights, please contact service@support.mexc.com for removal. MEXC makes no guarantees regarding the accuracy, completeness, or timeliness of the content and is not responsible for any actions taken based on the information provided. The content does not constitute financial, legal, or other professional advice, nor should it be considered a recommendation or endorsement by MEXC.

You May Also Like

CEO Sandeep Nailwal Shared Highlights About RWA on Polygon

CEO Sandeep Nailwal Shared Highlights About RWA on Polygon

The post CEO Sandeep Nailwal Shared Highlights About RWA on Polygon appeared on BitcoinEthereumNews.com. Polygon CEO Sandeep Nailwal highlighted Polygon’s lead in global bonds, Spiko US T-Bill, and Spiko Euro T-Bill. Polygon published an X post to share that its roadmap to GigaGas was still scaling. Sentiments around POL price were last seen to be bearish. Polygon CEO Sandeep Nailwal shared key pointers from the Dune and RWA.xyz report. These pertain to highlights about RWA on Polygon. Simultaneously, Polygon underlined its roadmap towards GigaGas. Sentiments around POL price were last seen fumbling under bearish emotions. Polygon CEO Sandeep Nailwal on Polygon RWA CEO Sandeep Nailwal highlighted three key points from the Dune and RWA.xyz report. The Chief Executive of Polygon maintained that Polygon PoS was hosting RWA TVL worth $1.13 billion across 269 assets plus 2,900 holders. Nailwal confirmed from the report that RWA was happening on Polygon. The Dune and https://t.co/W6WSFlHoQF report on RWA is out and it shows that RWA is happening on Polygon. Here are a few highlights: – Leading in Global Bonds: Polygon holds 62% share of tokenized global bonds (driven by Spiko’s euro MMF and Cashlink euro issues) – Spiko U.S.… — Sandeep | CEO, Polygon Foundation (※,※) (@sandeepnailwal) September 17, 2025 The X post published by Polygon CEO Sandeep Nailwal underlined that the ecosystem was leading in global bonds by holding a 62% share of tokenized global bonds. He further highlighted that Polygon was leading with Spiko US T-Bill at approximately 29% share of TVL along with Ethereum, adding that the ecosystem had more than 50% share in the number of holders. Finally, Sandeep highlighted from the report that there was a strong adoption for Spiko Euro T-Bill with 38% share of TVL. He added that 68% of returns were on Polygon across all the chains. Polygon Roadmap to GigaGas In a different update from Polygon, the community…
Share
BitcoinEthereumNews2025/09/18 01:10
Russia’s Central Bank Prepares Crackdown on Crypto in New 2026–2028 Strategy

Russia’s Central Bank Prepares Crackdown on Crypto in New 2026–2028 Strategy

The Central Bank of Russia’s long-term strategy for 2026 to 2028 paints a picture of growing concern. The document, prepared […] The post Russia’s Central Bank Prepares Crackdown on Crypto in New 2026–2028 Strategy appeared first on Coindoo.
Share
Coindoo2025/09/18 02:30
United Kingdom CFTC GBP NC Net Positions declined to £-42.4K from previous £-25.8K

United Kingdom CFTC GBP NC Net Positions declined to £-42.4K from previous £-25.8K

The post United Kingdom CFTC GBP NC Net Positions declined to £-42.4K from previous £-25.8K appeared on BitcoinEthereumNews.com. Information on these pages contains
Share
BitcoinEthereumNews2026/02/21 04:50