// ExampleBuildTransaction creates and signs a simple transaction using the build package. // The build package is designed to make it easier and more intuitive to configure and sign // a transaction. func ExampleBuildTransaction() { source := "SA26PHIKZM6CXDGR472SSGUQQRYXM6S437ZNHZGRM6QA4FOPLLLFRGDX" tx := b.Transaction( b.SourceAccount{source}, b.Sequence{1}, b.Payment( b.Destination{"SBQHO2IMYKXAYJFCWGXC7YKLJD2EGDPSK3IUDHVJ6OOTTKLSCK6Z6POM"}, b.NativeAmount{"50.0"}, ), ) txe := tx.Sign(source) txeB64, err := txe.Base64() if err != nil { panic(err) } fmt.Printf("tx base64: %s", txeB64) }
// ToTransactionMutator returns go-stellar-base TransactionMutator func (op PaymentOperationBody) ToTransactionMutator() b.TransactionMutator { mutators := []interface{}{ b.Destination{op.Destination}, } if op.Asset.Code != "" && op.Asset.Issuer != "" { mutators = append( mutators, b.CreditAmount{op.Asset.Code, op.Asset.Issuer, op.Amount}, ) } else { mutators = append( mutators, b.NativeAmount{op.Amount}, ) } if op.Source != nil { mutators = append(mutators, b.SourceAccount{*op.Source}) } return b.Payment(mutators...) }
// ToTransactionMutator returns go-stellar-base TransactionMutator func (op PathPaymentOperationBody) ToTransactionMutator() b.TransactionMutator { var path []b.Asset for _, pathAsset := range op.Path { path = append(path, pathAsset.ToBaseAsset()) } mutators := []interface{}{ b.Destination{op.Destination}, b.PayWithPath{ Asset: op.SendAsset.ToBaseAsset(), MaxAmount: op.SendMax, Path: path, }, } if op.DestinationAsset.Code != "" && op.DestinationAsset.Issuer != "" { mutators = append( mutators, b.CreditAmount{ op.DestinationAsset.Code, op.DestinationAsset.Issuer, op.DestinationAmount, }, ) } else { mutators = append( mutators, b.NativeAmount{op.DestinationAmount}, ) } if op.Source != nil { mutators = append(mutators, b.SourceAccount{*op.Source}) } return b.Payment(mutators...) }
func (rh *RequestHandler) Payment(w http.ResponseWriter, r *http.Request) { source := r.PostFormValue("source") sourceKeypair, err := keypair.Parse(source) if err != nil { log.WithFields(log.Fields{"source": source}).Print("Invalid source parameter") errorBadRequest(w, errorResponseString("invalid_source", "source parameter is invalid")) return } destination := r.PostFormValue("destination") destinationObject, err := rh.AddressResolver.Resolve(destination) if err != nil { log.WithFields(log.Fields{"destination": destination}).Print("Cannot resolve address") errorBadRequest(w, errorResponseString("invalid_destination", "Cannot resolve destination")) return } _, err = keypair.Parse(destinationObject.AccountId) if err != nil { log.WithFields(log.Fields{"AccountId": destinationObject.AccountId}).Print("Invalid AccountId in destination") errorBadRequest(w, errorResponseString("invalid_destination", "destination parameter is invalid")) return } amount := r.PostFormValue("amount") assetCode := r.PostFormValue("asset_code") assetIssuer := r.PostFormValue("asset_issuer") var operationBuilder interface{} if assetCode != "" && assetIssuer != "" { issuerKeypair, err := keypair.Parse(assetIssuer) if err != nil { log.WithFields(log.Fields{"asset_issuer": assetIssuer}).Print("Invalid asset_issuer parameter") errorBadRequest(w, errorResponseString("invalid_issuer", "asset_issuer parameter is invalid")) return } operationBuilder = b.Payment( b.Destination{destinationObject.AccountId}, b.CreditAmount{assetCode, issuerKeypair.Address(), amount}, ) } else if assetCode == "" && assetIssuer == "" { mutators := []interface{}{ b.Destination{destinationObject.AccountId}, b.NativeAmount{amount}, } // Check if destination account exist _, err = rh.Horizon.LoadAccount(destinationObject.AccountId) if err != nil { log.WithFields(log.Fields{"error": err}).Error("Error loading account") operationBuilder = b.CreateAccount(mutators...) } else { operationBuilder = b.Payment(mutators...) } } else { log.Print("Missing asset param.") errorBadRequest(w, errorResponseString("asset_missing_param", "When passing asser both params: `asset_code`, `asset_issuer` are required")) return } memoType := r.PostFormValue("memo_type") memo := r.PostFormValue("memo") if !(((memoType == "") && (memo == "")) || ((memoType != "") && (memo != ""))) { log.Print("Missing one of memo params.") errorBadRequest(w, errorResponseString("memo_missing_param", "When passing memo both params: `memo_type`, `memo` are required")) return } if destinationObject.MemoType != nil { if memoType != "" { log.Print("Memo given in request but federation returned memo fields.") errorBadRequest(w, errorResponseString("cannot_use_memo", "Memo given in request but federation returned memo fields")) return } memoType = *destinationObject.MemoType memo = *destinationObject.Memo } var memoMutator interface{} switch { case memoType == "": break case memoType == "id": id, err := strconv.ParseUint(memo, 10, 64) if err != nil { log.WithFields(log.Fields{"memo": memo}).Print("Cannot convert memo_id value to uint64") errorBadRequest(w, errorResponseString("cannot_convert_memo_id", "Cannot convert memo_id value")) return } memoMutator = b.MemoID{id} case memoType == "text": memoMutator = &b.MemoText{memo} default: log.Print("Not supported memo type: ", memoType) errorBadRequest(w, errorResponseString("memo_not_supported", "Not supported memo type")) return } accountResponse, err := rh.Horizon.LoadAccount(sourceKeypair.Address()) if err != nil { log.WithFields(log.Fields{"error": err}).Error("Cannot load source account") errorBadRequest(w, errorResponseString("source_not_exist", "source account does not exist")) return } sequenceNumber, err := strconv.ParseUint(accountResponse.SequenceNumber, 10, 64) if err != nil { log.WithFields(log.Fields{"error": err}).Error("Cannot convert SequenceNumber") errorServerError(w) return } transactionMutators := []b.TransactionMutator{ b.SourceAccount{source}, b.Sequence{sequenceNumber + 1}, b.Network{rh.Config.NetworkPassphrase}, operationBuilder.(b.TransactionMutator), } if memoMutator != nil { transactionMutators = append(transactionMutators, memoMutator.(b.TransactionMutator)) } tx := b.Transaction(transactionMutators...) if tx.Err != nil { log.WithFields(log.Fields{"err": tx.Err}).Print("Transaction builder error") // TODO when build.OperationBuilder interface is ready check for // create_account and payment errors separately switch { case tx.Err.Error() == "Asset code length is invalid": errorBadRequest(w, errorResponseString("asset_code_invalid", "asset_code param is invalid")) case strings.Contains(tx.Err.Error(), "cannot parse amount"): errorBadRequest(w, errorResponseString("invalid_amount", "amount is invalid")) default: log.WithFields(log.Fields{"err": tx.Err}).Print("Transaction builder error") errorServerError(w) } return } txe := tx.Sign(source) txeB64, err := txe.Base64() if err != nil { log.WithFields(log.Fields{"error": err}).Error("Cannot encode transaction envelope") errorServerError(w) return } submitResponse, err := rh.Horizon.SubmitTransaction(txeB64) if err != nil { log.WithFields(log.Fields{"error": err}).Error("Error submitting transaction") errorServerError(w) return } response, err := json.MarshalIndent(submitResponse, "", " ") if err != nil { log.WithFields(log.Fields{"error": err}).Error("Cannot Marshal submitResponse") errorServerError(w) return } if submitResponse.Ledger != nil { w.Write(response) } else { errorBadRequest(w, string(response)) } }
// HandlerSend implements /send endpoint func (rh *RequestHandler) HandlerSend(c web.C, w http.ResponseWriter, r *http.Request) { request := &compliance.SendRequest{} request.FromRequest(r) err := request.Validate() if err != nil { errorResponse := err.(*protocols.ErrorResponse) log.WithFields(errorResponse.LogData).Error(errorResponse.Error()) server.Write(w, errorResponse) return } destinationObject, stellarToml, err := rh.FederationResolver.Resolve(request.Destination) if err != nil { log.WithFields(log.Fields{ "destination": request.Destination, "err": err, }).Print("Cannot resolve address") server.Write(w, compliance.CannotResolveDestination) return } if stellarToml.AuthServer == "" { log.Print("No AUTH_SERVER in stellar.toml") server.Write(w, compliance.AuthServerNotDefined) return } var payWithMutator *b.PayWithPath if request.SendMax != "" { // Path payment var sendAsset b.Asset if request.SendAssetCode != "" && request.SendAssetIssuer != "" { sendAsset = b.CreditAsset(request.SendAssetCode, request.SendAssetIssuer) } else if request.SendAssetCode == "" && request.SendAssetIssuer == "" { sendAsset = b.NativeAsset() } else { log.Print("Missing send asset param.") server.Write(w, protocols.MissingParameterError) return } payWith := b.PayWith(sendAsset, request.SendMax) for _, asset := range request.Path { if asset.Code == "" && asset.Issuer == "" { payWith = payWith.Through(b.NativeAsset()) } else { payWith = payWith.Through(b.CreditAsset(asset.Code, asset.Issuer)) } } payWithMutator = &payWith } mutators := []interface{}{ b.Destination{destinationObject.AccountID}, b.CreditAmount{ request.AssetCode, request.AssetIssuer, request.Amount, }, } if payWithMutator != nil { mutators = append(mutators, *payWithMutator) } operationMutator := b.Payment(mutators...) if operationMutator.Err != nil { log.WithFields(log.Fields{ "err": operationMutator.Err, }).Error("Error creating operation") server.Write(w, protocols.InternalServerError) return } // Fetch Sender Info senderInfo := "" if rh.Config.Callbacks.FetchInfo != "" { fetchInfoRequest := compliance.FetchInfoRequest{Address: request.Sender} resp, err := rh.Client.PostForm( rh.Config.Callbacks.FetchInfo, fetchInfoRequest.ToValues(), ) if err != nil { log.WithFields(log.Fields{ "fetch_info": rh.Config.Callbacks.FetchInfo, "err": err, }).Error("Error sending request to fetch_info server") server.Write(w, protocols.InternalServerError) return } defer resp.Body.Close() body, err := ioutil.ReadAll(resp.Body) if err != nil { log.WithFields(log.Fields{ "fetch_info": rh.Config.Callbacks.FetchInfo, "err": err, }).Error("Error reading fetch_info server response") server.Write(w, protocols.InternalServerError) return } if resp.StatusCode != http.StatusOK { log.WithFields(log.Fields{ "fetch_info": rh.Config.Callbacks.FetchInfo, "status": resp.StatusCode, "body": string(body), }).Error("Error response from fetch_info server") server.Write(w, protocols.InternalServerError) return } senderInfo = string(body) } memoPreimage := &memo.Memo{ Transaction: memo.Transaction{ SenderInfo: senderInfo, Route: destinationObject.Memo, Extra: request.ExtraMemo, }, } memoJSON := memoPreimage.Marshal() memoHashBytes := sha256.Sum256(memoJSON) memoMutator := &b.MemoHash{xdr.Hash(memoHashBytes)} transaction, err := submitter.BuildTransaction( request.Source, rh.Config.NetworkPassphrase, operationMutator, memoMutator, ) var txBytes bytes.Buffer _, err = xdr.Marshal(&txBytes, transaction) if err != nil { log.Error("Error mashaling transaction") server.Write(w, protocols.InternalServerError) return } txBase64 := base64.StdEncoding.EncodeToString(txBytes.Bytes()) authData := compliance.AuthData{ Sender: request.Sender, NeedInfo: rh.Config.NeedsAuth, Tx: txBase64, Memo: string(memoJSON), } data, err := json.Marshal(authData) if err != nil { log.Error("Error mashaling authData") server.Write(w, protocols.InternalServerError) return } sig, err := rh.SignatureSignerVerifier.Sign(rh.Config.Keys.SigningSeed, data) if err != nil { log.Error("Error signing authData") server.Write(w, protocols.InternalServerError) return } authRequest := compliance.AuthRequest{ Data: string(data), Signature: sig, } resp, err := rh.Client.PostForm( stellarToml.AuthServer, authRequest.ToValues(), ) if err != nil { log.WithFields(log.Fields{ "auth_server": stellarToml.AuthServer, "err": err, }).Error("Error sending request to auth server") server.Write(w, protocols.InternalServerError) return } defer resp.Body.Close() body, err := ioutil.ReadAll(resp.Body) if err != nil { log.Error("Error reading auth server response") server.Write(w, protocols.InternalServerError) return } if resp.StatusCode != 200 { log.WithFields(log.Fields{ "status": resp.StatusCode, "body": string(body), }).Error("Error response from auth server") server.Write(w, protocols.InternalServerError) return } var authResponse compliance.AuthResponse err = json.Unmarshal(body, &authResponse) if err != nil { log.WithFields(log.Fields{ "status": resp.StatusCode, "body": string(body), }).Error("Error unmarshalling auth response") server.Write(w, protocols.InternalServerError) return } response := compliance.SendResponse{ AuthResponse: authResponse, TransactionXdr: txBase64, } server.Write(w, &response) }
// Payment implements /payment endpoint func (rh *RequestHandler) Payment(w http.ResponseWriter, r *http.Request) { request := &bridge.PaymentRequest{} request.FromRequest(r) err := request.Validate() if err != nil { errorResponse := err.(*protocols.ErrorResponse) log.WithFields(errorResponse.LogData).Error(errorResponse.Error()) server.Write(w, errorResponse) return } if request.Source == "" { request.Source = rh.Config.Accounts.BaseSeed } sourceKeypair, _ := keypair.Parse(request.Source) var submitResponse horizon.SubmitTransactionResponse var submitError error if request.ExtraMemo != "" && rh.Config.Compliance != "" { // Compliance server part sendRequest := request.ToComplianceSendRequest() resp, err := rh.Client.PostForm( rh.Config.Compliance+"/send", sendRequest.ToValues(), ) if err != nil { log.WithFields(log.Fields{"err": err}).Error("Error sending request to compliance server") server.Write(w, protocols.InternalServerError) return } defer resp.Body.Close() body, err := ioutil.ReadAll(resp.Body) if err != nil { log.Error("Error reading compliance server response") server.Write(w, protocols.InternalServerError) return } if resp.StatusCode != 200 { log.WithFields(log.Fields{ "status": resp.StatusCode, "body": string(body), }).Error("Error response from compliance server") server.Write(w, protocols.InternalServerError) return } var complianceSendResponse compliance.SendResponse err = json.Unmarshal(body, &complianceSendResponse) if err != nil { log.Error("Error unmarshalling from compliance server") server.Write(w, protocols.InternalServerError) return } if complianceSendResponse.AuthResponse.InfoStatus == compliance.AuthStatusPending || complianceSendResponse.AuthResponse.TxStatus == compliance.AuthStatusPending { log.WithFields(log.Fields{"response": complianceSendResponse}).Info("Compliance response pending") server.Write(w, bridge.NewPaymentPendingError(complianceSendResponse.AuthResponse.Pending)) return } if complianceSendResponse.AuthResponse.InfoStatus == compliance.AuthStatusDenied || complianceSendResponse.AuthResponse.TxStatus == compliance.AuthStatusDenied { log.WithFields(log.Fields{"response": complianceSendResponse}).Info("Compliance response denied") server.Write(w, bridge.PaymentDenied) return } var tx xdr.Transaction err = xdr.SafeUnmarshalBase64(complianceSendResponse.TransactionXdr, &tx) if err != nil { log.Error("Error unmarshalling transaction returned by compliance server") server.Write(w, protocols.InternalServerError) return } submitResponse, submitError = rh.TransactionSubmitter.SignAndSubmitRawTransaction(request.Source, &tx) } else { // Payment without compliance server destinationObject, _, err := rh.FederationResolver.Resolve(request.Destination) if err != nil { log.WithFields(log.Fields{"destination": request.Destination, "err": err}).Print("Cannot resolve address") server.Write(w, bridge.PaymentCannotResolveDestination) return } _, err = keypair.Parse(destinationObject.AccountID) if err != nil { log.WithFields(log.Fields{"AccountId": destinationObject.AccountID}).Print("Invalid AccountId in destination") server.Write(w, protocols.NewInvalidParameterError("destination", request.Destination)) return } var payWithMutator *b.PayWithPath if request.SendMax != "" { // Path payment var sendAsset b.Asset if request.SendAssetCode == "" && request.SendAssetIssuer == "" { sendAsset = b.NativeAsset() } else { sendAsset = b.CreditAsset(request.SendAssetCode, request.SendAssetIssuer) } payWith := b.PayWith(sendAsset, request.SendMax) for i := 0; ; i++ { codeFieldName := fmt.Sprintf("path[%d][asset_code]", i) issuerFieldName := fmt.Sprintf("path[%d][asset_issuer]", i) // If the element does not exist in PostForm break the loop if _, exists := r.PostForm[codeFieldName]; !exists { break } code := r.PostFormValue(codeFieldName) issuer := r.PostFormValue(issuerFieldName) if code == "" && issuer == "" { payWith = payWith.Through(b.NativeAsset()) } else { payWith = payWith.Through(b.CreditAsset(code, issuer)) } } payWithMutator = &payWith } var operationBuilder interface{} if request.AssetCode != "" && request.AssetIssuer != "" { mutators := []interface{}{ b.Destination{destinationObject.AccountID}, b.CreditAmount{request.AssetCode, request.AssetIssuer, request.Amount}, } if payWithMutator != nil { mutators = append(mutators, *payWithMutator) } operationBuilder = b.Payment(mutators...) } else { mutators := []interface{}{ b.Destination{destinationObject.AccountID}, b.NativeAmount{request.Amount}, } if payWithMutator != nil { mutators = append(mutators, *payWithMutator) } // Check if destination account exist _, err = rh.Horizon.LoadAccount(destinationObject.AccountID) if err != nil { log.WithFields(log.Fields{"error": err}).Error("Error loading account") operationBuilder = b.CreateAccount(mutators...) } else { operationBuilder = b.Payment(mutators...) } } memoType := request.MemoType memo := request.Memo if destinationObject.MemoType != "" { if request.MemoType != "" { log.Print("Memo given in request but federation returned memo fields.") server.Write(w, bridge.PaymentCannotUseMemo) return } memoType = destinationObject.MemoType memo = destinationObject.Memo } var memoMutator interface{} switch { case memoType == "": break case memoType == "id": id, err := strconv.ParseUint(memo, 10, 64) if err != nil { log.WithFields(log.Fields{"memo": memo}).Print("Cannot convert memo_id value to uint64") server.Write(w, protocols.NewInvalidParameterError("memo", request.Memo)) return } memoMutator = b.MemoID{id} case memoType == "text": memoMutator = &b.MemoText{memo} case memoType == "hash": memoBytes, err := hex.DecodeString(memo) if err != nil || len(memoBytes) != 32 { log.WithFields(log.Fields{"memo": memo}).Print("Cannot decode hash memo value") server.Write(w, protocols.NewInvalidParameterError("memo", request.Memo)) return } var b32 [32]byte copy(b32[:], memoBytes[0:32]) hash := xdr.Hash(b32) memoMutator = &b.MemoHash{hash} default: log.Print("Not supported memo type: ", memoType) server.Write(w, protocols.NewInvalidParameterError("memo", request.Memo)) return } accountResponse, err := rh.Horizon.LoadAccount(sourceKeypair.Address()) if err != nil { log.WithFields(log.Fields{"error": err}).Error("Cannot load source account") server.Write(w, bridge.PaymentSourceNotExist) return } sequenceNumber, err := strconv.ParseUint(accountResponse.SequenceNumber, 10, 64) if err != nil { log.WithFields(log.Fields{"error": err}).Error("Cannot convert SequenceNumber") server.Write(w, protocols.InternalServerError) return } transactionMutators := []b.TransactionMutator{ b.SourceAccount{request.Source}, b.Sequence{sequenceNumber + 1}, b.Network{rh.Config.NetworkPassphrase}, operationBuilder.(b.TransactionMutator), } if memoMutator != nil { transactionMutators = append(transactionMutators, memoMutator.(b.TransactionMutator)) } tx := b.Transaction(transactionMutators...) if tx.Err != nil { log.WithFields(log.Fields{"err": tx.Err}).Print("Transaction builder error") // TODO when build.OperationBuilder interface is ready check for // create_account and payment errors separately switch { case tx.Err.Error() == "Asset code length is invalid": server.Write( w, protocols.NewInvalidParameterError("asset_code", request.AssetCode), ) case strings.Contains(tx.Err.Error(), "cannot parse amount"): server.Write( w, protocols.NewInvalidParameterError("amount", request.Amount), ) default: log.WithFields(log.Fields{"err": tx.Err}).Print("Transaction builder error") server.Write(w, protocols.InternalServerError) } return } txe := tx.Sign(request.Source) txeB64, err := txe.Base64() if err != nil { log.WithFields(log.Fields{"error": err}).Error("Cannot encode transaction envelope") server.Write(w, protocols.InternalServerError) return } submitResponse, submitError = rh.Horizon.SubmitTransaction(txeB64) } if submitError != nil { log.WithFields(log.Fields{"error": submitError}).Error("Error submitting transaction") server.Write(w, protocols.InternalServerError) return } errorResponse := bridge.ErrorFromHorizonResponse(submitResponse) if errorResponse != nil { log.WithFields(errorResponse.LogData).Error(errorResponse.Error()) server.Write(w, errorResponse) return } // Path payment send amount if submitResponse.ResultXdr != nil { var transactionResult xdr.TransactionResult reader := strings.NewReader(*submitResponse.ResultXdr) b64r := base64.NewDecoder(base64.StdEncoding, reader) _, err := xdr.Unmarshal(b64r, &transactionResult) if err == nil && transactionResult.Result.Code == xdr.TransactionResultCodeTxSuccess { operationResult := (*transactionResult.Result.Results)[0] if operationResult.Tr.PathPaymentResult != nil { sendAmount := operationResult.Tr.PathPaymentResult.SendAmount() submitResponse.SendAmount = amount.String(sendAmount) } } } server.Write(w, &submitResponse) }
func TestTransactionSubmitter(t *testing.T) { mockHorizon := new(mocks.MockHorizon) mockEntityManager := new(mocks.MockEntityManager) mocks.PredefinedTime = time.Now() Convey("TransactionSubmitter", t, func() { seed := "SDZT3EJZ7FZRYNTLOZ7VH6G5UYBFO2IO3Q5PGONMILPCZU3AL7QNZHTE" accountID := "GCLOMB72ODBFUGK4E2BK7VMR3RNZ5WSTMEOGNA2YUVHFR3WMH2XBAB6H" Convey("LoadAccount", func() { transactionSubmitter := NewTransactionSubmitter( mockHorizon, mockEntityManager, "Test SDF Network ; September 2015", mocks.Now, ) Convey("When seed is invalid", func() { _, err := transactionSubmitter.LoadAccount("invalidSeed") assert.NotNil(t, err) }) Convey("When there is a problem loading an account", func() { mockHorizon.On( "LoadAccount", accountID, ).Return( horizon.AccountResponse{}, errors.New("Account not found"), ).Once() _, err := transactionSubmitter.LoadAccount(seed) assert.NotNil(t, err) mockHorizon.AssertExpectations(t) }) Convey("Successfully loads an account", func() { mockHorizon.On( "LoadAccount", accountID, ).Return( horizon.AccountResponse{ AccountID: accountID, SequenceNumber: "10372672437354496", }, nil, ).Once() account, err := transactionSubmitter.LoadAccount(seed) assert.Nil(t, err) assert.Equal(t, account.Keypair.Address(), accountID) assert.Equal(t, account.Seed, seed) assert.Equal(t, account.SequenceNumber, uint64(10372672437354496)) mockHorizon.AssertExpectations(t) }) }) Convey("SubmitTransaction", func() { Convey("Submits transaction without a memo", func() { operation := b.Payment( b.Destination{"GB3W7VQ2A2IOQIS4LUFUMRC2DWXONUDH24ROLE6RS4NGUNHVSXKCABOM"}, b.NativeAmount{"100"}, ) Convey("Error response from horizon", func() { transactionSubmitter := NewTransactionSubmitter( mockHorizon, mockEntityManager, "Test SDF Network ; September 2015", mocks.Now, ) mockHorizon.On( "LoadAccount", accountID, ).Return( horizon.AccountResponse{ AccountID: accountID, SequenceNumber: "10372672437354496", }, nil, ).Once() err := transactionSubmitter.InitAccount(seed) assert.Nil(t, err) txB64 := "AAAAAJbmB/pwwloZXCaCr9WR3Fue2lNhHGaDWKVOWO7MPq4QAAAAZAAk2eQAAAABAAAAAAAAAAAAAAABAAAAAAAAAAEAAAAAd2/WGgaQ6CJcXQtGRFodrubQZ9ci5ZPRlxpqNPWV1CAAAAAAAAAAADuaygAAAAAAAAAAAcw+rhAAAABAyFjIMIZOtstCWtZlVBDj1AhTmsk5v1i2GGY4by2b5mgZoXXGgFTB8sfbQav0LzFKCcxY8h+9xPMT2e9xznAfDw==" // Persist sending transaction mockEntityManager.On( "Persist", mock.AnythingOfType("*entities.SentTransaction"), ).Return(nil).Once().Run(func(args mock.Arguments) { transaction := args.Get(0).(*entities.SentTransaction) assert.Equal(t, "4f885999be6ea7891052a53e496bcfb5c5a1a5bfb31923f649b028fdc74dd050", transaction.TransactionID) assert.Equal(t, "sending", string(transaction.Status)) assert.Equal(t, "GCLOMB72ODBFUGK4E2BK7VMR3RNZ5WSTMEOGNA2YUVHFR3WMH2XBAB6H", transaction.Source) assert.Equal(t, mocks.PredefinedTime, transaction.SubmittedAt) assert.Equal(t, txB64, transaction.EnvelopeXdr) }) // Persist failure mockEntityManager.On( "Persist", mock.AnythingOfType("*entities.SentTransaction"), ).Return(nil).Once().Run(func(args mock.Arguments) { transaction := args.Get(0).(*entities.SentTransaction) assert.Equal(t, "4f885999be6ea7891052a53e496bcfb5c5a1a5bfb31923f649b028fdc74dd050", transaction.TransactionID) assert.Equal(t, "failure", string(transaction.Status)) assert.Equal(t, "GCLOMB72ODBFUGK4E2BK7VMR3RNZ5WSTMEOGNA2YUVHFR3WMH2XBAB6H", transaction.Source) assert.Equal(t, mocks.PredefinedTime, transaction.SubmittedAt) assert.Equal(t, txB64, transaction.EnvelopeXdr) }) mockHorizon.On("SubmitTransaction", txB64).Return( horizon.SubmitTransactionResponse{ Ledger: nil, Extras: &horizon.SubmitTransactionResponseExtras{ ResultXdr: "AAAAAAAAAGT/////AAAAAQAAAAAAAAAB////+wAAAAA=", // no_destination }, }, nil, ).Once() _, err = transactionSubmitter.SubmitTransaction(seed, operation, nil) assert.Nil(t, err) mockHorizon.AssertExpectations(t) }) Convey("Bad Sequence response from horizon", func() { transactionSubmitter := NewTransactionSubmitter( mockHorizon, mockEntityManager, "Test SDF Network ; September 2015", mocks.Now, ) mockHorizon.On( "LoadAccount", accountID, ).Return( horizon.AccountResponse{ AccountID: accountID, SequenceNumber: "10372672437354496", }, nil, ).Once() err := transactionSubmitter.InitAccount(seed) assert.Nil(t, err) txB64 := "AAAAAJbmB/pwwloZXCaCr9WR3Fue2lNhHGaDWKVOWO7MPq4QAAAAZAAk2eQAAAABAAAAAAAAAAAAAAABAAAAAAAAAAEAAAAAd2/WGgaQ6CJcXQtGRFodrubQZ9ci5ZPRlxpqNPWV1CAAAAAAAAAAADuaygAAAAAAAAAAAcw+rhAAAABAyFjIMIZOtstCWtZlVBDj1AhTmsk5v1i2GGY4by2b5mgZoXXGgFTB8sfbQav0LzFKCcxY8h+9xPMT2e9xznAfDw==" // Persist sending transaction mockEntityManager.On( "Persist", mock.AnythingOfType("*entities.SentTransaction"), ).Return(nil).Once().Run(func(args mock.Arguments) { transaction := args.Get(0).(*entities.SentTransaction) assert.Equal(t, "4f885999be6ea7891052a53e496bcfb5c5a1a5bfb31923f649b028fdc74dd050", transaction.TransactionID) assert.Equal(t, "sending", string(transaction.Status)) assert.Equal(t, "GCLOMB72ODBFUGK4E2BK7VMR3RNZ5WSTMEOGNA2YUVHFR3WMH2XBAB6H", transaction.Source) assert.Equal(t, mocks.PredefinedTime, transaction.SubmittedAt) assert.Equal(t, txB64, transaction.EnvelopeXdr) }) // Persist failure mockEntityManager.On( "Persist", mock.AnythingOfType("*entities.SentTransaction"), ).Return(nil).Once().Run(func(args mock.Arguments) { transaction := args.Get(0).(*entities.SentTransaction) assert.Equal(t, "4f885999be6ea7891052a53e496bcfb5c5a1a5bfb31923f649b028fdc74dd050", transaction.TransactionID) assert.Equal(t, "failure", string(transaction.Status)) assert.Equal(t, "GCLOMB72ODBFUGK4E2BK7VMR3RNZ5WSTMEOGNA2YUVHFR3WMH2XBAB6H", transaction.Source) assert.Equal(t, mocks.PredefinedTime, transaction.SubmittedAt) assert.Equal(t, txB64, transaction.EnvelopeXdr) }) mockHorizon.On("SubmitTransaction", txB64).Return( horizon.SubmitTransactionResponse{ Ledger: nil, Extras: &horizon.SubmitTransactionResponseExtras{ ResultXdr: "AAAAAAAAAAD////7AAAAAA==", // tx_bad_seq }, }, nil, ).Once() // Updating sequence number mockHorizon.On( "LoadAccount", accountID, ).Return( horizon.AccountResponse{ AccountID: accountID, SequenceNumber: "100", }, nil, ).Once() _, err = transactionSubmitter.SubmitTransaction(seed, operation, nil) assert.Nil(t, err) assert.Equal(t, uint64(100), transactionSubmitter.Accounts[seed].SequenceNumber) mockHorizon.AssertExpectations(t) }) Convey("Successfully submits a transaction", func() { transactionSubmitter := NewTransactionSubmitter( mockHorizon, mockEntityManager, "Test SDF Network ; September 2015", mocks.Now, ) mockHorizon.On( "LoadAccount", accountID, ).Return( horizon.AccountResponse{ AccountID: accountID, SequenceNumber: "10372672437354496", }, nil, ).Once() err := transactionSubmitter.InitAccount(seed) assert.Nil(t, err) txB64 := "AAAAAJbmB/pwwloZXCaCr9WR3Fue2lNhHGaDWKVOWO7MPq4QAAAAZAAk2eQAAAABAAAAAAAAAAAAAAABAAAAAAAAAAEAAAAAd2/WGgaQ6CJcXQtGRFodrubQZ9ci5ZPRlxpqNPWV1CAAAAAAAAAAADuaygAAAAAAAAAAAcw+rhAAAABAyFjIMIZOtstCWtZlVBDj1AhTmsk5v1i2GGY4by2b5mgZoXXGgFTB8sfbQav0LzFKCcxY8h+9xPMT2e9xznAfDw==" // Persist sending transaction mockEntityManager.On( "Persist", mock.AnythingOfType("*entities.SentTransaction"), ).Return(nil).Once().Run(func(args mock.Arguments) { transaction := args.Get(0).(*entities.SentTransaction) assert.Equal(t, "4f885999be6ea7891052a53e496bcfb5c5a1a5bfb31923f649b028fdc74dd050", transaction.TransactionID) assert.Equal(t, "sending", string(transaction.Status)) assert.Equal(t, "GCLOMB72ODBFUGK4E2BK7VMR3RNZ5WSTMEOGNA2YUVHFR3WMH2XBAB6H", transaction.Source) assert.Equal(t, mocks.PredefinedTime, transaction.SubmittedAt) assert.Equal(t, txB64, transaction.EnvelopeXdr) }) // Persist failure mockEntityManager.On( "Persist", mock.AnythingOfType("*entities.SentTransaction"), ).Return(nil).Once().Run(func(args mock.Arguments) { transaction := args.Get(0).(*entities.SentTransaction) assert.Equal(t, "4f885999be6ea7891052a53e496bcfb5c5a1a5bfb31923f649b028fdc74dd050", transaction.TransactionID) assert.Equal(t, "success", string(transaction.Status)) assert.Equal(t, "GCLOMB72ODBFUGK4E2BK7VMR3RNZ5WSTMEOGNA2YUVHFR3WMH2XBAB6H", transaction.Source) assert.Equal(t, mocks.PredefinedTime, transaction.SubmittedAt) assert.Equal(t, txB64, transaction.EnvelopeXdr) }) ledger := uint64(1486276) mockHorizon.On("SubmitTransaction", txB64).Return( horizon.SubmitTransactionResponse{Ledger: &ledger}, nil, ).Once() response, err := transactionSubmitter.SubmitTransaction(seed, operation, nil) assert.Nil(t, err) assert.Equal(t, *response.Ledger, ledger) assert.Equal(t, uint64(10372672437354497), transactionSubmitter.Accounts[seed].SequenceNumber) mockHorizon.AssertExpectations(t) }) }) Convey("Submits transaction with a memo", func() { operation := b.Payment( b.Destination{"GB3W7VQ2A2IOQIS4LUFUMRC2DWXONUDH24ROLE6RS4NGUNHVSXKCABOM"}, b.NativeAmount{"100"}, ) memo := b.MemoText{"Testing!"} Convey("Successfully submits a transaction", func() { transactionSubmitter := NewTransactionSubmitter( mockHorizon, mockEntityManager, "Test SDF Network ; September 2015", mocks.Now, ) mockHorizon.On( "LoadAccount", accountID, ).Return( horizon.AccountResponse{ AccountID: accountID, SequenceNumber: "10372672437354496", }, nil, ).Once() err := transactionSubmitter.InitAccount(seed) assert.Nil(t, err) txB64 := "AAAAAJbmB/pwwloZXCaCr9WR3Fue2lNhHGaDWKVOWO7MPq4QAAAAZAAk2eQAAAABAAAAAAAAAAEAAAAIVGVzdGluZyEAAAABAAAAAAAAAAEAAAAAd2/WGgaQ6CJcXQtGRFodrubQZ9ci5ZPRlxpqNPWV1CAAAAAAAAAAADuaygAAAAAAAAAAAcw+rhAAAABAU5ahFsd28sVKSUFcmAiEf+zSLXhf9HG/pJuQirR0s43zs7Y43vM8T3sIvJWHgwMADaZiy/D+evYWd/vS/uO8Ag==" // Persist sending transaction mockEntityManager.On( "Persist", mock.AnythingOfType("*entities.SentTransaction"), ).Return(nil).Once().Run(func(args mock.Arguments) { transaction := args.Get(0).(*entities.SentTransaction) assert.Equal(t, "60cb3c020b0c97352cbabdf68a822b04baea61927b0f1ac31260a9f8d0150316", transaction.TransactionID) assert.Equal(t, "sending", string(transaction.Status)) assert.Equal(t, "GCLOMB72ODBFUGK4E2BK7VMR3RNZ5WSTMEOGNA2YUVHFR3WMH2XBAB6H", transaction.Source) assert.Equal(t, mocks.PredefinedTime, transaction.SubmittedAt) assert.Equal(t, txB64, transaction.EnvelopeXdr) }) // Persist failure mockEntityManager.On( "Persist", mock.AnythingOfType("*entities.SentTransaction"), ).Return(nil).Once().Run(func(args mock.Arguments) { transaction := args.Get(0).(*entities.SentTransaction) assert.Equal(t, "60cb3c020b0c97352cbabdf68a822b04baea61927b0f1ac31260a9f8d0150316", transaction.TransactionID) assert.Equal(t, "success", string(transaction.Status)) assert.Equal(t, "GCLOMB72ODBFUGK4E2BK7VMR3RNZ5WSTMEOGNA2YUVHFR3WMH2XBAB6H", transaction.Source) assert.Equal(t, mocks.PredefinedTime, transaction.SubmittedAt) assert.Equal(t, txB64, transaction.EnvelopeXdr) }) ledger := uint64(1486276) mockHorizon.On("SubmitTransaction", txB64).Return( horizon.SubmitTransactionResponse{Ledger: &ledger}, nil, ).Once() response, err := transactionSubmitter.SubmitTransaction(seed, operation, memo) assert.Nil(t, err) assert.Equal(t, *response.Ledger, ledger) assert.Equal(t, uint64(10372672437354497), transactionSubmitter.Accounts[seed].SequenceNumber) mockHorizon.AssertExpectations(t) }) }) }) }) }
func (rh *RequestHandler) Send(w http.ResponseWriter, r *http.Request) { destination := r.PostFormValue("destination") assetCode := r.PostFormValue("asset_code") amount := r.PostFormValue("amount") destinationObject, err := rh.AddressResolver.Resolve(destination) if err != nil { log.WithFields(log.Fields{"destination": destination}).Print("Cannot resolve address") errorBadRequest(w, errorResponseString("invalid_destination", "Cannot resolve destination")) return } _, err = keypair.Parse(destinationObject.AccountId) if err != nil { log.WithFields(log.Fields{"AccountId": destinationObject.AccountId}).Print("Invalid AccountId in destination") errorBadRequest(w, errorResponseString("invalid_destination", "destination parameter is invalid")) return } if !rh.isAssetAllowed(assetCode) { log.Print("Asset code not allowed: ", assetCode) errorBadRequest(w, errorResponseString("invalid_asset_code", "Given assetCode not allowed")) return } issuingKeypair, err := keypair.Parse(*rh.Config.Accounts.IssuingSeed) if err != nil { log.Print("Invalid issuingSeed") errorServerError(w) return } operationMutator := b.Payment( b.Destination{destinationObject.AccountId}, b.CreditAmount{assetCode, issuingKeypair.Address(), amount}, ) if operationMutator.Err != nil { log.Print("Error creating operationMutator ", operationMutator.Err) errorServerError(w) return } memoType := r.PostFormValue("memo_type") memo := r.PostFormValue("memo") if !(((memoType == "") && (memo == "")) || ((memoType != "") && (memo != ""))) { log.Print("Missing one of memo params.") errorBadRequest(w, errorResponseString("memo_missing_param", "When passing memo both params: `memo_type`, `memo` are required")) return } if destinationObject.MemoType != nil { if memoType != "" { log.Print("Memo given in request but federation returned memo fields.") errorBadRequest(w, errorResponseString("cannot_use_memo", "Memo given in request but federation returned memo fields")) return } memoType = *destinationObject.MemoType memo = *destinationObject.Memo } var memoMutator interface{} switch { case memoType == "": break case memoType == "id": id, err := strconv.ParseUint(memo, 10, 64) if err != nil { log.WithFields(log.Fields{"memo": memo}).Print("Cannot convert memo_id value to uint64") errorBadRequest(w, errorResponseString("cannot_convert_memo_id", "Cannot convert memo_id value")) return } memoMutator = b.MemoID{id} case memoType == "text": memoMutator = b.MemoText{memo} default: log.Print("Not supported memo type: ", memoType) errorBadRequest(w, errorResponseString("memo_not_supported", "Not supported memo type")) return } submitResponse, err := rh.TransactionSubmitter.SubmitTransaction( *rh.Config.Accounts.IssuingSeed, operationMutator, memoMutator, ) if err != nil { log.Print("Error submitting transaction ", err) errorServerError(w) return } if submitResponse.Errors != nil { var errorString string if submitResponse.Errors.OperationErrorCode != "" { switch submitResponse.Errors.OperationErrorCode { case "payment_malformed": errorString = errorResponseString( "payment_malformed", "Operation is malformed.", ) case "payment_underfunded": errorString = errorResponseString( "payment_underfunded", "Not enough funds to send this transaction.", ) case "payment_src_no_trust": errorString = errorResponseString( "payment_src_no_trust", "No trustline on source account.", ) case "payment_src_not_authorized": errorString = errorResponseString( "payment_src_not_authorized", "Source not authorized to transfer.", ) case "payment_no_destination": errorString = errorResponseString( "payment_no_destination", "Destination account does not exist.", ) case "payment_no_trust": errorString = errorResponseString( "payment_no_trust", "Destination missing a trust line for asset.", ) case "payment_not_authorized": errorString = errorResponseString( "payment_not_authorized", "Destination not authorized to trust asset. It needs to be allowed first by using /authorize endpoint.", ) case "payment_line_full": errorString = errorResponseString( "payment_line_full", "Sending this payment would make a destination go above their limit.", ) case "payment_no_issuer": errorString = errorResponseString( "payment_no_issuer", "Missing issuer on asset.", ) default: errorServerError(w) return } } else if submitResponse.Errors.TransactionErrorCode != "" { switch submitResponse.Errors.TransactionErrorCode { case "transaction_bad_seq": errorString = errorResponseString( "transaction_bad_seq", "Bad Sequence. Please, try again.", ) default: errorServerError(w) return } } errorBadRequest(w, errorString) return } json, err := json.MarshalIndent(submitResponse, "", " ") if err != nil { errorServerError(w) return } w.Write(json) }
func TestRequestHandlerSend(t *testing.T) { mockAddressResolverHelper := new(MockAddressResolverHelper) addressResolver := AddressResolver{mockAddressResolverHelper} mockTransactionSubmitter := new(mocks.MockTransactionSubmitter) IssuingSeed := "SC34WILLHVADXMP6ACPMIRA6TRAWJMVCLPFNW7S6MUMXJVLAZUC4EWHP" AuthorizingSeed := "SC37TBSIAYKIDQ6GTGLT2HSORLIHZQHBXVFI5P5K4Q5TSHRTRBK3UNWG" config := config.Config{ Assets: []string{"USD", "EUR"}, Accounts: &config.Accounts{ // GD4I7AFSLZGTDL34TQLWJOM2NHLIIOEKD5RHHZUW54HERBLSIRKUOXRR IssuingSeed: &IssuingSeed, // GBQXA3ABGQGTCLEVZIUTDRWWJOQD5LSAEDZAG7GMOGD2HBLWONGUVO4I AuthorizingSeed: &AuthorizingSeed, }, } issuingKeypair, err := keypair.Parse(*config.Accounts.IssuingSeed) if err != nil { panic(err) } requestHandler := RequestHandler{ AddressResolver: addressResolver, Config: &config, TransactionSubmitter: mockTransactionSubmitter, } testServer := httptest.NewServer(http.HandlerFunc(requestHandler.Send)) defer testServer.Close() Convey("Given send request", t, func() { Convey("When destination is invalid", func() { destination := "GD3YBOYIUVLU" assetCode := "USD" Convey("it should return error", func() { statusCode, response := getResponse(testServer, url.Values{"destination": {destination}, "asset_code": {assetCode}}) responseString := strings.TrimSpace(string(response)) assert.Equal(t, 400, statusCode) assert.Equal(t, errorResponseString("invalid_destination", "destination parameter is invalid"), responseString) }) }) Convey("When assetCode is invalid", func() { destination := "GDSIKW43UA6JTOA47WVEBCZ4MYC74M3GNKNXTVDXFHXYYTNO5GGVN632" assetCode := "GBP" Convey("it should return error", func() { statusCode, response := getResponse(testServer, url.Values{"destination": {destination}, "asset_code": {assetCode}}) responseString := strings.TrimSpace(string(response)) assert.Equal(t, 400, statusCode) assert.Equal(t, errorResponseString("invalid_asset_code", "Given assetCode not allowed"), responseString) }) }) Convey("When destination is a Stellar address", func() { params := url.Values{ "asset_code": {"USD"}, "amount": {"20"}, "destination": {"bob*stellar.org"}, } Convey("When stellar.toml does not exist", func() { mockAddressResolverHelper.On( "GetStellarToml", "stellar.org", ).Return( StellarToml{}, errors.New("stellar.toml response status code indicates error"), ).Once() Convey("it should return error", func() { statusCode, response := getResponse(testServer, params) responseString := strings.TrimSpace(string(response)) assert.Equal(t, 400, statusCode) assert.Equal(t, errorResponseString("invalid_destination", "Cannot resolve destination"), responseString) }) }) Convey("When stellar.toml does not contain FEDERATION_SERVER", func() { mockAddressResolverHelper.On( "GetStellarToml", "stellar.org", ).Return( StellarToml{}, nil, ).Once() Convey("it should return error", func() { statusCode, response := getResponse(testServer, params) responseString := strings.TrimSpace(string(response)) assert.Equal(t, 400, statusCode) assert.Equal(t, errorResponseString("invalid_destination", "Cannot resolve destination"), responseString) }) }) Convey("When GetDestination() errors", func() { federationServer := "http://api.example.com" mockAddressResolverHelper.On( "GetStellarToml", "stellar.org", ).Return( StellarToml{&federationServer}, nil, ).Once() mockAddressResolverHelper.On( "GetDestination", "http://api.example.com", "bob*stellar.org", ).Return( StellarDestination{}, errors.New("Only HTTPS federation servers allowed"), ).Once() Convey("it should return error", func() { statusCode, response := getResponse(testServer, params) responseString := strings.TrimSpace(string(response)) assert.Equal(t, 400, statusCode) assert.Equal(t, errorResponseString("invalid_destination", "Cannot resolve destination"), responseString) }) }) Convey("When federation response is correct", func() { federationServer := "http://api.example.com" mockAddressResolverHelper.On( "GetStellarToml", "stellar.org", ).Return( StellarToml{&federationServer}, nil, ).Once() mockAddressResolverHelper.On( "GetDestination", "http://api.example.com", "bob*stellar.org", ).Return(StellarDestination{AccountId: "GDSIKW43UA6JTOA47WVEBCZ4MYC74M3GNKNXTVDXFHXYYTNO5GGVN632"}, nil).Once() var ledger uint64 ledger = 1988728 expectedSubmitResponse := horizon.SubmitTransactionResponse{&ledger, nil, nil} operation := b.Payment( b.Destination{"GDSIKW43UA6JTOA47WVEBCZ4MYC74M3GNKNXTVDXFHXYYTNO5GGVN632"}, b.CreditAmount{ params.Get("asset_code"), issuingKeypair.Address(), params.Get("amount"), }, ) mockTransactionSubmitter.On( "SubmitTransaction", *config.Accounts.IssuingSeed, operation, nil, ).Return(expectedSubmitResponse, nil).Once() Convey("it should return success", func() { statusCode, response := getResponse(testServer, params) responseString := strings.TrimSpace(string(response)) expectedResponse, err := json.MarshalIndent(expectedSubmitResponse, "", " ") if err != nil { panic(err) } assert.Equal(t, 200, statusCode) assert.Equal(t, string(expectedResponse), responseString) }) }) }) Convey("When destination is an accountId", func() { params := url.Values{ "asset_code": {"USD"}, "amount": {"20"}, "destination": {"GDSIKW43UA6JTOA47WVEBCZ4MYC74M3GNKNXTVDXFHXYYTNO5GGVN632"}, } Convey("When params are valid", func() { operation := b.Payment( b.Destination{params.Get("destination")}, b.CreditAmount{ params.Get("asset_code"), issuingKeypair.Address(), params.Get("amount"), }, ) Convey("transaction fails", func() { mockTransactionSubmitter.On( "SubmitTransaction", *config.Accounts.IssuingSeed, operation, nil, ).Return( horizon.SubmitTransactionResponse{}, errors.New("Error sending transaction"), ).Once() Convey("it should return server error", func() { statusCode, response := getResponse(testServer, params) responseString := strings.TrimSpace(string(response)) assert.Equal(t, 500, statusCode) assert.Equal(t, getServerErrorResponseString(), responseString) mockTransactionSubmitter.AssertExpectations(t) }) }) Convey("transaction succeeds (no memo)", func() { var ledger uint64 ledger = 100 expectedSubmitResponse := horizon.SubmitTransactionResponse{ Ledger: &ledger, } mockTransactionSubmitter.On( "SubmitTransaction", *config.Accounts.IssuingSeed, operation, nil, ).Return(expectedSubmitResponse, nil).Once() Convey("it should succeed", func() { statusCode, response := getResponse(testServer, params) var actualSubmitTransactionResponse horizon.SubmitTransactionResponse json.Unmarshal(response, &actualSubmitTransactionResponse) assert.Equal(t, 200, statusCode) assert.Equal(t, expectedSubmitResponse, actualSubmitTransactionResponse) mockTransactionSubmitter.AssertExpectations(t) }) }) Convey("transaction succeeds (with memo)", func() { params.Add("memo_type", "id") params.Add("memo", "123") var ledger uint64 ledger = 100 expectedSubmitResponse := horizon.SubmitTransactionResponse{ Ledger: &ledger, } memo := b.MemoID{123} mockTransactionSubmitter.On( "SubmitTransaction", *config.Accounts.IssuingSeed, operation, memo, ).Return(expectedSubmitResponse, nil).Once() Convey("it should succeed", func() { statusCode, response := getResponse(testServer, params) var actualSubmitTransactionResponse horizon.SubmitTransactionResponse json.Unmarshal(response, &actualSubmitTransactionResponse) assert.Equal(t, 200, statusCode) assert.Equal(t, expectedSubmitResponse, actualSubmitTransactionResponse) mockTransactionSubmitter.AssertExpectations(t) }) }) }) }) }) }