@coinmasters/e2e-staking-suite 1.11.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/index.ts ADDED
@@ -0,0 +1,909 @@
1
+ /*
2
+ E2E Staking Test Suite
3
+
4
+ This test suite focuses on Cosmos staking operations:
5
+ - Delegate to validators
6
+ - Undelegate from validators
7
+ - Claim staking rewards
8
+ - Validator API testing
9
+
10
+ ============================================================================
11
+ ๐Ÿ”ง CONFIGURATION - Edit TEST_CONFIG object to enable/disable specific tests
12
+ ============================================================================
13
+
14
+ TEST_VALIDATOR_API: Test validator fetching APIs
15
+ TEST_STAKING_POSITIONS_API: Test staking positions detection
16
+ TEST_DIRECT_STAKING_API: Test direct staking API calls
17
+ TEST_CLAIM_REWARDS: Test reward claiming (๐ŸŽฏ RECOMMENDED TO START)
18
+ TEST_DELEGATE: Test delegation transactions
19
+ TEST_UNDELEGATE: Test undelegation transactions
20
+ TEST_STAKING_INTEGRATION: Test staking data integration
21
+ ACTUALLY_EXECUTE_TRANSACTIONS: Set to true to actually execute transactions
22
+ NETWORKS: Array of networks to test
23
+
24
+ Example for reward claiming only:
25
+ {
26
+ TEST_CLAIM_REWARDS: true,
27
+ TEST_DELEGATE: false,
28
+ TEST_UNDELEGATE: false,
29
+ ACTUALLY_EXECUTE_TRANSACTIONS: true, // To actually claim rewards
30
+ NETWORKS: ['GAIA'] // Cosmos Hub only
31
+ }
32
+ */
33
+
34
+ require("dotenv").config()
35
+ require('dotenv').config({path:"../../.env"});
36
+ require('dotenv').config({path:"./../../.env"});
37
+ require("dotenv").config({path:'../../../.env'})
38
+ require("dotenv").config({path:'../../../../.env'})
39
+
40
+ const TAG = " | staking-e2e-test | "
41
+ // @ts-ignore
42
+ import { shortListSymbolToCaip, caipToNetworkId, networkIdToCaip } from '@pioneer-platform/pioneer-caip';
43
+ import { getChainEnumValue } from "@coinmasters/types";
44
+ const log = require("@pioneer-platform/loggerdog")()
45
+ let assert = require('assert')
46
+ let SDK = require('@coinmasters/pioneer-sdk')
47
+ let wait = require('wait-promise');
48
+ let {ChainToNetworkId} = require('@pioneer-platform/pioneer-caip');
49
+ let sleep = wait.sleep;
50
+
51
+ import {
52
+ getPaths,
53
+ // @ts-ignore
54
+ } from '@pioneer-platform/pioneer-coins';
55
+
56
+ interface StakingPosition {
57
+ type: 'delegation' | 'reward' | 'unbonding';
58
+ balance: string;
59
+ ticker: string;
60
+ valueUsd: number;
61
+ validator: string;
62
+ validatorAddress: string;
63
+ networkId: string;
64
+ caip: string;
65
+ }
66
+
67
+ interface Validator {
68
+ address: string;
69
+ moniker: string;
70
+ commission: string;
71
+ status: string;
72
+ tokens: string;
73
+ description?: {
74
+ moniker: string;
75
+ identity: string;
76
+ website: string;
77
+ details: string;
78
+ };
79
+ }
80
+
81
+ const test_staking_service = async function () {
82
+ let tag = TAG + " | test_staking_service | "
83
+ try {
84
+ console.time('staking-test-start');
85
+
86
+ // ๐ŸŽฏ Simple Configuration - Focus on reward claiming
87
+ const TEST_CONFIG = {
88
+ TEST_VALIDATOR_API: false,
89
+ TEST_STAKING_POSITIONS_API: true, // Need to find existing delegations
90
+ TEST_DIRECT_STAKING_API: true, // ๐Ÿ” Enable to check direct API
91
+ TEST_CLAIM_REWARDS: true, // ๐ŸŽฏ Main focus - claim rewards
92
+ TEST_DELEGATE: false,
93
+ TEST_UNDELEGATE: false,
94
+ TEST_STAKING_INTEGRATION: false,
95
+ ACTUALLY_EXECUTE_TRANSACTIONS: true, // ๐Ÿš€ Actually claim rewards
96
+ NETWORKS: ['GAIA'] // Cosmos Hub only
97
+ };
98
+
99
+ log.info(tag, "๐Ÿ”ง Test Configuration:", TEST_CONFIG);
100
+
101
+ // Test configuration
102
+ const queryKey = "sdk:staking-test:"+Math.random();
103
+ log.info(tag, "queryKey: ", queryKey)
104
+
105
+ const username = "staking-user:"+Math.random()
106
+ assert(username)
107
+
108
+ // Use local or remote Pioneer API
109
+ let spec = process.env.PIONEER_SPEC || 'https://pioneers.dev/spec/swagger.json'
110
+ // let spec = 'http://127.0.0.1:9001/spec/swagger.json' // For local testing
111
+
112
+ // Focus on Cosmos chains that support staking
113
+ let stakingChains = TEST_CONFIG.NETWORKS
114
+
115
+ const allByCaip = stakingChains.map(chainStr => {
116
+ const chain = getChainEnumValue(chainStr);
117
+ if (chain) {
118
+ return ChainToNetworkId[chain];
119
+ }
120
+ return;
121
+ }).filter(Boolean);
122
+
123
+ let blockchains = allByCaip
124
+ log.info(tag, "Staking blockchains: ", blockchains)
125
+
126
+ // Get paths for staking chains
127
+ let paths = getPaths(blockchains)
128
+ log.info(tag, "Staking paths: ", paths.length)
129
+
130
+ let config: any = {
131
+ username,
132
+ queryKey,
133
+ spec,
134
+ wss: process.env.VITE_PIONEER_URL_WSS || 'wss://pioneers.dev',
135
+ keepkeyApiKey: process.env.KEEPKEY_API_KEY || 'e4ea6479-5ea4-4c7d-b824-e075101bf9fd',
136
+ keepkeyEndpoint: 'http://127.0.0.1:1647',
137
+ paths,
138
+ blockchains,
139
+ nodes: [],
140
+ pubkeys: [],
141
+ balances: [],
142
+ };
143
+
144
+ log.info(tag, "Initializing Pioneer SDK for staking tests...")
145
+ log.info(tag, "๐ŸŒ WebSocket URL:", config.wss)
146
+ log.info(tag, "๐Ÿ”‘ KeepKey API Key:", config.keepkeyApiKey ? 'configured' : 'missing')
147
+ log.info(tag, "๐Ÿ”Œ KeepKey Endpoint:", config.keepkeyEndpoint)
148
+
149
+ let app = new SDK.SDK(spec, config)
150
+ let resultInit = await app.init({}, {})
151
+
152
+ log.info(tag, "โœ… Pioneer SDK initialized")
153
+
154
+ // Manual step-by-step initialization like working test
155
+ log.info(tag, "๐Ÿ” Starting getGasAssets()...")
156
+ await app.getGasAssets()
157
+ log.info(tag, "โœ… getGasAssets() complete")
158
+
159
+ log.info(tag, "๐Ÿ” Starting getPubkeys()...")
160
+ await app.getPubkeys()
161
+ log.info(tag, "โœ… getPubkeys() complete - pubkeys count:", app.pubkeys.length)
162
+
163
+ log.info(tag, "๐Ÿ” Starting getBalances()...")
164
+ await app.getBalances()
165
+ log.info(tag, "โœ… getBalances() complete - balances count:", app.balances.length)
166
+
167
+ // Verify basic requirements
168
+ assert(app.blockchains, "Blockchains not initialized")
169
+ assert(app.pubkeys, "Pubkeys not initialized")
170
+ assert(app.balances, "Balances not initialized")
171
+ assert(app.pioneer, "Pioneer API not initialized")
172
+
173
+ // Debug: Check what we have after initialization
174
+ log.info(tag, "๐Ÿ“Š After initialization:")
175
+ log.info(tag, ` - Pubkeys: ${app.pubkeys.length}`)
176
+ log.info(tag, ` - Balances: ${app.balances.length}`)
177
+ log.info(tag, ` - KeepKey SDK: ${app.keepKeySdk ? 'initialized' : 'not initialized'}`)
178
+
179
+ if (app.pubkeys.length > 0) {
180
+ log.info(tag, "๐Ÿ”‘ Available pubkeys:")
181
+ app.pubkeys.forEach((pubkey: any, index: number) => {
182
+ log.info(tag, ` ${index + 1}. ${pubkey.address} (${pubkey.networks.join(', ')})`)
183
+ })
184
+ }
185
+
186
+ if (app.balances.length > 0) {
187
+ log.info(tag, "๐Ÿ’ฐ Available balances:")
188
+ app.balances.slice(0, 10).forEach((balance: any, index: number) => {
189
+ log.info(tag, ` ${index + 1}. ${balance.balance} ${balance.ticker} (${balance.type || 'normal'})`)
190
+ })
191
+ }
192
+
193
+ // **TEST 1: Validator API Testing**
194
+ if (TEST_CONFIG.TEST_VALIDATOR_API) {
195
+ log.info(tag, "")
196
+ log.info(tag, "=".repeat(60))
197
+ log.info(tag, "๐Ÿงช TEST 1: Validator API Testing")
198
+ log.info(tag, "=".repeat(60))
199
+ } else {
200
+ log.info(tag, "โญ๏ธ Skipping TEST 1: Validator API Testing")
201
+ }
202
+
203
+ const validatorTests = new Map<string, Validator[]>()
204
+
205
+ if (TEST_CONFIG.TEST_VALIDATOR_API) {
206
+ for (const networkId of blockchains) {
207
+ log.info(tag, `Testing validator API for ${networkId}...`)
208
+
209
+ try {
210
+ let network: string;
211
+ if (networkId === 'cosmos:cosmoshub-4') {
212
+ network = 'cosmos';
213
+ } else if (networkId === 'cosmos:osmosis-1') {
214
+ network = 'osmosis';
215
+ } else {
216
+ log.warn(tag, `Unsupported networkId for validator testing: ${networkId}`)
217
+ continue;
218
+ }
219
+
220
+ // Test GetValidators API
221
+ log.info(tag, `Fetching validators for ${network}...`)
222
+ const validatorsResponse = await app.pioneer.GetValidators({ network })
223
+
224
+ if (validatorsResponse && validatorsResponse.data && validatorsResponse.data.length > 0) {
225
+ const validators = validatorsResponse.data
226
+ log.info(tag, `โœ… Found ${validators.length} validators for ${network}`)
227
+
228
+ // Validate validator structure
229
+ const firstValidator = validators[0]
230
+ assert(firstValidator.address, `Validator missing address`)
231
+ assert(firstValidator.moniker, `Validator missing moniker`)
232
+ assert(firstValidator.commission !== undefined, `Validator missing commission`)
233
+ assert(firstValidator.status, `Validator missing status`)
234
+
235
+ // Store for later use in delegation tests
236
+ validatorTests.set(networkId, validators.slice(0, 3)) // Keep top 3 validators
237
+
238
+ // Log sample validators
239
+ log.info(tag, `Sample validators for ${network}:`)
240
+ validators.slice(0, 3).forEach((validator: Validator, index: number) => {
241
+ log.info(tag, ` ${index + 1}. ${validator.moniker} - ${(parseFloat(validator.commission) * 100).toFixed(2)}% commission`)
242
+ })
243
+ } else {
244
+ log.warn(tag, `โš ๏ธ No validators found for ${network}`)
245
+ }
246
+
247
+ } catch (error) {
248
+ log.error(tag, `โŒ Error fetching validators for ${networkId}:`, error)
249
+ // Continue with other networks
250
+ }
251
+ }
252
+ }
253
+
254
+ // **TEST 2: Existing Staking Positions Detection**
255
+ if (TEST_CONFIG.TEST_STAKING_POSITIONS_API) {
256
+ log.info(tag, "")
257
+ log.info(tag, "=".repeat(60))
258
+ log.info(tag, "๐Ÿงช TEST 2: Existing Staking Positions Detection")
259
+ log.info(tag, "=".repeat(60))
260
+ } else {
261
+ log.info(tag, "โญ๏ธ Skipping TEST 2: Existing Staking Positions Detection")
262
+ }
263
+
264
+ // Find existing staking positions
265
+ let existingStakingPositions: any[] = []
266
+
267
+ if (TEST_CONFIG.TEST_STAKING_POSITIONS_API) {
268
+ existingStakingPositions = app.balances.filter((balance: any) =>
269
+ balance.chart === 'staking' || balance.type === 'delegation' || balance.type === 'reward' || balance.type === 'unbonding'
270
+ )
271
+
272
+ log.info(tag, `Found ${existingStakingPositions.length} existing staking positions`)
273
+
274
+ if (existingStakingPositions.length > 0) {
275
+ log.info(tag, "๐Ÿ“Š Existing staking positions:")
276
+ existingStakingPositions.forEach((position: any, index: number) => {
277
+ log.info(tag, ` ${index + 1}. ${position.type} - ${position.balance} ${position.ticker} (${position.validator || 'N/A'})`)
278
+ })
279
+ } else {
280
+ log.info(tag, "โ„น๏ธ No existing staking positions found")
281
+ }
282
+ }
283
+
284
+ // **TEST 3: Direct Staking Position API Testing**
285
+ if (TEST_CONFIG.TEST_DIRECT_STAKING_API) {
286
+ log.info(tag, "")
287
+ log.info(tag, "=".repeat(60))
288
+ log.info(tag, "๐Ÿงช TEST 3: Direct Staking Position API Testing")
289
+ log.info(tag, "=".repeat(60))
290
+ } else {
291
+ log.info(tag, "โญ๏ธ Skipping TEST 3: Direct Staking Position API Testing")
292
+ }
293
+
294
+ const stakingPositionsByNetwork = new Map<string, StakingPosition[]>()
295
+
296
+ if (TEST_CONFIG.TEST_DIRECT_STAKING_API) {
297
+ for (const networkId of blockchains) {
298
+ log.info(tag, `Testing staking positions API for ${networkId}...`)
299
+
300
+ // Find pubkeys for this network
301
+ const networkPubkeys = app.pubkeys.filter((pubkey: any) =>
302
+ pubkey.networks.includes(networkId)
303
+ )
304
+
305
+ if (networkPubkeys.length === 0) {
306
+ log.warn(tag, `No pubkeys found for ${networkId}`)
307
+ continue
308
+ }
309
+
310
+ try {
311
+ let network: string;
312
+ if (networkId === 'cosmos:cosmoshub-4') {
313
+ network = 'cosmos';
314
+ } else if (networkId === 'cosmos:osmosis-1') {
315
+ network = 'osmosis';
316
+ } else {
317
+ log.warn(tag, `Unsupported networkId for staking positions: ${networkId}`)
318
+ continue;
319
+ }
320
+
321
+ for (const pubkey of networkPubkeys) {
322
+ log.info(tag, `Checking staking positions for ${pubkey.address} on ${network}...`)
323
+
324
+ const stakingResponse = await app.pioneer.GetStakingPositions({
325
+ network: network,
326
+ address: pubkey.address
327
+ })
328
+
329
+ if (stakingResponse && stakingResponse.data && stakingResponse.data.length > 0) {
330
+ const positions = stakingResponse.data
331
+ log.info(tag, `โœ… Found ${positions.length} staking positions for ${pubkey.address}`)
332
+
333
+ // Store positions for later use
334
+ stakingPositionsByNetwork.set(networkId, positions)
335
+
336
+ // Also add to existingStakingPositions for reward claiming
337
+ positions.forEach((position: StakingPosition) => {
338
+ // Add missing fields for compatibility
339
+ if (!position.networkId) position.networkId = networkId
340
+ if (!position.caip) position.caip = networkIdToCaip(networkId) || networkId
341
+ existingStakingPositions.push(position)
342
+ })
343
+
344
+ // Log position details
345
+ positions.forEach((position: StakingPosition, index: number) => {
346
+ log.info(tag, ` ${index + 1}. ${position.type}: ${position.balance} ${position.ticker} (${position.validator})`)
347
+ })
348
+
349
+ // Debug: Log the actual API response structure
350
+ log.info(tag, `Raw API response for ${network}:`, JSON.stringify(positions, null, 2))
351
+
352
+ // Validate position structure
353
+ positions.forEach((position: StakingPosition, index: number) => {
354
+ log.info(tag, `Position ${index + 1} raw data:`, position)
355
+ assert(position.type, 'Position must have type')
356
+ assert(position.balance, 'Position must have balance')
357
+ assert(position.ticker, 'Position must have ticker')
358
+
359
+ if (position.type === 'delegation') {
360
+ if (!position.validatorAddress) {
361
+ log.error(tag, `โŒ CRITICAL: Delegation position missing validatorAddress`)
362
+ log.error(tag, `Position data:`, position)
363
+ throw new Error(`API response missing validatorAddress for delegation position. This is required for transactions.`)
364
+ }
365
+ }
366
+
367
+ if (position.type === 'reward') {
368
+ if (!position.validatorAddress) {
369
+ log.error(tag, `โŒ CRITICAL: Reward position missing validatorAddress`)
370
+ log.error(tag, `Position data:`, position)
371
+ throw new Error(`API response missing validatorAddress for reward position. This is required for claiming rewards.`)
372
+ }
373
+ }
374
+ })
375
+
376
+ break // Found positions for this network, no need to check other pubkeys
377
+ } else {
378
+ log.info(tag, `โ„น๏ธ No staking positions found for ${pubkey.address} on ${network}`)
379
+ }
380
+ }
381
+
382
+ } catch (error) {
383
+ log.error(tag, `โŒ Error fetching staking positions for ${networkId}:`, error)
384
+ // Continue with other networks
385
+ }
386
+ }
387
+ }
388
+
389
+ // **TEST 4: Undelegation Flow Testing**
390
+ if (TEST_CONFIG.TEST_UNDELEGATE) {
391
+ log.info(tag, "")
392
+ log.info(tag, "=".repeat(60))
393
+ log.info(tag, "๐Ÿงช TEST 4: Undelegation Flow Testing")
394
+ log.info(tag, "=".repeat(60))
395
+ } else {
396
+ log.info(tag, "โญ๏ธ Skipping TEST 4: Undelegation Flow Testing")
397
+ }
398
+
399
+ if (TEST_CONFIG.TEST_UNDELEGATE) {
400
+ // Find delegation positions to undelegate from
401
+ const delegationPositions = existingStakingPositions.filter((position: any) =>
402
+ position.type === 'delegation' && parseFloat(position.balance) > 0
403
+ )
404
+
405
+ if (delegationPositions.length > 0) {
406
+ log.info(tag, `Found ${delegationPositions.length} delegation positions available for undelegation`)
407
+
408
+ // Test undelegation with the first delegation position
409
+ const targetDelegation = delegationPositions[0]
410
+ log.info(tag, `๐ŸŽฏ Testing undelegation from: ${targetDelegation.validator}`)
411
+ log.info(tag, `๐Ÿ“Š Current delegation: ${targetDelegation.balance} ${targetDelegation.ticker}`)
412
+
413
+ const networkId = targetDelegation.networkId || caipToNetworkId(targetDelegation.caip)
414
+ const caip = targetDelegation.caip
415
+
416
+ // Set asset context for undelegation
417
+ await app.setAssetContext({ caip })
418
+
419
+ // Calculate small undelegation amount (10% of current delegation)
420
+ const currentBalance = parseFloat(targetDelegation.balance)
421
+ const undelegateAmount = Math.max(0.01, currentBalance * 0.1) // At least 0.01 or 10%
422
+
423
+ log.info(tag, `๐Ÿ“‰ Attempting to undelegate ${undelegateAmount} ${targetDelegation.ticker}...`)
424
+
425
+ try {
426
+ // Build undelegation transaction
427
+ const undelegatePayload = {
428
+ validatorAddress: targetDelegation.validatorAddress || targetDelegation.validator,
429
+ amount: undelegateAmount,
430
+ memo: 'E2E Undelegation Test'
431
+ }
432
+
433
+ log.info(tag, `๐Ÿ”จ Building undelegation transaction...`)
434
+ log.info(tag, `๐Ÿ“ค Payload:`, undelegatePayload)
435
+
436
+ // Test actual undelegation transaction building
437
+ try {
438
+ const unsignedTx = await app.buildUndelegateTx(caip, undelegatePayload)
439
+ log.info(tag, `โœ… Undelegation transaction built successfully`)
440
+ log.info(tag, `๐Ÿ“‹ Transaction structure:`, JSON.stringify(unsignedTx, null, 2))
441
+
442
+ // Validate transaction structure
443
+ assert(unsignedTx.signDoc, 'Transaction must have signDoc')
444
+ assert(unsignedTx.signDoc.msgs, 'Transaction must have messages')
445
+ assert(unsignedTx.signDoc.msgs[0].type === 'cosmos-sdk/MsgUndelegate', 'Must be MsgUndelegate')
446
+ assert(unsignedTx.signDoc.msgs[0].value.delegator_address, 'Must have delegator_address')
447
+ assert(unsignedTx.signDoc.msgs[0].value.validator_address, 'Must have validator_address')
448
+ assert(unsignedTx.signDoc.msgs[0].value.amount, 'Must have amount')
449
+
450
+ log.info(tag, `โœ… Undelegation transaction structure validated`)
451
+
452
+ if (TEST_CONFIG.ACTUALLY_EXECUTE_TRANSACTIONS) {
453
+ log.info(tag, `๐Ÿš€ EXECUTING UNDELEGATION TRANSACTION...`)
454
+ log.info(tag, `โš ๏ธ This will start unbonding period (21 days for Cosmos)`)
455
+
456
+ // Sign the transaction
457
+ log.info(tag, `โœ๏ธ Signing transaction...`)
458
+ const signedTx = await app.signTx({ caip, unsignedTx })
459
+ log.info(tag, `โœ… Transaction signed successfully`)
460
+
461
+ // Broadcast the transaction
462
+ log.info(tag, `๐Ÿ“ก Broadcasting transaction...`)
463
+ const broadcast = await app.broadcastTx(caip, signedTx)
464
+ log.info(tag, `โœ… Transaction broadcasted:`, broadcast)
465
+
466
+ // Follow the transaction
467
+ log.info(tag, `๐Ÿ‘€ Following transaction...`)
468
+ const followResult = await app.followTransaction(caip, broadcast)
469
+ log.info(tag, `โœ… Transaction completed:`, followResult)
470
+
471
+ } else {
472
+ log.info(tag, `โ„น๏ธ Skipping execution - set ACTUALLY_EXECUTE_TRANSACTIONS to true to execute`)
473
+ }
474
+
475
+ } catch (buildError) {
476
+ log.error(tag, `โŒ Error building undelegation transaction:`, buildError)
477
+ // Continue with test - this is expected if no actual delegation exists
478
+ }
479
+
480
+ } catch (error) {
481
+ log.error(tag, `โŒ Error in undelegation flow:`, error)
482
+ }
483
+
484
+ } else {
485
+ log.info(tag, `โ„น๏ธ No delegation positions found for undelegation testing`)
486
+ }
487
+ }
488
+
489
+ // **TEST 5: Delegation Flow Testing**
490
+ if (TEST_CONFIG.TEST_DELEGATE) {
491
+ log.info(tag, "")
492
+ log.info(tag, "=".repeat(60))
493
+ log.info(tag, "๐Ÿงช TEST 5: Delegation Flow Testing")
494
+ log.info(tag, "=".repeat(60))
495
+ } else {
496
+ log.info(tag, "โญ๏ธ Skipping TEST 5: Delegation Flow Testing")
497
+ }
498
+
499
+ if (TEST_CONFIG.TEST_DELEGATE) {
500
+ // Test delegation for each network with available balance
501
+ for (const networkId of blockchains) {
502
+ log.info(tag, `Testing delegation flow for ${networkId}...`)
503
+
504
+ const caip = networkIdToCaip(networkId)
505
+ if (!caip) {
506
+ log.warn(tag, `Could not convert networkId to CAIP: ${networkId}`)
507
+ continue
508
+ }
509
+
510
+ // Find available balance for delegation
511
+ const availableBalance = app.balances.find((balance: any) =>
512
+ balance.caip === caip && balance.chart !== 'staking'
513
+ )
514
+
515
+ if (!availableBalance || parseFloat(availableBalance.balance) <= 0) {
516
+ log.info(tag, `โ„น๏ธ No available balance for delegation on ${networkId}`)
517
+ continue
518
+ }
519
+
520
+ // Get validators for this network
521
+ const validators = validatorTests.get(networkId)
522
+ if (!validators || validators.length === 0) {
523
+ log.info(tag, `โ„น๏ธ No validators available for delegation on ${networkId}`)
524
+ continue
525
+ }
526
+
527
+ // Set asset context
528
+ await app.setAssetContext({ caip })
529
+
530
+ // Calculate small delegation amount
531
+ const currentBalance = parseFloat(availableBalance.balance)
532
+ const delegateAmount = Math.min(0.1, currentBalance * 0.1) // Max 0.1 or 10% of balance
533
+
534
+ if (delegateAmount < 0.01) {
535
+ log.info(tag, `โ„น๏ธ Balance too low for delegation test on ${networkId}`)
536
+ continue
537
+ }
538
+
539
+ // Select first validator for delegation
540
+ const targetValidator = validators[0]
541
+
542
+ log.info(tag, `๐ŸŽฏ Testing delegation to: ${targetValidator.moniker}`)
543
+ log.info(tag, `๐Ÿ’ฐ Available balance: ${currentBalance} ${availableBalance.ticker}`)
544
+ log.info(tag, `๐Ÿ“ˆ Delegation amount: ${delegateAmount} ${availableBalance.ticker}`)
545
+
546
+ try {
547
+ // Build delegation transaction payload
548
+ const delegatePayload = {
549
+ validatorAddress: targetValidator.address,
550
+ amount: delegateAmount,
551
+ memo: 'E2E Delegation Test'
552
+ }
553
+
554
+ log.info(tag, `๐Ÿ”จ Building delegation transaction...`)
555
+ log.info(tag, `๐Ÿ“ค Payload:`, delegatePayload)
556
+
557
+ // Test actual delegation transaction building
558
+ try {
559
+ const unsignedTx = await app.buildDelegateTx(caip, delegatePayload)
560
+ log.info(tag, `โœ… Delegation transaction built successfully`)
561
+ log.info(tag, `๐Ÿ“‹ Transaction structure:`, JSON.stringify(unsignedTx, null, 2))
562
+
563
+ // Validate transaction structure
564
+ assert(unsignedTx.signDoc, 'Transaction must have signDoc')
565
+ assert(unsignedTx.signDoc.msgs, 'Transaction must have messages')
566
+ assert(unsignedTx.signDoc.msgs[0].type === 'cosmos-sdk/MsgDelegate', 'Must be MsgDelegate')
567
+ assert(unsignedTx.signDoc.msgs[0].value.delegator_address, 'Must have delegator_address')
568
+ assert(unsignedTx.signDoc.msgs[0].value.validator_address, 'Must have validator_address')
569
+ assert(unsignedTx.signDoc.msgs[0].value.amount, 'Must have amount')
570
+
571
+ log.info(tag, `โœ… Delegation transaction structure validated`)
572
+
573
+ if (TEST_CONFIG.ACTUALLY_EXECUTE_TRANSACTIONS) {
574
+ log.info(tag, `๐Ÿš€ EXECUTING DELEGATION TRANSACTION...`)
575
+
576
+ // Sign the transaction
577
+ log.info(tag, `โœ๏ธ Signing transaction...`)
578
+ const signedTx = await app.signTx({ caip, unsignedTx })
579
+ log.info(tag, `โœ… Transaction signed successfully`)
580
+
581
+ // Broadcast the transaction
582
+ log.info(tag, `๐Ÿ“ก Broadcasting transaction...`)
583
+ const broadcast = await app.broadcastTx(caip, signedTx)
584
+ log.info(tag, `โœ… Transaction broadcasted:`, broadcast)
585
+
586
+ // Follow the transaction
587
+ log.info(tag, `๐Ÿ‘€ Following transaction...`)
588
+ const followResult = await app.followTransaction(caip, broadcast)
589
+ log.info(tag, `โœ… Transaction completed:`, followResult)
590
+
591
+ } else {
592
+ log.info(tag, `โ„น๏ธ Skipping execution - set ACTUALLY_EXECUTE_TRANSACTIONS to true to execute`)
593
+ }
594
+
595
+ } catch (buildError) {
596
+ log.error(tag, `โŒ Error building delegation transaction:`, buildError)
597
+ // Continue with test - this is expected if no balance available
598
+ }
599
+
600
+ } catch (error) {
601
+ log.error(tag, `โŒ Error in delegation flow:`, error)
602
+ }
603
+ }
604
+ }
605
+
606
+ // **TEST 6: Reward Claiming Testing**
607
+ if (TEST_CONFIG.TEST_CLAIM_REWARDS) {
608
+ log.info(tag, "")
609
+ log.info(tag, "=".repeat(60))
610
+ log.info(tag, "๐Ÿงช TEST 6: Reward Claiming Testing")
611
+ log.info(tag, "=".repeat(60))
612
+ } else {
613
+ log.info(tag, "โญ๏ธ Skipping TEST 6: Reward Claiming Testing")
614
+ }
615
+
616
+ if (TEST_CONFIG.TEST_CLAIM_REWARDS) {
617
+ // Find reward positions
618
+ const rewardPositions = existingStakingPositions.filter((position: any) =>
619
+ position.type === 'reward' && parseFloat(position.balance) > 0
620
+ )
621
+
622
+ if (rewardPositions.length > 0) {
623
+ log.info(tag, `Found ${rewardPositions.length} reward positions available for claiming`)
624
+
625
+ rewardPositions.forEach((reward: any, index: number) => {
626
+ log.info(tag, ` ${index + 1}. ${reward.balance} ${reward.ticker} from ${reward.validatorAddress}`)
627
+ })
628
+
629
+ // Test reward claiming with the first reward position
630
+ const targetReward = rewardPositions[0]
631
+ const networkId = targetReward.networkId || caipToNetworkId(targetReward.caip)
632
+ const caip = targetReward.caip
633
+
634
+ log.info(tag, `๐ŸŽฏ Testing reward claiming for: ${targetReward.validatorAddress}`)
635
+ log.info(tag, `๐Ÿ’ฐ Available rewards: ${targetReward.balance} ${targetReward.ticker}`)
636
+
637
+ try {
638
+ // Build reward claiming transaction payload
639
+ const claimPayload = {
640
+ validatorAddress: targetReward.validatorAddress,
641
+ memo: 'E2E Reward Claim Test'
642
+ }
643
+
644
+ log.info(tag, `๐Ÿ”จ Building reward claim transaction...`)
645
+ log.info(tag, `๐Ÿ“ค Payload:`, claimPayload)
646
+
647
+ // Test actual reward claiming transaction building
648
+ try {
649
+ const unsignedTx = await app.buildClaimRewardsTx(caip, claimPayload)
650
+ log.info(tag, `โœ… Reward claim transaction built successfully`)
651
+ log.info(tag, `๐Ÿ“‹ Transaction structure:`, JSON.stringify(unsignedTx, null, 2))
652
+
653
+ // Validate transaction structure
654
+ assert(unsignedTx.signDoc, 'Transaction must have signDoc')
655
+ assert(unsignedTx.signDoc.msgs, 'Transaction must have messages')
656
+ assert(unsignedTx.signDoc.msgs[0].type === 'cosmos-sdk/MsgWithdrawDelegationReward', 'Must be MsgWithdrawDelegationReward')
657
+ assert(unsignedTx.signDoc.msgs[0].value.delegator_address, 'Must have delegator_address')
658
+ assert(unsignedTx.signDoc.msgs[0].value.validator_address, 'Must have validator_address')
659
+
660
+ log.info(tag, `โœ… Reward claim transaction structure validated`)
661
+
662
+ if (TEST_CONFIG.ACTUALLY_EXECUTE_TRANSACTIONS) {
663
+ log.info(tag, `๐Ÿš€ EXECUTING REWARD CLAIM TRANSACTION...`)
664
+
665
+ // Sign the transaction
666
+ log.info(tag, `โœ๏ธ Signing transaction...`)
667
+ log.info(tag, `๐Ÿ“‹ Signing with CAIP: ${caip}`)
668
+ log.info(tag, `๐Ÿ“‹ Signing with unsignedTx keys: ${Object.keys(unsignedTx)}`)
669
+ const signedTx = await app.signTx({ caip, unsignedTx })
670
+ log.info(tag, `โœ… Transaction signed successfully`)
671
+
672
+ // Broadcast the transaction
673
+ log.info(tag, `๐Ÿ“ก Broadcasting transaction...`)
674
+ const broadcast = await app.broadcastTx(caip, signedTx)
675
+ log.info(tag, `โœ… Transaction broadcasted:`, broadcast)
676
+
677
+ // Follow the transaction
678
+ log.info(tag, `๐Ÿ‘€ Following transaction...`)
679
+ const followResult = await app.followTransaction(caip, broadcast)
680
+ log.info(tag, `โœ… Transaction completed:`, followResult)
681
+
682
+ } else {
683
+ log.info(tag, `โ„น๏ธ Skipping execution - set ACTUALLY_EXECUTE_TRANSACTIONS to true to execute`)
684
+ }
685
+
686
+ } catch (buildError) {
687
+ log.error(tag, `โŒ Error building reward claim transaction:`, buildError)
688
+ // Continue with test - this is expected if no rewards available
689
+ }
690
+
691
+ } catch (error) {
692
+ log.error(tag, `โŒ Error in reward claiming flow:`, error)
693
+ }
694
+
695
+ } else {
696
+ log.info(tag, `โ„น๏ธ No reward positions found for claiming testing`)
697
+
698
+ // Try to find delegation positions that might have rewards
699
+ const delegationPositions = existingStakingPositions.filter((position: any) =>
700
+ position.type === 'delegation' && parseFloat(position.balance) > 0
701
+ )
702
+
703
+ if (delegationPositions.length > 0) {
704
+ log.info(tag, `๐Ÿ’ก Found ${delegationPositions.length} delegation positions that might have rewards:`)
705
+ delegationPositions.forEach((delegation: any, index: number) => {
706
+ log.info(tag, ` ${index + 1}. ${delegation.balance} ${delegation.ticker} delegated to ${delegation.validatorAddress}`)
707
+ })
708
+
709
+ // Test claiming ALL rewards from all delegations
710
+ const validatorAddresses = delegationPositions.map((delegation: any) =>
711
+ delegation.validatorAddress
712
+ )
713
+
714
+ const caip = delegationPositions[0].caip
715
+
716
+ log.info(tag, `๐ŸŽฏ Testing CLAIM ALL REWARDS from ${validatorAddresses.length} validators`)
717
+
718
+ try {
719
+ const claimAllPayload = {
720
+ validatorAddresses: validatorAddresses,
721
+ memo: 'E2E Claim All Rewards Test'
722
+ }
723
+
724
+ log.info(tag, `๐Ÿ”จ Building claim all rewards transaction...`)
725
+ log.info(tag, `๐Ÿ“ค Payload:`, claimAllPayload)
726
+
727
+ const unsignedTx = await app.buildClaimAllRewardsTx(caip, claimAllPayload)
728
+ log.info(tag, `โœ… Claim all rewards transaction built successfully`)
729
+ log.info(tag, `๐Ÿ“‹ Transaction structure:`, JSON.stringify(unsignedTx, null, 2))
730
+
731
+ // Validate transaction structure
732
+ assert(unsignedTx.signDoc, 'Transaction must have signDoc')
733
+ assert(unsignedTx.signDoc.msgs, 'Transaction must have messages')
734
+ assert(unsignedTx.signDoc.msgs.length === validatorAddresses.length, `Must have ${validatorAddresses.length} messages`)
735
+
736
+ // Validate each message
737
+ unsignedTx.signDoc.msgs.forEach((msg: any, index: number) => {
738
+ assert(msg.type === 'cosmos-sdk/MsgWithdrawDelegationReward', `Message ${index} must be MsgWithdrawDelegationReward`)
739
+ assert(msg.value.delegator_address, `Message ${index} must have delegator_address`)
740
+ assert(msg.value.validator_address, `Message ${index} must have validator_address`)
741
+ })
742
+
743
+ log.info(tag, `โœ… Claim all rewards transaction structure validated`)
744
+
745
+ if (TEST_CONFIG.ACTUALLY_EXECUTE_TRANSACTIONS) {
746
+ log.info(tag, `๐Ÿš€ EXECUTING CLAIM ALL REWARDS TRANSACTION...`)
747
+ log.info(tag, `๐Ÿ’ฐ Claiming rewards from ${validatorAddresses.length} validators`)
748
+
749
+ // Sign the transaction
750
+ log.info(tag, `โœ๏ธ Signing transaction...`)
751
+ const signedTx = await app.signTx({ caip, unsignedTx })
752
+ log.info(tag, `โœ… Transaction signed successfully`)
753
+
754
+ // Broadcast the transaction
755
+ log.info(tag, `๐Ÿ“ก Broadcasting transaction...`)
756
+ const broadcast = await app.broadcastTx(caip, signedTx)
757
+ log.info(tag, `โœ… Transaction broadcasted:`, broadcast)
758
+
759
+ // Follow the transaction
760
+ log.info(tag, `๐Ÿ‘€ Following transaction...`)
761
+ const followResult = await app.followTransaction(caip, broadcast)
762
+ log.info(tag, `โœ… Transaction completed:`, followResult)
763
+
764
+ } else {
765
+ log.info(tag, `โ„น๏ธ Skipping execution - set ACTUALLY_EXECUTE_TRANSACTIONS to true to execute`)
766
+ }
767
+
768
+ } catch (buildError) {
769
+ log.error(tag, `โŒ Error building claim all rewards transaction:`, buildError)
770
+ }
771
+ }
772
+ }
773
+ }
774
+
775
+ // **TEST 7: Staking Integration Validation**
776
+ if (TEST_CONFIG.TEST_STAKING_INTEGRATION) {
777
+ log.info(tag, "")
778
+ log.info(tag, "=".repeat(60))
779
+ log.info(tag, "๐Ÿงช TEST 7: Staking Integration Validation")
780
+ log.info(tag, "=".repeat(60))
781
+ } else {
782
+ log.info(tag, "โญ๏ธ Skipping TEST 7: Staking Integration Validation")
783
+ }
784
+
785
+ if (TEST_CONFIG.TEST_STAKING_INTEGRATION) {
786
+ // Validate that staking positions are properly integrated into the app
787
+ const allStakingInBalances = app.balances.filter((balance: any) =>
788
+ balance.chart === 'staking' || ['delegation', 'reward', 'unbonding'].includes(balance.type)
789
+ )
790
+
791
+ log.info(tag, `Total staking positions in app.balances: ${allStakingInBalances.length}`)
792
+
793
+ if (allStakingInBalances.length > 0) {
794
+ const delegations = allStakingInBalances.filter((p: any) => p.type === 'delegation')
795
+ const rewards = allStakingInBalances.filter((p: any) => p.type === 'reward')
796
+ const unbonding = allStakingInBalances.filter((p: any) => p.type === 'unbonding')
797
+
798
+ log.info(tag, `๐Ÿ“Š Breakdown:`)
799
+ log.info(tag, ` - Delegations: ${delegations.length}`)
800
+ log.info(tag, ` - Rewards: ${rewards.length}`)
801
+ log.info(tag, ` - Unbonding: ${unbonding.length}`)
802
+
803
+ // Validate pricing integration
804
+ const stakingWithPricing = allStakingInBalances.filter((p: any) =>
805
+ p.priceUsd && p.priceUsd > 0 && p.valueUsd && p.valueUsd > 0
806
+ )
807
+
808
+ log.info(tag, `๐Ÿ’ฐ Positions with pricing: ${stakingWithPricing.length}/${allStakingInBalances.length}`)
809
+
810
+ // Calculate total staking value
811
+ const totalStakingValue = allStakingInBalances.reduce((sum: number, position: any) =>
812
+ sum + (parseFloat(position.valueUsd) || 0), 0
813
+ )
814
+
815
+ log.info(tag, `๐Ÿ’ต Total staking value: $${totalStakingValue.toFixed(2)}`)
816
+ }
817
+ }
818
+
819
+ // **TEST RESULTS VALIDATION**
820
+ log.info(tag, "")
821
+ log.info(tag, "=".repeat(60))
822
+ log.info(tag, "๐Ÿ STAKING TEST SUITE RESULTS")
823
+ log.info(tag, "=".repeat(60))
824
+
825
+ console.timeEnd('staking-test-start')
826
+
827
+ // Check critical requirements for test success
828
+ let testsPassed = 0
829
+ let testsTotal = 0
830
+ let criticalFailures = []
831
+
832
+ // CRITICAL: KeepKey device connection
833
+ testsTotal++
834
+ if (app.pubkeys.length > 0) {
835
+ log.info(tag, `โœ… KeepKey device connected: ${app.pubkeys.length} pubkeys loaded`)
836
+ testsPassed++
837
+ } else {
838
+ log.error(tag, `โŒ CRITICAL FAILURE: KeepKey device not connected (0 pubkeys)`)
839
+ criticalFailures.push("KeepKey device not connected - no pubkeys loaded")
840
+ }
841
+
842
+ // CRITICAL: Staking positions found
843
+ testsTotal++
844
+ if (existingStakingPositions.length > 0) {
845
+ log.info(tag, `โœ… Staking positions found: ${existingStakingPositions.length}`)
846
+ testsPassed++
847
+ } else {
848
+ log.error(tag, `โŒ CRITICAL FAILURE: No staking positions found`)
849
+ criticalFailures.push("No staking positions found - nothing to claim rewards from")
850
+ }
851
+
852
+ // CRITICAL: Transaction execution (if enabled)
853
+ if (TEST_CONFIG.ACTUALLY_EXECUTE_TRANSACTIONS) {
854
+ testsTotal++
855
+ // This will be updated by the transaction execution logic
856
+ log.info(tag, `โณ Transaction execution: Checking if any transactions were executed...`)
857
+
858
+ // For now, mark as failed since we know no transactions were executed
859
+ log.error(tag, `โŒ CRITICAL FAILURE: No transactions were executed`)
860
+ criticalFailures.push("No transactions were executed - test set to execute but nothing happened")
861
+ }
862
+
863
+ // Final test result
864
+ log.info(tag, "")
865
+ log.info(tag, "=".repeat(60))
866
+ if (criticalFailures.length > 0) {
867
+ log.error(tag, "โŒ STAKING TEST SUITE FAILED!")
868
+ log.error(tag, "")
869
+ log.error(tag, "๐Ÿ’ฅ CRITICAL FAILURES:")
870
+ criticalFailures.forEach((failure, index) => {
871
+ log.error(tag, ` ${index + 1}. ${failure}`)
872
+ })
873
+ log.error(tag, "")
874
+ log.error(tag, "๐Ÿ”ง TO FIX:")
875
+ log.error(tag, " 1. Connect and unlock your KeepKey device")
876
+ log.error(tag, " 2. Ensure you have existing staking positions with rewards")
877
+ log.error(tag, " 3. Make sure your device is properly paired")
878
+ log.error(tag, "")
879
+
880
+ // Exit with error code
881
+ process.exit(1)
882
+ } else {
883
+ log.info(tag, "๐ŸŽ‰ STAKING TEST SUITE COMPLETED SUCCESSFULLY!")
884
+ log.info(tag, `๐Ÿ“Š Tests passed: ${testsPassed}/${testsTotal}`)
885
+ }
886
+ log.info(tag, "")
887
+
888
+ // All staking functionality is now implemented and ready to use:
889
+ log.info(tag, "โœ… Implementation Status:")
890
+ log.info(tag, " โœ… buildDelegateTx() method - IMPLEMENTED")
891
+ log.info(tag, " โœ… buildUndelegateTx() method - IMPLEMENTED")
892
+ log.info(tag, " โœ… buildClaimRewardsTx() method - IMPLEMENTED")
893
+ log.info(tag, " โœ… buildClaimAllRewardsTx() method - IMPLEMENTED")
894
+ log.info(tag, " โœ… Staking transaction templates - IMPLEMENTED")
895
+ log.info(tag, " โœ… KeepKey SDK staking signing methods - IMPLEMENTED")
896
+ log.info(tag, "")
897
+ log.info(tag, "๐Ÿ’ก To test staking functionality:")
898
+ log.info(tag, " 1. Make sure your KeepKey device is connected and unlocked")
899
+ log.info(tag, " 2. Ensure you have existing staking positions with rewards")
900
+ log.info(tag, " 3. Run this test - it will ask you to sign transactions if positions are found")
901
+
902
+ } catch (e) {
903
+ log.error(tag, "โŒ STAKING TEST FAILED:", e)
904
+ process.exit(1)
905
+ }
906
+ }
907
+
908
+ // Run the staking test suite
909
+ test_staking_service()