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) diff --git a/internal/data/operations.go b/internal/data/operations.go index d8f92e8e..2d862bd3 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,7 @@ func (m *OperationModel) BatchInsert( for i, op := range operations { ids[i] = op.ID operationTypes[i] = string(op.OperationType) - operationXDRs[i] = op.OperationXDR + operationXDRs[i] = []byte(op.OperationXDR) resultCodes[i] = op.ResultCode successfulFlags[i] = op.Successful ledgerNumbers[i] = op.LedgerNumber @@ -327,7 +327,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, @@ -418,7 +418,7 @@ func (m *OperationModel) BatchCopy( return []any{ pgtype.Int8{Int64: op.ID, Valid: true}, pgtype.Text{String: string(op.OperationType), Valid: true}, - pgtype.Text{String: op.OperationXDR, Valid: true}, + []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}, diff --git a/internal/data/operations_test.go b/internal/data/operations_test.go index 36420617..40a5181e 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([]byte(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([]byte("operation1")), LedgerCreatedAt: now, } op2 := types.Operation{ ID: 8193, // in range (8192, 12288) OperationType: types.OperationTypeCreateAccount, - OperationXDR: "operation2", + OperationXDR: types.XDRBytea([]byte("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([]byte("operation1")), LedgerCreatedAt: now, } op2 := types.Operation{ ID: 8193, // in range (8192, 12288) OperationType: types.OperationTypeCreateAccount, - OperationXDR: "operation2", + OperationXDR: types.XDRBytea([]byte("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([]byte("operation_xdr_dup_test")), LedgerNumber: 1, LedgerCreatedAt: now, } @@ -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 { @@ -762,21 +771,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([]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 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.String(), operations[0].OperationXDR.String()) + assert.Equal(t, xdr3.String(), operations[1].OperationXDR.String()) } func TestOperationModel_BatchGetByAccountAddresses(t *testing.T) { @@ -819,13 +831,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([]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), - (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 @@ -868,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() @@ -889,7 +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) - assert.Equal(t, "xdr1", operation.OperationXDR) + assert.Equal(t, opXdr1.String(), operation.OperationXDR.String()) assert.Equal(t, uint32(1), operation.LedgerNumber) assert.WithinDuration(t, now, operation.LedgerCreatedAt, time.Second) } @@ -934,13 +951,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([]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), - (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/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 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, diff --git a/internal/indexer/processors/utils.go b/internal/indexer/processors/utils.go index 04c88b4f..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: xdrOpStr, + OperationXDR: types.XDRBytea(xdrBytes), ResultCode: resultCode, Successful: successful, LedgerCreatedAt: transaction.Ledger.ClosedAt(), diff --git a/internal/indexer/processors/utils_test.go b/internal/indexer/processors/utils_test.go index 0f68c129..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: opXDRStr, + OperationXDR: types.XDRBytea(expectedXDRBytes), ResultCode: OpSuccess, Successful: true, LedgerCreatedAt: time.Date(2025, time.June, 19, 0, 3, 16, 0, time.UTC), diff --git a/internal/indexer/types/types.go b/internal/indexer/types/types.go index 9c939abe..ae6d2344 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,39 @@ 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: raw bytes internally, base64 string via String() +type XDRBytea []byte + +// Scan implements sql.Scanner - reads raw bytes from BYTEA column +func (x *XDRBytea) Scan(value any) error { + if value == nil { + *x = nil + return nil + } + bytes, ok := value.([]byte) + if !ok { + return fmt.Errorf("expected []byte, got %T", value) + } + *x = make([]byte, len(bytes)) + copy(*x, bytes) + return nil +} + +// Value implements driver.Valuer - returns raw bytes for BYTEA storage +func (x XDRBytea) Value() (driver.Value, error) { + if len(x) == 0 { + return nil, nil + } + return []byte(x), nil +} + +// String returns the XDR as a base64 string. +func (x XDRBytea) String() string { + return base64.StdEncoding.EncodeToString(x) +} + type ContractType string const ( @@ -372,7 +406,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"` diff --git a/internal/serve/graphql/generated/generated.go b/internal/serve/graphql/generated/generated.go index 47cb64d3..836693bd 100644 --- a/internal/serve/graphql/generated/generated.go +++ b/internal/serve/graphql/generated/generated.go @@ -164,7 +164,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 +395,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 +1011,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 +2318,7 @@ type CreateFeeBumpTransactionPayload { type Operation{ id: Int64! operationType: OperationType! - operationXdr: String! + operationXdr: String! @goField(forceResolver: true) resultCode: String! successful: Boolean! ledgerNumber: UInt32! @@ -6822,7 +6824,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 +6845,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 +17014,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/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/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 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 1e71dfb0..099ee9ab 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([]byte(fmt.Sprintf("opxdr%d", opIdx))), 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) }) 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! diff --git a/internal/services/ingest_test.go b/internal/services/ingest_test.go index aac607e1..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: "test_operation_xdr", + OperationXDR: types.XDRBytea([]byte("test_operation_xdr")), LedgerNumber: 1000, LedgerCreatedAt: now, IngestedAt: now,