# Basic 蓝图

Basic 蓝图是一个预先设计好的模板，可帮助你快速构建一个基础版本的 Agent，并根据需求进行定制。

### 解析 Basic 蓝图

#### Basic 蓝图基础功能

* [**提现操作(Withdraw)**](#id-1.-ti-xian-cao-zuo-withdraw)**:** 支持 Agent 所有者从 Agent 中提取指定资产。
* [**接单结算(TakeOrder)**](#id-2.-jie-dan-jie-suan-takeorder)**:** 支持从 FFP 系统中获取指定票据(Notes)，验证有效性并提交结算。
* [**结算完成通知(Settle Done)**](#id-3.-jie-suan-wan-cheng-tong-zhi-done)**:** 监听 FFP 结算中心的结算通知并更新票据和结算状态。

#### Basic 蓝图代码结构

以下是 basic.lua 的代码结构：

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

-- Agent FFP 相关配置，可自定义配置
FFP = FFP or {}
FFP.Settle = FFP.Settle or 'rKpOUxssKxgfXQOpaCq22npHno6oRw66L3kZeoo_Ndk' -- 指定 FFP 结算中心
FFP.Version = FFP.Version or '0.31'   -- FFP 版本号
FFP.MaxNotesToSettle = FFP.MaxNotesToSettle or 2 -- 最大票据结算数量

-- 数据存储
FFP.Notes = FFP.Notes or {}   -- 存储票据
FFP.Settled = FFP.Settled or {} -- 存储已结算票据

-- 提现操作
Handlers.add('ffp.withdraw', 'FFP.Withdraw', function(msg)
  -- 提现逻辑
end)

-- 接单结算
Handlers.add('ffp.takeOrder', 'FFP.TakeOrder', function(msg)
  -- 接单结算逻辑
end)

-- 结算完成通知操作
Handlers.add('ffp.done', function(msg)
  -- 结算完成通知
end, function(msg)
  -- 结算状态更新
end)

```

### 代码详解

#### **1. 提现操作-Withdraw**

<details>

<summary>code</summary>

```lua
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)
```

</details>

该功能允许 Agent 的所有者从 Agent 中提取指定资产。

典型场景：

* Agent 所有者提取收益。
* 定期清空 Agent 中的剩余资产。

#### 2. 接单结算-**TakeOrder**

<details>

<summary>code</summary>

```lua
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)
```

</details>

该功能允许 Agent 从 FFP 协议中获取指定的票据集（Notes)，验证其有效性，并提交到 FFP 协议中生成结算订单进行结算。

关键代码逻辑：

* 验证票据有效性（例如状态、来源、过期时间等）。
* 调用 FFP 协议中的 StartSettle 接口生成结算订单。
* 执行结算订单逻辑（比如资金划转）。

#### 3. 结算完成通知-Done

<details>

<summary>code</summary>

```lua
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 settleID = msg['X-FFP-SettleID']
    if settleID and FFP.Settled[settleID] then
        if msg['X-FFP-For'] == 'Settled' then
            FFP.Settled[settleID].Status = 'Settled'
        else
            FFP.Settled[settleID].Status = 'Rejected'
        end
        FFP.Settled[settleID].SettledDate = msg['X-FFP-SettledDate']
        for _, noteID in ipairs(FFP.Settled[settleID].NoteIDs) do
            local data = Send({
                Target = FFP.Settle,
                Action = 'GetNote',
                NoteID = noteID
            }).receive().Data
            FFP.Notes[noteID] = json.decode(data)
        end
    else
        print('SettleID not found: ' .. settleID)
    end
end)
```

</details>

该功能监听 FFP 协议发送的结算完成通知，并更新票据和结算订单状态。

主要作用是保证 Agent 保存的票据和结算订单状态和 FFP 协议的状态保持一致。

### 完整代码

<details>

<summary>basic.lua </summary>

```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 {}

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.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 settleID = msg['X-FFP-SettleID']
    if settleID and FFP.Settled[settleID] then
        if msg['X-FFP-For'] == 'Settled' then
            FFP.Settled[settleID].Status = 'Settled'
        else
            FFP.Settled[settleID].Status = 'Rejected'
        end
        FFP.Settled[settleID].SettledDate = msg['X-FFP-SettledDate']
        for _, noteID in ipairs(FFP.Settled[settleID].NoteIDs) do
            local data = Send({
                Target = FFP.Settle,
                Action = 'GetNote',
                NoteID = noteID
            }).receive().Data
            FFP.Notes[noteID] = json.decode(data)
        end
    else
        print('SettleID not found: ' .. settleID)
    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)

```

</details>

<details>

<summary>utils.lua</summary>

```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
```

</details>

### 如何基于蓝图构建自定义 FFP Agent

#### 1. 克隆蓝图

将 basic.lua 和 utils.lua 代码复制到开发环境。

#### 2. 添加自定义逻辑

通常情况下，您无需修改 basic.lua 中已经实现的核心功能代码。只需根据业务需求，在此基础上添加额外的功能逻辑。

例如，我们可以构建一个 FFP 中的 [无损套利 Agent](basic-lan-tu/wu-ben-tao-li-agent)。

### 总结

Basic 蓝图是构建 FFP Agent 的标准模板，所有 FFP Agent 均以此为基础，根据具体业务需求扩展更多功能。通过定制化的 Agent，开发者可以高效融入 FusionFi 协议生态，参与各类金融活动。
