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 && (
+
+ )}
+
+
+ );
+}
+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.
+
+ )}
+
+
+
+ );
+}
+
+// ------------------------------------------------------------------------------
+// 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 && (
+
+ )}
+
+ {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'),
+};