lyra-plugin-onchain 0.1.8 → 0.1.10
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 +2 -2
- package/src/client.ts +12 -3
- package/src/index.ts +13 -5
- package/src/protocol-ids.ts +48 -0
- package/src/protocols.ts +2 -9
- package/src/tools/liquid-stake.ts +8 -1
- package/src/tools/navi.ts +22 -7
- package/src/tools/scallop.ts +59 -42
- package/src/tools/send.ts +35 -9
- package/src/tools/stake.ts +9 -1
- package/src/tools/suilend.ts +101 -39
- package/src/tools/swap.ts +9 -1
- package/src/types.ts +21 -0
- package/src/vault-fund.ts +107 -0
- package/src/vault.ts +64 -0
- package/src/derive.test.ts +0 -45
- package/src/minimums.test.ts +0 -36
- package/src/policy.test.ts +0 -187
- package/src/tools/onchain.integration.test.ts +0 -111
- package/src/tools/tools.test.ts +0 -142
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "lyra-plugin-onchain",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.10",
|
|
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",
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
"@scallop-io/sui-scallop-sdk": "^2.4.5",
|
|
38
38
|
"@suilend/sdk": "1.1.99",
|
|
39
39
|
"@suilend/sui-fe": "0.3.49",
|
|
40
|
-
"lyra-core": "^0.1.
|
|
40
|
+
"lyra-core": "^0.1.10",
|
|
41
41
|
"navi-sdk": "^1.7.3",
|
|
42
42
|
"zod": "^3.23.8"
|
|
43
43
|
}
|
package/src/client.ts
CHANGED
|
@@ -11,14 +11,23 @@ import { Ed25519Keypair } from '@mysten/sui/keypairs/ed25519'
|
|
|
11
11
|
|
|
12
12
|
export type SuiNetwork = 'testnet' | 'mainnet'
|
|
13
13
|
|
|
14
|
-
/**
|
|
14
|
+
/**
|
|
15
|
+
* Fullnode JSON-RPC URL for a network. `LYRA_RPC_URL` overrides the default
|
|
16
|
+
* public endpoint — recommended in production: the public `fullnode.mainnet.
|
|
17
|
+
* sui.io` is load-balanced across replicas at different checkpoint heights, so
|
|
18
|
+
* a freshly-created object (e.g. a just-opened lending obligation) can flicker
|
|
19
|
+
* in and out between calls. A single dedicated node gives read-your-writes
|
|
20
|
+
* consistency. The override applies to mainnet only (testnet keeps its default).
|
|
21
|
+
*/
|
|
15
22
|
export function suiRpcUrl(network: SuiNetwork): string {
|
|
23
|
+
const override = process.env.LYRA_RPC_URL?.trim()
|
|
24
|
+
if (network === 'mainnet' && override) return override
|
|
16
25
|
return getFullnodeUrl(network)
|
|
17
26
|
}
|
|
18
27
|
|
|
19
|
-
/** A Sui JSON-RPC client for the given network. */
|
|
28
|
+
/** A Sui JSON-RPC client for the given network (honours `LYRA_RPC_URL`). */
|
|
20
29
|
export function makeSuiClient(network: SuiNetwork): SuiClient {
|
|
21
|
-
return new SuiClient({ url:
|
|
30
|
+
return new SuiClient({ url: suiRpcUrl(network) })
|
|
22
31
|
}
|
|
23
32
|
|
|
24
33
|
/**
|
package/src/index.ts
CHANGED
|
@@ -37,10 +37,8 @@ import {
|
|
|
37
37
|
import { makePolicyCreate, makePolicyShow } from './tools/policy'
|
|
38
38
|
import { makeProtocolsList } from './tools/protocols'
|
|
39
39
|
import {
|
|
40
|
-
makeScallopBorrow,
|
|
41
40
|
makeScallopMarkets,
|
|
42
41
|
makeScallopPosition,
|
|
43
|
-
makeScallopRepay,
|
|
44
42
|
makeScallopSupply,
|
|
45
43
|
makeScallopWithdraw,
|
|
46
44
|
} from './tools/scallop'
|
|
@@ -76,9 +74,21 @@ export {
|
|
|
76
74
|
} from './policy'
|
|
77
75
|
export { policyRequiresApprovalForCall } from './approval'
|
|
78
76
|
export { deriveAgentKeypair, deriveAgentAddress } from './derive'
|
|
79
|
-
export {
|
|
77
|
+
export {
|
|
78
|
+
resolveOwnerVault,
|
|
79
|
+
resolveVaultForAgent,
|
|
80
|
+
type OwnerVault,
|
|
81
|
+
type AgentVault,
|
|
82
|
+
} from './vault'
|
|
80
83
|
export { ONCHAIN_GUIDANCE } from './guidance'
|
|
81
84
|
export type { OnchainRuntimeContext } from './types'
|
|
85
|
+
export {
|
|
86
|
+
PROTOCOL_IDS,
|
|
87
|
+
PROTOCOL_LABELS,
|
|
88
|
+
ALLOWLISTABLE_PROTOCOLS,
|
|
89
|
+
NO_PROTOCOL,
|
|
90
|
+
type ProtocolKey,
|
|
91
|
+
} from './protocol-ids'
|
|
82
92
|
|
|
83
93
|
const plugin: NativePlugin = {
|
|
84
94
|
name: 'onchain',
|
|
@@ -105,8 +115,6 @@ const plugin: NativePlugin = {
|
|
|
105
115
|
ctx.registerTool(makeScallopPosition(onchain) as ToolDef)
|
|
106
116
|
ctx.registerTool(makeScallopSupply(onchain) as ToolDef)
|
|
107
117
|
ctx.registerTool(makeScallopWithdraw(onchain) as ToolDef)
|
|
108
|
-
ctx.registerTool(makeScallopBorrow(onchain) as ToolDef)
|
|
109
|
-
ctx.registerTool(makeScallopRepay(onchain) as ToolDef)
|
|
110
118
|
ctx.registerTool(makeNaviMarkets(onchain) as ToolDef)
|
|
111
119
|
ctx.registerTool(makeNaviPosition(onchain) as ToolDef)
|
|
112
120
|
ctx.registerTool(makeNaviSupply(onchain) as ToolDef)
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canonical on-chain protocol ids for the policy's protocol allowlist.
|
|
3
|
+
*
|
|
4
|
+
* When a tool draws from the vault via `vault_spend`, it tags the action with the
|
|
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
|
|
12
|
+
* (the tag is a self-reported label for the allowlist, not the call target), so an
|
|
13
|
+
* owner's allowlist keeps working across protocol upgrades. Sourced from each
|
|
14
|
+
* protocol's SDK on mainnet.
|
|
15
|
+
*
|
|
16
|
+
* `0x0` is the reserved "no specific protocol" tag used by transfers + swaps —
|
|
17
|
+
* always allowed by the policy (they're bounded by budget/cap/coin/recipient/
|
|
18
|
+
* slippage instead), so restricting protocols never blocks a plain transfer/swap.
|
|
19
|
+
*/
|
|
20
|
+
export const NO_PROTOCOL = '0x0'
|
|
21
|
+
|
|
22
|
+
export const PROTOCOL_IDS = {
|
|
23
|
+
suiStaking: '0x3',
|
|
24
|
+
navi: '0x1e4a13a0494d5facdbe8473e74127b838c2d446ecec0ce262e2eddafa77259cb',
|
|
25
|
+
suilend: '0xf95b06141ed4a174f239417323bde3f209b972f5930d8521ea38a52aff3a6ddf',
|
|
26
|
+
volo: '0x68d22cf8bdbcd11ecba1e094922873e4080d4d11133e2443fddda0bfd11dae20',
|
|
27
|
+
scallop: '0xde5c09ad171544aa3724dc67216668c80e754860f419136a68d78504eb2e2805',
|
|
28
|
+
} as const
|
|
29
|
+
|
|
30
|
+
export type ProtocolKey = keyof typeof PROTOCOL_IDS
|
|
31
|
+
|
|
32
|
+
/** Human labels for each allowlistable protocol (for the owner UI + receipts). */
|
|
33
|
+
export const PROTOCOL_LABELS: Record<string, string> = {
|
|
34
|
+
[PROTOCOL_IDS.suiStaking]: 'Sui staking',
|
|
35
|
+
[PROTOCOL_IDS.navi]: 'NAVI',
|
|
36
|
+
[PROTOCOL_IDS.suilend]: 'Suilend',
|
|
37
|
+
[PROTOCOL_IDS.volo]: 'Volo (vSUI)',
|
|
38
|
+
[PROTOCOL_IDS.scallop]: 'Scallop',
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** The allowlistable protocols in display order, for building the owner UI. */
|
|
42
|
+
export const ALLOWLISTABLE_PROTOCOLS: { key: ProtocolKey; id: string; label: string }[] = [
|
|
43
|
+
{ key: 'suiStaking', id: PROTOCOL_IDS.suiStaking, label: 'Sui staking' },
|
|
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
|
+
]
|
package/src/protocols.ts
CHANGED
|
@@ -51,16 +51,9 @@ export const PROTOCOLS: ProtocolCapability[] = [
|
|
|
51
51
|
category: 'lending',
|
|
52
52
|
read: true,
|
|
53
53
|
execute: true,
|
|
54
|
-
tools: [
|
|
55
|
-
'scallop.markets',
|
|
56
|
-
'scallop.position',
|
|
57
|
-
'scallop.supply',
|
|
58
|
-
'scallop.withdraw',
|
|
59
|
-
'scallop.borrow',
|
|
60
|
-
'scallop.repay',
|
|
61
|
-
],
|
|
54
|
+
tools: ['scallop.markets', 'scallop.position', 'scallop.supply', 'scallop.withdraw'],
|
|
62
55
|
llamaProjects: ['scallop-lending', 'scallop'],
|
|
63
|
-
note: '
|
|
56
|
+
note: 'supply / withdraw lending yield (borrow/repay via NAVI or Suilend)',
|
|
64
57
|
},
|
|
65
58
|
{
|
|
66
59
|
id: 'navi',
|
|
@@ -16,8 +16,10 @@ import { stakeTovSuiPTB, unstakeTovSui } from 'navi-sdk'
|
|
|
16
16
|
import { z } from 'zod'
|
|
17
17
|
import { checkMinimum } from '../minimums'
|
|
18
18
|
import { evaluatePolicy, suiToMist } from '../policy'
|
|
19
|
+
import { PROTOCOL_IDS } from '../protocol-ids'
|
|
19
20
|
import { simulate } from '../simulate'
|
|
20
21
|
import type { OnchainRuntimeContext } from '../types'
|
|
22
|
+
import { fundSui } from '../vault-fund'
|
|
21
23
|
|
|
22
24
|
const SUI_TYPE = '0x2::sui::SUI'
|
|
23
25
|
const VSUI_TYPE = '0x549e8b69270defbfafd4f94e17ec44cdbdd99820b33bda2278dea3b9a32d3f55::cert::CERT'
|
|
@@ -61,7 +63,12 @@ export function makeVoloStake(ctx: OnchainRuntimeContext): ToolDef<StakeArgs> {
|
|
|
61
63
|
}
|
|
62
64
|
|
|
63
65
|
const tx = new Transaction()
|
|
64
|
-
|
|
66
|
+
// Source the stake from the treasury vault (policy-enforced) when wired.
|
|
67
|
+
const suiCoin = fundSui(tx, ctx, amountMist, {
|
|
68
|
+
protocol: PROTOCOL_IDS.volo,
|
|
69
|
+
kind: 'stake',
|
|
70
|
+
memo: 'volo liquid stake',
|
|
71
|
+
})
|
|
65
72
|
const vsui = await stakeTovSuiPTB(tx as never, suiCoin as never)
|
|
66
73
|
tx.transferObjects([vsui as never], ctx.agentAddress)
|
|
67
74
|
|
package/src/tools/navi.ts
CHANGED
|
@@ -14,8 +14,10 @@ import { NAVISDKClient, borrowCoin, depositCoin, pool, repayDebt, withdrawCoin }
|
|
|
14
14
|
import { z } from 'zod'
|
|
15
15
|
import { checkMinimum } from '../minimums'
|
|
16
16
|
import { evaluatePolicy, suiToMist } from '../policy'
|
|
17
|
+
import { PROTOCOL_IDS } from '../protocol-ids'
|
|
17
18
|
import { simulate } from '../simulate'
|
|
18
19
|
import type { OnchainRuntimeContext } from '../types'
|
|
20
|
+
import { fundSui, returnSuiToVault } from '../vault-fund'
|
|
19
21
|
|
|
20
22
|
const SUI_TYPE = '0x2::sui::SUI'
|
|
21
23
|
|
|
@@ -142,20 +144,33 @@ async function runNaviWrite(
|
|
|
142
144
|
const tx = new Transaction()
|
|
143
145
|
const suiPool = (pool as Record<string, unknown>).Sui
|
|
144
146
|
if (kind === 'supply') {
|
|
145
|
-
|
|
147
|
+
// Source the supply from the treasury vault (policy-enforced) when wired.
|
|
148
|
+
const coin = fundSui(tx, ctx, amountMist, {
|
|
149
|
+
protocol: PROTOCOL_IDS.navi,
|
|
150
|
+
kind: 'supply',
|
|
151
|
+
memo: 'navi supply',
|
|
152
|
+
})
|
|
146
153
|
await depositCoin(tx as never, suiPool as never, coin as never, Number(amountMist))
|
|
147
154
|
} else if (kind === 'withdraw') {
|
|
148
155
|
// navi-sdk's withdrawCoin already wraps the withdrawn Balance into a Coin
|
|
149
|
-
// (via coin::from_balance) and returns [coin]
|
|
156
|
+
// (via coin::from_balance) and returns [coin]. Option 1: cycle it back into
|
|
157
|
+
// the vault (treasury stays intact); fall back to the agent when no vault.
|
|
150
158
|
const [coin] = await withdrawCoin(tx as never, suiPool as never, Number(amountMist))
|
|
151
|
-
tx
|
|
159
|
+
if (!returnSuiToVault(tx, ctx, coin as never))
|
|
160
|
+
tx.transferObjects([coin as never], ctx.agentAddress)
|
|
152
161
|
} else if (kind === 'borrow') {
|
|
153
|
-
// Borrow against supplied collateral;
|
|
162
|
+
// Borrow against supplied collateral; park the borrowed SUI in the vault
|
|
163
|
+
// (spendable under policy) when wired, else hand it to the agent.
|
|
154
164
|
const [coin] = await borrowCoin(tx as never, suiPool as never, Number(amountMist))
|
|
155
|
-
tx
|
|
165
|
+
if (!returnSuiToVault(tx, ctx, coin as never))
|
|
166
|
+
tx.transferObjects([coin as never], ctx.agentAddress)
|
|
156
167
|
} else {
|
|
157
|
-
// repay:
|
|
158
|
-
const
|
|
168
|
+
// repay: draw the repayment from the vault (policy-enforced) and pay the debt.
|
|
169
|
+
const coin = fundSui(tx, ctx, amountMist, {
|
|
170
|
+
protocol: PROTOCOL_IDS.navi,
|
|
171
|
+
kind: 'repay',
|
|
172
|
+
memo: 'navi repay',
|
|
173
|
+
})
|
|
159
174
|
await repayDebt(tx as never, suiPool as never, coin as never, Number(amountMist))
|
|
160
175
|
}
|
|
161
176
|
|
package/src/tools/scallop.ts
CHANGED
|
@@ -14,8 +14,10 @@ import type { ToolDef } from 'lyra-core'
|
|
|
14
14
|
import { z } from 'zod'
|
|
15
15
|
import { checkMinimum } from '../minimums'
|
|
16
16
|
import { evaluatePolicy, suiToMist } from '../policy'
|
|
17
|
+
import { PROTOCOL_IDS } from '../protocol-ids'
|
|
17
18
|
import { simulate } from '../simulate'
|
|
18
19
|
import type { OnchainRuntimeContext } from '../types'
|
|
20
|
+
import { fundSui, returnSuiToVault } from '../vault-fund'
|
|
19
21
|
|
|
20
22
|
const SUI_TYPE = '0x2::sui::SUI'
|
|
21
23
|
|
|
@@ -129,32 +131,30 @@ const AmountSchema = z.object({
|
|
|
129
131
|
})
|
|
130
132
|
type AmountArgs = z.infer<typeof AmountSchema>
|
|
131
133
|
|
|
134
|
+
// Scallop redeems a MARKET-COIN amount on withdraw (not underlying SUI). We
|
|
135
|
+
// find the agent's SUI market coin by type pattern (robust to package upgrades).
|
|
136
|
+
const MARKET_COIN_SUI_RE = /::reserve::MarketCoin<0x0*2::sui::SUI>$/
|
|
137
|
+
|
|
132
138
|
async function runScallopWrite(
|
|
133
139
|
ctx: OnchainRuntimeContext,
|
|
134
140
|
amount: string,
|
|
135
|
-
kind: 'supply' | 'withdraw'
|
|
141
|
+
kind: 'supply' | 'withdraw',
|
|
136
142
|
): Promise<{ ok: true; data: unknown } | { ok: false; error: string }> {
|
|
137
143
|
const err = ensureMainnet(ctx)
|
|
138
144
|
if (err) return { ok: false, error: err }
|
|
139
145
|
const amountMist = suiToMist(amount)
|
|
140
146
|
if (amountMist === undefined || amountMist <= 0n)
|
|
141
147
|
return { ok: false, error: `invalid amount "${amount}"` }
|
|
142
|
-
// Minimum guard
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
const tooSmall = checkMinimum(minAction, amountMist)
|
|
148
|
+
// Minimum guard on supply (withdraw pulls the agent's own funds back).
|
|
149
|
+
if (kind === 'supply') {
|
|
150
|
+
const tooSmall = checkMinimum('supply', amountMist)
|
|
146
151
|
if (tooSmall) return { ok: false, error: tooSmall }
|
|
147
152
|
}
|
|
148
153
|
|
|
149
|
-
// Policy gate on value-moving
|
|
150
|
-
if (ctx.policy && kind
|
|
154
|
+
// Policy gate on the value-moving supply.
|
|
155
|
+
if (ctx.policy && kind === 'supply') {
|
|
151
156
|
const verdict = evaluatePolicy(
|
|
152
|
-
{
|
|
153
|
-
kind: 'transfer',
|
|
154
|
-
coinType: SUI_TYPE,
|
|
155
|
-
amountMist,
|
|
156
|
-
protocol: kind === 'borrow' ? 'borrow' : 'scallop',
|
|
157
|
-
},
|
|
157
|
+
{ kind: 'transfer', coinType: SUI_TYPE, amountMist, protocol: 'scallop' },
|
|
158
158
|
ctx.policy,
|
|
159
159
|
)
|
|
160
160
|
if (!verdict.allowed)
|
|
@@ -166,16 +166,48 @@ async function runScallopWrite(
|
|
|
166
166
|
const builder = await sdk.createScallopBuilder()
|
|
167
167
|
const tx = builder.createTxBlock()
|
|
168
168
|
tx.setSender(ctx.agentAddress)
|
|
169
|
-
// *Quick helpers auto-select the user's coins/obligation. supply/withdraw and
|
|
170
|
-
// borrow return a coin to hand back; repay consumes coins. depositQuick's 3rd
|
|
171
|
-
// arg returnSCoin=false keeps the old market-coin standard (withdrawQuick
|
|
172
|
-
// redeems it; the new sCoin standard → "No valid coins").
|
|
173
169
|
let out: unknown
|
|
174
|
-
if (kind === 'supply')
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
170
|
+
if (kind === 'supply') {
|
|
171
|
+
// Source the deposit from the treasury vault (policy-enforced) when wired,
|
|
172
|
+
// then hand the vault-drawn Coin<SUI> to Scallop's non-quick `deposit` (the
|
|
173
|
+
// `*Quick` helpers auto-select the agent's own coins, so they can't be vault-
|
|
174
|
+
// funded). `deposit` returns the market coin, sent to the agent below.
|
|
175
|
+
const coin = fundSui(tx.txBlock as never, ctx, amountMist, {
|
|
176
|
+
protocol: PROTOCOL_IDS.scallop,
|
|
177
|
+
kind: 'supply',
|
|
178
|
+
memo: 'scallop supply',
|
|
179
|
+
})
|
|
180
|
+
out = await tx.deposit(coin as never, 'sui')
|
|
181
|
+
} else {
|
|
182
|
+
// withdrawQuick redeems a MARKET-COIN amount, not underlying SUI. Convert
|
|
183
|
+
// the requested SUI to market coins at the position's rate and clamp to the
|
|
184
|
+
// held balance, so a full/over-withdraw cleanly redeems everything.
|
|
185
|
+
const [balances, q] = await Promise.all([
|
|
186
|
+
ctx.client.getAllBalances({ owner: ctx.agentAddress }),
|
|
187
|
+
sdk.createScallopQuery().then(async query => {
|
|
188
|
+
await query.init()
|
|
189
|
+
return query
|
|
190
|
+
}),
|
|
191
|
+
])
|
|
192
|
+
const mc = balances.find(b => MARKET_COIN_SUI_RE.test(b.coinType))
|
|
193
|
+
const heldMc = BigInt(mc?.totalBalance ?? '0')
|
|
194
|
+
if (heldMc <= 0n) return { ok: false, error: 'no Scallop SUI position to withdraw' }
|
|
195
|
+
const port = (await q.getUserPortfolio({ walletAddress: ctx.agentAddress })) as {
|
|
196
|
+
lendings?: Array<{ coinName?: string; suppliedCoin?: number }>
|
|
197
|
+
}
|
|
198
|
+
const supplied = (port.lendings ?? []).find(l => l.coinName === 'sui')?.suppliedCoin ?? 0
|
|
199
|
+
const suppliedMist = BigInt(Math.floor(supplied * 1e9))
|
|
200
|
+
// redeem = requested/rate, where rate = supplied/heldMc; clamp to heldMc.
|
|
201
|
+
let redeemMc = suppliedMist > 0n ? (amountMist * heldMc) / suppliedMist : heldMc
|
|
202
|
+
if (redeemMc > heldMc || redeemMc <= 0n) redeemMc = heldMc
|
|
203
|
+
out = await tx.withdrawQuick(Number(redeemMc), 'sui')
|
|
204
|
+
}
|
|
205
|
+
if (out) {
|
|
206
|
+
// Option 1: cycle withdrawn SUI back into the vault (treasury stays intact);
|
|
207
|
+
// supply's market coin and the no-vault case go to the agent.
|
|
208
|
+
const cycled = kind === 'withdraw' && returnSuiToVault(tx.txBlock as never, ctx, out as never)
|
|
209
|
+
if (!cycled) tx.transferObjects([out as never], ctx.agentAddress)
|
|
210
|
+
}
|
|
179
211
|
const transaction = tx.txBlock
|
|
180
212
|
|
|
181
213
|
const sim = await simulate(ctx.client, transaction, ctx.agentAddress)
|
|
@@ -229,23 +261,8 @@ export function makeScallopWithdraw(ctx: OnchainRuntimeContext): ToolDef<AmountA
|
|
|
229
261
|
}
|
|
230
262
|
}
|
|
231
263
|
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
searchHint: 'scallop borrow loan leverage debt against collateral sui',
|
|
238
|
-
schema: AmountSchema,
|
|
239
|
-
handler: async args => runScallopWrite(ctx, args.amount, 'borrow'),
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
export function makeScallopRepay(ctx: OnchainRuntimeContext): ToolDef<AmountArgs> {
|
|
244
|
-
return {
|
|
245
|
-
name: 'scallop.repay',
|
|
246
|
-
description: 'Repay borrowed SUI debt on Scallop. Simulated, then executed.',
|
|
247
|
-
searchHint: 'scallop repay pay back debt loan close sui',
|
|
248
|
-
schema: AmountSchema,
|
|
249
|
-
handler: async args => runScallopWrite(ctx, args.amount, 'repay'),
|
|
250
|
-
}
|
|
251
|
-
}
|
|
264
|
+
// NOTE: Scallop borrow/repay require opening a Scallop obligation + posting
|
|
265
|
+
// collateral (a separate flow from lending deposits — borrowQuick alone aborts
|
|
266
|
+
// "No obligation found"). Until that flow is wired, borrowing routes through the
|
|
267
|
+
// verified NAVI/Suilend adapters instead, so we don't ship a borrow tool here
|
|
268
|
+
// that can't execute.
|
package/src/tools/send.ts
CHANGED
|
@@ -15,6 +15,7 @@ import { checkMinimum } from '../minimums'
|
|
|
15
15
|
import { evaluatePolicy, suiToMist } from '../policy'
|
|
16
16
|
import { simulate } from '../simulate'
|
|
17
17
|
import type { OnchainRuntimeContext } from '../types'
|
|
18
|
+
import { canFundFromVault } from '../vault-fund'
|
|
18
19
|
|
|
19
20
|
const SUI_TYPE = '0x2::sui::SUI'
|
|
20
21
|
|
|
@@ -55,26 +56,51 @@ export function makeSuiSend(ctx: OnchainRuntimeContext): ToolDef<Args> {
|
|
|
55
56
|
}
|
|
56
57
|
}
|
|
57
58
|
|
|
58
|
-
// 2. Build the PTB
|
|
59
|
+
// 2. Build the PTB.
|
|
59
60
|
const tx = new Transaction()
|
|
60
|
-
const
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
61
|
+
const vaultBacked = canFundFromVault(ctx, amountMist)
|
|
62
|
+
if (vaultBacked) {
|
|
63
|
+
// Vault-backed transfer: draw from the treasury via the full policy gate
|
|
64
|
+
// AND enforce the recipient allowlist ON-CHAIN (vault_transfer sends the
|
|
65
|
+
// funds to `to` and routes the ActionReceipt to the owner). A prompt-
|
|
66
|
+
// injected agent can't pay an un-allowlisted address even within budget.
|
|
64
67
|
tx.moveCall({
|
|
65
|
-
target: `${ctx.packageId}::
|
|
68
|
+
target: `${ctx.packageId}::vault::vault_transfer`,
|
|
66
69
|
typeArguments: [SUI_TYPE],
|
|
67
70
|
arguments: [
|
|
71
|
+
tx.object(ctx.vaultId as string),
|
|
68
72
|
tx.object(ctx.policyObjectId as string),
|
|
69
73
|
tx.pure.u64(amountMist),
|
|
70
|
-
tx.pure.address(
|
|
71
|
-
tx.pure.vector('u8', Array.from(new TextEncoder().encode('transfer'))),
|
|
72
|
-
tx.pure.vector('u8', Array.from(new TextEncoder().encode(`to ${to}`))),
|
|
74
|
+
tx.pure.address(to),
|
|
75
|
+
tx.pure.vector('u8', Array.from(new TextEncoder().encode('send transfer'))),
|
|
73
76
|
tx.object.clock(),
|
|
74
77
|
],
|
|
75
78
|
})
|
|
79
|
+
} else {
|
|
80
|
+
// Single-key mode (no vault wired): split from the agent's coin + transfer,
|
|
81
|
+
// plus an on-chain receipt when a policy object is configured.
|
|
82
|
+
const [coin] = tx.splitCoins(tx.gas, [amountMist])
|
|
83
|
+
tx.transferObjects([coin], to)
|
|
84
|
+
if (ctx.packageId && ctx.policyObjectId) {
|
|
85
|
+
tx.moveCall({
|
|
86
|
+
target: `${ctx.packageId}::policy::record_action`,
|
|
87
|
+
typeArguments: [SUI_TYPE],
|
|
88
|
+
arguments: [
|
|
89
|
+
tx.object(ctx.policyObjectId as string),
|
|
90
|
+
tx.pure.u64(amountMist),
|
|
91
|
+
tx.pure.address('0x0'),
|
|
92
|
+
tx.pure.vector('u8', Array.from(new TextEncoder().encode('transfer'))),
|
|
93
|
+
tx.pure.vector('u8', Array.from(new TextEncoder().encode(`to ${to}`))),
|
|
94
|
+
tx.object.clock(),
|
|
95
|
+
],
|
|
96
|
+
})
|
|
97
|
+
}
|
|
76
98
|
}
|
|
77
99
|
|
|
100
|
+
// Both paths enforce on-chain when configured: vault_transfer always mints
|
|
101
|
+
// a receipt; the gas path records only when a policy object is set.
|
|
102
|
+
const recordsOnChain = vaultBacked || Boolean(ctx.packageId && ctx.policyObjectId)
|
|
103
|
+
|
|
78
104
|
// 3. Simulate-before-write.
|
|
79
105
|
const sim = await simulate(ctx.client, tx, ctx.agentAddress)
|
|
80
106
|
if (!sim.ok) {
|
package/src/tools/stake.ts
CHANGED
|
@@ -15,8 +15,10 @@ import type { ToolDef } from 'lyra-core'
|
|
|
15
15
|
import { z } from 'zod'
|
|
16
16
|
import { checkMinimum } from '../minimums'
|
|
17
17
|
import { evaluatePolicy, suiToMist } from '../policy'
|
|
18
|
+
import { PROTOCOL_IDS } from '../protocol-ids'
|
|
18
19
|
import { simulate } from '../simulate'
|
|
19
20
|
import type { OnchainRuntimeContext } from '../types'
|
|
21
|
+
import { fundSui } from '../vault-fund'
|
|
20
22
|
|
|
21
23
|
const SUI_TYPE = '0x2::sui::SUI'
|
|
22
24
|
const SUI_SYSTEM_STATE = '0x5'
|
|
@@ -85,7 +87,13 @@ export function makeStake(ctx: OnchainRuntimeContext): ToolDef<StakeArgs> {
|
|
|
85
87
|
if (!validator) return { ok: false, error: 'no active validator found to stake with' }
|
|
86
88
|
|
|
87
89
|
const tx = new Transaction()
|
|
88
|
-
|
|
90
|
+
// Source the stake from the treasury vault (policy-enforced) when one is
|
|
91
|
+
// wired; otherwise from the agent's gas coin (single-key mode).
|
|
92
|
+
const coin = fundSui(tx, ctx, amountMist, {
|
|
93
|
+
protocol: PROTOCOL_IDS.suiStaking,
|
|
94
|
+
kind: 'stake',
|
|
95
|
+
memo: `stake ${args.amount} SUI`,
|
|
96
|
+
})
|
|
89
97
|
tx.moveCall({
|
|
90
98
|
target: '0x3::sui_system::request_add_stake',
|
|
91
99
|
arguments: [tx.object(SUI_SYSTEM_STATE), coin, tx.pure.address(validator.address)],
|