OrderBook 蓝图

OrderBook 蓝图是在 Basic 蓝图的基础上扩展的标准模板,旨在支持开发者构建具有订单簿功能的自定义 Agent。除了继承 Basic 蓝图的所有功能之外,OrderBook 蓝图增加了几个核心功能,以实现订单的创建、管理和结算操作。

解析 OrderBook 蓝图

新增核心功能

核心功能代码详解

1. 创建订单-MakeOrder

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

该功能为 Agent 提供了发布交易意图的能力,通过票据(Note)来明确交易的条件(资产类型和数量等),从而支持订单薄中订单的生成。

功能描述:

  • 支持 Agent 所有者通过调用该接口创建新的 Note。

关键代码逻辑:

  • 验证输入参数的合法性(例如资产类型,数量,过期时间等)。

  • 调用 FPP 协议的 CreateNote 接口,生成票据并存储在 Agent 内部数据表中。

2. 执行结算操作-Execute

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
)

功能描述:

  • 当 Agent 发行的 Note 进入结算阶段时,由 FFP 结算中心触发 Agent 执行资产的转移操作。

  • 支持 Agent 处理自身创建的票据在结算过程中需要的资产交割。

关键代码逻辑:

  • 验证 Note 的合法性(如是否存在、发行者是否为 Agent、状态是否为 Open)。

  • 更新 Note 状态为 Executed。

  • 将对应的资产转移到 FFP 结算中心。

3. 取消订单-CancelOrders

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
)

功能描述:

  • 允许 Agent 所有者取消未被结算的票据。

  • 仅支持取消状态为 Open 的票据,其他状态(如 Executed 或 Settled)的票据无法取消。

关键代码逻辑:

  • 验证票据是否属于当前 Agent,并确保其状态为 Open。

  • 调用 FFP 协议的 Cancel 接口取消票据。

  • 将本地票据状态更新为取消状态。

完整代码

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

开发之前,把 orderbook.lua 和 utils.lua 复制到开发环境即可。

总结

OrderBook 蓝图是 Basic 蓝图的功能升级版,适合需要实现复杂交易和订单管理逻辑的开发者。它为构建自定义订单簿 Agent 提供了标准化的基础功能,使开发者能够快速上手并专注于业务逻辑的优化与实现。

Last updated

Was this helpful?