diff --git a/internal/core/domain/vtxo.go b/internal/core/domain/vtxo.go index 6f66dac62..a0578fa32 100644 --- a/internal/core/domain/vtxo.go +++ b/internal/core/domain/vtxo.go @@ -65,8 +65,20 @@ type Vtxo struct { Depth uint32 // chain depth: 0 for vtxos from batch, increments on each chain MarkerIDs []string // marker IDs for DAG traversal optimization (supports multiple parent markers) Assets []AssetDenomination + Kind VtxoKind // how the vtxo is held (offchain by default, onchain for on-chain Arkade UTXOs) } +// VtxoKind distinguishes how a vtxo is held. Offchain (the default) is a batch +// leaf or an offchain-tx output. Onchain marks a vtxo held in an on-chain +// Arkade UTXO (issue #1159). It is an open enum so future on-chain sub-kinds can +// be added without another schema migration. +type VtxoKind uint8 + +const ( + VtxoKindOffchain VtxoKind = iota + VtxoKindOnchain +) + func (v Vtxo) String() string { // nolint b, _ := json.MarshalIndent(v, "", " ") @@ -74,11 +86,16 @@ func (v Vtxo) String() string { } func (v Vtxo) IsNote() bool { - return len(v.CommitmentTxids) <= 0 && v.RootCommitmentTxid == "" + // An on-chain Arkade UTXO also has no commitment txids, so the kind check + // keeps it from reading as a note. + return v.Kind != VtxoKindOnchain && + len(v.CommitmentTxids) <= 0 && v.RootCommitmentTxid == "" } func (v Vtxo) RequiresForfeit() bool { - return !v.Swept && !v.IsNote() && !v.Unrolled + // An on-chain Arkade UTXO joins a batch as a boarding input, which is + // signed directly and never forfeited. + return v.Kind != VtxoKindOnchain && !v.Swept && !v.IsNote() && !v.Unrolled } func (v Vtxo) IsSettled() bool { @@ -102,5 +119,12 @@ func (v Vtxo) OutputScript() ([]byte, error) { } func (v Vtxo) IsExpired() bool { + // An on-chain Arkade UTXO has no batch expiry, so ExpiresAt is not + // meaningful for it. Without this an on-chain vtxo (which carries a zero + // ExpiresAt) would read as permanently expired and be treated as + // unspendable by every caller. + if v.Kind == VtxoKindOnchain { + return false + } return time.Now().After(time.Unix(v.ExpiresAt, 0)) } diff --git a/internal/core/domain/vtxo_test.go b/internal/core/domain/vtxo_test.go index e1a86317d..5502a8b2e 100644 --- a/internal/core/domain/vtxo_test.go +++ b/internal/core/domain/vtxo_test.go @@ -66,6 +66,15 @@ func TestVtxo_IsNote(t *testing.T) { }, isNote: false, }, + { + // An on-chain Arkade UTXO has no commitment txids either, so + // the kind discriminator must keep it from reading as a note. + name: "onchain kind is not a note despite empty commitments", + vtxo: domain.Vtxo{ + Kind: domain.VtxoKindOnchain, + }, + isNote: false, + }, } for _, f := range fixtures { t.Run(f.name, func(t *testing.T) { @@ -114,6 +123,16 @@ func TestVtxo_IsExpired(t *testing.T) { vtxo: domain.Vtxo{ExpiresAt: time.Now().Add(time.Hour).Unix()}, isExpired: false, }, + { + // An on-chain Arkade UTXO has no batch expiry, so a zero ExpiresAt + // must not read as expired. + name: "onchain kind never expires", + vtxo: domain.Vtxo{ + Kind: domain.VtxoKindOnchain, + ExpiresAt: time.Now().Add(-time.Hour).Unix(), + }, + isExpired: false, + }, } for _, f := range fixtures { t.Run(f.name, func(t *testing.T) { @@ -172,6 +191,18 @@ func TestVtxo_RequiresForfeit(t *testing.T) { }, requiresForfeit: false, }, + { + // An on-chain Arkade UTXO is a boarding input in a batch, never a + // forfeited vtxo, even when it carries commitment txids and so + // would not read as a note. + name: "should be false (onchain kind)", + vtxo: domain.Vtxo{ + CommitmentTxids: []string{"txid1"}, + ExpiresAt: futureExpiry, + Kind: domain.VtxoKindOnchain, + }, + requiresForfeit: false, + }, } for _, f := range fixtures { t.Run(f.name, func(t *testing.T) { diff --git a/internal/infrastructure/db/postgres/marker_repo.go b/internal/infrastructure/db/postgres/marker_repo.go index f528cb787..6aa60bf43 100644 --- a/internal/infrastructure/db/postgres/marker_repo.go +++ b/internal/infrastructure/db/postgres/marker_repo.go @@ -331,6 +331,7 @@ func rowToVtxoFromVtxoVw(row queries.VtxoVw) domain.Vtxo { CreatedAt: row.CreatedAt, Depth: uint32(row.Depth), MarkerIDs: parseMarkersJSONB(row.Markers), + Kind: domain.VtxoKind(row.VtxoKind), } } @@ -369,6 +370,7 @@ func rowToVtxoFromMarkerQuery(row queries.SelectVtxosByMarkerIdRow) domain.Vtxo ExpiresAt: row.VtxoVw.ExpiresAt, CreatedAt: row.VtxoVw.CreatedAt, Depth: uint32(row.VtxoVw.Depth), + Kind: domain.VtxoKind(row.VtxoVw.VtxoKind), MarkerIDs: parseMarkersJSONB(row.VtxoVw.Markers), } } diff --git a/internal/infrastructure/db/postgres/migration/20260901000000_add_vtxo_kind.down.sql b/internal/infrastructure/db/postgres/migration/20260901000000_add_vtxo_kind.down.sql new file mode 100644 index 000000000..1e6a3094d --- /dev/null +++ b/internal/infrastructure/db/postgres/migration/20260901000000_add_vtxo_kind.down.sql @@ -0,0 +1,69 @@ +-- Reverse add_vtxo_kind: drop the views, drop the column, recreate the views +-- without vtxo_kind. +DROP VIEW IF EXISTS intent_with_inputs_vw; +DROP VIEW IF EXISTS vtxo_vw; + +ALTER TABLE vtxo DROP COLUMN IF EXISTS vtxo_kind; + +CREATE VIEW vtxo_vw AS +SELECT v.*, + COALESCE(vc.commitments, '') AS commitments, + ( + EXISTS ( + SELECT 1 + FROM jsonb_array_elements_text(v.markers) AS m(marker_id) + JOIN swept_marker sm ON sm.marker_id = m.marker_id + ) + OR EXISTS ( + SELECT 1 FROM swept_vtxo sv + WHERE sv.txid = v.txid AND sv.vout = v.vout + ) + ) AS swept, + COALESCE(ap.asset_id, '') AS asset_id, + COALESCE(ap.amount, 0) AS asset_amount +FROM vtxo v +LEFT JOIN LATERAL ( + SELECT string_agg(commitment_txid, ',') AS commitments + FROM vtxo_commitment_txid + WHERE vtxo_txid = v.txid AND vtxo_vout = v.vout +) vc ON true +LEFT JOIN ( + SELECT txid, vout, asset_id, amount + FROM asset_projection + GROUP BY txid, vout, asset_id, amount +) ap +ON ap.txid = v.txid AND ap.vout = v.vout; + +CREATE VIEW intent_with_inputs_vw AS +SELECT + v.txid, v.vout, v.pubkey, v.amount, v.expires_at, v.created_at, + v.commitment_txid, v.spent_by, v.spent, v.unrolled, v.preconfirmed, + v.settled_by, v.ark_txid, v.intent_id, v.updated_at, v.depth, v.markers, + COALESCE(vc.commitments, '') AS commitments, + ( + EXISTS ( + SELECT 1 + FROM jsonb_array_elements_text(v.markers) AS m(marker_id) + JOIN swept_marker sm ON sm.marker_id = m.marker_id + ) + OR EXISTS ( + SELECT 1 FROM swept_vtxo sv + WHERE sv.txid = v.txid AND sv.vout = v.vout + ) + ) AS swept, + COALESCE(ap.asset_id, '') AS asset_id, + COALESCE(ap.amount, 0) AS asset_amount, + intent.id, intent.round_id, intent.proof, intent.message, + intent.txid AS intent_txid +FROM intent +LEFT OUTER JOIN vtxo v ON intent.id = v.intent_id +LEFT JOIN LATERAL ( + SELECT string_agg(commitment_txid, ',') AS commitments + FROM vtxo_commitment_txid + WHERE vtxo_txid = v.txid AND vtxo_vout = v.vout +) vc ON true +LEFT JOIN ( + SELECT txid, vout, asset_id, amount + FROM asset_projection + GROUP BY txid, vout, asset_id, amount +) ap ON ap.txid = v.txid AND ap.vout = v.vout; diff --git a/internal/infrastructure/db/postgres/migration/20260901000000_add_vtxo_kind.up.sql b/internal/infrastructure/db/postgres/migration/20260901000000_add_vtxo_kind.up.sql new file mode 100644 index 000000000..93cf9b2e4 --- /dev/null +++ b/internal/infrastructure/db/postgres/migration/20260901000000_add_vtxo_kind.up.sql @@ -0,0 +1,72 @@ +-- Add the vtxo_kind discriminator: 0 = offchain (batch leaf or offchain-tx +-- output), 1 = onchain (on-chain Arkade UTXO, issue #1159). Existing rows +-- default to offchain, so no backfill is needed. +ALTER TABLE vtxo ADD COLUMN IF NOT EXISTS vtxo_kind INTEGER NOT NULL DEFAULT 0; + +-- Recreate the views so vtxo_kind is visible. vtxo_vw is SELECT v.* and picks it +-- up on recreation; intent_with_inputs_vw enumerates columns, so add it by hand. +DROP VIEW IF EXISTS intent_with_inputs_vw; +DROP VIEW IF EXISTS vtxo_vw; + +CREATE VIEW vtxo_vw AS +SELECT v.*, + COALESCE(vc.commitments, '') AS commitments, + ( + EXISTS ( + SELECT 1 + FROM jsonb_array_elements_text(v.markers) AS m(marker_id) + JOIN swept_marker sm ON sm.marker_id = m.marker_id + ) + OR EXISTS ( + SELECT 1 FROM swept_vtxo sv + WHERE sv.txid = v.txid AND sv.vout = v.vout + ) + ) AS swept, + COALESCE(ap.asset_id, '') AS asset_id, + COALESCE(ap.amount, 0) AS asset_amount +FROM vtxo v +LEFT JOIN LATERAL ( + SELECT string_agg(commitment_txid, ',') AS commitments + FROM vtxo_commitment_txid + WHERE vtxo_txid = v.txid AND vtxo_vout = v.vout +) vc ON true +LEFT JOIN ( + SELECT txid, vout, asset_id, amount + FROM asset_projection + GROUP BY txid, vout, asset_id, amount +) ap +ON ap.txid = v.txid AND ap.vout = v.vout; + +CREATE VIEW intent_with_inputs_vw AS +SELECT + v.txid, v.vout, v.pubkey, v.amount, v.expires_at, v.created_at, + v.commitment_txid, v.spent_by, v.spent, v.unrolled, v.preconfirmed, + v.settled_by, v.ark_txid, v.intent_id, v.updated_at, v.depth, v.markers, v.vtxo_kind, + COALESCE(vc.commitments, '') AS commitments, + ( + EXISTS ( + SELECT 1 + FROM jsonb_array_elements_text(v.markers) AS m(marker_id) + JOIN swept_marker sm ON sm.marker_id = m.marker_id + ) + OR EXISTS ( + SELECT 1 FROM swept_vtxo sv + WHERE sv.txid = v.txid AND sv.vout = v.vout + ) + ) AS swept, + COALESCE(ap.asset_id, '') AS asset_id, + COALESCE(ap.amount, 0) AS asset_amount, + intent.id, intent.round_id, intent.proof, intent.message, + intent.txid AS intent_txid +FROM intent +LEFT OUTER JOIN vtxo v ON intent.id = v.intent_id +LEFT JOIN LATERAL ( + SELECT string_agg(commitment_txid, ',') AS commitments + FROM vtxo_commitment_txid + WHERE vtxo_txid = v.txid AND vtxo_vout = v.vout +) vc ON true +LEFT JOIN ( + SELECT txid, vout, asset_id, amount + FROM asset_projection + GROUP BY txid, vout, asset_id, amount +) ap ON ap.txid = v.txid AND ap.vout = v.vout; diff --git a/internal/infrastructure/db/postgres/round_repo.go b/internal/infrastructure/db/postgres/round_repo.go index 4037fb1ca..6adbbf5ba 100644 --- a/internal/infrastructure/db/postgres/round_repo.go +++ b/internal/infrastructure/db/postgres/round_repo.go @@ -724,6 +724,7 @@ func combinedRowToVtxo(row queries.IntentWithInputsVw) domain.Vtxo { CreatedAt: row.CreatedAt.Int64, ArkTxid: row.ArkTxid.String, SettledBy: row.SettledBy.String, + Kind: domain.VtxoKind(row.VtxoKind.Int32), } } diff --git a/internal/infrastructure/db/postgres/sqlc/queries/models.go b/internal/infrastructure/db/postgres/sqlc/queries/models.go index 1669c7b70..f63d1e147 100644 --- a/internal/infrastructure/db/postgres/sqlc/queries/models.go +++ b/internal/infrastructure/db/postgres/sqlc/queries/models.go @@ -81,6 +81,7 @@ type IntentWithInputsVw struct { UpdatedAt sql.NullInt64 Depth sql.NullInt32 Markers pqtype.NullRawMessage + VtxoKind sql.NullInt32 Commitments []byte Swept sql.NullBool AssetID sql.NullString @@ -301,6 +302,7 @@ type Vtxo struct { UpdatedAt int64 Depth int32 Markers json.RawMessage + VtxoKind int32 } type VtxoCommitmentTxid struct { @@ -327,6 +329,7 @@ type VtxoVw struct { UpdatedAt int64 Depth int32 Markers json.RawMessage + VtxoKind int32 Commitments []byte Swept sql.NullBool AssetID string diff --git a/internal/infrastructure/db/postgres/sqlc/queries/query.sql.go b/internal/infrastructure/db/postgres/sqlc/queries/query.sql.go index 9dc790e63..bcfc99e7c 100644 --- a/internal/infrastructure/db/postgres/sqlc/queries/query.sql.go +++ b/internal/infrastructure/db/postgres/sqlc/queries/query.sql.go @@ -179,7 +179,7 @@ func (q *Queries) SelectActiveScriptConvictions(ctx context.Context, arg SelectA } const selectAllVtxos = `-- name: SelectAllVtxos :many -SELECT vtxo_vw.txid, vtxo_vw.vout, vtxo_vw.pubkey, vtxo_vw.amount, vtxo_vw.expires_at, vtxo_vw.created_at, vtxo_vw.commitment_txid, vtxo_vw.spent_by, vtxo_vw.spent, vtxo_vw.unrolled, vtxo_vw.preconfirmed, vtxo_vw.settled_by, vtxo_vw.ark_txid, vtxo_vw.intent_id, vtxo_vw.updated_at, vtxo_vw.depth, vtxo_vw.markers, vtxo_vw.commitments, vtxo_vw.swept, vtxo_vw.asset_id, vtxo_vw.asset_amount FROM vtxo_vw +SELECT vtxo_vw.txid, vtxo_vw.vout, vtxo_vw.pubkey, vtxo_vw.amount, vtxo_vw.expires_at, vtxo_vw.created_at, vtxo_vw.commitment_txid, vtxo_vw.spent_by, vtxo_vw.spent, vtxo_vw.unrolled, vtxo_vw.preconfirmed, vtxo_vw.settled_by, vtxo_vw.ark_txid, vtxo_vw.intent_id, vtxo_vw.updated_at, vtxo_vw.depth, vtxo_vw.markers, vtxo_vw.vtxo_kind, vtxo_vw.commitments, vtxo_vw.swept, vtxo_vw.asset_id, vtxo_vw.asset_amount FROM vtxo_vw ` type SelectAllVtxosRow struct { @@ -213,6 +213,7 @@ func (q *Queries) SelectAllVtxos(ctx context.Context) ([]SelectAllVtxosRow, erro &i.VtxoVw.UpdatedAt, &i.VtxoVw.Depth, &i.VtxoVw.Markers, + &i.VtxoVw.VtxoKind, &i.VtxoVw.Commitments, &i.VtxoVw.Swept, &i.VtxoVw.AssetID, @@ -727,7 +728,7 @@ func (q *Queries) SelectMarkersByIds(ctx context.Context, ids []string) ([]Marke } const selectNotUnrolledVtxos = `-- name: SelectNotUnrolledVtxos :many -SELECT vtxo_vw.txid, vtxo_vw.vout, vtxo_vw.pubkey, vtxo_vw.amount, vtxo_vw.expires_at, vtxo_vw.created_at, vtxo_vw.commitment_txid, vtxo_vw.spent_by, vtxo_vw.spent, vtxo_vw.unrolled, vtxo_vw.preconfirmed, vtxo_vw.settled_by, vtxo_vw.ark_txid, vtxo_vw.intent_id, vtxo_vw.updated_at, vtxo_vw.depth, vtxo_vw.markers, vtxo_vw.commitments, vtxo_vw.swept, vtxo_vw.asset_id, vtxo_vw.asset_amount FROM vtxo_vw WHERE unrolled = false +SELECT vtxo_vw.txid, vtxo_vw.vout, vtxo_vw.pubkey, vtxo_vw.amount, vtxo_vw.expires_at, vtxo_vw.created_at, vtxo_vw.commitment_txid, vtxo_vw.spent_by, vtxo_vw.spent, vtxo_vw.unrolled, vtxo_vw.preconfirmed, vtxo_vw.settled_by, vtxo_vw.ark_txid, vtxo_vw.intent_id, vtxo_vw.updated_at, vtxo_vw.depth, vtxo_vw.markers, vtxo_vw.vtxo_kind, vtxo_vw.commitments, vtxo_vw.swept, vtxo_vw.asset_id, vtxo_vw.asset_amount FROM vtxo_vw WHERE unrolled = false ` type SelectNotUnrolledVtxosRow struct { @@ -761,6 +762,7 @@ func (q *Queries) SelectNotUnrolledVtxos(ctx context.Context) ([]SelectNotUnroll &i.VtxoVw.UpdatedAt, &i.VtxoVw.Depth, &i.VtxoVw.Markers, + &i.VtxoVw.VtxoKind, &i.VtxoVw.Commitments, &i.VtxoVw.Swept, &i.VtxoVw.AssetID, @@ -780,7 +782,7 @@ func (q *Queries) SelectNotUnrolledVtxos(ctx context.Context) ([]SelectNotUnroll } const selectNotUnrolledVtxosWithPubkey = `-- name: SelectNotUnrolledVtxosWithPubkey :many -SELECT vtxo_vw.txid, vtxo_vw.vout, vtxo_vw.pubkey, vtxo_vw.amount, vtxo_vw.expires_at, vtxo_vw.created_at, vtxo_vw.commitment_txid, vtxo_vw.spent_by, vtxo_vw.spent, vtxo_vw.unrolled, vtxo_vw.preconfirmed, vtxo_vw.settled_by, vtxo_vw.ark_txid, vtxo_vw.intent_id, vtxo_vw.updated_at, vtxo_vw.depth, vtxo_vw.markers, vtxo_vw.commitments, vtxo_vw.swept, vtxo_vw.asset_id, vtxo_vw.asset_amount FROM vtxo_vw WHERE unrolled = false AND pubkey = $1 +SELECT vtxo_vw.txid, vtxo_vw.vout, vtxo_vw.pubkey, vtxo_vw.amount, vtxo_vw.expires_at, vtxo_vw.created_at, vtxo_vw.commitment_txid, vtxo_vw.spent_by, vtxo_vw.spent, vtxo_vw.unrolled, vtxo_vw.preconfirmed, vtxo_vw.settled_by, vtxo_vw.ark_txid, vtxo_vw.intent_id, vtxo_vw.updated_at, vtxo_vw.depth, vtxo_vw.markers, vtxo_vw.vtxo_kind, vtxo_vw.commitments, vtxo_vw.swept, vtxo_vw.asset_id, vtxo_vw.asset_amount FROM vtxo_vw WHERE unrolled = false AND pubkey = $1 ` type SelectNotUnrolledVtxosWithPubkeyRow struct { @@ -814,6 +816,7 @@ func (q *Queries) SelectNotUnrolledVtxosWithPubkey(ctx context.Context, pubkey s &i.VtxoVw.UpdatedAt, &i.VtxoVw.Depth, &i.VtxoVw.Markers, + &i.VtxoVw.VtxoKind, &i.VtxoVw.Commitments, &i.VtxoVw.Swept, &i.VtxoVw.AssetID, @@ -992,7 +995,7 @@ func (q *Queries) SelectOffchainTxsInRange(ctx context.Context, arg SelectOffcha } const selectPendingSpentVtxo = `-- name: SelectPendingSpentVtxo :many -SELECT v.txid, v.vout, v.pubkey, v.amount, v.expires_at, v.created_at, v.commitment_txid, v.spent_by, v.spent, v.unrolled, v.preconfirmed, v.settled_by, v.ark_txid, v.intent_id, v.updated_at, v.depth, v.markers, v.commitments, v.swept, v.asset_id, v.asset_amount +SELECT v.txid, v.vout, v.pubkey, v.amount, v.expires_at, v.created_at, v.commitment_txid, v.spent_by, v.spent, v.unrolled, v.preconfirmed, v.settled_by, v.ark_txid, v.intent_id, v.updated_at, v.depth, v.markers, v.vtxo_kind, v.commitments, v.swept, v.asset_id, v.asset_amount FROM vtxo_vw v WHERE v.txid = $1 AND v.vout = $2 AND v.spent = TRUE AND v.unrolled = FALSE and COALESCE(v.settled_by, '') = '' @@ -1033,6 +1036,7 @@ func (q *Queries) SelectPendingSpentVtxo(ctx context.Context, arg SelectPendingS &i.UpdatedAt, &i.Depth, &i.Markers, + &i.VtxoKind, &i.Commitments, &i.Swept, &i.AssetID, @@ -1052,7 +1056,7 @@ func (q *Queries) SelectPendingSpentVtxo(ctx context.Context, arg SelectPendingS } const selectPendingSpentVtxosWithPubkeys = `-- name: SelectPendingSpentVtxosWithPubkeys :many -SELECT v.txid, v.vout, v.pubkey, v.amount, v.expires_at, v.created_at, v.commitment_txid, v.spent_by, v.spent, v.unrolled, v.preconfirmed, v.settled_by, v.ark_txid, v.intent_id, v.updated_at, v.depth, v.markers, v.commitments, v.swept, v.asset_id, v.asset_amount +SELECT v.txid, v.vout, v.pubkey, v.amount, v.expires_at, v.created_at, v.commitment_txid, v.spent_by, v.spent, v.unrolled, v.preconfirmed, v.settled_by, v.ark_txid, v.intent_id, v.updated_at, v.depth, v.markers, v.vtxo_kind, v.commitments, v.swept, v.asset_id, v.asset_amount FROM vtxo_vw v WHERE v.spent = TRUE AND v.unrolled = FALSE and COALESCE(v.settled_by, '') = '' AND v.pubkey = ANY($1::varchar[]) @@ -1096,6 +1100,7 @@ func (q *Queries) SelectPendingSpentVtxosWithPubkeys(ctx context.Context, arg Se &i.UpdatedAt, &i.Depth, &i.Markers, + &i.VtxoKind, &i.Commitments, &i.Swept, &i.AssetID, @@ -1415,7 +1420,7 @@ func (q *Queries) SelectRoundVtxoTree(ctx context.Context, txid string) ([]Tx, e } const selectRoundVtxoTreeLeaves = `-- name: SelectRoundVtxoTreeLeaves :many -SELECT vtxo_vw.txid, vtxo_vw.vout, vtxo_vw.pubkey, vtxo_vw.amount, vtxo_vw.expires_at, vtxo_vw.created_at, vtxo_vw.commitment_txid, vtxo_vw.spent_by, vtxo_vw.spent, vtxo_vw.unrolled, vtxo_vw.preconfirmed, vtxo_vw.settled_by, vtxo_vw.ark_txid, vtxo_vw.intent_id, vtxo_vw.updated_at, vtxo_vw.depth, vtxo_vw.markers, vtxo_vw.commitments, vtxo_vw.swept, vtxo_vw.asset_id, vtxo_vw.asset_amount FROM vtxo_vw WHERE commitment_txid = $1 AND preconfirmed = false +SELECT vtxo_vw.txid, vtxo_vw.vout, vtxo_vw.pubkey, vtxo_vw.amount, vtxo_vw.expires_at, vtxo_vw.created_at, vtxo_vw.commitment_txid, vtxo_vw.spent_by, vtxo_vw.spent, vtxo_vw.unrolled, vtxo_vw.preconfirmed, vtxo_vw.settled_by, vtxo_vw.ark_txid, vtxo_vw.intent_id, vtxo_vw.updated_at, vtxo_vw.depth, vtxo_vw.markers, vtxo_vw.vtxo_kind, vtxo_vw.commitments, vtxo_vw.swept, vtxo_vw.asset_id, vtxo_vw.asset_amount FROM vtxo_vw WHERE commitment_txid = $1 AND preconfirmed = false ` type SelectRoundVtxoTreeLeavesRow struct { @@ -1449,6 +1454,7 @@ func (q *Queries) SelectRoundVtxoTreeLeaves(ctx context.Context, commitmentTxid &i.VtxoVw.UpdatedAt, &i.VtxoVw.Depth, &i.VtxoVw.Markers, + &i.VtxoVw.VtxoKind, &i.VtxoVw.Commitments, &i.VtxoVw.Swept, &i.VtxoVw.AssetID, @@ -1472,7 +1478,7 @@ SELECT round.id, round.starting_timestamp, round.ending_timestamp, round.ended, round_intents_vw.id, round_intents_vw.round_id, round_intents_vw.proof, round_intents_vw.message, round_intents_vw.txid, round_txs_vw.txid, round_txs_vw.tx, round_txs_vw.round_id, round_txs_vw.type, round_txs_vw.position, round_txs_vw.children, intent_with_receivers_vw.intent_id, intent_with_receivers_vw.pubkey, intent_with_receivers_vw.onchain_address, intent_with_receivers_vw.amount, intent_with_receivers_vw.id, intent_with_receivers_vw.round_id, intent_with_receivers_vw.proof, intent_with_receivers_vw.message, intent_with_receivers_vw.txid, - intent_with_inputs_vw.txid, intent_with_inputs_vw.vout, intent_with_inputs_vw.pubkey, intent_with_inputs_vw.amount, intent_with_inputs_vw.expires_at, intent_with_inputs_vw.created_at, intent_with_inputs_vw.commitment_txid, intent_with_inputs_vw.spent_by, intent_with_inputs_vw.spent, intent_with_inputs_vw.unrolled, intent_with_inputs_vw.preconfirmed, intent_with_inputs_vw.settled_by, intent_with_inputs_vw.ark_txid, intent_with_inputs_vw.intent_id, intent_with_inputs_vw.updated_at, intent_with_inputs_vw.depth, intent_with_inputs_vw.markers, intent_with_inputs_vw.commitments, intent_with_inputs_vw.swept, intent_with_inputs_vw.asset_id, intent_with_inputs_vw.asset_amount, intent_with_inputs_vw.id, intent_with_inputs_vw.round_id, intent_with_inputs_vw.proof, intent_with_inputs_vw.message, intent_with_inputs_vw.intent_txid + intent_with_inputs_vw.txid, intent_with_inputs_vw.vout, intent_with_inputs_vw.pubkey, intent_with_inputs_vw.amount, intent_with_inputs_vw.expires_at, intent_with_inputs_vw.created_at, intent_with_inputs_vw.commitment_txid, intent_with_inputs_vw.spent_by, intent_with_inputs_vw.spent, intent_with_inputs_vw.unrolled, intent_with_inputs_vw.preconfirmed, intent_with_inputs_vw.settled_by, intent_with_inputs_vw.ark_txid, intent_with_inputs_vw.intent_id, intent_with_inputs_vw.updated_at, intent_with_inputs_vw.depth, intent_with_inputs_vw.markers, intent_with_inputs_vw.vtxo_kind, intent_with_inputs_vw.commitments, intent_with_inputs_vw.swept, intent_with_inputs_vw.asset_id, intent_with_inputs_vw.asset_amount, intent_with_inputs_vw.id, intent_with_inputs_vw.round_id, intent_with_inputs_vw.proof, intent_with_inputs_vw.message, intent_with_inputs_vw.intent_txid FROM round LEFT OUTER JOIN round_intents_vw ON round.id=round_intents_vw.round_id LEFT OUTER JOIN round_txs_vw ON round.id=round_txs_vw.round_id @@ -1548,6 +1554,7 @@ func (q *Queries) SelectRoundWithId(ctx context.Context, id string) ([]SelectRou &i.IntentWithInputsVw.UpdatedAt, &i.IntentWithInputsVw.Depth, &i.IntentWithInputsVw.Markers, + &i.IntentWithInputsVw.VtxoKind, &i.IntentWithInputsVw.Commitments, &i.IntentWithInputsVw.Swept, &i.IntentWithInputsVw.AssetID, @@ -1576,7 +1583,7 @@ SELECT round.id, round.starting_timestamp, round.ending_timestamp, round.ended, round_intents_vw.id, round_intents_vw.round_id, round_intents_vw.proof, round_intents_vw.message, round_intents_vw.txid, round_txs_vw.txid, round_txs_vw.tx, round_txs_vw.round_id, round_txs_vw.type, round_txs_vw.position, round_txs_vw.children, intent_with_receivers_vw.intent_id, intent_with_receivers_vw.pubkey, intent_with_receivers_vw.onchain_address, intent_with_receivers_vw.amount, intent_with_receivers_vw.id, intent_with_receivers_vw.round_id, intent_with_receivers_vw.proof, intent_with_receivers_vw.message, intent_with_receivers_vw.txid, - intent_with_inputs_vw.txid, intent_with_inputs_vw.vout, intent_with_inputs_vw.pubkey, intent_with_inputs_vw.amount, intent_with_inputs_vw.expires_at, intent_with_inputs_vw.created_at, intent_with_inputs_vw.commitment_txid, intent_with_inputs_vw.spent_by, intent_with_inputs_vw.spent, intent_with_inputs_vw.unrolled, intent_with_inputs_vw.preconfirmed, intent_with_inputs_vw.settled_by, intent_with_inputs_vw.ark_txid, intent_with_inputs_vw.intent_id, intent_with_inputs_vw.updated_at, intent_with_inputs_vw.depth, intent_with_inputs_vw.markers, intent_with_inputs_vw.commitments, intent_with_inputs_vw.swept, intent_with_inputs_vw.asset_id, intent_with_inputs_vw.asset_amount, intent_with_inputs_vw.id, intent_with_inputs_vw.round_id, intent_with_inputs_vw.proof, intent_with_inputs_vw.message, intent_with_inputs_vw.intent_txid + intent_with_inputs_vw.txid, intent_with_inputs_vw.vout, intent_with_inputs_vw.pubkey, intent_with_inputs_vw.amount, intent_with_inputs_vw.expires_at, intent_with_inputs_vw.created_at, intent_with_inputs_vw.commitment_txid, intent_with_inputs_vw.spent_by, intent_with_inputs_vw.spent, intent_with_inputs_vw.unrolled, intent_with_inputs_vw.preconfirmed, intent_with_inputs_vw.settled_by, intent_with_inputs_vw.ark_txid, intent_with_inputs_vw.intent_id, intent_with_inputs_vw.updated_at, intent_with_inputs_vw.depth, intent_with_inputs_vw.markers, intent_with_inputs_vw.vtxo_kind, intent_with_inputs_vw.commitments, intent_with_inputs_vw.swept, intent_with_inputs_vw.asset_id, intent_with_inputs_vw.asset_amount, intent_with_inputs_vw.id, intent_with_inputs_vw.round_id, intent_with_inputs_vw.proof, intent_with_inputs_vw.message, intent_with_inputs_vw.intent_txid FROM round LEFT OUTER JOIN round_intents_vw ON round.id=round_intents_vw.round_id LEFT OUTER JOIN round_txs_vw ON round.id=round_txs_vw.round_id @@ -1654,6 +1661,7 @@ func (q *Queries) SelectRoundWithTxid(ctx context.Context, txid string) ([]Selec &i.IntentWithInputsVw.UpdatedAt, &i.IntentWithInputsVw.Depth, &i.IntentWithInputsVw.Markers, + &i.IntentWithInputsVw.VtxoKind, &i.IntentWithInputsVw.Commitments, &i.IntentWithInputsVw.Swept, &i.IntentWithInputsVw.AssetID, @@ -1862,7 +1870,7 @@ func (q *Queries) SelectSweepableRounds(ctx context.Context) ([]string, error) { } const selectSweepableUnrolledVtxos = `-- name: SelectSweepableUnrolledVtxos :many -SELECT vtxo_vw.txid, vtxo_vw.vout, vtxo_vw.pubkey, vtxo_vw.amount, vtxo_vw.expires_at, vtxo_vw.created_at, vtxo_vw.commitment_txid, vtxo_vw.spent_by, vtxo_vw.spent, vtxo_vw.unrolled, vtxo_vw.preconfirmed, vtxo_vw.settled_by, vtxo_vw.ark_txid, vtxo_vw.intent_id, vtxo_vw.updated_at, vtxo_vw.depth, vtxo_vw.markers, vtxo_vw.commitments, vtxo_vw.swept, vtxo_vw.asset_id, vtxo_vw.asset_amount FROM vtxo_vw WHERE spent = true AND unrolled = true AND swept = false AND COALESCE(settled_by, '') = '' +SELECT vtxo_vw.txid, vtxo_vw.vout, vtxo_vw.pubkey, vtxo_vw.amount, vtxo_vw.expires_at, vtxo_vw.created_at, vtxo_vw.commitment_txid, vtxo_vw.spent_by, vtxo_vw.spent, vtxo_vw.unrolled, vtxo_vw.preconfirmed, vtxo_vw.settled_by, vtxo_vw.ark_txid, vtxo_vw.intent_id, vtxo_vw.updated_at, vtxo_vw.depth, vtxo_vw.markers, vtxo_vw.vtxo_kind, vtxo_vw.commitments, vtxo_vw.swept, vtxo_vw.asset_id, vtxo_vw.asset_amount FROM vtxo_vw WHERE spent = true AND unrolled = true AND swept = false AND COALESCE(settled_by, '') = '' ` type SelectSweepableUnrolledVtxosRow struct { @@ -1896,6 +1904,7 @@ func (q *Queries) SelectSweepableUnrolledVtxos(ctx context.Context) ([]SelectSwe &i.VtxoVw.UpdatedAt, &i.VtxoVw.Depth, &i.VtxoVw.Markers, + &i.VtxoVw.VtxoKind, &i.VtxoVw.Commitments, &i.VtxoVw.Swept, &i.VtxoVw.AssetID, @@ -2042,7 +2051,7 @@ func (q *Queries) SelectTxs(ctx context.Context, dollar_1 []string) ([]SelectTxs } const selectVtxo = `-- name: SelectVtxo :many -SELECT vtxo_vw.txid, vtxo_vw.vout, vtxo_vw.pubkey, vtxo_vw.amount, vtxo_vw.expires_at, vtxo_vw.created_at, vtxo_vw.commitment_txid, vtxo_vw.spent_by, vtxo_vw.spent, vtxo_vw.unrolled, vtxo_vw.preconfirmed, vtxo_vw.settled_by, vtxo_vw.ark_txid, vtxo_vw.intent_id, vtxo_vw.updated_at, vtxo_vw.depth, vtxo_vw.markers, vtxo_vw.commitments, vtxo_vw.swept, vtxo_vw.asset_id, vtxo_vw.asset_amount FROM vtxo_vw WHERE txid = $1 AND vout = $2 +SELECT vtxo_vw.txid, vtxo_vw.vout, vtxo_vw.pubkey, vtxo_vw.amount, vtxo_vw.expires_at, vtxo_vw.created_at, vtxo_vw.commitment_txid, vtxo_vw.spent_by, vtxo_vw.spent, vtxo_vw.unrolled, vtxo_vw.preconfirmed, vtxo_vw.settled_by, vtxo_vw.ark_txid, vtxo_vw.intent_id, vtxo_vw.updated_at, vtxo_vw.depth, vtxo_vw.markers, vtxo_vw.vtxo_kind, vtxo_vw.commitments, vtxo_vw.swept, vtxo_vw.asset_id, vtxo_vw.asset_amount FROM vtxo_vw WHERE txid = $1 AND vout = $2 ` type SelectVtxoParams struct { @@ -2081,6 +2090,7 @@ func (q *Queries) SelectVtxo(ctx context.Context, arg SelectVtxoParams) ([]Selec &i.VtxoVw.UpdatedAt, &i.VtxoVw.Depth, &i.VtxoVw.Markers, + &i.VtxoVw.VtxoKind, &i.VtxoVw.Commitments, &i.VtxoVw.Swept, &i.VtxoVw.AssetID, @@ -2100,7 +2110,7 @@ func (q *Queries) SelectVtxo(ctx context.Context, arg SelectVtxoParams) ([]Selec } const selectVtxoChainByMarker = `-- name: SelectVtxoChainByMarker :many -SELECT txid, vout, pubkey, amount, expires_at, created_at, commitment_txid, spent_by, spent, unrolled, preconfirmed, settled_by, ark_txid, intent_id, updated_at, depth, markers, commitments, swept, asset_id, asset_amount FROM vtxo_vw +SELECT txid, vout, pubkey, amount, expires_at, created_at, commitment_txid, spent_by, spent, unrolled, preconfirmed, settled_by, ark_txid, intent_id, updated_at, depth, markers, vtxo_kind, commitments, swept, asset_id, asset_amount FROM vtxo_vw WHERE markers ?| $1::TEXT[] ORDER BY depth DESC ` @@ -2133,6 +2143,7 @@ func (q *Queries) SelectVtxoChainByMarker(ctx context.Context, markerIds []strin &i.UpdatedAt, &i.Depth, &i.Markers, + &i.VtxoKind, &i.Commitments, &i.Swept, &i.AssetID, @@ -2235,7 +2246,7 @@ func (q *Queries) SelectVtxoPubKeysByCommitmentTxids(ctx context.Context, arg Se } const selectVtxosByArkTxid = `-- name: SelectVtxosByArkTxid :many -SELECT txid, vout, pubkey, amount, expires_at, created_at, commitment_txid, spent_by, spent, unrolled, preconfirmed, settled_by, ark_txid, intent_id, updated_at, depth, markers, commitments, swept, asset_id, asset_amount FROM vtxo_vw WHERE ark_txid = $1 +SELECT txid, vout, pubkey, amount, expires_at, created_at, commitment_txid, spent_by, spent, unrolled, preconfirmed, settled_by, ark_txid, intent_id, updated_at, depth, markers, vtxo_kind, commitments, swept, asset_id, asset_amount FROM vtxo_vw WHERE ark_txid = $1 ` // Get all VTXOs created by a specific ark tx (offchain tx) @@ -2266,6 +2277,7 @@ func (q *Queries) SelectVtxosByArkTxid(ctx context.Context, arkTxid sql.NullStri &i.UpdatedAt, &i.Depth, &i.Markers, + &i.VtxoKind, &i.Commitments, &i.Swept, &i.AssetID, @@ -2286,7 +2298,7 @@ func (q *Queries) SelectVtxosByArkTxid(ctx context.Context, arkTxid sql.NullStri const selectVtxosByDepthRange = `-- name: SelectVtxosByDepthRange :many -SELECT txid, vout, pubkey, amount, expires_at, created_at, commitment_txid, spent_by, spent, unrolled, preconfirmed, settled_by, ark_txid, intent_id, updated_at, depth, markers, commitments, swept, asset_id, asset_amount FROM vtxo_vw +SELECT txid, vout, pubkey, amount, expires_at, created_at, commitment_txid, spent_by, spent, unrolled, preconfirmed, settled_by, ark_txid, intent_id, updated_at, depth, markers, vtxo_kind, commitments, swept, asset_id, asset_amount FROM vtxo_vw WHERE depth >= $1 AND depth <= $2 ORDER BY depth DESC ` @@ -2325,6 +2337,7 @@ func (q *Queries) SelectVtxosByDepthRange(ctx context.Context, arg SelectVtxosBy &i.UpdatedAt, &i.Depth, &i.Markers, + &i.VtxoKind, &i.Commitments, &i.Swept, &i.AssetID, @@ -2344,7 +2357,7 @@ func (q *Queries) SelectVtxosByDepthRange(ctx context.Context, arg SelectVtxosBy } const selectVtxosByMarkerId = `-- name: SelectVtxosByMarkerId :many -SELECT vtxo_vw.txid, vtxo_vw.vout, vtxo_vw.pubkey, vtxo_vw.amount, vtxo_vw.expires_at, vtxo_vw.created_at, vtxo_vw.commitment_txid, vtxo_vw.spent_by, vtxo_vw.spent, vtxo_vw.unrolled, vtxo_vw.preconfirmed, vtxo_vw.settled_by, vtxo_vw.ark_txid, vtxo_vw.intent_id, vtxo_vw.updated_at, vtxo_vw.depth, vtxo_vw.markers, vtxo_vw.commitments, vtxo_vw.swept, vtxo_vw.asset_id, vtxo_vw.asset_amount FROM vtxo_vw WHERE markers @> jsonb_build_array($1::TEXT) +SELECT vtxo_vw.txid, vtxo_vw.vout, vtxo_vw.pubkey, vtxo_vw.amount, vtxo_vw.expires_at, vtxo_vw.created_at, vtxo_vw.commitment_txid, vtxo_vw.spent_by, vtxo_vw.spent, vtxo_vw.unrolled, vtxo_vw.preconfirmed, vtxo_vw.settled_by, vtxo_vw.ark_txid, vtxo_vw.intent_id, vtxo_vw.updated_at, vtxo_vw.depth, vtxo_vw.markers, vtxo_vw.vtxo_kind, vtxo_vw.commitments, vtxo_vw.swept, vtxo_vw.asset_id, vtxo_vw.asset_amount FROM vtxo_vw WHERE markers @> jsonb_build_array($1::TEXT) ` type SelectVtxosByMarkerIdRow struct { @@ -2379,6 +2392,7 @@ func (q *Queries) SelectVtxosByMarkerId(ctx context.Context, markerID string) ([ &i.VtxoVw.UpdatedAt, &i.VtxoVw.Depth, &i.VtxoVw.Markers, + &i.VtxoVw.VtxoKind, &i.VtxoVw.Commitments, &i.VtxoVw.Swept, &i.VtxoVw.AssetID, @@ -2468,7 +2482,7 @@ func (q *Queries) SelectVtxosOutpointsByArkTxidRecursive(ctx context.Context, ar } const selectVtxosWithPubkeys = `-- name: SelectVtxosWithPubkeys :many -SELECT vtxo_vw.txid, vtxo_vw.vout, vtxo_vw.pubkey, vtxo_vw.amount, vtxo_vw.expires_at, vtxo_vw.created_at, vtxo_vw.commitment_txid, vtxo_vw.spent_by, vtxo_vw.spent, vtxo_vw.unrolled, vtxo_vw.preconfirmed, vtxo_vw.settled_by, vtxo_vw.ark_txid, vtxo_vw.intent_id, vtxo_vw.updated_at, vtxo_vw.depth, vtxo_vw.markers, vtxo_vw.commitments, vtxo_vw.swept, vtxo_vw.asset_id, vtxo_vw.asset_amount FROM vtxo_vw +SELECT vtxo_vw.txid, vtxo_vw.vout, vtxo_vw.pubkey, vtxo_vw.amount, vtxo_vw.expires_at, vtxo_vw.created_at, vtxo_vw.commitment_txid, vtxo_vw.spent_by, vtxo_vw.spent, vtxo_vw.unrolled, vtxo_vw.preconfirmed, vtxo_vw.settled_by, vtxo_vw.ark_txid, vtxo_vw.intent_id, vtxo_vw.updated_at, vtxo_vw.depth, vtxo_vw.markers, vtxo_vw.vtxo_kind, vtxo_vw.commitments, vtxo_vw.swept, vtxo_vw.asset_id, vtxo_vw.asset_amount FROM vtxo_vw WHERE vtxo_vw.pubkey = ANY($1::varchar[]) AND vtxo_vw.updated_at >= $2::bigint AND ($3::bigint = 0 OR vtxo_vw.updated_at <= $3::bigint) @@ -2511,6 +2525,7 @@ func (q *Queries) SelectVtxosWithPubkeys(ctx context.Context, arg SelectVtxosWit &i.VtxoVw.UpdatedAt, &i.VtxoVw.Depth, &i.VtxoVw.Markers, + &i.VtxoVw.VtxoKind, &i.VtxoVw.Commitments, &i.VtxoVw.Swept, &i.VtxoVw.AssetID, @@ -3072,11 +3087,11 @@ func (q *Queries) UpsertTx(ctx context.Context, arg UpsertTxParams) error { const upsertVtxo = `-- name: UpsertVtxo :exec INSERT INTO vtxo ( txid, vout, pubkey, amount, commitment_txid, settled_by, ark_txid, - spent_by, spent, unrolled, preconfirmed, expires_at, created_at, updated_at, depth, markers + spent_by, spent, unrolled, preconfirmed, expires_at, created_at, updated_at, depth, markers, vtxo_kind ) VALUES ( $1, $2, $3, $4, $5, $6, $7, - $8, $9, $10, $11, $12, $13, (EXTRACT(EPOCH FROM NOW()) * 1000)::BIGINT, $14, $15::jsonb + $8, $9, $10, $11, $12, $13, (EXTRACT(EPOCH FROM NOW()) * 1000)::BIGINT, $14, $15::jsonb, $16 ) ON CONFLICT(txid, vout) DO UPDATE SET pubkey = EXCLUDED.pubkey, amount = EXCLUDED.amount, @@ -3091,7 +3106,8 @@ VALUES ( created_at = EXCLUDED.created_at, updated_at = (EXTRACT(EPOCH FROM NOW()) * 1000)::BIGINT, depth = EXCLUDED.depth, - markers = EXCLUDED.markers + markers = EXCLUDED.markers, + vtxo_kind = EXCLUDED.vtxo_kind ` type UpsertVtxoParams struct { @@ -3110,6 +3126,7 @@ type UpsertVtxoParams struct { CreatedAt int64 Depth int32 Markers json.RawMessage + VtxoKind int32 } func (q *Queries) UpsertVtxo(ctx context.Context, arg UpsertVtxoParams) error { @@ -3129,6 +3146,7 @@ func (q *Queries) UpsertVtxo(ctx context.Context, arg UpsertVtxoParams) error { arg.CreatedAt, arg.Depth, arg.Markers, + arg.VtxoKind, ) return err } diff --git a/internal/infrastructure/db/postgres/sqlc/query.sql b/internal/infrastructure/db/postgres/sqlc/query.sql index 02e12ddc5..3239994bb 100644 --- a/internal/infrastructure/db/postgres/sqlc/query.sql +++ b/internal/infrastructure/db/postgres/sqlc/query.sql @@ -49,11 +49,11 @@ ON CONFLICT(intent_id, pubkey, onchain_address) DO UPDATE SET -- name: UpsertVtxo :exec INSERT INTO vtxo ( txid, vout, pubkey, amount, commitment_txid, settled_by, ark_txid, - spent_by, spent, unrolled, preconfirmed, expires_at, created_at, updated_at, depth, markers + spent_by, spent, unrolled, preconfirmed, expires_at, created_at, updated_at, depth, markers, vtxo_kind ) VALUES ( @txid, @vout, @pubkey, @amount, @commitment_txid, @settled_by, @ark_txid, - @spent_by, @spent, @unrolled, @preconfirmed, @expires_at, @created_at, (EXTRACT(EPOCH FROM NOW()) * 1000)::BIGINT, @depth, @markers::jsonb + @spent_by, @spent, @unrolled, @preconfirmed, @expires_at, @created_at, (EXTRACT(EPOCH FROM NOW()) * 1000)::BIGINT, @depth, @markers::jsonb, @vtxo_kind ) ON CONFLICT(txid, vout) DO UPDATE SET pubkey = EXCLUDED.pubkey, amount = EXCLUDED.amount, @@ -68,7 +68,8 @@ VALUES ( created_at = EXCLUDED.created_at, updated_at = (EXTRACT(EPOCH FROM NOW()) * 1000)::BIGINT, depth = EXCLUDED.depth, - markers = EXCLUDED.markers; + markers = EXCLUDED.markers, + vtxo_kind = EXCLUDED.vtxo_kind; -- name: InsertVtxoCommitmentTxid :exec INSERT INTO vtxo_commitment_txid (vtxo_txid, vtxo_vout, commitment_txid) diff --git a/internal/infrastructure/db/postgres/vtxo_repo.go b/internal/infrastructure/db/postgres/vtxo_repo.go index 019f9d615..17c70a99b 100644 --- a/internal/infrastructure/db/postgres/vtxo_repo.go +++ b/internal/infrastructure/db/postgres/vtxo_repo.go @@ -74,8 +74,9 @@ func (v *vtxoRepository) AddVtxos(ctx context.Context, vtxos []domain.Vtxo) erro ArkTxid: sql.NullString{ String: vtxo.ArkTxid, Valid: len(vtxo.ArkTxid) > 0, }, - Depth: int32(vtxo.Depth), - Markers: markersJSON, + Depth: int32(vtxo.Depth), + Markers: markersJSON, + VtxoKind: int32(vtxo.Kind), }, ); err != nil { return err @@ -582,6 +583,7 @@ func rowToVtxo(row queries.VtxoVw) domain.Vtxo { Depth: uint32(row.Depth), MarkerIDs: parseMarkersJSONBFromVtxo(row.Markers), Assets: assets, + Kind: domain.VtxoKind(row.VtxoKind), } } diff --git a/internal/infrastructure/db/sqlite/marker_repo.go b/internal/infrastructure/db/sqlite/marker_repo.go index 2243213e5..ef71fe7a7 100644 --- a/internal/infrastructure/db/sqlite/marker_repo.go +++ b/internal/infrastructure/db/sqlite/marker_repo.go @@ -399,6 +399,7 @@ func rowToVtxoFromMarkerQuery(row queries.SelectVtxosByMarkerIdRow) domain.Vtxo ExpiresAt: row.VtxoVw.ExpiresAt, CreatedAt: row.VtxoVw.CreatedAt, Depth: uint32(row.VtxoVw.Depth), + Kind: domain.VtxoKind(row.VtxoVw.VtxoKind), MarkerIDs: parseMarkersJSON(row.VtxoVw.Markers), } } @@ -427,6 +428,7 @@ func rowToVtxoFromDepthRangeQuery(row queries.SelectVtxosByDepthRangeRow) domain ExpiresAt: row.VtxoVw.ExpiresAt, CreatedAt: row.VtxoVw.CreatedAt, Depth: uint32(row.VtxoVw.Depth), + Kind: domain.VtxoKind(row.VtxoVw.VtxoKind), MarkerIDs: parseMarkersJSON(row.VtxoVw.Markers), } } @@ -455,6 +457,7 @@ func rowToVtxoFromArkTxidQuery(row queries.SelectVtxosByArkTxidRow) domain.Vtxo ExpiresAt: row.VtxoVw.ExpiresAt, CreatedAt: row.VtxoVw.CreatedAt, Depth: uint32(row.VtxoVw.Depth), + Kind: domain.VtxoKind(row.VtxoVw.VtxoKind), MarkerIDs: parseMarkersJSON(row.VtxoVw.Markers), } } @@ -483,6 +486,7 @@ func rowToVtxoFromChainQuery(row queries.SelectVtxoChainByMarkerRow) domain.Vtxo ExpiresAt: row.VtxoVw.ExpiresAt, CreatedAt: row.VtxoVw.CreatedAt, Depth: uint32(row.VtxoVw.Depth), + Kind: domain.VtxoKind(row.VtxoVw.VtxoKind), MarkerIDs: parseMarkersJSON(row.VtxoVw.Markers), } } diff --git a/internal/infrastructure/db/sqlite/migration/20260901000000_add_vtxo_kind.down.sql b/internal/infrastructure/db/sqlite/migration/20260901000000_add_vtxo_kind.down.sql new file mode 100644 index 000000000..93a9de316 --- /dev/null +++ b/internal/infrastructure/db/sqlite/migration/20260901000000_add_vtxo_kind.down.sql @@ -0,0 +1,77 @@ +-- Reverse add_vtxo_kind: drop the views, drop the column, recreate the views +-- without vtxo_kind. +DROP VIEW IF EXISTS intent_with_inputs_vw; +DROP VIEW IF EXISTS vtxo_vw; + +ALTER TABLE vtxo DROP COLUMN vtxo_kind; + +CREATE VIEW vtxo_vw AS +SELECT v.*, + COALESCE(( + SELECT group_concat(commitment_txid, ',') + FROM vtxo_commitment_txid + WHERE vtxo_txid = v.txid AND vtxo_vout = v.vout + ), '') AS commitments, + ( + EXISTS ( + SELECT 1 FROM swept_marker sm + JOIN json_each(v.markers) j ON j.value = sm.marker_id + ) + OR EXISTS ( + SELECT 1 FROM swept_vtxo sv + WHERE sv.txid = v.txid AND sv.vout = v.vout + ) + ) AS swept, + COALESCE(ap.asset_id, '') AS asset_id, + COALESCE(ap.amount, 0) AS asset_amount +FROM vtxo v +LEFT JOIN ( + SELECT DISTINCT txid, vout, asset_id, amount + FROM asset_projection +) AS ap +ON ap.txid = v.txid AND ap.vout = v.vout; + +CREATE VIEW intent_with_inputs_vw AS +SELECT + v.txid, + v.vout, + v.pubkey, + v.amount, + v.expires_at, + v.created_at, + v.commitment_txid, + v.spent_by, + v.spent, + v.unrolled, + v.preconfirmed, + v.settled_by, + v.ark_txid, + v.intent_id, + v.updated_at, + v.depth, + v.markers, + COALESCE(( + SELECT group_concat(vc.commitment_txid) + FROM vtxo_commitment_txid vc + WHERE vc.vtxo_txid = v.txid AND vc.vtxo_vout = v.vout + ), '') AS commitments, + ( + EXISTS ( + SELECT 1 FROM swept_marker sm + JOIN json_each(v.markers) j ON j.value = sm.marker_id + ) + OR EXISTS ( + SELECT 1 FROM swept_vtxo sv + WHERE sv.txid = v.txid AND sv.vout = v.vout + ) + ) AS swept, + COALESCE(ap.asset_id, '') AS asset_id, + COALESCE(ap.amount, 0) AS asset_amount, + intent.id, + intent.round_id, + intent.proof, + intent.message, + intent.txid AS intent_txid +FROM intent +LEFT OUTER JOIN vtxo v ON intent.id = v.intent_id +LEFT JOIN asset_projection ap ON v.txid = ap.txid AND v.vout = ap.vout; diff --git a/internal/infrastructure/db/sqlite/migration/20260901000000_add_vtxo_kind.up.sql b/internal/infrastructure/db/sqlite/migration/20260901000000_add_vtxo_kind.up.sql new file mode 100644 index 000000000..89338cc26 --- /dev/null +++ b/internal/infrastructure/db/sqlite/migration/20260901000000_add_vtxo_kind.up.sql @@ -0,0 +1,81 @@ +-- Add the vtxo_kind discriminator: 0 = offchain (batch leaf or offchain-tx +-- output), 1 = onchain (on-chain Arkade UTXO, issue #1159). Existing rows +-- default to offchain, so no backfill is needed. +ALTER TABLE vtxo ADD COLUMN vtxo_kind INTEGER NOT NULL DEFAULT 0; + +-- Recreate the views so vtxo_kind is visible. vtxo_vw is SELECT v.* and picks it +-- up on recreation; intent_with_inputs_vw enumerates columns, so add it by hand. +DROP VIEW IF EXISTS intent_with_inputs_vw; +DROP VIEW IF EXISTS vtxo_vw; + +CREATE VIEW vtxo_vw AS +SELECT v.*, + COALESCE(( + SELECT group_concat(commitment_txid, ',') + FROM vtxo_commitment_txid + WHERE vtxo_txid = v.txid AND vtxo_vout = v.vout + ), '') AS commitments, + ( + EXISTS ( + SELECT 1 FROM swept_marker sm + JOIN json_each(v.markers) j ON j.value = sm.marker_id + ) + OR EXISTS ( + SELECT 1 FROM swept_vtxo sv + WHERE sv.txid = v.txid AND sv.vout = v.vout + ) + ) AS swept, + COALESCE(ap.asset_id, '') AS asset_id, + COALESCE(ap.amount, 0) AS asset_amount +FROM vtxo v +LEFT JOIN ( + SELECT DISTINCT txid, vout, asset_id, amount + FROM asset_projection +) AS ap +ON ap.txid = v.txid AND ap.vout = v.vout; + +CREATE VIEW intent_with_inputs_vw AS +SELECT + v.txid, + v.vout, + v.pubkey, + v.amount, + v.expires_at, + v.created_at, + v.commitment_txid, + v.spent_by, + v.spent, + v.unrolled, + v.preconfirmed, + v.settled_by, + v.ark_txid, + v.intent_id, + v.updated_at, + v.depth, + v.markers, + v.vtxo_kind, + COALESCE(( + SELECT group_concat(vc.commitment_txid) + FROM vtxo_commitment_txid vc + WHERE vc.vtxo_txid = v.txid AND vc.vtxo_vout = v.vout + ), '') AS commitments, + ( + EXISTS ( + SELECT 1 FROM swept_marker sm + JOIN json_each(v.markers) j ON j.value = sm.marker_id + ) + OR EXISTS ( + SELECT 1 FROM swept_vtxo sv + WHERE sv.txid = v.txid AND sv.vout = v.vout + ) + ) AS swept, + COALESCE(ap.asset_id, '') AS asset_id, + COALESCE(ap.amount, 0) AS asset_amount, + intent.id, + intent.round_id, + intent.proof, + intent.message, + intent.txid AS intent_txid +FROM intent +LEFT OUTER JOIN vtxo v ON intent.id = v.intent_id +LEFT JOIN asset_projection ap ON v.txid = ap.txid AND v.vout = ap.vout; diff --git a/internal/infrastructure/db/sqlite/round_repo.go b/internal/infrastructure/db/sqlite/round_repo.go index aa8539cf6..9229152fa 100644 --- a/internal/infrastructure/db/sqlite/round_repo.go +++ b/internal/infrastructure/db/sqlite/round_repo.go @@ -866,6 +866,7 @@ func combinedRowToVtxo(row queries.IntentWithInputsVw) domain.Vtxo { CreatedAt: row.CreatedAt.Int64, ArkTxid: row.ArkTxid.String, SettledBy: row.SettledBy.String, + Kind: domain.VtxoKind(row.VtxoKind.Int64), } } diff --git a/internal/infrastructure/db/sqlite/sqlc/queries/models.go b/internal/infrastructure/db/sqlite/sqlc/queries/models.go index b00a47a4d..accb7f22d 100644 --- a/internal/infrastructure/db/sqlite/sqlc/queries/models.go +++ b/internal/infrastructure/db/sqlite/sqlc/queries/models.go @@ -78,6 +78,7 @@ type IntentWithInputsVw struct { UpdatedAt sql.NullInt64 Depth sql.NullInt64 Markers sql.NullString + VtxoKind sql.NullInt64 Commitments interface{} Swept interface{} AssetID string @@ -280,6 +281,7 @@ type Vtxo struct { UpdatedAt sql.NullInt64 Depth int64 Markers string + VtxoKind int64 } type VtxoCommitmentTxid struct { @@ -306,6 +308,7 @@ type VtxoVw struct { UpdatedAt sql.NullInt64 Depth int64 Markers string + VtxoKind int64 Commitments interface{} Swept interface{} AssetID string diff --git a/internal/infrastructure/db/sqlite/sqlc/queries/query.sql.go b/internal/infrastructure/db/sqlite/sqlc/queries/query.sql.go index 7d6177bc8..4dfc16d05 100644 --- a/internal/infrastructure/db/sqlite/sqlc/queries/query.sql.go +++ b/internal/infrastructure/db/sqlite/sqlc/queries/query.sql.go @@ -163,7 +163,7 @@ func (q *Queries) SelectActiveScriptConvictions(ctx context.Context, arg SelectA } const selectAllVtxos = `-- name: SelectAllVtxos :many -SELECT vtxo_vw.txid, vtxo_vw.vout, vtxo_vw.pubkey, vtxo_vw.amount, vtxo_vw.expires_at, vtxo_vw.created_at, vtxo_vw.commitment_txid, vtxo_vw.spent_by, vtxo_vw.spent, vtxo_vw.unrolled, vtxo_vw.preconfirmed, vtxo_vw.settled_by, vtxo_vw.ark_txid, vtxo_vw.intent_id, vtxo_vw.updated_at, vtxo_vw.depth, vtxo_vw.markers, vtxo_vw.commitments, vtxo_vw.swept, vtxo_vw.asset_id, vtxo_vw.asset_amount FROM vtxo_vw +SELECT vtxo_vw.txid, vtxo_vw.vout, vtxo_vw.pubkey, vtxo_vw.amount, vtxo_vw.expires_at, vtxo_vw.created_at, vtxo_vw.commitment_txid, vtxo_vw.spent_by, vtxo_vw.spent, vtxo_vw.unrolled, vtxo_vw.preconfirmed, vtxo_vw.settled_by, vtxo_vw.ark_txid, vtxo_vw.intent_id, vtxo_vw.updated_at, vtxo_vw.depth, vtxo_vw.markers, vtxo_vw.vtxo_kind, vtxo_vw.commitments, vtxo_vw.swept, vtxo_vw.asset_id, vtxo_vw.asset_amount FROM vtxo_vw ` type SelectAllVtxosRow struct { @@ -197,6 +197,7 @@ func (q *Queries) SelectAllVtxos(ctx context.Context) ([]SelectAllVtxosRow, erro &i.VtxoVw.UpdatedAt, &i.VtxoVw.Depth, &i.VtxoVw.Markers, + &i.VtxoVw.VtxoKind, &i.VtxoVw.Commitments, &i.VtxoVw.Swept, &i.VtxoVw.AssetID, @@ -808,7 +809,7 @@ func (q *Queries) SelectMarkersByIds(ctx context.Context, ids []string) ([]Marke } const selectNotUnrolledVtxos = `-- name: SelectNotUnrolledVtxos :many -SELECT vtxo_vw.txid, vtxo_vw.vout, vtxo_vw.pubkey, vtxo_vw.amount, vtxo_vw.expires_at, vtxo_vw.created_at, vtxo_vw.commitment_txid, vtxo_vw.spent_by, vtxo_vw.spent, vtxo_vw.unrolled, vtxo_vw.preconfirmed, vtxo_vw.settled_by, vtxo_vw.ark_txid, vtxo_vw.intent_id, vtxo_vw.updated_at, vtxo_vw.depth, vtxo_vw.markers, vtxo_vw.commitments, vtxo_vw.swept, vtxo_vw.asset_id, vtxo_vw.asset_amount FROM vtxo_vw WHERE unrolled = false +SELECT vtxo_vw.txid, vtxo_vw.vout, vtxo_vw.pubkey, vtxo_vw.amount, vtxo_vw.expires_at, vtxo_vw.created_at, vtxo_vw.commitment_txid, vtxo_vw.spent_by, vtxo_vw.spent, vtxo_vw.unrolled, vtxo_vw.preconfirmed, vtxo_vw.settled_by, vtxo_vw.ark_txid, vtxo_vw.intent_id, vtxo_vw.updated_at, vtxo_vw.depth, vtxo_vw.markers, vtxo_vw.vtxo_kind, vtxo_vw.commitments, vtxo_vw.swept, vtxo_vw.asset_id, vtxo_vw.asset_amount FROM vtxo_vw WHERE unrolled = false ` type SelectNotUnrolledVtxosRow struct { @@ -842,6 +843,7 @@ func (q *Queries) SelectNotUnrolledVtxos(ctx context.Context) ([]SelectNotUnroll &i.VtxoVw.UpdatedAt, &i.VtxoVw.Depth, &i.VtxoVw.Markers, + &i.VtxoVw.VtxoKind, &i.VtxoVw.Commitments, &i.VtxoVw.Swept, &i.VtxoVw.AssetID, @@ -861,7 +863,7 @@ func (q *Queries) SelectNotUnrolledVtxos(ctx context.Context) ([]SelectNotUnroll } const selectNotUnrolledVtxosWithPubkey = `-- name: SelectNotUnrolledVtxosWithPubkey :many -SELECT vtxo_vw.txid, vtxo_vw.vout, vtxo_vw.pubkey, vtxo_vw.amount, vtxo_vw.expires_at, vtxo_vw.created_at, vtxo_vw.commitment_txid, vtxo_vw.spent_by, vtxo_vw.spent, vtxo_vw.unrolled, vtxo_vw.preconfirmed, vtxo_vw.settled_by, vtxo_vw.ark_txid, vtxo_vw.intent_id, vtxo_vw.updated_at, vtxo_vw.depth, vtxo_vw.markers, vtxo_vw.commitments, vtxo_vw.swept, vtxo_vw.asset_id, vtxo_vw.asset_amount FROM vtxo_vw WHERE unrolled = false AND pubkey = ?1 +SELECT vtxo_vw.txid, vtxo_vw.vout, vtxo_vw.pubkey, vtxo_vw.amount, vtxo_vw.expires_at, vtxo_vw.created_at, vtxo_vw.commitment_txid, vtxo_vw.spent_by, vtxo_vw.spent, vtxo_vw.unrolled, vtxo_vw.preconfirmed, vtxo_vw.settled_by, vtxo_vw.ark_txid, vtxo_vw.intent_id, vtxo_vw.updated_at, vtxo_vw.depth, vtxo_vw.markers, vtxo_vw.vtxo_kind, vtxo_vw.commitments, vtxo_vw.swept, vtxo_vw.asset_id, vtxo_vw.asset_amount FROM vtxo_vw WHERE unrolled = false AND pubkey = ?1 ` type SelectNotUnrolledVtxosWithPubkeyRow struct { @@ -895,6 +897,7 @@ func (q *Queries) SelectNotUnrolledVtxosWithPubkey(ctx context.Context, pubkey s &i.VtxoVw.UpdatedAt, &i.VtxoVw.Depth, &i.VtxoVw.Markers, + &i.VtxoVw.VtxoKind, &i.VtxoVw.Commitments, &i.VtxoVw.Swept, &i.VtxoVw.AssetID, @@ -1083,7 +1086,7 @@ func (q *Queries) SelectOffchainTxsInRange(ctx context.Context, arg SelectOffcha } const selectPendingSpentVtxo = `-- name: SelectPendingSpentVtxo :many -SELECT v.txid, v.vout, v.pubkey, v.amount, v.expires_at, v.created_at, v.commitment_txid, v.spent_by, v.spent, v.unrolled, v.preconfirmed, v.settled_by, v.ark_txid, v.intent_id, v.updated_at, v.depth, v.markers, v.commitments, v.swept, v.asset_id, v.asset_amount +SELECT v.txid, v.vout, v.pubkey, v.amount, v.expires_at, v.created_at, v.commitment_txid, v.spent_by, v.spent, v.unrolled, v.preconfirmed, v.settled_by, v.ark_txid, v.intent_id, v.updated_at, v.depth, v.markers, v.vtxo_kind, v.commitments, v.swept, v.asset_id, v.asset_amount FROM vtxo_vw v WHERE v.txid = ?1 AND v.vout = ?2 AND v.spent = TRUE AND v.unrolled = FALSE AND COALESCE(v.settled_by, '') = '' @@ -1124,6 +1127,7 @@ func (q *Queries) SelectPendingSpentVtxo(ctx context.Context, arg SelectPendingS &i.UpdatedAt, &i.Depth, &i.Markers, + &i.VtxoKind, &i.Commitments, &i.Swept, &i.AssetID, @@ -1143,7 +1147,7 @@ func (q *Queries) SelectPendingSpentVtxo(ctx context.Context, arg SelectPendingS } const selectPendingSpentVtxosWithPubkeys = `-- name: SelectPendingSpentVtxosWithPubkeys :many -SELECT v.txid, v.vout, v.pubkey, v.amount, v.expires_at, v.created_at, v.commitment_txid, v.spent_by, v.spent, v.unrolled, v.preconfirmed, v.settled_by, v.ark_txid, v.intent_id, v.updated_at, v.depth, v.markers, v.commitments, v.swept, v.asset_id, v.asset_amount +SELECT v.txid, v.vout, v.pubkey, v.amount, v.expires_at, v.created_at, v.commitment_txid, v.spent_by, v.spent, v.unrolled, v.preconfirmed, v.settled_by, v.ark_txid, v.intent_id, v.updated_at, v.depth, v.markers, v.vtxo_kind, v.commitments, v.swept, v.asset_id, v.asset_amount FROM vtxo_vw v WHERE v.spent = TRUE AND v.unrolled = FALSE AND COALESCE(v.settled_by, '') = '' AND v.ark_txid IS NOT NULL AND NOT EXISTS ( @@ -1199,6 +1203,7 @@ func (q *Queries) SelectPendingSpentVtxosWithPubkeys(ctx context.Context, arg Se &i.UpdatedAt, &i.Depth, &i.Markers, + &i.VtxoKind, &i.Commitments, &i.Swept, &i.AssetID, @@ -1310,7 +1315,7 @@ SELECT r.ending_timestamp, ( SELECT COALESCE(SUM(amount), 0) FROM ( - SELECT DISTINCT v2.txid, v2.vout, v2.pubkey, v2.amount, v2.expires_at, v2.created_at, v2.commitment_txid, v2.spent_by, v2.spent, v2.unrolled, v2.preconfirmed, v2.settled_by, v2.ark_txid, v2.intent_id, v2.updated_at, v2.depth, v2.markers FROM vtxo v2 JOIN intent i2 ON i2.id = v2.intent_id WHERE i2.round_id = r.id + SELECT DISTINCT v2.txid, v2.vout, v2.pubkey, v2.amount, v2.expires_at, v2.created_at, v2.commitment_txid, v2.spent_by, v2.spent, v2.unrolled, v2.preconfirmed, v2.settled_by, v2.ark_txid, v2.intent_id, v2.updated_at, v2.depth, v2.markers, v2.vtxo_kind FROM vtxo v2 JOIN intent i2 ON i2.id = v2.intent_id WHERE i2.round_id = r.id ) as intent_with_inputs_amount ) AS total_forfeit_amount, ( @@ -1523,7 +1528,7 @@ func (q *Queries) SelectRoundVtxoTree(ctx context.Context, txid string) ([]Tx, e } const selectRoundVtxoTreeLeaves = `-- name: SelectRoundVtxoTreeLeaves :many -SELECT vtxo_vw.txid, vtxo_vw.vout, vtxo_vw.pubkey, vtxo_vw.amount, vtxo_vw.expires_at, vtxo_vw.created_at, vtxo_vw.commitment_txid, vtxo_vw.spent_by, vtxo_vw.spent, vtxo_vw.unrolled, vtxo_vw.preconfirmed, vtxo_vw.settled_by, vtxo_vw.ark_txid, vtxo_vw.intent_id, vtxo_vw.updated_at, vtxo_vw.depth, vtxo_vw.markers, vtxo_vw.commitments, vtxo_vw.swept, vtxo_vw.asset_id, vtxo_vw.asset_amount FROM vtxo_vw WHERE commitment_txid = ?1 AND preconfirmed = false +SELECT vtxo_vw.txid, vtxo_vw.vout, vtxo_vw.pubkey, vtxo_vw.amount, vtxo_vw.expires_at, vtxo_vw.created_at, vtxo_vw.commitment_txid, vtxo_vw.spent_by, vtxo_vw.spent, vtxo_vw.unrolled, vtxo_vw.preconfirmed, vtxo_vw.settled_by, vtxo_vw.ark_txid, vtxo_vw.intent_id, vtxo_vw.updated_at, vtxo_vw.depth, vtxo_vw.markers, vtxo_vw.vtxo_kind, vtxo_vw.commitments, vtxo_vw.swept, vtxo_vw.asset_id, vtxo_vw.asset_amount FROM vtxo_vw WHERE commitment_txid = ?1 AND preconfirmed = false ` type SelectRoundVtxoTreeLeavesRow struct { @@ -1557,6 +1562,7 @@ func (q *Queries) SelectRoundVtxoTreeLeaves(ctx context.Context, commitmentTxid &i.VtxoVw.UpdatedAt, &i.VtxoVw.Depth, &i.VtxoVw.Markers, + &i.VtxoVw.VtxoKind, &i.VtxoVw.Commitments, &i.VtxoVw.Swept, &i.VtxoVw.AssetID, @@ -1898,7 +1904,7 @@ func (q *Queries) SelectSweepableRounds(ctx context.Context) ([]string, error) { } const selectSweepableUnrolledVtxos = `-- name: SelectSweepableUnrolledVtxos :many -SELECT vtxo_vw.txid, vtxo_vw.vout, vtxo_vw.pubkey, vtxo_vw.amount, vtxo_vw.expires_at, vtxo_vw.created_at, vtxo_vw.commitment_txid, vtxo_vw.spent_by, vtxo_vw.spent, vtxo_vw.unrolled, vtxo_vw.preconfirmed, vtxo_vw.settled_by, vtxo_vw.ark_txid, vtxo_vw.intent_id, vtxo_vw.updated_at, vtxo_vw.depth, vtxo_vw.markers, vtxo_vw.commitments, vtxo_vw.swept, vtxo_vw.asset_id, vtxo_vw.asset_amount FROM vtxo_vw WHERE spent = true AND unrolled = true AND swept = false AND (COALESCE(settled_by, '') = '') +SELECT vtxo_vw.txid, vtxo_vw.vout, vtxo_vw.pubkey, vtxo_vw.amount, vtxo_vw.expires_at, vtxo_vw.created_at, vtxo_vw.commitment_txid, vtxo_vw.spent_by, vtxo_vw.spent, vtxo_vw.unrolled, vtxo_vw.preconfirmed, vtxo_vw.settled_by, vtxo_vw.ark_txid, vtxo_vw.intent_id, vtxo_vw.updated_at, vtxo_vw.depth, vtxo_vw.markers, vtxo_vw.vtxo_kind, vtxo_vw.commitments, vtxo_vw.swept, vtxo_vw.asset_id, vtxo_vw.asset_amount FROM vtxo_vw WHERE spent = true AND unrolled = true AND swept = false AND (COALESCE(settled_by, '') = '') ` type SelectSweepableUnrolledVtxosRow struct { @@ -1932,6 +1938,7 @@ func (q *Queries) SelectSweepableUnrolledVtxos(ctx context.Context) ([]SelectSwe &i.VtxoVw.UpdatedAt, &i.VtxoVw.Depth, &i.VtxoVw.Markers, + &i.VtxoVw.VtxoKind, &i.VtxoVw.Commitments, &i.VtxoVw.Swept, &i.VtxoVw.AssetID, @@ -2120,7 +2127,7 @@ func (q *Queries) SelectTxs(ctx context.Context, arg SelectTxsParams) ([]SelectT } const selectVtxo = `-- name: SelectVtxo :many -SELECT vtxo_vw.txid, vtxo_vw.vout, vtxo_vw.pubkey, vtxo_vw.amount, vtxo_vw.expires_at, vtxo_vw.created_at, vtxo_vw.commitment_txid, vtxo_vw.spent_by, vtxo_vw.spent, vtxo_vw.unrolled, vtxo_vw.preconfirmed, vtxo_vw.settled_by, vtxo_vw.ark_txid, vtxo_vw.intent_id, vtxo_vw.updated_at, vtxo_vw.depth, vtxo_vw.markers, vtxo_vw.commitments, vtxo_vw.swept, vtxo_vw.asset_id, vtxo_vw.asset_amount FROM vtxo_vw WHERE txid = ?1 AND vout = ?2 +SELECT vtxo_vw.txid, vtxo_vw.vout, vtxo_vw.pubkey, vtxo_vw.amount, vtxo_vw.expires_at, vtxo_vw.created_at, vtxo_vw.commitment_txid, vtxo_vw.spent_by, vtxo_vw.spent, vtxo_vw.unrolled, vtxo_vw.preconfirmed, vtxo_vw.settled_by, vtxo_vw.ark_txid, vtxo_vw.intent_id, vtxo_vw.updated_at, vtxo_vw.depth, vtxo_vw.markers, vtxo_vw.vtxo_kind, vtxo_vw.commitments, vtxo_vw.swept, vtxo_vw.asset_id, vtxo_vw.asset_amount FROM vtxo_vw WHERE txid = ?1 AND vout = ?2 ` type SelectVtxoParams struct { @@ -2159,6 +2166,7 @@ func (q *Queries) SelectVtxo(ctx context.Context, arg SelectVtxoParams) ([]Selec &i.VtxoVw.UpdatedAt, &i.VtxoVw.Depth, &i.VtxoVw.Markers, + &i.VtxoVw.VtxoKind, &i.VtxoVw.Commitments, &i.VtxoVw.Swept, &i.VtxoVw.AssetID, @@ -2178,7 +2186,7 @@ func (q *Queries) SelectVtxo(ctx context.Context, arg SelectVtxoParams) ([]Selec } const selectVtxoChainByMarker = `-- name: SelectVtxoChainByMarker :many -SELECT vtxo_vw.txid, vtxo_vw.vout, vtxo_vw.pubkey, vtxo_vw.amount, vtxo_vw.expires_at, vtxo_vw.created_at, vtxo_vw.commitment_txid, vtxo_vw.spent_by, vtxo_vw.spent, vtxo_vw.unrolled, vtxo_vw.preconfirmed, vtxo_vw.settled_by, vtxo_vw.ark_txid, vtxo_vw.intent_id, vtxo_vw.updated_at, vtxo_vw.depth, vtxo_vw.markers, vtxo_vw.commitments, vtxo_vw.swept, vtxo_vw.asset_id, vtxo_vw.asset_amount FROM vtxo_vw +SELECT vtxo_vw.txid, vtxo_vw.vout, vtxo_vw.pubkey, vtxo_vw.amount, vtxo_vw.expires_at, vtxo_vw.created_at, vtxo_vw.commitment_txid, vtxo_vw.spent_by, vtxo_vw.spent, vtxo_vw.unrolled, vtxo_vw.preconfirmed, vtxo_vw.settled_by, vtxo_vw.ark_txid, vtxo_vw.intent_id, vtxo_vw.updated_at, vtxo_vw.depth, vtxo_vw.markers, vtxo_vw.vtxo_kind, vtxo_vw.commitments, vtxo_vw.swept, vtxo_vw.asset_id, vtxo_vw.asset_amount FROM vtxo_vw WHERE markers LIKE '%"' || ?1 || '"%' ORDER BY vtxo_vw.depth DESC ` @@ -2217,6 +2225,7 @@ func (q *Queries) SelectVtxoChainByMarker(ctx context.Context, markerID sql.Null &i.VtxoVw.UpdatedAt, &i.VtxoVw.Depth, &i.VtxoVw.Markers, + &i.VtxoVw.VtxoKind, &i.VtxoVw.Commitments, &i.VtxoVw.Swept, &i.VtxoVw.AssetID, @@ -2236,7 +2245,7 @@ func (q *Queries) SelectVtxoChainByMarker(ctx context.Context, markerID sql.Null } const selectVtxoInputsByRoundId = `-- name: SelectVtxoInputsByRoundId :many -SELECT intent_with_inputs_vw.txid, intent_with_inputs_vw.vout, intent_with_inputs_vw.pubkey, intent_with_inputs_vw.amount, intent_with_inputs_vw.expires_at, intent_with_inputs_vw.created_at, intent_with_inputs_vw.commitment_txid, intent_with_inputs_vw.spent_by, intent_with_inputs_vw.spent, intent_with_inputs_vw.unrolled, intent_with_inputs_vw.preconfirmed, intent_with_inputs_vw.settled_by, intent_with_inputs_vw.ark_txid, intent_with_inputs_vw.intent_id, intent_with_inputs_vw.updated_at, intent_with_inputs_vw.depth, intent_with_inputs_vw.markers, intent_with_inputs_vw.commitments, intent_with_inputs_vw.swept, intent_with_inputs_vw.asset_id, intent_with_inputs_vw.asset_amount, intent_with_inputs_vw.id, intent_with_inputs_vw.round_id, intent_with_inputs_vw.proof, intent_with_inputs_vw.message, intent_with_inputs_vw.intent_txid +SELECT intent_with_inputs_vw.txid, intent_with_inputs_vw.vout, intent_with_inputs_vw.pubkey, intent_with_inputs_vw.amount, intent_with_inputs_vw.expires_at, intent_with_inputs_vw.created_at, intent_with_inputs_vw.commitment_txid, intent_with_inputs_vw.spent_by, intent_with_inputs_vw.spent, intent_with_inputs_vw.unrolled, intent_with_inputs_vw.preconfirmed, intent_with_inputs_vw.settled_by, intent_with_inputs_vw.ark_txid, intent_with_inputs_vw.intent_id, intent_with_inputs_vw.updated_at, intent_with_inputs_vw.depth, intent_with_inputs_vw.markers, intent_with_inputs_vw.vtxo_kind, intent_with_inputs_vw.commitments, intent_with_inputs_vw.swept, intent_with_inputs_vw.asset_id, intent_with_inputs_vw.asset_amount, intent_with_inputs_vw.id, intent_with_inputs_vw.round_id, intent_with_inputs_vw.proof, intent_with_inputs_vw.message, intent_with_inputs_vw.intent_txid FROM intent_with_inputs_vw WHERE intent_with_inputs_vw.round_id = ?1 ` @@ -2272,6 +2281,7 @@ func (q *Queries) SelectVtxoInputsByRoundId(ctx context.Context, roundID sql.Nul &i.IntentWithInputsVw.UpdatedAt, &i.IntentWithInputsVw.Depth, &i.IntentWithInputsVw.Markers, + &i.IntentWithInputsVw.VtxoKind, &i.IntentWithInputsVw.Commitments, &i.IntentWithInputsVw.Swept, &i.IntentWithInputsVw.AssetID, @@ -2404,7 +2414,7 @@ func (q *Queries) SelectVtxoPubKeysByCommitmentTxids(ctx context.Context, arg Se } const selectVtxosByArkTxid = `-- name: SelectVtxosByArkTxid :many -SELECT vtxo_vw.txid, vtxo_vw.vout, vtxo_vw.pubkey, vtxo_vw.amount, vtxo_vw.expires_at, vtxo_vw.created_at, vtxo_vw.commitment_txid, vtxo_vw.spent_by, vtxo_vw.spent, vtxo_vw.unrolled, vtxo_vw.preconfirmed, vtxo_vw.settled_by, vtxo_vw.ark_txid, vtxo_vw.intent_id, vtxo_vw.updated_at, vtxo_vw.depth, vtxo_vw.markers, vtxo_vw.commitments, vtxo_vw.swept, vtxo_vw.asset_id, vtxo_vw.asset_amount FROM vtxo_vw WHERE ark_txid = ?1 +SELECT vtxo_vw.txid, vtxo_vw.vout, vtxo_vw.pubkey, vtxo_vw.amount, vtxo_vw.expires_at, vtxo_vw.created_at, vtxo_vw.commitment_txid, vtxo_vw.spent_by, vtxo_vw.spent, vtxo_vw.unrolled, vtxo_vw.preconfirmed, vtxo_vw.settled_by, vtxo_vw.ark_txid, vtxo_vw.intent_id, vtxo_vw.updated_at, vtxo_vw.depth, vtxo_vw.markers, vtxo_vw.vtxo_kind, vtxo_vw.commitments, vtxo_vw.swept, vtxo_vw.asset_id, vtxo_vw.asset_amount FROM vtxo_vw WHERE ark_txid = ?1 ` type SelectVtxosByArkTxidRow struct { @@ -2439,6 +2449,7 @@ func (q *Queries) SelectVtxosByArkTxid(ctx context.Context, arkTxid sql.NullStri &i.VtxoVw.UpdatedAt, &i.VtxoVw.Depth, &i.VtxoVw.Markers, + &i.VtxoVw.VtxoKind, &i.VtxoVw.Commitments, &i.VtxoVw.Swept, &i.VtxoVw.AssetID, @@ -2459,7 +2470,7 @@ func (q *Queries) SelectVtxosByArkTxid(ctx context.Context, arkTxid sql.NullStri const selectVtxosByDepthRange = `-- name: SelectVtxosByDepthRange :many -SELECT vtxo_vw.txid, vtxo_vw.vout, vtxo_vw.pubkey, vtxo_vw.amount, vtxo_vw.expires_at, vtxo_vw.created_at, vtxo_vw.commitment_txid, vtxo_vw.spent_by, vtxo_vw.spent, vtxo_vw.unrolled, vtxo_vw.preconfirmed, vtxo_vw.settled_by, vtxo_vw.ark_txid, vtxo_vw.intent_id, vtxo_vw.updated_at, vtxo_vw.depth, vtxo_vw.markers, vtxo_vw.commitments, vtxo_vw.swept, vtxo_vw.asset_id, vtxo_vw.asset_amount FROM vtxo_vw +SELECT vtxo_vw.txid, vtxo_vw.vout, vtxo_vw.pubkey, vtxo_vw.amount, vtxo_vw.expires_at, vtxo_vw.created_at, vtxo_vw.commitment_txid, vtxo_vw.spent_by, vtxo_vw.spent, vtxo_vw.unrolled, vtxo_vw.preconfirmed, vtxo_vw.settled_by, vtxo_vw.ark_txid, vtxo_vw.intent_id, vtxo_vw.updated_at, vtxo_vw.depth, vtxo_vw.markers, vtxo_vw.vtxo_kind, vtxo_vw.commitments, vtxo_vw.swept, vtxo_vw.asset_id, vtxo_vw.asset_amount FROM vtxo_vw WHERE depth >= ?1 AND depth <= ?2 ORDER BY depth DESC ` @@ -2502,6 +2513,7 @@ func (q *Queries) SelectVtxosByDepthRange(ctx context.Context, arg SelectVtxosBy &i.VtxoVw.UpdatedAt, &i.VtxoVw.Depth, &i.VtxoVw.Markers, + &i.VtxoVw.VtxoKind, &i.VtxoVw.Commitments, &i.VtxoVw.Swept, &i.VtxoVw.AssetID, @@ -2521,7 +2533,7 @@ func (q *Queries) SelectVtxosByDepthRange(ctx context.Context, arg SelectVtxosBy } const selectVtxosByMarkerId = `-- name: SelectVtxosByMarkerId :many -SELECT vtxo_vw.txid, vtxo_vw.vout, vtxo_vw.pubkey, vtxo_vw.amount, vtxo_vw.expires_at, vtxo_vw.created_at, vtxo_vw.commitment_txid, vtxo_vw.spent_by, vtxo_vw.spent, vtxo_vw.unrolled, vtxo_vw.preconfirmed, vtxo_vw.settled_by, vtxo_vw.ark_txid, vtxo_vw.intent_id, vtxo_vw.updated_at, vtxo_vw.depth, vtxo_vw.markers, vtxo_vw.commitments, vtxo_vw.swept, vtxo_vw.asset_id, vtxo_vw.asset_amount FROM vtxo_vw WHERE markers LIKE '%"' || ?1 || '"%' +SELECT vtxo_vw.txid, vtxo_vw.vout, vtxo_vw.pubkey, vtxo_vw.amount, vtxo_vw.expires_at, vtxo_vw.created_at, vtxo_vw.commitment_txid, vtxo_vw.spent_by, vtxo_vw.spent, vtxo_vw.unrolled, vtxo_vw.preconfirmed, vtxo_vw.settled_by, vtxo_vw.ark_txid, vtxo_vw.intent_id, vtxo_vw.updated_at, vtxo_vw.depth, vtxo_vw.markers, vtxo_vw.vtxo_kind, vtxo_vw.commitments, vtxo_vw.swept, vtxo_vw.asset_id, vtxo_vw.asset_amount FROM vtxo_vw WHERE markers LIKE '%"' || ?1 || '"%' ` type SelectVtxosByMarkerIdRow struct { @@ -2558,6 +2570,7 @@ func (q *Queries) SelectVtxosByMarkerId(ctx context.Context, markerID sql.NullSt &i.VtxoVw.UpdatedAt, &i.VtxoVw.Depth, &i.VtxoVw.Markers, + &i.VtxoVw.VtxoKind, &i.VtxoVw.Commitments, &i.VtxoVw.Swept, &i.VtxoVw.AssetID, @@ -2646,7 +2659,7 @@ func (q *Queries) SelectVtxosOutpointsByArkTxidRecursive(ctx context.Context, ar } const selectVtxosWithPubkeys = `-- name: SelectVtxosWithPubkeys :many -SELECT vtxo_vw.txid, vtxo_vw.vout, vtxo_vw.pubkey, vtxo_vw.amount, vtxo_vw.expires_at, vtxo_vw.created_at, vtxo_vw.commitment_txid, vtxo_vw.spent_by, vtxo_vw.spent, vtxo_vw.unrolled, vtxo_vw.preconfirmed, vtxo_vw.settled_by, vtxo_vw.ark_txid, vtxo_vw.intent_id, vtxo_vw.updated_at, vtxo_vw.depth, vtxo_vw.markers, vtxo_vw.commitments, vtxo_vw.swept, vtxo_vw.asset_id, vtxo_vw.asset_amount FROM vtxo_vw WHERE updated_at >= ?1 +SELECT vtxo_vw.txid, vtxo_vw.vout, vtxo_vw.pubkey, vtxo_vw.amount, vtxo_vw.expires_at, vtxo_vw.created_at, vtxo_vw.commitment_txid, vtxo_vw.spent_by, vtxo_vw.spent, vtxo_vw.unrolled, vtxo_vw.preconfirmed, vtxo_vw.settled_by, vtxo_vw.ark_txid, vtxo_vw.intent_id, vtxo_vw.updated_at, vtxo_vw.depth, vtxo_vw.markers, vtxo_vw.vtxo_kind, vtxo_vw.commitments, vtxo_vw.swept, vtxo_vw.asset_id, vtxo_vw.asset_amount FROM vtxo_vw WHERE updated_at >= ?1 AND (CAST(?2 AS INTEGER) = 0 OR updated_at <= CAST(?2 AS INTEGER)) AND pubkey IN (/*SLICE:pubkeys*/?) ` @@ -2700,6 +2713,7 @@ func (q *Queries) SelectVtxosWithPubkeys(ctx context.Context, arg SelectVtxosWit &i.VtxoVw.UpdatedAt, &i.VtxoVw.Depth, &i.VtxoVw.Markers, + &i.VtxoVw.VtxoKind, &i.VtxoVw.Commitments, &i.VtxoVw.Swept, &i.VtxoVw.AssetID, @@ -3261,11 +3275,11 @@ func (q *Queries) UpsertTx(ctx context.Context, arg UpsertTxParams) error { const upsertVtxo = `-- name: UpsertVtxo :exec INSERT INTO vtxo ( txid, vout, pubkey, amount, commitment_txid, settled_by, ark_txid, - spent_by, spent, unrolled, preconfirmed, expires_at, created_at, updated_at, depth, markers + spent_by, spent, unrolled, preconfirmed, expires_at, created_at, updated_at, depth, markers, vtxo_kind ) VALUES ( ?1, ?2, ?3, ?4, ?5, ?6, ?7, - ?8, ?9, ?10, ?11, ?12, ?13, (CAST((strftime('%s','now') || substr(strftime('%f','now'),4,3)) AS INTEGER)), ?14, ?15 + ?8, ?9, ?10, ?11, ?12, ?13, (CAST((strftime('%s','now') || substr(strftime('%f','now'),4,3)) AS INTEGER)), ?14, ?15, ?16 ) ON CONFLICT(txid, vout) DO UPDATE SET pubkey = EXCLUDED.pubkey, amount = EXCLUDED.amount, @@ -3280,7 +3294,8 @@ VALUES ( created_at = EXCLUDED.created_at, updated_at = (CAST((strftime('%s','now') || substr(strftime('%f','now'),4,3)) AS INTEGER)), depth = EXCLUDED.depth, - markers = EXCLUDED.markers + markers = EXCLUDED.markers, + vtxo_kind = EXCLUDED.vtxo_kind ` type UpsertVtxoParams struct { @@ -3299,6 +3314,7 @@ type UpsertVtxoParams struct { CreatedAt int64 Depth int64 Markers string + VtxoKind int64 } func (q *Queries) UpsertVtxo(ctx context.Context, arg UpsertVtxoParams) error { @@ -3318,6 +3334,7 @@ func (q *Queries) UpsertVtxo(ctx context.Context, arg UpsertVtxoParams) error { arg.CreatedAt, arg.Depth, arg.Markers, + arg.VtxoKind, ) return err } diff --git a/internal/infrastructure/db/sqlite/sqlc/query.sql b/internal/infrastructure/db/sqlite/sqlc/query.sql index 5628e43fd..59a3d881f 100644 --- a/internal/infrastructure/db/sqlite/sqlc/query.sql +++ b/internal/infrastructure/db/sqlite/sqlc/query.sql @@ -49,11 +49,11 @@ ON CONFLICT(intent_id, pubkey, onchain_address) DO UPDATE SET -- name: UpsertVtxo :exec INSERT INTO vtxo ( txid, vout, pubkey, amount, commitment_txid, settled_by, ark_txid, - spent_by, spent, unrolled, preconfirmed, expires_at, created_at, updated_at, depth, markers + spent_by, spent, unrolled, preconfirmed, expires_at, created_at, updated_at, depth, markers, vtxo_kind ) VALUES ( @txid, @vout, @pubkey, @amount, @commitment_txid, @settled_by, @ark_txid, - @spent_by, @spent, @unrolled, @preconfirmed, @expires_at, @created_at, (CAST((strftime('%s','now') || substr(strftime('%f','now'),4,3)) AS INTEGER)), @depth, @markers + @spent_by, @spent, @unrolled, @preconfirmed, @expires_at, @created_at, (CAST((strftime('%s','now') || substr(strftime('%f','now'),4,3)) AS INTEGER)), @depth, @markers, @vtxo_kind ) ON CONFLICT(txid, vout) DO UPDATE SET pubkey = EXCLUDED.pubkey, amount = EXCLUDED.amount, @@ -68,7 +68,8 @@ VALUES ( created_at = EXCLUDED.created_at, updated_at = (CAST((strftime('%s','now') || substr(strftime('%f','now'),4,3)) AS INTEGER)), depth = EXCLUDED.depth, - markers = EXCLUDED.markers; + markers = EXCLUDED.markers, + vtxo_kind = EXCLUDED.vtxo_kind; -- name: InsertVtxoCommitmentTxid :exec INSERT INTO vtxo_commitment_txid (vtxo_txid, vtxo_vout, commitment_txid) diff --git a/internal/infrastructure/db/sqlite/vtxo_repo.go b/internal/infrastructure/db/sqlite/vtxo_repo.go index 8b9ae98ab..3a4e6ac73 100644 --- a/internal/infrastructure/db/sqlite/vtxo_repo.go +++ b/internal/infrastructure/db/sqlite/vtxo_repo.go @@ -76,8 +76,9 @@ func (v *vtxoRepository) AddVtxos(ctx context.Context, vtxos []domain.Vtxo) erro String: vtxo.SettledBy, Valid: len(vtxo.SettledBy) > 0, }, - Depth: int64(vtxo.Depth), - Markers: markersJSON, + Depth: int64(vtxo.Depth), + Markers: markersJSON, + VtxoKind: int64(vtxo.Kind), }, ); err != nil { return err @@ -736,6 +737,7 @@ func rowToVtxo(row queries.VtxoVw) domain.Vtxo { Depth: uint32(row.Depth), MarkerIDs: parseMarkersJSONFromVtxo(row.Markers), Assets: assets, + Kind: domain.VtxoKind(row.VtxoKind), } } diff --git a/internal/infrastructure/db/vtxo_kind_down_test.go b/internal/infrastructure/db/vtxo_kind_down_test.go new file mode 100644 index 000000000..1db70d9bf --- /dev/null +++ b/internal/infrastructure/db/vtxo_kind_down_test.go @@ -0,0 +1,176 @@ +package db_test + +import ( + "database/sql" + "embed" + "fmt" + "testing" + + pgdb "github.com/arkade-os/arkd/internal/infrastructure/db/postgres" + "github.com/golang-migrate/migrate/v4" + migratepg "github.com/golang-migrate/migrate/v4/database/postgres" + "github.com/golang-migrate/migrate/v4/source/iofs" + "github.com/stretchr/testify/require" +) + +const ( + addVtxoKindMigrationVersion = 20260901000000 + // addVtxoKindPostgresDsn names a database this test owns on the same test + // server TestService uses, so the two never share state. + addVtxoKindPostgresDsn = "postgresql://root:secret@127.0.0.1:5432/vtxo_kind_migration?sslmode=disable" +) + +// TestAddVtxoKindDownMigration verifies the add_vtxo_kind migration is +// reversible on both SQL backends: up adds the vtxo_kind column and surfaces +// it through vtxo_vw, down drops the column and recreates the views without +// it, and a re-apply works cleanly. +func TestAddVtxoKindDownMigration(t *testing.T) { + t.Run("sqlite", func(t *testing.T) { + m, db := newSweptVtxoMigrator(t) + t.Cleanup(func() { + //nolint:errcheck + db.Close() + }) + testAddVtxoKindDownMigration(t, m, sqliteSchema{db}) + }) + + t.Run("postgres", func(t *testing.T) { + m, db := newVtxoKindPostgresMigrator(t) + t.Cleanup(func() { + //nolint:errcheck + db.Close() + }) + testAddVtxoKindDownMigration(t, m, postgresSchema{db}) + }) +} + +func testAddVtxoKindDownMigration(t *testing.T, m *migrate.Migrate, s schema) { + require.NoError(t, m.Migrate(addVtxoKindMigrationVersion)) + + // Up: vtxo_kind exists on the base table and is visible through vtxo_vw. + require.True(t, s.hasColumn(t, "vtxo", "vtxo_kind"), + "vtxo.vtxo_kind should exist after the up migration") + require.True(t, s.hasColumn(t, "vtxo_vw", "vtxo_kind"), + "vtxo_vw should expose vtxo_kind after the up migration") + + // Down one step reverses add_vtxo_kind. + require.NoError(t, m.Steps(-1), "down migration must succeed") + require.False(t, s.hasColumn(t, "vtxo", "vtxo_kind"), + "vtxo.vtxo_kind should be gone after the down migration") + require.False(t, s.hasColumn(t, "vtxo_vw", "vtxo_kind"), + "vtxo_vw should not expose vtxo_kind after the down migration") + require.True(t, s.viewExists(t, "vtxo_vw"), + "vtxo_vw should be recreated by the down migration") + + // Re-applying forward must succeed. + require.NoError(t, m.Steps(1), "re-applying the up migration must succeed") + require.True(t, s.hasColumn(t, "vtxo", "vtxo_kind"), + "vtxo.vtxo_kind should exist again after re-applying") +} + +// --- helpers --- + +//go:embed postgres/migration/* +var vtxoKindPostgresMigrations embed.FS + +// schema inspects a backend's catalog for the columns and views the test +// asserts on. +type schema interface { + hasColumn(t *testing.T, table, column string) bool + viewExists(t *testing.T, name string) bool +} + +// newVtxoKindPostgresMigrator opens the test's own postgres database, creating +// it if needed, wipes whatever a previous run left in it, and returns a +// migrate.Migrate bound to the embedded postgres migration source. +func newVtxoKindPostgresMigrator(t *testing.T) (*migrate.Migrate, *sql.DB) { + t.Helper() + db, err := pgdb.OpenDb(addVtxoKindPostgresDsn, true) + require.NoError(t, err) + + newMigrator := func() *migrate.Migrate { + driver, err := migratepg.WithInstance(db, &migratepg.Config{}) + require.NoError(t, err) + source, err := iofs.New(vtxoKindPostgresMigrations, "postgres/migration") + require.NoError(t, err) + m, err := migrate.NewWithInstance("iofs", source, "postgres", driver) + require.NoError(t, err) + return m + } + + // Drop invalidates the instance it ran on, so build a fresh one after it. + require.NoError(t, newMigrator().Drop()) + return newMigrator(), db +} + +type sqliteSchema struct{ db *sql.DB } + +func (s sqliteSchema) hasColumn(t *testing.T, table, column string) bool { + return hasColumn(t, s.db, table, column) +} + +func (s sqliteSchema) viewExists(t *testing.T, name string) bool { + return viewExists(t, s.db, name) +} + +type postgresSchema struct{ db *sql.DB } + +// hasColumn reads information_schema.columns, which lists view columns as +// well as table columns. +func (s postgresSchema) hasColumn(t *testing.T, table, column string) bool { + t.Helper() + var n int + require.NoError(t, s.db.QueryRow( + `SELECT count(*) FROM information_schema.columns + WHERE table_schema = current_schema() AND table_name = $1 AND column_name = $2`, + table, column, + ).Scan(&n)) + return n > 0 +} + +func (s postgresSchema) viewExists(t *testing.T, name string) bool { + t.Helper() + var n int + require.NoError(t, s.db.QueryRow( + `SELECT count(*) FROM information_schema.views + WHERE table_schema = current_schema() AND table_name = $1`, name, + ).Scan(&n)) + return n > 0 +} + +// hasColumn reports whether the given table or view exposes a column, via +// sqlite's PRAGMA table_info (which works for views too). +func hasColumn(t *testing.T, db *sql.DB, table, column string) bool { + t.Helper() + rows, err := db.Query(fmt.Sprintf("PRAGMA table_info(%s)", table)) + require.NoError(t, err) + defer rows.Close() + for rows.Next() { + var ( + cid int + name, ctype string + notnull, pk int + dflt sql.NullString + ) + require.NoError(t, rows.Scan(&cid, &name, &ctype, ¬null, &dflt, &pk)) + if name == column { + return true + } + } + require.NoError(t, rows.Err()) + return false +} + +// viewExists reports whether a view of the given name exists. +func viewExists(t *testing.T, db *sql.DB, name string) bool { + t.Helper() + var got string + err := db.QueryRow( + "SELECT name FROM sqlite_master WHERE type='view' AND name=?", name, + ).Scan(&got) + if err == sql.ErrNoRows { + return false + } + require.NoError(t, err) + return got == name +}