Oasis a introdus cadrul pentru logica runtime off-chain (ROFL) pentru a ajuta la construirea și rularea aplicațiilor off-chain, asigurând în același timp confidențialitatea și menținând încrederea cu verificabilitate on-chain. Există multe componente în mișcare la construirea cu ROFL.
În acest tutorial, voi demonstra cum să construiești o aplicație TypeScript mică, generând o cheie secp256k1 în interiorul ROFL. Va utiliza @oasisprotocol/rofl-client TypeScript SDK, care comunică cu appd REST API în fundal. Aplicația TypeScript va face:
Va exista un simplu smoke test care afișează în jurnale.
Pentru a face pașii descriși în acest ghid, vei avea nevoie de:
Pentru detaliile de configurare, te rugăm să consulți documentația despre Cerințe preliminare pentru Quickstart.
Primul pas este să inițializezi o nouă aplicație folosind Oasis CLI.
oasis rofl init rofl-keygen
cd rofl-keygen
În momentul creării aplicației pe Testnet, va trebui să depui token-uri. Alocă 100 de token-uri TEST în acest moment.
oasis rofl create --network testnet
Ca rezultat, CLI-ul va produce App ID, notat prin rofl1….
Acum, ești pregătit să începi proiectul.
npx hardhat init
Deoarece prezentăm o aplicație TypeScript, alege TypeScript când ești întrebat, apoi acceptă setările implicite.
Următorul pas ar fi să adaugi dependențele mici de runtime pentru utilizare în afara Hardhat.
npm i @oasisprotocol/rofl-client ethers dotenv @types/node
npm i -D tsx
Șablonul TypeScript al Hardhat creează automat un tsconfig.json. Trebuie să adăugăm un script mic astfel încât codul aplicației să poată fi compilat în dist/.
// tsconfig.json
{
"compilerOptions": {
"rootDir": "./src",
"outDir": "./dist"
},
"include": ["src"]
}
În această secțiune, vom adăuga câteva fișiere TS mici și un contract Solidity.
src/
├── appd.ts # wrapper subțire peste @oasisprotocol/rofl-client
├── evm.ts # ajutoare ethers (provider, wallet, tx, deploy)
├── keys.ts # ajutoare mici (checksum)
└── scripts/
├── deploy-contract.ts # script generic de deploy pentru artefacte compilate
└── smoke-test.ts # demo end-to-end (jurnale)
contracts/
└── Counter.sol # contract exemplu
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();
}
/**
* Generează (sau re-derivă în mod determinist) o cheie secp256k1 în interiorul ROFL și
* o returnează ca un șir hex cu prefix 0x (pentru ethers.js Wallet).
*
* Dezvoltare locală DOAR (în afara ROFL): Dacă socket-ul lipsește și setezi
* ALLOW_LOCAL_DEV=true și LOCAL_DEV_SK=0x<64-hex>, acea valoare este folosită.
*/
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 — ajutoare ethers
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 — ajutoare mici
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 — flux end-to-end singular
Acesta este un pas important deoarece acest script are multiple funcții:
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);
// NOTĂ: Acest demo are încredere în furnizorul RPC configurat. Pentru producție, preferă un
// light client (de exemplu, Helios) astfel încât să poți verifica starea lanțului la distanță.
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 — exemplu minimal
// 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 — deployer generic
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");
/**
* Utilizare:
* npm run deploy-contract -- ./artifacts/MyContract.json '[arg0, arg1]'
* Artefactul trebuie să conțină { 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);
// NOTĂ: Acest demo are încredere în furnizorul RPC configurat. Pentru producție, preferă un
// light client (de exemplu, Helios) astfel încât să poți verifica starea lanțului la distanță.
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);
});
În această etapă, vom avea nevoie de configurație minimă pentru a compila 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;
Punctul de reținut este că compilarea locală este opțională, deci poți să o sari dacă vrei. Următorul pas este o alegere — fie ștergi fișierul existent contracts/Lock.sol, fie îl poți actualiza la Solidity versiunea 0.8.24.
npx hardhat compile
Acesta este un pas esențial. Aici, trebuie să creezi un Dockerfile care construiește TS și compilează contractul. Fișierul va rula și smoke test o dată, apoi va rămâne inactiv în timp ce inspectezi jurnalele.
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"]
Apoi, trebuie să montezi appd socket furnizat de ROFL. Fii sigur că niciun port public nu este expus în 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
Este important să reții că ROFL rulează doar pe hardware cu Intel TDX activat. Deci, dacă compilezi imagini pe un host diferit, cum ar fi macOS, atunci trecerea parametrului — platform linux/amd64 este un pas suplimentar esențial.
docker buildx build --platform linux/amd64 \
-t docker.io/YOURUSER/rofl-keygen:0.1.0 --push .
Un punct interesant de reținut aici este că poți opta pentru securitate și verificabilitate suplimentară. Trebuie doar să fixezi digest-ul și să folosești image: …@sha256:… în compose.yaml.
Există un pas pe care trebuie să îl faci înainte de a rula comanda oasis rofl build. Deoarece construirea segmentului de imagine vine după containerizare, va trebui să actualizezi services.demo.image în compose.yaml la imaginea pe care ai construit-o.
Pentru proiecte TypeScript simple, precum acesta, există uneori posibilitatea ca dimensiunea imaginii să fie mai mare decât se anticipează. Este astfel recomandabil să actualizezi secțiunea rofl.yaml resources la cel puțin: memory: 1024 și storage.size: 4096.
Acum, ești pregătit.
oasis rofl build
Poți apoi publica identitățile enclave și config.
oasis rofl update
Acesta este un pas suficient de ușor unde implementezi la un furnizor Testnet.
oasis rofl deploy
Acesta este un proces în 2 pași, deși al doilea pas este opțional.
Mai întâi, vizualizezi jurnalele smoke-test.
oasis rofl machine logs
Dacă ai finalizat corect toți pașii până acum, vei vedea în rezultat:
Apoi, dev local. Aici, trebuie să rulezi npm run build:all pentru a compila codul TypeScript și contractul Solidity. Sari acest pas dacă nu este necesar.
export ALLOW_LOCAL_DEV=true
export LOCAL_DEV_SK=0x<64-hex-dev-secret-key> # NU FOLOSI ÎN PROD
npm run smoke-test
Există o demo de generare de chei în GitHub-ul Oasis, pe care o poți consulta ca exemplu al acestui tutorial. https://github.com/oasisprotocol/demo-rofl-keygen
Acum că ai generat cu succes o cheie în ROFL cu appd, ai semnat mesaje, ai implementat un contract și ai transferat ETH pe Base Sepolia, anunță-ne în secțiunea de comentarii feedback-ul tău. Pentru o discuție rapidă cu echipa de inginerie Oasis pentru ajutor cu probleme specifice, poți lăsa comentariile tale în canalul dev-central din Discord oficial.
Publicat inițial la https://dev.to pe 20 februarie 2026.
Guide To Cross-Chain Key Generation (EVM / Base) With Oasis ROFL a fost publicat inițial în Coinmonks pe Medium, unde oamenii continuă conversația evidențiind și răspunzând la această poveste.


