diff --git a/docs/release-notes/release-notes-0.8.0.md b/docs/release-notes/release-notes-0.8.0.md index 2221af873..e04eaa8ca 100644 --- a/docs/release-notes/release-notes-0.8.0.md +++ b/docs/release-notes/release-notes-0.8.0.md @@ -77,6 +77,11 @@ quotes are restored into the active map so payment flows survive restarts. +* [PR#2051](https://github.com/lightninglabs/taproot-assets/pull/2051) + persists peer-accepted sell quotes to the database on acceptance and + restores them into the active cache on startup, ensuring sell-side + payment flows survive restarts. + * [PR#2010](https://github.com/lightninglabs/taproot-assets/pull/2010) fixes an issue that prevented asset roots from being deleted on universes with existing federation sync log entries. diff --git a/rfq/interface.go b/rfq/interface.go index e59c55373..577143c7d 100644 --- a/rfq/interface.go +++ b/rfq/interface.go @@ -25,6 +25,11 @@ const ( // quote that was persisted for historical SCID lookup. //nolint:lll RfqPolicyTypeAssetPeerAcceptedBuy RfqPolicyType = "RFQ_POLICY_TYPE_PEER_ACCEPTED_BUY" + + // RfqPolicyTypeAssetPeerAcceptedSell identifies a peer-accepted + // sell quote that was persisted for restart continuity. + //nolint:lll + RfqPolicyTypeAssetPeerAcceptedSell RfqPolicyType = "RFQ_POLICY_TYPE_PEER_ACCEPTED_SELL" ) // String converts the policy type to its string representation. @@ -41,16 +46,23 @@ type PolicyStore interface { StorePurchasePolicy(ctx context.Context, accept rfqmsg.SellAccept) error // FetchAcceptedQuotes fetches all non-expired accepted quotes. - // Returns sale policies as buy accepts, purchase policies as sell - // accepts, and peer-accepted buy quotes separately. + // Returns sale policies as buy accepts, purchase policies as + // sell accepts, peer-accepted buy quotes, and peer-accepted + // sell quotes separately. FetchAcceptedQuotes(ctx context.Context) ([]rfqmsg.BuyAccept, - []rfqmsg.SellAccept, []rfqmsg.BuyAccept, error) + []rfqmsg.SellAccept, []rfqmsg.BuyAccept, + []rfqmsg.SellAccept, error) - // StorePeerAcceptedBuyQuote persists a peer-accepted buy quote for - // historical SCID-to-peer lookup. + // StorePeerAcceptedBuyQuote persists a peer-accepted buy quote + // for historical SCID-to-peer lookup. StorePeerAcceptedBuyQuote(ctx context.Context, accept rfqmsg.BuyAccept) error + // StorePeerAcceptedSellQuote persists a peer-accepted sell + // quote for restart continuity. + StorePeerAcceptedSellQuote(ctx context.Context, + accept rfqmsg.SellAccept) error + // LookUpScid looks up the peer associated with the given SCID from // persisted peer-accepted buy quote policies. LookUpScid(ctx context.Context, scid uint64) (route.Vertex, error) diff --git a/rfq/manager.go b/rfq/manager.go index b424bddf6..c72412d11 100644 --- a/rfq/manager.go +++ b/rfq/manager.go @@ -529,6 +529,18 @@ func (m *Manager) handleIncomingMessage(ctx context.Context, scid := msg.ShortChannelId() m.orderHandler.peerSellQuotes.Store(scid, msg) + // Persist the peer sell quote to DB so that + // the peerSellQuotes cache survives restarts. + pStore := m.cfg.PolicyStore + storeErr := pStore.StorePeerAcceptedSellQuote( + ctx, msg, + ) + if storeErr != nil { + log.Errorf("Failed to persist peer sell "+ + "quote for SCID %d: %v", scid, + storeErr) + } + // Notify subscribers of the incoming peer accepted // asset sell quote. event := NewPeerAcceptedSellQuoteEvent(&msg) diff --git a/rfq/manager_test.go b/rfq/manager_test.go index 9fb63ac8d..4e827e5bf 100644 --- a/rfq/manager_test.go +++ b/rfq/manager_test.go @@ -78,9 +78,10 @@ func (mockPolicyStore) StorePurchasePolicy(context.Context, } func (mockPolicyStore) FetchAcceptedQuotes(context.Context) ( - []rfqmsg.BuyAccept, []rfqmsg.SellAccept, []rfqmsg.BuyAccept, error) { + []rfqmsg.BuyAccept, []rfqmsg.SellAccept, []rfqmsg.BuyAccept, + []rfqmsg.SellAccept, error) { - return nil, nil, nil, nil + return nil, nil, nil, nil, nil } func (mockPolicyStore) StorePeerAcceptedBuyQuote(context.Context, @@ -89,6 +90,12 @@ func (mockPolicyStore) StorePeerAcceptedBuyQuote(context.Context, return nil } +func (mockPolicyStore) StorePeerAcceptedSellQuote(context.Context, + rfqmsg.SellAccept) error { + + return nil +} + func (mockPolicyStore) LookUpScid(_ context.Context, _ uint64) (route.Vertex, error) { @@ -349,6 +356,12 @@ func (f *failingPolicyStore) StorePeerAcceptedBuyQuote(context.Context, return fmt.Errorf("simulated DB write failure") } +func (f *failingPolicyStore) StorePeerAcceptedSellQuote(context.Context, + rfqmsg.SellAccept) error { + + return fmt.Errorf("simulated DB write failure") +} + func (f *failingPolicyStore) LookUpScid(_ context.Context, scid uint64) (route.Vertex, error) { diff --git a/rfq/order.go b/rfq/order.go index 68783e806..e3894b9f2 100644 --- a/rfq/order.go +++ b/rfq/order.go @@ -1448,7 +1448,7 @@ func (h *OrderHandler) RegisterAssetPurchasePolicy(ctx context.Context, // restorePersistedPolicies restores persisted policies from the policy store. func (h *OrderHandler) restorePersistedPolicies(ctx context.Context) error { - buyAccepts, sellAccepts, peerBuyAccepts, + buyAccepts, sellAccepts, peerBuyAccepts, peerSellAccepts, err := h.cfg.PolicyStore.FetchAcceptedQuotes(ctx) if err != nil { return fmt.Errorf("error fetching persisted policies: %w", err) @@ -1478,6 +1478,10 @@ func (h *OrderHandler) restorePersistedPolicies(ctx context.Context) error { h.peerBuyQuotes.Store(accept.ShortChannelId(), accept) } + for _, accept := range peerSellAccepts { + h.peerSellQuotes.Store(accept.ShortChannelId(), accept) + } + return nil } diff --git a/tapdb/migrations.go b/tapdb/migrations.go index 0f0981e7b..b316711de 100644 --- a/tapdb/migrations.go +++ b/tapdb/migrations.go @@ -24,7 +24,7 @@ const ( // daemon. // // NOTE: This MUST be updated when a new migration is added. - LatestMigrationVersion = 54 + LatestMigrationVersion = 55 ) // DatabaseBackend is an interface that contains all methods our different diff --git a/tapdb/rfq_policies.go b/tapdb/rfq_policies.go index 4ed06dcfb..bfcc578a8 100644 --- a/tapdb/rfq_policies.go +++ b/tapdb/rfq_policies.go @@ -172,17 +172,20 @@ func (s *PersistedPolicyStore) storePolicy(ctx context.Context, }) } -// FetchAcceptedQuotes retrieves all non-expired policies from the database. -// Sale policies are returned as buy accepts, purchase policies as sell accepts, -// and peer-accepted buy quotes are returned separately. +// FetchAcceptedQuotes retrieves all non-expired policies from the +// database. Sale policies are returned as buy accepts, purchase +// policies as sell accepts, and peer-accepted buy/sell quotes are +// returned separately. func (s *PersistedPolicyStore) FetchAcceptedQuotes(ctx context.Context) ( - []rfqmsg.BuyAccept, []rfqmsg.SellAccept, []rfqmsg.BuyAccept, error) { + []rfqmsg.BuyAccept, []rfqmsg.SellAccept, []rfqmsg.BuyAccept, + []rfqmsg.SellAccept, error) { readOpts := ReadTxOption() var ( - buyAccepts []rfqmsg.BuyAccept - sellAccepts []rfqmsg.SellAccept - peerBuyAccepts []rfqmsg.BuyAccept + buyAccepts []rfqmsg.BuyAccept + sellAccepts []rfqmsg.SellAccept + peerBuyAccepts []rfqmsg.BuyAccept + peerSellAccepts []rfqmsg.SellAccept ) now := time.Now().UTC() @@ -222,6 +225,17 @@ func (s *PersistedPolicyStore) FetchAcceptedQuotes(ctx context.Context) ( peerBuyAccepts, accept, ) + case rfq.RfqPolicyTypeAssetPeerAcceptedSell: + accept, err := sellAcceptFromStored(policy) + if err != nil { + return fmt.Errorf("error restoring "+ + "peer sell quote: %w", + err) + } + peerSellAccepts = append( + peerSellAccepts, accept, + ) + default: // This should never happen by assertion. return fmt.Errorf("unknown policy type: %s", @@ -232,10 +246,11 @@ func (s *PersistedPolicyStore) FetchAcceptedQuotes(ctx context.Context) ( return nil }) if err != nil { - return nil, nil, nil, err + return nil, nil, nil, nil, err } - return buyAccepts, sellAccepts, peerBuyAccepts, nil + return buyAccepts, sellAccepts, peerBuyAccepts, + peerSellAccepts, nil } // StorePeerAcceptedBuyQuote persists a peer-accepted buy quote for historical @@ -267,6 +282,40 @@ func (s *PersistedPolicyStore) StorePeerAcceptedBuyQuote(ctx context.Context, return s.storePolicy(ctx, record) } +// StorePeerAcceptedSellQuote persists a peer-accepted sell quote for +// restart continuity. +func (s *PersistedPolicyStore) StorePeerAcceptedSellQuote( + ctx context.Context, acpt rfqmsg.SellAccept) error { + + assetID, groupKey := specifierPointers( + acpt.Request.AssetSpecifier, + ) + rateBytes := coefficientBytes(acpt.AssetRate.Rate) + expiry := acpt.AssetRate.Expiry.UTC() + paymentMax := int64(acpt.Request.PaymentMaxAmt) + + record := rfqPolicy{ + PolicyType: rfq.RfqPolicyTypeAssetPeerAcceptedSell, + Scid: uint64(acpt.ShortChannelId()), + RfqID: rfqIDArray(acpt.ID), + Peer: serializePeer(acpt.Peer), + AssetID: assetID, + AssetGroupKey: groupKey, + RateCoefficient: rateBytes, + RateScale: acpt.AssetRate.Rate.Scale, + ExpiryUnix: uint64(expiry.Unix()), + PaymentMaxMsat: fn.Ptr(paymentMax), + RequestPaymentMaxMsat: fn.Ptr(paymentMax), + PriceOracleMetadata: acpt.Request.PriceOracleMetadata, + RequestVersion: fn.Ptr( + uint32(acpt.Request.Version), + ), + AgreedAt: acpt.AgreedAt.UTC(), + } + + return s.storePolicy(ctx, record) +} + // LookUpScid looks up the peer associated with the given SCID by querying // persisted peer-accepted buy quote policies. func (s *PersistedPolicyStore) LookUpScid(ctx context.Context, diff --git a/tapdb/rfq_policies_test.go b/tapdb/rfq_policies_test.go index 542a0e43c..9b225fbc6 100644 --- a/tapdb/rfq_policies_test.go +++ b/tapdb/rfq_policies_test.go @@ -142,7 +142,7 @@ func TestFetchAcceptedQuotesSeparatesPeerAcceptedBuy(t *testing.T) { err = store.StoreSalePolicy(ctx, saleAccept) require.NoError(t, err) - buyAccepts, sellAccepts, peerBuys, err := + buyAccepts, sellAccepts, peerBuys, peerSells, err := store.FetchAcceptedQuotes(ctx) require.NoError(t, err) @@ -151,6 +151,7 @@ func TestFetchAcceptedQuotesSeparatesPeerAcceptedBuy(t *testing.T) { require.Len(t, buyAccepts, 1) require.Len(t, sellAccepts, 0) require.Len(t, peerBuys, 1) + require.Len(t, peerSells, 0) require.Equal(t, saleAccept.ID, buyAccepts[0].ID) require.Equal(t, accept.ID, peerBuys[0].ID) } @@ -216,11 +217,11 @@ func testSellAccept(t *testing.T) rfqmsg.SellAccept { } } -// TestFetchAcceptedQuotesAllThreeTypes verifies that FetchAcceptedQuotes -// correctly categorises all three policy types: sale policies appear as buy -// accepts, purchase policies appear as sell accepts, and peer-accepted buy -// quotes are returned separately. -func TestFetchAcceptedQuotesAllThreeTypes(t *testing.T) { +// TestFetchAcceptedQuotesAllFourTypes verifies that FetchAcceptedQuotes +// correctly categorises all four policy types: sale policies appear as +// buy accepts, purchase policies appear as sell accepts, and +// peer-accepted buy/sell quotes are returned separately. +func TestFetchAcceptedQuotesAllFourTypes(t *testing.T) { t.Parallel() ctx := context.Background() @@ -239,17 +240,22 @@ func TestFetchAcceptedQuotesAllThreeTypes(t *testing.T) { err = store.StorePeerAcceptedBuyQuote(ctx, peerBuy) require.NoError(t, err) - buyAccepts, sellAccepts, peerBuys, err := + peerSell := testSellAccept(t) + err = store.StorePeerAcceptedSellQuote(ctx, peerSell) + require.NoError(t, err) + + buyAccepts, sellAccepts, peerBuys, peerSells, err := store.FetchAcceptedQuotes(ctx) require.NoError(t, err) - // Sale → buyAccepts, Purchase → sellAccepts, PeerBuy → peerBuys. require.Len(t, buyAccepts, 1) require.Len(t, sellAccepts, 1) require.Len(t, peerBuys, 1) + require.Len(t, peerSells, 1) require.Equal(t, sale.ID, buyAccepts[0].ID) require.Equal(t, purchase.ID, sellAccepts[0].ID) require.Equal(t, peerBuy.ID, peerBuys[0].ID) + require.Equal(t, peerSell.ID, peerSells[0].ID) } // TestLookUpScidIgnoresSalePolicy verifies that a sale policy stored in the @@ -269,3 +275,61 @@ func TestLookUpScidIgnoresSalePolicy(t *testing.T) { require.Error(t, err) require.Contains(t, err.Error(), "error fetching policy by SCID") } + +// TestStorePeerAcceptedSellQuote tests that a peer-accepted sell quote +// can be persisted and round-tripped through FetchAcceptedQuotes. +func TestStorePeerAcceptedSellQuote(t *testing.T) { + t.Parallel() + + ctx := context.Background() + store := newPolicyStore(t) + + accept := testSellAccept(t) + + err := store.StorePeerAcceptedSellQuote(ctx, accept) + require.NoError(t, err) + + _, _, _, peerSells, err := store.FetchAcceptedQuotes(ctx) + require.NoError(t, err) + + require.Len(t, peerSells, 1) + require.Equal(t, accept.ID, peerSells[0].ID) + require.Equal(t, accept.Peer, peerSells[0].Peer) + require.Equal( + t, accept.Request.PaymentMaxAmt, + peerSells[0].Request.PaymentMaxAmt, + ) +} + +// TestFetchAcceptedQuotesSeparatesPeerAcceptedSell verifies that +// FetchAcceptedQuotes returns peer-accepted sell quotes in the fourth +// return value, separate from purchase policies. +func TestFetchAcceptedQuotesSeparatesPeerAcceptedSell(t *testing.T) { + t.Parallel() + + ctx := context.Background() + store := newPolicyStore(t) + + // Store a peer-accepted sell quote. + peerSell := testSellAccept(t) + err := store.StorePeerAcceptedSellQuote(ctx, peerSell) + require.NoError(t, err) + + // Also store a regular purchase policy. + purchase := testSellAccept(t) + err = store.StorePurchasePolicy(ctx, purchase) + require.NoError(t, err) + + buyAccepts, sellAccepts, peerBuys, peerSells, err := + store.FetchAcceptedQuotes(ctx) + require.NoError(t, err) + + // Purchase policy appears in sellAccepts, peer sell quote + // appears separately in peerSells. + require.Len(t, buyAccepts, 0) + require.Len(t, sellAccepts, 1) + require.Len(t, peerBuys, 0) + require.Len(t, peerSells, 1) + require.Equal(t, purchase.ID, sellAccepts[0].ID) + require.Equal(t, peerSell.ID, peerSells[0].ID) +} diff --git a/tapdb/sqlc/migrations/000055_rfq_peer_accepted_sell.down.sql b/tapdb/sqlc/migrations/000055_rfq_peer_accepted_sell.down.sql new file mode 100644 index 000000000..6ae1cf552 --- /dev/null +++ b/tapdb/sqlc/migrations/000055_rfq_peer_accepted_sell.down.sql @@ -0,0 +1,154 @@ +-- Revert rfq_policies CHECK constraint to 3 types (remove +-- peer-accepted sell). Must also handle the forwards table FK +-- reference. + +DELETE FROM rfq_policies + WHERE policy_type = 'RFQ_POLICY_TYPE_PEER_ACCEPTED_SELL'; + +-- 1. Save existing forwards data and drop the table. +CREATE TEMP TABLE IF NOT EXISTS forwards_backup + AS SELECT * FROM forwards; +DROP TABLE IF EXISTS forwards; + +DROP INDEX IF EXISTS rfq_policies_scid_idx; + +-- 2. Rename and recreate rfq_policies with 3-type CHECK constraint. +ALTER TABLE rfq_policies RENAME TO rfq_policies_old; + +CREATE TABLE IF NOT EXISTS rfq_policies ( + id INTEGER PRIMARY KEY, + + -- policy_type denotes the type of the policy. + policy_type TEXT NOT NULL CHECK ( + policy_type IN ( + 'RFQ_POLICY_TYPE_SALE', + 'RFQ_POLICY_TYPE_PURCHASE', + 'RFQ_POLICY_TYPE_PEER_ACCEPTED_BUY' + ) + ), + + -- scid is the short channel ID associated with the policy. + scid BIGINT NOT NULL, + + -- rfq_id is the unique identifier for the RFQ session. + rfq_id BLOB NOT NULL CHECK (length(rfq_id) = 32), + + -- peer is the public key of the peer node. + peer BLOB NOT NULL CHECK (length(peer) = 33), + + -- asset_id is the optional asset ID. + asset_id BLOB CHECK (length(asset_id) = 32), + + -- asset_group_key is the optional asset group key. + asset_group_key BLOB CHECK (length(asset_group_key) = 33), + + -- rate_coefficient is the coefficient of the exchange rate. + rate_coefficient BLOB NOT NULL, + + -- rate_scale is the scale of the exchange rate. + rate_scale INTEGER NOT NULL, + + -- expiry is the expiration timestamp of the policy. + expiry BIGINT NOT NULL, + + -- max_out_asset_amt is the maximum asset amount for sale + -- policies. + max_out_asset_amt BIGINT, + + -- payment_max_msat is the maximum payment amount for purchase + -- policies. + payment_max_msat BIGINT, + + -- request_asset_max_amt is the requested maximum asset amount. + request_asset_max_amt BIGINT, + + -- request_payment_max_msat is the requested maximum payment + -- amount. + request_payment_max_msat BIGINT, + + -- price_oracle_metadata contains metadata about the price + -- oracle. + price_oracle_metadata TEXT, + + -- request_version is the version of the RFQ request. + request_version INTEGER, + + -- agreed_at is the timestamp when the policy was agreed upon. + agreed_at BIGINT NOT NULL +); + +INSERT INTO rfq_policies ( + policy_type, scid, rfq_id, peer, asset_id, asset_group_key, + rate_coefficient, rate_scale, expiry, max_out_asset_amt, + payment_max_msat, request_asset_max_amt, + request_payment_max_msat, price_oracle_metadata, + request_version, agreed_at +) SELECT + policy_type, scid, rfq_id, peer, asset_id, asset_group_key, + rate_coefficient, rate_scale, expiry, max_out_asset_amt, + payment_max_msat, request_asset_max_amt, + request_payment_max_msat, price_oracle_metadata, + request_version, agreed_at +FROM rfq_policies_old; +DROP TABLE rfq_policies_old; + +CREATE UNIQUE INDEX IF NOT EXISTS rfq_policies_rfq_id_idx + ON rfq_policies (rfq_id); +CREATE INDEX IF NOT EXISTS rfq_policies_scid_idx + ON rfq_policies (scid); + +-- 3. Recreate forwards table and restore data. +CREATE TABLE IF NOT EXISTS forwards ( + id INTEGER PRIMARY KEY, + + -- opened_at is the timestamp when the forward was initiated. + opened_at TIMESTAMP NOT NULL, + + -- settled_at is the timestamp when the forward settled. + settled_at TIMESTAMP, + + -- failed_at is the timestamp when the forward failed. + failed_at TIMESTAMP, + + -- rfq_id is the foreign key to the RFQ policy. + rfq_id BLOB NOT NULL CHECK (length(rfq_id) = 32) + REFERENCES rfq_policies(rfq_id), + + -- chan_id_in is the short channel ID of the incoming channel. + chan_id_in BIGINT NOT NULL, + + -- chan_id_out is the short channel ID of the outgoing channel. + chan_id_out BIGINT NOT NULL, + + -- htlc_id is the HTLC ID on the incoming channel. + htlc_id BIGINT NOT NULL, + + -- asset_amt is the asset amount involved in this swap. + asset_amt BIGINT NOT NULL, + + -- amt_in_msat is the actual amount received on the incoming + -- channel in millisatoshis. + amt_in_msat BIGINT NOT NULL, + + -- amt_out_msat is the actual amount sent on the outgoing + -- channel in millisatoshis. + amt_out_msat BIGINT NOT NULL, + + UNIQUE(chan_id_in, htlc_id) +); + +INSERT INTO forwards ( + opened_at, settled_at, failed_at, rfq_id, chan_id_in, + chan_id_out, htlc_id, asset_amt, amt_in_msat, amt_out_msat +) SELECT + opened_at, settled_at, failed_at, rfq_id, chan_id_in, + chan_id_out, htlc_id, asset_amt, amt_in_msat, amt_out_msat +FROM forwards_backup; +DROP TABLE forwards_backup; + +CREATE INDEX IF NOT EXISTS forwards_opened_at_idx + ON forwards(opened_at); +CREATE INDEX IF NOT EXISTS forwards_settled_at_idx + ON forwards(settled_at); +CREATE INDEX IF NOT EXISTS forwards_rfq_id_idx + ON forwards(rfq_id); diff --git a/tapdb/sqlc/migrations/000055_rfq_peer_accepted_sell.up.sql b/tapdb/sqlc/migrations/000055_rfq_peer_accepted_sell.up.sql new file mode 100644 index 000000000..73f85b09e --- /dev/null +++ b/tapdb/sqlc/migrations/000055_rfq_peer_accepted_sell.up.sql @@ -0,0 +1,158 @@ +-- Recreate rfq_policies with an expanded CHECK constraint that also +-- allows peer-accepted sell quotes +-- ('RFQ_POLICY_TYPE_PEER_ACCEPTED_SELL'). +-- SQLite does not support ALTER CONSTRAINT, so we recreate the table. +-- +-- Because the forwards table has a foreign key reference to +-- rfq_policies(rfq_id), and SQLite rewrites FK references on table +-- rename, we must also drop and recreate the forwards table to avoid +-- a dangling FK. + +-- 1. Save existing forwards data and drop the table. +CREATE TEMP TABLE IF NOT EXISTS forwards_backup + AS SELECT * FROM forwards; +DROP TABLE IF EXISTS forwards; + +-- 2. Rename the old rfq_policies table and create the new one with +-- the expanded CHECK constraint. +ALTER TABLE rfq_policies RENAME TO rfq_policies_old; + +CREATE TABLE IF NOT EXISTS rfq_policies ( + id INTEGER PRIMARY KEY, + + -- policy_type denotes the type of the policy. + policy_type TEXT NOT NULL CHECK ( + policy_type IN ( + 'RFQ_POLICY_TYPE_SALE', + 'RFQ_POLICY_TYPE_PURCHASE', + 'RFQ_POLICY_TYPE_PEER_ACCEPTED_BUY', + 'RFQ_POLICY_TYPE_PEER_ACCEPTED_SELL' + ) + ), + + -- scid is the short channel ID associated with the policy. + scid BIGINT NOT NULL, + + -- rfq_id is the unique identifier for the RFQ session. + rfq_id BLOB NOT NULL CHECK (length(rfq_id) = 32), + + -- peer is the public key of the peer node. + peer BLOB NOT NULL CHECK (length(peer) = 33), + + -- asset_id is the optional asset ID. + asset_id BLOB CHECK (length(asset_id) = 32), + + -- asset_group_key is the optional asset group key. + asset_group_key BLOB CHECK (length(asset_group_key) = 33), + + -- rate_coefficient is the coefficient of the exchange rate. + rate_coefficient BLOB NOT NULL, + + -- rate_scale is the scale of the exchange rate. + rate_scale INTEGER NOT NULL, + + -- expiry is the expiration timestamp of the policy. + expiry BIGINT NOT NULL, + + -- max_out_asset_amt is the maximum asset amount for sale + -- policies. + max_out_asset_amt BIGINT, + + -- payment_max_msat is the maximum payment amount for purchase + -- policies. + payment_max_msat BIGINT, + + -- request_asset_max_amt is the requested maximum asset amount. + request_asset_max_amt BIGINT, + + -- request_payment_max_msat is the requested maximum payment + -- amount. + request_payment_max_msat BIGINT, + + -- price_oracle_metadata contains metadata about the price + -- oracle. + price_oracle_metadata TEXT, + + -- request_version is the version of the RFQ request. + request_version INTEGER, + + -- agreed_at is the timestamp when the policy was agreed upon. + agreed_at BIGINT NOT NULL +); + +INSERT INTO rfq_policies ( + policy_type, scid, rfq_id, peer, asset_id, asset_group_key, + rate_coefficient, rate_scale, expiry, max_out_asset_amt, + payment_max_msat, request_asset_max_amt, + request_payment_max_msat, price_oracle_metadata, + request_version, agreed_at +) SELECT + policy_type, scid, rfq_id, peer, asset_id, asset_group_key, + rate_coefficient, rate_scale, expiry, max_out_asset_amt, + payment_max_msat, request_asset_max_amt, + request_payment_max_msat, price_oracle_metadata, + request_version, agreed_at +FROM rfq_policies_old; +DROP TABLE rfq_policies_old; + +CREATE UNIQUE INDEX IF NOT EXISTS rfq_policies_rfq_id_idx + ON rfq_policies (rfq_id); +CREATE INDEX IF NOT EXISTS rfq_policies_scid_idx + ON rfq_policies (scid); + +-- 3. Recreate the forwards table with the FK pointing to the new +-- rfq_policies table, restore data and indexes. +CREATE TABLE IF NOT EXISTS forwards ( + id INTEGER PRIMARY KEY, + + -- opened_at is the timestamp when the forward was initiated. + opened_at TIMESTAMP NOT NULL, + + -- settled_at is the timestamp when the forward settled. + settled_at TIMESTAMP, + + -- failed_at is the timestamp when the forward failed. + failed_at TIMESTAMP, + + -- rfq_id is the foreign key to the RFQ policy. + rfq_id BLOB NOT NULL CHECK (length(rfq_id) = 32) + REFERENCES rfq_policies(rfq_id), + + -- chan_id_in is the short channel ID of the incoming channel. + chan_id_in BIGINT NOT NULL, + + -- chan_id_out is the short channel ID of the outgoing channel. + chan_id_out BIGINT NOT NULL, + + -- htlc_id is the HTLC ID on the incoming channel. + htlc_id BIGINT NOT NULL, + + -- asset_amt is the asset amount involved in this swap. + asset_amt BIGINT NOT NULL, + + -- amt_in_msat is the actual amount received on the incoming + -- channel in millisatoshis. + amt_in_msat BIGINT NOT NULL, + + -- amt_out_msat is the actual amount sent on the outgoing + -- channel in millisatoshis. + amt_out_msat BIGINT NOT NULL, + + UNIQUE(chan_id_in, htlc_id) +); + +INSERT INTO forwards ( + opened_at, settled_at, failed_at, rfq_id, chan_id_in, + chan_id_out, htlc_id, asset_amt, amt_in_msat, amt_out_msat +) SELECT + opened_at, settled_at, failed_at, rfq_id, chan_id_in, + chan_id_out, htlc_id, asset_amt, amt_in_msat, amt_out_msat +FROM forwards_backup; +DROP TABLE forwards_backup; + +CREATE INDEX IF NOT EXISTS forwards_opened_at_idx + ON forwards(opened_at); +CREATE INDEX IF NOT EXISTS forwards_settled_at_idx + ON forwards(settled_at); +CREATE INDEX IF NOT EXISTS forwards_rfq_id_idx + ON forwards(rfq_id); diff --git a/tapdb/sqlc/schemas/generated_schema.sql b/tapdb/sqlc/schemas/generated_schema.sql index 17bc919e4..0220cb2bc 100644 --- a/tapdb/sqlc/schemas/generated_schema.sql +++ b/tapdb/sqlc/schemas/generated_schema.sql @@ -529,22 +529,25 @@ CREATE TABLE forwards ( -- asset_amt is the asset amount involved in this swap. asset_amt BIGINT NOT NULL, - -- amt_in_msat is the actual amount received on the incoming channel in - -- millisatoshis. + -- amt_in_msat is the actual amount received on the incoming + -- channel in millisatoshis. amt_in_msat BIGINT NOT NULL, - -- amt_out_msat is the actual amount sent on the outgoing channel in - -- millisatoshis. + -- amt_out_msat is the actual amount sent on the outgoing + -- channel in millisatoshis. amt_out_msat BIGINT NOT NULL, UNIQUE(chan_id_in, htlc_id) ); -CREATE INDEX forwards_opened_at_idx ON forwards(opened_at); +CREATE INDEX forwards_opened_at_idx + ON forwards(opened_at); -CREATE INDEX forwards_rfq_id_idx ON forwards(rfq_id); +CREATE INDEX forwards_rfq_id_idx + ON forwards(rfq_id); -CREATE INDEX forwards_settled_at_idx ON forwards(settled_at); +CREATE INDEX forwards_settled_at_idx + ON forwards(settled_at); CREATE TABLE genesis_assets ( gen_asset_id INTEGER PRIMARY KEY, @@ -834,7 +837,8 @@ CREATE TABLE rfq_policies ( policy_type IN ( 'RFQ_POLICY_TYPE_SALE', 'RFQ_POLICY_TYPE_PURCHASE', - 'RFQ_POLICY_TYPE_PEER_ACCEPTED_BUY' + 'RFQ_POLICY_TYPE_PEER_ACCEPTED_BUY', + 'RFQ_POLICY_TYPE_PEER_ACCEPTED_SELL' ) ), @@ -862,19 +866,23 @@ CREATE TABLE rfq_policies ( -- expiry is the expiration timestamp of the policy. expiry BIGINT NOT NULL, - -- max_out_asset_amt is the maximum asset amount for sale policies. + -- max_out_asset_amt is the maximum asset amount for sale + -- policies. max_out_asset_amt BIGINT, - -- payment_max_msat is the maximum payment amount for purchase policies. + -- payment_max_msat is the maximum payment amount for purchase + -- policies. payment_max_msat BIGINT, -- request_asset_max_amt is the requested maximum asset amount. request_asset_max_amt BIGINT, - -- request_payment_max_msat is the requested maximum payment amount. + -- request_payment_max_msat is the requested maximum payment + -- amount. request_payment_max_msat BIGINT, - -- price_oracle_metadata contains metadata about the price oracle. + -- price_oracle_metadata contains metadata about the price + -- oracle. price_oracle_metadata TEXT, -- request_version is the version of the RFQ request. @@ -884,9 +892,11 @@ CREATE TABLE rfq_policies ( agreed_at BIGINT NOT NULL ); -CREATE UNIQUE INDEX rfq_policies_rfq_id_idx ON rfq_policies (rfq_id); +CREATE UNIQUE INDEX rfq_policies_rfq_id_idx + ON rfq_policies (rfq_id); -CREATE INDEX rfq_policies_scid_idx ON rfq_policies (scid); +CREATE INDEX rfq_policies_scid_idx + ON rfq_policies (scid); CREATE TABLE script_keys ( script_key_id INTEGER PRIMARY KEY,