Skip to content

Commit 948d2df

Browse files
committed
assets+loopdb: implement new asset deposit functionality
This commit extends the asset deposit manager along with the underlying sql store, adding functionality to create and fund new asset deposits.
1 parent 40ceaf0 commit 948d2df

File tree

6 files changed

+603
-3
lines changed

6 files changed

+603
-3
lines changed

assets/deposit/manager.go

Lines changed: 328 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,24 @@ import (
44
"context"
55
"errors"
66
"fmt"
7+
"time"
78

9+
"github.com/btcsuite/btcd/btcec/v2"
810
"github.com/btcsuite/btcd/chaincfg"
11+
"github.com/btcsuite/btcd/wire"
912
"github.com/lightninglabs/lndclient"
1013
"github.com/lightninglabs/loop/assets"
1114
"github.com/lightninglabs/loop/swapserverrpc"
1215
"github.com/lightninglabs/taproot-assets/address"
16+
"github.com/lightninglabs/taproot-assets/asset"
17+
"github.com/lightninglabs/taproot-assets/taprpc"
1318
)
1419

1520
var (
21+
// AssetDepositKeyFamily is the key family used for generating asset
22+
// deposit keys.
23+
AssetDepositKeyFamily = int32(1122)
24+
1625
// ErrManagerShuttingDown signals that the asset deposit manager is
1726
// shutting down and that no further calls should be made to it.
1827
ErrManagerShuttingDown = errors.New("asset deposit manager is " +
@@ -270,3 +279,322 @@ func (m *Manager) handleDepositStateUpdate(ctx context.Context,
270279

271280
return nil
272281
}
282+
283+
// NewDeposit creates a new asset deposit with the given parameters.
284+
func (m *Manager) NewDeposit(ctx context.Context, assetID asset.ID,
285+
amount uint64, csvExpiry uint32) (DepositInfo, error) {
286+
287+
clientKeyDesc, err := m.walletKit.DeriveNextKey(
288+
ctx, AssetDepositKeyFamily,
289+
)
290+
if err != nil {
291+
return DepositInfo{}, err
292+
}
293+
clientInternalPubKey, _, err := DeriveSharedDepositKey(
294+
ctx, m.signer, clientKeyDesc.PubKey,
295+
)
296+
if err != nil {
297+
return DepositInfo{}, err
298+
}
299+
300+
clientScriptPubKeyBytes := clientKeyDesc.PubKey.SerializeCompressed()
301+
clientInternalPubKeyBytes := clientInternalPubKey.SerializeCompressed()
302+
303+
resp, err := m.depositServiceClient.NewAssetDeposit(
304+
ctx, &swapserverrpc.NewAssetDepositServerReq{
305+
AssetId: assetID[:],
306+
Amount: amount,
307+
ClientInternalPubkey: clientInternalPubKeyBytes,
308+
ClientScriptPubkey: clientScriptPubKeyBytes,
309+
CsvExpiry: int32(csvExpiry),
310+
},
311+
)
312+
if err != nil {
313+
log.Errorf("Swap server was unable to create the deposit: %v",
314+
err)
315+
316+
return DepositInfo{}, err
317+
}
318+
319+
serverScriptPubKey, err := btcec.ParsePubKey(resp.ServerScriptPubkey)
320+
if err != nil {
321+
return DepositInfo{}, err
322+
}
323+
324+
serverInternalPubKey, err := btcec.ParsePubKey(
325+
resp.ServerInternalPubkey,
326+
)
327+
if err != nil {
328+
return DepositInfo{}, err
329+
}
330+
331+
kit, err := NewKit(
332+
clientKeyDesc.PubKey, clientInternalPubKey, serverScriptPubKey,
333+
serverInternalPubKey, clientKeyDesc.KeyLocator, assetID,
334+
csvExpiry, &m.addressParams,
335+
)
336+
if err != nil {
337+
return DepositInfo{}, err
338+
}
339+
340+
deposit := &Deposit{
341+
Kit: kit,
342+
DepositInfo: &DepositInfo{
343+
ID: resp.DepositId,
344+
Version: CurrentProtocolVersion(),
345+
CreatedAt: time.Now(),
346+
Amount: amount,
347+
Addr: resp.DepositAddr,
348+
State: StateInitiated,
349+
},
350+
}
351+
352+
err = m.store.AddAssetDeposit(ctx, deposit)
353+
if err != nil {
354+
log.Errorf("Unable to add deposit to store: %v", err)
355+
356+
return DepositInfo{}, err
357+
}
358+
359+
err = m.handleNewDeposit(ctx, deposit)
360+
if err != nil {
361+
log.Errorf("Unable to add deposit to active deposits: %v", err)
362+
363+
return DepositInfo{}, err
364+
}
365+
366+
return *deposit.DepositInfo.Copy(), nil
367+
}
368+
369+
// handleNewDeposit adds the deposit to the active deposits map and starts the
370+
// funding process, all on the main event loop goroutine.
371+
func (m *Manager) handleNewDeposit(ctx context.Context, deposit *Deposit) error {
372+
done, err := m.scheduleNextCall()
373+
if err != nil {
374+
return err
375+
}
376+
defer done()
377+
378+
m.deposits[deposit.ID] = deposit
379+
380+
return m.fundDepositIfNeeded(ctx, deposit)
381+
}
382+
383+
// fundDepositIfNeeded attempts to fund the passed deposit if it is not already
384+
// funded.
385+
func (m *Manager) fundDepositIfNeeded(ctx context.Context, d *Deposit) error {
386+
// Now list transfers from tapd and check if the deposit is funded.
387+
funded, transfer, outIndex, err := m.isDepositFunded(ctx, d)
388+
if err != nil {
389+
log.Errorf("Unable to check if deposit %v is funded: %v", d.ID,
390+
err)
391+
392+
return err
393+
}
394+
395+
if !funded {
396+
// No funding transfer found, so we'll attempt to fund the
397+
// deposit by sending the asset to the deposit address. Note
398+
// that we label the send request with a specific label in order
399+
// to be able to subscribe to send events with a label filter.
400+
sendResp, err := m.tapClient.SendAsset(
401+
ctx, &taprpc.SendAssetRequest{
402+
TapAddrs: []string{d.Addr},
403+
Label: d.label(),
404+
},
405+
)
406+
if err != nil {
407+
log.Errorf("Unable to send asset to deposit %v: %v",
408+
d.ID, err)
409+
410+
return err
411+
}
412+
413+
// Extract the funding outpoint from the transfer.
414+
transfer, outIndex, err = d.GetMatchingOut(
415+
d.Amount, []*taprpc.AssetTransfer{sendResp.Transfer},
416+
)
417+
if err != nil {
418+
log.Errorf("Unable to get funding out for %v: %v ",
419+
d.ID, err)
420+
421+
return err
422+
}
423+
}
424+
425+
log.Infof("Deposit %v is funded in anchor %x:%d, "+
426+
"anchor tx block height: %v", d.ID,
427+
transfer.AnchorTxHash, outIndex, transfer.AnchorTxBlockHeight)
428+
429+
// If the deposit is confirmed, then we don't need to wait for the
430+
// confirmation to happen.
431+
// TODO(bhandras): once backlog events are supported we can remove this.
432+
if transfer.AnchorTxBlockHeight != 0 {
433+
return m.markDepositConfirmed(ctx, d, transfer)
434+
}
435+
436+
// Wait for deposit confirmation otherwise.
437+
err = m.waitForDepositConfirmation(m.runCtx(), d)
438+
if err != nil {
439+
log.Errorf("Unable to wait for deposit confirmation: %v", err)
440+
441+
return err
442+
}
443+
444+
return nil
445+
}
446+
447+
// isDepositFunded checks if the deposit is funded with the expected amount. It
448+
// does so by checking if there is a deposit output with the expected keys and
449+
// amount in the list of transfers of the funder.
450+
func (m *Manager) isDepositFunded(ctx context.Context, d *Deposit) (bool,
451+
*taprpc.AssetTransfer, int, error) {
452+
453+
res, err := m.tapClient.ListTransfers(
454+
ctx, &taprpc.ListTransfersRequest{},
455+
)
456+
if err != nil {
457+
return false, nil, 0, err
458+
}
459+
460+
transfer, outIndex, err := d.GetMatchingOut(d.Amount, res.Transfers)
461+
if err != nil {
462+
return false, nil, 0, err
463+
}
464+
465+
if transfer == nil {
466+
return false, nil, 0, nil
467+
}
468+
469+
return true, transfer, outIndex, nil
470+
}
471+
472+
// waitForDepositConfirmation waits for the deposit to be confirmed.
473+
//
474+
// NOTE: currently SubscribeSendEvents does not support streaming backlog
475+
// events. To avoid missing the confirmation event, we also poll asset transfers
476+
// upon restart.
477+
func (m *Manager) waitForDepositConfirmation(ctx context.Context,
478+
d *Deposit) error {
479+
480+
log.Infof("Subscribing to send events for pending deposit %s, "+
481+
"addr=%v, created_at=%v", d.ID, d.Addr, d.CreatedAt)
482+
483+
resChan, errChan, err := m.tapClient.WaitForSendComplete(
484+
ctx, nil, d.label(),
485+
)
486+
if err != nil {
487+
log.Errorf("unable to subscribe to send events: %v", err)
488+
return err
489+
}
490+
491+
go func() {
492+
select {
493+
case res := <-resChan:
494+
done, err := m.scheduleNextCall()
495+
if err != nil {
496+
log.Errorf("Unable to schedule next call: %v",
497+
err)
498+
499+
m.criticalError(err)
500+
}
501+
defer done()
502+
503+
err = m.markDepositConfirmed(ctx, d, res.Transfer)
504+
if err != nil {
505+
log.Errorf("Unable to mark deposit %v as "+
506+
"confirmed: %v", d.ID, err)
507+
508+
m.criticalError(err)
509+
}
510+
511+
case err := <-errChan:
512+
m.criticalError(err)
513+
}
514+
}()
515+
516+
return nil
517+
}
518+
519+
// cacheProofInfo caches the proof information for the deposit in-memory.
520+
func (m *Manager) cacheProofInfo(ctx context.Context, d *Deposit) error {
521+
proofFile, err := d.ExportProof(ctx, m.tapClient, d.Outpoint)
522+
if err != nil {
523+
log.Errorf("Unable to export proof for deposit %v: %v", d.ID,
524+
err)
525+
526+
return err
527+
}
528+
529+
// Import the proof in order to be able to spend the deposit later on
530+
// either into an HTLC or a timeout sweep.
531+
//
532+
// TODO(bhandras): do we need to check/handle if/when the proof is
533+
// already imported?
534+
depositProof, err := m.tapClient.ImportProofFile(
535+
ctx, proofFile.RawProofFile,
536+
)
537+
if err != nil {
538+
return err
539+
}
540+
541+
d.Proof = depositProof
542+
543+
// Verify that the proof is valid for the deposit and get the root hash
544+
// which we may use later when signing the HTLC transaction.
545+
anchorRootHash, err := d.VerifyProof(depositProof)
546+
if err != nil {
547+
log.Errorf("failed to verify deposity proof: %v", err)
548+
549+
return err
550+
}
551+
552+
d.AnchorRootHash = anchorRootHash
553+
554+
return nil
555+
}
556+
557+
// markDepositConfirmed marks the deposit as confirmed in the store and moves it
558+
// to the active deposits map. It also updates the outpoint and the confirmation
559+
// height of the deposit.
560+
func (m *Manager) markDepositConfirmed(ctx context.Context, d *Deposit,
561+
transfer *taprpc.AssetTransfer) error {
562+
563+
// Extract the funding outpoint from the transfer.
564+
_, outIdx, err := d.GetMatchingOut(
565+
d.Amount, []*taprpc.AssetTransfer{transfer},
566+
)
567+
if err != nil {
568+
return err
569+
}
570+
571+
outpoint, err := wire.NewOutPointFromString(
572+
transfer.Outputs[outIdx].Anchor.Outpoint,
573+
)
574+
if err != nil {
575+
log.Errorf("Unable to parse deposit outpoint %v: %v",
576+
transfer.Outputs[outIdx].Anchor.Outpoint, err)
577+
578+
return err
579+
}
580+
581+
d.Outpoint = outpoint
582+
d.PkScript = transfer.Outputs[outIdx].Anchor.PkScript
583+
d.ConfirmationHeight = transfer.AnchorTxBlockHeight
584+
d.State = StateConfirmed
585+
586+
err = m.handleDepositStateUpdate(ctx, d)
587+
if err != nil {
588+
return err
589+
}
590+
591+
err = m.cacheProofInfo(ctx, d)
592+
if err != nil {
593+
return err
594+
}
595+
596+
log.Infof("Deposit %v is confirmed at block %v", d.ID,
597+
d.ConfirmationHeight)
598+
599+
return nil
600+
}

0 commit comments

Comments
 (0)