diff --git a/contracts/lending_pool/src/flash_loan.rs b/contracts/lending_pool/src/flash_loan.rs new file mode 100644 index 000000000..d65ab8be3 --- /dev/null +++ b/contracts/lending_pool/src/flash_loan.rs @@ -0,0 +1,124 @@ +//! # Flash-Loan Provider for Lending Pool (SC-031) +//! +//! Provides uncollateralized flash loans of pool assets to any borrower contract, +//! requiring full repayment plus a 0.09% (9 BPS) fee within the exact same +//! execution frame / transaction envelope. +//! Accrues fee revenue directly to the pool reserves. + +use soroban_sdk::{ + panic_with_error, symbol_short, token, Address, Env, IntoVal, Symbol, +}; +use crate::{LPError, BPS}; + +/// Protocol flash loan fee in basis points (0.09% = 9 bps) +pub const FLASH_LOAN_FEE_BPS: i128 = 9; + +/// Storage key for tracking active flash loan reentrancy +const FLASH_LOAN_LOCK: Symbol = symbol_short!("fl_lock"); + +/// Interface expected for flash-loan receivers +pub trait FlashLoanReceiver { + fn execute_operation( + env: Env, + caller: Address, + token: Address, + amount: i128, + fee: i128, + params: soroban_sdk::Bytes, + ); +} + +pub struct FlashLoan; + +impl FlashLoan { + /// Executes an uncollateralized flash loan: + /// 1. Verifies the amount is positive. + /// 2. Sets a transaction-scoped flash-loan lock in temporary storage. + /// 3. Records pre-loan pool balance of the token. + /// 4. Transfers `amount` to `receiver`. + /// 5. Invokes `receiver.execute_operation(...)`. + /// 6. Verifies that the pool balance is now >= pre-balance + fee (0.09%). + /// 7. Clears lock and records fee revenue. + pub fn execute_flash_loan( + env: &Env, + receiver: Address, + token: Address, + amount: i128, + params: soroban_sdk::Bytes, + ) -> i128 { + if amount <= 0 { + panic_with_error!(env, LPError::ZeroAmount); + } + + // Prevent nested flash loans in the same frame + if env.storage().temporary().has(&FLASH_LOAN_LOCK) { + panic_with_error!(env, LPError::ReentrancyGuardTriggered); + } + env.storage().temporary().set(&FLASH_LOAN_LOCK, &true); + + let token_client = token::Client::new(env, &token); + let contract_addr = env.current_contract_address(); + + let balance_before = token_client.balance(&contract_addr); + if balance_before < amount { + env.storage().temporary().remove(&FLASH_LOAN_LOCK); + panic_with_error!(env, LPError::InsufficientBal); + } + + // Fee = ceil(amount * 9 / 10,000) + let fee = match amount + .checked_mul(FLASH_LOAN_FEE_BPS) + .and_then(|prod| prod.checked_add(BPS - 1)) + .and_then(|num| num.checked_div(BPS)) + { + Some(f) => f.max(1), + None => { + env.storage().temporary().remove(&FLASH_LOAN_LOCK); + panic_with_error!(env, LPError::Overflow); + } + }; + + // Transfer funds to receiver + token_client.transfer(&contract_addr, &receiver, &amount); + + // Call borrower's contract hook + let receiver_args = ( + contract_addr.clone(), + token.clone(), + amount, + fee, + params, + ); + env.invoke_contract::<()>( + &receiver, + &Symbol::new(env, "execute_operation"), + receiver_args.into_val(env), + ); + + // Verification: ensure full repayment + fee + let balance_after = token_client.balance(&contract_addr); + let required_balance = match balance_before.checked_add(fee) { + Some(req) => req, + None => { + env.storage().temporary().remove(&FLASH_LOAN_LOCK); + panic_with_error!(env, LPError::Overflow); + } + }; + + if balance_after < required_balance { + env.storage().temporary().remove(&FLASH_LOAN_LOCK); + panic_with_error!(env, LPError::InsufficientBal); + } + + // Clear temporary lock + env.storage().temporary().remove(&FLASH_LOAN_LOCK); + + // Emit flash-loan event + env.events().publish( + (symbol_short!("flash"), token, receiver), + (amount, fee), + ); + + fee + } +} diff --git a/contracts/lending_pool/src/lib.rs b/contracts/lending_pool/src/lib.rs index da99bb431..81d1ddcfc 100644 --- a/contracts/lending_pool/src/lib.rs +++ b/contracts/lending_pool/src/lib.rs @@ -42,7 +42,9 @@ use soroban_sdk::{ Address, Env, IntoVal, Symbol, }; +pub mod flash_loan; pub mod math; +pub mod staking_rewards; // ── Constants ───────────────────────────────────────────────────────────────── @@ -476,6 +478,65 @@ impl LendingPool { env.events().publish((symbol_short!("lp_pause"),), paused); } + // ── Flash Loan (SC-031) ─────────────────────────────────────────────────── + + /// Executes an uncollateralized flash loan requiring 0.09% fee and full repayment. + pub fn flash_loan( + env: Env, + receiver: Address, + token: Address, + amount: i128, + params: soroban_sdk::Bytes, + ) -> i128 { + Self::check_not_paused(&env); + Self::check_supported(&env, &token); + flash_loan::FlashLoan::execute_flash_loan(&env, receiver, token, amount, params) + } + + // ── Continuous Staking Rewards (SC-032) ─────────────────────────────────── + + /// Initialize continuous yield farming rewards. + pub fn init_staking( + env: Env, + staking_token: Address, + reward_token: Address, + reward_rate: u128, + duration_seconds: u64, + ) { + Self::only_admin(&env); + staking_rewards::StakingRewards::initialize( + &env, + staking_token, + reward_token, + reward_rate, + duration_seconds, + ); + } + + /// Stake tokens into the yield farming pool. + pub fn stake(env: Env, user: Address, amount: u128) { + user.require_auth(); + Self::check_not_paused(&env); + staking_rewards::StakingRewards::stake(&env, user, amount); + } + + /// Unstake tokens from the yield farming pool. + pub fn unstake(env: Env, user: Address, amount: u128) { + user.require_auth(); + staking_rewards::StakingRewards::unstake(&env, user, amount); + } + + /// Claim accumulated yield farming rewards. + pub fn claim_reward(env: Env, user: Address) -> u128 { + user.require_auth(); + staking_rewards::StakingRewards::claim_reward(&env, user) + } + + /// View earned rewards for a user. + pub fn earned(env: Env, user: Address) -> u128 { + staking_rewards::StakingRewards::earned(&env, &user) + } + // ── Views ───────────────────────────────────────────────────────────────── pub fn collateral_of(env: Env, user: Address, token: Address) -> i128 { @@ -723,7 +784,7 @@ impl LendingPool { #[cfg(test)] mod tests { use super::*; - use soroban_sdk::{testutils::Address as _, Env}; + use soroban_sdk::{testutils::Address as _, testutils::Ledger as _, Env}; /// Minimal mock oracle: always returns SCALE for any token. #[contract] @@ -735,12 +796,20 @@ mod tests { } } - /// Minimal mock token: transfer is a no-op (avoids balance accounting). + /// Mock token tracking balances. #[contract] struct MockToken; #[contractimpl] impl MockToken { - pub fn transfer(_env: Env, _from: Address, _to: Address, _amount: i128) {} + pub fn transfer(env: Env, from: Address, to: Address, amount: i128) { + let from_bal: i128 = env.storage().instance().get(&from).unwrap_or(10_000_000_000); + let to_bal: i128 = env.storage().instance().get(&to).unwrap_or(10_000_000_000); + env.storage().instance().set(&from, &(from_bal - amount)); + env.storage().instance().set(&to, &(to_bal + amount)); + } + pub fn balance(env: Env, id: Address) -> i128 { + env.storage().instance().get(&id).unwrap_or(10_000_000_000) + } } fn setup(env: &Env) -> (LendingPoolClient<'static>, Address, Address) { @@ -859,6 +928,65 @@ mod tests { client.deposit_collateral(&user, &token, &1_000); } + #[contract] + struct MockReceiver; + #[contractimpl] + impl MockReceiver { + pub fn execute_operation( + env: Env, + caller: Address, + token: Address, + amount: i128, + fee: i128, + _params: soroban_sdk::Bytes, + ) { + token::Client::new(&env, &token).transfer(&env.current_contract_address(), &caller, &(amount + fee)); + } + } + + #[test] + fn flash_loan_executes_and_charges_fee() { + let env = Env::default(); + env.mock_all_auths(); + let (client, _, _) = setup(&env); + let token = add_token(&env, &client); + let receiver = env.register(MockReceiver, ()); + + let fee = client.flash_loan(&receiver, &token, &10_000, &soroban_sdk::Bytes::new(&env)); + // 10_000 * 9 / 10_000 = 9 + assert_eq!(fee, 9); + } + + #[test] + fn staking_pool_accumulates_rewards() { + let env = Env::default(); + env.mock_all_auths(); + let (client, admin, _) = setup(&env); + let staking_token = add_token(&env, &client); + let reward_token = add_token(&env, &client); + let user1 = Address::generate(&env); + + let reward_rate: u128 = 100_000_000_000_000_000; // 0.1 tokens/sec + client.init_staking(&staking_token, &reward_token, &reward_rate, &3600); + + client.stake(&user1, &100_000_000_000_000_000_000); // 100 tokens + assert_eq!(client.earned(&user1), 0); + + // Advance ledger timestamp by 10 seconds + let current_time = env.ledger().timestamp(); + env.ledger().set_timestamp(current_time + 10); + + let earned = client.earned(&user1); + // 10s * 0.1 = 1 token (1e18) + assert_eq!(earned, 1_000_000_000_000_000_000); + + let claimed = client.claim_reward(&user1); + assert_eq!(claimed, 1_000_000_000_000_000_000); + assert_eq!(client.earned(&user1), 0); + + client.unstake(&user1, &100_000_000_000_000_000_000); + } + use proptest::prelude::*; proptest! { diff --git a/contracts/lending_pool/src/staking_rewards.rs b/contracts/lending_pool/src/staking_rewards.rs new file mode 100644 index 000000000..60d9d6eed --- /dev/null +++ b/contracts/lending_pool/src/staking_rewards.rs @@ -0,0 +1,264 @@ +//! # Synthetix-Style Staking Rewards Pool (SC-032) +//! +//! Provides continuous yield farming with $O(1)$ constant time complexity for stake, +//! unstake, and reward claim operations. Uses a global `reward_per_token_stored` accumulator +//! and user reward debt snapshots to prevent iteration loops, eliminating out-of-gas risks. +//! High-precision 128-bit fixed-point math (`WAD = 10^18`) eliminates rounding dilution. + +use soroban_sdk::{ + contracttype, panic_with_error, symbol_short, token, Address, Env, +}; +use crate::math::{mul_div, Rounding, WAD}; +use crate::LPError; + +#[contracttype] +#[derive(Clone)] +pub enum StakingKey { + StakingToken, + RewardToken, + RewardRate, // Reward tokens per second (scaled to WAD) + PeriodFinish, // Timestamp when current reward distribution period ends + LastUpdateTime, // Last timestamp when reward_per_token was updated + RewardPerTokenStored, // Accumulated reward per token stored (scaled to WAD) + TotalSupply, // Total tokens currently staked + UserBalance(Address), // User staked balance + UserRewardDebt(Address),// User rewardPerToken snapshot + UserRewards(Address), // Unclaimed accumulated rewards for user +} + +pub struct StakingRewards; + +impl StakingRewards { + /// Initializes staking parameters. + pub fn initialize( + env: &Env, + staking_token: Address, + reward_token: Address, + reward_rate: u128, + duration_seconds: u64, + ) { + let now = env.ledger().timestamp(); + env.storage().instance().set(&StakingKey::StakingToken, &staking_token); + env.storage().instance().set(&StakingKey::RewardToken, &reward_token); + env.storage().instance().set(&StakingKey::RewardRate, &reward_rate); + env.storage().instance().set(&StakingKey::RewardPerTokenStored, &0u128); + env.storage().instance().set(&StakingKey::TotalSupply, &0u128); + env.storage().instance().set(&StakingKey::LastUpdateTime, &now); + env.storage().instance().set(&StakingKey::PeriodFinish, &(now + duration_seconds)); + } + + /// Computes the last timestamp applicable for rewards. + pub fn last_time_reward_applicable(env: &Env) -> u64 { + let now = env.ledger().timestamp(); + let finish = env + .storage() + .instance() + .get(&StakingKey::PeriodFinish) + .unwrap_or(0u64); + now.min(finish) + } + + /// Computes the current accumulated reward per staked token (WAD precision). + pub fn reward_per_token(env: &Env) -> u128 { + let total_supply: u128 = env + .storage() + .instance() + .get(&StakingKey::TotalSupply) + .unwrap_or(0u128); + + let stored_rpt: u128 = env + .storage() + .instance() + .get(&StakingKey::RewardPerTokenStored) + .unwrap_or(0u128); + + if total_supply == 0 { + return stored_rpt; + } + + let last_time = env + .storage() + .instance() + .get(&StakingKey::LastUpdateTime) + .unwrap_or(0u64); + let applicable_time = Self::last_time_reward_applicable(env); + + if applicable_time <= last_time { + return stored_rpt; + } + + let time_delta = (applicable_time - last_time) as u128; + let reward_rate: u128 = env + .storage() + .instance() + .get(&StakingKey::RewardRate) + .unwrap_or(0u128); + + // delta_rpt = (time_delta * reward_rate * 1e18) / total_supply + let reward_accrued = time_delta.saturating_mul(reward_rate); + let additional_rpt = mul_div(reward_accrued, WAD, total_supply, Rounding::Down); + + stored_rpt.saturating_add(additional_rpt) + } + + /// Computes the total rewards earned by `account`. + pub fn earned(env: &Env, account: &Address) -> u128 { + let balance: u128 = env + .storage() + .instance() + .get(&StakingKey::UserBalance(account.clone())) + .unwrap_or(0u128); + + let user_debt: u128 = env + .storage() + .instance() + .get(&StakingKey::UserRewardDebt(account.clone())) + .unwrap_or(0u128); + + let current_rpt = Self::reward_per_token(env); + let existing_rewards: u128 = env + .storage() + .instance() + .get(&StakingKey::UserRewards(account.clone())) + .unwrap_or(0u128); + + let rpt_diff = current_rpt.saturating_sub(user_debt); + let newly_earned = mul_div(balance, rpt_diff, WAD, Rounding::Down); + + existing_rewards.saturating_add(newly_earned) + } + + /// Internal hook called before any balance/state mutation to sync reward indices. + pub fn update_reward(env: &Env, account: Option<&Address>) { + let rpt = Self::reward_per_token(env); + env.storage().instance().set(&StakingKey::RewardPerTokenStored, &rpt); + env.storage().instance().set( + &StakingKey::LastUpdateTime, + &Self::last_time_reward_applicable(env), + ); + + if let Some(user) = account { + let total_earned = Self::earned(env, user); + env.storage().instance().set(&StakingKey::UserRewards(user.clone()), &total_earned); + env.storage().instance().set(&StakingKey::UserRewardDebt(user.clone()), &rpt); + } + } + + /// Stakes `amount` tokens into the yield farming pool. + pub fn stake(env: &Env, user: Address, amount: u128) { + if amount == 0 { + panic_with_error!(env, LPError::ZeroAmount); + } + + Self::update_reward(env, Some(&user)); + + let staking_token: Address = env + .storage() + .instance() + .get(&StakingKey::StakingToken) + .unwrap(); + + token::Client::new(env, &staking_token).transfer( + &user, + &env.current_contract_address(), + &(amount as i128), + ); + + let cur_supply: u128 = env + .storage() + .instance() + .get(&StakingKey::TotalSupply) + .unwrap_or(0u128); + let cur_bal: u128 = env + .storage() + .instance() + .get(&StakingKey::UserBalance(user.clone())) + .unwrap_or(0u128); + + env.storage() + .instance() + .set(&StakingKey::TotalSupply, &cur_supply.saturating_add(amount)); + env.storage() + .instance() + .set(&StakingKey::UserBalance(user.clone()), &cur_bal.saturating_add(amount)); + + env.events().publish((symbol_short!("staked"), user), amount as i128); + } + + /// Unstakes `amount` tokens from the pool. + pub fn unstake(env: &Env, user: Address, amount: u128) { + if amount == 0 { + panic_with_error!(env, LPError::ZeroAmount); + } + + let cur_bal: u128 = env + .storage() + .instance() + .get(&StakingKey::UserBalance(user.clone())) + .unwrap_or(0u128); + + if cur_bal < amount { + panic_with_error!(env, LPError::InsufficientBal); + } + + Self::update_reward(env, Some(&user)); + + let cur_supply: u128 = env + .storage() + .instance() + .get(&StakingKey::TotalSupply) + .unwrap_or(0u128); + + env.storage() + .instance() + .set(&StakingKey::TotalSupply, &cur_supply.saturating_sub(amount)); + env.storage() + .instance() + .set(&StakingKey::UserBalance(user.clone()), &cur_bal.saturating_sub(amount)); + + let staking_token: Address = env + .storage() + .instance() + .get(&StakingKey::StakingToken) + .unwrap(); + + token::Client::new(env, &staking_token).transfer( + &env.current_contract_address(), + &user, + &(amount as i128), + ); + + env.events().publish((symbol_short!("unstake"), user), amount as i128); + } + + /// Claims all accumulated reward tokens. + pub fn claim_reward(env: &Env, user: Address) -> u128 { + Self::update_reward(env, Some(&user)); + + let reward: u128 = env + .storage() + .instance() + .get(&StakingKey::UserRewards(user.clone())) + .unwrap_or(0u128); + + if reward > 0 { + env.storage().instance().set(&StakingKey::UserRewards(user.clone()), &0u128); + + let reward_token: Address = env + .storage() + .instance() + .get(&StakingKey::RewardToken) + .unwrap(); + + token::Client::new(env, &reward_token).transfer( + &env.current_contract_address(), + &user, + &(reward as i128), + ); + + env.events().publish((symbol_short!("reward"), user), reward as i128); + } + + reward + } +} diff --git a/frontend/src/app/wasm-analyzer/__tests__/page.test.tsx b/frontend/src/app/wasm-analyzer/__tests__/page.test.tsx new file mode 100644 index 000000000..d1be26640 --- /dev/null +++ b/frontend/src/app/wasm-analyzer/__tests__/page.test.tsx @@ -0,0 +1,92 @@ +import React from 'react'; +import { render, screen, fireEvent, waitFor } from '@testing-library/react'; +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import WasmAnalyzerPage from '../page'; + +// Minimal valid WASM binary: "\0asm" version 1 with export "test_fn" +const createValidWasm = (): Uint8Array => { + return new Uint8Array([ + 0x00, 0x61, 0x73, 0x6d, // magic: \0asm + 0x01, 0x00, 0x00, 0x00, // version: 1 + // Type section (id 1, size 4, 1 type: () -> ()) + 0x01, 0x04, 0x01, 0x60, 0x00, 0x00, + // Function section (id 3, size 2, 1 function with type 0) + 0x03, 0x02, 0x01, 0x00, + // Memory section (id 5, size 3, 1 memory min 1, no max) + 0x05, 0x03, 0x01, 0x00, 0x01, + // Export section (id 7, size 11, export "test_fn" func 0) + 0x07, 0x0b, 0x01, 0x07, 0x74, 0x65, 0x73, 0x74, 0x5f, 0x66, 0x6e, 0x00, 0x00, + // Code section (id 10, size 4, 1 code body) + 0x0a, 0x04, 0x01, 0x02, 0x00, 0x0b, + ]); +}; + +describe('WasmAnalyzerPage Component', () => { + beforeEach(() => { + vi.clearAllMocks(); + if (typeof File !== 'undefined' && !File.prototype.arrayBuffer) { + File.prototype.arrayBuffer = function () { + return new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.onload = () => resolve(reader.result as ArrayBuffer); + reader.onerror = () => reject(reader.error); + reader.readAsArrayBuffer(this); + }); + }; + } + }); + + it('renders the WASM analyzer header and file drop zone', () => { + render(); + expect(screen.getByRole('heading', { level: 1, name: /WASM Analyzer/i })).toBeInTheDocument(); + expect(screen.getByText(/Drop \.wasm contract binary here or click to browse/i)).toBeInTheDocument(); + }); + + it('handles dropping a valid wasm file and displays digest, sections, and exports', async () => { + render(); + + const wasmBytes = createValidWasm(); + const file = new File([wasmBytes], 'contract.wasm', { type: 'application/wasm' }); + + const dropZone = screen.getByText(/Drop \.wasm contract binary here or click to browse/i); + + fireEvent.drop(dropZone, { + dataTransfer: { + files: [file], + }, + }); + + await waitFor(() => { + expect(screen.getByText(/File: contract\.wasm/i)).toBeInTheDocument(); + }); + + // Displays Bytecode Digest (SHA-256) + await waitFor(() => { + expect(screen.getByText(/Bytecode Digest \(SHA-256\)/i)).toBeInTheDocument(); + expect(screen.getByText(/Section headers/i)).toBeInTheDocument(); + expect(screen.getByText(/Exported functions & symbols/i)).toBeInTheDocument(); + expect(screen.getByText(/test_fn/i)).toBeInTheDocument(); + expect(screen.getByText(/Linear memory limits/i)).toBeInTheDocument(); + }); + }); + + it('shows error banner when dropping an invalid file', async () => { + render(); + + const invalidBytes = new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8]); + const file = new File([invalidBytes], 'bad.wasm', { type: 'application/wasm' }); + + const dropZone = screen.getByText(/Drop \.wasm contract binary here or click to browse/i); + + fireEvent.drop(dropZone, { + dataTransfer: { + files: [file], + }, + }); + + await waitFor(() => { + expect(screen.getByText(/Cannot parse module/i)).toBeInTheDocument(); + expect(screen.getByText(/missing the \\0asm magic number/i)).toBeInTheDocument(); + }); + }); +}); diff --git a/frontend/src/app/wasm-analyzer/page.tsx b/frontend/src/app/wasm-analyzer/page.tsx index ff16d221a..9d9680456 100644 --- a/frontend/src/app/wasm-analyzer/page.tsx +++ b/frontend/src/app/wasm-analyzer/page.tsx @@ -24,6 +24,8 @@ import type { WatResponse } from '@/workers/wat.worker'; export default function WasmAnalyzerPage() { const [info, setInfo] = useState(null); const [fileName, setFileName] = useState(''); + const [sha256, setSha256] = useState(''); + const [isDragging, setIsDragging] = useState(false); const [wat, setWat] = useState(''); const [watError, setWatError] = useState(null); const [disassembling, setDisassembling] = useState(false); @@ -40,11 +42,26 @@ export default function WasmAnalyzerPage() { }; }, []); + const computeSha256 = async (bytes: ArrayBuffer): Promise => { + try { + const hashBuffer = await crypto.subtle.digest('SHA-256', bytes); + const hashArray = Array.from(new Uint8Array(hashBuffer)); + return hashArray.map((b) => b.toString(16).padStart(2, '0')).join(''); + } catch { + return ''; + } + }; + const disassemble = useCallback((bytes: ArrayBuffer) => { setDisassembling(true); setWat(''); setWatError(null); + if (typeof Worker === 'undefined') { + setDisassembling(false); + return; + } + if (!workerRef.current) { workerRef.current = new Worker(new URL('../../workers/wat.worker.ts', import.meta.url)); } @@ -67,10 +84,27 @@ export default function WasmAnalyzerPage() { worker.postMessage({ id, bytes }, [bytes]); }, []); + const readFileBuffer = async (file: File): Promise => { + if (typeof file.arrayBuffer === 'function') { + return await file.arrayBuffer(); + } + return new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.onload = () => { + if (reader.result instanceof ArrayBuffer) resolve(reader.result); + else reject(new Error('Failed to read file as ArrayBuffer')); + }; + reader.onerror = () => reject(reader.error); + reader.readAsArrayBuffer(file); + }); + }; + const onFile = useCallback( async (file: File) => { setFileName(file.name); - const buffer = await file.arrayBuffer(); + const buffer = await readFileBuffer(file); + const hash = await computeSha256(buffer.slice(0)); + setSha256(hash); // Analyse before transferring the buffer to the worker, which neuters it. setInfo(analyzeWasm(new Uint8Array(buffer.slice(0)))); @@ -109,24 +143,46 @@ export default function WasmAnalyzerPage() {

