lyra-plugin-onchain 0.1.11 → 0.2.1
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/README.md +21 -17
- package/package.json +9 -5
- package/src/catalog.ts +120 -0
- package/src/guidance.ts +14 -29
- package/src/index.ts +5 -68
- package/src/protocol-ids.ts +54 -32
- package/src/protocols.ts +9 -0
- package/src/tools/walrus-stake.ts +326 -0
package/README.md
CHANGED
|
@@ -1,26 +1,30 @@
|
|
|
1
1
|
# lyra-plugin-onchain
|
|
2
2
|
|
|
3
|
-
The **Sui limbs** for **lyra** — the
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
The **Sui limbs** for **lyra** — the tools that do real on-chain work. Every
|
|
4
|
+
value-moving call is routed through the deterministic **policy → simulate →
|
|
5
|
+
approve → execute** pipeline, and (when a vault is provisioned) sources funds from
|
|
6
|
+
the on-chain `Vault` via `vault_spend`, so nothing moves outside the on-chain
|
|
7
|
+
`lyra::policy` limits.
|
|
6
8
|
|
|
7
|
-
- **Wallet /
|
|
8
|
-
- **Transfers** — `
|
|
9
|
-
- **
|
|
10
|
-
|
|
11
|
-
- **Lending** —
|
|
12
|
-
`withdraw
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
9
|
+
- **Wallet / reads** — `account.info`, `sui.balance`
|
|
10
|
+
- **Transfers** — `sui.send` (recipient-allowlist aware)
|
|
11
|
+
- **Swaps** — `swap` (7k aggregator best-route across Cetus / Turbos / FlowX /
|
|
12
|
+
Bluefin / Aftermath / Momentum / Kriya / DeepBook), `cetus.quote`
|
|
13
|
+
- **Lending** — `scallop.*` (markets / position / supply / withdraw),
|
|
14
|
+
`navi.*` (markets / position / supply / withdraw / borrow / repay),
|
|
15
|
+
`suilend.*` (position / supply / withdraw / borrow / repay)
|
|
16
|
+
- **Staking** — `sui.stake` / `sui.unstake` (native delegation), `volo.stake` /
|
|
17
|
+
`volo.unstake` (liquid staking → vSUI)
|
|
18
|
+
- **Discovery** — `defi.yields` (DeFiLlama), `deepbook.markets`, `protocols.list`
|
|
19
|
+
(the honest read-vs-execute capability map)
|
|
20
|
+
- **Storage** — `walrus.store` (durable, verifiable receipts/memory on Walrus)
|
|
21
|
+
- **Policy** — `policy.create` (provision an `AgentPolicy` + `Vault`), `policy.show`
|
|
20
22
|
|
|
21
23
|
## Install
|
|
22
24
|
|
|
23
25
|
Auto-installed with [`lyra-ai-agent`](https://www.npmjs.com/package/lyra-ai-agent).
|
|
24
26
|
Or directly: `bun add lyra-plugin-onchain`.
|
|
25
27
|
|
|
26
|
-
|
|
28
|
+
The on-chain package it targets lives in
|
|
29
|
+
[`lyraai-protocol/contracts`](https://github.com/lyraai-protocol/contracts). See the
|
|
30
|
+
[root README](https://github.com/lyraai-protocol/lyra#readme) for the full architecture.
|
package/package.json
CHANGED
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "lyra-plugin-onchain",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "The Sui on-chain tools for Lyra: SUI transfers, DeepBook market data, Walrus receipts/memory — every write policy-checked, simulated, and recorded on-chain via lyra::policy",
|
|
6
6
|
"license": "MIT",
|
|
7
|
-
"homepage": "https://github.com/
|
|
7
|
+
"homepage": "https://github.com/lyraai-protocol/lyra",
|
|
8
8
|
"repository": {
|
|
9
9
|
"type": "git",
|
|
10
|
-
"url": "git+https://github.com/
|
|
10
|
+
"url": "git+https://github.com/lyraai-protocol/lyra.git",
|
|
11
11
|
"directory": "packages/plugin-onchain"
|
|
12
12
|
},
|
|
13
13
|
"bugs": {
|
|
14
|
-
"url": "https://github.com/
|
|
14
|
+
"url": "https://github.com/lyraai-protocol/lyra/issues"
|
|
15
15
|
},
|
|
16
16
|
"keywords": [
|
|
17
17
|
"lyra",
|
|
@@ -37,6 +37,10 @@
|
|
|
37
37
|
],
|
|
38
38
|
"main": "./src/index.ts",
|
|
39
39
|
"types": "./src/index.ts",
|
|
40
|
+
"exports": {
|
|
41
|
+
".": "./src/index.ts",
|
|
42
|
+
"./protocol-ids": "./src/protocol-ids.ts"
|
|
43
|
+
},
|
|
40
44
|
"scripts": {
|
|
41
45
|
"build": "tsc -b",
|
|
42
46
|
"test": "bun test"
|
|
@@ -51,7 +55,7 @@
|
|
|
51
55
|
"@scallop-io/sui-scallop-sdk": "^2.4.5",
|
|
52
56
|
"@suilend/sdk": "1.1.99",
|
|
53
57
|
"@suilend/sui-fe": "0.3.49",
|
|
54
|
-
"lyra-core": "^0.1
|
|
58
|
+
"lyra-core": "^0.2.1",
|
|
55
59
|
"navi-sdk": "^1.7.3",
|
|
56
60
|
"zod": "^3.23.8"
|
|
57
61
|
}
|
package/src/catalog.ts
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The tool catalog — the ONE place every on-chain tool is declared. Adding a tool
|
|
3
|
+
* is a single entry here (plus the tool file itself); everything downstream is
|
|
4
|
+
* derived:
|
|
5
|
+
*
|
|
6
|
+
* - registration → the plugin iterates TOOLS (index.ts)
|
|
7
|
+
* - web console tool set → WEB_TOOL_NAMES (apps import it; no hand-kept allowlist)
|
|
8
|
+
* - agent guidance → capabilitySummary() feeds the system prompt (CLI + web),
|
|
9
|
+
* so the model learns a new capability with no prose edit
|
|
10
|
+
*
|
|
11
|
+
* `web` marks a tool for the browser console's inline-execute set (the rest run on
|
|
12
|
+
* the CLI/gateway, or have a web-native equivalent). `blurb` is an optional one-line
|
|
13
|
+
* capability description surfaced in the guidance — omit it for secondary tools in a
|
|
14
|
+
* family that a sibling already covers.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import type { ToolDef } from 'lyra-core'
|
|
18
|
+
import { makeAccountInfo, makeSuiBalance } from './tools/balance'
|
|
19
|
+
import { makeCetusQuote } from './tools/cetus'
|
|
20
|
+
import { makeDeepbookMarkets } from './tools/deepbook'
|
|
21
|
+
import { makeDefiYields } from './tools/defillama'
|
|
22
|
+
import { makeVoloStake, makeVoloUnstake } from './tools/liquid-stake'
|
|
23
|
+
import {
|
|
24
|
+
makeNaviBorrow,
|
|
25
|
+
makeNaviMarkets,
|
|
26
|
+
makeNaviPosition,
|
|
27
|
+
makeNaviRepay,
|
|
28
|
+
makeNaviSupply,
|
|
29
|
+
makeNaviWithdraw,
|
|
30
|
+
} from './tools/navi'
|
|
31
|
+
import { makePolicyCreate, makePolicyShow } from './tools/policy'
|
|
32
|
+
import { makeProtocolsList } from './tools/protocols'
|
|
33
|
+
import {
|
|
34
|
+
makeScallopMarkets,
|
|
35
|
+
makeScallopPosition,
|
|
36
|
+
makeScallopSupply,
|
|
37
|
+
makeScallopWithdraw,
|
|
38
|
+
} from './tools/scallop'
|
|
39
|
+
import { makeSuiSend } from './tools/send'
|
|
40
|
+
import { makeStake, makeUnstake } from './tools/stake'
|
|
41
|
+
import {
|
|
42
|
+
makeSuilendBorrow,
|
|
43
|
+
makeSuilendPosition,
|
|
44
|
+
makeSuilendRepay,
|
|
45
|
+
makeSuilendSupply,
|
|
46
|
+
makeSuilendWithdraw,
|
|
47
|
+
} from './tools/suilend'
|
|
48
|
+
import { makeSwap } from './tools/swap'
|
|
49
|
+
import { makeWalrusStore } from './tools/walrus'
|
|
50
|
+
import { makeWalrusStake, makeWalrusStaking, makeWalrusUnstake } from './tools/walrus-stake'
|
|
51
|
+
import type { OnchainRuntimeContext } from './types'
|
|
52
|
+
|
|
53
|
+
// biome-ignore lint/suspicious/noExplicitAny: tools carry heterogeneous arg schemas
|
|
54
|
+
type Make = (ctx: OnchainRuntimeContext) => ToolDef<any>
|
|
55
|
+
|
|
56
|
+
export interface CatalogEntry {
|
|
57
|
+
/** The tool's registered name (also its dispatch key). */
|
|
58
|
+
name: string
|
|
59
|
+
/** Factory that binds the tool to a runtime context. */
|
|
60
|
+
make: Make
|
|
61
|
+
/** Exposed in the browser console's inline-execute tool set. */
|
|
62
|
+
web: boolean
|
|
63
|
+
/** One-line capability blurb for the agent guidance (optional). */
|
|
64
|
+
blurb?: string
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export const TOOLS: CatalogEntry[] = [
|
|
68
|
+
// Reads / discovery
|
|
69
|
+
{ name: 'account.info', make: makeAccountInfo, web: false, blurb: 'account.info — the agent address, network, balance, and active policy.' },
|
|
70
|
+
{ name: 'sui.balance', make: makeSuiBalance, web: false, blurb: 'sui.balance — SUI + coin balances for the agent or any address.' },
|
|
71
|
+
{ name: 'policy.show', make: makePolicyShow, web: false, blurb: 'policy.show — the active fund-control policy (caps, allowlists, expiry).' },
|
|
72
|
+
{ name: 'protocols.list', make: makeProtocolsList, web: true, blurb: 'protocols.list — which protocols Lyra can READ vs EXECUTE on.' },
|
|
73
|
+
{ name: 'defi.yields', make: makeDefiYields, web: true, blurb: 'defi.yields — best yields across Sui (DefiLlama), tagged executable / executeWith.' },
|
|
74
|
+
{ name: 'deepbook.markets', make: makeDeepbookMarkets, web: true, blurb: 'deepbook.markets — DeepBook spot mid prices.' },
|
|
75
|
+
{ name: 'cetus.quote', make: makeCetusQuote, web: false, blurb: 'cetus.quote — best swap route/price across DEXes (read-only aggregator).' },
|
|
76
|
+
{ name: 'scallop.markets', make: makeScallopMarkets, web: true },
|
|
77
|
+
{ name: 'scallop.position', make: makeScallopPosition, web: true },
|
|
78
|
+
{ name: 'navi.markets', make: makeNaviMarkets, web: true },
|
|
79
|
+
{ name: 'navi.position', make: makeNaviPosition, web: true },
|
|
80
|
+
{ name: 'suilend.position', make: makeSuilendPosition, web: true },
|
|
81
|
+
{ name: 'walrus.staking', make: makeWalrusStaking, web: true, blurb: "walrus.staking — the agent's WAL balance, StakedWal positions, and nodes to stake with." },
|
|
82
|
+
|
|
83
|
+
// Policy / transfer / swap
|
|
84
|
+
{ name: 'policy.create', make: makePolicyCreate, web: false, blurb: 'policy.create — publish a shared on-chain AgentPolicy (arms enforcement + receipts).' },
|
|
85
|
+
{ name: 'sui.send', make: makeSuiSend, web: false, blurb: 'sui.send — transfer SUI; blocked if out of policy, mints an on-chain receipt.' },
|
|
86
|
+
{ name: 'swap', make: makeSwap, web: false, blurb: 'swap — best-route swap across the major Sui DEXes (7k aggregator).' },
|
|
87
|
+
|
|
88
|
+
// Lending
|
|
89
|
+
{ name: 'scallop.supply', make: makeScallopSupply, web: true, blurb: 'Lending — scallop.supply/withdraw, navi.supply/withdraw/borrow/repay, suilend.supply/withdraw/borrow/repay.' },
|
|
90
|
+
{ name: 'scallop.withdraw', make: makeScallopWithdraw, web: true },
|
|
91
|
+
{ name: 'navi.supply', make: makeNaviSupply, web: true },
|
|
92
|
+
{ name: 'navi.withdraw', make: makeNaviWithdraw, web: true },
|
|
93
|
+
{ name: 'navi.borrow', make: makeNaviBorrow, web: true },
|
|
94
|
+
{ name: 'navi.repay', make: makeNaviRepay, web: true },
|
|
95
|
+
{ name: 'suilend.supply', make: makeSuilendSupply, web: true },
|
|
96
|
+
{ name: 'suilend.withdraw', make: makeSuilendWithdraw, web: true },
|
|
97
|
+
{ name: 'suilend.borrow', make: makeSuilendBorrow, web: true },
|
|
98
|
+
{ name: 'suilend.repay', make: makeSuilendRepay, web: true },
|
|
99
|
+
|
|
100
|
+
// Staking
|
|
101
|
+
{ name: 'sui.stake', make: makeStake, web: true, blurb: 'sui.stake / sui.unstake — native SUI staking to a validator (min 1 SUI).' },
|
|
102
|
+
{ name: 'sui.unstake', make: makeUnstake, web: true },
|
|
103
|
+
{ name: 'volo.stake', make: makeVoloStake, web: true, blurb: 'volo.stake / volo.unstake — liquid staking, SUI ↔ vSUI.' },
|
|
104
|
+
{ name: 'volo.unstake', make: makeVoloUnstake, web: true },
|
|
105
|
+
{ name: 'walrus.stake', make: makeWalrusStake, web: true, blurb: 'walrus.stake / walrus.unstake — stake WAL to a Walrus storage node (min 1 WAL; unstake returns WAL next epoch).' },
|
|
106
|
+
{ name: 'walrus.unstake', make: makeWalrusUnstake, web: true },
|
|
107
|
+
|
|
108
|
+
// Storage
|
|
109
|
+
{ name: 'walrus.store', make: makeWalrusStore, web: false, blurb: 'walrus.store — persist a receipt/report/memory artifact to Walrus.' },
|
|
110
|
+
]
|
|
111
|
+
|
|
112
|
+
/** Tool names exposed in the browser console's inline-execute set (derived). */
|
|
113
|
+
export const WEB_TOOL_NAMES: string[] = TOOLS.filter(t => t.web).map(t => t.name)
|
|
114
|
+
|
|
115
|
+
/** The capability inventory for the agent guidance (derived from the catalog). */
|
|
116
|
+
export function capabilitySummary(): string {
|
|
117
|
+
return TOOLS.filter(t => t.blurb)
|
|
118
|
+
.map(t => `- ${t.blurb}`)
|
|
119
|
+
.join('\n')
|
|
120
|
+
}
|
package/src/guidance.ts
CHANGED
|
@@ -1,44 +1,29 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* System-prompt guidance injected when the onchain plugin is active.
|
|
3
|
-
*
|
|
4
|
-
*
|
|
2
|
+
* System-prompt guidance injected when the onchain plugin is active. The policy
|
|
3
|
+
* framing + rules are hand-written; the CAPABILITY LIST is generated from the tool
|
|
4
|
+
* catalog (see `./catalog`), so adding a tool there surfaces it to the model here
|
|
5
|
+
* automatically — no prose edit per integration.
|
|
5
6
|
*/
|
|
6
7
|
|
|
8
|
+
import { capabilitySummary } from './catalog'
|
|
9
|
+
|
|
7
10
|
export const ONCHAIN_GUIDANCE = `# Sui on-chain tools (Lyra)
|
|
8
11
|
|
|
9
12
|
You operate a single Sui agent address. You can read and move funds ONLY through
|
|
10
13
|
these tools; every value-moving action is checked by a deterministic policy in
|
|
11
14
|
code AND re-enforced on-chain by the lyra::policy Move package. You cannot talk
|
|
12
|
-
your way past a cap — if
|
|
15
|
+
your way past a cap — if an action exceeds the per-tx cap or budget, the tool
|
|
13
16
|
returns "policy blocked" before anything is signed.
|
|
14
17
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
- sui.balance — SUI and coin balances for the agent or any address.
|
|
18
|
-
- policy.show — the active fund-control policy (caps, allowlists, expiry, tier).
|
|
19
|
-
- protocols.list — which protocols Lyra can READ vs EXECUTE on.
|
|
20
|
-
- defi.yields — best yields across ALL Sui protocols (DefiLlama). Each result is
|
|
21
|
-
tagged executable / executeWith.
|
|
22
|
-
- deepbook.markets — DeepBook spot mid prices.
|
|
23
|
-
- cetus.quote — best swap route/price across many DEXes (aggregator, read-only).
|
|
24
|
-
- scallop.markets / scallop.position, navi.markets / navi.position — lending
|
|
25
|
-
rates + the agent's positions.
|
|
26
|
-
|
|
27
|
-
Writes (policy-checked → simulated → executed):
|
|
28
|
-
- policy.create — publish a shared on-chain AgentPolicy (budget, per-tx cap,
|
|
29
|
-
expiry). Do this first to arm on-chain enforcement + receipts.
|
|
30
|
-
- sui.send — transfer SUI. Blocked if out of policy; mints an on-chain receipt.
|
|
31
|
-
- scallop.supply / scallop.withdraw, navi.supply / navi.withdraw — lend or
|
|
32
|
-
redeem idle SUI on Scallop / NAVI (the two largest Sui money markets).
|
|
33
|
-
- walrus.store — persist a receipt/report/memory artifact to Walrus.
|
|
18
|
+
Capabilities (each write runs policy-checked → simulated → executed):
|
|
19
|
+
${capabilitySummary()}
|
|
34
20
|
|
|
35
21
|
The capability boundary (important):
|
|
36
|
-
- Discovery is broad; EXECUTION is bounded
|
|
37
|
-
protocols.list
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
policy's protocol-allowlist enforces this on-chain too.
|
|
22
|
+
- Discovery is broad; EXECUTION is bounded to the protocols above (see
|
|
23
|
+
protocols.list). If the best yield/action a user wants is on a protocol Lyra has
|
|
24
|
+
NOT integrated, DO NOT invent a transaction. Say so honestly, then offer the best
|
|
25
|
+
executable alternative or concise manual steps. The policy's protocol-allowlist
|
|
26
|
+
enforces this on-chain too.
|
|
42
27
|
|
|
43
28
|
Rules:
|
|
44
29
|
- Call policy.show / protocols.list before claiming what you can spend or do.
|
package/src/index.ts
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
* Transfers: sui.send (policy → simulate → execute → on-chain receipt)
|
|
8
8
|
* Policy: policy.show, policy.create (lyra::policy AgentPolicy)
|
|
9
9
|
* Storage: walrus.store (durable, verifiable receipts/memory)
|
|
10
|
+
* WAL staking: walrus.stake, walrus.unstake, walrus.staking (Walrus storage nodes)
|
|
10
11
|
* Markets: deepbook.markets (DeepBook spot mid prices, read-only)
|
|
11
12
|
*
|
|
12
13
|
* Value-moving tools run through policy → simulate → (approval) → execute, and
|
|
@@ -21,40 +22,10 @@
|
|
|
21
22
|
// navi-sdk → axios → follow-redirects, which crashes Bun at import otherwise.
|
|
22
23
|
import './capture-shim'
|
|
23
24
|
import type { NativePlugin, ToolDef } from 'lyra-core'
|
|
24
|
-
import {
|
|
25
|
-
import { makeCetusQuote } from './tools/cetus'
|
|
26
|
-
import { makeDeepbookMarkets } from './tools/deepbook'
|
|
27
|
-
import { makeDefiYields } from './tools/defillama'
|
|
28
|
-
import { makeVoloStake, makeVoloUnstake } from './tools/liquid-stake'
|
|
29
|
-
import {
|
|
30
|
-
makeNaviBorrow,
|
|
31
|
-
makeNaviMarkets,
|
|
32
|
-
makeNaviPosition,
|
|
33
|
-
makeNaviRepay,
|
|
34
|
-
makeNaviSupply,
|
|
35
|
-
makeNaviWithdraw,
|
|
36
|
-
} from './tools/navi'
|
|
37
|
-
import { makePolicyCreate, makePolicyShow } from './tools/policy'
|
|
38
|
-
import { makeProtocolsList } from './tools/protocols'
|
|
39
|
-
import {
|
|
40
|
-
makeScallopMarkets,
|
|
41
|
-
makeScallopPosition,
|
|
42
|
-
makeScallopSupply,
|
|
43
|
-
makeScallopWithdraw,
|
|
44
|
-
} from './tools/scallop'
|
|
45
|
-
import { makeSuiSend } from './tools/send'
|
|
46
|
-
import { makeStake, makeUnstake } from './tools/stake'
|
|
47
|
-
import {
|
|
48
|
-
makeSuilendBorrow,
|
|
49
|
-
makeSuilendPosition,
|
|
50
|
-
makeSuilendRepay,
|
|
51
|
-
makeSuilendSupply,
|
|
52
|
-
makeSuilendWithdraw,
|
|
53
|
-
} from './tools/suilend'
|
|
54
|
-
import { makeSwap } from './tools/swap'
|
|
55
|
-
import { makeWalrusStore } from './tools/walrus'
|
|
25
|
+
import { TOOLS } from './catalog'
|
|
56
26
|
import type { OnchainRuntimeContext } from './types'
|
|
57
27
|
|
|
28
|
+
export { TOOLS, WEB_TOOL_NAMES, capabilitySummary, type CatalogEntry } from './catalog'
|
|
58
29
|
export {
|
|
59
30
|
makeSuiClient,
|
|
60
31
|
keypairFromSecret,
|
|
@@ -96,42 +67,8 @@ const plugin: NativePlugin = {
|
|
|
96
67
|
const onchain = (ctx as unknown as { onchain?: OnchainRuntimeContext }).onchain
|
|
97
68
|
if (!onchain) return // soft-init for tests / non-onchain contexts
|
|
98
69
|
|
|
99
|
-
|
|
100
|
-
ctx.registerTool(
|
|
101
|
-
ctx.registerTool(makeSuiSend(onchain) as ToolDef)
|
|
102
|
-
ctx.registerTool(makeSwap(onchain) as ToolDef)
|
|
103
|
-
ctx.registerTool(makePolicyShow(onchain) as ToolDef)
|
|
104
|
-
ctx.registerTool(makePolicyCreate(onchain) as ToolDef)
|
|
105
|
-
ctx.registerTool(makeWalrusStore(onchain) as ToolDef)
|
|
106
|
-
ctx.registerTool(makeDeepbookMarkets(onchain) as ToolDef)
|
|
107
|
-
|
|
108
|
-
// Discovery + capability map.
|
|
109
|
-
ctx.registerTool(makeProtocolsList(onchain) as ToolDef)
|
|
110
|
-
ctx.registerTool(makeDefiYields(onchain) as ToolDef)
|
|
111
|
-
ctx.registerTool(makeCetusQuote(onchain) as ToolDef)
|
|
112
|
-
|
|
113
|
-
// Lending (the two largest Sui money markets).
|
|
114
|
-
ctx.registerTool(makeScallopMarkets(onchain) as ToolDef)
|
|
115
|
-
ctx.registerTool(makeScallopPosition(onchain) as ToolDef)
|
|
116
|
-
ctx.registerTool(makeScallopSupply(onchain) as ToolDef)
|
|
117
|
-
ctx.registerTool(makeScallopWithdraw(onchain) as ToolDef)
|
|
118
|
-
ctx.registerTool(makeNaviMarkets(onchain) as ToolDef)
|
|
119
|
-
ctx.registerTool(makeNaviPosition(onchain) as ToolDef)
|
|
120
|
-
ctx.registerTool(makeNaviSupply(onchain) as ToolDef)
|
|
121
|
-
ctx.registerTool(makeNaviWithdraw(onchain) as ToolDef)
|
|
122
|
-
ctx.registerTool(makeNaviBorrow(onchain) as ToolDef)
|
|
123
|
-
ctx.registerTool(makeNaviRepay(onchain) as ToolDef)
|
|
124
|
-
ctx.registerTool(makeSuilendSupply(onchain) as ToolDef)
|
|
125
|
-
ctx.registerTool(makeSuilendWithdraw(onchain) as ToolDef)
|
|
126
|
-
ctx.registerTool(makeSuilendBorrow(onchain) as ToolDef)
|
|
127
|
-
ctx.registerTool(makeSuilendRepay(onchain) as ToolDef)
|
|
128
|
-
ctx.registerTool(makeSuilendPosition(onchain) as ToolDef)
|
|
129
|
-
|
|
130
|
-
// Staking: native delegation (min 1 SUI) + Volo liquid staking (vSUI).
|
|
131
|
-
ctx.registerTool(makeStake(onchain) as ToolDef)
|
|
132
|
-
ctx.registerTool(makeUnstake(onchain) as ToolDef)
|
|
133
|
-
ctx.registerTool(makeVoloStake(onchain) as ToolDef)
|
|
134
|
-
ctx.registerTool(makeVoloUnstake(onchain) as ToolDef)
|
|
70
|
+
// Every tool is declared once in the catalog; registration just iterates it.
|
|
71
|
+
for (const tool of TOOLS) ctx.registerTool(tool.make(onchain) as ToolDef)
|
|
135
72
|
},
|
|
136
73
|
}
|
|
137
74
|
|
package/src/protocol-ids.ts
CHANGED
|
@@ -1,48 +1,70 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Canonical on-chain protocol
|
|
2
|
+
* Canonical on-chain protocol registry — the SINGLE SOURCE OF TRUTH for the
|
|
3
|
+
* policy's protocol allowlist, shared by the tools (which tag a vault_spend with a
|
|
4
|
+
* protocol id), the console UI (which lets the owner toggle each one), and the
|
|
5
|
+
* landing. Adding a protocol is ONE entry in `REGISTRY` below — `PROTOCOL_IDS`,
|
|
6
|
+
* `PROTOCOL_LABELS`, and `ALLOWLISTABLE_PROTOCOLS` are all derived from it, and the
|
|
7
|
+
* web + landing import `ALLOWLISTABLE_PROTOCOLS` from here (via the package's
|
|
8
|
+
* `./protocol-ids` subpath) instead of maintaining their own copies.
|
|
3
9
|
*
|
|
4
|
-
*
|
|
5
|
-
* protocol's package id. The owner's `allowed_protocols` allowlist (settable via
|
|
6
|
-
* `lyra::policy::set_allowed_protocols`) is checked against this tag on-chain — so
|
|
7
|
-
* an owner can restrict the agent to, say, only staking + NAVI. These ids are the
|
|
8
|
-
* SINGLE SOURCE OF TRUTH shared by the tools (which tag) and the console UI (which
|
|
9
|
-
* lets the owner toggle) so the two never drift.
|
|
10
|
-
*
|
|
11
|
-
* They are STABLE labels: a protocol upgrading its package doesn't change the tag
|
|
10
|
+
* The ids are STABLE labels: a protocol upgrading its package doesn't change the tag
|
|
12
11
|
* (the tag is a self-reported label for the allowlist, not the call target), so an
|
|
13
12
|
* owner's allowlist keeps working across protocol upgrades. Sourced from each
|
|
14
13
|
* protocol's SDK on mainnet.
|
|
15
14
|
*
|
|
15
|
+
* This file is a dependency-free leaf so the browser can import it safely.
|
|
16
|
+
*
|
|
16
17
|
* `0x0` is the reserved "no specific protocol" tag used by transfers + swaps —
|
|
17
18
|
* always allowed by the policy (they're bounded by budget/cap/coin/recipient/
|
|
18
19
|
* slippage instead), so restricting protocols never blocks a plain transfer/swap.
|
|
19
20
|
*/
|
|
20
21
|
export const NO_PROTOCOL = '0x0'
|
|
21
22
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
23
|
+
/** The one place a protocol is declared. Everything below is derived. */
|
|
24
|
+
const REGISTRY = [
|
|
25
|
+
{ key: 'suiStaking', id: '0x3', label: 'Sui staking' },
|
|
26
|
+
{
|
|
27
|
+
key: 'navi',
|
|
28
|
+
id: '0x1e4a13a0494d5facdbe8473e74127b838c2d446ecec0ce262e2eddafa77259cb',
|
|
29
|
+
label: 'NAVI',
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
key: 'suilend',
|
|
33
|
+
id: '0xf95b06141ed4a174f239417323bde3f209b972f5930d8521ea38a52aff3a6ddf',
|
|
34
|
+
label: 'Suilend',
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
key: 'volo',
|
|
38
|
+
id: '0x68d22cf8bdbcd11ecba1e094922873e4080d4d11133e2443fddda0bfd11dae20',
|
|
39
|
+
label: 'Volo (vSUI)',
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
key: 'scallop',
|
|
43
|
+
id: '0xde5c09ad171544aa3724dc67216668c80e754860f419136a68d78504eb2e2805',
|
|
44
|
+
label: 'Scallop',
|
|
45
|
+
},
|
|
46
|
+
// The type-defining (original) Walrus package — where `StakedWal` lives — as the
|
|
47
|
+
// stable tag for WAL staking (survives Walrus package upgrades).
|
|
48
|
+
{
|
|
49
|
+
key: 'walrusStaking',
|
|
50
|
+
id: '0xfdc88f7d7cf30afab2f82e8380d11ee8f70efb90e863d1de8616fae1bb09ea77',
|
|
51
|
+
label: 'Walrus staking',
|
|
52
|
+
},
|
|
53
|
+
] as const
|
|
54
|
+
|
|
55
|
+
export type ProtocolKey = (typeof REGISTRY)[number]['key']
|
|
29
56
|
|
|
30
|
-
|
|
57
|
+
/** `key → package id`, for the tools that tag a vault_spend. */
|
|
58
|
+
export const PROTOCOL_IDS = Object.fromEntries(REGISTRY.map(p => [p.key, p.id])) as Record<
|
|
59
|
+
ProtocolKey,
|
|
60
|
+
string
|
|
61
|
+
>
|
|
31
62
|
|
|
32
|
-
/**
|
|
33
|
-
export const PROTOCOL_LABELS: Record<string, string> =
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
[PROTOCOL_IDS.suilend]: 'Suilend',
|
|
37
|
-
[PROTOCOL_IDS.volo]: 'Volo (vSUI)',
|
|
38
|
-
[PROTOCOL_IDS.scallop]: 'Scallop',
|
|
39
|
-
}
|
|
63
|
+
/** `package id → human label`, for the owner UI + receipts. */
|
|
64
|
+
export const PROTOCOL_LABELS: Record<string, string> = Object.fromEntries(
|
|
65
|
+
REGISTRY.map(p => [p.id, p.label]),
|
|
66
|
+
)
|
|
40
67
|
|
|
41
68
|
/** The allowlistable protocols in display order, for building the owner UI. */
|
|
42
|
-
export const ALLOWLISTABLE_PROTOCOLS: { key: ProtocolKey; id: string; label: string }[] =
|
|
43
|
-
{ key:
|
|
44
|
-
{ key: 'navi', id: PROTOCOL_IDS.navi, label: 'NAVI' },
|
|
45
|
-
{ key: 'suilend', id: PROTOCOL_IDS.suilend, label: 'Suilend' },
|
|
46
|
-
{ key: 'volo', id: PROTOCOL_IDS.volo, label: 'Volo (vSUI)' },
|
|
47
|
-
{ key: 'scallop', id: PROTOCOL_IDS.scallop, label: 'Scallop' },
|
|
48
|
-
]
|
|
69
|
+
export const ALLOWLISTABLE_PROTOCOLS: { key: ProtocolKey; id: string; label: string }[] =
|
|
70
|
+
REGISTRY.map(p => ({ key: p.key, id: p.id, label: p.label }))
|
package/src/protocols.ts
CHANGED
|
@@ -35,6 +35,15 @@ export const PROTOCOLS: ProtocolCapability[] = [
|
|
|
35
35
|
execute: true,
|
|
36
36
|
tools: ['walrus.store'],
|
|
37
37
|
},
|
|
38
|
+
{
|
|
39
|
+
id: 'walrus-staking',
|
|
40
|
+
name: 'Walrus staking',
|
|
41
|
+
category: 'staking',
|
|
42
|
+
read: true,
|
|
43
|
+
execute: true,
|
|
44
|
+
tools: ['walrus.stake', 'walrus.unstake', 'walrus.staking'],
|
|
45
|
+
note: 'stake WAL to a Walrus storage node to earn rewards + secure decentralized storage (min 1 WAL)',
|
|
46
|
+
},
|
|
38
47
|
{
|
|
39
48
|
id: 'deepbook',
|
|
40
49
|
name: 'DeepBook',
|
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Walrus (WAL) staking — delegate WAL to a Walrus storage node's staking pool to
|
|
3
|
+
* earn rewards + help secure the storage network. This is the integration behind
|
|
4
|
+
* https://stake-wal.wal.app, exposed to the agent:
|
|
5
|
+
*
|
|
6
|
+
* walrus.stake → <walrus>::staking::stake_with_pool → StakedWal
|
|
7
|
+
* walrus.unstake → <walrus>::staking::request_withdraw_stake (returns WAL next epoch)
|
|
8
|
+
* walrus.staking → read: WAL balance, StakedWal positions, top nodes
|
|
9
|
+
*
|
|
10
|
+
* Same guarded pipeline as the other write tools: minimum-amount guard →
|
|
11
|
+
* deterministic off-chain policy → dry-run simulate → execute → on-chain effects
|
|
12
|
+
* check. WAL is drawn from the agent's WAL balance (not the SUI vault — the vault
|
|
13
|
+
* is SUI-typed), so the on-chain SUI budget doesn't apply; the action is bounded
|
|
14
|
+
* by the `walrus` protocol allowlist and audited by the on-chain StakedWal object.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { Transaction } from '@mysten/sui/transactions'
|
|
18
|
+
import {
|
|
19
|
+
MAINNET_WALRUS_PACKAGE_CONFIG,
|
|
20
|
+
TESTNET_WALRUS_PACKAGE_CONFIG,
|
|
21
|
+
WalrusClient,
|
|
22
|
+
} from '@mysten/walrus'
|
|
23
|
+
import type { ToolDef } from 'lyra-core'
|
|
24
|
+
import { z } from 'zod'
|
|
25
|
+
import { evaluatePolicy, suiToMist } from '../policy'
|
|
26
|
+
import { simulate } from '../simulate'
|
|
27
|
+
import type { OnchainRuntimeContext } from '../types'
|
|
28
|
+
|
|
29
|
+
// WAL has 9 decimals (like SUI). Walrus rejects a pool contribution below its
|
|
30
|
+
// minimum stake (staked_wal::mint aborts with code 7 for a too-small stake) — the
|
|
31
|
+
// on-chain floor is 1 WAL, so reject anything smaller early with a clear message.
|
|
32
|
+
const MIN_WAL_FROST = 1_000_000_000n // 1 WAL (Walrus minimum stake)
|
|
33
|
+
const STAKED_WAL_SUFFIX = '::staked_wal::StakedWal'
|
|
34
|
+
|
|
35
|
+
const WAL_TYPE: Record<string, string> = {
|
|
36
|
+
mainnet: '0x356a26eb9e012a68958082340d4c4116e7f55615cf27affcff209cf0ae544f59::wal::WAL',
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function fmtWal(frost: bigint): string {
|
|
40
|
+
return (Number(frost) / 1e9).toString()
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
interface WalrusStaking {
|
|
44
|
+
pkg: string // walrus package (moveCall target; resolved live so upgrades are safe)
|
|
45
|
+
stakingId: string // the shared Staking system object
|
|
46
|
+
walType: string
|
|
47
|
+
walrus: WalrusClient
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Resolve the live Walrus staking objects + WAL coin type for the network. */
|
|
51
|
+
async function resolveWalrus(ctx: OnchainRuntimeContext): Promise<WalrusStaking> {
|
|
52
|
+
const network = ctx.walrusNetwork ?? ctx.network
|
|
53
|
+
const walrus = new WalrusClient({
|
|
54
|
+
network,
|
|
55
|
+
// cast: @mysten/walrus bundles a different @mysten/sui minor; runtime-compatible.
|
|
56
|
+
suiClient: ctx.client as never,
|
|
57
|
+
})
|
|
58
|
+
const stakingObj = await walrus.stakingObject()
|
|
59
|
+
const cfg = network === 'testnet' ? TESTNET_WALRUS_PACKAGE_CONFIG : MAINNET_WALRUS_PACKAGE_CONFIG
|
|
60
|
+
let walType = WAL_TYPE[network]
|
|
61
|
+
if (!walType) {
|
|
62
|
+
// Non-mainnet: discover the WAL type from the agent's own balances.
|
|
63
|
+
const balances = await ctx.client.getAllBalances({ owner: ctx.agentAddress })
|
|
64
|
+
walType = balances.find(b => b.coinType.endsWith('::wal::WAL'))?.coinType ?? ''
|
|
65
|
+
if (!walType) throw new Error(`could not resolve the WAL coin type on ${network}`)
|
|
66
|
+
}
|
|
67
|
+
return { pkg: stakingObj.package_id, stakingId: cfg.stakingPoolId, walType, walrus }
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Pick a storage node to stake with: match `want` by node id, else the node with
|
|
71
|
+
* the most stake weight (a large, reliable committee member). */
|
|
72
|
+
async function resolveNode(
|
|
73
|
+
walrus: WalrusClient,
|
|
74
|
+
want?: string,
|
|
75
|
+
): Promise<{ nodeId: string; weight: number } | null> {
|
|
76
|
+
const state = await walrus.systemState()
|
|
77
|
+
const members = state.committee.members
|
|
78
|
+
if (members.length === 0) return null
|
|
79
|
+
if (want?.trim()) {
|
|
80
|
+
const w = want.trim().toLowerCase()
|
|
81
|
+
const m = members.find(x => String(x.node_id).toLowerCase() === w)
|
|
82
|
+
if (m) return { nodeId: m.node_id, weight: m.weight }
|
|
83
|
+
if (w.startsWith('0x')) return { nodeId: want.trim(), weight: 0 }
|
|
84
|
+
return null
|
|
85
|
+
}
|
|
86
|
+
const best = members.reduce((a, b) => (Number(b.weight) > Number(a.weight) ? b : a))
|
|
87
|
+
return { nodeId: best.node_id, weight: best.weight }
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// ─────────────────────────── walrus.stake ───────────────────────────
|
|
91
|
+
|
|
92
|
+
const StakeSchema = z.object({
|
|
93
|
+
amount: z.string().min(1).describe('Amount of WAL to stake (minimum 1 WAL).'),
|
|
94
|
+
node: z
|
|
95
|
+
.string()
|
|
96
|
+
.optional()
|
|
97
|
+
.describe('Storage node id (0x…) to stake with. Optional — defaults to a large active node.'),
|
|
98
|
+
})
|
|
99
|
+
type StakeArgs = z.infer<typeof StakeSchema>
|
|
100
|
+
|
|
101
|
+
export function makeWalrusStake(ctx: OnchainRuntimeContext): ToolDef<StakeArgs> {
|
|
102
|
+
return {
|
|
103
|
+
name: 'walrus.stake',
|
|
104
|
+
description:
|
|
105
|
+
'Stake WAL to a Walrus storage node to earn staking rewards and help secure decentralized storage. Minimum 1 WAL. Policy-checked → simulated → executed; returns a StakedWal object.',
|
|
106
|
+
searchHint: 'walrus stake WAL storage node delegate earn rewards staking wal.app',
|
|
107
|
+
schema: StakeSchema,
|
|
108
|
+
handler: async args => {
|
|
109
|
+
try {
|
|
110
|
+
const frost = suiToMist(args.amount) // WAL shares SUI's 9-decimal scaling
|
|
111
|
+
if (frost === undefined || frost <= 0n) {
|
|
112
|
+
return { ok: false, error: `invalid amount "${args.amount}"` }
|
|
113
|
+
}
|
|
114
|
+
if (frost < MIN_WAL_FROST) {
|
|
115
|
+
return {
|
|
116
|
+
ok: false,
|
|
117
|
+
error: `amount too small: ${fmtWal(frost)} WAL is below the ${fmtWal(MIN_WAL_FROST)} WAL minimum for staking`,
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const { pkg, stakingId, walType, walrus } = await resolveWalrus(ctx)
|
|
122
|
+
|
|
123
|
+
if (ctx.policy) {
|
|
124
|
+
const verdict = evaluatePolicy(
|
|
125
|
+
{ kind: 'transfer', coinType: walType, amountMist: frost, protocol: 'walrus' },
|
|
126
|
+
ctx.policy,
|
|
127
|
+
)
|
|
128
|
+
if (!verdict.allowed) {
|
|
129
|
+
return { ok: false, error: `policy blocked: ${verdict.violations.join('; ')}` }
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const bal = await ctx.client.getBalance({ owner: ctx.agentAddress, coinType: walType })
|
|
134
|
+
if (BigInt(bal.totalBalance) < frost) {
|
|
135
|
+
return {
|
|
136
|
+
ok: false,
|
|
137
|
+
error: `insufficient WAL: have ${fmtWal(BigInt(bal.totalBalance))}, need ${fmtWal(frost)}`,
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const node = await resolveNode(walrus, args.node)
|
|
142
|
+
if (!node) return { ok: false, error: 'no active Walrus storage node found to stake with' }
|
|
143
|
+
|
|
144
|
+
const coins = await ctx.client.getCoins({ owner: ctx.agentAddress, coinType: walType })
|
|
145
|
+
const [firstCoin, ...restCoins] = coins.data
|
|
146
|
+
if (!firstCoin) return { ok: false, error: 'no WAL coins in the agent wallet' }
|
|
147
|
+
|
|
148
|
+
const tx = new Transaction()
|
|
149
|
+
const primary = tx.object(firstCoin.coinObjectId)
|
|
150
|
+
if (restCoins.length > 0) {
|
|
151
|
+
tx.mergeCoins(
|
|
152
|
+
primary,
|
|
153
|
+
restCoins.map(c => tx.object(c.coinObjectId)),
|
|
154
|
+
)
|
|
155
|
+
}
|
|
156
|
+
const [toStake] = tx.splitCoins(primary, [tx.pure.u64(frost)])
|
|
157
|
+
const staked = tx.moveCall({
|
|
158
|
+
target: `${pkg}::staking::stake_with_pool`,
|
|
159
|
+
arguments: [tx.object(stakingId), toStake, tx.pure.id(node.nodeId)],
|
|
160
|
+
})
|
|
161
|
+
tx.transferObjects([staked], ctx.agentAddress)
|
|
162
|
+
|
|
163
|
+
const sim = await simulate(ctx.client, tx, ctx.agentAddress)
|
|
164
|
+
if (!sim.ok) return { ok: false, error: `pre-flight simulation failed: ${sim.reason}` }
|
|
165
|
+
|
|
166
|
+
const res = await ctx.client.signAndExecuteTransaction({
|
|
167
|
+
signer: ctx.keypair,
|
|
168
|
+
transaction: tx,
|
|
169
|
+
options: { showEffects: true, showObjectChanges: true },
|
|
170
|
+
})
|
|
171
|
+
if (res.effects?.status?.status !== 'success') {
|
|
172
|
+
return {
|
|
173
|
+
ok: false,
|
|
174
|
+
error: `execution failed: ${res.effects?.status?.error ?? 'unknown'}`,
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
await ctx.client.waitForTransaction({ digest: res.digest })
|
|
178
|
+
const stakedWal = res.objectChanges?.find(
|
|
179
|
+
c =>
|
|
180
|
+
c.type === 'created' &&
|
|
181
|
+
String((c as { objectType?: string }).objectType).endsWith(STAKED_WAL_SUFFIX),
|
|
182
|
+
) as { objectId?: string } | undefined
|
|
183
|
+
|
|
184
|
+
return {
|
|
185
|
+
ok: true,
|
|
186
|
+
data: {
|
|
187
|
+
protocol: 'walrus-staking',
|
|
188
|
+
action: 'stake',
|
|
189
|
+
amountWal: args.amount,
|
|
190
|
+
node: node.nodeId,
|
|
191
|
+
digest: res.digest,
|
|
192
|
+
stakedWalId: stakedWal?.objectId ?? null,
|
|
193
|
+
status: 'success',
|
|
194
|
+
},
|
|
195
|
+
}
|
|
196
|
+
} catch (e) {
|
|
197
|
+
return { ok: false, error: (e as Error).message.slice(0, 240) }
|
|
198
|
+
}
|
|
199
|
+
},
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// ─────────────────────────── walrus.unstake ───────────────────────────
|
|
204
|
+
|
|
205
|
+
const UnstakeSchema = z.object({
|
|
206
|
+
stakedWalId: z
|
|
207
|
+
.string()
|
|
208
|
+
.optional()
|
|
209
|
+
.describe('StakedWal object id to withdraw. Optional — defaults to the first staked position.'),
|
|
210
|
+
})
|
|
211
|
+
type UnstakeArgs = z.infer<typeof UnstakeSchema>
|
|
212
|
+
|
|
213
|
+
export function makeWalrusUnstake(ctx: OnchainRuntimeContext): ToolDef<UnstakeArgs> {
|
|
214
|
+
return {
|
|
215
|
+
name: 'walrus.unstake',
|
|
216
|
+
description:
|
|
217
|
+
'Request to withdraw staked WAL from a Walrus node. The principal + rewards become withdrawable after the next epoch. Uses a StakedWal object id, or the first staked position if omitted.',
|
|
218
|
+
searchHint: 'walrus unstake withdraw WAL staking redeem claim rewards storage node',
|
|
219
|
+
schema: UnstakeSchema,
|
|
220
|
+
handler: async args => {
|
|
221
|
+
try {
|
|
222
|
+
const { pkg, stakingId, walType } = await resolveWalrus(ctx)
|
|
223
|
+
|
|
224
|
+
let stakedId = args.stakedWalId?.trim()
|
|
225
|
+
if (!stakedId) {
|
|
226
|
+
const owned = await ctx.client.getOwnedObjects({
|
|
227
|
+
owner: ctx.agentAddress,
|
|
228
|
+
filter: { StructType: `${walType.split('::')[0]}${STAKED_WAL_SUFFIX}` },
|
|
229
|
+
})
|
|
230
|
+
stakedId = owned.data[0]?.data?.objectId
|
|
231
|
+
if (!stakedId) return { ok: false, error: 'no StakedWal position found to unstake' }
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
if (ctx.policy) {
|
|
235
|
+
const verdict = evaluatePolicy(
|
|
236
|
+
{ kind: 'transfer', coinType: walType, amountMist: 0n, protocol: 'walrus' },
|
|
237
|
+
ctx.policy,
|
|
238
|
+
)
|
|
239
|
+
if (!verdict.allowed) {
|
|
240
|
+
return { ok: false, error: `policy blocked: ${verdict.violations.join('; ')}` }
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const tx = new Transaction()
|
|
245
|
+
tx.moveCall({
|
|
246
|
+
target: `${pkg}::staking::request_withdraw_stake`,
|
|
247
|
+
arguments: [tx.object(stakingId), tx.object(stakedId)],
|
|
248
|
+
})
|
|
249
|
+
|
|
250
|
+
const sim = await simulate(ctx.client, tx, ctx.agentAddress)
|
|
251
|
+
if (!sim.ok) return { ok: false, error: `pre-flight simulation failed: ${sim.reason}` }
|
|
252
|
+
|
|
253
|
+
const res = await ctx.client.signAndExecuteTransaction({
|
|
254
|
+
signer: ctx.keypair,
|
|
255
|
+
transaction: tx,
|
|
256
|
+
options: { showEffects: true },
|
|
257
|
+
})
|
|
258
|
+
if (res.effects?.status?.status !== 'success') {
|
|
259
|
+
return {
|
|
260
|
+
ok: false,
|
|
261
|
+
error: `execution failed: ${res.effects?.status?.error ?? 'unknown'}`,
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
await ctx.client.waitForTransaction({ digest: res.digest })
|
|
265
|
+
|
|
266
|
+
return {
|
|
267
|
+
ok: true,
|
|
268
|
+
data: {
|
|
269
|
+
protocol: 'walrus-staking',
|
|
270
|
+
action: 'unstake',
|
|
271
|
+
stakedWalId: stakedId,
|
|
272
|
+
digest: res.digest,
|
|
273
|
+
note: 'WAL becomes withdrawable after the next Walrus epoch',
|
|
274
|
+
status: 'success',
|
|
275
|
+
},
|
|
276
|
+
}
|
|
277
|
+
} catch (e) {
|
|
278
|
+
return { ok: false, error: (e as Error).message.slice(0, 240) }
|
|
279
|
+
}
|
|
280
|
+
},
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// ─────────────────────────── walrus.staking (read) ───────────────────────────
|
|
285
|
+
|
|
286
|
+
const ReadSchema = z.object({})
|
|
287
|
+
type ReadArgs = z.infer<typeof ReadSchema>
|
|
288
|
+
|
|
289
|
+
export function makeWalrusStaking(ctx: OnchainRuntimeContext): ToolDef<ReadArgs> {
|
|
290
|
+
return {
|
|
291
|
+
name: 'walrus.staking',
|
|
292
|
+
description:
|
|
293
|
+
"Read the agent's Walrus staking position: WAL balance, current StakedWal positions, and a few large storage nodes to stake with.",
|
|
294
|
+
searchHint: 'walrus staking position WAL balance staked nodes committee read status',
|
|
295
|
+
schema: ReadSchema,
|
|
296
|
+
handler: async () => {
|
|
297
|
+
try {
|
|
298
|
+
const { walType, walrus } = await resolveWalrus(ctx)
|
|
299
|
+
const [bal, owned, state] = await Promise.all([
|
|
300
|
+
ctx.client.getBalance({ owner: ctx.agentAddress, coinType: walType }),
|
|
301
|
+
ctx.client.getOwnedObjects({
|
|
302
|
+
owner: ctx.agentAddress,
|
|
303
|
+
filter: { StructType: `${walType.split('::')[0]}${STAKED_WAL_SUFFIX}` },
|
|
304
|
+
options: { showType: true },
|
|
305
|
+
}),
|
|
306
|
+
walrus.systemState(),
|
|
307
|
+
])
|
|
308
|
+
const topNodes = [...state.committee.members]
|
|
309
|
+
.sort((a, b) => Number(b.weight) - Number(a.weight))
|
|
310
|
+
.slice(0, 5)
|
|
311
|
+
.map(m => ({ nodeId: m.node_id, weight: m.weight }))
|
|
312
|
+
return {
|
|
313
|
+
ok: true,
|
|
314
|
+
data: {
|
|
315
|
+
walBalance: fmtWal(BigInt(bal.totalBalance)),
|
|
316
|
+
stakedPositions: owned.data.map(o => o.data?.objectId).filter(Boolean),
|
|
317
|
+
committeeSize: state.committee.members.length,
|
|
318
|
+
topNodes,
|
|
319
|
+
},
|
|
320
|
+
}
|
|
321
|
+
} catch (e) {
|
|
322
|
+
return { ok: false, error: (e as Error).message.slice(0, 240) }
|
|
323
|
+
}
|
|
324
|
+
},
|
|
325
|
+
}
|
|
326
|
+
}
|