staticutil: refactor methods into utils

This commit is contained in:
Slyghtning 2025-05-21 14:08:54 +02:00
parent e77dc53760
commit a918842374
No known key found for this signature in database
GPG key ID: F82D456EA023C9BF
8 changed files with 505 additions and 71 deletions

View file

@ -76,6 +76,17 @@ func (m *mockStaticAddressClient) ServerWithdrawDeposits(ctx context.Context,
args.Error(1)
}
func (m *mockStaticAddressClient) ServerPsbtWithdrawDeposits(ctx context.Context,
in *swapserverrpc.ServerPsbtWithdrawRequest,
opts ...grpc.CallOption) (*swapserverrpc.ServerPsbtWithdrawResponse,
error) {
args := m.Called(ctx, in, opts)
return args.Get(0).(*swapserverrpc.ServerPsbtWithdrawResponse),
args.Error(1)
}
func (m *mockStaticAddressClient) ServerNewAddress(ctx context.Context,
in *swapserverrpc.ServerNewAddressRequest, opts ...grpc.CallOption) (
*swapserverrpc.ServerNewAddressResponse, error) {

View file

@ -93,6 +93,17 @@ func (m *mockStaticAddressClient) ServerWithdrawDeposits(ctx context.Context,
args.Error(1)
}
func (m *mockStaticAddressClient) ServerPsbtWithdrawDeposits(ctx context.Context,
in *swapserverrpc.ServerPsbtWithdrawRequest,
opts ...grpc.CallOption) (*swapserverrpc.ServerPsbtWithdrawResponse,
error) {
args := m.Called(ctx, in, opts)
return args.Get(0).(*swapserverrpc.ServerPsbtWithdrawResponse),
args.Error(1)
}
func (m *mockStaticAddressClient) ServerNewAddress(ctx context.Context,
in *swapserverrpc.ServerNewAddressRequest, opts ...grpc.CallOption) (
*swapserverrpc.ServerNewAddressResponse, error) {

View file

@ -17,6 +17,7 @@ import (
"github.com/lightninglabs/loop"
"github.com/lightninglabs/loop/fsm"
"github.com/lightninglabs/loop/staticaddr/deposit"
"github.com/lightninglabs/loop/staticaddr/staticutil"
"github.com/lightninglabs/loop/staticaddr/version"
"github.com/lightninglabs/loop/swap"
"github.com/lightninglabs/loop/swapserverrpc"
@ -318,8 +319,11 @@ func (f *FSM) SignHtlcTxAction(ctx context.Context,
// Create a musig2 session for each deposit and different htlc tx fee
// rates.
createSession := f.loopIn.createMusig2Sessions
htlcSessions, clientHtlcNonces, err := createSession(ctx, f.cfg.Signer)
createSession := staticutil.CreateMusig2Sessions
htlcSessions, clientHtlcNonces, err := createSession(
ctx, f.cfg.Signer, f.loopIn.Deposits, f.loopIn.AddressParams,
f.loopIn.Address,
)
if err != nil {
err = fmt.Errorf("unable to create musig2 sessions: %w", err)
@ -328,7 +332,8 @@ func (f *FSM) SignHtlcTxAction(ctx context.Context,
defer f.cleanUpSessions(ctx, htlcSessions)
htlcSessionsHighFee, highFeeNonces, err := createSession(
ctx, f.cfg.Signer,
ctx, f.cfg.Signer, f.loopIn.Deposits, f.loopIn.AddressParams,
f.loopIn.Address,
)
if err != nil {
return f.HandleError(err)
@ -336,7 +341,8 @@ func (f *FSM) SignHtlcTxAction(ctx context.Context,
defer f.cleanUpSessions(ctx, htlcSessionsHighFee)
htlcSessionsExtremelyHighFee, extremelyHighNonces, err := createSession(
ctx, f.cfg.Signer,
ctx, f.cfg.Signer, f.loopIn.Deposits, f.loopIn.AddressParams,
f.loopIn.Address,
)
if err != nil {
err = fmt.Errorf("unable to convert nonces: %w", err)

View file

@ -21,6 +21,7 @@ import (
"github.com/lightninglabs/loop/staticaddr/address"
"github.com/lightninglabs/loop/staticaddr/deposit"
"github.com/lightninglabs/loop/staticaddr/script"
"github.com/lightninglabs/loop/staticaddr/staticutil"
"github.com/lightninglabs/loop/staticaddr/version"
"github.com/lightninglabs/loop/swap"
"github.com/lightningnetwork/lnd/input"
@ -169,47 +170,6 @@ func (l *StaticAddressLoopIn) getHtlc(chainParams *chaincfg.Params) (*swap.Htlc,
)
}
// createMusig2Sessions creates a musig2 session for a number of deposits.
func (l *StaticAddressLoopIn) createMusig2Sessions(ctx context.Context,
signer lndclient.SignerClient) ([]*input.MuSig2SessionInfo, [][]byte,
error) {
musig2Sessions := make([]*input.MuSig2SessionInfo, len(l.Deposits))
clientNonces := make([][]byte, len(l.Deposits))
// Create the sessions and nonces from the deposits.
for i := 0; i < len(l.Deposits); i++ {
session, err := l.createMusig2Session(ctx, signer)
if err != nil {
return nil, nil, err
}
musig2Sessions[i] = session
clientNonces[i] = session.PublicNonce[:]
}
return musig2Sessions, clientNonces, nil
}
// Musig2CreateSession creates a musig2 session for the deposit.
func (l *StaticAddressLoopIn) createMusig2Session(ctx context.Context,
signer lndclient.SignerClient) (*input.MuSig2SessionInfo, error) {
signers := [][]byte{
l.AddressParams.ClientPubkey.SerializeCompressed(),
l.AddressParams.ServerPubkey.SerializeCompressed(),
}
expiryLeaf := l.Address.TimeoutLeaf
rootHash := expiryLeaf.TapHash()
return signer.MuSig2CreateSession(
ctx, input.MuSig2Version100RC2, &l.AddressParams.KeyLocator,
signers, lndclient.MuSig2TaprootTweakOpt(rootHash[:], false),
)
}
// signMusig2Tx adds the server nonces to the musig2 sessions and signs the
// transaction.
func (l *StaticAddressLoopIn) signMusig2Tx(ctx context.Context,
@ -217,7 +177,9 @@ func (l *StaticAddressLoopIn) signMusig2Tx(ctx context.Context,
musig2sessions []*input.MuSig2SessionInfo,
counterPartyNonces [][musig2.PubNonceSize]byte) ([][]byte, error) {
prevOuts, err := l.toPrevOuts(l.Deposits, l.AddressParams.PkScript)
prevOuts, err := staticutil.ToPrevOuts(
l.Deposits, l.AddressParams.PkScript,
)
if err != nil {
return nil, err
}
@ -523,29 +485,6 @@ func (l *StaticAddressLoopIn) Outpoints() []wire.OutPoint {
return outpoints
}
func (l *StaticAddressLoopIn) toPrevOuts(deposits []*deposit.Deposit,
pkScript []byte) (map[wire.OutPoint]*wire.TxOut, error) {
prevOuts := make(map[wire.OutPoint]*wire.TxOut, len(deposits))
for _, d := range deposits {
outpoint := wire.OutPoint{
Hash: d.Hash,
Index: d.Index,
}
txOut := &wire.TxOut{
Value: int64(d.Value),
PkScript: pkScript,
}
if _, ok := prevOuts[outpoint]; ok {
return nil, fmt.Errorf("duplicate outpoint %v",
outpoint)
}
prevOuts[outpoint] = txOut
}
return prevOuts, nil
}
// GetState returns the current state of the loop-in swap.
func (l *StaticAddressLoopIn) GetState() fsm.StateType {
l.mu.Lock()

View file

@ -21,6 +21,7 @@ import (
"github.com/lightninglabs/loop/labels"
"github.com/lightninglabs/loop/staticaddr/address"
"github.com/lightninglabs/loop/staticaddr/deposit"
"github.com/lightninglabs/loop/staticaddr/staticutil"
"github.com/lightninglabs/loop/swapserverrpc"
"github.com/lightningnetwork/lnd/input"
"github.com/lightningnetwork/lnd/lntypes"
@ -391,8 +392,8 @@ func (m *Manager) handleLoopInSweepReq(ctx context.Context,
)
copy(serverNonce[:], nonce)
musig2Session, err := loopIn.createMusig2Session(
ctx, m.cfg.Signer,
musig2Session, err := staticutil.CreateMusig2Session(
ctx, m.cfg.Signer, loopIn.AddressParams, loopIn.Address,
)
if err != nil {
return err

View file

@ -0,0 +1,25 @@
package staticutil
import (
"fmt"
"github.com/btcsuite/btcd/wire"
"github.com/lightningnetwork/lnd/lnrpc"
)
// ToWireOutpoints converts lnrpc.OutPoint protos into wire.OutPoint structs so
// they can be consumed by lower level transaction building code.
func ToWireOutpoints(outpoints []*lnrpc.OutPoint) ([]wire.OutPoint, error) {
serverOutpoints := make([]wire.OutPoint, 0, len(outpoints))
for _, o := range outpoints {
outpointStr := fmt.Sprintf("%s:%d", o.TxidStr, o.OutputIndex)
newOutpoint, err := wire.NewOutPointFromString(outpointStr)
if err != nil {
return nil, err
}
serverOutpoints = append(serverOutpoints, *newOutpoint)
}
return serverOutpoints, nil
}

View file

@ -0,0 +1,205 @@
package staticutil
import (
"bytes"
"context"
"fmt"
"sort"
"github.com/btcsuite/btcd/btcutil"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/wire"
"github.com/lightninglabs/lndclient"
"github.com/lightninglabs/loop/staticaddr/address"
"github.com/lightninglabs/loop/staticaddr/deposit"
"github.com/lightninglabs/loop/staticaddr/script"
"github.com/lightninglabs/loop/swapserverrpc"
"github.com/lightningnetwork/lnd/input"
"github.com/lightningnetwork/lnd/lnwallet"
)
// ToPrevOuts converts a slice of deposits to a map of outpoints to TxOuts.
func ToPrevOuts(deposits []*deposit.Deposit,
pkScript []byte) (map[wire.OutPoint]*wire.TxOut, error) {
prevOuts := make(map[wire.OutPoint]*wire.TxOut, len(deposits))
for _, d := range deposits {
outpoint := wire.OutPoint{
Hash: d.Hash,
Index: d.Index,
}
txOut := &wire.TxOut{
Value: int64(d.Value),
PkScript: pkScript,
}
if _, ok := prevOuts[outpoint]; ok {
return nil, fmt.Errorf("duplicate outpoint %v",
outpoint)
}
prevOuts[outpoint] = txOut
}
return prevOuts, nil
}
// CreateMusig2Sessions creates a musig2 session for a number of deposits.
func CreateMusig2Sessions(ctx context.Context,
signer lndclient.SignerClient, deposits []*deposit.Deposit,
addrParams *address.Parameters,
staticAddress *script.StaticAddress) ([]*input.MuSig2SessionInfo,
[][]byte, error) {
musig2Sessions := make([]*input.MuSig2SessionInfo, len(deposits))
clientNonces := make([][]byte, len(deposits))
// Create the sessions and nonces from the deposits.
for i := 0; i < len(deposits); i++ {
session, err := CreateMusig2Session(
ctx, signer, addrParams, staticAddress,
)
if err != nil {
return nil, nil, err
}
musig2Sessions[i] = session
clientNonces[i] = session.PublicNonce[:]
}
return musig2Sessions, clientNonces, nil
}
// CreateMusig2SessionsPerDeposit creates a musig2 session for a number of
// deposits.
func CreateMusig2SessionsPerDeposit(ctx context.Context,
signer lndclient.SignerClient, deposits []*deposit.Deposit,
addrParams *address.Parameters,
staticAddress *script.StaticAddress) (
map[string]*input.MuSig2SessionInfo, map[string][]byte, map[string]int,
error) {
sessions := make(map[string]*input.MuSig2SessionInfo)
nonces := make(map[string][]byte)
depositToIdx := make(map[string]int)
// Create the musig2 sessions for the sweepless sweep tx.
for i, deposit := range deposits {
session, err := CreateMusig2Session(
ctx, signer, addrParams, staticAddress,
)
if err != nil {
return nil, nil, nil, err
}
sessions[deposit.String()] = session
nonces[deposit.String()] = session.PublicNonce[:]
depositToIdx[deposit.String()] = i
}
return sessions, nonces, depositToIdx, nil
}
// CreateMusig2Session creates a musig2 session for the deposit.
func CreateMusig2Session(ctx context.Context,
signer lndclient.SignerClient, addrParams *address.Parameters,
staticAddress *script.StaticAddress) (*input.MuSig2SessionInfo, error) {
signers := [][]byte{
addrParams.ClientPubkey.SerializeCompressed(),
addrParams.ServerPubkey.SerializeCompressed(),
}
expiryLeaf := staticAddress.TimeoutLeaf
rootHash := expiryLeaf.TapHash()
return signer.MuSig2CreateSession(
ctx, input.MuSig2Version100RC2, &addrParams.KeyLocator,
signers, lndclient.MuSig2TaprootTweakOpt(rootHash[:], false),
)
}
// GetPrevoutInfo converts a map of prevOuts to protobuf.
func GetPrevoutInfo(prevOuts map[wire.OutPoint]*wire.TxOut,
) []*swapserverrpc.PrevoutInfo {
prevoutInfos := make([]*swapserverrpc.PrevoutInfo, 0, len(prevOuts))
for outpoint, txOut := range prevOuts {
prevoutInfo := &swapserverrpc.PrevoutInfo{
TxidBytes: outpoint.Hash[:],
OutputIndex: outpoint.Index,
Value: uint64(txOut.Value),
PkScript: txOut.PkScript,
}
prevoutInfos = append(prevoutInfos, prevoutInfo)
}
// Sort UTXOs by txid:index using BIP-0069 rule. The function is used
// in unit tests a lot, and it is useful to make it deterministic.
sort.Slice(prevoutInfos, func(i, j int) bool {
return bip69inputLess(prevoutInfos[i], prevoutInfos[j])
})
return prevoutInfos
}
// bip69inputLess returns true if input1 < input2 according to BIP-0069
// First sort based on input hash (reversed / rpc-style), then index.
// The code is based on btcd/btcutil/txsort/txsort.go.
func bip69inputLess(input1, input2 *swapserverrpc.PrevoutInfo) bool {
// Input hashes are the same, so compare the index.
var ihash, jhash chainhash.Hash
copy(ihash[:], input1.TxidBytes)
copy(jhash[:], input2.TxidBytes)
if ihash == jhash {
return input1.OutputIndex < input2.OutputIndex
}
// At this point, the hashes are not equal, so reverse them to
// big-endian and return the result of the comparison.
const hashSize = chainhash.HashSize
for b := 0; b < hashSize/2; b++ {
ihash[b], ihash[hashSize-1-b] = ihash[hashSize-1-b], ihash[b]
jhash[b], jhash[hashSize-1-b] = jhash[hashSize-1-b], jhash[b]
}
return bytes.Compare(ihash[:], jhash[:]) == -1
}
// SelectDeposits sorts the deposits by amount in descending order. It then
// selects the deposits that are needed to cover the amount requested without
// leaving a dust change. It returns an error if the sum of deposits minus dust
// is less than the requested amount.
func SelectDeposits(deposits []*deposit.Deposit, amount int64) (
[]*deposit.Deposit, error) {
// Check that sum of deposits covers the swap amount while leaving no
// dust change.
dustLimit := lnwallet.DustLimitForSize(input.P2TRSize)
var depositSum btcutil.Amount
for _, deposit := range deposits {
depositSum += deposit.Value
}
if depositSum-dustLimit < btcutil.Amount(amount) {
return nil, fmt.Errorf("insufficient funds to cover swap " +
"amount, try manually selecting deposits")
}
// Sort the deposits by amount in descending order.
sort.Slice(deposits, func(i, j int) bool {
return deposits[i].Value > deposits[j].Value
})
// Select the deposits that are needed to cover the swap amount without
// leaving a dust change.
var selectedDeposits []*deposit.Deposit
var selectedAmount btcutil.Amount
for _, deposit := range deposits {
if selectedAmount >= btcutil.Amount(amount)+dustLimit {
break
}
selectedDeposits = append(selectedDeposits, deposit)
selectedAmount += deposit.Value
}
return selectedDeposits, nil
}

View file

@ -0,0 +1,236 @@
package staticutil
import (
"bytes"
"context"
"testing"
"github.com/btcsuite/btcd/btcec/v2"
"github.com/btcsuite/btcd/btcutil"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/wire"
"github.com/lightninglabs/loop/staticaddr/address"
"github.com/lightninglabs/loop/staticaddr/deposit"
"github.com/lightninglabs/loop/staticaddr/script"
"github.com/lightninglabs/loop/swapserverrpc"
looptest "github.com/lightninglabs/loop/test"
"github.com/lightningnetwork/lnd/input"
"github.com/lightningnetwork/lnd/keychain"
"github.com/stretchr/testify/require"
)
// mustHash converts a hex string to a chainhash.Hash and panics on error.
func mustHash(t *testing.T, s string) chainhash.Hash {
t.Helper()
h, err := chainhash.NewHashFromStr(s)
require.NoError(t, err)
return *h
}
func TestToPrevOuts_Success(t *testing.T) {
// Prepare two distinct deposits with different outpoints and values.
d1 := &deposit.Deposit{
OutPoint: wire.OutPoint{
Hash: mustHash(t, "0000000000000000000000000000000000000000000000000000000000000001"),
Index: 0,
},
Value: btcutil.Amount(12345),
}
d2 := &deposit.Deposit{
OutPoint: wire.OutPoint{
Hash: mustHash(t, "1111111111111111111111111111111111111111111111111111111111111111"),
Index: 7,
},
Value: btcutil.Amount(987654321),
}
pkScript := []byte{0x51, 0x21, 0x02, 0x52} // arbitrary bytes
prevOuts, err := ToPrevOuts([]*deposit.Deposit{d1, d2}, pkScript)
require.NoError(t, err)
// We expect two entries.
require.Len(t, prevOuts, 2)
// Check the first outpoint mapping.
txOut1, ok := prevOuts[d1.OutPoint]
require.True(t, ok, "expected outpoint d1 to be present")
require.EqualValues(t, int64(d1.Value), txOut1.Value)
require.Equal(t, pkScript, txOut1.PkScript)
// Check the second outpoint mapping.
txOut2, ok := prevOuts[d2.OutPoint]
require.True(t, ok, "expected outpoint d2 to be present")
require.EqualValues(t, int64(d2.Value), txOut2.Value)
require.Equal(t, pkScript, txOut2.PkScript)
// Ensure the keys in the map are exactly the outpoints we provided.
for op := range prevOuts {
require.True(t, op == d1.OutPoint || op == d2.OutPoint)
}
}
func TestToPrevOuts_DuplicateOutpoint(t *testing.T) {
// Two deposits that share the exact same outpoint should cause an error.
shared := wire.OutPoint{
Hash: mustHash(t, "2222222222222222222222222222222222222222222222222222222222222222"),
Index: 2,
}
d1 := &deposit.Deposit{OutPoint: shared, Value: btcutil.Amount(100)}
d2 := &deposit.Deposit{OutPoint: shared, Value: btcutil.Amount(200)}
_, err := ToPrevOuts([]*deposit.Deposit{d1, d2}, []byte{0x00})
require.Error(t, err)
}
func TestGetPrevoutInfo_ConversionAndSorting(t *testing.T) {
// Helper to create a hash from string.
must := func(s string) chainhash.Hash {
h, err := chainhash.NewHashFromStr(s)
require.NoError(t, err)
return *h
}
// Choose txids such that after reversal, ordering is determined by the
// last byte of the original hex string.
txidA := must("0000000000000000000000000000000000000000000000000000000000000001")
txidB := must("0000000000000000000000000000000000000000000000000000000000000002")
pkScript := []byte{0xaa, 0xbb}
prevOuts := map[wire.OutPoint]*wire.TxOut{
{Hash: txidA, Index: 5}: {Value: 11, PkScript: pkScript},
{Hash: txidA, Index: 2}: {Value: 22, PkScript: pkScript},
{Hash: txidB, Index: 0}: {Value: 33, PkScript: pkScript},
}
infos := GetPrevoutInfo(prevOuts)
// Expect deterministic ordering:
// 1) All entries with txidA (..01) before txidB (..02) due to BIP-69
// compare on reversed hashes.
// 2) Within txidA, index 2 before index 5.
require.Len(t, infos, 3)
require.Equal(t, &swapserverrpc.PrevoutInfo{
TxidBytes: txidA[:],
OutputIndex: 2,
Value: 22,
PkScript: pkScript,
}, infos[0])
require.Equal(t, &swapserverrpc.PrevoutInfo{
TxidBytes: txidA[:],
OutputIndex: 5,
Value: 11,
PkScript: pkScript,
}, infos[1])
require.Equal(t, &swapserverrpc.PrevoutInfo{
TxidBytes: txidB[:],
OutputIndex: 0,
Value: 33,
PkScript: pkScript,
}, infos[2])
}
func TestBip69InputLess_SameHashIndexOrder(t *testing.T) {
txid := make([]byte, 32)
txid[31] = 0x7f // Arbitrary value.
a := &swapserverrpc.PrevoutInfo{TxidBytes: txid, OutputIndex: 1}
b := &swapserverrpc.PrevoutInfo{TxidBytes: txid, OutputIndex: 3}
require.True(t, bip69inputLess(a, b))
require.False(t, bip69inputLess(b, a))
}
func TestBip69InputLess_DifferentHashes(t *testing.T) {
// txid1 ends with 0x01, txid2 ends with 0x02. After reversing for
// comparison, txid1 should still come before txid2 in lexicographic
// order.
h1, _ := chainhash.NewHashFromStr("0000000000000000000000000000000000000000000000000000000000000001")
h2, _ := chainhash.NewHashFromStr("0000000000000000000000000000000000000000000000000000000000000002")
a := &swapserverrpc.PrevoutInfo{TxidBytes: h1[:], OutputIndex: 9}
b := &swapserverrpc.PrevoutInfo{TxidBytes: h2[:], OutputIndex: 0}
require.True(t, bip69inputLess(a, b))
require.False(t, bip69inputLess(b, a))
}
func TestCreateMusig2Session_Success(t *testing.T) {
// Set up mock signer from loop/test package.
lnd := looptest.NewMockLnd()
signer := lnd.Signer
// Create dummy key material for address parameters.
clientKey, err := btcec.NewPrivateKey()
require.NoError(t, err)
serverKey, err := btcec.NewPrivateKey()
require.NoError(t, err)
params := &address.Parameters{
ClientPubkey: clientKey.PubKey(),
ServerPubkey: serverKey.PubKey(),
Expiry: 10,
PkScript: []byte{0x51},
KeyLocator: keychain.KeyLocator{Family: 1, Index: 2},
}
// Build a static address for tweak options.
staticAddr, err := script.NewStaticAddress(
input.MuSig2Version100RC2, int64(params.Expiry), params.ClientPubkey, params.ServerPubkey,
)
require.NoError(t, err)
sess, err := CreateMusig2Session(context.Background(), signer, params, staticAddr)
require.NoError(t, err)
require.NotNil(t, sess)
}
func TestCreateMusig2Sessions_Multiple(t *testing.T) {
lnd := looptest.NewMockLnd()
signer := lnd.Signer
// Keys/params/static address.
clientKey, err := btcec.NewPrivateKey()
require.NoError(t, err)
serverKey, err := btcec.NewPrivateKey()
require.NoError(t, err)
params := &address.Parameters{
ClientPubkey: clientKey.PubKey(),
ServerPubkey: serverKey.PubKey(),
Expiry: 12,
PkScript: []byte{0xaa},
KeyLocator: keychain.KeyLocator{Family: 9, Index: 8},
}
staticAddr, err := script.NewStaticAddress(
input.MuSig2Version100RC2, int64(params.Expiry), params.ClientPubkey, params.ServerPubkey,
)
require.NoError(t, err)
// Prepare N deposits; only the length matters for session count.
deposits := []*deposit.Deposit{
{OutPoint: wire.OutPoint{Index: 0}},
{OutPoint: wire.OutPoint{Index: 1}},
{OutPoint: wire.OutPoint{Index: 2}},
}
sessions, nonces, err := CreateMusig2Sessions(
context.Background(), signer, deposits, params, staticAddr,
)
require.NoError(t, err)
require.Len(t, sessions, len(deposits))
require.Len(t, nonces, len(deposits))
// The mock signer returns a zero-value PublicNonce; assert consistency.
for i := range sessions {
require.NotNil(t, sessions[i])
require.True(t, bytes.Equal(nonces[i], sessions[i].PublicNonce[:]))
}
}