lyra-plugin-onchain 0.1.5 → 0.1.7
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/package.json +1 -1
- package/src/index.ts +15 -1
- package/src/minimums.test.ts +36 -0
- package/src/minimums.ts +35 -0
- package/src/tools/navi.ts +43 -5
- package/src/tools/scallop.ts +5 -0
- package/src/tools/send.ts +3 -0
- package/src/tools/stake.ts +203 -0
- package/src/tools/swap.ts +6 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "lyra-plugin-onchain",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.7",
|
|
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",
|
package/src/index.ts
CHANGED
|
@@ -25,7 +25,14 @@ import { makeAccountInfo, makeSuiBalance } from './tools/balance'
|
|
|
25
25
|
import { makeCetusQuote } from './tools/cetus'
|
|
26
26
|
import { makeDeepbookMarkets } from './tools/deepbook'
|
|
27
27
|
import { makeDefiYields } from './tools/defillama'
|
|
28
|
-
import {
|
|
28
|
+
import {
|
|
29
|
+
makeNaviBorrow,
|
|
30
|
+
makeNaviMarkets,
|
|
31
|
+
makeNaviPosition,
|
|
32
|
+
makeNaviRepay,
|
|
33
|
+
makeNaviSupply,
|
|
34
|
+
makeNaviWithdraw,
|
|
35
|
+
} from './tools/navi'
|
|
29
36
|
import { makePolicyCreate, makePolicyShow } from './tools/policy'
|
|
30
37
|
import { makeProtocolsList } from './tools/protocols'
|
|
31
38
|
import {
|
|
@@ -35,6 +42,7 @@ import {
|
|
|
35
42
|
makeScallopWithdraw,
|
|
36
43
|
} from './tools/scallop'
|
|
37
44
|
import { makeSuiSend } from './tools/send'
|
|
45
|
+
import { makeStake, makeUnstake } from './tools/stake'
|
|
38
46
|
import { makeSwap } from './tools/swap'
|
|
39
47
|
import { makeWalrusStore } from './tools/walrus'
|
|
40
48
|
import type { OnchainRuntimeContext } from './types'
|
|
@@ -91,6 +99,12 @@ const plugin: NativePlugin = {
|
|
|
91
99
|
ctx.registerTool(makeNaviPosition(onchain) as ToolDef)
|
|
92
100
|
ctx.registerTool(makeNaviSupply(onchain) as ToolDef)
|
|
93
101
|
ctx.registerTool(makeNaviWithdraw(onchain) as ToolDef)
|
|
102
|
+
ctx.registerTool(makeNaviBorrow(onchain) as ToolDef)
|
|
103
|
+
ctx.registerTool(makeNaviRepay(onchain) as ToolDef)
|
|
104
|
+
|
|
105
|
+
// Native staking (delegate SUI to a validator; min 1 SUI).
|
|
106
|
+
ctx.registerTool(makeStake(onchain) as ToolDef)
|
|
107
|
+
ctx.registerTool(makeUnstake(onchain) as ToolDef)
|
|
94
108
|
},
|
|
95
109
|
}
|
|
96
110
|
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test'
|
|
2
|
+
import { MIN_MIST, checkMinimum } from './minimums'
|
|
3
|
+
|
|
4
|
+
describe('checkMinimum', () => {
|
|
5
|
+
test('rejects a too-small stake (0.1 SUI < 1 SUI minimum)', () => {
|
|
6
|
+
const err = checkMinimum('stake', 100_000_000n) // 0.1 SUI
|
|
7
|
+
expect(err).not.toBeNull()
|
|
8
|
+
expect(err).toContain('amount too small')
|
|
9
|
+
expect(err).toContain('1 SUI minimum for stake')
|
|
10
|
+
})
|
|
11
|
+
|
|
12
|
+
test('accepts a stake at the 1 SUI minimum', () => {
|
|
13
|
+
expect(checkMinimum('stake', MIN_MIST.stake)).toBeNull()
|
|
14
|
+
expect(checkMinimum('stake', 2_000_000_000n)).toBeNull()
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
test('rejects dust transfers/swaps/supplies below their minimums', () => {
|
|
18
|
+
expect(checkMinimum('transfer', 1n)).not.toBeNull()
|
|
19
|
+
expect(checkMinimum('swap', 1_000_000n)).not.toBeNull() // 0.001 < 0.01
|
|
20
|
+
expect(checkMinimum('supply', 5_000_000n)).not.toBeNull() // 0.005 < 0.01
|
|
21
|
+
expect(checkMinimum('borrow', 9_999_999n)).not.toBeNull()
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
test('accepts amounts at/above the minimum', () => {
|
|
25
|
+
expect(checkMinimum('transfer', MIN_MIST.transfer)).toBeNull()
|
|
26
|
+
expect(checkMinimum('swap', MIN_MIST.swap)).toBeNull()
|
|
27
|
+
expect(checkMinimum('supply', MIN_MIST.supply)).toBeNull()
|
|
28
|
+
expect(checkMinimum('borrow', MIN_MIST.borrow)).toBeNull()
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
test('the message names the amount, the minimum, and the action', () => {
|
|
32
|
+
const err = checkMinimum('swap', 5_000_000n) // 0.005 SUI
|
|
33
|
+
expect(err).toContain('0.005 SUI')
|
|
34
|
+
expect(err).toContain('0.01 SUI minimum for swap')
|
|
35
|
+
})
|
|
36
|
+
})
|
package/src/minimums.ts
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-action minimum amounts (in MIST). Below these an on-chain call either
|
|
3
|
+
* hard-aborts (Sui native staking requires ≥ 1 SUI) or is economically
|
|
4
|
+
* pointless (dust deposits). We reject BEFORE building/simulating/executing and
|
|
5
|
+
* tell the user the minimum — so a too-small amount returns a clear
|
|
6
|
+
* "amount too small" error instead of a doomed transaction that could otherwise
|
|
7
|
+
* look like it succeeded.
|
|
8
|
+
*/
|
|
9
|
+
export const MIN_MIST = {
|
|
10
|
+
transfer: 1_000_000n, // 0.001 SUI
|
|
11
|
+
swap: 10_000_000n, // 0.01 SUI
|
|
12
|
+
supply: 10_000_000n, // 0.01 SUI (lending deposit)
|
|
13
|
+
borrow: 10_000_000n, // 0.01 SUI
|
|
14
|
+
stake: 1_000_000_000n, // 1 SUI (Sui native staking hard minimum)
|
|
15
|
+
} as const
|
|
16
|
+
|
|
17
|
+
export type MinAction = keyof typeof MIN_MIST
|
|
18
|
+
|
|
19
|
+
const fmtSui = (mist: bigint): string => {
|
|
20
|
+
const s = (Number(mist) / 1e9).toString()
|
|
21
|
+
return s
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Returns an error string when `amountMist` is below the action's minimum,
|
|
26
|
+
* otherwise null. Callers should early-return the string as `{ ok: false, error }`
|
|
27
|
+
* BEFORE executing anything.
|
|
28
|
+
*/
|
|
29
|
+
export function checkMinimum(action: MinAction, amountMist: bigint): string | null {
|
|
30
|
+
const min = MIN_MIST[action]
|
|
31
|
+
if (amountMist < min) {
|
|
32
|
+
return `amount too small: ${fmtSui(amountMist)} SUI is below the ${fmtSui(min)} SUI minimum for ${action} — try at least ${fmtSui(min)} SUI`
|
|
33
|
+
}
|
|
34
|
+
return null
|
|
35
|
+
}
|
package/src/tools/navi.ts
CHANGED
|
@@ -10,8 +10,9 @@
|
|
|
10
10
|
|
|
11
11
|
import { Transaction } from '@mysten/sui/transactions'
|
|
12
12
|
import type { ToolDef } from 'lyra-core'
|
|
13
|
-
import { NAVISDKClient, depositCoin, pool, withdrawCoin } from 'navi-sdk'
|
|
13
|
+
import { NAVISDKClient, borrowCoin, depositCoin, pool, repayDebt, withdrawCoin } from 'navi-sdk'
|
|
14
14
|
import { z } from 'zod'
|
|
15
|
+
import { checkMinimum } from '../minimums'
|
|
15
16
|
import { evaluatePolicy, suiToMist } from '../policy'
|
|
16
17
|
import { simulate } from '../simulate'
|
|
17
18
|
import type { OnchainRuntimeContext } from '../types'
|
|
@@ -107,17 +108,25 @@ type AmountArgs = z.infer<typeof AmountSchema>
|
|
|
107
108
|
async function runNaviWrite(
|
|
108
109
|
ctx: OnchainRuntimeContext,
|
|
109
110
|
amount: string,
|
|
110
|
-
kind: 'supply' | 'withdraw',
|
|
111
|
+
kind: 'supply' | 'withdraw' | 'borrow' | 'repay',
|
|
111
112
|
): Promise<{ ok: true; data: unknown } | { ok: false; error: string }> {
|
|
112
113
|
const err = ensureMainnet(ctx)
|
|
113
114
|
if (err) return { ok: false, error: err }
|
|
114
115
|
const amountMist = suiToMist(amount)
|
|
115
116
|
if (amountMist === undefined || amountMist <= 0n)
|
|
116
117
|
return { ok: false, error: `invalid amount "${amount}"` }
|
|
118
|
+
// Minimum guard for value-moving actions (withdraw brings value back in).
|
|
119
|
+
const minAction = kind === 'borrow' ? 'borrow' : kind === 'withdraw' ? null : 'supply'
|
|
120
|
+
if (minAction) {
|
|
121
|
+
const tooSmall = checkMinimum(minAction, amountMist)
|
|
122
|
+
if (tooSmall) return { ok: false, error: tooSmall }
|
|
123
|
+
}
|
|
117
124
|
|
|
118
|
-
|
|
125
|
+
// Policy gate on value-moving actions (borrow creates debt + hands out funds;
|
|
126
|
+
// repay/supply move SUI out). Withdraw pulls the agent's own funds back.
|
|
127
|
+
if (ctx.policy && kind !== 'withdraw') {
|
|
119
128
|
const verdict = evaluatePolicy(
|
|
120
|
-
{ kind: 'transfer', coinType: SUI_TYPE, amountMist, protocol: 'navi' },
|
|
129
|
+
{ kind: 'transfer', coinType: SUI_TYPE, amountMist, protocol: kind === 'borrow' ? 'borrow' : 'navi' },
|
|
121
130
|
ctx.policy,
|
|
122
131
|
)
|
|
123
132
|
if (!verdict.allowed)
|
|
@@ -130,11 +139,19 @@ async function runNaviWrite(
|
|
|
130
139
|
if (kind === 'supply') {
|
|
131
140
|
const [coin] = tx.splitCoins(tx.gas, [amountMist])
|
|
132
141
|
await depositCoin(tx as never, suiPool as never, coin as never, Number(amountMist))
|
|
133
|
-
} else {
|
|
142
|
+
} else if (kind === 'withdraw') {
|
|
134
143
|
// navi-sdk's withdrawCoin already wraps the withdrawn Balance into a Coin
|
|
135
144
|
// (via coin::from_balance) and returns [coin]; destructure and transfer it.
|
|
136
145
|
const [coin] = await withdrawCoin(tx as never, suiPool as never, Number(amountMist))
|
|
137
146
|
tx.transferObjects([coin as never], ctx.agentAddress)
|
|
147
|
+
} else if (kind === 'borrow') {
|
|
148
|
+
// Borrow against supplied collateral; hand the borrowed coin to the agent.
|
|
149
|
+
const [coin] = await borrowCoin(tx as never, suiPool as never, Number(amountMist))
|
|
150
|
+
tx.transferObjects([coin as never], ctx.agentAddress)
|
|
151
|
+
} else {
|
|
152
|
+
// repay: split the repayment from gas and pay down the debt.
|
|
153
|
+
const [coin] = tx.splitCoins(tx.gas, [amountMist])
|
|
154
|
+
await repayDebt(tx as never, suiPool as never, coin as never, Number(amountMist))
|
|
138
155
|
}
|
|
139
156
|
|
|
140
157
|
const sim = await simulate(ctx.client, tx, ctx.agentAddress)
|
|
@@ -187,3 +204,24 @@ export function makeNaviWithdraw(ctx: OnchainRuntimeContext): ToolDef<AmountArgs
|
|
|
187
204
|
handler: async args => runNaviWrite(ctx, args.amount, 'withdraw'),
|
|
188
205
|
}
|
|
189
206
|
}
|
|
207
|
+
|
|
208
|
+
export function makeNaviBorrow(ctx: OnchainRuntimeContext): ToolDef<AmountArgs> {
|
|
209
|
+
return {
|
|
210
|
+
name: 'navi.borrow',
|
|
211
|
+
description:
|
|
212
|
+
'Borrow SUI from NAVI against supplied collateral. Requires an existing supply position with enough health factor; the pre-flight simulation fails cleanly if under-collateralized. Policy-checked, simulated, then executed.',
|
|
213
|
+
searchHint: 'navi borrow loan leverage debt against collateral sui',
|
|
214
|
+
schema: AmountSchema,
|
|
215
|
+
handler: async args => runNaviWrite(ctx, args.amount, 'borrow'),
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
export function makeNaviRepay(ctx: OnchainRuntimeContext): ToolDef<AmountArgs> {
|
|
220
|
+
return {
|
|
221
|
+
name: 'navi.repay',
|
|
222
|
+
description: 'Repay borrowed SUI debt on NAVI. Simulated, then executed.',
|
|
223
|
+
searchHint: 'navi repay pay back debt loan close sui',
|
|
224
|
+
schema: AmountSchema,
|
|
225
|
+
handler: async args => runNaviWrite(ctx, args.amount, 'repay'),
|
|
226
|
+
}
|
|
227
|
+
}
|
package/src/tools/scallop.ts
CHANGED
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
import { Scallop } from '@scallop-io/sui-scallop-sdk'
|
|
13
13
|
import type { ToolDef } from 'lyra-core'
|
|
14
14
|
import { z } from 'zod'
|
|
15
|
+
import { checkMinimum } from '../minimums'
|
|
15
16
|
import { evaluatePolicy, suiToMist } from '../policy'
|
|
16
17
|
import { simulate } from '../simulate'
|
|
17
18
|
import type { OnchainRuntimeContext } from '../types'
|
|
@@ -138,6 +139,10 @@ async function runScallopWrite(
|
|
|
138
139
|
const amountMist = suiToMist(amount)
|
|
139
140
|
if (amountMist === undefined || amountMist <= 0n)
|
|
140
141
|
return { ok: false, error: `invalid amount "${amount}"` }
|
|
142
|
+
if (kind === 'supply') {
|
|
143
|
+
const tooSmall = checkMinimum('supply', amountMist)
|
|
144
|
+
if (tooSmall) return { ok: false, error: tooSmall }
|
|
145
|
+
}
|
|
141
146
|
|
|
142
147
|
// Policy gate (deterministic). Supplying moves funds out; withdrawing back in.
|
|
143
148
|
if (ctx.policy && kind === 'supply') {
|
package/src/tools/send.ts
CHANGED
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
import { Transaction } from '@mysten/sui/transactions'
|
|
12
12
|
import type { ToolDef } from 'lyra-core'
|
|
13
13
|
import { z } from 'zod'
|
|
14
|
+
import { checkMinimum } from '../minimums'
|
|
14
15
|
import { evaluatePolicy, suiToMist } from '../policy'
|
|
15
16
|
import { simulate } from '../simulate'
|
|
16
17
|
import type { OnchainRuntimeContext } from '../types'
|
|
@@ -40,6 +41,8 @@ export function makeSuiSend(ctx: OnchainRuntimeContext): ToolDef<Args> {
|
|
|
40
41
|
if (amountMist === undefined || amountMist <= 0n) {
|
|
41
42
|
return { ok: false, error: `invalid amount "${args.amount}"` }
|
|
42
43
|
}
|
|
44
|
+
const tooSmall = checkMinimum('transfer', amountMist)
|
|
45
|
+
if (tooSmall) return { ok: false, error: tooSmall }
|
|
43
46
|
|
|
44
47
|
// 1. Policy gate (deterministic) — block before simulate/execute.
|
|
45
48
|
if (ctx.policy) {
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sui native staking — delegate SUI to a validator and unstake.
|
|
3
|
+
*
|
|
4
|
+
* sui.stake → 0x3::sui_system::request_add_stake (min 1 SUI, hard on-chain)
|
|
5
|
+
* sui.unstake → 0x3::sui_system::request_withdraw_stake
|
|
6
|
+
*
|
|
7
|
+
* Same guarded pipeline as the other write tools: minimum-amount guard →
|
|
8
|
+
* deterministic policy → dry-run simulate → execute → on-chain effects check.
|
|
9
|
+
* Sui native staking rejects anything below 1 SUI, so a too-small amount returns
|
|
10
|
+
* a clear "amount too small" error BEFORE any transaction is built.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { Transaction } from '@mysten/sui/transactions'
|
|
14
|
+
import type { ToolDef } from 'lyra-core'
|
|
15
|
+
import { z } from 'zod'
|
|
16
|
+
import { checkMinimum } from '../minimums'
|
|
17
|
+
import { evaluatePolicy, suiToMist } from '../policy'
|
|
18
|
+
import { simulate } from '../simulate'
|
|
19
|
+
import type { OnchainRuntimeContext } from '../types'
|
|
20
|
+
|
|
21
|
+
const SUI_TYPE = '0x2::sui::SUI'
|
|
22
|
+
const SUI_SYSTEM_STATE = '0x5'
|
|
23
|
+
const STAKED_SUI_TYPE = '0x3::staking_pool::StakedSui'
|
|
24
|
+
|
|
25
|
+
/** Resolve a validator: match `want` by address/name, else pick a large active one. */
|
|
26
|
+
async function resolveValidator(
|
|
27
|
+
client: OnchainRuntimeContext['client'],
|
|
28
|
+
want?: string,
|
|
29
|
+
): Promise<{ address: string; name: string } | null> {
|
|
30
|
+
const state = await client.getLatestSuiSystemState()
|
|
31
|
+
// biome-ignore lint/suspicious/noExplicitAny: SuiSystemState validator fields
|
|
32
|
+
const vals = (state.activeValidators ?? []) as any[]
|
|
33
|
+
if (vals.length === 0) return null
|
|
34
|
+
if (want && want.trim()) {
|
|
35
|
+
const w = want.trim().toLowerCase()
|
|
36
|
+
const m = vals.find(
|
|
37
|
+
v => String(v.suiAddress).toLowerCase() === w || String(v.name ?? '').toLowerCase() === w,
|
|
38
|
+
)
|
|
39
|
+
if (m) return { address: m.suiAddress, name: m.name ?? '' }
|
|
40
|
+
// A raw 0x address the user gave is honored even if not in the top set.
|
|
41
|
+
if (w.startsWith('0x')) return { address: want.trim(), name: '' }
|
|
42
|
+
return null
|
|
43
|
+
}
|
|
44
|
+
// Default: the validator with the most voting power (a large, reliable one).
|
|
45
|
+
const best = vals.reduce((a, b) => (Number(b.votingPower) > Number(a.votingPower) ? b : a))
|
|
46
|
+
return { address: best.suiAddress, name: best.name ?? '' }
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const StakeSchema = z.object({
|
|
50
|
+
amount: z.string().min(1).describe('Amount of SUI to stake (minimum 1 SUI).'),
|
|
51
|
+
validator: z
|
|
52
|
+
.string()
|
|
53
|
+
.optional()
|
|
54
|
+
.describe('Validator address (0x…) or name. Optional — defaults to a large active validator.'),
|
|
55
|
+
})
|
|
56
|
+
type StakeArgs = z.infer<typeof StakeSchema>
|
|
57
|
+
|
|
58
|
+
export function makeStake(ctx: OnchainRuntimeContext): ToolDef<StakeArgs> {
|
|
59
|
+
return {
|
|
60
|
+
name: 'sui.stake',
|
|
61
|
+
description:
|
|
62
|
+
'Stake SUI to a validator (Sui native staking) to earn staking rewards. Minimum 1 SUI (enforced by the network). Policy-checked → simulated → executed; returns a StakedSui object.',
|
|
63
|
+
searchHint: 'stake staking delegate validator earn rewards yield sui native',
|
|
64
|
+
schema: StakeSchema,
|
|
65
|
+
handler: async args => {
|
|
66
|
+
try {
|
|
67
|
+
const amountMist = suiToMist(args.amount)
|
|
68
|
+
if (amountMist === undefined || amountMist <= 0n) {
|
|
69
|
+
return { ok: false, error: `invalid amount "${args.amount}"` }
|
|
70
|
+
}
|
|
71
|
+
const tooSmall = checkMinimum('stake', amountMist)
|
|
72
|
+
if (tooSmall) return { ok: false, error: tooSmall }
|
|
73
|
+
|
|
74
|
+
if (ctx.policy) {
|
|
75
|
+
const verdict = evaluatePolicy(
|
|
76
|
+
{ kind: 'transfer', coinType: SUI_TYPE, amountMist, protocol: 'stake' },
|
|
77
|
+
ctx.policy,
|
|
78
|
+
)
|
|
79
|
+
if (!verdict.allowed) {
|
|
80
|
+
return { ok: false, error: `policy blocked: ${verdict.violations.join('; ')}` }
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const validator = await resolveValidator(ctx.client, args.validator)
|
|
85
|
+
if (!validator) return { ok: false, error: 'no active validator found to stake with' }
|
|
86
|
+
|
|
87
|
+
const tx = new Transaction()
|
|
88
|
+
const [coin] = tx.splitCoins(tx.gas, [amountMist])
|
|
89
|
+
tx.moveCall({
|
|
90
|
+
target: '0x3::sui_system::request_add_stake',
|
|
91
|
+
arguments: [tx.object(SUI_SYSTEM_STATE), coin, tx.pure.address(validator.address)],
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
const sim = await simulate(ctx.client, tx, ctx.agentAddress)
|
|
95
|
+
if (!sim.ok) return { ok: false, error: `pre-flight simulation failed: ${sim.reason}` }
|
|
96
|
+
|
|
97
|
+
const res = await ctx.client.signAndExecuteTransaction({
|
|
98
|
+
signer: ctx.keypair,
|
|
99
|
+
transaction: tx,
|
|
100
|
+
options: { showEffects: true, showObjectChanges: true },
|
|
101
|
+
})
|
|
102
|
+
if (res.effects?.status?.status !== 'success') {
|
|
103
|
+
return { ok: false, error: `execution failed: ${res.effects?.status?.error ?? 'unknown'}` }
|
|
104
|
+
}
|
|
105
|
+
await ctx.client.waitForTransaction({ digest: res.digest })
|
|
106
|
+
const staked = res.objectChanges?.find(
|
|
107
|
+
c =>
|
|
108
|
+
c.type === 'created' &&
|
|
109
|
+
String((c as { objectType?: string }).objectType).includes('staking_pool::StakedSui'),
|
|
110
|
+
) as { objectId?: string } | undefined
|
|
111
|
+
|
|
112
|
+
return {
|
|
113
|
+
ok: true,
|
|
114
|
+
data: {
|
|
115
|
+
protocol: 'sui-staking',
|
|
116
|
+
action: 'stake',
|
|
117
|
+
amountSui: args.amount,
|
|
118
|
+
validator: validator.address,
|
|
119
|
+
validatorName: validator.name || undefined,
|
|
120
|
+
digest: res.digest,
|
|
121
|
+
stakedSuiId: staked?.objectId ?? null,
|
|
122
|
+
status: 'success',
|
|
123
|
+
},
|
|
124
|
+
}
|
|
125
|
+
} catch (e) {
|
|
126
|
+
return { ok: false, error: (e as Error).message.slice(0, 240) }
|
|
127
|
+
}
|
|
128
|
+
},
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const UnstakeSchema = z.object({
|
|
133
|
+
stakedSuiId: z
|
|
134
|
+
.string()
|
|
135
|
+
.optional()
|
|
136
|
+
.describe('StakedSui object id to withdraw. Optional — defaults to the first staked position.'),
|
|
137
|
+
})
|
|
138
|
+
type UnstakeArgs = z.infer<typeof UnstakeSchema>
|
|
139
|
+
|
|
140
|
+
export function makeUnstake(ctx: OnchainRuntimeContext): ToolDef<UnstakeArgs> {
|
|
141
|
+
return {
|
|
142
|
+
name: 'sui.unstake',
|
|
143
|
+
description:
|
|
144
|
+
'Withdraw staked SUI (unstake) from a validator, returning principal + earned rewards. Uses a specific StakedSui object id, or the first staked position if omitted.',
|
|
145
|
+
searchHint: 'unstake withdraw staking redeem claim rewards validator',
|
|
146
|
+
schema: UnstakeSchema,
|
|
147
|
+
handler: async args => {
|
|
148
|
+
try {
|
|
149
|
+
let stakedId = args.stakedSuiId?.trim()
|
|
150
|
+
if (!stakedId) {
|
|
151
|
+
const owned = await ctx.client.getOwnedObjects({
|
|
152
|
+
owner: ctx.agentAddress,
|
|
153
|
+
filter: { StructType: STAKED_SUI_TYPE },
|
|
154
|
+
})
|
|
155
|
+
stakedId = owned.data[0]?.data?.objectId
|
|
156
|
+
if (!stakedId) return { ok: false, error: 'no staked SUI position found to unstake' }
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (ctx.policy) {
|
|
160
|
+
const verdict = evaluatePolicy(
|
|
161
|
+
{ kind: 'transfer', coinType: SUI_TYPE, amountMist: 0n, protocol: 'stake' },
|
|
162
|
+
ctx.policy,
|
|
163
|
+
)
|
|
164
|
+
if (!verdict.allowed) {
|
|
165
|
+
return { ok: false, error: `policy blocked: ${verdict.violations.join('; ')}` }
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const tx = new Transaction()
|
|
170
|
+
tx.moveCall({
|
|
171
|
+
target: '0x3::sui_system::request_withdraw_stake',
|
|
172
|
+
arguments: [tx.object(SUI_SYSTEM_STATE), tx.object(stakedId)],
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
const sim = await simulate(ctx.client, tx, ctx.agentAddress)
|
|
176
|
+
if (!sim.ok) return { ok: false, error: `pre-flight simulation failed: ${sim.reason}` }
|
|
177
|
+
|
|
178
|
+
const res = await ctx.client.signAndExecuteTransaction({
|
|
179
|
+
signer: ctx.keypair,
|
|
180
|
+
transaction: tx,
|
|
181
|
+
options: { showEffects: true },
|
|
182
|
+
})
|
|
183
|
+
if (res.effects?.status?.status !== 'success') {
|
|
184
|
+
return { ok: false, error: `execution failed: ${res.effects?.status?.error ?? 'unknown'}` }
|
|
185
|
+
}
|
|
186
|
+
await ctx.client.waitForTransaction({ digest: res.digest })
|
|
187
|
+
|
|
188
|
+
return {
|
|
189
|
+
ok: true,
|
|
190
|
+
data: {
|
|
191
|
+
protocol: 'sui-staking',
|
|
192
|
+
action: 'unstake',
|
|
193
|
+
stakedSuiId: stakedId,
|
|
194
|
+
digest: res.digest,
|
|
195
|
+
status: 'success',
|
|
196
|
+
},
|
|
197
|
+
}
|
|
198
|
+
} catch (e) {
|
|
199
|
+
return { ok: false, error: (e as Error).message.slice(0, 240) }
|
|
200
|
+
}
|
|
201
|
+
},
|
|
202
|
+
}
|
|
203
|
+
}
|
package/src/tools/swap.ts
CHANGED
|
@@ -13,6 +13,7 @@ import { MetaAg } from '@7kprotocol/sdk-ts'
|
|
|
13
13
|
import { Transaction, coinWithBalance } from '@mysten/sui/transactions'
|
|
14
14
|
import type { ToolDef } from 'lyra-core'
|
|
15
15
|
import { z } from 'zod'
|
|
16
|
+
import { checkMinimum } from '../minimums'
|
|
16
17
|
import { evaluatePolicy } from '../policy'
|
|
17
18
|
import { simulate } from '../simulate'
|
|
18
19
|
import type { OnchainRuntimeContext } from '../types'
|
|
@@ -63,6 +64,11 @@ export function makeSwap(ctx: OnchainRuntimeContext): ToolDef<Args> {
|
|
|
63
64
|
if (from.type === to.type) return { ok: false, error: 'from and to are the same coin' }
|
|
64
65
|
const amountIn = BigInt(Math.round(Number(args.amount) * 10 ** from.decimals))
|
|
65
66
|
if (amountIn <= 0n) return { ok: false, error: `invalid amount "${args.amount}"` }
|
|
67
|
+
// Minimum only bounds SUI-denominated input (amountIn is then in MIST).
|
|
68
|
+
if (from.type === SUI_TYPE) {
|
|
69
|
+
const tooSmall = checkMinimum('swap', amountIn)
|
|
70
|
+
if (tooSmall) return { ok: false, error: tooSmall }
|
|
71
|
+
}
|
|
66
72
|
|
|
67
73
|
// Policy gate. The MIST per-tx cap is SUI-denominated, so it only bounds
|
|
68
74
|
// SUI-input swaps; the slippage/protocol/expiry checks always apply.
|