{"action":"created","comment":{"url":"https://api.github.com/repos/lightningnetwork/lnd/pulls/comments/241800492","pull_request_review_id":185168767,"id":241800492,"node_id":"MDI0OlB1bGxSZXF1ZXN0UmV2aWV3Q29tbWVudDI0MTgwMDQ5Mg==","diff_hunk":"@@ -1,40 +1,701 @@\n package sweep\n \n import (\n+\t\"errors\"\n+\t\"fmt\"\n+\t\"math/rand\"\n+\t\"sync\"\n+\t\"sync/atomic\"\n+\t\"time\"\n+\n \t\"github.com/btcsuite/btcd/wire\"\n+\t\"github.com/lightningnetwork/lnd/chainntnfs\"\n \t\"github.com/lightningnetwork/lnd/lnwallet\"\n )\n \n-// UtxoSweeper provides the functionality to generate sweep txes. The plan is\n-// to extend UtxoSweeper in the future to also manage the actual sweeping\n-// process by itself.\n+var (\n+\t// ErrRemoteSpend is returned in case an output that we try to sweep is\n+\t// confirmed in a tx of the remote party.\n+\tErrRemoteSpend = errors.New(\"remote party swept utxo\")\n+\n+\t// DefaultMaxSweepAttempts specifies the default maximum number of times\n+\t// an input is included in a publish attempt before giving up and\n+\t// returning an error to the caller.\n+\tDefaultMaxSweepAttempts = 10\n+)\n+\n+// pendingInput is created when an input reaches the main loop for the first\n+// time. It tracks all relevant state that is needed for sweeping.\n+type pendingInput struct {\n+\t// listeners is a list of channels over which the final outcome of the\n+\t// sweep needs to be broadcasted.\n+\tlisteners []chan Result\n+\n+\t// input is the original struct that contains the input and sign\n+\t// descriptor.\n+\tinput Input\n+\n+\t// ntfnRegCancel is populated with a function that cancels the chain\n+\t// notifier spend registration.\n+\tntfnRegCancel func()\n+\n+\t// minPublishHeight indicates the minimum block height at which this\n+\t// input may be (re)published.\n+\tminPublishHeight int32\n+\n+\t// publishAttempts records the number of attempts that have already been\n+\t// made to sweep this tx.\n+\tpublishAttempts int\n+}\n+\n+// UtxoSweeper is responsible for sweeping outputs back into the wallet\n type UtxoSweeper struct {\n+\tstarted uint32 // To be used atomically.\n+\tstopped uint32 // To be used atomically.\n+\n \tcfg *UtxoSweeperConfig\n+\n+\tnewInputs chan *sweepInputMessage\n+\tspendChan chan *chainntnfs.SpendDetail\n+\n+\tpendingInputs map[wire.OutPoint]*pendingInput\n+\n+\t// timer is the channel that signals expiry of the sweep batch timer.\n+\ttimer <-chan time.Time\n+\n+\ttestSpendChan chan wire.OutPoint\n+\n+\tcurrentOutputScript []byte\n+\n+\trelayFeePerKW lnwallet.SatPerKWeight\n+\n+\tquit chan struct{}\n+\twg   sync.WaitGroup\n }\n \n // UtxoSweeperConfig contains dependencies of UtxoSweeper.\n type UtxoSweeperConfig struct {\n-\t// GenSweepScript generates a P2WKH script belonging to the wallet\n-\t// where funds can be swept.\n+\t// GenSweepScript generates a P2WKH script belonging to the wallet where\n+\t// funds can be swept.\n \tGenSweepScript func() ([]byte, error)\n \n \t// Estimator is used when crafting sweep transactions to estimate the\n \t// necessary fee relative to the expected size of the sweep\n \t// transaction.\n \tEstimator lnwallet.FeeEstimator\n \n+\t// PublishTransaction facilitates the process of broadcasting a signed\n+\t// transaction to the appropriate network.\n+\tPublishTransaction func(*wire.MsgTx) error\n+\n+\t// NewBatchTimer creates a channel that will be sent on when a certain\n+\t// time window has passed. During this time window, new inputs can still\n+\t// be added to the sweep tx that is about to be generated.\n+\tNewBatchTimer func() <-chan time.Time\n+\n+\t// Notifier is an instance of a chain notifier we'll use to watch for\n+\t// certain on-chain events.\n+\tNotifier chainntnfs.ChainNotifier\n+\n+\t// ChainIO is used  to determine the current block height.\n+\tChainIO lnwallet.BlockChainIO\n+\n+\t// Store stores the published sweeper txes.\n+\tStore SweeperStore\n+\n \t// Signer is used by the sweeper to generate valid witnesses at the\n \t// time the incubated outputs need to be spent.\n \tSigner lnwallet.Signer\n+\n+\t// SweepTxConfTarget assigns a confirmation target for sweep txes on\n+\t// which the fee calculation will be based.\n+\tSweepTxConfTarget uint32\n+\n+\t// MaxInputsPerTx specifies the default maximum number of inputs allowed\n+\t// in a single sweep tx. If more need to be swept, multiple txes are\n+\t// created and published.\n+\tMaxInputsPerTx int\n+\n+\t// MaxSweepAttempts specifies the maximum number of times an input is\n+\t// included in a publish attempt before giving up and returning an error\n+\t// to the caller.\n+\tMaxSweepAttempts int\n+\n+\t// NextAttemptDeltaFunc returns given the number of already attempted\n+\t// sweeps, how many blocks to wait before retrying to sweep.\n+\tNextAttemptDeltaFunc func(int) int32\n }\n \n-// New returns a new UtxoSweeper instance.\n+// Result is the struct that is pushed through the result channel. Callers\n+// can use this to be informed of the final sweep result. In case of a remote\n+// spend, Err will be ErrRemoteSpend.\n+type Result struct {\n+\t// Err is the final result of the sweep. It is nil when the input is\n+\t// swept successfully by us. ErrRemoteSpend is returned when another\n+\t// party took the input.\n+\tErr error\n+\n+\t// Tx is the transaction that spent the input.\n+\tTx *wire.MsgTx\n+}\n+\n+// sweepInputMessage structs are used in the internal channel between the\n+// SweepInput call and the sweeper main loop.\n+type sweepInputMessage struct {\n+\tinput      Input\n+\tresultChan chan Result\n+}\n+\n+// New returns a new Sweeper instance.\n func New(cfg *UtxoSweeperConfig) *UtxoSweeper {\n+\n \treturn &UtxoSweeper{\n-\t\tcfg: cfg,\n+\t\tcfg:           cfg,\n+\t\tnewInputs:     make(chan *sweepInputMessage),\n+\t\tspendChan:     make(chan *chainntnfs.SpendDetail),\n+\t\tquit:          make(chan struct{}),\n+\t\tpendingInputs: make(map[wire.OutPoint]*pendingInput),\n \t}\n }\n \n+// Start starts the process of constructing and publish sweep txes.\n+func (s *UtxoSweeper) Start() error {\n+\tif !atomic.CompareAndSwapUint32(&s.started, 0, 1) {\n+\t\treturn nil\n+\t}\n+\n+\tlog.Tracef(\"Sweeper starting\")\n+\n+\t// Retrieve last published tx from database.\n+\tlastTx, err := s.cfg.Store.GetLastPublishedTx()\n+\tif err != nil {\n+\t\treturn err\n+\t}\n+\n+\t// Republish in case the previous call crashed lnd. We don't care about\n+\t// the return value, because inputs will be reoffered and retried\n+\t// anyway. The only reason we republish here is to prevent the corner\n+\t// case where lnd goes into a restart loop because of a crashing publish\n+\t// tx where we keep deriving new output script. By publishing and\n+\t// possibly crashing already now, we haven't derived a new output script\n+\t// yet.\n+\tif lastTx != nil {\n+\t\tlog.Debugf(\"Publishing last tx %v\", lastTx.TxHash())\n+\n+\t\t// Error can be ignored. Because we are starting up, there are\n+\t\t// no pending inputs to update based on the publish result.\n+\t\terr := s.cfg.PublishTransaction(lastTx)\n+\t\tif err != nil {\n+\t\t\tlog.Errorf(\"Last tx publish: %v\", err)\n+\t\t}\n+\t}\n+\n+\t// Retrieve relay fee for dust limit calculation. Assume that this will\n+\t// not change from here on.\n+\ts.relayFeePerKW = s.cfg.Estimator.RelayFeePerKW()\n+\n+\t// Register for block epochs to retry sweeping every block.\n+\tbestHash, bestHeight, err := s.cfg.ChainIO.GetBestBlock()\n+\tif err != nil {\n+\t\treturn err\n+\t}\n+\n+\tlog.Debugf(\"Best height: %v\", bestHeight)\n+\n+\tblockEpochs, err := s.cfg.Notifier.RegisterBlockEpochNtfn(\n+\t\t&chainntnfs.BlockEpoch{\n+\t\t\tHeight: bestHeight,\n+\t\t\tHash:   bestHash,\n+\t\t},\n+\t)\n+\tif err != nil {\n+\t\treturn err\n+\t}\n+\n+\t// Start sweeper main loop.\n+\ts.wg.Add(1)\n+\tgo func() {\n+\t\tdefer blockEpochs.Cancel()\n+\t\tdefer s.wg.Done()\n+\n+\t\terr := s.collector(blockEpochs.Epochs, bestHeight)\n+\t\tif err != nil {\n+\t\t\tlog.Errorf(\"Sweeper stopped: %v\", err)\n+\t\t}\n+\t}()\n+\n+\treturn nil\n+}\n+\n+// Stop stops sweeper from listening to block epochs and constructing sweep\n+// txes.\n+func (s *UtxoSweeper) Stop() error {\n+\tif !atomic.CompareAndSwapUint32(&s.stopped, 0, 1) {\n+\t\treturn nil\n+\t}\n+\n+\tlog.Tracef(\"Sweeper shutting down\")\n+\n+\tclose(s.quit)\n+\ts.wg.Wait()\n+\n+\tlog.Tracef(\"Sweeper shut down\")\n+\n+\treturn nil\n+}\n+\n+// SweepInput sweeps inputs back into the wallet. The inputs will be batched and\n+// swept after the batch time window ends.\n+//\n+// NOTE: Extreme care needs to be taken that input isn't changed externally.\n+// Because it is an interface and we don't know what is exactly behind it, we\n+// cannot make a local copy in sweeper.\n+func (s *UtxoSweeper) SweepInput(input Input) (chan Result, error) {\n+\tif input == nil || input.OutPoint() == nil || input.SignDesc() == nil {\n+\t\treturn nil, errors.New(\"nil input received\")\n+\t}\n+\n+\tlog.Debugf(\"Sweep request received: %v\", input.OutPoint())\n+\n+\tsweeperInput := &sweepInputMessage{\n+\t\tinput:      input,\n+\t\tresultChan: make(chan Result, 1),\n+\t}\n+\n+\t// Deliver input to main event loop.\n+\tselect {\n+\tcase s.newInputs <- sweeperInput:\n+\tcase <-s.quit:\n+\t\treturn nil, fmt.Errorf(\"sweeper shutting down\")\n+\t}\n+\n+\treturn sweeperInput.resultChan, nil\n+}\n+\n+// collector is the sweeper main loop. It processes new inputs, spend\n+// notifications and counts down to publication of the sweep tx.\n+func (s *UtxoSweeper) collector(blockEpochs <-chan *chainntnfs.BlockEpoch,\n+\tbestHeight int32) error {\n+\n+\tfor {\n+\t\tselect {\n+\t\t// A new inputs is offered to the sweeper. We check to see if we\n+\t\t// are already trying to sweep this input and if not, set up a\n+\t\t// listener for spend and schedule a sweep.\n+\t\tcase input := <-s.newInputs:\n+\t\t\toutpoint := *input.input.OutPoint()\n+\t\t\tpendInput, pending := s.pendingInputs[outpoint]\n+\t\t\tif pending {\n+\t\t\t\tlog.Debugf(\"Already pending input %v received\",\n+\t\t\t\t\toutpoint)\n+\t\t\t\t// Add additional result channel to signal spend\n+\t\t\t\t// of this input.\n+\t\t\t\tpendInput.listeners = append(\n+\t\t\t\t\tpendInput.listeners, input.resultChan)\n+\t\t\t\tcontinue\n+\t\t\t}\n+\n+\t\t\t// Create a new pendingInput and initialize the\n+\t\t\t// listeners slice with the passed in result channel. If\n+\t\t\t// this input is offered for sweep again, the result\n+\t\t\t// channel will be appended to this slice.\n+\t\t\tpendInput = &pendingInput{\n+\t\t\t\tlisteners:        []chan Result{input.resultChan},\n+\t\t\t\tinput:            input.input,\n+\t\t\t\tminPublishHeight: bestHeight,\n+\t\t\t}\n+\t\t\ts.pendingInputs[outpoint] = pendInput\n+\n+\t\t\t// Start watching for spend of this input, either by us\n+\t\t\t// or the remote party.\n+\t\t\tcancel, err := s.waitForSpend(\n+\t\t\t\toutpoint,\n+\t\t\t\tinput.input.SignDesc().Output.PkScript,\n+\t\t\t\tinput.input.HeightHint(),\n+\t\t\t)\n+\t\t\tif err != nil {\n+\t\t\t\terr = fmt.Errorf(\n+\t\t\t\t\t\"cannot watch for spend: %v\", err,\n+\t\t\t\t)\n+\n+\t\t\t\ts.signalAndRemove(&outpoint, Result{Err: err})\n+\n+\t\t\t\tcontinue\n+\t\t\t}\n+\t\t\tpendInput.ntfnRegCancel = cancel\n+\n+\t\t\t// Check to see if with this new input a sweep tx can be\n+\t\t\t// formed.\n+\t\t\ts.scheduleSweep(bestHeight)","path":"sweep/sweeper.go","position":null,"original_position":336,"commit_id":"d425012d081db543f4e742854a6d4f3e97d915a1","original_commit_id":"4a6ef7881e4436609712e6503f76dda0f41e2b92","user":{"login":"joostjager","id":4638168,"node_id":"MDQ6VXNlcjQ2MzgxNjg=","avatar_url":"https://avatars2.githubusercontent.com/u/4638168?v=4","gravatar_id":"","url":"https://api.github.com/users/joostjager","html_url":"https://github.com/joostjager","followers_url":"https://api.github.com/users/joostjager/followers","following_url":"https://api.github.com/users/joostjager/following{/other_user}","gists_url":"https://api.github.com/users/joostjager/gists{/gist_id}","starred_url":"https://api.github.com/users/joostjager/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/joostjager/subscriptions","organizations_url":"https://api.github.com/users/joostjager/orgs","repos_url":"https://api.github.com/users/joostjager/repos","events_url":"https://api.github.com/users/joostjager/events{/privacy}","received_events_url":"https://api.github.com/users/joostjager/received_events","type":"User","site_admin":false},"body":"Fixed in later commit.","created_at":"2018-12-14T15:53:28Z","updated_at":"2018-12-14T15:53:28Z","html_url":"https://github.com/lightningnetwork/lnd/pull/1960#discussion_r241800492","pull_request_url":"https://api.github.com/repos/lightningnetwork/lnd/pulls/1960","author_association":"COLLABORATOR","_links":{"self":{"href":"https://api.github.com/repos/lightningnetwork/lnd/pulls/comments/241800492"},"html":{"href":"https://github.com/lightningnetwork/lnd/pull/1960#discussion_r241800492"},"pull_request":{"href":"https://api.github.com/repos/lightningnetwork/lnd/pulls/1960"}},"in_reply_to_id":241750163},"pull_request":{"url":"https://api.github.com/repos/lightningnetwork/lnd/pulls/1960","id":217363123,"node_id":"MDExOlB1bGxSZXF1ZXN0MjE3MzYzMTIz","html_url":"https://github.com/lightningnetwork/lnd/pull/1960","diff_url":"https://github.com/lightningnetwork/lnd/pull/1960.diff","patch_url":"https://github.com/lightningnetwork/lnd/pull/1960.patch","issue_url":"https://api.github.com/repos/lightningnetwork/lnd/issues/1960","number":1960,"state":"open","locked":false,"title":"sweep: create sweeper","user":{"login":"joostjager","id":4638168,"node_id":"MDQ6VXNlcjQ2MzgxNjg=","avatar_url":"https://avatars2.githubusercontent.com/u/4638168?v=4","gravatar_id":"","url":"https://api.github.com/users/joostjager","html_url":"https://github.com/joostjager","followers_url":"https://api.github.com/users/joostjager/followers","following_url":"https://api.github.com/users/joostjager/following{/other_user}","gists_url":"https://api.github.com/users/joostjager/gists{/gist_id}","starred_url":"https://api.github.com/users/joostjager/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/joostjager/subscriptions","organizations_url":"https://api.github.com/users/joostjager/orgs","repos_url":"https://api.github.com/users/joostjager/repos","events_url":"https://api.github.com/users/joostjager/events{/privacy}","received_events_url":"https://api.github.com/users/joostjager/received_events","type":"User","site_admin":false},"body":"This PR moves the sweeper logic from `utxonursery` into a separate sweeper struct.\r\n\r\nA follow up PR is #2000, which will let resolvers use the sweeper directly instead of going through nursery. The end goal is to remove nursery completely.\r\n\r\nIn this PR, there is no db migration to clean up now unused nursery store data. This allows us to keep the downgrade path open for a while.","created_at":"2018-09-21T18:19:23Z","updated_at":"2018-12-14T15:53:28Z","closed_at":null,"merged_at":null,"merge_commit_sha":"be46b17068fd9cf40ee4288e95224560a6ee37cc","assignee":null,"assignees":[],"requested_reviewers":[],"requested_teams":[],"labels":[{"id":990013318,"node_id":"MDU6TGFiZWw5OTAwMTMzMTg=","url":"https://api.github.com/repos/lightningnetwork/lnd/labels/P2","name":"P2","color":"fbca04","default":false},{"id":778122277,"node_id":"MDU6TGFiZWw3NzgxMjIyNzc=","url":"https://api.github.com/repos/lightningnetwork/lnd/labels/channel%20management","name":"channel management","color":"5fa7bf","default":false},{"id":923266457,"node_id":"MDU6TGFiZWw5MjMyNjY0NTc=","url":"https://api.github.com/repos/lightningnetwork/lnd/labels/contracts","name":"contracts","color":"e09523","default":false},{"id":312148187,"node_id":"MDU6TGFiZWwzMTIxNDgxODc=","url":"https://api.github.com/repos/lightningnetwork/lnd/labels/enhancement","name":"enhancement","color":"84b6eb","default":true},{"id":990064318,"node_id":"MDU6TGFiZWw5OTAwNjQzMTg=","url":"https://api.github.com/repos/lightningnetwork/lnd/labels/needs%20testing","name":"needs testing","color":"a4f963","default":false},{"id":990053448,"node_id":"MDU6TGFiZWw5OTAwNTM0NDg=","url":"https://api.github.com/repos/lightningnetwork/lnd/labels/utxo%20sweeping","name":"utxo sweeping","color":"5df4e0","default":false}],"milestone":null,"commits_url":"https://api.github.com/repos/lightningnetwork/lnd/pulls/1960/commits","review_comments_url":"https://api.github.com/repos/lightningnetwork/lnd/pulls/1960/comments","review_comment_url":"https://api.github.com/repos/lightningnetwork/lnd/pulls/comments{/number}","comments_url":"https://api.github.com/repos/lightningnetwork/lnd/issues/1960/comments","statuses_url":"https://api.github.com/repos/lightningnetwork/lnd/statuses/d425012d081db543f4e742854a6d4f3e97d915a1","head":{"label":"joostjager:sweeper","ref":"sweeper","sha":"d425012d081db543f4e742854a6d4f3e97d915a1","user":{"login":"joostjager","id":4638168,"node_id":"MDQ6VXNlcjQ2MzgxNjg=","avatar_url":"https://avatars2.githubusercontent.com/u/4638168?v=4","gravatar_id":"","url":"https://api.github.com/users/joostjager","html_url":"https://github.com/joostjager","followers_url":"https://api.github.com/users/joostjager/followers","following_url":"https://api.github.com/users/joostjager/following{/other_user}","gists_url":"https://api.github.com/users/joostjager/gists{/gist_id}","starred_url":"https://api.github.com/users/joostjager/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/joostjager/subscriptions","organizations_url":"https://api.github.com/users/joostjager/orgs","repos_url":"https://api.github.com/users/joostjager/repos","events_url":"https://api.github.com/users/joostjager/events{/privacy}","received_events_url":"https://api.github.com/users/joostjager/received_events","type":"User","site_admin":false},"repo":{"id":133927509,"node_id":"MDEwOlJlcG9zaXRvcnkxMzM5Mjc1MDk=","name":"lnd","full_name":"joostjager/lnd","private":false,"owner":{"login":"joostjager","id":4638168,"node_id":"MDQ6VXNlcjQ2MzgxNjg=","avatar_url":"https://avatars2.githubusercontent.com/u/4638168?v=4","gravatar_id":"","url":"https://api.github.com/users/joostjager","html_url":"https://github.com/joostjager","followers_url":"https://api.github.com/users/joostjager/followers","following_url":"https://api.github.com/users/joostjager/following{/other_user}","gists_url":"https://api.github.com/users/joostjager/gists{/gist_id}","starred_url":"https://api.github.com/users/joostjager/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/joostjager/subscriptions","organizations_url":"https://api.github.com/users/joostjager/orgs","repos_url":"https://api.github.com/users/joostjager/repos","events_url":"https://api.github.com/users/joostjager/events{/privacy}","received_events_url":"https://api.github.com/users/joostjager/received_events","type":"User","site_admin":false},"html_url":"https://github.com/joostjager/lnd","description":"Lightning Network Daemon ⚡️","fork":true,"url":"https://api.github.com/repos/joostjager/lnd","forks_url":"https://api.github.com/repos/joostjager/lnd/forks","keys_url":"https://api.github.com/repos/joostjager/lnd/keys{/key_id}","collaborators_url":"https://api.github.com/repos/joostjager/lnd/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/joostjager/lnd/teams","hooks_url":"https://api.github.com/repos/joostjager/lnd/hooks","issue_events_url":"https://api.github.com/repos/joostjager/lnd/issues/events{/number}","events_url":"https://api.github.com/repos/joostjager/lnd/events","assignees_url":"https://api.github.com/repos/joostjager/lnd/assignees{/user}","branches_url":"https://api.github.com/repos/joostjager/lnd/branches{/branch}","tags_url":"https://api.github.com/repos/joostjager/lnd/tags","blobs_url":"https://api.github.com/repos/joostjager/lnd/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/joostjager/lnd/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/joostjager/lnd/git/refs{/sha}","trees_url":"https://api.github.com/repos/joostjager/lnd/git/trees{/sha}","statuses_url":"https://api.github.com/repos/joostjager/lnd/statuses/{sha}","languages_url":"https://api.github.com/repos/joostjager/lnd/languages","stargazers_url":"https://api.github.com/repos/joostjager/lnd/stargazers","contributors_url":"https://api.github.com/repos/joostjager/lnd/contributors","subscribers_url":"https://api.github.com/repos/joostjager/lnd/subscribers","subscription_url":"https://api.github.com/repos/joostjager/lnd/subscription","commits_url":"https://api.github.com/repos/joostjager/lnd/commits{/sha}","git_commits_url":"https://api.github.com/repos/joostjager/lnd/git/commits{/sha}","comments_url":"https://api.github.com/repos/joostjager/lnd/comments{/number}","issue_comment_url":"https://api.github.com/repos/joostjager/lnd/issues/comments{/number}","contents_url":"https://api.github.com/repos/joostjager/lnd/contents/{+path}","compare_url":"https://api.github.com/repos/joostjager/lnd/compare/{base}...{head}","merges_url":"https://api.github.com/repos/joostjager/lnd/merges","archive_url":"https://api.github.com/repos/joostjager/lnd/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/joostjager/lnd/downloads","issues_url":"https://api.github.com/repos/joostjager/lnd/issues{/number}","pulls_url":"https://api.github.com/repos/joostjager/lnd/pulls{/number}","milestones_url":"https://api.github.com/repos/joostjager/lnd/milestones{/number}","notifications_url":"https://api.github.com/repos/joostjager/lnd/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/joostjager/lnd/labels{/name}","releases_url":"https://api.github.com/repos/joostjager/lnd/releases{/id}","deployments_url":"https://api.github.com/repos/joostjager/lnd/deployments","created_at":"2018-05-18T08:40:36Z","updated_at":"2018-10-08T12:26:51Z","pushed_at":"2018-12-14T08:07:03Z","git_url":"git://github.com/joostjager/lnd.git","ssh_url":"git@github.com:joostjager/lnd.git","clone_url":"https://github.com/joostjager/lnd.git","svn_url":"https://github.com/joostjager/lnd","homepage":"","size":24172,"stargazers_count":0,"watchers_count":0,"language":"Go","has_issues":false,"has_projects":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":0,"mirror_url":null,"archived":false,"open_issues_count":2,"license":{"key":"mit","name":"MIT License","spdx_id":"MIT","url":"https://api.github.com/licenses/mit","node_id":"MDc6TGljZW5zZTEz"},"forks":0,"open_issues":2,"watchers":0,"default_branch":"master"}},"base":{"label":"lightningnetwork:master","ref":"master","sha":"0fafd5e2fd824f38ec6a03a56488de9c0798f34f","user":{"login":"lightningnetwork","id":13424735,"node_id":"MDEyOk9yZ2FuaXphdGlvbjEzNDI0NzM1","avatar_url":"https://avatars0.githubusercontent.com/u/13424735?v=4","gravatar_id":"","url":"https://api.github.com/users/lightningnetwork","html_url":"https://github.com/lightningnetwork","followers_url":"https://api.github.com/users/lightningnetwork/followers","following_url":"https://api.github.com/users/lightningnetwork/following{/other_user}","gists_url":"https://api.github.com/users/lightningnetwork/gists{/gist_id}","starred_url":"https://api.github.com/users/lightningnetwork/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/lightningnetwork/subscriptions","organizations_url":"https://api.github.com/users/lightningnetwork/orgs","repos_url":"https://api.github.com/users/lightningnetwork/repos","events_url":"https://api.github.com/users/lightningnetwork/events{/privacy}","received_events_url":"https://api.github.com/users/lightningnetwork/received_events","type":"Organization","site_admin":false},"repo":{"id":49765480,"node_id":"MDEwOlJlcG9zaXRvcnk0OTc2NTQ4MA==","name":"lnd","full_name":"lightningnetwork/lnd","private":false,"owner":{"login":"lightningnetwork","id":13424735,"node_id":"MDEyOk9yZ2FuaXphdGlvbjEzNDI0NzM1","avatar_url":"https://avatars0.githubusercontent.com/u/13424735?v=4","gravatar_id":"","url":"https://api.github.com/users/lightningnetwork","html_url":"https://github.com/lightningnetwork","followers_url":"https://api.github.com/users/lightningnetwork/followers","following_url":"https://api.github.com/users/lightningnetwork/following{/other_user}","gists_url":"https://api.github.com/users/lightningnetwork/gists{/gist_id}","starred_url":"https://api.github.com/users/lightningnetwork/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/lightningnetwork/subscriptions","organizations_url":"https://api.github.com/users/lightningnetwork/orgs","repos_url":"https://api.github.com/users/lightningnetwork/repos","events_url":"https://api.github.com/users/lightningnetwork/events{/privacy}","received_events_url":"https://api.github.com/users/lightningnetwork/received_events","type":"Organization","site_admin":false},"html_url":"https://github.com/lightningnetwork/lnd","description":"Lightning Network Daemon ⚡️","fork":false,"url":"https://api.github.com/repos/lightningnetwork/lnd","forks_url":"https://api.github.com/repos/lightningnetwork/lnd/forks","keys_url":"https://api.github.com/repos/lightningnetwork/lnd/keys{/key_id}","collaborators_url":"https://api.github.com/repos/lightningnetwork/lnd/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/lightningnetwork/lnd/teams","hooks_url":"https://api.github.com/repos/lightningnetwork/lnd/hooks","issue_events_url":"https://api.github.com/repos/lightningnetwork/lnd/issues/events{/number}","events_url":"https://api.github.com/repos/lightningnetwork/lnd/events","assignees_url":"https://api.github.com/repos/lightningnetwork/lnd/assignees{/user}","branches_url":"https://api.github.com/repos/lightningnetwork/lnd/branches{/branch}","tags_url":"https://api.github.com/repos/lightningnetwork/lnd/tags","blobs_url":"https://api.github.com/repos/lightningnetwork/lnd/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/lightningnetwork/lnd/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/lightningnetwork/lnd/git/refs{/sha}","trees_url":"https://api.github.com/repos/lightningnetwork/lnd/git/trees{/sha}","statuses_url":"https://api.github.com/repos/lightningnetwork/lnd/statuses/{sha}","languages_url":"https://api.github.com/repos/lightningnetwork/lnd/languages","stargazers_url":"https://api.github.com/repos/lightningnetwork/lnd/stargazers","contributors_url":"https://api.github.com/repos/lightningnetwork/lnd/contributors","subscribers_url":"https://api.github.com/repos/lightningnetwork/lnd/subscribers","subscription_url":"https://api.github.com/repos/lightningnetwork/lnd/subscription","commits_url":"https://api.github.com/repos/lightningnetwork/lnd/commits{/sha}","git_commits_url":"https://api.github.com/repos/lightningnetwork/lnd/git/commits{/sha}","comments_url":"https://api.github.com/repos/lightningnetwork/lnd/comments{/number}","issue_comment_url":"https://api.github.com/repos/lightningnetwork/lnd/issues/comments{/number}","contents_url":"https://api.github.com/repos/lightningnetwork/lnd/contents/{+path}","compare_url":"https://api.github.com/repos/lightningnetwork/lnd/compare/{base}...{head}","merges_url":"https://api.github.com/repos/lightningnetwork/lnd/merges","archive_url":"https://api.github.com/repos/lightningnetwork/lnd/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/lightningnetwork/lnd/downloads","issues_url":"https://api.github.com/repos/lightningnetwork/lnd/issues{/number}","pulls_url":"https://api.github.com/repos/lightningnetwork/lnd/pulls{/number}","milestones_url":"https://api.github.com/repos/lightningnetwork/lnd/milestones{/number}","notifications_url":"https://api.github.com/repos/lightningnetwork/lnd/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/lightningnetwork/lnd/labels{/name}","releases_url":"https://api.github.com/repos/lightningnetwork/lnd/releases{/id}","deployments_url":"https://api.github.com/repos/lightningnetwork/lnd/deployments","created_at":"2016-01-16T08:19:33Z","updated_at":"2018-12-14T13:22:12Z","pushed_at":"2018-12-14T13:44:30Z","git_url":"git://github.com/lightningnetwork/lnd.git","ssh_url":"git@github.com:lightningnetwork/lnd.git","clone_url":"https://github.com/lightningnetwork/lnd.git","svn_url":"https://github.com/lightningnetwork/lnd","homepage":"","size":24074,"stargazers_count":3288,"watchers_count":3288,"language":"Go","has_issues":true,"has_projects":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":838,"mirror_url":null,"archived":false,"open_issues_count":334,"license":{"key":"mit","name":"MIT License","spdx_id":"MIT","url":"https://api.github.com/licenses/mit","node_id":"MDc6TGljZW5zZTEz"},"forks":838,"open_issues":334,"watchers":3288,"default_branch":"master"}},"_links":{"self":{"href":"https://api.github.com/repos/lightningnetwork/lnd/pulls/1960"},"html":{"href":"https://github.com/lightningnetwork/lnd/pull/1960"},"issue":{"href":"https://api.github.com/repos/lightningnetwork/lnd/issues/1960"},"comments":{"href":"https://api.github.com/repos/lightningnetwork/lnd/issues/1960/comments"},"review_comments":{"href":"https://api.github.com/repos/lightningnetwork/lnd/pulls/1960/comments"},"review_comment":{"href":"https://api.github.com/repos/lightningnetwork/lnd/pulls/comments{/number}"},"commits":{"href":"https://api.github.com/repos/lightningnetwork/lnd/pulls/1960/commits"},"statuses":{"href":"https://api.github.com/repos/lightningnetwork/lnd/statuses/d425012d081db543f4e742854a6d4f3e97d915a1"}},"author_association":"COLLABORATOR"}}
{"actor":{"display_login":"joostjager"}}