@@ -1277,6 +1277,176 @@ func convertSolanaPacketToABI(packet solana.SolanaPacket) ics26router.IICS26Rout
12771277 }
12781278}
12791279
1280+ // Test_Attestation_AccessManagerTransfer tests propose/accept/cancel access manager
1281+ // transfer on the attestation light client program.
1282+ func (s * IbcSolanaAttestationTestSuite ) Test_Attestation_AccessManagerTransfer () {
1283+ ctx := context .Background ()
1284+ s .SetupSuite (ctx )
1285+
1286+ const keypairDir = "solana-keypairs/localnet"
1287+ const deployerPath = keypairDir + "/deployer_wallet.json"
1288+
1289+ // --- Deploy and initialize AM-B ---
1290+
1291+ var amBProgramID solanago.PublicKey
1292+
1293+ s .Require ().True (s .Run ("Deploy AM-B (test_access_manager)" , func () {
1294+ var err error
1295+ amBKeypairPath := fmt .Sprintf ("%s/test_access_manager-keypair.json" , keypairDir )
1296+ amBProgramID , err = s .Solana .Chain .DeploySolanaProgramAsync (ctx , "test_access_manager" , amBKeypairPath , deployerPath )
1297+ s .Require ().NoError (err , "failed to deploy test_access_manager" )
1298+ }))
1299+
1300+ s .Require ().True (s .Run ("Initialize AM-B with user as admin" , func () {
1301+ deployerWallet , err := solana .LoadDeployerWallet (deployerPath )
1302+ s .Require ().NoError (err )
1303+
1304+ amBAccessManagerPDA , _ := solana .AccessManager .AccessManagerPDA (amBProgramID )
1305+ amBProgramDataPDA , err := solana .GetProgramDataAddress (amBProgramID )
1306+ s .Require ().NoError (err )
1307+
1308+ savedProgramID := access_manager .ProgramID
1309+ access_manager .ProgramID = amBProgramID
1310+ defer func () { access_manager .ProgramID = savedProgramID }()
1311+
1312+ initIx , err := access_manager .NewInitializeInstruction (
1313+ s .SolanaUser .PublicKey (),
1314+ amBAccessManagerPDA ,
1315+ s .SolanaUser .PublicKey (),
1316+ solanago .SystemProgramID ,
1317+ solanago .SysVarInstructionsPubkey ,
1318+ amBProgramDataPDA ,
1319+ solana .DeployerPubkey ,
1320+ )
1321+ s .Require ().NoError (err )
1322+
1323+ tx , err := s .Solana .Chain .NewTransactionFromInstructions (s .SolanaUser .PublicKey (), initIx )
1324+ s .Require ().NoError (err )
1325+
1326+ _ , err = s .Solana .Chain .SignAndBroadcastTxWithRetryAndTimeout (ctx , tx , rpc .CommitmentConfirmed , 30 , s .SolanaUser , deployerWallet )
1327+ s .Require ().NoError (err , "failed to initialize AM-B" )
1328+ }))
1329+
1330+ // --- Helper: read attestation app state ---
1331+
1332+ appStatePDA , _ := solana .Attestation .AppStatePDA (attestation .ProgramID )
1333+
1334+ readAppState := func () * attestation.AttestationTypesAppState {
1335+ s .T ().Helper ()
1336+ accountInfo , err := s .Solana .Chain .RPCClient .GetAccountInfoWithOpts (ctx , appStatePDA , & rpc.GetAccountInfoOpts {
1337+ Commitment : rpc .CommitmentConfirmed ,
1338+ })
1339+ s .Require ().NoError (err )
1340+ s .Require ().NotNil (accountInfo .Value )
1341+ state , err := attestation .ParseAccount_AttestationTypesAppState (accountInfo .Value .Data .GetBinary ())
1342+ s .Require ().NoError (err )
1343+ return state
1344+ }
1345+
1346+ amAAccessManagerPDA , _ := solana .AccessManager .AccessManagerPDA (access_manager .ProgramID )
1347+
1348+ // --- Verify initial state ---
1349+
1350+ s .Require ().True (s .Run ("Verify initial state: AM-A is active, no pending" , func () {
1351+ state := readAppState ()
1352+ s .Require ().Equal (access_manager .ProgramID , state .AccessManager , "Attestation should point to AM-A" )
1353+ s .Require ().Nil (state .PendingAccessManager , "No pending transfer initially" )
1354+ }))
1355+
1356+ // --- Propose transfer to AM-B ---
1357+
1358+ s .Require ().True (s .Run ("Propose access manager transfer to AM-B" , func () {
1359+ proposeIx , err := attestation .NewProposeAccessManagerTransferInstruction (
1360+ amBProgramID ,
1361+ appStatePDA ,
1362+ amAAccessManagerPDA ,
1363+ s .SolanaUser .PublicKey (),
1364+ solanago .SysVarInstructionsPubkey ,
1365+ )
1366+ s .Require ().NoError (err )
1367+
1368+ tx , err := s .Solana .Chain .NewTransactionFromInstructions (s .SolanaUser .PublicKey (), proposeIx )
1369+ s .Require ().NoError (err )
1370+
1371+ _ , err = s .Solana .Chain .SignAndBroadcastTxWithRetry (ctx , tx , rpc .CommitmentConfirmed , s .SolanaUser )
1372+ s .Require ().NoError (err , "propose should succeed" )
1373+ }))
1374+
1375+ s .Require ().True (s .Run ("Verify: pending set, AM unchanged" , func () {
1376+ state := readAppState ()
1377+ s .Require ().Equal (access_manager .ProgramID , state .AccessManager , "AM should still be AM-A" )
1378+ s .Require ().NotNil (state .PendingAccessManager , "Pending should be set" )
1379+ s .Require ().Equal (amBProgramID , * state .PendingAccessManager , "Pending should be AM-B" )
1380+ }))
1381+
1382+ // --- Accept transfer ---
1383+
1384+ amBAccessManagerPDA , _ := solana .AccessManager .AccessManagerPDA (amBProgramID )
1385+
1386+ s .Require ().True (s .Run ("Accept access manager transfer (AM-B admin)" , func () {
1387+ acceptIx , err := attestation .NewAcceptAccessManagerTransferInstruction (
1388+ appStatePDA ,
1389+ amBAccessManagerPDA ,
1390+ s .SolanaUser .PublicKey (),
1391+ solanago .SysVarInstructionsPubkey ,
1392+ )
1393+ s .Require ().NoError (err )
1394+
1395+ tx , err := s .Solana .Chain .NewTransactionFromInstructions (s .SolanaUser .PublicKey (), acceptIx )
1396+ s .Require ().NoError (err )
1397+
1398+ _ , err = s .Solana .Chain .SignAndBroadcastTxWithRetry (ctx , tx , rpc .CommitmentConfirmed , s .SolanaUser )
1399+ s .Require ().NoError (err , "accept should succeed" )
1400+ }))
1401+
1402+ s .Require ().True (s .Run ("Verify: AM is now AM-B, pending cleared" , func () {
1403+ state := readAppState ()
1404+ s .Require ().Equal (amBProgramID , state .AccessManager , "AM should now be AM-B" )
1405+ s .Require ().Nil (state .PendingAccessManager , "Pending should be cleared after accept" )
1406+ }))
1407+
1408+ // --- Propose back to AM-A and cancel ---
1409+
1410+ s .Require ().True (s .Run ("Propose transfer back to AM-A" , func () {
1411+ proposeIx , err := attestation .NewProposeAccessManagerTransferInstruction (
1412+ access_manager .ProgramID ,
1413+ appStatePDA ,
1414+ amBAccessManagerPDA ,
1415+ s .SolanaUser .PublicKey (),
1416+ solanago .SysVarInstructionsPubkey ,
1417+ )
1418+ s .Require ().NoError (err )
1419+
1420+ tx , err := s .Solana .Chain .NewTransactionFromInstructions (s .SolanaUser .PublicKey (), proposeIx )
1421+ s .Require ().NoError (err )
1422+
1423+ _ , err = s .Solana .Chain .SignAndBroadcastTxWithRetry (ctx , tx , rpc .CommitmentConfirmed , s .SolanaUser )
1424+ s .Require ().NoError (err , "propose back to AM-A should succeed" )
1425+ }))
1426+
1427+ s .Require ().True (s .Run ("Cancel pending transfer" , func () {
1428+ cancelIx , err := attestation .NewCancelAccessManagerTransferInstruction (
1429+ appStatePDA ,
1430+ amBAccessManagerPDA ,
1431+ s .SolanaUser .PublicKey (),
1432+ solanago .SysVarInstructionsPubkey ,
1433+ )
1434+ s .Require ().NoError (err )
1435+
1436+ tx , err := s .Solana .Chain .NewTransactionFromInstructions (s .SolanaUser .PublicKey (), cancelIx )
1437+ s .Require ().NoError (err )
1438+
1439+ _ , err = s .Solana .Chain .SignAndBroadcastTxWithRetry (ctx , tx , rpc .CommitmentConfirmed , s .SolanaUser )
1440+ s .Require ().NoError (err , "cancel should succeed" )
1441+ }))
1442+
1443+ s .Require ().True (s .Run ("Verify: pending cleared, AM still AM-B" , func () {
1444+ state := readAppState ()
1445+ s .Require ().Equal (amBProgramID , state .AccessManager , "AM should still be AM-B" )
1446+ s .Require ().Nil (state .PendingAccessManager , "Pending should be cleared after cancel" )
1447+ }))
1448+ }
1449+
12801450// deriveAttestationConsensusStatePDA fetches the attestation client state to get the latest height,
12811451// then derives the consensus state PDA.
12821452func (s * IbcSolanaAttestationTestSuite ) deriveAttestationConsensusStatePDA (ctx context.Context , clientStatePDA solanago.PublicKey ) solanago.PublicKey {
0 commit comments