Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit 00fbd82

Browse files
committedMar 7, 2025··
tapas_loopout
1 parent 800f0e0 commit 00fbd82

35 files changed

+6367
-10
lines changed
 

‎assets/actions.go

Lines changed: 421 additions & 0 deletions
Large diffs are not rendered by default.

‎assets/client.go

Lines changed: 244 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,18 +8,31 @@ import (
88
"sync"
99
"time"
1010

11+
"bytes"
12+
1113
"github.com/btcsuite/btcd/btcutil"
14+
"github.com/btcsuite/btcd/btcutil/psbt"
15+
"github.com/lightninglabs/taproot-assets/asset"
1216
"github.com/lightninglabs/taproot-assets/tapcfg"
17+
"github.com/lightninglabs/taproot-assets/tappsbt"
1318
"github.com/lightninglabs/taproot-assets/taprpc"
19+
wrpc "github.com/lightninglabs/taproot-assets/taprpc/assetwalletrpc"
20+
"github.com/lightninglabs/taproot-assets/taprpc/mintrpc"
1421
"github.com/lightninglabs/taproot-assets/taprpc/priceoraclerpc"
1522
"github.com/lightninglabs/taproot-assets/taprpc/rfqrpc"
1623
"github.com/lightninglabs/taproot-assets/taprpc/tapchannelrpc"
24+
"github.com/lightninglabs/taproot-assets/taprpc/tapdevrpc"
1725
"github.com/lightninglabs/taproot-assets/taprpc/universerpc"
26+
"github.com/lightninglabs/taproot-assets/tapsend"
27+
"github.com/lightningnetwork/lnd/keychain"
1828
"github.com/lightningnetwork/lnd/lnrpc"
29+
"github.com/lightningnetwork/lnd/lnwallet/chainfee"
1930
"github.com/lightningnetwork/lnd/lnwire"
2031
"github.com/lightningnetwork/lnd/macaroons"
2132
"google.golang.org/grpc"
33+
"google.golang.org/grpc/codes"
2234
"google.golang.org/grpc/credentials"
35+
"google.golang.org/grpc/status"
2336
"gopkg.in/macaroon.v2"
2437
)
2538

