OrderBook Blueprint

The OrderBook Blueprint is an extended standard template built on top of the Basic Blueprint. It is designed to help developers create custom Agents with order book functionality. In addition to inheriting all features of the Basic Blueprint, the OrderBook Blueprint introduces several core functionalities to support the creation, management, and settlement of orders.

OrderBook Blueprint Analysis

New Core Features

  • Make Order: Allows the Agent to create Notes (orders).

  • Execute Settlement: The core operation during the matching and settlement phases, enabling the actual transfer of assets.

  • Cancel Order: Cancels published Notes that have not yet been settled.

Detailed Code Explanation

1. Make Order

code
Handlers.add('ffp.makeOrder', 'FFP.MakeOrder',
  function(msg)
    assert(msg.From == Owner or msg.From == ao.id, 'Only owner can make order')
    assert(type(msg.AssetID) == 'string', 'AssetID is required')
    assert(type(msg.Amount) == 'string', 'Amount is required')
    assert(bint.__lt(0, bint(msg.Amount)), 'Amount must be greater than 0')
    assert(type(msg.HolderAssetID) == 'string', 'HolderAssetID is required')
    assert(type(msg.HolderAmount) == 'string', 'HolderAmount is required')
    assert(bint.__lt(0, bint(msg.HolderAmount)), 'HolderAmount must be greater than 0')

    local expireDate = msg.ExpireDate
    if expireDate and tonumber(expireDate) < msg.Timestamp then
      msg.reply({Error = 'err_invalid_expire_date', ['X-FFP-MakeOrderID'] = msg.Id})
      return
    end
    if not expireDate then expireDate = '' end

    local res = Send({
      Target = FFP.Settle,
      Action = 'CreateNote',
      AssetID = msg.AssetID,
      Amount = msg.Amount,
      HolderAssetID = msg.HolderAssetID,
      HolderAmount = msg.HolderAmount,
      IssueDate = tostring(msg.Timestamp),
      ExpireDate = expireDate,
      Version = FFP.SettleVersion
    }).receive()
    local noteID = res.NoteID
    local note = json.decode(res.Data)
    FFP.Notes[noteID] = note
    FFP.MakeTxToNoteID[msg.Id] = noteID
    msg.reply({Action = 'OrderMade-Notice', NoteID = note.NoteID, Data = json.encode(note)})
  end

This feature enables the Agent to publish trading intentions using Notes, which specify trading conditions (e.g., asset type and quantity), supporting the creation of orders in the order book.

Feature Description:

  • Allows the Agent owner to create new Notes by calling this interface.

Key Code Logic:

  • Validate the legality of input parameters (e.g., asset type, quantity, expiration time).

  • Call the FFP protocol’s CreateNote interface to generate a Note and store it in the Agent’s internal data table.

2. Execute Settlement

code
Handlers.add('ffp.execute', 'Execute',
  function(msg)
    assert(msg.From == FFP.Settle, 'Only settle can start exectue')
    assert(type(msg.NoteID) == 'string', 'NoteID is required')
    assert(type(msg.SettleID) == 'string', 'SettleID is required')

    local note = FFP.Notes[msg.NoteID]
    if not note then
      msg.reply({Action = 'Reject', Error = 'err_not_found', SettleID = msg.SettleID, NoteID = msg.NoteID})
      return
    end
    if note.Status ~= 'Open' then
      msg.reply({Action = 'Reject', Error = 'err_not_open', SettleID = msg.SettleID, NoteID = msg.NoteID})
      return
    end
    if note.Issuer ~= ao.id then
      msg.reply({Action = 'Reject', Error = 'err_invalid_issuer', SettleID = msg.SettleID, NoteID = msg.NoteID})
      return
    end
    
    msg.reply({Action = 'StartExecute', SettleID = msg.SettleID, NoteID = msg.NoteID})

    FFP.Notes[msg.NoteID].Status = 'Executed'
    Send({Target = note.AssetID, Action = 'Transfer', Quantity = note.Amount, Recipient = FFP.Settle, 
      ['X-FFP-SettleID'] = msg.SettleID, 
      ['X-FFP-NoteID'] = msg.NoteID,
      ['X-FFP-For'] = 'Execute'
    })
  end
)

Feature Description:

  • When Notes issued by the Agent enter the settlement phase, the FFP settlement center triggers the Agent to execute asset transfer operations.

  • Supports the Agent in handling asset delivery for Notes it created during settlement.

Key Code Logic:

  • Validate the Note’s legitimacy (e.g., ensure it exists, was issued by the Agent, and has an Open status).

  • Update the Note’s status to Executed.

  • Transfer the corresponding assets to the FFP settlement center.

3. Cancel Orders

code
Handlers.add('ffp.cancelOrder', 'FFP.CancelOrder',
  function(msg)
    assert(msg.From == Owner or msg.From == ao.id, 'Only owner can cancel order')
    assert(type(msg.NoteID) == 'string', 'NoteID is required')

    local noteID = msg.NoteID
    local note = FFP.Notes[noteID]
    
    if not note then
      msg.reply({Error = 'err_not_found', ['X-FFP-CancelOrderID'] = msg.Id})
      return
    end
    if note.Issuer ~= ao.id then
      msg.reply({Error = 'err_invalid_user', ['X-FFP-CancelOrderID'] = msg.Id})
      return
    end
    if note.Status ~= 'Open' then
      msg.reply({Error = 'err_not_open', ['X-FFP-CancelOrderID'] = msg.Id})
      return
    end
    
    local res = Send({Target = FFP.Settle, Action = 'Cancel', Version = FFP.SettleVersion, NoteID = noteID}).receive()
    if res.Error then
      msg.reply({Error = res.Error, ['X-FFP-CancelOrderID'] = msg.Id})
      return
    end
    FFP.Notes[noteID] = json.decode(res.Data)
    msg.reply({Action = 'OrderCancelled-Notice', NoteID = noteID})
  end
)

Feature Description:

  • Allows the Agent owner to cancel Notes that have not yet been settled.

  • Only Notes in the Open status can be canceled; other statuses (e.g., Executed or Settled) cannot be canceled.

Key Code Logic:

  • Verify that the Note belongs to the current Agent and ensure its status is Open.

  • Call the FFP protocol’s Cancel interface to cancel the Note.

  • Update the local Note status to Canceled.

Complete Code

orderbook.lua
local json = require('json')
local bint = require('.bint')(512)
local utils = require('.utils')

FFP = FFP or {}
-- config
FFP.Settle = FFP.Settle or 'rKpOUxssKxgfXQOpaCq22npHno6oRw66L3kZeoo_Ndk'
FFP.SettleVersion = FFP.SettleVersion or '0.31'
FFP.MaxNotesToSettle = FFP.MaxNotesToSettle or 2

-- database
FFP.Notes = FFP.Notes or {}
FFP.Settled = FFP.Settled or {}
FFP.MakeTxToNoteID = FFP.MakeTxToNoteID or {}

Handlers.add('ffp.withdraw', 'FFP.Withdraw',
  function(msg)
    assert(msg.From == Owner or msg.From == ao.id, 'Only owner can withdraw')
    assert(type(msg.Token) == 'string', 'Token is required')
    assert(type(msg.Amount) == 'string', 'Amount is required')
    assert(bint.__lt(0, bint(msg.Amount)), 'Amount must be greater than 0')
    
    Send({ 
      Target = msg.Token, 
      Action = 'Transfer', 
      Quantity = msg.Amount, 
      Recipient = Owner
    })
  end
)

Handlers.add('ffp.takeOrder', 'FFP.TakeOrder',
  function(msg)
    assert(msg.From == Owner or msg.From == ao.id, 'Only owner can take order')
    
    local noteIDs = utils.getNoteIDs(msg.Data)
    if noteIDs == nil then
      msg.reply({Error = 'err_invalid_note_ids', ['X-FFP-TakeOrderID'] = msg.Id})
      return
    end
    
    if #noteIDs > FFP.MaxNotesToSettle then
      msg.reply({Error = 'err_too_many_orders', ['X-FFP-TakeOrderID'] = msg.Id})
      return
    end

    local notes = {}
    for _, noteID in ipairs(noteIDs) do
      local data = Send({Target = FFP.Settle, Action = 'GetNote', NoteID = noteID}).receive().Data
      if data == '' then
        msg.reply({Error = 'err_not_found', ['X-FFP-TakeOrderID'] = msg.Id})
        return
      end
      local note = json.decode(data)
      if note.Status ~= 'Open' then
        msg.reply({Error = 'err_not_open', ['X-FFP-TakeOrderID'] = msg.Id, Data = noteID})
        return
      end
      if note.Issuer == ao.id then
        msg.reply({Error = 'err_cannot_take_self_order', ['X-FFP-TakeOrderID'] = msg.Id, Data = noteID})
        return
      end
      if note.ExpireDate and note.ExpireDate < msg.Timestamp then
        msg.reply({Error = 'err_expired', ['X-FFP-TakeOrderID'] = msg.Id, Data = noteID})
        return
      end
      table.insert(notes, note)
    end
    
    local si, so = utils.SettlerIO(notes)
    -- todo: make sure we have enough balance

    -- start a settle session
    local res = Send({Target = FFP.Settle, Action = 'StartSettle', Version = FFP.SettleVersion, Data = json.encode(noteIDs)}).receive()
    if res.Error then
      msg.reply({Error = res.Error, ['X-FFP-TakeOrderID'] = msg.Id})     
      return
    end
   
    local settleID = res.SettleID
    FFP.Settled[settleID] = {SettleID = settleID, NoteIDs = noteIDs, Status = 'Pending'}
   
    if next(so) == nil then
      print('TakeOrder: Settler no need to transfer to settle process')
      Send({Target = FFP.Settle, Action = 'Settle', Version = FFP.SettleVersion, SettleID = settleID})
      msg.reply({Action = 'TakeOrder-Settle-Sent-Notice', SettleID = settleID})
      return
    end

    for k, v in pairs(so) do
      local amount = utils.subtract('0', v)
      Send({Target = k, Action = 'Transfer', Quantity = amount,  Recipient = FFP.Settle, 
        ['X-FFP-SettleID'] = settleID, 
        ['X-FFP-For'] = 'Settle'
      })
    end
    msg.reply({Action = 'TakeOrder-Settle-Sent-Notice', SettleID = settleID})
  end
)

Handlers.add('ffp.makeOrder', 'FFP.MakeOrder',
  function(msg)
    assert(msg.From == Owner or msg.From == ao.id, 'Only owner can make order')
    assert(type(msg.AssetID) == 'string', 'AssetID is required')
    assert(type(msg.Amount) == 'string', 'Amount is required')
    assert(bint.__lt(0, bint(msg.Amount)), 'Amount must be greater than 0')
    assert(type(msg.HolderAssetID) == 'string', 'HolderAssetID is required')
    assert(type(msg.HolderAmount) == 'string', 'HolderAmount is required')
    assert(bint.__lt(0, bint(msg.HolderAmount)), 'HolderAmount must be greater than 0')

    local expireDate = msg.ExpireDate
    if expireDate and tonumber(expireDate) < msg.Timestamp then
      msg.reply({Error = 'err_invalid_expire_date', ['X-FFP-MakeOrderID'] = msg.Id})
      return
    end
    if not expireDate then expireDate = '' end

    local res = Send({
      Target = FFP.Settle,
      Action = 'CreateNote',
      AssetID = msg.AssetID,
      Amount = msg.Amount,
      HolderAssetID = msg.HolderAssetID,
      HolderAmount = msg.HolderAmount,
      IssueDate = tostring(msg.Timestamp),
      ExpireDate = expireDate,
      Version = FFP.SettleVersion
    }).receive()
    local noteID = res.NoteID
    local note = json.decode(res.Data)
    FFP.Notes[noteID] = note
    FFP.MakeTxToNoteID[msg.Id] = noteID
    msg.reply({Action = 'OrderMade-Notice', NoteID = note.NoteID, Data = json.encode(note)})
  end
)

Handlers.add('ffp.execute', 'Execute',
  function(msg)
    assert(msg.From == FFP.Settle, 'Only settle can start exectue')
    assert(type(msg.NoteID) == 'string', 'NoteID is required')
    assert(type(msg.SettleID) == 'string', 'SettleID is required')

    local note = FFP.Notes[msg.NoteID]
    if not note then
      msg.reply({Action = 'Reject', Error = 'err_not_found', SettleID = msg.SettleID, NoteID = msg.NoteID})
      return
    end
    if note.Status ~= 'Open' then
      msg.reply({Action = 'Reject', Error = 'err_not_open', SettleID = msg.SettleID, NoteID = msg.NoteID})
      return
    end
    if note.Issuer ~= ao.id then
      msg.reply({Action = 'Reject', Error = 'err_invalid_issuer', SettleID = msg.SettleID, NoteID = msg.NoteID})
      return
    end
    
    msg.reply({Action = 'StartExecute', SettleID = msg.SettleID, NoteID = msg.NoteID})

    FFP.Notes[msg.NoteID].Status = 'Executed'
    Send({Target = note.AssetID, Action = 'Transfer', Quantity = note.Amount, Recipient = FFP.Settle, 
      ['X-FFP-SettleID'] = msg.SettleID, 
      ['X-FFP-NoteID'] = msg.NoteID,
      ['X-FFP-For'] = 'Execute'
    })
  end
)

Handlers.add('ffp.done',
  function(msg) 
    return (msg.Action == 'Credit-Notice') and (msg['X-FFP-For'] == 'Settled' or msg['X-FFP-For'] == 'Refund') 
  end,
  function(msg)
    assert(msg.Sender == FFP.Settle, 'Only settle can send settled or refund notice')

    local noteID = msg['X-FFP-NoteID']
    local settleID = msg['X-FFP-SettleID']
    
    if noteID and FFP.Notes[noteID] then
      if msg['X-FFP-For'] == 'Settled' then
        FFP.Notes[noteID].Status = 'Settled'
      else
        FFP.Notes[noteID].Status = 'Open'
      end
      return
    end

    if settleID and Settled[settleID] then
      if msg['X-FFP-For'] == 'Settled' then
        Settled[settleID].Status = 'Settled'
      else
        Settled[settleID].Status = 'Rejected'
      end
      Settled[settleID].SettledDate = msg['X-FFP-SettledDate']
      for _, noteID in ipairs(Settled[settleID].NoteIDs) do
        local data = Send({Target = FFP.Settle, Action = 'GetNote', NoteID = noteID}).receive().Data
        FFP.Notes[noteID] = json.decode(data)
      end
    end
  end
)

Handlers.add('ffp.cancelOrder', 'FFP.CancelOrder',
  function(msg)
    assert(msg.From == Owner or msg.From == ao.id, 'Only owner can cancel order')
    assert(type(msg.NoteID) == 'string', 'NoteID is required')

    local noteID = msg.NoteID
    local note = FFP.Notes[noteID]
    
    if not note then
      msg.reply({Error = 'err_not_found', ['X-FFP-CancelOrderID'] = msg.Id})
      return
    end
    if note.Issuer ~= ao.id then
      msg.reply({Error = 'err_invalid_user', ['X-FFP-CancelOrderID'] = msg.Id})
      return
    end
    if note.Status ~= 'Open' then
      msg.reply({Error = 'err_not_open', ['X-FFP-CancelOrderID'] = msg.Id})
      return
    end
    
    local res = Send({Target = FFP.Settle, Action = 'Cancel', Version = FFP.SettleVersion, NoteID = noteID}).receive()
    if res.Error then
      msg.reply({Error = res.Error, ['X-FFP-CancelOrderID'] = msg.Id})
      return
    end
    FFP.Notes[noteID] = json.decode(res.Data)
    msg.reply({Action = 'OrderCancelled-Notice', NoteID = noteID})
  end
)

Handlers.add('ffp.getNote', 'FFP.GetNote', 
    function(msg)
        assert(type(msg.MakeTx) == 'string', 'MakeTx is required')
        local noteID = FFP.MakeTxToNoteID[msg.MakeTx]
        if noteID and FFP.Notes[noteID] then
          msg.reply({NoteID=noteID, Data = json.encode(FFP.Notes[noteID])})
        else
          msg.reply({Error = 'err_not_found'})
        end
    end
)

Handlers.add('ffp.getOrders', 'FFP.GetOrders',
  function (msg)
    msg.reply({Action = 'GetOrders-Notice', Data = json.encode(FFP.Notes)})
  end
)

Handlers.add('ffp.getSettled', 'FFP.GetSettled',
  function (msg)
    msg.reply({Action = 'GetSettled-Notice', Data = json.encode(FFP.Settled)})
  end
)
utils.lua
local json = require('json')
local bint = require('.bint')(512)

local mod = {
  add = function(a, b)
      return tostring(bint(a) + bint(b))
  end,
  subtract = function(a, b)
      return tostring(bint(a) - bint(b))
  end,
  toBalanceValue = function(a)
      return tostring(bint(a))
  end,
  toNumber = function(a)
      return tonumber(a)
  end,
  lt = function (a, b)
      return bint.__lt(bint(a), bint(b))
  end
}

mod.getNoteIDs = function(data)
  if string.find(data, "null") then
    return nil
  end
  
  local noteIDs, err = json.decode(data)
  if err then
    return nil
  end

  if type(noteIDs) == "string" then
    return {noteIDs}
  end

  if type(noteIDs) == "table" then
    for i = 1, #noteIDs do
      if noteIDs[i] == nil or type(noteIDs[i]) ~= "string" then
        return nil
      end
    end
    return noteIDs
  end

  return nil
end

mod.SettlerIO = function (notes)
  local settlerIn = {}
  local settlerOut = {}

  local bc = {}
  for _, note in ipairs(notes) do
      if not bc[note.AssetID] then bc[note.AssetID] = bint(0) end
      if not bc[note.HolderAssetID] then bc[note.HolderAssetID] = bint(0) end
      bc[note.AssetID] = mod.add(bc[note.AssetID], note.Amount)
      bc[note.HolderAssetID] = mod.subtract(bc[note.HolderAssetID], note.HolderAmount)
  end
  
  for k, v in pairs(bc) do
      if v == '0' then bc[k] = nil end
  end

  for k, v in pairs(bc) do
      if mod.lt(v, '0') then
          settlerOut[k] = v
      else
          settlerIn[k] = v
      end
  end

  return settlerIn, settlerOut
end
    
return mod

Before development, copy orderbook.lua and utils.lua into your development environment.

Summary

The OrderBook Blueprint is an upgraded version of the Basic Blueprint, designed for developers requiring advanced transaction and order management logic. It provides standardized foundational features for building custom order book Agents, enabling developers to quickly get started and focus on optimizing and implementing their business logic.

Last updated

Was this helpful?