diff --git a/.changeset/olive-pandas-repeat.md b/.changeset/olive-pandas-repeat.md new file mode 100644 index 000000000..09257c2ba --- /dev/null +++ b/.changeset/olive-pandas-repeat.md @@ -0,0 +1,5 @@ +--- +'@mysten/sui': minor +--- + +Add `listTransactions` and `listEvents` core API methods for querying transactions and events with filters, pagination, and ordering. The methods behave identically across the gRPC, GraphQL, and JSON-RPC transports. The regenerated gRPC protos also add the new `SubscribeTransactions` and `SubscribeEvents` subscription APIs. diff --git a/packages/docs/content/sui/clients/core.mdx b/packages/docs/content/sui/clients/core.mdx index b3871e748..d277dd277 100644 --- a/packages/docs/content/sui/clients/core.mdx +++ b/packages/docs/content/sui/clients/core.mdx @@ -460,6 +460,114 @@ const finalResult = await client.core.waitForTransaction({ }); ``` +## Query methods + +Query methods return pages of transactions or events. They behave identically on every transport, +and share a common set of options: + +| Option | Type | Description | +| -------- | ---------------------------------- | ----------------------------------------------------- | +| `filter` | `TransactionFilter \| EventFilter` | Filter to apply (transactions and events) | +| `limit` | `number` | Maximum number of items to return (defaults to 50) | +| `after` | `string \| null` | Return items strictly after this cursor (ascending) | +| `before` | `string \| null` | Return items strictly before this cursor (descending) | +| `order` | `'ascending' \| 'descending'` | Order of results (defaults to `ascending`) | + +A filter specifies exactly one predicate. MVR names in `function` and `eventType` predicates are +resolved automatically. + +`after` and `before` are exclusive ledger-position bounds, and each page reports the positions of +its first and last items as `startCursor` and `endCursor`. You can provide at most one bound per +query, and the bound implies the traversal direction (`after` reads ascending, `before` reads +descending), so a feed of recent activity can page in both directions from any point: + +```typescript +// Fetch the most recent transactions +const latest = await client.core.listTransactions({ + filter: { sender: '0xabc...' }, + order: 'descending', + limit: 10, +}); + +// Load older transactions +const older = await client.core.listTransactions({ + filter: { sender: '0xabc...' }, + before: latest.endCursor, + limit: 10, +}); + +// Poll for transactions executed since +const newer = await client.core.listTransactions({ + filter: { sender: '0xabc...' }, + after: latest.startCursor, +}); +``` + + + Richer filtering (combined predicates, additional predicates like affected addresses and objects, + and checkpoint ranges on transaction and event queries) is available through the [raw gRPC ledger + service](/sui/clients/grpc#ledger-service) and [raw GraphQL queries](/sui/clients/graphql). + + +### `listTransactions` + +Query transactions matching a filter. Results support the same `include` options as +[`getTransaction`](#gettransaction). + +```typescript +const result = await client.core.listTransactions({ + filter: { function: '0x2::coin::transfer' }, + limit: 10, + include: { effects: true }, +}); + +for (const tx of result.transactions) { + const transaction = tx.Transaction ?? tx.FailedTransaction; + console.log(transaction.digest, tx.$kind); +} + +// Paginate +if (result.hasNextPage) { + const nextPage = await client.core.listTransactions({ + filter: { function: '0x2::coin::transfer' }, + after: result.endCursor, + }); +} +``` + +Transaction filters support one of the following predicates: + +| Predicate | Description | +| ---------- | --------------------------------------------------------------------------- | +| `sender` | Transactions sent by an address | +| `function` | Transactions calling a Move function (`pkg`, `pkg::mod`, or `pkg::mod::fn`) | + +### `listEvents` + +Query events matching a filter. Each event includes its position in the ledger (`transactionDigest` +and `eventIndex`, plus `checkpoint` on transports that can provide it; `checkpoint` is `null` on +JSON-RPC). + +```typescript +const result = await client.core.listEvents({ + filter: { eventType: '0x2::coin::CoinCreated' }, + order: 'descending', + limit: 10, +}); + +for (const event of result.events) { + console.log(event.eventType, event.transactionDigest, event.json); +} +``` + +Event filters support one of the following predicates: + +| Predicate | Description | +| ------------ | --------------------------------------------------------------------------------- | +| `sender` | Events from transactions sent by an address | +| `emitModule` | Events emitted by a module (`pkg::mod`) | +| `eventType` | Events with types defined in a module (`pkg::mod`) or a fully qualified type name | + ## System methods ### `getReferenceGasPrice` @@ -613,6 +721,9 @@ const options: SuiClientTypes.GetObjectOptions<{ content: true }> = { | `TransactionResult` | Success or failure result from execution | | `TransactionEffects` | Detailed effects from transaction execution | | `Event` | Emitted event from a transaction | +| `EventEntry` | Queried event with its ledger position | +| `TransactionFilter` | Filter for transaction queries | +| `EventFilter` | Filter for event queries | | `ObjectOwner` | Union of all owner types | | `ExecutionStatus` | Success/failure status with error details | | `DynamicFieldName` | Name identifier for dynamic fields | @@ -643,3 +754,5 @@ const options: SuiClientTypes.GetObjectOptions<{ content: true }> = { | `SignAndExecuteTransactionOptions` | Options for `signAndExecuteTransaction` | | `GetTransactionOptions` | Options for `getTransaction` | | `WaitForTransactionOptions` | Options for `waitForTransaction` | +| `ListTransactionsOptions` | Options for `listTransactions` | +| `ListEventsOptions` | Options for `listEvents` | diff --git a/packages/docs/content/sui/clients/grpc.mdx b/packages/docs/content/sui/clients/grpc.mdx index 138235f23..de1f72c7c 100644 --- a/packages/docs/content/sui/clients/grpc.mdx +++ b/packages/docs/content/sui/clients/grpc.mdx @@ -176,6 +176,74 @@ const { response } = await grpcClient.ledgerService.getTransaction({ const { response: epochInfo } = await grpcClient.ledgerService.getEpoch({}); ``` +The ledger service also provides streaming `listCheckpoints`, `listTransactions`, and `listEvents` +RPCs. For most use cases, prefer the corresponding +[core API query methods](/sui/clients/core#query-methods), which handle pagination and filter +construction for you. The raw RPCs additionally support DNF filters (combined, negated, and +additional predicates like `affected_address` and `package_write`) and checkpoint range bounds that +the core API does not expose: + +```typescript +// Transactions that affected an address but were not sent by it: +const stream = grpcClient.ledgerService.listTransactions({ + filter: { + terms: [ + { + literals: [ + { + negated: false, + predicate: { + oneofKind: 'affectedAddress', + affectedAddress: { address: '0xabc...' }, + }, + }, + { + negated: true, + predicate: { oneofKind: 'sender', sender: { address: '0xabc...' } }, + }, + ], + }, + ], + }, + readMask: { paths: ['digest'] }, +}); + +for await (const frame of stream.responses) { + if (frame.transaction) { + console.log(frame.transaction.digest); + } +} +``` + +### Subscription service + +Subscribe to filtered, real-time streams of checkpoints, transactions, or events. Streams begin at +the current tip of the chain: + +```typescript +const stream = grpcClient.subscriptionService.subscribeTransactions({ + filter: { + terms: [ + { + literals: [ + { + negated: false, + predicate: { oneofKind: 'sender', sender: { address: '0xabc...' } }, + }, + ], + }, + ], + }, + readMask: { paths: ['digest', 'effects.status'] }, +}); + +for await (const frame of stream.responses) { + if (frame.transaction) { + console.log(frame.transaction.digest); + } +} +``` + ### State service ```typescript diff --git a/packages/docs/content/sui/migrations/sui-2.0/json-rpc-migration.mdx b/packages/docs/content/sui/migrations/sui-2.0/json-rpc-migration.mdx index cb4bc96ea..3278b7cb5 100644 --- a/packages/docs/content/sui/migrations/sui-2.0/json-rpc-migration.mdx +++ b/packages/docs/content/sui/migrations/sui-2.0/json-rpc-migration.mdx @@ -18,16 +18,17 @@ This guide covers migrating from `SuiJsonRpcClient` to the new client APIs. The being deprecated in favor of `SuiGrpcClient` and `SuiGraphQLClient`. - We recommend using `SuiGrpcClient` for most operations and `SuiGraphQLClient` for complex queries - like filtering transactions and events. + Use `SuiGrpcClient` for most operations, including filtering transactions and events with the core + query API. `SuiGraphQLClient` remains useful for complex queries that aren't covered by the core + API. ## Choosing a client -| Client | Best For | -| ------------------ | --------------------------------------------------------------- | -| `SuiGrpcClient` | Most operations, SDK integrations, real-time data | -| `SuiGraphQLClient` | Complex queries, filtering transactions/events, historical data | +| Client | Best For | +| ------------------ | -------------------------------------------------------------------- | +| `SuiGrpcClient` | Most operations, SDK integrations, queries, real-time data | +| `SuiGraphQLClient` | Complex queries not covered by the core API, like historical objects | ## Quick migration to gRPC @@ -72,6 +73,60 @@ These JSON-RPC methods have direct replacements in the core API: | `dryRunTransactionBlock` | `simulateTransaction` | | `getNormalizedMoveFunction` | `getMoveFunction` | | `getMoveFunctionArgTypes` | `getMoveFunction` | +| `queryTransactionBlocks` | `listTransactions` | +| `queryEvents` | `listEvents` | + + + The query methods (`listTransactions` and `listEvents`) behave identically on every client, + including `SuiJsonRpcClient`. See [Core API query methods](/sui/clients/core#query-methods) for + details. + + +### Example: Migrating queryTransactionBlocks + +```diff +- const result = await jsonRpcClient.queryTransactionBlocks({ +- filter: { FromAddress: '0xabc...' }, +- options: { showEffects: true }, +- limit: 10, +- }); +- const digests = result.data.map((tx) => tx.digest); ++ const result = await client.core.listTransactions({ ++ filter: { sender: '0xabc...' }, ++ include: { effects: true }, ++ limit: 10, ++ }); ++ const digests = result.transactions.map( ++ (tx) => (tx.Transaction ?? tx.FailedTransaction).digest, ++ ); +``` + +The core API filters map to the JSON-RPC `TransactionFilter` variants: + +| JSON-RPC filter | Core API filter predicate | +| -------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------- | +| `FromAddress` | `sender` | +| `MoveFunction` | `function` | +| `ToAddress` / `FromOrToAddress` / `ChangedObject` / `AffectedObject` | No core equivalent. Use raw [gRPC](/sui/clients/grpc#ledger-service) or [GraphQL](/sui/clients/graphql) queries | + +### Example: Migrating queryEvents + +```diff +- const result = await jsonRpcClient.queryEvents({ +- query: { MoveEventType: '0x2::coin::CoinCreated' }, +- limit: 10, +- order: 'descending', +- }); ++ const result = await client.core.listEvents({ ++ filter: { eventType: '0x2::coin::CoinCreated' }, ++ limit: 10, ++ order: 'descending', ++ }); +``` + +The JSON-RPC `EventFilter` variants map to `sender`, `emitModule` (replaces `MoveModule` / +`MoveEventModule`), and `eventType` (replaces `MoveEventType`). Each returned event includes its +ledger position (`checkpoint`, `transactionDigest`, and `eventIndex`). ### Example: Migrating devInspectTransactionBlock @@ -109,6 +164,7 @@ These JSON-RPC methods can be replaced by calling gRPC service clients directly: | JSON-RPC Method | gRPC Service Replacement | | ----------------------------------- | ---------------------------------------------------------------- | | `getCheckpoint` | `ledgerService.getCheckpoint` | +| `getCheckpoints` | `ledgerService.listCheckpoints` | | `getLatestCheckpointSequenceNumber` | `ledgerService.getServiceInfo` (read `checkpointHeight`) | | `getCurrentEpoch` | `ledgerService.getEpoch` (omit `epoch` for the current) | | `getLatestSuiSystemState` | `ledgerService.getEpoch` with `system_state` in the read mask | @@ -123,10 +179,10 @@ These JSON-RPC methods can be replaced by calling gRPC service clients directly: | `resolveNameServiceNames` | `nameService.reverseLookupName` | - `getCheckpoints` and `getEpochs` (paginated listings) have no gRPC equivalent — use the - `checkpoints` / `epochs` GraphQL queries instead. `getValidatorsApy` also has no replacement: - there is no canonical definition of validator APY, so it must be computed client-side from - validator exchange rates (see [Validator APY](#validator-apy) below). + `getEpochs` (paginated listing) has no gRPC equivalent. Use the `epochs` GraphQL query instead. + `getValidatorsApy` also has no replacement: there is no canonical definition of validator APY, so + it must be computed client-side from validator exchange rates (see [Validator APY](#validator-apy) + below). ### Example: using gRPC service clients @@ -184,16 +240,53 @@ const { response: address } = await client.nameService.lookupName({ }); ``` +## Methods replaced by gRPC subscriptions + +Replace the deprecated JSON-RPC websocket subscriptions with the gRPC `subscriptionService`, which +provides filtered, real-time streams: + +| JSON-RPC Method | gRPC Service Replacement | +| ---------------------- | ------------------------------------------- | +| `subscribeTransaction` | `subscriptionService.subscribeTransactions` | +| `subscribeEvent` | `subscriptionService.subscribeEvents` | + +```typescript +const stream = client.subscriptionService.subscribeEvents({ + filter: { + terms: [ + { + literals: [ + { + negated: false, + predicate: { + oneofKind: 'eventType', + eventType: { eventType: '0x2::coin::CoinCreated' }, + }, + }, + ], + }, + ], + }, + readMask: { paths: ['event_type', 'contents', 'json', 'checkpoint', 'transaction_digest'] }, +}); + +for await (const frame of stream.responses) { + if (frame.event) { + console.log(frame.event.eventType, frame.event.json); + } +} +``` + +Subscriptions always begin at the current tip of the chain. To recover events you missed between +subscriptions, replay the gap with `client.core.listEvents` using the same filter. + ## Methods requiring GraphQL Some JSON-RPC methods don't have gRPC equivalents and require using `SuiGraphQLClient` instead: | JSON-RPC Method | GraphQL Alternative | | --------------------------- | --------------------------------------------------------- | -| `queryTransactionBlocks` | `transactions` query | | `multiGetTransactionBlocks` | `multiGetTransactionEffects` query | -| `queryEvents` | `events` query | -| `getCheckpoints` | `checkpoints` query | | `getEpochs` | `epochs` query | | `getCoinMetadata` | `coinMetadata` query (or gRPC `stateService.getCoinInfo`) | | `getTotalSupply` | `coinMetadata` query (or gRPC `stateService.getCoinInfo`) | @@ -215,94 +308,6 @@ const graphqlClient = new SuiGraphQLClient({ }); ``` -### Querying transactions - -Replace `queryTransactionBlocks` with a GraphQL query: - -```typescript -const result = await graphqlClient.query({ - query: ` - query QueryTransactions($sender: SuiAddress, $first: Int, $after: String) { - transactions( - first: $first - after: $after - filter: { sentAddress: $sender } - ) { - pageInfo { - hasNextPage - endCursor - } - nodes { - digest - effects { - status - epoch { epochId } - } - } - } - } - `, - variables: { - sender: '0xabc...', - first: 10, - }, -}); -``` - -**Available transaction filters:** - -- `sentAddress`: Filter by sender address -- `affectedAddress`: Filter by any address involved in the transaction -- `affectedObject`: Filter by object ID that was affected -- `function`: Filter by Move function called (for example, `0x2::coin::transfer`) -- `kind`: Filter by transaction kind (`SYSTEM` or `PROGRAMMABLE`) -- `atCheckpoint` / `beforeCheckpoint` / `afterCheckpoint` - Filter by checkpoint - -### Querying events - -Replace `queryEvents` with a GraphQL query: - -```typescript -const result = await graphqlClient.query({ - query: ` - query QueryEvents($type: String, $first: Int, $after: String) { - events( - first: $first - after: $after - filter: { type: $type } - ) { - pageInfo { - hasNextPage - endCursor - } - nodes { - transactionModule { - package { address } - name - } - sender { address } - contents { - type { repr } - bcs - } - } - } - } - `, - variables: { - type: '0x2::coin::CoinCreated', - first: 10, - }, -}); -``` - -**Available event filters:** - -- `type`: Filter by event type (package, package::module, or full type) -- `module`: Filter by emitting module -- `sender`: Filter by transaction sender -- `atCheckpoint` / `beforeCheckpoint` / `afterCheckpoint`: Filter by checkpoint - ### Fetching multiple transactions Replace `multiGetTransactionBlocks` with a GraphQL query: diff --git a/packages/sui/JSON_RPC_SUNSET_FOLLOW_UPS.md b/packages/sui/JSON_RPC_SUNSET_FOLLOW_UPS.md new file mode 100644 index 000000000..6cdfdfa83 --- /dev/null +++ b/packages/sui/JSON_RPC_SUNSET_FOLLOW_UPS.md @@ -0,0 +1,76 @@ +# Core query API follow-ups gated on the JSON-RPC sunset + +The core query APIs (`listTransactions`, `listEvents`, `listCheckpoints`) are intentionally +restricted to what all three transports (gRPC, GraphQL, JSON-RPC) can support with identical +behavior, verified by the parity tests in `test/e2e/clients/core/queries.test.ts`. Several +capabilities were left out of the core API only because JSON-RPC cannot express them. Once JSON-RPC +is fully deprecated and removed, they can be promoted into the core API. + +Until then, advanced filtering is available through the raw gRPC ledger service +(`client.ledgerService.listTransactions` and friends take full DNF filters), and through raw GraphQL +queries where the schema supports it. Promoting a capability means extending the core filter types +in `src/client/types.ts` and the shared resolution helpers in `src/client/query-filters.ts`, +implementing the gRPC/GraphQL mappings, and extending the parity tests. + +## Blocked only by JSON-RPC + +- **`affectedAddress` transaction predicate** — no JSON-RPC equivalent: `FromOrToAddress` is + rejected server-side ("CURRENTLY NOT SUPPORTED"), and emulating it with `FromAddress` + + `ToAddress` queries silently misses sponsored transactions and address-balance deposits, and + cannot reproduce exact intra-checkpoint ordering. +- **`affectedObject` transaction predicate** — the JSON-RPC `AffectedObject` filter is feature-gated + server-side and rejected by default (`Feature is not supported`), and the older `ChangedObject` + filter has narrower semantics (misses deleted/wrapped/read objects). +- **`startCheckpoint`/`endCheckpoint` on `listTransactions` and `listEvents`** — JSON-RPC has no + checkpoint range support, and its single-checkpoint `Checkpoint` filter cannot be combined with + other filters. +- **A `listCheckpoints` core method** — a summaries-only version (sequence number, digest, epoch, + timestamp) was prototyped and dropped from the initial release because listing checkpoints is + mostly useful for getting the transactions they contain, which is better served by + `listTransactions` with checkpoint ranges once those are promoted. Including per-checkpoint + transaction digests has exact parity on gRPC and JSON-RPC, but GraphQL exposes them as a nested + connection capped at the server page size, which would silently truncate busy checkpoints. If a + core method is added, checkpoint ranges are implementable exactly on every transport (JSON-RPC's + sequence-number cursor emulates them). +- **Combining `after` and `before` bounds, and mixing bounds with either order** — JSON-RPC cursors + are direction-relative, so a query currently accepts at most one bound and the bound must match + the traversal direction (`after` implies ascending, `before` descending). gRPC treats both as + order-independent canonical position bounds and GraphQL accepts both Relay bounds together, so + windowed queries (`after` + `before`) and reversed reads within a bound can be enabled by relaxing + `resolvePagination` once JSON-RPC is gone. +- **Combining multiple predicates in one filter (AND)** — JSON-RPC accepts exactly one filter per + query. GraphQL supports one instance of each predicate ANDed together, so this can be promoted to + that level once JSON-RPC is gone. +- **Pagination bounds with partial function filters** — JSON-RPC rejects cursors on package- or + module-level `MoveFunction` filters ("Cannot supply cursor without supplying module and + function"), so `validateTransactionQuery` requires a fully qualified `package::module::function` + when `after`/`before` is provided. gRPC and GraphQL paginate partial function filters without + restriction. +- **Package-only prefixes for event predicates** — core `emitModule` requires `package::module` and + `eventType` requires at least a module because JSON-RPC's `MoveModule`/`MoveEventModule` filters + require a module name. GraphQL and gRPC both support package-only prefixes. +- **Non-nullable `EventEntry.checkpoint`** — JSON-RPC `queryEvents` responses carry no checkpoint + information, so the field is `string | null` and `null` on JSON-RPC. + +## Blocked by GraphQL as well + +- **OR of filters, negated predicates, and repeated predicate values** — the gRPC DNF filter model + (`terms` of ANDed, optionally negated literals). GraphQL currently has no equivalent. +- **`emitModule`/`eventType`/`eventStreamHead`/`packageWrite` as _transaction_ predicates** — not + present on the GraphQL `TransactionFilter` input. +- **Transaction filters on `listCheckpoints`** — the GraphQL `checkpoints` query only supports + checkpoint/epoch bounds, not transaction predicates. +- **`eventStreamHead` event predicate** — not present on the GraphQL `EventFilter` input. + +## Other tracked items + +- The gRPC parity e2e tests in `test/e2e/clients/core/queries.test.ts` are excluded via the + `EXCLUDE` constant until `SUI_TOOLS_TAG` points at an image that serves the + `ListCheckpoints`/`ListTransactions`/`ListEvents` RPCs. +- `MoveEventType` matching semantics for generic types differ between JSON-RPC (exact struct tag + match) and gRPC/GraphQL (a bare `pkg::mod::Name` matches any instantiation). The parity tests only + cover non-generic event types; revisit when promoting package-only prefixes. +- The JSON-RPC `MoveFunction` filter matches aborted transactions, while the GraphQL `function` + filter only matches successful ones (gRPC behavior is unverified until the List RPCs are + testable). The parity tests avoid this by querying a freshly published package whose calls all + succeed; revisit once JSON-RPC is gone. diff --git a/packages/sui/src/bcs/bcs.ts b/packages/sui/src/bcs/bcs.ts index 250e982d6..31178a608 100644 --- a/packages/sui/src/bcs/bcs.ts +++ b/packages/sui/src/bcs/bcs.ts @@ -228,13 +228,6 @@ export const ProgrammableTransaction = bcs.struct('ProgrammableTransaction', { commands: bcs.vector(Command), }); -export const TransactionKind = bcs.enum('TransactionKind', { - ProgrammableTransaction: ProgrammableTransaction, - ChangeEpoch: null, - Genesis: null, - ConsensusCommitPrologue: null, -}); - // Rust: crates/sui-types/src/transaction.rs export const ValidDuring = bcs.struct('ValidDuring', { minEpoch: bcs.option(bcs.u64()), @@ -265,17 +258,6 @@ export const GasData = bcs.struct('GasData', { budget: bcs.u64(), }); -export const TransactionDataV1 = bcs.struct('TransactionDataV1', { - kind: TransactionKind, - sender: Address, - gasData: GasData, - expiration: TransactionExpiration, -}); - -export const TransactionData = bcs.enum('TransactionData', { - V1: TransactionDataV1, -}); - export const IntentScope = bcs.enum('IntentScope', { TransactionData: null, TransactionEffects: null, @@ -341,15 +323,6 @@ export const base64String = bcs.byteVector().transform({ output: (val) => toBase64(new Uint8Array(val)), }); -export const SenderSignedTransaction = bcs.struct('SenderSignedTransaction', { - intentMessage: IntentMessage(TransactionData), - txSignatures: bcs.vector(base64String), -}); - -export const SenderSignedData = bcs.vector(SenderSignedTransaction, { - name: 'SenderSignedData', -}); - export const PasskeyAuthenticator = bcs.struct('PasskeyAuthenticator', { authenticatorData: bcs.byteVector(), clientDataJson: bcs.string(), diff --git a/packages/sui/src/bcs/index.ts b/packages/sui/src/bcs/index.ts index 616d7582b..d26c55054 100644 --- a/packages/sui/src/bcs/index.ts +++ b/packages/sui/src/bcs/index.ts @@ -30,19 +30,21 @@ import { ProgrammableMoveCall, ProgrammableTransaction, PublicKey, - SenderSignedData, - SenderSignedTransaction, SharedObjectRef, StructTag, SuiObjectRef, - TransactionData, - TransactionDataV1, TransactionExpiration, - TransactionKind, TypeOrigin, TypeTag, UpgradeInfo, } from './bcs.js'; +import { + SenderSignedData, + SenderSignedTransaction, + TransactionData, + TransactionDataV1, + TransactionKind, +} from './transactions.js'; import { TransactionEffects } from './effects.js'; export type { TypeTag } from './types.js'; diff --git a/packages/sui/src/bcs/transactions.ts b/packages/sui/src/bcs/transactions.ts new file mode 100644 index 000000000..feb3f5a4d --- /dev/null +++ b/packages/sui/src/bcs/transactions.ts @@ -0,0 +1,226 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +import { bcs } from '@mysten/bcs'; + +import { + Address, + base64String, + Data, + GasData, + IntentMessage, + ObjectDigest, + Owner, + ProgrammableTransaction, + TransactionExpiration, + TypeTag, +} from './bcs.js'; + +// Rust: crates/sui-types/src/transaction.rs +export const ChangeEpoch = bcs.struct('ChangeEpoch', { + epoch: bcs.u64(), + protocolVersion: bcs.u64(), + storageCharge: bcs.u64(), + computationCharge: bcs.u64(), + storageRebate: bcs.u64(), + nonRefundableStorageFee: bcs.u64(), + epochStartTimestampMs: bcs.u64(), + systemPackages: bcs.vector( + bcs.tuple([bcs.u64(), bcs.vector(bcs.byteVector()), bcs.vector(Address)]), + ), +}); + +// Rust: crates/sui-types/src/transaction.rs +export const GenesisObject = bcs.enum('GenesisObject', { + RawObject: bcs.struct('RawObject', { + data: Data, + owner: Owner, + }), +}); + +// Rust: crates/sui-types/src/transaction.rs +export const GenesisTransaction = bcs.struct('GenesisTransaction', { + objects: bcs.vector(GenesisObject), +}); + +// Rust: crates/sui-types/src/messages_consensus.rs +export const ConsensusCommitPrologue = bcs.struct('ConsensusCommitPrologue', { + epoch: bcs.u64(), + round: bcs.u64(), + commitTimestampMs: bcs.u64(), +}); + +// Rust: crates/sui-types/src/messages_consensus.rs +export const ConsensusCommitPrologueV2 = bcs.struct('ConsensusCommitPrologueV2', { + epoch: bcs.u64(), + round: bcs.u64(), + commitTimestampMs: bcs.u64(), + consensusCommitDigest: ObjectDigest, +}); + +// Rust: crates/sui-types/src/messages_consensus.rs +export const ConsensusDeterminedVersionAssignments = bcs.enum( + 'ConsensusDeterminedVersionAssignments', + { + CancelledTransactions: bcs.vector( + bcs.tuple([ObjectDigest, bcs.vector(bcs.tuple([Address, bcs.u64()]))]), + ), + CancelledTransactionsV2: bcs.vector( + bcs.tuple([ + ObjectDigest, + bcs.vector(bcs.tuple([bcs.tuple([Address, bcs.u64()]), bcs.u64()])), + ]), + ), + }, +); + +// Rust: crates/sui-types/src/messages_consensus.rs +export const ConsensusCommitPrologueV3 = bcs.struct('ConsensusCommitPrologueV3', { + epoch: bcs.u64(), + round: bcs.u64(), + subDagIndex: bcs.option(bcs.u64()), + commitTimestampMs: bcs.u64(), + consensusCommitDigest: ObjectDigest, + consensusDeterminedVersionAssignments: ConsensusDeterminedVersionAssignments, +}); + +// Rust: crates/sui-types/src/messages_consensus.rs +export const ConsensusCommitPrologueV4 = bcs.struct('ConsensusCommitPrologueV4', { + epoch: bcs.u64(), + round: bcs.u64(), + subDagIndex: bcs.option(bcs.u64()), + commitTimestampMs: bcs.u64(), + consensusCommitDigest: ObjectDigest, + consensusDeterminedVersionAssignments: ConsensusDeterminedVersionAssignments, + additionalStateDigest: ObjectDigest, +}); + +// Rust: crates/sui-types/src/authenticator_state.rs +export const ActiveJwk = bcs.struct('ActiveJwk', { + jwkId: bcs.struct('JwkId', { + iss: bcs.string(), + kid: bcs.string(), + }), + jwk: bcs.struct('JWK', { + kty: bcs.string(), + e: bcs.string(), + n: bcs.string(), + alg: bcs.string(), + }), + epoch: bcs.u64(), +}); + +// Rust: crates/sui-types/src/transaction.rs +export const AuthenticatorStateUpdate = bcs.struct('AuthenticatorStateUpdate', { + epoch: bcs.u64(), + round: bcs.u64(), + newActiveJwks: bcs.vector(ActiveJwk), + authenticatorObjInitialSharedVersion: bcs.u64(), +}); + +// Rust: crates/sui-types/src/transaction.rs +export const RandomnessStateUpdate = bcs.struct('RandomnessStateUpdate', { + epoch: bcs.u64(), + randomnessRound: bcs.u64(), + randomBytes: bcs.byteVector(), + randomnessObjInitialSharedVersion: bcs.u64(), +}); + +// Rust: crates/sui-types/src/transaction.rs +export const AuthenticatorStateExpire = bcs.struct('AuthenticatorStateExpire', { + minEpoch: bcs.u64(), + authenticatorObjInitialSharedVersion: bcs.u64(), +}); + +// Rust: crates/sui-types/src/execution.rs +export const ExecutionTimeObservationKey = bcs.enum('ExecutionTimeObservationKey', { + MoveEntryPoint: bcs.struct('MoveEntryPoint', { + package: Address, + module: bcs.string(), + function: bcs.string(), + typeArguments: bcs.vector(TypeTag), + }), + TransferObjects: null, + SplitCoins: null, + MergeCoins: null, + Publish: null, + MakeMoveVec: null, + Upgrade: null, +}); + +// Rust: crates/sui-types/src/transaction.rs +export const StoredExecutionTimeObservations = bcs.enum('StoredExecutionTimeObservations', { + V1: bcs.vector( + bcs.tuple([ + ExecutionTimeObservationKey, + bcs.vector( + bcs.tuple([ + // AuthorityName (BLS public key bytes) + bcs.byteVector(), + bcs.struct('Duration', { + secs: bcs.u64(), + nanos: bcs.u32(), + }), + ]), + ), + ]), + ), +}); + +// Rust: crates/sui-types/src/transaction.rs +export const WriteAccumulatorStorageCost = bcs.struct('WriteAccumulatorStorageCost', { + storageCost: bcs.u64(), +}); + +// Rust: crates/sui-types/src/transaction.rs +export const EndOfEpochTransactionKind = bcs.enum('EndOfEpochTransactionKind', { + ChangeEpoch: ChangeEpoch, + AuthenticatorStateCreate: null, + AuthenticatorStateExpire: AuthenticatorStateExpire, + RandomnessStateCreate: null, + DenyListStateCreate: null, + // ChainIdentifier (the genesis checkpoint digest) + BridgeStateCreate: ObjectDigest, + BridgeCommitteeInit: bcs.u64(), + StoreExecutionTimeObservations: StoredExecutionTimeObservations, + AccumulatorRootCreate: null, + CoinRegistryCreate: null, + DisplayRegistryCreate: null, + AddressAliasStateCreate: null, + WriteAccumulatorStorageCost: WriteAccumulatorStorageCost, +}); + +// Rust: crates/sui-types/src/transaction.rs +export const TransactionKind = bcs.enum('TransactionKind', { + ProgrammableTransaction: ProgrammableTransaction, + ChangeEpoch: ChangeEpoch, + Genesis: GenesisTransaction, + ConsensusCommitPrologue: ConsensusCommitPrologue, + AuthenticatorStateUpdate: AuthenticatorStateUpdate, + EndOfEpochTransaction: bcs.vector(EndOfEpochTransactionKind), + RandomnessStateUpdate: RandomnessStateUpdate, + ConsensusCommitPrologueV2: ConsensusCommitPrologueV2, + ConsensusCommitPrologueV3: ConsensusCommitPrologueV3, + ConsensusCommitPrologueV4: ConsensusCommitPrologueV4, + ProgrammableSystemTransaction: ProgrammableTransaction, +}); + +export const TransactionDataV1 = bcs.struct('TransactionDataV1', { + kind: TransactionKind, + sender: Address, + gasData: GasData, + expiration: TransactionExpiration, +}); + +export const TransactionData = bcs.enum('TransactionData', { + V1: TransactionDataV1, +}); + +export const SenderSignedTransaction = bcs.struct('SenderSignedTransaction', { + intentMessage: IntentMessage(TransactionData), + txSignatures: bcs.vector(base64String), +}); + +export const SenderSignedData = bcs.vector(SenderSignedTransaction, { + name: 'SenderSignedData', +}); diff --git a/packages/sui/src/client/core.ts b/packages/sui/src/client/core.ts index bb1139a19..332f21418 100644 --- a/packages/sui/src/client/core.ts +++ b/packages/sui/src/client/core.ts @@ -112,6 +112,14 @@ export abstract class CoreClient extends BaseClient implements SuiClientTypes.Tr options: SuiClientTypes.ListDynamicFieldsOptions, ): Promise; + abstract listTransactions( + options: SuiClientTypes.ListTransactionsOptions, + ): Promise>; + + abstract listEvents( + options: SuiClientTypes.ListEventsOptions, + ): Promise; + abstract resolveTransactionPlugin(): TransactionPlugin; abstract verifyZkLoginSignature( diff --git a/packages/sui/src/client/query-filters.ts b/packages/sui/src/client/query-filters.ts new file mode 100644 index 000000000..1d0a85400 --- /dev/null +++ b/packages/sui/src/client/query-filters.ts @@ -0,0 +1,191 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +import { normalizeSuiAddress } from '../utils/sui-types.js'; +import type { SuiClientTypes } from './types.js'; + +export interface ResolvedPagination { + descending: boolean; + after: string | undefined; + before: string | undefined; + limit: number; +} + +/** + * Default page size applied when `limit` is omitted, so every transport + * requests the same page size instead of falling back to differing server + * defaults. + */ +const DEFAULT_PAGE_SIZE = 50; + +/** + * Validates pagination bounds and resolves the traversal direction. + * + * `after` and `before` are exclusive ledger-position bounds. Until every + * transport can combine bounds freely, a query accepts at most one bound, and + * the bound must match the traversal direction (`after` implies ascending, + * `before` implies descending). + */ +export function resolvePagination(options: { + after?: string | null; + before?: string | null; + order?: 'ascending' | 'descending'; + limit?: number; +}): ResolvedPagination { + if (options.after != null && options.before != null) { + throw new Error('Only one of `after` or `before` may be provided'); + } + + const descending = options.order ? options.order === 'descending' : options.before != null; + + if (options.after != null && descending) { + throw new Error('`after` can not be combined with descending queries'); + } + + if (options.before != null && !descending) { + throw new Error('`before` can not be combined with ascending queries'); + } + + return { + descending, + after: options.after ?? undefined, + before: options.before ?? undefined, + limit: options.limit ?? DEFAULT_PAGE_SIZE, + }; +} + +export type ResolvedTransactionFilter = + | { $kind: 'sender'; sender: string } + | { $kind: 'function'; package: string; module?: string; function?: string }; + +/** + * Validates that a transaction query's filter supports pagination bounds. + * + * Function filters can only be paginated when fully qualified, since JSON-RPC + * rejects cursors on package- or module-level `MoveFunction` filters. + */ +export function validateTransactionQuery( + filter: ResolvedTransactionFilter | undefined, + pagination: ResolvedPagination, +): void { + if ( + filter?.$kind === 'function' && + (pagination.after != null || pagination.before != null) && + (!filter.module || !filter.function) + ) { + throw new Error( + 'Paginating transactions filtered by function requires a fully qualified `package::module::function`', + ); + } +} + +export type ResolvedEventFilter = + | { $kind: 'sender'; sender: string } + | { $kind: 'emitModule'; package: string; module: string } + | { $kind: 'eventTypeModule'; package: string; module: string } + | { $kind: 'eventType'; eventType: string }; + +/** + * Validates a transaction filter and resolves any MVR names it contains. + * + * Every transport resolves filters through this function so that validation + * errors and MVR handling are identical across clients. + */ +export async function resolveTransactionFilter( + mvr: SuiClientTypes.MvrMethods, + filter: SuiClientTypes.TransactionFilter, +): Promise { + assertSinglePredicate(filter, ['sender', 'function'], 'transaction'); + + if (filter.sender != null) { + return { $kind: 'sender', sender: normalizeSuiAddress(filter.sender) }; + } + + const [pkg, module, fn, ...rest] = filter.function!.split('::'); + + if (!pkg || rest.length > 0 || module === '' || fn === '') { + throw new Error( + `Invalid function filter "${filter.function}": expected "package", "package::module", or "package::module::function"`, + ); + } + + return { + $kind: 'function', + package: normalizeSuiAddress((await mvr.resolvePackage({ package: pkg })).package), + module, + function: fn, + }; +} + +/** + * Validates an event filter and resolves any MVR names it contains. + * + * `emitModule` must name a module (`package::module`), and `eventType` must be + * at least module-qualified (`package::module` or a full type). Every transport + * resolves filters through this function so that validation errors and MVR + * handling are identical across clients. + */ +export async function resolveEventFilter( + mvr: SuiClientTypes.MvrMethods, + filter: SuiClientTypes.EventFilter, +): Promise { + assertSinglePredicate(filter, ['sender', 'emitModule', 'eventType'], 'event'); + + if (filter.sender != null) { + return { $kind: 'sender', sender: normalizeSuiAddress(filter.sender) }; + } + + if (filter.emitModule != null) { + const [pkg, module, ...rest] = filter.emitModule.split('::'); + + if (!pkg || !module || rest.length > 0) { + throw new Error( + `Invalid emitModule filter "${filter.emitModule}": expected "package::module"`, + ); + } + + return { + $kind: 'emitModule', + package: normalizeSuiAddress((await mvr.resolvePackage({ package: pkg })).package), + module, + }; + } + + const eventType = filter.eventType!; + const segments = eventType.split('::'); + + if (segments.length === 2 && segments.every((segment) => segment !== '')) { + return { + $kind: 'eventTypeModule', + package: normalizeSuiAddress((await mvr.resolvePackage({ package: segments[0] })).package), + module: segments[1], + }; + } + + if (segments.length < 3 || segments.some((segment) => segment === '')) { + throw new Error( + `Invalid eventType filter "${eventType}": expected "package::module" or a fully qualified type name`, + ); + } + + return { + $kind: 'eventType', + eventType: (await mvr.resolveType({ type: eventType })).type, + }; +} + +function assertSinglePredicate( + filter: Record, + predicates: string[], + kind: string, +): void { + const present = predicates.filter((predicate) => filter[predicate] != null); + + if (present.length !== 1) { + throw new Error( + `A ${kind} filter must specify exactly one of ${predicates.join(', ')}${ + present.length ? ` (got ${present.join(', ')})` : '' + }`, + ); + } +} diff --git a/packages/sui/src/client/types.ts b/packages/sui/src/client/types.ts index f9635bc31..a00780e84 100644 --- a/packages/sui/src/client/types.ts +++ b/packages/sui/src/client/types.ts @@ -461,6 +461,150 @@ export namespace SuiClientTypes { checksEnabled?: boolean; } + /** Query methods */ + export interface TransportMethods { + listTransactions?: ( + options: ListTransactionsOptions, + ) => Promise>; + listEvents?: (options: ListEventsOptions) => Promise; + } + + /** + * A filter matching transactions. Exactly one predicate must be specified. + * + * All predicates are supported identically by every transport. + */ + export type TransactionFilter = + | { + /** Match transactions sent by this address. */ + sender: string; + function?: never; + } + | { + /** + * Match transactions that called a Move function, specified as a + * `::`-delimited Move path: `package`, `package::module`, or + * `package::module::function`. + */ + function: string; + sender?: never; + }; + + /** + * A filter matching events. Exactly one predicate must be specified. + * + * All predicates are supported identically by every transport. + */ + export type EventFilter = + | { + /** Match events emitted by transactions sent by this address. */ + sender: string; + emitModule?: never; + eventType?: never; + } + | { + /** Match events emitted by a module, specified as `package::module`. */ + emitModule: string; + sender?: never; + eventType?: never; + } + | { + /** + * Match events by type: either every event whose type is defined in a + * module (`package::module`), or events with a fully qualified type + * name (`package::module::Name` or `package::module::Name`). + */ + eventType: string; + sender?: never; + emitModule?: never; + }; + + export interface ListTransactionsOptions< + Include extends TransactionInclude = {}, + > extends CoreClientMethodOptions { + filter?: TransactionFilter; + limit?: number; + /** + * Return items strictly after this cursor in ledger order (usually the `endCursor` of + * an ascending page, or the `startCursor` of a descending page to poll for new items). + * Implies `order: 'ascending'`, and cannot be combined with `before`. + */ + after?: string | null; + /** + * Return items strictly before this cursor in ledger order (usually the `endCursor` of + * a descending page). Implies `order: 'descending'`, and cannot be combined with + * `after`. + */ + before?: string | null; + /** Order of returned results. Defaults to `ascending` (oldest first). */ + order?: 'ascending' | 'descending'; + include?: Include & TransactionInclude; + } + + export interface ListTransactionsResponse { + transactions: TransactionResult[]; + hasNextPage: boolean; + /** + * Cursor at the first returned item. After a descending read of the most recent + * items, pass as `after` to poll for items added since. + */ + startCursor: string | null; + /** + * Cursor at the last returned item. Continue the query by passing it as `after` + * (ascending) or `before` (descending). + */ + endCursor: string | null; + } + + export interface ListEventsOptions extends CoreClientMethodOptions { + filter?: EventFilter; + limit?: number; + /** + * Return items strictly after this cursor in ledger order (usually the `endCursor` of + * an ascending page, or the `startCursor` of a descending page to poll for new items). + * Implies `order: 'ascending'`, and cannot be combined with `before`. + */ + after?: string | null; + /** + * Return items strictly before this cursor in ledger order (usually the `endCursor` of + * a descending page). Implies `order: 'descending'`, and cannot be combined with + * `after`. + */ + before?: string | null; + /** Order of returned results. Defaults to `ascending` (oldest first). */ + order?: 'ascending' | 'descending'; + } + + /** An event returned from a query, along with its position in the ledger. */ + export interface EventEntry extends Event { + /** + * The sequence number of the checkpoint containing the emitting transaction. + * + * `null` on the JSON-RPC transport, which does not expose checkpoint + * information for queried events. + */ + checkpoint: string | null; + /** The digest of the transaction that emitted this event. */ + transactionDigest: string; + /** The index of this event within its transaction's event list. */ + eventIndex: number; + } + + export interface ListEventsResponse { + events: EventEntry[]; + hasNextPage: boolean; + /** + * Cursor at the first returned item. After a descending read of the most recent + * items, pass as `after` to poll for items added since. + */ + startCursor: string | null; + /** + * Cursor at the last returned item. Continue the query by passing it as `after` + * (ascending) or `before` (descending). + */ + endCursor: string | null; + } + export interface GetReferenceGasPriceOptions extends CoreClientMethodOptions {} export interface TransportMethods { diff --git a/packages/sui/src/graphql/client.ts b/packages/sui/src/graphql/client.ts index cb90a2f4a..fed517a78 100644 --- a/packages/sui/src/graphql/client.ts +++ b/packages/sui/src/graphql/client.ts @@ -324,6 +324,16 @@ export class SuiGraphQLClient = return this.core.getDynamicField(input); } + listTransactions( + input: SuiClientTypes.ListTransactionsOptions, + ): Promise> { + return this.core.listTransactions(input); + } + + listEvents(input: SuiClientTypes.ListEventsOptions): Promise { + return this.core.listEvents(input); + } + getMoveFunction( input: SuiClientTypes.GetMoveFunctionOptions, ): Promise { diff --git a/packages/sui/src/graphql/core.ts b/packages/sui/src/graphql/core.ts index 34745decc..2d467babc 100644 --- a/packages/sui/src/graphql/core.ts +++ b/packages/sui/src/graphql/core.ts @@ -28,6 +28,8 @@ import { GetOwnedObjectsDocument, GetReferenceGasPriceDocument, GetTransactionBlockDocument, + ListEventsDocument, + ListTransactionsDocument, MultiGetObjectsDocument, ResolveTransactionDocument, SimulateTransactionDocument, @@ -36,7 +38,7 @@ import { } from './generated/queries.js'; import { ObjectError, SimulationError } from '../client/errors.js'; import { chunk, fromBase64, toBase64 } from '@mysten/utils'; -import { normalizeSuiAddress } from '../utils/sui-types.js'; +import { normalizeStructTag, normalizeSuiAddress } from '../utils/sui-types.js'; import { formatMoveAbortMessage, parseTransactionEffectsBcs } from '../client/utils.js'; import type { OpenMoveTypeSignatureBody, OpenMoveTypeSignature } from './types.js'; import { @@ -49,6 +51,12 @@ import { TransactionEffects as TransactionEffectsType } from '../grpc/proto/sui/ import { Transaction as GrpcTransactionType } from '../grpc/proto/sui/rpc/v2/transaction.js'; import { TransactionDataBuilder } from '../transactions/TransactionData.js'; import type { BuildTransactionOptions } from '../transactions/index.js'; +import { + resolveEventFilter, + resolvePagination, + resolveTransactionFilter, + validateTransactionQuery, +} from '../client/query-filters.js'; export class GraphQLCoreClient extends CoreClient { #graphqlClient: SuiGraphQLClient; @@ -592,6 +600,112 @@ export class GraphQLCoreClient extends CoreClient { return this.#graphqlClient.listDynamicFields(options); } + async listTransactions( + options: SuiClientTypes.ListTransactionsOptions, + ): Promise> { + const pagination = resolvePagination(options); + const { descending, after, before, limit } = pagination; + const filter = options.filter + ? await resolveTransactionFilter(this.mvr, options.filter) + : undefined; + validateTransactionQuery(filter, pagination); + + const transactions = await this.#graphqlQuery( + { + query: ListTransactionsDocument, + signal: options.signal, + variables: { + filter: filter && { + sentAddress: filter.$kind === 'sender' ? filter.sender : undefined, + function: + filter.$kind === 'function' + ? [filter.package, filter.module, filter.function].filter(Boolean).join('::') + : undefined, + }, + first: descending ? undefined : limit, + after, + last: descending ? limit : undefined, + before, + includeTransaction: options.include?.transaction ?? false, + includeEffects: options.include?.effects ?? false, + includeEvents: options.include?.events ?? false, + includeBalanceChanges: options.include?.balanceChanges ?? false, + includeObjectTypes: options.include?.objectTypes ?? false, + includeBcs: options.include?.bcs ?? false, + }, + }, + (result) => result.transactions, + ); + + // Backwards pagination returns nodes in ascending order, so reverse them for descending reads + const nodes = descending ? [...transactions.nodes].reverse() : transactions.nodes; + + return { + transactions: nodes.map((transaction) => parseTransaction(transaction, options.include)), + hasNextPage: descending + ? transactions.pageInfo.hasPreviousPage + : transactions.pageInfo.hasNextPage, + startCursor: + (descending ? transactions.pageInfo.endCursor : transactions.pageInfo.startCursor) ?? null, + endCursor: + (descending ? transactions.pageInfo.startCursor : transactions.pageInfo.endCursor) ?? null, + }; + } + + async listEvents( + options: SuiClientTypes.ListEventsOptions, + ): Promise { + const { descending, after, before, limit } = resolvePagination(options); + const filter = options.filter ? await resolveEventFilter(this.mvr, options.filter) : undefined; + + const events = await this.#graphqlQuery( + { + query: ListEventsDocument, + signal: options.signal, + variables: { + filter: filter && { + sender: filter.$kind === 'sender' ? filter.sender : undefined, + module: + filter.$kind === 'emitModule' ? `${filter.package}::${filter.module}` : undefined, + type: + filter.$kind === 'eventTypeModule' + ? `${filter.package}::${filter.module}` + : filter.$kind === 'eventType' + ? filter.eventType + : undefined, + }, + first: descending ? undefined : limit, + after, + last: descending ? limit : undefined, + before, + }, + }, + (result) => result.events, + ); + + // Backwards pagination returns nodes in ascending order, so reverse them for descending reads + const nodes = descending ? [...events.nodes].reverse() : events.nodes; + + return { + events: nodes.map( + (event): SuiClientTypes.EventEntry => ({ + packageId: normalizeSuiAddress(event.transactionModule?.package?.address!), + module: event.transactionModule?.name!, + sender: normalizeSuiAddress(event.sender?.address!), + eventType: normalizeStructTag(event.contents?.type?.repr!), + bcs: event.contents?.bcs ? fromBase64(event.contents.bcs) : new Uint8Array(), + json: (event.contents?.json as Record) ?? null, + checkpoint: event.transaction?.effects?.checkpoint?.sequenceNumber?.toString() ?? null, + transactionDigest: event.transaction?.digest!, + eventIndex: event.sequenceNumber, + }), + ), + hasNextPage: descending ? events.pageInfo.hasPreviousPage : events.pageInfo.hasNextPage, + startCursor: (descending ? events.pageInfo.endCursor : events.pageInfo.startCursor) ?? null, + endCursor: (descending ? events.pageInfo.startCursor : events.pageInfo.endCursor) ?? null, + }; + } + async verifyZkLoginSignature( options: SuiClientTypes.VerifyZkLoginSignatureOptions, ): Promise { diff --git a/packages/sui/src/graphql/generated/queries.ts b/packages/sui/src/graphql/generated/queries.ts index ebc1679f7..23152f43f 100644 --- a/packages/sui/src/graphql/generated/queries.ts +++ b/packages/sui/src/graphql/generated/queries.ts @@ -8,6 +8,31 @@ type Exact = { [K in keyof T]: T[K] }; export type Incremental = T | { [P in keyof T]?: P extends ' $fragmentName' | '__typename' ? T[P] : never }; import { OpenMoveTypeSignature } from '../types.js'; import { DocumentTypeDecoration } from '@graphql-typed-document-node/core'; +export type EventFilter = { + /** Limit to events that occured strictly after the given checkpoint. */ + afterCheckpoint?: number | null | undefined; + /** Limit to events in the given checkpoint. */ + atCheckpoint?: number | null | undefined; + /** Limit to event that occured strictly before the given checkpoint. */ + beforeCheckpoint?: number | null | undefined; + /** + * Events emitted by a particular module. An event is emitted by a particular module if some function in the module is called by a PTB and emits an event. + * + * Modules can be filtered by their package, or package::module. We currently do not support filtering by emitting module and event type at the same time so if both are provided in one filter, the query will error. + */ + module?: string | null | undefined; + /** Filter on events by transaction sender address. */ + sender?: string | null | undefined; + /** + * This field is used to specify the type of event emitted. + * + * Events can be filtered by their type's package, package::module, or their fully qualified type name. + * + * Generic types can be queried by either the generic type name, e.g. `0x2::coin::Coin`, or by the full type name, such as `0x2::coin::Coin<0x2::sui::SUI>`. + */ + type?: string | null | undefined; +}; + /** The execution status of this transaction: success or failure. */ export enum ExecutionStatus { /** The transaction could not be executed. */ @@ -116,6 +141,43 @@ export enum OwnerKind { Shared = 'SHARED' } +export type TransactionFilter = { + /** + * Limit to transactions that interacted with the given address. + * The address could be a sender, sponsor, or recipient of the transaction. + */ + affectedAddress?: string | null | undefined; + /** + * Limit to transactions that interacted with the given object. + * The object could have been created, read, modified, deleted, wrapped, or unwrapped by the transaction. + * Objects that were passed as a `Receiving` input are not considered to have been affected by a transaction unless they were actually received. + */ + affectedObject?: string | null | undefined; + /** Filter to transactions that occurred strictly after the given checkpoint. */ + afterCheckpoint?: number | null | undefined; + /** Filter to transactions in the given checkpoint. */ + atCheckpoint?: number | null | undefined; + /** Filter to transaction that occurred strictly before the given checkpoint. */ + beforeCheckpoint?: number | null | undefined; + /** Filter transactions by move function called. Calls can be filtered by the `package`, `package::module`, or the `package::module::name` of their function. */ + function?: string | null | undefined; + /** An input filter selecting for either system or programmable transactions. */ + kind?: TransactionKindInput | null | undefined; + /** Limit to transactions that were sent by the given address. */ + sentAddress?: string | null | undefined; +}; + +/** An input filter selecting for either system or programmable transactions. */ +export enum TransactionKindInput { + /** A user submitted transaction block. */ + ProgrammableTx = 'PROGRAMMABLE_TX', + /** + * A system transaction can be one of several types of transactions. + * See [unions/transaction-block-kind] for more details. + */ + SystemTx = 'SYSTEM_TX' +} + /** An enum that specifies the intent scope to be used to parse the bytes for signature verification. */ export enum ZkLoginIntentScope { /** Indicates that the bytes are to be parsed as a personal message. */ @@ -206,6 +268,17 @@ export type GetReferenceGasPriceQueryVariables = Exact<{ [key: string]: never; } export type GetReferenceGasPriceQuery = { epoch: { referenceGasPrice: string | null } | null }; +export type ListEventsQueryVariables = Exact<{ + filter?: EventFilter | null | undefined; + first?: number | null | undefined; + after?: string | null | undefined; + last?: number | null | undefined; + before?: string | null | undefined; +}>; + + +export type ListEventsQuery = { events: { pageInfo: { hasNextPage: boolean, hasPreviousPage: boolean, startCursor: string | null, endCursor: string | null }, nodes: Array<{ sequenceNumber: number, transaction: { digest: string, effects: { checkpoint: { sequenceNumber: number } | null } | null } | null, transactionModule: { name: string, package: { address: string } | null } | null, sender: { address: string } | null, contents: { bcs: string | null, json: unknown, type: { repr: string } | null } | null }> } | null }; + export type DefaultSuinsNameQueryVariables = Exact<{ address: string; }>; @@ -329,6 +402,23 @@ export type GetTransactionBlockQueryVariables = Exact<{ export type GetTransactionBlockQuery = { transaction: { digest: string, transactionJson?: unknown, transactionBcs?: string | null, signatures: Array<{ signatureBytes: string | null }>, effects: { status: ExecutionStatus | null, effectsBcs?: string | null, effectsJson?: unknown, balanceChangesJson?: unknown, executionError: { message: string, abortCode: string | null, identifier: string | null, constant: string | null, sourceLineNumber: number | null, instructionOffset: number | null, module: { name: string, package: { address: string } | null } | null, function: { name: string } | null } | null, epoch: { epochId: number } | null, objectChanges?: { nodes: Array<{ address: string, outputState: { asMoveObject: { contents: { type: { repr: string } | null } | null } | null } | null }> } | null, events?: { pageInfo: { hasNextPage: boolean }, nodes: Array<{ transactionModule: { name: string, package: { address: string } | null } | null, sender: { address: string } | null, contents: { bcs: string | null, json: unknown, type: { repr: string } | null } | null }> } | null } | null } | null }; +export type ListTransactionsQueryVariables = Exact<{ + filter?: TransactionFilter | null | undefined; + first?: number | null | undefined; + after?: string | null | undefined; + last?: number | null | undefined; + before?: string | null | undefined; + includeTransaction?: boolean | null | undefined; + includeEffects?: boolean | null | undefined; + includeEvents?: boolean | null | undefined; + includeBalanceChanges?: boolean | null | undefined; + includeObjectTypes?: boolean | null | undefined; + includeBcs?: boolean | null | undefined; +}>; + + +export type ListTransactionsQuery = { transactions: { pageInfo: { hasNextPage: boolean, hasPreviousPage: boolean, startCursor: string | null, endCursor: string | null }, nodes: Array<{ digest: string, transactionJson?: unknown, transactionBcs?: string | null, signatures: Array<{ signatureBytes: string | null }>, effects: { status: ExecutionStatus | null, effectsBcs?: string | null, effectsJson?: unknown, balanceChangesJson?: unknown, executionError: { message: string, abortCode: string | null, identifier: string | null, constant: string | null, sourceLineNumber: number | null, instructionOffset: number | null, module: { name: string, package: { address: string } | null } | null, function: { name: string } | null } | null, epoch: { epochId: number } | null, objectChanges?: { nodes: Array<{ address: string, outputState: { asMoveObject: { contents: { type: { repr: string } | null } | null } | null } | null }> } | null, events?: { pageInfo: { hasNextPage: boolean }, nodes: Array<{ transactionModule: { name: string, package: { address: string } | null } | null, sender: { address: string } | null, contents: { bcs: string | null, json: unknown, type: { repr: string } | null } | null }> } | null } | null }> } | null }; + export type Transaction_FieldsFragment = { digest: string, transactionJson?: unknown, transactionBcs?: string | null, signatures: Array<{ signatureBytes: string | null }>, effects: { status: ExecutionStatus | null, effectsBcs?: string | null, effectsJson?: unknown, balanceChangesJson?: unknown, executionError: { message: string, abortCode: string | null, identifier: string | null, constant: string | null, sourceLineNumber: number | null, instructionOffset: number | null, module: { name: string, package: { address: string } | null } | null, function: { name: string } | null } | null, epoch: { epochId: number } | null, objectChanges?: { nodes: Array<{ address: string, outputState: { asMoveObject: { contents: { type: { repr: string } | null } | null } | null } | null }> } | null, events?: { pageInfo: { hasNextPage: boolean }, nodes: Array<{ transactionModule: { name: string, package: { address: string } | null } | null, sender: { address: string } | null, contents: { bcs: string | null, json: unknown, type: { repr: string } | null } | null }> } | null } | null }; export type ResolveTransactionQueryVariables = Exact<{ @@ -760,6 +850,51 @@ export const GetReferenceGasPriceDocument = new TypedDocumentString(` } } `) as unknown as TypedDocumentString; +export const ListEventsDocument = new TypedDocumentString(` + query listEvents($filter: EventFilter, $first: Int, $after: String, $last: Int, $before: String) { + events( + filter: $filter + first: $first + after: $after + last: $last + before: $before + ) { + pageInfo { + hasNextPage + hasPreviousPage + startCursor + endCursor + } + nodes { + sequenceNumber + transaction { + digest + effects { + checkpoint { + sequenceNumber + } + } + } + transactionModule { + package { + address + } + name + } + sender { + address + } + contents { + type { + repr + } + bcs + json + } + } + } +} + `) as unknown as TypedDocumentString; export const DefaultSuinsNameDocument = new TypedDocumentString(` query defaultSuinsName($address: SuiAddress!) { address(address: $address) { @@ -1142,6 +1277,97 @@ export const GetTransactionBlockDocument = new TypedDocumentString(` } } }`) as unknown as TypedDocumentString; +export const ListTransactionsDocument = new TypedDocumentString(` + query listTransactions($filter: TransactionFilter, $first: Int, $after: String, $last: Int, $before: String, $includeTransaction: Boolean = false, $includeEffects: Boolean = false, $includeEvents: Boolean = false, $includeBalanceChanges: Boolean = false, $includeObjectTypes: Boolean = false, $includeBcs: Boolean = false) { + transactions( + filter: $filter + first: $first + after: $after + last: $last + before: $before + ) { + pageInfo { + hasNextPage + hasPreviousPage + startCursor + endCursor + } + nodes { + ...TRANSACTION_FIELDS + } + } +} + fragment TRANSACTION_FIELDS on Transaction { + digest + transactionJson @include(if: $includeTransaction) + transactionBcs @include(if: $includeBcs) + signatures { + signatureBytes + } + effects { + status + executionError { + message + abortCode + identifier + constant + sourceLineNumber + instructionOffset + module { + name + package { + address + } + } + function { + name + } + } + epoch { + epochId + } + effectsBcs @include(if: $includeEffects) + effectsJson @include(if: $includeObjectTypes) + objectChanges(first: 50) @include(if: $includeObjectTypes) { + nodes { + address + outputState { + asMoveObject { + contents { + type { + repr + } + } + } + } + } + } + balanceChangesJson @include(if: $includeBalanceChanges) + events(first: 50) @include(if: $includeEvents) { + pageInfo { + hasNextPage + } + nodes { + transactionModule { + package { + address + } + name + } + sender { + address + } + contents { + type { + repr + } + bcs + json + } + } + } + } +}`) as unknown as TypedDocumentString; export const ResolveTransactionDocument = new TypedDocumentString(` query resolveTransaction($transaction: JSON!, $doGasSelection: Boolean = true, $checksEnabled: Boolean = true) { simulateTransaction( diff --git a/packages/sui/src/graphql/generated/schema.graphql b/packages/sui/src/graphql/generated/schema.graphql index 603669b41..13e23960d 100644 --- a/packages/sui/src/graphql/generated/schema.graphql +++ b/packages/sui/src/graphql/generated/schema.graphql @@ -84,6 +84,16 @@ type Address implements Node & IAddressable { """ asObject: Object """ + How this address (interpreted as an object ID) was referenced by a specific transaction. + + Returns `null` if the object was not referenced, or was present only as a non-object marker variant of unchanged consensus input (e.g. cancelled, stream-ended, per-epoch). + + The `transactionDigest` argument may be omitted when the query is scoped under a transaction context (e.g. a parent `Transaction`, `TransactionEffects`, or `Event`); the field then resolves against the in-scope transaction. + + Passing an explicit `transactionDigest` other than the in-scope transaction in subscription context is not supported; for arbitrary transaction lookups, use the indexed Query API. + """ + asTransactionObject(transactionDigest: String): TransactionObject + """ Fetch the total balance for coins with marker type `coinType` (e.g. `0x2::sui::SUI`), owned by this address. Returns `None` when no checkpoint is set in scope (e.g. execution scope). @@ -617,6 +627,16 @@ type CoinMetadata implements IAddressable & IMoveObject & IObject { """ allowGlobalPause: Boolean """ + How this object was referenced by a specific transaction. + + Returns `null` if the object was not referenced, or was present only as a non-object marker variant of unchanged consensus input (e.g. cancelled, stream-ended, per-epoch). + + The `transactionDigest` argument may be omitted when the query is scoped under a transaction context (e.g. a parent `Transaction`, `TransactionEffects`, or `Event`); the field then resolves against the in-scope transaction. + + Passing an explicit `transactionDigest` other than the in-scope transaction in subscription context is not supported; for arbitrary transaction lookups, use the indexed Query API. + """ + asTransactionObject(transactionDigest: String): TransactionObject + """ Fetch the total balance for coins with marker type `coinType` (e.g. `0x2::sui::SUI`), owned by this address. If the address does not own any coins of that type, a balance of zero is returned. @@ -986,6 +1006,16 @@ type DynamicField implements Node & IAddressable & IMoveObject & IObject { """ addressAt(rootVersion: UInt53, checkpoint: UInt53): Address """ + How this object was referenced by a specific transaction. + + Returns `null` if the object was not referenced, or was present only as a non-object marker variant of unchanged consensus input (e.g. cancelled, stream-ended, per-epoch). + + The `transactionDigest` argument may be omitted when the query is scoped under a transaction context (e.g. a parent `Transaction`, `TransactionEffects`, or `Event`); the field then resolves against the in-scope transaction. + + Passing an explicit `transactionDigest` other than the in-scope transaction in subscription context is not supported; for arbitrary transaction lookups, use the indexed Query API. + """ + asTransactionObject(transactionDigest: String): TransactionObject + """ Fetch the total balance for coins with marker type `coinType` (e.g. `0x2::sui::SUI`), owned by this address. If the address does not own any coins of that type, a balance of zero is returned. @@ -1641,6 +1671,16 @@ interface IAddressable { """ addressAt(rootVersion: UInt53, checkpoint: UInt53): Address """ + How this addressable entity was referenced by a specific transaction. + + Returns `null` if the object was not referenced, or was present only as a non-object marker variant of unchanged consensus input (e.g. cancelled, stream-ended, per-epoch). + + The `transactionDigest` argument may be omitted when the query is scoped under a transaction context (e.g. a parent `Transaction`, `TransactionEffects`, or `Event`); the field then resolves against the in-scope transaction. + + Passing an explicit `transactionDigest` other than the in-scope transaction in subscription context is not supported; for arbitrary transaction lookups, use the indexed Query API. + """ + asTransactionObject(transactionDigest: String): TransactionObject + """ Fetch the total balance for coins with marker type `coinType` (e.g. `0x2::sui::SUI`), owned by this address. If the address does not own any coins of that type, a balance of zero is returned. @@ -2275,6 +2315,16 @@ type MoveObject implements Node & IAddressable & IMoveObject & IObject { """ asDynamicField: DynamicField """ + How this object was referenced by a specific transaction. + + Returns `null` if the object was not referenced, or was present only as a non-object marker variant of unchanged consensus input (e.g. cancelled, stream-ended, per-epoch). + + The `transactionDigest` argument may be omitted when the query is scoped under a transaction context (e.g. a parent `Transaction`, `TransactionEffects`, or `Event`); the field then resolves against the in-scope transaction. + + Passing an explicit `transactionDigest` other than the in-scope transaction in subscription context is not supported; for arbitrary transaction lookups, use the indexed Query API. + """ + asTransactionObject(transactionDigest: String): TransactionObject + """ Fetch the total balance for coins with marker type `coinType` (e.g. `0x2::sui::SUI`), owned by this address. If the address does not own any coins of that type, a balance of zero is returned. @@ -2434,6 +2484,16 @@ type MovePackage implements Node & IAddressable & IObject { """ addressAt(rootVersion: UInt53, checkpoint: UInt53): Address """ + How this object was referenced by a specific transaction. + + Returns `null` if the object was not referenced, or was present only as a non-object marker variant of unchanged consensus input (e.g. cancelled, stream-ended, per-epoch). + + The `transactionDigest` argument may be omitted when the query is scoped under a transaction context (e.g. a parent `Transaction`, `TransactionEffects`, or `Event`); the field then resolves against the in-scope transaction. + + Passing an explicit `transactionDigest` other than the in-scope transaction in subscription context is not supported; for arbitrary transaction lookups, use the indexed Query API. + """ + asTransactionObject(transactionDigest: String): TransactionObject + """ Fetch the total balance for coins with marker type `coinType` (e.g. `0x2::sui::SUI`), owned by this address. If the address does not own any coins of that type, a balance of zero is returned. @@ -2955,6 +3015,16 @@ type Object implements Node & IAddressable & IObject { """ asMovePackage: MovePackage """ + How this object was referenced by a specific transaction. + + Returns `null` if the object was not referenced, or was present only as a non-object marker variant of unchanged consensus input (e.g. cancelled, stream-ended, per-epoch). + + The `transactionDigest` argument may be omitted when the query is scoped under a transaction context (e.g. a parent `Transaction`, `TransactionEffects`, or `Event`); the field then resolves against the in-scope transaction. + + Passing an explicit `transactionDigest` other than the in-scope transaction in subscription context is not supported; for arbitrary transaction lookups, use the indexed Query API. + """ + asTransactionObject(transactionDigest: String): TransactionObject + """ Fetch the total balance for coins with marker type `coinType` (e.g. `0x2::sui::SUI`), owned by this address. If the address does not own any coins of that type, a balance of zero is returned. @@ -4334,6 +4404,11 @@ enum TransactionKindInput { PROGRAMMABLE_TX } +""" +An object as it appears in a specific transaction. The variant discriminates whether the object was changed by the transaction or read as an unchanged consensus (shared) input. +""" +union TransactionObject = ObjectChange | ConsensusObjectRead + """ Transfers `inputs` to `address`. All inputs must have the `store` ability (allows public transfer) and must not be previously immutable or shared. """ diff --git a/packages/sui/src/graphql/generated/tada-env.ts b/packages/sui/src/graphql/generated/tada-env.ts index 6a3489a3a..bf239e326 100644 --- a/packages/sui/src/graphql/generated/tada-env.ts +++ b/packages/sui/src/graphql/generated/tada-env.ts @@ -253,6 +253,23 @@ const introspection = { "args": [], "isDeprecated": false }, + { + "name": "asTransactionObject", + "type": { + "kind": "UNION", + "name": "TransactionObject" + }, + "args": [ + { + "name": "transactionDigest", + "type": { + "kind": "SCALAR", + "name": "String" + } + } + ], + "isDeprecated": false + }, { "name": "balance", "type": { @@ -1739,6 +1756,23 @@ const introspection = { "args": [], "isDeprecated": false }, + { + "name": "asTransactionObject", + "type": { + "kind": "UNION", + "name": "TransactionObject" + }, + "args": [ + { + "name": "transactionDigest", + "type": { + "kind": "SCALAR", + "name": "String" + } + } + ], + "isDeprecated": false + }, { "name": "balance", "type": { @@ -2805,6 +2839,23 @@ const introspection = { ], "isDeprecated": false }, + { + "name": "asTransactionObject", + "type": { + "kind": "UNION", + "name": "TransactionObject" + }, + "args": [ + { + "name": "transactionDigest", + "type": { + "kind": "SCALAR", + "name": "String" + } + } + ], + "isDeprecated": false + }, { "name": "balance", "type": { @@ -4741,6 +4792,23 @@ const introspection = { ], "isDeprecated": false }, + { + "name": "asTransactionObject", + "type": { + "kind": "UNION", + "name": "TransactionObject" + }, + "args": [ + { + "name": "transactionDigest", + "type": { + "kind": "SCALAR", + "name": "String" + } + } + ], + "isDeprecated": false + }, { "name": "balance", "type": { @@ -6808,6 +6876,23 @@ const introspection = { "args": [], "isDeprecated": false }, + { + "name": "asTransactionObject", + "type": { + "kind": "UNION", + "name": "TransactionObject" + }, + "args": [ + { + "name": "transactionDigest", + "type": { + "kind": "SCALAR", + "name": "String" + } + } + ], + "isDeprecated": false + }, { "name": "balance", "type": { @@ -7499,6 +7584,23 @@ const introspection = { ], "isDeprecated": false }, + { + "name": "asTransactionObject", + "type": { + "kind": "UNION", + "name": "TransactionObject" + }, + "args": [ + { + "name": "transactionDigest", + "type": { + "kind": "SCALAR", + "name": "String" + } + } + ], + "isDeprecated": false + }, { "name": "balance", "type": { @@ -9041,6 +9143,23 @@ const introspection = { "args": [], "isDeprecated": false }, + { + "name": "asTransactionObject", + "type": { + "kind": "UNION", + "name": "TransactionObject" + }, + "args": [ + { + "name": "transactionDigest", + "type": { + "kind": "SCALAR", + "name": "String" + } + } + ], + "isDeprecated": false + }, { "name": "balance", "type": { @@ -13053,6 +13172,20 @@ const introspection = { } ] }, + { + "kind": "UNION", + "name": "TransactionObject", + "possibleTypes": [ + { + "kind": "OBJECT", + "name": "ConsensusObjectRead" + }, + { + "kind": "OBJECT", + "name": "ObjectChange" + } + ] + }, { "kind": "OBJECT", "name": "TransferObjectsCommand", diff --git a/packages/sui/src/graphql/queries/listEvents.graphql b/packages/sui/src/graphql/queries/listEvents.graphql new file mode 100644 index 000000000..520c35931 --- /dev/null +++ b/packages/sui/src/graphql/queries/listEvents.graphql @@ -0,0 +1,40 @@ +# Copyright (c) Mysten Labs, Inc. +# SPDX-License-Identifier: Apache-2.0 + +query listEvents($filter: EventFilter, $first: Int, $after: String, $last: Int, $before: String) { + events(filter: $filter, first: $first, after: $after, last: $last, before: $before) { + pageInfo { + hasNextPage + hasPreviousPage + startCursor + endCursor + } + nodes { + sequenceNumber + transaction { + digest + effects { + checkpoint { + sequenceNumber + } + } + } + transactionModule { + package { + address + } + name + } + sender { + address + } + contents { + type { + repr + } + bcs + json + } + } + } +} diff --git a/packages/sui/src/graphql/queries/transactions.graphql b/packages/sui/src/graphql/queries/transactions.graphql index 76bb67332..fd5dd745c 100644 --- a/packages/sui/src/graphql/queries/transactions.graphql +++ b/packages/sui/src/graphql/queries/transactions.graphql @@ -71,6 +71,32 @@ query getTransactionBlock( } } +query listTransactions( + $filter: TransactionFilter + $first: Int + $after: String + $last: Int + $before: String + $includeTransaction: Boolean = false + $includeEffects: Boolean = false + $includeEvents: Boolean = false + $includeBalanceChanges: Boolean = false + $includeObjectTypes: Boolean = false + $includeBcs: Boolean = false +) { + transactions(filter: $filter, first: $first, after: $after, last: $last, before: $before) { + pageInfo { + hasNextPage + hasPreviousPage + startCursor + endCursor + } + nodes { + ...TRANSACTION_FIELDS + } + } +} + fragment TRANSACTION_FIELDS on Transaction { digest transactionJson @include(if: $includeTransaction) diff --git a/packages/sui/src/grpc/client.ts b/packages/sui/src/grpc/client.ts index f85dd63af..683746653 100644 --- a/packages/sui/src/grpc/client.ts +++ b/packages/sui/src/grpc/client.ts @@ -221,6 +221,16 @@ export class SuiGrpcClient extends BaseClient implements SuiClientTypes.Transpor return this.core.getDynamicField(input); } + listTransactions( + input: SuiClientTypes.ListTransactionsOptions, + ): Promise> { + return this.core.listTransactions(input); + } + + listEvents(input: SuiClientTypes.ListEventsOptions): Promise { + return this.core.listEvents(input); + } + getMoveFunction( input: SuiClientTypes.GetMoveFunctionOptions, ): Promise { diff --git a/packages/sui/src/grpc/core.ts b/packages/sui/src/grpc/core.ts index 94bdf1988..3dbfb18f1 100644 --- a/packages/sui/src/grpc/core.ts +++ b/packages/sui/src/grpc/core.ts @@ -45,6 +45,16 @@ import { } from '../client/transaction-resolver.js'; import { Value } from './proto/google/protobuf/struct.js'; import { SimulateTransactionRequest_TransactionChecks } from './proto/sui/rpc/v2/transaction_execution_service.js'; +import type { QueryEnd, QueryOptions } from './proto/sui/rpc/v2/query_options.js'; +import { Ordering, QueryEndReason } from './proto/sui/rpc/v2/query_options.js'; +import type { ResolvedPagination } from '../client/query-filters.js'; +import { + resolveEventFilter, + resolvePagination, + resolveTransactionFilter, + validateTransactionQuery, +} from '../client/query-filters.js'; +import { toGrpcEventFilter, toGrpcTransactionFilter } from './filters.js'; export interface GrpcCoreClientOptions extends CoreClientOptions { client: SuiGrpcClient; @@ -305,31 +315,7 @@ export class GrpcCoreClient extends CoreClient { async getTransaction( options: SuiClientTypes.GetTransactionOptions, ): Promise> { - const paths = ['digest', 'transaction.digest', 'signatures', 'effects.status']; - if (options.include?.transaction) { - paths.push( - 'transaction.sender', - 'transaction.gas_payment', - 'transaction.expiration', - 'transaction.kind', - ); - } - if (options.include?.bcs) { - paths.push('transaction.bcs'); - } - if (options.include?.balanceChanges) { - paths.push('balance_changes'); - } - if (options.include?.effects) { - paths.push('effects'); - } - if (options.include?.events) { - paths.push('events'); - } - if (options.include?.objectTypes) { - paths.push('effects.changed_objects.object_type'); - paths.push('effects.changed_objects.object_id'); - } + const paths = transactionReadMaskPaths(options.include); const { response } = await this.#client.ledgerService.getTransaction({ digest: options.digest, @@ -347,31 +333,7 @@ export class GrpcCoreClient extends CoreClient { async executeTransaction( options: SuiClientTypes.ExecuteTransactionOptions, ): Promise> { - const paths = ['digest', 'transaction.digest', 'signatures', 'effects.status']; - if (options.include?.transaction) { - paths.push( - 'transaction.sender', - 'transaction.gas_payment', - 'transaction.expiration', - 'transaction.kind', - ); - } - if (options.include?.bcs) { - paths.push('transaction.bcs'); - } - if (options.include?.balanceChanges) { - paths.push('balance_changes'); - } - if (options.include?.effects) { - paths.push('effects'); - } - if (options.include?.events) { - paths.push('events'); - } - if (options.include?.objectTypes) { - paths.push('effects.changed_objects.object_type'); - paths.push('effects.changed_objects.object_id'); - } + const paths = transactionReadMaskPaths(options.include); const { response } = await this.#client.transactionExecutionService.executeTransaction({ transaction: { @@ -397,37 +359,8 @@ export class GrpcCoreClient extends CoreClient { async simulateTransaction( options: SuiClientTypes.SimulateTransactionOptions, ): Promise> { - const paths = [ - 'transaction.digest', - 'transaction.transaction.digest', - 'transaction.signatures', - 'transaction.effects.status', - ]; - if (options.include?.transaction) { - paths.push( - 'transaction.transaction.sender', - 'transaction.transaction.gas_payment', - 'transaction.transaction.expiration', - 'transaction.transaction.kind', - ); - } - if (options.include?.bcs) { - paths.push('transaction.transaction.bcs'); - } - if (options.include?.balanceChanges) { - paths.push('transaction.balance_changes'); - } - if (options.include?.effects) { - paths.push('transaction.effects'); - } - if (options.include?.events) { - paths.push('transaction.events'); - } - if (options.include?.objectTypes) { - // Use effects.changed_objects to match JSON-RPC behavior (which uses objectChanges) - paths.push('transaction.effects.changed_objects.object_type'); - paths.push('transaction.effects.changed_objects.object_id'); - } + // The simulated transaction is nested one level deeper in the response + const paths = transactionReadMaskPaths(options.include, 'transaction.'); if (options.include?.commandResults) { paths.push('command_outputs'); } @@ -612,6 +545,127 @@ export class GrpcCoreClient extends CoreClient { return this.#client.listDynamicFields(options); } + async listTransactions( + options: SuiClientTypes.ListTransactionsOptions, + ): Promise> { + const paths = transactionReadMaskPaths(options.include); + + const filter = options.filter + ? await resolveTransactionFilter(this.mvr, options.filter) + : undefined; + const pagination = resolvePagination(options); + validateTransactionQuery(filter, pagination); + + const call = this.#client.ledgerService.listTransactions( + { + readMask: { paths }, + filter: filter && toGrpcTransactionFilter(filter), + options: toGrpcQueryOptions(pagination), + }, + { abort: options.signal }, + ); + + const transactions: SuiClientTypes.TransactionResult[] = []; + let startCursor: string | null = null; + let endCursor: string | null = null; + let frontier: string | null = null; + let end: QueryEnd | undefined; + + for await (const frame of call.responses) { + if (frame.watermark?.cursor) { + frontier = toBase64(frame.watermark.cursor); + } + if (frame.transaction) { + startCursor ??= frontier; + endCursor = frontier; + transactions.push(parseTransaction(frame.transaction, options.include)); + } + if (frame.end) { + end = frame.end; + } + } + + const hasNextPage = queryHasNextPage(end); + + return { + transactions, + hasNextPage, + startCursor, + // Item-less scans that stopped early continue from the scan frontier, + // while terminal empty pages have no positions to continue from + endCursor: endCursor ?? (hasNextPage ? frontier : null), + }; + } + + async listEvents( + options: SuiClientTypes.ListEventsOptions, + ): Promise { + const call = this.#client.ledgerService.listEvents( + { + readMask: { + paths: [ + 'package_id', + 'module', + 'sender', + 'event_type', + 'contents', + 'json', + 'checkpoint', + 'transaction_digest', + 'event_index', + ], + }, + filter: options.filter + ? toGrpcEventFilter(await resolveEventFilter(this.mvr, options.filter)) + : undefined, + options: toGrpcQueryOptions(resolvePagination(options)), + }, + { abort: options.signal }, + ); + + const events: SuiClientTypes.EventEntry[] = []; + let startCursor: string | null = null; + let endCursor: string | null = null; + let frontier: string | null = null; + let end: QueryEnd | undefined; + + for await (const frame of call.responses) { + if (frame.watermark?.cursor) { + frontier = toBase64(frame.watermark.cursor); + } + if (frame.event) { + startCursor ??= frontier; + endCursor = frontier; + const event = frame.event; + events.push({ + packageId: normalizeSuiAddress(event.packageId!), + module: event.module!, + sender: normalizeSuiAddress(event.sender!), + eventType: normalizeStructTag(event.eventType!), + bcs: event.contents?.value ?? new Uint8Array(), + json: event.json ? (Value.toJson(event.json) as Record) : null, + checkpoint: event.checkpoint?.toString() ?? null, + transactionDigest: event.transactionDigest!, + eventIndex: event.eventIndex!, + }); + } + if (frame.end) { + end = frame.end; + } + } + + const hasNextPage = queryHasNextPage(end); + + return { + events, + hasNextPage, + startCursor, + // Item-less scans that stopped early continue from the scan frontier, + // while terminal empty pages have no positions to continue from + endCursor: endCursor ?? (hasNextPage ? frontier : null), + }; + } + async verifyZkLoginSignature( options: SuiClientTypes.VerifyZkLoginSignatureOptions, ): Promise { @@ -805,6 +859,54 @@ export class GrpcCoreClient extends CoreClient { } } +function toGrpcQueryOptions(pagination: ResolvedPagination): QueryOptions { + return { + limit: pagination.limit, + ordering: pagination.descending ? Ordering.DESCENDING : Ordering.ASCENDING, + after: pagination.after ? fromBase64(pagination.after) : undefined, + before: pagination.before ? fromBase64(pagination.before) : undefined, + }; +} + +function queryHasNextPage(end: QueryEnd | undefined): boolean { + return end?.reason === QueryEndReason.ITEM_LIMIT || end?.reason === QueryEndReason.SCAN_LIMIT; +} + +function transactionReadMaskPaths( + include: SuiClientTypes.TransactionInclude | undefined, + prefix = '', +): string[] { + const paths = ['digest', 'transaction.digest', 'signatures', 'effects.status']; + + if (include?.transaction) { + paths.push( + 'transaction.sender', + 'transaction.gas_payment', + 'transaction.expiration', + 'transaction.kind', + ); + } + if (include?.bcs) { + paths.push('transaction.bcs'); + } + if (include?.balanceChanges) { + paths.push('balance_changes'); + } + if (include?.effects) { + paths.push('effects'); + } + if (include?.events) { + paths.push('events'); + } + if (include?.objectTypes) { + // Use effects.changed_objects to match JSON-RPC behavior (which uses objectChanges) + paths.push('effects.changed_objects.object_type'); + paths.push('effects.changed_objects.object_id'); + } + + return prefix ? paths.map((path) => prefix + path) : paths; +} + function mapDisplayProto( include: boolean | undefined, display: { output?: Value; errors?: Value } | undefined, diff --git a/packages/sui/src/grpc/filters.ts b/packages/sui/src/grpc/filters.ts new file mode 100644 index 000000000..e51938935 --- /dev/null +++ b/packages/sui/src/grpc/filters.ts @@ -0,0 +1,68 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +import type { ResolvedEventFilter, ResolvedTransactionFilter } from '../client/query-filters.js'; +import type { EventFilter, TransactionFilter } from './proto/sui/rpc/v2/filter.js'; + +export function toGrpcTransactionFilter(filter: ResolvedTransactionFilter): TransactionFilter { + return { + terms: [ + { + literals: [ + filter.$kind === 'sender' + ? { + negated: false, + predicate: { oneofKind: 'sender', sender: { address: filter.sender } }, + } + : { + negated: false, + predicate: { + oneofKind: 'moveCall', + moveCall: { + function: [filter.package, filter.module, filter.function] + .filter(Boolean) + .join('::'), + }, + }, + }, + ], + }, + ], + }; +} + +export function toGrpcEventFilter(filter: ResolvedEventFilter): EventFilter { + return { + terms: [ + { + literals: [ + filter.$kind === 'sender' + ? { + negated: false, + predicate: { oneofKind: 'sender', sender: { address: filter.sender } }, + } + : filter.$kind === 'emitModule' + ? { + negated: false, + predicate: { + oneofKind: 'emitModule', + emitModule: { module: `${filter.package}::${filter.module}` }, + }, + } + : { + negated: false, + predicate: { + oneofKind: 'eventType', + eventType: { + eventType: + filter.$kind === 'eventTypeModule' + ? `${filter.package}::${filter.module}` + : filter.eventType, + }, + }, + }, + ], + }, + ], + }; +} diff --git a/packages/sui/src/grpc/proto/sui/rpc/v2/event.ts b/packages/sui/src/grpc/proto/sui/rpc/v2/event.ts index 473a7a129..2e0e17843 100644 --- a/packages/sui/src/grpc/proto/sui/rpc/v2/event.ts +++ b/packages/sui/src/grpc/proto/sui/rpc/v2/event.ts @@ -81,6 +81,37 @@ export interface Event { * @generated from protobuf field: optional google.protobuf.Value json = 6; */ json?: Value; + /** + * The sequence number of the checkpoint that includes the transaction + * that emitted this event. Populated when the event is delivered on its + * own (for example via `LedgerService.ListEvents`); left unset when the + * event is carried inside its transaction's `events` list, where the + * enclosing `ExecutedTransaction` already provides this context. + * + * @generated from protobuf field: optional uint64 checkpoint = 7; + */ + checkpoint?: bigint; + /** + * The digest of the transaction that emitted this event. + * + * @generated from protobuf field: optional string transaction_digest = 8; + */ + transactionDigest?: string; + /** + * Zero-based position of the emitting transaction within its containing + * checkpoint. For clients verifying authenticated event streams this + * index is part of the BCS-encoded `EventCommitment` leaf used to + * construct the per-checkpoint merkle root. + * + * @generated from protobuf field: optional uint64 transaction_index = 9; + */ + transactionIndex?: bigint; + /** + * Zero-based index of this event within its transaction's event list. + * + * @generated from protobuf field: optional uint32 event_index = 10; + */ + eventIndex?: number; } // @generated message type with reflection information, may provide speed optimized methods class TransactionEvents$Type extends MessageType { @@ -106,6 +137,24 @@ class Event$Type extends MessageType { { no: 4, name: 'event_type', kind: 'scalar', opt: true, T: 9 /*ScalarType.STRING*/ }, { no: 5, name: 'contents', kind: 'message', T: () => Bcs }, { no: 6, name: 'json', kind: 'message', T: () => Value }, + { + no: 7, + name: 'checkpoint', + kind: 'scalar', + opt: true, + T: 4 /*ScalarType.UINT64*/, + L: 0 /*LongType.BIGINT*/, + }, + { no: 8, name: 'transaction_digest', kind: 'scalar', opt: true, T: 9 /*ScalarType.STRING*/ }, + { + no: 9, + name: 'transaction_index', + kind: 'scalar', + opt: true, + T: 4 /*ScalarType.UINT64*/, + L: 0 /*LongType.BIGINT*/, + }, + { no: 10, name: 'event_index', kind: 'scalar', opt: true, T: 13 /*ScalarType.UINT32*/ }, ]); } } diff --git a/packages/sui/src/grpc/proto/sui/rpc/v2/executed_transaction.ts b/packages/sui/src/grpc/proto/sui/rpc/v2/executed_transaction.ts index 5a3c117fa..86e7471a0 100644 --- a/packages/sui/src/grpc/proto/sui/rpc/v2/executed_transaction.ts +++ b/packages/sui/src/grpc/proto/sui/rpc/v2/executed_transaction.ts @@ -80,6 +80,13 @@ export interface ExecutedTransaction { * @generated from protobuf field: optional sui.rpc.v2.ObjectSet objects = 9; */ objects?: ObjectSet; + /** + * Zero-based position of this transaction within the checkpoint that + * includes it. + * + * @generated from protobuf field: optional uint64 transaction_index = 10; + */ + transactionIndex?: bigint; } // @generated message type with reflection information, may provide speed optimized methods class ExecutedTransaction$Type extends MessageType { @@ -113,6 +120,14 @@ class ExecutedTransaction$Type extends MessageType { T: () => BalanceChange, }, { no: 9, name: 'objects', kind: 'message', T: () => ObjectSet }, + { + no: 10, + name: 'transaction_index', + kind: 'scalar', + opt: true, + T: 4 /*ScalarType.UINT64*/, + L: 0 /*LongType.BIGINT*/, + }, ]); } } diff --git a/packages/sui/src/grpc/proto/sui/rpc/v2/filter.ts b/packages/sui/src/grpc/proto/sui/rpc/v2/filter.ts new file mode 100644 index 000000000..3f74eadf6 --- /dev/null +++ b/packages/sui/src/grpc/proto/sui/rpc/v2/filter.ts @@ -0,0 +1,589 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +// @generated by protobuf-ts 2.9.6 with parameter force_server_none,optimize_code_size,ts_nocheck +// @generated from protobuf file "sui/rpc/v2/filter.proto" (package "sui.rpc.v2", syntax proto3) +// tslint:disable +// @ts-nocheck +// +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 +// +import { MessageType } from '@protobuf-ts/runtime'; +/** + * DNF filter for transactions: any term may match, and each term is an AND + * of signed literals. + * An absent filter matches everything. A present filter must have at least one + * term. + * + * @generated from protobuf message sui.rpc.v2.TransactionFilter + */ +export interface TransactionFilter { + /** + * Terms are ORed together. + * + * @generated from protobuf field: repeated sui.rpc.v2.TransactionTerm terms = 1; + */ + terms: TransactionTerm[]; +} +/** + * One conjunction in a transaction DNF filter. + * + * @generated from protobuf message sui.rpc.v2.TransactionTerm + */ +export interface TransactionTerm { + /** + * Literals are ANDed together. + * + * @generated from protobuf field: repeated sui.rpc.v2.TransactionLiteral literals = 1; + */ + literals: TransactionLiteral[]; +} +/** + * One signed transaction predicate literal: a predicate, optionally negated. + * + * @generated from protobuf message sui.rpc.v2.TransactionLiteral + */ +export interface TransactionLiteral { + /** + * When true, the literal matches transactions that the predicate does *not* + * match. + * + * @generated from protobuf field: bool negated = 1; + */ + negated: boolean; + /** + * @generated from protobuf oneof: predicate + */ + predicate: + | { + oneofKind: 'sender'; + /** + * Match transactions sent by the specified address. + * + * @generated from protobuf field: sui.rpc.v2.SenderFilter sender = 2; + */ + sender: SenderFilter; + } + | { + oneofKind: 'affectedAddress'; + /** + * Match transactions where the specified address's state moved as a side + * effect: it owns an object after the txn, owned an object before the + * txn that was mutated/transferred away/deleted/wrapped, or its + * address-balance changed via an accumulator event. + * + * @generated from protobuf field: sui.rpc.v2.AffectedAddressFilter affected_address = 3; + */ + affectedAddress: AffectedAddressFilter; + } + | { + oneofKind: 'affectedObject'; + /** + * Match transactions whose effects include a change for the specified + * object. + * + * @generated from protobuf field: sui.rpc.v2.AffectedObjectFilter affected_object = 4; + */ + affectedObject: AffectedObjectFilter; + } + | { + oneofKind: 'moveCall'; + /** + * Match transactions that made a Move call matching the specified filter. + * + * @generated from protobuf field: sui.rpc.v2.MoveCallFilter move_call = 5; + */ + moveCall: MoveCallFilter; + } + | { + oneofKind: 'emitModule'; + /** + * Match transactions that emitted an event whose package/module fields + * match the specified filter. + * + * @generated from protobuf field: sui.rpc.v2.EmitModuleFilter emit_module = 6; + */ + emitModule: EmitModuleFilter; + } + | { + oneofKind: 'eventType'; + /** + * Match transactions that emitted an event with a type matching the + * specified filter. + * + * @generated from protobuf field: sui.rpc.v2.EventTypeFilter event_type = 7; + */ + eventType: EventTypeFilter; + } + | { + oneofKind: 'eventStreamHead'; + /** + * Match transactions that wrote to the specified authenticated event + * stream head. + * + * @generated from protobuf field: sui.rpc.v2.EventStreamHeadFilter event_stream_head = 8; + */ + eventStreamHead: EventStreamHeadFilter; + } + | { + oneofKind: 'packageWrite'; + /** + * Match transactions that wrote a Move package — a first publish or an + * upgrade, of any package. + * + * @generated from protobuf field: sui.rpc.v2.PackageWriteFilter package_write = 9; + */ + packageWrite: PackageWriteFilter; + } + | { + oneofKind: undefined; + }; +} +/** + * DNF filter for events: any term may match, and each term is an AND of + * signed literals. Sender predicates match all events from matching + * transactions; emit-module, event-type, and event-stream-head predicates match + * individual event-space dimensions. An absent filter matches everything. A + * present filter must have at least one term. + * + * @generated from protobuf message sui.rpc.v2.EventFilter + */ +export interface EventFilter { + /** + * Terms are ORed together. + * + * @generated from protobuf field: repeated sui.rpc.v2.EventTerm terms = 1; + */ + terms: EventTerm[]; +} +/** + * One conjunction in an event DNF filter. + * + * @generated from protobuf message sui.rpc.v2.EventTerm + */ +export interface EventTerm { + /** + * Literals are ANDed together. + * + * @generated from protobuf field: repeated sui.rpc.v2.EventLiteral literals = 1; + */ + literals: EventLiteral[]; +} +/** + * One signed event predicate literal: a predicate, optionally negated. + * + * @generated from protobuf message sui.rpc.v2.EventLiteral + */ +export interface EventLiteral { + /** + * When true, the literal matches events that the predicate does *not* match. + * + * @generated from protobuf field: bool negated = 1; + */ + negated: boolean; + /** + * @generated from protobuf oneof: predicate + */ + predicate: + | { + oneofKind: 'sender'; + /** + * Match events from transactions sent by the specified address. + * + * @generated from protobuf field: sui.rpc.v2.SenderFilter sender = 2; + */ + sender: SenderFilter; + } + | { + oneofKind: 'emitModule'; + /** + * Match events whose package/module fields match the specified filter. + * + * @generated from protobuf field: sui.rpc.v2.EmitModuleFilter emit_module = 3; + */ + emitModule: EmitModuleFilter; + } + | { + oneofKind: 'eventType'; + /** + * Match events whose type matches the specified filter. + * + * @generated from protobuf field: sui.rpc.v2.EventTypeFilter event_type = 4; + */ + eventType: EventTypeFilter; + } + | { + oneofKind: 'eventStreamHead'; + /** + * Match events committed to the specified authenticated event stream head. + * + * @generated from protobuf field: sui.rpc.v2.EventStreamHeadFilter event_stream_head = 5; + */ + eventStreamHead: EventStreamHeadFilter; + } + | { + oneofKind: undefined; + }; +} +/** + * Match by transaction sender address. + * + * @generated from protobuf message sui.rpc.v2.SenderFilter + */ +export interface SenderFilter { + /** + * The sender address (hex-encoded). + * + * @generated from protobuf field: optional string address = 1; + */ + address?: string; +} +/** + * Match by any address whose state moved as a side effect of the + * transaction: object ownership changes (in either direction), prior + * owners of removed/wrapped objects, and address-balance changes via + * accumulator events. + * + * @generated from protobuf message sui.rpc.v2.AffectedAddressFilter + */ +export interface AffectedAddressFilter { + /** + * The affected address (hex-encoded). + * + * @generated from protobuf field: optional string address = 1; + */ + address?: string; +} +/** + * Match by changed object ID. + * + * @generated from protobuf message sui.rpc.v2.AffectedObjectFilter + */ +export interface AffectedObjectFilter { + /** + * The changed object ID (hex-encoded). + * + * @generated from protobuf field: optional string object_id = 1; + */ + objectId?: string; +} +/** + * Match by Move function call, specified as a `::`-delimited Move path. + * + * Specificity levels: + * "0xpkg" -> matches any call in the package + * "0xpkg::module" -> matches any call in the module + * "0xpkg::module::function" -> matches calls to the exact function + * + * @generated from protobuf message sui.rpc.v2.MoveCallFilter + */ +export interface MoveCallFilter { + /** + * Required. Move path of the form `package[::module[::function]]`. + * + * @generated from protobuf field: optional string function = 1; + */ + function?: string; +} +/** + * Match by an event's package/module fields, specified as a `::`-delimited + * Move path. These identify the top-level Move call that triggered the event. + * + * Specificity levels: + * "0xpkg" -> matches events with this package_id + * "0xpkg::module" -> matches events with this package_id and module + * + * @generated from protobuf message sui.rpc.v2.EmitModuleFilter + */ +export interface EmitModuleFilter { + /** + * Required. Move path of the form `package[::module]`. + * + * @generated from protobuf field: optional string module = 1; + */ + module?: string; +} +/** + * Match by event struct type, specified as a Move type string. + * + * Specificity levels: + * "0xaddr" -> matches events whose type is defined at this address + * "0xaddr::module" -> matches events whose type is in this module + * "0xaddr::module::Name" -> matches events with this type name (any instantiation) + * "0xaddr::module::Name" -> matches events with this exact generic instantiation + * + * @generated from protobuf message sui.rpc.v2.EventTypeFilter + */ +export interface EventTypeFilter { + /** + * Required. Move type string of the form + * `address[::module[::Name[]]]`. + * + * @generated from protobuf field: optional string event_type = 1; + */ + eventType?: string; +} +/** + * Match by authenticated event stream head. + * + * @generated from protobuf message sui.rpc.v2.EventStreamHeadFilter + */ +export interface EventStreamHeadFilter { + /** + * The stream id address (hex-encoded). + * + * @generated from protobuf field: optional string stream_id = 1; + */ + streamId?: string; +} +/** + * Match transactions that wrote a Move package. + * + * @generated from protobuf message sui.rpc.v2.PackageWriteFilter + */ +export interface PackageWriteFilter {} +// @generated message type with reflection information, may provide speed optimized methods +class TransactionFilter$Type extends MessageType { + constructor() { + super('sui.rpc.v2.TransactionFilter', [ + { + no: 1, + name: 'terms', + kind: 'message', + repeat: 2 /*RepeatType.UNPACKED*/, + T: () => TransactionTerm, + }, + ]); + } +} +/** + * @generated MessageType for protobuf message sui.rpc.v2.TransactionFilter + */ +export const TransactionFilter = new TransactionFilter$Type(); +// @generated message type with reflection information, may provide speed optimized methods +class TransactionTerm$Type extends MessageType { + constructor() { + super('sui.rpc.v2.TransactionTerm', [ + { + no: 1, + name: 'literals', + kind: 'message', + repeat: 2 /*RepeatType.UNPACKED*/, + T: () => TransactionLiteral, + }, + ]); + } +} +/** + * @generated MessageType for protobuf message sui.rpc.v2.TransactionTerm + */ +export const TransactionTerm = new TransactionTerm$Type(); +// @generated message type with reflection information, may provide speed optimized methods +class TransactionLiteral$Type extends MessageType { + constructor() { + super('sui.rpc.v2.TransactionLiteral', [ + { no: 1, name: 'negated', kind: 'scalar', T: 8 /*ScalarType.BOOL*/ }, + { no: 2, name: 'sender', kind: 'message', oneof: 'predicate', T: () => SenderFilter }, + { + no: 3, + name: 'affected_address', + kind: 'message', + oneof: 'predicate', + T: () => AffectedAddressFilter, + }, + { + no: 4, + name: 'affected_object', + kind: 'message', + oneof: 'predicate', + T: () => AffectedObjectFilter, + }, + { no: 5, name: 'move_call', kind: 'message', oneof: 'predicate', T: () => MoveCallFilter }, + { + no: 6, + name: 'emit_module', + kind: 'message', + oneof: 'predicate', + T: () => EmitModuleFilter, + }, + { no: 7, name: 'event_type', kind: 'message', oneof: 'predicate', T: () => EventTypeFilter }, + { + no: 8, + name: 'event_stream_head', + kind: 'message', + oneof: 'predicate', + T: () => EventStreamHeadFilter, + }, + { + no: 9, + name: 'package_write', + kind: 'message', + oneof: 'predicate', + T: () => PackageWriteFilter, + }, + ]); + } +} +/** + * @generated MessageType for protobuf message sui.rpc.v2.TransactionLiteral + */ +export const TransactionLiteral = new TransactionLiteral$Type(); +// @generated message type with reflection information, may provide speed optimized methods +class EventFilter$Type extends MessageType { + constructor() { + super('sui.rpc.v2.EventFilter', [ + { + no: 1, + name: 'terms', + kind: 'message', + repeat: 2 /*RepeatType.UNPACKED*/, + T: () => EventTerm, + }, + ]); + } +} +/** + * @generated MessageType for protobuf message sui.rpc.v2.EventFilter + */ +export const EventFilter = new EventFilter$Type(); +// @generated message type with reflection information, may provide speed optimized methods +class EventTerm$Type extends MessageType { + constructor() { + super('sui.rpc.v2.EventTerm', [ + { + no: 1, + name: 'literals', + kind: 'message', + repeat: 2 /*RepeatType.UNPACKED*/, + T: () => EventLiteral, + }, + ]); + } +} +/** + * @generated MessageType for protobuf message sui.rpc.v2.EventTerm + */ +export const EventTerm = new EventTerm$Type(); +// @generated message type with reflection information, may provide speed optimized methods +class EventLiteral$Type extends MessageType { + constructor() { + super('sui.rpc.v2.EventLiteral', [ + { no: 1, name: 'negated', kind: 'scalar', T: 8 /*ScalarType.BOOL*/ }, + { no: 2, name: 'sender', kind: 'message', oneof: 'predicate', T: () => SenderFilter }, + { + no: 3, + name: 'emit_module', + kind: 'message', + oneof: 'predicate', + T: () => EmitModuleFilter, + }, + { no: 4, name: 'event_type', kind: 'message', oneof: 'predicate', T: () => EventTypeFilter }, + { + no: 5, + name: 'event_stream_head', + kind: 'message', + oneof: 'predicate', + T: () => EventStreamHeadFilter, + }, + ]); + } +} +/** + * @generated MessageType for protobuf message sui.rpc.v2.EventLiteral + */ +export const EventLiteral = new EventLiteral$Type(); +// @generated message type with reflection information, may provide speed optimized methods +class SenderFilter$Type extends MessageType { + constructor() { + super('sui.rpc.v2.SenderFilter', [ + { no: 1, name: 'address', kind: 'scalar', opt: true, T: 9 /*ScalarType.STRING*/ }, + ]); + } +} +/** + * @generated MessageType for protobuf message sui.rpc.v2.SenderFilter + */ +export const SenderFilter = new SenderFilter$Type(); +// @generated message type with reflection information, may provide speed optimized methods +class AffectedAddressFilter$Type extends MessageType { + constructor() { + super('sui.rpc.v2.AffectedAddressFilter', [ + { no: 1, name: 'address', kind: 'scalar', opt: true, T: 9 /*ScalarType.STRING*/ }, + ]); + } +} +/** + * @generated MessageType for protobuf message sui.rpc.v2.AffectedAddressFilter + */ +export const AffectedAddressFilter = new AffectedAddressFilter$Type(); +// @generated message type with reflection information, may provide speed optimized methods +class AffectedObjectFilter$Type extends MessageType { + constructor() { + super('sui.rpc.v2.AffectedObjectFilter', [ + { no: 1, name: 'object_id', kind: 'scalar', opt: true, T: 9 /*ScalarType.STRING*/ }, + ]); + } +} +/** + * @generated MessageType for protobuf message sui.rpc.v2.AffectedObjectFilter + */ +export const AffectedObjectFilter = new AffectedObjectFilter$Type(); +// @generated message type with reflection information, may provide speed optimized methods +class MoveCallFilter$Type extends MessageType { + constructor() { + super('sui.rpc.v2.MoveCallFilter', [ + { no: 1, name: 'function', kind: 'scalar', opt: true, T: 9 /*ScalarType.STRING*/ }, + ]); + } +} +/** + * @generated MessageType for protobuf message sui.rpc.v2.MoveCallFilter + */ +export const MoveCallFilter = new MoveCallFilter$Type(); +// @generated message type with reflection information, may provide speed optimized methods +class EmitModuleFilter$Type extends MessageType { + constructor() { + super('sui.rpc.v2.EmitModuleFilter', [ + { no: 1, name: 'module', kind: 'scalar', opt: true, T: 9 /*ScalarType.STRING*/ }, + ]); + } +} +/** + * @generated MessageType for protobuf message sui.rpc.v2.EmitModuleFilter + */ +export const EmitModuleFilter = new EmitModuleFilter$Type(); +// @generated message type with reflection information, may provide speed optimized methods +class EventTypeFilter$Type extends MessageType { + constructor() { + super('sui.rpc.v2.EventTypeFilter', [ + { no: 1, name: 'event_type', kind: 'scalar', opt: true, T: 9 /*ScalarType.STRING*/ }, + ]); + } +} +/** + * @generated MessageType for protobuf message sui.rpc.v2.EventTypeFilter + */ +export const EventTypeFilter = new EventTypeFilter$Type(); +// @generated message type with reflection information, may provide speed optimized methods +class EventStreamHeadFilter$Type extends MessageType { + constructor() { + super('sui.rpc.v2.EventStreamHeadFilter', [ + { no: 1, name: 'stream_id', kind: 'scalar', opt: true, T: 9 /*ScalarType.STRING*/ }, + ]); + } +} +/** + * @generated MessageType for protobuf message sui.rpc.v2.EventStreamHeadFilter + */ +export const EventStreamHeadFilter = new EventStreamHeadFilter$Type(); +// @generated message type with reflection information, may provide speed optimized methods +class PackageWriteFilter$Type extends MessageType { + constructor() { + super('sui.rpc.v2.PackageWriteFilter', []); + } +} +/** + * @generated MessageType for protobuf message sui.rpc.v2.PackageWriteFilter + */ +export const PackageWriteFilter = new PackageWriteFilter$Type(); diff --git a/packages/sui/src/grpc/proto/sui/rpc/v2/ledger_service.client.ts b/packages/sui/src/grpc/proto/sui/rpc/v2/ledger_service.client.ts index ad2342d98..fa61bcc55 100644 --- a/packages/sui/src/grpc/proto/sui/rpc/v2/ledger_service.client.ts +++ b/packages/sui/src/grpc/proto/sui/rpc/v2/ledger_service.client.ts @@ -12,6 +12,13 @@ import type { RpcTransport } from '@protobuf-ts/runtime-rpc'; import type { ServiceInfo } from '@protobuf-ts/runtime-rpc'; import { LedgerService } from './ledger_service.js'; +import type { ListEventsResponse } from './ledger_service.js'; +import type { ListEventsRequest } from './ledger_service.js'; +import type { ListTransactionsResponse } from './ledger_service.js'; +import type { ListTransactionsRequest } from './ledger_service.js'; +import type { ListCheckpointsResponse } from './ledger_service.js'; +import type { ListCheckpointsRequest } from './ledger_service.js'; +import type { ServerStreamingCall } from '@protobuf-ts/runtime-rpc'; import type { GetEpochResponse } from './ledger_service.js'; import type { GetEpochRequest } from './ledger_service.js'; import type { GetCheckpointResponse } from './ledger_service.js'; @@ -84,6 +91,43 @@ export interface ILedgerServiceClient { input: GetEpochRequest, options?: RpcOptions, ): UnaryCall; + /** + * List checkpoints matching the provided filters. + * + * Checkpoints are returned in ascending or descending checkpoint sequence + * number order according to the query options ordering. + * A checkpoint matches if any transaction it contains satisfies the filter. + * + * @generated from protobuf rpc: ListCheckpoints(sui.rpc.v2.ListCheckpointsRequest) returns (stream sui.rpc.v2.ListCheckpointsResponse); + */ + listCheckpoints( + input: ListCheckpointsRequest, + options?: RpcOptions, + ): ServerStreamingCall; + /** + * List transactions matching the provided filters. + * + * Transactions are returned in ascending or descending transaction sequence + * order according to the query options ordering. + * + * @generated from protobuf rpc: ListTransactions(sui.rpc.v2.ListTransactionsRequest) returns (stream sui.rpc.v2.ListTransactionsResponse); + */ + listTransactions( + input: ListTransactionsRequest, + options?: RpcOptions, + ): ServerStreamingCall; + /** + * List events matching the provided filters. + * + * Events are returned in ascending or descending packed event sequence order + * according to the query options ordering. + * + * @generated from protobuf rpc: ListEvents(sui.rpc.v2.ListEventsRequest) returns (stream sui.rpc.v2.ListEventsResponse); + */ + listEvents( + input: ListEventsRequest, + options?: RpcOptions, + ): ServerStreamingCall; } /** * @generated from protobuf service sui.rpc.v2.LedgerService @@ -214,4 +258,71 @@ export class LedgerServiceClient implements ILedgerServiceClient, ServiceInfo { input, ); } + /** + * List checkpoints matching the provided filters. + * + * Checkpoints are returned in ascending or descending checkpoint sequence + * number order according to the query options ordering. + * A checkpoint matches if any transaction it contains satisfies the filter. + * + * @generated from protobuf rpc: ListCheckpoints(sui.rpc.v2.ListCheckpointsRequest) returns (stream sui.rpc.v2.ListCheckpointsResponse); + */ + listCheckpoints( + input: ListCheckpointsRequest, + options?: RpcOptions, + ): ServerStreamingCall { + const method = this.methods[7], + opt = this._transport.mergeOptions(options); + return stackIntercept( + 'serverStreaming', + this._transport, + method, + opt, + input, + ); + } + /** + * List transactions matching the provided filters. + * + * Transactions are returned in ascending or descending transaction sequence + * order according to the query options ordering. + * + * @generated from protobuf rpc: ListTransactions(sui.rpc.v2.ListTransactionsRequest) returns (stream sui.rpc.v2.ListTransactionsResponse); + */ + listTransactions( + input: ListTransactionsRequest, + options?: RpcOptions, + ): ServerStreamingCall { + const method = this.methods[8], + opt = this._transport.mergeOptions(options); + return stackIntercept( + 'serverStreaming', + this._transport, + method, + opt, + input, + ); + } + /** + * List events matching the provided filters. + * + * Events are returned in ascending or descending packed event sequence order + * according to the query options ordering. + * + * @generated from protobuf rpc: ListEvents(sui.rpc.v2.ListEventsRequest) returns (stream sui.rpc.v2.ListEventsResponse); + */ + listEvents( + input: ListEventsRequest, + options?: RpcOptions, + ): ServerStreamingCall { + const method = this.methods[9], + opt = this._transport.mergeOptions(options); + return stackIntercept( + 'serverStreaming', + this._transport, + method, + opt, + input, + ); + } } diff --git a/packages/sui/src/grpc/proto/sui/rpc/v2/ledger_service.ts b/packages/sui/src/grpc/proto/sui/rpc/v2/ledger_service.ts index e0520375b..1ca74db92 100644 --- a/packages/sui/src/grpc/proto/sui/rpc/v2/ledger_service.ts +++ b/packages/sui/src/grpc/proto/sui/rpc/v2/ledger_service.ts @@ -11,6 +11,12 @@ // import { ServiceType } from '@protobuf-ts/runtime-rpc'; import { MessageType } from '@protobuf-ts/runtime'; +import { Event } from './event.js'; +import { EventFilter } from './filter.js'; +import { QueryEnd } from './query_options.js'; +import { Watermark } from './query_options.js'; +import { QueryOptions } from './query_options.js'; +import { TransactionFilter } from './filter.js'; import { Epoch } from './epoch.js'; import { Checkpoint } from './checkpoint.js'; import { ExecutedTransaction } from './executed_transaction.js'; @@ -320,6 +326,253 @@ export interface GetEpochResponse { */ epoch?: Epoch; } +/** + * Request message for LedgerService.ListCheckpoints. + * + * @generated from protobuf message sui.rpc.v2.ListCheckpointsRequest + */ +export interface ListCheckpointsRequest { + /** + * Optional. Mask for specifying which parts of the Checkpoint should be + * returned (e.g. summary, contents, signatures). + * + * @generated from protobuf field: optional google.protobuf.FieldMask read_mask = 1; + */ + readMask?: FieldMask; + /** + * Optional. Start of the checkpoint range to query (inclusive). Defaults to + * genesis. + * + * @generated from protobuf field: optional uint64 start_checkpoint = 2; + */ + startCheckpoint?: bigint; + /** + * Optional. End of the checkpoint range to query (exclusive). Defaults to the + * current indexed ledger tip. + * + * @generated from protobuf field: optional uint64 end_checkpoint = 3; + */ + endCheckpoint?: bigint; + /** + * Optional. DNF filter over indexed transaction dimensions. A checkpoint + * matches if any transaction it contains satisfies the filter. If absent, + * all checkpoints in the range are returned. + * + * @generated from protobuf field: optional sui.rpc.v2.TransactionFilter filter = 4; + */ + filter?: TransactionFilter; + /** + * Optional cursor-bounded query options. If unspecified, reads in ascending + * order with the default item limit. The server enforces a maximum item + * limit and silently coerces larger values down to it. To paginate, pass + * the last received `Watermark.cursor` as `options.after` (ascending) or + * `options.before` (descending) on the next request. + * + * @generated from protobuf field: optional sui.rpc.v2.QueryOptions options = 5; + */ + options?: QueryOptions; +} +/** + * Response message for LedgerService.ListCheckpoints. + * + * Every frame carries a `watermark` with a safe resume cursor. A frame + * with `checkpoint` set delivers one matching item; a frame without it reports + * scan progress or terminal completion. Watermarks never regress in the + * requested ordering but may repeat. + * + * `end` is set exactly once, on the final frame of a successful stream. For + * `QUERY_END_REASON_ITEM_LIMIT`, that frame also carries the final item. For + * every other end reason, the final frame has no `checkpoint` payload. + * + * @generated from protobuf message sui.rpc.v2.ListCheckpointsResponse + */ +export interface ListCheckpointsResponse { + /** + * One matching checkpoint. + * + * @generated from protobuf field: optional sui.rpc.v2.Checkpoint checkpoint = 1; + */ + checkpoint?: Checkpoint; + /** + * Progress watermark as of this frame. Present on every frame. A + * ScanLimit terminal watermark may repeat the previous frame's cursor when + * its authoritative scan frontier was already emitted. + * + * @generated from protobuf field: optional sui.rpc.v2.Watermark watermark = 2; + */ + watermark?: Watermark; + /** + * Set exactly once, on the final frame of a successful query stream. + * + * @generated from protobuf field: optional sui.rpc.v2.QueryEnd end = 3; + */ + end?: QueryEnd; +} +/** + * Request message for LedgerService.ListTransactions. + * + * @generated from protobuf message sui.rpc.v2.ListTransactionsRequest + */ +export interface ListTransactionsRequest { + /** + * Optional. Mask for specifying which parts of the ExecutedTransaction + * should be returned. + * + * @generated from protobuf field: optional google.protobuf.FieldMask read_mask = 1; + */ + readMask?: FieldMask; + /** + * Optional. Start of the checkpoint range to query (inclusive). Defaults to + * genesis. + * + * @generated from protobuf field: optional uint64 start_checkpoint = 2; + */ + startCheckpoint?: bigint; + /** + * Optional. End of the checkpoint range to query (exclusive). Defaults to the + * current indexed ledger tip. + * + * @generated from protobuf field: optional uint64 end_checkpoint = 3; + */ + endCheckpoint?: bigint; + /** + * Optional. DNF filter over indexed dimensions. + * If absent, all transactions in the range are returned. + * + * @generated from protobuf field: optional sui.rpc.v2.TransactionFilter filter = 4; + */ + filter?: TransactionFilter; + /** + * Optional cursor-bounded query options. If unspecified, reads in ascending + * order with the default item limit. The server enforces a maximum item + * limit and silently coerces larger values down to it. To paginate, pass + * the last received `Watermark.cursor` as `options.after` (ascending) or + * `options.before` (descending) on the next request. + * + * @generated from protobuf field: optional sui.rpc.v2.QueryOptions options = 5; + */ + options?: QueryOptions; +} +/** + * Response message for LedgerService.ListTransactions. + * + * Every frame carries a `watermark` with a safe resume cursor. A frame + * with `transaction` set delivers one matching item; a frame without it reports + * scan progress or terminal completion. Watermarks never regress in the + * requested ordering but may repeat. + * + * `end` is set exactly once, on the final frame of a successful stream. For + * `QUERY_END_REASON_ITEM_LIMIT`, that frame also carries the final item. For + * every other end reason, the final frame has no `transaction` payload. + * + * @generated from protobuf message sui.rpc.v2.ListTransactionsResponse + */ +export interface ListTransactionsResponse { + /** + * One matching transaction. Its position within the containing checkpoint + * is reported by `ExecutedTransaction.transaction_index`. + * + * @generated from protobuf field: optional sui.rpc.v2.ExecutedTransaction transaction = 1; + */ + transaction?: ExecutedTransaction; + /** + * Progress watermark as of this frame. Present on every frame. A + * ScanLimit terminal watermark may repeat the previous frame's cursor when + * its authoritative scan frontier was already emitted. + * + * @generated from protobuf field: optional sui.rpc.v2.Watermark watermark = 2; + */ + watermark?: Watermark; + /** + * Set exactly once, on the final frame of a successful query stream. + * + * @generated from protobuf field: optional sui.rpc.v2.QueryEnd end = 3; + */ + end?: QueryEnd; +} +/** + * Request message for LedgerService.ListEvents. + * + * @generated from protobuf message sui.rpc.v2.ListEventsRequest + */ +export interface ListEventsRequest { + /** + * Optional. Mask for specifying which parts of the Event should be returned. + * + * @generated from protobuf field: optional google.protobuf.FieldMask read_mask = 1; + */ + readMask?: FieldMask; + /** + * Optional. Start of the checkpoint range to query (inclusive). Defaults to + * genesis. + * + * @generated from protobuf field: optional uint64 start_checkpoint = 2; + */ + startCheckpoint?: bigint; + /** + * Optional. End of the checkpoint range to query (exclusive). Defaults to the + * current indexed ledger tip. + * + * @generated from protobuf field: optional uint64 end_checkpoint = 3; + */ + endCheckpoint?: bigint; + /** + * Optional. DNF filter over indexed dimensions. + * If absent, all events in the range are returned. + * + * @generated from protobuf field: optional sui.rpc.v2.EventFilter filter = 4; + */ + filter?: EventFilter; + /** + * Optional cursor-bounded query options. If unspecified, reads in ascending + * order with the default item limit. The server enforces a maximum item + * limit and silently coerces larger values down to it. To paginate, pass + * the last received `Watermark.cursor` as `options.after` (ascending) or + * `options.before` (descending) on the next request. + * + * @generated from protobuf field: optional sui.rpc.v2.QueryOptions options = 5; + */ + options?: QueryOptions; +} +/** + * Response message for LedgerService.ListEvents. + * + * Every frame carries a `watermark` with a safe resume cursor. A frame + * with `event` set delivers one matching item; a frame without it reports scan + * progress or terminal completion. Watermarks never regress in the requested + * ordering but may repeat. + * + * `end` is set exactly once, on the final frame of a successful stream. For + * `QUERY_END_REASON_ITEM_LIMIT`, that frame also carries the final item. For + * every other end reason, the final frame has no `event` payload. + * + * @generated from protobuf message sui.rpc.v2.ListEventsResponse + */ +export interface ListEventsResponse { + /** + * One matching event. Its ledger position -- containing checkpoint, + * emitting transaction digest and offset, and index within that + * transaction's event list -- is reported by the corresponding fields on + * `Event`. + * + * @generated from protobuf field: optional sui.rpc.v2.Event event = 1; + */ + event?: Event; + /** + * Progress watermark as of this frame. Present on every frame. A + * ScanLimit terminal watermark may repeat the previous frame's cursor when + * its authoritative scan frontier was already emitted. + * + * @generated from protobuf field: optional sui.rpc.v2.Watermark watermark = 2; + */ + watermark?: Watermark; + /** + * Set exactly once, on the final frame of a successful query stream. + * + * @generated from protobuf field: optional sui.rpc.v2.QueryEnd end = 3; + */ + end?: QueryEnd; +} // @generated message type with reflection information, may provide speed optimized methods class GetServiceInfoRequest$Type extends MessageType { constructor() { @@ -606,6 +859,138 @@ class GetEpochResponse$Type extends MessageType { * @generated MessageType for protobuf message sui.rpc.v2.GetEpochResponse */ export const GetEpochResponse = new GetEpochResponse$Type(); +// @generated message type with reflection information, may provide speed optimized methods +class ListCheckpointsRequest$Type extends MessageType { + constructor() { + super('sui.rpc.v2.ListCheckpointsRequest', [ + { no: 1, name: 'read_mask', kind: 'message', T: () => FieldMask }, + { + no: 2, + name: 'start_checkpoint', + kind: 'scalar', + opt: true, + T: 4 /*ScalarType.UINT64*/, + L: 0 /*LongType.BIGINT*/, + }, + { + no: 3, + name: 'end_checkpoint', + kind: 'scalar', + opt: true, + T: 4 /*ScalarType.UINT64*/, + L: 0 /*LongType.BIGINT*/, + }, + { no: 4, name: 'filter', kind: 'message', T: () => TransactionFilter }, + { no: 5, name: 'options', kind: 'message', T: () => QueryOptions }, + ]); + } +} +/** + * @generated MessageType for protobuf message sui.rpc.v2.ListCheckpointsRequest + */ +export const ListCheckpointsRequest = new ListCheckpointsRequest$Type(); +// @generated message type with reflection information, may provide speed optimized methods +class ListCheckpointsResponse$Type extends MessageType { + constructor() { + super('sui.rpc.v2.ListCheckpointsResponse', [ + { no: 1, name: 'checkpoint', kind: 'message', T: () => Checkpoint }, + { no: 2, name: 'watermark', kind: 'message', T: () => Watermark }, + { no: 3, name: 'end', kind: 'message', T: () => QueryEnd }, + ]); + } +} +/** + * @generated MessageType for protobuf message sui.rpc.v2.ListCheckpointsResponse + */ +export const ListCheckpointsResponse = new ListCheckpointsResponse$Type(); +// @generated message type with reflection information, may provide speed optimized methods +class ListTransactionsRequest$Type extends MessageType { + constructor() { + super('sui.rpc.v2.ListTransactionsRequest', [ + { no: 1, name: 'read_mask', kind: 'message', T: () => FieldMask }, + { + no: 2, + name: 'start_checkpoint', + kind: 'scalar', + opt: true, + T: 4 /*ScalarType.UINT64*/, + L: 0 /*LongType.BIGINT*/, + }, + { + no: 3, + name: 'end_checkpoint', + kind: 'scalar', + opt: true, + T: 4 /*ScalarType.UINT64*/, + L: 0 /*LongType.BIGINT*/, + }, + { no: 4, name: 'filter', kind: 'message', T: () => TransactionFilter }, + { no: 5, name: 'options', kind: 'message', T: () => QueryOptions }, + ]); + } +} +/** + * @generated MessageType for protobuf message sui.rpc.v2.ListTransactionsRequest + */ +export const ListTransactionsRequest = new ListTransactionsRequest$Type(); +// @generated message type with reflection information, may provide speed optimized methods +class ListTransactionsResponse$Type extends MessageType { + constructor() { + super('sui.rpc.v2.ListTransactionsResponse', [ + { no: 1, name: 'transaction', kind: 'message', T: () => ExecutedTransaction }, + { no: 2, name: 'watermark', kind: 'message', T: () => Watermark }, + { no: 3, name: 'end', kind: 'message', T: () => QueryEnd }, + ]); + } +} +/** + * @generated MessageType for protobuf message sui.rpc.v2.ListTransactionsResponse + */ +export const ListTransactionsResponse = new ListTransactionsResponse$Type(); +// @generated message type with reflection information, may provide speed optimized methods +class ListEventsRequest$Type extends MessageType { + constructor() { + super('sui.rpc.v2.ListEventsRequest', [ + { no: 1, name: 'read_mask', kind: 'message', T: () => FieldMask }, + { + no: 2, + name: 'start_checkpoint', + kind: 'scalar', + opt: true, + T: 4 /*ScalarType.UINT64*/, + L: 0 /*LongType.BIGINT*/, + }, + { + no: 3, + name: 'end_checkpoint', + kind: 'scalar', + opt: true, + T: 4 /*ScalarType.UINT64*/, + L: 0 /*LongType.BIGINT*/, + }, + { no: 4, name: 'filter', kind: 'message', T: () => EventFilter }, + { no: 5, name: 'options', kind: 'message', T: () => QueryOptions }, + ]); + } +} +/** + * @generated MessageType for protobuf message sui.rpc.v2.ListEventsRequest + */ +export const ListEventsRequest = new ListEventsRequest$Type(); +// @generated message type with reflection information, may provide speed optimized methods +class ListEventsResponse$Type extends MessageType { + constructor() { + super('sui.rpc.v2.ListEventsResponse', [ + { no: 1, name: 'event', kind: 'message', T: () => Event }, + { no: 2, name: 'watermark', kind: 'message', T: () => Watermark }, + { no: 3, name: 'end', kind: 'message', T: () => QueryEnd }, + ]); + } +} +/** + * @generated MessageType for protobuf message sui.rpc.v2.ListEventsResponse + */ +export const ListEventsResponse = new ListEventsResponse$Type(); /** * @generated ServiceType for protobuf service sui.rpc.v2.LedgerService */ @@ -622,4 +1007,25 @@ export const LedgerService = new ServiceType('sui.rpc.v2.LedgerService', [ }, { name: 'GetCheckpoint', options: {}, I: GetCheckpointRequest, O: GetCheckpointResponse }, { name: 'GetEpoch', options: {}, I: GetEpochRequest, O: GetEpochResponse }, + { + name: 'ListCheckpoints', + serverStreaming: true, + options: {}, + I: ListCheckpointsRequest, + O: ListCheckpointsResponse, + }, + { + name: 'ListTransactions', + serverStreaming: true, + options: {}, + I: ListTransactionsRequest, + O: ListTransactionsResponse, + }, + { + name: 'ListEvents', + serverStreaming: true, + options: {}, + I: ListEventsRequest, + O: ListEventsResponse, + }, ]); diff --git a/packages/sui/src/grpc/proto/sui/rpc/v2/protocol_config.ts b/packages/sui/src/grpc/proto/sui/rpc/v2/protocol_config.ts index 1c9db5a72..807a098e4 100644 --- a/packages/sui/src/grpc/proto/sui/rpc/v2/protocol_config.ts +++ b/packages/sui/src/grpc/proto/sui/rpc/v2/protocol_config.ts @@ -10,6 +10,7 @@ // SPDX-License-Identifier: Apache-2.0 // import { MessageType } from '@protobuf-ts/runtime'; +import { Value } from '../../../google/protobuf/struct.js'; /** * @generated from protobuf message sui.rpc.v2.ProtocolConfig */ @@ -19,17 +20,29 @@ export interface ProtocolConfig { */ protocolVersion?: bigint; /** - * @generated from protobuf field: map feature_flags = 2; + * Deprecated in favor of the lossless `configs` field. + * + * @deprecated + * @generated from protobuf field: map feature_flags = 2 [deprecated = true]; */ featureFlags: { [key: string]: boolean; }; /** - * @generated from protobuf field: map attributes = 3; + * Deprecated in favor of the lossless `configs` field. + * + * @deprecated + * @generated from protobuf field: map attributes = 3 [deprecated = true]; */ attributes: { [key: string]: string; }; + /** + * @generated from protobuf field: map configs = 4; + */ + configs: { + [key: string]: Value; + }; } // @generated message type with reflection information, may provide speed optimized methods class ProtocolConfig$Type extends MessageType { @@ -57,6 +70,13 @@ class ProtocolConfig$Type extends MessageType { K: 9 /*ScalarType.STRING*/, V: { kind: 'scalar', T: 9 /*ScalarType.STRING*/ }, }, + { + no: 4, + name: 'configs', + kind: 'map', + K: 9 /*ScalarType.STRING*/, + V: { kind: 'message', T: () => Value }, + }, ]); } } diff --git a/packages/sui/src/grpc/proto/sui/rpc/v2/query_options.ts b/packages/sui/src/grpc/proto/sui/rpc/v2/query_options.ts new file mode 100644 index 000000000..1ac1374ae --- /dev/null +++ b/packages/sui/src/grpc/proto/sui/rpc/v2/query_options.ts @@ -0,0 +1,255 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +// @generated by protobuf-ts 2.9.6 with parameter force_server_none,optimize_code_size,ts_nocheck +// @generated from protobuf file "sui/rpc/v2/query_options.proto" (package "sui.rpc.v2", syntax proto3) +// tslint:disable +// @ts-nocheck +// +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 +// +import { MessageType } from '@protobuf-ts/runtime'; +/** + * Cursor-bounded query options. + * + * `after` and `before` are canonical ledger-position bounds, not + * ordering-relative cursors. `after` always excludes items at or below that + * cursor, and `before` always excludes items at or above that cursor. Ordering + * only controls the order of returned items within the resulting open interval. + * + * When a request also specifies a checkpoint range, cursor bounds and + * checkpoint bounds compose by intersection: results come only from ledger + * positions inside both. Checkpoint bounds are likewise canonical and + * ordering-independent. + * + * For example, with `after = A`, `before = B`, `ordering = DESCENDING`, and + * `limit = N`, the response contains up to N matching items in descending + * order from the interval `(A, B)`. If the response ends with + * `QUERY_END_REASON_ITEM_LIMIT`, resume by keeping `after = A` and setting + * `before` to the last `Watermark.cursor` received. That cursor is the + * lowest position reached in ledger order, so it becomes the next exclusive + * upper bound. + * + * @generated from protobuf message sui.rpc.v2.QueryOptions + */ +export interface QueryOptions { + /** + * The maximum number of items to return. Each method applies its own default + * and maximum. QueryEnd does not count against this limit. + * + * @generated from protobuf field: optional uint32 limit = 1; + */ + limit?: number; + /** + * Opaque exclusive lower bound. Results must be strictly after this cursor in + * canonical ledger order. + * + * @generated from protobuf field: optional bytes after = 2; + */ + after?: Uint8Array; + /** + * Opaque exclusive upper bound. Results must be strictly before this cursor + * in canonical ledger order. + * + * @generated from protobuf field: optional bytes before = 3; + */ + before?: Uint8Array; + /** + * Ordering for returned results. Defaults to ASCENDING. + * + * Ordering only controls the order of results within the bounded interval; + * cursor bounds keep the same meaning for ascending and descending reads. + * + * @generated from protobuf field: optional sui.rpc.v2.Ordering ordering = 4; + */ + ordering?: Ordering; +} +/** + * Progress marker for a query scan. Carried on every response frame, whether or + * not the frame delivers a matching item. Watermarks never regress in the + * requested ordering, but consecutive frames may carry the same watermark when + * additional work does not advance the safe resume frontier. + * + * @generated from protobuf message sui.rpc.v2.Watermark + */ +export interface Watermark { + /** + * Opaque cursor at this scan position. Set on every watermark. Use as + * `options.after` (ascending) or `options.before` (descending) on the next + * request to resume from here. The most recently received cursor is always + * the safe resume point. + * + * @generated from protobuf field: optional bytes cursor = 1; + */ + cursor?: Uint8Array; + /** + * The inclusive boundary checkpoint that the scan has fully covered within + * the request's effective interval, in the request's ordering direction: an + * ascending scan has emitted every matching item in the interval at + * checkpoints `<= checkpoint` (strictly greater ones may still hold + * matches); a descending scan has emitted every matching item in the + * interval at checkpoints `>= checkpoint`. This boundary never regresses in + * the scan direction, but it may repeat while the cursor advances. + * + * Unset until the scan's first checkpoint is fully covered. For example, a + * scan resumed from a cursor that lands mid-checkpoint leaves this unset + * until the next checkpoint boundary in the scan direction is fully covered. + * A watermark still has a valid resume cursor while this field is unset. + * + * @generated from protobuf field: optional uint64 checkpoint = 2; + */ + checkpoint?: bigint; +} +/** + * Marker for the final frame of a successful query stream. Every successful + * stream sets `QueryEnd` on exactly one frame, after which no further frames + * are sent. That frame always carries the final watermark. For `ItemLimit`, it + * also carries the final matching item; for every other reason it carries no + * item. A ScanLimit terminal watermark may repeat the previous frame's cursor + * when its authoritative scan frontier was already emitted; this does not + * repeat an item. + * + * A stream that fails or is cancelled terminates with a gRPC status and does + * not send `QueryEnd`. + * + * @generated from protobuf message sui.rpc.v2.QueryEnd + */ +export interface QueryEnd { + /** + * Reason this response stopped. + * + * @generated from protobuf field: optional sui.rpc.v2.QueryEndReason reason = 1; + */ + reason?: QueryEndReason; +} +/** + * Ordering for the returned result set. + * + * @generated from protobuf enum sui.rpc.v2.Ordering + */ +export enum Ordering { + /** + * Return results in increasing cursor order. + * + * @generated from protobuf enum value: ORDERING_ASCENDING = 0; + */ + ASCENDING = 0, + /** + * Return results in decreasing cursor order. + * + * @generated from protobuf enum value: ORDERING_DESCENDING = 1; + */ + DESCENDING = 1, +} +/** + * Reason the server stopped this query response. + * + * @generated from protobuf enum sui.rpc.v2.QueryEndReason + */ +export enum QueryEndReason { + /** + * The stop reason was not specified. + * + * @generated from protobuf enum value: QUERY_END_REASON_UNKNOWN = 0; + */ + UNKNOWN = 0, + /** + * The response reached the requested item limit. The final matching item's + * frame carries `QueryEnd` and the final watermark. Resume from that frame's + * `Watermark.cursor` to continue reading the same effective interval. + * + * @generated from protobuf enum value: QUERY_END_REASON_ITEM_LIMIT = 1; + */ + ITEM_LIMIT = 1, + /** + * The response reached the server's per-request bucket-fetch budget for + * filtered scans before reaching the effective interval bound. The terminal + * frame carries no item. Its watermark cursor is the authoritative scan + * frontier from which to resume. + * + * @generated from protobuf enum value: QUERY_END_REASON_SCAN_LIMIT = 2; + */ + SCAN_LIMIT = 2, + /** + * The scan reached a requested checkpoint range bound. The terminal frame + * carries no item. + * + * @generated from protobuf enum value: QUERY_END_REASON_CHECKPOINT_BOUND = 3; + */ + CHECKPOINT_BOUND = 3, + /** + * The scan reached an exclusive cursor bound. The terminal frame carries no + * item. Its watermark cursor represents that resolved bound without claiming + * that its containing checkpoint was fully covered. + * + * @generated from protobuf enum value: QUERY_END_REASON_CURSOR_BOUND = 4; + */ + CURSOR_BOUND = 4, + /** + * The scan reached the currently indexed ledger tip. The terminal frame + * carries no item. + * + * @generated from protobuf enum value: QUERY_END_REASON_LEDGER_TIP = 5; + */ + LEDGER_TIP = 5, +} +// @generated message type with reflection information, may provide speed optimized methods +class QueryOptions$Type extends MessageType { + constructor() { + super('sui.rpc.v2.QueryOptions', [ + { no: 1, name: 'limit', kind: 'scalar', opt: true, T: 13 /*ScalarType.UINT32*/ }, + { no: 2, name: 'after', kind: 'scalar', opt: true, T: 12 /*ScalarType.BYTES*/ }, + { no: 3, name: 'before', kind: 'scalar', opt: true, T: 12 /*ScalarType.BYTES*/ }, + { + no: 4, + name: 'ordering', + kind: 'enum', + opt: true, + T: () => ['sui.rpc.v2.Ordering', Ordering, 'ORDERING_'], + }, + ]); + } +} +/** + * @generated MessageType for protobuf message sui.rpc.v2.QueryOptions + */ +export const QueryOptions = new QueryOptions$Type(); +// @generated message type with reflection information, may provide speed optimized methods +class Watermark$Type extends MessageType { + constructor() { + super('sui.rpc.v2.Watermark', [ + { no: 1, name: 'cursor', kind: 'scalar', opt: true, T: 12 /*ScalarType.BYTES*/ }, + { + no: 2, + name: 'checkpoint', + kind: 'scalar', + opt: true, + T: 4 /*ScalarType.UINT64*/, + L: 0 /*LongType.BIGINT*/, + }, + ]); + } +} +/** + * @generated MessageType for protobuf message sui.rpc.v2.Watermark + */ +export const Watermark = new Watermark$Type(); +// @generated message type with reflection information, may provide speed optimized methods +class QueryEnd$Type extends MessageType { + constructor() { + super('sui.rpc.v2.QueryEnd', [ + { + no: 1, + name: 'reason', + kind: 'enum', + opt: true, + T: () => ['sui.rpc.v2.QueryEndReason', QueryEndReason, 'QUERY_END_REASON_'], + }, + ]); + } +} +/** + * @generated MessageType for protobuf message sui.rpc.v2.QueryEnd + */ +export const QueryEnd = new QueryEnd$Type(); diff --git a/packages/sui/src/grpc/proto/sui/rpc/v2/subscription_service.client.ts b/packages/sui/src/grpc/proto/sui/rpc/v2/subscription_service.client.ts index f84cd81b9..21c617918 100644 --- a/packages/sui/src/grpc/proto/sui/rpc/v2/subscription_service.client.ts +++ b/packages/sui/src/grpc/proto/sui/rpc/v2/subscription_service.client.ts @@ -12,27 +12,54 @@ import type { RpcTransport } from '@protobuf-ts/runtime-rpc'; import type { ServiceInfo } from '@protobuf-ts/runtime-rpc'; import { SubscriptionService } from './subscription_service.js'; +import type { SubscribeEventsResponse } from './subscription_service.js'; +import type { SubscribeEventsRequest } from './subscription_service.js'; +import type { SubscribeTransactionsResponse } from './subscription_service.js'; +import type { SubscribeTransactionsRequest } from './subscription_service.js'; import { stackIntercept } from '@protobuf-ts/runtime-rpc'; import type { SubscribeCheckpointsResponse } from './subscription_service.js'; import type { SubscribeCheckpointsRequest } from './subscription_service.js'; import type { ServerStreamingCall } from '@protobuf-ts/runtime-rpc'; import type { RpcOptions } from '@protobuf-ts/runtime-rpc'; /** + * SubscriptionService provides filtered, real-time streams of checkpoints, + * transactions, and events. + * + * Each Subscribe API pairs with the LedgerService List API of the same name: + * requests take the same filter message, and responses carry the same item + * and watermark shapes with identical cursor semantics. + * + * Subscriptions do not support resumption. A new subscription always begins + * at the current tip of the chain as seen by the server (the latest executed + * checkpoint). To recover data missed between subscriptions, replay the gap + * with the paired List API: pass the last received `Watermark.cursor` as + * `options.after` on the List request (for checkpoints, pass the last + * received `cursor + 1` as `start_checkpoint`). The List scan reads from the + * indexed tip, which may trail the subscription's start position; repeat the + * List call as the index advances until the replay reaches the position + * established by the subscription's first frame. + * + * A subscription behaves like an unbounded ascending scan: every frame + * carries the subscriber's resume point, and progress advances as + * checkpoints are fully covered. Two delivery guarantees keep sparse + * filters live: the first frame on a filtered subscription is a + * progress-only frame establishing the stream's start position, and + * progress continues to advance with bounded staleness even when no item + * matches. + * + * Subscription streams have no successful end: they run until cancelled by + * the client or terminated by the server with a gRPC status. + * * @generated from protobuf service sui.rpc.v2.SubscriptionService */ export interface ISubscriptionServiceClient { /** * Subscribe to the stream of checkpoints. * - * This API provides a subscription to the checkpoint stream for the Sui - * blockchain. When a subscription is initialized the stream will begin with - * the latest executed checkpoint as seen by the server. Responses are - * guaranteed to return checkpoints in-order and without gaps. This enables - * clients to know exactly the last checkpoint they have processed and in the - * event the subscription terminates (either by the client/server or by the - * connection breaking), clients will be able to reinitialize a subscription - * and then leverage other APIs in order to request data for the checkpoints - * they missed. + * The stream begins at the latest executed checkpoint as seen by the + * server and yields checkpoints matching the filter as they are executed. + * A checkpoint matches if any transaction it contains satisfies the + * filter. * * @generated from protobuf rpc: SubscribeCheckpoints(sui.rpc.v2.SubscribeCheckpointsRequest) returns (stream sui.rpc.v2.SubscribeCheckpointsResponse); */ @@ -40,8 +67,60 @@ export interface ISubscriptionServiceClient { input: SubscribeCheckpointsRequest, options?: RpcOptions, ): ServerStreamingCall; + /** + * Subscribe to the stream of transactions. + * + * The stream begins at the latest executed checkpoint as seen by the + * server and yields transactions matching the filter as they are executed. + * + * @generated from protobuf rpc: SubscribeTransactions(sui.rpc.v2.SubscribeTransactionsRequest) returns (stream sui.rpc.v2.SubscribeTransactionsResponse); + */ + subscribeTransactions( + input: SubscribeTransactionsRequest, + options?: RpcOptions, + ): ServerStreamingCall; + /** + * Subscribe to the stream of events. + * + * The stream begins at the latest executed checkpoint as seen by the + * server and yields events matching the filter as they are emitted. + * + * @generated from protobuf rpc: SubscribeEvents(sui.rpc.v2.SubscribeEventsRequest) returns (stream sui.rpc.v2.SubscribeEventsResponse); + */ + subscribeEvents( + input: SubscribeEventsRequest, + options?: RpcOptions, + ): ServerStreamingCall; } /** + * SubscriptionService provides filtered, real-time streams of checkpoints, + * transactions, and events. + * + * Each Subscribe API pairs with the LedgerService List API of the same name: + * requests take the same filter message, and responses carry the same item + * and watermark shapes with identical cursor semantics. + * + * Subscriptions do not support resumption. A new subscription always begins + * at the current tip of the chain as seen by the server (the latest executed + * checkpoint). To recover data missed between subscriptions, replay the gap + * with the paired List API: pass the last received `Watermark.cursor` as + * `options.after` on the List request (for checkpoints, pass the last + * received `cursor + 1` as `start_checkpoint`). The List scan reads from the + * indexed tip, which may trail the subscription's start position; repeat the + * List call as the index advances until the replay reaches the position + * established by the subscription's first frame. + * + * A subscription behaves like an unbounded ascending scan: every frame + * carries the subscriber's resume point, and progress advances as + * checkpoints are fully covered. Two delivery guarantees keep sparse + * filters live: the first frame on a filtered subscription is a + * progress-only frame establishing the stream's start position, and + * progress continues to advance with bounded staleness even when no item + * matches. + * + * Subscription streams have no successful end: they run until cancelled by + * the client or terminated by the server with a gRPC status. + * * @generated from protobuf service sui.rpc.v2.SubscriptionService */ export class SubscriptionServiceClient implements ISubscriptionServiceClient, ServiceInfo { @@ -52,15 +131,10 @@ export class SubscriptionServiceClient implements ISubscriptionServiceClient, Se /** * Subscribe to the stream of checkpoints. * - * This API provides a subscription to the checkpoint stream for the Sui - * blockchain. When a subscription is initialized the stream will begin with - * the latest executed checkpoint as seen by the server. Responses are - * guaranteed to return checkpoints in-order and without gaps. This enables - * clients to know exactly the last checkpoint they have processed and in the - * event the subscription terminates (either by the client/server or by the - * connection breaking), clients will be able to reinitialize a subscription - * and then leverage other APIs in order to request data for the checkpoints - * they missed. + * The stream begins at the latest executed checkpoint as seen by the + * server and yields checkpoints matching the filter as they are executed. + * A checkpoint matches if any transaction it contains satisfies the + * filter. * * @generated from protobuf rpc: SubscribeCheckpoints(sui.rpc.v2.SubscribeCheckpointsRequest) returns (stream sui.rpc.v2.SubscribeCheckpointsResponse); */ @@ -78,4 +152,48 @@ export class SubscriptionServiceClient implements ISubscriptionServiceClient, Se input, ); } + /** + * Subscribe to the stream of transactions. + * + * The stream begins at the latest executed checkpoint as seen by the + * server and yields transactions matching the filter as they are executed. + * + * @generated from protobuf rpc: SubscribeTransactions(sui.rpc.v2.SubscribeTransactionsRequest) returns (stream sui.rpc.v2.SubscribeTransactionsResponse); + */ + subscribeTransactions( + input: SubscribeTransactionsRequest, + options?: RpcOptions, + ): ServerStreamingCall { + const method = this.methods[1], + opt = this._transport.mergeOptions(options); + return stackIntercept( + 'serverStreaming', + this._transport, + method, + opt, + input, + ); + } + /** + * Subscribe to the stream of events. + * + * The stream begins at the latest executed checkpoint as seen by the + * server and yields events matching the filter as they are emitted. + * + * @generated from protobuf rpc: SubscribeEvents(sui.rpc.v2.SubscribeEventsRequest) returns (stream sui.rpc.v2.SubscribeEventsResponse); + */ + subscribeEvents( + input: SubscribeEventsRequest, + options?: RpcOptions, + ): ServerStreamingCall { + const method = this.methods[2], + opt = this._transport.mergeOptions(options); + return stackIntercept( + 'serverStreaming', + this._transport, + method, + opt, + input, + ); + } } diff --git a/packages/sui/src/grpc/proto/sui/rpc/v2/subscription_service.ts b/packages/sui/src/grpc/proto/sui/rpc/v2/subscription_service.ts index 3af8258d2..ceef1fae6 100644 --- a/packages/sui/src/grpc/proto/sui/rpc/v2/subscription_service.ts +++ b/packages/sui/src/grpc/proto/sui/rpc/v2/subscription_service.ts @@ -11,47 +11,161 @@ // import { ServiceType } from '@protobuf-ts/runtime-rpc'; import { MessageType } from '@protobuf-ts/runtime'; +import { Event } from './event.js'; +import { EventFilter } from './filter.js'; +import { Watermark } from './query_options.js'; +import { ExecutedTransaction } from './executed_transaction.js'; import { Checkpoint } from './checkpoint.js'; +import { TransactionFilter } from './filter.js'; import { FieldMask } from '../../../google/protobuf/field_mask.js'; /** - * Request message for SubscriptionService.SubscribeCheckpoints + * Request message for SubscriptionService.SubscribeCheckpoints. * * @generated from protobuf message sui.rpc.v2.SubscribeCheckpointsRequest */ export interface SubscribeCheckpointsRequest { /** - * Optional. Mask for specifying which parts of the - * SubscribeCheckpointsResponse should be returned. + * Optional. Mask for specifying which parts of the Checkpoint should be + * returned (e.g. summary, contents, signatures). `cursor` is always + * populated and is not subject to the mask. * * @generated from protobuf field: optional google.protobuf.FieldMask read_mask = 1; */ readMask?: FieldMask; + /** + * Optional. DNF filter over indexed transaction dimensions. A checkpoint + * matches if any transaction it contains satisfies the filter. If absent, + * every checkpoint is streamed. + * + * @generated from protobuf field: optional sui.rpc.v2.TransactionFilter filter = 2; + */ + filter?: TransactionFilter; } /** - * Response message for SubscriptionService.SubscribeCheckpoints + * Response message for SubscriptionService.SubscribeCheckpoints. + * + * A checkpoint stream's position is checkpoint-granular, so the `cursor` + * sequence number stands in for the `Watermark` message the other + * subscription responses carry. Progress-only frames (with `checkpoint` + * unset) occur only on filtered streams: on an unfiltered stream every frame + * carries both fields, in order and without gaps. * * @generated from protobuf message sui.rpc.v2.SubscribeCheckpointsResponse */ export interface SubscribeCheckpointsResponse { /** - * Required. The checkpoint sequence number and value of the current cursor - * into the checkpoint stream + * Required. The checkpoint sequence number the stream has fully covered, + * inclusive: every matching checkpoint from the stream's start position + * through `cursor` has been delivered. Present on every frame and + * advances monotonically. * * @generated from protobuf field: optional uint64 cursor = 1; */ cursor?: bigint; /** - * The requested data for this checkpoint + * The matching checkpoint. Unset when this frame only advances the + * cursor past non-matching checkpoints. * * @generated from protobuf field: optional sui.rpc.v2.Checkpoint checkpoint = 2; */ checkpoint?: Checkpoint; } +/** + * Request message for SubscriptionService.SubscribeTransactions. + * + * @generated from protobuf message sui.rpc.v2.SubscribeTransactionsRequest + */ +export interface SubscribeTransactionsRequest { + /** + * Optional. Mask for specifying which parts of the ExecutedTransaction + * should be returned. + * + * @generated from protobuf field: optional google.protobuf.FieldMask read_mask = 1; + */ + readMask?: FieldMask; + /** + * Optional. DNF filter over indexed dimensions. If absent, every + * transaction is streamed. + * + * @generated from protobuf field: optional sui.rpc.v2.TransactionFilter filter = 2; + */ + filter?: TransactionFilter; +} +/** + * Response message for SubscriptionService.SubscribeTransactions. + * + * Mirrors ListTransactionsResponse, except there is no `end` field: a + * subscription stream has no successful end. + * + * @generated from protobuf message sui.rpc.v2.SubscribeTransactionsResponse + */ +export interface SubscribeTransactionsResponse { + /** + * One matching transaction. Its position within the containing checkpoint + * is reported by `ExecutedTransaction.transaction_index`. + * + * @generated from protobuf field: optional sui.rpc.v2.ExecutedTransaction transaction = 1; + */ + transaction?: ExecutedTransaction; + /** + * Progress watermark as of this frame. Present on every frame. + * + * @generated from protobuf field: optional sui.rpc.v2.Watermark watermark = 2; + */ + watermark?: Watermark; +} +/** + * Request message for SubscriptionService.SubscribeEvents. + * + * @generated from protobuf message sui.rpc.v2.SubscribeEventsRequest + */ +export interface SubscribeEventsRequest { + /** + * Optional. Mask for specifying which parts of the Event should be + * returned. + * + * @generated from protobuf field: optional google.protobuf.FieldMask read_mask = 1; + */ + readMask?: FieldMask; + /** + * Optional. DNF filter over indexed dimensions. If absent, every event is + * streamed. + * + * @generated from protobuf field: optional sui.rpc.v2.EventFilter filter = 2; + */ + filter?: EventFilter; +} +/** + * Response message for SubscriptionService.SubscribeEvents. + * + * Mirrors ListEventsResponse, except there is no `end` field: a subscription + * stream has no successful end. + * + * @generated from protobuf message sui.rpc.v2.SubscribeEventsResponse + */ +export interface SubscribeEventsResponse { + /** + * One matching event. Its ledger position -- containing checkpoint, + * emitting transaction digest and offset, and index within that + * transaction's event list -- is reported by the corresponding fields on + * `Event`. + * + * @generated from protobuf field: optional sui.rpc.v2.Event event = 1; + */ + event?: Event; + /** + * Progress watermark as of this frame. Present on every frame. + * + * @generated from protobuf field: optional sui.rpc.v2.Watermark watermark = 2; + */ + watermark?: Watermark; +} // @generated message type with reflection information, may provide speed optimized methods class SubscribeCheckpointsRequest$Type extends MessageType { constructor() { super('sui.rpc.v2.SubscribeCheckpointsRequest', [ { no: 1, name: 'read_mask', kind: 'message', T: () => FieldMask }, + { no: 2, name: 'filter', kind: 'message', T: () => TransactionFilter }, ]); } } @@ -79,6 +193,58 @@ class SubscribeCheckpointsResponse$Type extends MessageType { + constructor() { + super('sui.rpc.v2.SubscribeTransactionsRequest', [ + { no: 1, name: 'read_mask', kind: 'message', T: () => FieldMask }, + { no: 2, name: 'filter', kind: 'message', T: () => TransactionFilter }, + ]); + } +} +/** + * @generated MessageType for protobuf message sui.rpc.v2.SubscribeTransactionsRequest + */ +export const SubscribeTransactionsRequest = new SubscribeTransactionsRequest$Type(); +// @generated message type with reflection information, may provide speed optimized methods +class SubscribeTransactionsResponse$Type extends MessageType { + constructor() { + super('sui.rpc.v2.SubscribeTransactionsResponse', [ + { no: 1, name: 'transaction', kind: 'message', T: () => ExecutedTransaction }, + { no: 2, name: 'watermark', kind: 'message', T: () => Watermark }, + ]); + } +} +/** + * @generated MessageType for protobuf message sui.rpc.v2.SubscribeTransactionsResponse + */ +export const SubscribeTransactionsResponse = new SubscribeTransactionsResponse$Type(); +// @generated message type with reflection information, may provide speed optimized methods +class SubscribeEventsRequest$Type extends MessageType { + constructor() { + super('sui.rpc.v2.SubscribeEventsRequest', [ + { no: 1, name: 'read_mask', kind: 'message', T: () => FieldMask }, + { no: 2, name: 'filter', kind: 'message', T: () => EventFilter }, + ]); + } +} +/** + * @generated MessageType for protobuf message sui.rpc.v2.SubscribeEventsRequest + */ +export const SubscribeEventsRequest = new SubscribeEventsRequest$Type(); +// @generated message type with reflection information, may provide speed optimized methods +class SubscribeEventsResponse$Type extends MessageType { + constructor() { + super('sui.rpc.v2.SubscribeEventsResponse', [ + { no: 1, name: 'event', kind: 'message', T: () => Event }, + { no: 2, name: 'watermark', kind: 'message', T: () => Watermark }, + ]); + } +} +/** + * @generated MessageType for protobuf message sui.rpc.v2.SubscribeEventsResponse + */ +export const SubscribeEventsResponse = new SubscribeEventsResponse$Type(); /** * @generated ServiceType for protobuf service sui.rpc.v2.SubscriptionService */ @@ -90,4 +256,18 @@ export const SubscriptionService = new ServiceType('sui.rpc.v2.SubscriptionServi I: SubscribeCheckpointsRequest, O: SubscribeCheckpointsResponse, }, + { + name: 'SubscribeTransactions', + serverStreaming: true, + options: {}, + I: SubscribeTransactionsRequest, + O: SubscribeTransactionsResponse, + }, + { + name: 'SubscribeEvents', + serverStreaming: true, + options: {}, + I: SubscribeEventsRequest, + O: SubscribeEventsResponse, + }, ]); diff --git a/packages/sui/src/grpc/proto/types.ts b/packages/sui/src/grpc/proto/types.ts index 7fab4f7c9..85e287be3 100644 --- a/packages/sui/src/grpc/proto/types.ts +++ b/packages/sui/src/grpc/proto/types.ts @@ -23,6 +23,7 @@ export * from './sui/rpc/v2/error_reason.js'; export * from './sui/rpc/v2/event.js'; export * from './sui/rpc/v2/executed_transaction.js'; export * from './sui/rpc/v2/execution_status.js'; +export * from './sui/rpc/v2/filter.js'; export * from './sui/rpc/v2/gas_cost_summary.js'; export * from './sui/rpc/v2/input.js'; export * from './sui/rpc/v2/jwk.js'; @@ -34,6 +35,7 @@ export * from './sui/rpc/v2/object.js'; export * from './sui/rpc/v2/object_reference.js'; export * from './sui/rpc/v2/owner.js'; export * from './sui/rpc/v2/protocol_config.js'; +export * from './sui/rpc/v2/query_options.js'; export * from './sui/rpc/v2/signature.js'; export * from './sui/rpc/v2/signature_scheme.js'; export * from './sui/rpc/v2/signature_verification_service.js'; diff --git a/packages/sui/src/jsonRpc/core.ts b/packages/sui/src/jsonRpc/core.ts index 0cf875a2e..9e2f3bb00 100644 --- a/packages/sui/src/jsonRpc/core.ts +++ b/packages/sui/src/jsonRpc/core.ts @@ -1,12 +1,13 @@ // Copyright (c) Mysten Labs, Inc. // SPDX-License-Identifier: Apache-2.0 -import { fromBase64, type InferBcsInput } from '@mysten/bcs'; +import { fromBase58, fromBase64, type InferBcsInput } from '@mysten/bcs'; import { bcs, TypeTagSerializer } from '../bcs/index.js'; import type { DevInspectResults, DryRunTransactionBlockResponse, + EventId, ExecutionStatus as JsonRpcExecutionStatus, ObjectOwner, SuiMoveAbilitySet, @@ -19,6 +20,12 @@ import type { SuiTransactionBlockResponse, TransactionEffects, } from './types/index.js'; +import { + resolveEventFilter, + resolvePagination, + resolveTransactionFilter, + validateTransactionQuery, +} from '../client/query-filters.js'; import { Transaction } from '../transactions/Transaction.js'; import { computeGasBudget, coreClientResolveTransactionPlugin } from '../client/core-resolver.js'; import { TransactionDataBuilder } from '../transactions/TransactionData.js'; @@ -650,6 +657,116 @@ export class JSONRpcCoreClient extends CoreClient { }; } + /** + * @deprecated JSON-RPC APIs are deprecated in the Sui TypeScript SDK. Use `SuiGrpcClient` + * from `@mysten/sui/grpc` or `SuiGraphQLClient` from `@mysten/sui/graphql` instead. + */ + async listTransactions( + options: SuiClientTypes.ListTransactionsOptions, + ): Promise> { + const filter = options.filter + ? await resolveTransactionFilter(this.mvr, options.filter) + : undefined; + + // Transaction cursors are digests, and bounds are interpreted relative to the + // traversal direction + const pagination = resolvePagination(options); + const { descending, after, before } = pagination; + validateTransactionQuery(filter, pagination); + + const page = await this.#jsonRpcClient.queryTransactionBlocks({ + filter: + filter && + (filter.$kind === 'sender' + ? { FromAddress: filter.sender } + : { + MoveFunction: { + package: filter.package, + module: filter.module ?? null, + function: filter.function ?? null, + }, + }), + cursor: after ?? before, + limit: pagination.limit, + order: descending ? 'descending' : 'ascending', + options: { + // showRawInput is always needed to extract signatures from SenderSignedData + showRawInput: true, + // showEffects is always needed to get status + showEffects: true, + showObjectChanges: options.include?.objectTypes ?? false, + showRawEffects: options.include?.effects ?? false, + showEvents: options.include?.events ?? false, + showBalanceChanges: options.include?.balanceChanges ?? false, + }, + signal: options.signal, + }); + + return { + transactions: page.data.map((transaction) => parseTransaction(transaction, options.include)), + hasNextPage: page.hasNextPage, + startCursor: page.data[0]?.digest ?? null, + endCursor: page.data.length + ? (page.nextCursor ?? page.data[page.data.length - 1].digest) + : null, + }; + } + + /** + * @deprecated JSON-RPC APIs are deprecated in the Sui TypeScript SDK. Use `SuiGrpcClient` + * from `@mysten/sui/grpc` or `SuiGraphQLClient` from `@mysten/sui/graphql` instead. + */ + async listEvents( + options: SuiClientTypes.ListEventsOptions, + ): Promise { + const filter = options.filter ? await resolveEventFilter(this.mvr, options.filter) : undefined; + // Event cursors are event ids, and bounds are interpreted relative to the + // traversal direction + const { descending, after, before, limit } = resolvePagination(options); + const cursor = after ?? before; + + const page = await this.#jsonRpcClient.queryEvents({ + query: !filter + ? { All: [] } + : filter.$kind === 'sender' + ? { Sender: filter.sender } + : filter.$kind === 'emitModule' + ? { MoveModule: { package: filter.package, module: filter.module } } + : filter.$kind === 'eventTypeModule' + ? { MoveEventModule: { package: filter.package, module: filter.module } } + : { MoveEventType: filter.eventType }, + cursor: cursor ? parseEventCursor(cursor) : undefined, + limit, + order: descending ? 'descending' : 'ascending', + signal: options.signal, + }); + + return { + events: page.data.map( + (event): SuiClientTypes.EventEntry => ({ + packageId: normalizeSuiAddress(event.packageId), + module: event.transactionModule, + sender: normalizeSuiAddress(event.sender), + eventType: normalizeStructTag(event.type), + bcs: event.bcsEncoding === 'base58' ? fromBase58(event.bcs) : fromBase64(event.bcs), + json: (event.parsedJson as Record) ?? null, + // queryEvents responses do not include checkpoint information + checkpoint: null, + transactionDigest: event.id.txDigest, + eventIndex: Number(event.id.eventSeq), + }), + ), + hasNextPage: page.hasNextPage, + startCursor: page.data.length ? JSON.stringify(page.data[0].id) : null, + endCursor: + page.data.length && page.nextCursor + ? JSON.stringify(page.nextCursor) + : page.data.length + ? JSON.stringify(page.data[page.data.length - 1].id) + : null, + }; + } + /** * @deprecated JSON-RPC APIs are deprecated in the Sui TypeScript SDK. Use `SuiGrpcClient` * from `@mysten/sui/grpc` or `SuiGraphQLClient` from `@mysten/sui/graphql` instead. @@ -948,6 +1065,18 @@ function parseOwnerAddress(owner: ObjectOwner): string | null { throw new Error(`Unknown owner type: ${JSON.stringify(owner)}`); } +function parseEventCursor(cursor: string): EventId { + try { + const parsed = JSON.parse(cursor) as EventId; + if (typeof parsed?.txDigest !== 'string' || typeof parsed?.eventSeq !== 'string') { + throw new Error('malformed event cursor'); + } + return parsed; + } catch { + throw new Error(`Invalid event cursor: ${cursor}`); + } +} + function parseTransaction( transaction: SuiTransactionBlockResponse, include?: Include, @@ -968,7 +1097,10 @@ function parseTransaction = ['grpc']; + +describe('Core API - Queries', () => { + let toolbox: TestToolbox; + let senderAddress: string; + let senderKeypair: Awaited>['keypair']; + let packageId: string; + let digests: string[]; + + const testWithAllClients = createTestWithAllClients(() => toolbox); + + function stripCursors( + result: T, + ) { + return { + ...result, + startCursor: result.startCursor === null ? null : '', + endCursor: result.endCursor === null ? null : '', + }; + } + + function normalizeEvents(result: SuiClientTypes.ListEventsResponse) { + return { + ...stripCursors(result), + // JSON-RPC does not expose checkpoint information for queried events + events: result.events.map((event) => ({ ...event, checkpoint: null })), + }; + } + + beforeAll(async () => { + toolbox = await setup(); + + // Publish a fresh copy of the test package so package-scoped filters only + // match this file's transactions, keeping results deterministic while other + // test files execute transactions against the same localnet + const published = await publishPackage('shared/test_data', toolbox); + packageId = published.packageId; + await toolbox.waitForTransaction({ digest: published.publishTxn.digest }); + + const { keypair, address } = await toolbox.getSigner({ coins: [1_000_000_000n] }); + senderAddress = address; + senderKeypair = keypair; + + digests = []; + for (let i = 0; i < 3; i++) { + const tx = new Transaction(); + tx.moveCall({ + target: `${packageId}::test_objects::create_object_with_event`, + arguments: [tx.pure.u64(i)], + }); + + const result = await toolbox.signAndExecuteTransaction({ + transaction: tx, + signer: keypair, + include: { effects: true }, + }); + + if (result.$kind !== 'Transaction') { + throw new Error( + `Setup tx failed: ${result.FailedTransaction.status.error?.message ?? 'unknown error'}`, + ); + } + + digests.push(result.Transaction.digest); + } + }); + + describe('listTransactions', () => { + it('all clients return same data: no filter', async () => { + await toolbox.expectAllClientsReturnSameData( + (client) => client.core.listTransactions({ limit: 3 }), + stripCursors, + { exclude: EXCLUDE }, + ); + }); + + it('all clients return same data: filter by sender with all includes', async () => { + await toolbox.expectAllClientsReturnSameData( + (client) => + client.core.listTransactions({ + filter: { sender: senderAddress }, + limit: 10, + include: { + transaction: true, + effects: true, + events: true, + balanceChanges: true, + objectTypes: true, + }, + }), + stripCursors, + { exclude: EXCLUDE }, + ); + }); + + it('all clients return same data: filter by function at each specificity', async () => { + for (const fn of [ + packageId, + `${packageId}::test_objects`, + `${packageId}::test_objects::create_object_with_event`, + ]) { + await toolbox.expectAllClientsReturnSameData( + (client) => client.core.listTransactions({ filter: { function: fn }, limit: 10 }), + stripCursors, + { exclude: EXCLUDE }, + ); + } + + // Fully qualified function filters also support pagination bounds + await toolbox.expectAllClientsReturnSameData( + async (client) => { + const fn = `${packageId}::test_objects::create_object_with_event`; + const page = await client.core.listTransactions({ filter: { function: fn }, limit: 3 }); + return client.core.listTransactions({ + filter: { function: fn }, + before: page.endCursor, + }); + }, + stripCursors, + { exclude: EXCLUDE }, + ); + }); + + it('all clients return same data: descending order and pagination', async () => { + await toolbox.expectAllClientsReturnSameData( + async (client) => { + const firstPage = await client.core.listTransactions({ + filter: { sender: senderAddress }, + limit: 2, + order: 'descending', + }); + const secondPage = await client.core.listTransactions({ + filter: { sender: senderAddress }, + limit: 2, + before: firstPage.endCursor, + }); + return { firstPage: stripCursors(firstPage), secondPage: stripCursors(secondPage) }; + }, + undefined, + { exclude: EXCLUDE }, + ); + }); + + testWithAllClients( + 'should list transactions filtered by sender', + async (client) => { + const result = await client.core.listTransactions({ + filter: { sender: senderAddress }, + include: { events: true }, + }); + + expect(result.transactions.map((tx) => tx.Transaction?.digest)).toEqual(digests); + expect(result.hasNextPage).toBe(false); + expect(result.transactions[0].Transaction?.events?.length).toBeGreaterThan(0); + }, + { skip: EXCLUDE }, + ); + + testWithAllClients( + 'should paginate transactions', + async (client) => { + const firstPage = await client.core.listTransactions({ + filter: { sender: senderAddress }, + limit: 2, + }); + + expect(firstPage.transactions.map((tx) => tx.Transaction?.digest)).toEqual( + digests.slice(0, 2), + ); + expect(firstPage.hasNextPage).toBe(true); + + const secondPage = await client.core.listTransactions({ + filter: { sender: senderAddress }, + limit: 2, + after: firstPage.endCursor, + }); + + expect(secondPage.transactions.map((tx) => tx.Transaction?.digest)).toEqual( + digests.slice(2), + ); + }, + { skip: EXCLUDE }, + ); + + testWithAllClients( + 'should list transactions in descending order', + async (client) => { + const result = await client.core.listTransactions({ + filter: { sender: senderAddress }, + order: 'descending', + }); + + expect(result.transactions.map((tx) => tx.Transaction?.digest)).toEqual( + [...digests].reverse(), + ); + }, + { skip: EXCLUDE }, + ); + + it('all clients return same data: polling for new transactions', async () => { + const clients = ( + [ + ['jsonrpc', toolbox.jsonRpcClient], + ['grpc', toolbox.grpcClient], + ['graphql', toolbox.graphqlClient], + ] as const + ).filter(([kind]) => !EXCLUDE.includes(kind)); + + // Anchor at the most recent transaction from the sender on every client + const anchors = await Promise.all( + clients.map(([, client]) => + client.core.listTransactions({ + filter: { sender: senderAddress }, + order: 'descending', + limit: 1, + }), + ), + ); + + // Execute a new transaction after the anchors were established + const tx = new Transaction(); + const [coin] = tx.splitCoins(tx.gas, [1n]); + tx.transferObjects([coin], senderAddress); + const result = await toolbox.signAndExecuteTransaction({ + transaction: tx, + signer: senderKeypair, + }); + + // Polling after each anchor's startCursor returns exactly the new transaction + const polls = await Promise.all( + clients.map(([, client], index) => + client.core.listTransactions({ + filter: { sender: senderAddress }, + after: anchors[index].startCursor, + }), + ), + ); + + for (const poll of polls) { + expect(poll.transactions.map((transaction) => transaction.Transaction?.digest)).toEqual([ + result.Transaction?.digest, + ]); + expect(poll.hasNextPage).toBe(false); + } + }); + + testWithAllClients('should reject conflicting pagination bounds', async (client) => { + await expect(client.core.listTransactions({ after: 'A', before: 'B' })).rejects.toThrow( + 'Only one of `after` or `before` may be provided', + ); + await expect( + client.core.listTransactions({ after: 'A', order: 'descending' }), + ).rejects.toThrow('`after` can not be combined with descending queries'); + await expect( + client.core.listTransactions({ before: 'B', order: 'ascending' }), + ).rejects.toThrow('`before` can not be combined with ascending queries'); + }); + + testWithAllClients('should reject filters with multiple predicates', async (client) => { + await expect( + client.core.listTransactions({ + filter: { sender: senderAddress, function: `${packageId}::test_objects` } as never, + }), + ).rejects.toThrow('exactly one of sender, function'); + }); + + testWithAllClients('should reject paginated partial function filters', async (client) => { + await expect( + client.core.listTransactions({ filter: { function: packageId }, after: 'A' }), + ).rejects.toThrow('requires a fully qualified'); + await expect( + client.core.listTransactions({ + filter: { function: `${packageId}::test_objects` }, + before: 'B', + }), + ).rejects.toThrow('requires a fully qualified'); + }); + + testWithAllClients('should reject invalid function filters', async (client) => { + await expect( + client.core.listTransactions({ filter: { function: '0x2::a::b::c' } }), + ).rejects.toThrow('Invalid function filter'); + }); + }); + + describe('listEvents', () => { + it('all clients return same data: no filter', async () => { + await toolbox.expectAllClientsReturnSameData( + (client) => client.core.listEvents({ limit: 5 }), + normalizeEvents, + { exclude: EXCLUDE }, + ); + }); + + it('all clients return same data: each filter predicate', async () => { + const filters: SuiClientTypes.EventFilter[] = [ + { sender: senderAddress }, + { emitModule: `${packageId}::test_objects` }, + { eventType: `${packageId}::test_objects` }, + { eventType: `${packageId}::test_objects::ObjectCreated` }, + ]; + + for (const filter of filters) { + await toolbox.expectAllClientsReturnSameData( + (client) => client.core.listEvents({ filter, limit: 10 }), + normalizeEvents, + { exclude: EXCLUDE }, + ); + } + }); + + it('all clients return same data: descending order and pagination', async () => { + await toolbox.expectAllClientsReturnSameData( + async (client) => { + const firstPage = await client.core.listEvents({ + filter: { sender: senderAddress }, + limit: 2, + order: 'descending', + }); + const secondPage = await client.core.listEvents({ + filter: { sender: senderAddress }, + limit: 2, + before: firstPage.endCursor, + }); + return { + firstPage: normalizeEvents(firstPage), + secondPage: normalizeEvents(secondPage), + }; + }, + undefined, + { exclude: EXCLUDE }, + ); + }); + + it('all clients return same data: event checkpoints', async () => { + // JSON-RPC cannot provide checkpoint information for queried events, so + // checkpoint parity only applies to the other transports + await toolbox.expectAllClientsReturnSameData( + async (client) => { + const result = await client.core.listEvents({ + filter: { sender: senderAddress }, + limit: 10, + }); + return result.events.map((event) => event.checkpoint); + }, + undefined, + { exclude: [...EXCLUDE, 'jsonrpc'] }, + ); + }); + + testWithAllClients( + 'should list events with ledger positions', + async (client, kind) => { + const result = await client.core.listEvents({ + filter: { eventType: `${packageId}::test_objects::ObjectCreated` }, + }); + + expect(result.events.length).toBe(digests.length); + expect(result.events.map((event) => event.transactionDigest)).toEqual(digests); + + for (const event of result.events) { + expect(event.packageId).toBe(packageId); + expect(event.module).toBe('test_objects'); + expect(event.sender).toBe(senderAddress); + expect(event.eventType).toBe(`${packageId}::test_objects::ObjectCreated`); + expect(event.bcs).toBeInstanceOf(Uint8Array); + expect(event.eventIndex).toBe(0); + + if (kind === 'jsonrpc') { + expect(event.checkpoint).toBeNull(); + } else { + expect(event.checkpoint).toMatch(/^\d+$/); + } + } + }, + { skip: EXCLUDE }, + ); + + testWithAllClients( + 'should paginate events in descending order', + async (client) => { + const firstPage = await client.core.listEvents({ + filter: { sender: senderAddress }, + limit: 2, + order: 'descending', + }); + + expect(firstPage.events.map((event) => event.transactionDigest)).toEqual( + [...digests].reverse().slice(0, 2), + ); + expect(firstPage.hasNextPage).toBe(true); + + const secondPage = await client.core.listEvents({ + filter: { sender: senderAddress }, + limit: 2, + before: firstPage.endCursor, + }); + + expect(secondPage.events.map((event) => event.transactionDigest)).toEqual( + [...digests].reverse().slice(2), + ); + }, + { skip: EXCLUDE }, + ); + + testWithAllClients('should reject module-less event filters', async (client) => { + await expect(client.core.listEvents({ filter: { emitModule: packageId } })).rejects.toThrow( + 'Invalid emitModule filter', + ); + await expect(client.core.listEvents({ filter: { eventType: packageId } })).rejects.toThrow( + 'Invalid eventType filter', + ); + }); + }); +}); diff --git a/packages/sui/test/e2e/utils/setup.ts b/packages/sui/test/e2e/utils/setup.ts index 39e585d2d..86d2b337e 100644 --- a/packages/sui/test/e2e/utils/setup.ts +++ b/packages/sui/test/e2e/utils/setup.ts @@ -234,25 +234,32 @@ export class TestToolbox { async expectAllClientsReturnSameData( queryFn: (client: ClientWithCoreApi, kind: 'jsonrpc' | 'grpc' | 'graphql') => Promise, normalize?: (result: T) => N, - options?: { skip?: boolean }, + options?: { skip?: boolean; exclude?: Array<'jsonrpc' | 'grpc' | 'graphql'> }, ) { if (options?.skip) { test.skip('all clients return same data', () => {}); return; } - const [jsonRpcResult, grpcResult, graphqlResult] = await Promise.all([ - queryFn(this.jsonRpcClient, 'jsonrpc'), - queryFn(this.grpcClient, 'grpc'), - queryFn(this.graphqlClient, 'graphql'), - ]); - - const normalizedJson = normalize ? normalize(jsonRpcResult) : jsonRpcResult; - const normalizedGrpc = normalize ? normalize(grpcResult) : grpcResult; - const normalizedGraphql = normalize ? normalize(graphqlResult) : graphqlResult; + const clients = ( + [ + ['jsonrpc', this.jsonRpcClient], + ['grpc', this.grpcClient], + ['graphql', this.graphqlClient], + ] as const + ).filter(([kind]) => !options?.exclude?.includes(kind)); + + const results = await Promise.all( + clients.map(async ([kind, client]) => { + const result = await queryFn(client, kind); + return [kind, normalize ? normalize(result) : result] as const; + }), + ); - expect(normalizedJson).toEqual(normalizedGrpc); - expect(normalizedJson).toEqual(normalizedGraphql); + const [[, first]] = results; + for (const [, result] of results.slice(1)) { + expect(result).toEqual(first); + } } } diff --git a/packages/sui/test/unit/client/query-filters.test.ts b/packages/sui/test/unit/client/query-filters.test.ts new file mode 100644 index 000000000..abfdbbdc6 --- /dev/null +++ b/packages/sui/test/unit/client/query-filters.test.ts @@ -0,0 +1,281 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +import { describe, expect, it } from 'vitest'; + +import { + resolveEventFilter, + resolvePagination, + resolveTransactionFilter, + validateTransactionQuery, +} from '../../../src/client/query-filters.js'; +import type { SuiClientTypes } from '../../../src/client/types.js'; +import { toGrpcEventFilter, toGrpcTransactionFilter } from '../../../src/grpc/filters.js'; + +const mvr: SuiClientTypes.MvrMethods = { + resolvePackage: async ({ package: pkg }) => ({ + package: pkg === '@mysten/demo' ? '0x123' : pkg, + }), + resolveType: async ({ type }) => ({ type: type.replaceAll('@mysten/demo', '0x123') }), + resolve: async () => ({ packages: {}, types: {} }), +}; + +describe('resolvePagination', () => { + it('defaults to ascending without bounds', () => { + expect(resolvePagination({})).toEqual({ + descending: false, + after: undefined, + before: undefined, + limit: 50, + }); + expect(resolvePagination({ order: 'descending' })).toEqual({ + descending: true, + after: undefined, + before: undefined, + limit: 50, + }); + }); + + it('applies a shared default page size', () => { + expect(resolvePagination({}).limit).toBe(50); + expect(resolvePagination({ limit: 7 }).limit).toBe(7); + }); + + it('infers direction from the provided bound', () => { + expect(resolvePagination({ after: 'A' })).toEqual({ + descending: false, + after: 'A', + before: undefined, + limit: 50, + }); + expect(resolvePagination({ before: 'B' })).toEqual({ + descending: true, + after: undefined, + before: 'B', + limit: 50, + }); + }); + + it('accepts bounds with a matching explicit order', () => { + expect(resolvePagination({ after: 'A', order: 'ascending' }).after).toBe('A'); + expect(resolvePagination({ before: 'B', order: 'descending' }).before).toBe('B'); + }); + + it('rejects conflicting bounds and orders', () => { + expect(() => resolvePagination({ after: 'A', before: 'B' })).toThrowError( + 'Only one of `after` or `before` may be provided', + ); + expect(() => resolvePagination({ after: 'A', order: 'descending' })).toThrowError( + '`after` can not be combined with descending queries', + ); + expect(() => resolvePagination({ before: 'B', order: 'ascending' })).toThrowError( + '`before` can not be combined with ascending queries', + ); + }); +}); + +describe('validateTransactionQuery', () => { + it('allows bounds with sender and fully qualified function filters', async () => { + const sender = await resolveTransactionFilter(mvr, { sender: '0x1' }); + const fn = await resolveTransactionFilter(mvr, { function: '0x2::coin::transfer' }); + + expect(() => validateTransactionQuery(sender, resolvePagination({ after: 'A' }))).not.toThrow(); + expect(() => validateTransactionQuery(fn, resolvePagination({ before: 'B' }))).not.toThrow(); + expect(() => + validateTransactionQuery(undefined, resolvePagination({ after: 'A' })), + ).not.toThrow(); + }); + + it('rejects bounds with partial function filters', async () => { + const pkg = await resolveTransactionFilter(mvr, { function: '0x2' }); + const mod = await resolveTransactionFilter(mvr, { function: '0x2::coin' }); + + expect(() => validateTransactionQuery(pkg, resolvePagination({ after: 'A' }))).toThrowError( + 'requires a fully qualified', + ); + expect(() => validateTransactionQuery(mod, resolvePagination({ before: 'B' }))).toThrowError( + 'requires a fully qualified', + ); + expect(() => validateTransactionQuery(pkg, resolvePagination({}))).not.toThrow(); + }); +}); + +describe('resolveTransactionFilter', () => { + it('normalizes sender addresses', async () => { + expect(await resolveTransactionFilter(mvr, { sender: '0x2' })).toEqual({ + $kind: 'sender', + sender: '0x0000000000000000000000000000000000000000000000000000000000000002', + }); + }); + + it('parses function paths at each specificity level', async () => { + const pkg = '0x0000000000000000000000000000000000000000000000000000000000000002'; + + expect(await resolveTransactionFilter(mvr, { function: '0x2' })).toEqual({ + $kind: 'function', + package: pkg, + module: undefined, + function: undefined, + }); + expect(await resolveTransactionFilter(mvr, { function: '0x2::coin' })).toEqual({ + $kind: 'function', + package: pkg, + module: 'coin', + function: undefined, + }); + expect(await resolveTransactionFilter(mvr, { function: '0x2::coin::transfer' })).toEqual({ + $kind: 'function', + package: pkg, + module: 'coin', + function: 'transfer', + }); + }); + + it('resolves MVR names in function paths', async () => { + expect(await resolveTransactionFilter(mvr, { function: '@mysten/demo::foo::bar' })).toEqual({ + $kind: 'function', + package: '0x0000000000000000000000000000000000000000000000000000000000000123', + module: 'foo', + function: 'bar', + }); + }); + + it('throws for invalid function paths', async () => { + await expect(resolveTransactionFilter(mvr, { function: '0x2::a::b::c' })).rejects.toThrowError( + 'Invalid function filter', + ); + await expect(resolveTransactionFilter(mvr, { function: '0x2::' })).rejects.toThrowError( + 'Invalid function filter', + ); + }); + + it('throws unless exactly one predicate is specified', async () => { + await expect( + resolveTransactionFilter(mvr, {} as SuiClientTypes.TransactionFilter), + ).rejects.toThrowError('exactly one of sender, function'); + await expect( + resolveTransactionFilter(mvr, { + sender: '0x1', + function: '0x2::coin', + } as never), + ).rejects.toThrowError('exactly one of sender, function'); + }); +}); + +describe('resolveEventFilter', () => { + it('normalizes sender addresses', async () => { + expect(await resolveEventFilter(mvr, { sender: '0x2' })).toEqual({ + $kind: 'sender', + sender: '0x0000000000000000000000000000000000000000000000000000000000000002', + }); + }); + + it('parses emitModule filters', async () => { + expect(await resolveEventFilter(mvr, { emitModule: '0x2::coin' })).toEqual({ + $kind: 'emitModule', + package: '0x0000000000000000000000000000000000000000000000000000000000000002', + module: 'coin', + }); + }); + + it('requires emitModule to name a module', async () => { + await expect(resolveEventFilter(mvr, { emitModule: '0x2' })).rejects.toThrowError( + 'Invalid emitModule filter', + ); + await expect(resolveEventFilter(mvr, { emitModule: '0x2::a::b' })).rejects.toThrowError( + 'Invalid emitModule filter', + ); + }); + + it('parses module-level eventType filters', async () => { + expect(await resolveEventFilter(mvr, { eventType: '0x2::coin' })).toEqual({ + $kind: 'eventTypeModule', + package: '0x0000000000000000000000000000000000000000000000000000000000000002', + module: 'coin', + }); + }); + + it('parses fully qualified eventType filters', async () => { + expect(await resolveEventFilter(mvr, { eventType: '0x2::coin::CoinCreated' })).toEqual({ + $kind: 'eventType', + eventType: '0x2::coin::CoinCreated', + }); + }); + + it('resolves MVR names in eventType filters', async () => { + expect(await resolveEventFilter(mvr, { eventType: '@mysten/demo::foo::Bar' })).toEqual({ + $kind: 'eventType', + eventType: '0x123::foo::Bar', + }); + }); + + it('requires eventType to be at least module-qualified', async () => { + await expect(resolveEventFilter(mvr, { eventType: '0x2' })).rejects.toThrowError( + 'Invalid eventType filter', + ); + }); + + it('throws unless exactly one predicate is specified', async () => { + await expect(resolveEventFilter(mvr, {} as SuiClientTypes.EventFilter)).rejects.toThrowError( + 'exactly one of sender, emitModule, eventType', + ); + }); +}); + +describe('gRPC filter conversion', () => { + it('converts sender filters', async () => { + expect(toGrpcTransactionFilter(await resolveTransactionFilter(mvr, { sender: '0x1' }))).toEqual( + { + terms: [ + { + literals: [ + { + negated: false, + predicate: { + oneofKind: 'sender', + sender: { + address: '0x0000000000000000000000000000000000000000000000000000000000000001', + }, + }, + }, + ], + }, + ], + }, + ); + }); + + it('converts function filters at each specificity', async () => { + const pkg = '0x0000000000000000000000000000000000000000000000000000000000000002'; + + for (const [input, expected] of [ + ['0x2', pkg], + ['0x2::coin', `${pkg}::coin`], + ['0x2::coin::transfer', `${pkg}::coin::transfer`], + ]) { + expect( + toGrpcTransactionFilter(await resolveTransactionFilter(mvr, { function: input })).terms[0] + .literals[0].predicate, + ).toEqual({ oneofKind: 'moveCall', moveCall: { function: expected } }); + } + }); + + it('converts event filters for each predicate', async () => { + const pkg = '0x0000000000000000000000000000000000000000000000000000000000000002'; + + expect( + toGrpcEventFilter(await resolveEventFilter(mvr, { emitModule: '0x2::coin' })).terms[0] + .literals[0].predicate, + ).toEqual({ oneofKind: 'emitModule', emitModule: { module: `${pkg}::coin` } }); + + expect( + toGrpcEventFilter(await resolveEventFilter(mvr, { eventType: '0x2::coin' })).terms[0] + .literals[0].predicate, + ).toEqual({ oneofKind: 'eventType', eventType: { eventType: `${pkg}::coin` } }); + + expect( + toGrpcEventFilter(await resolveEventFilter(mvr, { eventType: '0x2::coin::CoinCreated' })) + .terms[0].literals[0].predicate, + ).toEqual({ oneofKind: 'eventType', eventType: { eventType: '0x2::coin::CoinCreated' } }); + }); +}); diff --git a/packages/wallet-sdk/test/mocks/MockSuiClient.ts b/packages/wallet-sdk/test/mocks/MockSuiClient.ts index 68208c4aa..f083359c5 100644 --- a/packages/wallet-sdk/test/mocks/MockSuiClient.ts +++ b/packages/wallet-sdk/test/mocks/MockSuiClient.ts @@ -341,6 +341,18 @@ export class MockSuiClient extends CoreClient { throw new Error('defaultNameServiceName not implemented in MockSuiClient'); } + async listTransactions( + _options: SuiClientTypes.ListTransactionsOptions, + ): Promise> { + throw new Error('listTransactions not implemented in MockSuiClient'); + } + + async listEvents( + _options: SuiClientTypes.ListEventsOptions, + ): Promise { + throw new Error('listEvents not implemented in MockSuiClient'); + } + async simulateTransaction( _options: SuiClientTypes.SimulateTransactionOptions, ): Promise> {