@wowok/agent-mcp 2.3.1 → 2.3.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +65 -51
- package/dist/schema/call/guard.d.ts +149 -101
- package/dist/schema/call/guard.js +61 -8
- package/dist/schema/query/index.js +21 -21
- package/dist/schema/utils/guard-parser.js +18 -0
- package/dist/schema/utils/guard-query-utils.d.ts +6 -0
- package/dist/schema/utils/guard-query-utils.js +12 -1
- package/dist/schema-query/index.js +45 -0
- package/dist/schemas/guard-node-examples.md +199 -0
- package/dist/schemas/guard-node-schema.json +918 -0
- package/dist/schemas/index.json +1 -1
- package/dist/schemas/onchain_operations_guard.schema.json +12 -14
- package/package.json +3 -4
|
@@ -2,28 +2,81 @@ import { z } from 'zod';
|
|
|
2
2
|
import { CallEnvSchema, NamedObjectSchema, SubmissionCallSchema } from './base.js';
|
|
3
3
|
import { GuardNodeSchema } from '../query/index.js';
|
|
4
4
|
import { DescriptionSchema, GuardTableItemBaseSchema, NameOrAddressSchema } from '../common/index.js';
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
5
|
+
import { isBoolReturningQuery } from '../utils/guard-query-utils.js';
|
|
6
|
+
import { ValueType } from '@wowok/wowok';
|
|
7
|
+
export const CallGuard_RootSchema = z.union([
|
|
8
|
+
GuardNodeSchema.refine((node) => {
|
|
9
|
+
const allowedTypes = [
|
|
10
|
+
'logic_as_u256_greater', 'logic_as_u256_lesser', 'logic_as_u256_equal',
|
|
11
|
+
'logic_as_u256_greater_or_equal', 'logic_as_u256_lesser_or_equal',
|
|
12
|
+
'logic_equal', 'logic_not', 'logic_and', 'logic_or',
|
|
13
|
+
'logic_string_contains', 'logic_string_nocase_contains', 'logic_string_nocase_equal',
|
|
14
|
+
'calc_string_contains', 'calc_string_nocase_contains', 'calc_string_nocase_equal',
|
|
15
|
+
'vec_contains_bool', 'vec_contains_address', 'vec_contains_string',
|
|
16
|
+
'vec_contains_string_nocase', 'vec_contains_number',
|
|
17
|
+
'query_reward_record_exists',
|
|
18
|
+
'query',
|
|
19
|
+
'identifier'
|
|
20
|
+
];
|
|
21
|
+
if (!allowedTypes.includes(node.type)) {
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
if (node.type === 'query') {
|
|
25
|
+
const queryNode = node;
|
|
26
|
+
if (!isBoolReturningQuery(queryNode.query)) {
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return true;
|
|
31
|
+
}, {
|
|
32
|
+
message: "Guard root node must be an operation that returns Bool. Allowed types: logic_*, calc_string_contains, calc_string_nocase_*, vec_contains_*, reward_record_exist, identifier (must reference Bool type in table). For 'query' type, ONLY queries that return Bool are allowed. Use 'wowok_buildin_info' tool with {\"info\": \"guard instructions\", \"filter\": {\"returnType\": \"Bool\", \"scope\": \"object query\"}} to list all Bool-returning queries. Other nodes (calc_number_*, context, non-Bool queries) must be wrapped with a logic operation."
|
|
33
|
+
}).describe("Guard computational tree root node. MUST be an operation that returns a Bool value (pass/fail). For 'query' type, the specific query must return Bool. For 'identifier' type, the referenced table item must have value_type=Bool."),
|
|
10
34
|
z.object({
|
|
11
35
|
type: z.literal("file"),
|
|
12
36
|
file_path: z.string().describe("Path to JSON or Markdown file containing Guard definition. File can define: namedNew, description, table, root, rely."),
|
|
13
37
|
format: z.enum(["json", "markdown"]).optional().default("json")
|
|
14
38
|
.describe("File format: 'json' or 'markdown'. Default is 'json'."),
|
|
15
39
|
}).strict(),
|
|
16
|
-
]).describe("Guard root definition. Either provide a direct
|
|
40
|
+
]).describe("Guard root definition. Either provide a direct GuardNode tree (must be a logical operation returning Bool) or reference a file to load. When using file type, fields defined in the schema (namedNew, description, table, rely) will OVERRIDE the corresponding fields in the file.");
|
|
41
|
+
const isBoolValueType = (valueType) => {
|
|
42
|
+
if (typeof valueType === 'number') {
|
|
43
|
+
return valueType === ValueType.Bool;
|
|
44
|
+
}
|
|
45
|
+
if (typeof valueType === 'string') {
|
|
46
|
+
return valueType.toLowerCase() === 'bool' || valueType === '0';
|
|
47
|
+
}
|
|
48
|
+
return false;
|
|
49
|
+
};
|
|
17
50
|
export const CallGuard_DataSchema = z.object({
|
|
18
51
|
namedNew: NamedObjectSchema.optional().describe("Name and optional tags for the new Guard object. Set 'onChain: true' to create a public on-chain identity. When using root.type='file', this field OVERRIDES namedNew in the file."),
|
|
19
52
|
description: DescriptionSchema.optional().describe("Guard description. When using root.type='file', this field OVERRIDES description in the file."),
|
|
20
53
|
table: z.array(GuardTableItemBaseSchema).optional().describe("Data table of the Guard object. When using root.type='file', this field OVERRIDES table in the file."),
|
|
21
|
-
root: CallGuard_RootSchema.describe("Root definition: either a direct
|
|
54
|
+
root: CallGuard_RootSchema.describe("Root definition: either a direct GuardNode (must be a logical operation returning Bool) or a file reference (type='file'). When type='file', the file can define all Guard fields (namedNew, description, table, root, rely), and schema fields override file content."),
|
|
22
55
|
rely: z.object({
|
|
23
56
|
guards: z.array(NameOrAddressSchema).describe("List of dependent Guard object IDs or names."),
|
|
24
57
|
logic_or: z.boolean().optional().describe("Whether to use logical OR operator."),
|
|
25
58
|
}).optional().describe("All Guard objects that the new Guard object depends on. If logic_or is true, the execution result of the new Guard object is the logical OR of the execution results of all dependent Guard objects; otherwise, it is logical AND. When using root.type='file', this field OVERRIDES rely in the file."),
|
|
26
|
-
}).strict().
|
|
59
|
+
}).strict().refine((data) => {
|
|
60
|
+
if (typeof data.root === 'object' && data.root !== null && 'type' in data.root) {
|
|
61
|
+
const rootObj = data.root;
|
|
62
|
+
if (rootObj.type === 'file') {
|
|
63
|
+
return true;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
const rootNode = data.root;
|
|
67
|
+
if (rootNode.type === 'identifier' && rootNode.identifier !== undefined) {
|
|
68
|
+
const tableItem = data.table?.find(item => item.identifier === rootNode.identifier);
|
|
69
|
+
if (!tableItem) {
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
if (!isBoolValueType(tableItem.value_type)) {
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return true;
|
|
77
|
+
}, {
|
|
78
|
+
message: "Invalid Guard root: When using 'identifier' as root, the referenced table item must exist and have value_type='Bool' (or 0)."
|
|
79
|
+
}).describe("On-chain Guard creation. IMPORTANT: All defined data (include all submitted data) must be defined in the 'table' field. USAGE: Set 'namedNew' field with {name, tags?, onChain?} to name the new Guard. The Guard is immutable once created. Define the validation logic in 'root' field. When root.type='file', the file can contain all Guard fields, and any fields defined in the schema will OVERRIDE the file content.");
|
|
27
80
|
export const CallGuard_InputSchema = z.object({
|
|
28
81
|
data: CallGuard_DataSchema.describe("Guard data definition."),
|
|
29
82
|
env: CallEnvSchema.optional(),
|
|
@@ -625,58 +625,58 @@ export const GuardNodeSchema = z.lazy(() => z.discriminatedUnion('type', [
|
|
|
625
625
|
identifier: GuardIdentifierSchema,
|
|
626
626
|
}).strict().describe("Reference to the Reward object in the Guard table (must be Address type)."),
|
|
627
627
|
find: z.enum(['first', 'last']).describe("'first' finds the earliest matching record, 'last' finds the most recent matching record."),
|
|
628
|
-
recipient:
|
|
628
|
+
recipient: z.object({
|
|
629
|
+
identifier: GuardIdentifierSchema,
|
|
630
|
+
}).strict().optional().describe("Optional: Identifier reference to the recipient address in the Guard table (must be Address type). If not specified, uses the transaction signer address."),
|
|
629
631
|
where: z.object({
|
|
630
632
|
guard: z.string().optional().describe("Optional filter: Guard object address constant to match."),
|
|
631
633
|
timeMin: z.string().optional().describe("Optional filter: minimum timestamp (inclusive) for record creation time. Use string to represent U64 value."),
|
|
632
634
|
timeMax: z.string().optional().describe("Optional filter: maximum timestamp (inclusive) for record creation time. Use string to represent U64 value."),
|
|
633
635
|
amountMin: z.string().optional().describe("Optional filter: minimum reward amount (inclusive). Use string to represent U64 value."),
|
|
634
636
|
amountMax: z.string().optional().describe("Optional filter: maximum reward amount (inclusive). Use string to represent U64 value."),
|
|
635
|
-
storeFromId: z.
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
z.object({ eq: z.string().describe("Address string to match") })
|
|
639
|
-
]).optional().describe("Optional filter for store_from_id field: 'exists' means any value exists, 'not_exists' means no value, {eq: string} matches specific address."),
|
|
637
|
+
storeFromId: z.object({
|
|
638
|
+
identifier: GuardIdentifierSchema,
|
|
639
|
+
}).strict().optional().describe("Optional filter: identifier reference to the store_from_id address in the Guard table (must be Address type). If not specified, the store_from_id field is ignored during matching. The server resolves this identifier to the actual address from the Guard table constants."),
|
|
640
640
|
}).strict().describe("Filter conditions for the reward record query."),
|
|
641
|
-
}).strict().describe("Returns U64. Finds the index of the first or last reward record matching the specified recipient and filter conditions. Returns the record index (U64) if found. Throws error if no matching record is found (Guard validation fails)."),
|
|
641
|
+
}).strict().describe("Returns U64. Finds the index of the first or last reward record matching the specified recipient and filter conditions. Returns the record index (U64) if found. Throws error if no matching record is found (Guard validation fails). If recipient is not specified, uses the transaction signer address."),
|
|
642
642
|
z.object({
|
|
643
643
|
type: z.literal('query_reward_record_count'),
|
|
644
644
|
object: z.object({
|
|
645
645
|
identifier: GuardIdentifierSchema,
|
|
646
646
|
}).strict().describe("Reference to the Reward object in the Guard table (must be Address type)."),
|
|
647
|
-
recipient:
|
|
647
|
+
recipient: z.object({
|
|
648
|
+
identifier: GuardIdentifierSchema,
|
|
649
|
+
}).strict().optional().describe("Optional: Identifier reference to the recipient address in the Guard table (must be Address type). If not specified, uses the transaction signer address."),
|
|
648
650
|
where: z.object({
|
|
649
651
|
guard: z.string().optional().describe("Optional filter: Guard object address constant to match."),
|
|
650
652
|
timeMin: z.string().optional().describe("Optional filter: minimum timestamp (inclusive) for record creation time. Use string to represent U64 value."),
|
|
651
653
|
timeMax: z.string().optional().describe("Optional filter: maximum timestamp (inclusive) for record creation time. Use string to represent U64 value."),
|
|
652
654
|
amountMin: z.string().optional().describe("Optional filter: minimum reward amount (inclusive). Use string to represent U64 value."),
|
|
653
655
|
amountMax: z.string().optional().describe("Optional filter: maximum reward amount (inclusive). Use string to represent U64 value."),
|
|
654
|
-
storeFromId: z.
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
z.object({ eq: z.string().describe("Address string to match") })
|
|
658
|
-
]).optional().describe("Optional filter for store_from_id field: 'exists' means any value exists, 'not_exists' means no value, {eq: string} matches specific address."),
|
|
656
|
+
storeFromId: z.object({
|
|
657
|
+
identifier: GuardIdentifierSchema,
|
|
658
|
+
}).strict().optional().describe("Optional filter: identifier reference to the store_from_id address in the Guard table (must be Address type). If not specified, the store_from_id field is ignored during matching. The server resolves this identifier to the actual address from the Guard table constants."),
|
|
659
659
|
}).strict().describe("Filter conditions for the reward record count query."),
|
|
660
|
-
}).strict().describe("Returns U64. Counts the number of reward records matching the specified recipient and filter conditions."),
|
|
660
|
+
}).strict().describe("Returns U64. Counts the number of reward records matching the specified recipient and filter conditions. If recipient is not specified, uses the transaction signer address."),
|
|
661
661
|
z.object({
|
|
662
662
|
type: z.literal('query_reward_record_exists'),
|
|
663
663
|
object: z.object({
|
|
664
664
|
identifier: GuardIdentifierSchema,
|
|
665
665
|
}).strict().describe("Reference to the Reward object in the Guard table (must be Address type)."),
|
|
666
|
-
recipient:
|
|
666
|
+
recipient: z.object({
|
|
667
|
+
identifier: GuardIdentifierSchema,
|
|
668
|
+
}).strict().optional().describe("Optional: Identifier reference to the recipient address in the Guard table (must be Address type). If not specified, uses the transaction signer address."),
|
|
667
669
|
where: z.object({
|
|
668
670
|
guard: z.string().optional().describe("Optional filter: Guard object address constant to match."),
|
|
669
671
|
timeMin: z.string().optional().describe("Optional filter: minimum timestamp (inclusive) for record creation time. Use string to represent U64 value."),
|
|
670
672
|
timeMax: z.string().optional().describe("Optional filter: maximum timestamp (inclusive) for record creation time. Use string to represent U64 value."),
|
|
671
673
|
amountMin: z.string().optional().describe("Optional filter: minimum reward amount (inclusive). Use string to represent U64 value."),
|
|
672
674
|
amountMax: z.string().optional().describe("Optional filter: maximum reward amount (inclusive). Use string to represent U64 value."),
|
|
673
|
-
storeFromId: z.
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
z.object({ eq: z.string().describe("Address string to match") })
|
|
677
|
-
]).optional().describe("Optional filter for store_from_id field: 'exists' means any value exists, 'not_exists' means no value, {eq: string} matches specific address."),
|
|
675
|
+
storeFromId: z.object({
|
|
676
|
+
identifier: GuardIdentifierSchema,
|
|
677
|
+
}).strict().optional().describe("Optional filter: identifier reference to the store_from_id address in the Guard table (must be Address type). If not specified, the store_from_id field is ignored during matching. The server resolves this identifier to the actual address from the Guard table constants."),
|
|
678
678
|
}).strict().describe("Filter conditions for the reward record existence query."),
|
|
679
|
-
}).strict().describe("Returns Bool. Returns true if at least one reward record exists matching the specified recipient and filter conditions, otherwise false."),
|
|
679
|
+
}).strict().describe("Returns Bool. Returns true if at least one reward record exists matching the specified recipient and filter conditions, otherwise false. If recipient is not specified, uses the transaction signer address."),
|
|
680
680
|
z.object({
|
|
681
681
|
type: z.literal('query_progress_history_find'),
|
|
682
682
|
object: z.object({
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { GuardNodeSchema } from '../query/index.js';
|
|
2
2
|
import { CallGuard_DataSchema } from '../call/guard.js';
|
|
3
|
+
import { isBoolReturningQuery, BOOL_RETURNING_QUERIES } from './guard-query-utils.js';
|
|
3
4
|
function detectFormat(text) {
|
|
4
5
|
const trimmed = text.trim();
|
|
5
6
|
if (trimmed.startsWith('{') || trimmed.startsWith('[')) {
|
|
@@ -344,6 +345,23 @@ export function parseGuardFromText(text) {
|
|
|
344
345
|
}
|
|
345
346
|
const result = CallGuard_DataSchema.safeParse(parsedData);
|
|
346
347
|
if (result.success) {
|
|
348
|
+
const root = result.data.root;
|
|
349
|
+
if (typeof root === 'object' && root !== null && 'type' in root) {
|
|
350
|
+
const rootNode = root;
|
|
351
|
+
if (rootNode.type === 'query' && rootNode.query !== undefined) {
|
|
352
|
+
if (!isBoolReturningQuery(rootNode.query)) {
|
|
353
|
+
const queryStr = typeof rootNode.query === 'number' ? `ID ${rootNode.query}` : `"${rootNode.query}"`;
|
|
354
|
+
const boolQueryExamples = BOOL_RETURNING_QUERIES.slice(0, 5).map(q => `"${q.name}"`).join(', ');
|
|
355
|
+
return {
|
|
356
|
+
success: false,
|
|
357
|
+
errors: [{
|
|
358
|
+
message: `Invalid Guard root: query ${queryStr} does not return Bool. Only queries that return Bool can be used as root. Examples: ${boolQueryExamples}... Use guard2file to see all Bool-returning queries.`,
|
|
359
|
+
path: '/root/query',
|
|
360
|
+
}]
|
|
361
|
+
};
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
}
|
|
347
365
|
return { success: true, data: result.data, errors: [] };
|
|
348
366
|
}
|
|
349
367
|
for (const issue of result.error.issues) {
|
|
@@ -3,3 +3,9 @@ export declare const isValidGuardQueryName: (name: string) => boolean;
|
|
|
3
3
|
export declare const isValidGuardQueryIdOrName: (input: number | string) => boolean;
|
|
4
4
|
export declare const getGuardQueryByIdOrName: (input: number | string) => import("@wowok/wowok").GuardQuery | undefined;
|
|
5
5
|
export declare const GUARD_QUERY_NAMES: string[];
|
|
6
|
+
export declare const isBoolReturningQuery: (input: number | string) => boolean;
|
|
7
|
+
export declare const BOOL_RETURNING_QUERIES: {
|
|
8
|
+
id: number;
|
|
9
|
+
name: string;
|
|
10
|
+
description: string;
|
|
11
|
+
}[];
|
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
import { GUARDQUERY } from "@wowok/wowok";
|
|
1
|
+
import { GUARDQUERY, ValueType } from "@wowok/wowok";
|
|
2
2
|
const validGuardQueryIds = new Set(GUARDQUERY.map(item => item.id));
|
|
3
3
|
const validGuardQueryNames = new Set(GUARDQUERY.map(item => item.name.toLowerCase()));
|
|
4
|
+
const boolReturningQueryIds = new Set(GUARDQUERY.filter(item => item.return === ValueType.Bool).map(item => item.id));
|
|
5
|
+
const boolReturningQueryNames = new Set(GUARDQUERY.filter(item => item.return === ValueType.Bool).map(item => item.name.toLowerCase()));
|
|
4
6
|
export const isValidGuardQueryId = (id) => {
|
|
5
7
|
return validGuardQueryIds.has(id);
|
|
6
8
|
};
|
|
@@ -20,3 +22,12 @@ export const getGuardQueryByIdOrName = (input) => {
|
|
|
20
22
|
return GUARDQUERY.find(item => item.name.toLowerCase() === input.toLowerCase());
|
|
21
23
|
};
|
|
22
24
|
export const GUARD_QUERY_NAMES = GUARDQUERY.map(item => item.name);
|
|
25
|
+
export const isBoolReturningQuery = (input) => {
|
|
26
|
+
if (typeof input === 'number') {
|
|
27
|
+
return boolReturningQueryIds.has(input);
|
|
28
|
+
}
|
|
29
|
+
return boolReturningQueryNames.has(input.toLowerCase());
|
|
30
|
+
};
|
|
31
|
+
export const BOOL_RETURNING_QUERIES = GUARDQUERY
|
|
32
|
+
.filter(item => item.return === ValueType.Bool)
|
|
33
|
+
.map(item => ({ id: item.id, name: item.name, description: item.description }));
|
|
@@ -38,6 +38,51 @@ export function getSchema(name) {
|
|
|
38
38
|
return null;
|
|
39
39
|
}
|
|
40
40
|
const content = readFileSync(schemaPath, "utf-8");
|
|
41
|
+
const schema = JSON.parse(content);
|
|
42
|
+
return resolveSchemaRefs(schema);
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
function resolveSchemaRefs(schema, visited = new Set()) {
|
|
49
|
+
if (!schema || typeof schema !== "object") {
|
|
50
|
+
return schema;
|
|
51
|
+
}
|
|
52
|
+
if (Array.isArray(schema)) {
|
|
53
|
+
return schema.map(item => resolveSchemaRefs(item, visited));
|
|
54
|
+
}
|
|
55
|
+
const result = {};
|
|
56
|
+
for (const [key, value] of Object.entries(schema)) {
|
|
57
|
+
if (key === "$ref" && typeof value === "string") {
|
|
58
|
+
const refSchema = loadRefSchema(value, visited);
|
|
59
|
+
if (refSchema) {
|
|
60
|
+
Object.assign(result, resolveSchemaRefs(refSchema, visited));
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
else if (typeof value === "object") {
|
|
64
|
+
result[key] = resolveSchemaRefs(value, visited);
|
|
65
|
+
}
|
|
66
|
+
else {
|
|
67
|
+
result[key] = value;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return result;
|
|
71
|
+
}
|
|
72
|
+
function loadRefSchema(ref, visited) {
|
|
73
|
+
if (ref.startsWith("#")) {
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
if (visited.has(ref)) {
|
|
77
|
+
return { description: `Circular reference to ${ref}` };
|
|
78
|
+
}
|
|
79
|
+
visited.add(ref);
|
|
80
|
+
try {
|
|
81
|
+
const refPath = join(SCHEMAS_DIR, ref);
|
|
82
|
+
if (!existsSync(refPath)) {
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
const content = readFileSync(refPath, "utf-8");
|
|
41
86
|
return JSON.parse(content);
|
|
42
87
|
}
|
|
43
88
|
catch {
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
# GuardNode Usage Guide
|
|
2
|
+
|
|
3
|
+
This document provides practical examples of GuardNode usage for AI agents.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
A GuardNode is a computational tree node that represents an operation returning a typed value. Nodes can be nested to build complex validation logic. The root node of a Guard MUST return a `Bool` value (pass/fail).
|
|
8
|
+
|
|
9
|
+
## Common Patterns
|
|
10
|
+
|
|
11
|
+
### Pattern 1: Verify Transaction Signer is Order Owner
|
|
12
|
+
|
|
13
|
+
```json
|
|
14
|
+
{
|
|
15
|
+
"type": "logic_equal",
|
|
16
|
+
"nodes": [
|
|
17
|
+
{ "type": "context", "context": "Signer" },
|
|
18
|
+
{
|
|
19
|
+
"type": "query",
|
|
20
|
+
"query": 1562,
|
|
21
|
+
"object": { "identifier": 0 },
|
|
22
|
+
"parameters": []
|
|
23
|
+
}
|
|
24
|
+
]
|
|
25
|
+
}
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
**Explanation:**
|
|
29
|
+
- `context: Signer` returns the transaction sender's address
|
|
30
|
+
- `query: 1562` gets the order owner (order.owner query)
|
|
31
|
+
- `logic_equal` compares the two addresses
|
|
32
|
+
|
|
33
|
+
### Pattern 2: Verify Order Progress Node
|
|
34
|
+
|
|
35
|
+
```json
|
|
36
|
+
{
|
|
37
|
+
"type": "logic_equal",
|
|
38
|
+
"nodes": [
|
|
39
|
+
{
|
|
40
|
+
"type": "query",
|
|
41
|
+
"query": 1253,
|
|
42
|
+
"object": { "identifier": 0, "convert_witness": 100 },
|
|
43
|
+
"parameters": []
|
|
44
|
+
},
|
|
45
|
+
{ "type": "identifier", "identifier": 1 }
|
|
46
|
+
]
|
|
47
|
+
}
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
**Explanation:**
|
|
51
|
+
- `query: 1253` gets the current progress node name
|
|
52
|
+
- `convert_witness: 100` (TypeOrderProgress) converts the object for progress query
|
|
53
|
+
- Compares with the expected node name stored at identifier 1
|
|
54
|
+
|
|
55
|
+
### Pattern 3: Verify Reward Not Claimed
|
|
56
|
+
|
|
57
|
+
```json
|
|
58
|
+
{
|
|
59
|
+
"type": "logic_not",
|
|
60
|
+
"node": {
|
|
61
|
+
"type": "query_reward_record_exists",
|
|
62
|
+
"object": { "identifier": 0 },
|
|
63
|
+
"recipient": { "identifier": 1 },
|
|
64
|
+
"where": {}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
**Explanation:**
|
|
70
|
+
- `query_reward_record_exists` checks if a reward record exists for the recipient
|
|
71
|
+
- `logic_not` inverts the result (returns true if NO record exists)
|
|
72
|
+
- This verifies the reward has NOT been claimed
|
|
73
|
+
|
|
74
|
+
### Pattern 4: Combined Validation (Reward Guard)
|
|
75
|
+
|
|
76
|
+
```json
|
|
77
|
+
{
|
|
78
|
+
"type": "logic_and",
|
|
79
|
+
"nodes": [
|
|
80
|
+
{
|
|
81
|
+
"type": "logic_equal",
|
|
82
|
+
"nodes": [
|
|
83
|
+
{
|
|
84
|
+
"type": "query",
|
|
85
|
+
"query": 1253,
|
|
86
|
+
"object": { "identifier": 0, "convert_witness": 100 },
|
|
87
|
+
"parameters": []
|
|
88
|
+
},
|
|
89
|
+
{ "type": "identifier", "identifier": 1 }
|
|
90
|
+
]
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
"type": "logic_equal",
|
|
94
|
+
"nodes": [
|
|
95
|
+
{ "type": "context", "context": "Signer" },
|
|
96
|
+
{
|
|
97
|
+
"type": "query",
|
|
98
|
+
"query": 1562,
|
|
99
|
+
"object": { "identifier": 0 },
|
|
100
|
+
"parameters": []
|
|
101
|
+
}
|
|
102
|
+
]
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
"type": "logic_not",
|
|
106
|
+
"node": {
|
|
107
|
+
"type": "query_reward_record_exists",
|
|
108
|
+
"object": { "identifier": 2 },
|
|
109
|
+
"recipient": { "identifier": 3 },
|
|
110
|
+
"where": {}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
]
|
|
114
|
+
}
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
**Explanation:**
|
|
118
|
+
This is a complete Reward Guard that verifies:
|
|
119
|
+
1. Order is at expected progress node (e.g., "delivered")
|
|
120
|
+
2. Transaction signer is the order owner
|
|
121
|
+
3. Order has NOT claimed the reward yet
|
|
122
|
+
|
|
123
|
+
## Query Instruction Reference
|
|
124
|
+
|
|
125
|
+
| Query ID | Name | Description | Parameters | Returns |
|
|
126
|
+
|----------|------|-------------|------------|---------|
|
|
127
|
+
| 1253 | order.progress.node | Get current progress node name | None | String |
|
|
128
|
+
| 1562 | order.owner | Get order owner address | None | Address |
|
|
129
|
+
| 1563 | order.service | Get order service address | None | Address |
|
|
130
|
+
| 1564 | order.payer | Get order payer address | None | Address |
|
|
131
|
+
|
|
132
|
+
## Witness Types
|
|
133
|
+
|
|
134
|
+
| Witness ID | Name | Description |
|
|
135
|
+
|------------|------|-------------|
|
|
136
|
+
| 100 | TypeOrderProgress | Access order progress data |
|
|
137
|
+
| 200 | TypeOrder | Access order data |
|
|
138
|
+
|
|
139
|
+
## Node Return Types
|
|
140
|
+
|
|
141
|
+
### Logic Nodes (Return Bool)
|
|
142
|
+
- `logic_and`, `logic_or`, `logic_not`
|
|
143
|
+
- `logic_equal`, `logic_as_u256_equal`
|
|
144
|
+
- `logic_as_u256_greater`, `logic_as_u256_lesser`
|
|
145
|
+
- `logic_as_u256_greater_or_equal`, `logic_as_u256_lesser_or_equal`
|
|
146
|
+
- `logic_string_contains`, `logic_string_nocase_contains`, `logic_string_nocase_equal`
|
|
147
|
+
|
|
148
|
+
### Calculation Nodes (Return Numeric)
|
|
149
|
+
- `calc_number_add`, `calc_number_subtract`, `calc_number_multiply`, `calc_number_divide`, `calc_number_mod` → U256
|
|
150
|
+
- `calc_string_length` → U64
|
|
151
|
+
- `calc_string_contains`, `calc_string_nocase_contains`, `calc_string_nocase_equal` → Bool
|
|
152
|
+
- `calc_string_indexof`, `calc_string_nocase_indexof` → U64
|
|
153
|
+
|
|
154
|
+
### Conversion Nodes
|
|
155
|
+
- `convert_number_address` → Address
|
|
156
|
+
- `convert_address_number` → U64
|
|
157
|
+
- `convert_number_string` → String
|
|
158
|
+
- `convert_string_number` → U256
|
|
159
|
+
- `convert_safe_u8`, `convert_safe_u16`, `convert_safe_u32`, `convert_safe_u64`, `convert_safe_u128`, `convert_safe_u256` → Respective type
|
|
160
|
+
|
|
161
|
+
### Query Nodes (Return Varies)
|
|
162
|
+
- `query` → Depends on query instruction
|
|
163
|
+
- `query_reward_record_find`, `query_reward_record_count` → U64
|
|
164
|
+
- `query_reward_record_exists` → Bool
|
|
165
|
+
- `query_progress_history_find`, `query_progress_history_session_find`, `query_progress_history_session_forward_find` → U64
|
|
166
|
+
- `query_progress_history_session_count`, `query_progress_history_session_forward_count`, `query_progress_history_session_forward_retained_submission_count` → U64
|
|
167
|
+
|
|
168
|
+
### Context Nodes
|
|
169
|
+
- `context: Signer` → Address
|
|
170
|
+
- `context: Clock` → U64 (timestamp)
|
|
171
|
+
- `context: Guard` → Address
|
|
172
|
+
|
|
173
|
+
### Vector Nodes
|
|
174
|
+
- `vec_length` → U64
|
|
175
|
+
- `vec_contains_bool`, `vec_contains_address`, `vec_contains_string`, `vec_contains_string_nocase`, `vec_contains_number` → Bool
|
|
176
|
+
|
|
177
|
+
## Best Practices
|
|
178
|
+
|
|
179
|
+
1. **Root node must return Bool**: Always use a logic node (logic_and, logic_or) as the root.
|
|
180
|
+
|
|
181
|
+
2. **Use logic_and for multiple conditions**: Most common pattern for combining validations.
|
|
182
|
+
|
|
183
|
+
3. **Check node return types**: Ensure child nodes return the expected type for the parent node.
|
|
184
|
+
- Logic nodes expect Bool children
|
|
185
|
+
- Calculation nodes expect numeric children
|
|
186
|
+
- Comparison nodes expect matching types
|
|
187
|
+
|
|
188
|
+
4. **Use identifier references**: Store constants (addresses, strings, numbers) in the Guard table and reference them by identifier.
|
|
189
|
+
|
|
190
|
+
5. **Query with convert_witness**: When querying object data, use the appropriate witness type to access the data you need.
|
|
191
|
+
|
|
192
|
+
6. **Handle optional fields**: In where clauses, only include fields you want to filter by. Omitting a field means "don't filter by this field".
|
|
193
|
+
|
|
194
|
+
## Common Errors
|
|
195
|
+
|
|
196
|
+
1. **Type mismatch**: Trying to compare incompatible types (e.g., Address with String).
|
|
197
|
+
2. **Missing identifier**: Referencing an identifier that doesn't exist in the Guard table.
|
|
198
|
+
3. **Wrong return type**: Using a non-Bool node as the root or as a child of a logic node.
|
|
199
|
+
4. **Invalid query ID**: Using a query instruction ID that doesn't exist.
|