├── .editorconfig ├── .eslintrc.js ├── .github └── workflows │ └── npm-publish.yml ├── .gitignore ├── .npmrc ├── CHANGELOG.md ├── LICENSE ├── README.md ├── __tests__ ├── .env.test.sh ├── .gitignore ├── assets │ └── testract │ │ ├── Move.toml │ │ └── sources │ │ └── main.move ├── common.ts ├── main.test.ts ├── orderbook.ts ├── safe.ts ├── stresstest.ts └── tsconfig.stress.json ├── bin ├── build.sh ├── setup-test-env.sh └── start-localnet.sh ├── examples ├── 0-fetch-nft.ts ├── 0-use-parsers.ts ├── 1-create-fee.ts ├── 10-buy-from-market.ts ├── 11-create-orderbook-market.ts ├── 13-place-bid-with-split-coin-with-commission.ts ├── 13-place-bid-with-split-coin.ts ├── 14-place-ask-using-kiosk-nft.ts ├── 14-place-ask-with-commission.ts ├── 15-fetch-orderbook.ts ├── 16-cancel-ask.ts ├── 16-cancel-bid-with-split-coin.ts ├── 17-buy-nft-with-split-coin.ts ├── 18-market-sell.ts ├── 19-market-buy.ts ├── 2-init-marketplace.ts ├── 25-edit-ask.ts ├── 25-edit-bid-with-split-coin.ts ├── 3-init-listing.ts ├── 33-create-kiosk.ts ├── 34-get-kiosk.ts ├── 35-place-bid.ts ├── 36-close-bid.ts ├── 37-sell-nft.ts ├── 38-create-kiosk-and-buy-nft-into-kiosk.ts ├── 38-deposit-nft.ts ├── 39-create-kiosk-and-buy-whitelisted-nft-into-kiosk.ts ├── 39-sell-nft-from-kiosk.ts ├── 4-init-warehouse.ts ├── 40-get-nfts-for-kiosk.ts ├── 5-mint-nft.ts ├── 6-add-warehouse-to-listing.ts ├── 7-create-market.ts ├── 8-attach-marketplace.ts ├── 9-enable-sales.ts ├── common.ts ├── full-cycle.ts ├── test.ts └── tsconfig.json ├── jest.config.js ├── package.json ├── src ├── client │ ├── FullClient.ts │ ├── NftClient.ts │ ├── ReadClient.ts │ ├── bidding │ │ ├── BiddingContractFullClient.ts │ │ ├── BiddingContractReadClient.ts │ │ ├── txBuilder.ts │ │ └── types.ts │ ├── consts.ts │ ├── index.ts │ ├── kiosk │ │ ├── KioskFullClient.ts │ │ ├── KioskReadClient.ts │ │ ├── txBuilder.ts │ │ └── types.ts │ ├── orderbook │ │ ├── OrderbookFullClient.ts │ │ ├── OrderbookReadClient.ts │ │ └── txBuilder.ts │ ├── parsers.ts │ ├── safe │ │ ├── SafeFullClient.ts │ │ ├── SafeReadClient.ts │ │ └── txBuilder.ts │ ├── sui-contract │ │ ├── SuiContractFullClient.ts │ │ ├── SuiContractReadClient.ts │ │ ├── txBuilder.ts │ │ └── types.ts │ ├── transfer-request │ │ ├── TransferRequestFullClient.ts │ │ ├── txBuilder.ts │ │ └── types.ts │ ├── txBuilders.ts │ ├── types.ts │ └── utils.ts ├── index.ts ├── transaction.ts └── utils.ts ├── tsconfig-typecheck.json ├── tsconfig.json └── yarn.lock /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = space 6 | insert_final_newline = true 7 | trim_trailing_whitespace = true 8 | 9 | [*.ts] 10 | indent_size = 2 11 | 12 | [*.json] 13 | indent_size = 2 14 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | browser: true, 4 | es2021: true, 5 | node: true, 6 | }, 7 | ignorePatterns: ["__tests__/**/*"], 8 | extends: ["airbnb-base", "prettier"], 9 | parser: "@typescript-eslint/parser", 10 | parserOptions: { 11 | ecmaVersion: 13, 12 | sourceType: "module", 13 | }, 14 | plugins: ["import", "@typescript-eslint"], 15 | rules: { 16 | quotes: ["error", "double"], 17 | "import/prefer-default-export": "off", 18 | "import/no-default-export": "error", 19 | "import/extensions": "off", 20 | "no-unused-vars": "warn", 21 | "arrow-body-style": "off", 22 | "class-methods-use-this": "warn", 23 | "max-len": ["error", { code: 140 }], 24 | "no-shadow": "off", 25 | "@typescript-eslint/no-shadow": ["error"], 26 | }, 27 | settings: { 28 | "import/parsers": { 29 | "@typescript-eslint/parser": [".ts"], 30 | }, 31 | "import/resolver": { 32 | typescript: { 33 | // always try to resolve types under `@types` 34 | // directory even it doesn't contain any source code, like `@types/unist` 35 | 36 | alwaysTryTypes: true, 37 | 38 | project: "path/to/folder", 39 | }, 40 | }, 41 | }, 42 | }; 43 | -------------------------------------------------------------------------------- /.github/workflows/npm-publish.yml: -------------------------------------------------------------------------------- 1 | # This workflow will run tests using node and then publish a package to GitHub Packages when a release is created 2 | # For more information see: https://docs.github.com/en/actions/publishing-packages/publishing-nodejs-packages 3 | 4 | name: npm-publish 5 | 6 | on: 7 | release: 8 | types: [created] 9 | 10 | jobs: 11 | build: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v3 15 | - uses: actions/setup-node@v3 16 | with: 17 | node-version: 16 18 | - run: yarn install --frozen-lockfile 19 | 20 | publish-npm: 21 | needs: build 22 | runs-on: ubuntu-latest 23 | steps: 24 | - uses: actions/checkout@v3 25 | - uses: actions/setup-node@v3 26 | with: 27 | node-version: 16 28 | registry-url: https://registry.npmjs.org/ 29 | - run: npm config set '//registry.npmjs.org/:_authToken' "${NODE_AUTH_TOKEN}" 30 | - run: yarn install --frozen-lockfile 31 | - run: npm config ls -l 32 | - run: npm publish 33 | env: 34 | NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}} 35 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | npm-debug.log 4 | coverage 5 | yarn-error.log 6 | tmp 7 | coverage 8 | sui.log.* 9 | examples/nft-uploader/ 10 | wget-log 11 | dist-stress 12 | package-lock.json 13 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | always-auth=true 2 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a 6 | Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to 7 | [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 8 | 9 | ## Unreleased 10 | 11 | ### Changed 12 | 13 | - Formatting adheres to `prettier` rules. 14 | 15 | ### Added 16 | 17 | - Initial orderbook SDKs support. We support all actions and fetch state. 18 | - Initial safe SDKs support. We support all actions and fetch state. 19 | - Orderbook support for trading of generic collections. 20 | - Orderbook events support 21 | - Option to sort orderbook bids and asks by price. 22 | - Fetching and parsing `TradeIntermediary` object by its ID. 23 | - Returning list of `TradePayment` IDs when finishing a trade. 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # OriginByte JS SDK 2 | 3 | [![npm version](https://badge.fury.io/js/@originbyte%2Fjs-sdk.svg)](https://badge.fury.io/js/@originbyte%2Fjs-sdk) 4 | [![License](https://img.shields.io/badge/License-Apache_2.0-blue.svg)](https://opensource.org/licenses/Apache-2.0) 5 | 6 | Javascript/Typescript SDK to work with Origin Byte NFT protocol. 7 | 8 | Please note: The project is in an active development stage. Hence, some methods might change dramatically. 9 | 10 | ## Installation 11 | 12 | ``` 13 | yarn add @originbyte/js-sdk 14 | ``` 15 | 16 | or 17 | 18 | ``` 19 | npm add @originbyte/js-sdk 20 | ``` 21 | 22 | ## Usage 23 | 24 | The main point of the SDK is `NftClient` class. It provides all necessary methods to fetch the data from the blockchain and build transactions to interact with the contract. 25 | However, under the hood, the `NftClient` data fetcher uses the idea of a parsers' approach: you can create your own parser and fetch any data from the blockchain - by user wallet address or directly by Object ID. 26 | 27 | The parser, by itself, is an object which implements an interface: 28 | 29 | ```typescript 30 | export interface SuiObjectParser { 31 | parser: ( 32 | typedData: RpcResponse, 33 | suiObject: SuiObject, 34 | rpcResponse: GetObjectDataResponse 35 | ) => DataModel | undefined; // Parsing function, which takes RPC response and transform it into the plain JS object. 36 | regex: RegExp; // Regular expression to filter objects 37 | } 38 | ``` 39 | 40 | Example of the parser: 41 | 42 | ```typescript 43 | export const CollectionParser: SuiObjectParser< 44 | NftCollectionRpcResponse, 45 | NftCollection 46 | > = { 47 | parser: ( 48 | data: NftCollectionRpcResponse, 49 | suiData: SuiObject, 50 | rpcResponse: GetObjectDataResponse 51 | ) => ({ 52 | name: data.name, 53 | description: data.description, 54 | creators: data.creators, 55 | symbol: data.symbol, 56 | currentSupply: data.cap.fields.supply.fields.current, 57 | totalSupply: data.cap.fields.supply.fields.cap, 58 | receiver: data.receiver, 59 | type: suiData.data.dataType, 60 | id: suiData.reference.objectId, 61 | tags: data.tags.fields.enumerations.fields.contents.map( 62 | (_) => _.fields.value 63 | ), 64 | rawResponse: rpcResponse, 65 | }), 66 | regex: 67 | /0x[a-f0-9]{40}::collection::Collection<0x[a-f0-9]{40}::[a-zA-Z]{1,}::[a-zA-Z]{1,}, 0x[a-f0-9]{40}::std_collection::StdMeta, 0x[a-f0-9]{40}::cap::[a-zA-Z]{1,}>/, 68 | }; 69 | ``` 70 | 71 | Besides of that, the SDK provides predefined parsers and methods to interact with Origin Byte's NFT protocol. Next methods are available: 72 | 73 | - fetchAndParseObjectsById 74 | - fetchAndParseObjectsForAddress 75 | - getMintAuthoritiesById 76 | - getMarketsByParams 77 | - getCollectionsById 78 | - getCollectionsForAddress 79 | - getNftsById 80 | - getNftsForAddress 81 | - getNftCertificatesById 82 | - getNftCertificatesForAddress 83 | 84 | Take a look at [Examples](#examples) for more details. 85 | 86 | ### Examples 87 | 88 | #### Fetch Onchain Data: Collection and NFTs 89 | 90 | ```typescript 91 | import { NftClient } from "@originbyte/js-sdk"; 92 | 93 | const getNfts = async () => { 94 | const client = new NftClient(); 95 | const collection = await client.getCollectionsById({ 96 | objectIds: ["0xfc18b65338d4bb906018e5f73b586a57b777d46d"], 97 | }); 98 | const nfts = await client.getNftsForAddress( 99 | "0x0ec841965c95866d38fa7bcd09047f4e0dfa0ed9" 100 | ); 101 | console.log("nfts", collection, nfts); 102 | }; 103 | 104 | getNfts(); 105 | ``` 106 | 107 | #### Mint new NFT 108 | 109 | ```typescript 110 | const mintToLaunchpad = async () => { 111 | const collections = await client.getCollectionsForAddress( 112 | `0x${keypair.getPublicKey().toSuiAddress()}` 113 | ); 114 | 115 | const collectionsForWallet = collections.filter( 116 | (_) => _.packageObjectId === PACKAGE_OBJECT_ID 117 | ); 118 | 119 | console.log("collectionForWallet", collectionsForWallet); 120 | if (collectionsForWallet.length) { 121 | const collection = collectionsForWallet[0]; 122 | const mintNftTransaction = NftClient.buildMintNftTx({ 123 | mintAuthority: collection.mintAuthorityId, 124 | moduleName: "suimarines", 125 | name: "My First NFT", 126 | description: "My First NFT", 127 | packageObjectId: collection.packageObjectId, 128 | url: "https://i.imgur.com/D5yhcTC.png", 129 | attributes: { 130 | Rarity: "Ultra-rare", 131 | Author: "OriginByte", 132 | }, 133 | launchpadId: LAUNCHPAD_ID, 134 | }); 135 | // console.log('signer', keypair.getPublicKey().toSuiAddress()); 136 | const mintResult = await signer.executeMoveCall(mintNftTransaction); 137 | console.log("mintResult", mintResult); 138 | } 139 | }; 140 | ``` 141 | 142 | More examples could be found [there](https://github.com/Origin-Byte/originbyte-js-sdk/tree/main/examples). 143 | 144 | ## Tests 145 | 146 | See the `__tests__` directory. 147 | We deploy `__tests__/assets/testract` contract to the local validator. 148 | It is used to set resources up which would otherwise be beyond the scope of the test suite. 149 | 150 | The `__tests__/assets/.tmp` directory 151 | 152 | - contains the localnet validator network; 153 | - `originmate` dependency; 154 | - `ntf-protocol` dependency. 155 | 156 | Start the localnet validator network with `$ ./bin/start-localnet.sh`. 157 | Then, in a separate terminal, run the test suite with `$ ./bin/test.sh`. 158 | 159 | ## Useful Links 160 | 161 | - [Website](https://originbyte.io) 162 | - [Protocol Source Code](https://github.com/Origin-Byte/nft-protocol) 163 | -------------------------------------------------------------------------------- /__tests__/.env.test.sh: -------------------------------------------------------------------------------- 1 | export SUI_TAG="devnet-0.27.0" 2 | export SUI_VERSION="sui 0.27.0-598f106" 3 | export NFT_PROTOCOL_REV=e5893612eb470848474e01c7e12bf53681392b8b 4 | -------------------------------------------------------------------------------- /__tests__/.gitignore: -------------------------------------------------------------------------------- 1 | build 2 | .tmp 3 | -------------------------------------------------------------------------------- /__tests__/assets/testract/Move.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "Testract" 3 | version = "0.1.0" 4 | 5 | [dependencies.NftProtocol] 6 | local = "../.tmp/nft-protocol" 7 | # git = "https://github.com/Origin-Byte/nft-protocol.git" 8 | # rev = "7a19d30" 9 | 10 | [addresses] 11 | testract = "0x0" 12 | -------------------------------------------------------------------------------- /__tests__/assets/testract/sources/main.move: -------------------------------------------------------------------------------- 1 | module testract::testract { 2 | use std::option; 3 | use std::vector; 4 | use std::string; 5 | 6 | use sui::coin; 7 | use sui::object::{Self, UID}; 8 | use sui::transfer::{share_object, transfer}; 9 | use sui::tx_context::{Self, TxContext}; 10 | 11 | use nft_protocol::collection::{Self, Collection}; 12 | use nft_protocol::display; 13 | use nft_protocol::nft; 14 | use nft_protocol::royalties::{Self, TradePayment}; 15 | use nft_protocol::royalty; 16 | use nft_protocol::tags; 17 | use nft_protocol::transfer_allowlist_domain; 18 | use nft_protocol::transfer_allowlist; 19 | use nft_protocol::witness; 20 | 21 | // mint more NFTs if not enough for new tests 22 | const NFTS_TO_MINT: u64 = 32; 23 | 24 | struct TESTRACT has drop {} 25 | 26 | struct Witness has drop {} 27 | 28 | // simulates a generic NFT 29 | struct CapyNft has key, store { 30 | id: UID, 31 | } 32 | 33 | const TEST_USER: address = @0xddcdd8e07b59852f58ba8db8daff1b585d2fca23; 34 | 35 | fun init(witness: TESTRACT, ctx: &mut TxContext) { 36 | let (mint_cap, collection) = collection::create( 37 | &witness, 38 | ctx, 39 | ); 40 | let delegated_witness = witness::from_witness(&Witness {}); 41 | 42 | add_domains(&mut collection, ctx); 43 | 44 | let allowlist = transfer_allowlist::create(&Witness {}, ctx); 45 | transfer_allowlist::insert_collection( 46 | &Witness {}, 47 | witness::from_witness(&Witness {}), 48 | &mut allowlist, 49 | ); 50 | 51 | collection::add_domain( 52 | delegated_witness, 53 | &mut collection, 54 | transfer_allowlist_domain::from_id(object::id(&allowlist)), 55 | ); 56 | 57 | let i = 0; 58 | while (i < NFTS_TO_MINT) { 59 | transfer( 60 | nft::new( 61 | &Witness {}, 62 | string::utf8(b"some nft"), 63 | sui::url::new_unsafe_from_bytes(b"http://example.com"), 64 | TEST_USER, 65 | ctx, 66 | ), 67 | TEST_USER, 68 | ); 69 | transfer(CapyNft { id: object::new(ctx) }, TEST_USER); 70 | i = i + 1; 71 | }; 72 | 73 | let (treasury_cap, meta) = coin::create_currency( 74 | witness, 75 | 0, 76 | vector::empty(), 77 | vector::empty(), 78 | vector::empty(), 79 | option::none(), 80 | ctx, 81 | ); 82 | 83 | transfer(treasury_cap, TEST_USER); 84 | transfer(mint_cap, TEST_USER); 85 | transfer(allowlist, TEST_USER); 86 | 87 | share_object(collection); 88 | share_object(meta); 89 | } 90 | 91 | public entry fun mint_n_nfts( 92 | n: u64, 93 | safe: &mut nft_protocol::safe::Safe, 94 | ctx: &mut TxContext, 95 | ) { 96 | let sender = sui::tx_context::sender(ctx); 97 | 98 | let i = 0; 99 | while (i < n) { 100 | let nft = nft::new( 101 | &Witness {}, 102 | string::utf8(b"hi there"), 103 | sui::url::new_unsafe_from_bytes(b"http://example.com"), 104 | sender, 105 | ctx, 106 | ); 107 | nft_protocol::safe::deposit_nft(nft, safe, ctx); 108 | 109 | i = i + 1; 110 | }; 111 | } 112 | 113 | public entry fun create_bid( 114 | price: u64, 115 | safe: &mut nft_protocol::safe::Safe, 116 | treasury: &mut sui::coin::TreasuryCap, 117 | orderbook: &mut nft_protocol::orderbook::Orderbook, 118 | ctx: &mut TxContext, 119 | ) { 120 | let wallet = coin::mint(treasury, price, ctx); 121 | 122 | nft_protocol::orderbook::create_bid( 123 | orderbook, 124 | safe, 125 | price, 126 | &mut wallet, 127 | ctx, 128 | ); 129 | 130 | coin::burn(treasury, wallet); 131 | } 132 | 133 | public entry fun collect_royalty( 134 | payment: &mut TradePayment, 135 | ctx: &mut TxContext, 136 | ) { 137 | royalties::transfer_remaining_to_beneficiary(Witness {}, payment, ctx); 138 | } 139 | 140 | fun add_domains( 141 | collection: &mut Collection, 142 | ctx: &mut TxContext, 143 | ) { 144 | let delegated_witness = witness::from_witness(&Witness {}); 145 | 146 | display::add_collection_display_domain( 147 | delegated_witness, 148 | collection, 149 | string::utf8(b"Suimarines"), 150 | string::utf8(b"A unique NFT collection of Suimarines on Sui"), 151 | ); 152 | 153 | display::add_collection_url_domain( 154 | delegated_witness, 155 | collection, 156 | sui::url::new_unsafe_from_bytes(b"https://originbyte.io/"), 157 | ); 158 | 159 | display::add_collection_symbol_domain( 160 | delegated_witness, 161 | collection, 162 | string::utf8(b"SUIM"), 163 | ); 164 | 165 | let royalty = royalty::from_address(tx_context::sender(ctx), ctx); 166 | royalty::add_proportional_royalty(&mut royalty, 125); // 1.25% 167 | royalty::add_royalty_domain( 168 | nft_protocol::witness::from_witness(&Witness {}), 169 | collection, 170 | royalty, 171 | ); 172 | 173 | let tags = tags::empty(ctx); 174 | tags::add_tag(&mut tags, tags::art()); 175 | tags::add_collection_tag_domain( 176 | nft_protocol::witness::from_witness(&Witness {}), 177 | collection, 178 | tags, 179 | ); 180 | } 181 | } 182 | -------------------------------------------------------------------------------- /__tests__/common.ts: -------------------------------------------------------------------------------- 1 | import { Connection, Ed25519Keypair, JsonRpcProvider } from "@mysten/sui.js"; 2 | import { OrderbookFullClient, SafeFullClient } from "../src"; 3 | 4 | export const TESTRACT_ADDRESS = process.env.TESTRACT_ADDRESS; 5 | export const TESTRACT_OTW_TYPE = `${TESTRACT_ADDRESS}::testract::TESTRACT`; 6 | export const TESTRACT_C_TYPE = TESTRACT_OTW_TYPE; 7 | export const NFT_GENERIC_TYPE = `${TESTRACT_ADDRESS}::testract::CapyNft`; 8 | export const NFT_PROTOCOL_ADDRESS = process.env.NFT_PROTOCOL_ADDRESS; 9 | export const NFT_TYPE = `${NFT_PROTOCOL_ADDRESS}::nft::Nft<${TESTRACT_C_TYPE}>`; 10 | 11 | const provider = new JsonRpcProvider( 12 | new Connection({ fullnode: "http://localhost:9000" }) 13 | ); 14 | // suiaddr: ddcdd8e07b59852f58ba8db8daff1b585d2fca23 15 | // also determines addr in test.sh 16 | export const keypair = new Ed25519Keypair({ 17 | publicKey: Uint8Array.from([ 18 | 123, 49, 136, 138, 93, 52, 142, 26, 32, 156, 52, 154, 223, 80, 191, 2, 136, 19 | 183, 246, 194, 17, 192, 124, 120, 97, 137, 189, 25, 225, 196, 206, 252, 20 | ]), 21 | secretKey: Uint8Array.from([ 22 | 247, 8, 180, 26, 178, 76, 142, 156, 80, 194, 241, 66, 143, 182, 235, 102, 23 | 66, 242, 47, 157, 43, 116, 165, 212, 124, 189, 163, 59, 11, 212, 187, 138, 24 | 123, 49, 136, 138, 93, 52, 142, 26, 32, 156, 52, 154, 223, 80, 191, 2, 136, 25 | 183, 246, 194, 17, 192, 124, 120, 97, 137, 189, 25, 225, 196, 206, 252, 26 | ]), 27 | }); 28 | export const user = keypair.getPublicKey().toSuiAddress(); 29 | export const safeClient = SafeFullClient.fromKeypair(keypair, provider, { 30 | packageObjectId: NFT_PROTOCOL_ADDRESS, 31 | }); 32 | export const orderbookClient = OrderbookFullClient.fromKeypair( 33 | keypair, 34 | provider, 35 | { 36 | packageObjectId: NFT_PROTOCOL_ADDRESS, 37 | } 38 | ); 39 | 40 | export async function getGas() { 41 | const coins = (await provider.getCoins({owner: user})).data; 42 | 43 | if (coins.length === 0) { 44 | throw new Error(`No gas object for user '${user}'`); 45 | } 46 | const coin = coins[0]; 47 | if (typeof coin !== "object" || !("data" in coin)) { 48 | throw new Error(`Unexpected coin type: ${JSON.stringify(coin)}`); 49 | } 50 | 51 | return coin.coinObjectId; 52 | } 53 | 54 | export async function fetchNfts() { 55 | const objs = await safeClient.client.getObjects(user); 56 | return objs.filter((o) => o.type === NFT_TYPE).map((o) => o.objectId); 57 | } 58 | 59 | export async function fetchGenericNfts() { 60 | const objs = await safeClient.client.getObjects(user); 61 | const nfts = objs 62 | .filter((o) => o.type === NFT_GENERIC_TYPE) 63 | .map((o) => o.objectId); 64 | 65 | return nfts; 66 | } 67 | -------------------------------------------------------------------------------- /__tests__/main.test.ts: -------------------------------------------------------------------------------- 1 | // we want all tests ran sequentially 2 | import safeSuite from "./safe"; 3 | import orderbookSuite from "./orderbook"; 4 | 5 | safeSuite(); 6 | orderbookSuite(); 7 | -------------------------------------------------------------------------------- /__tests__/orderbook.ts: -------------------------------------------------------------------------------- 1 | import { SUI_TYPE_ARG } from "@mysten/sui.js"; 2 | import { 3 | fetchGenericNfts, 4 | fetchNfts, 5 | getGas, 6 | NFT_GENERIC_TYPE, 7 | NFT_PROTOCOL_ADDRESS, 8 | orderbookClient, 9 | safeClient, 10 | TESTRACT_C_TYPE, 11 | user, 12 | } from "./common"; 13 | 14 | export default function suite() { 15 | test("create orderbook", async () => { 16 | const { orderbook } = await orderbookClient.createOrderbook({ 17 | collection: TESTRACT_C_TYPE, 18 | ft: SUI_TYPE_ARG, 19 | }); 20 | const state = await orderbookClient.fetchOrderbook(orderbook); 21 | expect(state.asks.length).toBe(0); 22 | expect(state.bids.length).toBe(0); 23 | expect(state.protectedActions).toStrictEqual({ 24 | buyNft: false, 25 | cancelAsk: false, 26 | cancelBid: false, 27 | createAsk: false, 28 | createBid: false, 29 | }); 30 | }); 31 | 32 | test("create bid", async () => { 33 | const { safe } = await safeClient.createSafeForSender(); 34 | 35 | const { orderbook } = await orderbookClient.createOrderbook({ 36 | collection: TESTRACT_C_TYPE, 37 | ft: SUI_TYPE_ARG, 38 | }); 39 | 40 | await orderbookClient.createBid({ 41 | buyerSafe: safe, 42 | collection: TESTRACT_C_TYPE, 43 | ft: SUI_TYPE_ARG, 44 | orderbook, 45 | price: 10, 46 | wallet: await getGas(), 47 | }); 48 | 49 | const state = await orderbookClient.fetchOrderbook(orderbook); 50 | expect(state.bids.length).toBe(1); 51 | expect(state.bids[0].offer).toBe(10); 52 | expect(state.bids[0].owner).toBe(`0x${user}`); 53 | expect(state.bids[0].safe).toBe(safe); 54 | expect(state.bids[0].commission).toBeUndefined(); 55 | }); 56 | 57 | test("create ask", async () => { 58 | const allNfts = await fetchNfts(); 59 | expect(allNfts.length).toBeGreaterThan(0); 60 | const nft = allNfts[0]; 61 | const { safe, ownerCap } = await safeClient.createSafeForSender(); 62 | await safeClient.depositNft({ 63 | safe, 64 | nft, 65 | collection: TESTRACT_C_TYPE, 66 | }); 67 | const { transferCap } = 68 | await safeClient.createExclusiveTransferCapForSender({ 69 | safe, 70 | nft, 71 | ownerCap, 72 | }); 73 | 74 | const { orderbook } = await orderbookClient.createOrderbook({ 75 | collection: TESTRACT_C_TYPE, 76 | ft: SUI_TYPE_ARG, 77 | }); 78 | 79 | await orderbookClient.createAsk({ 80 | sellerSafe: safe, 81 | collection: TESTRACT_C_TYPE, 82 | ft: SUI_TYPE_ARG, 83 | orderbook, 84 | transferCap, 85 | price: 10, 86 | }); 87 | 88 | const state = await orderbookClient.fetchOrderbook(orderbook); 89 | expect(state.asks.length).toBe(1); 90 | expect(state.asks[0].price).toBe(10); 91 | expect(state.asks[0].owner).toBe(`0x${user}`); 92 | expect(state.asks[0].transferCap.nft).toBe(nft); 93 | expect(state.asks[0].commission).toBeUndefined(); 94 | }); 95 | 96 | test("create bid with commission", async () => { 97 | const { safe } = await safeClient.createSafeForSender(); 98 | 99 | const { orderbook } = await orderbookClient.createOrderbook({ 100 | collection: TESTRACT_C_TYPE, 101 | ft: SUI_TYPE_ARG, 102 | }); 103 | 104 | const someBeneficiary = "0xe71f60229c0ed838b7fe25f0ce57690b7067f199"; 105 | await orderbookClient.createBidWithCommission({ 106 | buyerSafe: safe, 107 | collection: TESTRACT_C_TYPE, 108 | ft: SUI_TYPE_ARG, 109 | orderbook, 110 | price: 10, 111 | wallet: await getGas(), 112 | beneficiary: someBeneficiary, 113 | commission: 5, 114 | }); 115 | 116 | const state = await orderbookClient.fetchOrderbook(orderbook); 117 | expect(state.bids.length).toBe(1); 118 | expect(state.bids[0].offer).toBe(10); 119 | expect(state.bids[0].owner).toBe(`0x${user}`); 120 | expect(state.bids[0].safe).toBe(safe); 121 | expect(state.bids[0].commission).toStrictEqual({ 122 | beneficiary: someBeneficiary, 123 | cut: 5, 124 | }); 125 | }); 126 | 127 | test("create ask with commission", async () => { 128 | const allNfts = await fetchNfts(); 129 | expect(allNfts.length).toBeGreaterThan(0); 130 | const nft = allNfts[0]; 131 | const { safe, ownerCap } = await safeClient.createSafeForSender(); 132 | await safeClient.depositNft({ 133 | safe, 134 | nft, 135 | collection: TESTRACT_C_TYPE, 136 | }); 137 | const { transferCap } = 138 | await safeClient.createExclusiveTransferCapForSender({ 139 | safe, 140 | nft, 141 | ownerCap, 142 | }); 143 | 144 | const { orderbook } = await orderbookClient.createOrderbook({ 145 | collection: TESTRACT_C_TYPE, 146 | ft: SUI_TYPE_ARG, 147 | }); 148 | 149 | const someBeneficiary = "0xe71f60229c0ed838b7fe25f0ce57690b7067f199"; 150 | await orderbookClient.createAskWithCommission({ 151 | sellerSafe: safe, 152 | collection: TESTRACT_C_TYPE, 153 | ft: SUI_TYPE_ARG, 154 | orderbook, 155 | transferCap, 156 | price: 10, 157 | beneficiary: someBeneficiary, 158 | commission: 5, 159 | }); 160 | 161 | const state = await orderbookClient.fetchOrderbook(orderbook); 162 | expect(state.asks.length).toBe(1); 163 | expect(state.asks[0].price).toBe(10); 164 | expect(state.asks[0].owner).toBe(`0x${user}`); 165 | expect(state.asks[0].transferCap.nft).toBe(nft); 166 | expect(state.asks[0].commission).toStrictEqual({ 167 | beneficiary: someBeneficiary, 168 | cut: 5, 169 | }); 170 | }); 171 | 172 | test("buy generic NFT", async () => { 173 | const allNfts = await fetchGenericNfts(); 174 | expect(allNfts.length).toBeGreaterThan(0); 175 | const nft = allNfts[0]; 176 | 177 | const { safe: sellerSafe, ownerCap: sellerOwnerCap } = 178 | await safeClient.createSafeForSender(); 179 | await safeClient.depositGenericNft({ 180 | safe: sellerSafe, 181 | nft, 182 | collection: NFT_GENERIC_TYPE, 183 | }); 184 | const { transferCap } = 185 | await safeClient.createExclusiveTransferCapForSender({ 186 | safe: sellerSafe, 187 | nft, 188 | ownerCap: sellerOwnerCap, 189 | }); 190 | const { orderbook } = await orderbookClient.createOrderbook({ 191 | collection: NFT_GENERIC_TYPE, 192 | ft: SUI_TYPE_ARG, 193 | }); 194 | 195 | await orderbookClient.createAsk({ 196 | sellerSafe, 197 | collection: NFT_GENERIC_TYPE, 198 | ft: SUI_TYPE_ARG, 199 | orderbook, 200 | transferCap, 201 | price: 10, 202 | }); 203 | 204 | const state = await orderbookClient.fetchOrderbook(orderbook); 205 | expect(state.asks.length).toBe(1); 206 | expect(state.asks[0].price).toBe(10); 207 | expect(state.asks[0].owner).toBe(`0x${user}`); 208 | expect(state.asks[0].transferCap.nft).toBe(nft); 209 | expect(state.asks[0].transferCap.isGeneric).toBe(true); 210 | 211 | const { safe: buyerSafe, ownerCap: buyerOwnerCap } = 212 | await safeClient.createSafeForSender(); 213 | 214 | await orderbookClient.buyGenericNft({ 215 | buyerSafe, 216 | collection: NFT_GENERIC_TYPE, 217 | ft: SUI_TYPE_ARG, 218 | nft, 219 | orderbook, 220 | price: 10, 221 | sellerSafe, 222 | wallet: await getGas(), 223 | }); 224 | 225 | await safeClient.createTransferCapForSender({ 226 | safe: buyerSafe, 227 | nft, 228 | ownerCap: buyerOwnerCap, 229 | }); 230 | }); 231 | 232 | test("cancel bid", async () => { 233 | const { safe } = await safeClient.createSafeForSender(); 234 | 235 | const { orderbook } = await orderbookClient.createOrderbook({ 236 | collection: TESTRACT_C_TYPE, 237 | ft: SUI_TYPE_ARG, 238 | }); 239 | 240 | const wallet = await getGas(); 241 | 242 | await orderbookClient.createBid({ 243 | buyerSafe: safe, 244 | collection: TESTRACT_C_TYPE, 245 | ft: SUI_TYPE_ARG, 246 | orderbook, 247 | price: 10, 248 | wallet, 249 | }); 250 | 251 | await orderbookClient.cancelBid({ 252 | collection: TESTRACT_C_TYPE, 253 | ft: SUI_TYPE_ARG, 254 | orderbook, 255 | price: 10, 256 | wallet, 257 | }); 258 | }); 259 | 260 | test("cancel ask", async () => { 261 | const allNfts = await fetchNfts(); 262 | expect(allNfts.length).toBeGreaterThan(0); 263 | const nft = allNfts[0]; 264 | const { safe, ownerCap } = await safeClient.createSafeForSender(); 265 | await safeClient.depositNft({ 266 | safe, 267 | nft, 268 | collection: TESTRACT_C_TYPE, 269 | }); 270 | const { transferCap } = 271 | await safeClient.createExclusiveTransferCapForSender({ 272 | safe, 273 | nft, 274 | ownerCap, 275 | }); 276 | 277 | const { orderbook } = await orderbookClient.createOrderbook({ 278 | collection: TESTRACT_C_TYPE, 279 | ft: SUI_TYPE_ARG, 280 | }); 281 | 282 | await orderbookClient.createAsk({ 283 | sellerSafe: safe, 284 | collection: TESTRACT_C_TYPE, 285 | ft: SUI_TYPE_ARG, 286 | orderbook, 287 | transferCap, 288 | price: 10, 289 | }); 290 | 291 | await orderbookClient.cancelAsk({ 292 | collection: TESTRACT_C_TYPE, 293 | ft: SUI_TYPE_ARG, 294 | nft, 295 | orderbook, 296 | price: 10, 297 | }); 298 | }); 299 | 300 | // this test depends on all the other tests running first 301 | test("events are emitted", async () => { 302 | const { events } = await orderbookClient.fetchEvents({ 303 | packageId: NFT_PROTOCOL_ADDRESS!, 304 | }); 305 | 306 | expect(events.length).toBeGreaterThan(0); 307 | expect( 308 | events.find((event) => "BidCreatedEvent" in event.data) 309 | ).toBeTruthy(); 310 | expect( 311 | events.find((event) => "AskCreatedEvent" in event.data) 312 | ).toBeTruthy(); 313 | expect( 314 | events.find((event) => "OrderbookCreatedEvent" in event.data) 315 | ).toBeTruthy(); 316 | expect(events.find((event) => "AskClosedEvent" in event.data)).toBeTruthy(); 317 | expect(events.find((event) => "BidClosedEvent" in event.data)).toBeTruthy(); 318 | }); 319 | } 320 | -------------------------------------------------------------------------------- /__tests__/safe.ts: -------------------------------------------------------------------------------- 1 | import { 2 | TESTRACT_C_TYPE, 3 | NFT_GENERIC_TYPE, 4 | safeClient, 5 | user, 6 | fetchNfts, 7 | fetchGenericNfts, 8 | } from "./common"; 9 | 10 | export default function suite() { 11 | test("create safe", async () => { 12 | const { safe, ownerCap, effects } = await safeClient.createSafeForSender(); 13 | 14 | expect(effects.created?.length).toBe(2); 15 | if ( 16 | typeof effects.created![0].owner === "object" && 17 | "Shared" in effects.created![0].owner 18 | ) { 19 | expect(effects.created![0].reference.objectId).toBe(safe); 20 | expect(effects.created![1].reference.objectId).toBe(ownerCap); 21 | } else { 22 | expect(effects.created![0].reference.objectId).toBe(ownerCap); 23 | expect(effects.created![1].reference.objectId).toBe(safe); 24 | } 25 | 26 | const ownerCaps = await safeClient.fetchOwnerCapsIds(user); 27 | expect(ownerCaps.length).toBe(1); 28 | expect(ownerCaps[0]).toBe(ownerCap); 29 | 30 | expect(await safeClient.fetchOwnerCapSafeId(ownerCap)).toBe(safe); 31 | 32 | const state = await safeClient.fetchSafeByOwnerCap(ownerCap); 33 | expect(state.id).toBe(safe); 34 | }); 35 | 36 | test("restrict and enable deposits", async () => { 37 | const { safe, ownerCap } = await safeClient.createSafeForSender(); 38 | const stateOne = await safeClient.fetchSafe(safe); 39 | expect(stateOne.enableAnyDeposits).toBe(true); 40 | 41 | await safeClient.restrictDeposits({ safe, ownerCap }); 42 | const stateTwo = await safeClient.fetchSafe(safe); 43 | expect(stateTwo.enableAnyDeposits).toBe(false); 44 | 45 | await safeClient.enableAnyDeposit({ safe, ownerCap }); 46 | const stateThree = await safeClient.fetchSafe(safe); 47 | expect(stateThree.enableAnyDeposits).toBe(true); 48 | }); 49 | 50 | test("restrict and enable deposits of specific collection", async () => { 51 | const { safe, ownerCap } = await safeClient.createSafeForSender(); 52 | const stateOne = await safeClient.fetchSafe(safe); 53 | expect(stateOne.collectionsWithEnabledDeposits.length).toBe(0); 54 | 55 | await safeClient.enableDepositsOfCollection({ 56 | safe, 57 | ownerCap, 58 | collection: TESTRACT_C_TYPE, 59 | }); 60 | const stateTwo = await safeClient.fetchSafe(safe); 61 | expect(stateTwo.collectionsWithEnabledDeposits.length).toBe(1); 62 | expect(stateTwo.collectionsWithEnabledDeposits[0]).toBe(TESTRACT_C_TYPE); 63 | 64 | await safeClient.disableDepositsOfCollection({ 65 | safe, 66 | ownerCap, 67 | collection: TESTRACT_C_TYPE, 68 | }); 69 | const stateThree = await safeClient.fetchSafe(safe); 70 | expect(stateThree.collectionsWithEnabledDeposits.length).toBe(0); 71 | }); 72 | 73 | test("deposit NFT", async () => { 74 | const allNfts = await fetchNfts(); 75 | expect(allNfts.length).toBeGreaterThan(3); 76 | const nfts = allNfts.slice(0, 3); 77 | const { safe, ownerCap } = await safeClient.createSafeForSender(); 78 | 79 | await safeClient.depositNftPrivileged({ 80 | safe, 81 | ownerCap, 82 | nft: nfts[0], 83 | collection: TESTRACT_C_TYPE, 84 | }); 85 | for (const nft of nfts.slice(1)) { 86 | await safeClient.depositNft({ 87 | safe, 88 | nft, 89 | collection: TESTRACT_C_TYPE, 90 | }); 91 | } 92 | 93 | const state = await safeClient.fetchSafe(safe); 94 | expect(state.nfts.length).toBe(nfts.length); 95 | nfts.forEach((nft) => { 96 | const needle = state.nfts.find((needle) => needle.id === nft)!; 97 | expect(needle).toBeTruthy(); 98 | expect(needle.id).toBe(nft); 99 | expect(needle.isExclusivelyListed).toBe(false); 100 | expect(needle.transferCapsCount).toBe(0); 101 | expect(needle.version).toBeTruthy(); 102 | }); 103 | }); 104 | 105 | test("deposit generic NFT", async () => { 106 | const allNfts = await fetchGenericNfts(); 107 | expect(allNfts.length).toBeGreaterThan(5); 108 | const nfts = allNfts.slice(0, 5); 109 | 110 | const { safe, ownerCap } = await safeClient.createSafeForSender(); 111 | 112 | await safeClient.depositGenericNftPrivileged({ 113 | safe, 114 | ownerCap, 115 | nft: nfts[0], 116 | collection: NFT_GENERIC_TYPE, 117 | }); 118 | for (const nft of nfts.slice(1)) { 119 | await safeClient.depositGenericNft({ 120 | safe, 121 | nft, 122 | collection: NFT_GENERIC_TYPE, 123 | }); 124 | } 125 | 126 | const state = await safeClient.fetchSafe(safe); 127 | expect(state.nfts.length).toBe(nfts.length); 128 | nfts.forEach((nft) => { 129 | const needle = state.nfts.find((needle) => needle.id === nft)!; 130 | expect(needle).toBeTruthy(); 131 | expect(needle.id).toBe(nft); 132 | expect(needle.isExclusivelyListed).toBe(false); 133 | expect(needle.transferCapsCount).toBe(0); 134 | expect(needle.version).toBeTruthy(); 135 | }); 136 | }); 137 | 138 | test("transfer cap operations", async () => { 139 | const nft = (await fetchNfts())[0]; 140 | expect(nft).not.toBeUndefined(); 141 | const { safe, ownerCap } = await safeClient.createSafeForSender(); 142 | 143 | await safeClient.depositNft({ 144 | safe, 145 | nft, 146 | collection: TESTRACT_C_TYPE, 147 | }); 148 | 149 | const { transferCap } = await safeClient.createTransferCapForSender({ 150 | safe, 151 | nft, 152 | ownerCap, 153 | }); 154 | const safeState = await safeClient.fetchSafe(safe); 155 | expect(safeState.nfts.length).toBe(1); 156 | expect(safeState.nfts[0].transferCapsCount).toBe(1); 157 | expect(safeState.nfts[0].isExclusivelyListed).toBe(false); 158 | 159 | const transferCapState = await safeClient.fetchTransferCap(transferCap); 160 | expect(transferCapState.safe).toBe(safe); 161 | expect(transferCapState.isExclusivelyListed).toBe(false); 162 | expect(transferCapState.nft).toBe(nft); 163 | expect(transferCapState.isGeneric).toBe(false); 164 | expect(transferCapState.version).toBe(safeState.nfts[0].version); 165 | 166 | await safeClient.delistNft({ safe, nft, ownerCap }); 167 | const safeStateAfterDelist = await safeClient.fetchSafe(safe); 168 | expect(safeStateAfterDelist.nfts.length).toBe(1); 169 | expect(transferCapState.version).not.toBe( 170 | safeStateAfterDelist.nfts[0].version 171 | ); 172 | 173 | await safeClient.burnTransferCap({ safe, transferCap }); 174 | try { 175 | await safeClient.fetchTransferCap(transferCap); 176 | fail("Transfer cap should be burned"); 177 | } catch (error) { 178 | expect(error.message).toContain("does not exist"); 179 | } 180 | }); 181 | 182 | test("exclusive transfer cap operations", async () => { 183 | const nft = (await fetchNfts())[0]; 184 | expect(nft).not.toBeUndefined(); 185 | const { safe, ownerCap } = await safeClient.createSafeForSender(); 186 | 187 | await safeClient.depositNft({ 188 | safe, 189 | nft, 190 | collection: TESTRACT_C_TYPE, 191 | }); 192 | 193 | const { transferCap } = 194 | await safeClient.createExclusiveTransferCapForSender({ 195 | safe, 196 | nft, 197 | ownerCap, 198 | }); 199 | const safeState = await safeClient.fetchSafe(safe); 200 | expect(safeState.nfts.length).toBe(1); 201 | expect(safeState.nfts[0].transferCapsCount).toBe(1); 202 | expect(safeState.nfts[0].isExclusivelyListed).toBe(true); 203 | 204 | const transferCapState = await safeClient.fetchTransferCap(transferCap); 205 | expect(transferCapState.safe).toBe(safe); 206 | expect(transferCapState.isExclusivelyListed).toBe(true); 207 | expect(transferCapState.nft).toBe(nft); 208 | expect(transferCapState.version).toBeTruthy(); 209 | }); 210 | } 211 | -------------------------------------------------------------------------------- /__tests__/stresstest.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Keeps trading NFTs between bunch of saves. 3 | * 4 | * ENV variables: 5 | * - RPC_ENDPOINT - RPC endpoint to use, defaults to "LOCAL" 6 | * - MNEMONIC - mnemonic to use, defaults to one with pubkey of 0xddcdd8e07b59852f58ba8db8daff1b585d2fca23 7 | * - TESTRACT_ADDRESS - address of testract package on the chain of RPC_ENDPOINT 8 | * - NFT_PROTOCOL_ADDRESS - address of testract package NFT dependency on the chain of RPC_ENDPOINT 9 | */ 10 | 11 | console.warn = () => {}; 12 | require("dotenv").config(); 13 | 14 | import { 15 | Connection, 16 | Ed25519Keypair, 17 | JsonRpcProvider, 18 | ObjectId, 19 | } from "@mysten/sui.js"; 20 | import { SafeFullClient, OrderbookFullClient } from "../src"; 21 | 22 | // Initial setup 23 | const SAVES_WITH_NFTS_COUNT = 5; 24 | const NFTS_PER_SAFE = 5; 25 | const SAVES_WITHOUT_NFTS_COUNT = 15; 26 | // After each tick, in which we trade NFTs, we sleep for this amount of time 27 | const SLEEP_AFTER_TICK_MS = 0; 28 | const MAX_CONSEQUENT_ERRORS_COUNT = 3; 29 | const SLEEP_AFTER_FIRST_ERROR_MS = 1000; 30 | const DEFAULT_GAS_BUDGET = 100_000; 31 | const TESTRACT_ADDRESS = process.env.TESTRACT_ADDRESS; 32 | const TESTRACT_OTW_TYPE = `${TESTRACT_ADDRESS}::testract::TESTRACT`; 33 | const TESTRACT_C_TYPE = TESTRACT_OTW_TYPE; 34 | const NFT_PROTOCOL_ADDRESS = process.env.NFT_PROTOCOL_ADDRESS; 35 | const ENV = process.env.RPC_ENDPOINT || "LOCAL"; 36 | const KEYPAIR = process.env.MNEMONIC 37 | ? Ed25519Keypair.deriveKeypair(process.env.MNEMONIC) 38 | : new Ed25519Keypair({ 39 | publicKey: Uint8Array.from([ 40 | 123, 49, 136, 138, 93, 52, 142, 26, 32, 156, 52, 154, 223, 80, 191, 2, 41 | 136, 183, 246, 194, 17, 192, 124, 120, 97, 137, 189, 25, 225, 196, 206, 42 | 252, 43 | ]), 44 | secretKey: Uint8Array.from([ 45 | 247, 8, 180, 26, 178, 76, 142, 156, 80, 194, 241, 66, 143, 182, 235, 46 | 102, 66, 242, 47, 157, 43, 116, 165, 212, 124, 189, 163, 59, 11, 212, 47 | 187, 138, 123, 49, 136, 138, 93, 52, 142, 26, 32, 156, 52, 154, 223, 80, 48 | 191, 2, 136, 183, 246, 194, 17, 192, 124, 120, 97, 137, 189, 25, 225, 49 | 196, 206, 252, 50 | ]), 51 | }); 52 | console.log("Using keypair", KEYPAIR.getPublicKey().toSuiAddress()); 53 | const safeClient = SafeFullClient.fromKeypair( 54 | KEYPAIR, 55 | new JsonRpcProvider(new Connection({ fullnode: ENV })), 56 | { 57 | packageObjectId: NFT_PROTOCOL_ADDRESS, 58 | } 59 | ); 60 | const orderbookClient = OrderbookFullClient.fromKeypair( 61 | KEYPAIR, 62 | safeClient.client.provider, 63 | { 64 | packageObjectId: NFT_PROTOCOL_ADDRESS, 65 | } 66 | ); 67 | 68 | /** 69 | * Taken from https://www.npmjs.com/package/random 70 | * 71 | * https://en.wikipedia.org/wiki/Normal_distribution 72 | */ 73 | function normalDistribution(mu: number, sigma: number) { 74 | let x: number, y: number, r: number; 75 | do { 76 | x = Math.random() * 2 - 1; 77 | y = Math.random() * 2 - 1; 78 | r = x * x + y * y; 79 | } while (!r || r > 1); 80 | return mu + sigma * y * Math.sqrt((-2 * Math.log(r)) / r); 81 | } 82 | 83 | // Price distributions 84 | const BID_DISTRIBUTION = () => Math.round(normalDistribution(510, 70)); 85 | const ASK_DISTRIBUTION = () => Math.round(normalDistribution(520, 50)); 86 | 87 | const tradeIntermediaries: string[] = []; 88 | 89 | async function createSafeWithNfts() { 90 | const { safe, ownerCap } = await safeClient.createSafeForSender(); 91 | if (ENV !== "LOCAL") { 92 | await new Promise((resolve) => setTimeout(resolve, 500)); 93 | } 94 | 95 | await safeClient.client.sendTxWaitForEffects({ 96 | packageObjectId: TESTRACT_ADDRESS!, 97 | module: "testract", 98 | function: "mint_n_nfts", 99 | typeArguments: [], 100 | arguments: [String(NFTS_PER_SAFE), safe], 101 | gasBudget: DEFAULT_GAS_BUDGET * 4, 102 | }); 103 | 104 | return { safe, ownerCap }; 105 | } 106 | 107 | async function createAsk( 108 | nftToList: ObjectId, 109 | orderbook: ObjectId, 110 | safe: ObjectId, 111 | ownerCap: ObjectId 112 | ) { 113 | const { transferCap } = await safeClient.createExclusiveTransferCapForSender({ 114 | safe, 115 | ownerCap, 116 | nft: nftToList, 117 | }); 118 | 119 | const price = ASK_DISTRIBUTION(); 120 | const { trade } = await orderbookClient.createAsk({ 121 | collection: TESTRACT_C_TYPE, 122 | ft: TESTRACT_OTW_TYPE, 123 | orderbook, 124 | price, 125 | sellerSafe: safe, 126 | transferCap, 127 | }); 128 | 129 | if (trade) { 130 | tradeIntermediaries.push(trade); 131 | } 132 | } 133 | 134 | async function createBid( 135 | treasury: ObjectId, 136 | orderbook: ObjectId, 137 | safe: ObjectId 138 | ) { 139 | const price = BID_DISTRIBUTION(); 140 | const { created } = await orderbookClient.client.sendTxWaitForEffects({ 141 | packageObjectId: TESTRACT_ADDRESS!, 142 | module: "testract", 143 | function: "create_bid", 144 | typeArguments: [], 145 | arguments: [String(price), safe, treasury, orderbook], 146 | gasBudget: DEFAULT_GAS_BUDGET, 147 | }); 148 | 149 | const trade = created?.find( 150 | (obj) => typeof obj.owner === "object" && "Shared" in obj.owner 151 | ); 152 | if (trade) { 153 | tradeIntermediaries.push(trade.reference.objectId); 154 | } 155 | } 156 | 157 | async function getGlobalObjects(): Promise<{ 158 | treasury: ObjectId; 159 | allowlist: ObjectId; 160 | }> { 161 | console.log("Getting treasury and allowlist..."); 162 | 163 | const objs = await orderbookClient.client.getObjects( 164 | KEYPAIR.getPublicKey().toSuiAddress() 165 | ); 166 | 167 | const treasury = objs.find( 168 | (obj) => 169 | obj.type.includes("TreasuryCap") && 170 | // https://github.com/MystenLabs/sui/issues/8017 171 | obj.type.includes(TESTRACT_ADDRESS!.replace("0x0", "0x")) 172 | )?.objectId; 173 | if (!treasury) { 174 | objs 175 | .filter((obj) => obj.type.includes("TreasuryCap")) 176 | .forEach((obj) => console.log(obj.type)); 177 | throw new Error("Treasury not found"); 178 | } 179 | 180 | const allowlists = objs 181 | .filter( 182 | (obj) => 183 | // https://github.com/MystenLabs/sui/issues/8017 184 | obj.type === 185 | `${NFT_PROTOCOL_ADDRESS!.replace( 186 | "0x0", 187 | "0x" 188 | )}::transfer_allowlist::Allowlist` 189 | ) 190 | .map((o) => o.objectId); 191 | 192 | while (allowlists.length !== 0) { 193 | const allowlist = allowlists.pop()!; 194 | const { data } = (await orderbookClient.client.getObject(allowlist)) as any; 195 | const hasTestract = data.fields.collections.fields.contents.some( 196 | ({ fields }: any) => fields.name.startsWith(TESTRACT_ADDRESS?.slice(2)) 197 | ); 198 | if (hasTestract) { 199 | return { treasury, allowlist }; 200 | } 201 | } 202 | 203 | throw new Error("Allowlist not found"); 204 | } 205 | 206 | async function finishAllTrades(allowlist: ObjectId) { 207 | while (tradeIntermediaries.length !== 0) { 208 | const trade = tradeIntermediaries.pop()!; 209 | const { transferCap, buyerSafe } = 210 | await orderbookClient.fetchTradeIntermediary(trade); 211 | if (!transferCap) { 212 | throw new Error("Expected transfer cap to be present"); 213 | } 214 | 215 | const { tradePayments } = await orderbookClient.finishTrade({ 216 | trade, 217 | allowlist, 218 | collection: TESTRACT_C_TYPE, 219 | ft: TESTRACT_OTW_TYPE, 220 | buyerSafe, 221 | sellerSafe: transferCap.safe, 222 | }); 223 | if (tradePayments.length !== 1) { 224 | throw new Error("Expected exactly one TradePayment object"); 225 | } 226 | if (ENV !== "LOCAL") { 227 | await new Promise((resolve) => setTimeout(resolve, 500)); 228 | } 229 | 230 | await safeClient.client.sendTxWaitForEffects({ 231 | packageObjectId: TESTRACT_ADDRESS!, 232 | module: "testract", 233 | function: "collect_royalty", 234 | typeArguments: [], 235 | arguments: [tradePayments[0]], 236 | gasBudget: DEFAULT_GAS_BUDGET, 237 | }); 238 | } 239 | } 240 | 241 | async function tick( 242 | orderbook: ObjectId, 243 | treasury: ObjectId, 244 | allowlist: ObjectId, 245 | saves: Array<{ safe: ObjectId; ownerCap: ObjectId }> 246 | ) { 247 | const safeIndex = Math.floor(Math.random() * saves.length); 248 | const { safe, ownerCap } = saves[safeIndex]; 249 | 250 | const { nfts } = await safeClient.fetchSafe(safe); 251 | const nftToList = nfts.find((nft) => nft.transferCapsCount === 0); 252 | 253 | // we don't want to execute a trade where both seller and buyer is the same 254 | // safe 255 | const { asks, bids } = await orderbookClient.fetchOrderbook( 256 | orderbook, 257 | true // sort 258 | ); 259 | 260 | if (nftToList && !(bids.length > 0 && bids[0].safe === safe)) { 261 | await createAsk(nftToList.id, orderbook, safe, ownerCap); 262 | } else if (!(asks.length > 0 && asks[0].transferCap.safe === safe)) { 263 | await createBid(treasury, orderbook, safe); 264 | } 265 | 266 | if (SLEEP_AFTER_TICK_MS) { 267 | await new Promise((resolve) => setTimeout(resolve, SLEEP_AFTER_TICK_MS)); 268 | } 269 | 270 | await finishAllTrades(allowlist); 271 | } 272 | 273 | async function start( 274 | orderbook: ObjectId, 275 | treasury: ObjectId, 276 | allowlist: ObjectId, 277 | saves: Array<{ safe: ObjectId; ownerCap: ObjectId }> 278 | ) { 279 | let consequentErrors = 0; 280 | while (true) { 281 | try { 282 | await tick(orderbook, treasury, allowlist, saves); 283 | 284 | consequentErrors = 0; 285 | } catch (error) { 286 | console.error(error); 287 | consequentErrors += 1; 288 | 289 | if (consequentErrors > MAX_CONSEQUENT_ERRORS_COUNT) { 290 | break; 291 | } 292 | 293 | await new Promise((resolve) => 294 | setTimeout(resolve, SLEEP_AFTER_FIRST_ERROR_MS * consequentErrors) 295 | ); 296 | } 297 | } 298 | 299 | console.log(); 300 | console.log(); 301 | console.error("Terminated due to too many errors"); 302 | } 303 | 304 | async function main() { 305 | const { treasury, allowlist } = await getGlobalObjects(); 306 | 307 | const { orderbook } = await orderbookClient.createOrderbook({ 308 | ft: TESTRACT_OTW_TYPE, 309 | collection: TESTRACT_C_TYPE, 310 | }); 311 | 312 | const saves: Array<{ safe: ObjectId; ownerCap: ObjectId }> = []; 313 | console.log(`Creating ${SAVES_WITHOUT_NFTS_COUNT} empty safes...`); 314 | for (let i = 0; i < SAVES_WITHOUT_NFTS_COUNT; i++) { 315 | const { safe, ownerCap } = await safeClient.createSafeForSender(); 316 | saves.push({ safe, ownerCap }); 317 | } 318 | console.log(`Creating ${SAVES_WITH_NFTS_COUNT} safes with NFTs...`); 319 | for (let i = 0; i < SAVES_WITH_NFTS_COUNT; i++) { 320 | saves.push(await createSafeWithNfts()); 321 | } 322 | 323 | console.log("Trading begins..."); 324 | await start(orderbook, treasury, allowlist, saves); 325 | } 326 | 327 | main(); 328 | -------------------------------------------------------------------------------- /__tests__/tsconfig.stress.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "../dist-stress", 4 | "sourceMap": false, 5 | "module": "commonjs", 6 | "moduleResolution": "node", 7 | "noEmitOnError": true, 8 | "esModuleInterop": true, 9 | "target": "es2017", 10 | "declaration": false 11 | }, 12 | "include": ["stresstest.ts"] 13 | } 14 | -------------------------------------------------------------------------------- /bin/build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -ex 2 | 3 | yarn typecheck 4 | yarn lint 5 | 6 | rm -rf dist 7 | npm run tsc -- -p . 8 | -------------------------------------------------------------------------------- /bin/setup-test-env.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | # set SKIP_DEPS env to skip fetching dependencies and deploying nft-protocol 6 | # set TEST_VALIDATOR_CONFIG env to use a custom validator config 7 | # set SUI_BIN env to use a custom sui binary 8 | 9 | source __tests__/.env.test.sh 10 | 11 | # current dir 12 | root="$(pwd)" 13 | # defined by mnemonic in test consts 14 | test_addr="ddcdd8e07b59852f58ba8db8daff1b585d2fca23" 15 | # path to test assets 16 | test_assets_dir="${root}/__tests__/assets" 17 | test_assets_tmp_dir="${test_assets_dir}/.tmp" 18 | nft_protocol_dir="${test_assets_tmp_dir}/nft-protocol" 19 | originmate_dir="${test_assets_tmp_dir}/originmate" 20 | test_validator_config=${TEST_VALIDATOR_CONFIG:-"${test_assets_tmp_dir}/localnet/client.yaml"} 21 | sui_bin=${SUI_BIN:-"${test_assets_tmp_dir}/sui"} 22 | 23 | mkdir -p "${test_assets_tmp_dir}" 24 | 25 | installed_sui_version=$($sui_bin --version) 26 | if [[ "${installed_sui_version}" != "${SUI_VERSION}" ]]; then 27 | echo "ERROR: expected sui version '${SUI_VERSION}'" 28 | echo "But running '$ sui --version' returned:" 29 | echo 30 | echo "${installed_sui_version}" 31 | echo 32 | exit 1 33 | fi 34 | 35 | # check for dependencies 36 | toml --version &>/dev/null || 37 | (echo "ERROR: missing dependency toml" && echo "\$ cargo install toml-cli" && exit 1) 38 | curl --version &>/dev/null || (echo "ERROR: missing dependency curl" && exit 1) 39 | jq --version &>/dev/null || (echo "ERROR: missing dependency jq" && exit 1) 40 | git --version &>/dev/null || (echo "ERROR: missing dependency git" && exit 1) 41 | 42 | sui_validator_http="http://0.0.0.0:9000" 43 | # exit if sui local validator not running 44 | curl --silent \ 45 | --request POST "${sui_validator_http}" \ 46 | --header 'Content-Type: application/json' \ 47 | --data-raw '{ "jsonrpc":"2.0", "method":"rpc.discover","id":1}' 1>/dev/null || 48 | (echo "Sui local validator not running on ${sui_validator_http}" && exit 1) 49 | 50 | function deploy_package { 51 | # @arg path to package 52 | # @returns the address of the published module 53 | 54 | package_dir="${1}" 55 | 56 | publish_output=$($sui_bin client --client.config "${test_validator_config}" \ 57 | publish \ 58 | --skip-dependency-verification \ 59 | --gas-budget 30000 \ 60 | --json \ 61 | "${package_dir}") 62 | 63 | # if last command failed, print 64 | if [ $? -ne 0 ]; then 65 | echo >&2 "Cannot deploy ${package_dir}:" 66 | echo >&2 67 | echo >&2 68 | echo >&2 "${publish_output}" 69 | exit 1 70 | fi 71 | 72 | echo "${publish_output}" | 73 | jq -r '.effects.created[] | select( .owner == "Immutable" ) | .reference.objectId' 74 | } 75 | 76 | # if env "SKIP_DEPS" is not set, fetch dependencies 77 | if [ -z "${SKIP_DEPS}" ]; then 78 | echo "Fetching nft-protocol dependency" 79 | rm -rf "${test_assets_tmp_dir}/nft-protocol" 80 | git clone --quiet --depth 1 "git@github.com:Origin-Byte/nft-protocol.git" "${nft_protocol_dir}" 81 | cd "${nft_protocol_dir}" 82 | git fetch --quiet --depth 1 origin "${NFT_PROTOCOL_REV}" 83 | git checkout --quiet "${NFT_PROTOCOL_REV}" 84 | originmate_rev=$( 85 | toml get "${nft_protocol_dir}/Move.toml" dependencies.Originmate.rev | 86 | tr -d '"' 87 | ) 88 | 89 | echo "Fetching originmate dependency (rev ${originmate_rev})" 90 | rm -rf "${test_assets_tmp_dir}/originmate" 91 | git clone --quiet --depth 1 "git@github.com:Origin-Byte/originmate.git" "${originmate_dir}" 92 | cd "${originmate_dir}" 93 | git fetch --quiet --depth 1 origin "${originmate_rev}" 94 | git checkout --quiet "${originmate_rev}" 95 | 96 | cd "${root}" 97 | 98 | echo 99 | echo "Deploying originmate to local validator" 100 | # is publishable only of the addr is 0x0 101 | sed -i -r 's/originmate = "0x(.+)/originmate = "0x0"/' \ 102 | "${originmate_dir}/Move.toml" 103 | originmate_address=$(deploy_package "${originmate_dir}") 104 | if [ -z "${originmate_address}" ]; then 105 | echo "Failed to deploy originmate to local validator" 106 | exit 1 107 | fi 108 | 109 | echo "Using originmate address '${originmate_address}'" 110 | # in originmate manifest so that nft-protocol can use it as a dep 111 | sed -i -r "s/originmate = \"0x0\"/originmate = \"${originmate_address}\"/" \ 112 | "${originmate_dir}/Move.toml" 113 | # in nft-protocol manifest so that it can be published (otherwise missing dep) 114 | sed -i -r 's/nft_protocol = "0x(.+)/nft_protocol = "0x0"/' \ 115 | "${nft_protocol_dir}/Move.toml" 116 | # nft-protocol will point to local copy of originmate instead of the git one 117 | # piping directly to tee doesn't work (perhaps there are two streams) 118 | new_manifest=$( 119 | toml set "${nft_protocol_dir}/Move.toml" \ 120 | dependencies.Originmate \ 121 | "REPLACE" 122 | ) 123 | echo "${new_manifest/\"REPLACE\"/\{ local = \"../originmate\" \}}" | 124 | tee "${nft_protocol_dir}/Move.toml" >/dev/null 125 | # is publishable only of the addr is 0x0 126 | sed -i -r 's/nft_protocol = "0x(.+)/nft_protocol = "0x0"/' \ 127 | "${nft_protocol_dir}/Move.toml" 128 | 129 | echo 130 | echo "Deploying nft-protocol to local validator" 131 | nft_protocol_address=$(deploy_package "${nft_protocol_dir}") 132 | echo "Using nft-profocol address '${nft_protocol_address}'" 133 | # in nft_protocol manifest so that testract can use it as a dep 134 | sed -i -r "s/nft_protocol = \"0x0\"/nft_protocol = \"${nft_protocol_address}\"/" \ 135 | "${nft_protocol_dir}/Move.toml" 136 | 137 | coin_obj_count=$( 138 | $sui_bin client --client.config "${test_validator_config}" objects "${test_addr}" --json | 139 | jq 'map( select( .type | contains("Coin") ) ) | length' 140 | ) 141 | # transfer some coins to test user if they don't have any 142 | if [ "${coin_obj_count}" -eq 0 ]; then 143 | echo "Transfering SUI to test user" 144 | 145 | # one for gas, one for SUI wallet 146 | for _i in {1..2}; do 147 | coin_id=$( 148 | $sui_bin client --client.config "${test_validator_config}" gas --json | 149 | jq -r '.[0].id.id' 150 | ) 151 | $sui_bin client --client.config "${test_validator_config}" \ 152 | pay_all_sui \ 153 | --gas-budget 100000 \ 154 | --input-coins "${coin_id}" \ 155 | --recipient "0x${test_addr}" 1>/dev/null 156 | done 157 | fi 158 | fi 159 | 160 | echo 161 | echo "Deploying testract to local validator" 162 | testract_address=$(deploy_package "${test_assets_dir}/testract") 163 | echo "Testract deployed under address '${testract_address}'" 164 | 165 | export NFT_PROTOCOL_ADDRESS=$( 166 | toml get "${nft_protocol_dir}/Move.toml" addresses.nft_protocol | 167 | tr -d '"' 168 | ) 169 | export TESTRACT_ADDRESS="${testract_address}" 170 | 171 | eval "$@" 172 | -------------------------------------------------------------------------------- /bin/start-localnet.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | source __tests__/.env.test.sh 6 | 7 | localnet_dir="__tests__/assets/.tmp/localnet" 8 | sui_bin="__tests__/assets/.tmp/sui" 9 | 10 | mkdir -p "${localnet_dir}" 11 | 12 | # if sui file does not exist, download it 13 | # TODO: check sui version as well 14 | if [ ! -f "${sui_bin}" ]; then 15 | echo "Downloading sui binary version '${SUI_TAG}'" 16 | 17 | wget "https://github.com/MystenLabs/sui/releases/download/${SUI_TAG}/sui" \ 18 | -O "${sui_bin}" -q --show-progress 19 | chmod +x "${sui_bin}" 20 | fi 21 | 22 | # check if dir exists 23 | if [ ! -f "${localnet_dir}/network.yaml" ]; then 24 | $sui_bin genesis -f --working-dir "${localnet_dir}" 25 | fi 26 | 27 | $sui_bin start --network.config "${localnet_dir}/network.yaml" 28 | -------------------------------------------------------------------------------- /examples/0-fetch-nft.ts: -------------------------------------------------------------------------------- 1 | import { CollectionParser } from "../src"; 2 | import { client, provider } from "./common"; 3 | 4 | const fetchNft = async () => { 5 | const collection = await client.getInventoryById({ 6 | inventoryId:"0x6e5d999c7224e86c3ace86813277c2c9aca462c98e1d91d118c62c4dece70a9c", 7 | }); 8 | 9 | console.log("collection", collection); 10 | }; 11 | 12 | fetchNft(); 13 | -------------------------------------------------------------------------------- /examples/0-use-parsers.ts: -------------------------------------------------------------------------------- 1 | import { 2 | AllowlistParser, 3 | CollectionParser, 4 | FixedPriceMarketParser, 5 | FlatFeeParser, 6 | InventoryParser, 7 | LimitedFixedPriceMarketParser, 8 | ListingParser, 9 | MarketplaceParser, 10 | MintCapParser, 11 | OrderbookParser, 12 | VenueParser, 13 | } from "../src"; 14 | import { client, provider } from "./common"; 15 | 16 | const useParsers = async () => { 17 | const [fee] = await client.fetchAndParseObjectsById( 18 | ["0x0b4220239e180479f118f4694ef8c8c0b91635ae"], 19 | FlatFeeParser 20 | ); 21 | console.log("fee.packageObjectId", fee.packageObjectId); 22 | const [marketplace] = await client.fetchAndParseObjectsById( 23 | ["0x638a1c1e1dea1ccf6a45b297d2fd3924f3cd78cd"], 24 | MarketplaceParser 25 | ); 26 | console.log("marketplace.packageObjectId", marketplace.packageObjectId); 27 | 28 | const [listing] = await client.fetchAndParseObjectsById( 29 | ["0xf78576498556a856f86bdb513dfd4e57ba0b6eb9"], 30 | ListingParser 31 | ); 32 | console.log("listing.packageObjectId", listing.packageObjectId); 33 | 34 | const [orderbook] = await client.fetchAndParseObjectsById( 35 | ["0x03db742286dbd75d12c6746422e685f1942d9796"], 36 | OrderbookParser 37 | ); 38 | console.log("orderbook.packageObjectId", orderbook.packageObjectId); 39 | 40 | const [inventory] = await client.fetchAndParseObjectsById( 41 | ["0x9418fd7f0906d444262aa081a30c75761a7bd253"], 42 | InventoryParser 43 | ); 44 | console.log("inventory.packageObjectId", inventory.packageObjectId); 45 | 46 | const [venue] = await client.fetchAndParseObjectsById( 47 | ["0xbb0a3aedaee230a2df3e2defb5e1f7f7650618f1"], 48 | VenueParser 49 | ); 50 | console.log("venue.packageObjectId", venue.packageObjectId); 51 | 52 | const marketObj = await provider.getObject({ 53 | id: "0xc22904b27db6a160640bfb81d95319d2cab341ea", 54 | }); 55 | const [openMarket] = await client.parseObjects( 56 | [marketObj], 57 | FixedPriceMarketParser 58 | ); 59 | const [fixedMarket] = await client.parseObjects( 60 | [marketObj], 61 | LimitedFixedPriceMarketParser 62 | ); 63 | 64 | const market = openMarket || fixedMarket; 65 | console.log("market.packageObjectId", market.packageObjectId); 66 | 67 | const [collection] = await client.fetchAndParseObjectsById( 68 | ["0xca5e9b706359c0a1f8ef766dd4811c0110c7de2b"], 69 | CollectionParser 70 | ); 71 | console.log( 72 | "collection.packageObjectId", 73 | collection.packageObjectId, 74 | collection.nftProtocolPackageObjectId 75 | ); 76 | 77 | const [mintCap] = await client.fetchAndParseObjectsById( 78 | ["0x273c0cfb47456d8bdeabdeb7b95a2c600a5486ab"], 79 | MintCapParser 80 | ); 81 | 82 | console.log("mintCap.packageObjectId", mintCap.packageObjectId); 83 | 84 | const [allowList] = await client.fetchAndParseObjectsById( 85 | ["0x1c7d9438358f09317beb7320cc7f63b4d915489a"], 86 | AllowlistParser 87 | ); 88 | 89 | console.log("allowList.packageObjectId", allowList.packageObjectId); 90 | }; 91 | 92 | useParsers(); 93 | -------------------------------------------------------------------------------- /examples/1-create-fee.ts: -------------------------------------------------------------------------------- 1 | import { NftClient } from "../src"; 2 | import { PACKAGE_OBJECT_ID, signer } from "./common"; 3 | 4 | export const createFee = async () => { 5 | const [transactionBlock] = NftClient.buildCreateFlatFee({ 6 | packageObjectId: PACKAGE_OBJECT_ID, 7 | rate: 10000000, 8 | }); 9 | const createFeeResult = await signer.signAndExecuteTransactionBlock({ 10 | transactionBlock, 11 | options: { showEffects: true, showObjectChanges: true }, 12 | }); 13 | console.log("createFeeResult", JSON.stringify(createFeeResult)); 14 | }; 15 | 16 | createFee(); 17 | -------------------------------------------------------------------------------- /examples/10-buy-from-market.ts: -------------------------------------------------------------------------------- 1 | import { NftClient } from "../src"; 2 | import { 3 | LISTING_ID, 4 | NFT_TYPE, 5 | PACKAGE_OBJECT_ID, 6 | signer, 7 | VENUE_ID, 8 | } from "./common"; 9 | 10 | const buyFromLaunchpad = async () => { 11 | console.log("Address", await signer.getAddress()); 12 | const [buyCertificateTransaction] = NftClient.buildBuyNft({ 13 | packageObjectId: PACKAGE_OBJECT_ID, 14 | nftType: NFT_TYPE, 15 | coin: "0x12ddc56e41944a83893ef99df474b2f0d249e05f9ff9593f1254624d5794d9a8", 16 | listing: LISTING_ID, 17 | venue: VENUE_ID, 18 | }); 19 | 20 | buyCertificateTransaction.setGasBudget(20000); 21 | const buyResult = await signer.signAndExecuteTransactionBlock({ 22 | transactionBlock: buyCertificateTransaction, 23 | options: { showEffects: true }, 24 | }); 25 | 26 | console.log("buyResult", JSON.stringify(buyResult)); 27 | }; 28 | 29 | buyFromLaunchpad(); 30 | -------------------------------------------------------------------------------- /examples/11-create-orderbook-market.ts: -------------------------------------------------------------------------------- 1 | import { OrderbookFullClient } from "../src"; 2 | import { NFT_TYPE, PACKAGE_OBJECT_ID, signer } from "./common"; 3 | 4 | export const createOrderbookMarket = async () => { 5 | const [protectionTx, protection] = OrderbookFullClient.createProtectionTx({ 6 | packageObjectId: PACKAGE_OBJECT_ID, 7 | buyNft: false, 8 | cancelAsk: false, 9 | cancelBid: false, 10 | createAsk: false, 11 | createBid: false, 12 | }); 13 | 14 | const [obTx, orderbook] = OrderbookFullClient.newOrderbookTx({ 15 | transaction: protectionTx, 16 | packageObjectId: PACKAGE_OBJECT_ID, 17 | collection: NFT_TYPE, 18 | protectedActions: protection, 19 | ft: "0x2::sui::SUI", 20 | }); 21 | 22 | const [shareOb] = OrderbookFullClient.shareOrderbookTx({ 23 | transaction: obTx, 24 | orderbook, 25 | collection: NFT_TYPE, 26 | packageObjectId: PACKAGE_OBJECT_ID, 27 | ft: "0x2::sui::SUI", 28 | }); 29 | 30 | shareOb.setGasBudget(1000000); 31 | 32 | const orderbookResult = await signer.signAndExecuteTransactionBlock({ 33 | transactionBlock: shareOb, 34 | options: { showEffects: true, showObjectChanges: true }, 35 | }); 36 | 37 | console.log("OrderBook creation result:", orderbookResult); 38 | }; 39 | 40 | createOrderbookMarket(); 41 | -------------------------------------------------------------------------------- /examples/13-place-bid-with-split-coin-with-commission.ts: -------------------------------------------------------------------------------- 1 | import { SUI_TYPE_ARG, TransactionBlock } from "@mysten/sui.js"; 2 | import { 3 | ORDERBOOK_ID, 4 | COLLECTION_ID_NAME, 5 | signer, 6 | getKiosks, 7 | ORDERBOOK_PACKAGE_ID, 8 | } from "./common"; 9 | import { OrderbookFullClient } from "../src"; 10 | 11 | export const placeBidWithCommission = async () => { 12 | const pubkeyAddress = await signer.getAddress(); 13 | console.log("Address: ", pubkeyAddress); 14 | 15 | const kiosks = await getKiosks(); 16 | 17 | if (kiosks.length === 0) { 18 | console.debug("No kiosks found"); 19 | return; 20 | } 21 | 22 | let tx = new TransactionBlock(); 23 | const coinCreationResult = tx.splitCoins(tx.gas, [tx.pure(100_000_000)]); 24 | 25 | [tx] = OrderbookFullClient.createBidWithCommissionTx({ 26 | packageObjectId: ORDERBOOK_PACKAGE_ID, 27 | buyersKiosk: kiosks[0].id.id, 28 | collection: COLLECTION_ID_NAME, 29 | ft: SUI_TYPE_ARG, 30 | orderbook: ORDERBOOK_ID, 31 | price: 35_000_000, 32 | wallet: coinCreationResult, 33 | beneficiary: 34 | "0x610b690cdf5104a8cd1e49a2ae0cf2e9f621b1f41c0648adbeb95da013f6ca2c", 35 | commission: 10_000_000, 36 | transaction: tx, 37 | }); 38 | 39 | const transferRes = tx.transferObjects( 40 | [coinCreationResult], 41 | tx.pure(pubkeyAddress) 42 | ); 43 | tx.setGasBudget(100_000_000); 44 | 45 | // console.debug("tx: ", tx.blockData) 46 | 47 | const result = await signer.signAndExecuteTransactionBlock({ 48 | transactionBlock: tx, 49 | options: { showEffects: true }, 50 | }); 51 | 52 | console.log("result", JSON.stringify(result)); 53 | }; 54 | 55 | placeBidWithCommission(); 56 | -------------------------------------------------------------------------------- /examples/13-place-bid-with-split-coin.ts: -------------------------------------------------------------------------------- 1 | import { SUI_TYPE_ARG, TransactionBlock } from "@mysten/sui.js"; 2 | import { 3 | ORDERBOOK_ID, 4 | COLLECTION_ID_NAME, 5 | signer, 6 | getKiosks, 7 | ORDERBOOK_PACKAGE_ID, 8 | } from "./common"; 9 | import { OrderbookFullClient } from "../src"; 10 | 11 | export const placeBid = async () => { 12 | const pubkeyAddress = await signer.getAddress(); 13 | console.log("Address: ", pubkeyAddress); 14 | 15 | const kiosks = await getKiosks(); 16 | 17 | if (kiosks.length === 0) { 18 | console.debug("No kiosks found"); 19 | return; 20 | } 21 | 22 | let tx = new TransactionBlock(); 23 | const coinCreationResult = tx.splitCoins(tx.gas, [tx.pure(100_000_000)]); 24 | 25 | [tx] = OrderbookFullClient.createBidTx({ 26 | packageObjectId: ORDERBOOK_PACKAGE_ID, 27 | buyersKiosk: kiosks[0].id.id, 28 | collection: COLLECTION_ID_NAME, 29 | ft: SUI_TYPE_ARG, 30 | orderbook: ORDERBOOK_ID, 31 | price: 65_000_000, 32 | wallet: coinCreationResult, 33 | transaction: tx, 34 | }); 35 | 36 | const transferRes = tx.transferObjects( 37 | [coinCreationResult], 38 | tx.pure(pubkeyAddress) 39 | ); 40 | tx.setGasBudget(100_000_000); 41 | 42 | // console.debug("tx: ", tx.blockData); 43 | 44 | const result = await signer.signAndExecuteTransactionBlock({ 45 | transactionBlock: tx, 46 | options: { showEffects: true }, 47 | }); 48 | 49 | console.log("result", JSON.stringify(result)); 50 | }; 51 | 52 | placeBid(); 53 | -------------------------------------------------------------------------------- /examples/14-place-ask-using-kiosk-nft.ts: -------------------------------------------------------------------------------- 1 | import { SUI_TYPE_ARG, TransactionBlock } from "@mysten/sui.js"; 2 | import { 3 | ORDERBOOK_ID, 4 | COLLECTION_ID_NAME, 5 | signer, 6 | client, 7 | getKiosks, 8 | ORDERBOOK_PACKAGE_ID, 9 | } from "./common"; 10 | import { OrderbookFullClient } from "../src"; 11 | 12 | export const placeAsk = async () => { 13 | const pubkeyAddress = await signer.getAddress(); 14 | console.log("Address: ", pubkeyAddress); 15 | 16 | const kiosks = await getKiosks() 17 | 18 | if (kiosks.length === 0) { 19 | console.error("No kiosks found"); 20 | return; 21 | } 22 | 23 | const kiosk = kiosks[0]; 24 | const kioskFields = await client.getDynamicFields(kiosk.id.id); 25 | 26 | console.log("kioskFields: ", kioskFields); 27 | 28 | const nftsFromKiosk = kioskFields 29 | .filter((el) => { 30 | const displayData = el?.data?.display?.data; 31 | return ( 32 | displayData != null && 33 | typeof displayData === "object" && 34 | "image_url" in displayData 35 | ); 36 | }) 37 | .filter((el) => { 38 | const displayData = el?.data?.display?.data; 39 | return ( 40 | displayData != null && 41 | typeof displayData === "object" && 42 | "image_url" in displayData && 43 | el?.data?.display != null && 44 | typeof el.data.display === "object" && 45 | el.data.display.data != null && 46 | typeof el.data.display.data === "object" 47 | ); 48 | }) 49 | .filter((el) => el?.data?.type?.includes(COLLECTION_ID_NAME)) 50 | .map((el) => el?.data?.objectId); 51 | 52 | const nft = nftsFromKiosk[0]; 53 | 54 | if (!nft) { 55 | console.error("No nft found"); 56 | return; 57 | } 58 | 59 | console.log("nft: ", nft); 60 | 61 | let tx = new TransactionBlock(); 62 | 63 | [tx] = OrderbookFullClient.createAskTx({ 64 | packageObjectId: ORDERBOOK_PACKAGE_ID, 65 | sellersKiosk: kiosks[0].id.id, 66 | nft, 67 | collection: COLLECTION_ID_NAME, 68 | ft: SUI_TYPE_ARG, 69 | orderbook: ORDERBOOK_ID, 70 | price: 175_000_000, 71 | }); 72 | 73 | tx.setGasBudget(100_000_000); 74 | const result = await signer.signAndExecuteTransactionBlock({ 75 | transactionBlock: tx, 76 | options: { showEffects: true }, 77 | }); 78 | 79 | console.log("result: ", result); 80 | }; 81 | 82 | placeAsk(); 83 | -------------------------------------------------------------------------------- /examples/14-place-ask-with-commission.ts: -------------------------------------------------------------------------------- 1 | import { ObjectId, SUI_TYPE_ARG, TransactionBlock } from "@mysten/sui.js"; 2 | import { 3 | ORDERBOOK_ID, 4 | COLLECTION_ID_NAME, 5 | signer, 6 | client, 7 | getKiosks, 8 | ORDERBOOK_PACKAGE_ID, 9 | orderbookClient, 10 | } from "./common"; 11 | import { OrderbookFullClient } from "../src"; 12 | import { toMap } from "../src/utils"; 13 | 14 | export const placeAsk = async () => { 15 | const pubkeyAddress = await signer.getAddress(); 16 | console.log("Address: ", pubkeyAddress); 17 | 18 | const kiosks = await getKiosks(); 19 | 20 | if (kiosks.length === 0) { 21 | console.error("No kiosks found"); 22 | return; 23 | } 24 | 25 | const kiosk = kiosks[0]; 26 | const kioskFields = await client.getDynamicFields(kiosk.id.id); 27 | 28 | console.log("kioskFields: ", kioskFields); 29 | 30 | const orderbookState = await orderbookClient.fetchOrderbook(ORDERBOOK_ID); 31 | const nftIdsMapInOrderbook = toMap(orderbookState.asks, (el => el.nft)) 32 | 33 | const nftsFromKiosk = kioskFields 34 | .filter((el) => { 35 | const displayData = el?.data?.display?.data; 36 | return ( 37 | displayData != null && 38 | typeof displayData === "object" && 39 | "image_url" in displayData 40 | ); 41 | }) 42 | .filter((el) => { 43 | const displayData = el?.data?.display?.data; 44 | return ( 45 | displayData != null && 46 | typeof displayData === "object" && 47 | "image_url" in displayData && 48 | el?.data?.display != null && 49 | typeof el.data.display === "object" && 50 | el.data.display.data != null && 51 | typeof el.data.display.data === "object" 52 | ); 53 | }) 54 | .filter((el) => el?.data?.type?.includes(COLLECTION_ID_NAME)) 55 | .map((el) => el?.data?.objectId) 56 | .filter((el) => { 57 | if (el === undefined) { 58 | return false; 59 | } 60 | if (nftIdsMapInOrderbook.get(el)) { 61 | return false; 62 | } 63 | 64 | return true; 65 | }); 66 | 67 | const nft = nftsFromKiosk[0]; 68 | 69 | if (!nft) { 70 | console.error("No nft found"); 71 | return; 72 | } 73 | 74 | console.log("nft: ", nft); 75 | 76 | let tx = new TransactionBlock(); 77 | 78 | [tx] = OrderbookFullClient.createAskWithCommissionTx({ 79 | packageObjectId: ORDERBOOK_PACKAGE_ID, 80 | sellersKiosk: kiosks[0].id.id, 81 | nft, 82 | collection: COLLECTION_ID_NAME, 83 | ft: SUI_TYPE_ARG, 84 | orderbook: ORDERBOOK_ID, 85 | price: 135_000_000, 86 | beneficiary: 87 | "0x610b690cdf5104a8cd1e49a2ae0cf2e9f621b1f41c0648adbeb95da013f6ca2c", 88 | commission: 10_000_000, 89 | }); 90 | 91 | tx.setGasBudget(100_000_000); 92 | const result = await signer.signAndExecuteTransactionBlock({ 93 | transactionBlock: tx, 94 | options: { showEffects: true }, 95 | }); 96 | 97 | console.log("result: ", result); 98 | }; 99 | 100 | placeAsk(); 101 | -------------------------------------------------------------------------------- /examples/15-fetch-orderbook.ts: -------------------------------------------------------------------------------- 1 | import { orderbookClient, ORDERBOOK_ID } from "./common"; 2 | 3 | export const fetchOrderbook = async () => { 4 | const orderbookState = await orderbookClient.fetchOrderbook(ORDERBOOK_ID); 5 | 6 | console.log("ORDERBOOK_ID: ", ORDERBOOK_ID); 7 | console.log("orderbookState: ", JSON.stringify(orderbookState)); 8 | console.log("orderbookState: ", orderbookState) 9 | }; 10 | 11 | fetchOrderbook(); 12 | -------------------------------------------------------------------------------- /examples/16-cancel-ask.ts: -------------------------------------------------------------------------------- 1 | import { SUI_TYPE_ARG, TransactionBlock } from "@mysten/sui.js"; 2 | import { 3 | ORDERBOOK_ID, 4 | COLLECTION_ID_NAME, 5 | signer, 6 | orderbookClient, 7 | getKiosks, 8 | ORDERBOOK_PACKAGE_ID, 9 | } from "./common"; 10 | import { OrderbookFullClient } from "../src"; 11 | 12 | export const cancelAsk = async () => { 13 | const pubkeyAddress = await signer.getAddress(); 14 | console.log("Address: ", pubkeyAddress); 15 | 16 | const kiosks = await getKiosks(); 17 | if (kiosks.length === 0) { 18 | console.error("No kiosks found"); 19 | return; 20 | } 21 | 22 | const kiosk = kiosks[0]; 23 | const orderbook = await orderbookClient.fetchOrderbook(ORDERBOOK_ID, true); 24 | console.log("orderbook", orderbook); 25 | 26 | if (orderbook.asks.length === 0) { 27 | console.error("No asks found"); 28 | return; 29 | } 30 | 31 | const askToCancel = orderbook.asks.find((ask) => ask.kiosk === kiosk.id.id); 32 | 33 | if (askToCancel === undefined) { 34 | console.error("No asks from user in the orderbook found"); 35 | return; 36 | } 37 | 38 | let tx = new TransactionBlock(); 39 | 40 | [tx] = OrderbookFullClient.cancelAskTx({ 41 | packageObjectId: ORDERBOOK_PACKAGE_ID, 42 | sellersKiosk: kiosks[0].id.id, 43 | nft: askToCancel.nft, 44 | collection: COLLECTION_ID_NAME, 45 | ft: SUI_TYPE_ARG, 46 | orderbook: ORDERBOOK_ID, 47 | price: askToCancel.price, 48 | }); 49 | 50 | tx.setGasBudget(100_000_000); 51 | const result = await signer.signAndExecuteTransactionBlock({ 52 | transactionBlock: tx, 53 | options: { showEffects: true }, 54 | }); 55 | 56 | console.log("result: ", result); 57 | }; 58 | 59 | cancelAsk(); 60 | -------------------------------------------------------------------------------- /examples/16-cancel-bid-with-split-coin.ts: -------------------------------------------------------------------------------- 1 | import { SUI_TYPE_ARG, TransactionBlock } from "@mysten/sui.js"; 2 | import { 3 | ORDERBOOK_ID, 4 | COLLECTION_ID_NAME, 5 | signer, 6 | orderbookClient, 7 | getKiosks, 8 | ORDERBOOK_PACKAGE_ID, 9 | } from "./common"; 10 | import { OrderbookFullClient } from "../src"; 11 | 12 | export const cancelBid = async () => { 13 | const pubkeyAddress = await signer.getAddress(); 14 | console.log("Address: ", pubkeyAddress); 15 | 16 | const kiosks = await getKiosks(); 17 | if (kiosks.length === 0) { 18 | console.error("No kiosks found"); 19 | return; 20 | } 21 | 22 | const kiosk = kiosks[0]; 23 | 24 | const orderbook = await orderbookClient.fetchOrderbook(ORDERBOOK_ID, true); 25 | // console.log("orderbook: ", orderbook) 26 | 27 | if (orderbook.bids.length === 0) { 28 | console.error("No bids found"); 29 | return; 30 | } 31 | 32 | const bidToCancel = orderbook.bids.find((bid) => bid.kiosk === kiosk.id.id); 33 | 34 | if (bidToCancel === undefined) { 35 | console.error("No bids from user in the orderbook found"); 36 | return; 37 | } 38 | 39 | let tx = new TransactionBlock(); 40 | const coinCreationResult = tx.splitCoins(tx.gas, [tx.pure(100_000_000)]); 41 | 42 | [tx] = OrderbookFullClient.cancelBidTx({ 43 | packageObjectId: ORDERBOOK_PACKAGE_ID, 44 | wallet: coinCreationResult, 45 | collection: COLLECTION_ID_NAME, 46 | ft: SUI_TYPE_ARG, 47 | orderbook: ORDERBOOK_ID, 48 | price: bidToCancel.offer, 49 | transaction: tx, 50 | }); 51 | 52 | const transferRes = tx.transferObjects( 53 | [coinCreationResult], 54 | tx.pure(pubkeyAddress) 55 | ); 56 | tx.setGasBudget(100_000_000); 57 | const result = await signer.signAndExecuteTransactionBlock({ 58 | transactionBlock: tx, 59 | options: { showEffects: true }, 60 | }); 61 | 62 | console.log("result: ", result); 63 | }; 64 | 65 | cancelBid(); 66 | -------------------------------------------------------------------------------- /examples/17-buy-nft-with-split-coin.ts: -------------------------------------------------------------------------------- 1 | import { SUI_TYPE_ARG, TransactionBlock } from "@mysten/sui.js"; 2 | import { 3 | ORDERBOOK_ID, 4 | orderbookClient, 5 | COLLECTION_ID_NAME, 6 | signer, 7 | getKiosks, 8 | ORDERBOOK_PACKAGE_ID, 9 | ALLOW_LIST_ID, 10 | TRANSFER_REQUEST_POLICY_ID, 11 | CONTRACT_BPS_ROYALTY_STRATEGY_ID, 12 | } from "./common"; 13 | import { OrderbookFullClient, TransferRequestFullClient } from "../src"; 14 | 15 | export const buyNFT = async () => { 16 | const pubkeyAddress = await signer.getAddress(); 17 | console.log("Address: ", pubkeyAddress); 18 | 19 | const orderbookStateInitial = await orderbookClient.fetchOrderbook( 20 | ORDERBOOK_ID 21 | ); 22 | 23 | if (orderbookStateInitial.asks.length === 0) { 24 | console.error("No asks to buy in the orderbook"); 25 | 26 | return; 27 | } 28 | 29 | const ask = orderbookStateInitial.asks[0]; 30 | 31 | if (!ask) { 32 | console.log("No such NFT listed on provided orderbook"); 33 | return; 34 | } 35 | 36 | const kiosks = await getKiosks(); 37 | if (kiosks.length === 0) { 38 | console.error("No kiosks found"); 39 | return; 40 | } 41 | 42 | const kiosk = kiosks[0]; 43 | 44 | const { nft } = ask; 45 | const sellersKiosk = ask.kiosk; 46 | const askPrice = ask.price; 47 | 48 | let tx = new TransactionBlock(); 49 | const coinCreationResult = tx.splitCoins(tx.gas, [tx.pure(askPrice)]); 50 | 51 | const [txBuyNftBlock, buyNftResult] = OrderbookFullClient.buyNftTx({ 52 | packageObjectId: ORDERBOOK_PACKAGE_ID, 53 | sellersKiosk, 54 | buyersKiosk: kiosk.id.id, 55 | collection: COLLECTION_ID_NAME, 56 | ft: SUI_TYPE_ARG, 57 | nft, 58 | orderbook: ORDERBOOK_ID, 59 | price: askPrice, 60 | wallet: coinCreationResult, 61 | transaction: tx, 62 | }); 63 | tx = txBuyNftBlock; 64 | 65 | TransferRequestFullClient.confirmTx({ 66 | transaction: tx, 67 | transferRequest: buyNftResult, 68 | allowListId: ALLOW_LIST_ID, 69 | policyId: TRANSFER_REQUEST_POLICY_ID, 70 | bpsRoyaltyStrategy: CONTRACT_BPS_ROYALTY_STRATEGY_ID, 71 | ft: SUI_TYPE_ARG, 72 | transferRequestType: COLLECTION_ID_NAME, 73 | }); 74 | 75 | const transferRes = tx.transferObjects( 76 | [coinCreationResult], 77 | tx.pure(pubkeyAddress) 78 | ); 79 | tx.setGasBudget(100_000_000); 80 | 81 | console.log("tx: ", tx.blockData); 82 | 83 | const result = await signer.signAndExecuteTransactionBlock({ 84 | transactionBlock: tx, 85 | options: { showEffects: true }, 86 | }); 87 | 88 | console.log("result: ", result); 89 | }; 90 | 91 | buyNFT(); 92 | -------------------------------------------------------------------------------- /examples/18-market-sell.ts: -------------------------------------------------------------------------------- 1 | import { SUI_TYPE_ARG, TransactionBlock } from "@mysten/sui.js"; 2 | import { 3 | ORDERBOOK_ID, 4 | COLLECTION_ID_NAME, 5 | signer, 6 | client, 7 | getKiosks, 8 | ORDERBOOK_PACKAGE_ID, 9 | } from "./common"; 10 | import { OrderbookFullClient } from "../src"; 11 | 12 | export const marketSell = async () => { 13 | const pubkeyAddress = await signer.getAddress(); 14 | console.log("Address: ", pubkeyAddress); 15 | 16 | const kiosks = await getKiosks() 17 | 18 | if (kiosks.length === 0) { 19 | console.error("No kiosks found"); 20 | return; 21 | } 22 | 23 | const kiosk = kiosks[0]; 24 | const kioskFields = await client.getDynamicFields(kiosk.id.id); 25 | 26 | console.log("kioskFields: ", kioskFields); 27 | 28 | const nftsFromKiosk = kioskFields 29 | .filter((el) => { 30 | const displayData = el?.data?.display?.data; 31 | return ( 32 | displayData != null && 33 | typeof displayData === "object" && 34 | "image_url" in displayData 35 | ); 36 | }) 37 | .filter((el) => { 38 | const displayData = el?.data?.display?.data; 39 | return ( 40 | displayData != null && 41 | typeof displayData === "object" && 42 | "image_url" in displayData && 43 | el?.data?.display != null && 44 | typeof el.data.display === "object" && 45 | el.data.display.data != null && 46 | typeof el.data.display.data === "object" 47 | ); 48 | }) 49 | .filter((el) => el?.data?.type?.includes(COLLECTION_ID_NAME)) 50 | .map((el) => el?.data?.objectId); 51 | 52 | const nft = nftsFromKiosk[0]; 53 | 54 | if (!nft) { 55 | console.error("No nft found"); 56 | return; 57 | } 58 | 59 | console.log("nft: ", nft); 60 | 61 | let tx = new TransactionBlock(); 62 | 63 | [tx] = OrderbookFullClient.marketSellTx({ 64 | packageObjectId: ORDERBOOK_PACKAGE_ID, 65 | sellersKiosk: kiosks[0].id.id, 66 | nft, 67 | collection: COLLECTION_ID_NAME, 68 | ft: SUI_TYPE_ARG, 69 | orderbook: ORDERBOOK_ID, 70 | price: 45_000_000, 71 | }); 72 | 73 | tx.setGasBudget(100_000_000); 74 | const result = await signer.signAndExecuteTransactionBlock({ 75 | transactionBlock: tx, 76 | options: { showEffects: true }, 77 | }); 78 | 79 | console.log("result: ", result); 80 | }; 81 | 82 | marketSell(); 83 | -------------------------------------------------------------------------------- /examples/19-market-buy.ts: -------------------------------------------------------------------------------- 1 | import { SUI_TYPE_ARG, TransactionBlock } from "@mysten/sui.js"; 2 | import { 3 | ORDERBOOK_ID, 4 | COLLECTION_ID_NAME, 5 | signer, 6 | getKiosks, 7 | ORDERBOOK_PACKAGE_ID, 8 | orderbookClient, 9 | } from "./common"; 10 | import { OrderbookFullClient } from "../src"; 11 | 12 | export const marketBuy = async () => { 13 | const pubkeyAddress = await signer.getAddress(); 14 | console.log("Address: ", pubkeyAddress); 15 | 16 | const orderbookStateInitial = await orderbookClient.fetchOrderbook( 17 | ORDERBOOK_ID 18 | ); 19 | 20 | if (orderbookStateInitial.asks.length === 0) { 21 | console.error("No asks to buy in the orderbook"); 22 | 23 | return; 24 | } 25 | 26 | const ask = orderbookStateInitial.asks[0]; 27 | 28 | if (!ask) { 29 | console.log("No such NFT listed on provided orderbook"); 30 | return; 31 | } 32 | 33 | const kiosks = await getKiosks(); 34 | if (kiosks.length === 0) { 35 | console.error("No kiosks found"); 36 | return; 37 | } 38 | 39 | const kiosk = kiosks[0]; 40 | 41 | const { nft } = ask; 42 | const sellersKiosk = ask.kiosk; 43 | const maxAskPrice = ask.price; 44 | 45 | let tx = new TransactionBlock(); 46 | const coinCreationResult = tx.splitCoins(tx.gas, [tx.pure(maxAskPrice)]); 47 | 48 | const [txBuyNftBlock, buyNftResult] = OrderbookFullClient.marketBuyTx({ 49 | packageObjectId: ORDERBOOK_PACKAGE_ID, 50 | buyersKiosk: kiosk.id.id, 51 | collection: COLLECTION_ID_NAME, 52 | ft: SUI_TYPE_ARG, 53 | orderbook: ORDERBOOK_ID, 54 | price: maxAskPrice, 55 | wallet: coinCreationResult, 56 | transaction: tx, 57 | }); 58 | tx = txBuyNftBlock; 59 | 60 | const transferRes = tx.transferObjects( 61 | [coinCreationResult], 62 | tx.pure(pubkeyAddress) 63 | ); 64 | tx.setGasBudget(100_000_000); 65 | 66 | console.log("tx: ", tx.blockData); 67 | // console.log("tx: ", tx.blockData.transactions[1]); 68 | 69 | const result = await signer.signAndExecuteTransactionBlock({ 70 | transactionBlock: tx, 71 | options: { showEffects: true }, 72 | }); 73 | 74 | console.log("result: ", result); 75 | }; 76 | 77 | marketBuy(); 78 | -------------------------------------------------------------------------------- /examples/2-init-marketplace.ts: -------------------------------------------------------------------------------- 1 | import { NftClient } from "../src"; 2 | import { FEE_OBJECT_ID, PACKAGE_OBJECT_ID, signer } from "./common"; 3 | 4 | export const initMarketplace = async () => { 5 | const pubKey = await signer.getAddress(); 6 | const [transactionBlock] = NftClient.buildInitMarketplace({ 7 | packageObjectId: PACKAGE_OBJECT_ID, 8 | admin: pubKey, // launchpad admin, 9 | receiver: pubKey, // launchpad receiver 10 | defaultFee: FEE_OBJECT_ID, 11 | }); 12 | const initMarketplaceResult = await signer.signAndExecuteTransactionBlock({ 13 | transactionBlock, 14 | options: { showEffects: true, showObjectChanges: true }, 15 | }); 16 | console.log("initMarketplaceResult", JSON.stringify(initMarketplaceResult)); 17 | }; 18 | 19 | initMarketplace(); 20 | -------------------------------------------------------------------------------- /examples/25-edit-ask.ts: -------------------------------------------------------------------------------- 1 | import { SUI_TYPE_ARG, TransactionBlock } from "@mysten/sui.js"; 2 | import { 3 | ORDERBOOK_ID, 4 | orderbookClient, 5 | COLLECTION_ID_NAME, 6 | signer, 7 | getKiosks, 8 | ORDERBOOK_PACKAGE_ID, 9 | } from "./common"; 10 | import { OrderbookFullClient } from "../src"; 11 | 12 | export const editAsk = async () => { 13 | const pubkeyAddress = await signer.getAddress(); 14 | console.log("Address: ", pubkeyAddress); 15 | 16 | const kiosks = await getKiosks(); 17 | if (kiosks.length === 0) { 18 | console.error("No kiosks found"); 19 | return; 20 | } 21 | 22 | const kiosk = kiosks[0]; 23 | const orderbook = await orderbookClient.fetchOrderbook(ORDERBOOK_ID, true); 24 | console.log("orderbook", orderbook); 25 | 26 | if (orderbook.asks.length === 0) { 27 | console.error("No asks found"); 28 | return; 29 | } 30 | 31 | const askToEdit = orderbook.asks.find((ask) => ask.kiosk === kiosk.id.id); 32 | 33 | if (askToEdit === undefined) { 34 | console.error("No asks from user in the orderbook found"); 35 | return; 36 | } 37 | 38 | let tx = new TransactionBlock(); 39 | 40 | const { nft, kiosk: sellersKiosk, price } = askToEdit; 41 | 42 | [tx] = OrderbookFullClient.editAskTx({ 43 | packageObjectId: ORDERBOOK_PACKAGE_ID, 44 | sellersKiosk, 45 | collection: COLLECTION_ID_NAME, 46 | ft: SUI_TYPE_ARG, 47 | nft, 48 | orderbook: ORDERBOOK_ID, 49 | oldPrice: price, 50 | newPrice: 275_000_000, 51 | transaction: tx, 52 | }); 53 | 54 | tx.setGasBudget(100_000_000); 55 | const result = await signer.signAndExecuteTransactionBlock({ 56 | transactionBlock: tx, 57 | options: { showEffects: true }, 58 | }); 59 | 60 | console.log("result: ", result); 61 | }; 62 | 63 | editAsk(); 64 | -------------------------------------------------------------------------------- /examples/25-edit-bid-with-split-coin.ts: -------------------------------------------------------------------------------- 1 | import { SUI_TYPE_ARG, TransactionBlock } from "@mysten/sui.js"; 2 | import { 3 | ORDERBOOK_ID, 4 | orderbookClient, 5 | COLLECTION_ID_NAME, 6 | signer, 7 | getKiosks, 8 | ORDERBOOK_PACKAGE_ID, 9 | } from "./common"; 10 | import { OrderbookFullClient } from "../src"; 11 | 12 | export const editBid = async () => { 13 | const pubkeyAddress = await signer.getAddress(); 14 | console.log("Address: ", pubkeyAddress); 15 | 16 | const kiosks = await getKiosks(); 17 | if (kiosks.length === 0) { 18 | console.error("No kiosks found"); 19 | return; 20 | } 21 | 22 | const kiosk = kiosks[0]; 23 | const orderbook = await orderbookClient.fetchOrderbook(ORDERBOOK_ID, true); 24 | console.log("orderbook", orderbook); 25 | 26 | if (orderbook.bids.length === 0) { 27 | // console.error("No bids found"); 28 | return; 29 | } 30 | 31 | const bidToEdit = orderbook.bids.find((bid) => bid.kiosk === kiosk.id.id); 32 | 33 | if (bidToEdit === undefined) { 34 | console.error("No bids from user in the orderbook found"); 35 | return; 36 | } 37 | 38 | let tx = new TransactionBlock(); 39 | const coinCreationResult = tx.splitCoins(tx.gas, [tx.pure(100_000_000)]); 40 | 41 | [tx] = OrderbookFullClient.editBidTx({ 42 | packageObjectId: ORDERBOOK_PACKAGE_ID, 43 | buyersKiosk: kiosk.id.id, 44 | collection: COLLECTION_ID_NAME, 45 | ft: SUI_TYPE_ARG, 46 | orderbook: ORDERBOOK_ID, 47 | oldPrice: bidToEdit.offer, 48 | newPrice: 25_000_000, 49 | wallet: coinCreationResult, 50 | transaction: tx, 51 | }); 52 | 53 | const transferRes = tx.transferObjects( 54 | [coinCreationResult], 55 | tx.pure(pubkeyAddress) 56 | ); 57 | tx.setGasBudget(100_000_000); 58 | const result = await signer.signAndExecuteTransactionBlock({ 59 | transactionBlock: tx, 60 | options: { showEffects: true }, 61 | }); 62 | 63 | console.log("result: ", result); 64 | }; 65 | 66 | editBid(); 67 | -------------------------------------------------------------------------------- /examples/3-init-listing.ts: -------------------------------------------------------------------------------- 1 | import { NftClient } from "../src"; 2 | import { PACKAGE_OBJECT_ID, signer } from "./common"; 3 | 4 | export const initListing = async () => { 5 | const pubKey = await signer.getAddress(); 6 | const [transactionBlock] = NftClient.buildInitListing({ 7 | packageObjectId: PACKAGE_OBJECT_ID, 8 | listingAdmin: pubKey, // launchpad admin, 9 | receiver: pubKey, // launchpad receiver 10 | }); 11 | const initListingResult = await signer.signAndExecuteTransactionBlock({ 12 | transactionBlock, 13 | options: { showEffects: true, showObjectChanges: true }, 14 | }); 15 | console.log("initListingResult", JSON.stringify(initListingResult)); 16 | }; 17 | 18 | initListing(); 19 | -------------------------------------------------------------------------------- /examples/33-create-kiosk.ts: -------------------------------------------------------------------------------- 1 | import { TransactionBlock } from "@mysten/sui.js"; 2 | import { signer, user } from "./common"; 3 | import { KioskFullClient } from "../src"; 4 | 5 | export const createKiosk = async (): Promise => { 6 | const transaction = new TransactionBlock(); 7 | const kiosk = KioskFullClient.newKioskTx({transaction})[1]; 8 | 9 | const share = KioskFullClient.shareKioskTx({ 10 | transaction, 11 | kiosk 12 | }) 13 | 14 | transaction.setGasBudget(200_000_000); 15 | 16 | await signer.signAndExecuteTransactionBlock({ 17 | transactionBlock: transaction, 18 | options: { showEffects: true }, 19 | }); 20 | console.log("user", user); 21 | console.log("kiosk:", kiosk, share); 22 | } 23 | // eslint-disable-next-line no-global-assign 24 | 25 | createKiosk(); -------------------------------------------------------------------------------- /examples/34-get-kiosk.ts: -------------------------------------------------------------------------------- 1 | import { client, kioskClient, user } from "./common"; 2 | 3 | export const getKiosks = async (): Promise => { 4 | const kiosks = (await kioskClient.getWalletKiosks(user)) ?? []; 5 | console.log("kiosk:", kiosks); 6 | 7 | kiosks.forEach(async (kiosk) => { 8 | const dynamicFieldsOfKiosk = await client.getDynamicFields(kiosk.id.id); 9 | console.log(`dynamicFieldsOfKiosk ${kiosk.id.id} `, dynamicFieldsOfKiosk); 10 | 11 | console.log( 12 | "kioskFields display", 13 | dynamicFieldsOfKiosk[0].data?.display, 14 | "kioskFields[0].data?.content", 15 | dynamicFieldsOfKiosk[0].data?.content 16 | ); 17 | }); 18 | }; 19 | 20 | getKiosks(); 21 | -------------------------------------------------------------------------------- /examples/35-place-bid.ts: -------------------------------------------------------------------------------- 1 | import { SUI_TYPE_ARG, TransactionBlock } from "@mysten/sui.js"; 2 | import { BiddingContractClient, KioskFullClient } from "../src"; 3 | import { signer, user } from "./common"; 4 | 5 | export const placeBid = async () => { 6 | const nftId = "0x0626c9b4e068346891791d7e0c64aa83136fca111d0f8a229641dd4928af9ae8"; 7 | const transaction = new TransactionBlock(); 8 | const kioskId: any = undefined // ((await kioskClient.getWalletKiosk(user))?.id as any).id; 9 | let kioskIdTransaction; 10 | let kiosk; 11 | const coin = transaction.splitCoins(transaction.gas, [transaction.pure(2_000_000_000)]); 12 | if(!kioskId) { 13 | // eslint-disable-next-line prefer-destructuring 14 | kiosk = KioskFullClient.newKioskTx({transaction})[1]; 15 | // eslint-disable-next-line prefer-destructuring 16 | kioskIdTransaction = KioskFullClient.getObjectIdTx({transaction, kiosk})[1]; 17 | } 18 | 19 | BiddingContractClient.createBidWithCommissionTx({ 20 | transaction, 21 | nft: nftId, 22 | buyersKiosk: kioskIdTransaction || kioskId, 23 | ft: SUI_TYPE_ARG, 24 | wallet: coin, 25 | beneficiary: "0xd4066588fee19db7524ec85bc9e1b873b5ccc1cea3e6e2c9c8b12ed76c593cb9", 26 | commission: 1, 27 | price: 1 28 | }); 29 | 30 | if(kioskIdTransaction) { 31 | KioskFullClient.shareKioskTx({ 32 | transaction, 33 | kiosk 34 | }); 35 | } 36 | 37 | transaction.transferObjects([coin], transaction.pure(user)); 38 | 39 | transaction.setGasBudget(2_000_000_000); 40 | 41 | await signer.signAndExecuteTransactionBlock({ 42 | transactionBlock: transaction, 43 | options: { showEffects: true }, 44 | }); 45 | // console.log("bid tx", bidTx); 46 | } 47 | 48 | placeBid(); -------------------------------------------------------------------------------- /examples/36-close-bid.ts: -------------------------------------------------------------------------------- 1 | import { SUI_TYPE_ARG, TransactionBlock } from "@mysten/sui.js"; 2 | import { BiddingContractClient } from "../src"; 3 | import { signer } from "./common"; 4 | 5 | const closeBid = async () => { 6 | const bidId = process.env.BID_ID; 7 | const kioskId = process.env.KIOSK_ID; 8 | if(!bidId) throw new Error("Use BID_ID env variable to provide a bid id"); 9 | if(!kioskId) throw new Error("Use KioskId env variable to provide a bid id"); 10 | const transaction = new TransactionBlock(); 11 | transaction.setGasBudget(2_000_000_000); 12 | BiddingContractClient.closeBid({bid: bidId, ft: SUI_TYPE_ARG, kioskId, transaction}); 13 | await signer.signAndExecuteTransactionBlock({ 14 | transactionBlock: transaction, 15 | options: { 16 | showEffects: true 17 | } 18 | }); 19 | } 20 | 21 | closeBid(); -------------------------------------------------------------------------------- /examples/37-sell-nft.ts: -------------------------------------------------------------------------------- 1 | import { SUI_TYPE_ARG, TransactionBlock } from "@mysten/sui.js"; 2 | import { BiddingContractClient } from "../src"; 3 | import { signer } from "./common"; 4 | 5 | const sellNft = async () => { 6 | const bidId = process.env.BID_ID; 7 | 8 | if(!bidId) throw new Error("Use BID_ID env variable to provide a bid id"); 9 | const nftId = "0x0626c9b4e068346891791d7e0c64aa83136fca111d0f8a229641dd4928af9ae8"; 10 | const transaction = new TransactionBlock(); 11 | transaction.setGasBudget(2_000_000_000); 12 | const transferRequest = BiddingContractClient.sellNftTx({ 13 | transaction, 14 | bid: bidId, 15 | buyersKiosk: "0x7f164ff0d8df960ec64950ad12e04655284bf8bd5fe9ede3811f7c7a67364b8a", 16 | ft: SUI_TYPE_ARG, 17 | // eslint-disable-next-line max-len 18 | nftType: "0x48a968571e9665929f4ec54e0e102d7432dde1312fd704b231b450591b51fa90::clutchynfts::ClutchyNFT", 19 | nft: nftId 20 | })[1]; 21 | 22 | transaction.transferObjects([transferRequest], transaction.pure("0x7f164ff0d8df960ec64950ad12e04655284bf8bd5fe9ede3811f7c7a67364b8a")); 23 | 24 | await signer.signAndExecuteTransactionBlock({ 25 | transactionBlock: transaction, 26 | options: { 27 | showEffects: true 28 | } 29 | }); 30 | } 31 | 32 | sellNft(); -------------------------------------------------------------------------------- /examples/38-create-kiosk-and-buy-nft-into-kiosk.ts: -------------------------------------------------------------------------------- 1 | import { TransactionBlock } from "@mysten/sui.js"; 2 | import { KioskFullClient, NftClient } from "../src"; 3 | import { 4 | LISTING_ID, 5 | NFT_TYPE, 6 | PACKAGE_OBJECT_ID, 7 | signer, 8 | VENUE_ID, 9 | COLLECTION_ID_NAME, 10 | kioskClient, 11 | } from "./common"; 12 | 13 | const buyFromLaunchpad = async () => { 14 | const pubkeyAddress = await signer.getAddress(); 15 | console.log("signer address: ", pubkeyAddress); 16 | 17 | let tx = new TransactionBlock(); 18 | const coinCreationResult = tx.splitCoins(tx.gas, [tx.pure(250_000_000)]); 19 | const [kioskTxBlock, kioskTxResult] = KioskFullClient.newKioskTx({ 20 | transaction: tx, 21 | }); 22 | tx = kioskTxBlock; 23 | 24 | const module: "limited_fixed_price" = "limited_fixed_price"; 25 | 26 | const args = { 27 | module, 28 | packageObjectId: PACKAGE_OBJECT_ID, 29 | nftType: COLLECTION_ID_NAME, 30 | coin: coinCreationResult, 31 | buyersKiosk: kioskTxResult, 32 | // buyersKiosk: "0xec117b5a1a33832fc91c9d81976775b16e5f27621a60185920a3d0ebcaacc62b", 33 | listing: LISTING_ID, 34 | venue: VENUE_ID, 35 | transaction: tx, 36 | }; 37 | 38 | console.log("[buyFromLaunchpad] args: ", args); 39 | 40 | [tx] = NftClient.buildBuyNftIntoKiosk(args); 41 | const [shareKioskTxBlock, shareKioskTxResult] = KioskFullClient.shareKioskTx({ 42 | transaction: tx, 43 | kiosk: kioskTxResult, 44 | }); 45 | tx = shareKioskTxBlock; 46 | 47 | tx.transferObjects([coinCreationResult], tx.pure(pubkeyAddress)); 48 | tx.setGasBudget(1_200_000_000); 49 | 50 | console.log("tx: ", tx.blockData); 51 | 52 | const buyResult = await signer.signAndExecuteTransactionBlock({ 53 | transactionBlock: tx, 54 | options: { showEffects: true }, 55 | }); 56 | 57 | console.log("buyResult", JSON.stringify(buyResult)); 58 | }; 59 | 60 | buyFromLaunchpad(); 61 | -------------------------------------------------------------------------------- /examples/38-deposit-nft.ts: -------------------------------------------------------------------------------- 1 | import {TransactionBlock } from "@mysten/sui.js"; 2 | import { KioskFullClient } from "../src"; 3 | import { signer } from "./common"; 4 | 5 | const depositNft = async () => { 6 | const bidId = process.env.BID_ID; 7 | 8 | if(!bidId) throw new Error("Use BID_ID env variable to provide a bid id"); 9 | const nftId = "0x0626c9b4e068346891791d7e0c64aa83136fca111d0f8a229641dd4928af9ae8"; 10 | const transaction = new TransactionBlock(); 11 | transaction.setGasBudget(2_000_000_000); 12 | KioskFullClient.depositTx({ 13 | transaction, 14 | kiosk: "0x7f164ff0d8df960ec64950ad12e04655284bf8bd5fe9ede3811f7c7a67364b8a", 15 | nftType: "0x48a968571e9665929f4ec54e0e102d7432dde1312fd704b231b450591b51fa90::clutchynfts::ClutchyNFT", 16 | nft: nftId 17 | }); 18 | await signer.signAndExecuteTransactionBlock({ 19 | transactionBlock: transaction, 20 | options: { 21 | showEffects: true 22 | } 23 | }); 24 | } 25 | 26 | depositNft(); -------------------------------------------------------------------------------- /examples/39-create-kiosk-and-buy-whitelisted-nft-into-kiosk.ts: -------------------------------------------------------------------------------- 1 | import { TransactionBlock } from "@mysten/sui.js"; 2 | import { KioskFullClient, NftClient } from "../src"; 3 | import { 4 | LISTING_ID, 5 | NFT_TYPE, 6 | PACKAGE_OBJECT_ID, 7 | signer, 8 | VENUE_ID, 9 | COLLECTION_ID_NAME, 10 | kioskClient, 11 | } from "./common"; 12 | 13 | const buyFromLaunchpad = async () => { 14 | const pubkeyAddress = await signer.getAddress(); 15 | console.log("signer address: ", pubkeyAddress); 16 | 17 | const WHITELIST_CERTIFICATE_OBJECT_ID = 18 | "0xea72ced7d1bfb223395a2818ccc47b72eb838ae2ecc26bddc1a6d04d4cfef2fa"; 19 | 20 | let tx = new TransactionBlock(); 21 | const coinCreationResult = tx.splitCoins(tx.gas, [tx.pure(250_000_000)]); 22 | const [kioskTxBlock, kioskTxResult] = KioskFullClient.newKioskTx({ 23 | transaction: tx, 24 | }); 25 | 26 | const module: "limited_fixed_price" = "limited_fixed_price"; 27 | 28 | const args = { 29 | module, 30 | packageObjectId: PACKAGE_OBJECT_ID, 31 | nftType: COLLECTION_ID_NAME, 32 | coin: coinCreationResult, 33 | buyersKiosk: kioskTxResult, 34 | listing: LISTING_ID, 35 | venue: VENUE_ID, 36 | whitelistCertificate: WHITELIST_CERTIFICATE_OBJECT_ID, 37 | transaction: tx, 38 | }; 39 | 40 | console.log("[buyFromLaunchpad] args: ", args); 41 | 42 | [tx] = NftClient.buildBuyWhitelistedNftIntoKiosk(args); 43 | tx = kioskTxBlock; 44 | KioskFullClient.shareKioskTx({ 45 | transaction: tx, 46 | kiosk: kioskTxResult, 47 | }); 48 | 49 | tx.transferObjects([coinCreationResult], tx.pure(pubkeyAddress)); 50 | tx.setGasBudget(1_200_000_000); 51 | 52 | console.log("tx: ", tx.blockData); 53 | 54 | const buyResult = await signer.signAndExecuteTransactionBlock({ 55 | transactionBlock: tx, 56 | options: { showEffects: true }, 57 | }); 58 | 59 | console.log("buyResult", JSON.stringify(buyResult)); 60 | }; 61 | 62 | buyFromLaunchpad(); 63 | -------------------------------------------------------------------------------- /examples/39-sell-nft-from-kiosk.ts: -------------------------------------------------------------------------------- 1 | import { SUI_TYPE_ARG, TransactionBlock } from "@mysten/sui.js"; 2 | import { BiddingContractClient, TransferRequestFullClient } from "../src"; 3 | import { signer } from "./common"; 4 | 5 | const sellNft = async () => { 6 | const nftId = "0x2ea383137c90c07fe46ba53b85010d5bf4c79c403590a2c59706db6a724d42fc"; 7 | const transaction = new TransactionBlock(); 8 | transaction.setGasBudget(2_000_000_000); 9 | const transferRequest = BiddingContractClient.sellNftFromKioskTx({ 10 | packageObjectId: "0xd34b56feab8ec4e31e32b30564e1d6b11eb32f2985c3fbb85b5be715df006536", 11 | transaction, 12 | sellersKiosk: "0xabd99fcc4295899342fde765e758ad724ca1baa90bec7dbba6af777f9483f94f", 13 | bid: "0x31045b89422ac100cb5c3ca4495395c34291a9730a31693584f2582f9b3270ba", 14 | buyersKiosk: "0x4c1992360d60a8b1628aa734bdc844d52d999f00f7bc75a705518780fa11be36", 15 | ft: SUI_TYPE_ARG, 16 | // eslint-disable-next-line max-len 17 | nftType: "0xc64714be5cb0c9c9ab6c349964b18eb11b9739155dd1dfd9af0abe2d71eebb86::clutchynfts::ClutchyNft", 18 | nft: nftId 19 | })[1]; 20 | 21 | TransferRequestFullClient.confirmTx({ 22 | transaction, 23 | requestContractId: "0x33324b87a09f5b2928d8d62a00eb66f93baa8d7545330c8c8ca15da2c80cbc82", 24 | nftProtocolContractId: "0xd624568412019443dbea9c4e97a6c474cececa7e9daef307457cb34dd04eee0d", 25 | transferRequest, 26 | allowListId: "0x641dcb7bf80a537e46e29e27c637f639ba8f644d5daf396e2b212b9bbe6c0383", 27 | policyId: "0x82fc231d6aa2488a4420099841476e658ef1ce39aae557efca5fddc7da156929", 28 | bpsRoyaltyStrategy: "0x721b29839f5c93329afc040316128272679363774440f3f8d596079d62446e24", 29 | ft: SUI_TYPE_ARG, 30 | transferRequestType: "0xc64714be5cb0c9c9ab6c349964b18eb11b9739155dd1dfd9af0abe2d71eebb86::clutchynfts::ClutchyNft" 31 | }); 32 | 33 | await signer.signAndExecuteTransactionBlock({ 34 | transactionBlock: transaction, 35 | options: { 36 | showEffects: true 37 | } 38 | }); 39 | } 40 | 41 | sellNft(); -------------------------------------------------------------------------------- /examples/4-init-warehouse.ts: -------------------------------------------------------------------------------- 1 | import { NftClient } from "../src"; 2 | import { PACKAGE_OBJECT_ID, signer, NFT_TYPE } from "./common"; 3 | 4 | export const initInventory = async () => { 5 | const [transactionBlock] = NftClient.buildInitWarehouse({ 6 | packageObjectId: PACKAGE_OBJECT_ID, 7 | nftType: NFT_TYPE, 8 | }); 9 | const initWarehouseResult = await signer.signAndExecuteTransactionBlock({ 10 | transactionBlock, 11 | options: { showEffects: true, showObjectChanges: true }, 12 | }); 13 | console.log("initWarehouseResult", JSON.stringify(initWarehouseResult)); 14 | }; 15 | 16 | initInventory(); 17 | -------------------------------------------------------------------------------- /examples/40-get-nfts-for-kiosk.ts: -------------------------------------------------------------------------------- 1 | import { KioskReadClient } from "../src/client/kiosk/KioskReadClient"; 2 | import { kioskClient, user } from "./common"; 3 | 4 | const getNftForKiosk = async () => { 5 | const nfts = await kioskClient.getAllNftKioskAssociations(user); 6 | console.log(nfts); 7 | } 8 | 9 | getNftForKiosk(); -------------------------------------------------------------------------------- /examples/5-mint-nft.ts: -------------------------------------------------------------------------------- 1 | import { MoveCallTransaction } from "@mysten/sui.js"; 2 | import { NftClient } from "../src"; 3 | import { MINT_CAP_ID, PACKAGE_OBJECT_ID, signer, WAREHOUSE_ID } from "./common"; 4 | 5 | export const splitBy = (list: T[], chunkSize: number): T[][] => { 6 | const result: T[][] = []; 7 | for (let i = 0; i < list.length; i += chunkSize) { 8 | result.push(list.slice(i, i + chunkSize)); 9 | } 10 | 11 | return result; 12 | }; 13 | 14 | // eslint-disable-next-line no-promise-executor-return 15 | const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); 16 | 17 | const mintChunk = async (txs: MoveCallTransaction[]) => { 18 | for (let i = 0; i < txs.length; i += 1) { 19 | const tx = txs[i]; 20 | // eslint-disable-next-line no-await-in-loop 21 | // await delay(500 + Math.random() * 2000); 22 | console.log("Mint...", Date.now(), tx.arguments[0], i); 23 | // eslint-disable-next-line no-await-in-loop 24 | const result = await signer.executeMoveCall(tx); 25 | console.log("result", tx.arguments[0], "EffectsCert" in result); 26 | } 27 | // const createMarketResult = await signer.executeMoveCall(transaction); 28 | // console.log('createMarketResult', JSON.stringify(createMarketResult)); 29 | }; 30 | 31 | export const mintNFt = async () => { 32 | const txs: MoveCallTransaction[] = []; 33 | for (let i = 1; i <= 950; i += 1) { 34 | txs.push( 35 | NftClient.buildMintNft({ 36 | name: "Game Over", 37 | description: "Try Next Time", 38 | mintCap: MINT_CAP_ID, 39 | packageObjectId: PACKAGE_OBJECT_ID, 40 | warehouseId: WAREHOUSE_ID, 41 | moduleName: "suimarines", 42 | url: "ipfs://QmcUZmoDWyBB7Cra15XpSpcq628E5T3qvedB5NcbVm9yKM", 43 | attributes: { 44 | Status: "Loss", 45 | }, 46 | }) 47 | ); 48 | } 49 | for (let i = 1; i <= 50; i += 1) { 50 | txs.push( 51 | NftClient.buildMintNft({ 52 | name: `Golden Ticket #${i}`, 53 | description: "You have been selected", 54 | mintCap: MINT_CAP_ID, 55 | packageObjectId: PACKAGE_OBJECT_ID, 56 | warehouseId: WAREHOUSE_ID, 57 | moduleName: "suimarines", 58 | url: "ipfs://QmWD14oS1P91mr4vSzqMXmZJP5jp8BbPtmJkdYpBD9eBdJ", 59 | attributes: { 60 | Status: "Win", 61 | }, 62 | }) 63 | ); 64 | } 65 | const chunks = splitBy( 66 | txs.sort((a, b) => 0.5 - Math.random()), 67 | 1000 68 | ); 69 | await Promise.all(chunks.map((chunk) => mintChunk(chunk))); 70 | // const createMarketResult = await signer.executeMoveCall(transaction); 71 | // console.log('createMarketResult', JSON.stringify(createMarketResult)); 72 | }; 73 | 74 | mintNFt(); 75 | -------------------------------------------------------------------------------- /examples/6-add-warehouse-to-listing.ts: -------------------------------------------------------------------------------- 1 | import { NftClient } from "../src"; 2 | import { 3 | WAREHOUSE_ID, 4 | LISTING_ID, 5 | PACKAGE_OBJECT_ID, 6 | signer, 7 | COLLECTION_ID, 8 | } from "./common"; 9 | 10 | export const addInventoryToListing = async () => { 11 | const transaction = NftClient.buildAddWarehouseToListing({ 12 | packageObjectId: PACKAGE_OBJECT_ID, 13 | warehouse: WAREHOUSE_ID, 14 | listing: LISTING_ID, 15 | nftClassName: "SUIMARINES", 16 | nftModuleName: "suimarines", 17 | collection: COLLECTION_ID, 18 | }); 19 | const addInventoryResult = await signer.executeMoveCall(transaction); 20 | console.log("addInventoryResult", JSON.stringify(addInventoryResult)); 21 | }; 22 | 23 | addInventoryToListing(); 24 | -------------------------------------------------------------------------------- /examples/7-create-market.ts: -------------------------------------------------------------------------------- 1 | import { NftClient } from "../src"; 2 | import { 3 | WAREHOUSE_ID, 4 | LISTING_ID, 5 | PACKAGE_OBJECT_ID, 6 | signer, 7 | INVENTORY_ID, 8 | } from "./common"; 9 | 10 | export const createMarket = async () => { 11 | const transaction = NftClient.buildInitVenue({ 12 | packageObjectId: PACKAGE_OBJECT_ID, 13 | inventory: INVENTORY_ID, 14 | listing: LISTING_ID, 15 | price: 100000, 16 | isWhitelisted: false, 17 | nftClassName: "SUIMARINES", 18 | nftModuleName: "suimarines", 19 | }); 20 | const createMarketResult = await signer.executeMoveCall(transaction); 21 | console.log("createMarketResult", JSON.stringify(createMarketResult)); 22 | }; 23 | 24 | createMarket(); 25 | -------------------------------------------------------------------------------- /examples/8-attach-marketplace.ts: -------------------------------------------------------------------------------- 1 | import { NftClient } from "../src"; 2 | import { 3 | LISTING_ID, 4 | MARKETPLACE_ID, 5 | PACKAGE_OBJECT_ID, 6 | signer, 7 | } from "./common"; 8 | 9 | export const attachMarketplace = async () => { 10 | const transaction = NftClient.buildRequestToJoinMarketplace({ 11 | packageObjectId: PACKAGE_OBJECT_ID, 12 | marketplace: MARKETPLACE_ID, 13 | listing: LISTING_ID, 14 | }); 15 | const buildRequestResult = await signer.executeMoveCall(transaction); 16 | console.log("buildRequestResult", JSON.stringify(buildRequestResult)); 17 | 18 | const transaction2 = NftClient.buildAcceptListingRequest({ 19 | packageObjectId: PACKAGE_OBJECT_ID, 20 | marketplace: MARKETPLACE_ID, 21 | listing: LISTING_ID, 22 | }); 23 | 24 | const acceptRequestResult = await signer.executeMoveCall(transaction2); 25 | console.log("acceptRequestResult", JSON.stringify(acceptRequestResult)); 26 | }; 27 | 28 | attachMarketplace(); 29 | -------------------------------------------------------------------------------- /examples/9-enable-sales.ts: -------------------------------------------------------------------------------- 1 | import { NftClient } from "../src"; 2 | import { LISTING_ID, PACKAGE_OBJECT_ID, signer, VENUE_ID } from "./common"; 3 | 4 | const enableSales = async () => { 5 | const mintNftTransaction = NftClient.buildEnableSales({ 6 | packageObjectId: PACKAGE_OBJECT_ID, 7 | listing: LISTING_ID, 8 | venue: VENUE_ID, 9 | }); 10 | 11 | const enableSalesResult = await signer.executeMoveCall(mintNftTransaction); 12 | console.log("enableSalesResult", JSON.stringify(enableSalesResult)); 13 | }; 14 | 15 | enableSales(); 16 | -------------------------------------------------------------------------------- /examples/common.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Connection, 3 | Ed25519Keypair, 4 | JsonRpcProvider, 5 | RawSigner, 6 | SUI_TYPE_ARG, 7 | } from "@mysten/sui.js"; 8 | import { 9 | BiddingContractClient, 10 | KioskFullClient, 11 | NftClient, 12 | OrderbookFullClient, 13 | SafeFullClient, 14 | } from "../src"; 15 | 16 | // export const mnemonic = 'muffin tuition fit fish average true slender tower salmon artist song biology'; 17 | export const mnemonic = 18 | "elder okay depart innocent soldier tennis lumber eternal permit bunker urge brother"; 19 | 20 | /** 21 | * Created Objects: 22 | - ID: , Owner: Account Address ( 0xddcdd8e07b59852f58ba8db8daff1b585d2fca23 ) 23 | - ID: , Owner: Immutable 24 | - ID: , Owner: Shared 25 | - ID: , Owner: Shared 26 | */ 27 | // Step 0 - Publish 28 | 29 | export const PACKAGE_OBJECT_ID = 30 | "0xd624568412019443dbea9c4e97a6c474cececa7e9daef307457cb34dd04eee0d"; // Change to your deployed contract 31 | export const COLLECTION_ID = 32 | "0xc64714be5cb0c9c9ab6c349964b18eb11b9739155dd1dfd9af0abe2d71eebb86"; // Change to your deployed contract 33 | export const COLLECTION_ID_NAME = `${COLLECTION_ID}::clutchynfts::ClutchyNft`; 34 | export const COLLECTION_TYPE = `${PACKAGE_OBJECT_ID}::collection::Collection<${COLLECTION_ID_NAME}>`; 35 | export const NFT_TYPE = `${PACKAGE_OBJECT_ID}::nft::Nft<${COLLECTION_ID_NAME}>`; 36 | 37 | export const MINT_CAP_ID = "0x2ac4bb34ae39625385d0d0355f5ff82b53df1db1"; // Change to your deployed contract 38 | // Step 1 - create Flat fee 39 | export const FEE_OBJECT_ID = "0x2a78356fdb1d91dca38b2ab47082aca02bfd4efb"; // Change to your deployed contract 40 | // Step 2 - Init marketplace 41 | export const MARKETPLACE_ID = "0x717aae68dfc03479818c2c550832c6d2668fe87d"; // Change to your deployed contract 42 | // Step 3 - Init Listing 43 | export const LISTING_ID = 44 | "0x2ce87cf327c2f7da2619eb98ff5ddc531c888751789d5b4a62931838e5c8b677"; // Change to your deployed contract 45 | // Step 4 - Create Warehouse 46 | export const WAREHOUSE_ID = "0xa3f7cfc3124b7e65de6a0f9444b5299f2691f0ab"; // Change to your deployed contract 47 | // Step 5 - Add inventory to listing 48 | export const INVENTORY_ID = "0x940bf37390b0b5a7ce10d0b433e39faddae3d136"; // Change to your deployed contract 49 | // Step 6 - Create Market 50 | export const VENUE_ID = 51 | "0x52c63cb8018fecbc2c3ce8806757d79f27569001f182c3eabfa1c59e4abbcd05"; // Change to your deployed contract 52 | // Step 7 - Create Orderbook 53 | export const ORDERBOOK_ID = 54 | "0x687dd7bcd3112180e1a2f2f9e980d72764cd1b6214cc52ae99ef548d1a1346b7"; // Change to your deployed contract 55 | // Step 8 - Create AllowList 56 | export const ALLOW_LIST_ID = 57 | "0x641dcb7bf80a537e46e29e27c637f639ba8f644d5daf396e2b212b9bbe6c0383"; // Change to your deployed contract 58 | 59 | export const TRANSFER_REQUEST_POLICY_ID = 60 | "0x82fc231d6aa2488a4420099841476e658ef1ce39aae557efca5fddc7da156929"; 61 | 62 | export const TRANSFER_REQUEST_POLICY_TYPE = `${TRANSFER_REQUEST_POLICY_ID}::clutchynfts::ClutchyNft`; 63 | 64 | export const CONTRACT_BPS_ROYALTY_STRATEGY_ID = 65 | "0x721b29839f5c93329afc040316128272679363774440f3f8d596079d62446e24"; 66 | 67 | export const KIOSK_ID = 68 | "0xb880efb88af9174c16cf561a07db6aebbca74ace27916761c374f711abb42a76"; 69 | 70 | export const ORDERBOOK_PACKAGE_ID = 71 | "0xd34b56feab8ec4e31e32b30564e1d6b11eb32f2985c3fbb85b5be715df006536"; 72 | 73 | export function normalizeMnemonics(mnemonics: string): string { 74 | return mnemonics 75 | .trim() 76 | .split(/\s+/) 77 | .map((part) => part.toLowerCase()) 78 | .join(" "); 79 | } 80 | 81 | function hexStringToUint8Array(hexStr: string) { 82 | if (hexStr.length % 2 !== 0) { 83 | throw new Error("Invalid hex string length."); 84 | } 85 | 86 | const byteValues: number[] = []; 87 | 88 | for (let i = 0; i < hexStr.length; i += 2) { 89 | const byte: number = parseInt(hexStr.slice(i, i + 2), 16); 90 | 91 | if (Number.isNaN(byte)) { 92 | throw new Error( 93 | `Invalid hex value at position ${i}: ${hexStr.slice(i, i + 2)}` 94 | ); 95 | } 96 | 97 | byteValues.push(byte); 98 | } 99 | 100 | return new Uint8Array(byteValues); 101 | } 102 | 103 | // eslint-disable-next-line max-len 104 | export const keypair = process.env.WALLET_PK 105 | ? Ed25519Keypair.fromSecretKey(hexStringToUint8Array(process.env.WALLET_PK)) 106 | : Ed25519Keypair.deriveKeypair(mnemonic); 107 | 108 | export const provider = new JsonRpcProvider( 109 | new Connection({ fullnode: "https://sui-rpc-mainnet.testnet-pride.com" }) 110 | ); 111 | export const signer = new RawSigner(keypair, provider); 112 | export const client = new NftClient(provider); 113 | export const orderbookClient = OrderbookFullClient.fromKeypair( 114 | keypair, 115 | provider, 116 | { 117 | packageObjectId: PACKAGE_OBJECT_ID, 118 | } 119 | ); 120 | export const kioskClient = KioskFullClient.fromKeypair(keypair, provider, { 121 | packageObjectId: PACKAGE_OBJECT_ID, 122 | }); 123 | export const biddingClient = BiddingContractClient.fromKeypair( 124 | keypair, 125 | provider, 126 | { 127 | packageObjectId: PACKAGE_OBJECT_ID, 128 | } 129 | ); 130 | export const safeClient = SafeFullClient.fromKeypair(keypair, provider, { 131 | packageObjectId: PACKAGE_OBJECT_ID, 132 | }); 133 | 134 | export const user = keypair.getPublicKey().toSuiAddress(); 135 | 136 | export const feeCollectorAddress = "0x29c8227d3c77ead5816b243be14dc53f09b59c09"; 137 | 138 | export async function fetchNfts() { 139 | const objs = await safeClient.client.getObjects(user); 140 | // console.log("objs: ", objs) 141 | return objs 142 | .filter((o) => o.data?.type === NFT_TYPE) 143 | .map((o) => o.data?.objectId); 144 | } 145 | 146 | export async function getGas() { 147 | const coins = ( 148 | await provider.getCoins({ owner: user, coinType: SUI_TYPE_ARG }) 149 | ).data; 150 | if (coins.length === 0) { 151 | throw new Error(`No gas object for user '${user}'`); 152 | } 153 | const coin = coins[0]; 154 | if (typeof coin !== "object") { 155 | throw new Error(`Unexpected coin type: ${JSON.stringify(coin)}`); 156 | } 157 | 158 | console.debug("debug: ", coin, coins); 159 | return coin.coinObjectId; 160 | } 161 | 162 | export async function getKiosks() { 163 | const pubkeyAddress = await signer.getAddress(); 164 | const kiosks = await kioskClient.getWalletKiosks(pubkeyAddress, { 165 | packageObjectId: KIOSK_ID, 166 | }); 167 | 168 | return kiosks; 169 | } 170 | -------------------------------------------------------------------------------- /examples/test.ts: -------------------------------------------------------------------------------- 1 | import { Ed25519Keypair, JsonRpcProvider, RawSigner } from "@mysten/sui.js"; 2 | 3 | const mnemonic = 4 | "celery access afford success prize fish huge vacuum shiver orient wine knock"; 5 | 6 | export const keypair = Ed25519Keypair.deriveKeypair(mnemonic); 7 | 8 | console.log( 9 | "keypair.getPublicKey().toSuiAddress()", 10 | keypair.getPublicKey().toSuiAddress() 11 | ); 12 | -------------------------------------------------------------------------------- /examples/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "sourceMap": true, 4 | "module": "commonjs", 5 | "moduleResolution": "node", 6 | "noEmitOnError": true, 7 | "esModuleInterop": true, 8 | "target": "es5", 9 | "declaration": true, 10 | "removeComments": false, 11 | "noImplicitAny": true, 12 | "forceConsistentCasingInFileNames": true, 13 | "resolveJsonModule":true, 14 | "typeRoots" : ["./node_modules/@types", "./typings"], 15 | }, 16 | "include": ["src"] 17 | } 18 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | const NO_COVERAGE = process.env.NO_COVERAGE === "1"; 2 | const CLEAR_CONSOLE = process.env.CLEAR_CONSOLE === "1"; 3 | 4 | const notice = () => console.log("Using Jest config from `jest.config.js`"); 5 | 6 | if (CLEAR_CONSOLE) { 7 | require("clear")(); 8 | console.log(); 9 | notice(); 10 | console.log("Clearing console due to CLEAR_CONSOLE=1"); 11 | } else { 12 | notice(); 13 | } 14 | 15 | if (NO_COVERAGE) { 16 | console.log("Coverage not collected due to NO_COVERAGE=1"); 17 | } 18 | 19 | console.log( 20 | "Type checking is disabled during Jest for performance reasons, use `jest typecheck` when necessary." 21 | ); 22 | 23 | module.exports = { 24 | rootDir: __dirname, 25 | roots: [""], 26 | cache: true, 27 | verbose: true, 28 | cacheDirectory: "/tmp/jest", 29 | moduleFileExtensions: ["ts", "tsx", "js", "json"], 30 | // preset configs 31 | // preset: 'ts-jest/presets/js-with-ts', 32 | // which files to test and which to ignore 33 | testMatch: ["**/__tests__/*.test.(ts|tsx)"], 34 | testPathIgnorePatterns: [ 35 | "/node_modules/", 36 | "/tmp/", 37 | "/coverage/", 38 | "/stories/", 39 | "/\\.storybook/", 40 | ], 41 | // don't watch for file changes in node_modules 42 | watchPathIgnorePatterns: ["/node_modules/"], 43 | // jest automock settings 44 | automock: false, 45 | unmockedModulePathPatterns: ["/node_modules/"], 46 | // test environment setup 47 | // setupFiles: [`${__dirname}/setup/setup.js`], 48 | // setupFilesAfterEnv: [`${__dirname}/setup/setupAfterEnv.ts`], 49 | // coverage settings 50 | collectCoverage: NO_COVERAGE === false, 51 | collectCoverageFrom: NO_COVERAGE 52 | ? [] 53 | : ["**/*.{ts,tsx}", "!**/*.d.ts", "!**/node_modules/**"], 54 | coveragePathIgnorePatterns: [ 55 | "/node_modules/", 56 | "\\.json$", 57 | "/__tests__/", 58 | "/stories/", 59 | "/\\.storybook/", 60 | ], 61 | 62 | globals: { 63 | "ts-jest": { 64 | tsConfig: `${__dirname}/tsconfig.json`, 65 | 66 | // https://huafu.github.io/ts-jest/user/config/diagnostics 67 | diagnostics: false, 68 | 69 | // Makes jest test run much faster, BUT, without type checking. 70 | // Type checking in CI is done with `tsc --noEmit` or `yarn typecheck` command. 71 | // https://huafu.github.io/ts-jest/user/config/isolatedModules 72 | isolatedModules: true, 73 | }, 74 | }, 75 | 76 | transformIgnorePatterns: [ 77 | "/node_modules/(?!(lodash-es|antd|[^/]+/es|rc-animate|rc-util)/).*", 78 | ], 79 | transform: { 80 | "\\.(ts|tsx)$": "ts-jest", 81 | "/node_modules/((lodash-es|[^/]+/es)|rc-animate|rc-util)/.*": "ts-jest", 82 | }, 83 | testTimeout: 30_000, 84 | }; 85 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@originbyte/js-sdk", 3 | "version": "0.29.66", 4 | "keywords": [ 5 | "originbyte", 6 | "sdk", 7 | "javascript", 8 | "sui", 9 | "move" 10 | ], 11 | "scripts": { 12 | "build": "./bin/build.sh", 13 | "fmt": "npm run format", 14 | "format": "prettier --write 'src/**/*.{ts, tsx}' 'examples/**/*.{ts, tsx}' '__tests__/**/*.{ts, tsx}'", 15 | "lint": "eslint 'src/**/*.{ts, tsx}'", 16 | "stress-build": "tsc --project ./__tests__/tsconfig.stress.json", 17 | "stress-local": "./bin/setup-test-env.sh ts-node __tests__/stresstest.ts", 18 | "test": "./bin/setup-test-env.sh yarn jest --detectOpenHandles --runInBand", 19 | "tsc": "tsc", 20 | "typecheck": "yarn tsc --project ./tsconfig-typecheck.json" 21 | }, 22 | "homepage": "https://originbyte.io/", 23 | "main": "dist/index.js", 24 | "typings": "dist/index.d.ts", 25 | "license": "Apache", 26 | "files": [ 27 | "dist/", 28 | "src/" 29 | ], 30 | "repository": { 31 | "type": "git", 32 | "url": "https://github.com/Origin-Byte/originbyte-js-sdk" 33 | }, 34 | "devDependencies": { 35 | "@types/bn.js": "^5.1.1", 36 | "@types/jest": "^29.5.1", 37 | "@types/node": "^18.16.1", 38 | "@typescript-eslint/eslint-plugin": "^5.38.1", 39 | "@typescript-eslint/parser": "^5.38.1", 40 | "dotenv": "^16.0.3", 41 | "eslint": "^8.39.0", 42 | "eslint-config-airbnb": "^19.0.4", 43 | "eslint-config-airbnb-base": "^15.0.0", 44 | "eslint-config-prettier": "^8.5.0", 45 | "eslint-config-typescript": "^3.0.0", 46 | "eslint-formatter-pretty": "^5.0.0", 47 | "eslint-import-resolver-typescript": "^3.5.1", 48 | "eslint-plugin-import": "^2.26.0", 49 | "eslint-plugin-jsx-a11y": "^6.0.2", 50 | "eslint-plugin-prettier": "^4.0.0", 51 | "eslint-plugin-typescript": "^0.14.0", 52 | "jest": "^29.5.0", 53 | "prettier": "^2.4.1", 54 | "ts-jest": "^29.1.0", 55 | "ts-node": "^10.9.1", 56 | "typescript": "^5.0.4", 57 | "typescript-eslint-parser": "^22.0.0" 58 | }, 59 | "dependencies": { 60 | "@mysten/sui.js": "0.34.0", 61 | "superstruct": "^1.0.3" 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/client/FullClient.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Ed25519Keypair, 3 | TransactionBlock, 4 | JsonRpcProvider, 5 | RawSigner, 6 | SignerWithProvider, 7 | TransactionEffects, 8 | } from "@mysten/sui.js"; 9 | import { TransactionResult } from "../transaction"; 10 | import { ReadClient } from "./ReadClient"; 11 | 12 | export class FullClient extends ReadClient { 13 | // eslint-disable-next-line 14 | constructor(public signer: SignerWithProvider) { 15 | super(signer.provider); 16 | // In case the provider is not connected, we default it in the constructor 17 | // of ReadClient. 18 | // Hence reconnecting here. 19 | signer.connect(this.provider); 20 | } 21 | 22 | public static fromKeypair( 23 | keypair: Ed25519Keypair, 24 | provider?: JsonRpcProvider 25 | ) { 26 | return new FullClient(new RawSigner(keypair, provider)); 27 | } 28 | 29 | async sendTx(transactionBlock: TransactionBlock) { 30 | return this.signer.signAndExecuteTransactionBlock({ 31 | transactionBlock, 32 | options: { showEffects: true, showObjectChanges: true }, 33 | }); 34 | } 35 | 36 | async sendTxWaitForEffects( 37 | tx: [TransactionBlock, TransactionResult] 38 | ): Promise { 39 | const [transactionBlock] = tx; 40 | // there's a bug in the SDKs - the return type doesn't match the actual response 41 | const res = await this.sendTx(transactionBlock); 42 | if (typeof res !== "object" || !("effects" in res)) { 43 | throw new Error( 44 | `Response does not contain effects: ${JSON.stringify(res)}` 45 | ); 46 | } 47 | 48 | if (res.effects.status.status === "failure") { 49 | throw new Error(`Transaction failed: ${res.effects.status.error}`); 50 | } 51 | 52 | return res.effects; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/client/ReadClient.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Connection, 3 | JsonRpcProvider, 4 | ObjectId, 5 | SuiAddress, 6 | SuiObjectDataFilter, 7 | SuiObjectDataOptions, 8 | } from "@mysten/sui.js"; 9 | import { TESTNET_URL } from "./consts"; 10 | import { DynField } from "./types"; 11 | 12 | export class ReadClient { 13 | // eslint-disable-next-line 14 | constructor( 15 | // eslint-disable-next-line 16 | public provider: JsonRpcProvider = new JsonRpcProvider( 17 | new Connection({ fullnode: TESTNET_URL }) 18 | ) 19 | ) { 20 | // 21 | } 22 | 23 | public static fromRpcUrl(url: string) { 24 | return new ReadClient( 25 | new JsonRpcProvider(new Connection({ fullnode: url })) 26 | ); 27 | } 28 | 29 | public async getObjects(owner: SuiAddress, options?: SuiObjectDataOptions, filter?: SuiObjectDataFilter) { 30 | const items = []; 31 | // eslint-disable-next-line prefer-const 32 | let {hasNextPage, nextCursor, data} = await this.getPaginatedObjects(owner, filter, options); 33 | items.push(...data); 34 | while (hasNextPage) { 35 | // eslint-disable-next-line no-await-in-loop 36 | const result = await this.getPaginatedObjects(owner, filter, options, nextCursor); 37 | hasNextPage = result.hasNextPage; 38 | nextCursor = result.nextCursor; 39 | items.push(...result.data); 40 | } 41 | return items; 42 | } 43 | 44 | public async getPaginatedObjects(owner: SuiAddress, filter?: SuiObjectDataFilter, options?: SuiObjectDataOptions, cursor: any = null) { 45 | return this.provider.getOwnedObjects({ 46 | owner, 47 | // filter, 48 | cursor, 49 | options 50 | }); 51 | } 52 | 53 | public async getObject(id: ObjectId) { 54 | const { data } = await this.provider.getObject({ 55 | id, 56 | options: { showContent: true }, 57 | }); 58 | 59 | return data; 60 | } 61 | 62 | public async getDynamicFields(id: ObjectId) { 63 | const dynamicFields: DynField[] = [] 64 | 65 | // eslint-disable-next-line prefer-const 66 | let {data, hasNextPage, nextCursor} = await this.provider.getDynamicFields({ 67 | parentId: id, 68 | }); 69 | dynamicFields.push(...data) 70 | 71 | while(hasNextPage) { 72 | // eslint-disable-next-line no-await-in-loop 73 | const result = await this.provider.getDynamicFields({ 74 | parentId: id, 75 | cursor: nextCursor, 76 | }); 77 | 78 | hasNextPage = result.hasNextPage; 79 | nextCursor = result.nextCursor; 80 | dynamicFields.push(...result.data) 81 | } 82 | 83 | return dynamicFields; 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/client/bidding/BiddingContractFullClient.ts: -------------------------------------------------------------------------------- 1 | import { Ed25519Keypair, JsonRpcProvider } from "@mysten/sui.js"; 2 | import { CreateBidInput } from "./types"; 3 | import { GlobalParams } from "../types"; 4 | import { FullClient } from "../FullClient"; 5 | import { closeBidTx, createBidTx, createBidWithCommissionTx, sellNftFromKioskTx, sellNftTx } from "./txBuilder"; 6 | import { BiddingContractReadClient } from "./BiddingContractReadClient"; 7 | 8 | export class BiddingContractClient extends BiddingContractReadClient { 9 | // eslint-disable-next-line no-useless-constructor 10 | constructor( 11 | public client: FullClient, 12 | public opts: Partial 13 | // eslint-disable-next-line no-empty-function 14 | ) { 15 | super(client, opts); 16 | } 17 | 18 | public static fromKeypair( 19 | keypair: Ed25519Keypair, 20 | provider?: JsonRpcProvider, 21 | opts?: Partial 22 | ) { 23 | return new BiddingContractClient( 24 | FullClient.fromKeypair(keypair, provider), 25 | opts 26 | ); 27 | } 28 | 29 | static createBidTx = createBidTx; 30 | 31 | static createBidWithCommissionTx = createBidWithCommissionTx; 32 | 33 | static sellNftFromKioskTx = sellNftFromKioskTx; 34 | 35 | static sellNftTx = sellNftTx; 36 | 37 | static closeBid = closeBidTx; 38 | 39 | async createBid(p: CreateBidInput): Promise { 40 | const effect = await this.client.sendTxWaitForEffects( 41 | createBidTx({ 42 | ...this.opts, 43 | ...p 44 | }) 45 | ); 46 | 47 | return effect; 48 | } 49 | 50 | } -------------------------------------------------------------------------------- /src/client/bidding/BiddingContractReadClient.ts: -------------------------------------------------------------------------------- 1 | import { JsonRpcProvider } from "@mysten/sui.js"; 2 | import { ReadClient } from "../ReadClient"; 3 | import { GlobalParams } from "../types"; 4 | import { DEFAULT_KIOSK_MODULE, DEFAULT_PACKAGE_ID } from "../consts"; 5 | 6 | export class BiddingContractReadClient { 7 | // eslint-disable-next-line 8 | constructor( 9 | // eslint-disable-next-line 10 | public client: ReadClient = new ReadClient(), 11 | // eslint-disable-next-line 12 | public opts: Partial = {} 13 | ) { 14 | // 15 | } 16 | 17 | public static fromProvider(provider: JsonRpcProvider) { 18 | return new BiddingContractReadClient(new ReadClient(provider)); 19 | } 20 | 21 | public static fromRpcUrl(url: string) { 22 | return new BiddingContractReadClient(ReadClient.fromRpcUrl(url)); 23 | } 24 | 25 | public get package() { 26 | return this.opts.packageObjectId ?? DEFAULT_PACKAGE_ID; 27 | } 28 | 29 | public get module() { 30 | return this.opts.moduleName ?? DEFAULT_KIOSK_MODULE; 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /src/client/bidding/txBuilder.ts: -------------------------------------------------------------------------------- 1 | import { TransactionBlock } from "@mysten/sui.js"; 2 | import { TransactionBlockArgument, TransactionResult, txObj as txCommon } from "../../transaction"; 3 | import { GlobalParams } from "../types"; 4 | import { DEFAULT_BIDDING_MODULE, DEFAULT_PACKAGE_ID } from "../consts"; 5 | import { CloseBidParams, CreateBidInput, CreateBidWithCommissionInput, SellNft, SellNftFromKiosk } from "./types"; 6 | import { wrapToObject } from "../utils"; 7 | 8 | const txObj = ( 9 | fun: string, 10 | p: GlobalParams, 11 | args: ( 12 | tx: TransactionBlock 13 | ) => (TransactionBlockArgument | TransactionResult)[], 14 | tArgs: string[] 15 | ): [TransactionBlock, TransactionResult] => { 16 | return txCommon( 17 | { 18 | packageObjectId: p.packageObjectId ?? DEFAULT_PACKAGE_ID, 19 | moduleName: p.moduleName ?? DEFAULT_BIDDING_MODULE, 20 | fun, 21 | transaction: p.transaction, 22 | }, 23 | args, 24 | tArgs 25 | ); 26 | } 27 | 28 | 29 | export const createBidTx = (p: CreateBidInput) => { 30 | return txObj( 31 | "create_bid", 32 | p, 33 | (tx) => [ 34 | typeof p.buyersKiosk === "string" ? tx.object(p.buyersKiosk) : p.buyersKiosk, 35 | tx.object(p.nft), 36 | tx.pure(String(p.price)), 37 | wrapToObject(tx, p.wallet) 38 | ], 39 | [p.ft] 40 | ); 41 | }; 42 | 43 | export const createBidWithCommissionTx = async (p: CreateBidWithCommissionInput) => { 44 | return txObj( 45 | "create_bid_with_commission", 46 | p, 47 | (tx) => [ 48 | typeof p.buyersKiosk === "string" ? tx.object(p.buyersKiosk) : p.buyersKiosk, 49 | tx.object(p.nft), 50 | tx.pure(String(p.price)), 51 | tx.object(p.beneficiary), 52 | tx.pure(String(p.commission)), 53 | wrapToObject(tx, p.wallet) 54 | ], 55 | [p.ft] 56 | ); 57 | }; 58 | 59 | export const sellNftFromKioskTx = (p: SellNftFromKiosk) => { 60 | return txObj( 61 | "sell_nft_from_kiosk", 62 | p, 63 | (tx) => [ 64 | tx.object(p.bid), 65 | wrapToObject(tx, p.sellersKiosk), 66 | wrapToObject(tx, p.buyersKiosk), 67 | tx.object(p.nft) 68 | ], 69 | [p.nftType, p.ft] 70 | ); 71 | }; 72 | 73 | export const sellNftTx = (p: SellNft) => { 74 | return txObj( 75 | "sell_nft", 76 | p, 77 | (tx) => [ 78 | tx.object(p.bid), 79 | wrapToObject(tx, p.buyersKiosk), 80 | tx.object(p.nft) 81 | ], 82 | [p.nftType, p.ft] 83 | ); 84 | }; 85 | 86 | export const closeBidTx = (p: CloseBidParams) => { 87 | return txObj( 88 | "close_bid", 89 | p, 90 | (tx) => [ 91 | tx.object(p.bid) 92 | ], 93 | [p.ft] 94 | ) 95 | } 96 | -------------------------------------------------------------------------------- /src/client/bidding/types.ts: -------------------------------------------------------------------------------- 1 | import { ObjectId } from "@mysten/sui.js"; 2 | import { 3 | BidParam, 4 | BuyersKioskParam, 5 | CommissionParams, 6 | FTParam, 7 | GlobalParams, 8 | NftParam, 9 | NftTypeParam, 10 | PriceParam, 11 | SellersKioskParam, 12 | WalletParam 13 | } from "../types"; 14 | 15 | export interface BidCommission { 16 | beneficiary: string; 17 | amount: string; 18 | } 19 | 20 | export interface Bid { 21 | id: string; 22 | nft: string; 23 | buyer: string; 24 | kiosk: string; 25 | offer: string; 26 | commission?: BidCommission; 27 | } 28 | 29 | export interface CreateBidInput extends GlobalParams, FTParam, BuyersKioskParam, NftParam, WalletParam, PriceParam {} 30 | 31 | export interface CreateBidWithCommissionInput 32 | extends GlobalParams, FTParam, NftParam, WalletParam, BuyersKioskParam, CommissionParams, PriceParam {} 33 | 34 | export interface SellNftFromKiosk extends GlobalParams, FTParam, SellersKioskParam, BuyersKioskParam, NftParam, NftTypeParam { 35 | bid: ObjectId, 36 | } 37 | 38 | export interface SellNft extends GlobalParams, FTParam, NftParam, BuyersKioskParam, NftTypeParam { 39 | bid: ObjectId 40 | } 41 | 42 | export interface CloseBidParams extends GlobalParams, BidParam, FTParam {} -------------------------------------------------------------------------------- /src/client/consts.ts: -------------------------------------------------------------------------------- 1 | export const DEFAULT_PACKAGE_ID = "0xda5ce01d0e365f2aac8df7d85d1cdfe271fd75db338daf248132991d74c2f1c8"; 2 | export const DEFAULT_SUI_PACKAGE_ID = "0x2"; 3 | export const DEFAULT_SUI_TRANSFER_MODULE = "transfer"; 4 | export const DEFAULT_TRANSFER_REQUEST_MODULE = "transfer_request"; 5 | export const DEFAULT_SAFE_MODULE = "safe"; 6 | export const DEFAULT_KIOSK_MODULE = "ob_kiosk"; 7 | export const DEFAULT_GAS_BUDGET = 100_000; 8 | export const TESTNET_URL = "https://testnet.suiet.app"; 9 | export const DEFAULT_ORDERBOOK_MODULE = "orderbook"; 10 | export const DEFAULT_BIDDING_MODULE = "bidding"; 11 | export const DEFAULT_PAGINATION_LIMIT = 100; 12 | -------------------------------------------------------------------------------- /src/client/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./parsers"; 2 | 3 | export * from "./orderbook/OrderbookFullClient"; 4 | export * from "./orderbook/OrderbookReadClient"; 5 | 6 | export * from "./safe/SafeReadClient"; 7 | export * from "./safe/SafeFullClient"; 8 | 9 | export * from "./bidding/BiddingContractFullClient"; 10 | export * from "./bidding/types"; 11 | 12 | export * from "./transfer-request/TransferRequestFullClient"; 13 | export * from "./transfer-request/types"; 14 | 15 | export * from "./kiosk/KioskFullClient"; 16 | export * from "./kiosk/types"; 17 | 18 | export * from "./NftClient"; 19 | export * from "./types"; 20 | -------------------------------------------------------------------------------- /src/client/kiosk/KioskFullClient.ts: -------------------------------------------------------------------------------- 1 | import { Ed25519Keypair, JsonRpcProvider } from "@mysten/sui.js"; 2 | import { FullClient } from "../FullClient"; 3 | import { GlobalParams } from "../types"; 4 | import { KioskReadClient } from "./KioskReadClient"; 5 | import { 6 | authExclusiveTransferTx, 7 | authTransferTx, 8 | createKioskTx, 9 | delistNftAsOwnerTx, 10 | depositTx, 11 | disableDepositsOfCollectionTx, 12 | enableAnyDepositTx, 13 | getObjectIdTx, 14 | newKioskTx, 15 | removeAuthTransferAsOwnerTx, 16 | removeAuthTransferTx, 17 | restrictDepositsTx, 18 | shareKioskTx, 19 | transferDelegatedTx, 20 | transferSignedTx, 21 | withdrawNftTx 22 | } from "./txBuilder"; 23 | import { DEFAULT_PACKAGE_ID } from "../consts"; 24 | import { enableDepositsOfCollectionTx } from "../safe/txBuilder"; 25 | 26 | export class KioskFullClient extends KioskReadClient { 27 | constructor( 28 | public client: FullClient, 29 | // eslint-disable-next-line 30 | public opts: Partial = {} 31 | ) { 32 | super(client, opts); 33 | } 34 | 35 | public static fromKeypair( 36 | keypair: Ed25519Keypair, 37 | provider?: JsonRpcProvider, 38 | opts?: Partial 39 | ) { 40 | return new KioskFullClient(FullClient.fromKeypair(keypair, provider), opts); 41 | } 42 | 43 | static createKioskTx = createKioskTx; 44 | 45 | static getObjectIdTx = getObjectIdTx; 46 | 47 | static newKioskTx = newKioskTx; 48 | 49 | static shareKioskTx = shareKioskTx; 50 | 51 | static depositTx = depositTx; 52 | 53 | static authTransferTx = authTransferTx; 54 | 55 | static authExclusiveTransferTx = authExclusiveTransferTx; 56 | 57 | static transferDelegated = transferDelegatedTx; 58 | 59 | static transferSigned = transferSignedTx; 60 | 61 | static withdrawNftTx = withdrawNftTx; 62 | 63 | // static withdrawNftSignedTx = withdrawNftSignedTx; TODO 64 | 65 | // static transferBetweenOwned = transferBetweenOwned; TODO 66 | 67 | // static setTransferRequestAuth = setTransferRequestAuthTx; TODO 68 | 69 | // static getTransferRequestAuth = getTransferRequestAuthTx; TODO 70 | 71 | static delistNftAsOwnerTx = delistNftAsOwnerTx; 72 | 73 | static removeAuthTransferAsOwnerTx = removeAuthTransferAsOwnerTx; 74 | 75 | static removeAuthTransferTx = removeAuthTransferTx; 76 | 77 | static restrictDepositsTx = restrictDepositsTx; 78 | 79 | static enableAnyDepositTx = enableAnyDepositTx; 80 | 81 | static disableDepositOfCollectionTx = disableDepositsOfCollectionTx; 82 | 83 | static enableDepositsOfCollectionTx = enableDepositsOfCollectionTx; 84 | 85 | public createKiosk(params: GlobalParams) { 86 | return this.client.sendTxWaitForEffects(createKioskTx({ 87 | ...this.opts, 88 | ...params, 89 | packageObjectId: DEFAULT_PACKAGE_ID 90 | })); 91 | } 92 | 93 | 94 | 95 | } -------------------------------------------------------------------------------- /src/client/kiosk/KioskReadClient.ts: -------------------------------------------------------------------------------- 1 | import { JsonRpcProvider, ObjectId, SuiAddress } from "@mysten/sui.js"; 2 | import { ReadClient } from "../ReadClient"; 3 | import { GlobalParams } from "../types"; 4 | import { DEFAULT_KIOSK_MODULE, DEFAULT_PACKAGE_ID } from "../consts"; 5 | import { Kiosk, OwnerToken } from "./types"; 6 | 7 | const KIOSK_ITEM_TYPE = "0x0000000000000000000000000000000000000000000000000000000000000002::kiosk::Item"; 8 | 9 | export class KioskReadClient { 10 | // eslint-disable-next-line 11 | constructor( 12 | // eslint-disable-next-line 13 | public client: ReadClient = new ReadClient(), 14 | // eslint-disable-next-line 15 | public opts: Partial = {} 16 | ) { 17 | // 18 | } 19 | 20 | public static fromProvider(provider: JsonRpcProvider, opts: Partial) { 21 | return new KioskReadClient(new ReadClient(provider), opts); 22 | } 23 | 24 | public static fromRpcUrl(url: string, opts: Partial) { 25 | return new KioskReadClient(ReadClient.fromRpcUrl(url), opts); 26 | } 27 | 28 | public get package() { 29 | return this.opts.packageObjectId ?? DEFAULT_PACKAGE_ID; 30 | } 31 | 32 | public get module() { 33 | return this.opts.moduleName ?? DEFAULT_KIOSK_MODULE; 34 | } 35 | 36 | public async fetchOwnerTokens( 37 | user: SuiAddress, 38 | p: Partial = {} 39 | ): Promise { 40 | const ownerTokenType = `${p.packageObjectId || this.package}::${p.moduleName || this.module 41 | }::OwnerToken`; 42 | const objs = (await this.client.getObjects(user, { 43 | showType: true, 44 | showContent: true 45 | }, { 46 | Package: p.packageObjectId || this.package 47 | })); 48 | return objs 49 | .filter((o) => o.data.type === ownerTokenType) 50 | .map((o) => (o.data.content as any)?.fields as OwnerToken); 51 | } 52 | 53 | public async fetchSafeByOwnerCap(walletAddress: SuiAddress) { 54 | const kioskId = (await this.fetchOwnerTokens(walletAddress))[0].kiosk; 55 | return this.fetchKiosk(kioskId); 56 | } 57 | 58 | public async getWalletKiosks(user: SuiAddress, p: Partial = {}): Promise { 59 | return Promise.all((await this.fetchOwnerTokens(user, p)).map(async (ot) => this.fetchKiosk(ot.kiosk))); 60 | } 61 | 62 | public async getWalletKiosk(user: SuiAddress, p: Partial = {}): Promise { 63 | const ownerTokens = await this.fetchOwnerTokens(user, p); 64 | if (ownerTokens.length > 0) { 65 | return this.fetchKiosk(ownerTokens[0].kiosk); 66 | } 67 | return undefined; 68 | } 69 | 70 | public async getWalletKioskId(user: SuiAddress, p: Partial = {}) { 71 | const ownerTokens = await this.fetchOwnerTokens(user, p); 72 | if (ownerTokens.length > 0) { 73 | return ownerTokens[0].kiosk; 74 | } 75 | return undefined; 76 | } 77 | 78 | /* 79 | Returns all the nfts for a specific kiosks 80 | */ 81 | public async getKioskNfts(kioskId: ObjectId, p: Partial = {}) { 82 | const kiosk = await this.client.getDynamicFields(kioskId); 83 | return kiosk.filter((item) => 84 | item.name.type === KIOSK_ITEM_TYPE 85 | ).map((item) => ({ kioskId, nft: item.name.value.id })); 86 | } 87 | 88 | /* 89 | Returns all the nfts that are contained in every kiosk connected to a specific address 90 | */ 91 | public async getAllNftKioskAssociations(user: SuiAddress, p: Partial = {}) { 92 | const kiosks = await this.getWalletKiosks(user, p); 93 | return (await Promise.all(kiosks.map((kiosk) => this.getKioskNfts((kiosk.id as any).id)))).flat(); 94 | } 95 | 96 | public async fetchKiosk(kioskId: ObjectId): Promise { 97 | const details = await this.client.getObject(kioskId); 98 | if (typeof details !== "object" || !("content" in details) || !("fields" in details.content)) { 99 | throw new Error("Cannot fetch owner cap details"); 100 | } 101 | 102 | return details.content.fields as Kiosk; 103 | } 104 | 105 | } 106 | -------------------------------------------------------------------------------- /src/client/kiosk/txBuilder.ts: -------------------------------------------------------------------------------- 1 | import { TransactionBlock } from "@mysten/sui.js"; 2 | import { TransactionBlockArgument, TransactionResult , txObj as txCommon } from "../../transaction"; 3 | import { GlobalParams, KioskParam } from "../types"; 4 | import { DEFAULT_KIOSK_MODULE, DEFAULT_PACKAGE_ID } from "../consts"; 5 | import { 6 | AuthExclusiveTransferProps, 7 | AuthTransferProps, 8 | DelistNftAsOwnerInputProps, 9 | DepositProps, 10 | DisableDepositsOfCollectionProps, 11 | EnableAnyDepositProps, 12 | EnableDepositsOfCollectionProps, 13 | RemoveAuthTransferAsOwnerProps, 14 | RemoveAuthTransferProps, 15 | RestrictDepositsProps, 16 | SetPermissionlessToPermissionedProps, 17 | TransferBetweenOwnedProps, 18 | TransferDelegatedProps, 19 | TransferSignedProps, 20 | WithDrawNftProps 21 | } from "./types"; 22 | import { wrapToObject } from "../utils"; 23 | import { SuiContractFullClient } from "../sui-contract/SuiContractFullClient"; 24 | 25 | 26 | function txObj( 27 | fun: string, 28 | p: GlobalParams, 29 | args: ( 30 | tx: TransactionBlock 31 | ) => (TransactionBlockArgument | TransactionResult)[], 32 | tArgs: string[] 33 | ): [TransactionBlock, TransactionResult] { 34 | // eslint-disable-next-line no-undef 35 | return txCommon( 36 | { 37 | packageObjectId: p.packageObjectId ?? DEFAULT_PACKAGE_ID, 38 | moduleName: p.moduleName ?? DEFAULT_KIOSK_MODULE, 39 | fun, 40 | transaction: p.transaction, 41 | }, 42 | args, 43 | tArgs 44 | ); 45 | } 46 | 47 | export const createKioskTx = (params: GlobalParams): [TransactionBlock, TransactionResult] => { 48 | return txObj( 49 | "create_for_sender", 50 | params, 51 | () => [], []); 52 | } 53 | 54 | export const newKioskTx = (params: GlobalParams): [TransactionBlock, TransactionResult] => { 55 | return txObj( 56 | "new", 57 | params, 58 | () => [], []); 59 | } 60 | 61 | export const shareKioskTx = (params: GlobalParams & KioskParam): [TransactionBlock, TransactionResult] => { 62 | return SuiContractFullClient.publicShareObject({ 63 | transaction: params.transaction, 64 | value: params.kiosk, 65 | type: "0x2::kiosk::Kiosk" 66 | }); 67 | } 68 | 69 | export const getObjectIdTx = (params: GlobalParams & KioskParam): [TransactionBlock, TransactionResult] => { 70 | return SuiContractFullClient.getId({ 71 | transaction: params.transaction, 72 | value: params.kiosk, 73 | type: "0x2::kiosk::Kiosk" 74 | }); 75 | } 76 | 77 | export const setPermissionLessToPermissioned = (params: SetPermissionlessToPermissionedProps) => { 78 | throw new Error("Not yet implemented"); 79 | /* return txObj( 80 | "set_permissionless_to_permissioned", 81 | params, 82 | (tx) => [ 83 | tx.object(params.kiosk), 84 | tx.pure(props.user) 85 | ], 86 | [] 87 | ); */ 88 | } 89 | 90 | /// Always works if the sender is the owner. 91 | /// Fails if permissionless deposits are not enabled for `T`. 92 | /// See `DepositSetting`. 93 | export const depositTx = (params: DepositProps): [TransactionBlock, TransactionResult] => { 94 | return txObj( 95 | "deposit", 96 | params, 97 | (tx) => [ 98 | wrapToObject(tx, params.kiosk), 99 | tx.object(params.nft) 100 | ], [params.nftType]); 101 | } 102 | 103 | // === Withdraw from the Kiosk === 104 | 105 | /// Authorizes given entity to take given NFT out. 106 | /// The entity must prove with their `&UID` in `transfer_delegated` or 107 | /// must be the signer in `transfer_signed`. 108 | /// 109 | /// Use the `object::id_to_address` to authorize entities which only live 110 | /// on chain. 111 | export const authTransferTx = (params: AuthTransferProps) => { 112 | return txObj( 113 | "auth_transfer", 114 | params, 115 | (tx) => [ 116 | wrapToObject(tx, params.kiosk), 117 | tx.object(params.nft), 118 | tx.pure(params.entity) 119 | ], 120 | [] 121 | ); 122 | } 123 | 124 | /// Authorizes ONLY given entity to take given NFT out. 125 | /// No one else (including the owner) can perform a transfer. 126 | /// 127 | /// The entity must prove with their `&UID` in `transfer_delegated`. 128 | /// 129 | /// Only the given entity can then delist their listing. 130 | /// This is a dangerous action to be used only with audited contracts 131 | /// because the NFT is locked until given entity agrees to release it. 132 | export const authExclusiveTransferTx = (params: AuthExclusiveTransferProps) => { 133 | return txObj( 134 | "auth_exclusive_transfer", 135 | params, 136 | (tx) => [ 137 | wrapToObject(tx, params.kiosk), 138 | tx.object(params.nft), 139 | tx.object(params.entity) 140 | ], 141 | [] 142 | ) 143 | } 144 | 145 | /// Can be called by an entity that has been authorized by the owner to 146 | /// withdraw given NFT. 147 | /// 148 | /// Returns a builder to the calling entity. 149 | /// The entity then populates it with trade information of which fungible 150 | /// tokens were paid. 151 | /// 152 | /// The builder then _must_ be transformed into a hot potato `TransferRequest` 153 | /// which is then used by logic that has access to `TransferPolicy`. 154 | /// 155 | /// Can only be called on kiosks in the OB ecosystem. 156 | /// 157 | /// We adhere to the deposit rules of the target kiosk. 158 | /// If we didn't, it'd be pointless to even have them since a spammer 159 | /// could simply simulate a transfer and select any target. 160 | export const transferDelegatedTx = (params: TransferDelegatedProps) => { 161 | return txObj( 162 | "transfer_delegated", 163 | params, 164 | (tx) => [ 165 | tx.object(params.source), 166 | tx.object(params.target), 167 | tx.object(params.nft), 168 | tx.object(params.entityId) 169 | ], 170 | [ 171 | params.transferType 172 | ] 173 | ) 174 | } 175 | 176 | /// Similar to `transfer_delegated` but instead of proving origin with 177 | /// `&UID` we check that the entity is the signer. 178 | /// 179 | /// This will always work if the signer is the owner of the kiosk 180 | export const transferSignedTx = (params: TransferSignedProps) => { 181 | return txObj( 182 | "transfer_signed", 183 | params, 184 | (tx) => [ 185 | tx.object(params.source), 186 | tx.object(params.target), 187 | tx.object(params.nft) 188 | ], 189 | [ 190 | params.transferType 191 | ] 192 | ) 193 | } 194 | 195 | /// We allow withdrawing NFTs for some use cases. 196 | /// If an NFT leaves our kiosk ecosystem, we can no longer guarantee 197 | /// royalty enforcement. 198 | /// Therefore, creators might not allow entities which enable withdrawing 199 | /// NFTs to trade their collection. 200 | /// 201 | /// You almost certainly want to use `transfer_delegated`. 202 | /// 203 | /// Handy for migrations. 204 | export const withdrawNftTx = (params: WithDrawNftProps): [TransactionBlock, TransactionResult] => { 205 | return txObj( 206 | "withdraw_nft", 207 | params, 208 | (tx) => [ 209 | wrapToObject(tx, params.kiosk), 210 | tx.object(params.nft), 211 | tx.pure(params.entityId) 212 | ], [ 213 | params.nftType 214 | ]); 215 | } 216 | 217 | /// If both kiosks are owned by the same user, then we allow free transfer. 218 | export const transferBetweenOwnedTx = (params: TransferBetweenOwnedProps): [TransactionBlock, TransactionResult] => { 219 | return txObj( 220 | "transfer_between_owned", 221 | params, 222 | (tx) => [ 223 | tx.object(params.source), 224 | tx.object(params.target), 225 | tx.object(params.nft) 226 | ], [ 227 | params.nftType 228 | ]); 229 | } 230 | 231 | /// Removes _all_ entities from access to the NFT. 232 | /// Cannot be performed if the NFT is exclusively listed. 233 | export const delistNftAsOwnerTx = (params: DelistNftAsOwnerInputProps): [TransactionBlock, TransactionResult] => { 234 | return txObj( 235 | "delist_nft_as_owner", 236 | params, 237 | (tx) => [ 238 | wrapToObject(tx, params.kiosk) 239 | ], 240 | [] 241 | ) 242 | } 243 | 244 | 245 | /// This is the only path to delist an exclusively listed NFT. 246 | export const removeAuthTransferTx = (params: RemoveAuthTransferProps): [TransactionBlock, TransactionResult] => { 247 | return txObj( 248 | "remove_auth_transfer", 249 | params, 250 | (tx) => [ 251 | wrapToObject(tx, params.kiosk), 252 | tx.object(params.nft), 253 | tx.pure(params.entity) 254 | ], 255 | [] 256 | ) 257 | } 258 | 259 | 260 | /// Removes a specific NFT from access to the NFT. 261 | /// Cannot be performed if the NFT is exclusively listed. 262 | export const removeAuthTransferAsOwnerTx = (params: RemoveAuthTransferAsOwnerProps): [TransactionBlock, TransactionResult] => { 263 | return txObj( 264 | "remove_auth_transfer_as_owner", 265 | params, 266 | (tx) => [ 267 | wrapToObject(tx, params.kiosk), 268 | tx.object(params.nft), 269 | tx.pure(params.entity) 270 | ], 271 | [] 272 | ) 273 | } 274 | 275 | // === Configure deposit settings === 276 | 277 | /// Only owner or allowlisted collections can deposit. 278 | export const restrictDepositsTx = (params: RestrictDepositsProps): [TransactionBlock, TransactionResult] => { 279 | return txObj( 280 | "restrict_deposits", 281 | params, 282 | (tx) => [ 283 | wrapToObject(tx, params.kiosk), 284 | ], 285 | [] 286 | ) 287 | } 288 | 289 | 290 | /// No restriction on deposits. 291 | export const enableAnyDepositTx = (params: EnableAnyDepositProps): [TransactionBlock, TransactionResult] => { 292 | return txObj( 293 | "enable_any_deposit", 294 | params, 295 | (tx) => [ 296 | wrapToObject(tx, params.kiosk), 297 | ], 298 | [] 299 | ) 300 | } 301 | 302 | 303 | /// The owner can restrict deposits into the `Kiosk` from given 304 | /// collection. 305 | /// 306 | /// However, if the flag `DepositSetting::enable_any_deposit` is set to 307 | /// true, then it takes precedence. 308 | export const disableDepositsOfCollectionTx = (params: DisableDepositsOfCollectionProps) => { 309 | return txObj( 310 | "disable_deposits_of_collection", 311 | params, 312 | (tx) => [ 313 | wrapToObject(tx, params.kiosk), 314 | ], 315 | [ 316 | params.collectionType 317 | ] 318 | ) 319 | } 320 | 321 | 322 | /// The owner can enable deposits into the `Kiosk` from given 323 | /// collection. 324 | /// 325 | /// However, if the flag `Kiosk::enable_any_deposit` is set to 326 | /// true, then it takes precedence anyway. 327 | export const enableDespositsOfCollectionTx = (params: EnableDepositsOfCollectionProps): [TransactionBlock, TransactionResult] => { 328 | return txObj( 329 | "enable_deposits_of_collection", 330 | params, 331 | (tx) => [ 332 | wrapToObject(tx, params.kiosk), 333 | ], 334 | [params.collectionType] 335 | ) 336 | } -------------------------------------------------------------------------------- /src/client/kiosk/types.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable camelcase */ 2 | import { ObjectId, SuiAddress } from "@mysten/sui.js"; 3 | import { GlobalParams, KioskParam, NftParam, WithNftType } from "../types"; 4 | 5 | export interface OwnerToken { 6 | id: ObjectId, 7 | kiosk: ObjectId, 8 | owner: SuiAddress 9 | } 10 | 11 | export interface Kiosk { 12 | id: { id: string }, 13 | profits: {value: number}, 14 | owner: string, 15 | item_count: number, 16 | allow_extensions: boolean 17 | } 18 | 19 | export interface KioskNftItem { 20 | name: { 21 | type: string, 22 | value: { 23 | id: string 24 | } 25 | }, 26 | value: any 27 | } 28 | 29 | export interface DepositProps extends KioskParam, NftParam, WithNftType, GlobalParams {} 30 | export interface SetPermissionlessToPermissionedProps extends GlobalParams, KioskParam { 31 | user: string 32 | } 33 | export interface WithDrawNftProps extends KioskParam, NftParam, WithNftType, GlobalParams { 34 | entityId: string 35 | } 36 | 37 | export interface WithDrawNftSignedProps extends KioskParam, NftParam, WithNftType, GlobalParams {} 38 | 39 | export interface TransferBetweenOwnedProps extends NftParam, WithNftType, GlobalParams { 40 | source: string, 41 | target: string 42 | } 43 | 44 | export interface DelistNftAsOwnerInputProps extends GlobalParams, KioskParam, NftParam {} 45 | 46 | export interface AuthTransferProps extends GlobalParams, KioskParam, NftParam { 47 | entity: string 48 | } 49 | 50 | export interface AuthExclusiveTransferProps extends GlobalParams, NftParam, KioskParam { 51 | entity: string 52 | } 53 | 54 | export interface TransferDelegatedProps extends GlobalParams, NftParam { 55 | source: string, 56 | target: string, 57 | entityId: string, 58 | transferType: string 59 | } 60 | 61 | export interface TransferSignedProps extends GlobalParams, NftParam { 62 | source: string, 63 | target: string, 64 | transferType: string 65 | } 66 | 67 | export interface SetTransferRequestProps extends GlobalParams { 68 | transferRequestType: string, 69 | transferRequest: ObjectId, 70 | auth: ObjectId 71 | } 72 | 73 | export interface GetTransferRequestProps extends GlobalParams { 74 | transferRequestType: string, 75 | transferRequest: ObjectId 76 | } 77 | 78 | // Delisting of NFTs 79 | 80 | export interface DelistNftAsOwnerProps extends GlobalParams, KioskParam, NftParam {} 81 | 82 | export interface RemoveAuthTransferAsOwnerProps extends GlobalParams, KioskParam, NftParam { 83 | entity: string 84 | } 85 | 86 | export interface RemoveAuthTransferProps extends GlobalParams, KioskParam, NftParam { 87 | entity: string 88 | } 89 | 90 | 91 | // DEPOSIT SETTING CONFIGURATION 92 | 93 | export interface RestrictDepositsProps extends GlobalParams, KioskParam {} 94 | 95 | export interface EnableAnyDepositProps extends GlobalParams, KioskParam {} 96 | export interface DisableDepositsOfCollectionProps extends GlobalParams, KioskParam { 97 | collectionType: string 98 | } 99 | export interface EnableDepositsOfCollectionProps extends GlobalParams, KioskParam { 100 | collectionType: string 101 | } 102 | 103 | // NFT Accessors 104 | 105 | // TODO 106 | 107 | 108 | -------------------------------------------------------------------------------- /src/client/orderbook/OrderbookFullClient.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Ed25519Keypair, 3 | ObjectId, 4 | JsonRpcProvider, 5 | SuiAddress, 6 | } from "@mysten/sui.js"; 7 | import { FullClient } from "../FullClient"; 8 | import { GlobalParams } from "../types"; 9 | import { OrderbookReadClient } from "./OrderbookReadClient"; 10 | import { 11 | buyNftTx, 12 | cancelAskTx, 13 | cancelBidTx, 14 | createAskTx, 15 | createAskWithCommissionTx, 16 | createBidTx, 17 | createBidWithCommissionTx, 18 | newOrderbookTx, 19 | finishTradeTx, 20 | editAskTx, 21 | editBidTx, 22 | createProtectionTx, 23 | CreateProtectionParams, 24 | createUnprotectedOrderbookTx, 25 | createUnprotectedTx, 26 | shareOrderbookTx, 27 | marketBuyTx, 28 | marketSellTx, 29 | } from "./txBuilder"; 30 | 31 | import { TransactionResult } from "../../transaction"; 32 | 33 | export class OrderbookFullClient extends OrderbookReadClient { 34 | constructor( 35 | public client: FullClient, 36 | // eslint-disable-next-line 37 | public opts: Partial = {} 38 | ) { 39 | super(client); 40 | } 41 | 42 | public static fromKeypair( 43 | keypair: Ed25519Keypair, 44 | provider?: JsonRpcProvider, 45 | opts?: Partial 46 | ) { 47 | return new OrderbookFullClient( 48 | FullClient.fromKeypair(keypair, provider), 49 | opts 50 | ); 51 | } 52 | 53 | static newOrderbookTx = newOrderbookTx; 54 | 55 | static createUnprotectedOrderbookTx = createUnprotectedOrderbookTx; 56 | 57 | static createUnprotectedTx = createUnprotectedTx; 58 | 59 | static shareOrderbookTx = shareOrderbookTx; 60 | 61 | static createProtectionTx = createProtectionTx; 62 | 63 | static createAskTx = createAskTx; 64 | 65 | static createAskWithCommissionTx = createAskWithCommissionTx; 66 | 67 | static cancelAskTx = cancelAskTx; 68 | 69 | static editAskTx = editAskTx; 70 | 71 | static editBidTx = editBidTx; 72 | 73 | static createBidTx = createBidTx; 74 | 75 | static createBidWithCommissionTx = createBidWithCommissionTx; 76 | 77 | static cancelBidTx = cancelBidTx; 78 | 79 | static buyNftTx = buyNftTx; 80 | 81 | static finishTradeTx = finishTradeTx; 82 | 83 | static marketBuyTx = marketBuyTx; 84 | 85 | static marketSellTx = marketSellTx; 86 | 87 | public async createProtection(p: CreateProtectionParams) { 88 | const effects = await this.client.sendTxWaitForEffects( 89 | createProtectionTx({ 90 | ...this.opts, 91 | ...p, 92 | }) 93 | ); 94 | 95 | return effects 96 | } 97 | 98 | public async createAsk(p: { 99 | collection: string; 100 | ft: string; 101 | orderbook: ObjectId; 102 | sellersKiosk: ObjectId | TransactionResult; 103 | price: number; 104 | nft: ObjectId; 105 | }) { 106 | const effects = await this.client.sendTxWaitForEffects( 107 | createAskTx({ 108 | ...this.opts, 109 | ...p, 110 | }) 111 | ); 112 | 113 | return effects; 114 | } 115 | 116 | public async createAskWithCommission(p: { 117 | collection: string; 118 | orderbook: ObjectId; 119 | price: number; 120 | sellersKiosk: ObjectId | TransactionResult; 121 | nft: ObjectId; 122 | ft: string; 123 | beneficiary: SuiAddress; 124 | commission: number; 125 | }) { 126 | const effects = await this.client.sendTxWaitForEffects( 127 | createAskWithCommissionTx({ 128 | ...this.opts, 129 | ...p, 130 | }) 131 | ); 132 | 133 | return effects; 134 | } 135 | 136 | public async buyNft(p: { 137 | orderbook: ObjectId; 138 | sellersKiosk: ObjectId | TransactionResult; 139 | buyersKiosk: ObjectId | TransactionResult; 140 | collection: string; 141 | ft: string; 142 | nft: ObjectId; 143 | price: number; 144 | wallet: ObjectId | TransactionResult; 145 | }) { 146 | const effects = await this.client.sendTxWaitForEffects( 147 | buyNftTx({ 148 | ...this.opts, 149 | ...p, 150 | }) 151 | ); 152 | 153 | return effects; 154 | } 155 | 156 | public async cancelAsk(p: { 157 | orderbook: ObjectId; 158 | sellersKiosk: ObjectId | TransactionResult; 159 | price: number; 160 | nft: ObjectId; 161 | collection: string; 162 | ft: string; 163 | }) { 164 | return this.client.sendTxWaitForEffects( 165 | cancelAskTx({ 166 | ...this.opts, 167 | ...p, 168 | }) 169 | ); 170 | } 171 | 172 | public async editAsk(p: { 173 | orderbook: ObjectId; 174 | sellersKiosk: ObjectId | TransactionResult; 175 | collection: string; 176 | ft: string; 177 | nft: ObjectId; 178 | oldPrice: number; 179 | newPrice: number; 180 | }) { 181 | return this.client.sendTxWaitForEffects( 182 | editAskTx({ 183 | ...this.opts, 184 | ...p, 185 | }) 186 | ); 187 | } 188 | 189 | public async editBid(p: { 190 | orderbook: ObjectId; 191 | buyersKiosk: ObjectId | TransactionResult; 192 | collection: string; 193 | ft: string; 194 | oldPrice: number; 195 | newPrice: number; 196 | wallet: ObjectId | TransactionResult; 197 | }) { 198 | return this.client.sendTxWaitForEffects( 199 | editBidTx({ 200 | ...this.opts, 201 | ...p, 202 | }) 203 | ); 204 | } 205 | 206 | public async cancelBid(p: { 207 | collection: string; 208 | ft: string; 209 | orderbook: ObjectId; 210 | price: number; 211 | wallet: ObjectId | TransactionResult; 212 | }) { 213 | return this.client.sendTxWaitForEffects( 214 | cancelBidTx({ 215 | ...this.opts, 216 | ...p, 217 | }) 218 | ); 219 | } 220 | 221 | public async finishTrade(p: { 222 | orderbook: ObjectId; 223 | collection: string; 224 | ft: string; 225 | buyersKiosk: ObjectId | TransactionResult; 226 | sellersKiosk: ObjectId | TransactionResult; 227 | trade: ObjectId; 228 | }) { 229 | const effects = await this.client.sendTxWaitForEffects( 230 | finishTradeTx({ 231 | ...this.opts, 232 | ...p, 233 | }) 234 | ); 235 | 236 | return effects; 237 | } 238 | 239 | public async createBid(p: { 240 | buyersKiosk: ObjectId | TransactionResult; 241 | collection: string; 242 | ft: string; 243 | orderbook: ObjectId; 244 | price: number; 245 | wallet: ObjectId | TransactionResult; 246 | }) { 247 | const effects = await this.client.sendTxWaitForEffects( 248 | createBidTx({ 249 | ...this.opts, 250 | ...p, 251 | }) 252 | ); 253 | 254 | return effects; 255 | } 256 | 257 | public async createBidWithCommission(p: { 258 | orderbook: ObjectId; 259 | buyersKiosk: ObjectId | TransactionResult; 260 | collection: string; 261 | ft: string; 262 | price: number; 263 | wallet: ObjectId | TransactionResult; 264 | beneficiary: SuiAddress; 265 | commission: number; 266 | }) { 267 | const effects = await this.client.sendTxWaitForEffects( 268 | createBidWithCommissionTx({ 269 | ...this.opts, 270 | ...p, 271 | }) 272 | ); 273 | 274 | return effects; 275 | } 276 | 277 | public async marketBuy(p: { 278 | orderbook: ObjectId; 279 | buyersKiosk: ObjectId | TransactionResult; 280 | wallet: ObjectId | TransactionResult; 281 | price: number; 282 | collection: string; 283 | ft: string; 284 | }) { 285 | const effects = await this.client.sendTxWaitForEffects( 286 | marketBuyTx({ 287 | ...this.opts, 288 | ...p, 289 | }) 290 | ); 291 | 292 | return effects; 293 | } 294 | 295 | public async marketSell(p: { 296 | orderbook: ObjectId; 297 | sellersKiosk: ObjectId | TransactionResult; 298 | price: number; 299 | nft: ObjectId; 300 | collection: string; 301 | ft: string; 302 | }) { 303 | const effects = await this.client.sendTxWaitForEffects( 304 | marketSellTx({ 305 | ...this.opts, 306 | ...p, 307 | }) 308 | ); 309 | 310 | return effects; 311 | } 312 | } 313 | -------------------------------------------------------------------------------- /src/client/orderbook/OrderbookReadClient.ts: -------------------------------------------------------------------------------- 1 | import { 2 | EventId, 3 | ObjectId, 4 | JsonRpcProvider, 5 | SubscriptionId, 6 | SuiAddress, 7 | PaginatedEvents, 8 | } from "@mysten/sui.js"; 9 | import { DEFAULT_ORDERBOOK_MODULE, DEFAULT_PAGINATION_LIMIT } from "../consts"; 10 | import { ReadClient } from "../ReadClient"; 11 | import { TransferCapState, transformTransferCap } from "../safe/SafeReadClient"; 12 | 13 | export interface ProtectedActions { 14 | buyNft: boolean; 15 | cancelAsk: boolean; 16 | cancelBid: boolean; 17 | createAsk: boolean; 18 | createBid: boolean; 19 | } 20 | 21 | export interface AskCommissionState { 22 | beneficiary: SuiAddress; 23 | cut: number; 24 | } 25 | 26 | export interface AskState { 27 | owner: SuiAddress; 28 | kiosk: ObjectId; 29 | nft: ObjectId; 30 | price: number; 31 | commission?: AskCommissionState; 32 | } 33 | 34 | export interface BidState { 35 | owner: SuiAddress; 36 | kiosk: ObjectId; 37 | offer: number; 38 | commission?: { 39 | beneficiary: SuiAddress; 40 | cut: number; 41 | }; 42 | } 43 | 44 | export interface OrderbookState { 45 | protectedActions: ProtectedActions; 46 | asks: AskState[]; 47 | bids: BidState[]; 48 | } 49 | 50 | export type OrderbookEvent = 51 | | { 52 | BidCreatedEvent: { 53 | orderbook: ObjectId; 54 | owner: SuiAddress; 55 | price: number; 56 | }; 57 | } 58 | | { 59 | AskCreatedEvent: { 60 | nft: ObjectId; 61 | orderbook: ObjectId; 62 | owner: SuiAddress; 63 | price: number; 64 | safe: ObjectId; 65 | }; 66 | } 67 | | { 68 | BidClosedEvent: { 69 | orderbook: ObjectId; 70 | owner: SuiAddress; 71 | price: number; 72 | }; 73 | } 74 | | { 75 | AskClosedEvent: { 76 | nft: ObjectId; 77 | orderbook: ObjectId; 78 | owner: SuiAddress; 79 | price: number; 80 | }; 81 | } 82 | | { 83 | OrderbookCreatedEvent: { 84 | collectionType: string; 85 | ftType: string; 86 | orderbook: ObjectId; 87 | }; 88 | }; 89 | 90 | interface TradeIntermediaryState { 91 | paid: number; 92 | buyer: SuiAddress; 93 | buyerSafe: ObjectId; 94 | commission?: AskCommissionState; 95 | transferCap?: TransferCapState; 96 | } 97 | 98 | export function parseCommission(fields: any): AskCommissionState | undefined { 99 | return fields === null 100 | ? undefined 101 | : { 102 | beneficiary: fields.beneficiary, 103 | cut: parseInt(fields.cut, 10), 104 | }; 105 | } 106 | 107 | type Unpacked = T extends (infer U)[] ? U : T; 108 | 109 | type Envelope = Unpacked; 110 | 111 | function parseOrderbookEvent(e: Envelope): OrderbookEvent | null { 112 | const { type, parsedJson: fields } = e; 113 | 114 | if (type.endsWith("BidCreatedEvent")) { 115 | return { 116 | BidCreatedEvent: { 117 | orderbook: fields.orderbook, 118 | owner: fields.owner, 119 | price: parseInt(fields.price, 10), 120 | }, 121 | }; 122 | } 123 | if (type.endsWith("BidClosedEvent")) { 124 | return { 125 | BidClosedEvent: { 126 | orderbook: fields.orderbook, 127 | owner: fields.owner, 128 | price: parseInt(fields.price, 10), 129 | }, 130 | }; 131 | } 132 | if (type.endsWith("AskCreatedEvent")) { 133 | return { 134 | AskCreatedEvent: { 135 | nft: fields.nft, 136 | orderbook: fields.orderbook, 137 | owner: fields.owner, 138 | price: parseInt(fields.price, 10), 139 | safe: fields.safe, 140 | }, 141 | }; 142 | } 143 | if (type.endsWith("AskClosedEvent")) { 144 | return { 145 | AskClosedEvent: { 146 | nft: fields.nft, 147 | orderbook: fields.orderbook, 148 | owner: fields.owner, 149 | price: parseInt(fields.price, 10), 150 | }, 151 | }; 152 | } 153 | if (type.endsWith("OrderbookCreatedEvent")) { 154 | return { 155 | OrderbookCreatedEvent: { 156 | collectionType: fields.collection_type, 157 | ftType: fields.fungible_token_type, 158 | orderbook: fields.orderbook, 159 | }, 160 | }; 161 | } 162 | 163 | // eslint-disable-next-line no-console 164 | console.warn(`Unknown orderbook event in tx '${e.id}'`); 165 | return null; 166 | } 167 | 168 | export class OrderbookReadClient { 169 | // eslint-disable-next-line 170 | constructor(public client: ReadClient = new ReadClient()) { 171 | // 172 | } 173 | 174 | public static fromProvider(provider: JsonRpcProvider) { 175 | return new OrderbookReadClient(new ReadClient(provider)); 176 | } 177 | 178 | public static fromRpcUrl(url: string) { 179 | return new OrderbookReadClient(ReadClient.fromRpcUrl(url)); 180 | } 181 | 182 | /** 183 | * If sorted, then asks are sorted by price in ascending order and bids are 184 | * sorted by price in descending order. 185 | */ 186 | public async fetchOrderbook( 187 | orderbookId: ObjectId, 188 | sort: boolean = false 189 | ): Promise { 190 | const details = await this.client.getObject(orderbookId); 191 | 192 | if (typeof details !== "object" || !("content" in details)) { 193 | throw new Error("Cannot fetch owner cap details"); 194 | } 195 | 196 | const { fields } = details.content as any; 197 | 198 | const protectedActions = { 199 | buyNft: fields.protected_actions.fields.buy_nft, 200 | cancelAsk: fields.protected_actions.fields.cancel_ask, 201 | cancelBid: fields.protected_actions.fields.cancel_bid, 202 | createAsk: fields.protected_actions.fields.create_ask, 203 | createBid: fields.protected_actions.fields.create_bid, 204 | }; 205 | 206 | const asks: AskState[] = []; 207 | fields.asks.fields.o.forEach((priceLevel: any) => { 208 | const price = parseInt(priceLevel.fields.k, 10); 209 | asks.push( 210 | ...priceLevel.fields.v.map((a: any) => { 211 | return { 212 | owner: a.fields.owner, 213 | kiosk: a.fields.kiosk_id, 214 | nft: a.fields.nft_id, 215 | price, 216 | commission: a.fields.commission 217 | ? { 218 | beneficiary: a.fields.commission.fields.beneficiary, 219 | cut: parseInt(a.fields.commission.fields.cut, 10), 220 | } 221 | : undefined, 222 | }; 223 | }) 224 | ); 225 | }); 226 | 227 | const bids: BidState[] = []; 228 | fields.bids.fields.o.forEach((priceLevel: any) => { 229 | const offer = parseInt(priceLevel.fields.k, 10); 230 | bids.push( 231 | ...priceLevel.fields.v.map((b: any) => { 232 | return { 233 | offer, 234 | owner: b.fields.owner, 235 | kiosk: b.fields.kiosk, 236 | commission: b.fields.commission 237 | ? { 238 | beneficiary: b.fields.commission.fields.beneficiary, 239 | cut: parseInt(b.fields.commission.fields.cut, 10), 240 | } 241 | : undefined, 242 | }; 243 | }) 244 | ); 245 | }); 246 | 247 | if (sort) { 248 | asks.sort((a, b) => a.price - b.price); 249 | bids.sort((a, b) => b.offer - a.offer); 250 | } 251 | 252 | return { 253 | asks, 254 | bids, 255 | protectedActions, 256 | }; 257 | } 258 | 259 | /** 260 | * Legacy method - use with caution! 261 | * This method will be removed in future versions and may not work properly in its current state. 262 | */ 263 | public async fetchTradeIntermediary( 264 | trade: ObjectId 265 | ): Promise { 266 | const details = await this.client.getObject(trade); 267 | 268 | if (typeof details !== "object" || !("data" in details)) { 269 | throw new Error("Cannot fetch trade intermediary details"); 270 | } 271 | 272 | const { fields } = details.data as any; 273 | return { 274 | buyer: fields.buyer, 275 | buyerSafe: fields.buyer_safe, 276 | paid: parseInt(fields.paid, 10), 277 | commission: parseCommission(fields.commission), 278 | transferCap: fields.transfer_cap 279 | ? transformTransferCap(fields.transfer_cap) 280 | : undefined, 281 | }; 282 | } 283 | 284 | /** 285 | * Fetch orderbook events, but may require patching to ensure proper functionality. 286 | * @note This method may not work correctly in its current state and should be reviewed before use. 287 | * @todo Patch this method to address any known issues and ensure proper functionality. 288 | */ 289 | public async fetchEvents(p: { 290 | packageId: ObjectId; 291 | module?: string; 292 | cursor?: PaginatedEvents["nextCursor"]; 293 | limit?: number; // or DEFAULT_PAGINATION_LIMIT 294 | order?: "ascending" | "descending"; 295 | }): Promise<{ 296 | events: Array<{ 297 | txDigest: string; 298 | data: OrderbookEvent; 299 | }>; 300 | nextCursor: EventId | null; 301 | }> { 302 | const { data, nextCursor } = await this.client.provider.queryEvents({ 303 | query: { 304 | MoveModule: { 305 | package: p.packageId, 306 | module: p.module || DEFAULT_ORDERBOOK_MODULE, 307 | }, 308 | }, 309 | cursor: p.cursor || (null as any), 310 | limit: p.limit || DEFAULT_PAGINATION_LIMIT, 311 | order: p.order || "ascending", 312 | }); 313 | 314 | return { 315 | events: data 316 | .map((envelope) => { 317 | return { 318 | txDigest: envelope.id.txDigest, 319 | data: parseOrderbookEvent(envelope), 320 | }; 321 | }) 322 | .filter((e) => e.data !== null), 323 | nextCursor, 324 | }; 325 | } 326 | 327 | /** 328 | * Subscribes for orderbook events, but may require patching to ensure proper functionality. 329 | * @note This method may not work correctly in its current state and should be reviewed before use. 330 | * @todo Patch this method to address any known issues and ensure proper functionality. 331 | */ 332 | public subscribeToEvents( 333 | p: { packageId: ObjectId; module?: string }, 334 | // eslint-disable-next-line 335 | cb: (event: OrderbookEvent) => void 336 | ): Promise { 337 | return this.client.provider.subscribeEvent({ 338 | filter: { 339 | All: [ 340 | { 341 | MoveModule: { 342 | package: p.packageId, 343 | module: p.module || DEFAULT_ORDERBOOK_MODULE, 344 | }, 345 | }, 346 | { MoveEventType: "MoveEvent" }, 347 | ], 348 | }, 349 | onMessage: (envelope) => { 350 | const parsedEvent = parseOrderbookEvent(envelope); 351 | if (parsedEvent) { 352 | cb(parsedEvent); 353 | } 354 | }, 355 | }); 356 | } 357 | } 358 | -------------------------------------------------------------------------------- /src/client/orderbook/txBuilder.ts: -------------------------------------------------------------------------------- 1 | import { TransactionBlock } from "@mysten/sui.js"; 2 | import { DEFAULT_ORDERBOOK_MODULE, DEFAULT_PACKAGE_ID } from "../consts"; 3 | import { 4 | CollectionParam, 5 | CommissionParams, 6 | FTParam, 7 | GlobalParams, 8 | NftParam, 9 | OrderbookParam, 10 | PriceParam, 11 | TradeParam, 12 | WalletParam, 13 | OldPriceParam, 14 | NewPriceParam, 15 | BuyersKioskParam, 16 | SellersKioskParam, 17 | } from "../types"; 18 | import { txObj as txCommon, TransactionResult } from "../../transaction"; 19 | import { wrapToObject } from "../utils"; 20 | 21 | export type TransactionBlockArgument = { 22 | kind: "Input"; 23 | index: number; 24 | type?: "object" | "pure" | undefined; 25 | value?: any; 26 | }; 27 | 28 | function txObj( 29 | fun: string, 30 | p: GlobalParams, 31 | args: ( 32 | tx: TransactionBlock 33 | ) => (TransactionBlockArgument | TransactionResult)[], 34 | tArgs: string[] 35 | ): [TransactionBlock, TransactionResult] { 36 | return txCommon( 37 | { 38 | packageObjectId: p.packageObjectId ?? DEFAULT_PACKAGE_ID, 39 | moduleName: p.moduleName ?? DEFAULT_ORDERBOOK_MODULE, 40 | fun, 41 | transaction: p.transaction, 42 | }, 43 | args, 44 | tArgs 45 | ); 46 | } 47 | 48 | export type CreateProtectionParams = { 49 | buyNft: boolean; 50 | createAsk: boolean; 51 | createBid: boolean; 52 | }; 53 | 54 | export type OrderbookTParams = GlobalParams & CollectionParam & FTParam; 55 | export type ShareOrderbookParams = OrderbookTParams & { 56 | orderbook: TransactionResult; 57 | }; 58 | export type OrderbookProtectionParams = GlobalParams & CreateProtectionParams; 59 | export type OrderbookParams = OrderbookTParams & OrderbookParam; 60 | export type DebitParams = PriceParam & WalletParam; 61 | 62 | export const newOrderbookTx = ( 63 | p: OrderbookTParams & { 64 | protectedActions: TransactionResult; 65 | } 66 | ) => { 67 | return txObj("new", p, () => [p.protectedActions], [p.collection, p.ft]); 68 | }; 69 | 70 | export const newUnprotectedOrderbookTx = (p: OrderbookTParams) => { 71 | return txObj("new_unprotected", p, () => [], [p.collection, p.ft]); 72 | }; 73 | 74 | export const createUnprotectedOrderbookTx = (p: OrderbookTParams) => { 75 | return txObj("create_unprotected", p, () => [], [p.collection, p.ft]); 76 | }; 77 | 78 | export const shareOrderbookTx = (p: ShareOrderbookParams) => { 79 | return txObj("share", p, () => [p.orderbook], [p.collection, p.ft]); 80 | }; 81 | 82 | export const createProtectionTx = (p: OrderbookProtectionParams) => { 83 | return txObj( 84 | "custom_protection", 85 | p, 86 | (t) => [t.pure(p.buyNft), t.pure(p.createAsk), t.pure(p.createBid)], 87 | [] 88 | ); 89 | }; 90 | export const createUnprotectedTx = (p: GlobalParams) => { 91 | return txObj("no_protection", p, (t) => [], []); 92 | }; 93 | 94 | export const createAskTx = ( 95 | p: OrderbookParams & SellersKioskParam & PriceParam & NftParam 96 | ) => { 97 | return txObj( 98 | "create_ask", 99 | p, 100 | (t) => [ 101 | t.object(p.orderbook), 102 | wrapToObject(t, p.sellersKiosk), 103 | t.pure(String(p.price)), 104 | t.object(p.nft), 105 | ], 106 | [p.collection, p.ft] 107 | ); 108 | }; 109 | 110 | export const createAskWithCommissionTx = ( 111 | p: OrderbookParams & 112 | SellersKioskParam & 113 | PriceParam & 114 | NftParam & 115 | CommissionParams 116 | ) => { 117 | return txObj( 118 | "create_ask_with_commission", 119 | p, 120 | (t) => [ 121 | t.object(p.orderbook), 122 | wrapToObject(t, p.sellersKiosk), 123 | t.pure(String(p.price)), 124 | t.pure(p.nft), 125 | t.object(p.beneficiary), 126 | t.pure(String(p.commission)), 127 | ], 128 | [p.collection, p.ft] 129 | ); 130 | }; 131 | 132 | export const cancelAskTx = ( 133 | p: OrderbookParams & SellersKioskParam & NftParam & PriceParam 134 | ) => { 135 | return txObj( 136 | "cancel_ask", 137 | p, 138 | (t) => [ 139 | t.object(p.orderbook), 140 | wrapToObject(t, p.sellersKiosk), 141 | t.pure(String(p.price)), 142 | t.object(p.nft), 143 | ], 144 | [p.collection, p.ft] 145 | ); 146 | }; 147 | 148 | export const editAskTx = ( 149 | p: OrderbookParams & 150 | NftParam & 151 | OldPriceParam & 152 | NewPriceParam & 153 | SellersKioskParam 154 | ) => { 155 | return txObj( 156 | "edit_ask", 157 | p, 158 | (t) => [ 159 | t.object(p.orderbook), 160 | wrapToObject(t, p.sellersKiosk), 161 | t.pure(String(p.oldPrice)), 162 | t.object(p.nft), 163 | t.pure(String(p.newPrice)), 164 | ], 165 | [p.collection, p.ft] 166 | ); 167 | }; 168 | 169 | export const createBidTx = ( 170 | p: OrderbookParams & DebitParams & BuyersKioskParam 171 | ) => { 172 | return txObj( 173 | "create_bid", 174 | p, 175 | (t) => [ 176 | t.object(p.orderbook), 177 | wrapToObject(t, p.buyersKiosk), 178 | t.pure(String(p.price)), 179 | wrapToObject(t, p.wallet), 180 | ], 181 | [p.collection, p.ft] 182 | ); 183 | }; 184 | 185 | export const createBidWithCommissionTx = ( 186 | p: OrderbookParams & CommissionParams & DebitParams & BuyersKioskParam 187 | ) => { 188 | return txObj( 189 | "create_bid_with_commission", 190 | p, 191 | (t) => [ 192 | t.object(p.orderbook), 193 | wrapToObject(t, p.buyersKiosk), 194 | t.pure(String(p.price)), 195 | t.object(p.beneficiary), 196 | t.pure(String(p.commission)), 197 | wrapToObject(t, p.wallet), 198 | ], 199 | [p.collection, p.ft] 200 | ); 201 | }; 202 | 203 | export const editBidTx = ( 204 | p: OrderbookParams & 205 | OldPriceParam & 206 | NewPriceParam & 207 | BuyersKioskParam & 208 | WalletParam 209 | ) => { 210 | return txObj( 211 | "edit_bid", 212 | p, 213 | (t) => [ 214 | t.object(p.orderbook), 215 | wrapToObject(t, p.buyersKiosk), 216 | t.pure(String(p.oldPrice)), 217 | t.pure(String(p.newPrice)), 218 | wrapToObject(t, p.wallet), 219 | ], 220 | [p.collection, p.ft] 221 | ); 222 | }; 223 | 224 | export const cancelBidTx = (p: OrderbookParams & PriceParam & WalletParam) => { 225 | return txObj( 226 | "cancel_bid", 227 | p, 228 | (t) => [ 229 | t.object(p.orderbook), 230 | t.pure(String(p.price)), 231 | wrapToObject(t, p.wallet), 232 | ], 233 | [p.collection, p.ft] 234 | ); 235 | }; 236 | 237 | export const finishTradeTx = ( 238 | p: OrderbookParams & 239 | OrderbookTParams & 240 | BuyersKioskParam & 241 | SellersKioskParam & 242 | TradeParam 243 | ) => { 244 | return txObj( 245 | "finish_trade", 246 | p, 247 | (t) => [ 248 | t.object(p.orderbook), 249 | t.object(p.trade), 250 | wrapToObject(t, p.sellersKiosk), 251 | wrapToObject(t, p.buyersKiosk), 252 | ], 253 | [p.collection, p.ft] 254 | ); 255 | }; 256 | 257 | export const buyNftTx = ( 258 | p: OrderbookParams & 259 | NftParam & 260 | DebitParams & 261 | SellersKioskParam & 262 | BuyersKioskParam 263 | ) => { 264 | return txObj( 265 | "buy_nft", 266 | p, 267 | (t) => [ 268 | t.object(p.orderbook), 269 | wrapToObject(t, p.sellersKiosk), 270 | wrapToObject(t, p.buyersKiosk), 271 | t.object(p.nft), 272 | t.pure(String(p.price)), 273 | wrapToObject(t, p.wallet), 274 | ], 275 | [p.collection, p.ft] 276 | ); 277 | }; 278 | 279 | export const marketBuyTx = ( 280 | p: OrderbookParams & BuyersKioskParam & DebitParams 281 | ) => { 282 | return txObj( 283 | "market_buy", 284 | p, 285 | (t) => [ 286 | t.object(p.orderbook), 287 | wrapToObject(t, p.buyersKiosk), 288 | wrapToObject(t, p.wallet), 289 | t.pure(String(p.price)), 290 | ], 291 | [p.collection, p.ft] 292 | ); 293 | }; 294 | 295 | export const marketSellTx = ( 296 | p: OrderbookParams & SellersKioskParam & PriceParam & NftParam 297 | ) => { 298 | return txObj( 299 | "market_sell", 300 | p, 301 | (t) => [ 302 | t.object(p.orderbook), 303 | wrapToObject(t, p.sellersKiosk), 304 | t.pure(String(p.price)), 305 | t.object(p.nft), 306 | ], 307 | [p.collection, p.ft] 308 | ); 309 | }; 310 | -------------------------------------------------------------------------------- /src/client/safe/SafeFullClient.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Ed25519Keypair, 3 | ObjectId, 4 | JsonRpcProvider, 5 | TransactionEffects, 6 | } from "@mysten/sui.js"; 7 | import { FullClient } from "../FullClient"; 8 | import { GlobalParams } from "../types"; 9 | import { 10 | parseCreateSafeForSenderTxData, 11 | parseTransferCapForSenderTxData, 12 | SafeReadClient, 13 | } from "./SafeReadClient"; 14 | import { 15 | burnTransferCapTx, 16 | createExclusiveTransferCapForSenderTx, 17 | createSafeForSenderTx, 18 | createTransferCapForSenderTx, 19 | delistNftTx, 20 | depositGenericNftPrivilegedTx, 21 | depositGenericNftTx, 22 | depositNftPrivilegedTx, 23 | depositNftTx, 24 | disableDepositsOfCollectionTx, 25 | enableAnyDepositTx, 26 | enableDepositsOfCollectionTx, 27 | restrictDepositsTx, 28 | } from "./txBuilder"; 29 | 30 | export class SafeFullClient extends SafeReadClient { 31 | constructor( 32 | public client: FullClient, 33 | // eslint-disable-next-line 34 | public opts: Partial = {} 35 | ) { 36 | super(client, opts); 37 | } 38 | 39 | public static fromKeypair( 40 | keypair: Ed25519Keypair, 41 | provider?: JsonRpcProvider, 42 | opts?: Partial 43 | ) { 44 | return new SafeFullClient(FullClient.fromKeypair(keypair, provider), opts); 45 | } 46 | 47 | static burnTransferCapTx = burnTransferCapTx; 48 | 49 | static createExclusiveTransferCapForSenderTx = 50 | createExclusiveTransferCapForSenderTx; 51 | 52 | static createSafeForSenderTx = createSafeForSenderTx; 53 | 54 | static createTransferCapForSenderTx = createTransferCapForSenderTx; 55 | 56 | static delistNftTx = delistNftTx; 57 | 58 | static depositGenericNftPrivilegedTx = depositGenericNftPrivilegedTx; 59 | 60 | static depositGenericNftTx = depositGenericNftTx; 61 | 62 | static depositNftPrivilegedTx = depositNftPrivilegedTx; 63 | 64 | static depositNftTx = depositNftTx; 65 | 66 | static disableDepositsOfCollectionTx = disableDepositsOfCollectionTx; 67 | 68 | static enableAnyDepositTx = enableAnyDepositTx; 69 | 70 | static enableDepositsOfCollectionTx = enableDepositsOfCollectionTx; 71 | 72 | static restrictDepositsTx = restrictDepositsTx; 73 | 74 | public burnTransferCap(p: { transferCap: ObjectId; safe: ObjectId }) { 75 | return this.client.sendTxWaitForEffects( 76 | burnTransferCapTx({ 77 | ...this.opts, 78 | ...p, 79 | }) 80 | ); 81 | } 82 | 83 | public async createExclusiveTransferCapForSender(p: { 84 | safe: ObjectId; 85 | ownerCap: ObjectId; 86 | nft: ObjectId; 87 | }): Promise<{ transferCap: ObjectId; effects: TransactionEffects }> { 88 | const effects = await this.client.sendTxWaitForEffects( 89 | createExclusiveTransferCapForSenderTx({ 90 | ...this.opts, 91 | ...p, 92 | }) 93 | ); 94 | 95 | const { transferCap } = parseTransferCapForSenderTxData(effects); 96 | 97 | return { effects, transferCap }; 98 | } 99 | 100 | public async createSafeForSender(): Promise<{ 101 | safe: ObjectId; 102 | ownerCap: ObjectId; 103 | effects: TransactionEffects; 104 | }> { 105 | const effects = await this.client.sendTxWaitForEffects( 106 | createSafeForSenderTx(this.opts) 107 | ); 108 | 109 | const { safe, ownerCap } = parseCreateSafeForSenderTxData(effects); 110 | 111 | return { effects, safe, ownerCap }; 112 | } 113 | 114 | public async createTransferCapForSender(p: { 115 | safe: ObjectId; 116 | ownerCap: ObjectId; 117 | nft: ObjectId; 118 | }) { 119 | const effects = await this.client.sendTxWaitForEffects( 120 | createTransferCapForSenderTx({ 121 | ...this.opts, 122 | ...p, 123 | }) 124 | ); 125 | 126 | // there's always exactly one object created in this tx 127 | return { transferCap: effects.created[0].reference.objectId, effects }; 128 | } 129 | 130 | public async delistNft(p: { 131 | safe: ObjectId; 132 | nft: ObjectId; 133 | ownerCap: ObjectId; 134 | }) { 135 | return this.client.sendTxWaitForEffects( 136 | delistNftTx({ 137 | ...this.opts, 138 | ...p, 139 | }) 140 | ); 141 | } 142 | 143 | public async depositGenericNftPrivileged(p: { 144 | safe: ObjectId; 145 | nft: ObjectId; 146 | ownerCap: ObjectId; 147 | collection: string; 148 | }) { 149 | return this.client.sendTxWaitForEffects( 150 | depositGenericNftPrivilegedTx({ 151 | ...this.opts, 152 | ...p, 153 | }) 154 | ); 155 | } 156 | 157 | public async depositGenericNft(p: { 158 | safe: ObjectId; 159 | nft: ObjectId; 160 | collection: string; 161 | }) { 162 | return this.client.sendTxWaitForEffects( 163 | depositGenericNftTx({ 164 | ...this.opts, 165 | ...p, 166 | }) 167 | ); 168 | } 169 | 170 | public async depositNftPrivileged(p: { 171 | safe: ObjectId; 172 | nft: ObjectId; 173 | ownerCap: ObjectId; 174 | collection: string; 175 | }) { 176 | return this.client.sendTxWaitForEffects( 177 | depositNftPrivilegedTx({ 178 | ...this.opts, 179 | ...p, 180 | }) 181 | ); 182 | } 183 | 184 | public async depositNft(p: { 185 | safe: ObjectId; 186 | nft: ObjectId; 187 | collection: string; 188 | }) { 189 | return this.client.sendTxWaitForEffects( 190 | depositNftTx({ 191 | ...this.opts, 192 | ...p, 193 | }) 194 | ); 195 | } 196 | 197 | public async disableDepositsOfCollection(p: { 198 | safe: ObjectId; 199 | collection: string; 200 | ownerCap: ObjectId; 201 | }) { 202 | return this.client.sendTxWaitForEffects( 203 | disableDepositsOfCollectionTx({ 204 | ...this.opts, 205 | ...p, 206 | }) 207 | ); 208 | } 209 | 210 | public async enableDepositsOfCollection(p: { 211 | safe: ObjectId; 212 | collection: string; 213 | ownerCap: ObjectId; 214 | }) { 215 | return this.client.sendTxWaitForEffects( 216 | enableDepositsOfCollectionTx({ 217 | ...this.opts, 218 | ...p, 219 | }) 220 | ); 221 | } 222 | 223 | public async enableAnyDeposit(p: { safe: ObjectId; ownerCap: ObjectId }) { 224 | return this.client.sendTxWaitForEffects( 225 | enableAnyDepositTx({ 226 | ...this.opts, 227 | ...p, 228 | }) 229 | ); 230 | } 231 | 232 | public async restrictDeposits(p: { safe: ObjectId; ownerCap: ObjectId }) { 233 | return this.client.sendTxWaitForEffects( 234 | restrictDepositsTx({ 235 | ...this.opts, 236 | ...p, 237 | }) 238 | ); 239 | } 240 | } 241 | -------------------------------------------------------------------------------- /src/client/safe/SafeReadClient.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ObjectId, 3 | JsonRpcProvider, 4 | SuiAddress, 5 | TransactionEffects, 6 | } from "@mysten/sui.js"; 7 | import { DEFAULT_PACKAGE_ID, DEFAULT_SAFE_MODULE } from "../consts"; 8 | import { ReadClient } from "../ReadClient"; 9 | import { GlobalParams } from "../types"; 10 | import { parseObjectOwner } from "../utils"; 11 | 12 | export interface SafeState { 13 | id: ObjectId; 14 | collectionsWithEnabledDeposits: string[]; 15 | enableAnyDeposits: boolean; 16 | nfts: Array<{ 17 | id: ObjectId; 18 | version: string; 19 | isExclusivelyListed: boolean; 20 | transferCapsCount: number; 21 | }>; 22 | } 23 | 24 | export interface TransferCapState { 25 | safe: ObjectId; 26 | isExclusivelyListed: boolean; 27 | nft: ObjectId; 28 | version: string; 29 | isGeneric: boolean; 30 | } 31 | 32 | export function transformTransferCap({ fields }: any): TransferCapState { 33 | return { 34 | safe: fields.safe, 35 | isExclusivelyListed: fields.inner.fields.is_exclusive, 36 | isGeneric: fields.inner.fields.is_generic, 37 | nft: fields.inner.fields.nft, 38 | version: fields.inner.fields.version, 39 | }; 40 | } 41 | 42 | export function parseCreateSafeForSenderTxData(effects: TransactionEffects) { 43 | const [object1, object2] = effects.created; 44 | 45 | let safe; 46 | let ownerCap; 47 | 48 | // two objects are created, one is the safe which is a shared object, 49 | // the other is the owner cap which is owned by the sender 50 | if (parseObjectOwner(object1.owner) === "shared") { 51 | safe = object1.reference.objectId; 52 | ownerCap = object2.reference.objectId; 53 | } else { 54 | safe = object2.reference.objectId; 55 | ownerCap = object1.reference.objectId; 56 | } 57 | 58 | return { safe, ownerCap }; 59 | } 60 | 61 | export function parseTransferCapForSenderTxData(effects: TransactionEffects) { 62 | // there's always exactly one object created in this tx 63 | return { transferCap: effects.created[0].reference.objectId }; 64 | } 65 | 66 | export class SafeReadClient { 67 | // eslint-disable-next-line 68 | constructor( 69 | // eslint-disable-next-line 70 | public client: ReadClient = new ReadClient(), 71 | // eslint-disable-next-line 72 | public opts: Partial = {} 73 | ) { 74 | // 75 | } 76 | 77 | public static fromProvider(provider: JsonRpcProvider) { 78 | return new SafeReadClient(new ReadClient(provider)); 79 | } 80 | 81 | public static fromRpcUrl(url: string) { 82 | return new SafeReadClient(ReadClient.fromRpcUrl(url)); 83 | } 84 | 85 | public get package() { 86 | return this.opts.packageObjectId ?? DEFAULT_PACKAGE_ID; 87 | } 88 | 89 | public get module() { 90 | return this.opts.moduleName ?? DEFAULT_SAFE_MODULE; 91 | } 92 | 93 | public async fetchOwnerCapsIds( 94 | user: SuiAddress, 95 | p: Partial = {} 96 | ): Promise { 97 | const ownerCapType = `${p.packageObjectId || this.package}::${ 98 | p.moduleName || this.module 99 | }::OwnerCap`; 100 | const objs = await this.client.getObjects(user); 101 | return objs 102 | .filter((o) => o.data.type === ownerCapType) 103 | .map((o) => o.data.objectId); 104 | } 105 | 106 | public async fetchAllOwnerCapsByUser(user: SuiAddress) { 107 | const allObjects = await this.client.getObjects(user); 108 | const ownerCapObjects = allObjects.filter((obj) => 109 | obj.data.type.endsWith(`::${this.module}::OwnerCap`) 110 | ); 111 | 112 | return ownerCapObjects; 113 | } 114 | 115 | public async fetchOwnerCapSafeId(ownerCap: ObjectId): Promise { 116 | const details = await this.client.getObject(ownerCap); 117 | 118 | if (typeof details !== "object" || !("data" in details)) { 119 | throw new Error("Cannot fetch owner cap details"); 120 | } 121 | 122 | return (details.data as any).fields.safe; 123 | } 124 | 125 | public async fetchSafeByOwnerCap(ownerCap: ObjectId) { 126 | const safeId = await this.fetchOwnerCapSafeId(ownerCap); 127 | return this.fetchSafe(safeId); 128 | } 129 | 130 | public async fetchSafe(safeId: ObjectId): Promise { 131 | const details = await this.client.getObject(safeId); 132 | 133 | if (typeof details !== "object" || !("data" in details)) { 134 | throw new Error("Cannot fetch owner cap details"); 135 | } 136 | 137 | const { fields } = details.data as any; 138 | 139 | const refs = fields.inner.fields.refs.fields.contents; 140 | const nfts = refs.map((r: any) => ({ 141 | id: r.fields.key, 142 | version: r.fields.value.fields.version, 143 | isExclusivelyListed: r.fields.value.fields.is_exclusively_listed, 144 | transferCapsCount: parseInt( 145 | r.fields.value.fields.transfer_cap_counter, 146 | 10 147 | ), 148 | })); 149 | 150 | const collectionsWithEnabledDeposits = 151 | fields.collections_with_enabled_deposits.fields.contents.map( 152 | (o: any) => `0x${o.fields.name}` 153 | ); 154 | 155 | return { 156 | id: safeId, 157 | enableAnyDeposits: fields.enable_any_deposit, 158 | collectionsWithEnabledDeposits, 159 | nfts, 160 | }; 161 | } 162 | 163 | public async fetchTransferCap( 164 | transferCap: ObjectId 165 | ): Promise { 166 | const details = await this.client.getObject(transferCap); 167 | 168 | if (typeof details !== "object" || !("data" in details)) { 169 | throw new Error("Cannot fetch owner cap details"); 170 | } 171 | 172 | return transformTransferCap(details.data as any); 173 | } 174 | } 175 | -------------------------------------------------------------------------------- /src/client/safe/txBuilder.ts: -------------------------------------------------------------------------------- 1 | import { TransactionBlock } from "@mysten/sui.js"; 2 | import { 3 | AuthParam, 4 | CollectionParam, 5 | GlobalParams, 6 | NftParam, 7 | SafeParam, 8 | TransferCapParam, 9 | } from "../types"; 10 | import { DEFAULT_PACKAGE_ID, DEFAULT_SAFE_MODULE } from "../consts"; 11 | import { txObj as txCommon, TransactionResult } from "../../transaction"; 12 | 13 | export type NftParams = GlobalParams & NftParam & SafeParam; 14 | export type AuthParams = GlobalParams & SafeParam & AuthParam; 15 | export type ColAuthParams = AuthParams & CollectionParam; 16 | export type NftAuthParams = AuthParam & NftParams; 17 | 18 | export type TransactionBlockArgument = { 19 | kind: "Input"; 20 | index: number; 21 | type?: "object" | "pure" | undefined; 22 | value?: any; 23 | }; 24 | 25 | function txObj( 26 | fun: string, 27 | p: GlobalParams, 28 | args: ( 29 | tx: TransactionBlock 30 | ) => (TransactionBlockArgument | TransactionResult)[], 31 | tArgs: string[] 32 | ): [TransactionBlock, TransactionResult] { 33 | return txCommon( 34 | { 35 | packageObjectId: p.packageObjectId ?? DEFAULT_PACKAGE_ID, 36 | moduleName: p.moduleName ?? DEFAULT_SAFE_MODULE, 37 | fun, 38 | transaction: p.transaction, 39 | }, 40 | args, 41 | tArgs 42 | ); 43 | } 44 | 45 | function authNftArgs(p: NftParam & AuthParam & SafeParam, t: TransactionBlock) { 46 | return [t.object(p.nft), t.object(p.ownerCap), t.object(p.safe)]; 47 | } 48 | 49 | export const createSafeForSenderTx = (p: GlobalParams) => { 50 | return txObj("create_for_sender", p, () => [], []); 51 | }; 52 | 53 | export const restrictDepositsTx = (p: AuthParams) => { 54 | return txObj( 55 | "restrict_deposits", 56 | p, 57 | (t) => [t.object(p.ownerCap), t.object(p.safe)], 58 | [] 59 | ); 60 | }; 61 | 62 | export const enableAnyDepositTx = (p: AuthParams) => { 63 | return txObj( 64 | "enable_any_deposit", 65 | p, 66 | (t) => [t.object(p.ownerCap), t.object(p.safe)], 67 | [] 68 | ); 69 | }; 70 | 71 | export const enableDepositsOfCollectionTx = (p: ColAuthParams) => { 72 | const fun = "enable_deposits_of_collection"; 73 | return txObj(fun, p, (t) => [t.object(p.ownerCap), t.object(p.safe)], [ 74 | p.collection, 75 | ]); 76 | }; 77 | 78 | export const disableDepositsOfCollectionTx = (p: ColAuthParams) => { 79 | const fun = "disable_deposits_of_collection"; 80 | return txObj(fun, p, (t) => [t.object(p.ownerCap), t.object(p.safe)], [ 81 | p.collection, 82 | ]); 83 | }; 84 | 85 | export const depositNftTx = (p: NftParams & CollectionParam) => { 86 | return txObj("deposit_nft", p, (t) => [t.object(p.nft), t.object(p.safe)], [ 87 | p.collection, 88 | ]); 89 | }; 90 | 91 | export const depositGenericNftTx = (p: NftParams & CollectionParam) => { 92 | return txObj( 93 | "deposit_generic_nft", 94 | p, 95 | (t) => [t.object(p.nft), t.object(p.safe)], 96 | [p.collection] 97 | ); 98 | }; 99 | 100 | export const depositNftPrivilegedTx = (p: ColAuthParams & NftParam) => { 101 | return txObj("deposit_nft_privileged", p, (t) => authNftArgs(p, t), [ 102 | p.collection, 103 | ]); 104 | }; 105 | 106 | export const depositGenericNftPrivilegedTx = (p: ColAuthParams & NftParam) => { 107 | const fun = "deposit_generic_nft_privileged"; 108 | return txObj(fun, p, (t) => authNftArgs(p, t), [p.collection]); 109 | }; 110 | 111 | export const createTransferCapForSenderTx = (p: NftAuthParams) => { 112 | const fun = "create_transfer_cap_for_sender"; 113 | return txObj(fun, p, (t) => authNftArgs(p, t), []); 114 | }; 115 | 116 | export const createExclusiveTransferCapForSenderTx = (p: NftAuthParams) => { 117 | const fun = "create_exclusive_transfer_cap_for_sender"; 118 | return txObj(fun, p, (t) => authNftArgs(p, t), []); 119 | }; 120 | 121 | export const delistNftTx = (p: NftAuthParams) => { 122 | return txObj("delist_nft", p, (t) => authNftArgs(p, t), []); 123 | }; 124 | 125 | export const burnTransferCapTx = ( 126 | p: GlobalParams & TransferCapParam & SafeParam 127 | ) => { 128 | return txObj( 129 | "burn_transfer_cap", 130 | p, 131 | (t) => [t.object(p.transferCap), t.object(p.safe)], 132 | [] 133 | ); 134 | }; 135 | -------------------------------------------------------------------------------- /src/client/sui-contract/SuiContractFullClient.ts: -------------------------------------------------------------------------------- 1 | 2 | import { Ed25519Keypair, JsonRpcProvider } from "@mysten/sui.js"; 3 | import { GlobalParams } from "../types"; 4 | import { FullClient } from "../FullClient"; 5 | import { getId, publicShareObject } from "./txBuilder"; 6 | import { SuiContractReadClient } from "./SuiContractReadClient"; 7 | 8 | export class SuiContractFullClient extends SuiContractReadClient { 9 | // eslint-disable-next-line no-useless-constructor 10 | constructor( 11 | public client: FullClient, 12 | public opts: Partial 13 | // eslint-disable-next-line no-empty-function 14 | ) { 15 | super(client, opts); 16 | } 17 | 18 | public static fromKeypair( 19 | keypair: Ed25519Keypair, 20 | provider?: JsonRpcProvider, 21 | opts?: Partial 22 | ) { 23 | return new SuiContractFullClient( 24 | FullClient.fromKeypair(keypair, provider), 25 | opts 26 | ); 27 | } 28 | 29 | static publicShareObject = publicShareObject; 30 | 31 | static getId = getId; 32 | 33 | } -------------------------------------------------------------------------------- /src/client/sui-contract/SuiContractReadClient.ts: -------------------------------------------------------------------------------- 1 | import { JsonRpcProvider } from "@mysten/sui.js"; 2 | import { ReadClient } from "../ReadClient"; 3 | import { GlobalParams } from "../types"; 4 | import { DEFAULT_KIOSK_MODULE, DEFAULT_PACKAGE_ID } from "../consts"; 5 | 6 | export class SuiContractReadClient { 7 | // eslint-disable-next-line 8 | constructor( 9 | // eslint-disable-next-line 10 | public client: ReadClient = new ReadClient(), 11 | // eslint-disable-next-line 12 | public opts: Partial = {} 13 | ) { 14 | // 15 | } 16 | 17 | public static fromProvider(provider: JsonRpcProvider) { 18 | return new SuiContractReadClient(new ReadClient(provider)); 19 | } 20 | 21 | public static fromRpcUrl(url: string) { 22 | return new SuiContractReadClient(ReadClient.fromRpcUrl(url)); 23 | } 24 | 25 | public get package() { 26 | return this.opts.packageObjectId ?? DEFAULT_PACKAGE_ID; 27 | } 28 | 29 | public get module() { 30 | return this.opts.moduleName ?? DEFAULT_KIOSK_MODULE; 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /src/client/sui-contract/txBuilder.ts: -------------------------------------------------------------------------------- 1 | import { TransactionBlock } from "@mysten/sui.js"; 2 | import { 3 | TransactionBlockArgument, 4 | txObj as txCommon, 5 | TransactionResult 6 | } from "../../transaction"; 7 | import { DEFAULT_SUI_PACKAGE_ID } from "../consts"; 8 | import { GlobalParams } from "../types"; 9 | import { ObjectReferenceProps } from "./types"; 10 | import { wrapToObject } from "../utils"; 11 | 12 | function txObj( 13 | fun: string, 14 | p: GlobalParams, 15 | args: ( 16 | tx: TransactionBlock 17 | ) => (TransactionBlockArgument | TransactionResult)[], 18 | tArgs: string[] 19 | ): [TransactionBlock, TransactionResult] { 20 | // eslint-disable-next-line no-undef 21 | if (!p.moduleName) throw new Error("Module name required"); 22 | return txCommon( 23 | { 24 | packageObjectId: p.packageObjectId ?? DEFAULT_SUI_PACKAGE_ID, 25 | moduleName: p.moduleName, 26 | fun, 27 | transaction: p.transaction, 28 | }, 29 | args, 30 | tArgs 31 | ); 32 | } 33 | 34 | 35 | export const publicShareObject = (params: ObjectReferenceProps) => { 36 | return txObj( 37 | "public_share_object", 38 | {...params, moduleName: "transfer"}, 39 | (tx) => [ 40 | wrapToObject(tx, params.value) 41 | ], 42 | [params.type] 43 | ); 44 | } 45 | 46 | 47 | export const getId = (params: ObjectReferenceProps) => { 48 | return txObj( 49 | "id", 50 | {...params, moduleName: "object"}, 51 | (tx) => [ 52 | wrapToObject(tx, params.value) 53 | ], 54 | [params.type] 55 | ); 56 | } 57 | -------------------------------------------------------------------------------- /src/client/sui-contract/types.ts: -------------------------------------------------------------------------------- 1 | import { ObjectId } from "@mysten/sui.js"; 2 | import { GlobalParams } from "../types"; 3 | import { TransactionResult } from "../../transaction"; 4 | 5 | export interface ObjectReferenceProps extends GlobalParams { 6 | value: ObjectId | TransactionResult, 7 | type: string 8 | } -------------------------------------------------------------------------------- /src/client/transfer-request/TransferRequestFullClient.ts: -------------------------------------------------------------------------------- 1 | import { confirmTx, buildInserCollectionToAllowListTx } from "./txBuilder"; 2 | 3 | export class TransferRequestFullClient { 4 | 5 | static confirmTx = confirmTx; 6 | 7 | static buildInserCollectionToAllowListTx = buildInserCollectionToAllowListTx; 8 | } 9 | -------------------------------------------------------------------------------- /src/client/transfer-request/txBuilder.ts: -------------------------------------------------------------------------------- 1 | import { TransactionBlock } from "@mysten/sui.js"; 2 | import { TransactionBlockArgument, TransactionResult , txObj as txCommon } from "../../transaction"; 3 | import { GlobalParams } from "../types"; 4 | import { BuildInserCollectionToAllowListParams, ConfirmParams } from "./types"; 5 | 6 | function txObj( 7 | fun: string, 8 | p: GlobalParams, 9 | args: ( 10 | tx: TransactionBlock 11 | ) => (TransactionBlockArgument | TransactionResult)[], 12 | tArgs: string[] 13 | ): [TransactionBlock, TransactionResult] { 14 | // eslint-disable-next-line no-undef 15 | return txCommon( 16 | { 17 | packageObjectId: p.packageObjectId, 18 | moduleName: p.moduleName, 19 | fun, 20 | transaction: p.transaction, 21 | }, 22 | args, 23 | tArgs 24 | ); 25 | } 26 | 27 | 28 | export const confirmTx = (params: ConfirmParams) => { 29 | txObj( 30 | "confirm_transfer", 31 | {...params, moduleName: "transfer_allowlist", packageObjectId: params.nftProtocolContractId}, 32 | (tx) => [ 33 | tx.object(params.allowListId), 34 | typeof params.transferRequest === "string" ? tx.object(params.transferRequest) : params.transferRequest 35 | ], 36 | [params.transferRequestType] 37 | ); 38 | txObj( 39 | "confirm_transfer", 40 | {...params, moduleName: "royalty_strategy_bps", packageObjectId: params.nftProtocolContractId}, 41 | (tx) => [ 42 | tx.object(params.bpsRoyaltyStrategy), 43 | typeof params.transferRequest === "string" ? tx.object(params.transferRequest) : params.transferRequest 44 | ], 45 | [ 46 | params.transferRequestType, 47 | params.ft 48 | ] 49 | ); 50 | return txObj( 51 | "confirm", 52 | {...params, moduleName: "transfer_request", packageObjectId: params.requestContractId}, 53 | (tx) => [ 54 | typeof params.transferRequest === "string" ? tx.object(params.transferRequest) : params.transferRequest, 55 | tx.object(params.policyId) 56 | ], 57 | [ 58 | params.transferRequestType, 59 | params.ft 60 | ] 61 | ) 62 | } 63 | 64 | export const buildInserCollectionToAllowListTx = (params: BuildInserCollectionToAllowListParams) => { 65 | return txObj( 66 | "insert_collection", 67 | { moduleName: "allowlist", packageObjectId: params.allowlistPackageId, transaction: params.transaction }, 68 | (tx) => [ 69 | typeof params.allowlist === "string" ? tx.object(params.allowlist) : params.allowlist, 70 | typeof params.publisher === "string" ? tx.object(params.publisher) : params.publisher, 71 | ], 72 | [ 73 | params.nftType, 74 | ] 75 | ) 76 | } 77 | -------------------------------------------------------------------------------- /src/client/transfer-request/types.ts: -------------------------------------------------------------------------------- 1 | import { TransactionResult } from "../../transaction"; 2 | import { FTParam, GlobalParams, TransactionParams } from "../types"; 3 | 4 | export interface ConfirmParams extends GlobalParams, FTParam { 5 | transferRequest: TransactionResult | string, 6 | nftProtocolContractId: string, 7 | requestContractId: string, 8 | transferRequestType: string; 9 | policyId: string; 10 | bpsRoyaltyStrategy: string; 11 | allowListId: string 12 | } 13 | 14 | export type BuildInserCollectionToAllowListParams = TransactionParams & { 15 | allowlistPackageId: string; 16 | allowlist: string | TransactionResult; 17 | publisher: string; 18 | nftType: string; 19 | } 20 | -------------------------------------------------------------------------------- /src/client/utils.ts: -------------------------------------------------------------------------------- 1 | import { ObjectOwner, TransactionBlock } from "@mysten/sui.js"; 2 | import { TransactionResult } from "../transaction"; 3 | 4 | export const parseObjectOwner = ( 5 | owner: ObjectOwner 6 | ): "shared" | "immutable" | string => { 7 | if (typeof owner === "object") { 8 | if ("AddressOwner" in owner) { 9 | return owner.AddressOwner; 10 | } 11 | if ("ObjectOwner" in owner) { 12 | return owner.ObjectOwner; 13 | } 14 | if ("Shared" in owner) { 15 | return "shared"; 16 | } 17 | if ("Immutable" in owner) { 18 | return "immutable"; 19 | } 20 | } 21 | 22 | console.warn("Unexpected owner type", owner); 23 | throw new Error("Unexpected owner type"); 24 | }; 25 | 26 | export const wrapToObject = (tx: TransactionBlock, objectRef: string | TransactionResult) => 27 | typeof objectRef === "string" ? tx.object(objectRef) : objectRef; 28 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./client"; 2 | -------------------------------------------------------------------------------- /src/transaction.ts: -------------------------------------------------------------------------------- 1 | import { TransactionArgument, TransactionBlock } from "@mysten/sui.js"; 2 | 3 | export type BuildTxParams = { 4 | transaction?: TransactionBlock; 5 | packageObjectId: string; 6 | moduleName: string; 7 | fun: string; 8 | }; 9 | 10 | export type TransactionResult = TransactionArgument & TransactionArgument[]; 11 | 12 | export type TransactionBlockArgument = { 13 | kind: "Input"; 14 | index: number; 15 | type?: "object" | "pure" | undefined; 16 | value?: any; 17 | }; 18 | 19 | export function txObj( 20 | p: BuildTxParams, 21 | args: ( 22 | tx: TransactionBlock 23 | ) => (TransactionBlockArgument | TransactionResult)[], 24 | tArgs: string[] 25 | ): [TransactionBlock, TransactionResult] { 26 | const tx = p.transaction ?? new TransactionBlock(); 27 | 28 | const callResult = tx.moveCall({ 29 | target: `${p.packageObjectId}::${p.moduleName}::${p.fun}`, 30 | typeArguments: tArgs, 31 | arguments: args(tx), 32 | }); 33 | 34 | return [tx, callResult]; 35 | } 36 | -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | export const strToByteArray = (str: string): number[] => { 2 | const utf8Encode = new TextEncoder(); 3 | return Array.from(utf8Encode.encode(str).values()); 4 | }; 5 | 6 | // https://stackoverflow.com/a/10456644 7 | export const splitBy = (list: T[], chunkSize: number): T[][] => { 8 | const result: T[][] = []; 9 | for (let i = 0; i < list.length; i += chunkSize) { 10 | result.push(list.slice(i, i + chunkSize)); 11 | } 12 | 13 | return result; 14 | }; 15 | 16 | export const toMap = (list: T[], keyFn: (el: T) => K): Map => { 17 | return new Map(list.map((elem) => [keyFn(elem), elem])); 18 | }; 19 | 20 | export const groupBy = (list: T[], keyFn: (el: T) => K): Map => { 21 | return list.reduce((acc, elem) => { 22 | const group = acc.get(keyFn(elem)) || []; 23 | group.push(elem); 24 | acc.set(keyFn(elem), group); 25 | return acc; 26 | }, new Map()); 27 | }; 28 | 29 | // TODO: Replace with lodash functions 30 | export const uniq = (list: T[]): T[] => Array.from(new Set(list).values()); 31 | 32 | export const uniqBy = (list: T[], keyFn: (el: T) => K): T[] => { 33 | const m = toMap(list, keyFn); 34 | return Array.from(m.values()); 35 | }; 36 | -------------------------------------------------------------------------------- /tsconfig-typecheck.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "noEmit": true, 5 | "sourceMap": false, 6 | "incremental": false 7 | }, 8 | "include": ["src"] 9 | } 10 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "dist/", 4 | "sourceMap": true, 5 | "module": "commonjs", 6 | "moduleResolution": "node", 7 | "noEmitOnError": true, 8 | "esModuleInterop": true, 9 | "target": "es2017", 10 | "declaration": true, 11 | "removeComments": false, 12 | "noImplicitAny": true, 13 | "forceConsistentCasingInFileNames": true, 14 | "noUnusedLocals": true, 15 | "pretty": true, 16 | "skipLibCheck": true 17 | }, 18 | "include": ["src"], 19 | "exclude": [ 20 | "node_modules/@types/babel__traverse" 21 | ] 22 | } 23 | --------------------------------------------------------------------------------