Home

Awesome

ilp-plugin-payment-channel-framework npm circle codecov

ILP virtual ledger plugin for directly transacting connectors, including a framework for attaching on-ledger settlement mechanisms.

Installation

npm install --save ilp-plugin-payment-channel-framework

Usage in ILP Kit

This section explains how to use ilp-plugin-payment-channel-framework for an asymmetric trustline in ILP Kit.

To setup an asymmetric trustline server, add the following to ILP Kit's configuration file (Note that all of the following configuration should go into a single line in your config file):

CONNECTOR_LEDGERS={
  "g.us.usd.myledger": {
      // your five-bells-ledger goes here
    }
  },
  "g.eur.mytrustline.": {
    "currency": "EUR",
    "plugin": "ilp-plugin-payment-channel-framework",
    "options": {
      // listen for incoming connections
      "listener": {
        port: 1234,

        // if a certificate is provided, the server will listen using TLS
        // instead of plain websockets
        cert: '/tmp/snakeoil.crt',
        key: '/tmp/snakeoil.key',
        ca: '/tmp/snakeoil-ca.crt'
      },
      "incomingSecret": "shared_secret", // auth_token which the server expects the client to send
      // the server determines the properties of the trustline
      "maxBalance": "1000000000",
      "prefix": "g.eur.mytrustline.",
      "info": {
      	"currencyScale": 9,
      	"currencyCode": "EUR",
      	"prefix": "g.eur.mytrustline.",
      	"connectors": ["g.eur.mytrustline.server"]
      }
    },
    "store": true
  }
}

Similarly, add the following to your ILP Kit's config to setup an asymmetric trustline client (as with the server config, all of the following should go on a single line in your config file):

CONNECTOR_LEDGERS={
  "g.us.usd.anotherledger": {
      // your five-bells-ledger goes here
    }
  },
  "g.eur.mytrustline.": {
    "currency": "EUR",
    "plugin": "ilp-plugin-payment-channel-framework",
    "options": {
      "server": "btp+wss://username:shared_secret@wallet1.example:1234/example_path"
    },
    "store": false
  }
}

Note: You can provide both the server and listener properties in which case the BTP peers will automatically elect one of them to be the server and one of them to be the client.

ILP Plugin Payment Channel Framework

The plugin payment channel framework includes all the functionality of ilp-plugin-virtual, but wrapped around a Payment Channel Module. A payment channel module includes methods for securing a trustline balance, whether by payment channel claims or by periodically sending unconditional payments. The common functionality, such as implementing the ledger plugin interface, logging transfers, keeping balances, etc. are handled by the payment channel framework itself.

ILP Plugin virtual exposes a field called makePaymentChannelPlugin. This function takes a Payment Channel Module, and returns a LedgerPlugin class.

Minimal client-server config example

const ObjStore = require('./test/helpers/objStore')
const Plugin = require('.')
const port = 9000
const incomingSecret = 'pass'

const server = new Plugin({
  listener: { port },
  prefix: 'some.ledger.',
  info: {},
  incomingSecret,
  maxBalance: '10000',
  _store: new ObjStore()
})

const client = new Plugin({
  server: 'btp+ws://:' + incomingSecret + '@localhost:' + port,
  maxBalance: '10000',
  _store: new ObjStore()
})

server.connect()
  .then(() => client.connect())
  .then(() => client.disconnect())
  .then(() => server.disconnect())

Example Code with Claim-Based Settlement

Claim-based settlement is the simple case that this framework uses as its abstraction for settlement. Claim based settlement uses a unidirectional payment channel. You put your funds on hold, and give your peer signed claims for more and more of the funds. These signed claims are passed off-ledger, and your peer submits the highest claim when they want to get their funds.

Claim based settlement has been implemented on ripple with the PayChan functionality, or on bitcoin (and many other blockchains) by signing transactions that pay out of some script output.

const { makePaymentChannelPlugin } = require('ilp-plugin-virtual')
const { NotAcceptedError } = require('ilp-plugin-shared').Errors
const Network = require('some-example-network')
const BigNumber = require('bignumber.js')

