OByte自治代理编程指南(一):入门
自治代理(Autonomous Agents, AA)是OByte的DAG账本上的一类特殊地址(账户),它以预定的程序进行工作。自治代理的行为特性类似于一台接受投币的自动售货机,接受投币后由键盘输入数据指令,然后做出相应的动作,例如冲一杯咖啡、播一首歌、或者其它已设定好的动作。
自治代理可以用于在OByte平台上迅速构建去中心化金融应用。
基本语法
自治代理的代码在OByte上部署之后就永远不会更改。代码内容是开放的,且由网络上的所有节点执行。
任何人都可以通过向自治代理的地址发送交易来激活它。 这是一个简单自治代理的例子:
['autonomous agent', {
messages: [
{
app: 'payment',
payload: {
asset: 'base',
outputs: [
{address: "{trigger.address}", amount: "{trigger.output[[asset=base]] - 1000}"}
]
}
}
]
}]
messages
是自治代理生成的响应交易的模板。它遵循常规OByte交易的结构,但它的花括号{}
之间的部分生成的响应交易依赖于触发(激活)交易的内容。这类似于如何将PHP代码插入HTML,从而使结果页面根据请求来生成。编写自治代理的语言称为Oscript。
上面的自治代理示例是将收到的资金(减去1000字节手续费)发送回发送方。trigger.address
是激活自治代理的发送方地址。trigger.output
允许查找发送给自治代理的不同资产的金额,trigger.output [[asset = base]]
是基础货币bytes。
另一个例子是以1.5 tokens/byte
的价格出售代币:
['autonomous agent', {
messages: [
{
app: 'payment',
payload: {
asset: 'n9y3VomFeWFeZZ2PcSEcmyBb/bI7kzZduBJigNetnkY=', // this is the token being sold
outputs: [
{address: "{trigger.address}", amount: "{ round(trigger.output[[asset=base]] * 1.5) }"}
]
}
}
]
}]
在使用上述自治代理之前,需要将自治代理的代码部署在OByte上面。访问oscript.org,复制粘贴上面的代码,然后单击“Deploy”,你将获得自治代理的地址。任何人现在都可以发送交易到这个地址来触发执行自治代理的代码。
参数传递
自治代理可以根据它在触发交易中收到的数据来生成响应交易。
在OByte中,任何交易都可以包含一个或多个不同类型的消息。消息类型字段为app
。最常见的消息类型是payment
。每个交易必须包含至少一个payment
类型消息,用来支付存储交易的手续费。另一种消息类型是data
。这种消息类型提供了一个可以包含任何数据的json对象:
{
app: 'data',
payload: {
withdrawal_amount: 25000,
nested_array: [
99,
{another_field: 44}
],
nested_object: {yet_another_field: "value2"}
},
...
}
在payload
中的json对象是交易发送的数据。
当自治代理在触发交易中收到data
类型消息(当然还有payment
类型消息)时,它可以通过trigger.data
访问交易中发送的数据,其动作可以取决于收到的数据:
['autonomous agent', {
messages: [
{
app: 'payment',
payload: {
asset: 'base',
outputs: [
{address: "{trigger.address}", amount: "{trigger.data.withdrawal_amount}"}
]
}
}
]
}]
上面的自治代理尝试将trigger.data.withdrawal_amount
字节发送回发送方。withdraw_amount
是触发交易的data
类型消息中的字段:
{
withdrawal_amount: 3000
}
错误处理
在上面的示例中,如果withdraw_amount
大于自治代理的余额(包括从触发交易接收的金额),则自治代理将执行失败。当自治代理因某种原因未能处理触发交易时,它会回滚所有更改并尝试将所有收到的资金(包括资产)退回给发送方,但需要减去发送费用。默认情况下,基础资产的退回费用为10000字节,所有其它资产的退回费用为0。这也是最低限度。可以通过在自治代理定义中添加bounce_fees
字段来覆盖默认值:
['autonomous agent', {
bounce_fees: {
base: 20000,
"n9y3VomFeWFeZZ2PcSEcmyBb/bI7kzZduBJigNetnkY=": 100
},
messages: [
{
app: 'payment',
payload: {
asset: 'base',
outputs: [
{address: "{trigger.address}", amount: "{trigger.data.withdrawal_amount}"}
]
}
}
]
}]
如果必须回滚触发交易,上述自治代理将收取20000字节和100个单位的资产n9y3VomFeWFeZZ2PcSEcmyBb/bI7kzZduBJigNetnkY=
。 向自治代理发送少于20000个字节将导致自治代理吞掉相应资产(回滚费用也不够)。当资产n9y3VomFeWFeZZ2PcSEcmyBb/bI7kzZduBJigNetnkY=
被发送到自治代理时,它也应该至少为100,否则自治代理会吞掉相应资产。
发送数据消息
如上所述,payment
是最常见的消息类型,但是其它类型的消息也可以通过任何地址发送,包括自治代理。
发送data_feed
消息
自治代理可以作为预言机(oracle)发送data_feed
消息:
['autonomous agent', {
messages: [
{
app: 'data_feed',
payload: {
"{trigger.data.feed_name}": "{trigger.data.feed_value}"
}
}
]
}]
上面示例展示了数据对象的键也可以通过Oscript进行参数化,就像对象的值一样。
上述自治代理没有payment
消息,但会自动添加用来支付发送手续费。
发送data
消息
自治代理可以发送任何结构化数据:
['autonomous agent', {
messages: [
{
app: 'data',
payload: {
forwarded: 1,
initial_data: "{trigger.data}"
}
},
{
app: 'payment',
payload: {
asset: 'base',
outputs: [
{address: "JVUJQ7OPBJ7ZLZ57TTNFJIC3EW7AE2RY", amount: "{trigger.output[[asset=base]] - 1000}"}
]
}
}
]
}]
与data_feed
消息不同,data
消息:
- 可以具有任何嵌套深度的结构(
data_feed
是键值对); - 没有进行搜索索引;
- 可以向其它自治代理发送数据。
上述自治代理将收到的资金(减去1000字节手续费)转发到另一个地址,该地址可能是其它自治代理。它还可以传递数据,其中包括接收的数据参数。trigger.data
的值将扩展为一个对象,并作为initial_data
字段的值添加。
如果接收者(JVUJQ7OPBJ7ZLZ57TTNFJIC3EW7AE2RY
)恰好是另一个自治代理,则只有在当前自治代理的执行完成后才会执行,以保证没有双花问题。
发送text
消息
['autonomous agent', {
messages: [
{
app: 'text',
payload: `{"Hello, " || trigger.data.name || "!"}`
}
]
}]
上面自治代理将会发送一个简单的文本消息,并将其存在DAG上。||
是字符串拼接运算。
定义新资产
['autonomous agent', {
messages: [
{
app: 'text',
payload: {
cap: "{trigger.data.cap otherwise ''}",
is_private: false,
is_transferrable: true,
auto_destroy: "{!!trigger.data.auto_destroy}"
fixed_denominations: false,
issued_by_definer_only: "{!!trigger.data.issued_by_definer_only}",
cosigned_by_definer: false,
spender_attested: false,
attestors: [
"{trigger.data.attestor1 otherwise ''}",
"{trigger.data.attestor2 otherwise ''}",
"{trigger.data.attestor3 otherwise ''}",
]
}
}
]
}]
上面自治代理根据触发交易中的data
数据参数来定义新的资产。
其中,有些第一次出现的运算符:
!
是逻辑非运算;otherwise
是条件运算符,当第一个参数为真时(除了false
,0
和空字符串之外)返回第一个参数,否则返回第二个参数;- 当对象或数组值的计算结果为空字符串时,将删除相应的对象或数组元素。这意味着如果未传递
cap
参数,则payload
中的cap
为空字符串,因此将从资产定义中删除,并定义具有无限供应的资产; - 值为空的数组或对象字段也将被删除。因此,如果没有设置
attestor1
,attestor2
,attestor3
字段,则attestors
数组将变为空,并将从最终定义中排除。
状态查询
自治代理的行为还可以依赖于描述整个账本的各种状态变量。
余额
['autonomous agent', {
messages: [
{
app: 'payment',
payload: {
asset: 'base',
outputs: [
{address: "{trigger.address}", amount: "{ round(balance[base]/2) }"}
]
}
}
]
}]
其它自治代理(非普通地址)的余额也可以查询:
['autonomous agent', {
messages: [
{
app: 'payment',
payload: {
asset: 'base',
outputs: [
{
address: `{ (balance["JVUJQ7OPBJ7ZLZ57TTNFJIC3EW7AE2RY"][base] < 1e6) ? "JVUJQ7OPBJ7ZLZ57TTNFJIC3EW7AE2RY" : trigger.address }`,
amount: `{ trigger.output[[asset=base]] - 1000 }`
}
]
}
}
]
}]
如果余额小于1,000,000字节,则上述自治代理将收到的任何付款(减小1000字节手续费)转发给另一个自治代理JVUJQ7OPBJ7ZLZ57TTNFJIC3EW7AE2RY
,否则返回发送方。
数据订阅
['autonomous agent', {
messages: [
{
app: 'payment',
payload: {
asset: 'base',
outputs: [
{
address: `{(data_feed[[oracles='TKT4UESIKTTRALRRLWS4SENSTJX6ODCW', feed_name='BROOKLYNNETS_CHARLOTTEHORNETS_2019-07-21']] == 'CHARLOTTEHORNETS') ? "7XZSBB32I5XPIAVWUYCABKO67TZLHKZW" : "FDZVVIOJZZVFAP6FLW6GMPDYYHI6W4JG"}`
}
]
}
}
]
}]
如果夏洛特黄蜂队获胜,则上述自治代理支付给7XZSBB32I5XPIAVWUYCABKO67TZLHKZW
,否则支付给FDZVVIOJZZVFAP6FLW6GMPDYYHI6W4JG
。TKT4UESIKTTRALRRLWS4SENSTJX6ODCW
是发布体育赛事结果的预言机地址。
在预言机发布数据之前,任何触发自治代理的交易都将失败并回滚。
请注意,输出中的amount
字段被省略,这意味着将支付自治代理的所有余额。
认证
['autonomous agent', {
messages: [
{
app: 'payment',
payload: {
asset: 'base',
outputs: [
{
address: "{trigger.address}",
amount: `{ (attestation[[attestors='JEDZYC2HMGDBIDQKG3XSTXUSHMCBK725', address=trigger.address]].reputation >= 50) ? 50000 : 0 }`
}
]
}
}
]
}]
如果发送方地址由Steem认证机器人JEDZYC2HMGDBIDQKG3XSTXUSHMCBK725
认证并且他的Steem声誉至少为50,则上述自治代理向发送方支付50000字节,否则支付0字节。尝试支付0字节金额会导致该输出被删除。由于这种情况下唯一输出被删除,因此对生成的响应交易不会产生任何影响,也就根本不会创建它。
变量
状态变量
自治代理可以在多次调用之间保存自己的状态。使用var
来访问和分配状态变量:
['autonomous agent', {
messages: [
{
if: `{ var['previous_address'] }`,
app: 'payment',
payload: {
asset: 'base',
outputs: [
{
address: "{ var['previous_address'] }",
amount: `{ trigger.output[[asset=base]] - 1000 }`
}
]
}
},
{
app: "state",
state: `{
var['previous_address'] = trigger.address;
}`
}
]
}]
上述自治代理保存之前发送触发交易的地址,并在下一次调用时将收到的资金(减去1000字节手续费)发送给前一个用户。
只有在消息类型为state
的特殊消息中才允许分配状态变量。此消息必须具有一个名为state
的字段,该字段是一组Oscript语句,允许这些语句分配状态变量。状态消息不会被包含在最终响应交易中,它只能通过修改状态变量来影响自治代理的状态。
当访问未初始化的var
变量,其值返回false
。
上面的示例在第一条消息中有一个if
字段。 当var ['previous_address']
尚未初始化(第一次调用)时,if
的计算结果为false
,第一条消息内容不出现在响应消息中。
其它自治代理的状态变量也可以读取,但是不能写入:
var['JVUJQ7OPBJ7ZLZ57TTNFJIC3EW7AE2RY']['var_name']
局部变量
在脚本执行过程中,局部变量可以用来存储中间结果:
['autonomous agent', {
messages: [
{
app: 'payment',
payload: {
asset: 'base',
outputs: [
{
address: "{ trigger.address }",
amount: `{
$amount1 = trigger.output[[asset=base]] - 5000;
$amount2 = round(balance[base] * 0.9);
max($amount1, $amount2)
}`
}
]
}
}
]
}]
局部变量名称以$
为前缀。 与状态变量不同,它们不会在多次调用之间保存。
为了更容易确定局部变量的值,有一个在其他语言中不常见的限制:单一赋值规则。局部变量只能赋值一次,并且该值在脚本结束前保持不变。尝试重新分配值将使脚本失败并使自治代理回退。
条件字段
正如我们之前在状态变量示例中看到的那样,自治代理生成的响应交易的某些部分可以通过添加if
字段进行判断。如果if
中的Oscript语句计算为true
或任何真值(除false
,0
和空字符串之外的任何内容),则其后面的对象将包含在最终响应交易中,否则将被排除。
if
字段本身当然总是会从最终响应交易中删除。
初始化字段
与if
字段一样,任何对象都可以有一个包含初始化代码的init
字段:
['autonomous agent', {
messages: [
{
if: `{ trigger.data.pay }`,
init: `{
$amount1 = trigger.output[[asset=base]] - 5000;
$amount2 = round(balance[base] * 0.9);
}`,
app: 'payment',
payload: {
asset: 'base',
outputs: [
{
address: "{ trigger.address }",
amount: `{
max($amount1, $amount2)
}`
}
]
}
}
]
}]
与大多数其他脚本不同,init
代码只包含语句,它不返回任何值。 另一个例外是上面状态变量中描述的状态消息,它也只是语句。 所有其他脚本可以包含0个或更多语句,但必须以表达式(不带;
)结束,其值将成脚本的返回值。
init
代码在if
之后(如果它存在并且当然为真),且在其他语句立即执行。它通常用于设置稍后使用的局部变量。
条件和初始化字段中的局部变量
在if
和init
中设置的局部变量可以用于当前和嵌套对象的所有其他脚本中。在上面的示例中,$amount1
和$amount2
在消息对象的init
中设置,并且它们用在此消息的payload/outputs/output-0/amount
中。
在if
中设置的局部变量也可在相应的init
中使用。
两种类型的脚本
如上所述,有两种类型的脚本:仅包含语句的和具有返回值的。
只有两个仅包含语句的脚本:init
和state
脚本。这是我们之前见过的init
脚本的示例:
{
init: `{
$amount1 = trigger.output[[asset=base]] - 5000;
$amount2 = round(balance[base] * 0.9);
}`,
...
}
所有其它的脚本都具有返回值。它们要不包含一个单独的表达式:
{
address: "{ trigger.address }",
...
}
或者一组语句,并在结尾加上一个表达式:
{
amount: `{
$amount1 = trigger.output[[asset=base]] - 5000;
$amount2 = round(balance[base] * 0.9);
max($amount1, $amount2)
}`,
...
}
表达式的计算结果就是脚本的返回值。
匹配字段
当你需要根据输入和环境参数在几个互斥选项中进行代码执行时,可以使用cases
:
['autonomous agent', {
messages: {
cases: [
{
if: "{trigger.data.define}",
messages: [
// first version of messages
....
]
},
{
if: "{trigger.data.issue}",
init: `{
$amount = trigger.output[[asset=base]];
}`,
messages: [
// second version of messages
....
]
},
{
messages: [
// default version of messages
....
]
}
]
}
}]
常规messages
数组被替换为一个名为cases
的数组。cases
是一个列出几个互斥选项的数组。除了最后一个元素之外的每个选项都必须有一个if
字段。 if
为真的第一个选项定义了消息字段的消息。
各个选项中的if
条件可能不是互斥的,但cases
列表中前面列出的选项优先级更高。最终只会有一个选项被选择。
在上面的示例中,如果trigger.data
没有define
字段但有issue
字段,则选择第二个选项,消息对象将变为:
['autonomous agent', {
messages: [
// second version of messages
....
]
}]
cases
可以嵌套。以上关于if
,init
和局部变量的所有内容也适用于cases
。
条件语句
上面的例子展示了如何在响应消息模板的不同部分分支执行。
使用if/else
可以分支执行Oscript代码本身:
if (var['started']){
$amount = balance[base];
$current_winner = var['current_winner'];
}
else{
$amount = trigger.output[[asset=base]];
$current_winner = trigger.address;
}
返回语句
如果要中断脚本的执行并返回(可以返回或不返回值),请使用return
语句。
以下是在具有返回值的脚本中使用return
的示例,它必须返回一个值:
$amount = trigger.output[[asset=base]];
if ($amount > 1e6)
return $amount;
// here comes some other code that will not be executed if $amount > 1e6
在仅包含语句的脚本(例如init
)中,return
不返回任何值,它只是中断脚本:
$amount = trigger.output[[asset=base]];
if ($amount > 1e6)
return;
// here comes some other code that will not be executed if $amount > 1e6
回滚语句
如果自治代理发现错误情况并希望提前停止执行并回滚交易,请使用bounce
:
$maturity = var['maturity_timestamp'];
if (timestamp < $maturity)
bounce("too early");
在此示例中,timestamp
是当前时间戳(自1970年1月1日00:00:00 UTC以来的秒数)。
所有应用的更改都将被回滚,任何收到的资金将被退回给发送方(减去回滚费用)。
文章目录
版权所有。发布者:Alan During,转载请注明出处:https://bbfans.org/2019/07/21/obyte-aa-guide1/