Skip to content

Commit

Permalink
Move Peerswap functionality into a plugin
Browse files Browse the repository at this point in the history
  • Loading branch information
remyers committed Oct 21, 2022
1 parent 32c2a96 commit fb4ea5f
Show file tree
Hide file tree
Showing 36 changed files with 894 additions and 331 deletions.
19 changes: 19 additions & 0 deletions plugins/peerswap/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Peerswap plugin

This plugin allows implements the PeerSwap protocol: https://github.com/ElementsProject/peerswap-spec/blob/main/peer-protocol.md

## Build

To build this plugin, run the following command in this directory:

```sh
mvn package
```

## Run

To run eclair with this plugin, start eclair with the following command:

```sh
eclair-node-<version>/bin/eclair-node.sh <path-to-plugin-jar>/peerswap-plugin-<version>.jar
```
163 changes: 163 additions & 0 deletions plugins/peerswap/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
~ Copyright 2022 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.
-->

<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_2.13</artifactId>
<version>0.7.1-SNAPSHOT</version>
</parent>

<artifactId>peerswap-plugin_2.13</artifactId>
<packaging>jar</packaging>
<name>peerswap-plugin</name>

<build>
<plugins>
<plugin>
<groupId>com.googlecode.maven-download-plugin</groupId>
<artifactId>download-maven-plugin</artifactId>
<version>1.3.0</version>
<executions>
<execution>
<id>download-bitcoind</id>
<phase>generate-test-resources</phase>
<goals>
<goal>wget</goal>
</goals>
<configuration>
<skip>${maven.test.skip}</skip>
<url>${bitcoind.url}</url>
<unpack>true</unpack>
<outputDirectory>${project.build.directory}</outputDirectory>
<md5>${bitcoind.md5}</md5>
<sha1>${bitcoind.sha1}</sha1>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.2.1</version>
<configuration>
<transformers>
<!-- Add a manifest entry for Main-Class with the FQDN of the implementation. -->
<transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
<manifestEntries>
<Main-Class>fr.acinq.eclair.plugins.peerswap.PeerSwapPlugin</Main-Class>
</manifestEntries>
</transformer>
</transformers>
</configuration>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>

<profiles>
<profile>
<id>default</id>
<activation>
<activeByDefault>true</activeByDefault>
</activation>
<properties>
<bitcoind.url>https://bitcoincore.org/bin/bitcoin-core-0.21.1/bitcoin-0.21.1-x86_64-linux-gnu.tar.gz</bitcoind.url>
<bitcoind.md5>e283a98b5e9f0b58e625e1dde661201d</bitcoind.md5>
<bitcoind.sha1>5101e29b39c33cc8e40d5f3b46dda37991b037a0</bitcoind.sha1>
</properties>
</profile>
<profile>
<id>Mac</id>
<activation>
<os>
<family>mac</family>
</os>
</activation>
<properties>
<bitcoind.url>https://bitcoincore.org/bin/bitcoin-core-0.21.1/bitcoin-0.21.1-osx64.tar.gz</bitcoind.url>
<bitcoind.md5>dfd1f323678eede14ae2cf6afb26ff6a</bitcoind.md5>
<bitcoind.sha1>4273696f90a2648f90142438221f5d1ade16afa2</bitcoind.sha1>
</properties>
</profile>
<profile>
<id>Windows</id>
<activation>
<os>
<family>Windows</family>
</os>
</activation>
<properties>
<bitcoind.url>https://bitcoincore.org/bin/bitcoin-core-0.21.1/bitcoin-0.21.1-win64.zip</bitcoind.url>
<bitcoind.md5>1c6f5081ea68dcec7eddb9e6cdfc508d</bitcoind.md5>
<bitcoind.sha1>a782cd413fc736f05fad3831d6a9f59dde779520</bitcoind.sha1>
</properties>
</profile>
</profiles>