@@ -63,7 +76,10 @@ type TapdClient struct {
6376
tapchannelrpc.TaprootAssetChannelsClient
6477
priceoraclerpc.PriceOracleClient
6578
rfqrpc.RfqClient
79+
wrpc.AssetWalletClient
80+
mintrpc.MintClient
6681
universerpc.UniverseClient
82+
tapdevrpc.TapDevClient
6783

6884
cfg *TapdConfig
6985
assetNameCache map[string]string
@@ -242,3 +258,231 @@ func getClientConn(config *TapdConfig) (*grpc.ClientConn, error) {
242258

243259
return conn, nil
244260
}
261+
262+
// FundAndSignVpacket funds ands signs a vpacket.
263+
func (t *TapdClient) FundAndSignVpacket(ctx context.Context,
264+
vpkt *tappsbt.VPacket) (*tappsbt.VPacket, error) {
265+
266+
// Fund the packet.
267+
var buf bytes.Buffer
268+
err := vpkt.Serialize(&buf)
269+
if err != nil {
270+
return nil, err
271+
}
272+
273+
fundResp, err := t.FundVirtualPsbt(
274+
ctx, &wrpc.FundVirtualPsbtRequest{
275+
Template: &wrpc.FundVirtualPsbtRequest_Psbt{
276+
Psbt: buf.Bytes(),
277+
},
278+
},
279+
)
280+
if err != nil {
281+
return nil, err
282+
}
283+
284+
// Sign the packet.
285+
signResp, err := t.SignVirtualPsbt(
286+
ctx, &wrpc.SignVirtualPsbtRequest{
287+
FundedPsbt: fundResp.FundedPsbt,
288+
},
289+
)
290+
if err != nil {
291+
return nil, err
292+
}
293+
294+
return tappsbt.NewFromRawBytes(
295+
bytes.NewReader(signResp.SignedPsbt), false,
296+
)
297+
}
298+
299+
// PrepareAndCommitVirtualPsbts prepares and commits virtual psbts.
300+
func (t *TapdClient) PrepareAndCommitVirtualPsbts(ctx context.Context,
301+
vpkt *tappsbt.VPacket, feeRateSatPerKVByte chainfee.SatPerVByte) (
302+
*psbt.Packet, []*tappsbt.VPacket, []*tappsbt.VPacket,
303+
*wrpc.CommitVirtualPsbtsResponse, error) {
304+
305+
htlcVPackets, err := tappsbt.Encode(vpkt)
306+
if err != nil {
307+
return nil, nil, nil, nil, err
308+
}
309+
310+
htlcBtcPkt, err := tapsend.PrepareAnchoringTemplate(
311+
[]*tappsbt.VPacket{vpkt},
312+
)
313+
if err != nil {
314+
return nil, nil, nil, nil, err
315+
}
316+
317+
var buf bytes.Buffer
318+
err = htlcBtcPkt.Serialize(&buf)
319+
if err != nil {
320+
return nil, nil, nil, nil, err
321+
}
322+
323+
commitResponse, err := t.AssetWalletClient.CommitVirtualPsbts(
324+
ctx, &wrpc.CommitVirtualPsbtsRequest{
325+
AnchorPsbt: buf.Bytes(),
326+
Fees: &wrpc.CommitVirtualPsbtsRequest_SatPerVbyte{
327+
SatPerVbyte: uint64(feeRateSatPerKVByte),
328+
},
329+
AnchorChangeOutput: &wrpc.CommitVirtualPsbtsRequest_Add{
330+
Add: true,
331+
},
332+
VirtualPsbts: [][]byte{
333+
htlcVPackets,
334+
},
335+
},
336+
)
337+
if err != nil {
338+
return nil, nil, nil, nil, err
339+
}
340+
341+
fundedPacket, err := psbt.NewFromRawBytes(
342+
bytes.NewReader(commitResponse.AnchorPsbt), false,
343+
)
344+
if err != nil {
345+
return nil, nil, nil, nil, err
346+
}
347+
348+
activePackets := make(
349+
[]*tappsbt.VPacket, len(commitResponse.VirtualPsbts),
350+
)
351+
for idx := range commitResponse.VirtualPsbts {
352+
activePackets[idx], err = tappsbt.Decode(
353+
commitResponse.VirtualPsbts[idx],
354+
)
355+
if err != nil {
356+
return nil, nil, nil, nil, err
357+
}
358+
}
359+
360+
passivePackets := make(
361+
[]*tappsbt.VPacket, len(commitResponse.PassiveAssetPsbts),
362+
)
363+
for idx := range commitResponse.PassiveAssetPsbts {
364+
passivePackets[idx], err = tappsbt.Decode(
365+
commitResponse.PassiveAssetPsbts[idx],
366+
)
367+
if err != nil {
368+
return nil, nil, nil, nil, err
369+
}
370+
}
371+
372+
return fundedPacket, activePackets, passivePackets, commitResponse, nil
373+
}
374+
375+
// LogAndPublish logs and publishes the virtual psbts.
376+
func (t *TapdClient) LogAndPublish(ctx context.Context, btcPkt *psbt.Packet,
377+
activeAssets []*tappsbt.VPacket, passiveAssets []*tappsbt.VPacket,
378+
commitResp *wrpc.CommitVirtualPsbtsResponse) (*taprpc.SendAssetResponse,
379+
error) {
380+
381+
var buf bytes.Buffer
382+
err := btcPkt.Serialize(&buf)
383+
if err != nil {
384+
return nil, err
385+
}
386+
387+
request := &wrpc.PublishAndLogRequest{
388+
AnchorPsbt: buf.Bytes(),
389+
VirtualPsbts: make([][]byte, len(activeAssets)),
390+
PassiveAssetPsbts: make([][]byte, len(passiveAssets)),
391+
ChangeOutputIndex: commitResp.ChangeOutputIndex,
392+
LndLockedUtxos: commitResp.LndLockedUtxos,
393+
}
394+
395+
for idx := range activeAssets {
396+
request.VirtualPsbts[idx], err = tappsbt.Encode(
397+
activeAssets[idx],
398+
)
399+
if err != nil {
400+
return nil, err
401+
}
402+
}
403+
for idx := range passiveAssets {
404+
request.PassiveAssetPsbts[idx], err = tappsbt.Encode(
405+
passiveAssets[idx],
406+
)
407+
if err != nil {
408+
return nil, err
409+
}
410+
}
411+
412+
resp, err := t.PublishAndLogTransfer(ctx, request)
413+
if err != nil {
414+
return nil, err
415+
}
416+
417+
return resp, nil
418+
}
419+
420+
// CheckBalanceById checks the balance of an asset by its id.
421+
func (t *TapdClient) CheckBalanceById(ctx context.Context, assetId []byte,
422+
requestedBalance btcutil.Amount) error {
423+
424+
// Check if we have enough funds to do the swap.
425+
balanceResp, err := t.ListBalances(
426+
ctx, &taprpc.ListBalancesRequest{
427+
GroupBy: &taprpc.ListBalancesRequest_AssetId{
428+
AssetId: true,
429+
},
430+
AssetFilter: assetId,
431+
},
432+
)
433+
if err != nil {
434+
return err
435+
}
436+
437+
// Check if we have enough funds to do the swap.
438+
balance, ok := balanceResp.AssetBalances[hex.EncodeToString(
439+
assetId,
440+
)]
441+
if !ok {
442+
return status.Error(
443+
codes.Internal, "internal error",
444+
)
445+
}
446+
if balance.Balance < uint64(requestedBalance) {
447+
return status.Error(
448+
codes.Internal, "internal error",
449+
)
450+
}
451+
452+
return nil
453+
}
454+
455+
// DeriveNewKeys derives a new internal and script key.
456+
func (t *TapdClient) DeriveNewKeys(ctx context.Context) (asset.ScriptKey,
457+
keychain.KeyDescriptor, error) {
458+
scriptKeyDesc, err := t.NextScriptKey(
459+
ctx, &wrpc.NextScriptKeyRequest{
460+
KeyFamily: uint32(asset.TaprootAssetsKeyFamily),
461+
},
462+
)
463+
if err != nil {
464+
return asset.ScriptKey{}, keychain.KeyDescriptor{}, err
465+
}
466+
467+
scriptKey, err := taprpc.UnmarshalScriptKey(scriptKeyDesc.ScriptKey)
468+
if err != nil {
469+
return asset.ScriptKey{}, keychain.KeyDescriptor{}, err
470+
}
471+
472+
internalKeyDesc, err := t.NextInternalKey(
473+
ctx, &wrpc.NextInternalKeyRequest{
474+
KeyFamily: uint32(asset.TaprootAssetsKeyFamily),
475+
},
476+
)
477+
if err != nil {
478+
return asset.ScriptKey{}, keychain.KeyDescriptor{}, err
479+
}
480+
internalKeyLnd, err := taprpc.UnmarshalKeyDescriptor(
481+
internalKeyDesc.InternalKey,
482+
)
483+
if err != nil {
484+
return asset.ScriptKey{}, keychain.KeyDescriptor{}, err
485+
}
486+
487+
return *scriptKey, internalKeyLnd, nil
488+
}

‎assets/interfaces.go

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
package assets
2+
3+
import (
4+
"context"
5+
6+
"github.com/btcsuite/btcd/btcutil"
7+
"github.com/btcsuite/btcd/btcutil/psbt"
8+
"github.com/btcsuite/btcd/chaincfg/chainhash"
9+
"github.com/btcsuite/btcd/wire"
10+
"github.com/lightninglabs/lndclient"
11+
"github.com/lightninglabs/loop/fsm"
12+
"github.com/lightninglabs/taproot-assets/asset"
13+
"github.com/lightninglabs/taproot-assets/tappsbt"
14+
"github.com/lightninglabs/taproot-assets/taprpc"
15+
wrpc "github.com/lightninglabs/taproot-assets/taprpc/assetwalletrpc"
16+
"github.com/lightninglabs/taproot-assets/taprpc/mintrpc"
17+
"github.com/lightninglabs/taproot-assets/taprpc/tapdevrpc"
18+
"github.com/lightninglabs/taproot-assets/taprpc/universerpc"
19+
"github.com/lightningnetwork/lnd/chainntnfs"
20+
"github.com/lightningnetwork/lnd/keychain"
21+
"github.com/lightningnetwork/lnd/lntypes"
22+
"github.com/lightningnetwork/lnd/lnwallet/chainfee"
23+
)
24+
25+
const (
26+
// DefaultSwapCSVExpiry is the default expiry for a swap in blocks.
27+
DefaultSwapCSVExpiry = int32(24)
28+
29+
defaultHtlcFeeConfTarget = 3
30+
defaultHtlcConfRequirement = 2
31+
32+
AssetKeyFamily = 696969
33+
)
34+
35+
// TapdClient is an interface that groups the methods required to interact with
36+
// the taproot-assets server and the wallet.
37+
type AssetClient interface {
38+
taprpc.TaprootAssetsClient
39+
wrpc.AssetWalletClient
40+
mintrpc.MintClient
41+
universerpc.UniverseClient
42+
tapdevrpc.TapDevClient
43+
44+
// FundAndSignVpacket funds ands signs a vpacket.
45+
FundAndSignVpacket(ctx context.Context,
46+
vpkt *tappsbt.VPacket) (*tappsbt.VPacket, error)
47+
48+
// PrepareAndCommitVirtualPsbts prepares and commits virtual psbts.
49+
PrepareAndCommitVirtualPsbts(ctx context.Context,
50+
vpkt *tappsbt.VPacket, feeRateSatPerKVByte chainfee.SatPerVByte) (
51+
*psbt.Packet, []*tappsbt.VPacket, []*tappsbt.VPacket,
52+
*wrpc.CommitVirtualPsbtsResponse, error)
53+
54+
// LogAndPublish logs and publishes the virtual psbts.
55+
LogAndPublish(ctx context.Context, btcPkt *psbt.Packet,
56+
activeAssets []*tappsbt.VPacket, passiveAssets []*tappsbt.VPacket,
57+
commitResp *wrpc.CommitVirtualPsbtsResponse) (*taprpc.SendAssetResponse,
58+
error)
59+
60+
// CheckBalanceById checks the balance of an asset by its id.
61+
CheckBalanceById(ctx context.Context, assetId []byte,
62+
requestedBalance btcutil.Amount) error
63+
64+
// DeriveNewKeys derives a new internal and script key.
65+
DeriveNewKeys(ctx context.Context) (asset.ScriptKey,
66+
keychain.KeyDescriptor, error)
67+
}
68+
69+
// SwapStore is an interface that groups the methods required to store swap
70+
// information.
71+
type SwapStore interface {
72+
// CreateAssetSwapOut creates a new swap out in the store.
73+
CreateAssetSwapOut(ctx context.Context, swap *SwapOut) error
74+
75+
// UpdateAssetSwapHtlcOutpoint updates the htlc outpoint of a swap out.
76+
UpdateAssetSwapHtlcOutpoint(ctx context.Context, swapHash lntypes.Hash,
77+
outpoint *wire.OutPoint, confirmationHeight int32) error
78+
79+
// UpdateAssetSwapOutProof updates the proof of a swap out.
80+
UpdateAssetSwapOutProof(ctx context.Context, swapHash lntypes.Hash,
81+
rawProof []byte) error
82+
83+
// UpdateAssetSwapOutSweepTx updates the sweep tx of a swap out.
84+
UpdateAssetSwapOutSweepTx(ctx context.Context,
85+
swapHash lntypes.Hash, sweepTxid chainhash.Hash,
86+
confHeight int32, sweepPkscript []byte) error
87+
88+
// InsertAssetSwapUpdate inserts a new swap update in the store.
89+
InsertAssetSwapUpdate(ctx context.Context,
90+
swapHash lntypes.Hash, state fsm.StateType) error
91+
92+
UpdateAssetSwapOutPreimage(ctx context.Context,
93+
swapHash lntypes.Hash, preimage lntypes.Preimage) error
94+
}
95+
96+
// BlockHeightSubscriber is responsible for subscribing to the expiry height
97+
// of a swap, as well as getting the current block height.
98+
type BlockHeightSubscriber interface {
99+
// SubscribeExpiry subscribes to the expiry of a swap. It returns true
100+
// if the expiry is already past. Otherwise, it returns false and calls
101+
// the expiryFunc when the expiry height is reached.
102+
SubscribeExpiry(swapHash [32]byte,
103+
expiryHeight int32, expiryFunc func()) bool
104+
// GetBlockHeight returns the current block height.
105+
GetBlockHeight() int32
106+
}
107+
108+
// InvoiceSubscriber is responsible for subscribing to an invoice.
109+
type InvoiceSubscriber interface {
110+
// SubscribeInvoice subscribes to an invoice. The update callback is
111+
// called when the invoice is updated and the error callback is called
112+
// when an error occurs.
113+
SubscribeInvoice(ctx context.Context, invoiceHash lntypes.Hash,
114+
updateCallback func(lndclient.InvoiceUpdate, error)) error
115+
}
116+
117+
// TxConfirmationSubscriber is responsible for subscribing to the confirmation
118+
// of a transaction.
119+
type TxConfirmationSubscriber interface {
120+
121+
// SubscribeTxConfirmation subscribes to the confirmation of a
122+
// pkscript on the chain. The callback is called when the pkscript is
123+
// confirmed or when an error occurs.
124+
SubscribeTxConfirmation(ctx context.Context, swapHash lntypes.Hash,
125+
txid *chainhash.Hash, pkscript []byte, numConfs int32,
126+
eightHint int32, cb func(*chainntnfs.TxConfirmation, error)) error
127+
}
128+
129+
// ExchangeRateProvider is responsible for providing the exchange rate between
130+
// assets.
131+
type ExchangeRateProvider interface {
132+
// GetSatsPerAssetUnit returns the amount of satoshis per asset unit.
133+
GetSatsPerAssetUnit(assetId []byte) (btcutil.Amount, error)
134+
}

‎assets/log.go

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
package assets
2+
3+
import (
4+
"github.com/btcsuite/btclog"
5+
"github.com/lightningnetwork/lnd/build"
6+
)
7+
8+
// Subsystem defines the sub system name of this package.
9+
const Subsystem = "ASSETS"
10+
11+
// log is a logger that is initialized with no output filters. This means the
12+
// package will not perform any logging by default until the caller requests
13+
// it.
14+
var log btclog.Logger
15+
16+
// The default amount of logging is none.
17+
func init() {
18+
UseLogger(build.NewSubLogger(Subsystem, nil))
19+
}
20+
21+
// UseLogger uses a specified Logger to output package logging info. This
22+
// should be used in preference to SetLogWriter if the caller is also using
23+
// btclog.
24+
func UseLogger(logger btclog.Logger) {
25+
log = logger
26+
}

‎assets/manager.go

Lines changed: 203 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,203 @@
1+
package assets
2+
3+
import (
4+
"context"
5+
"sync"
6+
"time"
7+
8+
"github.com/btcsuite/btcd/btcutil"
9+
"github.com/lightninglabs/lndclient"
10+
"github.com/lightninglabs/loop/fsm"
11+
loop_rpc "github.com/lightninglabs/loop/swapserverrpc"
12+
"github.com/lightninglabs/loop/utils"
13+
"github.com/lightninglabs/taproot-assets/taprpc"
14+
"github.com/lightningnetwork/lnd/lntypes"
15+
)
16+
17+
const (
18+
ClientKeyFamily = 696969
19+
)
20+
21+
type Config struct {
22+
AssetClient *TapdClient
23+
Wallet lndclient.WalletKitClient
24+
// ExchangeRateProvider is the exchange rate provider.
25+
ExchangeRateProvider *FixedExchangeRateProvider
26+
Signer lndclient.SignerClient
27+
ChainNotifier lndclient.ChainNotifierClient
28+
Router lndclient.RouterClient
29+
LndClient lndclient.LightningClient
30+
Store *PostgresStore
31+
ServerClient loop_rpc.AssetsSwapServerClient
32+
}
33+
34+
type AssetsSwapManager struct {
35+
cfg *Config
36+
37+
expiryManager *utils.ExpiryManager
38+
txConfManager *utils.TxSubscribeConfirmationManager
39+
40+
blockHeight int32
41+
runCtx context.Context
42+
activeSwapOuts map[lntypes.Hash]*OutFSM
43+
44+
sync.Mutex
45+
}
46+
47+
func NewAssetSwapServer(config *Config) *AssetsSwapManager {
48+
return &AssetsSwapManager{
49+
cfg: config,
50+
51+
activeSwapOuts: make(map[lntypes.Hash]*OutFSM),
52+
}
53+
}
54+
55+
func (m *AssetsSwapManager) Run(ctx context.Context, blockHeight int32) error {
56+
m.runCtx = ctx
57+
m.blockHeight = blockHeight
58+
59+
// Get our tapd client info.
60+
tapdInfo, err := m.cfg.AssetClient.GetInfo(
61+
ctx, &taprpc.GetInfoRequest{},
62+
)
63+
if err != nil {
64+
return err
65+
}
66+
log.Infof("Tapd info: %v", tapdInfo)
67+
68+
// Create our subscriptionManagers.
69+
m.expiryManager = utils.NewExpiryManager(m.cfg.ChainNotifier)
70+
m.txConfManager = utils.NewTxSubscribeConfirmationManager(
71+
m.cfg.ChainNotifier,
72+
)
73+
74+
// Start the expiry manager.
75+
errChan := make(chan error, 1)
76+
wg := &sync.WaitGroup{}
77+
wg.Add(1)
78+
go func() {
79+
defer wg.Done()
80+
err := m.expiryManager.Start(ctx, blockHeight)
81+
if err != nil {
82+
log.Errorf("Expiry manager failed: %v", err)
83+
errChan <- err
84+
log.Errorf("Gude1")
85+
}
86+
}()
87+
88+
// Recover all the active asset swap outs from the database.
89+
err = m.recoverSwapOuts(ctx)
90+
if err != nil {
91+
return err
92+
}
93+
94+
for {
95+
select {
96+
case err := <-errChan:
97+
log.Errorf("Gude2")
98+
return err
99+
100+
case <-ctx.Done():
101+
log.Errorf("Gude3")
102+
// wg.Wait()
103+
log.Errorf("Gude4")
104+
return nil
105+
}
106+
}
107+
}
108+
109+
func (m *AssetsSwapManager) NewSwapOut(ctx context.Context,
110+
amt btcutil.Amount, asset []byte) (*OutFSM, error) {
111+
112+
// Create a new out fsm.
113+
outFSM := NewOutFSM(m.runCtx, m.getFSMOutConfig())
114+
115+
// Send the initial event to the fsm.
116+
err := outFSM.SendEvent(
117+
ctx, OnRequestAssetOut, &InitSwapOutContext{
118+
Amount: amt,
119+
AssetId: asset,
120+
},
121+
)
122+
if err != nil {
123+
return nil, err
124+
}
125+
// Check if the fsm has an error.
126+
if outFSM.LastActionError != nil {
127+
return nil, outFSM.LastActionError
128+
}
129+
130+
// Wait for the fsm to be in the state we expect.
131+
err = outFSM.DefaultObserver.WaitForState(
132+
ctx, time.Second*15, PayPrepay,
133+
fsm.WithAbortEarlyOnErrorOption(),
134+
)
135+
if err != nil {
136+
return nil, err
137+
}
138+
139+
// Add the swap to the active swap outs.
140+
m.Lock()
141+
m.activeSwapOuts[outFSM.SwapOut.SwapHash] = outFSM
142+
m.Unlock()
143+
144+
return outFSM, nil
145+
}
146+
147+
// recoverSwapOuts recovers all the active asset swap outs from the database.
148+
func (m *AssetsSwapManager) recoverSwapOuts(ctx context.Context) error {
149+
// Fetch all the active asset swap outs from the database.
150+
activeSwapOuts, err := m.cfg.Store.GetActiveAssetOuts(ctx)
151+
if err != nil {
152+
return err
153+
}
154+
155+
for _, swapOut := range activeSwapOuts {
156+
log.Debugf("Recovering asset out %v with state %v",
157+
swapOut.SwapHash, swapOut.State)
158+
159+
swapOutFSM := NewOutFSMFromSwap(
160+
ctx, m.getFSMOutConfig(), swapOut,
161+
)
162+
163+
m.Lock()
164+
m.activeSwapOuts[swapOut.SwapHash] = swapOutFSM
165+
m.Unlock()
166+
167+
// As SendEvent can block, we'll start a goroutine to process
168+
// the event.
169+
go func() {
170+
err := swapOutFSM.SendEvent(ctx, OnRecover, nil)
171+
if err != nil {
172+
log.Errorf("FSM %v Error sending recover "+
173+
"event %v, state: %v",
174+
swapOutFSM.SwapOut.SwapHash,
175+
err, swapOutFSM.SwapOut.State)
176+
}
177+
}()
178+
}
179+
180+
return nil
181+
}
182+
183+
// getFSMOutConfig returns a fsmconfig from the manager.
184+
func (m *AssetsSwapManager) getFSMOutConfig() *FSMConfig {
185+
return &FSMConfig{
186+
TapdClient: m.cfg.AssetClient,
187+
AssetClient: m.cfg.ServerClient,
188+
BlockHeightSubscriber: m.expiryManager,
189+
TxConfSubscriber: m.txConfManager,
190+
ExchangeRateProvider: m.cfg.ExchangeRateProvider,
191+
Wallet: m.cfg.Wallet,
192+
Router: m.cfg.Router,
193+
194+
Store: m.cfg.Store,
195+
Signer: m.cfg.Signer,
196+
}
197+
}
198+
199+
func (m *AssetsSwapManager) ListSwapOutoutputs(ctx context.Context) ([]*SwapOut,
200+
error) {
201+
202+
return m.cfg.Store.GetAllAssetOuts(ctx)
203+
}

‎assets/out_fsm.go

Lines changed: 612 additions & 0 deletions
Large diffs are not rendered by default.

‎assets/rateprovider.go

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package assets
2+
3+
import "github.com/btcsuite/btcd/btcutil"
4+
5+
const (
6+
fixedPrice = 100
7+
)
8+
9+
// FixedExchangeRateProvider is a fixed exchange rate provider.
10+
type FixedExchangeRateProvider struct {
11+
price btcutil.Amount
12+
}
13+
14+
// NewFixedExchangeRateProvider creates a new fixed exchange rate provider.
15+
func NewFixedExchangeRateProvider() *FixedExchangeRateProvider {
16+
return &FixedExchangeRateProvider{}
17+
}
18+
19+
// GetSatsPerAssetUnit returns the fixed price in sats per asset unit.
20+
func (e *FixedExchangeRateProvider) GetSatsPerAssetUnit(assetId []byte) (
21+
btcutil.Amount, error) {
22+
23+
return btcutil.Amount(fixedPrice), nil
24+
}

‎assets/script.go

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
package assets
2+
3+
import (
4+
"github.com/btcsuite/btcd/btcec/v2"
5+
"github.com/btcsuite/btcd/btcec/v2/schnorr"
6+
"github.com/btcsuite/btcd/txscript"
7+
"github.com/decred/dcrd/dcrec/secp256k1/v4"
8+
"github.com/lightninglabs/taproot-assets/asset"
9+
"github.com/lightningnetwork/lnd/input"
10+
"github.com/lightningnetwork/lnd/keychain"
11+
"github.com/lightningnetwork/lnd/lntypes"
12+
)
13+
14+
// GenSuccessPathScript constructs an HtlcScript for the success payment path.
15+
func GenSuccessPathScript(receiverHtlcKey *btcec.PublicKey,
16+
swapHash lntypes.Hash) ([]byte, error) {
17+
18+
builder := txscript.NewScriptBuilder()
19+
20+
builder.AddData(schnorr.SerializePubKey(receiverHtlcKey))
21+
builder.AddOp(txscript.OP_CHECKSIGVERIFY)
22+
builder.AddOp(txscript.OP_SIZE)
23+
builder.AddInt64(32)
24+
builder.AddOp(txscript.OP_EQUALVERIFY)
25+
builder.AddOp(txscript.OP_HASH160)
26+
builder.AddData(input.Ripemd160H(swapHash[:]))
27+
builder.AddOp(txscript.OP_EQUALVERIFY)
28+
builder.AddInt64(1)
29+
builder.AddOp(txscript.OP_CHECKSEQUENCEVERIFY)
30+
31+
return builder.Script()
32+
}
33+
34+
// GenTimeoutPathScript constructs an HtlcScript for the timeout payment path.
35+
func GenTimeoutPathScript(senderHtlcKey *btcec.PublicKey, cltvExpiry int64) (
36+
[]byte, error) {
37+
38+
builder := txscript.NewScriptBuilder()
39+
builder.AddData(schnorr.SerializePubKey(senderHtlcKey))
40+
builder.AddOp(txscript.OP_CHECKSIGVERIFY)
41+
builder.AddInt64(cltvExpiry)
42+
builder.AddOp(txscript.OP_CHECKSEQUENCEVERIFY)
43+
return builder.Script()
44+
}
45+
46+
// GetOpTrueScript returns a script that always evaluates to true.
47+
func GetOpTrueScript() ([]byte, error) {
48+
return txscript.NewScriptBuilder().AddOp(txscript.OP_TRUE).Script()
49+
}
50+
51+
// createOpTrueLeaf creates a taproot leaf that always evaluates to true.
52+
func createOpTrueLeaf() (asset.ScriptKey, txscript.TapLeaf,
53+
*txscript.IndexedTapScriptTree, *txscript.ControlBlock, error) {
54+
55+
// Create the taproot OP_TRUE script.
56+
tapScript, err := GetOpTrueScript()
57+
if err != nil {
58+
return asset.ScriptKey{}, txscript.TapLeaf{}, nil, nil, err
59+
}
60+
61+
tapLeaf := txscript.NewBaseTapLeaf(tapScript)
62+
tree := txscript.AssembleTaprootScriptTree(tapLeaf)
63+
rootHash := tree.RootNode.TapHash()
64+
tapKey := txscript.ComputeTaprootOutputKey(asset.NUMSPubKey, rootHash[:])
65+
66+
merkleRootHash := tree.RootNode.TapHash()
67+
68+
controlBlock := &txscript.ControlBlock{
69+
LeafVersion: txscript.BaseLeafVersion,
70+
InternalKey: asset.NUMSPubKey,
71+
}
72+
tapScriptKey := asset.ScriptKey{
73+
PubKey: tapKey,
74+
TweakedScriptKey: &asset.TweakedScriptKey{
75+
RawKey: keychain.KeyDescriptor{
76+
PubKey: asset.NUMSPubKey,
77+
},
78+
Tweak: merkleRootHash[:],
79+
},
80+
}
81+
if tapKey.SerializeCompressed()[0] ==
82+
secp256k1.PubKeyFormatCompressedOdd {
83+
84+
controlBlock.OutputKeyYIsOdd = true
85+
}
86+
87+
return tapScriptKey, tapLeaf, tree, controlBlock, nil
88+
}

‎assets/server.go

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
package assets
2+
3+
import (
4+
"context"
5+
6+
"github.com/btcsuite/btcd/btcutil"
7+
clientrpc "github.com/lightninglabs/loop/looprpc"
8+
"github.com/lightninglabs/loop/swapserverrpc"
9+
"github.com/lightninglabs/taproot-assets/taprpc/universerpc"
10+
)
11+
12+
type AssetsClientServer struct {
13+
manager *AssetsSwapManager
14+
15+
clientrpc.UnimplementedAssetsClientServer
16+
}
17+
18+
func NewAssetsServer(manager *AssetsSwapManager) *AssetsClientServer {
19+
return &AssetsClientServer{
20+
manager: manager,
21+
}
22+
}
23+
24+
func (a *AssetsClientServer) SwapOut(ctx context.Context,
25+
req *clientrpc.SwapOutRequest) (*clientrpc.SwapOutResponse, error) {
26+
27+
swap, err := a.manager.NewSwapOut(
28+
ctx, btcutil.Amount(req.Amt), req.Asset,
29+
)
30+
if err != nil {
31+
return nil, err
32+
}
33+
return &clientrpc.SwapOutResponse{
34+
SwapStatus: &clientrpc.AssetSwapStatus{
35+
SwapHash: swap.SwapOut.SwapHash[:],
36+
SwapStatus: string(swap.SwapOut.State),
37+
},
38+
}, nil
39+
}
40+
41+
func (a *AssetsClientServer) ListAssetSwaps(ctx context.Context,
42+
_ *clientrpc.ListAssetSwapsRequest) (*clientrpc.ListAssetSwapsResponse,
43+
error) {
44+
45+
swaps, err := a.manager.ListSwapOutoutputs(ctx)
46+
if err != nil {
47+
return nil, err
48+
}
49+
50+
rpcSwaps := make([]*clientrpc.AssetSwapStatus, 0, len(swaps))
51+
for _, swap := range swaps {
52+
rpcSwaps = append(rpcSwaps, &clientrpc.AssetSwapStatus{
53+
SwapHash: swap.SwapHash[:],
54+
SwapStatus: string(swap.State),
55+
})
56+
}
57+
58+
return &clientrpc.ListAssetSwapsResponse{
59+
SwapStatus: rpcSwaps,
60+
}, nil
61+
}
62+
63+
func (a *AssetsClientServer) ClientListAvailableAssets(ctx context.Context,
64+
req *clientrpc.ClientListAvailableAssetsRequest,
65+
) (*clientrpc.ClientListAvailableAssetsResponse, error) {
66+
67+
assets, err := a.manager.cfg.ServerClient.ListAvailableAssets(
68+
ctx, &swapserverrpc.ListAvailableAssetsRequest{},
69+
)
70+
if err != nil {
71+
return nil, err
72+
}
73+
74+
availableAssets := make([]*clientrpc.Asset, 0, len(assets.Assets))
75+
76+
for _, asset := range assets.Assets {
77+
asset := asset
78+
clientAsset := &clientrpc.Asset{
79+
AssetId: asset.AssetId,
80+
SatsPerUnit: asset.CurrentSatsPerAssetUnit,
81+
Name: "Asset unknown in known universes",
82+
}
83+
universeRes, err := a.manager.cfg.AssetClient.QueryAssetRoots(
84+
ctx, &universerpc.AssetRootQuery{
85+
Id: &universerpc.ID{
86+
Id: &universerpc.ID_AssetId{
87+
AssetId: asset.AssetId,
88+
},
89+
ProofType: universerpc.ProofType_PROOF_TYPE_ISSUANCE,
90+
},
91+
},
92+
)
93+
if err != nil {
94+
return nil, err
95+
}
96+
97+
if universeRes.IssuanceRoot != nil {
98+
clientAsset.Name = universeRes.IssuanceRoot.AssetName
99+
}
100+
101+
availableAssets = append(availableAssets, clientAsset)
102+
}
103+
104+
return &clientrpc.ClientListAvailableAssetsResponse{
105+
AvailableAssets: availableAssets,
106+
}, nil
107+
}
108+
func (a *AssetsClientServer) ClientGetAssetSwapOutQuote(ctx context.Context,
109+
req *clientrpc.ClientGetAssetSwapOutQuoteRequest,
110+
) (*clientrpc.ClientGetAssetSwapOutQuoteResponse, error) {
111+
112+
// Get the quote from the server.
113+
quoteRes, err := a.manager.cfg.ServerClient.QuoteAssetLoopOut(
114+
ctx, &swapserverrpc.QuoteAssetLoopOutRequest{
115+
Amount: req.Amt,
116+
Asset: req.Asset,
117+
},
118+
)
119+
if err != nil {
120+
return nil, err
121+
}
122+
123+
return &clientrpc.ClientGetAssetSwapOutQuoteResponse{
124+
SwapFee: quoteRes.SwapFeeRate,
125+
PrepayAmt: quoteRes.FixedPrepayAmt,
126+
SatsPerUnit: quoteRes.CurrentSatsPerAssetUnit,
127+
}, nil
128+
}

‎assets/store.go

Lines changed: 292 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,292 @@
1+
package assets
2+
3+
import (
4+
"context"
5+
6+
"github.com/btcsuite/btcd/btcec/v2"
7+
"github.com/btcsuite/btcd/btcutil"
8+
"github.com/btcsuite/btcd/chaincfg/chainhash"
9+
"github.com/btcsuite/btcd/wire"
10+
"github.com/lightninglabs/loop/fsm"
11+
"github.com/lightninglabs/loop/loopdb"
12+
"github.com/lightninglabs/loop/loopdb/sqlc"
13+
"github.com/lightningnetwork/lnd/clock"
14+
"github.com/lightningnetwork/lnd/keychain"
15+
"github.com/lightningnetwork/lnd/lntypes"
16+
)
17+
18+
const (
19+
emptyUserAgent = ""
20+
)
21+
22+
// BaseDB is the interface that contains all the queries generated
23+
// by sqlc for the instantout table.
24+
type BaseDB interface {
25+
// ExecTx allows for executing a function in the context of a database
26+
// transaction.
27+
ExecTx(ctx context.Context, txOptions loopdb.TxOptions,
28+
txBody func(*sqlc.Queries) error) error
29+
30+
CreateAssetSwap(ctx context.Context, arg sqlc.CreateAssetSwapParams) error
31+
CreateAssetOutSwap(ctx context.Context, swapHash []byte) error
32+
GetAllAssetOutSwaps(ctx context.Context) ([]sqlc.GetAllAssetOutSwapsRow, error)
33+
GetAssetOutSwap(ctx context.Context, swapHash []byte) (sqlc.GetAssetOutSwapRow, error)
34+
InsertAssetSwapUpdate(ctx context.Context, arg sqlc.InsertAssetSwapUpdateParams) error
35+
UpdateAssetSwapHtlcTx(ctx context.Context, arg sqlc.UpdateAssetSwapHtlcTxParams) error
36+
UpdateAssetSwapOutPreimage(ctx context.Context, arg sqlc.UpdateAssetSwapOutPreimageParams) error
37+
UpdateAssetSwapOutProof(ctx context.Context, arg sqlc.UpdateAssetSwapOutProofParams) error
38+
UpdateAssetSwapSweepTx(ctx context.Context, arg sqlc.UpdateAssetSwapSweepTxParams) error
39+
}
40+
41+
// PostgresStore is the backing store for the instant out manager.
42+
type PostgresStore struct {
43+
queries BaseDB
44+
clock clock.Clock
45+
}
46+
47+
// NewPostgresStore creates a new PostgresStore.
48+
func NewPostgresStore(queries BaseDB) *PostgresStore {
49+
50+
return &PostgresStore{
51+
queries: queries,
52+
clock: clock.NewDefaultClock(),
53+
}
54+
}
55+
56+
// CreateAssetSwapOut creates a new asset swap out in the database.
57+
func (p *PostgresStore) CreateAssetSwapOut(ctx context.Context,
58+
swap *SwapOut) error {
59+
60+
params := sqlc.CreateAssetSwapParams{
61+
SwapHash: swap.SwapHash[:],
62+
AssetID: swap.AssetId,
63+
Amt: int64(swap.Amount),
64+
SenderPubkey: swap.SenderPubkey.SerializeCompressed(),
65+
ReceiverPubkey: swap.ReceiverPubkey.SerializeCompressed(),
66+
CsvExpiry: swap.CsvExpiry,
67+
InitiationHeight: swap.InitiationHeight,
68+
CreatedTime: p.clock.Now(),
69+
ServerKeyFamily: int64(swap.ClientKeyLocator.Family),
70+
ServerKeyIndex: int64(swap.ClientKeyLocator.Index),
71+
}
72+
73+
return p.queries.ExecTx(
74+
ctx, &loopdb.SqliteTxOptions{}, func(q *sqlc.Queries) error {
75+
err := q.CreateAssetSwap(ctx, params)
76+
if err != nil {
77+
return err
78+
}
79+
80+
return q.CreateAssetOutSwap(ctx, swap.SwapHash[:])
81+
},
82+
)
83+
}
84+
85+
// UpdateAssetSwapHtlcOutpoint updates the htlc outpoint of the swap out in the
86+
// database.
87+
func (p *PostgresStore) UpdateAssetSwapHtlcOutpoint(ctx context.Context,
88+
swapHash lntypes.Hash, outpoint *wire.OutPoint, confirmationHeight int32) error {
89+
90+
return p.queries.ExecTx(
91+
ctx, &loopdb.SqliteTxOptions{}, func(q *sqlc.Queries) error {
92+
return q.UpdateAssetSwapHtlcTx(
93+
ctx, sqlc.UpdateAssetSwapHtlcTxParams{
94+
SwapHash: swapHash[:],
95+
HtlcTxid: outpoint.Hash[:],
96+
HtlcVout: int32(outpoint.Index),
97+
HtlcConfirmationHeight: confirmationHeight,
98+
})
99+
},
100+
)
101+
}
102+
103+
// UpdateAssetSwapOutProof updates the raw proof of the swap out in the
104+
// database.
105+
func (p *PostgresStore) UpdateAssetSwapOutProof(ctx context.Context,
106+
swapHash lntypes.Hash, rawProof []byte) error {
107+
108+
return p.queries.ExecTx(
109+
ctx, &loopdb.SqliteTxOptions{}, func(q *sqlc.Queries) error {
110+
return q.UpdateAssetSwapOutProof(
111+
ctx, sqlc.UpdateAssetSwapOutProofParams{
112+
SwapHash: swapHash[:],
113+
RawProofFile: rawProof,
114+
})
115+
},
116+
)
117+
}
118+
119+
// UpdateAssetSwapOutPreimage updates the preimage of the swap out in the
120+
// database.
121+
func (p *PostgresStore) UpdateAssetSwapOutPreimage(ctx context.Context,
122+
swapHash lntypes.Hash, preimage lntypes.Preimage) error {
123+
124+
return p.queries.ExecTx(
125+
ctx, &loopdb.SqliteTxOptions{}, func(q *sqlc.Queries) error {
126+
return q.UpdateAssetSwapOutPreimage(
127+
ctx, sqlc.UpdateAssetSwapOutPreimageParams{
128+
SwapHash: swapHash[:],
129+
SwapPreimage: preimage[:],
130+
})
131+
},
132+
)
133+
}
134+
135+
// UpdateAssetSwapOutSweepTx updates the sweep tx of the swap out in the
136+
// database.
137+
func (p *PostgresStore) UpdateAssetSwapOutSweepTx(ctx context.Context,
138+
swapHash lntypes.Hash, sweepTxid chainhash.Hash, confHeight int32,
139+
sweepPkscript []byte) error {
140+
141+
return p.queries.ExecTx(
142+
ctx, &loopdb.SqliteTxOptions{}, func(q *sqlc.Queries) error {
143+
return q.UpdateAssetSwapSweepTx(
144+
ctx, sqlc.UpdateAssetSwapSweepTxParams{
145+
SwapHash: swapHash[:],
146+
SweepTxid: sweepTxid[:],
147+
SweepConfirmationHeight: confHeight,
148+
SweepPkscript: sweepPkscript,
149+
})
150+
},
151+
)
152+
}
153+
154+
// InsertAssetSwapUpdate inserts a new swap update in the database.
155+
func (p *PostgresStore) InsertAssetSwapUpdate(ctx context.Context,
156+
swapHash lntypes.Hash, state fsm.StateType) error {
157+
158+
return p.queries.ExecTx(
159+
ctx, &loopdb.SqliteTxOptions{}, func(q *sqlc.Queries) error {
160+
return q.InsertAssetSwapUpdate(
161+
ctx, sqlc.InsertAssetSwapUpdateParams{
162+
SwapHash: swapHash[:],
163+
UpdateState: string(state),
164+
UpdateTimestamp: p.clock.Now(),
165+
})
166+
},
167+
)
168+
}
169+
170+
// GetAllAssetOuts returns all the asset outs from the database.
171+
func (p *PostgresStore) GetAllAssetOuts(ctx context.Context) ([]*SwapOut, error) {
172+
dbAssetOuts, err := p.queries.GetAllAssetOutSwaps(ctx)
173+
if err != nil {
174+
return nil, err
175+
}
176+
177+
assetOuts := make([]*SwapOut, 0, len(dbAssetOuts))
178+
for _, dbAssetOut := range dbAssetOuts {
179+
assetOut, err := newSwapOutFromDB(
180+
dbAssetOut.AssetSwap, dbAssetOut.AssetOutSwap,
181+
dbAssetOut.UpdateState,
182+
)
183+
if err != nil {
184+
return nil, err
185+
}
186+
assetOuts = append(assetOuts, assetOut)
187+
}
188+
return assetOuts, nil
189+
}
190+
191+
// GetActiveAssetOuts returns all the active asset outs from the database.
192+
func (p *PostgresStore) GetActiveAssetOuts(ctx context.Context) ([]*SwapOut,
193+
error) {
194+
195+
dbAssetOuts, err := p.queries.GetAllAssetOutSwaps(ctx)
196+
if err != nil {
197+
return nil, err
198+
}
199+
200+
assetOuts := make([]*SwapOut, 0)
201+
for _, dbAssetOut := range dbAssetOuts {
202+
if IsFinishedState(fsm.StateType(dbAssetOut.UpdateState)) {
203+
continue
204+
}
205+
206+
assetOut, err := newSwapOutFromDB(
207+
dbAssetOut.AssetSwap, dbAssetOut.AssetOutSwap,
208+
dbAssetOut.UpdateState,
209+
)
210+
if err != nil {
211+
return nil, err
212+
}
213+
assetOuts = append(assetOuts, assetOut)
214+
}
215+
216+
return assetOuts, nil
217+
}
218+
219+
// newSwapOutFromDB creates a new SwapOut from the databse rows.
220+
func newSwapOutFromDB(assetSwap sqlc.AssetSwap,
221+
assetOutSwap sqlc.AssetOutSwap, state string) (
222+
*SwapOut, error) {
223+
224+
swapHash, err := lntypes.MakeHash(assetSwap.SwapHash)
225+
if err != nil {
226+
return nil, err
227+
}
228+
229+
var swapPreimage lntypes.Preimage
230+
if assetSwap.SwapPreimage != nil {
231+
swapPreimage, err = lntypes.MakePreimage(assetSwap.SwapPreimage)
232+
if err != nil {
233+
return nil, err
234+
}
235+
}
236+
237+
senderPubkey, err := btcec.ParsePubKey(assetSwap.SenderPubkey)
238+
if err != nil {
239+
return nil, err
240+
}
241+
242+
receiverPubkey, err := btcec.ParsePubKey(assetSwap.ReceiverPubkey)
243+
if err != nil {
244+
return nil, err
245+
}
246+
247+
var htlcOutpoint *wire.OutPoint
248+
if assetSwap.HtlcTxid != nil {
249+
htlcHash, err := chainhash.NewHash(assetSwap.HtlcTxid)
250+
if err != nil {
251+
return nil, err
252+
}
253+
htlcOutpoint = wire.NewOutPoint(
254+
htlcHash, uint32(assetSwap.HtlcVout),
255+
)
256+
}
257+
258+
var sweepOutpoint *wire.OutPoint
259+
if assetSwap.SweepTxid != nil {
260+
sweepHash, err := chainhash.NewHash(assetSwap.SweepTxid)
261+
if err != nil {
262+
return nil, err
263+
}
264+
sweepOutpoint = wire.NewOutPoint(
265+
sweepHash, 0,
266+
)
267+
}
268+
269+
return &SwapOut{
270+
SwapHash: swapHash,
271+
SwapPreimage: swapPreimage,
272+
State: fsm.StateType(state),
273+
Amount: btcutil.Amount(assetSwap.Amt),
274+
SenderPubkey: senderPubkey,
275+
ReceiverPubkey: receiverPubkey,
276+
CsvExpiry: int32(assetSwap.CsvExpiry),
277+
AssetId: assetSwap.AssetID,
278+
InitiationHeight: int32(assetSwap.InitiationHeight),
279+
ClientKeyLocator: keychain.KeyLocator{
280+
Family: keychain.KeyFamily(
281+
assetSwap.ServerKeyFamily,
282+
),
283+
Index: uint32(assetSwap.ServerKeyIndex),
284+
},
285+
HtlcOutPoint: htlcOutpoint,
286+
HtlcConfirmationHeight: uint32(assetSwap.HtlcConfirmationHeight),
287+
SweepOutpoint: sweepOutpoint,
288+
SweepConfirmationHeight: uint32(assetSwap.SweepConfirmationHeight),
289+
SweepPkscript: assetSwap.SweepPkscript,
290+
RawHtlcProof: assetOutSwap.RawProofFile,
291+
}, nil
292+
}

‎assets/store_test.go

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
package assets
2+
3+
import (
4+
"context"
5+
"crypto/rand"
6+
"encoding/hex"
7+
"testing"
8+
9+
"github.com/btcsuite/btcd/btcec/v2"
10+
"github.com/btcsuite/btcd/chaincfg/chainhash"
11+
"github.com/btcsuite/btcd/wire"
12+
"github.com/lightninglabs/loop/fsm"
13+
"github.com/lightninglabs/loop/loopdb"
14+
"github.com/lightningnetwork/lnd/keychain"
15+
"github.com/lightningnetwork/lnd/lntypes"
16+
"github.com/stretchr/testify/require"
17+
)
18+
19+
var (
20+
defaultClientPubkeyBytes, _ = hex.DecodeString("021c97a90a411ff2b10dc2a8e32de2f29d2fa49d41bfbb52bd416e460db0747d0d")
21+
defaultClientPubkey, _ = btcec.ParsePubKey(defaultClientPubkeyBytes)
22+
defaultServerPubkeyBytes, _ = hex.DecodeString("021d97a90a411ff2b10dc2a8e32de2f29d2fa49d41bfbb52bd416e460db0747d0d")
23+
defaultServerPubkey, _ = btcec.ParsePubKey(defaultServerPubkeyBytes)
24+
25+
defaultOutpoint = &wire.OutPoint{
26+
Hash: chainhash.Hash{0x01},
27+
Index: 1,
28+
}
29+
)
30+
31+
// TestSqlStore tests the asset swap store.
32+
func TestSqlStore(t *testing.T) {
33+
34+
ctxb := context.Background()
35+
testDb := loopdb.NewTestDB(t)
36+
defer testDb.Close()
37+
38+
store := NewPostgresStore(testDb)
39+
40+
swapPreimage := getRandomPreimage()
41+
SwapHash := swapPreimage.Hash()
42+
43+
// Create a new SwapOut.
44+
swapOut := &SwapOut{
45+
SwapHash: SwapHash,
46+
SwapPreimage: swapPreimage,
47+
State: fsm.StateType("init"),
48+
Amount: 100,
49+
SenderPubkey: defaultClientPubkey,
50+
ReceiverPubkey: defaultClientPubkey,
51+
CsvExpiry: 100,
52+
AssetId: []byte("assetid"),
53+
InitiationHeight: 1,
54+
ClientKeyLocator: keychain.KeyLocator{
55+
Family: 1,
56+
Index: 1,
57+
},
58+
}
59+
60+
// Save the swap out in the db.
61+
err := store.CreateAssetSwapOut(ctxb, swapOut)
62+
require.NoError(t, err)
63+
64+
// Insert a new swap out update.
65+
err = store.InsertAssetSwapUpdate(
66+
ctxb, SwapHash, fsm.StateType("state2"),
67+
)
68+
require.NoError(t, err)
69+
70+
// Try to fetch all swap outs.
71+
swapOuts, err := store.GetAllAssetOuts(ctxb)
72+
require.NoError(t, err)
73+
require.Len(t, swapOuts, 1)
74+
75+
// Update the htlc outpoint.
76+
err = store.UpdateAssetSwapHtlcOutpoint(
77+
ctxb, SwapHash, defaultOutpoint, 100,
78+
)
79+
require.NoError(t, err)
80+
81+
// Update the offchain payment amount.
82+
err = store.UpdateAssetSwapOutProof(
83+
ctxb, SwapHash, []byte("proof"),
84+
)
85+
require.NoError(t, err)
86+
87+
// Try to fetch all active swap outs.
88+
activeSwapOuts, err := store.GetActiveAssetOuts(ctxb)
89+
require.NoError(t, err)
90+
require.Len(t, activeSwapOuts, 1)
91+
92+
// Update the swap out state to a finished state.
93+
err = store.InsertAssetSwapUpdate(
94+
ctxb, SwapHash, fsm.StateType(FinishedStates()[0]),
95+
)
96+
require.NoError(t, err)
97+
98+
// Try to fetch all active swap outs.
99+
activeSwapOuts, err = store.GetActiveAssetOuts(ctxb)
100+
require.NoError(t, err)
101+
require.Len(t, activeSwapOuts, 0)
102+
}
103+
104+
// getRandomPreimage generates a random reservation ID.
105+
func getRandomPreimage() lntypes.Preimage {
106+
var id lntypes.Preimage
107+
rand.Read(id[:])
108+
return id
109+
}

‎assets/swap_out.go

Lines changed: 295 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,295 @@
1+
package assets
2+
3+
import (
4+
"context"
5+
6+
"github.com/btcsuite/btcd/btcec/v2"
7+
"github.com/btcsuite/btcd/btcutil"
8+
"github.com/btcsuite/btcd/txscript"
9+
"github.com/btcsuite/btcd/wire"
10+
"github.com/decred/dcrd/dcrec/secp256k1/v4"
11+
"github.com/lightninglabs/loop/fsm"
12+
"github.com/lightninglabs/taproot-assets/address"
13+
"github.com/lightninglabs/taproot-assets/asset"
14+
"github.com/lightninglabs/taproot-assets/commitment"
15+
"github.com/lightninglabs/taproot-assets/proof"
16+
"github.com/lightninglabs/taproot-assets/tapsend"
17+
18+
"github.com/lightninglabs/taproot-assets/tappsbt"
19+
"github.com/lightningnetwork/lnd/input"
20+
"github.com/lightningnetwork/lnd/keychain"
21+
"github.com/lightningnetwork/lnd/lntypes"
22+
)
23+
24+
// SwapOut is a struct that represents a swap out. It contains all the
25+
// information needed to perform a swap out.
26+
type SwapOut struct {
27+
// SwapHash is the hash of the swap.
28+
SwapHash lntypes.Hash
29+
30+
// SwapPreimage is the preimage of the swap, that enables spending
31+
// the success path, it's hash is the main identifier of the swap.
32+
SwapPreimage lntypes.Preimage
33+
34+
// State is the current state of the swap.
35+
State fsm.StateType
36+
37+
// Amount is the amount of the asset to swap.
38+
Amount btcutil.Amount
39+
40+
// SenderPubkey is the pubkey of the sender of onchain asset funds.
41+
SenderPubkey *btcec.PublicKey
42+
43+
// ReceiverPubkey is the pubkey of the receiver of the onchain asset
44+
// funds.
45+
ReceiverPubkey *btcec.PublicKey
46+
47+
// CsvExpiry is the relative timelock in blocks for the swap.
48+
CsvExpiry int32
49+
50+
// AssetId is the identifier of the asset to swap.
51+
AssetId []byte
52+
53+
// InitiationHeight is the height at which the swap was initiated.
54+
InitiationHeight int32
55+
56+
// ClientKeyLocator is the key locator of the clients key.
57+
ClientKeyLocator keychain.KeyLocator
58+
59+
// HtlcOutPoint is the outpoint of the htlc that was created to
60+
// perform the swap.
61+
HtlcOutPoint *wire.OutPoint
62+
63+
// HtlcConfirmationHeight is the height at which the htlc was
64+
// confirmed.
65+
HtlcConfirmationHeight uint32
66+
67+
// SweepOutpoint is the outpoint of the htlc that was swept.
68+
SweepOutpoint *wire.OutPoint
69+
70+
// SweepConfirmationHeight is the height at which the sweep was
71+
// confirmed.
72+
SweepConfirmationHeight uint32
73+
74+
// SweepPkscript is the pkscript of the sweep transaction.
75+
SweepPkscript []byte
76+
77+
// RawHtlcProof is the raw htlc proof that we need to send to the
78+
// receiver. We only keep this in the OutFSM struct as we don't want
79+
// to save it in the store.
80+
RawHtlcProof []byte
81+
}
82+
83+
// NewSwapOut creates a new swap out.
84+
func NewSwapOut(swapHash lntypes.Hash, amt btcutil.Amount,
85+
assetId []byte, clientKeyDesc *keychain.KeyDescriptor,
86+
senderPubkey *btcec.PublicKey, csvExpiry, initiationHeight int32,
87+
) *SwapOut {
88+
89+
return &SwapOut{
90+
SwapHash: swapHash,
91+
State: Init,
92+
Amount: amt,
93+
SenderPubkey: senderPubkey,
94+
ReceiverPubkey: clientKeyDesc.PubKey,
95+
CsvExpiry: csvExpiry,
96+
InitiationHeight: initiationHeight,
97+
ClientKeyLocator: clientKeyDesc.KeyLocator,
98+
AssetId: assetId,
99+
}
100+
}
101+
102+
// GetSuccesScript returns the success path script of the swap.
103+
func (s *SwapOut) GetSuccesScript() ([]byte, error) {
104+
return GenSuccessPathScript(s.ReceiverPubkey, s.SwapHash)
105+
}
106+
107+
// GetTimeoutScript returns the timeout path script of the swap.
108+
func (s *SwapOut) GetTimeoutScript() ([]byte, error) {
109+
return GenTimeoutPathScript(s.SenderPubkey, int64(s.CsvExpiry))
110+
}
111+
112+
// getAggregateKey returns the aggregate musig2 key of the swap.
113+
func (s *SwapOut) getAggregateKey() (*btcec.PublicKey, error) {
114+
aggregateKey, err := input.MuSig2CombineKeys(
115+
input.MuSig2Version100RC2,
116+
[]*btcec.PublicKey{
117+
s.SenderPubkey, s.ReceiverPubkey,
118+
},
119+
true,
120+
&input.MuSig2Tweaks{},
121+
)
122+
if err != nil {
123+
return nil, err
124+
}
125+
126+
return aggregateKey.PreTweakedKey, nil
127+
}
128+
129+
// GetTimeOutLeaf returns the timeout leaf of the swap.
130+
func (s *SwapOut) GetTimeOutLeaf() (txscript.TapLeaf, error) {
131+
timeoutScript, err := s.GetTimeoutScript()
132+
if err != nil {
133+
return txscript.TapLeaf{}, err
134+
}
135+
136+
timeoutLeaf := txscript.NewBaseTapLeaf(timeoutScript)
137+
138+
return timeoutLeaf, nil
139+
}
140+
141+
// GetSuccessLeaf returns the success leaf of the swap.
142+
func (s *SwapOut) GetSuccessLeaf() (txscript.TapLeaf, error) {
143+
successScript, err := s.GetSuccesScript()
144+
if err != nil {
145+
return txscript.TapLeaf{}, err
146+
}
147+
148+
successLeaf := txscript.NewBaseTapLeaf(successScript)
149+
150+
return successLeaf, nil
151+
}
152+
153+
// getSiblingPreimage returns the sibling preimage of the htlc bitcoin toplevel
154+
// output.
155+
func (s *SwapOut) getSiblingPreimage() (commitment.TapscriptPreimage, error) {
156+
timeOutLeaf, err := s.GetTimeOutLeaf()
157+
if err != nil {
158+
return commitment.TapscriptPreimage{}, err
159+
}
160+
161+
successLeaf, err := s.GetSuccessLeaf()
162+
if err != nil {
163+
return commitment.TapscriptPreimage{}, err
164+
}
165+
166+
branch := txscript.NewTapBranch(timeOutLeaf, successLeaf)
167+
168+
siblingPreimage := commitment.NewPreimageFromBranch(branch)
169+
170+
return siblingPreimage, nil
171+
}
172+
173+
// createSweepVpkt creates the vpacket for the sweep.
174+
func (s *SwapOut) createSweepVpkt(ctx context.Context, htlcProof *proof.Proof,
175+
scriptKey asset.ScriptKey, internalKey keychain.KeyDescriptor,
176+
) (*tappsbt.VPacket, error) {
177+
178+
sweepVpkt, err := tappsbt.FromProofs(
179+
[]*proof.Proof{htlcProof}, &address.RegressionNetTap,
180+
)
181+
if err != nil {
182+
return nil, err
183+
}
184+
sweepVpkt.Outputs = append(sweepVpkt.Outputs, &tappsbt.VOutput{
185+
AssetVersion: asset.Version(1),
186+
Amount: uint64(s.Amount),
187+
Interactive: true,
188+
AnchorOutputIndex: 0,
189+
ScriptKey: scriptKey,
190+
AnchorOutputInternalKey: internalKey.PubKey,
191+
})
192+
sweepVpkt.Outputs[0].SetAnchorInternalKey(
193+
internalKey, address.RegressionNetTap.HDCoinType,
194+
)
195+
196+
err = tapsend.PrepareOutputAssets(ctx, sweepVpkt)
197+
if err != nil {
198+
return nil, err
199+
}
200+
201+
_, _, _, controlBlock, err := createOpTrueLeaf()
202+
if err != nil {
203+
return nil, err
204+
}
205+
206+
controlBlockBytes, err := controlBlock.ToBytes()
207+
if err != nil {
208+
return nil, err
209+
}
210+
211+
opTrueScript, err := GetOpTrueScript()
212+
if err != nil {
213+
return nil, err
214+
}
215+
216+
witness := wire.TxWitness{
217+
opTrueScript,
218+
controlBlockBytes,
219+
}
220+
firstPrevWitness := &sweepVpkt.Outputs[0].Asset.PrevWitnesses[0]
221+
if sweepVpkt.Outputs[0].Asset.HasSplitCommitmentWitness() {
222+
rootAsset := firstPrevWitness.SplitCommitment.RootAsset
223+
firstPrevWitness = &rootAsset.PrevWitnesses[0]
224+
}
225+
firstPrevWitness.TxWitness = witness
226+
227+
return sweepVpkt, nil
228+
}
229+
230+
// genSuccessBtcControlBlock generates the control block for the timeout path of
231+
// the swap.
232+
func (s *SwapOut) genSuccessBtcControlBlock(taprootAssetRoot []byte) (
233+
*txscript.ControlBlock, error) {
234+
235+
internalKey, err := s.getAggregateKey()
236+
if err != nil {
237+
return nil, err
238+
}
239+
240+
timeOutLeaf, err := s.GetTimeOutLeaf()
241+
if err != nil {
242+
return nil, err
243+
}
244+
245+
timeOutLeafHash := timeOutLeaf.TapHash()
246+
247+
btcControlBlock := &txscript.ControlBlock{
248+
LeafVersion: txscript.BaseLeafVersion,
249+
InternalKey: internalKey,
250+
InclusionProof: append(timeOutLeafHash[:], taprootAssetRoot[:]...),
251+
}
252+
253+
successPathScript, err := s.GetSuccesScript()
254+
if err != nil {
255+
return nil, err
256+
}
257+
258+
rootHash := btcControlBlock.RootHash(successPathScript)
259+
tapKey := txscript.ComputeTaprootOutputKey(internalKey, rootHash)
260+
if tapKey.SerializeCompressed()[0] ==
261+
secp256k1.PubKeyFormatCompressedOdd {
262+
263+
btcControlBlock.OutputKeyYIsOdd = true
264+
}
265+
266+
return btcControlBlock, nil
267+
}
268+
269+
// genTaprootAssetRootFromProof generates the taproot asset root from the proof
270+
// of the swap.
271+
func (s *SwapOut) genTaprootAssetRootFromProof(proof *proof.Proof) ([]byte,
272+
error) {
273+
274+
assetCpy := proof.Asset.Copy()
275+
assetCpy.PrevWitnesses[0].SplitCommitment = nil
276+
sendCommitment, err := commitment.NewAssetCommitment(
277+
assetCpy,
278+
)
279+
if err != nil {
280+
return nil, err
281+
}
282+
283+
version := commitment.TapCommitmentV2
284+
assetCommitment, err := commitment.NewTapCommitment(
285+
&version, sendCommitment,
286+
)
287+
if err != nil {
288+
return nil, err
289+
}
290+
taprootAssetRoot := txscript.AssembleTaprootScriptTree(
291+
assetCommitment.TapLeaf(),
292+
).RootNode.TapHash()
293+
294+
return taprootAssetRoot[:], nil
295+
}

‎cmd/loop/assets.go

Lines changed: 229 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,229 @@
1+
package main
2+
3+
import (
4+
"bytes"
5+
"context"
6+
"encoding/hex"
7+
"errors"
8+
"fmt"
9+
10+
"github.com/btcsuite/btcd/btcutil"
11+
"github.com/lightninglabs/loop/looprpc"
12+
"github.com/urfave/cli"
13+
)
14+
15+
var assetsCommands = cli.Command{
16+
17+
Name: "assets",
18+
ShortName: "a",
19+
Usage: "manage asset swaps",
20+
Description: `
21+
`,
22+
Subcommands: []cli.Command{
23+
assetsOutCommand,
24+
listOutCommand,
25+
listAvailableAssetsComand,
26+
},
27+
}
28+
var (
29+
assetsOutCommand = cli.Command{
30+
Name: "out",
31+
ShortName: "o",
32+
Usage: "swap asset out",
33+
ArgsUsage: "",
34+
Description: `
35+
List all reservations.
36+
`,
37+
Flags: []cli.Flag{
38+
cli.Uint64Flag{
39+
Name: "amt",
40+
Usage: "the amount in satoshis to loop out.",
41+
},
42+
cli.StringFlag{
43+
Name: "asset_id",
44+
Usage: "asset_id",
45+
},
46+
},
47+
Action: assetSwapOut,
48+
}
49+
listAvailableAssetsComand = cli.Command{
50+
Name: "available",
51+
ShortName: "a",
52+
Usage: "list available assets",
53+
ArgsUsage: "",
54+
Description: `
55+
List available assets from the loop server
56+
`,
57+
58+
Action: listAvailable,
59+
}
60+
listOutCommand = cli.Command{
61+
Name: "list",
62+
ShortName: "l",
63+
Usage: "list asset swaps",
64+
ArgsUsage: "",
65+
Description: `
66+
List all reservations.
67+
`,
68+
Action: listOut,
69+
}
70+
)
71+
72+
func assetSwapOut(ctx *cli.Context) error {
73+
// First set up the swap client itself.
74+
client, cleanup, err := getAssetsClient(ctx)
75+
if err != nil {
76+
return err
77+
}
78+
defer cleanup()
79+
80+
args := ctx.Args()
81+
82+
var amtStr string
83+
switch {
84+
case ctx.IsSet("amt"):
85+
amtStr = ctx.String("amt")
86+
case ctx.NArg() > 0:
87+
amtStr = args[0]
88+
args = args.Tail()
89+
default:
90+
// Show command help if no arguments and flags were provided.
91+
return cli.ShowCommandHelp(ctx, "out")
92+
}
93+
94+
amt, err := parseAmt(amtStr)
95+
if err != nil {
96+
return err
97+
}
98+
if amt <= 0 {
99+
return fmt.Errorf("amount must be greater than zero")
100+
}
101+
102+
assetId, err := hex.DecodeString(ctx.String("asset_id"))
103+
if err != nil {
104+
return err
105+
}
106+
107+
if len(assetId) != 32 {
108+
return fmt.Errorf("invalid asset id")
109+
}
110+
111+
// First we'll list the available assets.
112+
assets, err := client.ClientListAvailableAssets(
113+
context.Background(),
114+
&looprpc.ClientListAvailableAssetsRequest{},
115+
)
116+
if err != nil {
117+
return err
118+
}
119+
120+
// We now extract the asset name from the list of available assets.
121+
var assetName string
122+
for _, asset := range assets.AvailableAssets {
123+
if bytes.Equal(asset.AssetId, assetId) {
124+
assetName = asset.Name
125+
break
126+
}
127+
}
128+
if assetName == "" {
129+
return fmt.Errorf("asset not found")
130+
}
131+
132+
// First we'll quote the swap out to get the current fee and rate.
133+
quote, err := client.ClientGetAssetSwapOutQuote(
134+
context.Background(),
135+
&looprpc.ClientGetAssetSwapOutQuoteRequest{
136+
Amt: uint64(amt),
137+
Asset: assetId,
138+
},
139+
)
140+
if err != nil {
141+
return err
142+
}
143+
144+
totalSats := btcutil.Amount(amt * btcutil.Amount(quote.SatsPerUnit)).MulF64(float64(1) + quote.SwapFee)
145+
146+
fmt.Printf(satAmtFmt, "Fixed prepay cost:", quote.PrepayAmt)
147+
fmt.Printf(bpsFmt, "Swap fee:", int64(quote.SwapFee*10000))
148+
fmt.Printf(satAmtFmt, "Sats per unit:", quote.SatsPerUnit)
149+
fmt.Printf(satAmtFmt, "Swap Offchain payment:", totalSats)
150+
fmt.Printf(satAmtFmt, "Total Send off-chain:", totalSats+btcutil.Amount(quote.PrepayAmt))
151+
fmt.Printf(assetFmt, "Receive assets on-chain:", int64(amt), assetName)
152+
153+
fmt.Println("CONTINUE SWAP? (y/n): ")
154+
155+
var answer string
156+
fmt.Scanln(&answer)
157+
if answer != "y" {
158+
return errors.New("swap canceled")
159+
}
160+
161+
res, err := client.SwapOut(
162+
context.Background(),
163+
&looprpc.SwapOutRequest{
164+
Amt: uint64(amt),
165+
Asset: assetId,
166+
},
167+
)
168+
if err != nil {
169+
return err
170+
}
171+
172+
printRespJSON(res)
173+
return nil
174+
}
175+
176+
func listAvailable(ctx *cli.Context) error {
177+
// First set up the swap client itself.
178+
client, cleanup, err := getAssetsClient(ctx)
179+
if err != nil {
180+
return err
181+
}
182+
defer cleanup()
183+
184+
res, err := client.ClientListAvailableAssets(
185+
context.Background(),
186+
&looprpc.ClientListAvailableAssetsRequest{},
187+
)
188+
if err != nil {
189+
return err
190+
}
191+
192+
printRespJSON(res)
193+
return nil
194+
}
195+
func listOut(ctx *cli.Context) error {
196+
// First set up the swap client itself.
197+
client, cleanup, err := getAssetsClient(ctx)
198+
if err != nil {
199+
return err
200+
}
201+
defer cleanup()
202+
203+
res, err := client.ListAssetSwaps(
204+
context.Background(),
205+
&looprpc.ListAssetSwapsRequest{},
206+
)
207+
if err != nil {
208+
return err
209+
}
210+
211+
printRespJSON(res)
212+
return nil
213+
}
214+
215+
func getAssetsClient(ctx *cli.Context) (looprpc.AssetsClientClient, func(), error) {
216+
rpcServer := ctx.GlobalString("rpcserver")
217+
tlsCertPath, macaroonPath, err := extractPathArgs(ctx)
218+
if err != nil {
219+
return nil, nil, err
220+
}
221+
conn, err := getClientConn(rpcServer, tlsCertPath, macaroonPath)
222+
if err != nil {
223+
return nil, nil, err
224+
}
225+
cleanup := func() { conn.Close() }
226+
227+
loopClient := looprpc.NewAssetsClientClient(conn)
228+
return loopClient, cleanup, nil
229+
}

‎cmd/loop/main.go

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ var (
8383
listSwapsCommand, swapInfoCommand, getLiquidityParamsCommand,
8484
setLiquidityRuleCommand, suggestSwapCommand, setParamsCommand,
8585
getInfoCommand, abandonSwapCommand, reservationsCommands,
86-
instantOutCommand, listInstantOutsCommand,
86+
instantOutCommand, listInstantOutsCommand, assetsCommands,
8787
}
8888
)
8989

