diff --git a/Dockerfile b/Dockerfile index ce82d84..e7da071 100644 --- a/Dockerfile +++ b/Dockerfile @@ -14,11 +14,21 @@ FROM stellar/soroban-rpc # Install bash or sh RUN apt-get update && apt-get install -y bash + # Step 2: Install Stellar Core and copy over app binary FROM ubuntu:jammy AS core-build +RUN apt-get update && \ + apt-get install -y --no-install-recommends ca-certificates curl wget gnupg apt-utils gpg && \ + curl -sSL https://apt.stellar.org/SDF.asc | gpg --dearmor >/etc/apt/trusted.gpg.d/SDF.gpg && \ + echo "deb https://apt.stellar.org jammy stable" >/etc/apt/sources.list.d/SDF.list && \ + echo "deb https://apt.stellar.org jammy testing" >/etc/apt/sources.list.d/SDF-testing.list && \ + echo "deb https://apt.stellar.org jammy unstable" >/etc/apt/sources.list.d/SDF-unstable.list + COPY --from=api-build /bin/wallet-backend /app/ EXPOSE 8001 WORKDIR /app ENTRYPOINT ["./wallet-backend"] + + diff --git a/cmd/serve.go b/cmd/serve.go index d0ce900..3968911 100644 --- a/cmd/serve.go +++ b/cmd/serve.go @@ -93,7 +93,7 @@ func (c *serveCmd) Command() *cobra.Command { Usage: "The minimum number of Channel Accounts that must exist in the database.", OptType: types.Int, ConfigKey: &cfg.NumberOfChannelAccounts, - FlagDefault: 5, + FlagDefault: 15, Required: true, }, } diff --git a/cmd/utils/tss_options.go b/cmd/utils/tss_options.go index 6cb729b..4e23991 100644 --- a/cmd/utils/tss_options.go +++ b/cmd/utils/tss_options.go @@ -33,7 +33,7 @@ func ErrorHandlerJitterChannelBufferSizeOption(configKey *int) *config.ConfigOpt Usage: "Set the buffer size of the Error Handler Jitter channel.", OptType: types.Int, ConfigKey: configKey, - FlagDefault: 100, + FlagDefault: 1000, Required: true, } } @@ -44,7 +44,7 @@ func ErrorHandlerJitterChannelMaxWorkersOption(configKey *int) *config.ConfigOpt Usage: "Set the maximum number of workers for the Error Handler Jitter channel.", OptType: types.Int, ConfigKey: configKey, - FlagDefault: 10, + FlagDefault: 100, Required: true, } } @@ -55,7 +55,7 @@ func ErrorHandlerNonJitterChannelBufferSizeOption(configKey *int) *config.Config Usage: "Set the buffer size of the Error Handler Non Jitter channel.", OptType: types.Int, ConfigKey: configKey, - FlagDefault: 100, + FlagDefault: 1000, Required: true, } @@ -67,7 +67,7 @@ func ErrorHandlerNonJitterChannelMaxWorkersOption(configKey *int) *config.Config Usage: "Set the maximum number of workers for the Error Handler Non Jitter channel.", OptType: types.Int, ConfigKey: configKey, - FlagDefault: 10, + FlagDefault: 100, Required: true, } } @@ -100,7 +100,7 @@ func ErrorHandlerJitterChannelMaxRetriesOptions(configKey *int) *config.ConfigOp Usage: "Set the number of retries for each task in the Error Handler Jitter channel.", OptType: types.Int, ConfigKey: configKey, - FlagDefault: 10, + FlagDefault: 3, Required: true, } @@ -112,7 +112,7 @@ func ErrorHandlerNonJitterChannelMaxRetriesOption(configKey *int) *config.Config Usage: "Set the number of retries for each task in the Error Handler Service Jitter channel.", OptType: types.Int, ConfigKey: configKey, - FlagDefault: 10, + FlagDefault: 3, Required: true, } } @@ -123,7 +123,7 @@ func WebhookHandlerChannelMaxBufferSizeOption(configKey *int) *config.ConfigOpti Usage: "Set the buffer size of the webhook channel.", OptType: types.Int, ConfigKey: configKey, - FlagDefault: 100, + FlagDefault: 1000, Required: true, } } @@ -134,7 +134,7 @@ func WebhookHandlerChannelMaxWorkersOptions(configKey *int) *config.ConfigOption Usage: "Set the max number of workers for the webhook channel.", OptType: types.Int, ConfigKey: configKey, - FlagDefault: 10, + FlagDefault: 100, Required: true, } } diff --git a/internal/tss/channels/webhook_channel.go b/internal/tss/channels/webhook_channel.go index e844f42..17a9839 100644 --- a/internal/tss/channels/webhook_channel.go +++ b/internal/tss/channels/webhook_channel.go @@ -68,20 +68,20 @@ func (p *webhookPool) Receive(payload tss.Payload) { httpResp, err := p.HTTPClient.Post(payload.WebhookURL, "application/json", bytes.NewBuffer(jsonData)) if err != nil { log.Errorf("%s: error making POST request to webhook: %e", WebhookChannelName, err) - } - defer httpResp.Body.Close() - - if httpResp.StatusCode == http.StatusOK { - sent = true - err := p.Store.UpsertTransaction( - ctx, payload.WebhookURL, payload.TransactionHash, payload.TransactionXDR, tss.RPCTXStatus{OtherStatus: tss.SentStatus}) - if err != nil { - log.Errorf("%s: error updating transaction status: %e", WebhookChannelName, err) + } else { + defer httpResp.Body.Close() + if httpResp.StatusCode == http.StatusOK { + sent = true + err := p.Store.UpsertTransaction( + ctx, payload.WebhookURL, payload.TransactionHash, payload.TransactionXDR, tss.RPCTXStatus{OtherStatus: tss.SentStatus}) + if err != nil { + log.Errorf("%s: error updating transaction status: %e", WebhookChannelName, err) + } + break } - break + currentBackoff := p.MinWaitBtwnRetriesMS * (1 << i) + time.Sleep(jitter(time.Duration(currentBackoff)) * time.Millisecond) } - currentBackoff := p.MinWaitBtwnRetriesMS * (1 << i) - time.Sleep(jitter(time.Duration(currentBackoff)) * time.Millisecond) } if !sent { err := p.Store.UpsertTransaction( diff --git a/internal/tss/router/router.go b/internal/tss/router/router.go index ef45e76..3313686 100644 --- a/internal/tss/router/router.go +++ b/internal/tss/router/router.go @@ -65,6 +65,8 @@ func (r *router) Route(payload tss.Payload) error { } } else if payload.RpcGetIngestTxResponse.Status != "" { channel = r.WebhookChannel + } else { + channel = r.RPCCallerChannel } if channel == nil { return fmt.Errorf("payload could not be routed - channel is nil") diff --git a/internal/tss/router/router_test.go b/internal/tss/router/router_test.go index 3ba7c4a..e9d9335 100644 --- a/internal/tss/router/router_test.go +++ b/internal/tss/router/router_test.go @@ -172,12 +172,17 @@ func TestRouter(t *testing.T) { assert.NoError(t, err) webhookChannel.AssertCalled(t, "Send", payload) }) - t.Run("nil_channel_does_not_route", func(t *testing.T) { + t.Run("empty_payload_routes_to_rpc_caller_channel", func(t *testing.T) { payload := tss.Payload{} + rpcCallerChannel. + On("Send", payload). + Return(). + Once() + err := router.Route(payload) - errorJitterChannel.AssertNotCalled(t, "Send", payload) - assert.Equal(t, "payload could not be routed - channel is nil", err.Error()) + assert.NoError(t, err) + rpcCallerChannel.AssertCalled(t, "Send", payload) }) } diff --git a/internal/tss/services/transaction_service.go b/internal/tss/services/transaction_service.go index 9ecf7d9..f3afd90 100644 --- a/internal/tss/services/transaction_service.go +++ b/internal/tss/services/transaction_service.go @@ -5,6 +5,7 @@ import ( "fmt" "github.com/stellar/go/clients/horizonclient" + "github.com/stellar/go/support/log" "github.com/stellar/go/txnbuild" "github.com/stellar/wallet-backend/internal/signing" tsserror "github.com/stellar/wallet-backend/internal/tss/errors" @@ -67,6 +68,25 @@ func (t *transactionService) NetworkPassphrase() string { return t.DistributionAccountSignatureClient.NetworkPassphrase() } +func buildPayments(srcAccount string, operations []txnbuild.Operation) ([]txnbuild.Operation, error) { + var payments []txnbuild.Operation + for _, op := range operations { + origPayment, ok := op.(*txnbuild.Payment) + if !ok { + return nil, fmt.Errorf("unable to convert operation to payment op") + } + payment := &txnbuild.Payment{ + SourceAccount: srcAccount, + Amount: origPayment.Amount, + Destination: origPayment.Destination, + Asset: origPayment.Asset, + } + payments = append(payments, payment) + + } + return payments, nil +} + func (t *transactionService) SignAndBuildNewFeeBumpTransaction(ctx context.Context, origTxXdr string) (*txnbuild.FeeBumpTransaction, error) { genericTx, err := txnbuild.TransactionFromXDR(origTxXdr) if err != nil { @@ -84,10 +104,22 @@ func (t *transactionService) SignAndBuildNewFeeBumpTransaction(ctx context.Conte if err != nil { return nil, fmt.Errorf("getting channel account details from horizon: %w", err) } + + distributionAccountPublicKey, err := t.DistributionAccountSignatureClient.GetAccountPublicKey(ctx) + if err != nil { + return nil, fmt.Errorf("getting distribution account public key: %w", err) + } + + operations, err := buildPayments(distributionAccountPublicKey, originalTx.Operations()) + if err != nil { + return nil, fmt.Errorf("building payment operations: %w", err) + } + log.Info(operations) + tx, err := txnbuild.NewTransaction( txnbuild.TransactionParams{ SourceAccount: &channelAccount, - Operations: originalTx.Operations(), + Operations: operations, BaseFee: int64(t.BaseFee), Preconditions: txnbuild.Preconditions{ TimeBounds: txnbuild.NewTimeout(120), @@ -102,10 +134,10 @@ func (t *transactionService) SignAndBuildNewFeeBumpTransaction(ctx context.Conte if err != nil { return nil, fmt.Errorf("signing transaction with channel account: %w", err) } - // Wrap the transaction in a fee bump tx, signed by the distribution account - distributionAccountPublicKey, err := t.DistributionAccountSignatureClient.GetAccountPublicKey(ctx) + + tx, err = t.DistributionAccountSignatureClient.SignStellarTransaction(ctx, tx, distributionAccountPublicKey) if err != nil { - return nil, fmt.Errorf("getting distribution account public key: %w", err) + return nil, fmt.Errorf("signing transaction with distribution account: %w", err) } feeBumpTx, err := txnbuild.NewFeeBumpTransaction( diff --git a/internal/tss/services/transaction_service_test.go b/internal/tss/services/transaction_service_test.go index 4dd137c..3035932 100644 --- a/internal/tss/services/transaction_service_test.go +++ b/internal/tss/services/transaction_service_test.go @@ -63,6 +63,23 @@ func TestValidateOptions(t *testing.T) { }) } +func TestBuildPayments(t *testing.T) { + dest := "ABCD" + operations := []txnbuild.Operation{ + &txnbuild.Payment{ + Destination: dest, + Amount: "1.0", + Asset: txnbuild.NativeAsset{}, + }, + } + src := "EFGH" + payments, error := buildPayments(src, operations) + assert.Empty(t, error) + assert.Equal(t, src, payments[0].(*txnbuild.Payment).SourceAccount) + assert.Equal(t, dest, payments[0].(*txnbuild.Payment).Destination) + assert.Equal(t, txnbuild.NativeAsset{}, payments[0].(*txnbuild.Payment).Asset) +} + func TestSignAndBuildNewFeeBumpTransaction(t *testing.T) { distributionAccountSignatureClient := signing.SignatureClientMock{} defer distributionAccountSignatureClient.AssertExpectations(t) @@ -115,7 +132,31 @@ func TestSignAndBuildNewFeeBumpTransaction(t *testing.T) { assert.Equal(t, "getting channel account details from horizon: horizon down", err.Error()) }) - t.Run("horizon_client_sign_stellar_transaction_w_channel_account_err", func(t *testing.T) { + t.Run("distribution_account_signature_client_get_account_public_key_err", func(t *testing.T) { + channelAccount := keypair.MustRandom() + channelAccountSignatureClient. + On("GetAccountPublicKey", context.Background()). + Return(channelAccount.Address(), nil). + Once() + + distributionAccountSignatureClient. + On("GetAccountPublicKey", context.Background()). + Return("", errors.New("client down")). + Once() + + horizonClient. + On("AccountDetail", horizonclient.AccountRequest{ + AccountID: channelAccount.Address(), + }). + Return(horizon.Account{AccountID: channelAccount.Address(), Sequence: 1}, nil). + Once() + + feeBumpTx, err := txService.SignAndBuildNewFeeBumpTransaction(context.Background(), txStr) + assert.Empty(t, feeBumpTx) + assert.Equal(t, "getting distribution account public key: client down", err.Error()) + }) + + t.Run("sign_stellar_transaction_w_channel_account_err", func(t *testing.T) { channelAccount := keypair.MustRandom() channelAccountSignatureClient. On("GetAccountPublicKey", context.Background()). @@ -124,6 +165,11 @@ func TestSignAndBuildNewFeeBumpTransaction(t *testing.T) { On("SignStellarTransaction", context.Background(), mock.AnythingOfType("*txnbuild.Transaction"), []string{channelAccount.Address()}). Return(nil, errors.New("unable to sign")). Once() + distributionAccount := keypair.MustRandom() + distributionAccountSignatureClient. + On("GetAccountPublicKey", context.Background()). + Return(distributionAccount.Address(), nil). + Once() horizonClient. On("AccountDetail", horizonclient.AccountRequest{ @@ -137,20 +183,24 @@ func TestSignAndBuildNewFeeBumpTransaction(t *testing.T) { assert.Equal(t, "signing transaction with channel account: unable to sign", err.Error()) }) - t.Run("distribution_account_signature_client_get_account_public_key_err", func(t *testing.T) { + t.Run("sign_stellar_transaction_w_distribition_account_err", func(t *testing.T) { channelAccount := keypair.MustRandom() - signedTx := txnbuild.Transaction{} + signedTx := utils.BuildTestTransaction() channelAccountSignatureClient. On("GetAccountPublicKey", context.Background()). Return(channelAccount.Address(), nil). Once(). On("SignStellarTransaction", context.Background(), mock.AnythingOfType("*txnbuild.Transaction"), []string{channelAccount.Address()}). - Return(&signedTx, nil). + Return(signedTx, nil). Once() + distributionAccount := keypair.MustRandom() distributionAccountSignatureClient. On("GetAccountPublicKey", context.Background()). - Return("", errors.New("client down")). + Return(distributionAccount.Address(), nil). + Once(). + On("SignStellarTransaction", context.Background(), mock.AnythingOfType("*txnbuild.Transaction"), []string{distributionAccount.Address()}). + Return(nil, errors.New("unable to sign")). Once() horizonClient. @@ -162,33 +212,36 @@ func TestSignAndBuildNewFeeBumpTransaction(t *testing.T) { feeBumpTx, err := txService.SignAndBuildNewFeeBumpTransaction(context.Background(), txStr) assert.Empty(t, feeBumpTx) - assert.Equal(t, "getting distribution account public key: client down", err.Error()) + assert.Equal(t, "signing transaction with distribution account: unable to sign", err.Error()) }) - t.Run("horizon_client_sign_stellar_transaction_w_distribition_account_err", func(t *testing.T) { - account := keypair.MustRandom() + t.Run("sign_feebump_transaction_w_distribition_account_err", func(t *testing.T) { + channelAccount := keypair.MustRandom() signedTx := utils.BuildTestTransaction() channelAccountSignatureClient. On("GetAccountPublicKey", context.Background()). - Return(account.Address(), nil). + Return(channelAccount.Address(), nil). Once(). - On("SignStellarTransaction", context.Background(), mock.AnythingOfType("*txnbuild.Transaction"), []string{account.Address()}). + On("SignStellarTransaction", context.Background(), mock.AnythingOfType("*txnbuild.Transaction"), []string{channelAccount.Address()}). Return(signedTx, nil). Once() + distributionAccount := keypair.MustRandom() distributionAccountSignatureClient. On("GetAccountPublicKey", context.Background()). - Return(account.Address(), nil). + Return(distributionAccount.Address(), nil). Once(). + On("SignStellarTransaction", context.Background(), mock.AnythingOfType("*txnbuild.Transaction"), []string{distributionAccount.Address()}). + Return(signedTx, nil). On("SignStellarFeeBumpTransaction", context.Background(), mock.AnythingOfType("*txnbuild.FeeBumpTransaction")). Return(nil, errors.New("unable to sign")). Once() horizonClient. On("AccountDetail", horizonclient.AccountRequest{ - AccountID: account.Address(), + AccountID: channelAccount.Address(), }). - Return(horizon.Account{AccountID: account.Address(), Sequence: 1}, nil). + Return(horizon.Account{AccountID: channelAccount.Address(), Sequence: 1}, nil). Once() feeBumpTx, err := txService.SignAndBuildNewFeeBumpTransaction(context.Background(), txStr) @@ -197,36 +250,39 @@ func TestSignAndBuildNewFeeBumpTransaction(t *testing.T) { }) t.Run("returns_signed_tx", func(t *testing.T) { - account := keypair.MustRandom() + channelAccount := keypair.MustRandom() signedTx := utils.BuildTestTransaction() testFeeBumpTx, _ := txnbuild.NewFeeBumpTransaction( txnbuild.FeeBumpTransactionParams{ Inner: signedTx, - FeeAccount: account.Address(), + FeeAccount: channelAccount.Address(), BaseFee: int64(100), }, ) channelAccountSignatureClient. On("GetAccountPublicKey", context.Background()). - Return(account.Address(), nil). + Return(channelAccount.Address(), nil). Once(). - On("SignStellarTransaction", context.Background(), mock.AnythingOfType("*txnbuild.Transaction"), []string{account.Address()}). + On("SignStellarTransaction", context.Background(), mock.AnythingOfType("*txnbuild.Transaction"), []string{channelAccount.Address()}). Return(signedTx, nil). Once() + distributionAccount := keypair.MustRandom() distributionAccountSignatureClient. On("GetAccountPublicKey", context.Background()). - Return(account.Address(), nil). + Return(distributionAccount.Address(), nil). Once(). + On("SignStellarTransaction", context.Background(), mock.AnythingOfType("*txnbuild.Transaction"), []string{distributionAccount.Address()}). + Return(signedTx, nil). On("SignStellarFeeBumpTransaction", context.Background(), mock.AnythingOfType("*txnbuild.FeeBumpTransaction")). Return(testFeeBumpTx, nil). Once() horizonClient. On("AccountDetail", horizonclient.AccountRequest{ - AccountID: account.Address(), + AccountID: channelAccount.Address(), }). - Return(horizon.Account{AccountID: account.Address(), Sequence: 1}, nil). + Return(horizon.Account{AccountID: channelAccount.Address(), Sequence: 1}, nil). Once() feeBumpTx, err := txService.SignAndBuildNewFeeBumpTransaction(context.Background(), txStr) diff --git a/internal/tss/types.go b/internal/tss/types.go index 4f2088e..b23fa3e 100644 --- a/internal/tss/types.go +++ b/internal/tss/types.go @@ -100,8 +100,16 @@ var FinalCodes = []xdr.TransactionResultCode{ xdr.TransactionResultCodeTxFailed, xdr.TransactionResultCodeTxMissingOperation, xdr.TransactionResultCodeTxInsufficientBalance, + xdr.TransactionResultCodeTxBadAuth, xdr.TransactionResultCodeTxBadAuthExtra, xdr.TransactionResultCodeTxMalformed, + xdr.TransactionResultCodeTxNotSupported, + xdr.TransactionResultCodeTxFeeBumpInnerFailed, + xdr.TransactionResultCodeTxFeeBumpInnerSuccess, + xdr.TransactionResultCodeTxNoAccount, + xdr.TransactionResultCodeTxBadSponsorship, + xdr.TransactionResultCodeTxSorobanInvalid, + xdr.TransactionResultCodeTxBadMinSeqAgeOrGap, } var NonJitterErrorCodes = []xdr.TransactionResultCode{