Oasis a introduit le framework pour la logique off-chain d'exécution (ROFL) afin d'aider à construire et exécuter des applications off-chain tout en garantissant la confidentialité et en maintenant la confiance grâce à la vérifiabilité on-chain. La construction avec ROFL comporte de nombreux éléments mobiles.
Dans ce tutoriel, je vais démontrer comment créer une petite application TypeScript, générant une clé secp256k1 dans ROFL. Elle utilisera le SDK TypeScript @oasisprotocol/rofl-client, qui communique avec l'API REST appd en arrière-plan. L'application TypeScript pourra également :
Il y aura un simple test de smoke qui affiche dans les journaux.
Pour effectuer les étapes décrites dans ce guide, vous aurez besoin de :
Pour les détails de configuration, veuillez vous référer à la documentation sur les prérequis du démarrage rapide.
La première étape consiste à initialiser une nouvelle application à l'aide de l'Oasis CLI.
oasis rofl init rofl-keygen
cd rofl-keygen
Au moment de créer l'application sur le Testnet, vous devrez déposer des tokens. Assignez 100 tokens TEST à ce stade.
oasis rofl create --network testnet
En sortie, le CLI produira l'App ID, indiqué par rofl1….
Maintenant, vous êtes prêt à lancer le projet.
npx hardhat init
Puisque nous présentons une application TypeScript, choisissez TypeScript lorsque vous y êtes invité, puis acceptez les valeurs par défaut.
L'étape suivante consisterait à ajouter les petites dépendances d'exécution pour une utilisation en dehors de Hardhat.
npm i @oasisprotocol/rofl-client ethers dotenv @types/node
npm i -D tsx
Le modèle TypeScript de Hardhat crée automatiquement un tsconfig.json. Nous devons ajouter un petit script pour que le code de l'application puisse se compiler dans dist/.
// tsconfig.json
{
"compilerOptions": {
"rootDir": "./src",
"outDir": "./dist"
},
"include": ["src"]
}
Dans cette section, nous ajouterons quelques petits fichiers TS et un contrat Solidity.
src/
├── appd.ts # wrapper mince sur @oasisprotocol/rofl-client
├── evm.ts # assistants ethers (fournisseur, portefeuille, tx, déploiement)
├── keys.ts # petits assistants (somme de contrôle)
└── scripts/
├── deploy-contract.ts # script de déploiement générique pour les artefacts compilés
└── smoke-test.ts # démo de bout en bout (journaux)
contracts/
└── Counter.sol # contrat d'exemple
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();
}
/**
* Génère (ou re-dérive de manière déterministe) une clé secp256k1 dans ROFL et
* la renvoie sous forme de chaîne hexadécimale préfixée par 0x (pour ethers.js Wallet).
*
* Développement local UNIQUEMENT (en dehors de ROFL) : Si le socket est manquant et que vous définissez
* ALLOW_LOCAL_DEV=true et LOCAL_DEV_SK=0x<64-hex>, cette valeur est utilisée.
*/
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(
'Socket rofl-appd introuvable et aucun LOCAL_DEV_SK fourni (dev uniquement).'
);
}
2. src/evm.ts — assistants 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 abandonnée ou remplacée avant 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("TX de déploiement non minée");
}
return { address: contract.target as string, receipt };
}
3. src/keys.ts — petits assistants
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 de bout en bout unique
Il s'agit d'une étape importante car ce script a plusieurs fonctions :
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(`En attente du financement... solde actuel=${formatEther(bal)} ETH`);
await sleep(pollMs);
}
throw new Error("Délai d'attente dépassé pour le financement.");
}
async function main() {
const appId = await getAppId().catch(() => null);
console.log(`ROFL App ID: ${appId ?? "(non disponible en dehors de ROFL)"}`);
const sk = await getEvmSecretKey(KEY_ID);
// NOTE : Cette démo fait confiance au fournisseur RPC configuré. Pour la production, préférez un
// client léger (par exemple, Helios) afin de pouvoir vérifier l'état de la chaîne distante.
const wallet = secretKeyToWallet(sk).connect(makeProvider(RPC_URL, CHAIN_ID));
const addr = checksumAddress(await wallet.getAddress());
console.log(`Adresse EVM (Base Sepolia): ${addr}`);
const msg = "hello from rofl";
const sig = await signPersonalMessage(wallet, msg);
console.log(`Message signé : "${msg}"`);
console.log(`Signature : ${sig}`);
const provider = wallet.provider as JsonRpcProvider;
let bal = await provider.getBalance(addr);
if (bal === 0n) {
console.log("Veuillez financer l'adresse ci-dessus avec de l'ETH Base Sepolia pour continuer.");
bal = await waitForFunding(provider, addr);
}
console.log(`Solde détecté : ${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("Artefact Counter manquant abi/bytecode");
}
const { address: contractAddress, receipt: deployRcpt } =
await deployContract(wallet, artifact.abi, artifact.bytecode, []);
console.log(`Counter déployé à ${contractAddress} (tx=${deployRcpt.hash})`);
console.log("Test de smoke terminé avec succès !");
}
main().catch((e) => {
console.error(e);
process.exit(1);
});
5. contracts/Counter.sol — exemple 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 — déployeur générique
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");
/**
* Utilisation :
* npm run deploy-contract -- ./artifacts/MyContract.json '[arg0, arg1]'
* L'artefact doit contenir { abi, bytecode }.
*/
async function main() {
const [artifactPath, ctorJson = "[]"] = process.argv.slice(2);
if (!artifactPath) {
console.error("Utilisation : 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("L'artefact doit contenir { abi, bytecode }");
}
let args: unknown[];
try {
args = JSON.parse(ctorJson);
if (!Array.isArray(args)) throw new Error("les arguments du constructeur doivent être un tableau JSON");
} catch (e) {
throw new Error(`Échec de l'analyse des arguments du constructeur JSON : ${String(e)}`);
}
const sk = await getEvmSecretKey(KEY_ID);
// NOTE : Cette démo fait confiance au fournisseur RPC configuré. Pour la production, préférez un
// client léger (par exemple, Helios) afin de pouvoir vérifier l'état de la chaîne distante.
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);
});
À ce stade, nous aurons besoin d'une configuration minimale pour compiler 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 à noter : la compilation locale est facultative, vous pouvez donc l'ignorer si vous le souhaitez. L'étape suivante est un choix — soit supprimer le fichier contracts/Lock.sol existant, soit le mettre à jour vers Solidity version 0.8.24.
npx hardhat compile
Il s'agit d'une étape essentielle. Ici, vous avez besoin d'un Dockerfile qui construit TS et compile le contrat. Le fichier exécutera également le test de smoke une fois, puis restera inactif pendant que vous inspectez les journaux.
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"]
Ensuite, vous devez monter le socket appd fourni par ROFL. Rassurez-vous, aucun port public n'est exposé dans le processus.
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
Il est important de se rappeler que ROFL ne fonctionne que sur du matériel compatible Intel TDX. Donc, si vous compilez des images sur un hôte différent, tel que macOS, le passage du paramètre — platform linux/amd64 est une étape supplémentaire essentielle.
docker buildx build --platform linux/amd64 \
-t docker.io/YOURUSER/rofl-keygen:0.1.0 --push .
Un point intéressant à noter ici est que vous pouvez opter pour une sécurité et une vérifiabilité supplémentaires. Il vous suffit d'épingler le digest et d'utiliser image: …@sha256:… dans compose.yaml.
Il y a une étape que vous devez effectuer avant d'exécuter la commande oasis rofl build. Puisque la construction du segment d'image vient après la conteneurisation, vous devrez mettre à jour services.demo.image dans compose.yaml vers l'image que vous avez construite.
Pour les projets TypeScript simples, comme celui-ci, il existe parfois une possibilité que la taille de l'image soit plus grande que prévu. Il est donc conseillé de mettre à jour la section resources de rofl.yaml au moins vers : memory: 1024 et storage.size: 4096.
Maintenant, vous êtes prêt.
oasis rofl build
Vous pouvez ensuite publier les identités de l'enclave et la configuration.
oasis rofl update
Il s'agit d'une étape assez simple où vous déployez vers un fournisseur Testnet.
oasis rofl deploy
Il s'agit d'un processus en 2 étapes, bien que la deuxième étape soit facultative.
Tout d'abord, vous visualisez les journaux du test de smoke.
oasis rofl machine logs
Si vous avez correctement effectué toutes les étapes jusqu'à présent, vous verrez dans la sortie :
Ensuite, dev local. Ici, vous devez exécuter npm run build:all pour compiler le code TypeScript et le contrat Solidity. Ignorez cette étape si elle n'est pas nécessaire.
export ALLOW_LOCAL_DEV=true
export LOCAL_DEV_SK=0x<64-hex-dev-secret-key> # NE PAS UTILISER EN PROD
npm run smoke-test
Il existe une démo de génération de clés dans le GitHub Oasis, que vous pouvez consulter comme exemple de ce tutoriel. https://github.com/oasisprotocol/demo-rofl-keygen
Maintenant que vous avez réussi à générer une clé dans ROFL avec appd, signé des messages, déployé un contrat et déplacé de l'ETH sur Base Sepolia, faites-nous part de vos commentaires dans la section des commentaires. Pour une discussion rapide avec l'équipe d'ingénierie Oasis pour obtenir de l'aide sur des problèmes spécifiques, vous pouvez déposer vos commentaires dans le canal dev-central sur le Discord officiel.
Publié à l'origine sur https://dev.to le 20 février 2026.
Guide To Cross-Chain Key Generation (EVM / Base) With Oasis ROFL a été publié à l'origine dans Coinmonks sur Medium, où les gens poursuivent la conversation en mettant en évidence et en répondant à cette histoire.