- {/* File input */} - + {/* File drag-and-drop input zone */} +
{ + e.preventDefault(); + setIsDragging(true); + }} + onDragLeave={(e) => { + e.preventDefault(); + setIsDragging(false); + }} + onDrop={(e) => { + e.preventDefault(); + setIsDragging(false); + const file = e.dataTransfer.files?.[0]; + if (file) void onFile(file); + }} + className={`mb-8 flex flex-col items-center justify-center rounded-2xl border-2 border-dashed p-10 text-center transition ${ + isDragging + ? 'border-red-500 bg-red-950/20' + : 'border-zinc-700 bg-zinc-950 hover:border-red-500/50' + }`} + > + +
{info && !info.valid && (
@@ -138,10 +194,11 @@ export default function WasmAnalyzerPage() { {info?.valid && (
{/* Summary */} -
+

Size

{formatBytes(info.totalBytes)}

+

{info.totalBytes.toLocaleString()} B

Sections

@@ -155,8 +212,28 @@ export default function WasmAnalyzerPage() {

Exports

{info.exports.length}

+
+

SHA-256 Hash

+

+ {sha256 ? `${sha256.slice(0, 16)}…` : 'Calculating…'} +

+
+ {/* SHA-256 Hash Banner */} + {sha256 && ( +
+
+ + Bytecode Digest (SHA-256) + + + {sha256} + +
+
+ )} + {/* Sections */}

@@ -180,6 +257,60 @@ export default function WasmAnalyzerPage() {

+ {/* Exported symbols & functions */} + {info.exports.length > 0 && ( +
+

+ Exported functions & symbols ({info.exports.length}) +

+
+ {info.exports.map((exp) => ( +
+ {exp.name} + + {exp.kindName} + +
+ ))} +
+
+ )} + + {/* Memory limits & sections */} + {info.memories.length > 0 && ( +
+

+ Linear memory limits ({info.memories.length}) +

+
+ {info.memories.map((mem, idx) => ( +
+ Memory #{idx} +
+ + Min Pages: {mem.min} ( + {formatBytes(mem.min * 65536)}) + + + Max Pages:{' '} + + {mem.max !== undefined ? mem.max : 'unlimited'} + {' '} + {mem.max !== undefined && `(${formatBytes(mem.max * 65536)})`} + +
+
+ ))} +
+
+ )} + {/* Host functions */} {info.hostFunctions.length > 0 && (
diff --git a/frontend/src/components/playground/CallPanel.tsx b/frontend/src/components/playground/CallPanel.tsx new file mode 100644 index 000000000..7a780d8f8 --- /dev/null +++ b/frontend/src/components/playground/CallPanel.tsx @@ -0,0 +1,142 @@ +'use client'; + +import React, { useState } from 'react'; +import { Play, CheckCircle2, AlertCircle, RefreshCw } from 'lucide-react'; + +export interface CallArg { + name: string; + type: string; + value: string; +} + +export interface CallPanelProps { + contractId?: string; + functions?: Array<{ name: string; args: Array<{ name: string; type: string }> }>; + onInvoke?: (fnName: string, args: Record) => Promise; +} + +export function CallPanel({ + contractId = '', + functions = [ + { name: 'hello', args: [{ name: 'to', type: 'Symbol' }] }, + { name: 'increment', args: [] }, + ], + onInvoke, +}: CallPanelProps) { + const [selectedFn, setSelectedFn] = useState(functions[0]?.name || ''); + const [argValues, setArgValues] = useState>({}); + const [result, setResult] = useState(null); + const [error, setError] = useState(null); + const [invoking, setInvoking] = useState(false); + + const currentFn = functions.find((f) => f.name === selectedFn); + + const handleInvoke = async () => { + if (!selectedFn) return; + setInvoking(true); + setError(null); + setResult(null); + + try { + if (onInvoke) { + const res = await onInvoke(selectedFn, argValues); + setResult(typeof res === 'object' ? JSON.stringify(res, null, 2) : String(res)); + } else { + await new Promise((r) => setTimeout(r, 100)); + setResult(`Success: ${selectedFn} invoked`); + } + } catch (err: any) { + setError(err?.message || 'Invocation failed'); + } finally { + setInvoking(false); + } + }; + + return ( +
+
+

+ Invoke Contract Function +

+ {contractId && ( + + {contractId} + + )} +
+ +
+
+ + +
+ + {currentFn && currentFn.args.length > 0 && ( +
+ + {currentFn.args.map((arg) => ( +
+ + {arg.name} ({arg.type}) + + + setArgValues((prev) => ({ ...prev, [arg.name]: e.target.value })) + } + className="flex-1 rounded border border-zinc-700 bg-black px-3 py-1.5 text-xs text-white" + /> +
+ ))} +
+ )} + + + + {result && ( +
+
+ Return Value +
+
{result}
+
+ )} + + {error && ( +
+
+ Invocation Error +
+

{error}

+
+ )} +
+
+ ); +} +export default CallPanel; diff --git a/frontend/src/components/playground/DeployPanel.tsx b/frontend/src/components/playground/DeployPanel.tsx index c1aa92359..d39f38d0a 100644 --- a/frontend/src/components/playground/DeployPanel.tsx +++ b/frontend/src/components/playground/DeployPanel.tsx @@ -1 +1,748 @@ -aW1wb3J0IFJlYWN0LCB7IHVzZUNhbGxiYWNrLCB1c2VFZmZlY3QsIHVzZU1lbW8sIHVzZVJlZHVjZXIsIHVzZVJlZiwgdXNlU3RhdGUgfSBmcm9tICdyZWFjdCc7CmltcG9ydCB7IENoZWNrLCBDaGVja0NpcmNsZSwgQ2xpcGJvYXJkQ29weSwgRXh0ZXJuYWxMaW5rLCBMb2FkZXIyLCBQbGF5LCBSZWZyZXNoQ3csIFRlcm1pbmFsLCBXYWxydXMsIFhDaXJjbGUgfSBmcm9tICdsdWNpZGUtcmVhY3QnOwoKLy8gLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tCi8vIFR5cGVzCi8vIC0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLQoKZXhwb3J0IHR5cGUgRGVwbG95U3RhZ2UgPSAndXBsb2FkJyB8ICdpbnN0YWxsJyB8ICdpbnN0YW50aWF0ZScgfCAnaW5pdGlhbGl6ZSc7CgpleHBvcnQgdHlwZSBTdGFnZVN0YXR1cyA9ICdwZW5kaW5nJyB8ICdhY3RpdmUnIHwgJ3N1Y2Nlc3MnIHwgJ2Vycm9yJzsKCmV4cG9ydCBpbnRlcmZhY2UgU3RhZ2VTdGF0ZSB7CiAgc3RhZ2U6IERlcGxveVN0YWdlOwogIHN0YXR1czogU3RhZ2VTdGF0dXM7CiAgbGFiZWw6IHN0cmluZzsKICBkZXNjcmlwdGlvbjogc3RyaW5nOwogIG1lc3NhZ2U/OiBzdHJpbmc7CiAgaGFzaD86IHN0cmluZzsKICBzdGFydGVkQXQ/OiBudW1iZXI7CiAgZmluaXNoZWRBdD86IG51bWJlcjsKfQoKZXhwb3J0IGludGVyZmFjZSBDb25zdHJ1Y3RvckFyZ3VtZW50IHsKICBuYW1lOiBzdHJpbmc7CiAgdHlwZTogJ3N0cmluZycgfCAnbnVtYmVyJyB8ICdib29sZWFuJyB8ICdhZGRyZXNzJyB8ICdzeW1ib2wnIHwgJ2J5dGVzJzsKICB2YWx1ZTogc3RyaW5nOwogIHJlcXVpcmVkPzogYm9vbGVhbjsKICBkZXNjcmlwdGlvbj86IHN0cmluZzsKfQoKZXhwb3J0IGludGVyZmFjZSBEZXBsb3lSZXN1bHQgewogIGNvbnRyYWN0SWQ6IHN0cmluZzsKICB3YXNtSGFzaDogc3RyaW5nOwogIGluc3RhbGxUeEhhc2g6IHN0cmluZzsKICBpbnN0YW50aWF0ZVR4SGFzaDogc3RyaW5nOwogIGluaXRpYWxpemVUeEhhc2g/OiBzdHJpbmc7CiAgZXhwbG9yZXJVcmw6IHN0cmluZzsKICB2ZXJpZmllZDogYm9vbGVhbjsKfQoKZXhwb3J0IGludGVyZmFjZSBEZXBsb3lQYW5lbFByb3BzIHsKICAvKiogV0FTTSBieXRlcyB0byBkZXBsb3kgKGZyb20gYnVpbGQgb3IgdXBsb2FkKS4gKi8KICB3YXNtQnl0ZXM/OiBVaW50OEFycmF5IHwgQXJyYXlCdWZmZXIgfCBudWxsOwogIC8qKiBOYW1lIG9mIHRoZSBjb250cmFjdCBmb3IgZGlzcGxheS9sb2dnaW5nLiAqLwogIGNvbnRyYWN0TmFtZT86IHN0cmluZzsKICAvKiogU3RlbGxhciBuZXR3b3JrIHRvIGRlcGxveSB0by4gKi8KICBuZXR3b3JrPzogJ3Rlc3RuZXQnIHwgJ2Z1dHVyZW5ldCcgfCAnbWFpbm5ldCc7CiAgLyoqIFNvcm9iYW4gUlBDIFVSTCB0byB1c2UuICovCiAgcnBjVXJsPzogc3RyaW5nOwogIC8qKiBOZXR3b3JrIHBhc3NwaHJhc2UgZm9yIHRyYW5zYWN0aW9uIHNpZ25pbmcuICovCiAgbmV0d29ya1Bhc3NwaHJhc2U/OiBzdHJpbmc7CiAgLyoqIFNlY3JldCBrZXkgb2YgdGhlIGRlcGxveWVyIGFjY291bnQgKFN0ZWxsYXIgUy4uLikgKi8KICBzZWNyZXRLZXk/OiBzdHJpbmc7CiAgLyoqIFB1YmxpYyBrZXkgb2YgdGhlIGRlcGxveWVyIGFjY291bnQgKEcuLi4pICovCiAgcHVibGljS2V5Pzogc3RyaW5nOwogIC8qKiBDb25zdHJ1Y3RvciBhcmd1bWVudHMgdG8gcGFzcyB0byB0aGUgY29udHJhY3QuICovCiAgY29uc3RydWN0b3JBcmdzPzogQ29uc3RydWN0b3JBcmd1bWVudFtdOwogIC8qKiBPcHRpb25hbCBpbml0aWFsaXphdGlvbiBhcmd1bWVudHMgKGZvciB0aGUgaW5pdGlhbGl6ZSBjYWxsKS4gKi8KICBpbml0aWFsaXplQXJncz86IENvbnN0cnVjdG9yQXJndW1lbnRbXTsKICAvKiogQ2FsbGVkIHdoZW4gZGVwbG95bWVudCBjb21wbGV0ZXMgc3VjY2Vzc2Z1bGx5LiAqLwogIG9uRGVwbG95ZWQ/OiAocmVzdWx0OiBEZXBsb3lSZXN1bHQpID0+IHZvaWQ7CiAgLyoqIENhbGxlZCBvbiBhbnkgZXJyb3IuICovCiAgb25FcnJvcj86IChlcnJvcjogRXJyb3IpID0+IHZvaWQ7CiAgLyoqIE9wdGlvbmFsIGV4dGVybmFsIGRlcGxveSBmdW5jdGlvbiAoZm9yIHRlc3RpbmcvaW5qZWN0aW9uKS4gKi8KICBkZXBsb3lPdmVycmlkZT86IChpbnB1dDogRGVwbG95SW5wdXQpID0+IFByb21pc2U8RGVwbG95UmVzdWx0PjsKfQoKZXhwb3J0IGludGVyZmFjZSBEZXBsb3lJbnB1dCB7CiAgd2FzbUJ5dGVzOiBVaW50OEFycmF5OwogIGNvbnRyYWN0TmFtZTogc3RyaW5nOwogIG5ldHdvcms6ICd0ZXN0bmV0JyB8ICdmdXR1cmVuZXQnIHwgJ21haW5uZXQnOwogIHJwY1VybDogc3RyaW5nOwogIG5ldHdvcmtQYXNzcGhyYXNlOiBzdHJpbmc7CiAgc2VjcmV0S2V5OiBzdHJpbmc7CiAgcHVibGljS2V5OiBzdHJpbmc7CiAgY29uc3RydWN0b3JBcmdzOiBDb25zdHJ1Y3RvckFyZ3VtZW50W107CiAgaW5pdGlhbGl6ZUFyZ3M6IENvbnN0cnVjdG9yQXJndW1lbnRbXTsKICBvblN0YWdlVXBkYXRlOiAoc3RhZ2U6IERlcGxveVN0YWdlLCB1cGRhdGU6IFBhcnRpYWw8U3RhZ2VTdGF0ZT4pID0+IHZvaWQ7Cn0KCi8vIC0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLQovLyBOZXR3b3JrIGNvbmZpZ3VyYXRpb24KLy8gLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tCgpjb25zdCBORVRXT1JLX0NPTkZJR1M6IFJlY29yZDxzdHJpbmcsIHsgcnBjVXJsOiBzdHJpbmc7IHBhc3NwaHJhc2U6IHN0cmluZzsgZXhwbG9yZXI6IHN0cmluZyB9PiA9IHsKICB0ZXN0bmV0OiB7CiAgICBycGNVcmw6ICdodHRwczovL3Nvcm9iYW4tdGVzdG5ldC5zdGVsbGFyLm9yZycsCiAgICBwYXNzcGhyYXNlOiAnVGVzdCBTdGVsbGFyIE5ldHdvcmsgOyBTZXB0ZW1iZXIgMjAxNScsCiAgICBleHBsb3JlcjogJ2h0dHBzOi8vc3RlbGxhci5leHBlcnQvZXhwbG9yZXIvdGVzdG5ldCcsCiAgfSwKICBmdXR1cmVuZXQ6IHsKICAgIHJwY1VybDogJ2h0dHBzOi8vc29yb2Jhbi1mdXR1cmVuZXQuc3RlbGxhci5vcmcnLAogICAgcGFzc3BocmFzZTogJ1Rlc3QgU3RlbGxhciBOZXR3b3JrIDsgU2VwdGVtYmVyIDIwMTUnLAogICAgZXhwbG9yZXI6ICdodHRwczovL3N0ZWxsYXIuZXhwZXJ0L2V4cGxvcmVyL2Z1dHVyZW5ldCcsCiAgfSwKICBtYWlubmV0OiB7CiAgICBycGNVcmw6ICdodHRwczovL3Nvcm9iYW4tbWFpbm5ldC5zdGVsbGFyLm9yZycsCiAgICBwYXNzcGhyYXNlOiAnUHVibGljIEdsb2JhbCBTdGVsbGFyIE5ldHdvcmsgOyBTZXB0ZW1iZXIgMjAxNScsCiAgICBleHBsb3JlcjogJ2h0dHBzOi8vc3RlbGxhci5leHBlcnQvZXhwbG9yZXIvcHVibGljJywKICB9LAp9OwoKY29uc3QgU1RBR0VfREVGSU5JVElPTlM6IEFycmF5PHsgc3RhZ2U6IERlcGxveVN0YWdlOyBsYWJlbDogc3RyaW5nOyBkZXNjcmlwdGlvbjogc3RyaW5nIH0+ID0gWwogIHsKICAgIHN0YWdlOiAndXBsb2FkJywKICAgIGxhYmVsOiAnVXBsb2FkIFdBU00nLAogICAgZGVzY3JpcHRpb246ICdVcGxvYWQgY29tcGlsZWQgV0FTTSBieXRlY29kZSB0byB0aGUgbmV0d29yaycsCiAgfSwKICB7CiAgICBzdGFnZTogJ2luc3RhbGwnLAogICAgbGFiZWw6ICdJbnN0YWxsIENvbnRyYWN0JywKICAgIGRlc2NyaXB0aW9uOiAnSW5zdGFsbCB0aGUgV0FTTSBhbmQgY3JlYXRlIGEgY29udHJhY3QgaW5zdGFuY2UnLAogIH0sCiAgewogICAgc3RhZ2U6ICdpbnN0YW50aWF0ZScsCiAgICBsYWJlbDogJ0luc3RhbnRpYXRlJywKICAgIGRlc2NyaXB0aW9uOiAnQ3JlYXRlIHRoZSBjb250cmFjdCBpbnN0YW5jZSB3aXRoIGNvbnN0cnVjdG9yIGFyZ3VtZW50cycsCiAgfSwKICB7CiAgICBzdGFnZTogJ2luaXRpYWxpemUnLAogICAgbGFiZWw6ICdJbml0aWFsaXplIFN0YXRlJywKICAgIGRlc2NyaXB0aW9uOiAnSW52b2tlIHRoZSBjb250cmFjdCBpbml0aWFsaXplciBhbmQgdmVyaWZ5IG9uIFN0ZWxsYXIgRXhwZXJ0JywKICB9LApdOwoKLy8gLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tCi8vIFNvcm9iYW4gU0RLIGxhenkgbG9hZGVyCi8vIC0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLQoKdHlwZSBTb3JvYmFuU0RLID0gewogIENvbnRyYWN0OiBhbnk7CiAgQWRkcmVzczogYW55OwogIEtleXBhaXI6IGFueTsKICBUcmFuc2FjdGlvbkJ1aWxkZXI6IGFueTsKICBSZWFkZXI6IGFueTsKICB4ZHI6IGFueTsKICBBc3NldDogYW55OwogIE5hdGl2ZUFzc2V0OiBhbnk7CiAgT3BlcmF0aW9uOiBhbnk7CiAgU2N2YWw6IGFueTsKfTsKCmFzeW5jIGZ1bmN0aW9uIGxvYWRTb3JvYmFuU0RLKCk6IFByb21pc2U8U29yb2JhblNESz4gewogIGNvbnN0IG1vZHVsZXMgPSBhd2FpdCBQcm9taXNlLmFsbChbCiAgICBpbXBvcnQoLyogd2VicGFja0NodW5rTmFtZTogInNvcm9iYW4tY2xpZW50IiAqLyAnQHN0ZWxsYXItc3RlbGxhci1zZGsvY2xpZW50JyksCiAgICBpbXBvcnQoLyogd2VicGFja0NodW5rTmFtZTogInNvcm9iYW4tYmFzZSIgKi8gJ0BzdGVsbGFyLXN0ZWxsYXItc2RrL2Jhc2UnKSwKICAgIGltcG9ydCgvKiB3ZWJwYWNrQ2h1bmtOYW1lOiAic29yb2Jhbi1iYXNlIiAqLyAnQHN0ZWxsYXItc3RlbGxhci1zZGsvYmFzZScpLAogIF0pOwogIGNvbnN0IFtjbGllbnRNb2QsIGJhc2VNb2RdID0gbW9kdWxlczsKICByZXR1cm4gewogICAgQ29udHJhY3Q6IGNsaWVudE1vZC5Db250cmFjdCwKICAgIEFkZHJlc3M6IGJhc2VNb2QuQWRkcmVzcywKICAgIEtleXBhaXI6IGJhc2VNb2QuS2V5cGFpciwKICAgIFRyYW5zYWN0aW9uQnVpbGRlcjogYmFzZU1vZC5UcmFuc2FjdGlvbkJ1aWxkZXIsCiAgICBSZWFkZXI6IGJhc2VNb2QuUmVhZGVyLAogICAgeGRyOiBiYXNlTW9kLnhkciwKICAgIEFzc2V0OiBiYXNlTW9kLkFzc2V0LAogICAgTmF0aXZlQXNzZXQ6IGJhc2VNb2QuTmF0aXZlQXNzZXQsCiAgICBPcGVyYXRpb246IGJhc2VNb2QuT3BlcmF0aW9uLAogICAgU2N2YWw6IGJhc2VNb2QuU2N2YWwsCiAgfTsKfQoKLy8gLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tCi8vIEhlbHBlcnMKLy8gLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tCgpmdW5jdGlvbiB0b1VpbnQ4QXJyYXkoaW5wdXQ6IFVpbnQ4QXJyYXkgfCBBcnJheUJ1ZmZlciB8IG51bGwgfCB1bmRlZmluZWQpOiBVaW50OEFycmF5IHsKICBpZiAoIWlucHV0KSByZXR1cm4gbmV3IFVpbnQ4QXJyYXkoMCk7CiAgaWYgKGlucHV0IGluc3RhbmNlb2YgVWludDhBcnJheSkgcmV0dXJuIGlucHV0OwogIHJldHVybiBuZXcgVWludDhBcnJheShpbnB1dCk7Cn0KCmZ1bmN0aW9uIGJ5dGVzVG9IZXgoYnl0ZXM6IFVpbnQ4QXJyYXkpOiBzdHJpbmcgewogIHJldHVybiBBcnJheS5mcm9tKGJ5dGVzKQogICAgLm1hcCgoYikgPT4gYi50b1N0cmluZygxNikucGFkU3RhcnQoMiwgJzAnKSkKICAgIC5qb2luKCcnKTsKfQoKZnVuY3Rpb24gY29udmVydEFyZ3VtZW50VmFsdWUoYXJnOiBDb25zdHJ1Y3RvckFyZ3VtZW50LCBzZGs6IFNvcm9iYW5TREspOiBhbnkgewogIGNvbnN0IHJhdyA9IGFyZy52YWx1ZTsKICBzd2l0Y2ggKGFyZy50eXBlKSB7CiAgICBjYXNlICdzdHJpbmcnOgogICAgICByZXR1cm4gcmF3OwogICAgY2FzZSAnc3ltYm9sJzoKICAgICAgcmV0dXJuIHNkay54ZHIuU2N2YWwuc3ltYm9sKHJhdyk7CiAgICBjYXNlICdudW1iZXInOiB7CiAgICAgIGNvbnN0IG51bSA9IE51bWJlcihyYXcpOwogICAgICBpZiAoTnVtYmVyLmlzTmFOKG51bSkpIHRocm93IG5ldyBFcnJvcihgSW52YWxpZCBudW1iZXIgZm9yIGFyZ3VtZW50ICIke2FyZy5uYW1lfSI6ICR7cmF3fWApOwogICAgICByZXR1cm4gQmlnSW50KG51bSk7CiAgICB9CiAgICBjYXNlICdib29sZWFuJzoKICAgICAgcmV0dXJuIHJhdyA9PT0gJ3RydWUnIHx8IHJhdyA9PT0gJzEnOwogICAgY2FzZSAnYWRkcmVzcyc6CiAgICAgIHJldHVybiBuZXcgc2RrLkFkZHJlc3MocmF3KS50b1NjcmlwdGlvbigpOwogICAgY2FzZSAnYnl0ZXMnOgogICAgICByZXR1cm4gQnVmZmVyLmZyb20ocmF3LCAnaGV4Jyk7CiAgICBkZWZhdWx0OgogICAgICByZXR1cm4gcmF3OwogIH0KfQoKZnVuY3Rpb24gYnVpbGRFeHBsb3JlclVybChuZXR3b3JrOiBzdHJpbmcsIGNvbnRyYWN0SWQ6IHN0cmluZyk6IHN0cmluZyB7CiAgY29uc3QgY29uZmlnID0gTkVUV09SS19DT05GSUdTW25ldHdvcmtdID8/IE5FVFdPUktfQ09ORklHUy50ZXN0bmV0OwogIHJldHVybiBgJHtjb25maWcuZXhwbG9yZXJ9L2NvbnRyYWN0LyR7Y29udHJhY3RJZH1gOwp9Cgphc3luYyBmdW5jdGlvbiB2ZXJpZnlPbkV4cGxvcmVyKHVybDogc3RyaW5nLCB0aW1lb3V0TXMgPSAxNTAwMCk6IFByb21pc2U8Ym9vbGVhbj4gewogIGNvbnN0IGRlYWRsaW5lID0gRGF0ZS5ub3coKSArIHRpbWVvdXRNczsKICB3aGlsZSAoRGF0ZS5ub3coKSA8IGRlYWRsaW5lKSB7CiAgICB0cnkgewogICAgICBjb25zdCByZXMgPSBhd2FpdCBmZXRjaCh1cmwsIHsgbWV0aG9kOiAnR0VUJywgaGVhZGVyczogeyBBY2NlcHQ6ICd0ZXh0L2h0bWwnIH0gfSk7CiAgICAgIGlmIChyZXMub2spIHsKICAgICAgICBjb25zdCB0ZXh0ID0gYXdhaXQgcmVzLnRleHQoKTsKICAgICAgICBpZiAoIS9ub3QgZm91bmR8NDA0L2kudGVzdCh0ZXh0KSkgewogICAgICAgICAgcmV0dXJuIHRydWU7CiAgICAgICAgfQogICAgICB9CiAgICB9IGNhdGNoIHsKICAgICAgLy8gZXhwbG9yZXIgbWF5IG5vdCBiZSByZWFjaGFibGUgZnJvbSB0aGUgYnJvd3Nlcjsga2VlcCBwb2xsaW5nCiAgICB9CiAgICBhd2FpdCBuZXcgUHJvbWlzZSgocikgPT4gc2V0VGltZW91dChyLCAxNTAwKSk7CiAgfQogIHJldHVybiBmYWxzZTsKfQoKLy8gLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tCi8vIFJlYWwgZGVwbG95IHBpcGVsaW5lCi8vIC0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLQoKYXN5bmMgZnVuY3Rpb24gcnVuRGVwbG95UGlwZWxpbmUoaW5wdXQ6IERlcGxveUlucHV0KTogUHJvbWlzZTxEZXBsb3lSZXN1bHQ+IHsKICBjb25zdCB7CiAgICB3YXNtQnl0ZXMsCiAgICBuZXR3b3JrLAogICAgcnBjVXJsLAogICAgbmV0d29ya1Bhc3NwaHJhc2UsCiAgICBzZWNyZXRLZXksCiAgICBjb25zdHJ1Y3RvckFyZ3MsCiAgICBpbml0aWFsaXplQXJncywKICAgIG9uU3RhZ2VVcGRhdGUsCiAgfSA9IGlucHV0OwoKICBjb25zdCBzZGsgPSBhd2FpdCBsb2FkU29yb2JhblNESygpOwogIGNvbnN0IGtleXBhaXIgPSBzZGsuS2V5cGFpci5mcm9tU2VjcmV0KHNlY3JldEtleSk7CiAgY29uc3QgcHVibGljS2V5ID0ga2V5cGFpci5wdWJsaWNLZXkoKTsKICBjb25zdCBzZXJ2ZXIgPSBuZXcgc2RrLlNvcm9iYW5ScGMuU2VydmVyKHJwY1VybCwgeyBhbGxvd0h0dHA6IHRydWUgfSk7CiAgY29uc3QgbmV0d29yayA9IGF3YWl0IHNlcnZlci5nZXROZXR3b3JrKCk7CiAgY29uc3QgdHhCdWlsZGVyID0gbmV3IHNkay5UcmFuc2FjdGlvbkJ1aWxkZXIobmV0d29ya1Bhc3NwaHJhc2UsIHsKICAgIGZlZUJ1bXA6ICcxMDAnLAogICAgbmV0d29yaywKICB9KTsKCiAgLy8gU3RhZ2UgMTogVXBsb2FkIFdBU00KICBvblN0YWdlVXBkYXRlKCd1cGxvYWQnLCB7IHN0YXR1czogJ2FjdGl2ZScsIHN0YXJ0ZWRBdDogRGF0ZS5ub3coKSB9KTsKICBjb25zdCB1cGxvYWRUeCA9IGF3YWl0IHR4QnVpbGRlci5jcmVhdGVVcGxvYWRXYXNtT3BlcmF0aW9uKHsgd2FzbTogQnVmZmVyLmZyb20od2FzbUJ5dGVzKSB9KTsKICBjb25zdCB1cGxvYWRUeCA9IGF3YWl0IHR4QnVpbGRlci5idWlsZCgpOwogIHVwbG9hZFR4LnNpZ24oa2V5cGFpcik7CiAgY29uc3QgdXBsb2FkUmVzcG9uc2UgPSBhd2FpdCBzZXJ2ZXIuc2VuZFRyYW5zYWN0aW9uKHVwbG9hZFR4KTsKICBjb25zdCB1cGxvYWRSZXN1bHQgPSBhd2FpdCBzZXJ2ZXIucG9sbFRyYW5zYWN0aW9uKHVwbG9hZFJlc3BvbnNlKTsKICBjb25zdCB3YXNtSGFzaCA9IHVwbG9hZFJlc3VsdC5yZXR1cm5WYWx1ZT8udG9TdHJpbmcoKSA/PyAnJzsKICBvblN0YWdlVXBkYXRlKCd1cGxvYWQnLCB7CiAgICBzdGF0dXM6ICdzdWNjZXNzJywKICAgIGhhc2g6IHVwbG9hZFJlc3VsdC5oYXNoLAogICAgZmluaXNoZWRBdDogRGF0ZS5ub3coKSwKICAgIG1lc3NhZ2U6IGBXQVNNIGhhc2g6ICR7d2FzbUhhc2h9YCwKICB9KTsKCiAgLy8gU3RhZ2UgMjogSW5zdGFsbCBjb250cmFjdCBjb2RlCiAgb25TdGFnZVVwZGF0ZSgnaW5zdGFsbCcsIHsgc3RhdHVzOiAnYWN0aXZlJywgc3RhcnRlZEF0OiBEYXRlLm5vdygpIH0pOwogIGNvbnN0IGluc3RhbGxUeCA9IGF3YWl0IHR4QnVpbGRlci5jcmVhdGVDdXN0b21Db250cmFjdE9wZXJhdGlvbih7CiAgICB3YXNtSGFzaCwKICAgIGFkZHJlc3M6IHB1YmxpY0tleSwKICB9KTsKICBjb25zdCBpbnN0YWxsVHggPSBhd2FpdCB0eEJ1aWxkZXIuYnVpbGQoKTsKICBpbnN0YWxsVHguc2lnbihrZXlwYWlyKTsKICBjb25zdCBpbnN0YWxsUmVzcG9uc2UgPSBhd2FpdCBzZXJ2ZXIuc2VuZFRyYW5zYWN0aW9uKGluc3RhbGxUeCk7CiAgY29uc3QgaW5zdGFsbFJlc3VsdCA9IGF3YWl0IHNlcnZlci5wb2xsVHJhbnNhY3Rpb24oaW5zdGFsbFJlc3BvbnNlKTsKICBjb25zdCBjb250cmFjdElkID0gaW5zdGFsbFJlc3VsdC5yZXR1cm5WYWx1ZT8udG9TdHJpbmcoKSA/PyAnJzsKICBvblN0YWdlVXBkYXRlKCdpbnN0YWxsJywgewogICAgc3RhdHVzOiAnc3VjY2VzcycsCiAgICBoYXNoOiBpbnN0YWxsUmVzdWx0Lmhhc2gsCiAgICBmaW5pc2hlZEF0OiBEYXRlLm5vdygpLAogICAgbWVzc2FnZTogYENvbnRyYWN0IElEOiAke2NvbnRyYWN0SWR9YCwKICB9KTsKCiAgLy8gU3RhZ2UgMzogSW5zdGFudGlhdGUgd2l0aCBjb25zdHJ1Y3RvciBhcmd1bWVudHMKICBvblN0YWdlVXBkYXRlKCdpbnN0YW50aWF0ZScsIHsgc3RhdHVzOiAnYWN0aXZlJywgc3RhcnRlZEF0OiBEYXRlLm5vdygpIH0pOwogIGNvbnN0IGNvbnRyYWN0ID0gbmV3IHNkay5Db250cmFjdChjb250cmFjdElkKTsKICBjb25zdCBjb25zdHJ1Y3RvclZhbHVlcyA9IGNvbnN0cnVjdG9yQXJncy5tYXAoKGFyZykgPT4gY29udmVydEFyZ3VtZW50VmFsdWUoYXJnLCBzZGspKTsKICBjb25zdCBpbnN0YW50aWF0ZVR4ID0gYXdhaXQgdHhCdWlsZGVyLmNyZWF0ZUNvbnRyYWN0T3BlcmF0aW9uKHsKICAgIGNvbnRyYWN0LAogICAgbWV0aG9kOiAnY29uc3RydWN0b3InLAogICAgYXJnczogY29uc3RydWN0b3JWYWx1ZXMsCiAgfSk7CiAgY29uc3QgaW5zdGFudGlhdGVUeCA9IGF3YWl0IHR4QnVpbGRlci5idWlsZCgpOwogIGluc3RhbnRpYXRlVHguc2lnbihrZXlwYWlyKTsKICBjb25zdCBpbnN0YW50aWF0ZVJlc3BvbnNlID0gYXdhaXQgc2VydmVyLnNlbmRUcmFuc2FjdGlvbihpbnN0YW50aWF0ZVR4KTsKICBjb25zdCBpbnN0YW50aWF0ZVJlc3VsdCA9IGF3YWl0IHNlcnZlci5wb2xsVHJhbnNhY3Rpb24oaW5zdGFudGlhdGVSZXNwb25zZSk7CiAgb25TdGFnZVVwZGF0ZSgnaW5zdGFudGlhdGUnLCB7CiAgICBzdGF0dXM6ICdzdWNjZXNzJywKICAgIGhhc2g6IGluc3RhbnRpYXRlUmVzdWx0Lmhhc2gsCiAgICBmaW5pc2hlZEF0OiBEYXRlLm5vdygpLAogICAgbWVzc2FnZTogYEluc3RhbmNlIGluaXRpYWxpemVkIGF0ICR7Y29udHJhY3RJZH1gLAogIH0pOwoKICAvLyBTdGFnZSA0OiBJbml0aWFsaXplIHN0YXRlIChvcHRpb25hbCkKICBvblN0YWdlVXBkYXRlKCdpbml0aWFsaXplJywgeyBzdGF0dXM6ICdhY3RpdmUnLCBzdGFydGVkQXQ6IERhdGUubm93KCkgfSk7CiAgbGV0IGluaXRpYWxpemVUeEhhc2g6IHN0cmluZyB8IHVuZGVmaW5lZDsKICBpZiAoaW5pdGlhbGl6ZUFyZ3MubGVuZ3RoID4gMCkgewogICAgY29uc3QgaW5pdFZhbHVlcyA9IGluaXRpYWxpemVBcmdzLm1hcCgoYXJnKSA9PiBjb252ZXJ0QXJndW1lbnRWYWx1ZShhcmcsIHNkaykpOwogICAgY29uc3QgaW5pdFR4ID0gYXdhaXQgdHhCdWlsZGVyLmNyZWF0ZUNvbnRyYWN0T3BlcmF0aW9uKHsKICAgICAgY29udHJhY3QsCiAgICAgIG1ldGhvZDogJ2luaXRpYWxpemUnLAogICAgICBhcmdzOiBpbml0VmFsdWVzLAogICAgfSk7CiAgICBjb25zdCBpbml0QnVpbHQgPSBhd2FpdCB0eEJ1aWxkZXIuYnVpbGQoKTsKICAgIGluaXRCdWlsdC5zaWduKGtleXBhaXIpOwogICAgY29uc3QgaW5pdFJlc3BvbnNlID0gYXdhaXQgc2VydmVyLnNlbmRUcmFuc2FjdGlvbihpbml0QnVpbHQpOwogICAgY29uc3QgaW5pdFJlc3VsdCA9IGF3YWl0IHNlcnZlci5wb2xsVHJhbnNhY3Rpb24oaW5pdFJlc3BvbnNlKTsKICAgIGluaXRpYWxpemVUeEhhc2ggPSBpbml0UmVzdWx0Lmhhc2g7CiAgfQoKICAvLyBWZXJpZnkgb24gU3RlbGxhciBFeHBlcnQKICBjb25zdCBleHBsb3JlclVybCA9IGJ1aWxkRXhwbG9yZXJVcmwobmV0d29yaywgY29udHJhY3RJZCk7CiAgY29uc3QgdmVyaWZpZWQgPSBhd2FpdCB2ZXJpZnlPbkV4cGxvcmVyKGV4cGxvcmVyVXJsKTsKICBvblN0YWdlVXBkYXRlKCdpbml0aWFsaXplJywgewogICAgc3RhdHVzOiAnc3VjY2VzcycsCiAgICBoYXNoOiBpbml0aWFsaXplVHhIYXNoLAogICAgZmluaXNoZWRBdDogRGF0ZS5ub3coKSwKICAgIG1lc3NhZ2U6IHZlcmlmaWVkID8gJ1ZlcmlmaWVkIG9uIFN0ZWxsYXIgRXhwZXJ0JyA6ICdEZXBsb3llZCAoZXhwbG9yZXIgcGVuZGluZyknLAogIH0pOwoKICByZXR1cm4gewogICAgY29udHJhY3RJZCwKICAgIHdhc21IYXNoLAogICAgaW5zdGFsbFR4SGFzaDogaW5zdGFsbFJlc3VsdC5oYXNoLAogICAgaW5zdGFudGlhdGVUeEhhc2g6IGluc3RhbnRpYXRlUmVzdWx0Lmhhc2gsCiAgICBpbml0aWFsaXplVHhIYXNoLAogICAgZXhwbG9yZXJVcmwsCiAgICB2ZXJpZmllZCwKICB9LAp9CgovLyAtLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0KLy8gUmVkdWNlcgovLyAtLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0KCnR5cGUgU3RhdGUgPSB7CiAgc3RhZ2VzOiBSZWNvcmQ8RGVwbG95U3RhZ2UsIFN0YWdlU3RhdGU+OwogIGlzUnVubmluZzogYm9vbGVhbjsKICByZXN1bHQ6IERlcGxveVJlc3VsdCB8IG51bGw7CiAgZXJyb3I6IHN0cmluZyB8IG51bGw7CiAgbG9nczogQXJyYXk8eyB0aW1lc3RhbXA6IG51bWJlcjsgbWVzc2FnZTogc3RyaW5nOyBsZXZlbDogJ2luZm8nIHwgJ2Vycm9yJyB8ICdzdWNjZXNzJyB9PjsKfTsKCnR5cGUgQWN0aW9uID0KICB8IHsgdHlwZTogJ1JFU0VUJyB9CiAgfCB7IHR5cGU6ICdTVEFSVF9SVU4nIH0KICB8IHsgdHlwZTogJ1NUQUdFX1VQREFURSc7IHN0YWdlOiBEZXBsb3lTdGFnZTsgdXBkYXRlOiBQYXJ0aWFsPFN0YWdlU3RhdGU+IH0KICB8IHsgdHlwZTogJ1NVQ0NFU1MnOyByZXN1bHQ6IERlcGxveVJlc3VsdCB9CiAgfCB7IHR5cGU6ICdGQUlMVVJFJzsgZXJyb3I6IHN0cmluZzsgc3RhZ2U/OiBEZXBsb3lTdGFnZSB9CiAgfCB7IHR5cGU6ICdMT0cnOyBtZXNzYWdlOiBzdHJpbmc7IGxldmVsPzogJ2luZm8nIHwgJ2Vycm9yJyB8ICdzdWNjZXNzJyB9OwoKY29uc3QgaW5pdGlhbFN0YWdlcyA9ICgpOiBSZWNvcmQ8RGVwbG95U3RhZ2UsIFN0YWdlU3RhdGU+ID0+IHsKICBjb25zdCBlbnRyaWVzID0gU1RBR0VfREVGSU5JVElPTlMubWFwKChkZWYpID0+IFsKICAgIGRlZi5zdGFnZSwKICAgIHsgc3RhZ2U6IGRlZi5zdGFnZSwgc3RhdHVzOiAncGVuZGluZycgYXMgU3RhZ2VTdGF0dXMsIGxhYmVsOiBkZWYubGFiZWwsIGRlc2NyaXB0aW9uOiBkZWYuZGVzY3JpcHRpb24gfSwKICBdKTsKICByZXR1cm4gT2JqZWN0LmZyb21FbnRyaWVzKGVudHJpZXMpIGFzIFJlY29yZDxEZXBsb3lTdGFnZSwgU3RhZ2VTdGF0ZT47Cn07Cgpjb25zdCBpbml0aWFsU3RhdGU6IFN0YXRlID0gewogIHN0YWdlczogaW5pdGlhbFN0YWdlcygpLAogIGlzUnVubmluZzogZmFsc2UsCiAgcmVzdWx0OiBudWxsLAogIGVycm9yOiBudWxsLAogIGxvZ3M6IFtdLAp9OwoKZnVuY3Rpb24gcmVkdWNlcihzdGF0ZTogU3RhdGUsIGFjdGlvbjogQWN0aW9uKTogU3RhdGUgewogIHN3aXRjaCAoYWN0aW9uLnR5cGUpIHsKICAgIGNhc2UgJ1JFU0VUJzoKICAgICAgcmV0dXJuIHsKICAgICAgICAuLi5pbml0aWFsU3RhdGUsCiAgICAgICAgc3RhZ2VzOiBpbml0aWFsU3RhZ2VzKCksCiAgICAgICAgbG9nczogW10sCiAgICAgIH07CiAgICBjYXNlICdTVEFSVF9SVU4nOgogICAgICByZXR1cm4gewogICAgICAgIC4uLnN0YXRlLAogICAgICAgIGlzUnVubmluZzogdHJ1ZSwKICAgICAgICBlcnJvcjogbnVsbCwKICAgICAgICByZXN1bHQ6IG51bGwsCiAgICAgICAgc3RhZ2VzOiBpbml0aWFsU3RhZ2VzKCksCiAgICAgICAgbG9nczogWwogICAgICAgICAgLi4uc3RhdGUubG9ncywKICAgICAgICAgIHsgdGltZXN0YW1wOiBEYXRlLm5vdygpLCBtZXNzYWdlOiAnRGVwbG95bWVudCBwaXBlbGluZSBzdGFydGVkJywgbGV2ZWw6ICdpbmZvJyB9LAogICAgICAgIF0sCiAgICAgIH07CiAgICBjYXNlICdTVEFHRV9VUERBVEUnOgogICAgICByZXR1cm4gewogICAgICAgIC4uLnN0YXRlLAogICAgICAgIHN0YWdlczogewogICAgICAgICAgLi4uc3RhdGUuc3RhZ2VzLAogICAgICAgICAgW2FjdGlvbi5zdGFnZV06IHsKICAgICAgICAgICAgLi4uc3RhdGUuc3RhZ2VzW2FjdGlvbi5zdGFnZV0sCiAgICAgICAgICAgIC4uLmFjdGlvbi51cGRhdGUsCiAgICAgICAgICB9LAogICAgICAgIH0sCiAgICAgIH07CiAgICBjYXNlICdTVUNDRVNTJzoKICAgICAgcmV0dXJuIHsKICAgICAgICAuLi5zdGF0ZSwKICAgICAgICBpc1J1bm5pbmc6IGZhbHNlLAogICAgICAgIHJlc3VsdDogYWN0aW9uLnJlc3VsdCwKICAgICAgICBsb2dzOiBbCiAgICAgICAgICAuLi5zdGF0ZS5sb2dzLAogICAgICAgICAgeyB0aW1lc3RhbXA6IERhdGUubm93KCksIG1lc3NhZ2U6IGBEZXBsb3ltZW50IGNvbXBsZXRlOiAke2FjdGlvbi5yZXN1bHQuY29udHJhY3RJZH1gLCBsZXZlbDogJ3N1Y2Nlc3MnIH0sCiAgICAgICAgXSwKICAgICAgfTsKICAgIGNhc2UgJ0ZBSUxVUkUnOgogICAgICByZXR1cm4gewogICAgICAgIC4uLnN0YXRlLAogICAgICAgIGlzUnVubmluZzogZmFsc2UsCiAgICAgICAgZXJyb3I6IGFjdGlvbi5lcnJvciwKICAgICAgICBzdGFnZXM6IGFjdGlvbi5zdGFnZQogICAgICAgICAgPyB7CiAgICAgICAgICAgICAgLi4uc3RhdGUuc3RhZ2VzLAogICAgICAgICAgICAgIFthY3Rpb24uc3RhZ2VdOiB7IC4uLnN0YXRlLnN0YWdlc1thY3Rpb24uc3RhZ2VdLCBzdGF0dXM6ICdlcnJvcicsIG1lc3NhZ2U6IGFjdGlvbi5lcnJvciB9LAogICAgICAgICAgICB9CiAgICAgICAgICA6IHN0YXRlLnN0YWdlcywKICAgICAgICBsb2dzOiBbCiAgICAgICAgICAuLi5zdGF0ZS5sb2dzLAogICAgICAgICAgeyB0aW1lc3RhbXA6IERhdGUubm93KCksIG1lc3NhZ2U6IGBEZXBsb3ltZW50IGZhaWxlZDogJHthY3Rpb24uZXJyb3J9YCwgbGV2ZWw6ICdlcnJvcicgfSwKICAgICAgICBdLAogICAgICB9OwogICAgY2FzZSAnTE9HJzoKICAgICAgcmV0dXJuIHsKICAgICAgICAuLi5zdGF0ZSwKICAgICAgICBsb2dzOiBbCiAgICAgICAgICAuLi5zdGF0ZS5sb2dzLAogICAgICAgICAgeyB0aW1lc3RhbXA6IERhdGUubm93KCksIG1lc3NhZ2U6IGFjdGlvbi5tZXNzYWdlLCBsZXZlbDogYWN0aW9uLmxldmVsID8/ICdpbmZvJyB9LAogICAgICAgIF0sCiAgICAgIH07CiAgICBkZWZhdWx0OgogICAgICByZXR1cm4gc3RhdGU7CiAgfQp9CgovLyAtLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0KLy8gU3ViLWNvbXBvbmVudHMKLy8gLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tCgpmdW5jdGlvbiBTdGFnZUluZGljYXRvcih7IHN0YWdlLCBpbmRleCB9OiB7IHN0YWdlOiBTdGFnZVN0YXRlOyBpbmRleDogbnVtYmVyIH0pIHsKICBjb25zdCBpY29uID0gKCgpID0+IHsKICAgIHN3aXRjaCAoc3RhZ2Uuc3RhdHVzKSB7CiAgICAgIGNhc2UgJ3N1Y2Nlc3MnOgogICAgICAgIHJldHVybiA8Q2hlY2tDaXJjbGUgY2xhc3NOYW1lPSJ3LTUgaC01IHRleHQtZW1lcmFsZC00MDAiIC8+OwogICAgICBjYXNlICdlcnJvcic6CiAgICAgICAgcmV0dXJuIDxYQ2lyY2xlIGNsYXNzTmFtZT0idy01IGgtNSB0ZXh0LXJlZC00MDAiIC8+OwogICAgICBjYXNlICdhY3RpdmUnOgogICAgICAgIHJldHVybiA8TG9hZGVyMiBjbGFzc05hbWU9InctNSBoLTUgYW5pbWF0ZS1zcGluIHRleHQtYmx1ZS00MDAiIC8+OwogICAgICBkZWZhdWx0OgogICAgICAgIHJldHVybiAoCiAgICAgICAgICA8c3BhbiBjbGFzc05hbWU9ImZsZXggaXRlbXMtY2VudGVyIGp1c3RpZnktY2VudGVyIHctNSBoLTUgcm91bmRlZC1mdWxsIGJvcmRlciBib3JkZXItc2xhdGUtNjAwIHRleHQteHMgdGV4dC1zbGF0ZS00MDAiPgogICAgICAgICAgICB7aW5kZXggKyAxfQogICAgICAgICAgPC9zcGFuPgogICAgICAgICk7CiAgICB9CiAgfSkoKTsKCiAgY29uc3QgYm9yZGVyQ29sb3IgPQogICAgc3RhZ2Uuc3RhdHVzID09PSAnc3VjY2VzcycKICAgICAgPyAnYm9yZGVyLWVtZXJhbGQtNTAwLzQwJwogICAgICA6IHN0YWdlLnN0YXR1cyA9PT0gJ2Vycm9yJwogICAgICAgID8gJ2JvcmRlci1yZWQtNTAwLzQwJwogICAgICAgIDogc3RhZ2Uuc3RhdHVzID09PSAnYWN0aXZlJwogICAgICAgICAgPyAnYm9yZGVyLWJsdWUtNTAwLzYwIGJnLWJsdWUtNTAwLzUnCiAgICAgICAgICA6ICdib3JkZXItc2xhdGUtNzAwLzYwJzsKCiAgcmV0dXJuICgKICAgIDxkaXYgY2xhc3NOYW1lPXtgZmxleCBpdGVtcy1zdGFydCBnYXAtMyByb3VuZGVkLWxnIGJvcmRlciBweC0zIHB5LTIuNSB0cmFuc2l0aW9uLWNvbG9ycyAke2JvcmRlckNvbG9yfWB9PgogICAgICA8ZGl2IGNsYXNzTmFtZT0iZmxleC1zaHJpbmstMCBwdC0wLjUiPntpY29ufTwvZGl2PgogICAgICA8ZGl2IGNsYXNzTmFtZT0iZmxleC0xIG1pbi13LTAiPgogICAgICAgIDxkaXYgY2xhc3NOYW1lPSJmbGV4IGl0ZW1zLWNlbnRlciBqdXN0aWZ5LWJldHdlZW4gZ2FwLTIiPgogICAgICAgICAgPHNwYW4gY2xhc3NOYW1lPSJ0ZXh0LXNtIGZvbnQtbWVkaXVtIHRleHQtc2xhdGUtMTAwIj57c3RhZ2UubGFiZWx9PC9zcGFuPgogICAgICAgICAge3N0YWdlLmhhc2ggJiYgKAogICAgICAgICAgICA8c3BhbiBjbGFzc05hbWU9ImZvbnQtbW9ubyB0ZXh0LVsxMHB4XSB0ZXh0LXNsYXRlLTUwMCI+CiAgICAgICAgICAgICAge3N0YWdlLmhhc2guc2xpY2UoMCwgMTApfS4uLgogICAgICAgICAgICA8L3NwYW4+CiAgICAgICAgICApfQogICAgICAgIDwvZGl2PgogICAgICAgIDxwIGNsYXNzTmFtZT0idGV4dC14cyB0ZXh0LXNsYXRlLTQwMCBtdC0wLjUiPntzdGFnZS5kZXNjcmlwdGlvbn08L3A+CiAgICAgICAge3N0YWdlLm1lc3NhZ2UgJiYgKAogICAgICAgICAgPHAKICAgICAgICAgICAgY2xhc3NOYW1lPXsKICAgICAgICAgICAgICAndGV4dC1bMTFweF0gbXQtMSBmb250LW1vbm8gJyArCiAgICAgICAgICAgICAgKHN0YWdlLnN0YXR1cyA9PT0gJ2Vycm9yJyA/ICd0ZXh0LXJlZC00MDAnIDogJ3RleHQtZW1lcmFsZC00MDAnKQogICAgICAgICAgICB9CiAgICAgICAgICA+CiAgICAgICAgICAgIHtzdGFnZS5tZXNzYWdlfQogICAgICAgICAgPC9wPgogICAgICAgICl9CiAgICAgIDwvZGl2PgogICAgPC9kaXY+CiAgKTsKfQoKZnVuY3Rpb24gQ29weUJ1dHRvbih7IHZhbHVlLCBsYWJlbCB9OiB7IHZhbHVlOiBzdHJpbmc7IGxhYmVsPzogc3RyaW5nIH0pIHsKICBjb25zdCBbY29waWVkLCBzZXRDb3BpZWRdID0gdXNlU3RhdGUoZmFsc2UpOwogIGNvbnN0IGhhbmRsZUNvcHkgPSB1c2VDYWxsYmFjayhhc3luYyAoKSA9PiB7CiAgICB0cnkgewogICAgICBhd2FpdCBuYXZpZ2F0b3IuY2xpcGJvYXJkLndyaXRlVGV4dCh2YWx1ZSk7CiAgICAgIHNldENvcGllZCh0cnVlKTsKICAgICAgc2V0VGltZW91dCgoKSA9PiBzZXRDb3BpZWQoZmFsc2UpLCAxNTAwKTsKICAgIH0gY2F0Y2ggewogICAgICAvLyBjbGlwYm9hcmQgdW5hdmFpbGFibGUgLSBzaWxlbnRseSBpZ25vcmUKICAgIH0KICB9LCBbdmFsdWVdKTsKCiAgcmV0dXJuICgKICAgIDxidXR0b24KICAgICAgdHlwZT0iYnV0dG9uIgogICAgICBvbkNsaWNrPXtoYW5kbGVDb3B5fQogICAgICBjbGFzc05hbWU9ImlubGluZS1mbGV4IGl0ZW1zLWNlbnRlciBnYXAtMSByb3VuZGVkLW1kIGJvcmRlciBib3JkZXItc2xhdGUtNzAwIGJnLXNsYXRlLTgwMC80MCBweC0yIHB5LTEgdGV4dC14cyB0ZXh0LXNsYXRlLTIwMCBob3ZlcjpiZy1zbGF0ZS03MDAvNjAgdHJhbnNpdGlvbi1jb2xvcnMiCiAgICAgIGFyaWEtbGFiZWw9e2xhYmVsID8/ICdDb3B5J30KICAgID4KICAgICAge2NvcGllZCA/IDxDaGVjayBjbGFzc05hbWU9InctMy41IGgtMy41IHRleHQtZW1lcmFsZC00MDAiIC8+IDogPENsaXBib2FyZENvcHkgY2xhc3NOYW1lPSJ3LTMuNSBoLTMuNSIgLz59CiAgICAgIDxzcGFuPntjb3BpZWQgPyAnQ29waWVkJyA6ICdDb3B5J308L3NwYW4+CiAgICA8L2J1dHRvbj4KICApOwp9CgpmdW5jdGlvbiBBcmd1bWVudEVkaXRvcih7CiAgYXJncywKICBvbkNoYW5nZSwKICB0aXRsZSwKICBkZXNjcmlwdGlvbiwKfTogewogIGFyZ3M6IENvbnN0cnVjdG9yQXJndW1lbnRbXTsKICBvbkNoYW5nZTogKGFyZ3M6IENvbnN0cnVjdG9yQXJndW1lbnRbXSkgPT4gdm9pZDsKICB0aXRsZTogc3RyaW5nOwogIGRlc2NyaXB0aW9uPzogc3RyaW5nOwp9KSB7CiAgY29uc3QgdXBkYXRlQXJnID0gKGluZGV4OiBudW1iZXIsIHBhdGNoOiBQYXJ0aWFsPENvbnN0cnVjdG9yQXJndW1lbnQ+KSA9PiB7CiAgICBjb25zdCBuZXh0ID0gYXJncy5zbGljZSgpOwogICAgbmV4dFtpbmRleF0gPSB7IC4uLm5leHRbaW5kZXhdLCAuLi5wYXRjaCB9OwogICAgb25DaGFuZ2UobmV4dCk7CiAgfTsKCiAgY29uc3QgYWRkQXJnID0gKCkgPT4gewogICAgb25DaGFuZ2UoWwogICAgICAuLi5hcmdzLAKICAgICAgeyBuYW1lOiBgYXJnJHthcmdzLmxlbmd0aCArIDF9YCwgdHlwZTogJ3N0cmluZycsIHZhbHVlOiAnJywgcmVxdWlyZWQ6IHRydWUgfSwKICAgIF0pOwogIH07CgogIGNvbnN0IHJlbW92ZUFyZyA9IChpbmRleDogbnVtYmVyKSA9PiB7CiAgICBvbkNoYW5nZShhcmdzLmZpbHRlcigoXywgaSkgPT4gaSAhPT0gaW5kZXgpKTsKICB9OwoKICByZXR1cm4gKAogICAgPGRpdiBjbGFzc05hbWU9InNwYWNlLXktMyI+CiAgICAgIDxkaXYgY2xhc3NOYW1lPSJmbGV4IGl0ZW1zLWNlbnRlciBqdXN0aWZ5LWJldHdlZW4iPgogICAgICAgIDxkaXY+CiAgICAgICAgICA8aDMgY2xhc3NOYW1lPSJ0ZXh0LXNtIGZvbnQtc2VtaWJvbGQgdGV4dC1zbGF0ZS0yMDAiPnt0aXRsZX08L2gzPgogICAgICAgICAge2Rlc2NyaXB0aW9uICYmIDxwIGNsYXNzTmFtZT0idGV4dC14cyB0ZXh0LXNsYXRlLTUwMCI+e2Rlc2NyaXB0aW9ufTwvcD59CiAgICAgICAgPC9kaXY+CiAgICAgICAgPGJ1dHRvbgogICAgICAgICAgdHlwZT0iYnV0dG9uIgogICAgICAgICAgb25DbGljaz17YWRkQXJnfQogICAgICAgICAgY2xhc3NOYW1lPSJyb3VuZGVkLW1kIGJvcmRlciBib3JkZXItc2xhdGUtNzAwIGJnLXNsYXRlLTgwMC80MCBweC0yIHB5LTEgdGV4dC14cyB0ZXh0LXNsYXRlLTIwMCBob3ZlcjpiZy1zbGF0ZS03MDAvNjAiCiAgICAgICAgPgogICAgICAgICAgKyBBZGQgYXJndW1lbnQKICAgICAgICA8L2J1dHRvbj4KICAgICAgPC9kaXY+CgogICAgICB7YXJncy5sZW5ndGggPT09IDAgJiYgKAogICAgICAgIDxwIGNsYXNzTmFtZT0icm91bmRlZC1sZyBib3JkZXIgYm9yZGVyLWRhc2hlZCBib3JkZXItc2xhdGUtNzAwIHB4LTMgcHktNCB0ZXh0LXhzIHRleHQtc2xhdGUtNTAwIj4KICAgICAgICAgIE5vIGFyZ3VtZW50cy4gQWRkIG9uZSBpZiB0aGUgY29uc3RydWN0b3IgcmVxdWlyZXMgaW5wdXRzLgogICAgICAgIDwvcD4KICAgICAgKX0KCiAgICAgIDxkaXYgY2xhc3NOYW1lPSJzcGFjZS15LTIiPgogICAgICAgIHthcmdzLm1hcCgoYXJnLCBpbmRleCkgPT4gKAogICAgICAgICAgPGRpdiBrZXk9e2luZGV4fSBjbGFzc05hbWU9ImdyaWQgZ3JpZC1jb2xzLTEyIGdhcC0yIGl0ZW1zLWNlbnRlciI+CiAgICAgICAgICAgIDxpbnB1dAogICAgICAgICAgICAgIGNsYXNzTmFtZT0iY29sLXNwYW4tNCByb3VuZGVkLW1kIGJvcmRlciBib3JkZXItc2xhdGUtNzAwIGJnLXNsYXRlLTkwMC80MCBweC0yIHB5LTEuNSB0ZXh0LXhzIHRleHQtc2xhdGUtMTAwIHBsYWNlaG9sZGVyOnRleHQtc2xhdGUtNjAwIGZvY3VzOm91dGxpbmUtbm9uZSBmb2N1czpib3JkZXItYmx1ZS01MDAiCiAgICAgICAgICAgICAgcGxhY2Vob2xkZXI9Im5hbWUiCiAgICAgICAgICAgICAgdmFsdWU9e2FyZy5uYW1lfQogICAgICAgICAgICAgIG9uQ2hhbmdlPXsoZSkgPT4gdXBkYXRlQXJnKGluZGV4LCB7IG5hbWU6IGUudGFyZ2V0LnZhbHVlIH0pfQogICAgICAgICAgICAvPgogICAgICAgICAgICA8c2VsZWN0CiAgICAgICAgICAgICAgY2xhc3NOYW1lPSJjb2wtc3Bhbi0zIHJvdW5kZWQtbWQgYm9yZGVyIGJvcmRlci1zbGF0ZS03MDAgYmctc2xhdGUtOTAwLzQwIHB4LTIgcHktMS41IHRleHQteHMgdGV4dC1zbGF0ZS0xMDAgZm9jdXM6b3V0bGluZS1ub25lIGZvY3VzOmJvcmRlci1ibHVlLTUwMCIKICAgICAgICAgICAgICB2YWx1ZT17YXJnLnR5cGV9CiAgICAgICAgICAgICAgb25DaGFuZ2U9eyhlKSA9PiB1cGRhdGVBcmcoaW5kZXgsIHsgdHlwZTogZS50YXJnZXQudmFsdWUgYXMgQ29uc3RydWN0b3JBcmd1bWVudFsndHlwZSddIH0pfQogICAgICAgICAgICA+CiAgICAgICAgICAgICAgPG9wdGlvbiB2YWx1ZT0ic3RyaW5nIj5zdHJpbmc8L29wdGlvbj4KICAgICAgICAgICAgICA8b3B0aW9uIHZhbHVlPSJudW1iZXIiPm51bWJlcjwvb3B0aW9uPgogICAgICAgICAgICAgIDxvcHRpb24gdmFsdWU9ImJvb2xlYW4iPmJvb2xlYW48L29wdGlvbj4KICAgICAgICAgICAgICA8b3B0aW9uIHZhbHVlPSJhZGRyZXNzIj5hZGRyZXNzPC9vcHRpb24+CiAgICAgICAgICAgICAgPG9wdGlvbiB2YWx1ZT0ic3ltYm9sIj5zeW1ib2w8L29wdGlvbj4KICAgICAgICAgICAgICA8b3B0aW9uIHZhbHVlPSJieXRlcyI+Ynl0ZXM8L29wdGlvbj4KICAgICAgICAgICAgPC9zZWxlY3Q+CiAgICAgICAgICAgIDxpbnB1dAogICAgICAgICAgICAgIGNsYXNzTmFtZT0iY29sLXNwYW4tNCByb3VuZGVkLW1kIGJvcmRlciBib3JkZXItc2xhdGUtNzAwIGJnLXNsYXRlLTkwMC80MCBweC0yIHB5LTEuNSB0ZXh0LXhzIHRleHQtc2xhdGUtMTAwIHBsYWNlaG9sZGVyOnRleHQtc2xhdGUtNjAwIGZvY3VzOm91dGxpbmUtbm9uZSBmb2N1czpib3JkZXItYmx1ZS01MDAiCiAgICAgICAgICAgICAgcGxhY2Vob2xkZXI9InZhbHVlIgogICAgICAgICAgICAgIHZhbHVlPXthcmcudmFsdWV9CiAgICAgICAgICAgICAgb25DaGFuZ2U9eyhlKSA9PiB1cGRhdGVBcmcoaW5kZXgsIHsgdmFsdWU6IGUudGFyZ2V0LnZhbHVlIH0pfQogICAgICAgICAgICAvPgogICAgICAgICAgICA8YnV0dG9uCiAgICAgICAgICAgICAgdHlwZT0iYnV0dG9uIgogICAgICAgICAgICAgIG9uQ2xpY2s9eygpID0+IHJlbW92ZUFyZyhpbmRleCl9CiAgICAgICAgICAgICAgY2xhc3NOYW1lPSJjb2wtc3Bhbi0xIHJvdW5kZWQtbWQgYm9yZGVyIGJvcmRlci1zbGF0ZS03MDAgYmctc2xhdGUtODAwLzQwIHB4LTIgcHktMS41IHRleHQteHMgdGV4dC1zbGF0ZS0zMDAgaG92ZXI6YmctcmVkLTUwMC8yMCBob3Zlcjp0ZXh0LXJlZC0zMDAiCiAgICAgICAgICAgID4KICAgICAgICAgICAgICB4CiAgICAgICAgICAgIDwvYnV0dG9uPgogICAgICAgICAgPC9kaXY+CiAgICAgICAgKSl9CiAgICAgIDwvZGl2PgogICAgPC9kaXY+CiAgKTsKfQoKLy8gLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tCi8vIE1haW4gY29tcG9uZW50Ci8vIC0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLQoKZXhwb3J0IGRlZmF1bHQgZnVuY3Rpb24gRGVwbG95UGFuZWwoewogIHdhc21CeXRlcywKICBjb250cmFjdE5hbWUgPSAndW50aXRsZWQtY29udHJhY3QnLAogIG5ldHdvcmsgPSAndGVzdG5ldCcsCiAgcnBjVXJsLAogIG5ldHdvcmtQYXNzcGhyYXNlLAogIHNlY3JldEtleSwKICBwdWJsaWNLZXksCiAgY29uc3RydWN0b3JBcmdzLAogIGluaXRpYWxpemVBcmdzLAogIG9uRGVwbG95ZWQsCiAgb25FcnJvciwKICBkZXBsb3lPdmVycmlkZSwKfTogRGVwbG95UGFuZWxQcm9wcykgewogIGNvbnN0IFtzdGF0ZSwgZGlzcGF0Y2hdID0gdXNlUmVkdWNlcihyZWR1Y2VyLCBpbml0aWFsU3RhdGUpOwogIGNvbnN0IFtsb2NhbENvbnN0cnVjdG9yQXJncywgc2V0TG9jYWxDb25zdHJ1Y3RvckFyZ3NdID0gdXNlU3RhdGU8Q29uc3RydWN0b3JBcmd1bWVudFtdPihjb25zdHJ1Y3RvckFyZ3MgPz8gW10pOwogIGNvbnN0IFtsb2NhbEluaXRpYWxpemVBcmdzLCBzZXRMb2NhbEluaXRpYWxpemVBcmdzXSA9IHVzZVN0YXRlPENvbnN0cnVjdG9yQXJndW1lbnRbXT4oaW5pdGlhbGl6ZUFyZ3MgPz8gW10pOwogIGNvbnN0IFtzZWNyZXRLZXlJbnB1dCwgc2V0U2VjcmV0S2V5SW5wdXRdID0gdXNlU3RhdGUoc2VjcmV0S2V5ID8/ICcnKTsKICBjb25zdCBbY29weUZsYXNoLCBzZXRDb3B5Rmxhc2hdID0gdXNlU3RhdGUoZmFsc2UpOwogIGNvbnN0IGxvZ1JlZiA9IHVzZVJlZjxIVE1MRGl2RWxlbWVudCB8IG51bGw+KG51bGwpOwoKICBjb25zdCBuZXR3b3JrQ29uZmlnID0gTkVUV09SS19DT05GSUdTW25ldHdvcmtdID8/IE5FVFdPUktfQ09ORklHUy50ZXN0bmV0OwogIGNvbnN0IGVmZmVjdGl2ZVJwY1VybCA9IHJwY1VybCA/PyBuZXR3b3JrQ29uZmlnLnJwY1VybDsKICBjb25zdCBlZmZlY3RpdmVQYXNzcGhyYXNlID0gbmV0d29ya1Bhc3NwaHJhc2UgPz8gbmV0d29ya0NvbmZpZy5wYXNzcGhyYXNlOwoKICBjb25zdCB3YXNtVWludDggPSB1c2VNZW1vKCgpID0+IHRvVWludDhBcnJheSh3YXNtQnl0ZXMpLCBbd2FzbUJ5dGVzXSk7CiAgY29uc3Qgd2FzbVNpemUgPSB3YXNtVWludDguYnl0ZUxlbmd0aDsKICBjb25zdCB3YXNtSGFzaFByZXZpZXcgPSB1c2VNZW1vKCgpID0+IHdhc21TaXplID4gMCA/IGJ5dGVzVG9IZXgod2FzbVVpbnQ4LnNsaWNlKDAsIDgpKSA6ICcnLCBbd2FzbVVpbnQ4LCB3YXNtU2l6ZV0pOwoKICBjb25zdCBjYW5EZXBsb3kgPSB1c2VNZW1vKCgpID0+IHsKICAgIGlmIChzdGF0ZS5pc1J1bm5pbmcpIHJldHVybiBmYWxzZTsKICAgIGlmICh3YXNtU2l6ZSA9PT0gMCkgcmV0dXJuIGZhbHNlOwogICAgaWYgKCFzZWNyZXRLZXlJbnB1dC50cmltKCkpIHJldHVybiBmYWxzZTsKICAgIGNvbnN0IG1pc3NpbmcgPSBsb2NhbENvbnN0cnVjdG9yQXJncy5zb21lKAogICAgICAoYSkgPT4gYS5yZXF1aXJlZCAmJiAhYS52YWx1ZS50cmltKCksCiAgICApOwogICAgcmV0dXJuICFtaXNzaW5nOwogIH0sIFtzdGF0ZS5pc1J1bm5pbmcsIHdhc21TaXplLCBzZWNyZXRLZXlJbnB1dCwgbG9jYWxDb25zdHJ1Y3RvckFyZ3NdKTsKCiAgdXNlRWZmZWN0KCgpID0+IHsKICAgIGlmIChsb2dSZWYuY3VycmVudCkgewogICAgICBsb2dSZWYuY3VycmVudC5zY3JvbGxUb3AgPSBsb2dSZWYuY3VycmVudC5zY3JvbGxIZWlnaHQ7CiAgICB9CiAgfSwgW3N0YXRlLmxvZ3NdKTsKCiAgY29uc3QgaGFuZGxlU3RhZ2VVcGRhdGUgPSB1c2VDYWxsYmFjaygKICAgIChzdGFnZTogRGVwbG95U3RhZ2UsIHVwZGF0ZTogUGFydGlhbDxTdGFnZVN0YXRlPikgPT4gewogICAgICBkaXNwYXRjaCh7IHR5cGU6ICdTVEFHRV9VUERBVEUnLCBzdGFnZSwgdXBkYXRlIH0pOwogICAgICBpZiAodXBkYXRlLm1lc3NhZ2UpIHsKICAgICAgICBkaXNwYXRjaCh7CiAgICAgICAgICB0eXBlOiAnTE9HJywKICAgICAgICAgIG1lc3NhZ2U6IGBbJHtzdGFnZX1dICR7dXBkYXRlLm1lc3NhZ2V9YCwKICAgICAgICAgIGxldmVsOiB1cGRhdGUuc3RhdHVzID09PSAnZXJyb3InID8gJ2Vycm9yJyA6ICdpbmZvJywKICAgICAgICB9KTsKICAgICAgfQogICAgfSwKICAgIFtdLAogICk7CgogIGNvbnN0IGhhbmRsZURlcGxveSA9IHVzZUNhbGxiYWNrKGFzeW5jICgpID0+IHsKICAgIGlmICghY2FuRGVwbG95KSByZXR1cm47CiAgICBkaXNwYXRjaCh7IHR5cGU6ICdTVEFSVF9SVU4nIH0pOwoKICAgIGNvbnN0IGlucHV0OiBEZXBsb3lJbnB1dCA9IHsKICAgICAgd2FzbUJ5dGVzOiB3YXNtVWludDgsCiAgICAgIGNvbnRyYWN0TmFtZSwKICAgICAgbmV0d29yaywKICAgICAgcnBjVXJsOiBlZmZlY3RpdmVScGNVcmwsCiAgICAgIG5ldHdvcmtQYXNzcGhyYXNlOiBlZmZlY3RpdmVQYXNzcGhyYXNlLAogICAgICBzZWNyZXRLZXk6IHNlY3JldEtleUlucHV0LnRyaW0oKSwKICAgICAgcHVibGljS2V5OiBwdWJsaWNLZXkgPz8gJycsCiAgICAgIGNvbnN0cnVjdG9yQXJnczogbG9jYWxDb25zdHJ1Y3RvckFyZ3MsCiAgICAgIGluaXRpYWxpemVBcmdzOiBsb2NhbEluaXRpYWxpemVBcmdzLAogICAgICBvblN0YWdlVXBkYXRlOiBoYW5kbGVTdGFnZVVwZGF0ZSwKICAgIH07CgogICAgdHJ5IHsKICAgICAgY29uc3QgcmVzdWx0ID0gZGVwbG95T3ZlcnJpZGUgPyBhd2FpdCBkZXBsb3lPdmVycmlkZShpbnB1dCkgOiBhd2FpdCBydW5EZXBsb3lQaXBlbGluZShpbnB1dCk7CiAgICAgIGRpc3BhdGNoKHsgdHlwZTogJ1NVQ0NFU1MnLCByZXN1bHQgfSk7CiAgICAgIG9uRGVwbG95ZWQ/LihyZXN1bHQpOwogICAgfSBjYXRjaCAoZXJyKSB7CiAgICAgIGNvbnN0IG1lc3NhZ2UgPSBlcnIgaW5zdGFuY2VvZiBFcnJvciA/IGVyci5tZXNzYWdlIDogU3RyaW5nKGVycik7CiAgICAgIGRpc3BhdGNoKHsgdHlwZTogJ0ZBSUxVUkUnLCBlcnJvcjogbWVzc2FnZSB9KTsKICAgICAgb25FcnJvcj8uKGVyciBpbnN0YW5jZW9mIEVycm9yID8gZXJyIDogbmV3IEVycm9yKG1lc3NhZ2UpKTsKICAgIH0KICB9LCBbCiAgICBjYW5EZXBsb3ksCiAgICB3YXNtVWludDgsCiAgICBjb250cmFjdE5hbWUsCiAgICBuZXR3b3JrLAogICAgZWZmZWN0aXZlUnBjVXJsLAogICAgZWZmZWN0aXZlUGFzc3BocmFzZSwKICAgIHNlY3JldEtleUlucHV0LAogICAgcHVibGljS2V5LAogICAgbG9jYWxDb25zdHJ1Y3RvckFyZ3MsCiAgICBsb2NhbEluaXRpYWxpemVBcmdzLAogICAgaGFuZGxlU3RhZ2VVcGRhdGUsCiAgICBkZXBsb3lPdmVycmlkZSwKICAgIG9uRGVwbG95ZWQsCiAgICBvbkVycm9yLAogIF0pOwoKICBjb25zdCBoYW5kbGVSZXNldCA9IHVzZUNhbGxiYWNrKCgpID0+IHsKICAgIGRpc3BhdGNoKHsgdHlwZTogJ1JFU0VUJyB9KTsKICB9LCBbXSk7CgogIGNvbnN0IGhhbmRsZUNvcHlDb250cmFjdElkID0gdXNlQ2FsbGJhY2soYXN5bmMgKCkgPT4gewogICAgaWYgKCFzdGF0ZS5yZXN1bHQpIHJldHVybjsKICAgIHRyeSB7CiAgICAgIGF3YWl0IG5hdmlnYXRvci5jbGlwYm9hcmQud3JpdGVUZXh0KHN0YXRlLnJlc3VsdC5jb250cmFjdElkKTsKICAgICAgc2V0Q29weUZsYXNoKHRydWUpOwogICAgICBzZXRUaW1lb3V0KCgpID0+IHNldENvcHlGbGFzaChmYWxzZSksIDE1MDApOwogICAgfSBjYXRjaCB7CiAgICAgIC8vIGlnbm9yZQogICAgfQogIH0sIFtzdGF0ZS5yZXN1bHRdKTsKCiAgY29uc3Qgc3RhZ2VPcmRlciA9IFNUQUdFX0RFRklOSVRJT05TLm1hcCgocykgPT4gcy5zdGFnZSk7CgogIHJldHVybiAoCiAgICA8ZGl2IGNsYXNzTmFtZT0iZmxleCBmbGV4LWNvbCBnYXAtNCByb3VuZGVkLXhsIGJvcmRlciBib3JkZXItc2xhdGUtODAwIGJnLXNsYXRlLTkwMC85MCBwLTQiIHRleHQtY29sb3I9InNsYXRlLTIwMCI+CiAgICAgIDxkaXYgY2xhc3NOYW1lPSJmbGV4IGl0ZW1zLWNlbnRlciBqdXN0aWZ5LWJldHdlZW4gZ2FwLTIiPgogICAgICAgIDxkaXYgY2xhc3NOYW1lPSJmbGV4IGl0ZW1zLWNlbnRlciBnYXAtMiI+CiAgICAgICAgICA8V2FsbHJ1cyBjbGFzc05hbWU9InctNSBoLTUgdGV4dC1ibHVlLTQwMCIgLz4KICAgICAgICAgIDxkaXY+CiAgICAgICAgICAgIDxoMiBjbGFzc05hbWU9InRleHQtc20gZm9udC1zZW1pYm9sZCB0ZXh0LXNsYXRlLTEwMCI+RGVwbG95IENvbnRyYWN0PC9oMj4KICAgICAgICAgICAgPHAgY2xhc3NOYW1lPSJ0ZXh0LXhzIHRleHQtc2xhdGUtNTAwIj4KICAgICAgICAgICAgICB7Y29udHJhY3ROYW1lfSA8c3BhbiBjbGFzc05hbWU9InRleHQtc2xhdGUtNjAwIj7Ctzwvc3Bhbj4ge25ldHdvcmt9CiAgICAgICAgICAgIDwvcD4KICAgICAgICAgIDwvZGl2PgogICAgICAgIDwvZGl2PgogICAgICAgIDxkaXYgY2xhc3NOYW1lPSJmbGV4IGl0ZW1zLWNlbnRlciBnYXAtMiI+CiAgICAgICAgICB7c3RhdGUucmVzdWx0ICYmICgKICAgICAgICAgICAgPGJ1dHRvbgogICAgICAgICAgICAgIHR5cGU9ImJ1dHRvbiIKICAgICAgICAgICAgICBvbkNsaWNrPXtoYW5kbGVSZXNldH0KICAgICAgICAgICAgICBjbGFzc05hbWU9ImlubGluZS1mbGV4IGl0ZW1zLWNlbnRlciBnYXAtMSByb3VuZGVkLW1kIGJvcmRlciBib3JkZXItc2xhdGUtNzAwIGJnLXNsYXRlLTgwMC80MCBweC0yIHB5LTEgdGV4dC14cyB0ZXh0LXNsYXRlLTIwMCBob3ZlcjpiZy1zbGF0ZS03MDAvNjAiCiAgICAgICAgICAgID4KICAgICAgICAgICAgICA8UmVmcmVzaEN3IGNsYXNzTmFtZT0idy0zLjUgaC0zLjUiICAvPgogICAgICAgICAgICAgIERlcGxveSBhZ2FpbgogICAgICAgICAgICA8L2J1dHRvbj4KICAgICAgICAgICl9CiAgICAgICAgPC9kaXY+CiAgICAgIDwvZGl2PgoKICAgICAgey8qIFdBU00gc291cmNlIHN1bW1hcnkgKi99CiAgICAgIDxkaXYgY2xhc3NOYW1lPSJyb3VuZGVkLWxnIGJvcmRlciBib3JkZXItc2xhdGUtNzAwLzYwIGJnLXNsYXRlLTgwMC8zMCBweC0zIHB5LTIuNSI+CiAgICAgICAgPGRpdiBjbGFzczNOYW1lPSJmbGV4IGl0ZW1zLWNlbnRlciBqdXN0aWZ5LWJldHdlZW4gZ2FwLTIiPgogICAgICAgICAgPHNwYW4gY2xhc3NOYW1lPSJ0ZXh0LXhzIGZvbnQtbWVkaXVtIHRleHQtc2xhdGUtMzAwIj5XQVNNIGJ5dGVjb2RlPC9zcGFuPgogICAgICAgICAge3dhc21TaXplID4gMCA/ICgKICAgICAgICAgICAgPHNwYW4gY2xhc3NOYW1lPSJmb250LW1vbm8gdGV4dC1bMTFweF0gdGV4dC1zbGF0ZS00MDAiPgogICAgICAgICAgICAgIHt3YXNtU2l6ZS50b0xvY2FsZVN0cmluZygpfSBieXRlcyDCtyB7d2FzbUhhc2hQcmV2aWV3fS4uLgogICAgICAgICAgICA8L3NwYW4+CiAgICAgICAgICApIDogKAogICAgICAgICAgICA8c3BhbiBjbGFzc05hbWU9InRleHQtWzExcHhdIHRleHQtYW1iZXItNDAwIj5ObyBXQVNNIGxvYWRlZDwvc3Bhbj4KICAgICAgICAgICl9CiAgICAgICAgPC9kaXY+CiAgICAgIDwvZGl2PgoKICAgICAgey8qIERlcGxveWVyIGtleSAqL30KICAgICAgPGRpdiBjbGFzc05hbWU9InNwYWNlLXktMiI+CiAgICAgICAgPGxhYmVsIGNsYXNzTmFtZT0iYmxvY2sgdGV4dC14cyBmb250LW1lZGl1bSB0ZXh0LXNsYXRlLTMwMCI+RGVwbG95ZXIgc2VjcmV0IGtleTwvbGFiZWw+CiAgICAgICAgPGlucHV0CiAgICAgICAgICB0eXBlPSJwYXNzd29yZCIKICAgICAgICAgIGF1dG9Db21wbGV0ZT0ib2ZmIgogICAgICAgICAgc3BlbGxDaGVjaz1mYWxzZQogICAgICAgICAgY2xhc3NOYW1lPSJ3LWZ1bGwgcm91bmRlZC1tZCБib3JkZXIgYm9yZGVyLXNsYXRlLTcwMCBiZy1zbGF0ZS05MDAvNDAgcHgtMyBweS0yIHRleHQteHMgZm9udC1tb25vIHRleHQtc2xhdGUtMTAwIHBsYWNlaG9sZGVyOnRleHQtc2xhdGUtNjAwIGZvY3VzOm91dGxpbmUtbm9uZSBmb2N1czpib3JkZXItYmx1ZS01MDAiCiAgICAgICAgICBwbGFjZWhvbGRlcj0iUy4uLiIKICAgICAgICAgIHZhbHVlPXtzZWNyZXRLZXlJbnB1dH0KICAgICAgICAgIG9uQ2hhbmdlPXsoZSkgPT4gc2V0U2VjcmV0S2V5SW5wdXQoZS50YXJnZXQudmFsdWUpfQogICAgICAgIC8+CiAgICAgICAgPHAgY2xhc3NOYW1lPSJ0ZXh0LVsxMHB4XSB0ZXh0LXNsYXRlLTUwMCI+CiAgICAgICAgICBLZXlzIGFyZSBrZXB0IGluIG1lbW9yeSBvbmx5IGFuZCBuZXZlciBzZW50IGFueXdoZXJlIGV4Y2VwdCB0aGUgU29yb2JhbiBSLVBDLgogICAgICAgIDwvcD4KICAgICAgPC9kaXY+CgogICAgICB7LyogQ29uc3RydWN0b3IgYXJndW1lbnRzICovfQogICAgICA8QXJndW1lbnRFZGl0b3IKICAgICAgICB0aXRsZT0iQ29uc3RydWN0b3IgYXJndW1lbnRzIgogICAgICAgIGRlc2NyaXB0aW9uPSJQYXNzZWQgdG8gdGhlIGNvbnRyYWN0IGF0IGluc3RhbnRpYXRpb24gdGltZS4iCiAgICAgICAgYXJncz17bG9jYWxDb25zdHJ1Y3RvckFyZ3N9CiAgICAgICAgb25DaGFuZ2U9e3NldExvY2FsQ29uc3RydWN0b3JBcmdzfQogICAgICAvPgoKICAgICAgey8qIEluaXRpYWxpemUgYXJndW1lbnRzICovfQogICAgICA8QXJndW1lbnRFZGl0b3IKICAgICAgICB0aXRsZT0iSW5pdGlhbGl6ZSBhcmd1bWVudHMgKG9wdGlvbmFsKSIKICAgICAgICBkZXNjcmlwdGlvbj0iSW52b2tlZCBhZnRlciBpbnN0YW50aWF0aW9uIHRvIHNlZWQgaW5pdGlhbCBjb250cmFjdCBzdGF0ZS4iCiAgICAgICAgYXJncz17bG9jYWxJbml0aWFsaXplQXJnc30KICAgICAgICBvbkNoYW5nZT17c2V0TG9jYWxJbml0aWFsaXplQXJnc30KICAgICAgLz4KCiAgICAgIHsvKiBTdGFnZSB0cmFja2VyICovfQogICAgICA8ZGl2IGNsYXNzTmFtZT0ic3BhY2UteS0yIj4KICAgICAgICB7c3RhZ2VPcmRlci5tYXAoKHN0YWdlLCBpbmRleCkgPT4gKAogICAgICAgICAgPFN0YWdlSW5kaWNhdG9yIGtleT17c3RhZ2V9IHN0YWdlPXtzdGF0ZS5zdGFnZXNbc3RhZ2VdfSBpbmRleD17aW5kZXh9IC8+CiAgICAgICAgKSl9CiAgICAgIDwvZGl2PgoKICAgICAgey8qIExvZ3MgKi99CiAgICAgIDxkaXYgY2xhc3NOYW1lPSJzcGFjZS15LTEiPgogICAgICAgIDxkaXYgY2xhc3NOYW1lPSJmbGV4IGl0ZW1zLWNlbnRlciBnYXAtMiB0ZXh0LXhzIHRleHQtc2xhdGUtNDAwIj4KICAgICAgICAgIDxUZXJtaW5hbCBjbGFzc05hbWU9InctMy41IGgtMy41IiAvPgogICAgICAgICAgPHNwYW4+RGVwbG95bWVudCBsb2c8L3NwYW4+CiAgICAgICAgPC9kaXY+CiAgICAgICAgPGRpdgogICAgICAgICAgcmVmPXtsb2dSZWZ9CiAgICAgICAgICBjbGFzc05hbWU9Im1heC1oLTM2IG92ZXJmbG93LXktYXV0byByb3VuZGVkLWxnIGJvcmRlciBib3JkZXItc2xhdGUtODAwIGJnLWJsYWNrLzUwIHAtMyBmb250LW1vbm8gdGV4dC1bMTFweF0gbGVhZGluZy1yZWxheGVkIHRleHQtc2xhdGUtMzAwIgogICAgICAgID4KICAgICAgICAgIHtzdGF0ZS5sb2dzLmxlbmd0aCA9PT0gMCA/ICgKICAgICAgICAgICAgPHNwYW4gY2xhc3NOYW1lPSJ0ZXh0LXNsYXRlLTYwMCI+V2FpdGluZyBmb3IgZGVwbG95bWVudOKApjwvc3Bhbj4KICAgICAgICAgICkgOiAoCiAgICAgICAgICAgIHN0YXRlLmxvZ3MubWFwKChsb2csIGkpID0+ICgKICAgICAgICAgICAgICA8ZGl2CiAgICAgICAgICAgICAgICBrZXk9e2l9CiAgICAgICAgICAgICAgICBjbGFzc05hbWU9ewogICAgICAgICAgICAgICAgICBsb2cubGV2ZWwgPT09ICdlcnJvcicKICAgICAgICAgICAgICAgICAgICA/ICd0ZXh0LXJlZC00MDAnCiAgICAgICAgICAgICAgICAgICAgOiBsb2cubGV2ZWwgPT09ICdzdWNjZXNzJwogICAgICAgICAgICAgICAgICAgICAgPyAndGV4dC1lbWVyYWxkLTQwMCcKICAgICAgICAgICAgICAgICAgICAgIDogJ3RleHQtc2xhdGUtMzAwJwogICAgICAgICAgICAgICAgfQogICAgICAgICAgICAgID4KICAgICAgICAgICAgICAgIDxzcGFuIGNsYXNzTmFtZT0idGV4dC1zbGF0ZS02MDAiPgogICAgICAgICAgICAgICAgICB7bmV3IERhdGUobG9nLnRpbWVzdGFtcCkudG9Mb2NhbGVUaW1lU3RyaW5nKCl9IAogICAgICAgICAgICAgICAgPC9zcGFuPgogICAgICAgICAgICAgICAge2xvZy5tZXNzYWdlfQogICAgICAgICAgICAgIDwvZGl2PgogICAgICAgICAgICApKQogICAgICAgICAgKX0KICAgICAgICA8L2Rpdj4KICAgICAgPC9kaXY+CgogICAgICB7LyogRXJyb3IgKi99CiAgICAgIHtzdGF0ZS5lcnJvciAmJiAoCiAgICAgICAgPGRpdiBjbGFzc05hbWU9InJvdW5kZWQtbGcgYm9yZGVyIGJvcmRlci1yZWQtNTAwLzQwIGJnLXJlZC01MDAvMTAgcHgtMyBweS0yIHRleHQteHMgdGV4dC1yZWQtMzAwIj4KICAgICAgICAgIHtzdGF0ZS5lcnJvcn0KICAgICAgICA8L2Rpdj4KICAgICAgKX0KCiAgICAgIHsvKiBSZXN1bHQgKi99CiAgICAgIHtzdGF0ZS5yZXN1bHQgJiYgKAogICAgICAgIDxkaXYgY2xhc3NOYW1lPSJzcGFjZS15LTIgcm91bmRlZC1sZyBib3JkZXIgYm9yZGVyLWVtZXJhbGQtNTAwLzQwIGJnLWVtZXJhbGQtNTAwLzUgcC0zIj4KICAgICAgICAgIDxkaXYgY2xhc3NOYW1lPSJmbGV4IGl0ZW1zLWNlbnRlciBnYXAtMiI+CiAgICAgICAgICAgIDxDaGVja0NpcmNsZSBjbGFzc05hbWU9InctNCBoLTQgdGV4dC1lbWVyYWxkLTQwMCIgLz4KICAgICAgICAgICAgPHNwYW4gY2xhc3NOYW1lPSJ0ZXh0LXNtIGZvbnQtc2VtaWJvbGQgdGV4dC1lbWVyYWxkLTIwMCI+CiAgICAgICAgICAgICAge3N0YXRlLnJlc3VsdC52ZXJpZmllZCA/ICdWZXJpZmllZCBvbiBTdGVsbGFyIEV4cGVydCcgOiAnRGVwbG95ZWQnfQogICAgICAgICAgICA8L3NwYW4+CiAgICAgICAgICA8L2Rpdj4KCiAgICAgICAgICA8ZGl2IGNsYXNzTmFtZT0ic3BhY2UteS0xIj4KICAgICAgICAgICAgPGRpdiBjbGFzc05hbWU9ImZsZXggaXRlbXMtY2VudGVyIGp1c3RpZnktYmV0d2VlbiBnYXAtMiI+CiAgICAgICAgICAgICAgPHNwYW4gY2xhc3NOYW1lPSJ0ZXh0LVsxMHB4XSB1cHBlcmNhc2UgdHJhY2tpbmctd2lkZSB0ZXh0LXNsYXRlLTQwMCI+CiAgICAgICAgICAgICAgICBDb250cmFjdCBJRAogICAgICAgICAgICAgIDwvc3Bhbj4KICAgICAgICAgICAgICA8Q29weUJ1dHRvbiB2YWx1ZT17c3RhdGUucmVzdWx0LmNvbnRyYWN0SWR9IGxhYmVsPSJDb3B5IGNvbnRyYWN0IElEIiAvPgogICAgICAgICAgICA8L2Rpdj4KICAgICAgICAgICAgPGRpdiBjbGFzc05hbWU9ImZvbnQtbW9ubyB0ZXh0LVsxMXB4XSB0ZXh0LXNsYXRlLTIwMCBicmVhay1hbGwiPgogICAgICAgICAgICAgIHtzdGF0ZS5yZXN1bHQuY29udHJhY3RJZH0KICAgICAgICAgICAgPC9kaXY+CiAgICAgICAgICA8L2Rpdj4KCiAgICAgICAgICA8ZGl2IGNsYXNzTmFtZT0iZ3JpZCBncmlkLWNvbHMtMiBnYXAtMiB0ZXh0LVsxMHB4XSB0ZXh0LXNsYXRlLTQwMCI+CiAgICAgICAgICAgIDxkaXY+CiAgICAgICAgICAgICAgPHNwYW4gY2xhc3NOYW1lPSJibG9jayB1cHBlcmNhc2UgdHJhY2tpbmctd2lkZSI+V0FTTSBoYXNoPC9zcGFuPgogICAgICAgICAgICAgIDxzcGFuIGNsYXNzTmFtZT0iZm9udC1tb25vIHRleHQtc2xhdGUtMzAwIGJyZWFrLWFsbCI+e3N0YXRlLnJlc3VsdC53YXNtSGFzaH08L3NwYW4+CiAgICAgICAgICAgIDwvZGl2PgogICAgICAgICAgICA8ZGl2PgogICAgICAgICAgICAgIDxzcGFuIGNsYXNzTmFtZT0iYmxvY2sgdXBwZXJjYXNlIHRyYWNraW5nLXdpZGUiPkluc3RhbGwgdHg8L3NwYW4+CiAgICAgICAgICAgICAgPHNwYW4gY2xhc3NOYW1lPSJmb250LW1vbm8gdGV4dC1zbGF0ZS0zMDAgYnJlYWstYWxsIj57c3RhdGUucmVzdWx0Lmluc3RhbGxUeEhhc2h9PC9zcGFuPgogICAgICAgICAgICA8L2Rpdj4KICAgICAgICAgICAgPGRpdj4KICAgICAgICAgICAgICA8c3BhbiBjbGFzc05hbWU9ImJsb2NrIHVwcGVyY2FzZSB0cmFja2luZy13aWRlIj5JbnN0YW50aWF0ZSB0eDwvc3Bhbj4KICAgICAgICAgICAgICA8c3BhbiBjbGFzc05hbWU9ImZvbnQtbW9ubyB0ZXh0LXNsYXRlLTMwMCBicmVhay1hbGwiPntzdGF0ZS5yZXN1bHQuaW5zdGFudGlhdGVUeEhhc2h9PC9zcGFuPgogICAgICAgICAgICA8L2Rpdj4KICAgICAgICAgICAge3N0YXRlLnJlc3VsdC5pbml0aWFsaXplVHhIYXNoICYmICgKICAgICAgICAgICAgICA8ZGl2PgogICAgICAgICAgICAgICAgPHNwYW4gY2xhc3NOYW1lPSJibG9jayB1cHBlcmNhc2UgdHJhY2tpbmctd2lkZSI+SW5pdGlhbGl6ZSB0eDwvc3Bhbj4KICAgICAgICAgICAgICAgIDxzcGFuIGNsYXNzTmFtZT0iZm9udC1tb25vIHRleHQtc2xhdGUtMzAwIGJyZWFrLWFsbCI+e3N0YXRlLnJlc3VsdC5pbml0aWFsaXplVHhIYXNofTwvc3Bhbj4KICAgICAgICAgICAgICA8L2Rpdj4KICAgICAgICAgICAgKX0KICAgICAgICAgIDwvZGl2PgoKICAgICAgICAgIDxhCiAgICAgICAgICAgIGhyZWY9e3N0YXRlLnJlc3VsdC5leHBsb3JlclVybH0KICAgICAgICAgICAgdGFyZ2V0PSJfYmxhbmsiCiAgICAgICAgICAgIHJlbD0ibm9vcGVuZXIgbm9yZWZlcnJlciIKICAgICAgICAgICAgY2xhc3NOYW1lPSJpbmxpbmUtZmxleCBpdGVtcy1jZW50ZXIgZ2FwLTEgcm91bmRlZC1tZCBib3JkZXIgYm9yZGVyLWVtZXJhbGQtNTAwLzQwIGJnLWVtZXJhbGQtNTAwLzEwIHB4LTIgcHktMSB0ZXh0LXhzIHRleHQtZW1lcmFsZC0yMDAgaG92ZXI6YmctZW1lcmFsZC01MDAvMjAiCiAgICAgICAgICA+CiAgICAgICAgICAgIDxFeHRlcm5hbExpbmsgY2xhc3NOYW1lPSJ3LTMuNSBoLTMuNSIgLz4KICAgICAgICAgICAgVmlldyBvbiBTdGVsbGFyIEV4cGVydAogICAgICAgICAgPC9hPgogICAgICAgIDwvZGl2PgogICAgICApfQoKICAgICAgey8qIEFjdGlvbnMgKi99CiAgICAgIDxkaXYgY2xhc3NOYW1lPSJmbGV4IGl0ZW1zLWNlbnRlciBnYXAtMiI+CiAgICAgICAgPGJ1dHRvbgogICAgICAgICAgdHlwZT0iYnV0dG9uIgogICAgICAgICAgb25DbGljaz17aGFuZGxlRGVwbG95fQogICAgICAgICAgZGlzYWJsZWQ9eyFjYW5EZXBsb3l9CiAgICAgICAgICBjbGFzc05hbWU9ewogICAgICAgICAgICAnaW5saW5lLWZsZXggaXRlbXMtY2VudGVyIGdhcC0yIHJvdW5kZWQtbGcgcHgtNCBweS0yIHRleHQtc20gZm9udC1tZWRpdW0gdHJhbnNpdGlvbi1jb2xvcnMgJyArCiAgICAgICAgICAgIChjYW5EZXBsb3kKICAgICAgICAgICAgICA/ICdiZy1ibHVlLTYwMCB0ZXh0LXdoaXRlIGhvdmVyOmJnLWJsdWUtNTAwJwogICAgICAgICAgICAgIDogJ2N1cnNvci1ub3QtYWxsb3dlZCBiZy1zbGF0ZS04MDAgdGV4dC1zbGF0ZS01MDAnKQogICAgICAgICAgfQogICAgICAgID4KICAgICAgICAgIHtzdGF0ZS5pc1J1bm5pbmcgPyAoCiAgICAgICAgICAgIDw+CiAgICAgICAgICAgICAgPExvYWRlcjIgY2xhc3NOYW1lPSJ3LTQgaC00IGFuaW1hdGUtc3BpbiIgLz4KICAgICAgICAgICAgICBEZXBsb3lpbmfigKYKICAgICAgICAgICAgPC8+CiAgICAgICAgICApIDogKAogICAgICAgICAgICA8PgogICAgICAgICAgICAgIDxQbGF5IGNsYXNzTmFtZT0idy00IGgtNCIgLz4KICAgICAgICAgICAgICBEZXBsb3kgQ29udHJhY3QKICAgICAgICAgICAgPC8+CiAgICAgICAgICApfQogICAgICAgIDwvYnV0dG9uPgogICAgICA8L2Rpdj4KICAgIDwvZGl2PgogICk7Cn0K \ No newline at end of file +'use client'; + +import React, { useCallback, useEffect, useMemo, useReducer, useRef, useState } from 'react'; +import { Check, CheckCircle, ClipboardCopy, ExternalLink, Loader2, Play, RefreshCw, Terminal, XCircle, Rocket } from 'lucide-react'; + +// ------------------------------------------------------------------------------ +// Types +// ------------------------------------------------------------------------------ + +export type DeployStage = 'upload' | 'install' | 'instantiate' | 'initialize'; + +export type StageStatus = 'pending' | 'active' | 'success' | 'error'; + +export interface StageState { + stage: DeployStage; + status: StageStatus; + label: string; + description: string; + message?: string; + hash?: string; + startedAt?: number; + finishedAt?: number; +} + +export interface ConstructorArgument { + name: string; + type: 'string' | 'number' | 'boolean' | 'address' | 'symbol' | 'bytes'; + value: string; + required?: boolean; + description?: string; +} + +export interface DeployResult { + contractId: string; + wasmHash: string; + installTxHash: string; + instantiateTxHash: string; + initializeTxHash?: string; + explorerUrl: string; + verified: boolean; +} + +export interface DeployPanelProps { + /** WASM bytes to deploy (from build or upload). */ + wasmBytes?: Uint8Array | ArrayBuffer | null; + /** Name of the contract for display/logging. */ + contractName?: string; + /** Stellar network to deploy to. */ + network?: 'testnet' | 'futurenet' | 'mainnet'; + /** Soroban RPC URL to use. */ + rpcUrl?: string; + /** Network passphrase for transaction signing. */ + networkPassphrase?: string; + /** Secret key of the deployer account (Stellar S...) */ + secretKey?: string; + /** Public key of the deployer account (G...) */ + publicKey?: string; + /** Constructor arguments to pass to the contract. */ + constructorArgs?: ConstructorArgument[]; + /** Optional initialization arguments (for the initialize call). */ + initializeArgs?: ConstructorArgument[]; + /** Called when deployment completes successfully. */ + onDeployed?: (result: DeployResult) => void; + /** Called on any error. */ + onError?: (error: Error) => void; + /** Optional external deploy function (for testing/injection). */ + deployOverride?: (input: DeployInput) => Promise; +} + +export interface DeployInput { + wasmBytes: Uint8Array; + contractName: string; + network: 'testnet' | 'futurenet' | 'mainnet'; + rpcUrl: string; + networkPassphrase: string; + secretKey: string; + publicKey: string; + constructorArgs: ConstructorArgument[]; + initializeArgs: ConstructorArgument[]; + onStageUpdate: (stage: DeployStage, update: Partial) => void; +} + +// ------------------------------------------------------------------------------ +// Network configuration +// ------------------------------------------------------------------------------ + +const NETWORK_CONFIGS: Record = { + testnet: { + rpcUrl: 'https://soroban-testnet.stellar.org', + passphrase: 'Test Stellar Network ; September 2015', + explorer: 'https://stellar.expert/explorer/testnet', + }, + futurenet: { + rpcUrl: 'https://soroban-futurenet.stellar.org', + passphrase: 'Test Stellar Network ; September 2015', + explorer: 'https://stellar.expert/explorer/futurenet', + }, + mainnet: { + rpcUrl: 'https://soroban-mainnet.stellar.org', + passphrase: 'Public Global Stellar Network ; September 2015', + explorer: 'https://stellar.expert/explorer/public', + }, +}; + +const STAGE_DEFINITIONS: Array<{ stage: DeployStage; label: string; description: string }> = [ + { + stage: 'upload', + label: 'Upload WASM', + description: 'Upload compiled WASM bytecode to the network', + }, + { + stage: 'install', + label: 'Install Contract', + description: 'Install the WASM and create a contract instance', + }, + { + stage: 'instantiate', + label: 'Instantiate', + description: 'Create the contract instance with constructor arguments', + }, + { + stage: 'initialize', + label: 'Initialize State', + description: 'Invoke the contract initializer and verify on Stellar Expert', + }, +]; + +// ------------------------------------------------------------------------------ +// Helpers +// ------------------------------------------------------------------------------ + +function toUint8Array(input: Uint8Array | ArrayBuffer | null | undefined): Uint8Array { + if (!input) return new Uint8Array(0); + if (input instanceof Uint8Array) return input; + return new Uint8Array(input); +} + +function bytesToHex(bytes: Uint8Array): string { + return Array.from(bytes) + .map((b) => b.toString(16).padStart(2, '0')) + .join(''); +} + +function buildExplorerUrl(network: string, contractId: string): string { + const config = NETWORK_CONFIGS[network] ?? NETWORK_CONFIGS.testnet; + return `${config.explorer}/contract/${contractId}`; +} + +// ------------------------------------------------------------------------------ +// Reducer +// ------------------------------------------------------------------------------ + +type State = { + stages: Record; + isRunning: boolean; + result: DeployResult | null; + error: string | null; + logs: Array<{ timestamp: number; message: string; level: 'info' | 'error' | 'success' }>; +}; + +type Action = + | { type: 'RESET' } + | { type: 'START_RUN' } + | { type: 'STAGE_UPDATE'; stage: DeployStage; update: Partial } + | { type: 'SUCCESS'; result: DeployResult } + | { type: 'FAILURE'; error: string; stage?: DeployStage } + | { type: 'LOG'; message: string; level?: 'info' | 'error' | 'success' }; + +const initialStages = (): Record => { + const entries = STAGE_DEFINITIONS.map((def) => [ + def.stage, + { stage: def.stage, status: 'pending' as StageStatus, label: def.label, description: def.description }, + ]); + return Object.fromEntries(entries) as Record; +}; + +const initialState: State = { + stages: initialStages(), + isRunning: false, + result: null, + error: null, + logs: [], +}; + +function reducer(state: State, action: Action): State { + switch (action.type) { + case 'RESET': + return { + ...initialState, + stages: initialStages(), + logs: [], + }; + case 'START_RUN': + return { + ...state, + isRunning: true, + error: null, + result: null, + stages: initialStages(), + logs: [ + ...state.logs, + { timestamp: Date.now(), message: 'Deployment pipeline started', level: 'info' }, + ], + }; + case 'STAGE_UPDATE': + return { + ...state, + stages: { + ...state.stages, + [action.stage]: { + ...state.stages[action.stage], + ...action.update, + }, + }, + }; + case 'SUCCESS': + return { + ...state, + isRunning: false, + result: action.result, + logs: [ + ...state.logs, + { timestamp: Date.now(), message: `Deployment complete: ${action.result.contractId}`, level: 'success' }, + ], + }; + case 'FAILURE': + return { + ...state, + isRunning: false, + error: action.error, + stages: action.stage + ? { + ...state.stages, + [action.stage]: { ...state.stages[action.stage], status: 'error', message: action.error }, + } + : state.stages, + logs: [ + ...state.logs, + { timestamp: Date.now(), message: `Deployment failed: ${action.error}`, level: 'error' }, + ], + }; + case 'LOG': + return { + ...state, + logs: [ + ...state.logs, + { timestamp: Date.now(), message: action.message, level: action.level ?? 'info' }, + ], + }; + default: + return state; + } +} + +// ------------------------------------------------------------------------------ +// Sub-components +// ------------------------------------------------------------------------------ + +function StageIndicator({ stage, index }: { stage: StageState; index: number }) { + const icon = (() => { + switch (stage.status) { + case 'success': + return ; + case 'error': + return ; + case 'active': + return ; + default: + return ( + + {index + 1} + + ); + } + })(); + + const borderColor = + stage.status === 'success' + ? 'border-emerald-500/40' + : stage.status === 'error' + ? 'border-red-500/40' + : stage.status === 'active' + ? 'border-blue-500/60 bg-blue-500/5' + : 'border-slate-700/60'; + + return ( +
+
{icon}
+
+
+ {stage.label} + {stage.hash && ( + + {stage.hash.slice(0, 10)}... + + )} +
+

{stage.description}

+ {stage.message && ( +

+ {stage.message} +

+ )} +
+
+ ); +} + +function CopyButton({ value, label }: { value: string; label?: string }) { + const [copied, setCopied] = useState(false); + const handleCopy = useCallback(async () => { + try { + await navigator.clipboard.writeText(value); + setCopied(true); + setTimeout(() => setCopied(false), 1500); + } catch { + // clipboard unavailable - silently ignore + } + }, [value]); + + return ( + + ); +} + +function ArgumentEditor({ + args, + onChange, + title, + description, +}: { + args: ConstructorArgument[]; + onChange: (args: ConstructorArgument[]) => void; + title: string; + description?: string; +}) { + const updateArg = (index: number, patch: Partial) => { + const next = args.slice(); + next[index] = { ...next[index], ...patch }; + onChange(next); + }; + + const addArg = () => { + onChange([ + ...args, + { name: `arg${args.length + 1}`, type: 'string', value: '', required: true }, + ]); + }; + + const removeArg = (index: number) => { + onChange(args.filter((_, i) => i !== index)); + }; + + return ( +
+
+
+

{title}

+ {description &&

{description}

} +
+ +
+ + {args.length === 0 && ( +

+ No arguments. Add one if the constructor requires inputs. +

+ )} + +
+ {args.map((arg, index) => ( +
+ updateArg(index, { name: e.target.value })} + /> + + updateArg(index, { value: e.target.value })} + /> + +
+ ))} +
+
+ ); +} + +// ------------------------------------------------------------------------------ +// Main component +// ------------------------------------------------------------------------------ + +export function DeployPanel({ + wasmBytes, + contractName = 'untitled-contract', + network = 'testnet', + rpcUrl, + networkPassphrase, + secretKey, + publicKey, + constructorArgs, + initializeArgs, + onDeployed, + onError, + deployOverride, +}: DeployPanelProps) { + const [state, dispatch] = useReducer(reducer, initialState); + const [localConstructorArgs, setLocalConstructorArgs] = useState(constructorArgs ?? []); + const [localInitializeArgs, setLocalInitializeArgs] = useState(initializeArgs ?? []); + const [secretKeyInput, setSecretKeyInput] = useState(secretKey ?? ''); + const logRef = useRef(null); + + const networkConfig = NETWORK_CONFIGS[network] ?? NETWORK_CONFIGS.testnet; + const effectiveRpcUrl = rpcUrl ?? networkConfig.rpcUrl; + const effectivePassphrase = networkPassphrase ?? networkConfig.passphrase; + + const wasmUint8 = useMemo(() => toUint8Array(wasmBytes), [wasmBytes]); + const wasmSize = wasmUint8.byteLength; + const wasmHashPreview = useMemo(() => (wasmSize > 0 ? bytesToHex(wasmUint8.slice(0, 8)) : ''), [wasmUint8, wasmSize]); + + const canDeploy = useMemo(() => { + if (state.isRunning) return false; + if (wasmSize === 0) return false; + if (!secretKeyInput.trim()) return false; + const missing = localConstructorArgs.some((a) => a.required && !a.value.trim()); + return !missing; + }, [state.isRunning, wasmSize, secretKeyInput, localConstructorArgs]); + + useEffect(() => { + if (logRef.current) { + logRef.current.scrollTop = logRef.current.scrollHeight; + } + }, [state.logs]); + + const handleStageUpdate = useCallback( + (stage: DeployStage, update: Partial) => { + dispatch({ type: 'STAGE_UPDATE', stage, update }); + if (update.message) { + dispatch({ + type: 'LOG', + message: `[${stage}] ${update.message}`, + level: update.status === 'error' ? 'error' : 'info', + }); + } + }, + [], + ); + + const handleDeploy = useCallback(async () => { + if (!canDeploy) return; + dispatch({ type: 'START_RUN' }); + + const input: DeployInput = { + wasmBytes: wasmUint8, + contractName, + network, + rpcUrl: effectiveRpcUrl, + networkPassphrase: effectivePassphrase, + secretKey: secretKeyInput.trim(), + publicKey: publicKey ?? '', + constructorArgs: localConstructorArgs, + initializeArgs: localInitializeArgs, + onStageUpdate: handleStageUpdate, + }; + + try { + if (!deployOverride) { + throw new Error('Deployment pipeline requires live Soroban connection or deployOverride in test mode.'); + } + const result = await deployOverride(input); + dispatch({ type: 'SUCCESS', result }); + onDeployed?.(result); + } catch (err) { + const message = err instanceof Error ? err.message : String(err); + dispatch({ type: 'FAILURE', error: message }); + onError?.(err instanceof Error ? err : new Error(message)); + } + }, [ + canDeploy, + wasmUint8, + contractName, + network, + effectiveRpcUrl, + effectivePassphrase, + secretKeyInput, + publicKey, + localConstructorArgs, + localInitializeArgs, + handleStageUpdate, + deployOverride, + onDeployed, + onError, + ]); + + const handleReset = useCallback(() => { + dispatch({ type: 'RESET' }); + }, []); + + const stageOrder = STAGE_DEFINITIONS.map((s) => s.stage); + + return ( +
+
+
+ +
+

Deploy Contract

+

+ {contractName} · {network} +

+
+
+
+ {state.result && ( + + )} +
+
+ + {/* WASM source summary */} +
+
+ WASM bytecode + {wasmSize > 0 ? ( + + {wasmSize.toLocaleString()} bytes · {wasmHashPreview}... + + ) : ( + No WASM loaded + )} +
+
+ + {/* Deployer key */} +
+ + setSecretKeyInput(e.target.value)} + /> +

+ Keys are kept in memory only and never sent anywhere except the Soroban RPC. +

+
+ + {/* Constructor arguments */} + + + {/* Initialize arguments */} + + + {/* Stage tracker */} +
+ {stageOrder.map((stage, index) => ( + + ))} +
+ + {/* Logs */} +
+
+ + Deployment log +
+
+ {state.logs.length === 0 ? ( + Waiting for deployment… + ) : ( + state.logs.map((log, i) => ( +
+ + {new Date(log.timestamp).toLocaleTimeString()}{' '} + + {log.message} +
+ )) + )} +
+
+ + {/* Error */} + {state.error && ( +
+ {state.error} +
+ )} + + {/* Result */} + {state.result && ( +
+
+ + + {state.result.verified ? 'Verified on Stellar Expert' : 'Deployed'} + +
+ +
+
+ + Contract ID + + +
+
+ {state.result.contractId} +
+
+ +
+
+ WASM hash + {state.result.wasmHash} +
+
+ Install tx + {state.result.installTxHash} +
+
+ Instantiate tx + {state.result.instantiateTxHash} +
+ {state.result.initializeTxHash && ( +
+ Initialize tx + {state.result.initializeTxHash} +
+ )} +
+ + + + View on Stellar Expert + +
+ )} + + {/* Actions */} +
+ +
+
+ ); +} + +export default DeployPanel; diff --git a/frontend/src/components/playground/__tests__/PlaygroundIntegration.test.tsx b/frontend/src/components/playground/__tests__/PlaygroundIntegration.test.tsx new file mode 100644 index 000000000..d12a212e2 --- /dev/null +++ b/frontend/src/components/playground/__tests__/PlaygroundIntegration.test.tsx @@ -0,0 +1,156 @@ +import React from 'react'; +import { render, screen, fireEvent, waitFor } from '@testing-library/react'; +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { DeployPanel } from '../DeployPanel'; +import { CallPanel } from '../CallPanel'; +import { StorageViewer } from '../StorageViewer'; +import { WalletModal } from '@/components/wallet/WalletModal'; +import { WalletProvider } from '@/contexts/WalletContext'; + +// Mock Freighter API +vi.mock('@stellar/freighter-api', () => ({ + isConnected: vi.fn().mockResolvedValue({ isConnected: true }), + requestAccess: vi.fn().mockResolvedValue({ address: 'GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF' }), + getAddress: vi.fn().mockResolvedValue({ address: 'GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF' }), + getPublicKey: vi.fn().mockResolvedValue('GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF'), + signTransaction: vi.fn().mockResolvedValue('AAAA...signedXDR'), + getNetwork: vi.fn().mockResolvedValue('TESTNET'), +})); + +describe('Interactive Playground Components Integration Suite', () => { + beforeEach(() => { + vi.clearAllMocks(); + Object.defineProperty(navigator, 'clipboard', { + value: { + writeText: vi.fn().mockResolvedValue(undefined), + }, + writable: true, + configurable: true, + }); + }); + + describe('DeployPanel component', () => { + it('renders the deployment panel with stages and handles deploy override', async () => { + const mockDeployOverride = vi.fn().mockResolvedValue({ + contractId: 'CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM', + wasmHash: 'aabbcc112233', + installTxHash: 'tx_install_123', + instantiateTxHash: 'tx_instantiate_123', + explorerUrl: 'https://stellar.expert/explorer/testnet/contract/CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM', + verified: true, + }); + + const onDeployed = vi.fn(); + + render( + + ); + + expect(screen.getByRole('heading', { level: 2, name: /Deploy Contract/i })).toBeInTheDocument(); + expect(screen.getByText(/hello-soroban/i)).toBeInTheDocument(); + expect(screen.getByText('Upload WASM')).toBeInTheDocument(); + expect(screen.getByText('Install Contract')).toBeInTheDocument(); + expect(screen.getByText('Instantiate')).toBeInTheDocument(); + + const deployBtn = screen.getByRole('button', { name: /Deploy Contract/i }); + expect(deployBtn).not.toBeDisabled(); + + fireEvent.click(deployBtn); + + await waitFor(() => { + expect(mockDeployOverride).toHaveBeenCalledTimes(1); + expect(onDeployed).toHaveBeenCalledTimes(1); + }); + + expect(screen.getByText('Verified on Stellar Expert')).toBeInTheDocument(); + expect(screen.getByText('CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM')).toBeInTheDocument(); + }); + + it('disables deploy button when required fields are missing', () => { + render( + + ); + + const deployBtn = screen.getByRole('button', { name: /Deploy Contract/i }); + expect(deployBtn).toBeDisabled(); + }); + }); + + describe('CallPanel component', () => { + it('renders function selector and invokes functions successfully', async () => { + const onInvoke = vi.fn().mockResolvedValue({ message: 'Hello World' }); + + render( + + ); + + expect(screen.getByText('Invoke Contract Function')).toBeInTheDocument(); + expect(screen.getByText('CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM')).toBeInTheDocument(); + expect(screen.getByDisplayValue('greet')).toBeInTheDocument(); + + const input = screen.getByPlaceholderText('Value for name'); + fireEvent.change(input, { target: { value: 'Stellar' } }); + + const invokeBtn = screen.getByRole('button', { name: /Invoke/i }); + fireEvent.click(invokeBtn); + + await waitFor(() => { + expect(onInvoke).toHaveBeenCalledWith('greet', { name: 'Stellar' }); + }); + + expect(await screen.findByText(/Hello World/i)).toBeInTheDocument(); + }); + }); + + describe('StorageViewer component', () => { + it('renders storage tabs and table properly', () => { + render(); + + expect(screen.getByText(/Soroban State Diff Inspector/i)).toBeInTheDocument(); + expect(screen.getByText(/FE-HARD-20/i)).toBeInTheDocument(); + }); + }); + + describe('WalletModal component', () => { + it('does not render when closed', () => { + render( + + + + ); + + expect(screen.queryByText('Connect Stellar Wallet')).not.toBeInTheDocument(); + }); + + it('renders wallet options and allows closing when open', () => { + const onClose = vi.fn(); + render( + + + + ); + + expect(screen.getByText('Connect Stellar Wallet')).toBeInTheDocument(); + expect(screen.getByText('Freighter')).toBeInTheDocument(); + + const closeBtn = screen.getByRole('button', { name: /Close modal/i }); + fireEvent.click(closeBtn); + expect(onClose).toHaveBeenCalledTimes(1); + }); + }); +}); diff --git a/frontend/src/components/wallet/WalletModal.tsx b/frontend/src/components/wallet/WalletModal.tsx new file mode 100644 index 000000000..dc3a0442f --- /dev/null +++ b/frontend/src/components/wallet/WalletModal.tsx @@ -0,0 +1,100 @@ +'use client'; + +import React from 'react'; +import { X, Wallet, CheckCircle2, AlertCircle } from 'lucide-react'; +import { useWallet, WALLET_PROVIDERS } from '@/contexts/WalletContext'; + +export interface WalletModalProps { + isOpen: boolean; + onClose: () => void; +} + +export function WalletModal({ isOpen, onClose }: WalletModalProps) { + const { publicKey, activeWallet, isConnecting, error, connect, disconnect } = useWallet(); + + if (!isOpen) return null; + + return ( +
+
+ + +
+ +

Connect Stellar Wallet

+
+ + {error && ( +
+ + {error} +
+ )} + + {publicKey ? ( +
+
+
+ Connected +
+

Active Provider: {activeWallet}

+

+ {publicKey} +

+
+ + +
+ ) : ( +
+

+ Select a wallet provider to connect your account: +

+ {WALLET_PROVIDERS.map((provider) => ( + + ))} +
+ )} +
+
+ ); +} + +export default WalletModal; diff --git a/frontend/src/mocks/freighter.ts b/frontend/src/mocks/freighter.ts new file mode 100644 index 000000000..380018bd9 --- /dev/null +++ b/frontend/src/mocks/freighter.ts @@ -0,0 +1,10 @@ +import { vi } from 'vitest'; + +export const mockFreighter = { + isConnected: vi.fn().mockResolvedValue({ isConnected: true }), + requestAccess: vi.fn().mockResolvedValue({ address: 'GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF' }), + getAddress: vi.fn().mockResolvedValue({ address: 'GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF' }), + getPublicKey: vi.fn().mockResolvedValue('GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF'), + signTransaction: vi.fn().mockResolvedValue('AAAA...signedXDR'), + getNetwork: vi.fn().mockResolvedValue('TESTNET'), +};