قدمت Oasis إطار عمل لمنطق وقت التشغيل خارج السلسلة (ROFL) للمساعدة في بناء وتشغيل التطبيقات خارج السلسلة مع ضمان الخصوصية والحفاظ على الثقة من خلال إمكانية التحقق على السلسلة. هناك العديد من الأجزاء المتحركة للبناء باستخدام ROFL.
في هذا البرنامج التعليمي، سأوضح كيفية بناء تطبيق TypeScript صغير، يولد مفتاح secp256k1 داخل ROFL. سيستخدم @oasisprotocol/rofl-client TypeScript SDK، والذي يتواصل مع appd REST API تحت الغطاء. سيقوم تطبيق TypeScript أيضًا بـ:
سيكون هناك اختبار دخان بسيط يطبع في السجلات.
لإجراء الخطوات الموضحة في هذا الدليل، ستحتاج إلى:
للحصول على تفاصيل الإعداد، يرجى الرجوع إلى الوثائق الخاصة بالمتطلبات الأساسية للبدء السريع.
الخطوة الأولى هي تهيئة تطبيق جديد باستخدام Oasis CLI.
oasis rofl init rofl-keygen
cd rofl-keygen
في وقت إنشاء التطبيق على Testnet، سيُطلب منك إيداع الرموز. قم بتخصيص 100 TEST tokens في هذه المرحلة.
oasis rofl create --network testnet
كمخرج، ستنتج CLI معرف التطبيق، المشار إليه بـ rofl1….
الآن، أنت جاهز لبدء المشروع.
npx hardhat init
نظرًا لأننا نعرض تطبيق TypeScript، اختر TypeScript عند المطالبة، ثم اقبل الإعدادات الافتراضية.
الخطوة التالية ستكون إضافة تبعيات وقت التشغيل الصغيرة للاستخدام خارج 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 # غلاف رقيق فوق @oasisprotocol/rofl-client
├── evm.ts # مساعدو ethers (المزود، المحفظة، المعاملة، النشر)
├── keys.ts # مساعدون صغار (المجموع الاختباري)
└── scripts/
├── deploy-contract.ts # نص نشر عام للملفات المترجمة
└── smoke-test.ts # عرض توضيحي شامل (السجلات)
contracts/
└── Counter.sol # عقد نموذجي
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();
}
/**
* يولد (أو يعيد اشتقاق بشكل حتمي) مفتاح secp256k1 داخل ROFL و
* يعيده كسلسلة سداسية عشرية مسبوقة بـ 0x (لمحفظة ethers.js).
*
* التطوير المحلي فقط (خارج ROFL): إذا كان المقبس مفقودًا وقمت بتعيين
* ALLOW_LOCAL_DEV=true و LOCAL_DEV_SK=0x<64-hex>، يتم استخدام تلك القيمة.
*/
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
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 — مساعدون صغار
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 — تدفق شامل واحد
هذه خطوة مهمة حيث أن هذا النص البرمجي له وظائف متعددة:
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);
// ملاحظة: هذا العرض التوضيحي يثق بمزود RPC المكوّن. للإنتاج، يفضل استخدام
// عميل خفيف (على سبيل المثال، Helios) حتى تتمكن من التحقق من حالة السلسلة البعيدة.
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 — عينة بسيطة
// 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 — مُنشر عام
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");
/**
* الاستخدام:
* npm run deploy-contract -- ./artifacts/MyContract.json '[arg0, arg1]'
* يجب أن يحتوي الملف على { 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);
// ملاحظة: هذا العرض التوضيحي يثق بمزود RPC المكوّن. للإنتاج، يفضل استخدام
// عميل خفيف (على سبيل المثال، Helios) حتى تتمكن من التحقق من حالة السلسلة البعيدة.
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 الإصدار 0.8.24.
npx hardhat compile
هذه خطوة أساسية. هنا، تحتاج إلى Dockerfile يبني TS ويترجم العقد. سيقوم الملف أيضًا بتشغيل اختبار الدخان مرة واحدة، ثم يبقى في وضع الخمول أثناء فحص السجلات.
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 المقدم من 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 .
نقطة مثيرة للاهتمام يجب ملاحظتها هنا هي أنه يمكنك اختيار الأمان الإضافي وإمكانية التحقق. تحتاج فقط إلى تثبيت الملخص واستخدام image: …@sha256:… في compose.yaml.
هناك خطوة يجب عليك اتخاذها قبل تشغيل أمر oasis rofl build. نظرًا لأن بناء جزء الصورة يأتي بعد الحاوية، ستحتاج إلى تحديث services.demo.image في compose.yaml إلى الصورة التي قمت ببنائها.
بالنسبة لمشاريع TypeScript البسيطة، مثل هذا، هناك أحيانًا احتمال أن يكون حجم الصورة أكبر من المتوقع. لذلك يُنصح بتحديث قسم موارد rofl.yaml إلى الأقل: memory: 1024 و storage.size: 4096.
الآن، أنت جاهز.
oasis rofl build
يمكنك بعد ذلك نشر هويات الجيب المحمي والتكوين.
oasis rofl update
هذه خطوة سهلة بما يكفي حيث تنشر على مزود Testnet.
oasis rofl deploy
هذه عملية من خطوتين، على الرغم من أن الخطوة الثانية اختيارية.
أولاً، تعرض سجلات اختبار الدخان.
oasis rofl machine logs
إذا أكملت جميع الخطوات حتى الآن بشكل صحيح، فسترى في المخرجات:
بعد ذلك، التطوير المحلي. هنا، تحتاج إلى تشغيل npm run build:all لترجمة كود TypeScript وعقد Solidity. تخطى هذه الخطوة إذا لم تكن هناك حاجة.
export ALLOW_LOCAL_DEV=true
export LOCAL_DEV_SK=0x<64-hex-dev-secret-key> # لا تستخدم في الإنتاج
npm run smoke-test
هناك عرض توضيحي لتوليد المفتاح في Oasis GitHub، والذي يمكنك الرجوع إليه كمثال على هذا البرنامج التعليمي. https://github.com/oasisprotocol/demo-rofl-keygen
الآن بعد أن قمت بنجاح بتوليد مفتاح في ROFL مع appd، ووقعت الرسائل، ونشرت عقدًا، ونقلت ETH على Base Sepolia، أخبرنا في قسم التعليقات بملاحظاتك. للدردشة السريعة مع فريق هندسة Oasis للحصول على مساعدة بشأن قضايا محددة، يمكنك ترك تعليقاتك في قناة dev-central في Discord الرسمي.
نُشر في الأصل على https://dev.to في 20 فبراير 2026.
تم نشر دليل توليد المفاتيح عبر السلاسل (EVM / Base) مع Oasis ROFL في الأصل في Coinmonks على Medium، حيث يواصل الناس المحادثة من خلال تسليط الضوء والرد على هذه القصة.