<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>${project.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>fr.acinq.eclair</groupId>
<artifactId>eclair-node_${scala.version.short}</artifactId>
<version>${project.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,66 @@
/*
* Copyright 2022 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.peerswap

import akka.http.scaladsl.common.{NameReceptacle, NameUnmarshallerReceptacle}
import akka.http.scaladsl.server.Route
import fr.acinq.bitcoin.scalacompat.{ByteVector32, Satoshi}
import fr.acinq.eclair.api.directives.EclairDirectives
import fr.acinq.eclair.api.serde.FormParamExtractors._

object ApiHandlers {

import fr.acinq.eclair.api.serde.JsonSupport.{marshaller, serialization}
import fr.acinq.eclair.plugins.peerswap.json.PeerSwapJsonSerializers.formats

def registerRoutes(kit: PeerSwapKit, eclairDirectives: EclairDirectives): Route = {
import eclairDirectives._

val swapIdFormParam: NameUnmarshallerReceptacle[ByteVector32] = "swapId".as[ByteVector32](sha256HashUnmarshaller)

val amountSatFormParam: NameReceptacle[Satoshi] = "amountSat".as[Satoshi]

val swapIn: Route = postRequest("swapin") { implicit t =>
formFields(shortChannelIdFormParam, amountSatFormParam) { (channelId, amount) =>
complete(kit.swapIn(channelId, amount))
}
}

val swapOut: Route = postRequest("swapout") { implicit t =>
formFields(shortChannelIdFormParam, amountSatFormParam) { (channelId, amount) =>
complete(kit.swapOut(channelId, amount))
}
}

val listSwaps: Route = postRequest("listswaps") { implicit t =>
complete(kit.listSwaps())
}

val cancelSwap: Route = postRequest("cancelswap") { implicit t =>
formFields(swapIdFormParam) { swapId =>
complete(kit.cancelSwap(swapId.toString()))
}
}

val peerSwapRoutes: Route = swapIn ~ swapOut ~ listSwaps ~ cancelSwap

peerSwapRoutes
}

}


Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
* limitations under the License.
*/

package fr.acinq.eclair.swap
package fr.acinq.eclair.plugins.peerswap

import com.google.common.cache.{CacheBuilder, CacheLoader, LoadingCache}
import fr.acinq.bitcoin.scalacompat.DeterministicWallet._
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
/*
* Copyright 2022 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.peerswap

import akka.actor.ActorSystem
import akka.actor.typed.scaladsl.AskPattern.Askable
import akka.actor.typed.scaladsl.Behaviors
import akka.actor.typed.scaladsl.adapter.{ClassicActorSystemOps, ClassicSchedulerOps}
import akka.actor.typed.{ActorRef, SupervisorStrategy}
import akka.http.scaladsl.server.Route
import akka.util.Timeout
import fr.acinq.bitcoin.scalacompat.Satoshi
import fr.acinq.eclair.api.directives.EclairDirectives
import fr.acinq.eclair.db.sqlite.SqliteUtils
import fr.acinq.eclair.plugins.peerswap.SwapResponses.{Response, Status}
import fr.acinq.eclair.plugins.peerswap.db.SwapsDb
import fr.acinq.eclair.plugins.peerswap.db.sqlite.SqliteSwapsDb
import fr.acinq.eclair.{CustomFeaturePlugin, Feature, InitFeature, Kit, NodeFeature, NodeParams, Plugin, PluginParams, RouteProvider, Setup, ShortChannelId}
import grizzled.slf4j.Logging
import scodec.bits.ByteVector

import java.io.File
import java.nio.file.Files
import scala.concurrent.Future

/**
* This plugin implements the PeerSwap protocol: https://github.com/ElementsProject/peerswap-spec/blob/main/peer-protocol.md
*/
object PeerSwapPlugin {
// TODO: derive this set from peerSwapMessageCodec tags
val peerSwapTags: Set[Int] = Set(42069, 42071, 42073, 42075, 42077, 42079, 42081)
}

class PeerSwapPlugin extends Plugin with RouteProvider with Logging {

var db: SwapsDb = _
var swapKeyManager: LocalSwapKeyManager = _
var pluginKit: PeerSwapKit = _

case object PeerSwapFeature extends Feature with InitFeature with NodeFeature {
val rfcName = "peer_swap_plugin_prototype"
val mandatory = 158
}

override def params: PluginParams = new CustomFeaturePlugin {
// @formatter:off
override def messageTags: Set[Int] = PeerSwapPlugin.peerSwapTags
override def feature: Feature = PeerSwapFeature
override def name: String = "PeerSwap"
// @formatter:on
}

override def onSetup(setup: Setup): Unit = {
val chain = setup.config.getString("chain")
val chainDir = new File(setup.datadir, chain)
db = new SqliteSwapsDb(SqliteUtils.openSqliteFile(chainDir, "peer-swap.sqlite", exclusiveLock = false, journalMode = "wal", syncFlag = "normal"))

// load seed
val seedFilename: String = "swap_seed.dat"
val seedPath: File = new File(setup.datadir, seedFilename)
val swapSeed: ByteVector = ByteVector(Files.readAllBytes(seedPath.toPath))
swapKeyManager = new LocalSwapKeyManager(swapSeed, NodeParams.hashFromChain(chain))
}

override def onKit(kit: Kit): Unit = {
val data = db.restore().toSet
val swapRegister = kit.system.spawn(Behaviors.supervise(SwapRegister(kit.nodeParams, kit.paymentInitiator, kit.watcher, kit.register, kit.wallet, swapKeyManager, db, data)).onFailure(SupervisorStrategy.restart), "peerswap-plugin-swap-register")
pluginKit = PeerSwapKit(kit.nodeParams, kit.system, swapRegister)
}

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

}

case class PeerSwapKit(nodeParams: NodeParams, system: ActorSystem, swapRegister: ActorRef[SwapRegister.Command]) {
def swapIn(shortChannelId: ShortChannelId, amount: Satoshi)(implicit timeout: Timeout): Future[Response] =
swapRegister.ask(ref => SwapRegister.SwapInRequested(ref, amount, shortChannelId))(timeout, system.scheduler.toTyped)

def swapOut(shortChannelId: ShortChannelId, amount: Satoshi)(implicit timeout: Timeout): Future[Response] =
swapRegister.ask(ref => SwapRegister.SwapOutRequested(ref, amount, shortChannelId))(timeout, system.scheduler.toTyped)

def listSwaps()(implicit timeout: Timeout): Future[Iterable[Status]] =
swapRegister.ask(ref => SwapRegister.ListPendingSwaps(ref))(timeout, system.scheduler.toTyped)

def cancelSwap(swapId: String)(implicit timeout: Timeout): Future[Response] =
swapRegister.ask(ref => SwapRegister.CancelSwapRequested(ref, swapId))(timeout, system.scheduler.toTyped)
}
Loading

0 comments on commit fb4ea5f

Please sign in to comment.