diff --git a/Cargo.lock b/Cargo.lock index d9a753544374f..e66e4199dc1ec 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5291,6 +5291,12 @@ dependencies = [ "libc", ] +[[package]] +name = "markdown-gen" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8034621d7f1258317ca1dfb9205e3925d27ee4aa2a46620a09c567daf0310562" + [[package]] name = "match_opt" version = "0.1.2" @@ -10612,6 +10618,7 @@ dependencies = [ name = "sui-graphql-rpc" version = "0.1.0" dependencies = [ + "anyhow", "async-graphql", "async-graphql-axum", "async-trait", @@ -10627,6 +10634,7 @@ dependencies = [ "hyper", "insta", "lru 0.10.0", + "markdown-gen", "move-binary-format", "move-bytecode-utils", "move-compiler", @@ -10642,6 +10650,7 @@ dependencies = [ "serde_with", "serde_yaml 0.8.26", "serial_test", + "similar", "simulacrum", "sui-indexer", "sui-json-rpc", @@ -14211,6 +14220,7 @@ dependencies = [ "lz4", "lz4-sys", "mach2", + "markdown-gen", "match_opt", "matchers", "matchit 0.5.0", diff --git a/Cargo.toml b/Cargo.toml index 9e7b6527a2edc..d3ed281ab73d7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -333,6 +333,7 @@ json_to_table = { git = "https://github.com/zhiburt/tabled/", rev = "e449317a1c0 leb128 = "0.2.5" linked-hash-map = "0.5.6" lru = "0.10" +markdown-gen = "1.2.1" match_opt = "0.1.2" mime = "0.3" mockall = "0.11.4" diff --git a/crates/sui-graphql-rpc/Cargo.toml b/crates/sui-graphql-rpc/Cargo.toml index 86a736df90e55..6e864cd2130f6 100644 --- a/crates/sui-graphql-rpc/Cargo.toml +++ b/crates/sui-graphql-rpc/Cargo.toml @@ -8,6 +8,7 @@ edition = "2021" [dependencies] +anyhow.workspace = true async-graphql = {workspace = true, features = ["dataloader"] } async-graphql-axum.workspace = true async-trait.workspace = true @@ -21,6 +22,7 @@ hex.workspace = true hyper.workspace = true lru.workspace = true move-binary-format.workspace = true +markdown-gen.workspace = true mysten-metrics.workspace = true move-core-types.workspace = true once_cell.workspace = true @@ -33,6 +35,7 @@ serde.workspace = true serde_json.workspace = true serde_with.workspace = true serde_yaml.workspace = true +similar.workspace = true sui-types.workspace = true telemetry-subscribers.workspace = true tracing.workspace = true diff --git a/crates/sui-graphql-rpc/docs/examples.md b/crates/sui-graphql-rpc/docs/examples.md new file mode 100644 index 0000000000000..c2068ed559e70 --- /dev/null +++ b/crates/sui-graphql-rpc/docs/examples.md @@ -0,0 +1,1323 @@ +# Sui GraphQL Examples +### [Address](#0) +####   [Transaction Block Connection](#0) +### [Balance Connection](#1) +####   [Balance Connection](#65535) +### [Chain Id](#2) +####   [Chain Id](#131070) +### [Checkpoint](#3) +####   [At Digest](#196605) +####   [At Seq Num](#196606) +####   [First Two Tx Blocks For Checkpoint](#196607) +####   [Latest Checkpoint](#196608) +####   [Multiple Selections](#196609) +####   [With Timestamp Tx Block Live Objects](#196610) +####   [With Tx Sent Addr Filter](#196611) +### [Checkpoint Connection](#4) +####   [Ascending Fetch](#262140) +####   [First Ten After Checkpoint](#262141) +####   [Last Ten After Checkpoint](#262142) +### [Coin Connection](#5) +####   [Coin Connection](#327675) +### [Epoch](#6) +####   [Latest Epoch](#393210) +####   [Specific Epoch](#393211) +####   [With Checkpoint Connection](#393212) +####   [With Tx Block Connection](#393213) +####   [With Tx Block Connection Latest Epoch](#393214) +### [Event Connection](#7) +####   [Event Connection](#458745) +### [Name Service](#8) +####   [Name Service](#524280) +### [Object](#9) +####   [Object](#589815) +### [Object Connection](#10) +####   [Filter Object Ids](#655350) +####   [Filter Owner](#655351) +####   [Object Connection](#655352) +### [Owner](#11) +####   [Owner](#720885) +### [Protocol Configs](#12) +####   [Key Value](#786420) +####   [Key Value Feature Flag](#786421) +####   [Specific Config](#786422) +####   [Specific Feature Flag](#786423) +### [Stake Connection](#13) +####   [Stake Connection](#851955) +### [Sui System State Summary](#14) +####   [Sui System State Summary](#917490) +### [Transaction Block](#15) +####   [Transaction Block Kind](#983025) +### [Transaction Block Connection](#16) +####   [Before After Checkpoint](#1048560) +####   [Changed Object Filter](#1048561) +####   [Input Object Filter](#1048562) +####   [Input Object Sent Addr Filter](#1048563) +####   [Package Filter](#1048564) +####   [Package Module Filter](#1048565) +####   [Package Module Func Filter](#1048566) +####   [Recv Addr Filter](#1048567) +####   [Sent Addr Filter](#1048568) +####   [Tx Ids Filter](#1048569) +####   [Tx Kind Filter](#1048570) +####   [With Defaults Ascending](#1048571) +### [Transaction Block Effects](#17) +####   [Transaction Block Effects](#1114095) +## +## Address +### +### Transaction Block Connection +#### See examples in Query::transactionBlockConnection as this is +#### similar behavior to the `transactionBlockConnection` in Query but +#### supports additional `AddressTransactionBlockRelationship` filter +#### Filtering on package where the sender of the TX is the current address +#### and displaying the transaction's sender and the gas price and budget + +>
# See examples in Query::transactionBlockConnection as this is
+># similar behavior to the `transactionBlockConnection` in Query but
+># supports additional `AddressTransactionBlockRelationship` filter
+>
+># Filtering on package where the sender of the TX is the current address
+># and displaying the transaction's sender and the gas price and budget
+>query transaction_block_with_relation_filter {
+>  address(address: "0x2") {
+>    transactionBlockConnection(relation: SENT, filter: { package: "0x2" }) {
+>      nodes {
+>        sender {
+>          location
+>        }
+>        gasInput {
+>          gasPrice
+>          gasBudget
+>        }
+>      }
+>    }
+>  }
+>}
+ +## +## Balance Connection +### +### Balance Connection +#### Query the balance for objects of type COIN and then for each coin +#### get the coin type, the number of objects, and the total balance + +>
{
+>  address(
+>    address: "0x5094652429957619e6efa79a404a6714d1126e63f551f4b6c7fb76440f8118c9"
+>  ) {
+>    balance(
+>      type: "0xc060006111016b8a020ad5b33834984a437aaa7d3c74c18e09a95d48aceab08c::coin::COIN"
+>    ) {
+>      coinObjectCount
+>      totalBalance
+>    }
+>    balanceConnection {
+>      nodes {
+>        coinType
+>        coinObjectCount
+>        totalBalance
+>      }
+>      pageInfo {
+>        endCursor
+>      }
+>    }
+>  }
+>}
+ +## +## Chain Id +### +### Chain Id +#### Returns the chain identifier for the chain that the server is tracking + +>
{
+>  chainIdentifier
+>}
+ +## +## Checkpoint +### +### At Digest +#### Get the checkpoint's information at a particular digest + +>
{
+>  checkpoint(id: { digest: "GaDeWEfbSQCQ8FBQHUHVdm4KjrnbgMqEZPuhStoq5njU" }) {
+>    digest
+>    sequenceNumber
+>    validatorSignature
+>    previousCheckpointDigest
+>    networkTotalTransactions
+>    rollingGasSummary {
+>      computationCost
+>      storageCost
+>      storageRebate
+>      nonRefundableStorageFee
+>    }
+>    epoch {
+>      epochId
+>      referenceGasPrice
+>      startTimestamp
+>      endTimestamp
+>    }
+>    endOfEpoch {
+>      nextProtocolVersion
+>    }
+>  }
+>}
+ +### +### At Seq Num +#### Get the checkpoint's information at a particular sequence number + +>
{
+>  checkpoint(id: { sequenceNumber: 10 }) {
+>    digest
+>    sequenceNumber
+>    validatorSignature
+>    previousCheckpointDigest
+>    networkTotalTransactions
+>    rollingGasSummary {
+>      computationCost
+>      storageCost
+>      storageRebate
+>      nonRefundableStorageFee
+>    }
+>    epoch {
+>      epochId
+>      referenceGasPrice
+>      startTimestamp
+>      endTimestamp
+>    }
+>    endOfEpoch {
+>      nextProtocolVersion
+>    }
+>  }
+>}
+ +### +### First Two Tx Blocks For Checkpoint +#### Get data for the first two transaction blocks of checkpoint at sequence number 10 + +>
{
+>  checkpoint(id: { sequenceNumber: 10 }) {
+>    transactionBlockConnection(first: 2) {
+>      edges {
+>        node {
+>          kind {
+>            __typename
+>          }
+>          digest
+>          sender {
+>            location
+>          }
+>          expiration {
+>            epochId
+>          }
+>        }
+>      }
+>      pageInfo {
+>        startCursor
+>        hasNextPage
+>        hasPreviousPage
+>        endCursor
+>      }
+>    }
+>  }
+>}
+ +### +### Latest Checkpoint +#### Latest checkpoint's data + +>
{
+>  checkpoint {
+>    digest
+>    sequenceNumber
+>    validatorSignature
+>    previousCheckpointDigest
+>    networkTotalTransactions
+>    rollingGasSummary {
+>      computationCost
+>      storageCost
+>      storageRebate
+>      nonRefundableStorageFee
+>    }
+>    epoch {
+>      epochId
+>      referenceGasPrice
+>      startTimestamp
+>      endTimestamp
+>    }
+>    endOfEpoch {
+>      nextProtocolVersion
+>    }
+>  }
+>}
+ +### +### Multiple Selections +#### Get the checkpoint at sequence 9769 and show +#### the new committe authority and stake units + +>
{
+>  checkpoint(id: { sequenceNumber: 9769 }) {
+>    digest
+>    sequenceNumber
+>    timestamp
+>    validatorSignature
+>    previousCheckpointDigest
+>    liveObjectSetDigest
+>    networkTotalTransactions
+>    rollingGasSummary {
+>      computationCost
+>      storageCost
+>      storageRebate
+>      nonRefundableStorageFee
+>    }
+>    epoch {
+>      epochId
+>    }
+>    endOfEpoch {
+>      newCommittee {
+>        authorityName
+>        stakeUnit
+>      }
+>      nextProtocolVersion
+>    }
+>    transactionBlockConnection {
+>      edges {
+>        node {
+>          digest
+>          sender {
+>            location
+>          }
+>          expiration {
+>            epochId
+>          }
+>        }
+>      }
+>    }
+>  }
+>}
+ +### +### With Timestamp Tx Block Live Objects +#### Latest checkpoint's timestamp, liveObjectSetDigest, and transaction block data + +>
{
+>  checkpoint {
+>    digest
+>    sequenceNumber
+>    timestamp
+>    liveObjectSetDigest
+>    transactionBlockConnection {
+>      edges {
+>        node {
+>          digest
+>          sender {
+>            location
+>          }
+>          expiration {
+>            epochId
+>          }
+>        }
+>      }
+>    }
+>  }
+>}
+ +### +### With Tx Sent Addr Filter +#### Select checkpoint at sequence number 14830285 for transactions from sentAddress + +>
{
+>  checkpoint(id: { sequenceNumber: 14830285 }) {
+>    digest
+>    sequenceNumber
+>    timestamp
+>    liveObjectSetDigest
+>    transactionBlockConnection(
+>      filter: {
+>        sentAddress: "0x0000000000000000000000000000000000000000000000000000000000000000"
+>      }
+>    ) {
+>      edges {
+>        node {
+>          digest
+>          sender {
+>            location
+>          }
+>          expiration {
+>            epochId
+>          }
+>        }
+>      }
+>    }
+>  }
+>}
+ +## +## Checkpoint Connection +### +### Ascending Fetch +#### Use the checkpoint connection to fetch some default amount of checkpoints in an ascending order + +>
{
+>  checkpointConnection {
+>    nodes {
+>      digest
+>      sequenceNumber
+>      validatorSignature
+>      previousCheckpointDigest
+>      networkTotalTransactions
+>      rollingGasSummary {
+>        computationCost
+>        storageCost
+>        storageRebate
+>        nonRefundableStorageFee
+>      }
+>      epoch {
+>        epochId
+>        referenceGasPrice
+>        startTimestamp
+>        endTimestamp
+>      }
+>      endOfEpoch {
+>        nextProtocolVersion
+>      }
+>    }
+>  }
+>}
+ +### +### First Ten After Checkpoint +#### Fetch the digest and sequence number of the first 10 checkpoints after the cursor, which in this example is set to be checkpoint 11. Note that cursor will be opaque + +>
{
+>  checkpointConnection(first: 10, after: "11") {
+>    nodes {
+>      sequenceNumber
+>      digest
+>    }
+>  }
+>}
+ +### +### Last Ten After Checkpoint +#### Fetch the digest and the sequence number of the last 20 checkpoints before the cursor + +>
{
+>  checkpointConnection(last: 20, before: "100") {
+>    nodes {
+>      sequenceNumber
+>      digest
+>    }
+>  }
+>}
+ +## +## Coin Connection +### +### Coin Connection +#### Get last 3 coins before coins at cursor 13034947 + +>
{
+>  address(
+>    address: "0x0000000000000000000000000000000000000000000000000000000000000000"
+>  ) {
+>    coinConnection(last: 3, before: "0x13034947") {
+>      nodes {
+>        id
+>        balance
+>      }
+>      pageInfo {
+>        endCursor
+>        hasNextPage
+>      }
+>    }
+>  }
+>}
+ +## +## Epoch +### +### Latest Epoch +#### Latest epoch, since epoch omitted + +>
{
+>  epoch {
+>    protocolConfigs {
+>      protocolVersion
+>    }
+>    epochId
+>    referenceGasPrice
+>    startTimestamp
+>    endTimestamp
+>    validatorSet {
+>      totalStake
+>      pendingActiveValidatorsSize
+>      stakePoolMappingsSize
+>      inactivePoolsSize
+>      validatorCandidatesSize
+>      activeValidators {
+>        name
+>        description
+>        imageUrl
+>        projectUrl
+>        exchangeRates {
+>          asObject {
+>            storageRebate
+>            bcs
+>            kind
+>          }
+>          hasPublicTransfer
+>        }
+>        exchangeRatesSize
+>        stakingPoolActivationEpoch
+>        stakingPoolSuiBalance
+>        rewardsPool
+>        poolTokenBalance
+>        pendingStake
+>        pendingTotalSuiWithdraw
+>        pendingPoolTokenWithdraw
+>        votingPower
+>        gasPrice
+>        commissionRate
+>        nextEpochStake
+>        nextEpochGasPrice
+>        nextEpochCommissionRate
+>        atRisk
+>      }
+>    }
+>  }
+>}
+ +### +### Specific Epoch +#### Selecting all fields for epoch 100 + +>
{
+>  epoch(id: 100) {
+>    protocolConfigs {
+>      protocolVersion
+>    }
+>    epochId
+>    referenceGasPrice
+>    startTimestamp
+>    endTimestamp
+>    validatorSet {
+>      totalStake
+>      pendingActiveValidatorsSize
+>      stakePoolMappingsSize
+>      inactivePoolsSize
+>      validatorCandidatesSize
+>      activeValidators {
+>        name
+>        description
+>        imageUrl
+>        projectUrl
+>        exchangeRates {
+>          asObject {
+>            storageRebate
+>            bcs
+>            kind
+>          }
+>          hasPublicTransfer
+>        }
+>        exchangeRatesSize
+>        stakingPoolActivationEpoch
+>        stakingPoolSuiBalance
+>        rewardsPool
+>        poolTokenBalance
+>        pendingStake
+>        pendingTotalSuiWithdraw
+>        pendingPoolTokenWithdraw
+>        votingPower
+>        gasPrice
+>        commissionRate
+>        nextEpochStake
+>        nextEpochGasPrice
+>        nextEpochCommissionRate
+>        atRisk
+>      }
+>    }
+>  }
+>}
+ +### +### With Checkpoint Connection + +>
{
+>  epoch {
+>    checkpointConnection {
+>      nodes {
+>        transactionBlockConnection(first: 10) {
+>          pageInfo {
+>            hasNextPage
+>            endCursor
+>          }
+>          edges {
+>            cursor
+>            node {
+>              sender {
+>                location
+>              }
+>              effects {
+>                gasEffects {
+>                  gasObject {
+>                    location
+>                  }
+>                }
+>              }
+>              gasInput {
+>                gasPrice
+>                gasBudget
+>              }
+>            }
+>          }
+>        }
+>      }
+>    }
+>  }
+>}
+ +### +### With Tx Block Connection +#### Fetch the first 20 transactions after 231220100 for epoch 97 + +>
{
+>  epoch(id:97) {
+>    transactionBlockConnection(first: 20, after:"231220100") {
+>      pageInfo {
+>        hasNextPage
+>        endCursor
+>      }
+>      edges {
+>        cursor
+>        node {
+>          digest
+>          sender {
+>            location
+>          }
+>          effects {
+>            gasEffects {
+>              gasObject {
+>                location
+>              }
+>            }
+>          }
+>          gasInput {
+>            gasPrice
+>            gasBudget
+>          }
+>        }
+>      }
+>    }
+>  }
+>}
+ +### +### With Tx Block Connection Latest Epoch +#### the last checkpoint of epoch 97 is 8097645 +#### last tx number of the checkpoint is 261225985 + +>
{
+>  epoch {
+>    transactionBlockConnection(first: 20, after: "261225985") {
+>      pageInfo {
+>        hasNextPage
+>        endCursor
+>      }
+>      edges {
+>        cursor
+>        node {
+>          sender {
+>            location
+>          }
+>          effects {
+>            gasEffects {
+>              gasObject {
+>                location
+>              }
+>            }
+>          }
+>          gasInput {
+>            gasPrice
+>            gasBudget
+>          }
+>        }
+>      }
+>    }
+>  }
+>}
+ +## +## Event Connection +### +### Event Connection + +>
{
+>  eventConnection(
+>    filter: {
+>      eventType: "0x3164fcf73eb6b41ff3d2129346141bd68469964c2d95a5b1533e8d16e6ea6e13::Market::ChangePriceEvent<0x2::sui::SUI>"
+>    }
+>  ) {
+>    nodes {
+>      id
+>      sendingModuleId {
+>        name
+>        package {
+>          asObject {
+>            digest
+>          }
+>        }
+>      }
+>      eventType {
+>        repr
+>      }
+>      senders {
+>        location
+>      }
+>      timestamp
+>      json
+>      bcs
+>    }
+>  }
+>}
+ +## +## Name Service +### +### Name Service + +>
{
+>  resolveNameServiceAddress(name: "example.sui") {
+>    location
+>  }
+>  address(
+>    address: "0x0b86be5d779fac217b41d484b8040ad5145dc9ba0cba099d083c6cbda50d983e"
+>  ) {
+>    location
+>    balance(type: "0x2::sui::SUI") {
+>      coinType
+>      coinObjectCount
+>      totalBalance
+>    }
+>    defaultNameServiceName
+>  }
+>}
+ +## +## Object +### +### Object + +>
{
+>  object(
+>    address: "0x04e20ddf36af412a4096f9014f4a565af9e812db9a05cc40254846cf6ed0ad91"
+>  ) {
+>    location
+>    version
+>    digest
+>    storageRebate
+>    owner {
+>      defaultNameServiceName
+>    }
+>    previousTransactionBlock {
+>      digest
+>    }
+>    kind
+>  }
+>}
+ +## +## Object Connection +### +### Filter Object Ids +#### Filter on objectIds + +>
{
+>  objectConnection(
+>    filter: {
+>      objectIds: [
+>        "0x4bba2c7b9574129c272bca8f58594eba933af8001257aa6e0821ad716030f149"
+>      ]
+>    }
+>  ) {
+>    edges {
+>      node {
+>        storageRebate
+>        kind
+>      }
+>    }
+>  }
+>}
+ +### +### Filter Owner +#### Filter on owner + +>
{
+>  objectConnection(
+>    filter: {
+>      owner: "0x23b7b0e2badb01581ba9b3ab55587d8d9fdae087e0cfc79f2c72af36f5059439"
+>    }
+>  ) {
+>    edges {
+>      node {
+>        storageRebate
+>        kind
+>      }
+>    }
+>  }
+>}
+ +### +### Object Connection + +>
{
+>  objectConnection {
+>    nodes {
+>      version
+>      digest
+>      storageRebate
+>      previousTransactionBlock {
+>        digest
+>        sender {
+>          defaultNameServiceName
+>        }
+>        gasInput {
+>          gasPrice
+>          gasBudget
+>        }
+>      }
+>    }
+>    pageInfo {
+>      endCursor
+>    }
+>  }
+>}
+ +## +## Owner +### +### Owner + +>
{
+>  owner(
+>    address: "0x931f293ce7f65fd5ebe9542653e1fd92fafa03dda563e13b83be35da8a2eecbe"
+>  ) {
+>    location
+>  }
+>}
+ +## +## Protocol Configs +### +### Key Value +#### Select the key and value of the protocol configuration + +>
{
+>  protocolConfig {
+>    configs {
+>      key
+>      value
+>    }
+>  }
+>}
+ +### +### Key Value Feature Flag +#### Select the key and value of the feature flag + +>
{
+>  protocolConfig {
+>    featureFlags {
+>      key
+>      value
+>    }
+>  }
+>}
+ +### +### Specific Config +#### Select the key and value of the specific protocol configuration, in this case `max_move_identifier_len` + +>
{
+>  protocolConfig {
+>    config(key: "max_move_identifier_len") {
+>      key
+>      value
+>    }
+>  }
+>}
+ +### +### Specific Feature Flag + +>
{
+>  protocolConfig {
+>    protocolVersion
+>    featureFlag(key: "advance_epoch_start_time_in_safe_mode") {
+>      value
+>    }
+>  }
+>}
+ +## +## Stake Connection +### +### Stake Connection +#### Get all the staked objects for this address and all the active validators at the epoch when the stake became active + +>
{
+>  address(
+>    address: "0xc0a5b916d0e406ddde11a29558cd91b29c49e644eef597b7424a622955280e1e"
+>  ) {
+>    location
+>    balance(type: "0x2::sui::SUI") {
+>      coinType
+>      totalBalance
+>    }
+>    stakeConnection {
+>      nodes {
+>        id
+>        status
+>        principal
+>        estimatedReward
+>        activeEpoch {
+>          epochId
+>          referenceGasPrice
+>          validatorSet {
+>            activeValidators {
+>              name
+>              description
+>              exchangeRatesSize
+>            }
+>            totalStake
+>          }
+>        }
+>        requestEpoch {
+>          epochId
+>        }
+>      }
+>    }
+>  }
+>}
+ +## +## Sui System State Summary +### +### Sui System State Summary + +>
{
+>  latestSuiSystemState {
+>    systemStateVersion
+>    referenceGasPrice
+>    startTimestamp
+>    validatorSet {
+>      totalStake
+>      pendingActiveValidatorsSize
+>      stakePoolMappingsSize
+>      inactivePoolsSize
+>      validatorCandidatesSize
+>      activeValidators {
+>        name
+>        description
+>        imageUrl
+>        projectUrl
+>        exchangeRates {
+>          asObject {
+>            storageRebate
+>            bcs
+>            kind
+>          }
+>          hasPublicTransfer
+>        }
+>        exchangeRatesSize
+>        stakingPoolActivationEpoch
+>        stakingPoolSuiBalance
+>        rewardsPool
+>        poolTokenBalance
+>        pendingStake
+>        pendingTotalSuiWithdraw
+>        pendingPoolTokenWithdraw
+>        votingPower
+>        gasPrice
+>        commissionRate
+>        nextEpochStake
+>        nextEpochGasPrice
+>        nextEpochCommissionRate
+>        atRisk
+>      }
+>    }
+>  }
+>}
+ +## +## Transaction Block +### +### Transaction Block Kind + +>
{
+>  object(
+>    address: "0xd6b9c261ab53d636760a104e4ab5f46c2a3e9cda58bd392488fc4efa6e43728c"
+>  ) {
+>    previousTransactionBlock {
+>      sender {
+>        location
+>      }
+>      kind {
+>        __typename
+>        ... on ConsensusCommitPrologueTransaction {
+>          timestamp
+>          round
+>          epoch {
+>            epochId
+>            referenceGasPrice
+>          }
+>        }
+>        ... on ChangeEpochTransaction {
+>          computationCharge
+>          storageCharge
+>          timestamp
+>          storageRebate
+>        }
+>        ... on GenesisTransaction {
+>          objects
+>        }
+>      }
+>    }
+>  }
+>}
+ +## +## Transaction Block Connection +### +### Before After Checkpoint +#### Filter on before_ and after_checkpoint. If both are provided, before must be greater than after + +>
{
+>  transactionBlockConnection(
+>    filter: { afterCheckpoint: 10, beforeCheckpoint: 20 }
+>  ) {
+>    nodes {
+>      sender {
+>        location
+>      }
+>      gasInput {
+>        gasPrice
+>        gasBudget
+>      }
+>    }
+>  }
+>}
+ +### +### Changed Object Filter +#### Filter on changedObject + +>
{
+>  transactionBlockConnection(
+>    filter: {
+>      changedObject: "0x0000000000000000000000000000000000000000000000000000000000000006"
+>    }
+>  ) {
+>    nodes {
+>      sender {
+>        location
+>      }
+>      gasInput {
+>        gasPrice
+>        gasBudget
+>      }
+>    }
+>  }
+>}
+ +### +### Input Object Filter +#### Filter on inputObject + +>
{
+>  transactionBlockConnection(
+>    filter: {
+>      inputObject: "0x0000000000000000000000000000000000000000000000000000000000000006"
+>    }
+>  ) {
+>    nodes {
+>      sender {
+>        location
+>      }
+>      gasInput {
+>        gasPrice
+>        gasBudget
+>      }
+>    }
+>  }
+>}
+ +### +### Input Object Sent Addr Filter +#### multiple filters + +>
{
+>  transactionBlockConnection(
+>    filter: {
+>      inputObject: "0x0000000000000000000000000000000000000000000000000000000000000006"
+>      sentAddress: "0x0000000000000000000000000000000000000000000000000000000000000000"
+>    }
+>  ) {
+>    nodes {
+>      sender {
+>        location
+>      }
+>      effects {
+>        gasEffects {
+>          gasObject {
+>            location
+>          }
+>        }
+>      }
+>      gasInput {
+>        gasPrice
+>        gasBudget
+>      }
+>    }
+>  }
+>}
+ +### +### Package Filter +#### Filtering on package + +>
{
+>  transactionBlockConnection(
+>    filter: {
+>      package: "0x0000000000000000000000000000000000000000000000000000000000000003"
+>    }
+>  ) {
+>    nodes {
+>      sender {
+>        location
+>      }
+>      gasInput {
+>        gasPrice
+>        gasBudget
+>      }
+>    }
+>  }
+>}
+ +### +### Package Module Filter +#### Filtering on package and module + +>
{
+>  transactionBlockConnection(
+>    filter: {
+>      package: "0x0000000000000000000000000000000000000000000000000000000000000003"
+>      module: "sui_system"
+>    }
+>  ) {
+>    nodes {
+>      sender {
+>        location
+>      }
+>      gasInput {
+>        gasPrice
+>        gasBudget
+>      }
+>    }
+>  }
+>}
+ +### +### Package Module Func Filter +#### Filtering on package, module and function + +>
{
+>  transactionBlockConnection(
+>    filter: {
+>      package: "0x0000000000000000000000000000000000000000000000000000000000000003"
+>      module: "sui_system"
+>      function: "request_withdraw_stake"
+>    }
+>  ) {
+>    nodes {
+>      sender {
+>        location
+>      }
+>      gasInput {
+>        gasPrice
+>        gasBudget
+>      }
+>    }
+>  }
+>}
+ +### +### Recv Addr Filter +#### Filter on recvAddress + +>
{
+>  transactionBlockConnection(
+>    filter: {
+>      recvAddress: "0x0000000000000000000000000000000000000000000000000000000000000000"
+>    }
+>  ) {
+>    nodes {
+>      sender {
+>        location
+>      }
+>      gasInput {
+>        gasPrice
+>        gasBudget
+>      }
+>    }
+>  }
+>}
+ +### +### Sent Addr Filter +#### Filter on sign or sentAddress + +>
{
+>  transactionBlockConnection(
+>    filter: {
+>      sentAddress: "0x0000000000000000000000000000000000000000000000000000000000000000"
+>    }
+>  ) {
+>    nodes {
+>      sender {
+>        location
+>      }
+>      gasInput {
+>        gasPrice
+>        gasBudget
+>      }
+>    }
+>  }
+>}
+ +### +### Tx Ids Filter +#### Filter on transactionIds + +>
{
+>  transactionBlockConnection(
+>    filter: { transactionIds: ["DtQ6v6iJW4wMLgadENPUCEUS5t8AP7qvdG5jX84T1akR"] }
+>  ) {
+>    nodes {
+>      sender {
+>        location
+>      }
+>      gasInput {
+>        gasPrice
+>        gasBudget
+>      }
+>    }
+>  }
+>}
+ +### +### Tx Kind Filter +#### Filter on TransactionKind (only SYSTEM_TX or PROGRAMMABLE_TX) + +>
{
+>  transactionBlockConnection(filter: { kind: SYSTEM_TX }) {
+>    nodes {
+>      sender {
+>        location
+>      }
+>      gasInput {
+>        gasPrice
+>        gasBudget
+>      }
+>    }
+>  }
+>}
+ +### +### With Defaults Ascending +#### Fetch some default amount of transactions, ascending + +>
{
+>  transactionBlockConnection {
+>    nodes {
+>      digest
+>      effects {
+>        gasEffects {
+>          gasObject {
+>            version
+>            digest
+>          }
+>          gasSummary {
+>            computationCost
+>            storageCost
+>            storageRebate
+>            nonRefundableStorageFee
+>          }
+>        }
+>        errors
+>      }
+>      sender {
+>        location
+>      }
+>      gasInput {
+>        gasPrice
+>        gasBudget
+>      }
+>    }
+>    pageInfo {
+>      endCursor
+>    }
+>  }
+>}
+ +## +## Transaction Block Effects +### +### Transaction Block Effects + +>
{
+>  object(
+>    address: "0x0bba1e7d907dc2832edfc3bf4468b6deacd9a2df435a35b17e640e135d2d5ddc"
+>  ) {
+>    version
+>    kind
+>    previousTransactionBlock {
+>      effects {
+>        status
+>        checkpoint {
+>          sequenceNumber
+>        }
+>        lamportVersion
+>        gasEffects {
+>          gasSummary {
+>            computationCost
+>            storageCost
+>            storageRebate
+>            nonRefundableStorageFee
+>          }
+>        }
+>        balanceChanges {
+>          owner {
+>            location
+>            balance(type: "0x2::sui::SUI") {
+>              totalBalance
+>            }
+>          }
+>          amount
+>        }
+>        dependencies {
+>          sender {
+>            location
+>          }
+>        }
+>      }
+>    }
+>  }
+>}
+ diff --git a/crates/sui-graphql-rpc/src/commands.rs b/crates/sui-graphql-rpc/src/commands.rs index d3d6e83a12bed..9a149ea835aaa 100644 --- a/crates/sui-graphql-rpc/src/commands.rs +++ b/crates/sui-graphql-rpc/src/commands.rs @@ -18,6 +18,11 @@ pub enum Command { #[clap(short, long)] file: Option, }, + GenerateExamples { + /// Path to output examples docs. + #[clap(short, long)] + file: Option, + }, FromConfig { /// Path to TOML file containing configuration for server. #[clap(short, long)] diff --git a/crates/sui-graphql-rpc/src/examples.rs b/crates/sui-graphql-rpc/src/examples.rs new file mode 100644 index 0000000000000..0687a2a175507 --- /dev/null +++ b/crates/sui-graphql-rpc/src/examples.rs @@ -0,0 +1,208 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +use anyhow::anyhow; +use markdown_gen::markdown::{AsMarkdown, Markdown}; +use std::io::{BufWriter, Read}; +use std::path::PathBuf; + +#[derive(Debug)] +pub struct ExampleQuery { + pub name: String, + pub contents: String, + pub path: PathBuf, +} + +#[derive(Debug)] +pub struct ExampleQueryGroup { + pub name: String, + pub queries: Vec, + pub _path: PathBuf, +} + +const QUERY_EXT: &str = "graphql"; + +fn regularize_string(s: &str) -> String { + // Replace underscore with space and make every word first letter uppercase + s.replace('_', " ") + .split_whitespace() + .map(|word| { + let mut chars = word.chars(); + match chars.next() { + None => String::new(), + Some(f) => f.to_uppercase().chain(chars).collect(), + } + }) + .collect::>() + .join(" ") +} + +pub fn load_examples() -> anyhow::Result> { + let mut buf: PathBuf = PathBuf::from(env!("CARGO_MANIFEST_DIR")); + buf.push("examples"); + + let mut groups = vec![]; + for entry in std::fs::read_dir(buf).map_err(|e| anyhow::anyhow!(e))? { + let entry = entry.map_err(|e| anyhow::anyhow!(e))?; + let path = entry.path(); + let group_name = path + .file_stem() + .ok_or(anyhow::anyhow!("File stem cannot be read"))? + .to_str() + .ok_or(anyhow::anyhow!("File stem cannot be read"))? + .to_string(); + + let mut group = ExampleQueryGroup { + name: group_name.clone(), + queries: vec![], + _path: path.clone(), + }; + + for file in std::fs::read_dir(path).map_err(|e| anyhow::anyhow!(e))? { + assert!(file.is_ok()); + let file = file.map_err(|e| anyhow::anyhow!(e))?; + assert!(file.path().extension().is_some()); + let ext = file + .path() + .extension() + .ok_or(anyhow!("File extension cannot be read"))? + .to_str() + .ok_or(anyhow!("File extension cannot be read to string"))? + .to_string(); + assert_eq!(ext, QUERY_EXT, "wrong file extension for example"); + + let file_path = file.path(); + let query_name = file_path + .file_stem() + .ok_or(anyhow!("File stem cannot be read"))? + .to_str() + .ok_or(anyhow!("File extension cannot be read to string"))? + .to_string(); + + let mut contents = String::new(); + let mut fp = std::fs::File::open(file_path.clone()).map_err(|e| anyhow!(e))?; + fp.read_to_string(&mut contents).map_err(|e| anyhow!(e))?; + group.queries.push(ExampleQuery { + name: query_name, + contents, + path: file_path, + }); + } + group.queries.sort_by(|x, y| x.name.cmp(&y.name)); + + groups.push(group); + } + + groups.sort_by(|x, y| x.name.cmp(&y.name)); + Ok(groups) +} + +pub fn generate_markdown() -> anyhow::Result { + let groups = load_examples()?; + + let mut output = BufWriter::new(Vec::new()); + let mut md = Markdown::new(&mut output); + + md.write("Sui GraphQL Examples".heading(1)) + .map_err(|e| anyhow!(e))?; + + // TODO: reduce multiple loops + // Generate the table of contents + for (id, group) in groups.iter().enumerate() { + let group_name = regularize_string(&group.name); + let group_name_toc = format!("[{}](#{})", group_name, id); + md.write(group_name_toc.heading(3)) + .map_err(|e| anyhow!(e))?; + + for (inner, query) in group.queries.iter().enumerate() { + let inner_id = inner + 0xFFFF * id; + let inner_name = regularize_string(&query.name); + let inner_name_toc = format!("  [{}](#{})", inner_name, inner_id); + md.write(inner_name_toc.heading(4)) + .map_err(|e| anyhow!(e))?; + } + } + + for (id, group) in groups.iter().enumerate() { + let group_name = regularize_string(&group.name); + + let id_tag = format!("", id); + md.write(id_tag.heading(2)) + .map_err(|e| anyhow::anyhow!(e))?; + md.write(group_name.heading(2)) + .map_err(|e| anyhow::anyhow!(e))?; + for (inner, query) in group.queries.iter().enumerate() { + let inner_id = inner + 0xFFFF * id; + let name = regularize_string(&query.name); + + let id_tag = format!("", inner_id); + md.write(id_tag.heading(3)) + .map_err(|e| anyhow::anyhow!(e))?; + md.write(name.heading(3)).map_err(|e| anyhow::anyhow!(e))?; + + // Extract all lines that start with `#` and use them as headers + let mut headers = vec![]; + let mut query_start = 0; + for (idx, line) in query.contents.lines().enumerate() { + let line = line.trim(); + if line.starts_with('#') { + headers.push(line.trim_start_matches('#')); + } else if line.starts_with('{') { + query_start = idx; + break; + } + } + + // Remove headers from query + let query = query + .contents + .lines() + .skip(query_start) + .collect::>() + .join("\n"); + + let content = format!("
{}
", query); + for header in headers { + md.write(header.heading(4)) + .map_err(|e| anyhow::anyhow!(e))?; + } + md.write(content.quote()).map_err(|e| anyhow::anyhow!(e))?; + } + } + let bytes = output.into_inner().map_err(|e| anyhow::anyhow!(e))?; + Ok(String::from_utf8(bytes) + .map_err(|e| anyhow::anyhow!(e))? + .replace('\\', "")) +} + +#[test] +fn test_generate_markdown() { + use similar::*; + use std::fs::File; + + let mut buf: PathBuf = PathBuf::from(env!("CARGO_MANIFEST_DIR")); + buf.push("docs"); + buf.push("examples.md"); + let mut out_file: File = File::open(buf).expect("Could not open examples.md"); + + // Read the current content of `out_file` + let mut current_content = String::new(); + out_file + .read_to_string(&mut current_content) + .expect("Could not read examples.md"); + let new_content: String = generate_markdown().expect("Generating examples markdown failed"); + + if current_content != new_content { + let mut res = vec![]; + let diff = TextDiff::from_lines(¤t_content, &new_content); + for change in diff.iter_all_changes() { + let sign = match change.tag() { + ChangeTag::Delete => "---", + ChangeTag::Insert => "+++", + ChangeTag::Equal => " ", + }; + res.push(format!("{}{}", sign, change)); + } + panic!("Doc examples have changed. Please run `sui-graphql-rpc generate-examples` to update the docs. Diff: {}", res.join("")); + } +} diff --git a/crates/sui-graphql-rpc/src/lib.rs b/crates/sui-graphql-rpc/src/lib.rs index 15439fabff998..a5b09e5f449cf 100644 --- a/crates/sui-graphql-rpc/src/lib.rs +++ b/crates/sui-graphql-rpc/src/lib.rs @@ -11,6 +11,7 @@ pub mod client; pub mod cluster; pub mod context_data; mod error; +pub mod examples; mod extensions; mod metrics; mod types; diff --git a/crates/sui-graphql-rpc/src/main.rs b/crates/sui-graphql-rpc/src/main.rs index 85d89310121de..4d807fb68a016 100644 --- a/crates/sui-graphql-rpc/src/main.rs +++ b/crates/sui-graphql-rpc/src/main.rs @@ -25,6 +25,18 @@ async fn main() { println!("{}", &out); } } + Command::GenerateExamples { file } => { + let new_content: String = sui_graphql_rpc::examples::generate_markdown() + .expect("Generating examples markdown failed"); + + let mut buf: PathBuf = PathBuf::from(env!("CARGO_MANIFEST_DIR")); + buf.push("docs"); + buf.push("examples.md"); + let file = file.unwrap_or(buf); + + std::fs::write(file.clone(), new_content).expect("Writing examples markdown failed"); + println!("Written examples to file: {:?}", file); + } Command::StartServer { db_url, port, diff --git a/crates/sui-graphql-rpc/tests/examples_validation_tests.rs b/crates/sui-graphql-rpc/tests/examples_validation_tests.rs index 3d9d7741d2635..5cc7ea649add1 100644 --- a/crates/sui-graphql-rpc/tests/examples_validation_tests.rs +++ b/crates/sui-graphql-rpc/tests/examples_validation_tests.rs @@ -7,71 +7,11 @@ mod tests { use rand::SeedableRng; use serial_test::serial; use simulacrum::Simulacrum; - use std::io::Read; use std::path::PathBuf; use std::sync::Arc; use sui_graphql_rpc::cluster::SimulatorCluster; use sui_graphql_rpc::config::ConnectionConfig; - - struct ExampleQuery { - pub name: String, - pub contents: String, - pub path: PathBuf, - } - struct ExampleQueryGroup { - pub name: String, - pub queries: Vec, - pub _path: PathBuf, - } - - const QUERY_EXT: &str = "graphql"; - - fn verify_examples_impl() -> Vec { - let mut buf: PathBuf = PathBuf::from(env!("CARGO_MANIFEST_DIR")); - buf.push("examples"); - - let mut groups = vec![]; - for entry in std::fs::read_dir(buf).unwrap() { - let entry = entry.unwrap(); - let path = entry.path(); - let group_name = path.file_stem().unwrap().to_str().unwrap().to_string(); - - let mut group = ExampleQueryGroup { - name: group_name.clone(), - queries: vec![], - _path: path.clone(), - }; - - for file in std::fs::read_dir(path).unwrap() { - assert!(file.is_ok()); - let file = file.unwrap(); - assert!(file.path().extension().is_some()); - let ext = file - .path() - .extension() - .unwrap() - .to_str() - .unwrap() - .to_string(); - assert_eq!(ext, QUERY_EXT); - - let file_path = file.path(); - let query_name = file_path.file_stem().unwrap().to_str().unwrap().to_string(); - - let mut contents = String::new(); - let mut fp = std::fs::File::open(file_path.clone()).unwrap(); - fp.read_to_string(&mut contents).unwrap(); - group.queries.push(ExampleQuery { - name: query_name, - contents, - path: file_path, - }); - } - - groups.push(group); - } - groups - } + use sui_graphql_rpc::examples::{load_examples, ExampleQuery, ExampleQueryGroup}; fn bad_examples() -> ExampleQueryGroup { ExampleQueryGroup { @@ -139,7 +79,7 @@ mod tests { let cluster = sui_graphql_rpc::cluster::serve_simulator(connection_config, 3000, Arc::new(sim)).await; - let groups = verify_examples_impl(); + let groups = load_examples().expect("Could not load examples"); let mut errors = vec![]; for group in groups { diff --git a/crates/workspace-hack/Cargo.toml b/crates/workspace-hack/Cargo.toml index 242937781f3b2..3cbc23c42e8be 100644 --- a/crates/workspace-hack/Cargo.toml +++ b/crates/workspace-hack/Cargo.toml @@ -387,6 +387,7 @@ lru-93f6ce9d446188ac = { package = "lru", version = "0.10" } lru-ca01ad9e24f5d932 = { package = "lru", version = "0.7" } lz4 = { version = "1", default-features = false } lz4-sys = { version = "1", default-features = false } +markdown-gen = { version = "1", default-features = false } match_opt = { version = "0.1", default-features = false } matchers = { version = "0.1", default-features = false } matchit-ca01ad9e24f5d932 = { package = "matchit", version = "0.7" } @@ -1200,6 +1201,7 @@ lru-93f6ce9d446188ac = { package = "lru", version = "0.10" } lru-ca01ad9e24f5d932 = { package = "lru", version = "0.7" } lz4 = { version = "1", default-features = false } lz4-sys = { version = "1", default-features = false } +markdown-gen = { version = "1", default-features = false } match_opt = { version = "0.1", default-features = false } matchers = { version = "0.1", default-features = false } matchit-ca01ad9e24f5d932 = { package = "matchit", version = "0.7" }