Oasis memperkenalkan rangka kerja untuk logik luar rantaian runtime (ROFL) untuk membantu membina dan menjalankan aplikasi luar rantaian sambil memastikan privasi dan mengekalkan kepercayaan dengan kebolehsahkan dalam rantaian. Terdapat banyak bahagian bergerak untuk membina dengan ROFL.
Dalam tutorial ini, saya akan menunjukkan cara membina aplikasi TypeScript kecil, menjana kunci secp256k1 di dalam ROFL. Ia akan menggunakan @oasisprotocol/rofl-client TypeScript SDK, yang berkomunikasi dengan appd REST API di belakang tabir. Aplikasi TypeScript juga akan:
Akan ada ujian asap mudah yang mencetak ke log.
Untuk melakukan langkah-langkah yang diterangkan dalam panduan ini, anda memerlukan:
Untuk butiran persediaan, sila rujuk dokumentasi mengenai Prasyarat Mulakan Pantas.
Langkah pertama adalah memulakan aplikasi baharu menggunakan Oasis CLI.
oasis rofl init rofl-keygen
cd rofl-keygen
Pada masa mencipta aplikasi di Testnet, anda akan dikehendaki mendepositkan token. Peruntukkan 100 token TEST pada ketika ini.
oasis rofl create --network testnet
Sebagai output, CLI akan menghasilkan App ID, ditandakan dengan rofl1….
Sekarang, anda sudah bersedia untuk memulakan projek.
npx hardhat init
Memandangkan kami mempamerkan aplikasi TypeScript, pilih TypeScript apabila diminta, kemudian terima tetapan lalai.
Langkah seterusnya adalah menambah deps runtime kecil untuk kegunaan di luar Hardhat.
npm i @oasisprotocol/rofl-client ethers dotenv @types/node
npm i -D tsx
Templat TypeScript Hardhat secara automatik mencipta tsconfig.json. Kita perlu menambah skrip kecil supaya kod aplikasi boleh dikompilasi ke dist/.
// tsconfig.json
{
"compilerOptions": {
"rootDir": "./src",
"outDir": "./dist"
},
"include": ["src"]
}
Dalam bahagian ini, kami akan menambah beberapa fail TS kecil dan satu kontrak Solidity.
src/
├── appd.ts # pembungkus nipis atas @oasisprotocol/rofl-client
├── evm.ts # pembantu ethers (pembekal, dompet, tx, deploy)
├── keys.ts # pembantu kecil (checksum)
└── scripts/
├── deploy-contract.ts # skrip deploy generik untuk artifak yang dikompilasi
└── smoke-test.ts # demo hujung-ke-hujung (log)
contracts/
└── Counter.sol # kontrak sampel
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();
}
/**
* Menjana (atau menerbitkan semula secara deterministik) kunci secp256k1 di dalam ROFL dan
* mengembalikannya sebagai rentetan hex berprefiks 0x (untuk Wallet ethers.js).
*
* Pembangunan tempatan SAHAJA (di luar ROFL): Jika soket hilang dan anda menetapkan
* ALLOW_LOCAL_DEV=true dan LOCAL_DEV_SK=0x<64-hex>, nilai tersebut digunakan.
*/
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(
'soket rofl-appd tidak ditemui dan tiada LOCAL_DEV_SK disediakan (dev sahaja).'
);
}
2. src/evm.ts — pembantu 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("Transaksi digugurkan atau digantikan sebelum pengesahan");
}
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 deployment tidak dilombong");
}
return { address: contract.target as string, receipt };
}
3. src/keys.ts — pembantu kecil
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 — aliran hujung-ke-hujung tunggal
Ini adalah langkah penting kerana skrip ini mempunyai berbilang fungsi:
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(`Menunggu pembiayaan... baki semasa=${formatEther(bal)} ETH`);
await sleep(pollMs);
}
throw new Error("Tamat masa menunggu pembiayaan.");
}
async function main() {
const appId = await getAppId().catch(() => null);
console.log(`ROFL App ID: ${appId ?? "(tidak tersedia di luar ROFL)"}`);
const sk = await getEvmSecretKey(KEY_ID);
// NOTA: Demo ini mempercayai pembekal RPC yang dikonfigurasikan. Untuk pengeluaran, pilih
// klien ringan (contohnya, Helios) supaya anda boleh mengesahkan keadaan rantaian jauh.
const wallet = secretKeyToWallet(sk).connect(makeProvider(RPC_URL, CHAIN_ID));
const addr = checksumAddress(await wallet.getAddress());
console.log(`Alamat EVM (Base Sepolia): ${addr}`);
const msg = "hello from rofl";
const sig = await signPersonalMessage(wallet, msg);
console.log(`Mesej ditandatangani: "${msg}"`);
console.log(`Tandatangan: ${sig}`);
const provider = wallet.provider as JsonRpcProvider;
let bal = await provider.getBalance(addr);
if (bal === 0n) {
console.log("Sila biayai alamat di atas dengan Base Sepolia ETH untuk meneruskan.");
bal = await waitForFunding(provider, addr);
}
console.log(`Baki dikesan: ${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("Artifak counter kehilangan abi/bytecode");
}
const { address: contractAddress, receipt: deployRcpt } =
await deployContract(wallet, artifact.abi, artifact.bytecode, []);
console.log(`Deployed Counter di ${contractAddress} (tx=${deployRcpt.hash})`);
console.log("Ujian asap berjaya diselesaikan!");
}
main().catch((e) => {
console.error(e);
process.exit(1);
});
5. contracts/Counter.sol — sampel minimum
// 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 generik
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");
/**
* Penggunaan:
* npm run deploy-contract -- ./artifacts/MyContract.json '[arg0, arg1]'
* Artifak mesti mengandungi { abi, bytecode }.
*/
async function main() {
const [artifactPath, ctorJson = "[]"] = process.argv.slice(2);
if (!artifactPath) {
console.error("Penggunaan: 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("Artifak mesti mengandungi { abi, bytecode }");
}
let args: unknown[];
try {
args = JSON.parse(ctorJson);
if (!Array.isArray(args)) throw new Error("args constructor mesti array JSON");
} catch (e) {
throw new Error(`Gagal menghuraikan args constructor JSON: ${String(e)}`);
}
const sk = await getEvmSecretKey(KEY_ID);
// NOTA: Demo ini mempercayai pembekal RPC yang dikonfigurasikan. Untuk pengeluaran, pilih
// klien ringan (contohnya, Helios) supaya anda boleh mengesahkan keadaan rantaian jauh.
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);
});
Pada peringkat ini, kita memerlukan konfigurasi minimum untuk mengkompil 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;
Perkara yang perlu diingat ialah kompilasi tempatan adalah pilihan, jadi anda boleh melangkaunya jika mahu. Langkah seterusnya adalah pilihan — sama ada padam fail contracts/Lock.sol sedia ada atau anda boleh mengemas kininya kepada Solidity versi 0.8.24.
npx hardhat compile
Ini adalah langkah penting. Di sini, anda memerlukan Dockerfile yang membina TS dan mengkompil kontrak. Fail tersebut juga akan menjalankan ujian asap sekali, kemudian berdiri diam semasa anda memeriksa log.
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"]
Seterusnya, anda mesti melekap soket appd yang disediakan oleh ROFL. Tenang kerana tiada port awam didedahkan dalam proses ini.
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
Adalah penting untuk diingat bahawa ROFL hanya berjalan pada perkakasan yang didayakan Intel TDX. Jadi, jika anda mengkompil imej pada hos yang berbeza, seperti macOS, maka menghantar parameter — platform linux/amd64 adalah langkah tambahan yang penting.
docker buildx build --platform linux/amd64 \
-t docker.io/YOURUSER/rofl-keygen:0.1.0 --push .
Perkara menarik yang perlu diperhatikan di sini ialah anda boleh memilih keselamatan tambahan dan kebolehsahkan. Anda hanya perlu menyematkan digest dan menggunakan image: …@sha256:… dalam compose.yaml.
Terdapat langkah yang mesti anda ambil sebelum menjalankan arahan oasis rofl build. Memandangkan membina segmen imej datang selepas kontainerisasi, anda perlu mengemas kini services.demo.image dalam compose.yaml kepada imej yang anda bina.
Untuk projek TypeScript mudah, seperti yang ini, kadang-kadang terdapat kemungkinan bahawa saiz imej lebih besar daripada yang dijangkakan. Oleh itu, adalah dinasihatkan untuk mengemas kini bahagian rofl.yaml resources kepada sekurang-kurangnya: memory: 1024 dan storage.size: 4096.
Sekarang, anda sudah bersedia.
oasis rofl build
Anda boleh seterusnya menerbitkan identiti enclave dan konfigurasi.
oasis rofl update
Ini adalah langkah yang cukup mudah di mana anda men-deploy ke pembekal Testnet.
oasis rofl deploy
Ini adalah proses 2 langkah, walaupun langkah kedua adalah pilihan.
Pertama, anda melihat log ujian-asap.
oasis rofl machine logs
Jika anda telah menyelesaikan semua langkah sehingga sekarang dengan betul, anda akan melihat dalam output:
Seterusnya, dev tempatan. Di sini, anda perlu menjalankan npm run build:all untuk mengkompil kod TypeScript dan kontrak Solidity. Langkau langkah ini jika tidak diperlukan.
export ALLOW_LOCAL_DEV=true
export LOCAL_DEV_SK=0x<64-hex-dev-secret-key> # JANGAN GUNAKAN DALAM PROD
npm run smoke-test
Terdapat demo penjanaan kunci dalam GitHub Oasis, yang boleh anda rujuk sebagai contoh tutorial ini. https://github.com/oasisprotocol/demo-rofl-keygen
Sekarang setelah anda berjaya menjana kunci dalam ROFL dengan appd, menandatangani mesej, men-deploy kontrak, dan memindahkan ETH pada Base Sepolia, beritahu kami di bahagian komen maklum balas anda. Untuk berbual pantas dengan pasukan kejuruteraan Oasis untuk bantuan mengenai isu tertentu, anda boleh mengemukakan komen anda di saluran dev-central dalam Discord rasmi.
Asalnya diterbitkan di https://dev.to pada 20 Februari 2026.
Panduan Penjanaan Kunci Merentas Rantaian (EVM / Base) Dengan Oasis ROFL pada asalnya diterbitkan dalam Coinmonks di Medium, di mana orang ramai meneruskan perbualan dengan menyerlahkan dan membalas cerita ini.


财经
分享
分享这篇文章
复制链接X (Twitter)LinkedInFacebook电子邮件
迪拜迈出下一步,使房地产fl
