diff --git a/internal/core/application/onchain_spend.go b/internal/core/application/onchain_spend.go index 79d38be2f..9a2bcfa1d 100644 --- a/internal/core/application/onchain_spend.go +++ b/internal/core/application/onchain_spend.go @@ -67,10 +67,11 @@ func (s *service) applyOnchainSpends(ctx context.Context, spends []ports.Spend) spentBy := make(map[domain.Outpoint]string) for _, vtxo := range vtxos { - // Only an unrolled vtxo has an onchain output to spend. A vtxo already - // spent inside the Ark is left alone, since MarkVtxosOnchainSpent would ignore - // it anyway, and filtering here keeps the log honest. - if !vtxo.Unrolled { + // Only a vtxo with an onchain output, unrolled or onchain-kind, can be + // spent onchain. A vtxo already spent inside the Ark is left alone, since + // MarkVtxosOnchainSpent would ignore it anyway, and filtering here keeps + // the log honest. + if !vtxo.HasOnchainOutput() { continue } if vtxo.Spent && !vtxo.IsOnchainSpent() { diff --git a/internal/core/application/onchain_spend_test.go b/internal/core/application/onchain_spend_test.go index 7a28736c0..4388b8b16 100644 --- a/internal/core/application/onchain_spend_test.go +++ b/internal/core/application/onchain_spend_test.go @@ -60,6 +60,24 @@ func TestOnchainSpends(t *testing.T) { ) }) + // An on-chain Arkade UTXO is never unrolled but has an onchain output all + // the same, so its spend is recorded like an unrolled vtxo's. + t.Run("records an onchain-kind vtxo spent onchain", func(t *testing.T) { + svc, vtxos := newService(t, []domain.Vtxo{ + {Outpoint: out, Kind: domain.VtxoKindOnchain}, + }) + vtxos.On("MarkVtxosOnchainSpent", mock.Anything, mock.Anything).Return(nil) + + require.NoError(t, svc.applyOnchainSpends( + context.Background(), []ports.Spend{spendOf(out, spendingTxid, 1)}, + )) + + vtxos.AssertCalled( + t, "MarkVtxosOnchainSpent", mock.Anything, + map[domain.Outpoint]string{out: spendingTxid}, + ) + }) + // The wallet watches boarding scripts as well as vtxo scripts, so most // notified spends refer to outputs that are not unrolled vtxos at all. t.Run("ignores a vtxo that was never unrolled", func(t *testing.T) { diff --git a/internal/core/application/service.go b/internal/core/application/service.go index 35a75841d..2e83713f1 100644 --- a/internal/core/application/service.go +++ b/internal/core/application/service.go @@ -4143,9 +4143,10 @@ func (s *service) restoreWatchingVtxos() error { addKey(key) } - // Unrolled vtxos are watched independently of any round. Their batch may - // no longer be sweepable, so the loop above would not restore them, and an - // unwatched script is invisible to onchain spend tracking twice over. No + // Unrolled and onchain-kind vtxos are watched independently of any round. + // An unrolled vtxo's batch may no longer be sweepable, and an onchain-kind + // vtxo has no batch at all, so the loop above would not restore them, and + // an unwatched script is invisible to onchain spend tracking twice over. No // push notification arrives, and NBXplorer only records the matched inputs // the reconciler reads for sources it was tracking when it indexed the // spending transaction. Both directions are restored. Still-unspent vtxos @@ -4158,7 +4159,7 @@ func (s *service) restoreWatchingVtxos() error { } { vtxos, err := load(ctx) if err != nil { - log.WithError(err).Warn("failed to fetch unrolled vtxos for restore") + log.WithError(err).Warn("failed to fetch onchain vtxos for restore") continue } for _, vtxo := range vtxos { diff --git a/internal/core/domain/vtxo.go b/internal/core/domain/vtxo.go index 8a26181a5..bff63a1d7 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. 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,24 +86,36 @@ 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 { return v.SettledBy != "" } -// IsOnchainSpent reports a vtxo that was unrolled and then spent onchain, -// outside the Ark. There is no dedicated column. An in-Ark spend always sets -// either ArkTxid (SpendVtxos, on an accepted offchain tx) or SettledBy -// (SettleVtxos, at batch settlement), so their absence on a spent and unrolled -// vtxo is what identifies the spend as onchain. +// HasOnchainOutput reports a vtxo with an output that can be spent onchain, +// outside the Ark, either one that was unrolled or one held in an on-chain +// Arkade UTXO. Those are the vtxos the onchain spend tracking watches. +func (v Vtxo) HasOnchainOutput() bool { + return v.Unrolled || v.Kind == VtxoKindOnchain +} + +// IsOnchainSpent reports a vtxo with an onchain output that was then spent +// onchain, outside the Ark. There is no dedicated column. An in-Ark spend +// always sets either ArkTxid (SpendVtxos, on an accepted offchain tx) or +// SettledBy (SettleVtxos, at batch settlement), so their absence on a spent +// vtxo with an onchain output is what identifies the spend as onchain. func (v Vtxo) IsOnchainSpent() bool { - return v.Unrolled && v.Spent && v.SettledBy == "" && v.ArkTxid == "" + return v.HasOnchainOutput() && v.Spent && v.SettledBy == "" && v.ArkTxid == "" } func (v Vtxo) TapKey() (*btcec.PublicKey, error) { @@ -111,5 +135,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_repo.go b/internal/core/domain/vtxo_repo.go index f87745203..d41df30a2 100644 --- a/internal/core/domain/vtxo_repo.go +++ b/internal/core/domain/vtxo_repo.go @@ -7,8 +7,9 @@ type VtxoRepository interface { SettleVtxos(ctx context.Context, spentVtxos map[Outpoint]string, commitmentTxid string) error SpendVtxos(ctx context.Context, spentVtxos map[Outpoint]string, arkTxid string) error UnrollVtxos(ctx context.Context, outpoints []Outpoint) error - // MarkVtxosOnchainSpent records unrolled vtxos spent onchain, outside the - // Ark, mapping each outpoint to the txid that spent it. It also re-points an + // MarkVtxosOnchainSpent records vtxos with an onchain output, unrolled or + // onchain-kind, spent onchain outside the Ark, mapping each outpoint to the + // txid that spent it. It also re-points an // already onchain-spent vtxo at a new spender, so an RBF replacement is // picked up. It never touches a vtxo spent offchain or settled in a batch. MarkVtxosOnchainSpent(ctx context.Context, spentBy map[Outpoint]string) error @@ -19,8 +20,9 @@ type VtxoRepository interface { GetVtxos(ctx context.Context, outpoints []Outpoint) ([]Vtxo, error) GetAllNonUnrolledVtxos(ctx context.Context, pubkey string) ([]Vtxo, []Vtxo, error) GetAllSweepableUnrolledVtxos(ctx context.Context) ([]Vtxo, error) - // GetUnrolledUnspentVtxos returns unrolled vtxos currently believed unspent, - // the candidate set the onchain-spend reconciler checks against the chain. + // GetUnrolledUnspentVtxos returns the vtxos with an onchain output, unrolled + // or onchain-kind, currently believed unspent, the candidate set the + // onchain-spend reconciler checks against the chain. GetUnrolledUnspentVtxos(ctx context.Context) ([]Vtxo, error) // GetOnchainSpentVtxos returns vtxos currently recorded as spent onchain, so // the reconciler can re-point or retract them. diff --git a/internal/core/domain/vtxo_test.go b/internal/core/domain/vtxo_test.go index 7170f4993..63f9ca889 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) { @@ -217,6 +248,23 @@ func TestVtxo_IsOnchainSpent(t *testing.T) { vtxo: domain.Vtxo{Unrolled: true, Spent: true, SettledBy: "commitmenttxid"}, expected: false, }, + { + name: "true (onchain kind, spent onchain)", + vtxo: domain.Vtxo{Kind: domain.VtxoKindOnchain, Spent: true}, + expected: true, + }, + { + name: "false (onchain kind, not spent)", + vtxo: domain.Vtxo{Kind: domain.VtxoKindOnchain}, + expected: false, + }, + { + name: "false (onchain kind, settled in a batch)", + vtxo: domain.Vtxo{ + Kind: domain.VtxoKindOnchain, Spent: true, SettledBy: "commitmenttxid", + }, + expected: false, + }, } for _, f := range fixtures { t.Run(f.name, func(t *testing.T) { diff --git a/internal/infrastructure/db/badger/vtxo_repo.go b/internal/infrastructure/db/badger/vtxo_repo.go index 4c9bdd8ad..350403ca3 100644 --- a/internal/infrastructure/db/badger/vtxo_repo.go +++ b/internal/infrastructure/db/badger/vtxo_repo.go @@ -266,12 +266,20 @@ func (r *VtxoRepository) GetAllSweepableUnrolledVtxos( func (r *VtxoRepository) GetUnrolledUnspentVtxos( ctx context.Context, ) ([]domain.Vtxo, error) { + // An onchain-kind vtxo has an onchain output without being unrolled, so + // both shapes are candidates. query := badgerhold.Where("Unrolled"). Eq(true). And("Spent"). Eq(false). And("Swept"). - Eq(false) + Eq(false). + Or(badgerhold.Where("Kind"). + Eq(domain.VtxoKindOnchain). + And("Spent"). + Eq(false). + And("Swept"). + Eq(false)) return r.findVtxos(ctx, query) } @@ -285,7 +293,15 @@ func (r *VtxoRepository) GetOnchainSpentVtxos( And("SettledBy"). Eq(""). And("ArkTxid"). - Eq("") + Eq(""). + Or(badgerhold.Where("Kind"). + Eq(domain.VtxoKindOnchain). + And("Spent"). + Eq(true). + And("SettledBy"). + Eq(""). + And("ArkTxid"). + Eq("")) return r.findVtxos(ctx, query) } @@ -821,7 +837,7 @@ func (r *VtxoRepository) markOnchainSpentVtxo( tx *badger.Txn, outpoint domain.Outpoint, spendingTxid string, ) error { vtxo, err := r.getVtxoTx(tx, outpoint) - if err != nil || vtxo == nil || !vtxo.Unrolled { + if err != nil || vtxo == nil || !vtxo.HasOnchainOutput() { return err } if vtxo.Spent && !vtxo.IsOnchainSpent() { diff --git a/internal/infrastructure/db/onchain_spend_repo_test.go b/internal/infrastructure/db/onchain_spend_repo_test.go index c88d591bb..9f9f2a283 100644 --- a/internal/infrastructure/db/onchain_spend_repo_test.go +++ b/internal/infrastructure/db/onchain_spend_repo_test.go @@ -95,6 +95,74 @@ func TestOnchainSpendRepository(t *testing.T) { require.Empty(t, got.SpentBy) }) + // An on-chain Arkade UTXO has an onchain output without ever being + // unrolled, so the same mark, re-point and retract apply to it. + t.Run("marks, re-points and retracts an onchain-kind vtxo", func(t *testing.T) { + vtxo := onchainKindVtxo(randomString(32)) + require.NoError(t, repo.AddVtxos(ctx, []domain.Vtxo{vtxo})) + + require.NoError(t, repo.MarkVtxosOnchainSpent( + ctx, map[domain.Outpoint]string{vtxo.Outpoint: "spendingtxid"}, + )) + got := getOnchainSpendVtxo(t, repo, vtxo.Outpoint) + require.True(t, got.Spent) + require.Equal(t, "spendingtxid", got.SpentBy) + require.True(t, got.IsOnchainSpent()) + require.Equal(t, domain.VtxoKindOnchain, got.Kind) + + require.NoError(t, repo.MarkVtxosOnchainSpent( + ctx, map[domain.Outpoint]string{vtxo.Outpoint: "replacementtxid"}, + )) + got = getOnchainSpendVtxo(t, repo, vtxo.Outpoint) + require.Equal(t, "replacementtxid", got.SpentBy) + + require.NoError(t, repo.UnmarkVtxosOnchainSpent( + ctx, []domain.Outpoint{vtxo.Outpoint}, + )) + got = getOnchainSpendVtxo(t, repo, vtxo.Outpoint) + require.False(t, got.Spent) + require.Empty(t, got.SpentBy) + require.False(t, got.Unrolled, "an onchain-kind vtxo is never unrolled") + require.Equal(t, domain.VtxoKindOnchain, got.Kind) + }) + + t.Run("selectors partition by kind as by unrolled", func(t *testing.T) { + unspent := onchainKindVtxo(randomString(32)) + spent := onchainKindVtxo(randomString(32)) + offchain := onchainSpendVtxo(randomString(32)) + require.NoError(t, repo.AddVtxos(ctx, []domain.Vtxo{unspent, spent, offchain})) + require.NoError(t, repo.MarkVtxosOnchainSpent( + ctx, map[domain.Outpoint]string{spent.Outpoint: "spendingtxid"}, + )) + + candidates, err := repo.GetUnrolledUnspentVtxos(ctx) + require.NoError(t, err) + require.True(t, containsOutpoint(candidates, unspent.Outpoint)) + require.False(t, containsOutpoint(candidates, spent.Outpoint)) + require.False(t, containsOutpoint(candidates, offchain.Outpoint), + "an offchain vtxo that was never unrolled has no onchain output") + + recorded, err := repo.GetOnchainSpentVtxos(ctx) + require.NoError(t, err) + require.True(t, containsOutpoint(recorded, spent.Outpoint)) + require.False(t, containsOutpoint(recorded, unspent.Outpoint)) + require.False(t, containsOutpoint(recorded, offchain.Outpoint)) + }) + + // The sweeper resolves SpentBy as a checkpoint tx. An onchain-kind + // UTXO has none, so its onchain spend must never reach the sweeper. + t.Run("an onchain-kind vtxo never enters the sweepable set", func(t *testing.T) { + vtxo := onchainKindVtxo(randomString(32)) + require.NoError(t, repo.AddVtxos(ctx, []domain.Vtxo{vtxo})) + require.NoError(t, repo.MarkVtxosOnchainSpent( + ctx, map[domain.Outpoint]string{vtxo.Outpoint: "spendingtxid"}, + )) + + sweepable, err := repo.GetAllSweepableUnrolledVtxos(ctx) + require.NoError(t, err) + require.False(t, containsOutpoint(sweepable, vtxo.Outpoint)) + }) + // The sweeper resolves SpentBy as a checkpoint tx, so an onchain // spend must stay out of its candidate set while an in-Ark spend that // was later unrolled must stay in it. @@ -300,6 +368,17 @@ func onchainSpendVtxo(txid string) domain.Vtxo { } } +// onchainKindVtxo is an on-chain Arkade UTXO, meaning onchain kind, no +// commitment, no batch expiry and never unrolled. +func onchainKindVtxo(txid string) domain.Vtxo { + vtxo := onchainSpendVtxo(txid) + vtxo.Kind = domain.VtxoKindOnchain + vtxo.CommitmentTxids = nil + vtxo.RootCommitmentTxid = "" + vtxo.ExpiresAt = 0 + return vtxo +} + func getOnchainSpendVtxo( t *testing.T, repo domain.VtxoRepository, outpoint domain.Outpoint, ) domain.Vtxo { 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..10a303321 --- /dev/null +++ b/internal/infrastructure/db/postgres/migration/20260901000000_add_vtxo_kind.down.sql @@ -0,0 +1,69 @@ +-- Reverse add_vtxo_kind by dropping the views, dropping the column, then +-- recreating 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..e69bc50fa --- /dev/null +++ b/internal/infrastructure/db/postgres/migration/20260901000000_add_vtxo_kind.up.sql @@ -0,0 +1,72 @@ +-- Add the vtxo_kind discriminator, where 0 = offchain (batch leaf or +-- offchain-tx output) and 1 = onchain (on-chain Arkade UTXO). 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 3b7e3ff3d..2945ffb48 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 selectOnchainSpentVtxos = `-- name: SelectOnchainSpentVtxos :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 = true AND spent = true +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 = true OR vtxo_kind = 1) AND spent = true AND COALESCE(settled_by, '') = '' AND COALESCE(ark_txid, '') = '' ` @@ -1029,6 +1032,7 @@ func (q *Queries) SelectOnchainSpentVtxos(ctx context.Context) ([]SelectOnchainS &i.VtxoVw.UpdatedAt, &i.VtxoVw.Depth, &i.VtxoVw.Markers, + &i.VtxoVw.VtxoKind, &i.VtxoVw.Commitments, &i.VtxoVw.Swept, &i.VtxoVw.AssetID, @@ -1048,7 +1052,7 @@ func (q *Queries) SelectOnchainSpentVtxos(ctx context.Context) ([]SelectOnchainS } 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, '') = '' @@ -1089,6 +1093,7 @@ func (q *Queries) SelectPendingSpentVtxo(ctx context.Context, arg SelectPendingS &i.UpdatedAt, &i.Depth, &i.Markers, + &i.VtxoKind, &i.Commitments, &i.Swept, &i.AssetID, @@ -1108,7 +1113,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[]) @@ -1152,6 +1157,7 @@ func (q *Queries) SelectPendingSpentVtxosWithPubkeys(ctx context.Context, arg Se &i.UpdatedAt, &i.Depth, &i.Markers, + &i.VtxoKind, &i.Commitments, &i.Swept, &i.AssetID, @@ -1471,7 +1477,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 { @@ -1505,6 +1511,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, @@ -1528,7 +1535,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 @@ -1604,6 +1611,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, @@ -1632,7 +1640,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 @@ -1710,6 +1718,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, @@ -1918,7 +1927,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, '') = '' AND COALESCE(ark_txid, '') <> '' +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, '') = '' AND COALESCE(ark_txid, '') <> '' ` type SelectSweepableUnrolledVtxosRow struct { @@ -1958,6 +1967,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, @@ -2104,7 +2114,7 @@ func (q *Queries) SelectTxs(ctx context.Context, dollar_1 []string) ([]SelectTxs } const selectUnrolledUnspentVtxos = `-- name: SelectUnrolledUnspentVtxos :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 = true AND spent = false AND swept = 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 = true OR vtxo_kind = 1) AND spent = false AND swept = false ` type SelectUnrolledUnspentVtxosRow struct { @@ -2140,6 +2150,7 @@ func (q *Queries) SelectUnrolledUnspentVtxos(ctx context.Context) ([]SelectUnrol &i.VtxoVw.UpdatedAt, &i.VtxoVw.Depth, &i.VtxoVw.Markers, + &i.VtxoVw.VtxoKind, &i.VtxoVw.Commitments, &i.VtxoVw.Swept, &i.VtxoVw.AssetID, @@ -2159,7 +2170,7 @@ func (q *Queries) SelectUnrolledUnspentVtxos(ctx context.Context) ([]SelectUnrol } 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 { @@ -2198,6 +2209,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, @@ -2217,7 +2229,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 ` @@ -2250,6 +2262,7 @@ func (q *Queries) SelectVtxoChainByMarker(ctx context.Context, markerIds []strin &i.UpdatedAt, &i.Depth, &i.Markers, + &i.VtxoKind, &i.Commitments, &i.Swept, &i.AssetID, @@ -2352,7 +2365,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) @@ -2383,6 +2396,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, @@ -2403,7 +2417,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 ` @@ -2442,6 +2456,7 @@ func (q *Queries) SelectVtxosByDepthRange(ctx context.Context, arg SelectVtxosBy &i.UpdatedAt, &i.Depth, &i.Markers, + &i.VtxoKind, &i.Commitments, &i.Swept, &i.AssetID, @@ -2461,7 +2476,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 { @@ -2496,6 +2511,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, @@ -2585,7 +2601,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) @@ -2628,6 +2644,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, @@ -2702,7 +2719,7 @@ func (q *Queries) UpdateVtxoMarkers(ctx context.Context, arg UpdateVtxoMarkersPa const updateVtxoOnchainSpent = `-- name: UpdateVtxoOnchainSpent :exec UPDATE vtxo SET spent = true, spent_by = $1, updated_at = (EXTRACT(EPOCH FROM NOW()) * 1000)::BIGINT -WHERE txid = $2 AND vout = $3 AND unrolled = true AND spent = false +WHERE txid = $2 AND vout = $3 AND (unrolled = true OR vtxo_kind = 1) AND spent = false ` type UpdateVtxoOnchainSpentParams struct { @@ -2716,6 +2733,9 @@ type UpdateVtxoOnchainSpentParams struct { // The spent = false guard makes the write idempotent and prevents it clobbering // an offchain spend that lands concurrently, which would erase that vtxo's // ark_txid and hide a genuine fraud case from the sweeper. +// vtxo_kind 1 is an on-chain Arkade UTXO (add_vtxo_kind migration). It has an +// onchain output without ever being unrolled, so the onchain spend statements +// take it alongside unrolled vtxos. func (q *Queries) UpdateVtxoOnchainSpent(ctx context.Context, arg UpdateVtxoOnchainSpentParams) error { _, err := q.db.ExecContext(ctx, updateVtxoOnchainSpent, arg.SpentBy, arg.Txid, arg.Vout) return err @@ -2723,7 +2743,7 @@ func (q *Queries) UpdateVtxoOnchainSpent(ctx context.Context, arg UpdateVtxoOnch const updateVtxoOnchainSpentBy = `-- name: UpdateVtxoOnchainSpentBy :exec UPDATE vtxo SET spent_by = $1, updated_at = (EXTRACT(EPOCH FROM NOW()) * 1000)::BIGINT -WHERE txid = $2 AND vout = $3 AND unrolled = true AND spent = true +WHERE txid = $2 AND vout = $3 AND (unrolled = true OR vtxo_kind = 1) AND spent = true AND COALESCE(settled_by, '') = '' AND COALESCE(ark_txid, '') = '' ` @@ -2743,7 +2763,7 @@ func (q *Queries) UpdateVtxoOnchainSpentBy(ctx context.Context, arg UpdateVtxoOn const updateVtxoOnchainUnspent = `-- name: UpdateVtxoOnchainUnspent :exec UPDATE vtxo SET spent = false, spent_by = NULL, updated_at = (EXTRACT(EPOCH FROM NOW()) * 1000)::BIGINT -WHERE txid = $1 AND vout = $2 AND unrolled = true AND spent = true +WHERE txid = $1 AND vout = $2 AND (unrolled = true OR vtxo_kind = 1) AND spent = true AND COALESCE(settled_by, '') = '' AND COALESCE(ark_txid, '') = '' ` @@ -3249,11 +3269,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, @@ -3268,7 +3288,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 { @@ -3287,6 +3308,7 @@ type UpsertVtxoParams struct { CreatedAt int64 Depth int32 Markers json.RawMessage + VtxoKind int32 } func (q *Queries) UpsertVtxo(ctx context.Context, arg UpsertVtxoParams) error { @@ -3306,6 +3328,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 da1dea184..4a0880018 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) @@ -117,16 +118,19 @@ WHERE txid = @txid AND vout = @vout; -- The spent = false guard makes the write idempotent and prevents it clobbering -- an offchain spend that lands concurrently, which would erase that vtxo's -- ark_txid and hide a genuine fraud case from the sweeper. +-- vtxo_kind 1 is an on-chain Arkade UTXO (add_vtxo_kind migration). It has an +-- onchain output without ever being unrolled, so the onchain spend statements +-- take it alongside unrolled vtxos. -- name: UpdateVtxoOnchainSpent :exec UPDATE vtxo SET spent = true, spent_by = @spent_by, updated_at = (EXTRACT(EPOCH FROM NOW()) * 1000)::BIGINT -WHERE txid = @txid AND vout = @vout AND unrolled = true AND spent = false; +WHERE txid = @txid AND vout = @vout AND (unrolled = true OR vtxo_kind = 1) AND spent = false; -- Re-points an already onchain-spent vtxo at a new spending tx, for when RBF -- replaces the spender. Scoped to rows that are onchain-spent so it can never -- rewrite the spent_by of an offchain spend or a settlement. -- name: UpdateVtxoOnchainSpentBy :exec UPDATE vtxo SET spent_by = @spent_by, updated_at = (EXTRACT(EPOCH FROM NOW()) * 1000)::BIGINT -WHERE txid = @txid AND vout = @vout AND unrolled = true AND spent = true +WHERE txid = @txid AND vout = @vout AND (unrolled = true OR vtxo_kind = 1) AND spent = true AND COALESCE(settled_by, '') = '' AND COALESCE(ark_txid, '') = ''; -- Retracts an onchain spend whose transaction was evicted or reorged out. Same @@ -134,7 +138,7 @@ WHERE txid = @txid AND vout = @vout AND unrolled = true AND spent = true -- be undone by this statement. -- name: UpdateVtxoOnchainUnspent :exec UPDATE vtxo SET spent = false, spent_by = NULL, updated_at = (EXTRACT(EPOCH FROM NOW()) * 1000)::BIGINT -WHERE txid = @txid AND vout = @vout AND unrolled = true AND spent = true +WHERE txid = @txid AND vout = @vout AND (unrolled = true OR vtxo_kind = 1) AND spent = true AND COALESCE(settled_by, '') = '' AND COALESCE(ark_txid, '') = ''; -- name: SelectRoundWithId :many @@ -426,12 +430,12 @@ SELECT sqlc.embed(vtxo_vw) FROM vtxo_vw WHERE spent = true AND unrolled = true A -- Candidates for onchain-spend reconciliation, meaning unrolled vtxos we currently -- believe are unspent. -- name: SelectUnrolledUnspentVtxos :many -SELECT sqlc.embed(vtxo_vw) FROM vtxo_vw WHERE unrolled = true AND spent = false AND swept = false; +SELECT sqlc.embed(vtxo_vw) FROM vtxo_vw WHERE (unrolled = true OR vtxo_kind = 1) AND spent = false AND swept = false; -- Vtxos we currently record as spent onchain, so the reconciler can re-point -- them on RBF or retract them if the spend disappears. -- name: SelectOnchainSpentVtxos :many -SELECT sqlc.embed(vtxo_vw) FROM vtxo_vw WHERE unrolled = true AND spent = true +SELECT sqlc.embed(vtxo_vw) FROM vtxo_vw WHERE (unrolled = true OR vtxo_kind = 1) AND spent = true AND COALESCE(settled_by, '') = '' AND COALESCE(ark_txid, '') = ''; -- name: SelectPendingSpentVtxosWithPubkeys :many diff --git a/internal/infrastructure/db/postgres/vtxo_repo.go b/internal/infrastructure/db/postgres/vtxo_repo.go index 72be8a4f1..6ecfc4b42 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 @@ -675,6 +676,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..d73a30bbf --- /dev/null +++ b/internal/infrastructure/db/sqlite/migration/20260901000000_add_vtxo_kind.down.sql @@ -0,0 +1,77 @@ +-- Reverse add_vtxo_kind by dropping the views, dropping the column, then +-- recreating 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..ec1d63b44 --- /dev/null +++ b/internal/infrastructure/db/sqlite/migration/20260901000000_add_vtxo_kind.up.sql @@ -0,0 +1,81 @@ +-- Add the vtxo_kind discriminator, where 0 = offchain (batch leaf or +-- offchain-tx output) and 1 = onchain (on-chain Arkade UTXO). 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 0223c1d4e..3ac5b45d5 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 selectOnchainSpentVtxos = `-- name: SelectOnchainSpentVtxos :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 = true AND spent = true +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 = true OR vtxo_kind = 1) AND spent = true AND (COALESCE(settled_by, '') = '') AND (COALESCE(ark_txid, '') = '') ` @@ -1120,6 +1123,7 @@ func (q *Queries) SelectOnchainSpentVtxos(ctx context.Context) ([]SelectOnchainS &i.VtxoVw.UpdatedAt, &i.VtxoVw.Depth, &i.VtxoVw.Markers, + &i.VtxoVw.VtxoKind, &i.VtxoVw.Commitments, &i.VtxoVw.Swept, &i.VtxoVw.AssetID, @@ -1139,7 +1143,7 @@ func (q *Queries) SelectOnchainSpentVtxos(ctx context.Context) ([]SelectOnchainS } 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, '') = '' @@ -1180,6 +1184,7 @@ func (q *Queries) SelectPendingSpentVtxo(ctx context.Context, arg SelectPendingS &i.UpdatedAt, &i.Depth, &i.Markers, + &i.VtxoKind, &i.Commitments, &i.Swept, &i.AssetID, @@ -1199,7 +1204,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 ( @@ -1255,6 +1260,7 @@ func (q *Queries) SelectPendingSpentVtxosWithPubkeys(ctx context.Context, arg Se &i.UpdatedAt, &i.Depth, &i.Markers, + &i.VtxoKind, &i.Commitments, &i.Swept, &i.AssetID, @@ -1366,7 +1372,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, ( @@ -1579,7 +1585,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 { @@ -1613,6 +1619,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, @@ -1954,7 +1961,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, '') = '') AND (COALESCE(ark_txid, '') <> '') +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, '') = '') AND (COALESCE(ark_txid, '') <> '') ` type SelectSweepableUnrolledVtxosRow struct { @@ -1994,6 +2001,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, @@ -2182,7 +2190,7 @@ func (q *Queries) SelectTxs(ctx context.Context, arg SelectTxsParams) ([]SelectT } const selectUnrolledUnspentVtxos = `-- name: SelectUnrolledUnspentVtxos :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 = true AND spent = false AND swept = 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 = true OR vtxo_kind = 1) AND spent = false AND swept = false ` type SelectUnrolledUnspentVtxosRow struct { @@ -2218,6 +2226,7 @@ func (q *Queries) SelectUnrolledUnspentVtxos(ctx context.Context) ([]SelectUnrol &i.VtxoVw.UpdatedAt, &i.VtxoVw.Depth, &i.VtxoVw.Markers, + &i.VtxoVw.VtxoKind, &i.VtxoVw.Commitments, &i.VtxoVw.Swept, &i.VtxoVw.AssetID, @@ -2237,7 +2246,7 @@ func (q *Queries) SelectUnrolledUnspentVtxos(ctx context.Context) ([]SelectUnrol } 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 { @@ -2276,6 +2285,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, @@ -2295,7 +2305,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 ` @@ -2334,6 +2344,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, @@ -2353,7 +2364,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 ` @@ -2389,6 +2400,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, @@ -2521,7 +2533,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 { @@ -2556,6 +2568,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, @@ -2576,7 +2589,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 ` @@ -2619,6 +2632,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, @@ -2638,7 +2652,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 { @@ -2675,6 +2689,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, @@ -2763,7 +2778,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*/?) ` @@ -2817,6 +2832,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, @@ -2891,7 +2907,7 @@ func (q *Queries) UpdateVtxoMarkers(ctx context.Context, arg UpdateVtxoMarkersPa const updateVtxoOnchainSpent = `-- name: UpdateVtxoOnchainSpent :exec UPDATE vtxo SET spent = true, spent_by = ?1, updated_at = (CAST((strftime('%s','now') || substr(strftime('%f','now'),4,3)) AS INTEGER)) -WHERE txid = ?2 AND vout = ?3 AND unrolled = true AND spent = false +WHERE txid = ?2 AND vout = ?3 AND (unrolled = true OR vtxo_kind = 1) AND spent = false ` type UpdateVtxoOnchainSpentParams struct { @@ -2905,6 +2921,9 @@ type UpdateVtxoOnchainSpentParams struct { // The spent = false guard makes the write idempotent and prevents it clobbering // an offchain spend that lands concurrently, which would erase that vtxo's // ark_txid and hide a genuine fraud case from the sweeper. +// vtxo_kind 1 is an on-chain Arkade UTXO (add_vtxo_kind migration). It has an +// onchain output without ever being unrolled, so the onchain spend statements +// take it alongside unrolled vtxos. func (q *Queries) UpdateVtxoOnchainSpent(ctx context.Context, arg UpdateVtxoOnchainSpentParams) error { _, err := q.db.ExecContext(ctx, updateVtxoOnchainSpent, arg.SpentBy, arg.Txid, arg.Vout) return err @@ -2912,7 +2931,7 @@ func (q *Queries) UpdateVtxoOnchainSpent(ctx context.Context, arg UpdateVtxoOnch const updateVtxoOnchainSpentBy = `-- name: UpdateVtxoOnchainSpentBy :exec UPDATE vtxo SET spent_by = ?1, updated_at = (CAST((strftime('%s','now') || substr(strftime('%f','now'),4,3)) AS INTEGER)) -WHERE txid = ?2 AND vout = ?3 AND unrolled = true AND spent = true +WHERE txid = ?2 AND vout = ?3 AND (unrolled = true OR vtxo_kind = 1) AND spent = true AND COALESCE(settled_by, '') = '' AND COALESCE(ark_txid, '') = '' ` @@ -2932,7 +2951,7 @@ func (q *Queries) UpdateVtxoOnchainSpentBy(ctx context.Context, arg UpdateVtxoOn const updateVtxoOnchainUnspent = `-- name: UpdateVtxoOnchainUnspent :exec UPDATE vtxo SET spent = false, spent_by = NULL, updated_at = (CAST((strftime('%s','now') || substr(strftime('%f','now'),4,3)) AS INTEGER)) -WHERE txid = ?1 AND vout = ?2 AND unrolled = true AND spent = true +WHERE txid = ?1 AND vout = ?2 AND (unrolled = true OR vtxo_kind = 1) AND spent = true AND COALESCE(settled_by, '') = '' AND COALESCE(ark_txid, '') = '' ` @@ -3438,11 +3457,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, @@ -3457,7 +3476,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 { @@ -3476,6 +3496,7 @@ type UpsertVtxoParams struct { CreatedAt int64 Depth int64 Markers string + VtxoKind int64 } func (q *Queries) UpsertVtxo(ctx context.Context, arg UpsertVtxoParams) error { @@ -3495,6 +3516,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 8f279c83e..afb65a178 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) @@ -117,16 +118,19 @@ WHERE txid = @txid AND vout = @vout; -- The spent = false guard makes the write idempotent and prevents it clobbering -- an offchain spend that lands concurrently, which would erase that vtxo's -- ark_txid and hide a genuine fraud case from the sweeper. +-- vtxo_kind 1 is an on-chain Arkade UTXO (add_vtxo_kind migration). It has an +-- onchain output without ever being unrolled, so the onchain spend statements +-- take it alongside unrolled vtxos. -- name: UpdateVtxoOnchainSpent :exec UPDATE vtxo SET spent = true, spent_by = @spent_by, updated_at = (CAST((strftime('%s','now') || substr(strftime('%f','now'),4,3)) AS INTEGER)) -WHERE txid = @txid AND vout = @vout AND unrolled = true AND spent = false; +WHERE txid = @txid AND vout = @vout AND (unrolled = true OR vtxo_kind = 1) AND spent = false; -- Re-points an already onchain-spent vtxo at a new spending tx, for when RBF -- replaces the spender. Scoped to rows that are onchain-spent so it can never -- rewrite the spent_by of an offchain spend or a settlement. -- name: UpdateVtxoOnchainSpentBy :exec UPDATE vtxo SET spent_by = @spent_by, updated_at = (CAST((strftime('%s','now') || substr(strftime('%f','now'),4,3)) AS INTEGER)) -WHERE txid = @txid AND vout = @vout AND unrolled = true AND spent = true +WHERE txid = @txid AND vout = @vout AND (unrolled = true OR vtxo_kind = 1) AND spent = true AND COALESCE(settled_by, '') = '' AND COALESCE(ark_txid, '') = ''; -- Retracts an onchain spend whose transaction was evicted or reorged out. Same @@ -134,7 +138,7 @@ WHERE txid = @txid AND vout = @vout AND unrolled = true AND spent = true -- be undone by this statement. -- name: UpdateVtxoOnchainUnspent :exec UPDATE vtxo SET spent = false, spent_by = NULL, updated_at = (CAST((strftime('%s','now') || substr(strftime('%f','now'),4,3)) AS INTEGER)) -WHERE txid = @txid AND vout = @vout AND unrolled = true AND spent = true +WHERE txid = @txid AND vout = @vout AND (unrolled = true OR vtxo_kind = 1) AND spent = true AND COALESCE(settled_by, '') = '' AND COALESCE(ark_txid, '') = ''; -- name: SelectRoundWithId :many @@ -437,12 +441,12 @@ SELECT sqlc.embed(vtxo_vw) FROM vtxo_vw WHERE spent = true AND unrolled = true A -- Candidates for onchain-spend reconciliation, meaning unrolled vtxos we currently -- believe are unspent. -- name: SelectUnrolledUnspentVtxos :many -SELECT sqlc.embed(vtxo_vw) FROM vtxo_vw WHERE unrolled = true AND spent = false AND swept = false; +SELECT sqlc.embed(vtxo_vw) FROM vtxo_vw WHERE (unrolled = true OR vtxo_kind = 1) AND spent = false AND swept = false; -- Vtxos we currently record as spent onchain, so the reconciler can re-point -- them on RBF or retract them if the spend disappears. -- name: SelectOnchainSpentVtxos :many -SELECT sqlc.embed(vtxo_vw) FROM vtxo_vw WHERE unrolled = true AND spent = true +SELECT sqlc.embed(vtxo_vw) FROM vtxo_vw WHERE (unrolled = true OR vtxo_kind = 1) AND spent = true AND (COALESCE(settled_by, '') = '') AND (COALESCE(ark_txid, '') = ''); -- name: SelectPendingSpentVtxosWithPubkeys :many diff --git a/internal/infrastructure/db/sqlite/vtxo_repo.go b/internal/infrastructure/db/sqlite/vtxo_repo.go index df0e89b37..9503efb92 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 @@ -839,6 +840,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..085cb5c48 --- /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" +) + +// 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 leaves vtxo_kind on the base table and 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 +} + +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" +)