diff --git a/universalClient/externalchains/svm/tx_builder.go b/universalClient/externalchains/svm/tx_builder.go index 5bb7eb3a..a957ed00 100644 --- a/universalClient/externalchains/svm/tx_builder.go +++ b/universalClient/externalchains/svm/tx_builder.go @@ -66,6 +66,7 @@ var ( rateLimitConfigSeed = []byte("rate_limit_config") tokenRateLimitSeed = []byte("rate_limit") storedIxDataSeed = []byte("stored_ix_data") + eventAuthoritySeed = []byte("__event_authority") // TSS message envelope — cross-protocol replay guard. tssMessagePrefix = []byte("PUSH_CHAIN_SVM") @@ -1468,6 +1469,16 @@ func (tb *TxBuilder) deriveTSSPDA() (solana.PublicKey, error) { return address, err } +// deriveEventAuthorityPDA derives the account Anchor's #[event_cpi] macro requires +// on every instruction that may self-CPI to emit an event. Every gateway +// instruction that emits an event needs this PDA, plus the gateway program +// itself, appended after its named accounts and before any remaining accounts. +func (tb *TxBuilder) deriveEventAuthorityPDA() (solana.PublicKey, error) { + seeds := [][]byte{eventAuthoritySeed} + address, _, err := solana.FindProgramAddress(seeds, tb.gatewayAddress) + return address, err +} + // fetchTSSChainID reads the TSS PDA account from on-chain and extracts the chain ID. // // On-chain layout (Borsh-serialized TssPda struct from state.rs): @@ -2079,8 +2090,11 @@ func (tb *TxBuilder) buildRescueData( // --- Optional ref-finalize accounts (19-20) --- // 19 stored_ix_data read/None StoredIxData PDA (only used by ref-finalize route) // 20 store_refund_recipient mut/None Receives store-tx fee reimbursement (ref route only) +// --- #[event_cpi] accounts (21-22) --- +// 21 event_authority read-only PDA ["__event_authority"], for the gateway's self-CPI +// 22 program read-only The gateway program itself // --- Execute-only remaining accounts --- -// 21+ remaining_accounts varies Accounts that the target program needs +// 23+ remaining_accounts varies Accounts that the target program needs // // For Anchor Option fields: passing the gateway program's own ID = None. // This is Anchor's convention for encoding "this optional account is not provided". @@ -2203,6 +2217,14 @@ func (tb *TxBuilder) buildWithdrawAndExecuteAccounts( accounts = append(accounts, &solana.AccountMeta{PublicKey: storeRefundRecipient, IsWritable: true, IsSigner: false}) } + // #[event_cpi] accounts (#21-22): required on every named-account boundary + // before remaining_accounts, so the gateway can self-CPI its emit_cpi event. + eventAuthority, _ := tb.deriveEventAuthorityPDA() + accounts = append(accounts, + &solana.AccountMeta{PublicKey: eventAuthority, IsWritable: false, IsSigner: false}, + &solana.AccountMeta{PublicKey: tb.gatewayAddress, IsWritable: false, IsSigner: false}, + ) + // For execute mode: append the target program's accounts as "remaining_accounts". // These are the accounts that the gateway will pass through via CPI to the target program. if instructionID == 2 { @@ -2233,11 +2255,16 @@ func (tb *TxBuilder) buildWithdrawAndExecuteAccounts( // 6 executed_sub_tx mut Replay protection (gets created) // 7 caller signer,mut Relayer // 8 system_program read-only -// --- Optional SPL accounts (9-12) --- +// --- Optional SPL accounts (9-14) --- // 9 token_vault mut/None Vault's ATA for the token // 10 recipient_token_account mut/None Recipient's ATA // 11 token_mint read/None The SPL token mint // 12 token_program read/None SPL Token program +// 13 associated_token_program read/None Needed to create the recipient ATA +// 14 rent read/None Needed to create the recipient ATA +// --- #[event_cpi] accounts (15-16) --- +// 15 event_authority read-only PDA ["__event_authority"], for the gateway's self-CPI +// 16 program read-only The gateway program itself func (tb *TxBuilder) buildRevertAccounts( configPDA solana.PublicKey, vaultPDA solana.PublicKey, @@ -2286,6 +2313,14 @@ func (tb *TxBuilder) buildRevertAccounts( ) } + // #[event_cpi] accounts: required so the gateway can self-CPI its emit_cpi + // event. revert_universal_tx has no remaining_accounts, so these are last. + eventAuthority, _ := tb.deriveEventAuthorityPDA() + accounts = append(accounts, + &solana.AccountMeta{PublicKey: eventAuthority, IsWritable: false, IsSigner: false}, + &solana.AccountMeta{PublicKey: tb.gatewayAddress, IsWritable: false, IsSigner: false}, + ) + return accounts } diff --git a/universalClient/externalchains/svm/tx_builder_pc20.go b/universalClient/externalchains/svm/tx_builder_pc20.go index 74a9ed97..4daffed8 100644 --- a/universalClient/externalchains/svm/tx_builder_pc20.go +++ b/universalClient/externalchains/svm/tx_builder_pc20.go @@ -753,6 +753,17 @@ func (tb *TxBuilder) buildPC20ExportAccounts( accounts = append(accounts, &solana.AccountMeta{PublicKey: storeRefundRecipient, IsWritable: true, IsSigner: false}) } + // #[event_cpi] accounts: required, at the same named-account boundary as the + // non-PC20 finalize route, so the gateway can self-CPI its emit_cpi event. + eventAuthority, err := tb.deriveEventAuthorityPDA() + if err != nil { + return nil, fmt.Errorf("failed to derive event_authority PDA: %w", err) + } + accounts = append(accounts, + &solana.AccountMeta{PublicKey: eventAuthority, IsWritable: false, IsSigner: false}, + &solana.AccountMeta{PublicKey: tb.gatewayAddress, IsWritable: false, IsSigner: false}, + ) + // Remaining accounts: [pc20_state, pc20_mint] + the payload accounts (only when // user_data is present). The gateway requires exactly these two when user_data is // empty — no recipient_ata, since the wrapper is minted to cea_ata. @@ -775,7 +786,12 @@ func (tb *TxBuilder) buildPC20ExportAccounts( // buildPC20RemintAccounts builds the revert_universal_tx / rescue_funds account list for // the PC20 remint branch: token_vault + recipient_token_account None, token_mint + token_program -// present, remaining = [pc20_state, pc20_mint(w), recipient_ata(w), ATA_program, rent]. +// present, associated_token_program + rent None (unused: the remaining accounts below carry +// their own copies for creating recipient_ata), then the #[event_cpi] pair, then +// remaining = [pc20_state, pc20_mint(w), recipient_ata(w), ATA_program, rent]. +// +// Must match the same RevertUniversalTx struct as buildRevertAccounts — PC20 remint +// dispatches through the same instruction, so positions 1-14 have to line up with it. func (tb *TxBuilder) buildPC20RemintAccounts( configPDA solana.PublicKey, vaultPDA solana.PublicKey, @@ -812,12 +828,28 @@ func (tb *TxBuilder) buildPC20RemintAccounts( none, // recipient_token_account {PublicKey: mint, IsWritable: false, IsSigner: false}, {PublicKey: solana.TokenProgramID, IsWritable: false, IsSigner: false}, - // remaining accounts — exact shape required by is_pc20_remint_account_shape - {PublicKey: pc20State, IsWritable: false, IsSigner: false}, - {PublicKey: mint, IsWritable: true, IsSigner: false}, - {PublicKey: recipientATA, IsWritable: true, IsSigner: false}, - {PublicKey: solana.SPLAssociatedTokenAccountProgramID, IsWritable: false, IsSigner: false}, - {PublicKey: solana.SysVarRentPubkey, IsWritable: false, IsSigner: false}, + none, // associated_token_program + none, // rent + } + + // #[event_cpi] accounts: required so the gateway can self-CPI its emit_cpi + // event, at the same named-account boundary buildRevertAccounts uses. + eventAuthority, err := tb.deriveEventAuthorityPDA() + if err != nil { + return nil, fmt.Errorf("failed to derive event_authority PDA: %w", err) } + accounts = append(accounts, + &solana.AccountMeta{PublicKey: eventAuthority, IsWritable: false, IsSigner: false}, + &solana.AccountMeta{PublicKey: tb.gatewayAddress, IsWritable: false, IsSigner: false}, + ) + + // remaining accounts — exact shape required by is_pc20_remint_account_shape + accounts = append(accounts, + &solana.AccountMeta{PublicKey: pc20State, IsWritable: false, IsSigner: false}, + &solana.AccountMeta{PublicKey: mint, IsWritable: true, IsSigner: false}, + &solana.AccountMeta{PublicKey: recipientATA, IsWritable: true, IsSigner: false}, + &solana.AccountMeta{PublicKey: solana.SPLAssociatedTokenAccountProgramID, IsWritable: false, IsSigner: false}, + &solana.AccountMeta{PublicKey: solana.SysVarRentPubkey, IsWritable: false, IsSigner: false}, + ) return accounts, nil } diff --git a/universalClient/externalchains/svm/tx_builder_pc20_test.go b/universalClient/externalchains/svm/tx_builder_pc20_test.go index 58c92c96..592dd1f3 100644 --- a/universalClient/externalchains/svm/tx_builder_pc20_test.go +++ b/universalClient/externalchains/svm/tx_builder_pc20_test.go @@ -210,9 +210,9 @@ func TestBuildPC20ExportAccounts_Direct(t *testing.T) { ) require.NoError(t, err) - // 20 typed slots + [pc20_state, pc20_mint] (export-only mints to cea_ata, so no - // recipient_ata in the remaining accounts). - require.Len(t, accounts, 22) + // 20 typed slots + event_cpi(2) + [pc20_state, pc20_mint] (export-only mints to + // cea_ata, so no recipient_ata in the remaining accounts). + require.Len(t, accounts, 24) assert.Equal(t, caller, accounts[0].PublicKey) assert.True(t, accounts[0].IsSigner) @@ -236,11 +236,17 @@ func TestBuildPC20ExportAccounts_Direct(t *testing.T) { assert.Equal(t, solana.SysVarRentPubkey, accounts[13].PublicKey) assert.Equal(t, solana.SPLAssociatedTokenAccountProgramID, accounts[14].PublicKey) + // event_cpi accounts (20-21) + wantEventAuthority, err := tb.deriveEventAuthorityPDA() + require.NoError(t, err) + assert.Equal(t, wantEventAuthority, accounts[20].PublicKey, "event_authority") + assert.Equal(t, tb.gatewayAddress, accounts[21].PublicKey, "program") + // Remaining accounts: [pc20_state, pc20_mint] - assert.Equal(t, state, accounts[20].PublicKey) - assert.True(t, accounts[20].IsWritable) - assert.Equal(t, mint, accounts[21].PublicKey) - assert.True(t, accounts[21].IsWritable) + assert.Equal(t, state, accounts[22].PublicKey) + assert.True(t, accounts[22].IsWritable) + assert.Equal(t, mint, accounts[23].PublicKey) + assert.True(t, accounts[23].IsWritable) } func TestBuildPC20ExportAccounts_WithPayload(t *testing.T) { @@ -267,8 +273,8 @@ func TestBuildPC20ExportAccounts_WithPayload(t *testing.T) { ) require.NoError(t, err) - // 20 typed slots + [pc20_state, pc20_mint] + 1 payload account - require.Len(t, accounts, 23) + // 20 typed slots + event_cpi(2) + [pc20_state, pc20_mint] + 1 payload account + require.Len(t, accounts, 25) assert.Equal(t, target, accounts[7].PublicKey) // destination_program = payload target // cea_ata (slot 10) is the CEA's ATA, writable @@ -279,11 +285,17 @@ func TestBuildPC20ExportAccounts_WithPayload(t *testing.T) { assert.Equal(t, expectedCeaATA, accounts[10].PublicKey) assert.True(t, accounts[10].IsWritable) + // event_cpi accounts (20-21) + wantEventAuthority, err := tb.deriveEventAuthorityPDA() + require.NoError(t, err) + assert.Equal(t, wantEventAuthority, accounts[20].PublicKey, "event_authority") + assert.Equal(t, tb.gatewayAddress, accounts[21].PublicKey, "program") + // remaining: state, mint, then payload accounts (no recipient_ata) - assert.Equal(t, state, accounts[20].PublicKey) - assert.Equal(t, mint, accounts[21].PublicKey) - assert.Equal(t, recipient, accounts[22].PublicKey) - assert.True(t, accounts[22].IsWritable) + assert.Equal(t, state, accounts[22].PublicKey) + assert.Equal(t, mint, accounts[23].PublicKey) + assert.Equal(t, recipient, accounts[24].PublicKey) + assert.True(t, accounts[24].IsWritable) } func TestBuildPC20RemintAccounts(t *testing.T) { @@ -308,8 +320,8 @@ func TestBuildPC20RemintAccounts(t *testing.T) { ) require.NoError(t, err) - // 12 typed slots + 5 remaining - require.Len(t, accounts, 17) + // 12 typed slots + associated_token_program/rent None sentinels(2) + event_cpi(2) + 5 remaining + require.Len(t, accounts, 21) assert.Equal(t, configPDA, accounts[0].PublicKey) assert.Equal(t, recipient, accounts[4].PublicKey) @@ -322,21 +334,32 @@ func TestBuildPC20RemintAccounts(t *testing.T) { assert.Equal(t, mint, accounts[10].PublicKey) assert.Equal(t, solana.TokenProgramID, accounts[11].PublicKey) + // associated_token_program(12) and rent(13) are also None sentinels — PC20 remint + // creates recipient_ata via the remaining accounts below, not this typed slot. + assert.Equal(t, tb.gatewayAddress, accounts[12].PublicKey) + assert.Equal(t, tb.gatewayAddress, accounts[13].PublicKey) + + // event_cpi accounts (14-15) + wantEventAuthority, err := tb.deriveEventAuthorityPDA() + require.NoError(t, err) + assert.Equal(t, wantEventAuthority, accounts[14].PublicKey, "event_authority") + assert.Equal(t, tb.gatewayAddress, accounts[15].PublicKey, "program") + // remaining: [pc20_state(ro), pc20_mint(w), recipient_ata(w), ATA program(ro), rent(ro)] - assert.Equal(t, state, accounts[12].PublicKey) - assert.False(t, accounts[12].IsWritable) - assert.Equal(t, mint, accounts[13].PublicKey) - assert.True(t, accounts[13].IsWritable) + assert.Equal(t, state, accounts[16].PublicKey) + assert.False(t, accounts[16].IsWritable) + assert.Equal(t, mint, accounts[17].PublicKey) + assert.True(t, accounts[17].IsWritable) expectedATA, _, _ := solana.FindProgramAddress( [][]byte{recipient.Bytes(), solana.TokenProgramID.Bytes(), mint.Bytes()}, solana.SPLAssociatedTokenAccountProgramID, ) - assert.Equal(t, expectedATA, accounts[14].PublicKey) - assert.True(t, accounts[14].IsWritable) - assert.Equal(t, solana.SPLAssociatedTokenAccountProgramID, accounts[15].PublicKey) - assert.False(t, accounts[15].IsWritable) - assert.Equal(t, solana.SysVarRentPubkey, accounts[16].PublicKey) - assert.False(t, accounts[16].IsWritable) + assert.Equal(t, expectedATA, accounts[18].PublicKey) + assert.True(t, accounts[18].IsWritable) + assert.Equal(t, solana.SPLAssociatedTokenAccountProgramID, accounts[19].PublicKey) + assert.False(t, accounts[19].IsWritable) + assert.Equal(t, solana.SysVarRentPubkey, accounts[20].PublicKey) + assert.False(t, accounts[20].IsWritable) } func TestValidatePC20UserData(t *testing.T) { diff --git a/universalClient/externalchains/svm/tx_builder_test.go b/universalClient/externalchains/svm/tx_builder_test.go index b22d531c..67ca609e 100644 --- a/universalClient/externalchains/svm/tx_builder_test.go +++ b/universalClient/externalchains/svm/tx_builder_test.go @@ -1188,7 +1188,13 @@ func TestBuildWithdrawAndExecuteAccounts(t *testing.T) { assert.Equal(t, builder.gatewayAddress, accounts[18].PublicKey, "stored_ix_data should be gateway sentinel for direct route") assert.Equal(t, builder.gatewayAddress, accounts[19].PublicKey, "store_refund_recipient should be gateway sentinel for direct route") - assert.Len(t, accounts, 20, "total accounts for SOL withdraw (direct route)") + // event_cpi accounts (21-22) + wantEventAuthority, err := builder.deriveEventAuthorityPDA() + require.NoError(t, err) + assert.Equal(t, wantEventAuthority, accounts[20].PublicKey, "event_authority") + assert.Equal(t, builder.gatewayAddress, accounts[21].PublicKey, "program") + + assert.Len(t, accounts, 22, "total accounts for SOL withdraw (direct route)") }) t.Run("execute (id=2) appends remaining_accounts", func(t *testing.T) { @@ -1209,8 +1215,8 @@ func TestBuildWithdrawAndExecuteAccounts(t *testing.T) { // For execute: recipient should be gateway sentinel (None) assert.Equal(t, builder.gatewayAddress, accounts[8].PublicKey, "recipient should be None for execute") - // remaining_accounts appended at the end - totalRequired := 20 // 8 required + 8 SPL optional + 2 rate limit + 2 ref-finalize optional + // remaining_accounts appended at the end, after the event_cpi accounts + totalRequired := 22 // 8 required + 8 SPL optional + 2 rate limit + 2 ref-finalize optional + 2 event_cpi assert.Len(t, accounts, totalRequired+2) // Check remaining_accounts @@ -1249,7 +1255,7 @@ func TestBuildWithdrawAndExecuteAccounts_SPLSlots(t *testing.T) { nil, solana.PublicKey{}, solana.PublicKey{}, ) - require.Len(t, accounts, 20) + require.Len(t, accounts, 22) wantVaultATA, _, err := solana.FindAssociatedTokenAddress(vault, mint) require.NoError(t, err) @@ -1305,10 +1311,13 @@ func TestBuildRevertAccounts(t *testing.T) { caller := solana.NewWallet().PublicKey() tokenMint := solana.NewWallet().PublicKey() - t.Run("SOL revert has 14 accounts (8 required + 6 None sentinels)", func(t *testing.T) { + wantEventAuthority, err := builder.deriveEventAuthorityPDA() + require.NoError(t, err) + + t.Run("SOL revert has 16 accounts (8 required + 6 None sentinels + event_cpi)", func(t *testing.T) { accounts := builder.buildRevertAccounts(config, vault, feeVault, tss, recipient, executed, caller, true, solana.PublicKey{}) - assert.Len(t, accounts, 14) + assert.Len(t, accounts, 16) assert.Equal(t, config, accounts[0].PublicKey, "config") assert.False(t, accounts[0].IsWritable) assert.Equal(t, vault, accounts[1].PublicKey, "vault") @@ -1328,12 +1337,16 @@ func TestBuildRevertAccounts(t *testing.T) { for i := 8; i < 14; i++ { assert.Equal(t, builder.gatewayAddress, accounts[i].PublicKey, "SOL sentinel account %d", i) } + assert.Equal(t, wantEventAuthority, accounts[14].PublicKey, "event_authority") + assert.False(t, accounts[14].IsWritable) + assert.Equal(t, builder.gatewayAddress, accounts[15].PublicKey, "program") + assert.False(t, accounts[15].IsWritable) }) - t.Run("SPL revert has 14 accounts (8 required + 6 SPL accounts)", func(t *testing.T) { + t.Run("SPL revert has 16 accounts (8 required + 6 SPL accounts + event_cpi)", func(t *testing.T) { accounts := builder.buildRevertAccounts(config, vault, feeVault, tss, recipient, executed, caller, false, tokenMint) - assert.Len(t, accounts, 14) + assert.Len(t, accounts, 16) // First 8 same as SOL assert.Equal(t, config, accounts[0].PublicKey, "config") assert.Equal(t, vault, accounts[1].PublicKey, "vault") @@ -1351,6 +1364,8 @@ func TestBuildRevertAccounts(t *testing.T) { assert.Equal(t, solana.TokenProgramID, accounts[11].PublicKey, "token_program") assert.Equal(t, solana.SPLAssociatedTokenAccountProgramID, accounts[12].PublicKey, "associated_token_program") assert.Equal(t, solana.SysVarRentPubkey, accounts[13].PublicKey, "rent") + assert.Equal(t, wantEventAuthority, accounts[14].PublicKey, "event_authority") + assert.Equal(t, builder.gatewayAddress, accounts[15].PublicKey, "program") wantRecipientATA, _, err := solana.FindAssociatedTokenAddress(recipient, tokenMint) require.NoError(t, err) @@ -1979,7 +1994,7 @@ func TestBuildCloseStoredIxDataAccounts(t *testing.T) { func TestBuildWithdrawAndExecuteAccounts_RefRouteSlots(t *testing.T) { // Verify the ref-finalize slots (#19-20) carry real values when populated, - // and that remaining_accounts still land at position 21+ in execute mode. + // and that remaining_accounts still land after the event_cpi slots in execute mode. builder := newTestBuilder(t) caller := solana.NewWallet().PublicKey() configPDA := solana.NewWallet().PublicKey() @@ -2012,11 +2027,17 @@ func TestBuildWithdrawAndExecuteAccounts_RefRouteSlots(t *testing.T) { assert.Equal(t, storeRefund, accounts[19].PublicKey, "store_refund_recipient slot") assert.True(t, accounts[19].IsWritable, "store_refund_recipient must be writable for reimbursement") - // Position 20+: remaining_accounts (the execute CPI accounts) - require.Len(t, accounts, 21, "8 required + 8 SPL + 2 rate-limit + 2 ref + 1 remaining") + // Position 20-21: event_cpi accounts + wantEventAuthority, err := builder.deriveEventAuthorityPDA() + require.NoError(t, err) + assert.Equal(t, wantEventAuthority, accounts[20].PublicKey, "event_authority slot") + assert.Equal(t, builder.gatewayAddress, accounts[21].PublicKey, "program slot") + + // Position 22+: remaining_accounts (the execute CPI accounts) + require.Len(t, accounts, 23, "8 required + 8 SPL + 2 rate-limit + 2 ref + 2 event_cpi + 1 remaining") expectedRemaining := makeTxID(0xAA) - assert.Equal(t, solana.PublicKeyFromBytes(expectedRemaining[:]), accounts[20].PublicKey, "remaining account 0") - assert.True(t, accounts[20].IsWritable) + assert.Equal(t, solana.PublicKeyFromBytes(expectedRemaining[:]), accounts[22].PublicKey, "remaining account 0") + assert.True(t, accounts[22].IsWritable) } // newTestBuilderWithKeypair returns a TxBuilder whose nodeHome contains a @@ -2286,7 +2307,7 @@ func TestBuildPC20ExportAccounts_RefSlots(t *testing.T) { storedIx, refund, ) require.NoError(t, err) - require.Len(t, ref, 22) + require.Len(t, ref, 24) assert.Equal(t, storedIx, ref[18].PublicKey) assert.True(t, ref[18].IsWritable) assert.Equal(t, refund, ref[19].PublicKey) @@ -2299,7 +2320,7 @@ func TestBuildPC20ExportAccounts_RefSlots(t *testing.T) { solana.PublicKey{}, solana.PublicKey{}, ) require.NoError(t, err) - require.Len(t, direct, 22) + require.Len(t, direct, 24) assert.Equal(t, tb.gatewayAddress, direct[18].PublicKey) assert.False(t, direct[18].IsWritable) assert.Equal(t, tb.gatewayAddress, direct[19].PublicKey) @@ -3191,7 +3212,7 @@ func TestBuildOutboundTransaction_NoClientSideATACreate(t *testing.T) { gatewayIx := tx.Message.Instructions[len(tx.Message.Instructions)-1] metas, err := gatewayIx.ResolveInstructionAccounts(&tx.Message) require.NoError(t, err) - require.Len(t, metas, 14) + require.Len(t, metas, 16) wantATA, _, err := solana.FindAssociatedTokenAddress(recipient, mint) require.NoError(t, err) @@ -3793,11 +3814,11 @@ func TestPC20DummyGateway_ExportBuild(t *testing.T) { solana.PublicKey{}, solana.PublicKey{}, ) require.NoError(t, err) - require.Len(t, accounts, 22) + require.Len(t, accounts, 24) // Remaining accounts [pc20_state, pc20_mint] hit the frozen dummy-gateway addresses. - assert.Equal(t, "7G5dx7WgVyxvzstv3ZS6ohrb1w17qAkXFXaYyoCewzL1", accounts[20].PublicKey.String()) - assert.Equal(t, "BCwdEfbVJtt47ukvdX7jW5kzAUKZhjyXveSZvQL3MF8", accounts[21].PublicKey.String()) + assert.Equal(t, "7G5dx7WgVyxvzstv3ZS6ohrb1w17qAkXFXaYyoCewzL1", accounts[22].PublicKey.String()) + assert.Equal(t, "BCwdEfbVJtt47ukvdX7jW5kzAUKZhjyXveSZvQL3MF8", accounts[23].PublicKey.String()) } func TestPC20DummyGateway_RemintMessage(t *testing.T) {