From 1829d3ed7e53745f0408fe2a224bc787d1f0e312 Mon Sep 17 00:00:00 2001 From: Aditya Vyas Date: Thu, 5 Feb 2026 14:45:45 -0500 Subject: [PATCH 01/23] Change operation_xdr column from TEXT to BYTEA Part of the plan to store XDR data as raw bytes for efficiency. --- internal/db/migrations/2025-06-10.3-operations.sql | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/db/migrations/2025-06-10.3-operations.sql b/internal/db/migrations/2025-06-10.3-operations.sql index a7cb9161..11fc2cbb 100644 --- a/internal/db/migrations/2025-06-10.3-operations.sql +++ b/internal/db/migrations/2025-06-10.3-operations.sql @@ -17,7 +17,7 @@ CREATE TABLE operations ( 'INVOKE_HOST_FUNCTION', 'EXTEND_FOOTPRINT_TTL', 'RESTORE_FOOTPRINT' ) ), - operation_xdr TEXT, + operation_xdr BYTEA, result_code TEXT NOT NULL, successful BOOLEAN NOT NULL, ledger_number INTEGER NOT NULL, From c844520cbb9119498b22823f9d00e24a0845da41 Mon Sep 17 00:00:00 2001 From: Aditya Vyas Date: Thu, 5 Feb 2026 14:46:05 -0500 Subject: [PATCH 02/23] Add XDRBytea type for storing XDR data as BYTEA Similar pattern to HashBytea but uses base64 encoding (standard for XDR) and supports variable-length data. --- internal/indexer/types/types.go | 37 +++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/internal/indexer/types/types.go b/internal/indexer/types/types.go index 9c939abe..7ab221e0 100644 --- a/internal/indexer/types/types.go +++ b/internal/indexer/types/types.go @@ -34,6 +34,7 @@ package types import ( "database/sql" "database/sql/driver" + "encoding/base64" "encoding/hex" "encoding/json" "fmt" @@ -166,6 +167,42 @@ func (h HashBytea) String() string { return string(h) } +// XDRBytea represents XDR data stored as BYTEA in the database. +// Storage format: raw XDR bytes (variable length) +// Go representation: base64 string +type XDRBytea string + +// Scan implements sql.Scanner - converts BYTEA (raw bytes) to base64 string +func (x *XDRBytea) Scan(value any) error { + if value == nil { + *x = "" + return nil + } + bytes, ok := value.([]byte) + if !ok { + return fmt.Errorf("expected []byte, got %T", value) + } + *x = XDRBytea(base64.StdEncoding.EncodeToString(bytes)) + return nil +} + +// Value implements driver.Valuer - converts base64 string to raw bytes +func (x XDRBytea) Value() (driver.Value, error) { + if x == "" { + return nil, nil + } + bytes, err := base64.StdEncoding.DecodeString(string(x)) + if err != nil { + return nil, fmt.Errorf("decoding base64 XDR %s: %w", x, err) + } + return bytes, nil +} + +// String returns the XDR as a base64 string. +func (x XDRBytea) String() string { + return string(x) +} + type ContractType string const ( From b5ac8adfe7df14c821b9e35d5554af48a2c59466 Mon Sep 17 00:00:00 2001 From: Aditya Vyas Date: Thu, 5 Feb 2026 14:46:15 -0500 Subject: [PATCH 03/23] Update Operation struct to use XDRBytea type Changed OperationXDR field from string to XDRBytea for automatic base64/BYTEA conversion. --- internal/indexer/types/types.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/indexer/types/types.go b/internal/indexer/types/types.go index 7ab221e0..6e873f90 100644 --- a/internal/indexer/types/types.go +++ b/internal/indexer/types/types.go @@ -409,7 +409,7 @@ type Operation struct { // The parent transaction's to_id can be derived: ID &^ 0xFFF ID int64 `json:"id,omitempty" db:"id"` OperationType OperationType `json:"operationType,omitempty" db:"operation_type"` - OperationXDR string `json:"operationXdr,omitempty" db:"operation_xdr"` + OperationXDR XDRBytea `json:"operationXdr,omitempty" db:"operation_xdr"` ResultCode string `json:"resultCode,omitempty" db:"result_code"` Successful bool `json:"successful,omitempty" db:"successful"` LedgerNumber uint32 `json:"ledgerNumber,omitempty" db:"ledger_number"` From c23554ee31d15d9bc2109ffb3571edbf2fde8b94 Mon Sep 17 00:00:00 2001 From: Aditya Vyas Date: Thu, 5 Feb 2026 14:46:26 -0500 Subject: [PATCH 04/23] Update ConvertOperation to use XDRBytea type Cast the base64 XDR string to XDRBytea for proper database storage. --- internal/indexer/processors/utils.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/indexer/processors/utils.go b/internal/indexer/processors/utils.go index 04c88b4f..dc00ba48 100644 --- a/internal/indexer/processors/utils.go +++ b/internal/indexer/processors/utils.go @@ -350,7 +350,7 @@ func ConvertOperation( return &types.Operation{ ID: opID, OperationType: types.OperationTypeFromXDR(op.Body.Type), - OperationXDR: xdrOpStr, + OperationXDR: types.XDRBytea(xdrOpStr), ResultCode: resultCode, Successful: successful, LedgerCreatedAt: transaction.Ledger.ClosedAt(), From 23ba1117aa94bbbb696e8a0c804711ed7254d7cf Mon Sep 17 00:00:00 2001 From: Aditya Vyas Date: Thu, 5 Feb 2026 14:46:59 -0500 Subject: [PATCH 05/23] Update operations.go for BYTEA operation_xdr storage - Change UNNEST cast from text[] to bytea[] - Convert XDRBytea to raw bytes in BatchInsert - Update BatchCopy to pass raw bytes instead of pgtype.Text --- internal/data/operations.go | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/internal/data/operations.go b/internal/data/operations.go index d8f92e8e..b5557032 100644 --- a/internal/data/operations.go +++ b/internal/data/operations.go @@ -284,7 +284,7 @@ func (m *OperationModel) BatchInsert( // 1. Flatten the operations into parallel slices ids := make([]int64, len(operations)) operationTypes := make([]string, len(operations)) - operationXDRs := make([]string, len(operations)) + operationXDRs := make([][]byte, len(operations)) resultCodes := make([]string, len(operations)) successfulFlags := make([]bool, len(operations)) ledgerNumbers := make([]uint32, len(operations)) @@ -293,7 +293,13 @@ func (m *OperationModel) BatchInsert( for i, op := range operations { ids[i] = op.ID operationTypes[i] = string(op.OperationType) - operationXDRs[i] = op.OperationXDR + xdrBytes, err := op.OperationXDR.Value() + if err != nil { + return nil, fmt.Errorf("converting operation XDR to bytes for op %d: %w", op.ID, err) + } + if xdrBytes != nil { + operationXDRs[i] = xdrBytes.([]byte) + } resultCodes[i] = op.ResultCode successfulFlags[i] = op.Successful ledgerNumbers[i] = op.LedgerNumber @@ -327,7 +333,7 @@ func (m *OperationModel) BatchInsert( SELECT UNNEST($1::bigint[]) AS id, UNNEST($2::text[]) AS operation_type, - UNNEST($3::text[]) AS operation_xdr, + UNNEST($3::bytea[]) AS operation_xdr, UNNEST($4::text[]) AS result_code, UNNEST($5::boolean[]) AS successful, UNNEST($6::bigint[]) AS ledger_number, @@ -415,10 +421,14 @@ func (m *OperationModel) BatchCopy( []string{"id", "operation_type", "operation_xdr", "result_code", "successful", "ledger_number", "ledger_created_at"}, pgx.CopyFromSlice(len(operations), func(i int) ([]any, error) { op := operations[i] + xdrBytes, xdrErr := op.OperationXDR.Value() + if xdrErr != nil { + return nil, fmt.Errorf("converting operation XDR to bytes for op %d: %w", op.ID, xdrErr) + } return []any{ pgtype.Int8{Int64: op.ID, Valid: true}, pgtype.Text{String: string(op.OperationType), Valid: true}, - pgtype.Text{String: op.OperationXDR, Valid: true}, + xdrBytes, pgtype.Text{String: op.ResultCode, Valid: true}, pgtype.Bool{Bool: op.Successful, Valid: true}, pgtype.Int4{Int32: int32(op.LedgerNumber), Valid: true}, From a347cc7678592ea4e6cbd9e042700009bd9069cf Mon Sep 17 00:00:00 2001 From: Aditya Vyas Date: Thu, 5 Feb 2026 14:47:10 -0500 Subject: [PATCH 06/23] Add forceResolver directive to operationXdr field Forces GraphQL to use a resolver for operationXdr to convert XDRBytea to base64 string for API consumers. --- internal/serve/graphql/schema/operation.graphqls | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/serve/graphql/schema/operation.graphqls b/internal/serve/graphql/schema/operation.graphqls index 0755fc38..dcec1b37 100644 --- a/internal/serve/graphql/schema/operation.graphqls +++ b/internal/serve/graphql/schema/operation.graphqls @@ -3,7 +3,7 @@ type Operation{ id: Int64! operationType: OperationType! - operationXdr: String! + operationXdr: String! @goField(forceResolver: true) resultCode: String! successful: Boolean! ledgerNumber: UInt32! From ddaeeee25534b9d4923515f9ea602d286ad7891e Mon Sep 17 00:00:00 2001 From: Aditya Vyas Date: Thu, 5 Feb 2026 14:47:36 -0500 Subject: [PATCH 07/23] Run gql-generate and fix test_utils.go type - Update test_utils.go to use types.XDRBytea - Regenerate GraphQL code with OperationXdr resolver --- internal/serve/graphql/generated/generated.go | 58 ++++++++++++++----- .../serve/graphql/resolvers/test_utils.go | 2 +- 2 files changed, 46 insertions(+), 14 deletions(-) diff --git a/internal/serve/graphql/generated/generated.go b/internal/serve/graphql/generated/generated.go index 47cb64d3..d24604b7 100644 --- a/internal/serve/graphql/generated/generated.go +++ b/internal/serve/graphql/generated/generated.go @@ -14,11 +14,10 @@ import ( "github.com/99designs/gqlgen/graphql" "github.com/99designs/gqlgen/graphql/introspection" - gqlparser "github.com/vektah/gqlparser/v2" - "github.com/vektah/gqlparser/v2/ast" - "github.com/stellar/wallet-backend/internal/indexer/types" "github.com/stellar/wallet-backend/internal/serve/graphql/scalars" + gqlparser "github.com/vektah/gqlparser/v2" + "github.com/vektah/gqlparser/v2/ast" ) // region ************************** generated!.gotpl ************************** @@ -164,7 +163,7 @@ type ComplexityRoot struct { LedgerCreatedAt func(childComplexity int) int LedgerNumber func(childComplexity int) int OperationType func(childComplexity int) int - OperationXDR func(childComplexity int) int + OperationXdr func(childComplexity int) int ResultCode func(childComplexity int) int StateChanges func(childComplexity int, first *int32, after *string, last *int32, before *string) int Successful func(childComplexity int) int @@ -395,6 +394,8 @@ type MutationResolver interface { CreateFeeBumpTransaction(ctx context.Context, input CreateFeeBumpTransactionInput) (*CreateFeeBumpTransactionPayload, error) } type OperationResolver interface { + OperationXdr(ctx context.Context, obj *types.Operation) (string, error) + Transaction(ctx context.Context, obj *types.Operation) (*types.Transaction, error) Accounts(ctx context.Context, obj *types.Operation) ([]*types.Account, error) StateChanges(ctx context.Context, obj *types.Operation, first *int32, after *string, last *int32, before *string) (*StateChangeConnection, error) @@ -1009,11 +1010,11 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin return e.complexity.Operation.OperationType(childComplexity), true case "Operation.operationXdr": - if e.complexity.Operation.OperationXDR == nil { + if e.complexity.Operation.OperationXdr == nil { break } - return e.complexity.Operation.OperationXDR(childComplexity), true + return e.complexity.Operation.OperationXdr(childComplexity), true case "Operation.resultCode": if e.complexity.Operation.ResultCode == nil { @@ -2316,7 +2317,7 @@ type CreateFeeBumpTransactionPayload { type Operation{ id: Int64! operationType: OperationType! - operationXdr: String! + operationXdr: String! @goField(forceResolver: true) resultCode: String! successful: Boolean! ledgerNumber: UInt32! @@ -6822,7 +6823,7 @@ func (ec *executionContext) _Operation_operationXdr(ctx context.Context, field g }() resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { ctx = rctx // use context from middleware stack in children - return obj.OperationXDR, nil + return ec.resolvers.Operation().OperationXdr(rctx, obj) }) if err != nil { ec.Error(ctx, err) @@ -6843,8 +6844,8 @@ func (ec *executionContext) fieldContext_Operation_operationXdr(_ context.Contex fc = &graphql.FieldContext{ Object: "Operation", Field: field, - IsMethod: false, - IsResolver: false, + IsMethod: true, + IsResolver: true, Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { return nil, errors.New("field of type String does not have child fields") }, @@ -17012,10 +17013,41 @@ func (ec *executionContext) _Operation(ctx context.Context, sel ast.SelectionSet atomic.AddUint32(&out.Invalids, 1) } case "operationXdr": - out.Values[i] = ec._Operation_operationXdr(ctx, field, obj) - if out.Values[i] == graphql.Null { - atomic.AddUint32(&out.Invalids, 1) + field := field + + innerFunc := func(ctx context.Context, fs *graphql.FieldSet) (res graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + } + }() + res = ec._Operation_operationXdr(ctx, field, obj) + if res == graphql.Null { + atomic.AddUint32(&fs.Invalids, 1) + } + return res + } + + if field.Deferrable != nil { + dfs, ok := deferred[field.Deferrable.Label] + di := 0 + if ok { + dfs.AddField(field) + di = len(dfs.Values) - 1 + } else { + dfs = graphql.NewFieldSet([]graphql.CollectedField{field}) + deferred[field.Deferrable.Label] = dfs + } + dfs.Concurrently(di, func(ctx context.Context) graphql.Marshaler { + return innerFunc(ctx, dfs) + }) + + // don't run the out.Concurrently() call below + out.Values[i] = graphql.Null + continue } + + out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) }) case "resultCode": out.Values[i] = ec._Operation_resultCode(ctx, field, obj) if out.Values[i] == graphql.Null { diff --git a/internal/serve/graphql/resolvers/test_utils.go b/internal/serve/graphql/resolvers/test_utils.go index 1e71dfb0..672d0da0 100644 --- a/internal/serve/graphql/resolvers/test_utils.go +++ b/internal/serve/graphql/resolvers/test_utils.go @@ -88,7 +88,7 @@ func setupDB(ctx context.Context, t *testing.T, dbConnectionPool db.ConnectionPo ops = append(ops, &types.Operation{ ID: toid.New(testLedger, int32(i+1), int32(j+1)).ToInt64(), OperationType: "PAYMENT", - OperationXDR: fmt.Sprintf("opxdr%d", opIdx), + OperationXDR: types.XDRBytea(fmt.Sprintf("opxdr%d", opIdx)), ResultCode: "op_success", Successful: true, LedgerNumber: 1, From aee79f28ea2d94f4ce4bbb4fb4b138666b6b2c56 Mon Sep 17 00:00:00 2001 From: Aditya Vyas Date: Thu, 5 Feb 2026 14:47:48 -0500 Subject: [PATCH 08/23] Implement OperationXdr GraphQL resolver Returns the XDRBytea as a base64-encoded string for API consumers. --- internal/serve/graphql/resolvers/operation.resolvers.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/internal/serve/graphql/resolvers/operation.resolvers.go b/internal/serve/graphql/resolvers/operation.resolvers.go index ca23c400..4097f59c 100644 --- a/internal/serve/graphql/resolvers/operation.resolvers.go +++ b/internal/serve/graphql/resolvers/operation.resolvers.go @@ -15,6 +15,12 @@ import ( "github.com/stellar/wallet-backend/internal/serve/middleware" ) +// OperationXdr is the resolver for the operationXdr field. +// Returns the operation XDR as a base64-encoded string. +func (r *operationResolver) OperationXdr(ctx context.Context, obj *types.Operation) (string, error) { + return obj.OperationXDR.String(), nil +} + // Transaction is the resolver for the transaction field. // This is a field resolver - it resolves the "transaction" field on an Operation object // gqlgen calls this when a GraphQL query requests the transaction field on an Operation From 00eeaa33a6fbd3994b3812e82b21ec050da2b619 Mon Sep 17 00:00:00 2001 From: Aditya Vyas Date: Thu, 5 Feb 2026 14:50:42 -0500 Subject: [PATCH 09/23] Update tests to use XDRBytea type - Use parameterized queries with XDRBytea for SQL inserts - Update assertions to use .String() for comparison - Fix type casting in test data creation --- internal/data/operations_test.go | 51 +++++++++++++---------- internal/indexer/processors/utils_test.go | 2 +- internal/services/ingest_test.go | 2 +- 3 files changed, 32 insertions(+), 23 deletions(-) diff --git a/internal/data/operations_test.go b/internal/data/operations_test.go index 36420617..9be08575 100644 --- a/internal/data/operations_test.go +++ b/internal/data/operations_test.go @@ -33,7 +33,7 @@ func generateTestOperations(n int, startID int64) ([]*types.Operation, map[int64 ops[i] = &types.Operation{ ID: opID, OperationType: types.OperationTypePayment, - OperationXDR: fmt.Sprintf("operation_xdr_%d", i), + OperationXDR: types.XDRBytea(fmt.Sprintf("operation_xdr_%d", i)), LedgerNumber: uint32(i + 1), LedgerCreatedAt: now, } @@ -101,13 +101,13 @@ func Test_OperationModel_BatchInsert(t *testing.T) { op1 := types.Operation{ ID: 4097, // in range (4096, 8192) OperationType: types.OperationTypePayment, - OperationXDR: "operation1", + OperationXDR: types.XDRBytea("operation1"), LedgerCreatedAt: now, } op2 := types.Operation{ ID: 8193, // in range (8192, 12288) OperationType: types.OperationTypeCreateAccount, - OperationXDR: "operation2", + OperationXDR: types.XDRBytea("operation2"), LedgerCreatedAt: now, } @@ -288,13 +288,13 @@ func Test_OperationModel_BatchCopy(t *testing.T) { op1 := types.Operation{ ID: 4097, // in range (4096, 8192) OperationType: types.OperationTypePayment, - OperationXDR: "operation1", + OperationXDR: types.XDRBytea("operation1"), LedgerCreatedAt: now, } op2 := types.Operation{ ID: 8193, // in range (8192, 12288) OperationType: types.OperationTypeCreateAccount, - OperationXDR: "operation2", + OperationXDR: types.XDRBytea("operation2"), LedgerCreatedAt: now, } @@ -432,7 +432,7 @@ func Test_OperationModel_BatchCopy_DuplicateFails(t *testing.T) { op1 := types.Operation{ ID: 999, OperationType: types.OperationTypePayment, - OperationXDR: "operation_xdr_dup_test", + OperationXDR: types.XDRBytea("operation_xdr_dup_test"), LedgerNumber: 1, LedgerCreatedAt: now, } @@ -762,21 +762,24 @@ func TestOperationModel_BatchGetByToID(t *testing.T) { // Create test operations - IDs must be in TOID range for each transaction // For tx1 (to_id=4096): ops 4097, 4098 // For tx2 (to_id=8192): op 8193 + xdr1 := types.XDRBytea("xdr1") + xdr2 := types.XDRBytea("xdr2") + xdr3 := types.XDRBytea("xdr3") _, err = dbConnectionPool.ExecContext(ctx, ` INSERT INTO operations (id, operation_type, operation_xdr, result_code, successful, ledger_number, ledger_created_at) VALUES - (4097, 'PAYMENT', 'xdr1', 'op_success', true, 1, $1), - (8193, 'CREATE_ACCOUNT', 'xdr2', 'op_success', true, 2, $1), - (4098, 'PAYMENT', 'xdr3', 'op_success', true, 3, $1) - `, now) + (4097, 'PAYMENT', $2, 'op_success', true, 1, $1), + (8193, 'CREATE_ACCOUNT', $3, 'op_success', true, 2, $1), + (4098, 'PAYMENT', $4, 'op_success', true, 3, $1) + `, now, xdr1, xdr2, xdr3) require.NoError(t, err) // Test BatchGetByToID operations, err := m.BatchGetByToID(ctx, 4096, "", nil, nil, ASC) require.NoError(t, err) assert.Len(t, operations, 2) - assert.Equal(t, "xdr1", operations[0].OperationXDR) - assert.Equal(t, "xdr3", operations[1].OperationXDR) + assert.Equal(t, "xdr1", operations[0].OperationXDR.String()) + assert.Equal(t, "xdr3", operations[1].OperationXDR.String()) } func TestOperationModel_BatchGetByAccountAddresses(t *testing.T) { @@ -819,13 +822,16 @@ func TestOperationModel_BatchGetByAccountAddresses(t *testing.T) { require.NoError(t, err) // Create test operations (IDs must be in TOID range for each transaction) + xdr1 := types.XDRBytea("xdr1") + xdr2 := types.XDRBytea("xdr2") + xdr3 := types.XDRBytea("xdr3") _, err = dbConnectionPool.ExecContext(ctx, ` INSERT INTO operations (id, operation_type, operation_xdr, result_code, successful, ledger_number, ledger_created_at) VALUES - (4097, 'PAYMENT', 'xdr1', 'op_success', true, 1, $1), - (8193, 'CREATE_ACCOUNT', 'xdr2', 'op_success', true, 2, $1), - (12289, 'PAYMENT', 'xdr3', 'op_success', true, 3, $1) - `, now) + (4097, 'PAYMENT', $2, 'op_success', true, 1, $1), + (8193, 'CREATE_ACCOUNT', $3, 'op_success', true, 2, $1), + (12289, 'PAYMENT', $4, 'op_success', true, 3, $1) + `, now, xdr1, xdr2, xdr3) require.NoError(t, err) // Create test operations_accounts links @@ -889,7 +895,7 @@ func TestOperationModel_GetByID(t *testing.T) { operation, err := m.GetByID(ctx, 4097, "") require.NoError(t, err) assert.Equal(t, int64(4097), operation.ID) - assert.Equal(t, "xdr1", operation.OperationXDR) + assert.Equal(t, "xdr1", operation.OperationXDR.String()) assert.Equal(t, uint32(1), operation.LedgerNumber) assert.WithinDuration(t, now, operation.LedgerCreatedAt, time.Second) } @@ -934,13 +940,16 @@ func TestOperationModel_BatchGetByStateChangeIDs(t *testing.T) { require.NoError(t, err) // Create test operations (IDs must be in TOID range for each transaction) + xdr1 := types.XDRBytea("xdr1") + xdr2 := types.XDRBytea("xdr2") + xdr3 := types.XDRBytea("xdr3") _, err = dbConnectionPool.ExecContext(ctx, ` INSERT INTO operations (id, operation_type, operation_xdr, result_code, successful, ledger_number, ledger_created_at) VALUES - (4097, 'PAYMENT', 'xdr1', 'op_success', true, 1, $1), - (8193, 'CREATE_ACCOUNT', 'xdr2', 'op_success', true, 2, $1), - (12289, 'PAYMENT', 'xdr3', 'op_success', true, 3, $1) - `, now) + (4097, 'PAYMENT', $2, 'op_success', true, 1, $1), + (8193, 'CREATE_ACCOUNT', $3, 'op_success', true, 2, $1), + (12289, 'PAYMENT', $4, 'op_success', true, 3, $1) + `, now, xdr1, xdr2, xdr3) require.NoError(t, err) // Create test state changes diff --git a/internal/indexer/processors/utils_test.go b/internal/indexer/processors/utils_test.go index 0f68c129..14005820 100644 --- a/internal/indexer/processors/utils_test.go +++ b/internal/indexer/processors/utils_test.go @@ -104,7 +104,7 @@ func Test_ConvertOperation(t *testing.T) { wantDataOp := &types.Operation{ ID: opID, OperationType: types.OperationTypeFromXDR(op.Body.Type), - OperationXDR: opXDRStr, + OperationXDR: types.XDRBytea(opXDRStr), ResultCode: OpSuccess, Successful: true, LedgerCreatedAt: time.Date(2025, time.June, 19, 0, 3, 16, 0, time.UTC), diff --git a/internal/services/ingest_test.go b/internal/services/ingest_test.go index aac607e1..dd3e04f6 100644 --- a/internal/services/ingest_test.go +++ b/internal/services/ingest_test.go @@ -543,7 +543,7 @@ func createTestOperation(id int64) types.Operation { return types.Operation{ ID: id, OperationType: types.OperationTypePayment, - OperationXDR: "test_operation_xdr", + OperationXDR: types.XDRBytea("test_operation_xdr"), LedgerNumber: 1000, LedgerCreatedAt: now, IngestedAt: now, From ec34cefd2f30af85191f85075c3c28bd4b06d018 Mon Sep 17 00:00:00 2001 From: Aditya Vyas Date: Thu, 5 Feb 2026 14:57:46 -0500 Subject: [PATCH 10/23] Update GraphQL resolver tests for XDRBytea type - Use base64-encoded test XDR data in test_utils.go - Add testOpXDR helper functions in test files - Update all assertions to use .String() method --- .../resolvers/account_resolvers_test.go | 47 ++++++++++-------- .../resolvers/queries_resolvers_test.go | 49 +++++++++++-------- .../serve/graphql/resolvers/test_utils.go | 5 +- .../resolvers/transaction_resolvers_test.go | 19 ++++--- 4 files changed, 72 insertions(+), 48 deletions(-) diff --git a/internal/serve/graphql/resolvers/account_resolvers_test.go b/internal/serve/graphql/resolvers/account_resolvers_test.go index 470287f2..609786c5 100644 --- a/internal/serve/graphql/resolvers/account_resolvers_test.go +++ b/internal/serve/graphql/resolvers/account_resolvers_test.go @@ -1,6 +1,8 @@ package resolvers import ( + "encoding/base64" + "fmt" "testing" "github.com/stretchr/testify/assert" @@ -15,6 +17,11 @@ import ( graphql1 "github.com/stellar/wallet-backend/internal/serve/graphql/generated" ) +// testOpXDRAcc returns the expected base64-encoded XDR for test operation N +func testOpXDRAcc(n int) string { + return base64.StdEncoding.EncodeToString([]byte(fmt.Sprintf("opxdr%d", n))) +} + func TestAccountResolver_Transactions(t *testing.T) { parentAccount := &types.Account{StellarAddress: types.AddressBytea(sharedTestAccountAddress)} @@ -166,10 +173,10 @@ func TestAccountResolver_Operations(t *testing.T) { require.NoError(t, err) require.Len(t, operations.Edges, 8) - assert.Equal(t, "opxdr1", operations.Edges[0].Node.OperationXDR) - assert.Equal(t, "opxdr2", operations.Edges[1].Node.OperationXDR) - assert.Equal(t, "opxdr3", operations.Edges[2].Node.OperationXDR) - assert.Equal(t, "opxdr4", operations.Edges[3].Node.OperationXDR) + assert.Equal(t, testOpXDRAcc(1), operations.Edges[0].Node.OperationXDR.String()) + assert.Equal(t, testOpXDRAcc(2), operations.Edges[1].Node.OperationXDR.String()) + assert.Equal(t, testOpXDRAcc(3), operations.Edges[2].Node.OperationXDR.String()) + assert.Equal(t, testOpXDRAcc(4), operations.Edges[3].Node.OperationXDR.String()) }) t.Run("get operations with first/after limit and cursor", func(t *testing.T) { @@ -178,8 +185,8 @@ func TestAccountResolver_Operations(t *testing.T) { ops, err := resolver.Operations(ctx, parentAccount, &first, nil, nil, nil) require.NoError(t, err) assert.Len(t, ops.Edges, 2) - assert.Equal(t, "opxdr1", ops.Edges[0].Node.OperationXDR) - assert.Equal(t, "opxdr2", ops.Edges[1].Node.OperationXDR) + assert.Equal(t, testOpXDRAcc(1), ops.Edges[0].Node.OperationXDR.String()) + assert.Equal(t, testOpXDRAcc(2), ops.Edges[1].Node.OperationXDR.String()) assert.True(t, ops.PageInfo.HasNextPage) assert.False(t, ops.PageInfo.HasPreviousPage) @@ -189,8 +196,8 @@ func TestAccountResolver_Operations(t *testing.T) { ops, err = resolver.Operations(ctx, parentAccount, &first, nextCursor, nil, nil) require.NoError(t, err) assert.Len(t, ops.Edges, 2) - assert.Equal(t, "opxdr3", ops.Edges[0].Node.OperationXDR) - assert.Equal(t, "opxdr4", ops.Edges[1].Node.OperationXDR) + assert.Equal(t, testOpXDRAcc(3), ops.Edges[0].Node.OperationXDR.String()) + assert.Equal(t, testOpXDRAcc(4), ops.Edges[1].Node.OperationXDR.String()) assert.True(t, ops.PageInfo.HasNextPage) assert.True(t, ops.PageInfo.HasPreviousPage) @@ -200,10 +207,10 @@ func TestAccountResolver_Operations(t *testing.T) { ops, err = resolver.Operations(ctx, parentAccount, &first, nextCursor, nil, nil) require.NoError(t, err) assert.Len(t, ops.Edges, 4) - assert.Equal(t, "opxdr5", ops.Edges[0].Node.OperationXDR) - assert.Equal(t, "opxdr6", ops.Edges[1].Node.OperationXDR) - assert.Equal(t, "opxdr7", ops.Edges[2].Node.OperationXDR) - assert.Equal(t, "opxdr8", ops.Edges[3].Node.OperationXDR) + assert.Equal(t, testOpXDRAcc(5), ops.Edges[0].Node.OperationXDR.String()) + assert.Equal(t, testOpXDRAcc(6), ops.Edges[1].Node.OperationXDR.String()) + assert.Equal(t, testOpXDRAcc(7), ops.Edges[2].Node.OperationXDR.String()) + assert.Equal(t, testOpXDRAcc(8), ops.Edges[3].Node.OperationXDR.String()) assert.False(t, ops.PageInfo.HasNextPage) assert.True(t, ops.PageInfo.HasPreviousPage) }) @@ -214,8 +221,8 @@ func TestAccountResolver_Operations(t *testing.T) { ops, err := resolver.Operations(ctx, parentAccount, nil, nil, &last, nil) require.NoError(t, err) assert.Len(t, ops.Edges, 2) - assert.Equal(t, "opxdr7", ops.Edges[0].Node.OperationXDR) - assert.Equal(t, "opxdr8", ops.Edges[1].Node.OperationXDR) + assert.Equal(t, testOpXDRAcc(7), ops.Edges[0].Node.OperationXDR.String()) + assert.Equal(t, testOpXDRAcc(8), ops.Edges[1].Node.OperationXDR.String()) assert.True(t, ops.PageInfo.HasPreviousPage) assert.False(t, ops.PageInfo.HasNextPage) @@ -225,8 +232,8 @@ func TestAccountResolver_Operations(t *testing.T) { ops, err = resolver.Operations(ctx, parentAccount, nil, nil, &last, nextCursor) require.NoError(t, err) assert.Len(t, ops.Edges, 2) - assert.Equal(t, "opxdr5", ops.Edges[0].Node.OperationXDR) - assert.Equal(t, "opxdr6", ops.Edges[1].Node.OperationXDR) + assert.Equal(t, testOpXDRAcc(5), ops.Edges[0].Node.OperationXDR.String()) + assert.Equal(t, testOpXDRAcc(6), ops.Edges[1].Node.OperationXDR.String()) assert.True(t, ops.PageInfo.HasNextPage) assert.True(t, ops.PageInfo.HasPreviousPage) @@ -236,10 +243,10 @@ func TestAccountResolver_Operations(t *testing.T) { ops, err = resolver.Operations(ctx, parentAccount, nil, nil, &last, nextCursor) require.NoError(t, err) assert.Len(t, ops.Edges, 4) - assert.Equal(t, "opxdr1", ops.Edges[0].Node.OperationXDR) - assert.Equal(t, "opxdr2", ops.Edges[1].Node.OperationXDR) - assert.Equal(t, "opxdr3", ops.Edges[2].Node.OperationXDR) - assert.Equal(t, "opxdr4", ops.Edges[3].Node.OperationXDR) + assert.Equal(t, testOpXDRAcc(1), ops.Edges[0].Node.OperationXDR.String()) + assert.Equal(t, testOpXDRAcc(2), ops.Edges[1].Node.OperationXDR.String()) + assert.Equal(t, testOpXDRAcc(3), ops.Edges[2].Node.OperationXDR.String()) + assert.Equal(t, testOpXDRAcc(4), ops.Edges[3].Node.OperationXDR.String()) assert.True(t, ops.PageInfo.HasNextPage) assert.False(t, ops.PageInfo.HasPreviousPage) }) diff --git a/internal/serve/graphql/resolvers/queries_resolvers_test.go b/internal/serve/graphql/resolvers/queries_resolvers_test.go index bb139b9d..2be5511c 100644 --- a/internal/serve/graphql/resolvers/queries_resolvers_test.go +++ b/internal/serve/graphql/resolvers/queries_resolvers_test.go @@ -1,6 +1,8 @@ package resolvers import ( + "encoding/base64" + "fmt" "testing" "github.com/stellar/go-stellar-sdk/toid" @@ -12,6 +14,11 @@ import ( "github.com/stellar/wallet-backend/internal/metrics" ) +// testOpXDR returns the expected base64-encoded XDR for test operation N +func testOpXDR(n int) string { + return base64.StdEncoding.EncodeToString([]byte(fmt.Sprintf("opxdr%d", n))) +} + func TestQueryResolver_TransactionByHash(t *testing.T) { mockMetricsService := &metrics.MockMetricsService{} mockMetricsService.On("ObserveDBQueryDuration", "GetByHash", "transactions", mock.Anything).Return() @@ -278,10 +285,10 @@ func TestQueryResolver_Operations(t *testing.T) { require.NoError(t, err) require.Len(t, operations.Edges, 8) // Operations are ordered by ID ascending - assert.Equal(t, "opxdr1", operations.Edges[0].Node.OperationXDR) - assert.Equal(t, "opxdr2", operations.Edges[1].Node.OperationXDR) - assert.Equal(t, "opxdr3", operations.Edges[2].Node.OperationXDR) - assert.Equal(t, "opxdr4", operations.Edges[3].Node.OperationXDR) + assert.Equal(t, testOpXDR(1), operations.Edges[0].Node.OperationXDR.String()) + assert.Equal(t, testOpXDR(2), operations.Edges[1].Node.OperationXDR.String()) + assert.Equal(t, testOpXDR(3), operations.Edges[2].Node.OperationXDR.String()) + assert.Equal(t, testOpXDR(4), operations.Edges[3].Node.OperationXDR.String()) }) t.Run("get operations with first/after limit and cursor", func(t *testing.T) { @@ -290,8 +297,8 @@ func TestQueryResolver_Operations(t *testing.T) { ops, err := resolver.Operations(ctx, &first, nil, nil, nil) require.NoError(t, err) assert.Len(t, ops.Edges, 2) - assert.Equal(t, "opxdr1", ops.Edges[0].Node.OperationXDR) - assert.Equal(t, "opxdr2", ops.Edges[1].Node.OperationXDR) + assert.Equal(t, testOpXDR(1), ops.Edges[0].Node.OperationXDR.String()) + assert.Equal(t, testOpXDR(2), ops.Edges[1].Node.OperationXDR.String()) assert.True(t, ops.PageInfo.HasNextPage) assert.False(t, ops.PageInfo.HasPreviousPage) @@ -302,7 +309,7 @@ func TestQueryResolver_Operations(t *testing.T) { ops, err = resolver.Operations(ctx, &first, nextCursor, nil, nil) require.NoError(t, err) assert.Len(t, ops.Edges, 1) - assert.Equal(t, "opxdr3", ops.Edges[0].Node.OperationXDR) + assert.Equal(t, testOpXDR(3), ops.Edges[0].Node.OperationXDR.String()) assert.True(t, ops.PageInfo.HasNextPage) assert.True(t, ops.PageInfo.HasPreviousPage) @@ -313,11 +320,11 @@ func TestQueryResolver_Operations(t *testing.T) { ops, err = resolver.Operations(ctx, &first, nextCursor, nil, nil) require.NoError(t, err) assert.Len(t, ops.Edges, 5) - assert.Equal(t, "opxdr4", ops.Edges[0].Node.OperationXDR) - assert.Equal(t, "opxdr5", ops.Edges[1].Node.OperationXDR) - assert.Equal(t, "opxdr6", ops.Edges[2].Node.OperationXDR) - assert.Equal(t, "opxdr7", ops.Edges[3].Node.OperationXDR) - assert.Equal(t, "opxdr8", ops.Edges[4].Node.OperationXDR) + assert.Equal(t, testOpXDR(4), ops.Edges[0].Node.OperationXDR.String()) + assert.Equal(t, testOpXDR(5), ops.Edges[1].Node.OperationXDR.String()) + assert.Equal(t, testOpXDR(6), ops.Edges[2].Node.OperationXDR.String()) + assert.Equal(t, testOpXDR(7), ops.Edges[3].Node.OperationXDR.String()) + assert.Equal(t, testOpXDR(8), ops.Edges[4].Node.OperationXDR.String()) assert.False(t, ops.PageInfo.HasNextPage) assert.True(t, ops.PageInfo.HasPreviousPage) }) @@ -329,8 +336,8 @@ func TestQueryResolver_Operations(t *testing.T) { require.NoError(t, err) assert.Len(t, ops.Edges, 2) // With backward pagination, we get the last 2 items - assert.Equal(t, "opxdr7", ops.Edges[0].Node.OperationXDR) - assert.Equal(t, "opxdr8", ops.Edges[1].Node.OperationXDR) + assert.Equal(t, testOpXDR(7), ops.Edges[0].Node.OperationXDR.String()) + assert.Equal(t, testOpXDR(8), ops.Edges[1].Node.OperationXDR.String()) assert.False(t, ops.PageInfo.HasNextPage) assert.True(t, ops.PageInfo.HasPreviousPage) @@ -341,7 +348,7 @@ func TestQueryResolver_Operations(t *testing.T) { ops, err = resolver.Operations(ctx, nil, nil, &last, prevCursor) require.NoError(t, err) assert.Len(t, ops.Edges, 1) - assert.Equal(t, "opxdr6", ops.Edges[0].Node.OperationXDR) + assert.Equal(t, testOpXDR(6), ops.Edges[0].Node.OperationXDR.String()) assert.True(t, ops.PageInfo.HasNextPage) assert.True(t, ops.PageInfo.HasPreviousPage) @@ -352,11 +359,11 @@ func TestQueryResolver_Operations(t *testing.T) { require.NoError(t, err) // There are 5 operations before (2,1): (2,2), (3,1), (3,2), (4,1), (4,2) assert.Len(t, ops.Edges, 5) - assert.Equal(t, "opxdr1", ops.Edges[0].Node.OperationXDR) - assert.Equal(t, "opxdr2", ops.Edges[1].Node.OperationXDR) - assert.Equal(t, "opxdr3", ops.Edges[2].Node.OperationXDR) - assert.Equal(t, "opxdr4", ops.Edges[3].Node.OperationXDR) - assert.Equal(t, "opxdr5", ops.Edges[4].Node.OperationXDR) + assert.Equal(t, testOpXDR(1), ops.Edges[0].Node.OperationXDR.String()) + assert.Equal(t, testOpXDR(2), ops.Edges[1].Node.OperationXDR.String()) + assert.Equal(t, testOpXDR(3), ops.Edges[2].Node.OperationXDR.String()) + assert.Equal(t, testOpXDR(4), ops.Edges[3].Node.OperationXDR.String()) + assert.Equal(t, testOpXDR(5), ops.Edges[4].Node.OperationXDR.String()) assert.True(t, ops.PageInfo.HasNextPage) assert.False(t, ops.PageInfo.HasPreviousPage) }) @@ -442,7 +449,7 @@ func TestQueryResolver_OperationByID(t *testing.T) { require.NoError(t, err) assert.Equal(t, toid.New(1000, 1, 1).ToInt64(), op.ID) - assert.Equal(t, "opxdr1", op.OperationXDR) + assert.Equal(t, testOpXDR(1), op.OperationXDR.String()) assert.Equal(t, uint32(1), op.LedgerNumber) }) diff --git a/internal/serve/graphql/resolvers/test_utils.go b/internal/serve/graphql/resolvers/test_utils.go index 672d0da0..dd096911 100644 --- a/internal/serve/graphql/resolvers/test_utils.go +++ b/internal/serve/graphql/resolvers/test_utils.go @@ -2,6 +2,7 @@ package resolvers import ( "context" + "encoding/base64" "fmt" "testing" "time" @@ -85,10 +86,12 @@ func setupDB(ctx context.Context, t *testing.T, dbConnectionPool db.ConnectionPo // Add 2 operations for each transaction for j := range 2 { + // Create base64-encoded test XDR data (encoding "opxdr1", "opxdr2", etc.) + testXDR := base64.StdEncoding.EncodeToString([]byte(fmt.Sprintf("opxdr%d", opIdx))) ops = append(ops, &types.Operation{ ID: toid.New(testLedger, int32(i+1), int32(j+1)).ToInt64(), OperationType: "PAYMENT", - OperationXDR: types.XDRBytea(fmt.Sprintf("opxdr%d", opIdx)), + OperationXDR: types.XDRBytea(testXDR), ResultCode: "op_success", Successful: true, LedgerNumber: 1, diff --git a/internal/serve/graphql/resolvers/transaction_resolvers_test.go b/internal/serve/graphql/resolvers/transaction_resolvers_test.go index a27b2851..275cbefc 100644 --- a/internal/serve/graphql/resolvers/transaction_resolvers_test.go +++ b/internal/serve/graphql/resolvers/transaction_resolvers_test.go @@ -2,6 +2,8 @@ package resolvers import ( "context" + "encoding/base64" + "fmt" "testing" "github.com/stretchr/testify/assert" @@ -17,6 +19,11 @@ import ( "github.com/stellar/wallet-backend/internal/serve/middleware" ) +// testOpXDR returns the expected base64-encoded XDR for test operation N +func testOpXDRTx(n int) string { + return base64.StdEncoding.EncodeToString([]byte(fmt.Sprintf("opxdr%d", n))) +} + func TestTransactionResolver_Operations(t *testing.T) { mockMetricsService := &metrics.MockMetricsService{} mockMetricsService.On("IncDBQuery", "BatchGetByToID", "operations").Return() @@ -42,8 +49,8 @@ func TestTransactionResolver_Operations(t *testing.T) { require.NoError(t, err) require.Len(t, operations.Edges, 2) - assert.Equal(t, "opxdr1", operations.Edges[0].Node.OperationXDR) - assert.Equal(t, "opxdr2", operations.Edges[1].Node.OperationXDR) + assert.Equal(t, testOpXDRTx(1), operations.Edges[0].Node.OperationXDR.String()) + assert.Equal(t, testOpXDRTx(2), operations.Edges[1].Node.OperationXDR.String()) }) t.Run("nil transaction panics", func(t *testing.T) { @@ -73,7 +80,7 @@ func TestTransactionResolver_Operations(t *testing.T) { ops, err := resolver.Operations(ctx, parentTx, &first, nil, nil, nil) require.NoError(t, err) assert.Len(t, ops.Edges, 1) - assert.Equal(t, "opxdr1", ops.Edges[0].Node.OperationXDR) + assert.Equal(t, testOpXDRTx(1), ops.Edges[0].Node.OperationXDR.String()) assert.True(t, ops.PageInfo.HasNextPage) assert.False(t, ops.PageInfo.HasPreviousPage) @@ -83,7 +90,7 @@ func TestTransactionResolver_Operations(t *testing.T) { ops, err = resolver.Operations(ctx, parentTx, &first, nextCursor, nil, nil) require.NoError(t, err) assert.Len(t, ops.Edges, 1) - assert.Equal(t, "opxdr2", ops.Edges[0].Node.OperationXDR) + assert.Equal(t, testOpXDRTx(2), ops.Edges[0].Node.OperationXDR.String()) assert.False(t, ops.PageInfo.HasNextPage) assert.True(t, ops.PageInfo.HasPreviousPage) }) @@ -95,7 +102,7 @@ func TestTransactionResolver_Operations(t *testing.T) { ops, err := resolver.Operations(ctx, parentTx, nil, nil, &last, nil) require.NoError(t, err) assert.Len(t, ops.Edges, 1) - assert.Equal(t, "opxdr2", ops.Edges[0].Node.OperationXDR) + assert.Equal(t, testOpXDRTx(2), ops.Edges[0].Node.OperationXDR.String()) assert.False(t, ops.PageInfo.HasNextPage) assert.True(t, ops.PageInfo.HasPreviousPage) @@ -105,7 +112,7 @@ func TestTransactionResolver_Operations(t *testing.T) { ops, err = resolver.Operations(ctx, parentTx, nil, nil, &last, prevCursor) require.NoError(t, err) assert.Len(t, ops.Edges, 1) - assert.Equal(t, "opxdr1", ops.Edges[0].Node.OperationXDR) + assert.Equal(t, testOpXDRTx(1), ops.Edges[0].Node.OperationXDR.String()) assert.True(t, ops.PageInfo.HasNextPage) assert.False(t, ops.PageInfo.HasPreviousPage) }) From b58b99430aef84ffc9de966dd88dfc29999a02a4 Mon Sep 17 00:00:00 2001 From: Aditya Vyas Date: Thu, 5 Feb 2026 15:04:22 -0500 Subject: [PATCH 11/23] Fix test data to use valid base64-encoded XDR strings - Update createTestOperation to use proper base64 XDR - Update generateTestOperations to encode test data as base64 - Update operations_test.go with base64 encoding for all test XDR data - Fix assertions to compare against the stored XDRBytea values --- internal/data/operations_test.go | 19 ++++++++++--------- internal/services/ingest_test.go | 2 +- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/internal/data/operations_test.go b/internal/data/operations_test.go index 9be08575..b6a0481f 100644 --- a/internal/data/operations_test.go +++ b/internal/data/operations_test.go @@ -2,6 +2,7 @@ package data import ( "context" + "encoding/base64" "fmt" "testing" "time" @@ -33,7 +34,7 @@ func generateTestOperations(n int, startID int64) ([]*types.Operation, map[int64 ops[i] = &types.Operation{ ID: opID, OperationType: types.OperationTypePayment, - OperationXDR: types.XDRBytea(fmt.Sprintf("operation_xdr_%d", i)), + OperationXDR: types.XDRBytea(base64.StdEncoding.EncodeToString([]byte(fmt.Sprintf("operation_xdr_%d", i)))), LedgerNumber: uint32(i + 1), LedgerCreatedAt: now, } @@ -101,13 +102,13 @@ func Test_OperationModel_BatchInsert(t *testing.T) { op1 := types.Operation{ ID: 4097, // in range (4096, 8192) OperationType: types.OperationTypePayment, - OperationXDR: types.XDRBytea("operation1"), + OperationXDR: types.XDRBytea(base64.StdEncoding.EncodeToString([]byte("operation1"))), LedgerCreatedAt: now, } op2 := types.Operation{ ID: 8193, // in range (8192, 12288) OperationType: types.OperationTypeCreateAccount, - OperationXDR: types.XDRBytea("operation2"), + OperationXDR: types.XDRBytea(base64.StdEncoding.EncodeToString([]byte("operation2"))), LedgerCreatedAt: now, } @@ -288,13 +289,13 @@ func Test_OperationModel_BatchCopy(t *testing.T) { op1 := types.Operation{ ID: 4097, // in range (4096, 8192) OperationType: types.OperationTypePayment, - OperationXDR: types.XDRBytea("operation1"), + OperationXDR: types.XDRBytea(base64.StdEncoding.EncodeToString([]byte("operation1"))), LedgerCreatedAt: now, } op2 := types.Operation{ ID: 8193, // in range (8192, 12288) OperationType: types.OperationTypeCreateAccount, - OperationXDR: types.XDRBytea("operation2"), + OperationXDR: types.XDRBytea(base64.StdEncoding.EncodeToString([]byte("operation2"))), LedgerCreatedAt: now, } @@ -432,7 +433,7 @@ func Test_OperationModel_BatchCopy_DuplicateFails(t *testing.T) { op1 := types.Operation{ ID: 999, OperationType: types.OperationTypePayment, - OperationXDR: types.XDRBytea("operation_xdr_dup_test"), + OperationXDR: types.XDRBytea(base64.StdEncoding.EncodeToString([]byte("operation_xdr_dup_test"))), LedgerNumber: 1, LedgerCreatedAt: now, } @@ -778,8 +779,8 @@ func TestOperationModel_BatchGetByToID(t *testing.T) { operations, err := m.BatchGetByToID(ctx, 4096, "", nil, nil, ASC) require.NoError(t, err) assert.Len(t, operations, 2) - assert.Equal(t, "xdr1", operations[0].OperationXDR.String()) - assert.Equal(t, "xdr3", operations[1].OperationXDR.String()) + assert.Equal(t, xdr1.String(), operations[0].OperationXDR.String()) + assert.Equal(t, xdr3.String(), operations[1].OperationXDR.String()) } func TestOperationModel_BatchGetByAccountAddresses(t *testing.T) { @@ -895,7 +896,7 @@ func TestOperationModel_GetByID(t *testing.T) { operation, err := m.GetByID(ctx, 4097, "") require.NoError(t, err) assert.Equal(t, int64(4097), operation.ID) - assert.Equal(t, "xdr1", operation.OperationXDR.String()) + assert.Equal(t, xdr1.String(), operation.OperationXDR.String()) assert.Equal(t, uint32(1), operation.LedgerNumber) assert.WithinDuration(t, now, operation.LedgerCreatedAt, time.Second) } diff --git a/internal/services/ingest_test.go b/internal/services/ingest_test.go index dd3e04f6..7fe297ea 100644 --- a/internal/services/ingest_test.go +++ b/internal/services/ingest_test.go @@ -543,7 +543,7 @@ func createTestOperation(id int64) types.Operation { return types.Operation{ ID: id, OperationType: types.OperationTypePayment, - OperationXDR: types.XDRBytea("test_operation_xdr"), + OperationXDR: types.XDRBytea("dGVzdF9vcGVyYXRpb25feGRy"), // base64 of "test_operation_xdr" LedgerNumber: 1000, LedgerCreatedAt: now, IngestedAt: now, From 525a59da4f87dc414533366f61100f400b96f3e3 Mon Sep 17 00:00:00 2001 From: Aditya Vyas Date: Thu, 5 Feb 2026 15:19:01 -0500 Subject: [PATCH 12/23] Change XDRBytea underlying type from string to []byte This simplifies the XDR storage flow by storing raw bytes directly instead of encoding to base64 and then decoding. The String() method now handles base64 encoding for external representation. --- internal/indexer/types/types.go | 22 +++++++++------------- 1 file changed, 9 insertions(+), 13 deletions(-) diff --git a/internal/indexer/types/types.go b/internal/indexer/types/types.go index 6e873f90..24fa71bd 100644 --- a/internal/indexer/types/types.go +++ b/internal/indexer/types/types.go @@ -169,38 +169,34 @@ func (h HashBytea) String() string { // XDRBytea represents XDR data stored as BYTEA in the database. // Storage format: raw XDR bytes (variable length) -// Go representation: base64 string -type XDRBytea string +// Go representation: raw bytes internally, base64 string via String() +type XDRBytea []byte -// Scan implements sql.Scanner - converts BYTEA (raw bytes) to base64 string +// Scan implements sql.Scanner - reads raw bytes from BYTEA column func (x *XDRBytea) Scan(value any) error { if value == nil { - *x = "" + *x = nil return nil } bytes, ok := value.([]byte) if !ok { return fmt.Errorf("expected []byte, got %T", value) } - *x = XDRBytea(base64.StdEncoding.EncodeToString(bytes)) + *x = bytes return nil } -// Value implements driver.Valuer - converts base64 string to raw bytes +// Value implements driver.Valuer - returns raw bytes for BYTEA storage func (x XDRBytea) Value() (driver.Value, error) { - if x == "" { + if len(x) == 0 { return nil, nil } - bytes, err := base64.StdEncoding.DecodeString(string(x)) - if err != nil { - return nil, fmt.Errorf("decoding base64 XDR %s: %w", x, err) - } - return bytes, nil + return []byte(x), nil } // String returns the XDR as a base64 string. func (x XDRBytea) String() string { - return string(x) + return base64.StdEncoding.EncodeToString(x) } type ContractType string From 5e221f72a62eb9f52d065f79b6e724ce9a80b4a3 Mon Sep 17 00:00:00 2001 From: Aditya Vyas Date: Thu, 5 Feb 2026 15:19:25 -0500 Subject: [PATCH 13/23] Use MarshalBinary directly in ConvertOperation Skip the intermediate base64 encoding step by using MarshalBinary() instead of MarshalBase64(). The raw bytes are now stored directly in XDRBytea. --- internal/indexer/processors/utils.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/indexer/processors/utils.go b/internal/indexer/processors/utils.go index dc00ba48..c4cfb614 100644 --- a/internal/indexer/processors/utils.go +++ b/internal/indexer/processors/utils.go @@ -328,7 +328,7 @@ func ConvertOperation( opIndex uint32, opResults []xdr.OperationResult, ) (*types.Operation, error) { - xdrOpStr, err := xdr.MarshalBase64(op) + xdrBytes, err := op.MarshalBinary() if err != nil { return nil, fmt.Errorf("marshalling operation %d: %w", opID, err) } @@ -350,7 +350,7 @@ func ConvertOperation( return &types.Operation{ ID: opID, OperationType: types.OperationTypeFromXDR(op.Body.Type), - OperationXDR: types.XDRBytea(xdrOpStr), + OperationXDR: types.XDRBytea(xdrBytes), ResultCode: resultCode, Successful: successful, LedgerCreatedAt: transaction.Ledger.ClosedAt(), From ce12f8a98cb7ae5ba6923165f1e1d300b0da4330 Mon Sep 17 00:00:00 2001 From: Aditya Vyas Date: Thu, 5 Feb 2026 15:19:55 -0500 Subject: [PATCH 14/23] Simplify BatchInsert and BatchCopy in operations.go Remove unnecessary Value() calls since XDRBytea is now []byte. Access raw bytes directly via type conversion. --- internal/data/operations.go | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-) diff --git a/internal/data/operations.go b/internal/data/operations.go index b5557032..2d862bd3 100644 --- a/internal/data/operations.go +++ b/internal/data/operations.go @@ -293,13 +293,7 @@ func (m *OperationModel) BatchInsert( for i, op := range operations { ids[i] = op.ID operationTypes[i] = string(op.OperationType) - xdrBytes, err := op.OperationXDR.Value() - if err != nil { - return nil, fmt.Errorf("converting operation XDR to bytes for op %d: %w", op.ID, err) - } - if xdrBytes != nil { - operationXDRs[i] = xdrBytes.([]byte) - } + operationXDRs[i] = []byte(op.OperationXDR) resultCodes[i] = op.ResultCode successfulFlags[i] = op.Successful ledgerNumbers[i] = op.LedgerNumber @@ -421,14 +415,10 @@ func (m *OperationModel) BatchCopy( []string{"id", "operation_type", "operation_xdr", "result_code", "successful", "ledger_number", "ledger_created_at"}, pgx.CopyFromSlice(len(operations), func(i int) ([]any, error) { op := operations[i] - xdrBytes, xdrErr := op.OperationXDR.Value() - if xdrErr != nil { - return nil, fmt.Errorf("converting operation XDR to bytes for op %d: %w", op.ID, xdrErr) - } return []any{ pgtype.Int8{Int64: op.ID, Valid: true}, pgtype.Text{String: string(op.OperationType), Valid: true}, - xdrBytes, + []byte(op.OperationXDR), pgtype.Text{String: op.ResultCode, Valid: true}, pgtype.Bool{Bool: op.Successful, Valid: true}, pgtype.Int4{Int32: int32(op.LedgerNumber), Valid: true}, From 52bdb282c1e3532b21c072b09d31eb9631df4caf Mon Sep 17 00:00:00 2001 From: Aditya Vyas Date: Thu, 5 Feb 2026 15:20:30 -0500 Subject: [PATCH 15/23] Update utils_test.go for XDRBytea []byte type Decode expected base64 XDR string to raw bytes for comparison since XDRBytea now uses []byte underlying type. --- internal/indexer/processors/utils_test.go | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/internal/indexer/processors/utils_test.go b/internal/indexer/processors/utils_test.go index 14005820..483e1e97 100644 --- a/internal/indexer/processors/utils_test.go +++ b/internal/indexer/processors/utils_test.go @@ -1,6 +1,7 @@ package processors import ( + "encoding/base64" "testing" "time" @@ -101,10 +102,14 @@ func Test_ConvertOperation(t *testing.T) { gotDataOp, err := ConvertOperation(&ingestTx, &op, opID, opIndex, opResults) require.NoError(t, err) + // Decode expected base64 XDR to raw bytes for comparison + expectedXDRBytes, err := base64.StdEncoding.DecodeString(opXDRStr) + require.NoError(t, err) + wantDataOp := &types.Operation{ ID: opID, OperationType: types.OperationTypeFromXDR(op.Body.Type), - OperationXDR: types.XDRBytea(opXDRStr), + OperationXDR: types.XDRBytea(expectedXDRBytes), ResultCode: OpSuccess, Successful: true, LedgerCreatedAt: time.Date(2025, time.June, 19, 0, 3, 16, 0, time.UTC), From 5bb41f36fbfcba5006f66457dae4549d827d6152 Mon Sep 17 00:00:00 2001 From: Aditya Vyas Date: Thu, 5 Feb 2026 15:21:16 -0500 Subject: [PATCH 16/23] Update operations_test.go for XDRBytea []byte type Use raw bytes directly instead of base64-encoded strings when creating test data for XDRBytea fields. --- internal/data/operations_test.go | 31 +++++++++++++++---------------- 1 file changed, 15 insertions(+), 16 deletions(-) diff --git a/internal/data/operations_test.go b/internal/data/operations_test.go index b6a0481f..ea07e10f 100644 --- a/internal/data/operations_test.go +++ b/internal/data/operations_test.go @@ -2,7 +2,6 @@ package data import ( "context" - "encoding/base64" "fmt" "testing" "time" @@ -34,7 +33,7 @@ func generateTestOperations(n int, startID int64) ([]*types.Operation, map[int64 ops[i] = &types.Operation{ ID: opID, OperationType: types.OperationTypePayment, - OperationXDR: types.XDRBytea(base64.StdEncoding.EncodeToString([]byte(fmt.Sprintf("operation_xdr_%d", i)))), + OperationXDR: types.XDRBytea([]byte(fmt.Sprintf("operation_xdr_%d", i))), LedgerNumber: uint32(i + 1), LedgerCreatedAt: now, } @@ -102,13 +101,13 @@ func Test_OperationModel_BatchInsert(t *testing.T) { op1 := types.Operation{ ID: 4097, // in range (4096, 8192) OperationType: types.OperationTypePayment, - OperationXDR: types.XDRBytea(base64.StdEncoding.EncodeToString([]byte("operation1"))), + OperationXDR: types.XDRBytea([]byte("operation1")), LedgerCreatedAt: now, } op2 := types.Operation{ ID: 8193, // in range (8192, 12288) OperationType: types.OperationTypeCreateAccount, - OperationXDR: types.XDRBytea(base64.StdEncoding.EncodeToString([]byte("operation2"))), + OperationXDR: types.XDRBytea([]byte("operation2")), LedgerCreatedAt: now, } @@ -289,13 +288,13 @@ func Test_OperationModel_BatchCopy(t *testing.T) { op1 := types.Operation{ ID: 4097, // in range (4096, 8192) OperationType: types.OperationTypePayment, - OperationXDR: types.XDRBytea(base64.StdEncoding.EncodeToString([]byte("operation1"))), + OperationXDR: types.XDRBytea([]byte("operation1")), LedgerCreatedAt: now, } op2 := types.Operation{ ID: 8193, // in range (8192, 12288) OperationType: types.OperationTypeCreateAccount, - OperationXDR: types.XDRBytea(base64.StdEncoding.EncodeToString([]byte("operation2"))), + OperationXDR: types.XDRBytea([]byte("operation2")), LedgerCreatedAt: now, } @@ -433,7 +432,7 @@ func Test_OperationModel_BatchCopy_DuplicateFails(t *testing.T) { op1 := types.Operation{ ID: 999, OperationType: types.OperationTypePayment, - OperationXDR: types.XDRBytea(base64.StdEncoding.EncodeToString([]byte("operation_xdr_dup_test"))), + OperationXDR: types.XDRBytea([]byte("operation_xdr_dup_test")), LedgerNumber: 1, LedgerCreatedAt: now, } @@ -763,9 +762,9 @@ func TestOperationModel_BatchGetByToID(t *testing.T) { // Create test operations - IDs must be in TOID range for each transaction // For tx1 (to_id=4096): ops 4097, 4098 // For tx2 (to_id=8192): op 8193 - xdr1 := types.XDRBytea("xdr1") - xdr2 := types.XDRBytea("xdr2") - xdr3 := types.XDRBytea("xdr3") + xdr1 := types.XDRBytea([]byte("xdr1")) + xdr2 := types.XDRBytea([]byte("xdr2")) + xdr3 := types.XDRBytea([]byte("xdr3")) _, err = dbConnectionPool.ExecContext(ctx, ` INSERT INTO operations (id, operation_type, operation_xdr, result_code, successful, ledger_number, ledger_created_at) VALUES @@ -823,9 +822,9 @@ func TestOperationModel_BatchGetByAccountAddresses(t *testing.T) { require.NoError(t, err) // Create test operations (IDs must be in TOID range for each transaction) - xdr1 := types.XDRBytea("xdr1") - xdr2 := types.XDRBytea("xdr2") - xdr3 := types.XDRBytea("xdr3") + xdr1 := types.XDRBytea([]byte("xdr1")) + xdr2 := types.XDRBytea([]byte("xdr2")) + xdr3 := types.XDRBytea([]byte("xdr3")) _, err = dbConnectionPool.ExecContext(ctx, ` INSERT INTO operations (id, operation_type, operation_xdr, result_code, successful, ledger_number, ledger_created_at) VALUES @@ -941,9 +940,9 @@ func TestOperationModel_BatchGetByStateChangeIDs(t *testing.T) { require.NoError(t, err) // Create test operations (IDs must be in TOID range for each transaction) - xdr1 := types.XDRBytea("xdr1") - xdr2 := types.XDRBytea("xdr2") - xdr3 := types.XDRBytea("xdr3") + xdr1 := types.XDRBytea([]byte("xdr1")) + xdr2 := types.XDRBytea([]byte("xdr2")) + xdr3 := types.XDRBytea([]byte("xdr3")) _, err = dbConnectionPool.ExecContext(ctx, ` INSERT INTO operations (id, operation_type, operation_xdr, result_code, successful, ledger_number, ledger_created_at) VALUES From a5429a0fd47ab283079380536b4d6f607dfe61f5 Mon Sep 17 00:00:00 2001 From: Aditya Vyas Date: Thu, 5 Feb 2026 15:21:35 -0500 Subject: [PATCH 17/23] Update test_utils.go for XDRBytea []byte type Use raw bytes directly for test XDR data instead of base64-encoding. The String() method will handle base64 encoding for assertions. --- internal/serve/graphql/resolvers/test_utils.go | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/internal/serve/graphql/resolvers/test_utils.go b/internal/serve/graphql/resolvers/test_utils.go index dd096911..099ee9ab 100644 --- a/internal/serve/graphql/resolvers/test_utils.go +++ b/internal/serve/graphql/resolvers/test_utils.go @@ -2,7 +2,6 @@ package resolvers import ( "context" - "encoding/base64" "fmt" "testing" "time" @@ -86,12 +85,10 @@ func setupDB(ctx context.Context, t *testing.T, dbConnectionPool db.ConnectionPo // Add 2 operations for each transaction for j := range 2 { - // Create base64-encoded test XDR data (encoding "opxdr1", "opxdr2", etc.) - testXDR := base64.StdEncoding.EncodeToString([]byte(fmt.Sprintf("opxdr%d", opIdx))) ops = append(ops, &types.Operation{ ID: toid.New(testLedger, int32(i+1), int32(j+1)).ToInt64(), OperationType: "PAYMENT", - OperationXDR: types.XDRBytea(testXDR), + OperationXDR: types.XDRBytea([]byte(fmt.Sprintf("opxdr%d", opIdx))), ResultCode: "op_success", Successful: true, LedgerNumber: 1, From 41dfbf7925551272ac17efc845d1655e8b8a4828 Mon Sep 17 00:00:00 2001 From: Aditya Vyas Date: Thu, 5 Feb 2026 15:21:46 -0500 Subject: [PATCH 18/23] Update ingest_test.go for XDRBytea []byte type Use raw bytes directly instead of pre-encoded base64 string. --- internal/data/operations_test.go | 5 ++++- internal/services/ingest_test.go | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/internal/data/operations_test.go b/internal/data/operations_test.go index ea07e10f..f51273f3 100644 --- a/internal/data/operations_test.go +++ b/internal/data/operations_test.go @@ -895,7 +895,10 @@ func TestOperationModel_GetByID(t *testing.T) { operation, err := m.GetByID(ctx, 4097, "") require.NoError(t, err) assert.Equal(t, int64(4097), operation.ID) - assert.Equal(t, xdr1.String(), operation.OperationXDR.String()) + // 'xdr1' was inserted as raw string in SQL, which becomes raw bytes in BYTEA + // String() returns base64 encoding of the raw bytes + expectedXDR := types.XDRBytea([]byte("xdr1")) + assert.Equal(t, expectedXDR.String(), operation.OperationXDR.String()) assert.Equal(t, uint32(1), operation.LedgerNumber) assert.WithinDuration(t, now, operation.LedgerCreatedAt, time.Second) } diff --git a/internal/services/ingest_test.go b/internal/services/ingest_test.go index 7fe297ea..c558abb5 100644 --- a/internal/services/ingest_test.go +++ b/internal/services/ingest_test.go @@ -543,7 +543,7 @@ func createTestOperation(id int64) types.Operation { return types.Operation{ ID: id, OperationType: types.OperationTypePayment, - OperationXDR: types.XDRBytea("dGVzdF9vcGVyYXRpb25feGRy"), // base64 of "test_operation_xdr" + OperationXDR: types.XDRBytea([]byte("test_operation_xdr")), LedgerNumber: 1000, LedgerCreatedAt: now, IngestedAt: now, From bd270992e06e85260863ee814e5f5c775f35a89e Mon Sep 17 00:00:00 2001 From: Aditya Vyas Date: Thu, 5 Feb 2026 15:48:19 -0500 Subject: [PATCH 19/23] Fix operations_test.go for XDRBytea []byte type Use parameterized queries instead of raw SQL string literals for BYTEA operation_xdr column. Fix .String() assertion to compare base64 values via opXdr1.String(). --- internal/data/operations_test.go | 44 +++++++++++++++++++------------- 1 file changed, 26 insertions(+), 18 deletions(-) diff --git a/internal/data/operations_test.go b/internal/data/operations_test.go index f51273f3..40a5181e 100644 --- a/internal/data/operations_test.go +++ b/internal/data/operations_test.go @@ -516,13 +516,16 @@ func TestOperationModel_GetAll(t *testing.T) { require.NoError(t, err) // Create test operations (IDs must be in TOID range for each transaction: (to_id, to_id + 4096)) + xdr1 := types.XDRBytea([]byte("xdr1")) + xdr2 := types.XDRBytea([]byte("xdr2")) + xdr3 := types.XDRBytea([]byte("xdr3")) _, err = dbConnectionPool.ExecContext(ctx, ` INSERT INTO operations (id, operation_type, operation_xdr, result_code, successful, ledger_number, ledger_created_at) VALUES - (2, 'PAYMENT', 'xdr1', 'op_success', true, 1, $1), - (4098, 'CREATE_ACCOUNT', 'xdr2', 'op_success', true, 2, $1), - (8194, 'PAYMENT', 'xdr3', 'op_success', true, 3, $1) - `, now) + (2, 'PAYMENT', $2, 'op_success', true, 1, $1), + (4098, 'CREATE_ACCOUNT', $3, 'op_success', true, 2, $1), + (8194, 'PAYMENT', $4, 'op_success', true, 3, $1) + `, now, xdr1, xdr2, xdr3) require.NoError(t, err) // Test GetAll without limit (gets all operations) @@ -571,16 +574,22 @@ func TestOperationModel_BatchGetByToIDs(t *testing.T) { // For tx1 (to_id=4096): ops 4097, 4098, 4099 // For tx2 (to_id=8192): ops 8193, 8194 // For tx3 (to_id=12288): op 12289 + xdr1 := types.XDRBytea([]byte("xdr1")) + xdr2 := types.XDRBytea([]byte("xdr2")) + xdr3 := types.XDRBytea([]byte("xdr3")) + xdr4 := types.XDRBytea([]byte("xdr4")) + xdr5 := types.XDRBytea([]byte("xdr5")) + xdr6 := types.XDRBytea([]byte("xdr6")) _, err = dbConnectionPool.ExecContext(ctx, ` INSERT INTO operations (id, operation_type, operation_xdr, result_code, successful, ledger_number, ledger_created_at) VALUES - (4097, 'PAYMENT', 'xdr1', 'op_success', true, 1, $1), - (8193, 'CREATE_ACCOUNT', 'xdr2', 'op_success', true, 2, $1), - (4098, 'PAYMENT', 'xdr3', 'op_success', true, 3, $1), - (4099, 'MANAGE_SELL_OFFER', 'xdr4', 'op_success', true, 4, $1), - (8194, 'PAYMENT', 'xdr5', 'op_success', true, 5, $1), - (12289, 'CHANGE_TRUST', 'xdr6', 'op_success', true, 6, $1) - `, now) + (4097, 'PAYMENT', $2, 'op_success', true, 1, $1), + (8193, 'CREATE_ACCOUNT', $3, 'op_success', true, 2, $1), + (4098, 'PAYMENT', $4, 'op_success', true, 3, $1), + (4099, 'MANAGE_SELL_OFFER', $5, 'op_success', true, 4, $1), + (8194, 'PAYMENT', $6, 'op_success', true, 5, $1), + (12289, 'CHANGE_TRUST', $7, 'op_success', true, 6, $1) + `, now, xdr1, xdr2, xdr3, xdr4, xdr5, xdr6) require.NoError(t, err) testCases := []struct { @@ -874,12 +883,14 @@ func TestOperationModel_GetByID(t *testing.T) { require.NoError(t, err) // Create test operations (IDs must be in TOID range for each transaction) + opXdr1 := types.XDRBytea([]byte("xdr1")) + opXdr2 := types.XDRBytea([]byte("xdr2")) _, err = dbConnectionPool.ExecContext(ctx, ` INSERT INTO operations (id, operation_type, operation_xdr, result_code, successful, ledger_number, ledger_created_at) VALUES - (4097, 'PAYMENT', 'xdr1', 'op_success', true, 1, $1), - (8193, 'CREATE_ACCOUNT', 'xdr2', 'op_success', true, 2, $1) - `, now) + (4097, 'PAYMENT', $2, 'op_success', true, 1, $1), + (8193, 'CREATE_ACCOUNT', $3, 'op_success', true, 2, $1) + `, now, opXdr1, opXdr2) require.NoError(t, err) mockMetricsService := metrics.NewMockMetricsService() @@ -895,10 +906,7 @@ func TestOperationModel_GetByID(t *testing.T) { operation, err := m.GetByID(ctx, 4097, "") require.NoError(t, err) assert.Equal(t, int64(4097), operation.ID) - // 'xdr1' was inserted as raw string in SQL, which becomes raw bytes in BYTEA - // String() returns base64 encoding of the raw bytes - expectedXDR := types.XDRBytea([]byte("xdr1")) - assert.Equal(t, expectedXDR.String(), operation.OperationXDR.String()) + assert.Equal(t, opXdr1.String(), operation.OperationXDR.String()) assert.Equal(t, uint32(1), operation.LedgerNumber) assert.WithinDuration(t, now, operation.LedgerCreatedAt, time.Second) } From 762593e4959fe8a1bdf6f7661d89a1e5ff438496 Mon Sep 17 00:00:00 2001 From: Aditya Vyas Date: Thu, 5 Feb 2026 15:48:36 -0500 Subject: [PATCH 20/23] Fix accounts_test.go for XDRBytea []byte type Use parameterized queries instead of raw SQL string literals for BYTEA operation_xdr column in BatchGetByOperationIDs and BatchGetByStateChangeIDs tests. --- internal/data/accounts_test.go | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/internal/data/accounts_test.go b/internal/data/accounts_test.go index f34b35f3..2e1abd43 100644 --- a/internal/data/accounts_test.go +++ b/internal/data/accounts_test.go @@ -327,7 +327,9 @@ func TestAccountModelBatchGetByOperationIDs(t *testing.T) { require.NoError(t, err) // Insert test operations (IDs don't need to be in TOID range here since we're just testing operations_accounts links) - _, err = m.DB.ExecContext(ctx, "INSERT INTO operations (id, operation_type, operation_xdr, result_code, successful, ledger_number, ledger_created_at) VALUES ($1, 'PAYMENT', 'xdr1', 'op_success', true, 1, NOW()), ($2, 'PAYMENT', 'xdr2', 'op_success', true, 2, NOW())", operationID1, operationID2) + xdr1 := types.XDRBytea([]byte("xdr1")) + xdr2 := types.XDRBytea([]byte("xdr2")) + _, err = m.DB.ExecContext(ctx, "INSERT INTO operations (id, operation_type, operation_xdr, result_code, successful, ledger_number, ledger_created_at) VALUES ($1, 'PAYMENT', $3, 'op_success', true, 1, NOW()), ($2, 'PAYMENT', $4, 'op_success', true, 2, NOW())", operationID1, operationID2, xdr1, xdr2) require.NoError(t, err) // Insert test operations_accounts links (account_id is BYTEA) @@ -422,7 +424,9 @@ func TestAccountModelBatchGetByStateChangeIDs(t *testing.T) { require.NoError(t, err) // Insert test operations (IDs must be in TOID range for each transaction) - _, err = m.DB.ExecContext(ctx, "INSERT INTO operations (id, operation_type, operation_xdr, result_code, successful, ledger_number, ledger_created_at) VALUES (4097, 'PAYMENT', 'xdr1', 'op_success', true, 1, NOW()), (8193, 'PAYMENT', 'xdr2', 'op_success', true, 2, NOW())") + xdr1 := types.XDRBytea([]byte("xdr1")) + xdr2 := types.XDRBytea([]byte("xdr2")) + _, err = m.DB.ExecContext(ctx, "INSERT INTO operations (id, operation_type, operation_xdr, result_code, successful, ledger_number, ledger_created_at) VALUES (4097, 'PAYMENT', $1, 'op_success', true, 1, NOW()), (8193, 'PAYMENT', $2, 'op_success', true, 2, NOW())", xdr1, xdr2) require.NoError(t, err) // Insert test state changes that reference the accounts (state_changes.account_id is TEXT) From 5f26b91dc8c8904c1293766977c7bf385d3a23a8 Mon Sep 17 00:00:00 2001 From: Aditya Vyas Date: Thu, 5 Feb 2026 15:48:46 -0500 Subject: [PATCH 21/23] Fix transactions_test.go for XDRBytea []byte type Use parameterized queries instead of raw SQL string literals for BYTEA operation_xdr column in BatchGetByOperationIDs test. --- internal/data/transactions_test.go | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/internal/data/transactions_test.go b/internal/data/transactions_test.go index bf15daea..489e5691 100644 --- a/internal/data/transactions_test.go +++ b/internal/data/transactions_test.go @@ -650,13 +650,16 @@ func TestTransactionModel_BatchGetByOperationIDs(t *testing.T) { // Create test operations (IDs must be in TOID range for each transaction) // opTestHash1 (to_id=4096): ops 4097, 4098 // opTestHash2 (to_id=8192): op 8193 + xdr1 := types.XDRBytea([]byte("xdr1")) + xdr2 := types.XDRBytea([]byte("xdr2")) + xdr3 := types.XDRBytea([]byte("xdr3")) _, err = dbConnectionPool.ExecContext(ctx, ` INSERT INTO operations (id, operation_type, operation_xdr, result_code, successful, ledger_number, ledger_created_at) VALUES - (4097, 'PAYMENT', 'xdr1', 'op_success', true, 1, $1), - (8193, 'CREATE_ACCOUNT', 'xdr2', 'op_success', true, 2, $1), - (4098, 'PAYMENT', 'xdr3', 'op_success', true, 3, $1) - `, now) + (4097, 'PAYMENT', $2, 'op_success', true, 1, $1), + (8193, 'CREATE_ACCOUNT', $3, 'op_success', true, 2, $1), + (4098, 'PAYMENT', $4, 'op_success', true, 3, $1) + `, now, xdr1, xdr2, xdr3) require.NoError(t, err) // Test BatchGetByOperationIDs From 25ccec90548eb458de761fb9457b4f42324a0aab Mon Sep 17 00:00:00 2001 From: Aditya Vyas Date: Thu, 5 Feb 2026 15:54:24 -0500 Subject: [PATCH 22/23] Update generated.go --- internal/serve/graphql/generated/generated.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/internal/serve/graphql/generated/generated.go b/internal/serve/graphql/generated/generated.go index d24604b7..836693bd 100644 --- a/internal/serve/graphql/generated/generated.go +++ b/internal/serve/graphql/generated/generated.go @@ -14,10 +14,11 @@ import ( "github.com/99designs/gqlgen/graphql" "github.com/99designs/gqlgen/graphql/introspection" - "github.com/stellar/wallet-backend/internal/indexer/types" - "github.com/stellar/wallet-backend/internal/serve/graphql/scalars" gqlparser "github.com/vektah/gqlparser/v2" "github.com/vektah/gqlparser/v2/ast" + + "github.com/stellar/wallet-backend/internal/indexer/types" + "github.com/stellar/wallet-backend/internal/serve/graphql/scalars" ) // region ************************** generated!.gotpl ************************** From b84442d95d973a902d27fe20589d370c7e3bb366 Mon Sep 17 00:00:00 2001 From: Aditya Vyas Date: Thu, 5 Feb 2026 15:54:36 -0500 Subject: [PATCH 23/23] Fix XDRBytea.Scan buffer reuse bug Copy the byte slice from the database driver instead of referencing it directly. The pgx driver reuses its internal buffer across rows, so without copying, all scanned XDRBytea values end up pointing to the same (overwritten) buffer. --- internal/indexer/types/types.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/internal/indexer/types/types.go b/internal/indexer/types/types.go index 24fa71bd..ae6d2344 100644 --- a/internal/indexer/types/types.go +++ b/internal/indexer/types/types.go @@ -182,7 +182,8 @@ func (x *XDRBytea) Scan(value any) error { if !ok { return fmt.Errorf("expected []byte, got %T", value) } - *x = bytes + *x = make([]byte, len(bytes)) + copy(*x, bytes) return nil }