diff --git a/Builds/VisualStudio2015/RippleD.vcxproj b/Builds/VisualStudio2015/RippleD.vcxproj
index 04e8a4d23..c2acc96da 100644
--- a/Builds/VisualStudio2015/RippleD.vcxproj
+++ b/Builds/VisualStudio2015/RippleD.vcxproj
@@ -1179,6 +1179,10 @@
True
True
+
+ True
+ True
+
True
True
@@ -1273,6 +1277,12 @@
+
+ True
+ True
+
+
+
True
True
@@ -2854,6 +2864,8 @@
+
+
@@ -3051,6 +3063,10 @@
+
+ True
+ True
+
True
True
@@ -3183,6 +3199,10 @@
True
True
+
+ True
+ True
+
True
True
diff --git a/Builds/VisualStudio2015/RippleD.vcxproj.filters b/Builds/VisualStudio2015/RippleD.vcxproj.filters
index 493226509..cb10cbe3e 100644
--- a/Builds/VisualStudio2015/RippleD.vcxproj.filters
+++ b/Builds/VisualStudio2015/RippleD.vcxproj.filters
@@ -1647,6 +1647,9 @@
ripple\app\tests
+
+ ripple\app\tests
+
ripple\app\tests
@@ -1734,6 +1737,12 @@
ripple\app\tx\impl
+
+ ripple\app\tx\impl
+
+
+ ripple\app\tx\impl
+
ripple\app\tx\impl
@@ -3351,6 +3360,9 @@
ripple\protocol
+
+ ripple\protocol
+
ripple\protocol
@@ -3564,6 +3576,9 @@
ripple\rpc
+
+ ripple\rpc\handlers
+
ripple\rpc\handlers
@@ -3666,6 +3681,9 @@
ripple\rpc\handlers
+
+ ripple\rpc\handlers
+
ripple\rpc\handlers
diff --git a/src/ripple/app/main/Amendments.cpp b/src/ripple/app/main/Amendments.cpp
index 2d15c9691..157114538 100644
--- a/src/ripple/app/main/Amendments.cpp
+++ b/src/ripple/app/main/Amendments.cpp
@@ -45,7 +45,8 @@ supportedAmendments ()
{ "6781F8368C4771B83E8B821D88F580202BCB4228075297B19E4FDC5233F1EFDC TrustSetAuth" },
{ "42426C4D4F1009EE67080A9B7965B44656D7714D104A72F9B4369F97ABF044EE FeeEscalation" },
{ "5CC22CFF2864B020BD79E0E1F048F63EF3594F95E650E43B3F837EF1DF5F4B26 FlowV2"},
- { "9178256A980A86CF3D70D0260A7DA6402AAFE43632FDBCB88037978404188871 OwnerPaysFee"}
+ { "9178256A980A86CF3D70D0260A7DA6402AAFE43632FDBCB88037978404188871 OwnerPaysFee"},
+ { "08DE7D96082187F6E6578530258C77FAABABE4C20474BDB82F04B021F1A68647 PayChan"}
};
}
diff --git a/src/ripple/app/main/Main.cpp b/src/ripple/app/main/Main.cpp
index 0df70a6f8..d0386a104 100644
--- a/src/ripple/app/main/Main.cpp
+++ b/src/ripple/app/main/Main.cpp
@@ -121,11 +121,14 @@ void printHelp (const po::options_description& desc)
" account_currencies [] [strict]\n"
" account_info ||| [] [strict]\n"
" account_lines |\"\" []\n"
+ " account_channels |\"\" []\n"
" account_objects [] [strict]\n"
" account_offers | []\n"
" account_tx accountID [ledger_min [ledger_max [limit [offset]]]] [binary] [count] [descending]\n"
" book_offers [ [ [ []]]]]\n"
" can_delete [||now|always|never]\n"
+ " channel_authorize \n"
+ " channel_verify \n"
" connect []\n"
" consensus_info\n"
" feature [ [accept|reject]]\n"
diff --git a/src/ripple/app/tests/PayChan_test.cpp b/src/ripple/app/tests/PayChan_test.cpp
new file mode 100644
index 000000000..d9b667c3c
--- /dev/null
+++ b/src/ripple/app/tests/PayChan_test.cpp
@@ -0,0 +1,840 @@
+//------------------------------------------------------------------------------
+/*
+ This file is part of rippled: https://github.com/ripple/rippled
+ Copyright (c) 2012, 2013 Ripple Labs Inc.
+
+ Permission to use, copy, modify, and/or distribute this software for any
+ purpose with or without fee is hereby granted, provided that the above
+ copyright notice and this permission notice appear in all copies.
+
+ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+*/
+//==============================================================================
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include
+
+namespace ripple
+{
+namespace test
+{
+struct PayChan_test : public beast::unit_test::suite
+{
+ static
+ uint256
+ channel (ReadView const& view,
+ jtx::Account const& account,
+ jtx::Account const& dst)
+ {
+ auto const sle = view.read (keylet::account (account));
+ if (!sle)
+ return beast::zero;
+ auto const k = keylet::payChan (account, dst, (*sle)[sfSequence] - 1);
+ return k.key;
+ }
+
+ static Buffer
+ signClaimAuth (PublicKey const& pk,
+ SecretKey const& sk,
+ uint256 const& channel,
+ STAmount const& authAmt)
+ {
+ Serializer msg;
+ serializePayChanAuthorization (msg, channel, authAmt.xrp ());
+ return sign (pk, sk, msg.slice ());
+ }
+
+ static
+ STAmount
+ channelBalance (ReadView const& view, uint256 const& chan)
+ {
+ auto const slep = view.read ({ltPAYCHAN, chan});
+ if (!slep)
+ return XRPAmount{-1};
+ return (*slep)[sfBalance];
+ }
+
+ static
+ bool
+ channelExists (ReadView const& view, uint256 const& chan)
+ {
+ auto const slep = view.read ({ltPAYCHAN, chan});
+ return bool(slep);
+ }
+
+ static
+ STAmount
+ channelAmount (ReadView const& view, uint256 const& chan)
+ {
+ auto const slep = view.read ({ltPAYCHAN, chan});
+ if (!slep)
+ return XRPAmount{-1};
+ return (*slep)[sfAmount];
+ }
+
+ static
+ boost::optional
+ channelExpiration (ReadView const& view, uint256 const& chan)
+ {
+ auto const slep = view.read ({ltPAYCHAN, chan});
+ if (!slep)
+ return boost::none;
+ if (auto const r = (*slep)[~sfExpiration])
+ return r.value();
+ return boost::none;
+
+ }
+
+ static Json::Value
+ create (jtx::Account const& account,
+ jtx::Account const& to,
+ STAmount const& amount,
+ NetClock::duration const& settleDelay,
+ PublicKey const& pk,
+ boost::optional const& cancelAfter = boost::none,
+ boost::optional const& dstTag = boost::none)
+ {
+ using namespace jtx;
+ Json::Value jv;
+ jv[jss::TransactionType] = "PaymentChannelCreate";
+ jv[jss::Flags] = tfUniversal;
+ jv[jss::Account] = account.human ();
+ jv[jss::Destination] = to.human ();
+ jv[jss::Amount] = amount.getJson (0);
+ jv["SettleDelay"] = settleDelay.count ();
+ jv["PublicKey"] = strHex (pk.slice ());
+ if (cancelAfter)
+ jv["CancelAfter"] = cancelAfter->time_since_epoch ().count ();
+ if (dstTag)
+ jv["DestinationTag"] = *dstTag;
+ return jv;
+ }
+
+ static
+ Json::Value
+ fund (jtx::Account const& account,
+ uint256 const& channel,
+ STAmount const& amount,
+ boost::optional const& expiration = boost::none)
+ {
+ using namespace jtx;
+ Json::Value jv;
+ jv[jss::TransactionType] = "PaymentChannelFund";
+ jv[jss::Flags] = tfUniversal;
+ jv[jss::Account] = account.human ();
+ jv["Channel"] = to_string (channel);
+ jv[jss::Amount] = amount.getJson (0);
+ if (expiration)
+ jv["Expiration"] = expiration->time_since_epoch ().count ();
+ return jv;
+ }
+
+ static
+ Json::Value
+ claim (jtx::Account const& account,
+ uint256 const& channel,
+ boost::optional const& balance = boost::none,
+ boost::optional const& amount = boost::none,
+ boost::optional const& signature = boost::none,
+ boost::optional const& pk = boost::none)
+ {
+ using namespace jtx;
+ Json::Value jv;
+ jv[jss::TransactionType] = "PaymentChannelClaim";
+ jv[jss::Flags] = tfUniversal;
+ jv[jss::Account] = account.human ();
+ jv["Channel"] = to_string (channel);
+ if (amount)
+ jv[jss::Amount] = amount->getJson (0);
+ if (balance)
+ jv["Balance"] = balance->getJson (0);
+ if (signature)
+ jv["Signature"] = strHex (*signature);
+ if (pk)
+ jv["PublicKey"] = strHex (pk->slice ());
+ return jv;
+ }
+
+ void
+ testSimple ()
+ {
+ testcase ("simple");
+ using namespace jtx;
+ using namespace std::literals::chrono_literals;
+ Env env (*this, features (featurePayChan));
+ auto const alice = Account ("alice");
+ auto const bob = Account ("bob");
+ auto USDA = alice["USD"];
+ env.fund (XRP (10000), alice, bob);
+ auto const pk = alice.pk ();
+ auto const settleDelay = 100s;
+ env (create (alice, bob, XRP (1000), settleDelay, pk));
+ auto const chan = channel (*env.current (), alice, bob);
+ BEAST_EXPECT (channelBalance (*env.current (), chan) == XRP (0));
+ BEAST_EXPECT (channelAmount (*env.current (), chan) == XRP (1000));
+
+ {
+ auto const preAlice = env.balance (alice);
+ env (fund (alice, chan, XRP (1000)));
+ auto const feeDrops = env.current ()->fees ().base;
+ BEAST_EXPECT (env.balance (alice) == preAlice - XRP (1000) - feeDrops);
+ }
+
+ auto chanBal = channelBalance (*env.current (), chan);
+ auto chanAmt = channelAmount (*env.current (), chan);
+ BEAST_EXPECT (chanBal == XRP (0));
+ BEAST_EXPECT (chanAmt == XRP (2000));
+
+ {
+ // bad amounts (non-xrp, negative amounts)
+ env (create (alice, bob, USDA (1000), settleDelay, pk),
+ ter (temBAD_AMOUNT));
+ env (fund (alice, chan, USDA (1000)),
+ ter (temBAD_AMOUNT));
+ env (create (alice, bob, XRP (-1000), settleDelay, pk),
+ ter (temBAD_AMOUNT));
+ env (fund (alice, chan, XRP (-1000)),
+ ter (temBAD_AMOUNT));
+ }
+
+ // invalid account
+ env (create (alice, "noAccount", XRP (1000), settleDelay, pk),
+ ter (tecNO_DST));
+ // can't create channel to the same account
+ env (create (alice, alice, XRP (1000), settleDelay, pk),
+ ter (temDST_IS_SRC));
+ // invalid channel
+ env (fund (alice, channel (*env.current (), alice, "noAccount"), XRP (1000)),
+ ter (tecNO_ENTRY));
+ // not enough funds
+ env (create (alice, bob, XRP (10000), settleDelay, pk),
+ ter (tecUNFUNDED));
+
+ {
+ // No signature claim with bad amounts (negative and non-xrp)
+ auto const iou = USDA (100).value ();
+ auto const negXRP = XRP (-100).value ();
+ auto const posXRP = XRP (100).value ();
+ env (claim (alice, chan, iou, iou), ter (temBAD_AMOUNT));
+ env (claim (alice, chan, posXRP, iou), ter (temBAD_AMOUNT));
+ env (claim (alice, chan, iou, posXRP), ter (temBAD_AMOUNT));
+ env (claim (alice, chan, negXRP, negXRP), ter (temBAD_AMOUNT));
+ env (claim (alice, chan, posXRP, negXRP), ter (temBAD_AMOUNT));
+ env (claim (alice, chan, negXRP, posXRP), ter (temBAD_AMOUNT));
+ }
+ {
+ // No signature claim more than authorized
+ auto const delta = XRP (500);
+ auto const reqBal = chanBal + delta;
+ auto const authAmt = reqBal + XRP (-100);
+ assert (reqBal <= chanAmt);
+ env (claim (alice, chan, reqBal, authAmt), ter (tecNO_PERMISSION));
+ }
+ {
+ // No signature needed since the owner is claiming
+ auto const preBob = env.balance (bob);
+ auto const delta = XRP (500);
+ auto const reqBal = chanBal + delta;
+ auto const authAmt = reqBal + XRP (100);
+ assert (reqBal <= chanAmt);
+ env (claim (alice, chan, reqBal, authAmt));
+ BEAST_EXPECT (channelBalance (*env.current (), chan) == reqBal);
+ BEAST_EXPECT (channelAmount (*env.current (), chan) == chanAmt);
+ BEAST_EXPECT (env.balance (bob) == preBob + delta);
+ chanBal = reqBal;
+ }
+ {
+ // Claim with signature
+ auto preBob = env.balance (bob);
+ auto const delta = XRP (500);
+ auto const reqBal = chanBal + delta;
+ auto const authAmt = reqBal + XRP (100);
+ assert (reqBal <= chanAmt);
+ auto const sig =
+ signClaimAuth (alice.pk (), alice.sk (), chan, authAmt);
+ env (claim (bob, chan, reqBal, authAmt, Slice (sig), alice.pk ()));
+ BEAST_EXPECT (channelBalance (*env.current (), chan) == reqBal);
+ BEAST_EXPECT (channelAmount (*env.current (), chan) == chanAmt);
+ auto const feeDrops = env.current ()->fees ().base;
+ BEAST_EXPECT (env.balance (bob) == preBob + delta - feeDrops);
+ chanBal = reqBal;
+
+ // claim again
+ preBob = env.balance (bob);
+ env (claim (bob, chan, reqBal, authAmt, Slice (sig), alice.pk ()),
+ ter (tecUNFUNDED_PAYMENT));
+ BEAST_EXPECT (channelBalance (*env.current (), chan) == chanBal);
+ BEAST_EXPECT (channelAmount (*env.current (), chan) == chanAmt);
+ BEAST_EXPECT (env.balance (bob) == preBob - feeDrops);
+ }
+ {
+ // Try to claim more than authorized
+ auto const preBob = env.balance (bob);
+ STAmount const authAmt = chanBal + XRP (500);
+ STAmount const reqAmt = authAmt + 1;
+ assert (reqAmt <= chanAmt);
+ auto const sig =
+ signClaimAuth (alice.pk (), alice.sk (), chan, authAmt);
+ env (claim (bob, chan, reqAmt, authAmt, Slice (sig), alice.pk ()),
+ ter (tecNO_PERMISSION));
+ BEAST_EXPECT (channelBalance (*env.current (), chan) == chanBal);
+ BEAST_EXPECT (channelAmount (*env.current (), chan) == chanAmt);
+ auto const feeDrops = env.current ()->fees ().base;
+ BEAST_EXPECT (env.balance (bob) == preBob - feeDrops);
+ }
+
+ // Dst tries to fund the channel
+ env (fund (bob, chan, XRP (1000)), ter (tecNO_PERMISSION));
+ BEAST_EXPECT (channelBalance (*env.current (), chan) == chanBal);
+ BEAST_EXPECT (channelAmount (*env.current (), chan) == chanAmt);
+
+ {
+ // Wrong signing key
+ auto const sig =
+ signClaimAuth (bob.pk (), bob.sk (), chan, XRP (1500));
+ env (claim (bob, chan, XRP (1500).value (), XRP (1500).value (),
+ Slice (sig), bob.pk ()),
+ ter (temBAD_SIGNER));
+ BEAST_EXPECT (channelBalance (*env.current (), chan) == chanBal);
+ BEAST_EXPECT (channelAmount (*env.current (), chan) == chanAmt);
+ }
+ {
+ // Bad signature
+ auto const sig =
+ signClaimAuth (bob.pk (), bob.sk (), chan, XRP (1500));
+ env (claim (bob, chan, XRP (1500).value (), XRP (1500).value (),
+ Slice (sig), alice.pk ()),
+ ter (temBAD_SIGNATURE));
+ BEAST_EXPECT (channelBalance (*env.current (), chan) == chanBal);
+ BEAST_EXPECT (channelAmount (*env.current (), chan) == chanAmt);
+ }
+ {
+ // Dst closes channel
+ auto const preAlice = env.balance (alice);
+ auto const preBob = env.balance (bob);
+ env (claim (bob, chan), txflags (tfClose));
+ BEAST_EXPECT (!channelExists (*env.current (), chan));
+ auto const feeDrops = env.current ()->fees ().base;
+ auto const delta = chanAmt - chanBal;
+ assert (delta > beast::zero);
+ BEAST_EXPECT (env.balance (alice) == preAlice + delta);
+ BEAST_EXPECT (env.balance (bob) == preBob - feeDrops);
+ }
+ }
+
+ void
+ testCancelAfter ()
+ {
+ testcase ("cancel after");
+ using namespace jtx;
+ using namespace std::literals::chrono_literals;
+ auto const alice = Account ("alice");
+ auto const bob = Account ("bob");
+ auto const carol = Account ("carol");
+ {
+ // If dst claims after cancel after, channel closes
+ Env env (*this, features (featurePayChan));
+ env.fund (XRP (10000), alice, bob);
+ auto const pk = alice.pk ();
+ auto const settleDelay = 100s;
+ NetClock::time_point const cancelAfter =
+ env.current ()->info ().parentCloseTime + 3600s;
+ auto const channelFunds = XRP (1000);
+ env (create (
+ alice, bob, channelFunds, settleDelay, pk, cancelAfter));
+ auto const chan = channel (*env.current (), alice, bob);
+ if (!chan)
+ {
+ fail ();
+ return;
+ }
+ BEAST_EXPECT (channelExists (*env.current (), chan));
+ env.close (cancelAfter);
+ {
+ // dst cannot claim after cancelAfter
+ auto const chanBal = channelBalance (*env.current (), chan);
+ auto const chanAmt = channelAmount (*env.current (), chan);
+ auto preAlice = env.balance (alice);
+ auto preBob = env.balance (bob);
+ auto const delta = XRP (500);
+ auto const reqBal = chanBal + delta;
+ auto const authAmt = reqBal + XRP (100);
+ assert (reqBal <= chanAmt);
+ auto const sig =
+ signClaimAuth (alice.pk (), alice.sk (), chan, authAmt);
+ env (claim (
+ bob, chan, reqBal, authAmt, Slice (sig), alice.pk ()));
+ auto const feeDrops = env.current ()->fees ().base;
+ BEAST_EXPECT (!channelExists (*env.current (), chan));
+ BEAST_EXPECT (env.balance (bob) == preBob - feeDrops);
+ BEAST_EXPECT (env.balance (alice) == preAlice + channelFunds);
+ }
+ }
+ {
+ // Third party can close after cancel after
+ Env env (*this, features (featurePayChan));
+ env.fund (XRP (10000), alice, bob, carol);
+ auto const pk = alice.pk ();
+ auto const settleDelay = 100s;
+ NetClock::time_point const cancelAfter =
+ env.current ()->info ().parentCloseTime + 3600s;
+ auto const channelFunds = XRP (1000);
+ env (create (
+ alice, bob, channelFunds, settleDelay, pk, cancelAfter));
+ auto const chan = channel (*env.current (), alice, bob);
+ BEAST_EXPECT (channelExists (*env.current (), chan));
+ // third party close before cancelAfter
+ env (claim (carol, chan), txflags (tfClose),
+ ter (tecNO_PERMISSION));
+ BEAST_EXPECT (channelExists (*env.current (), chan));
+ env.close (cancelAfter);
+ // third party close after cancelAfter
+ auto const preAlice = env.balance (alice);
+ env (claim (carol, chan), txflags (tfClose));
+ BEAST_EXPECT (!channelExists (*env.current (), chan));
+ BEAST_EXPECT (env.balance (alice) == preAlice + channelFunds);
+ }
+ }
+
+ void
+ testExpiration ()
+ {
+ testcase ("expiration");
+ using namespace jtx;
+ using namespace std::literals::chrono_literals;
+ Env env (*this, features (featurePayChan));
+ auto const alice = Account ("alice");
+ auto const bob = Account ("bob");
+ auto const carol = Account ("carol");
+ env.fund (XRP (10000), alice, bob, carol);
+ auto const pk = alice.pk ();
+ auto const settleDelay = 3600s;
+ auto const closeTime = env.current ()->info ().parentCloseTime;
+ auto const minExpiration = closeTime + settleDelay;
+ NetClock::time_point const cancelAfter = closeTime + 7200s;
+ auto const channelFunds = XRP (1000);
+ env (create (alice, bob, channelFunds, settleDelay, pk, cancelAfter));
+ auto const chan = channel (*env.current (), alice, bob);
+ BEAST_EXPECT (channelExists (*env.current (), chan));
+ BEAST_EXPECT (!channelExpiration (*env.current (), chan));
+ // Owner closes, will close after settleDelay
+ env (claim (alice, chan), txflags (tfClose));
+ auto counts = [](
+ auto const& t) { return t.time_since_epoch ().count (); };
+ BEAST_EXPECT (*channelExpiration (*env.current (), chan) ==
+ counts (minExpiration));
+ // increase the expiration time
+ env (fund (
+ alice, chan, XRP (1), NetClock::time_point{minExpiration + 100s}));
+ BEAST_EXPECT (*channelExpiration (*env.current (), chan) ==
+ counts (minExpiration) + 100);
+ // decrease the expiration, but still above minExpiration
+ env (fund (
+ alice, chan, XRP (1), NetClock::time_point{minExpiration + 50s}));
+ BEAST_EXPECT (*channelExpiration (*env.current (), chan) ==
+ counts (minExpiration) + 50);
+ // decrease the expiration below minExpiration
+ env (fund (alice, chan, XRP (1),
+ NetClock::time_point{minExpiration - 50s}),
+ ter (temBAD_EXPIRATION));
+ BEAST_EXPECT (*channelExpiration (*env.current (), chan) ==
+ counts (minExpiration) + 50);
+ env (claim (bob, chan), txflags (tfRenew), ter (tecNO_PERMISSION));
+ BEAST_EXPECT (*channelExpiration (*env.current (), chan) ==
+ counts (minExpiration) + 50);
+ env (claim (alice, chan), txflags (tfRenew));
+ BEAST_EXPECT (!channelExpiration (*env.current (), chan));
+ // decrease the expiration below minExpiration
+ env (fund (alice, chan, XRP (1),
+ NetClock::time_point{minExpiration - 50s}),
+ ter (temBAD_EXPIRATION));
+ BEAST_EXPECT (!channelExpiration (*env.current (), chan));
+ env (fund (alice, chan, XRP (1), NetClock::time_point{minExpiration}));
+ env.close (minExpiration);
+ // Try to extend the expiration after the expiration has already passed
+ env (fund (
+ alice, chan, XRP (1), NetClock::time_point{minExpiration + 1000s}));
+ BEAST_EXPECT (!channelExists (*env.current (), chan));
+ }
+
+ void
+ testSettleDelay ()
+ {
+ testcase ("settle delay");
+ using namespace jtx;
+ using namespace std::literals::chrono_literals;
+ Env env (*this, features (featurePayChan));
+ auto const alice = Account ("alice");
+ auto const bob = Account ("bob");
+ env.fund (XRP (10000), alice, bob);
+ auto const pk = alice.pk ();
+ auto const settleDelay = 3600s;
+ NetClock::time_point const settleTimepoint =
+ env.current ()->info ().parentCloseTime + settleDelay;
+ auto const channelFunds = XRP (1000);
+ env (create (alice, bob, channelFunds, settleDelay, pk));
+ auto const chan = channel (*env.current (), alice, bob);
+ BEAST_EXPECT (channelExists (*env.current (), chan));
+ // Owner closes, will close after settleDelay
+ env (claim (alice, chan), txflags (tfClose));
+ BEAST_EXPECT (channelExists (*env.current (), chan));
+ env.close (settleTimepoint-settleDelay/2);
+ {
+ // receiver can still claim
+ auto const chanBal = channelBalance (*env.current (), chan);
+ auto const chanAmt = channelAmount (*env.current (), chan);
+ auto preBob = env.balance (bob);
+ auto const delta = XRP (500);
+ auto const reqBal = chanBal + delta;
+ auto const authAmt = reqBal + XRP (100);
+ assert (reqBal <= chanAmt);
+ auto const sig =
+ signClaimAuth (alice.pk (), alice.sk (), chan, authAmt);
+ env (claim (bob, chan, reqBal, authAmt, Slice (sig), alice.pk ()));
+ BEAST_EXPECT (channelBalance (*env.current (), chan) == reqBal);
+ BEAST_EXPECT (channelAmount (*env.current (), chan) == chanAmt);
+ auto const feeDrops = env.current ()->fees ().base;
+ BEAST_EXPECT (env.balance (bob) == preBob + delta - feeDrops);
+ }
+ env.close (settleTimepoint);
+ {
+ // past settleTime, channel will close
+ auto const chanBal = channelBalance (*env.current (), chan);
+ auto const chanAmt = channelAmount (*env.current (), chan);
+ auto const preAlice = env.balance (alice);
+ auto preBob = env.balance (bob);
+ auto const delta = XRP (500);
+ auto const reqBal = chanBal + delta;
+ auto const authAmt = reqBal + XRP (100);
+ assert (reqBal <= chanAmt);
+ auto const sig =
+ signClaimAuth (alice.pk (), alice.sk (), chan, authAmt);
+ env (claim (bob, chan, reqBal, authAmt, Slice (sig), alice.pk ()));
+ BEAST_EXPECT (!channelExists (*env.current (), chan));
+ auto const feeDrops = env.current ()->fees ().base;
+ BEAST_EXPECT (env.balance (alice) == preAlice + chanAmt - chanBal);
+ BEAST_EXPECT (env.balance (bob) == preBob - feeDrops);
+ }
+ }
+
+ void
+ testCloseDry ()
+ {
+ testcase ("close dry");
+ using namespace jtx;
+ using namespace std::literals::chrono_literals;
+ Env env (*this, features (featurePayChan));
+ auto const alice = Account ("alice");
+ auto const bob = Account ("bob");
+ env.fund (XRP (10000), alice, bob);
+ auto const pk = alice.pk ();
+ auto const settleDelay = 3600s;
+ auto const channelFunds = XRP (1000);
+ env (create (alice, bob, channelFunds, settleDelay, pk));
+ auto const chan = channel (*env.current (), alice, bob);
+ BEAST_EXPECT (channelExists (*env.current (), chan));
+ // Owner tries to close channel, but it will remain open (settle delay)
+ env (claim (alice, chan), txflags (tfClose));
+ BEAST_EXPECT (channelExists (*env.current (), chan));
+ {
+ // claim the entire amount
+ auto const preBob = env.balance (bob);
+ env (claim (
+ alice, chan, channelFunds.value (), channelFunds.value ()));
+ BEAST_EXPECT (channelBalance (*env.current (), chan) == channelFunds);
+ BEAST_EXPECT (env.balance (bob) == preBob + channelFunds);
+ }
+ auto const preAlice = env.balance (alice);
+ // Channel is now dry, can close before expiration date
+ env (claim (alice, chan), txflags (tfClose));
+ BEAST_EXPECT (!channelExists (*env.current (), chan));
+ auto const feeDrops = env.current ()->fees ().base;
+ BEAST_EXPECT (env.balance (alice) == preAlice - feeDrops);
+ }
+
+ void
+ testDefaultAmount ()
+ {
+ // auth amount defaults to balance if not present
+ testcase ("default amount");
+ using namespace jtx;
+ using namespace std::literals::chrono_literals;
+ Env env (*this, features (featurePayChan));
+ auto const alice = Account ("alice");
+ auto const bob = Account ("bob");
+ env.fund (XRP (10000), alice, bob);
+ auto const pk = alice.pk ();
+ auto const settleDelay = 3600s;
+ auto const channelFunds = XRP (1000);
+ env (create (alice, bob, channelFunds, settleDelay, pk));
+ auto const chan = channel (*env.current (), alice, bob);
+ BEAST_EXPECT (channelExists (*env.current (), chan));
+ // Owner tries to close channel, but it will remain open (settle delay)
+ env (claim (alice, chan), txflags (tfClose));
+ BEAST_EXPECT (channelExists (*env.current (), chan));
+ {
+ auto chanBal = channelBalance (*env.current (), chan);
+ auto chanAmt = channelAmount (*env.current (), chan);
+ auto const preBob = env.balance (bob);
+
+ auto const delta = XRP (500);
+ auto const reqBal = chanBal + delta;
+ assert (reqBal <= chanAmt);
+ auto const sig =
+ signClaimAuth (alice.pk (), alice.sk (), chan, reqBal);
+ env (claim (
+ bob, chan, reqBal, boost::none, Slice (sig), alice.pk ()));
+ BEAST_EXPECT (channelBalance (*env.current (), chan) == reqBal);
+ auto const feeDrops = env.current ()->fees ().base;
+ BEAST_EXPECT (env.balance (bob) == preBob + delta - feeDrops);
+ chanBal = reqBal;
+ }
+ {
+ // Claim again
+ auto chanBal = channelBalance (*env.current (), chan);
+ auto chanAmt = channelAmount (*env.current (), chan);
+ auto const preBob = env.balance (bob);
+
+ auto const delta = XRP (500);
+ auto const reqBal = chanBal + delta;
+ assert (reqBal <= chanAmt);
+ auto const sig =
+ signClaimAuth (alice.pk (), alice.sk (), chan, reqBal);
+ env (claim (
+ bob, chan, reqBal, boost::none, Slice (sig), alice.pk ()));
+ BEAST_EXPECT (channelBalance (*env.current (), chan) == reqBal);
+ auto const feeDrops = env.current ()->fees ().base;
+ BEAST_EXPECT (env.balance (bob) == preBob + delta - feeDrops);
+ chanBal = reqBal;
+ }
+ }
+
+ void
+ testDisallowXRP ()
+ {
+ // auth amount defaults to balance if not present
+ testcase ("Disallow XRP");
+ using namespace jtx;
+ using namespace std::literals::chrono_literals;
+ {
+ // Create a channel where dst disallows XRP
+ Env env (*this, features (featurePayChan));
+ auto const alice = Account ("alice");
+ auto const bob = Account ("bob");
+ env.fund (XRP (10000), alice, bob);
+ env (fset (bob, asfDisallowXRP));
+ auto const pk = alice.pk ();
+ auto const settleDelay = 3600s;
+ auto const channelFunds = XRP (1000);
+ env (create (alice, bob, channelFunds, settleDelay, pk),
+ ter (tecNO_TARGET));
+ auto const chan = channel (*env.current (), alice, bob);
+ BEAST_EXPECT (!channelExists (*env.current (), chan));
+ }
+ {
+ // Claim to a channel where dst disallows XRP
+ // (channel is created before disallow xrp is set)
+ Env env (*this, features (featurePayChan));
+ auto const alice = Account ("alice");
+ auto const bob = Account ("bob");
+ env.fund (XRP (10000), alice, bob);
+ auto const pk = alice.pk ();
+ auto const settleDelay = 3600s;
+ auto const channelFunds = XRP (1000);
+ env (create (alice, bob, channelFunds, settleDelay, pk));
+ auto const chan = channel (*env.current (), alice, bob);
+ BEAST_EXPECT (channelExists (*env.current (), chan));
+
+ env (fset (bob, asfDisallowXRP));
+ auto const preBob = env.balance (bob);
+ auto const reqBal = XRP (500).value();
+ env (claim (alice, chan, reqBal, reqBal), ter(tecNO_TARGET));
+ }
+ }
+
+ void
+ testDstTag ()
+ {
+ // auth amount defaults to balance if not present
+ testcase ("Dst Tag");
+ using namespace jtx;
+ using namespace std::literals::chrono_literals;
+ // Create a channel where dst disallows XRP
+ Env env (*this, features (featurePayChan));
+ auto const alice = Account ("alice");
+ auto const bob = Account ("bob");
+ env.fund (XRP (10000), alice, bob);
+ env (fset (bob, asfRequireDest));
+ auto const pk = alice.pk ();
+ auto const settleDelay = 3600s;
+ auto const channelFunds = XRP (1000);
+ env (create (alice, bob, channelFunds, settleDelay, pk),
+ ter (tecDST_TAG_NEEDED));
+ BEAST_EXPECT (!channelExists (
+ *env.current (), channel (*env.current (), alice, bob)));
+ env (
+ create (alice, bob, channelFunds, settleDelay, pk, boost::none, 1));
+ BEAST_EXPECT (channelExists (
+ *env.current (), channel (*env.current (), alice, bob)));
+ }
+
+ void
+ testMultiple ()
+ {
+ // auth amount defaults to balance if not present
+ testcase ("Multiple channels to the same account");
+ using namespace jtx;
+ using namespace std::literals::chrono_literals;
+ Env env (*this, features (featurePayChan));
+ auto const alice = Account ("alice");
+ auto const bob = Account ("bob");
+ env.fund (XRP (10000), alice, bob);
+ auto const pk = alice.pk ();
+ auto const settleDelay = 3600s;
+ auto const channelFunds = XRP (1000);
+ env (create (alice, bob, channelFunds, settleDelay, pk));
+ auto const chan1 = channel (*env.current (), alice, bob);
+ BEAST_EXPECT (channelExists (*env.current (), chan1));
+ env (create (alice, bob, channelFunds, settleDelay, pk));
+ auto const chan2 = channel (*env.current (), alice, bob);
+ BEAST_EXPECT (channelExists (*env.current (), chan2));
+ BEAST_EXPECT (chan1 != chan2);
+ }
+
+ void
+ testRPC ()
+ {
+ testcase ("RPC");
+ using namespace jtx;
+ using namespace std::literals::chrono_literals;
+ Env env (*this, features (featurePayChan));
+ auto const alice = Account ("alice");
+ auto const bob = Account ("bob");
+ env.fund (XRP (10000), alice, bob);
+ auto const pk = alice.pk ();
+ auto const settleDelay = 3600s;
+ auto const channelFunds = XRP (1000);
+ env (create (alice, bob, channelFunds, settleDelay, pk));
+ env.close();
+ auto const chan1Str = to_string (channel (*env.current (), alice, bob));
+ std::string chan1PkStr;
+ {
+ auto const r =
+ env.rpc ("account_channels", alice.human (), bob.human ());
+ BEAST_EXPECT (r[jss::result][jss::channels].size () == 1);
+ BEAST_EXPECT (r[jss::result][jss::channels][0u][jss::channel_id] == chan1Str);
+ chan1PkStr = r[jss::result][jss::channels][0u][jss::public_key].asString();
+ }
+ {
+ auto const r =
+ env.rpc ("account_channels", bob.human (), alice.human ());
+ BEAST_EXPECT (r[jss::result][jss::channels].size () == 0);
+ }
+ env (create (alice, bob, channelFunds, settleDelay, pk));
+ env.close();
+ auto const chan2Str = to_string (channel (*env.current (), alice, bob));
+ {
+ auto const r =
+ env.rpc ("account_channels", alice.human (), bob.human ());
+ BEAST_EXPECT (r[jss::result][jss::channels].size () == 2);
+ BEAST_EXPECT (chan1Str != chan2Str);
+ for (auto const& c : {chan1Str, chan2Str})
+ BEAST_EXPECT (r[jss::result][jss::channels][0u][jss::channel_id] == c ||
+ r[jss::result][jss::channels][1u][jss::channel_id] == c);
+ }
+ {
+ // Verify chan1 auth
+ auto const rs =
+ env.rpc ("channel_authorize", "alice", chan1Str, "1000");
+ auto const sig = rs[jss::result][jss::signature].asString ();
+ BEAST_EXPECT (!sig.empty ());
+ auto const rv = env.rpc (
+ "channel_verify", chan1PkStr, chan1Str, "1000", sig);
+ BEAST_EXPECT (rv[jss::result][jss::signature_verified].asBool ());
+ }
+ {
+ // Try to verify chan2 auth with chan1 key
+ auto const rs =
+ env.rpc ("channel_authorize", "alice", chan2Str, "1000");
+ auto const sig = rs[jss::result][jss::signature].asString ();
+ BEAST_EXPECT (!sig.empty ());
+ auto const rv =
+ env.rpc ("channel_verify", chan1PkStr, chan1Str, "1000", sig);
+ BEAST_EXPECT (!rv[jss::result][jss::signature_verified].asBool ());
+ }
+ }
+
+ void
+ testOptionalFields ()
+ {
+ testcase ("Optional Fields");
+ using namespace jtx;
+ using namespace std::literals::chrono_literals;
+ Env env (*this, features (featurePayChan));
+ auto const alice = Account ("alice");
+ auto const bob = Account ("bob");
+ auto const carol = Account ("carol");
+ auto const dan = Account ("dan");
+ env.fund (XRP (10000), alice, bob, carol, dan);
+ auto const pk = alice.pk ();
+ auto const settleDelay = 3600s;
+ auto const channelFunds = XRP (1000);
+
+ boost::optional cancelAfter;
+
+ {
+ env (create (alice, bob, channelFunds, settleDelay, pk));
+ auto const chan = to_string (channel (*env.current (), alice, bob));
+ auto const r =
+ env.rpc ("account_channels", alice.human (), bob.human ());
+ BEAST_EXPECT (r[jss::result][jss::channels].size () == 1);
+ BEAST_EXPECT (r[jss::result][jss::channels][0u][jss::channel_id] == chan);
+ BEAST_EXPECT (!r[jss::result][jss::channels][0u].isMember(jss::destination_tag));
+ }
+ {
+ std::uint32_t dstTag=42;
+ env (create (
+ alice, carol, channelFunds, settleDelay, pk, cancelAfter, dstTag));
+ auto const chan = to_string (channel (*env.current (), alice, carol));
+ auto const r =
+ env.rpc ("account_channels", alice.human (), carol.human ());
+ BEAST_EXPECT (r[jss::result][jss::channels].size () == 1);
+ BEAST_EXPECT (r[jss::result][jss::channels][0u][jss::channel_id] == chan);
+ BEAST_EXPECT (r[jss::result][jss::channels][0u][jss::destination_tag] == dstTag);
+ }
+ }
+
+ void
+ run () override
+ {
+ testSimple ();
+ testCancelAfter ();
+ testSettleDelay ();
+ testExpiration ();
+ testCloseDry ();
+ testDefaultAmount ();
+ testDisallowXRP ();
+ testDstTag ();
+ testMultiple ();
+ testRPC ();
+ testOptionalFields ();
+ }
+};
+
+BEAST_DEFINE_TESTSUITE (PayChan, app, ripple);
+} // test
+} // ripple
diff --git a/src/ripple/app/tx/impl/PayChan.cpp b/src/ripple/app/tx/impl/PayChan.cpp
new file mode 100644
index 000000000..000a3606c
--- /dev/null
+++ b/src/ripple/app/tx/impl/PayChan.cpp
@@ -0,0 +1,486 @@
+//------------------------------------------------------------------------------
+/*
+ This file is part of rippled: https://github.com/ripple/rippled
+ Copyright (c) 2012, 2013 Ripple Labs Inc.
+
+ Permission to use, copy, modify, and/or distribute this software for any
+ purpose with or without fee is hereby granted, provided that the above
+ copyright notice and this permission notice appear in all copies.
+
+ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+*/
+//==============================================================================
+
+#include
+#include
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+namespace ripple {
+
+/*
+ PaymentChannel
+
+ Payment channels permit off-ledger checkpoints of XRP payments flowing
+ in a single direction. A channel sequesters the owner's XRP in its own
+ ledger entry. The owner can authorize the recipient to claim up to a
+ given balance by giving the receiver a signed message (off-ledger). The
+ recipient can use this signed message to claim any unpaid balance while
+ the channel remains open. The owner can top off the line as needed. If
+ the channel has not paid out all its funds, the owner must wait out a
+ delay to close the channel to give the recipient a chance to supply any
+ claims. The recipient can close the channel at any time. Any transaction
+ that touches the channel after the expiration time will close the
+ channel. The total amount paid increases monotonically as newer claims
+ are issued. When the channel is closed any remaining balance is returned
+ to the owner. Channels are intended to permit intermittent off-ledger
+ settlement of ILP trust lines as balances get substantial. For
+ bidirectional channels, a payment channel can be used in each direction.
+
+ PaymentChannelCreate
+
+ Create a unidirectional channel. The parameters are:
+ Destination
+ The recipient at the end of the channel.
+ Amount
+ The amount of XRP to deposit in the channel immediately.
+ SettleDelay
+ The amount of time everyone but the recipient must wait for a
+ superior claim.
+ PublicKey
+ The key that will sign claims against the channel.
+ CancelAfter (optional)
+ Any channel transaction that touches this channel after the
+ `CancelAfter` time will close it.
+ DestinationTag (optional)
+ Destination tags allow the different accounts inside of a Hosted
+ Wallet to be mapped back onto the Ripple ledger. The destination tag
+ tells the server to which account in the Hosted Wallet the funds are
+ intended to go to. Required if the destination has lsfRequireDestTag
+ set.
+ SourceTag (optional)
+ Source tags allow the different accounts inside of a Hosted Wallet
+ to be mapped back onto the Ripple ledger. Source tags are similar to
+ destination tags but are for the channel owner to identify their own
+ transactions.
+
+ PaymentChannelFund
+
+ Add additional funds to the payment channel. Only the channel owner may
+ use this transaction. The parameters are:
+ Channel
+ The 256-bit ID of the channel.
+ Amount
+ The amount of XRP to add.
+ Expiration (optional)
+ Time the channel closes. The transaction will fail if the expiration
+ times does not satisfy the SettleDelay constraints.
+
+ PaymentChannelClaim
+
+ Place a claim against an existing channel. The parameters are:
+ Channel
+ The 256-bit ID of the channel.
+ Balance (optional)
+ The total amount of XRP delivered after this claim is processed (optional, not
+ needed if just closing).
+ Amount (optional)
+ The amount of XRP the signature is for (not needed if equal to Balance or just
+ closing the line).
+ Signature (optional)
+ Authorization for the balance above, signed by the owner (optional,
+ not needed if closing or owner is performing the transaction). The
+ signature if for the following message: CLM\0 followed by the
+ 256-bit channel ID, and a 64-bit integer drops.
+ PublicKey (optional)
+ The public key that made the signature (optional, required if a signature
+ is present)
+ Flags
+ tfCloseChannel
+ Request that the channel be closed
+ tfRenewChannel
+ Request that the channel's expiration be reset. Only the owner
+ may renew a channel.
+
+*/
+
+//------------------------------------------------------------------------------
+
+static
+TER
+closeChannel (
+ std::shared_ptr const& slep,
+ ApplyView& view,
+ uint256 const& key,
+ beast::Journal j)
+{
+ AccountID const src = (*slep)[sfAccount];
+ // Remove PayChan from owner directory
+ {
+ auto const page = (*slep)[sfOwnerNode];
+ TER const ter = dirDelete (view, true, page, keylet::ownerDir (src).key,
+ key, false, page == 0, j);
+ if (!isTesSuccess (ter))
+ return ter;
+ }
+
+ // Transfer amount back to owner, decrement owner count
+ auto const sle = view.peek (keylet::account (src));
+ assert ((*slep)[sfAmount] >= (*slep)[sfBalance]);
+ (*sle)[sfBalance] =
+ (*sle)[sfBalance] + (*slep)[sfAmount] - (*slep)[sfBalance];
+ (*sle)[sfOwnerCount] = (*sle)[sfOwnerCount] - 1;
+ view.update (sle);
+
+ // Remove PayChan from ledger
+ view.erase (slep);
+ return tesSUCCESS;
+}
+
+//------------------------------------------------------------------------------
+
+TER
+PayChanCreate::preflight (PreflightContext const& ctx)
+{
+ if (!ctx.rules.enabled (featurePayChan, ctx.app.config ().features))
+ return temDISABLED;
+
+ auto const ret = preflight1 (ctx);
+ if (!isTesSuccess (ret))
+ return ret;
+
+ if (!isXRP (ctx.tx[sfAmount]) || (ctx.tx[sfAmount] <= beast::zero))
+ return temBAD_AMOUNT;
+
+ if (ctx.tx[sfAccount] == ctx.tx[sfDestination])
+ return temDST_IS_SRC;
+
+ return preflight2 (ctx);
+}
+
+TER
+PayChanCreate::preclaim(PreclaimContext const &ctx)
+{
+ auto const account = ctx.tx[sfAccount];
+ auto const sle = ctx.view.read (keylet::account (account));
+
+ // Check reserve and funds availability
+ {
+ auto const balance = (*sle)[sfBalance];
+ auto const reserve =
+ ctx.view.fees ().accountReserve ((*sle)[sfOwnerCount] + 1);
+
+ if (balance < reserve)
+ return tecINSUFFICIENT_RESERVE;
+
+ if (balance < reserve + ctx.tx[sfAmount])
+ return tecUNFUNDED;
+ }
+
+ auto const dst = ctx.tx[sfDestination];
+
+ {
+ // Check destination account
+ auto const sled = ctx.view.read (keylet::account (dst));
+ if (!sled)
+ return tecNO_DST;
+ if (((*sled)[sfFlags] & lsfRequireDestTag) &&
+ !ctx.tx[~sfDestinationTag])
+ return tecDST_TAG_NEEDED;
+ if ((*sled)[sfFlags] & lsfDisallowXRP)
+ return tecNO_TARGET;
+ }
+
+ return tesSUCCESS;
+}
+
+TER
+PayChanCreate::doApply()
+{
+ auto const account = ctx_.tx[sfAccount];
+ auto const sle = ctx_.view ().peek (keylet::account (account));
+ auto const dst = ctx_.tx[sfDestination];
+
+ // Create PayChan in ledger
+ auto const slep = std::make_shared (
+ keylet::payChan (account, dst, (*sle)[sfSequence] - 1));
+ // Funds held in this channel
+ (*slep)[sfAmount] = ctx_.tx[sfAmount];
+ // Amount channel has already paid
+ (*slep)[sfBalance] = ctx_.tx[sfAmount].zeroed ();
+ (*slep)[sfAccount] = account;
+ (*slep)[sfDestination] = dst;
+ (*slep)[sfSettleDelay] = ctx_.tx[sfSettleDelay];
+ (*slep)[sfPublicKey] = ctx_.tx[sfPublicKey];
+ (*slep)[~sfCancelAfter] = ctx_.tx[~sfCancelAfter];
+ (*slep)[~sfSourceTag] = ctx_.tx[~sfSourceTag];
+ (*slep)[~sfDestinationTag] = ctx_.tx[~sfDestinationTag];
+
+ ctx_.view ().insert (slep);
+
+ // Add PayChan to owner directory
+ {
+ uint64_t page;
+ auto result = dirAdd (ctx_.view (), page, keylet::ownerDir (account),
+ slep->key (), describeOwnerDir (account),
+ ctx_.app.journal ("View"));
+ if (!isTesSuccess (result.first))
+ return result.first;
+ (*slep)[sfOwnerNode] = page;
+ }
+
+ // Deduct owner's balance, increment owner count
+ (*sle)[sfBalance] = (*sle)[sfBalance] - ctx_.tx[sfAmount];
+ (*sle)[sfOwnerCount] = (*sle)[sfOwnerCount] + 1;
+ ctx_.view ().update (sle);
+
+ return tesSUCCESS;
+}
+
+//------------------------------------------------------------------------------
+
+TER
+PayChanFund::preflight (PreflightContext const& ctx)
+{
+ if (!ctx.rules.enabled (featurePayChan, ctx.app.config ().features))
+ return temDISABLED;
+
+ auto const ret = preflight1 (ctx);
+ if (!isTesSuccess (ret))
+ return ret;
+
+ if (!isXRP (ctx.tx[sfAmount]) || (ctx.tx[sfAmount] <= beast::zero))
+ return temBAD_AMOUNT;
+
+ return preflight2 (ctx);
+}
+
+TER
+PayChanFund::doApply()
+{
+ Keylet const k (ltPAYCHAN, ctx_.tx[sfPayChannel]);
+ auto const slep = ctx_.view ().peek (k);
+ if (!slep)
+ return tecNO_ENTRY;
+
+ AccountID const src = (*slep)[sfAccount];
+ auto const txAccount = ctx_.tx[sfAccount];
+ auto const expiration = (*slep)[~sfExpiration];
+
+ {
+ auto const cancelAfter = (*slep)[~sfCancelAfter];
+ auto const closeTime =
+ ctx_.view ().info ().parentCloseTime.time_since_epoch ().count ();
+ if ((cancelAfter && closeTime >= *cancelAfter) ||
+ (expiration && closeTime >= *expiration))
+ return closeChannel (
+ slep, ctx_.view (), k.key, ctx_.app.journal ("View"));
+ }
+
+ if (src != txAccount)
+ // only the owner can add funds or extend
+ return tecNO_PERMISSION;
+
+ if (auto extend = ctx_.tx[~sfExpiration])
+ {
+ auto minExpiration =
+ ctx_.view ().info ().parentCloseTime.time_since_epoch ().count () +
+ (*slep)[sfSettleDelay];
+ if (expiration && *expiration < minExpiration)
+ minExpiration = *expiration;
+
+ if (*extend < minExpiration)
+ return temBAD_EXPIRATION;
+ (*slep)[~sfExpiration] = *extend;
+ ctx_.view ().update (slep);
+ }
+
+ auto const sle = ctx_.view ().peek (keylet::account (txAccount));
+
+ {
+ // Check reserve and funds availability
+ auto const balance = (*sle)[sfBalance];
+ auto const reserve =
+ ctx_.view ().fees ().accountReserve ((*sle)[sfOwnerCount]);
+
+ if (balance < reserve)
+ return tecINSUFFICIENT_RESERVE;
+
+ if (balance < reserve + ctx_.tx[sfAmount])
+ return tecUNFUNDED;
+ }
+
+ (*slep)[sfAmount] = (*slep)[sfAmount] + ctx_.tx[sfAmount];
+ ctx_.view ().update (slep);
+
+ (*sle)[sfBalance] = (*sle)[sfBalance] - ctx_.tx[sfAmount];
+ ctx_.view ().update (sle);
+
+ return tesSUCCESS;
+}
+
+//------------------------------------------------------------------------------
+
+TER
+PayChanClaim::preflight (PreflightContext const& ctx)
+{
+ if (! ctx.rules.enabled(featurePayChan,
+ ctx.app.config().features))
+ return temDISABLED;
+
+ auto const ret = preflight1 (ctx);
+ if (!isTesSuccess (ret))
+ return ret;
+
+ auto const bal = ctx.tx[~sfBalance];
+ if (bal && (!isXRP (*bal) || *bal <= beast::zero))
+ return temBAD_AMOUNT;
+
+ auto const amt = ctx.tx[~sfAmount];
+ if (amt && (!isXRP (*amt) || *amt <= beast::zero))
+ return temBAD_AMOUNT;
+
+ if (bal && amt && *bal > *amt)
+ return tecNO_PERMISSION;
+
+ auto const flags = ctx.tx.getFlags ();
+ if ((flags & tfClose) && (flags & tfRenew))
+ return temMALFORMED;
+
+ if (auto const sig = ctx.tx[~sfSignature])
+ {
+ if (!(ctx.tx[~sfPublicKey] && bal))
+ return temMALFORMED;
+
+ // Check the signature
+ // The signature isn't needed if txAccount == src, but if it's
+ // present, check it
+
+ auto const reqBalance = bal->xrp ();
+ auto const authAmt = amt ? amt->xrp() : reqBalance;
+
+ if (reqBalance > authAmt)
+ return tecNO_PERMISSION;
+
+ Keylet const k (ltPAYCHAN, ctx.tx[sfPayChannel]);
+ PublicKey const pk (ctx.tx[sfPublicKey]);
+ Serializer msg;
+ serializePayChanAuthorization (msg, k.key, authAmt);
+ if (!verify (pk, msg.slice (), *sig, /*canonical*/ true))
+ return temBAD_SIGNATURE;
+ }
+
+ return preflight2 (ctx);
+}
+
+TER
+PayChanClaim::doApply()
+{
+ Keylet const k (ltPAYCHAN, ctx_.tx[sfPayChannel]);
+ auto const slep = ctx_.view ().peek (k);
+ if (!slep)
+ return tecNO_TARGET;
+
+ AccountID const src = (*slep)[sfAccount];
+ AccountID const dst = (*slep)[sfDestination];
+ AccountID const txAccount = ctx_.tx[sfAccount];
+
+ auto const curExpiration = (*slep)[~sfExpiration];
+ {
+ auto const cancelAfter = (*slep)[~sfCancelAfter];
+ auto const closeTime =
+ ctx_.view ().info ().parentCloseTime.time_since_epoch ().count ();
+ if ((cancelAfter && closeTime >= *cancelAfter) ||
+ (curExpiration && closeTime >= *curExpiration))
+ return closeChannel (
+ slep, ctx_.view (), k.key, ctx_.app.journal ("View"));
+ }
+
+ if (txAccount != src && txAccount != dst)
+ return tecNO_PERMISSION;
+
+ if (ctx_.tx[~sfBalance])
+ {
+ auto const chanBalance = slep->getFieldAmount (sfBalance).xrp ();
+ auto const chanFunds = slep->getFieldAmount (sfAmount).xrp ();
+ auto const reqBalance = ctx_.tx[sfBalance].xrp ();
+
+ if (txAccount == dst && !ctx_.tx[~sfSignature])
+ return temBAD_SIGNATURE;
+
+ if (ctx_.tx[~sfSignature])
+ {
+ PublicKey const pk ((*slep)[sfPublicKey]);
+ if (ctx_.tx[sfPublicKey] != pk)
+ return temBAD_SIGNER;
+ }
+
+ if (reqBalance > chanFunds)
+ return tecUNFUNDED_PAYMENT;
+
+ if (reqBalance <= chanBalance)
+ // nothing requested
+ return tecUNFUNDED_PAYMENT;
+
+ auto const sled = ctx_.view ().peek (keylet::account (dst));
+ if (!sled)
+ return terNO_ACCOUNT;
+
+ if (txAccount == src && ((*sled)[sfFlags] & lsfDisallowXRP))
+ return tecNO_TARGET;
+
+ (*slep)[sfBalance] = ctx_.tx[sfBalance];
+ XRPAmount const reqDelta = reqBalance - chanBalance;
+ assert (reqDelta >= beast::zero);
+ (*sled)[sfBalance] = (*sled)[sfBalance] + reqDelta;
+ ctx_.view ().update (sled);
+ ctx_.view ().update (slep);
+ }
+
+ if (ctx_.tx.getFlags () & tfRenew)
+ {
+ if (src != txAccount)
+ return tecNO_PERMISSION;
+ (*slep)[~sfExpiration] = boost::none;
+ ctx_.view ().update (slep);
+ }
+
+ if (ctx_.tx.getFlags () & tfClose)
+ {
+ // Channel will close immediately if dry or the receiver closes
+ if (dst == txAccount || (*slep)[sfBalance] == (*slep)[sfAmount])
+ return closeChannel (
+ slep, ctx_.view (), k.key, ctx_.app.journal ("View"));
+
+ auto const settleExpiration =
+ ctx_.view ().info ().parentCloseTime.time_since_epoch ().count () +
+ (*slep)[sfSettleDelay];
+
+ if (!curExpiration || *curExpiration > settleExpiration)
+ {
+ (*slep)[~sfExpiration] = settleExpiration;
+ ctx_.view ().update (slep);
+ }
+ }
+
+ return tesSUCCESS;
+}
+
+} // ripple
+
diff --git a/src/ripple/app/tx/impl/PayChan.h b/src/ripple/app/tx/impl/PayChan.h
new file mode 100644
index 000000000..d04687ac3
--- /dev/null
+++ b/src/ripple/app/tx/impl/PayChan.h
@@ -0,0 +1,91 @@
+//------------------------------------------------------------------------------
+/*
+ This file is part of rippled: https://github.com/ripple/rippled
+ Copyright (c) 2012, 2013 Ripple Labs Inc.
+
+ Permission to use, copy, modify, and/or distribute this software for any
+ purpose with or without fee is hereby granted, provided that the above
+ copyright notice and this permission notice appear in all copies.
+
+ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+*/
+//==============================================================================
+
+#ifndef RIPPLE_TX_PAYCHAN_H_INCLUDED
+#define RIPPLE_TX_PAYCHAN_H_INCLUDED
+
+#include
+
+namespace ripple {
+
+class PayChanCreate
+ : public Transactor
+{
+public:
+ explicit
+ PayChanCreate (ApplyContext& ctx)
+ : Transactor(ctx)
+ {
+ }
+
+ static
+ TER
+ preflight (PreflightContext const& ctx);
+
+ static
+ TER
+ preclaim(PreclaimContext const &ctx);
+
+ TER
+ doApply() override;
+};
+
+//------------------------------------------------------------------------------
+
+class PayChanFund
+ : public Transactor
+{
+public:
+ explicit
+ PayChanFund (ApplyContext& ctx)
+ : Transactor(ctx)
+ {
+ }
+
+ static
+ TER
+ preflight (PreflightContext const& ctx);
+
+ TER
+ doApply() override;
+};
+
+//------------------------------------------------------------------------------
+
+class PayChanClaim
+ : public Transactor
+{
+public:
+ explicit
+ PayChanClaim (ApplyContext& ctx)
+ : Transactor(ctx)
+ {
+ }
+
+ static
+ TER
+ preflight (PreflightContext const& ctx);
+
+ TER
+ doApply() override;
+};
+
+} // ripple
+
+#endif
diff --git a/src/ripple/app/tx/impl/applySteps.cpp b/src/ripple/app/tx/impl/applySteps.cpp
index 3885d4cf0..9a1e98d49 100644
--- a/src/ripple/app/tx/impl/applySteps.cpp
+++ b/src/ripple/app/tx/impl/applySteps.cpp
@@ -31,6 +31,7 @@
#include
#include
#include
+#include
namespace ripple {
@@ -54,6 +55,9 @@ invoke_preflight (PreflightContext const& ctx)
case ttTRUST_SET: return SetTrust ::preflight(ctx);
case ttAMENDMENT:
case ttFEE: return Change ::preflight(ctx);
+ case ttPAYCHAN_CREATE: return PayChanCreate ::preflight(ctx);
+ case ttPAYCHAN_FUND: return PayChanFund ::preflight(ctx);
+ case ttPAYCHAN_CLAIM: return PayChanClaim ::preflight(ctx);
default:
assert(false);
return temUNKNOWN;
@@ -124,6 +128,9 @@ invoke_preclaim (PreclaimContext const& ctx)
case ttTRUST_SET: return invoke_preclaim(ctx);
case ttAMENDMENT:
case ttFEE: return invoke_preclaim(ctx);
+ case ttPAYCHAN_CREATE: return invoke_preclaim(ctx);
+ case ttPAYCHAN_FUND: return invoke_preclaim(ctx);
+ case ttPAYCHAN_CLAIM: return invoke_preclaim(ctx);
default:
assert(false);
return { temUNKNOWN, 0 };
@@ -150,6 +157,9 @@ invoke_calculateBaseFee(PreclaimContext const& ctx)
case ttTRUST_SET: return SetTrust::calculateBaseFee(ctx);
case ttAMENDMENT:
case ttFEE: return Change::calculateBaseFee(ctx);
+ case ttPAYCHAN_CREATE: return PayChanCreate::calculateBaseFee(ctx);
+ case ttPAYCHAN_FUND: return PayChanFund::calculateBaseFee(ctx);
+ case ttPAYCHAN_CLAIM: return PayChanClaim::calculateBaseFee(ctx);
default:
assert(false);
return 0;
@@ -187,6 +197,9 @@ invoke_calculateConsequences(STTx const& tx)
case ttTICKET_CANCEL: return invoke_calculateConsequences(tx);
case ttTICKET_CREATE: return invoke_calculateConsequences(tx);
case ttTRUST_SET: return invoke_calculateConsequences(tx);
+ case ttPAYCHAN_CREATE: return invoke_calculateConsequences(tx);
+ case ttPAYCHAN_FUND: return invoke_calculateConsequences(tx);
+ case ttPAYCHAN_CLAIM: return invoke_calculateConsequences(tx);
case ttAMENDMENT:
case ttFEE:
// fall through to default
@@ -217,6 +230,9 @@ invoke_apply (ApplyContext& ctx)
case ttTRUST_SET: { SetTrust p(ctx); return p(); }
case ttAMENDMENT:
case ttFEE: { Change p(ctx); return p(); }
+ case ttPAYCHAN_CREATE: { PayChanCreate p(ctx); return p(); }
+ case ttPAYCHAN_FUND: { PayChanFund p(ctx); return p(); }
+ case ttPAYCHAN_CLAIM: { PayChanClaim p(ctx); return p(); }
default:
assert(false);
return { temUNKNOWN, false };
diff --git a/src/ripple/ledger/impl/ApplyStateTable.cpp b/src/ripple/ledger/impl/ApplyStateTable.cpp
index 9687b672c..ee0b46f86 100644
--- a/src/ripple/ledger/impl/ApplyStateTable.cpp
+++ b/src/ripple/ledger/impl/ApplyStateTable.cpp
@@ -629,6 +629,12 @@ ApplyStateTable::threadOwners (ReadView const& base,
threadTx (base, meta, (*sle)[sfDestination], mods, j);
break;
}
+ case ltPAYCHAN:
+ {
+ threadTx (base, meta, (*sle)[sfAccount], mods, j);
+ threadTx (base, meta, (*sle)[sfDestination], mods, j);
+ break;
+ }
case ltRIPPLE_STATE:
{
threadTx (base, meta, (*sle)[sfLowLimit].getIssuer(), mods, j);
diff --git a/src/ripple/net/impl/RPCCall.cpp b/src/ripple/net/impl/RPCCall.cpp
index 1c872732b..d8be72d76 100644
--- a/src/ripple/net/impl/RPCCall.cpp
+++ b/src/ripple/net/impl/RPCCall.cpp
@@ -583,6 +583,73 @@ private:
return parseAccountRaw2 (jvParams, "peer");
}
+ // account_channels |"" []
+ Json::Value parseAccountChannels (Json::Value const& jvParams)
+ {
+ return parseAccountRaw2 (jvParams, jss::destination_account);
+ }
+
+ // channel_authorize
+ Json::Value parseChannelAuthorize (Json::Value const& jvParams)
+ {
+ Json::Value jvRequest (Json::objectValue);
+
+ jvRequest[jss::secret] = jvParams[0u];
+ {
+ // verify the channel id is a valid 256 bit number
+ uint256 channelId;
+ if (!channelId.SetHexExact (jvParams[1u].asString ()))
+ return rpcError (rpcCHANNEL_MALFORMED);
+ }
+ jvRequest[jss::channel_id] = jvParams[1u].asString ();
+
+ try
+ {
+ auto const drops = std::stoul (jvParams[2u].asString ());
+ (void)drops; // just used for error checking
+ jvRequest[jss::amount] = jvParams[2u];
+ }
+ catch (std::exception const&)
+ {
+ return rpcError (rpcCHANNEL_AMT_MALFORMED);
+ }
+
+ return jvRequest;
+ }
+
+ // channel_verify
+ Json::Value parseChannelVerify (Json::Value const& jvParams)
+ {
+ std::string const strPk = jvParams[0u].asString ();
+
+ if (!parseBase58 (TokenType::TOKEN_ACCOUNT_PUBLIC, strPk))
+ return rpcError (rpcPUBLIC_MALFORMED);
+
+ Json::Value jvRequest (Json::objectValue);
+
+ jvRequest[jss::public_key] = strPk;
+ {
+ // verify the channel id is a valid 256 bit number
+ uint256 channelId;
+ if (!channelId.SetHexExact (jvParams[1u].asString ()))
+ return rpcError (rpcCHANNEL_MALFORMED);
+ }
+ jvRequest[jss::channel_id] = jvParams[1u].asString ();
+ try
+ {
+ auto const drops = std::stoul (jvParams[2u].asString ());
+ (void)drops; // just used for error checking
+ jvRequest[jss::amount] = jvParams[2u];
+ }
+ catch (std::exception const&)
+ {
+ return rpcError (rpcCHANNEL_AMT_MALFORMED);
+ }
+ jvRequest[jss::signature] = jvParams[3u].asString ();
+
+ return jvRequest;
+ }
+
Json::Value parseAccountRaw2 (Json::Value const& jvParams,
char const * const acc2Field)
{
@@ -916,11 +983,14 @@ public:
{ "account_currencies", &RPCParser::parseAccountCurrencies, 1, 2 },
{ "account_info", &RPCParser::parseAccountItems, 1, 2 },
{ "account_lines", &RPCParser::parseAccountLines, 1, 5 },
+ { "account_channels", &RPCParser::parseAccountChannels, 1, 3 },
{ "account_objects", &RPCParser::parseAccountItems, 1, 5 },
{ "account_offers", &RPCParser::parseAccountItems, 1, 4 },
{ "account_tx", &RPCParser::parseAccountTransactions, 1, 8 },
{ "book_offers", &RPCParser::parseBookOffers, 2, 7 },
{ "can_delete", &RPCParser::parseCanDelete, 0, 1 },
+ { "channel_authorize", &RPCParser::parseChannelAuthorize, 3, 3 },
+ { "channel_verify", &RPCParser::parseChannelVerify, 4, 4 },
{ "connect", &RPCParser::parseConnect, 1, 2 },
{ "consensus_info", &RPCParser::parseAsIs, 0, 0 },
{ "feature", &RPCParser::parseFeature, 0, 2 },
diff --git a/src/ripple/protocol/ErrorCodes.h b/src/ripple/protocol/ErrorCodes.h
index 0034cb81b..e9e31f3ac 100644
--- a/src/ripple/protocol/ErrorCodes.h
+++ b/src/ripple/protocol/ErrorCodes.h
@@ -87,6 +87,8 @@ enum error_code_i
rpcBAD_MARKET,
rpcBAD_SECRET,
rpcBAD_SEED,
+ rpcCHANNEL_MALFORMED,
+ rpcCHANNEL_AMT_MALFORMED,
rpcCOMMAND_MISSING,
rpcDST_ACT_MALFORMED,
rpcDST_ACT_MISSING,
diff --git a/src/ripple/protocol/Feature.h b/src/ripple/protocol/Feature.h
index 33ec8ef5b..607fe1f1c 100644
--- a/src/ripple/protocol/Feature.h
+++ b/src/ripple/protocol/Feature.h
@@ -43,6 +43,7 @@ extern uint256 const featureFlowV2;
extern uint256 const featureOwnerPaysFee;
extern uint256 const featureCompareFlowV1V2;
extern uint256 const featureSHAMapV2;
+extern uint256 const featurePayChan;
} // ripple
diff --git a/src/ripple/protocol/HashPrefix.h b/src/ripple/protocol/HashPrefix.h
index 814256d99..0e0c8f6d7 100644
--- a/src/ripple/protocol/HashPrefix.h
+++ b/src/ripple/protocol/HashPrefix.h
@@ -99,6 +99,9 @@ public:
/** Manifest */
static HashPrefix const manifest;
+
+ /** Payment Channel Claim */
+ static HashPrefix const paymentChannelClaim;
};
template
diff --git a/src/ripple/protocol/Indexes.h b/src/ripple/protocol/Indexes.h
index b1dd42b9a..b82d9850f 100644
--- a/src/ripple/protocol/Indexes.h
+++ b/src/ripple/protocol/Indexes.h
@@ -238,6 +238,10 @@ Keylet page (uint256 const& key)
Keylet
susPay (AccountID const& source, std::uint32_t seq);
+/** A PaymentChannel */
+Keylet
+payChan (AccountID const& source, AccountID const& dst, std::uint32_t seq);
+
} // keylet
}
diff --git a/src/ripple/protocol/JsonFields.h b/src/ripple/protocol/JsonFields.h
index 9aa4c5763..cde457a0f 100644
--- a/src/ripple/protocol/JsonFields.h
+++ b/src/ripple/protocol/JsonFields.h
@@ -50,6 +50,7 @@ JSS ( Paths ); // in/out: TransactionSign
JSS ( TransferRate ); // in: TransferRate
JSS ( historical_perminute ); // historical_perminute
JSS ( SLE_hit_rate ); // out: GetCounts
+JSS ( SettleDelay ); // in: TransactionSign
JSS ( SendMax ); // in: TransactionSign
JSS ( Sequence ); // in/out: TransactionSign; field.
JSS ( SetFlag ); // field.
@@ -80,6 +81,7 @@ JSS ( age ); // out: NetworkOPs, Peers
JSS ( alternatives ); // out: PathRequest, RipplePathFind
JSS ( amendment_blocked ); // out: NetworkOPs
JSS ( amendments ); // in: AccountObjects, out: NetworkOPs
+JSS ( amount ); // out: AccountChannels
JSS ( asks ); // out: Subscribe
JSS ( assets ); // out: GatewayBalances
JSS ( authorized ); // out: AccountLines
@@ -98,7 +100,10 @@ JSS ( both ); // in: Subscribe, Unsubscribe
JSS ( both_sides ); // in: Subscribe, Unsubscribe
JSS ( build_path ); // in: TransactionSign
JSS ( build_version ); // out: NetworkOPs
+JSS ( cancel_after ); // out: AccountChannels
JSS ( can_delete ); // out: CanDelete
+JSS ( channel_id ); // out: AccountChannels
+JSS ( channels ); // out: AccountChannels
JSS ( check_nodes ); // in: LedgerCleaner
JSS ( clear ); // in/out: FetchInfo
JSS ( close_flags ); // out: LedgerToJson
@@ -135,10 +140,12 @@ JSS ( debug_signing ); // in: TransactionSign
JSS ( delivered_amount ); // out: addPaymentDeliveredAmount
JSS ( deprecated ); // out: WalletSeed
JSS ( descending ); // in: AccountTx*
-JSS ( destination_account ); // in: PathRequest, RipplePathFind
+JSS ( destination_account ); // in: PathRequest, RipplePathFind, account_lines
+ // out: AccountChannels
JSS ( destination_amount ); // in: PathRequest, RipplePathFind
JSS ( destination_currencies ); // in: PathRequest, RipplePathFind
JSS ( destination_tag ); // in: PathRequest
+ // out: AccountChannels
JSS ( dir_entry ); // out: DirectoryEntryIterator
JSS ( dir_index ); // out: DirectoryEntryIterator
JSS ( dir_root ); // out: DirectoryEntryIterator
@@ -156,7 +163,7 @@ JSS ( error_exception ); // out: Submit
JSS ( error_message ); // out: error
JSS ( expand ); // in: handler/Ledger
JSS ( expected_ledger_size ); // out: TxQ
-JSS ( expiration ); // out: AccountOffers
+JSS ( expiration ); // out: AccountOffers, AccountChannels
JSS ( fail_hard ); // in: Sign, Submit
JSS ( failed ); // out: InboundLedger
JSS ( feature ); // in: Feature
@@ -347,7 +354,8 @@ JSS ( rt_accounts ); // in: Subscribe, Unsubscribe
JSS ( sanity ); // out: PeerImp
JSS ( search_depth ); // in: RipplePathFind
JSS ( secret ); // in: TransactionSign, WalletSeed,
- // ValidationCreate, ValidationSeed
+ // ValidationCreate, ValidationSeed,
+ // channel_authorize
JSS ( seed ); // in: WalletAccounts, out: WalletSeed
JSS ( seed_hex ); // in: WalletPropose, TransactionSign
JSS ( send_currencies ); // out: AccountCurrencies
@@ -357,8 +365,10 @@ JSS ( seq ); // in: LedgerEntry;
JSS ( seqNum ); // out: LedgerToJson
JSS ( server_state ); // out: NetworkOPs
JSS ( server_status ); // out: NetworkOPs
+JSS ( settle_delay ); // out: AccountChannels
JSS ( severity ); // in: LogLevel
-JSS ( signature ); // out: NetworkOPs
+JSS ( signature ); // out: NetworkOPs, ChannelAuthorize
+JSS ( signature_verified ); // out: ChannelVerify
JSS ( signing_key ); // out: NetworkOPs
JSS ( signing_time ); // out: NetworkOPs
JSS ( signer_list ); // in: AccountObjects
@@ -367,6 +377,7 @@ JSS ( snapshot ); // in: Subscribe
JSS ( source_account ); // in: PathRequest, RipplePathFind
JSS ( source_amount ); // in: PathRequest, RipplePathFind
JSS ( source_currencies ); // in: PathRequest, RipplePathFind
+JSS ( source_tag ); // out: AccountChannels
JSS ( stand_alone ); // out: NetworkOPs
JSS ( start ); // in: TxHistory
JSS ( state ); // out: Logic.h, ServerState, LedgerData
diff --git a/src/ripple/protocol/LedgerFormats.h b/src/ripple/protocol/LedgerFormats.h
index 49e57e612..856c90ad1 100644
--- a/src/ripple/protocol/LedgerFormats.h
+++ b/src/ripple/protocol/LedgerFormats.h
@@ -80,6 +80,9 @@ enum LedgerEntryType
ltSUSPAY = 'u',
+ // Simple unidirection xrp channel
+ ltPAYCHAN = 'x',
+
// No longer used or supported. Left here to prevent accidental
// reassignment of the ledger type.
ltNICKNAME = 'n',
@@ -107,6 +110,7 @@ enum LedgerNameSpace
spaceFee = 'e',
spaceTicket = 'T',
spaceSignerList = 'S',
+ spaceXRPUChannel = 'x',
// No longer used or supported. Left here to reserve the space and
// avoid accidental reuse of the space.
diff --git a/src/ripple/protocol/PayChan.h b/src/ripple/protocol/PayChan.h
new file mode 100644
index 000000000..90352b8a8
--- /dev/null
+++ b/src/ripple/protocol/PayChan.h
@@ -0,0 +1,44 @@
+//------------------------------------------------------------------------------
+/*
+ This file is part of rippled: https://github.com/ripple/rippled
+ Copyright (c) 2012, 2013 Ripple Labs Inc.
+
+ Permission to use, copy, modify, and/or distribute this software for any
+ purpose with or without fee is hereby granted, provided that the above
+ copyright notice and this permission notice appear in all copies.
+
+ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+*/
+//==============================================================================
+
+#ifndef RIPPLE_PROTOCOL_PAYCHAN_H_INCLUDED
+#define RIPPLE_PROTOCOL_PAYCHAN_H_INCLUDED
+
+#include
+#include
+#include
+#include
+
+namespace ripple {
+
+inline
+void
+serializePayChanAuthorization (
+ Serializer& msg,
+ uint256 const& key,
+ XRPAmount const& amt)
+{
+ msg.add32 (HashPrefix::paymentChannelClaim);
+ msg.add256 (key);
+ msg.add64 (amt.drops ());
+}
+
+} // ripple
+
+#endif
diff --git a/src/ripple/protocol/SField.h b/src/ripple/protocol/SField.h
index c31c1fb9b..802d7cd67 100644
--- a/src/ripple/protocol/SField.h
+++ b/src/ripple/protocol/SField.h
@@ -375,6 +375,7 @@ extern SF_U32 const sfSignerQuorum;
extern SF_U32 const sfCancelAfter;
extern SF_U32 const sfFinishAfter;
extern SF_U32 const sfSignerListID;
+extern SF_U32 const sfSettleDelay;
// 64-bit integers
extern SF_U64 const sfIndexNext;
@@ -413,6 +414,7 @@ extern SF_U256 const sfNickname;
extern SF_U256 const sfAmendment;
extern SF_U256 const sfTicketID;
extern SF_U256 const sfDigest;
+extern SF_U256 const sfPayChannel;
// currency amount (common)
extern SF_Amount const sfAmount;
diff --git a/src/ripple/protocol/TxFlags.h b/src/ripple/protocol/TxFlags.h
index bd67b15ac..d2b2eeaf7 100644
--- a/src/ripple/protocol/TxFlags.h
+++ b/src/ripple/protocol/TxFlags.h
@@ -95,6 +95,10 @@ const std::uint32_t tfTrustSetMask = ~ (tfUniversal | tfSetfAuth | tfSet
const std::uint32_t tfGotMajority = 0x00010000;
const std::uint32_t tfLostMajority = 0x00020000;
+// PaymentChannel flags:
+const std::uint32_t tfRenew = 0x00010000;
+const std::uint32_t tfClose = 0x00020000;
+
} // ripple
#endif
diff --git a/src/ripple/protocol/TxFormats.h b/src/ripple/protocol/TxFormats.h
index 74dccc0f4..36b23c049 100644
--- a/src/ripple/protocol/TxFormats.h
+++ b/src/ripple/protocol/TxFormats.h
@@ -47,6 +47,9 @@ enum TxType
ttTICKET_CREATE = 10,
ttTICKET_CANCEL = 11,
ttSIGNER_LIST_SET = 12,
+ ttPAYCHAN_CREATE = 13,
+ ttPAYCHAN_FUND = 14,
+ ttPAYCHAN_CLAIM = 15,
ttTRUST_SET = 20,
diff --git a/src/ripple/protocol/impl/ErrorCodes.cpp b/src/ripple/protocol/impl/ErrorCodes.cpp
index aebab6f0e..4a9253872 100644
--- a/src/ripple/protocol/impl/ErrorCodes.cpp
+++ b/src/ripple/protocol/impl/ErrorCodes.cpp
@@ -61,6 +61,8 @@ public:
add (rpcBAD_SECRET, "badSecret", "Secret does not match account.");
add (rpcBAD_SEED, "badSeed", "Disallowed seed.");
add (rpcBAD_SYNTAX, "badSyntax", "Syntax error.");
+ add (rpcCHANNEL_MALFORMED, "channelMalformed", "Payment channel is malformed.");
+ add (rpcCHANNEL_AMT_MALFORMED, "channelAmtMalformed","Payment channel amount is malformed.");
add (rpcCOMMAND_MISSING, "commandMissing", "Missing command entry.");
add (rpcDST_ACT_MALFORMED, "dstActMalformed", "Destination account is malformed.");
add (rpcDST_ACT_MISSING, "dstActMissing", "Destination account does not exist.");
diff --git a/src/ripple/protocol/impl/Feature.cpp b/src/ripple/protocol/impl/Feature.cpp
index b43e9edf6..b2e5d5f18 100644
--- a/src/ripple/protocol/impl/Feature.cpp
+++ b/src/ripple/protocol/impl/Feature.cpp
@@ -54,5 +54,6 @@ uint256 const featureFlowV2 = feature("FlowV2");
uint256 const featureOwnerPaysFee = feature("OwnerPaysFee");
uint256 const featureCompareFlowV1V2 = feature("CompareFlowV1V2");
uint256 const featureSHAMapV2 = feature("SHAMapV2");
+uint256 const featurePayChan = feature("PayChan");
} // ripple
diff --git a/src/ripple/protocol/impl/HashPrefix.cpp b/src/ripple/protocol/impl/HashPrefix.cpp
index e4c42fff1..2f33bda0c 100644
--- a/src/ripple/protocol/impl/HashPrefix.cpp
+++ b/src/ripple/protocol/impl/HashPrefix.cpp
@@ -36,5 +36,6 @@ HashPrefix const HashPrefix::txMultiSign ('S', 'M', 'T');
HashPrefix const HashPrefix::validation ('V', 'A', 'L');
HashPrefix const HashPrefix::proposal ('P', 'R', 'P');
HashPrefix const HashPrefix::manifest ('M', 'A', 'N');
+HashPrefix const HashPrefix::paymentChannelClaim ('C', 'L', 'M');
} // ripple
diff --git a/src/ripple/protocol/impl/Indexes.cpp b/src/ripple/protocol/impl/Indexes.cpp
index a10071c97..169d6e578 100644
--- a/src/ripple/protocol/impl/Indexes.cpp
+++ b/src/ripple/protocol/impl/Indexes.cpp
@@ -323,6 +323,18 @@ susPay (AccountID const& source, std::uint32_t seq)
return { ltSUSPAY, static_cast(h) };
}
+Keylet
+payChan (AccountID const& source, AccountID const& dst, std::uint32_t seq)
+{
+ sha512_half_hasher h;
+ using beast::hash_append;
+ hash_append(h, spaceXRPUChannel);
+ hash_append(h, source);
+ hash_append(h, dst);
+ hash_append(h, seq);
+ return { ltPAYCHAN, static_cast(h) };
+}
+
} // keylet
} // ripple
diff --git a/src/ripple/protocol/impl/LedgerFormats.cpp b/src/ripple/protocol/impl/LedgerFormats.cpp
index 9a1e65b27..062e40ac1 100644
--- a/src/ripple/protocol/impl/LedgerFormats.cpp
+++ b/src/ripple/protocol/impl/LedgerFormats.cpp
@@ -130,6 +130,22 @@ LedgerFormats::LedgerFormats ()
<< SOElement (sfPreviousTxnID, SOE_REQUIRED)
<< SOElement (sfPreviousTxnLgrSeq, SOE_REQUIRED)
;
+
+ add ("PayChannel", ltPAYCHAN)
+ << SOElement (sfAccount, SOE_REQUIRED)
+ << SOElement (sfDestination, SOE_REQUIRED)
+ << SOElement (sfAmount, SOE_REQUIRED)
+ << SOElement (sfBalance, SOE_REQUIRED)
+ << SOElement (sfPublicKey, SOE_REQUIRED)
+ << SOElement (sfSettleDelay, SOE_REQUIRED)
+ << SOElement (sfExpiration, SOE_OPTIONAL)
+ << SOElement (sfCancelAfter, SOE_OPTIONAL)
+ << SOElement (sfSourceTag, SOE_OPTIONAL)
+ << SOElement (sfDestinationTag, SOE_OPTIONAL)
+ << SOElement (sfOwnerNode, SOE_REQUIRED)
+ << SOElement (sfPreviousTxnID, SOE_REQUIRED)
+ << SOElement (sfPreviousTxnLgrSeq, SOE_REQUIRED)
+ ;
}
void LedgerFormats::addCommonFields (Item& item)
diff --git a/src/ripple/protocol/impl/SField.cpp b/src/ripple/protocol/impl/SField.cpp
index 44a809c54..2983da0ca 100644
--- a/src/ripple/protocol/impl/SField.cpp
+++ b/src/ripple/protocol/impl/SField.cpp
@@ -126,6 +126,7 @@ SF_U32 const sfSignerQuorum = make::one(&sfSignerQuorum,
SF_U32 const sfCancelAfter = make::one(&sfCancelAfter, STI_UINT32, 36, "CancelAfter");
SF_U32 const sfFinishAfter = make::one(&sfFinishAfter, STI_UINT32, 37, "FinishAfter");
SF_U32 const sfSignerListID = make::one(&sfSignerListID, STI_UINT32, 38, "SignerListID");
+SF_U32 const sfSettleDelay = make::one(&sfSettleDelay, STI_UINT32, 39, "SettleDelay");
// 64-bit integers
SF_U64 const sfIndexNext = make::one(&sfIndexNext, STI_UINT64, 1, "IndexNext");
@@ -164,6 +165,7 @@ SF_U256 const sfNickname = make::one(&sfNickname, STI_H
SF_U256 const sfAmendment = make::one(&sfAmendment, STI_HASH256, 19, "Amendment");
SF_U256 const sfTicketID = make::one(&sfTicketID, STI_HASH256, 20, "TicketID");
SF_U256 const sfDigest = make::one(&sfDigest, STI_HASH256, 21, "Digest");
+SF_U256 const sfPayChannel = make::one(&sfPayChannel, STI_HASH256, 22, "Channel");
// currency amount (common)
SF_Amount const sfAmount = make::one(&sfAmount, STI_AMOUNT, 1, "Amount");
diff --git a/src/ripple/protocol/impl/TxFormats.cpp b/src/ripple/protocol/impl/TxFormats.cpp
index 85e213b83..2263d660e 100644
--- a/src/ripple/protocol/impl/TxFormats.cpp
+++ b/src/ripple/protocol/impl/TxFormats.cpp
@@ -113,6 +113,26 @@ TxFormats::TxFormats ()
<< SOElement (sfSignerQuorum, SOE_REQUIRED)
<< SOElement (sfSignerEntries, SOE_OPTIONAL)
;
+
+ add ("PaymentChannelCreate", ttPAYCHAN_CREATE) <<
+ SOElement (sfDestination, SOE_REQUIRED) <<
+ SOElement (sfAmount, SOE_REQUIRED) <<
+ SOElement (sfSettleDelay, SOE_REQUIRED) <<
+ SOElement (sfPublicKey, SOE_REQUIRED) <<
+ SOElement (sfCancelAfter, SOE_OPTIONAL) <<
+ SOElement (sfDestinationTag, SOE_OPTIONAL);
+
+ add ("PaymentChannelFund", ttPAYCHAN_FUND) <<
+ SOElement (sfPayChannel, SOE_REQUIRED) <<
+ SOElement (sfAmount, SOE_REQUIRED) <<
+ SOElement (sfExpiration, SOE_OPTIONAL);
+
+ add ("PaymentChannelClaim", ttPAYCHAN_CLAIM) <<
+ SOElement (sfPayChannel, SOE_REQUIRED) <<
+ SOElement (sfAmount, SOE_OPTIONAL) <<
+ SOElement (sfBalance, SOE_OPTIONAL) <<
+ SOElement (sfSignature, SOE_OPTIONAL) <<
+ SOElement (sfPublicKey, SOE_OPTIONAL);
}
void TxFormats::addCommonFields (Item& item)
diff --git a/src/ripple/rpc/handlers/AccountChannels.cpp b/src/ripple/rpc/handlers/AccountChannels.cpp
new file mode 100644
index 000000000..6a82aa00f
--- /dev/null
+++ b/src/ripple/rpc/handlers/AccountChannels.cpp
@@ -0,0 +1,184 @@
+//------------------------------------------------------------------------------
+/*
+ This file is part of rippled: https://github.com/ripple/rippled
+ Copyright (c) 2012-2014 Ripple Labs Inc.
+
+ Permission to use, copy, modify, and/or distribute this software for any
+ purpose with or without fee is hereby granted, provided that the above
+ copyright notice and this permission notice appear in all copies.
+
+ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+*/
+//==============================================================================
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+namespace ripple {
+
+void addChannel (Json::Value& jsonLines, SLE const& line)
+{
+ Json::Value& jDst (jsonLines.append (Json::objectValue));
+ jDst[jss::channel_id] = to_string (line.key ());
+ jDst[jss::account] = to_string (line[sfAccount]);
+ jDst[jss::destination_account] = to_string (line[sfDestination]);
+ jDst[jss::amount] = line[sfAmount].getText ();
+ jDst[jss::balance] = line[sfBalance].getText ();
+ PublicKey const pk (line[sfPublicKey]);
+ jDst[jss::public_key] = toBase58 (TokenType::TOKEN_ACCOUNT_PUBLIC, pk);
+ jDst[jss::public_key_hex] = strHex (pk);
+ jDst[jss::settle_delay] = line[sfSettleDelay];
+ if (auto const& v = line[~sfExpiration])
+ jDst[jss::expiration] = *v;
+ if (auto const& v = line[~sfCancelAfter])
+ jDst[jss::cancel_after] = *v;
+ if (auto const& v = line[~sfSourceTag])
+ jDst[jss::source_tag] = *v;
+ if (auto const& v = line[~sfDestinationTag])
+ jDst[jss::destination_tag] = *v;
+}
+
+// {
+// account: |
+// ledger_hash :
+// ledger_index :
+// limit: integer // optional
+// marker: opaque // optional, resume previous query
+// }
+Json::Value doAccountChannels (RPC::Context& context)
+{
+ auto const& params (context.params);
+ if (! params.isMember (jss::account))
+ return RPC::missing_field_error (jss::account);
+
+ std::shared_ptr ledger;
+ auto result = RPC::lookupLedger (ledger, context);
+ if (! ledger)
+ return result;
+
+ std::string strIdent (params[jss::account].asString ());
+ AccountID accountID;
+
+ result = RPC::accountFromString (accountID, strIdent);
+ if (result)
+ return result;
+
+ if (! ledger->exists(keylet::account (accountID)))
+ return rpcError (rpcACT_NOT_FOUND);
+
+ std::string strDst;
+ if (params.isMember (jss::destination_account))
+ strDst = params[jss::destination_account].asString ();
+ auto hasDst = ! strDst.empty ();
+
+ AccountID raDstAccount;
+ if (hasDst)
+ {
+ result = RPC::accountFromString (raDstAccount, strDst);
+ if (result)
+ return result;
+ }
+
+ unsigned int limit;
+ if (auto err = readLimitField(limit, RPC::Tuning::accountChannels, context))
+ return *err;
+
+ Json::Value jsonChannels{Json::arrayValue};
+ struct VisitData
+ {
+ std::vector > items;
+ AccountID const& accountID;
+ bool hasDst;
+ AccountID const& raDstAccount;
+ };
+ VisitData visitData = {{}, accountID, hasDst, raDstAccount};
+ unsigned int reserve (limit);
+ uint256 startAfter;
+ std::uint64_t startHint;
+
+ if (params.isMember (jss::marker))
+ {
+ // We have a start point. Use limit - 1 from the result and use the
+ // very last one for the resume.
+ Json::Value const& marker (params[jss::marker]);
+
+ if (! marker.isString ())
+ return RPC::expected_field_error (jss::marker, "string");
+
+ startAfter.SetHex (marker.asString ());
+ auto const sleChannel = ledger->read({ltPAYCHAN, startAfter});
+
+ if (! sleChannel)
+ return rpcError (rpcINVALID_PARAMS);
+
+ if (sleChannel->getFieldAmount (sfLowLimit).getIssuer () == accountID)
+ startHint = sleChannel->getFieldU64 (sfLowNode);
+ else if (sleChannel->getFieldAmount (sfHighLimit).getIssuer () == accountID)
+ startHint = sleChannel->getFieldU64 (sfHighNode);
+ else
+ return rpcError (rpcINVALID_PARAMS);
+
+ addChannel (jsonChannels, *sleChannel);
+ visitData.items.reserve (reserve);
+ }
+ else
+ {
+ startHint = 0;
+ // We have no start point, limit should be one higher than requested.
+ visitData.items.reserve (++reserve);
+ }
+
+ if (! forEachItemAfter(*ledger, accountID,
+ startAfter, startHint, reserve,
+ [&visitData](std::shared_ptr const& sleCur)
+ {
+
+ if (sleCur && sleCur->getType () == ltPAYCHAN &&
+ (! visitData.hasDst ||
+ visitData.raDstAccount == (*sleCur)[sfDestination]))
+ {
+ visitData.items.emplace_back (sleCur);
+ return true;
+ }
+
+ return false;
+ }))
+ {
+ return rpcError (rpcINVALID_PARAMS);
+ }
+
+ if (visitData.items.size () == reserve)
+ {
+ result[jss::limit] = limit;
+
+ result[jss::marker] = to_string (visitData.items.back()->getIndex());
+ visitData.items.pop_back ();
+ }
+
+ result[jss::account] = context.app.accountIDCache().toBase58 (accountID);
+
+ for (auto const& item : visitData.items)
+ addChannel (jsonChannels, *item);
+
+ context.loadType = Resource::feeMediumBurdenRPC;
+ result[jss::channels] = std::move(jsonChannels);
+ return result;
+}
+
+} // ripple
diff --git a/src/ripple/rpc/handlers/Handlers.h b/src/ripple/rpc/handlers/Handlers.h
index edb2ee06e..d2b932da5 100644
--- a/src/ripple/rpc/handlers/Handlers.h
+++ b/src/ripple/rpc/handlers/Handlers.h
@@ -27,6 +27,7 @@ namespace ripple {
Json::Value doAccountCurrencies (RPC::Context&);
Json::Value doAccountInfo (RPC::Context&);
Json::Value doAccountLines (RPC::Context&);
+Json::Value doAccountChannels (RPC::Context&);
Json::Value doAccountObjects (RPC::Context&);
Json::Value doAccountOffers (RPC::Context&);
Json::Value doAccountTx (RPC::Context&);
@@ -35,6 +36,8 @@ Json::Value doAccountTxOld (RPC::Context&);
Json::Value doBookOffers (RPC::Context&);
Json::Value doBlackList (RPC::Context&);
Json::Value doCanDelete (RPC::Context&);
+Json::Value doChannelAuthorize (RPC::Context&);
+Json::Value doChannelVerify (RPC::Context&);
Json::Value doConnect (RPC::Context&);
Json::Value doConsensusInfo (RPC::Context&);
Json::Value doFeature (RPC::Context&);
diff --git a/src/ripple/rpc/handlers/PayChanClaim.cpp b/src/ripple/rpc/handlers/PayChanClaim.cpp
new file mode 100644
index 000000000..01a80af0a
--- /dev/null
+++ b/src/ripple/rpc/handlers/PayChanClaim.cpp
@@ -0,0 +1,130 @@
+//------------------------------------------------------------------------------
+/*
+ This file is part of rippled: https://github.com/ripple/rippled
+ Copyright (c) 2012-2014 Ripple Labs Inc.
+
+ Permission to use, copy, modify, and/or distribute this software for any
+ purpose with or without fee is hereby granted, provided that the above
+ copyright notice and this permission notice appear in all copies.
+
+ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+*/
+//==============================================================================
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+namespace ripple {
+
+// {
+// secret_key:
+// channel_id: 256-bit channel id
+// drops: 64-bit uint (as string)
+// }
+Json::Value doChannelAuthorize (RPC::Context& context)
+{
+ auto const& params (context.params);
+ for (auto const& p : {jss::secret, jss::channel_id, jss::amount})
+ if (!params.isMember (p))
+ return RPC::missing_field_error (p);
+
+ Json::Value result;
+ auto const keypair = RPC::keypairForSignature (params, result);
+ if (RPC::contains_error (result))
+ return result;
+
+ uint256 channelId;
+ if (!channelId.SetHexExact (params[jss::channel_id].asString ()))
+ return rpcError (rpcCHANNEL_MALFORMED);
+
+ std::uint64_t drops = 0;
+ try
+ {
+ drops = std::stoul (params[jss::amount].asString ());
+ }
+ catch (std::exception const&)
+ {
+ return rpcError (rpcCHANNEL_AMT_MALFORMED);
+ }
+
+ Serializer msg;
+ serializePayChanAuthorization (msg, channelId, XRPAmount (drops));
+
+ try
+ {
+ auto const buf = sign (keypair.first, keypair.second, msg.slice ());
+ result[jss::signature] = strHex (buf);
+ }
+ catch (std::exception&)
+ {
+ result =
+ RPC::make_error (rpcINTERNAL, "Exception occurred during signing.");
+ }
+ return result;
+}
+
+// {
+// public_key:
+// channel_id: 256-bit channel id
+// drops: 64-bit uint (as string)
+// signature: signature to verify
+// }
+Json::Value doChannelVerify (RPC::Context& context)
+{
+ auto const& params (context.params);
+ for (auto const& p :
+ {jss::public_key, jss::channel_id, jss::amount, jss::signature})
+ if (!params.isMember (p))
+ return RPC::missing_field_error (p);
+
+ std::string const strPk = params[jss::public_key].asString ();
+ auto const pk =
+ parseBase58 (TokenType::TOKEN_ACCOUNT_PUBLIC, strPk);
+ if (!pk)
+ return rpcError (rpcPUBLIC_MALFORMED);
+
+ uint256 channelId;
+ if (!channelId.SetHexExact (params[jss::channel_id].asString ()))
+ return rpcError (rpcCHANNEL_MALFORMED);
+
+ std::uint64_t drops = 0;
+ try
+ {
+ drops = std::stoul (params[jss::amount].asString ());
+ }
+ catch (std::exception const&)
+ {
+ return rpcError (rpcCHANNEL_AMT_MALFORMED);
+ }
+
+ std::pair sig(strUnHex (params[jss::signature].asString ()));
+ if (!sig.second || !sig.first.size ())
+ return rpcError (rpcINVALID_PARAMS);
+
+ Serializer msg;
+ serializePayChanAuthorization (msg, channelId, XRPAmount (drops));
+
+ Json::Value result;
+ result[jss::signature_verified] =
+ verify (*pk, msg.slice (), makeSlice (sig.first), /*canonical*/ true);
+ return result;
+}
+
+} // ripple
diff --git a/src/ripple/rpc/impl/Handler.cpp b/src/ripple/rpc/impl/Handler.cpp
index f0ddeda4d..e4e32ee94 100644
--- a/src/ripple/rpc/impl/Handler.cpp
+++ b/src/ripple/rpc/impl/Handler.cpp
@@ -104,12 +104,15 @@ Handler handlerArray[] {
{ "account_info", byRef (&doAccountInfo), Role::USER, NO_CONDITION },
{ "account_currencies", byRef (&doAccountCurrencies), Role::USER, NO_CONDITION },
{ "account_lines", byRef (&doAccountLines), Role::USER, NO_CONDITION },
+ { "account_channels", byRef (&doAccountChannels), Role::USER, NO_CONDITION },
{ "account_objects", byRef (&doAccountObjects), Role::USER, NO_CONDITION },
{ "account_offers", byRef (&doAccountOffers), Role::USER, NO_CONDITION },
{ "account_tx", byRef (&doAccountTxSwitch), Role::USER, NO_CONDITION },
{ "blacklist", byRef (&doBlackList), Role::ADMIN, NO_CONDITION },
{ "book_offers", byRef (&doBookOffers), Role::USER, NO_CONDITION },
{ "can_delete", byRef (&doCanDelete), Role::ADMIN, NO_CONDITION },
+ { "channel_authorize", byRef (&doChannelAuthorize), Role::USER, NO_CONDITION },
+ { "channel_verify", byRef (&doChannelVerify), Role::USER, NO_CONDITION },
{ "connect", byRef (&doConnect), Role::ADMIN, NO_CONDITION },
{ "consensus_info", byRef (&doConsensusInfo), Role::ADMIN, NO_CONDITION },
{ "gateway_balances", byRef (&doGatewayBalances), Role::USER, NO_CONDITION },
diff --git a/src/ripple/rpc/impl/Tuning.h b/src/ripple/rpc/impl/Tuning.h
index 14693e8e9..75f658aec 100644
--- a/src/ripple/rpc/impl/Tuning.h
+++ b/src/ripple/rpc/impl/Tuning.h
@@ -35,6 +35,9 @@ struct LimitRange {
/** Limits for the account_lines command. */
static LimitRange const accountLines = {10, 200, 400};
+/** Limits for the account_channels command. */
+static LimitRange const accountChannels = {10, 200, 400};
+
/** Limits for the account_objects command. */
static LimitRange const accountObjects = {10, 200, 400};
diff --git a/src/ripple/unity/app_tests.cpp b/src/ripple/unity/app_tests.cpp
index 5439e4a19..8bdd2097e 100644
--- a/src/ripple/unity/app_tests.cpp
+++ b/src/ripple/unity/app_tests.cpp
@@ -30,6 +30,7 @@
#include
#include
#include
+#include
#include
#include
#include
diff --git a/src/ripple/unity/app_tx.cpp b/src/ripple/unity/app_tx.cpp
index 8e6bb61c8..e84af3d91 100644
--- a/src/ripple/unity/app_tx.cpp
+++ b/src/ripple/unity/app_tx.cpp
@@ -29,6 +29,7 @@
#include
#include
#include
+#include
#include
#include
#include
diff --git a/src/ripple/unity/rpcx.cpp b/src/ripple/unity/rpcx.cpp
index 401848f4a..acd466910 100644
--- a/src/ripple/unity/rpcx.cpp
+++ b/src/ripple/unity/rpcx.cpp
@@ -33,6 +33,7 @@
#include
#include
#include
+#include
#include
#include
#include
@@ -62,6 +63,7 @@
#include
#include
#include
+#include
#include
#include
#include