2026-02-18 00:19:05 +01:00
import type { Page , BrowserContext } from '@playwright/test' ;
import { writeFileSync , mkdirSync , readFileSync , existsSync } from 'fs' ;
import { join } from 'path' ;
// Global snapshot state for chain resets - persisted to disk
const SNAPSHOT_FILE = join ( process . cwd ( ) , 'tmp' , '.chain-snapshot-id' ) ;
let initialSnapshotId : string | null = null ;
let currentSnapshotId : string | null = null ;
// Load snapshot ID from disk if it exists
function loadSnapshotId ( ) : string | null {
try {
if ( existsSync ( SNAPSHOT_FILE ) ) {
return readFileSync ( SNAPSHOT_FILE , 'utf-8' ) . trim ( ) ;
}
} catch ( e ) {
console . warn ( ` [CHAIN] Could not read snapshot file: ${ e } ` ) ;
}
return null ;
}
// Save snapshot ID to disk
function saveSnapshotId ( id : string ) : void {
try {
mkdirSync ( join ( process . cwd ( ) , 'tmp' ) , { recursive : true } ) ;
writeFileSync ( SNAPSHOT_FILE , id , 'utf-8' ) ;
} catch ( e ) {
console . warn ( ` [CHAIN] Could not write snapshot file: ${ e } ` ) ;
}
}
/ * *
* Reset chain state using evm_snapshot / evm_revert
* On first call : takes the initial snapshot ( clean state ) and saves to disk
* On subsequent calls : reverts to initial snapshot , then takes a new snapshot
* This preserves deployed contracts but resets balances and pool state to initial conditions
*
* Snapshot ID is persisted to disk so it survives module reloads between tests
* /
export async function resetChainState ( rpcUrl : string ) : Promise < void > {
// Try to load from disk first (in case module was reloaded)
if ( ! initialSnapshotId ) {
initialSnapshotId = loadSnapshotId ( ) ;
if ( initialSnapshotId ) {
console . log ( ` [CHAIN] Loaded initial snapshot from disk: ${ initialSnapshotId } ` ) ;
}
}
if ( initialSnapshotId ) {
// Revert to the initial snapshot
console . log ( ` [CHAIN] Reverting to initial snapshot ${ initialSnapshotId } ... ` ) ;
const revertRes = await fetch ( rpcUrl , {
method : 'POST' ,
headers : { 'content-type' : 'application/json' } ,
body : JSON.stringify ( {
jsonrpc : '2.0' ,
method : 'evm_revert' ,
params : [ initialSnapshotId ] ,
id : 1
} )
} ) ;
const revertData = await revertRes . json ( ) ;
if ( ! revertData . result ) {
// Revert failed - clear snapshot file and take a fresh one
console . error ( ` [CHAIN] Revert FAILED: ${ JSON . stringify ( revertData ) } ` ) ;
console . log ( ` [CHAIN] Clearing snapshot file and taking fresh snapshot... ` ) ;
initialSnapshotId = null ;
// Fall through to take fresh snapshot below
} else {
console . log ( ` [CHAIN] Reverted successfully to initial state ` ) ;
// After successful revert, take a new snapshot (anvil consumes the old one)
console . log ( '[CHAIN] Taking new snapshot after successful revert...' ) ;
const newSnapshotRes = await fetch ( rpcUrl , {
method : 'POST' ,
headers : { 'content-type' : 'application/json' } ,
body : JSON.stringify ( {
jsonrpc : '2.0' ,
method : 'evm_snapshot' ,
params : [ ] ,
id : 1
} )
} ) ;
const newSnapshotData = await newSnapshotRes . json ( ) ;
currentSnapshotId = newSnapshotData . result ;
// CRITICAL: Update initialSnapshotId because anvil consumed it during revert
initialSnapshotId = currentSnapshotId ;
saveSnapshotId ( initialSnapshotId ) ;
console . log ( ` [CHAIN] New initial snapshot taken (replaces consumed one): ${ initialSnapshotId } ` ) ;
return ;
}
}
// First call OR revert failed: take initial snapshot of CURRENT state
console . log ( '[CHAIN] Taking FIRST initial snapshot...' ) ;
const snapshotRes = await fetch ( rpcUrl , {
method : 'POST' ,
headers : { 'content-type' : 'application/json' } ,
body : JSON.stringify ( {
jsonrpc : '2.0' ,
method : 'evm_snapshot' ,
params : [ ] ,
id : 1
} )
} ) ;
const snapshotData = await snapshotRes . json ( ) ;
initialSnapshotId = snapshotData . result ;
currentSnapshotId = initialSnapshotId ;
saveSnapshotId ( initialSnapshotId ) ;
console . log ( ` [CHAIN] Initial snapshot taken and saved to disk: ${ initialSnapshotId } ` ) ;
}
export interface TestReport {
personaName : string ;
testDate : string ;
pagesVisited : Array < {
page : string ;
url : string ;
timeSpent : number ; // milliseconds
timestamp : string ;
} > ;
actionsAttempted : Array < {
action : string ;
success : boolean ;
error? : string ;
timestamp : string ;
} > ;
screenshots : string [ ] ;
uiObservations : string [ ] ;
copyFeedback : string [ ] ;
tokenomicsQuestions : string [ ] ;
overallSentiment : string ;
}
/ * *
* Connect wallet using the injected test provider
* /
export async function connectWallet ( page : Page ) : Promise < void > {
console . log ( '[HELPER] Connecting wallet...' ) ;
2026-03-25 09:29:53 +00:00
2026-02-18 00:19:05 +01:00
// Wait for Vue app to mount (increased timeout for post-chain-reset scenarios)
const navbarTitle = page . locator ( '.navbar-title' ) . first ( ) ;
await navbarTitle . waitFor ( { state : 'visible' , timeout : 60_000 } ) ;
2026-03-25 09:29:53 +00:00
2026-03-06 08:47:22 +00:00
// Trigger resize event for mobile detection; connectButton.isVisible below waits for layout
2026-02-18 00:19:05 +01:00
await page . evaluate ( ( ) = > {
window . dispatchEvent ( new Event ( 'resize' ) ) ;
} ) ;
2026-03-06 08:15:48 +00:00
2026-03-25 09:29:53 +00:00
// Wait for wagmi to settle — the connect button or connected display must appear.
// After the wallet-provider fix, eth_accounts returns [] when not connected,
// so wagmi should land on 'disconnected' status and render the connect button.
2026-02-18 00:19:05 +01:00
const connectButton = page . locator ( '.connect-button--disconnected' ) . first ( ) ;
2026-03-25 09:29:53 +00:00
const connectedButton = page . locator ( '.connect-button--connected' ) . first ( ) ;
// Wait for either the disconnect button (normal case) or connected button (auto-reconnect)
const desktopButton = page . locator ( '.connect-button--disconnected, .connect-button--connected' ) . first ( ) ;
if ( await desktopButton . isVisible ( { timeout : 10_000 } ) ) {
if ( await connectedButton . isVisible ( { timeout : 500 } ) . catch ( ( ) = > false ) ) {
// Wallet already connected (e.g. wagmi reconnected from storage) — skip connect flow
console . log ( '[HELPER] Wallet already connected (auto-reconnect)' ) ;
} else {
// Desktop connect button found — click to open connector panel
console . log ( '[HELPER] Found desktop Connect button' ) ;
await connectButton . click ( ) ;
// Wait for the connector panel to open — .connectors-element appearing is the observable event
const injectedConnector = page . locator ( '.connectors-element' ) . first ( ) ;
await injectedConnector . waitFor ( { state : 'visible' , timeout : 10_000 } ) ;
console . log ( '[HELPER] Clicking wallet connector...' ) ;
await injectedConnector . click ( ) ;
}
2026-02-18 00:19:05 +01:00
} else {
// Try mobile fallback
const mobileLoginIcon = page . locator ( '.navbar-end svg' ) . first ( ) ;
if ( await mobileLoginIcon . isVisible ( { timeout : 2_000 } ) ) {
console . log ( '[HELPER] Using mobile login icon' ) ;
await mobileLoginIcon . click ( ) ;
const injectedConnector = page . locator ( '.connectors-element' ) . first ( ) ;
2026-03-06 08:47:22 +00:00
await injectedConnector . waitFor ( { state : 'visible' , timeout : 10_000 } ) ;
await injectedConnector . click ( ) ;
2026-02-18 00:19:05 +01:00
}
}
2026-03-25 09:29:53 +00:00
2026-02-18 00:19:05 +01:00
// Verify wallet is connected
const walletDisplay = page . getByText ( /0x[a-fA-F0-9]{4}/i ) . first ( ) ;
await walletDisplay . waitFor ( { state : 'visible' , timeout : 15_000 } ) ;
console . log ( '[HELPER] Wallet connected successfully' ) ;
}
/ * *
* Mint ETH on the local Anvil fork ( via RPC , not UI )
* This is a direct RPC call to anvil_setBalance
* /
export async function mintEth (
page : Page ,
rpcUrl : string ,
recipientAddress : string ,
amount : string = '10'
) : Promise < void > {
console . log ( ` [HELPER] Minting ${ amount } ETH to ${ recipientAddress } via RPC... ` ) ;
const amountWei = BigInt ( parseFloat ( amount ) * 1 e18 ) . toString ( 16 ) ;
const paddedAmount = '0x' + amountWei . padStart ( 64 , '0' ) ;
const response = await fetch ( rpcUrl , {
method : 'POST' ,
headers : { 'content-type' : 'application/json' } ,
body : JSON.stringify ( {
jsonrpc : '2.0' ,
method : 'anvil_setBalance' ,
params : [ recipientAddress , paddedAmount ] ,
id : 1
} )
} ) ;
const result = await response . json ( ) ;
if ( result . error ) {
throw new Error ( ` Failed to mint ETH: ${ result . error . message } ` ) ;
}
console . log ( ` [HELPER] ETH minted successfully via RPC ` ) ;
}
// Helper: send RPC call and return result
async function sendRpc ( rpcUrl : string , method : string , params : unknown [ ] ) : Promise < string > {
const resp = await fetch ( rpcUrl , {
method : 'POST' ,
headers : { 'content-type' : 'application/json' } ,
body : JSON.stringify ( { jsonrpc : '2.0' , id : Date.now ( ) , method , params } )
} ) ;
const data = await resp . json ( ) ;
if ( data . error ) throw new Error ( ` RPC ${ method } failed: ${ data . error . message } ` ) ;
return data . result ;
}
// Helper: wait for transaction receipt
async function waitForReceipt ( rpcUrl : string , txHash : string , timeoutMs = 15000 ) : Promise < any > {
const start = Date . now ( ) ;
while ( Date . now ( ) - start < timeoutMs ) {
const resp = await fetch ( rpcUrl , {
method : 'POST' ,
headers : { 'content-type' : 'application/json' } ,
body : JSON.stringify ( { jsonrpc : '2.0' , id : 1 , method : 'eth_getTransactionReceipt' , params : [ txHash ] } )
} ) ;
const data = await resp . json ( ) ;
if ( data . result ) return data . result ;
2026-03-03 20:58:01 +00:00
// eslint-disable-next-line no-restricted-syntax -- Polling with timeout: no event source for transaction receipt over HTTP RPC (eth_subscribe not available). See AGENTS.md #Engineering Principles.
2026-02-18 00:19:05 +01:00
await new Promise ( r = > setTimeout ( r , 500 ) ) ;
}
throw new Error ( ` Transaction ${ txHash } not mined within ${ timeoutMs } ms ` ) ;
}
/ * *
* Fund a wallet with KRK tokens by transferring from the deployer ( Anvil # 0 ) .
* On the local fork , the deployer holds the initial KRK supply .
* The ethAmount parameter is kept for API compatibility but controls KRK amount
* ( 1 ETH ≈ 1000 KRK at the ~ 0.01 initialization price ) .
* /
export async function buyKrk (
page : Page ,
ethAmount : string ,
rpcUrl : string = 'http://localhost:8545' ,
privateKey? : string
) : Promise < void > {
const deployments = JSON . parse ( readFileSync ( join ( process . cwd ( ) , 'onchain' , 'deployments-local.json' ) , 'utf-8' ) ) ;
const krkAddress = deployments . contracts . Kraiken ;
// Determine recipient address
let walletAddr : string ;
if ( privateKey ) {
const { Wallet } = await import ( 'ethers' ) ;
walletAddr = new Wallet ( privateKey ) . address ;
} else {
walletAddr = '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266' ; // default Anvil #0
}
// Transfer KRK from deployer (Anvil #0) to recipient
// Give 100 KRK per "ETH" parameter (deployer has ~2K KRK after bootstrap)
const krkAmount = Math . min ( parseFloat ( ethAmount ) * 100 , 500 ) ;
const { ethers } = await import ( 'ethers' ) ;
const provider = new ethers . JsonRpcProvider ( rpcUrl ) ;
const DEPLOYER_KEY = '0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80' ;
const deployer = new ethers . Wallet ( DEPLOYER_KEY , provider ) ;
const krk = new ethers . Contract ( krkAddress , [
'function transfer(address,uint256) returns (bool)' ,
'function balanceOf(address) view returns (uint256)'
] , deployer ) ;
const amount = ethers . parseEther ( krkAmount . toString ( ) ) ;
console . log ( ` [HELPER] Transferring ${ krkAmount } KRK to ${ walletAddr } ... ` ) ;
const tx = await krk . transfer ( walletAddr , amount ) ;
await tx . wait ( ) ;
const balance = await krk . balanceOf ( walletAddr ) ;
console . log ( ` [HELPER] KRK balance: ${ ethers . formatEther ( balance ) } KRK ` ) ;
}
/ * *
* Take an annotated screenshot with a description
* /
export async function takeScreenshot (
page : Page ,
personaName : string ,
moment : string ,
report : TestReport
) : Promise < void > {
const timestamp = new Date ( ) . toISOString ( ) . replace ( /[:.]/g , '-' ) ;
const filename = ` ${ personaName . toLowerCase ( ) . replace ( /\s+/g , '-' ) } - ${ moment . toLowerCase ( ) . replace ( /\s+/g , '-' ) } - ${ timestamp } .png ` ;
const dirPath = join ( 'test-results' , 'usertest' , personaName . toLowerCase ( ) . replace ( /\s+/g , '-' ) ) ;
try {
mkdirSync ( dirPath , { recursive : true } ) ;
} catch ( e ) {
// Directory may already exist
}
const filepath = join ( dirPath , filename ) ;
await page . screenshot ( { path : filepath , fullPage : true } ) ;
report . screenshots . push ( filepath ) ;
console . log ( ` [SCREENSHOT] ${ moment } : ${ filepath } ` ) ;
}
/ * *
* Log a persona observation ( what they think / feel )
* /
export function logObservation ( personaName : string , observation : string , report : TestReport ) : void {
const message = ` [ ${ personaName } ] ${ observation } ` ;
console . log ( message ) ;
report . uiObservations . push ( observation ) ;
}
/ * *
* Log copy / messaging feedback
* /
export function logCopyFeedback ( personaName : string , feedback : string , report : TestReport ) : void {
const message = ` [ ${ personaName } - COPY] ${ feedback } ` ;
console . log ( message ) ;
report . copyFeedback . push ( feedback ) ;
}
/ * *
* Log a tokenomics question the persona would have
* /
export function logTokenomicsQuestion ( personaName : string , question : string , report : TestReport ) : void {
const message = ` [ ${ personaName } - TOKENOMICS] ${ question } ` ;
console . log ( message ) ;
report . tokenomicsQuestions . push ( question ) ;
}
/ * *
* Record a page visit
* /
export function recordPageVisit (
pageName : string ,
url : string ,
startTime : number ,
report : TestReport
) : void {
const timeSpent = Date . now ( ) - startTime ;
report . pagesVisited . push ( {
page : pageName ,
url ,
timeSpent ,
timestamp : new Date ( ) . toISOString ( ) ,
} ) ;
}
/ * *
* Record an action attempt
* /
export function recordAction (
action : string ,
success : boolean ,
error : string | undefined ,
report : TestReport
) : void {
report . actionsAttempted . push ( {
action ,
success ,
error ,
timestamp : new Date ( ) . toISOString ( ) ,
} ) ;
}
/ * *
* Write the final report to JSON
* /
export function writeReport ( personaName : string , report : TestReport ) : void {
const dirPath = join ( process . cwd ( ) , 'tmp' , 'usertest-results' ) ;
try {
mkdirSync ( dirPath , { recursive : true } ) ;
} catch ( e ) {
// Directory may already exist
}
const filename = ` ${ personaName . toLowerCase ( ) . replace ( /\s+/g , '-' ) } .json ` ;
const filepath = join ( dirPath , filename ) ;
writeFileSync ( filepath , JSON . stringify ( report , null , 2 ) , 'utf-8' ) ;
console . log ( ` [REPORT] Written to ${ filepath } ` ) ;
}
/ * *
* Create a new test report
* /
export function createReport ( personaName : string ) : TestReport {
return {
personaName ,
testDate : new Date ( ) . toISOString ( ) ,
pagesVisited : [ ] ,
actionsAttempted : [ ] ,
screenshots : [ ] ,
uiObservations : [ ] ,
copyFeedback : [ ] ,
tokenomicsQuestions : [ ] ,
overallSentiment : '' ,
} ;
}
/ * *
* New feedback structure for redesigned tests
* /
export interface PersonaFeedback {
persona : string ;
test : 'A' | 'B' ;
timestamp : string ;
journey : 'passive-holder' | 'staker' ;
steps : Array < {
step : string ;
screenshot? : string ;
feedback : string [ ] ;
} > ;
overall : {
wouldBuy? : boolean ;
wouldReturn? : boolean ;
wouldStake? : boolean ;
friction : string [ ] ;
} ;
}
/ * *
* Create new feedback structure
* /
export function createPersonaFeedback (
persona : string ,
test : 'A' | 'B' ,
journey : 'passive-holder' | 'staker'
) : PersonaFeedback {
return {
persona ,
test ,
timestamp : new Date ( ) . toISOString ( ) ,
journey ,
steps : [ ] ,
overall : {
friction : [ ]
}
} ;
}
/ * *
* Add a step to persona feedback
* /
export function addFeedbackStep (
feedback : PersonaFeedback ,
step : string ,
observations : string [ ] ,
screenshot? : string
) : void {
feedback . steps . push ( {
step ,
screenshot ,
feedback : observations
} ) ;
}
/ * *
* Write persona feedback to JSON
* /
export function writePersonaFeedback ( feedback : PersonaFeedback ) : void {
const dirPath = join ( process . cwd ( ) , 'tmp' , 'usertest-results' ) ;
try {
mkdirSync ( dirPath , { recursive : true } ) ;
} catch ( e ) {
// Directory may already exist
}
const filename = ` ${ feedback . persona . toLowerCase ( ) } -test- ${ feedback . test . toLowerCase ( ) } .json ` ;
const filepath = join ( dirPath , filename ) ;
writeFileSync ( filepath , JSON . stringify ( feedback , null , 2 ) , 'utf-8' ) ;
console . log ( ` [FEEDBACK] Written to ${ filepath } ` ) ;
}
/ * *
* Navigate to stake page and attempt to stake
* /
export async function attemptStake (
page : Page ,
amount : string ,
taxRateIndex : string ,
personaName : string ,
report : TestReport
) : Promise < void > {
console . log ( ` [ ${ personaName } ] Attempting to stake ${ amount } KRK at tax rate ${ taxRateIndex } %... ` ) ;
2026-03-26 08:17:17 +00:00
const origin = new URL ( page . url ( ) ) . origin ;
await page . goto ( ` ${ origin } /stake ` ) ;
2026-03-06 08:15:48 +00:00
2026-02-18 00:19:05 +01:00
try {
// Wait for stake form to fully load
const tokenAmountSlider = page . getByRole ( 'slider' , { name : 'Token Amount' } ) ;
await tokenAmountSlider . waitFor ( { state : 'visible' , timeout : 15_000 } ) ;
// Wait for KRK balance to load in UI (critical — without this, button shows "Insufficient Balance")
console . log ( ` [ ${ personaName } ] Waiting for KRK balance to load in UI... ` ) ;
try {
await page . waitForFunction ( ( ) = > {
const balEl = document . querySelector ( '.balance' ) ;
if ( ! balEl ) return false ;
const text = balEl . textContent || '' ;
const match = text . match ( /([\d,.]+)/ ) ;
return match && parseFloat ( match [ 1 ] . replace ( /,/g , '' ) ) > 0 ;
} , { timeout : 150_000 } ) ;
const balText = await page . locator ( '.balance' ) . first ( ) . textContent ( ) ;
console . log ( ` [ ${ personaName } ] Balance loaded: ${ balText } ` ) ;
} catch ( e ) {
console . log ( ` [ ${ personaName } ] WARNING: Balance did not load within 90s — staking may fail ` ) ;
}
// Fill amount
const stakeAmountInput = page . getByLabel ( 'Staking Amount' ) ;
await stakeAmountInput . waitFor ( { state : 'visible' , timeout : 10_000 } ) ;
await stakeAmountInput . fill ( amount ) ;
2026-03-06 08:15:48 +00:00
2026-02-18 00:19:05 +01:00
// Select tax rate
const taxSelect = page . getByRole ( 'combobox' , { name : 'Tax' } ) ;
await taxSelect . selectOption ( { value : taxRateIndex } ) ;
2026-03-06 08:47:22 +00:00
2026-02-18 00:19:05 +01:00
// Take screenshot before attempting to click
const screenshotDir = join ( 'test-results' , 'usertest' , personaName . toLowerCase ( ) . replace ( /\s+/g , '-' ) ) ;
mkdirSync ( screenshotDir , { recursive : true } ) ;
const timestamp = new Date ( ) . toISOString ( ) . replace ( /[:.]/g , '-' ) ;
const screenshotPath = join ( screenshotDir , ` stake-form-filled- ${ timestamp } .png ` ) ;
await page . screenshot ( { path : screenshotPath , fullPage : true } ) ;
report . screenshots . push ( screenshotPath ) ;
console . log ( ` [ ${ personaName } ] Screenshot: ${ screenshotPath } ` ) ;
// Find ALL buttons in the stake form to see actual state
const allButtons = await page . getByRole ( 'main' ) . getByRole ( 'button' ) . all ( ) ;
const buttonTexts = await Promise . all (
allButtons . map ( async ( btn ) = > {
try {
return await btn . textContent ( ) ;
} catch {
return null ;
}
} )
) ;
console . log ( ` [ ${ personaName } ] Available buttons: ${ buttonTexts . filter ( Boolean ) . join ( ', ' ) } ` ) ;
// Check for error state buttons
const buttonText = buttonTexts . join ( ' ' ) ;
if ( buttonText . includes ( 'Insufficient Balance' ) ) {
const errorMsg = 'Cannot stake: Insufficient KRK balance. Buy more KRK first.' ;
console . log ( ` [ ${ personaName } ] ${ errorMsg } ` ) ;
recordAction ( ` Stake ${ amount } KRK at ${ taxRateIndex } % tax ` , false , errorMsg , report ) ;
throw new Error ( errorMsg ) ;
}
if ( buttonText . includes ( 'Stake Amount Too Low' ) ) {
const errorMsg = 'Cannot stake: Amount is below minimum stake requirement.' ;
console . log ( ` [ ${ personaName } ] ${ errorMsg } ` ) ;
recordAction ( ` Stake ${ amount } KRK at ${ taxRateIndex } % tax ` , false , errorMsg , report ) ;
throw new Error ( errorMsg ) ;
}
if ( buttonText . includes ( 'Tax Rate Too Low' ) ) {
const errorMsg = 'Cannot stake: No open positions at this tax rate. Increase tax rate.' ;
console . log ( ` [ ${ personaName } ] ${ errorMsg } ` ) ;
recordAction ( ` Stake ${ amount } KRK at ${ taxRateIndex } % tax ` , false , errorMsg , report ) ;
throw new Error ( errorMsg ) ;
}
// Wait for stake button with longer timeout
const stakeButton = page . getByRole ( 'main' ) . getByRole ( 'button' , { name : /^(Stake|Snatch and Stake)$/i } ) ;
await stakeButton . waitFor ( { state : 'visible' , timeout : 15_000 } ) ;
const finalButtonText = await stakeButton . textContent ( ) ;
console . log ( ` [ ${ personaName } ] Clicking button: " ${ finalButtonText } " ` ) ;
await stakeButton . click ( ) ;
// Wait for transaction
try {
await page . getByRole ( 'button' , { name : /Sign Transaction|Waiting/i } ) . waitFor ( { state : 'visible' , timeout : 5_000 } ) ;
await page . getByRole ( 'button' , { name : /^(Stake|Snatch and Stake)$/i } ) . waitFor ( { state : 'visible' , timeout : 60_000 } ) ;
} catch ( e ) {
// May complete instantly
}
recordAction ( ` Stake ${ amount } KRK at ${ taxRateIndex } % tax ` , true , undefined , report ) ;
console . log ( ` [ ${ personaName } ] Stake successful ` ) ;
} catch ( error : any ) {
recordAction ( ` Stake ${ amount } KRK at ${ taxRateIndex } % tax ` , false , error . message , report ) ;
console . log ( ` [ ${ personaName } ] Stake failed: ${ error . message } ` ) ;
throw error ;
}
}