return makePaymentChannelPlugin({
  // initialize fields and validate options in the constructor
  constructor: function (ctx, opts) {
    // we have one maxValueTracker to track the best incoming claim we've
    // gotten so far. it starts with a value of '0', and contains no data.
    ctx.state.bestClaim = ctx.backend.getMaxValueTracker('incoming_claim')

    // the 'maxUnsecured' option is taken from the plugin's constructor.
    // it defines how much the best incoming claim can differs from the amount
    // of incoming transfers.
    ctx.state.maxUnsecured = opts.maxUnsecured

    // use some preconfigured secret for authentication
    ctx.state.authToken = opts.authToken
  },

  // This token will be sent as a bearer token with outgoing requests, and used
  // to authenticate the incoming requests. If the network you're using has a
  // way to construct a shared secret, the authToken can be created
  // automatically instead of being pre-shared and configured.
  getAuthToken: (ctx) => (ctx.state.authToken),

  // the connect function runs when the plugin is connected.
  connect: async function (ctx) {
    // network initiation should happen here. In a claim-based plugin, this
    // would be the place to connect to the network and initiate payment
    // channels if they don't exist already.
    await Network.connectToNetwork()

    // establish metadata during the connection phase
    ctx.state.prefix = 'peer.network.' + (await Network.getChannelId()) + '.'

    // create ILP addresses for self and peer by appending identifiers from the
    // network onto the prefix.
    ctx.state.account = ctx.state.prefix + (await Network.getChannelSource())
    ctx.state.peer = ctx.state.prefix + (await Network.getChannelDestination())

    ctx.state.info = {
      prefix: ctx.state.prefix,
      currencyScale: 6,
      currencyCode: 'X??',
      connectors: []
    }
  },

  // Synchronous functions in order to get metadata. They won't be called until
  // after the plugin is connected.
  getAccount: (ctx) => (ctx.state.account),
  getPeerAccount: (ctx) => (ctx.state.peer),
  getInfo: (ctx) => (ctx.state.info),

  // this function is called every time an incoming transfer has been prepared.
  // throwing an error will stop the incoming transfer from being emitted as an
  // event.
  handleIncomingPrepare: async function (ctx, transfer) {
    // we get the incomingFulfilledAndPrepared because it represents the most
    // that can be owed to us, if all prepared transfers get fulfilled. The
    // 'transfer' has already been applied to this balance.
    const incoming = await ctx.transferLog.getIncomingFulfilledAndPrepared()
    const bestClaim = await ctx.state.bestClaim.getMax() || { value: '0', data: null }

    // make sure that if all incoming transfers are fulfilled (including the
    // new one), it won't put us too far from the best incoming claim we've
    // gotten.  'incoming - bestClaim.value' is the amount that our peer can
    // default on, so it's important we limit it.
    const exceeds = new BigNumber(incoming)
      .minus(bestClaim.value)
      .greaterThan(ctx.state.maxUnsecured)

    if (exceeds) {
      throw new NotAcceptedError(transfer.id + ' exceeds max unsecured balance')
    }
  },

  // this function is called whenever the outgoingBalance changes by a
  // significant amount. Exactly when and how often it will be called may
  // become configurable or change in the future, so your code should not rely
  // on it being called after every transfer.
  createOutgoingClaim: async function (ctx, outgoingBalance) {
    // create a claim for the total outgoing balance. This call is idempotent,
    // because it's relating to the absolute amount owed, and doesn't modify
    // anything.
    const claim = Network.createClaim(outgoingBalance)

    // return an object with the claim and the amount that the claim is for.
    // this will be passed into your peer's handleIncomingClaim function.
    return {
      balance: outgoingBalance,
      claim: claim
    }
  },

  // this function is called right after the peer calls createOutgoingClaim.
  handleIncomingClaim: async function (ctx, claimObject) {
    const { balance, claim } = claimObject

    if (Network.verify(claim, balance)) {
      // if the incoming claim is valid and it's better than your previous best
      // claim, set the bestClaim to the new one. If you already have a better
      // claim this will leave it intact. It's important to use the backend's
      // maxValueTracker here, because it will be shared across many processes.
      await ctx.state.bestClaim.setIfMax({ value: balance, data: claim })
    }
  },

  // called on plugin disconnect
  disconnect: async function (ctx) {
    const claim = await ctx.state.bestClaim.getMax()
    if (!claim) {
      return
    }

    // submit the best claim before disconnecting. This is the only time we
    // have to wait on the underlying ledger.
    await Network.submitClaim(claim)
  }
})

