@mysten-incubation/sui-agent-pass 0.0.0 → 0.1.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/LICENSE +201 -0
- package/README.md +423 -4
- package/deployments/README.md +28 -0
- package/deployments/mainnet.json +65 -0
- package/deployments/testnet.json +69 -0
- package/dist/activation.js +287 -0
- package/dist/agent.js +409 -0
- package/dist/chain.js +763 -0
- package/dist/cli.js +250 -0
- package/dist/describe.js +352 -0
- package/dist/domain.js +393 -0
- package/dist/explorer.js +143 -0
- package/dist/float.js +80 -0
- package/dist/mcp.js +180 -0
- package/dist/navi.js +125 -0
- package/dist/owner.js +410 -0
- package/dist/plugin.js +795 -0
- package/dist/server.js +229 -0
- package/dist/signer.js +40 -0
- package/dist/storage.js +180 -0
- package/docs/AGENT.md +207 -0
- package/docs/PLAYBOOK.md +199 -0
- package/package.json +95 -10
- package/web-dist/assets/index-GamQ8vny.js +844 -0
- package/web-dist/assets/index-IgtbvOwM.css +1 -0
- package/web-dist/assets/mount-B6ddx2sS.js +3842 -0
- package/web-dist/deployments/mainnet.json +65 -0
- package/web-dist/deployments/testnet.json +69 -0
- package/web-dist/favicon.svg +10 -0
- package/web-dist/index.html +19 -0
package/dist/mcp.js
ADDED
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
// Copyright (c) Mysten Labs, Inc.
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
import { readFile, stat } from 'node:fs/promises';
|
|
4
|
+
import { sep } from 'node:path';
|
|
5
|
+
import { fileURLToPath } from 'node:url';
|
|
6
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
7
|
+
import { z } from 'zod';
|
|
8
|
+
import { clientFor, deepbookSizes, readAllHoldings, readStatus } from './chain.js';
|
|
9
|
+
import { coinInfo } from './domain.js';
|
|
10
|
+
export const result = (value) => ({
|
|
11
|
+
content: [{ type: 'text', text: JSON.stringify(value) }],
|
|
12
|
+
});
|
|
13
|
+
export const failure = (error, fallback) => ({
|
|
14
|
+
...result({ error: error instanceof Error ? error.message : fallback }),
|
|
15
|
+
isError: true,
|
|
16
|
+
});
|
|
17
|
+
/**
|
|
18
|
+
* What every client shows the model before any tool call, so a plain request ("sell 0.05 SUI")
|
|
19
|
+
* is enough: no resource to know about, no project file to read.
|
|
20
|
+
*/
|
|
21
|
+
export const INSTRUCTIONS = [
|
|
22
|
+
'Sui Agent Pass: an owner authorized this agent key to act on their assets within rules enforced on chain. The owner keeps their coins; you build transactions, this server signs them, and the chain refuses anything outside the rules.',
|
|
23
|
+
"To act (pay, trade, lend, stake): 1. Call session_info (name the pass if there are several). It returns the rules, what the key may still spend today, the coins with their types and decimals, the pools, the session directory and sdk (install command, import line, and api, the whole SDK surface, so you never need to read the package\'s files). 2. In your working directory run sdk.install once, then write a Node script: sdk.import, `const agent = await Agent.open(directory)`, build the action with a builder (send, swap, supply, withdraw, borrow, repay, stake, unstake) naming the coin type and the amount in base units (units() from the domain module), and `await agent.submit(tx)`. Before a swap, `await quote(identity, pair, amountIn)` and set minOut a little under its amountOut. 3. Read the Outcome: success true means it executed; success false means the chain executed and refused it, and reason is the owner's rule, which you quote; a thrown error means nothing executed, which you never describe as enforcement. 4. Report the digest, what moved and the remaining daily limit from a fresh session_info.",
|
|
24
|
+
'Never invent amounts or recipients: recipients are the names in the rules, and a missing amount is a question for the owner. After a missing network response, look up the digest before submitting the same action again. Use English for anything the owner reads.',
|
|
25
|
+
'To set up a pass, change its rules (propose_change, one owner signature, the key stays), size limits, or end it, read agent-pass://playbook; agent-pass://guide documents the SDK and the contract for hand-built transactions.',
|
|
26
|
+
].join(' ');
|
|
27
|
+
/** Where this code runs from: a checkout is installed by path, a published package by name. */
|
|
28
|
+
async function sdkInfo(directory) {
|
|
29
|
+
const root = fileURLToPath(new URL('..', import.meta.url));
|
|
30
|
+
const pkg = JSON.parse(await readFile(new URL('../package.json', import.meta.url), 'utf8'));
|
|
31
|
+
const published = root.split(sep).includes('node_modules');
|
|
32
|
+
return {
|
|
33
|
+
package: pkg.name,
|
|
34
|
+
install: published ? `npm install ${pkg.name}@${pkg.version}` : `npm install ${root}`,
|
|
35
|
+
import: `import { Agent, quote, send, swap, supply, withdraw, borrow, repay, stake, unstake, pickValidator } from '${pkg.name}/agent'`,
|
|
36
|
+
open: directory ? `await Agent.open(${JSON.stringify(directory)})` : null,
|
|
37
|
+
// The whole surface, so a script needs nothing from the package's source tree.
|
|
38
|
+
api: [
|
|
39
|
+
'Agent.open(directory): Promise<Agent>; agent.identity (pass this to builders); agent.client (SuiGrpcClient for reads); agent.submit(tx, { proveChainRefusal?: boolean }): Promise<Outcome>; agent.close()',
|
|
40
|
+
'quote(identity, { coinIn, coinOut, poolId?, venue? }, amountIn): Promise<{ pool, amountIn, amountOut }> prices a sale on a listed pool from the venue itself without touching any allowance; amountIn is what the pool would take (DeepBook pays its fee from the input and rounds down to its lot size) and amountOut what comes back. Quote before every swap and set minOut a little under amountOut; never size minOut from a mid price.',
|
|
41
|
+
'Builders return a Transaction; coins are full type strings (session_info coins lists the ones these rules name, with symbol and decimals) and amounts are base-unit strings: send(identity, coin, amount, recipientName); swap(identity, { coinIn, coinOut, poolId?, venue? }, amountIn, minOut) sells coinIn for coinOut on a listed pool (the first matching pool when none is named); supply(identity, coin, amount) on NAVI, and await withdraw(identity, coin, amount), await borrow(identity, coin, amount), await repay(identity, coin, amount), which first add the oracle price refresh NAVI requires on mainnet; stake(identity, amount, validator) in SUI with validator from await pickValidator(identity); unstake(identity, stakeId); refillGas(identity, amount)',
|
|
42
|
+
"Outcome: { digest, success, reason (the owner's rule behind a refusal, or null), error, balanceChanges: [{ address, coinType, amount }] }; submit throws when nothing executed",
|
|
43
|
+
`units(decimal, decimals) from '${pkg.name}/domain' turns "0.05" into base units; each coin's decimals are in session_info coins`,
|
|
44
|
+
],
|
|
45
|
+
note: published ? null : `${root} must be built (npm run build) before installing from it`,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
/** Which code answers: the package version and when it was built, so a stale server shows. */
|
|
49
|
+
async function serverInfo() {
|
|
50
|
+
const pkg = JSON.parse(await readFile(new URL('../package.json', import.meta.url), 'utf8'));
|
|
51
|
+
const built = await stat(fileURLToPath(import.meta.url)).then((s) => s.mtime.toISOString());
|
|
52
|
+
return {
|
|
53
|
+
version: pkg.version,
|
|
54
|
+
builtAt: built,
|
|
55
|
+
note: 'This process keeps the code it started with. After this package is rebuilt or upgraded, restart the agent client so it starts a new server; compare builtAt.',
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* The pools a session may trade on, each with the coins it sells either way and, for DeepBook,
|
|
60
|
+
* the order sizes the pool enforces: an order under `minSize` fills nothing.
|
|
61
|
+
*/
|
|
62
|
+
export async function venues(client, d) {
|
|
63
|
+
return Promise.all(d.pools.map(async (pool) => {
|
|
64
|
+
const sizes = pool.venue === 'deepbook' ? await deepbookSizes(client, d, pool).catch(() => null) : null;
|
|
65
|
+
const base = coinInfo(d, pool.coinA).symbol, quote = coinInfo(d, pool.coinB).symbol;
|
|
66
|
+
return {
|
|
67
|
+
venue: pool.venue,
|
|
68
|
+
poolId: pool.poolId,
|
|
69
|
+
coinA: pool.coinA,
|
|
70
|
+
coinB: pool.coinB,
|
|
71
|
+
pair: `${base}/${quote}`,
|
|
72
|
+
entries: pool.venue === 'deepbook'
|
|
73
|
+
? {
|
|
74
|
+
[`${base}→${quote}`]: 'pass::deepbook_base_to_quote',
|
|
75
|
+
[`${quote}→${base}`]: 'pass::deepbook_quote_to_base',
|
|
76
|
+
}
|
|
77
|
+
: {
|
|
78
|
+
[`${base}→${quote}`]: 'pass::cetus_a_to_b',
|
|
79
|
+
[`${quote}→${base}`]: 'pass::cetus_b_to_a',
|
|
80
|
+
},
|
|
81
|
+
typeArguments: [pool.coinA, pool.coinB],
|
|
82
|
+
objects: pool.venue === 'deepbook' ? [pool.poolId] : [d.protocols.cetus.configId, pool.poolId],
|
|
83
|
+
...(sizes
|
|
84
|
+
? {
|
|
85
|
+
minSize: sizes.minSize,
|
|
86
|
+
lotSize: sizes.lotSize,
|
|
87
|
+
note: `DeepBook pays its taker fee out of the input and then rounds the order down to lotSize; anything under minSize after that fills nothing and the pass refuses it. Sell at least minSize + lotSize (${(BigInt(sizes.minSize) + BigInt(sizes.lotSize)).toString()} base units of ${base}) and let quote() tell you the fill. A Cetus pool takes any size when the rules list one.`,
|
|
88
|
+
}
|
|
89
|
+
: {}),
|
|
90
|
+
};
|
|
91
|
+
}));
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Everything an agent needs to act: identity, live rules and usage, connection details and the
|
|
95
|
+
* SDK. `directory` is the session directory when the server knows it, for `Agent.open`.
|
|
96
|
+
*/
|
|
97
|
+
export async function sessionInfo(identity, directory = null) {
|
|
98
|
+
const { deployment: d, active, sessionAddress } = identity;
|
|
99
|
+
const client = clientFor(d);
|
|
100
|
+
const [status, sdk, venueList, server, ownerCoins] = await Promise.all([
|
|
101
|
+
readStatus(client, d, active, sessionAddress),
|
|
102
|
+
sdkInfo(directory),
|
|
103
|
+
venues(client, d),
|
|
104
|
+
serverInfo(),
|
|
105
|
+
readAllHoldings(client, d, active.owner).catch(() => null),
|
|
106
|
+
]);
|
|
107
|
+
return {
|
|
108
|
+
...identity,
|
|
109
|
+
...status,
|
|
110
|
+
...(directory ? { directory } : {}),
|
|
111
|
+
server,
|
|
112
|
+
sdk,
|
|
113
|
+
coins: d.coins,
|
|
114
|
+
// Everything the owner holds, so an agent can see a coin the rules do not cover yet; only
|
|
115
|
+
// listed coins can appear in rules, since a rule names a coin's pools and reserves.
|
|
116
|
+
ownerCoins,
|
|
117
|
+
venues: venueList,
|
|
118
|
+
transactionGuide: `Write a Node script: ${sdk.install}, then ${sdk.import}; open the session with Agent.open(directory) and submit(tx) a builder's transaction. submit signs through this server, executes on Sui and returns the persisted outcome (success, reason, error, balanceChanges); it throws when nothing executed. For a PTB the builders do not cover: funds are in the owner's address balance ${active.owner}; every entry takes the pass object then the shared config ${d.configId}, and each spend adds a tx.withdrawal({ from: "allowance", allowance, funder, amount, type }) input and the allowance object as a mutable shared input; calls go to ${d.publishedAt}::pass; sender and gas owner ${sessionAddress}. Read resource agent-pass://guide for the SDK and the complete contract API.`,
|
|
119
|
+
limits: [
|
|
120
|
+
"ownerBalances splits each coin into addressBalance and coinBalance. Allowances draw from the owner's address balance only; their coin objects are out of reach but still theirs, so a low address balance next to coin objects is not an owner who is out of money. The owner's own transactions can pay gas from either side.",
|
|
121
|
+
'Gas stipend is freely controllable by this key; the gas allowance tops it up within its own cap.',
|
|
122
|
+
"Every action needs an installed policy for its coin type; recipients are named, swaps run only on listed pools, lending keeps the health factor above the owner's floor, stakes go to listed validators (or any active one when the list is empty) and stay in the pass.",
|
|
123
|
+
],
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
export const SIGN_DESCRIPTION = 'Sign full base64 Sui TransactionData constructed by you. Does not build, simulate, approve, or execute trades. A signature is NOT evidence that the policies permit the transaction; the chain enforces them. Never submit the same financial action again merely because a network response is missing; inspect its digest first.';
|
|
127
|
+
export const SIGN_ANNOTATIONS = {
|
|
128
|
+
readOnlyHint: false,
|
|
129
|
+
destructiveHint: true,
|
|
130
|
+
idempotentHint: true,
|
|
131
|
+
openWorldHint: true,
|
|
132
|
+
};
|
|
133
|
+
/** The two documents every client can read: the playbook for conduct, the guide for the API. */
|
|
134
|
+
export function registerDocs(server) {
|
|
135
|
+
const docs = [
|
|
136
|
+
{
|
|
137
|
+
name: 'playbook',
|
|
138
|
+
file: 'PLAYBOOK.md',
|
|
139
|
+
description: 'How to turn a request into rules, get the signature, act within the rules, report and revoke',
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
name: 'transaction-guide',
|
|
143
|
+
file: 'AGENT.md',
|
|
144
|
+
description: 'Complete contract interface and agent-side construction examples',
|
|
145
|
+
},
|
|
146
|
+
];
|
|
147
|
+
for (const doc of docs)
|
|
148
|
+
server.registerResource(doc.name, `agent-pass://${doc.name === 'transaction-guide' ? 'guide' : doc.name}`, { description: doc.description, mimeType: 'text/markdown' }, async (uri) => ({
|
|
149
|
+
contents: [
|
|
150
|
+
{
|
|
151
|
+
uri: uri.href,
|
|
152
|
+
mimeType: 'text/markdown',
|
|
153
|
+
text: await readFile(new URL(`../docs/${doc.file}`, import.meta.url), 'utf8'),
|
|
154
|
+
},
|
|
155
|
+
],
|
|
156
|
+
}));
|
|
157
|
+
}
|
|
158
|
+
/** The single-session server behind the authenticated HTTP transport. */
|
|
159
|
+
export function createMcp(identity, signer, directory = null) {
|
|
160
|
+
const server = new McpServer({ name: 'sui-agent-pass', version: '0.1.0' }, { instructions: INSTRUCTIONS });
|
|
161
|
+
registerDocs(server);
|
|
162
|
+
server.registerTool('session_info', {
|
|
163
|
+
description: "Read the pass's installed policies with their live usage, the owner's balances and allowances, the agent term, protocol object IDs, Sui connection details and the SDK install command. Act with a Node script against the SDK, which signs here and submits to Sui itself; or build your own PTB, call sign_transaction and submit the returned bytes and signature yourself.",
|
|
164
|
+
inputSchema: {},
|
|
165
|
+
annotations: { readOnlyHint: true },
|
|
166
|
+
}, async () => result(await sessionInfo(identity, directory)));
|
|
167
|
+
server.registerTool('sign_transaction', {
|
|
168
|
+
description: SIGN_DESCRIPTION,
|
|
169
|
+
inputSchema: { transactionBytes: z.string().min(1).max(180_000) },
|
|
170
|
+
annotations: SIGN_ANNOTATIONS,
|
|
171
|
+
}, async ({ transactionBytes }) => {
|
|
172
|
+
try {
|
|
173
|
+
return result(await signer.sign(transactionBytes));
|
|
174
|
+
}
|
|
175
|
+
catch (error) {
|
|
176
|
+
return failure(error, 'Signing failed');
|
|
177
|
+
}
|
|
178
|
+
});
|
|
179
|
+
return server;
|
|
180
|
+
}
|
package/dist/navi.js
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
// Copyright (c) Mysten Labs, Inc.
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
/**
|
|
4
|
+
* What both sides of the product need to know about NAVI: the oracle refresh every
|
|
5
|
+
* health-checked action must carry on mainnet, a position's balances, and emptying a position
|
|
6
|
+
* into its owner's wallet. NAVI's app works on wallet addresses, not on account-cap positions,
|
|
7
|
+
* so a cap the owner holds is only useful through code like this.
|
|
8
|
+
*/
|
|
9
|
+
import { bcs } from '@mysten/sui/bcs';
|
|
10
|
+
import { Transaction, } from '@mysten/sui/transactions';
|
|
11
|
+
import { normalizeStructTag, normalizeSuiAddress, parseStructTag } from '@mysten/sui/utils';
|
|
12
|
+
import { getPriceFeeds, updateOraclePricesPTB } from '@naviprotocol/lending';
|
|
13
|
+
import { clientFor, naviAccountOwner } from './chain.js';
|
|
14
|
+
import { sameCoin } from './domain.js';
|
|
15
|
+
const CLOCK = '0x6';
|
|
16
|
+
const SUI_SYSTEM = '0x5';
|
|
17
|
+
/**
|
|
18
|
+
* NAVI values a position at oracle prices no older than its update interval, so every action
|
|
19
|
+
* that checks health first refreshes the prices of the reserves this deployment lends, the way
|
|
20
|
+
* NAVI's own clients do: Pyth updates through Wormhole, then `oracle_pro::update_single_price_v2`.
|
|
21
|
+
* NAVI's SDK carries the feed configuration and fetches it live, so an oracle upgrade on their
|
|
22
|
+
* side needs no change here. The stand-ins off mainnet keep fixed prices and have nothing to
|
|
23
|
+
* refresh.
|
|
24
|
+
*/
|
|
25
|
+
export async function refreshNaviPrices(tx, d) {
|
|
26
|
+
if (d.network !== 'mainnet')
|
|
27
|
+
return;
|
|
28
|
+
const lent = Object.keys(d.protocols.navi.reserves);
|
|
29
|
+
const feeds = (await getPriceFeeds({ env: 'prod' })).filter((f) => lent.some((coin) => sameCoin(coin, f.coinType)));
|
|
30
|
+
await updateOraclePricesPTB(tx, feeds, {
|
|
31
|
+
env: 'prod',
|
|
32
|
+
client: clientFor(d),
|
|
33
|
+
updatePythPriceFeeds: true,
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
/** Reads a position's collateral and debt in every reserve this deployment lends, by simulation. */
|
|
37
|
+
export async function readNaviPosition(client, d, user) {
|
|
38
|
+
const n = d.protocols.navi, reserves = Object.entries(n.reserves);
|
|
39
|
+
const tx = new Transaction();
|
|
40
|
+
tx.setSender(normalizeSuiAddress('0x1'));
|
|
41
|
+
for (const [, r] of reserves)
|
|
42
|
+
for (const view of ['user_collateral_balance', 'user_loan_balance'])
|
|
43
|
+
tx.moveCall({
|
|
44
|
+
target: `${n.publishedAt}::logic::${view}`,
|
|
45
|
+
arguments: [tx.object(n.storageId), tx.pure.u8(r.assetId), tx.pure.address(user)],
|
|
46
|
+
});
|
|
47
|
+
const result = await client.simulateTransaction({
|
|
48
|
+
transaction: tx,
|
|
49
|
+
include: { commandResults: true },
|
|
50
|
+
});
|
|
51
|
+
const executed = result.$kind === 'Transaction' ? result.Transaction : result.FailedTransaction;
|
|
52
|
+
if (!executed?.status.success)
|
|
53
|
+
throw new Error(executed?.status.error?.message ?? 'Could not read the NAVI position');
|
|
54
|
+
const value = (i) => {
|
|
55
|
+
const v = result.commandResults?.[i]?.returnValues?.[0]?.bcs;
|
|
56
|
+
return v ? bcs.u256().parse(v) : '0';
|
|
57
|
+
};
|
|
58
|
+
const position = { collateral: {}, debt: {} };
|
|
59
|
+
reserves.forEach(([coin], i) => {
|
|
60
|
+
const c = value(2 * i), l = value(2 * i + 1);
|
|
61
|
+
if (c !== '0')
|
|
62
|
+
position.collateral[normalizeStructTag(coin)] = c;
|
|
63
|
+
if (l !== '0')
|
|
64
|
+
position.debt[normalizeStructTag(coin)] = l;
|
|
65
|
+
});
|
|
66
|
+
return position;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Withdraws every collateral balance of a position into `owner`'s wallet, through the cap. NAVI
|
|
70
|
+
* caps a withdrawal at the user's balance but first checks the request against the reserve's
|
|
71
|
+
* free liquidity, so the request is the balance read a moment ago plus a small margin for the
|
|
72
|
+
* interest since: everything comes out, and the request stays well inside the liquidity. A
|
|
73
|
+
* position with debt cannot be emptied: NAVI would refuse the health check, so callers skip the
|
|
74
|
+
* sweep and hand the cap over instead. `refreshNaviPrices` must precede this in the transaction.
|
|
75
|
+
*/
|
|
76
|
+
export function withdrawAllInto(tx, d, cap, position, owner) {
|
|
77
|
+
const n = d.protocols.navi, coins = [];
|
|
78
|
+
for (const [coin, r] of Object.entries(n.reserves)) {
|
|
79
|
+
const have = position.collateral[normalizeStructTag(coin)];
|
|
80
|
+
if (!have)
|
|
81
|
+
continue;
|
|
82
|
+
const request = (BigInt(have) + BigInt(have) / 1000n + 1n).toString();
|
|
83
|
+
const balance = tx.moveCall({
|
|
84
|
+
target: `${n.publishedAt}::incentive_v3::withdraw_with_account_cap_v2`,
|
|
85
|
+
typeArguments: [coin],
|
|
86
|
+
arguments: [
|
|
87
|
+
tx.object(CLOCK),
|
|
88
|
+
tx.object(n.oracleId),
|
|
89
|
+
tx.object(n.storageId),
|
|
90
|
+
tx.object(r.poolId),
|
|
91
|
+
tx.pure.u8(r.assetId),
|
|
92
|
+
tx.pure.u64(request),
|
|
93
|
+
tx.object(n.incentiveV2Id),
|
|
94
|
+
tx.object(n.incentiveV3Id),
|
|
95
|
+
cap,
|
|
96
|
+
tx.object(SUI_SYSTEM),
|
|
97
|
+
],
|
|
98
|
+
});
|
|
99
|
+
coins.push(tx.moveCall({
|
|
100
|
+
target: '0x2::coin::from_balance',
|
|
101
|
+
typeArguments: [coin],
|
|
102
|
+
arguments: [balance],
|
|
103
|
+
}));
|
|
104
|
+
}
|
|
105
|
+
if (coins.length)
|
|
106
|
+
tx.transferObjects(coins, owner);
|
|
107
|
+
}
|
|
108
|
+
/** Whether a position can be emptied in one go: something to withdraw and nothing owed. */
|
|
109
|
+
export const canSweep = (p) => !!p && Object.keys(p.collateral).length > 0 && Object.keys(p.debt).length === 0;
|
|
110
|
+
/** NAVI account caps an address holds, with the position each controls. */
|
|
111
|
+
export async function naviCapsOf(client, d, owner) {
|
|
112
|
+
const wanted = `${normalizeSuiAddress(d.protocols.navi.packageId)}::account::AccountCap`;
|
|
113
|
+
const found = [];
|
|
114
|
+
let cursor = null;
|
|
115
|
+
do {
|
|
116
|
+
const page = await client.listOwnedObjects({ owner, cursor, limit: 50, include: { content: true } });
|
|
117
|
+
for (const o of page.objects) {
|
|
118
|
+
const tag = parseStructTag(o.type);
|
|
119
|
+
if (`${normalizeSuiAddress(tag.address)}::${tag.module}::${tag.name}` === wanted && o.content)
|
|
120
|
+
found.push({ capId: o.objectId, user: naviAccountOwner(o.content) });
|
|
121
|
+
}
|
|
122
|
+
cursor = page.hasNextPage ? page.cursor : null;
|
|
123
|
+
} while (cursor && found.length < 200);
|
|
124
|
+
return found;
|
|
125
|
+
}
|