diff --git a/config.go b/config.go index b4e9d800e..681e958bc 100644 --- a/config.go +++ b/config.go @@ -15,6 +15,7 @@ import ( "github.com/lightninglabs/taproot-assets/tapdb" "github.com/lightninglabs/taproot-assets/tapfreighter" "github.com/lightninglabs/taproot-assets/tapgarden" + "github.com/lightninglabs/taproot-assets/taprpc/priceoraclerpc" "github.com/lightninglabs/taproot-assets/universe" "github.com/lightningnetwork/lnd" "github.com/lightningnetwork/lnd/build" @@ -52,6 +53,8 @@ type RPCConfig struct { AllowPublicStats bool + AllowPublicPriceOracle bool + LetsEncryptDir string LetsEncryptListen string @@ -197,6 +200,10 @@ type Config struct { PriceOracle rfq.PriceOracle + // ProxyPriceOracle is an optional price oracle that can be used to + // proxy price requests to another price oracle. + ProxyPriceOracle priceoraclerpc.PriceOracleClient + UniverseStats universe.Telemetry AuxLeafSigner *tapchannel.AuxLeafSigner diff --git a/itest/integration_test.go b/itest/integration_test.go index fd8c87c9c..23ce88307 100644 --- a/itest/integration_test.go +++ b/itest/integration_test.go @@ -10,6 +10,7 @@ import ( "time" "github.com/lightninglabs/taproot-assets/taprpc" + "github.com/lightninglabs/taproot-assets/taprpc/priceoraclerpc" "github.com/lightningnetwork/lnd/lntest" "github.com/stretchr/testify/require" ) @@ -129,3 +130,23 @@ func testGetInfo(t *harnessTest) { // Ensure the response matches the expected response. require.Equal(t.t, resp, respCli) } + +// testPriceOracleProxy tests the price oracle proxy. +func testPriceOracleProxy(t *harnessTest) { + ctxb := context.Background() + ctxt, cancel := context.WithTimeout(ctxb, defaultWaitTimeout) + defer cancel() + + resp, err := t.tapd.PriceOracleClient.QueryAssetRates( + ctxt, &priceoraclerpc.QueryAssetRatesRequest{}, + ) + require.NoError(t.t, err) + + okResp := resp.GetOk() + require.NotNil(t.t, okResp) + + require.Equal( + t.t, fmt.Sprintf("%v", mockPriceOracleCoef), + okResp.AssetRates.SubjectAssetRate.Coefficient, + ) +} diff --git a/itest/tapd_harness.go b/itest/tapd_harness.go index 11a61ba63..98e534357 100644 --- a/itest/tapd_harness.go +++ b/itest/tapd_harness.go @@ -23,6 +23,7 @@ import ( "github.com/lightninglabs/taproot-assets/taprpc" "github.com/lightninglabs/taproot-assets/taprpc/assetwalletrpc" "github.com/lightninglabs/taproot-assets/taprpc/mintrpc" + "github.com/lightninglabs/taproot-assets/taprpc/priceoraclerpc" "github.com/lightninglabs/taproot-assets/taprpc/rfqrpc" tchrpc "github.com/lightninglabs/taproot-assets/taprpc/tapchannelrpc" "github.com/lightninglabs/taproot-assets/taprpc/tapdevrpc" @@ -83,6 +84,10 @@ const ( // timeout we'll use for waiting for a receiver to acknowledge a proof // transfer. defaultProofTransferReceiverAckTimeout = 500 * time.Millisecond + + // mockPriceOracleCoef is the coefficient used in the mock price oracle + // to calculate the asset rate. + mockPriceOracleCoef = 5_820_600 ) // tapdHarness is a test harness that holds everything that is needed to @@ -102,6 +107,7 @@ type tapdHarness struct { tchrpc.TaprootAssetChannelsClient universerpc.UniverseClient tapdevrpc.TapDevClient + priceoraclerpc.PriceOracleClient } // tapdConfig holds all configuration items that are required to start a tapd @@ -223,7 +229,7 @@ func newTapdHarness(t *testing.T, ht *harnessTest, cfg tapdConfig, Rfq: rfq.CliConfig{ //nolint:lll PriceOracleAddress: rfq.MockPriceOracleServiceAddress, - MockOracleAssetsPerBTC: 5_820_600, + MockOracleAssetsPerBTC: mockPriceOracleCoef, }, } @@ -425,6 +431,7 @@ func (hs *tapdHarness) start(expectErrExit bool) error { ) hs.UniverseClient = universerpc.NewUniverseClient(rpcConn) hs.TapDevClient = tapdevrpc.NewTapDevClient(rpcConn) + hs.PriceOracleClient = priceoraclerpc.NewPriceOracleClient(rpcConn) return nil } diff --git a/itest/test_list_on_test.go b/itest/test_list_on_test.go index 02f6dd0a0..a1db82168 100644 --- a/itest/test_list_on_test.go +++ b/itest/test_list_on_test.go @@ -336,6 +336,10 @@ var testCases = []*testCase{ name: "asset signing after lnd restore from seed", test: testRestoreLndFromSeed, }, + { + name: "price oracle proxy", + test: testPriceOracleProxy, + }, } var optionalTestCases = []*testCase{ diff --git a/perms/perms.go b/perms/perms.go index d1b74f7f2..91c382d14 100644 --- a/perms/perms.go +++ b/perms/perms.go @@ -311,6 +311,10 @@ var ( Entity: "assets", Action: "write", }}, + "/priceoraclerpc.PriceOracle/QueryAssetRates": {{ + Entity: "priceoracle", + Action: "read", + }}, } // defaultMacaroonWhitelist defines a default set of RPC endpoints that @@ -332,7 +336,8 @@ var ( // macaroon authentication. func MacaroonWhitelist(allowUniPublicAccessRead bool, allowUniPublicAccessWrite bool, allowPublicUniProofCourier bool, - allowPublicStats bool) map[string]struct{} { + allowPublicStats bool, allowPublicPriceOracle bool, +) map[string]struct{} { // Make a copy of the default whitelist. whitelist := make(map[string]struct{}) @@ -357,5 +362,10 @@ func MacaroonWhitelist(allowUniPublicAccessRead bool, whitelist["/universerpc.Universe/QueryEvents"] = struct{}{} } + // Conditionally add public price oracle RPC endpoints to the whitelist. + if allowPublicPriceOracle { + whitelist["/priceoraclerpc.PriceOracle/QueryAssetRates"] = struct{}{} // nolint: lll + } + return whitelist } diff --git a/rfq/mock.go b/rfq/mock.go index d384183c4..985f8c8a2 100644 --- a/rfq/mock.go +++ b/rfq/mock.go @@ -9,8 +9,11 @@ import ( "github.com/lightninglabs/taproot-assets/fn" "github.com/lightninglabs/taproot-assets/rfqmath" "github.com/lightninglabs/taproot-assets/rfqmsg" + "github.com/lightninglabs/taproot-assets/taprpc/priceoraclerpc" + oraclerpc "github.com/lightninglabs/taproot-assets/taprpc/priceoraclerpc" "github.com/lightningnetwork/lnd/lnwire" "github.com/stretchr/testify/mock" + "google.golang.org/grpc" ) // MockPriceOracle is a mock implementation of the PriceOracle interface. @@ -113,6 +116,34 @@ func (m *MockPriceOracle) QueryBidPrice(ctx context.Context, return resp, args.Error(1) } +// QueryAssetRates is a mock implementation of the QueryAssetRates method. +func (m *MockPriceOracle) QueryAssetRates(ctx context.Context, + in *priceoraclerpc.QueryAssetRatesRequest, opts ...grpc.CallOption, +) (*priceoraclerpc.QueryAssetRatesResponse, error) { + + // Unmarshal the subject asset to BTC rate. + rpcRate := &priceoraclerpc.FixedPoint{ + Coefficient: m.assetToBtcRate.Coefficient.String(), + Scale: uint32(m.assetToBtcRate.Scale), + } + + return &priceoraclerpc.QueryAssetRatesResponse{ + Result: &priceoraclerpc.QueryAssetRatesResponse_Ok{ + Ok: &priceoraclerpc.QueryAssetRatesOkResponse{ + AssetRates: &priceoraclerpc.AssetRates{ + SubjectAssetRate: rpcRate, + }, + }, + }, + }, nil +} + +// Client returns a client that can be used to interact with the mock price +// oracle. +func (m *MockPriceOracle) Client() oraclerpc.PriceOracleClient { + return m +} + // Ensure that MockPriceOracle implements the PriceOracle interface. var _ PriceOracle = (*MockPriceOracle)(nil) diff --git a/rfq/oracle.go b/rfq/oracle.go index 8528c6395..4409bcaf8 100644 --- a/rfq/oracle.go +++ b/rfq/oracle.go @@ -107,6 +107,9 @@ type PriceOracle interface { paymentMaxAmt fn.Option[lnwire.MilliSatoshi], assetRateHint fn.Option[rfqmsg.AssetRate]) ( *OracleResponse, error) + + // RawClient returns the underlying RPC client. + Client() oraclerpc.PriceOracleClient } // RpcPriceOracle is a price oracle that uses an external RPC server to get @@ -404,5 +407,10 @@ func (r *RpcPriceOracle) QueryBidPrice(ctx context.Context, } } +// Client returns the underlying RPC client. +func (r *RpcPriceOracle) Client() oraclerpc.PriceOracleClient { + return r.client +} + // Ensure that RpcPriceOracle implements the PriceOracle interface. var _ PriceOracle = (*RpcPriceOracle)(nil) diff --git a/rpcserver.go b/rpcserver.go index cdb471d39..630e66dbc 100644 --- a/rpcserver.go +++ b/rpcserver.go @@ -46,6 +46,7 @@ import ( "github.com/lightninglabs/taproot-assets/taprpc" wrpc "github.com/lightninglabs/taproot-assets/taprpc/assetwalletrpc" "github.com/lightninglabs/taproot-assets/taprpc/mintrpc" + "github.com/lightninglabs/taproot-assets/taprpc/priceoraclerpc" "github.com/lightninglabs/taproot-assets/taprpc/rfqrpc" tchrpc "github.com/lightninglabs/taproot-assets/taprpc/tapchannelrpc" "github.com/lightninglabs/taproot-assets/taprpc/tapdevrpc" @@ -70,6 +71,8 @@ import ( "golang.org/x/exp/maps" "golang.org/x/time/rate" "google.golang.org/grpc" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" ) var ( @@ -84,6 +87,11 @@ var ( // P2TRChangeType is the type of change address that should be used for // funding PSBTs, as we'll always want to use P2TR change addresses. P2TRChangeType = walletrpc.ChangeAddressType_CHANGE_ADDRESS_TYPE_P2TR + + // ErrPriceOracleUnimplemented is the error returned when the price + // oracle service is unimplemented. + ErrPriceOracleUnimplemented = status.Error( + codes.Unimplemented, "price oracle service is unimplemented") ) const ( @@ -161,6 +169,7 @@ type rpcServer struct { tchrpc.UnimplementedTaprootAssetChannelsServer tapdevrpc.UnimplementedTapDevServer unirpc.UnimplementedUniverseServer + priceoraclerpc.UnimplementedPriceOracleServer interceptor signal.Interceptor @@ -230,6 +239,8 @@ func (r *rpcServer) RegisterWithGrpcServer(grpcServer *grpc.Server) error { tchrpc.RegisterTaprootAssetChannelsServer(grpcServer, r) unirpc.RegisterUniverseServer(grpcServer, r) tapdevrpc.RegisterGrpcServer(grpcServer, r) + priceoraclerpc.RegisterPriceOracleServer(grpcServer, r) + return nil } @@ -8259,3 +8270,18 @@ func (r *rpcServer) RegisterTransfer(ctx context.Context, RegisteredAsset: rpcAsset, }, nil } + +// QueryAssetRates retrieves the exchange rate between a tap asset and BTC for +// a specified transaction type, subject asset, and payment asset. The asset +// rate represents the number of tap asset units per BTC. +// NOTE: This call is proxied to the underlying price oracle. +func (r *rpcServer) QueryAssetRates(ctx context.Context, + req *priceoraclerpc.QueryAssetRatesRequest) ( + *priceoraclerpc.QueryAssetRatesResponse, error) { + + if r.cfg.ProxyPriceOracle == nil { + return nil, ErrPriceOracleUnimplemented + } + + return r.cfg.ProxyPriceOracle.QueryAssetRates(ctx, req) +} diff --git a/sample-tapd.conf b/sample-tapd.conf index ba91f742b..dc6b26640 100644 --- a/sample-tapd.conf +++ b/sample-tapd.conf @@ -128,6 +128,9 @@ ; Disable macaroon authentication for stats RPC endpoints ; allow-public-stats=false +; Disble macaroon authentication for price oracle proxy RPC endpoints +; allow-public-price-oracle=false + ; Add an ip:port/hostname to allow cross origin access from ; To allow all origins, set as "*" ; restcors= diff --git a/server.go b/server.go index bede71108..61ee92c7c 100644 --- a/server.go +++ b/server.go @@ -315,6 +315,7 @@ func (s *Server) RunUntilShutdown(mainErrChan <-chan error) error { s.cfg.UniversePublicAccess.IsWriteAccessGranted(), s.cfg.RPCConfig.AllowPublicUniProofCourier, s.cfg.RPCConfig.AllowPublicStats, + s.cfg.RPCConfig.AllowPublicPriceOracle, ) // Create a new RPC interceptor that we'll add to the GRPC server. This diff --git a/tapcfg/config.go b/tapcfg/config.go index 4635de007..d7fd2282e 100644 --- a/tapcfg/config.go +++ b/tapcfg/config.go @@ -238,6 +238,7 @@ type RpcConfig struct { AllowPublicUniProofCourier bool `long:"allow-public-uni-proof-courier" description:"Disable macaroon authentication for universe proof courier RPC endpoints."` AllowPublicStats bool `long:"allow-public-stats" description:"Disable macaroon authentication for stats RPC endpoints."` + AllowPublicPriceOracle bool `long:"allow-public-price-oracle" description:"Disable macaroon authentication for price oracle RPC endpoints."` RestCORS []string `long:"restcors" description:"Add an ip:port/hostname to allow cross origin access from. To allow all origins, set as \"*\"."` diff --git a/tapcfg/server.go b/tapcfg/server.go index a91d51a75..9836276e4 100644 --- a/tapcfg/server.go +++ b/tapcfg/server.go @@ -20,6 +20,7 @@ import ( "github.com/lightninglabs/taproot-assets/tapdb/sqlc" "github.com/lightninglabs/taproot-assets/tapfreighter" "github.com/lightninglabs/taproot-assets/tapgarden" + "github.com/lightninglabs/taproot-assets/taprpc/priceoraclerpc" "github.com/lightninglabs/taproot-assets/tapscript" "github.com/lightninglabs/taproot-assets/universe" "github.com/lightningnetwork/lnd" @@ -366,6 +367,7 @@ func genServerConfig(cfg *Config, cfgLogger btclog.Logger, // Determine whether we should use the mock price oracle service or a // real price oracle service. var priceOracle rfq.PriceOracle + var rpcPriceOracleClient priceoraclerpc.PriceOracleClient rfqCfg := cfg.Experimental.Rfq switch rfqCfg.PriceOracleAddress { @@ -381,6 +383,7 @@ func genServerConfig(cfg *Config, cfgLogger btclog.Logger, 3600, rfqCfg.MockOracleSatsPerAsset, ) } + rpcPriceOracleClient = priceOracle.Client() case "": // Leave the price oracle as nil, which will cause the RFQ @@ -395,6 +398,7 @@ func genServerConfig(cfg *Config, cfgLogger btclog.Logger, return nil, fmt.Errorf("unable to create price "+ "oracle: %w", err) } + rpcPriceOracleClient = priceOracle.Client() } // Construct the RFQ manager. @@ -590,6 +594,7 @@ func genServerConfig(cfg *Config, cfgLogger btclog.Logger, UniverseQueriesBurst: cfg.Universe.UniverseQueriesBurst, RfqManager: rfqManager, PriceOracle: priceOracle, + ProxyPriceOracle: rpcPriceOracleClient, AuxLeafSigner: auxLeafSigner, AuxFundingController: auxFundingController, AuxChanCloser: auxChanCloser,