lyra-plugin-onchain 0.1.0

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.
@@ -0,0 +1,217 @@
1
+ /**
2
+ * Scallop lending tools (supporting integration): markets + position (read) and
3
+ * SUI supply/withdraw (policy-gated writes).
4
+ *
5
+ * Scallop is an over-collateralized money market on Sui (mainnet-only SDK).
6
+ * Writes are SUI-denominated so the MIST policy cap applies directly, and they
7
+ * route through the same policy → simulate → execute pipeline as sui.send. The
8
+ * dry-run simulate is the safety net: a malformed or out-of-funds supply/
9
+ * withdraw is caught before broadcast.
10
+ */
11
+
12
+ import { Scallop } from '@scallop-io/sui-scallop-sdk'
13
+ import type { ToolDef } from 'lyra-core'
14
+ import { z } from 'zod'
15
+ import { evaluatePolicy, suiToMist } from '../policy'
16
+ import { simulate } from '../simulate'
17
+ import type { OnchainRuntimeContext } from '../types'
18
+
19
+ const SUI_TYPE = '0x2::sui::SUI'
20
+
21
+ function ensureMainnet(ctx: OnchainRuntimeContext): string | null {
22
+ return ctx.network === 'mainnet' ? null : 'Scallop SDK supports mainnet only'
23
+ }
24
+
25
+ async function newScallop(ctx: OnchainRuntimeContext): Promise<Scallop> {
26
+ return new Scallop({ networkType: 'mainnet', walletAddress: ctx.agentAddress })
27
+ }
28
+
29
+ // --- scallop.markets -------------------------------------------------------
30
+
31
+ const MarketsSchema = z.object({
32
+ coins: z
33
+ .string()
34
+ .optional()
35
+ .describe('Comma-separated coin names, e.g. "sui,usdc,usdt". Default sui,usdc,usdt.'),
36
+ })
37
+ type MarketsArgs = z.infer<typeof MarketsSchema>
38
+
39
+ export function makeScallopMarkets(ctx: OnchainRuntimeContext): ToolDef<MarketsArgs> {
40
+ return {
41
+ name: 'scallop.markets',
42
+ description:
43
+ 'Read Scallop lending markets on Sui: supply APY, borrow APY, and utilization per asset. Read-only discovery for where idle funds could earn yield.',
44
+ searchHint: 'scallop lending market supply apy borrow yield utilization rates earn',
45
+ schema: MarketsSchema,
46
+ handler: async args => {
47
+ const err = ensureMainnet(ctx)
48
+ if (err) return { ok: false, error: err }
49
+ try {
50
+ const sdk = await newScallop(ctx)
51
+ const q = await sdk.createScallopQuery()
52
+ await q.init()
53
+ const coins = args.coins
54
+ ?.split(',')
55
+ .map(s => s.trim())
56
+ .filter(Boolean) ?? ['sui', 'usdc', 'usdt']
57
+ const pools = await Promise.all(
58
+ coins.map(async coin => {
59
+ try {
60
+ const p = await q.getMarketPool(coin)
61
+ return {
62
+ coin,
63
+ supplyApy: p?.supplyApy ?? null,
64
+ borrowApy: p?.borrowApy ?? null,
65
+ utilization: p?.utilizationRate ?? null,
66
+ }
67
+ } catch (e) {
68
+ return { coin, error: (e as Error).message.slice(0, 100) }
69
+ }
70
+ }),
71
+ )
72
+ return { ok: true, data: { protocol: 'scallop', network: 'mainnet', pools } }
73
+ } catch (e) {
74
+ return { ok: false, error: (e as Error).message.slice(0, 240) }
75
+ }
76
+ },
77
+ }
78
+ }
79
+
80
+ // --- scallop.position ------------------------------------------------------
81
+
82
+ const PositionSchema = z.object({})
83
+ type PositionArgs = z.infer<typeof PositionSchema>
84
+
85
+ export function makeScallopPosition(ctx: OnchainRuntimeContext): ToolDef<PositionArgs> {
86
+ return {
87
+ name: 'scallop.position',
88
+ description:
89
+ "The agent's Scallop position: total supplied, collateral, debt, and per-asset lendings/borrowings. Read-only.",
90
+ searchHint: 'scallop position portfolio supplied collateral debt lending borrowing health',
91
+ schema: PositionSchema,
92
+ handler: async () => {
93
+ const err = ensureMainnet(ctx)
94
+ if (err) return { ok: false, error: err }
95
+ try {
96
+ const sdk = await newScallop(ctx)
97
+ const q = await sdk.createScallopQuery()
98
+ await q.init()
99
+ const p = await q.getUserPortfolio({ walletAddress: ctx.agentAddress })
100
+ return {
101
+ ok: true,
102
+ data: {
103
+ protocol: 'scallop',
104
+ totalSupplyValue: p?.totalSupplyValue ?? 0,
105
+ totalCollateralValue: p?.totalCollateralValue ?? 0,
106
+ totalDebtValue: p?.totalDebtValue ?? 0,
107
+ lendings: (p?.lendings ?? []).map(
108
+ (l: { coinName?: string; suppliedCoin?: number; suppliedValue?: number }) => ({
109
+ coin: l.coinName,
110
+ supplied: l.suppliedCoin,
111
+ value: l.suppliedValue,
112
+ }),
113
+ ),
114
+ borrowings: p?.borrowings ?? [],
115
+ },
116
+ }
117
+ } catch (e) {
118
+ return { ok: false, error: (e as Error).message.slice(0, 240) }
119
+ }
120
+ },
121
+ }
122
+ }
123
+
124
+ // --- scallop.supply / scallop.withdraw (SUI) -------------------------------
125
+
126
+ const AmountSchema = z.object({
127
+ amount: z.string().min(1).describe('Amount of SUI, e.g. "1.5".'),
128
+ })
129
+ type AmountArgs = z.infer<typeof AmountSchema>
130
+
131
+ async function runScallopWrite(
132
+ ctx: OnchainRuntimeContext,
133
+ amount: string,
134
+ kind: 'supply' | 'withdraw',
135
+ ): Promise<{ ok: true; data: unknown } | { ok: false; error: string }> {
136
+ const err = ensureMainnet(ctx)
137
+ if (err) return { ok: false, error: err }
138
+ const amountMist = suiToMist(amount)
139
+ if (amountMist === undefined || amountMist <= 0n)
140
+ return { ok: false, error: `invalid amount "${amount}"` }
141
+
142
+ // Policy gate (deterministic). Supplying moves funds out; withdrawing back in.
143
+ if (ctx.policy && kind === 'supply') {
144
+ const verdict = evaluatePolicy(
145
+ { kind: 'transfer', coinType: SUI_TYPE, amountMist, protocol: 'scallop' },
146
+ ctx.policy,
147
+ )
148
+ if (!verdict.allowed)
149
+ return { ok: false, error: `policy blocked: ${verdict.violations.join('; ')}` }
150
+ }
151
+
152
+ try {
153
+ const sdk = await newScallop(ctx)
154
+ const builder = await sdk.createScallopBuilder()
155
+ const tx = builder.createTxBlock()
156
+ tx.setSender(ctx.agentAddress)
157
+ // deposit/withdraw auto-select the user's coins and return the resulting
158
+ // coin to hand back to the owner. depositQuick's 3rd arg returnSCoin=false
159
+ // keeps the old market-coin standard, which withdrawQuick can redeem (the
160
+ // new sCoin standard is not found by withdrawQuick → "No valid coins").
161
+ const out =
162
+ kind === 'supply'
163
+ ? await tx.depositQuick(Number(amountMist), 'sui', false)
164
+ : await tx.withdrawQuick(Number(amountMist), 'sui')
165
+ if (out) tx.transferObjects([out], ctx.agentAddress)
166
+ const transaction = tx.txBlock
167
+
168
+ const sim = await simulate(ctx.client, transaction, ctx.agentAddress)
169
+ if (!sim.ok) return { ok: false, error: `pre-flight simulation failed: ${sim.reason}` }
170
+
171
+ const res = await ctx.client.signAndExecuteTransaction({
172
+ signer: ctx.keypair,
173
+ transaction,
174
+ options: { showEffects: true },
175
+ })
176
+ if (res.effects?.status?.status !== 'success') {
177
+ return { ok: false, error: `execution failed: ${res.effects?.status?.error ?? 'unknown'}` }
178
+ }
179
+ // Wait for the tx to settle/index so a follow-up action (e.g. an immediate
180
+ // withdraw after a supply) doesn't race the not-yet-indexed position.
181
+ await ctx.client.waitForTransaction({ digest: res.digest })
182
+ return {
183
+ ok: true,
184
+ data: {
185
+ protocol: 'scallop',
186
+ action: kind,
187
+ amountSui: amount,
188
+ digest: res.digest,
189
+ simGasUsed: sim.gasUsed,
190
+ policyEnforced: ctx.policy != null,
191
+ },
192
+ }
193
+ } catch (e) {
194
+ return { ok: false, error: (e as Error).message.slice(0, 240) }
195
+ }
196
+ }
197
+
198
+ export function makeScallopSupply(ctx: OnchainRuntimeContext): ToolDef<AmountArgs> {
199
+ return {
200
+ name: 'scallop.supply',
201
+ description:
202
+ 'Supply (deposit) idle SUI into Scallop to earn lending yield. Policy-checked, simulated, then executed.',
203
+ searchHint: 'scallop supply deposit lend sui earn yield idle',
204
+ schema: AmountSchema,
205
+ handler: async args => runScallopWrite(ctx, args.amount, 'supply'),
206
+ }
207
+ }
208
+
209
+ export function makeScallopWithdraw(ctx: OnchainRuntimeContext): ToolDef<AmountArgs> {
210
+ return {
211
+ name: 'scallop.withdraw',
212
+ description: 'Withdraw supplied SUI from Scallop back to the agent. Simulated, then executed.',
213
+ searchHint: 'scallop withdraw redeem unlend remove sui',
214
+ schema: AmountSchema,
215
+ handler: async args => runScallopWrite(ctx, args.amount, 'withdraw'),
216
+ }
217
+ }
@@ -0,0 +1,121 @@
1
+ /**
2
+ * `sui.send` — transfer SUI through the full guarded pipeline:
3
+ * policy → simulate → execute → on-chain lyra::policy receipt.
4
+ *
5
+ * The deterministic policy is checked in code BEFORE anything is built. When a
6
+ * shared AgentPolicy object is configured, the PTB also calls
7
+ * `lyra::policy::record_action`, so the SAME on-chain limits are re-enforced in
8
+ * Move and an ActionReceipt is minted atomically with the transfer.
9
+ */
10
+
11
+ import { Transaction } from '@mysten/sui/transactions'
12
+ import type { ToolDef } from 'lyra-core'
13
+ import { z } from 'zod'
14
+ import { evaluatePolicy, suiToMist } from '../policy'
15
+ import { simulate } from '../simulate'
16
+ import type { OnchainRuntimeContext } from '../types'
17
+
18
+ const SUI_TYPE = '0x2::sui::SUI'
19
+
20
+ const Schema = z.object({
21
+ to: z.string().min(3).describe('Recipient Sui address (0x...).'),
22
+ amount: z.string().min(1).describe('Amount in SUI, e.g. "0.05".'),
23
+ })
24
+ type Args = z.infer<typeof Schema>
25
+
26
+ export function makeSuiSend(ctx: OnchainRuntimeContext): ToolDef<Args> {
27
+ return {
28
+ name: 'sui.send',
29
+ description:
30
+ 'Transfer SUI from the agent to a recipient. Deterministically policy-checked, dry-run simulated, then executed; mints an on-chain lyra::policy ActionReceipt when a policy object is configured.',
31
+ searchHint: 'send transfer sui pay native move funds remit',
32
+ schema: Schema,
33
+ handler: async args => {
34
+ try {
35
+ const to = args.to.trim()
36
+ if (!to.startsWith('0x')) {
37
+ return { ok: false, error: `invalid recipient "${to}": expected a 0x Sui address` }
38
+ }
39
+ const amountMist = suiToMist(args.amount)
40
+ if (amountMist === undefined || amountMist <= 0n) {
41
+ return { ok: false, error: `invalid amount "${args.amount}"` }
42
+ }
43
+
44
+ // 1. Policy gate (deterministic) — block before simulate/execute.
45
+ if (ctx.policy) {
46
+ const verdict = evaluatePolicy(
47
+ { kind: 'transfer', coinType: SUI_TYPE, amountMist, to, protocol: 'transfer' },
48
+ ctx.policy,
49
+ )
50
+ if (!verdict.allowed) {
51
+ return { ok: false, error: `policy blocked: ${verdict.violations.join('; ')}` }
52
+ }
53
+ }
54
+
55
+ // 2. Build the PTB: split + transfer, plus on-chain receipt/enforcement.
56
+ const tx = new Transaction()
57
+ const [coin] = tx.splitCoins(tx.gas, [amountMist])
58
+ tx.transferObjects([coin], to)
59
+ const recordsOnChain = Boolean(ctx.packageId && ctx.policyObjectId)
60
+ if (recordsOnChain) {
61
+ tx.moveCall({
62
+ target: `${ctx.packageId}::policy::record_action`,
63
+ typeArguments: [SUI_TYPE],
64
+ arguments: [
65
+ tx.object(ctx.policyObjectId as string),
66
+ tx.pure.u64(amountMist),
67
+ tx.pure.address('0x0'),
68
+ tx.pure.vector('u8', Array.from(new TextEncoder().encode('transfer'))),
69
+ tx.pure.vector('u8', Array.from(new TextEncoder().encode(`to ${to}`))),
70
+ tx.object.clock(),
71
+ ],
72
+ })
73
+ }
74
+
75
+ // 3. Simulate-before-write.
76
+ const sim = await simulate(ctx.client, tx, ctx.agentAddress)
77
+ if (!sim.ok) {
78
+ return { ok: false, error: `pre-flight simulation failed: ${sim.reason}` }
79
+ }
80
+
81
+ // 4. Execute.
82
+ const res = await ctx.client.signAndExecuteTransaction({
83
+ signer: ctx.keypair,
84
+ transaction: tx,
85
+ options: { showEffects: true, showObjectChanges: true },
86
+ })
87
+ if (res.effects?.status?.status !== 'success') {
88
+ return {
89
+ ok: false,
90
+ error: `execution failed: ${res.effects?.status?.error ?? 'unknown'}`,
91
+ }
92
+ }
93
+ // Settle/index before returning so a follow-up balance read is accurate.
94
+ await ctx.client.waitForTransaction({ digest: res.digest })
95
+ const receipt = res.objectChanges?.find(
96
+ c =>
97
+ c.type === 'created' &&
98
+ String((c as { objectType?: string }).objectType).endsWith('::policy::ActionReceipt'),
99
+ ) as { objectId?: string } | undefined
100
+
101
+ return {
102
+ ok: true,
103
+ data: {
104
+ digest: res.digest,
105
+ recipient: to,
106
+ amountSui: args.amount,
107
+ amountMist: amountMist.toString(),
108
+ status: 'success',
109
+ // Decision receipt: proof this write was policy-checked + simulated.
110
+ simGasUsed: sim.gasUsed,
111
+ policyEnforced: ctx.policy != null,
112
+ onchainReceipt: recordsOnChain,
113
+ receiptId: receipt?.objectId ?? null,
114
+ },
115
+ }
116
+ } catch (e) {
117
+ return { ok: false, error: (e as Error).message.slice(0, 240) }
118
+ }
119
+ },
120
+ }
121
+ }
@@ -0,0 +1,171 @@
1
+ /**
2
+ * `swap` — execute a token swap on Sui via the 7k aggregator (best route across
3
+ * Cetus, FlowX, Bluefin, DeepBook, ...). Runs through the same policy → simulate
4
+ * → execute pipeline as every other write.
5
+ *
6
+ * Why 7k and not the Cetus aggregator directly: Cetus's aggregator SDK pins
7
+ * @mysten/sui v2 (incompatible with this stack's v1.45), and DeepBook's own
8
+ * swap needs DEEP for fees + a 1-SUI min lot. 7k (@7kprotocol/sdk-ts) is
9
+ * v1-compatible and routes across all of them, paying fees from the route.
10
+ */
11
+
12
+ import { MetaAg } from '@7kprotocol/sdk-ts'
13
+ import { Transaction, coinWithBalance } from '@mysten/sui/transactions'
14
+ import type { ToolDef } from 'lyra-core'
15
+ import { z } from 'zod'
16
+ import { evaluatePolicy } from '../policy'
17
+ import { simulate } from '../simulate'
18
+ import type { OnchainRuntimeContext } from '../types'
19
+
20
+ const SUI_TYPE = '0x2::sui::SUI'
21
+
22
+ /** Symbol → mainnet coin type + decimals for the common assets. */
23
+ const COINS: Record<string, { type: string; decimals: number }> = {
24
+ sui: { type: SUI_TYPE, decimals: 9 },
25
+ usdc: {
26
+ type: '0xdba34672e30cb065b1f93e3ab55318768fd6fef66c15942c9f7cb846e2f900e7::usdc::USDC',
27
+ decimals: 6,
28
+ },
29
+ deep: {
30
+ type: '0xdeeb7a4662eec9f2f3def03fb937a663dddaa2e215b8078a284d026b7946c270::deep::DEEP',
31
+ decimals: 6,
32
+ },
33
+ wal: {
34
+ type: '0x356a26eb9e012a68958082340d4c4116e7f55615cf27affcff209cf0ae544f59::wal::WAL',
35
+ decimals: 9,
36
+ },
37
+ }
38
+
39
+ function resolve(input: string): { type: string; decimals: number } {
40
+ const k = input.trim().toLowerCase()
41
+ return COINS[k] ?? { type: input.trim(), decimals: 9 }
42
+ }
43
+
44
+ const Schema = z.object({
45
+ from: z.string().min(1).describe('Input coin: symbol (sui, usdc, deep, wal) or full coin type.'),
46
+ to: z.string().min(1).describe('Output coin: symbol or full coin type.'),
47
+ amount: z.string().min(1).describe('Input amount in whole units of `from`, e.g. "1".'),
48
+ })
49
+ type Args = z.infer<typeof Schema>
50
+
51
+ export function makeSwap(ctx: OnchainRuntimeContext): ToolDef<Args> {
52
+ return {
53
+ name: 'swap',
54
+ description:
55
+ 'Swap one coin for another on Sui via the 7k aggregator (best route across Cetus/FlowX/Bluefin/DeepBook). Policy-checked → simulated → executed. Use for "swap 1 SUI to USDC", "trade X for Y".',
56
+ searchHint: 'swap exchange trade convert dex aggregator best route sui usdc buy sell',
57
+ schema: Schema,
58
+ handler: async args => {
59
+ if (ctx.network !== 'mainnet') return { ok: false, error: 'swap supports mainnet only' }
60
+ try {
61
+ const from = resolve(args.from)
62
+ const to = resolve(args.to)
63
+ if (from.type === to.type) return { ok: false, error: 'from and to are the same coin' }
64
+ const amountIn = BigInt(Math.round(Number(args.amount) * 10 ** from.decimals))
65
+ if (amountIn <= 0n) return { ok: false, error: `invalid amount "${args.amount}"` }
66
+
67
+ // Policy gate. The MIST per-tx cap is SUI-denominated, so it only bounds
68
+ // SUI-input swaps; the slippage/protocol/expiry checks always apply.
69
+ if (ctx.policy) {
70
+ const verdict = evaluatePolicy(
71
+ {
72
+ kind: 'swap',
73
+ coinType: from.type,
74
+ amountMist: from.type === SUI_TYPE ? amountIn : 0n,
75
+ toCoinType: to.type,
76
+ protocol: 'swap',
77
+ slippageBps: ctx.policy.maxSlippageBps,
78
+ },
79
+ ctx.policy,
80
+ )
81
+ if (!verdict.allowed)
82
+ return { ok: false, error: `policy blocked: ${verdict.violations.join('; ')}` }
83
+ }
84
+
85
+ const me = ctx.agentAddress
86
+ const ag = new MetaAg({ slippageBps: ctx.policy?.maxSlippageBps ?? 100 })
87
+ // Quote without the SDK's internal pre-simulation (it throws on routes it
88
+ // can't simulate); we simulate the chosen route ourselves below.
89
+ const quotes = (
90
+ await ag.quote({
91
+ coinTypeIn: from.type,
92
+ coinTypeOut: to.type,
93
+ amountIn: amountIn.toString(),
94
+ signer: me,
95
+ })
96
+ )
97
+ .filter(Boolean)
98
+ .sort((a, b) => Number(b.amountOut ?? 0) - Number(a.amountOut ?? 0))
99
+ if (quotes.length === 0) return { ok: false, error: 'no swap route found' }
100
+
101
+ // Best-execution with reliability: try routes in output order, skipping
102
+ // any that fail simulation (some providers build PTBs that leave an
103
+ // unconsumed value / revert). Use the first that simulates cleanly.
104
+ let used: {
105
+ provider: string
106
+ amountOut: number
107
+ tx: Transaction
108
+ gasUsed?: string
109
+ } | null = null
110
+ const failures: string[] = []
111
+ for (const q of quotes) {
112
+ const tx = new Transaction()
113
+ tx.setSender(me)
114
+ tx.setGasBudget(150_000_000)
115
+ try {
116
+ const coinIn =
117
+ from.type === SUI_TYPE
118
+ ? tx.splitCoins(tx.gas, [amountIn])[0]
119
+ : coinWithBalance({ type: from.type, balance: amountIn })
120
+ const coinOut = await ag.swap({ quote: q, signer: me, tx, coinIn: coinIn as never })
121
+ tx.transferObjects([coinOut as never], me)
122
+ const sim = await simulate(ctx.client, tx, me)
123
+ if (sim.ok) {
124
+ used = {
125
+ provider: String(q.provider),
126
+ amountOut: Number(q.amountOut ?? 0),
127
+ tx,
128
+ gasUsed: sim.gasUsed,
129
+ }
130
+ break
131
+ }
132
+ failures.push(`${q.provider}: ${sim.reason}`)
133
+ } catch (e) {
134
+ failures.push(`${q.provider}: ${(e as Error).message.slice(0, 60)}`)
135
+ }
136
+ }
137
+ if (!used)
138
+ return { ok: false, error: `no route simulated cleanly — ${failures.join(' | ')}` }
139
+
140
+ const res = await ctx.client.signAndExecuteTransaction({
141
+ signer: ctx.keypair,
142
+ transaction: used.tx,
143
+ options: { showEffects: true },
144
+ })
145
+ if (res.effects?.status?.status !== 'success') {
146
+ return {
147
+ ok: false,
148
+ error: `execution failed: ${res.effects?.status?.error ?? 'unknown'}`,
149
+ }
150
+ }
151
+ await ctx.client.waitForTransaction({ digest: res.digest })
152
+
153
+ return {
154
+ ok: true,
155
+ data: {
156
+ from: args.from,
157
+ to: args.to,
158
+ amountIn: args.amount,
159
+ amountOut: (used.amountOut / 10 ** to.decimals).toString(),
160
+ route: used.provider,
161
+ digest: res.digest,
162
+ simGasUsed: used.gasUsed,
163
+ policyEnforced: ctx.policy != null,
164
+ },
165
+ }
166
+ } catch (e) {
167
+ return { ok: false, error: (e as Error).message.slice(0, 240) }
168
+ }
169
+ },
170
+ }
171
+ }
@@ -0,0 +1,66 @@
1
+ /**
2
+ * `walrus.store` — write a durable, verifiable artifact (receipt, report,
3
+ * memory snapshot) to Walrus and return its blobId.
4
+ *
5
+ * This is what makes a short-lived action auditable across sessions: the agent
6
+ * stores receipts and analysis on Walrus, and on-chain ActionReceipts can
7
+ * reference the blob. Storing costs WAL from the agent address.
8
+ */
9
+
10
+ import { WalrusClient } from '@mysten/walrus'
11
+ import type { ToolDef } from 'lyra-core'
12
+ import { z } from 'zod'
13
+ import type { OnchainRuntimeContext } from '../types'
14
+
15
+ const Schema = z.object({
16
+ content: z.string().min(1).describe('The text/JSON artifact to store durably.'),
17
+ epochs: z
18
+ .number()
19
+ .int()
20
+ .min(1)
21
+ .max(53)
22
+ .optional()
23
+ .describe('Storage duration in Walrus epochs. Default 3.'),
24
+ })
25
+ type Args = z.infer<typeof Schema>
26
+
27
+ export function makeWalrusStore(ctx: OnchainRuntimeContext): ToolDef<Args> {
28
+ return {
29
+ name: 'walrus.store',
30
+ description:
31
+ 'Store a durable, verifiable artifact (receipt, report, or memory) on Walrus. Returns a blobId you can cite later. Use to persist agent memory or an execution receipt across sessions.',
32
+ searchHint: 'walrus store blob save receipt memory durable artifact persist record',
33
+ schema: Schema,
34
+ handler: async args => {
35
+ try {
36
+ const walrus = new WalrusClient({
37
+ network: ctx.walrusNetwork ?? ctx.network,
38
+ // cast: @mysten/walrus bundles a different @mysten/sui minor; runtime-
39
+ // compatible (verified: real mainnet blob written) but not type-identical.
40
+ suiClient: ctx.client as never,
41
+ })
42
+ const blob = new TextEncoder().encode(args.content)
43
+ const result = await walrus.writeBlob({
44
+ blob,
45
+ deletable: false,
46
+ epochs: args.epochs ?? 3,
47
+ signer: ctx.keypair as never,
48
+ })
49
+ return {
50
+ ok: true,
51
+ data: {
52
+ blobId: result.blobId,
53
+ bytes: blob.length,
54
+ epochs: args.epochs ?? 3,
55
+ network: ctx.walrusNetwork ?? ctx.network,
56
+ },
57
+ }
58
+ } catch (e) {
59
+ return {
60
+ ok: false,
61
+ error: `walrus store failed (needs WAL for storage): ${(e as Error).message.slice(0, 200)}`,
62
+ }
63
+ }
64
+ },
65
+ }
66
+ }
package/src/types.ts ADDED
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Public types for lyra-plugin-onchain. The runtime context is side-banded onto
3
+ * PluginContext under `.onchain` (the harness builds it; the plugin reads it via
4
+ * `(ctx as any).onchain`), keeping PluginContext free of plugin-specific fields.
5
+ */
6
+
7
+ import type { SuiClient } from '@mysten/sui/client'
8
+ import type { Ed25519Keypair } from '@mysten/sui/keypairs/ed25519'
9
+ import type { SuiNetwork } from './client'
10
+ import type { SuiPolicy } from './policy'
11
+
12
+ export interface OnchainRuntimeContext {
13
+ /** Sui JSON-RPC client for `network`. */
14
+ client: SuiClient
15
+ /** The agent signer (signs + pays gas). */
16
+ keypair: Ed25519Keypair
17
+ /** `keypair.toSuiAddress()`, cached. */
18
+ agentAddress: string
19
+ network: SuiNetwork
20
+ /** Deterministic fund-control policy. When set, every write is checked before simulate/execute. */
21
+ policy?: SuiPolicy
22
+ /** Deployed `lyra::policy` package id (for on-chain receipts + policy objects). */
23
+ packageId?: string
24
+ /** Shared `AgentPolicy` object id. When set, writes compose an on-chain receipt + enforcement. */
25
+ policyObjectId?: string
26
+ /** Walrus network for receipt/memory storage (defaults to `network`). */
27
+ walrusNetwork?: SuiNetwork
28
+ agentDir: string
29
+ /** Optional: brain provider/model, surfaced by account.info. */
30
+ brainProvider?: string | null
31
+ brainModel?: string | null
32
+ }
package/src/vault.ts ADDED
@@ -0,0 +1,76 @@
1
+ /**
2
+ * Resolve an owner's on-chain setup: their `AgentPolicy` (delegating to their
3
+ * derived agent) + the treasury `Vault` bound to it. The binding lives on-chain
4
+ * (owner holds the `PolicyOwnerCap` → policy → vault), so any surface resolves
5
+ * "this owner's agent + vault" without a database. Mirrors
6
+ * `apps/web/lib/vault.ts`.
7
+ */
8
+
9
+ import { type SuiNetwork, makeSuiClient } from './client'
10
+ import { deriveAgentAddress } from './derive'
11
+
12
+ // A module keeps its DEFINING package id for types/events: `policy` shipped in
13
+ // the original publish; `vault` was added in the first upgrade.
14
+ const ORIGINAL_PKG = '0x250880a4c1a268da8011b164f599d4e100cefce84f862d36396cd1a943ee8a35'
15
+ const VAULT_PKG = '0xa40689cc541f57af123e90819e73eab8a551e4385ab91bee89d02f6691590211'
16
+
17
+ export interface OwnerVault {
18
+ policyId: string
19
+ vaultId: string
20
+ capId: string
21
+ agent: string
22
+ vaultMist: string
23
+ }
24
+
25
+ /** Find the owner's policy (delegating to their derived agent) + its SUI vault. */
26
+ export async function resolveOwnerVault(
27
+ owner: string,
28
+ network: SuiNetwork = 'mainnet',
29
+ masterSecret?: string,
30
+ ): Promise<OwnerVault | null> {
31
+ const client = makeSuiClient(network)
32
+ const agent = deriveAgentAddress(owner, masterSecret)
33
+ const caps = await client
34
+ .getOwnedObjects({
35
+ owner,
36
+ filter: { StructType: `${ORIGINAL_PKG}::policy::PolicyOwnerCap` },
37
+ options: { showContent: true },
38
+ })
39
+ .catch(() => null)
40
+ if (!caps) return null
41
+
42
+ for (const c of caps.data) {
43
+ // biome-ignore lint/suspicious/noExplicitAny: dynamic Move object fields
44
+ const policyId = (c.data?.content as any)?.fields?.policy_id as string | undefined
45
+ if (!policyId) continue
46
+ const pol = await client
47
+ .getObject({ id: policyId, options: { showContent: true } })
48
+ .catch(() => null)
49
+ // biome-ignore lint/suspicious/noExplicitAny: dynamic Move object fields
50
+ const pf = (pol?.data?.content as any)?.fields
51
+ if (!pf || pf.agent !== agent || pf.revoked) continue
52
+
53
+ const ev = await client
54
+ .queryEvents({
55
+ query: { MoveEventType: `${VAULT_PKG}::vault::VaultOpened` },
56
+ limit: 100,
57
+ order: 'descending',
58
+ })
59
+ .catch(() => null)
60
+ // biome-ignore lint/suspicious/noExplicitAny: parsed Move event
61
+ const match = ev?.data.find(e => (e.parsedJson as any)?.policy_id === policyId)
62
+ // biome-ignore lint/suspicious/noExplicitAny: parsed Move event
63
+ const vaultId = (match?.parsedJson as any)?.vault_id as string | undefined
64
+ if (!vaultId) continue
65
+
66
+ const v = await client
67
+ .getObject({ id: vaultId, options: { showContent: true } })
68
+ .catch(() => null)
69
+ // biome-ignore lint/suspicious/noExplicitAny: dynamic Move object fields
70
+ const vaultMist = String((v?.data?.content as any)?.fields?.balance ?? '0')
71
+ const capId = c.data?.objectId
72
+ if (!capId) continue
73
+ return { policyId, vaultId, capId, agent, vaultMist }
74
+ }
75
+ return null
76
+ }