loopd: add loop in state InvoiceSettled

pull/34/head
Joost Jager 5 years ago
parent a7f4feb9ea
commit b5d2fb3894
No known key found for this signature in database
GPG Key ID: A61B9D4C393C59C7

@ -95,6 +95,8 @@ func (s *swapClientServer) marshallSwap(loopSwap *loop.SwapInfo) (
state = looprpc.SwapState_PREIMAGE_REVEALED
case loopdb.StateHtlcPublished:
state = looprpc.SwapState_HTLC_PUBLISHED
case loopdb.StateInvoiceSettled:
state = looprpc.SwapState_INVOICE_SETTLED
case loopdb.StateSuccess:
state = looprpc.SwapState_SUCCESS
default:

@ -51,6 +51,10 @@ const (
// StateHtlcPublished means that the client published the on-chain htlc.
StateHtlcPublished = 8
// StateInvoiceSettled means that the swap invoice has been paid by the
// server.
StateInvoiceSettled = 9
)
// SwapStateType defines the types of swap states that exist. Every swap state
@ -114,6 +118,9 @@ func (s SwapState) String() string {
case StateFailTemporary:
return "FailTemporary"
case StateInvoiceSettled:
return "InvoiceSettled"
default:
return "Unknown"
}

@ -252,7 +252,7 @@ func (s *loopInSwap) execute(mainCtx context.Context,
// permanently and losing funds.
if err != nil {
s.log.Errorf("Swap error: %v", err)
s.state = loopdb.StateFailTemporary
s.setState(loopdb.StateFailTemporary)
// If we cannot send out this update, there is nothing we can do.
_ = s.sendUpdate(mainCtx)
@ -282,7 +282,7 @@ func (s *loopInSwap) executeSwap(globalCtx context.Context) error {
// If an external htlc was indicated, we can move to the
// HtlcPublished state directly and wait for
// confirmation.
s.state = loopdb.StateHtlcPublished
s.setState(loopdb.StateHtlcPublished)
err = s.persistState(globalCtx)
if err != nil {
return err
@ -320,41 +320,7 @@ func (s *loopInSwap) executeSwap(globalCtx context.Context) error {
// invoice, receive the preimage and sweep the htlc. We are waiting for
// this to happen and simultaneously watch the htlc expiry height. When
// the htlc expires, we will publish a timeout tx to reclaim the funds.
spend, err := s.waitForHtlcSpend(globalCtx, htlcOutpoint)
if err != nil {
return err
}
// Determine the htlc input of the spending tx and inspect the witness
// to findout whether a success or a timeout tx spend the htlc.
htlcInput := spend.SpendingTx.TxIn[spend.SpenderInputIndex]
if s.htlc.IsSuccessWitness(htlcInput.Witness) {
s.state = loopdb.StateSuccess
// Server swept the htlc. The htlc value can be added to the
// server cost balance.
s.cost.Server += htlcValue
} else {
s.state = loopdb.StateFailTimeout
// Now that the timeout tx confirmed, we can safely cancel the
// swap invoice. We still need to query the final invoice state.
// This is not a hodl invoice, so it may be that the invoice was
// already settled. This means that the server didn't succeed in
// sweeping the htlc after paying the invoice.
err := s.lnd.Invoices.CancelInvoice(globalCtx, s.hash)
if err != nil && err != channeldb.ErrInvoiceAlreadySettled {
return err
}
// TODO: Add miner fee of timeout tx to swap cost balance.
}
// Wait for a final state of the swap invoice. It should either be
// settled because the server successfully paid it or canceled because
// we canceled after our timeout tx confirmed.
err = s.waitForSwapInvoiceResult(globalCtx)
err = s.waitForSwapComplete(globalCtx, htlcOutpoint, htlcValue)
if err != nil {
return err
}
@ -411,7 +377,7 @@ func (s *loopInSwap) publishOnChainHtlc(ctx context.Context) (bool, error) {
// Verify whether it still makes sense to publish the htlc.
if blocksRemaining < MinLoopInPublishDelta {
s.state = loopdb.StateFailTimeout
s.setState(loopdb.StateFailTimeout)
return false, s.persistState(ctx)
}
@ -425,7 +391,7 @@ func (s *loopInSwap) publishOnChainHtlc(ctx context.Context) (bool, error) {
// Transition to state HtlcPublished before calling SendOutputs to
// prevent us from ever paying multiple times after a crash.
s.state = loopdb.StateHtlcPublished
s.setState(loopdb.StateHtlcPublished)
err = s.persistState(ctx)
if err != nil {
return false, err
@ -448,10 +414,11 @@ func (s *loopInSwap) publishOnChainHtlc(ctx context.Context) (bool, error) {
}
// waitForHtlcSpend waits until a spending tx of the htlc gets confirmed and
// returns the spend details.
func (s *loopInSwap) waitForHtlcSpend(ctx context.Context,
htlc *wire.OutPoint) (*chainntnfs.SpendDetail, error) {
// waitForSwapComplete waits until a spending tx of the htlc gets confirmed and
// the swap invoice is either settled or canceled. If the htlc times out, the
// timeout tx will be published.
func (s *loopInSwap) waitForSwapComplete(ctx context.Context,
htlc *wire.OutPoint, htlcValue btcutil.Amount) error {
// Register the htlc spend notification.
rpcCtx, cancel := context.WithCancel(ctx)
@ -460,58 +427,61 @@ func (s *loopInSwap) waitForHtlcSpend(ctx context.Context,
rpcCtx, nil, s.htlc.ScriptHash, s.InitiationHeight,
)
if err != nil {
return nil, fmt.Errorf("register spend ntfn: %v", err)
return fmt.Errorf("register spend ntfn: %v", err)
}
for {
// Register for swap invoice updates.
rpcCtx, cancel = context.WithCancel(ctx)
defer cancel()
s.log.Infof("Subscribing to swap invoice %v", s.hash)
swapInvoiceChan, swapInvoiceErr, err := s.lnd.Invoices.SubscribeSingleInvoice(
rpcCtx, s.hash,
)
if err != nil {
return fmt.Errorf("subscribe to swap invoice: %v", err)
}
htlcSpend := false
invoiceFinalized := false
for !htlcSpend || !invoiceFinalized {
select {
// Spend notification error.
case err := <-spendErr:
return nil, err
return err
// Receive block epochs and start publishing the timeout tx
// whenever possible.
case notification := <-s.blockEpochChan:
s.height = notification.(int32)
if s.height >= s.LoopInContract.CltvExpiry {
err := s.publishTimeoutTx(ctx, htlc)
if err != nil {
return nil, err
return err
}
}
// Htlc spend, break loop.
// The htlc spend is confirmed. Inspect the spending tx to
// determine the final swap state.
case spendDetails := <-spendChan:
s.log.Infof("Htlc spend by tx: %v",
spendDetails.SpenderTxHash)
return spendDetails, nil
case <-ctx.Done():
return nil, ctx.Err()
}
}
}
// waitForSwapPaid waits until our swap invoice gets paid by the server.
func (s *loopInSwap) waitForSwapInvoiceResult(ctx context.Context) error {
// Wait for swap invoice to be paid.
rpcCtx, cancel := context.WithCancel(ctx)
defer cancel()
s.log.Infof("Subscribing to swap invoice %v", s.hash)
swapInvoiceChan, swapInvoiceErr, err := s.lnd.Invoices.SubscribeSingleInvoice(
rpcCtx, s.hash,
)
if err != nil {
return err
}
err := s.processHtlcSpend(
ctx, spendDetails, htlcValue,
)
if err != nil {
return err
}
for {
select {
htlcSpend = true
// Swap invoice ntfn error.
case err := <-swapInvoiceErr:
return err
// An update to the swap invoice occured. Check the new state
// and update the swap state accordingly.
case update := <-swapInvoiceChan:
s.log.Infof("Received swap invoice update: %v",
update.State)
@ -521,18 +491,67 @@ func (s *loopInSwap) waitForSwapInvoiceResult(ctx context.Context) error {
// Swap invoice was paid, so update server cost balance.
case channeldb.ContractSettled:
s.cost.Server -= update.AmtPaid
return nil
// If invoice settlement and htlc spend happen
// in the expected order, move the swap to an
// intermediate state that indicates that the
// swap is complete from the user point of view,
// but still incomplete with regards to
// accounting data.
if s.state == loopdb.StateHtlcPublished {
s.setState(loopdb.StateInvoiceSettled)
err := s.persistState(ctx)
if err != nil {
return err
}
}
invoiceFinalized = true
// Canceled invoice has no effect on server cost
// balance.
case channeldb.ContractCanceled:
return nil
invoiceFinalized = true
}
case <-ctx.Done():
return ctx.Err()
}
}
return nil
}
func (s *loopInSwap) processHtlcSpend(ctx context.Context,
spend *chainntnfs.SpendDetail, htlcValue btcutil.Amount) error {
// Determine the htlc input of the spending tx and inspect the witness
// to findout whether a success or a timeout tx spend the htlc.
htlcInput := spend.SpendingTx.TxIn[spend.SpenderInputIndex]
if s.htlc.IsSuccessWitness(htlcInput.Witness) {
s.setState(loopdb.StateSuccess)
// Server swept the htlc. The htlc value can be added to the
// server cost balance.
s.cost.Server += htlcValue
} else {
s.setState(loopdb.StateFailTimeout)
// Now that the timeout tx confirmed, we can safely cancel the
// swap invoice. We still need to query the final invoice state.
// This is not a hodl invoice, so it may be that the invoice was
// already settled. This means that the server didn't succeed in
// sweeping the htlc after paying the invoice.
err := s.lnd.Invoices.CancelInvoice(ctx, s.hash)
if err != nil && err != channeldb.ErrInvoiceAlreadySettled {
return err
}
// TODO: Add miner fee of timeout tx to swap cost balance.
}
return nil
}
// publishTimeoutTx publishes a timeout tx after the on-chain htlc has expired.
@ -582,12 +601,8 @@ func (s *loopInSwap) publishTimeoutTx(ctx context.Context,
// persistState updates the swap state and sends out an update notification.
func (s *loopInSwap) persistState(ctx context.Context) error {
updateTime := time.Now()
s.lastUpdateTime = updateTime
// Update state in store.
err := s.store.UpdateLoopIn(s.hash, updateTime, s.state)
err := s.store.UpdateLoopIn(s.hash, s.lastUpdateTime, s.state)
if err != nil {
return err
}
@ -595,3 +610,9 @@ func (s *loopInSwap) persistState(ctx context.Context) error {
// Send out swap update
return s.sendUpdate(ctx)
}
// setState updates the swap state and last update timestamp.
func (s *loopInSwap) setState(state loopdb.SwapState) {
s.lastUpdateTime = time.Now()
s.state = state
}

@ -76,17 +76,6 @@ func TestLoopInSuccess(t *testing.T) {
// Client starts listening for spend of htlc.
<-ctx.lnd.RegisterSpendChannel
// Server spends htlc.
successTx := wire.MsgTx{}
successTx.AddTxIn(&wire.TxIn{
Witness: [][]byte{{}, {}, {}},
})
ctx.lnd.SpendChannel <- &chainntnfs.SpendDetail{
SpendingTx: &successTx,
SpenderInputIndex: 0,
}
// Client starts listening for swap invoice updates.
subscription := <-ctx.lnd.SingleInvoiceSubcribeChannel
if subscription.Hash != ctx.server.swapHash {
@ -100,6 +89,21 @@ func TestLoopInSuccess(t *testing.T) {
AmtPaid: 49000,
}
// Swap is expected to move to the state InvoiceSettled
ctx.assertState(loopdb.StateInvoiceSettled)
ctx.store.assertLoopInState(loopdb.StateInvoiceSettled)
// Server spends htlc.
successTx := wire.MsgTx{}
successTx.AddTxIn(&wire.TxIn{
Witness: [][]byte{{}, {}, {}},
})
ctx.lnd.SpendChannel <- &chainntnfs.SpendDetail{
SpendingTx: &successTx,
SpenderInputIndex: 0,
}
ctx.assertState(loopdb.StateSuccess)
ctx.store.assertLoopInState(loopdb.StateSuccess)
@ -162,6 +166,12 @@ func TestLoopInTimeout(t *testing.T) {
// Client starts listening for spend of htlc.
<-ctx.lnd.RegisterSpendChannel
// Client starts listening for swap invoice updates.
subscription := <-ctx.lnd.SingleInvoiceSubcribeChannel
if subscription.Hash != ctx.server.swapHash {
t.Fatal("client subscribing to wrong invoice")
}
// Let htlc expire.
ctx.blockEpochChan <- swap.LoopInContract.CltvExpiry
@ -178,12 +188,6 @@ func TestLoopInTimeout(t *testing.T) {
// safely cancel the swap invoice.
<-ctx.lnd.FailInvoiceChannel
// Client starts listening for swap invoice updates.
subscription := <-ctx.lnd.SingleInvoiceSubcribeChannel
if subscription.Hash != ctx.server.swapHash {
t.Fatal("client subscribing to wrong invoice")
}
// Signal the the invoice was canceled.
subscription.Update <- lndclient.InvoiceUpdate{
State: channeldb.ContractCanceled,
@ -340,19 +344,6 @@ func testLoopInResume(t *testing.T, state loopdb.SwapState, expired bool) {
// Client starts listening for spend of htlc.
<-ctx.lnd.RegisterSpendChannel
// Server spends htlc.
successTx := wire.MsgTx{}
successTx.AddTxIn(&wire.TxIn{
Witness: [][]byte{{}, {}, {}},
})
successTxHash := successTx.TxHash()
ctx.lnd.SpendChannel <- &chainntnfs.SpendDetail{
SpendingTx: &successTx,
SpenderTxHash: &successTxHash,
SpenderInputIndex: 0,
}
// Client starts listening for swap invoice updates.
subscription := <-ctx.lnd.SingleInvoiceSubcribeChannel
if subscription.Hash != testPreimage.Hash() {
@ -366,5 +357,22 @@ func testLoopInResume(t *testing.T, state loopdb.SwapState, expired bool) {
AmtPaid: 49000,
}
// Swap is expected to move to the state InvoiceSettled
ctx.assertState(loopdb.StateInvoiceSettled)
ctx.store.assertLoopInState(loopdb.StateInvoiceSettled)
// Server spends htlc.
successTx := wire.MsgTx{}
successTx.AddTxIn(&wire.TxIn{
Witness: [][]byte{{}, {}, {}},
})
successTxHash := successTx.TxHash()
ctx.lnd.SpendChannel <- &chainntnfs.SpendDetail{
SpendingTx: &successTx,
SpenderTxHash: &successTxHash,
SpenderInputIndex: 0,
}
ctx.assertState(loopdb.StateSuccess)
}

@ -46,7 +46,7 @@ func (x SwapType) String() string {
return proto.EnumName(SwapType_name, int32(x))
}
func (SwapType) EnumDescriptor() ([]byte, []int) {
return fileDescriptor_client_e350cb39252fe551, []int{0}
return fileDescriptor_client_ba4b73c10b9bbc2a, []int{0}
}
type SwapState int32
@ -76,6 +76,10 @@ const (
// FAILED is the final swap state for a failed swap with or without loss of
// the swap amount.
SwapState_FAILED SwapState = 4
// *
// INVOICE_SETTLED is reached when the swap invoice in a loop in swap has been
// paid, but we are still waiting for the htlc spend to confirm.
SwapState_INVOICE_SETTLED SwapState = 5
)
var SwapState_name = map[int32]string{
@ -84,6 +88,7 @@ var SwapState_name = map[int32]string{
2: "HTLC_PUBLISHED",
3: "SUCCESS",
4: "FAILED",
5: "INVOICE_SETTLED",
}
var SwapState_value = map[string]int32{
"INITIATED": 0,
@ -91,13 +96,14 @@ var SwapState_value = map[string]int32{
"HTLC_PUBLISHED": 2,
"SUCCESS": 3,
"FAILED": 4,
"INVOICE_SETTLED": 5,
}
func (x SwapState) String() string {
return proto.EnumName(SwapState_name, int32(x))
}
func (SwapState) EnumDescriptor() ([]byte, []int) {
return fileDescriptor_client_e350cb39252fe551, []int{1}
return fileDescriptor_client_ba4b73c10b9bbc2a, []int{1}
}
type LoopOutRequest struct {
@ -155,7 +161,7 @@ func (m *LoopOutRequest) Reset() { *m = LoopOutRequest{} }
func (m *LoopOutRequest) String() string { return proto.CompactTextString(m) }
func (*LoopOutRequest) ProtoMessage() {}
func (*LoopOutRequest) Descriptor() ([]byte, []int) {
return fileDescriptor_client_e350cb39252fe551, []int{0}
return fileDescriptor_client_ba4b73c10b9bbc2a, []int{0}
}
func (m *LoopOutRequest) XXX_Unmarshal(b []byte) error {
return xxx_messageInfo_LoopOutRequest.Unmarshal(m, b)
@ -268,7 +274,7 @@ func (m *LoopInRequest) Reset() { *m = LoopInRequest{} }
func (m *LoopInRequest) String() string { return proto.CompactTextString(m) }
func (*LoopInRequest) ProtoMessage() {}
func (*LoopInRequest) Descriptor() ([]byte, []int) {
return fileDescriptor_client_e350cb39252fe551, []int{1}
return fileDescriptor_client_ba4b73c10b9bbc2a, []int{1}
}
func (m *LoopInRequest) XXX_Unmarshal(b []byte) error {
return xxx_messageInfo_LoopInRequest.Unmarshal(m, b)
@ -340,7 +346,7 @@ func (m *SwapResponse) Reset() { *m = SwapResponse{} }
func (m *SwapResponse) String() string { return proto.CompactTextString(m) }
func (*SwapResponse) ProtoMessage() {}
func (*SwapResponse) Descriptor() ([]byte, []int) {
return fileDescriptor_client_e350cb39252fe551, []int{2}
return fileDescriptor_client_ba4b73c10b9bbc2a, []int{2}
}
func (m *SwapResponse) XXX_Unmarshal(b []byte) error {
return xxx_messageInfo_SwapResponse.Unmarshal(m, b)
@ -384,7 +390,7 @@ func (m *MonitorRequest) Reset() { *m = MonitorRequest{} }
func (m *MonitorRequest) String() string { return proto.CompactTextString(m) }
func (*MonitorRequest) ProtoMessage() {}
func (*MonitorRequest) Descriptor() ([]byte, []int) {
return fileDescriptor_client_e350cb39252fe551, []int{3}
return fileDescriptor_client_ba4b73c10b9bbc2a, []int{3}
}
func (m *MonitorRequest) XXX_Unmarshal(b []byte) error {
return xxx_messageInfo_MonitorRequest.Unmarshal(m, b)
@ -437,7 +443,7 @@ func (m *SwapStatus) Reset() { *m = SwapStatus{} }
func (m *SwapStatus) String() string { return proto.CompactTextString(m) }
func (*SwapStatus) ProtoMessage() {}
func (*SwapStatus) Descriptor() ([]byte, []int) {
return fileDescriptor_client_e350cb39252fe551, []int{4}
return fileDescriptor_client_ba4b73c10b9bbc2a, []int{4}
}
func (m *SwapStatus) XXX_Unmarshal(b []byte) error {
return xxx_messageInfo_SwapStatus.Unmarshal(m, b)
@ -516,7 +522,7 @@ func (m *TermsRequest) Reset() { *m = TermsRequest{} }
func (m *TermsRequest) String() string { return proto.CompactTextString(m) }
func (*TermsRequest) ProtoMessage() {}
func (*TermsRequest) Descriptor() ([]byte, []int) {
return fileDescriptor_client_e350cb39252fe551, []int{5}
return fileDescriptor_client_ba4b73c10b9bbc2a, []int{5}
}
func (m *TermsRequest) XXX_Unmarshal(b []byte) error {
return xxx_messageInfo_TermsRequest.Unmarshal(m, b)
@ -568,7 +574,7 @@ func (m *TermsResponse) Reset() { *m = TermsResponse{} }
func (m *TermsResponse) String() string { return proto.CompactTextString(m) }
func (*TermsResponse) ProtoMessage() {}
func (*TermsResponse) Descriptor() ([]byte, []int) {
return fileDescriptor_client_e350cb39252fe551, []int{6}
return fileDescriptor_client_ba4b73c10b9bbc2a, []int{6}
}
func (m *TermsResponse) XXX_Unmarshal(b []byte) error {
return xxx_messageInfo_TermsResponse.Unmarshal(m, b)
@ -650,7 +656,7 @@ func (m *QuoteRequest) Reset() { *m = QuoteRequest{} }
func (m *QuoteRequest) String() string { return proto.CompactTextString(m) }
func (*QuoteRequest) ProtoMessage() {}
func (*QuoteRequest) Descriptor() ([]byte, []int) {
return fileDescriptor_client_e350cb39252fe551, []int{7}
return fileDescriptor_client_ba4b73c10b9bbc2a, []int{7}
}
func (m *QuoteRequest) XXX_Unmarshal(b []byte) error {
return xxx_messageInfo_QuoteRequest.Unmarshal(m, b)
@ -696,7 +702,7 @@ func (m *QuoteResponse) Reset() { *m = QuoteResponse{} }
func (m *QuoteResponse) String() string { return proto.CompactTextString(m) }
func (*QuoteResponse) ProtoMessage() {}
func (*QuoteResponse) Descriptor() ([]byte, []int) {
return fileDescriptor_client_e350cb39252fe551, []int{8}
return fileDescriptor_client_ba4b73c10b9bbc2a, []int{8}
}
func (m *QuoteResponse) XXX_Unmarshal(b []byte) error {
return xxx_messageInfo_QuoteResponse.Unmarshal(m, b)
@ -1093,66 +1099,67 @@ var _SwapClient_serviceDesc = grpc.ServiceDesc{
Metadata: "client.proto",
}
func init() { proto.RegisterFile("client.proto", fileDescriptor_client_e350cb39252fe551) }
var fileDescriptor_client_e350cb39252fe551 = []byte{
// 925 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x9c, 0x55, 0xdd, 0x8e, 0xdb, 0x44,
0x18, 0xad, 0x9d, 0x6c, 0x7e, 0xbe, 0x75, 0xbc, 0xce, 0xb4, 0xdd, 0x86, 0x40, 0xa5, 0x60, 0x68,
0x89, 0xf6, 0x62, 0x03, 0xdb, 0x0b, 0x04, 0x37, 0x28, 0x4d, 0xd2, 0x6e, 0xa4, 0x6c, 0x37, 0x38,
0x59, 0x24, 0x6e, 0xb0, 0xa6, 0xc9, 0x74, 0x6b, 0xc9, 0x9e, 0x71, 0xed, 0x71, 0x9b, 0x15, 0xe2,
0x86, 0x37, 0x00, 0xde, 0x04, 0xf1, 0x26, 0xbc, 0x02, 0xaf, 0xc0, 0x3d, 0x9a, 0x9f, 0xb8, 0x76,
0xc2, 0xde, 0xf4, 0xce, 0xf9, 0xe6, 0xcc, 0x99, 0xef, 0x3b, 0x73, 0xce, 0x04, 0xac, 0x55, 0x18,
0x10, 0xca, 0x4f, 0xe3, 0x84, 0x71, 0x86, 0xea, 0x21, 0x63, 0x71, 0x12, 0xaf, 0xba, 0x9f, 0x5c,
0x33, 0x76, 0x1d, 0x92, 0x01, 0x8e, 0x83, 0x01, 0xa6, 0x94, 0x71, 0xcc, 0x03, 0x46, 0x53, 0x05,
0x73, 0xff, 0x34, 0xc1, 0x9e, 0x31, 0x16, 0x5f, 0x66, 0xdc, 0x23, 0x6f, 0x32, 0x92, 0x72, 0xe4,
0x40, 0x05, 0x47, 0xbc, 0x63, 0xf4, 0x8c, 0x7e, 0xc5, 0x13, 0x9f, 0x08, 0x41, 0x75, 0x4d, 0x52,
0xde, 0x31, 0x7b, 0x46, 0xbf, 0xe9, 0xc9, 0x6f, 0x34, 0x80, 0x7b, 0x11, 0xde, 0xf8, 0xe9, 0x3b,
0x1c, 0xfb, 0x09, 0xcb, 0x78, 0x40, 0xaf, 0xfd, 0x57, 0x84, 0x74, 0x2a, 0x72, 0x5b, 0x3b, 0xc2,
0x9b, 0xc5, 0x3b, 0x1c, 0x7b, 0x6a, 0xe5, 0x19, 0x21, 0xe8, 0x09, 0x1c, 0x8b, 0x0d, 0x71, 0x42,
0x62, 0x7c, 0x53, 0xda, 0x52, 0x95, 0x5b, 0xee, 0x46, 0x78, 0x33, 0x97, 0x8b, 0x85, 0x4d, 0x3d,
0xb0, 0xf2, 0x53, 0x04, 0xf4, 0x40, 0x42, 0x41, 0xb3, 0x0b, 0xc4, 0xe7, 0x60, 0x17, 0x68, 0x45,
0xe3, 0x35, 0x89, 0xb1, 0x72, 0xba, 0x61, 0xc4, 0x91, 0x0b, 0x2d, 0x81, 0x8a, 0x02, 0x4a, 0x12,
0x49, 0x54, 0x97, 0xa0, 0xc3, 0x08, 0x6f, 0x2e, 0x44, 0x4d, 0x30, 0xf5, 0xc1, 0x11, 0x9a, 0xf9,
0x2c, 0xe3, 0xfe, 0xea, 0x35, 0xa6, 0x94, 0x84, 0x9d, 0x46, 0xcf, 0xe8, 0x57, 0x3d, 0x3b, 0x54,
0x0a, 0x8d, 0x54, 0xd5, 0xfd, 0xcb, 0x80, 0x96, 0x10, 0x6d, 0x4a, 0x6f, 0xd7, 0x6c, 0xb7, 0x73,
0x73, 0xaf, 0xf3, 0xbd, 0x9e, 0x2a, 0xfb, 0x3d, 0x3d, 0x86, 0x23, 0xd9, 0x53, 0x40, 0xf3, 0x96,
0xaa, 0xb2, 0xa5, 0x56, 0x28, 0xcf, 0xd7, 0x1d, 0xa1, 0xcf, 0xa0, 0x45, 0x36, 0x9c, 0x24, 0x14,
0x87, 0xfe, 0x6b, 0x1e, 0xae, 0xa4, 0x50, 0x0d, 0xcf, 0xda, 0x16, 0xcf, 0x79, 0xb8, 0x72, 0x87,
0x60, 0xc9, 0x3b, 0x21, 0x69, 0xcc, 0x68, 0x4a, 0x90, 0x0d, 0x66, 0xb0, 0x96, 0x3d, 0x37, 0x3d,
0x33, 0x58, 0xa3, 0x4f, 0xc1, 0x12, 0x7b, 0x7d, 0xbc, 0x5e, 0x27, 0x24, 0x4d, 0xf5, 0x75, 0x1f,
0x8a, 0xda, 0x50, 0x95, 0x5c, 0x07, 0xec, 0x0b, 0x46, 0x03, 0xce, 0x12, 0x3d, 0xb9, 0xfb, 0xaf,
0x01, 0x20, 0x58, 0x17, 0x1c, 0xf3, 0x2c, 0xfd, 0x1f, 0x21, 0xd4, 0x29, 0x66, 0x7e, 0xca, 0x23,
0xa8, 0xf2, 0x9b, 0x58, 0x4d, 0x6b, 0x9f, 0xb5, 0x4f, 0xb5, 0x4f, 0x4f, 0x05, 0xc9, 0xf2, 0x26,
0x26, 0x9e, 0x5c, 0x46, 0x7d, 0x38, 0x48, 0x39, 0xe6, 0xca, 0x1d, 0xf6, 0x19, 0x2a, 0xe1, 0xc4,
0x61, 0xc4, 0x53, 0x00, 0xf4, 0x05, 0x1c, 0x05, 0x34, 0xe0, 0x81, 0xf4, 0xb5, 0xcf, 0x83, 0x68,
0x6b, 0x13, 0xfb, 0x7d, 0x79, 0x19, 0x44, 0xea, 0x82, 0x71, 0xca, 0xfd, 0x2c, 0x5e, 0x63, 0x4e,
0x14, 0x52, 0x99, 0xc5, 0x16, 0xf5, 0x2b, 0x59, 0x96, 0xc8, 0x5d, 0x25, 0xea, 0xfb, 0x4a, 0xd8,
0x60, 0x2d, 0x49, 0x12, 0xa5, 0x5b, 0x1d, 0x7e, 0x33, 0xa1, 0xa5, 0x0b, 0x5a, 0xde, 0x13, 0x68,
0xcb, 0xdb, 0x8f, 0xf1, 0x4d, 0x44, 0x28, 0xf7, 0x65, 0x84, 0x94, 0xda, 0x47, 0x62, 0x61, 0xae,
0xea, 0x63, 0xe1, 0x1f, 0x17, 0x5a, 0x5b, 0xa7, 0xf8, 0x2f, 0x71, 0xba, 0xb5, 0xcb, 0x61, 0xaa,
0xbc, 0xf2, 0x14, 0xa7, 0xa4, 0x84, 0x49, 0x84, 0x32, 0x95, 0x12, 0xc6, 0x13, 0x5a, 0x3c, 0x04,
0x28, 0x24, 0x41, 0x05, 0xab, 0x19, 0xe7, 0x31, 0x78, 0x0c, 0x47, 0x51, 0x40, 0x95, 0x29, 0x71,
0xc4, 0x32, 0xca, 0xb5, 0x54, 0xad, 0x28, 0xa0, 0x42, 0xd8, 0xa1, 0x2c, 0x4a, 0xdc, 0xd6, 0xbc,
0x1a, 0x57, 0xd3, 0x38, 0xe5, 0x5f, 0x8d, 0x7b, 0x08, 0xb0, 0x0a, 0xf9, 0x5b, 0x7f, 0x4d, 0x42,
0x8e, 0xa5, 0x4a, 0x07, 0x5e, 0x53, 0x54, 0xc6, 0xa2, 0xe0, 0xf6, 0xc0, 0xfa, 0x3e, 0x63, 0x9c,
0xdc, 0x9a, 0x12, 0xf7, 0x15, 0xb4, 0x34, 0x42, 0x8b, 0xf6, 0x11, 0x34, 0xf2, 0xc8, 0x28, 0x5c,
0x5d, 0xcf, 0xb7, 0x33, 0x9b, 0xb9, 0x3b, 0xdb, 0xc7, 0xd0, 0xdc, 0x8d, 0x52, 0x23, 0xd2, 0x39,
0x3a, 0x79, 0x04, 0x8d, 0xad, 0xbf, 0x90, 0x05, 0x8d, 0xd9, 0xe5, 0xe5, 0xdc, 0xbf, 0xbc, 0x5a,
0x3a, 0x77, 0xd0, 0x21, 0xd4, 0xe5, 0xaf, 0xe9, 0x0b, 0xc7, 0x38, 0xf9, 0x09, 0x9a, 0xb9, 0xbd,
0x50, 0x0b, 0x9a, 0xd3, 0x17, 0xd3, 0xe5, 0x74, 0xb8, 0x9c, 0x8c, 0x9d, 0x3b, 0xe8, 0x3e, 0xb4,
0xe7, 0xde, 0x64, 0x7a, 0x31, 0x7c, 0x3e, 0xf1, 0xbd, 0xc9, 0x0f, 0x93, 0xe1, 0x6c, 0x32, 0x76,
0x0c, 0x84, 0xc0, 0x3e, 0x5f, 0xce, 0x46, 0xfe, 0xfc, 0xea, 0xe9, 0x6c, 0xba, 0x38, 0x9f, 0x8c,
0x1d, 0x53, 0x70, 0x2e, 0xae, 0x46, 0xa3, 0xc9, 0x62, 0xe1, 0x54, 0x10, 0x40, 0xed, 0xd9, 0x70,
0x2a, 0xc0, 0xd5, 0xb3, 0xdf, 0xab, 0x2a, 0x2c, 0x23, 0xf9, 0x52, 0x23, 0x0f, 0xea, 0xfa, 0xed,
0x45, 0x0f, 0x72, 0x7f, 0x97, 0x5f, 0xe3, 0xee, 0xfd, 0x92, 0xf1, 0xb7, 0x3a, 0xb9, 0x0f, 0x7e,
0xfd, 0xfb, 0x9f, 0x3f, 0xcc, 0xb6, 0x6b, 0x0d, 0xde, 0x7e, 0x35, 0x10, 0x88, 0x01, 0xcb, 0xf8,
0xb7, 0xc6, 0x09, 0xfa, 0x1a, 0x6a, 0xea, 0x69, 0x42, 0xc7, 0x25, 0xca, 0xfc, 0xad, 0xba, 0x85,
0x11, 0x7d, 0x03, 0x75, 0x1d, 0xed, 0x42, 0x33, 0xe5, 0xb0, 0x77, 0xef, 0xee, 0xa5, 0x30, 0x4b,
0xbf, 0x34, 0xd0, 0x8f, 0x60, 0xe9, 0xae, 0x65, 0x02, 0xd0, 0xfb, 0x13, 0x8a, 0x11, 0xe9, 0x1e,
0xef, 0x96, 0xf5, 0x2c, 0x5d, 0x39, 0xcb, 0x3d, 0x84, 0x8a, 0xb3, 0x0c, 0xb8, 0xa4, 0xf2, 0x73,
0x6a, 0xe9, 0x93, 0x02, 0x75, 0xd1, 0x59, 0x05, 0xea, 0x92, 0x9d, 0xdc, 0x9e, 0xa4, 0xee, 0xa2,
0x4e, 0x89, 0xfa, 0x8d, 0xc0, 0x0c, 0x7e, 0xc6, 0x11, 0xff, 0x05, 0x7d, 0x07, 0xf6, 0x73, 0xc2,
0x95, 0x42, 0x1f, 0xd2, 0x7d, 0x89, 0xe0, 0x43, 0x7a, 0x7c, 0x59, 0x93, 0xff, 0xc4, 0x4f, 0xfe,
0x0b, 0x00, 0x00, 0xff, 0xff, 0xe3, 0xae, 0x80, 0xa3, 0xc0, 0x07, 0x00, 0x00,
func init() { proto.RegisterFile("client.proto", fileDescriptor_client_ba4b73c10b9bbc2a) }
var fileDescriptor_client_ba4b73c10b9bbc2a = []byte{
// 941 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x9c, 0x55, 0xdd, 0x8e, 0xda, 0x46,
0x18, 0x8d, 0x0d, 0xbb, 0xc0, 0xb7, 0xc6, 0xc0, 0x6c, 0xb2, 0xa1, 0xb4, 0x91, 0xa8, 0xdb, 0xa4,
0x68, 0x2f, 0x96, 0x76, 0x73, 0x51, 0xb5, 0x37, 0x15, 0x01, 0x27, 0x6b, 0x89, 0x5d, 0xa8, 0x61,
0x23, 0xf5, 0xca, 0x9a, 0xc0, 0x64, 0x63, 0xc9, 0xf6, 0x38, 0xf6, 0x38, 0x61, 0x55, 0xf5, 0xa6,
0x6f, 0xd0, 0xf6, 0x4d, 0xaa, 0xbe, 0x49, 0x5f, 0xa1, 0xaf, 0xd0, 0xfb, 0x6a, 0x7e, 0x70, 0x6c,
0xe8, 0xde, 0xe4, 0xce, 0x9c, 0x39, 0x73, 0xe6, 0xfb, 0xce, 0x9c, 0x6f, 0x00, 0x63, 0x15, 0xf8,
0x24, 0x62, 0x67, 0x71, 0x42, 0x19, 0x45, 0xb5, 0x80, 0xd2, 0x38, 0x89, 0x57, 0xbd, 0xcf, 0x6e,
0x28, 0xbd, 0x09, 0xc8, 0x10, 0xc7, 0xfe, 0x10, 0x47, 0x11, 0x65, 0x98, 0xf9, 0x34, 0x4a, 0x25,
0xcd, 0xfa, 0x53, 0x07, 0x73, 0x4a, 0x69, 0x3c, 0xcb, 0x98, 0x4b, 0xde, 0x66, 0x24, 0x65, 0xa8,
0x0d, 0x15, 0x1c, 0xb2, 0xae, 0xd6, 0xd7, 0x06, 0x15, 0x97, 0x7f, 0x22, 0x04, 0xd5, 0x35, 0x49,
0x59, 0x57, 0xef, 0x6b, 0x83, 0x86, 0x2b, 0xbe, 0xd1, 0x10, 0xee, 0x87, 0x78, 0xe3, 0xa5, 0xef,
0x71, 0xec, 0x25, 0x34, 0x63, 0x7e, 0x74, 0xe3, 0xbd, 0x26, 0xa4, 0x5b, 0x11, 0xdb, 0x3a, 0x21,
0xde, 0x2c, 0xde, 0xe3, 0xd8, 0x95, 0x2b, 0xcf, 0x09, 0x41, 0x4f, 0xe1, 0x84, 0x6f, 0x88, 0x13,
0x12, 0xe3, 0xdb, 0xd2, 0x96, 0xaa, 0xd8, 0x72, 0x1c, 0xe2, 0xcd, 0x5c, 0x2c, 0x16, 0x36, 0xf5,
0xc1, 0xc8, 0x4f, 0xe1, 0xd4, 0x03, 0x41, 0x05, 0xa5, 0xce, 0x19, 0x5f, 0x82, 0x59, 0x90, 0xe5,
0x85, 0x1f, 0x0a, 0x8e, 0x91, 0xcb, 0x8d, 0x42, 0x86, 0x2c, 0x68, 0x72, 0x56, 0xe8, 0x47, 0x24,
0x11, 0x42, 0x35, 0x41, 0x3a, 0x0a, 0xf1, 0xe6, 0x92, 0x63, 0x5c, 0x69, 0x00, 0x6d, 0xee, 0x99,
0x47, 0x33, 0xe6, 0xad, 0xde, 0xe0, 0x28, 0x22, 0x41, 0xb7, 0xde, 0xd7, 0x06, 0x55, 0xd7, 0x0c,
0xa4, 0x43, 0x63, 0x89, 0x5a, 0x7f, 0x69, 0xd0, 0xe4, 0xa6, 0x39, 0xd1, 0xdd, 0x9e, 0xed, 0x56,
0xae, 0xef, 0x55, 0xbe, 0x57, 0x53, 0x65, 0xbf, 0xa6, 0x27, 0xd0, 0x12, 0x35, 0xf9, 0x51, 0x5e,
0x52, 0x55, 0x94, 0xd4, 0x0c, 0xc4, 0xf9, 0xaa, 0x22, 0xf4, 0x05, 0x34, 0xc9, 0x86, 0x91, 0x24,
0xc2, 0x81, 0xf7, 0x86, 0x05, 0x2b, 0x61, 0x54, 0xdd, 0x35, 0xb6, 0xe0, 0x05, 0x0b, 0x56, 0xd6,
0x08, 0x0c, 0x71, 0x27, 0x24, 0x8d, 0x69, 0x94, 0x12, 0x64, 0x82, 0xee, 0xaf, 0x45, 0xcd, 0x0d,
0x57, 0xf7, 0xd7, 0xe8, 0x73, 0x30, 0xf8, 0x5e, 0x0f, 0xaf, 0xd7, 0x09, 0x49, 0x53, 0x75, 0xdd,
0x47, 0x1c, 0x1b, 0x49, 0xc8, 0x6a, 0x83, 0x79, 0x49, 0x23, 0x9f, 0xd1, 0x44, 0x75, 0x6e, 0xfd,
0xab, 0x01, 0x70, 0xd5, 0x05, 0xc3, 0x2c, 0x4b, 0xff, 0xc7, 0x08, 0x79, 0x8a, 0x9e, 0x9f, 0xf2,
0x18, 0xaa, 0xec, 0x36, 0x96, 0xdd, 0x9a, 0xe7, 0x9d, 0x33, 0x95, 0xd3, 0x33, 0x2e, 0xb2, 0xbc,
0x8d, 0x89, 0x2b, 0x96, 0xd1, 0x00, 0x0e, 0x52, 0x86, 0x99, 0x4c, 0x87, 0x79, 0x8e, 0x4a, 0x3c,
0x7e, 0x18, 0x71, 0x25, 0x01, 0x7d, 0x05, 0x2d, 0x3f, 0xf2, 0x99, 0x2f, 0x72, 0xed, 0x31, 0x3f,
0xdc, 0xc6, 0xc4, 0xfc, 0x00, 0x2f, 0xfd, 0x50, 0x5e, 0x30, 0x4e, 0x99, 0x97, 0xc5, 0x6b, 0xcc,
0x88, 0x64, 0xca, 0xb0, 0x98, 0x1c, 0xbf, 0x16, 0xb0, 0x60, 0xee, 0x3a, 0x51, 0xdb, 0x77, 0xc2,
0x04, 0x63, 0x49, 0x92, 0x30, 0xdd, 0xfa, 0xf0, 0x9b, 0x0e, 0x4d, 0x05, 0x28, 0x7b, 0x4f, 0xa1,
0x23, 0x6e, 0x3f, 0xc6, 0xb7, 0x21, 0x89, 0x98, 0x27, 0x46, 0x48, 0xba, 0xdd, 0xe2, 0x0b, 0x73,
0x89, 0x4f, 0x78, 0x7e, 0x2c, 0x68, 0x6e, 0x93, 0xe2, 0xbd, 0xc2, 0xe9, 0x36, 0x2e, 0x47, 0xa9,
0xcc, 0xca, 0x33, 0x9c, 0x92, 0x12, 0x27, 0xe1, 0xce, 0x54, 0x4a, 0x1c, 0x97, 0x7b, 0xf1, 0x08,
0xa0, 0x30, 0x09, 0x72, 0xb0, 0x1a, 0x71, 0x3e, 0x06, 0x4f, 0xa0, 0x15, 0xfa, 0x91, 0x0c, 0x25,
0x0e, 0x69, 0x16, 0x31, 0x65, 0x55, 0x33, 0xf4, 0x23, 0x6e, 0xec, 0x48, 0x80, 0x82, 0xb7, 0x0d,
0xaf, 0xe2, 0x1d, 0x2a, 0x9e, 0xcc, 0xaf, 0xe2, 0x3d, 0x02, 0x58, 0x05, 0xec, 0x9d, 0xb7, 0x26,
0x01, 0xc3, 0xc2, 0xa5, 0x03, 0xb7, 0xc1, 0x91, 0x09, 0x07, 0xac, 0x3e, 0x18, 0x3f, 0x66, 0x94,
0x91, 0x3b, 0xa7, 0xc4, 0x7a, 0x0d, 0x4d, 0xc5, 0x50, 0xa6, 0x7d, 0x02, 0xf5, 0x7c, 0x64, 0x24,
0xaf, 0xa6, 0xfa, 0xdb, 0xe9, 0x4d, 0xdf, 0xed, 0xed, 0x53, 0x68, 0xec, 0x8e, 0x52, 0x3d, 0x54,
0x73, 0x74, 0xfa, 0x18, 0xea, 0xdb, 0x7c, 0x21, 0x03, 0xea, 0xd3, 0xd9, 0x6c, 0xee, 0xcd, 0xae,
0x97, 0xed, 0x7b, 0xe8, 0x08, 0x6a, 0xe2, 0x97, 0x73, 0xd5, 0xd6, 0x4e, 0x53, 0x68, 0xe4, 0xf1,
0x42, 0x4d, 0x68, 0x38, 0x57, 0xce, 0xd2, 0x19, 0x2d, 0xed, 0x49, 0xfb, 0x1e, 0x7a, 0x00, 0x9d,
0xb9, 0x6b, 0x3b, 0x97, 0xa3, 0x17, 0xb6, 0xe7, 0xda, 0x2f, 0xed, 0xd1, 0xd4, 0x9e, 0xb4, 0x35,
0x84, 0xc0, 0xbc, 0x58, 0x4e, 0xc7, 0xde, 0xfc, 0xfa, 0xd9, 0xd4, 0x59, 0x5c, 0xd8, 0x93, 0xb6,
0xce, 0x35, 0x17, 0xd7, 0xe3, 0xb1, 0xbd, 0x58, 0xb4, 0x2b, 0x08, 0xe0, 0xf0, 0xf9, 0xc8, 0xe1,
0xe4, 0x2a, 0x3a, 0x86, 0x96, 0x73, 0xf5, 0x72, 0xe6, 0x8c, 0x6d, 0x6f, 0x61, 0x2f, 0x97, 0x1c,
0x3c, 0x38, 0xff, 0xbd, 0x2a, 0x27, 0x68, 0x2c, 0x9e, 0x6f, 0xe4, 0x42, 0x4d, 0x3d, 0xc8, 0xe8,
0x61, 0x1e, 0xfa, 0xf2, 0x13, 0xdd, 0x7b, 0x50, 0x9a, 0x86, 0xad, 0x79, 0xd6, 0xc3, 0x5f, 0xff,
0xfe, 0xe7, 0x0f, 0xbd, 0x63, 0x19, 0xc3, 0x77, 0xdf, 0x0c, 0x39, 0x63, 0x48, 0x33, 0xf6, 0xbd,
0x76, 0x8a, 0xbe, 0x85, 0x43, 0xf9, 0x5e, 0xa1, 0x93, 0x92, 0x64, 0xfe, 0x80, 0xdd, 0xa1, 0x88,
0xbe, 0x83, 0x9a, 0x9a, 0xf7, 0x42, 0x31, 0xe5, 0x17, 0xa0, 0x77, 0xbc, 0x37, 0x9a, 0x59, 0xfa,
0xb5, 0x86, 0x7e, 0x02, 0x43, 0x55, 0x2d, 0xc6, 0x02, 0x7d, 0x38, 0xa1, 0x38, 0x37, 0xbd, 0x93,
0x5d, 0x58, 0xf5, 0xd2, 0x13, 0xbd, 0xdc, 0x47, 0xa8, 0xd8, 0xcb, 0x90, 0x09, 0x29, 0x2f, 0x97,
0x16, 0xe1, 0x29, 0x48, 0x17, 0xe3, 0x56, 0x90, 0x2e, 0x65, 0xcc, 0xea, 0x0b, 0xe9, 0x1e, 0xea,
0x96, 0xa4, 0xdf, 0x72, 0xce, 0xf0, 0x67, 0x1c, 0xb2, 0x5f, 0xd0, 0x0f, 0x60, 0xbe, 0x20, 0x4c,
0x3a, 0xf4, 0x31, 0xd5, 0x97, 0x04, 0x3e, 0xa6, 0xc6, 0x57, 0x87, 0xe2, 0xef, 0xf9, 0xe9, 0x7f,
0x01, 0x00, 0x00, 0xff, 0xff, 0xc4, 0x2d, 0xd7, 0x0b, 0xd5, 0x07, 0x00, 0x00,
}

@ -265,6 +265,12 @@ enum SwapState {
the swap amount.
*/
FAILED = 4;
/**
INVOICE_SETTLED is reached when the swap invoice in a loop in swap has been
paid, but we are still waiting for the htlc spend to confirm.
*/
INVOICE_SETTLED = 5;
}
message TermsRequest {

@ -171,10 +171,11 @@
"PREIMAGE_REVEALED",
"HTLC_PUBLISHED",
"SUCCESS",
"FAILED"
"FAILED",
"INVOICE_SETTLED"
],
"default": "INITIATED",
"description": " - INITIATED: *\nINITIATED is the initial state of a swap. At that point, the initiation\ncall to the server has been made and the payment process has been started\nfor the swap and prepayment invoices.\n - PREIMAGE_REVEALED: *\nPREIMAGE_REVEALED is reached when the sweep tx publication is first\nattempted. From that point on, we should consider the preimage to no\nlonger be secret and we need to do all we can to get the sweep confirmed.\nThis state will mostly coalesce with StateHtlcConfirmed, except in the\ncase where we wait for fees to come down before we sweep.\n - HTLC_PUBLISHED: *\nHTLC_PUBLISHED is reached when the htlc tx has been published in a loop in\nswap.\n - SUCCESS: *\nSUCCESS is the final swap state that is reached when the sweep tx has\nthe required confirmation depth.\n - FAILED: *\nFAILED is the final swap state for a failed swap with or without loss of\nthe swap amount."
"description": " - INITIATED: *\nINITIATED is the initial state of a swap. At that point, the initiation\ncall to the server has been made and the payment process has been started\nfor the swap and prepayment invoices.\n - PREIMAGE_REVEALED: *\nPREIMAGE_REVEALED is reached when the sweep tx publication is first\nattempted. From that point on, we should consider the preimage to no\nlonger be secret and we need to do all we can to get the sweep confirmed.\nThis state will mostly coalesce with StateHtlcConfirmed, except in the\ncase where we wait for fees to come down before we sweep.\n - HTLC_PUBLISHED: *\nHTLC_PUBLISHED is reached when the htlc tx has been published in a loop in\nswap.\n - SUCCESS: *\nSUCCESS is the final swap state that is reached when the sweep tx has\nthe required confirmation depth.\n - FAILED: *\nFAILED is the final swap state for a failed swap with or without loss of\nthe swap amount.\n - INVOICE_SETTLED: *\nINVOICE_SETTLED is reached when the swap invoice in a loop in swap has been\npaid, but we are still waiting for the htlc spend to confirm."
},
"looprpcSwapStatus": {
"type": "object",

Loading…
Cancel
Save