# 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](https://ffp.gitbook.io/fusionfi/fusionfi-protocol-cn/kai-fa-zhi-nan/blueprints/basic-lan-tu/wu-ben-tao-li-agent)。

### 总结

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


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://ffp.gitbook.io/fusionfi/fusionfi-protocol-cn/kai-fa-zhi-nan/blueprints/basic-lan-tu.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
