Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add tip jar plugin #6

Merged
merged 4 commits into from
Feb 28, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
17 changes: 17 additions & 0 deletions bolt12-tip-jar/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Bolt12 Tip Jar

Plugin that creates a basic offer to receive tips.

## Config

```
tip-jar {
description = "donation to eclair"
default-amount-msat = 100000000 // Amount to use if the invoice request does not specify an amount
max-final-expiry-delta = 1000 // How long (in blocks) the route to pay the invoice will be valid
}
```

## API

`tipjarshowoffer` will print the tip jar offer.
84 changes: 84 additions & 0 deletions bolt12-tip-jar/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>

<parent>
<groupId>fr.acinq.eclair</groupId>
<artifactId>eclair-plugins_2.13</artifactId>
<version>0.9.1-SNAPSHOT</version>
</parent>

<artifactId>bolt12-tip-jar</artifactId>
<packaging>jar</packaging>
<name>bolt12-tip-jar</name>

<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.2.1</version>
<configuration>
<transformers>
<transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
<manifestEntries>
<Main-Class>fr.acinq.eclair.plugins.tipjar.TipJarPlugin</Main-Class>
</manifestEntries>
</transformer>
</transformers>
</configuration>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>

<dependencies>
<dependency>
<groupId>org.scala-lang</groupId>
<artifactId>scala-library</artifactId>
<version>${scala.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>fr.acinq.eclair</groupId>
<artifactId>eclair-core_${scala.version.short}</artifactId>
<version>${eclair.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>fr.acinq.eclair</groupId>
<artifactId>eclair-node_${scala.version.short}</artifactId>
<version>${eclair.version}</version>
<scope>provided</scope>
</dependency>
<!-- TESTS -->
<dependency>
<groupId>com.typesafe.akka</groupId>
<artifactId>akka-testkit_${scala.version.short}</artifactId>
<version>${akka.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.typesafe.akka</groupId>
<artifactId>akka-actor-testkit-typed_${scala.version.short}</artifactId>
<version>${akka.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>fr.acinq.eclair</groupId>
<artifactId>eclair-core_${scala.version.short}</artifactId>
<version>${project.version}</version>
<classifier>tests</classifier>
<type>test-jar</type>
<scope>test</scope>
</dependency>
</dependencies>

</project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/*
* Copyright 2023 ACINQ SAS
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package fr.acinq.eclair.plugins.tipjar

import akka.http.scaladsl.server.Route
import fr.acinq.eclair.api.directives.EclairDirectives

object ApiHandlers {

def registerRoutes(kit: TipJarKit, eclairDirectives: EclairDirectives): Route = {
import eclairDirectives._
import fr.acinq.eclair.api.serde.JsonSupport.{formats, marshaller, serialization}

val showOffer: Route = postRequest("tipjarshowoffer") { implicit t =>
complete(kit.offer.toString)
}

showOffer
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/*
* Copyright 2023 ACINQ SAS
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package fr.acinq.eclair.plugins.tipjar

import akka.actor.typed.Behavior
import akka.actor.typed.scaladsl.Behaviors
import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey
import fr.acinq.eclair.payment.offer.OfferManager
import fr.acinq.eclair.payment.offer.OfferManager.HandlerCommand
import fr.acinq.eclair.payment.offer.OfferManager.InvoiceRequestActor.ApproveRequest
import fr.acinq.eclair.payment.offer.OfferManager.PaymentActor.AcceptPayment
import fr.acinq.eclair.payment.receive.MultiPartHandler.ReceivingRoute
import fr.acinq.eclair.{CltvExpiryDelta, MilliSatoshi}

object TipJarHandler {

def apply(nodeId: PublicKey, defaultAmount: MilliSatoshi, maxFinalExpiryDelta: CltvExpiryDelta): Behavior[HandlerCommand] = {
Behaviors.receiveMessage {
case OfferManager.HandleInvoiceRequest(replyTo, invoiceRequest) =>
replyTo ! ApproveRequest(invoiceRequest.amount.getOrElse(defaultAmount), Seq(ReceivingRoute(Seq(nodeId), maxFinalExpiryDelta)), None)
Behaviors.same
case OfferManager.HandlePayment(replyTo, _, _) =>
replyTo ! AcceptPayment()
Behaviors.same
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
/*
* Copyright 2023 ACINQ SAS
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package fr.acinq.eclair.plugins.tipjar

import akka.actor.ActorSystem
import akka.actor.typed.scaladsl.Behaviors
import akka.actor.typed.scaladsl.adapter.ClassicActorSystemOps
import akka.actor.typed.{ActorRef, SupervisorStrategy}
import akka.http.scaladsl.server.Route
import fr.acinq.eclair.api.directives.EclairDirectives
import fr.acinq.eclair.payment.offer.OfferManager
import fr.acinq.eclair.payment.offer.OfferManager.RegisterOffer
import fr.acinq.eclair.wire.protocol.OfferTypes.Offer
import fr.acinq.eclair.{CltvExpiryDelta, Features, Kit, MilliSatoshi, NodeParams, Plugin, PluginParams, RouteProvider, Setup}
import grizzled.slf4j.Logging

class TipJarPlugin extends Plugin with RouteProvider with Logging {

private var pluginKit: TipJarKit = _
private var config: TipJarConfig = _

override def params: PluginParams = new PluginParams {
override def name: String = "TipJarPlugin"
}

override def onSetup(setup: Setup): Unit = {
config = TipJarConfig(
setup.config.getString("tip-jar.description"),
MilliSatoshi(setup.config.getLong("tip-jar.default-amount-msat")),
CltvExpiryDelta(setup.config.getInt("tip-jar.max-final-expiry-delta")))
}

override def onKit(kit: Kit): Unit = {
val tipJarHandler = kit.system.spawn(Behaviors.supervise(TipJarHandler(kit.nodeParams.nodeId, config.defaultAmount, config.maxFinalExpiryDelta)).onFailure(SupervisorStrategy.restart), "tip-jar-handler")
val offer = Offer(None, config.offerDescription, kit.nodeParams.nodeId, Features.empty, kit.nodeParams.chainHash)
kit.offerManager ! RegisterOffer(offer, kit.nodeParams.privateKey, None, tipJarHandler)
pluginKit = TipJarKit(kit.nodeParams, offer, kit.system, tipJarHandler)
}

override def route(eclairDirectives: EclairDirectives): Route = ApiHandlers.registerRoutes(pluginKit, eclairDirectives)

}

case class TipJarConfig(offerDescription: String, defaultAmount: MilliSatoshi, maxFinalExpiryDelta: CltvExpiryDelta)

case class TipJarKit(nodeParams: NodeParams, offer: Offer, system: ActorSystem, tipJarHandler: ActorRef[OfferManager.HandlerCommand])
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
/*
* Copyright 2023 ACINQ SAS
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package fr.acinq.eclair.plugins.tipjar

import akka.actor.testkit.typed.scaladsl.{ScalaTestWithActorTestKit, TestProbe}
import fr.acinq.bitcoin.scalacompat.ByteVector64
import fr.acinq.eclair.payment.offer.OfferManager.InvoiceRequestActor.ApproveRequest
import fr.acinq.eclair.payment.offer.OfferManager.PaymentActor.AcceptPayment
import fr.acinq.eclair.payment.offer.OfferManager.{HandleInvoiceRequest, HandlePayment, InvoiceRequestActor, PaymentActor}
import fr.acinq.eclair.wire.protocol.OfferTypes.{InvoiceRequest, InvoiceRequestChain, InvoiceRequestMetadata, InvoiceRequestPayerId, Offer, Signature}
import fr.acinq.eclair.wire.protocol.TlvStream
import fr.acinq.eclair.{CltvExpiryDelta, Features, MilliSatoshiLong, TestConstants, randomBytes32, randomKey}
import org.scalatest.funsuite.AnyFunSuiteLike

class TipJarHandlerSpec extends ScalaTestWithActorTestKit with AnyFunSuiteLike {
test("handle invoice request") {
val nodeParams = TestConstants.Alice.nodeParams
val handler = testKit.spawn(TipJarHandler(nodeParams.nodeId, 100_000_000 msat, CltvExpiryDelta(1000)))

val probe = TestProbe[InvoiceRequestActor.Command]()

val offer = Offer(None, "test tip jar", nodeParams.nodeId, Features.empty, nodeParams.chainHash)
val invoiceRequest = InvoiceRequest(TlvStream(offer.records.records ++ Set(InvoiceRequestMetadata(randomBytes32()), InvoiceRequestChain(nodeParams.chainHash), InvoiceRequestPayerId(randomKey().publicKey), Signature(ByteVector64.Zeroes))))
handler ! HandleInvoiceRequest(probe.ref, invoiceRequest)

val approve = probe.expectMessageType[ApproveRequest]
assert(approve.amount == 100_000_000.msat)
}

test("handle payment"){
val nodeParams = TestConstants.Alice.nodeParams
val handler = testKit.spawn(TipJarHandler(nodeParams.nodeId, 100_000_000 msat, CltvExpiryDelta(1000)))

val probe = TestProbe[PaymentActor.Command]()

handler ! HandlePayment(probe.ref, randomBytes32(), None)

probe.expectMessage(AcceptPayment())
}
}
1 change: 1 addition & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
<modules>
<module>historical-gossip</module>
<module>offline-commands</module>
<module>bolt12-tip-jar</module>
<module>channel-funding</module>
</modules>

Expand Down