diff --git a/protocols/horizon/effects/main.go b/protocols/horizon/effects/main.go index c0df32b3d1..3b30f62590 100644 --- a/protocols/horizon/effects/main.go +++ b/protocols/horizon/effects/main.go @@ -161,7 +161,7 @@ const ( // EffectClaimableBalanceSponsorshipRemoved occurs when the sponsorship of a claimable balance ledger entry // is removed - EffectClaimableBalanceSponsorshipRemoved EffectType = 71 // from revoke_sponsorship + EffectClaimableBalanceSponsorshipRemoved EffectType = 71 // from claim_claimable_balance // EffectSignerSponsorshipCreated occurs when the sponsorship of a signer is created EffectSignerSponsorshipCreated EffectType = 72 // from set_options diff --git a/protocols/horizon/operations/main.go b/protocols/horizon/operations/main.go index d2cd4cda80..162457554e 100644 --- a/protocols/horizon/operations/main.go +++ b/protocols/horizon/operations/main.go @@ -256,7 +256,7 @@ type RevokeSponsorship struct { ClaimableBalanceID *string `json:"claimable_balance_id,omitempty"` DataAccountID *string `json:"data_account_id,omitempty"` DataName *string `json:"data_name,omitempty"` - OfferID *string `json:"offer_id,omitempty"` + OfferID *int64 `json:"offer_id,omitempty"` TrustlineAccountID *string `json:"trustline_account_id,omitempty"` TrustlineAsset *string `json:"trustline_asset,omitempty"` SignerAccountID *string `json:"signer_account_id,omitempty"` diff --git a/services/horizon/internal/db2/history/main.go b/services/horizon/internal/db2/history/main.go index e2030b427d..27b8e03b39 100644 --- a/services/horizon/internal/db2/history/main.go +++ b/services/horizon/internal/db2/history/main.go @@ -163,7 +163,7 @@ const ( // EffectClaimableBalanceSponsorshipRemoved occurs when the sponsorship of a claimable balance ledger entry // is removed - EffectClaimableBalanceSponsorshipRemoved EffectType = 71 // from revoke_sponsorship + EffectClaimableBalanceSponsorshipRemoved EffectType = 71 // from claim_claimable_balance // EffectSignerSponsorshipCreated occurs when the sponsorship of a signer is created EffectSignerSponsorshipCreated EffectType = 72 // from set_options diff --git a/services/horizon/internal/integration/protocol14_sponsorship_ops_test.go b/services/horizon/internal/integration/protocol14_sponsorship_ops_test.go index 8490108de7..5f6bcaf007 100644 --- a/services/horizon/internal/integration/protocol14_sponsorship_ops_test.go +++ b/services/horizon/internal/integration/protocol14_sponsorship_ops_test.go @@ -21,7 +21,7 @@ func TestSponsoredAccount(t *testing.T) { tt := assert.New(t) itest := test.NewIntegrationTest(t, protocol14Config) defer itest.Close() - sponsor := itest.MasterAccount() + sponsor := itest.MasterAccount sponsorPair := itest.Master() // We will create the following operation structure: @@ -29,25 +29,27 @@ func TestSponsoredAccount(t *testing.T) { // CreateAccount A // EndSponsoringFutureReserves (with A as a source) - ops := make([]txnbuild.Operation, 3, 3) newAccountPair, err := keypair.Random() tt.NoError(err) - ops[0] = &txnbuild.BeginSponsoringFutureReserves{ - SponsoredID: newAccountPair.Address(), - } - ops[1] = &txnbuild.CreateAccount{ - Destination: newAccountPair.Address(), - Amount: "1000", - } - ops[2] = &txnbuild.EndSponsoringFutureReserves{ - SourceAccount: &txnbuild.SimpleAccount{ - AccountID: newAccountPair.Address(), + ops := []txnbuild.Operation{ + &txnbuild.BeginSponsoringFutureReserves{ + SponsoredID: newAccountPair.Address(), + }, + + &txnbuild.CreateAccount{ + Destination: newAccountPair.Address(), + Amount: "1000", + }, + &txnbuild.EndSponsoringFutureReserves{ + SourceAccount: &txnbuild.SimpleAccount{ + AccountID: newAccountPair.Address(), + }, }, } signers := []*keypair.Full{sponsorPair, newAccountPair} - txResp, err := itest.SubmitMultiSigOperations(sponsor, signers, ops...) + txResp, err := itest.SubmitMultiSigOperations(sponsor(), signers, ops...) tt.NoError(err) var txResult xdr.TransactionResult @@ -104,26 +106,74 @@ func TestSponsoredAccount(t *testing.T) { account.NumSponsored = 1 // Check effects of CreateAccount Operation - eResponse, err := itest.Client().Effects(sdk.EffectRequest{ForOperation: opRecords[1].GetID()}) + effectsResponse, err := itest.Client().Effects(sdk.EffectRequest{ForOperation: opRecords[1].GetID()}) tt.NoError(err) - effectRecords := eResponse.Embedded.Records + effectRecords := effectsResponse.Embedded.Records tt.Len(effectRecords, 4) tt.IsType(effects.AccountSponsorshipCreated{}, effectRecords[3]) tt.Equal(sponsorPair.Address(), effectRecords[3].(effects.AccountSponsorshipCreated).Sponsor) - // Submit sponsorship revocation in a separate transaction + // Update sponsor + accountToRevoke := newAccountPair.Address() - op := &txnbuild.RevokeSponsorship{ - SponsorshipType: txnbuild.RevokeSponsorshipTypeAccount, - Account: &accountToRevoke, + pairs, _ := itest.CreateAccounts(1, "1000") + newSponsorPair := pairs[0] + newSponsor := func() txnbuild.Account { + request := sdk.AccountRequest{AccountID: newSponsorPair.Address()} + account, e := itest.Client().AccountDetail(request) + tt.NoError(e) + return &account } - txResp, err = itest.SubmitOperations(sponsor, sponsorPair, op) + ops = []txnbuild.Operation{ + &txnbuild.BeginSponsoringFutureReserves{ + SourceAccount: newSponsor(), + SponsoredID: sponsorPair.Address(), + }, + &txnbuild.RevokeSponsorship{ + SourceAccount: sponsor(), + SponsorshipType: txnbuild.RevokeSponsorshipTypeAccount, + Account: &accountToRevoke, + }, + &txnbuild.EndSponsoringFutureReserves{}, + } + signers = []*keypair.Full{sponsorPair, newSponsorPair} + txResp, err = itest.SubmitMultiSigOperations(sponsor(), signers, ops...) tt.NoError(err) err = xdr.SafeUnmarshalBase64(txResp.ResultXdr, &txResult) tt.NoError(err) tt.Equal(xdr.TransactionResultCodeTxSuccess, txResult.Result.Code) + // Verify operation details + response, err = itest.Client().Operations(sdk.OperationRequest{ + ForTransaction: txResp.Hash, + }) + opRecords = response.Embedded.Records + tt.NoError(err) + tt.Len(opRecords, 3) + tt.True(opRecords[1].IsTransactionSuccessful()) + + revokeOp := opRecords[1].(operations.RevokeSponsorship) + tt.Equal(accountToRevoke, *revokeOp.AccountID) + + // Check effects + effectsResponse, err = itest.Client().Effects(sdk.EffectRequest{ForOperation: revokeOp.ID}) + tt.NoError(err) + effectRecords = effectsResponse.Embedded.Records + tt.Len(effectRecords, 1) + tt.IsType(effects.AccountSponsorshipUpdated{}, effectRecords[0]) + effect := effectRecords[0].(effects.AccountSponsorshipUpdated) + tt.Equal(sponsorPair.Address(), effect.FormerSponsor) + tt.Equal(newSponsorPair.Address(), effect.NewSponsor) + + // Revoke sponsorship + + op := &txnbuild.RevokeSponsorship{ + SponsorshipType: txnbuild.RevokeSponsorshipTypeAccount, + Account: &accountToRevoke, + } + txResp = itest.MustSubmitOperations(newSponsor(), newSponsorPair, op) + // Verify operation details response, err = itest.Client().Operations(sdk.OperationRequest{ ForTransaction: txResp.Hash, @@ -133,8 +183,8 @@ func TestSponsoredAccount(t *testing.T) { tt.Len(opRecords, 1) tt.True(opRecords[0].IsTransactionSuccessful()) - revokeOp := opRecords[0].(operations.RevokeSponsorship) - tt.Equal(*op.Account, *revokeOp.AccountID) + revokeOp = opRecords[0].(operations.RevokeSponsorship) + tt.Equal(accountToRevoke, *revokeOp.AccountID) // Make sure that the sponsoree is an (implicit) participant in the revocation operation response, err = itest.Client().Operations(sdk.OperationRequest{ @@ -153,20 +203,182 @@ func TestSponsoredAccount(t *testing.T) { tt.Condition(sponsorshipRevocationPresent) // Check effects - eResponse, err = itest.Client().Effects(sdk.EffectRequest{ForOperation: revokeOp.ID}) + effectsResponse, err = itest.Client().Effects(sdk.EffectRequest{ForOperation: revokeOp.ID}) tt.NoError(err) - effectRecords = eResponse.Embedded.Records + effectRecords = effectsResponse.Embedded.Records tt.Len(effectRecords, 1) tt.IsType(effects.AccountSponsorshipRemoved{}, effectRecords[0]) - tt.Equal(sponsorPair.Address(), effectRecords[0].(effects.AccountSponsorshipRemoved).FormerSponsor) + tt.Equal(newSponsorPair.Address(), effectRecords[0].(effects.AccountSponsorshipRemoved).FormerSponsor) +} + +func TestSponsoredSigner(t *testing.T) { + tt := assert.New(t) + itest := test.NewIntegrationTest(t, protocol14Config) + defer itest.Close() + sponsorPair := itest.Master() + sponsor := itest.MasterAccount + + // Let's create a new account + pairs, _ := itest.CreateAccounts(1, "1000") + newAccountPair := pairs[0] + newAccount := func() txnbuild.Account { + request := sdk.AccountRequest{AccountID: newAccountPair.Address()} + account, err := itest.Client().AccountDetail(request) + tt.NoError(err) + return &account + } + + // Let's add a sponsored data entry + // + // BeginSponsorship N (Source=sponsor) + // SetOptionsSigner (Source=N) + // EndSponsorship (Source=N) + + // unspecific signer + signerKey := "GC3C4AKRBQLHOJ45U4XG35ESVWRDECWO5XLDGYADO6DPR3L7KIDVUMML" + + ops := []txnbuild.Operation{ + &txnbuild.BeginSponsoringFutureReserves{ + SponsoredID: newAccountPair.Address(), + }, + &txnbuild.SetOptions{ + SourceAccount: newAccount(), + Signer: &txnbuild.Signer{ + Address: signerKey, + Weight: 1, + }, + }, + &txnbuild.EndSponsoringFutureReserves{ + SourceAccount: newAccount(), + }, + } + + signers := []*keypair.Full{sponsorPair, newAccountPair} + txResp, err := itest.SubmitMultiSigOperations(sponsor(), signers, ops...) + tt.NoError(err) + + var txResult xdr.TransactionResult + err = xdr.SafeUnmarshalBase64(txResp.ResultXdr, &txResult) + tt.NoError(err) + tt.Equal(xdr.TransactionResultCodeTxSuccess, txResult.Result.Code) + + // Verify that the signer was incorporated + signerAdded := func() bool { + signers := newAccount().(*protocol.Account).Signers + for _, signer := range signers { + if signer.Key == signerKey { + tt.Equal(sponsorPair.Address(), signer.Sponsor) + return true + } + } + return false + } + tt.Eventually(signerAdded, time.Second*10, time.Millisecond*100) + + // Check effects and details of the SetOptions operation + operationsResponse, err := itest.Client().Operations(sdk.OperationRequest{ + ForTransaction: txResp.Hash, + }) + tt.NoError(err) + tt.Len(operationsResponse.Embedded.Records, 3) + setOptionsOp := operationsResponse.Embedded.Records[1].(operations.SetOptions) + tt.Equal(sponsorPair.Address(), setOptionsOp.Sponsor) + + effectsResponse, err := itest.Client().Effects(sdk.EffectRequest{ + ForOperation: setOptionsOp.GetID(), + }) + tt.NoError(err) + if tt.Len(effectsResponse.Embedded.Records, 2) { + signerSponsorshipEffect := effectsResponse.Embedded.Records[1].(effects.SignerSponsorshipCreated) + tt.Equal(sponsorPair.Address(), signerSponsorshipEffect.Sponsor) + tt.Equal(newAccountPair.Address(), signerSponsorshipEffect.Account) + tt.Equal(signerKey, signerSponsorshipEffect.Signer) + } + + // Update sponsor + + pairs, _ = itest.CreateAccounts(1, "1000") + newSponsorPair := pairs[0] + newSponsor := func() txnbuild.Account { + request := sdk.AccountRequest{AccountID: newSponsorPair.Address()} + account, e := itest.Client().AccountDetail(request) + tt.NoError(e) + return &account + } + ops = []txnbuild.Operation{ + &txnbuild.BeginSponsoringFutureReserves{ + SourceAccount: newSponsor(), + SponsoredID: sponsorPair.Address(), + }, + &txnbuild.RevokeSponsorship{ + SponsorshipType: txnbuild.RevokeSponsorshipTypeSigner, + Signer: &txnbuild.SignerID{ + AccountID: newAccountPair.Address(), + SignerAddress: signerKey, + }, + }, + &txnbuild.EndSponsoringFutureReserves{}, + } + signers = []*keypair.Full{sponsorPair, newSponsorPair} + txResp, err = itest.SubmitMultiSigOperations(sponsor(), signers, ops...) + tt.NoError(err) + + err = xdr.SafeUnmarshalBase64(txResp.ResultXdr, &txResult) + tt.NoError(err) + tt.Equal(xdr.TransactionResultCodeTxSuccess, txResult.Result.Code) + + // Verify operation details + response, err := itest.Client().Operations(sdk.OperationRequest{ + ForTransaction: txResp.Hash, + }) + opRecords := response.Embedded.Records + tt.NoError(err) + tt.Len(opRecords, 3) + tt.True(opRecords[1].IsTransactionSuccessful()) + + revokeOp := opRecords[1].(operations.RevokeSponsorship) + tt.Equal(newAccountPair.Address(), *revokeOp.SignerAccountID) + tt.Equal(signerKey, *revokeOp.SignerKey) + + // Check effects + effectsResponse, err = itest.Client().Effects(sdk.EffectRequest{ForOperation: revokeOp.ID}) + tt.NoError(err) + effectRecords := effectsResponse.Embedded.Records + tt.Len(effectRecords, 1) + tt.IsType(effects.SignerSponsorshipUpdated{}, effectRecords[0]) + effect := effectRecords[0].(effects.SignerSponsorshipUpdated) + tt.Equal(sponsorPair.Address(), effect.FormerSponsor) + tt.Equal(newSponsorPair.Address(), effect.NewSponsor) + tt.Equal(signerKey, effect.Signer) + + // Revoke sponsorship + + revoke := txnbuild.RevokeSponsorship{ + SponsorshipType: txnbuild.RevokeSponsorshipTypeSigner, + Signer: &txnbuild.SignerID{ + AccountID: newAccountPair.Address(), + SignerAddress: signerKey, + }, + } + txResp = itest.MustSubmitOperations(newSponsor(), newSponsorPair, &revoke) + + effectsResponse, err = itest.Client().Effects(sdk.EffectRequest{ + ForTransaction: txResp.ID, + }) + tt.NoError(err) + tt.Len(effectsResponse.Embedded.Records, 1) + sponsorshipRemoved := effectsResponse.Embedded.Records[0].(effects.SignerSponsorshipRemoved) + tt.Equal(newSponsorPair.Address(), sponsorshipRemoved.FormerSponsor) + tt.Equal(signerKey, sponsorshipRemoved.Signer) + } -func TestSponsorPreAuthSigner(t *testing.T) { +func TestSponsoredPreAuthSigner(t *testing.T) { tt := assert.New(t) itest := test.NewIntegrationTest(t, protocol14Config) defer itest.Close() sponsorPair := itest.Master() - sponsor := func() txnbuild.Account { return itest.MasterAccount() } + sponsor := itest.MasterAccount // Let's create a new account pairs, _ := itest.CreateAccounts(1, "1000") @@ -204,23 +416,25 @@ func TestSponsorPreAuthSigner(t *testing.T) { // BeginSponsorship N (Source=sponsor) // SetOptionsSigner preAuthHash (Source=N) // EndSponsorship (Source=N) - ops := make([]txnbuild.Operation, 3, 3) - ops[0] = &txnbuild.BeginSponsoringFutureReserves{ - SponsoredID: newAccountPair.Address(), - } preAuthSignerKey := xdr.SignerKey{ Type: xdr.SignerKeyTypeSignerKeyTypePreAuthTx, PreAuthTx: (*xdr.Uint256)(&preAuthHash), } - ops[1] = &txnbuild.SetOptions{ - SourceAccount: newAccount(), - Signer: &txnbuild.Signer{ - Address: preAuthSignerKey.Address(), - Weight: 1, + ops := []txnbuild.Operation{ + &txnbuild.BeginSponsoringFutureReserves{ + SponsoredID: newAccountPair.Address(), + }, + + &txnbuild.SetOptions{ + SourceAccount: newAccount(), + Signer: &txnbuild.Signer{ + Address: preAuthSignerKey.Address(), + Weight: 1, + }, + }, + &txnbuild.EndSponsoringFutureReserves{ + SourceAccount: newAccount(), }, - } - ops[2] = &txnbuild.EndSponsoringFutureReserves{ - SourceAccount: newAccount(), } signers := []*keypair.Full{sponsorPair, newAccountPair} @@ -314,7 +528,7 @@ func TestSponsoredData(t *testing.T) { itest := test.NewIntegrationTest(t, protocol14Config) defer itest.Close() sponsorPair := itest.Master() - sponsor := func() txnbuild.Account { return itest.MasterAccount() } + sponsor := itest.MasterAccount // Let's create a new account pairs, _ := itest.CreateAccounts(1, "1000") @@ -331,17 +545,18 @@ func TestSponsoredData(t *testing.T) { // BeginSponsorship N (Source=sponsor) // ManageData "SponsoredData"="SponsoredValue" (Source=N) // EndSponsorship (Source=N) - ops := make([]txnbuild.Operation, 3, 3) - ops[0] = &txnbuild.BeginSponsoringFutureReserves{ - SponsoredID: newAccountPair.Address(), - } - ops[1] = &txnbuild.ManageData{ - Name: "SponsoredData", - Value: []byte("SponsoredValue"), - SourceAccount: newAccount(), - } - ops[2] = &txnbuild.EndSponsoringFutureReserves{ - SourceAccount: newAccount(), + ops := []txnbuild.Operation{ + &txnbuild.BeginSponsoringFutureReserves{ + SponsoredID: newAccountPair.Address(), + }, + &txnbuild.ManageData{ + Name: "SponsoredData", + Value: []byte("SponsoredValue"), + SourceAccount: newAccount(), + }, + &txnbuild.EndSponsoringFutureReserves{ + SourceAccount: newAccount(), + }, } signers := []*keypair.Full{sponsorPair, newAccountPair} @@ -387,7 +602,64 @@ func TestSponsoredData(t *testing.T) { tt.Equal("SponsoredData", dataSponsorshipEffect.DataName) } - // Test revocation of sponsorship + // Update sponsor + + pairs, _ = itest.CreateAccounts(1, "1000") + newSponsorPair := pairs[0] + newSponsor := func() txnbuild.Account { + request := sdk.AccountRequest{AccountID: newSponsorPair.Address()} + account, e := itest.Client().AccountDetail(request) + tt.NoError(e) + return &account + } + ops = []txnbuild.Operation{ + &txnbuild.BeginSponsoringFutureReserves{ + SourceAccount: newSponsor(), + SponsoredID: sponsorPair.Address(), + }, + &txnbuild.RevokeSponsorship{ + SponsorshipType: txnbuild.RevokeSponsorshipTypeData, + Data: &txnbuild.DataID{ + Account: newAccountPair.Address(), + DataName: "SponsoredData", + }, + }, + &txnbuild.EndSponsoringFutureReserves{}, + } + signers = []*keypair.Full{sponsorPair, newSponsorPair} + txResp, err = itest.SubmitMultiSigOperations(sponsor(), signers, ops...) + tt.NoError(err) + + err = xdr.SafeUnmarshalBase64(txResp.ResultXdr, &txResult) + tt.NoError(err) + tt.Equal(xdr.TransactionResultCodeTxSuccess, txResult.Result.Code) + + // Verify operation details + response, err := itest.Client().Operations(sdk.OperationRequest{ + ForTransaction: txResp.Hash, + }) + opRecords := response.Embedded.Records + tt.NoError(err) + tt.Len(opRecords, 3) + tt.True(opRecords[1].IsTransactionSuccessful()) + + revokeOp := opRecords[1].(operations.RevokeSponsorship) + tt.Equal(newAccountPair.Address(), *revokeOp.DataAccountID) + tt.Equal("SponsoredData", *revokeOp.DataName) + + // Check effects + effectsResponse, err = itest.Client().Effects(sdk.EffectRequest{ForOperation: revokeOp.ID}) + tt.NoError(err) + effectRecords := effectsResponse.Embedded.Records + tt.Len(effectRecords, 1) + tt.IsType(effects.DataSponsorshipUpdated{}, effectRecords[0]) + effect := effectRecords[0].(effects.DataSponsorshipUpdated) + tt.Equal(sponsorPair.Address(), effect.FormerSponsor) + tt.Equal(newSponsorPair.Address(), effect.NewSponsor) + tt.Equal("SponsoredData", effect.DataName) + + // Revoke sponsorship + revoke := txnbuild.RevokeSponsorship{ SponsorshipType: txnbuild.RevokeSponsorshipTypeData, Data: &txnbuild.DataID{ @@ -395,15 +667,15 @@ func TestSponsoredData(t *testing.T) { DataName: "SponsoredData", }, } - tx := itest.MustSubmitOperations(itest.MasterAccount(), itest.Master(), &revoke) + txResp = itest.MustSubmitOperations(newSponsor(), newSponsorPair, &revoke) - effectRecords, err := itest.Client().Effects(sdk.EffectRequest{ - ForTransaction: tx.ID, + effectsResponse, err = itest.Client().Effects(sdk.EffectRequest{ + ForTransaction: txResp.ID, }) tt.NoError(err) - tt.Len(effectRecords.Embedded.Records, 1) - sponsorshipRemoved := effectRecords.Embedded.Records[0].(effects.DataSponsorshipRemoved) - tt.Equal(sponsorPair.Address(), sponsorshipRemoved.FormerSponsor) + tt.Len(effectsResponse.Embedded.Records, 1) + sponsorshipRemoved := effectsResponse.Embedded.Records[0].(effects.DataSponsorshipRemoved) + tt.Equal(newSponsorPair.Address(), sponsorshipRemoved.FormerSponsor) tt.Equal("SponsoredData", sponsorshipRemoved.DataName) } @@ -413,7 +685,7 @@ func TestSponsoredTrustlineAndOffer(t *testing.T) { itest := test.NewIntegrationTest(t, protocol14Config) defer itest.Close() sponsorPair := itest.Master() - sponsor := func() txnbuild.Account { return itest.MasterAccount() } + sponsor := itest.MasterAccount // Let's create a new account pairs, _ := itest.CreateAccounts(1, "1000") @@ -431,24 +703,25 @@ func TestSponsoredTrustlineAndOffer(t *testing.T) { // Change Trust (ABC, sponsor) (Source=N) // ManageSellOffer Buying (ABC, sponsor) (Source=N) // EndSponsorship (Source=N) - ops := make([]txnbuild.Operation, 4, 4) - ops[0] = &txnbuild.BeginSponsoringFutureReserves{ - SponsoredID: newAccountPair.Address(), - } - ops[1] = &txnbuild.ChangeTrust{ - SourceAccount: newAccount(), - Line: txnbuild.CreditAsset{"ABCD", sponsorPair.Address()}, - Limit: txnbuild.MaxTrustlineLimit, - } - ops[2] = &txnbuild.ManageSellOffer{ - SourceAccount: newAccount(), - Selling: txnbuild.NativeAsset{}, - Buying: txnbuild.CreditAsset{"ABCD", sponsorPair.Address()}, - Amount: "3", - Price: "1", - } - ops[3] = &txnbuild.EndSponsoringFutureReserves{ - SourceAccount: newAccount(), + ops := []txnbuild.Operation{ + &txnbuild.BeginSponsoringFutureReserves{ + SponsoredID: newAccountPair.Address(), + }, + &txnbuild.ChangeTrust{ + SourceAccount: newAccount(), + Line: txnbuild.CreditAsset{"ABCD", sponsorPair.Address()}, + Limit: txnbuild.MaxTrustlineLimit, + }, + &txnbuild.ManageSellOffer{ + SourceAccount: newAccount(), + Selling: txnbuild.NativeAsset{}, + Buying: txnbuild.CreditAsset{"ABCD", sponsorPair.Address()}, + Amount: "3", + Price: "1", + }, + &txnbuild.EndSponsoringFutureReserves{ + SourceAccount: newAccount(), + }, } signers := []*keypair.Full{sponsorPair, newAccountPair} @@ -506,33 +779,105 @@ func TestSponsoredTrustlineAndOffer(t *testing.T) { manageOffer := operationsResponse.Embedded.Records[2].(operations.ManageSellOffer) tt.Equal(sponsorPair.Address(), manageOffer.Sponsor) - // Test revocation of sponsorships - revoke1 := txnbuild.RevokeSponsorship{ - SponsorshipType: txnbuild.RevokeSponsorshipTypeOffer, - Offer: &txnbuild.OfferID{ - SellerAccountAddress: offer.Seller, - OfferID: offer.ID, - }, - } - revoke2 := txnbuild.RevokeSponsorship{ - SponsorshipType: txnbuild.RevokeSponsorshipTypeTrustLine, - TrustLine: &txnbuild.TrustLineID{ - Account: newAccountPair.Address(), - Asset: txnbuild.CreditAsset{ - Code: "ABCD", - Issuer: sponsorPair.Address(), + // Update sponsor + + pairs, _ = itest.CreateAccounts(1, "1000") + newSponsorPair := pairs[0] + newSponsor := func() txnbuild.Account { + request := sdk.AccountRequest{AccountID: newSponsorPair.Address()} + account, e := itest.Client().AccountDetail(request) + tt.NoError(e) + return &account + } + ops = []txnbuild.Operation{ + &txnbuild.BeginSponsoringFutureReserves{ + SourceAccount: newSponsor(), + SponsoredID: sponsorPair.Address(), + }, + &txnbuild.RevokeSponsorship{ + SponsorshipType: txnbuild.RevokeSponsorshipTypeOffer, + Offer: &txnbuild.OfferID{ + SellerAccountAddress: offer.Seller, + OfferID: offer.ID, + }, + }, + &txnbuild.RevokeSponsorship{ + SponsorshipType: txnbuild.RevokeSponsorshipTypeTrustLine, + TrustLine: &txnbuild.TrustLineID{ + Account: newAccountPair.Address(), + Asset: txnbuild.CreditAsset{ + Code: "ABCD", + Issuer: sponsorPair.Address(), + }, + }, + }, + &txnbuild.EndSponsoringFutureReserves{}, + } + signers = []*keypair.Full{sponsorPair, newSponsorPair} + txResp, err = itest.SubmitMultiSigOperations(sponsor(), signers, ops...) + tt.NoError(err) + + err = xdr.SafeUnmarshalBase64(txResp.ResultXdr, &txResult) + tt.NoError(err) + tt.Equal(xdr.TransactionResultCodeTxSuccess, txResult.Result.Code) + + // Verify operation details + response, err := itest.Client().Operations(sdk.OperationRequest{ + ForTransaction: txResp.Hash, + }) + opRecords := response.Embedded.Records + tt.NoError(err) + tt.Len(opRecords, 4) + + tt.True(opRecords[1].IsTransactionSuccessful()) + revokeOp := opRecords[1].(operations.RevokeSponsorship) + tt.Equal(offer.ID, *revokeOp.OfferID) + + tt.True(opRecords[2].IsTransactionSuccessful()) + revokeOp = opRecords[2].(operations.RevokeSponsorship) + tt.Equal(newAccountPair.Address(), *revokeOp.TrustlineAccountID) + tt.Equal("ABCD:"+sponsorPair.Address(), *revokeOp.TrustlineAsset) + + // Check effects + effectsResponse, err := itest.Client().Effects(sdk.EffectRequest{ForOperation: revokeOp.ID}) + tt.NoError(err) + effectRecords := effectsResponse.Embedded.Records + tt.Len(effectRecords, 1) + tt.IsType(effects.TrustlineSponsorshipUpdated{}, effectRecords[0]) + effect := effectRecords[0].(effects.TrustlineSponsorshipUpdated) + tt.Equal(sponsorPair.Address(), effect.FormerSponsor) + tt.Equal(newSponsorPair.Address(), effect.NewSponsor) + + // Revoke sponsorship + ops = []txnbuild.Operation{ + &txnbuild.RevokeSponsorship{ + SponsorshipType: txnbuild.RevokeSponsorshipTypeOffer, + Offer: &txnbuild.OfferID{ + SellerAccountAddress: offer.Seller, + OfferID: offer.ID, + }, + }, + &txnbuild.RevokeSponsorship{ + SponsorshipType: txnbuild.RevokeSponsorshipTypeTrustLine, + TrustLine: &txnbuild.TrustLineID{ + Account: newAccountPair.Address(), + Asset: txnbuild.CreditAsset{ + Code: "ABCD", + Issuer: sponsorPair.Address(), + }, }, }, } - tx := itest.MustSubmitOperations(itest.MasterAccount(), itest.Master(), &revoke1, &revoke2) + txResp = itest.MustSubmitOperations(newSponsor(), newSponsorPair, ops...) - effectRecords, err := itest.Client().Effects(sdk.EffectRequest{ - ForTransaction: tx.ID, + effectsResponse, err = itest.Client().Effects(sdk.EffectRequest{ + ForTransaction: txResp.ID, }) tt.NoError(err) - tt.Len(effectRecords.Embedded.Records, 1) - sponsorshipRemoved := effectRecords.Embedded.Records[0].(effects.TrustlineSponsorshipRemoved) - tt.Equal(sponsorPair.Address(), sponsorshipRemoved.FormerSponsor) + // There are intentionally no effects when revoking an Offer + tt.Len(effectsResponse.Embedded.Records, 1) + sponsorshipRemoved := effectsResponse.Embedded.Records[0].(effects.TrustlineSponsorshipRemoved) + tt.Equal(newSponsorPair.Address(), sponsorshipRemoved.FormerSponsor) tt.Equal("ABCD:"+sponsorPair.Address(), sponsorshipRemoved.Asset) } @@ -540,18 +885,26 @@ func TestSponsoredClaimableBalance(t *testing.T) { tt := assert.New(t) itest := test.NewIntegrationTest(t, protocol14Config) defer itest.Close() - master := itest.Master() + sponsorPair := itest.Master() + sponsor := itest.MasterAccount - keys, accounts := itest.CreateAccounts(1, "50") + pairs, _ := itest.CreateAccounts(1, "1000") + newAccountPair := pairs[0] + newAccount := func() txnbuild.Account { + request := sdk.AccountRequest{AccountID: newAccountPair.Address()} + account, err := itest.Client().AccountDetail(request) + tt.NoError(err) + return &account + } ops := []txnbuild.Operation{ &txnbuild.BeginSponsoringFutureReserves{ - SourceAccount: itest.MasterAccount(), - SponsoredID: keys[0].Address(), + SourceAccount: sponsor(), + SponsoredID: newAccountPair.Address(), }, &txnbuild.CreateClaimableBalance{ - SourceAccount: accounts[0], + SourceAccount: newAccount(), Destinations: []txnbuild.Claimant{ - txnbuild.NewClaimant(master.Address(), nil), + txnbuild.NewClaimant(sponsorPair.Address(), nil), }, Amount: "20", Asset: txnbuild.NativeAsset{}, @@ -559,7 +912,8 @@ func TestSponsoredClaimableBalance(t *testing.T) { &txnbuild.EndSponsoringFutureReserves{}, } - txResp, err := itest.SubmitMultiSigOperations(accounts[0], []*keypair.Full{keys[0], master}, ops...) + signers := []*keypair.Full{newAccountPair, sponsorPair} + txResp, err := itest.SubmitMultiSigOperations(newAccount(), signers, ops...) tt.NoError(err) var txResult xdr.TransactionResult @@ -573,6 +927,92 @@ func TestSponsoredClaimableBalance(t *testing.T) { claims := balances.Embedded.Records tt.Len(claims, 1) balance := claims[0] - tt.Equal(master.Address(), balance.Sponsor) + tt.Equal(sponsorPair.Address(), balance.Sponsor) + + // Check effects and details of the CreateClaimableBalance operation + operationsResponse, err := itest.Client().Operations(sdk.OperationRequest{ + ForTransaction: txResp.Hash, + }) + tt.NoError(err) + tt.Len(operationsResponse.Embedded.Records, 3) + createClaimableBalance := operationsResponse.Embedded.Records[1].(operations.CreateClaimableBalance) + tt.Equal(sponsorPair.Address(), createClaimableBalance.Sponsor) + + effectsResponse, err := itest.Client().Effects(sdk.EffectRequest{ + ForOperation: createClaimableBalance.GetID(), + }) + tt.NoError(err) + if tt.Len(effectsResponse.Embedded.Records, 4) { + cbSponsorshipEffect := effectsResponse.Embedded.Records[3].(effects.ClaimableBalanceSponsorshipCreated) + tt.Equal(sponsorPair.Address(), cbSponsorshipEffect.Sponsor) + tt.Equal(newAccountPair.Address(), cbSponsorshipEffect.Account) + tt.Equal(balance.BalanceID, cbSponsorshipEffect.BalanceID) + } + + // Update sponsor + + pairs, _ = itest.CreateAccounts(1, "1000") + newSponsorPair := pairs[0] + newSponsor := func() txnbuild.Account { + request := sdk.AccountRequest{AccountID: newSponsorPair.Address()} + account, e := itest.Client().AccountDetail(request) + tt.NoError(e) + return &account + } + ops = []txnbuild.Operation{ + &txnbuild.BeginSponsoringFutureReserves{ + SourceAccount: newSponsor(), + SponsoredID: sponsorPair.Address(), + }, + &txnbuild.RevokeSponsorship{ + SponsorshipType: txnbuild.RevokeSponsorshipTypeClaimableBalance, + ClaimableBalance: &balance.BalanceID, + }, + &txnbuild.EndSponsoringFutureReserves{}, + } + signers = []*keypair.Full{sponsorPair, newSponsorPair} + txResp, err = itest.SubmitMultiSigOperations(sponsor(), signers, ops...) + tt.NoError(err) + err = xdr.SafeUnmarshalBase64(txResp.ResultXdr, &txResult) + tt.NoError(err) + tt.Equal(xdr.TransactionResultCodeTxSuccess, txResult.Result.Code) + + // Verify operation details + response, err := itest.Client().Operations(sdk.OperationRequest{ + ForTransaction: txResp.Hash, + }) + opRecords := response.Embedded.Records + tt.NoError(err) + tt.Len(opRecords, 3) + tt.True(opRecords[1].IsTransactionSuccessful()) + + revokeOp := opRecords[1].(operations.RevokeSponsorship) + tt.Equal(balance.BalanceID, *revokeOp.ClaimableBalanceID) + + // Check effects + effectsResponse, err = itest.Client().Effects(sdk.EffectRequest{ForOperation: revokeOp.ID}) + tt.NoError(err) + effectRecords := effectsResponse.Embedded.Records + tt.Len(effectRecords, 1) + tt.IsType(effects.ClaimableBalanceSponsorshipUpdated{}, effectRecords[0]) + effect := effectRecords[0].(effects.ClaimableBalanceSponsorshipUpdated) + tt.Equal(sponsorPair.Address(), effect.FormerSponsor) + tt.Equal(newSponsorPair.Address(), effect.NewSponsor) + + // It's not possible to explicitly revoke the sponsorship of a claimable balance, + // so we claim the balance instead + claimOp := &txnbuild.ClaimClaimableBalance{ + BalanceID: balance.BalanceID, + } + txResp = itest.MustSubmitOperations(sponsor(), sponsorPair, claimOp) + effectsResponse, err = itest.Client().Effects(sdk.EffectRequest{ + ForTransaction: txResp.ID, + }) + tt.NoError(err) + effectRecords = effectsResponse.Embedded.Records + tt.Len(effectRecords, 3) + sponsorshipRemoved := effectRecords[2].(effects.ClaimableBalanceSponsorshipRemoved) + tt.Equal(newSponsorPair.Address(), sponsorshipRemoved.FormerSponsor) + tt.Equal(balance.BalanceID, sponsorshipRemoved.BalanceID) } diff --git a/services/horizon/internal/resourceadapter/operations_test.go b/services/horizon/internal/resourceadapter/operations_test.go index 9d39cbe01a..b32e93a53f 100644 --- a/services/horizon/internal/resourceadapter/operations_test.go +++ b/services/horizon/internal/resourceadapter/operations_test.go @@ -230,12 +230,12 @@ func TestPopulateOperation_OperationTypeRevokeSponsorship_Offer(t *testing.T) { tt := assert.New(t) details := `{ - "offer_id": "1000" + "offer_id": 1000 }` resp, err := getJSONResponse(xdr.OperationTypeRevokeSponsorship, details) tt.NoError(err) - tt.Equal("1000", resp["offer_id"]) + tt.Equal(int64(1000), int64((resp["offer_id"]).(float64))) } func TestPopulateOperation_OperationTypeRevokeSponsorship_Trustline(t *testing.T) { diff --git a/txnbuild/revoke_sponsorship.go b/txnbuild/revoke_sponsorship.go index 278bff2301..01436af921 100644 --- a/txnbuild/revoke_sponsorship.go +++ b/txnbuild/revoke_sponsorship.go @@ -25,12 +25,12 @@ const ( type RevokeSponsorship struct { SourceAccount Account SponsorshipType RevokeSponsorshipType - // Account Id (strkey) + // Account ID (strkey) Account *string TrustLine *TrustLineID Offer *OfferID Data *DataID - // Claimable Balance Id + // Claimable Balance ID ClaimableBalance *string Signer *SignerID }