Oasis ได้แนะนำเฟรมเวิร์กสำหรับ runtime off-chain logic (ROFL) เพื่อช่วยในการสร้างและรันแอปพลิเคชันนอกเชนในขณะที่รับประกันความเป็นส่วนตัวและรักษาความไว้วางใจด้วยการตรวจสอบได้บนเชน มีส่วนประกอบหลายอย่างในการสร้างด้วย ROFL
ในบทช่วยสอนนี้ ผมจะสาธิตวิธีการสร้างแอป TypeScript ขนาดเล็ก สร้างคีย์ secp256k1 ภายใน ROFL จะใช้ @oasisprotocol/rofl-client TypeScript SDK ซึ่งสื่อสารกับ appd REST API ภายใต้พื้นผิว แอป TypeScript จะ ยัง:
จะมีsmoke test แบบง่ายๆ ที่พิมพ์ไปยัง บันทึก
เพื่อทำตามขั้นตอนที่อธิบายไว้ในคู่มือนี้ คุณจะ ต้องมี:
สำหรับรายละเอียดการตั้งค่า โปรดดูเอกสารเกี่ยวกับ Quickstart Prerequisites
ขั้นตอนแรกคือการเริ่มต้นแอปใหม่โดยใช้ Oasis CLI
oasis rofl init rofl-keygen
cd rofl-keygen
ในขณะที่สร้างแอปบน Testnet คุณจะต้องฝากโทเค็น กำหนด 100 โทเค็น TEST ใน จุดนี้
oasis rofl create --network testnet
เป็นเอาต์พุต CLI จะสร้าง App ID แสดงด้วย rofl1….
ตอนนี้ คุณพร้อมที่จะเริ่มต้น โปรเจกต์
npx hardhat init
เนื่องจากเรากำลังนำเสนอแอป TypeScript เลือก TypeScript เมื่อได้รับแจ้ง จากนั้นยอมรับค่าเริ่มต้น
ขั้นตอนถัดไปคือการเพิ่ม runtime deps ขนาดเล็กสำหรับใช้งานนอก Hardhat
npm i @oasisprotocol/rofl-client ethers dotenv @types/node
npm i -D tsx
เทมเพลต TypeScript ของ Hardhat สร้าง tsconfig.json โดยอัตโนมัติ เราจำเป็นต้องเพิ่มสคริปต์เล็กๆ เพื่อให้โค้ดแอปสามารถคอมไพล์ไปยัง dist/
// tsconfig.json
{
"compilerOptions": {
"rootDir": "./src",
"outDir": "./dist"
},
"include": ["src"]
}
ในส่วนนี้ เราจะเพิ่มไฟล์ TS ขนาดเล็กสองสามไฟล์และสัญญา Solidity หนึ่งฉบับ
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
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
นี่เป็นขั้นตอนที่สำคัญเนื่องจากสคริปต์นี้มีหลายฟังก์ชัน:
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);
});
ในขั้นตอนนี้ เราจะต้องใช้การกำหนดค่าขั้นต่ำในการคอมไพล์ 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;
จุดที่ควรสังเกตคือการคอมไพล์ในเครื่องเป็นตัวเลือก ดังนั้นคุณสามารถข้ามได้หากต้องการ ขั้นตอนถัดไปคือทางเลือก — ลบไฟล์ contracts/Lock.sol ที่มีอยู่หรือคุณสามารถอัปเดตเป็น Solidity version 0.8.24
npx hardhat compile
นี่เป็นขั้นตอนที่สำคัญ ที่นี่ คุณต้องใช้ Dockerfile ที่สร้าง TS และคอมไพล์สัญญา ไฟล์จะรัน smoke test ครั้งเดียว จากนั้นจะพักรอในขณะที่คุณตรวจสอบ บันทึก
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"]
ถัดไป คุณต้องเมานต์ appd socket ที่จัดเตรียมโดย ROFL มั่นใจได้ว่าไม่มีพอร์ตสาธารณะถูกเปิดเผยใน กระบวนการ
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
สิ่งสำคัญที่ต้องจำไว้คือ ROFL รันได้เฉพาะบนฮาร์ดแวร์ที่เปิดใช้งาน Intel TDX เท่านั้น ดังนั้น หากคุณกำลังคอมไพล์อิมเมจบนโฮสต์อื่น เช่น macOS การส่งพารามิเตอร์ — platform linux/amd64 เป็นขั้นตอนเพิ่มเติมที่ จำเป็น
docker buildx build --platform linux/amd64 \
-t docker.io/YOURUSER/rofl-keygen:0.1.0 --push .
ประเด็นที่น่าสนใจที่ควรทราบคือคุณสามารถเลือกความปลอดภัยและการตรวจสอบได้เพิ่มเติม คุณเพียงแค่ต้องปักหมุด digest และใช้ image: …@sha256:… ใน compose.yaml
มีขั้นตอนที่คุณต้องทำก่อนรันคำสั่ง oasis rofl build เนื่องจากการสร้างเซกเมนต์อิมเมจมาหลังจากการสร้างคอนเทนเนอร์ คุณจะต้องอัปเดต services.demo.image ใน compose.yaml เป็นอิมเมจที่คุณสร้าง
สำหรับโปรเจกต์ TypeScript แบบง่ายๆ เช่นนี้ บางครั้งอาจมีความเป็นไปได้ว่าขนาดอิมเมจจะใหญ่กว่าที่คาดไว้ ดังนั้นจึงแนะนำให้อัปเดตส่วน resources ของ rofl.yaml เป็นอย่างน้อย: memory: 1024 และ storage.size: 4096
ตอนนี้ คุณ พร้อมแล้ว
oasis rofl build
คุณสามารถเผยแพร่ enclave identities และ config ต่อไป
oasis rofl update
นี่เป็นขั้นตอนที่ค่อนข้างง่ายซึ่งคุณปรับใช้ไปยังผู้ให้บริการ Testnet
oasis rofl deploy
นี่เป็นกระบวนการ 2 ขั้นตอน แม้ว่าขั้นตอนที่สองจะเป็นตัวเลือก
ก่อนอื่น คุณดู smoke‑test logs
oasis rofl machine logs
หากคุณทำตามขั้นตอนทั้งหมดจนถึงตอนนี้อย่างถูกต้อง คุณจะเห็นใน เอาต์พุต:
ถัดไป local dev ที่นี่ คุณต้องรัน npm run build:all เพื่อคอมไพล์โค้ด TypeScript และสัญญา Solidity ข้ามขั้นตอนนี้หากไม่ จำเป็น
export ALLOW_LOCAL_DEV=true
export LOCAL_DEV_SK=0x<64-hex-dev-secret-key> # DO NOT USE IN PROD
npm run smoke-test
มี key generation demo ใน Oasis GitHub ซึ่งคุณสามารถอ้างอิงเป็นตัวอย่างของบทช่วยสอนนี้ https://github.com/oasisprotocol/demo-rofl-keygen
ตอนนี้ที่คุณสร้างคีย์ใน ROFL ด้วย appd ลงนามข้อความ ปรับใช้สัญญา และย้าย ETH บน Base Sepolia สำเร็จแล้ว แจ้งให้เราทราบในส่วนความคิดเห็นเกี่ยวกับความคิดเห็นของคุณ สำหรับการแชทอย่างรวดเร็วกับทีมวิศวกร Oasis เพื่อขอความช่วยเหลือเกี่ยวกับปัญหาเฉพาะ คุณสามารถโพสต์ความคิดเห็นของคุณใน dev-central channel ใน Discord อย่างเป็นทางการ
เผยแพร่ครั้งแรกที่ https://dev.to เมื่อวันที่ 20 กุมภาพันธ์ 2026
Guide To Cross-Chain Key Generation (EVM / Base) With Oasis ROFL ได้รับการเผยแพร่ครั้งแรกใน Coinmonks บน Medium ซึ่งผู้คนกำลังสนทนาต่อโดยการเน้นและตอบสนองต่อเรื่องราวนี้