Example Code with Unconditional Payment-Based Settlement

Unconditional payment settlement secures a trustline balance by sending payments on a system that doesn't support conditional transfers. Hashed timelock transfers go through plugin virtual like a clearing layer, and every so often a settlement is sent to make sure the amount secured on the ledger doesn't get too far from the finalized amount owed.

Unlike creating a claim, sending a payment has side-effects (it alters an external system). Therefore, the code is slightly more complicated.

const { makePaymentChannelPlugin } = require('ilp-plugin-virtual')
const { NotAcceptedError } = require('ilp-plugin-shared').Errors
const Network = require('some-example-network')
const BigNumber = require('bignumber.js')

return makePaymentChannelPlugin({
  // initialize fields and validate options in the constructor
  constructor: function (ctx, opts) {
    // In this type of payment channel module, we create a log of incoming
    // settlements to track all the transfers sent to us on the ledger we're
    // using for settlement.  We use a transferLog in order to make sure a
    // single transfer can't be added twice.
    ctx.state.incomingSettlements = ctx.backend.getTransferLog('incoming_settlements')

    // The amount settled is used to track how much we've paid out in total.
    // We'll go deeper into how it's used in the `createOutgoingClaim`
    // function.
    ctx.state.amountSettled = ctx.backend.getMaxValueTracker('amount_settled')

    // In this type of payment channel backend, the unsecured balance we want
    // to limit is the total amount of incoming transfers minus the sum of all
    // the settlement transfers we've received.
    ctx.state.maxUnsecured = opts.maxUnsecured

    // use some preconfigured secret for authentication
    ctx.state.authToken = opts.authToken
  },

  getAuthToken: (ctx) => (ctx.state.authToken),

  connect: async function (ctx, opts) {
    await Network.connectToNetwork()

    // establish metadata during the connection phase
    ctx.state.prefix = 'peer.network.' + (await Network.getChannelId())
    ctx.state.account = await Network.getChannelSource()
    ctx.state.peer = await Network.getChannelDestination()
    ctx.state.info = {
      prefix: ctx.state.prefix,
      currencyScale: 6,
      currencyCode: 'X??',
      connectors: []
    }
  },

  // we don't need to define a disconnect handler in this case
  disconnect: function () => Promise.resolve(),

  // Synchronous functions in order to get metadata. They won't be called until
  // after the plugin is connected.
  getAccount: (ctx) => (ctx.state.prefix + ctx.state.account),
  getPeerAccount: (ctx) => (ctx.state.prefix + ctx.state.peer),
  getInfo: (ctx) => (ctx.state.info),

  handleIncomingPrepare: async function (ctx, transfer) {
    const incoming = await ctx.transferLog.getIncomingFulfilledAndPrepared()

    // Instead of getting the best claim, we're getting the sum of all our
    // incoming settlement transfers. This tells us how much incoming money has
    // been secured.
    const amountReceived = await ctx.state.incomingSettlements.getIncomingFulfilledAndPrepared()

    // The peer can default on 'incoming - amountReceived', so we want to limit
    // that amount.
    const exceeds = new BigNumber(incoming)
      .subtract(amountReceived)
      .greaterThan(ctx.state.maxUnsecured)

    if (exceeds) {
      throw new NotAcceptedError(transfer.id + ' exceeds max unsecured balance')
    }
  },

  // Even though this function is designed for creating a claim, we can
  // very easily repurpose it to make a payment for settlement.
  createOutgoingClaim: async function (ctx, outgoingBalance) {
    // If a new max value is set, the maxValueTracker returns the previous max
    // value. We tell the maxValueTracker that we're gonna pay the entire
    // outgoingBalance we owe, and then look at the difference between the last
    // balance and the outgoingBalance to determine how much to pay.
    // If we've already paid out more than outgoingBalance, then it won't be the
    // max value. The maxValueTracker will return outgoingBalance as the result,
    // and outgoingBalance - outgoingBalance is 0. Therefore, we send no payment.
    const lastPaid = await ctx.state.amountSettled.setIfMax({ value: outgoingBalance, data: null })
    const diff = new BigNumber(outgoingBalance)
      .sub(lastPaid.value)

    if (diff.lessThanOrEqualTo('0')) {
      return
    }

    // We take the transaction ID from the payment we send, and give it as an
    // identifier so our peer can look it up on the network and verify that we
    // paid them. Another approach could be to return nothing from this
    // function, and have the peer automatically track all incoming payments
    // they're notified of on the settlement ledger.
    const txid = await Network.makePaymentToPeer(diff)

    return { txid }
  },

  handleIncomingClaim: async function (ctx, claim) {
    const { txid } = claim
    const payment = await Network.getPayment(txid)

    if (!payment) {
      return
    }

    // It doesn't really matter whether this is fulfilled or not, we just need
    // it to affect the incoming balance so we know how much has been received.
    // We use the txid as the ID of the incoming payment, so it's impossible to
    // apply the same incoming settlement transfer twice.
    await ctx.state.incomingSettlements.prepare({
      id: txid,
      amount: payment.amount
    }, true) // isIncoming: true
  }

}

Backend API, with Extensions for Payment Channels


getMaxValueTracker (opts)

Get a MaxValueTracker.

Parameters

Returns


getTransferLog (opts)

Get a TransferLog.

Parameters

Returns


async TransferLog.setMaximum (max)

Set the TransferLog's maximum balance.

Parameters


async TransferLog.setMinimum (min)

Set the TransferLog's minimum balance.

Parameters


async TransferLog.getMaximum ()

Get the TransferLog's maximum balance. This is as high as the balance can go, including all fulfilled transfers and incoming prepared transfers but not including outgoing prepared transfers.

Returns


async TransferLog.getMinimum ()

Get the TransferLog's minimum balance. This is as low as the balance can go, including all fulfilled transfers and outgoing prepared transfers but not including incoming prepared transfers.

Returns


async TransferLog.getBalance ()

Get the TransferLog's balance, including only fulfilled transfers. This function is best used for display purposes only. Validation should be done using getIncomingFulfilled, getIncomingFulfilledAndPrepared, getOutgoingFulfilled, or getOutgoingFulfilledAndPrepared.

Returns


async TransferLog.get (id)

Get a transfer with info from the transfer log.

Parameters

Returns


async TransferLog.prepare (transfer, isIncoming)

Get a transfer from the transfer log. If the transfer would cause the balance to go over the transfer log's maximum or under its minimum, this function will throw an error. If the transfer cannot be applied for any other reason, an error will be thrown.

If a transfer with the same ID and the same contents as a previous transfer is added, it will return successfully, without modifying the database. If a transfer with the same ID as a previous transfer is added with different contents, an error will be thrown.

Parameters


async TransferLog.fulfill (transferId, fulfillment)

Fulfill a transfer currently in the prepared state. If the transfer's state is already fullfilled, the function will return without error. If the transfer's state is cancelled, the function will throw an error. If the transfer's state is prepared, it will be set to fulfilled and the fulfillment will be stored.

Important: The TransferLog is concerned only with storage, and making sure that the sum of the transfers does not exceed its given limits. As such, the fulfillment is not compared against the executionCondition of the transfer. This allows more flexibility in how the TransferLog is used, but developers should be careful to perform proper fulfillment validation in their own code. Remember that SHA256(fulfillment) must equal executionCondition, and the fulfillment should always be exactly 32 bytes.

Parameters


async TransferLog.cancel (transferId)

Cancel a transfer currently in the prepared state. If the transfer's state is already cancelled, the function will return without error. If the transfer's state is fulfilled, an error will be thrown. If the transfer's state is prepared, it will be set to cancelled.

Parameters


async TransferLog.getIncomingFulfilled ()

Get the sum of all incoming payments in the fulfilled state.

Returns


async TransferLog.getIncomingFulfilledAndPrepared ()

Get the sum of all incoming payments, including those which are in the prepared state.

Returns


async TransferLog.getOutgoingFulfilled ()

Get the sum of all outgoing payments in the fulfilled state.

Returns


async TransferLog.getOutgoingFulfilledAndPrepared ()

Get the sum of all outgoing payments, including those which are in the prepared state.

Returns


async MaxValueTracker.setIfMax (entry)

Put entry into the MaxValueTracker. If entry.value is larger than the previous entry's value, then entry becomes the new max entry, and the previous max entry is returned. If entry.value is not larger than the previous max entry's value, then the max entry remains the same and entry is returned back.

Parameters

Return


async MaxValueTracker.getMax ()

Returns the max value tracker's maximum entry.

Return


Plugin Context API

The PluginContext is a bundle of objects passed into the Payment Channel Backend methods, in order to access useful plugin state.

FieldTypeDescription
stateObjectObject to keep Payment Channel Module state. Persists between function calls, but not if the plugin is restarted.
rpcRPCRPC object for this plugin. Can be used to call methods on peer.
backendExtendedPluginBackendPlugin backend, for creating TransferLogs and MaxValueTrackers.
transferLogTransferLogPlugin's TransferLog, containing all its ILP transfers.
pluginLedgerPluginPlugin object. Only LedgerPlugin Interface functions should be accessed.

Payment Channel Module API

Calling makePaymentChannelPlugin with an object containing all of the functions defined below will return a class. This new class will perform all the functionality of ILP Plugin Virtual, and additionally use the supplied callbacks to handle settlement.

Aside from connect and disconnect, the functions below might be called during the flow of an RPC request, so they should run fast. Any of these calls SHOULD NOT take longer than 500 ms if await-ed. If a slower operation is required, it should be run in the background so it doesn't block the flow of the function.


pluginName

This optional field defines the type of this plugin. For instance, if it is set to example, the class name will become PluginExample, and debug statements will be printed as ilp-plugin-example.

Type

String


constructor (ctx, opts)

Called when the plugin is constructed.

Parameters


async connect (ctx, opts)

Called when plugin.connect() is called.

Parameters


getInfo (ctx)

Return the LedgerInfo of this payment channel. This function will not be called until after the plugin is connected. The prefix must be deterministic, as the connector requires plugin prefixes to be preconfigured.

The framework code will automatically create a deep clone of the return value before returning to the plugin user.

Parameters


getAccount (ctx)

Return the ILP address of this account on the payment channel. The ILP prefix must match getInfo().prefix. This function will not be called until after the plugin is connected.

Parameters


getPeerAccount (ctx)

Return the ILP address of the peer's account on the payment channel. The ILP prefix must match getInfo().prefix. This function will not be called until after the plugin is connected.

Parameters


async disconnect (ctx)

Called when plugin.disconnect() is called.

Parameters


async handleIncomingPrepare (ctx, transfer)

Called when an incoming transfer is being processed, but has not yet been prepared. If this function throws an error, the transfer will not be prepared and the error will be passed back to the peer.

Parameters


async createOutgoingClaim (ctx, balance)

Called when settlement is triggered. This may occur in the flow of a single payment, or it may occur only once per several payments. The return value of this function is passed to the peer, and into their handleIncomingClaim() function. The return value must be stringifiable to JSON.

Parameters


async handleIncomingClaim (ctx, claim)

Called after peer's createOutgoingClaim() function is called.

Parameters