diff --git a/cln_plugin/channel_acceptor.go b/cln_plugin/channel_acceptor.go new file mode 100644 index 00000000..848dc600 --- /dev/null +++ b/cln_plugin/channel_acceptor.go @@ -0,0 +1,37 @@ +package cln_plugin + +import ( + "encoding/json" + "fmt" + + sj "go.starlark.net/lib/json" + "go.starlark.net/starlark" +) + +func channelAcceptor(acceptScript string, method string, openChannel json.RawMessage) (json.RawMessage, error) { + reject, _ := json.Marshal(struct { + Result string `json:"result"` + }{Result: "reject"}) + + sd := starlark.StringDict{ + "method": starlark.String(method), + "openchannel": starlark.String(openChannel), + } + for _, k := range sj.Module.Members.Keys() { + sd[k] = sj.Module.Members[k] + } + value, err := starlark.Eval( + &starlark.Thread{}, + "", + acceptScript, + sd, + ) + if err != nil { + return reject, err + } + s, ok := value.(starlark.String) + if !ok { + return reject, fmt.Errorf("not a string") + } + return json.RawMessage(s.GoString()), nil +} diff --git a/cln_plugin/cln_plugin.go b/cln_plugin/cln_plugin.go index 7bbfe86f..36a6c212 100644 --- a/cln_plugin/cln_plugin.go +++ b/cln_plugin/cln_plugin.go @@ -19,6 +19,7 @@ import ( const ( SubscriberTimeoutOption = "lsp-subscribertimeout" ListenAddressOption = "lsp-listen" + channelAcceptScript = "lsp-channel-accept-script" ) var ( @@ -43,11 +44,12 @@ var ( ) type ClnPlugin struct { - done chan struct{} - server *server - in *os.File - out *bufio.Writer - writeMtx sync.Mutex + done chan struct{} + server *server + in *os.File + out *bufio.Writer + writeMtx sync.Mutex + channelAcceptScript string } func NewClnPlugin(in, out *os.File) *ClnPlugin { @@ -210,6 +212,20 @@ func (c *ClnPlugin) processRequest(request *Request) { c.handleShutdown(request) case "htlc_accepted": c.handleHtlcAccepted(request) + case "openchannel": + // handle open channel in a goroutine, because order doesn't matter. + go c.handleOpenChannel(request) + case "openchannel2": + // handle open channel in a goroutine, because order doesn't matter. + go c.handleOpenChannel(request) + case "getchannelacceptscript": + c.sendToCln(&Response{ + JsonRpc: SpecVersion, + Id: request.Id, + Result: c.channelAcceptScript, + }) + case "setchannelacceptscript": + c.handleSetChannelAcceptScript(request) default: c.sendError( request.Id, @@ -239,14 +255,28 @@ func (c *ClnPlugin) handleGetManifest(request *Request) { "if no subscriber is active. golang duration string.", Default: &DefaultSubscriberTimeout, }, + { + Name: channelAcceptScript, + Type: "string", + Description: "starlark script for channel acceptor.", + }, }, - RpcMethods: []*RpcMethod{}, - Dynamic: true, - Hooks: []Hook{ + RpcMethods: []*RpcMethod{ + { + Name: "getchannelacceptscript", + Description: "Get the startlark channel acceptor script", + }, { - Name: "htlc_accepted", + Name: "setchannelacceptscript", + Description: "Set the startlark channel acceptor script", }, }, + Dynamic: true, + Hooks: []Hook{ + {Name: "htlc_accepted"}, + {Name: "openchannel"}, + {Name: "openchannel2"}, + }, NonNumericIds: true, Subscriptions: []string{ "shutdown", @@ -270,6 +300,31 @@ func (c *ClnPlugin) handleInit(request *Request) { return } + // Get the channel acceptor script option. + sc, ok := initMsg.Options[channelAcceptScript] + if !ok { + c.sendError( + request.Id, + InvalidParams, + fmt.Sprintf("Missing option '%s'", channelAcceptScript), + ) + return + } + + c.channelAcceptScript, ok = sc.(string) + if !ok { + c.sendError( + request.Id, + InvalidParams, + fmt.Sprintf( + "Invalid value '%v' for option '%s'", + sc, + channelAcceptScript, + ), + ) + return + } + // Get the listen address option. l, ok := initMsg.Options[ListenAddressOption] if !ok { @@ -382,6 +437,79 @@ func (c *ClnPlugin) handleHtlcAccepted(request *Request) { c.server.Send(idToString(request.Id), &htlc) } +func (c *ClnPlugin) handleSetChannelAcceptScript(request *Request) { + var params []string + err := json.Unmarshal(request.Params, ¶ms) + if err != nil { + c.sendError( + request.Id, + ParseError, + fmt.Sprintf( + "Failed to unmarshal setchannelacceptscript params:%s [%s]", + err.Error(), + request.Params, + ), + ) + return + } + if len(params) >= 1 { + c.channelAcceptScript = params[0] + } + c.sendToCln(&Response{ + JsonRpc: SpecVersion, + Id: request.Id, + Result: c.channelAcceptScript, + }) +} + +func unmarshalOpenChannel(request *Request) (r json.RawMessage, err error) { + switch request.Method { + case "openchannel": + var openChannel struct { + OpenChannel json.RawMessage `json:"openchannel"` + } + err = json.Unmarshal(request.Params, &openChannel) + if err != nil { + return + } + r = openChannel.OpenChannel + case "openchannel2": + var openChannel struct { + OpenChannel json.RawMessage `json:"openchannel2"` + } + err = json.Unmarshal(request.Params, &openChannel) + if err != nil { + return + } + r = openChannel.OpenChannel + } + return r, nil +} +func (c *ClnPlugin) handleOpenChannel(request *Request) { + p, err := unmarshalOpenChannel(request) + if err != nil { + c.sendError( + request.Id, + ParseError, + fmt.Sprintf( + "Failed to unmarshal openchannel params:%s [%s]", + err.Error(), + request.Params, + ), + ) + return + } + result, err := channelAcceptor(c.channelAcceptScript, request.Method, p) + if err != nil { + log.Printf("channelAcceptor error - request: %s error: %v", request, err) + } + c.sendToCln(&Response{ + JsonRpc: SpecVersion, + Id: request.Id, + Result: result, + }) +} + // Sends an error to cln. func (c *ClnPlugin) sendError(id json.RawMessage, code int, message string) { // Log the error to cln first. diff --git a/go.mod b/go.mod index d46b2718..14a06e6d 100644 --- a/go.mod +++ b/go.mod @@ -21,6 +21,7 @@ require ( github.com/lightningnetwork/lnd/tlv v1.1.0 github.com/niftynei/glightning v0.8.2 github.com/stretchr/testify v1.8.1 + go.starlark.net v0.0.0-20230612165344-9532f5667272 golang.org/x/exp v0.0.0-20230321023759-10a507213a29 golang.org/x/sync v0.1.0 google.golang.org/grpc v1.50.1