Oasis introduceerde het framework voor runtime off-chain logic (ROFL) om te helpen bij het bouwen en uitvoeren van apps off-chain, terwijl privacy wordt gewaarborgd en vertrouwen wordt behouden met on-chain verifieerbaarheid. Er zijn veel onderdelen betrokken bij het bouwen met ROFL.
In deze tutorial laat ik zien hoe je een kleine TypeScript-app kunt bouwen die een secp256k1-sleutel genereert binnen ROFL. Het zal gebruikmaken van de @oasisprotocol/rofl-client TypeScript SDK, die onder de motorkap communiceert met de appd REST API. De TypeScript-app zal ook:
Er zal een eenvoudige smoke test zijn die naar logs print.
Om de stappen in deze handleiding uit te voeren, heb je nodig:
Voor de setup-details, raadpleeg de documentatie over Quickstart Prerequisites.
De eerste stap is het initialiseren van een nieuwe app met behulp van de Oasis CLI.
oasis rofl init rofl-keygen
cd rofl-keygen
Bij het aanmaken van de app op het Testnet moet je tokens storten. Wijs op dit moment 100 TEST-tokens toe.
oasis rofl create --network testnet
Als output produceert de CLI de App ID, aangeduid met rofl1….
Nu ben je klaar om het project te starten.
npx hardhat init
Aangezien we een TypeScript-app demonstreren, kies TypeScript wanneer daarom wordt gevraagd en accepteer vervolgens de standaardinstellingen.
De volgende stap is het toevoegen van de kleine runtime-afhankelijkheden voor gebruik buiten Hardhat.
npm i @oasisprotocol/rofl-client ethers dotenv @types/node
npm i -D tsx
Het TypeScript-sjabloon van Hardhat creëert automatisch een tsconfig.json. We moeten een klein script toevoegen zodat de app-code kan compileren naar dist/.
// tsconfig.json
{
"compilerOptions": {
"rootDir": "./src",
"outDir": "./dist"
},
"include": ["src"]
}
In dit gedeelte voegen we een paar kleine TS-bestanden toe en één Solidity-contract.
src/
├── appd.ts # dunne wrapper over @oasisprotocol/rofl-client
├── evm.ts # ethers helpers (provider, wallet, tx, deploy)
├── keys.ts # kleine helpers (checksum)
└── scripts/
├── deploy-contract.ts # generiek deploy-script voor gecompileerde artefacten
└── smoke-test.ts # end-to-end demo (logs)
contracts/
└── Counter.sol # voorbeeldcontract
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();
}
/**
* Genereert (of determistisch herleidt) een secp256k1-sleutel binnen ROFL en
* retourneert deze als een 0x-prefix hex string (voor ethers.js Wallet).
*
* Lokale ontwikkeling ALLEEN (buiten ROFL): Als de socket ontbreekt en je stelt
* ALLOW_LOCAL_DEV=true en LOCAL_DEV_SK=0x<64-hex> in, wordt die waarde gebruikt.
*/
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 niet gevonden en geen LOCAL_DEV_SK opgegeven (alleen dev).'
);
}
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("Transactie gedropt of vervangen vóór bevestiging");
}
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 niet gemined");
}
return { address: contract.target as string, receipt };
}
3. src/keys.ts — kleine 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 — enkele end-to-end flow
Dit is een belangrijke stap omdat dit script meerdere functies heeft:
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(`Wachten op financiering... huidig saldo=${formatEther(bal)} ETH`);
await sleep(pollMs);
}
throw new Error("Time-out tijdens wachten op financiering.");
}
async function main() {
const appId = await getAppId().catch(() => null);
console.log(`ROFL App ID: ${appId ?? "(niet beschikbaar buiten ROFL)"}`);
const sk = await getEvmSecretKey(KEY_ID);
// OPMERKING: Deze demo vertrouwt de geconfigureerde RPC-provider. Voor productie, geef de voorkeur aan een
// light client (bijvoorbeeld Helios) zodat je externe chain-status kunt verifiëren.
const wallet = secretKeyToWallet(sk).connect(makeProvider(RPC_URL, CHAIN_ID));
const addr = checksumAddress(await wallet.getAddress());
console.log(`EVM-adres (Base Sepolia): ${addr}`);
const msg = "hello from rofl";
const sig = await signPersonalMessage(wallet, msg);
console.log(`Ondertekend bericht: "${msg}"`);
console.log(`Handtekening: ${sig}`);
const provider = wallet.provider as JsonRpcProvider;
let bal = await provider.getBalance(addr);
if (bal === 0n) {
console.log("Vul het bovenstaande adres aan met Base Sepolia ETH om door te gaan.");
bal = await waitForFunding(provider, addr);
}
console.log(`Saldo gedetecteerd: ${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 mist abi/bytecode");
}
const { address: contractAddress, receipt: deployRcpt } =
await deployContract(wallet, artifact.abi, artifact.bytecode, []);
console.log(`Counter gedeployed op ${contractAddress} (tx=${deployRcpt.hash})`);
console.log("Smoke test succesvol voltooid!");
}
main().catch((e) => {
console.error(e);
process.exit(1);
});
5. contracts/Counter.sol — minimaal voorbeeld
// 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 — generieke 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");
/**
* Gebruik:
* npm run deploy-contract -- ./artifacts/MyContract.json '[arg0, arg1]'
* Het artefact moet { abi, bytecode } bevatten.
*/
async function main() {
const [artifactPath, ctorJson = "[]"] = process.argv.slice(2);
if (!artifactPath) {
console.error("Gebruik: 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("Artefact moet { abi, bytecode } bevatten");
}
let args: unknown[];
try {
args = JSON.parse(ctorJson);
if (!Array.isArray(args)) throw new Error("constructor args moeten een JSON-array zijn");
} catch (e) {
throw new Error(`Mislukt om constructor args JSON te parsen: ${String(e)}`);
}
const sk = await getEvmSecretKey(KEY_ID);
// OPMERKING: Deze demo vertrouwt de geconfigureerde RPC-provider. Voor productie, geef de voorkeur aan een
// light client (bijvoorbeeld Helios) zodat je externe chain-status kunt verifiëren.
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);
});
In deze fase hebben we minimale configuratie nodig om Counter.sol te compileren
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;
Een belangrijk punt om te noteren is dat lokale compilatie optioneel is, dus je kunt dit overslaan als je wilt. De volgende stap is een keuze — verwijder het bestaande contracts/Lock.sol-bestand of je kunt het bijwerken naar Solidity versie 0.8.24.
npx hardhat compile
Dit is een essentiële stap. Hier heb je een Dockerfile nodig die TS bouwt en het contract compileert. Het bestand zal ook de smoke test eenmaal uitvoeren en vervolgens inactief blijven terwijl je logs inspecteert.
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"]
Vervolgens moet je de appd socket mounten die door ROFL wordt verstrekt. Wees gerust dat er geen openbare poorten worden blootgesteld in het proces.
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
Het is belangrijk om te onthouden dat ROFL alleen draait op Intel TDX-enabled hardware. Dus als je images compileert op een andere host, zoals macOS, dan is het doorgeven van de parameter --platform linux/amd64 een essentiële extra stap.
docker buildx build --platform linux/amd64 \
-t docker.io/YOURUSER/rofl-keygen:0.1.0 --push .
Een interessant punt om hier op te merken is dat je kunt kiezen voor extra beveiliging en verifieerbaarheid. Je hoeft alleen de digest vast te pinnen en image: …@sha256:… in compose.yaml te gebruiken.
Er is een stap die je moet nemen voordat je het commando oasis rofl build uitvoert. Omdat het bouwen van het image-segment na containerisatie komt, moet je de services.demo.image in compose.yaml bijwerken naar de image die je hebt gebouwd.
Voor eenvoudige TypeScript-projecten, zoals deze, is er soms een mogelijkheid dat de image-grootte groter is dan verwacht. Het is daarom raadzaam om de rofl.yaml resources sectie bij te werken naar ten minste: memory: 1024 en storage.size: 4096.
Nu ben je klaar.
oasis rofl build
Je kunt vervolgens de enclave-identiteiten en config publiceren.
oasis rofl update
Dit is een eenvoudige stap waarbij je deployt naar een Testnet-provider.
oasis rofl deploy
Dit is een proces van 2 stappen, hoewel de tweede stap optioneel is.
Eerst bekijk je smoke-test logs.
oasis rofl machine logs
Als je alle stappen tot nu toe correct hebt voltooid, zie je in de output:
Vervolgens, lokale dev. Hier moet je npm run build:all uitvoeren om de TypeScript-code en het Solidity-contract te compileren. Sla deze stap over indien niet nodig.
export ALLOW_LOCAL_DEV=true
export LOCAL_DEV_SK=0x<64-hex-dev-secret-key> # NIET GEBRUIKEN IN PROD
npm run smoke-test
Er is een key generation demo in de Oasis GitHub, die je kunt raadplegen als voorbeeld van deze tutorial. https://github.com/oasisprotocol/demo-rofl-keygen
Nu je met succes een sleutel hebt gegenereerd in ROFL met appd, berichten hebt ondertekend, een contract hebt gedeployed en ETH hebt verplaatst op Base Sepolia, laat ons in de reactiesectie je feedback weten. Voor een snelle chat met het Oasis engineering team voor hulp bij specifieke problemen, kun je je opmerkingen plaatsen in het dev-central channel in de officiële Discord.
Oorspronkelijk gepubliceerd op https://dev.to op 20 februari 2026.
Guide To Cross-Chain Key Generation (EVM / Base) With Oasis ROFL was oorspronkelijk gepubliceerd in Coinmonks op Medium, waar mensen het gesprek voortzetten door dit verhaal te markeren en erop te reageren.



Financiën
Delen
Deel dit artikel
Link kopierenX (Twitter)LinkedInFacebookE-mail
Pantera leidt $11,5M-ronde in Based, een Hyper