Skip to content

Commit

Permalink
services: wrap transactions with fee bump for sponsored accounts (#20)
Browse files Browse the repository at this point in the history
What
This PR adds the POST /tx/create-fee-bump endpoint. Also, the AccountModel was created.

Why
Implement the fee bump functionality.
  • Loading branch information
CaioTeixeira95 authored Jun 18, 2024
1 parent 6e43cc6 commit f41bed0
Show file tree
Hide file tree
Showing 16 changed files with 991 additions and 46 deletions.
43 changes: 43 additions & 0 deletions internal/data/accounts.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package data

import (
"context"
"fmt"

"github.com/stellar/wallet-backend/internal/db"
)

type AccountModel struct {
DB db.ConnectionPool
}

func (m *AccountModel) Insert(ctx context.Context, address string) error {
const query = `INSERT INTO accounts (stellar_address) VALUES ($1) ON CONFLICT DO NOTHING`
_, err := m.DB.ExecContext(ctx, query, address)
if err != nil {
return fmt.Errorf("inserting address %s: %w", address, err)
}

return nil
}

func (m *AccountModel) Delete(ctx context.Context, address string) error {
const query = `DELETE FROM accounts WHERE stellar_address = $1`
_, err := m.DB.ExecContext(ctx, query, address)
if err != nil {
return fmt.Errorf("deleting address %s: %w", address, err)
}

return nil
}

func (m *AccountModel) Exists(ctx context.Context, address string) (bool, error) {
const query = `SELECT EXISTS(SELECT stellar_address FROM accounts WHERE stellar_address = $1)`
var exists bool
err := m.DB.GetContext(ctx, &exists, query, address)
if err != nil {
return false, fmt.Errorf("checking if account %s exists: %w", address, err)
}

return exists, nil
}
96 changes: 96 additions & 0 deletions internal/data/accounts_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
package data

import (
"context"
"database/sql"
"testing"

"github.com/stellar/go/keypair"
"github.com/stellar/wallet-backend/internal/db"
"github.com/stellar/wallet-backend/internal/db/dbtest"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestAccountModelInsert(t *testing.T) {
dbt := dbtest.Open(t)
defer dbt.Close()

dbConnectionPool, err := db.OpenDBConnectionPool(dbt.DSN)
require.NoError(t, err)
defer dbConnectionPool.Close()

m := &AccountModel{
DB: dbConnectionPool,
}

ctx := context.Background()
address := keypair.MustRandom().Address()
err = m.Insert(ctx, address)
require.NoError(t, err)

var dbAddress sql.NullString
err = m.DB.GetContext(ctx, &dbAddress, "SELECT stellar_address FROM accounts LIMIT 1")
require.NoError(t, err)

assert.True(t, dbAddress.Valid)
assert.Equal(t, address, dbAddress.String)
}

func TestAccountModelDelete(t *testing.T) {
dbt := dbtest.Open(t)
defer dbt.Close()

dbConnectionPool, err := db.OpenDBConnectionPool(dbt.DSN)
require.NoError(t, err)
defer dbConnectionPool.Close()

m := &AccountModel{
DB: dbConnectionPool,
}

ctx := context.Background()
address := keypair.MustRandom().Address()
result, err := m.DB.ExecContext(ctx, "INSERT INTO accounts (stellar_address) VALUES ($1)", address)
require.NoError(t, err)
rowAffected, err := result.RowsAffected()
require.NoError(t, err)
require.Equal(t, int64(1), rowAffected)

err = m.Delete(ctx, address)
require.NoError(t, err)

var dbAddress sql.NullString
err = m.DB.GetContext(ctx, &dbAddress, "SELECT stellar_address FROM accounts LIMIT 1")
assert.ErrorIs(t, err, sql.ErrNoRows)
}

func TestAccountModelExists(t *testing.T) {
dbt := dbtest.Open(t)
defer dbt.Close()

dbConnectionPool, err := db.OpenDBConnectionPool(dbt.DSN)
require.NoError(t, err)
defer dbConnectionPool.Close()

m := &AccountModel{
DB: dbConnectionPool,
}

ctx := context.Background()
address := keypair.MustRandom().Address()

exists, err := m.Exists(ctx, address)
require.NoError(t, err)
assert.False(t, exists)

result, err := m.DB.ExecContext(ctx, "INSERT INTO accounts (stellar_address) VALUES ($1)", address)
require.NoError(t, err)
rowAffected, err := result.RowsAffected()
require.NoError(t, err)
require.Equal(t, int64(1), rowAffected)

exists, err = m.Exists(ctx, address)
require.NoError(t, err)
assert.True(t, exists)
}
2 changes: 2 additions & 0 deletions internal/data/models.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (

type Models struct {
Payments *PaymentModel
Account *AccountModel
}

func NewModels(db db.ConnectionPool) (*Models, error) {
Expand All @@ -17,5 +18,6 @@ func NewModels(db db.ConnectionPool) (*Models, error) {

return &Models{
Payments: &PaymentModel{DB: db},
Account: &AccountModel{DB: db},
}, nil
}
22 changes: 1 addition & 21 deletions internal/data/payments.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ type Payment struct {

func (m *PaymentModel) GetLatestLedgerSynced(ctx context.Context, cursorName string) (uint32, error) {
var lastSyncedLedger uint32
err := m.DB.QueryRowxContext(ctx, `SELECT value FROM ingest_store WHERE key = $1`, cursorName).Scan(&lastSyncedLedger)
err := m.DB.GetContext(ctx, &lastSyncedLedger, `SELECT value FROM ingest_store WHERE key = $1`, cursorName)
// First run, key does not exist yet
if err == sql.ErrNoRows {
return 0, nil
Expand Down Expand Up @@ -91,23 +91,3 @@ func (m *PaymentModel) AddPayment(ctx context.Context, tx db.Transaction, paymen

return nil
}

func (m *PaymentModel) SubscribeAddress(ctx context.Context, address string) error {
const query = `INSERT INTO accounts (stellar_address) VALUES ($1) ON CONFLICT DO NOTHING`
_, err := m.DB.ExecContext(ctx, query, address)
if err != nil {
return fmt.Errorf("subscribing address %s to payments tracking: %w", address, err)
}

return nil
}

func (m *PaymentModel) UnsubscribeAddress(ctx context.Context, address string) error {
const query = `DELETE FROM accounts WHERE stellar_address = $1`
_, err := m.DB.ExecContext(ctx, query, address)
if err != nil {
return fmt.Errorf("unsubscribing address %s to payments tracking: %w", address, err)
}

return nil
}
44 changes: 32 additions & 12 deletions internal/data/payments_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,16 @@ package data

import (
"context"
"database/sql"
"testing"
"time"

"github.com/stellar/go/keypair"
"github.com/stellar/wallet-backend/internal/db"
"github.com/stellar/wallet-backend/internal/db/dbtest"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestAddPayment(t *testing.T) {
func TestPaymentModelAddPayment(t *testing.T) {
dbt := dbtest.Open(t)
defer dbt.Close()

Expand Down Expand Up @@ -101,27 +99,49 @@ func TestAddPayment(t *testing.T) {
})
}

func TestSubscribeAddress(t *testing.T) {
func TestPaymentModelGetLatestLedgerSynced(t *testing.T) {
dbt := dbtest.Open(t)
defer dbt.Close()

dbConnectionPool, err := db.OpenDBConnectionPool(dbt.DSN)
require.NoError(t, err)
defer dbConnectionPool.Close()

ctx := context.Background()
m := &PaymentModel{
DB: dbConnectionPool,
}

ctx := context.Background()
address := keypair.MustRandom().Address()
err = m.SubscribeAddress(ctx, address)
const key = "ingest_store_key"
lastSyncedLedger, err := m.GetLatestLedgerSynced(ctx, key)
require.NoError(t, err)
assert.Equal(t, uint32(0), lastSyncedLedger)

_, err = dbConnectionPool.ExecContext(ctx, `INSERT INTO ingest_store (key, value) VALUES ($1, $2)`, key, 123)
require.NoError(t, err)

lastSyncedLedger, err = m.GetLatestLedgerSynced(ctx, key)
require.NoError(t, err)
assert.Equal(t, uint32(123), lastSyncedLedger)
}

func TestPaymentModelUpdateLatestLedgerSynced(t *testing.T) {
dbt := dbtest.Open(t)
defer dbt.Close()
dbConnectionPool, err := db.OpenDBConnectionPool(dbt.DSN)
require.NoError(t, err)
defer dbConnectionPool.Close()

ctx := context.Background()
m := &PaymentModel{
DB: dbConnectionPool,
}

var dbAddress sql.NullString
err = m.DB.GetContext(ctx, &dbAddress, "SELECT stellar_address FROM accounts LIMIT 1")
const key = "ingest_store_key"
err = m.UpdateLatestLedgerSynced(ctx, key, 123)
require.NoError(t, err)

assert.True(t, dbAddress.Valid)
assert.Equal(t, address, dbAddress.String)
var lastSyncedLedger uint32
err = m.DB.GetContext(ctx, &lastSyncedLedger, `SELECT value FROM ingest_store WHERE key = $1`, key)
require.NoError(t, err)
assert.Equal(t, uint32(123), lastSyncedLedger)
}
49 changes: 48 additions & 1 deletion internal/serve/httphandler/account_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (

"github.com/stellar/go/support/http/httpdecode"
"github.com/stellar/go/support/render/httpjson"
"github.com/stellar/go/txnbuild"
"github.com/stellar/wallet-backend/internal/entities"
"github.com/stellar/wallet-backend/internal/serve/httperror"
"github.com/stellar/wallet-backend/internal/services"
Expand All @@ -21,6 +22,10 @@ type SponsorAccountCreationRequest struct {
Signers []entities.Signer `json:"signers" validate:"required,gt=0,dive"`
}

type CreateFeeBumpTransactionRequest struct {
Transaction string `json:"transaction" validate:"required"`
}

type TransactionEnvelopeResponse struct {
Transaction string `json:"transaction"`
NetworkPassphrase string `json:"networkPassphrase"`
Expand All @@ -47,7 +52,6 @@ func (h AccountHandler) SponsorAccountCreation(rw http.ResponseWriter, req *http
return
}

// TODO: store the sponsored account on the database.
txe, networkPassphrase, err := h.AccountSponsorshipService.SponsorAccountCreationTransaction(ctx, reqBody.Address, reqBody.Signers, h.SupportedAssets)
if err != nil {
if errors.Is(err, services.ErrSponsorshipLimitExceeded) {
Expand All @@ -70,3 +74,46 @@ func (h AccountHandler) SponsorAccountCreation(rw http.ResponseWriter, req *http
}
httpjson.Render(rw, respBody, httpjson.JSON)
}

func (h AccountHandler) CreateFeeBumpTransaction(rw http.ResponseWriter, req *http.Request) {
ctx := req.Context()

var reqBody CreateFeeBumpTransactionRequest
httpErr := DecodeJSONAndValidate(ctx, req, &reqBody)
if httpErr != nil {
httpErr.Render(rw)
return
}

genericTx, err := txnbuild.TransactionFromXDR(reqBody.Transaction)
if err != nil {
httperror.BadRequest("Could not parse transaction envelope.", nil).Render(rw)
return
}

tx, ok := genericTx.Transaction()
if !ok {
httperror.BadRequest("Cannot accept a fee-bump transaction.", nil).Render(rw)
return
}

feeBumpTxe, networkPassphrase, err := h.AccountSponsorshipService.WrapTransaction(ctx, tx)
if err != nil {
var errOperationNotAllowed *services.ErrOperationNotAllowed
switch {
case errors.Is(err, services.ErrAccountNotEligibleForBeingSponsored), errors.Is(err, services.ErrFeeExceedsMaximumBaseFee),
errors.Is(err, services.ErrNoSignaturesProvided), errors.As(err, &errOperationNotAllowed):
httperror.BadRequest(err.Error(), nil).Render(rw)
return
default:
httperror.InternalServerError(ctx, "", err, nil).Render(rw)
return
}
}

respBody := TransactionEnvelopeResponse{
Transaction: feeBumpTxe,
NetworkPassphrase: networkPassphrase,
}
httpjson.Render(rw, respBody, httpjson.JSON)
}
Loading

0 comments on commit f41bed0

Please sign in to comment.