@@ -110,6 +110,20 @@ const (
110110
// Exchange rate: 0.0002 USD/SAT
111111
rateFmt = "%-36s %12.4f %s/SAT\n"
112112

113+
// bpsFmt formats a basis point value into a one line string, intended to
114+
// prettify the terminal output. For Instance,
115+
// fmt.Printf(f, "Service fee:", fee)
116+
// prints out as,
117+
// Service fee: 20 bps
118+
bpsFmt = "%-36s %12d bps\n"
119+
120+
// assetFmt formats an asset into a one line string, intended to
121+
// prettify the terminal output. For Instance,
122+
// fmt.Printf(f, "Receive asset onchain:", assetName, assetAmt)
123+
// prints out as,
124+
// Receive asset onchain: 0.0001 USD
125+
assetFmt = "%-36s %12d %s\n"
126+
113127
// blkFmt formats the number of blocks into a one line string, intended
114128
// to prettify the terminal output. For Instance,
115129
// fmt.Printf(f, "Conf target", target)

‎fsm/fsm.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -297,6 +297,16 @@ func (s *StateMachine) RemoveObserver(observer Observer) bool {
297297
return false
298298
}
299299

300+
// Lock locks the state machine.
301+
func (s *StateMachine) Lock() {
302+
s.mutex.Lock()
303+
}
304+
305+
// Unlock unlocks the state machine.
306+
func (s *StateMachine) Unlock() {
307+
s.mutex.Unlock()
308+
}
309+
300310
// HandleError is a helper function that can be used by actions to handle
301311
// errors.
302312
func (s *StateMachine) HandleError(err error) EventType {

‎go.mod

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,8 @@ require (
4242
modernc.org/sqlite v1.30.0
4343
)
4444

45+
require github.com/lightningnetwork/lnd/kvdb v1.4.10 // indirect
46+
4547
require (
4648
dario.cat/mergo v1.0.1 // indirect
4749
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 // indirect
@@ -122,7 +124,6 @@ require (
122124
github.com/lightningnetwork/lightning-onion v1.2.1-0.20240712235311-98bd56499dfb // indirect
123125
github.com/lightningnetwork/lnd/fn v1.2.3 // indirect
124126
github.com/lightningnetwork/lnd/healthcheck v1.2.5 // indirect
125-
github.com/lightningnetwork/lnd/kvdb v1.4.10 // indirect
126127
github.com/lightningnetwork/lnd/sqldb v1.0.4 // indirect
127128
github.com/ltcsuite/ltcd v0.0.0-20190101042124-f37f8bf35796 // indirect
128129
github.com/mattn/go-isatty v0.0.20 // indirect
@@ -213,6 +214,8 @@ replace github.com/lightninglabs/loop/swapserverrpc => ./swapserverrpc
213214

214215
replace github.com/lightninglabs/loop/looprpc => ./looprpc
215216

217+
// replace github.com/lightninglabs/taproot-assets => ../taproot-assets
218+
216219
go 1.22.6
217220

218221
toolchain go1.22.7

‎loopd/config.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,8 @@ type Config struct {
200200
Tapd *assets.TapdConfig `group:"tapd" namespace:"tapd"`
201201

202202
View viewParameters `command:"view" alias:"v" description:"View all swaps in the database. This command can only be executed when loopd is not running."`
203+
204+
TapdConfig *assets.TapdConfig `group:"tapd" namespace:"tapd"`
203205
}
204206

205207
const (
@@ -247,6 +249,7 @@ func DefaultConfig() Config {
247249
MacaroonPath: DefaultLndMacaroonPath,
248250
RPCTimeout: DefaultLndRPCTimeout,
249251
},
252+
TapdConfig: assets.DefaultTapdConfig(),
250253
}
251254
}
252255

‎loopd/daemon.go

Lines changed: 56 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -249,6 +249,8 @@ func (d *Daemon) startWebServers() error {
249249
)
250250
loop_looprpc.RegisterSwapClientServer(d.grpcServer, d)
251251

252+
loop_looprpc.RegisterAssetsClientServer(d.grpcServer, d.assetsServer)
253+
252254
// Register our debug server if it is compiled in.
253255
d.registerDebugServer()
254256

@@ -332,7 +334,7 @@ func (d *Daemon) startWebServers() error {
332334
d.wg.Add(1)
333335
go func() {
334336
defer d.wg.Done()
335-
337+
defer log.Info("REST proxy stopped")
336338
log.Infof("REST proxy listening on %s",
337339
d.restListener.Addr())
338340
err := d.restServer.Serve(d.restListener)
@@ -354,7 +356,7 @@ func (d *Daemon) startWebServers() error {
354356
d.wg.Add(1)
355357
go func() {
356358
defer d.wg.Done()
357-
359+
defer log.Info("RPC server stopped")
358360
log.Infof("RPC server listening on %s", d.grpcListener.Addr())
359361
err = d.grpcServer.Serve(d.grpcListener)
360362
if err != nil && !errors.Is(err, grpc.ErrServerStopped) {
@@ -487,6 +489,11 @@ func (d *Daemon) initialize(withMacaroonService bool) error {
487489
swapClient.Conn,
488490
)
489491

492+
// Create a assets server client.
493+
assetsClient := loop_swaprpc.NewAssetsSwapServerClient(
494+
swapClient.Conn,
495+
)
496+
490497
// Both the client RPC server and the swap server client should stop
491498
// on main context cancel. So we create it early and pass it down.
492499
d.mainCtx, d.mainCtxCancel = context.WithCancel(context.Background())
@@ -636,6 +643,8 @@ func (d *Daemon) initialize(withMacaroonService bool) error {
636643
var (
637644
reservationManager *reservation.Manager
638645
instantOutManager *instantout.Manager
646+
assetManager *assets.AssetsSwapManager
647+
assetClientServer *assets.AssetsClientServer
639648
)
640649

641650
// Create the reservation and instantout managers.
@@ -676,6 +685,27 @@ func (d *Daemon) initialize(withMacaroonService bool) error {
676685
instantOutManager = instantout.NewInstantOutManager(
677686
instantOutConfig,
678687
)
688+
689+
tapdClient, err := assets.NewTapdClient(
690+
d.cfg.TapdConfig,
691+
)
692+
if err != nil {
693+
return err
694+
}
695+
assetsStore := assets.NewPostgresStore(baseDb)
696+
assetsConfig := &assets.Config{
697+
ServerClient: assetsClient,
698+
Store: assetsStore,
699+
AssetClient: tapdClient,
700+
LndClient: d.lnd.Client,
701+
Router: d.lnd.Router,
702+
ChainNotifier: d.lnd.ChainNotifier,
703+
Signer: d.lnd.Signer,
704+
Wallet: d.lnd.WalletKit,
705+
ExchangeRateProvider: assets.NewFixedExchangeRateProvider(),
706+
}
707+
assetManager = assets.NewAssetSwapServer(assetsConfig)
708+
assetClientServer = assets.NewAssetsServer(assetManager)
679709
}
680710

681711
// Now finally fully initialize the swap client RPC server instance.
@@ -696,6 +726,8 @@ func (d *Daemon) initialize(withMacaroonService bool) error {
696726
withdrawalManager: withdrawalManager,
697727
staticLoopInManager: staticLoopInManager,
698728
assetClient: d.assetClient,
729+
assetManager: assetManager,
730+
assetsServer: assetClientServer,
699731
}
700732

701733
// Retrieve all currently existing swaps from the database.
@@ -801,6 +833,10 @@ func (d *Daemon) initialize(withMacaroonService bool) error {
801833
cancel()
802834
}
803835
}
836+
getInfo, err := d.lnd.Client.GetInfo(d.mainCtx)
837+
if err != nil {
838+
return err
839+
}
804840

805841
// Start the instant out manager.
806842
if d.instantOutManager != nil {
@@ -809,12 +845,6 @@ func (d *Daemon) initialize(withMacaroonService bool) error {
809845
go func() {
810846
defer d.wg.Done()
811847

812-
getInfo, err := d.lnd.Client.GetInfo(d.mainCtx)
813-
if err != nil {
814-
d.internalErrChan <- err
815-
return
816-
}
817-
818848
log.Info("Starting instantout manager")
819849
defer log.Info("Instantout manager stopped")
820850

@@ -933,6 +963,20 @@ func (d *Daemon) initialize(withMacaroonService bool) error {
933963
staticLoopInManager.WaitInitComplete()
934964
}
935965

966+
// Start the asset manager.
967+
if d.assetManager != nil {
968+
d.wg.Add(1)
969+
go func() {
970+
defer d.wg.Done()
971+
log.Infof("Starting asset manager")
972+
defer log.Infof("Asset manager stopped")
973+
err := d.assetManager.Run(d.mainCtx, int32(getInfo.BlockHeight))
974+
if err != nil && !errors.Is(err, context.Canceled) {
975+
d.internalErrChan <- err
976+
}
977+
}()
978+
}
979+
936980
// Last, start our internal error handler. This will return exactly one
937981
// error or nil on the main error channel to inform the caller that
938982
// something went wrong or that shutdown is complete. We don't add to
@@ -978,6 +1022,9 @@ func (d *Daemon) Stop() {
9781022

9791023
// stop does the actual shutdown and blocks until all goroutines have exit.
9801024
func (d *Daemon) stop() {
1025+
// Sleep a second in order to fix a blocking issue when having a
1026+
// startup error.
1027+
<-time.After(time.Second)
9811028
// First of all, we can cancel the main context that all event handlers
9821029
// are using. This should stop all swap activity and all event handlers
9831030
// should exit.
@@ -995,6 +1042,7 @@ func (d *Daemon) stop() {
9951042
if d.restServer != nil {
9961043
// Don't return the error here, we first want to give everything
9971044
// else a chance to shut down cleanly.
1045+
9981046
err := d.restServer.Close()
9991047
if err != nil {
10001048
log.Errorf("Error stopping REST server: %v", err)

‎loopd/log.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"github.com/lightninglabs/aperture/l402"
66
"github.com/lightninglabs/lndclient"
77
"github.com/lightninglabs/loop"
8+
"github.com/lightninglabs/loop/assets"
89
"github.com/lightninglabs/loop/fsm"
910
"github.com/lightninglabs/loop/instantout"
1011
"github.com/lightninglabs/loop/instantout/reservation"
@@ -14,6 +15,7 @@ import (
1415
"github.com/lightninglabs/loop/staticaddr"
1516
"github.com/lightninglabs/loop/sweep"
1617
"github.com/lightninglabs/loop/sweepbatcher"
18+
"github.com/lightninglabs/loop/utils"
1719
"github.com/lightningnetwork/lnd"
1820
"github.com/lightningnetwork/lnd/build"
1921
"github.com/lightningnetwork/lnd/signal"
@@ -58,6 +60,13 @@ func SetupLoggers(root *build.RotatingLogWriter, intercept signal.Interceptor) {
5860
lnd.AddSubLogger(
5961
root, sweep.Subsystem, intercept, sweep.UseLogger,
6062
)
63+
64+
lnd.AddSubLogger(
65+
root, assets.Subsystem, intercept, assets.UseLogger,
66+
)
67+
lnd.AddSubLogger(
68+
root, utils.Subsystem, intercept, utils.UseLogger,
69+
)
6170
}
6271

6372
// genSubLogger creates a logger for a subsystem. We provide an instance of

‎loopd/perms/perms.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,4 +169,20 @@ var RequiredPermissions = map[string][]bakery.Op{
169169
Entity: "swap",
170170
Action: "read",
171171
}},
172+
"/looprpc.AssetsClient/SwapOut": {{
173+
Entity: "swap",
174+
Action: "execute",
175+
}},
176+
"/looprpc.AssetsClient/ListAssetSwaps": {{
177+
Entity: "swap",
178+
Action: "read",
179+
}},
180+
"/looprpc.AssetsClient/ClientListAvailableAssets": {{
181+
Entity: "swap",
182+
Action: "read",
183+
}},
184+
"/looprpc.AssetsClient/ClientGetAssetSwapOutQuote": {{
185+
Entity: "swap",
186+
Action: "read",
187+
}},
172188
}

‎loopd/swapclient_server.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,8 @@ type swapClientServer struct {
9696
withdrawalManager *withdraw.Manager
9797
staticLoopInManager *loopin.Manager
9898
assetClient *assets.TapdClient
99+
assetManager *assets.AssetsSwapManager
100+
assetsServer *assets.AssetsClientServer
99101
swaps map[lntypes.Hash]loop.SwapInfo
100102
subscribers map[int]chan<- interface{}
101103
statusChan chan loop.SwapInfo

‎loopdb/sqlc/asset_swaps.sql.go

Lines changed: 314 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
DROP INDEX IF EXISTS asset_out_swaps_swap_hash_idx;
2+
DROP TABLE IF EXISTS asset_out_swaps;
3+
DROP INDEX IF EXISTS asset_swaps_updates_swap_hash_idx;
4+
DROP TABLE IF EXISTS asset_swaps;
5+
DROP TABLE IF EXISTS asset_swaps;
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
CREATE TABLE IF NOT EXISTS asset_swaps (
2+
--- id is the autoincrementing primary key.
3+
id INTEGER PRIMARY KEY,
4+
5+
-- swap_hash is the randomly generated hash of the swap, which is used
6+
-- as the swap identifier for the clients.
7+
swap_hash BLOB NOT NULL UNIQUE,
8+
9+
-- swap_preimage is the preimage of the swap.
10+
swap_preimage BLOB,
11+
12+
-- asset_id is the identifier of the asset being swapped.
13+
asset_id BLOB NOT NULL,
14+
15+
-- amt is the requested amount to be swapped.
16+
amt BIGINT NOT NULL,
17+
18+
-- sender_pubkey is the pubkey of the sender.
19+
sender_pubkey BLOB NOT NULL,
20+
21+
-- receiver_pubkey is the pubkey of the receiver.
22+
receiver_pubkey BLOB NOT NULL,
23+
24+
-- csv_expiry is the expiry of the swap.
25+
csv_expiry INTEGER NOT NULL,
26+
27+
-- server_key_family is the family of key being identified.
28+
server_key_family BIGINT NOT NULL,
29+
30+
-- server_key_index is the precise index of the key being identified.
31+
server_key_index BIGINT NOT NULL,
32+
33+
-- initiation_height is the height at which the swap was initiated.
34+
initiation_height INTEGER NOT NULL,
35+
36+
-- created_time is the time at which the swap was created.
37+
created_time TIMESTAMP NOT NULL,
38+
39+
-- htlc_confirmation_height is the height at which the swap was confirmed.
40+
htlc_confirmation_height INTEGER NOT NULL DEFAULT(0),
41+
42+
-- htlc_txid is the txid of the confirmation transaction.
43+
htlc_txid BLOB,
44+
45+
-- htlc_vout is the vout of the confirmation transaction.
46+
htlc_vout INTEGER NOT NULL DEFAULT (0),
47+
48+
-- sweep_txid is the txid of the sweep transaction.
49+
sweep_txid BLOB,
50+
51+
-- sweep_confirmation_height is the height at which the swap was swept.
52+
sweep_confirmation_height INTEGER NOT NULL DEFAULT(0),
53+
54+
sweep_pkscript BLOB
55+
);
56+
57+
58+
CREATE TABLE IF NOT EXISTS asset_swaps_updates (
59+
-- id is auto incremented for each update.
60+
id INTEGER PRIMARY KEY,
61+
62+
-- swap_hash is the hash of the swap that this update is for.
63+
swap_hash BLOB NOT NULL REFERENCES asset_swaps(swap_hash),
64+
65+
-- update_state is the state of the swap at the time of the update.
66+
update_state TEXT NOT NULL,
67+
68+
-- update_timestamp is the time at which the update was created.
69+
update_timestamp TIMESTAMP NOT NULL
70+
);
71+
72+
73+
CREATE INDEX IF NOT EXISTS asset_swaps_updates_swap_hash_idx ON asset_swaps_updates(swap_hash);
74+
75+
76+
CREATE TABLE IF NOT EXISTS asset_out_swaps (
77+
-- swap_hash is the identifier of the swap.
78+
swap_hash BLOB PRIMARY KEY REFERENCES asset_swaps(swap_hash),
79+
80+
-- raw_proof_file is the file containing the raw proof.
81+
raw_proof_file BLOB
82+
);
83+
84+
CREATE INDEX IF NOT EXISTS asset_out_swaps_swap_hash_idx ON asset_out_swaps(swap_hash);
85+

‎loopdb/sqlc/models.go

Lines changed: 33 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎loopdb/sqlc/querier.go

Lines changed: 9 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎loopdb/sqlc/queries/asset_swaps.sql

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
-- name: CreateAssetSwap :exec
2+
INSERT INTO asset_swaps(
3+
swap_hash,
4+
swap_preimage,
5+
asset_id,
6+
amt,
7+
sender_pubkey,
8+
receiver_pubkey,
9+
csv_expiry,
10+
initiation_height,
11+
created_time,
12+
server_key_family,
13+
server_key_index
14+
)
15+
VALUES
16+
(
17+
$1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11
18+
);
19+
20+
-- name: CreateAssetOutSwap :exec
21+
INSERT INTO asset_out_swaps (
22+
swap_hash
23+
) VALUES (
24+
$1
25+
);
26+
27+
-- name: UpdateAssetSwapHtlcTx :exec
28+
UPDATE asset_swaps
29+
SET
30+
htlc_confirmation_height = $2,
31+
htlc_txid = $3,
32+
htlc_vout = $4
33+
WHERE
34+
asset_swaps.swap_hash = $1;
35+
36+
-- name: UpdateAssetSwapOutProof :exec
37+
UPDATE asset_out_swaps
38+
SET
39+
raw_proof_file = $2
40+
WHERE
41+
asset_out_swaps.swap_hash = $1;
42+
43+
-- name: UpdateAssetSwapOutPreimage :exec
44+
UPDATE asset_swaps
45+
SET
46+
swap_preimage = $2
47+
WHERE
48+
asset_swaps.swap_hash = $1;
49+
50+
-- name: UpdateAssetSwapSweepTx :exec
51+
UPDATE asset_swaps
52+
SET
53+
sweep_confirmation_height = $2,
54+
sweep_txid = $3,
55+
sweep_pkscript = $4
56+
WHERE
57+
asset_swaps.swap_hash = $1;
58+
59+
-- name: InsertAssetSwapUpdate :exec
60+
INSERT INTO asset_swaps_updates (
61+
swap_hash,
62+
update_state,
63+
update_timestamp
64+
) VALUES (
65+
$1,
66+
$2,
67+
$3
68+
);
69+
70+
71+
-- name: GetAssetOutSwap :one
72+
SELECT DISTINCT
73+
sqlc.embed(asw),
74+
sqlc.embed(aos),
75+
asu.update_state
76+
FROM
77+
asset_swaps asw
78+
INNER JOIN (
79+
SELECT
80+
swap_hash,
81+
update_state,
82+
ROW_NUMBER() OVER(PARTITION BY swap_hash ORDER BY id DESC) as rn
83+
FROM
84+
asset_swaps_updates
85+
) asu ON asw.swap_hash = asu.swap_hash AND asu.rn = 1
86+
INNER JOIN asset_out_swaps aos ON asw.swap_hash = aos.swap_hash
87+
WHERE
88+
asw.swap_hash = $1;
89+
90+
-- name: GetAllAssetOutSwaps :many
91+
SELECT DISTINCT
92+
sqlc.embed(asw),
93+
sqlc.embed(aos),
94+
asu.update_state
95+
FROM
96+
asset_swaps asw
97+
INNER JOIN (
98+
SELECT
99+
swap_hash,
100+
update_state,
101+
ROW_NUMBER() OVER(PARTITION BY swap_hash ORDER BY id DESC) as rn
102+
FROM
103+
asset_swaps_updates
104+
) asu ON asw.swap_hash = asu.swap_hash AND asu.rn = 1
105+
INNER JOIN asset_out_swaps aos ON asw.swap_hash = aos.swap_hash
106+
ORDER BY
107+
asw.id;
108+

‎looprpc/clientassets.pb.go

Lines changed: 805 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎looprpc/clientassets.proto

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
syntax = "proto3";
2+
3+
import "swapserverrpc/common.proto";
4+
5+
package looprpc;
6+
7+
option go_package = "github.com/lightninglabs/loop/looprpc";
8+
9+
service AssetsClient {
10+
rpc SwapOut (SwapOutRequest) returns (SwapOutResponse);
11+
rpc ListAssetSwaps (ListAssetSwapsRequest) returns (ListAssetSwapsResponse);
12+
rpc ClientListAvailableAssets (ClientListAvailableAssetsRequest)
13+
returns (ClientListAvailableAssetsResponse);
14+
rpc ClientGetAssetSwapOutQuote (ClientGetAssetSwapOutQuoteRequest)
15+
returns (ClientGetAssetSwapOutQuoteResponse);
16+
}
17+
18+
message SwapOutRequest {
19+
uint64 amt = 1;
20+
bytes asset = 2;
21+
}
22+
23+
message SwapOutResponse {
24+
AssetSwapStatus swap_status = 1;
25+
}
26+
27+
message ListAssetSwapsRequest {
28+
}
29+
30+
message ListAssetSwapsResponse {
31+
repeated AssetSwapStatus swap_status = 1;
32+
}
33+
34+
message AssetSwapStatus {
35+
bytes swap_hash = 1;
36+
string swap_status = 2;
37+
}
38+
39+
message ClientListAvailableAssetsRequest {
40+
}
41+
42+
message ClientListAvailableAssetsResponse {
43+
repeated Asset available_assets = 1;
44+
}
45+
46+
message Asset {
47+
bytes asset_id = 1;
48+
string name = 2;
49+
uint64 sats_per_unit = 3;
50+
}
51+
52+
message ClientGetAssetSwapOutQuoteRequest {
53+
uint64 amt = 1;
54+
bytes asset = 2;
55+
}
56+
57+
message ClientGetAssetSwapOutQuoteResponse {
58+
double swap_fee = 1;
59+
uint64 prepay_amt = 2;
60+
uint64 sats_per_unit = 3;
61+
}

‎looprpc/clientassets_grpc.pb.go

Lines changed: 209 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)
Please sign in to comment.