diff --git a/.github/workflows/claude-code-review.yml b/.github/workflows/claude-code-review.yml new file mode 100644 index 000000000..ccac86cbe --- /dev/null +++ b/.github/workflows/claude-code-review.yml @@ -0,0 +1,53 @@ +name: Claude Code Review + +on: + pull_request: + types: [opened, synchronize] + # Optional: Only run on specific file changes + # paths: + # - "src/**/*.ts" + # - "src/**/*.tsx" + # - "src/**/*.js" + # - "src/**/*.jsx" + +jobs: + claude-review: + if: | + github.event.pull_request.user.login == 'external-contributor' || + github.event.pull_request.user.login == 'new-developer' || + github.event.pull_request.author_association == 'FIRST_TIME_CONTRIBUTOR' + + runs-on: ubuntu-latest + permissions: + contents: read + pull-requests: write + issues: read + id-token: write + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 1 + + - name: Run Claude Code Review + id: claude-review + uses: anthropics/claude-code-action@v1 + with: + claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} + prompt: | + Please review this pull request and provide feedback on: + - Code quality and best practices + - Potential bugs or issues + - Performance considerations + - Security concerns + - Test coverage + + Use the repository's CLAUDE.md for guidance on style and conventions. Be constructive and helpful in your feedback. + + Use `gh pr comment` with your Bash tool to leave your review as a comment on the PR. + + # See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md + # or https://docs.anthropic.com/en/docs/claude-code/sdk#command-line for available options + claude_args: '--allowed-tools "Bash(gh issue view:*),Bash(gh search:*),Bash(gh issue list:*),Bash(gh pr comment:*),Bash(gh pr diff:*),Bash(gh pr view:*),Bash(gh pr list:*)"' + diff --git a/.github/workflows/claude.yml b/.github/workflows/claude.yml new file mode 100644 index 000000000..7cd18e207 --- /dev/null +++ b/.github/workflows/claude.yml @@ -0,0 +1,63 @@ +name: Claude Code + +on: + issue_comment: + types: [created] + pull_request_review_comment: + types: [created] + issues: + types: [opened, assigned] + pull_request_review: + types: [submitted] + +jobs: + claude: + if: | + (github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude')) || + (github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude')) || + (github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude')) || + (github.event_name == 'issues' && (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude'))) + runs-on: ubuntu-latest + permissions: + contents: write # Needed to create commits/PRs + pull-requests: write # Needed to comment on PRs + issues: write # Needed to comment on issues + id-token: write + actions: read # Required for Claude to read CI results on PRs + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 1 + # For PR comments, checkout the PR branch instead of main + ref: ${{ github.event.issue.pull_request && format('refs/pull/{0}/head', github.event.issue.number) || '' }} + + - name: Run Claude Code + id: claude + uses: anthropics/claude-code-action@v1 + with: + claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} + + # This is an optional setting that allows Claude to read CI results on PRs + additional_permissions: | + actions: read + + # Use review prompt for simple @claude mentions in PR comments, otherwise follow specific instructions + prompt: | + ${{ github.event_name == 'issue_comment' && github.event.issue.pull_request && github.event.comment.body == '@claude' && + format('Please review pull request #{0} and provide feedback on: + - Code quality and best practices + - Potential bugs or issues + - Performance considerations + - Security concerns + - Test coverage + + Use the repository''s CLAUDE.md for guidance on style and conventions. Be constructive and helpful in your feedback. + + Use `gh pr comment {0}` with your Bash tool to leave your review as a comment on the PR.', github.event.issue.number) || '' }} + + # Optional: Add claude_args to customize behavior and configuration + # See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md + # or https://docs.anthropic.com/en/docs/claude-code/sdk#command-line for available options + claude_args: '--allowed-tools "Bash(gh issue view:*),Bash(gh search:*),Bash(gh issue list:*),Bash(gh pr comment:*),Bash(gh pr diff:*),Bash(gh pr view:*),Bash(gh pr list:*)"' + diff --git a/.github/workflows/deepbookv3-build-tx.yml b/.github/workflows/deepbookv3-build-tx.yml index 4339368d3..36630f0d9 100644 --- a/.github/workflows/deepbookv3-build-tx.yml +++ b/.github/workflows/deepbookv3-build-tx.yml @@ -13,12 +13,17 @@ on: - Enable Version - Disable Version - Unregister Pool and Create - - Prep MVR - - Prep Kiosk MVR - - Prep Kiosk MVR Registration - - Package Info Creation - - Register Deepbook with MVR - Add Stable Coins + - Adjust Tick Size + - Adjust Min Lot Size + - Transfer Funds + - Prep Deepbook MVR + - Margin Setup + - Margin PackageInfo Creation + - Fund Margin Pool + - Fund Abyss Vault + - Fund Liquidation Vault + - Update Interest Rates sui_tools_image: description: "image reference of sui_tools" default: "mysten/sui-tools:mainnet" @@ -36,7 +41,7 @@ on: jobs: deepbook: name: deepbook create tx - runs-on: ubuntu-latest + runs-on: macos-latest steps: - name: Selected transaction type @@ -57,8 +62,8 @@ jobs: - name: YAML Setup run: | - sui client --yes new-env --rpc https://fullnode.mainnet.sui.io:443 --alias mainnet - sui client switch --env mainnet + sui client --yes new-env --rpc https://fullnode.mainnet.sui.io:443 --alias mainnet-env + sui client switch --env mainnet-env - name: NPM BUILD TX Environment uses: actions/setup-node@v4 @@ -184,6 +189,206 @@ jobs: run: | cd scripts && pnpm install && pnpm ts-node transactions/addStablecoin.ts + - name: Transfer Mvr Kiosk + if: ${{ inputs.transaction_type == 'Transfer Mvr Kiosk' }} + env: + NODE_ENV: production + GAS_OBJECT: ${{ inputs.gas_object_id }} + NETWORK: mainnet + ORIGIN: gh_action + run: | + cd scripts && pnpm install && pnpm ts-node transactions/transferMvrObjectsKiosk.ts + + - name: Finish MVR Setup + if: ${{ inputs.transaction_type == 'Finish MVR Setup' }} + env: + NODE_ENV: production + GAS_OBJECT: ${{ inputs.gas_object_id }} + NETWORK: mainnet + ORIGIN: gh_action + run: | + cd scripts && pnpm install && pnpm ts-node transactions/allMvrSetup.ts + + - name: MVR Package Reverse Resolution + if: ${{ inputs.transaction_type == 'MVR Package Reverse Resolution' }} + env: + NODE_ENV: production + GAS_OBJECT: ${{ inputs.gas_object_id }} + NETWORK: mainnet + ORIGIN: gh_action + run: | + cd scripts && pnpm install && pnpm ts-node transactions/mvrPackageReverseResolution.ts + + - name: Setup Denylist + if: ${{ inputs.transaction_type == 'Setup Denylist' }} + env: + NODE_ENV: production + GAS_OBJECT: ${{ inputs.gas_object_id }} + NETWORK: mainnet + ORIGIN: gh_action + run: | + cd scripts && pnpm install && pnpm ts-node transactions/setupDenylist.ts + + - name: MVR Package Metadata + if: ${{ inputs.transaction_type == 'MVR Package Metadata' }} + env: + NODE_ENV: production + GAS_OBJECT: ${{ inputs.gas_object_id }} + NETWORK: mainnet + ORIGIN: gh_action + run: | + cd scripts && pnpm install && pnpm ts-node transactions/mvrPackageMetadata.ts + + - name: Adjust Tick Size + if: ${{ inputs.transaction_type == 'Adjust Tick Size' }} + env: + NODE_ENV: production + GAS_OBJECT: ${{ inputs.gas_object_id }} + NETWORK: mainnet + ORIGIN: gh_action + run: | + cd scripts && pnpm install && pnpm ts-node transactions/updatePoolTickSize.ts + + - name: Adjust Min Lot Size + if: ${{ inputs.transaction_type == 'Adjust Min Lot Size' }} + env: + NODE_ENV: production + GAS_OBJECT: ${{ inputs.gas_object_id }} + NETWORK: mainnet + ORIGIN: gh_action + run: | + cd scripts && pnpm install && pnpm ts-node transactions/updatePoolMinLotSize.ts + + - name: Fix MVR Path + if: ${{ inputs.transaction_type == 'Fix MVR Path' }} + env: + NODE_ENV: production + GAS_OBJECT: ${{ inputs.gas_object_id }} + NETWORK: mainnet + ORIGIN: gh_action + run: | + cd scripts && pnpm install && pnpm ts-node transactions/mvrFix.ts + + - name: Setup Walrus Site + if: ${{ inputs.transaction_type == 'Setup Walrus Site' }} + env: + NODE_ENV: production + GAS_OBJECT: ${{ inputs.gas_object_id }} + NETWORK: mainnet + ORIGIN: gh_action + run: | + cd scripts && pnpm install && pnpm ts-node transactions/walrusSitesSetup.ts + + - name: Nautilus Setup + if: ${{ inputs.transaction_type == 'Nautilus Setup' }} + env: + NODE_ENV: production + GAS_OBJECT: ${{ inputs.gas_object_id }} + NETWORK: mainnet + ORIGIN: gh_action + run: | + cd scripts && pnpm install && pnpm ts-node transactions/nautilus-setup.ts + + - name: Payment Setup + if: ${{ inputs.transaction_type == 'Payment Setup' }} + env: + NODE_ENV: production + GAS_OBJECT: ${{ inputs.gas_object_id }} + NETWORK: mainnet + ORIGIN: gh_action + run: | + cd scripts && pnpm install && pnpm ts-node transactions/paymentSetup.ts + + - name: Margin Setup + if: ${{ inputs.transaction_type == 'Margin Setup' }} + env: + NODE_ENV: production + GAS_OBJECT: ${{ inputs.gas_object_id }} + NETWORK: mainnet + ORIGIN: gh_action + run: | + cd scripts && pnpm install && pnpm ts-node transactions/marginSetup.ts + + - name: Margin PackageInfo Creation + if: ${{ inputs.transaction_type == 'Margin PackageInfo Creation' }} + env: + NODE_ENV: production + GAS_OBJECT: ${{ inputs.gas_object_id }} + NETWORK: mainnet + ORIGIN: gh_action + run: | + cd scripts && pnpm install && pnpm ts-node transactions/marginPackageInfo.ts + + - name: Fund Margin Pool + if: ${{ inputs.transaction_type == 'Fund Margin Pool' }} + env: + NODE_ENV: production + GAS_OBJECT: ${{ inputs.gas_object_id }} + NETWORK: mainnet + ORIGIN: gh_action + run: | + cd scripts && pnpm install && pnpm ts-node transactions/supplyToMarginPool.ts + + - name: Fund Abyss Vault + if: ${{ inputs.transaction_type == 'Fund Abyss Vault' }} + env: + NODE_ENV: production + GAS_OBJECT: ${{ inputs.gas_object_id }} + NETWORK: mainnet + ORIGIN: gh_action + run: | + cd scripts && pnpm install && pnpm ts-node transactions/fundAbyssVault.ts + + - name: Create Liquidation Vault + if: ${{ inputs.transaction_type == 'Create Liquidation Vault' }} + env: + NODE_ENV: production + GAS_OBJECT: ${{ inputs.gas_object_id }} + NETWORK: mainnet + ORIGIN: gh_action + run: | + cd scripts && pnpm install && pnpm ts-node transactions/createLiquidationVault.ts + + - name: Transfer Funds + if: ${{ inputs.transaction_type == 'Transfer Funds' }} + env: + NODE_ENV: production + GAS_OBJECT: ${{ inputs.gas_object_id }} + NETWORK: mainnet + ORIGIN: gh_action + run: | + cd scripts && pnpm install && pnpm ts-node transactions/transferFunds.ts + + - name: Fund Liquidation Vault + if: ${{ inputs.transaction_type == 'Fund Liquidation Vault' }} + env: + NODE_ENV: production + GAS_OBJECT: ${{ inputs.gas_object_id }} + NETWORK: mainnet + ORIGIN: gh_action + run: | + cd scripts && pnpm install && pnpm ts-node transactions/fundLiquidationVault.ts + + - name: Update Interest Rates + if: ${{ inputs.transaction_type == 'Update Interest Rates' }} + env: + NODE_ENV: production + GAS_OBJECT: ${{ inputs.gas_object_id }} + NETWORK: mainnet + ORIGIN: gh_action + run: | + cd scripts && pnpm install && pnpm ts-node transactions/updateInterestRates.ts + + - name: Prep Deepbook MVR + if: ${{ inputs.transaction_type == 'Prep Deepbook MVR' }} + env: + NODE_ENV: production + GAS_OBJECT: ${{ inputs.gas_object_id }} + NETWORK: mainnet + ORIGIN: gh_action + run: | + cd scripts && pnpm install && pnpm ts-node transactions/newDeepbookVersion.ts + - name: Show Transaction Data (To sign) run: | cat scripts/tx/tx-data.txt diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml new file mode 100644 index 000000000..a999cee5f --- /dev/null +++ b/.github/workflows/deploy.yml @@ -0,0 +1,45 @@ +name: Deploy DeepBook Services + +on: + workflow_dispatch: + push: + branches: + - main + paths: + - crates/** + - docker/** + - Cargo.lock + - Cargo.toml + +concurrency: + group: deployment + cancel-in-progress: true + +jobs: + deploy-pulumi: + name: Deploy to Testnet + runs-on: ubuntu-latest + permissions: + contents: read + environment: production + + steps: + - name: Checkout code + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # pin@v4.2.2 + + - name: Trigger Pulumi Deployment + uses: peter-evans/repository-dispatch@ff45666b9427631e3450c54a1bcbee4d9ff4d7c0 # pin@v3.0.0 + with: + repository: MystenLabs/sui-operations + token: ${{ secrets.DEPLOY_PULUMI_DISPATCH_TOKEN }} + event-type: pulumi-up + client-payload: |- + { + "project": "apps/deepbook", + "stack": "testnet" + } + + - name: View deployment status + run: | + echo "🚀 View the status of the deployment here: https://github.com/MystenLabs/sui-operations/actions/workflows/pulumi-up.yaml" + diff --git a/.github/workflows/move-formatter.yml b/.github/workflows/move-formatter.yml index f7b4de0d3..c9952de60 100644 --- a/.github/workflows/move-formatter.yml +++ b/.github/workflows/move-formatter.yml @@ -25,3 +25,5 @@ jobs: uses: actions/setup-node@v4 - run: npm i @mysten/prettier-plugin-move - run: npx prettier-move -c $PWD/../packages/deepbook/**/*.move + - run: npx prettier-move -c $PWD/../packages/deepbook_margin/**/*.move + - run: npx prettier-move -c $PWD/../packages/margin_liquidation/**/*.move diff --git a/.github/workflows/move_test.yml b/.github/workflows/move_test.yml index 5c0fb740f..d0beeef6c 100644 --- a/.github/workflows/move_test.yml +++ b/.github/workflows/move_test.yml @@ -22,34 +22,13 @@ jobs: with: fetch-depth: 1 - - name: Install Sui 1.45.3 + - name: Install Homebrew run: | - echo "Installing Sui 1.45.3..." - mkdir -p $HOME/sui-bin + /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" + echo "/home/linuxbrew/.linuxbrew/bin" >> $GITHUB_PATH - SUI_URL="https://github.com/MystenLabs/sui/releases/download/mainnet-v1.45.3/sui-mainnet-v1.45.3-macos-x86_64.tgz" - echo "Downloading Sui from $SUI_URL" - - # Use curl with fail flag and check response - HTTP_STATUS=$(curl -o sui.tar.gz -w "%{http_code}" -L $SUI_URL) - - if [[ "$HTTP_STATUS" -ne 200 ]]; then - echo "Error: Failed to download Sui. HTTP Status: $HTTP_STATUS" - exit 1 - fi - - if ! file sui.tar.gz | grep -q "gzip compressed"; then - echo "Error: Downloaded file is not a valid tar.gz archive." - exit 1 - fi - - tar -xvzf sui.tar.gz -C $HOME/sui-bin - chmod +x $HOME/sui-bin/sui - echo "$HOME/sui-bin" >> $GITHUB_PATH - export PATH="$HOME/sui-bin:$PATH" - - # Verify installation - sui --version + - name: Install Sui using Homebrew + run: brew install sui - name: Run Move tests in all package subdirectories, with exclusions run: | diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml new file mode 100644 index 000000000..725007223 --- /dev/null +++ b/.github/workflows/rust.yml @@ -0,0 +1,51 @@ +name: Rust + +on: + push: + branches: ["main"] + paths: + - crates/** + - rust.yml + - Cargo.lock + - Cargo.toml + pull_request: + branches: ["main"] + paths: + - crates/** + - rust.yml + - Cargo.lock + - Cargo.toml + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: ${{ github.ref != 'refs/heads/main' }} + +env: + CARGO_TERM_COLOR: always + +jobs: + test-crates: + name: test-indexer + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: taiki-e/install-action@cargo-nextest + - name: Install postgres + shell: bash + run: | + sudo apt update && sudo apt install postgresql + + - name: Add postgres to PATH + run: echo "/usr/lib/postgresql/16/bin" >> $GITHUB_PATH + + - name: Run deepbook-indexer tests + run: | + cargo nextest run -E 'package(deepbook-indexer)' + + + rustfmt: + runs-on: [ ubuntu-latest ] + steps: + - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # Pin v4.1.1 + - run: rustup component add rustfmt + - run: cargo fmt --check diff --git a/.gitignore b/.gitignore index 59fa539f8..05755b6f3 100644 --- a/.gitignore +++ b/.gitignore @@ -13,9 +13,13 @@ build # Node.js node_modules package-lock.json +**/pnpm-lock.yaml # misc .env +.envrc **/benchmark_data/ tx-data.txt example.ts +/data +/handoff \ No newline at end of file diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 000000000..b98e33ad5 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,640 @@ +Sui Move instructions (.move files): + +- Only put comments to document functions, struct fields, and items that need clarification. DO NOT PUT EXTRANEOUS COMMENTS THROUGHOUT + +- Sui is an object-oriented blockchain. Sui smart contracts are written in the Move language. + +- Sui's object ownership model guarantees that the sender of a transaction has permission to use the objects it passes to functions as arguments. + +- Sui object ownership model in a nutshell: + - Single owner objects: owned by a single address - granting it exclusive control over the object. + - Shared objects: any address can use them in transactions and pass them to functions. + - Immutable objects: like Shared objects, any address can use them, but they are read-only. + +- Abilities are a Move typing feature that control what actions are permissible on a struct: + - `key`: the struct can be used as a key in storage. If an struct does not have the key ability, it has to be stored under another struct or destroyed before the end of the transaction. + - `store`: the struct can be stored inside other structs. It also relaxes transfer restrictions. + - `drop`: the struct can be dropped or discarded. Simply allowing the object to go out of scope will destroy it. + - `copy`: the struct can be copied. + +- Structs can only be created within the module that defines them. A module exposes functions to determine how its structs can be created, read, modified and destroyed. + +- Similarly, the `transfer::transfer/share/freeze/receive/party_transfer` functions can only be called within the module that defines the struct being transferred. However, if the struct has the `store` ability, the `transfer::public_transfer/public_share/etc` functions can be called on that object from other modules. + +- All numbers are unsigned integers (u8, u16, u32, u64, u128, u256). + +- Functions calls are all or nothing (atomic). If there's an error, the transaction is reverted. + +- Race conditions are impossible. + +- It is allowed to compare a reference to a value using == or !=. The language automatically borrows the value if one operand is a reference and the other is not. + +- Integer overflows/underflows are automatically reverted. Any transaction that causes an integer overflow/underflow cannot succeed. E.g. `std::u64::max_value!() + 1` raises an arithmetic error. + +- Don't worry about "missing imports", because the compiler includes many std::/sui:: imports by default. + +- Don't worry about emitting additional events. + +- Prefer macros over constants. + +- Put public functions first, then public(package), then private. + + +## TOOL CALLING INSTRUCTIONS +- `sui move build` to build the package, must be run in a directory with Move.toml in it +- `sui move test` to run tests, must be run in a directory with Move.toml in it +- can pass `--skip-fetch-latest-git-deps` if the dependencies haven't changed after an initial successful build +- when you have completed making changes, run `bunx prettier-move -c *.move --write` on any files that are modified to format them correctly. + +# Code Quality Checklist + +The rapid evolution of the Move language and its ecosystem has rendered many older practices +outdated. This guide serves as a checklist for developers to review their code and ensure it aligns +with current best practices in Move development. Please read carefully and apply as many +recommendations as possible to your code. + +## Code Organization + +Some of the issues mentioned in this guide can be fixed by using +[Move Formatter](https://www.npmjs.com/package/@mysten/prettier-plugin-move) either as a CLI tool, +or [as a CI check](https://github.com/marketplace/actions/move-formatter), or +[as a plugin for VSCode (Cursor)](https://marketplace.visualstudio.com/items?itemName=mysten.prettier-move). + +## Package Manifest + +### Use Right Edition + +All of the features in this guide require Move 2024 Edition, and it has to be specified in the +package manifest. + +```toml +[package] +name = "my_package" +edition = "2024.beta" # or (just) "2024" +``` + +### Implicit Framework Dependency + +Starting with Sui 1.45 you no longer need to specify framework dependency in the `Move.toml`: + +```toml +# old, pre 1.45 +[dependencies] +Sui = { ... } + +# modern day, Sui, Bridge, MoveStdlib and SuiSystem are imported implicitly! +[dependencies] +``` + +### Prefix Named Addresses + +If your package has a generic name (e.g., `token`) – especially if your project includes multiple +packages – make sure to add a prefix to the named address: + +```toml +# bad! not indicative of anything, and can conflict +[addresses] +math = "0x0" + +# good! clearly states project, unlikely to conflict +[addresses] +my_protocol_math = "0x0" +``` + +## Imports, Module and Constants + +### Using Module Label + +```move +// bad: increases indentation, legacy style +module my_package::my_module { + public struct A {} +} + +// good! +module my_package::my_module; + +public struct A {} +``` + +### No Single `Self` in `use` Statements + +```move +// correct, member + self import +use my_package::other::{Self, OtherMember}; + +// bad! `{Self}` is redundant +use my_package::my_module::{Self}; + +// good! +use my_package::my_module; +``` + +### Group `use` Statements with `Self` + +```move +// bad! +use my_package::my_module; +use my_package::my_module::OtherMember; + +// good! +use my_package::my_module::{Self, OtherMember}; +``` + +### Error Constants are in `EPascalCase` + +```move +// bad! all-caps are used for regular constants +const NOT_AUTHORIZED: u64 = 0; + +// good! clear indication it's an error constant +const ENotAuthorized: u64 = 0; +``` + +### Regular Constant are `ALL_CAPS` + +```move +// bad! PascalCase is associated with error consts +const MyConstant: vector = b"my const"; + +// good! clear indication that it's a constant value +const MY_CONSTANT: vector = b"my const"; +``` + +## Structs + +### Capabilities are Suffixed with `Cap` + +```move +// bad! if it's a capability, add a `Cap` suffix +public struct Admin has key, store { + id: UID, +} + +// good! reviewer knows what to expect from type +public struct AdminCap has key, store { + id: UID, +} +``` + +### No `Potato` in Names + +```move +// bad! it has no abilities, we already know it's a Hot-Potato type +public struct PromisePotato {} + +// good! +public struct Promise {} +``` + +### Events Should Be Named in Past Tense + +```move +// bad! not clear what this struct does +public struct RegisterUser has copy, drop { user: address } + +// good! clear, it's an event +public struct UserRegistered has copy, drop { user: address } +``` + +### Use Positional Structs for Dynamic Field Keys + `Key` Suffix + +```move +// not as bad, but goes against canonical style +public struct DynamicField has copy, drop, store {} + +// good! canonical style, Key suffix +public struct DynamicFieldKey() has copy, drop, store; +``` + +## Functions + +### No `public entry`, Only `public` or `entry` + +```move +// bad! entry is not required for a function to be callable in a transaction +public entry fun do_something() { /* ... */ } + +// good! public functions are more permissive, can return value +public fun do_something_2(): T { /* ... */ } +``` + +### Write Composable Functions for PTBs + +```move +// bad! not composable, harder to test! +public fun mint_and_transfer(ctx: &mut TxContext) { + /* ... */ + transfer::transfer(nft, ctx.sender()); +} + +// good! composable! +public fun mint(ctx: &mut TxContext): NFT { /* ... */ } + +// good! intentionally not composable +entry fun mint_and_keep(ctx: &mut TxContext) { /* ... */ } +``` + +### Objects Go First (Except for Clock) + +```move +// bad! hard to read! +public fun call_app( + value: u8, + app: &mut App, + is_smth: bool, + cap: &AppCap, + clock: &Clock, + ctx: &mut TxContext, +) { /* ... */ } + +// good! +public fun call_app( + app: &mut App, + cap: &AppCap, + value: u8, + is_smth: bool, + clock: &Clock, + ctx: &mut TxContext, +) { /* ... */ } +``` + +### Capabilities Go Second + +```move +// bad! breaks method associativity +public fun authorize_action(cap: &AdminCap, app: &mut App) { /* ... */ } + +// good! keeps Cap visible in the signature and maintains `.calls()` +public fun authorize_action(app: &mut App, cap: &AdminCap) { /* ... */ } +``` + +### Getters Named After Field + `_mut` + +```move +// bad! unnecessary `get_` +public fun get_name(u: &User): String { /* ... */ } + +// good! clear that it accesses field `name` +public fun name(u: &User): String { /* ... */ } + +// good! for mutable references use `_mut` +public fun details_mut(u: &mut User): &mut Details { /* ... */ } +``` + +## Function Body: Struct Methods + +### Common Coin Operations + +```move +// bad! legacy code, hard to read! +let paid = coin::split(&mut payment, amount, ctx); +let balance = coin::into_balance(paid); + +// good! struct methods make it easier! +let balance = payment.split(amount, ctx).into_balance(); + +// even better (in this example - no need to create temporary coin) +let balance = payment.balance_mut().split(amount); + +// also can do this! +let coin = balance.into_coin(ctx); +``` + +### Do Not Import `std::string::utf8` + +```move +// bad! unfortunately, very common! +use std::string::utf8; + +let str = utf8(b"hello, world!"); + +// good! +let str = b"hello, world!".to_string(); + +// also, for ASCII string +let ascii = b"hello, world!".to_ascii_string(); +``` + +### UID has `delete` + +```move +// bad! +object::delete(id); + +// good! +id.delete(); +``` + +### `ctx` has `sender()` + +```move +// bad! +tx_context::sender(ctx); + +// good! +ctx.sender() +``` + +### Vector Has a Literal. And Associated Functions + +```move +// bad! +let mut my_vec = vector::empty(); +vector::push_back(&mut my_vec, 10); +let first_el = vector::borrow(&my_vec); +assert!(vector::length(&my_vec) == 1); + +// good! +let mut my_vec = vector[10]; +let first_el = my_vec[0]; +assert!(my_vec.length() == 1); +``` + +### Collections Support Index Syntax + +```move +let x: VecMap = /* ... */; + +// bad! +x.get(&10); +x.get_mut(&10); + +// good! +&x[&10]; +&mut x[&10]; +``` + +## Option -> Macros + +### Destroy And Call Function + +```move +// bad! +if (opt.is_some()) { + let inner = opt.destroy_some(); + call_function(inner); +}; + +// good! there's a macro for it! +opt.do!(|value| call_function(value)); +``` + +### Destroy Some With Default + +```move +let opt = option::none(); + +// bad! +let value = if (opt.is_some()) { + opt.destroy_some() +} else { + abort EError +}; + +// good! there's a macro! +let value = opt.destroy_or!(default_value); + +// you can even do abort on `none` +let value = opt.destroy_or!(abort ECannotBeEmpty); +``` + +## Loops -> Macros + +### Do Operation N Times + +```move +// bad! hard to read! +let mut i = 0; +while (i < 32) { + do_action(); + i = i + 1; +}; + +// good! any uint has this macro! +32u8.do!(|_| do_action()); +``` + +### New Vector From Iteration + +```move +// harder to read! +let mut i = 0; +let mut elements = vector[]; +while (i < 32) { + elements.push_back(i); + i = i + 1; +}; + +// easy to read! +vector::tabulate!(32, |i| i); +``` + +### Do Operation on Every Element of a Vector + +```move +// bad! +let mut i = 0; +while (i < vec.length()) { + call_function(&vec[i]); + i = i + 1; +}; + +// good! +vec.do_ref!(|e| call_function(e)); +``` + +### Destroy a Vector and Call a Function on Each Element + +```move +// bad! +while (!vec.is_empty()) { + call(vec.pop_back()); +}; + +// good! +vec.destroy!(|e| call(e)); +``` + +### Fold Vector Into a Single Value + +```move +// bad! +let mut aggregate = 0; +let mut i = 0; + +while (i < source.length()) { + aggregate = aggregate + source[i]; + i = i + 1; +}; + +// good! +let aggregate = source.fold!(0, |acc, v| { + acc + v +}); +``` + +### Filter Elements of the Vector + +> Note: `T: drop` in the `source` vector + +```move +// bad! +let mut filtered = []; +let mut i = 0; +while (i < source.length()) { + if (source[i] > 10) { + filtered.push_back(source[i]); + }; + i = i + 1; +}; + +// good! +let filtered = source.filter!(|e| e > 10); +``` + +## Other + +### Ignored Values In Unpack Can Be Ignored Altogether + +```move +// bad! very sparse! +let MyStruct { id, field_1: _, field_2: _, field_3: _ } = value; +id.delete(); + +// good! 2024 syntax +let MyStruct { id, .. } = value; +id.delete(); +``` + +## Testing + +### Merge `#[test]` and `#[expected_failure(...)]` + +```move +// bad! +#[test] +#[expected_failure] +fun value_passes_check() { + abort +} + +// good! +#[test, expected_failure] +fun value_passes_check() { + abort +} +``` + +### Do Not Clean Up `expected_failure` Tests + +```move +// bad! clean up is not necessary +#[test, expected_failure(abort_code = my_app::EIncorrectValue)] +fun try_take_missing_object_fail() { + let mut test = test_scenario::begin(@0); + my_app::call_function(test.ctx()); + test.end(); +} + +// good! easy to see where test is expected to fail +#[test, expected_failure(abort_code = my_app::EIncorrectValue)] +fun try_take_missing_object_fail() { + let mut test = test_scenario::begin(@0); + my_app::call_function(test.ctx()); + + abort // will differ from EIncorrectValue +} +``` + +### Do Not Prefix Tests With `test_` in Testing Modules + +```move +// bad! the module is already called _tests +module my_package::my_module_tests; + +#[test] +fun test_this_feature() { /* ... */ } + +// good! better function name as the result +#[test] +fun this_feature_works() { /* ... */ } +``` + +### Do Not Use `TestScenario` Where Not Necessary + +```move +// bad! no need, only using ctx +let mut test = test_scenario::begin(@0); +let nft = app::mint(test.ctx()); +app::destroy(nft); +test.end(); + +// good! there's a dummy context for simple cases +let ctx = &mut tx_context::dummy(); +app::mint(ctx).destroy(); +``` + +### Do Not Use Abort Codes in `assert!` in Tests + +```move +// bad! may match application error codes by accident +assert!(is_success, 0); + +// good! +assert!(is_success); +``` + +### Use `assert_eq!` Whenever Possible + +```move +// bad! old-style code +assert!(result == b"expected_value", 0); + +// good! will print both values if fails +use std::unit_test::assert_eq; + +assert_eq!(result, expected_value); +``` + +### Use "Black Hole" `destroy` Function + +```move +// bad! +nft.destroy_for_testing(); +app.destroy_for_testing(); + +// good! - no need to define special functions for cleanup +use sui::test_utils::destroy; + +destroy(nft); +destroy(app); +``` + +## Comments + +### Doc Comments Start With `///` + +```move +// bad! tooling doesn't support JavaDoc-style comments +/** + * Cool method + * @param ... + */ +public fun do_something() { /* ... */ } + +// good! will be rendered as a doc comment in docgen and IDE's +/// Cool method! +public fun do_something() { /* ... */ } +``` + +### Complex Logic? Leave a Comment `//` + +Being friendly and helping reviewers understand the code! + +```move +// good! +// Note: can underflow if a value is smaller than 10. +// TODO: add an `assert!` here +let value = external_call(value, ctx); +``` diff --git a/Cargo.lock b/Cargo.lock index 27eb6f077..eb761e990 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,16 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "Inflector" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe438c63458706e03479442743baae6c88256498e6431708f6dfc520a26515d3" +dependencies = [ + "lazy_static", + "regex", +] + [[package]] name = "addchain" version = "0.2.0" @@ -15,18 +25,18 @@ dependencies = [ [[package]] name = "addr2line" -version = "0.24.2" +version = "0.25.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" +checksum = "1b5d307320b3181d6d7954e663bd7c774a838b8220fe0593c86d9fb09f498b4b" dependencies = [ "gimli", ] [[package]] name = "adler2" -version = "2.0.0" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" [[package]] name = "aead" @@ -63,34 +73,49 @@ dependencies = [ "subtle", ] +[[package]] +name = "aes-gcm-siv" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae0784134ba9375416d469ec31e7c5f9fa94405049cf08c5ce5b4698be673e0d" +dependencies = [ + "aead", + "aes", + "cipher", + "ctr", + "polyval", + "subtle", + "zeroize", +] + [[package]] name = "ahash" version = "0.7.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "891477e0c6a8957309ee5c45a6368af3ae14bb510732d2684ffa19af310920f9" dependencies = [ - "getrandom 0.2.15", + "getrandom 0.2.17", "once_cell", "version_check", ] [[package]] name = "ahash" -version = "0.8.11" +version = "0.8.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" dependencies = [ "cfg-if", "once_cell", "version_check", - "zerocopy 0.7.35", + "zerocopy", ] [[package]] name = "aho-corasick" -version = "1.1.3" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" dependencies = [ "memchr", ] @@ -117,16 +142,34 @@ dependencies = [ ] [[package]] -name = "allocator-api2" -version = "0.2.21" +name = "allocative" +version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" +checksum = "8fac2ce611db8b8cee9b2aa886ca03c924e9da5e5295d0dbd0526e5d0b0710f7" +dependencies = [ + "allocative_derive", + "bumpalo", + "ctor", + "hashbrown 0.14.5", + "num-bigint 0.4.6", +] [[package]] -name = "android-tzdata" -version = "0.1.1" +name = "allocative_derive" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe233a377643e0fc1a56421d7c90acdec45c291b30345eb9f08e8d0ddce5a4ab" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "allocator-api2" +version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" [[package]] name = "android_system_properties" @@ -140,7 +183,7 @@ dependencies = [ [[package]] name = "anemo" version = "0.0.0" -source = "git+https://github.com/mystenlabs/anemo.git?rev=9c52c3c7946532163a79129db15180cdb984bab4#9c52c3c7946532163a79129db15180cdb984bab4" +source = "git+https://github.com/mystenlabs/anemo.git?rev=4b5f0f1d06a31c8ef78ec2e5b446bc633e4e2f77#4b5f0f1d06a31c8ef78ec2e5b446bc633e4e2f77" dependencies = [ "anyhow", "async-trait", @@ -149,7 +192,7 @@ dependencies = [ "ed25519", "futures", "hex", - "http 1.3.1", + "http", "matchit 0.5.0", "pin-project-lite", "pkcs8 0.10.2", @@ -162,11 +205,11 @@ dependencies = [ "rustls-webpki", "serde", "serde_json", - "socket2 0.5.8", + "socket2 0.5.10", "tap", "thiserror 1.0.69", "tokio", - "tokio-util 0.7.14", + "tokio-util 0.7.18", "tower 0.4.13", "tracing", "x509-parser", @@ -175,7 +218,7 @@ dependencies = [ [[package]] name = "anemo-tower" version = "0.0.0" -source = "git+https://github.com/mystenlabs/anemo.git?rev=9c52c3c7946532163a79129db15180cdb984bab4#9c52c3c7946532163a79129db15180cdb984bab4" +source = "git+https://github.com/mystenlabs/anemo.git?rev=4b5f0f1d06a31c8ef78ec2e5b446bc633e4e2f77#4b5f0f1d06a31c8ef78ec2e5b446bc633e4e2f77" dependencies = [ "anemo", "bytes", @@ -190,11 +233,20 @@ dependencies = [ "uuid", ] +[[package]] +name = "annotate-snippets" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccaf7e9dfbb6ab22c82e473cd1a8a7bd313c19a5b7e40970f3d89ef5a5c9e81e" +dependencies = [ + "unicode-width 0.1.14", +] + [[package]] name = "anstream" -version = "0.6.18" +version = "0.6.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b" +checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" dependencies = [ "anstyle", "anstyle-parse", @@ -207,55 +259,71 @@ dependencies = [ [[package]] name = "anstyle" -version = "1.0.10" +version = "1.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" +checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" [[package]] name = "anstyle-parse" -version = "0.2.6" +version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9" +checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" dependencies = [ "utf8parse", ] [[package]] name = "anstyle-query" -version = "1.1.2" +version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] name = "anstyle-wincon" -version = "3.0.7" +version = "3.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca3534e77181a9cc07539ad51f2141fe32f6c3ffd4df76db8ad92346b003ae4e" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" dependencies = [ "anstyle", + "once_cell_polyfill", + "windows-sys 0.61.2", +] + +[[package]] +name = "antithesis_sdk" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa9414f07948b76e1f6aec64946ef8768db0534d8954a6bc7080e4f4575b6cea" +dependencies = [ + "libc", + "libloading", + "linkme", "once_cell", - "windows-sys 0.59.0", + "rand 0.8.5", + "rustc_version_runtime", + "serde", + "serde_json", ] [[package]] name = "anyhow" -version = "1.0.97" +version = "1.0.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dcfed56ad506cb2c684a14971b8861fdc3baaaae314b9e5f9bb532cbe3ba7a4f" +checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" dependencies = [ "backtrace", ] [[package]] -name = "arc-swap" -version = "1.7.1" +name = "ar_archive_writer" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69f7f8c3906b62b754cd5326047894316021dcfe5a194c8ea52bdd94934a3457" +checksum = "f0c269894b6fe5e9d7ada0cf69b5bf847ff35bc25fc271f08e1d080fce80339a" dependencies = [ - "serde", + "object 0.32.2", ] [[package]] @@ -284,7 +352,7 @@ dependencies = [ "blake2", "derivative", "digest 0.10.7", - "sha2 0.10.8", + "sha2 0.10.9", ] [[package]] @@ -386,6 +454,17 @@ dependencies = [ "tracing", ] +[[package]] +name = "ark-secp256k1" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c02e954eaeb4ddb29613fee20840c2bbc85ca4396d53e33837e11905363c5f2" +dependencies = [ + "ark-ec", + "ark-ff", + "ark-std", +] + [[package]] name = "ark-secp256r1" version = "0.4.0" @@ -454,6 +533,15 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" +[[package]] +name = "ascii-canvas" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8824ecca2e851cec16968d54a01dd372ef8f95b244fb84b84e70128be347c3c6" +dependencies = [ + "term", +] + [[package]] name = "asn1-rs" version = "0.7.1" @@ -466,7 +554,7 @@ dependencies = [ "nom", "num-traits", "rusticata-macros", - "thiserror 2.0.12", + "thiserror 2.0.17", "time", ] @@ -478,8 +566,8 @@ checksum = "3109e49b1e4909e9db6515a30c633684d68cdeaa252f215214cb4fa1a5bfee2c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", - "synstructure 0.13.1", + "syn 2.0.114", + "synstructure 0.13.2", ] [[package]] @@ -490,23 +578,19 @@ checksum = "7b18050c2cd6fe86c3a76584ef5e0baf286d038cda203eb6223df2cc413565f7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.114", ] [[package]] name = "async-compression" -version = "0.4.21" +version = "0.4.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0cf008e5e1a9e9e22a7d3c9a4992e21a350290069e36d8fb72304ed17e8f2d2" +checksum = "d10e4f991a553474232bc0a31799f6d24b034a84c0971d80d2e2f78b2e576e40" dependencies = [ - "brotli", - "flate2", - "futures-core", - "memchr", + "compression-codecs", + "compression-core", "pin-project-lite", "tokio", - "zstd 0.13.3", - "zstd-safe 7.2.3", ] [[package]] @@ -528,7 +612,7 @@ checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.114", ] [[package]] @@ -538,13 +622,13 @@ source = "git+https://github.com/mystenmark/async-task?rev=4e45b26e11126b191701b [[package]] name = "async-trait" -version = "0.1.88" +version = "0.1.89" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e539d3fca749fcee5236ab05e93a52867dd549cc157c8cb7f99595f3cedffdb5" +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.114", ] [[package]] @@ -576,24 +660,25 @@ checksum = "7460f7dd8e100147b82a63afca1a20eb6c231ee36b90ba7272e14951cb58af59" [[package]] name = "autocfg" -version = "1.4.0" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" [[package]] name = "axum" -version = "0.6.20" +version = "0.7.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b829e4e32b91e643de6eafe82b1d90675f5874230191a4ffbc1b336dec4d6bf" +checksum = "edca88bc138befd0323b20752846e6587272d3b03b0343c8ea28a6f819e6e71f" dependencies = [ "async-trait", - "axum-core 0.3.4", - "bitflags 1.3.2", + "axum-core 0.4.5", "bytes", "futures-util", - "http 0.2.12", - "http-body 0.4.6", - "hyper 0.14.32", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-util", "itoa", "matchit 0.7.3", "memchr", @@ -602,120 +687,101 @@ dependencies = [ "pin-project-lite", "rustversion", "serde", - "sync_wrapper 0.1.2", - "tower 0.4.13", + "serde_json", + "serde_path_to_error", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tower 0.5.3", "tower-layer", "tower-service", + "tracing", ] [[package]] name = "axum" -version = "0.7.9" +version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edca88bc138befd0323b20752846e6587272d3b03b0343c8ea28a6f819e6e71f" +checksum = "8b52af3cb4058c895d37317bb27508dccc8e5f2d39454016b297bf4a400597b8" dependencies = [ - "async-trait", - "axum-core 0.4.5", + "axum-core 0.5.6", "axum-macros", "base64 0.22.1", "bytes", + "form_urlencoded", "futures-util", - "http 1.3.1", - "http-body 1.0.1", + "http", + "http-body", "http-body-util", - "hyper 1.6.0", + "hyper", "hyper-util", "itoa", - "matchit 0.7.3", + "matchit 0.8.4", "memchr", "mime", "percent-encoding", "pin-project-lite", - "rustversion", - "serde", + "serde_core", "serde_json", "serde_path_to_error", "serde_urlencoded", "sha1", - "sync_wrapper 1.0.2", + "sync_wrapper", "tokio", "tokio-tungstenite", - "tower 0.5.2", + "tower 0.5.3", "tower-layer", "tower-service", - "tracing", ] [[package]] name = "axum-core" -version = "0.3.4" +version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "759fa577a247914fd3f7f76d62972792636412fbfd634cd452f6a385a74d2d2c" +checksum = "09f2bd6146b97ae3359fa0cc6d6b376d9539582c7b4220f041a33ec24c226199" dependencies = [ "async-trait", "bytes", "futures-util", - "http 0.2.12", - "http-body 0.4.6", + "http", + "http-body", + "http-body-util", "mime", + "pin-project-lite", "rustversion", + "sync_wrapper", "tower-layer", "tower-service", + "tracing", ] [[package]] name = "axum-core" -version = "0.4.5" +version = "0.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09f2bd6146b97ae3359fa0cc6d6b376d9539582c7b4220f041a33ec24c226199" +checksum = "08c78f31d7b1291f7ee735c1c6780ccde7785daae9a9206026862dab7d8792d1" dependencies = [ - "async-trait", "bytes", - "futures-util", - "http 1.3.1", - "http-body 1.0.1", + "futures-core", + "http", + "http-body", "http-body-util", "mime", "pin-project-lite", - "rustversion", - "sync_wrapper 1.0.2", + "sync_wrapper", "tower-layer", "tower-service", - "tracing", ] [[package]] name = "axum-macros" -version = "0.4.2" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57d123550fa8d071b7255cb0cc04dc302baa6c8c4a79f55701552684d8399bce" +checksum = "604fde5e028fea851ce1d8570bbdc034bec850d157f7569d10f347d06808c05c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", -] - -[[package]] -name = "axum-server" -version = "0.6.1" -source = "git+https://github.com/bmwill/axum-server.git?rev=f44323e271afdd1365fd0c8b0a4c0bbdf4956cb7#f44323e271afdd1365fd0c8b0a4c0bbdf4956cb7" -dependencies = [ - "arc-swap", - "bytes", - "futures-util", - "http 1.3.1", - "http-body 1.0.1", - "http-body-util", - "hyper 1.6.0", - "hyper-util", - "pin-project-lite", - "rustls", - "rustls-pemfile", - "rustls-pki-types", - "tokio", - "tokio-rustls", - "tower 0.4.13", - "tower-service", + "syn 2.0.114", ] [[package]] @@ -725,7 +791,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b62ddb9cb1ec0a098ad4bbf9344d0713fa193ae1a80af55febcff2627b6a00c1" dependencies = [ "futures-core", - "getrandom 0.2.15", + "getrandom 0.2.17", "instant", "pin-project-lite", "rand 0.8.5", @@ -734,17 +800,17 @@ dependencies = [ [[package]] name = "backtrace" -version = "0.3.74" +version = "0.3.76" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a" +checksum = "bb531853791a215d7c62a30daf0dde835f381ab5de4589cfe7c649d2cbe92bd6" dependencies = [ "addr2line", "cfg-if", "libc", "miniz_oxide", - "object", + "object 0.37.3", "rustc-demangle", - "windows-targets 0.52.6", + "windows-link", ] [[package]] @@ -765,6 +831,16 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" +[[package]] +name = "base256emoji" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5e9430d9a245a77c92176e649af6e275f20839a48389859d1661e9a128d077c" +dependencies = [ + "const-str", + "match-lookup", +] + [[package]] name = "base64" version = "0.21.7" @@ -788,9 +864,9 @@ dependencies = [ [[package]] name = "base64ct" -version = "1.7.3" +version = "1.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89e25b6adfb930f02d1981565a6e5d9c547ac15a96606256d3b59040e5cd4ca3" +checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" [[package]] name = "bb8" @@ -804,6 +880,16 @@ dependencies = [ "tokio", ] +[[package]] +name = "bcder" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f7c42c9913f68cf9390a225e81ad56a5c515347287eb98baa710090ca1de86d" +dependencies = [ + "bytes", + "smallvec", +] + [[package]] name = "bcs" version = "0.1.6" @@ -820,6 +906,12 @@ version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d86b93f97252c47b41663388e6d155714a9d0c398b99f1005cbc5f978b29f445" +[[package]] +name = "beef" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a8241f3ebb85c056b509d4327ad0358fbbba6ffb340bf388f26350aeda225b1" + [[package]] name = "bellpepper" version = "0.4.1" @@ -866,15 +958,16 @@ dependencies = [ [[package]] name = "bigdecimal" -version = "0.4.7" +version = "0.4.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f31f3af01c5c65a07985c804d3366560e6fa7883d640a122819b14ec327482c" +checksum = "4d6867f1565b3aad85681f1015055b087fcfd840d6aeee6eee7f2da317603695" dependencies = [ "autocfg", "libm", "num-bigint 0.4.6", "num-integer", "num-traits", + "serde", ] [[package]] @@ -888,23 +981,40 @@ dependencies = [ [[package]] name = "bindgen" -version = "0.65.1" +version = "0.69.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfdf7b466f9a4903edc73f95d6d2bcd5baf8ae620638762244d3f60143643cc5" +checksum = "271383c67ccabffb7381723dea0672a673f292304fcb45c01cc648c7a8d58088" dependencies = [ - "bitflags 1.3.2", + "bitflags 2.10.0", "cexpr", "clang-sys", + "itertools 0.12.1", "lazy_static", "lazycell", - "peeking_take_while", - "prettyplease", "proc-macro2", "quote", "regex", "rustc-hash 1.1.0", "shlex", - "syn 2.0.100", + "syn 2.0.114", +] + +[[package]] +name = "bindgen" +version = "0.72.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895" +dependencies = [ + "bitflags 2.10.0", + "cexpr", + "clang-sys", + "itertools 0.13.0", + "proc-macro2", + "quote", + "regex", + "rustc-hash 2.1.1", + "shlex", + "syn 2.0.114", ] [[package]] @@ -915,25 +1025,40 @@ checksum = "b30ed1d6f8437a487a266c8293aeb95b61a23261273e3e02912cdb8b68bf798b" dependencies = [ "bs58 0.4.0", "hmac", - "k256", + "k256 0.11.6", "once_cell", "pbkdf2", "rand_core 0.6.4", "ripemd", - "sha2 0.10.8", + "sha2 0.10.9", "subtle", "zeroize", ] +[[package]] +name = "bit-set" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0700ddab506f33b20a03b13996eccd309a48e5ff77d0d95926aa0210fb4e95f1" +dependencies = [ + "bit-vec 0.6.3", +] + [[package]] name = "bit-set" version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" dependencies = [ - "bit-vec", + "bit-vec 0.8.0", ] +[[package]] +name = "bit-vec" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" + [[package]] name = "bit-vec" version = "0.8.0" @@ -963,11 +1088,11 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.9.0" +version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd" +checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" dependencies = [ - "serde", + "serde_core", ] [[package]] @@ -1014,9 +1139,9 @@ dependencies = [ [[package]] name = "blake2b_simd" -version = "1.0.3" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06e903a20b159e944f91ec8499fe1e55651480c541ea0a584f5d967c49ad9d99" +checksum = "b79834656f71332577234b50bfc009996f7449e0c056884e6a02492ded0ca2f3" dependencies = [ "arrayref", "arrayvec", @@ -1025,9 +1150,9 @@ dependencies = [ [[package]] name = "blake2s_simd" -version = "1.0.3" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e90f7deecfac93095eb874a40febd69427776e24e1bd7f87f33ac62d6f0174df" +checksum = "ee29928bad1e3f94c9d1528da29e07a1d3d04817ae8332de1e8b846c8439f4b3" dependencies = [ "arrayref", "arrayvec", @@ -1063,9 +1188,9 @@ dependencies = [ [[package]] name = "blst" -version = "0.3.14" +version = "0.3.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47c79a94619fade3c0b887670333513a67ac28a6a7e653eb260bf0d4103db38d" +checksum = "dcdb4c7013139a150f9fc55d123186dbfaba0d912817466282c73ac49e71fb45" dependencies = [ "cc", "glob", @@ -1095,11 +1220,17 @@ version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f781dba93de3a5ef6dc5b17c9958b208f6f3f021623b360fb605ea51ce443f10" +[[package]] +name = "bnum" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "119771309b95163ec7aaf79810da82f7cd0599c19722d48b9c03894dca833966" + [[package]] name = "brotli" -version = "7.0.0" +version = "8.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc97b8f16f944bba54f0433f07e30be199b6dc2bd25937444bbad560bcea29bd" +checksum = "4bd8b9603c7aa97359dbd97ecf258968c95f3adddd6db2f7e7a5bef101c84560" dependencies = [ "alloc-no-stdlib", "alloc-stdlib", @@ -1108,9 +1239,9 @@ dependencies = [ [[package]] name = "brotli-decompressor" -version = "4.0.2" +version = "5.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74fa05ad7d803d413eb8380983b092cbbaf9a85f151b871360e7b00cd7060b37" +checksum = "874bb8112abecc98cbd6d81ea4fa7e94fb9449648c93cc89aa40c81c24d7de03" dependencies = [ "alloc-no-stdlib", "alloc-stdlib", @@ -1136,9 +1267,9 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.17.0" +version = "3.19.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf" +checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510" [[package]] name = "byte-slice-cast" @@ -1148,15 +1279,15 @@ checksum = "7575182f7272186991736b70173b0ea045398f984bf5ebbb3804736ce1330c9d" [[package]] name = "bytecount" -version = "0.6.8" +version = "0.6.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ce89b21cab1437276d2650d57e971f9d548a2d9037cc231abdc0562b97498ce" +checksum = "175812e0be2bccb6abe50bb8d566126198344f707e304f45c648fd8f2cc0365e" [[package]] name = "bytemuck" -version = "1.22.0" +version = "1.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6b1fc10dbac614ebc03540c9dbd60e83887fda27794998c6528f1782047d540" +checksum = "1fbdf580320f38b612e485521afda1ee26d10cc9884efaaa750d383e13e3c5f4" [[package]] name = "byteorder" @@ -1166,9 +1297,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.10.1" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" +checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3" dependencies = [ "serde", ] @@ -1182,6 +1313,15 @@ dependencies = [ "bytes", ] +[[package]] +name = "bytestring" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "113b4343b5f6617e7ad401ced8de3cc8b012e73a594347c307b90db3e9271289" +dependencies = [ + "bytes", +] + [[package]] name = "bzip2-sys" version = "0.1.13+1.0.8" @@ -1203,10 +1343,11 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.16" +version = "1.2.52" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be714c154be609ec7f5dad223a33bf1482fff90472de28f7362806e6d4832b8c" +checksum = "cd4932aefd12402b36c60956a4fe0035421f544799057659ff86f923657aada3" dependencies = [ + "find-msvc-tools", "jobserver", "libc", "shlex", @@ -1229,9 +1370,15 @@ dependencies = [ [[package]] name = "cfg-if" -version = "1.0.0" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "cfg_aliases" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd16c4719339c4530435d38e511904438d07cce7950afa3718a84ac36c10e89e" [[package]] name = "cfg_aliases" @@ -1241,17 +1388,16 @@ checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" [[package]] name = "chrono" -version = "0.4.39" +version = "0.4.43" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e36cc9d416881d2e24f9a963be5fb1cd90966419ac844274161d10488b3e825" +checksum = "fac4744fb15ae8337dc853fee7fb3f4e48c0fbaa23d0afe49c447b4fab126118" dependencies = [ - "android-tzdata", "iana-time-zone", "js-sys", "num-traits", "serde", "wasm-bindgen", - "windows-targets 0.52.6", + "windows-link", ] [[package]] @@ -1304,9 +1450,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.32" +version = "4.5.54" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6088f3ae8c3608d19260cd7445411865a485688711b78b5be70d78cd96136f83" +checksum = "c6e6ff9dcd79cff5cd969a17a545d79e84ab086e444102a591e288a8aa3ce394" dependencies = [ "clap_builder", "clap_derive", @@ -1314,9 +1460,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.32" +version = "4.5.54" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22a7ef7f676155edfb82daa97f99441f3ebf4a58d5e32f295a56259f1b6facc8" +checksum = "fa42cf4d2b7a41bc8f663a7cab4031ebafa1bf3875705bfaf8466dc60ab52c00" dependencies = [ "anstream", "anstyle", @@ -1327,21 +1473,36 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.5.32" +version = "4.5.49" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09176aae279615badda0765c0c0b3f6ed53f4709118af73cf4655d85d1530cd7" +checksum = "2a0b5487afeab2deb2ff4e03a807ad1a03ac532ff5a2cee5d86884440c7f7671" dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.114", ] [[package]] name = "clap_lex" -version = "0.7.4" +version = "0.7.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" +checksum = "c3e64b0cc0439b12df2fa678eae89a1c56a529fd067a9115f7827f1fffd22b32" + +[[package]] +name = "clipboard-win" +version = "5.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bde03770d3df201d4fb868f2c9c59e66a3e4e2bd06692a0fe701e7103c7e84d4" +dependencies = [ + "error-code", +] + +[[package]] +name = "cmp_any" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9b18233253483ce2f65329a24072ec414db782531bdbb7d0bbc4bd2ce6b7e21" [[package]] name = "codespan" @@ -1372,9 +1533,9 @@ checksum = "08abddbaad209601e53c7dd4308d8c04c06f17bb7df006434e586a22b83be45a" [[package]] name = "colorchoice" -version = "1.0.3" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" +checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" [[package]] name = "colored" @@ -1396,6 +1557,26 @@ dependencies = [ "memchr", ] +[[package]] +name = "compression-codecs" +version = "0.4.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00828ba6fd27b45a448e57dbfe84f1029d4c9f26b368157e9a448a5f49a2ec2a" +dependencies = [ + "brotli", + "compression-core", + "flate2", + "memchr", + "zstd 0.13.3", + "zstd-safe 7.2.4", +] + +[[package]] +name = "compression-core" +version = "0.4.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75984efb6ed102a0d42db99afb6c1948f0380d1d91808d5529916e6c08b49d8d" + [[package]] name = "concurrent-queue" version = "2.5.0" @@ -1408,15 +1589,26 @@ dependencies = [ [[package]] name = "consensus-config" version = "0.1.0" -source = "git+https://github.com/MystenLabs/sui.git?rev=88ba4e08e96ba1ab965c11ce1a915331dd3ed68d#88ba4e08e96ba1ab965c11ce1a915331dd3ed68d" +source = "git+https://github.com/MystenLabs/sui.git?branch=testnet#caeb21b13ef6e26cd8c5e48be520bcf0598d48c3" dependencies = [ - "fastcrypto 0.1.8", + "fastcrypto", "mysten-network", "rand 0.8.5", "serde", "shared-crypto", ] +[[package]] +name = "consensus-types" +version = "0.1.0" +source = "git+https://github.com/MystenLabs/sui.git?branch=testnet#caeb21b13ef6e26cd8c5e48be520bcf0598d48c3" +dependencies = [ + "base64 0.21.7", + "consensus-config", + "fastcrypto", + "serde", +] + [[package]] name = "console" version = "0.15.11" @@ -1426,28 +1618,40 @@ dependencies = [ "encode_unicode", "libc", "once_cell", - "unicode-width 0.2.0", "windows-sys 0.59.0", ] [[package]] -name = "console-api" -version = "0.6.0" +name = "console" +version = "0.16.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03e45a4a8926227e4197636ba97a9fc9b00477e9f4bd711395687c5f0734bec4" +dependencies = [ + "encode_unicode", + "libc", + "once_cell", + "unicode-width 0.2.2", + "windows-sys 0.61.2", +] + +[[package]] +name = "console-api" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd326812b3fd01da5bb1af7d340d0d555fd3d4b641e7f1dfcf5962a902952787" +checksum = "8030735ecb0d128428b64cd379809817e620a40e5001c54465b99ec5feec2857" dependencies = [ "futures-core", - "prost 0.12.6", - "prost-types 0.12.6", - "tonic 0.10.2", + "prost 0.13.5", + "prost-types 0.13.5", + "tonic 0.12.3", "tracing-core", ] [[package]] name = "console-subscriber" -version = "0.2.0" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7481d4c57092cd1c19dd541b92bdce883de840df30aa5d03fd48a3935c01842e" +checksum = "6539aa9c6a4cd31f4b1c040f860a1eac9aa80e7df6b05d506a6e7179936d6a01" dependencies = [ "console-api", "crossbeam-channel", @@ -1455,13 +1659,15 @@ dependencies = [ "futures-task", "hdrhistogram", "humantime", - "prost-types 0.12.6", + "hyper-util", + "prost 0.13.5", + "prost-types 0.13.5", "serde", "serde_json", "thread_local", "tokio", "tokio-stream", - "tonic 0.10.2", + "tonic 0.12.3", "tracing", "tracing-core", "tracing-subscriber", @@ -1473,11 +1679,17 @@ version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" +[[package]] +name = "const-str" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f421161cb492475f1661ddc9815a745a1c894592070661180fdec3d4872e9c3" + [[package]] name = "constant_time_eq" -version = "0.3.1" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c74b8349d32d297c9134b8c88677813a227df8f779daa29bfc29c183fe3dca6" +checksum = "3d52eff69cd5e647efe296129160853a42795992097e8af39800e1060caeea9b" [[package]] name = "convert_case" @@ -1496,19 +1708,9 @@ dependencies = [ [[package]] name = "core-foundation" -version = "0.9.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" -dependencies = [ - "core-foundation-sys", - "libc", -] - -[[package]] -name = "core-foundation" -version = "0.10.0" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b55271e5c8c478ad3f38ad24ef34923091e0548492a266d19b3c0b4d82574c63" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" dependencies = [ "core-foundation-sys", "libc", @@ -1550,9 +1752,9 @@ dependencies = [ [[package]] name = "crc" -version = "3.2.1" +version = "3.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69e6e4d7b33a94f0991c26729976b10ebde1d34c3ee82408fb536164fa10d636" +checksum = "5eb8a2a1cd12ab0d987a5d5e825195d372001a4094a0376319d5a0ad71c1ba0d" dependencies = [ "crc-catalog", ] @@ -1565,18 +1767,18 @@ checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" [[package]] name = "crc32fast" -version = "1.4.2" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" dependencies = [ "cfg-if", ] [[package]] name = "crossbeam-channel" -version = "0.5.14" +version = "0.5.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06ba6d68e24814cb8de6bb986db8222d3a027d15872cabc0d18817bc3c0e4471" +checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" dependencies = [ "crossbeam-utils", ] @@ -1642,9 +1844,9 @@ dependencies = [ [[package]] name = "crunchy" -version = "0.2.3" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43da5946c66ffcc7745f48db692ffbb10a83bfe0afd96235c5c2a4fb23994929" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" [[package]] name = "crypto-bigint" @@ -1672,9 +1874,9 @@ dependencies = [ [[package]] name = "crypto-common" -version = "0.1.6" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" dependencies = [ "generic-array", "rand_core 0.6.4", @@ -1683,25 +1885,35 @@ dependencies = [ [[package]] name = "csv" -version = "1.3.1" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "acdc4883a9c96732e4733212c01447ebd805833b7275a73ca3ee080fd77afdaf" +checksum = "52cd9d68cf7efc6ddfaaee42e7288d3a99d613d4b50f76ce9827ae0c6e14f938" dependencies = [ "csv-core", "itoa", "ryu", - "serde", + "serde_core", ] [[package]] name = "csv-core" -version = "0.1.12" +version = "0.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d02f3b0da4c6504f86e9cd789d8dbafab48c2321be74e9987593de5a894d93d" +checksum = "704a3c26996a80471189265814dbc2c257598b96b8a7feae2d31ace646bb9782" dependencies = [ "memchr", ] +[[package]] +name = "ctor" +version = "0.1.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d2301688392eb071b0bf1a37be05c469d3cc4dbbd95df672fe28ab021e6a096" +dependencies = [ + "quote", + "syn 1.0.109", +] + [[package]] name = "ctr" version = "0.9.2" @@ -1711,6 +1923,33 @@ dependencies = [ "cipher", ] +[[package]] +name = "curve25519-dalek" +version = "4.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" +dependencies = [ + "cfg-if", + "cpufeatures", + "curve25519-dalek-derive", + "digest 0.10.7", + "fiat-crypto", + "rustc_version", + "subtle", + "zeroize", +] + +[[package]] +name = "curve25519-dalek-derive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + [[package]] name = "curve25519-dalek-ng" version = "4.1.1" @@ -1736,12 +1975,22 @@ dependencies = [ [[package]] name = "darling" -version = "0.20.10" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" +dependencies = [ + "darling_core 0.20.11", + "darling_macro 0.20.11", +] + +[[package]] +name = "darling" +version = "0.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f63b86c8a8826a49b8c21f08a2d07338eec8d900540f8630dc76284be802989" +checksum = "9cdf337090841a411e2a7f3deb9187445851f91b309c0c0a29e05f74a00a48c0" dependencies = [ - "darling_core 0.20.10", - "darling_macro 0.20.10", + "darling_core 0.21.3", + "darling_macro 0.21.3", ] [[package]] @@ -1760,16 +2009,30 @@ dependencies = [ [[package]] name = "darling_core" -version = "0.20.10" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim 0.11.1", + "syn 2.0.114", +] + +[[package]] +name = "darling_core" +version = "0.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95133861a8032aaea082871032f5815eb9e98cef03fa916ab4500513994df9e5" +checksum = "1247195ecd7e3c85f83c8d2a366e4210d588e802133e1e355180a9870b517ea4" dependencies = [ "fnv", "ident_case", "proc-macro2", "quote", "strsim 0.11.1", - "syn 2.0.100", + "syn 2.0.114", ] [[package]] @@ -1785,13 +2048,24 @@ dependencies = [ [[package]] name = "darling_macro" -version = "0.20.10" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" +dependencies = [ + "darling_core 0.20.11", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "darling_macro" +version = "0.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806" +checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81" dependencies = [ - "darling_core 0.20.10", + "darling_core 0.21.3", "quote", - "syn 2.0.100", + "syn 2.0.114", ] [[package]] @@ -1809,15 +2083,15 @@ dependencies = [ [[package]] name = "data-encoding" -version = "2.8.0" +version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "575f75dfd25738df5b91b8e43e14d44bda14637a58fae779fd2b064f8bf3e010" +checksum = "d7a1e2f27636f116493b8b860f5546edb47c8d8f8ea73e1d2a20be88e28d1fea" [[package]] name = "data-encoding-macro" -version = "0.1.17" +version = "0.1.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f9724adfcf41f45bf652b3995837669d73c4d49a1b5ac1ff82905ac7d9b5558" +checksum = "8142a83c17aa9461d637e649271eae18bf2edd00e91f2e105df36c3c16355bdb" dependencies = [ "data-encoding", "data-encoding-macro-internal", @@ -1825,12 +2099,23 @@ dependencies = [ [[package]] name = "data-encoding-macro-internal" -version = "0.1.15" +version = "0.1.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18e4fdb82bd54a12e42fb58a800dcae6b9e13982238ce2296dc3570b92148e1f" +checksum = "7ab67060fc6b8ef687992d439ca0fa36e7ed17e9a0b16b25b601e8757df720de" dependencies = [ "data-encoding", - "syn 2.0.100", + "syn 2.0.114", +] + +[[package]] +name = "debugserver-types" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bf6834a70ed14e8e4e41882df27190bea150f1f6ecf461f1033f8739cd8af4a" +dependencies = [ + "schemafy", + "serde", + "serde_json", ] [[package]] @@ -1846,50 +2131,39 @@ dependencies = [ "deepbook-schema", "diesel", "diesel-async", - "fastcrypto 0.1.9 (git+https://github.com/MystenLabs/fastcrypto)", - "futures", + "fastcrypto", + "hex", "insta", - "itertools 0.14.0", - "move-binding-derive", - "move-core-types 0.0.4 (git+https://github.com/MystenLabs/sui.git?rev=88ba4e08e96ba1ab965c11ce1a915331dd3ed68d)", - "move-types", - "mysten-metrics", + "move-core-types", "prometheus", - "reqwest", "serde", "serde_json", "sqlx", - "sui-config", - "sui-data-ingestion-core", "sui-indexer-alt-framework", "sui-indexer-alt-metrics", "sui-pg-db", - "sui-sdk-types 0.0.2 (git+https://github.com/mystenlabs/sui-rust-sdk?rev=86a9e06)", + "sui-sdk-types 0.0.7", "sui-storage", - "sui-transaction-builder 0.1.0 (git+https://github.com/mystenlabs/sui-rust-sdk?rev=86a9e06)", + "sui-transaction-builder 0.0.7", "sui-types", "telemetry-subscribers", - "tempfile", "tokio", - "tokio-util 0.7.14", "tracing", "url", - "uuid", ] [[package]] name = "deepbook-schema" version = "0.1.0" dependencies = [ - "anyhow", + "bigdecimal", "chrono", "diesel", "diesel_migrations", - "dotenvy", "serde", "serde_json", - "strum 0.27.1", - "strum_macros 0.27.1", + "strum 0.27.2", + "strum_macros 0.27.2", "sui-field-count", ] @@ -1898,24 +2172,29 @@ name = "deepbook-server" version = "0.1.0" dependencies = [ "anyhow", - "async-trait", "axum 0.7.9", "bcs", + "bigdecimal", "chrono", "clap", "deepbook-schema", "diesel", "diesel-async", "futures", + "prometheus", "serde", "serde_json", + "sui-futures", + "sui-indexer-alt-metrics", "sui-json-rpc-types", "sui-pg-db", "sui-sdk", "sui-types", + "telemetry-subscribers", + "thiserror 1.0.69", "tokio", - "tower-http", - "tracing", + "tokio-util 0.7.18", + "tower-http 0.5.2", "url", ] @@ -1932,9 +2211,9 @@ dependencies = [ [[package]] name = "der" -version = "0.7.9" +version = "0.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f55bf8e7b65898637379c1b74eb1551107c8294ed26d855ceb9fd1a09cfc9bc0" +checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" dependencies = [ "const-oid", "pem-rfc7468 0.7.0", @@ -1957,12 +2236,12 @@ dependencies = [ [[package]] name = "deranged" -version = "0.4.0" +version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c9e6a11ca8224451684bc0d7d5a7adbf8f2fd6887261a1cfc3c0432f9d4068e" +checksum = "ececcb659e7ba858fb4f10388c250a7252eb0a27373f1a72b8748afdd248e587" dependencies = [ "powerfmt", - "serde", + "serde_core", ] [[package]] @@ -1989,15 +2268,15 @@ dependencies = [ [[package]] name = "derive_more" -version = "0.99.19" +version = "0.99.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3da29a38df43d6f156149c9b43ded5e018ddff2a855cf2cfd62e8cd7d079c69f" +checksum = "6edb4b64a43d977b8e99788fe3a04d483834fba1215a7e02caa415b626497f7f" dependencies = [ "convert_case 0.4.0", "proc-macro2", "quote", "rustc_version", - "syn 2.0.100", + "syn 2.0.114", ] [[package]] @@ -2015,20 +2294,21 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cb7330aeadfbe296029522e6c40f315320aba36fc43a5b3632f3795348f3bd22" dependencies = [ + "convert_case 0.6.0", "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.114", "unicode-xid", ] [[package]] name = "diesel" -version = "2.2.8" +version = "2.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "470eb10efc8646313634c99bb1593f402a6434cbd86e266770c6e39219adb86a" +checksum = "229850a212cd9b84d4f0290ad9d294afc0ae70fccaa8949dbe8b43ffafa1e20c" dependencies = [ "bigdecimal", - "bitflags 2.9.0", + "bitflags 2.10.0", "byteorder", "chrono", "diesel_derives", @@ -2058,15 +2338,15 @@ dependencies = [ [[package]] name = "diesel_derives" -version = "2.2.4" +version = "2.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a93958254b70bea63b4187ff73d10180599d9d8d177071b7f91e6da4e0c0ad55" +checksum = "1b96984c469425cb577bf6f17121ecb3e4fe1e81de5d8f780dd372802858d756" dependencies = [ "diesel_table_macro_syntax", "dsl_auto_type", "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.114", ] [[package]] @@ -2086,9 +2366,21 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "209c735641a413bc68c4923a9d6ad4bcb3ca306b794edaa7eb0b3228a99ffb25" dependencies = [ - "syn 2.0.100", + "syn 2.0.114", ] +[[package]] +name = "diff" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8" + +[[package]] +name = "difflib" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6184e33543162437515c2e2b48714794e37845ec9851711914eec9d308f6ebe8" + [[package]] name = "digest" version = "0.9.0" @@ -2151,6 +2443,16 @@ dependencies = [ "winapi", ] +[[package]] +name = "display_container" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a110a75c96bedec8e65823dea00a1d710288b7a369d95fd8a0f5127639466fa" +dependencies = [ + "either", + "indenter", +] + [[package]] name = "displaydoc" version = "0.2.5" @@ -2159,7 +2461,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.114", ] [[package]] @@ -2168,6 +2470,12 @@ version = "0.15.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" +[[package]] +name = "downcast" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1435fa1053d8b2fbbe9be7e97eca7f33d37b28409959813daefc1446a14247f1" + [[package]] name = "downcast-rs" version = "1.2.1" @@ -2180,12 +2488,12 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "139ae9aca7527f85f26dd76483eb38533fd84bd571065da1739656ef71c5ff5b" dependencies = [ - "darling 0.20.10", + "darling 0.20.11", "either", "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.114", ] [[package]] @@ -2194,11 +2502,31 @@ version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" +[[package]] +name = "dupe" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ed2bc011db9c93fbc2b6cdb341a53737a55bafb46dbb74cf6764fc33a2fbf9c" +dependencies = [ + "dupe_derive", +] + +[[package]] +name = "dupe_derive" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83e195b4945e88836d826124af44fdcb262ec01ef94d44f14f4fb5103f19892a" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + [[package]] name = "dyn-clone" -version = "1.0.19" +version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c7a8fb8a9fbf66c1f703fe16184d10ca0ee9d23be5b4436400408ba54a95005" +checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" [[package]] name = "ecdsa" @@ -2218,7 +2546,7 @@ version = "0.16.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca" dependencies = [ - "der 0.7.9", + "der 0.7.10", "digest 0.10.7", "elliptic-curve 0.13.8", "rfc6979 0.4.0", @@ -2252,6 +2580,20 @@ dependencies = [ "zeroize", ] +[[package]] +name = "ed25519-dalek" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70e796c081cee67dc755e1a36a0a172b897fab85fc3f6bc48307991f64e4eca9" +dependencies = [ + "curve25519-dalek", + "ed25519", + "serde", + "sha2 0.10.9", + "subtle", + "zeroize", +] + [[package]] name = "either" version = "1.15.0" @@ -2300,6 +2642,15 @@ dependencies = [ "zeroize", ] +[[package]] +name = "ena" +version = "0.14.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d248bdd43ce613d87415282f69b9bb99d947d290b10962dd6c56233312c2ad5" +dependencies = [ + "log", +] + [[package]] name = "encode_unicode" version = "1.0.0" @@ -2307,26 +2658,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" [[package]] -name = "encoding_rs" -version = "0.8.35" +name = "endian-type" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "enum-compat-util" -version = "0.1.0" -source = "git+https://github.com/MystenLabs/sui.git?rev=42ba6c0#42ba6c03128233cdeb3fc6e0a22dabd0bfc55385" -dependencies = [ - "serde_yaml", -] +checksum = "c34f04666d835ff5d62e058c3995147c06f42fe86ff053337632bca83e42702d" [[package]] name = "enum-compat-util" version = "0.1.0" -source = "git+https://github.com/MystenLabs/sui.git?rev=88ba4e08e96ba1ab965c11ce1a915331dd3ed68d#88ba4e08e96ba1ab965c11ce1a915331dd3ed68d" +source = "git+https://github.com/MystenLabs/sui.git?branch=testnet#caeb21b13ef6e26cd8c5e48be520bcf0598d48c3" dependencies = [ "serde_yaml", ] @@ -2340,7 +2680,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.114", ] [[package]] @@ -2368,16 +2708,31 @@ dependencies = [ "typeid", ] +[[package]] +name = "erased-serde" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c138974f9d5e7fe373eb04df7cae98833802ae4b11c24ac7039a21d5af4b26c" +dependencies = [ + "serde", +] + [[package]] name = "errno" -version = "0.3.10" +version = "0.3.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] +[[package]] +name = "error-code" +version = "3.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dea2df4cf52843e0452895c455a1a2cfbb842a1e7329671acf418fdc53ed4c59" + [[package]] name = "etcetera" version = "0.8.0" @@ -2391,15 +2746,15 @@ dependencies = [ [[package]] name = "ethnum" -version = "1.5.0" +version = "1.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b90ca2580b73ab6a1f724b76ca11ab632df820fd6040c336200d2c1df7b3c82c" +checksum = "ca81e6b4777c89fd810c25a4be2b1bd93ea034fbe58e6a75216a34c6b82c539b" [[package]] name = "event-listener" -version = "5.4.0" +version = "5.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3492acde4c3fc54c845eaab3eed8bd00c7a7d881f78bfc801e43a93dec1331ae" +checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab" dependencies = [ "concurrent-queue", "parking", @@ -2422,19 +2777,34 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4443176a9f2c162692bd3d352d745ef9413eec5782a80d8fd6f8a1ac692a07f7" +[[package]] +name = "fastbloom" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7f34442dbe69c60fe8eaf58a8cafff81a1f278816d8ab4db255b3bef4ac3c4" +dependencies = [ + "getrandom 0.3.4", + "libm", + "rand 0.9.2", + "siphasher", +] + [[package]] name = "fastcrypto" -version = "0.1.8" -source = "git+https://github.com/MystenLabs/fastcrypto?rev=69d496c71fb37e3d22fe85e5bbfd4256d61422b9#69d496c71fb37e3d22fe85e5bbfd4256d61422b9" +version = "0.1.9" +source = "git+https://github.com/MystenLabs/fastcrypto?rev=4db0e90c732bbf7420ca20de808b698883148d9c#4db0e90c732bbf7420ca20de808b698883148d9c" dependencies = [ "aes", "aes-gcm", + "aes-gcm-siv", "ark-ec", "ark-ff", + "ark-secp256k1", "ark-secp256r1", "ark-serialize", "auto_ops", "base64ct", + "bcs", "bech32", "bincode", "blake2", @@ -2443,12 +2813,12 @@ dependencies = [ "cbc", "ctr", "curve25519-dalek-ng", - "derive_more 0.99.19", + "derive_more 0.99.20", "digest 0.10.7", "ecdsa 0.16.9", "ed25519-consensus", "elliptic-curve 0.13.8", - "fastcrypto-derive 0.1.3 (git+https://github.com/MystenLabs/fastcrypto?rev=69d496c71fb37e3d22fe85e5bbfd4256d61422b9)", + "fastcrypto-derive", "generic-array", "hex", "hex-literal", @@ -2461,12 +2831,12 @@ dependencies = [ "readonly", "rfc6979 0.4.0", "rsa 0.8.2", - "schemars", + "schemars 0.8.22", "secp256k1", "serde", "serde_json", "serde_with", - "sha2 0.10.8", + "sha2 0.10.9", "sha3", "signature 2.2.0", "static_assertions", @@ -2477,148 +2847,27 @@ dependencies = [ ] [[package]] -name = "fastcrypto" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e06674cac3bf7ec9a951971285e6051a45273dc4e265cca27c02a0d4ebcb46f8" +name = "fastcrypto-derive" +version = "0.1.3" +source = "git+https://github.com/MystenLabs/fastcrypto?rev=4db0e90c732bbf7420ca20de808b698883148d9c#4db0e90c732bbf7420ca20de808b698883148d9c" dependencies = [ - "ark-ec", - "ark-ff", - "ark-secp256r1", - "ark-serialize", - "auto_ops", - "base64ct", - "bech32", - "bincode", - "blake2", - "blst", - "bs58 0.4.0", - "curve25519-dalek-ng", - "derive_more 0.99.19", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "fastcrypto-tbls" +version = "0.1.0" +source = "git+https://github.com/MystenLabs/fastcrypto?rev=4db0e90c732bbf7420ca20de808b698883148d9c#4db0e90c732bbf7420ca20de808b698883148d9c" +dependencies = [ + "bcs", "digest 0.10.7", - "ecdsa 0.16.9", - "ed25519-consensus", - "elliptic-curve 0.13.8", - "fastcrypto-derive 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)", - "generic-array", + "fastcrypto", "hex", - "hex-literal", - "hkdf", - "lazy_static", - "num-bigint 0.4.6", - "once_cell", - "p256", - "rand 0.8.5", - "readonly", - "rfc6979 0.4.0", - "rsa 0.8.2", - "schemars", - "secp256k1", - "serde", - "serde_json", - "serde_with", - "sha2 0.10.8", - "sha3", - "signature 2.2.0", - "static_assertions", - "thiserror 1.0.69", - "tokio", - "typenum", - "zeroize", -] - -[[package]] -name = "fastcrypto" -version = "0.1.9" -source = "git+https://github.com/MystenLabs/fastcrypto#0acf0ff1a163c60e0dec1e16e4fbad4a4cf853bd" -dependencies = [ - "ark-ec", - "ark-ff", - "ark-secp256r1", - "ark-serialize", - "auto_ops", - "base64ct", - "bech32", - "bincode", - "blake2", - "blst", - "bs58 0.4.0", - "curve25519-dalek-ng", - "derive_more 0.99.19", - "digest 0.10.7", - "ecdsa 0.16.9", - "ed25519-consensus", - "elliptic-curve 0.13.8", - "fastcrypto-derive 0.1.3 (git+https://github.com/MystenLabs/fastcrypto)", - "generic-array", - "hex", - "hex-literal", - "hkdf", - "lazy_static", - "num-bigint 0.4.6", - "once_cell", - "p256", - "rand 0.8.5", - "readonly", - "rfc6979 0.4.0", - "rsa 0.8.2", - "schemars", - "secp256k1", - "serde", - "serde_json", - "serde_with", - "sha2 0.10.8", - "sha3", - "signature 2.2.0", - "static_assertions", - "thiserror 1.0.69", - "tokio", - "typenum", - "zeroize", -] - -[[package]] -name = "fastcrypto-derive" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c0c2af2157f416cb885e11d36cd0de2753f6d5384752d364075c835f5f8f891" -dependencies = [ - "convert_case 0.6.0", - "proc-macro2", - "quote", - "syn 1.0.109", -] - -[[package]] -name = "fastcrypto-derive" -version = "0.1.3" -source = "git+https://github.com/MystenLabs/fastcrypto?rev=69d496c71fb37e3d22fe85e5bbfd4256d61422b9#69d496c71fb37e3d22fe85e5bbfd4256d61422b9" -dependencies = [ - "quote", - "syn 1.0.109", -] - -[[package]] -name = "fastcrypto-derive" -version = "0.1.3" -source = "git+https://github.com/MystenLabs/fastcrypto#0acf0ff1a163c60e0dec1e16e4fbad4a4cf853bd" -dependencies = [ - "quote", - "syn 1.0.109", -] - -[[package]] -name = "fastcrypto-tbls" -version = "0.1.0" -source = "git+https://github.com/MystenLabs/fastcrypto?rev=69d496c71fb37e3d22fe85e5bbfd4256d61422b9#69d496c71fb37e3d22fe85e5bbfd4256d61422b9" -dependencies = [ - "bcs", - "digest 0.10.7", - "fastcrypto 0.1.8", - "hex", - "itertools 0.10.5", + "itertools 0.10.5", "rand 0.8.5", "serde", + "serde-big-array", "sha3", "tap", "tracing", @@ -2629,7 +2878,7 @@ dependencies = [ [[package]] name = "fastcrypto-zkp" version = "0.1.3" -source = "git+https://github.com/MystenLabs/fastcrypto?rev=69d496c71fb37e3d22fe85e5bbfd4256d61422b9#69d496c71fb37e3d22fe85e5bbfd4256d61422b9" +source = "git+https://github.com/MystenLabs/fastcrypto?rev=4db0e90c732bbf7420ca20de808b698883148d9c#4db0e90c732bbf7420ca20de808b698883148d9c" dependencies = [ "ark-bn254", "ark-ec", @@ -2640,8 +2889,8 @@ dependencies = [ "ark-snark", "bcs", "byte-slice-cast", - "derive_more 0.99.19", - "fastcrypto 0.1.8", + "derive_more 0.99.20", + "fastcrypto", "ff 0.13.1", "im", "itertools 0.12.1", @@ -2651,7 +2900,7 @@ dependencies = [ "once_cell", "regex", "reqwest", - "schemars", + "schemars 0.8.22", "serde", "serde_json", "typenum", @@ -2663,6 +2912,17 @@ version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" +[[package]] +name = "fd-lock" +version = "4.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ce92ff622d6dadf7349484f42c93271a0d49b7cc4d466a936405bacbe10aa78" +dependencies = [ + "cfg-if", + "rustix", + "windows-sys 0.59.0", +] + [[package]] name = "fdlimit" version = "0.2.1" @@ -2711,16 +2971,16 @@ dependencies = [ ] [[package]] -name = "filetime" -version = "0.2.25" +name = "fiat-crypto" +version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35c0522e981e68cbfa8c3f978441a5f34b30b96e146b33cd3359176b50fe8586" -dependencies = [ - "cfg-if", - "libc", - "libredox", - "windows-sys 0.59.0", -] +checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" + +[[package]] +name = "find-msvc-tools" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f449e6c6c08c865631d4890cfacf252b3d396c9bcc83adb6623cdb02a8336c41" [[package]] name = "fixed-hash" @@ -2736,20 +2996,35 @@ dependencies = [ [[package]] name = "fixedbitset" -version = "0.2.0" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37ab347416e802de484e4d03c7316c48f1ecb56574dfd4a46a80f173ce1de04d" +checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" + +[[package]] +name = "fixedbitset" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99" [[package]] name = "flate2" -version = "1.1.0" +version = "1.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11faaf5a5236997af9848be0bef4db95824b1d534ebc64d0f0c6cf3e67bd38dc" +checksum = "b375d6465b98090a5f25b1c7703f3859783755aa9a80433b36e0379a3ec2f369" dependencies = [ "crc32fast", "miniz_oxide", ] +[[package]] +name = "float-cmp" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98de4bbd547a563b716d8dfa9aad1cb19bfab00f4fa09a6a4ed21dbcf44ce9c4" +dependencies = [ + "num-traits", +] + [[package]] name = "flume" version = "0.11.1" @@ -2773,38 +3048,20 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" -[[package]] -name = "foreign-types" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" -dependencies = [ - "foreign-types-shared", -] - -[[package]] -name = "foreign-types-shared" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" - [[package]] name = "form_urlencoded" -version = "1.2.1" +version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" dependencies = [ "percent-encoding", ] [[package]] -name = "fsevent-sys" -version = "4.1.0" +name = "fragile" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76ee7a02da4d231650c7cea31349b889be2f45ddb3ef3032d2ec8185f6313fd2" -dependencies = [ - "libc", -] +checksum = "28dd6caf6059519a65843af8fe2a3ae298b14b80179855aeb4adc2c1934ee619" [[package]] name = "funty" @@ -2885,7 +3142,7 @@ checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.114", ] [[package]] @@ -2925,16 +3182,12 @@ dependencies = [ ] [[package]] -name = "generator" -version = "0.8.4" +name = "fxhash" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc6bd114ceda131d3b1d665eba35788690ad37f5916457286b32ab6fd3c438dd" +checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c" dependencies = [ - "cfg-if", - "libc", - "log", - "rustversion", - "windows", + "byteorder", ] [[package]] @@ -2951,28 +3204,28 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.15" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" dependencies = [ "cfg-if", "js-sys", "libc", - "wasi 0.11.0+wasi-snapshot-preview1", + "wasi 0.11.1+wasi-snapshot-preview1", "wasm-bindgen", ] [[package]] name = "getrandom" -version = "0.3.2" +version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73fea8450eea4bac3940448fb7ae50d91f034f941199fcd9d909a5a07aa455f0" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" dependencies = [ "cfg-if", "js-sys", "libc", "r-efi", - "wasi 0.14.2+wasi-0.2.4", + "wasip2", "wasm-bindgen", ] @@ -2988,15 +3241,15 @@ dependencies = [ [[package]] name = "gimli" -version = "0.31.1" +version = "0.32.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" +checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7" [[package]] name = "glob" -version = "0.3.2" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2" +checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" [[package]] name = "governor" @@ -3038,56 +3291,38 @@ dependencies = [ "ff 0.13.1", "rand 0.8.5", "rand_core 0.6.4", - "rand_xorshift", + "rand_xorshift 0.3.0", "subtle", ] [[package]] name = "h2" -version = "0.3.26" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81fe527a889e1532da5c525686d96d4c2e74cdd345badf8dfef9f6b39dd5f5e8" -dependencies = [ - "bytes", - "fnv", - "futures-core", - "futures-sink", - "futures-util", - "http 0.2.12", - "indexmap 2.8.0", - "slab", - "tokio", - "tokio-util 0.7.14", - "tracing", -] - -[[package]] -name = "h2" -version = "0.4.8" +version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5017294ff4bb30944501348f6f8e42e6ad28f42c8bbef7a74029aff064a4e3c2" +checksum = "2f44da3a8150a6703ed5d34e164b875fd14c2cdab9af1252a9a1020bde2bdc54" dependencies = [ "atomic-waker", "bytes", "fnv", "futures-core", "futures-sink", - "http 1.3.1", - "indexmap 2.8.0", + "http", + "indexmap 2.13.0", "slab", "tokio", - "tokio-util 0.7.14", + "tokio-util 0.7.18", "tracing", ] [[package]] name = "half" -version = "2.5.0" +version = "2.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7db2ff139bba50379da6aa0766b52fdcb62cb5b263009b09ed58ba604e14bbd1" +checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b" dependencies = [ "cfg-if", "crunchy", + "zerocopy", ] [[package]] @@ -3102,7 +3337,7 @@ version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "43a3c133739dddd0d2990f9a4bdf8eb4b21ef50e4851ca85ab661199821d510e" dependencies = [ - "ahash 0.8.11", + "ahash 0.8.12", ] [[package]] @@ -3110,25 +3345,35 @@ name = "hashbrown" version = "0.14.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" +dependencies = [ + "ahash 0.8.12", + "allocator-api2", +] [[package]] name = "hashbrown" -version = "0.15.2" +version = "0.15.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" dependencies = [ "allocator-api2", "equivalent", "foldhash", ] +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" + [[package]] name = "hashlink" version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1" dependencies = [ - "hashbrown 0.15.2", + "hashbrown 0.15.5", ] [[package]] @@ -3159,9 +3404,9 @@ checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" [[package]] name = "hermit-abi" -version = "0.3.9" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" +checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" [[package]] name = "hex" @@ -3204,46 +3449,23 @@ checksum = "77e806677ce663d0a199541030c816847b36e8dc095f70dae4a4f4ad63da5383" [[package]] name = "home" -version = "0.5.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "589533453244b0995c858700322199b2becb13b627df2851f64a2775d024abcf" -dependencies = [ - "windows-sys 0.59.0", -] - -[[package]] -name = "http" -version = "0.2.12" +version = "0.5.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1" +checksum = "cc627f471c528ff0c4a49e1d5e60450c8f6461dd6d10ba9dcd3a61d3dff7728d" dependencies = [ - "bytes", - "fnv", - "itoa", + "windows-sys 0.61.2", ] [[package]] name = "http" -version = "1.3.1" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565" +checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" dependencies = [ "bytes", - "fnv", "itoa", ] -[[package]] -name = "http-body" -version = "0.4.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" -dependencies = [ - "bytes", - "http 0.2.12", - "pin-project-lite", -] - [[package]] name = "http-body" version = "1.0.1" @@ -3251,7 +3473,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" dependencies = [ "bytes", - "http 1.3.1", + "http", ] [[package]] @@ -3262,8 +3484,8 @@ checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" dependencies = [ "bytes", "futures-core", - "http 1.3.1", - "http-body 1.0.1", + "http", + "http-body", "pin-project-lite", ] @@ -3287,50 +3509,28 @@ checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" [[package]] name = "humantime" -version = "2.2.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b112acc8b3adf4b107a8ec20977da0273a8c386765a3ec0229bd500a1443f9f" +checksum = "135b12329e5e3ce057a9f972339ea52bc954fe1e9358ef27f95e89716fbc5424" [[package]] name = "hyper" -version = "0.14.32" +version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41dfc780fdec9373c01bae43289ea34c972e40ee3c9f6b3c8801a35f35586ce7" +checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11" dependencies = [ + "atomic-waker", "bytes", "futures-channel", "futures-core", - "futures-util", - "h2 0.3.26", - "http 0.2.12", - "http-body 0.4.6", - "httparse", - "httpdate", - "itoa", - "pin-project-lite", - "socket2 0.5.8", - "tokio", - "tower-service", - "tracing", - "want", -] - -[[package]] -name = "hyper" -version = "1.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc2b571658e38e0c01b1fdca3bbbe93c00d3d71693ff2770043f8c29bc7d6f80" -dependencies = [ - "bytes", - "futures-channel", - "futures-util", - "h2 0.4.8", - "http 1.3.1", - "http-body 1.0.1", + "h2", + "http", + "http-body", "httparse", "httpdate", "itoa", "pin-project-lite", + "pin-utils", "smallvec", "tokio", "want", @@ -3338,13 +3538,12 @@ dependencies = [ [[package]] name = "hyper-rustls" -version = "0.27.5" +version = "0.27.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d191583f3da1305256f22463b9bb0471acad48a4e534a5218b9963e9c1f59b2" +checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" dependencies = [ - "futures-util", - "http 1.3.1", - "hyper 1.6.0", + "http", + "hyper", "hyper-util", "log", "rustls", @@ -3353,19 +3552,7 @@ dependencies = [ "tokio", "tokio-rustls", "tower-service", - "webpki-roots", -] - -[[package]] -name = "hyper-timeout" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbb958482e8c7be4bc3cf272a766a2b0bf1a6755e7a6ae777f017a31d11b13b1" -dependencies = [ - "hyper 0.14.32", - "pin-project-lite", - "tokio", - "tokio-io-timeout", + "webpki-roots 1.0.5", ] [[package]] @@ -3374,43 +3561,32 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b90d566bffbce6a75bd8b09a05aa8c2cb1fabb6cb348f8840c9e4c90a0d83b0" dependencies = [ - "hyper 1.6.0", + "hyper", "hyper-util", "pin-project-lite", "tokio", "tower-service", ] -[[package]] -name = "hyper-tls" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" -dependencies = [ - "bytes", - "http-body-util", - "hyper 1.6.0", - "hyper-util", - "native-tls", - "tokio", - "tokio-native-tls", - "tower-service", -] - [[package]] name = "hyper-util" -version = "0.1.10" +version = "0.1.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df2dcfbe0677734ab2f3ffa7fa7bfd4706bfdc1ef393f2ee30184aed67e631b4" +checksum = "727805d60e7938b76b826a6ef209eb70eaa1812794f9424d4a4e2d740662df5f" dependencies = [ + "base64 0.22.1", "bytes", "futures-channel", + "futures-core", "futures-util", - "http 1.3.1", - "http-body 1.0.1", - "hyper 1.6.0", + "http", + "http-body", + "hyper", + "ipnet", + "libc", + "percent-encoding", "pin-project-lite", - "socket2 0.5.8", + "socket2 0.6.1", "tokio", "tower-service", "tracing", @@ -3418,16 +3594,17 @@ dependencies = [ [[package]] name = "iana-time-zone" -version = "0.1.61" +version = "0.1.64" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "235e081f3925a06703c2d0117ea8b91f042756fd6e7a6e5d901e8ca1a996b220" +checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb" dependencies = [ "android_system_properties", "core-foundation-sys", "iana-time-zone-haiku", "js-sys", + "log", "wasm-bindgen", - "windows-core 0.52.0", + "windows-core", ] [[package]] @@ -3441,21 +3618,22 @@ dependencies = [ [[package]] name = "icu_collections" -version = "1.5.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db2fa452206ebee18c4b5c2274dbf1de17008e874b4dc4f0aea9d01ca79e4526" +checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" dependencies = [ "displaydoc", + "potential_utf", "yoke", "zerofrom", "zerovec", ] [[package]] -name = "icu_locid" -version = "1.5.0" +name = "icu_locale_core" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13acbb8371917fc971be86fc8057c41a64b521c184808a698c02acc242dbf637" +checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" dependencies = [ "displaydoc", "litemap", @@ -3464,99 +3642,61 @@ dependencies = [ "zerovec", ] -[[package]] -name = "icu_locid_transform" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01d11ac35de8e40fdeda00d9e1e9d92525f3f9d887cdd7aa81d727596788b54e" -dependencies = [ - "displaydoc", - "icu_locid", - "icu_locid_transform_data", - "icu_provider", - "tinystr", - "zerovec", -] - -[[package]] -name = "icu_locid_transform_data" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fdc8ff3388f852bede6b579ad4e978ab004f139284d7b28715f773507b946f6e" - [[package]] name = "icu_normalizer" -version = "1.5.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19ce3e0da2ec68599d193c93d088142efd7f9c5d6fc9b803774855747dc6a84f" +checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" dependencies = [ - "displaydoc", "icu_collections", "icu_normalizer_data", "icu_properties", "icu_provider", "smallvec", - "utf16_iter", - "utf8_iter", - "write16", "zerovec", ] [[package]] name = "icu_normalizer_data" -version = "1.5.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8cafbf7aa791e9b22bec55a167906f9e1215fd475cd22adfcf660e03e989516" +checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" [[package]] name = "icu_properties" -version = "1.5.1" +version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93d6020766cfc6302c15dbbc9c8778c37e62c14427cb7f6e601d849e092aeef5" +checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec" dependencies = [ - "displaydoc", "icu_collections", - "icu_locid_transform", + "icu_locale_core", "icu_properties_data", "icu_provider", - "tinystr", + "zerotrie", "zerovec", ] [[package]] name = "icu_properties_data" -version = "1.5.0" +version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67a8effbc3dd3e4ba1afa8ad918d5684b8868b3b26500753effea8d2eed19569" +checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af" [[package]] name = "icu_provider" -version = "1.5.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ed421c8a8ef78d3e2dbc98a973be2f3770cb42b606e3ab18d6237c4dfde68d9" +checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" dependencies = [ "displaydoc", - "icu_locid", - "icu_provider_macros", - "stable_deref_trait", - "tinystr", + "icu_locale_core", "writeable", "yoke", "zerofrom", + "zerotrie", "zerovec", ] -[[package]] -name = "icu_provider_macros" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.100", -] - [[package]] name = "ident_case" version = "1.0.1" @@ -3565,9 +3705,9 @@ checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" [[package]] name = "idna" -version = "1.0.3" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" dependencies = [ "idna_adapter", "smallvec", @@ -3576,9 +3716,9 @@ dependencies = [ [[package]] name = "idna_adapter" -version = "1.2.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "daca1df1c957320b2cf139ac61e7bd64fed304c5040df000a745aa1de3b4ef71" +checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" dependencies = [ "icu_normalizer", "icu_properties", @@ -3624,14 +3764,14 @@ checksum = "a0eb5a3343abf848c0984fe4604b2b105da9539376e24fc0a3b0007411ae4fd9" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.114", ] [[package]] name = "indenter" -version = "0.3.3" +version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce23b50ad8242c51a442f3ff322d56b02f08852c77e4c0b4d3fd684abc89c683" +checksum = "964de6e86d545b246d84badc0fef527924ace5134f30641c203ef52ba83f58d5" [[package]] name = "indexmap" @@ -3646,25 +3786,26 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.8.0" +version = "2.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3954d50fe15b02142bf25d3b8bdadb634ec3948f103d04ffe3031bc8fe9d7058" +checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" dependencies = [ "equivalent", - "hashbrown 0.15.2", + "hashbrown 0.16.1", "serde", + "serde_core", ] [[package]] name = "indicatif" -version = "0.17.11" +version = "0.18.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "183b3088984b400f4cfac3620d5e076c84da5364016b4f49473de574b2586235" +checksum = "9375e112e4b463ec1b1c6c011953545c65a30164fbab5b581df32b3abf0dcb88" dependencies = [ - "console", - "number_prefix", + "console 0.16.2", "portable-atomic", - "unicode-width 0.2.0", + "unicode-width 0.2.2", + "unit-prefix", "web-time", ] @@ -3674,26 +3815,6 @@ version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc1804bdb6a9784758b200007273a8b84e2b0b0b97a8f1e18e763eceb3e9f98a" -[[package]] -name = "inotify" -version = "0.9.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8069d3ec154eb856955c1c0fbffefbf5f3c40a104ec912d4797314c1801abff" -dependencies = [ - "bitflags 1.3.2", - "inotify-sys", - "libc", -] - -[[package]] -name = "inotify-sys" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e05c02b5e89bff3b946cedeca278abc628fe811e604f027c45a8aa3cf793d0eb" -dependencies = [ - "libc", -] - [[package]] name = "inout" version = "0.1.4" @@ -3706,18 +3827,15 @@ dependencies = [ [[package]] name = "insta" -version = "1.42.2" +version = "1.46.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50259abbaa67d11d2bcafc7ba1d094ed7a0c70e3ce893f0d0997f73558cb3084" +checksum = "1b66886d14d18d420ab5052cbff544fc5d34d0b2cdd35eb5976aaa10a4a472e5" dependencies = [ - "console", - "linked-hash-map", + "console 0.15.11", "once_cell", - "pest", - "pest_derive", - "pin-project", "serde", "similar", + "tempfile", ] [[package]] @@ -3735,6 +3853,26 @@ version = "3.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8bb03732005da905c88227371639bf1ad885cc712789c011c31c5fb3ab3ccf02" +[[package]] +name = "inventory" +version = "0.3.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc61209c082fbeb19919bee74b176221b27223e27b65d781eb91af24eb1fb46e" +dependencies = [ + "rustversion", +] + +[[package]] +name = "io-uring" +version = "0.7.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdd7bddefd0a8833b88a4b68f90dae22c7450d11b354198baee3874fd811b344" +dependencies = [ + "bitflags 2.10.0", + "cfg-if", + "libc", +] + [[package]] name = "ipnet" version = "2.11.0" @@ -3743,19 +3881,30 @@ checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" [[package]] name = "iri-string" -version = "0.7.7" +version = "0.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc0f0a572e8ffe56e2ff4f769f32ffe919282c3916799f8b68688b6030063bea" +checksum = "c91338f0783edbd6195decb37bae672fd3b165faffb89bf7b9e6942f8b1a731a" dependencies = [ "memchr", "serde", ] +[[package]] +name = "is-terminal" +version = "0.4.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3640c1c38b8e4e43584d8df18be5fc6b0aa314ce6ebf51b53313d4306cca8e46" +dependencies = [ + "hermit-abi", + "libc", + "windows-sys 0.61.2", +] + [[package]] name = "is_terminal_polyfill" -version = "1.70.1" +version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" [[package]] name = "itertools" @@ -3795,9 +3944,9 @@ dependencies = [ [[package]] name = "itoa" -version = "1.0.15" +version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" +checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" [[package]] name = "jni" @@ -3823,18 +3972,19 @@ checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" [[package]] name = "jobserver" -version = "0.1.32" +version = "0.1.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48d1dbcbbeb6a7fec7e059840aa538bd62aaccf972c7346c4d9d2059312853d0" +checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" dependencies = [ + "getrandom 0.3.4", "libc", ] [[package]] name = "js-sys" -version = "0.3.77" +version = "0.3.85" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" +checksum = "8c942ebf8e95485ca0d52d97da7c5a2c387d0e7f0ba4c35e93bfcaee045955b3" dependencies = [ "once_cell", "wasm-bindgen", @@ -3849,11 +3999,23 @@ dependencies = [ "tabled", ] +[[package]] +name = "jsonrpc" +version = "0.1.0" +source = "git+https://github.com/MystenLabs/sui.git?branch=testnet#caeb21b13ef6e26cd8c5e48be520bcf0598d48c3" +dependencies = [ + "serde", + "serde_json", + "thiserror 1.0.69", + "tokio", + "tracing", +] + [[package]] name = "jsonrpsee" -version = "0.24.9" +version = "0.24.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37b26c20e2178756451cfeb0661fb74c47dd5988cb7e3939de7e9241fd604d42" +checksum = "e281ae70cc3b98dac15fced3366a880949e65fc66e345ce857a5682d152f3e62" dependencies = [ "jsonrpsee-core", "jsonrpsee-http-client", @@ -3867,13 +4029,13 @@ dependencies = [ [[package]] name = "jsonrpsee-client-transport" -version = "0.24.9" +version = "0.24.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bacb85abf4117092455e1573625e21b8f8ef4dec8aff13361140b2dc266cdff2" +checksum = "cc4280b709ac3bb5e16cf3bad5056a0ec8df55fa89edfe996361219aadc2c7ea" dependencies = [ "base64 0.22.1", "futures-util", - "http 1.3.1", + "http", "jsonrpsee-core", "pin-project", "rustls", @@ -3883,23 +4045,23 @@ dependencies = [ "thiserror 1.0.69", "tokio", "tokio-rustls", - "tokio-util 0.7.14", + "tokio-util 0.7.18", "tracing", "url", ] [[package]] name = "jsonrpsee-core" -version = "0.24.9" +version = "0.24.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "456196007ca3a14db478346f58c7238028d55ee15c1df15115596e411ff27925" +checksum = "348ee569eaed52926b5e740aae20863762b16596476e943c9e415a6479021622" dependencies = [ "async-trait", "bytes", "futures-timer", "futures-util", - "http 1.3.1", - "http-body 1.0.1", + "http", + "http-body", "http-body-util", "jsonrpsee-types", "parking_lot", @@ -3916,14 +4078,14 @@ dependencies = [ [[package]] name = "jsonrpsee-http-client" -version = "0.24.9" +version = "0.24.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c872b6c9961a4ccc543e321bb5b89f6b2d2c7fe8b61906918273a3333c95400c" +checksum = "f50c389d6e6a52eb7c3548a6600c90cf74d9b71cb5912209833f00a5479e9a01" dependencies = [ "async-trait", "base64 0.22.1", - "http-body 1.0.1", - "hyper 1.6.0", + "http-body", + "hyper", "hyper-rustls", "hyper-util", "jsonrpsee-core", @@ -3941,28 +4103,28 @@ dependencies = [ [[package]] name = "jsonrpsee-proc-macros" -version = "0.24.9" +version = "0.24.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e65763c942dfc9358146571911b0cd1c361c2d63e2d2305622d40d36376ca80" +checksum = "7398cddf5013cca4702862a2692b66c48a3bd6cf6ec681a47453c93d63cf8de5" dependencies = [ "heck 0.5.0", - "proc-macro-crate 3.3.0", + "proc-macro-crate 3.4.0", "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.114", ] [[package]] name = "jsonrpsee-server" -version = "0.24.9" +version = "0.24.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55e363146da18e50ad2b51a0a7925fc423137a0b1371af8235b1c231a0647328" +checksum = "21429bcdda37dcf2d43b68621b994adede0e28061f816b038b0f18c70c143d51" dependencies = [ "futures-util", - "http 1.3.1", - "http-body 1.0.1", + "http", + "http-body", "http-body-util", - "hyper 1.6.0", + "hyper", "hyper-util", "jsonrpsee-core", "jsonrpsee-types", @@ -3974,18 +4136,18 @@ dependencies = [ "thiserror 1.0.69", "tokio", "tokio-stream", - "tokio-util 0.7.14", + "tokio-util 0.7.18", "tower 0.4.13", "tracing", ] [[package]] name = "jsonrpsee-types" -version = "0.24.9" +version = "0.24.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08a8e70baf945b6b5752fc8eb38c918a48f1234daf11355e07106d963f860089" +checksum = "b0f05e0028e55b15dbd2107163b3c744cd3bb4474f193f95d9708acbf5677e44" dependencies = [ - "http 1.3.1", + "http", "serde", "serde_json", "thiserror 1.0.69", @@ -3993,11 +4155,11 @@ dependencies = [ [[package]] name = "jsonrpsee-ws-client" -version = "0.24.9" +version = "0.24.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01b3323d890aa384f12148e8d2a1fd18eb66e9e7e825f9de4fa53bcc19b93eef" +checksum = "78fc744f17e7926d57f478cf9ca6e1ee5d8332bf0514860b1a3cdf1742e614cc" dependencies = [ - "http 1.3.1", + "http", "jsonrpsee-client-transport", "jsonrpsee-core", "jsonrpsee-types", @@ -4013,10 +4175,22 @@ dependencies = [ "cfg-if", "ecdsa 0.14.8", "elliptic-curve 0.12.3", - "sha2 0.10.8", + "sha2 0.10.9", "sha3", ] +[[package]] +name = "k256" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6e3919bbaa2945715f0bb6d3934a173d1e9a59ac23767fbaaef277265a7411b" +dependencies = [ + "cfg-if", + "ecdsa 0.16.9", + "elliptic-curve 0.13.8", + "sha2 0.10.9", +] + [[package]] name = "keccak" version = "0.1.5" @@ -4027,23 +4201,34 @@ dependencies = [ ] [[package]] -name = "kqueue" -version = "1.0.8" +name = "lalrpop" +version = "0.19.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7447f1ca1b7b563588a205fe93dea8df60fd981423a768bc1c0ded35ed147d0c" +checksum = "0a1cbf952127589f2851ab2046af368fd20645491bb4b376f04b7f94d7a9837b" dependencies = [ - "kqueue-sys", - "libc", + "ascii-canvas", + "bit-set 0.5.3", + "diff", + "ena", + "is-terminal", + "itertools 0.10.5", + "lalrpop-util", + "petgraph 0.6.5", + "regex", + "regex-syntax 0.6.29", + "string_cache", + "term", + "tiny-keccak", + "unicode-xid", ] [[package]] -name = "kqueue-sys" -version = "1.0.4" +name = "lalrpop-util" +version = "0.19.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed9625ffda8729b85e45cf04090035ac368927b8cebc34898e7c120f52e4838b" +checksum = "d3c48237b9604c5a4702de6b824e02006c3214327564636aef27c1028a8fa0ed" dependencies = [ - "bitflags 1.3.2", - "libc", + "regex", ] [[package]] @@ -4061,6 +4246,15 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" +[[package]] +name = "lcov" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ccfa6d5e585a884db65b37f38184e4364eaf74d884ac35d0a90fe9baf80b723" +dependencies = [ + "thiserror 1.0.69", +] + [[package]] name = "leb128" version = "0.2.5" @@ -4069,50 +4263,51 @@ checksum = "884e2677b40cc8c339eaefcb701c32ef1fd2493d71118dc0ca4b6a736c93bd67" [[package]] name = "libc" -version = "0.2.171" +version = "0.2.180" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c19937216e9d3aa9956d9bb8dfc0b0c8beb6058fc4f7a4dc4d850edf86a237d6" +checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc" [[package]] name = "libloading" -version = "0.8.6" +version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc2f4eb4bc735547cfed7c0a4922cbd04a4655978c09b54f1f7b228750664c34" +checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" dependencies = [ "cfg-if", - "windows-targets 0.52.6", + "windows-link", ] [[package]] name = "libm" -version = "0.2.11" +version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8355be11b20d696c8f18f6cc018c4e372165b1fa8126cef092399c9951984ffa" +checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de" [[package]] name = "libredox" -version = "0.1.3" +version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" +checksum = "3d0b95e02c851351f877147b7deea7b1afb1df71b63aa5f8270716e0c5720616" dependencies = [ - "bitflags 2.9.0", + "bitflags 2.10.0", "libc", - "redox_syscall", + "redox_syscall 0.7.0", ] [[package]] name = "librocksdb-sys" -version = "0.11.0+8.1.1" +version = "0.16.0+8.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3386f101bcb4bd252d8e9d2fb41ec3b0862a15a62b478c355b2982efa469e3e" +checksum = "ce3d60bc059831dc1c83903fb45c103f75db65c5a7bf22272764d9cc683e348c" dependencies = [ - "bindgen", + "bindgen 0.69.5", "bzip2-sys", "cc", "glob", "libc", "libz-sys", "lz4-sys", + "tikv-jemalloc-sys", "zstd-sys", ] @@ -4128,9 +4323,9 @@ dependencies = [ [[package]] name = "libz-sys" -version = "1.1.22" +version = "1.1.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b70e7a7df205e92a1a4cd9aaae7898dac0aa555503cc0a649494d0d60e7651d" +checksum = "15d118bbf3771060e7311cc7bb0545b01d08a8b4a7de949198dec1fa0ca1c0f7" dependencies = [ "cc", "pkg-config", @@ -4143,48 +4338,111 @@ version = "0.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" +[[package]] +name = "linkme" +version = "0.3.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e3283ed2d0e50c06dd8602e0ab319bb048b6325d0bba739db64ed8205179898" +dependencies = [ + "linkme-impl", +] + +[[package]] +name = "linkme-impl" +version = "0.3.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5cec0ec4228b4853bb129c84dbf093a27e6c7a20526da046defc334a1b017f7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + [[package]] name = "linux-raw-sys" -version = "0.9.3" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe7db12097d22ec582439daf8618b8fdd1a7bef6270e9af3b1ebcd30893cf413" +checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" [[package]] name = "litemap" -version = "0.7.5" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23fb14cb19457329c82206317a5663005a4d404783dc74f4252769b0d5f42856" +checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" [[package]] name = "lock_api" -version = "0.4.12" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" dependencies = [ - "autocfg", "scopeguard", ] [[package]] name = "log" -version = "0.4.26" +version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30bde2b3dc3671ae49d8e2e9f044c7c005836e7a023ee57cffa25ab82764bb9e" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" dependencies = [ - "serde", + "serde_core", ] [[package]] -name = "loom" -version = "0.7.2" +name = "logos" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "419e0dc8046cb947daa77eb95ae174acfbddb7673b4151f56d1eed8e93fbfaca" +checksum = "bf8b031682c67a8e3d5446840f9573eb7fe26efe7ec8d195c9ac4c0647c502f1" dependencies = [ - "cfg-if", - "generator", - "scoped-tls", - "tracing", - "tracing-subscriber", + "logos-derive 0.12.1", +] + +[[package]] +name = "logos" +version = "0.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff472f899b4ec2d99161c51f60ff7075eeb3097069a36050d8037a6325eb8154" +dependencies = [ + "logos-derive 0.15.1", +] + +[[package]] +name = "logos-codegen" +version = "0.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "192a3a2b90b0c05b27a0b2c43eecdb7c415e29243acc3f89cc8247a5b693045c" +dependencies = [ + "beef", + "fnv", + "lazy_static", + "proc-macro2", + "quote", + "regex-syntax 0.8.8", + "rustc_version", + "syn 2.0.114", +] + +[[package]] +name = "logos-derive" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1d849148dbaf9661a6151d1ca82b13bb4c4c128146a88d05253b38d4e2f496c" +dependencies = [ + "beef", + "fnv", + "proc-macro2", + "quote", + "regex-syntax 0.6.29", + "syn 1.0.109", +] + +[[package]] +name = "logos-derive" +version = "0.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "605d9697bcd5ef3a42d38efc51541aa3d6a4a25f7ab6d1ed0da5ac632a26b470" +dependencies = [ + "logos-codegen", ] [[package]] @@ -4196,6 +4454,25 @@ dependencies = [ "hashbrown 0.13.2", ] +[[package]] +name = "lru-slab" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" + +[[package]] +name = "lsp-types" +version = "0.94.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c66bfd44a06ae10647fe3f8214762e9369fd4248df1350924b4ef9e770a85ea1" +dependencies = [ + "bitflags 1.3.2", + "serde", + "serde_json", + "serde_repr", + "url", +] + [[package]] name = "lsp-types" version = "0.95.1" @@ -4219,13 +4496,30 @@ dependencies = [ "libc", ] +[[package]] +name = "maplit" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e2e65a1a2e43cfcb47a895c4c8b10d1f4a61097f9f254f183aee60cad9c651d" + +[[package]] +name = "match-lookup" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "757aee279b8bdbb9f9e676796fd459e4207a1f986e87886700abf589f5abf771" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + [[package]] name = "matchers" -version = "0.1.0" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" +checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" dependencies = [ - "regex-automata 0.1.10", + "regex-automata", ] [[package]] @@ -4241,29 +4535,66 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94" [[package]] -name = "md-5" -version = "0.10.6" +name = "matchit" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" + +[[package]] +name = "md-5" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" +dependencies = [ + "cfg-if", + "digest 0.10.7", +] + +[[package]] +name = "memchr" +version = "2.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" + +[[package]] +name = "memoffset" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5aa361d4faea93603064a027415f07bd8e1d5c88c9fbf68bf56a285428fd79ce" +dependencies = [ + "autocfg", +] + +[[package]] +name = "miette" +version = "7.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" +checksum = "5f98efec8807c63c752b5bd61f862c165c115b0a35685bdcfd9238c7aeb592b7" dependencies = [ "cfg-if", - "digest 0.10.7", + "miette-derive", + "unicode-width 0.1.14", ] [[package]] -name = "memchr" -version = "2.7.4" +name = "miette-derive" +version = "7.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" +checksum = "db5b29714e950dbb20d5e6f74f9dcec4edbcc1067bb7f8ed198c097b8c1a818b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] [[package]] name = "migrations_internals" -version = "2.2.0" +version = "2.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd01039851e82f8799046eabbb354056283fb265c8ec0996af940f4e85a380ff" +checksum = "3bda1634d70d5bd53553cf15dca9842a396e8c799982a3ad22998dc44d961f24" dependencies = [ "serde", - "toml 0.8.20", + "toml 0.9.11+spec-1.1.0", ] [[package]] @@ -4301,11 +4632,12 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] name = "miniz_oxide" -version = "0.8.5" +version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e3e04debbb59698c15bacbb6d93584a8c0ca9cc3213cb423d31f760d8843ce5" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" dependencies = [ "adler2", + "simd-adler32", ] [[package]] @@ -4316,118 +4648,106 @@ checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" dependencies = [ "libc", "log", - "wasi 0.11.0+wasi-snapshot-preview1", + "wasi 0.11.1+wasi-snapshot-preview1", "windows-sys 0.48.0", ] [[package]] name = "mio" -version = "1.0.3" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" +checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" dependencies = [ "libc", - "wasi 0.11.0+wasi-snapshot-preview1", - "windows-sys 0.52.0", + "wasi 0.11.1+wasi-snapshot-preview1", + "windows-sys 0.61.2", +] + +[[package]] +name = "mockall" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c84490118f2ee2d74570d114f3d0493cbf02790df303d2707606c3e14e07c96" +dependencies = [ + "cfg-if", + "downcast", + "fragile", + "lazy_static", + "mockall_derive", + "predicates", + "predicates-tree", +] + +[[package]] +name = "mockall_derive" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22ce75669015c4f47b289fd4d4f56e894e4c96003ffdf3ac51313126f94c6cbb" +dependencies = [ + "cfg-if", + "proc-macro2", + "quote", + "syn 1.0.109", ] [[package]] name = "moka" -version = "0.12.10" +version = "0.12.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9321642ca94a4282428e6ea4af8cc2ca4eac48ac7a6a4ea8f33f76d0ce70926" +checksum = "a3dec6bd31b08944e08b58fd99373893a6c17054d6f3ea5006cc894f4f4eee2a" dependencies = [ "crossbeam-channel", "crossbeam-epoch", "crossbeam-utils", - "loom", + "equivalent", "parking_lot", "portable-atomic", - "rustc_version", "smallvec", "tagptr", - "thiserror 1.0.69", "uuid", ] [[package]] name = "move-abstract-interpreter" version = "0.1.0" -source = "git+https://github.com/MystenLabs/sui.git?rev=88ba4e08e96ba1ab965c11ce1a915331dd3ed68d#88ba4e08e96ba1ab965c11ce1a915331dd3ed68d" -dependencies = [ - "move-binary-format 0.0.3 (git+https://github.com/MystenLabs/sui.git?rev=88ba4e08e96ba1ab965c11ce1a915331dd3ed68d)", - "move-bytecode-verifier-meter", -] +source = "git+https://github.com/MystenLabs/sui.git?branch=testnet#caeb21b13ef6e26cd8c5e48be520bcf0598d48c3" [[package]] name = "move-abstract-stack" version = "0.0.1" -source = "git+https://github.com/MystenLabs/sui.git?rev=88ba4e08e96ba1ab965c11ce1a915331dd3ed68d#88ba4e08e96ba1ab965c11ce1a915331dd3ed68d" - -[[package]] -name = "move-binary-format" -version = "0.0.3" -source = "git+https://github.com/MystenLabs/sui.git?rev=42ba6c0#42ba6c03128233cdeb3fc6e0a22dabd0bfc55385" -dependencies = [ - "anyhow", - "enum-compat-util 0.1.0 (git+https://github.com/MystenLabs/sui.git?rev=42ba6c0)", - "move-core-types 0.0.4 (git+https://github.com/MystenLabs/sui.git?rev=42ba6c0)", - "move-proc-macros 0.1.0 (git+https://github.com/MystenLabs/sui.git?rev=42ba6c0)", - "ref-cast", - "serde", - "variant_count", -] +source = "git+https://github.com/MystenLabs/sui.git?branch=testnet#caeb21b13ef6e26cd8c5e48be520bcf0598d48c3" [[package]] name = "move-binary-format" version = "0.0.3" -source = "git+https://github.com/MystenLabs/sui.git?rev=88ba4e08e96ba1ab965c11ce1a915331dd3ed68d#88ba4e08e96ba1ab965c11ce1a915331dd3ed68d" +source = "git+https://github.com/MystenLabs/sui.git?branch=testnet#caeb21b13ef6e26cd8c5e48be520bcf0598d48c3" dependencies = [ "anyhow", - "enum-compat-util 0.1.0 (git+https://github.com/MystenLabs/sui.git?rev=88ba4e08e96ba1ab965c11ce1a915331dd3ed68d)", - "move-core-types 0.0.4 (git+https://github.com/MystenLabs/sui.git?rev=88ba4e08e96ba1ab965c11ce1a915331dd3ed68d)", - "move-proc-macros 0.1.0 (git+https://github.com/MystenLabs/sui.git?rev=88ba4e08e96ba1ab965c11ce1a915331dd3ed68d)", + "enum-compat-util", + "indexmap 2.13.0", + "move-abstract-interpreter", + "move-core-types", + "move-proc-macros", "ref-cast", "serde", "variant_count", ] -[[package]] -name = "move-binding-derive" -version = "0.1.0" -source = "git+https://github.com/MystenLabs/move-binding.git?rev=99f68a28c2f19be40a09e5f1281af748df9a8d3e#99f68a28c2f19be40a09e5f1281af748df9a8d3e" -dependencies = [ - "bcs", - "fastcrypto 0.1.9 (registry+https://github.com/rust-lang/crates.io-index)", - "itertools 0.14.0", - "move-binary-format 0.0.3 (git+https://github.com/MystenLabs/sui.git?rev=42ba6c0)", - "move-core-types 0.0.4 (git+https://github.com/MystenLabs/sui.git?rev=42ba6c0)", - "move-types", - "proc-macro2", - "quote", - "reqwest", - "serde", - "serde_json", - "sui-sdk-types 0.0.2 (git+https://github.com/mystenlabs/sui-rust-sdk?rev=86a9e06)", - "sui-transaction-builder 0.1.0 (git+https://github.com/mystenlabs/sui-rust-sdk?rev=86a9e06)", - "syn 2.0.100", -] - [[package]] name = "move-borrow-graph" version = "0.0.1" -source = "git+https://github.com/MystenLabs/sui.git?rev=88ba4e08e96ba1ab965c11ce1a915331dd3ed68d#88ba4e08e96ba1ab965c11ce1a915331dd3ed68d" +source = "git+https://github.com/MystenLabs/sui.git?branch=testnet#caeb21b13ef6e26cd8c5e48be520bcf0598d48c3" [[package]] name = "move-bytecode-source-map" version = "0.1.0" -source = "git+https://github.com/MystenLabs/sui.git?rev=88ba4e08e96ba1ab965c11ce1a915331dd3ed68d#88ba4e08e96ba1ab965c11ce1a915331dd3ed68d" +source = "git+https://github.com/MystenLabs/sui.git?branch=testnet#caeb21b13ef6e26cd8c5e48be520bcf0598d48c3" dependencies = [ "anyhow", "bcs", - "move-binary-format 0.0.3 (git+https://github.com/MystenLabs/sui.git?rev=88ba4e08e96ba1ab965c11ce1a915331dd3ed68d)", + "move-binary-format", "move-command-line-common", - "move-core-types 0.0.4 (git+https://github.com/MystenLabs/sui.git?rev=88ba4e08e96ba1ab965c11ce1a915331dd3ed68d)", + "move-core-types", "move-ir-types", "move-symbol-pool", "serde", @@ -4437,45 +4757,46 @@ dependencies = [ [[package]] name = "move-bytecode-utils" version = "0.1.0" -source = "git+https://github.com/MystenLabs/sui.git?rev=88ba4e08e96ba1ab965c11ce1a915331dd3ed68d#88ba4e08e96ba1ab965c11ce1a915331dd3ed68d" +source = "git+https://github.com/MystenLabs/sui.git?branch=testnet#caeb21b13ef6e26cd8c5e48be520bcf0598d48c3" dependencies = [ "anyhow", - "indexmap 2.8.0", - "move-binary-format 0.0.3 (git+https://github.com/MystenLabs/sui.git?rev=88ba4e08e96ba1ab965c11ce1a915331dd3ed68d)", - "move-core-types 0.0.4 (git+https://github.com/MystenLabs/sui.git?rev=88ba4e08e96ba1ab965c11ce1a915331dd3ed68d)", - "petgraph", + "indexmap 2.13.0", + "move-binary-format", + "move-core-types", + "petgraph 0.8.3", "serde-reflection", ] [[package]] name = "move-bytecode-verifier" version = "0.1.0" -source = "git+https://github.com/MystenLabs/sui.git?rev=88ba4e08e96ba1ab965c11ce1a915331dd3ed68d#88ba4e08e96ba1ab965c11ce1a915331dd3ed68d" +source = "git+https://github.com/MystenLabs/sui.git?branch=testnet#caeb21b13ef6e26cd8c5e48be520bcf0598d48c3" dependencies = [ "move-abstract-interpreter", "move-abstract-stack", - "move-binary-format 0.0.3 (git+https://github.com/MystenLabs/sui.git?rev=88ba4e08e96ba1ab965c11ce1a915331dd3ed68d)", + "move-binary-format", "move-borrow-graph", "move-bytecode-verifier-meter", - "move-core-types 0.0.4 (git+https://github.com/MystenLabs/sui.git?rev=88ba4e08e96ba1ab965c11ce1a915331dd3ed68d)", + "move-core-types", + "move-regex-borrow-graph", "move-vm-config", - "petgraph", + "petgraph 0.8.3", ] [[package]] name = "move-bytecode-verifier-meter" version = "0.1.0" -source = "git+https://github.com/MystenLabs/sui.git?rev=88ba4e08e96ba1ab965c11ce1a915331dd3ed68d#88ba4e08e96ba1ab965c11ce1a915331dd3ed68d" +source = "git+https://github.com/MystenLabs/sui.git?branch=testnet#caeb21b13ef6e26cd8c5e48be520bcf0598d48c3" dependencies = [ - "move-binary-format 0.0.3 (git+https://github.com/MystenLabs/sui.git?rev=88ba4e08e96ba1ab965c11ce1a915331dd3ed68d)", - "move-core-types 0.0.4 (git+https://github.com/MystenLabs/sui.git?rev=88ba4e08e96ba1ab965c11ce1a915331dd3ed68d)", + "move-binary-format", + "move-core-types", "move-vm-config", ] [[package]] name = "move-command-line-common" version = "0.1.0" -source = "git+https://github.com/MystenLabs/sui.git?rev=88ba4e08e96ba1ab965c11ce1a915331dd3ed68d#88ba4e08e96ba1ab965c11ce1a915331dd3ed68d" +source = "git+https://github.com/MystenLabs/sui.git?branch=testnet#caeb21b13ef6e26cd8c5e48be520bcf0598d48c3" dependencies = [ "anyhow", "bcs", @@ -4483,9 +4804,10 @@ dependencies = [ "dirs-next", "hex", "insta", - "move-binary-format 0.0.3 (git+https://github.com/MystenLabs/sui.git?rev=88ba4e08e96ba1ab965c11ce1a915331dd3ed68d)", - "move-core-types 0.0.4 (git+https://github.com/MystenLabs/sui.git?rev=88ba4e08e96ba1ab965c11ce1a915331dd3ed68d)", + "move-binary-format", + "move-core-types", "once_cell", + "packed_struct", "serde", "sha2 0.9.9", "vfs", @@ -4495,7 +4817,7 @@ dependencies = [ [[package]] name = "move-compiler" version = "0.0.1" -source = "git+https://github.com/MystenLabs/sui.git?rev=88ba4e08e96ba1ab965c11ce1a915331dd3ed68d#88ba4e08e96ba1ab965c11ce1a915331dd3ed68d" +source = "git+https://github.com/MystenLabs/sui.git?branch=testnet#caeb21b13ef6e26cd8c5e48be520bcf0598d48c3" dependencies = [ "anyhow", "bcs", @@ -4504,19 +4826,20 @@ dependencies = [ "dunce", "hex", "insta", - "lsp-types", - "move-binary-format 0.0.3 (git+https://github.com/MystenLabs/sui.git?rev=88ba4e08e96ba1ab965c11ce1a915331dd3ed68d)", + "lsp-types 0.95.1", + "move-abstract-interpreter", + "move-binary-format", "move-borrow-graph", "move-bytecode-source-map", "move-bytecode-verifier", "move-command-line-common", - "move-core-types 0.0.4 (git+https://github.com/MystenLabs/sui.git?rev=88ba4e08e96ba1ab965c11ce1a915331dd3ed68d)", + "move-core-types", "move-ir-to-bytecode", "move-ir-types", - "move-proc-macros 0.1.0 (git+https://github.com/MystenLabs/sui.git?rev=88ba4e08e96ba1ab965c11ce1a915331dd3ed68d)", + "move-proc-macros", "move-symbol-pool", "once_cell", - "petgraph", + "petgraph 0.8.3", "rayon", "regex", "serde", @@ -4530,39 +4853,16 @@ dependencies = [ [[package]] name = "move-core-types" version = "0.0.4" -source = "git+https://github.com/MystenLabs/sui.git?rev=42ba6c0#42ba6c03128233cdeb3fc6e0a22dabd0bfc55385" -dependencies = [ - "anyhow", - "bcs", - "enum-compat-util 0.1.0 (git+https://github.com/MystenLabs/sui.git?rev=42ba6c0)", - "ethnum", - "hex", - "leb128", - "move-proc-macros 0.1.0 (git+https://github.com/MystenLabs/sui.git?rev=42ba6c0)", - "num", - "once_cell", - "primitive-types", - "rand 0.8.5", - "ref-cast", - "serde", - "serde_bytes", - "serde_with", - "thiserror 1.0.69", - "uint", -] - -[[package]] -name = "move-core-types" -version = "0.0.4" -source = "git+https://github.com/MystenLabs/sui.git?rev=88ba4e08e96ba1ab965c11ce1a915331dd3ed68d#88ba4e08e96ba1ab965c11ce1a915331dd3ed68d" +source = "git+https://github.com/MystenLabs/sui.git?branch=testnet#caeb21b13ef6e26cd8c5e48be520bcf0598d48c3" dependencies = [ "anyhow", "bcs", - "enum-compat-util 0.1.0 (git+https://github.com/MystenLabs/sui.git?rev=88ba4e08e96ba1ab965c11ce1a915331dd3ed68d)", + "enum-compat-util", "ethnum", "hex", + "indexmap 2.13.0", "leb128", - "move-proc-macros 0.1.0 (git+https://github.com/MystenLabs/sui.git?rev=88ba4e08e96ba1ab965c11ce1a915331dd3ed68d)", + "move-proc-macros", "num", "once_cell", "primitive-types", @@ -4578,28 +4878,32 @@ dependencies = [ [[package]] name = "move-coverage" version = "0.1.0" -source = "git+https://github.com/MystenLabs/sui.git?rev=88ba4e08e96ba1ab965c11ce1a915331dd3ed68d#88ba4e08e96ba1ab965c11ce1a915331dd3ed68d" +source = "git+https://github.com/MystenLabs/sui.git?branch=testnet#caeb21b13ef6e26cd8c5e48be520bcf0598d48c3" dependencies = [ "anyhow", "bcs", "clap", "codespan", "colored", - "indexmap 2.8.0", + "indexmap 2.13.0", + "lcov", "move-abstract-interpreter", - "move-binary-format 0.0.3 (git+https://github.com/MystenLabs/sui.git?rev=88ba4e08e96ba1ab965c11ce1a915331dd3ed68d)", + "move-binary-format", "move-bytecode-source-map", + "move-bytecode-verifier", "move-command-line-common", - "move-core-types 0.0.4 (git+https://github.com/MystenLabs/sui.git?rev=88ba4e08e96ba1ab965c11ce1a915331dd3ed68d)", + "move-compiler", + "move-core-types", "move-ir-types", - "petgraph", + "move-trace-format", + "petgraph 0.8.3", "serde", ] [[package]] name = "move-disassembler" version = "0.1.0" -source = "git+https://github.com/MystenLabs/sui.git?rev=88ba4e08e96ba1ab965c11ce1a915331dd3ed68d#88ba4e08e96ba1ab965c11ce1a915331dd3ed68d" +source = "git+https://github.com/MystenLabs/sui.git?branch=testnet#caeb21b13ef6e26cd8c5e48be520bcf0598d48c3" dependencies = [ "anyhow", "bcs", @@ -4607,11 +4911,11 @@ dependencies = [ "hex", "inline_colorization", "move-abstract-interpreter", - "move-binary-format 0.0.3 (git+https://github.com/MystenLabs/sui.git?rev=88ba4e08e96ba1ab965c11ce1a915331dd3ed68d)", + "move-binary-format", "move-bytecode-source-map", "move-command-line-common", "move-compiler", - "move-core-types 0.0.4 (git+https://github.com/MystenLabs/sui.git?rev=88ba4e08e96ba1ab965c11ce1a915331dd3ed68d)", + "move-core-types", "move-coverage", "move-ir-types", "move-symbol-pool", @@ -4620,15 +4924,15 @@ dependencies = [ [[package]] name = "move-ir-to-bytecode" version = "0.1.0" -source = "git+https://github.com/MystenLabs/sui.git?rev=88ba4e08e96ba1ab965c11ce1a915331dd3ed68d#88ba4e08e96ba1ab965c11ce1a915331dd3ed68d" +source = "git+https://github.com/MystenLabs/sui.git?branch=testnet#caeb21b13ef6e26cd8c5e48be520bcf0598d48c3" dependencies = [ "anyhow", "codespan-reporting", "log", - "move-binary-format 0.0.3 (git+https://github.com/MystenLabs/sui.git?rev=88ba4e08e96ba1ab965c11ce1a915331dd3ed68d)", + "move-binary-format", "move-bytecode-source-map", "move-command-line-common", - "move-core-types 0.0.4 (git+https://github.com/MystenLabs/sui.git?rev=88ba4e08e96ba1ab965c11ce1a915331dd3ed68d)", + "move-core-types", "move-ir-to-bytecode-syntax", "move-ir-types", "move-symbol-pool", @@ -4638,12 +4942,12 @@ dependencies = [ [[package]] name = "move-ir-to-bytecode-syntax" version = "0.1.0" -source = "git+https://github.com/MystenLabs/sui.git?rev=88ba4e08e96ba1ab965c11ce1a915331dd3ed68d#88ba4e08e96ba1ab965c11ce1a915331dd3ed68d" +source = "git+https://github.com/MystenLabs/sui.git?branch=testnet#caeb21b13ef6e26cd8c5e48be520bcf0598d48c3" dependencies = [ "anyhow", "hex", "move-command-line-common", - "move-core-types 0.0.4 (git+https://github.com/MystenLabs/sui.git?rev=88ba4e08e96ba1ab965c11ce1a915331dd3ed68d)", + "move-core-types", "move-ir-types", "move-symbol-pool", ] @@ -4651,11 +4955,11 @@ dependencies = [ [[package]] name = "move-ir-types" version = "0.1.0" -source = "git+https://github.com/MystenLabs/sui.git?rev=88ba4e08e96ba1ab965c11ce1a915331dd3ed68d#88ba4e08e96ba1ab965c11ce1a915331dd3ed68d" +source = "git+https://github.com/MystenLabs/sui.git?branch=testnet#caeb21b13ef6e26cd8c5e48be520bcf0598d48c3" dependencies = [ "hex", "move-command-line-common", - "move-core-types 0.0.4 (git+https://github.com/MystenLabs/sui.git?rev=88ba4e08e96ba1ab965c11ce1a915331dd3ed68d)", + "move-core-types", "move-symbol-pool", "once_cell", "serde", @@ -4664,59 +4968,64 @@ dependencies = [ [[package]] name = "move-proc-macros" version = "0.1.0" -source = "git+https://github.com/MystenLabs/sui.git?rev=42ba6c0#42ba6c03128233cdeb3fc6e0a22dabd0bfc55385" +source = "git+https://github.com/MystenLabs/sui.git?branch=testnet#caeb21b13ef6e26cd8c5e48be520bcf0598d48c3" dependencies = [ - "enum-compat-util 0.1.0 (git+https://github.com/MystenLabs/sui.git?rev=42ba6c0)", + "enum-compat-util", "quote", - "syn 2.0.100", + "syn 2.0.114", ] [[package]] -name = "move-proc-macros" -version = "0.1.0" -source = "git+https://github.com/MystenLabs/sui.git?rev=88ba4e08e96ba1ab965c11ce1a915331dd3ed68d#88ba4e08e96ba1ab965c11ce1a915331dd3ed68d" +name = "move-regex-borrow-graph" +version = "0.0.1" +source = "git+https://github.com/MystenLabs/sui.git?branch=testnet#caeb21b13ef6e26cd8c5e48be520bcf0598d48c3" dependencies = [ - "enum-compat-util 0.1.0 (git+https://github.com/MystenLabs/sui.git?rev=88ba4e08e96ba1ab965c11ce1a915331dd3ed68d)", - "quote", - "syn 2.0.100", + "insta", + "itertools 0.10.5", + "move-binary-format", + "move-command-line-common", + "move-core-types", + "petgraph 0.8.3", + "proptest", ] [[package]] name = "move-symbol-pool" version = "0.1.0" -source = "git+https://github.com/MystenLabs/sui.git?rev=88ba4e08e96ba1ab965c11ce1a915331dd3ed68d#88ba4e08e96ba1ab965c11ce1a915331dd3ed68d" +source = "git+https://github.com/MystenLabs/sui.git?branch=testnet#caeb21b13ef6e26cd8c5e48be520bcf0598d48c3" dependencies = [ "once_cell", - "phf", + "phf 0.11.3", "serde", ] [[package]] -name = "move-types" -version = "0.1.0" -source = "git+https://github.com/MystenLabs/move-binding.git?rev=99f68a28c2f19be40a09e5f1281af748df9a8d3e#99f68a28c2f19be40a09e5f1281af748df9a8d3e" +name = "move-trace-format" +version = "0.0.1" +source = "git+https://github.com/MystenLabs/sui.git?branch=testnet#caeb21b13ef6e26cd8c5e48be520bcf0598d48c3" dependencies = [ - "move-core-types 0.0.4 (git+https://github.com/MystenLabs/sui.git?rev=42ba6c0)", - "primitive-types", + "move-binary-format", + "move-core-types", "serde", - "sui-sdk-types 0.0.2 (git+https://github.com/mystenlabs/sui-rust-sdk?rev=86a9e06)", - "sui-transaction-builder 0.1.0 (git+https://github.com/mystenlabs/sui-rust-sdk?rev=86a9e06)", + "serde_json", + "zstd 0.13.3", ] [[package]] name = "move-vm-config" version = "0.1.0" -source = "git+https://github.com/MystenLabs/sui.git?rev=88ba4e08e96ba1ab965c11ce1a915331dd3ed68d#88ba4e08e96ba1ab965c11ce1a915331dd3ed68d" +source = "git+https://github.com/MystenLabs/sui.git?branch=testnet#caeb21b13ef6e26cd8c5e48be520bcf0598d48c3" dependencies = [ - "move-binary-format 0.0.3 (git+https://github.com/MystenLabs/sui.git?rev=88ba4e08e96ba1ab965c11ce1a915331dd3ed68d)", + "move-binary-format", "once_cell", ] [[package]] name = "move-vm-profiler" version = "0.1.0" -source = "git+https://github.com/MystenLabs/sui.git?rev=88ba4e08e96ba1ab965c11ce1a915331dd3ed68d#88ba4e08e96ba1ab965c11ce1a915331dd3ed68d" +source = "git+https://github.com/MystenLabs/sui.git?branch=testnet#caeb21b13ef6e26cd8c5e48be520bcf0598d48c3" dependencies = [ + "move-trace-format", "move-vm-config", "once_cell", "serde", @@ -4727,11 +5036,11 @@ dependencies = [ [[package]] name = "move-vm-test-utils" version = "0.1.0" -source = "git+https://github.com/MystenLabs/sui.git?rev=88ba4e08e96ba1ab965c11ce1a915331dd3ed68d#88ba4e08e96ba1ab965c11ce1a915331dd3ed68d" +source = "git+https://github.com/MystenLabs/sui.git?branch=testnet#caeb21b13ef6e26cd8c5e48be520bcf0598d48c3" dependencies = [ "anyhow", - "move-binary-format 0.0.3 (git+https://github.com/MystenLabs/sui.git?rev=88ba4e08e96ba1ab965c11ce1a915331dd3ed68d)", - "move-core-types 0.0.4 (git+https://github.com/MystenLabs/sui.git?rev=88ba4e08e96ba1ab965c11ce1a915331dd3ed68d)", + "move-binary-format", + "move-core-types", "move-vm-profiler", "move-vm-types", "once_cell", @@ -4741,11 +5050,11 @@ dependencies = [ [[package]] name = "move-vm-types" version = "0.1.0" -source = "git+https://github.com/MystenLabs/sui.git?rev=88ba4e08e96ba1ab965c11ce1a915331dd3ed68d#88ba4e08e96ba1ab965c11ce1a915331dd3ed68d" +source = "git+https://github.com/MystenLabs/sui.git?branch=testnet#caeb21b13ef6e26cd8c5e48be520bcf0598d48c3" dependencies = [ "bcs", - "move-binary-format 0.0.3 (git+https://github.com/MystenLabs/sui.git?rev=88ba4e08e96ba1ab965c11ce1a915331dd3ed68d)", - "move-core-types 0.0.4 (git+https://github.com/MystenLabs/sui.git?rev=88ba4e08e96ba1ab965c11ce1a915331dd3ed68d)", + "move-binary-format", + "move-core-types", "move-vm-profiler", "serde", "smallvec", @@ -4754,7 +5063,7 @@ dependencies = [ [[package]] name = "msim" version = "0.1.0" -source = "git+https://github.com/MystenLabs/mysten-sim.git?rev=9f175e3517812929ad6bdd8db443f1bbd8107965#9f175e3517812929ad6bdd8db443f1bbd8107965" +source = "git+https://github.com/MystenLabs/mysten-sim.git?rev=9d787303d855f6cec92eb94933717d4ee1963548#9d787303d855f6cec92eb94933717d4ee1963548" dependencies = [ "ahash 0.7.8", "async-task", @@ -4774,7 +5083,7 @@ dependencies = [ "serde", "socket2 0.4.10", "tap", - "tokio-util 0.7.13", + "tokio-util 0.7.15", "toml 0.5.11", "tracing", "tracing-subscriber", @@ -4783,7 +5092,7 @@ dependencies = [ [[package]] name = "msim-macros" version = "0.1.0" -source = "git+https://github.com/MystenLabs/mysten-sim.git?rev=9f175e3517812929ad6bdd8db443f1bbd8107965#9f175e3517812929ad6bdd8db443f1bbd8107965" +source = "git+https://github.com/MystenLabs/mysten-sim.git?rev=9d787303d855f6cec92eb94933717d4ee1963548#9d787303d855f6cec92eb94933717d4ee1963548" dependencies = [ "darling 0.14.4", "proc-macro2", @@ -4812,11 +5121,12 @@ dependencies = [ [[package]] name = "multibase" -version = "0.9.1" +version = "0.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b3539ec3c1f04ac9748a260728e855f261b4977f5c3406612c884564f329404" +checksum = "8694bb4835f452b0e3bb06dbebb1d6fc5385b6ca1caf2e55fd165c042390ec77" dependencies = [ "base-x", + "base256emoji", "data-encoding", "data-encoding-macro", ] @@ -4846,21 +5156,31 @@ dependencies = [ "synstructure 0.12.6", ] +[[package]] +name = "multimap" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d87ecb2933e8aeadb3e3a02b828fed80a7528047e68b4f424523a0981a3a084" + [[package]] name = "mysten-common" version = "0.1.0" -source = "git+https://github.com/MystenLabs/sui.git?rev=88ba4e08e96ba1ab965c11ce1a915331dd3ed68d#88ba4e08e96ba1ab965c11ce1a915331dd3ed68d" +source = "git+https://github.com/MystenLabs/sui.git?branch=testnet#caeb21b13ef6e26cd8c5e48be520bcf0598d48c3" dependencies = [ + "antithesis_sdk", "anyhow", - "fastcrypto 0.1.8", + "either", + "fastcrypto", "futures", "mysten-metrics", + "once_cell", "parking_lot", - "prometheus", + "rand 0.8.5", "reqwest", + "serde_json", "snap", - "sui-tls", - "sui-types", + "sui-macros", + "tempfile", "tokio", "tracing", ] @@ -4868,10 +5188,10 @@ dependencies = [ [[package]] name = "mysten-metrics" version = "0.7.0" -source = "git+https://github.com/MystenLabs/sui.git?rev=88ba4e08e96ba1ab965c11ce1a915331dd3ed68d#88ba4e08e96ba1ab965c11ce1a915331dd3ed68d" +source = "git+https://github.com/MystenLabs/sui.git?branch=testnet#caeb21b13ef6e26cd8c5e48be520bcf0598d48c3" dependencies = [ "async-trait", - "axum 0.7.9", + "axum 0.8.8", "dashmap", "futures", "once_cell", @@ -4889,33 +5209,40 @@ dependencies = [ [[package]] name = "mysten-network" version = "0.2.0" -source = "git+https://github.com/MystenLabs/sui.git?rev=88ba4e08e96ba1ab965c11ce1a915331dd3ed68d#88ba4e08e96ba1ab965c11ce1a915331dd3ed68d" +source = "git+https://github.com/MystenLabs/sui.git?branch=testnet#caeb21b13ef6e26cd8c5e48be520bcf0598d48c3" dependencies = [ "anemo", "anemo-tower", + "anyhow", "async-stream", "bcs", "bytes", + "dashmap", "eyre", + "fastcrypto", "futures", - "http 1.3.1", - "http-body 1.0.1", + "http", + "http-body", "hyper-rustls", "hyper-util", "multiaddr", + "mysten-metrics", "once_cell", "pin-project-lite", "prometheus", + "quinn-proto", + "rand 0.8.5", + "rustls", "serde", "snap", "sui-http", "tokio", "tokio-rustls", "tokio-stream", - "tonic 0.12.3", + "tonic 0.14.2", "tonic-health", - "tower 0.4.13", - "tower-http", + "tower 0.5.3", + "tower-http 0.5.2", "tracing", ] @@ -4925,23 +5252,6 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "034a0ad7deebf0c2abcf2435950a6666c3c15ea9d8fad0c0f48efa8a7f843fed" -[[package]] -name = "native-tls" -version = "0.2.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87de3442987e9dbec73158d5c715e7ad9072fda936bb03d19d7fa10e00520f0e" -dependencies = [ - "libc", - "log", - "openssl", - "openssl-probe", - "openssl-sys", - "schannel", - "security-framework 2.11.1", - "security-framework-sys", - "tempfile", -] - [[package]] name = "neptune" version = "13.0.0" @@ -4961,6 +5271,33 @@ dependencies = [ "trait-set", ] +[[package]] +name = "new_debug_unreachable" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" + +[[package]] +name = "nibble_vec" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77a5d83df9f36fe23f0c3648c6bbb8b0298bb5f1939c8f2704431371f4b84d43" +dependencies = [ + "smallvec", +] + +[[package]] +name = "nix" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab2156c4fce2f8df6c499cc1c763e4394b7482525bf2a9701c9d79d215f519e4" +dependencies = [ + "bitflags 2.10.0", + "cfg-if", + "cfg_aliases 0.1.1", + "libc", +] + [[package]] name = "no-std-compat" version = "0.4.1" @@ -4982,6 +5319,9 @@ name = "nonempty" version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "995defdca0a589acfdd1bd2e8e3b896b4d4f7675a31fd14c32611440c7f608e6" +dependencies = [ + "serde", +] [[package]] name = "nonzero_ext" @@ -4990,32 +5330,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "38bf9645c8b145698bb0b18a4637dcacbc421ea49bef2317e4fd8065a387cf21" [[package]] -name = "notify" -version = "6.1.1" +name = "normalize-line-endings" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6205bd8bb1e454ad2e27422015fb5e4f2bcc7e08fa8f27058670d208324a4d2d" -dependencies = [ - "bitflags 2.9.0", - "crossbeam-channel", - "filetime", - "fsevent-sys", - "inotify", - "kqueue", - "libc", - "log", - "mio 0.8.11", - "walkdir", - "windows-sys 0.48.0", -] +checksum = "61807f77802ff30975e01f4f071c8ba10c022052f98b3294119f3e615d13e5be" [[package]] name = "nu-ansi-term" -version = "0.46.0" +version = "0.50.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" dependencies = [ - "overload", - "winapi", + "windows-sys 0.61.2", ] [[package]] @@ -5056,11 +5382,10 @@ dependencies = [ [[package]] name = "num-bigint-dig" -version = "0.8.4" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc84195820f291c7697304f3cbdadd1cb7199c0efc917ff5eafd71225c136151" +checksum = "e661dda6640fad38e827a6d4a310ff4763082116fe217f279885c97f511bb0b7" dependencies = [ - "byteorder", "lazy_static", "libm", "num-integer", @@ -5129,9 +5454,9 @@ dependencies = [ [[package]] name = "num_cpus" -version = "1.16.0" +version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" +checksum = "91df4bbde75afed763b708b7eee1e8e7651e02d97f6d5dd763e89367e957b23b" dependencies = [ "hermit-abi", "libc", @@ -5155,20 +5480,23 @@ dependencies = [ "proc-macro-crate 1.1.3", "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.114", ] [[package]] -name = "number_prefix" -version = "0.4.0" +name = "object" +version = "0.32.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3" +checksum = "a6a622008b6e321afc04970976f62ee297fdbaa6f95318ca343e3eebb9648441" +dependencies = [ + "memchr", +] [[package]] name = "object" -version = "0.36.7" +version = "0.37.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" +checksum = "ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe" dependencies = [ "memchr", ] @@ -5186,7 +5514,7 @@ dependencies = [ "futures", "httparse", "humantime", - "hyper 1.6.0", + "hyper", "itertools 0.13.0", "md-5", "parking_lot", @@ -5216,59 +5544,27 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.21.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d75b0bedcc4fe52caa0e03d9f1151a323e4aa5e2d78ba3580400cd3c9e2bc4bc" - -[[package]] -name = "opaque-debug" -version = "0.3.1" +version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" [[package]] -name = "openssl" -version = "0.10.71" +name = "once_cell_polyfill" +version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e14130c6a98cd258fdcb0fb6d744152343ff729cbfcb28c656a9d12b999fbcd" -dependencies = [ - "bitflags 2.9.0", - "cfg-if", - "foreign-types", - "libc", - "once_cell", - "openssl-macros", - "openssl-sys", -] +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" [[package]] -name = "openssl-macros" -version = "0.1.1" +name = "opaque-debug" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.100", -] +checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" [[package]] name = "openssl-probe" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" - -[[package]] -name = "openssl-sys" -version = "0.9.106" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8bb61ea9811cc39e3c2069f40b8b8e2e70d8569b361f879786cc7ed48b777cdd" -dependencies = [ - "cc", - "libc", - "pkg-config", - "vcpkg", -] +checksum = "9f50d9b3dabb09ecd771ad0aa242ca6894994c130308ca3d7684634df8037391" [[package]] name = "opentelemetry" @@ -5292,7 +5588,7 @@ checksum = "91cf61a1868dacc576bf2b2a1c3e9ab150af7272909e80085c3173384fe11f76" dependencies = [ "async-trait", "futures-core", - "http 1.3.1", + "http", "opentelemetry", "opentelemetry-proto", "opentelemetry_sdk", @@ -5359,15 +5655,9 @@ dependencies = [ "proc-macro-error", "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.114", ] -[[package]] -name = "overload" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" - [[package]] name = "p256" version = "0.13.2" @@ -5377,7 +5667,7 @@ dependencies = [ "ecdsa 0.16.9", "elliptic-curve 0.13.8", "primeorder", - "sha2 0.10.8", + "sha2 0.10.9", ] [[package]] @@ -5389,7 +5679,29 @@ dependencies = [ "ecdsa 0.16.9", "elliptic-curve 0.13.8", "primeorder", - "sha2 0.10.8", + "sha2 0.10.9", +] + +[[package]] +name = "packed_struct" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36b29691432cc9eff8b282278473b63df73bea49bc3ec5e67f31a3ae9c3ec190" +dependencies = [ + "bitvec 1.0.1", + "packed_struct_codegen", + "serde", +] + +[[package]] +name = "packed_struct_codegen" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cd6706dfe50d53e0f6aa09e12c034c44faacd23e966ae5a209e8bdb8f179f98" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", ] [[package]] @@ -5446,9 +5758,9 @@ checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" [[package]] name = "parking_lot" -version = "0.12.3" +version = "0.12.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" dependencies = [ "lock_api", "parking_lot_core", @@ -5456,15 +5768,15 @@ dependencies = [ [[package]] name = "parking_lot_core" -version = "0.9.10" +version = "0.9.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" dependencies = [ "cfg-if", "libc", - "redox_syscall", + "redox_syscall 0.5.18", "smallvec", - "windows-targets 0.52.6", + "windows-link", ] [[package]] @@ -5473,17 +5785,17 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77144664f6aac5f629d7efa815f5098a054beeeca6ccafee5ec453fd2b0c53f9" dependencies = [ - "bitflags 2.9.0", + "bitflags 2.10.0", "ciborium", "coset", "data-encoding", - "getrandom 0.2.15", + "getrandom 0.2.17", "hmac", - "indexmap 2.8.0", + "indexmap 2.13.0", "rand 0.8.5", "serde", "serde_json", - "sha2 0.10.8", + "sha2 0.10.9", "strum 0.25.0", "typeshare", "zeroize", @@ -5521,99 +5833,60 @@ dependencies = [ "digest 0.10.7", ] -[[package]] -name = "peeking_take_while" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19b17cddbe7ec3f8bc800887bab5e717348c95ea2ca0b1bf0837fb964dc67099" - [[package]] name = "pem" -version = "3.0.5" +version = "3.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38af38e8470ac9dee3ce1bae1af9c1671fffc44ddfd8bd1d0a3445bf349a8ef3" +checksum = "1d30c53c26bc5b31a98cd02d20f25a7c8567146caf63ed593a9d87b2775291be" dependencies = [ "base64 0.22.1", - "serde", -] - -[[package]] -name = "pem-rfc7468" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24d159833a9105500e0398934e205e0773f0b27529557134ecfc51c27646adac" -dependencies = [ - "base64ct", + "serde_core", ] [[package]] name = "pem-rfc7468" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" -dependencies = [ - "base64ct", -] - -[[package]] -name = "percent-encoding" -version = "2.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" - -[[package]] -name = "pest" -version = "2.7.15" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b7cafe60d6cf8e62e1b9b2ea516a089c008945bb5a275416789e7db0bc199dc" +checksum = "24d159833a9105500e0398934e205e0773f0b27529557134ecfc51c27646adac" dependencies = [ - "memchr", - "thiserror 2.0.12", - "ucd-trie", + "base64ct", ] [[package]] -name = "pest_derive" -version = "2.7.15" +name = "pem-rfc7468" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "816518421cfc6887a0d62bf441b6ffb4536fcc926395a69e1a85852d4363f57e" +checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" dependencies = [ - "pest", - "pest_generator", + "base64ct", ] [[package]] -name = "pest_generator" -version = "2.7.15" +name = "percent-encoding" +version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d1396fd3a870fc7838768d171b4616d5c91f6cc25e377b673d714567d99377b" -dependencies = [ - "pest", - "pest_meta", - "proc-macro2", - "quote", - "syn 2.0.100", -] +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] -name = "pest_meta" -version = "2.7.15" +name = "petgraph" +version = "0.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1e58089ea25d717bfd31fb534e4f3afcc2cc569c70de3e239778991ea3b7dea" +checksum = "b4c5cc86750666a3ed20bdaf5ca2a0344f9c67674cae0515bec2da16fbaa47db" dependencies = [ - "once_cell", - "pest", - "sha2 0.10.8", + "fixedbitset 0.4.2", + "indexmap 2.13.0", ] [[package]] name = "petgraph" -version = "0.5.1" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "467d164a6de56270bd7c4d070df81d07beace25012d5103ced4e9ff08d6afdb7" +checksum = "8701b58ea97060d5e5b155d383a69952a60943f0e6dfe30b04c287beb0b27455" dependencies = [ - "fixedbitset", - "indexmap 1.9.3", + "fixedbitset 0.5.7", + "hashbrown 0.15.5", + "indexmap 2.13.0", + "serde", ] [[package]] @@ -5623,7 +5896,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078" dependencies = [ "phf_macros", - "phf_shared", + "phf_shared 0.11.3", +] + +[[package]] +name = "phf" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1562dc717473dbaa4c1f85a36410e03c047b2e7df7f45ee938fbef64ae7fadf" +dependencies = [ + "phf_shared 0.13.1", + "serde", ] [[package]] @@ -5632,7 +5915,7 @@ version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" dependencies = [ - "phf_shared", + "phf_shared 0.11.3", "rand 0.8.5", ] @@ -5643,10 +5926,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f84ac04429c13a7ff43785d75ad27569f2951ce0ffd30a3321230db2fc727216" dependencies = [ "phf_generator", - "phf_shared", + "phf_shared 0.11.3", "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.114", ] [[package]] @@ -5658,6 +5941,15 @@ dependencies = [ "siphasher", ] +[[package]] +name = "phf_shared" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e57fef6bc5981e38c2ce2d63bfa546861309f875b8a75f092d1d54ae2d64f266" +dependencies = [ + "siphasher", +] + [[package]] name = "pin-project" version = "1.1.10" @@ -5675,7 +5967,7 @@ checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.114", ] [[package]] @@ -5708,7 +6000,7 @@ version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f" dependencies = [ - "der 0.7.9", + "der 0.7.10", "pkcs8 0.10.2", "spki 0.7.3", ] @@ -5729,7 +6021,7 @@ version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" dependencies = [ - "der 0.7.9", + "der 0.7.10", "spki 0.7.3", ] @@ -5753,15 +6045,15 @@ dependencies = [ [[package]] name = "portable-atomic" -version = "1.11.0" +version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "350e9b48cbc6b0e028b0473b114454c6316e57336ee184ceab6e53f72c178b3e" +checksum = "f89776e4d69bb58bc6993e99ffa1d11f228b839984854c7daeb5d37f87cbe950" [[package]] name = "postgres-protocol" -version = "0.6.8" +version = "0.6.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76ff0abab4a9b844b93ef7b81f1efc0a366062aaef2cd702c76256b5dc075c54" +checksum = "3ee9dd5fe15055d2b6806f4736aa0c9637217074e224bbec46d4041b91bb9491" dependencies = [ "base64 0.22.1", "byteorder", @@ -5770,22 +6062,31 @@ dependencies = [ "hmac", "md-5", "memchr", - "rand 0.9.0", - "sha2 0.10.8", + "rand 0.9.2", + "sha2 0.10.9", "stringprep", ] [[package]] name = "postgres-types" -version = "0.2.9" +version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "613283563cd90e1dfc3518d548caee47e0e725455ed619881f5cf21f36de4b48" +checksum = "54b858f82211e84682fecd373f68e1ceae642d8d751a1ebd13f33de6257b3e20" dependencies = [ "bytes", "fallible-iterator", "postgres-protocol", ] +[[package]] +name = "potential_utf" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" +dependencies = [ + "zerovec", +] + [[package]] name = "powerfmt" version = "0.2.0" @@ -5798,27 +6099,64 @@ version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" dependencies = [ - "zerocopy 0.8.23", + "zerocopy", ] [[package]] name = "pq-sys" -version = "0.7.0" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30b51d65ebe1cb1f40641b15abae017fed35ccdda46e3dab1ff8768f625a3222" +checksum = "574ddd6a267294433f140b02a726b0640c43cf7c6f717084684aaa3b285aba61" dependencies = [ "libc", + "pkg-config", "vcpkg", ] +[[package]] +name = "precomputed-hash" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" + +[[package]] +name = "predicates" +version = "2.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59230a63c37f3e18569bdb90e4a89cbf5bf8b06fea0b84e65ea10cc4df47addd" +dependencies = [ + "difflib", + "float-cmp", + "itertools 0.10.5", + "normalize-line-endings", + "predicates-core", + "regex", +] + +[[package]] +name = "predicates-core" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "727e462b119fe9c93fd0eb1429a5f7647394014cf3c04ab2c0350eeb09095ffa" + +[[package]] +name = "predicates-tree" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72dd2d6d381dfb73a193c7fca536518d7caee39fc8503f74e7dc0be0531b425c" +dependencies = [ + "predicates-core", + "termtree", +] + [[package]] name = "prettyplease" -version = "0.2.31" +version = "0.2.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5316f57387668042f561aae71480de936257848f9c43ce528e311d89a07cadeb" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" dependencies = [ "proc-macro2", - "syn 2.0.100", + "syn 2.0.114", ] [[package]] @@ -5854,9 +6192,9 @@ dependencies = [ [[package]] name = "proc-macro-crate" -version = "3.3.0" +version = "3.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edce586971a4dfaa28950c6f18ed55e0406c1ab88bbce2c6f6293a7aaba73d35" +checksum = "219cb19e96be00ab2e37d6e299658a0cfa83e52429179969b0f0121b4ac46983" dependencies = [ "toml_edit", ] @@ -5887,9 +6225,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.94" +version = "1.0.105" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a31971752e70b8b2686d7e46ec17fb38dad4051d94024c88df49b667caea9c84" +checksum = "535d180e0ecab6268a3e718bb9fd44db66bbbc256257165fc699dadf70d16fe7" dependencies = [ "unicode-ident", ] @@ -5912,7 +6250,7 @@ dependencies = [ [[package]] name = "prometheus-closure-metric" version = "0.1.0" -source = "git+https://github.com/MystenLabs/sui.git?rev=88ba4e08e96ba1ab965c11ce1a915331dd3ed68d#88ba4e08e96ba1ab965c11ce1a915331dd3ed68d" +source = "git+https://github.com/MystenLabs/sui.git?branch=testnet#caeb21b13ef6e26cd8c5e48be520bcf0598d48c3" dependencies = [ "anyhow", "prometheus", @@ -5921,19 +6259,18 @@ dependencies = [ [[package]] name = "proptest" -version = "1.6.0" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14cae93065090804185d3b75f0bf93b8eeda30c7a9b4a33d3bdb3988d6229e50" +checksum = "bee689443a2bd0a16ab0348b52ee43e3b2d1b1f931c8aa5c9f8de4c86fbe8c40" dependencies = [ - "bit-set", - "bit-vec", - "bitflags 2.9.0", - "lazy_static", + "bit-set 0.8.0", + "bit-vec 0.8.0", + "bitflags 2.10.0", "num-traits", - "rand 0.8.5", - "rand_chacha 0.3.1", - "rand_xorshift", - "regex-syntax 0.8.5", + "rand 0.9.2", + "rand_chacha 0.9.0", + "rand_xorshift 0.4.0", + "regex-syntax 0.8.8", "rusty-fork", "tempfile", "unarray", @@ -5947,62 +6284,86 @@ checksum = "4ee1c9ac207483d5e7db4940700de86a9aae46ef90c48b57f99fe7edb8345e49" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.114", ] [[package]] name = "prost" -version = "0.12.6" +version = "0.13.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "deb1435c188b76130da55f17a466d252ff7b1418b2ad3e037d127b94e3411f29" +checksum = "2796faa41db3ec313a31f7624d9286acf277b52de526150b7e69f3debf891ee5" dependencies = [ "bytes", - "prost-derive 0.12.6", + "prost-derive 0.13.5", ] [[package]] name = "prost" -version = "0.13.5" +version = "0.14.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2796faa41db3ec313a31f7624d9286acf277b52de526150b7e69f3debf891ee5" +checksum = "d2ea70524a2f82d518bce41317d0fae74151505651af45faf1ffbd6fd33f0568" dependencies = [ "bytes", - "prost-derive 0.13.5", + "prost-derive 0.14.3", +] + +[[package]] +name = "prost-build" +version = "0.14.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "343d3bd7056eda839b03204e68deff7d1b13aba7af2b2fd16890697274262ee7" +dependencies = [ + "heck 0.5.0", + "itertools 0.14.0", + "log", + "multimap", + "petgraph 0.8.3", + "prettyplease", + "prost 0.14.3", + "prost-types 0.14.3", + "pulldown-cmark", + "pulldown-cmark-to-cmark", + "regex", + "syn 2.0.114", + "tempfile", ] [[package]] name = "prost-derive" -version = "0.12.6" +version = "0.13.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81bddcdb20abf9501610992b6759a4c888aef7d1a7247ef75e2404275ac24af1" +checksum = "8a56d757972c98b346a9b766e3f02746cde6dd1cd1d1d563472929fdd74bec4d" dependencies = [ "anyhow", - "itertools 0.12.1", + "itertools 0.14.0", "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.114", ] [[package]] name = "prost-derive" -version = "0.13.5" +version = "0.14.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a56d757972c98b346a9b766e3f02746cde6dd1cd1d1d563472929fdd74bec4d" +checksum = "27c6023962132f4b30eb4c172c91ce92d933da334c59c23cddee82358ddafb0b" dependencies = [ "anyhow", "itertools 0.14.0", "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.114", ] [[package]] -name = "prost-types" -version = "0.12.6" +name = "prost-reflect" +version = "0.16.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9091c90b0a32608e984ff2fa4091273cbdd755d54935c51d520887f4a1dbd5b0" +checksum = "b89455ef41ed200cafc47c76c552ee7792370ac420497e551f16123a9135f76e" dependencies = [ - "prost 0.12.6", + "logos 0.15.1", + "miette", + "prost 0.14.3", + "prost-types 0.14.3", ] [[package]] @@ -6014,6 +6375,15 @@ dependencies = [ "prost 0.13.5", ] +[[package]] +name = "prost-types" +version = "0.14.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8991c4cbdb8bc5b11f0b074ffe286c30e523de90fee5ba8132f1399f23cb3dd7" +dependencies = [ + "prost 0.14.3", +] + [[package]] name = "protobuf" version = "2.28.0" @@ -6023,26 +6393,74 @@ dependencies = [ "bytes", ] +[[package]] +name = "protox" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f25a07a73c6717f0b9bbbd685918f5df9815f7efba450b83d9c9dea41f0e3a1" +dependencies = [ + "bytes", + "miette", + "prost 0.14.3", + "prost-reflect", + "prost-types 0.14.3", + "protox-parse", + "thiserror 2.0.17", +] + +[[package]] +name = "protox-parse" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "072eee358134396a4643dff81cfff1c255c9fbd3fb296be14bdb6a26f9156366" +dependencies = [ + "logos 0.15.1", + "miette", + "prost-types 0.14.3", + "thiserror 2.0.17", +] + [[package]] name = "psm" -version = "0.1.25" +version = "0.1.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f58e5423e24c18cc840e1c98370b3993c6649cd1678b4d24318bcf0a083cbe88" +checksum = "d11f2fedc3b7dafdc2851bc52f277377c5473d378859be234bc7ebb593144d01" dependencies = [ + "ar_archive_writer", "cc", ] +[[package]] +name = "pulldown-cmark" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e8bbe1a966bd2f362681a44f6edce3c2310ac21e4d5067a6e7ec396297a6ea0" +dependencies = [ + "bitflags 2.10.0", + "memchr", + "unicase", +] + +[[package]] +name = "pulldown-cmark-to-cmark" +version = "22.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50793def1b900256624a709439404384204a5dc3a6ec580281bfaac35e882e90" +dependencies = [ + "pulldown-cmark", +] + [[package]] name = "quanta" -version = "0.12.5" +version = "0.12.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3bd1fe6824cea6538803de3ff1bc0cf3949024db3d43c9643024bfb33a807c0e" +checksum = "f3ab5a9d756f0d97bdc89019bd2e4ea098cf9cde50ee7564dde6b81ccc8f06c7" dependencies = [ "crossbeam-utils", "libc", "once_cell", "raw-cpuid", - "wasi 0.11.0+wasi-snapshot-preview1", + "wasi 0.11.1+wasi-snapshot-preview1", "web-sys", "winapi", ] @@ -6055,9 +6473,9 @@ checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" [[package]] name = "quick-xml" -version = "0.37.2" +version = "0.37.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "165859e9e55f79d67b96c5d96f4e88b6f2695a1972849c15a6a3f5c59fc2c003" +checksum = "331e97a1af0bf59823e6eadffe373d7b27f485be8748f71471c662c1f269b7fb" dependencies = [ "memchr", "serde", @@ -6065,20 +6483,20 @@ dependencies = [ [[package]] name = "quinn" -version = "0.11.7" +version = "0.11.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3bd15a6f2967aef83887dcb9fec0014580467e33720d073560cf015a5683012" +checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20" dependencies = [ "bytes", - "cfg_aliases", + "cfg_aliases 0.2.1", "futures-io", "pin-project-lite", "quinn-proto", "quinn-udp", "rustc-hash 2.1.1", "rustls", - "socket2 0.5.8", - "thiserror 2.0.12", + "socket2 0.6.1", + "thiserror 2.0.17", "tokio", "tracing", "web-time", @@ -6086,19 +6504,21 @@ dependencies = [ [[package]] name = "quinn-proto" -version = "0.11.10" +version = "0.11.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b820744eb4dc9b57a3398183639c511b5a26d2ed702cedd3febaa1393caa22cc" +checksum = "f1906b49b0c3bc04b5fe5d86a77925ae6524a19b816ae38ce1e426255f1d8a31" dependencies = [ "bytes", - "getrandom 0.3.2", - "rand 0.9.0", + "fastbloom", + "getrandom 0.3.4", + "lru-slab", + "rand 0.9.2", "ring", "rustc-hash 2.1.1", "rustls", "rustls-pki-types", "slab", - "thiserror 2.0.12", + "thiserror 2.0.17", "tinyvec", "tracing", "web-time", @@ -6106,32 +6526,32 @@ dependencies = [ [[package]] name = "quinn-udp" -version = "0.5.10" +version = "0.5.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e46f3055866785f6b92bc6164b76be02ca8f2eb4b002c0354b28cf4c119e5944" +checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd" dependencies = [ - "cfg_aliases", + "cfg_aliases 0.2.1", "libc", "once_cell", - "socket2 0.5.8", + "socket2 0.6.1", "tracing", - "windows-sys 0.59.0", + "windows-sys 0.60.2", ] [[package]] name = "quote" -version = "1.0.40" +version = "1.0.43" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" +checksum = "dc74d9a594b72ae6656596548f56f667211f8a97b3d4c3d467150794690dc40a" dependencies = [ "proc-macro2", ] [[package]] name = "r-efi" -version = "5.2.0" +version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" [[package]] name = "radium" @@ -6145,6 +6565,16 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" +[[package]] +name = "radix_trie" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c069c179fcdc6a2fe24d8d18305cf085fdbd4f922c041943e203685d6a1c58fd" +dependencies = [ + "endian-type", + "nibble_vec", +] + [[package]] name = "rand" version = "0.8.5" @@ -6158,13 +6588,12 @@ dependencies = [ [[package]] name = "rand" -version = "0.9.0" +version = "0.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3779b94aeb87e8bd4e834cee3650289ee9e0d5677f976ecdb6d219e5f4f6cd94" +checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" dependencies = [ "rand_chacha 0.9.0", - "rand_core 0.9.3", - "zerocopy 0.8.23", + "rand_core 0.9.5", ] [[package]] @@ -6184,7 +6613,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" dependencies = [ "ppv-lite86", - "rand_core 0.9.3", + "rand_core 0.9.5", ] [[package]] @@ -6193,16 +6622,16 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ - "getrandom 0.2.15", + "getrandom 0.2.17", ] [[package]] name = "rand_core" -version = "0.9.3" +version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" dependencies = [ - "getrandom 0.3.2", + "getrandom 0.3.4", ] [[package]] @@ -6214,6 +6643,15 @@ dependencies = [ "rand_core 0.6.4", ] +[[package]] +name = "rand_xorshift" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "513962919efc330f829edb2535844d1b912b0fbe2ca165d613e4e8788bb05a5a" +dependencies = [ + "rand_core 0.9.5", +] + [[package]] name = "rand_xoshiro" version = "0.6.0" @@ -6225,18 +6663,18 @@ dependencies = [ [[package]] name = "raw-cpuid" -version = "11.5.0" +version = "11.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6df7ab838ed27997ba19a4664507e6f82b41fe6e20be42929332156e5e85146" +checksum = "498cd0dc59d73224351ee52a95fee0f1a617a2eae0e7d9d720cc622c73a54186" dependencies = [ - "bitflags 2.9.0", + "bitflags 2.10.0", ] [[package]] name = "rayon" -version = "1.10.0" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa" +checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f" dependencies = [ "either", "rayon-core", @@ -6244,9 +6682,9 @@ dependencies = [ [[package]] name = "rayon-core" -version = "1.12.1" +version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2" +checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" dependencies = [ "crossbeam-deque", "crossbeam-utils", @@ -6273,33 +6711,44 @@ checksum = "f2a62d85ed81ca5305dc544bd42c8804c5060b78ffa5ad3c64b0fb6a8c13d062" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.114", ] [[package]] name = "real_tokio" -version = "1.43.0" -source = "git+https://github.com/MystenLabs/tokio-msim-fork.git?rev=7329bff6ee996d8df6cf810a9c2e59631ad5a2fb#7329bff6ee996d8df6cf810a9c2e59631ad5a2fb" +version = "1.47.1" +source = "git+https://github.com/MystenLabs/tokio-msim-fork.git?rev=c59702c3177a31405d42ec12e01fa4a445728326#c59702c3177a31405d42ec12e01fa4a445728326" dependencies = [ "backtrace", "bytes", + "io-uring", "libc", - "mio 1.0.3", + "mio 1.1.1", "parking_lot", "pin-project-lite", "signal-hook-registry", - "socket2 0.5.8", - "tokio-macros 2.5.0 (git+https://github.com/MystenLabs/tokio-msim-fork.git?rev=7329bff6ee996d8df6cf810a9c2e59631ad5a2fb)", - "windows-sys 0.52.0", + "slab", + "socket2 0.6.1", + "tokio-macros 2.5.0 (git+https://github.com/MystenLabs/tokio-msim-fork.git?rev=c59702c3177a31405d42ec12e01fa4a445728326)", + "windows-sys 0.59.0", ] [[package]] name = "redox_syscall" -version = "0.5.10" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags 2.10.0", +] + +[[package]] +name = "redox_syscall" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b8c0c260b63a8219631167be35e6a988e9554dbd323f8bd08439c8ed1302bd1" +checksum = "49f3fe0889e69e2ae9e41f4d6c4c0181701d00e4697b356fb1f74173a5e0ee27" dependencies = [ - "bitflags 2.9.0", + "bitflags 2.10.0", ] [[package]] @@ -6308,61 +6757,52 @@ version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" dependencies = [ - "getrandom 0.2.15", + "getrandom 0.2.17", "libredox", "thiserror 1.0.69", ] [[package]] name = "ref-cast" -version = "1.0.24" +version = "1.0.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a0ae411dbe946a674d89546582cea4ba2bb8defac896622d6496f14c23ba5cf" +checksum = "f354300ae66f76f1c85c5f84693f0ce81d747e2c3f21a45fef496d89c960bf7d" dependencies = [ "ref-cast-impl", ] [[package]] name = "ref-cast-impl" -version = "1.0.24" +version = "1.0.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1165225c21bff1f3bbce98f5a1f889949bc902d3575308cc7b0de30b4f6d27c7" +checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.114", ] [[package]] name = "regex" -version = "1.11.1" +version = "1.12.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" +checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4" dependencies = [ "aho-corasick", "memchr", - "regex-automata 0.4.9", - "regex-syntax 0.8.5", -] - -[[package]] -name = "regex-automata" -version = "0.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" -dependencies = [ - "regex-syntax 0.6.29", + "regex-automata", + "regex-syntax 0.8.8", ] [[package]] name = "regex-automata" -version = "0.4.9" +version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" +checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" dependencies = [ "aho-corasick", "memchr", - "regex-syntax 0.8.5", + "regex-syntax 0.8.8", ] [[package]] @@ -6379,61 +6819,52 @@ checksum = "dbb5fb1acd8a1a18b3dd5be62d25485eb770e05afb408a9627d14d451bae12da" [[package]] name = "regex-syntax" -version = "0.8.5" +version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" +checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" [[package]] name = "reqwest" -version = "0.12.15" +version = "0.12.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d19c46a6fdd48bc4dab94b6103fccc55d34c67cc0ad04653aad4ea2a07cd7bbb" +checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" dependencies = [ "base64 0.22.1", "bytes", - "encoding_rs", "futures-channel", "futures-core", "futures-util", - "h2 0.4.8", - "http 1.3.1", - "http-body 1.0.1", + "h2", + "http", + "http-body", "http-body-util", - "hyper 1.6.0", + "hyper", "hyper-rustls", - "hyper-tls", "hyper-util", - "ipnet", "js-sys", "log", - "mime", - "native-tls", - "once_cell", "percent-encoding", "pin-project-lite", "quinn", "rustls", "rustls-native-certs", - "rustls-pemfile", "rustls-pki-types", "serde", "serde_json", "serde_urlencoded", - "sync_wrapper 1.0.2", - "system-configuration", + "sync_wrapper", "tokio", - "tokio-native-tls", "tokio-rustls", - "tokio-util 0.7.14", - "tower 0.5.2", + "tokio-util 0.7.18", + "tower 0.5.3", + "tower-http 0.6.8", "tower-service", "url", "wasm-bindgen", "wasm-bindgen-futures", "wasm-streams", "web-sys", - "webpki-roots", - "windows-registry", + "webpki-roots 1.0.5", ] [[package]] @@ -6465,7 +6896,7 @@ checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" dependencies = [ "cc", "cfg-if", - "getrandom 0.2.15", + "getrandom 0.2.17", "libc", "untrusted", "windows-sys 0.52.0", @@ -6482,9 +6913,19 @@ dependencies = [ [[package]] name = "roaring" -version = "0.10.10" +version = "0.10.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19e8d2cfa184d94d0726d650a9f4a1be7f9b76ac9fdb954219878dc00c1c1e7b" +dependencies = [ + "bytemuck", + "byteorder", +] + +[[package]] +name = "roaring" +version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a652edd001c53df0b3f96a36a8dc93fce6866988efc16808235653c6bcac8bf2" +checksum = "8ba9ce64a8f45d7fc86358410bb1a82e8c987504c0d4900e9141d69a9f26c885" dependencies = [ "bytemuck", "byteorder", @@ -6492,9 +6933,9 @@ dependencies = [ [[package]] name = "rocksdb" -version = "0.21.0" +version = "0.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb6f170a4041d50a0ce04b0d2e14916d6ca863ea2e422689a5b694395d299ffe" +checksum = "6bd13e55d6d7b8cd0ea569161127567cd587676c99f4472f779a0279aa60a7a7" dependencies = [ "libc", "librocksdb-sys", @@ -6521,7 +6962,7 @@ dependencies = [ "pkcs1 0.4.1", "pkcs8 0.9.0", "rand_core 0.6.4", - "sha2 0.10.8", + "sha2 0.10.9", "signature 2.2.0", "subtle", "zeroize", @@ -6529,9 +6970,9 @@ dependencies = [ [[package]] name = "rsa" -version = "0.9.8" +version = "0.9.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78928ac1ed176a5ca1d17e578a1825f3d81ca54cf41053a592584b020cfd691b" +checksum = "b8573f03f5883dcaebdfcf4725caa1ecb9c15b2ef50c43a07b816e06799bb12d" dependencies = [ "const-oid", "digest 0.10.7", @@ -6549,9 +6990,9 @@ dependencies = [ [[package]] name = "rustc-demangle" -version = "0.1.24" +version = "0.1.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" +checksum = "b50b8869d9fc858ce7266cce0194bd74df58b9d0e3f6df3a9fc8eb470d95c09d" [[package]] name = "rustc-hash" @@ -6580,6 +7021,16 @@ dependencies = [ "semver", ] +[[package]] +name = "rustc_version_runtime" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dd18cd2bae1820af0b6ad5e54f4a51d0f3fcc53b05f845675074efcc7af071d" +dependencies = [ + "rustc_version", + "semver", +] + [[package]] name = "rusticata-macros" version = "4.1.0" @@ -6591,22 +7042,22 @@ dependencies = [ [[package]] name = "rustix" -version = "1.0.3" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e56a18552996ac8d29ecc3b190b4fdbb2d91ca4ec396de7bbffaf43f3d637e96" +checksum = "146c9e247ccc180c1f61615433868c99f3de3ae256a30a43b49f67c2d9171f34" dependencies = [ - "bitflags 2.9.0", + "bitflags 2.10.0", "errno", "libc", "linux-raw-sys", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] name = "rustls" -version = "0.23.25" +version = "0.23.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "822ee9188ac4ec04a2f0531e55d035fb2de73f18b41a63c70c2712503b6fb13c" +checksum = "c665f33d38cea657d9614f766881e4d510e0eda4239891eea56b4cadcf01801b" dependencies = [ "log", "once_cell", @@ -6619,14 +7070,14 @@ dependencies = [ [[package]] name = "rustls-native-certs" -version = "0.8.1" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fcff2dd52b58a8d98a70243663a0d234c4e2b79235637849d15913394a247d3" +checksum = "612460d5f7bea540c490b2b6395d8e34a953e52b491accd6c86c8164c5932a63" dependencies = [ "openssl-probe", "rustls-pki-types", "schannel", - "security-framework 3.2.0", + "security-framework", ] [[package]] @@ -6640,20 +7091,21 @@ dependencies = [ [[package]] name = "rustls-pki-types" -version = "1.11.0" +version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "917ce264624a4b4db1c364dcc35bfca9ded014d0a958cd47ad3e960e988ea51c" +checksum = "21e6f2ab2928ca4291b86736a8bd920a277a399bba1589409d72154ff87c1282" dependencies = [ "web-time", + "zeroize", ] [[package]] name = "rustls-platform-verifier" -version = "0.5.1" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a5467026f437b4cb2a533865eaa73eb840019a0916f4b9ec563c6e617e086c9" +checksum = "19787cda76408ec5404443dc8b31795c87cd8fec49762dc75fa727740d34acc1" dependencies = [ - "core-foundation 0.10.0", + "core-foundation", "core-foundation-sys", "jni", "log", @@ -6662,9 +7114,9 @@ dependencies = [ "rustls-native-certs", "rustls-platform-verifier-android", "rustls-webpki", - "security-framework 3.2.0", + "security-framework", "security-framework-sys", - "webpki-root-certs", + "webpki-root-certs 0.26.11", "windows-sys 0.59.0", ] @@ -6676,9 +7128,9 @@ checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f" [[package]] name = "rustls-webpki" -version = "0.103.0" +version = "0.103.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0aa4eeac2588ffff23e9d7a7e9b3f971c5fb5b7ebc9452745e0c232c64f83b2f" +checksum = "2ffdfa2f5286e2247234e03f680868ac2815974dc39e00ea15adc445d0aafe52" dependencies = [ "ring", "rustls-pki-types", @@ -6687,15 +7139,15 @@ dependencies = [ [[package]] name = "rustversion" -version = "1.0.20" +version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eded382c5f5f786b989652c49544c4877d9f015cc22e145a5ea8ea66c2921cd2" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" [[package]] name = "rusty-fork" -version = "0.3.0" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb3dcc6e454c328bb824492db107ab7c0ae8fcffe4ad210136ef014458c1bc4f" +checksum = "cc6bf79ff24e648f6da1f8d1f011e9cac26491b619e6b9280f2b47f1774e6ee2" dependencies = [ "fnv", "quick-error", @@ -6703,28 +7155,92 @@ dependencies = [ "wait-timeout", ] +[[package]] +name = "rustyline" +version = "14.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7803e8936da37efd9b6d4478277f4b2b9bb5cdb37a113e8d63222e58da647e63" +dependencies = [ + "bitflags 2.10.0", + "cfg-if", + "clipboard-win", + "fd-lock", + "home", + "libc", + "log", + "memchr", + "nix", + "radix_trie", + "unicode-segmentation", + "unicode-width 0.1.14", + "utf8parse", + "windows-sys 0.52.0", +] + [[package]] name = "ryu" -version = "1.0.20" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a50f4cf475b65d88e057964e0e9bb1f0aa9bbb2036dc65c64596b42932536984" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "schannel" +version = "0.1.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "891d81b926048e76efe18581bf793546b4c0eaf8448d72be8de2bbee5fd166e1" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "schemafy" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" +checksum = "8aea5ba40287dae331f2c48b64dbc8138541f5e97ee8793caa7948c1f31d86d5" +dependencies = [ + "Inflector", + "schemafy_core", + "schemafy_lib", + "serde", + "serde_derive", + "serde_json", + "serde_repr", + "syn 1.0.109", +] [[package]] -name = "same-file" -version = "1.0.6" +name = "schemafy_core" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +checksum = "41781ae092f4fd52c9287efb74456aea0d3b90032d2ecad272bd14dbbcb0511b" dependencies = [ - "winapi-util", + "serde", + "serde_json", ] [[package]] -name = "schannel" -version = "0.1.27" +name = "schemafy_lib" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f29ebaa345f945cec9fbbc532eb307f0fdad8161f281b6369539c8d84876b3d" +checksum = "e953db32579999ca98c451d80801b6f6a7ecba6127196c5387ec0774c528befa" dependencies = [ - "windows-sys 0.59.0", + "Inflector", + "proc-macro2", + "quote", + "schemafy_core", + "serde", + "serde_derive", + "serde_json", + "syn 1.0.109", ] [[package]] @@ -6740,6 +7256,30 @@ dependencies = [ "serde_json", ] +[[package]] +name = "schemars" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd191f9397d57d581cddd31014772520aa448f65ef991055d7f61582c65165f" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + +[[package]] +name = "schemars" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54e910108742c57a770f492731f99be216a52fadd361b06c8fb59d74ccc267d2" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + [[package]] name = "schemars_derive" version = "0.8.22" @@ -6749,7 +7289,7 @@ dependencies = [ "proc-macro2", "quote", "serde_derive_internals", - "syn 2.0.100", + "syn 2.0.114", ] [[package]] @@ -6761,12 +7301,6 @@ dependencies = [ "pin-project-lite", ] -[[package]] -name = "scoped-tls" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" - [[package]] name = "scopeguard" version = "1.2.0" @@ -6793,7 +7327,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" dependencies = [ "base16ct 0.2.0", - "der 0.7.9", + "der 0.7.10", "generic-array", "pkcs8 0.10.2", "subtle", @@ -6813,34 +7347,21 @@ dependencies = [ [[package]] name = "secp256k1-sys" -version = "0.8.1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70a129b9e9efbfb223753b9163c4ab3b13cff7fd9c7f010fbac25ab4099fa07e" +checksum = "4473013577ec77b4ee3668179ef1186df3146e2cf2d927bd200974c6fe60fd99" dependencies = [ "cc", ] [[package]] name = "security-framework" -version = "2.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" -dependencies = [ - "bitflags 2.9.0", - "core-foundation 0.9.4", - "core-foundation-sys", - "libc", - "security-framework-sys", -] - -[[package]] -name = "security-framework" -version = "3.2.0" +version = "3.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "271720403f46ca04f7ba6f55d438f8bd878d6b8ca0a1046e8228c4145bcbb316" +checksum = "b3297343eaf830f66ede390ea39da1d462b6b0c1b000f420d0a83f898bbbe6ef" dependencies = [ - "bitflags 2.9.0", - "core-foundation 0.10.0", + "bitflags 2.10.0", + "core-foundation", "core-foundation-sys", "libc", "security-framework-sys", @@ -6848,9 +7369,9 @@ dependencies = [ [[package]] name = "security-framework-sys" -version = "2.14.0" +version = "2.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49db231d56a190491cb4aeda9527f1ad45345af50b0851622a7adb8c03b01c32" +checksum = "cc1f0cbffaac4852523ce30d8bd3c5cdc873501d96ff467ca09b6767bb8cd5c0" dependencies = [ "core-foundation-sys", "libc", @@ -6858,19 +7379,29 @@ dependencies = [ [[package]] name = "semver" -version = "1.0.26" +version = "1.0.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56e6fa9c48d24d85fb3de5ad847117517440f6beceb7798af16b4a87d616b8d0" +checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" [[package]] name = "serde" -version = "1.0.219" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" dependencies = [ + "serde_core", "serde_derive", ] +[[package]] +name = "serde-big-array" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11fc7cc2c76d73e0f27ee52abbd64eec84d46f370c88371120433196934e4b7f" +dependencies = [ + "serde", +] + [[package]] name = "serde-env" version = "0.2.0" @@ -6893,35 +7424,46 @@ dependencies = [ [[package]] name = "serde-reflection" -version = "0.5.0" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5bef77b40d103fda6c10d29c21f5c78c980e8570e1a290a648a9ff5011f96e1" +checksum = "bfe23e63efbe7af1bc1859ead4a05014bdd5478be550762a40f6fcf91ad5473c" dependencies = [ "erased-discriminant", "once_cell", "serde", + "serde_json", "thiserror 1.0.69", "typeid", ] [[package]] name = "serde_bytes" -version = "0.11.17" +version = "0.11.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8437fd221bde2d4ca316d61b90e337e9e702b3820b87d63caa9ba6c02bd06d96" +checksum = "a5d440709e79d88e51ac01c4b72fc6cb7314017bb7da9eeff678aa94c10e3ea8" dependencies = [ "serde", + "serde_core", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.219" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.114", ] [[package]] @@ -6932,30 +7474,32 @@ checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.114", ] [[package]] name = "serde_json" -version = "1.0.140" +version = "1.0.149" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" dependencies = [ - "indexmap 2.8.0", + "indexmap 2.13.0", "itoa", "memchr", - "ryu", "serde", + "serde_core", + "zmij", ] [[package]] name = "serde_path_to_error" -version = "0.1.17" +version = "0.1.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59fab13f937fa393d08645bf3a84bdfe86e296747b506ada67bb15f10f218b2a" +checksum = "10a9ff822e371bb5403e391ecd83e182e0e77ba7f6fe0160b795797109d1b457" dependencies = [ "itoa", "serde", + "serde_core", ] [[package]] @@ -6966,16 +7510,16 @@ checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.114", ] [[package]] name = "serde_spanned" -version = "0.6.8" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1" +checksum = "f8bbf91e5a4d6315eee45e704372590b30e260ee83af6639d64557f51b067776" dependencies = [ - "serde", + "serde_core", ] [[package]] @@ -6992,17 +7536,18 @@ dependencies = [ [[package]] name = "serde_with" -version = "3.12.0" +version = "3.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6b6f7f2fcb69f747921f79f3926bd1e203fce4fef62c268dd3abfb6d86029aa" +checksum = "4fa237f2807440d238e0364a218270b98f767a00d3dada77b1c53ae88940e2e7" dependencies = [ "base64 0.22.1", "chrono", "hex", "indexmap 1.9.3", - "indexmap 2.8.0", - "serde", - "serde_derive", + "indexmap 2.13.0", + "schemars 0.9.0", + "schemars 1.2.0", + "serde_core", "serde_json", "serde_with_macros", "time", @@ -7010,14 +7555,14 @@ dependencies = [ [[package]] name = "serde_with_macros" -version = "3.12.0" +version = "3.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d00caa5193a3c8362ac2b73be6b9e768aa5a4b2f721d8f4b339600c3cb51f8e" +checksum = "52a8e3ca0ca629121f70ab50f95249e5a6f925cc0f6ffe8256c45b728875706c" dependencies = [ - "darling 0.20.10", + "darling 0.21.3", "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.114", ] [[package]] @@ -7058,9 +7603,9 @@ dependencies = [ [[package]] name = "sha2" -version = "0.10.8" +version = "0.10.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" dependencies = [ "cfg-if", "cpufeatures", @@ -7089,11 +7634,11 @@ dependencies = [ [[package]] name = "shared-crypto" version = "0.0.0" -source = "git+https://github.com/MystenLabs/sui.git?rev=88ba4e08e96ba1ab965c11ce1a915331dd3ed68d#88ba4e08e96ba1ab965c11ce1a915331dd3ed68d" +source = "git+https://github.com/MystenLabs/sui.git?branch=testnet#caeb21b13ef6e26cd8c5e48be520bcf0598d48c3" dependencies = [ "bcs", "eyre", - "fastcrypto 0.1.8", + "fastcrypto", "serde", "serde_repr", ] @@ -7106,9 +7651,9 @@ checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" [[package]] name = "signal-hook" -version = "0.3.17" +version = "0.3.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8621587d4798caf8eb44879d42e56b9a93ea5dcd315a6487c357130095b62801" +checksum = "d881a16cf4426aa584979d30bd82cb33429027e42122b169753d6ef1085ed6e2" dependencies = [ "libc", "signal-hook-registry", @@ -7116,9 +7661,9 @@ dependencies = [ [[package]] name = "signal-hook-mio" -version = "0.2.4" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34db1a06d485c9142248b7a054f034b349b212551f3dfd19c94d45a754a217cd" +checksum = "b75a19a7a740b25bc7944bdee6172368f988763b744e3d4dfe753f6b4ece40cc" dependencies = [ "libc", "mio 0.8.11", @@ -7127,10 +7672,11 @@ dependencies = [ [[package]] name = "signal-hook-registry" -version = "1.4.2" +version = "1.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" dependencies = [ + "errno", "libc", ] @@ -7154,6 +7700,12 @@ dependencies = [ "rand_core 0.6.4", ] +[[package]] +name = "simd-adler32" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2" + [[package]] name = "similar" version = "2.7.0" @@ -7184,12 +7736,9 @@ dependencies = [ [[package]] name = "slab" -version = "0.4.9" +version = "0.4.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" -dependencies = [ - "autocfg", -] +checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" [[package]] name = "slip10_ed25519" @@ -7202,32 +7751,32 @@ dependencies = [ [[package]] name = "smallvec" -version = "1.14.0" +version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fcf8323ef1faaee30a44a340193b1ac6814fd9b7b4e88e9d4519a3e4abe1cfd" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" dependencies = [ "serde", ] [[package]] name = "snafu" -version = "0.8.5" +version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "223891c85e2a29c3fe8fb900c1fae5e69c2e42415e3177752e8718475efa5019" +checksum = "6e84b3f4eacbf3a1ce05eac6763b4d629d60cbc94d632e4092c54ade71f1e1a2" dependencies = [ "snafu-derive", ] [[package]] name = "snafu-derive" -version = "0.8.5" +version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03c3c6b7927ffe7ecaa769ee0e3994da3b8cafc8f444578982c83ecb161af917" +checksum = "c1c97747dbf44bb1ca44a561ece23508e99cb592e862f22222dcf42f51d1e451" dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.114", ] [[package]] @@ -7248,14 +7797,24 @@ dependencies = [ [[package]] name = "socket2" -version = "0.5.8" +version = "0.5.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c970269d99b64e60ec3bd6ad27270092a5394c4e309314b18ae3fe575695fbe8" +checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678" dependencies = [ "libc", "windows-sys 0.52.0", ] +[[package]] +name = "socket2" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17129e116933cf371d018bb80ae557e889637989d8638274fb25622827b03881" +dependencies = [ + "libc", + "windows-sys 0.60.2", +] + [[package]] name = "soketto" version = "0.8.1" @@ -7265,7 +7824,7 @@ dependencies = [ "base64 0.22.1", "bytes", "futures", - "http 1.3.1", + "http", "httparse", "log", "rand 0.8.5", @@ -7307,14 +7866,14 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" dependencies = [ "base64ct", - "der 0.7.9", + "der 0.7.10", ] [[package]] name = "sqlx" -version = "0.8.3" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4410e73b3c0d8442c5f99b425d7a435b5ee0ae4167b3196771dd3f7a01be745f" +checksum = "1fefb893899429669dcdd979aff487bd78f4064e5e7907e4269081e0ef7d97dc" dependencies = [ "sqlx-core", "sqlx-macros", @@ -7325,10 +7884,12 @@ dependencies = [ [[package]] name = "sqlx-core" -version = "0.8.3" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a007b6936676aa9ab40207cde35daab0a04b823be8ae004368c0793b96a61e0" +checksum = "ee6798b1838b6a0f69c007c133b8df5866302197e404e8b6ee8ed3e3a5e68dc6" dependencies = [ + "base64 0.22.1", + "bigdecimal", "bytes", "chrono", "crc", @@ -7339,18 +7900,18 @@ dependencies = [ "futures-intrusive", "futures-io", "futures-util", - "hashbrown 0.15.2", + "hashbrown 0.15.5", "hashlink", - "indexmap 2.8.0", + "indexmap 2.13.0", "log", "memchr", "once_cell", "percent-encoding", "serde", "serde_json", - "sha2 0.10.8", + "sha2 0.10.9", "smallvec", - "thiserror 2.0.12", + "thiserror 2.0.17", "tokio", "tokio-stream", "tracing", @@ -7359,22 +7920,22 @@ dependencies = [ [[package]] name = "sqlx-macros" -version = "0.8.3" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3112e2ad78643fef903618d78cf0aec1cb3134b019730edb039b69eaf531f310" +checksum = "a2d452988ccaacfbf5e0bdbc348fb91d7c8af5bee192173ac3636b5fb6e6715d" dependencies = [ "proc-macro2", "quote", "sqlx-core", "sqlx-macros-core", - "syn 2.0.100", + "syn 2.0.114", ] [[package]] name = "sqlx-macros-core" -version = "0.8.3" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e9f90acc5ab146a99bf5061a7eb4976b573f560bc898ef3bf8435448dd5e7ad" +checksum = "19a9c1841124ac5a61741f96e1d9e2ec77424bf323962dd894bdb93f37d5219b" dependencies = [ "dotenvy", "either", @@ -7385,26 +7946,26 @@ dependencies = [ "quote", "serde", "serde_json", - "sha2 0.10.8", + "sha2 0.10.9", "sqlx-core", "sqlx-mysql", "sqlx-postgres", "sqlx-sqlite", - "syn 2.0.100", - "tempfile", + "syn 2.0.114", "tokio", "url", ] [[package]] name = "sqlx-mysql" -version = "0.8.3" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4560278f0e00ce64938540546f59f590d60beee33fffbd3b9cd47851e5fff233" +checksum = "aa003f0038df784eb8fecbbac13affe3da23b45194bd57dba231c8f48199c526" dependencies = [ "atoi", "base64 0.22.1", - "bitflags 2.9.0", + "bigdecimal", + "bitflags 2.10.0", "byteorder", "bytes", "chrono", @@ -7427,27 +7988,28 @@ dependencies = [ "once_cell", "percent-encoding", "rand 0.8.5", - "rsa 0.9.8", + "rsa 0.9.10", "serde", "sha1", - "sha2 0.10.8", + "sha2 0.10.9", "smallvec", "sqlx-core", "stringprep", - "thiserror 2.0.12", + "thiserror 2.0.17", "tracing", - "whoami", + "whoami 1.6.1", ] [[package]] name = "sqlx-postgres" -version = "0.8.3" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5b98a57f363ed6764d5b3a12bfedf62f07aa16e1856a7ddc2a0bb190a959613" +checksum = "db58fcd5a53cf07c184b154801ff91347e4c30d17a3562a635ff028ad5deda46" dependencies = [ "atoi", "base64 0.22.1", - "bitflags 2.9.0", + "bigdecimal", + "bitflags 2.10.0", "byteorder", "chrono", "crc", @@ -7464,24 +8026,25 @@ dependencies = [ "log", "md-5", "memchr", + "num-bigint 0.4.6", "once_cell", "rand 0.8.5", "serde", "serde_json", - "sha2 0.10.8", + "sha2 0.10.9", "smallvec", "sqlx-core", "stringprep", - "thiserror 2.0.12", + "thiserror 2.0.17", "tracing", - "whoami", + "whoami 1.6.1", ] [[package]] name = "sqlx-sqlite" -version = "0.8.3" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f85ca71d3a5b24e64e1d08dd8fe36c6c95c339a896cc33068148906784620540" +checksum = "c2d12fe70b2c1b4401038055f90f151b78208de1f9f89a7dbfd41587a10c3eea" dependencies = [ "atoi", "chrono", @@ -7497,21 +8060,22 @@ dependencies = [ "serde", "serde_urlencoded", "sqlx-core", + "thiserror 2.0.17", "tracing", "url", ] [[package]] name = "stable_deref_trait" -version = "1.2.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" [[package]] name = "stacker" -version = "0.1.20" +version = "0.1.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "601f9201feb9b09c00266478bf459952b9ef9a6b94edb2f21eba14ab681a60a9" +checksum = "e1f8b29fb42aafcea4edeeb6b2f2d7ecd0d969c48b4cf0d2e64aafc471dd6e59" dependencies = [ "cc", "cfg-if", @@ -7520,12 +8084,114 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "starlark" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f53849859f05d9db705b221bd92eede93877fd426c1b4a3c3061403a5912a8f" +dependencies = [ + "allocative", + "anyhow", + "bumpalo", + "cmp_any", + "debugserver-types", + "derivative", + "derive_more 1.0.0", + "display_container", + "dupe", + "either", + "erased-serde", + "hashbrown 0.14.5", + "inventory", + "itertools 0.13.0", + "maplit", + "memoffset", + "num-bigint 0.4.6", + "num-traits", + "once_cell", + "paste", + "ref-cast", + "regex", + "rustyline", + "serde", + "serde_json", + "starlark_derive", + "starlark_map", + "starlark_syntax", + "static_assertions", + "strsim 0.10.0", + "textwrap", + "thiserror 1.0.69", +] + +[[package]] +name = "starlark_derive" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe58bc6c8b7980a1fe4c9f8f48200c3212db42ebfe21ae6a0336385ab53f082a" +dependencies = [ + "dupe", + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "starlark_map" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92659970f120df0cc1c0bb220b33587b7a9a90e80d4eecc5c5af5debb950173d" +dependencies = [ + "allocative", + "dupe", + "equivalent", + "fxhash", + "hashbrown 0.14.5", + "serde", +] + +[[package]] +name = "starlark_syntax" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe53b3690d776aafd7cb6b9fed62d94f83280e3b87d88e3719cc0024638461b3" +dependencies = [ + "allocative", + "annotate-snippets", + "anyhow", + "derivative", + "derive_more 1.0.0", + "dupe", + "lalrpop", + "lalrpop-util", + "logos 0.12.1", + "lsp-types 0.94.1", + "memchr", + "num-bigint 0.4.6", + "num-traits", + "once_cell", + "starlark_map", + "thiserror 1.0.69", +] + [[package]] name = "static_assertions" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" +[[package]] +name = "string_cache" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf776ba3fa74f83bf4b63c3dcbbf82173db2632ed8452cb2d891d33f459de70f" +dependencies = [ + "new_debug_unreachable", + "parking_lot", + "phf_shared 0.11.3", + "precomputed-hash", +] + [[package]] name = "stringprep" version = "0.1.5" @@ -7549,15 +8215,6 @@ version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" -[[package]] -name = "strum" -version = "0.24.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "063e6045c0e62079840579a7e47a355ae92f60eb74daaf156fb1e84ba164e63f" -dependencies = [ - "strum_macros 0.24.3", -] - [[package]] name = "strum" version = "0.25.0" @@ -7569,21 +8226,11 @@ dependencies = [ [[package]] name = "strum" -version = "0.27.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f64def088c51c9510a8579e3c5d67c65349dcf755e5479ad3d010aa6454e2c32" - -[[package]] -name = "strum_macros" -version = "0.24.3" +version = "0.27.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e385be0d24f186b4ce2f9982191e7101bb737312ad61c1f2f984f34bcf85d59" +checksum = "af23d6f6c1a224baef9d3f61e287d2761385a5b88fdab4eb4c6f11aeb54c4bcf" dependencies = [ - "heck 0.4.1", - "proc-macro2", - "quote", - "rustversion", - "syn 1.0.109", + "strum_macros 0.27.2", ] [[package]] @@ -7596,20 +8243,19 @@ dependencies = [ "proc-macro2", "quote", "rustversion", - "syn 2.0.100", + "syn 2.0.114", ] [[package]] name = "strum_macros" -version = "0.27.1" +version = "0.27.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c77a8c5abcaf0f9ce05d62342b7d298c346515365c36b673df4ebe3ced01fde8" +checksum = "7695ce3845ea4b33927c055a39dc438a45b059f7c1b3d91d38d10355fb8cbca7" dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "rustversion", - "syn 2.0.100", + "syn 2.0.114", ] [[package]] @@ -7627,7 +8273,7 @@ checksum = "734676eb262c623cec13c3155096e08d1f8f29adce39ba17948b18dad1e54142" [[package]] name = "sui-config" version = "0.0.0" -source = "git+https://github.com/MystenLabs/sui.git?rev=88ba4e08e96ba1ab965c11ce1a915331dd3ed68d#88ba4e08e96ba1ab965c11ce1a915331dd3ed68d" +source = "git+https://github.com/MystenLabs/sui.git?branch=testnet#caeb21b13ef6e26cd8c5e48be520bcf0598d48c3" dependencies = [ "anemo", "anyhow", @@ -7636,7 +8282,7 @@ dependencies = [ "consensus-config", "csv", "dirs", - "fastcrypto 0.1.8", + "fastcrypto", "move-vm-config", "mysten-common", "nonzero_ext", @@ -7646,152 +8292,182 @@ dependencies = [ "rand 0.8.5", "reqwest", "serde", + "serde_json", "serde_with", "serde_yaml", + "starlark", "sui-keys", "sui-protocol-config", - "sui-rpc-api", "sui-types", + "thiserror 1.0.69", "tracing", ] [[package]] -name = "sui-data-ingestion-core" +name = "sui-crypto" version = "0.1.0" -source = "git+https://github.com/MystenLabs/sui.git?rev=88ba4e08e96ba1ab965c11ce1a915331dd3ed68d#88ba4e08e96ba1ab965c11ce1a915331dd3ed68d" +source = "git+https://github.com/MystenLabs/sui-rust-sdk.git?rev=339c2272fd5b8fb4e1fa6662cfa9acdbb0d05704#339c2272fd5b8fb4e1fa6662cfa9acdbb0d05704" dependencies = [ - "anyhow", - "async-trait", - "backoff", - "bcs", - "futures", - "mysten-metrics", - "notify", - "object_store", - "prometheus", + "ark-bn254", + "ark-ff", + "ark-groth16", + "ark-snark", + "ark-std", + "base64ct", + "bnum 0.13.0", + "ed25519-dalek", + "itertools 0.14.0", + "k256 0.13.4", + "p256", + "rand_core 0.6.4", "serde", + "serde_derive", "serde_json", - "sui-protocol-config", - "sui-rpc-api", - "sui-storage", - "sui-types", - "tap", - "tempfile", - "tokio", - "tokio-stream", - "tracing", - "url", + "sha2 0.10.9", + "signature 2.2.0", + "sui-sdk-types 0.1.1", ] [[package]] name = "sui-enum-compat-util" version = "0.1.0" -source = "git+https://github.com/MystenLabs/sui.git?rev=88ba4e08e96ba1ab965c11ce1a915331dd3ed68d#88ba4e08e96ba1ab965c11ce1a915331dd3ed68d" +source = "git+https://github.com/MystenLabs/sui.git?branch=testnet#caeb21b13ef6e26cd8c5e48be520bcf0598d48c3" dependencies = [ "serde_yaml", ] [[package]] name = "sui-field-count" -version = "1.45.2" -source = "git+https://github.com/MystenLabs/sui.git?rev=88ba4e08e96ba1ab965c11ce1a915331dd3ed68d#88ba4e08e96ba1ab965c11ce1a915331dd3ed68d" +version = "1.63.3" +source = "git+https://github.com/MystenLabs/sui.git?branch=testnet#caeb21b13ef6e26cd8c5e48be520bcf0598d48c3" dependencies = [ "sui-field-count-derive", ] [[package]] name = "sui-field-count-derive" -version = "1.45.2" -source = "git+https://github.com/MystenLabs/sui.git?rev=88ba4e08e96ba1ab965c11ce1a915331dd3ed68d#88ba4e08e96ba1ab965c11ce1a915331dd3ed68d" +version = "1.63.3" +source = "git+https://github.com/MystenLabs/sui.git?branch=testnet#caeb21b13ef6e26cd8c5e48be520bcf0598d48c3" dependencies = [ "quote", "syn 1.0.109", ] +[[package]] +name = "sui-futures" +version = "1.63.3" +source = "git+https://github.com/MystenLabs/sui.git?branch=testnet#caeb21b13ef6e26cd8c5e48be520bcf0598d48c3" +dependencies = [ + "anyhow", + "futures", + "tap", + "thiserror 1.0.69", + "tokio", + "tracing", +] + [[package]] name = "sui-http" version = "0.0.0" -source = "git+https://github.com/MystenLabs/sui.git?rev=88ba4e08e96ba1ab965c11ce1a915331dd3ed68d#88ba4e08e96ba1ab965c11ce1a915331dd3ed68d" +source = "git+https://github.com/MystenLabs/sui.git?branch=testnet#caeb21b13ef6e26cd8c5e48be520bcf0598d48c3" dependencies = [ "bytes", - "http 1.3.1", - "http-body 1.0.1", + "http", + "http-body", "http-body-util", - "hyper 1.6.0", + "hyper", "hyper-util", "pin-project-lite", - "socket2 0.5.8", + "socket2 0.5.10", "tokio", "tokio-rustls", - "tokio-util 0.7.14", - "tower 0.4.13", + "tokio-util 0.7.18", + "tower 0.5.3", "tracing", ] [[package]] name = "sui-indexer-alt-framework" -version = "1.45.2" -source = "git+https://github.com/MystenLabs/sui.git?rev=88ba4e08e96ba1ab965c11ce1a915331dd3ed68d#88ba4e08e96ba1ab965c11ce1a915331dd3ed68d" +version = "1.63.3" +source = "git+https://github.com/MystenLabs/sui.git?branch=testnet#caeb21b13ef6e26cd8c5e48be520bcf0598d48c3" dependencies = [ "anyhow", "async-trait", - "axum 0.7.9", + "axum 0.8.8", "backoff", "bb8", + "bytes", "chrono", "clap", "diesel", "diesel-async", "diesel_migrations", "futures", + "pin-project-lite", "prometheus", + "prost-types 0.14.3", "reqwest", + "scoped-futures", "serde", "sui-field-count", + "sui-futures", + "sui-indexer-alt-framework-store-traits", "sui-indexer-alt-metrics", "sui-pg-db", + "sui-rpc", "sui-rpc-api", - "sui-sql-macro", + "sui-sdk-types 0.1.1", "sui-storage", "sui-types", "tempfile", "thiserror 1.0.69", "tokio", "tokio-stream", - "tokio-util 0.7.14", - "tonic 0.12.3", + "tonic 0.14.2", "tracing", "tracing-subscriber", "url", ] +[[package]] +name = "sui-indexer-alt-framework-store-traits" +version = "1.63.3" +source = "git+https://github.com/MystenLabs/sui.git?branch=testnet#caeb21b13ef6e26cd8c5e48be520bcf0598d48c3" +dependencies = [ + "anyhow", + "async-trait", + "chrono", + "scoped-futures", +] + [[package]] name = "sui-indexer-alt-metrics" -version = "1.45.2" -source = "git+https://github.com/MystenLabs/sui.git?rev=88ba4e08e96ba1ab965c11ce1a915331dd3ed68d#88ba4e08e96ba1ab965c11ce1a915331dd3ed68d" +version = "1.63.3" +source = "git+https://github.com/MystenLabs/sui.git?branch=testnet#caeb21b13ef6e26cd8c5e48be520bcf0598d48c3" dependencies = [ "anyhow", - "axum 0.7.9", + "axum 0.8.8", "clap", "prometheus", + "prometheus-closure-metric", + "sui-futures", "sui-pg-db", "tokio", - "tokio-util 0.7.14", "tracing", ] [[package]] name = "sui-json" version = "0.0.0" -source = "git+https://github.com/MystenLabs/sui.git?rev=88ba4e08e96ba1ab965c11ce1a915331dd3ed68d#88ba4e08e96ba1ab965c11ce1a915331dd3ed68d" +source = "git+https://github.com/MystenLabs/sui.git?branch=testnet#caeb21b13ef6e26cd8c5e48be520bcf0598d48c3" dependencies = [ "anyhow", "bcs", - "fastcrypto 0.1.8", - "move-binary-format 0.0.3 (git+https://github.com/MystenLabs/sui.git?rev=88ba4e08e96ba1ab965c11ce1a915331dd3ed68d)", + "fastcrypto", + "move-binary-format", "move-bytecode-utils", - "move-core-types 0.0.4 (git+https://github.com/MystenLabs/sui.git?rev=88ba4e08e96ba1ab965c11ce1a915331dd3ed68d)", - "schemars", + "move-core-types", + "schemars 0.8.22", "serde", "serde_json", "sui-types", @@ -7800,10 +8476,10 @@ dependencies = [ [[package]] name = "sui-json-rpc-api" version = "0.0.0" -source = "git+https://github.com/MystenLabs/sui.git?rev=88ba4e08e96ba1ab965c11ce1a915331dd3ed68d#88ba4e08e96ba1ab965c11ce1a915331dd3ed68d" +source = "git+https://github.com/MystenLabs/sui.git?branch=testnet#caeb21b13ef6e26cd8c5e48be520bcf0598d48c3" dependencies = [ "anyhow", - "fastcrypto 0.1.8", + "fastcrypto", "jsonrpsee", "mysten-metrics", "once_cell", @@ -7820,22 +8496,24 @@ dependencies = [ [[package]] name = "sui-json-rpc-types" version = "0.0.0" -source = "git+https://github.com/MystenLabs/sui.git?rev=88ba4e08e96ba1ab965c11ce1a915331dd3ed68d#88ba4e08e96ba1ab965c11ce1a915331dd3ed68d" +source = "git+https://github.com/MystenLabs/sui.git?branch=testnet#caeb21b13ef6e26cd8c5e48be520bcf0598d48c3" dependencies = [ "anyhow", "bcs", "colored", "enum_dispatch", - "fastcrypto 0.1.8", + "fastcrypto", "itertools 0.13.0", "json_to_table", - "move-binary-format 0.0.3 (git+https://github.com/MystenLabs/sui.git?rev=88ba4e08e96ba1ab965c11ce1a915331dd3ed68d)", + "move-binary-format", "move-bytecode-utils", - "move-core-types 0.0.4 (git+https://github.com/MystenLabs/sui.git?rev=88ba4e08e96ba1ab965c11ce1a915331dd3ed68d)", + "move-command-line-common", + "move-core-types", "move-disassembler", "move-ir-types", "mysten-metrics", - "schemars", + "nonempty", + "schemars 0.8.22", "serde", "serde_json", "serde_with", @@ -7852,11 +8530,17 @@ dependencies = [ [[package]] name = "sui-keys" version = "0.0.0" -source = "git+https://github.com/MystenLabs/sui.git?rev=88ba4e08e96ba1ab965c11ce1a915331dd3ed68d#88ba4e08e96ba1ab965c11ce1a915331dd3ed68d" +source = "git+https://github.com/MystenLabs/sui.git?branch=testnet#caeb21b13ef6e26cd8c5e48be520bcf0598d48c3" dependencies = [ "anyhow", + "async-trait", + "base64 0.21.7", + "bcs", "bip32", - "fastcrypto 0.1.8", + "colored", + "fastcrypto", + "jsonrpc", + "mockall", "rand 0.8.5", "regex", "serde", @@ -7866,12 +8550,13 @@ dependencies = [ "slip10_ed25519", "sui-types", "tiny-bip39", + "tokio", ] [[package]] name = "sui-macros" version = "0.7.0" -source = "git+https://github.com/MystenLabs/sui.git?rev=88ba4e08e96ba1ab965c11ce1a915331dd3ed68d#88ba4e08e96ba1ab965c11ce1a915331dd3ed68d" +source = "git+https://github.com/MystenLabs/sui.git?branch=testnet#caeb21b13ef6e26cd8c5e48be520bcf0598d48c3" dependencies = [ "futures", "once_cell", @@ -7879,13 +8564,25 @@ dependencies = [ "tracing", ] +[[package]] +name = "sui-name-service" +version = "1.63.3" +source = "git+https://github.com/MystenLabs/sui.git?branch=testnet#caeb21b13ef6e26cd8c5e48be520bcf0598d48c3" +dependencies = [ + "bcs", + "move-core-types", + "serde", + "sui-types", + "thiserror 1.0.69", +] + [[package]] name = "sui-open-rpc" -version = "1.45.2" -source = "git+https://github.com/MystenLabs/sui.git?rev=88ba4e08e96ba1ab965c11ce1a915331dd3ed68d#88ba4e08e96ba1ab965c11ce1a915331dd3ed68d" +version = "1.63.3" +source = "git+https://github.com/MystenLabs/sui.git?branch=testnet#caeb21b13ef6e26cd8c5e48be520bcf0598d48c3" dependencies = [ "bcs", - "schemars", + "schemars 0.8.22", "serde", "serde_json", "versions", @@ -7894,7 +8591,7 @@ dependencies = [ [[package]] name = "sui-open-rpc-macros" version = "0.1.0" -source = "git+https://github.com/MystenLabs/sui.git?rev=88ba4e08e96ba1ab965c11ce1a915331dd3ed68d#88ba4e08e96ba1ab965c11ce1a915331dd3ed68d" +source = "git+https://github.com/MystenLabs/sui.git?branch=testnet#caeb21b13ef6e26cd8c5e48be520bcf0598d48c3" dependencies = [ "derive-syn-parse", "itertools 0.13.0", @@ -7907,17 +8604,16 @@ dependencies = [ [[package]] name = "sui-package-resolver" version = "0.1.0" -source = "git+https://github.com/MystenLabs/sui.git?rev=88ba4e08e96ba1ab965c11ce1a915331dd3ed68d#88ba4e08e96ba1ab965c11ce1a915331dd3ed68d" +source = "git+https://github.com/MystenLabs/sui.git?branch=testnet#caeb21b13ef6e26cd8c5e48be520bcf0598d48c3" dependencies = [ "async-trait", "bcs", "eyre", "lru", - "move-binary-format 0.0.3 (git+https://github.com/MystenLabs/sui.git?rev=88ba4e08e96ba1ab965c11ce1a915331dd3ed68d)", + "move-binary-format", "move-command-line-common", - "move-core-types 0.0.4 (git+https://github.com/MystenLabs/sui.git?rev=88ba4e08e96ba1ab965c11ce1a915331dd3ed68d)", + "move-core-types", "serde", - "sui-rpc-api", "sui-types", "thiserror 1.0.69", "tokio", @@ -7925,42 +8621,57 @@ dependencies = [ [[package]] name = "sui-pg-db" -version = "1.45.2" -source = "git+https://github.com/MystenLabs/sui.git?rev=88ba4e08e96ba1ab965c11ce1a915331dd3ed68d#88ba4e08e96ba1ab965c11ce1a915331dd3ed68d" +version = "1.63.3" +source = "git+https://github.com/MystenLabs/sui.git?branch=testnet#caeb21b13ef6e26cd8c5e48be520bcf0598d48c3" dependencies = [ "anyhow", + "async-trait", "bb8", + "chrono", "clap", "diesel", "diesel-async", "diesel_migrations", + "futures", + "rustls", + "rustls-pemfile", + "scoped-futures", + "sui-field-count", + "sui-indexer-alt-framework-store-traits", + "sui-sql-macro", "tempfile", "tokio", + "tokio-postgres", + "tokio-postgres-rustls", "tracing", "url", + "webpki-roots 0.26.11", ] [[package]] name = "sui-proc-macros" version = "0.7.0" -source = "git+https://github.com/MystenLabs/sui.git?rev=88ba4e08e96ba1ab965c11ce1a915331dd3ed68d#88ba4e08e96ba1ab965c11ce1a915331dd3ed68d" +source = "git+https://github.com/MystenLabs/sui.git?branch=testnet#caeb21b13ef6e26cd8c5e48be520bcf0598d48c3" dependencies = [ "msim-macros", "proc-macro2", "quote", "sui-enum-compat-util", - "syn 2.0.100", + "syn 2.0.114", ] [[package]] name = "sui-protocol-config" version = "0.1.0" -source = "git+https://github.com/MystenLabs/sui.git?rev=88ba4e08e96ba1ab965c11ce1a915331dd3ed68d#88ba4e08e96ba1ab965c11ce1a915331dd3ed68d" +source = "git+https://github.com/MystenLabs/sui.git?branch=testnet#caeb21b13ef6e26cd8c5e48be520bcf0598d48c3" dependencies = [ "clap", - "insta", + "fastcrypto", + "move-binary-format", + "move-core-types", "move-vm-config", - "schemars", + "mysten-common", + "schemars 0.8.22", "serde", "serde-env", "serde_with", @@ -7971,60 +8682,90 @@ dependencies = [ [[package]] name = "sui-protocol-config-macros" version = "0.1.0" -source = "git+https://github.com/MystenLabs/sui.git?rev=88ba4e08e96ba1ab965c11ce1a915331dd3ed68d#88ba4e08e96ba1ab965c11ce1a915331dd3ed68d" +source = "git+https://github.com/MystenLabs/sui.git?branch=testnet#caeb21b13ef6e26cd8c5e48be520bcf0598d48c3" dependencies = [ "proc-macro2", "quote", "syn 1.0.109", ] +[[package]] +name = "sui-rpc" +version = "0.1.1" +source = "git+https://github.com/MystenLabs/sui-rust-sdk.git?rev=339c2272fd5b8fb4e1fa6662cfa9acdbb0d05704#339c2272fd5b8fb4e1fa6662cfa9acdbb0d05704" +dependencies = [ + "base64 0.22.1", + "bcs", + "bytes", + "futures", + "http", + "prost 0.14.3", + "prost-types 0.14.3", + "serde", + "serde_json", + "sui-sdk-types 0.1.1", + "tap", + "tokio", + "tonic 0.14.2", + "tonic-prost", +] + [[package]] name = "sui-rpc-api" version = "0.1.0" -source = "git+https://github.com/MystenLabs/sui.git?rev=88ba4e08e96ba1ab965c11ce1a915331dd3ed68d#88ba4e08e96ba1ab965c11ce1a915331dd3ed68d" +source = "git+https://github.com/MystenLabs/sui.git?branch=testnet#caeb21b13ef6e26cd8c5e48be520bcf0598d48c3" dependencies = [ "anyhow", "async-stream", "async-trait", - "axum 0.7.9", + "axum 0.8.8", "base64 0.21.7", "bcs", "bytes", - "fastcrypto 0.1.8", - "http 1.3.1", + "fastcrypto", + "http", "itertools 0.13.0", "mime", - "move-binary-format 0.0.3 (git+https://github.com/MystenLabs/sui.git?rev=88ba4e08e96ba1ab965c11ce1a915331dd3ed68d)", - "move-core-types 0.0.4 (git+https://github.com/MystenLabs/sui.git?rev=88ba4e08e96ba1ab965c11ce1a915331dd3ed68d)", + "move-binary-format", + "move-core-types", "mysten-network", "prometheus", - "prost 0.13.5", - "prost-types 0.13.5", + "prost 0.14.3", + "prost-types 0.14.3", + "protox", "rand 0.8.5", - "roaring", + "roaring 0.10.12", "serde", "serde_json", "serde_with", + "sui-config", + "sui-crypto", + "sui-name-service", + "sui-package-resolver", "sui-protocol-config", - "sui-sdk-types 0.0.2 (git+https://github.com/MystenLabs/sui-rust-sdk.git?rev=5e11579031793f086178332219f5847ec94da0c4)", - "sui-transaction-builder 0.1.0 (git+https://github.com/MystenLabs/sui-rust-sdk.git?rev=5e11579031793f086178332219f5847ec94da0c4)", + "sui-rpc", + "sui-sdk-types 0.1.1", "sui-types", "tap", "thiserror 1.0.69", "tokio", "tokio-stream", - "tonic 0.12.3", + "tonic 0.14.2", "tonic-health", + "tonic-prost", + "tonic-prost-build", "tonic-reflection", - "tower 0.4.13", + "tonic-web", + "tower 0.5.3", "tracing", "url", + "walkdir", ] [[package]] name = "sui-sdk" -version = "1.45.2" -source = "git+https://github.com/MystenLabs/sui.git?rev=88ba4e08e96ba1ab965c11ce1a915331dd3ed68d#88ba4e08e96ba1ab965c11ce1a915331dd3ed68d" +version = "1.63.3" +source = "git+https://github.com/MystenLabs/sui.git?branch=testnet#caeb21b13ef6e26cd8c5e48be520bcf0598d48c3" dependencies = [ "anyhow", "async-trait", @@ -8032,11 +8773,12 @@ dependencies = [ "bcs", "clap", "colored", - "fastcrypto 0.1.8", + "fastcrypto", "futures", "futures-core", "jsonrpsee", - "move-core-types 0.0.4 (git+https://github.com/MystenLabs/sui.git?rev=88ba4e08e96ba1ab965c11ce1a915331dd3ed68d)", + "move-core-types", + "mysten-common", "reqwest", "serde", "serde_json", @@ -8056,46 +8798,50 @@ dependencies = [ [[package]] name = "sui-sdk-types" -version = "0.0.2" -source = "git+https://github.com/MystenLabs/sui-rust-sdk.git?rev=5e11579031793f086178332219f5847ec94da0c4#5e11579031793f086178332219f5847ec94da0c4" +version = "0.0.7" +source = "git+https://github.com/mystenlabs/sui-rust-sdk?rev=048124e484f14b9bf2a402227c9bc255c7621bc1#048124e484f14b9bf2a402227c9bc255c7621bc1" dependencies = [ "base64ct", "bcs", "blake2", - "bnum", + "bnum 0.12.1", "bs58 0.5.1", - "hex", - "roaring", + "bytes", + "bytestring", + "itertools 0.13.0", + "roaring 0.10.12", "serde", "serde_derive", "serde_json", "serde_with", - "winnow 0.7.4", + "winnow", ] [[package]] name = "sui-sdk-types" -version = "0.0.2" -source = "git+https://github.com/mystenlabs/sui-rust-sdk?rev=86a9e06#86a9e06cde84671e96e776d926a85fc1f229314d" +version = "0.1.1" +source = "git+https://github.com/MystenLabs/sui-rust-sdk.git?rev=339c2272fd5b8fb4e1fa6662cfa9acdbb0d05704#339c2272fd5b8fb4e1fa6662cfa9acdbb0d05704" dependencies = [ "base64ct", "bcs", "blake2", - "bnum", + "bnum 0.13.0", "bs58 0.5.1", - "hex", - "roaring", + "bytes", + "bytestring", + "itertools 0.14.0", + "roaring 0.11.3", "serde", "serde_derive", "serde_json", "serde_with", - "winnow 0.6.26", + "winnow", ] [[package]] name = "sui-sql-macro" -version = "1.45.2" -source = "git+https://github.com/MystenLabs/sui.git?rev=88ba4e08e96ba1ab965c11ce1a915331dd3ed68d#88ba4e08e96ba1ab965c11ce1a915331dd3ed68d" +version = "1.63.3" +source = "git+https://github.com/MystenLabs/sui.git?branch=testnet#caeb21b13ef6e26cd8c5e48be520bcf0598d48c3" dependencies = [ "quote", "syn 1.0.109", @@ -8105,7 +8851,7 @@ dependencies = [ [[package]] name = "sui-storage" version = "0.1.0" -source = "git+https://github.com/MystenLabs/sui.git?rev=88ba4e08e96ba1ab965c11ce1a915331dd3ed68d#88ba4e08e96ba1ab965c11ce1a915331dd3ed68d" +source = "git+https://github.com/MystenLabs/sui.git?branch=testnet#caeb21b13ef6e26cd8c5e48be520bcf0598d48c3" dependencies = [ "anyhow", "async-trait", @@ -8117,18 +8863,18 @@ dependencies = [ "chrono", "clap", "eyre", - "fastcrypto 0.1.8", + "fastcrypto", "futures", - "hyper 1.6.0", + "hyper", "hyper-rustls", "indicatif", "integer-encoding", "itertools 0.13.0", "lru", "moka", - "move-binary-format 0.0.3 (git+https://github.com/MystenLabs/sui.git?rev=88ba4e08e96ba1ab965c11ce1a915331dd3ed68d)", + "move-binary-format", "move-bytecode-utils", - "move-core-types 0.0.4 (git+https://github.com/MystenLabs/sui.git?rev=88ba4e08e96ba1ab965c11ce1a915331dd3ed68d)", + "move-core-types", "mysten-metrics", "num_enum", "object_store", @@ -8152,39 +8898,17 @@ dependencies = [ "zstd 0.12.4", ] -[[package]] -name = "sui-tls" -version = "0.0.0" -source = "git+https://github.com/MystenLabs/sui.git?rev=88ba4e08e96ba1ab965c11ce1a915331dd3ed68d#88ba4e08e96ba1ab965c11ce1a915331dd3ed68d" -dependencies = [ - "anyhow", - "arc-swap", - "axum 0.7.9", - "axum-server", - "ed25519", - "fastcrypto 0.1.8", - "pkcs8 0.10.2", - "rcgen", - "reqwest", - "rustls", - "rustls-webpki", - "tokio", - "tokio-rustls", - "tower-layer", - "x509-parser", -] - [[package]] name = "sui-transaction-builder" version = "0.0.0" -source = "git+https://github.com/MystenLabs/sui.git?rev=88ba4e08e96ba1ab965c11ce1a915331dd3ed68d#88ba4e08e96ba1ab965c11ce1a915331dd3ed68d" +source = "git+https://github.com/MystenLabs/sui.git?branch=testnet#caeb21b13ef6e26cd8c5e48be520bcf0598d48c3" dependencies = [ "anyhow", "async-trait", "bcs", "futures", - "move-binary-format 0.0.3 (git+https://github.com/MystenLabs/sui.git?rev=88ba4e08e96ba1ab965c11ce1a915331dd3ed68d)", - "move-core-types 0.0.4 (git+https://github.com/MystenLabs/sui.git?rev=88ba4e08e96ba1ab965c11ce1a915331dd3ed68d)", + "move-binary-format", + "move-core-types", "sui-json", "sui-json-rpc-types", "sui-protocol-config", @@ -8193,62 +8917,53 @@ dependencies = [ [[package]] name = "sui-transaction-builder" -version = "0.1.0" -source = "git+https://github.com/MystenLabs/sui-rust-sdk.git?rev=5e11579031793f086178332219f5847ec94da0c4#5e11579031793f086178332219f5847ec94da0c4" -dependencies = [ - "base64ct", - "bcs", - "serde", - "serde_json", - "serde_with", - "sui-sdk-types 0.0.2 (git+https://github.com/MystenLabs/sui-rust-sdk.git?rev=5e11579031793f086178332219f5847ec94da0c4)", - "thiserror 2.0.12", -] - -[[package]] -name = "sui-transaction-builder" -version = "0.1.0" -source = "git+https://github.com/mystenlabs/sui-rust-sdk?rev=86a9e06#86a9e06cde84671e96e776d926a85fc1f229314d" +version = "0.0.7" +source = "git+https://github.com/mystenlabs/sui-rust-sdk?rev=048124e484f14b9bf2a402227c9bc255c7621bc1#048124e484f14b9bf2a402227c9bc255c7621bc1" dependencies = [ "base64ct", "bcs", "serde", "serde_json", "serde_with", - "sui-sdk-types 0.0.2 (git+https://github.com/mystenlabs/sui-rust-sdk?rev=86a9e06)", - "thiserror 2.0.12", + "sui-sdk-types 0.0.7", + "thiserror 2.0.17", ] [[package]] name = "sui-types" version = "0.1.0" -source = "git+https://github.com/MystenLabs/sui.git?rev=88ba4e08e96ba1ab965c11ce1a915331dd3ed68d#88ba4e08e96ba1ab965c11ce1a915331dd3ed68d" +source = "git+https://github.com/MystenLabs/sui.git?branch=testnet#caeb21b13ef6e26cd8c5e48be520bcf0598d48c3" dependencies = [ "anemo", "anyhow", "async-trait", + "base64 0.21.7", "bcs", "better_any", "bincode", "byteorder", + "bytes", "chrono", "ciborium", "consensus-config", + "consensus-types", "derive_more 1.0.0", "enum_dispatch", "eyre", - "fastcrypto 0.1.8", + "fastcrypto", "fastcrypto-tbls", "fastcrypto-zkp", "im", - "indexmap 2.8.0", + "indexmap 2.13.0", "itertools 0.13.0", "lru", - "move-binary-format 0.0.3 (git+https://github.com/MystenLabs/sui.git?rev=88ba4e08e96ba1ab965c11ce1a915331dd3ed68d)", + "move-binary-format", "move-bytecode-utils", - "move-core-types 0.0.4 (git+https://github.com/MystenLabs/sui.git?rev=88ba4e08e96ba1ab965c11ce1a915331dd3ed68d)", + "move-core-types", + "move-trace-format", "move-vm-profiler", "move-vm-test-utils", + "mysten-common", "mysten-metrics", "mysten-network", "nonempty", @@ -8262,10 +8977,12 @@ dependencies = [ "prometheus", "proptest", "proptest-derive", + "prost 0.14.3", + "prost-types 0.14.3", "rand 0.8.5", - "roaring", + "roaring 0.10.12", "rustls-pemfile", - "schemars", + "schemars 0.8.22", "serde", "serde-name", "serde_json", @@ -8273,15 +8990,16 @@ dependencies = [ "shared-crypto", "signature 1.6.4", "static_assertions", - "strum 0.24.1", - "strum_macros 0.24.3", + "strum 0.27.2", + "strum_macros 0.27.2", "sui-enum-compat-util", "sui-macros", "sui-protocol-config", - "sui-sdk-types 0.0.2 (git+https://github.com/MystenLabs/sui-rust-sdk.git?rev=5e11579031793f086178332219f5847ec94da0c4)", + "sui-rpc", + "sui-sdk-types 0.1.1", "tap", "thiserror 1.0.69", - "tonic 0.12.3", + "tonic 0.14.2", "tracing", "typed-store-error", "x509-parser", @@ -8300,21 +9018,15 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.100" +version = "2.0.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b09a44accad81e1ba1cd74a32461ba89dee89095ba17b32f5d03683b1b1fc2a0" +checksum = "d4d107df263a3013ef9b1879b0df87d706ff80f65a86ea879bd9c31f9b307c2a" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] -[[package]] -name = "sync_wrapper" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" - [[package]] name = "sync_wrapper" version = "1.0.2" @@ -8338,34 +9050,13 @@ dependencies = [ [[package]] name = "synstructure" -version = "0.13.1" +version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", -] - -[[package]] -name = "system-configuration" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" -dependencies = [ - "bitflags 2.9.0", - "core-foundation 0.9.4", - "system-configuration-sys", -] - -[[package]] -name = "system-configuration-sys" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" -dependencies = [ - "core-foundation-sys", - "libc", + "syn 2.0.114", ] [[package]] @@ -8407,7 +9098,7 @@ checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" [[package]] name = "telemetry-subscribers" version = "0.2.0" -source = "git+https://github.com/MystenLabs/sui.git?rev=88ba4e08e96ba1ab965c11ce1a915331dd3ed68d#88ba4e08e96ba1ab965c11ce1a915331dd3ed68d" +source = "git+https://github.com/MystenLabs/sui.git?branch=testnet#caeb21b13ef6e26cd8c5e48be520bcf0598d48c3" dependencies = [ "atomic_float", "bytes", @@ -8433,15 +9124,26 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.19.0" +version = "3.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "488960f40a3fd53d72c2a29a58722561dee8afdd175bd88e3db4677d7b2ba600" +checksum = "655da9c7eb6305c55742045d5a8d2037996d61d8de95806335c7c86ce0f82e9c" dependencies = [ "fastrand", - "getrandom 0.3.2", + "getrandom 0.3.4", "once_cell", "rustix", - "windows-sys 0.59.0", + "windows-sys 0.61.2", +] + +[[package]] +name = "term" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c59df8ac95d96ff9bede18eb7300b0fda5e5d8d90960e76f8e14ae765eedbf1f" +dependencies = [ + "dirs-next", + "rustversion", + "winapi", ] [[package]] @@ -8455,12 +9157,27 @@ dependencies = [ [[package]] name = "terminal_size" -version = "0.4.2" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "45c6481c4829e4cc63825e62c49186a34538b7b2750b73b266581ffb612fb5ed" +checksum = "60b8cb979cb11c32ce1603f8137b22262a9d131aaa5c37b5678025f22b8becd0" dependencies = [ "rustix", - "windows-sys 0.59.0", + "windows-sys 0.60.2", +] + +[[package]] +name = "termtree" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f50febec83f5ee1df3015341d8bd429f2d1cc62bcba7ea2076759d315084683" + +[[package]] +name = "textwrap" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060" +dependencies = [ + "unicode-width 0.1.14", ] [[package]] @@ -8474,11 +9191,11 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.12" +version = "2.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" +checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" dependencies = [ - "thiserror-impl 2.0.12", + "thiserror-impl 2.0.17", ] [[package]] @@ -8489,28 +9206,27 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.114", ] [[package]] name = "thiserror-impl" -version = "2.0.12" +version = "2.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" +checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.114", ] [[package]] name = "thread_local" -version = "1.1.8" +version = "1.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c" +checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" dependencies = [ "cfg-if", - "once_cell", ] [[package]] @@ -8522,32 +9238,42 @@ dependencies = [ "num_cpus", ] +[[package]] +name = "tikv-jemalloc-sys" +version = "0.5.4+5.3.0-patched" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9402443cb8fd499b6f327e40565234ff34dbda27460c5b47db0db77443dd85d1" +dependencies = [ + "cc", + "libc", +] + [[package]] name = "time" -version = "0.3.40" +version = "0.3.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d9c75b47bdff86fa3334a3db91356b8d7d86a9b839dab7d0bdc5c3d3a077618" +checksum = "f9e442fc33d7fdb45aa9bfeb312c095964abdf596f7567261062b2a7107aaabd" dependencies = [ "deranged", "itoa", "num-conv", "powerfmt", - "serde", + "serde_core", "time-core", "time-macros", ] [[package]] name = "time-core" -version = "0.1.4" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9e9a38711f559d9e3ce1cdb06dd7c5b8ea546bc90052da6d06bb76da74bb07c" +checksum = "8b36ee98fd31ec7426d599183e8fe26932a8dc1fb76ddb6214d05493377d34ca" [[package]] name = "time-macros" -version = "0.2.21" +version = "0.2.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29aa485584182073ed57fd5004aa09c371f021325014694e432313345865fd04" +checksum = "71e552d1249bf61ac2a52db88179fd0673def1e1ad8243a00d9ec9ed71fee3dd" dependencies = [ "num-conv", "time-core", @@ -8565,18 +9291,27 @@ dependencies = [ "pbkdf2", "rand 0.8.5", "rustc-hash 1.1.0", - "sha2 0.10.8", + "sha2 0.10.9", "thiserror 1.0.69", "unicode-normalization", "wasm-bindgen", "zeroize", ] +[[package]] +name = "tiny-keccak" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237" +dependencies = [ + "crunchy", +] + [[package]] name = "tinystr" -version = "0.7.6" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f" +checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" dependencies = [ "displaydoc", "zerovec", @@ -8584,9 +9319,9 @@ dependencies = [ [[package]] name = "tinyvec" -version = "1.9.0" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09b3661f17e86524eccd4371ab0429194e0d7c008abb45f7a7495b1719463c71" +checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa" dependencies = [ "tinyvec_macros", ] @@ -8599,31 +9334,23 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.44.1" +version = "1.47.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f382da615b842244d4b8738c82ed1275e6c5dd90c459a30941cd07080b06c91a" +checksum = "89e49afdadebb872d3145a5638b59eb0691ea23e46ca484037cfab3b76b95038" dependencies = [ "backtrace", "bytes", + "io-uring", "libc", - "mio 1.0.3", + "mio 1.1.1", "parking_lot", "pin-project-lite", "signal-hook-registry", - "socket2 0.5.8", + "slab", + "socket2 0.6.1", "tokio-macros 2.5.0 (registry+https://github.com/rust-lang/crates.io-index)", "tracing", - "windows-sys 0.52.0", -] - -[[package]] -name = "tokio-io-timeout" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30b74022ada614a1b4834de765f9bb43877f910cc8ce4be40e89042c9223a8bf" -dependencies = [ - "pin-project-lite", - "tokio", + "windows-sys 0.59.0", ] [[package]] @@ -8634,34 +9361,24 @@ checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.114", ] [[package]] name = "tokio-macros" version = "2.5.0" -source = "git+https://github.com/MystenLabs/tokio-msim-fork.git?rev=7329bff6ee996d8df6cf810a9c2e59631ad5a2fb#7329bff6ee996d8df6cf810a9c2e59631ad5a2fb" +source = "git+https://github.com/MystenLabs/tokio-msim-fork.git?rev=c59702c3177a31405d42ec12e01fa4a445728326#c59702c3177a31405d42ec12e01fa4a445728326" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", -] - -[[package]] -name = "tokio-native-tls" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" -dependencies = [ - "native-tls", - "tokio", + "syn 2.0.114", ] [[package]] name = "tokio-postgres" -version = "0.7.13" +version = "0.7.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c95d533c83082bb6490e0189acaa0bbeef9084e60471b696ca6988cd0541fb0" +checksum = "dcea47c8f71744367793f16c2db1f11cb859d28f436bdb4ca9193eb1f787ee42" dependencies = [ "async-trait", "byteorder", @@ -8672,22 +9389,36 @@ dependencies = [ "log", "parking_lot", "percent-encoding", - "phf", + "phf 0.13.1", "pin-project-lite", "postgres-protocol", "postgres-types", - "rand 0.9.0", - "socket2 0.5.8", + "rand 0.9.2", + "socket2 0.6.1", + "tokio", + "tokio-util 0.7.18", + "whoami 2.0.2", +] + +[[package]] +name = "tokio-postgres-rustls" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04fb792ccd6bbcd4bba408eb8a292f70fc4a3589e5d793626f45190e6454b6ab" +dependencies = [ + "ring", + "rustls", "tokio", - "tokio-util 0.7.14", - "whoami", + "tokio-postgres", + "tokio-rustls", + "x509-certificate", ] [[package]] name = "tokio-rustls" -version = "0.26.2" +version = "0.26.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e727b36a1a0e8b74c376ac2211e40c2c8af09fb4013c60d910495810f008e9b" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" dependencies = [ "rustls", "tokio", @@ -8695,21 +9426,21 @@ dependencies = [ [[package]] name = "tokio-stream" -version = "0.1.17" +version = "0.1.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eca58d7bba4a75707817a2c44174253f9236b2d5fbd055602e9d5c07c139a047" +checksum = "32da49809aab5c3bc678af03902d4ccddea2a87d028d86392a4b1560c6906c70" dependencies = [ "futures-core", "pin-project-lite", "tokio", - "tokio-util 0.7.14", + "tokio-util 0.7.18", ] [[package]] name = "tokio-tungstenite" -version = "0.24.0" +version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edc5f74e248dc973e0dbb7b74c7e0d6fcc301c694ff50049504004ef4d0cdcd9" +checksum = "d25a406cddcc431a75d3d9afc6a7c0f7428d4891dd973e4d54c56b46127bf857" dependencies = [ "futures-util", "log", @@ -8719,15 +9450,15 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.13" -source = "git+https://github.com/MystenLabs/tokio-msim-fork.git?rev=7329bff6ee996d8df6cf810a9c2e59631ad5a2fb#7329bff6ee996d8df6cf810a9c2e59631ad5a2fb" +version = "0.7.15" +source = "git+https://github.com/MystenLabs/tokio-msim-fork.git?rev=c59702c3177a31405d42ec12e01fa4a445728326#c59702c3177a31405d42ec12e01fa4a445728326" dependencies = [ "bytes", "futures-core", "futures-io", "futures-sink", "futures-util", - "hashbrown 0.14.5", + "hashbrown 0.15.5", "pin-project-lite", "real_tokio", "slab", @@ -8735,9 +9466,9 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.14" +version = "0.7.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b9590b93e6fcc1739458317cccd391ad3955e2bde8913edf6f95f9e65a8f034" +checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" dependencies = [ "bytes", "futures-core", @@ -8758,57 +9489,77 @@ dependencies = [ [[package]] name = "toml" -version = "0.8.20" +version = "0.9.11+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd87a5cdd6ffab733b2f74bc4fd7ee5fff6634124999ac278c35fc78c6120148" +checksum = "f3afc9a848309fe1aaffaed6e1546a7a14de1f935dc9d89d32afd9a44bab7c46" dependencies = [ - "serde", + "indexmap 2.13.0", + "serde_core", "serde_spanned", "toml_datetime", - "toml_edit", + "toml_parser", + "toml_writer", + "winnow", ] [[package]] name = "toml_datetime" -version = "0.6.8" +version = "0.7.5+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" +checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347" dependencies = [ - "serde", + "serde_core", ] [[package]] name = "toml_edit" -version = "0.22.24" +version = "0.23.10+spec-1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17b4795ff5edd201c7cd6dca065ae59972ce77d1b80fa0a84d94950ece7d1474" +checksum = "84c8b9f757e028cee9fa244aea147aab2a9ec09d5325a9b01e0a49730c2b5269" dependencies = [ - "indexmap 2.8.0", - "serde", - "serde_spanned", + "indexmap 2.13.0", "toml_datetime", - "winnow 0.7.4", + "toml_parser", + "winnow", ] +[[package]] +name = "toml_parser" +version = "1.0.6+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3198b4b0a8e11f09dd03e133c0280504d0801269e9afa46362ffde1cbeebf44" +dependencies = [ + "winnow", +] + +[[package]] +name = "toml_writer" +version = "1.0.6+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab16f14aed21ee8bfd8ec22513f7287cd4a91aa92e44edfe2c17ddd004e92607" + [[package]] name = "tonic" -version = "0.10.2" +version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d560933a0de61cf715926b9cac824d4c883c2c43142f787595e48280c40a1d0e" +checksum = "877c5b330756d856ffcc4553ab34a5684481ade925ecc54bcd1bf02b1d0d4d52" dependencies = [ "async-stream", "async-trait", - "axum 0.6.20", - "base64 0.21.7", + "axum 0.7.9", + "base64 0.22.1", "bytes", - "h2 0.3.26", - "http 0.2.12", - "http-body 0.4.6", - "hyper 0.14.32", - "hyper-timeout 0.4.1", + "h2", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-timeout", + "hyper-util", "percent-encoding", "pin-project", - "prost 0.12.6", + "prost 0.13.5", + "socket2 0.5.10", "tokio", "tokio-stream", "tower 0.4.13", @@ -8819,62 +9570,118 @@ dependencies = [ [[package]] name = "tonic" -version = "0.12.3" +version = "0.14.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "877c5b330756d856ffcc4553ab34a5684481ade925ecc54bcd1bf02b1d0d4d52" +checksum = "eb7613188ce9f7df5bfe185db26c5814347d110db17920415cf2fbcad85e7203" dependencies = [ - "async-stream", "async-trait", - "axum 0.7.9", + "axum 0.8.8", "base64 0.22.1", "bytes", - "h2 0.4.8", - "http 1.3.1", - "http-body 1.0.1", + "h2", + "http", + "http-body", "http-body-util", - "hyper 1.6.0", - "hyper-timeout 0.5.2", + "hyper", + "hyper-timeout", "hyper-util", "percent-encoding", "pin-project", - "prost 0.13.5", - "rustls-pemfile", - "socket2 0.5.8", + "socket2 0.6.1", + "sync_wrapper", "tokio", "tokio-rustls", "tokio-stream", - "tower 0.4.13", + "tower 0.5.3", "tower-layer", "tower-service", "tracing", - "webpki-roots", + "webpki-roots 1.0.5", "zstd 0.13.3", ] [[package]] -name = "tonic-health" -version = "0.12.3" +name = "tonic-build" +version = "0.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c40aaccc9f9eccf2cd82ebc111adc13030d23e887244bc9cfa5d1d636049de3" +dependencies = [ + "prettyplease", + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "tonic-health" +version = "0.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a82868bf299e0a1d2e8dce0dc33a46c02d6f045b2c1f1d6cc8dc3d0bf1812ef" +dependencies = [ + "prost 0.14.3", + "tokio", + "tokio-stream", + "tonic 0.14.2", + "tonic-prost", +] + +[[package]] +name = "tonic-prost" +version = "0.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66bd50ad6ce1252d87ef024b3d64fe4c3cf54a86fb9ef4c631fdd0ded7aeaa67" +dependencies = [ + "bytes", + "prost 0.14.3", + "tonic 0.14.2", +] + +[[package]] +name = "tonic-prost-build" +version = "0.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4a16cba4043dc3ff43fcb3f96b4c5c154c64cbd18ca8dce2ab2c6a451d058a2" +dependencies = [ + "prettyplease", + "proc-macro2", + "prost-build", + "prost-types 0.14.3", + "quote", + "syn 2.0.114", + "tempfile", + "tonic-build", +] + +[[package]] +name = "tonic-reflection" +version = "0.14.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1eaf34ddb812120f5c601162d5429933c9b527d901ab0e7f930d3147e33a09b2" +checksum = "34da53e8387581d66db16ff01f98a70b426b091fdf76856e289d5c1bd386ed7b" dependencies = [ - "async-stream", - "prost 0.13.5", + "prost 0.14.3", + "prost-types 0.14.3", "tokio", "tokio-stream", - "tonic 0.12.3", + "tonic 0.14.2", + "tonic-prost", ] [[package]] -name = "tonic-reflection" -version = "0.12.3" +name = "tonic-web" +version = "0.14.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "878d81f52e7fcfd80026b7fdb6a9b578b3c3653ba987f87f0dce4b64043cba27" +checksum = "75214f6b6bd28c19aa752ac09fdf0eea546095670906c21fe3940e180a4c43f2" dependencies = [ - "prost 0.13.5", - "prost-types 0.13.5", - "tokio", + "base64 0.22.1", + "bytes", + "http", + "http-body", + "pin-project", "tokio-stream", - "tonic 0.12.3", + "tonic 0.14.2", + "tower-layer", + "tower-service", + "tracing", ] [[package]] @@ -8892,7 +9699,7 @@ dependencies = [ "rand 0.8.5", "slab", "tokio", - "tokio-util 0.7.14", + "tokio-util 0.7.18", "tower-layer", "tower-service", "tracing", @@ -8900,15 +9707,19 @@ dependencies = [ [[package]] name = "tower" -version = "0.5.2" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" dependencies = [ "futures-core", "futures-util", + "hdrhistogram", + "indexmap 2.13.0", "pin-project-lite", - "sync_wrapper 1.0.2", + "slab", + "sync_wrapper", "tokio", + "tokio-util 0.7.18", "tower-layer", "tower-service", "tracing", @@ -8922,12 +9733,12 @@ checksum = "1e9cd434a998747dd2c4276bc96ee2e0c7a2eadf3cae88e52be55a05fa9053f5" dependencies = [ "async-compression", "base64 0.21.7", - "bitflags 2.9.0", + "bitflags 2.10.0", "bytes", "futures-core", "futures-util", - "http 1.3.1", - "http-body 1.0.1", + "http", + "http-body", "http-body-util", "http-range-header", "httpdate", @@ -8937,7 +9748,7 @@ dependencies = [ "percent-encoding", "pin-project-lite", "tokio", - "tokio-util 0.7.14", + "tokio-util 0.7.18", "tower 0.4.13", "tower-layer", "tower-service", @@ -8945,6 +9756,24 @@ dependencies = [ "uuid", ] +[[package]] +name = "tower-http" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" +dependencies = [ + "bitflags 2.10.0", + "bytes", + "futures-util", + "http", + "http-body", + "iri-string", + "pin-project-lite", + "tower 0.5.3", + "tower-layer", + "tower-service", +] + [[package]] name = "tower-layer" version = "0.3.3" @@ -8959,9 +9788,9 @@ checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" [[package]] name = "tracing" -version = "0.1.41" +version = "0.1.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" dependencies = [ "log", "pin-project-lite", @@ -8971,32 +9800,32 @@ dependencies = [ [[package]] name = "tracing-appender" -version = "0.2.3" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3566e8ce28cc0a3fe42519fc80e6b4c943cc4c8cef275620eb8dac2d3d4e06cf" +checksum = "786d480bce6247ab75f005b14ae1624ad978d3029d9113f0a22fa1ac773faeaf" dependencies = [ "crossbeam-channel", - "thiserror 1.0.69", + "thiserror 2.0.17", "time", "tracing-subscriber", ] [[package]] name = "tracing-attributes" -version = "0.1.28" +version = "0.1.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.114", ] [[package]] name = "tracing-core" -version = "0.1.33" +version = "0.1.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" dependencies = [ "once_cell", "valuable", @@ -9043,14 +9872,14 @@ dependencies = [ [[package]] name = "tracing-subscriber" -version = "0.3.19" +version = "0.3.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008" +checksum = "2f30143827ddab0d256fd843b7a66d164e9f271cfa0dde49142c5ca0ca291f1e" dependencies = [ "matchers", "nu-ansi-term", "once_cell", - "regex", + "regex-automata", "serde", "serde_json", "sharded-slab", @@ -9082,36 +9911,40 @@ checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" [[package]] name = "tungstenite" -version = "0.24.0" +version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18e5b8366ee7a95b16d32197d0b2604b43a0be89dc5fac9f8e96ccafbaedda8a" +checksum = "8628dcc84e5a09eb3d8423d6cb682965dea9133204e8fb3efee74c2a0c259442" dependencies = [ - "byteorder", "bytes", "data-encoding", - "http 1.3.1", + "http", "httparse", "log", - "rand 0.8.5", + "rand 0.9.2", "sha1", - "thiserror 1.0.69", + "thiserror 2.0.17", "utf-8", ] [[package]] name = "typed-store" version = "0.4.0" -source = "git+https://github.com/MystenLabs/sui.git?rev=88ba4e08e96ba1ab965c11ce1a915331dd3ed68d#88ba4e08e96ba1ab965c11ce1a915331dd3ed68d" +source = "git+https://github.com/MystenLabs/sui.git?branch=testnet#caeb21b13ef6e26cd8c5e48be520bcf0598d48c3" dependencies = [ + "anyhow", "async-trait", + "backoff", "bcs", "bincode", "collectable", "eyre", + "fastcrypto", "fdlimit", "hdrhistogram", "itertools 0.13.0", "msim", + "mysten-common", + "mysten-metrics", "once_cell", "prometheus", "rand 0.8.5", @@ -9130,7 +9963,7 @@ dependencies = [ [[package]] name = "typed-store-derive" version = "0.3.0" -source = "git+https://github.com/MystenLabs/sui.git?rev=88ba4e08e96ba1ab965c11ce1a915331dd3ed68d#88ba4e08e96ba1ab965c11ce1a915331dd3ed68d" +source = "git+https://github.com/MystenLabs/sui.git?branch=testnet#caeb21b13ef6e26cd8c5e48be520bcf0598d48c3" dependencies = [ "itertools 0.13.0", "proc-macro2", @@ -9141,7 +9974,7 @@ dependencies = [ [[package]] name = "typed-store-error" version = "0.4.0" -source = "git+https://github.com/MystenLabs/sui.git?rev=88ba4e08e96ba1ab965c11ce1a915331dd3ed68d#88ba4e08e96ba1ab965c11ce1a915331dd3ed68d" +source = "git+https://github.com/MystenLabs/sui.git?branch=testnet#caeb21b13ef6e26cd8c5e48be520bcf0598d48c3" dependencies = [ "serde", "thiserror 1.0.69", @@ -9150,7 +9983,7 @@ dependencies = [ [[package]] name = "typed-store-workspace-hack" version = "0.0.0" -source = "git+https://github.com/MystenLabs/sui.git?rev=88ba4e08e96ba1ab965c11ce1a915331dd3ed68d#88ba4e08e96ba1ab965c11ce1a915331dd3ed68d" +source = "git+https://github.com/MystenLabs/sui.git?branch=testnet#caeb21b13ef6e26cd8c5e48be520bcf0598d48c3" dependencies = [ "cc", "lazy_static", @@ -9161,7 +9994,7 @@ dependencies = [ "quote", "regex", "regex-syntax 0.7.5", - "syn 2.0.100", + "syn 2.0.114", "zstd-sys", ] @@ -9173,15 +10006,15 @@ checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c" [[package]] name = "typenum" -version = "1.18.0" +version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f" +checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" [[package]] name = "typeshare" -version = "1.0.4" +version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19be0f411120091e76e13e5a0186d8e2bcc3e7e244afdb70152197f1a8486ceb" +checksum = "da1bf9fe204f358ffea7f8f779b53923a20278b3ab8e8d97962c5e1b3a54edb7" dependencies = [ "chrono", "serde", @@ -9191,20 +10024,14 @@ dependencies = [ [[package]] name = "typeshare-annotation" -version = "1.0.4" +version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a615d6c2764852a2e88a4f16e9ce1ea49bb776b5872956309e170d63a042a34f" +checksum = "621963e302416b389a1ec177397e9e62de849a78bd8205d428608553def75350" dependencies = [ "quote", - "syn 2.0.100", + "syn 2.0.114", ] -[[package]] -name = "ucd-trie" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" - [[package]] name = "uint" version = "0.9.5" @@ -9231,9 +10058,9 @@ checksum = "ccb97dac3243214f8d8507998906ca3e2e0b900bf9bf4870477f125b82e68f6e" [[package]] name = "unicase" -version = "2.8.1" +version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539" +checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142" [[package]] name = "unicode-bidi" @@ -9243,24 +10070,24 @@ checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" [[package]] name = "unicode-ident" -version = "1.0.18" +version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" +checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" [[package]] name = "unicode-normalization" -version = "0.1.24" +version = "0.1.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5033c97c4262335cded6d6fc3e5c18ab755e1a3dc96376350f3d8e9f009ad956" +checksum = "5fd4f6878c9cb28d874b009da9e8d183b5abc80117c40bbd187a1fde336be6e8" dependencies = [ "tinyvec", ] [[package]] name = "unicode-properties" -version = "0.1.3" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e70f2a8b45122e719eb623c01822704c4e0907e7e426a05927e1a1cfff5b75d0" +checksum = "7df058c713841ad818f1dc5d3fd88063241cc61f49f5fbea4b951e8cf5a8d71d" [[package]] name = "unicode-segmentation" @@ -9276,9 +10103,9 @@ checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" [[package]] name = "unicode-width" -version = "0.2.0" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd" +checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" [[package]] name = "unicode-xid" @@ -9286,6 +10113,12 @@ version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" +[[package]] +name = "unit-prefix" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81e544489bf3d8ef66c953931f56617f423cd4b5494be343d9b9d3dda037b9a3" + [[package]] name = "universal-hash" version = "0.5.1" @@ -9310,14 +10143,15 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" [[package]] name = "url" -version = "2.5.4" +version = "2.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" dependencies = [ "form_urlencoded", "idna", "percent-encoding", "serde", + "serde_derive", ] [[package]] @@ -9326,12 +10160,6 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" -[[package]] -name = "utf16_iter" -version = "1.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246" - [[package]] name = "utf8_iter" version = "1.0.4" @@ -9346,13 +10174,14 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "uuid" -version = "1.16.0" +version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "458f7a779bf54acc9f347480ac654f68407d3aab21269a6e3c9f922acd9e2da9" +checksum = "e2e054861b4bd027cd373e18e8d8d8e6548085000e41290d95ce0c373a654b4a" dependencies = [ - "getrandom 0.3.2", - "rand 0.9.0", - "serde", + "getrandom 0.3.4", + "js-sys", + "rand 0.9.2", + "wasm-bindgen", ] [[package]] @@ -9363,12 +10192,13 @@ checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" [[package]] name = "variant_count" -version = "1.1.0" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aae2faf80ac463422992abf4de234731279c058aaf33171ca70277c98406b124" +checksum = "a1935e10c6f04d22688d07c0790f2fc0e1b1c5c2c55bc0cc87ed67656e587dd8" dependencies = [ + "proc-macro2", "quote", - "syn 1.0.109", + "syn 2.0.114", ] [[package]] @@ -9429,17 +10259,26 @@ dependencies = [ [[package]] name = "wasi" -version = "0.11.0+wasi-snapshot-preview1" +version = "0.11.1+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] name = "wasi" -version = "0.14.2+wasi-0.2.4" +version = "0.14.7+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "883478de20367e224c0090af9cf5f9fa85bed63a95c1abf3afc5c083ebc06e8c" +dependencies = [ + "wasip2", +] + +[[package]] +name = "wasip2" +version = "1.0.2+wasi-0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" +checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" dependencies = [ - "wit-bindgen-rt", + "wit-bindgen", ] [[package]] @@ -9449,38 +10288,35 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b" [[package]] -name = "wasm-bindgen" -version = "0.2.100" +name = "wasite" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" +checksum = "66fe902b4a6b8028a753d5424909b764ccf79b7a209eac9bf97e59cda9f71a42" dependencies = [ - "cfg-if", - "once_cell", - "rustversion", - "wasm-bindgen-macro", + "wasi 0.14.7+wasi-0.2.4", ] [[package]] -name = "wasm-bindgen-backend" -version = "0.2.100" +name = "wasm-bindgen" +version = "0.2.108" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" +checksum = "64024a30ec1e37399cf85a7ffefebdb72205ca1c972291c51512360d90bd8566" dependencies = [ - "bumpalo", - "log", - "proc-macro2", - "quote", - "syn 2.0.100", + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-futures" -version = "0.4.50" +version = "0.4.58" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "555d470ec0bc3bb57890405e5d4322cc9ea83cebb085523ced7be4144dac1e61" +checksum = "70a6e77fd0ae8029c9ea0063f87c46fde723e7d887703d74ad2616d792e51e6f" dependencies = [ "cfg-if", + "futures-util", "js-sys", "once_cell", "wasm-bindgen", @@ -9489,9 +10325,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.100" +version = "0.2.108" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" +checksum = "008b239d9c740232e71bd39e8ef6429d27097518b6b30bdf9086833bd5b6d608" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -9499,22 +10335,22 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.100" +version = "0.2.108" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" +checksum = "5256bae2d58f54820e6490f9839c49780dff84c65aeab9e772f15d5f0e913a55" dependencies = [ + "bumpalo", "proc-macro2", "quote", - "syn 2.0.100", - "wasm-bindgen-backend", + "syn 2.0.114", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.100" +version = "0.2.108" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" +checksum = "1f01b580c9ac74c8d8f0c0e4afb04eeef2acf145458e52c03845ee9cd23e3d12" dependencies = [ "unicode-ident", ] @@ -9534,9 +10370,9 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.77" +version = "0.3.85" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33b6dd2ef9186f1f2072e409e99cd22a975331a6b3591b12c764e0e55c60d5d2" +checksum = "312e32e551d92129218ea9a2452120f4aabc03529ef03e4d0d82fb2780608598" dependencies = [ "js-sys", "wasm-bindgen", @@ -9554,30 +10390,58 @@ dependencies = [ [[package]] name = "webpki-root-certs" -version = "0.26.8" +version = "0.26.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75c7f0ef91146ebfb530314f5f1d24528d7f0767efbfd31dce919275413e393e" +dependencies = [ + "webpki-root-certs 1.0.5", +] + +[[package]] +name = "webpki-root-certs" +version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09aed61f5e8d2c18344b3faa33a4c837855fe56642757754775548fee21386c4" +checksum = "36a29fc0408b113f68cf32637857ab740edfafdf460c326cd2afaa2d84cc05dc" dependencies = [ "rustls-pki-types", ] [[package]] name = "webpki-roots" -version = "0.26.8" +version = "0.26.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9" +dependencies = [ + "webpki-roots 1.0.5", +] + +[[package]] +name = "webpki-roots" +version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2210b291f7ea53617fbafcc4939f10914214ec15aace5ba62293a668f322c5c9" +checksum = "12bed680863276c63889429bfd6cab3b99943659923822de1c8a39c49e4d722c" dependencies = [ "rustls-pki-types", ] [[package]] name = "whoami" -version = "1.5.2" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d4a4db5077702ca3015d3d02d74974948aba2ad9e12ab7df718ee64ccd7e97d" +dependencies = [ + "libredox", + "wasite 0.1.0", +] + +[[package]] +name = "whoami" +version = "2.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "372d5b87f58ec45c384ba03563b03544dc5fadc3983e434b286913f5b4a9bb6d" +checksum = "ace4d5c7b5ab3d99629156d4e0997edbe98a4beb6d5ba99e2cae830207a81983" dependencies = [ - "redox_syscall", - "wasite", + "libredox", + "wasite 1.0.2", "web-sys", ] @@ -9599,11 +10463,11 @@ checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" [[package]] name = "winapi-util" -version = "0.1.9" +version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -9612,110 +10476,61 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" -[[package]] -name = "windows" -version = "0.58.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd04d41d93c4992d421894c18c8b43496aa748dd4c081bac0dc93eb0489272b6" -dependencies = [ - "windows-core 0.58.0", - "windows-targets 0.52.6", -] - -[[package]] -name = "windows-core" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" -dependencies = [ - "windows-targets 0.52.6", -] - [[package]] name = "windows-core" -version = "0.58.0" +version = "0.62.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ba6d44ec8c2591c134257ce647b7ea6b20335bf6379a27dac5f1641fcf59f99" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" dependencies = [ "windows-implement", "windows-interface", - "windows-result 0.2.0", - "windows-strings 0.1.0", - "windows-targets 0.52.6", + "windows-link", + "windows-result", + "windows-strings", ] [[package]] name = "windows-implement" -version = "0.58.0" +version = "0.60.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2bbd5b46c938e506ecbce286b6628a02171d56153ba733b6c741fc627ec9579b" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.114", ] [[package]] name = "windows-interface" -version = "0.58.0" +version = "0.59.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "053c4c462dc91d3b1504c6fe5a726dd15e216ba718e84a0e46a88fbe5ded3515" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.114", ] [[package]] name = "windows-link" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76840935b766e1b0a05c0066835fb9ec80071d4c09a16f6bd5f7e655e3c14c38" - -[[package]] -name = "windows-registry" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4286ad90ddb45071efd1a66dfa43eb02dd0dfbae1545ad6cc3c51cf34d7e8ba3" -dependencies = [ - "windows-result 0.3.2", - "windows-strings 0.3.1", - "windows-targets 0.53.0", -] - -[[package]] -name = "windows-result" -version = "0.2.0" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d1043d8214f791817bab27572aaa8af63732e11bf84aa21a45a78d6c317ae0e" -dependencies = [ - "windows-targets 0.52.6", -] +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" [[package]] name = "windows-result" -version = "0.3.2" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c64fd11a4fd95df68efcfee5f44a294fe71b8bc6a91993e2791938abcc712252" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" dependencies = [ "windows-link", ] [[package]] name = "windows-strings" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10" -dependencies = [ - "windows-result 0.2.0", - "windows-targets 0.52.6", -] - -[[package]] -name = "windows-strings" -version = "0.3.1" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87fa48cc5d406560701792be122a10132491cff9d0aeb23583cc2dcafc847319" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" dependencies = [ "windows-link", ] @@ -9756,6 +10571,24 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.5", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + [[package]] name = "windows-targets" version = "0.42.2" @@ -9804,18 +10637,19 @@ dependencies = [ [[package]] name = "windows-targets" -version = "0.53.0" +version = "0.53.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1e4c7e8ceaaf9cb7d7507c974735728ab453b67ef8f18febdd7c11fe59dca8b" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" dependencies = [ - "windows_aarch64_gnullvm 0.53.0", - "windows_aarch64_msvc 0.53.0", - "windows_i686_gnu 0.53.0", - "windows_i686_gnullvm 0.53.0", - "windows_i686_msvc 0.53.0", - "windows_x86_64_gnu 0.53.0", - "windows_x86_64_gnullvm 0.53.0", - "windows_x86_64_msvc 0.53.0", + "windows-link", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", ] [[package]] @@ -9838,9 +10672,9 @@ checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" [[package]] name = "windows_aarch64_gnullvm" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" [[package]] name = "windows_aarch64_msvc" @@ -9862,9 +10696,9 @@ checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" [[package]] name = "windows_aarch64_msvc" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" [[package]] name = "windows_i686_gnu" @@ -9886,9 +10720,9 @@ checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" [[package]] name = "windows_i686_gnu" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" [[package]] name = "windows_i686_gnullvm" @@ -9898,9 +10732,9 @@ checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" [[package]] name = "windows_i686_gnullvm" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" [[package]] name = "windows_i686_msvc" @@ -9922,9 +10756,9 @@ checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" [[package]] name = "windows_i686_msvc" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" [[package]] name = "windows_x86_64_gnu" @@ -9946,9 +10780,9 @@ checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" [[package]] name = "windows_x86_64_gnu" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" [[package]] name = "windows_x86_64_gnullvm" @@ -9970,9 +10804,9 @@ checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" [[package]] name = "windows_x86_64_gnullvm" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" [[package]] name = "windows_x86_64_msvc" @@ -9994,48 +10828,30 @@ checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] name = "windows_x86_64_msvc" -version = "0.53.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" - -[[package]] -name = "winnow" -version = "0.6.26" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e90edd2ac1aa278a5c4599b1d89cf03074b610800f866d4026dc199d7929a28" -dependencies = [ - "memchr", -] +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" [[package]] name = "winnow" -version = "0.7.4" +version = "0.7.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e97b544156e9bebe1a0ffbc03484fc1ffe3100cbce3ffb17eac35f7cdd7ab36" +checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829" dependencies = [ "memchr", ] [[package]] -name = "wit-bindgen-rt" -version = "0.39.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" -dependencies = [ - "bitflags 2.9.0", -] - -[[package]] -name = "write16" -version = "1.0.0" +name = "wit-bindgen" +version = "0.51.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1890f4022759daae28ed4fe62859b1236caebfc61ede2f63ed4e695f3f6d936" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" [[package]] name = "writeable" -version = "0.5.5" +version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" +checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" [[package]] name = "wyz" @@ -10052,6 +10868,25 @@ dependencies = [ "tap", ] +[[package]] +name = "x509-certificate" +version = "0.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66534846dec7a11d7c50a74b7cdb208b9a581cad890b7866430d438455847c85" +dependencies = [ + "bcder", + "bytes", + "chrono", + "der 0.7.10", + "hex", + "pem", + "ring", + "signature 2.2.0", + "spki 0.7.3", + "thiserror 1.0.69", + "zeroize", +] + [[package]] name = "x509-parser" version = "0.17.0" @@ -10066,7 +10901,7 @@ dependencies = [ "oid-registry", "ring", "rusticata-macros", - "thiserror 2.0.12", + "thiserror 2.0.17", "time", ] @@ -10090,11 +10925,10 @@ dependencies = [ [[package]] name = "yoke" -version = "0.7.5" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "120e6aef9aa629e3d4f52dc8cc43a015c7724194c97dfaf45180d2daf2b77f40" +checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" dependencies = [ - "serde", "stable_deref_trait", "yoke-derive", "zerofrom", @@ -10102,54 +10936,34 @@ dependencies = [ [[package]] name = "yoke-derive" -version = "0.7.5" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154" +checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", - "synstructure 0.13.1", -] - -[[package]] -name = "zerocopy" -version = "0.7.35" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" -dependencies = [ - "zerocopy-derive 0.7.35", + "syn 2.0.114", + "synstructure 0.13.2", ] [[package]] name = "zerocopy" -version = "0.8.23" +version = "0.8.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd97444d05a4328b90e75e503a34bad781f14e28a823ad3557f0750df1ebcbc6" +checksum = "668f5168d10b9ee831de31933dc111a459c97ec93225beb307aed970d1372dfd" dependencies = [ - "zerocopy-derive 0.8.23", -] - -[[package]] -name = "zerocopy-derive" -version = "0.7.35" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.100", + "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.23" +version = "0.8.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6352c01d0edd5db859a63e2605f4ea3183ddbd15e2c4a9e7d32184df75e4f154" +checksum = "2c7962b26b0a8685668b671ee4b54d007a67d4eaf05fda79ac0ecf41e32270f1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.114", ] [[package]] @@ -10169,35 +10983,46 @@ checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", - "synstructure 0.13.1", + "syn 2.0.114", + "synstructure 0.13.2", ] [[package]] name = "zeroize" -version = "1.8.1" +version = "1.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" dependencies = [ "zeroize_derive", ] [[package]] name = "zeroize_derive" -version = "1.4.2" +version = "1.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" +checksum = "85a5b4158499876c763cb03bc4e49185d3cccbabb15b33c627f7884f43db852e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.114", +] + +[[package]] +name = "zerotrie" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", ] [[package]] name = "zerovec" -version = "0.10.4" +version = "0.11.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa2b893d79df23bfb12d5461018d408ea19dfafe76c2c7ef6d4eba614f8ff079" +checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" dependencies = [ "yoke", "zerofrom", @@ -10206,15 +11031,21 @@ dependencies = [ [[package]] name = "zerovec-derive" -version = "0.10.3" +version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6" +checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.114", ] +[[package]] +name = "zmij" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd8f3f50b848df28f887acb68e41201b5aea6bc8a8dacc00fb40635ff9a72fea" + [[package]] name = "zstd" version = "0.12.4" @@ -10230,7 +11061,7 @@ version = "0.13.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e91ee311a569c327171651566e07972200e76fcfe2242a4fa446149a3881c08a" dependencies = [ - "zstd-safe 7.2.3", + "zstd-safe 7.2.4", ] [[package]] @@ -10245,19 +11076,20 @@ dependencies = [ [[package]] name = "zstd-safe" -version = "7.2.3" +version = "7.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3051792fbdc2e1e143244dc28c60f73d8470e93f3f9cbd0ead44da5ed802722" +checksum = "8f49c4d5f0abb602a93fb8736af2a4f4dd9512e36f7f570d66e65ff867ed3b9d" dependencies = [ "zstd-sys", ] [[package]] name = "zstd-sys" -version = "2.0.14+zstd.1.5.7" +version = "2.0.16+zstd.1.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fb060d4926e4ac3a3ad15d864e99ceb5f343c6b34f5bd6d81ae6ed417311be5" +checksum = "91e19ebc2adc8f83e43039e79776e3fda8ca919132d68a1fed6a5faca2683748" dependencies = [ + "bindgen 0.72.1", "cc", "pkg-config", ] diff --git a/Cargo.toml b/Cargo.toml index 3df5cd36e..abd585cd3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,28 +8,23 @@ members = [ ] [workspace.dependencies] -tokio = "1.38.0" +tokio = "1.47.1" serde = "1.0.217" serde_json = "1.0.138" -dotenvy = "0.15.7" -chrono = { version = "=0.4.39", features = ["clock", "serde"] } -diesel = "2.2.7" +diesel = "2.2.12" diesel-async = "0.5.2" diesel_migrations = "2.2.0" -anyhow = "1.0.95" -thiserror = "2.0.11" -once_cell = "1.20.3" +anyhow = "1.0.98" tracing = "0.1.41" clap = "4.5.31" async-trait = "0.1.83" bcs = "0.1.6" url = "2.5.4" prometheus = "0.13.4" -tokio-util = "0.7.13" -sui-indexer-alt-metrics = { git = "https://github.com/MystenLabs/sui.git", rev = "88ba4e08e96ba1ab965c11ce1a915331dd3ed68d" } -mysten-metrics = { git = "https://github.com/MystenLabs/sui.git", rev = "88ba4e08e96ba1ab965c11ce1a915331dd3ed68d" } -telemetry-subscribers = { git = "https://github.com/MystenLabs/sui.git", rev = "88ba4e08e96ba1ab965c11ce1a915331dd3ed68d" } -sui-pg-db = { git = "https://github.com/MystenLabs/sui.git", rev = "88ba4e08e96ba1ab965c11ce1a915331dd3ed68d" } -move-core-types = { git = "https://github.com/MystenLabs/sui.git", rev = "88ba4e08e96ba1ab965c11ce1a915331dd3ed68d" } -sui-types = { git = "https://github.com/MystenLabs/sui.git", rev = "88ba4e08e96ba1ab965c11ce1a915331dd3ed68d" } +sui-futures = { git = "https://github.com/MystenLabs/sui.git", branch = "testnet" } +sui-indexer-alt-metrics = { git = "https://github.com/MystenLabs/sui.git", branch = "testnet" } +telemetry-subscribers = { git = "https://github.com/MystenLabs/sui.git", branch = "testnet" } +sui-pg-db = { git = "https://github.com/MystenLabs/sui.git", branch = "testnet" } +move-core-types = { git = "https://github.com/MystenLabs/sui.git", branch = "testnet" } +sui-types = { git = "https://github.com/MystenLabs/sui.git", branch = "testnet" } diff --git a/README.md b/README.md index 8b66113cc..421a3e3c9 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,7 @@ DeepBook V3 is a next generation decentralized central limit order book (CLOB) b - [SDK Documentation](https://docs.sui.io/standards/deepbookv3-sdk) - [Example SDK Usage](https://github.com/MystenLabs/ts-sdks/tree/main/packages/deepbook-v3/examples) - [Whitepaper](https://cdn.prod.website-files.com/65fdccb65290aeb1c597b611/66059b44041261e3fe4a330d_deepbook_whitepaper.pdf) +- [Rust SDK(Unofficial)](https://github.com/hoh-zone/sui-deepbookv3) ## DeepBook Architecture diff --git a/crates/indexer/Cargo.toml b/crates/indexer/Cargo.toml index 4ca202f4c..a8797bd44 100644 --- a/crates/indexer/Cargo.toml +++ b/crates/indexer/Cargo.toml @@ -8,53 +8,39 @@ edition = "2021" [dependencies] tokio.workspace = true -futures = "0.3.31" -sui-indexer-alt-framework = { git = "https://github.com/MystenLabs/sui.git", rev = "88ba4e08e96ba1ab965c11ce1a915331dd3ed68d" } -sui-config = { git = "https://github.com/MystenLabs/sui.git", rev = "88ba4e08e96ba1ab965c11ce1a915331dd3ed68d" } -sui-data-ingestion-core = { git = "https://github.com/MystenLabs/sui.git", rev = "88ba4e08e96ba1ab965c11ce1a915331dd3ed68d" } -fastcrypto = { git = "https://github.com/MystenLabs/fastcrypto" } -reqwest = { version = "^0.12", features = ["blocking", "json"] } - -move-binding-derive = { git = "https://github.com/MystenLabs/move-binding.git", rev = "99f68a28c2f19be40a09e5f1281af748df9a8d3e" } -move-types = { git = "https://github.com/MystenLabs/move-binding.git", rev = "99f68a28c2f19be40a09e5f1281af748df9a8d3e" } - -sui-sdk-types = { git = "https://github.com/mystenlabs/sui-rust-sdk", package = "sui-sdk-types", features = ["serde"], rev = "86a9e06" } -sui-transaction-builder = { git = "https://github.com/mystenlabs/sui-rust-sdk", rev = "86a9e06" } - +sui-indexer-alt-framework = { git = "https://github.com/MystenLabs/sui.git", branch = "testnet" } +sui-sdk-types = { git = "https://github.com/mystenlabs/sui-rust-sdk", features = ["serde"], rev = "048124e484f14b9bf2a402227c9bc255c7621bc1" } +sui-transaction-builder = { git = "https://github.com/mystenlabs/sui-rust-sdk", rev = "048124e484f14b9bf2a402227c9bc255c7621bc1" } clap = { workspace = true, features = ["env"] } -tempfile = "3.13.0" -uuid = { version = "1.11.0", features = ["serde", "v4"] } -bigdecimal = "0.4.5" -itertools = "0.14.0" diesel = { workspace = true, features = ["postgres", "uuid", "chrono", "serde_json", "numeric"] } diesel-async = { workspace = true, features = ["bb8", "postgres"] } +hex = "0.4" +bigdecimal = "0.4" tracing.workspace = true async-trait.workspace = true bcs.workspace = true serde.workspace = true +serde_json = { workspace = true } anyhow.workspace = true -serde_json.workspace = true -chrono.workspace = true url.workspace = true sui-pg-db.workspace = true prometheus.workspace = true sui-indexer-alt-metrics.workspace = true -mysten-metrics.workspace = true sui-types.workspace = true move-core-types.workspace = true telemetry-subscribers.workspace = true -tokio-util.workspace = true deepbook-schema = { path = "../schema" } [dev-dependencies] -sui-storage = { git = "https://github.com/MystenLabs/sui.git", rev = "88ba4e08e96ba1ab965c11ce1a915331dd3ed68d" } -sqlx = { version = "0.8.3", features = ["runtime-tokio", "postgres", "chrono"] } -fastcrypto = { git = "https://github.com/MystenLabs/fastcrypto" } -insta = "1.42.2" +sui-storage = { git = "https://github.com/MystenLabs/sui.git", branch = "testnet" } +insta = { version = "1.43.1", features = ["json"] } +serde_json = "1.0.140" +sqlx = { version = "0.8.3", features = ["runtime-tokio", "postgres", "chrono", "bigdecimal"] } +fastcrypto = { git = "https://github.com/MystenLabs/fastcrypto", rev = "4db0e90c732bbf7420ca20de808b698883148d9c" } +chrono = "0.4.39" [[bin]] name = "deepbook-indexer" path = "src/main.rs" - diff --git a/crates/indexer/README.md b/crates/indexer/README.md index 68549a4f7..85d882eb6 100644 --- a/crates/indexer/README.md +++ b/crates/indexer/README.md @@ -26,11 +26,54 @@ cd deepbookv3/crates/indexer ### Running the Indexer -To run the DeepBook Indexer, specify the PostgreSQL connection URL: +To run the DeepBook Indexer, you need to specify the environment and which packages to index: +#### Basic Usage + +```bash +DATABASE_URL="postgresql://user:pass@localhost/test_db" \ +cargo run --package deepbook-indexer -- --env testnet --packages deepbook +``` + +#### Parameters + +- `--env` (required) – Choose the SUI network environment: + - `testnet` – For development and testing + - `mainnet` – For production (note: margin trading not yet deployed on mainnet) + +- `--packages` (required) – Specify which event types to index: + - `deepbook` – Core DeepBook events (orders, trades, pools, governance) + - `deepbook-margin` – Margin trading events (lending, borrowing, liquidations) + - You can specify multiple packages: `--packages deepbook deepbook-margin` + +- `--database-url` (optional) – PostgreSQL connection string. Can also be set via `DATABASE_URL` environment variable. + +- `--metrics-address` (optional, default: `0.0.0.0:9184`) – Prometheus metrics endpoint address. + +#### Examples + +**Index only core DeepBook events on testnet:** ```bash -cargo run --package deepbook-indexer --bin deepbook-indexer -- --database-url=postgres://postgres:postgrespw@localhost:5432/deepbook +DATABASE_URL="postgresql://user:pass@localhost/test_db" \ +cargo run --package deepbook-indexer -- --env testnet --packages deepbook ``` -* `--database-url` – Connection string for the PostgreSQL database. + +**Index both core and margin events on testnet:** +```bash +DATABASE_URL="postgresql://user:pass@localhost/test_db" \ +cargo run --package deepbook-indexer -- --env testnet --packages deepbook deepbook-margin +``` + +**Index only core events on mainnet:** +```bash +DATABASE_URL="postgresql://user:pass@localhost/test_db" \ +cargo run --package deepbook-indexer -- --env mainnet --packages deepbook +``` + +#### Important Notes + +- **Margin events on mainnet**: The margin trading package is not yet deployed on mainnet, so `--packages deepbook-margin` will fail on mainnet. +- **Database migrations**: The indexer automatically runs database migrations on startup. +- **Environment variable**: You can set `DATABASE_URL` as an environment variable instead of using the `--database-url` parameter. --- \ No newline at end of file diff --git a/crates/indexer/deepbook-indexer-openapi.yaml b/crates/indexer/deepbook-indexer-openapi.yaml new file mode 100644 index 000000000..1c42b9af8 --- /dev/null +++ b/crates/indexer/deepbook-indexer-openapi.yaml @@ -0,0 +1,602 @@ +openapi: 3.0.3 +info: + title: DeepBookV3 Indexer API + version: "1.0.0" + description: | + REST endpoints for DeepBookV3 order book + analytics data. + Volumes are returned in the smallest unit of the asset unless stated otherwise. +servers: + - url: https://deepbook-indexer.mainnet.mystenlabs.com + description: Public DeepBookV3 Indexer + +tags: + - name: Pools + - name: Volume + - name: Market Data + - name: Order Flow + - name: Reference + - name: Health + +paths: + /: + get: + tags: [Health] + summary: Basic health check + description: Returns HTTP 200 if the server is running. + operationId: healthCheck + responses: + "200": + description: Server is running + + /status: + get: + tags: [Health] + summary: Indexer synchronization status + description: | + Returns detailed information about indexer health, including: + - Current checkpoint lag (difference between on-chain and indexed checkpoints) + - Time lag (how long ago the last checkpoint was indexed) + - Per-pipeline status for multi-pipeline setups + + The client can specify custom health thresholds via query parameters. + The server will compute the status based on these thresholds. + + **Default thresholds:** + - `max_checkpoint_lag`: 100 + - `max_time_lag_seconds`: 60 + + **Health computation:** + - `healthy`: checkpoint_lag < max_checkpoint_lag AND time_lag < max_time_lag_seconds + - `degraded`: exceeds the specified thresholds + operationId: getStatus + parameters: + - in: query + name: max_checkpoint_lag + schema: + type: integer + format: int64 + default: 100 + description: Maximum acceptable checkpoint lag for "healthy" status + - in: query + name: max_time_lag_seconds + schema: + type: integer + format: int64 + default: 60 + description: Maximum acceptable time lag in seconds for "healthy" status + responses: + "200": + description: Indexer status and health metrics + content: + application/json: + schema: + $ref: "#/components/schemas/IndexerStatus" + x-docs: "https://docs.sui.io/standards/deepbookv3-indexer" + + /get_pools: + get: + tags: [Pools] + summary: Get all pool information + description: Returns all available pools with asset metadata and trading parameters. + operationId: getPools + responses: + "200": + description: List of pools + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/Pool" + x-docs: "https://docs.sui.io/standards/deepbookv3-indexer" # reference + + /historical_volume/{pool_names}: + get: + tags: [Volume] + summary: Historical volume for pools in a time range + description: | + Comma-delimited pool names. By default returns 24h volume in **quote** asset. + Set `volume_in_base=true` to return volumes in base asset units. + operationId: getHistoricalVolume + parameters: + - in: path + name: pool_names + required: true + schema: + type: string + description: Comma-separated list, e.g. `DEEP_SUI,SUI_USDC`. + - in: query + name: start_time + schema: { type: integer, format: int64 } + description: Unix timestamp (seconds) + - in: query + name: end_time + schema: { type: integer, format: int64 } + description: Unix timestamp (seconds) + - in: query + name: volume_in_base + schema: { type: boolean, default: false } + responses: + "200": + description: Map of pool -> total volume + content: + application/json: + schema: + $ref: "#/components/schemas/PoolVolumes" + x-docs: "https://docs.sui.io/standards/deepbookv3-indexer" # reference + + /all_historical_volume: + get: + tags: [Volume] + summary: Historical volume for all pools + operationId: getAllHistoricalVolume + parameters: + - in: query + name: start_time + schema: { type: integer, format: int64 } + - in: query + name: end_time + schema: { type: integer, format: int64 } + - in: query + name: volume_in_base + schema: { type: boolean, default: false } + responses: + "200": + description: Map of pool -> total volume across all pools in range + content: + application/json: + schema: + $ref: "#/components/schemas/PoolVolumes" + x-docs: "https://docs.sui.io/standards/deepbookv3-indexer" + + /historical_volume_by_balance_manager_id/{pool_names}/{balance_manager_id}: + get: + tags: [Volume] + summary: Historical maker/taker volume for a BalanceManager + operationId: getHistoricalVolumeByBM + parameters: + - in: path + name: pool_names + required: true + schema: { type: string } + description: Comma-separated pool names. + - in: path + name: balance_manager_id + required: true + schema: { type: string } + - in: query + name: start_time + schema: { type: integer, format: int64 } + - in: query + name: end_time + schema: { type: integer, format: int64 } + - in: query + name: volume_in_base + schema: { type: boolean, default: false } + responses: + "200": + description: Map of pool -> [maker_volume, taker_volume] + content: + application/json: + schema: + $ref: "#/components/schemas/PoolMakerTakerVolumes" + x-docs: "https://docs.sui.io/standards/deepbookv3-indexer" + + /historical_volume_by_balance_manager_id_with_interval/{pool_names}/{balance_manager_id}: + get: + tags: [Volume] + summary: Intervalized maker/taker volume for a BalanceManager + operationId: getHistoricalVolumeByBMWithInterval + parameters: + - in: path + name: pool_names + required: true + schema: { type: string } + - in: path + name: balance_manager_id + required: true + schema: { type: string } + - in: query + name: start_time + schema: { type: integer, format: int64 } + - in: query + name: end_time + schema: { type: integer, format: int64 } + - in: query + name: interval + required: true + schema: { type: integer, format: int64, minimum: 1 } + description: Interval length in seconds. + - in: query + name: volume_in_base + schema: { type: boolean, default: false } + responses: + "200": + description: | + Map of "[start,end]" (Unix seconds) -> { pool: [maker,taker], ... } + content: + application/json: + schema: + $ref: "#/components/schemas/IntervalMakerTakerVolumes" + x-docs: "https://docs.sui.io/standards/deepbookv3-indexer" + + /summary: + get: + tags: [Market Data] + summary: Summary for all trading pairs + operationId: getSummary + responses: + "200": + description: Array of summary objects + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/SummaryItem" + x-docs: "https://docs.sui.io/standards/deepbookv3-indexer" + + /ticker: + get: + tags: [Market Data] + summary: Ticker info for all trading pairs + operationId: getTicker + responses: + "200": + description: Map of pair -> ticker fields + content: + application/json: + schema: + $ref: "#/components/schemas/TickerMap" + x-docs: "https://docs.sui.io/standards/deepbookv3-indexer" + + /trades/{pool_name}: + get: + tags: [Market Data] + summary: Recent trades for a pool + operationId: getTrades + parameters: + - in: path + name: pool_name + required: true + schema: { type: string } + - in: query + name: limit + schema: { type: integer, minimum: 1 } + description: Number of trades to return. + - in: query + name: start_time + schema: { type: integer, format: int64 } + description: Unix timestamp (seconds) + - in: query + name: end_time + schema: { type: integer, format: int64 } + description: Unix timestamp (seconds) + - in: query + name: maker_balance_manager_id + schema: { type: string } + - in: query + name: taker_balance_manager_id + schema: { type: string } + responses: + "200": + description: List of trades (timestamp in ms) + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/Trade" + x-docs: "https://docs.sui.io/standards/deepbookv3-indexer" + + /order_updates/{pool_name}: + get: + tags: [Order Flow] + summary: Recently placed or canceled orders in a pool + operationId: getOrderUpdates + parameters: + - in: path + name: pool_name + required: true + schema: { type: string } + - in: query + name: limit + schema: { type: integer, minimum: 1 } + - in: query + name: start_time + schema: { type: integer, format: int64 } + - in: query + name: end_time + schema: { type: integer, format: int64 } + - in: query + name: status + schema: + type: string + enum: ["Placed", "Canceled"] + - in: query + name: balance_manager_id + schema: { type: string } + responses: + "200": + description: Order update rows + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/OrderUpdate" + x-docs: "https://docs.sui.io/standards/deepbookv3-indexer" + + /orderbook/{pool_name}: + get: + tags: [Market Data] + summary: Order book snapshot for a pool + operationId: getOrderbook + parameters: + - in: path + name: pool_name + required: true + schema: { type: string } + - in: query + name: level + schema: + type: integer + enum: [1, 2] + description: 1 = best bid/ask only; 2 = aggregated levels (default). + - in: query + name: depth + schema: + type: integer + minimum: 0 + description: 0 = full book; >1 returns N/2 bids + N/2 asks. + responses: + "200": + description: Bids/asks sorted best→worst; timestamp in ms (string) + content: + application/json: + schema: + $ref: "#/components/schemas/Orderbook" + x-docs: "https://docs.sui.io/standards/deepbookv3-indexer" + + /assets: + get: + tags: [Reference] + summary: Asset information for all coins + operationId: getAssets + responses: + "200": + description: Symbol -> metadata + content: + application/json: + schema: + $ref: "#/components/schemas/AssetsMap" + x-docs: "https://docs.sui.io/standards/deepbookv3-indexer" + +components: + schemas: + Pool: + type: object + required: + [ + pool_id, + pool_name, + base_asset_id, + quote_asset_id, + min_size, + lot_size, + tick_size, + ] + properties: + pool_id: { type: string } + pool_name: { type: string } + base_asset_id: { type: string } + base_asset_decimals: { type: integer } + base_asset_symbol: { type: string } + base_asset_name: { type: string } + quote_asset_id: { type: string } + quote_asset_decimals: { type: integer } + quote_asset_symbol: { type: string } + quote_asset_name: { type: string } + min_size: { type: integer, description: "Smallest units of base asset" } + lot_size: { type: integer, description: "Increment in base units" } + tick_size: { type: integer, description: "Min price increment" } + + PoolVolumes: + type: object + additionalProperties: + type: integer + description: Volume in smallest unit (base or quote depending on query) + + PoolMakerTakerVolumes: + type: object + additionalProperties: + type: array + minItems: 2 + maxItems: 2 + items: + type: integer + description: "[maker_volume, taker_volume] in smallest units" + + IntervalMakerTakerVolumes: + type: object + additionalProperties: + type: object + additionalProperties: + type: array + minItems: 2 + maxItems: 2 + items: { type: integer } + + SummaryItem: + type: object + properties: + trading_pairs: { type: string } + quote_currency: { type: string } + last_price: { type: number } + lowest_price_24h: { type: number } + highest_bid: { type: number } + base_volume: { type: number } + price_change_percent_24h: { type: number } + quote_volume: { type: number } + lowest_ask: { type: number } + highest_price_24h: { type: number } + base_currency: { type: string } + + TickerMap: + type: object + additionalProperties: + type: object + properties: + base_volume: { type: number } + quote_volume: { type: number } + last_price: { type: number } + isFrozen: + type: integer + enum: [0, 1] + description: "0=active, 1=inactive" + + Trade: + type: object + properties: + trade_id: { type: string } + base_volume: { type: integer } + quote_volume: { type: integer } + price: { type: integer } + type: { type: string } + timestamp: { type: integer, format: int64, description: "Unix ms" } + maker_order_id: { type: string } + taker_order_id: { type: string } + maker_balance_manager_id: { type: string } + taker_balance_manager_id: { type: string } + + OrderUpdate: + type: object + properties: + order_id: { type: string } + balance_manager_id: { type: string } + timestamp: { type: integer, format: int64, description: "Unix ms" } + original_quantity: { type: integer } + remaining_quantity: { type: integer } + filled_quantity: { type: integer } + price: { type: integer } + status: + type: string + enum: [Placed, Canceled] + type: + type: string + description: buy/sell + + Orderbook: + type: object + properties: + timestamp: + type: string + description: Unix ms as string + bids: + type: array + items: + type: array + minItems: 2 + maxItems: 2 + items: + type: string + description: "[price, size]; best → worst" + asks: + type: array + items: + type: array + minItems: 2 + maxItems: 2 + items: + type: string + description: "[price, size]; best → worst" + + AssetsMap: + type: object + additionalProperties: + $ref: "#/components/schemas/Asset" + + Asset: + type: object + properties: + unified_cryptoasset_id: { type: string } + name: { type: string } + contractAddress: { type: string } + contractAddressUrl: { type: string } + can_deposit: { type: string, enum: ["true", "false"] } + can_withdraw: { type: string, enum: ["true", "false"] } + + IndexerStatus: + type: object + required: [status, latest_onchain_checkpoint, current_time_ms, earliest_checkpoint, max_lag_pipeline, pipelines, max_checkpoint_lag, max_time_lag_seconds] + properties: + status: + type: string + enum: [OK, UNHEALTHY] + description: "Overall indexer health status based on client-provided thresholds" + latest_onchain_checkpoint: + type: integer + format: int64 + description: "Latest checkpoint sequence number on the blockchain" + current_time_ms: + type: integer + format: int64 + description: "Current server timestamp in milliseconds" + earliest_checkpoint: + type: integer + format: int64 + description: "The lowest checkpoint across all pipelines (useful for alerting)" + max_lag_pipeline: + type: string + description: "Name of the pipeline with the highest checkpoint lag (useful for alerting)" + pipelines: + type: array + items: + $ref: "#/components/schemas/PipelineStatus" + description: "Status for each indexer pipeline" + max_checkpoint_lag: + type: integer + format: int64 + description: "Maximum checkpoint lag across all pipelines" + max_time_lag_seconds: + type: integer + format: int64 + description: "Maximum time lag in seconds across all pipelines" + + PipelineStatus: + type: object + required: [pipeline, indexed_checkpoint, indexed_epoch, indexed_timestamp_ms, checkpoint_lag, time_lag_seconds, latest_onchain_checkpoint] + properties: + pipeline: + type: string + description: "Pipeline name (e.g., 'deepbook_indexer')" + indexed_checkpoint: + type: integer + format: int64 + description: "Latest checkpoint indexed by this pipeline" + indexed_epoch: + type: integer + format: int64 + description: "Latest epoch indexed by this pipeline" + indexed_timestamp_ms: + type: integer + format: int64 + description: "Timestamp of the latest indexed checkpoint in milliseconds" + checkpoint_lag: + type: integer + format: int64 + description: "Difference between on-chain and indexed checkpoint" + time_lag_seconds: + type: integer + format: int64 + description: "Time difference in seconds since last indexed checkpoint" + latest_onchain_checkpoint: + type: integer + format: int64 + description: "Latest on-chain checkpoint (included for completeness)" + +x-notes: + asset_scalars: | + Asset “scalars” (decimal places for smallest unit) used by volume endpoints: + AUSD:6, bETH:8, DEEP:6, USDC:6, SUI:9, NS:6, TYPUS:9, wUSDC:6, wUSDT:6. + Convert human units by dividing by 10^SCALAR. (See docs.) diff --git a/crates/indexer/src/handlers/asset_supplied_handler.rs b/crates/indexer/src/handlers/asset_supplied_handler.rs new file mode 100644 index 000000000..46e0341f8 --- /dev/null +++ b/crates/indexer/src/handlers/asset_supplied_handler.rs @@ -0,0 +1,24 @@ +use crate::models::deepbook_margin::margin_pool::AssetSupplied; +use deepbook_schema::models::AssetSupplied as AssetSuppliedModel; + +define_handler! { + name: AssetSuppliedHandler, + processor_name: "asset_supplied", + event_type: AssetSupplied, + db_model: AssetSuppliedModel, + table: asset_supplied, + map_event: |event, meta| AssetSuppliedModel { + event_digest: meta.event_digest(), + digest: meta.digest(), + sender: meta.sender(), + checkpoint: meta.checkpoint(), + checkpoint_timestamp_ms: meta.checkpoint_timestamp_ms(), + package: meta.package(), + margin_pool_id: event.margin_pool_id.to_string(), + asset_type: event.asset_type.to_string(), + supplier: event.supplier.to_string(), + amount: event.supply_amount as i64, + shares: event.supply_shares as i64, + onchain_timestamp: event.timestamp as i64, + } +} diff --git a/crates/indexer/src/handlers/asset_withdrawn_handler.rs b/crates/indexer/src/handlers/asset_withdrawn_handler.rs new file mode 100644 index 000000000..38952b39c --- /dev/null +++ b/crates/indexer/src/handlers/asset_withdrawn_handler.rs @@ -0,0 +1,24 @@ +use crate::models::deepbook_margin::margin_pool::AssetWithdrawn; +use deepbook_schema::models::AssetWithdrawn as AssetWithdrawnModel; + +define_handler! { + name: AssetWithdrawnHandler, + processor_name: "asset_withdrawn", + event_type: AssetWithdrawn, + db_model: AssetWithdrawnModel, + table: asset_withdrawn, + map_event: |event, meta| AssetWithdrawnModel { + event_digest: meta.event_digest(), + digest: meta.digest(), + sender: meta.sender(), + checkpoint: meta.checkpoint(), + checkpoint_timestamp_ms: meta.checkpoint_timestamp_ms(), + package: meta.package(), + margin_pool_id: event.margin_pool_id.to_string(), + asset_type: event.asset_type.to_string(), + supplier: event.supplier.to_string(), + amount: event.withdraw_amount as i64, + shares: event.withdraw_shares as i64, + onchain_timestamp: event.timestamp as i64, + } +} diff --git a/crates/indexer/src/handlers/balances_handler.rs b/crates/indexer/src/handlers/balances_handler.rs index 9f040e96a..157145554 100644 --- a/crates/indexer/src/handlers/balances_handler.rs +++ b/crates/indexer/src/handlers/balances_handler.rs @@ -1,85 +1,22 @@ -use crate::handlers::{is_deepbook_tx, struct_tag, try_extract_move_call_package}; use crate::models::deepbook::balance_manager::BalanceEvent; -use async_trait::async_trait; use deepbook_schema::models::Balances; -use deepbook_schema::schema::balances; -use diesel_async::RunQueryDsl; -use move_core_types::account_address::AccountAddress; -use move_core_types::language_storage::StructTag; -use std::sync::Arc; -use sui_indexer_alt_framework::pipeline::concurrent::Handler; -use sui_indexer_alt_framework::pipeline::Processor; -use sui_pg_db::Connection; -use sui_types::full_checkpoint_content::CheckpointData; -use tracing::debug; -pub struct BalancesHandler { - event_type: StructTag, -} - -impl BalancesHandler { - pub fn new(package_id_override: Option) -> Self { - Self { - event_type: struct_tag::(package_id_override), - } - } -} - -impl Processor for BalancesHandler { - const NAME: &'static str = "Balances"; - type Value = Balances; - - fn process(&self, checkpoint: &Arc) -> anyhow::Result> { - checkpoint - .transactions - .iter() - .try_fold(vec![], |result, tx| { - if !is_deepbook_tx(tx) { - return Ok(result); - } - let Some(events) = &tx.events else { - return Ok(result); - }; - - let package = try_extract_move_call_package(tx).unwrap_or_default(); - let checkpoint_timestamp_ms = checkpoint.checkpoint_summary.timestamp_ms as i64; - let checkpoint = checkpoint.checkpoint_summary.sequence_number as i64; - let digest = tx.transaction.digest(); - - return events - .data - .iter() - .filter(|ev| ev.type_ == self.event_type) - .enumerate() - .try_fold(result, |mut result, (index, ev)| { - let event: BalanceEvent = bcs::from_bytes(&ev.contents)?; - let data = Balances { - digest: digest.to_string(), - event_digest: format!("{digest}{index}"), - sender: tx.transaction.sender_address().to_string(), - checkpoint, - checkpoint_timestamp_ms, - package: package.clone(), - balance_manager_id: event.balance_manager_id.to_string(), - asset: event.asset.to_string(), - amount: event.amount as i64, - deposit: event.deposit, - }; - debug!("Observed Deepbook Balance Event {:?}", data); - result.push(data); - Ok(result) - }); - }) - } -} - -#[async_trait] -impl Handler for BalancesHandler { - async fn commit(values: &[Self::Value], conn: &mut Connection<'_>) -> anyhow::Result { - Ok(diesel::insert_into(balances::table) - .values(values) - .on_conflict_do_nothing() - .execute(conn) - .await?) +define_handler! { + name: BalancesHandler, + processor_name: "balances", + event_type: BalanceEvent, + db_model: Balances, + table: balances, + map_event: |event, meta| Balances { + event_digest: meta.event_digest(), + digest: meta.digest(), + sender: meta.sender(), + checkpoint: meta.checkpoint(), + checkpoint_timestamp_ms: meta.checkpoint_timestamp_ms(), + package: meta.package(), + balance_manager_id: event.balance_manager_id.to_string(), + asset: event.asset.to_string(), + amount: event.amount as i64, + deposit: event.deposit, } } diff --git a/crates/indexer/src/handlers/conditional_order_added_handler.rs b/crates/indexer/src/handlers/conditional_order_added_handler.rs new file mode 100644 index 000000000..5dac80116 --- /dev/null +++ b/crates/indexer/src/handlers/conditional_order_added_handler.rs @@ -0,0 +1,36 @@ +use bigdecimal::BigDecimal; + +use crate::models::deepbook_margin::tpsl::ConditionalOrderAdded as ConditionalOrderAddedEvent; +use deepbook_schema::models::ConditionalOrderEvent; + +define_handler! { + name: ConditionalOrderAddedHandler, + processor_name: "conditional_order_added", + event_type: ConditionalOrderAddedEvent, + db_model: ConditionalOrderEvent, + table: conditional_order_events, + map_event: |event, meta| ConditionalOrderEvent { + event_digest: meta.event_digest(), + digest: meta.digest(), + sender: meta.sender(), + checkpoint: meta.checkpoint(), + checkpoint_timestamp_ms: meta.checkpoint_timestamp_ms(), + package: meta.package(), + event_type: "added".to_string(), + manager_id: event.manager_id.to_string(), + pool_id: None, + conditional_order_id: event.conditional_order_id as i64, + trigger_below_price: event.conditional_order.condition.trigger_below_price, + trigger_price: BigDecimal::from(event.conditional_order.condition.trigger_price), + is_limit_order: event.conditional_order.pending_order.is_limit_order, + client_order_id: event.conditional_order.pending_order.client_order_id as i64, + order_type: event.conditional_order.pending_order.order_type.unwrap_or(0) as i16, + self_matching_option: event.conditional_order.pending_order.self_matching_option as i16, + price: BigDecimal::from(event.conditional_order.pending_order.price.unwrap_or(0)), + quantity: BigDecimal::from(event.conditional_order.pending_order.quantity), + is_bid: event.conditional_order.pending_order.is_bid, + pay_with_deep: event.conditional_order.pending_order.pay_with_deep, + expire_timestamp: event.conditional_order.pending_order.expire_timestamp.unwrap_or(0) as i64, + onchain_timestamp: event.timestamp as i64, + } +} diff --git a/crates/indexer/src/handlers/conditional_order_cancelled_handler.rs b/crates/indexer/src/handlers/conditional_order_cancelled_handler.rs new file mode 100644 index 000000000..80958bb03 --- /dev/null +++ b/crates/indexer/src/handlers/conditional_order_cancelled_handler.rs @@ -0,0 +1,36 @@ +use bigdecimal::BigDecimal; + +use crate::models::deepbook_margin::tpsl::ConditionalOrderCancelled as ConditionalOrderCancelledEvent; +use deepbook_schema::models::ConditionalOrderEvent; + +define_handler! { + name: ConditionalOrderCancelledHandler, + processor_name: "conditional_order_cancelled", + event_type: ConditionalOrderCancelledEvent, + db_model: ConditionalOrderEvent, + table: conditional_order_events, + map_event: |event, meta| ConditionalOrderEvent { + event_digest: meta.event_digest(), + digest: meta.digest(), + sender: meta.sender(), + checkpoint: meta.checkpoint(), + checkpoint_timestamp_ms: meta.checkpoint_timestamp_ms(), + package: meta.package(), + event_type: "cancelled".to_string(), + manager_id: event.manager_id.to_string(), + pool_id: None, + conditional_order_id: event.conditional_order_id as i64, + trigger_below_price: event.conditional_order.condition.trigger_below_price, + trigger_price: BigDecimal::from(event.conditional_order.condition.trigger_price), + is_limit_order: event.conditional_order.pending_order.is_limit_order, + client_order_id: event.conditional_order.pending_order.client_order_id as i64, + order_type: event.conditional_order.pending_order.order_type.unwrap_or(0) as i16, + self_matching_option: event.conditional_order.pending_order.self_matching_option as i16, + price: BigDecimal::from(event.conditional_order.pending_order.price.unwrap_or(0)), + quantity: BigDecimal::from(event.conditional_order.pending_order.quantity), + is_bid: event.conditional_order.pending_order.is_bid, + pay_with_deep: event.conditional_order.pending_order.pay_with_deep, + expire_timestamp: event.conditional_order.pending_order.expire_timestamp.unwrap_or(0) as i64, + onchain_timestamp: event.timestamp as i64, + } +} diff --git a/crates/indexer/src/handlers/conditional_order_executed_handler.rs b/crates/indexer/src/handlers/conditional_order_executed_handler.rs new file mode 100644 index 000000000..4e184388a --- /dev/null +++ b/crates/indexer/src/handlers/conditional_order_executed_handler.rs @@ -0,0 +1,36 @@ +use bigdecimal::BigDecimal; + +use crate::models::deepbook_margin::tpsl::ConditionalOrderExecuted as ConditionalOrderExecutedEvent; +use deepbook_schema::models::ConditionalOrderEvent; + +define_handler! { + name: ConditionalOrderExecutedHandler, + processor_name: "conditional_order_executed", + event_type: ConditionalOrderExecutedEvent, + db_model: ConditionalOrderEvent, + table: conditional_order_events, + map_event: |event, meta| ConditionalOrderEvent { + event_digest: meta.event_digest(), + digest: meta.digest(), + sender: meta.sender(), + checkpoint: meta.checkpoint(), + checkpoint_timestamp_ms: meta.checkpoint_timestamp_ms(), + package: meta.package(), + event_type: "executed".to_string(), + manager_id: event.manager_id.to_string(), + pool_id: Some(event.pool_id.to_string()), + conditional_order_id: event.conditional_order_id as i64, + trigger_below_price: event.conditional_order.condition.trigger_below_price, + trigger_price: BigDecimal::from(event.conditional_order.condition.trigger_price), + is_limit_order: event.conditional_order.pending_order.is_limit_order, + client_order_id: event.conditional_order.pending_order.client_order_id as i64, + order_type: event.conditional_order.pending_order.order_type.unwrap_or(0) as i16, + self_matching_option: event.conditional_order.pending_order.self_matching_option as i16, + price: BigDecimal::from(event.conditional_order.pending_order.price.unwrap_or(0)), + quantity: BigDecimal::from(event.conditional_order.pending_order.quantity), + is_bid: event.conditional_order.pending_order.is_bid, + pay_with_deep: event.conditional_order.pending_order.pay_with_deep, + expire_timestamp: event.conditional_order.pending_order.expire_timestamp.unwrap_or(0) as i64, + onchain_timestamp: event.timestamp as i64, + } +} diff --git a/crates/indexer/src/handlers/conditional_order_insufficient_funds_handler.rs b/crates/indexer/src/handlers/conditional_order_insufficient_funds_handler.rs new file mode 100644 index 000000000..4f768de1a --- /dev/null +++ b/crates/indexer/src/handlers/conditional_order_insufficient_funds_handler.rs @@ -0,0 +1,36 @@ +use bigdecimal::BigDecimal; + +use crate::models::deepbook_margin::tpsl::ConditionalOrderInsufficientFunds as ConditionalOrderInsufficientFundsEvent; +use deepbook_schema::models::ConditionalOrderEvent; + +define_handler! { + name: ConditionalOrderInsufficientFundsHandler, + processor_name: "conditional_order_insufficient_funds", + event_type: ConditionalOrderInsufficientFundsEvent, + db_model: ConditionalOrderEvent, + table: conditional_order_events, + map_event: |event, meta| ConditionalOrderEvent { + event_digest: meta.event_digest(), + digest: meta.digest(), + sender: meta.sender(), + checkpoint: meta.checkpoint(), + checkpoint_timestamp_ms: meta.checkpoint_timestamp_ms(), + package: meta.package(), + event_type: "insufficient_funds".to_string(), + manager_id: event.manager_id.to_string(), + pool_id: None, + conditional_order_id: event.conditional_order_id as i64, + trigger_below_price: event.conditional_order.condition.trigger_below_price, + trigger_price: BigDecimal::from(event.conditional_order.condition.trigger_price), + is_limit_order: event.conditional_order.pending_order.is_limit_order, + client_order_id: event.conditional_order.pending_order.client_order_id as i64, + order_type: event.conditional_order.pending_order.order_type.unwrap_or(0) as i16, + self_matching_option: event.conditional_order.pending_order.self_matching_option as i16, + price: BigDecimal::from(event.conditional_order.pending_order.price.unwrap_or(0)), + quantity: BigDecimal::from(event.conditional_order.pending_order.quantity), + is_bid: event.conditional_order.pending_order.is_bid, + pay_with_deep: event.conditional_order.pending_order.pay_with_deep, + expire_timestamp: event.conditional_order.pending_order.expire_timestamp.unwrap_or(0) as i64, + onchain_timestamp: event.timestamp as i64, + } +} diff --git a/crates/indexer/src/handlers/deep_burned_handler.rs b/crates/indexer/src/handlers/deep_burned_handler.rs new file mode 100644 index 000000000..7fb690351 --- /dev/null +++ b/crates/indexer/src/handlers/deep_burned_handler.rs @@ -0,0 +1,87 @@ +use crate::handlers::{is_deepbook_tx, try_extract_move_call_package}; +use crate::models::deepbook::pool::DeepBurned as DeepBurnedEvent; +use crate::models::sui::sui::SUI; +use crate::traits::MoveStruct; +use crate::DeepbookEnv; +use async_trait::async_trait; +use deepbook_schema::models::DeepBurned; +use deepbook_schema::schema::deep_burned; +use diesel_async::RunQueryDsl; +use std::sync::Arc; +use sui_indexer_alt_framework::pipeline::Processor; +use sui_indexer_alt_framework::postgres::handler::Handler; +use sui_indexer_alt_framework::postgres::Connection; +use sui_indexer_alt_framework::types::full_checkpoint_content::Checkpoint; +use sui_types::transaction::TransactionDataAPI; +use tracing::debug; + +pub struct DeepBurnedHandler { + env: DeepbookEnv, +} + +impl DeepBurnedHandler { + pub fn new(env: DeepbookEnv) -> Self { + Self { env } + } +} + +#[async_trait] +impl Processor for DeepBurnedHandler { + const NAME: &'static str = "deep_burned"; + type Value = DeepBurned; + + async fn process(&self, checkpoint: &Arc) -> anyhow::Result> { + let mut results = vec![]; + + for tx in &checkpoint.transactions { + if !is_deepbook_tx(tx, &checkpoint.object_set, self.env) { + continue; + } + let Some(events) = &tx.events else { + continue; + }; + + let package = try_extract_move_call_package(tx).unwrap_or_default(); + let checkpoint_timestamp_ms = checkpoint.summary.timestamp_ms as i64; + let checkpoint_seq = checkpoint.summary.sequence_number as i64; + let digest = tx.transaction.digest(); + + for (index, ev) in events.data.iter().enumerate() { + // Match base type (ignore type parameters) + if !DeepBurnedEvent::::matches_event_type(&ev.type_, self.env) { + continue; + } + + // Can use since it doesn't affect deserialization + let event: DeepBurnedEvent = bcs::from_bytes(&ev.contents)?; + let data = DeepBurned { + digest: digest.to_string(), + event_digest: format!("{digest}{index}"), + sender: tx.transaction.sender().to_string(), + checkpoint: checkpoint_seq, + checkpoint_timestamp_ms, + package: package.clone(), + pool_id: event.pool_id.to_string(), + burned_amount: event.deep_burned as i64, + }; + debug!("Observed Deepbook DeepBurned {:?}", data); + results.push(data); + } + } + Ok(results) + } +} + +#[async_trait] +impl Handler for DeepBurnedHandler { + async fn commit<'a>( + values: &[Self::Value], + conn: &mut Connection<'a>, + ) -> anyhow::Result { + Ok(diesel::insert_into(deep_burned::table) + .values(values) + .on_conflict_do_nothing() + .execute(conn) + .await?) + } +} diff --git a/crates/indexer/src/handlers/deepbook_pool_config_updated_handler.rs b/crates/indexer/src/handlers/deepbook_pool_config_updated_handler.rs new file mode 100644 index 000000000..6253b2d69 --- /dev/null +++ b/crates/indexer/src/handlers/deepbook_pool_config_updated_handler.rs @@ -0,0 +1,21 @@ +use crate::models::deepbook_margin::margin_registry::DeepbookPoolConfigUpdated; +use deepbook_schema::models::DeepbookPoolConfigUpdated as DeepbookPoolConfigUpdatedModel; + +define_handler! { + name: DeepbookPoolConfigUpdatedHandler, + processor_name: "deepbook_pool_config_updated", + event_type: DeepbookPoolConfigUpdated, + db_model: DeepbookPoolConfigUpdatedModel, + table: deepbook_pool_config_updated, + map_event: |event, meta| DeepbookPoolConfigUpdatedModel { + event_digest: meta.event_digest(), + digest: meta.digest(), + sender: meta.sender(), + checkpoint: meta.checkpoint(), + checkpoint_timestamp_ms: meta.checkpoint_timestamp_ms(), + package: meta.package(), + pool_id: event.pool_id.to_string(), + config_json: serde_json::to_value(&event.config).unwrap(), + onchain_timestamp: event.timestamp as i64, + } +} diff --git a/crates/indexer/src/handlers/deepbook_pool_registered_handler.rs b/crates/indexer/src/handlers/deepbook_pool_registered_handler.rs new file mode 100644 index 000000000..6acd48677 --- /dev/null +++ b/crates/indexer/src/handlers/deepbook_pool_registered_handler.rs @@ -0,0 +1,21 @@ +use crate::models::deepbook_margin::margin_registry::DeepbookPoolRegistered; +use deepbook_schema::models::DeepbookPoolRegistered as DeepbookPoolRegisteredModel; + +define_handler! { + name: DeepbookPoolRegisteredHandler, + processor_name: "deepbook_pool_registered", + event_type: DeepbookPoolRegistered, + db_model: DeepbookPoolRegisteredModel, + table: deepbook_pool_registered, + map_event: |event, meta| DeepbookPoolRegisteredModel { + event_digest: meta.event_digest(), + digest: meta.digest(), + sender: meta.sender(), + checkpoint: meta.checkpoint(), + checkpoint_timestamp_ms: meta.checkpoint_timestamp_ms(), + package: meta.package(), + pool_id: event.pool_id.to_string(), + config_json: Some(serde_json::to_value(&event.config).unwrap()), + onchain_timestamp: event.timestamp as i64, + } +} diff --git a/crates/indexer/src/handlers/deepbook_pool_updated_handler.rs b/crates/indexer/src/handlers/deepbook_pool_updated_handler.rs new file mode 100644 index 000000000..1d834383d --- /dev/null +++ b/crates/indexer/src/handlers/deepbook_pool_updated_handler.rs @@ -0,0 +1,23 @@ +use crate::models::deepbook_margin::margin_pool::DeepbookPoolUpdated; +use deepbook_schema::models::DeepbookPoolUpdated as DeepbookPoolUpdatedModel; + +define_handler! { + name: DeepbookPoolUpdatedHandler, + processor_name: "deepbook_pool_updated", + event_type: DeepbookPoolUpdated, + db_model: DeepbookPoolUpdatedModel, + table: deepbook_pool_updated, + map_event: |event, meta| DeepbookPoolUpdatedModel { + event_digest: meta.event_digest(), + digest: meta.digest(), + sender: meta.sender(), + checkpoint: meta.checkpoint(), + checkpoint_timestamp_ms: meta.checkpoint_timestamp_ms(), + package: meta.package(), + margin_pool_id: event.margin_pool_id.to_string(), + deepbook_pool_id: event.deepbook_pool_id.to_string(), + pool_cap_id: event.pool_cap_id.to_string(), + enabled: event.enabled, + onchain_timestamp: event.timestamp as i64, + } +} diff --git a/crates/indexer/src/handlers/deepbook_pool_updated_registry_handler.rs b/crates/indexer/src/handlers/deepbook_pool_updated_registry_handler.rs new file mode 100644 index 000000000..e7cf576ed --- /dev/null +++ b/crates/indexer/src/handlers/deepbook_pool_updated_registry_handler.rs @@ -0,0 +1,21 @@ +use crate::models::deepbook_margin::margin_registry::DeepbookPoolUpdated; +use deepbook_schema::models::DeepbookPoolUpdatedRegistry; + +define_handler! { + name: DeepbookPoolUpdatedRegistryHandler, + processor_name: "deepbook_pool_updated_registry", + event_type: DeepbookPoolUpdated, + db_model: DeepbookPoolUpdatedRegistry, + table: deepbook_pool_updated_registry, + map_event: |event, meta| DeepbookPoolUpdatedRegistry { + event_digest: meta.event_digest(), + digest: meta.digest(), + sender: meta.sender(), + checkpoint: meta.checkpoint(), + checkpoint_timestamp_ms: meta.checkpoint_timestamp_ms(), + package: meta.package(), + pool_id: event.pool_id.to_string(), + enabled: event.enabled, + onchain_timestamp: event.timestamp as i64, + } +} diff --git a/crates/indexer/src/handlers/deposit_collateral_handler.rs b/crates/indexer/src/handlers/deposit_collateral_handler.rs new file mode 100644 index 000000000..7cf01ddf1 --- /dev/null +++ b/crates/indexer/src/handlers/deposit_collateral_handler.rs @@ -0,0 +1,36 @@ +use bigdecimal::BigDecimal; + +use crate::models::deepbook_margin::margin_manager::DepositCollateralEvent; +use deepbook_schema::models::CollateralEvent; + +define_handler! { + name: DepositCollateralHandler, + processor_name: "deposit_collateral", + event_type: DepositCollateralEvent, + db_model: CollateralEvent, + table: collateral_events, + map_event: |event, meta| CollateralEvent { + event_digest: meta.event_digest(), + digest: meta.digest(), + sender: meta.sender(), + checkpoint: meta.checkpoint(), + checkpoint_timestamp_ms: meta.checkpoint_timestamp_ms(), + package: meta.package(), + event_type: "deposit".to_string(), + margin_manager_id: event.margin_manager_id.to_string(), + amount: BigDecimal::from(event.amount), + asset_type: event.asset.name.clone(), + pyth_decimals: event.pyth_decimals as i16, + pyth_price: BigDecimal::from(event.pyth_price), + withdraw_base_asset: None, + base_pyth_decimals: None, + base_pyth_price: None, + quote_pyth_decimals: None, + quote_pyth_price: None, + remaining_base_asset: None, + remaining_quote_asset: None, + remaining_base_debt: None, + remaining_quote_debt: None, + onchain_timestamp: event.timestamp as i64, + } +} diff --git a/crates/indexer/src/handlers/flash_loan_handler.rs b/crates/indexer/src/handlers/flash_loan_handler.rs index 35057fd83..d08c3373f 100644 --- a/crates/indexer/src/handlers/flash_loan_handler.rs +++ b/crates/indexer/src/handlers/flash_loan_handler.rs @@ -1,85 +1,22 @@ -use crate::handlers::{is_deepbook_tx, struct_tag, try_extract_move_call_package}; use crate::models::deepbook::vault::FlashLoanBorrowed; -use async_trait::async_trait; use deepbook_schema::models::Flashloan; -use deepbook_schema::schema::flashloans; -use diesel_async::RunQueryDsl; -use move_core_types::account_address::AccountAddress; -use move_core_types::language_storage::StructTag; -use std::sync::Arc; -use sui_indexer_alt_framework::pipeline::concurrent::Handler; -use sui_indexer_alt_framework::pipeline::Processor; -use sui_pg_db::Connection; -use sui_types::full_checkpoint_content::CheckpointData; -use tracing::debug; -pub struct FlashLoanHandler { - event_type: StructTag, -} - -impl FlashLoanHandler { - pub fn new(package_id_override: Option) -> Self { - Self { - event_type: struct_tag::(package_id_override), - } - } -} - -impl Processor for FlashLoanHandler { - const NAME: &'static str = "FlashLoan"; - type Value = Flashloan; - - fn process(&self, checkpoint: &Arc) -> anyhow::Result> { - checkpoint - .transactions - .iter() - .try_fold(vec![], |result, tx| { - if !is_deepbook_tx(tx) { - return Ok(result); - } - let Some(events) = &tx.events else { - return Ok(result); - }; - - let package = try_extract_move_call_package(tx).unwrap_or_default(); - let checkpoint_timestamp_ms = checkpoint.checkpoint_summary.timestamp_ms as i64; - let checkpoint = checkpoint.checkpoint_summary.sequence_number as i64; - let digest = tx.transaction.digest(); - - return events - .data - .iter() - .filter(|ev| ev.type_ == self.event_type) - .enumerate() - .try_fold(result, |mut result, (index, ev)| { - let event: FlashLoanBorrowed = bcs::from_bytes(&ev.contents)?; - let data = Flashloan { - digest: digest.to_string(), - event_digest: format!("{digest}{index}"), - sender: tx.transaction.sender_address().to_string(), - checkpoint, - checkpoint_timestamp_ms, - package: package.clone(), - pool_id: event.pool_id.to_string(), - borrow_quantity: event.borrow_quantity as i64, - borrow: true, - type_name: event.type_name.to_string(), - }; - debug!("Observed Deepbook Flash Loan Borrowed {:?}", data); - result.push(data); - Ok(result) - }); - }) - } -} - -#[async_trait] -impl Handler for FlashLoanHandler { - async fn commit(values: &[Self::Value], conn: &mut Connection<'_>) -> anyhow::Result { - Ok(diesel::insert_into(flashloans::table) - .values(values) - .on_conflict_do_nothing() - .execute(conn) - .await?) +define_handler! { + name: FlashLoanHandler, + processor_name: "flash_loan", + event_type: FlashLoanBorrowed, + db_model: Flashloan, + table: flashloans, + map_event: |event, meta| Flashloan { + event_digest: meta.event_digest(), + digest: meta.digest(), + sender: meta.sender(), + checkpoint: meta.checkpoint(), + checkpoint_timestamp_ms: meta.checkpoint_timestamp_ms(), + package: meta.package(), + pool_id: event.pool_id.to_string(), + borrow_quantity: event.borrow_quantity as i64, + borrow: true, + type_name: event.type_name.to_string(), } } diff --git a/crates/indexer/src/handlers/interest_params_updated_handler.rs b/crates/indexer/src/handlers/interest_params_updated_handler.rs new file mode 100644 index 000000000..3bc9f3c8d --- /dev/null +++ b/crates/indexer/src/handlers/interest_params_updated_handler.rs @@ -0,0 +1,22 @@ +use crate::models::deepbook_margin::margin_pool::InterestParamsUpdated; +use deepbook_schema::models::InterestParamsUpdated as InterestParamsUpdatedModel; + +define_handler! { + name: InterestParamsUpdatedHandler, + processor_name: "interest_params_updated", + event_type: InterestParamsUpdated, + db_model: InterestParamsUpdatedModel, + table: interest_params_updated, + map_event: |event, meta| InterestParamsUpdatedModel { + event_digest: meta.event_digest(), + digest: meta.digest(), + sender: meta.sender(), + checkpoint: meta.checkpoint(), + checkpoint_timestamp_ms: meta.checkpoint_timestamp_ms(), + package: meta.package(), + margin_pool_id: event.margin_pool_id.to_string(), + pool_cap_id: event.pool_cap_id.to_string(), + config_json: serde_json::to_value(&event.interest_config).unwrap(), + onchain_timestamp: event.timestamp as i64, + } +} diff --git a/crates/indexer/src/handlers/liquidation_handler.rs b/crates/indexer/src/handlers/liquidation_handler.rs new file mode 100644 index 000000000..8c0faf5cd --- /dev/null +++ b/crates/indexer/src/handlers/liquidation_handler.rs @@ -0,0 +1,35 @@ +use bigdecimal::BigDecimal; + +use crate::models::deepbook_margin::margin_manager::LiquidationEvent; +use deepbook_schema::models::Liquidation; + +define_handler! { + name: LiquidationHandler, + processor_name: "liquidation", + event_type: LiquidationEvent, + db_model: Liquidation, + table: liquidation, + map_event: |event, meta| Liquidation { + event_digest: meta.event_digest(), + digest: meta.digest(), + sender: meta.sender(), + checkpoint: meta.checkpoint(), + checkpoint_timestamp_ms: meta.checkpoint_timestamp_ms(), + package: meta.package(), + margin_manager_id: event.margin_manager_id.to_string(), + margin_pool_id: event.margin_pool_id.to_string(), + liquidation_amount: event.liquidation_amount as i64, + pool_reward: event.pool_reward as i64, + pool_default: event.pool_default as i64, + risk_ratio: event.risk_ratio as i64, + onchain_timestamp: event.timestamp as i64, + remaining_base_asset: BigDecimal::from(event.remaining_base_asset), + remaining_quote_asset: BigDecimal::from(event.remaining_quote_asset), + remaining_base_debt: BigDecimal::from(event.remaining_base_debt), + remaining_quote_debt: BigDecimal::from(event.remaining_quote_debt), + base_pyth_price: event.base_pyth_price as i64, + base_pyth_decimals: event.base_pyth_decimals as i16, + quote_pyth_price: event.quote_pyth_price as i64, + quote_pyth_decimals: event.quote_pyth_decimals as i16, + } +} diff --git a/crates/indexer/src/handlers/loan_borrowed_handler.rs b/crates/indexer/src/handlers/loan_borrowed_handler.rs new file mode 100644 index 000000000..ab1d60485 --- /dev/null +++ b/crates/indexer/src/handlers/loan_borrowed_handler.rs @@ -0,0 +1,23 @@ +use crate::models::deepbook_margin::margin_manager::LoanBorrowedEvent; +use deepbook_schema::models::LoanBorrowed; + +define_handler! { + name: LoanBorrowedHandler, + processor_name: "loan_borrowed", + event_type: LoanBorrowedEvent, + db_model: LoanBorrowed, + table: loan_borrowed, + map_event: |event, meta| LoanBorrowed { + event_digest: meta.event_digest(), + digest: meta.digest(), + sender: meta.sender(), + checkpoint: meta.checkpoint(), + checkpoint_timestamp_ms: meta.checkpoint_timestamp_ms(), + package: meta.package(), + margin_manager_id: event.margin_manager_id.to_string(), + margin_pool_id: event.margin_pool_id.to_string(), + loan_amount: event.loan_amount as i64, + loan_shares: event.loan_shares as i64, + onchain_timestamp: event.timestamp as i64, + } +} diff --git a/crates/indexer/src/handlers/loan_repaid_handler.rs b/crates/indexer/src/handlers/loan_repaid_handler.rs new file mode 100644 index 000000000..6a089809d --- /dev/null +++ b/crates/indexer/src/handlers/loan_repaid_handler.rs @@ -0,0 +1,23 @@ +use crate::models::deepbook_margin::margin_manager::LoanRepaidEvent; +use deepbook_schema::models::LoanRepaid; + +define_handler! { + name: LoanRepaidHandler, + processor_name: "loan_repaid", + event_type: LoanRepaidEvent, + db_model: LoanRepaid, + table: loan_repaid, + map_event: |event, meta| LoanRepaid { + event_digest: meta.event_digest(), + digest: meta.digest(), + sender: meta.sender(), + checkpoint: meta.checkpoint(), + checkpoint_timestamp_ms: meta.checkpoint_timestamp_ms(), + package: meta.package(), + margin_manager_id: event.margin_manager_id.to_string(), + margin_pool_id: event.margin_pool_id.to_string(), + repay_amount: event.repay_amount as i64, + repay_shares: event.repay_shares as i64, + onchain_timestamp: event.timestamp as i64, + } +} diff --git a/crates/indexer/src/handlers/maintainer_cap_updated_handler.rs b/crates/indexer/src/handlers/maintainer_cap_updated_handler.rs new file mode 100644 index 000000000..6cf469371 --- /dev/null +++ b/crates/indexer/src/handlers/maintainer_cap_updated_handler.rs @@ -0,0 +1,21 @@ +use crate::models::deepbook_margin::margin_registry::MaintainerCapUpdated; +use deepbook_schema::models::MaintainerCapUpdated as MaintainerCapUpdatedModel; + +define_handler! { + name: MaintainerCapUpdatedHandler, + processor_name: "maintainer_cap_updated", + event_type: MaintainerCapUpdated, + db_model: MaintainerCapUpdatedModel, + table: maintainer_cap_updated, + map_event: |event, meta| MaintainerCapUpdatedModel { + event_digest: meta.event_digest(), + digest: meta.digest(), + sender: meta.sender(), + checkpoint: meta.checkpoint(), + checkpoint_timestamp_ms: meta.checkpoint_timestamp_ms(), + package: meta.package(), + maintainer_cap_id: event.maintainer_cap_id.to_string(), + allowed: event.allowed, + onchain_timestamp: event.timestamp as i64, + } +} diff --git a/crates/indexer/src/handlers/maintainer_fees_withdrawn_handler.rs b/crates/indexer/src/handlers/maintainer_fees_withdrawn_handler.rs new file mode 100644 index 000000000..502fb0027 --- /dev/null +++ b/crates/indexer/src/handlers/maintainer_fees_withdrawn_handler.rs @@ -0,0 +1,22 @@ +use crate::models::deepbook_margin::margin_pool::MaintainerFeesWithdrawn; +use deepbook_schema::models::MaintainerFeesWithdrawn as MaintainerFeesWithdrawnModel; + +define_handler! { + name: MaintainerFeesWithdrawnHandler, + processor_name: "maintainer_fees_withdrawn", + event_type: MaintainerFeesWithdrawn, + db_model: MaintainerFeesWithdrawnModel, + table: maintainer_fees_withdrawn, + map_event: |event, meta| MaintainerFeesWithdrawnModel { + event_digest: meta.event_digest(), + digest: meta.digest(), + sender: meta.sender(), + checkpoint: meta.checkpoint(), + checkpoint_timestamp_ms: meta.checkpoint_timestamp_ms(), + package: meta.package(), + margin_pool_id: event.margin_pool_id.to_string(), + margin_pool_cap_id: event.margin_pool_cap_id.to_string(), + maintainer_fees: event.maintainer_fees as i64, + onchain_timestamp: event.timestamp as i64, + } +} diff --git a/crates/indexer/src/handlers/margin_manager_created_handler.rs b/crates/indexer/src/handlers/margin_manager_created_handler.rs new file mode 100644 index 000000000..aff8a9755 --- /dev/null +++ b/crates/indexer/src/handlers/margin_manager_created_handler.rs @@ -0,0 +1,23 @@ +use crate::models::deepbook_margin::margin_manager::MarginManagerCreatedEvent; +use deepbook_schema::models::MarginManagerCreated; + +define_handler! { + name: MarginManagerCreatedHandler, + processor_name: "margin_manager_created", + event_type: MarginManagerCreatedEvent, + db_model: MarginManagerCreated, + table: margin_manager_created, + map_event: |event, meta| MarginManagerCreated { + event_digest: meta.event_digest(), + digest: meta.digest(), + sender: meta.sender(), + checkpoint: meta.checkpoint(), + checkpoint_timestamp_ms: meta.checkpoint_timestamp_ms(), + package: meta.package(), + margin_manager_id: event.margin_manager_id.to_string(), + balance_manager_id: event.balance_manager_id.to_string(), + deepbook_pool_id: Some(event.deepbook_pool_id.to_string()), + owner: event.owner.to_string(), + onchain_timestamp: event.timestamp as i64, + } +} diff --git a/crates/indexer/src/handlers/margin_pool_config_updated_handler.rs b/crates/indexer/src/handlers/margin_pool_config_updated_handler.rs new file mode 100644 index 000000000..e2d1222f8 --- /dev/null +++ b/crates/indexer/src/handlers/margin_pool_config_updated_handler.rs @@ -0,0 +1,22 @@ +use crate::models::deepbook_margin::margin_pool::MarginPoolConfigUpdated; +use deepbook_schema::models::MarginPoolConfigUpdated as MarginPoolConfigUpdatedModel; + +define_handler! { + name: MarginPoolConfigUpdatedHandler, + processor_name: "margin_pool_config_updated", + event_type: MarginPoolConfigUpdated, + db_model: MarginPoolConfigUpdatedModel, + table: margin_pool_config_updated, + map_event: |event, meta| MarginPoolConfigUpdatedModel { + event_digest: meta.event_digest(), + digest: meta.digest(), + sender: meta.sender(), + checkpoint: meta.checkpoint(), + checkpoint_timestamp_ms: meta.checkpoint_timestamp_ms(), + package: meta.package(), + margin_pool_id: event.margin_pool_id.to_string(), + pool_cap_id: event.pool_cap_id.to_string(), + config_json: serde_json::to_value(&event.margin_pool_config).unwrap(), + onchain_timestamp: event.timestamp as i64, + } +} diff --git a/crates/indexer/src/handlers/margin_pool_created_handler.rs b/crates/indexer/src/handlers/margin_pool_created_handler.rs new file mode 100644 index 000000000..7345793fa --- /dev/null +++ b/crates/indexer/src/handlers/margin_pool_created_handler.rs @@ -0,0 +1,23 @@ +use crate::models::deepbook_margin::margin_pool::MarginPoolCreated; +use deepbook_schema::models::MarginPoolCreated as MarginPoolCreatedModel; + +define_handler! { + name: MarginPoolCreatedHandler, + processor_name: "margin_pool_created", + event_type: MarginPoolCreated, + db_model: MarginPoolCreatedModel, + table: margin_pool_created, + map_event: |event, meta| MarginPoolCreatedModel { + event_digest: meta.event_digest(), + digest: meta.digest(), + sender: meta.sender(), + checkpoint: meta.checkpoint(), + checkpoint_timestamp_ms: meta.checkpoint_timestamp_ms(), + package: meta.package(), + margin_pool_id: event.margin_pool_id.to_string(), + maintainer_cap_id: event.maintainer_cap_id.to_string(), + asset_type: event.asset_type.to_string(), + config_json: serde_json::to_value(&event.config).unwrap(), + onchain_timestamp: event.timestamp as i64, + } +} diff --git a/crates/indexer/src/handlers/mod.rs b/crates/indexer/src/handlers/mod.rs index 4fbdfe7c6..33791c559 100644 --- a/crates/indexer/src/handlers/mod.rs +++ b/crates/indexer/src/handlers/mod.rs @@ -1,41 +1,250 @@ -use move_core_types::account_address::AccountAddress; -use move_core_types::language_storage::StructTag as MoveStructTag; -use move_types::MoveStruct; -use std::str::FromStr; -use sui_sdk_types::StructTag; -use sui_types::full_checkpoint_content::CheckpointTransaction; +use crate::DeepbookEnv; +use std::sync::Arc; +use sui_indexer_alt_framework::types::full_checkpoint_content::{ + Checkpoint, ExecutedTransaction, ObjectSet, +}; +use sui_types::effects::TransactionEffectsAPI; use sui_types::transaction::{Command, TransactionDataAPI}; +/// Captures common transaction metadata for event processing. +/// Used by the `define_handler!` macro to avoid repetitive field extraction. +pub struct EventMeta { + digest: Arc, + sender: Arc, + checkpoint: i64, + checkpoint_timestamp_ms: i64, + package: Arc, + event_index: usize, +} + +impl EventMeta { + pub fn from_checkpoint_tx(checkpoint: &Checkpoint, tx: &ExecutedTransaction) -> Self { + Self { + digest: tx.effects.transaction_digest().to_string().into(), + sender: tx.transaction.sender().to_string().into(), + checkpoint: checkpoint.summary.sequence_number as i64, + checkpoint_timestamp_ms: checkpoint.summary.timestamp_ms as i64, + package: try_extract_move_call_package(tx).unwrap_or_default().into(), + event_index: 0, + } + } + + pub fn with_index(&self, index: usize) -> Self { + Self { + digest: Arc::clone(&self.digest), + sender: Arc::clone(&self.sender), + checkpoint: self.checkpoint, + checkpoint_timestamp_ms: self.checkpoint_timestamp_ms, + package: Arc::clone(&self.package), + event_index: index, + } + } + + pub fn event_digest(&self) -> String { + format!("{}{}", self.digest, self.event_index) + } + + pub fn digest(&self) -> String { + self.digest.to_string() + } + + pub fn sender(&self) -> String { + self.sender.to_string() + } + + pub fn checkpoint(&self) -> i64 { + self.checkpoint + } + + pub fn checkpoint_timestamp_ms(&self) -> i64 { + self.checkpoint_timestamp_ms + } + + pub fn package(&self) -> String { + self.package.to_string() + } +} + +/// Macro to generate a complete handler from minimal configuration. +/// +/// This macro generates the handler struct, constructor, `Processor` impl, +/// and `Handler` impl from a declarative specification. +/// +/// # Example +/// ```ignore +/// define_handler! { +/// name: BalancesHandler, +/// processor_name: "balances", +/// event_type: BalanceEvent, +/// db_model: Balances, +/// table: balances, +/// map_event: |event, meta| Balances { +/// event_digest: meta.event_digest(), +/// digest: meta.digest(), +/// // ... field mappings +/// } +/// } +/// ``` +#[macro_export] +macro_rules! define_handler { + { + name: $handler:ident, + processor_name: $proc_name:literal, + event_type: $event:ty, + db_model: $model:ty, + table: $table:ident, + map_event: |$ev:ident, $meta:ident| $body:expr + } => { + pub struct $handler { + env: $crate::DeepbookEnv, + } + + impl $handler { + pub fn new(env: $crate::DeepbookEnv) -> Self { + Self { env } + } + } + + #[async_trait::async_trait] + impl sui_indexer_alt_framework::pipeline::Processor for $handler { + const NAME: &'static str = $proc_name; + type Value = $model; + + async fn process( + &self, + checkpoint: &std::sync::Arc, + ) -> anyhow::Result> { + use $crate::handlers::{is_deepbook_tx, EventMeta}; + use $crate::traits::MoveStruct; + + let mut results = vec![]; + for tx in &checkpoint.transactions { + if !is_deepbook_tx(tx, &checkpoint.object_set, self.env) { + continue; + } + let Some(events) = &tx.events else { continue }; + + let base_meta = EventMeta::from_checkpoint_tx(checkpoint, tx); + + for (index, ev) in events.data.iter().enumerate() { + if <$event>::matches_event_type(&ev.type_, self.env) { + let $ev: $event = bcs::from_bytes(&ev.contents)?; + let $meta = base_meta.with_index(index); + results.push($body); + tracing::debug!("Observed {} event", $proc_name); + } + } + } + Ok(results) + } + } + + #[async_trait::async_trait] + impl sui_indexer_alt_framework::postgres::handler::Handler for $handler { + async fn commit<'a>( + values: &[Self::Value], + conn: &mut sui_pg_db::Connection<'a>, + ) -> anyhow::Result { + use diesel_async::RunQueryDsl; + Ok(diesel::insert_into(deepbook_schema::schema::$table::table) + .values(values) + .on_conflict_do_nothing() + .execute(conn) + .await?) + } + } + }; +} +pub mod asset_supplied_handler; +pub mod asset_withdrawn_handler; pub mod balances_handler; +pub mod conditional_order_added_handler; +pub mod conditional_order_cancelled_handler; +pub mod conditional_order_executed_handler; +pub mod conditional_order_insufficient_funds_handler; +pub mod deep_burned_handler; +pub mod deepbook_pool_config_updated_handler; +pub mod deepbook_pool_registered_handler; +pub mod deepbook_pool_updated_handler; +pub mod deepbook_pool_updated_registry_handler; +pub mod deposit_collateral_handler; pub mod flash_loan_handler; +pub mod interest_params_updated_handler; +pub mod liquidation_handler; +pub mod loan_borrowed_handler; +pub mod loan_repaid_handler; +pub mod maintainer_cap_updated_handler; +pub mod maintainer_fees_withdrawn_handler; +pub mod margin_manager_created_handler; +pub mod margin_pool_config_updated_handler; +pub mod margin_pool_created_handler; pub mod order_fill_handler; pub mod order_update_handler; +pub mod pause_cap_updated_handler; +pub mod pool_created_handler; pub mod pool_price_handler; pub mod proposals_handler; +pub mod protocol_fees_increased_handler; +pub mod protocol_fees_withdrawn_handler; pub mod rebates_handler; +pub mod referral_fee_event_handler; +pub mod referral_fees_claimed_handler; pub mod stakes_handler; +pub mod supplier_cap_minted_handler; +pub mod supply_referral_minted_handler; pub mod trade_params_update_handler; pub mod vote_handler; +pub mod withdraw_collateral_handler; -const DEEPBOOK_PKG_ADDRESS: AccountAddress = - AccountAddress::new(*crate::models::deepbook::registry::PACKAGE_ID.inner()); - -// Convert rust sdk struct tag to move struct tag. -pub(crate) fn convert_struct_tag(tag: StructTag) -> MoveStructTag { - MoveStructTag::from_str(&tag.to_string()).unwrap() -} +pub(crate) fn is_deepbook_tx( + tx: &ExecutedTransaction, + checkpoint_objects: &ObjectSet, + env: DeepbookEnv, +) -> bool { + let deepbook_addresses = env.package_addresses(); + let deepbook_packages = env.package_ids(); -pub(crate) fn is_deepbook_tx(tx: &CheckpointTransaction) -> bool { - tx.input_objects.iter().any(|obj| { + // Check input objects against all known package versions + let has_deepbook_input = tx.input_objects(checkpoint_objects).any(|obj| { obj.data .type_() - .map(|t| t.address() == DEEPBOOK_PKG_ADDRESS) + .map(|t| deepbook_addresses.iter().any(|addr| t.address() == *addr)) .unwrap_or_default() - }) + }); + + if has_deepbook_input { + return true; + } + + // Check if transaction has deepbook events from any version + if let Some(events) = &tx.events { + let has_deepbook_event = events.data.iter().any(|event| { + deepbook_addresses + .iter() + .any(|addr| event.type_.address == *addr) + }); + if has_deepbook_event { + return true; + } + } + + // Check if transaction calls a deepbook function from any version + let txn_kind = tx.transaction.kind(); + let has_deepbook_call = txn_kind.iter_commands().any(|cmd| { + if let Command::MoveCall(move_call) = cmd { + deepbook_packages + .iter() + .any(|pkg| *pkg == move_call.package) + } else { + false + } + }); + + has_deepbook_call } -pub(crate) fn try_extract_move_call_package(tx: &CheckpointTransaction) -> Option { - let txn_kind = tx.transaction.transaction_data().kind(); +pub(crate) fn try_extract_move_call_package(tx: &ExecutedTransaction) -> Option { + let txn_kind = tx.transaction.kind(); let first_command = txn_kind.iter_commands().next()?; if let Command::MoveCall(move_call) = first_command { Some(move_call.package.to_string()) @@ -43,13 +252,3 @@ pub(crate) fn try_extract_move_call_package(tx: &CheckpointTransaction) -> Optio None } } - -fn struct_tag( - package_id_override: Option, -) -> move_core_types::language_storage::StructTag { - let mut event_type = convert_struct_tag(T::struct_type()); - if let Some(package_id_override) = package_id_override { - event_type.address = package_id_override; - } - event_type -} diff --git a/crates/indexer/src/handlers/order_fill_handler.rs b/crates/indexer/src/handlers/order_fill_handler.rs index 4f2b0fcd4..630be4a96 100644 --- a/crates/indexer/src/handlers/order_fill_handler.rs +++ b/crates/indexer/src/handlers/order_fill_handler.rs @@ -1,97 +1,34 @@ -use crate::handlers::{is_deepbook_tx, struct_tag, try_extract_move_call_package}; use crate::models::deepbook::order_info::OrderFilled; -use async_trait::async_trait; use deepbook_schema::models::OrderFill; -use deepbook_schema::schema::order_fills; -use diesel_async::RunQueryDsl; -use move_core_types::account_address::AccountAddress; -use move_core_types::language_storage::StructTag; -use std::sync::Arc; -use sui_indexer_alt_framework::pipeline::concurrent::Handler; -use sui_indexer_alt_framework::pipeline::Processor; -use sui_pg_db::Connection; -use sui_types::full_checkpoint_content::CheckpointData; -use tracing::debug; -pub struct OrderFillHandler { - event_type: StructTag, -} - -impl OrderFillHandler { - pub fn new(package_id_override: Option) -> Self { - Self { - event_type: struct_tag::(package_id_override), - } - } -} - -impl Processor for OrderFillHandler { - const NAME: &'static str = "OrderFill"; - type Value = OrderFill; - - fn process(&self, checkpoint: &Arc) -> anyhow::Result> { - checkpoint - .transactions - .iter() - .try_fold(vec![], |result, tx| { - if !is_deepbook_tx(tx) { - return Ok(result); - } - let Some(events) = &tx.events else { - return Ok(result); - }; - - let package = try_extract_move_call_package(tx).unwrap_or_default(); - let checkpoint_timestamp_ms = checkpoint.checkpoint_summary.timestamp_ms as i64; - let checkpoint = checkpoint.checkpoint_summary.sequence_number as i64; - let digest = tx.transaction.digest(); - - return events - .data - .iter() - .filter(|ev| ev.type_ == self.event_type) - .enumerate() - .try_fold(result, |mut result, (index, ev)| { - let event: OrderFilled = bcs::from_bytes(&ev.contents)?; - let data = OrderFill { - digest: digest.to_string(), - event_digest: format!("{digest}{index}"), - sender: tx.transaction.sender_address().to_string(), - checkpoint, - checkpoint_timestamp_ms, - package: package.clone(), - pool_id: event.pool_id.to_string(), - maker_order_id: event.maker_order_id.to_string(), - taker_order_id: event.taker_order_id.to_string(), - maker_client_order_id: event.maker_client_order_id as i64, - taker_client_order_id: event.taker_client_order_id as i64, - price: event.price as i64, - taker_is_bid: event.taker_is_bid, - taker_fee: event.taker_fee as i64, - taker_fee_is_deep: event.taker_fee_is_deep, - maker_fee: event.maker_fee as i64, - maker_fee_is_deep: event.maker_fee_is_deep, - base_quantity: event.base_quantity as i64, - quote_quantity: event.quote_quantity as i64, - maker_balance_manager_id: event.maker_balance_manager_id.to_string(), - taker_balance_manager_id: event.taker_balance_manager_id.to_string(), - onchain_timestamp: event.timestamp as i64, - }; - debug!("Observed Deepbook Order Filled {:?}", data); - result.push(data); - Ok(result) - }); - }) - } -} - -#[async_trait] -impl Handler for OrderFillHandler { - async fn commit(values: &[Self::Value], conn: &mut Connection<'_>) -> anyhow::Result { - Ok(diesel::insert_into(order_fills::table) - .values(values) - .on_conflict_do_nothing() - .execute(conn) - .await?) +define_handler! { + name: OrderFillHandler, + processor_name: "order_fill", + event_type: OrderFilled, + db_model: OrderFill, + table: order_fills, + map_event: |event, meta| OrderFill { + event_digest: meta.event_digest(), + digest: meta.digest(), + sender: meta.sender(), + checkpoint: meta.checkpoint(), + checkpoint_timestamp_ms: meta.checkpoint_timestamp_ms(), + package: meta.package(), + pool_id: event.pool_id.to_string(), + maker_order_id: event.maker_order_id.to_string(), + taker_order_id: event.taker_order_id.to_string(), + maker_client_order_id: event.maker_client_order_id as i64, + taker_client_order_id: event.taker_client_order_id as i64, + price: event.price as i64, + taker_is_bid: event.taker_is_bid, + taker_fee: event.taker_fee as i64, + taker_fee_is_deep: event.taker_fee_is_deep, + maker_fee: event.maker_fee as i64, + maker_fee_is_deep: event.maker_fee_is_deep, + base_quantity: event.base_quantity as i64, + quote_quantity: event.quote_quantity as i64, + maker_balance_manager_id: event.maker_balance_manager_id.to_string(), + taker_balance_manager_id: event.taker_balance_manager_id.to_string(), + onchain_timestamp: event.timestamp as i64, } } diff --git a/crates/indexer/src/handlers/order_update_handler.rs b/crates/indexer/src/handlers/order_update_handler.rs index f57194a6f..086d531ce 100644 --- a/crates/indexer/src/handlers/order_update_handler.rs +++ b/crates/indexer/src/handlers/order_update_handler.rs @@ -1,92 +1,87 @@ -use crate::handlers::{is_deepbook_tx, struct_tag, try_extract_move_call_package}; +use crate::handlers::{is_deepbook_tx, try_extract_move_call_package}; use crate::models::deepbook::order::{OrderCanceled, OrderModified}; use crate::models::deepbook::order_info::{OrderExpired, OrderPlaced}; +use crate::traits::MoveStruct; +use crate::DeepbookEnv; +use async_trait::async_trait; use deepbook_schema::models::{OrderUpdate, OrderUpdateStatus}; use deepbook_schema::schema::order_updates; use diesel_async::RunQueryDsl; -use move_core_types::account_address::AccountAddress; -use move_core_types::language_storage::StructTag; use std::sync::Arc; -use sui_indexer_alt_framework::pipeline::concurrent::Handler; use sui_indexer_alt_framework::pipeline::Processor; -use sui_pg_db::Connection; -use sui_types::full_checkpoint_content::CheckpointData; +use sui_indexer_alt_framework::postgres::handler::Handler; +use sui_indexer_alt_framework::postgres::Connection; +use sui_indexer_alt_framework::types::full_checkpoint_content::Checkpoint; +use sui_types::transaction::TransactionDataAPI; use tracing::debug; type TransactionMetadata = (String, u64, u64, String, String); pub struct OrderUpdateHandler { - order_placed_type: StructTag, - order_modified_type: StructTag, - order_canceled_type: StructTag, - order_expired_type: StructTag, + env: DeepbookEnv, } impl OrderUpdateHandler { - pub fn new(package_id_override: Option) -> Self { - Self { - order_placed_type: struct_tag::(package_id_override), - order_modified_type: struct_tag::(package_id_override), - order_canceled_type: struct_tag::(package_id_override), - order_expired_type: struct_tag::(package_id_override), - } + pub fn new(env: DeepbookEnv) -> Self { + Self { env } } } +#[async_trait] impl Processor for OrderUpdateHandler { - const NAME: &'static str = "OrderUpdate"; + const NAME: &'static str = "order_update"; type Value = OrderUpdate; - fn process(&self, checkpoint: &Arc) -> anyhow::Result> { - checkpoint - .transactions - .iter() - .try_fold(vec![], |result, tx| { - if !is_deepbook_tx(tx) { - return Ok(result); - } - let Some(events) = &tx.events else { - return Ok(result); - }; - let package = try_extract_move_call_package(tx).unwrap_or_default(); - let metadata = ( - tx.transaction.sender_address().to_string(), - checkpoint.checkpoint_summary.sequence_number, - checkpoint.checkpoint_summary.timestamp_ms, - tx.transaction.digest().to_string(), - package.clone(), - ); + async fn process(&self, checkpoint: &Arc) -> anyhow::Result> { + let mut results = vec![]; + + for tx in &checkpoint.transactions { + if !is_deepbook_tx(tx, &checkpoint.object_set, self.env) { + continue; + } + let Some(events) = &tx.events else { + continue; + }; - return events.data.iter().enumerate().try_fold( - result, - |mut result, (index, ev)| { - if ev.type_ == self.order_placed_type { - let event = bcs::from_bytes(&ev.contents)?; - result.push(process_order_placed(event, metadata.clone(), index)); - debug!("Observed Deepbook Order Placed {:?}", tx); - } else if ev.type_ == self.order_modified_type { - let event = bcs::from_bytes(&ev.contents)?; - result.push(process_order_modified(event, metadata.clone(), index)); - debug!("Observed Deepbook Order Modified {:?}", tx); - } else if ev.type_ == self.order_canceled_type { - let event = bcs::from_bytes(&ev.contents)?; - result.push(process_order_canceled(event, metadata.clone(), index)); - debug!("Observed Deepbook Order Canceled {:?}", tx); - } else if ev.type_ == self.order_expired_type { - let event = bcs::from_bytes(&ev.contents)?; - result.push(process_order_expired(event, metadata.clone(), index)); - debug!("Observed Deepbook Order Expired {:?}", tx); - } - Ok(result) - }, - ); - }) + let package = try_extract_move_call_package(tx).unwrap_or_default(); + let metadata = ( + tx.transaction.sender().to_string(), + checkpoint.summary.sequence_number, + checkpoint.summary.timestamp_ms, + tx.transaction.digest().to_string(), + package.clone(), + ); + + for (index, ev) in events.data.iter().enumerate() { + if OrderPlaced::matches_event_type(&ev.type_, self.env) { + let event = bcs::from_bytes(&ev.contents)?; + results.push(process_order_placed(event, metadata.clone(), index)); + debug!("Observed Deepbook Order Placed {:?}", tx); + } else if OrderModified::matches_event_type(&ev.type_, self.env) { + let event = bcs::from_bytes(&ev.contents)?; + results.push(process_order_modified(event, metadata.clone(), index)); + debug!("Observed Deepbook Order Modified {:?}", tx); + } else if OrderCanceled::matches_event_type(&ev.type_, self.env) { + let event = bcs::from_bytes(&ev.contents)?; + results.push(process_order_canceled(event, metadata.clone(), index)); + debug!("Observed Deepbook Order Canceled {:?}", tx); + } else if OrderExpired::matches_event_type(&ev.type_, self.env) { + let event = bcs::from_bytes(&ev.contents)?; + results.push(process_order_expired(event, metadata.clone(), index)); + debug!("Observed Deepbook Order Expired {:?}", tx); + } + } + } + Ok(results) } } -#[async_trait::async_trait] +#[async_trait] impl Handler for OrderUpdateHandler { - async fn commit(values: &[Self::Value], conn: &mut Connection<'_>) -> anyhow::Result { + async fn commit<'a>( + values: &[Self::Value], + conn: &mut Connection<'a>, + ) -> anyhow::Result { Ok(diesel::insert_into(order_updates::table) .values(values) .on_conflict_do_nothing() diff --git a/crates/indexer/src/handlers/pause_cap_updated_handler.rs b/crates/indexer/src/handlers/pause_cap_updated_handler.rs new file mode 100644 index 000000000..d750d201d --- /dev/null +++ b/crates/indexer/src/handlers/pause_cap_updated_handler.rs @@ -0,0 +1,21 @@ +use crate::models::deepbook_margin::margin_registry::PauseCapUpdated; +use deepbook_schema::models::PauseCapUpdated as PauseCapUpdatedModel; + +define_handler! { + name: PauseCapUpdatedHandler, + processor_name: "pause_cap_updated", + event_type: PauseCapUpdated, + db_model: PauseCapUpdatedModel, + table: pause_cap_updated, + map_event: |event, meta| PauseCapUpdatedModel { + event_digest: meta.event_digest(), + digest: meta.digest(), + sender: meta.sender(), + checkpoint: meta.checkpoint(), + checkpoint_timestamp_ms: meta.checkpoint_timestamp_ms(), + package: meta.package(), + pause_cap_id: event.pause_cap_id.to_string(), + allowed: event.allowed, + onchain_timestamp: event.timestamp as i64, + } +} diff --git a/crates/indexer/src/handlers/pool_created_handler.rs b/crates/indexer/src/handlers/pool_created_handler.rs new file mode 100644 index 000000000..e8ebbf7fd --- /dev/null +++ b/crates/indexer/src/handlers/pool_created_handler.rs @@ -0,0 +1,91 @@ +use crate::handlers::{is_deepbook_tx, try_extract_move_call_package}; +use crate::models::deepbook::pool::PoolCreated as PoolCreatedEvent; +use crate::models::sui::sui::SUI; +use crate::traits::MoveStruct; +use crate::DeepbookEnv; +use async_trait::async_trait; +use deepbook_schema::models::PoolCreated; +use deepbook_schema::schema::pool_created; +use diesel_async::RunQueryDsl; +use std::sync::Arc; +use sui_indexer_alt_framework::pipeline::Processor; +use sui_indexer_alt_framework::postgres::handler::Handler; +use sui_indexer_alt_framework::postgres::Connection; +use sui_indexer_alt_framework::types::full_checkpoint_content::Checkpoint; +use sui_types::transaction::TransactionDataAPI; +use tracing::debug; + +pub struct PoolCreatedHandler { + env: DeepbookEnv, +} + +impl PoolCreatedHandler { + pub fn new(env: DeepbookEnv) -> Self { + Self { env } + } +} + +#[async_trait] +impl Processor for PoolCreatedHandler { + const NAME: &'static str = "pool_created"; + type Value = PoolCreated; + + async fn process(&self, checkpoint: &Arc) -> anyhow::Result> { + let mut results = vec![]; + + for tx in &checkpoint.transactions { + if !is_deepbook_tx(tx, &checkpoint.object_set, self.env) { + continue; + } + let Some(events) = &tx.events else { + continue; + }; + + let package = try_extract_move_call_package(tx).unwrap_or_default(); + let checkpoint_timestamp_ms = checkpoint.summary.timestamp_ms as i64; + let checkpoint_seq = checkpoint.summary.sequence_number as i64; + let digest = tx.transaction.digest(); + + for (index, ev) in events.data.iter().enumerate() { + if !PoolCreatedEvent::::matches_event_type(&ev.type_, self.env) { + continue; + } + + let event: PoolCreatedEvent = bcs::from_bytes(&ev.contents)?; + let data = PoolCreated { + digest: digest.to_string(), + event_digest: format!("{digest}{index}"), + sender: tx.transaction.sender().to_string(), + checkpoint: checkpoint_seq, + checkpoint_timestamp_ms, + package: package.clone(), + pool_id: event.pool_id.to_string(), + taker_fee: event.taker_fee as i64, + maker_fee: event.maker_fee as i64, + tick_size: event.tick_size as i64, + lot_size: event.lot_size as i64, + min_size: event.min_size as i64, + whitelisted_pool: event.whitelisted_pool, + treasury_address: event.treasury_address.to_string(), + }; + debug!("Observed Deepbook PoolCreated {:?}", data); + results.push(data); + } + } + Ok(results) + } +} + +#[async_trait] +impl Handler for PoolCreatedHandler { + async fn commit<'a>( + values: &[Self::Value], + conn: &mut Connection<'a>, + ) -> anyhow::Result { + Ok(diesel::insert_into(pool_created::table) + .values(values) + .on_conflict_do_nothing() + .execute(conn) + .await?) + } +} diff --git a/crates/indexer/src/handlers/pool_price_handler.rs b/crates/indexer/src/handlers/pool_price_handler.rs index ede18d74a..634f68772 100644 --- a/crates/indexer/src/handlers/pool_price_handler.rs +++ b/crates/indexer/src/handlers/pool_price_handler.rs @@ -1,84 +1,21 @@ -use crate::handlers::{is_deepbook_tx, struct_tag, try_extract_move_call_package}; use crate::models::deepbook::deep_price::PriceAdded; -use async_trait::async_trait; use deepbook_schema::models::PoolPrice; -use deepbook_schema::schema::pool_prices; -use diesel_async::RunQueryDsl; -use move_core_types::account_address::AccountAddress; -use move_core_types::language_storage::StructTag; -use std::sync::Arc; -use sui_indexer_alt_framework::pipeline::concurrent::Handler; -use sui_indexer_alt_framework::pipeline::Processor; -use sui_pg_db::Connection; -use sui_types::full_checkpoint_content::CheckpointData; -use tracing::debug; -pub struct PoolPriceHandler { - event_type: StructTag, -} - -impl PoolPriceHandler { - pub fn new(package_id_override: Option) -> Self { - Self { - event_type: struct_tag::(package_id_override), - } - } -} - -impl Processor for PoolPriceHandler { - const NAME: &'static str = "PoolPrice"; - type Value = PoolPrice; - - fn process(&self, checkpoint: &Arc) -> anyhow::Result> { - checkpoint - .transactions - .iter() - .try_fold(vec![], |result, tx| { - if !is_deepbook_tx(tx) { - return Ok(result); - } - let Some(events) = &tx.events else { - return Ok(result); - }; - - let package = try_extract_move_call_package(tx).unwrap_or_default(); - let checkpoint_timestamp_ms = checkpoint.checkpoint_summary.timestamp_ms as i64; - let checkpoint = checkpoint.checkpoint_summary.sequence_number as i64; - let digest = tx.transaction.digest(); - - return events - .data - .iter() - .filter(|ev| ev.type_ == self.event_type) - .enumerate() - .try_fold(result, |mut result, (index, ev)| { - let event: PriceAdded = bcs::from_bytes(&ev.contents)?; - let data = PoolPrice { - digest: digest.to_string(), - event_digest: format!("{digest}{index}"), - sender: tx.transaction.sender_address().to_string(), - checkpoint, - checkpoint_timestamp_ms, - package: package.clone(), - target_pool: event.target_pool.to_string(), - conversion_rate: event.conversion_rate as i64, - reference_pool: event.reference_pool.to_string(), - }; - debug!("Observed Deepbook Price Addition {:?}", data); - result.push(data); - Ok(result) - }); - }) - } -} - -#[async_trait] -impl Handler for PoolPriceHandler { - async fn commit(values: &[Self::Value], conn: &mut Connection<'_>) -> anyhow::Result { - Ok(diesel::insert_into(pool_prices::table) - .values(values) - .on_conflict_do_nothing() - .execute(conn) - .await?) +define_handler! { + name: PoolPriceHandler, + processor_name: "pool_price", + event_type: PriceAdded, + db_model: PoolPrice, + table: pool_prices, + map_event: |event, meta| PoolPrice { + event_digest: meta.event_digest(), + digest: meta.digest(), + sender: meta.sender(), + checkpoint: meta.checkpoint(), + checkpoint_timestamp_ms: meta.checkpoint_timestamp_ms(), + package: meta.package(), + target_pool: event.target_pool.to_string(), + conversion_rate: event.conversion_rate as i64, + reference_pool: event.reference_pool.to_string(), } } diff --git a/crates/indexer/src/handlers/proposals_handler.rs b/crates/indexer/src/handlers/proposals_handler.rs index 2088e8df6..fd311167d 100644 --- a/crates/indexer/src/handlers/proposals_handler.rs +++ b/crates/indexer/src/handlers/proposals_handler.rs @@ -1,87 +1,24 @@ -use crate::handlers::{is_deepbook_tx, struct_tag, try_extract_move_call_package}; use crate::models::deepbook::state::ProposalEvent; -use async_trait::async_trait; use deepbook_schema::models::Proposals; -use deepbook_schema::schema::proposals; -use diesel_async::RunQueryDsl; -use move_core_types::account_address::AccountAddress; -use move_core_types::language_storage::StructTag; -use std::sync::Arc; -use sui_indexer_alt_framework::pipeline::concurrent::Handler; -use sui_indexer_alt_framework::pipeline::Processor; -use sui_pg_db::Connection; -use sui_types::full_checkpoint_content::CheckpointData; -use tracing::debug; -pub struct ProposalsHandler { - event_type: StructTag, -} - -impl ProposalsHandler { - pub fn new(package_id_override: Option) -> Self { - Self { - event_type: struct_tag::(package_id_override), - } - } -} - -impl Processor for ProposalsHandler { - const NAME: &'static str = "Proposals"; - type Value = Proposals; - - fn process(&self, checkpoint: &Arc) -> anyhow::Result> { - checkpoint - .transactions - .iter() - .try_fold(vec![], |result, tx| { - if !is_deepbook_tx(tx) { - return Ok(result); - } - let Some(events) = &tx.events else { - return Ok(result); - }; - - let package = try_extract_move_call_package(tx).unwrap_or_default(); - let checkpoint_timestamp_ms = checkpoint.checkpoint_summary.timestamp_ms as i64; - let checkpoint = checkpoint.checkpoint_summary.sequence_number as i64; - let digest = tx.transaction.digest(); - - return events - .data - .iter() - .filter(|ev| ev.type_ == self.event_type) - .enumerate() - .try_fold(result, |mut result, (index, ev)| { - let event: ProposalEvent = bcs::from_bytes(&ev.contents)?; - let data = Proposals { - digest: digest.to_string(), - event_digest: format!("{digest}{index}"), - sender: tx.transaction.sender_address().to_string(), - checkpoint, - checkpoint_timestamp_ms, - package: package.clone(), - pool_id: event.pool_id.to_string(), - balance_manager_id: event.balance_manager_id.to_string(), - epoch: event.epoch as i64, - taker_fee: event.taker_fee as i64, - maker_fee: event.maker_fee as i64, - stake_required: event.stake_required as i64, - }; - debug!("Observed Deepbook Proposal Event {:?}", data); - result.push(data); - Ok(result) - }); - }) - } -} - -#[async_trait] -impl Handler for ProposalsHandler { - async fn commit(values: &[Self::Value], conn: &mut Connection<'_>) -> anyhow::Result { - Ok(diesel::insert_into(proposals::table) - .values(values) - .on_conflict_do_nothing() - .execute(conn) - .await?) +define_handler! { + name: ProposalsHandler, + processor_name: "proposals", + event_type: ProposalEvent, + db_model: Proposals, + table: proposals, + map_event: |event, meta| Proposals { + event_digest: meta.event_digest(), + digest: meta.digest(), + sender: meta.sender(), + checkpoint: meta.checkpoint(), + checkpoint_timestamp_ms: meta.checkpoint_timestamp_ms(), + package: meta.package(), + pool_id: event.pool_id.to_string(), + balance_manager_id: event.balance_manager_id.to_string(), + epoch: event.epoch as i64, + taker_fee: event.taker_fee as i64, + maker_fee: event.maker_fee as i64, + stake_required: event.stake_required as i64, } } diff --git a/crates/indexer/src/handlers/protocol_fees_increased_handler.rs b/crates/indexer/src/handlers/protocol_fees_increased_handler.rs new file mode 100644 index 000000000..c72685327 --- /dev/null +++ b/crates/indexer/src/handlers/protocol_fees_increased_handler.rs @@ -0,0 +1,24 @@ +use crate::models::deepbook_margin::protocol_fees::ProtocolFeesIncreasedEvent; +use deepbook_schema::models::ProtocolFeesIncreasedEvent as ProtocolFeesIncreasedEventModel; + +define_handler! { + name: ProtocolFeesIncreasedHandler, + processor_name: "protocol_fees_increased", + event_type: ProtocolFeesIncreasedEvent, + db_model: ProtocolFeesIncreasedEventModel, + table: protocol_fees_increased, + map_event: |event, meta| ProtocolFeesIncreasedEventModel { + event_digest: meta.event_digest(), + digest: meta.digest(), + sender: meta.sender(), + checkpoint: meta.checkpoint(), + checkpoint_timestamp_ms: meta.checkpoint_timestamp_ms(), + package: meta.package(), + margin_pool_id: event.margin_pool_id.to_string(), + total_shares: event.total_shares as i64, + referral_fees: event.referral_fees as i64, + maintainer_fees: event.maintainer_fees as i64, + protocol_fees: event.protocol_fees as i64, + onchain_timestamp: meta.checkpoint_timestamp_ms(), // No timestamp in event + } +} diff --git a/crates/indexer/src/handlers/protocol_fees_withdrawn_handler.rs b/crates/indexer/src/handlers/protocol_fees_withdrawn_handler.rs new file mode 100644 index 000000000..b47929397 --- /dev/null +++ b/crates/indexer/src/handlers/protocol_fees_withdrawn_handler.rs @@ -0,0 +1,21 @@ +use crate::models::deepbook_margin::margin_pool::ProtocolFeesWithdrawn; +use deepbook_schema::models::ProtocolFeesWithdrawn as ProtocolFeesWithdrawnModel; + +define_handler! { + name: ProtocolFeesWithdrawnHandler, + processor_name: "protocol_fees_withdrawn", + event_type: ProtocolFeesWithdrawn, + db_model: ProtocolFeesWithdrawnModel, + table: protocol_fees_withdrawn, + map_event: |event, meta| ProtocolFeesWithdrawnModel { + event_digest: meta.event_digest(), + digest: meta.digest(), + sender: meta.sender(), + checkpoint: meta.checkpoint(), + checkpoint_timestamp_ms: meta.checkpoint_timestamp_ms(), + package: meta.package(), + margin_pool_id: event.margin_pool_id.to_string(), + protocol_fees: event.protocol_fees as i64, + onchain_timestamp: event.timestamp as i64, + } +} diff --git a/crates/indexer/src/handlers/rebates_handler.rs b/crates/indexer/src/handlers/rebates_handler.rs index 5d7b22e68..8b1bb9180 100644 --- a/crates/indexer/src/handlers/rebates_handler.rs +++ b/crates/indexer/src/handlers/rebates_handler.rs @@ -1,85 +1,22 @@ -use crate::handlers::{is_deepbook_tx, struct_tag, try_extract_move_call_package}; use crate::models::deepbook::state::RebateEvent; -use async_trait::async_trait; use deepbook_schema::models::Rebates; -use deepbook_schema::schema::rebates; -use diesel_async::RunQueryDsl; -use move_core_types::account_address::AccountAddress; -use move_core_types::language_storage::StructTag; -use std::sync::Arc; -use sui_indexer_alt_framework::pipeline::concurrent::Handler; -use sui_indexer_alt_framework::pipeline::Processor; -use sui_pg_db::Connection; -use sui_types::full_checkpoint_content::CheckpointData; -use tracing::debug; -pub struct RebatesHandler { - event_type: StructTag, -} - -impl RebatesHandler { - pub fn new(package_id_override: Option) -> Self { - Self { - event_type: struct_tag::(package_id_override), - } - } -} - -impl Processor for RebatesHandler { - const NAME: &'static str = "Rebates"; - type Value = Rebates; - - fn process(&self, checkpoint: &Arc) -> anyhow::Result> { - checkpoint - .transactions - .iter() - .try_fold(vec![], |result, tx| { - if !is_deepbook_tx(tx) { - return Ok(result); - } - let Some(events) = &tx.events else { - return Ok(result); - }; - - let package = try_extract_move_call_package(tx).unwrap_or_default(); - let checkpoint_timestamp_ms = checkpoint.checkpoint_summary.timestamp_ms as i64; - let checkpoint = checkpoint.checkpoint_summary.sequence_number as i64; - let digest = tx.transaction.digest(); - - return events - .data - .iter() - .filter(|ev| ev.type_ == self.event_type) - .enumerate() - .try_fold(result, |mut result, (index, ev)| { - let event: RebateEvent = bcs::from_bytes(&ev.contents)?; - let data = Rebates { - digest: digest.to_string(), - event_digest: format!("{digest}{index}"), - sender: tx.transaction.sender_address().to_string(), - checkpoint, - checkpoint_timestamp_ms, - package: package.clone(), - pool_id: event.pool_id.to_string(), - balance_manager_id: event.balance_manager_id.to_string(), - epoch: event.epoch as i64, - claim_amount: event.claim_amount as i64, - }; - debug!("Observed Deepbook Rebate Event {:?}", data); - result.push(data); - Ok(result) - }); - }) - } -} - -#[async_trait] -impl Handler for RebatesHandler { - async fn commit(values: &[Self::Value], conn: &mut Connection<'_>) -> anyhow::Result { - Ok(diesel::insert_into(rebates::table) - .values(values) - .on_conflict_do_nothing() - .execute(conn) - .await?) +define_handler! { + name: RebatesHandler, + processor_name: "rebates", + event_type: RebateEvent, + db_model: Rebates, + table: rebates, + map_event: |event, meta| Rebates { + event_digest: meta.event_digest(), + digest: meta.digest(), + sender: meta.sender(), + checkpoint: meta.checkpoint(), + checkpoint_timestamp_ms: meta.checkpoint_timestamp_ms(), + package: meta.package(), + pool_id: event.pool_id.to_string(), + balance_manager_id: event.balance_manager_id.to_string(), + epoch: event.epoch as i64, + claim_amount: event.claim_amount as i64, } } diff --git a/crates/indexer/src/handlers/referral_fee_event_handler.rs b/crates/indexer/src/handlers/referral_fee_event_handler.rs new file mode 100644 index 000000000..8ed02c6b7 --- /dev/null +++ b/crates/indexer/src/handlers/referral_fee_event_handler.rs @@ -0,0 +1,23 @@ +use crate::models::deepbook::pool::ReferralFeeEvent; +use deepbook_schema::models::ReferralFeeEvent as ReferralFeeEventModel; + +define_handler! { + name: ReferralFeeEventHandler, + processor_name: "referral_fee_events", + event_type: ReferralFeeEvent, + db_model: ReferralFeeEventModel, + table: referral_fee_events, + map_event: |event, meta| ReferralFeeEventModel { + event_digest: meta.event_digest(), + digest: meta.digest(), + sender: meta.sender(), + checkpoint: meta.checkpoint(), + checkpoint_timestamp_ms: meta.checkpoint_timestamp_ms(), + package: meta.package(), + pool_id: event.pool_id.to_string(), + referral_id: event.referral_id.to_string(), + base_fee: event.base_fee as i64, + quote_fee: event.quote_fee as i64, + deep_fee: event.deep_fee as i64, + } +} diff --git a/crates/indexer/src/handlers/referral_fees_claimed_handler.rs b/crates/indexer/src/handlers/referral_fees_claimed_handler.rs new file mode 100644 index 000000000..6f4872ad2 --- /dev/null +++ b/crates/indexer/src/handlers/referral_fees_claimed_handler.rs @@ -0,0 +1,22 @@ +use crate::models::deepbook_margin::protocol_fees::ReferralFeesClaimedEvent; +use deepbook_schema::models::ReferralFeesClaimedEvent as ReferralFeesClaimedEventModel; + +define_handler! { + name: ReferralFeesClaimedHandler, + processor_name: "referral_fees_claimed", + event_type: ReferralFeesClaimedEvent, + db_model: ReferralFeesClaimedEventModel, + table: referral_fees_claimed, + map_event: |event, meta| ReferralFeesClaimedEventModel { + event_digest: meta.event_digest(), + digest: meta.digest(), + sender: meta.sender(), + checkpoint: meta.checkpoint(), + checkpoint_timestamp_ms: meta.checkpoint_timestamp_ms(), + package: meta.package(), + referral_id: event.referral_id.to_string(), + owner: event.owner.to_string(), + fees: event.fees as i64, + onchain_timestamp: meta.checkpoint_timestamp_ms(), // No timestamp in event + } +} diff --git a/crates/indexer/src/handlers/stakes_handler.rs b/crates/indexer/src/handlers/stakes_handler.rs index cf043ea66..0f2073143 100644 --- a/crates/indexer/src/handlers/stakes_handler.rs +++ b/crates/indexer/src/handlers/stakes_handler.rs @@ -1,86 +1,23 @@ -use crate::handlers::{is_deepbook_tx, struct_tag, try_extract_move_call_package}; use crate::models::deepbook::state::StakeEvent; -use async_trait::async_trait; use deepbook_schema::models::Stakes; -use deepbook_schema::schema::stakes; -use diesel_async::RunQueryDsl; -use move_core_types::account_address::AccountAddress; -use move_core_types::language_storage::StructTag; -use std::sync::Arc; -use sui_indexer_alt_framework::pipeline::concurrent::Handler; -use sui_indexer_alt_framework::pipeline::Processor; -use sui_pg_db::Connection; -use sui_types::full_checkpoint_content::CheckpointData; -use tracing::debug; -pub struct StakesHandler { - event_type: StructTag, -} - -impl StakesHandler { - pub fn new(package_id_override: Option) -> Self { - Self { - event_type: struct_tag::(package_id_override), - } - } -} - -impl Processor for StakesHandler { - const NAME: &'static str = "Stakes"; - type Value = Stakes; - - fn process(&self, checkpoint: &Arc) -> anyhow::Result> { - checkpoint - .transactions - .iter() - .try_fold(vec![], |result, tx| { - if !is_deepbook_tx(tx) { - return Ok(result); - } - let Some(events) = &tx.events else { - return Ok(result); - }; - - let package = try_extract_move_call_package(tx).unwrap_or_default(); - let checkpoint_timestamp_ms = checkpoint.checkpoint_summary.timestamp_ms as i64; - let checkpoint = checkpoint.checkpoint_summary.sequence_number as i64; - let digest = tx.transaction.digest(); - - return events - .data - .iter() - .filter(|ev| ev.type_ == self.event_type) - .enumerate() - .try_fold(result, |mut result, (index, ev)| { - let event: StakeEvent = bcs::from_bytes(&ev.contents)?; - let data = Stakes { - digest: digest.to_string(), - event_digest: format!("{digest}{index}"), - sender: tx.transaction.sender_address().to_string(), - checkpoint, - checkpoint_timestamp_ms, - package: package.clone(), - pool_id: event.pool_id.to_string(), - balance_manager_id: event.balance_manager_id.to_string(), - epoch: event.epoch as i64, - amount: event.amount as i64, - stake: event.stake, - }; - debug!("Observed Deepbook Stake Event {:?}", data); - result.push(data); - Ok(result) - }); - }) - } -} - -#[async_trait] -impl Handler for StakesHandler { - async fn commit(values: &[Self::Value], conn: &mut Connection<'_>) -> anyhow::Result { - Ok(diesel::insert_into(stakes::table) - .values(values) - .on_conflict_do_nothing() - .execute(conn) - .await?) +define_handler! { + name: StakesHandler, + processor_name: "stakes", + event_type: StakeEvent, + db_model: Stakes, + table: stakes, + map_event: |event, meta| Stakes { + event_digest: meta.event_digest(), + digest: meta.digest(), + sender: meta.sender(), + checkpoint: meta.checkpoint(), + checkpoint_timestamp_ms: meta.checkpoint_timestamp_ms(), + package: meta.package(), + pool_id: event.pool_id.to_string(), + balance_manager_id: event.balance_manager_id.to_string(), + epoch: event.epoch as i64, + amount: event.amount as i64, + stake: event.stake, } } diff --git a/crates/indexer/src/handlers/supplier_cap_minted_handler.rs b/crates/indexer/src/handlers/supplier_cap_minted_handler.rs new file mode 100644 index 000000000..d53354aca --- /dev/null +++ b/crates/indexer/src/handlers/supplier_cap_minted_handler.rs @@ -0,0 +1,20 @@ +use crate::models::deepbook_margin::margin_pool::SupplierCapMinted; +use deepbook_schema::models::SupplierCapMinted as SupplierCapMintedModel; + +define_handler! { + name: SupplierCapMintedHandler, + processor_name: "supplier_cap_minted", + event_type: SupplierCapMinted, + db_model: SupplierCapMintedModel, + table: supplier_cap_minted, + map_event: |event, meta| SupplierCapMintedModel { + event_digest: meta.event_digest(), + digest: meta.digest(), + sender: meta.sender(), + checkpoint: meta.checkpoint(), + checkpoint_timestamp_ms: meta.checkpoint_timestamp_ms(), + package: meta.package(), + supplier_cap_id: event.supplier_cap_id.to_string(), + onchain_timestamp: event.timestamp as i64, + } +} diff --git a/crates/indexer/src/handlers/supply_referral_minted_handler.rs b/crates/indexer/src/handlers/supply_referral_minted_handler.rs new file mode 100644 index 000000000..b867c36c8 --- /dev/null +++ b/crates/indexer/src/handlers/supply_referral_minted_handler.rs @@ -0,0 +1,22 @@ +use crate::models::deepbook_margin::margin_pool::SupplyReferralMinted; +use deepbook_schema::models::SupplyReferralMinted as SupplyReferralMintedModel; + +define_handler! { + name: SupplyReferralMintedHandler, + processor_name: "supply_referral_minted", + event_type: SupplyReferralMinted, + db_model: SupplyReferralMintedModel, + table: supply_referral_minted, + map_event: |event, meta| SupplyReferralMintedModel { + event_digest: meta.event_digest(), + digest: meta.digest(), + sender: meta.sender(), + checkpoint: meta.checkpoint(), + checkpoint_timestamp_ms: meta.checkpoint_timestamp_ms(), + package: meta.package(), + margin_pool_id: event.margin_pool_id.to_string(), + supply_referral_id: event.supply_referral_id.to_string(), + owner: event.owner.to_string(), + onchain_timestamp: event.timestamp as i64, + } +} diff --git a/crates/indexer/src/handlers/trade_params_update_handler.rs b/crates/indexer/src/handlers/trade_params_update_handler.rs index 1bb4757ec..94600cfd9 100644 --- a/crates/indexer/src/handlers/trade_params_update_handler.rs +++ b/crates/indexer/src/handlers/trade_params_update_handler.rs @@ -1,91 +1,91 @@ -use crate::handlers::{is_deepbook_tx, struct_tag, try_extract_move_call_package}; +use crate::handlers::{is_deepbook_tx, try_extract_move_call_package}; use crate::models::deepbook::governance::TradeParamsUpdateEvent; -use crate::models::deepbook::pool; +use crate::traits::MoveStruct; +use crate::DeepbookEnv; use async_trait::async_trait; use deepbook_schema::models::TradeParamsUpdate; use deepbook_schema::schema::trade_params_update; use diesel_async::RunQueryDsl; -use move_core_types::account_address::AccountAddress; -use move_core_types::language_storage::StructTag; use std::sync::Arc; -use sui_indexer_alt_framework::pipeline::concurrent::Handler; use sui_indexer_alt_framework::pipeline::Processor; -use sui_pg_db::Connection; -use sui_types::full_checkpoint_content::CheckpointData; +use sui_indexer_alt_framework::postgres::handler::Handler; +use sui_indexer_alt_framework::postgres::Connection; +use sui_indexer_alt_framework::types::full_checkpoint_content::Checkpoint; +use sui_types::transaction::TransactionDataAPI; use tracing::debug; pub struct TradeParamsUpdateHandler { - event_type: StructTag, + env: DeepbookEnv, } impl TradeParamsUpdateHandler { - pub fn new(package_id_override: Option) -> Self { - Self { - event_type: struct_tag::(package_id_override), - } + pub fn new(env: DeepbookEnv) -> Self { + Self { env } } } +#[async_trait] impl Processor for TradeParamsUpdateHandler { - const NAME: &'static str = "TradeParamsUpdate"; + const NAME: &'static str = "trade_params_update"; type Value = TradeParamsUpdate; - fn process(&self, checkpoint: &Arc) -> anyhow::Result> { - checkpoint - .transactions - .iter() - .try_fold(vec![], |result, tx| { - if !is_deepbook_tx(tx) { - return Ok(result); - } - let Some(events) = &tx.events else { - return Ok(result); - }; + async fn process(&self, checkpoint: &Arc) -> anyhow::Result> { + let mut results = vec![]; + for tx in &checkpoint.transactions { + if !is_deepbook_tx(tx, &checkpoint.object_set, self.env) { + continue; + } + let Some(events) = &tx.events else { + continue; + }; - let package = try_extract_move_call_package(tx).unwrap_or_default(); - let checkpoint_timestamp_ms = checkpoint.checkpoint_summary.timestamp_ms as i64; - let checkpoint = checkpoint.checkpoint_summary.sequence_number as i64; - let digest = tx.transaction.digest(); + let package = try_extract_move_call_package(tx).unwrap_or_default(); + let checkpoint_timestamp_ms = checkpoint.summary.timestamp_ms as i64; + let checkpoint_seq = checkpoint.summary.sequence_number as i64; + let digest = tx.transaction.digest(); - let pool = tx - .input_objects - .iter() - .find(|o| matches!(o.data.struct_tag(), Some(struct_tag) - if struct_tag.address == AccountAddress::new(*pool::PACKAGE_ID.inner()) && struct_tag.name.as_str() == "Pool")); - let pool_id = pool - .map(|o| o.id().to_hex_uncompressed()) - .unwrap_or("0x0".to_string()); + // Get package addresses for deepbook + let deepbook_addresses = self.env.package_addresses(); - return events - .data - .iter() - .filter(|ev| ev.type_ == self.event_type) - .enumerate() - .try_fold(result, |mut result, (index, ev)| { - let event: TradeParamsUpdateEvent = bcs::from_bytes(&ev.contents)?; - let data = TradeParamsUpdate { - digest: digest.to_string(), - event_digest: format!("{digest}{index}"), - sender: tx.transaction.sender_address().to_string(), - checkpoint, - checkpoint_timestamp_ms, - package: package.clone(), - pool_id: pool_id.clone(), - taker_fee: event.taker_fee as i64, - maker_fee: event.maker_fee as i64, - stake_required: event.stake_required as i64, - }; - debug!("Observed Deepbook Trade Params Update Event {:?}", data); - result.push(data); - Ok(result) - }); - }) + let pool = tx + .input_objects(&checkpoint.object_set) + .find(|o| matches!(o.data.struct_tag(), Some(struct_tag) + if deepbook_addresses.iter().any(|addr| struct_tag.address == *addr) && struct_tag.name.as_str() == "Pool")); + let pool_id = pool + .map(|o| o.id().to_hex_uncompressed()) + .unwrap_or("0x0".to_string()); + + for (index, ev) in events.data.iter().enumerate() { + if !TradeParamsUpdateEvent::matches_event_type(&ev.type_, self.env) { + continue; + } + let event: TradeParamsUpdateEvent = bcs::from_bytes(&ev.contents)?; + let data = TradeParamsUpdate { + digest: digest.to_string(), + event_digest: format!("{digest}{index}"), + sender: tx.transaction.sender().to_string(), + checkpoint: checkpoint_seq, + checkpoint_timestamp_ms, + package: package.clone(), + pool_id: pool_id.clone(), + taker_fee: event.taker_fee as i64, + maker_fee: event.maker_fee as i64, + stake_required: event.stake_required as i64, + }; + debug!("Observed Deepbook Trade Params Update Event {:?}", data); + results.push(data); + } + } + Ok(results) } } #[async_trait] impl Handler for TradeParamsUpdateHandler { - async fn commit(values: &[Self::Value], conn: &mut Connection<'_>) -> anyhow::Result { + async fn commit<'a>( + values: &[Self::Value], + conn: &mut Connection<'a>, + ) -> anyhow::Result { Ok(diesel::insert_into(trade_params_update::table) .values(values) .on_conflict_do_nothing() diff --git a/crates/indexer/src/handlers/vote_handler.rs b/crates/indexer/src/handlers/vote_handler.rs index 56216d5fa..aa664d6d4 100644 --- a/crates/indexer/src/handlers/vote_handler.rs +++ b/crates/indexer/src/handlers/vote_handler.rs @@ -1,87 +1,24 @@ -use crate::handlers::{is_deepbook_tx, struct_tag, try_extract_move_call_package}; use crate::models::deepbook::state::VoteEvent; -use async_trait::async_trait; use deepbook_schema::models::Votes; -use deepbook_schema::schema::votes; -use diesel_async::RunQueryDsl; -use move_core_types::account_address::AccountAddress; -use move_core_types::language_storage::StructTag; -use std::sync::Arc; -use sui_indexer_alt_framework::pipeline::concurrent::Handler; -use sui_indexer_alt_framework::pipeline::Processor; -use sui_pg_db::Connection; -use sui_types::full_checkpoint_content::CheckpointData; -use tracing::debug; -pub struct VotesHandler { - event_type: StructTag, -} - -impl VotesHandler { - pub fn new(package_id_override: Option) -> Self { - Self { - event_type: struct_tag::(package_id_override), - } - } -} - -impl Processor for VotesHandler { - const NAME: &'static str = "Votes"; - type Value = Votes; - - fn process(&self, checkpoint: &Arc) -> anyhow::Result> { - checkpoint - .transactions - .iter() - .try_fold(vec![], |result, tx| { - if !is_deepbook_tx(tx) { - return Ok(result); - } - let Some(events) = &tx.events else { - return Ok(result); - }; - - let package = try_extract_move_call_package(tx).unwrap_or_default(); - let checkpoint_timestamp_ms = checkpoint.checkpoint_summary.timestamp_ms as i64; - let checkpoint = checkpoint.checkpoint_summary.sequence_number as i64; - let digest = tx.transaction.digest(); - - return events - .data - .iter() - .filter(|ev| ev.type_ == self.event_type) - .enumerate() - .try_fold(result, |mut result, (index, ev)| { - let event: VoteEvent = bcs::from_bytes(&ev.contents)?; - let data = Votes { - digest: digest.to_string(), - event_digest: format!("{digest}{index}"), - sender: tx.transaction.sender_address().to_string(), - checkpoint, - checkpoint_timestamp_ms, - package: package.clone(), - pool_id: event.pool_id.to_string(), - balance_manager_id: event.balance_manager_id.to_string(), - epoch: event.epoch as i64, - from_proposal_id: event.from_proposal_id.map(|id| id.to_string()), - to_proposal_id: event.to_proposal_id.to_string(), - stake: event.stake as i64, - }; - debug!("Observed Deepbook Vote Event {:?}", data); - result.push(data); - Ok(result) - }); - }) - } -} - -#[async_trait] -impl Handler for VotesHandler { - async fn commit(values: &[Self::Value], conn: &mut Connection<'_>) -> anyhow::Result { - Ok(diesel::insert_into(votes::table) - .values(values) - .on_conflict_do_nothing() - .execute(conn) - .await?) +define_handler! { + name: VotesHandler, + processor_name: "votes", + event_type: VoteEvent, + db_model: Votes, + table: votes, + map_event: |event, meta| Votes { + event_digest: meta.event_digest(), + digest: meta.digest(), + sender: meta.sender(), + checkpoint: meta.checkpoint(), + checkpoint_timestamp_ms: meta.checkpoint_timestamp_ms(), + package: meta.package(), + pool_id: event.pool_id.to_string(), + balance_manager_id: event.balance_manager_id.to_string(), + epoch: event.epoch as i64, + from_proposal_id: event.from_proposal_id.map(|id| id.to_string()), + to_proposal_id: event.to_proposal_id.to_string(), + stake: event.stake as i64, } } diff --git a/crates/indexer/src/handlers/withdraw_collateral_handler.rs b/crates/indexer/src/handlers/withdraw_collateral_handler.rs new file mode 100644 index 000000000..e7bc38112 --- /dev/null +++ b/crates/indexer/src/handlers/withdraw_collateral_handler.rs @@ -0,0 +1,36 @@ +use bigdecimal::BigDecimal; + +use crate::models::deepbook_margin::margin_manager::WithdrawCollateralEvent; +use deepbook_schema::models::CollateralEvent; + +define_handler! { + name: WithdrawCollateralHandler, + processor_name: "withdraw_collateral", + event_type: WithdrawCollateralEvent, + db_model: CollateralEvent, + table: collateral_events, + map_event: |event, meta| CollateralEvent { + event_digest: meta.event_digest(), + digest: meta.digest(), + sender: meta.sender(), + checkpoint: meta.checkpoint(), + checkpoint_timestamp_ms: meta.checkpoint_timestamp_ms(), + package: meta.package(), + event_type: "withdraw".to_string(), + margin_manager_id: event.margin_manager_id.to_string(), + amount: BigDecimal::from(event.amount), + asset_type: event.asset.name.clone(), + pyth_decimals: event.base_pyth_decimals as i16, + pyth_price: BigDecimal::from(event.base_pyth_price), + withdraw_base_asset: Some(event.withdraw_base_asset), + base_pyth_decimals: Some(event.base_pyth_decimals as i16), + base_pyth_price: Some(BigDecimal::from(event.base_pyth_price)), + quote_pyth_decimals: Some(event.quote_pyth_decimals as i16), + quote_pyth_price: Some(BigDecimal::from(event.quote_pyth_price)), + remaining_base_asset: Some(BigDecimal::from(event.remaining_base_asset)), + remaining_quote_asset: Some(BigDecimal::from(event.remaining_quote_asset)), + remaining_base_debt: Some(BigDecimal::from(event.remaining_base_debt)), + remaining_quote_debt: Some(BigDecimal::from(event.remaining_quote_debt)), + onchain_timestamp: event.timestamp as i64, + } +} diff --git a/crates/indexer/src/lib.rs b/crates/indexer/src/lib.rs index a7f1bca92..853846280 100644 --- a/crates/indexer/src/lib.rs +++ b/crates/indexer/src/lib.rs @@ -1,5 +1,253 @@ +use url::Url; + pub mod handlers; pub(crate) mod models; +pub mod traits; + +pub const NOT_MAINNET_PACKAGE: &str = ""; pub const MAINNET_REMOTE_STORE_URL: &str = "https://checkpoints.mainnet.sui.io"; pub const TESTNET_REMOTE_STORE_URL: &str = "https://checkpoints.testnet.sui.io"; + +// Package addresses for different environments +const MAINNET_PACKAGES: &[&str] = &[ + "0xb29d83c26cdd2a64959263abbcfc4a6937f0c9fccaf98580ca56faded65be244", + "0x2c8d603bc51326b8c13cef9dd07031a408a48dddb541963357661df5d3204809", + "0xcaf6ba059d539a97646d47f0b9ddf843e138d215e2a12ca1f4585d386f7aec3a", + "0x00c1a56ec8c4c623a848b2ed2f03d23a25d17570b670c22106f336eb933785cc", + "0x2d93777cc8b67c064b495e8606f2f8f5fd578450347bbe7b36e0bc03963c1c40", // Latest +]; + +const TESTNET_PACKAGES: &[&str] = &[ + "0x467e34e75debeea8b89d03aea15755373afc39a7c96c9959549c7f5f689843cf", + "0x5d520a3e3059b68530b2ef4080126dbb5d234e0afd66561d0d9bd48127a06044", + "0xcd40faffa91c00ce019bfe4a4b46f8d623e20bf331eb28990ee0305e9b9f3e3c", + "0x16c4e050b9b19b25ce1365b96861bc50eb7e58383348a39ea8a8e1d063cfef73", + "0xc483dba510597205749f2e8410c23f19be31a710aef251f353bc1b97755efd4d", + "0x5da5bbf6fb097d108eaf2c2306f88beae4014c90a44b95c7e76a6bfccec5f5ee", + "0xa3886aaa8aa831572dd39549242ca004a438c3a55967af9f0387ad2b01595068", + "0x9592ac923593f37f4fed15ee15f760ebd4c39729f53ee3e8c214de7a17157769", + "0x984757fc7c0e6dd5f15c2c66e881dd6e5aca98b725f3dbd83c445e057ebb790a", + "0xfb28c4cbc6865bd1c897d26aecbe1f8792d1509a20ffec692c800660cbec6982", + "0x926c446869fa175ec3b0dbf6c4f14604d86a415c1fccd8c8f823cfc46a29baed", + "0xa0936c6ea82fbfc0356eedc2e740e260dedaaa9f909a0715b1cc31e9a8283719", + "0x9ae1cbfb7475f6a4c2d4d3273335459f8f9d265874c4d161c1966cdcbd4e9ebc", + "0xb48d47cb5f56d0f489f48f186d06672df59d64bd2f514b2f0ba40cbb8c8fd487", + "0xbc331f09e5c737d45f074ad2d17c3038421b3b9018699e370d88d94938c53d28", + "0x23018638bb4f11ef9ffb0de922519bea52f960e7a5891025ca9aaeeaff7d5034", + "0x22be4cade64bf2d02412c7e8d0e8beea2f78828b948118d46735315409371a3c", // Latest +]; + +// Mainnet margin package is not yet deployed - using placeholder +// This will cause the indexer to fail fast if margin modules are requested on mainnet +// When the margin package is deployed on mainnet, replace this with the actual address +const MAINNET_MARGIN_PACKAGES: &[&str] = + &["0x97d9473771b01f77b0940c589484184b49f6444627ec121314fae6a6d36fb86b"]; +const TESTNET_MARGIN_PACKAGES: &[&str] = &[ + "0xb8620c24c9ea1a4a41e79613d2b3d1d93648d1bb6f6b789a7c8f261c94110e4b", + "0xf978cf2b601c24e40ef82b6e51512b448696b44cb014c0a1162422aa8b9cb811", + "0x16d781c327a919dc55390f5cc60d58c7ec4535bb317e88850961222bbd5d4d9e", + "0xbf9e1b079fa68ffc54a84533b1c3d357019178b19e9901f262fb925454425177", + "0xe673d499eb03f1c31e8079dc73a700f2f085ff7b69c4aff396fad52d07ae6338", + "0x229d3cdbb327082a5c6773e8344b16c4040b360235e3cda75e1f232d4e9184cb", + "0x3d02a90ae1d2eff63ca8ae9bfd89ffa0f7e12d780563259c8271833c270ae842", + "0x3ca7f6ee86b42ebe05ab8de70fbc96832e65615f64f10dbdc1820fa599904c7b", + "0xb284008ea0a6ac0a68c41f50a631207cd8d9c197ba0884e0df29ea204256777e", + "0xc21637e41d3db1c7ca6258fb4de567ba09d4e41610da44a148b26e99b68e11b5", + "0xf0a090340d74ea598d59868378f27d2cc5e46a562ec3a5b26b5117572905d9f3", + "0x32e32dd608c4d83f82c64331a547bcb4bbfb819d4591197f2fe442b1661873d8", + "0xd6a42f4df4db73d68cbeb52be66698d2fe6a9464f45ad113ca52b0c6ebd918b6", +]; + +// Module definitions +/// Core DeepBook modules that handle trading, orders, and pool management +pub const CORE_MODULES: &[&str] = &[ + "balance_manager", + "order", + "order_info", + "vault", + "deep_price", + "state", + "governance", + "pool", +]; + +/// Margin trading modules that handle lending and borrowing +pub const MARGIN_MODULES: &[&str] = &[ + "margin_manager", + "margin_pool", + "margin_registry", + "protocol_fees", + "tpsl", +]; + +/// SUI system modules +pub const SUI_MODULES: &[&str] = &["sui"]; + +/// Enum representing different module types +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ModuleType { + Core, + Margin, + Sui, + Unknown, +} + +/// Check if a module is a core DeepBook module +pub fn is_core_module(module: &str) -> bool { + CORE_MODULES.contains(&module) +} + +/// Check if a module is a margin trading module +pub fn is_margin_module(module: &str) -> bool { + MARGIN_MODULES.contains(&module) +} + +/// Check if a module is a SUI system module +pub fn is_sui_module(module: &str) -> bool { + SUI_MODULES.contains(&module) +} + +/// Get the module type (core, margin, sui, or unknown) +pub fn get_module_type(module: &str) -> ModuleType { + if is_core_module(module) { + ModuleType::Core + } else if is_margin_module(module) { + ModuleType::Margin + } else if is_sui_module(module) { + ModuleType::Sui + } else { + ModuleType::Unknown + } +} + +/// Get all known module names +pub fn get_all_known_modules() -> Vec<&'static str> { + let mut modules = Vec::new(); + modules.extend_from_slice(CORE_MODULES); + modules.extend_from_slice(MARGIN_MODULES); + modules.extend_from_slice(SUI_MODULES); + modules +} + +/// Get all core module names +pub fn get_core_modules() -> &'static [&'static str] { + CORE_MODULES +} + +/// Get all margin module names +pub fn get_margin_modules() -> &'static [&'static str] { + MARGIN_MODULES +} + +/// Get all SUI module names +pub fn get_sui_modules() -> &'static [&'static str] { + SUI_MODULES +} + +/// Check if a margin package address is valid +pub fn is_valid_margin_package(package: &str) -> bool { + package != NOT_MAINNET_PACKAGE +} + +/// Check if any margin package addresses are valid for the given environment +pub fn is_valid_margin_packages(packages: &[&str]) -> bool { + packages.iter().any(|&pkg| is_valid_margin_package(pkg)) +} + +/// Check if margin trading is supported in the given environment +pub fn is_margin_supported(env: DeepbookEnv) -> bool { + match env { + DeepbookEnv::Mainnet => is_valid_margin_packages(MAINNET_MARGIN_PACKAGES), + DeepbookEnv::Testnet => is_valid_margin_packages(TESTNET_MARGIN_PACKAGES), + } +} + +/// Get the margin package addresses for the given environment +pub fn get_margin_package_addresses(env: DeepbookEnv) -> &'static [&'static str] { + match env { + DeepbookEnv::Mainnet => MAINNET_MARGIN_PACKAGES, + DeepbookEnv::Testnet => TESTNET_MARGIN_PACKAGES, + } +} + +/// Get the first valid margin package address for the given environment with validation +pub fn get_margin_package_address(env: DeepbookEnv) -> Result<&'static str, String> { + let packages = get_margin_package_addresses(env); + + // Find the first valid package + for &package in packages { + if is_valid_margin_package(package) { + return Ok(package); + } + } + + Err(format!( + "Margin trading is not supported on {:?}. \ + The margin package has not been deployed on this network.", + env + )) +} + +/// Get all core package addresses for the given environment +pub fn get_core_package_addresses(env: DeepbookEnv) -> &'static [&'static str] { + match env { + DeepbookEnv::Mainnet => MAINNET_PACKAGES, + DeepbookEnv::Testnet => TESTNET_PACKAGES, + } +} + +#[derive(Debug, Clone, Copy, clap::ValueEnum)] +pub enum DeepbookEnv { + Mainnet, + Testnet, +} + +impl DeepbookEnv { + pub fn remote_store_url(&self) -> Url { + let url = match self { + DeepbookEnv::Mainnet => MAINNET_REMOTE_STORE_URL, + DeepbookEnv::Testnet => TESTNET_REMOTE_STORE_URL, + }; + Url::parse(url).unwrap() + } + + /// Get all package addresses (DeepBook + Margin) for this environment + fn get_all_package_strings(&self) -> Vec<&str> { + let (packages, margin_packages) = match self { + DeepbookEnv::Mainnet => (MAINNET_PACKAGES, MAINNET_MARGIN_PACKAGES), + DeepbookEnv::Testnet => (TESTNET_PACKAGES, TESTNET_MARGIN_PACKAGES), + }; + + let mut all_packages = packages.to_vec(); + + // Add margin packages if they're not invalid + for &margin_package in margin_packages { + if margin_package != NOT_MAINNET_PACKAGE { + all_packages.push(margin_package); + } + } + + all_packages + } + + pub fn package_ids(&self) -> Vec { + use std::str::FromStr; + use sui_types::base_types::ObjectID; + + self.get_all_package_strings() + .iter() + .map(|pkg| ObjectID::from_str(pkg).unwrap()) + .collect() + } + + pub fn package_addresses(&self) -> Vec { + use move_core_types::account_address::AccountAddress; + use std::str::FromStr; + + self.get_all_package_strings() + .iter() + .map(|pkg| AccountAddress::from_str(pkg).unwrap()) + .collect() + } +} diff --git a/crates/indexer/src/main.rs b/crates/indexer/src/main.rs index b3880a00a..c274091bb 100644 --- a/crates/indexer/src/main.rs +++ b/crates/indexer/src/main.rs @@ -1,27 +1,80 @@ use anyhow::Context; use clap::Parser; use deepbook_indexer::handlers::balances_handler::BalancesHandler; +use deepbook_indexer::handlers::deep_burned_handler::DeepBurnedHandler; use deepbook_indexer::handlers::flash_loan_handler::FlashLoanHandler; use deepbook_indexer::handlers::order_fill_handler::OrderFillHandler; use deepbook_indexer::handlers::order_update_handler::OrderUpdateHandler; use deepbook_indexer::handlers::pool_price_handler::PoolPriceHandler; use deepbook_indexer::handlers::proposals_handler::ProposalsHandler; use deepbook_indexer::handlers::rebates_handler::RebatesHandler; +use deepbook_indexer::handlers::referral_fee_event_handler::ReferralFeeEventHandler; use deepbook_indexer::handlers::stakes_handler::StakesHandler; use deepbook_indexer::handlers::trade_params_update_handler::TradeParamsUpdateHandler; use deepbook_indexer::handlers::vote_handler::VotesHandler; -use deepbook_indexer::MAINNET_REMOTE_STORE_URL; + +// Margin Manager Events +use deepbook_indexer::handlers::liquidation_handler::LiquidationHandler; +use deepbook_indexer::handlers::loan_borrowed_handler::LoanBorrowedHandler; +use deepbook_indexer::handlers::loan_repaid_handler::LoanRepaidHandler; +use deepbook_indexer::handlers::margin_manager_created_handler::MarginManagerCreatedHandler; + +// Margin Pool Operations Events +use deepbook_indexer::handlers::asset_supplied_handler::AssetSuppliedHandler; +use deepbook_indexer::handlers::asset_withdrawn_handler::AssetWithdrawnHandler; +use deepbook_indexer::handlers::maintainer_fees_withdrawn_handler::MaintainerFeesWithdrawnHandler; +use deepbook_indexer::handlers::protocol_fees_withdrawn_handler::ProtocolFeesWithdrawnHandler; +use deepbook_indexer::handlers::supplier_cap_minted_handler::SupplierCapMintedHandler; +use deepbook_indexer::handlers::supply_referral_minted_handler::SupplyReferralMintedHandler; + +// Margin Pool Admin Events +use deepbook_indexer::handlers::deepbook_pool_updated_handler::DeepbookPoolUpdatedHandler; +use deepbook_indexer::handlers::interest_params_updated_handler::InterestParamsUpdatedHandler; +use deepbook_indexer::handlers::margin_pool_config_updated_handler::MarginPoolConfigUpdatedHandler; +use deepbook_indexer::handlers::margin_pool_created_handler::MarginPoolCreatedHandler; + +// Margin Registry Events +use deepbook_indexer::handlers::deepbook_pool_config_updated_handler::DeepbookPoolConfigUpdatedHandler; +use deepbook_indexer::handlers::deepbook_pool_registered_handler::DeepbookPoolRegisteredHandler; +use deepbook_indexer::handlers::deepbook_pool_updated_registry_handler::DeepbookPoolUpdatedRegistryHandler; +use deepbook_indexer::handlers::maintainer_cap_updated_handler::MaintainerCapUpdatedHandler; +use deepbook_indexer::handlers::pause_cap_updated_handler::PauseCapUpdatedHandler; + +// Protocol Fees Events +use deepbook_indexer::handlers::protocol_fees_increased_handler::ProtocolFeesIncreasedHandler; +use deepbook_indexer::handlers::referral_fees_claimed_handler::ReferralFeesClaimedHandler; + +// Collateral Events +use deepbook_indexer::handlers::deposit_collateral_handler::DepositCollateralHandler; +use deepbook_indexer::handlers::withdraw_collateral_handler::WithdrawCollateralHandler; + +// TPSL (Take Profit / Stop Loss) Events +use deepbook_indexer::handlers::conditional_order_added_handler::ConditionalOrderAddedHandler; +use deepbook_indexer::handlers::conditional_order_cancelled_handler::ConditionalOrderCancelledHandler; +use deepbook_indexer::handlers::conditional_order_executed_handler::ConditionalOrderExecutedHandler; +use deepbook_indexer::handlers::conditional_order_insufficient_funds_handler::ConditionalOrderInsufficientFundsHandler; + +use deepbook_indexer::DeepbookEnv; use deepbook_schema::MIGRATIONS; -use move_core_types::account_address::AccountAddress; use prometheus::Registry; use std::net::SocketAddr; -use sui_indexer_alt_framework::ingestion::ClientArgs; +use sui_indexer_alt_framework::ingestion::ingestion_client::IngestionClientArgs; +use sui_indexer_alt_framework::ingestion::{ClientArgs, IngestionConfig}; use sui_indexer_alt_framework::{Indexer, IndexerArgs}; +use sui_indexer_alt_metrics::db::DbConnectionStatsCollector; use sui_indexer_alt_metrics::{MetricsArgs, MetricsService}; -use sui_pg_db::DbArgs; -use tokio_util::sync::CancellationToken; +use sui_pg_db::{Db, DbArgs}; + use url::Url; +#[derive(Debug, Clone, clap::ValueEnum)] +pub enum Package { + /// Index DeepBook core events (order fills, updates, pools, etc.) + Deepbook, + /// Index DeepBook margin events (lending, borrowing, liquidations, etc.) + DeepbookMargin, +} + #[derive(Parser)] #[clap(rename_all = "kebab-case", author, version)] struct Args { @@ -37,12 +90,12 @@ struct Args { default_value = "postgres://postgres:postgrespw@localhost:5432/deepbook" )] database_url: Url, - /// Checkpoint remote store URL, defaulted to Sui mainnet remote store. - #[clap(env, long, default_value = MAINNET_REMOTE_STORE_URL)] - remote_store_url: Url, - /// Deepbook package id override, defaulted to the mainnet deepbook package id. + /// Deepbook environment, defaulted to SUI mainnet. #[clap(env, long)] - package_id_override: Option, + env: DeepbookEnv, + /// Packages to index events for (can specify multiple) + #[clap(long, value_enum, default_values = ["deepbook", "deepbook-margin"])] + packages: Vec, } #[tokio::main] @@ -55,96 +108,208 @@ async fn main() -> Result<(), anyhow::Error> { db_args, indexer_args, metrics_address, - remote_store_url, database_url, - package_id_override, + env, + packages, } = Args::parse(); - let cancel = CancellationToken::new(); let registry = Registry::new_custom(Some("deepbook".into()), None) .context("Failed to create Prometheus registry.")?; - let metrics = MetricsService::new( - MetricsArgs { metrics_address }, - registry, - cancel.child_token(), - ); + let metrics = MetricsService::new(MetricsArgs { metrics_address }, registry.clone()); + + // Prepare the store for the indexer + let store = Db::for_write(database_url, db_args) + .await + .context("Failed to connect to database")?; + + store + .run_migrations(Some(&MIGRATIONS)) + .await + .context("Failed to run pending migrations")?; + + registry.register(Box::new(DbConnectionStatsCollector::new( + Some("deepbook_indexer_db"), + store.clone(), + )))?; let mut indexer = Indexer::new( - database_url, - db_args, + store, indexer_args, ClientArgs { - remote_store_url: Some(remote_store_url), - local_ingestion_path: None, - rpc_api_url: None, - rpc_username: None, - rpc_password: None, + ingestion: IngestionClientArgs { + remote_store_url: Some(env.remote_store_url()), + local_ingestion_path: None, + rpc_api_url: None, + rpc_username: None, + rpc_password: None, + }, + streaming: Default::default(), }, - Default::default(), - Some(&MIGRATIONS), + IngestionConfig::default(), + None, metrics.registry(), - cancel.clone(), ) .await?; - indexer - .concurrent_pipeline( - BalancesHandler::new(package_id_override), - Default::default(), - ) - .await?; - indexer - .concurrent_pipeline( - FlashLoanHandler::new(package_id_override), - Default::default(), - ) - .await?; - indexer - .concurrent_pipeline( - OrderFillHandler::new(package_id_override), - Default::default(), - ) - .await?; - indexer - .concurrent_pipeline( - OrderUpdateHandler::new(package_id_override), - Default::default(), - ) - .await?; - indexer - .concurrent_pipeline( - PoolPriceHandler::new(package_id_override), - Default::default(), - ) - .await?; - indexer - .concurrent_pipeline( - ProposalsHandler::new(package_id_override), - Default::default(), - ) - .await?; - indexer - .concurrent_pipeline(RebatesHandler::new(package_id_override), Default::default()) - .await?; - indexer - .concurrent_pipeline(StakesHandler::new(package_id_override), Default::default()) - .await?; - indexer - .concurrent_pipeline( - TradeParamsUpdateHandler::new(package_id_override), - Default::default(), - ) - .await?; - indexer - .concurrent_pipeline(VotesHandler::new(package_id_override), Default::default()) - .await?; - - let h_indexer = indexer.run().await?; - let h_metrics = metrics.run().await?; - - let _ = h_indexer.await; - cancel.cancel(); - let _ = h_metrics.await; + // Register handlers based on selected packages + for package in &packages { + match package { + Package::Deepbook => { + // DeepBook core event handlers + indexer + .concurrent_pipeline(BalancesHandler::new(env), Default::default()) + .await?; + indexer + .concurrent_pipeline(DeepBurnedHandler::new(env), Default::default()) + .await?; + indexer + .concurrent_pipeline(FlashLoanHandler::new(env), Default::default()) + .await?; + indexer + .concurrent_pipeline(OrderFillHandler::new(env), Default::default()) + .await?; + indexer + .concurrent_pipeline(OrderUpdateHandler::new(env), Default::default()) + .await?; + indexer + .concurrent_pipeline(PoolPriceHandler::new(env), Default::default()) + .await?; + indexer + .concurrent_pipeline(ProposalsHandler::new(env), Default::default()) + .await?; + indexer + .concurrent_pipeline(RebatesHandler::new(env), Default::default()) + .await?; + indexer + .concurrent_pipeline(ReferralFeeEventHandler::new(env), Default::default()) + .await?; + indexer + .concurrent_pipeline(StakesHandler::new(env), Default::default()) + .await?; + indexer + .concurrent_pipeline(TradeParamsUpdateHandler::new(env), Default::default()) + .await?; + indexer + .concurrent_pipeline(VotesHandler::new(env), Default::default()) + .await?; + } + Package::DeepbookMargin => { + indexer + .concurrent_pipeline(MarginManagerCreatedHandler::new(env), Default::default()) + .await?; + indexer + .concurrent_pipeline(LoanBorrowedHandler::new(env), Default::default()) + .await?; + indexer + .concurrent_pipeline(LoanRepaidHandler::new(env), Default::default()) + .await?; + indexer + .concurrent_pipeline(LiquidationHandler::new(env), Default::default()) + .await?; + indexer + .concurrent_pipeline(AssetSuppliedHandler::new(env), Default::default()) + .await?; + indexer + .concurrent_pipeline(AssetWithdrawnHandler::new(env), Default::default()) + .await?; + indexer + .concurrent_pipeline(MarginPoolCreatedHandler::new(env), Default::default()) + .await?; + indexer + .concurrent_pipeline(DeepbookPoolUpdatedHandler::new(env), Default::default()) + .await?; + indexer + .concurrent_pipeline(InterestParamsUpdatedHandler::new(env), Default::default()) + .await?; + indexer + .concurrent_pipeline( + MarginPoolConfigUpdatedHandler::new(env), + Default::default(), + ) + .await?; + indexer + .concurrent_pipeline(MaintainerCapUpdatedHandler::new(env), Default::default()) + .await?; + indexer + .concurrent_pipeline( + DeepbookPoolRegisteredHandler::new(env), + Default::default(), + ) + .await?; + indexer + .concurrent_pipeline( + DeepbookPoolUpdatedRegistryHandler::new(env), + Default::default(), + ) + .await?; + indexer + .concurrent_pipeline( + DeepbookPoolConfigUpdatedHandler::new(env), + Default::default(), + ) + .await?; + indexer + .concurrent_pipeline( + MaintainerFeesWithdrawnHandler::new(env), + Default::default(), + ) + .await?; + indexer + .concurrent_pipeline(ProtocolFeesWithdrawnHandler::new(env), Default::default()) + .await?; + indexer + .concurrent_pipeline(SupplierCapMintedHandler::new(env), Default::default()) + .await?; + indexer + .concurrent_pipeline(SupplyReferralMintedHandler::new(env), Default::default()) + .await?; + indexer + .concurrent_pipeline(PauseCapUpdatedHandler::new(env), Default::default()) + .await?; + indexer + .concurrent_pipeline(ProtocolFeesIncreasedHandler::new(env), Default::default()) + .await?; + indexer + .concurrent_pipeline(ReferralFeesClaimedHandler::new(env), Default::default()) + .await?; + + // Collateral Events + indexer + .concurrent_pipeline(DepositCollateralHandler::new(env), Default::default()) + .await?; + indexer + .concurrent_pipeline(WithdrawCollateralHandler::new(env), Default::default()) + .await?; + + // TPSL (Take Profit / Stop Loss) Events + indexer + .concurrent_pipeline(ConditionalOrderAddedHandler::new(env), Default::default()) + .await?; + indexer + .concurrent_pipeline( + ConditionalOrderCancelledHandler::new(env), + Default::default(), + ) + .await?; + indexer + .concurrent_pipeline( + ConditionalOrderExecutedHandler::new(env), + Default::default(), + ) + .await?; + indexer + .concurrent_pipeline( + ConditionalOrderInsufficientFundsHandler::new(env), + Default::default(), + ) + .await?; + } + } + } + + let s_indexer = indexer.run().await?; + let s_metrics = metrics.run().await?; + s_indexer.attach(s_metrics).main().await?; Ok(()) } diff --git a/crates/indexer/src/models.rs b/crates/indexer/src/models.rs index f2881b741..5029f2a58 100644 --- a/crates/indexer/src/models.rs +++ b/crates/indexer/src/models.rs @@ -1,6 +1,818 @@ -use move_binding_derive::move_contract; +use crate::traits::MoveStruct; +use serde::{Deserialize, Serialize}; +use std::marker::PhantomData; +use sui_sdk_types::Address; +use sui_types::base_types::ObjectID; +use sui_types::collection_types::VecMap; -move_contract! {alias="sui", package="0x2"} -move_contract! {alias="token", package="0xdeeb7a4662eec9f2f3def03fb937a663dddaa2e215b8078a284d026b7946c270", deps = [crate::models::sui]} -move_contract! {alias="deepbook", package="@deepbook/core", deps = [crate::models::sui, crate::models::token]} -move_contract! {alias="deepbook_testnet", package="@deepbook/core", deps = [crate::models::sui, crate::models::token], network = "testnet"} +// DeepBook module +pub mod deepbook { + use super::*; + + pub mod balance_manager { + use super::*; + + #[derive(Debug, Clone, Serialize, Deserialize)] + pub struct BalanceEvent { + pub balance_manager_id: ObjectID, + pub asset: String, + pub amount: u64, + pub deposit: bool, + } + + impl MoveStruct for BalanceEvent { + const MODULE: &'static str = "balance_manager"; + const NAME: &'static str = "BalanceEvent"; + } + } + + pub mod order { + use super::*; + + #[derive(Debug, Clone, Serialize, Deserialize)] + pub struct OrderCanceled { + pub balance_manager_id: ObjectID, + pub pool_id: ObjectID, + pub order_id: u128, + pub client_order_id: u64, + pub trader: Address, + pub price: u64, + pub is_bid: bool, + pub original_quantity: u64, + pub base_asset_quantity_canceled: u64, + pub timestamp: u64, + } + + #[derive(Debug, Clone, Serialize, Deserialize)] + pub struct OrderModified { + pub balance_manager_id: ObjectID, + pub pool_id: ObjectID, + pub order_id: u128, + pub client_order_id: u64, + pub trader: Address, + pub price: u64, + pub is_bid: bool, + pub previous_quantity: u64, + pub filled_quantity: u64, + pub new_quantity: u64, + pub timestamp: u64, + } + + impl MoveStruct for OrderCanceled { + const MODULE: &'static str = "order"; + const NAME: &'static str = "OrderCanceled"; + } + + impl MoveStruct for OrderModified { + const MODULE: &'static str = "order"; + const NAME: &'static str = "OrderModified"; + } + } + + pub mod order_info { + use super::*; + + #[derive(Debug, Clone, Serialize, Deserialize)] + pub struct OrderFilled { + pub pool_id: ObjectID, + pub maker_order_id: u128, + pub taker_order_id: u128, + pub maker_client_order_id: u64, + pub taker_client_order_id: u64, + pub price: u64, + pub taker_is_bid: bool, + pub taker_fee: u64, + pub taker_fee_is_deep: bool, + pub maker_fee: u64, + pub maker_fee_is_deep: bool, + pub base_quantity: u64, + pub quote_quantity: u64, + pub maker_balance_manager_id: ObjectID, + pub taker_balance_manager_id: ObjectID, + pub timestamp: u64, + } + + #[derive(Debug, Clone, Serialize, Deserialize)] + pub struct OrderPlaced { + pub balance_manager_id: ObjectID, + pub pool_id: ObjectID, + pub order_id: u128, + pub client_order_id: u64, + pub trader: Address, + pub price: u64, + pub is_bid: bool, + pub placed_quantity: u64, + pub expire_timestamp: u64, + pub timestamp: u64, + } + + #[derive(Debug, Clone, Serialize, Deserialize)] + pub struct OrderExpired { + pub balance_manager_id: ObjectID, + pub pool_id: ObjectID, + pub order_id: u128, + pub client_order_id: u64, + pub trader: Address, + pub price: u64, + pub is_bid: bool, + pub original_quantity: u64, + pub base_asset_quantity_canceled: u64, + pub timestamp: u64, + } + + impl MoveStruct for OrderFilled { + const MODULE: &'static str = "order_info"; + const NAME: &'static str = "OrderFilled"; + } + + impl MoveStruct for OrderPlaced { + const MODULE: &'static str = "order_info"; + const NAME: &'static str = "OrderPlaced"; + } + + impl MoveStruct for OrderExpired { + const MODULE: &'static str = "order_info"; + const NAME: &'static str = "OrderExpired"; + } + } + + pub mod vault { + use super::*; + + #[derive(Debug, Clone, Serialize, Deserialize)] + pub struct FlashLoanBorrowed { + pub pool_id: ObjectID, + pub borrow_quantity: u64, + pub type_name: String, + } + + impl MoveStruct for FlashLoanBorrowed { + const MODULE: &'static str = "vault"; + const NAME: &'static str = "FlashLoanBorrowed"; + } + } + + pub mod deep_price { + use super::*; + + #[derive(Debug, Clone, Serialize, Deserialize)] + pub struct PriceAdded { + pub conversion_rate: u64, + pub timestamp: u64, + pub is_base_conversion: bool, + pub reference_pool: ObjectID, + pub target_pool: ObjectID, + } + + impl MoveStruct for PriceAdded { + const MODULE: &'static str = "deep_price"; + const NAME: &'static str = "PriceAdded"; + } + } + + pub mod state { + use super::*; + + #[derive(Debug, Clone, Serialize, Deserialize)] + pub struct VoteEvent { + pub pool_id: ObjectID, + pub balance_manager_id: ObjectID, + pub epoch: u64, + pub from_proposal_id: Option, + pub to_proposal_id: ObjectID, + pub stake: u64, + } + + #[derive(Debug, Clone, Serialize, Deserialize)] + pub struct StakeEvent { + pub pool_id: ObjectID, + pub balance_manager_id: ObjectID, + pub epoch: u64, + pub amount: u64, + pub stake: bool, + } + + #[derive(Debug, Clone, Serialize, Deserialize)] + pub struct RebateEvent { + pub pool_id: ObjectID, + pub balance_manager_id: ObjectID, + pub epoch: u64, + pub claim_amount: u64, + } + + #[derive(Debug, Clone, Serialize, Deserialize)] + pub struct ProposalEvent { + pub pool_id: ObjectID, + pub balance_manager_id: ObjectID, + pub epoch: u64, + pub taker_fee: u64, + pub maker_fee: u64, + pub stake_required: u64, + } + + impl MoveStruct for VoteEvent { + const MODULE: &'static str = "state"; + const NAME: &'static str = "VoteEvent"; + } + + impl MoveStruct for StakeEvent { + const MODULE: &'static str = "state"; + const NAME: &'static str = "StakeEvent"; + } + + impl MoveStruct for RebateEvent { + const MODULE: &'static str = "state"; + const NAME: &'static str = "RebateEvent"; + } + + impl MoveStruct for ProposalEvent { + const MODULE: &'static str = "state"; + const NAME: &'static str = "ProposalEvent"; + } + } + + pub mod governance { + use super::*; + + #[derive(Debug, Clone, Serialize, Deserialize)] + pub struct TradeParamsUpdateEvent { + pub taker_fee: u64, + pub maker_fee: u64, + pub stake_required: u64, + } + + impl MoveStruct for TradeParamsUpdateEvent { + const MODULE: &'static str = "governance"; + + const NAME: &'static str = "TradeParamsUpdateEvent"; + } + } + + pub mod pool { + use super::*; + + #[derive(Debug, Clone, Serialize, Deserialize)] + pub struct PoolCreated { + pub pool_id: ObjectID, + pub taker_fee: u64, + pub maker_fee: u64, + pub tick_size: u64, + pub lot_size: u64, + pub min_size: u64, + pub whitelisted_pool: bool, + pub treasury_address: Address, + #[serde(skip)] + pub phantom_base: PhantomData, + #[serde(skip)] + pub phantom_quote: PhantomData, + } + + impl MoveStruct for PoolCreated { + const MODULE: &'static str = "pool"; + const NAME: &'static str = "PoolCreated"; + } + + #[derive(Debug, Clone, Serialize, Deserialize)] + pub struct DeepBurned { + pub pool_id: ObjectID, + pub deep_burned: u64, + #[serde(skip)] + pub phantom_base: PhantomData, + #[serde(skip)] + pub phantom_quote: PhantomData, + } + + impl MoveStruct for DeepBurned { + const MODULE: &'static str = "pool"; + const NAME: &'static str = "DeepBurned"; + } + + #[derive(Debug, Clone, Serialize, Deserialize)] + pub struct ReferralFeeEvent { + pub pool_id: ObjectID, + pub referral_id: ObjectID, + pub base_fee: u64, + pub quote_fee: u64, + pub deep_fee: u64, + } + + impl MoveStruct for ReferralFeeEvent { + const MODULE: &'static str = "pool"; + const NAME: &'static str = "ReferralFeeEvent"; + } + } +} + +/// Represents a Sui TypeName (package::module::Type) +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct TypeName { + pub name: String, +} + +// DeepBook Margin module +pub mod deepbook_margin { + use super::*; + use crate::models::TypeName; + + pub mod margin_manager { + use super::*; + + #[derive(Debug, Clone, Serialize, Deserialize)] + pub struct MarginManagerCreatedEvent { + pub margin_manager_id: ObjectID, + pub balance_manager_id: ObjectID, + pub deepbook_pool_id: ObjectID, + pub owner: Address, + pub timestamp: u64, + } + + #[derive(Debug, Clone, Serialize, Deserialize)] + pub struct LoanBorrowedEvent { + pub margin_manager_id: ObjectID, + pub margin_pool_id: ObjectID, + pub loan_amount: u64, + pub loan_shares: u64, + pub timestamp: u64, + } + + #[derive(Debug, Clone, Serialize, Deserialize)] + pub struct LoanRepaidEvent { + pub margin_manager_id: ObjectID, + pub margin_pool_id: ObjectID, + pub repay_amount: u64, + pub repay_shares: u64, + pub timestamp: u64, + } + + #[derive(Debug, Clone, Serialize, Deserialize)] + pub struct LiquidationEvent { + pub margin_manager_id: ObjectID, + pub margin_pool_id: ObjectID, + pub liquidation_amount: u64, + pub pool_reward: u64, + pub pool_default: u64, + pub risk_ratio: u64, + pub remaining_base_asset: u64, + pub remaining_quote_asset: u64, + pub remaining_base_debt: u64, + pub remaining_quote_debt: u64, + pub base_pyth_price: u64, + pub base_pyth_decimals: u8, + pub quote_pyth_price: u64, + pub quote_pyth_decimals: u8, + pub timestamp: u64, + } + + impl MoveStruct for MarginManagerCreatedEvent { + const MODULE: &'static str = "margin_manager"; + const NAME: &'static str = "MarginManagerCreatedEvent"; + } + + impl MoveStruct for LoanBorrowedEvent { + const MODULE: &'static str = "margin_manager"; + const NAME: &'static str = "LoanBorrowedEvent"; + } + + impl MoveStruct for LoanRepaidEvent { + const MODULE: &'static str = "margin_manager"; + const NAME: &'static str = "LoanRepaidEvent"; + } + + impl MoveStruct for LiquidationEvent { + const MODULE: &'static str = "margin_manager"; + const NAME: &'static str = "LiquidationEvent"; + } + + #[derive(Debug, Clone, Serialize, Deserialize)] + pub struct DepositCollateralEvent { + pub margin_manager_id: ObjectID, + pub amount: u64, + pub asset: TypeName, + pub pyth_price: u64, + pub pyth_decimals: u8, + pub timestamp: u64, + } + + #[derive(Debug, Clone, Serialize, Deserialize)] + pub struct WithdrawCollateralEvent { + pub margin_manager_id: ObjectID, + pub amount: u64, + pub asset: TypeName, + pub withdraw_base_asset: bool, + pub remaining_base_asset: u64, + pub remaining_quote_asset: u64, + pub remaining_base_debt: u64, + pub remaining_quote_debt: u64, + pub base_pyth_price: u64, + pub base_pyth_decimals: u8, + pub quote_pyth_price: u64, + pub quote_pyth_decimals: u8, + pub timestamp: u64, + } + + impl MoveStruct for DepositCollateralEvent { + const MODULE: &'static str = "margin_manager"; + const NAME: &'static str = "DepositCollateralEvent"; + } + + impl MoveStruct for WithdrawCollateralEvent { + const MODULE: &'static str = "margin_manager"; + const NAME: &'static str = "WithdrawCollateralEvent"; + } + } + + pub mod margin_pool { + use super::*; + + #[derive(Debug, Clone, Serialize, Deserialize)] + pub struct MarginPoolConfig { + pub supply_cap: u64, + pub max_utilization_rate: u64, + pub protocol_spread: u64, + pub min_borrow: u64, + pub rate_limit_capacity: u64, + pub rate_limit_refill_rate_per_ms: u64, + pub rate_limit_enabled: bool, + } + + #[derive(Debug, Clone, Serialize, Deserialize)] + pub struct InterestConfig { + pub base_rate: u64, + pub base_slope: u64, + pub optimal_utilization: u64, + pub excess_slope: u64, + } + + #[derive(Debug, Clone, Serialize, Deserialize)] + pub struct ProtocolConfig { + pub margin_pool_config: MarginPoolConfig, + pub interest_config: InterestConfig, + pub extra_fields: VecMap, + } + + #[derive(Debug, Clone, Serialize, Deserialize)] + pub struct MarginPoolCreated { + pub margin_pool_id: ObjectID, + pub maintainer_cap_id: ObjectID, + pub asset_type: String, // TypeName in Move + pub config: ProtocolConfig, + pub timestamp: u64, + } + + #[derive(Debug, Clone, Serialize, Deserialize)] + pub struct DeepbookPoolUpdated { + pub margin_pool_id: ObjectID, + pub deepbook_pool_id: ObjectID, + pub pool_cap_id: ObjectID, + pub enabled: bool, + pub timestamp: u64, + } + + #[derive(Debug, Clone, Serialize, Deserialize)] + pub struct InterestParamsUpdated { + pub margin_pool_id: ObjectID, + pub pool_cap_id: ObjectID, + pub interest_config: InterestConfig, + pub timestamp: u64, + } + + #[derive(Debug, Clone, Serialize, Deserialize)] + pub struct MarginPoolConfigUpdated { + pub margin_pool_id: ObjectID, + pub pool_cap_id: ObjectID, + pub margin_pool_config: MarginPoolConfig, + pub timestamp: u64, + } + + #[derive(Debug, Clone, Serialize, Deserialize)] + pub struct AssetSupplied { + pub margin_pool_id: ObjectID, + pub asset_type: String, // TypeName in Move + pub supplier: Address, + pub supply_amount: u64, + pub supply_shares: u64, + pub timestamp: u64, + } + + #[derive(Debug, Clone, Serialize, Deserialize)] + pub struct AssetWithdrawn { + pub margin_pool_id: ObjectID, + pub asset_type: String, // TypeName in Move + pub supplier: Address, + pub withdraw_amount: u64, + pub withdraw_shares: u64, + pub timestamp: u64, + } + + #[derive(Debug, Clone, Serialize, Deserialize)] + pub struct MaintainerFeesWithdrawn { + pub margin_pool_id: ObjectID, + pub margin_pool_cap_id: ObjectID, + pub maintainer_fees: u64, + pub timestamp: u64, + } + + #[derive(Debug, Clone, Serialize, Deserialize)] + pub struct ProtocolFeesWithdrawn { + pub margin_pool_id: ObjectID, + pub protocol_fees: u64, + pub timestamp: u64, + } + + #[derive(Debug, Clone, Serialize, Deserialize)] + pub struct SupplierCapMinted { + pub supplier_cap_id: ObjectID, + pub timestamp: u64, + } + + #[derive(Debug, Clone, Serialize, Deserialize)] + pub struct SupplyReferralMinted { + pub margin_pool_id: ObjectID, + pub supply_referral_id: ObjectID, + pub owner: Address, + pub timestamp: u64, + } + + impl MoveStruct for MarginPoolCreated { + const MODULE: &'static str = "margin_pool"; + const NAME: &'static str = "MarginPoolCreated"; + } + + impl MoveStruct for DeepbookPoolUpdated { + const MODULE: &'static str = "margin_pool"; + const NAME: &'static str = "DeepbookPoolUpdated"; + } + + impl MoveStruct for InterestParamsUpdated { + const MODULE: &'static str = "margin_pool"; + const NAME: &'static str = "InterestParamsUpdated"; + } + + impl MoveStruct for MarginPoolConfigUpdated { + const MODULE: &'static str = "margin_pool"; + const NAME: &'static str = "MarginPoolConfigUpdated"; + } + + impl MoveStruct for AssetSupplied { + const MODULE: &'static str = "margin_pool"; + const NAME: &'static str = "AssetSupplied"; + } + + impl MoveStruct for AssetWithdrawn { + const MODULE: &'static str = "margin_pool"; + const NAME: &'static str = "AssetWithdrawn"; + } + + impl MoveStruct for MaintainerFeesWithdrawn { + const MODULE: &'static str = "margin_pool"; + const NAME: &'static str = "MaintainerFeesWithdrawn"; + } + + impl MoveStruct for ProtocolFeesWithdrawn { + const MODULE: &'static str = "margin_pool"; + const NAME: &'static str = "ProtocolFeesWithdrawn"; + } + + impl MoveStruct for SupplierCapMinted { + const MODULE: &'static str = "margin_pool"; + const NAME: &'static str = "SupplierCapMinted"; + } + + impl MoveStruct for SupplyReferralMinted { + const MODULE: &'static str = "margin_pool"; + const NAME: &'static str = "SupplyReferralMinted"; + } + } + + pub mod margin_registry { + use super::*; + + #[derive(Debug, Clone, Serialize, Deserialize)] + pub struct RiskRatios { + pub min_withdraw_risk_ratio: u64, + pub min_borrow_risk_ratio: u64, + pub liquidation_risk_ratio: u64, + pub target_liquidation_risk_ratio: u64, + } + + #[derive(Debug, Clone, Serialize, Deserialize)] + pub struct PoolConfig { + pub base_margin_pool_id: ObjectID, + pub quote_margin_pool_id: ObjectID, + pub risk_ratios: RiskRatios, + pub user_liquidation_reward: u64, + pub pool_liquidation_reward: u64, + pub enabled: bool, + pub extra_fields: VecMap, + } + + #[derive(Debug, Clone, Serialize, Deserialize)] + pub struct MaintainerCapUpdated { + pub maintainer_cap_id: ObjectID, + pub allowed: bool, + pub timestamp: u64, + } + + #[derive(Debug, Clone, Serialize, Deserialize)] + pub struct DeepbookPoolRegistered { + pub pool_id: ObjectID, + pub config: PoolConfig, + pub timestamp: u64, + } + + #[derive(Debug, Clone, Serialize, Deserialize)] + pub struct DeepbookPoolUpdated { + pub pool_id: ObjectID, + pub enabled: bool, + pub timestamp: u64, + } + + #[derive(Debug, Clone, Serialize, Deserialize)] + pub struct DeepbookPoolConfigUpdated { + pub pool_id: ObjectID, + pub config: PoolConfig, + pub timestamp: u64, + } + + #[derive(Debug, Clone, Serialize, Deserialize)] + pub struct PauseCapUpdated { + pub pause_cap_id: ObjectID, + pub allowed: bool, + pub timestamp: u64, + } + + impl MoveStruct for RiskRatios { + const MODULE: &'static str = "margin_registry"; + const NAME: &'static str = "RiskRatios"; + } + + impl MoveStruct for PoolConfig { + const MODULE: &'static str = "margin_registry"; + const NAME: &'static str = "PoolConfig"; + } + + impl MoveStruct for MaintainerCapUpdated { + const MODULE: &'static str = "margin_registry"; + const NAME: &'static str = "MaintainerCapUpdated"; + } + + impl MoveStruct for DeepbookPoolRegistered { + const MODULE: &'static str = "margin_registry"; + const NAME: &'static str = "DeepbookPoolRegistered"; + } + + impl MoveStruct for DeepbookPoolUpdated { + const MODULE: &'static str = "margin_registry"; + const NAME: &'static str = "DeepbookPoolUpdated"; + } + + impl MoveStruct for DeepbookPoolConfigUpdated { + const MODULE: &'static str = "margin_registry"; + const NAME: &'static str = "DeepbookPoolConfigUpdated"; + } + + impl MoveStruct for PauseCapUpdated { + const MODULE: &'static str = "margin_registry"; + const NAME: &'static str = "PauseCapUpdated"; + } + } + + pub mod protocol_fees { + use super::*; + + #[derive(Debug, Clone, Serialize, Deserialize)] + pub struct ProtocolFeesIncreasedEvent { + pub margin_pool_id: ObjectID, + pub total_shares: u64, + pub referral_fees: u64, + pub maintainer_fees: u64, + pub protocol_fees: u64, + } + + #[derive(Debug, Clone, Serialize, Deserialize)] + pub struct ReferralFeesClaimedEvent { + pub referral_id: ObjectID, + pub owner: Address, + pub fees: u64, + } + + impl MoveStruct for ProtocolFeesIncreasedEvent { + const MODULE: &'static str = "protocol_fees"; + const NAME: &'static str = "ProtocolFeesIncreasedEvent"; + } + + impl MoveStruct for ReferralFeesClaimedEvent { + const MODULE: &'static str = "protocol_fees"; + const NAME: &'static str = "ReferralFeesClaimedEvent"; + } + } + + pub mod tpsl { + use super::*; + + /// Condition for triggering a conditional order (take profit or stop loss) + #[derive(Debug, Clone, Serialize, Deserialize)] + pub struct Condition { + pub trigger_below_price: bool, + pub trigger_price: u64, + } + + /// Pending order details that will be placed when the condition is met + #[derive(Debug, Clone, Serialize, Deserialize)] + pub struct PendingOrder { + pub is_limit_order: bool, + pub client_order_id: u64, + pub order_type: Option, + pub self_matching_option: u8, + pub price: Option, + pub quantity: u64, + pub is_bid: bool, + pub pay_with_deep: bool, + pub expire_timestamp: Option, + } + + /// Complete conditional order containing both condition and pending order + #[derive(Debug, Clone, Serialize, Deserialize)] + pub struct ConditionalOrder { + pub conditional_order_id: u64, + pub condition: Condition, + pub pending_order: PendingOrder, + } + + /// Emitted when a new conditional order (TPSL) is created + #[derive(Debug, Clone, Serialize, Deserialize)] + pub struct ConditionalOrderAdded { + pub manager_id: ObjectID, + pub conditional_order_id: u64, + pub conditional_order: ConditionalOrder, + pub timestamp: u64, + } + + /// Emitted when a conditional order is cancelled + #[derive(Debug, Clone, Serialize, Deserialize)] + pub struct ConditionalOrderCancelled { + pub manager_id: ObjectID, + pub conditional_order_id: u64, + pub conditional_order: ConditionalOrder, + pub timestamp: u64, + } + + /// Emitted when a conditional order is triggered and executed + #[derive(Debug, Clone, Serialize, Deserialize)] + pub struct ConditionalOrderExecuted { + pub manager_id: ObjectID, + pub pool_id: ObjectID, + pub conditional_order_id: u64, + pub conditional_order: ConditionalOrder, + pub timestamp: u64, + } + + /// Emitted when a conditional order fails due to insufficient funds + #[derive(Debug, Clone, Serialize, Deserialize)] + pub struct ConditionalOrderInsufficientFunds { + pub manager_id: ObjectID, + pub conditional_order_id: u64, + pub conditional_order: ConditionalOrder, + pub timestamp: u64, + } + + impl MoveStruct for ConditionalOrderAdded { + const MODULE: &'static str = "tpsl"; + const NAME: &'static str = "ConditionalOrderAdded"; + } + + impl MoveStruct for ConditionalOrderCancelled { + const MODULE: &'static str = "tpsl"; + const NAME: &'static str = "ConditionalOrderCancelled"; + } + + impl MoveStruct for ConditionalOrderExecuted { + const MODULE: &'static str = "tpsl"; + const NAME: &'static str = "ConditionalOrderExecuted"; + } + + impl MoveStruct for ConditionalOrderInsufficientFunds { + const MODULE: &'static str = "tpsl"; + const NAME: &'static str = "ConditionalOrderInsufficientFunds"; + } + } +} + +// SUI module +pub mod sui { + pub mod sui { + use crate::models::MoveStruct; + use serde::{Deserialize, Serialize}; + use sui_sdk_types::Address; + + #[derive(Debug, Clone, Serialize, Deserialize)] + pub struct SUI { + pub id: Address, + } + + impl MoveStruct for SUI { + const MODULE: &'static str = "sui"; + const NAME: &'static str = "SUI"; + } + } +} diff --git a/crates/indexer/src/traits.rs b/crates/indexer/src/traits.rs new file mode 100644 index 000000000..f1bbbe6e5 --- /dev/null +++ b/crates/indexer/src/traits.rs @@ -0,0 +1,157 @@ +//! MoveStruct trait for DeepBook indexer +//! +//! This module provides the MoveStruct trait for handling Move structs and event types. +//! Module definitions and related functions have been moved to lib.rs for centralized configuration. + +use serde::Serialize; +use std::str::FromStr; +use sui_sdk_types::{Address, Identifier, StructTag}; + +// Import types and functions from lib.rs +use crate::{get_module_type, ModuleType}; + +/// Trait for Move structs that can be matched against event types +pub trait MoveStruct: Serialize { + // Event type matching constants + const MODULE: &'static str; + const NAME: &'static str; + const TYPE_PARAMS: &'static [&'static str] = &[]; + + /// Get the list of acceptable package addresses for this event type based on environment + fn acceptable_package_addresses(env: crate::DeepbookEnv) -> Result, String> { + get_package_addresses_for_module(Self::MODULE, env) + } + + /// Check if a struct tag matches this event type from any supported package version + fn matches_event_type( + event_type: &move_core_types::language_storage::StructTag, + env: crate::DeepbookEnv, + ) -> bool { + use move_core_types::account_address::AccountAddress; + + // Get all possible struct types for this event + let all_struct_types = Self::get_all_struct_types(env); + + // Check if the event type matches any of the generated struct types + // NOTE: We intentionally ignore type_params.len() because events may have phantom/generic type parameters + // that don't affect the actual event structure (e.g., DeepBurned) + all_struct_types.iter().any(|struct_type| { + event_type.address == AccountAddress::new(*struct_type.address.inner()) + && event_type.module.as_str() == struct_type.module.as_str() + && event_type.name.as_str() == struct_type.name.as_str() + }) + } + + /// Generate all possible struct types for this event across all supported package versions + fn get_all_struct_types(env: crate::DeepbookEnv) -> Vec { + let acceptable_addresses = match Self::acceptable_package_addresses(env) { + Ok(addresses) => addresses, + Err(_) => return Vec::new(), // Return empty vec if module is unknown + }; + let mut struct_types = Vec::new(); + + for address in acceptable_addresses { + let struct_tag = StructTag { + address: (*address.inner()).into(), + module: Identifier::from_str(Self::MODULE).unwrap(), + name: Identifier::from_str(Self::NAME).unwrap(), + type_params: Self::TYPE_PARAMS + .iter() + .map(|param| { + sui_sdk_types::TypeTag::Struct(Box::new(StructTag { + address: (*address.inner()).into(), + module: Identifier::from_str(Self::MODULE).unwrap(), + name: Identifier::from_str(param).unwrap(), + type_params: Vec::new(), + })) + }) + .collect(), + }; + struct_types.push(struct_tag); + } + + struct_types + } +} + +/// Generic helper that reads package addresses from lib.rs at runtime +pub fn get_package_addresses_for_module( + module: &str, + env: crate::DeepbookEnv, +) -> Result, String> { + match get_module_type(module) { + ModuleType::Core => { + // Get core package addresses using helper function + let core_packages = crate::get_core_package_addresses(env); + let mut addresses = Vec::new(); + + // Convert string addresses to Address types + for addr_str in core_packages { + if let Ok(addr) = parse_address_from_hex(addr_str) { + addresses.push(addr); + } + } + + Ok(addresses) + } + ModuleType::Margin => { + // Get margin package addresses with validation + // This will fail fast if margin trading is not supported on the current environment + let margin_packages = crate::get_margin_package_addresses(env); + let mut addresses = Vec::new(); + + // Convert string addresses to Address types + for addr_str in margin_packages { + if let Ok(addr) = parse_address_from_hex(addr_str) { + addresses.push(addr); + } + } + + if addresses.is_empty() { + Err(format!( + "Margin trading is not supported on {:?}. \ + The margin package has not been deployed on this network. \ + Requested module: '{}'", + env, module + )) + } else { + Ok(addresses) + } + } + ModuleType::Sui => { + const SUI_SYSTEM_ADDRESS: &str = + "0000000000000000000000000000000000000000000000000000000000000002"; + if let Ok(addr) = parse_address_from_hex(SUI_SYSTEM_ADDRESS) { + Ok(vec![addr]) + } else { + Err("Failed to parse SUI system address".to_string()) + } + } + ModuleType::Unknown => { + // Raise exception for unknown modules + Err(format!("Unknown module: {}", module)) + } + } +} + +/// Helper function to parse hex string addresses +fn parse_address_from_hex(hex_str: &str) -> Result { + // Remove 0x prefix if present + let hex_str = if hex_str.starts_with("0x") { + &hex_str[2..] + } else { + hex_str + }; + + // Parse hex string to bytes + let bytes = hex::decode(hex_str).map_err(|e| format!("Failed to decode hex: {}", e))?; + + if bytes.len() != 32 { + return Err(format!("Expected 32 bytes, got {}", bytes.len())); + } + + let mut addr_bytes = [0u8; 32]; + addr_bytes.copy_from_slice(&bytes); + + Ok(Address::new(addr_bytes)) +} diff --git a/crates/indexer/tests/README.md b/crates/indexer/tests/README.md new file mode 100644 index 000000000..0da0d92c7 --- /dev/null +++ b/crates/indexer/tests/README.md @@ -0,0 +1,220 @@ +# DeepBook Indexer Test Suite + +This directory contains the test suite for the DeepBook indexer, including snapshot tests and checkpoint data for margin events. + +## Overview + +The test suite uses snapshot testing with real checkpoint data from Sui to verify that the indexer correctly processes and stores margin events. Tests are organized by event type and use actual checkpoint files downloaded from Sui testnet. + +## Directory Structure + +``` +tests/ +├── README.md # This file +├── snapshot_tests.rs # Main test file with snapshot tests +├── checkpoints/ # Checkpoint data directory +│ ├── margin_manager_created/ # MarginManagerEvent checkpoints +│ ├── asset_supplied/ # AssetSupplied event checkpoints +│ ├── margin_pool_created/ # MarginPoolCreated event checkpoints +│ ├── deepbook_pool_registered/ # DeepbookPoolRegistered event checkpoints +│ └── [other_event_types]/ # Other event type directories +└── snapshots/ # Generated snapshot files + ├── snapshot_tests__margin_manager_created__margin_manager_created.snap + ├── snapshot_tests__asset_supplied__asset_supplied.snap + └── [other_snapshots] +``` + +## Finding and Downloading Checkpoint Files + +### 1. Using Sui GraphQL API + +Sui testnet provides a GraphQL API at `https://graphql.testnet.sui.io/graphql` for querying events and finding checkpoint numbers. + +#### Query for Events + +```bash +curl -X POST https://graphql.testnet.sui.io/graphql \ + -H "Content-Type: application/json" \ + -d '{ + "query": "query { events(filter: { type: \"0x442d21fd044b90274934614c3c41416c83582f42eaa8feb4fecea301aa6bdd54::margin_registry::DeepbookPoolRegistered\" }) { nodes { transaction { effects { checkpoint { sequenceNumber } } } sender { address } timestamp } } }" + }' +``` + +#### Get Checkpoint Information + +```bash +curl -X POST https://graphql.testnet.sui.io/graphql \ + -H "Content-Type: application/json" \ + -d '{ + "query": "query { checkpoint(sequenceNumber: 248053954) { sequenceNumber timestamp } }" + }' +``` + +### 2. Downloading Checkpoint Files + +Once you have the checkpoint sequence number, download the checkpoint file: + +```bash +# Navigate to the appropriate event directory +cd /crates/indexer/tests/checkpoints/[event_type] + +# Download the checkpoint file +curl -o [checkpoint_number].chk "https://checkpoints.testnet.sui.io/[checkpoint_number].chk" +``` + +#### Example: Downloading DeepbookPoolRegistered Event + +```bash +# Create directory if it doesn't exist +mkdir -p /crates/indexer/tests/checkpoints/deepbook_pool_registered + +# Download checkpoint 248053954 +cd /crates/indexer/tests/checkpoints/deepbook_pool_registered +curl -o 248053954.chk "https://checkpoints.testnet.sui.io/248053954.chk" +``` + +### 3. Verifying Checkpoint Files + +Check that the checkpoint file is downloadable and has content: + +```bash +curl -I "https://checkpoints.testnet.sui.io/248053954.chk" +``` + +Expected response should include: +- `HTTP/2 200` (success) +- `content-length: [size]` (file size > 0) +- `content-type: application/octet-stream` + +## Event Types and Package Information + +### DeepBook Margin Package + +- **Package ID (Testnet):** `0x442d21fd044b90274934614c3c41416c83582f42eaa8feb4fecea301aa6bdd54` +- **Network:** Sui Testnet +- **GraphQL Endpoint:** `https://graphql.testnet.sui.io/graphql` + + + +### 3. Snapshot Management + +#### Review New Snapshots +```bash +cargo insta review +``` + +#### Accept All Snapshots +```bash +cargo insta accept +``` + +#### Run All Snapshot Tests +```bash +cargo insta test +``` + + +## Adding New Event Tests + +### 1. Find Events on Testnet + +Use the GraphQL API to search for events: + +```bash +curl -X POST https://graphql.testnet.sui.io/graphql \ + -H "Content-Type: application/json" \ + -d '{ + "query": "query { events(filter: { type: \"[EVENT_TYPE]\" }) { nodes { transaction { effects { checkpoint { sequenceNumber } } } sender { address } timestamp } } }" + }' +``` + +### 2. Download Checkpoint Files + +```bash +# Create directory for the event type +mkdir -p /crates/indexer/tests/checkpoints/[event_type] + +# Download checkpoint files +cd /crates/indexer/tests/checkpoints/[event_type] +curl -o [checkpoint_number].chk "https://checkpoints.testnet.sui.io/[checkpoint_number].chk" +``` + +### 3. Update Test File + +Remove the `#[ignore]` attribute from the test in `snapshot_tests.rs`: + +```rust +#[tokio::test] +// #[ignore] // TODO: Add checkpoint test data <-- Remove this line +async fn [event_type]_test() -> Result<(), anyhow::Error> { + let handler = [EventHandler]::new(DeepbookEnv::Testnet); + data_test("[event_type]", handler, ["[event_type]"]).await?; + Ok(()) +} +``` + +### 4. Run the Test + +```bash +cd +PATH="/usr/lib/postgresql/16/bin:$PATH" cargo test [event_type]_test --package deepbook-indexer +``` + +### 5. Review and Accept Snapshot + +```bash +cd +cargo insta review +# Type 'y' to accept the snapshot +``` + +## Troubleshooting + +### Common Issues + +#### 1. `initdb` Command Not Found +```bash +# Install PostgreSQL development tools +sudo apt install -y postgresql-server-dev-all + +# Ensure PATH includes PostgreSQL binaries +export PATH="/usr/lib/postgresql/16/bin:$PATH" +``` + +#### 2. Database Connection Issues +The tests use temporary databases, so no external database setup is required. If you see connection errors, ensure PostgreSQL development tools are properly installed. + +#### 3. Checkpoint File Not Found +- Verify the checkpoint number is correct +- Check that the checkpoint file exists: `curl -I "https://checkpoints.testnet.sui.io/[checkpoint].chk"` +- Ensure the checkpoint is from the testnet (not mainnet) + +#### 4. No Events Found +- Verify the event type string is correct +- Check that events exist on the testnet using GraphQL API +- Some events may not have been triggered yet on testnet + +### Debug Commands + +#### Check PostgreSQL Installation +```bash +which initdb +initdb --version +``` + +#### Verify Checkpoint Files +```bash +ls -la /crates/indexer/tests/checkpoints/*/ +``` + +#### Check Test Compilation +```bash +cd +cargo check --package deepbook-indexer +``` + +## References + +- [Sui GraphQL API Documentation](https://docs.sui.io/guides/developer/getting-started/graphql-rpc) +- [Sui Testnet Checkpoints](https://checkpoints.testnet.sui.io/) +- [Insta Snapshot Testing](https://insta.rs/) diff --git a/crates/indexer/tests/checkpoints/asset_supplied/273617360.chk b/crates/indexer/tests/checkpoints/asset_supplied/273617360.chk new file mode 100644 index 000000000..702b5d551 Binary files /dev/null and b/crates/indexer/tests/checkpoints/asset_supplied/273617360.chk differ diff --git a/crates/indexer/tests/checkpoints/asset_withdrawn/273992699.chk b/crates/indexer/tests/checkpoints/asset_withdrawn/273992699.chk new file mode 100644 index 000000000..12d92dfc8 Binary files /dev/null and b/crates/indexer/tests/checkpoints/asset_withdrawn/273992699.chk differ diff --git a/crates/indexer/tests/checkpoints/balances/100000177.chk b/crates/indexer/tests/checkpoints/balances/100000177.chk new file mode 100644 index 000000000..52eb8a630 Binary files /dev/null and b/crates/indexer/tests/checkpoints/balances/100000177.chk differ diff --git a/crates/indexer/tests/checkpoints/balances_indirect/81855955.chk b/crates/indexer/tests/checkpoints/balances_indirect/81855955.chk new file mode 100644 index 000000000..e01de1b3b Binary files /dev/null and b/crates/indexer/tests/checkpoints/balances_indirect/81855955.chk differ diff --git a/crates/indexer/tests/checkpoints/conditional_order_added/234928955.chk b/crates/indexer/tests/checkpoints/conditional_order_added/234928955.chk new file mode 100644 index 000000000..738f7da34 Binary files /dev/null and b/crates/indexer/tests/checkpoints/conditional_order_added/234928955.chk differ diff --git a/crates/indexer/tests/checkpoints/conditional_order_cancelled/234928968.chk b/crates/indexer/tests/checkpoints/conditional_order_cancelled/234928968.chk new file mode 100644 index 000000000..8016f88a6 Binary files /dev/null and b/crates/indexer/tests/checkpoints/conditional_order_cancelled/234928968.chk differ diff --git a/crates/indexer/tests/checkpoints/deep_burned/193585515.chk b/crates/indexer/tests/checkpoints/deep_burned/193585515.chk new file mode 100644 index 000000000..13ffec277 Binary files /dev/null and b/crates/indexer/tests/checkpoints/deep_burned/193585515.chk differ diff --git a/crates/indexer/tests/checkpoints/deepbook_pool_registered/273618562.chk b/crates/indexer/tests/checkpoints/deepbook_pool_registered/273618562.chk new file mode 100644 index 000000000..0417430fa Binary files /dev/null and b/crates/indexer/tests/checkpoints/deepbook_pool_registered/273618562.chk differ diff --git a/crates/indexer/tests/checkpoints/deepbook_pool_updated/273561675.chk b/crates/indexer/tests/checkpoints/deepbook_pool_updated/273561675.chk new file mode 100644 index 000000000..00f5d8424 Binary files /dev/null and b/crates/indexer/tests/checkpoints/deepbook_pool_updated/273561675.chk differ diff --git a/crates/indexer/tests/checkpoints/deepbook_pool_updated_registry/273618562.chk b/crates/indexer/tests/checkpoints/deepbook_pool_updated_registry/273618562.chk new file mode 100644 index 000000000..0417430fa Binary files /dev/null and b/crates/indexer/tests/checkpoints/deepbook_pool_updated_registry/273618562.chk differ diff --git a/crates/indexer/tests/checkpoints/deposit_collateral/234918188.chk b/crates/indexer/tests/checkpoints/deposit_collateral/234918188.chk new file mode 100644 index 000000000..d45b5424e Binary files /dev/null and b/crates/indexer/tests/checkpoints/deposit_collateral/234918188.chk differ diff --git a/crates/indexer/tests/checkpoints/flash_loans/100001465.chk b/crates/indexer/tests/checkpoints/flash_loans/100001465.chk new file mode 100644 index 000000000..131eebf24 Binary files /dev/null and b/crates/indexer/tests/checkpoints/flash_loans/100001465.chk differ diff --git a/crates/indexer/tests/checkpoints/liquidation/275761458.chk b/crates/indexer/tests/checkpoints/liquidation/275761458.chk new file mode 100644 index 000000000..c8fe5a31a Binary files /dev/null and b/crates/indexer/tests/checkpoints/liquidation/275761458.chk differ diff --git a/crates/indexer/tests/checkpoints/loan_borrowed/273619256.chk b/crates/indexer/tests/checkpoints/loan_borrowed/273619256.chk new file mode 100644 index 000000000..bd4da5b2d Binary files /dev/null and b/crates/indexer/tests/checkpoints/loan_borrowed/273619256.chk differ diff --git a/crates/indexer/tests/checkpoints/maintainer_cap_updated/273301160.chk b/crates/indexer/tests/checkpoints/maintainer_cap_updated/273301160.chk new file mode 100644 index 000000000..a6fe601f3 Binary files /dev/null and b/crates/indexer/tests/checkpoints/maintainer_cap_updated/273301160.chk differ diff --git a/crates/indexer/tests/checkpoints/margin_manager_created/273618704.chk b/crates/indexer/tests/checkpoints/margin_manager_created/273618704.chk new file mode 100644 index 000000000..abb91b4ba Binary files /dev/null and b/crates/indexer/tests/checkpoints/margin_manager_created/273618704.chk differ diff --git a/crates/indexer/tests/checkpoints/margin_pool_created/273561287.chk b/crates/indexer/tests/checkpoints/margin_pool_created/273561287.chk new file mode 100644 index 000000000..9f81b1175 Binary files /dev/null and b/crates/indexer/tests/checkpoints/margin_pool_created/273561287.chk differ diff --git a/crates/indexer/tests/checkpoints/order_fill/100000337.chk b/crates/indexer/tests/checkpoints/order_fill/100000337.chk new file mode 100644 index 000000000..50c95ffeb Binary files /dev/null and b/crates/indexer/tests/checkpoints/order_fill/100000337.chk differ diff --git a/crates/indexer/tests/checkpoints/order_update/100000017.chk b/crates/indexer/tests/checkpoints/order_update/100000017.chk new file mode 100644 index 000000000..ba18cefb0 Binary files /dev/null and b/crates/indexer/tests/checkpoints/order_update/100000017.chk differ diff --git a/crates/indexer/tests/checkpoints/pool_created/231798277.chk b/crates/indexer/tests/checkpoints/pool_created/231798277.chk new file mode 100644 index 000000000..ccdbf6b9e Binary files /dev/null and b/crates/indexer/tests/checkpoints/pool_created/231798277.chk differ diff --git a/crates/indexer/tests/checkpoints/pool_created/67459304.chk b/crates/indexer/tests/checkpoints/pool_created/67459304.chk new file mode 100644 index 000000000..bf010d631 Binary files /dev/null and b/crates/indexer/tests/checkpoints/pool_created/67459304.chk differ diff --git a/crates/indexer/tests/checkpoints/pool_price/100005828.chk b/crates/indexer/tests/checkpoints/pool_price/100005828.chk new file mode 100644 index 000000000..c4227c0af Binary files /dev/null and b/crates/indexer/tests/checkpoints/pool_price/100005828.chk differ diff --git a/crates/indexer/tests/checkpoints/protocol_fees_increased/273995602.chk b/crates/indexer/tests/checkpoints/protocol_fees_increased/273995602.chk new file mode 100644 index 000000000..0745f0f67 Binary files /dev/null and b/crates/indexer/tests/checkpoints/protocol_fees_increased/273995602.chk differ diff --git a/crates/indexer/tests/checkpoints/referral_fee_events/233962755.chk b/crates/indexer/tests/checkpoints/referral_fee_events/233962755.chk new file mode 100644 index 000000000..ae2f37ac8 Binary files /dev/null and b/crates/indexer/tests/checkpoints/referral_fee_events/233962755.chk differ diff --git a/crates/indexer/tests/checkpoints/referral_fee_events/README.md b/crates/indexer/tests/checkpoints/referral_fee_events/README.md new file mode 100644 index 000000000..81b7a4c76 --- /dev/null +++ b/crates/indexer/tests/checkpoints/referral_fee_events/README.md @@ -0,0 +1,39 @@ +# ReferralFeeEvent Checkpoint + +This directory contains checkpoint files with `ReferralFeeEvent` events from mainnet. + +## Checkpoint 233962755 + +- **Transaction**: `D2YQEZ6D3SfUZ2bVGpMbrzGPoZAbrLYwy5m249aHwqL1` +- **Pool**: SUI/USDC (`0xe05dafb5133bcffb8d59f4e12465dc0e9faeaa05e3e342a08fe135800e3e4407`) +- **Referral ID**: `0xf66fc08674e5592b471d965c82410af5a2b44e2b4b92f191d91c7147d378bcaa` +- **Generated**: January 2026 + +The event was generated using the script in `_local_scripts/generate-referral-event/`. + +## Event Details + +The `ReferralFeeEvent` is emitted when a spot trade executes through a balance manager +that has an associated referral ID set via `set_balance_manager_referral()`. + +Event fields: +- `pool_id`: The trading pool where the order executed +- `referral_id`: The DeepBookPoolReferral object linked to the balance manager +- `base_fee`: Fee amount in base token (e.g., SUI) +- `quote_fee`: Fee amount in quote token (e.g., USDC) +- `deep_fee`: Fee amount in DEEP token + +## How to Generate More Test Data + +```bash +cd _local_scripts/generate-referral-event +cp .env.example .env +# Edit .env with your private key +npm install +npx tsx generate-referral-event-simple.ts +``` + +The script will output the checkpoint number. Download with: +```bash +curl -o .chk "https://checkpoints.mainnet.sui.io/.chk" +``` diff --git a/crates/indexer/tests/checkpoints/referral_fees_claimed/273993083.chk b/crates/indexer/tests/checkpoints/referral_fees_claimed/273993083.chk new file mode 100644 index 000000000..918a44031 Binary files /dev/null and b/crates/indexer/tests/checkpoints/referral_fees_claimed/273993083.chk differ diff --git a/crates/indexer/tests/checkpoints/supplier_cap_minted/273302447.chk b/crates/indexer/tests/checkpoints/supplier_cap_minted/273302447.chk new file mode 100644 index 000000000..d775f2e40 Binary files /dev/null and b/crates/indexer/tests/checkpoints/supplier_cap_minted/273302447.chk differ diff --git a/crates/indexer/tests/checkpoints/supply_referral_minted/273301811.chk b/crates/indexer/tests/checkpoints/supply_referral_minted/273301811.chk new file mode 100644 index 000000000..7817b3c1c Binary files /dev/null and b/crates/indexer/tests/checkpoints/supply_referral_minted/273301811.chk differ diff --git a/crates/indexer/tests/checkpoints/withdraw_collateral/234920766.chk b/crates/indexer/tests/checkpoints/withdraw_collateral/234920766.chk new file mode 100644 index 000000000..ad39c61e3 Binary files /dev/null and b/crates/indexer/tests/checkpoints/withdraw_collateral/234920766.chk differ diff --git a/crates/indexer/tests/snapshot_tests.rs b/crates/indexer/tests/snapshot_tests.rs new file mode 100644 index 000000000..3bbaacc0d --- /dev/null +++ b/crates/indexer/tests/snapshot_tests.rs @@ -0,0 +1,569 @@ +use bigdecimal::BigDecimal; +use chrono::NaiveDateTime; +use deepbook_indexer::handlers::asset_supplied_handler::AssetSuppliedHandler; +use deepbook_indexer::handlers::asset_withdrawn_handler::AssetWithdrawnHandler; +use deepbook_indexer::handlers::balances_handler::BalancesHandler; +use deepbook_indexer::handlers::deep_burned_handler::DeepBurnedHandler; +use deepbook_indexer::handlers::deepbook_pool_config_updated_handler::DeepbookPoolConfigUpdatedHandler; +use deepbook_indexer::handlers::deepbook_pool_registered_handler::DeepbookPoolRegisteredHandler; +use deepbook_indexer::handlers::deepbook_pool_updated_handler::DeepbookPoolUpdatedHandler; +use deepbook_indexer::handlers::deepbook_pool_updated_registry_handler::DeepbookPoolUpdatedRegistryHandler; +use deepbook_indexer::handlers::flash_loan_handler::FlashLoanHandler; +use deepbook_indexer::handlers::interest_params_updated_handler::InterestParamsUpdatedHandler; +use deepbook_indexer::handlers::liquidation_handler::LiquidationHandler; +use deepbook_indexer::handlers::loan_borrowed_handler::LoanBorrowedHandler; +use deepbook_indexer::handlers::loan_repaid_handler::LoanRepaidHandler; +use deepbook_indexer::handlers::maintainer_cap_updated_handler::MaintainerCapUpdatedHandler; +use deepbook_indexer::handlers::maintainer_fees_withdrawn_handler::MaintainerFeesWithdrawnHandler; +use deepbook_indexer::handlers::margin_manager_created_handler::MarginManagerCreatedHandler; +use deepbook_indexer::handlers::margin_pool_config_updated_handler::MarginPoolConfigUpdatedHandler; +use deepbook_indexer::handlers::margin_pool_created_handler::MarginPoolCreatedHandler; +use deepbook_indexer::handlers::order_fill_handler::OrderFillHandler; +use deepbook_indexer::handlers::order_update_handler::OrderUpdateHandler; +use deepbook_indexer::handlers::pause_cap_updated_handler::PauseCapUpdatedHandler; +use deepbook_indexer::handlers::pool_created_handler::PoolCreatedHandler; +use deepbook_indexer::handlers::pool_price_handler::PoolPriceHandler; +use deepbook_indexer::handlers::protocol_fees_increased_handler::ProtocolFeesIncreasedHandler; +use deepbook_indexer::handlers::protocol_fees_withdrawn_handler::ProtocolFeesWithdrawnHandler; +use deepbook_indexer::handlers::referral_fee_event_handler::ReferralFeeEventHandler; +use deepbook_indexer::handlers::referral_fees_claimed_handler::ReferralFeesClaimedHandler; +use deepbook_indexer::handlers::supplier_cap_minted_handler::SupplierCapMintedHandler; +use deepbook_indexer::handlers::supply_referral_minted_handler::SupplyReferralMintedHandler; + +// Collateral Events +use deepbook_indexer::handlers::deposit_collateral_handler::DepositCollateralHandler; +use deepbook_indexer::handlers::withdraw_collateral_handler::WithdrawCollateralHandler; + +// TPSL (Take Profit / Stop Loss) Events +use deepbook_indexer::handlers::conditional_order_added_handler::ConditionalOrderAddedHandler; +use deepbook_indexer::handlers::conditional_order_cancelled_handler::ConditionalOrderCancelledHandler; +use deepbook_indexer::handlers::conditional_order_executed_handler::ConditionalOrderExecutedHandler; +use deepbook_indexer::handlers::conditional_order_insufficient_funds_handler::ConditionalOrderInsufficientFundsHandler; + +use deepbook_indexer::DeepbookEnv; +use deepbook_schema::MIGRATIONS; +use fastcrypto::hash::{HashFunction, Sha256}; +use insta::assert_json_snapshot; +use serde_json::Value; +use sqlx::{Column, PgPool, Row, ValueRef}; +use std::env; +use std::fs; +use std::path::Path; +use std::sync::Arc; +use sui_indexer_alt_framework::pipeline::concurrent::Handler; +use sui_indexer_alt_framework::pipeline::Processor; +use sui_indexer_alt_framework::store::Store; +use sui_pg_db::temp::TempDb; +use sui_pg_db::Connection; +use sui_pg_db::Db; +use sui_pg_db::DbArgs; +use sui_storage::blob::Blob; +use sui_types::full_checkpoint_content::Checkpoint; +use sui_types::full_checkpoint_content::CheckpointData; + +#[tokio::test] +async fn balances_test() -> Result<(), anyhow::Error> { + let handler = BalancesHandler::new(DeepbookEnv::Mainnet); + data_test("balances", handler, ["balances"]).await?; + Ok(()) +} + +#[tokio::test] +async fn flash_loan_test() -> Result<(), anyhow::Error> { + let handler = FlashLoanHandler::new(DeepbookEnv::Mainnet); + data_test("flash_loans", handler, ["flashloans"]).await?; + Ok(()) +} + +#[tokio::test] +async fn order_fill_test() -> Result<(), anyhow::Error> { + let handler = OrderFillHandler::new(DeepbookEnv::Mainnet); + data_test("order_fill", handler, ["order_fills"]).await?; + Ok(()) +} +#[tokio::test] +async fn order_update_test() -> Result<(), anyhow::Error> { + let handler = OrderUpdateHandler::new(DeepbookEnv::Mainnet); + data_test("order_update", handler, ["order_updates"]).await?; + Ok(()) +} + +#[tokio::test] +async fn pool_price_test() -> Result<(), anyhow::Error> { + let handler = PoolPriceHandler::new(DeepbookEnv::Mainnet); + data_test("pool_price", handler, ["pool_prices"]).await?; + Ok(()) +} + +#[tokio::test] +async fn deep_burned_test() -> Result<(), anyhow::Error> { + let handler = DeepBurnedHandler::new(DeepbookEnv::Mainnet); + data_test("deep_burned", handler, ["deep_burned"]).await?; + Ok(()) +} + +#[tokio::test] +async fn pool_created_test() -> Result<(), anyhow::Error> { + let handler = PoolCreatedHandler::new(DeepbookEnv::Mainnet); + data_test("pool_created", handler, ["pool_created"]).await?; + Ok(()) +} + +#[tokio::test] +async fn balances_indirect_interaction_test() -> Result<(), anyhow::Error> { + // Test that balance events from transactions that interact with DeepBook + // indirectly (through other protocols) are still captured + let handler = BalancesHandler::new(DeepbookEnv::Mainnet); + data_test("balances_indirect", handler, ["balances"]).await?; + Ok(()) +} + +// Margin Manager Events Tests +#[tokio::test] +async fn margin_manager_created_test() -> Result<(), anyhow::Error> { + let handler = MarginManagerCreatedHandler::new(DeepbookEnv::Testnet); + data_test( + "margin_manager_created", + handler, + ["margin_manager_created"], + ) + .await?; + Ok(()) +} + +#[tokio::test] +async fn loan_borrowed_test() -> Result<(), anyhow::Error> { + let handler = LoanBorrowedHandler::new(DeepbookEnv::Testnet); + data_test("loan_borrowed", handler, ["loan_borrowed"]).await?; + Ok(()) +} + +#[tokio::test] +#[ignore] // TODO: Add checkpoint test data +async fn loan_repaid_test() -> Result<(), anyhow::Error> { + let handler = LoanRepaidHandler::new(DeepbookEnv::Testnet); + data_test("loan_repaid", handler, ["loan_repaid"]).await?; + Ok(()) +} + +#[tokio::test] +async fn liquidation_test() -> Result<(), anyhow::Error> { + let handler = LiquidationHandler::new(DeepbookEnv::Testnet); + data_test("liquidation", handler, ["liquidation"]).await?; + Ok(()) +} + +// Margin Pool Operations Events Tests +#[tokio::test] +async fn asset_supplied_test() -> Result<(), anyhow::Error> { + let handler = AssetSuppliedHandler::new(DeepbookEnv::Testnet); + data_test("asset_supplied", handler, ["asset_supplied"]).await?; + Ok(()) +} + +#[tokio::test] +async fn asset_withdrawn_test() -> Result<(), anyhow::Error> { + let handler = AssetWithdrawnHandler::new(DeepbookEnv::Testnet); + data_test("asset_withdrawn", handler, ["asset_withdrawn"]).await?; + Ok(()) +} + +// Margin Pool Admin Events Tests +#[tokio::test] +async fn margin_pool_created_test() -> Result<(), anyhow::Error> { + let handler = MarginPoolCreatedHandler::new(DeepbookEnv::Testnet); + data_test("margin_pool_created", handler, ["margin_pool_created"]).await?; + Ok(()) +} + +#[tokio::test] +async fn deepbook_pool_updated_test() -> Result<(), anyhow::Error> { + let handler = DeepbookPoolUpdatedHandler::new(DeepbookEnv::Testnet); + data_test("deepbook_pool_updated", handler, ["deepbook_pool_updated"]).await?; + Ok(()) +} + +#[tokio::test] +#[ignore] // TODO: Add checkpoint test data +async fn interest_params_updated_test() -> Result<(), anyhow::Error> { + let handler = InterestParamsUpdatedHandler::new(DeepbookEnv::Testnet); + data_test( + "interest_params_updated", + handler, + ["interest_params_updated"], + ) + .await?; + Ok(()) +} + +#[tokio::test] +#[ignore] // TODO: Add checkpoint test data +async fn margin_pool_config_updated_test() -> Result<(), anyhow::Error> { + let handler = MarginPoolConfigUpdatedHandler::new(DeepbookEnv::Testnet); + data_test( + "margin_pool_config_updated", + handler, + ["margin_pool_config_updated"], + ) + .await?; + Ok(()) +} + +// Margin Registry Events Tests +#[tokio::test] +async fn maintainer_cap_updated_test() -> Result<(), anyhow::Error> { + let handler = MaintainerCapUpdatedHandler::new(DeepbookEnv::Testnet); + data_test( + "maintainer_cap_updated", + handler, + ["maintainer_cap_updated"], + ) + .await?; + Ok(()) +} + +#[tokio::test] +async fn deepbook_pool_registered_test() -> Result<(), anyhow::Error> { + let handler = DeepbookPoolRegisteredHandler::new(DeepbookEnv::Testnet); + data_test( + "deepbook_pool_registered", + handler, + ["deepbook_pool_registered"], + ) + .await?; + Ok(()) +} + +#[tokio::test] +async fn deepbook_pool_updated_registry_test() -> Result<(), anyhow::Error> { + let handler = DeepbookPoolUpdatedRegistryHandler::new(DeepbookEnv::Testnet); + data_test( + "deepbook_pool_updated_registry", + handler, + ["deepbook_pool_updated_registry"], + ) + .await?; + Ok(()) +} + +#[tokio::test] +#[ignore] // TODO: Add checkpoint test data +async fn deepbook_pool_config_updated_test() -> Result<(), anyhow::Error> { + let handler = DeepbookPoolConfigUpdatedHandler::new(DeepbookEnv::Testnet); + data_test( + "deepbook_pool_config_updated", + handler, + ["deepbook_pool_config_updated"], + ) + .await?; + Ok(()) +} + +#[tokio::test] +#[ignore] // TODO: Add checkpoint test data - Event does not exist on testnet yet (checked all package versions) +async fn maintainer_fees_withdrawn_test() -> Result<(), anyhow::Error> { + let handler = MaintainerFeesWithdrawnHandler::new(DeepbookEnv::Testnet); + data_test( + "maintainer_fees_withdrawn", + handler, + ["maintainer_fees_withdrawn"], + ) + .await?; + Ok(()) +} + +#[tokio::test] +#[ignore] // TODO: Add checkpoint test data - Event does not exist on testnet yet (checked all package versions) +async fn protocol_fees_withdrawn_test() -> Result<(), anyhow::Error> { + let handler = ProtocolFeesWithdrawnHandler::new(DeepbookEnv::Testnet); + data_test( + "protocol_fees_withdrawn", + handler, + ["protocol_fees_withdrawn"], + ) + .await?; + Ok(()) +} + +#[tokio::test] +async fn supplier_cap_minted_test() -> Result<(), anyhow::Error> { + let handler = SupplierCapMintedHandler::new(DeepbookEnv::Testnet); + data_test("supplier_cap_minted", handler, ["supplier_cap_minted"]).await?; + Ok(()) +} + +#[tokio::test] +async fn supply_referral_minted_test() -> Result<(), anyhow::Error> { + let handler = SupplyReferralMintedHandler::new(DeepbookEnv::Testnet); + data_test( + "supply_referral_minted", + handler, + ["supply_referral_minted"], + ) + .await?; + Ok(()) +} + +#[tokio::test] +#[ignore] // TODO: Add checkpoint test data - Event does not exist on testnet yet (checked all package versions) +async fn pause_cap_updated_test() -> Result<(), anyhow::Error> { + let handler = PauseCapUpdatedHandler::new(DeepbookEnv::Testnet); + data_test("pause_cap_updated", handler, ["pause_cap_updated"]).await?; + Ok(()) +} + +#[tokio::test] +async fn protocol_fees_increased_test() -> Result<(), anyhow::Error> { + let handler = ProtocolFeesIncreasedHandler::new(DeepbookEnv::Testnet); + data_test( + "protocol_fees_increased", + handler, + ["protocol_fees_increased"], + ) + .await?; + Ok(()) +} + +#[tokio::test] +async fn referral_fee_event_test() -> Result<(), anyhow::Error> { + let handler = ReferralFeeEventHandler::new(DeepbookEnv::Mainnet); + data_test("referral_fee_events", handler, ["referral_fee_events"]).await?; + Ok(()) +} + +#[tokio::test] +async fn referral_fees_claimed_test() -> Result<(), anyhow::Error> { + let handler = ReferralFeesClaimedHandler::new(DeepbookEnv::Testnet); + data_test("referral_fees_claimed", handler, ["referral_fees_claimed"]).await?; + Ok(()) +} + +// === Collateral Events Tests === +// Checkpoint 234918188 - TX: GSNpevf2UcTeq3ACPMGRsvLFRRGB9w2H4KB9BR1cEYcQ +#[tokio::test] +async fn deposit_collateral_test() -> Result<(), anyhow::Error> { + let handler = DepositCollateralHandler::new(DeepbookEnv::Mainnet); + data_test("deposit_collateral", handler, ["collateral_events"]).await?; + Ok(()) +} + +// Checkpoint 234920766 - TX: 73DkKzySTo824MBEQREnhNwXbbSpX8YEEb7qbfxxaHGG +#[tokio::test] +async fn withdraw_collateral_test() -> Result<(), anyhow::Error> { + let handler = WithdrawCollateralHandler::new(DeepbookEnv::Mainnet); + data_test("withdraw_collateral", handler, ["collateral_events"]).await?; + Ok(()) +} + +// === TPSL (Take Profit / Stop Loss) Events Tests === +// Checkpoint 234928955 - TX: HRj2fF9ifRA8kXipJy2g6y6UKgMFNeKvvZqfrKY2L825 +#[tokio::test] +async fn conditional_order_added_test() -> Result<(), anyhow::Error> { + let handler = ConditionalOrderAddedHandler::new(DeepbookEnv::Mainnet); + data_test( + "conditional_order_added", + handler, + ["conditional_order_events"], + ) + .await?; + Ok(()) +} + +// Checkpoint 234928968 - TX: 5QcwuLcE7jmunStKgUSCrHPpAw1WC8B9XQLPph3jrKGn +#[tokio::test] +async fn conditional_order_cancelled_test() -> Result<(), anyhow::Error> { + let handler = ConditionalOrderCancelledHandler::new(DeepbookEnv::Mainnet); + data_test( + "conditional_order_cancelled", + handler, + ["conditional_order_events"], + ) + .await?; + Ok(()) +} + +#[tokio::test] +#[ignore] // No mainnet transactions yet - ConditionalOrderExecuted requires price trigger +async fn conditional_order_executed_test() -> Result<(), anyhow::Error> { + let handler = ConditionalOrderExecutedHandler::new(DeepbookEnv::Mainnet); + data_test( + "conditional_order_executed", + handler, + ["conditional_order_executed"], + ) + .await?; + Ok(()) +} + +#[tokio::test] +#[ignore] // No mainnet transactions yet - ConditionalOrderInsufficientFunds requires trigger with low balance +async fn conditional_order_insufficient_funds_test() -> Result<(), anyhow::Error> { + let handler = ConditionalOrderInsufficientFundsHandler::new(DeepbookEnv::Mainnet); + data_test( + "conditional_order_insufficient_funds", + handler, + ["conditional_order_insufficient_funds"], + ) + .await?; + Ok(()) +} + +async fn data_test( + test_name: &str, + handler: H, + tables_to_check: I, +) -> Result<(), anyhow::Error> +where + I: IntoIterator, + H: Processor, + H: Handler::Value>>, + for<'a> H::Store: Store = Connection<'a>>, +{ + // Set up database URL based on environment + // IMPORTANT: Keep temp_db in scope for the entire test, otherwise it gets cleaned up + let (temp_db_opt, url) = + if env::var("USE_REAL_DB").unwrap_or_else(|_| "false".to_string()) == "true" { + // Use REAL PostgreSQL database - DATABASE_URL must be provided + let database_url = env::var("DATABASE_URL") + .expect("DATABASE_URL environment variable must be set when USE_REAL_DB=true"); + (None, database_url) + } else { + // Use MOCK database (existing behavior) + let temp_db = TempDb::new()?; + let url = temp_db.database().url().to_string(); + (Some(temp_db), url) + }; + + let db = Arc::new(Db::for_write(url.parse()?, DbArgs::default()).await?); + + // Only run migrations if using mock database (real DB already has migrations) + if temp_db_opt.is_some() { + db.run_migrations(Some(&MIGRATIONS)).await?; + } + + let mut conn = db.connect().await?; + + // Test setup based on provided test_name + let test_path = Path::new("tests/checkpoints").join(test_name); + let checkpoints = get_checkpoints_in_folder(&test_path)?; + + // Run pipeline for each checkpoint + for checkpoint in checkpoints { + run_pipeline(&handler, &checkpoint, &mut conn).await?; + } + + // Check results by comparing database tables with snapshots + for table in tables_to_check { + let rows = read_table(&table, &url).await?; + + // Only create snapshots if using mock database + if temp_db_opt.is_some() { + assert_json_snapshot!(format!("{test_name}__{table}"), rows); + } + } + + Ok(()) +} + +async fn run_pipeline<'c, H, P: AsRef>( + handler: &H, + path: P, + conn: &mut Connection<'c>, +) -> Result<(), anyhow::Error> +where + H: Processor, + H: Handler::Value>>, + H::Store: Store = Connection<'c>>, +{ + let bytes = fs::read(path)?; + let data = Blob::from_bytes::(&bytes)?; + let cp: Checkpoint = data.into(); + let result = handler.process(&Arc::new(cp)).await?; + handler.commit(&result, conn).await?; + Ok(()) +} + +/// Read the entire table from database as json value. +/// note: bytea values will be hashed to reduce output size. +async fn read_table(table_name: &str, db_url: &str) -> Result, anyhow::Error> { + let pool = PgPool::connect(db_url).await?; + let rows = sqlx::query(&format!("SELECT * FROM {table_name}")) + .fetch_all(&pool) + .await?; + + // To json + Ok(rows + .iter() + .map(|row| { + let mut obj = serde_json::Map::new(); + + for column in row.columns() { + let column_name = column.name(); + + // timestamp is the insert time in deepbook DB, hardcoding it to a fix value. + if column_name == "timestamp" { + obj.insert( + column_name.to_string(), + Value::String("1970-01-01 00:00:00.000000".to_string()), + ); + continue; + } + + let value = if let Ok(v) = row.try_get::(column_name) { + Value::String(v) + } else if let Ok(v) = row.try_get::(column_name) { + Value::String(v.to_string()) + } else if let Ok(v) = row.try_get::(column_name) { + Value::String(v.to_string()) + } else if let Ok(v) = row.try_get::(column_name) { + Value::String(v.to_string()) + } else if let Ok(v) = row.try_get::(column_name) { + Value::String(v.to_string()) + } else if let Ok(v) = row.try_get::(column_name) { + Value::Bool(v) + } else if let Ok(v) = row.try_get::(column_name) { + v + } else if let Ok(v) = row.try_get::, _>(column_name) { + // hash bytea contents + let mut hash_function = Sha256::default(); + hash_function.update(v); + let digest2 = hash_function.finalize(); + Value::String(digest2.to_string()) + } else if let Ok(v) = row.try_get::(column_name) { + Value::String(v.to_string()) + } else if let Ok(true) = row.try_get_raw(column_name).map(|v| v.is_null()) { + Value::Null + } else { + panic!( + "Cannot parse DB value to json, type: {:?}, column: {column_name}", + row.try_get_raw(column_name) + .map(|v| v.type_info().to_string()) + ) + }; + obj.insert(column_name.to_string(), value); + } + + Value::Object(obj) + }) + .collect()) +} + +fn get_checkpoints_in_folder(folder: &Path) -> Result, anyhow::Error> { + let mut files = Vec::new(); + + // Read the directory + for entry in fs::read_dir(folder)? { + let entry = entry?; + let path = entry.path(); + + // Check if it's a file and ends with ".chk" + if path.is_file() && path.extension().and_then(|s| s.to_str()) == Some("chk") { + files.push(path.display().to_string()); + } + } + + // Sort files to ensure deterministic processing order across different systems + (&mut *files).sort(); + + Ok(files) +} diff --git a/crates/indexer/tests/snapshots/snapshot_tests__asset_supplied__asset_supplied.snap b/crates/indexer/tests/snapshots/snapshot_tests__asset_supplied__asset_supplied.snap new file mode 100644 index 000000000..236a8c9ee --- /dev/null +++ b/crates/indexer/tests/snapshots/snapshot_tests__asset_supplied__asset_supplied.snap @@ -0,0 +1,21 @@ +--- +source: crates/indexer/tests/snapshot_tests.rs +expression: rows +--- +[ + { + "event_digest": "9yfhHs3f3sFU7ruJphjLN6GmvToUjE2sqZmi9ebTcwPX0", + "digest": "9yfhHs3f3sFU7ruJphjLN6GmvToUjE2sqZmi9ebTcwPX", + "sender": "0xb3d277c50f7b846a5f609a8d13428ae482b5826bb98437997373f3a0d60d280e", + "checkpoint": "273617360", + "timestamp": "1970-01-01 00:00:00.000000", + "checkpoint_timestamp_ms": "1765396147217", + "package": "", + "margin_pool_id": "0xf3440b4aafcc8b12fc4b242e9590c52873b8238a0d0e52fbf9dae61d2970796a", + "asset_type": "6502dae813dbe5e42643c119a6450a518481f03063febc7e20238e43b6ea9e86::dbtc::DBTC", + "supplier": "0x291d20d07dbbff547ea583b57daed761b8cf9dc4f6090f49980e7a02f63602c6", + "amount": "1000000000", + "shares": "1000000000", + "onchain_timestamp": "1765396147217" + } +] diff --git a/crates/indexer/tests/snapshots/snapshot_tests__asset_withdrawn__asset_withdrawn.snap b/crates/indexer/tests/snapshots/snapshot_tests__asset_withdrawn__asset_withdrawn.snap new file mode 100644 index 000000000..ade354e4c --- /dev/null +++ b/crates/indexer/tests/snapshots/snapshot_tests__asset_withdrawn__asset_withdrawn.snap @@ -0,0 +1,21 @@ +--- +source: crates/indexer/tests/snapshot_tests.rs +expression: rows +--- +[ + { + "event_digest": "7Am25s8NcYd9r1P1Q4mV8H7ogcQ8ndkEWTwqkF2qCqXD3", + "digest": "7Am25s8NcYd9r1P1Q4mV8H7ogcQ8ndkEWTwqkF2qCqXD", + "sender": "0xb3d277c50f7b846a5f609a8d13428ae482b5826bb98437997373f3a0d60d280e", + "checkpoint": "273992699", + "timestamp": "1970-01-01 00:00:00.000000", + "checkpoint_timestamp_ms": "1765486350880", + "package": "0x21473617f3565d704aa67be73ea41243e9e34a42d434c31f8182c67ba01ccf49", + "margin_pool_id": "0xcdbbe6a72e639b647296788e2e4b1cac5cea4246028ba388ba1332ff9a382eea", + "asset_type": "0000000000000000000000000000000000000000000000000000000000000002::sui::SUI", + "supplier": "0x291d20d07dbbff547ea583b57daed761b8cf9dc4f6090f49980e7a02f63602c6", + "amount": "10000000000", + "shares": "10000000000", + "onchain_timestamp": "1765486350814" + } +] diff --git a/crates/indexer/tests/snapshots/snapshot_tests__balances__balances.snap b/crates/indexer/tests/snapshots/snapshot_tests__balances__balances.snap new file mode 100644 index 000000000..36980c4b9 --- /dev/null +++ b/crates/indexer/tests/snapshots/snapshot_tests__balances__balances.snap @@ -0,0 +1,162 @@ +--- +source: crates/indexer/tests/snapshot_tests.rs +expression: rows +--- +[ + { + "event_digest": "9AeQ2ApuPBCuR4mAAwFhz8phfWnVpKs7S81KBDe89M7m0", + "digest": "9AeQ2ApuPBCuR4mAAwFhz8phfWnVpKs7S81KBDe89M7m", + "sender": "0xdb2fabc66becb36d269f6e9a78c0279761a08f1a50a55c2c9f6071a4bac9cc66", + "checkpoint": "100000177", + "timestamp": "1970-01-01 00:00:00.000000", + "checkpoint_timestamp_ms": "1736510411558", + "package": "", + "balance_manager_id": "0xab9f57591f29313ba1d45a0fd5a23ecc6f6975d60aeced7fa7ed1c58b084e9a9", + "asset": "0000000000000000000000000000000000000000000000000000000000000002::sui::SUI", + "amount": "11000000000", + "deposit": true + }, + { + "event_digest": "9AeQ2ApuPBCuR4mAAwFhz8phfWnVpKs7S81KBDe89M7m1", + "digest": "9AeQ2ApuPBCuR4mAAwFhz8phfWnVpKs7S81KBDe89M7m", + "sender": "0xdb2fabc66becb36d269f6e9a78c0279761a08f1a50a55c2c9f6071a4bac9cc66", + "checkpoint": "100000177", + "timestamp": "1970-01-01 00:00:00.000000", + "checkpoint_timestamp_ms": "1736510411558", + "package": "", + "balance_manager_id": "0xab9f57591f29313ba1d45a0fd5a23ecc6f6975d60aeced7fa7ed1c58b084e9a9", + "asset": "dba34672e30cb065b1f93e3ab55318768fd6fef66c15942c9f7cb846e2f900e7::usdc::USDC", + "amount": "0", + "deposit": true + }, + { + "event_digest": "9AeQ2ApuPBCuR4mAAwFhz8phfWnVpKs7S81KBDe89M7m2", + "digest": "9AeQ2ApuPBCuR4mAAwFhz8phfWnVpKs7S81KBDe89M7m", + "sender": "0xdb2fabc66becb36d269f6e9a78c0279761a08f1a50a55c2c9f6071a4bac9cc66", + "checkpoint": "100000177", + "timestamp": "1970-01-01 00:00:00.000000", + "checkpoint_timestamp_ms": "1736510411558", + "package": "", + "balance_manager_id": "0xab9f57591f29313ba1d45a0fd5a23ecc6f6975d60aeced7fa7ed1c58b084e9a9", + "asset": "deeb7a4662eec9f2f3def03fb937a663dddaa2e215b8078a284d026b7946c270::deep::DEEP", + "amount": "364267", + "deposit": true + }, + { + "event_digest": "9AeQ2ApuPBCuR4mAAwFhz8phfWnVpKs7S81KBDe89M7m5", + "digest": "9AeQ2ApuPBCuR4mAAwFhz8phfWnVpKs7S81KBDe89M7m", + "sender": "0xdb2fabc66becb36d269f6e9a78c0279761a08f1a50a55c2c9f6071a4bac9cc66", + "checkpoint": "100000177", + "timestamp": "1970-01-01 00:00:00.000000", + "checkpoint_timestamp_ms": "1736510411558", + "package": "", + "balance_manager_id": "0xab9f57591f29313ba1d45a0fd5a23ecc6f6975d60aeced7fa7ed1c58b084e9a9", + "asset": "0000000000000000000000000000000000000000000000000000000000000002::sui::SUI", + "amount": "0", + "deposit": false + }, + { + "event_digest": "9AeQ2ApuPBCuR4mAAwFhz8phfWnVpKs7S81KBDe89M7m6", + "digest": "9AeQ2ApuPBCuR4mAAwFhz8phfWnVpKs7S81KBDe89M7m", + "sender": "0xdb2fabc66becb36d269f6e9a78c0279761a08f1a50a55c2c9f6071a4bac9cc66", + "checkpoint": "100000177", + "timestamp": "1970-01-01 00:00:00.000000", + "checkpoint_timestamp_ms": "1736510411558", + "package": "", + "balance_manager_id": "0xab9f57591f29313ba1d45a0fd5a23ecc6f6975d60aeced7fa7ed1c58b084e9a9", + "asset": "dba34672e30cb065b1f93e3ab55318768fd6fef66c15942c9f7cb846e2f900e7::usdc::USDC", + "amount": "55847000", + "deposit": false + }, + { + "event_digest": "9AeQ2ApuPBCuR4mAAwFhz8phfWnVpKs7S81KBDe89M7m7", + "digest": "9AeQ2ApuPBCuR4mAAwFhz8phfWnVpKs7S81KBDe89M7m", + "sender": "0xdb2fabc66becb36d269f6e9a78c0279761a08f1a50a55c2c9f6071a4bac9cc66", + "checkpoint": "100000177", + "timestamp": "1970-01-01 00:00:00.000000", + "checkpoint_timestamp_ms": "1736510411558", + "package": "", + "balance_manager_id": "0xab9f57591f29313ba1d45a0fd5a23ecc6f6975d60aeced7fa7ed1c58b084e9a9", + "asset": "deeb7a4662eec9f2f3def03fb937a663dddaa2e215b8078a284d026b7946c270::deep::DEEP", + "amount": "0", + "deposit": false + }, + { + "event_digest": "9AeQ2ApuPBCuR4mAAwFhz8phfWnVpKs7S81KBDe89M7m10", + "digest": "9AeQ2ApuPBCuR4mAAwFhz8phfWnVpKs7S81KBDe89M7m", + "sender": "0xdb2fabc66becb36d269f6e9a78c0279761a08f1a50a55c2c9f6071a4bac9cc66", + "checkpoint": "100000177", + "timestamp": "1970-01-01 00:00:00.000000", + "checkpoint_timestamp_ms": "1736510411558", + "package": "", + "balance_manager_id": "0xc8e754c3c0753664d995aa91e244ed89fcb3587b0a913c76f3f71f2521d2a1bc", + "asset": "deeb7a4662eec9f2f3def03fb937a663dddaa2e215b8078a284d026b7946c270::deep::DEEP", + "amount": "0", + "deposit": true + }, + { + "event_digest": "9AeQ2ApuPBCuR4mAAwFhz8phfWnVpKs7S81KBDe89M7m11", + "digest": "9AeQ2ApuPBCuR4mAAwFhz8phfWnVpKs7S81KBDe89M7m", + "sender": "0xdb2fabc66becb36d269f6e9a78c0279761a08f1a50a55c2c9f6071a4bac9cc66", + "checkpoint": "100000177", + "timestamp": "1970-01-01 00:00:00.000000", + "checkpoint_timestamp_ms": "1736510411558", + "package": "", + "balance_manager_id": "0xc8e754c3c0753664d995aa91e244ed89fcb3587b0a913c76f3f71f2521d2a1bc", + "asset": "dba34672e30cb065b1f93e3ab55318768fd6fef66c15942c9f7cb846e2f900e7::usdc::USDC", + "amount": "55847000", + "deposit": true + }, + { + "event_digest": "9AeQ2ApuPBCuR4mAAwFhz8phfWnVpKs7S81KBDe89M7m12", + "digest": "9AeQ2ApuPBCuR4mAAwFhz8phfWnVpKs7S81KBDe89M7m", + "sender": "0xdb2fabc66becb36d269f6e9a78c0279761a08f1a50a55c2c9f6071a4bac9cc66", + "checkpoint": "100000177", + "timestamp": "1970-01-01 00:00:00.000000", + "checkpoint_timestamp_ms": "1736510411558", + "package": "", + "balance_manager_id": "0xc8e754c3c0753664d995aa91e244ed89fcb3587b0a913c76f3f71f2521d2a1bc", + "asset": "deeb7a4662eec9f2f3def03fb937a663dddaa2e215b8078a284d026b7946c270::deep::DEEP", + "amount": "0", + "deposit": true + }, + { + "event_digest": "9AeQ2ApuPBCuR4mAAwFhz8phfWnVpKs7S81KBDe89M7m15", + "digest": "9AeQ2ApuPBCuR4mAAwFhz8phfWnVpKs7S81KBDe89M7m", + "sender": "0xdb2fabc66becb36d269f6e9a78c0279761a08f1a50a55c2c9f6071a4bac9cc66", + "checkpoint": "100000177", + "timestamp": "1970-01-01 00:00:00.000000", + "checkpoint_timestamp_ms": "1736510411558", + "package": "", + "balance_manager_id": "0xc8e754c3c0753664d995aa91e244ed89fcb3587b0a913c76f3f71f2521d2a1bc", + "asset": "deeb7a4662eec9f2f3def03fb937a663dddaa2e215b8078a284d026b7946c270::deep::DEEP", + "amount": "360000000", + "deposit": false + }, + { + "event_digest": "9AeQ2ApuPBCuR4mAAwFhz8phfWnVpKs7S81KBDe89M7m16", + "digest": "9AeQ2ApuPBCuR4mAAwFhz8phfWnVpKs7S81KBDe89M7m", + "sender": "0xdb2fabc66becb36d269f6e9a78c0279761a08f1a50a55c2c9f6071a4bac9cc66", + "checkpoint": "100000177", + "timestamp": "1970-01-01 00:00:00.000000", + "checkpoint_timestamp_ms": "1736510411558", + "package": "", + "balance_manager_id": "0xc8e754c3c0753664d995aa91e244ed89fcb3587b0a913c76f3f71f2521d2a1bc", + "asset": "dba34672e30cb065b1f93e3ab55318768fd6fef66c15942c9f7cb846e2f900e7::usdc::USDC", + "amount": "50600", + "deposit": false + }, + { + "event_digest": "9AeQ2ApuPBCuR4mAAwFhz8phfWnVpKs7S81KBDe89M7m17", + "digest": "9AeQ2ApuPBCuR4mAAwFhz8phfWnVpKs7S81KBDe89M7m", + "sender": "0xdb2fabc66becb36d269f6e9a78c0279761a08f1a50a55c2c9f6071a4bac9cc66", + "checkpoint": "100000177", + "timestamp": "1970-01-01 00:00:00.000000", + "checkpoint_timestamp_ms": "1736510411558", + "package": "", + "balance_manager_id": "0xc8e754c3c0753664d995aa91e244ed89fcb3587b0a913c76f3f71f2521d2a1bc", + "asset": "deeb7a4662eec9f2f3def03fb937a663dddaa2e215b8078a284d026b7946c270::deep::DEEP", + "amount": "0", + "deposit": false + } +] diff --git a/crates/indexer/tests/snapshots/snapshot_tests__balances_indirect__balances.snap b/crates/indexer/tests/snapshots/snapshot_tests__balances_indirect__balances.snap new file mode 100644 index 000000000..e48b9bd61 --- /dev/null +++ b/crates/indexer/tests/snapshots/snapshot_tests__balances_indirect__balances.snap @@ -0,0 +1,19 @@ +--- +source: crates/indexer/tests/snapshot_tests.rs +expression: rows +--- +[ + { + "event_digest": "83FbjZQQrVQL8RbbX5mmZEgsc2AZ42YT95kUMsJjcDdn1", + "digest": "83FbjZQQrVQL8RbbX5mmZEgsc2AZ42YT95kUMsJjcDdn", + "sender": "0x43952a2cd77aafa5f627d597937c07733a8be6f167066dcd1523fe372fd8ecc0", + "checkpoint": "81855955", + "timestamp": "1970-01-01 00:00:00.000000", + "checkpoint_timestamp_ms": "1732103173508", + "package": "", + "balance_manager_id": "0x12c5d9f88a0c96d37ea3b866aaa0e6b998ac9444d384e7c7f0bdd321817f3161", + "asset": "f82dc05634970553615eef6112a1ac4fb7bf10272bf6cbe0f80ef44a6c489385::typus::TYPUS", + "amount": "29999999000000000", + "deposit": true + } +] diff --git a/crates/indexer/tests/snapshots/snapshot_tests__conditional_order_added__conditional_order_events.snap b/crates/indexer/tests/snapshots/snapshot_tests__conditional_order_added__conditional_order_events.snap new file mode 100644 index 000000000..221abca08 --- /dev/null +++ b/crates/indexer/tests/snapshots/snapshot_tests__conditional_order_added__conditional_order_events.snap @@ -0,0 +1,31 @@ +--- +source: crates/indexer/tests/snapshot_tests.rs +expression: rows +--- +[ + { + "event_digest": "HRj2fF9ifRA8kXipJy2g6y6UKgMFNeKvvZqfrKY2L8250", + "digest": "HRj2fF9ifRA8kXipJy2g6y6UKgMFNeKvvZqfrKY2L825", + "sender": "0x064d87c3da8b7201b18c05bfc3189eb817920b2d089b33e207d1d99dc5ce08e0", + "checkpoint": "234928955", + "timestamp": "1970-01-01 00:00:00.000000", + "checkpoint_timestamp_ms": "1768504212477", + "package": "0x97d9473771b01f77b0940c589484184b49f6444627ec121314fae6a6d36fb86b", + "event_type": "added", + "manager_id": "0x16d8a37cfcacb4436a68f91eaf5d8e7d0aa1482a1958f8a90588f8d348ca9fc4", + "pool_id": null, + "conditional_order_id": "1768504199749", + "trigger_below_price": false, + "trigger_price": "5000000", + "is_limit_order": true, + "client_order_id": "1768504199749", + "order_type": "0", + "self_matching_option": "0", + "price": "5000000", + "quantity": "1000000000", + "is_bid": false, + "pay_with_deep": true, + "expire_timestamp": "1844674407370955161", + "onchain_timestamp": "1768504212477" + } +] diff --git a/crates/indexer/tests/snapshots/snapshot_tests__conditional_order_cancelled__conditional_order_events.snap b/crates/indexer/tests/snapshots/snapshot_tests__conditional_order_cancelled__conditional_order_events.snap new file mode 100644 index 000000000..8983f946d --- /dev/null +++ b/crates/indexer/tests/snapshots/snapshot_tests__conditional_order_cancelled__conditional_order_events.snap @@ -0,0 +1,31 @@ +--- +source: crates/indexer/tests/snapshot_tests.rs +expression: rows +--- +[ + { + "event_digest": "5QcwuLcE7jmunStKgUSCrHPpAw1WC8B9XQLPph3jrKGn0", + "digest": "5QcwuLcE7jmunStKgUSCrHPpAw1WC8B9XQLPph3jrKGn", + "sender": "0x064d87c3da8b7201b18c05bfc3189eb817920b2d089b33e207d1d99dc5ce08e0", + "checkpoint": "234928968", + "timestamp": "1970-01-01 00:00:00.000000", + "checkpoint_timestamp_ms": "1768504216291", + "package": "0x97d9473771b01f77b0940c589484184b49f6444627ec121314fae6a6d36fb86b", + "event_type": "cancelled", + "manager_id": "0x16d8a37cfcacb4436a68f91eaf5d8e7d0aa1482a1958f8a90588f8d348ca9fc4", + "pool_id": null, + "conditional_order_id": "1768504199749", + "trigger_below_price": false, + "trigger_price": "5000000", + "is_limit_order": true, + "client_order_id": "1768504199749", + "order_type": "0", + "self_matching_option": "0", + "price": "5000000", + "quantity": "1000000000", + "is_bid": false, + "pay_with_deep": true, + "expire_timestamp": "1844674407370955161", + "onchain_timestamp": "1768504216186" + } +] diff --git a/crates/indexer/tests/snapshots/snapshot_tests__deep_burned__deep_burned.snap b/crates/indexer/tests/snapshots/snapshot_tests__deep_burned__deep_burned.snap new file mode 100644 index 000000000..6334b9aef --- /dev/null +++ b/crates/indexer/tests/snapshots/snapshot_tests__deep_burned__deep_burned.snap @@ -0,0 +1,149 @@ +--- +source: crates/indexer/tests/snapshot_tests.rs +expression: rows +--- +[ + { + "event_digest": "2WBXEufgyowDZR4f88aX3sh9qX2DPhn4DHSqdSi1ZyMQ0", + "digest": "2WBXEufgyowDZR4f88aX3sh9qX2DPhn4DHSqdSi1ZyMQ", + "sender": "0xbd1d25f49cc9b65f1e41d6c264ad0e065923de7ce6fd8b86d87d25c0a58742b9", + "checkpoint": "193585515", + "timestamp": "1970-01-01 00:00:00.000000", + "checkpoint_timestamp_ms": "1758758411676", + "package": "0xb29d83c26cdd2a64959263abbcfc4a6937f0c9fccaf98580ca56faded65be244", + "pool_id": "0xe05dafb5133bcffb8d59f4e12465dc0e9faeaa05e3e342a08fe135800e3e4407", + "burned_amount": "22824399055" + }, + { + "event_digest": "2WBXEufgyowDZR4f88aX3sh9qX2DPhn4DHSqdSi1ZyMQ1", + "digest": "2WBXEufgyowDZR4f88aX3sh9qX2DPhn4DHSqdSi1ZyMQ", + "sender": "0xbd1d25f49cc9b65f1e41d6c264ad0e065923de7ce6fd8b86d87d25c0a58742b9", + "checkpoint": "193585515", + "timestamp": "1970-01-01 00:00:00.000000", + "checkpoint_timestamp_ms": "1758758411676", + "package": "0xb29d83c26cdd2a64959263abbcfc4a6937f0c9fccaf98580ca56faded65be244", + "pool_id": "0x4e2ca3988246e1d50b9bf209abb9c1cbfec65bd95afdacc620a36c67bdb8452f", + "burned_amount": "0" + }, + { + "event_digest": "2WBXEufgyowDZR4f88aX3sh9qX2DPhn4DHSqdSi1ZyMQ2", + "digest": "2WBXEufgyowDZR4f88aX3sh9qX2DPhn4DHSqdSi1ZyMQ", + "sender": "0xbd1d25f49cc9b65f1e41d6c264ad0e065923de7ce6fd8b86d87d25c0a58742b9", + "checkpoint": "193585515", + "timestamp": "1970-01-01 00:00:00.000000", + "checkpoint_timestamp_ms": "1758758411676", + "package": "0xb29d83c26cdd2a64959263abbcfc4a6937f0c9fccaf98580ca56faded65be244", + "pool_id": "0xa0b9ebefb38c963fd115f52d71fa64501b79d1adcb5270563f92ce0442376545", + "burned_amount": "0" + }, + { + "event_digest": "2WBXEufgyowDZR4f88aX3sh9qX2DPhn4DHSqdSi1ZyMQ3", + "digest": "2WBXEufgyowDZR4f88aX3sh9qX2DPhn4DHSqdSi1ZyMQ", + "sender": "0xbd1d25f49cc9b65f1e41d6c264ad0e065923de7ce6fd8b86d87d25c0a58742b9", + "checkpoint": "193585515", + "timestamp": "1970-01-01 00:00:00.000000", + "checkpoint_timestamp_ms": "1758758411676", + "package": "0xb29d83c26cdd2a64959263abbcfc4a6937f0c9fccaf98580ca56faded65be244", + "pool_id": "0x1109352b9112717bd2a7c3eb9a416fff1ba6951760f5bdd5424cf5e4e5b3e65c", + "burned_amount": "0" + }, + { + "event_digest": "2WBXEufgyowDZR4f88aX3sh9qX2DPhn4DHSqdSi1ZyMQ4", + "digest": "2WBXEufgyowDZR4f88aX3sh9qX2DPhn4DHSqdSi1ZyMQ", + "sender": "0xbd1d25f49cc9b65f1e41d6c264ad0e065923de7ce6fd8b86d87d25c0a58742b9", + "checkpoint": "193585515", + "timestamp": "1970-01-01 00:00:00.000000", + "checkpoint_timestamp_ms": "1758758411676", + "package": "0xb29d83c26cdd2a64959263abbcfc4a6937f0c9fccaf98580ca56faded65be244", + "pool_id": "0x0c0fdd4008740d81a8a7d4281322aee71a1b62c449eb5b142656753d89ebc060", + "burned_amount": "32720302" + }, + { + "event_digest": "2WBXEufgyowDZR4f88aX3sh9qX2DPhn4DHSqdSi1ZyMQ5", + "digest": "2WBXEufgyowDZR4f88aX3sh9qX2DPhn4DHSqdSi1ZyMQ", + "sender": "0xbd1d25f49cc9b65f1e41d6c264ad0e065923de7ce6fd8b86d87d25c0a58742b9", + "checkpoint": "193585515", + "timestamp": "1970-01-01 00:00:00.000000", + "checkpoint_timestamp_ms": "1758758411676", + "package": "0xb29d83c26cdd2a64959263abbcfc4a6937f0c9fccaf98580ca56faded65be244", + "pool_id": "0x27c4fdb3b846aa3ae4a65ef5127a309aa3c1f466671471a806d8912a18b253e8", + "burned_amount": "36767718" + }, + { + "event_digest": "2WBXEufgyowDZR4f88aX3sh9qX2DPhn4DHSqdSi1ZyMQ6", + "digest": "2WBXEufgyowDZR4f88aX3sh9qX2DPhn4DHSqdSi1ZyMQ", + "sender": "0xbd1d25f49cc9b65f1e41d6c264ad0e065923de7ce6fd8b86d87d25c0a58742b9", + "checkpoint": "193585515", + "timestamp": "1970-01-01 00:00:00.000000", + "checkpoint_timestamp_ms": "1758758411676", + "package": "0xb29d83c26cdd2a64959263abbcfc4a6937f0c9fccaf98580ca56faded65be244", + "pool_id": "0xe8e56f377ab5a261449b92ac42c8ddaacd5671e9fec2179d7933dd1a91200eec", + "burned_amount": "0" + }, + { + "event_digest": "2WBXEufgyowDZR4f88aX3sh9qX2DPhn4DHSqdSi1ZyMQ7", + "digest": "2WBXEufgyowDZR4f88aX3sh9qX2DPhn4DHSqdSi1ZyMQ", + "sender": "0xbd1d25f49cc9b65f1e41d6c264ad0e065923de7ce6fd8b86d87d25c0a58742b9", + "checkpoint": "193585515", + "timestamp": "1970-01-01 00:00:00.000000", + "checkpoint_timestamp_ms": "1758758411676", + "package": "0xb29d83c26cdd2a64959263abbcfc4a6937f0c9fccaf98580ca56faded65be244", + "pool_id": "0x183df694ebc852a5f90a959f0f563b82ac9691e42357e9a9fe961d71a1b809c8", + "burned_amount": "0" + }, + { + "event_digest": "2WBXEufgyowDZR4f88aX3sh9qX2DPhn4DHSqdSi1ZyMQ8", + "digest": "2WBXEufgyowDZR4f88aX3sh9qX2DPhn4DHSqdSi1ZyMQ", + "sender": "0xbd1d25f49cc9b65f1e41d6c264ad0e065923de7ce6fd8b86d87d25c0a58742b9", + "checkpoint": "193585515", + "timestamp": "1970-01-01 00:00:00.000000", + "checkpoint_timestamp_ms": "1758758411676", + "package": "0xb29d83c26cdd2a64959263abbcfc4a6937f0c9fccaf98580ca56faded65be244", + "pool_id": "0x5661fc7f88fbeb8cb881150a810758cf13700bb4e1f31274a244581b37c303c3", + "burned_amount": "0" + }, + { + "event_digest": "2WBXEufgyowDZR4f88aX3sh9qX2DPhn4DHSqdSi1ZyMQ9", + "digest": "2WBXEufgyowDZR4f88aX3sh9qX2DPhn4DHSqdSi1ZyMQ", + "sender": "0xbd1d25f49cc9b65f1e41d6c264ad0e065923de7ce6fd8b86d87d25c0a58742b9", + "checkpoint": "193585515", + "timestamp": "1970-01-01 00:00:00.000000", + "checkpoint_timestamp_ms": "1758758411676", + "package": "0xb29d83c26cdd2a64959263abbcfc4a6937f0c9fccaf98580ca56faded65be244", + "pool_id": "0x126865a0197d6ab44bfd15fd052da6db92fd2eb831ff9663451bbfa1219e2af2", + "burned_amount": "0" + }, + { + "event_digest": "2WBXEufgyowDZR4f88aX3sh9qX2DPhn4DHSqdSi1ZyMQ10", + "digest": "2WBXEufgyowDZR4f88aX3sh9qX2DPhn4DHSqdSi1ZyMQ", + "sender": "0xbd1d25f49cc9b65f1e41d6c264ad0e065923de7ce6fd8b86d87d25c0a58742b9", + "checkpoint": "193585515", + "timestamp": "1970-01-01 00:00:00.000000", + "checkpoint_timestamp_ms": "1758758411676", + "package": "0xb29d83c26cdd2a64959263abbcfc4a6937f0c9fccaf98580ca56faded65be244", + "pool_id": "0x1fe7b99c28ded39774f37327b509d58e2be7fff94899c06d22b407496a6fa990", + "burned_amount": "0" + }, + { + "event_digest": "2WBXEufgyowDZR4f88aX3sh9qX2DPhn4DHSqdSi1ZyMQ11", + "digest": "2WBXEufgyowDZR4f88aX3sh9qX2DPhn4DHSqdSi1ZyMQ", + "sender": "0xbd1d25f49cc9b65f1e41d6c264ad0e065923de7ce6fd8b86d87d25c0a58742b9", + "checkpoint": "193585515", + "timestamp": "1970-01-01 00:00:00.000000", + "checkpoint_timestamp_ms": "1758758411676", + "package": "0xb29d83c26cdd2a64959263abbcfc4a6937f0c9fccaf98580ca56faded65be244", + "pool_id": "0x56a1c985c1f1123181d6b881714793689321ba24301b3585eec427436eb1c76d", + "burned_amount": "1010611241" + }, + { + "event_digest": "2WBXEufgyowDZR4f88aX3sh9qX2DPhn4DHSqdSi1ZyMQ12", + "digest": "2WBXEufgyowDZR4f88aX3sh9qX2DPhn4DHSqdSi1ZyMQ", + "sender": "0xbd1d25f49cc9b65f1e41d6c264ad0e065923de7ce6fd8b86d87d25c0a58742b9", + "checkpoint": "193585515", + "timestamp": "1970-01-01 00:00:00.000000", + "checkpoint_timestamp_ms": "1758758411676", + "package": "0xb29d83c26cdd2a64959263abbcfc4a6937f0c9fccaf98580ca56faded65be244", + "pool_id": "0x81f5339934c83ea19dd6bcc75c52e83509629a5f71d3257428c2ce47cc94d08b", + "burned_amount": "607760763" + } +] diff --git a/crates/indexer/tests/snapshots/snapshot_tests__deepbook_pool_registered__deepbook_pool_registered.snap b/crates/indexer/tests/snapshots/snapshot_tests__deepbook_pool_registered__deepbook_pool_registered.snap new file mode 100644 index 000000000..ece1d0d5e --- /dev/null +++ b/crates/indexer/tests/snapshots/snapshot_tests__deepbook_pool_registered__deepbook_pool_registered.snap @@ -0,0 +1,33 @@ +--- +source: crates/indexer/tests/snapshot_tests.rs +expression: rows +--- +[ + { + "event_digest": "Bmv3Ko4iMAj5JYBdbZg74Ey1QKb8z61hc96fr3uJJHf3", + "digest": "Bmv3Ko4iMAj5JYBdbZg74Ey1QKb8z61hc96fr3uJJHf", + "sender": "0xb3d277c50f7b846a5f609a8d13428ae482b5826bb98437997373f3a0d60d280e", + "checkpoint": "273618562", + "timestamp": "1970-01-01 00:00:00.000000", + "checkpoint_timestamp_ms": "1765396435956", + "package": "0x21473617f3565d704aa67be73ea41243e9e34a42d434c31f8182c67ba01ccf49", + "pool_id": "0x0dce0aa771074eb83d1f4a29d48be8248d4d2190976a5241f66b43ec18fa34de", + "onchain_timestamp": "1765396435821", + "config_json": { + "enabled": false, + "risk_ratios": { + "min_borrow_risk_ratio": 1249900000, + "liquidation_risk_ratio": 1100000000, + "min_withdraw_risk_ratio": 2000000000, + "target_liquidation_risk_ratio": 1250000000 + }, + "extra_fields": { + "contents": [] + }, + "base_margin_pool_id": "0xf3440b4aafcc8b12fc4b242e9590c52873b8238a0d0e52fbf9dae61d2970796a", + "quote_margin_pool_id": "0xf08568da93834e1ee04f09902ac7b1e78d3fdf113ab4d2106c7265e95318b14d", + "pool_liquidation_reward": 20000000, + "user_liquidation_reward": 30000000 + } + } +] diff --git a/crates/indexer/tests/snapshots/snapshot_tests__deepbook_pool_updated__deepbook_pool_updated.snap b/crates/indexer/tests/snapshots/snapshot_tests__deepbook_pool_updated__deepbook_pool_updated.snap new file mode 100644 index 000000000..41807d3c4 --- /dev/null +++ b/crates/indexer/tests/snapshots/snapshot_tests__deepbook_pool_updated__deepbook_pool_updated.snap @@ -0,0 +1,20 @@ +--- +source: crates/indexer/tests/snapshot_tests.rs +expression: rows +--- +[ + { + "event_digest": "AtHxXU7fhG6KTTEDCzQ9rZ96aFWWQ7NWuSwWkK4G4hHM0", + "digest": "AtHxXU7fhG6KTTEDCzQ9rZ96aFWWQ7NWuSwWkK4G4hHM", + "sender": "0xb3d277c50f7b846a5f609a8d13428ae482b5826bb98437997373f3a0d60d280e", + "checkpoint": "273561675", + "timestamp": "1970-01-01 00:00:00.000000", + "checkpoint_timestamp_ms": "1765382811234", + "package": "0xb8620c24c9ea1a4a41e79613d2b3d1d93648d1bb6f6b789a7c8f261c94110e4b", + "margin_pool_id": "0xf3440b4aafcc8b12fc4b242e9590c52873b8238a0d0e52fbf9dae61d2970796a", + "deepbook_pool_id": "0x0dce0aa771074eb83d1f4a29d48be8248d4d2190976a5241f66b43ec18fa34de", + "pool_cap_id": "0xc6d58ea065bc8bf85cc4732a7b9e992b21c66147d89aa511d33c4df6f035a47b", + "enabled": true, + "onchain_timestamp": "1765382811170" + } +] diff --git a/crates/indexer/tests/snapshots/snapshot_tests__deepbook_pool_updated_registry__deepbook_pool_updated_registry.snap b/crates/indexer/tests/snapshots/snapshot_tests__deepbook_pool_updated_registry__deepbook_pool_updated_registry.snap new file mode 100644 index 000000000..d5a5cb592 --- /dev/null +++ b/crates/indexer/tests/snapshots/snapshot_tests__deepbook_pool_updated_registry__deepbook_pool_updated_registry.snap @@ -0,0 +1,18 @@ +--- +source: crates/indexer/tests/snapshot_tests.rs +expression: rows +--- +[ + { + "event_digest": "Bmv3Ko4iMAj5JYBdbZg74Ey1QKb8z61hc96fr3uJJHf4", + "digest": "Bmv3Ko4iMAj5JYBdbZg74Ey1QKb8z61hc96fr3uJJHf", + "sender": "0xb3d277c50f7b846a5f609a8d13428ae482b5826bb98437997373f3a0d60d280e", + "checkpoint": "273618562", + "timestamp": "1970-01-01 00:00:00.000000", + "checkpoint_timestamp_ms": "1765396435956", + "package": "0x21473617f3565d704aa67be73ea41243e9e34a42d434c31f8182c67ba01ccf49", + "pool_id": "0x0dce0aa771074eb83d1f4a29d48be8248d4d2190976a5241f66b43ec18fa34de", + "enabled": true, + "onchain_timestamp": "1765396435821" + } +] diff --git a/crates/indexer/tests/snapshots/snapshot_tests__deposit_collateral__collateral_events.snap b/crates/indexer/tests/snapshots/snapshot_tests__deposit_collateral__collateral_events.snap new file mode 100644 index 000000000..5f7065b1f --- /dev/null +++ b/crates/indexer/tests/snapshots/snapshot_tests__deposit_collateral__collateral_events.snap @@ -0,0 +1,31 @@ +--- +source: crates/indexer/tests/snapshot_tests.rs +expression: rows +--- +[ + { + "event_digest": "GSNpevf2UcTeq3ACPMGRsvLFRRGB9w2H4KB9BR1cEYcQ1", + "digest": "GSNpevf2UcTeq3ACPMGRsvLFRRGB9w2H4KB9BR1cEYcQ", + "sender": "0x064d87c3da8b7201b18c05bfc3189eb817920b2d089b33e207d1d99dc5ce08e0", + "checkpoint": "234918188", + "timestamp": "1970-01-01 00:00:00.000000", + "checkpoint_timestamp_ms": "1768501353090", + "package": "", + "event_type": "deposit", + "margin_manager_id": "0x16d8a37cfcacb4436a68f91eaf5d8e7d0aa1482a1958f8a90588f8d348ca9fc4", + "amount": "10000000", + "asset_type": "dba34672e30cb065b1f93e3ab55318768fd6fef66c15942c9f7cb846e2f900e7::usdc::USDC", + "pyth_decimals": "8", + "pyth_price": "99975279", + "withdraw_base_asset": null, + "base_pyth_decimals": null, + "base_pyth_price": null, + "quote_pyth_decimals": null, + "quote_pyth_price": null, + "remaining_base_asset": null, + "remaining_quote_asset": null, + "remaining_base_debt": null, + "remaining_quote_debt": null, + "onchain_timestamp": "1768501352962" + } +] diff --git a/crates/indexer/tests/snapshots/snapshot_tests__flash_loans__flashloans.snap b/crates/indexer/tests/snapshots/snapshot_tests__flash_loans__flashloans.snap new file mode 100644 index 000000000..608672b8d --- /dev/null +++ b/crates/indexer/tests/snapshots/snapshot_tests__flash_loans__flashloans.snap @@ -0,0 +1,32 @@ +--- +source: crates/indexer/tests/snapshot_tests.rs +expression: rows +--- +[ + { + "event_digest": "DzuGc5r1R6yocW2uNHh5ULbggTJLSqXdwtCFydtf8xfU0", + "digest": "DzuGc5r1R6yocW2uNHh5ULbggTJLSqXdwtCFydtf8xfU", + "sender": "0xfdb4ab707ca6c6c785ff4826d55862009d24902352a73ff0d79d8e0faaa9e7e8", + "checkpoint": "100001465", + "timestamp": "1970-01-01 00:00:00.000000", + "checkpoint_timestamp_ms": "1736510724877", + "package": "0x2c8d603bc51326b8c13cef9dd07031a408a48dddb541963357661df5d3204809", + "borrow": true, + "pool_id": "0xe05dafb5133bcffb8d59f4e12465dc0e9faeaa05e3e342a08fe135800e3e4407", + "borrow_quantity": "22976181", + "type_name": "dba34672e30cb065b1f93e3ab55318768fd6fef66c15942c9f7cb846e2f900e7::usdc::USDC" + }, + { + "event_digest": "8pwFqN1gh7Q59ZWiwvEXsjYzZF2G21gcPMbAaucjPD210", + "digest": "8pwFqN1gh7Q59ZWiwvEXsjYzZF2G21gcPMbAaucjPD21", + "sender": "0x6e50a6963c20b1cc12d6abde56148117f523a8d679496b070d88a8c35641f68d", + "checkpoint": "100001465", + "timestamp": "1970-01-01 00:00:00.000000", + "checkpoint_timestamp_ms": "1736510724877", + "package": "0x2c8d603bc51326b8c13cef9dd07031a408a48dddb541963357661df5d3204809", + "borrow": true, + "pool_id": "0xe05dafb5133bcffb8d59f4e12465dc0e9faeaa05e3e342a08fe135800e3e4407", + "borrow_quantity": "10348560026", + "type_name": "0000000000000000000000000000000000000000000000000000000000000002::sui::SUI" + } +] diff --git a/crates/indexer/tests/snapshots/snapshot_tests__liquidation__liquidation.snap b/crates/indexer/tests/snapshots/snapshot_tests__liquidation__liquidation.snap new file mode 100644 index 000000000..dc58fbc10 --- /dev/null +++ b/crates/indexer/tests/snapshots/snapshot_tests__liquidation__liquidation.snap @@ -0,0 +1,30 @@ +--- +source: crates/indexer/tests/snapshot_tests.rs +expression: rows +--- +[ + { + "event_digest": "4qXMCRFj7WnEjQD3Z4KTsKeMEbTqJqvaR1BEfoJYUhvb5", + "digest": "4qXMCRFj7WnEjQD3Z4KTsKeMEbTqJqvaR1BEfoJYUhvb", + "sender": "0xb3d277c50f7b846a5f609a8d13428ae482b5826bb98437997373f3a0d60d280e", + "checkpoint": "275761458", + "timestamp": "1970-01-01 00:00:00.000000", + "checkpoint_timestamp_ms": "1765914264190", + "package": "0x21473617f3565d704aa67be73ea41243e9e34a42d434c31f8182c67ba01ccf49", + "margin_manager_id": "0x70a5f28a2400fca515adce1262da0b45ba8f3d1e48f1f2a9568aa29642b5c104", + "margin_pool_id": "0xcdbbe6a72e639b647296788e2e4b1cac5cea4246028ba388ba1332ff9a382eea", + "liquidation_amount": "8823529392", + "pool_reward": "176470607", + "pool_default": "0", + "risk_ratio": "34665470205", + "onchain_timestamp": "1765914264017", + "remaining_base_asset": "20735294119", + "remaining_quote_asset": "1000000000", + "remaining_base_debt": "11185504617", + "remaining_quote_debt": "0", + "base_pyth_price": "150667253", + "base_pyth_decimals": "8", + "quote_pyth_price": "99986190", + "quote_pyth_decimals": "8" + } +] diff --git a/crates/indexer/tests/snapshots/snapshot_tests__loan_borrowed__loan_borrowed.snap b/crates/indexer/tests/snapshots/snapshot_tests__loan_borrowed__loan_borrowed.snap new file mode 100644 index 000000000..f1c2d885e --- /dev/null +++ b/crates/indexer/tests/snapshots/snapshot_tests__loan_borrowed__loan_borrowed.snap @@ -0,0 +1,20 @@ +--- +source: crates/indexer/tests/snapshot_tests.rs +expression: rows +--- +[ + { + "event_digest": "DexFS6kK2PjmFcFB8dXt4b3jegjijLu7oRyibL57ZZhP4", + "digest": "DexFS6kK2PjmFcFB8dXt4b3jegjijLu7oRyibL57ZZhP", + "sender": "0xb3d277c50f7b846a5f609a8d13428ae482b5826bb98437997373f3a0d60d280e", + "checkpoint": "273619256", + "timestamp": "1970-01-01 00:00:00.000000", + "checkpoint_timestamp_ms": "1765396605032", + "package": "0x21473617f3565d704aa67be73ea41243e9e34a42d434c31f8182c67ba01ccf49", + "margin_manager_id": "0x8f43e4dc799eb497c99bc88f002cceb0c03225e3cc567433053ee3fd692b720d", + "margin_pool_id": "0xf3440b4aafcc8b12fc4b242e9590c52873b8238a0d0e52fbf9dae61d2970796a", + "loan_amount": "10000000", + "onchain_timestamp": "1765396604841", + "loan_shares": "10000000" + } +] diff --git a/crates/indexer/tests/snapshots/snapshot_tests__maintainer_cap_updated__maintainer_cap_updated.snap b/crates/indexer/tests/snapshots/snapshot_tests__maintainer_cap_updated__maintainer_cap_updated.snap new file mode 100644 index 000000000..8a3ade2b5 --- /dev/null +++ b/crates/indexer/tests/snapshots/snapshot_tests__maintainer_cap_updated__maintainer_cap_updated.snap @@ -0,0 +1,18 @@ +--- +source: crates/indexer/tests/snapshot_tests.rs +expression: rows +--- +[ + { + "event_digest": "CW4mwSsMZnYjHKShk62UoeuYDSnmScj5fXBwqKfHBM1g0", + "digest": "CW4mwSsMZnYjHKShk62UoeuYDSnmScj5fXBwqKfHBM1g", + "sender": "0xb3d277c50f7b846a5f609a8d13428ae482b5826bb98437997373f3a0d60d280e", + "checkpoint": "273301160", + "timestamp": "1970-01-01 00:00:00.000000", + "checkpoint_timestamp_ms": "1765319724366", + "package": "0xb8620c24c9ea1a4a41e79613d2b3d1d93648d1bb6f6b789a7c8f261c94110e4b", + "maintainer_cap_id": "0xc4bc2b7a2b1f317b8a664294c5cc8501520289c3a6e9b9cc04eef668415b59bf", + "allowed": true, + "onchain_timestamp": "1765319724235" + } +] diff --git a/crates/indexer/tests/snapshots/snapshot_tests__margin_manager_created__margin_manager_created.snap b/crates/indexer/tests/snapshots/snapshot_tests__margin_manager_created__margin_manager_created.snap new file mode 100644 index 000000000..da3a41f3c --- /dev/null +++ b/crates/indexer/tests/snapshots/snapshot_tests__margin_manager_created__margin_manager_created.snap @@ -0,0 +1,20 @@ +--- +source: crates/indexer/tests/snapshot_tests.rs +expression: rows +--- +[ + { + "event_digest": "7QTFma4AaVz5N9TTfNJqNqpiyDsd7md43iyv63frJ75Z4", + "digest": "7QTFma4AaVz5N9TTfNJqNqpiyDsd7md43iyv63frJ75Z", + "sender": "0xb3d277c50f7b846a5f609a8d13428ae482b5826bb98437997373f3a0d60d280e", + "checkpoint": "273618704", + "timestamp": "1970-01-01 00:00:00.000000", + "checkpoint_timestamp_ms": "1765396470673", + "package": "0x21473617f3565d704aa67be73ea41243e9e34a42d434c31f8182c67ba01ccf49", + "margin_manager_id": "0x8f43e4dc799eb497c99bc88f002cceb0c03225e3cc567433053ee3fd692b720d", + "balance_manager_id": "0xfdfb55bedf7f1724066c73fc93dd69dee36e43d416ee356e881c37859539488f", + "owner": "0xb3d277c50f7b846a5f609a8d13428ae482b5826bb98437997373f3a0d60d280e", + "onchain_timestamp": "1765396470673", + "deepbook_pool_id": "0x0dce0aa771074eb83d1f4a29d48be8248d4d2190976a5241f66b43ec18fa34de" + } +] diff --git a/crates/indexer/tests/snapshots/snapshot_tests__margin_pool_created__margin_pool_created.snap b/crates/indexer/tests/snapshots/snapshot_tests__margin_pool_created__margin_pool_created.snap new file mode 100644 index 000000000..64671018b --- /dev/null +++ b/crates/indexer/tests/snapshots/snapshot_tests__margin_pool_created__margin_pool_created.snap @@ -0,0 +1,39 @@ +--- +source: crates/indexer/tests/snapshot_tests.rs +expression: rows +--- +[ + { + "event_digest": "826yfHmNs86XEcG52fZvF8wjNJR1cjKpJUttBq6cpX4w0", + "digest": "826yfHmNs86XEcG52fZvF8wjNJR1cjKpJUttBq6cpX4w", + "sender": "0xb3d277c50f7b846a5f609a8d13428ae482b5826bb98437997373f3a0d60d280e", + "checkpoint": "273561287", + "timestamp": "1970-01-01 00:00:00.000000", + "checkpoint_timestamp_ms": "1765382716273", + "package": "0xb8620c24c9ea1a4a41e79613d2b3d1d93648d1bb6f6b789a7c8f261c94110e4b", + "margin_pool_id": "0xf3440b4aafcc8b12fc4b242e9590c52873b8238a0d0e52fbf9dae61d2970796a", + "maintainer_cap_id": "0xc4bc2b7a2b1f317b8a664294c5cc8501520289c3a6e9b9cc04eef668415b59bf", + "asset_type": "6502dae813dbe5e42643c119a6450a518481f03063febc7e20238e43b6ea9e86::dbtc::DBTC", + "config_json": { + "extra_fields": { + "contents": [] + }, + "interest_config": { + "base_rate": 0, + "base_slope": 6250000, + "excess_slope": 4000000000, + "optimal_utilization": 800000000 + }, + "margin_pool_config": { + "min_borrow": 1000, + "supply_cap": 5000000000, + "protocol_spread": 100000000, + "rate_limit_enabled": false, + "rate_limit_capacity": 500000000, + "max_utilization_rate": 900000000, + "rate_limit_refill_rate_per_ms": 5 + } + }, + "onchain_timestamp": "1765382716212" + } +] diff --git a/crates/indexer/tests/snapshots/snapshot_tests__order_fill__order_fills.snap b/crates/indexer/tests/snapshots/snapshot_tests__order_fill__order_fills.snap new file mode 100644 index 000000000..ed3094385 --- /dev/null +++ b/crates/indexer/tests/snapshots/snapshot_tests__order_fill__order_fills.snap @@ -0,0 +1,106 @@ +--- +source: crates/indexer/tests/snapshot_tests.rs +expression: rows +--- +[ + { + "event_digest": "G5vNm6fofF2QJeNXXHQoXRx1pYD8mPA4RfCWZCbooxKP4", + "digest": "G5vNm6fofF2QJeNXXHQoXRx1pYD8mPA4RfCWZCbooxKP", + "sender": "0xe2582e9e38ac48d9e486863338b24f376464abc445785ce0386e76bcf5c04b9f", + "checkpoint": "100000337", + "timestamp": "1970-01-01 00:00:00.000000", + "checkpoint_timestamp_ms": "1736510450255", + "package": "0x9fdcb0fcb129f1f6282fe0bda7187de85cda7dd4d619c1cd31213c04e75fa0f2", + "pool_id": "0xe05dafb5133bcffb8d59f4e12465dc0e9faeaa05e3e342a08fe135800e3e4407", + "maker_order_id": "93727925085262305464784057", + "taker_order_id": "170141183460469231750134047789599572851", + "maker_client_order_id": "6631680315252210715", + "taker_client_order_id": "0", + "price": "5081000", + "taker_fee": "3314135", + "taker_fee_is_deep": true, + "maker_fee": "662827", + "maker_fee_is_deep": true, + "taker_is_bid": false, + "base_quantity": "100000000000", + "quote_quantity": "508100000", + "maker_balance_manager_id": "0x344c2734b1d211bd15212bfb7847c66a3b18803f3f5ab00f5ff6f87b6fe6d27d", + "taker_balance_manager_id": "0xecd5aa8e0529cd294bd61cfc6eb9ec76a46383f701cd71ee855595a63002444b", + "onchain_timestamp": "1736510450157" + }, + { + "event_digest": "G5vNm6fofF2QJeNXXHQoXRx1pYD8mPA4RfCWZCbooxKP14", + "digest": "G5vNm6fofF2QJeNXXHQoXRx1pYD8mPA4RfCWZCbooxKP", + "sender": "0xe2582e9e38ac48d9e486863338b24f376464abc445785ce0386e76bcf5c04b9f", + "checkpoint": "100000337", + "timestamp": "1970-01-01 00:00:00.000000", + "checkpoint_timestamp_ms": "1736510450255", + "package": "0x9fdcb0fcb129f1f6282fe0bda7187de85cda7dd4d619c1cd31213c04e75fa0f2", + "pool_id": "0xf948981b806057580f91622417534f491da5f61aeaf33d0ed8e69fd5691c95ce", + "maker_order_id": "170141183463330321737519655171529707732", + "taker_order_id": "170141183460469231731687303715880064919", + "maker_client_order_id": "123456789", + "taker_client_order_id": "0", + "price": "155100000", + "taker_fee": "0", + "taker_fee_is_deep": false, + "maker_fee": "0", + "maker_fee_is_deep": true, + "taker_is_bid": true, + "base_quantity": "1890000000", + "quote_quantity": "293139000", + "maker_balance_manager_id": "0x7feeb77a38c26ad4013369c103674f18496efaee6d417f14b3c1c97a3733de5b", + "taker_balance_manager_id": "0x7479dc44f83c04f8292abec200090f6b526e0dfae93703bfdb0e5ecb880a7657", + "onchain_timestamp": "1736510450157" + }, + { + "event_digest": "G5vNm6fofF2QJeNXXHQoXRx1pYD8mPA4RfCWZCbooxKP15", + "digest": "G5vNm6fofF2QJeNXXHQoXRx1pYD8mPA4RfCWZCbooxKP", + "sender": "0xe2582e9e38ac48d9e486863338b24f376464abc445785ce0386e76bcf5c04b9f", + "checkpoint": "100000337", + "timestamp": "1970-01-01 00:00:00.000000", + "checkpoint_timestamp_ms": "1736510450255", + "package": "0x9fdcb0fcb129f1f6282fe0bda7187de85cda7dd4d619c1cd31213c04e75fa0f2", + "pool_id": "0xf948981b806057580f91622417534f491da5f61aeaf33d0ed8e69fd5691c95ce", + "maker_order_id": "170141183463333457684012185795304427729", + "taker_order_id": "170141183460469231731687303715880064919", + "maker_client_order_id": "8092477148216240135", + "taker_client_order_id": "0", + "price": "155270000", + "taker_fee": "0", + "taker_fee_is_deep": false, + "maker_fee": "0", + "maker_fee_is_deep": true, + "taker_is_bid": true, + "base_quantity": "640000000", + "quote_quantity": "99372800", + "maker_balance_manager_id": "0x344c2734b1d211bd15212bfb7847c66a3b18803f3f5ab00f5ff6f87b6fe6d27d", + "taker_balance_manager_id": "0x7479dc44f83c04f8292abec200090f6b526e0dfae93703bfdb0e5ecb880a7657", + "onchain_timestamp": "1736510450157" + }, + { + "event_digest": "G5vNm6fofF2QJeNXXHQoXRx1pYD8mPA4RfCWZCbooxKP16", + "digest": "G5vNm6fofF2QJeNXXHQoXRx1pYD8mPA4RfCWZCbooxKP", + "sender": "0xe2582e9e38ac48d9e486863338b24f376464abc445785ce0386e76bcf5c04b9f", + "checkpoint": "100000337", + "timestamp": "1970-01-01 00:00:00.000000", + "checkpoint_timestamp_ms": "1736510450255", + "package": "0x9fdcb0fcb129f1f6282fe0bda7187de85cda7dd4d619c1cd31213c04e75fa0f2", + "pool_id": "0xf948981b806057580f91622417534f491da5f61aeaf33d0ed8e69fd5691c95ce", + "maker_order_id": "170141183463349506351356313105210347727", + "taker_order_id": "170141183460469231731687303715880064919", + "maker_client_order_id": "4776976116944917494", + "taker_client_order_id": "0", + "price": "156140000", + "taker_fee": "0", + "taker_fee_is_deep": false, + "maker_fee": "0", + "maker_fee_is_deep": true, + "taker_is_bid": true, + "base_quantity": "740000000", + "quote_quantity": "115543600", + "maker_balance_manager_id": "0x344c2734b1d211bd15212bfb7847c66a3b18803f3f5ab00f5ff6f87b6fe6d27d", + "taker_balance_manager_id": "0x7479dc44f83c04f8292abec200090f6b526e0dfae93703bfdb0e5ecb880a7657", + "onchain_timestamp": "1736510450157" + } +] diff --git a/crates/indexer/tests/snapshots/snapshot_tests__order_update__order_updates.snap b/crates/indexer/tests/snapshots/snapshot_tests__order_update__order_updates.snap new file mode 100644 index 000000000..3464fdd59 --- /dev/null +++ b/crates/indexer/tests/snapshots/snapshot_tests__order_update__order_updates.snap @@ -0,0 +1,111 @@ +--- +source: crates/indexer/tests/snapshot_tests.rs +expression: rows +--- +[ + { + "event_digest": "4c3YE3wdiiU4HGjfy7VKbLfMbGbdNMrymxsBU5hF2EZc0", + "digest": "4c3YE3wdiiU4HGjfy7VKbLfMbGbdNMrymxsBU5hF2EZc", + "sender": "0xcde6dbe01902be1f200ff03dbbd149e586847be8cee15235f82750d9b06c0e04", + "checkpoint": "100000017", + "timestamp": "1970-01-01 00:00:00.000000", + "checkpoint_timestamp_ms": "1736510372652", + "package": "0x2c8d603bc51326b8c13cef9dd07031a408a48dddb541963357661df5d3204809", + "status": "Placed", + "pool_id": "0xf948981b806057580f91622417534f491da5f61aeaf33d0ed8e69fd5691c95ce", + "order_id": "2855002598734771377313830840", + "client_order_id": "2696363258462237378", + "price": "154770000", + "is_bid": true, + "original_quantity": "650000000", + "quantity": "650000000", + "filled_quantity": "0", + "onchain_timestamp": "1736510372539", + "balance_manager_id": "0x344c2734b1d211bd15212bfb7847c66a3b18803f3f5ab00f5ff6f87b6fe6d27d", + "trader": "0xcde6dbe01902be1f200ff03dbbd149e586847be8cee15235f82750d9b06c0e04" + }, + { + "event_digest": "4c3YE3wdiiU4HGjfy7VKbLfMbGbdNMrymxsBU5hF2EZc2", + "digest": "4c3YE3wdiiU4HGjfy7VKbLfMbGbdNMrymxsBU5hF2EZc", + "sender": "0xcde6dbe01902be1f200ff03dbbd149e586847be8cee15235f82750d9b06c0e04", + "checkpoint": "100000017", + "timestamp": "1970-01-01 00:00:00.000000", + "checkpoint_timestamp_ms": "1736510372652", + "package": "0x2c8d603bc51326b8c13cef9dd07031a408a48dddb541963357661df5d3204809", + "status": "Placed", + "pool_id": "0xf948981b806057580f91622417534f491da5f61aeaf33d0ed8e69fd5691c95ce", + "order_id": "170141183463330137270078918076013547703", + "client_order_id": "6254035893388212517", + "price": "155090000", + "is_bid": false, + "original_quantity": "640000000", + "quantity": "640000000", + "filled_quantity": "0", + "onchain_timestamp": "1736510372539", + "balance_manager_id": "0x344c2734b1d211bd15212bfb7847c66a3b18803f3f5ab00f5ff6f87b6fe6d27d", + "trader": "0xcde6dbe01902be1f200ff03dbbd149e586847be8cee15235f82750d9b06c0e04" + }, + { + "event_digest": "4c3YE3wdiiU4HGjfy7VKbLfMbGbdNMrymxsBU5hF2EZc4", + "digest": "4c3YE3wdiiU4HGjfy7VKbLfMbGbdNMrymxsBU5hF2EZc", + "sender": "0xcde6dbe01902be1f200ff03dbbd149e586847be8cee15235f82750d9b06c0e04", + "checkpoint": "100000017", + "timestamp": "1970-01-01 00:00:00.000000", + "checkpoint_timestamp_ms": "1736510372652", + "package": "0x2c8d603bc51326b8c13cef9dd07031a408a48dddb541963357661df5d3204809", + "status": "Placed", + "pool_id": "0xf948981b806057580f91622417534f491da5f61aeaf33d0ed8e69fd5691c95ce", + "order_id": "170141183463353195700171055015533547704", + "client_order_id": "2259069948176513753", + "price": "156340000", + "is_bid": false, + "original_quantity": "63960000000", + "quantity": "63960000000", + "filled_quantity": "0", + "onchain_timestamp": "1736510372539", + "balance_manager_id": "0x344c2734b1d211bd15212bfb7847c66a3b18803f3f5ab00f5ff6f87b6fe6d27d", + "trader": "0xcde6dbe01902be1f200ff03dbbd149e586847be8cee15235f82750d9b06c0e04" + }, + { + "event_digest": "4c3YE3wdiiU4HGjfy7VKbLfMbGbdNMrymxsBU5hF2EZc6", + "digest": "4c3YE3wdiiU4HGjfy7VKbLfMbGbdNMrymxsBU5hF2EZc", + "sender": "0xcde6dbe01902be1f200ff03dbbd149e586847be8cee15235f82750d9b06c0e04", + "checkpoint": "100000017", + "timestamp": "1970-01-01 00:00:00.000000", + "checkpoint_timestamp_ms": "1736510372652", + "package": "0x2c8d603bc51326b8c13cef9dd07031a408a48dddb541963357661df5d3204809", + "status": "Placed", + "pool_id": "0xf948981b806057580f91622417534f491da5f61aeaf33d0ed8e69fd5691c95ce", + "order_id": "2811099347839342644467750839", + "client_order_id": "6079306980876303357", + "price": "152390000", + "is_bid": true, + "original_quantity": "246080000000", + "quantity": "246080000000", + "filled_quantity": "0", + "onchain_timestamp": "1736510372539", + "balance_manager_id": "0x344c2734b1d211bd15212bfb7847c66a3b18803f3f5ab00f5ff6f87b6fe6d27d", + "trader": "0xcde6dbe01902be1f200ff03dbbd149e586847be8cee15235f82750d9b06c0e04" + }, + { + "event_digest": "4c3YE3wdiiU4HGjfy7VKbLfMbGbdNMrymxsBU5hF2EZc8", + "digest": "4c3YE3wdiiU4HGjfy7VKbLfMbGbdNMrymxsBU5hF2EZc", + "sender": "0xcde6dbe01902be1f200ff03dbbd149e586847be8cee15235f82750d9b06c0e04", + "checkpoint": "100000017", + "timestamp": "1970-01-01 00:00:00.000000", + "checkpoint_timestamp_ms": "1736510372652", + "package": "0x2c8d603bc51326b8c13cef9dd07031a408a48dddb541963357661df5d3204809", + "status": "Placed", + "pool_id": "0xf948981b806057580f91622417534f491da5f61aeaf33d0ed8e69fd5691c95ce", + "order_id": "170141183463383263893011201584667627705", + "client_order_id": "4784898273119735712", + "price": "157970000", + "is_bid": false, + "original_quantity": "237390000000", + "quantity": "237390000000", + "filled_quantity": "0", + "onchain_timestamp": "1736510372539", + "balance_manager_id": "0x344c2734b1d211bd15212bfb7847c66a3b18803f3f5ab00f5ff6f87b6fe6d27d", + "trader": "0xcde6dbe01902be1f200ff03dbbd149e586847be8cee15235f82750d9b06c0e04" + } +] diff --git a/crates/indexer/tests/snapshots/snapshot_tests__pool_created__pool_created.snap b/crates/indexer/tests/snapshots/snapshot_tests__pool_created__pool_created.snap new file mode 100644 index 000000000..0c50fe567 --- /dev/null +++ b/crates/indexer/tests/snapshots/snapshot_tests__pool_created__pool_created.snap @@ -0,0 +1,57 @@ +--- +source: crates/indexer/tests/snapshot_tests.rs +expression: rows +--- +[ + { + "event_digest": "99Z2f9MPDsRDXs5wDLZHF4ZuuiE3z31VFHz77Hfuvkmq0", + "digest": "99Z2f9MPDsRDXs5wDLZHF4ZuuiE3z31VFHz77Hfuvkmq", + "sender": "0xd0ec0b201de6b4e7f425918bbd7151c37fc1b06c59b3961a2a00db74f6ea865e", + "checkpoint": "231798277", + "timestamp": "1970-01-01 00:00:00.000000", + "checkpoint_timestamp_ms": "1767732971503", + "package": "0x00c1a56ec8c4c623a848b2ed2f03d23a25d17570b670c22106f336eb933785cc", + "pool_id": "0xf5142aafa24866107df628bf92d0358c7da6acc46c2f10951690fd2b8570f117", + "taker_fee": "1000000", + "maker_fee": "500000", + "tick_size": "10000000", + "lot_size": "1000", + "min_size": "1000", + "whitelisted_pool": false, + "treasury_address": "0x37f187e1e54e9c9b8c78b6c46a7281f644ebc62e75493623edcaa6d1dfcf64d2" + }, + { + "event_digest": "9EhhYMmFPYoq5kvKMYraznXEhgmcXjmjNvXeNxXoR2wK0", + "digest": "9EhhYMmFPYoq5kvKMYraznXEhgmcXjmjNvXeNxXoR2wK", + "sender": "0xbd1d25f49cc9b65f1e41d6c264ad0e065923de7ce6fd8b86d87d25c0a58742b9", + "checkpoint": "67459304", + "timestamp": "1970-01-01 00:00:00.000000", + "checkpoint_timestamp_ms": "1728568237549", + "package": "0x2c8d603bc51326b8c13cef9dd07031a408a48dddb541963357661df5d3204809", + "pool_id": "0xe9aecf5859310f8b596fbe8488222a7fb15a55003455c9f42d1b60fab9cca9ba", + "taker_fee": "0", + "maker_fee": "0", + "tick_size": "1000000000", + "lot_size": "1000000", + "min_size": "10000000", + "whitelisted_pool": true, + "treasury_address": "0xbd1d25f49cc9b65f1e41d6c264ad0e065923de7ce6fd8b86d87d25c0a58742b9" + }, + { + "event_digest": "9EhhYMmFPYoq5kvKMYraznXEhgmcXjmjNvXeNxXoR2wK1", + "digest": "9EhhYMmFPYoq5kvKMYraznXEhgmcXjmjNvXeNxXoR2wK", + "sender": "0xbd1d25f49cc9b65f1e41d6c264ad0e065923de7ce6fd8b86d87d25c0a58742b9", + "checkpoint": "67459304", + "timestamp": "1970-01-01 00:00:00.000000", + "checkpoint_timestamp_ms": "1728568237549", + "package": "0x2c8d603bc51326b8c13cef9dd07031a408a48dddb541963357661df5d3204809", + "pool_id": "0xde096bb2c59538a25c89229127fe0bc8b63ecdbe52a3693099cc40a1d8a2cfd4", + "taker_fee": "0", + "maker_fee": "0", + "tick_size": "1000000", + "lot_size": "1000000", + "min_size": "10000000", + "whitelisted_pool": true, + "treasury_address": "0xbd1d25f49cc9b65f1e41d6c264ad0e065923de7ce6fd8b86d87d25c0a58742b9" + } +] diff --git a/crates/indexer/tests/snapshots/snapshot_tests__pool_price__pool_prices.snap b/crates/indexer/tests/snapshots/snapshot_tests__pool_price__pool_prices.snap new file mode 100644 index 000000000..628d86ccb --- /dev/null +++ b/crates/indexer/tests/snapshots/snapshot_tests__pool_price__pool_prices.snap @@ -0,0 +1,42 @@ +--- +source: crates/indexer/tests/snapshot_tests.rs +expression: rows +--- +[ + { + "event_digest": "GvWP4wQq2iehpvHVaDnrTC9SCFhAYxD6jXyY9VVcMfSc0", + "digest": "GvWP4wQq2iehpvHVaDnrTC9SCFhAYxD6jXyY9VVcMfSc", + "sender": "0xbd1d25f49cc9b65f1e41d6c264ad0e065923de7ce6fd8b86d87d25c0a58742b9", + "checkpoint": "100005828", + "timestamp": "1970-01-01 00:00:00.000000", + "checkpoint_timestamp_ms": "1736511782554", + "package": "0x2c8d603bc51326b8c13cef9dd07031a408a48dddb541963357661df5d3204809", + "target_pool": "0x1109352b9112717bd2a7c3eb9a416fff1ba6951760f5bdd5424cf5e4e5b3e65c", + "reference_pool": "0xf948981b806057580f91622417534f491da5f61aeaf33d0ed8e69fd5691c95ce", + "conversion_rate": "6631959412" + }, + { + "event_digest": "GvWP4wQq2iehpvHVaDnrTC9SCFhAYxD6jXyY9VVcMfSc1", + "digest": "GvWP4wQq2iehpvHVaDnrTC9SCFhAYxD6jXyY9VVcMfSc", + "sender": "0xbd1d25f49cc9b65f1e41d6c264ad0e065923de7ce6fd8b86d87d25c0a58742b9", + "checkpoint": "100005828", + "timestamp": "1970-01-01 00:00:00.000000", + "checkpoint_timestamp_ms": "1736511782554", + "package": "0x2c8d603bc51326b8c13cef9dd07031a408a48dddb541963357661df5d3204809", + "target_pool": "0xe8e56f377ab5a261449b92ac42c8ddaacd5671e9fec2179d7933dd1a91200eec", + "reference_pool": "0xb663828d6217467c8a1838a03793da896cbe745b150ebd57d82f814ca579fc22", + "conversion_rate": "32226877" + }, + { + "event_digest": "GvWP4wQq2iehpvHVaDnrTC9SCFhAYxD6jXyY9VVcMfSc2", + "digest": "GvWP4wQq2iehpvHVaDnrTC9SCFhAYxD6jXyY9VVcMfSc", + "sender": "0xbd1d25f49cc9b65f1e41d6c264ad0e065923de7ce6fd8b86d87d25c0a58742b9", + "checkpoint": "100005828", + "timestamp": "1970-01-01 00:00:00.000000", + "checkpoint_timestamp_ms": "1736511782554", + "package": "0x2c8d603bc51326b8c13cef9dd07031a408a48dddb541963357661df5d3204809", + "target_pool": "0x126865a0197d6ab44bfd15fd052da6db92fd2eb831ff9663451bbfa1219e2af2", + "reference_pool": "0xb663828d6217467c8a1838a03793da896cbe745b150ebd57d82f814ca579fc22", + "conversion_rate": "32226877" + } +] diff --git a/crates/indexer/tests/snapshots/snapshot_tests__protocol_fees_increased__protocol_fees_increased.snap b/crates/indexer/tests/snapshots/snapshot_tests__protocol_fees_increased__protocol_fees_increased.snap new file mode 100644 index 000000000..b96e0b8b3 --- /dev/null +++ b/crates/indexer/tests/snapshots/snapshot_tests__protocol_fees_increased__protocol_fees_increased.snap @@ -0,0 +1,21 @@ +--- +source: crates/indexer/tests/snapshot_tests.rs +expression: rows +--- +[ + { + "event_digest": "F5hfpMCpkAumKtrbmQHE6tV1SU6SwZaGBsLaDrKWS67t3", + "digest": "F5hfpMCpkAumKtrbmQHE6tV1SU6SwZaGBsLaDrKWS67t", + "sender": "0xb3d277c50f7b846a5f609a8d13428ae482b5826bb98437997373f3a0d60d280e", + "checkpoint": "273995602", + "timestamp": "1970-01-01 00:00:00.000000", + "checkpoint_timestamp_ms": "1765487051349", + "package": "0x21473617f3565d704aa67be73ea41243e9e34a42d434c31f8182c67ba01ccf49", + "margin_pool_id": "0xcdbbe6a72e639b647296788e2e4b1cac5cea4246028ba388ba1332ff9a382eea", + "total_shares": "90000000000", + "referral_fees": "141", + "maintainer_fees": "69", + "protocol_fees": "69", + "onchain_timestamp": "1765487051349" + } +] diff --git a/crates/indexer/tests/snapshots/snapshot_tests__referral_fee_events__referral_fee_events.snap b/crates/indexer/tests/snapshots/snapshot_tests__referral_fee_events__referral_fee_events.snap new file mode 100644 index 000000000..e1b120431 --- /dev/null +++ b/crates/indexer/tests/snapshots/snapshot_tests__referral_fee_events__referral_fee_events.snap @@ -0,0 +1,20 @@ +--- +source: crates/indexer/tests/snapshot_tests.rs +expression: rows +--- +[ + { + "event_digest": "D2YQEZ6D3SfUZ2bVGpMbrzGPoZAbrLYwy5m249aHwqL14", + "digest": "D2YQEZ6D3SfUZ2bVGpMbrzGPoZAbrLYwy5m249aHwqL1", + "sender": "0x064d87c3da8b7201b18c05bfc3189eb817920b2d089b33e207d1d99dc5ce08e0", + "checkpoint": "233962755", + "timestamp": "1970-01-01 00:00:00.000000", + "checkpoint_timestamp_ms": "1768251049003", + "package": "0x2d93777cc8b67c064b495e8606f2f8f5fd578450347bbe7b36e0bc03963c1c40", + "pool_id": "0xe05dafb5133bcffb8d59f4e12465dc0e9faeaa05e3e342a08fe135800e3e4407", + "referral_id": "0xf66fc08674e5592b471d965c82410af5a2b44e2b4b92f191d91c7147d378bcaa", + "base_fee": "12500", + "quote_fee": "0", + "deep_fee": "0" + } +] diff --git a/crates/indexer/tests/snapshots/snapshot_tests__referral_fees_claimed__referral_fees_claimed.snap b/crates/indexer/tests/snapshots/snapshot_tests__referral_fees_claimed__referral_fees_claimed.snap new file mode 100644 index 000000000..bbb128e99 --- /dev/null +++ b/crates/indexer/tests/snapshots/snapshot_tests__referral_fees_claimed__referral_fees_claimed.snap @@ -0,0 +1,19 @@ +--- +source: crates/indexer/tests/snapshot_tests.rs +expression: rows +--- +[ + { + "event_digest": "CuSyyFPjwxvvPb4QaSMv1eaVAGMYiMdUwswu59sqeTPd3", + "digest": "CuSyyFPjwxvvPb4QaSMv1eaVAGMYiMdUwswu59sqeTPd", + "sender": "0xb3d277c50f7b846a5f609a8d13428ae482b5826bb98437997373f3a0d60d280e", + "checkpoint": "273993083", + "timestamp": "1970-01-01 00:00:00.000000", + "checkpoint_timestamp_ms": "1765486444883", + "package": "0x21473617f3565d704aa67be73ea41243e9e34a42d434c31f8182c67ba01ccf49", + "referral_id": "0xaed597fe1a05b9838b198a3dfa2cdd191b6fa7b319f4c3fc676c7b7348cec194", + "owner": "0xb3d277c50f7b846a5f609a8d13428ae482b5826bb98437997373f3a0d60d280e", + "fees": "0", + "onchain_timestamp": "1765486444883" + } +] diff --git a/crates/indexer/tests/snapshots/snapshot_tests__supplier_cap_minted__supplier_cap_minted.snap b/crates/indexer/tests/snapshots/snapshot_tests__supplier_cap_minted__supplier_cap_minted.snap new file mode 100644 index 000000000..45e73cb3e --- /dev/null +++ b/crates/indexer/tests/snapshots/snapshot_tests__supplier_cap_minted__supplier_cap_minted.snap @@ -0,0 +1,17 @@ +--- +source: crates/indexer/tests/snapshot_tests.rs +expression: rows +--- +[ + { + "event_digest": "DZRk35vQi9Bt6mHxaqyieNygNnkWpaVvg41L451fz24j0", + "digest": "DZRk35vQi9Bt6mHxaqyieNygNnkWpaVvg41L451fz24j", + "sender": "0xb3d277c50f7b846a5f609a8d13428ae482b5826bb98437997373f3a0d60d280e", + "checkpoint": "273302447", + "timestamp": "1970-01-01 00:00:00.000000", + "checkpoint_timestamp_ms": "1765320025322", + "package": "0xb8620c24c9ea1a4a41e79613d2b3d1d93648d1bb6f6b789a7c8f261c94110e4b", + "supplier_cap_id": "0x291d20d07dbbff547ea583b57daed761b8cf9dc4f6090f49980e7a02f63602c6", + "onchain_timestamp": "1765320025263" + } +] diff --git a/crates/indexer/tests/snapshots/snapshot_tests__supply_referral_minted__supply_referral_minted.snap b/crates/indexer/tests/snapshots/snapshot_tests__supply_referral_minted__supply_referral_minted.snap new file mode 100644 index 000000000..b75a4775a --- /dev/null +++ b/crates/indexer/tests/snapshots/snapshot_tests__supply_referral_minted__supply_referral_minted.snap @@ -0,0 +1,19 @@ +--- +source: crates/indexer/tests/snapshot_tests.rs +expression: rows +--- +[ + { + "event_digest": "EvWNXJjWh5XWvEX75Sq4nr9eieHqkghR8zDuXo3Cj5QT0", + "digest": "EvWNXJjWh5XWvEX75Sq4nr9eieHqkghR8zDuXo3Cj5QT", + "sender": "0xb3d277c50f7b846a5f609a8d13428ae482b5826bb98437997373f3a0d60d280e", + "checkpoint": "273301811", + "timestamp": "1970-01-01 00:00:00.000000", + "checkpoint_timestamp_ms": "1765319876441", + "package": "0xb8620c24c9ea1a4a41e79613d2b3d1d93648d1bb6f6b789a7c8f261c94110e4b", + "margin_pool_id": "0xf08568da93834e1ee04f09902ac7b1e78d3fdf113ab4d2106c7265e95318b14d", + "supply_referral_id": "0x6e9b703a798382bb07d27fd7a44d09ef7682a53584d73d0061de98a699ad86ac", + "owner": "0xb3d277c50f7b846a5f609a8d13428ae482b5826bb98437997373f3a0d60d280e", + "onchain_timestamp": "1765319876306" + } +] diff --git a/crates/indexer/tests/snapshots/snapshot_tests__withdraw_collateral__collateral_events.snap b/crates/indexer/tests/snapshots/snapshot_tests__withdraw_collateral__collateral_events.snap new file mode 100644 index 000000000..317f8e389 --- /dev/null +++ b/crates/indexer/tests/snapshots/snapshot_tests__withdraw_collateral__collateral_events.snap @@ -0,0 +1,31 @@ +--- +source: crates/indexer/tests/snapshot_tests.rs +expression: rows +--- +[ + { + "event_digest": "73DkKzySTo824MBEQREnhNwXbbSpX8YEEb7qbfxxaHGG1", + "digest": "73DkKzySTo824MBEQREnhNwXbbSpX8YEEb7qbfxxaHGG", + "sender": "0x064d87c3da8b7201b18c05bfc3189eb817920b2d089b33e207d1d99dc5ce08e0", + "checkpoint": "234920766", + "timestamp": "1970-01-01 00:00:00.000000", + "checkpoint_timestamp_ms": "1768502033827", + "package": "0x97d9473771b01f77b0940c589484184b49f6444627ec121314fae6a6d36fb86b", + "event_type": "withdraw", + "margin_manager_id": "0x16d8a37cfcacb4436a68f91eaf5d8e7d0aa1482a1958f8a90588f8d348ca9fc4", + "amount": "5000000", + "asset_type": "dba34672e30cb065b1f93e3ab55318768fd6fef66c15942c9f7cb846e2f900e7::usdc::USDC", + "pyth_decimals": "8", + "pyth_price": "177211899", + "withdraw_base_asset": false, + "base_pyth_decimals": "8", + "base_pyth_price": "177211899", + "quote_pyth_decimals": "8", + "quote_pyth_price": "99977717", + "remaining_base_asset": "109999944", + "remaining_quote_asset": "0", + "remaining_base_debt": "0", + "remaining_quote_debt": "0", + "onchain_timestamp": "1768502033827" + } +] diff --git a/crates/schema/Cargo.toml b/crates/schema/Cargo.toml index dec20bf66..aa88dabec 100644 --- a/crates/schema/Cargo.toml +++ b/crates/schema/Cargo.toml @@ -7,13 +7,12 @@ publish = false edition = "2021" [dependencies] -sui-field-count = { git = "https://github.com/MystenLabs/sui.git", rev = "88ba4e08e96ba1ab965c11ce1a915331dd3ed68d"} +sui-field-count = { git = "https://github.com/MystenLabs/sui.git", branch = "testnet" } diesel = { workspace = true, features = ["postgres", "uuid", "chrono", "serde_json", "numeric"] } diesel_migrations.workspace = true -dotenvy = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } -chrono = { workspace = true } -anyhow = { workspace = true } strum = "0.27.1" strum_macros = "0.27.1" +bigdecimal = { version = "0.4", features = ["serde"] } +chrono = "0.4" diff --git a/crates/schema/docs/pg_cron.md b/crates/schema/docs/pg_cron.md new file mode 100644 index 000000000..26372ebf0 --- /dev/null +++ b/crates/schema/docs/pg_cron.md @@ -0,0 +1,102 @@ +# pg_cron Setup + +**Important**: This SQL script must be run manually outside of a diesel migration. + +Due to our production setup being on a different database than the one the cron is run on, we need to run this outside of a diesel migration. The pg_cron extension and scheduled jobs need to be set up on the database instance that will actually execute the cron jobs, which may be separate from the main application database. + +## Manual Setup Instructions + +1. Connect to the database where pg_cron is installed +2. Use `schedule_in_database` to schedule jobs that will run on the target database + - Note: If pg_cron is installed on the same database as your application, you can use `schedule()` instead +3. Run the SQL commands below to schedule the OHCLV update jobs + +-- Enable pg_cron +CREATE EXTENSION IF NOT EXISTS pg_cron; + +-- minute candle updates +SELECT cron.schedule_in_database( + 'update_ohclv_1m_recent', + '* * * * *', + $$ + CALL update_ohclv_1m( + (EXTRACT(EPOCH FROM NOW() - INTERVAL '5 minutes') * 1000)::BIGINT, + (EXTRACT(EPOCH FROM NOW()) * 1000)::BIGINT + ); + $$, + 'deepbook' -- target database name +); + +-- daily candle updates +SELECT cron.schedule_in_database( + 'update_ohclv_1d_recent', + '0 * * * *', + $$ + CALL update_ohclv_1d( + (EXTRACT(EPOCH FROM NOW() - INTERVAL '2 days') * 1000)::BIGINT, + (EXTRACT(EPOCH FROM NOW()) * 1000)::BIGINT + ); + $$, + 'deepbook' +); + +-- weekly full refresh for minute candles +SELECT cron.schedule_in_database( + 'refresh_ohclv_1m_full', + '0 2 * * 0', + $$ + CALL update_ohclv_1m( + 0::BIGINT, + (EXTRACT(EPOCH FROM NOW()) * 1000)::BIGINT + ); + $$, + 'deepbook' +); + +-- weekly full refresh for daily candles +SELECT cron.schedule_in_database( + 'refresh_ohclv_1d_full', + '0 3 * * 0', + $$ + CALL update_ohclv_1d( + 0::BIGINT, + (EXTRACT(EPOCH FROM NOW()) * 1000)::BIGINT + ); + $$, + 'deepbook' +); + +## Monitoring Cron Jobs + +### View Active Jobs +```sql +SELECT * FROM cron.job; +``` + +### View Job Run Details and Logs +```sql +SELECT * FROM cron.job_run_details +ORDER BY start_time DESC +LIMIT 10; +``` + +### View Specific Job Logs +```sql +SELECT * FROM cron.job_run_details +WHERE jobid = (SELECT jobid FROM cron.job WHERE jobname = 'update_ohclv_1m_recent') +ORDER BY start_time DESC; +``` + +### View Latest Job Run for Specific Job +```sql +SELECT * FROM cron.job_run_details +WHERE jobid = (SELECT jobid FROM cron.job WHERE jobname = 'update_ohclv_1m_recent') +ORDER BY start_time DESC +LIMIT 1; +``` + +### Clean Up Old Logs +```sql +DELETE FROM cron.job_run_details +WHERE end_time < now() - interval '7 days'; +``` diff --git a/crates/schema/migrations/.diesel_lock b/crates/schema/migrations/.diesel_lock new file mode 100644 index 000000000..e69de29bb diff --git a/crates/schema/migrations/2025-03-19-104023_deepbook/up.sql b/crates/schema/migrations/2025-03-19-104023_deepbook/up.sql index 3b07f707f..914d6805f 100644 --- a/crates/schema/migrations/2025-03-19-104023_deepbook/up.sql +++ b/crates/schema/migrations/2025-03-19-104023_deepbook/up.sql @@ -204,7 +204,7 @@ CREATE TABLE IF NOT EXISTS pools CREATE TABLE IF NOT EXISTS assets ( - type TEXT PRIMARY KEY, + asset_type TEXT PRIMARY KEY, name TEXT NOT NULL, symbol TEXT NOT NULL, decimals SMALLINT NOT NULL, diff --git a/crates/schema/migrations/2025-09-24-185631-0000_add_deep_burned_table/down.sql b/crates/schema/migrations/2025-09-24-185631-0000_add_deep_burned_table/down.sql new file mode 100644 index 000000000..4d1b64065 --- /dev/null +++ b/crates/schema/migrations/2025-09-24-185631-0000_add_deep_burned_table/down.sql @@ -0,0 +1,2 @@ +-- This file should undo anything in `up.sql` +DROP TABLE IF EXISTS deep_burned; \ No newline at end of file diff --git a/crates/schema/migrations/2025-09-24-185631-0000_add_deep_burned_table/up.sql b/crates/schema/migrations/2025-09-24-185631-0000_add_deep_burned_table/up.sql new file mode 100644 index 000000000..eaa3f8c30 --- /dev/null +++ b/crates/schema/migrations/2025-09-24-185631-0000_add_deep_burned_table/up.sql @@ -0,0 +1,17 @@ +-- Your SQL goes here + +CREATE TABLE IF NOT EXISTS deep_burned +( + event_digest TEXT PRIMARY KEY, + digest TEXT NOT NULL, + sender TEXT NOT NULL, + checkpoint BIGINT NOT NULL, + timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL, + checkpoint_timestamp_ms BIGINT NOT NULL, + package TEXT NOT NULL, + pool_id TEXT NOT NULL, + burned_amount BIGINT NOT NULL +); + +CREATE INDEX idx_deep_burned_pool_id ON deep_burned(pool_id); +CREATE INDEX idx_deep_burned_checkpoint ON deep_burned(checkpoint); \ No newline at end of file diff --git a/crates/schema/migrations/2025-09-26-150124-0000_alter_pool_fields_to_bigint/down.sql b/crates/schema/migrations/2025-09-26-150124-0000_alter_pool_fields_to_bigint/down.sql new file mode 100644 index 000000000..0eb6010ff --- /dev/null +++ b/crates/schema/migrations/2025-09-26-150124-0000_alter_pool_fields_to_bigint/down.sql @@ -0,0 +1,4 @@ +ALTER TABLE pools + ALTER COLUMN min_size TYPE INTEGER, + ALTER COLUMN lot_size TYPE INTEGER, + ALTER COLUMN tick_size TYPE INTEGER; diff --git a/crates/schema/migrations/2025-09-26-150124-0000_alter_pool_fields_to_bigint/up.sql b/crates/schema/migrations/2025-09-26-150124-0000_alter_pool_fields_to_bigint/up.sql new file mode 100644 index 000000000..d4931a0c6 --- /dev/null +++ b/crates/schema/migrations/2025-09-26-150124-0000_alter_pool_fields_to_bigint/up.sql @@ -0,0 +1,4 @@ +ALTER TABLE pools + ALTER COLUMN min_size TYPE BIGINT, + ALTER COLUMN lot_size TYPE BIGINT, + ALTER COLUMN tick_size TYPE BIGINT; diff --git a/crates/schema/migrations/2025-09-30-200612-0000_add_indexes/down.sql b/crates/schema/migrations/2025-09-30-200612-0000_add_indexes/down.sql new file mode 100644 index 000000000..d1e95f1a0 --- /dev/null +++ b/crates/schema/migrations/2025-09-30-200612-0000_add_indexes/down.sql @@ -0,0 +1,9 @@ +DROP INDEX IF EXISTS idx_order_fills_checkpoint_timestamp_ms; +DROP INDEX IF EXISTS idx_order_fills_pool_id_price; + +DROP INDEX IF EXISTS idx_order_updates_checkpoint_timestamp_ms; +DROP INDEX IF EXISTS idx_order_updates_pool_id_price; + +DROP INDEX IF EXISTS idx_balances_checkpoint_timestamp_ms; +DROP INDEX IF EXISTS idx_balances_balance_manager_id; +DROP INDEX IF EXISTS idx_balances_asset; \ No newline at end of file diff --git a/crates/schema/migrations/2025-09-30-200612-0000_add_indexes/up.sql b/crates/schema/migrations/2025-09-30-200612-0000_add_indexes/up.sql new file mode 100644 index 000000000..5d8bf5a1c --- /dev/null +++ b/crates/schema/migrations/2025-09-30-200612-0000_add_indexes/up.sql @@ -0,0 +1,20 @@ +CREATE INDEX IF NOT EXISTS idx_order_fills_checkpoint_timestamp_ms + ON order_fills (checkpoint_timestamp_ms); + +CREATE INDEX IF NOT EXISTS idx_order_fills_pool_id_price + ON order_fills (pool_id, price); + +CREATE INDEX IF NOT EXISTS idx_order_updates_checkpoint_timestamp_ms + ON order_updates (checkpoint_timestamp_ms); + +CREATE INDEX IF NOT EXISTS idx_order_updates_pool_id_price + ON order_updates (pool_id, price); + +CREATE INDEX IF NOT EXISTS idx_balances_checkpoint_timestamp_ms + ON balances (checkpoint_timestamp_ms); + +CREATE INDEX IF NOT EXISTS idx_balances_balance_manager_id + ON balances (balance_manager_id); + +CREATE INDEX IF NOT EXISTS idx_balances_asset + ON balances (asset); \ No newline at end of file diff --git a/crates/schema/migrations/2025-09-30-202714-0000_add_ohclv/down.sql b/crates/schema/migrations/2025-09-30-202714-0000_add_ohclv/down.sql new file mode 100644 index 000000000..dbc28746a --- /dev/null +++ b/crates/schema/migrations/2025-09-30-202714-0000_add_ohclv/down.sql @@ -0,0 +1,12 @@ +DROP PROCEDURE IF EXISTS update_all_ohclv(BIGINT, BIGINT); +DROP FUNCTION IF EXISTS get_ohclv(TEXT, TEXT, TIMESTAMP, TIMESTAMP, INTEGER); +DROP PROCEDURE IF EXISTS update_ohclv_1d(BIGINT, BIGINT); +DROP PROCEDURE IF EXISTS update_ohclv_1m(BIGINT, BIGINT); + +DROP INDEX IF EXISTS idx_ohclv_1d_time; +DROP INDEX IF EXISTS idx_ohclv_1d_pool_time; +DROP INDEX IF EXISTS idx_ohclv_1m_time; +DROP INDEX IF EXISTS idx_ohclv_1m_pool_time; + +DROP TABLE IF EXISTS ohclv_1d; +DROP TABLE IF EXISTS ohclv_1m; \ No newline at end of file diff --git a/crates/schema/migrations/2025-09-30-202714-0000_add_ohclv/up.sql b/crates/schema/migrations/2025-09-30-202714-0000_add_ohclv/up.sql new file mode 100644 index 000000000..11756e2bc --- /dev/null +++ b/crates/schema/migrations/2025-09-30-202714-0000_add_ohclv/up.sql @@ -0,0 +1,389 @@ +CREATE TABLE IF NOT EXISTS ohclv_1m ( + pool_id TEXT NOT NULL, + bucket_time TIMESTAMP NOT NULL, + open NUMERIC NOT NULL, + high NUMERIC NOT NULL, + low NUMERIC NOT NULL, + close NUMERIC NOT NULL, + base_volume NUMERIC NOT NULL, + quote_volume NUMERIC NOT NULL, + trade_count INTEGER NOT NULL, + first_trade_timestamp BIGINT NOT NULL, + last_trade_timestamp BIGINT NOT NULL, + PRIMARY KEY (pool_id, bucket_time) +); + +CREATE TABLE IF NOT EXISTS ohclv_1d ( + pool_id TEXT NOT NULL, + bucket_time DATE NOT NULL, + open NUMERIC NOT NULL, + high NUMERIC NOT NULL, + low NUMERIC NOT NULL, + close NUMERIC NOT NULL, + base_volume NUMERIC NOT NULL, + quote_volume NUMERIC NOT NULL, + trade_count INTEGER NOT NULL, + first_trade_timestamp BIGINT NOT NULL, + last_trade_timestamp BIGINT NOT NULL, + PRIMARY KEY (pool_id, bucket_time) +); + +CREATE INDEX IF NOT EXISTS idx_ohclv_1m_pool_time ON ohclv_1m (pool_id, bucket_time DESC); +CREATE INDEX IF NOT EXISTS idx_ohclv_1m_time ON ohclv_1m (bucket_time DESC); +CREATE INDEX IF NOT EXISTS idx_ohclv_1d_pool_time ON ohclv_1d (pool_id, bucket_time DESC); +CREATE INDEX IF NOT EXISTS idx_ohclv_1d_time ON ohclv_1d (bucket_time DESC); + +CREATE OR REPLACE PROCEDURE update_ohclv_1m( + start_timestamp BIGINT DEFAULT NULL, + end_timestamp BIGINT DEFAULT NULL +) +LANGUAGE plpgsql +AS $$ +BEGIN + -- Default to last 24 hours if no range specified + IF start_timestamp IS NULL THEN + start_timestamp := (EXTRACT(EPOCH FROM NOW() - INTERVAL '24 hours') * 1000)::BIGINT; + END IF; + + IF end_timestamp IS NULL THEN + end_timestamp := (EXTRACT(EPOCH FROM NOW()) * 1000)::BIGINT; + END IF; + + INSERT INTO ohclv_1m ( + pool_id, + bucket_time, + open, + high, + low, + close, + base_volume, + quote_volume, + trade_count, + first_trade_timestamp, + last_trade_timestamp + ) + SELECT DISTINCT ON (pool_id, bucket_time) + f.pool_id, + date_trunc('minute', to_timestamp(f.checkpoint_timestamp_ms / 1000.0)) as bucket_time, + FIRST_VALUE(f.price::numeric / POWER(10, p.quote_asset_decimals)) + OVER (PARTITION BY f.pool_id, date_trunc('minute', to_timestamp(f.checkpoint_timestamp_ms / 1000.0)) + ORDER BY f.checkpoint_timestamp_ms + ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING) as open, + MAX(f.price::numeric / POWER(10, p.quote_asset_decimals)) + OVER (PARTITION BY f.pool_id, date_trunc('minute', to_timestamp(f.checkpoint_timestamp_ms / 1000.0))) as high, + MIN(f.price::numeric / POWER(10, p.quote_asset_decimals)) + OVER (PARTITION BY f.pool_id, date_trunc('minute', to_timestamp(f.checkpoint_timestamp_ms / 1000.0))) as low, + LAST_VALUE(f.price::numeric / POWER(10, p.quote_asset_decimals)) + OVER (PARTITION BY f.pool_id, date_trunc('minute', to_timestamp(f.checkpoint_timestamp_ms / 1000.0)) + ORDER BY f.checkpoint_timestamp_ms + ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING) as close, + SUM(f.base_quantity::numeric / POWER(10, p.base_asset_decimals)) + OVER (PARTITION BY f.pool_id, date_trunc('minute', to_timestamp(f.checkpoint_timestamp_ms / 1000.0))) as base_volume, + SUM(f.quote_quantity::numeric / POWER(10, p.quote_asset_decimals)) + OVER (PARTITION BY f.pool_id, date_trunc('minute', to_timestamp(f.checkpoint_timestamp_ms / 1000.0))) as quote_volume, + COUNT(*) + OVER (PARTITION BY f.pool_id, date_trunc('minute', to_timestamp(f.checkpoint_timestamp_ms / 1000.0))) as trade_count, + MIN(f.checkpoint_timestamp_ms) + OVER (PARTITION BY f.pool_id, date_trunc('minute', to_timestamp(f.checkpoint_timestamp_ms / 1000.0))) as first_trade_timestamp, + MAX(f.checkpoint_timestamp_ms) + OVER (PARTITION BY f.pool_id, date_trunc('minute', to_timestamp(f.checkpoint_timestamp_ms / 1000.0))) as last_trade_timestamp + FROM order_fills f + INNER JOIN pools p ON f.pool_id = p.pool_id + WHERE f.checkpoint_timestamp_ms >= start_timestamp + AND f.checkpoint_timestamp_ms <= end_timestamp + ON CONFLICT (pool_id, bucket_time) + DO UPDATE SET + high = GREATEST(EXCLUDED.high, ohclv_1m.high), + low = LEAST(EXCLUDED.low, ohclv_1m.low), + close = EXCLUDED.close, -- Latest close wins + base_volume = EXCLUDED.base_volume, -- For simplicity, replace volume + quote_volume = EXCLUDED.quote_volume, + trade_count = EXCLUDED.trade_count, + first_trade_timestamp = LEAST(EXCLUDED.first_trade_timestamp, ohclv_1m.first_trade_timestamp), + last_trade_timestamp = GREATEST(EXCLUDED.last_trade_timestamp, ohclv_1m.last_trade_timestamp); +END; +$$; + +CREATE OR REPLACE PROCEDURE update_ohclv_1d( + start_timestamp BIGINT DEFAULT NULL, + end_timestamp BIGINT DEFAULT NULL +) +LANGUAGE plpgsql +AS $$ +BEGIN + -- Default to last 7 days if no range specified + IF start_timestamp IS NULL THEN + start_timestamp := (EXTRACT(EPOCH FROM NOW() - INTERVAL '7 days') * 1000)::BIGINT; + END IF; + + IF end_timestamp IS NULL THEN + end_timestamp := (EXTRACT(EPOCH FROM NOW()) * 1000)::BIGINT; + END IF; + + INSERT INTO ohclv_1d ( + pool_id, + bucket_time, + open, + high, + low, + close, + base_volume, + quote_volume, + trade_count, + first_trade_timestamp, + last_trade_timestamp + ) + SELECT DISTINCT ON (pool_id, bucket_time) + f.pool_id, + date_trunc('day', to_timestamp(f.checkpoint_timestamp_ms / 1000.0))::DATE as bucket_time, + FIRST_VALUE(f.price::numeric / POWER(10, p.quote_asset_decimals)) + OVER (PARTITION BY f.pool_id, date_trunc('day', to_timestamp(f.checkpoint_timestamp_ms / 1000.0)) + ORDER BY f.checkpoint_timestamp_ms + ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING) as open, + MAX(f.price::numeric / POWER(10, p.quote_asset_decimals)) + OVER (PARTITION BY f.pool_id, date_trunc('day', to_timestamp(f.checkpoint_timestamp_ms / 1000.0))) as high, + MIN(f.price::numeric / POWER(10, p.quote_asset_decimals)) + OVER (PARTITION BY f.pool_id, date_trunc('day', to_timestamp(f.checkpoint_timestamp_ms / 1000.0))) as low, + LAST_VALUE(f.price::numeric / POWER(10, p.quote_asset_decimals)) + OVER (PARTITION BY f.pool_id, date_trunc('day', to_timestamp(f.checkpoint_timestamp_ms / 1000.0)) + ORDER BY f.checkpoint_timestamp_ms + ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING) as close, + SUM(f.base_quantity::numeric / POWER(10, p.base_asset_decimals)) + OVER (PARTITION BY f.pool_id, date_trunc('day', to_timestamp(f.checkpoint_timestamp_ms / 1000.0))) as base_volume, + SUM(f.quote_quantity::numeric / POWER(10, p.quote_asset_decimals)) + OVER (PARTITION BY f.pool_id, date_trunc('day', to_timestamp(f.checkpoint_timestamp_ms / 1000.0))) as quote_volume, + COUNT(*) + OVER (PARTITION BY f.pool_id, date_trunc('day', to_timestamp(f.checkpoint_timestamp_ms / 1000.0))) as trade_count, + MIN(f.checkpoint_timestamp_ms) + OVER (PARTITION BY f.pool_id, date_trunc('day', to_timestamp(f.checkpoint_timestamp_ms / 1000.0))) as first_trade_timestamp, + MAX(f.checkpoint_timestamp_ms) + OVER (PARTITION BY f.pool_id, date_trunc('day', to_timestamp(f.checkpoint_timestamp_ms / 1000.0))) as last_trade_timestamp + FROM order_fills f + INNER JOIN pools p ON f.pool_id = p.pool_id + WHERE f.checkpoint_timestamp_ms >= start_timestamp + AND f.checkpoint_timestamp_ms <= end_timestamp + ON CONFLICT (pool_id, bucket_time) + DO UPDATE SET + high = GREATEST(EXCLUDED.high, ohclv_1d.high), + low = LEAST(EXCLUDED.low, ohclv_1d.low), + close = EXCLUDED.close, + base_volume = EXCLUDED.base_volume, + quote_volume = EXCLUDED.quote_volume, + trade_count = EXCLUDED.trade_count, + first_trade_timestamp = LEAST(EXCLUDED.first_trade_timestamp, ohclv_1d.first_trade_timestamp), + last_trade_timestamp = GREATEST(EXCLUDED.last_trade_timestamp, ohclv_1d.last_trade_timestamp); +END; +$$; + +-- combine intervals +CREATE OR REPLACE FUNCTION get_ohclv( + p_interval TEXT, + p_pool_id TEXT DEFAULT NULL, + p_start_time TIMESTAMP DEFAULT NULL, + p_end_time TIMESTAMP DEFAULT NULL, + p_limit INTEGER DEFAULT 1000 +) +RETURNS TABLE ( + pool_id TEXT, + bucket_time TIMESTAMP, + open NUMERIC, + high NUMERIC, + low NUMERIC, + close NUMERIC, + base_volume NUMERIC, + quote_volume NUMERIC, + trade_count INTEGER, + first_trade_timestamp BIGINT, + last_trade_timestamp BIGINT +) +LANGUAGE plpgsql +AS $$ +BEGIN + IF p_start_time IS NULL THEN + p_start_time := NOW() - INTERVAL '7 days'; + END IF; + + IF p_end_time IS NULL THEN + p_end_time := NOW(); + END IF; + + CASE p_interval + WHEN '1m' THEN + RETURN QUERY + SELECT + o.pool_id, o.bucket_time::TIMESTAMP, o.open, o.high, o.low, o.close, + o.base_volume, o.quote_volume, o.trade_count, + o.first_trade_timestamp, o.last_trade_timestamp + FROM ohclv_1m o + WHERE (p_pool_id IS NULL OR o.pool_id = p_pool_id) + AND o.bucket_time >= p_start_time + AND o.bucket_time <= p_end_time + ORDER BY o.bucket_time DESC + LIMIT p_limit; + + WHEN '5m' THEN + RETURN QUERY + SELECT + o.pool_id, + date_trunc('hour', o.bucket_time) + INTERVAL '5 minutes' * FLOOR(EXTRACT(minute FROM o.bucket_time) / 5) as bucket_time, + (array_agg(o.open ORDER BY o.bucket_time))[1] as open, + MAX(o.high) as high, + MIN(o.low) as low, + (array_agg(o.close ORDER BY o.bucket_time DESC))[1] as close, + SUM(o.base_volume) as base_volume, + SUM(o.quote_volume) as quote_volume, + SUM(o.trade_count)::INTEGER as trade_count, + MIN(o.first_trade_timestamp) as first_trade_timestamp, + MAX(o.last_trade_timestamp) as last_trade_timestamp + FROM ohclv_1m o + WHERE (p_pool_id IS NULL OR o.pool_id = p_pool_id) + AND o.bucket_time >= p_start_time + AND o.bucket_time <= p_end_time + GROUP BY o.pool_id, date_trunc('hour', o.bucket_time) + INTERVAL '5 minutes' * FLOOR(EXTRACT(minute FROM o.bucket_time) / 5) + ORDER BY bucket_time DESC + LIMIT p_limit; + + WHEN '15m' THEN + RETURN QUERY + SELECT + o.pool_id, + date_trunc('hour', o.bucket_time) + INTERVAL '15 minutes' * FLOOR(EXTRACT(minute FROM o.bucket_time) / 15) as bucket_time, + (array_agg(o.open ORDER BY o.bucket_time))[1] as open, + MAX(o.high) as high, + MIN(o.low) as low, + (array_agg(o.close ORDER BY o.bucket_time DESC))[1] as close, + SUM(o.base_volume) as base_volume, + SUM(o.quote_volume) as quote_volume, + SUM(o.trade_count)::INTEGER as trade_count, + MIN(o.first_trade_timestamp) as first_trade_timestamp, + MAX(o.last_trade_timestamp) as last_trade_timestamp + FROM ohclv_1m o + WHERE (p_pool_id IS NULL OR o.pool_id = p_pool_id) + AND o.bucket_time >= p_start_time + AND o.bucket_time <= p_end_time + GROUP BY o.pool_id, date_trunc('hour', o.bucket_time) + INTERVAL '15 minutes' * FLOOR(EXTRACT(minute FROM o.bucket_time) / 15) + ORDER BY bucket_time DESC + LIMIT p_limit; + + WHEN '30m' THEN + RETURN QUERY + SELECT + o.pool_id, + date_trunc('hour', o.bucket_time) + INTERVAL '30 minutes' * FLOOR(EXTRACT(minute FROM o.bucket_time) / 30) as bucket_time, + (array_agg(o.open ORDER BY o.bucket_time))[1] as open, + MAX(o.high) as high, + MIN(o.low) as low, + (array_agg(o.close ORDER BY o.bucket_time DESC))[1] as close, + SUM(o.base_volume) as base_volume, + SUM(o.quote_volume) as quote_volume, + SUM(o.trade_count)::INTEGER as trade_count, + MIN(o.first_trade_timestamp) as first_trade_timestamp, + MAX(o.last_trade_timestamp) as last_trade_timestamp + FROM ohclv_1m o + WHERE (p_pool_id IS NULL OR o.pool_id = p_pool_id) + AND o.bucket_time >= p_start_time + AND o.bucket_time <= p_end_time + GROUP BY o.pool_id, date_trunc('hour', o.bucket_time) + INTERVAL '30 minutes' * FLOOR(EXTRACT(minute FROM o.bucket_time) / 30) + ORDER BY bucket_time DESC + LIMIT p_limit; + + WHEN '1h' THEN + RETURN QUERY + SELECT + o.pool_id, + date_trunc('hour', o.bucket_time) as bucket_time, + (array_agg(o.open ORDER BY o.bucket_time))[1] as open, + MAX(o.high) as high, + MIN(o.low) as low, + (array_agg(o.close ORDER BY o.bucket_time DESC))[1] as close, + SUM(o.base_volume) as base_volume, + SUM(o.quote_volume) as quote_volume, + SUM(o.trade_count)::INTEGER as trade_count, + MIN(o.first_trade_timestamp) as first_trade_timestamp, + MAX(o.last_trade_timestamp) as last_trade_timestamp + FROM ohclv_1m o + WHERE (p_pool_id IS NULL OR o.pool_id = p_pool_id) + AND o.bucket_time >= p_start_time + AND o.bucket_time <= p_end_time + GROUP BY o.pool_id, date_trunc('hour', o.bucket_time) + ORDER BY bucket_time DESC + LIMIT p_limit; + + WHEN '4h' THEN + RETURN QUERY + SELECT + o.pool_id, + date_trunc('day', o.bucket_time) + INTERVAL '4 hours' * FLOOR(EXTRACT(hour FROM o.bucket_time) / 4) as bucket_time, + (array_agg(o.open ORDER BY o.bucket_time))[1] as open, + MAX(o.high) as high, + MIN(o.low) as low, + (array_agg(o.close ORDER BY o.bucket_time DESC))[1] as close, + SUM(o.base_volume) as base_volume, + SUM(o.quote_volume) as quote_volume, + SUM(o.trade_count)::INTEGER as trade_count, + MIN(o.first_trade_timestamp) as first_trade_timestamp, + MAX(o.last_trade_timestamp) as last_trade_timestamp + FROM ohclv_1m o + WHERE (p_pool_id IS NULL OR o.pool_id = p_pool_id) + AND o.bucket_time >= p_start_time + AND o.bucket_time <= p_end_time + GROUP BY o.pool_id, date_trunc('day', o.bucket_time) + INTERVAL '4 hours' * FLOOR(EXTRACT(hour FROM o.bucket_time) / 4) + ORDER BY bucket_time DESC + LIMIT p_limit; + + WHEN '1d' THEN + RETURN QUERY + SELECT + o.pool_id, o.bucket_time::TIMESTAMP, o.open, o.high, o.low, o.close, + o.base_volume, o.quote_volume, o.trade_count, + o.first_trade_timestamp, o.last_trade_timestamp + FROM ohclv_1d o + WHERE (p_pool_id IS NULL OR o.pool_id = p_pool_id) + AND o.bucket_time >= p_start_time::DATE + AND o.bucket_time <= p_end_time::DATE + ORDER BY o.bucket_time DESC + LIMIT p_limit; + + WHEN '1w' THEN + RETURN QUERY + SELECT + o.pool_id, + date_trunc('week', o.bucket_time)::TIMESTAMP as bucket_time, + (array_agg(o.open ORDER BY o.bucket_time))[1] as open, + MAX(o.high) as high, + MIN(o.low) as low, + (array_agg(o.close ORDER BY o.bucket_time DESC))[1] as close, + SUM(o.base_volume) as base_volume, + SUM(o.quote_volume) as quote_volume, + SUM(o.trade_count)::INTEGER as trade_count, + MIN(o.first_trade_timestamp) as first_trade_timestamp, + MAX(o.last_trade_timestamp) as last_trade_timestamp + FROM ohclv_1d o + WHERE (p_pool_id IS NULL OR o.pool_id = p_pool_id) + AND o.bucket_time >= p_start_time::DATE + AND o.bucket_time <= p_end_time::DATE + GROUP BY o.pool_id, date_trunc('week', o.bucket_time) + ORDER BY bucket_time DESC + LIMIT p_limit; + + ELSE + RAISE EXCEPTION 'Invalid interval: %. Valid intervals are: 1m, 5m, 15m, 30m, 1h, 4h, 1d, 1w', p_interval; + END CASE; +END; +$$; + +CREATE OR REPLACE PROCEDURE update_all_ohclv( + start_timestamp BIGINT DEFAULT NULL, + end_timestamp BIGINT DEFAULT NULL +) +LANGUAGE plpgsql +AS $$ +BEGIN + CALL update_ohclv_1m(start_timestamp, end_timestamp); + CALL update_ohclv_1d(start_timestamp, end_timestamp); +END; +$$; + +-- Examples +-- SELECT * FROM get_ohclv('5m', 'pool_123', NOW() - INTERVAL '1 day', NOW(), 100); +-- SELECT * FROM get_ohclv('1h', NULL, NULL, NULL, 500); -- All pools, last 7 days, 500 candles \ No newline at end of file diff --git a/crates/schema/migrations/2025-10-13-194059-0000_fix_ohclv_price_calculation/down.sql b/crates/schema/migrations/2025-10-13-194059-0000_fix_ohclv_price_calculation/down.sql new file mode 100644 index 000000000..9487219f7 --- /dev/null +++ b/crates/schema/migrations/2025-10-13-194059-0000_fix_ohclv_price_calculation/down.sql @@ -0,0 +1 @@ +-- Rollback not needed for this migration as it only fixes the calculation formula diff --git a/crates/schema/migrations/2025-10-13-194059-0000_fix_ohclv_price_calculation/up.sql b/crates/schema/migrations/2025-10-13-194059-0000_fix_ohclv_price_calculation/up.sql new file mode 100644 index 000000000..36b4092c6 --- /dev/null +++ b/crates/schema/migrations/2025-10-13-194059-0000_fix_ohclv_price_calculation/up.sql @@ -0,0 +1,149 @@ +-- Fix OHCLV price calculation to use correct formula +-- Correct formula: price_human = price_onchain / 10^(9 - base_decimals + quote_decimals) + +CREATE OR REPLACE PROCEDURE update_ohclv_1m( + start_timestamp BIGINT DEFAULT NULL, + end_timestamp BIGINT DEFAULT NULL +) +LANGUAGE plpgsql +AS $$ +BEGIN + -- Default to last 24 hours if no range specified + IF start_timestamp IS NULL THEN + start_timestamp := (EXTRACT(EPOCH FROM NOW() - INTERVAL '24 hours') * 1000)::BIGINT; + END IF; + + IF end_timestamp IS NULL THEN + end_timestamp := (EXTRACT(EPOCH FROM NOW()) * 1000)::BIGINT; + END IF; + + INSERT INTO ohclv_1m ( + pool_id, + bucket_time, + open, + high, + low, + close, + base_volume, + quote_volume, + trade_count, + first_trade_timestamp, + last_trade_timestamp + ) + SELECT DISTINCT ON (pool_id, bucket_time) + f.pool_id, + date_trunc('minute', to_timestamp(f.checkpoint_timestamp_ms / 1000.0)) as bucket_time, + FIRST_VALUE(f.price::numeric / POWER(10, 9 - p.base_asset_decimals + p.quote_asset_decimals)) + OVER (PARTITION BY f.pool_id, date_trunc('minute', to_timestamp(f.checkpoint_timestamp_ms / 1000.0)) + ORDER BY f.checkpoint_timestamp_ms + ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING) as open, + MAX(f.price::numeric / POWER(10, 9 - p.base_asset_decimals + p.quote_asset_decimals)) + OVER (PARTITION BY f.pool_id, date_trunc('minute', to_timestamp(f.checkpoint_timestamp_ms / 1000.0))) as high, + MIN(f.price::numeric / POWER(10, 9 - p.base_asset_decimals + p.quote_asset_decimals)) + OVER (PARTITION BY f.pool_id, date_trunc('minute', to_timestamp(f.checkpoint_timestamp_ms / 1000.0))) as low, + LAST_VALUE(f.price::numeric / POWER(10, 9 - p.base_asset_decimals + p.quote_asset_decimals)) + OVER (PARTITION BY f.pool_id, date_trunc('minute', to_timestamp(f.checkpoint_timestamp_ms / 1000.0)) + ORDER BY f.checkpoint_timestamp_ms + ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING) as close, + SUM(f.base_quantity::numeric / POWER(10, p.base_asset_decimals)) + OVER (PARTITION BY f.pool_id, date_trunc('minute', to_timestamp(f.checkpoint_timestamp_ms / 1000.0))) as base_volume, + SUM(f.quote_quantity::numeric / POWER(10, p.quote_asset_decimals)) + OVER (PARTITION BY f.pool_id, date_trunc('minute', to_timestamp(f.checkpoint_timestamp_ms / 1000.0))) as quote_volume, + COUNT(*) + OVER (PARTITION BY f.pool_id, date_trunc('minute', to_timestamp(f.checkpoint_timestamp_ms / 1000.0))) as trade_count, + MIN(f.checkpoint_timestamp_ms) + OVER (PARTITION BY f.pool_id, date_trunc('minute', to_timestamp(f.checkpoint_timestamp_ms / 1000.0))) as first_trade_timestamp, + MAX(f.checkpoint_timestamp_ms) + OVER (PARTITION BY f.pool_id, date_trunc('minute', to_timestamp(f.checkpoint_timestamp_ms / 1000.0))) as last_trade_timestamp + FROM order_fills f + INNER JOIN pools p ON f.pool_id = p.pool_id + WHERE f.checkpoint_timestamp_ms >= start_timestamp + AND f.checkpoint_timestamp_ms <= end_timestamp + ON CONFLICT (pool_id, bucket_time) + DO UPDATE SET + high = GREATEST(EXCLUDED.high, ohclv_1m.high), + low = LEAST(EXCLUDED.low, ohclv_1m.low), + close = EXCLUDED.close, -- Latest close wins + base_volume = EXCLUDED.base_volume, -- For simplicity, replace volume + quote_volume = EXCLUDED.quote_volume, + trade_count = EXCLUDED.trade_count, + first_trade_timestamp = LEAST(EXCLUDED.first_trade_timestamp, ohclv_1m.first_trade_timestamp), + last_trade_timestamp = GREATEST(EXCLUDED.last_trade_timestamp, ohclv_1m.last_trade_timestamp); +END; +$$; + +CREATE OR REPLACE PROCEDURE update_ohclv_1d( + start_timestamp BIGINT DEFAULT NULL, + end_timestamp BIGINT DEFAULT NULL +) +LANGUAGE plpgsql +AS $$ +BEGIN + -- Default to last 7 days if no range specified + IF start_timestamp IS NULL THEN + start_timestamp := (EXTRACT(EPOCH FROM NOW() - INTERVAL '7 days') * 1000)::BIGINT; + END IF; + + IF end_timestamp IS NULL THEN + end_timestamp := (EXTRACT(EPOCH FROM NOW()) * 1000)::BIGINT; + END IF; + + INSERT INTO ohclv_1d ( + pool_id, + bucket_time, + open, + high, + low, + close, + base_volume, + quote_volume, + trade_count, + first_trade_timestamp, + last_trade_timestamp + ) + SELECT DISTINCT ON (pool_id, bucket_time) + f.pool_id, + date_trunc('day', to_timestamp(f.checkpoint_timestamp_ms / 1000.0))::DATE as bucket_time, + FIRST_VALUE(f.price::numeric / POWER(10, 9 - p.base_asset_decimals + p.quote_asset_decimals)) + OVER (PARTITION BY f.pool_id, date_trunc('day', to_timestamp(f.checkpoint_timestamp_ms / 1000.0)) + ORDER BY f.checkpoint_timestamp_ms + ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING) as open, + MAX(f.price::numeric / POWER(10, 9 - p.base_asset_decimals + p.quote_asset_decimals)) + OVER (PARTITION BY f.pool_id, date_trunc('day', to_timestamp(f.checkpoint_timestamp_ms / 1000.0))) as high, + MIN(f.price::numeric / POWER(10, 9 - p.base_asset_decimals + p.quote_asset_decimals)) + OVER (PARTITION BY f.pool_id, date_trunc('day', to_timestamp(f.checkpoint_timestamp_ms / 1000.0))) as low, + LAST_VALUE(f.price::numeric / POWER(10, 9 - p.base_asset_decimals + p.quote_asset_decimals)) + OVER (PARTITION BY f.pool_id, date_trunc('day', to_timestamp(f.checkpoint_timestamp_ms / 1000.0)) + ORDER BY f.checkpoint_timestamp_ms + ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING) as close, + SUM(f.base_quantity::numeric / POWER(10, p.base_asset_decimals)) + OVER (PARTITION BY f.pool_id, date_trunc('day', to_timestamp(f.checkpoint_timestamp_ms / 1000.0))) as base_volume, + SUM(f.quote_quantity::numeric / POWER(10, p.quote_asset_decimals)) + OVER (PARTITION BY f.pool_id, date_trunc('day', to_timestamp(f.checkpoint_timestamp_ms / 1000.0))) as quote_volume, + COUNT(*) + OVER (PARTITION BY f.pool_id, date_trunc('day', to_timestamp(f.checkpoint_timestamp_ms / 1000.0))) as trade_count, + MIN(f.checkpoint_timestamp_ms) + OVER (PARTITION BY f.pool_id, date_trunc('day', to_timestamp(f.checkpoint_timestamp_ms / 1000.0))) as first_trade_timestamp, + MAX(f.checkpoint_timestamp_ms) + OVER (PARTITION BY f.pool_id, date_trunc('day', to_timestamp(f.checkpoint_timestamp_ms / 1000.0))) as last_trade_timestamp + FROM order_fills f + INNER JOIN pools p ON f.pool_id = p.pool_id + WHERE f.checkpoint_timestamp_ms >= start_timestamp + AND f.checkpoint_timestamp_ms <= end_timestamp + ON CONFLICT (pool_id, bucket_time) + DO UPDATE SET + high = GREATEST(EXCLUDED.high, ohclv_1d.high), + low = LEAST(EXCLUDED.low, ohclv_1d.low), + close = EXCLUDED.close, + base_volume = EXCLUDED.base_volume, + quote_volume = EXCLUDED.quote_volume, + trade_count = EXCLUDED.trade_count, + first_trade_timestamp = LEAST(EXCLUDED.first_trade_timestamp, ohclv_1d.first_trade_timestamp), + last_trade_timestamp = GREATEST(EXCLUDED.last_trade_timestamp, ohclv_1d.last_trade_timestamp); +END; +$$; + +TRUNCATE TABLE ohclv_1m; +TRUNCATE TABLE ohclv_1d; +CALL update_ohclv_1m(NULL, NULL); +CALL update_ohclv_1d(NULL, NULL); diff --git a/crates/schema/migrations/2025-10-20-185028-0000_add_individual_margin_tables/down.sql b/crates/schema/migrations/2025-10-20-185028-0000_add_individual_margin_tables/down.sql new file mode 100644 index 000000000..2ba43b451 --- /dev/null +++ b/crates/schema/migrations/2025-10-20-185028-0000_add_individual_margin_tables/down.sql @@ -0,0 +1,16 @@ +-- Drop all individual margin tables + +DROP TABLE IF EXISTS deepbook_pool_config_updated; +DROP TABLE IF EXISTS deepbook_pool_updated_registry; +DROP TABLE IF EXISTS deepbook_pool_registered; +DROP TABLE IF EXISTS maintainer_cap_updated; +DROP TABLE IF EXISTS margin_pool_config_updated; +DROP TABLE IF EXISTS interest_params_updated; +DROP TABLE IF EXISTS deepbook_pool_updated; +DROP TABLE IF EXISTS margin_pool_created; +DROP TABLE IF EXISTS asset_withdrawn; +DROP TABLE IF EXISTS asset_supplied; +DROP TABLE IF EXISTS liquidation; +DROP TABLE IF EXISTS loan_repaid; +DROP TABLE IF EXISTS loan_borrowed; +DROP TABLE IF EXISTS margin_manager_created; \ No newline at end of file diff --git a/crates/schema/migrations/2025-10-20-185028-0000_add_individual_margin_tables/up.sql b/crates/schema/migrations/2025-10-20-185028-0000_add_individual_margin_tables/up.sql new file mode 100644 index 000000000..efb976559 --- /dev/null +++ b/crates/schema/migrations/2025-10-20-185028-0000_add_individual_margin_tables/up.sql @@ -0,0 +1,202 @@ +CREATE TABLE margin_manager_created ( + event_digest TEXT PRIMARY KEY, + digest TEXT NOT NULL, + sender TEXT NOT NULL, + checkpoint BIGINT NOT NULL, + timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL, + checkpoint_timestamp_ms BIGINT NOT NULL, + package TEXT NOT NULL, + margin_manager_id TEXT NOT NULL, + balance_manager_id TEXT NOT NULL, + owner TEXT NOT NULL, + onchain_timestamp BIGINT NOT NULL +); + +CREATE TABLE loan_borrowed ( + event_digest TEXT PRIMARY KEY, + digest TEXT NOT NULL, + sender TEXT NOT NULL, + checkpoint BIGINT NOT NULL, + timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL, + checkpoint_timestamp_ms BIGINT NOT NULL, + package TEXT NOT NULL, + margin_manager_id TEXT NOT NULL, + margin_pool_id TEXT NOT NULL, + loan_amount BIGINT NOT NULL, + total_borrow BIGINT NOT NULL, + total_shares BIGINT NOT NULL, + onchain_timestamp BIGINT NOT NULL +); + +CREATE TABLE loan_repaid ( + event_digest TEXT PRIMARY KEY, + digest TEXT NOT NULL, + sender TEXT NOT NULL, + checkpoint BIGINT NOT NULL, + timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL, + checkpoint_timestamp_ms BIGINT NOT NULL, + package TEXT NOT NULL, + margin_manager_id TEXT NOT NULL, + margin_pool_id TEXT NOT NULL, + repay_amount BIGINT NOT NULL, + repay_shares BIGINT NOT NULL, + onchain_timestamp BIGINT NOT NULL +); + +CREATE TABLE liquidation ( + event_digest TEXT PRIMARY KEY, + digest TEXT NOT NULL, + sender TEXT NOT NULL, + checkpoint BIGINT NOT NULL, + timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL, + checkpoint_timestamp_ms BIGINT NOT NULL, + package TEXT NOT NULL, + margin_manager_id TEXT NOT NULL, + margin_pool_id TEXT NOT NULL, + liquidation_amount BIGINT NOT NULL, + pool_reward BIGINT NOT NULL, + pool_default BIGINT NOT NULL, + risk_ratio BIGINT NOT NULL, + onchain_timestamp BIGINT NOT NULL +); + +CREATE TABLE asset_supplied ( + event_digest TEXT PRIMARY KEY, + digest TEXT NOT NULL, + sender TEXT NOT NULL, + checkpoint BIGINT NOT NULL, + timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL, + checkpoint_timestamp_ms BIGINT NOT NULL, + package TEXT NOT NULL, + margin_pool_id TEXT NOT NULL, + asset_type TEXT NOT NULL, + supplier TEXT NOT NULL, + amount BIGINT NOT NULL, + shares BIGINT NOT NULL, + onchain_timestamp BIGINT NOT NULL +); + +CREATE TABLE asset_withdrawn ( + event_digest TEXT PRIMARY KEY, + digest TEXT NOT NULL, + sender TEXT NOT NULL, + checkpoint BIGINT NOT NULL, + timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL, + checkpoint_timestamp_ms BIGINT NOT NULL, + package TEXT NOT NULL, + margin_pool_id TEXT NOT NULL, + asset_type TEXT NOT NULL, + supplier TEXT NOT NULL, + amount BIGINT NOT NULL, + shares BIGINT NOT NULL, + onchain_timestamp BIGINT NOT NULL +); + +CREATE TABLE margin_pool_created ( + event_digest TEXT PRIMARY KEY, + digest TEXT NOT NULL, + sender TEXT NOT NULL, + checkpoint BIGINT NOT NULL, + timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL, + checkpoint_timestamp_ms BIGINT NOT NULL, + package TEXT NOT NULL, + margin_pool_id TEXT NOT NULL, + maintainer_cap_id TEXT NOT NULL, + asset_type TEXT NOT NULL, + config_json JSONB NOT NULL, + onchain_timestamp BIGINT NOT NULL +); + +CREATE TABLE deepbook_pool_updated ( + event_digest TEXT PRIMARY KEY, + digest TEXT NOT NULL, + sender TEXT NOT NULL, + checkpoint BIGINT NOT NULL, + timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL, + checkpoint_timestamp_ms BIGINT NOT NULL, + package TEXT NOT NULL, + margin_pool_id TEXT NOT NULL, + deepbook_pool_id TEXT NOT NULL, + pool_cap_id TEXT NOT NULL, + enabled BOOLEAN NOT NULL, + onchain_timestamp BIGINT NOT NULL +); + +CREATE TABLE interest_params_updated ( + event_digest TEXT PRIMARY KEY, + digest TEXT NOT NULL, + sender TEXT NOT NULL, + checkpoint BIGINT NOT NULL, + timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL, + checkpoint_timestamp_ms BIGINT NOT NULL, + package TEXT NOT NULL, + margin_pool_id TEXT NOT NULL, + pool_cap_id TEXT NOT NULL, + config_json JSONB NOT NULL, + onchain_timestamp BIGINT NOT NULL +); + +CREATE TABLE margin_pool_config_updated ( + event_digest TEXT PRIMARY KEY, + digest TEXT NOT NULL, + sender TEXT NOT NULL, + checkpoint BIGINT NOT NULL, + timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL, + checkpoint_timestamp_ms BIGINT NOT NULL, + package TEXT NOT NULL, + margin_pool_id TEXT NOT NULL, + pool_cap_id TEXT NOT NULL, + config_json JSONB NOT NULL, + onchain_timestamp BIGINT NOT NULL +); + +CREATE TABLE maintainer_cap_updated ( + event_digest TEXT PRIMARY KEY, + digest TEXT NOT NULL, + sender TEXT NOT NULL, + checkpoint BIGINT NOT NULL, + timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL, + checkpoint_timestamp_ms BIGINT NOT NULL, + package TEXT NOT NULL, + maintainer_cap_id TEXT NOT NULL, + allowed BOOLEAN NOT NULL, + onchain_timestamp BIGINT NOT NULL +); + +CREATE TABLE deepbook_pool_registered ( + event_digest TEXT PRIMARY KEY, + digest TEXT NOT NULL, + sender TEXT NOT NULL, + checkpoint BIGINT NOT NULL, + timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL, + checkpoint_timestamp_ms BIGINT NOT NULL, + package TEXT NOT NULL, + pool_id TEXT NOT NULL, + onchain_timestamp BIGINT NOT NULL +); + +CREATE TABLE deepbook_pool_updated_registry ( + event_digest TEXT PRIMARY KEY, + digest TEXT NOT NULL, + sender TEXT NOT NULL, + checkpoint BIGINT NOT NULL, + timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL, + checkpoint_timestamp_ms BIGINT NOT NULL, + package TEXT NOT NULL, + pool_id TEXT NOT NULL, + enabled BOOLEAN NOT NULL, + onchain_timestamp BIGINT NOT NULL +); + +CREATE TABLE deepbook_pool_config_updated ( + event_digest TEXT PRIMARY KEY, + digest TEXT NOT NULL, + sender TEXT NOT NULL, + checkpoint BIGINT NOT NULL, + timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL, + checkpoint_timestamp_ms BIGINT NOT NULL, + package TEXT NOT NULL, + pool_id TEXT NOT NULL, + config_json JSONB NOT NULL, + onchain_timestamp BIGINT NOT NULL +); \ No newline at end of file diff --git a/crates/schema/migrations/2025-10-24-000000-0000_add_margin_table_indexes/down.sql b/crates/schema/migrations/2025-10-24-000000-0000_add_margin_table_indexes/down.sql new file mode 100644 index 000000000..d42bf7893 --- /dev/null +++ b/crates/schema/migrations/2025-10-24-000000-0000_add_margin_table_indexes/down.sql @@ -0,0 +1,66 @@ +-- Drop indexes for margin_manager_created table +DROP INDEX IF EXISTS idx_margin_manager_created_checkpoint_timestamp_ms; +DROP INDEX IF EXISTS idx_margin_manager_created_margin_manager_id; +DROP INDEX IF EXISTS idx_margin_manager_created_balance_manager_id; +DROP INDEX IF EXISTS idx_margin_manager_created_owner; + +-- Drop indexes for loan_borrowed table +DROP INDEX IF EXISTS idx_loan_borrowed_checkpoint_timestamp_ms; +DROP INDEX IF EXISTS idx_loan_borrowed_margin_manager_id; +DROP INDEX IF EXISTS idx_loan_borrowed_margin_pool_id; + +-- Drop indexes for loan_repaid table +DROP INDEX IF EXISTS idx_loan_repaid_checkpoint_timestamp_ms; +DROP INDEX IF EXISTS idx_loan_repaid_margin_manager_id; +DROP INDEX IF EXISTS idx_loan_repaid_margin_pool_id; + +-- Drop indexes for liquidation table +DROP INDEX IF EXISTS idx_liquidation_checkpoint_timestamp_ms; +DROP INDEX IF EXISTS idx_liquidation_margin_manager_id; +DROP INDEX IF EXISTS idx_liquidation_margin_pool_id; + +-- Drop indexes for asset_supplied table +DROP INDEX IF EXISTS idx_asset_supplied_checkpoint_timestamp_ms; +DROP INDEX IF EXISTS idx_asset_supplied_margin_pool_id; +DROP INDEX IF EXISTS idx_asset_supplied_supplier; +DROP INDEX IF EXISTS idx_asset_supplied_asset_type; + +-- Drop indexes for asset_withdrawn table +DROP INDEX IF EXISTS idx_asset_withdrawn_checkpoint_timestamp_ms; +DROP INDEX IF EXISTS idx_asset_withdrawn_margin_pool_id; +DROP INDEX IF EXISTS idx_asset_withdrawn_supplier; +DROP INDEX IF EXISTS idx_asset_withdrawn_asset_type; + +-- Drop indexes for margin_pool_created table +DROP INDEX IF EXISTS idx_margin_pool_created_checkpoint_timestamp_ms; +DROP INDEX IF EXISTS idx_margin_pool_created_margin_pool_id; +DROP INDEX IF EXISTS idx_margin_pool_created_asset_type; + +-- Drop indexes for deepbook_pool_updated table +DROP INDEX IF EXISTS idx_deepbook_pool_updated_checkpoint_timestamp_ms; +DROP INDEX IF EXISTS idx_deepbook_pool_updated_margin_pool_id; +DROP INDEX IF EXISTS idx_deepbook_pool_updated_deepbook_pool_id; + +-- Drop indexes for interest_params_updated table +DROP INDEX IF EXISTS idx_interest_params_updated_checkpoint_timestamp_ms; +DROP INDEX IF EXISTS idx_interest_params_updated_margin_pool_id; + +-- Drop indexes for margin_pool_config_updated table +DROP INDEX IF EXISTS idx_margin_pool_config_updated_checkpoint_timestamp_ms; +DROP INDEX IF EXISTS idx_margin_pool_config_updated_margin_pool_id; + +-- Drop indexes for maintainer_cap_updated table +DROP INDEX IF EXISTS idx_maintainer_cap_updated_checkpoint_timestamp_ms; +DROP INDEX IF EXISTS idx_maintainer_cap_updated_maintainer_cap_id; + +-- Drop indexes for deepbook_pool_registered table +DROP INDEX IF EXISTS idx_deepbook_pool_registered_checkpoint_timestamp_ms; +DROP INDEX IF EXISTS idx_deepbook_pool_registered_pool_id; + +-- Drop indexes for deepbook_pool_updated_registry table +DROP INDEX IF EXISTS idx_deepbook_pool_updated_registry_checkpoint_timestamp_ms; +DROP INDEX IF EXISTS idx_deepbook_pool_updated_registry_pool_id; + +-- Drop indexes for deepbook_pool_config_updated table +DROP INDEX IF EXISTS idx_deepbook_pool_config_updated_checkpoint_timestamp_ms; +DROP INDEX IF EXISTS idx_deepbook_pool_config_updated_pool_id; diff --git a/crates/schema/migrations/2025-10-24-000000-0000_add_margin_table_indexes/up.sql b/crates/schema/migrations/2025-10-24-000000-0000_add_margin_table_indexes/up.sql new file mode 100644 index 000000000..0e54d9128 --- /dev/null +++ b/crates/schema/migrations/2025-10-24-000000-0000_add_margin_table_indexes/up.sql @@ -0,0 +1,130 @@ +-- Indexes for margin_manager_created table +CREATE INDEX IF NOT EXISTS idx_margin_manager_created_checkpoint_timestamp_ms + ON margin_manager_created (checkpoint_timestamp_ms); + +CREATE INDEX IF NOT EXISTS idx_margin_manager_created_margin_manager_id + ON margin_manager_created (margin_manager_id); + +CREATE INDEX IF NOT EXISTS idx_margin_manager_created_balance_manager_id + ON margin_manager_created (balance_manager_id); + +CREATE INDEX IF NOT EXISTS idx_margin_manager_created_owner + ON margin_manager_created (owner); + +-- Indexes for loan_borrowed table +CREATE INDEX IF NOT EXISTS idx_loan_borrowed_checkpoint_timestamp_ms + ON loan_borrowed (checkpoint_timestamp_ms); + +CREATE INDEX IF NOT EXISTS idx_loan_borrowed_margin_manager_id + ON loan_borrowed (margin_manager_id); + +CREATE INDEX IF NOT EXISTS idx_loan_borrowed_margin_pool_id + ON loan_borrowed (margin_pool_id); + +-- Indexes for loan_repaid table +CREATE INDEX IF NOT EXISTS idx_loan_repaid_checkpoint_timestamp_ms + ON loan_repaid (checkpoint_timestamp_ms); + +CREATE INDEX IF NOT EXISTS idx_loan_repaid_margin_manager_id + ON loan_repaid (margin_manager_id); + +CREATE INDEX IF NOT EXISTS idx_loan_repaid_margin_pool_id + ON loan_repaid (margin_pool_id); + +-- Indexes for liquidation table +CREATE INDEX IF NOT EXISTS idx_liquidation_checkpoint_timestamp_ms + ON liquidation (checkpoint_timestamp_ms); + +CREATE INDEX IF NOT EXISTS idx_liquidation_margin_manager_id + ON liquidation (margin_manager_id); + +CREATE INDEX IF NOT EXISTS idx_liquidation_margin_pool_id + ON liquidation (margin_pool_id); + +-- Indexes for asset_supplied table +CREATE INDEX IF NOT EXISTS idx_asset_supplied_checkpoint_timestamp_ms + ON asset_supplied (checkpoint_timestamp_ms); + +CREATE INDEX IF NOT EXISTS idx_asset_supplied_margin_pool_id + ON asset_supplied (margin_pool_id); + +CREATE INDEX IF NOT EXISTS idx_asset_supplied_supplier + ON asset_supplied (supplier); + +CREATE INDEX IF NOT EXISTS idx_asset_supplied_asset_type + ON asset_supplied (asset_type); + +-- Indexes for asset_withdrawn table +CREATE INDEX IF NOT EXISTS idx_asset_withdrawn_checkpoint_timestamp_ms + ON asset_withdrawn (checkpoint_timestamp_ms); + +CREATE INDEX IF NOT EXISTS idx_asset_withdrawn_margin_pool_id + ON asset_withdrawn (margin_pool_id); + +CREATE INDEX IF NOT EXISTS idx_asset_withdrawn_supplier + ON asset_withdrawn (supplier); + +CREATE INDEX IF NOT EXISTS idx_asset_withdrawn_asset_type + ON asset_withdrawn (asset_type); + +-- Indexes for margin_pool_created table +CREATE INDEX IF NOT EXISTS idx_margin_pool_created_checkpoint_timestamp_ms + ON margin_pool_created (checkpoint_timestamp_ms); + +CREATE INDEX IF NOT EXISTS idx_margin_pool_created_margin_pool_id + ON margin_pool_created (margin_pool_id); + +CREATE INDEX IF NOT EXISTS idx_margin_pool_created_asset_type + ON margin_pool_created (asset_type); + +-- Indexes for deepbook_pool_updated table +CREATE INDEX IF NOT EXISTS idx_deepbook_pool_updated_checkpoint_timestamp_ms + ON deepbook_pool_updated (checkpoint_timestamp_ms); + +CREATE INDEX IF NOT EXISTS idx_deepbook_pool_updated_margin_pool_id + ON deepbook_pool_updated (margin_pool_id); + +CREATE INDEX IF NOT EXISTS idx_deepbook_pool_updated_deepbook_pool_id + ON deepbook_pool_updated (deepbook_pool_id); + +-- Indexes for interest_params_updated table +CREATE INDEX IF NOT EXISTS idx_interest_params_updated_checkpoint_timestamp_ms + ON interest_params_updated (checkpoint_timestamp_ms); + +CREATE INDEX IF NOT EXISTS idx_interest_params_updated_margin_pool_id + ON interest_params_updated (margin_pool_id); + +-- Indexes for margin_pool_config_updated table +CREATE INDEX IF NOT EXISTS idx_margin_pool_config_updated_checkpoint_timestamp_ms + ON margin_pool_config_updated (checkpoint_timestamp_ms); + +CREATE INDEX IF NOT EXISTS idx_margin_pool_config_updated_margin_pool_id + ON margin_pool_config_updated (margin_pool_id); + +-- Indexes for maintainer_cap_updated table +CREATE INDEX IF NOT EXISTS idx_maintainer_cap_updated_checkpoint_timestamp_ms + ON maintainer_cap_updated (checkpoint_timestamp_ms); + +CREATE INDEX IF NOT EXISTS idx_maintainer_cap_updated_maintainer_cap_id + ON maintainer_cap_updated (maintainer_cap_id); + +-- Indexes for deepbook_pool_registered table +CREATE INDEX IF NOT EXISTS idx_deepbook_pool_registered_checkpoint_timestamp_ms + ON deepbook_pool_registered (checkpoint_timestamp_ms); + +CREATE INDEX IF NOT EXISTS idx_deepbook_pool_registered_pool_id + ON deepbook_pool_registered (pool_id); + +-- Indexes for deepbook_pool_updated_registry table +CREATE INDEX IF NOT EXISTS idx_deepbook_pool_updated_registry_checkpoint_timestamp_ms + ON deepbook_pool_updated_registry (checkpoint_timestamp_ms); + +CREATE INDEX IF NOT EXISTS idx_deepbook_pool_updated_registry_pool_id + ON deepbook_pool_updated_registry (pool_id); + +-- Indexes for deepbook_pool_config_updated table +CREATE INDEX IF NOT EXISTS idx_deepbook_pool_config_updated_checkpoint_timestamp_ms + ON deepbook_pool_config_updated (checkpoint_timestamp_ms); + +CREATE INDEX IF NOT EXISTS idx_deepbook_pool_config_updated_pool_id + ON deepbook_pool_config_updated (pool_id); diff --git a/crates/schema/migrations/2025-11-13-040002-0000_fix_loan_borrowed_schema/down.sql b/crates/schema/migrations/2025-11-13-040002-0000_fix_loan_borrowed_schema/down.sql new file mode 100644 index 000000000..b5e0331ff --- /dev/null +++ b/crates/schema/migrations/2025-11-13-040002-0000_fix_loan_borrowed_schema/down.sql @@ -0,0 +1,9 @@ +ALTER TABLE loan_borrowed DROP COLUMN IF EXISTS loan_shares; + +ALTER TABLE loan_borrowed ADD COLUMN total_borrow BIGINT NOT NULL DEFAULT 0; +ALTER TABLE loan_borrowed ADD COLUMN total_shares BIGINT NOT NULL DEFAULT 0; + +ALTER TABLE loan_borrowed ALTER COLUMN total_borrow DROP DEFAULT; +ALTER TABLE loan_borrowed ALTER COLUMN total_shares DROP DEFAULT; + +ALTER TABLE margin_manager_created DROP COLUMN IF EXISTS deepbook_pool_id; diff --git a/crates/schema/migrations/2025-11-13-040002-0000_fix_loan_borrowed_schema/up.sql b/crates/schema/migrations/2025-11-13-040002-0000_fix_loan_borrowed_schema/up.sql new file mode 100644 index 000000000..1390a0e44 --- /dev/null +++ b/crates/schema/migrations/2025-11-13-040002-0000_fix_loan_borrowed_schema/up.sql @@ -0,0 +1,8 @@ +ALTER TABLE loan_borrowed DROP COLUMN IF EXISTS total_borrow; +ALTER TABLE loan_borrowed DROP COLUMN IF EXISTS total_shares; + +ALTER TABLE loan_borrowed ADD COLUMN loan_shares BIGINT NOT NULL DEFAULT 0; + +ALTER TABLE loan_borrowed ALTER COLUMN loan_shares DROP DEFAULT; + +ALTER TABLE margin_manager_created ADD COLUMN deepbook_pool_id TEXT; diff --git a/crates/schema/migrations/2025-11-14-062636-0000_add_margin_manager_state/down.sql b/crates/schema/migrations/2025-11-14-062636-0000_add_margin_manager_state/down.sql new file mode 100644 index 000000000..79e274bea --- /dev/null +++ b/crates/schema/migrations/2025-11-14-062636-0000_add_margin_manager_state/down.sql @@ -0,0 +1 @@ +DROP TABLE IF EXISTS margin_manager_state; diff --git a/crates/schema/migrations/2025-11-14-062636-0000_add_margin_manager_state/up.sql b/crates/schema/migrations/2025-11-14-062636-0000_add_margin_manager_state/up.sql new file mode 100644 index 000000000..784a40b45 --- /dev/null +++ b/crates/schema/migrations/2025-11-14-062636-0000_add_margin_manager_state/up.sql @@ -0,0 +1,27 @@ +CREATE TABLE margin_manager_state ( + id SERIAL PRIMARY KEY, + margin_manager_id VARCHAR(66) NOT NULL, + deepbook_pool_id VARCHAR(66) NOT NULL, + base_margin_pool_id VARCHAR(66), + quote_margin_pool_id VARCHAR(66), + base_asset_id VARCHAR(255), + base_asset_symbol VARCHAR(50), + quote_asset_id VARCHAR(255), + quote_asset_symbol VARCHAR(50), + risk_ratio DECIMAL(20, 10), + base_asset DECIMAL(40, 20), + quote_asset DECIMAL(40, 20), + base_debt DECIMAL(40, 20), + quote_debt DECIMAL(40, 20), + base_pyth_price BIGINT, + base_pyth_decimals INTEGER, + quote_pyth_price BIGINT, + quote_pyth_decimals INTEGER, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +CREATE INDEX idx_margin_manager_state_manager_id ON margin_manager_state(margin_manager_id); +CREATE INDEX idx_margin_manager_state_deepbook_pool_id ON margin_manager_state(deepbook_pool_id); +CREATE INDEX idx_margin_manager_state_risk_ratio ON margin_manager_state(risk_ratio); +CREATE INDEX idx_margin_manager_state_updated_at ON margin_manager_state(updated_at); diff --git a/crates/schema/migrations/2025-11-15-194406-0000_add_missing_margin_events/down.sql b/crates/schema/migrations/2025-11-15-194406-0000_add_missing_margin_events/down.sql new file mode 100644 index 000000000..adce2b7d2 --- /dev/null +++ b/crates/schema/migrations/2025-11-15-194406-0000_add_missing_margin_events/down.sql @@ -0,0 +1,29 @@ +-- Drop indexes +DROP INDEX IF EXISTS idx_maintainer_fees_withdrawn_checkpoint_timestamp_ms; +DROP INDEX IF EXISTS idx_maintainer_fees_withdrawn_margin_pool_id; +DROP INDEX IF EXISTS idx_maintainer_fees_withdrawn_margin_pool_cap_id; +DROP INDEX IF EXISTS idx_protocol_fees_withdrawn_checkpoint_timestamp_ms; +DROP INDEX IF EXISTS idx_protocol_fees_withdrawn_margin_pool_id; +DROP INDEX IF EXISTS idx_supplier_cap_minted_checkpoint_timestamp_ms; +DROP INDEX IF EXISTS idx_supplier_cap_minted_supplier_cap_id; +DROP INDEX IF EXISTS idx_supply_referral_minted_checkpoint_timestamp_ms; +DROP INDEX IF EXISTS idx_supply_referral_minted_margin_pool_id; +DROP INDEX IF EXISTS idx_supply_referral_minted_owner; +DROP INDEX IF EXISTS idx_supply_referral_minted_supply_referral_id; +DROP INDEX IF EXISTS idx_pause_cap_updated_checkpoint_timestamp_ms; +DROP INDEX IF EXISTS idx_pause_cap_updated_pause_cap_id; +DROP INDEX IF EXISTS idx_protocol_fees_increased_checkpoint_timestamp_ms; +DROP INDEX IF EXISTS idx_protocol_fees_increased_margin_pool_id; +DROP INDEX IF EXISTS idx_referral_fees_claimed_checkpoint_timestamp_ms; +DROP INDEX IF EXISTS idx_referral_fees_claimed_referral_id; +DROP INDEX IF EXISTS idx_referral_fees_claimed_owner; + +-- Drop tables +DROP TABLE IF EXISTS maintainer_fees_withdrawn; +DROP TABLE IF EXISTS protocol_fees_withdrawn; +DROP TABLE IF EXISTS supplier_cap_minted; +DROP TABLE IF EXISTS supply_referral_minted; +DROP TABLE IF EXISTS pause_cap_updated; +DROP TABLE IF EXISTS protocol_fees_increased; +DROP TABLE IF EXISTS referral_fees_claimed; + diff --git a/crates/schema/migrations/2025-11-15-194406-0000_add_missing_margin_events/up.sql b/crates/schema/migrations/2025-11-15-194406-0000_add_missing_margin_events/up.sql new file mode 100644 index 000000000..431d59acf --- /dev/null +++ b/crates/schema/migrations/2025-11-15-194406-0000_add_missing_margin_events/up.sql @@ -0,0 +1,157 @@ +CREATE TABLE maintainer_fees_withdrawn ( + event_digest TEXT PRIMARY KEY, + digest TEXT NOT NULL, + sender TEXT NOT NULL, + checkpoint BIGINT NOT NULL, + timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL, + checkpoint_timestamp_ms BIGINT NOT NULL, + package TEXT NOT NULL, + margin_pool_id TEXT NOT NULL, + margin_pool_cap_id TEXT NOT NULL, + maintainer_fees BIGINT NOT NULL, + onchain_timestamp BIGINT NOT NULL +); + +CREATE TABLE protocol_fees_withdrawn ( + event_digest TEXT PRIMARY KEY, + digest TEXT NOT NULL, + sender TEXT NOT NULL, + checkpoint BIGINT NOT NULL, + timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL, + checkpoint_timestamp_ms BIGINT NOT NULL, + package TEXT NOT NULL, + margin_pool_id TEXT NOT NULL, + protocol_fees BIGINT NOT NULL, + onchain_timestamp BIGINT NOT NULL +); + +CREATE TABLE supplier_cap_minted ( + event_digest TEXT PRIMARY KEY, + digest TEXT NOT NULL, + sender TEXT NOT NULL, + checkpoint BIGINT NOT NULL, + timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL, + checkpoint_timestamp_ms BIGINT NOT NULL, + package TEXT NOT NULL, + supplier_cap_id TEXT NOT NULL, + onchain_timestamp BIGINT NOT NULL +); + +CREATE TABLE supply_referral_minted ( + event_digest TEXT PRIMARY KEY, + digest TEXT NOT NULL, + sender TEXT NOT NULL, + checkpoint BIGINT NOT NULL, + timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL, + checkpoint_timestamp_ms BIGINT NOT NULL, + package TEXT NOT NULL, + margin_pool_id TEXT NOT NULL, + supply_referral_id TEXT NOT NULL, + owner TEXT NOT NULL, + onchain_timestamp BIGINT NOT NULL +); + +CREATE TABLE pause_cap_updated ( + event_digest TEXT PRIMARY KEY, + digest TEXT NOT NULL, + sender TEXT NOT NULL, + checkpoint BIGINT NOT NULL, + timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL, + checkpoint_timestamp_ms BIGINT NOT NULL, + package TEXT NOT NULL, + pause_cap_id TEXT NOT NULL, + allowed BOOLEAN NOT NULL, + onchain_timestamp BIGINT NOT NULL +); + +CREATE TABLE protocol_fees_increased ( + event_digest TEXT PRIMARY KEY, + digest TEXT NOT NULL, + sender TEXT NOT NULL, + checkpoint BIGINT NOT NULL, + timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL, + checkpoint_timestamp_ms BIGINT NOT NULL, + package TEXT NOT NULL, + margin_pool_id TEXT NOT NULL, + total_shares BIGINT NOT NULL, + referral_fees BIGINT NOT NULL, + maintainer_fees BIGINT NOT NULL, + protocol_fees BIGINT NOT NULL, + onchain_timestamp BIGINT NOT NULL +); + +CREATE TABLE referral_fees_claimed ( + event_digest TEXT PRIMARY KEY, + digest TEXT NOT NULL, + sender TEXT NOT NULL, + checkpoint BIGINT NOT NULL, + timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL, + checkpoint_timestamp_ms BIGINT NOT NULL, + package TEXT NOT NULL, + referral_id TEXT NOT NULL, + owner TEXT NOT NULL, + fees BIGINT NOT NULL, + onchain_timestamp BIGINT NOT NULL +); + +-- Indexes for maintainer_fees_withdrawn table +CREATE INDEX IF NOT EXISTS idx_maintainer_fees_withdrawn_checkpoint_timestamp_ms + ON maintainer_fees_withdrawn (checkpoint_timestamp_ms); + +CREATE INDEX IF NOT EXISTS idx_maintainer_fees_withdrawn_margin_pool_id + ON maintainer_fees_withdrawn (margin_pool_id); + +CREATE INDEX IF NOT EXISTS idx_maintainer_fees_withdrawn_margin_pool_cap_id + ON maintainer_fees_withdrawn (margin_pool_cap_id); + +-- Indexes for protocol_fees_withdrawn table +CREATE INDEX IF NOT EXISTS idx_protocol_fees_withdrawn_checkpoint_timestamp_ms + ON protocol_fees_withdrawn (checkpoint_timestamp_ms); + +CREATE INDEX IF NOT EXISTS idx_protocol_fees_withdrawn_margin_pool_id + ON protocol_fees_withdrawn (margin_pool_id); + +-- Indexes for supplier_cap_minted table +CREATE INDEX IF NOT EXISTS idx_supplier_cap_minted_checkpoint_timestamp_ms + ON supplier_cap_minted (checkpoint_timestamp_ms); + +CREATE INDEX IF NOT EXISTS idx_supplier_cap_minted_supplier_cap_id + ON supplier_cap_minted (supplier_cap_id); + +-- Indexes for supply_referral_minted table +CREATE INDEX IF NOT EXISTS idx_supply_referral_minted_checkpoint_timestamp_ms + ON supply_referral_minted (checkpoint_timestamp_ms); + +CREATE INDEX IF NOT EXISTS idx_supply_referral_minted_margin_pool_id + ON supply_referral_minted (margin_pool_id); + +CREATE INDEX IF NOT EXISTS idx_supply_referral_minted_owner + ON supply_referral_minted (owner); + +CREATE INDEX IF NOT EXISTS idx_supply_referral_minted_supply_referral_id + ON supply_referral_minted (supply_referral_id); + +-- Indexes for pause_cap_updated table +CREATE INDEX IF NOT EXISTS idx_pause_cap_updated_checkpoint_timestamp_ms + ON pause_cap_updated (checkpoint_timestamp_ms); + +CREATE INDEX IF NOT EXISTS idx_pause_cap_updated_pause_cap_id + ON pause_cap_updated (pause_cap_id); + +-- Indexes for protocol_fees_increased table +CREATE INDEX IF NOT EXISTS idx_protocol_fees_increased_checkpoint_timestamp_ms + ON protocol_fees_increased (checkpoint_timestamp_ms); + +CREATE INDEX IF NOT EXISTS idx_protocol_fees_increased_margin_pool_id + ON protocol_fees_increased (margin_pool_id); + +-- Indexes for referral_fees_claimed table +CREATE INDEX IF NOT EXISTS idx_referral_fees_claimed_checkpoint_timestamp_ms + ON referral_fees_claimed (checkpoint_timestamp_ms); + +CREATE INDEX IF NOT EXISTS idx_referral_fees_claimed_referral_id + ON referral_fees_claimed (referral_id); + +CREATE INDEX IF NOT EXISTS idx_referral_fees_claimed_owner + ON referral_fees_claimed (owner); + diff --git a/crates/schema/migrations/2025-11-18-214608-0000_add_unique_constraint_margin_manager_state/down.sql b/crates/schema/migrations/2025-11-18-214608-0000_add_unique_constraint_margin_manager_state/down.sql new file mode 100644 index 000000000..d9ae34c97 --- /dev/null +++ b/crates/schema/migrations/2025-11-18-214608-0000_add_unique_constraint_margin_manager_state/down.sql @@ -0,0 +1,2 @@ +ALTER TABLE margin_manager_state +DROP CONSTRAINT unique_margin_manager_id; diff --git a/crates/schema/migrations/2025-11-18-214608-0000_add_unique_constraint_margin_manager_state/up.sql b/crates/schema/migrations/2025-11-18-214608-0000_add_unique_constraint_margin_manager_state/up.sql new file mode 100644 index 000000000..e5cc87a74 --- /dev/null +++ b/crates/schema/migrations/2025-11-18-214608-0000_add_unique_constraint_margin_manager_state/up.sql @@ -0,0 +1,3 @@ +ALTER TABLE margin_manager_state +ADD CONSTRAINT unique_margin_manager_id +UNIQUE (margin_manager_id); diff --git a/crates/schema/migrations/2025-11-23-202208-0000_add_config_to_deepbook_pool_registered/down.sql b/crates/schema/migrations/2025-11-23-202208-0000_add_config_to_deepbook_pool_registered/down.sql new file mode 100644 index 000000000..19d8de925 --- /dev/null +++ b/crates/schema/migrations/2025-11-23-202208-0000_add_config_to_deepbook_pool_registered/down.sql @@ -0,0 +1,3 @@ +-- Remove config_json column from deepbook_pool_registered table +ALTER TABLE deepbook_pool_registered DROP COLUMN IF EXISTS config_json; + diff --git a/crates/schema/migrations/2025-11-23-202208-0000_add_config_to_deepbook_pool_registered/up.sql b/crates/schema/migrations/2025-11-23-202208-0000_add_config_to_deepbook_pool_registered/up.sql new file mode 100644 index 000000000..48f45c801 --- /dev/null +++ b/crates/schema/migrations/2025-11-23-202208-0000_add_config_to_deepbook_pool_registered/up.sql @@ -0,0 +1,3 @@ +-- Add config_json column to deepbook_pool_registered table +ALTER TABLE deepbook_pool_registered ADD COLUMN config_json JSONB; + diff --git a/crates/schema/migrations/2025-12-10-164435-0000_add_price_trigger_fields/down.sql b/crates/schema/migrations/2025-12-10-164435-0000_add_price_trigger_fields/down.sql new file mode 100644 index 000000000..288c94a06 --- /dev/null +++ b/crates/schema/migrations/2025-12-10-164435-0000_add_price_trigger_fields/down.sql @@ -0,0 +1,4 @@ +ALTER TABLE margin_manager_state + DROP COLUMN current_price, + DROP COLUMN lowest_trigger_above_price, + DROP COLUMN highest_trigger_below_price; diff --git a/crates/schema/migrations/2025-12-10-164435-0000_add_price_trigger_fields/up.sql b/crates/schema/migrations/2025-12-10-164435-0000_add_price_trigger_fields/up.sql new file mode 100644 index 000000000..ca2f09010 --- /dev/null +++ b/crates/schema/migrations/2025-12-10-164435-0000_add_price_trigger_fields/up.sql @@ -0,0 +1,4 @@ +ALTER TABLE margin_manager_state + ADD COLUMN current_price BIGINT, + ADD COLUMN lowest_trigger_above_price BIGINT, + ADD COLUMN highest_trigger_below_price BIGINT; diff --git a/crates/schema/migrations/2025-12-12-153510_change_price_fields_to_numeric/down.sql b/crates/schema/migrations/2025-12-12-153510_change_price_fields_to_numeric/down.sql new file mode 100644 index 000000000..1f845dddd --- /dev/null +++ b/crates/schema/migrations/2025-12-12-153510_change_price_fields_to_numeric/down.sql @@ -0,0 +1,4 @@ +ALTER TABLE margin_manager_state + ALTER COLUMN current_price TYPE BIGINT, + ALTER COLUMN lowest_trigger_above_price TYPE BIGINT, + ALTER COLUMN highest_trigger_below_price TYPE BIGINT; diff --git a/crates/schema/migrations/2025-12-12-153510_change_price_fields_to_numeric/up.sql b/crates/schema/migrations/2025-12-12-153510_change_price_fields_to_numeric/up.sql new file mode 100644 index 000000000..b2621fff8 --- /dev/null +++ b/crates/schema/migrations/2025-12-12-153510_change_price_fields_to_numeric/up.sql @@ -0,0 +1,4 @@ +ALTER TABLE margin_manager_state + ALTER COLUMN current_price TYPE NUMERIC(20,0), + ALTER COLUMN lowest_trigger_above_price TYPE NUMERIC(20,0), + ALTER COLUMN highest_trigger_below_price TYPE NUMERIC(20,0); diff --git a/crates/schema/migrations/2025-12-23-165404-0000_add_liquidation_fields/down.sql b/crates/schema/migrations/2025-12-23-165404-0000_add_liquidation_fields/down.sql new file mode 100644 index 000000000..5bc6d4f7f --- /dev/null +++ b/crates/schema/migrations/2025-12-23-165404-0000_add_liquidation_fields/down.sql @@ -0,0 +1,9 @@ +ALTER TABLE liquidation + DROP COLUMN remaining_base_asset, + DROP COLUMN remaining_quote_asset, + DROP COLUMN remaining_base_debt, + DROP COLUMN remaining_quote_debt, + DROP COLUMN base_pyth_price, + DROP COLUMN base_pyth_decimals, + DROP COLUMN quote_pyth_price, + DROP COLUMN quote_pyth_decimals; diff --git a/crates/schema/migrations/2025-12-23-165404-0000_add_liquidation_fields/up.sql b/crates/schema/migrations/2025-12-23-165404-0000_add_liquidation_fields/up.sql new file mode 100644 index 000000000..0cfdea138 --- /dev/null +++ b/crates/schema/migrations/2025-12-23-165404-0000_add_liquidation_fields/up.sql @@ -0,0 +1,9 @@ +ALTER TABLE liquidation + ADD COLUMN remaining_base_asset BIGINT NOT NULL DEFAULT 0, + ADD COLUMN remaining_quote_asset BIGINT NOT NULL DEFAULT 0, + ADD COLUMN remaining_base_debt BIGINT NOT NULL DEFAULT 0, + ADD COLUMN remaining_quote_debt BIGINT NOT NULL DEFAULT 0, + ADD COLUMN base_pyth_price BIGINT NOT NULL DEFAULT 0, + ADD COLUMN base_pyth_decimals SMALLINT NOT NULL DEFAULT 0, + ADD COLUMN quote_pyth_price BIGINT NOT NULL DEFAULT 0, + ADD COLUMN quote_pyth_decimals SMALLINT NOT NULL DEFAULT 0; diff --git a/crates/schema/migrations/2025-12-23-171615-0000_change_liquidation_remaining_to_numeric/down.sql b/crates/schema/migrations/2025-12-23-171615-0000_change_liquidation_remaining_to_numeric/down.sql new file mode 100644 index 000000000..02e28d4c4 --- /dev/null +++ b/crates/schema/migrations/2025-12-23-171615-0000_change_liquidation_remaining_to_numeric/down.sql @@ -0,0 +1,5 @@ +ALTER TABLE liquidation + ALTER COLUMN remaining_base_asset TYPE BIGINT USING remaining_base_asset::BIGINT, + ALTER COLUMN remaining_quote_asset TYPE BIGINT USING remaining_quote_asset::BIGINT, + ALTER COLUMN remaining_base_debt TYPE BIGINT USING remaining_base_debt::BIGINT, + ALTER COLUMN remaining_quote_debt TYPE BIGINT USING remaining_quote_debt::BIGINT; diff --git a/crates/schema/migrations/2025-12-23-171615-0000_change_liquidation_remaining_to_numeric/up.sql b/crates/schema/migrations/2025-12-23-171615-0000_change_liquidation_remaining_to_numeric/up.sql new file mode 100644 index 000000000..cbf735abb --- /dev/null +++ b/crates/schema/migrations/2025-12-23-171615-0000_change_liquidation_remaining_to_numeric/up.sql @@ -0,0 +1,5 @@ +ALTER TABLE liquidation + ALTER COLUMN remaining_base_asset TYPE DECIMAL(20,0) USING remaining_base_asset::DECIMAL(20,0), + ALTER COLUMN remaining_quote_asset TYPE DECIMAL(20,0) USING remaining_quote_asset::DECIMAL(20,0), + ALTER COLUMN remaining_base_debt TYPE DECIMAL(20,0) USING remaining_base_debt::DECIMAL(20,0), + ALTER COLUMN remaining_quote_debt TYPE DECIMAL(20,0) USING remaining_quote_debt::DECIMAL(20,0); diff --git a/crates/schema/migrations/2026-01-06-000000-0000_add_margin_pool_snapshots/down.sql b/crates/schema/migrations/2026-01-06-000000-0000_add_margin_pool_snapshots/down.sql new file mode 100644 index 000000000..1df35907f --- /dev/null +++ b/crates/schema/migrations/2026-01-06-000000-0000_add_margin_pool_snapshots/down.sql @@ -0,0 +1 @@ +DROP TABLE IF EXISTS margin_pool_snapshots; diff --git a/crates/schema/migrations/2026-01-06-000000-0000_add_margin_pool_snapshots/up.sql b/crates/schema/migrations/2026-01-06-000000-0000_add_margin_pool_snapshots/up.sql new file mode 100644 index 000000000..5d84efdbd --- /dev/null +++ b/crates/schema/migrations/2026-01-06-000000-0000_add_margin_pool_snapshots/up.sql @@ -0,0 +1,22 @@ +CREATE TABLE margin_pool_snapshots ( + id BIGSERIAL PRIMARY KEY, + margin_pool_id TEXT NOT NULL, + asset_type TEXT NOT NULL, + timestamp TIMESTAMP NOT NULL DEFAULT NOW(), + + -- Pool state (raw values from RPC) + total_supply BIGINT NOT NULL, + total_borrow BIGINT NOT NULL, + vault_balance BIGINT NOT NULL, + supply_cap BIGINT NOT NULL, + interest_rate BIGINT NOT NULL, + available_withdrawal BIGINT NOT NULL, + + -- Computed metrics + utilization_rate DOUBLE PRECISION NOT NULL, + solvency_ratio DOUBLE PRECISION, + available_liquidity_pct DOUBLE PRECISION +); + +CREATE INDEX idx_margin_pool_snapshots_pool_time + ON margin_pool_snapshots (margin_pool_id, timestamp DESC); diff --git a/crates/schema/migrations/2026-01-07-000000-0000_add_pool_created_table/down.sql b/crates/schema/migrations/2026-01-07-000000-0000_add_pool_created_table/down.sql new file mode 100644 index 000000000..9133a29f0 --- /dev/null +++ b/crates/schema/migrations/2026-01-07-000000-0000_add_pool_created_table/down.sql @@ -0,0 +1 @@ +DROP TABLE IF EXISTS pool_created; diff --git a/crates/schema/migrations/2026-01-07-000000-0000_add_pool_created_table/up.sql b/crates/schema/migrations/2026-01-07-000000-0000_add_pool_created_table/up.sql new file mode 100644 index 000000000..f0687ea04 --- /dev/null +++ b/crates/schema/migrations/2026-01-07-000000-0000_add_pool_created_table/up.sql @@ -0,0 +1,21 @@ +CREATE TABLE IF NOT EXISTS pool_created +( + event_digest TEXT PRIMARY KEY, + digest TEXT NOT NULL, + sender TEXT NOT NULL, + checkpoint BIGINT NOT NULL, + timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL, + checkpoint_timestamp_ms BIGINT NOT NULL, + package TEXT NOT NULL, + pool_id TEXT NOT NULL, + taker_fee BIGINT NOT NULL, + maker_fee BIGINT NOT NULL, + tick_size BIGINT NOT NULL, + lot_size BIGINT NOT NULL, + min_size BIGINT NOT NULL, + whitelisted_pool BOOLEAN NOT NULL, + treasury_address TEXT NOT NULL +); + +CREATE INDEX idx_pool_created_pool_id ON pool_created(pool_id); +CREATE INDEX idx_pool_created_checkpoint ON pool_created(checkpoint); diff --git a/crates/schema/migrations/2026-01-12-000000-0000_add_referral_fee_events/down.sql b/crates/schema/migrations/2026-01-12-000000-0000_add_referral_fee_events/down.sql new file mode 100644 index 000000000..a607c4ceb --- /dev/null +++ b/crates/schema/migrations/2026-01-12-000000-0000_add_referral_fee_events/down.sql @@ -0,0 +1 @@ +DROP TABLE IF EXISTS referral_fee_events; diff --git a/crates/schema/migrations/2026-01-12-000000-0000_add_referral_fee_events/up.sql b/crates/schema/migrations/2026-01-12-000000-0000_add_referral_fee_events/up.sql new file mode 100644 index 000000000..8d1d73084 --- /dev/null +++ b/crates/schema/migrations/2026-01-12-000000-0000_add_referral_fee_events/up.sql @@ -0,0 +1,18 @@ +CREATE TABLE referral_fee_events ( + event_digest TEXT PRIMARY KEY, + digest TEXT NOT NULL, + sender TEXT NOT NULL, + checkpoint BIGINT NOT NULL, + timestamp TIMESTAMP NOT NULL DEFAULT NOW(), + checkpoint_timestamp_ms BIGINT NOT NULL, + package TEXT NOT NULL, + pool_id TEXT NOT NULL, + referral_id TEXT NOT NULL, + base_fee BIGINT NOT NULL, + quote_fee BIGINT NOT NULL, + deep_fee BIGINT NOT NULL +); + +CREATE INDEX idx_referral_fee_events_pool_id ON referral_fee_events (pool_id); +CREATE INDEX idx_referral_fee_events_referral_id ON referral_fee_events (referral_id); +CREATE INDEX idx_referral_fee_events_checkpoint_timestamp_ms ON referral_fee_events (checkpoint_timestamp_ms); diff --git a/crates/schema/migrations/2026-01-15-000000-0000_add_tpsl_and_collateral_events/down.sql b/crates/schema/migrations/2026-01-15-000000-0000_add_tpsl_and_collateral_events/down.sql new file mode 100644 index 000000000..44ca0cb3a --- /dev/null +++ b/crates/schema/migrations/2026-01-15-000000-0000_add_tpsl_and_collateral_events/down.sql @@ -0,0 +1,4 @@ +-- Rollback: Remove TPSL and Collateral event tables + +DROP TABLE IF EXISTS conditional_order_events; +DROP TABLE IF EXISTS collateral_events; diff --git a/crates/schema/migrations/2026-01-15-000000-0000_add_tpsl_and_collateral_events/up.sql b/crates/schema/migrations/2026-01-15-000000-0000_add_tpsl_and_collateral_events/up.sql new file mode 100644 index 000000000..dba8b6461 --- /dev/null +++ b/crates/schema/migrations/2026-01-15-000000-0000_add_tpsl_and_collateral_events/up.sql @@ -0,0 +1,71 @@ +-- Migration: Add TPSL (Take Profit/Stop Loss) and Collateral event tables +-- These tables support the margin module events that were previously missing from the indexer + +-- Collateral Events - tracks user collateral deposits and withdrawals into margin positions +-- event_type: 'deposit' or 'withdraw' +CREATE TABLE collateral_events ( + event_digest TEXT PRIMARY KEY, + digest TEXT NOT NULL, + sender TEXT NOT NULL, + checkpoint BIGINT NOT NULL, + timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL, + checkpoint_timestamp_ms BIGINT NOT NULL, + package TEXT NOT NULL, + event_type TEXT NOT NULL, + margin_manager_id TEXT NOT NULL, + amount NUMERIC NOT NULL, + asset_type TEXT NOT NULL, + -- Deposit fields (also used for withdraw single-asset pricing) + pyth_decimals SMALLINT NOT NULL, + pyth_price NUMERIC NOT NULL, + -- Withdraw-specific fields (nullable for deposits) + withdraw_base_asset BOOLEAN, + base_pyth_decimals SMALLINT, + base_pyth_price NUMERIC, + quote_pyth_decimals SMALLINT, + quote_pyth_price NUMERIC, + remaining_base_asset NUMERIC, + remaining_quote_asset NUMERIC, + remaining_base_debt NUMERIC, + remaining_quote_debt NUMERIC, + onchain_timestamp BIGINT NOT NULL +); + +-- Conditional Order Events - tracks TPSL (take profit/stop loss) order lifecycle +-- event_type: 'added', 'cancelled', 'executed', 'insufficient_funds' +CREATE TABLE conditional_order_events ( + event_digest TEXT PRIMARY KEY, + digest TEXT NOT NULL, + sender TEXT NOT NULL, + checkpoint BIGINT NOT NULL, + timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL, + checkpoint_timestamp_ms BIGINT NOT NULL, + package TEXT NOT NULL, + event_type TEXT NOT NULL, + manager_id TEXT NOT NULL, + pool_id TEXT, + conditional_order_id BIGINT NOT NULL, + trigger_below_price BOOLEAN NOT NULL, + trigger_price NUMERIC NOT NULL, + is_limit_order BOOLEAN NOT NULL, + client_order_id BIGINT NOT NULL, + order_type SMALLINT NOT NULL, + self_matching_option SMALLINT NOT NULL, + price NUMERIC NOT NULL, + quantity NUMERIC NOT NULL, + is_bid BOOLEAN NOT NULL, + pay_with_deep BOOLEAN NOT NULL, + expire_timestamp BIGINT NOT NULL, + onchain_timestamp BIGINT NOT NULL +); + +-- Create indexes for common query patterns +CREATE INDEX idx_collateral_events_margin_manager ON collateral_events(margin_manager_id); +CREATE INDEX idx_collateral_events_checkpoint ON collateral_events(checkpoint); +CREATE INDEX idx_collateral_events_type ON collateral_events(event_type); + +CREATE INDEX idx_conditional_order_events_manager ON conditional_order_events(manager_id); +CREATE INDEX idx_conditional_order_events_pool ON conditional_order_events(pool_id); +CREATE INDEX idx_conditional_order_events_checkpoint ON conditional_order_events(checkpoint); +CREATE INDEX idx_conditional_order_events_order_id ON conditional_order_events(conditional_order_id); +CREATE INDEX idx_conditional_order_events_type ON conditional_order_events(event_type); diff --git a/crates/schema/migrations/2026-01-15-234908-0000_add_net_deposits_view/down.sql b/crates/schema/migrations/2026-01-15-234908-0000_add_net_deposits_view/down.sql new file mode 100644 index 000000000..05747174d --- /dev/null +++ b/crates/schema/migrations/2026-01-15-234908-0000_add_net_deposits_view/down.sql @@ -0,0 +1 @@ +DROP MATERIALIZED VIEW IF EXISTS net_deposits_hourly; diff --git a/crates/schema/migrations/2026-01-15-234908-0000_add_net_deposits_view/up.sql b/crates/schema/migrations/2026-01-15-234908-0000_add_net_deposits_view/up.sql new file mode 100644 index 000000000..f9a38823c --- /dev/null +++ b/crates/schema/migrations/2026-01-15-234908-0000_add_net_deposits_view/up.sql @@ -0,0 +1,19 @@ +-- Materialized view for net deposits aggregated by asset at hourly intervals +-- Stores the net change (deposits - withdrawals) per asset per hour bucket + +CREATE MATERIALIZED VIEW IF NOT EXISTS net_deposits_hourly AS +SELECT + asset, + -- Truncate to hour boundary (ms) + (checkpoint_timestamp_ms / 3600000) * 3600000 AS hour_bucket_ms, + SUM(CASE WHEN deposit THEN amount ELSE -amount END)::BIGINT AS net_amount_delta +FROM balances +GROUP BY asset, (checkpoint_timestamp_ms / 3600000) * 3600000; + +-- Unique index required for REFRESH MATERIALIZED VIEW CONCURRENTLY +CREATE UNIQUE INDEX IF NOT EXISTS idx_net_deposits_hourly_asset_bucket + ON net_deposits_hourly (asset, hour_bucket_ms); + +-- Index for efficient lookups by asset with hour ordering +CREATE INDEX IF NOT EXISTS idx_net_deposits_hourly_bucket_asset + ON net_deposits_hourly (hour_bucket_ms, asset); diff --git a/crates/schema/migrations/2026-01-20-000000-0000_add_points_table/down.sql b/crates/schema/migrations/2026-01-20-000000-0000_add_points_table/down.sql new file mode 100644 index 000000000..c1c4da347 --- /dev/null +++ b/crates/schema/migrations/2026-01-20-000000-0000_add_points_table/down.sql @@ -0,0 +1 @@ +DROP TABLE IF EXISTS points; diff --git a/crates/schema/migrations/2026-01-20-000000-0000_add_points_table/up.sql b/crates/schema/migrations/2026-01-20-000000-0000_add_points_table/up.sql new file mode 100644 index 000000000..2bbd267be --- /dev/null +++ b/crates/schema/migrations/2026-01-20-000000-0000_add_points_table/up.sql @@ -0,0 +1,9 @@ +CREATE TABLE points ( + id BIGSERIAL PRIMARY KEY, + address TEXT NOT NULL, + amount BIGINT NOT NULL, + week INT4 NOT NULL, + timestamp TIMESTAMP NOT NULL DEFAULT NOW() +); + +CREATE INDEX idx_points_address ON points (address); diff --git a/crates/schema/src/models.rs b/crates/schema/src/models.rs index 800ee10c4..de2a831fa 100644 --- a/crates/schema/src/models.rs +++ b/crates/schema/src/models.rs @@ -1,17 +1,84 @@ use crate::schema::{ - balances, balances_summary, flashloans, order_fills, order_updates, pool_prices, pools, - proposals, rebates, stakes, sui_error_transactions, trade_params_update, votes, + // Margin Pool Operations Events + asset_supplied, + asset_withdrawn, + balances, + // Collateral Events (deposit/withdraw) + collateral_events, + // TPSL (Take Profit/Stop Loss) Events + conditional_order_events, + deep_burned, + deepbook_pool_config_updated, + deepbook_pool_registered, + deepbook_pool_updated, + deepbook_pool_updated_registry, + flashloans, + interest_params_updated, + liquidation, + loan_borrowed, + loan_repaid, + // Margin Registry Events + maintainer_cap_updated, + maintainer_fees_withdrawn, + // Margin Manager Events + margin_manager_created, + margin_manager_state, + margin_pool_config_updated, + // Margin Pool Admin Events + margin_pool_created, + // snapshots for analytics + margin_pool_snapshots, + order_fills, + order_updates, + pause_cap_updated, + points, + pool_created, + pool_prices, + pools, + proposals, + protocol_fees_increased, + protocol_fees_withdrawn, + rebates, + referral_fee_events, + referral_fees_claimed, + stakes, + sui_error_transactions, + supplier_cap_minted, + supply_referral_minted, + trade_params_update, + votes, }; +use bigdecimal::BigDecimal; use diesel::deserialize::FromSql; use diesel::pg::{Pg, PgValue}; use diesel::serialize::{Output, ToSql}; use diesel::sql_types::Text; use diesel::{AsExpression, Identifiable, Insertable, Queryable, QueryableByName, Selectable}; -use serde::Serialize; +use serde::{Serialize, Serializer}; use std::str::FromStr; use strum_macros::{AsRefStr, EnumString}; use sui_field_count::FieldCount; +fn serialize_bigdecimal_option( + value: &Option, + serializer: S, +) -> Result +where + S: Serializer, +{ + match value { + Some(v) => serializer.serialize_some(&v.to_string()), + None => serializer.serialize_none(), + } +} + +fn serialize_datetime(value: &chrono::NaiveDateTime, serializer: S) -> Result +where + S: Serializer, +{ + serializer.serialize_str(&value.to_string()) +} + #[derive(Queryable, Selectable, Insertable, Identifiable, Debug, FieldCount)] #[diesel(table_name = order_updates, primary_key(event_digest))] pub struct OrderUpdate { @@ -57,6 +124,30 @@ impl ToSql for OrderUpdateStatus { } } +#[derive(Debug, Clone, QueryableByName, Serialize)] +pub struct OrderStatus { + #[diesel(sql_type = diesel::sql_types::Text)] + pub order_id: String, + #[diesel(sql_type = diesel::sql_types::Text)] + pub balance_manager_id: String, + #[diesel(sql_type = diesel::sql_types::Bool)] + pub is_bid: bool, + #[diesel(sql_type = diesel::sql_types::Text)] + pub current_status: String, + #[diesel(sql_type = diesel::sql_types::BigInt)] + pub price: i64, + #[diesel(sql_type = diesel::sql_types::BigInt)] + pub placed_at: i64, + #[diesel(sql_type = diesel::sql_types::BigInt)] + pub last_updated_at: i64, + #[diesel(sql_type = diesel::sql_types::BigInt)] + pub original_quantity: i64, + #[diesel(sql_type = diesel::sql_types::BigInt)] + pub filled_quantity: i64, + #[diesel(sql_type = diesel::sql_types::BigInt)] + pub remaining_quantity: i64, +} + #[derive(Queryable, Selectable, Insertable, Identifiable, Debug, FieldCount)] #[diesel(table_name = order_fills, primary_key(event_digest))] pub struct OrderFill { @@ -93,10 +184,12 @@ pub struct OrderFillSummary { } #[derive(QueryableByName, Debug, Serialize, FieldCount)] -#[diesel(table_name = balances_summary)] pub struct BalancesSummary { + #[diesel(sql_type = Text)] pub asset: String, + #[diesel(sql_type = diesel::sql_types::BigInt)] pub amount: i64, + #[diesel(sql_type = diesel::sql_types::Bool)] pub deposit: bool, } @@ -144,6 +237,38 @@ pub struct Balances { pub deposit: bool, } +#[derive(Queryable, Selectable, Insertable, Identifiable, Debug, FieldCount)] +#[diesel(table_name = deep_burned, primary_key(event_digest))] +pub struct DeepBurned { + pub event_digest: String, + pub digest: String, + pub sender: String, + pub checkpoint: i64, + pub checkpoint_timestamp_ms: i64, + pub package: String, + pub pool_id: String, + pub burned_amount: i64, +} + +#[derive(Queryable, Selectable, Insertable, Identifiable, Debug, FieldCount, Serialize)] +#[diesel(table_name = pool_created, primary_key(event_digest))] +pub struct PoolCreated { + pub event_digest: String, + pub digest: String, + pub sender: String, + pub checkpoint: i64, + pub checkpoint_timestamp_ms: i64, + pub package: String, + pub pool_id: String, + pub taker_fee: i64, + pub maker_fee: i64, + pub tick_size: i64, + pub lot_size: i64, + pub min_size: i64, + pub whitelisted_pool: bool, + pub treasury_address: String, +} + #[derive(Queryable, Selectable, Insertable, Identifiable, Debug, FieldCount)] #[diesel(table_name = proposals, primary_key(event_digest))] pub struct Proposals { @@ -176,6 +301,22 @@ pub struct Rebates { pub claim_amount: i64, } +#[derive(Queryable, Selectable, Insertable, Identifiable, Debug, FieldCount, Serialize)] +#[diesel(table_name = referral_fee_events, primary_key(event_digest))] +pub struct ReferralFeeEvent { + pub event_digest: String, + pub digest: String, + pub sender: String, + pub checkpoint: i64, + pub checkpoint_timestamp_ms: i64, + pub package: String, + pub pool_id: String, + pub referral_id: String, + pub base_fee: i64, + pub quote_fee: i64, + pub deep_fee: i64, +} + #[derive(Queryable, Selectable, Insertable, Identifiable, Debug, FieldCount)] #[diesel(table_name = stakes, primary_key(event_digest))] pub struct Stakes { @@ -237,9 +378,9 @@ pub struct Pools { pub quote_asset_decimals: i16, pub quote_asset_symbol: String, pub quote_asset_name: String, - pub min_size: i32, - pub lot_size: i32, - pub tick_size: i32, + pub min_size: i64, + pub lot_size: i64, + pub tick_size: i64, } #[derive(Queryable, Selectable, Insertable, Identifiable, Debug, FieldCount)] @@ -252,3 +393,481 @@ pub struct SuiErrorTransactions { pub package: String, pub cmd_idx: Option, } + +// === Margin Manager Events === +#[derive(Queryable, Selectable, Insertable, Identifiable, Debug, FieldCount, Serialize)] +#[diesel(table_name = margin_manager_created, primary_key(event_digest))] +pub struct MarginManagerCreated { + pub event_digest: String, + pub digest: String, + pub sender: String, + pub checkpoint: i64, + pub checkpoint_timestamp_ms: i64, + pub package: String, + pub margin_manager_id: String, + pub balance_manager_id: String, + pub deepbook_pool_id: Option, + pub owner: String, + pub onchain_timestamp: i64, +} + +#[derive(Queryable, Selectable, Insertable, Identifiable, Debug, FieldCount, Serialize)] +#[diesel(table_name = loan_borrowed, primary_key(event_digest))] +pub struct LoanBorrowed { + pub event_digest: String, + pub digest: String, + pub sender: String, + pub checkpoint: i64, + pub checkpoint_timestamp_ms: i64, + pub package: String, + pub margin_manager_id: String, + pub margin_pool_id: String, + pub loan_amount: i64, + pub loan_shares: i64, + pub onchain_timestamp: i64, +} + +#[derive(Queryable, Selectable, Insertable, Identifiable, Debug, FieldCount, Serialize)] +#[diesel(table_name = loan_repaid, primary_key(event_digest))] +pub struct LoanRepaid { + pub event_digest: String, + pub digest: String, + pub sender: String, + pub checkpoint: i64, + pub checkpoint_timestamp_ms: i64, + pub package: String, + pub margin_manager_id: String, + pub margin_pool_id: String, + pub repay_amount: i64, + pub repay_shares: i64, + pub onchain_timestamp: i64, +} + +#[derive(Queryable, Selectable, Insertable, Identifiable, Debug, FieldCount, Serialize)] +#[diesel(table_name = liquidation, primary_key(event_digest))] +pub struct Liquidation { + pub event_digest: String, + pub digest: String, + pub sender: String, + pub checkpoint: i64, + pub checkpoint_timestamp_ms: i64, + pub package: String, + pub margin_manager_id: String, + pub margin_pool_id: String, + pub liquidation_amount: i64, + pub pool_reward: i64, + pub pool_default: i64, + pub risk_ratio: i64, + pub onchain_timestamp: i64, + pub remaining_base_asset: BigDecimal, + pub remaining_quote_asset: BigDecimal, + pub remaining_base_debt: BigDecimal, + pub remaining_quote_debt: BigDecimal, + pub base_pyth_price: i64, + pub base_pyth_decimals: i16, + pub quote_pyth_price: i64, + pub quote_pyth_decimals: i16, +} + +// === Margin Pool Operations Events === +#[derive(Queryable, Selectable, Insertable, Identifiable, Debug, FieldCount, Serialize)] +#[diesel(table_name = asset_supplied, primary_key(event_digest))] +pub struct AssetSupplied { + pub event_digest: String, + pub digest: String, + pub sender: String, + pub checkpoint: i64, + pub checkpoint_timestamp_ms: i64, + pub package: String, + pub margin_pool_id: String, + pub asset_type: String, + pub supplier: String, + pub amount: i64, + pub shares: i64, + pub onchain_timestamp: i64, +} + +#[derive(Queryable, Selectable, Insertable, Identifiable, Debug, FieldCount, Serialize)] +#[diesel(table_name = asset_withdrawn, primary_key(event_digest))] +pub struct AssetWithdrawn { + pub event_digest: String, + pub digest: String, + pub sender: String, + pub checkpoint: i64, + pub checkpoint_timestamp_ms: i64, + pub package: String, + pub margin_pool_id: String, + pub asset_type: String, + pub supplier: String, + pub amount: i64, + pub shares: i64, + pub onchain_timestamp: i64, +} + +// === Margin Pool Admin Events === +#[derive(Queryable, Selectable, Insertable, Identifiable, Debug, FieldCount, Serialize)] +#[diesel(table_name = margin_pool_created, primary_key(event_digest))] +pub struct MarginPoolCreated { + pub event_digest: String, + pub digest: String, + pub sender: String, + pub checkpoint: i64, + pub checkpoint_timestamp_ms: i64, + pub package: String, + pub margin_pool_id: String, + pub maintainer_cap_id: String, + pub asset_type: String, + pub config_json: serde_json::Value, + pub onchain_timestamp: i64, +} + +#[derive(Queryable, Selectable, Insertable, Identifiable, Debug, FieldCount, Serialize)] +#[diesel(table_name = deepbook_pool_updated, primary_key(event_digest))] +pub struct DeepbookPoolUpdated { + pub event_digest: String, + pub digest: String, + pub sender: String, + pub checkpoint: i64, + pub checkpoint_timestamp_ms: i64, + pub package: String, + pub margin_pool_id: String, + pub deepbook_pool_id: String, + pub pool_cap_id: String, + pub enabled: bool, + pub onchain_timestamp: i64, +} + +#[derive(Queryable, Selectable, Insertable, Identifiable, Debug, FieldCount, Serialize)] +#[diesel(table_name = interest_params_updated, primary_key(event_digest))] +pub struct InterestParamsUpdated { + pub event_digest: String, + pub digest: String, + pub sender: String, + pub checkpoint: i64, + pub checkpoint_timestamp_ms: i64, + pub package: String, + pub margin_pool_id: String, + pub pool_cap_id: String, + pub config_json: serde_json::Value, + pub onchain_timestamp: i64, +} + +#[derive(Queryable, Selectable, Insertable, Identifiable, Debug, FieldCount, Serialize)] +#[diesel(table_name = margin_pool_config_updated, primary_key(event_digest))] +pub struct MarginPoolConfigUpdated { + pub event_digest: String, + pub digest: String, + pub sender: String, + pub checkpoint: i64, + pub checkpoint_timestamp_ms: i64, + pub package: String, + pub margin_pool_id: String, + pub pool_cap_id: String, + pub config_json: serde_json::Value, + pub onchain_timestamp: i64, +} + +// === Margin Registry Events === +#[derive(Queryable, Selectable, Insertable, Identifiable, Debug, FieldCount, Serialize)] +#[diesel(table_name = maintainer_cap_updated, primary_key(event_digest))] +pub struct MaintainerCapUpdated { + pub event_digest: String, + pub digest: String, + pub sender: String, + pub checkpoint: i64, + pub checkpoint_timestamp_ms: i64, + pub package: String, + pub maintainer_cap_id: String, + pub allowed: bool, + pub onchain_timestamp: i64, +} + +#[derive(Queryable, Selectable, Insertable, Identifiable, Debug, FieldCount, Serialize)] +#[diesel(table_name = deepbook_pool_registered, primary_key(event_digest))] +pub struct DeepbookPoolRegistered { + pub event_digest: String, + pub digest: String, + pub sender: String, + pub checkpoint: i64, + pub checkpoint_timestamp_ms: i64, + pub package: String, + pub pool_id: String, + pub config_json: Option, + pub onchain_timestamp: i64, +} + +#[derive(Queryable, Selectable, Insertable, Identifiable, Debug, FieldCount, Serialize)] +#[diesel(table_name = deepbook_pool_updated_registry, primary_key(event_digest))] +pub struct DeepbookPoolUpdatedRegistry { + pub event_digest: String, + pub digest: String, + pub sender: String, + pub checkpoint: i64, + pub checkpoint_timestamp_ms: i64, + pub package: String, + pub pool_id: String, + pub enabled: bool, + pub onchain_timestamp: i64, +} + +#[derive(Queryable, Selectable, Insertable, Identifiable, Debug, FieldCount, Serialize)] +#[diesel(table_name = deepbook_pool_config_updated, primary_key(event_digest))] +pub struct DeepbookPoolConfigUpdated { + pub event_digest: String, + pub digest: String, + pub sender: String, + pub checkpoint: i64, + pub checkpoint_timestamp_ms: i64, + pub package: String, + pub pool_id: String, + pub config_json: serde_json::Value, + pub onchain_timestamp: i64, +} + +// === Additional Margin Pool Events === +#[derive(Queryable, Selectable, Insertable, Identifiable, Debug, FieldCount, Serialize)] +#[diesel(table_name = maintainer_fees_withdrawn, primary_key(event_digest))] +pub struct MaintainerFeesWithdrawn { + pub event_digest: String, + pub digest: String, + pub sender: String, + pub checkpoint: i64, + pub checkpoint_timestamp_ms: i64, + pub package: String, + pub margin_pool_id: String, + pub margin_pool_cap_id: String, + pub maintainer_fees: i64, + pub onchain_timestamp: i64, +} + +#[derive(Queryable, Selectable, Insertable, Identifiable, Debug, FieldCount, Serialize)] +#[diesel(table_name = protocol_fees_withdrawn, primary_key(event_digest))] +pub struct ProtocolFeesWithdrawn { + pub event_digest: String, + pub digest: String, + pub sender: String, + pub checkpoint: i64, + pub checkpoint_timestamp_ms: i64, + pub package: String, + pub margin_pool_id: String, + pub protocol_fees: i64, + pub onchain_timestamp: i64, +} + +#[derive(Queryable, Selectable, Insertable, Identifiable, Debug, FieldCount, Serialize)] +#[diesel(table_name = supplier_cap_minted, primary_key(event_digest))] +pub struct SupplierCapMinted { + pub event_digest: String, + pub digest: String, + pub sender: String, + pub checkpoint: i64, + pub checkpoint_timestamp_ms: i64, + pub package: String, + pub supplier_cap_id: String, + pub onchain_timestamp: i64, +} + +#[derive(Queryable, Selectable, Insertable, Identifiable, Debug, FieldCount, Serialize)] +#[diesel(table_name = supply_referral_minted, primary_key(event_digest))] +pub struct SupplyReferralMinted { + pub event_digest: String, + pub digest: String, + pub sender: String, + pub checkpoint: i64, + pub checkpoint_timestamp_ms: i64, + pub package: String, + pub margin_pool_id: String, + pub supply_referral_id: String, + pub owner: String, + pub onchain_timestamp: i64, +} + +#[derive(Queryable, Selectable, Insertable, Identifiable, Debug, FieldCount, Serialize)] +#[diesel(table_name = pause_cap_updated, primary_key(event_digest))] +pub struct PauseCapUpdated { + pub event_digest: String, + pub digest: String, + pub sender: String, + pub checkpoint: i64, + pub checkpoint_timestamp_ms: i64, + pub package: String, + pub pause_cap_id: String, + pub allowed: bool, + pub onchain_timestamp: i64, +} + +// === Protocol Fees Events === +#[derive(Queryable, Selectable, Insertable, Identifiable, Debug, FieldCount, Serialize)] +#[diesel(table_name = protocol_fees_increased, primary_key(event_digest))] +pub struct ProtocolFeesIncreasedEvent { + pub event_digest: String, + pub digest: String, + pub sender: String, + pub checkpoint: i64, + pub checkpoint_timestamp_ms: i64, + pub package: String, + pub margin_pool_id: String, + pub total_shares: i64, + pub referral_fees: i64, + pub maintainer_fees: i64, + pub protocol_fees: i64, + pub onchain_timestamp: i64, +} + +#[derive(Queryable, Selectable, Insertable, Identifiable, Debug, FieldCount, Serialize)] +#[diesel(table_name = referral_fees_claimed, primary_key(event_digest))] +pub struct ReferralFeesClaimedEvent { + pub event_digest: String, + pub digest: String, + pub sender: String, + pub checkpoint: i64, + pub checkpoint_timestamp_ms: i64, + pub package: String, + pub referral_id: String, + pub owner: String, + pub fees: i64, + pub onchain_timestamp: i64, +} + +// === Margin Manager State === +#[derive(Queryable, Selectable, Identifiable, Debug, Serialize)] +#[diesel(table_name = margin_manager_state)] +pub struct MarginManagerState { + pub id: i32, + pub margin_manager_id: String, + pub deepbook_pool_id: String, + pub base_margin_pool_id: Option, + pub quote_margin_pool_id: Option, + pub base_asset_id: Option, + pub base_asset_symbol: Option, + pub quote_asset_id: Option, + pub quote_asset_symbol: Option, + #[serde(serialize_with = "serialize_bigdecimal_option")] + pub risk_ratio: Option, + #[serde(serialize_with = "serialize_bigdecimal_option")] + pub base_asset: Option, + #[serde(serialize_with = "serialize_bigdecimal_option")] + pub quote_asset: Option, + #[serde(serialize_with = "serialize_bigdecimal_option")] + pub base_debt: Option, + #[serde(serialize_with = "serialize_bigdecimal_option")] + pub quote_debt: Option, + pub base_pyth_price: Option, + pub base_pyth_decimals: Option, + pub quote_pyth_price: Option, + pub quote_pyth_decimals: Option, + #[serde(serialize_with = "serialize_datetime")] + pub created_at: chrono::NaiveDateTime, + #[serde(serialize_with = "serialize_datetime")] + pub updated_at: chrono::NaiveDateTime, + #[serde(serialize_with = "serialize_bigdecimal_option")] + pub current_price: Option, + #[serde(serialize_with = "serialize_bigdecimal_option")] + pub lowest_trigger_above_price: Option, + #[serde(serialize_with = "serialize_bigdecimal_option")] + pub highest_trigger_below_price: Option, +} + +// === Margin Pool Snapshots (for metrics polling) === +#[derive(Queryable, Selectable, Insertable, Debug, Serialize)] +#[diesel(table_name = margin_pool_snapshots)] +pub struct MarginPoolSnapshot { + pub id: i64, + pub margin_pool_id: String, + pub asset_type: String, + #[serde(serialize_with = "serialize_datetime")] + pub timestamp: chrono::NaiveDateTime, + pub total_supply: i64, + pub total_borrow: i64, + pub vault_balance: i64, + pub supply_cap: i64, + pub interest_rate: i64, + pub available_withdrawal: i64, + pub utilization_rate: f64, + pub solvency_ratio: Option, + pub available_liquidity_pct: Option, +} + +#[derive(Insertable, Debug)] +#[diesel(table_name = margin_pool_snapshots)] +pub struct NewMarginPoolSnapshot { + pub margin_pool_id: String, + pub asset_type: String, + pub total_supply: i64, + pub total_borrow: i64, + pub vault_balance: i64, + pub supply_cap: i64, + pub interest_rate: i64, + pub available_withdrawal: i64, + pub utilization_rate: f64, + pub solvency_ratio: Option, + pub available_liquidity_pct: Option, +} + +// === Collateral Events === +#[derive(Queryable, Selectable, Insertable, Identifiable, Debug, FieldCount, Serialize)] +#[diesel(table_name = collateral_events, primary_key(event_digest))] +pub struct CollateralEvent { + pub event_digest: String, + pub digest: String, + pub sender: String, + pub checkpoint: i64, + pub checkpoint_timestamp_ms: i64, + pub package: String, + pub event_type: String, + pub margin_manager_id: String, + pub amount: BigDecimal, + pub asset_type: String, + pub pyth_decimals: i16, + pub pyth_price: BigDecimal, + pub withdraw_base_asset: Option, + pub base_pyth_decimals: Option, + pub base_pyth_price: Option, + pub quote_pyth_decimals: Option, + pub quote_pyth_price: Option, + pub remaining_base_asset: Option, + pub remaining_quote_asset: Option, + pub remaining_base_debt: Option, + pub remaining_quote_debt: Option, + pub onchain_timestamp: i64, +} + +// === TPSL (Take Profit / Stop Loss) Events === +#[derive(Queryable, Selectable, Insertable, Identifiable, Debug, FieldCount, Serialize)] +#[diesel(table_name = conditional_order_events, primary_key(event_digest))] +pub struct ConditionalOrderEvent { + pub event_digest: String, + pub digest: String, + pub sender: String, + pub checkpoint: i64, + pub checkpoint_timestamp_ms: i64, + pub package: String, + pub event_type: String, + pub manager_id: String, + pub pool_id: Option, + pub conditional_order_id: i64, + pub trigger_below_price: bool, + pub trigger_price: BigDecimal, + pub is_limit_order: bool, + pub client_order_id: i64, + pub order_type: i16, + pub self_matching_option: i16, + pub price: BigDecimal, + pub quantity: BigDecimal, + pub is_bid: bool, + pub pay_with_deep: bool, + pub expire_timestamp: i64, + pub onchain_timestamp: i64, +} + +// === Points === +#[derive(Queryable, Selectable, Insertable, Identifiable, Debug, FieldCount, Serialize)] +#[diesel(table_name = points, primary_key(id))] +pub struct Points { + pub id: i64, + pub address: String, + pub amount: i64, + pub week: i32, + #[serde(serialize_with = "serialize_datetime")] + pub timestamp: chrono::NaiveDateTime, +} diff --git a/crates/schema/src/schema.rs b/crates/schema/src/schema.rs index 353982b2f..df774fded 100644 --- a/crates/schema/src/schema.rs +++ b/crates/schema/src/schema.rs @@ -1,7 +1,53 @@ -// Copyright (c) Mysten Labs, Inc. -// SPDX-License-Identifier: Apache-2.0 // @generated automatically by Diesel CLI. +diesel::table! { + asset_supplied (event_digest) { + event_digest -> Text, + digest -> Text, + sender -> Text, + checkpoint -> Int8, + timestamp -> Timestamp, + checkpoint_timestamp_ms -> Int8, + package -> Text, + margin_pool_id -> Text, + asset_type -> Text, + supplier -> Text, + amount -> Int8, + shares -> Int8, + onchain_timestamp -> Int8, + } +} + +diesel::table! { + asset_withdrawn (event_digest) { + event_digest -> Text, + digest -> Text, + sender -> Text, + checkpoint -> Int8, + timestamp -> Timestamp, + checkpoint_timestamp_ms -> Int8, + package -> Text, + margin_pool_id -> Text, + asset_type -> Text, + supplier -> Text, + amount -> Int8, + shares -> Int8, + onchain_timestamp -> Int8, + } +} + +diesel::table! { + assets (asset_type) { + asset_type -> Text, + name -> Text, + symbol -> Text, + decimals -> Int2, + ucid -> Nullable, + package_id -> Nullable, + package_address_url -> Nullable, + } +} + diesel::table! { balances (event_digest) { event_digest -> Text, @@ -18,6 +64,138 @@ diesel::table! { } } +diesel::table! { + collateral_events (event_digest) { + event_digest -> Text, + digest -> Text, + sender -> Text, + checkpoint -> Int8, + timestamp -> Timestamp, + checkpoint_timestamp_ms -> Int8, + package -> Text, + event_type -> Text, + margin_manager_id -> Text, + amount -> Numeric, + asset_type -> Text, + pyth_decimals -> Int2, + pyth_price -> Numeric, + withdraw_base_asset -> Nullable, + base_pyth_decimals -> Nullable, + base_pyth_price -> Nullable, + quote_pyth_decimals -> Nullable, + quote_pyth_price -> Nullable, + remaining_base_asset -> Nullable, + remaining_quote_asset -> Nullable, + remaining_base_debt -> Nullable, + remaining_quote_debt -> Nullable, + onchain_timestamp -> Int8, + } +} + +diesel::table! { + conditional_order_events (event_digest) { + event_digest -> Text, + digest -> Text, + sender -> Text, + checkpoint -> Int8, + timestamp -> Timestamp, + checkpoint_timestamp_ms -> Int8, + package -> Text, + event_type -> Text, + manager_id -> Text, + pool_id -> Nullable, + conditional_order_id -> Int8, + trigger_below_price -> Bool, + trigger_price -> Numeric, + is_limit_order -> Bool, + client_order_id -> Int8, + order_type -> Int2, + self_matching_option -> Int2, + price -> Numeric, + quantity -> Numeric, + is_bid -> Bool, + pay_with_deep -> Bool, + expire_timestamp -> Int8, + onchain_timestamp -> Int8, + } +} + +diesel::table! { + deep_burned (event_digest) { + event_digest -> Text, + digest -> Text, + sender -> Text, + checkpoint -> Int8, + timestamp -> Timestamp, + checkpoint_timestamp_ms -> Int8, + package -> Text, + pool_id -> Text, + burned_amount -> Int8, + } +} + +diesel::table! { + deepbook_pool_config_updated (event_digest) { + event_digest -> Text, + digest -> Text, + sender -> Text, + checkpoint -> Int8, + timestamp -> Timestamp, + checkpoint_timestamp_ms -> Int8, + package -> Text, + pool_id -> Text, + config_json -> Jsonb, + onchain_timestamp -> Int8, + } +} + +diesel::table! { + deepbook_pool_registered (event_digest) { + event_digest -> Text, + digest -> Text, + sender -> Text, + checkpoint -> Int8, + timestamp -> Timestamp, + checkpoint_timestamp_ms -> Int8, + package -> Text, + pool_id -> Text, + onchain_timestamp -> Int8, + config_json -> Nullable, + } +} + +diesel::table! { + deepbook_pool_updated (event_digest) { + event_digest -> Text, + digest -> Text, + sender -> Text, + checkpoint -> Int8, + timestamp -> Timestamp, + checkpoint_timestamp_ms -> Int8, + package -> Text, + margin_pool_id -> Text, + deepbook_pool_id -> Text, + pool_cap_id -> Text, + enabled -> Bool, + onchain_timestamp -> Int8, + } +} + +diesel::table! { + deepbook_pool_updated_registry (event_digest) { + event_digest -> Text, + digest -> Text, + sender -> Text, + checkpoint -> Int8, + timestamp -> Timestamp, + checkpoint_timestamp_ms -> Int8, + package -> Text, + pool_id -> Text, + enabled -> Bool, + onchain_timestamp -> Int8, + } +} + diesel::table! { flashloans (event_digest) { event_digest -> Text, @@ -34,6 +212,250 @@ diesel::table! { } } +diesel::table! { + interest_params_updated (event_digest) { + event_digest -> Text, + digest -> Text, + sender -> Text, + checkpoint -> Int8, + timestamp -> Timestamp, + checkpoint_timestamp_ms -> Int8, + package -> Text, + margin_pool_id -> Text, + pool_cap_id -> Text, + config_json -> Jsonb, + onchain_timestamp -> Int8, + } +} + +diesel::table! { + liquidation (event_digest) { + event_digest -> Text, + digest -> Text, + sender -> Text, + checkpoint -> Int8, + timestamp -> Timestamp, + checkpoint_timestamp_ms -> Int8, + package -> Text, + margin_manager_id -> Text, + margin_pool_id -> Text, + liquidation_amount -> Int8, + pool_reward -> Int8, + pool_default -> Int8, + risk_ratio -> Int8, + onchain_timestamp -> Int8, + remaining_base_asset -> Numeric, + remaining_quote_asset -> Numeric, + remaining_base_debt -> Numeric, + remaining_quote_debt -> Numeric, + base_pyth_price -> Int8, + base_pyth_decimals -> Int2, + quote_pyth_price -> Int8, + quote_pyth_decimals -> Int2, + } +} + +diesel::table! { + loan_borrowed (event_digest) { + event_digest -> Text, + digest -> Text, + sender -> Text, + checkpoint -> Int8, + timestamp -> Timestamp, + checkpoint_timestamp_ms -> Int8, + package -> Text, + margin_manager_id -> Text, + margin_pool_id -> Text, + loan_amount -> Int8, + onchain_timestamp -> Int8, + loan_shares -> Int8, + } +} + +diesel::table! { + loan_repaid (event_digest) { + event_digest -> Text, + digest -> Text, + sender -> Text, + checkpoint -> Int8, + timestamp -> Timestamp, + checkpoint_timestamp_ms -> Int8, + package -> Text, + margin_manager_id -> Text, + margin_pool_id -> Text, + repay_amount -> Int8, + repay_shares -> Int8, + onchain_timestamp -> Int8, + } +} + +diesel::table! { + maintainer_cap_updated (event_digest) { + event_digest -> Text, + digest -> Text, + sender -> Text, + checkpoint -> Int8, + timestamp -> Timestamp, + checkpoint_timestamp_ms -> Int8, + package -> Text, + maintainer_cap_id -> Text, + allowed -> Bool, + onchain_timestamp -> Int8, + } +} + +diesel::table! { + maintainer_fees_withdrawn (event_digest) { + event_digest -> Text, + digest -> Text, + sender -> Text, + checkpoint -> Int8, + timestamp -> Timestamp, + checkpoint_timestamp_ms -> Int8, + package -> Text, + margin_pool_id -> Text, + margin_pool_cap_id -> Text, + maintainer_fees -> Int8, + onchain_timestamp -> Int8, + } +} + +diesel::table! { + margin_manager_created (event_digest) { + event_digest -> Text, + digest -> Text, + sender -> Text, + checkpoint -> Int8, + timestamp -> Timestamp, + checkpoint_timestamp_ms -> Int8, + package -> Text, + margin_manager_id -> Text, + balance_manager_id -> Text, + owner -> Text, + onchain_timestamp -> Int8, + deepbook_pool_id -> Nullable, + } +} + +diesel::table! { + margin_manager_state (id) { + id -> Int4, + #[max_length = 66] + margin_manager_id -> Varchar, + #[max_length = 66] + deepbook_pool_id -> Varchar, + #[max_length = 66] + base_margin_pool_id -> Nullable, + #[max_length = 66] + quote_margin_pool_id -> Nullable, + #[max_length = 255] + base_asset_id -> Nullable, + #[max_length = 50] + base_asset_symbol -> Nullable, + #[max_length = 255] + quote_asset_id -> Nullable, + #[max_length = 50] + quote_asset_symbol -> Nullable, + risk_ratio -> Nullable, + base_asset -> Nullable, + quote_asset -> Nullable, + base_debt -> Nullable, + quote_debt -> Nullable, + base_pyth_price -> Nullable, + base_pyth_decimals -> Nullable, + quote_pyth_price -> Nullable, + quote_pyth_decimals -> Nullable, + created_at -> Timestamp, + updated_at -> Timestamp, + current_price -> Nullable, + lowest_trigger_above_price -> Nullable, + highest_trigger_below_price -> Nullable, + } +} + +diesel::table! { + margin_pool_config_updated (event_digest) { + event_digest -> Text, + digest -> Text, + sender -> Text, + checkpoint -> Int8, + timestamp -> Timestamp, + checkpoint_timestamp_ms -> Int8, + package -> Text, + margin_pool_id -> Text, + pool_cap_id -> Text, + config_json -> Jsonb, + onchain_timestamp -> Int8, + } +} + +diesel::table! { + margin_pool_created (event_digest) { + event_digest -> Text, + digest -> Text, + sender -> Text, + checkpoint -> Int8, + timestamp -> Timestamp, + checkpoint_timestamp_ms -> Int8, + package -> Text, + margin_pool_id -> Text, + maintainer_cap_id -> Text, + asset_type -> Text, + config_json -> Jsonb, + onchain_timestamp -> Int8, + } +} + +diesel::table! { + margin_pool_snapshots (id) { + id -> Int8, + margin_pool_id -> Text, + asset_type -> Text, + timestamp -> Timestamp, + total_supply -> Int8, + total_borrow -> Int8, + vault_balance -> Int8, + supply_cap -> Int8, + interest_rate -> Int8, + available_withdrawal -> Int8, + utilization_rate -> Float8, + solvency_ratio -> Nullable, + available_liquidity_pct -> Nullable, + } +} + +diesel::table! { + ohclv_1d (pool_id, bucket_time) { + pool_id -> Text, + bucket_time -> Date, + open -> Numeric, + high -> Numeric, + low -> Numeric, + close -> Numeric, + base_volume -> Numeric, + quote_volume -> Numeric, + trade_count -> Int4, + first_trade_timestamp -> Int8, + last_trade_timestamp -> Int8, + } +} + +diesel::table! { + ohclv_1m (pool_id, bucket_time) { + pool_id -> Text, + bucket_time -> Timestamp, + open -> Numeric, + high -> Numeric, + low -> Numeric, + close -> Numeric, + base_volume -> Numeric, + quote_volume -> Numeric, + trade_count -> Int4, + first_trade_timestamp -> Int8, + last_trade_timestamp -> Int8, + } +} + diesel::table! { order_fills (event_digest) { event_digest -> Text, @@ -86,6 +508,21 @@ diesel::table! { } } +diesel::table! { + pause_cap_updated (event_digest) { + event_digest -> Text, + digest -> Text, + sender -> Text, + checkpoint -> Int8, + timestamp -> Timestamp, + checkpoint_timestamp_ms -> Int8, + package -> Text, + pause_cap_id -> Text, + allowed -> Bool, + onchain_timestamp -> Int8, + } +} + diesel::table! { pool_prices (event_digest) { event_digest -> Text, @@ -101,6 +538,26 @@ diesel::table! { } } +diesel::table! { + pool_created (event_digest) { + event_digest -> Text, + digest -> Text, + sender -> Text, + checkpoint -> Int8, + timestamp -> Timestamp, + checkpoint_timestamp_ms -> Int8, + package -> Text, + pool_id -> Text, + taker_fee -> Int8, + maker_fee -> Int8, + tick_size -> Int8, + lot_size -> Int8, + min_size -> Int8, + whitelisted_pool -> Bool, + treasury_address -> Text, + } +} + diesel::table! { pools (pool_id) { pool_id -> Text, @@ -113,9 +570,9 @@ diesel::table! { quote_asset_decimals -> Int2, quote_asset_symbol -> Text, quote_asset_name -> Text, - min_size -> Int4, - lot_size -> Int4, - tick_size -> Int4, + min_size -> Int8, + lot_size -> Int8, + tick_size -> Int8, } } @@ -137,6 +594,39 @@ diesel::table! { } } +diesel::table! { + protocol_fees_increased (event_digest) { + event_digest -> Text, + digest -> Text, + sender -> Text, + checkpoint -> Int8, + timestamp -> Timestamp, + checkpoint_timestamp_ms -> Int8, + package -> Text, + margin_pool_id -> Text, + total_shares -> Int8, + referral_fees -> Int8, + maintainer_fees -> Int8, + protocol_fees -> Int8, + onchain_timestamp -> Int8, + } +} + +diesel::table! { + protocol_fees_withdrawn (event_digest) { + event_digest -> Text, + digest -> Text, + sender -> Text, + checkpoint -> Int8, + timestamp -> Timestamp, + checkpoint_timestamp_ms -> Int8, + package -> Text, + margin_pool_id -> Text, + protocol_fees -> Int8, + onchain_timestamp -> Int8, + } +} + diesel::table! { rebates (event_digest) { event_digest -> Text, @@ -153,6 +643,39 @@ diesel::table! { } } +diesel::table! { + referral_fee_events (event_digest) { + event_digest -> Text, + digest -> Text, + sender -> Text, + checkpoint -> Int8, + timestamp -> Timestamp, + checkpoint_timestamp_ms -> Int8, + package -> Text, + pool_id -> Text, + referral_id -> Text, + base_fee -> Int8, + quote_fee -> Int8, + deep_fee -> Int8, + } +} + +diesel::table! { + referral_fees_claimed (event_digest) { + event_digest -> Text, + digest -> Text, + sender -> Text, + checkpoint -> Int8, + timestamp -> Timestamp, + checkpoint_timestamp_ms -> Int8, + package -> Text, + referral_id -> Text, + owner -> Text, + fees -> Int8, + onchain_timestamp -> Int8, + } +} + diesel::table! { stakes (event_digest) { event_digest -> Text, @@ -182,6 +705,36 @@ diesel::table! { } } +diesel::table! { + supplier_cap_minted (event_digest) { + event_digest -> Text, + digest -> Text, + sender -> Text, + checkpoint -> Int8, + timestamp -> Timestamp, + checkpoint_timestamp_ms -> Int8, + package -> Text, + supplier_cap_id -> Text, + onchain_timestamp -> Int8, + } +} + +diesel::table! { + supply_referral_minted (event_digest) { + event_digest -> Text, + digest -> Text, + sender -> Text, + checkpoint -> Int8, + timestamp -> Timestamp, + checkpoint_timestamp_ms -> Int8, + package -> Text, + margin_pool_id -> Text, + supply_referral_id -> Text, + owner -> Text, + onchain_timestamp -> Int8, + } +} + diesel::table! { trade_params_update (event_digest) { event_digest -> Text, @@ -217,37 +770,72 @@ diesel::table! { } diesel::table! { - assets (asset_type) { - asset_type -> Text, - name -> Text, - symbol -> Text, - decimals -> Int2, - ucid -> Nullable, - package_id -> Nullable, - package_address_url -> Nullable, + points (id) { + id -> Int8, + address -> Text, + amount -> Int8, + week -> Int4, + timestamp -> Timestamp, + } +} + +diesel::table! { + watermarks (pipeline) { + pipeline -> Text, + epoch_hi_inclusive -> Int8, + checkpoint_hi_inclusive -> Int8, + tx_hi -> Int8, + timestamp_ms_hi_inclusive -> Int8, + reader_lo -> Int8, + pruner_timestamp -> Timestamp, + pruner_hi -> Int8, } } diesel::allow_tables_to_appear_in_same_query!( + asset_supplied, + asset_withdrawn, + assets, balances, + collateral_events, + conditional_order_events, + deep_burned, + deepbook_pool_config_updated, + deepbook_pool_registered, + deepbook_pool_updated, + deepbook_pool_updated_registry, flashloans, + interest_params_updated, + liquidation, + loan_borrowed, + loan_repaid, + maintainer_cap_updated, + maintainer_fees_withdrawn, + margin_manager_created, + margin_manager_state, + margin_pool_config_updated, + margin_pool_created, + margin_pool_snapshots, + ohclv_1d, + ohclv_1m, order_fills, order_updates, + pause_cap_updated, + points, + pool_created, pool_prices, pools, proposals, + protocol_fees_increased, + protocol_fees_withdrawn, rebates, + referral_fee_events, + referral_fees_claimed, stakes, sui_error_transactions, + supplier_cap_minted, + supply_referral_minted, trade_params_update, votes, - assets, + watermarks, ); - -diesel::table! { - balances_summary (asset) { - asset -> Text, - amount -> Int8, - deposit -> Bool, - } -} diff --git a/crates/server/Cargo.toml b/crates/server/Cargo.toml index 6f082abc7..aede126a9 100644 --- a/crates/server/Cargo.toml +++ b/crates/server/Cargo.toml @@ -10,27 +10,29 @@ edition = "2021" deepbook-schema = { path = "../schema" } tokio.workspace = true futures = "0.3.31" - clap = { workspace = true, features = ["env"] } diesel = { workspace = true, features = ["postgres", "uuid", "chrono", "serde_json", "numeric"] } diesel-async = { workspace = true, features = ["bb8", "postgres"] } -tracing.workspace = true -async-trait.workspace = true bcs.workspace = true -serde.workspace = true anyhow.workspace = true +serde = { workspace = true, features = ["derive"] } serde_json.workspace = true -chrono.workspace = true url.workspace = true - +prometheus.workspace = true +sui-futures.workspace = true sui-types.workspace = true - +sui-indexer-alt-metrics.workspace = true +telemetry-subscribers.workspace = true axum = { version = "0.7", features = ["json"] } -sui-json-rpc-types = { git = "https://github.com/MystenLabs/sui.git", rev = "88ba4e08e96ba1ab965c11ce1a915331dd3ed68d" } -sui-pg-db = { git = "https://github.com/MystenLabs/sui.git", rev = "88ba4e08e96ba1ab965c11ce1a915331dd3ed68d" } +sui-json-rpc-types = { git = "https://github.com/MystenLabs/sui.git", branch = "testnet" } +sui-pg-db.workspace = true tower-http = { version = "0.5", features = ["cors"] } -sui-sdk = { git = "https://github.com/MystenLabs/sui.git", rev = "88ba4e08e96ba1ab965c11ce1a915331dd3ed68d" } +sui-sdk = { git = "https://github.com/MystenLabs/sui.git", branch = "testnet" } +bigdecimal = "0.4.9" +chrono = "0.4.42" +thiserror = "1.0" +tokio-util = "0.7" [[bin]] name = "deepbook-server" -path = "src/main.rs" \ No newline at end of file +path = "src/main.rs" diff --git a/crates/server/README.md b/crates/server/README.md index ea00160d7..375e39701 100644 --- a/crates/server/README.md +++ b/crates/server/README.md @@ -1,3 +1,75 @@ # DeepBook Server -The DeepBook Server is a Rust application that provides a RESTful API for the DeepBook project. It allows users to interact with the DeepBook database and retrieve information about DeepBook events. \ No newline at end of file +The DeepBook Server is a Rust application that provides a RESTful API for the DeepBook project. It allows users to interact with the DeepBook database and retrieve information about DeepBook events. + +## Health & Status Endpoints + +### `/` - Health Check +Basic health check endpoint that returns HTTP 200 OK if the server is running. + +```bash +curl http://localhost:9008/ +``` + +### `/status` - Indexer Status +Returns detailed information about the indexer's health, including checkpoint lag and synchronization status. + +```bash +curl http://localhost:9008/status +``` + +**Query Parameters:** +- `max_checkpoint_lag` (optional, default: 100) - Maximum acceptable checkpoint lag for "healthy" status +- `max_time_lag_seconds` (optional, default: 60) - Maximum acceptable time lag in seconds for "healthy" status + +**Examples:** +```bash +# Use default thresholds (checkpoint_lag < 100, time_lag < 60 seconds) +curl http://localhost:9008/status + +# Custom thresholds: allow up to 500 checkpoint lag and 300 seconds time lag +curl "http://localhost:9008/status?max_checkpoint_lag=500&max_time_lag_seconds=300" + +# Strict thresholds: only healthy if checkpoint_lag < 10 and time_lag < 5 seconds +curl "http://localhost:9008/status?max_checkpoint_lag=10&max_time_lag_seconds=5" +``` + +**Example Response:** +```json +{ + "status": "OK", + "latest_onchain_checkpoint": 12345678, + "current_time_ms": 1732567890000, + "earliest_checkpoint": 12345673, + "max_lag_pipeline": "deepbook_indexer", + "pipelines": [ + { + "pipeline": "deepbook_indexer", + "indexed_checkpoint": 12345673, + "indexed_epoch": 500, + "indexed_timestamp_ms": 1732567878000, + "checkpoint_lag": 5, + "time_lag_seconds": 12, + "latest_onchain_checkpoint": 12345678 + } + ], + "max_checkpoint_lag": 5, + "max_time_lag_seconds": 12 +} +``` + +**Response Fields:** +- `status` - Overall health: `"OK"` or `"UNHEALTHY"` (based on client-provided thresholds) +- `latest_onchain_checkpoint` - Latest checkpoint on the blockchain +- `current_time_ms` - Current server timestamp +- `earliest_checkpoint` - The lowest checkpoint across all pipelines (useful for alerting) +- `max_lag_pipeline` - Name of the pipeline with the highest checkpoint lag (useful for alerting) +- `pipelines` - Array of per-pipeline details +- `max_checkpoint_lag` - Maximum checkpoint lag across all pipelines +- `max_time_lag_seconds` - Maximum time lag in seconds across all pipelines + +**Status Values:** +- `OK` - Indexer is synced and up-to-date (based on thresholds) +- `UNHEALTHY` - Indexer is behind or experiencing delays + +This endpoint is useful for monitoring the indexer's synchronization status and detecting stale data. \ No newline at end of file diff --git a/crates/server/src/error.rs b/crates/server/src/error.rs index 525cb8d0b..21803a2d8 100644 --- a/crates/server/src/error.rs +++ b/crates/server/src/error.rs @@ -1,7 +1,93 @@ // Copyright (c) Mysten Labs, Inc. // SPDX-License-Identifier: Apache-2.0 -#[derive(Debug, Clone)] +use axum::http::StatusCode; +use axum::response::{IntoResponse, Response}; + +#[derive(Debug, thiserror::Error)] pub enum DeepBookError { - InternalError(String), + #[error("Resource not found: {resource}")] + NotFound { resource: String }, + + #[error("Database error: {0}")] + Database(String), + + #[error("Invalid request: {0}")] + BadRequest(String), + + #[error("RPC error: {0}")] + Rpc(String), + + #[error("Deserialization error: {0}")] + Deserialization(String), + + #[error("Internal error: {0}")] + Internal(String), +} + +impl DeepBookError { + pub fn not_found(resource: impl Into) -> Self { + Self::NotFound { + resource: resource.into(), + } + } + + pub fn database(msg: impl Into) -> Self { + Self::Database(msg.into()) + } + + pub fn bad_request(msg: impl Into) -> Self { + Self::BadRequest(msg.into()) + } + + pub fn rpc(msg: impl Into) -> Self { + Self::Rpc(msg.into()) + } + + pub fn deserialization(msg: impl Into) -> Self { + Self::Deserialization(msg.into()) + } + + pub fn internal(msg: impl Into) -> Self { + Self::Internal(msg.into()) + } +} + +impl IntoResponse for DeepBookError { + fn into_response(self) -> Response { + let (status, message) = match &self { + DeepBookError::NotFound { .. } => (StatusCode::NOT_FOUND, self.to_string()), + DeepBookError::BadRequest(_) => (StatusCode::BAD_REQUEST, self.to_string()), + DeepBookError::Database(_) + | DeepBookError::Rpc(_) + | DeepBookError::Deserialization(_) + | DeepBookError::Internal(_) => (StatusCode::INTERNAL_SERVER_ERROR, self.to_string()), + }; + + (status, message).into_response() + } +} + +impl From for DeepBookError { + fn from(err: diesel::result::Error) -> Self { + Self::Database(err.to_string()) + } +} + +impl From for DeepBookError { + fn from(err: anyhow::Error) -> Self { + Self::Internal(err.to_string()) + } +} + +impl From for DeepBookError { + fn from(err: sui_sdk::error::Error) -> Self { + Self::Rpc(err.to_string()) + } +} + +impl From for DeepBookError { + fn from(err: sui_types::base_types::ObjectIDParseError) -> Self { + Self::BadRequest(err.to_string()) + } } diff --git a/crates/server/src/lib.rs b/crates/server/src/lib.rs index 48554a4f2..7387d212d 100644 --- a/crates/server/src/lib.rs +++ b/crates/server/src/lib.rs @@ -2,4 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 pub mod error; +pub mod margin_metrics; +mod metrics; +mod reader; pub mod server; diff --git a/crates/server/src/main.rs b/crates/server/src/main.rs index 33776820b..fc3d57f93 100644 --- a/crates/server/src/main.rs +++ b/crates/server/src/main.rs @@ -3,15 +3,19 @@ use clap::Parser; use deepbook_server::server::run_server; -use std::net::{IpAddr, Ipv4Addr, SocketAddr}; -use sui_pg_db::{Db, DbArgs}; +use std::net::SocketAddr; +use sui_pg_db::DbArgs; use url::Url; #[derive(Parser)] #[clap(rename_all = "kebab-case", author, version)] struct Args { + #[command(flatten)] + db_args: DbArgs, #[clap(env, long, default_value_t = 9008)] server_port: u16, + #[clap(env, long, default_value = "0.0.0.0:9184")] + metrics_address: SocketAddr, #[clap( env, long, @@ -20,18 +24,64 @@ struct Args { database_url: Url, #[clap(env, long, default_value = "https://fullnode.mainnet.sui.io:443")] rpc_url: Url, + #[clap( + env, + long, + default_value = "0x2c8d603bc51326b8c13cef9dd07031a408a48dddb541963357661df5d3204809" + )] + deepbook_package_id: String, + #[clap( + env, + long, + default_value = "0xdeeb7a4662eec9f2f3def03fb937a663dddaa2e215b8078a284d026b7946c270" + )] + deep_token_package_id: String, + #[clap( + env, + long, + default_value = "0x032abf8948dda67a271bcc18e776dbbcfb0d58c8d288a700ff0d5521e57a1ffe" + )] + deep_treasury_id: String, + + // Margin metrics polling configuration + #[clap(env, long, default_value_t = 30)] + margin_poll_interval_secs: u64, + #[clap(env, long)] + margin_package_id: Option, } #[tokio::main] async fn main() -> Result<(), anyhow::Error> { + let _guard = telemetry_subscribers::TelemetryConfig::new() + .with_env() + .init(); + let Args { + db_args, server_port, + metrics_address, database_url, rpc_url, + deepbook_package_id, + deep_token_package_id, + deep_treasury_id, + margin_poll_interval_secs, + margin_package_id, } = Args::parse(); - let server_address = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)), server_port); - let db = Db::for_read(database_url, DbArgs::default()).await?; - run_server(server_address, db, rpc_url).await?; + + run_server( + server_port, + database_url, + db_args, + rpc_url, + metrics_address, + deepbook_package_id, + deep_token_package_id, + deep_treasury_id, + margin_poll_interval_secs, + margin_package_id, + ) + .await?; Ok(()) } diff --git a/crates/server/src/margin_metrics/metrics.rs b/crates/server/src/margin_metrics/metrics.rs new file mode 100644 index 000000000..1658af289 --- /dev/null +++ b/crates/server/src/margin_metrics/metrics.rs @@ -0,0 +1,191 @@ +use prometheus::{ + register_gauge_vec_with_registry, register_histogram_with_registry, + register_int_counter_with_registry, GaugeVec, Histogram, IntCounter, Registry, +}; +use std::sync::Arc; + +const LATENCY_SEC_BUCKETS: &[f64] = &[0.01, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0, 30.0]; + +#[derive(Clone)] +pub struct MarginMetrics { + // Per-pool metrics (labeled by pool_id and asset_type) + pub total_supply: GaugeVec, + pub total_borrow: GaugeVec, + pub vault_balance: GaugeVec, + pub supply_cap: GaugeVec, + pub interest_rate: GaugeVec, + pub available_withdrawal: GaugeVec, + pub utilization_rate: GaugeVec, + pub solvency_ratio: GaugeVec, + pub available_liquidity_pct: GaugeVec, + + // Operational metrics + pub poll_duration: Histogram, + pub poll_errors: IntCounter, + pub poll_success: IntCounter, +} + +impl MarginMetrics { + pub fn new(registry: &Registry) -> Arc { + Arc::new(Self { + total_supply: register_gauge_vec_with_registry!( + "margin_pool_total_supply", + "Total assets supplied to the margin pool (normalized by asset decimals)", + &["pool_id", "asset_type"], + registry + ) + .unwrap(), + + total_borrow: register_gauge_vec_with_registry!( + "margin_pool_total_borrow", + "Total assets borrowed from the margin pool (normalized by asset decimals)", + &["pool_id", "asset_type"], + registry + ) + .unwrap(), + + vault_balance: register_gauge_vec_with_registry!( + "margin_pool_vault_balance", + "Available liquidity in the margin pool vault (normalized by asset decimals)", + &["pool_id", "asset_type"], + registry + ) + .unwrap(), + + supply_cap: register_gauge_vec_with_registry!( + "margin_pool_supply_cap", + "Maximum allowed supply for the margin pool (normalized by asset decimals)", + &["pool_id", "asset_type"], + registry + ) + .unwrap(), + + interest_rate: register_gauge_vec_with_registry!( + "margin_pool_interest_rate", + "Current interest rate for the margin pool (normalized, 1.0 = 100%)", + &["pool_id", "asset_type"], + registry + ) + .unwrap(), + + available_withdrawal: register_gauge_vec_with_registry!( + "margin_pool_available_withdrawal", + "Maximum amount withdrawable without hitting rate limits (normalized by asset decimals)", + &["pool_id", "asset_type"], + registry + ) + .unwrap(), + + utilization_rate: register_gauge_vec_with_registry!( + "margin_pool_utilization_rate", + "Pool utilization rate (total_borrow / total_supply)", + &["pool_id", "asset_type"], + registry + ) + .unwrap(), + + solvency_ratio: register_gauge_vec_with_registry!( + "margin_pool_solvency_ratio", + "Pool solvency ratio (vault_balance / total_borrow, >1 = healthy)", + &["pool_id", "asset_type"], + registry + ) + .unwrap(), + + available_liquidity_pct: register_gauge_vec_with_registry!( + "margin_pool_available_liquidity_pct", + "Percentage of total supply available in vault", + &["pool_id", "asset_type"], + registry + ) + .unwrap(), + + // Operational + poll_duration: register_histogram_with_registry!( + "margin_rpc_poll_duration_seconds", + "Time taken to poll margin pool metrics via RPC", + LATENCY_SEC_BUCKETS.to_vec(), + registry + ) + .unwrap(), + + poll_errors: register_int_counter_with_registry!( + "margin_rpc_poll_errors_total", + "Number of failed margin pool metric polls", + registry + ) + .unwrap(), + + poll_success: register_int_counter_with_registry!( + "margin_rpc_poll_success_total", + "Number of successful margin pool metric polls", + registry + ) + .unwrap(), + }) + } + + pub fn update_pool_metrics( + &self, + pool_id: &str, + asset_type: &str, + total_supply: u64, + total_borrow: u64, + vault_balance: u64, + supply_cap: u64, + interest_rate: u64, + available_withdrawal: u64, + decimals: i16, + ) { + let divisor = 10_f64.powi(decimals as i32); + + self.total_supply + .with_label_values(&[pool_id, asset_type]) + .set(total_supply as f64 / divisor); + self.total_borrow + .with_label_values(&[pool_id, asset_type]) + .set(total_borrow as f64 / divisor); + self.vault_balance + .with_label_values(&[pool_id, asset_type]) + .set(vault_balance as f64 / divisor); + self.supply_cap + .with_label_values(&[pool_id, asset_type]) + .set(supply_cap as f64 / divisor); + // Interest rate uses 9 decimals + self.interest_rate + .with_label_values(&[pool_id, asset_type]) + .set(interest_rate as f64 / 1_000_000_000.0); + self.available_withdrawal + .with_label_values(&[pool_id, asset_type]) + .set(available_withdrawal as f64 / divisor); + + let utilization = if total_supply > 0 { + total_borrow as f64 / total_supply as f64 + } else { + 0.0 + }; + self.utilization_rate + .with_label_values(&[pool_id, asset_type]) + .set(utilization); + + let solvency = if total_borrow > 0 { + vault_balance as f64 / total_borrow as f64 + } else { + f64::INFINITY + }; + if solvency.is_finite() { + self.solvency_ratio + .with_label_values(&[pool_id, asset_type]) + .set(solvency); + } + + let liquidity_pct = if total_supply > 0 { + (vault_balance as f64 / total_supply as f64) * 100.0 + } else { + 100.0 + }; + self.available_liquidity_pct + .with_label_values(&[pool_id, asset_type]) + .set(liquidity_pct); + } +} diff --git a/crates/server/src/margin_metrics/mod.rs b/crates/server/src/margin_metrics/mod.rs new file mode 100644 index 000000000..7011b05a3 --- /dev/null +++ b/crates/server/src/margin_metrics/mod.rs @@ -0,0 +1,7 @@ +mod metrics; +mod poller; +mod rpc_client; + +pub use metrics::MarginMetrics; +pub use poller::MarginPoller; +pub use rpc_client::MarginRpcClient; diff --git a/crates/server/src/margin_metrics/poller.rs b/crates/server/src/margin_metrics/poller.rs new file mode 100644 index 000000000..609c645e0 --- /dev/null +++ b/crates/server/src/margin_metrics/poller.rs @@ -0,0 +1,225 @@ +use super::metrics::MarginMetrics; +use super::rpc_client::{MarginPoolState, MarginRpcClient}; +use anyhow::Result; +use deepbook_schema::models::NewMarginPoolSnapshot; +use deepbook_schema::schema::{assets, margin_pool_created, margin_pool_snapshots}; +use diesel::QueryDsl; +use diesel_async::RunQueryDsl; +use std::sync::Arc; +use std::time::Duration; +use sui_pg_db::Db; +use sui_sdk::SuiClientBuilder; +use tokio_util::sync::CancellationToken; +use url::Url; + +#[derive(Debug, Clone)] +struct MarginPoolInfo { + pool_id: String, + asset_type: String, + decimals: i16, +} + +pub struct MarginPoller { + db: Db, + rpc_url: Url, + margin_package_id: String, + metrics: Arc, + poll_interval: Duration, + cancellation_token: CancellationToken, +} + +impl MarginPoller { + pub fn new( + db: Db, + rpc_url: Url, + margin_package_id: String, + metrics: Arc, + poll_interval_secs: u64, + cancellation_token: CancellationToken, + ) -> Self { + Self { + db, + rpc_url, + margin_package_id, + metrics, + poll_interval: Duration::from_secs(poll_interval_secs), + cancellation_token, + } + } + + pub async fn run(self) -> Result<()> { + loop { + tokio::select! { + _ = self.cancellation_token.cancelled() => { + break; + } + _ = tokio::time::sleep(self.poll_interval) => { + if let Err(e) = self.poll_once().await { + eprintln!("[margin_poller] Failed to poll margin pool metrics: {}", e); + self.metrics.poll_errors.inc(); + } else { + self.metrics.poll_success.inc(); + } + } + } + } + + Ok(()) + } + + async fn poll_once(&self) -> Result<()> { + let timer = self.metrics.poll_duration.start_timer(); + + // 1. Get all margin pools from DB + let pools = self.get_margin_pools().await?; + + if pools.is_empty() { + timer.observe_duration(); + return Ok(()); + } + + // 2. Create RPC client + let sui_client = SuiClientBuilder::default() + .build(self.rpc_url.as_str()) + .await?; + let rpc_client = MarginRpcClient::new(sui_client, &self.margin_package_id)?; + + // 3. Query each pool and update metrics + for pool_info in &pools { + match rpc_client + .get_pool_state(&pool_info.pool_id, &pool_info.asset_type) + .await + { + Ok(state) => { + // Update Prometheus metrics with decimal normalization + self.metrics.update_pool_metrics( + &state.pool_id, + &state.asset_type, + state.total_supply, + state.total_borrow, + state.vault_balance, + state.supply_cap, + state.interest_rate, + state.available_withdrawal, + pool_info.decimals, + ); + + // Save snapshot to database + if let Err(e) = self.save_snapshot(&state).await { + eprintln!( + "[margin_poller] Failed to save snapshot for pool {}: {}", + state.pool_id, e + ); + } + } + Err(e) => { + eprintln!( + "[margin_poller] Failed to query pool {}: {}", + pool_info.pool_id, e + ); + } + } + } + + timer.observe_duration(); + + Ok(()) + } + + async fn get_margin_pools(&self) -> Result> { + let mut conn = self.db.connect().await?; + + // Get distinct margin pools from margin_pool_created table + let pools: Vec<(String, String)> = margin_pool_created::table + .select(( + margin_pool_created::margin_pool_id, + margin_pool_created::asset_type, + )) + .distinct() + .load(&mut conn) + .await?; + + // Build a map of normalized asset_type -> decimals from the assets table + let asset_decimals: Vec<(String, i16)> = assets::table + .select((assets::asset_type, assets::decimals)) + .load(&mut conn) + .await?; + + let decimals_map: std::collections::HashMap = asset_decimals + .into_iter() + .map(|(asset_type, decimals)| { + // Normalize by removing 0x prefix for comparison + let normalized = if asset_type.starts_with("0x") || asset_type.starts_with("0X") { + asset_type[2..].to_string() + } else { + asset_type + }; + (normalized, decimals) + }) + .collect(); + + Ok(pools + .into_iter() + .filter_map(|(pool_id, asset_type)| { + // Normalize the margin pool asset_type for lookup + let normalized = if asset_type.starts_with("0x") || asset_type.starts_with("0X") { + asset_type[2..].to_string() + } else { + asset_type.clone() + }; + + decimals_map + .get(&normalized) + .map(|&decimals| MarginPoolInfo { + pool_id, + asset_type, + decimals, + }) + }) + .collect()) + } + + async fn save_snapshot(&self, state: &MarginPoolState) -> Result<()> { + let mut conn = self.db.connect().await?; + + // Compute derived metrics + let utilization_rate = if state.total_supply > 0 { + state.total_borrow as f64 / state.total_supply as f64 + } else { + 0.0 + }; + + let solvency_ratio = if state.total_borrow > 0 { + Some(state.vault_balance as f64 / state.total_borrow as f64) + } else { + None + }; + + let available_liquidity_pct = if state.total_supply > 0 { + Some((state.vault_balance as f64 / state.total_supply as f64) * 100.0) + } else { + None + }; + + let snapshot = NewMarginPoolSnapshot { + margin_pool_id: state.pool_id.clone(), + asset_type: state.asset_type.clone(), + total_supply: state.total_supply as i64, + total_borrow: state.total_borrow as i64, + vault_balance: state.vault_balance as i64, + supply_cap: state.supply_cap as i64, + interest_rate: state.interest_rate as i64, + available_withdrawal: state.available_withdrawal as i64, + utilization_rate, + solvency_ratio, + available_liquidity_pct, + }; + + diesel::insert_into(margin_pool_snapshots::table) + .values(&snapshot) + .execute(&mut conn) + .await?; + + Ok(()) + } +} diff --git a/crates/server/src/margin_metrics/rpc_client.rs b/crates/server/src/margin_metrics/rpc_client.rs new file mode 100644 index 000000000..5d0bc0b15 --- /dev/null +++ b/crates/server/src/margin_metrics/rpc_client.rs @@ -0,0 +1,235 @@ +use anyhow::{anyhow, Result}; +use std::str::FromStr; +use sui_sdk::SuiClient; +use sui_types::{ + base_types::{ObjectID, SequenceNumber, SuiAddress}, + programmable_transaction_builder::ProgrammableTransactionBuilder, + transaction::{Argument, CallArg, Command, ObjectArg, ProgrammableMoveCall, TransactionKind}, + type_input::TypeInput, + TypeTag, +}; + +const MARGIN_POOL_MODULE: &str = "margin_pool"; +const SUI_CLOCK_OBJECT_ID: &str = + "0x0000000000000000000000000000000000000000000000000000000000000006"; + +/// Normalize asset type by ensuring the address has a 0x prefix. +/// The DB stores types like "abc123::module::Type" but TypeTag parser needs "0xabc123::module::Type" +fn normalize_asset_type(asset_type: &str) -> String { + if asset_type.starts_with("0x") || asset_type.starts_with("0X") { + asset_type.to_string() + } else { + format!("0x{}", asset_type) + } +} + +#[derive(Debug, Clone)] +pub struct MarginPoolState { + pub pool_id: String, + pub asset_type: String, + pub total_supply: u64, + pub total_borrow: u64, + pub vault_balance: u64, + pub supply_cap: u64, + pub interest_rate: u64, + pub available_withdrawal: u64, +} + +pub struct MarginRpcClient { + sui_client: SuiClient, + margin_package_id: ObjectID, +} + +impl MarginRpcClient { + pub fn new(sui_client: SuiClient, margin_package_id: &str) -> Result { + let package_id = ObjectID::from_hex_literal(margin_package_id) + .map_err(|e| anyhow!("Invalid margin package ID: {}", e))?; + Ok(Self { + sui_client, + margin_package_id: package_id, + }) + } + + pub async fn get_pool_state(&self, pool_id: &str, asset_type: &str) -> Result { + let pool_object_id = ObjectID::from_hex_literal(pool_id) + .map_err(|e| anyhow!("Invalid pool ID '{}': {}", pool_id, e))?; + + // Get the pool object to find its initial_shared_version + let pool_object = self + .sui_client + .read_api() + .get_object_with_options( + pool_object_id, + sui_json_rpc_types::SuiObjectDataOptions::full_content().with_owner(), + ) + .await?; + + let pool_data = pool_object + .data + .as_ref() + .ok_or_else(|| anyhow!("Pool {} not found", pool_id))?; + + let initial_shared_version = match &pool_data.owner { + Some(sui_types::object::Owner::Shared { + initial_shared_version, + }) => *initial_shared_version, + _ => return Err(anyhow!("Pool {} is not a shared object", pool_id)), + }; + + // Parse the asset type for type arguments + // The asset_type from DB may be missing the 0x prefix, so normalize it + let normalized_asset_type = normalize_asset_type(asset_type); + let type_tag = TypeTag::from_str(&normalized_asset_type) + .map_err(|e| anyhow!("Invalid asset type '{}': {}", normalized_asset_type, e))?; + + // Query all the view functions in a single PTB + let state = self + .query_pool_state(pool_object_id, initial_shared_version, &type_tag) + .await?; + + Ok(MarginPoolState { + pool_id: pool_object_id.to_hex_literal(), + asset_type: normalized_asset_type, + total_supply: state.0, + total_borrow: state.1, + vault_balance: state.2, + supply_cap: state.3, + interest_rate: state.4, + available_withdrawal: state.5, + }) + } + + async fn query_pool_state( + &self, + pool_id: ObjectID, + initial_shared_version: SequenceNumber, + type_tag: &TypeTag, + ) -> Result<(u64, u64, u64, u64, u64, u64)> { + let mut ptb = ProgrammableTransactionBuilder::new(); + + // Input 0: Pool object + let pool_input = CallArg::Object(ObjectArg::SharedObject { + id: pool_id, + initial_shared_version, + mutability: sui_types::transaction::SharedObjectMutability::Immutable, + }); + ptb.input(pool_input)?; + + // Input 1: Clock object (for get_available_withdrawal) + let clock_id = ObjectID::from_hex_literal(SUI_CLOCK_OBJECT_ID)?; + let clock_input = CallArg::Object(ObjectArg::SharedObject { + id: clock_id, + initial_shared_version: SequenceNumber::from_u64(1), + mutability: sui_types::transaction::SharedObjectMutability::Immutable, + }); + ptb.input(clock_input)?; + + let type_input = TypeInput::from(type_tag.clone()); + let type_args = vec![type_input]; + + // Command 0: total_supply(pool) + ptb.command(Command::MoveCall(Box::new(ProgrammableMoveCall { + package: self.margin_package_id, + module: MARGIN_POOL_MODULE.to_string(), + function: "total_supply".to_string(), + type_arguments: type_args.clone(), + arguments: vec![Argument::Input(0)], + }))); + + // Command 1: total_borrow(pool) + ptb.command(Command::MoveCall(Box::new(ProgrammableMoveCall { + package: self.margin_package_id, + module: MARGIN_POOL_MODULE.to_string(), + function: "total_borrow".to_string(), + type_arguments: type_args.clone(), + arguments: vec![Argument::Input(0)], + }))); + + // Command 2: vault_balance(pool) + ptb.command(Command::MoveCall(Box::new(ProgrammableMoveCall { + package: self.margin_package_id, + module: MARGIN_POOL_MODULE.to_string(), + function: "vault_balance".to_string(), + type_arguments: type_args.clone(), + arguments: vec![Argument::Input(0)], + }))); + + // Command 3: supply_cap(pool) + ptb.command(Command::MoveCall(Box::new(ProgrammableMoveCall { + package: self.margin_package_id, + module: MARGIN_POOL_MODULE.to_string(), + function: "supply_cap".to_string(), + type_arguments: type_args.clone(), + arguments: vec![Argument::Input(0)], + }))); + + // Command 4: interest_rate(pool) + ptb.command(Command::MoveCall(Box::new(ProgrammableMoveCall { + package: self.margin_package_id, + module: MARGIN_POOL_MODULE.to_string(), + function: "interest_rate".to_string(), + type_arguments: type_args.clone(), + arguments: vec![Argument::Input(0)], + }))); + + // Command 5: get_available_withdrawal(pool, clock) + ptb.command(Command::MoveCall(Box::new(ProgrammableMoveCall { + package: self.margin_package_id, + module: MARGIN_POOL_MODULE.to_string(), + function: "get_available_withdrawal".to_string(), + type_arguments: type_args, + arguments: vec![Argument::Input(0), Argument::Input(1)], + }))); + + let builder = ptb.finish(); + let tx = TransactionKind::ProgrammableTransaction(builder); + + let result = self + .sui_client + .read_api() + .dev_inspect_transaction_block(SuiAddress::default(), tx, None, None, None) + .await?; + + let results = result + .results + .ok_or_else(|| anyhow!("No results from dev_inspect_transaction_block"))?; + + // Extract each u64 result + let total_supply = self.extract_u64(&results, 0, "total_supply")?; + let total_borrow = self.extract_u64(&results, 1, "total_borrow")?; + let vault_balance = self.extract_u64(&results, 2, "vault_balance")?; + let supply_cap = self.extract_u64(&results, 3, "supply_cap")?; + let interest_rate = self.extract_u64(&results, 4, "interest_rate")?; + let available_withdrawal = self.extract_u64(&results, 5, "get_available_withdrawal")?; + + Ok(( + total_supply, + total_borrow, + vault_balance, + supply_cap, + interest_rate, + available_withdrawal, + )) + } + + fn extract_u64( + &self, + results: &[sui_json_rpc_types::SuiExecutionResult], + index: usize, + func_name: &str, + ) -> Result { + let result = results + .get(index) + .ok_or_else(|| anyhow!("Missing result for {}", func_name))?; + + let bytes = result + .return_values + .first() + .ok_or_else(|| anyhow!("No return value for {}", func_name))?; + + let value: u64 = bcs::from_bytes(&bytes.0) + .map_err(|e| anyhow!("Failed to deserialize {} result: {}", func_name, e))?; + + Ok(value) + } +} diff --git a/crates/server/src/metrics/middleware.rs b/crates/server/src/metrics/middleware.rs new file mode 100644 index 000000000..f23d34c99 --- /dev/null +++ b/crates/server/src/metrics/middleware.rs @@ -0,0 +1,56 @@ +use std::sync::Arc; + +use crate::server::AppState; +use axum::{ + body::Body, + extract::{MatchedPath, State}, + http::Request, + middleware::Next, + response::IntoResponse, +}; + +// Axum middleware to track metrics +pub(crate) async fn track_metrics( + State(app): State>, + req: Request, + next: Next, +) -> impl IntoResponse { + let axum_route = req + .extensions() + .get::() + .map(|p| p.as_str()) // Gets the route name e.g. `/v1/resolution/{*name}` + .unwrap_or("/UNSUPPORTED") + .to_string(); + + let route_labels = [axum_route.as_str()]; + + // check timers too. + let _guard = app + .metrics() + .request_latency + .with_label_values(&route_labels) + .start_timer(); + + app.metrics() + .requests_received + .with_label_values(&route_labels) + .inc(); + + let response = next.run(req).await; + let status = response.status(); + + // save success/failure metrics + if status.is_success() { + app.metrics() + .requests_succeeded + .with_label_values(&route_labels) + .inc(); + } else { + app.metrics() + .requests_failed + .with_label_values(&[axum_route.as_str(), status.as_str()]) + .inc(); + } + + response +} diff --git a/crates/server/src/metrics/mod.rs b/crates/server/src/metrics/mod.rs new file mode 100644 index 000000000..453d2e09e --- /dev/null +++ b/crates/server/src/metrics/mod.rs @@ -0,0 +1,85 @@ +pub mod middleware; + +use prometheus::{ + register_histogram_vec_with_registry, register_histogram_with_registry, + register_int_counter_vec_with_registry, register_int_counter_with_registry, Histogram, + HistogramVec, IntCounter, IntCounterVec, Registry, +}; +use std::sync::Arc; + +/// Histogram buckets for the distribution of latency (time between receiving a request and sending +/// a response). +const LATENCY_SEC_BUCKETS: &[f64] = &[ + 0.001, 0.002, 0.005, 0.01, 0.02, 0.05, 0.1, 0.2, 0.5, 1.0, 2.0, 5.0, 10.0, 20.0, 50.0, 100.0, + 200.0, 500.0, 1000.0, +]; + +#[derive(Clone)] +pub struct RpcMetrics { + pub db_latency: Histogram, + pub db_requests_succeeded: IntCounter, + pub db_requests_failed: IntCounter, + + pub request_latency: HistogramVec, + pub requests_received: IntCounterVec, + pub requests_succeeded: IntCounterVec, + pub requests_failed: IntCounterVec, +} + +impl RpcMetrics { + pub(crate) fn new(registry: &Registry) -> Arc { + Arc::new(Self { + db_latency: register_histogram_with_registry!( + "db_latency", + "Time taken by the database to respond to queries", + LATENCY_SEC_BUCKETS.to_vec(), + registry, + ).unwrap(), + + db_requests_succeeded: register_int_counter_with_registry!( + "db_requests_succeeded", + "Number of database requests that completed successfully", + registry, + ).unwrap(), + + db_requests_failed: register_int_counter_with_registry!( + "db_requests_failed", + "Number of database requests that completed with an error", + registry, + ).unwrap(), + + request_latency: register_histogram_vec_with_registry!( + "deepbook_api_request_latency", + "Time taken to respond to Deepbook API requests, by method", + &["method"], + LATENCY_SEC_BUCKETS.to_vec(), + registry + ) + .unwrap(), + + requests_received: register_int_counter_vec_with_registry!( + "deepbook_api_requests_received", + "Number of requests initiated for each Deepbook API method", + &["method"], + registry + ) + .unwrap(), + + requests_succeeded: register_int_counter_vec_with_registry!( + "deepbook_api_requests_succeeded", + "Number of requests that completed successfully for each Deepbook API method", + &["method"], + registry + ) + .unwrap(), + + requests_failed: register_int_counter_vec_with_registry!( + "deepbook_api_requests_failed", + "Number of requests that completed with an error for each Deepbook API method, by error code", + &["method", "code"], + registry + ) + .unwrap(), + }) + } +} diff --git a/crates/server/src/reader.rs b/crates/server/src/reader.rs new file mode 100644 index 000000000..896f372e8 --- /dev/null +++ b/crates/server/src/reader.rs @@ -0,0 +1,1399 @@ +use crate::error::DeepBookError; +use crate::metrics::RpcMetrics; +use deepbook_schema::models::{ + AssetSupplied, AssetWithdrawn, CollateralEvent, DeepbookPoolConfigUpdated, + DeepbookPoolRegistered, DeepbookPoolUpdated, DeepbookPoolUpdatedRegistry, + InterestParamsUpdated, Liquidation, LoanBorrowed, LoanRepaid, MaintainerCapUpdated, + MaintainerFeesWithdrawn, MarginManagerCreated, MarginManagerState, MarginPoolConfigUpdated, + MarginPoolCreated, OrderFillSummary, OrderStatus, PauseCapUpdated, Pools, + ProtocolFeesIncreasedEvent, ProtocolFeesWithdrawn, ReferralFeeEvent, ReferralFeesClaimedEvent, + SupplierCapMinted, SupplyReferralMinted, +}; +use deepbook_schema::schema; +use diesel::deserialize::FromSqlRow; +use diesel::dsl::sql; +use diesel::expression::QueryMetadata; +use diesel::pg::Pg; +use diesel::query_builder::{Query, QueryFragment, QueryId}; +use diesel::query_dsl::CompatibleType; +use diesel::sql_types::{BigInt, Double}; +use diesel::{ + BoolExpressionMethods, ExpressionMethods, QueryDsl, QueryableByName, SelectableHelper, + TextExpressionMethods, +}; + +/// Converts an empty string to "%" for SQL LIKE pattern matching. +/// This allows using required parameters instead of Option, +/// avoiding the need for boxed queries with dynamic filters. +fn to_pattern(s: &str) -> String { + if s.is_empty() { + "%".to_string() + } else { + s.to_string() + } +} +use diesel_async::methods::LoadQuery; +use diesel_async::{AsyncPgConnection, RunQueryDsl}; +use prometheus::Registry; +use std::sync::Arc; +use sui_indexer_alt_metrics::db::DbConnectionStatsCollector; +use sui_pg_db::{Db, DbArgs}; +use url::Url; + +#[derive(QueryableByName, Debug)] +struct OhclvRow { + #[diesel(sql_type = BigInt)] + timestamp_ms: i64, + #[diesel(sql_type = Double)] + open: f64, + #[diesel(sql_type = Double)] + high: f64, + #[diesel(sql_type = Double)] + low: f64, + #[diesel(sql_type = Double)] + close: f64, + #[diesel(sql_type = Double)] + base_volume: f64, +} + +#[derive(Clone)] +pub struct Reader { + db: Db, + metrics: Arc, +} + +impl Reader { + pub(crate) async fn new( + database_url: Url, + db_args: DbArgs, + metrics: Arc, + registry: &Registry, + ) -> Result { + let db = Db::for_read(database_url, db_args).await?; + registry.register(Box::new(DbConnectionStatsCollector::new( + Some("deepbook_api_db"), + db.clone(), + )))?; + + // Try to open a read connection to verify we can + // connect to the DB on startup. + let _ = db.connect().await?; + + Ok(Self { db, metrics }) + } + + pub(crate) async fn results(&self, query: Q) -> Result, anyhow::Error> + where + U: Send, + Q: RunQueryDsl + 'static, + Q: LoadQuery<'static, AsyncPgConnection, U> + QueryFragment + Send, + { + let mut conn = self.db.connect().await?; + let _guard = self.metrics.db_latency.start_timer(); + let res = query.get_results(&mut conn).await; + + if res.is_ok() { + self.metrics.db_requests_succeeded.inc(); + } else { + self.metrics.db_requests_failed.inc(); + } + + Ok(res?) + } + + pub async fn first<'q, Q, ST, U>(&self, query: Q) -> Result + where + Q: diesel::query_dsl::limit_dsl::LimitDsl, + Q::Output: Query + QueryFragment + QueryId + Send + 'q, + ::SqlType: CompatibleType, + U: Send + FromSqlRow + 'static, + Pg: QueryMetadata<::SqlType>, + ST: 'static, + { + let mut conn = self.db.connect().await?; + let _guard = self.metrics.db_latency.start_timer(); + + let res = query.first(&mut conn).await; + if res.is_ok() { + self.metrics.db_requests_succeeded.inc(); + } else { + self.metrics.db_requests_failed.inc(); + } + + Ok(res?) + } + + pub async fn get_pools(&self) -> Result, DeepBookError> { + Ok(self + .results(schema::pools::table.select(Pools::as_select())) + .await?) + } + + pub async fn get_historical_volume( + &self, + start_time: i64, + end_time: i64, + pool_ids: &Vec, + volume_in_base: bool, + ) -> Result, DeepBookError> { + let column_to_query = if volume_in_base { + sql::("base_quantity") + } else { + sql::("quote_quantity") + }; + + let query = schema::order_fills::table + .filter(schema::order_fills::checkpoint_timestamp_ms.between(start_time, end_time)) + .filter(schema::order_fills::pool_id.eq_any(pool_ids.clone())) + .select((schema::order_fills::pool_id, column_to_query)); + + Ok(self.results(query).await?) + } + + pub async fn get_order_fill_summary( + &self, + start_time: i64, + end_time: i64, + pool_ids: &Vec, + balance_manager_id: &str, + volume_in_base: bool, + ) -> Result, DeepBookError> { + let column_to_query = if volume_in_base { + sql::("base_quantity") + } else { + sql::("quote_quantity") + }; + let balance_manager_id = balance_manager_id.to_string(); + let query = schema::order_fills::table + .select(( + schema::order_fills::pool_id, + schema::order_fills::maker_balance_manager_id, + schema::order_fills::taker_balance_manager_id, + column_to_query, + )) + .filter(schema::order_fills::pool_id.eq_any(pool_ids.clone())) + .filter(schema::order_fills::checkpoint_timestamp_ms.between(start_time, end_time)) + .filter( + schema::order_fills::maker_balance_manager_id + .eq(balance_manager_id.clone()) + .or(schema::order_fills::taker_balance_manager_id.eq(balance_manager_id)), + ); + Ok(self.results(query).await?) + } + + pub async fn get_price( + &self, + start_time: i64, + end_time: i64, + pool_id: &str, + ) -> Result { + let query = schema::order_fills::table + .filter(schema::order_fills::checkpoint_timestamp_ms.between(start_time, end_time)) + .filter(schema::order_fills::pool_id.eq(pool_id)) + .order_by(schema::order_fills::checkpoint_timestamp_ms.desc()) + .select(schema::order_fills::price); + Ok(self.first(query).await?) + } + + pub async fn get_pool_decimals( + &self, + pool_name: &str, + ) -> Result<(String, i16, i16), DeepBookError> { + let query = schema::pools::table + .filter(schema::pools::pool_name.eq(pool_name)) + .select(( + schema::pools::pool_id, + schema::pools::base_asset_decimals, + schema::pools::quote_asset_decimals, + )); + self.first(query) + .await + .map_err(|_| DeepBookError::not_found(format!("Pool '{}'", pool_name))) + } + + pub async fn get_orders( + &self, + pool_name: String, + pool_id: String, + start_time: i64, + end_time: i64, + limit: i64, + maker_balance_manager: Option, + taker_balance_manager: Option, + balance_manager: Option, + ) -> Result< + Vec<( + String, + String, + String, + String, + i64, + i64, + i64, + i64, + i64, + i64, + bool, + String, + String, + bool, + bool, + i64, + i64, + )>, + DeepBookError, + > { + let mut connection = self.db.connect().await?; + // Build the query dynamically + let mut query = schema::order_fills::table + .filter(schema::order_fills::pool_id.eq(pool_id)) + .filter(schema::order_fills::checkpoint_timestamp_ms.between(start_time, end_time)) + .into_boxed(); + + // Apply optional filters if parameters are provided + if let Some(maker_id) = maker_balance_manager { + query = query.filter(schema::order_fills::maker_balance_manager_id.eq(maker_id)); + } + if let Some(taker_id) = taker_balance_manager { + query = query.filter(schema::order_fills::taker_balance_manager_id.eq(taker_id)); + } + if let Some(bm_id) = balance_manager { + query = query.filter( + schema::order_fills::maker_balance_manager_id + .eq(bm_id.clone()) + .or(schema::order_fills::taker_balance_manager_id.eq(bm_id)), + ); + } + + let _guard = self.metrics.db_latency.start_timer(); + + // Fetch latest trades (sorted by timestamp in descending order) within the time range, applying the limit + let res = query + .order_by(schema::order_fills::checkpoint_timestamp_ms.desc()) // Ensures latest trades come first + .limit(limit) // Apply limit to get the most recent trades + .select(( + schema::order_fills::event_digest, + schema::order_fills::digest, + schema::order_fills::maker_order_id, + schema::order_fills::taker_order_id, + schema::order_fills::maker_client_order_id, + schema::order_fills::taker_client_order_id, + schema::order_fills::price, + schema::order_fills::base_quantity, + schema::order_fills::quote_quantity, + schema::order_fills::checkpoint_timestamp_ms, + schema::order_fills::taker_is_bid, + schema::order_fills::maker_balance_manager_id, + schema::order_fills::taker_balance_manager_id, + schema::order_fills::taker_fee_is_deep, + schema::order_fills::maker_fee_is_deep, + schema::order_fills::taker_fee, + schema::order_fills::maker_fee, + )) + .load::<( + String, + String, + String, + String, + i64, + i64, + i64, + i64, + i64, + i64, + bool, + String, + String, + bool, + bool, + i64, + i64, + )>(&mut connection) + .await + .map_err(|_| { + DeepBookError::not_found(format!( + "Trades for pool '{}' in the specified time range", + pool_name + )) + }); + + if res.is_ok() { + self.metrics.db_requests_succeeded.inc(); + } else { + self.metrics.db_requests_failed.inc(); + } + res + } + + pub async fn get_order_updates( + &self, + pool_id: String, + start_time: i64, + end_time: i64, + limit: i64, + balance_manager_filter: Option, + status_filter: Option, + ) -> Result, DeepBookError> { + let mut connection = self.db.connect().await?; + let mut query = schema::order_updates::table + .filter(schema::order_updates::checkpoint_timestamp_ms.between(start_time, end_time)) + .filter(schema::order_updates::pool_id.eq(pool_id)) + .order_by(schema::order_updates::checkpoint_timestamp_ms.desc()) + .select(( + schema::order_updates::order_id, + schema::order_updates::price, + schema::order_updates::original_quantity, + schema::order_updates::quantity, + schema::order_updates::filled_quantity, + schema::order_updates::checkpoint_timestamp_ms, + schema::order_updates::is_bid, + schema::order_updates::balance_manager_id, + schema::order_updates::status, + )) + .limit(limit) + .into_boxed(); + + if let Some(manager_id) = balance_manager_filter { + query = query.filter(schema::order_updates::balance_manager_id.eq(manager_id)); + } + + if let Some(status) = status_filter { + query = query.filter(schema::order_updates::status.eq(status)); + } + + let _guard = self.metrics.db_latency.start_timer(); + + let res = query + .load::<(String, i64, i64, i64, i64, i64, bool, String, String)>(&mut connection) + .await + .map_err(|_| DeepBookError::database("Error fetching trade details")); + + if res.is_ok() { + self.metrics.db_requests_succeeded.inc(); + } else { + self.metrics.db_requests_failed.inc(); + } + res + } + + pub async fn get_orders_status( + &self, + pool_id: String, + limit: i64, + balance_manager_filter: Option, + status_filter: Option>, + ) -> Result, DeepBookError> { + let mut connection = self.db.connect().await?; + let _guard = self.metrics.db_latency.start_timer(); + + let balance_manager_clause = balance_manager_filter + .map(|id| format!("AND balance_manager_id = '{}'", id)) + .unwrap_or_default(); + + let status_clause = status_filter + .map(|statuses| { + let status_list = statuses + .iter() + .map(|s| format!("'{}'", s)) + .collect::>() + .join(", "); + format!("WHERE current_status IN ({})", status_list) + }) + .unwrap_or_default(); + + let query_str = format!( + r#" + WITH latest_events AS ( + SELECT DISTINCT ON (order_id) + order_id, + balance_manager_id, + is_bid, + status as event_status, + price, + original_quantity, + filled_quantity, + quantity as remaining_quantity, + checkpoint_timestamp_ms as last_updated_at + FROM order_updates + WHERE pool_id = '{pool_id}' + {balance_manager_clause} + ORDER BY order_id, checkpoint_timestamp_ms DESC + ), + placed_events AS ( + SELECT DISTINCT ON (order_id) + order_id, + checkpoint_timestamp_ms as placed_at + FROM order_updates + WHERE pool_id = '{pool_id}' + AND status = 'Placed' + ORDER BY order_id, checkpoint_timestamp_ms ASC + ), + order_status AS ( + SELECT + le.order_id, + le.balance_manager_id, + le.is_bid, + CASE + WHEN le.event_status = 'Canceled' THEN 'canceled' + WHEN le.event_status = 'Expired' THEN 'expired' + WHEN le.filled_quantity >= le.original_quantity THEN 'filled' + WHEN le.filled_quantity > 0 THEN 'partially_filled' + ELSE 'placed' + END as current_status, + le.price, + COALESCE(pe.placed_at, le.last_updated_at) as placed_at, + le.last_updated_at, + le.original_quantity, + le.filled_quantity, + le.remaining_quantity + FROM latest_events le + LEFT JOIN placed_events pe ON le.order_id = pe.order_id + ) + SELECT + order_id::text, + balance_manager_id::text, + is_bid, + current_status::text, + price, + placed_at, + last_updated_at, + original_quantity, + filled_quantity, + remaining_quantity + FROM order_status + {status_clause} + ORDER BY last_updated_at DESC + LIMIT {limit} + "#, + pool_id = pool_id, + balance_manager_clause = balance_manager_clause, + status_clause = status_clause, + limit = limit, + ); + + let res = diesel::sql_query(query_str) + .load::(&mut connection) + .await + .map_err(|e| DeepBookError::database(format!("Error fetching order status: {}", e))); + + if res.is_ok() { + self.metrics.db_requests_succeeded.inc(); + } else { + self.metrics.db_requests_failed.inc(); + } + res + } + + pub async fn get_ohclv( + &self, + pool_id: String, + interval: String, + start_time: Option, + end_time: Option, + limit: Option, + ) -> Result, DeepBookError> { + let mut connection = self.db.connect().await?; + let limit_val = limit.unwrap_or(1000); + let _guard = self.metrics.db_latency.start_timer(); + let query_str = format!( + "SELECT EXTRACT(EPOCH FROM bucket_time)::bigint * 1000 as timestamp_ms, \ + open::float8, high::float8, low::float8, close::float8, base_volume::float8 \ + FROM get_ohclv('{}', '{}', {}::timestamp, {}::timestamp, {})", + interval, + pool_id, + start_time + .map(|ts| format!("to_timestamp({})", ts / 1000)) + .unwrap_or_else(|| "NULL".to_string()), + end_time + .map(|ts| format!("to_timestamp({})", ts / 1000)) + .unwrap_or_else(|| "NULL".to_string()), + limit_val + ); + + let res = diesel::sql_query(query_str) + .load::(&mut connection) + .await + .map_err(|e| DeepBookError::database(format!("Error fetching OHCLV data: {}", e))) + .map(|rows| { + rows.into_iter() + .map(|row| { + ( + row.timestamp_ms, + row.open, + row.high, + row.low, + row.close, + row.base_volume, + ) + }) + .collect() + }); + + if res.is_ok() { + self.metrics.db_requests_succeeded.inc(); + } else { + self.metrics.db_requests_failed.inc(); + } + res + } + + // === Deepbook Margin Events === + pub async fn get_margin_manager_created( + &self, + start_time: i64, + end_time: i64, + limit: i64, + margin_manager_id_filter: String, + ) -> Result, DeepBookError> { + let query = schema::margin_manager_created::table + .select(MarginManagerCreated::as_select()) + .filter( + schema::margin_manager_created::checkpoint_timestamp_ms + .between(start_time, end_time), + ) + .filter( + schema::margin_manager_created::margin_manager_id + .like(to_pattern(&margin_manager_id_filter)), + ) + .order_by(schema::margin_manager_created::checkpoint_timestamp_ms.desc()) + .limit(limit); + + Ok(self.results(query).await?) + } + + pub async fn get_loan_borrowed( + &self, + start_time: i64, + end_time: i64, + limit: i64, + margin_manager_id_filter: String, + margin_pool_id_filter: String, + ) -> Result, DeepBookError> { + let query = schema::loan_borrowed::table + .select(LoanBorrowed::as_select()) + .filter(schema::loan_borrowed::checkpoint_timestamp_ms.between(start_time, end_time)) + .filter( + schema::loan_borrowed::margin_manager_id + .like(to_pattern(&margin_manager_id_filter)), + ) + .filter(schema::loan_borrowed::margin_pool_id.like(to_pattern(&margin_pool_id_filter))) + .order_by(schema::loan_borrowed::checkpoint_timestamp_ms.desc()) + .limit(limit); + + Ok(self.results(query).await?) + } + + pub async fn get_loan_repaid( + &self, + start_time: i64, + end_time: i64, + limit: i64, + margin_manager_id_filter: String, + margin_pool_id_filter: String, + ) -> Result, DeepBookError> { + let query = schema::loan_repaid::table + .select(LoanRepaid::as_select()) + .filter(schema::loan_repaid::checkpoint_timestamp_ms.between(start_time, end_time)) + .filter( + schema::loan_repaid::margin_manager_id.like(to_pattern(&margin_manager_id_filter)), + ) + .filter(schema::loan_repaid::margin_pool_id.like(to_pattern(&margin_pool_id_filter))) + .order_by(schema::loan_repaid::checkpoint_timestamp_ms.desc()) + .limit(limit); + + Ok(self.results(query).await?) + } + + pub async fn get_liquidation( + &self, + start_time: i64, + end_time: i64, + limit: i64, + margin_manager_id_filter: String, + margin_pool_id_filter: String, + ) -> Result, DeepBookError> { + let query = schema::liquidation::table + .select(Liquidation::as_select()) + .filter(schema::liquidation::checkpoint_timestamp_ms.between(start_time, end_time)) + .filter( + schema::liquidation::margin_manager_id.like(to_pattern(&margin_manager_id_filter)), + ) + .filter(schema::liquidation::margin_pool_id.like(to_pattern(&margin_pool_id_filter))) + .order_by(schema::liquidation::checkpoint_timestamp_ms.desc()) + .limit(limit); + + Ok(self.results(query).await?) + } + + pub async fn get_asset_supplied( + &self, + start_time: i64, + end_time: i64, + limit: i64, + margin_pool_id_filter: String, + supplier_filter: String, + ) -> Result, DeepBookError> { + let query = schema::asset_supplied::table + .select(AssetSupplied::as_select()) + .filter(schema::asset_supplied::checkpoint_timestamp_ms.between(start_time, end_time)) + .filter(schema::asset_supplied::margin_pool_id.like(to_pattern(&margin_pool_id_filter))) + .filter(schema::asset_supplied::supplier.like(to_pattern(&supplier_filter))) + .order_by(schema::asset_supplied::checkpoint_timestamp_ms.desc()) + .limit(limit); + + Ok(self.results(query).await?) + } + + pub async fn get_asset_withdrawn( + &self, + start_time: i64, + end_time: i64, + limit: i64, + margin_pool_id_filter: String, + supplier_filter: String, + ) -> Result, DeepBookError> { + let query = schema::asset_withdrawn::table + .select(AssetWithdrawn::as_select()) + .filter(schema::asset_withdrawn::checkpoint_timestamp_ms.between(start_time, end_time)) + .filter( + schema::asset_withdrawn::margin_pool_id.like(to_pattern(&margin_pool_id_filter)), + ) + .filter(schema::asset_withdrawn::supplier.like(to_pattern(&supplier_filter))) + .order_by(schema::asset_withdrawn::checkpoint_timestamp_ms.desc()) + .limit(limit); + + Ok(self.results(query).await?) + } + + pub async fn get_margin_pool_created( + &self, + margin_pool_id_filter: String, + ) -> Result, DeepBookError> { + let query = schema::margin_pool_created::table + .select(MarginPoolCreated::as_select()) + .filter( + schema::margin_pool_created::margin_pool_id + .like(to_pattern(&margin_pool_id_filter)), + ) + .order_by(schema::margin_pool_created::checkpoint_timestamp_ms.desc()); + + Ok(self.results(query).await?) + } + + pub async fn get_deepbook_pool_updated( + &self, + start_time: i64, + end_time: i64, + limit: i64, + margin_pool_id_filter: String, + deepbook_pool_id_filter: String, + ) -> Result, DeepBookError> { + let query = schema::deepbook_pool_updated::table + .select(DeepbookPoolUpdated::as_select()) + .filter( + schema::deepbook_pool_updated::checkpoint_timestamp_ms + .between(start_time, end_time), + ) + .filter( + schema::deepbook_pool_updated::margin_pool_id + .like(to_pattern(&margin_pool_id_filter)), + ) + .filter( + schema::deepbook_pool_updated::deepbook_pool_id + .like(to_pattern(&deepbook_pool_id_filter)), + ) + .order_by(schema::deepbook_pool_updated::checkpoint_timestamp_ms.desc()) + .limit(limit); + + Ok(self.results(query).await?) + } + + pub async fn get_interest_params_updated( + &self, + start_time: i64, + end_time: i64, + limit: i64, + margin_pool_id_filter: String, + ) -> Result, DeepBookError> { + let query = schema::interest_params_updated::table + .select(InterestParamsUpdated::as_select()) + .filter( + schema::interest_params_updated::checkpoint_timestamp_ms + .between(start_time, end_time), + ) + .filter( + schema::interest_params_updated::margin_pool_id + .like(to_pattern(&margin_pool_id_filter)), + ) + .order_by(schema::interest_params_updated::checkpoint_timestamp_ms.desc()) + .limit(limit); + + Ok(self.results(query).await?) + } + + pub async fn get_margin_pool_config_updated( + &self, + start_time: i64, + end_time: i64, + limit: i64, + margin_pool_id_filter: String, + ) -> Result, DeepBookError> { + let query = schema::margin_pool_config_updated::table + .select(MarginPoolConfigUpdated::as_select()) + .filter( + schema::margin_pool_config_updated::checkpoint_timestamp_ms + .between(start_time, end_time), + ) + .filter( + schema::margin_pool_config_updated::margin_pool_id + .like(to_pattern(&margin_pool_id_filter)), + ) + .order_by(schema::margin_pool_config_updated::checkpoint_timestamp_ms.desc()) + .limit(limit); + + Ok(self.results(query).await?) + } + + pub async fn get_maintainer_cap_updated( + &self, + start_time: i64, + end_time: i64, + limit: i64, + maintainer_cap_id_filter: String, + ) -> Result, DeepBookError> { + let query = schema::maintainer_cap_updated::table + .select(MaintainerCapUpdated::as_select()) + .filter( + schema::maintainer_cap_updated::checkpoint_timestamp_ms + .between(start_time, end_time), + ) + .filter( + schema::maintainer_cap_updated::maintainer_cap_id + .like(to_pattern(&maintainer_cap_id_filter)), + ) + .order_by(schema::maintainer_cap_updated::checkpoint_timestamp_ms.desc()) + .limit(limit); + + Ok(self.results(query).await?) + } + + pub async fn get_maintainer_fees_withdrawn( + &self, + start_time: i64, + end_time: i64, + limit: i64, + margin_pool_id_filter: String, + ) -> Result, DeepBookError> { + let query = schema::maintainer_fees_withdrawn::table + .select(MaintainerFeesWithdrawn::as_select()) + .filter( + schema::maintainer_fees_withdrawn::checkpoint_timestamp_ms + .between(start_time, end_time), + ) + .filter( + schema::maintainer_fees_withdrawn::margin_pool_id + .like(to_pattern(&margin_pool_id_filter)), + ) + .order_by(schema::maintainer_fees_withdrawn::checkpoint_timestamp_ms.desc()) + .limit(limit); + + Ok(self.results(query).await?) + } + + pub async fn get_protocol_fees_withdrawn( + &self, + start_time: i64, + end_time: i64, + limit: i64, + margin_pool_id_filter: String, + ) -> Result, DeepBookError> { + let query = schema::protocol_fees_withdrawn::table + .select(ProtocolFeesWithdrawn::as_select()) + .filter( + schema::protocol_fees_withdrawn::checkpoint_timestamp_ms + .between(start_time, end_time), + ) + .filter( + schema::protocol_fees_withdrawn::margin_pool_id + .like(to_pattern(&margin_pool_id_filter)), + ) + .order_by(schema::protocol_fees_withdrawn::checkpoint_timestamp_ms.desc()) + .limit(limit); + + Ok(self.results(query).await?) + } + + pub async fn get_supplier_cap_minted( + &self, + start_time: i64, + end_time: i64, + limit: i64, + supplier_cap_id_filter: String, + ) -> Result, DeepBookError> { + let query = schema::supplier_cap_minted::table + .select(SupplierCapMinted::as_select()) + .filter( + schema::supplier_cap_minted::checkpoint_timestamp_ms.between(start_time, end_time), + ) + .filter( + schema::supplier_cap_minted::supplier_cap_id + .like(to_pattern(&supplier_cap_id_filter)), + ) + .order_by(schema::supplier_cap_minted::checkpoint_timestamp_ms.desc()) + .limit(limit); + + Ok(self.results(query).await?) + } + + pub async fn get_supply_referral_minted( + &self, + start_time: i64, + end_time: i64, + limit: i64, + margin_pool_id_filter: String, + owner_filter: String, + ) -> Result, DeepBookError> { + let query = schema::supply_referral_minted::table + .select(SupplyReferralMinted::as_select()) + .filter( + schema::supply_referral_minted::checkpoint_timestamp_ms + .between(start_time, end_time), + ) + .filter( + schema::supply_referral_minted::margin_pool_id + .like(to_pattern(&margin_pool_id_filter)), + ) + .filter(schema::supply_referral_minted::owner.like(to_pattern(&owner_filter))) + .order_by(schema::supply_referral_minted::checkpoint_timestamp_ms.desc()) + .limit(limit); + + Ok(self.results(query).await?) + } + + pub async fn get_pause_cap_updated( + &self, + start_time: i64, + end_time: i64, + limit: i64, + pause_cap_id_filter: String, + ) -> Result, DeepBookError> { + let query = schema::pause_cap_updated::table + .select(PauseCapUpdated::as_select()) + .filter( + schema::pause_cap_updated::checkpoint_timestamp_ms.between(start_time, end_time), + ) + .filter(schema::pause_cap_updated::pause_cap_id.like(to_pattern(&pause_cap_id_filter))) + .order_by(schema::pause_cap_updated::checkpoint_timestamp_ms.desc()) + .limit(limit); + + Ok(self.results(query).await?) + } + + pub async fn get_protocol_fees_increased( + &self, + start_time: i64, + end_time: i64, + limit: i64, + margin_pool_id_filter: String, + ) -> Result, DeepBookError> { + let query = schema::protocol_fees_increased::table + .select(ProtocolFeesIncreasedEvent::as_select()) + .filter( + schema::protocol_fees_increased::checkpoint_timestamp_ms + .between(start_time, end_time), + ) + .filter( + schema::protocol_fees_increased::margin_pool_id + .like(to_pattern(&margin_pool_id_filter)), + ) + .order_by(schema::protocol_fees_increased::checkpoint_timestamp_ms.desc()) + .limit(limit); + + Ok(self.results(query).await?) + } + + pub async fn get_referral_fees_claimed( + &self, + start_time: i64, + end_time: i64, + limit: i64, + referral_id_filter: String, + owner_filter: String, + ) -> Result, DeepBookError> { + let query = schema::referral_fees_claimed::table + .select(ReferralFeesClaimedEvent::as_select()) + .filter( + schema::referral_fees_claimed::checkpoint_timestamp_ms + .between(start_time, end_time), + ) + .filter( + schema::referral_fees_claimed::referral_id.like(to_pattern(&referral_id_filter)), + ) + .filter(schema::referral_fees_claimed::owner.like(to_pattern(&owner_filter))) + .order_by(schema::referral_fees_claimed::checkpoint_timestamp_ms.desc()) + .limit(limit); + + Ok(self.results(query).await?) + } + + pub async fn get_referral_fee_events( + &self, + start_time: i64, + end_time: i64, + limit: i64, + pool_id_filter: String, + referral_id_filter: String, + ) -> Result, DeepBookError> { + let query = schema::referral_fee_events::table + .select(ReferralFeeEvent::as_select()) + .filter( + schema::referral_fee_events::checkpoint_timestamp_ms.between(start_time, end_time), + ) + .filter(schema::referral_fee_events::pool_id.like(to_pattern(&pool_id_filter))) + .filter(schema::referral_fee_events::referral_id.like(to_pattern(&referral_id_filter))) + .order_by(schema::referral_fee_events::checkpoint_timestamp_ms.desc()) + .limit(limit); + + Ok(self.results(query).await?) + } + + pub async fn get_deepbook_pool_registered( + &self, + start_time: i64, + end_time: i64, + limit: i64, + pool_id_filter: String, + ) -> Result, DeepBookError> { + let query = schema::deepbook_pool_registered::table + .select(DeepbookPoolRegistered::as_select()) + .filter( + schema::deepbook_pool_registered::checkpoint_timestamp_ms + .between(start_time, end_time), + ) + .filter(schema::deepbook_pool_registered::pool_id.like(to_pattern(&pool_id_filter))) + .order_by(schema::deepbook_pool_registered::checkpoint_timestamp_ms.desc()) + .limit(limit); + + Ok(self.results(query).await?) + } + + pub async fn get_deepbook_pool_updated_registry( + &self, + start_time: i64, + end_time: i64, + limit: i64, + pool_id_filter: String, + ) -> Result, DeepBookError> { + let query = schema::deepbook_pool_updated_registry::table + .select(DeepbookPoolUpdatedRegistry::as_select()) + .filter( + schema::deepbook_pool_updated_registry::checkpoint_timestamp_ms + .between(start_time, end_time), + ) + .filter( + schema::deepbook_pool_updated_registry::pool_id.like(to_pattern(&pool_id_filter)), + ) + .order_by(schema::deepbook_pool_updated_registry::checkpoint_timestamp_ms.desc()) + .limit(limit); + + Ok(self.results(query).await?) + } + + pub async fn get_deepbook_pool_config_updated( + &self, + start_time: i64, + end_time: i64, + limit: i64, + pool_id_filter: String, + ) -> Result, DeepBookError> { + let query = schema::deepbook_pool_config_updated::table + .select(DeepbookPoolConfigUpdated::as_select()) + .filter( + schema::deepbook_pool_config_updated::checkpoint_timestamp_ms + .between(start_time, end_time), + ) + .filter(schema::deepbook_pool_config_updated::pool_id.like(to_pattern(&pool_id_filter))) + .order_by(schema::deepbook_pool_config_updated::checkpoint_timestamp_ms.desc()) + .limit(limit); + + Ok(self.results(query).await?) + } + + pub async fn get_margin_managers_info( + &self, + ) -> Result< + Vec<( + String, // margin_manager_id + Option, // deepbook_pool_id + Option, // base_asset_id + Option, // base_asset_symbol + Option, // quote_asset_id + Option, // quote_asset_symbol + Option, // base_margin_pool_id + Option, // quote_margin_pool_id + )>, + DeepBookError, + > { + let mut connection = self.db.connect().await?; + + let query = diesel::sql_query( + r#" + WITH managers_with_pools AS ( + SELECT DISTINCT + mmc.margin_manager_id, + mmc.deepbook_pool_id, + p.base_asset_id, + p.base_asset_symbol, + p.quote_asset_id, + p.quote_asset_symbol, + base_mp.margin_pool_id as base_margin_pool_id, + quote_mp.margin_pool_id as quote_margin_pool_id + FROM margin_manager_created mmc + LEFT JOIN pools p ON mmc.deepbook_pool_id = p.pool_id + LEFT JOIN margin_pool_created base_mp + ON ('0x' || base_mp.asset_type = p.base_asset_id OR base_mp.asset_type = p.base_asset_id) + LEFT JOIN margin_pool_created quote_mp + ON ('0x' || quote_mp.asset_type = p.quote_asset_id OR quote_mp.asset_type = p.quote_asset_id) + ) + SELECT DISTINCT + margin_manager_id::text, + deepbook_pool_id::text, + base_asset_id::text, + base_asset_symbol::text, + quote_asset_id::text, + quote_asset_symbol::text, + base_margin_pool_id::text, + quote_margin_pool_id::text + FROM managers_with_pools + ORDER BY margin_manager_id + "#, + ); + + let _guard = self.metrics.db_latency.start_timer(); + + #[derive(QueryableByName)] + struct ManagerInfo { + #[diesel(sql_type = diesel::sql_types::Text)] + margin_manager_id: String, + #[diesel(sql_type = diesel::sql_types::Nullable)] + deepbook_pool_id: Option, + #[diesel(sql_type = diesel::sql_types::Nullable)] + base_asset_id: Option, + #[diesel(sql_type = diesel::sql_types::Nullable)] + base_asset_symbol: Option, + #[diesel(sql_type = diesel::sql_types::Nullable)] + quote_asset_id: Option, + #[diesel(sql_type = diesel::sql_types::Nullable)] + quote_asset_symbol: Option, + #[diesel(sql_type = diesel::sql_types::Nullable)] + base_margin_pool_id: Option, + #[diesel(sql_type = diesel::sql_types::Nullable)] + quote_margin_pool_id: Option, + } + + let res = query + .load::(&mut connection) + .await + .map(|items| { + items + .into_iter() + .map(|item| { + ( + item.margin_manager_id, + item.deepbook_pool_id, + item.base_asset_id, + item.base_asset_symbol, + item.quote_asset_id, + item.quote_asset_symbol, + item.base_margin_pool_id, + item.quote_margin_pool_id, + ) + }) + .collect() + }) + .map_err(|_| DeepBookError::database("Error fetching margin managers info")); + + if res.is_ok() { + self.metrics.db_requests_succeeded.inc(); + } else { + self.metrics.db_requests_failed.inc(); + } + res + } + + pub async fn get_margin_manager_states( + &self, + max_risk_ratio: Option, + deepbook_pool_id_filter: Option, + base_asset_symbol_filter: Option, + quote_asset_symbol_filter: Option, + ) -> Result, DeepBookError> { + use bigdecimal::BigDecimal; + use deepbook_schema::schema::margin_manager_state::dsl::*; + use diesel::PgSortExpressionMethods; + use std::str::FromStr; + + let mut connection = self.db.connect().await?; + let _guard = self.metrics.db_latency.start_timer(); + + let mut query = margin_manager_state + .select(MarginManagerState::as_select()) + .into_boxed(); + + if let Some(max_ratio) = max_risk_ratio { + let max_ratio_decimal = BigDecimal::from_str(&max_ratio.to_string()).unwrap(); + query = query.filter(risk_ratio.is_null().or(risk_ratio.le(max_ratio_decimal))); + } + if let Some(pool_id) = deepbook_pool_id_filter { + query = query.filter(deepbook_pool_id.eq(pool_id)); + } + if let Some(base_symbol) = base_asset_symbol_filter { + query = query.filter(base_asset_symbol.eq(base_symbol)); + } + if let Some(quote_symbol) = quote_asset_symbol_filter { + query = query.filter(quote_asset_symbol.eq(quote_symbol)); + } + query = query.order(risk_ratio.asc().nulls_last()); + + let res = query.load::(&mut connection).await; + + if res.is_ok() { + self.metrics.db_requests_succeeded.inc(); + } else { + self.metrics.db_requests_failed.inc(); + } + + res.map_err(|_| DeepBookError::database("Error fetching margin manager states")) + } + + pub async fn get_watermarks(&self) -> Result, DeepBookError> { + let mut connection = self.db.connect().await?; + let _guard = self.metrics.db_latency.start_timer(); + + let res = schema::watermarks::table + .select(( + schema::watermarks::pipeline, + schema::watermarks::checkpoint_hi_inclusive, + schema::watermarks::timestamp_ms_hi_inclusive, + schema::watermarks::epoch_hi_inclusive, + )) + .load::<(String, i64, i64, i64)>(&mut connection) + .await + .map_err(|_| DeepBookError::database("Error fetching watermarks")); + + if res.is_ok() { + self.metrics.db_requests_succeeded.inc(); + } else { + self.metrics.db_requests_failed.inc(); + } + res + } + + pub async fn get_deposited_assets_by_balance_managers( + &self, + balance_manager_ids: &[String], + ) -> Result, DeepBookError> { + let mut connection = self.db.connect().await?; + let _guard = self.metrics.db_latency.start_timer(); + + let res = schema::balances::table + .select(( + schema::balances::balance_manager_id, + schema::balances::asset, + )) + .filter(schema::balances::balance_manager_id.eq_any(balance_manager_ids)) + .filter(schema::balances::deposit.eq(true)) + .distinct() + .load::<(String, String)>(&mut connection) + .await + .map_err(|_| DeepBookError::database("Error fetching deposited assets")); + + if res.is_ok() { + self.metrics.db_requests_succeeded.inc(); + } else { + self.metrics.db_requests_failed.inc(); + } + res + } + + /// Query net deposits using the materialized view for efficiency. + /// Triggers a concurrent refresh of the view and queries for net deposits up to timestamp. + pub async fn get_net_deposits_from_view( + &self, + asset_ids: &[String], + timestamp_ms: i64, + ) -> Result, DeepBookError> { + let mut connection = self.db.connect().await?; + let _guard = self.metrics.db_latency.start_timer(); + + // Trigger concurrent refresh (non-blocking, won't fail if already refreshing) + let _ = diesel::sql_query("REFRESH MATERIALIZED VIEW CONCURRENTLY net_deposits_hourly") + .execute(&mut connection) + .await; + + // Calculate the hour boundary for the timestamp + let hour_bucket_ms = (timestamp_ms / 3600000) * 3600000; + + // Build asset filter for SQL + let asset_filter: String = asset_ids + .iter() + .map(|a| { + let trimmed = a.strip_prefix("0x").unwrap_or(a); + format!("'{}'", trimmed) + }) + .collect::>() + .join(","); + + // Query: sum all hourly deltas up to the hour boundary, then add partial hour from balances + let query_str = format!( + r#" + WITH hourly_totals AS ( + SELECT asset, SUM(net_amount_delta) AS net_amount + FROM net_deposits_hourly + WHERE hour_bucket_ms < {hour_bucket_ms} + AND asset IN ({asset_filter}) + GROUP BY asset + ), + partial_hour AS ( + SELECT asset, SUM(CASE WHEN deposit THEN amount ELSE -amount END) AS net_amount + FROM balances + WHERE checkpoint_timestamp_ms >= {hour_bucket_ms} + AND checkpoint_timestamp_ms < {timestamp_ms} + AND asset IN ({asset_filter}) + GROUP BY asset + ) + SELECT + COALESCE(h.asset, p.asset) AS asset, + (COALESCE(h.net_amount, 0) + COALESCE(p.net_amount, 0))::bigint AS net_amount + FROM hourly_totals h + FULL OUTER JOIN partial_hour p ON h.asset = p.asset + "#, + hour_bucket_ms = hour_bucket_ms, + timestamp_ms = timestamp_ms, + asset_filter = asset_filter, + ); + + #[derive(QueryableByName)] + struct NetDepositRow { + #[diesel(sql_type = diesel::sql_types::Text)] + asset: String, + #[diesel(sql_type = diesel::sql_types::BigInt)] + net_amount: i64, + } + + let res = diesel::sql_query(query_str) + .load::(&mut connection) + .await; + + if res.is_ok() { + self.metrics.db_requests_succeeded.inc(); + } else { + self.metrics.db_requests_failed.inc(); + } + + res.map(|rows| { + rows.into_iter() + .map(|row| { + let mut asset = row.asset; + if !asset.starts_with("0x") { + asset.insert_str(0, "0x"); + } + (asset, row.net_amount) + }) + .collect() + }) + .map_err(|e| DeepBookError::database(format!("Error fetching net deposits: {}", e))) + } + + pub async fn get_collateral_events( + &self, + start_time: i64, + end_time: i64, + limit: i64, + margin_manager_id_filter: String, + event_type_filter: String, + is_base_filter: Option, + ) -> Result, DeepBookError> { + let mut connection = self.db.connect().await?; + let _guard = self.metrics.db_latency.start_timer(); + + let mut query = schema::collateral_events::table + .select(CollateralEvent::as_select()) + .filter( + schema::collateral_events::checkpoint_timestamp_ms.between(start_time, end_time), + ) + .filter( + schema::collateral_events::margin_manager_id + .like(to_pattern(&margin_manager_id_filter)), + ) + .filter(schema::collateral_events::event_type.like(to_pattern(&event_type_filter))) + .order_by(schema::collateral_events::checkpoint_timestamp_ms.desc()) + .limit(limit) + .into_boxed(); + + if let Some(is_base) = is_base_filter { + query = query.filter(schema::collateral_events::withdraw_base_asset.eq(Some(is_base))); + } + + let res = query.load::(&mut connection).await; + + if res.is_ok() { + self.metrics.db_requests_succeeded.inc(); + } else { + self.metrics.db_requests_failed.inc(); + } + + res.map_err(|_| DeepBookError::database("Error fetching collateral events")) + } + + pub async fn get_points( + &self, + addresses: Option<&[String]>, + ) -> Result, DeepBookError> { + let mut connection = self.db.connect().await?; + let _guard = self.metrics.db_latency.start_timer(); + + let where_clause = addresses + .filter(|a| !a.is_empty()) + .map(|addrs| { + let list = addrs + .iter() + .map(|a| format!("'{}'", a)) + .collect::>() + .join(","); + format!("WHERE address IN ({})", list) + }) + .unwrap_or_default(); + + let query_str = format!( + "SELECT address, COALESCE(SUM(amount), 0)::bigint as total_points \ + FROM points {} GROUP BY address", + where_clause + ); + + #[derive(QueryableByName)] + struct PointsRow { + #[diesel(sql_type = diesel::sql_types::Text)] + address: String, + #[diesel(sql_type = diesel::sql_types::BigInt)] + total_points: i64, + } + + let res = diesel::sql_query(query_str) + .load::(&mut connection) + .await; + + if res.is_ok() { + self.metrics.db_requests_succeeded.inc(); + } else { + self.metrics.db_requests_failed.inc(); + } + + res.map(|rows| { + rows.into_iter() + .map(|r| (r.address, r.total_points)) + .collect() + }) + .map_err(|e| DeepBookError::database(format!("Error fetching points: {}", e))) + } +} diff --git a/crates/server/src/server.rs b/crates/server/src/server.rs index f55c121c5..43449e1fb 100644 --- a/crates/server/src/server.rs +++ b/crates/server/src/server.rs @@ -9,28 +9,44 @@ use axum::{ routing::get, Json, Router, }; -use deepbook_schema::models::{BalancesSummary, OrderFillSummary, Pools}; +use deepbook_schema::models::{ + AssetSupplied, AssetWithdrawn, CollateralEvent, DeepbookPoolConfigUpdated, + DeepbookPoolRegistered, DeepbookPoolUpdated, DeepbookPoolUpdatedRegistry, + InterestParamsUpdated, Liquidation, LoanBorrowed, LoanRepaid, MaintainerCapUpdated, + MaintainerFeesWithdrawn, MarginManagerCreated, MarginManagerState, MarginPoolConfigUpdated, + MarginPoolCreated, PauseCapUpdated, Pools, ProtocolFeesIncreasedEvent, ProtocolFeesWithdrawn, + ReferralFeeEvent, ReferralFeesClaimedEvent, SupplierCapMinted, SupplyReferralMinted, +}; use deepbook_schema::*; -use diesel::dsl::{count_star, sql}; +use diesel::dsl::count_star; use diesel::dsl::{max, min}; -use diesel::BoolExpressionMethods; -use diesel::QueryDsl; -use diesel::{ExpressionMethods, SelectableHelper}; -use diesel_async::RunQueryDsl; +use diesel::{ExpressionMethods, QueryDsl}; +use serde::Deserialize; use serde_json::Value; +use std::net::{IpAddr, Ipv4Addr}; use std::time::{SystemTime, UNIX_EPOCH}; use std::{collections::HashMap, net::SocketAddr}; -use sui_pg_db::Db; -use tokio::{net::TcpListener, task::JoinHandle}; +use sui_pg_db::DbArgs; +use tokio::net::TcpListener; +use tokio::sync::oneshot; +use tokio::sync::OnceCell; use tower_http::cors::{AllowMethods, Any, CorsLayer}; use url::Url; +use crate::metrics::middleware::track_metrics; +use crate::metrics::RpcMetrics; +use crate::reader::Reader; +use axum::middleware::from_fn_with_state; use futures::future::join_all; +use prometheus::Registry; use std::str::FromStr; +use std::sync::Arc; +use sui_futures::service::Service; +use sui_indexer_alt_metrics::{MetricsArgs, MetricsService}; use sui_json_rpc_types::{SuiObjectData, SuiObjectDataOptions, SuiObjectResponse}; use sui_sdk::SuiClientBuilder; use sui_types::{ - base_types::{ObjectID, ObjectRef, SuiAddress}, + base_types::{ObjectID, SuiAddress}, programmable_transaction_builder::ProgrammableTransactionBuilder, transaction::{Argument, CallArg, Command, ObjectArg, ProgrammableMoveCall, TransactionKind}, type_input::TypeInput, @@ -38,7 +54,6 @@ use sui_types::{ }; use tokio::join; -pub const SUI_MAINNET_URL: &str = "https://fullnode.mainnet.sui.io:443"; pub const GET_POOLS_PATH: &str = "/get_pools"; pub const GET_HISTORICAL_VOLUME_BY_BALANCE_MANAGER_ID_WITH_INTERVAL: &str = "/historical_volume_by_balance_manager_id_with_interval/:pool_names/:balance_manager_id"; @@ -50,31 +65,206 @@ pub const GET_NET_DEPOSITS: &str = "/get_net_deposits/:asset_ids/:timestamp"; pub const TICKER_PATH: &str = "/ticker"; pub const TRADES_PATH: &str = "/trades/:pool_name"; pub const ORDER_UPDATES_PATH: &str = "/order_updates/:pool_name"; +pub const ORDERS_PATH: &str = "/orders/:pool_name/:balance_manager_id"; pub const TRADE_COUNT_PATH: &str = "/trade_count"; pub const ASSETS_PATH: &str = "/assets"; pub const SUMMARY_PATH: &str = "/summary"; pub const LEVEL2_PATH: &str = "/orderbook/:pool_name"; pub const LEVEL2_MODULE: &str = "pool"; pub const LEVEL2_FUNCTION: &str = "get_level2_ticks_from_mid"; -pub const DEEPBOOK_PACKAGE_ID: &str = - "0x2c8d603bc51326b8c13cef9dd07031a408a48dddb541963357661df5d3204809"; -pub const DEEP_TOKEN_PACKAGE_ID: &str = - "0xdeeb7a4662eec9f2f3def03fb937a663dddaa2e215b8078a284d026b7946c270"; -pub const DEEP_TREASURY_ID: &str = - "0x032abf8948dda67a271bcc18e776dbbcfb0d58c8d288a700ff0d5521e57a1ffe"; pub const DEEP_SUPPLY_MODULE: &str = "deep"; pub const DEEP_SUPPLY_FUNCTION: &str = "total_supply"; pub const DEEP_SUPPLY_PATH: &str = "/deep_supply"; +pub const MARGIN_SUPPLY_PATH: &str = "/margin_supply"; +pub const MARGIN_POOL_MODULE: &str = "margin_pool"; +pub const OHCLV_PATH: &str = "/ohclv/:pool_name"; + +// Deepbook Margin Events +pub const MARGIN_MANAGER_CREATED_PATH: &str = "/margin_manager_created"; +pub const LOAN_BORROWED_PATH: &str = "/loan_borrowed"; +pub const LOAN_REPAID_PATH: &str = "/loan_repaid"; +pub const LIQUIDATION_PATH: &str = "/liquidation"; +pub const ASSET_SUPPLIED_PATH: &str = "/asset_supplied"; +pub const ASSET_WITHDRAWN_PATH: &str = "/asset_withdrawn"; +pub const MARGIN_POOL_CREATED_PATH: &str = "/margin_pool_created"; +pub const DEEPBOOK_POOL_UPDATED_PATH: &str = "/deepbook_pool_updated"; +pub const INTEREST_PARAMS_UPDATED_PATH: &str = "/interest_params_updated"; +pub const MARGIN_POOL_CONFIG_UPDATED_PATH: &str = "/margin_pool_config_updated"; +pub const MAINTAINER_CAP_UPDATED_PATH: &str = "/maintainer_cap_updated"; +pub const MAINTAINER_FEES_WITHDRAWN_PATH: &str = "/maintainer_fees_withdrawn"; +pub const PROTOCOL_FEES_WITHDRAWN_PATH: &str = "/protocol_fees_withdrawn"; +pub const SUPPLIER_CAP_MINTED_PATH: &str = "/supplier_cap_minted"; +pub const SUPPLY_REFERRAL_MINTED_PATH: &str = "/supply_referral_minted"; +pub const PAUSE_CAP_UPDATED_PATH: &str = "/pause_cap_updated"; +pub const PROTOCOL_FEES_INCREASED_PATH: &str = "/protocol_fees_increased"; +pub const REFERRAL_FEES_CLAIMED_PATH: &str = "/referral_fees_claimed"; +pub const REFERRAL_FEE_EVENTS_PATH: &str = "/referral_fee_events"; +pub const DEEPBOOK_POOL_REGISTERED_PATH: &str = "/deepbook_pool_registered"; +pub const DEEPBOOK_POOL_UPDATED_REGISTRY_PATH: &str = "/deepbook_pool_updated_registry"; +pub const DEEPBOOK_POOL_CONFIG_UPDATED_PATH: &str = "/deepbook_pool_config_updated"; +pub const MARGIN_MANAGERS_INFO_PATH: &str = "/margin_managers_info"; +pub const MARGIN_MANAGER_STATES_PATH: &str = "/margin_manager_states"; +pub const STATUS_PATH: &str = "/status"; +pub const DEPOSITED_ASSETS_PATH: &str = "/deposited_assets/:balance_manager_ids"; +pub const COLLATERAL_EVENTS_PATH: &str = "/collateral_events"; +pub const GET_POINTS_PATH: &str = "/get_points"; + +#[derive(Clone)] +pub struct AppState { + reader: Reader, + metrics: Arc, + rpc_url: Url, + sui_client: Arc>, + deepbook_package_id: String, + deep_token_package_id: String, + deep_treasury_id: String, + margin_package_id: Option, +} + +impl AppState { + pub async fn new( + database_url: Url, + args: DbArgs, + registry: &Registry, + rpc_url: Url, + deepbook_package_id: String, + deep_token_package_id: String, + deep_treasury_id: String, + margin_package_id: Option, + ) -> Result { + let metrics = RpcMetrics::new(registry); + let reader = Reader::new(database_url, args, metrics.clone(), registry).await?; + Ok(Self { + reader, + metrics, + rpc_url, + sui_client: Arc::new(OnceCell::new()), + deepbook_package_id, + deep_token_package_id, + deep_treasury_id, + margin_package_id, + }) + } -pub fn run_server(socket_address: SocketAddr, state: Db, rpc_url: Url) -> JoinHandle<()> { - tokio::spawn(async move { - let listener = TcpListener::bind(socket_address).await.unwrap(); - axum::serve(listener, make_router(state, rpc_url)) + /// Returns a reference to the shared SuiClient instance. + /// Lazily initializes the client on first access and caches it for subsequent calls + pub async fn sui_client(&self) -> Result<&sui_sdk::SuiClient, DeepBookError> { + self.sui_client + .get_or_try_init(|| async { + SuiClientBuilder::default() + .build(self.rpc_url.as_str()) + .await + }) .await - .unwrap(); - }) + .map_err(DeepBookError::from) + } + + pub(crate) fn metrics(&self) -> &RpcMetrics { + &self.metrics + } +} + +/// Query parameters for the /status endpoint +#[derive(Debug, Deserialize)] +pub struct StatusQueryParams { + /// Maximum acceptable checkpoint lag for "healthy" status (default: 100) + #[serde(default = "default_max_checkpoint_lag")] + pub max_checkpoint_lag: i64, + /// Maximum acceptable time lag in seconds for "healthy" status (default: 60) + #[serde(default = "default_max_time_lag_seconds")] + pub max_time_lag_seconds: i64, +} + +fn default_max_checkpoint_lag() -> i64 { + 100 +} + +fn default_max_time_lag_seconds() -> i64 { + 60 +} + +pub async fn run_server( + server_port: u16, + database_url: Url, + db_arg: DbArgs, + rpc_url: Url, + metrics_address: SocketAddr, + deepbook_package_id: String, + deep_token_package_id: String, + deep_treasury_id: String, + margin_poll_interval_secs: u64, + margin_package_id: Option, +) -> Result<(), anyhow::Error> { + let registry = Registry::new_custom(Some("deepbook_api".into()), None) + .expect("Failed to create Prometheus registry."); + + let metrics = MetricsService::new(MetricsArgs { metrics_address }, registry); + + let state = AppState::new( + database_url.clone(), + db_arg.clone(), + metrics.registry(), + rpc_url.clone(), + deepbook_package_id, + deep_token_package_id, + deep_treasury_id, + margin_package_id.clone(), + ) + .await?; + let socket_address = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)), server_port); + + println!("Server started successfully on port {}", server_port); + + // Start margin metrics poller if margin_package_id is provided + // Must be done before spawning the metrics service since we need access to the registry + if let Some(margin_pkg_id) = margin_package_id { + let cancellation_token = tokio_util::sync::CancellationToken::new(); + let margin_metrics = crate::margin_metrics::MarginMetrics::new(metrics.registry()); + let margin_db = sui_pg_db::Db::for_read(database_url, db_arg).await?; + let margin_poller = crate::margin_metrics::MarginPoller::new( + margin_db, + rpc_url.clone(), + margin_pkg_id, + margin_metrics, + margin_poll_interval_secs, + cancellation_token, + ); + tokio::spawn(async move { + if let Err(e) = margin_poller.run().await { + eprintln!("[margin_poller] Margin poller failed: {}", e); + } + }); + println!( + "Margin metrics poller started (interval: {}s)", + margin_poll_interval_secs + ); + } + + let s_metrics = metrics.run().await?; + + let listener = TcpListener::bind(socket_address).await?; + let (stx, srx) = oneshot::channel::<()>(); + + Service::new() + .attach(s_metrics) + .with_shutdown_signal(async move { + let _ = stx.send(()); + }) + .spawn(async move { + axum::serve(listener, make_router(Arc::new(state))) + .with_graceful_shutdown(async move { + let _ = srx.await; + }) + .await?; + + Ok(()) + }) + .main() + .await?; + + Ok(()) } -pub(crate) fn make_router(state: Db, rpc_url: Url) -> Router { +pub(crate) fn make_router(state: Arc) -> Router { let cors = CorsLayer::new() .allow_methods(AllowMethods::list(vec![Method::GET, Method::OPTIONS])) .allow_headers(Any) @@ -98,114 +288,191 @@ pub(crate) fn make_router(state: Db, rpc_url: Url) -> Router { .route(TRADES_PATH, get(trades)) .route(TRADE_COUNT_PATH, get(trade_count)) .route(ORDER_UPDATES_PATH, get(order_updates)) + .route(ORDERS_PATH, get(orders)) .route(ASSETS_PATH, get(assets)) + .route(OHCLV_PATH, get(ohclv)) + // Deepbook Margin Events + .route(MARGIN_MANAGER_CREATED_PATH, get(margin_manager_created)) + .route(LOAN_BORROWED_PATH, get(loan_borrowed)) + .route(LOAN_REPAID_PATH, get(loan_repaid)) + .route(LIQUIDATION_PATH, get(liquidation)) + .route(ASSET_SUPPLIED_PATH, get(asset_supplied)) + .route(ASSET_WITHDRAWN_PATH, get(asset_withdrawn)) + .route(MARGIN_POOL_CREATED_PATH, get(margin_pool_created)) + .route(DEEPBOOK_POOL_UPDATED_PATH, get(deepbook_pool_updated)) + .route(INTEREST_PARAMS_UPDATED_PATH, get(interest_params_updated)) + .route( + MARGIN_POOL_CONFIG_UPDATED_PATH, + get(margin_pool_config_updated), + ) + .route(MAINTAINER_CAP_UPDATED_PATH, get(maintainer_cap_updated)) + .route( + MAINTAINER_FEES_WITHDRAWN_PATH, + get(maintainer_fees_withdrawn), + ) + .route(PROTOCOL_FEES_WITHDRAWN_PATH, get(protocol_fees_withdrawn)) + .route(SUPPLIER_CAP_MINTED_PATH, get(supplier_cap_minted)) + .route(SUPPLY_REFERRAL_MINTED_PATH, get(supply_referral_minted)) + .route(PAUSE_CAP_UPDATED_PATH, get(pause_cap_updated)) + .route(PROTOCOL_FEES_INCREASED_PATH, get(protocol_fees_increased)) + .route(REFERRAL_FEES_CLAIMED_PATH, get(referral_fees_claimed)) + .route(REFERRAL_FEE_EVENTS_PATH, get(referral_fee_events)) + .route(DEEPBOOK_POOL_REGISTERED_PATH, get(deepbook_pool_registered)) + .route( + DEEPBOOK_POOL_UPDATED_REGISTRY_PATH, + get(deepbook_pool_updated_registry), + ) + .route( + DEEPBOOK_POOL_CONFIG_UPDATED_PATH, + get(deepbook_pool_config_updated), + ) + .route(MARGIN_MANAGERS_INFO_PATH, get(margin_managers_info)) + .route(MARGIN_MANAGER_STATES_PATH, get(margin_manager_states)) + .route(DEPOSITED_ASSETS_PATH, get(deposited_assets)) + .route(COLLATERAL_EVENTS_PATH, get(collateral_events)) + .route(GET_POINTS_PATH, get(get_points)) .with_state(state.clone()); let rpc_routes = Router::new() .route(LEVEL2_PATH, get(orderbook)) .route(DEEP_SUPPLY_PATH, get(deep_supply)) + .route(MARGIN_SUPPLY_PATH, get(margin_supply)) .route(SUMMARY_PATH, get(summary)) - .with_state((state, rpc_url)); + .route(STATUS_PATH, get(status)) + .with_state(state.clone()); - db_routes.merge(rpc_routes).layer(cors) + db_routes + .merge(rpc_routes) + .layer(cors) + .layer(from_fn_with_state(state, track_metrics)) } -impl axum::response::IntoResponse for DeepBookError { - // TODO: distinguish client error. - fn into_response(self) -> axum::response::Response { - ( - StatusCode::INTERNAL_SERVER_ERROR, - format!("Something went wrong: {:?}", self), - ) - .into_response() - } +async fn health_check() -> StatusCode { + StatusCode::OK } -impl From for DeepBookError -where - E: Into, -{ - fn from(err: E) -> Self { - Self::InternalError(err.into().to_string()) +/// Get indexer status including checkpoint lag +async fn status( + Query(params): Query, + State(state): State>, +) -> Result, DeepBookError> { + // Get watermarks from the database + let watermarks = state.reader.get_watermarks().await?; + + // Get the latest checkpoint from Sui RPC + let sui_client = state.sui_client().await?; + let latest_checkpoint = sui_client + .read_api() + .get_latest_checkpoint_sequence_number() + .await + .map_err(|e| DeepBookError::rpc(format!("Failed to get latest checkpoint: {}", e)))?; + + // Get current timestamp + let current_time_ms = SystemTime::now() + .duration_since(UNIX_EPOCH) + .map_err(|_| DeepBookError::internal("System time error"))? + .as_millis() as i64; + + // Build status for each pipeline + let mut pipelines = Vec::new(); + let mut min_checkpoint = i64::MAX; + let mut max_lag_pipeline_name = String::new(); + let mut max_checkpoint_lag = 0i64; + + for (pipeline, checkpoint_hi, timestamp_ms_hi, epoch_hi) in watermarks { + let checkpoint_lag = latest_checkpoint as i64 - checkpoint_hi; + let time_lag_ms = current_time_ms - timestamp_ms_hi; + let time_lag_seconds = time_lag_ms / 1000; + + // Track the earliest checkpoint and pipeline with max lag + if checkpoint_hi < min_checkpoint { + min_checkpoint = checkpoint_hi; + } + if checkpoint_lag > max_checkpoint_lag { + max_checkpoint_lag = checkpoint_lag; + max_lag_pipeline_name = pipeline.clone(); + } + + pipelines.push(serde_json::json!({ + "pipeline": pipeline, + "indexed_checkpoint": checkpoint_hi, + "indexed_epoch": epoch_hi, + "indexed_timestamp_ms": timestamp_ms_hi, + "checkpoint_lag": checkpoint_lag, + "time_lag_seconds": time_lag_seconds, + "latest_onchain_checkpoint": latest_checkpoint, + })); } -} -async fn health_check() -> StatusCode { - StatusCode::OK + let max_time_lag_seconds = pipelines + .iter() + .filter_map(|p| p["time_lag_seconds"].as_i64()) + .max() + .unwrap_or(0); + + // Handle case where no pipelines exist + let earliest_checkpoint = if min_checkpoint == i64::MAX { + 0 + } else { + min_checkpoint + }; + + let is_healthy = max_checkpoint_lag < params.max_checkpoint_lag + && max_time_lag_seconds < params.max_time_lag_seconds; + let status_str = if is_healthy { "OK" } else { "UNHEALTHY" }; + + Ok(Json(serde_json::json!({ + "status": status_str, + "latest_onchain_checkpoint": latest_checkpoint, + "current_time_ms": current_time_ms, + "earliest_checkpoint": earliest_checkpoint, + "max_lag_pipeline": max_lag_pipeline_name, + "pipelines": pipelines, + "max_checkpoint_lag": max_checkpoint_lag, + "max_time_lag_seconds": max_time_lag_seconds, + }))) } /// Get all pools stored in database -async fn get_pools(State(state): State) -> Result>, DeepBookError> { - let connection = &mut state.connect().await?; - let results = schema::pools::table - .select(Pools::as_select()) - .load(connection) - .await?; - - Ok(Json(results)) +async fn get_pools(State(state): State>) -> Result>, DeepBookError> { + Ok(Json(state.reader.get_pools().await?)) } async fn historical_volume( Path(pool_names): Path, Query(params): Query>, - State(state): State, + State(state): State>, ) -> Result>, DeepBookError> { // Fetch all pools to map names to IDs - let pools: Json> = get_pools(State(state.clone())).await?; - let pool_name_to_id: HashMap = pools - .0 + let pools = state.reader.get_pools().await?; + let pool_name_to_id = pools .into_iter() .map(|pool| (pool.pool_name, pool.pool_id)) - .collect(); + .collect::>(); // Map provided pool names to pool IDs - let pool_ids_list: Vec = pool_names + let pool_ids: Vec = pool_names .split(',') .filter_map(|name| pool_name_to_id.get(name).cloned()) .collect(); - if pool_ids_list.is_empty() { - return Err(DeepBookError::InternalError( - "No valid pool names provided".to_string(), - )); + if pool_ids.is_empty() { + return Err(DeepBookError::bad_request("No valid pool names provided")); } // Parse start_time and end_time from query parameters (in seconds) and convert to milliseconds - let end_time = params - .get("end_time") - .and_then(|v| v.parse::().ok()) - .map(|t| t * 1000) // Convert to milliseconds - .unwrap_or_else(|| { - SystemTime::now() - .duration_since(UNIX_EPOCH) - .unwrap() - .as_millis() as i64 - }); - + let end_time = params.end_time(); let start_time = params - .get("start_time") - .and_then(|v| v.parse::().ok()) - .map(|t| t * 1000) // Convert to milliseconds + .start_time() // Convert to milliseconds .unwrap_or_else(|| end_time - 24 * 60 * 60 * 1000); // Determine whether to query volume in base or quote - let volume_in_base = params - .get("volume_in_base") - .map(|v| v == "true") - .unwrap_or(false); - let column_to_query = if volume_in_base { - sql::("base_quantity") - } else { - sql::("quote_quantity") - }; + let volume_in_base = params.volume_in_base(); // Query the database for the historical volume - let connection = &mut state.connect().await?; - let results: Vec<(String, i64)> = schema::order_fills::table - .filter(schema::order_fills::checkpoint_timestamp_ms.between(start_time, end_time)) - .filter(schema::order_fills::pool_id.eq_any(pool_ids_list)) - .select((schema::order_fills::pool_id, column_to_query)) - .load(connection) + let results = state + .reader + .get_historical_volume(start_time, end_time, &pool_ids, volume_in_base) .await?; // Aggregate volume by pool ID and map back to pool names @@ -226,12 +493,11 @@ async fn historical_volume( /// Get all historical volume for all pools async fn all_historical_volume( Query(params): Query>, - State(state): State, + State(state): State>, ) -> Result>, DeepBookError> { - let pools: Json> = get_pools(State(state.clone())).await?; + let pools = state.reader.get_pools().await?; let pool_names: String = pools - .0 .into_iter() .map(|pool| pool.pool_name) .collect::>() @@ -243,71 +509,40 @@ async fn all_historical_volume( async fn get_historical_volume_by_balance_manager_id( Path((pool_names, balance_manager_id)): Path<(String, String)>, Query(params): Query>, - State(state): State, + State(state): State>, ) -> Result>>, DeepBookError> { - let connection = &mut state.connect().await?; - - let pools: Json> = get_pools(State(state.clone())).await?; - let pool_name_to_id: HashMap = pools - .0 + let pools = state.reader.get_pools().await?; + let pool_name_to_id = pools .into_iter() .map(|pool| (pool.pool_name, pool.pool_id)) - .collect(); + .collect::>(); - let pool_ids_list: Vec = pool_names + let pool_ids: Vec = pool_names .split(',') .filter_map(|name| pool_name_to_id.get(name).cloned()) .collect(); - if pool_ids_list.is_empty() { - return Err(DeepBookError::InternalError( - "No valid pool names provided".to_string(), - )); + if pool_ids.is_empty() { + return Err(DeepBookError::bad_request("No valid pool names provided")); } // Parse start_time and end_time - let end_time = params - .get("end_time") - .and_then(|v| v.parse::().ok()) - .map(|t| t * 1000) // Convert to milliseconds - .unwrap_or_else(|| { - SystemTime::now() - .duration_since(UNIX_EPOCH) - .unwrap() - .as_millis() as i64 - }); - + let end_time = params.end_time(); let start_time = params - .get("start_time") - .and_then(|v| v.parse::().ok()) - .map(|t| t * 1000) // Convert to milliseconds + .start_time() // Convert to milliseconds .unwrap_or_else(|| end_time - 24 * 60 * 60 * 1000); - let volume_in_base = params - .get("volume_in_base") - .map(|v| v == "true") - .unwrap_or(false); - let column_to_query = if volume_in_base { - sql::("base_quantity") - } else { - sql::("quote_quantity") - }; + let volume_in_base = params.volume_in_base(); - let results: Vec = schema::order_fills::table - .select(( - schema::order_fills::pool_id, - schema::order_fills::maker_balance_manager_id, - schema::order_fills::taker_balance_manager_id, - column_to_query, - )) - .filter(schema::order_fills::pool_id.eq_any(&pool_ids_list)) - .filter(schema::order_fills::checkpoint_timestamp_ms.between(start_time, end_time)) - .filter( - schema::order_fills::maker_balance_manager_id - .eq(&balance_manager_id) - .or(schema::order_fills::taker_balance_manager_id.eq(&balance_manager_id)), + let results = state + .reader + .get_order_fill_summary( + start_time, + end_time, + &pool_ids, + &balance_manager_id, + volume_in_base, ) - .load(connection) .await?; let mut volume_by_pool: HashMap> = HashMap::new(); @@ -335,26 +570,21 @@ async fn get_historical_volume_by_balance_manager_id( async fn get_historical_volume_by_balance_manager_id_with_interval( Path((pool_names, balance_manager_id)): Path<(String, String)>, Query(params): Query>, - State(state): State, + State(state): State>, ) -> Result>>>, DeepBookError> { - let connection = &mut state.connect().await?; - - let pools: Json> = get_pools(State(state.clone())).await?; + let pools = state.reader.get_pools().await?; let pool_name_to_id: HashMap = pools - .0 .into_iter() .map(|pool| (pool.pool_name, pool.pool_id)) .collect(); - let pool_ids_list: Vec = pool_names + let pool_ids = pool_names .split(',') .filter_map(|name| pool_name_to_id.get(name).cloned()) - .collect(); + .collect::>(); - if pool_ids_list.is_empty() { - return Err(DeepBookError::InternalError( - "No valid pool names provided".to_string(), - )); + if pool_ids.is_empty() { + return Err(DeepBookError::bad_request("No valid pool names provided")); } // Parse interval @@ -364,29 +594,17 @@ async fn get_historical_volume_by_balance_manager_id_with_interval( .unwrap_or(3600); // Default interval: 1 hour if interval <= 0 { - return Err(DeepBookError::InternalError( - "Interval must be greater than 0".to_string(), + return Err(DeepBookError::bad_request( + "Interval must be greater than 0", )); } let interval_ms = interval * 1000; - // Parse start_time and end_time - let end_time = params - .get("end_time") - .and_then(|v| v.parse::().ok()) - .map(|t| t * 1000) // Convert to milliseconds - .unwrap_or_else(|| { - SystemTime::now() - .duration_since(UNIX_EPOCH) - .unwrap() - .as_millis() as i64 - }); + let end_time = params.end_time(); let start_time = params - .get("start_time") - .and_then(|v| v.parse::().ok()) - .map(|t| t * 1000) // Convert to milliseconds + .start_time() // Convert to milliseconds .unwrap_or_else(|| end_time - 24 * 60 * 60 * 1000); let mut metrics_by_interval: HashMap>> = HashMap::new(); @@ -395,33 +613,17 @@ async fn get_historical_volume_by_balance_manager_id_with_interval( while current_start + interval_ms <= end_time { let current_end = current_start + interval_ms; - let volume_in_base = params - .get("volume_in_base") - .map(|v| v == "true") - .unwrap_or(false); - let column_to_query = if volume_in_base { - sql::("base_quantity") - } else { - sql::("quote_quantity") - }; + let volume_in_base = params.volume_in_base(); - let results: Vec = schema::order_fills::table - .select(( - schema::order_fills::pool_id, - schema::order_fills::maker_balance_manager_id, - schema::order_fills::taker_balance_manager_id, - column_to_query, - )) - .filter(schema::order_fills::pool_id.eq_any(&pool_ids_list)) - .filter( - schema::order_fills::checkpoint_timestamp_ms.between(current_start, current_end), - ) - .filter( - schema::order_fills::maker_balance_manager_id - .eq(&balance_manager_id) - .or(schema::order_fills::taker_balance_manager_id.eq(&balance_manager_id)), + let results = state + .reader + .get_order_fill_summary( + start_time, + end_time, + &pool_ids, + &balance_manager_id, + volume_in_base, ) - .load(connection) .await?; let mut volume_by_pool: HashMap> = HashMap::new(); @@ -456,41 +658,38 @@ async fn get_historical_volume_by_balance_manager_id_with_interval( async fn ticker( Query(params): Query>, - State(state): State, + State(state): State>, ) -> Result>>, DeepBookError> { // Fetch base and quote historical volumes let base_volumes = fetch_historical_volume(¶ms, true, &state).await?; let quote_volumes = fetch_historical_volume(¶ms, false, &state).await?; // Fetch pools data for metadata - let pools: Json> = get_pools(State(state.clone())).await?; + let pools = state.reader.get_pools().await?; let pool_map: HashMap = pools - .0 .iter() .map(|pool| (pool.pool_id.clone(), pool)) .collect(); let end_time = SystemTime::now() .duration_since(UNIX_EPOCH) - .map_err(|_| DeepBookError::InternalError("System time error".to_string()))? + .map_err(|_| DeepBookError::internal("System time error"))? .as_millis() as i64; // Calculate the start time for 24 hours ago let start_time = end_time - (24 * 60 * 60 * 1000); // Fetch last prices for all pools in a single query. Only trades in the last 24 hours will count. - let connection = &mut state.connect().await?; - let last_prices: Vec<(String, i64)> = schema::order_fills::table + let query = schema::order_fills::table .filter(schema::order_fills::checkpoint_timestamp_ms.between(start_time, end_time)) .select((schema::order_fills::pool_id, schema::order_fills::price)) .order_by(( schema::order_fills::pool_id.asc(), schema::order_fills::checkpoint_timestamp_ms.desc(), )) - .distinct_on(schema::order_fills::pool_id) - .load(connection) - .await?; + .distinct_on(schema::order_fills::pool_id); + let last_prices: Vec<(String, i64)> = state.reader.results(query).await?; let last_price_map: HashMap = last_prices.into_iter().collect(); let mut response = HashMap::new(); @@ -537,7 +736,7 @@ async fn ticker( async fn fetch_historical_volume( params: &HashMap, volume_in_base: bool, - state: &Db, + state: &Arc, ) -> Result, DeepBookError> { let mut params_with_volume = params.clone(); params_with_volume.insert("volume_in_base".to_string(), volume_in_base.to_string()); @@ -549,12 +748,11 @@ async fn fetch_historical_volume( #[allow(clippy::get_first)] async fn summary( - State((state, rpc_url)): State<(Db, Url)>, + State(state): State>, ) -> Result>>, DeepBookError> { // Fetch pools metadata first since it's required for other functions - let pools: Json> = get_pools(State(state.clone())).await?; + let pools = state.reader.get_pools().await?; let pool_metadata: HashMap = pools - .0 .iter() .map(|pool| { ( @@ -592,7 +790,7 @@ async fn summary( orderbook( Path(pool_name_clone), Query(HashMap::from([("level".to_string(), "1".to_string())])), - State((state.clone(), rpc_url.clone())), + State(state.clone()), ) }) .collect(); @@ -682,30 +880,27 @@ async fn summary( async fn high_low_prices_24h( pool_decimals: &HashMap, - State(state): State, + State(state): State>, ) -> Result, DeepBookError> { // Get the current timestamp in milliseconds let end_time = SystemTime::now() .duration_since(UNIX_EPOCH) - .map_err(|_| DeepBookError::InternalError("System time error".to_string()))? + .map_err(|_| DeepBookError::internal("System time error"))? .as_millis() as i64; // Calculate the start time for 24 hours ago let start_time = end_time - (24 * 60 * 60 * 1000); - let connection = &mut state.connect().await?; - // Query for trades within the last 24 hours for all pools - let results: Vec<(String, Option, Option)> = schema::order_fills::table + let query = schema::order_fills::table .filter(schema::order_fills::checkpoint_timestamp_ms.between(start_time, end_time)) .group_by(schema::order_fills::pool_id) .select(( schema::order_fills::pool_id, max(schema::order_fills::price), min(schema::order_fills::price), - )) - .load(connection) - .await?; + )); + let results: Vec<(String, Option, Option)> = state.reader.results(query).await?; // Aggregate the highest and lowest prices for each pool let mut price_map: HashMap = HashMap::new(); @@ -726,41 +921,29 @@ async fn high_low_prices_24h( async fn price_change_24h( pool_metadata: &HashMap, - State(state): State, + State(state): State>, ) -> Result, DeepBookError> { - let connection = &mut state.connect().await?; - // Calculate the timestamp for 24 hours ago let now = SystemTime::now() .duration_since(UNIX_EPOCH) - .map_err(|_| DeepBookError::InternalError("System time error".to_string()))? + .map_err(|_| DeepBookError::internal("System time error"))? .as_millis() as i64; let timestamp_24h_ago = now - (24 * 60 * 60 * 1000); // 24 hours in milliseconds - let timestamp_48h_ago = now - (48 * 60 * 60 * 1000); // 24 hours in milliseconds + let timestamp_48h_ago = now - (48 * 60 * 60 * 1000); // 48 hours in milliseconds let mut response = HashMap::new(); for (pool_name, (pool_id, (base_decimals, quote_decimals))) in pool_metadata.iter() { // Get the latest price <= 24 hours ago. Only trades until 48 hours ago will count. - let earliest_trade_24h = schema::order_fills::table - .filter( - schema::order_fills::checkpoint_timestamp_ms - .between(timestamp_48h_ago, timestamp_24h_ago), - ) - .filter(schema::order_fills::pool_id.eq(pool_id)) - .order_by(schema::order_fills::checkpoint_timestamp_ms.desc()) - .select(schema::order_fills::price) - .first::(connection) + let earliest_trade_24h = state + .reader + .get_price(timestamp_48h_ago, timestamp_24h_ago, pool_id) .await; - // Get the most recent price. Only trades until 24 hours ago will count. - let most_recent_trade = schema::order_fills::table - .filter(schema::order_fills::checkpoint_timestamp_ms.between(timestamp_24h_ago, now)) - .filter(schema::order_fills::pool_id.eq(pool_id)) - .order_by(schema::order_fills::checkpoint_timestamp_ms.desc()) - .select(schema::order_fills::price) - .first::(connection) + let most_recent_trade = state + .reader + .get_price(timestamp_24h_ago, now, pool_id) .await; if let (Ok(earliest_price), Ok(most_recent_price)) = (earliest_trade_24h, most_recent_trade) @@ -788,79 +971,36 @@ async fn price_change_24h( async fn order_updates( Path(pool_name): Path, Query(params): Query>, - State(state): State, + State(state): State>, ) -> Result>>, DeepBookError> { - let connection = &mut state.connect().await?; - // Fetch pool data with proper error handling - let (pool_id, base_decimals, quote_decimals) = schema::pools::table - .filter(schema::pools::pool_name.eq(pool_name.clone())) - .select(( - schema::pools::pool_id, - schema::pools::base_asset_decimals, - schema::pools::quote_asset_decimals, - )) - .first::<(String, i16, i16)>(connection) - .await - .map_err(|_| DeepBookError::InternalError(format!("Pool '{}' not found", pool_name)))?; - + let (pool_id, base_decimals, quote_decimals) = + state.reader.get_pool_decimals(&pool_name).await?; let base_decimals = base_decimals as u8; let quote_decimals = quote_decimals as u8; - let end_time = params - .get("end_time") - .and_then(|v| v.parse::().ok()) - .map(|t| t * 1000) // Convert to milliseconds - .unwrap_or_else(|| { - SystemTime::now() - .duration_since(UNIX_EPOCH) - .unwrap() - .as_millis() as i64 - }); + let end_time = params.end_time(); let start_time = params - .get("start_time") - .and_then(|v| v.parse::().ok()) - .map(|t| t * 1000) // Convert to milliseconds + .start_time() // Convert to milliseconds .unwrap_or_else(|| end_time - 24 * 60 * 60 * 1000); - let limit = params - .get("limit") - .and_then(|v| v.parse::().ok()) - .unwrap_or(1); - - let mut query = schema::order_updates::table - .filter(schema::order_updates::checkpoint_timestamp_ms.between(start_time, end_time)) - .filter(schema::order_updates::pool_id.eq(pool_id)) - .order_by(schema::order_updates::checkpoint_timestamp_ms.desc()) - .select(( - schema::order_updates::order_id, - schema::order_updates::price, - schema::order_updates::original_quantity, - schema::order_updates::quantity, - schema::order_updates::filled_quantity, - schema::order_updates::checkpoint_timestamp_ms, - schema::order_updates::is_bid, - schema::order_updates::balance_manager_id, - schema::order_updates::status, - )) - .limit(limit) - .into_boxed(); + let limit = params.limit(); let balance_manager_filter = params.get("balance_manager_id").cloned(); - if let Some(manager_id) = balance_manager_filter { - query = query.filter(schema::order_updates::balance_manager_id.eq(manager_id)); - } - let status_filter = params.get("status").cloned(); - if let Some(status) = status_filter { - query = query.filter(schema::order_updates::status.eq(status)); - } - let trades = query - .load::<(String, i64, i64, i64, i64, i64, bool, String, String)>(connection) - .await - .map_err(|_| DeepBookError::InternalError("Error fetching trade details".to_string()))?; + let trades = state + .reader + .get_order_updates( + pool_id, + start_time, + end_time, + limit, + balance_manager_filter, + status_filter, + ) + .await?; let base_factor = 10u64.pow(base_decimals as u32); let price_factor = 10u64.pow((9 - base_decimals + quote_decimals) as u32); @@ -913,106 +1053,134 @@ async fn order_updates( Ok(Json(trade_data)) } +async fn orders( + Path((pool_name, balance_manager_id)): Path<(String, String)>, + Query(params): Query>, + State(state): State>, +) -> Result>>, DeepBookError> { + let (pool_id, base_decimals, quote_decimals) = + state.reader.get_pool_decimals(&pool_name).await?; + let base_decimals = base_decimals as u8; + let quote_decimals = quote_decimals as u8; + + let limit = params + .get("limit") + .and_then(|v| v.parse::().ok()) + .unwrap_or(1000); + + let status_filter = params.get("status").map(|s| { + s.split(',') + .map(|status| status.trim().to_string()) + .collect::>() + }); + + let orders = state + .reader + .get_orders_status(pool_id, limit, Some(balance_manager_id), status_filter) + .await?; + + let base_factor = 10u64.pow(base_decimals as u32); + let price_factor = 10u64.pow((9 - base_decimals + quote_decimals) as u32); + + let order_data: Vec> = orders + .into_iter() + .map(|order| { + let order_type = if order.is_bid { "buy" } else { "sell" }; + HashMap::from([ + ("order_id".to_string(), Value::from(order.order_id)), + ( + "balance_manager_id".to_string(), + Value::from(order.balance_manager_id), + ), + ("type".to_string(), Value::from(order_type)), + ( + "current_status".to_string(), + Value::from(order.current_status), + ), + ( + "price".to_string(), + Value::from(order.price as f64 / price_factor as f64), + ), + ("placed_at".to_string(), Value::from(order.placed_at as u64)), + ( + "last_updated_at".to_string(), + Value::from(order.last_updated_at as u64), + ), + ( + "original_quantity".to_string(), + Value::from(order.original_quantity as f64 / base_factor as f64), + ), + ( + "filled_quantity".to_string(), + Value::from(order.filled_quantity as f64 / base_factor as f64), + ), + ( + "remaining_quantity".to_string(), + Value::from(order.remaining_quantity as f64 / base_factor as f64), + ), + ]) + }) + .collect(); + + Ok(Json(order_data)) +} + async fn trades( Path(pool_name): Path, Query(params): Query>, - State(state): State, + State(state): State>, ) -> Result>>, DeepBookError> { // Fetch all pools to map names to IDs and decimals - let connection = &mut state.connect().await?; - let pool_data = schema::pools::table - .filter(schema::pools::pool_name.eq(pool_name.clone())) - .select(( - schema::pools::pool_id, - schema::pools::base_asset_decimals, - schema::pools::quote_asset_decimals, - )) - .first::<(String, i16, i16)>(connection) - .await - .map_err(|_| DeepBookError::InternalError(format!("Pool '{}' not found", pool_name)))?; - + let (pool_id, base_decimals, quote_decimals) = + state.reader.get_pool_decimals(&pool_name).await?; // Parse start_time and end_time - let end_time = params - .get("end_time") - .and_then(|v| v.parse::().ok()) - .map(|t| t * 1000) // Convert to milliseconds - .unwrap_or_else(|| { - SystemTime::now() - .duration_since(UNIX_EPOCH) - .unwrap() - .as_millis() as i64 - }); - + let end_time = params.end_time(); let start_time = params - .get("start_time") - .and_then(|v| v.parse::().ok()) - .map(|t| t * 1000) // Convert to milliseconds + .start_time() .unwrap_or_else(|| end_time - 24 * 60 * 60 * 1000); // Parse limit (default to 1 if not provided) - let limit = params - .get("limit") - .and_then(|v| v.parse::().ok()) - .unwrap_or(1); + let limit = params.limit(); // Parse optional filters for balance managers let maker_balance_manager_filter = params.get("maker_balance_manager_id").cloned(); let taker_balance_manager_filter = params.get("taker_balance_manager_id").cloned(); + let balance_manager_filter = params.get("balance_manager_id").cloned(); - let (pool_id, base_decimals, quote_decimals) = pool_data; let base_decimals = base_decimals as u8; let quote_decimals = quote_decimals as u8; - // Build the query dynamically - let mut query = schema::order_fills::table - .filter(schema::order_fills::pool_id.eq(pool_id)) - .filter(schema::order_fills::checkpoint_timestamp_ms.between(start_time, end_time)) - .into_boxed(); - - // Apply optional filters if parameters are provided - if let Some(maker_id) = maker_balance_manager_filter { - query = query.filter(schema::order_fills::maker_balance_manager_id.eq(maker_id)); - } - if let Some(taker_id) = taker_balance_manager_filter { - query = query.filter(schema::order_fills::taker_balance_manager_id.eq(taker_id)); - } - - // Fetch latest trades (sorted by timestamp in descending order) within the time range, applying the limit - let trades = query - .order_by(schema::order_fills::checkpoint_timestamp_ms.desc()) // Ensures latest trades come first - .limit(limit) // Apply limit to get the most recent trades - .select(( - schema::order_fills::maker_order_id, - schema::order_fills::taker_order_id, - schema::order_fills::price, - schema::order_fills::base_quantity, - schema::order_fills::quote_quantity, - schema::order_fills::checkpoint_timestamp_ms, - schema::order_fills::taker_is_bid, - schema::order_fills::maker_balance_manager_id, - schema::order_fills::taker_balance_manager_id, - )) - .load::<(String, String, i64, i64, i64, i64, bool, String, String)>(connection) - .await - .map_err(|_| { - DeepBookError::InternalError(format!( - "No trades found for pool '{}' in the specified time range", - pool_name - )) - })?; + let trades = state + .reader + .get_orders( + pool_name, + pool_id, + start_time, + end_time, + limit, + maker_balance_manager_filter, + taker_balance_manager_filter, + balance_manager_filter, + ) + .await?; // Conversion factors for decimals let base_factor = 10u64.pow(base_decimals as u32); let quote_factor = 10u64.pow(quote_decimals as u32); + let deep_factor = 10u64.pow(6 as u32); let price_factor = 10u64.pow((9 - base_decimals + quote_decimals) as u32); // Map trades to JSON format - let trade_data: Vec> = trades + let trade_data = trades .into_iter() .map( |( + event_digest, + digest, maker_order_id, taker_order_id, + maker_client_order_id, + taker_client_order_id, price, base_quantity, quote_quantity, @@ -1020,14 +1188,50 @@ async fn trades( taker_is_bid, maker_balance_manager_id, taker_balance_manager_id, + taker_fee_is_deep, + maker_fee_is_deep, + taker_fee, + maker_fee, )| { let trade_id = calculate_trade_id(&maker_order_id, &taker_order_id).unwrap_or(0); let trade_type = if taker_is_bid { "buy" } else { "sell" }; + // Scale taker_fee based on taker_is_bid and taker_fee_is_deep + let scaled_taker_fee = if taker_fee_is_deep { + taker_fee as f64 / deep_factor as f64 + } else if taker_is_bid { + // taker is buying, fee paid in quote asset + taker_fee as f64 / quote_factor as f64 + } else { + // taker is selling, fee paid in base asset + taker_fee as f64 / base_factor as f64 + }; + + // Scale maker_fee based on taker_is_bid and maker_fee_is_deep + let scaled_maker_fee = if maker_fee_is_deep { + maker_fee as f64 / deep_factor as f64 + } else if taker_is_bid { + // taker is buying, maker is selling, fee paid in base asset + maker_fee as f64 / base_factor as f64 + } else { + // taker is selling, maker is buying, fee paid in quote asset + maker_fee as f64 / quote_factor as f64 + }; + HashMap::from([ + ("event_digest".to_string(), Value::from(event_digest)), + ("digest".to_string(), Value::from(digest)), ("trade_id".to_string(), Value::from(trade_id.to_string())), ("maker_order_id".to_string(), Value::from(maker_order_id)), ("taker_order_id".to_string(), Value::from(taker_order_id)), + ( + "maker_client_order_id".to_string(), + Value::from(maker_client_order_id.to_string()), + ), + ( + "taker_client_order_id".to_string(), + Value::from(taker_client_order_id.to_string()), + ), ( "maker_balance_manager_id".to_string(), Value::from(maker_balance_manager_id), @@ -1050,6 +1254,17 @@ async fn trades( ), ("timestamp".to_string(), Value::from(timestamp as u64)), ("type".to_string(), Value::from(trade_type)), + ("taker_is_bid".to_string(), Value::from(taker_is_bid)), + ("taker_fee".to_string(), Value::from(scaled_taker_fee)), + ("maker_fee".to_string(), Value::from(scaled_maker_fee)), + ( + "taker_fee_is_deep".to_string(), + Value::from(taker_fee_is_deep), + ), + ( + "maker_fee_is_deep".to_string(), + Value::from(maker_fee_is_deep), + ), ]) }, ) @@ -1060,33 +1275,19 @@ async fn trades( async fn trade_count( Query(params): Query>, - State(state): State, + State(state): State>, ) -> Result, DeepBookError> { // Parse start_time and end_time - let end_time = params - .get("end_time") - .and_then(|v| v.parse::().ok()) - .map(|t| t * 1000) // Convert to milliseconds - .unwrap_or_else(|| { - SystemTime::now() - .duration_since(UNIX_EPOCH) - .unwrap() - .as_millis() as i64 - }); - + let end_time = params.end_time(); let start_time = params - .get("start_time") - .and_then(|v| v.parse::().ok()) - .map(|t| t * 1000) // Convert to milliseconds + .start_time() // Convert to milliseconds .unwrap_or_else(|| end_time - 24 * 60 * 60 * 1000); - let connection = &mut state.connect().await?; - let result: i64 = schema::order_fills::table + let query = schema::order_fills::table .select(count_star()) - .filter(schema::order_fills::checkpoint_timestamp_ms.between(start_time, end_time)) - .first(connection) - .await?; + .filter(schema::order_fills::checkpoint_timestamp_ms.between(start_time, end_time)); + let result = state.reader.first(query).await?; Ok(Json(result)) } @@ -1094,10 +1295,10 @@ fn calculate_trade_id(maker_id: &str, taker_id: &str) -> Result() - .map_err(|_| DeepBookError::InternalError("Invalid maker_id".to_string()))?; + .map_err(|_| DeepBookError::bad_request("Invalid maker_id"))?; let taker_id = taker_id .parse::() - .map_err(|_| DeepBookError::InternalError("Invalid taker_id".to_string()))?; + .map_err(|_| DeepBookError::bad_request("Invalid taker_id"))?; // Ignore the most significant bit for both IDs let maker_id = maker_id & !(1 << 127); @@ -1108,26 +1309,34 @@ fn calculate_trade_id(maker_id: &str, taker_id: &str) -> Result, + State(state): State>, ) -> Result>>, DeepBookError> { - let connection = &mut state.connect().await?; - let assets = schema::assets::table - .select(( - schema::assets::symbol, - schema::assets::name, - schema::assets::ucid, - schema::assets::package_address_url, - schema::assets::package_id, - )) - .load::<(String, String, Option, Option, Option)>(connection) + let query = schema::assets::table.select(( + schema::assets::symbol, + schema::assets::name, + schema::assets::ucid, + schema::assets::package_address_url, + schema::assets::package_id, + schema::assets::asset_type, + )); + let assets: Vec<( + String, + String, + Option, + Option, + Option, + String, + )> = state + .reader + .results(query) .await - .map_err(|err| DeepBookError::InternalError(format!("Failed to query assets: {}", err)))?; - + .map_err(|err| DeepBookError::rpc(format!("Failed to query assets: {}", err)))?; let mut response = HashMap::new(); - for (symbol, name, ucid, package_address_url, package_id) in assets { + for (symbol, name, ucid, package_address_url, package_id, asset_type) in assets { let mut asset_info = HashMap::new(); asset_info.insert("name".to_string(), Value::String(name)); + asset_info.insert("asset_type".to_string(), Value::String(asset_type)); asset_info.insert( "can_withdraw".to_string(), Value::String("true".to_string()), @@ -1158,22 +1367,19 @@ pub async fn assets( async fn orderbook( Path(pool_name): Path, Query(params): Query>, - State((state, rpc_url)): State<(Db, Url)>, + State(state): State>, ) -> Result>, DeepBookError> { let depth = params .get("depth") .map(|v| v.parse::()) .transpose() - .map_err(|_| { - DeepBookError::InternalError("Depth must be a non-negative integer".to_string()) - })? + .map_err(|_| DeepBookError::bad_request("Depth must be a non-negative integer"))? .map(|depth| if depth == 0 { 200 } else { depth }); if let Some(depth) = depth { if depth == 1 { - return Err(DeepBookError::InternalError( - "Depth cannot be 1. Use a value greater than 1 or 0 for the entire orderbook" - .to_string(), + return Err(DeepBookError::bad_request( + "Depth cannot be 1. Use a value greater than 1 or 0 for the entire orderbook", )); } } @@ -1182,15 +1388,11 @@ async fn orderbook( .get("level") .map(|v| v.parse::()) .transpose() - .map_err(|_| { - DeepBookError::InternalError("Level must be an integer between 1 and 2".to_string()) - })?; + .map_err(|_| DeepBookError::bad_request("Level must be an integer between 1 and 2"))?; if let Some(level) = level { if !(1..=2).contains(&level) { - return Err(DeepBookError::InternalError( - "Level must be 1 or 2".to_string(), - )); + return Err(DeepBookError::bad_request("Level must be 1 or 2")); } } @@ -1203,8 +1405,7 @@ async fn orderbook( }; // Fetch the pool data from the `pools` table - let connection = &mut state.connect().await?; - let pool_data = schema::pools::table + let query = schema::pools::table .filter(schema::pools::pool_name.eq(pool_name.clone())) .select(( schema::pools::pool_id, @@ -1212,67 +1413,68 @@ async fn orderbook( schema::pools::base_asset_decimals, schema::pools::quote_asset_id, schema::pools::quote_asset_decimals, - )) - .first::<(String, String, i16, String, i16)>(connection) - .await?; - + )); + let pool_data: (String, String, i16, String, i16) = state.reader.first(query).await?; let (pool_id, base_asset_id, base_decimals, quote_asset_id, quote_decimals) = pool_data; let base_decimals = base_decimals as u8; let quote_decimals = quote_decimals as u8; let pool_address = ObjectID::from_hex_literal(&pool_id)?; - let sui_client = SuiClientBuilder::default().build(rpc_url.as_str()).await?; + let sui_client = state.sui_client().await?; let mut ptb = ProgrammableTransactionBuilder::new(); let pool_object: SuiObjectResponse = sui_client .read_api() - .get_object_with_options(pool_address, SuiObjectDataOptions::full_content()) + .get_object_with_options( + pool_address, + SuiObjectDataOptions::full_content().with_owner(), + ) .await?; - let pool_data: &SuiObjectData = - pool_object - .data - .as_ref() - .ok_or(DeepBookError::InternalError(format!( - "Missing data in pool object response for '{}'", + let pool_data: &SuiObjectData = pool_object.data.as_ref().ok_or(DeepBookError::rpc( + format!("Missing data in pool object response for '{}'", pool_name), + ))?; + + let initial_shared_version = match &pool_data.owner { + Some(sui_types::object::Owner::Shared { + initial_shared_version, + }) => *initial_shared_version, + _ => { + return Err(DeepBookError::rpc(format!( + "Pool '{}' is not a shared object or owner info missing", pool_name - )))?; - let pool_object_ref: ObjectRef = (pool_data.object_id, pool_data.version, pool_data.digest); + ))); + } + }; - let pool_input = CallArg::Object(ObjectArg::ImmOrOwnedObject(pool_object_ref)); + let pool_input = CallArg::Object(ObjectArg::SharedObject { + id: pool_data.object_id, + initial_shared_version, + mutability: sui_types::transaction::SharedObjectMutability::Immutable, + }); ptb.input(pool_input)?; - let input_argument = CallArg::Pure(bcs::to_bytes(&ticks_from_mid).map_err(|_| { - DeepBookError::InternalError("Failed to serialize ticks_from_mid".to_string()) - })?); + let input_argument = CallArg::Pure( + bcs::to_bytes(&ticks_from_mid) + .map_err(|_| DeepBookError::internal("Failed to serialize ticks_from_mid"))?, + ); ptb.input(input_argument)?; let sui_clock_object_id = ObjectID::from_hex_literal( "0x0000000000000000000000000000000000000000000000000000000000000006", )?; - let sui_clock_object: SuiObjectResponse = sui_client - .read_api() - .get_object_with_options(sui_clock_object_id, SuiObjectDataOptions::full_content()) - .await?; - let clock_data: &SuiObjectData = - sui_clock_object - .data - .as_ref() - .ok_or(DeepBookError::InternalError( - "Missing data in clock object response".to_string(), - ))?; - - let sui_clock_object_ref: ObjectRef = - (clock_data.object_id, clock_data.version, clock_data.digest); - - let clock_input = CallArg::Object(ObjectArg::ImmOrOwnedObject(sui_clock_object_ref)); + let clock_input = CallArg::Object(ObjectArg::SharedObject { + id: sui_clock_object_id, + initial_shared_version: sui_types::base_types::SequenceNumber::from_u64(1), + mutability: sui_types::transaction::SharedObjectMutability::Immutable, + }); ptb.input(clock_input)?; let base_coin_type = parse_type_input(&base_asset_id)?; let quote_coin_type = parse_type_input("e_asset_id)?; - let package = ObjectID::from_hex_literal(DEEPBOOK_PACKAGE_ID) - .map_err(|e| DeepBookError::InternalError(format!("Invalid pool ID: {}", e)))?; + let package = ObjectID::from_hex_literal(&state.deepbook_package_id) + .map_err(|e| DeepBookError::bad_request(format!("Invalid pool ID: {}", e)))?; let module = LEVEL2_MODULE.to_string(); let function = LEVEL2_FUNCTION.to_string(); @@ -1292,72 +1494,52 @@ async fn orderbook( .dev_inspect_transaction_block(SuiAddress::default(), tx, None, None, None) .await?; - let mut binding = result.results.ok_or(DeepBookError::InternalError( - "No results from dev_inspect_transaction_block".to_string(), + let mut binding = result.results.ok_or(DeepBookError::rpc( + "No results from dev_inspect_transaction_block", ))?; let bid_prices = &binding .first_mut() - .ok_or(DeepBookError::InternalError( - "No return values for bid prices".to_string(), - ))? + .ok_or(DeepBookError::rpc("No return values for bid prices"))? .return_values .first_mut() - .ok_or(DeepBookError::InternalError( - "No bid price data found".to_string(), - ))? + .ok_or(DeepBookError::rpc("No bid price data found"))? .0; - let bid_parsed_prices: Vec = bcs::from_bytes(bid_prices).map_err(|_| { - DeepBookError::InternalError("Failed to deserialize bid prices".to_string()) - })?; + let bid_parsed_prices: Vec = bcs::from_bytes(bid_prices) + .map_err(|_| DeepBookError::deserialization("Failed to deserialize bid prices"))?; let bid_quantities = &binding .first_mut() - .ok_or(DeepBookError::InternalError( - "No return values for bid quantities".to_string(), - ))? + .ok_or(DeepBookError::rpc("No return values for bid quantities"))? .return_values .get(1) - .ok_or(DeepBookError::InternalError( - "No bid quantity data found".to_string(), - ))? + .ok_or(DeepBookError::rpc("No bid quantity data found"))? .0; - let bid_parsed_quantities: Vec = bcs::from_bytes(bid_quantities).map_err(|_| { - DeepBookError::InternalError("Failed to deserialize bid quantities".to_string()) - })?; + let bid_parsed_quantities: Vec = bcs::from_bytes(bid_quantities) + .map_err(|_| DeepBookError::deserialization("Failed to deserialize bid quantities"))?; let ask_prices = &binding .first_mut() - .ok_or(DeepBookError::InternalError( - "No return values for ask prices".to_string(), - ))? + .ok_or(DeepBookError::rpc("No return values for ask prices"))? .return_values .get(2) - .ok_or(DeepBookError::InternalError( - "No ask price data found".to_string(), - ))? + .ok_or(DeepBookError::rpc("No ask price data found"))? .0; - let ask_parsed_prices: Vec = bcs::from_bytes(ask_prices).map_err(|_| { - DeepBookError::InternalError("Failed to deserialize ask prices".to_string()) - })?; + let ask_parsed_prices: Vec = bcs::from_bytes(ask_prices) + .map_err(|_| DeepBookError::deserialization("Failed to deserialize ask prices"))?; let ask_quantities = &binding .first_mut() - .ok_or(DeepBookError::InternalError( - "No return values for ask quantities".to_string(), - ))? + .ok_or(DeepBookError::rpc("No return values for ask quantities"))? .return_values .get(3) - .ok_or(DeepBookError::InternalError( - "No ask quantity data found".to_string(), - ))? + .ok_or(DeepBookError::rpc("No ask quantity data found"))? .0; - let ask_parsed_quantities: Vec = bcs::from_bytes(ask_quantities).map_err(|_| { - DeepBookError::InternalError("Failed to deserialize ask quantities".to_string()) - })?; + let ask_parsed_quantities: Vec = bcs::from_bytes(ask_quantities) + .map_err(|_| DeepBookError::deserialization("Failed to deserialize ask quantities"))?; let mut result = HashMap::new(); let timestamp = SystemTime::now() .duration_since(UNIX_EPOCH) - .map_err(|_| DeepBookError::InternalError("System time error".to_string()))? + .map_err(|_| DeepBookError::internal("System time error"))? .as_millis() as i64; result.insert("timestamp".to_string(), Value::from(timestamp.to_string())); @@ -1395,38 +1577,40 @@ async fn orderbook( } /// DEEP total supply -async fn deep_supply(State((_, rpc_url)): State<(Db, Url)>) -> Result, DeepBookError> { - let sui_client = SuiClientBuilder::default().build(rpc_url.as_str()).await?; +async fn deep_supply(State(state): State>) -> Result, DeepBookError> { + let sui_client = state.sui_client().await?; let mut ptb = ProgrammableTransactionBuilder::new(); - let deep_treasury_object_id = ObjectID::from_hex_literal(DEEP_TREASURY_ID)?; + let deep_treasury_object_id = ObjectID::from_hex_literal(&state.deep_treasury_id)?; let deep_treasury_object: SuiObjectResponse = sui_client .read_api() .get_object_with_options( deep_treasury_object_id, - SuiObjectDataOptions::full_content(), + SuiObjectDataOptions::full_content().with_owner(), ) .await?; - let deep_treasury_data: &SuiObjectData = - deep_treasury_object - .data - .as_ref() - .ok_or(DeepBookError::InternalError( - "Incorrect Treasury ID".to_string(), - ))?; - - let deep_treasury_ref: ObjectRef = ( - deep_treasury_data.object_id, - deep_treasury_data.version, - deep_treasury_data.digest, - ); - - let deep_treasury_input = CallArg::Object(ObjectArg::ImmOrOwnedObject(deep_treasury_ref)); + let deep_treasury_data: &SuiObjectData = deep_treasury_object + .data + .as_ref() + .ok_or(DeepBookError::rpc("Incorrect Treasury ID"))?; + + let initial_shared_version = match &deep_treasury_data.owner { + Some(sui_types::object::Owner::Shared { + initial_shared_version, + }) => *initial_shared_version, + _ => { + return Err(DeepBookError::rpc("Treasury is not a shared object")); + } + }; + let deep_treasury_input = CallArg::Object(ObjectArg::SharedObject { + id: deep_treasury_data.object_id, + initial_shared_version, + mutability: sui_types::transaction::SharedObjectMutability::Immutable, + }); ptb.input(deep_treasury_input)?; - let package = ObjectID::from_hex_literal(DEEP_TOKEN_PACKAGE_ID).map_err(|e| { - DeepBookError::InternalError(format!("Invalid deep token package ID: {}", e)) - })?; + let package = ObjectID::from_hex_literal(&state.deep_token_package_id) + .map_err(|e| DeepBookError::bad_request(format!("Invalid deep token package ID: {}", e)))?; let module = DEEP_SUPPLY_MODULE.to_string(); let function = DEEP_SUPPLY_FUNCTION.to_string(); @@ -1446,69 +1630,936 @@ async fn deep_supply(State((_, rpc_url)): State<(Db, Url)>) -> Result, .dev_inspect_transaction_block(SuiAddress::default(), tx, None, None, None) .await?; - let mut binding = result.results.ok_or(DeepBookError::InternalError( - "No results from dev_inspect_transaction_block".to_string(), + let mut binding = result.results.ok_or(DeepBookError::rpc( + "No results from dev_inspect_transaction_block", ))?; let total_supply = &binding .first_mut() - .ok_or(DeepBookError::InternalError( - "No return values for total supply".to_string(), - ))? + .ok_or(DeepBookError::rpc("No return values for total supply"))? .return_values .first_mut() - .ok_or(DeepBookError::InternalError( - "No total supply data found".to_string(), - ))? + .ok_or(DeepBookError::rpc("No total supply data found"))? .0; - let total_supply_value: u64 = bcs::from_bytes(total_supply).map_err(|_| { - DeepBookError::InternalError("Failed to deserialize total supply".to_string()) - })?; + let total_supply_value: u64 = bcs::from_bytes(total_supply) + .map_err(|_| DeepBookError::deserialization("Failed to deserialize total supply"))?; Ok(Json(total_supply_value)) } -async fn get_net_deposits( - Path((asset_ids, timestamp)): Path<(String, String)>, - State(state): State, -) -> Result>, DeepBookError> { - let connection = &mut state.connect().await?; - let mut query = - "SELECT asset, SUM(amount)::bigint AS amount, deposit FROM balances WHERE checkpoint_timestamp_ms < " - .to_string(); - query.push_str(×tamp); - query.push_str("000 AND asset in ("); - for asset in asset_ids.split(",") { - if asset.starts_with("0x") { - let len = asset.len(); - query.push_str(&format!("'{}',", &asset[2..len])); - } else { - query.push_str(&format!("'{}',", asset)); - } - } - query.pop(); - query.push_str(") GROUP BY asset, deposit"); - - let results: Vec = diesel::sql_query(query).load(connection).await?; - let mut net_deposits = HashMap::new(); - for result in results { - let mut asset = result.asset; - if !asset.starts_with("0x") { - asset.insert_str(0, "0x"); - } - let amount = result.amount; - if result.deposit { - *net_deposits.entry(asset).or_insert(0) += amount; - } else { - *net_deposits.entry(asset).or_insert(0) -= amount; - } +/// Get total supply for all margin pools +async fn margin_supply( + State(state): State>, +) -> Result>, DeepBookError> { + let margin_package_id = state + .margin_package_id + .as_ref() + .ok_or_else(|| DeepBookError::bad_request("Margin package ID not configured"))?; + + // Query all margin pools from the database + let query = schema::margin_pool_created::table.select(( + schema::margin_pool_created::margin_pool_id, + schema::margin_pool_created::asset_type, + )); + let pools: Vec<(String, String)> = state.reader.results(query).await?; + + if pools.is_empty() { + return Ok(Json(HashMap::new())); } - Ok(Json(net_deposits)) -} + let sui_client = state.sui_client().await?; + let package = ObjectID::from_hex_literal(margin_package_id) + .map_err(|e| DeepBookError::bad_request(format!("Invalid margin package ID: {}", e)))?; -fn parse_type_input(type_str: &str) -> Result { - let type_tag = TypeTag::from_str(type_str)?; - Ok(TypeInput::from(type_tag)) + let mut result: HashMap = HashMap::new(); + + for (pool_id, asset_type) in pools { + let pool_object_id = ObjectID::from_hex_literal(&pool_id).map_err(|e| { + DeepBookError::bad_request(format!("Invalid pool ID '{}': {}", pool_id, e)) + })?; + + // Get the pool object to find its initial_shared_version + let pool_object: SuiObjectResponse = sui_client + .read_api() + .get_object_with_options( + pool_object_id, + SuiObjectDataOptions::full_content().with_owner(), + ) + .await?; + + let pool_data: &SuiObjectData = pool_object.data.as_ref().ok_or(DeepBookError::rpc( + format!("Missing data in pool object response for '{}'", pool_id), + ))?; + + let initial_shared_version = match &pool_data.owner { + Some(sui_types::object::Owner::Shared { + initial_shared_version, + }) => *initial_shared_version, + _ => { + continue; + } + }; + + // Normalize asset type (ensure 0x prefix) + let normalized_asset_type = if asset_type.starts_with("0x") || asset_type.starts_with("0X") + { + asset_type.clone() + } else { + format!("0x{}", asset_type) + }; + + let type_tag = match TypeTag::from_str(&normalized_asset_type) { + Ok(t) => t, + Err(_) => continue, + }; + let type_input = TypeInput::from(type_tag); + + // Build PTB for total_supply call + let mut ptb = ProgrammableTransactionBuilder::new(); + + let pool_input = CallArg::Object(ObjectArg::SharedObject { + id: pool_data.object_id, + initial_shared_version, + mutability: sui_types::transaction::SharedObjectMutability::Immutable, + }); + ptb.input(pool_input)?; + + ptb.command(Command::MoveCall(Box::new(ProgrammableMoveCall { + package, + module: MARGIN_POOL_MODULE.to_string(), + function: "total_supply".to_string(), + type_arguments: vec![type_input], + arguments: vec![Argument::Input(0)], + }))); + + let builder = ptb.finish(); + let tx = TransactionKind::ProgrammableTransaction(builder); + + let inspect_result = sui_client + .read_api() + .dev_inspect_transaction_block(SuiAddress::default(), tx, None, None, None) + .await?; + + if let Some(mut results) = inspect_result.results { + if let Some(first_result) = results.first_mut() { + if let Some(return_value) = first_result.return_values.first() { + if let Ok(total_supply) = bcs::from_bytes::(&return_value.0) { + // Extract asset name from asset_type (e.g., "0x2::sui::SUI" -> "SUI") + let asset_name = asset_type + .rsplit("::") + .next() + .unwrap_or(&asset_type) + .to_string(); + result.insert(asset_name, total_supply); + } + } + } + } + } + + Ok(Json(result)) +} + +async fn get_net_deposits( + Path((asset_ids, timestamp)): Path<(String, String)>, + State(state): State>, +) -> Result>, DeepBookError> { + let timestamp_ms = timestamp + .parse::() + .map_err(|_| DeepBookError::bad_request("Invalid timestamp"))? + * 1000; // Convert seconds to milliseconds + + let assets: Vec = asset_ids.split(',').map(|s| s.to_string()).collect(); + + let net_deposits = state + .reader + .get_net_deposits_from_view(&assets, timestamp_ms) + .await?; + + Ok(Json(net_deposits)) +} + +fn parse_type_input(type_str: &str) -> Result { + let type_tag = TypeTag::from_str(type_str)?; + Ok(TypeInput::from(type_tag)) +} + +trait ParameterUtil { + fn start_time(&self) -> Option; + fn end_time(&self) -> i64; + fn volume_in_base(&self) -> bool; + + fn limit(&self) -> i64; +} + +impl ParameterUtil for HashMap { + fn start_time(&self) -> Option { + self.get("start_time") + .and_then(|v| v.parse::().ok()) + .map(|t| t * 1000) // Convert + } + + fn end_time(&self) -> i64 { + self.get("end_time") + .and_then(|v| v.parse::().ok()) + .map(|t| t * 1000) // Convert to milliseconds + .unwrap_or_else(|| { + SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_millis() as i64 + }) + } + + fn volume_in_base(&self) -> bool { + self.get("volume_in_base") + .map(|v| v == "true") + .unwrap_or_default() + } + + fn limit(&self) -> i64 { + self.get("limit") + .and_then(|v| v.parse::().ok()) + .unwrap_or(1) + } +} + +async fn ohclv( + Path(pool_name): Path, + Query(params): Query>, + State(state): State>, +) -> Result>, DeepBookError> { + let pools = state.reader.get_pools().await?; + let pool = pools + .iter() + .find(|p| p.pool_name == pool_name) + .ok_or_else(|| DeepBookError::not_found(format!("Pool '{}'", pool_name)))?; + + let interval = params.get("interval").unwrap_or(&"1m".to_string()).clone(); + let start_time = params.get("start_time").and_then(|v| v.parse::().ok()); + let end_time = params.get("end_time").and_then(|v| v.parse::().ok()); + let limit = params.get("limit").and_then(|v| v.parse::().ok()); + + let valid_intervals = vec!["1m", "5m", "15m", "30m", "1h", "4h", "1d", "1w"]; + if !valid_intervals.contains(&interval.as_str()) { + return Err(DeepBookError::bad_request(format!( + "Invalid interval: {}. Valid intervals are: {:?}", + interval, valid_intervals + ))); + } + + let candles = state + .reader + .get_ohclv(pool.pool_id.clone(), interval, start_time, end_time, limit) + .await?; + let candles_array: Vec = candles + .into_iter() + .map(|(timestamp, open, high, low, close, volume)| { + Value::Array(vec![ + Value::from(timestamp), + Value::from(open), + Value::from(high), + Value::from(low), + Value::from(close), + Value::from(volume), + ]) + }) + .collect(); + + let mut response = HashMap::new(); + response.insert("candles".to_string(), Value::Array(candles_array)); + + Ok(Json(response)) +} + +// === Margin Manager Events Handlers === +async fn margin_manager_created( + Query(params): Query>, + State(state): State>, +) -> Result>, DeepBookError> { + let end_time = params.end_time(); + let start_time = params + .start_time() + .unwrap_or_else(|| end_time - 24 * 60 * 60 * 1000); + let limit = params.limit(); + let margin_manager_id_filter = params.get("margin_manager_id").cloned().unwrap_or_default(); + + let results = state + .reader + .get_margin_manager_created(start_time, end_time, limit, margin_manager_id_filter) + .await?; + + Ok(Json(results)) +} + +async fn loan_borrowed( + Query(params): Query>, + State(state): State>, +) -> Result>, DeepBookError> { + let end_time = params.end_time(); + let start_time = params + .start_time() + .unwrap_or_else(|| end_time - 24 * 60 * 60 * 1000); + let limit = params.limit(); + let margin_manager_id_filter = params.get("margin_manager_id").cloned().unwrap_or_default(); + let margin_pool_id_filter = params.get("margin_pool_id").cloned().unwrap_or_default(); + + let results = state + .reader + .get_loan_borrowed( + start_time, + end_time, + limit, + margin_manager_id_filter, + margin_pool_id_filter, + ) + .await?; + + Ok(Json(results)) +} + +async fn loan_repaid( + Query(params): Query>, + State(state): State>, +) -> Result>, DeepBookError> { + let end_time = params.end_time(); + let start_time = params + .start_time() + .unwrap_or_else(|| end_time - 24 * 60 * 60 * 1000); + let limit = params.limit(); + let margin_manager_id_filter = params.get("margin_manager_id").cloned().unwrap_or_default(); + let margin_pool_id_filter = params.get("margin_pool_id").cloned().unwrap_or_default(); + + let results = state + .reader + .get_loan_repaid( + start_time, + end_time, + limit, + margin_manager_id_filter, + margin_pool_id_filter, + ) + .await?; + + Ok(Json(results)) +} + +async fn liquidation( + Query(params): Query>, + State(state): State>, +) -> Result>, DeepBookError> { + let end_time = params.end_time(); + let start_time = params + .start_time() + .unwrap_or_else(|| end_time - 24 * 60 * 60 * 1000); + let limit = params.limit(); + let margin_manager_id_filter = params.get("margin_manager_id").cloned().unwrap_or_default(); + let margin_pool_id_filter = params.get("margin_pool_id").cloned().unwrap_or_default(); + + let results = state + .reader + .get_liquidation( + start_time, + end_time, + limit, + margin_manager_id_filter, + margin_pool_id_filter, + ) + .await?; + + Ok(Json(results)) +} + +// === Margin Pool Operations Events Handlers === +async fn asset_supplied( + Query(params): Query>, + State(state): State>, +) -> Result>, DeepBookError> { + let end_time = params.end_time(); + let start_time = params + .start_time() + .unwrap_or_else(|| end_time - 24 * 60 * 60 * 1000); + let limit = params.limit(); + let margin_pool_id_filter = params.get("margin_pool_id").cloned().unwrap_or_default(); + let supplier_filter = params.get("supplier").cloned().unwrap_or_default(); + + let results = state + .reader + .get_asset_supplied( + start_time, + end_time, + limit, + margin_pool_id_filter, + supplier_filter, + ) + .await?; + + Ok(Json(results)) +} + +async fn asset_withdrawn( + Query(params): Query>, + State(state): State>, +) -> Result>, DeepBookError> { + let end_time = params.end_time(); + let start_time = params + .start_time() + .unwrap_or_else(|| end_time - 24 * 60 * 60 * 1000); + let limit = params.limit(); + let margin_pool_id_filter = params.get("margin_pool_id").cloned().unwrap_or_default(); + let supplier_filter = params.get("supplier").cloned().unwrap_or_default(); + + let results = state + .reader + .get_asset_withdrawn( + start_time, + end_time, + limit, + margin_pool_id_filter, + supplier_filter, + ) + .await?; + + Ok(Json(results)) +} + +// === Margin Pool Admin Events Handlers === +async fn margin_pool_created( + Query(params): Query>, + State(state): State>, +) -> Result>, DeepBookError> { + let margin_pool_id_filter = params.get("margin_pool_id").cloned().unwrap_or_default(); + + let results = state + .reader + .get_margin_pool_created(margin_pool_id_filter) + .await?; + + Ok(Json(results)) +} + +async fn deepbook_pool_updated( + Query(params): Query>, + State(state): State>, +) -> Result>, DeepBookError> { + let end_time = params.end_time(); + let start_time = params + .start_time() + .unwrap_or_else(|| end_time - 24 * 60 * 60 * 1000); + let limit = params.limit(); + let margin_pool_id_filter = params.get("margin_pool_id").cloned().unwrap_or_default(); + let deepbook_pool_id_filter = params.get("deepbook_pool_id").cloned().unwrap_or_default(); + + let results = state + .reader + .get_deepbook_pool_updated( + start_time, + end_time, + limit, + margin_pool_id_filter, + deepbook_pool_id_filter, + ) + .await?; + + Ok(Json(results)) +} + +async fn interest_params_updated( + Query(params): Query>, + State(state): State>, +) -> Result>, DeepBookError> { + let end_time = params.end_time(); + let start_time = params + .start_time() + .unwrap_or_else(|| end_time - 24 * 60 * 60 * 1000); + let limit = params.limit(); + let margin_pool_id_filter = params.get("margin_pool_id").cloned().unwrap_or_default(); + + let results = state + .reader + .get_interest_params_updated(start_time, end_time, limit, margin_pool_id_filter) + .await?; + + Ok(Json(results)) +} + +async fn margin_pool_config_updated( + Query(params): Query>, + State(state): State>, +) -> Result>, DeepBookError> { + let end_time = params.end_time(); + let start_time = params + .start_time() + .unwrap_or_else(|| end_time - 24 * 60 * 60 * 1000); + let limit = params.limit(); + let margin_pool_id_filter = params.get("margin_pool_id").cloned().unwrap_or_default(); + + let results = state + .reader + .get_margin_pool_config_updated(start_time, end_time, limit, margin_pool_id_filter) + .await?; + + Ok(Json(results)) +} + +// === Margin Registry Events Handlers === +async fn maintainer_cap_updated( + Query(params): Query>, + State(state): State>, +) -> Result>, DeepBookError> { + let end_time = params.end_time(); + let start_time = params + .start_time() + .unwrap_or_else(|| end_time - 24 * 60 * 60 * 1000); + let limit = params.limit(); + let maintainer_cap_id_filter = params.get("maintainer_cap_id").cloned().unwrap_or_default(); + + let results = state + .reader + .get_maintainer_cap_updated(start_time, end_time, limit, maintainer_cap_id_filter) + .await?; + + Ok(Json(results)) +} + +async fn maintainer_fees_withdrawn( + Query(params): Query>, + State(state): State>, +) -> Result>, DeepBookError> { + let end_time = params.end_time(); + let start_time = params + .start_time() + .unwrap_or_else(|| end_time - 24 * 60 * 60 * 1000); + let limit = params.limit(); + let margin_pool_id_filter = params.get("margin_pool_id").cloned().unwrap_or_default(); + + let results = state + .reader + .get_maintainer_fees_withdrawn(start_time, end_time, limit, margin_pool_id_filter) + .await?; + + Ok(Json(results)) +} + +async fn protocol_fees_withdrawn( + Query(params): Query>, + State(state): State>, +) -> Result>, DeepBookError> { + let end_time = params.end_time(); + let start_time = params + .start_time() + .unwrap_or_else(|| end_time - 24 * 60 * 60 * 1000); + let limit = params.limit(); + let margin_pool_id_filter = params.get("margin_pool_id").cloned().unwrap_or_default(); + + let results = state + .reader + .get_protocol_fees_withdrawn(start_time, end_time, limit, margin_pool_id_filter) + .await?; + + Ok(Json(results)) +} + +async fn supplier_cap_minted( + Query(params): Query>, + State(state): State>, +) -> Result>, DeepBookError> { + let end_time = params.end_time(); + let start_time = params + .start_time() + .unwrap_or_else(|| end_time - 24 * 60 * 60 * 1000); + let limit = params.limit(); + let supplier_cap_id_filter = params.get("supplier_cap_id").cloned().unwrap_or_default(); + + let results = state + .reader + .get_supplier_cap_minted(start_time, end_time, limit, supplier_cap_id_filter) + .await?; + + Ok(Json(results)) +} + +async fn supply_referral_minted( + Query(params): Query>, + State(state): State>, +) -> Result>, DeepBookError> { + let end_time = params.end_time(); + let start_time = params + .start_time() + .unwrap_or_else(|| end_time - 24 * 60 * 60 * 1000); + let limit = params.limit(); + let margin_pool_id_filter = params.get("margin_pool_id").cloned().unwrap_or_default(); + let owner_filter = params.get("owner").cloned().unwrap_or_default(); + + let results = state + .reader + .get_supply_referral_minted( + start_time, + end_time, + limit, + margin_pool_id_filter, + owner_filter, + ) + .await?; + + Ok(Json(results)) +} + +async fn pause_cap_updated( + Query(params): Query>, + State(state): State>, +) -> Result>, DeepBookError> { + let end_time = params.end_time(); + let start_time = params + .start_time() + .unwrap_or_else(|| end_time - 24 * 60 * 60 * 1000); + let limit = params.limit(); + let pause_cap_id_filter = params.get("pause_cap_id").cloned().unwrap_or_default(); + + let results = state + .reader + .get_pause_cap_updated(start_time, end_time, limit, pause_cap_id_filter) + .await?; + + Ok(Json(results)) +} + +async fn protocol_fees_increased( + Query(params): Query>, + State(state): State>, +) -> Result>, DeepBookError> { + let end_time = params.end_time(); + let start_time = params + .start_time() + .unwrap_or_else(|| end_time - 24 * 60 * 60 * 1000); + let limit = params.limit(); + let margin_pool_id_filter = params.get("margin_pool_id").cloned().unwrap_or_default(); + + let results = state + .reader + .get_protocol_fees_increased(start_time, end_time, limit, margin_pool_id_filter) + .await?; + + Ok(Json(results)) +} + +async fn referral_fees_claimed( + Query(params): Query>, + State(state): State>, +) -> Result>, DeepBookError> { + let end_time = params.end_time(); + let start_time = params + .start_time() + .unwrap_or_else(|| end_time - 24 * 60 * 60 * 1000); + let limit = params.limit(); + let referral_id_filter = params.get("referral_id").cloned().unwrap_or_default(); + let owner_filter = params.get("owner").cloned().unwrap_or_default(); + + let results = state + .reader + .get_referral_fees_claimed( + start_time, + end_time, + limit, + referral_id_filter, + owner_filter, + ) + .await?; + + Ok(Json(results)) +} + +async fn referral_fee_events( + Query(params): Query>, + State(state): State>, +) -> Result>, DeepBookError> { + let end_time = params.end_time(); + let start_time = params + .start_time() + .unwrap_or_else(|| end_time - 24 * 60 * 60 * 1000); + let limit = params.limit(); + let pool_id_filter = params.get("pool_id").cloned().unwrap_or_default(); + let referral_id_filter = params.get("referral_id").cloned().unwrap_or_default(); + + let results = state + .reader + .get_referral_fee_events( + start_time, + end_time, + limit, + pool_id_filter, + referral_id_filter, + ) + .await?; + + Ok(Json(results)) +} + +async fn deepbook_pool_registered( + Query(params): Query>, + State(state): State>, +) -> Result>, DeepBookError> { + let end_time = params.end_time(); + let start_time = params + .start_time() + .unwrap_or_else(|| end_time - 24 * 60 * 60 * 1000); + let limit = params.limit(); + let pool_id_filter = params.get("pool_id").cloned().unwrap_or_default(); + + let results = state + .reader + .get_deepbook_pool_registered(start_time, end_time, limit, pool_id_filter) + .await?; + + Ok(Json(results)) +} + +async fn deepbook_pool_updated_registry( + Query(params): Query>, + State(state): State>, +) -> Result>, DeepBookError> { + let end_time = params.end_time(); + let start_time = params + .start_time() + .unwrap_or_else(|| end_time - 24 * 60 * 60 * 1000); + let limit = params.limit(); + let pool_id_filter = params.get("pool_id").cloned().unwrap_or_default(); + + let results = state + .reader + .get_deepbook_pool_updated_registry(start_time, end_time, limit, pool_id_filter) + .await?; + + Ok(Json(results)) +} + +async fn deepbook_pool_config_updated( + Query(params): Query>, + State(state): State>, +) -> Result>, DeepBookError> { + let end_time = params.end_time(); + let start_time = params + .start_time() + .unwrap_or_else(|| end_time - 24 * 60 * 60 * 1000); + let limit = params.limit(); + let pool_id_filter = params.get("pool_id").cloned().unwrap_or_default(); + + let results = state + .reader + .get_deepbook_pool_config_updated(start_time, end_time, limit, pool_id_filter) + .await?; + + Ok(Json(results)) +} + +async fn margin_managers_info( + State(state): State>, +) -> Result>>, DeepBookError> { + let results = state.reader.get_margin_managers_info().await?; + + let data: Vec> = results + .into_iter() + .map( + |( + margin_manager_id, + deepbook_pool_id, + base_asset_id, + base_asset_symbol, + quote_asset_id, + quote_asset_symbol, + base_margin_pool_id, + quote_margin_pool_id, + )| { + HashMap::from([ + ( + "margin_manager_id".to_string(), + Value::from(margin_manager_id), + ), + ( + "deepbook_pool_id".to_string(), + deepbook_pool_id.map_or(Value::Null, Value::from), + ), + ( + "base_asset_id".to_string(), + base_asset_id.map_or(Value::Null, Value::from), + ), + ( + "base_asset_symbol".to_string(), + base_asset_symbol.map_or(Value::Null, Value::from), + ), + ( + "quote_asset_id".to_string(), + quote_asset_id.map_or(Value::Null, Value::from), + ), + ( + "quote_asset_symbol".to_string(), + quote_asset_symbol.map_or(Value::Null, Value::from), + ), + ( + "base_margin_pool_id".to_string(), + base_margin_pool_id.map_or(Value::Null, Value::from), + ), + ( + "quote_margin_pool_id".to_string(), + quote_margin_pool_id.map_or(Value::Null, Value::from), + ), + ]) + }, + ) + .collect(); + + Ok(Json(data)) +} + +async fn margin_manager_states( + Query(params): Query>, + State(state): State>, +) -> Result>, DeepBookError> { + let max_risk_ratio = params + .get("max_risk_ratio") + .and_then(|v| v.parse::().ok()); + let deepbook_pool_id = params.get("deepbook_pool_id").cloned(); + + // Parse pool parameter (e.g., "SUI_USDC" -> base="SUI", quote="USDC") + let (base_asset_symbol, quote_asset_symbol) = params + .get("pool") + .map(|p| { + let parts: Vec<&str> = p.split('_').collect(); + if parts.len() == 2 { + (Some(parts[0].to_string()), Some(parts[1].to_string())) + } else { + (None, None) + } + }) + .unwrap_or((None, None)); + + let states = state + .reader + .get_margin_manager_states( + max_risk_ratio, + deepbook_pool_id, + base_asset_symbol, + quote_asset_symbol, + ) + .await?; + + Ok(Json(states)) +} + +#[derive(serde::Serialize)] +struct BalanceManagerDepositedAssets { + balance_manager_id: String, + assets: Vec, +} + +async fn deposited_assets( + Path(balance_manager_ids): Path, + State(state): State>, +) -> Result>, DeepBookError> { + let ids: Vec = balance_manager_ids + .split(',') + .map(|s| s.trim().to_string()) + .filter(|s| !s.is_empty()) + .collect(); + + if ids.is_empty() { + return Err(DeepBookError::bad_request( + "No balance manager IDs provided", + )); + } + + let results = state + .reader + .get_deposited_assets_by_balance_managers(&ids) + .await?; + + let mut assets_by_manager: HashMap> = HashMap::new(); + for (balance_manager_id, asset) in results { + assets_by_manager + .entry(balance_manager_id) + .or_default() + .push(asset); + } + + let response: Vec = ids + .into_iter() + .map(|id| { + let assets = assets_by_manager.remove(&id).unwrap_or_default(); + BalanceManagerDepositedAssets { + balance_manager_id: id, + assets, + } + }) + .collect(); + + Ok(Json(response)) +} + +async fn collateral_events( + Query(params): Query>, + State(state): State>, +) -> Result>, DeepBookError> { + let end_time = params.end_time(); + let start_time = params + .start_time() + .unwrap_or_else(|| end_time - 24 * 60 * 60 * 1000); + let limit = params.limit(); + let margin_manager_id_filter = params.get("margin_manager_id").cloned().unwrap_or_default(); + let event_type_filter = params.get("type").cloned().unwrap_or_default(); + let is_base_filter = params.get("is_base").and_then(|v| v.parse::().ok()); + + let results = state + .reader + .get_collateral_events( + start_time, + end_time, + limit, + margin_manager_id_filter, + event_type_filter, + is_base_filter, + ) + .await?; + + Ok(Json(results)) +} + +// === Points === +#[derive(Deserialize)] +struct GetPointsQuery { + addresses: Option, +} + +async fn get_points( + Query(params): Query, + State(state): State>, +) -> Result>, DeepBookError> { + let addresses = params + .addresses + .map(|s| { + s.split(',') + .map(|a| a.trim().to_string()) + .filter(|a| !a.is_empty()) + .collect::>() + }) + .filter(|v| !v.is_empty()); + + let Some(requested) = addresses else { + return Ok(Json(vec![])); + }; + + let results = state.reader.get_points(Some(&requested)).await?; + let results_map: std::collections::HashMap<_, _> = results.into_iter().collect(); + + let response = requested + .iter() + .map(|addr| { + serde_json::json!({ + "address": addr, + "total_points": results_map.get(addr).copied().unwrap_or(0) + }) + }) + .collect(); + + Ok(Json(response)) } diff --git a/docker/deepbook-indexer/Dockerfile b/docker/deepbook-indexer/Dockerfile new file mode 100644 index 000000000..61348222f --- /dev/null +++ b/docker/deepbook-indexer/Dockerfile @@ -0,0 +1,36 @@ +FROM rust:1.90.0 AS builder + +ARG PROFILE=release +ARG GIT_REVISION +ENV GIT_REVISION=$GIT_REVISION + +WORKDIR work + +COPY Cargo.lock Cargo.toml ./ +COPY crates/ ./crates/ +COPY docker/deepbook-indexer/entry.sh ./ + +RUN apt-get update && apt-get install -y build-essential libssl-dev pkg-config curl cmake clang ca-certificates +ENV PATH="/root/.cargo/bin:${PATH}" + +RUN cargo build --profile $PROFILE --bin deepbook-indexer --config net.git-fetch-with-cli=true + +FROM debian:trixie-slim AS runtime + +RUN apt-get update +RUN apt-get -y --no-install-recommends install wget \ + iputils-ping procps bind9-host bind9-dnsutils \ + curl iproute2 git ca-certificates libpq-dev \ + postgresql + +COPY --from=builder /work/target/release/deepbook-indexer /opt/mysten/bin/ +COPY --from=builder /work/entry.sh . +RUN ["chmod", "+x", "/opt/mysten/bin/deepbook-indexer"] +RUN ["chmod", "+x", "entry.sh"] + +ARG BUILD_DATE +ARG GIT_REVISION +LABEL build-date=$BUILD_DATE +LABEL git-revision=$GIT_REVISION + +CMD ["./entry.sh"] diff --git a/docker/deepbook-indexer/entry.sh b/docker/deepbook-indexer/entry.sh new file mode 100644 index 000000000..33a039ed6 --- /dev/null +++ b/docker/deepbook-indexer/entry.sh @@ -0,0 +1,12 @@ +#!/bin/bash + +export RUST_BACKTRACE=1 +export RUST_LOG=${RUST_LOG:-info} + +# Build command arguments +args=(--database-url "$DATABASE_URL" --env "$NETWORK" --db-connection-pool-size 250) +if [ -n "$FIRST_CHECKPOINT" ]; then + args+=(--first-checkpoint "$FIRST_CHECKPOINT") +fi + +exec /opt/mysten/bin/deepbook-indexer "${args[@]}" diff --git a/docker/deepbook-server/Dockerfile b/docker/deepbook-server/Dockerfile new file mode 100644 index 000000000..0cf0ea13c --- /dev/null +++ b/docker/deepbook-server/Dockerfile @@ -0,0 +1,36 @@ +FROM rust:1.90.0 AS builder + +ARG PROFILE=release +ARG GIT_REVISION +ENV GIT_REVISION=$GIT_REVISION + +WORKDIR work + +COPY Cargo.lock Cargo.toml ./ +COPY crates/ ./crates/ +COPY docker/deepbook-server/entry.sh ./ + +RUN apt-get update && apt-get install -y build-essential libssl-dev pkg-config curl cmake clang ca-certificates +ENV PATH="/root/.cargo/bin:${PATH}" + +RUN cargo build --profile $PROFILE --bin deepbook-server --config net.git-fetch-with-cli=true + +FROM debian:trixie-slim AS runtime + +RUN apt-get update +RUN apt-get -y --no-install-recommends install wget \ + iputils-ping procps bind9-host bind9-dnsutils \ + curl iproute2 git ca-certificates libpq-dev \ + postgresql + +COPY --from=builder /work/target/release/deepbook-server /opt/mysten/bin/ +COPY --from=builder /work/entry.sh . +RUN ["chmod", "+x", "/opt/mysten/bin/deepbook-server"] +RUN ["chmod", "+x", "entry.sh"] + +ARG BUILD_DATE +ARG GIT_REVISION +LABEL build-date=$BUILD_DATE +LABEL git-revision=$GIT_REVISION + +CMD ["./entry.sh"] diff --git a/docker/deepbook-server/entry.sh b/docker/deepbook-server/entry.sh new file mode 100644 index 000000000..6c4a6fa02 --- /dev/null +++ b/docker/deepbook-server/entry.sh @@ -0,0 +1,12 @@ +#!/bin/bash + +export RUST_BACKTRACE=1 +export RUST_LOG=debug + +/opt/mysten/bin/deepbook-server \ + --database-url "$DATABASE_URL" \ + --rpc-url "$RPC_URL" \ + --deepbook-package-id "$DEEPBOOK_PACKAGE_ID" \ + --deep-token-package-id "$DEEP_TOKEN_PACKAGE_ID" \ + --deep-treasury-id "$DEEP_TREASURY_ID" \ + --margin-package-id "$MARGIN_PACKAGE_ID" diff --git a/docs/interest-rate-chart.html b/docs/interest-rate-chart.html new file mode 100644 index 000000000..242a1622b --- /dev/null +++ b/docs/interest-rate-chart.html @@ -0,0 +1,540 @@ + + + + + + DeepBook Margin - Interest Rate Curves + + + + +

DeepBook Margin Interest Rate Curves

+

Utilization vs Borrow APR for each margin pool

+ +
+
+
+
+ USDC + Stablecoin +
+
+
+ +
+
+
Base Rate0%
+
Base Slope15%
+
Optimal Util.80%
+
Excess Slope500%
+
+
+ +
+
+
+ SUI + Native +
+
+
+ +
+
+
Base Rate3%
+
Base Slope20%
+
Optimal Util.80%
+
Excess Slope500%
+
+
+ +
+
+
+ DEEP + Protocol +
+
+
+ +
+
+
Base Rate5%
+
Base Slope25%
+
Optimal Util.80%
+
Excess Slope500%
+
+
+ +
+
+
+ WAL + Walrus +
+
+
+ +
+
+
Base Rate5%
+
Base Slope25%
+
Optimal Util.80%
+
Excess Slope500%
+
+
+
+ +

Supply APR Curves

+

Utilization vs Supply APR (Borrow APR × Utilization × 0.8)

+ +
+
+
+
+ USDC + Supply +
+
+
+ +
+
+ +
+
+
+ SUI + Supply +
+
+
+ +
+
+ +
+
+
+ DEEP + Supply +
+
+
+ +
+
+ +
+
+
+ WAL + Supply +
+
+
+ +
+
+
+ +
+
Interest Rate Formula
+
+ if utilization < optimalUtilization:
+     rate = baseRate + utilization × baseSlope

+ else:
+     rate = baseRate + optimalUtilization × baseSlope + (utilization - optimalUtilization) × excessSlope +
+
+ + + + diff --git a/packages/.prettierrc b/packages/.prettierrc index 22fcc4fa1..886f157ab 100644 --- a/packages/.prettierrc +++ b/packages/.prettierrc @@ -1,6 +1,7 @@ { - "printWidth": 100, - "tabWidth": 4, - "useModuleLabel": true, - "autoGroupImports": "package" + "printWidth": 100, + "tabWidth": 4, + "useModuleLabel": true, + "autoGroupImports": "package", + "wrapComments": false } diff --git a/packages/dbtc/.gitignore b/packages/dbtc/.gitignore new file mode 100644 index 000000000..813de75b4 --- /dev/null +++ b/packages/dbtc/.gitignore @@ -0,0 +1,4 @@ +build/* +traces/* +.trace +.coverage* diff --git a/packages/dbtc/Move.lock b/packages/dbtc/Move.lock new file mode 100644 index 000000000..dfefc096a --- /dev/null +++ b/packages/dbtc/Move.lock @@ -0,0 +1,56 @@ +# @generated by Move, please check-in and do not edit manually. + +[move] +version = 3 +manifest_digest = "7CC00C0BD05D363BFA654C4C9C253A1E0A6FD91CAA8BBB2F4E50576A0E764C60" +deps_digest = "F9B494B64F0615AED0E98FC12A85B85ECD2BC5185C22D30E7F67786BB52E507C" +dependencies = [ + { id = "Bridge", name = "Bridge" }, + { id = "MoveStdlib", name = "MoveStdlib" }, + { id = "Sui", name = "Sui" }, + { id = "SuiSystem", name = "SuiSystem" }, +] + +[[move.package]] +id = "Bridge" +source = { git = "https://github.com/MystenLabs/sui.git", rev = "c1f1ae650fb9f9248b39a569400b4420820868db", subdir = "crates/sui-framework/packages/bridge" } + +dependencies = [ + { id = "MoveStdlib", name = "MoveStdlib" }, + { id = "Sui", name = "Sui" }, + { id = "SuiSystem", name = "SuiSystem" }, +] + +[[move.package]] +id = "MoveStdlib" +source = { git = "https://github.com/MystenLabs/sui.git", rev = "c1f1ae650fb9f9248b39a569400b4420820868db", subdir = "crates/sui-framework/packages/move-stdlib" } + +[[move.package]] +id = "Sui" +source = { git = "https://github.com/MystenLabs/sui.git", rev = "c1f1ae650fb9f9248b39a569400b4420820868db", subdir = "crates/sui-framework/packages/sui-framework" } + +dependencies = [ + { id = "MoveStdlib", name = "MoveStdlib" }, +] + +[[move.package]] +id = "SuiSystem" +source = { git = "https://github.com/MystenLabs/sui.git", rev = "c1f1ae650fb9f9248b39a569400b4420820868db", subdir = "crates/sui-framework/packages/sui-system" } + +dependencies = [ + { id = "MoveStdlib", name = "MoveStdlib" }, + { id = "Sui", name = "Sui" }, +] + +[move.toolchain-version] +compiler-version = "1.61.2" +edition = "2024.beta" +flavor = "sui" + +[env] + +[env.testnet] +chain-id = "4c78adac" +original-published-id = "0x6502dae813dbe5e42643c119a6450a518481f03063febc7e20238e43b6ea9e86" +latest-published-id = "0x6502dae813dbe5e42643c119a6450a518481f03063febc7e20238e43b6ea9e86" +published-version = "1" diff --git a/packages/dbtc/Move.toml b/packages/dbtc/Move.toml new file mode 100644 index 000000000..fc8034c56 --- /dev/null +++ b/packages/dbtc/Move.toml @@ -0,0 +1,8 @@ +[package] +name = "dbtc" +edition = "2024.beta" + +[dependencies] + +[addresses] +dbtc = "0x0" diff --git a/packages/dbtc/sources/dbtc.move b/packages/dbtc/sources/dbtc.move new file mode 100644 index 000000000..bb13a0dea --- /dev/null +++ b/packages/dbtc/sources/dbtc.move @@ -0,0 +1,23 @@ +module dbtc::dbtc; + +use sui::coin_registry; + +public struct DBTC has drop {} + +/// This is a token for testing purposes, used only on testnet. +fun init(witness: DBTC, ctx: &mut TxContext) { + let (builder, treasury_cap) = coin_registry::new_currency_with_otw( + witness, + 8, // Decimals + b"DBTC".to_string(), + b"DeepBook BTC".to_string(), + b"DeepBook Test BTC".to_string(), + b"https://upload.wikimedia.org/wikipedia/commons/4/46/Bitcoin.svg".to_string(), + ctx, + ); + + let metadata_cap = builder.finalize(ctx); + + transfer::public_transfer(treasury_cap, ctx.sender()); + transfer::public_transfer(metadata_cap, ctx.sender()); +} diff --git a/packages/deepbook/Move.lock b/packages/deepbook/Move.lock index 6430f51b4..e9a369a8b 100644 --- a/packages/deepbook/Move.lock +++ b/packages/deepbook/Move.lock @@ -1,49 +1,53 @@ -# @generated by Move, please check-in and do not edit manually. +# Generated by move; do not edit +# This file should be checked in. [move] -version = 3 -manifest_digest = "77450CAF5CB6CF95D38E61C7F44F5C35EF483F5B232C93A8039C28ECA9AC8BA1" -deps_digest = "3C4103934B1E040BB6B23F1D610B4EF9F2F1166A50A104EADCF77467C004C600" -dependencies = [ - { id = "Sui", name = "Sui" }, - { id = "token", name = "token" }, -] - -[[move.package]] -id = "MoveStdlib" -source = { git = "https://github.com/MystenLabs/sui.git", rev = "framework/mainnet", subdir = "crates/sui-framework/packages/move-stdlib" } - -[[move.package]] -id = "Sui" -source = { git = "https://github.com/MystenLabs/sui.git", rev = "framework/mainnet", subdir = "crates/sui-framework/packages/sui-framework" } - -dependencies = [ - { id = "MoveStdlib", name = "MoveStdlib" }, -] - -[[move.package]] -id = "token" -source = { local = "../token" } - -dependencies = [ - { id = "Sui", name = "Sui" }, -] - -[move.toolchain-version] -compiler-version = "1.43.1" -edition = "2024.beta" -flavor = "sui" - -[env] - -[env.testnet] -chain-id = "4c78adac" -original-published-id = "0xfb28c4cbc6865bd1c897d26aecbe1f8792d1509a20ffec692c800660cbec6982" -latest-published-id = "0x984757fc7c0e6dd5f15c2c66e881dd6e5aca98b725f3dbd83c445e057ebb790a" -published-version = "2" - -[env.mainnet] -chain-id = "35834a8a" -original-published-id = "0x2c8d603bc51326b8c13cef9dd07031a408a48dddb541963357661df5d3204809" -latest-published-id = "0xcaf6ba059d539a97646d47f0b9ddf843e138d215e2a12ca1f4585d386f7aec3a" -published-version = "2" +version = 4 + +[pinned.mainnet.MoveStdlib] +source = { git = "https://github.com/MystenLabs/sui.git", subdir = "crates/sui-framework/packages/move-stdlib", rev = "22f9fc9781732d651e18384c9a8eb1dabddf73a6" } +use_environment = "mainnet" +manifest_digest = "C4FE4C91DE74CBF223B2E380AE40F592177D21870DC2D7EB6227D2D694E05363" +deps = {} + +[pinned.mainnet.Sui] +source = { git = "https://github.com/MystenLabs/sui.git", subdir = "crates/sui-framework/packages/sui-framework", rev = "22f9fc9781732d651e18384c9a8eb1dabddf73a6" } +use_environment = "mainnet" +manifest_digest = "CD547CB1ACCE0880C835DAED2D8FFCB91D56C833AE5240D3AA5B918398263195" +deps = { MoveStdlib = "MoveStdlib" } + +[pinned.mainnet.deepbook] +source = { root = true } +use_environment = "mainnet" +manifest_digest = "F4948AC65D214ECC0561B7E94987B2AF2D7BF78658F6AE5CA5D0E1DA68873872" +deps = { std = "MoveStdlib", sui = "Sui", token = "token" } + +[pinned.mainnet.token] +source = { git = "https://github.com/MystenLabs/deepbookv3.git", subdir = "packages/token", rev = "5e82e2dd1ea7d47957855ddc66f835585d6fe091" } +use_environment = "mainnet" +manifest_digest = "E41BBD67BE8940D26C79D78B028477EF5B33BA217A1282C78ACB344CF8A5ECF6" +deps = { std = "MoveStdlib", sui = "Sui" } + +[pinned.testnet.MoveStdlib] +source = { git = "https://github.com/MystenLabs/sui.git", subdir = "crates/sui-framework/packages/move-stdlib", rev = "22f9fc9781732d651e18384c9a8eb1dabddf73a6" } +use_environment = "testnet" +manifest_digest = "C4FE4C91DE74CBF223B2E380AE40F592177D21870DC2D7EB6227D2D694E05363" +deps = {} + +[pinned.testnet.Sui] +source = { git = "https://github.com/MystenLabs/sui.git", subdir = "crates/sui-framework/packages/sui-framework", rev = "22f9fc9781732d651e18384c9a8eb1dabddf73a6" } +use_environment = "testnet" +manifest_digest = "7AFB66695545775FBFBB2D3078ADFD084244D5002392E837FDE21D9EA1C6D01C" +deps = { MoveStdlib = "MoveStdlib" } + +[pinned.testnet.deepbook] +source = { root = true } +use_environment = "testnet" +manifest_digest = "3101923B9428545A4F52FFAD1C4F959F9BFFF84CD09CE4BCC1CB831286999B5A" +deps = { std = "MoveStdlib", sui = "Sui", token = "token" } + +[pinned.testnet.token] +source = { git = "https://github.com/MystenLabs/deepbookv3.git", subdir = "packages/token", rev = "5e82e2dd1ea7d47957855ddc66f835585d6fe091" } +use_environment = "testnet" +manifest_digest = "5745706258F61D6CE210904B3E6AE87A73CE9D31A6F93BE4718C442529332A87" +deps = { std = "MoveStdlib", sui = "Sui" } diff --git a/packages/deepbook/Move.toml b/packages/deepbook/Move.toml index c73337104..44fd2228f 100644 --- a/packages/deepbook/Move.toml +++ b/packages/deepbook/Move.toml @@ -4,8 +4,7 @@ edition = "2024.beta" version = "0.0.1" [dependencies] -Sui = { git = "https://github.com/MystenLabs/sui.git", subdir = "crates/sui-framework/packages/sui-framework", rev = "framework/mainnet" } -token = { local = "../token"} +token = { git = "https://github.com/MystenLabs/deepbookv3.git", subdir = "packages/token", rev = "main"} [addresses] deepbook = "0x0" diff --git a/packages/deepbook/Published.toml b/packages/deepbook/Published.toml new file mode 100644 index 000000000..1fde0920a --- /dev/null +++ b/packages/deepbook/Published.toml @@ -0,0 +1,15 @@ +# Generated by Move +# This file contains metadata about published versions of this package in different environments +# This file SHOULD be committed to source control + +[published.mainnet] +chain-id = "35834a8a" +published-at = "0x337f4f4f6567fcd778d5454f27c16c70e2f274cc6377ea6249ddf491482ef497" +original-id = "0x2c8d603bc51326b8c13cef9dd07031a408a48dddb541963357661df5d3204809" +version = 6 + +[published.testnet] +chain-id = "4c78adac" +published-at = "0x22be4cade64bf2d02412c7e8d0e8beea2f78828b948118d46735315409371a3c" +original-id = "0xfb28c4cbc6865bd1c897d26aecbe1f8792d1509a20ffec692c800660cbec6982" +version = 17 diff --git a/packages/deepbook/sources/balance_manager.move b/packages/deepbook/sources/balance_manager.move index b2033bc4b..553271452 100644 --- a/packages/deepbook/sources/balance_manager.move +++ b/packages/deepbook/sources/balance_manager.move @@ -8,8 +8,22 @@ /// a `TradeProof`. Generally, a high frequency trading engine will trade as the default owner. module deepbook::balance_manager; +use deepbook::registry::Registry; use std::type_name::{Self, TypeName}; -use sui::{bag::{Self, Bag}, balance::{Self, Balance}, coin::Coin, event, vec_set::{Self, VecSet}}; +use sui::{ + bag::{Self, Bag}, + balance::{Self, Balance}, + coin::Coin, + dynamic_field as df, + event, + object::id_from_address, + vec_set::{Self, VecSet} +}; + +use fun df::borrow as UID.borrow; +use fun df::exists_ as UID.exists_; +use fun df::remove_if_exists as UID.remove_if_exists; +use fun df::add as UID.add; // === Errors === const EInvalidOwner: u64 = 0; @@ -18,6 +32,7 @@ const EInvalidProof: u64 = 2; const EBalanceManagerBalanceTooLow: u64 = 3; const EMaxCapsReached: u64 = 4; const ECapNotInList: u64 = 5; +const EInvalidReferralOwner: u64 = 6; // === Constants === const MAX_TRADE_CAPS: u64 = 1000; @@ -48,6 +63,9 @@ public struct BalanceEvent has copy, drop { /// Balance identifier. public struct BalanceKey has copy, drop, store {} +/// Referral identifier. +public struct ReferralKey(ID) has copy, drop, store; + /// Owners of a `TradeCap` need to get a `TradeProof` to trade across pools in a single PTB (drops after). public struct TradeCap has key, store { id: UID, @@ -66,6 +84,28 @@ public struct WithdrawCap has key, store { balance_manager_id: ID, } +#[deprecated(note = b"This struct is deprecated, replaced by `DeepBookPoolReferral`.")] +public struct DeepBookReferral has key, store { + id: UID, + owner: address, +} + +public struct DeepBookPoolReferral has key, store { + id: UID, + owner: address, + pool_id: ID, +} + +public struct DeepBookReferralCreatedEvent has copy, drop { + referral_id: ID, + owner: address, +} + +public struct DeepBookReferralSetEvent has copy, drop { + referral_id: ID, + balance_manager_id: ID, +} + /// BalanceManager owner and `TradeCap` owners can generate a `TradeProof`. /// `TradeProof` is used to validate the balance_manager when trading on DeepBook. public struct TradeProof has drop { @@ -89,8 +129,13 @@ public fun new(ctx: &mut TxContext): BalanceManager { } } +#[deprecated(note = b"This function is deprecated, use `new_with_custom_owner` instead.")] +public fun new_with_owner(_ctx: &mut TxContext, _owner: address): BalanceManager { + abort 1337 +} + /// Create a new balance manager with an owner. -public fun new_with_owner(ctx: &mut TxContext, owner: address): BalanceManager { +public fun new_with_custom_owner(owner: address, ctx: &mut TxContext): BalanceManager { let id = object::new(ctx); event::emit(BalanceManagerEvent { balance_manager_id: id.to_inner(), @@ -105,6 +150,70 @@ public fun new_with_owner(ctx: &mut TxContext, owner: address): BalanceManager { } } +#[deprecated(note = b"This function is deprecated, use `new_with_custom_owner_caps` instead.")] +public fun new_with_custom_owner_and_caps( + _owner: address, + _ctx: &mut TxContext, +): (BalanceManager, DepositCap, WithdrawCap, TradeCap) { abort 1337 } + +public fun new_with_custom_owner_caps( + deepbook_registry: &Registry, + owner: address, + ctx: &mut TxContext, +): (BalanceManager, DepositCap, WithdrawCap, TradeCap) { + deepbook_registry.assert_app_is_authorized(); + let mut balance_manager = new_with_custom_owner(owner, ctx); + + let deposit_cap = mint_deposit_cap_internal(&mut balance_manager, ctx); + let withdraw_cap = mint_withdraw_cap_internal(&mut balance_manager, ctx); + let trade_cap = mint_trade_cap_internal(&mut balance_manager, ctx); + + (balance_manager, deposit_cap, withdraw_cap, trade_cap) +} + +#[deprecated(note = b"This function is deprecated, use `set_balance_manager_referral` instead.")] +public fun set_referral( + _balance_manager: &mut BalanceManager, + _referral: &DeepBookReferral, + _trade_cap: &TradeCap, +) { abort } + +/// Set the referral for the balance manager. +public fun set_balance_manager_referral( + balance_manager: &mut BalanceManager, + referral: &DeepBookPoolReferral, + trade_cap: &TradeCap, +) { + balance_manager.validate_trader(trade_cap); + let _: Option = balance_manager.id.remove_if_exists(ReferralKey(referral.pool_id)); + balance_manager.id.add(ReferralKey(referral.pool_id), referral.id.to_inner()); + + event::emit(DeepBookReferralSetEvent { + referral_id: referral.id.to_inner(), + balance_manager_id: balance_manager.id.to_inner(), + }); +} + +#[deprecated(note = b"This function is deprecated, use `unset_balance_manager_referral` instead.")] +public fun unset_referral(_balance_manager: &mut BalanceManager, _trade_cap: &TradeCap) { + abort +} + +/// Unset the referral for the balance manager. +public fun unset_balance_manager_referral( + balance_manager: &mut BalanceManager, + pool_id: ID, + trade_cap: &TradeCap, +) { + balance_manager.validate_trader(trade_cap); + let _: Option = balance_manager.id.remove_if_exists(ReferralKey(pool_id)); + + event::emit(DeepBookReferralSetEvent { + referral_id: id_from_address(@0x0), + balance_manager_id: balance_manager.id.to_inner(), + }); +} + /// Returns the balance of a Coin in a balance manager. public fun balance(balance_manager: &BalanceManager): u64 { let key = BalanceKey {}; @@ -119,29 +228,13 @@ public fun balance(balance_manager: &BalanceManager): u64 { /// Mint a `TradeCap`, only owner can mint a `TradeCap`. public fun mint_trade_cap(balance_manager: &mut BalanceManager, ctx: &mut TxContext): TradeCap { balance_manager.validate_owner(ctx); - assert!(balance_manager.allow_listed.size() < MAX_TRADE_CAPS, EMaxCapsReached); - - let id = object::new(ctx); - balance_manager.allow_listed.insert(id.to_inner()); - - TradeCap { - id, - balance_manager_id: object::id(balance_manager), - } + balance_manager.mint_trade_cap_internal(ctx) } /// Mint a `DepositCap`, only owner can mint. public fun mint_deposit_cap(balance_manager: &mut BalanceManager, ctx: &mut TxContext): DepositCap { balance_manager.validate_owner(ctx); - assert!(balance_manager.allow_listed.size() < MAX_TRADE_CAPS, EMaxCapsReached); - - let id = object::new(ctx); - balance_manager.allow_listed.insert(id.to_inner()); - - DepositCap { - id, - balance_manager_id: object::id(balance_manager), - } + balance_manager.mint_deposit_cap_internal(ctx) } /// Mint a `WithdrawCap`, only owner can mint. @@ -150,15 +243,7 @@ public fun mint_withdraw_cap( ctx: &mut TxContext, ): WithdrawCap { balance_manager.validate_owner(ctx); - assert!(balance_manager.allow_listed.size() < MAX_TRADE_CAPS, EMaxCapsReached); - - let id = object::new(ctx); - balance_manager.allow_listed.insert(id.to_inner()); - - WithdrawCap { - id, - balance_manager_id: object::id(balance_manager), - } + balance_manager.mint_withdraw_cap_internal(ctx) } /// Revoke a `TradeCap`. Only the owner can revoke a `TradeCap`. @@ -206,7 +291,7 @@ public fun generate_proof_as_trader( /// Deposit funds to a balance manager. Only owner can call this directly. public fun deposit(balance_manager: &mut BalanceManager, coin: Coin, ctx: &mut TxContext) { balance_manager.emit_balance_event( - type_name::get(), + type_name::with_defining_ids(), coin.value(), true, ); @@ -223,7 +308,7 @@ public fun deposit_with_cap( ctx: &TxContext, ) { balance_manager.emit_balance_event( - type_name::get(), + type_name::with_defining_ids(), coin.value(), true, ); @@ -245,7 +330,7 @@ public fun withdraw_with_cap( ); let coin = balance_manager.withdraw_with_proof(&proof, withdraw_amount, false).into_coin(ctx); balance_manager.emit_balance_event( - type_name::get(), + type_name::with_defining_ids(), coin.value(), false, ); @@ -264,7 +349,7 @@ public fun withdraw( let proof = generate_proof_as_owner(balance_manager, ctx); let coin = balance_manager.withdraw_with_proof(&proof, withdraw_amount, false).into_coin(ctx); balance_manager.emit_balance_event( - type_name::get(), + type_name::with_defining_ids(), coin.value(), false, ); @@ -276,7 +361,7 @@ public fun withdraw_all(balance_manager: &mut BalanceManager, ctx: &mut TxCon let proof = generate_proof_as_owner(balance_manager, ctx); let coin = balance_manager.withdraw_with_proof(&proof, 0, true).into_coin(ctx); balance_manager.emit_balance_event( - type_name::get(), + type_name::with_defining_ids(), coin.value(), false, ); @@ -284,6 +369,41 @@ public fun withdraw_all(balance_manager: &mut BalanceManager, ctx: &mut TxCon coin } +#[deprecated(note = b"This function is deprecated, use `register_balance_manager` instead.")] +public fun register_manager(_balance_manager: &BalanceManager, _registry: &mut Registry) { + abort 1337 +} + +public fun register_balance_manager( + balance_manager: &BalanceManager, + registry: &mut Registry, + ctx: &mut TxContext, +) { + balance_manager.validate_owner(ctx); + let owner = balance_manager.owner(); + let manager_id = balance_manager.id(); + registry.add_balance_manager(owner, manager_id); +} + +#[deprecated(note = b"This function is deprecated, use `get_balance_manager_referral_id` instead.")] +public fun get_referral_id(_balance_manager: &BalanceManager): Option { + abort +} + +/// Get the referral id from the balance manager. +public fun get_balance_manager_referral_id( + balance_manager: &BalanceManager, + pool_id: ID, +): Option { + let ref_key = ReferralKey(pool_id); + if (!balance_manager.id.exists_(ref_key)) { + return option::none() + }; + let referral_id: &ID = balance_manager.id.borrow(ref_key); + + option::some(*referral_id) +} + public fun validate_proof(balance_manager: &BalanceManager, proof: &TradeProof) { assert!(object::id(balance_manager) == proof.balance_manager_id, EInvalidProof); } @@ -298,7 +418,44 @@ public fun id(balance_manager: &BalanceManager): ID { balance_manager.id.to_inner() } +#[deprecated(note = b"This function is deprecated, use `balance_manager_referral_owner` instead.")] +public fun referral_owner(_referral: &DeepBookReferral): address { + abort +} + +public fun balance_manager_referral_owner(referral: &DeepBookPoolReferral): address { + referral.owner +} + +public fun balance_manager_referral_pool_id(referral: &DeepBookPoolReferral): ID { + referral.pool_id +} + // === Public-Package Functions === +/// Mint a `DeepBookReferral` and share it. +public(package) fun mint_referral(pool_id: ID, ctx: &mut TxContext): ID { + let id = object::new(ctx); + let referral_id = id.to_inner(); + let referral = DeepBookPoolReferral { + id, + owner: ctx.sender(), + pool_id, + }; + + event::emit(DeepBookReferralCreatedEvent { + referral_id, + owner: ctx.sender(), + }); + + transfer::share_object(referral); + + referral_id +} + +public(package) fun assert_referral_owner(referral: &DeepBookPoolReferral, ctx: &TxContext) { + assert!(ctx.sender() == referral.owner, EInvalidReferralOwner); +} + /// Deposit funds to a balance_manager. Pool will call this to deposit funds. public(package) fun deposit_with_proof( balance_manager: &mut BalanceManager, @@ -317,6 +474,22 @@ public(package) fun deposit_with_proof( } } +/// Deposit funds to a balance_manager. Pool will call this to deposit funds. +/// This function is used by withdraw_settled_amounts_permissionless to deposit funds. +public(package) fun deposit_permissionless( + balance_manager: &mut BalanceManager, + to_deposit: Balance, +) { + let key = BalanceKey {}; + + if (balance_manager.balances.contains(key)) { + let balance: &mut Balance = &mut balance_manager.balances[key]; + balance.join(to_deposit); + } else { + balance_manager.balances.add(key, to_deposit); + } +} + /// Generate a `TradeProof` by a `DepositCap` owner. public(package) fun generate_proof_as_depositor( balance_manager: &BalanceManager, @@ -356,14 +529,12 @@ public(package) fun withdraw_with_proof( let key = BalanceKey {}; let key_exists = balance_manager.balances.contains(key); + if (!key_exists) { + balance_manager.balances.add(key, balance::zero()); + }; if (withdraw_all) { - if (key_exists) { - balance_manager.balances.remove(key) - } else { - balance::zero() - } + balance_manager.balances.remove(key) } else { - assert!(key_exists, EBalanceManagerBalanceTooLow); let acc_balance: &mut Balance = &mut balance_manager.balances[key]; let acc_value = acc_balance.value(); assert!(acc_value >= withdraw_amount, EBalanceManagerBalanceTooLow); @@ -408,6 +579,48 @@ public(package) fun emit_balance_event( } // === Private Functions === +fun mint_trade_cap_internal(balance_manager: &mut BalanceManager, ctx: &mut TxContext): TradeCap { + assert!(balance_manager.allow_listed.length() < MAX_TRADE_CAPS, EMaxCapsReached); + + let id = object::new(ctx); + balance_manager.allow_listed.insert(id.to_inner()); + + TradeCap { + id, + balance_manager_id: object::id(balance_manager), + } +} + +fun mint_deposit_cap_internal( + balance_manager: &mut BalanceManager, + ctx: &mut TxContext, +): DepositCap { + assert!(balance_manager.allow_listed.length() < MAX_TRADE_CAPS, EMaxCapsReached); + + let id = object::new(ctx); + balance_manager.allow_listed.insert(id.to_inner()); + + DepositCap { + id, + balance_manager_id: object::id(balance_manager), + } +} + +fun mint_withdraw_cap_internal( + balance_manager: &mut BalanceManager, + ctx: &mut TxContext, +): WithdrawCap { + assert!(balance_manager.allow_listed.length() < MAX_TRADE_CAPS, EMaxCapsReached); + + let id = object::new(ctx); + balance_manager.allow_listed.insert(id.to_inner()); + + WithdrawCap { + id, + balance_manager_id: object::id(balance_manager), + } +} + fun validate_owner(balance_manager: &BalanceManager, ctx: &TxContext) { assert!(ctx.sender() == balance_manager.owner(), EInvalidOwner); } diff --git a/packages/deepbook/sources/book/book.move b/packages/deepbook/sources/book/book.move index 2b2e2bcb3..31c065a49 100644 --- a/packages/deepbook/sources/book/book.move +++ b/packages/deepbook/sources/book/book.move @@ -119,12 +119,23 @@ public(package) fun get_quantity_out( ): (u64, u64, u64) { assert!((base_quantity > 0) != (quote_quantity > 0), EInvalidAmountIn); let is_bid = quote_quantity > 0; - let mut quantity_out = 0; - let mut quantity_in_left = if (is_bid) quote_quantity else base_quantity; let input_fee_rate = math::mul( constants::fee_penalty_multiplier(), taker_fee, ); + if (base_quantity > 0) { + let trading_base_quantity = if (pay_with_deep) { + base_quantity + } else { + math::div(base_quantity, constants::float_scaling() + input_fee_rate) + }; + if (trading_base_quantity < self.min_size) { + return (base_quantity, quote_quantity, 0) + } + }; + + let mut quantity_out = 0; + let mut quantity_in_left = if (is_bid) quote_quantity else base_quantity; let book_side = if (is_bid) &self.asks else &self.bids; let (mut ref, mut offset) = if (is_bid) book_side.min_slice() else book_side.max_slice(); @@ -205,12 +216,60 @@ public(package) fun get_quantity_out( }; if (is_bid) { - (quantity_out, quantity_in_left, deep_fee) + if (quantity_out < self.min_size) { + (base_quantity, quote_quantity, 0) + } else { + (quantity_out, quantity_in_left, deep_fee) + } } else { (quantity_in_left, quantity_out, deep_fee) } } +/// Given a target quote_quantity to receive from selling, calculate the minimum base_quantity needed. +/// This is the inverse of get_quantity_out for ask orders. +/// Returns (base_quantity_in, actual_quote_quantity_out, deep_quantity_required) +/// Returns (0, 0, 0) if insufficient liquidity or if result would be below min_size. +public(package) fun get_base_quantity_in( + self: &Book, + target_quote_quantity: u64, + taker_fee: u64, + deep_price: OrderDeepPrice, + pay_with_deep: bool, + current_timestamp: u64, +): (u64, u64, u64) { + self.get_quantity_in( + 0, // target_base_quantity = 0, we want quote + target_quote_quantity, + taker_fee, + deep_price, + pay_with_deep, + current_timestamp, + ) +} + +/// Given a target base_quantity to receive from buying, calculate the minimum quote_quantity needed. +/// This is the inverse of get_quantity_out for bid orders. +/// Returns (actual_base_quantity_out, quote_quantity_in, deep_quantity_required) +/// Returns (0, 0, 0) if insufficient liquidity or if result would be below min_size. +public(package) fun get_quote_quantity_in( + self: &Book, + target_base_quantity: u64, + taker_fee: u64, + deep_price: OrderDeepPrice, + pay_with_deep: bool, + current_timestamp: u64, +): (u64, u64, u64) { + self.get_quantity_in( + target_base_quantity, + 0, // target_quote_quantity = 0, we want base + taker_fee, + deep_price, + pay_with_deep, + current_timestamp, + ) +} + /// Cancels an order given order_id public(package) fun cancel_order(self: &mut Book, order_id: u128): Order { self.book_side_mut(order_id).remove(order_id) @@ -297,9 +356,9 @@ public(package) fun get_level2_range_and_ticks( // convert price_low and price_high to keys for searching let msb = if (is_bid) { - (0 as u128) + 0u128 } else { - (1 as u128) << 127 + 1u128 << 127 }; let key_low = ((price_low as u128) << 64) + msb; let key_high = ((price_high as u128) << 64) + (((1u128 << 64) - 1) as u128) + msb; @@ -357,6 +416,36 @@ public(package) fun get_level2_range_and_ticks( (price_vec, quantity_vec) } +public(package) fun check_limit_order_params( + self: &Book, + price: u64, + quantity: u64, + expire_timestamp: u64, + timestamp_ms: u64, +): bool { + if (expire_timestamp <= timestamp_ms) { + return false + }; + if (quantity < self.min_size || quantity % self.lot_size != 0) { + return false + }; + if ( + price % self.tick_size != 0 || price < constants::min_price() || price > constants::max_price() + ) { + return false + }; + + true +} + +public(package) fun check_market_order_params(self: &Book, quantity: u64): bool { + if (quantity < self.min_size || quantity % self.lot_size != 0) { + return false + }; + + true +} + public(package) fun get_order(self: &Book, order_id: u128): Order { let order = self.book_side(order_id).borrow(order_id); @@ -447,3 +536,145 @@ fun inject_limit_order(self: &mut Book, order_info: &OrderInfo) { self.asks.insert(order_info.order_id(), order); }; } + +/// Rounds up a quantity to the nearest lot_size multiple. +/// Returns the smallest multiple of lot_size that is >= quantity. +fun round_up_to_lot_size(quantity: u64, lot_size: u64): u64 { + let remainder = quantity % lot_size; + if (remainder == 0) quantity else quantity + lot_size - remainder +} + +/// If target_base_quantity > 0: Calculate quote needed to buy that base (bid order) +/// If target_quote_quantity > 0: Calculate base needed to get that quote (ask order) +/// Returns (base_result, quote_result, deep_quantity_required) +fun get_quantity_in( + self: &Book, + target_base_quantity: u64, + target_quote_quantity: u64, + taker_fee: u64, + deep_price: OrderDeepPrice, + pay_with_deep: bool, + current_timestamp: u64, +): (u64, u64, u64) { + assert!((target_base_quantity > 0) != (target_quote_quantity > 0), EInvalidAmountIn); + let is_bid = target_base_quantity > 0; + let input_fee_rate = math::mul( + constants::fee_penalty_multiplier(), + taker_fee, + ); + let lot_size = self.lot_size; + + let mut input_quantity = 0; // This will be quote for bid, base for ask (may include fees) + let mut output_accumulated = 0; // This will be base for bid, quote for ask + let mut traded_base = 0; // Raw base traded, used for min_size checks on asks + + // For bid: traverse asks (we're buying base with quote) + // For ask: traverse bids (we're selling base for quote) + let book_side = if (is_bid) &self.asks else &self.bids; + let (mut ref, mut offset) = if (is_bid) book_side.min_slice() else book_side.max_slice(); + let max_fills = constants::max_fills(); + let mut current_fills = 0; + let target = if (is_bid) target_base_quantity else target_quote_quantity; + + while (!ref.is_null() && output_accumulated < target && current_fills < max_fills) { + let order = slice_borrow(book_side.borrow_slice(ref), offset); + let cur_price = order.price(); + let cur_quantity = order.quantity() - order.filled_quantity(); + + if (current_timestamp <= order.expire_timestamp()) { + let output_needed = target - output_accumulated; + + if (is_bid) { + // Buying base with quote: find smallest lot-multiple >= output_needed, capped by cur_quantity + let target_lots = round_up_to_lot_size(output_needed, lot_size); + let matched_base = target_lots.min(cur_quantity); + + if (matched_base > 0) { + output_accumulated = output_accumulated + matched_base; + let matched_quote = math::mul(matched_base, cur_price); + + // Calculate quote needed including fees + if (pay_with_deep) { + input_quantity = input_quantity + matched_quote; + } else { + // Need extra quote to cover fees (fees taken from input) + let quote_with_fee = math::mul( + matched_quote, + constants::float_scaling() + input_fee_rate, + ); + input_quantity = input_quantity + quote_with_fee; + } + }; + + if (matched_base == 0) break; + } else { + // Selling base for quote: find smallest lot-multiple of base that yields >= output_needed quote + let base_for_quote = math::div_round_up(output_needed, cur_price); + let target_lots = round_up_to_lot_size(base_for_quote, lot_size); + let matched_base = target_lots.min(cur_quantity); + + if (matched_base > 0) { + traded_base = traded_base + matched_base; + + let matched_quote = math::mul(matched_base, cur_price); + output_accumulated = output_accumulated + matched_quote; + + // Calculate base needed including fees + if (pay_with_deep) { + input_quantity = input_quantity + matched_base; + } else { + // Need extra base to cover fees (fees taken from input) + let base_with_fee = math::mul( + matched_base, + constants::float_scaling() + input_fee_rate, + ); + input_quantity = input_quantity + base_with_fee; + } + }; + + if (matched_base == 0) break; + } + }; + + (ref, offset) = if (is_bid) book_side.next_slice(ref, offset) + else book_side.prev_slice(ref, offset); + current_fills = current_fills + 1; + }; + + // Calculate deep fee if paying with DEEP + let deep_fee = if (!pay_with_deep) { + 0 + } else { + let fee_quantity = if (is_bid) { + deep_price.fee_quantity( + output_accumulated, + input_quantity, + true, // is_bid + ) + } else { + deep_price.fee_quantity( + input_quantity, + output_accumulated, + false, // is_ask + ) + }; + math::mul(taker_fee, fee_quantity.deep()) + }; + + // Check if we accumulated enough and meets min_size + let sufficient = if (is_bid) { + output_accumulated >= target_base_quantity && output_accumulated >= self.min_size + } else { + output_accumulated >= target_quote_quantity && traded_base >= self.min_size + }; + + if (!sufficient) { + (0, 0, 0) // Couldn't satisfy the requirement + } else { + if (is_bid) { + (output_accumulated, input_quantity, deep_fee) + } else { + (input_quantity, output_accumulated, deep_fee) + } + } +} diff --git a/packages/deepbook/sources/book/order_info.move b/packages/deepbook/sources/book/order_info.move index 8598d9fca..b2824f83e 100644 --- a/packages/deepbook/sources/book/order_info.move +++ b/packages/deepbook/sources/book/order_info.move @@ -130,6 +130,17 @@ public struct OrderExpired has copy, drop, store { timestamp: u64, } +/// Emitted when an order is fully filled. +public struct OrderFullyFilled has copy, drop, store { + pool_id: ID, + order_id: u128, + client_order_id: u64, + balance_manager_id: ID, + original_quantity: u64, + is_bid: bool, + timestamp: u64, +} + // === Public-View Functions === public fun pool_id(self: &OrderInfo): ID { self.pool_id @@ -504,6 +515,16 @@ public(package) fun emit_orders_filled(self: &OrderInfo, timestamp: u64) { let num_fills = self.fills.length(); while (i < num_fills) { let fill = &self.fills[i]; + if (fill.completed()) { + self.emit_order_fully_filled( + fill.maker_order_id(), + fill.maker_client_order_id(), + fill.balance_manager_id(), + fill.original_maker_quantity(), + !fill.taker_is_bid(), + timestamp, + ); + }; if (!fill.expired()) { event::emit(self.order_filled_from_fill(fill, timestamp)); } else { @@ -537,6 +558,39 @@ public(package) fun emit_order_info(self: &OrderInfo) { event::emit(*self); } +public(package) fun emit_order_fully_filled_if_filled(self: &OrderInfo, timestamp: u64) { + if (self.status == constants::filled()) { + self.emit_order_fully_filled( + self.order_id, + self.client_order_id, + self.balance_manager_id, + self.original_quantity, + self.is_bid, + timestamp, + ); + } +} + +public(package) fun emit_order_fully_filled( + self: &OrderInfo, + order_id: u128, + client_order_id: u64, + balance_manager_id: ID, + original_quantity: u64, + is_bid: bool, + timestamp: u64, +) { + event::emit(OrderFullyFilled { + pool_id: self.pool_id, + order_id, + client_order_id, + balance_manager_id, + original_quantity, + is_bid, + timestamp, + }) +} + public(package) fun set_fill_limit_reached(self: &mut OrderInfo) { self.fill_limit_reached = true; } diff --git a/packages/deepbook/sources/helper/constants.move b/packages/deepbook/sources/helper/constants.move index 67273a750..677844437 100644 --- a/packages/deepbook/sources/helper/constants.move +++ b/packages/deepbook/sources/helper/constants.move @@ -3,7 +3,7 @@ module deepbook::constants; -const CURRENT_VERSION: u64 = 2; // Update version during upgrades +const CURRENT_VERSION: u64 = 6; // Update version during upgrades const POOL_CREATION_FEE: u64 = 500 * 1_000_000; // 500 DEEP const FLOAT_SCALING: u64 = 1_000_000_000; const FLOAT_SCALING_U128: u128 = 1_000_000_000; @@ -15,6 +15,17 @@ const DEFAULT_STAKE_REQUIRED: u64 = 100_000_000; // 100 DEEP const HALF: u64 = 500_000_000; const DEEP_UNIT: u64 = 1_000_000; const FEE_PENALTY_MULTIPLIER: u64 = 1_250_000_000; // 25% more than normal +const EWMA_DF_KEY: vector = b"ewma"; +const REFERRAL_MAX_MULTIPLIER: u64 = 2_000_000_000; // 2x multiplier +const REFERRAL_MULTIPLIER: u64 = 100_000_000; // 0.1x multiplier +const MAX_BALANCE_MANAGERS: u64 = 100; + +const DEFAULT_EWMA_ALPHA: u64 = 10_000_000; // 1% smoothing factor. at 3 TPS ~ one minute alpha +const MAX_EWMA_ALPHA: u64 = 100_000_000; // 10% smoothing factor. at 3 TPS ~ one minute alpha +const DEFAULT_Z_SCORE_THRESHOLD: u64 = 3_000_000_000; // 3 standard deviations +const MAX_Z_SCORE_THRESHOLD: u64 = 10_000_000_000; // 10 standard deviations +const DEFAULT_ADDITIONAL_TAKER_FEE: u64 = 1_000_000; // 10 bps +const MAX_ADDITIONAL_TAKER_FEE: u64 = 2_000_000; // 20 bps // Restrictions on limit orders. // No restriction on the order. @@ -221,6 +232,51 @@ public fun fee_penalty_multiplier(): u64 { FEE_PENALTY_MULTIPLIER } +public fun default_ewma_alpha(): u64 { + DEFAULT_EWMA_ALPHA +} + +public fun default_z_score_threshold(): u64 { + DEFAULT_Z_SCORE_THRESHOLD +} + +public fun default_additional_taker_fee(): u64 { + DEFAULT_ADDITIONAL_TAKER_FEE +} + +public fun max_ewma_alpha(): u64 { + MAX_EWMA_ALPHA +} + +public fun max_z_score_threshold(): u64 { + MAX_Z_SCORE_THRESHOLD +} + +public fun max_additional_taker_fee(): u64 { + MAX_ADDITIONAL_TAKER_FEE +} + +public fun ewma_df_key(): vector { + EWMA_DF_KEY +} + +public fun referral_max_multiplier(): u64 { + REFERRAL_MAX_MULTIPLIER +} + +public fun referral_multiplier(): u64 { + REFERRAL_MULTIPLIER +} + +public fun max_balance_managers(): u64 { + MAX_BALANCE_MANAGERS +} + +#[deprecated] +public fun referral_df_key(): vector { + abort +} + #[test_only] public fun maker_fee(): u64 { MAKER_FEE diff --git a/packages/deepbook/sources/helper/math.move b/packages/deepbook/sources/helper/math.move index 5f2bce4b3..6bbf2f1dd 100644 --- a/packages/deepbook/sources/helper/math.move +++ b/packages/deepbook/sources/helper/math.move @@ -13,13 +13,13 @@ const EInvalidPrecision: u64 = 0; /// Multiply two floating numbers. /// This function will round down the result. -public(package) fun mul(x: u64, y: u64): u64 { +public fun mul(x: u64, y: u64): u64 { let (_, result) = mul_internal(x, y); result } -public(package) fun mul_u128(x: u128, y: u128): u128 { +public fun mul_u128(x: u128, y: u128): u128 { let (_, result) = mul_internal_u128(x, y); result @@ -27,7 +27,7 @@ public(package) fun mul_u128(x: u128, y: u128): u128 { /// Multiply two floating numbers. /// This function will round up the result. -public(package) fun mul_round_up(x: u64, y: u64): u64 { +public fun mul_round_up(x: u64, y: u64): u64 { let (is_round_down, result) = mul_internal(x, y); result + is_round_down @@ -35,13 +35,13 @@ public(package) fun mul_round_up(x: u64, y: u64): u64 { /// Divide two floating numbers. /// This function will round down the result. -public(package) fun div(x: u64, y: u64): u64 { +public fun div(x: u64, y: u64): u64 { let (_, result) = div_internal(x, y); result } -public(package) fun div_u128(x: u128, y: u128): u128 { +public fun div_u128(x: u128, y: u128): u128 { let (_, result) = div_internal_u128(x, y); result @@ -49,14 +49,14 @@ public(package) fun div_u128(x: u128, y: u128): u128 { /// Divide two floating numbers. /// This function will round up the result. -public(package) fun div_round_up(x: u64, y: u64): u64 { +public fun div_round_up(x: u64, y: u64): u64 { let (is_round_down, result) = div_internal(x, y); result + is_round_down } -/// given a vector of u64, return the median -public(package) fun median(v: vector): u128 { +/// given a vector of u128, return the median +public fun median(v: vector): u128 { let n = v.length(); if (n == 0) { return 0 @@ -77,7 +77,7 @@ public(package) fun median(v: vector): u128 { /// original value /// is scaled by precision. The result will be in the same floating-point /// representation. -public(package) fun sqrt(x: u64, precision: u64): u64 { +public fun sqrt(x: u64, precision: u64): u64 { assert!(precision <= FLOAT_SCALING, EInvalidPrecision); let multiplier = (FLOAT_SCALING / precision) as u128; let scaled_x: u128 = (x as u128) * multiplier * FLOAT_SCALING_U128; @@ -86,7 +86,7 @@ public(package) fun sqrt(x: u64, precision: u64): u64 { (sqrt_scaled_x / multiplier) as u64 } -public(package) fun is_power_of_ten(n: u64): bool { +public fun is_power_of_ten(n: u64): bool { let mut num = n; if (num < 1) { diff --git a/packages/deepbook/sources/helper/utils.move b/packages/deepbook/sources/helper/utils.move index dec4023b2..687cc5b36 100644 --- a/packages/deepbook/sources/helper/utils.move +++ b/packages/deepbook/sources/helper/utils.move @@ -1,7 +1,7 @@ // Copyright (c) Mysten Labs, Inc. // SPDX-License-Identifier: Apache-2.0 -/// Deepbook utility functions. +/// DeepBook utility functions. module deepbook::utils; /// Pop elements from the back of `v` until its length equals `n`, diff --git a/packages/deepbook/sources/pool.move b/packages/deepbook/sources/pool.move index d2ed6188c..34cb63cea 100644 --- a/packages/deepbook/sources/pool.move +++ b/packages/deepbook/sources/pool.move @@ -6,11 +6,22 @@ module deepbook::pool; use deepbook::{ account::Account, - balance_manager::{Self, BalanceManager, TradeProof}, + balance_manager::{ + Self, + BalanceManager, + TradeProof, + DeepBookPoolReferral, + DeepBookReferral, + TradeCap, + DepositCap, + WithdrawCap + }, + balances, big_vector::BigVector, book::{Self, Book}, constants, deep_price::{Self, DeepPrice, OrderDeepPrice, emit_deep_price_added}, + ewma::{init_ewma_state, EWMAState}, math, order::Order, order_info::{Self, OrderInfo}, @@ -20,14 +31,21 @@ use deepbook::{ }; use std::type_name; use sui::{ + balance::{Self, Balance}, clock::Clock, coin::{Self, Coin}, + dynamic_field as df, event, vec_set::{Self, VecSet}, versioned::{Self, Versioned} }; use token::deep::{DEEP, ProtectedTreasury}; +use fun df::add as UID.add; +use fun df::borrow as UID.borrow; +use fun df::borrow_mut as UID.borrow_mut; +use fun df::exists_ as UID.exists_; + // === Errors === const EInvalidFee: u64 = 1; const ESameBaseAndQuote: u64 = 2; @@ -43,6 +61,11 @@ const EMinimumQuantityOutNotMet: u64 = 12; const EInvalidStake: u64 = 13; const EPoolNotRegistered: u64 = 14; const EPoolCannotBeBothWhitelistedAndStable: u64 = 15; +const EInvalidReferralMultiplier: u64 = 16; +const EInvalidEWMAAlpha: u64 = 17; +const EInvalidZScoreThreshold: u64 = 18; +const EInvalidAdditionalTakerFee: u64 = 19; +const EWrongPoolReferral: u64 = 20; // === Structs === public struct Pool has key { @@ -84,6 +107,39 @@ public struct DeepBurned has copy, drop, deep_burned: u64, } +public struct ReferralRewards has store { + multiplier: u64, + base: Balance, + quote: Balance, + deep: Balance, +} + +#[deprecated, allow(unused_field)] +public struct ReferralClaimedEvent has copy, drop, store { + referral_id: ID, + owner: address, + base_amount: u64, + quote_amount: u64, + deep_amount: u64, +} + +public struct ReferralClaimed has copy, drop, store { + pool_id: ID, + referral_id: ID, + owner: address, + base_amount: u64, + quote_amount: u64, + deep_amount: u64, +} + +public struct ReferralFeeEvent has copy, drop, store { + pool_id: ID, + referral_id: ID, + base_fee: u64, + quote_fee: u64, + deep_fee: u64, +} + // === Public-Mutative Functions * POOL CREATION * === /// Create a new pool. The pool is registered in the registry. /// Checks are performed to ensure the tick size, lot size, @@ -99,8 +155,8 @@ public fun create_permissionless_pool( ctx: &mut TxContext, ): ID { assert!(creation_fee.value() == constants::pool_creation_fee(), EInvalidFee); - let base_type = type_name::get(); - let quote_type = type_name::get(); + let base_type = type_name::with_defining_ids(); + let quote_type = type_name::with_defining_ids(); let whitelisted_pool = false; let stable_pool = registry.is_stablecoin(base_type) && registry.is_stablecoin(quote_type); @@ -209,6 +265,34 @@ public fun swap_exact_base_for_quote( ) } +/// Swap exact base for quote with a `balance_manager`. +/// Assumes fees are paid in DEEP. Assumes balance manager has enough DEEP for fees. +public fun swap_exact_base_for_quote_with_manager( + self: &mut Pool, + balance_manager: &mut BalanceManager, + trade_cap: &TradeCap, + deposit_cap: &DepositCap, + withdraw_cap: &WithdrawCap, + base_in: Coin, + min_quote_out: u64, + clock: &Clock, + ctx: &mut TxContext, +): (Coin, Coin) { + let quote_in = coin::zero(ctx); + + self.swap_exact_quantity_with_manager( + balance_manager, + trade_cap, + deposit_cap, + withdraw_cap, + base_in, + quote_in, + min_quote_out, + clock, + ctx, + ) +} + /// Swap exact quote quantity without needing a `balance_manager`. /// DEEP quantity can be overestimated. Returns three `Coin` objects: /// base, quote, and deep. Some quote quantity may be left over if the @@ -233,6 +317,34 @@ public fun swap_exact_quote_for_base( ) } +/// Swap exact quote for base with a `balance_manager`. +/// Assumes fees are paid in DEEP. Assumes balance manager has enough DEEP for fees. +public fun swap_exact_quote_for_base_with_manager( + self: &mut Pool, + balance_manager: &mut BalanceManager, + trade_cap: &TradeCap, + deposit_cap: &DepositCap, + withdraw_cap: &WithdrawCap, + quote_in: Coin, + min_base_out: u64, + clock: &Clock, + ctx: &mut TxContext, +): (Coin, Coin) { + let base_in = coin::zero(ctx); + + self.swap_exact_quantity_with_manager( + balance_manager, + trade_cap, + deposit_cap, + withdraw_cap, + base_in, + quote_in, + min_base_out, + clock, + ctx, + ) +} + /// Swap exact quantity without needing a balance_manager. public fun swap_exact_quantity( self: &mut Pool, @@ -307,6 +419,79 @@ public fun swap_exact_quantity( (base_out, quote_out, deep_out) } +/// Swap exact quantity with a `balance_manager`. +/// Assumes fees are paid in DEEP. Assumes balance manager has enough DEEP for fees. +public fun swap_exact_quantity_with_manager( + self: &mut Pool, + balance_manager: &mut BalanceManager, + trade_cap: &TradeCap, + deposit_cap: &DepositCap, + withdraw_cap: &WithdrawCap, + base_in: Coin, + quote_in: Coin, + min_out: u64, + clock: &Clock, + ctx: &mut TxContext, +): (Coin, Coin) { + let mut adjusted_base_quantity = base_in.value(); + let base_quantity = base_in.value(); + let quote_quantity = quote_in.value(); + assert!((adjusted_base_quantity > 0) != (quote_quantity > 0), EInvalidQuantityIn); + + let is_bid = quote_quantity > 0; + if (is_bid) { + (adjusted_base_quantity, _, _) = self.get_quantity_out(0, quote_quantity, clock) + } else { + adjusted_base_quantity = + adjusted_base_quantity - adjusted_base_quantity % self.load_inner().book.lot_size(); + }; + if (adjusted_base_quantity < self.load_inner().book.min_size()) { + return (base_in, quote_in) + }; + + balance_manager.deposit_with_cap(deposit_cap, base_in, ctx); + balance_manager.deposit_with_cap(deposit_cap, quote_in, ctx); + let trade_proof = balance_manager.generate_proof_as_trader(trade_cap, ctx); + let order_info = self.place_market_order( + balance_manager, + &trade_proof, + 0, + constants::self_matching_allowed(), + adjusted_base_quantity, + is_bid, + true, + clock, + ctx, + ); + + let (base_out_quantity, quote_out_quantity) = if (is_bid) { + let quote_left = quote_quantity - order_info.cumulative_quote_quantity(); + (order_info.executed_quantity(), quote_left) + } else { + let base_left = base_quantity - order_info.executed_quantity(); + (base_left, order_info.cumulative_quote_quantity()) + }; + + let base_out = if (base_out_quantity > 0) { + balance_manager.withdraw_with_cap(withdraw_cap, base_out_quantity, ctx) + } else { + coin::zero(ctx) + }; + let quote_out = if (quote_out_quantity > 0) { + balance_manager.withdraw_with_cap(withdraw_cap, quote_out_quantity, ctx) + } else { + coin::zero(ctx) + }; + + if (is_bid) { + assert!(base_out.value() >= min_out, EMinimumQuantityOutNotMet); + } else { + assert!(quote_out.value() >= min_out, EMinimumQuantityOutNotMet); + }; + + (base_out, quote_out) +} + /// Modifies an order given order_id and new_quantity. /// New quantity must be less than the original quantity and more /// than the filled quantity. Order must not have already expired. @@ -429,6 +614,16 @@ public fun withdraw_settled_amounts( self.vault.settle_balance_manager(settled, owed, balance_manager, trade_proof); } +/// Withdraw settled amounts permissionlessly to the `balance_manager`. +public fun withdraw_settled_amounts_permissionless( + self: &mut Pool, + balance_manager: &mut BalanceManager, +) { + let self = self.load_inner_mut(); + let (settled, owed) = self.state.withdraw_settled_amounts(balance_manager.id()); + self.vault.settle_balance_manager_permissionless(settled, owed, balance_manager); +} + // === Public-Mutative Functions * GOVERNANCE * === /// Stake DEEP tokens to the pool. The balance_manager must have enough DEEP /// tokens. @@ -590,11 +785,11 @@ public fun add_deep_price_point(); - let reference_quote_type = type_name::get(); - let target_base_type = type_name::get(); - let target_quote_type = type_name::get(); - let deep_type = type_name::get(); + let reference_base_type = type_name::with_defining_ids(); + let reference_quote_type = type_name::with_defining_ids(); + let target_base_type = type_name::with_defining_ids(); + let target_quote_type = type_name::with_defining_ids(); + let deep_type = type_name::with_defining_ids(); let timestamp = clock.timestamp_ms(); assert!( @@ -665,6 +860,109 @@ public fun burn_deep( amount_burned } +/// Mint a DeepBookReferral and set the additional bps for the referral. +public fun mint_referral( + self: &mut Pool, + multiplier: u64, + ctx: &mut TxContext, +): ID { + assert!(multiplier <= constants::referral_max_multiplier(), EInvalidReferralMultiplier); + assert!(multiplier % constants::referral_multiplier() == 0, EInvalidReferralMultiplier); + let _ = self.load_inner(); + let referral_id = balance_manager::mint_referral(self.id(), ctx); + self + .id + .add( + referral_id, + ReferralRewards { + multiplier, + base: balance::zero(), + quote: balance::zero(), + deep: balance::zero(), + }, + ); + + referral_id +} + +#[ + deprecated( + note = b"This function is deprecated, use `update_deepbook_referral_multiplier` instead.", + ), +] +public fun update_referral_multiplier( + _self: &mut Pool, + _referral: &DeepBookReferral, + _multiplier: u64, +) { + abort 1337 +} + +#[deprecated(note = b"This function is deprecated, use `update_pool_referral_multiplier` instead.")] +public fun update_deepbook_referral_multiplier( + _self: &mut Pool, + _referral: &DeepBookReferral, + _multiplier: u64, + _ctx: &TxContext, +) { + abort +} + +/// Update the multiplier for the referral. +public fun update_pool_referral_multiplier( + self: &mut Pool, + referral: &DeepBookPoolReferral, + multiplier: u64, + ctx: &TxContext, +) { + let _ = self.load_inner(); + referral.assert_referral_owner(ctx); + assert!(multiplier <= constants::referral_max_multiplier(), EInvalidReferralMultiplier); + assert!(multiplier % constants::referral_multiplier() == 0, EInvalidReferralMultiplier); + let referral_id = object::id(referral); + let referral_rewards: &mut ReferralRewards = self + .id + .borrow_mut(referral_id); + referral_rewards.multiplier = multiplier; +} + +#[deprecated(note = b"This function is deprecated, use `claim_pool_referral_rewards` instead.")] +public fun claim_referral_rewards( + _self: &mut Pool, + _referral: &DeepBookReferral, + _ctx: &mut TxContext, +): (Coin, Coin, Coin) { + abort +} + +/// Claim the rewards for the referral. +public fun claim_pool_referral_rewards( + self: &mut Pool, + referral: &DeepBookPoolReferral, + ctx: &mut TxContext, +): (Coin, Coin, Coin) { + let _ = self.load_inner(); + referral.assert_referral_owner(ctx); + let referral_id = object::id(referral); + let referral_rewards: &mut ReferralRewards = self + .id + .borrow_mut(referral_id); + let base = referral_rewards.base.withdraw_all().into_coin(ctx); + let quote = referral_rewards.quote.withdraw_all().into_coin(ctx); + let deep = referral_rewards.deep.withdraw_all().into_coin(ctx); + + event::emit(ReferralClaimed { + pool_id: self.id(), + referral_id, + owner: ctx.sender(), + base_amount: base.value(), + quote_amount: quote.value(), + deep_amount: deep.value(), + }); + + (base, quote, deep) +} + // === Public-Mutative Functions * ADMIN * === /// Create a new pool. The pool is registered in the registry. /// Checks are performed to ensure the tick size, lot size, and min size are @@ -718,6 +1016,18 @@ public fun update_allowed_versions( inner.allowed_versions = allowed_versions; } +/// Takes the registry and updates the allowed version within pool +/// Permissionless equivalent of `update_allowed_versions` +/// This function does not have version restrictions +public fun update_pool_allowed_versions( + self: &mut Pool, + registry: &Registry, +) { + let allowed_versions = registry.allowed_versions(); + let inner: &mut PoolInner = self.inner.load_value_mut(); + inner.allowed_versions = allowed_versions; +} + /// Adjust the tick size of the pool. Only admin can adjust the tick size. public fun adjust_tick_size_admin( self: &mut Pool, @@ -768,6 +1078,48 @@ public fun adjust_min_lot_size_admin( }); } +/// Enable the EWMA state for the pool. This allows the pool to use +/// the EWMA state for volatility calculations and additional taker fees. +public fun enable_ewma_state( + self: &mut Pool, + _cap: &DeepbookAdminCap, + enable: bool, + clock: &Clock, + ctx: &mut TxContext, +) { + let _ = self.load_inner_mut(); + let ewma_state = self.update_ewma_state(clock, ctx); + if (enable) { + ewma_state.enable(); + } else { + ewma_state.disable(); + } +} + +/// Set the EWMA parameters for the pool. +/// Only admin can set the parameters. +public fun set_ewma_params( + self: &mut Pool, + _cap: &DeepbookAdminCap, + alpha: u64, + z_score_threshold: u64, + additional_taker_fee: u64, + clock: &Clock, + ctx: &mut TxContext, +) { + assert!(alpha <= constants::max_ewma_alpha(), EInvalidEWMAAlpha); + assert!(z_score_threshold <= constants::max_z_score_threshold(), EInvalidZScoreThreshold); + assert!( + additional_taker_fee <= constants::max_additional_taker_fee(), + EInvalidAdditionalTakerFee, + ); + let _ = self.load_inner_mut(); + let ewma_state = self.update_ewma_state(clock, ctx); + ewma_state.set_alpha(alpha); + ewma_state.set_z_score_threshold(z_score_threshold); + ewma_state.set_additional_taker_fee(additional_taker_fee); +} + // === Public-View Functions === /// Accessor to check if the pool is whitelisted. public fun whitelisted(self: &Pool): bool { @@ -836,7 +1188,7 @@ public fun get_quantity_out( let whitelist = self.whitelisted(); let self = self.load_inner(); let params = self.state.governance().trade_params(); - let (taker_fee, _) = (params.taker_fee(), params.maker_fee()); + let taker_fee = params.taker_fee(); let deep_price = self.deep_price.get_order_deep_price(whitelist); self .book @@ -863,7 +1215,7 @@ public fun get_quantity_out_input_fee( ): (u64, u64, u64) { let self = self.load_inner(); let params = self.state.governance().trade_params(); - let (taker_fee, _) = (params.taker_fee(), params.maker_fee()); + let taker_fee = params.taker_fee(); let deep_price = self.deep_price.empty_deep_price(); self .book @@ -878,6 +1230,64 @@ public fun get_quantity_out_input_fee( ) } +/// Dry run to determine the base quantity needed to sell to receive a target quote quantity. +/// Returns (base_quantity_in, actual_quote_quantity_out, deep_quantity_required) +/// Returns (0, 0, 0) if insufficient liquidity or if result would be below min_size. +public fun get_base_quantity_in( + self: &Pool, + target_quote_quantity: u64, + pay_with_deep: bool, + clock: &Clock, +): (u64, u64, u64) { + let whitelist = self.whitelisted(); + let self = self.load_inner(); + let params = self.state.governance().trade_params(); + let taker_fee = params.taker_fee(); + let deep_price = if (pay_with_deep) { + self.deep_price.get_order_deep_price(whitelist) + } else { + self.deep_price.empty_deep_price() + }; + self + .book + .get_base_quantity_in( + target_quote_quantity, + taker_fee, + deep_price, + pay_with_deep, + clock.timestamp_ms(), + ) +} + +/// Dry run to determine the quote quantity needed to buy a target base quantity. +/// Returns (actual_base_quantity_out, quote_quantity_in, deep_quantity_required) +/// Returns (0, 0, 0) if insufficient liquidity or if result would be below min_size. +public fun get_quote_quantity_in( + self: &Pool, + target_base_quantity: u64, + pay_with_deep: bool, + clock: &Clock, +): (u64, u64, u64) { + let whitelist = self.whitelisted(); + let self = self.load_inner(); + let params = self.state.governance().trade_params(); + let taker_fee = params.taker_fee(); + let deep_price = if (pay_with_deep) { + self.deep_price.get_order_deep_price(whitelist) + } else { + self.deep_price.empty_deep_price() + }; + self + .book + .get_quote_quantity_in( + target_base_quantity, + taker_fee, + deep_price, + pay_with_deep, + clock.timestamp_ms(), + ) +} + /// Returns the mid price of the pool. public fun mid_price( self: &Pool, @@ -1071,7 +1481,184 @@ public fun locked_balance( (base_quantity, quote_quantity, deep_quantity) } +/// Check if a limit order can be placed based on balance manager balances. +/// Returns true if the balance manager has sufficient balance (accounting for fees) to place the order, false otherwise. +/// Assumes the limit order is a taker order as a worst case scenario. +public fun can_place_limit_order( + self: &Pool, + balance_manager: &BalanceManager, + price: u64, + quantity: u64, + is_bid: bool, + pay_with_deep: bool, + expire_timestamp: u64, + clock: &Clock, +): bool { + let whitelist = self.whitelisted(); + let pool_inner = self.load_inner(); + + if ( + !self.check_limit_order_params( + price, + quantity, + expire_timestamp, + clock, + ) + ) { + return false + }; + + let order_deep_price = if (pay_with_deep) { + pool_inner.deep_price.get_order_deep_price(whitelist) + } else { + pool_inner.deep_price.empty_deep_price() + }; + + let quote_quantity = math::mul(quantity, price); + + // Calculate fee quantity using taker fee (worst case for limit orders) + let taker_fee = pool_inner.state.governance().trade_params().taker_fee(); + let fee_balances = order_deep_price.fee_quantity(quantity, quote_quantity, is_bid); + + // Calculate required balances + let mut required_base = 0; + let mut required_quote = 0; + let mut required_deep = 0; + + if (is_bid) { + required_quote = quote_quantity; + if (pay_with_deep) { + required_deep = math::mul(fee_balances.deep(), taker_fee); + } else { + let fee_quote = math::mul(fee_balances.quote(), taker_fee); + required_quote = required_quote + fee_quote; + }; + } else { + required_base = quantity; + if (pay_with_deep) { + required_deep = math::mul(fee_balances.deep(), taker_fee); + } else { + let fee_base = math::mul(fee_balances.base(), taker_fee); + required_base = required_base + fee_base; + }; + }; + + // Get current balances from balance manager. Accounts for settled balances. + let settled_balances = if (!self.account_exists(balance_manager)) { + balances::empty() + } else { + self.account(balance_manager).settled_balances() + }; + let available_base = balance_manager.balance() + settled_balances.base(); + let available_quote = balance_manager.balance() + settled_balances.quote(); + let available_deep = balance_manager.balance() + settled_balances.deep(); + + // Check if available balances are sufficient + (available_base >= required_base) && (available_quote >= required_quote) && (available_deep >= required_deep) +} + +/// Check if a market order can be placed based on balance manager balances. +/// Returns true if the balance manager has sufficient balance (accounting for fees) to place the order, false otherwise. +/// Does not account for discounted taker fees +public fun can_place_market_order( + self: &Pool, + balance_manager: &BalanceManager, + quantity: u64, + is_bid: bool, + pay_with_deep: bool, + clock: &Clock, +): bool { + // Validate order parameters against pool book params + if (!self.check_market_order_params(quantity)) { + return false + }; + + let mut required_base = 0; + let mut required_deep = 0; + + // Get current balances from balance manager. Accounts for settled balances. + let settled_balances = if (!self.account_exists(balance_manager)) { + balances::empty() + } else { + self.account(balance_manager).settled_balances() + }; + let available_base = balance_manager.balance() + settled_balances.base(); + let available_quote = balance_manager.balance() + settled_balances.quote(); + let available_deep = balance_manager.balance() + settled_balances.deep(); + + if (is_bid) { + // For bid orders: calculate quote needed to acquire desired base quantity + // get_quote_quantity_in returns (base_out, quote_needed, deep_required) + let (base_out, quote_needed, deep_required) = self.get_quote_quantity_in( + quantity, + pay_with_deep, + clock, + ); + + // Not enough liquidity or available quote for the base quantity + if (base_out < quantity || available_quote < quote_needed) { + return false + }; + + if (pay_with_deep) { + required_deep = deep_required; + }; + } else { + // For ask orders: if paying fees in input token (base), need quantity + fees + // get_quantity_out_input_fee accounts for fees, so we need to check if we have enough base + // including fees that will be deducted + let (_, _, deep_required) = if (pay_with_deep) { + self.get_quantity_out(quantity, 0, clock) + } else { + self.get_quantity_out_input_fee(quantity, 0, clock) + }; + + // If paying fees in base asset, need quantity + fees + required_base = if (pay_with_deep) { + quantity + } else { + // Fees are deducted from base, so need more base to account for fees + let (taker_fee, _, _) = self.pool_trade_params(); + let input_fee_rate = math::mul(taker_fee, constants::fee_penalty_multiplier()); + math::mul(quantity, constants::float_scaling() + input_fee_rate) + }; + + if (pay_with_deep) { + required_deep = deep_required; + }; + }; + + // Check if available balances are sufficient + (available_base >= required_base) && (available_deep >= required_deep) +} + +/// Check if a market order can be placed based on pool book params. +/// Returns true if the order parameters are valid, false otherwise. +public fun check_market_order_params( + self: &Pool, + quantity: u64, +): bool { + let pool_inner = self.load_inner(); + pool_inner.book.check_market_order_params(quantity) +} + +/// Check if a limit order can be placed based on pool book params. +/// Returns true if the order parameters are valid, false otherwise. +public fun check_limit_order_params( + self: &Pool, + price: u64, + quantity: u64, + expire_timestamp: u64, + clock: &Clock, +): bool { + let pool_inner = self.load_inner(); + pool_inner + .book + .check_limit_order_params(price, quantity, expire_timestamp, clock.timestamp_ms()) +} + /// Returns the trade params for the pool. +/// Returns (taker_fee, maker_fee, stake_required) public fun pool_trade_params( self: &Pool, ): (u64, u64, u64) { @@ -1109,6 +1696,14 @@ public fun pool_book_params( (tick_size, lot_size, min_size) } +public fun account_exists( + self: &Pool, + balance_manager: &BalanceManager, +): bool { + let self = self.load_inner(); + self.state.account_exists(balance_manager.id()) +} + public fun account( self: &Pool, balance_manager: &BalanceManager, @@ -1123,6 +1718,47 @@ public fun quorum(self: &Pool): u6 self.load_inner().state.governance().quorum() } +public fun id(self: &Pool): ID { + self.load_inner().pool_id +} + +#[deprecated(note = b"This function is deprecated, use `get_pool_referral_balances` instead.")] +public fun get_referral_balances( + _self: &Pool, + _referral: &DeepBookReferral, +): (u64, u64, u64) { + abort +} + +public fun get_pool_referral_balances( + self: &Pool, + referral: &DeepBookPoolReferral, +): (u64, u64, u64) { + let _ = self.load_inner(); + assert!(referral.balance_manager_referral_pool_id() == self.id(), EWrongPoolReferral); + let referral_rewards: &ReferralRewards = self + .id + .borrow(object::id(referral)); + let base = referral_rewards.base.value(); + let quote = referral_rewards.quote.value(); + let deep = referral_rewards.deep.value(); + + (base, quote, deep) +} + +public fun pool_referral_multiplier( + self: &Pool, + referral: &DeepBookPoolReferral, +): u64 { + let _ = self.load_inner(); + assert!(referral.balance_manager_referral_pool_id() == self.id(), EWrongPoolReferral); + let referral_rewards: &ReferralRewards = self + .id + .borrow(object::id(referral)); + + referral_rewards.multiplier +} + // === Public-Package Functions === public(package) fun create_pool( registry: &mut Registry, @@ -1142,21 +1778,21 @@ public(package) fun create_pool( assert!(min_size % lot_size == 0, EInvalidMinSize); assert!(math::is_power_of_ten(min_size), EInvalidMinSize); assert!(!(whitelisted_pool && stable_pool), EPoolCannotBeBothWhitelistedAndStable); - assert!(type_name::get() != type_name::get(), ESameBaseAndQuote); + assert!( + type_name::with_defining_ids() != type_name::with_defining_ids(), + ESameBaseAndQuote, + ); let pool_id = object::new(ctx); - let mut pool_inner = PoolInner { + let pool_inner = PoolInner { allowed_versions: registry.allowed_versions(), pool_id: pool_id.to_inner(), book: book::empty(tick_size, lot_size, min_size, ctx), - state: state::empty(stable_pool, ctx), + state: state::empty(whitelisted_pool, stable_pool, ctx), vault: vault::empty(), deep_price: deep_price::empty(), registered_pool: true, }; - if (whitelisted_pool) { - pool_inner.set_whitelist(ctx); - }; let params = pool_inner.state.governance().trade_params(); let taker_fee = params.taker_fee(); let maker_fee = params.maker_fee(); @@ -1216,16 +1852,13 @@ public(package) fun load_inner_mut( inner } -// === Private Functions === -/// Set a pool as a whitelist pool at pool creation. Whitelist pools have zero -/// fees. -fun set_whitelist( - self: &mut PoolInner, - ctx: &TxContext, -) { - self.state.governance_mut(ctx).set_whitelist(true); +public(package) fun load_ewma_state( + self: &Pool, +): EWMAState { + *self.id.borrow(constants::ewma_df_key()) } +// === Private Functions === fun place_order_int( self: &mut Pool, balance_manager: &mut BalanceManager, @@ -1243,42 +1876,123 @@ fun place_order_int( ctx: &TxContext, ): OrderInfo { let whitelist = self.whitelisted(); - let self = self.load_inner_mut(); + self.update_ewma_state(clock, ctx); + let ewma_state = self.load_ewma_state(); + let order_info = { + let pool_inner = self.load_inner_mut(); - let order_deep_price = if (pay_with_deep) { - self.deep_price.get_order_deep_price(whitelist) - } else { - self.deep_price.empty_deep_price() + let order_deep_price = if (pay_with_deep) { + pool_inner.deep_price.get_order_deep_price(whitelist) + } else { + pool_inner.deep_price.empty_deep_price() + }; + + let mut order_info = order_info::new( + pool_inner.pool_id, + balance_manager.id(), + client_order_id, + ctx.sender(), + order_type, + self_matching_option, + price, + quantity, + is_bid, + pay_with_deep, + ctx.epoch(), + expire_timestamp, + order_deep_price, + market_order, + clock.timestamp_ms(), + ); + pool_inner.book.create_order(&mut order_info, clock.timestamp_ms()); + let (settled, owed) = pool_inner + .state + .process_create( + &mut order_info, + &ewma_state, + pool_inner.pool_id, + ctx, + ); + pool_inner.vault.settle_balance_manager(settled, owed, balance_manager, trade_proof); + order_info.emit_order_info(); + order_info.emit_orders_filled(clock.timestamp_ms()); + order_info.emit_order_fully_filled_if_filled(clock.timestamp_ms()); + + order_info }; - let mut order_info = order_info::new( - self.pool_id, - balance_manager.id(), - client_order_id, - ctx.sender(), - order_type, - self_matching_option, - price, - quantity, - is_bid, - pay_with_deep, - ctx.epoch(), - expire_timestamp, - order_deep_price, - market_order, - clock.timestamp_ms(), + self.process_referral_fees( + &order_info, + balance_manager, + trade_proof, ); - self.book.create_order(&mut order_info, clock.timestamp_ms()); - let (settled, owed) = self - .state - .process_create( - &mut order_info, - self.pool_id, - ctx, - ); - self.vault.settle_balance_manager(settled, owed, balance_manager, trade_proof); - order_info.emit_order_info(); - order_info.emit_orders_filled(clock.timestamp_ms()); order_info } + +fun process_referral_fees( + self: &mut Pool, + order_info: &OrderInfo, + balance_manager: &mut BalanceManager, + trade_proof: &TradeProof, +) { + let referral_id = balance_manager.get_balance_manager_referral_id(self.id()); + if (referral_id.is_some()) { + let referral_id = referral_id.destroy_some(); + let referral_rewards: &mut ReferralRewards = self + .id + .borrow_mut(referral_id); + let referral_multiplier = referral_rewards.multiplier; + let referral_fee = math::mul(order_info.paid_fees(), referral_multiplier); + if (referral_fee == 0) { + return + }; + let mut base_fee = 0; + let mut quote_fee = 0; + let mut deep_fee = 0; + if (order_info.fee_is_deep()) { + referral_rewards + .deep + .join(balance_manager.withdraw_with_proof(trade_proof, referral_fee, false)); + deep_fee = referral_fee; + } else if (!order_info.is_bid()) { + referral_rewards + .base + .join(balance_manager.withdraw_with_proof(trade_proof, referral_fee, false)); + base_fee = referral_fee; + } else { + referral_rewards + .quote + .join(balance_manager.withdraw_with_proof(trade_proof, referral_fee, false)); + quote_fee = referral_fee; + }; + + event::emit(ReferralFeeEvent { + pool_id: self.id(), + referral_id, + base_fee, + quote_fee, + deep_fee, + }); + }; +} + +fun update_ewma_state( + self: &mut Pool, + clock: &Clock, + ctx: &TxContext, +): &mut EWMAState { + let pool_id = self.id(); + if (!self.id.exists_(constants::ewma_df_key())) { + self.id.add(constants::ewma_df_key(), init_ewma_state(ctx)); + }; + + let ewma_state: &mut EWMAState = self + .id + .borrow_mut( + constants::ewma_df_key(), + ); + ewma_state.update(pool_id, clock, ctx); + + ewma_state +} diff --git a/packages/deepbook/sources/registry.move b/packages/deepbook/sources/registry.move index 72868f58f..9d67f48f9 100644 --- a/packages/deepbook/sources/registry.move +++ b/packages/deepbook/sources/registry.move @@ -6,7 +6,17 @@ module deepbook::registry; use deepbook::constants; use std::type_name::{Self, TypeName}; -use sui::{bag::{Self, Bag}, dynamic_field, vec_set::{Self, VecSet}, versioned::{Self, Versioned}}; +use sui::{ + bag::{Self, Bag}, + dynamic_field::{Self, Self as df}, + table::{Self, Table}, + vec_set::{Self, VecSet}, + versioned::{Self, Versioned} +}; + +use fun df::add as UID.add; +use fun df::exists_ as UID.exists_; +use fun df::remove as UID.remove; // === Errors === const EPoolAlreadyExists: u64 = 1; @@ -17,6 +27,8 @@ const EVersionAlreadyEnabled: u64 = 5; const ECannotDisableCurrentVersion: u64 = 6; const ECoinAlreadyWhitelisted: u64 = 7; const ECoinNotWhitelisted: u64 = 8; +const EMaxBalanceManagersReached: u64 = 9; +const EAppNotAuthorized: u64 = 10; public struct REGISTRY has drop {} @@ -43,6 +55,28 @@ public struct PoolKey has copy, drop, store { } public struct StableCoinKey has copy, drop, store {} +public struct BalanceManagerKey has copy, drop, store {} + +// === App Auth === + +/// An authorization Key kept in the Registry - allows applications access protected features of the DeepBook +/// The `App` type parameter is a witness which should be defined in the original module +public struct AppKey has copy, drop, store {} + +/// Authorize an application to access protected features of the DeepBook. +public fun authorize_app(self: &mut Registry, _admin_cap: &DeepbookAdminCap) { + self.id.add(AppKey {}, true); +} + +/// Deauthorize an application by removing its authorization key. +public fun deauthorize_app(self: &mut Registry, _admin_cap: &DeepbookAdminCap): bool { + self.id.remove(AppKey {}) +} + +/// Assert that an application is authorized to access protected features of DeepBook. +public fun assert_app_is_authorized(self: &Registry) { + assert!(self.id.exists_(AppKey {}), EAppNotAuthorized); +} fun init(_: REGISTRY, ctx: &mut TxContext) { let registry_inner = RegistryInner { @@ -98,7 +132,7 @@ public fun disable_version(self: &mut Registry, version: u64, _cap: &DeepbookAdm /// Only Admin can add stablecoin public fun add_stablecoin(self: &mut Registry, _cap: &DeepbookAdminCap) { let _: &mut RegistryInner = self.load_inner_mut(); - let stable_type = type_name::get(); + let stable_type = type_name::with_defining_ids(); if ( !dynamic_field::exists_( &self.id, @@ -124,7 +158,7 @@ public fun add_stablecoin(self: &mut Registry, _cap: &DeepbookAdminC /// Only Admin can remove stablecoin public fun remove_stablecoin(self: &mut Registry, _cap: &DeepbookAdminCap) { let _: &mut RegistryInner = self.load_inner_mut(); - let stable_type = type_name::get(); + let stable_type = type_name::with_defining_ids(); assert!( dynamic_field::exists_( &self.id, @@ -140,6 +174,40 @@ public fun remove_stablecoin(self: &mut Registry, _cap: &DeepbookAdm stable_coins.remove(&stable_type); } +/// Adds the BalanceManagerKey dynamic field to the registry +public fun init_balance_manager_map( + self: &mut Registry, + _cap: &DeepbookAdminCap, + ctx: &mut TxContext, +) { + let _: &mut RegistryInner = self.load_inner_mut(); + if ( + !dynamic_field::exists_( + &self.id, + BalanceManagerKey {}, + ) + ) { + dynamic_field::add( + &mut self.id, + BalanceManagerKey {}, + table::new>(ctx), + ); + }; +} + +/// Get the balance manager IDs for a given owner +public fun get_balance_manager_ids(self: &Registry, owner: address): VecSet { + let balance_manager_map: &Table> = dynamic_field::borrow( + &self.id, + BalanceManagerKey {}, + ); + if (balance_manager_map.contains(owner)) { + *balance_manager_map.borrow>(owner) + } else { + vec_set::empty() + } +} + /// Returns whether the given coin is whitelisted public fun is_stablecoin(self: &Registry, stable_type: TypeName): bool { let _: &RegistryInner = self.load_inner(); @@ -175,14 +243,14 @@ public(package) fun load_inner_mut(self: &mut Registry): &mut RegistryInner { public(package) fun register_pool(self: &mut Registry, pool_id: ID) { let self = self.load_inner_mut(); let key = PoolKey { - base: type_name::get(), - quote: type_name::get(), + base: type_name::with_defining_ids(), + quote: type_name::with_defining_ids(), }; assert!(!self.pools.contains(key), EPoolAlreadyExists); let key = PoolKey { - base: type_name::get(), - quote: type_name::get(), + base: type_name::with_defining_ids(), + quote: type_name::with_defining_ids(), }; assert!(!self.pools.contains(key), EPoolAlreadyExists); @@ -193,8 +261,8 @@ public(package) fun register_pool(self: &mut Registry, po public(package) fun unregister_pool(self: &mut Registry) { let self = self.load_inner_mut(); let key = PoolKey { - base: type_name::get(), - quote: type_name::get(), + base: type_name::with_defining_ids(), + quote: type_name::with_defining_ids(), }; assert!(self.pools.contains(key), EPoolDoesNotExist); self.pools.remove(key); @@ -208,12 +276,32 @@ public(package) fun load_inner(self: &Registry): &RegistryInner { inner } +/// Adds a balance_manager to the registry +public(package) fun add_balance_manager(self: &mut Registry, owner: address, manager_id: ID) { + let _: &mut RegistryInner = self.load_inner_mut(); + let balance_manager_map: &mut Table> = dynamic_field::borrow_mut( + &mut self.id, + BalanceManagerKey {}, + ); + if (!balance_manager_map.contains(owner)) { + balance_manager_map.add(owner, vec_set::empty()); + }; + let balance_manager_ids = balance_manager_map.borrow_mut(owner); + if (!balance_manager_ids.contains(&manager_id)) { + balance_manager_ids.insert(manager_id); + }; + assert!( + balance_manager_ids.length() <= constants::max_balance_managers(), + EMaxBalanceManagersReached, + ); +} + /// Get the pool id for the given base and quote assets. public(package) fun get_pool_id(self: &Registry): ID { let self = self.load_inner(); let key = PoolKey { - base: type_name::get(), - quote: type_name::get(), + base: type_name::with_defining_ids(), + quote: type_name::with_defining_ids(), }; assert!(self.pools.contains(key), EPoolDoesNotExist); diff --git a/packages/deepbook/sources/state/ewma.move b/packages/deepbook/sources/state/ewma.move new file mode 100644 index 000000000..3c401bced --- /dev/null +++ b/packages/deepbook/sources/state/ewma.move @@ -0,0 +1,180 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +/// The Exponentially Weighted Moving Average (EWMA) state for DeepBook +/// This state is used to calculate the smoothed mean and variance of gas prices +/// and apply a penalty to taker fees based on the Z-score of the current gas price +/// relative to the smoothed mean and variance. +/// The state is disabled by default and can be configured with different parameters. +module deepbook::ewma; + +use deepbook::{constants, math}; +use sui::{clock::Clock, event}; + +/// The EWMA state structure +/// It contains the smoothed mean, variance, alpha, Z-score threshold, +/// additional taker fee, and whether the state is enabled. +public struct EWMAState has copy, drop, store { + mean: u64, + variance: u64, + alpha: u64, + z_score_threshold: u64, + additional_taker_fee: u64, + last_updated_timestamp: u64, + enabled: bool, +} + +public struct EWMAUpdate has copy, drop, store { + pool_id: ID, + gas_price: u64, + mean: u64, + variance: u64, + timestamp: u64, +} + +public(package) fun init_ewma_state(ctx: &TxContext): EWMAState { + let gas_price = ctx.gas_price() * constants::float_scaling(); + + EWMAState { + mean: gas_price, + variance: 0, + alpha: constants::default_ewma_alpha(), + z_score_threshold: constants::default_z_score_threshold(), + additional_taker_fee: constants::default_additional_taker_fee(), + last_updated_timestamp: 0, + enabled: false, + } +} + +/// Updates the EWMA state with the current gas price +/// It calculates the new mean and variance based on the current gas price +/// and the previous mean and variance using the EWMA formula. +/// The alpha parameter controls the weight of the current gas price in the calculation. +/// The mean and variance are updated in the state. +public(package) fun update(self: &mut EWMAState, pool_id: ID, clock: &Clock, ctx: &TxContext) { + let current_timestamp = clock.timestamp_ms(); + if (current_timestamp == self.last_updated_timestamp) { + return + }; + self.last_updated_timestamp = current_timestamp; + + let alpha = self.alpha; + let one_minute_alpha = constants::float_scaling() - alpha; + let gas_price = ctx.gas_price() * constants::float_scaling(); + + let mean_new = math::mul(alpha, gas_price) + math::mul(one_minute_alpha, self.mean); + + let diff = if (gas_price > self.mean) { + gas_price - self.mean + } else { + self.mean - gas_price + }; + let diff_squared = math::mul(diff, diff); + + let variance_new = if (self.variance == 0) { + diff_squared + } else { + math::mul(self.variance, one_minute_alpha) + math::mul(alpha, diff_squared) + }; + + self.mean = mean_new; + self.variance = variance_new; + + event::emit(EWMAUpdate { + pool_id, + gas_price, + mean: self.mean, + variance: self.variance, + timestamp: current_timestamp, + }); +} + +/// Returns the Z-score of the current gas price relative to the smoothed mean and variance. +/// The Z-score is calculated as the difference between the current gas price and the mean, +/// divided by the standard deviation (square root of variance). +public(package) fun z_score(self: &EWMAState, ctx: &TxContext): u64 { + if (self.variance == 0) { + return 0 + }; + + let gas_price = ctx.gas_price() * constants::float_scaling(); + let diff = if (gas_price > self.mean) { + gas_price - self.mean + } else { + self.mean - gas_price + }; + + let std_dev = math::sqrt(self.variance, constants::float_scaling()); + let z = math::div(diff, std_dev); + + z +} + +/// Sets the alpha value for the EWMA state. Admin only. +public(package) fun set_alpha(self: &mut EWMAState, alpha: u64) { + self.alpha = alpha; +} + +/// Sets the Z-score threshold for the EWMA state. Admin only. +public(package) fun set_z_score_threshold(self: &mut EWMAState, threshold: u64) { + self.z_score_threshold = threshold; +} + +/// Sets the additional taker fee for the EWMA state. Admin only. +public(package) fun set_additional_taker_fee(self: &mut EWMAState, fee: u64) { + self.additional_taker_fee = fee; +} + +/// Enables the EWMA state. Admin only. +public(package) fun enable(self: &mut EWMAState) { + self.enabled = true; +} + +/// Disables the EWMA state. Admin only. +public(package) fun disable(self: &mut EWMAState) { + self.enabled = false; +} + +/// Applies the taker penalty based on the Z-score of the current gas price. +/// If the gas price is below the mean, the taker fee is not applied. +public(package) fun apply_taker_penalty(self: &EWMAState, taker_fee: u64, ctx: &TxContext): u64 { + let gas_price = ctx.gas_price() * constants::float_scaling(); + if (!self.enabled || gas_price < self.mean) { + return taker_fee + }; + + let z_score = self.z_score(ctx); + if (z_score > self.z_score_threshold) { + taker_fee + self.additional_taker_fee + } else { + taker_fee + } +} + +public(package) fun mean(self: &EWMAState): u64 { + self.mean +} + +public(package) fun variance(self: &EWMAState): u64 { + self.variance +} + +public(package) fun alpha(self: &EWMAState): u64 { + self.alpha +} + +public(package) fun z_score_threshold(self: &EWMAState): u64 { + self.z_score_threshold +} + +public(package) fun additional_taker_fee(self: &EWMAState): u64 { + self.additional_taker_fee +} + +public(package) fun enabled(self: &EWMAState): bool { + self.enabled +} + +public(package) fun last_updated_timestamp(self: &EWMAState): u64 { + self.last_updated_timestamp +} diff --git a/packages/deepbook/sources/state/governance.move b/packages/deepbook/sources/state/governance.move index 62d061b7d..276a915db 100644 --- a/packages/deepbook/sources/state/governance.move +++ b/packages/deepbook/sources/state/governance.move @@ -69,20 +69,24 @@ public struct TradeParamsUpdateEvent has copy, drop { } // === Public-Package Functions === -public(package) fun empty(stable_pool: bool, ctx: &TxContext): Governance { - let default_taker = if (stable_pool) { +public(package) fun empty(whitelisted: bool, stable_pool: bool, ctx: &TxContext): Governance { + let default_taker = if (whitelisted) { + 0 + } else if (stable_pool) { MAX_TAKER_STABLE } else { MAX_TAKER_VOLATILE }; - let default_maker = if (stable_pool) { + let default_maker = if (whitelisted) { + 0 + } else if (stable_pool) { MAX_MAKER_STABLE } else { MAX_MAKER_VOLATILE }; Governance { epoch: ctx.epoch(), - whitelisted: false, + whitelisted, stable: stable_pool, proposals: vec_map::empty(), trade_params: trade_params::new( @@ -100,13 +104,6 @@ public(package) fun empty(stable_pool: bool, ctx: &TxContext): Governance { } } -/// Whitelist a pool. This pool can be used as a DEEP reference price for -/// other pools. This pool will have zero fees. -public(package) fun set_whitelist(self: &mut Governance, whitelisted: bool) { - self.whitelisted = whitelisted; - self.reset_trade_params(); -} - public(package) fun whitelisted(self: &Governance): bool { self.whitelisted } @@ -169,7 +166,7 @@ public(package) fun add_proposal( }; let voting_power = stake_to_voting_power(stake_amount); - if (self.proposals.size() == MAX_PROPOSALS) { + if (self.proposals.length() == MAX_PROPOSALS) { self.remove_lowest_proposal(voting_power); }; @@ -262,7 +259,7 @@ fun remove_lowest_proposal(self: &mut Governance, voting_power: u64) { let mut cur_lowest_votes = constants::max_u64(); let (keys, values) = self.proposals.into_keys_values(); - self.proposals.size().do!(|i| { + self.proposals.length().do!(|i| { let proposal_votes = values[i].votes; if (proposal_votes < voting_power && proposal_votes <= cur_lowest_votes) { removal_id = option::some(keys[i]); @@ -274,19 +271,6 @@ fun remove_lowest_proposal(self: &mut Governance, voting_power: u64) { self.proposals.remove(removal_id.borrow()); } -fun reset_trade_params(self: &mut Governance) { - self.proposals = vec_map::empty(); - let stake = self.trade_params.stake_required(); - if (self.whitelisted) { - self.trade_params = trade_params::new(0, 0, 0); - } else if (self.stable) { - self.trade_params = trade_params::new(MAX_TAKER_STABLE, MAX_MAKER_STABLE, stake); - } else { - self.trade_params = trade_params::new(MAX_TAKER_VOLATILE, MAX_MAKER_VOLATILE, stake); - }; - self.next_trade_params = self.trade_params; -} - fun to_trade_params(proposal: &Proposal): TradeParams { trade_params::new( proposal.taker_fee, diff --git a/packages/deepbook/sources/state/state.move b/packages/deepbook/sources/state/state.move index 1a16b5c3f..3b90fe486 100644 --- a/packages/deepbook/sources/state/state.move +++ b/packages/deepbook/sources/state/state.move @@ -11,6 +11,7 @@ use deepbook::{ balance_manager::BalanceManager, balances::{Self, Balances}, constants, + ewma::EWMAState, fill::Fill, governance::{Self, Governance}, history::{Self, History}, @@ -75,8 +76,17 @@ public struct RebateEvent has copy, drop { claim_amount: u64, } -public(package) fun empty(stable_pool: bool, ctx: &mut TxContext): State { +public struct TakerFeePenaltyApplied has copy, drop { + pool_id: ID, + balance_manager_id: ID, + order_id: u128, + taker_fee_without_penalty: u64, + taker_fee: u64, +} + +public(package) fun empty(whitelisted: bool, stable_pool: bool, ctx: &mut TxContext): State { let governance = governance::empty( + whitelisted, stable_pool, ctx, ); @@ -97,6 +107,7 @@ public(package) fun empty(stable_pool: bool, ctx: &mut TxContext): State { public(package) fun process_create( self: &mut State, order_info: &mut OrderInfo, + ewma_state: &EWMAState, pool_id: ID, ctx: &TxContext, ): (Balances, Balances) { @@ -126,16 +137,26 @@ public(package) fun process_create( math::mul_u128(account_volume, avg_executed_price as u128), ); - // taker fee will almost be calculated as 0 for whitelisted pools by + // taker fee will always be calculated as 0 for whitelisted pools by // default, as account_volume_in_deep is 0 - let taker_fee = self + let taker_fee_without_penalty = self .governance .trade_params() .taker_fee_for_user(account_stake, account_volume_in_deep); + let taker_fee = ewma_state.apply_taker_penalty(taker_fee_without_penalty, ctx); + if (taker_fee > taker_fee_without_penalty) { + event::emit(TakerFeePenaltyApplied { + pool_id, + balance_manager_id: order_info.balance_manager_id(), + order_id: order_info.order_id(), + taker_fee_without_penalty, + taker_fee, + }); + }; let maker_fee = self.governance.trade_params().maker_fee(); if (order_info.order_inserted()) { - assert!(account.open_orders().size() < constants::max_open_orders(), EMaxOpenOrders); + assert!(account.open_orders().length() < constants::max_open_orders(), EMaxOpenOrders); account.add_order(order_info.order_id()); }; account.add_taker_volume(order_info.executed_quantity()); @@ -368,17 +389,17 @@ public(package) fun process_claim_rebates( claim_amount, }); balance_manager.emit_balance_event( - type_name::get(), + type_name::with_defining_ids(), claim_amount.deep(), true, ); balance_manager.emit_balance_event( - type_name::get(), + type_name::with_defining_ids(), claim_amount.base(), true, ); balance_manager.emit_balance_event( - type_name::get(), + type_name::with_defining_ids(), claim_amount.quote(), true, ); diff --git a/packages/deepbook/sources/vault/vault.move b/packages/deepbook/sources/vault/vault.move index a16d248c8..84117f31c 100644 --- a/packages/deepbook/sources/vault/vault.move +++ b/packages/deepbook/sources/vault/vault.move @@ -17,6 +17,8 @@ const EInvalidLoanQuantity: u64 = 3; const EIncorrectLoanPool: u64 = 4; const EIncorrectTypeReturned: u64 = 5; const EIncorrectQuantityReturned: u64 = 6; +const ENoBalanceToSettle: u64 = 7; +const EHasOwedBalances: u64 = 8; // === Structs === public struct Vault has store { @@ -99,6 +101,37 @@ public(package) fun settle_balance_manager( }; } +/// Transfer any settled amounts for the `balance_manager`. +public(package) fun settle_balance_manager_permissionless( + self: &mut Vault, + balances_out: Balances, + balances_in: Balances, + balance_manager: &mut BalanceManager, +) { + assert!( + balances_in.base() == 0 && balances_in.quote() == 0 && balances_in.deep() == 0, + EHasOwedBalances, + ); + let has_settled_balances = + balances_out.base() > 0 + || balances_out.quote() > 0 + || balances_out.deep() > 0; + assert!(has_settled_balances, ENoBalanceToSettle); + + if (balances_out.base() > 0) { + let balance = self.base_balance.split(balances_out.base()); + balance_manager.deposit_permissionless(balance); + }; + if (balances_out.quote() > 0) { + let balance = self.quote_balance.split(balances_out.quote()); + balance_manager.deposit_permissionless(balance); + }; + if (balances_out.deep() > 0) { + let balance = self.deep_balance.split(balances_out.deep()); + balance_manager.deposit_permissionless(balance); + }; +} + public(package) fun withdraw_deep_to_burn( self: &mut Vault, amount_to_burn: u64, @@ -114,7 +147,7 @@ public(package) fun borrow_flashloan_base( ): (Coin, FlashLoan) { assert!(borrow_quantity > 0, EInvalidLoanQuantity); assert!(self.base_balance.value() >= borrow_quantity, ENotEnoughBaseForLoan); - let borrow_type_name = type_name::get(); + let borrow_type_name = type_name::with_defining_ids(); let borrow: Coin = self.base_balance.split(borrow_quantity).into_coin(ctx); let flash_loan = FlashLoan { @@ -140,7 +173,7 @@ public(package) fun borrow_flashloan_quote( ): (Coin, FlashLoan) { assert!(borrow_quantity > 0, EInvalidLoanQuantity); assert!(self.quote_balance.value() >= borrow_quantity, ENotEnoughQuoteForLoan); - let borrow_type_name = type_name::get(); + let borrow_type_name = type_name::with_defining_ids(); let borrow: Coin = self.quote_balance.split(borrow_quantity).into_coin(ctx); let flash_loan = FlashLoan { @@ -165,7 +198,10 @@ public(package) fun return_flashloan_base( flash_loan: FlashLoan, ) { assert!(pool_id == flash_loan.pool_id, EIncorrectLoanPool); - assert!(type_name::get() == flash_loan.type_name, EIncorrectTypeReturned); + assert!( + type_name::with_defining_ids() == flash_loan.type_name, + EIncorrectTypeReturned, + ); assert!(coin.value() == flash_loan.borrow_quantity, EIncorrectQuantityReturned); self.base_balance.join(coin.into_balance()); @@ -184,7 +220,10 @@ public(package) fun return_flashloan_quote( flash_loan: FlashLoan, ) { assert!(pool_id == flash_loan.pool_id, EIncorrectLoanPool); - assert!(type_name::get() == flash_loan.type_name, EIncorrectTypeReturned); + assert!( + type_name::with_defining_ids() == flash_loan.type_name, + EIncorrectTypeReturned, + ); assert!(coin.value() == flash_loan.borrow_quantity, EIncorrectQuantityReturned); self.quote_balance.join(coin.into_balance()); diff --git a/packages/deepbook/tests/balance_manager_tests.move b/packages/deepbook/tests/balance_manager_tests.move index 185b51dc0..663ffe4ac 100644 --- a/packages/deepbook/tests/balance_manager_tests.move +++ b/packages/deepbook/tests/balance_manager_tests.move @@ -4,7 +4,18 @@ #[test_only] module deepbook::balance_manager_tests; -use deepbook::balance_manager::{Self, BalanceManager, TradeCap, DepositCap, WithdrawCap}; +use deepbook::{ + balance_manager::{ + Self, + BalanceManager, + TradeCap, + DepositCap, + WithdrawCap, + DeepBookPoolReferral + }, + registry +}; +use std::unit_test::destroy; use sui::{coin::mint_for_testing, sui::SUI, test_scenario::{Scenario, begin, end, return_shared}}; use token::deep::DEEP; @@ -12,6 +23,9 @@ public struct SPAM has store {} public struct USDC has store {} public struct USDT has store {} +// Unauthorized app for testing +public struct UnauthorizedApp has drop {} + #[test] fun test_deposit_ok() { let mut test = begin(@0xF); @@ -40,6 +54,40 @@ fun test_deposit_ok() { end(test); } +#[test] +fun test_deposit_custom_manager_ok() { + let mut test = begin(@0xF); + let alice = @0xA; + let bob = @0xB; + test.next_tx(alice); + { + let balance_manager = balance_manager::new_with_custom_owner(bob, test.ctx()); + assert!(balance_manager.owner() == bob, 0); + transfer::public_share_object(balance_manager); + }; + test.next_tx(bob); + { + let mut balance_manager = test.take_shared(); + balance_manager.deposit( + mint_for_testing(100, test.ctx()), + test.ctx(), + ); + let balance = balance_manager.balance(); + assert!(balance == 100, 0); + + balance_manager.deposit( + mint_for_testing(100, test.ctx()), + test.ctx(), + ); + let balance = balance_manager.balance(); + assert!(balance == 200, 0); + + return_shared(balance_manager); + }; + + end(test); +} + #[test, expected_failure(abort_code = balance_manager::EInvalidOwner)] fun test_deposit_as_owner_e() { let mut test = begin(@0xF); @@ -50,7 +98,7 @@ fun test_deposit_as_owner_e() { test.next_tx(alice); { let balance_manager = balance_manager::new(test.ctx()); - balance_manager_id = object::id(&balance_manager); + balance_manager_id = balance_manager.id(); transfer::public_share_object(balance_manager); }; @@ -79,7 +127,7 @@ fun test_remove_trader_e() { test.next_tx(alice); { let mut balance_manager = balance_manager::new(test.ctx()); - balance_manager_id = object::id(&balance_manager); + balance_manager_id = balance_manager.id(); let trade_cap = balance_manager.mint_trade_cap(test.ctx()); trade_cap_id = object::id(&trade_cap); transfer::public_transfer(trade_cap, bob); @@ -108,7 +156,7 @@ fun test_deposit_with_removed_trader_e() { test.next_tx(alice); { let mut balance_manager = balance_manager::new(test.ctx()); - balance_manager_id = object::id(&balance_manager); + balance_manager_id = balance_manager.id(); let trade_cap = balance_manager.mint_trade_cap(test.ctx()); let trade_proof = balance_manager.generate_proof_as_trader( &trade_cap, @@ -158,7 +206,7 @@ fun test_deposit_with_removed_deposit_cap_e() { test.next_tx(alice); { let mut balance_manager = balance_manager::new(test.ctx()); - balance_manager_id = object::id(&balance_manager); + balance_manager_id = balance_manager.id(); let deposit_cap = balance_manager.mint_deposit_cap(test.ctx()); deposit_cap_id = object::id(&deposit_cap); @@ -236,7 +284,7 @@ fun test_deposit_with_deposit_cap_ok() { test.next_tx(alice); { let mut balance_manager = balance_manager::new(test.ctx()); - balance_manager_id = object::id(&balance_manager); + balance_manager_id = balance_manager.id(); let deposit_cap = balance_manager.mint_deposit_cap(test.ctx()); balance_manager.deposit_with_cap( @@ -283,7 +331,7 @@ fun test_withdraw_with_removed_withdraw_cap_e() { test.next_tx(alice); { let mut balance_manager = balance_manager::new(test.ctx()); - balance_manager_id = object::id(&balance_manager); + balance_manager_id = balance_manager.id(); let withdraw_cap = balance_manager.mint_withdraw_cap(test.ctx()); withdraw_cap_id = object::id(&withdraw_cap); balance_manager.deposit( @@ -374,7 +422,7 @@ fun test_withdraw_with_withdraw_cap_ok() { test.next_tx(alice); { let mut balance_manager = balance_manager::new(test.ctx()); - balance_manager_id = object::id(&balance_manager); + balance_manager_id = balance_manager.id(); let withdraw_cap = balance_manager.mint_withdraw_cap(test.ctx()); balance_manager.deposit( mint_for_testing(1000, test.ctx()), @@ -493,6 +541,166 @@ fun test_withdraw_balance_too_low_e() { abort 0 } +#[test] +fun test_referral_ok() { + let mut test = begin(@0xF); + let alice = @0xA; + let referral_id1; + let referral_id2; + let pool_address = @0xD; + let pool_id = pool_address.to_id(); + + // Second pool for testing multiple pools with same balance manager + let pool_address2 = @0xE; + let pool_id2 = pool_address2.to_id(); + let referral_id3; + let referral_id4; + + test.next_tx(alice); + { + referral_id1 = balance_manager::mint_referral(pool_id, test.ctx()); + referral_id2 = balance_manager::mint_referral(pool_id, test.ctx()); + referral_id3 = balance_manager::mint_referral(pool_id2, test.ctx()); + referral_id4 = balance_manager::mint_referral(pool_id2, test.ctx()); + }; + + test.next_tx(alice); + { + let referral1 = test.take_shared_by_id(referral_id1); + assert!(referral1.balance_manager_referral_owner() == alice); + let referral2 = test.take_shared_by_id(referral_id2); + assert!(referral2.balance_manager_referral_owner() == alice); + let referral3 = test.take_shared_by_id(referral_id3); + assert!(referral3.balance_manager_referral_owner() == alice); + let referral4 = test.take_shared_by_id(referral_id4); + assert!(referral4.balance_manager_referral_owner() == alice); + + let mut balance_manager = balance_manager::new(test.ctx()); + let trade_cap = balance_manager.mint_trade_cap(test.ctx()); + + // Set referral for pool 1 + balance_manager.set_balance_manager_referral(&referral1, &trade_cap); + assert!( + balance_manager.get_balance_manager_referral_id(pool_id) == option::some(referral_id1), + ); + // Pool 2 should still have no referral + assert!(balance_manager.get_balance_manager_referral_id(pool_id2) == option::none()); + + // Set referral for pool 2 + balance_manager.set_balance_manager_referral(&referral3, &trade_cap); + assert!( + balance_manager.get_balance_manager_referral_id(pool_id2) == option::some(referral_id3), + ); + // Pool 1 referral should be unchanged + assert!( + balance_manager.get_balance_manager_referral_id(pool_id) == option::some(referral_id1), + ); + + // Update referral for pool 1 + balance_manager.set_balance_manager_referral(&referral2, &trade_cap); + assert!( + balance_manager.get_balance_manager_referral_id(pool_id) == option::some(referral_id2), + ); + // Pool 2 referral should be unchanged + assert!( + balance_manager.get_balance_manager_referral_id(pool_id2) == option::some(referral_id3), + ); + + // Update referral for pool 2 + balance_manager.set_balance_manager_referral(&referral4, &trade_cap); + assert!( + balance_manager.get_balance_manager_referral_id(pool_id2) == option::some(referral_id4), + ); + // Pool 1 referral should be unchanged + assert!( + balance_manager.get_balance_manager_referral_id(pool_id) == option::some(referral_id2), + ); + + // Unset referral for pool 1 + balance_manager.unset_balance_manager_referral(pool_id, &trade_cap); + assert!(balance_manager.get_balance_manager_referral_id(pool_id) == option::none()); + // Pool 2 referral should be unchanged + assert!( + balance_manager.get_balance_manager_referral_id(pool_id2) == option::some(referral_id4), + ); + + // Unset referral for pool 2 + balance_manager.unset_balance_manager_referral(pool_id2, &trade_cap); + assert!(balance_manager.get_balance_manager_referral_id(pool_id2) == option::none()); + // Pool 1 referral should still be none + assert!(balance_manager.get_balance_manager_referral_id(pool_id) == option::none()); + + transfer::public_share_object(balance_manager); + return_shared(referral1); + return_shared(referral2); + return_shared(referral3); + return_shared(referral4); + destroy(trade_cap); + }; + + end(test); +} + +#[test] +fun test_unset_no_referral_ok() { + let mut test = begin(@0xF); + let alice = @0xA; + let pool_address = @0xD; + let pool_id = pool_address.to_id(); + test.next_tx(alice); + { + let mut balance_manager = balance_manager::new(test.ctx()); + let trade_cap = balance_manager.mint_trade_cap(test.ctx()); + balance_manager.unset_balance_manager_referral(pool_id, &trade_cap); + assert!(balance_manager.get_balance_manager_referral_id(pool_id) == option::none(), 0); + + transfer::public_share_object(balance_manager); + destroy(trade_cap); + }; + + end(test); +} + +#[test, expected_failure(abort_code = registry::EAppNotAuthorized)] +fun test_unauthorized_custom_owner_creation_e() { + let mut test = begin(@0xF); + let alice = @0xA; + let victim = @0xB; + let registry_id; + + test.next_tx(alice); + { + registry_id = registry::test_registry(test.ctx()); + }; + + // Attempt to use unauthorized app + test.next_tx(alice); + { + let deepbook_registry = test.take_shared_by_id(registry_id); + + // Attempt to create a BalanceManager with custom owner using unauthorized app + // This should fail with EAppNotAuthorized since UnauthorizedApp is not registered + let ( + balance_manager, + deposit_cap, + withdraw_cap, + trade_cap, + ) = balance_manager::new_with_custom_owner_caps( + &deepbook_registry, + victim, + test.ctx(), + ); + + transfer::public_share_object(balance_manager); + destroy(deposit_cap); + destroy(withdraw_cap); + destroy(trade_cap); + return_shared(deepbook_registry); + }; + + abort 0 +} + public(package) fun deposit_into_account( balance_manager: &mut BalanceManager, amount: u64, @@ -519,13 +727,59 @@ public(package) fun create_acct_and_share_with_funds( deposit_into_account(&mut balance_manager, amount, test); let trade_cap = balance_manager.mint_trade_cap(test.ctx()); transfer::public_transfer(trade_cap, sender); - let id = object::id(&balance_manager); + let id = balance_manager.id(); + transfer::public_share_object(balance_manager); + + id + } +} + +public(package) fun create_acct_only_deep_and_share_with_funds( + sender: address, + amount: u64, + test: &mut Scenario, +): ID { + test.next_tx(sender); + { + let mut balance_manager = balance_manager::new(test.ctx()); + deposit_into_account(&mut balance_manager, amount, test); + let trade_cap = balance_manager.mint_trade_cap(test.ctx()); + transfer::public_transfer(trade_cap, sender); + let id = balance_manager.id(); transfer::public_share_object(balance_manager); id } } +public(package) fun create_caps(sender: address, balance_manager_id: ID, test: &mut Scenario) { + test.next_tx(sender); + { + let mut balance_manager = test.take_shared_by_id(balance_manager_id); + let deposit_cap = balance_manager.mint_deposit_cap(test.ctx()); + let withdraw_cap = balance_manager.mint_withdraw_cap(test.ctx()); + let trade_cap = balance_manager.mint_trade_cap(test.ctx()); + transfer::public_transfer(deposit_cap, sender); + transfer::public_transfer(withdraw_cap, sender); + transfer::public_transfer(trade_cap, sender); + return_shared(balance_manager); + } +} + +public(package) fun asset_balance( + sender: address, + balance_manager_id: ID, + test: &mut Scenario, +): u64 { + test.next_tx(sender); + { + let balance_manager = test.take_shared_by_id(balance_manager_id); + let balance = balance_manager.balance(); + return_shared(balance_manager); + balance + } +} + public(package) fun create_acct_and_share_with_funds_typed< BaseAsset, QuoteAsset, @@ -553,7 +807,7 @@ public(package) fun create_acct_and_share_with_funds_typed< ); let trade_cap = balance_manager.mint_trade_cap(test.ctx()); transfer::public_transfer(trade_cap, sender); - let id = object::id(&balance_manager); + let id = balance_manager.id(); transfer::public_share_object(balance_manager); id diff --git a/packages/deepbook/tests/book/order_info_tests.move b/packages/deepbook/tests/book/order_info_tests.move index 62b35c98b..fca34050a 100644 --- a/packages/deepbook/tests/book/order_info_tests.move +++ b/packages/deepbook/tests/book/order_info_tests.move @@ -5,7 +5,8 @@ module deepbook::order_info_tests; use deepbook::{balances, constants, deep_price, order_info::{Self, OrderInfo}, utils}; -use sui::{object::id_from_address, test_scenario::{next_tx, begin, end}, test_utils::assert_eq}; +use std::unit_test::assert_eq; +use sui::{object::id_from_address, test_scenario::{next_tx, begin, end}}; const OWNER: address = @0xF; const ALICE: address = @0xA; @@ -33,11 +34,8 @@ fun calculate_partial_fill_balances_ok() { constants::maker_fee(), ); - assert_eq(settled, balances::new(0, 0, 0)); - assert_eq( - owed, - balances::new(0, 1 * constants::usdc_unit(), 500_000), - ); // 5 bps of 1 SUI paid in DEEP + assert_eq!(settled, balances::new(0, 0, 0)); + assert_eq!(owed, balances::new(0, 1 * constants::usdc_unit(), 500_000)); // 5 bps of 1 SUI paid in DEEP end(test); } @@ -64,11 +62,8 @@ fun calculate_partial_fill_balances_precision_ok() { constants::maker_fee(), ); - assert_eq(settled, balances::new(0, 0, 0)); - assert_eq( - owed, - balances::new(0, 12_340_000, 5_000_000), - ); // 5 bps of 10 SUI paid in DEEP + assert_eq!(settled, balances::new(0, 0, 0)); + assert_eq!(owed, balances::new(0, 12_340_000, 5_000_000)); // 5 bps of 10 SUI paid in DEEP end(test); } @@ -93,10 +88,10 @@ fun calculate_partial_fill_balances_precision2_ok() { constants::maker_fee(), ); - assert_eq(settled, balances::new(0, 0, 0)); + assert_eq!(settled, balances::new(0, 0, 0)); // USDC owed = 1.234 * 10.86 = 13.40124 = 13401240 // DEEP owed = 10.86 * 0.0005 = 0.00543 = 5430000 (9 decimals in DEEP) - assert_eq(owed, balances::new(0, 13401240, 5430000)); + assert_eq!(owed, balances::new(0, 13401240, 5430000)); end(test); } @@ -121,10 +116,10 @@ fun calculate_partial_fill_balances_ask_no_fill_ok() { constants::maker_fee(), ); - assert_eq(settled, balances::new(0, 0, 0)); + assert_eq!(settled, balances::new(0, 0, 0)); // Since its an ask, transfer quantity amount worth of base token. // DEEP owed = 655.36 * 0.0005 = 0.32768 = 327680000 (9 decimals in DEEP) - assert_eq(owed, balances::new(655_360_000_000, 0, 327_680_000)); + assert_eq!(owed, balances::new(655_360_000_000, 0, 327_680_000)); end(test); } @@ -245,8 +240,8 @@ fun match_maker_multiple_ask_ok() { constants::maker_fee(), ); - assert_eq(settled, balances::new(0, 2_002_002, 0)); - assert_eq(owed, balances::new(10_000_000_000, 0, 6_000_500)); + assert_eq!(settled, balances::new(0, 2_002_002, 0)); + assert_eq!(owed, balances::new(10_000_000_000, 0, 6_000_500)); end(test); } @@ -323,7 +318,7 @@ fun calculate_partial_fill_balances_bid_partial_fill_ok() { ); // 100 SUI filled, the taker is owed 100 SUI. - assert_eq(settled, balances::new(100_000_000_000, 0, 0)); + assert_eq!(settled, balances::new(100_000_000_000, 0, 0)); // Taker paid 181305 USDC for 100 SUI, so they owe 181305 USDC. // The remaining 31.11 SUI is placed as a maker order at $1900 // Additional owed to create maker order 31.11 * 1900 = 59109 USDC. @@ -332,7 +327,7 @@ fun calculate_partial_fill_balances_bid_partial_fill_ok() { // Taker fee = 0.001 * 100 = 0.1 DEEP // Maker fee = 0.0005 * 31.11 = 0.015555 // Total fees owed = 0.1 + 0.015555 = 0.115555 = 115555000 - assert_eq(owed, balances::new(0, 240_414_000_000, 115_555_000)); + assert_eq!(owed, balances::new(0, 240_414_000_000, 115_555_000)); end(test); } @@ -374,14 +369,14 @@ fun calculate_partial_fill_balances_ask_partial_fill_ok() { ); // Sell of 0.001 SUI filled at $70,000, taker is owed 70 USDC - assert_eq(settled, balances::new(0, 70_000_000, 0)); + assert_eq!(settled, balances::new(0, 70_000_000, 0)); // Taker paid 70 USDC for 0.001 SUI, so they owe 70 USDC. // The remaining 0.004 SUI is placed as a maker order at $68,191.55 // Taker fee = 0.001 * 0.001 = 0.000001 DEEP // Maker fee = 0.0005 * 0.004 = 0.000002 DEEP // Total fees owed = 0.000003 DEEP = 3000 - assert_eq(owed, balances::new(5_000_000, 0, 3_000)); + assert_eq!(owed, balances::new(5_000_000, 0, 3_000)); end(test); } diff --git a/packages/deepbook/tests/book/order_tests.move b/packages/deepbook/tests/book/order_tests.move index 1364d6fa7..1401ecdcc 100644 --- a/packages/deepbook/tests/book/order_tests.move +++ b/packages/deepbook/tests/book/order_tests.move @@ -5,7 +5,8 @@ module deepbook::order_tests; use deepbook::{balances, constants, deep_price, order::{Self, Order}, utils}; -use sui::{object::id_from_address, test_scenario::{next_tx, begin, end}, test_utils::assert_eq}; +use std::unit_test::assert_eq; +use sui::{object::id_from_address, test_scenario::{next_tx, begin, end}}; const OWNER: address = @0xF; const ALICE: address = @0xA; @@ -27,7 +28,7 @@ fun generate_fill_partial_fill_ok() { assert!(fill.base_quantity() == 5 * constants::sui_unit(), 0); assert!(fill.taker_is_bid(), 0); assert!(fill.quote_quantity() == 75 * constants::usdc_unit(), 0); // 5 * $15 = $75 - assert_eq( + assert_eq!( fill.get_settled_maker_quantities(), balances::new(0, 75 * constants::usdc_unit(), 0), ); @@ -54,7 +55,7 @@ fun generate_fill_multiple_partial_fill_ok() { assert!(fill.base_quantity() == 5 * constants::sui_unit(), 0); assert!(fill.taker_is_bid(), 0); assert!(fill.quote_quantity() == 75 * constants::usdc_unit(), 0); // 5 * $15 = $75 - assert_eq( + assert_eq!( fill.get_settled_maker_quantities(), balances::new(0, 75 * constants::usdc_unit(), 0), ); @@ -68,7 +69,7 @@ fun generate_fill_multiple_partial_fill_ok() { assert!(fill.base_quantity() == 7 * constants::sui_unit(), 0); assert!(fill.taker_is_bid(), 0); assert!(fill.quote_quantity() == 105 * constants::usdc_unit(), 0); // 7 * $15 = $105 - assert_eq( + assert_eq!( fill.get_settled_maker_quantities(), balances::new(0, 105 * constants::usdc_unit(), 0), ); @@ -102,10 +103,7 @@ fun generate_fill_full_fill_ok() { assert!(fill.base_quantity() == 1 * constants::sui_unit() / 10, 0); assert!(fill.taker_is_bid(), 0); assert!(fill.quote_quantity() == 11_111_000, 0); // 0.1 * $111.11 = $11.111 - assert_eq( - fill.get_settled_maker_quantities(), - balances::new(0, 11_111_000, 0), - ); + assert_eq!(fill.get_settled_maker_quantities(), balances::new(0, 11_111_000, 0)); assert!(order.status() == constants::filled(), 0); assert!(order.filled_quantity() == 1 * constants::sui_unit() / 10, 0); @@ -136,7 +134,7 @@ fun generate_fill_partial_fill_ok_bid() { assert!(fill.base_quantity() == 1 * constants::sui_unit() / 100, 0); assert!(!fill.taker_is_bid(), 0); assert!(fill.quote_quantity() == 11_900, 0); // 0.01 * $1.19 = $0.0119 - assert_eq( + assert_eq!( fill.get_settled_maker_quantities(), balances::new(1 * constants::sui_unit() / 100, 0, 0), ); @@ -164,7 +162,7 @@ fun generate_fill_self_match_expire_ok() { assert!(!fill.completed(), 0); assert!(fill.base_quantity() == 10 * constants::sui_unit(), 0); assert!(fill.quote_quantity() == 100 * constants::usdc_unit(), 0); - assert_eq( + assert_eq!( fill.get_settled_maker_quantities(), balances::new(10 * constants::sui_unit(), 0, 0), ); @@ -214,7 +212,7 @@ fun generate_fill_expired_ok() { assert!(!fill.completed(), 0); assert!(fill.base_quantity() == 10 * constants::sui_unit(), 0); assert!(fill.quote_quantity() == 100 * constants::usdc_unit(), 0); - assert_eq( + assert_eq!( fill.get_settled_maker_quantities(), balances::new(0, 100 * constants::usdc_unit(), 0), ); @@ -263,10 +261,7 @@ fun generate_fill_expired_partial_ok() { assert!(!fill.completed(), 0); assert!(fill.base_quantity() == 5 * constants::sui_unit(), 0); assert!(fill.quote_quantity() == 50 * constants::usdc_unit(), 0); // 5 * $10 = $50 - assert_eq( - fill.get_settled_maker_quantities(), - balances::new(5 * constants::sui_unit(), 0, 0), - ); + assert_eq!(fill.get_settled_maker_quantities(), balances::new(5 * constants::sui_unit(), 0, 0)); assert!(order.status() == constants::partially_filled(), 0); assert!(order.filled_quantity() == 5 * constants::sui_unit(), 0); @@ -282,7 +277,7 @@ fun generate_fill_expired_partial_ok() { assert!(!fill.completed(), 0); assert!(fill.base_quantity() == 5 * constants::sui_unit(), 0); assert!(fill.quote_quantity() == 50 * constants::usdc_unit(), 0); - assert_eq( + assert_eq!( fill.get_settled_maker_quantities(), balances::new(0, 50 * constants::usdc_unit(), 0), ); diff --git a/packages/deepbook/tests/master_tests.move b/packages/deepbook/tests/master_tests.move index fea5f624f..50ce3f695 100644 --- a/packages/deepbook/tests/master_tests.move +++ b/packages/deepbook/tests/master_tests.move @@ -196,6 +196,141 @@ fun test_locked_balance_ask_ok() { test_locked_balance(false) } +#[test] +fun test_withdraw_settled_amounts_permissionless_ok() { + let mut test = begin(OWNER); + let registry_id = pool_tests::setup_test(OWNER, &mut test); + pool_tests::set_time(0, &mut test); + + let starting_balance = 10000 * constants::float_scaling(); + let alice_balance_manager_id = balance_manager_tests::create_acct_and_share_with_funds( + ALICE, + starting_balance, + &mut test, + ); + let bob_balance_manager_id = balance_manager_tests::create_acct_and_share_with_funds( + BOB, + starting_balance, + &mut test, + ); + + let pool_id = pool_tests::setup_pool_with_default_fees( + OWNER, + registry_id, + false, + false, + &mut test, + ); + + let price = 2 * constants::float_scaling(); + let quantity = 5 * constants::float_scaling(); + let client_order_id = 1; + let order_type = constants::no_restriction(); + let expire_timestamp = constants::max_u64(); + let pay_with_deep = false; + + // Alice places a bid order + pool_tests::place_limit_order( + ALICE, + pool_id, + alice_balance_manager_id, + client_order_id, + order_type, + constants::self_matching_allowed(), + price, + quantity, + true, // is_bid + pay_with_deep, + expire_timestamp, + &mut test, + ); + + // Bob places an ask order that matches Alice's bid + pool_tests::place_limit_order( + BOB, + pool_id, + bob_balance_manager_id, + client_order_id, + order_type, + constants::self_matching_allowed(), + price, + quantity, + false, // is_ask + pay_with_deep, + expire_timestamp, + &mut test, + ); + + // Check Alice's balance before withdrawal (settled amounts not yet withdrawn) + test.next_tx(ALICE); + let alice_sui_before = { + let alice_manager = test.take_shared_by_id( + alice_balance_manager_id, + ); + let balance = balance_manager::balance(&alice_manager); + return_shared(alice_manager); + balance + }; + + // Alice now has settled balances (received SUI from the trade) + // Bob (not the owner) calls withdraw_settled_amounts_permissionless for Alice + withdraw_settled_amounts_permissionless( + BOB, + pool_id, + alice_balance_manager_id, + &mut test, + ); + + // Verify Alice's balance increased by the traded quantity + test.next_tx(ALICE); + { + let alice_manager = test.take_shared_by_id( + alice_balance_manager_id, + ); + let alice_sui_after = balance_manager::balance(&alice_manager); + + // Alice should have received the full quantity (5 SUI) from her filled bid order + let expected_sui_received = quantity; + assert!(alice_sui_after == alice_sui_before + expected_sui_received, 0); + + return_shared(alice_manager); + }; + + test.end(); +} + +#[test, expected_failure(abort_code = ::deepbook::vault::ENoBalanceToSettle)] +fun test_withdraw_settled_amounts_permissionless_no_balance_e() { + let mut test = begin(OWNER); + let registry_id = pool_tests::setup_test(OWNER, &mut test); + pool_tests::set_time(0, &mut test); + + let starting_balance = 10000 * constants::float_scaling(); + let alice_balance_manager_id = balance_manager_tests::create_acct_and_share_with_funds( + ALICE, + starting_balance, + &mut test, + ); + + let pool_id = pool_tests::setup_pool_with_default_fees( + OWNER, + registry_id, + false, + false, + &mut test, + ); + + // Alice has no settled balances, try to withdraw + withdraw_settled_amounts_permissionless( + BOB, + pool_id, + alice_balance_manager_id, + &mut test, + ); + + test.end(); +} + // === Test Functions === fun test_locked_balance(is_bid: bool) { let mut test = begin(OWNER); @@ -904,7 +1039,7 @@ fun test_master(error_code: u64) { // Advance to epoch 28 let quantity = 1 * constants::float_scaling(); - let mut i = 23; + let mut i = 23u64; // For 23 epochs, Alice and Bob will both make 1 quantity per epoch, and // should get the full rebate // Alice will place a bid for quantity 1, bob will place ask for quantity 2, @@ -917,7 +1052,7 @@ fun test_master(error_code: u64) { // Total fees collected should be 0.065% for each epoch // Alice should have 46 more SUI at the end of the loop // Bob should have 92 more USDC at the end of the loop - while (i > 0) { + while (i > 0u64) { test.next_epoch(OWNER); execute_cross_trading( pool1_id, @@ -1640,7 +1775,7 @@ fun test_master_input_tokens(error_code: u64) { // Advance to epoch 28 let quantity = 1 * constants::float_scaling(); - let mut i = 23; + let mut i = 23u64; // For 23 epochs, Alice and Bob will both make 1 quantity per epoch, and // should get the full rebate // Alice will place a bid for quantity 1, bob will place ask for quantity 2, @@ -1652,7 +1787,7 @@ fun test_master_input_tokens(error_code: u64) { // 0.06% taker for Bob // Alice should have 46 more SUI at the end of the loop // Bob should have 92 more USDC at the end of the loop - while (i > 0) { + while (i > 0u64) { test.next_epoch(OWNER); execute_cross_trading( pool1_id, @@ -1918,6 +2053,31 @@ fun test_master_deep_price(error_code: u64) { // Trading within pool 1 should have no fees // Alice should get 2 more sui, Bob should lose 2 sui // Alice should get 200 less deep, Bob should get 200 deep + + // Alice places an order, then cancels + let order_info = pool_tests::place_limit_order( + ALICE, + pool1_id, + alice_balance_manager_id, + client_order_id, + order_type, + constants::self_matching_allowed(), + price, + quantity, + is_bid, + pay_with_deep, + expire_timestamp, + &mut test, + ); + + pool_tests::cancel_order( + ALICE, + pool1_id, + alice_balance_manager_id, + order_info.order_id(), + &mut test, + ); + execute_cross_trading( pool1_id, alice_balance_manager_id, @@ -3468,6 +3628,29 @@ fun withdraw_settled_amounts( } } +fun withdraw_settled_amounts_permissionless( + sender: address, + pool_id: ID, + balance_manager_id: ID, + test: &mut Scenario, +) { + test.next_tx(sender); + { + let mut my_manager = test.take_shared_by_id( + balance_manager_id, + ); + let mut pool = test.take_shared_by_id>( + pool_id, + ); + pool::withdraw_settled_amounts_permissionless( + &mut pool, + &mut my_manager, + ); + return_shared(my_manager); + return_shared(pool); + } +} + fun check_balance( balance_manager_id: ID, expected_balances: &ExpectedBalances, diff --git a/packages/deepbook/tests/order_query_tests.move b/packages/deepbook/tests/order_query_tests.move index 6c90307b1..1f0ca72dd 100644 --- a/packages/deepbook/tests/order_query_tests.move +++ b/packages/deepbook/tests/order_query_tests.move @@ -14,7 +14,8 @@ use deepbook::{ pool::Pool, pool_tests::{setup_test, setup_pool_with_default_fees_and_reference_pool, place_limit_order} }; -use sui::{sui::SUI, test_scenario::{begin, end, return_shared}, test_utils}; +use std::unit_test::destroy; +use sui::{sui::SUI, test_scenario::{begin, end, return_shared}}; use token::deep::DEEP; const OWNER: address = @0x1; @@ -158,6 +159,6 @@ fun test_place_orders_ok() { assert!(orders.orders().length() == 5); assert!(orders.has_next_page() == true); - test_utils::destroy(pool); + destroy(pool); end(test); } diff --git a/packages/deepbook/tests/pool_tests.move b/packages/deepbook/tests/pool_tests.move index 0194341d8..78cb67563 100644 --- a/packages/deepbook/tests/pool_tests.move +++ b/packages/deepbook/tests/pool_tests.move @@ -5,13 +5,23 @@ module deepbook::pool_tests; use deepbook::{ - balance_manager::{BalanceManager, TradeCap}, + balance_manager::{ + Self, + BalanceManager, + TradeCap, + DeepBookPoolReferral, + DepositCap, + WithdrawCap + }, balance_manager_tests::{ USDC, USDT, SPAM, create_acct_and_share_with_funds, - create_acct_and_share_with_funds_typed + create_acct_and_share_with_funds_typed, + create_acct_only_deep_and_share_with_funds, + create_caps, + asset_balance }, big_vector::BigVector, constants, @@ -23,12 +33,12 @@ use deepbook::{ registry::{Self, Registry}, utils }; +use std::unit_test::{assert_eq, destroy}; use sui::{ clock::{Self, Clock}, - coin::{Coin, mint_for_testing}, + coin::{Self, Coin, mint_for_testing}, sui::SUI, - test_scenario::{Scenario, begin, end, return_shared}, - test_utils + test_scenario::{Scenario, begin, end, return_shared} }; use token::deep::DEEP; @@ -449,12 +459,22 @@ fun test_self_matching_cancel_maker_ask() { #[test] fun test_swap_exact_amount_bid_ask() { - test_swap_exact_amount(true); + test_swap_exact_amount(true, false); } #[test] fun test_swap_exact_amount_ask_bid() { - test_swap_exact_amount(false); + test_swap_exact_amount(false, false); +} + +#[test] +fun test_swap_exact_amount_bid_ask_with_manager() { + test_swap_exact_amount(true, true); +} + +#[test] +fun test_swap_exact_amount_ask_bid_with_manager() { + test_swap_exact_amount(false, true); } #[test] @@ -467,6 +487,16 @@ fun test_swap_exact_amount_with_input_ask_bid() { test_swap_exact_amount_with_input(false); } +#[test] +fun test_get_quantity_out_input_fee_bid_ask_zero() { + test_get_quantity_out_zero(true); +} + +#[test] +fun test_get_quantity_out_input_fee_ask_bid_zero() { + test_get_quantity_out_zero(false); +} + #[test, expected_failure(abort_code = ::deepbook::big_vector::ENotFound)] fun test_cancel_all_orders_bid_e() { test_cancel_all_orders(true, true); @@ -564,42 +594,92 @@ fun test_mid_price_ok() { #[test] fun test_swap_exact_not_fully_filled_bid_ok() { - test_swap_exact_not_fully_filled(true, false, false, false); + test_swap_exact_not_fully_filled(true, false, false, false, false); +} + +#[test] +fun test_swap_exact_not_fully_filled_bid_with_manager_ok() { + test_swap_exact_not_fully_filled(true, false, false, false, true); } #[test] fun test_swap_exact_not_fully_filled_ask_ok() { - test_swap_exact_not_fully_filled(false, false, false, false); + test_swap_exact_not_fully_filled(false, false, false, false, false); +} + +#[test] +fun test_swap_exact_not_fully_filled_ask_with_manager_ok() { + test_swap_exact_not_fully_filled(false, false, false, false, true); } #[test] fun test_swap_exact_not_fully_filled_bid_low_qty_ok() { - test_swap_exact_not_fully_filled(true, true, false, false); + test_swap_exact_not_fully_filled(true, true, false, false, false); +} + +#[test] +fun test_swap_exact_not_fully_filled_bid_with_manager_low_qty_ok() { + test_swap_exact_not_fully_filled(true, true, false, false, true); } #[test] fun test_swap_exact_not_fully_filled_ask_low_qty_ok() { - test_swap_exact_not_fully_filled(false, true, false, false); + test_swap_exact_not_fully_filled(false, true, false, false, false); +} + +#[test] +fun test_swap_exact_not_fully_filled_ask_with_manager_low_qty_ok() { + test_swap_exact_not_fully_filled(false, true, false, false, true); } #[test, expected_failure(abort_code = ::deepbook::pool::EMinimumQuantityOutNotMet)] fun test_swap_exact_not_fully_filled_bid_min_e() { - test_swap_exact_not_fully_filled(true, false, true, false); + test_swap_exact_not_fully_filled(true, false, true, false, false); +} + +#[test, expected_failure(abort_code = ::deepbook::pool::EMinimumQuantityOutNotMet)] +fun test_swap_exact_not_fully_filled_bid_with_manager_min_e() { + test_swap_exact_not_fully_filled(true, false, true, false, true); } #[test, expected_failure(abort_code = ::deepbook::pool::EMinimumQuantityOutNotMet)] fun test_swap_exact_not_fully_filled_ask_min_e() { - test_swap_exact_not_fully_filled(false, false, true, false); + test_swap_exact_not_fully_filled(false, false, true, false, false); +} + +#[test, expected_failure(abort_code = ::deepbook::pool::EMinimumQuantityOutNotMet)] +fun test_swap_exact_not_fully_filled_ask_with_manager_min_e() { + test_swap_exact_not_fully_filled(false, false, true, false, true); } #[test] fun test_swap_exact_not_fully_filled_maker_partial_bid_ok() { - test_swap_exact_not_fully_filled(true, false, false, true); + test_swap_exact_not_fully_filled(true, false, false, true, false); +} + +#[test] +fun test_swap_exact_not_fully_filled_maker_partial_bid_with_manager_ok() { + test_swap_exact_not_fully_filled(true, false, false, true, true); } #[test] fun test_swap_exact_not_fully_filled_maker_partial_ask_ok() { - test_swap_exact_not_fully_filled(false, false, false, true); + test_swap_exact_not_fully_filled(false, false, false, true, false); +} + +#[test] +fun test_swap_exact_not_fully_filled_maker_partial_ask_with_manager_ok() { + test_swap_exact_not_fully_filled(false, false, false, true, true); +} + +#[test] +fun test_swap_with_manager_zero_base_out_ok() { + test_swap_with_manager_zero_out(true); +} + +#[test] +fun test_swap_with_manager_zero_quote_out_ok() { + test_swap_with_manager_zero_out(false); } #[test] @@ -892,6 +972,119 @@ fun test_get_order() { end(test); } +#[test] +fun test_place_cancel_whitelisted_pool() { + let mut test = begin(OWNER); + let registry_id = setup_test(OWNER, &mut test); + + let pool_id = setup_pool_with_default_fees( + OWNER, + registry_id, + true, + false, + &mut test, + ); + let balance_manager_id_alice = create_acct_and_share_with_funds( + ALICE, + 1000000 * constants::float_scaling(), + &mut test, + ); + + let order_info_1 = place_limit_order( + ALICE, + pool_id, + balance_manager_id_alice, + 1, + constants::no_restriction(), + constants::self_matching_allowed(), + 100 * constants::float_scaling(), + 1 * constants::float_scaling(), + true, + true, + constants::max_u64(), + &mut test, + ); + + cancel_order( + ALICE, + pool_id, + balance_manager_id_alice, + order_info_1.order_id(), + &mut test, + ); + + let order_info_2 = place_limit_order( + ALICE, + pool_id, + balance_manager_id_alice, + 1, + constants::no_restriction(), + constants::self_matching_allowed(), + 100 * constants::float_scaling(), + 1 * constants::float_scaling(), + true, + false, + constants::max_u64(), + &mut test, + ); + + cancel_order( + ALICE, + pool_id, + balance_manager_id_alice, + order_info_2.order_id(), + &mut test, + ); + + let order_info_3 = place_limit_order( + ALICE, + pool_id, + balance_manager_id_alice, + 1, + constants::no_restriction(), + constants::self_matching_allowed(), + 100 * constants::float_scaling(), + 1 * constants::float_scaling(), + false, + true, + constants::max_u64(), + &mut test, + ); + + cancel_order( + ALICE, + pool_id, + balance_manager_id_alice, + order_info_3.order_id(), + &mut test, + ); + + let order_info_4 = place_limit_order( + ALICE, + pool_id, + balance_manager_id_alice, + 1, + constants::no_restriction(), + constants::self_matching_allowed(), + 100 * constants::float_scaling(), + 1 * constants::float_scaling(), + false, + false, + constants::max_u64(), + &mut test, + ); + + cancel_order( + ALICE, + pool_id, + balance_manager_id_alice, + order_info_4.order_id(), + &mut test, + ); + + end(test); +} + #[test] fun test_get_orders() { let mut test = begin(OWNER); @@ -1547,7 +1740,7 @@ public(package) fun validate_open_orders( ); assert!( - pool.account_open_orders(&balance_manager).size() == + pool.account_open_orders(&balance_manager).length() == expected_open_orders, 1, ); @@ -1898,7 +2091,7 @@ fun test_order_limit(is_bid: bool) { &mut test, ); - num_orders = num_orders - 1; + num_orders = num_orders - 1u64; }; let match_quantity = 1000 * constants::float_scaling(); @@ -2114,7 +2307,7 @@ public(package) fun unregister_pool( ); return_shared(pool); return_shared(registry); - test_utils::destroy(admin_cap); + destroy(admin_cap); } } @@ -2239,6 +2432,7 @@ fun test_swap_exact_not_fully_filled( low_quantity: bool, minimum_enforced: bool, partially_filled_maker: bool, + with_manager: bool, ) { let mut test = begin(OWNER); let registry_id = setup_test(OWNER, &mut test); @@ -2364,30 +2558,79 @@ fun test_swap_exact_not_fully_filled( 0 }; + let initial_bob_balances = 1000000 * constants::float_scaling(); + let bob_balance_manager_id = create_acct_and_share_with_funds( + BOB, + initial_bob_balances, + &mut test, + ); + create_caps(BOB, bob_balance_manager_id, &mut test); + let bob_sui_balance_before = asset_balance(BOB, bob_balance_manager_id, &mut test); + let bob_usdc_balance_before = asset_balance(BOB, bob_balance_manager_id, &mut test); + let bob_deep_balance_before = asset_balance(BOB, bob_balance_manager_id, &mut test); + let (base_out, quote_out, deep_out) = if (is_bid) { - place_swap_exact_base_for_quote( - pool_id, - BOB, - base_in, - deep_in, - min_out, - &mut test, - ) + if (with_manager) { + let deep_out = coin::zero(test.ctx()); + let (base_out, quote_out) = place_exact_base_for_quote_with_manager( + pool_id, + BOB, + bob_balance_manager_id, + base_in, + min_out, + &mut test, + ); + + (base_out, quote_out, deep_out) + } else { + place_swap_exact_base_for_quote( + pool_id, + BOB, + base_in, + deep_in, + min_out, + &mut test, + ) + } } else { - place_swap_exact_quote_for_base( - pool_id, - BOB, - quote_in, - deep_in, - min_out, - &mut test, - ) + if (with_manager) { + let deep_out = coin::zero(test.ctx()); + let (base_out, quote_out) = place_exact_quote_for_base_with_manager( + pool_id, + BOB, + bob_balance_manager_id, + quote_in, + min_out, + &mut test, + ); + + (base_out, quote_out, deep_out) + } else { + place_swap_exact_quote_for_base( + pool_id, + BOB, + quote_in, + deep_in, + min_out, + &mut test, + ) + } }; + let bob_sui_balance_after = asset_balance(BOB, bob_balance_manager_id, &mut test); + let bob_usdc_balance_after = asset_balance(BOB, bob_balance_manager_id, &mut test); + let bob_deep_balance_after = asset_balance(BOB, bob_balance_manager_id, &mut test); if (low_quantity) { assert!(base_out.value() == base_in); assert!(quote_out.value() == quote_in); - assert!(deep_out.value() == deep_in); + if (with_manager) { + assert!(deep_out.value() == 0); + assert!(bob_sui_balance_before == bob_sui_balance_after); + assert!(bob_usdc_balance_before == bob_usdc_balance_after); + assert!(bob_deep_balance_before == bob_deep_balance_after); + } else { + assert!(deep_out.value() == deep_in); + }; } else if (!partially_filled_maker) { if (is_bid) { assert!( @@ -2409,14 +2652,27 @@ fun test_swap_exact_not_fully_filled( ); }; - assert!(deep_out.value() == residual, constants::e_order_info_mismatch()); + if (with_manager) { + assert!( + bob_deep_balance_before == bob_deep_balance_after + deep_in - residual, + constants::e_order_info_mismatch(), + ); + assert!( + deep_required == deep_required_2 && + deep_required == bob_deep_balance_before - bob_deep_balance_after, + constants::e_order_info_mismatch(), + ); + } else { + assert!(deep_out.value() == residual, constants::e_order_info_mismatch()); + assert!( + deep_required == deep_required_2 && + deep_required == deep_in - deep_out.value(), + constants::e_order_info_mismatch(), + ); + }; + assert!(base == base_2 && base == base_out.value(), constants::e_order_info_mismatch()); assert!(quote == quote_2 && quote == quote_out.value(), constants::e_order_info_mismatch()); - assert!( - deep_required == deep_required_2 && - deep_required == deep_in - deep_out.value(), - constants::e_order_info_mismatch(), - ); } else { if (is_bid) { assert!( @@ -2438,17 +2694,30 @@ fun test_swap_exact_not_fully_filled( ); }; - assert!( - deep_out.value() == constants::float_scaling() / 10 + residual, - constants::e_order_info_mismatch(), - ); + if (with_manager) { + assert!( + bob_deep_balance_before - bob_deep_balance_after == constants::float_scaling() / 10, + constants::e_order_info_mismatch(), + ); + assert!( + deep_required == deep_required_2 && + deep_required == bob_deep_balance_before - bob_deep_balance_after, + constants::e_order_info_mismatch(), + ) + } else { + assert!( + deep_out.value() == constants::float_scaling() / 10 + residual, + constants::e_order_info_mismatch(), + ); + assert!( + deep_required == deep_required_2 && + deep_required == deep_in - deep_out.value(), + constants::e_order_info_mismatch(), + ); + }; + assert!(base == base_2 && base == base_out.value(), constants::e_order_info_mismatch()); assert!(quote == quote_2 && quote == quote_out.value(), constants::e_order_info_mismatch()); - assert!( - deep_required == deep_required_2 && - deep_required == deep_in - deep_out.value(), - constants::e_order_info_mismatch(), - ); }; base_out.burn_for_testing(); @@ -3069,287 +3338,761 @@ fun place_and_cancel_order_empty_e() { end(test); } -#[test, expected_failure(abort_code = ::deepbook::order_info::EInvalidExpireTimestamp)] -/// Trying to place an order that's expiring should fail -fun place_order_expired_order_skipped() { +#[test] +fun mint_referral_ok() { let mut test = begin(OWNER); - let registry_id = setup_test(OWNER, &mut test); - let balance_manager_id_alice = create_acct_and_share_with_funds( - ALICE, - 1000000 * constants::float_scaling(), - &mut test, - ); - let pool_id = setup_pool_with_default_fees_and_reference_pool( - ALICE, - registry_id, - balance_manager_id_alice, - &mut test, - ); - set_time(100, &mut test); + let pool_id = setup_everything(&mut test); - let client_order_id = 1; - let order_type = constants::no_restriction(); - let price = 2 * constants::float_scaling(); - let quantity = 1 * constants::float_scaling(); - let expire_timestamp = 0; - let is_bid = true; - let pay_with_deep = true; + test.next_tx(ALICE); + { + let mut pool = test.take_shared_by_id>(pool_id); + let mut i = 1; + while (i <= 20) { + pool.mint_referral(100_000_000 * i, test.ctx()); + i = i + 1; + }; + return_shared(pool); + }; + + let referral_id; + test.next_tx(ALICE); + { + let mut pool = test.take_shared_by_id>(pool_id); + referral_id = pool.mint_referral(100_000_000, test.ctx()); + return_shared(pool); + }; + + test.next_tx(ALICE); + { + let pool = test.take_shared_by_id>(pool_id); + let referral = test.take_shared_by_id(referral_id); + let (base, quote, deep) = pool.get_pool_referral_balances(&referral); + assert!(base == 0, 0); + assert!(quote == 0, 0); + assert!(deep == 0, 0); + return_shared(referral); + return_shared(pool); + }; - place_limit_order( - ALICE, - pool_id, - balance_manager_id_alice, - client_order_id, - order_type, - constants::self_matching_allowed(), - price, - quantity, - is_bid, - pay_with_deep, - expire_timestamp, - &mut test, - ); end(test); } -fun test_cancel_all_orders(is_bid: bool, has_open_orders: bool) { +#[test, expected_failure(abort_code = ::deepbook::pool::EInvalidReferralMultiplier)] +fun mint_referral_max_multiplier_e() { let mut test = begin(OWNER); - let registry_id = setup_test(OWNER, &mut test); - let balance_manager_id_alice = create_acct_and_share_with_funds( - ALICE, - 1000000 * constants::float_scaling(), - &mut test, - ); - let pool_id = setup_pool_with_default_fees_and_reference_pool( - ALICE, - registry_id, - balance_manager_id_alice, - &mut test, - ); + let pool_id = setup_everything(&mut test); + test.next_tx(ALICE); + { + let mut pool = test.take_shared_by_id>(pool_id); + pool.mint_referral(2_100_000_000, test.ctx()); + }; - let client_order_id = 1; - let order_type = constants::no_restriction(); - let price = 2 * constants::float_scaling(); - let quantity = 1 * constants::float_scaling(); - let expire_timestamp = constants::max_u64(); - let pay_with_deep = true; - let mut order_info_1_id = 0; + abort (0) +} - if (has_open_orders) { - order_info_1_id = - place_limit_order( +#[test, expected_failure(abort_code = ::deepbook::pool::EInvalidReferralMultiplier)] +fun mint_referral_not_multiple_of_multiplier_e() { + let mut test = begin(OWNER); + let pool_id = setup_everything(&mut test); + test.next_tx(ALICE); + { + let mut pool = test.take_shared_by_id>(pool_id); + pool.mint_referral(100_000_001, test.ctx()); + }; + + abort (0) +} + +#[test, expected_failure(abort_code = ::deepbook::pool::EInvalidReferralMultiplier)] +fun test_update_deepbook_referral_multiplier_e() { + let mut test = begin(OWNER); + let pool_id = setup_everything(&mut test); + let referral_id; + test.next_tx(ALICE); + { + let mut pool = test.take_shared_by_id>(pool_id); + referral_id = pool.mint_referral(100_000_000, test.ctx()); + return_shared(pool); + }; + + test.next_tx(ALICE); + { + let mut pool = test.take_shared_by_id>(pool_id); + let referral = test.take_shared_by_id(referral_id); + pool.update_pool_referral_multiplier(&referral, 2_100_000_000, test.ctx()); + }; + + abort (0) +} + +#[test, expected_failure(abort_code = ::deepbook::balance_manager::EInvalidReferralOwner)] +fun test_update_deepbook_referral_multiplier_wrong_owner() { + let mut test = begin(OWNER); + let pool_id = setup_everything(&mut test); + let referral_id; + test.next_tx(ALICE); + { + let mut pool = test.take_shared_by_id>(pool_id); + referral_id = pool.mint_referral(100_000_000, test.ctx()); + return_shared(pool); + }; + + // BOB tries to update ALICE's referral multiplier + test.next_tx(BOB); + { + let mut pool = test.take_shared_by_id>(pool_id); + let referral = test.take_shared_by_id(referral_id); + pool.update_pool_referral_multiplier(&referral, 200_000_000, test.ctx()); + }; + + abort (0) +} + +#[test, expected_failure(abort_code = ::deepbook::balance_manager::EInvalidReferralOwner)] +fun test_claim_referral_rewards_wrong_owner() { + let mut test = begin(OWNER); + let pool_id = setup_everything(&mut test); + let referral_id; + test.next_tx(ALICE); + { + let mut pool = test.take_shared_by_id>(pool_id); + referral_id = pool.mint_referral(100_000_000, test.ctx()); + return_shared(pool); + }; + + // BOB tries to claim ALICE's referral rewards + test.next_tx(BOB); + { + let mut pool = test.take_shared_by_id>(pool_id); + let referral = test.take_shared_by_id(referral_id); + let (base, quote, deep) = pool.claim_pool_referral_rewards(&referral, test.ctx()); + destroy(base); + destroy(quote); + destroy(deep); + }; + + abort (0) +} + +#[test] +fun test_process_order_referral_ok() { + let mut test = begin(OWNER); + let pool_id = setup_everything(&mut test); + let referral_id; + test.next_tx(ALICE); + { + let mut pool = test.take_shared_by_id>(pool_id); + referral_id = pool.mint_referral(100_000_000, test.ctx()); + return_shared(pool); + }; + + let balance_manager_id_alice; + test.next_tx(ALICE); + { + balance_manager_id_alice = + create_acct_and_share_with_funds_typed( ALICE, - pool_id, - balance_manager_id_alice, - client_order_id, - order_type, - constants::self_matching_allowed(), - price, - quantity, - is_bid, - pay_with_deep, - expire_timestamp, + 1000000 * constants::float_scaling(), &mut test, - ).order_id(); + ); + }; - let client_order_id = 2; + test.next_tx(ALICE); + { + let mut balance_manager = test.take_shared_by_id(balance_manager_id_alice); + let referral = test.take_shared_by_id(referral_id); + let trade_cap = balance_manager.mint_trade_cap(test.ctx()); + balance_manager.set_balance_manager_referral(&referral, &trade_cap); + return_shared(balance_manager); + return_shared(referral); + destroy(trade_cap); + }; - let order_info_2_id = place_limit_order( + test.next_tx(ALICE); + { + let order_info = place_market_order( ALICE, pool_id, balance_manager_id_alice, - client_order_id, - order_type, + 1, constants::self_matching_allowed(), - price, - quantity, - is_bid, - pay_with_deep, - expire_timestamp, + 1_500_000_000, + true, + true, &mut test, - ).order_id(); + ); - borrow_order_ok( + assert_eq!(order_info.paid_fees(), 150_000_000); + }; + + test.next_tx(ALICE); + { + let pool = test.take_shared_by_id>(pool_id); + let referral = test.take_shared_by_id(referral_id); + let (base, quote, deep) = pool.get_pool_referral_balances(&referral); + assert_eq!(base, 0); + assert_eq!(quote, 0); + // 10bps fee, 0.1x multiplier + assert_eq!(deep, 15_000_000); + return_shared(referral); + return_shared(pool); + }; + + // increase multiplier from 0.1x to 2x + test.next_tx(ALICE); + { + let mut pool = test.take_shared_by_id>(pool_id); + let referral = test.take_shared_by_id(referral_id); + pool.update_pool_referral_multiplier(&referral, 2_000_000_000, test.ctx()); + return_shared(pool); + return_shared(referral); + }; + + test.next_tx(ALICE); + { + let order_info = place_market_order( + ALICE, pool_id, - order_info_1_id, + balance_manager_id_alice, + 1, + constants::self_matching_allowed(), + 1_500_000_000, + true, + true, &mut test, ); - borrow_order_ok( + assert_eq!(order_info.paid_fees(), 150_000_000); + }; + + test.next_tx(ALICE); + { + let pool = test.take_shared_by_id>(pool_id); + let referral = test.take_shared_by_id(referral_id); + let (base, quote, deep) = pool.get_pool_referral_balances(&referral); + assert_eq!(base, 0); + assert_eq!(quote, 0); + // 10bps fee, 2x multiplier = 300_000_000 + // + 10bps fee, 0.1x multiplier = 15_000_000 + assert_eq!(deep, 315_000_000); + return_shared(referral); + return_shared(pool); + }; + + test.next_tx(ALICE); + { + let order_info = place_market_order( + ALICE, pool_id, - order_info_2_id, + balance_manager_id_alice, + 1, + constants::self_matching_allowed(), + 1_500_000_000, + true, + false, &mut test, ); + + // fees paid in USDC = 1.5 filled @ $2 = 3_000_000_000 + // 10bps of that = 3_000_000 + // penalty 1.25x = 3_750_000 + assert_eq!(order_info.paid_fees(), 3_750_000); }; - cancel_all_orders( - pool_id, - ALICE, - balance_manager_id_alice, - &mut test, - ); + test.next_tx(ALICE); + { + let pool = test.take_shared_by_id>(pool_id); + let referral = test.take_shared_by_id(referral_id); + let (base, quote, deep) = pool.get_pool_referral_balances(&referral); + assert_eq!(base, 0); + // fees paid in USDC = 3_750_000 with 2x multiple = 7_500_000 + assert_eq!(quote, 7_500_000); + assert_eq!(deep, 315_000_000); + return_shared(referral); + return_shared(pool); + }; - if (has_open_orders) { - borrow_order_ok( + test.next_tx(ALICE); + { + let order_info = place_market_order( + ALICE, pool_id, - order_info_1_id, + balance_manager_id_alice, + 1, + constants::self_matching_allowed(), + 1_500_000_000, + false, + false, &mut test, ); + + // fees paid in SUI = 1.5 filled @ $1 = 1_500_000_000 + // 10bps of that = 1_500_000 + // penalty 1.25x = 1_875_000 + assert_eq!(order_info.paid_fees(), 1_875_000); + }; + + test.next_tx(ALICE); + { + let pool = test.take_shared_by_id>(pool_id); + let referral = test.take_shared_by_id(referral_id); + let (base, quote, deep) = pool.get_pool_referral_balances(&referral); + // fees paid in SUI = 1_875_000 with 2x multiple = 3_750_000 + assert_eq!(base, 3_750_000); + assert_eq!(quote, 7_500_000); + assert_eq!(deep, 315_000_000); + return_shared(referral); + return_shared(pool); }; + end(test); } -/// Alice places a bid order, Bob places a swap_exact_amount order -/// Make sure the assets returned to Bob are correct -/// Make sure expired orders are skipped over -fun test_swap_exact_amount(is_bid: bool) { +#[test] +fun test_referral_two_pools_comprehensive() { let mut test = begin(OWNER); + + // Setup registry let registry_id = setup_test(OWNER, &mut test); - let balance_manager_id_alice = create_acct_and_share_with_funds( + + // Alice creates balance manager with funds for both pools + let balance_manager_id_alice; + test.next_tx(ALICE); + { + balance_manager_id_alice = + create_acct_and_share_with_funds_typed( + ALICE, + 1000000 * constants::float_scaling(), + &mut test, + ); + }; + + // Also deposit USDT into Alice's balance manager for pool 2 + test.next_tx(ALICE); + { + let mut balance_manager = test.take_shared_by_id(balance_manager_id_alice); + balance_manager.deposit( + mint_for_testing(1000000 * constants::float_scaling(), test.ctx()), + test.ctx(), + ); + return_shared(balance_manager); + }; + + // Create reference pool (SUI/DEEP) with orders + let reference_pool_id = setup_reference_pool( ALICE, - 1000000 * constants::float_scaling(), + registry_id, + balance_manager_id_alice, + constants::deep_multiplier(), &mut test, ); - let pool_id = setup_pool_with_default_fees_and_reference_pool( - ALICE, + + set_time(0, &mut test); + + // Setup pool 1: SUI/USDC + let pool_id_1 = setup_pool_with_default_fees( + OWNER, registry_id, - balance_manager_id_alice, + false, + false, &mut test, ); - let alice_client_order_id = 1; - let alice_price = 2 * constants::float_scaling(); - let alice_quantity = 2 * constants::float_scaling(); - let expired_price = if (is_bid) { - 3 * constants::float_scaling() - } else { - 1 * constants::float_scaling() - }; + // Add deep price point for pool 1 + add_deep_price_point( + ALICE, + pool_id_1, + reference_pool_id, + &mut test, + ); + + // Place initial orders in pool 1 + let client_order_id = 1; + let order_type = constants::no_restriction(); let expire_timestamp = constants::max_u64(); - let expire_timestamp_e = get_time(&mut test) + 100; - let pay_with_deep = true; - let residual = constants::lot_size() - 1; + // Sell at $2 place_limit_order( ALICE, - pool_id, + pool_id_1, balance_manager_id_alice, - alice_client_order_id, - constants::no_restriction(), + client_order_id, + order_type, constants::self_matching_allowed(), - alice_price, - alice_quantity, - is_bid, - pay_with_deep, + 2 * constants::float_scaling(), + 1000 * constants::float_scaling(), + false, + true, expire_timestamp, &mut test, ); + // Buy at $1 place_limit_order( ALICE, - pool_id, + pool_id_1, balance_manager_id_alice, - alice_client_order_id, - constants::no_restriction(), + client_order_id, + order_type, constants::self_matching_allowed(), - expired_price, - alice_quantity, - is_bid, - pay_with_deep, - expire_timestamp_e, + 1 * constants::float_scaling(), + 1000 * constants::float_scaling(), + true, + true, + expire_timestamp, &mut test, ); - set_time(200, &mut test); + // Setup pool 2: SUI/USDT (shares SUI with reference pool SUI/DEEP) + let pool_id_2 = setup_pool_with_default_fees( + OWNER, + registry_id, + false, + false, + &mut test, + ); - let base_in = if (is_bid) { - 1 * constants::float_scaling() + residual - } else { - 0 - }; - let quote_in = if (is_bid) { - 0 - } else { - 2 * constants::float_scaling() + 2 * residual - }; - let deep_in = math::mul(constants::deep_multiplier(), constants::taker_fee()) + - residual; + // Add deep price point for pool 2 (reuse same reference pool) + add_deep_price_point( + ALICE, + pool_id_2, + reference_pool_id, + &mut test, + ); - let (base, quote, deep_required) = get_quantity_out( - pool_id, - base_in, - quote_in, + // Place initial orders in pool 2 + // Alice places sell order at $2 in pool 2 + place_limit_order( + ALICE, + pool_id_2, + balance_manager_id_alice, + client_order_id, + order_type, + constants::self_matching_allowed(), + 2 * constants::float_scaling(), + 1000 * constants::float_scaling(), + false, + true, + expire_timestamp, &mut test, ); - let (base_2, quote_2, deep_required_2) = if (is_bid) { - get_quote_quantity_out( - pool_id, - base_in, + // Alice places buy order at $1 in pool 2 + place_limit_order( + ALICE, + pool_id_2, + balance_manager_id_alice, + client_order_id, + order_type, + constants::self_matching_allowed(), + 1 * constants::float_scaling(), + 1000 * constants::float_scaling(), + true, + true, + expire_timestamp, + &mut test, + ); + + // Bob mints referral for pool 1 with 0.5x multiplier (500_000_000) + let referral_id_pool1; + test.next_tx(BOB); + { + let mut pool = test.take_shared_by_id>(pool_id_1); + referral_id_pool1 = pool.mint_referral(500_000_000, test.ctx()); + return_shared(pool); + }; + + // Bob mints referral for pool 2 with 1x multiplier (1_000_000_000) + let referral_id_pool2; + test.next_tx(BOB); + { + let mut pool = test.take_shared_by_id>(pool_id_2); + referral_id_pool2 = pool.mint_referral(1_000_000_000, test.ctx()); + return_shared(pool); + }; + + // Alice sets Bob's referrals on her balance manager + test.next_tx(ALICE); + { + let mut balance_manager = test.take_shared_by_id(balance_manager_id_alice); + let referral1 = test.take_shared_by_id(referral_id_pool1); + let referral2 = test.take_shared_by_id(referral_id_pool2); + let trade_cap = test.take_from_sender(); + + balance_manager.set_balance_manager_referral(&referral1, &trade_cap); + balance_manager.set_balance_manager_referral(&referral2, &trade_cap); + + // Verify referrals are set correctly + assert!( + balance_manager.get_balance_manager_referral_id(pool_id_1) == + option::some(referral_id_pool1), + ); + assert!( + balance_manager.get_balance_manager_referral_id(pool_id_2) == + option::some(referral_id_pool2), + ); + + return_shared(balance_manager); + return_shared(referral1); + return_shared(referral2); + test.return_to_sender(trade_cap); + }; + + // Alice trades in pool 1 (buy 1.5 SUI at $2) + test.next_tx(ALICE); + { + let order_info = place_market_order( + ALICE, + pool_id_1, + balance_manager_id_alice, + 1, + constants::self_matching_allowed(), + 1_500_000_000, // 1.5 SUI + true, + true, &mut test, - ) - } else { - get_base_quantity_out( - pool_id, - quote_in, + ); + // 10bps fee on 1.5 SUI = 150_000_000 DEEP + assert_eq!(order_info.paid_fees(), 150_000_000); + }; + + // Alice trades in pool 2 (buy 2.0 SUI at $2) + test.next_tx(ALICE); + { + let order_info = place_market_order( + ALICE, + pool_id_2, + balance_manager_id_alice, + 1, + constants::self_matching_allowed(), + 2_000_000_000, // 2.0 SUI + true, + true, &mut test, - ) + ); + // 10bps fee on 2.0 SUI = 200_000_000 DEEP + assert_eq!(order_info.paid_fees(), 200_000_000); }; - let (base_out, quote_out, deep_out) = if (is_bid) { - place_swap_exact_base_for_quote( + // Verify referral balances before claiming + // Pool 1: 150_000_000 fees * 0.5 multiplier = 75_000_000 DEEP + test.next_tx(BOB); + { + let pool = test.take_shared_by_id>(pool_id_1); + let referral = test.take_shared_by_id(referral_id_pool1); + let (base, quote, deep) = pool.get_pool_referral_balances(&referral); + assert_eq!(base, 0); + assert_eq!(quote, 0); + assert_eq!(deep, 75_000_000); // 150_000_000 * 0.5 = 75_000_000 + return_shared(referral); + return_shared(pool); + }; + + // Pool 2: 200_000_000 fees * 1.0 multiplier = 200_000_000 DEEP + test.next_tx(BOB); + { + let pool = test.take_shared_by_id>(pool_id_2); + let referral = test.take_shared_by_id(referral_id_pool2); + let (base, quote, deep) = pool.get_pool_referral_balances(&referral); + assert_eq!(base, 0); + assert_eq!(quote, 0); + assert_eq!(deep, 200_000_000); // 200_000_000 * 1.0 = 200_000_000 + return_shared(referral); + return_shared(pool); + }; + + // Bob claims rewards from pool 1 + test.next_tx(BOB); + { + let mut pool = test.take_shared_by_id>(pool_id_1); + let referral = test.take_shared_by_id(referral_id_pool1); + let (base, quote, deep) = pool.claim_pool_referral_rewards(&referral, test.ctx()); + + assert_eq!(base.value(), 0); + assert_eq!(quote.value(), 0); + assert_eq!(deep.value(), 75_000_000); + + destroy(base); + destroy(quote); + destroy(deep); + return_shared(referral); + return_shared(pool); + }; + + // Bob claims rewards from pool 2 + test.next_tx(BOB); + { + let mut pool = test.take_shared_by_id>(pool_id_2); + let referral = test.take_shared_by_id(referral_id_pool2); + let (base, quote, deep) = pool.claim_pool_referral_rewards(&referral, test.ctx()); + + assert_eq!(base.value(), 0); + assert_eq!(quote.value(), 0); + assert_eq!(deep.value(), 200_000_000); + + destroy(base); + destroy(quote); + destroy(deep); + return_shared(referral); + return_shared(pool); + }; + + // Verify balances are (0,0,0) after claiming + test.next_tx(BOB); + { + let pool = test.take_shared_by_id>(pool_id_1); + let referral = test.take_shared_by_id(referral_id_pool1); + let (base, quote, deep) = pool.get_pool_referral_balances(&referral); + assert_eq!(base, 0); + assert_eq!(quote, 0); + assert_eq!(deep, 0); + return_shared(referral); + return_shared(pool); + }; + + test.next_tx(BOB); + { + let pool = test.take_shared_by_id>(pool_id_2); + let referral = test.take_shared_by_id(referral_id_pool2); + let (base, quote, deep) = pool.get_pool_referral_balances(&referral); + assert_eq!(base, 0); + assert_eq!(quote, 0); + assert_eq!(deep, 0); + return_shared(referral); + return_shared(pool); + }; + + end(test); +} + +#[test] +fun test_enable_ewma_params_ok() { + let mut test = begin(OWNER); + let pool_id = setup_everything(&mut test); + let admin_cap = registry::get_admin_cap_for_testing(test.ctx()); + let clock = clock::create_for_testing(test.ctx()); + test.next_tx(ALICE); + { + let mut pool = test.take_shared_by_id>(pool_id); + pool.enable_ewma_state(&admin_cap, true, &clock, test.ctx()); + let ewma_state = pool.load_ewma_state(); + assert!(ewma_state.enabled(), 0); + assert!(ewma_state.alpha() == constants::default_ewma_alpha(), 1); + assert!(ewma_state.z_score_threshold() == constants::default_z_score_threshold(), 2); + assert!(ewma_state.additional_taker_fee() == constants::default_additional_taker_fee(), 3); + return_shared(pool); + }; + + test.next_tx(ALICE); + { + let mut pool = test.take_shared_by_id>(pool_id); + pool.set_ewma_params(&admin_cap, 10_000_000, 3_000_000_000, 1_000_000, &clock, test.ctx()); + let ewma_state = pool.load_ewma_state(); + assert!(ewma_state.enabled(), 0); + assert!(ewma_state.alpha() == 10_000_000, 1); + assert!(ewma_state.z_score_threshold() == 3_000_000_000, 2); + assert!(ewma_state.additional_taker_fee() == 1_000_000, 3); + return_shared(pool); + }; + + let balance_manager_id_alice; + test.next_tx(ALICE); + { + balance_manager_id_alice = + create_acct_and_share_with_funds_typed( + ALICE, + 1000000 * constants::float_scaling(), + &mut test, + ); + }; + + let gas_price = 1_000; + advance_scenario_with_gas_price(&mut test, gas_price, 1000); + test.next_tx(ALICE); + { + let order_info = place_market_order( + ALICE, pool_id, - BOB, - base_in, - deep_in, - 0, + balance_manager_id_alice, + 1, + constants::self_matching_allowed(), + 1_500_000_000, + true, + true, &mut test, - ) - } else { - place_swap_exact_quote_for_base( + ); + assert_eq!(order_info.paid_fees(), 150_000_000); + }; + + test.next_tx(ALICE); + { + let order_info = place_market_order( + ALICE, pool_id, - BOB, - quote_in, - deep_in, - 0, + balance_manager_id_alice, + 1, + constants::self_matching_allowed(), + 1_500_000_000, + true, + true, &mut test, - ) + ); + assert_eq!(order_info.paid_fees(), 150_000_000); }; - if (is_bid) { - assert!(base_out.value() == residual, constants::e_order_info_mismatch()); - assert!( - quote_out.value() == 2 * constants::float_scaling(), - constants::e_order_info_mismatch(), - ); - } else { - assert!( - base_out.value() == 1 * constants::float_scaling(), - constants::e_order_info_mismatch(), + // pay with high gas price + advance_scenario_with_gas_price(&mut test, gas_price * 5, 1000); + test.next_tx(ALICE); + { + let order_info = place_market_order( + ALICE, + pool_id, + balance_manager_id_alice, + 1, + constants::self_matching_allowed(), + 1_500_000_000, + true, + true, + &mut test, ); - assert!(quote_out.value() == 2 * residual, constants::e_order_info_mismatch()); + assert_eq!(order_info.paid_fees(), 300_000_000); }; - assert!(deep_out.value() == residual, constants::e_order_info_mismatch()); - assert!(base == base_2 && base == base_out.value(), constants::e_order_info_mismatch()); - assert!(quote == quote_2 && quote == quote_out.value(), constants::e_order_info_mismatch()); - assert!( - deep_required == deep_required_2 && - deep_required == deep_in - deep_out.value(), - constants::e_order_info_mismatch(), - ); + test.next_tx(ALICE); + { + let mut pool = test.take_shared_by_id>(pool_id); + pool.enable_ewma_state(&admin_cap, false, &clock, test.ctx()); + let ewma_state = pool.load_ewma_state(); + assert!(!ewma_state.enabled(), 0); + return_shared(pool); + }; - base_out.burn_for_testing(); - quote_out.burn_for_testing(); - deep_out.burn_for_testing(); + // pay with high gas price, but disabled ewma + advance_scenario_with_gas_price(&mut test, gas_price * 5, 1000); + test.next_tx(ALICE); + { + let order_info = place_market_order( + ALICE, + pool_id, + balance_manager_id_alice, + 1, + constants::self_matching_allowed(), + 1_500_000_000, + true, + true, + &mut test, + ); + assert_eq!(order_info.paid_fees(), 150_000_000); + }; + destroy(clock); + destroy(admin_cap); end(test); } -/// Alice places a bid order, Bob places a swap_exact_amount order -/// Make sure the assets returned to Bob are correct -/// Make sure expired orders are skipped over -fun test_swap_exact_amount_with_input(is_bid: bool) { +#[test, expected_failure(abort_code = ::deepbook::order_info::EInvalidExpireTimestamp)] +/// Trying to place an order that's expiring should fail +fun place_order_expired_order_skipped() { let mut test = begin(OWNER); let registry_id = setup_test(OWNER, &mut test); let balance_manager_id_alice = create_acct_and_share_with_funds( @@ -3357,49 +4100,175 @@ fun test_swap_exact_amount_with_input(is_bid: bool) { 1000000 * constants::float_scaling(), &mut test, ); - let pool_id = setup_pool_with_default_fees( + let pool_id = setup_pool_with_default_fees_and_reference_pool( ALICE, registry_id, - false, - false, + balance_manager_id_alice, &mut test, ); + set_time(100, &mut test); - let alice_client_order_id = 1; - let alice_price = 2 * constants::float_scaling(); - let alice_quantity = 2 * constants::float_scaling(); - let expired_price = if (is_bid) { - 3 * constants::float_scaling() - } else { - 1 * constants::float_scaling() - }; - let expire_timestamp = constants::max_u64(); - let expire_timestamp_e = get_time(&mut test) + 100; - let pay_with_deep = false; - let residual = constants::lot_size() - 1; - let input_fee_rate = math::mul( - constants::fee_penalty_multiplier(), - constants::taker_fee(), - ); + let client_order_id = 1; + let order_type = constants::no_restriction(); + let price = 2 * constants::float_scaling(); + let quantity = 1 * constants::float_scaling(); + let expire_timestamp = 0; + let is_bid = true; + let pay_with_deep = true; place_limit_order( ALICE, pool_id, balance_manager_id_alice, - alice_client_order_id, - constants::no_restriction(), + client_order_id, + order_type, constants::self_matching_allowed(), - alice_price, - alice_quantity, + price, + quantity, is_bid, pay_with_deep, expire_timestamp, &mut test, ); + end(test); +} - place_limit_order( +fun test_cancel_all_orders(is_bid: bool, has_open_orders: bool) { + let mut test = begin(OWNER); + let registry_id = setup_test(OWNER, &mut test); + let balance_manager_id_alice = create_acct_and_share_with_funds( ALICE, - pool_id, + 1000000 * constants::float_scaling(), + &mut test, + ); + let pool_id = setup_pool_with_default_fees_and_reference_pool( + ALICE, + registry_id, + balance_manager_id_alice, + &mut test, + ); + + let client_order_id = 1; + let order_type = constants::no_restriction(); + let price = 2 * constants::float_scaling(); + let quantity = 1 * constants::float_scaling(); + let expire_timestamp = constants::max_u64(); + let pay_with_deep = true; + let mut order_info_1_id = 0; + + if (has_open_orders) { + order_info_1_id = + place_limit_order( + ALICE, + pool_id, + balance_manager_id_alice, + client_order_id, + order_type, + constants::self_matching_allowed(), + price, + quantity, + is_bid, + pay_with_deep, + expire_timestamp, + &mut test, + ).order_id(); + + let client_order_id = 2; + + let order_info_2_id = place_limit_order( + ALICE, + pool_id, + balance_manager_id_alice, + client_order_id, + order_type, + constants::self_matching_allowed(), + price, + quantity, + is_bid, + pay_with_deep, + expire_timestamp, + &mut test, + ).order_id(); + + borrow_order_ok( + pool_id, + order_info_1_id, + &mut test, + ); + + borrow_order_ok( + pool_id, + order_info_2_id, + &mut test, + ); + }; + + cancel_all_orders( + pool_id, + ALICE, + balance_manager_id_alice, + &mut test, + ); + + if (has_open_orders) { + borrow_order_ok( + pool_id, + order_info_1_id, + &mut test, + ); + }; + end(test); +} + +/// Alice places a bid order, Bob places a swap_exact_amount order +/// Make sure the assets returned to Bob are correct +/// Make sure expired orders are skipped over +fun test_swap_exact_amount(is_bid: bool, with_manager: bool) { + let mut test = begin(OWNER); + let registry_id = setup_test(OWNER, &mut test); + let balance_manager_id_alice = create_acct_and_share_with_funds( + ALICE, + 1000000 * constants::float_scaling(), + &mut test, + ); + let pool_id = setup_pool_with_default_fees_and_reference_pool( + ALICE, + registry_id, + balance_manager_id_alice, + &mut test, + ); + + let alice_client_order_id = 1; + let alice_price = 2 * constants::float_scaling(); + let alice_quantity = 2 * constants::float_scaling(); + let expired_price = if (is_bid) { + 3 * constants::float_scaling() + } else { + 1 * constants::float_scaling() + }; + let expire_timestamp = constants::max_u64(); + let expire_timestamp_e = get_time(&mut test) + 100; + let pay_with_deep = true; + let residual = constants::lot_size() - 1; + + place_limit_order( + ALICE, + pool_id, + balance_manager_id_alice, + alice_client_order_id, + constants::no_restriction(), + constants::self_matching_allowed(), + alice_price, + alice_quantity, + is_bid, + pay_with_deep, + expire_timestamp, + &mut test, + ); + + place_limit_order( + ALICE, + pool_id, balance_manager_id_alice, alice_client_order_id, constants::no_restriction(), @@ -3415,18 +4284,19 @@ fun test_swap_exact_amount_with_input(is_bid: bool) { set_time(200, &mut test); let base_in = if (is_bid) { - math::mul(1 * constants::float_scaling(), constants::float_scaling() + input_fee_rate) + residual + 1 * constants::float_scaling() + residual } else { 0 }; let quote_in = if (is_bid) { 0 } else { - math::mul(2 * constants::float_scaling(),constants::float_scaling() + input_fee_rate) + 2 * residual + 2 * constants::float_scaling() + 2 * residual }; - let deep_in = 0; + let deep_in = math::mul(constants::deep_multiplier(), constants::taker_fee()) + + residual; - let (base, quote, deep_required) = get_quantity_out_input_fee( + let (base, quote, deep_required) = get_quantity_out( pool_id, base_in, quote_in, @@ -3434,38 +4304,76 @@ fun test_swap_exact_amount_with_input(is_bid: bool) { ); let (base_2, quote_2, deep_required_2) = if (is_bid) { - get_quote_quantity_out_input_fee( + get_quote_quantity_out( pool_id, base_in, &mut test, ) } else { - get_base_quantity_out_input_fee( + get_base_quantity_out( pool_id, quote_in, &mut test, ) }; + let initial_bob_balances = 1000000 * constants::float_scaling(); + let bob_balance_manager_id = create_acct_and_share_with_funds( + BOB, + initial_bob_balances, + &mut test, + ); + create_caps(BOB, bob_balance_manager_id, &mut test); + let bob_deep_balance_before = asset_balance(BOB, bob_balance_manager_id, &mut test); + let (base_out, quote_out, deep_out) = if (is_bid) { - place_swap_exact_base_for_quote( - pool_id, - BOB, - base_in, - deep_in, - 0, - &mut test, - ) + if (with_manager) { + let deep_out = coin::zero(test.ctx()); + let (base_out, quote_out) = place_exact_base_for_quote_with_manager( + pool_id, + BOB, + bob_balance_manager_id, + base_in, + 0, + &mut test, + ); + + (base_out, quote_out, deep_out) + } else { + place_swap_exact_base_for_quote( + pool_id, + BOB, + base_in, + deep_in, + 0, + &mut test, + ) + } } else { - place_swap_exact_quote_for_base( - pool_id, - BOB, - quote_in, - deep_in, - 0, - &mut test, - ) + if (with_manager) { + let deep_out = coin::zero(test.ctx()); + let (base_out, quote_out) = place_exact_quote_for_base_with_manager( + pool_id, + BOB, + bob_balance_manager_id, + quote_in, + 0, + &mut test, + ); + + (base_out, quote_out, deep_out) + } else { + place_swap_exact_quote_for_base( + pool_id, + BOB, + quote_in, + deep_in, + 0, + &mut test, + ) + } }; + let bob_deep_balance_after = asset_balance(BOB, bob_balance_manager_id, &mut test); if (is_bid) { assert!(base_out.value() == residual, constants::e_order_info_mismatch()); @@ -3481,14 +4389,27 @@ fun test_swap_exact_amount_with_input(is_bid: bool) { assert!(quote_out.value() == 2 * residual, constants::e_order_info_mismatch()); }; - assert!(deep_out.value() == 0, constants::e_order_info_mismatch()); + if (with_manager) { + assert!( + deep_required == bob_deep_balance_before - bob_deep_balance_after, + constants::e_order_info_mismatch(), + ); + assert!( + deep_required == deep_required_2 && + deep_required == bob_deep_balance_before - bob_deep_balance_after, + constants::e_order_info_mismatch(), + ); + } else { + assert!(deep_out.value() == residual, constants::e_order_info_mismatch()); + assert!( + deep_required == deep_required_2 && + deep_required == deep_in - deep_out.value(), + constants::e_order_info_mismatch(), + ); + }; + assert!(base == base_2 && base == base_out.value(), constants::e_order_info_mismatch()); assert!(quote == quote_2 && quote == quote_out.value(), constants::e_order_info_mismatch()); - assert!( - deep_required == deep_required_2 && - deep_required == deep_in - deep_out.value(), - constants::e_order_info_mismatch(), - ); base_out.burn_for_testing(); quote_out.burn_for_testing(); @@ -3497,11 +4418,10 @@ fun test_swap_exact_amount_with_input(is_bid: bool) { end(test); } -/// Alice places a bid/ask order -/// Alice then places an ask/bid order that crosses with that order with -/// cancel_taker option -/// Order should be rejected. -fun test_self_matching_cancel_taker(is_bid: bool) { +/// Alice places a bid order, Bob places a swap_exact_amount order +/// Make sure the assets returned to Bob are correct +/// Make sure expired orders are skipped over +fun test_swap_exact_amount_with_input(is_bid: bool) { let mut test = begin(OWNER); let registry_id = setup_test(OWNER, &mut test); let balance_manager_id_alice = create_acct_and_share_with_funds( @@ -3509,60 +4429,306 @@ fun test_self_matching_cancel_taker(is_bid: bool) { 1000000 * constants::float_scaling(), &mut test, ); - let pool_id = setup_pool_with_default_fees_and_reference_pool( + let pool_id = setup_pool_with_default_fees( ALICE, registry_id, - balance_manager_id_alice, + false, + false, &mut test, ); - let bid_client_order_id = 1; - let ask_client_order_id = 2; - let order_type = constants::no_restriction(); - let price_1 = 2 * constants::float_scaling(); - let price_2 = if (is_bid) { - 1 * constants::float_scaling() - } else { + let alice_client_order_id = 1; + let alice_price = 2 * constants::float_scaling(); + let alice_quantity = 2 * constants::float_scaling(); + let expired_price = if (is_bid) { 3 * constants::float_scaling() + } else { + 1 * constants::float_scaling() }; - let quantity = 1 * constants::float_scaling(); let expire_timestamp = constants::max_u64(); - let pay_with_deep = true; - let fee_is_deep = true; + let expire_timestamp_e = get_time(&mut test) + 100; + let pay_with_deep = false; + let residual = constants::lot_size() - 1; + let input_fee_rate = math::mul( + constants::fee_penalty_multiplier(), + constants::taker_fee(), + ); - let order_info_1 = place_limit_order( + place_limit_order( ALICE, pool_id, balance_manager_id_alice, - bid_client_order_id, - order_type, + alice_client_order_id, + constants::no_restriction(), constants::self_matching_allowed(), - price_1, - quantity, + alice_price, + alice_quantity, is_bid, pay_with_deep, expire_timestamp, &mut test, ); - verify_order_info( - &order_info_1, - bid_client_order_id, - price_1, - quantity, - 0, - 0, - 0, - fee_is_deep, - constants::live(), - expire_timestamp, - ); - place_limit_order( ALICE, pool_id, balance_manager_id_alice, - ask_client_order_id, + alice_client_order_id, + constants::no_restriction(), + constants::self_matching_allowed(), + expired_price, + alice_quantity, + is_bid, + pay_with_deep, + expire_timestamp_e, + &mut test, + ); + + set_time(200, &mut test); + + let base_in = if (is_bid) { + math::mul(1 * constants::float_scaling(), constants::float_scaling() + input_fee_rate) + residual + } else { + 0 + }; + let quote_in = if (is_bid) { + 0 + } else { + math::mul(2 * constants::float_scaling(),constants::float_scaling() + input_fee_rate) + 2 * residual + }; + let deep_in = 0; + + let (base, quote, deep_required) = get_quantity_out_input_fee( + pool_id, + base_in, + quote_in, + &mut test, + ); + + let (base_2, quote_2, deep_required_2) = if (is_bid) { + get_quote_quantity_out_input_fee( + pool_id, + base_in, + &mut test, + ) + } else { + get_base_quantity_out_input_fee( + pool_id, + quote_in, + &mut test, + ) + }; + + let (base_out, quote_out, deep_out) = if (is_bid) { + place_swap_exact_base_for_quote( + pool_id, + BOB, + base_in, + deep_in, + 0, + &mut test, + ) + } else { + place_swap_exact_quote_for_base( + pool_id, + BOB, + quote_in, + deep_in, + 0, + &mut test, + ) + }; + + if (is_bid) { + assert!(base_out.value() == residual, constants::e_order_info_mismatch()); + assert!( + quote_out.value() == 2 * constants::float_scaling(), + constants::e_order_info_mismatch(), + ); + } else { + assert!( + base_out.value() == 1 * constants::float_scaling(), + constants::e_order_info_mismatch(), + ); + assert!(quote_out.value() == 2 * residual, constants::e_order_info_mismatch()); + }; + + assert!(deep_out.value() == 0, constants::e_order_info_mismatch()); + assert!(base == base_2 && base == base_out.value(), constants::e_order_info_mismatch()); + assert!(quote == quote_2 && quote == quote_out.value(), constants::e_order_info_mismatch()); + assert!( + deep_required == deep_required_2 && + deep_required == deep_in - deep_out.value(), + constants::e_order_info_mismatch(), + ); + + base_out.burn_for_testing(); + quote_out.burn_for_testing(); + deep_out.burn_for_testing(); + + end(test); +} + +fun test_get_quantity_out_zero(is_bid: bool) { + let mut test = begin(OWNER); + let registry_id = setup_test(OWNER, &mut test); + let balance_manager_id_alice = create_acct_and_share_with_funds( + ALICE, + 1000000 * constants::float_scaling(), + &mut test, + ); + let pool_id = setup_pool_with_default_fees_and_reference_pool( + ALICE, + registry_id, + balance_manager_id_alice, + &mut test, + ); + + let alice_client_order_id = 1; + let alice_price = 2 * constants::float_scaling(); + let alice_quantity = 2 * constants::float_scaling(); + let expire_timestamp = constants::max_u64(); + let pay_with_deep = false; + + place_limit_order( + ALICE, + pool_id, + balance_manager_id_alice, + alice_client_order_id, + constants::no_restriction(), + constants::self_matching_allowed(), + alice_price, + alice_quantity, + is_bid, + pay_with_deep, + expire_timestamp, + &mut test, + ); + + set_time(200, &mut test); + + let base_in = if (is_bid) { + constants::min_size() + } else { + 0 + }; + let quote_in = if (is_bid) { + 0 + } else { + 2 * constants::min_size() + }; + + let (base, quote, deep_required) = get_quantity_out_input_fee( + pool_id, + base_in, + quote_in, + &mut test, + ); + let expected_base = if (is_bid) { + constants::min_size() + } else { + 0 + }; + let expected_quote = if (is_bid) { + 0 + } else { + 2 * constants::min_size() + }; + + assert!(base == expected_base, constants::e_order_info_mismatch()); + assert!(quote == expected_quote, constants::e_order_info_mismatch()); + assert!(deep_required == 0, constants::e_order_info_mismatch()); + + let (base, quote, _) = get_quantity_out( + pool_id, + base_in, + quote_in, + &mut test, + ); + + let expected_base = if (is_bid) { + 0 + } else { + constants::min_size() + }; + let expected_quote = if (is_bid) { + 2 * constants::min_size() + } else { + 0 + }; + + assert!(base == expected_base, constants::e_order_info_mismatch()); + assert!(quote == expected_quote, constants::e_order_info_mismatch()); + + end(test); +} + +/// Alice places a bid/ask order +/// Alice then places an ask/bid order that crosses with that order with +/// cancel_taker option +/// Order should be rejected. +fun test_self_matching_cancel_taker(is_bid: bool) { + let mut test = begin(OWNER); + let registry_id = setup_test(OWNER, &mut test); + let balance_manager_id_alice = create_acct_and_share_with_funds( + ALICE, + 1000000 * constants::float_scaling(), + &mut test, + ); + let pool_id = setup_pool_with_default_fees_and_reference_pool( + ALICE, + registry_id, + balance_manager_id_alice, + &mut test, + ); + + let bid_client_order_id = 1; + let ask_client_order_id = 2; + let order_type = constants::no_restriction(); + let price_1 = 2 * constants::float_scaling(); + let price_2 = if (is_bid) { + 1 * constants::float_scaling() + } else { + 3 * constants::float_scaling() + }; + let quantity = 1 * constants::float_scaling(); + let expire_timestamp = constants::max_u64(); + let pay_with_deep = true; + let fee_is_deep = true; + + let order_info_1 = place_limit_order( + ALICE, + pool_id, + balance_manager_id_alice, + bid_client_order_id, + order_type, + constants::self_matching_allowed(), + price_1, + quantity, + is_bid, + pay_with_deep, + expire_timestamp, + &mut test, + ); + + verify_order_info( + &order_info_1, + bid_client_order_id, + price_1, + quantity, + 0, + 0, + 0, + fee_is_deep, + constants::live(), + expire_timestamp, + ); + + place_limit_order( + ALICE, + pool_id, + balance_manager_id_alice, + ask_client_order_id, order_type, constants::cancel_taker(), price_2, @@ -4785,27 +5951,74 @@ fun place_swap_exact_base_for_quote( } } -fun place_swap_exact_quote_for_base( +fun place_exact_base_for_quote_with_manager( pool_id: ID, trader: address, - quote_in: u64, - deep_in: u64, - min_base_out: u64, + balance_manager_id: ID, + base_in: u64, + min_quote_out: u64, test: &mut Scenario, -): (Coin, Coin, Coin) { +): (Coin, Coin) { test.next_tx(trader); { let mut pool = test.take_shared_by_id>( pool_id, ); let clock = test.take_shared(); + let mut balance_manager = test.take_shared_by_id( + balance_manager_id, + ); + let trade_cap = test.take_from_sender(); + let deposit_cap = test.take_from_sender(); + let withdraw_cap = test.take_from_sender(); // Place order in pool - let (base_out, quote_out, deep_out) = pool.swap_exact_quote_for_base( - mint_for_testing(quote_in, test.ctx()), - mint_for_testing(deep_in, test.ctx()), - min_base_out, - &clock, + let (base_out, quote_out) = pool.swap_exact_base_for_quote_with_manager< + BaseAsset, + QuoteAsset, + >( + &mut balance_manager, + &trade_cap, + &deposit_cap, + &withdraw_cap, + mint_for_testing(base_in, test.ctx()), + min_quote_out, + &clock, + test.ctx(), + ); + + return_shared(pool); + return_shared(clock); + return_shared(balance_manager); + test.return_to_sender(trade_cap); + test.return_to_sender(deposit_cap); + test.return_to_sender(withdraw_cap); + + (base_out, quote_out) + } +} + +fun place_swap_exact_quote_for_base( + pool_id: ID, + trader: address, + quote_in: u64, + deep_in: u64, + min_base_out: u64, + test: &mut Scenario, +): (Coin, Coin, Coin) { + test.next_tx(trader); + { + let mut pool = test.take_shared_by_id>( + pool_id, + ); + let clock = test.take_shared(); + + // Place order in pool + let (base_out, quote_out, deep_out) = pool.swap_exact_quote_for_base( + mint_for_testing(quote_in, test.ctx()), + mint_for_testing(deep_in, test.ctx()), + min_base_out, + &clock, test.ctx(), ); return_shared(pool); @@ -4815,6 +6028,53 @@ fun place_swap_exact_quote_for_base( } } +fun place_exact_quote_for_base_with_manager( + pool_id: ID, + trader: address, + balance_manager_id: ID, + quote_in: u64, + min_base_out: u64, + test: &mut Scenario, +): (Coin, Coin) { + test.next_tx(trader); + { + let mut pool = test.take_shared_by_id>( + pool_id, + ); + let clock = test.take_shared(); + let mut balance_manager = test.take_shared_by_id( + balance_manager_id, + ); + let trade_cap = test.take_from_sender(); + let deposit_cap = test.take_from_sender(); + let withdraw_cap = test.take_from_sender(); + + // Place order in pool + let (base_out, quote_out) = pool.swap_exact_quote_for_base_with_manager< + BaseAsset, + QuoteAsset, + >( + &mut balance_manager, + &trade_cap, + &deposit_cap, + &withdraw_cap, + mint_for_testing(quote_in, test.ctx()), + min_base_out, + &clock, + test.ctx(), + ); + + return_shared(pool); + return_shared(clock); + return_shared(balance_manager); + test.return_to_sender(trade_cap); + test.return_to_sender(deposit_cap); + test.return_to_sender(withdraw_cap); + + (base_out, quote_out) + } +} + fun cancel_orders( sender: address, pool_id: ID, @@ -4913,7 +6173,7 @@ fun setup_pool( ); }; return_shared(registry); - test_utils::destroy(admin_cap); + destroy(admin_cap); pool_id } @@ -4945,7 +6205,7 @@ fun setup_permissionless_pool( ); }; return_shared(registry); - test_utils::destroy(admin_cap); + destroy(admin_cap); pool_id } @@ -4960,156 +6220,2992 @@ fun get_mid_price(pool_id: ID, test: &mut Scenario): u64 return_shared(pool); return_shared(clock); - mid_price - } -} + mid_price + } +} + +fun get_quantity_out( + pool_id: ID, + base_quantity: u64, + quote_quantity: u64, + test: &mut Scenario, +): (u64, u64, u64) { + test.next_tx(OWNER); + { + let pool = test.take_shared_by_id>(pool_id); + let clock = test.take_shared(); + + let (base_out, quote_out, deep_required) = pool.get_quantity_out( + base_quantity, + quote_quantity, + &clock, + ); + return_shared(pool); + return_shared(clock); + + (base_out, quote_out, deep_required) + } +} + +fun get_quantity_out_input_fee( + pool_id: ID, + base_quantity: u64, + quote_quantity: u64, + test: &mut Scenario, +): (u64, u64, u64) { + test.next_tx(OWNER); + { + let pool = test.take_shared_by_id>(pool_id); + let clock = test.take_shared(); + + let (base_out, quote_out, deep_required) = pool.get_quantity_out_input_fee< + BaseAsset, + QuoteAsset, + >( + base_quantity, + quote_quantity, + &clock, + ); + return_shared(pool); + return_shared(clock); + + (base_out, quote_out, deep_required) + } +} + +fun get_base_quantity_out( + pool_id: ID, + quote_quantity: u64, + test: &mut Scenario, +): (u64, u64, u64) { + test.next_tx(OWNER); + { + let pool = test.take_shared_by_id>(pool_id); + let clock = test.take_shared(); + + let (base_out, quote_out, deep_required) = pool.get_base_quantity_out< + BaseAsset, + QuoteAsset, + >( + quote_quantity, + &clock, + ); + return_shared(pool); + return_shared(clock); + + (base_out, quote_out, deep_required) + } +} + +fun get_quote_quantity_out( + pool_id: ID, + base_quantity: u64, + test: &mut Scenario, +): (u64, u64, u64) { + test.next_tx(OWNER); + { + let pool = test.take_shared_by_id>(pool_id); + let clock = test.take_shared(); + + let (base_out, quote_out, deep_required) = pool.get_quote_quantity_out< + BaseAsset, + QuoteAsset, + >( + base_quantity, + &clock, + ); + return_shared(pool); + return_shared(clock); + + (base_out, quote_out, deep_required) + } +} + +fun get_base_quantity_out_input_fee( + pool_id: ID, + quote_quantity: u64, + test: &mut Scenario, +): (u64, u64, u64) { + test.next_tx(OWNER); + { + let pool = test.take_shared_by_id>(pool_id); + let clock = test.take_shared(); + + let (base_out, quote_out, deep_required) = pool.get_base_quantity_out_input_fee< + BaseAsset, + QuoteAsset, + >( + quote_quantity, + &clock, + ); + return_shared(pool); + return_shared(clock); + + (base_out, quote_out, deep_required) + } +} + +fun get_quote_quantity_out_input_fee( + pool_id: ID, + base_quantity: u64, + test: &mut Scenario, +): (u64, u64, u64) { + test.next_tx(OWNER); + { + let pool = test.take_shared_by_id>(pool_id); + let clock = test.take_shared(); + + let (base_out, quote_out, deep_required) = pool.get_quote_quantity_out_input_fee< + BaseAsset, + QuoteAsset, + >( + base_quantity, + &clock, + ); + return_shared(pool); + return_shared(clock); + + (base_out, quote_out, deep_required) + } +} + +fun test_cancel_orders(is_bid: bool) { + let mut test = begin(OWNER); + let registry_id = setup_test(OWNER, &mut test); + let balance_manager_id_alice = create_acct_and_share_with_funds( + ALICE, + 1000000 * constants::float_scaling(), + &mut test, + ); + let pool_id = setup_pool_with_default_fees_and_reference_pool( + ALICE, + registry_id, + balance_manager_id_alice, + &mut test, + ); + + let client_order_id = 1; + let price = 2 * constants::float_scaling(); + let quantity = 1 * constants::float_scaling(); + let expire_timestamp = constants::max_u64(); + let pay_with_deep = true; + + let order_info_1 = place_limit_order( + ALICE, + pool_id, + balance_manager_id_alice, + client_order_id, + constants::no_restriction(), + constants::self_matching_allowed(), + price, + quantity, + is_bid, + pay_with_deep, + expire_timestamp, + &mut test, + ); + + let order_info_2 = place_limit_order( + ALICE, + pool_id, + balance_manager_id_alice, + client_order_id, + constants::no_restriction(), + constants::self_matching_allowed(), + price, + quantity, + is_bid, + pay_with_deep, + expire_timestamp, + &mut test, + ); + + let mut orders_to_cancel = vector[]; + orders_to_cancel.push_back(order_info_1.order_id()); + orders_to_cancel.push_back(order_info_2.order_id()); + + cancel_orders( + ALICE, + pool_id, + balance_manager_id_alice, + orders_to_cancel, + &mut test, + ); + + borrow_and_verify_book_order( + pool_id, + order_info_1.order_id(), + is_bid, + client_order_id, + quantity, + 0, + order_info_1.order_deep_price().deep_per_asset(), + test.ctx().epoch(), + constants::canceled(), + expire_timestamp, + &mut test, + ); + + end(test); +} + +fun test_update_pool_book_params(error: u8) { + let mut test = begin(OWNER); + let registry_id = setup_test(OWNER, &mut test); + let balance_manager_id_alice = create_acct_and_share_with_funds( + ALICE, + 1000000 * constants::float_scaling(), + &mut test, + ); + let pool_id = setup_pool( + OWNER, + constants::tick_size(), // tick size + 100_000, + 1_000_000, + registry_id, + true, + false, + &mut test, + ); + + let alice_client_order_id = 1; + let alice_quantity_1 = 1_000_000; + let alice_quantity_2 = 1_010_000; + let alice_price = 2 * constants::float_scaling(); + let expire_timestamp = constants::max_u64(); + let pay_with_deep = true; + + place_limit_order( + ALICE, + pool_id, + balance_manager_id_alice, + alice_client_order_id, + constants::no_restriction(), + constants::self_matching_allowed(), + alice_price, + alice_quantity_1, + true, + pay_with_deep, + expire_timestamp, + &mut test, + ); + + if (error == 0) { + adjust_min_lot_size_admin( + OWNER, + pool_id, + 1000, + 10000, + &mut test, + ); + }; + + if (error == 2) { + adjust_min_lot_size_admin( + OWNER, + pool_id, + 6000, + 60000, + &mut test, + ); + }; + + if (error == 3) { + adjust_tick_size_admin( + OWNER, + pool_id, + 50, + &mut test, + ); + }; + + if (error == 4) { + adjust_min_lot_size_admin( + OWNER, + pool_id, + 0, + 500, + &mut test, + ); + }; + + place_limit_order( + ALICE, + pool_id, + balance_manager_id_alice, + alice_client_order_id, + constants::no_restriction(), + constants::self_matching_allowed(), + alice_price, + alice_quantity_2, + true, + pay_with_deep, + expire_timestamp, + &mut test, + ); + adjust_tick_size_admin( + OWNER, + pool_id, + 100, + &mut test, + ); + place_limit_order( + ALICE, + pool_id, + balance_manager_id_alice, + alice_client_order_id, + constants::no_restriction(), + constants::self_matching_allowed(), + alice_price + 100, + alice_quantity_2, + true, + pay_with_deep, + expire_timestamp, + &mut test, + ); + end(test); +} + +fun adjust_min_lot_size_admin( + sender: address, + pool_id: ID, + new_lot_size: u64, + new_min_size: u64, + test: &mut Scenario, +) { + test.next_tx(sender); + let mut pool = test.take_shared_by_id>(pool_id); + let admin_cap = registry::get_admin_cap_for_testing(test.ctx()); + let clock = test.take_shared(); + pool::adjust_min_lot_size_admin( + &mut pool, + new_lot_size, + new_min_size, + &admin_cap, + &clock, + ); + destroy(admin_cap); + return_shared(pool); + return_shared(clock); +} + +fun adjust_tick_size_admin( + sender: address, + pool_id: ID, + new_tick_size: u64, + test: &mut Scenario, +) { + test.next_tx(sender); + let mut pool = test.take_shared_by_id>(pool_id); + let admin_cap = registry::get_admin_cap_for_testing(test.ctx()); + let clock = test.take_shared(); + pool::adjust_tick_size_admin( + &mut pool, + new_tick_size, + &admin_cap, + &clock, + ); + destroy(admin_cap); + return_shared(pool); + return_shared(clock); +} + +fun add_stablecoin(sender: address, registry_id: ID, test: &mut Scenario) { + test.next_tx(sender); + let admin_cap = registry::get_admin_cap_for_testing(test.ctx()); + let mut registry = test.take_shared_by_id(registry_id); + { + registry::add_stablecoin( + &mut registry, + &admin_cap, + ); + }; + return_shared(registry); + destroy(admin_cap); +} + +fun remove_stablecoin(sender: address, registry_id: ID, test: &mut Scenario) { + test.next_tx(sender); + let admin_cap = registry::get_admin_cap_for_testing(test.ctx()); + let mut registry = test.take_shared_by_id(registry_id); + { + registry::remove_stablecoin( + &mut registry, + &admin_cap, + ); + }; + return_shared(registry); + destroy(admin_cap); +} + +fun advance_scenario_with_gas_price(test: &mut Scenario, gas_price: u64, timestamp_advance: u64) { + let ts = test.ctx().epoch_timestamp_ms() + timestamp_advance; + let ctx = test.ctx_builder().set_gas_price(gas_price).set_epoch_timestamp(ts); + test.next_with_context(ctx); +} + +// ============== can_place_market_order tests ============== + +/// Test bid market order with sufficient quote balance and DEEP for fees +#[test] +fun test_can_place_market_order_bid_with_deep_sufficient() { + let mut test = begin(OWNER); + let registry_id = setup_test(OWNER, &mut test); + let balance_manager_id_alice = create_acct_and_share_with_funds( + ALICE, + 1000000 * constants::float_scaling(), + &mut test, + ); + + // Setup pool with reference pool (non-whitelisted) so DEEP fees are required + let pool_id = setup_pool_with_default_fees_and_reference_pool( + ALICE, + registry_id, + balance_manager_id_alice, + &mut test, + ); + + // Place a sell order on the book (so we can buy) + place_limit_order( + ALICE, + pool_id, + balance_manager_id_alice, + 1, + constants::no_restriction(), + constants::self_matching_allowed(), + 2 * constants::float_scaling(), // price: 2 USDC per SUI + 100 * constants::float_scaling(), // quantity: 100 SUI + false, // is_bid = false (sell order) + true, // pay_with_deep + constants::max_u64(), + &mut test, + ); + + test.next_tx(ALICE); + { + let pool = test.take_shared_by_id>(pool_id); + let balance_manager = test.take_shared_by_id(balance_manager_id_alice); + let clock = test.take_shared(); + + // Test: bid for 10 SUI with pay_with_deep = true + // Should succeed since we have enough USDC and DEEP + let can_place = pool.can_place_market_order( + &balance_manager, + 10 * constants::float_scaling(), // quantity: 10 SUI + true, // is_bid + true, // pay_with_deep + &clock, + ); + assert!(can_place); + + return_shared(pool); + return_shared(balance_manager); + return_shared(clock); + }; + + end(test); +} + +/// Test bid market order with insufficient quote balance +#[test] +fun test_can_place_market_order_bid_insufficient_quote() { + let mut test = begin(OWNER); + let registry_id = setup_test(OWNER, &mut test); + + // Create balance manager with minimal funds + test.next_tx(ALICE); + let balance_manager_id_alice; + { + let mut bm = balance_manager::new(test.ctx()); + // Only deposit 1 USDC (not enough to buy 10 SUI at price 2) + bm.deposit( + mint_for_testing(1 * constants::float_scaling(), test.ctx()), + test.ctx(), + ); + bm.deposit( + mint_for_testing(1000 * constants::float_scaling(), test.ctx()), + test.ctx(), + ); + balance_manager_id_alice = bm.id(); + transfer::public_share_object(bm); + }; + + // Create another balance manager with funds for liquidity + let balance_manager_id_bob = create_acct_and_share_with_funds( + BOB, + 1000000 * constants::float_scaling(), + &mut test, + ); + + // Setup pool + let pool_id = setup_pool_with_default_fees( + OWNER, + registry_id, + true, + false, + &mut test, + ); + + // Place a sell order on the book by Bob + place_limit_order( + BOB, + pool_id, + balance_manager_id_bob, + 1, + constants::no_restriction(), + constants::self_matching_allowed(), + 2 * constants::float_scaling(), // price: 2 USDC per SUI + 100 * constants::float_scaling(), // quantity: 100 SUI + false, // is_bid = false (sell order) + true, // pay_with_deep + constants::max_u64(), + &mut test, + ); + + test.next_tx(ALICE); + { + let pool = test.take_shared_by_id>(pool_id); + let balance_manager = test.take_shared_by_id(balance_manager_id_alice); + let clock = test.take_shared(); + + // Test: try to bid for 10 SUI but only have 1 USDC + let can_place = pool.can_place_market_order( + &balance_manager, + 10 * constants::float_scaling(), // quantity: 10 SUI + true, // is_bid + true, // pay_with_deep + &clock, + ); + assert!(!can_place); + + return_shared(pool); + return_shared(balance_manager); + return_shared(clock); + }; + + end(test); +} + +/// Test bid market order with insufficient DEEP for fees (using reference pool setup) +#[test] +fun test_can_place_market_order_bid_insufficient_deep() { + let mut test = begin(OWNER); + let registry_id = setup_test(OWNER, &mut test); + + // Create balance manager with USDC but no DEEP + test.next_tx(ALICE); + let balance_manager_id_alice; + { + let mut bm = balance_manager::new(test.ctx()); + bm.deposit( + mint_for_testing(1000 * constants::float_scaling(), test.ctx()), + test.ctx(), + ); + // No DEEP deposited + balance_manager_id_alice = bm.id(); + transfer::public_share_object(bm); + }; + + // Create balance manager for Bob with funds for liquidity and reference pool setup + let balance_manager_id_bob = create_acct_and_share_with_funds( + BOB, + 1000000 * constants::float_scaling(), + &mut test, + ); + + // Setup pool with reference pool (non-whitelisted) so DEEP is required for fees + let pool_id = setup_pool_with_default_fees_and_reference_pool( + BOB, + registry_id, + balance_manager_id_bob, + &mut test, + ); + + // Place a sell order on the book by Bob + place_limit_order( + BOB, + pool_id, + balance_manager_id_bob, + 1, + constants::no_restriction(), + constants::self_matching_allowed(), + 2 * constants::float_scaling(), // price: 2 USDC per SUI + 100 * constants::float_scaling(), // quantity: 100 SUI + false, // is_bid = false (sell order) + true, // pay_with_deep + constants::max_u64(), + &mut test, + ); + + test.next_tx(ALICE); + { + let pool = test.take_shared_by_id>(pool_id); + let balance_manager = test.take_shared_by_id(balance_manager_id_alice); + let clock = test.take_shared(); + + // Test: try to bid for 10 SUI with pay_with_deep but no DEEP + let can_place = pool.can_place_market_order( + &balance_manager, + 10 * constants::float_scaling(), // quantity: 10 SUI + true, // is_bid + true, // pay_with_deep + &clock, + ); + assert!(!can_place); + + return_shared(pool); + return_shared(balance_manager); + return_shared(clock); + }; + + end(test); +} + +/// Test bid market order with exactly the quote and DEEP needed +#[test] +fun test_can_place_market_order_bid_exact_quote_and_deep() { + let mut test = begin(OWNER); + let registry_id = setup_test(OWNER, &mut test); + + // Create balance manager for Bob with funds for liquidity and reference pool setup + let balance_manager_id_bob = create_acct_and_share_with_funds( + BOB, + 1000000 * constants::float_scaling(), + &mut test, + ); + + // Setup pool with reference pool (non-whitelisted) so DEEP is required for fees + let pool_id = setup_pool_with_default_fees_and_reference_pool( + BOB, + registry_id, + balance_manager_id_bob, + &mut test, + ); + + // Place a sell order on the book by Bob + place_limit_order( + BOB, + pool_id, + balance_manager_id_bob, + 1, + constants::no_restriction(), + constants::self_matching_allowed(), + 2 * constants::float_scaling(), // price: 2 USDC per SUI + 100 * constants::float_scaling(), // quantity: 100 SUI + false, // is_bid = false (sell order) + true, // pay_with_deep + constants::max_u64(), + &mut test, + ); + + // Get the exact quote and DEEP needed for a market bid of 10 SUI + let quantity = 10 * constants::float_scaling(); + let quote_needed; + let deep_needed; + test.next_tx(ALICE); + { + let pool = test.take_shared_by_id>(pool_id); + let clock = test.take_shared(); + + let (_base_out, quote_in, deep_required) = pool.get_quote_quantity_in( + quantity, + true, // pay_with_deep + &clock, + ); + quote_needed = quote_in; + deep_needed = deep_required; + + return_shared(pool); + return_shared(clock); + }; + + // Create balance manager for Alice with exactly the needed amounts + test.next_tx(ALICE); + let balance_manager_id_alice; + { + let mut bm = balance_manager::new(test.ctx()); + bm.deposit( + mint_for_testing(quote_needed, test.ctx()), + test.ctx(), + ); + bm.deposit( + mint_for_testing(deep_needed, test.ctx()), + test.ctx(), + ); + balance_manager_id_alice = bm.id(); + transfer::public_share_object(bm); + }; + + test.next_tx(ALICE); + { + let pool = test.take_shared_by_id>(pool_id); + let balance_manager = test.take_shared_by_id(balance_manager_id_alice); + let clock = test.take_shared(); + + // Test: bid for 10 SUI with exactly the quote and DEEP needed + let can_place = pool.can_place_market_order( + &balance_manager, + quantity, + true, // is_bid + true, // pay_with_deep + &clock, + ); + assert!(can_place); + + return_shared(pool); + return_shared(balance_manager); + return_shared(clock); + }; + + end(test); +} + +/// Test bid market order fails with one less unit of DEEP than needed +#[test] +fun test_can_place_market_order_bid_one_less_deep_than_needed() { + let mut test = begin(OWNER); + let registry_id = setup_test(OWNER, &mut test); + + // Create balance manager for Bob with funds for liquidity and reference pool setup + let balance_manager_id_bob = create_acct_and_share_with_funds( + BOB, + 1000000 * constants::float_scaling(), + &mut test, + ); + + // Setup pool with reference pool (non-whitelisted) so DEEP is required for fees + let pool_id = setup_pool_with_default_fees_and_reference_pool( + BOB, + registry_id, + balance_manager_id_bob, + &mut test, + ); + + // Place a sell order on the book by Bob + place_limit_order( + BOB, + pool_id, + balance_manager_id_bob, + 1, + constants::no_restriction(), + constants::self_matching_allowed(), + 2 * constants::float_scaling(), // price: 2 USDC per SUI + 100 * constants::float_scaling(), // quantity: 100 SUI + false, // is_bid = false (sell order) + true, // pay_with_deep + constants::max_u64(), + &mut test, + ); + + // Get the exact quote and DEEP needed for a market bid of 10 SUI + let quantity = 10 * constants::float_scaling(); + let quote_needed; + let deep_needed; + test.next_tx(ALICE); + { + let pool = test.take_shared_by_id>(pool_id); + let clock = test.take_shared(); + + let (_base_out, quote_in, deep_required) = pool.get_quote_quantity_in( + quantity, + true, // pay_with_deep + &clock, + ); + quote_needed = quote_in; + deep_needed = deep_required; + + return_shared(pool); + return_shared(clock); + }; + + // Create balance manager for Alice with exact quote but one less DEEP + test.next_tx(ALICE); + let balance_manager_id_alice; + { + let mut bm = balance_manager::new(test.ctx()); + bm.deposit( + mint_for_testing(quote_needed, test.ctx()), + test.ctx(), + ); + bm.deposit( + mint_for_testing(deep_needed - 1, test.ctx()), + test.ctx(), + ); + balance_manager_id_alice = bm.id(); + transfer::public_share_object(bm); + }; + + test.next_tx(ALICE); + { + let pool = test.take_shared_by_id>(pool_id); + let balance_manager = test.take_shared_by_id(balance_manager_id_alice); + let clock = test.take_shared(); + + // Test: bid for 10 SUI with one less DEEP than needed should fail + let can_place = pool.can_place_market_order( + &balance_manager, + quantity, + true, // is_bid + true, // pay_with_deep + &clock, + ); + assert!(!can_place); + + return_shared(pool); + return_shared(balance_manager); + return_shared(clock); + }; + + end(test); +} + +/// Test ask market order with sufficient base balance and DEEP for fees +#[test] +fun test_can_place_market_order_ask_with_deep_sufficient() { + let mut test = begin(OWNER); + let registry_id = setup_test(OWNER, &mut test); + let balance_manager_id_alice = create_acct_and_share_with_funds( + ALICE, + 1000000 * constants::float_scaling(), + &mut test, + ); + + // Setup pool with reference pool (non-whitelisted) so DEEP fees are required + let pool_id = setup_pool_with_default_fees_and_reference_pool( + ALICE, + registry_id, + balance_manager_id_alice, + &mut test, + ); + + // Place a buy order on the book (so we can sell) + place_limit_order( + ALICE, + pool_id, + balance_manager_id_alice, + 1, + constants::no_restriction(), + constants::self_matching_allowed(), + 1 * constants::float_scaling(), // price: 1 USDC per SUI + 100 * constants::float_scaling(), // quantity: 100 SUI + true, // is_bid = true (buy order) + true, // pay_with_deep + constants::max_u64(), + &mut test, + ); + + test.next_tx(ALICE); + { + let pool = test.take_shared_by_id>(pool_id); + let balance_manager = test.take_shared_by_id(balance_manager_id_alice); + let clock = test.take_shared(); + + // Test: ask (sell) 10 SUI with pay_with_deep = true + let can_place = pool.can_place_market_order( + &balance_manager, + 10 * constants::float_scaling(), // quantity: 10 SUI + false, // is_bid = false (ask/sell) + true, // pay_with_deep + &clock, + ); + assert!(can_place); + + return_shared(pool); + return_shared(balance_manager); + return_shared(clock); + }; + + end(test); +} + +/// Test ask market order with insufficient base balance +#[test] +fun test_can_place_market_order_ask_insufficient_base() { + let mut test = begin(OWNER); + let registry_id = setup_test(OWNER, &mut test); + + // Create balance manager with minimal SUI + test.next_tx(ALICE); + let balance_manager_id_alice; + { + let mut bm = balance_manager::new(test.ctx()); + // Only deposit 1 SUI (not enough to sell 10 SUI) + bm.deposit( + mint_for_testing(1 * constants::float_scaling(), test.ctx()), + test.ctx(), + ); + bm.deposit( + mint_for_testing(1000 * constants::float_scaling(), test.ctx()), + test.ctx(), + ); + balance_manager_id_alice = bm.id(); + transfer::public_share_object(bm); + }; + + // Create another balance manager with funds for liquidity + let balance_manager_id_bob = create_acct_and_share_with_funds( + BOB, + 1000000 * constants::float_scaling(), + &mut test, + ); + + // Setup pool + let pool_id = setup_pool_with_default_fees( + OWNER, + registry_id, + true, + false, + &mut test, + ); + + // Place a buy order on the book by Bob + place_limit_order( + BOB, + pool_id, + balance_manager_id_bob, + 1, + constants::no_restriction(), + constants::self_matching_allowed(), + 1 * constants::float_scaling(), // price: 1 USDC per SUI + 100 * constants::float_scaling(), // quantity: 100 SUI + true, // is_bid = true (buy order) + true, // pay_with_deep + constants::max_u64(), + &mut test, + ); + + test.next_tx(ALICE); + { + let pool = test.take_shared_by_id>(pool_id); + let balance_manager = test.take_shared_by_id(balance_manager_id_alice); + let clock = test.take_shared(); + + // Test: try to ask (sell) 10 SUI but only have 1 SUI + let can_place = pool.can_place_market_order( + &balance_manager, + 10 * constants::float_scaling(), // quantity: 10 SUI + false, // is_bid = false (ask/sell) + true, // pay_with_deep + &clock, + ); + assert!(!can_place); + + return_shared(pool); + return_shared(balance_manager); + return_shared(clock); + }; + + end(test); +} + +/// Test ask market order with insufficient DEEP for fees (using reference pool setup) +#[test] +fun test_can_place_market_order_ask_insufficient_deep() { + let mut test = begin(OWNER); + let registry_id = setup_test(OWNER, &mut test); + + // Create balance manager with SUI but no DEEP + test.next_tx(ALICE); + let balance_manager_id_alice; + { + let mut bm = balance_manager::new(test.ctx()); + bm.deposit( + mint_for_testing(100 * constants::float_scaling(), test.ctx()), + test.ctx(), + ); + // No DEEP deposited + balance_manager_id_alice = bm.id(); + transfer::public_share_object(bm); + }; + + // Create balance manager for Bob with funds for liquidity and reference pool setup + let balance_manager_id_bob = create_acct_and_share_with_funds( + BOB, + 1000000 * constants::float_scaling(), + &mut test, + ); + + // Setup pool with reference pool (non-whitelisted) so DEEP is required for fees + let pool_id = setup_pool_with_default_fees_and_reference_pool( + BOB, + registry_id, + balance_manager_id_bob, + &mut test, + ); + + // Place a buy order on the book by Bob + place_limit_order( + BOB, + pool_id, + balance_manager_id_bob, + 1, + constants::no_restriction(), + constants::self_matching_allowed(), + 1 * constants::float_scaling(), // price: 1 USDC per SUI + 100 * constants::float_scaling(), // quantity: 100 SUI + true, // is_bid = true (buy order) + true, // pay_with_deep + constants::max_u64(), + &mut test, + ); + + test.next_tx(ALICE); + { + let pool = test.take_shared_by_id>(pool_id); + let balance_manager = test.take_shared_by_id(balance_manager_id_alice); + let clock = test.take_shared(); + + // Test: try to ask (sell) 10 SUI with pay_with_deep but no DEEP + let can_place = pool.can_place_market_order( + &balance_manager, + 10 * constants::float_scaling(), // quantity: 10 SUI + false, // is_bid = false (ask/sell) + true, // pay_with_deep + &clock, + ); + assert!(!can_place); + + return_shared(pool); + return_shared(balance_manager); + return_shared(clock); + }; + + end(test); +} + +/// Test bid market order paying fees with input token (quote) +#[test] +fun test_can_place_market_order_bid_input_fee_sufficient() { + let mut test = begin(OWNER); + let registry_id = setup_test(OWNER, &mut test); + let balance_manager_id_alice = create_acct_and_share_with_funds( + ALICE, + 1000000 * constants::float_scaling(), + &mut test, + ); + + // Setup pool with liquidity on the book + let pool_id = setup_pool_with_default_fees( + OWNER, + registry_id, + true, + false, + &mut test, + ); + + // Place a sell order on the book + place_limit_order( + ALICE, + pool_id, + balance_manager_id_alice, + 1, + constants::no_restriction(), + constants::self_matching_allowed(), + 2 * constants::float_scaling(), // price: 2 USDC per SUI + 100 * constants::float_scaling(), // quantity: 100 SUI + false, // is_bid = false (sell order) + true, // pay_with_deep + constants::max_u64(), + &mut test, + ); + + test.next_tx(ALICE); + { + let pool = test.take_shared_by_id>(pool_id); + let balance_manager = test.take_shared_by_id(balance_manager_id_alice); + let clock = test.take_shared(); + + // Test: bid for 10 SUI with pay_with_deep = false (pay fees in USDC) + let can_place = pool.can_place_market_order( + &balance_manager, + 10 * constants::float_scaling(), // quantity: 10 SUI + true, // is_bid + false, // pay_with_deep = false (fees in quote) + &clock, + ); + assert!(can_place); + + return_shared(pool); + return_shared(balance_manager); + return_shared(clock); + }; + + end(test); +} + +/// Test ask market order paying fees with input token (base) +#[test] +fun test_can_place_market_order_ask_input_fee_sufficient() { + let mut test = begin(OWNER); + let registry_id = setup_test(OWNER, &mut test); + let balance_manager_id_alice = create_acct_and_share_with_funds( + ALICE, + 1000000 * constants::float_scaling(), + &mut test, + ); + + // Setup pool with liquidity on the book + let pool_id = setup_pool_with_default_fees( + OWNER, + registry_id, + true, + false, + &mut test, + ); + + // Place a buy order on the book + place_limit_order( + ALICE, + pool_id, + balance_manager_id_alice, + 1, + constants::no_restriction(), + constants::self_matching_allowed(), + 1 * constants::float_scaling(), // price: 1 USDC per SUI + 100 * constants::float_scaling(), // quantity: 100 SUI + true, // is_bid = true (buy order) + true, // pay_with_deep + constants::max_u64(), + &mut test, + ); + + test.next_tx(ALICE); + { + let pool = test.take_shared_by_id>(pool_id); + let balance_manager = test.take_shared_by_id(balance_manager_id_alice); + let clock = test.take_shared(); + + // Test: ask (sell) 10 SUI with pay_with_deep = false (pay fees in SUI) + let can_place = pool.can_place_market_order( + &balance_manager, + 10 * constants::float_scaling(), // quantity: 10 SUI + false, // is_bid = false (ask/sell) + false, // pay_with_deep = false (fees in base) + &clock, + ); + assert!(can_place); + + return_shared(pool); + return_shared(balance_manager); + return_shared(clock); + }; + + end(test); +} + +/// Test ask market order paying fees with input token but insufficient base (need extra for fees) +#[test] +fun test_can_place_market_order_ask_input_fee_insufficient() { + let mut test = begin(OWNER); + let registry_id = setup_test(OWNER, &mut test); + + // Create balance manager with only 9 SUI (clearly not enough to sell 10 SUI + fees) + test.next_tx(ALICE); + let balance_manager_id_alice; + { + let mut bm = balance_manager::new(test.ctx()); + // Deposit only 9 SUI - clearly not enough to sell 10 SUI when fees are in base + bm.deposit( + mint_for_testing(9 * constants::float_scaling(), test.ctx()), + test.ctx(), + ); + balance_manager_id_alice = bm.id(); + transfer::public_share_object(bm); + }; + + // Create another balance manager with funds for liquidity + let balance_manager_id_bob = create_acct_and_share_with_funds( + BOB, + 1000000 * constants::float_scaling(), + &mut test, + ); + + // Setup pool + let pool_id = setup_pool_with_default_fees( + OWNER, + registry_id, + true, + false, + &mut test, + ); + + // Place a buy order on the book by Bob + place_limit_order( + BOB, + pool_id, + balance_manager_id_bob, + 1, + constants::no_restriction(), + constants::self_matching_allowed(), + 1 * constants::float_scaling(), // price: 1 USDC per SUI + 100 * constants::float_scaling(), // quantity: 100 SUI + true, // is_bid = true (buy order) + true, // pay_with_deep + constants::max_u64(), + &mut test, + ); + + test.next_tx(ALICE); + { + let pool = test.take_shared_by_id>(pool_id); + let balance_manager = test.take_shared_by_id(balance_manager_id_alice); + let clock = test.take_shared(); + + // Test: try to ask (sell) 10 SUI with pay_with_deep = false + // Should fail because we need 10 SUI + fees, but only have 9 SUI + let can_place = pool.can_place_market_order( + &balance_manager, + 10 * constants::float_scaling(), // quantity: 10 SUI + false, // is_bid = false (ask/sell) + false, // pay_with_deep = false (fees in base) + &clock, + ); + assert!(!can_place); + + return_shared(pool); + return_shared(balance_manager); + return_shared(clock); + }; + + end(test); +} + +/// Test market order with no liquidity on the book +#[test] +fun test_can_place_market_order_no_liquidity() { + let mut test = begin(OWNER); + let registry_id = setup_test(OWNER, &mut test); + let balance_manager_id_alice = create_acct_and_share_with_funds( + ALICE, + 1000000 * constants::float_scaling(), + &mut test, + ); + + // Setup pool WITHOUT any liquidity + let pool_id = setup_pool_with_default_fees( + OWNER, + registry_id, + true, + false, + &mut test, + ); + + test.next_tx(ALICE); + { + let pool = test.take_shared_by_id>(pool_id); + let balance_manager = test.take_shared_by_id(balance_manager_id_alice); + let clock = test.take_shared(); + + // Test: bid for 10 SUI but no sell orders on book + // get_quantity_out will return 0 base_out since there's no liquidity + let can_place = pool.can_place_market_order( + &balance_manager, + 10 * constants::float_scaling(), // quantity: 10 SUI + true, // is_bid + true, // pay_with_deep + &clock, + ); + assert!(!can_place); + + return_shared(pool); + return_shared(balance_manager); + return_shared(clock); + }; + + end(test); +} + +/// Test market order for zero quantity (edge case) +#[test] +fun test_can_place_market_order_zero_quantity() { + let mut test = begin(OWNER); + let registry_id = setup_test(OWNER, &mut test); + let balance_manager_id_alice = create_acct_and_share_with_funds( + ALICE, + 1000000 * constants::float_scaling(), + &mut test, + ); + + let pool_id = setup_pool_with_default_fees( + OWNER, + registry_id, + true, + false, + &mut test, + ); + + test.next_tx(ALICE); + { + let pool = test.take_shared_by_id>(pool_id); + let balance_manager = test.take_shared_by_id(balance_manager_id_alice); + let clock = test.take_shared(); + + // Test: zero quantity should return false (fails min_size check) + let can_place = pool.can_place_market_order( + &balance_manager, + 0, // quantity: 0 + true, // is_bid + true, // pay_with_deep + &clock, + ); + assert!(!can_place); + + return_shared(pool); + return_shared(balance_manager); + return_shared(clock); + }; + + end(test); +} + +/// Test market order exactly at the limit of available balance +#[test] +fun test_can_place_market_order_bid_exact_balance() { + let mut test = begin(OWNER); + let registry_id = setup_test(OWNER, &mut test); + + // Create balance manager with funds for liquidity + let balance_manager_id_bob = create_acct_and_share_with_funds( + BOB, + 1000000 * constants::float_scaling(), + &mut test, + ); + + // Setup pool + let pool_id = setup_pool_with_default_fees( + OWNER, + registry_id, + true, + false, + &mut test, + ); + + // Place a sell order on the book by Bob at price 1 + place_limit_order( + BOB, + pool_id, + balance_manager_id_bob, + 1, + constants::no_restriction(), + constants::self_matching_allowed(), + 1 * constants::float_scaling(), // price: 1 USDC per SUI + 100 * constants::float_scaling(), // quantity: 100 SUI + false, // is_bid = false (sell order) + true, // pay_with_deep + constants::max_u64(), + &mut test, + ); + + // Create Alice's balance manager with exactly enough USDC to buy 10 SUI at price 1 + test.next_tx(ALICE); + let balance_manager_id_alice; + { + let mut bm = balance_manager::new(test.ctx()); + // 10 USDC to buy 10 SUI at price 1 + bm.deposit( + mint_for_testing(10 * constants::float_scaling(), test.ctx()), + test.ctx(), + ); + // Enough DEEP for fees + bm.deposit( + mint_for_testing(1000 * constants::float_scaling(), test.ctx()), + test.ctx(), + ); + balance_manager_id_alice = bm.id(); + transfer::public_share_object(bm); + }; + + test.next_tx(ALICE); + { + let pool = test.take_shared_by_id>(pool_id); + let balance_manager = test.take_shared_by_id(balance_manager_id_alice); + let clock = test.take_shared(); + + // Test: bid for exactly 10 SUI with exactly 10 USDC at price 1 + let can_place = pool.can_place_market_order( + &balance_manager, + 10 * constants::float_scaling(), // quantity: 10 SUI + true, // is_bid + true, // pay_with_deep + &clock, + ); + assert!(can_place); + + // Test: try to bid for 11 SUI (should fail) + let can_place_more = pool.can_place_market_order( + &balance_manager, + 11 * constants::float_scaling(), // quantity: 11 SUI + true, // is_bid + true, // pay_with_deep + &clock, + ); + assert!(!can_place_more); + + return_shared(pool); + return_shared(balance_manager); + return_shared(clock); + }; + + end(test); +} + +// ============== can_place_limit_order tests ============== + +/// Test bid limit order with sufficient quote balance and DEEP for fees +#[test] +fun test_can_place_limit_order_bid_with_deep_sufficient() { + let mut test = begin(OWNER); + let registry_id = setup_test(OWNER, &mut test); + let balance_manager_id_alice = create_acct_and_share_with_funds( + ALICE, + 1000000 * constants::float_scaling(), + &mut test, + ); + + // Setup pool with reference pool (non-whitelisted) so DEEP fees are required + let pool_id = setup_pool_with_default_fees_and_reference_pool( + ALICE, + registry_id, + balance_manager_id_alice, + &mut test, + ); + + test.next_tx(ALICE); + { + let pool = test.take_shared_by_id>(pool_id); + let balance_manager = test.take_shared_by_id(balance_manager_id_alice); + let clock = clock::create_for_testing(test.ctx()); + + // Test: bid for 10 SUI at price 2 with pay_with_deep = true + // Required quote = 10 * 2 = 20 USDC + DEEP fees + let can_place = pool.can_place_limit_order( + &balance_manager, + 2 * constants::float_scaling(), // price: 2 USDC per SUI + 10 * constants::float_scaling(), // quantity: 10 SUI + true, // is_bid + true, // pay_with_deep + constants::max_u64(), // expire_timestamp + &clock, + ); + assert!(can_place); + + clock.destroy_for_testing(); + return_shared(pool); + return_shared(balance_manager); + }; + + end(test); +} + +/// Test bid limit order with insufficient quote balance +#[test] +fun test_can_place_limit_order_bid_insufficient_quote() { + let mut test = begin(OWNER); + let registry_id = setup_test(OWNER, &mut test); + + // Create balance manager with minimal funds + test.next_tx(ALICE); + let balance_manager_id_alice; + { + let mut bm = balance_manager::new(test.ctx()); + // Only deposit 10 USDC (not enough to buy 10 SUI at price 2 = 20 USDC) + bm.deposit( + mint_for_testing(10 * constants::float_scaling(), test.ctx()), + test.ctx(), + ); + bm.deposit( + mint_for_testing(1000 * constants::float_scaling(), test.ctx()), + test.ctx(), + ); + balance_manager_id_alice = bm.id(); + transfer::public_share_object(bm); + }; + + // Setup pool + let pool_id = setup_pool_with_default_fees( + OWNER, + registry_id, + true, + false, + &mut test, + ); + + test.next_tx(ALICE); + { + let pool = test.take_shared_by_id>(pool_id); + let balance_manager = test.take_shared_by_id(balance_manager_id_alice); + let clock = clock::create_for_testing(test.ctx()); + + // Test: try to bid for 10 SUI at price 2 but only have 10 USDC (need 20) + let can_place = pool.can_place_limit_order( + &balance_manager, + 2 * constants::float_scaling(), // price: 2 USDC per SUI + 10 * constants::float_scaling(), // quantity: 10 SUI + true, // is_bid + true, // pay_with_deep + constants::max_u64(), // expire_timestamp + &clock, + ); + assert!(!can_place); + + clock.destroy_for_testing(); + return_shared(pool); + return_shared(balance_manager); + }; + + end(test); +} + +/// Test bid limit order with insufficient DEEP for fees (non-whitelisted pool) +#[test] +fun test_can_place_limit_order_bid_insufficient_deep() { + let mut test = begin(OWNER); + let registry_id = setup_test(OWNER, &mut test); + + // Create balance manager with USDC but no DEEP + test.next_tx(ALICE); + let balance_manager_id_alice; + { + let mut bm = balance_manager::new(test.ctx()); + bm.deposit( + mint_for_testing(1000 * constants::float_scaling(), test.ctx()), + test.ctx(), + ); + // No DEEP deposited + balance_manager_id_alice = bm.id(); + transfer::public_share_object(bm); + }; + + // Create balance manager for Bob with funds for reference pool setup + let balance_manager_id_bob = create_acct_and_share_with_funds( + BOB, + 1000000 * constants::float_scaling(), + &mut test, + ); + + // Setup pool with reference pool (non-whitelisted) so DEEP is required for fees + let pool_id = setup_pool_with_default_fees_and_reference_pool( + BOB, + registry_id, + balance_manager_id_bob, + &mut test, + ); + + test.next_tx(ALICE); + { + let pool = test.take_shared_by_id>(pool_id); + let balance_manager = test.take_shared_by_id(balance_manager_id_alice); + let clock = clock::create_for_testing(test.ctx()); + + // Test: try to bid for 10 SUI with pay_with_deep but no DEEP + let can_place = pool.can_place_limit_order( + &balance_manager, + 2 * constants::float_scaling(), // price: 2 USDC per SUI + 10 * constants::float_scaling(), // quantity: 10 SUI + true, // is_bid + true, // pay_with_deep + constants::max_u64(), // expire_timestamp + &clock, + ); + assert!(!can_place); + + clock.destroy_for_testing(); + return_shared(pool); + return_shared(balance_manager); + }; + + end(test); +} + +/// Test bid limit order with exactly the DEEP needed for taker fees +#[test] +fun test_can_place_limit_order_bid_exact_deep_for_taker_fee() { + let mut test = begin(OWNER); + let registry_id = setup_test(OWNER, &mut test); + + // Calculate the exact DEEP fee needed for a bid of 10 SUI at price 2 + // The SUI/DEEP reference pool sets deep_per_base (asset_is_base = true) + // So deep_quantity = math::mul(base_quantity, deep_per_asset) + // actual_deep_fee = math::mul(taker_fee, deep_quantity) + let price = 2 * constants::float_scaling(); + let quantity = 10 * constants::float_scaling(); + let quote_quantity = math::mul(quantity, price); + let deep_quantity = math::mul(quantity, constants::deep_multiplier()); // Use base quantity + let exact_deep_fee = math::mul(constants::taker_fee(), deep_quantity); + + // Create balance manager with exactly enough USDC and exactly the DEEP fee needed + test.next_tx(ALICE); + let balance_manager_id_alice; + { + let mut bm = balance_manager::new(test.ctx()); + bm.deposit( + mint_for_testing(quote_quantity, test.ctx()), + test.ctx(), + ); + bm.deposit( + mint_for_testing(exact_deep_fee, test.ctx()), + test.ctx(), + ); + balance_manager_id_alice = bm.id(); + transfer::public_share_object(bm); + }; + + // Create balance manager for Bob with funds for reference pool setup + let balance_manager_id_bob = create_acct_and_share_with_funds( + BOB, + 1000000 * constants::float_scaling(), + &mut test, + ); + + // Setup pool with reference pool (non-whitelisted) so DEEP is required for fees + let pool_id = setup_pool_with_default_fees_and_reference_pool( + BOB, + registry_id, + balance_manager_id_bob, + &mut test, + ); + + test.next_tx(ALICE); + { + let pool = test.take_shared_by_id>(pool_id); + let balance_manager = test.take_shared_by_id(balance_manager_id_alice); + let clock = clock::create_for_testing(test.ctx()); + + // Test: bid for 10 SUI at price 2 with exactly the DEEP needed + let can_place = pool.can_place_limit_order( + &balance_manager, + price, + quantity, + true, // is_bid + true, // pay_with_deep + constants::max_u64(), + &clock, + ); + assert!(can_place); + + clock.destroy_for_testing(); + return_shared(pool); + return_shared(balance_manager); + }; + + end(test); +} + +/// Test bid limit order fails with one less unit of DEEP than needed +#[test] +fun test_can_place_limit_order_bid_one_less_deep_than_needed() { + let mut test = begin(OWNER); + let registry_id = setup_test(OWNER, &mut test); + + // Calculate the exact DEEP fee needed (using base quantity since asset_is_base = true) + let price = 2 * constants::float_scaling(); + let quantity = 10 * constants::float_scaling(); + let quote_quantity = math::mul(quantity, price); + let deep_quantity = math::mul(quantity, constants::deep_multiplier()); // Use base quantity + let exact_deep_fee = math::mul(constants::taker_fee(), deep_quantity); + + // Create balance manager with exactly enough USDC but one less DEEP than needed + test.next_tx(ALICE); + let balance_manager_id_alice; + { + let mut bm = balance_manager::new(test.ctx()); + bm.deposit( + mint_for_testing(quote_quantity, test.ctx()), + test.ctx(), + ); + bm.deposit( + mint_for_testing(exact_deep_fee - 1, test.ctx()), + test.ctx(), + ); + balance_manager_id_alice = bm.id(); + transfer::public_share_object(bm); + }; + + // Create balance manager for Bob with funds for reference pool setup + let balance_manager_id_bob = create_acct_and_share_with_funds( + BOB, + 1000000 * constants::float_scaling(), + &mut test, + ); + + // Setup pool with reference pool (non-whitelisted) so DEEP is required for fees + let pool_id = setup_pool_with_default_fees_and_reference_pool( + BOB, + registry_id, + balance_manager_id_bob, + &mut test, + ); + + test.next_tx(ALICE); + { + let pool = test.take_shared_by_id>(pool_id); + let balance_manager = test.take_shared_by_id(balance_manager_id_alice); + let clock = clock::create_for_testing(test.ctx()); + + // Test: bid for 10 SUI at price 2 with one less DEEP than needed should fail + let can_place = pool.can_place_limit_order( + &balance_manager, + price, + quantity, + true, // is_bid + true, // pay_with_deep + constants::max_u64(), + &clock, + ); + assert!(!can_place); + + clock.destroy_for_testing(); + return_shared(pool); + return_shared(balance_manager); + }; + + end(test); +} + +/// Test ask limit order with sufficient base balance and DEEP for fees +#[test] +fun test_can_place_limit_order_ask_with_deep_sufficient() { + let mut test = begin(OWNER); + let registry_id = setup_test(OWNER, &mut test); + let balance_manager_id_alice = create_acct_and_share_with_funds( + ALICE, + 1000000 * constants::float_scaling(), + &mut test, + ); + + // Setup pool with reference pool (non-whitelisted) so DEEP fees are required + let pool_id = setup_pool_with_default_fees_and_reference_pool( + ALICE, + registry_id, + balance_manager_id_alice, + &mut test, + ); + + test.next_tx(ALICE); + { + let pool = test.take_shared_by_id>(pool_id); + let balance_manager = test.take_shared_by_id(balance_manager_id_alice); + let clock = clock::create_for_testing(test.ctx()); + + // Test: ask (sell) 10 SUI at price 2 with pay_with_deep = true + let can_place = pool.can_place_limit_order( + &balance_manager, + 2 * constants::float_scaling(), // price: 2 USDC per SUI + 10 * constants::float_scaling(), // quantity: 10 SUI + false, // is_bid = false (ask/sell) + true, // pay_with_deep + constants::max_u64(), // expire_timestamp + &clock, + ); + assert!(can_place); + + clock.destroy_for_testing(); + return_shared(pool); + return_shared(balance_manager); + }; + + end(test); +} + +/// Test ask limit order with insufficient base balance +#[test] +fun test_can_place_limit_order_ask_insufficient_base() { + let mut test = begin(OWNER); + let registry_id = setup_test(OWNER, &mut test); + + // Create balance manager with minimal SUI + test.next_tx(ALICE); + let balance_manager_id_alice; + { + let mut bm = balance_manager::new(test.ctx()); + // Only deposit 5 SUI (not enough to sell 10 SUI) + bm.deposit( + mint_for_testing(5 * constants::float_scaling(), test.ctx()), + test.ctx(), + ); + bm.deposit( + mint_for_testing(1000 * constants::float_scaling(), test.ctx()), + test.ctx(), + ); + balance_manager_id_alice = bm.id(); + transfer::public_share_object(bm); + }; + + // Setup pool + let pool_id = setup_pool_with_default_fees( + OWNER, + registry_id, + true, + false, + &mut test, + ); + + test.next_tx(ALICE); + { + let pool = test.take_shared_by_id>(pool_id); + let balance_manager = test.take_shared_by_id(balance_manager_id_alice); + let clock = clock::create_for_testing(test.ctx()); + + // Test: try to ask (sell) 10 SUI but only have 5 SUI + let can_place = pool.can_place_limit_order( + &balance_manager, + 2 * constants::float_scaling(), // price: 2 USDC per SUI + 10 * constants::float_scaling(), // quantity: 10 SUI + false, // is_bid = false (ask/sell) + true, // pay_with_deep + constants::max_u64(), // expire_timestamp + &clock, + ); + assert!(!can_place); + + clock.destroy_for_testing(); + return_shared(pool); + return_shared(balance_manager); + }; + + end(test); +} + +/// Test ask limit order with insufficient DEEP for fees (non-whitelisted pool) +#[test] +fun test_can_place_limit_order_ask_insufficient_deep() { + let mut test = begin(OWNER); + let registry_id = setup_test(OWNER, &mut test); + + // Create balance manager with SUI but no DEEP + test.next_tx(ALICE); + let balance_manager_id_alice; + { + let mut bm = balance_manager::new(test.ctx()); + bm.deposit( + mint_for_testing(100 * constants::float_scaling(), test.ctx()), + test.ctx(), + ); + // No DEEP deposited + balance_manager_id_alice = bm.id(); + transfer::public_share_object(bm); + }; + + // Create balance manager for Bob with funds for reference pool setup + let balance_manager_id_bob = create_acct_and_share_with_funds( + BOB, + 1000000 * constants::float_scaling(), + &mut test, + ); + + // Setup pool with reference pool (non-whitelisted) so DEEP is required for fees + let pool_id = setup_pool_with_default_fees_and_reference_pool( + BOB, + registry_id, + balance_manager_id_bob, + &mut test, + ); + + test.next_tx(ALICE); + { + let pool = test.take_shared_by_id>(pool_id); + let balance_manager = test.take_shared_by_id(balance_manager_id_alice); + let clock = clock::create_for_testing(test.ctx()); + + // Test: try to ask (sell) 10 SUI with pay_with_deep but no DEEP + let can_place = pool.can_place_limit_order( + &balance_manager, + 2 * constants::float_scaling(), // price: 2 USDC per SUI + 10 * constants::float_scaling(), // quantity: 10 SUI + false, // is_bid = false (ask/sell) + true, // pay_with_deep + constants::max_u64(), // expire_timestamp + &clock, + ); + assert!(!can_place); + + clock.destroy_for_testing(); + return_shared(pool); + return_shared(balance_manager); + }; + + end(test); +} + +/// Test bid limit order paying fees with input token (quote) +#[test] +fun test_can_place_limit_order_bid_input_fee_sufficient() { + let mut test = begin(OWNER); + let registry_id = setup_test(OWNER, &mut test); + let balance_manager_id_alice = create_acct_and_share_with_funds( + ALICE, + 1000000 * constants::float_scaling(), + &mut test, + ); + + // Setup pool + let pool_id = setup_pool_with_default_fees( + OWNER, + registry_id, + true, + false, + &mut test, + ); + + test.next_tx(ALICE); + { + let pool = test.take_shared_by_id>(pool_id); + let balance_manager = test.take_shared_by_id(balance_manager_id_alice); + let clock = clock::create_for_testing(test.ctx()); + + // Test: bid for 10 SUI at price 2 with pay_with_deep = false (pay fees in USDC) + let can_place = pool.can_place_limit_order( + &balance_manager, + 2 * constants::float_scaling(), // price: 2 USDC per SUI + 10 * constants::float_scaling(), // quantity: 10 SUI + true, // is_bid + false, // pay_with_deep = false (fees in quote) + constants::max_u64(), // expire_timestamp + &clock, + ); + assert!(can_place); + + clock.destroy_for_testing(); + return_shared(pool); + return_shared(balance_manager); + }; + + end(test); +} + +/// Test ask limit order paying fees with input token (base) +#[test] +fun test_can_place_limit_order_ask_input_fee_sufficient() { + let mut test = begin(OWNER); + let registry_id = setup_test(OWNER, &mut test); + let balance_manager_id_alice = create_acct_and_share_with_funds( + ALICE, + 1000000 * constants::float_scaling(), + &mut test, + ); + + // Setup pool + let pool_id = setup_pool_with_default_fees( + OWNER, + registry_id, + true, + false, + &mut test, + ); + + test.next_tx(ALICE); + { + let pool = test.take_shared_by_id>(pool_id); + let balance_manager = test.take_shared_by_id(balance_manager_id_alice); + let clock = clock::create_for_testing(test.ctx()); + + // Test: ask (sell) 10 SUI at price 2 with pay_with_deep = false (pay fees in SUI) + let can_place = pool.can_place_limit_order( + &balance_manager, + 2 * constants::float_scaling(), // price: 2 USDC per SUI + 10 * constants::float_scaling(), // quantity: 10 SUI + false, // is_bid = false (ask/sell) + false, // pay_with_deep = false (fees in base) + constants::max_u64(), // expire_timestamp + &clock, + ); + assert!(can_place); + + clock.destroy_for_testing(); + return_shared(pool); + return_shared(balance_manager); + }; + + end(test); +} + +/// Test ask limit order paying fees with input token but insufficient base (need extra for fees) +#[test] +fun test_can_place_limit_order_ask_input_fee_insufficient() { + let mut test = begin(OWNER); + let registry_id = setup_test(OWNER, &mut test); + + // Create balance manager with only 9 SUI (not enough to sell 10 SUI + fees) + test.next_tx(ALICE); + let balance_manager_id_alice; + { + let mut bm = balance_manager::new(test.ctx()); + // Deposit only 9 SUI - not enough to sell 10 SUI when fees are in base + bm.deposit( + mint_for_testing(9 * constants::float_scaling(), test.ctx()), + test.ctx(), + ); + balance_manager_id_alice = bm.id(); + transfer::public_share_object(bm); + }; + + // Setup pool + let pool_id = setup_pool_with_default_fees( + OWNER, + registry_id, + true, + false, + &mut test, + ); + + test.next_tx(ALICE); + { + let pool = test.take_shared_by_id>(pool_id); + let balance_manager = test.take_shared_by_id(balance_manager_id_alice); + let clock = clock::create_for_testing(test.ctx()); + + // Test: try to ask (sell) 10 SUI with pay_with_deep = false + // Should fail because we need 10 SUI + fees, but only have 9 SUI + let can_place = pool.can_place_limit_order( + &balance_manager, + 2 * constants::float_scaling(), // price: 2 USDC per SUI + 10 * constants::float_scaling(), // quantity: 10 SUI + false, // is_bid = false (ask/sell) + false, // pay_with_deep = false (fees in base) + constants::max_u64(), // expire_timestamp + &clock, + ); + assert!(!can_place); + + clock.destroy_for_testing(); + return_shared(pool); + return_shared(balance_manager); + }; + + end(test); +} + +/// Test limit order for zero quantity (edge case) +#[test] +fun test_can_place_limit_order_zero_quantity() { + let mut test = begin(OWNER); + let registry_id = setup_test(OWNER, &mut test); + let balance_manager_id_alice = create_acct_and_share_with_funds( + ALICE, + 1000000 * constants::float_scaling(), + &mut test, + ); + + let pool_id = setup_pool_with_default_fees( + OWNER, + registry_id, + true, + false, + &mut test, + ); + + test.next_tx(ALICE); + { + let pool = test.take_shared_by_id>(pool_id); + let balance_manager = test.take_shared_by_id(balance_manager_id_alice); + let clock = clock::create_for_testing(test.ctx()); + + // Test: zero quantity should return false (fails min_size check) + let can_place = pool.can_place_limit_order( + &balance_manager, + 2 * constants::float_scaling(), // price: 2 USDC per SUI + 0, // quantity: 0 + true, // is_bid + true, // pay_with_deep + constants::max_u64(), // expire_timestamp + &clock, + ); + assert!(!can_place); + + clock.destroy_for_testing(); + return_shared(pool); + return_shared(balance_manager); + }; + + end(test); +} + +/// Test limit order exactly at the limit of available balance +#[test] +fun test_can_place_limit_order_bid_exact_balance() { + let mut test = begin(OWNER); + let registry_id = setup_test(OWNER, &mut test); + + // Create Alice's balance manager with exactly enough USDC to bid for 10 SUI at price 2 + test.next_tx(ALICE); + let balance_manager_id_alice; + { + let mut bm = balance_manager::new(test.ctx()); + // 20 USDC to buy 10 SUI at price 2 + bm.deposit( + mint_for_testing(20 * constants::float_scaling(), test.ctx()), + test.ctx(), + ); + // Enough DEEP for fees + bm.deposit( + mint_for_testing(1000 * constants::float_scaling(), test.ctx()), + test.ctx(), + ); + balance_manager_id_alice = bm.id(); + transfer::public_share_object(bm); + }; + + // Setup pool (whitelisted, so DEEP fees are 0) + let pool_id = setup_pool_with_default_fees( + OWNER, + registry_id, + true, + false, + &mut test, + ); + + test.next_tx(ALICE); + { + let pool = test.take_shared_by_id>(pool_id); + let balance_manager = test.take_shared_by_id(balance_manager_id_alice); + let clock = clock::create_for_testing(test.ctx()); + + // Test: bid for exactly 10 SUI at price 2 with exactly 20 USDC + let can_place = pool.can_place_limit_order( + &balance_manager, + 2 * constants::float_scaling(), // price: 2 USDC per SUI + 10 * constants::float_scaling(), // quantity: 10 SUI + true, // is_bid + true, // pay_with_deep + constants::max_u64(), // expire_timestamp + &clock, + ); + assert!(can_place); + + // Test: try to bid for 11 SUI at price 2 (need 22 USDC, only have 20) + let can_place_more = pool.can_place_limit_order( + &balance_manager, + 2 * constants::float_scaling(), // price: 2 USDC per SUI + 11 * constants::float_scaling(), // quantity: 11 SUI + true, // is_bid + true, // pay_with_deep + constants::max_u64(), // expire_timestamp + &clock, + ); + assert!(!can_place_more); + + clock.destroy_for_testing(); + return_shared(pool); + return_shared(balance_manager); + }; + + end(test); +} + +/// Test limit order with different prices +#[test] +fun test_can_place_limit_order_price_variations() { + let mut test = begin(OWNER); + let registry_id = setup_test(OWNER, &mut test); + + // Create balance manager with 100 USDC + test.next_tx(ALICE); + let balance_manager_id_alice; + { + let mut bm = balance_manager::new(test.ctx()); + bm.deposit( + mint_for_testing(100 * constants::float_scaling(), test.ctx()), + test.ctx(), + ); + bm.deposit( + mint_for_testing(1000 * constants::float_scaling(), test.ctx()), + test.ctx(), + ); + balance_manager_id_alice = bm.id(); + transfer::public_share_object(bm); + }; + + // Setup pool (whitelisted) + let pool_id = setup_pool_with_default_fees( + OWNER, + registry_id, + true, + false, + &mut test, + ); + + test.next_tx(ALICE); + { + let pool = test.take_shared_by_id>(pool_id); + let balance_manager = test.take_shared_by_id(balance_manager_id_alice); + let clock = clock::create_for_testing(test.ctx()); + + // Test: bid for 10 SUI at price 5 (need 50 USDC, have 100) + let can_place_low_price = pool.can_place_limit_order( + &balance_manager, + 5 * constants::float_scaling(), // price: 5 USDC per SUI + 10 * constants::float_scaling(), // quantity: 10 SUI + true, // is_bid + true, // pay_with_deep + constants::max_u64(), // expire_timestamp + &clock, + ); + assert!(can_place_low_price); + + // Test: bid for 10 SUI at price 15 (need 150 USDC, only have 100) + let can_place_high_price = pool.can_place_limit_order( + &balance_manager, + 15 * constants::float_scaling(), // price: 15 USDC per SUI + 10 * constants::float_scaling(), // quantity: 10 SUI + true, // is_bid + true, // pay_with_deep + constants::max_u64(), // expire_timestamp + &clock, + ); + assert!(!can_place_high_price); + + clock.destroy_for_testing(); + return_shared(pool); + return_shared(balance_manager); + }; + + end(test); +} + +/// Test that fee_penalty_multiplier (1.25) is correctly applied only once +/// For a sell order of 1 SUI with input token fee: +/// required_base = quantity * (1 + fee_penalty_multiplier * taker_fee) +/// = 1 * (1 + 1.25 * 0.001) = 1.00125 SUI +#[test] +fun test_can_place_limit_order_fee_penalty_not_doubled() { + let mut test = begin(OWNER); + let registry_id = setup_test(OWNER, &mut test); + + // Calculate exact required amount: + // taker_fee = 1_000_000 (0.001 or 0.1%) + // fee_penalty_multiplier = 1_250_000_000 (1.25) + // For 1 SUI (1_000_000_000 base units): + // fee_balances.base() = 1_000_000_000 * 1.25 = 1_250_000_000 + // fee_base = 1_250_000_000 * 0.001 = 1_250_000 + // required_base = 1_000_000_000 + 1_250_000 = 1_001_250_000 + let quantity = constants::float_scaling(); // 1 SUI = 1_000_000_000 + let required_with_fee = 1_001_250_000u64; // 1.00125 SUI + + // Create balance manager for setup with lots of funds + let balance_manager_id_setup = create_acct_and_share_with_funds( + OWNER, + 1000000 * constants::float_scaling(), + &mut test, + ); + + // Create balance manager with exactly enough (should pass) + test.next_tx(ALICE); + let balance_manager_id_exact; + { + let mut bm = balance_manager::new(test.ctx()); + bm.deposit( + mint_for_testing(required_with_fee, test.ctx()), + test.ctx(), + ); + balance_manager_id_exact = bm.id(); + transfer::public_share_object(bm); + }; + + // Create balance manager with 1 less (should fail) + test.next_tx(BOB); + let balance_manager_id_insufficient; + { + let mut bm = balance_manager::new(test.ctx()); + bm.deposit( + mint_for_testing(required_with_fee - 1, test.ctx()), + test.ctx(), + ); + balance_manager_id_insufficient = bm.id(); + transfer::public_share_object(bm); + }; + + // Setup pool with reference pool to get proper fees (non-whitelisted) + let pool_id = setup_pool_with_default_fees_and_reference_pool( + OWNER, + registry_id, + balance_manager_id_setup, + &mut test, + ); + + test.next_tx(ALICE); + { + let pool = test.take_shared_by_id>(pool_id); + let balance_manager_exact = test.take_shared_by_id( + balance_manager_id_exact, + ); + let balance_manager_insufficient = test.take_shared_by_id( + balance_manager_id_insufficient, + ); + let clock = clock::create_for_testing(test.ctx()); + + // Verify taker fee is set correctly + let (taker_fee, _, _) = pool.pool_trade_params(); + assert!(taker_fee == constants::taker_fee()); + + // Test with exactly enough balance - should pass + let can_place_exact = pool.can_place_limit_order( + &balance_manager_exact, + 1 * constants::float_scaling(), // price: 1 USDC per SUI + quantity, // quantity: 1 SUI + false, // is_bid = false (ask/sell) + false, // pay_with_deep = false (fees in base) + constants::max_u64(), // expire_timestamp + &clock, + ); + assert!(can_place_exact); + + // Test with 1 unit less - should fail + let can_place_insufficient = pool.can_place_limit_order( + &balance_manager_insufficient, + 1 * constants::float_scaling(), // price: 1 USDC per SUI + quantity, // quantity: 1 SUI + false, // is_bid = false (ask/sell) + false, // pay_with_deep = false (fees in base) + constants::max_u64(), // expire_timestamp + &clock, + ); + assert!(!can_place_insufficient); + + clock.destroy_for_testing(); + return_shared(pool); + return_shared(balance_manager_exact); + return_shared(balance_manager_insufficient); + }; + + end(test); +} + +/// Test limit order with expired timestamp (should return false even with sufficient balance) +#[test] +fun test_can_place_limit_order_expired_timestamp() { + let mut test = begin(OWNER); + let registry_id = setup_test(OWNER, &mut test); + let balance_manager_id_alice = create_acct_and_share_with_funds( + ALICE, + 1000000 * constants::float_scaling(), + &mut test, + ); + + // Setup pool + let pool_id = setup_pool_with_default_fees( + OWNER, + registry_id, + true, + false, + &mut test, + ); + + test.next_tx(ALICE); + { + let pool = test.take_shared_by_id>(pool_id); + let balance_manager = test.take_shared_by_id(balance_manager_id_alice); + let mut clock = clock::create_for_testing(test.ctx()); + + // Set clock to 1000ms + clock.set_for_testing(1000); + + // Test: sufficient balance but expire_timestamp is in the past (500ms < 1000ms) + // Should return false because the order would be expired + let can_place = pool.can_place_limit_order( + &balance_manager, + 2 * constants::float_scaling(), // price: 2 USDC per SUI + 10 * constants::float_scaling(), // quantity: 10 SUI + true, // is_bid + true, // pay_with_deep + 500, // expire_timestamp: 500ms (in the past) + &clock, + ); + assert!(!can_place); + + // Test: same order but with future expire_timestamp should succeed + let can_place_future = pool.can_place_limit_order( + &balance_manager, + 2 * constants::float_scaling(), // price: 2 USDC per SUI + 10 * constants::float_scaling(), // quantity: 10 SUI + true, // is_bid + true, // pay_with_deep + 2000, // expire_timestamp: 2000ms (in the future) + &clock, + ); + assert!(can_place_future); + + // Test: expire_timestamp exactly at current time should return true + // (order is valid at the moment of expiration) + let can_place_exact = pool.can_place_limit_order( + &balance_manager, + 2 * constants::float_scaling(), // price: 2 USDC per SUI + 10 * constants::float_scaling(), // quantity: 10 SUI + true, // is_bid + true, // pay_with_deep + 1001, // expire_timestamp: 1001ms (just after current time) + &clock, + ); + assert!(can_place_exact); + + clock.destroy_for_testing(); + return_shared(pool); + return_shared(balance_manager); + }; + + end(test); +} + +/// Test that can_place_limit_order includes settled balances +/// Without settled balances, Alice wouldn't have enough USDC to place a bid. +/// With settled balances from a previous trade, she can place the order. +#[test] +fun test_can_place_limit_order_with_settled_balances() { + let mut test = begin(OWNER); + let registry_id = setup_test(OWNER, &mut test); + + // Create Alice's balance manager with only SUI (no USDC) + test.next_tx(ALICE); + let balance_manager_id_alice; + { + let mut bm = balance_manager::new(test.ctx()); + // Alice has 100 SUI but NO USDC + bm.deposit( + mint_for_testing(100 * constants::float_scaling(), test.ctx()), + test.ctx(), + ); + bm.deposit( + mint_for_testing(1000 * constants::float_scaling(), test.ctx()), + test.ctx(), + ); + balance_manager_id_alice = bm.id(); + transfer::public_share_object(bm); + }; + + // Create Bob's balance manager with USDC to buy Alice's SUI + test.next_tx(BOB); + let balance_manager_id_bob; + { + let mut bm = balance_manager::new(test.ctx()); + // Bob has USDC to buy SUI + bm.deposit( + mint_for_testing(200 * constants::float_scaling(), test.ctx()), + test.ctx(), + ); + bm.deposit( + mint_for_testing(1000 * constants::float_scaling(), test.ctx()), + test.ctx(), + ); + balance_manager_id_bob = bm.id(); + transfer::public_share_object(bm); + }; + + // Setup whitelisted pool (no DEEP fees required for simplicity) + let pool_id = setup_pool_with_default_fees( + OWNER, + registry_id, + true, + false, + &mut test, + ); + + // Alice places a limit sell order: sell 10 SUI at price 2 USDC per SUI + let client_order_id = 1; + place_limit_order( + ALICE, + pool_id, + balance_manager_id_alice, + client_order_id, + constants::no_restriction(), + constants::self_matching_allowed(), + 2 * constants::float_scaling(), // price: 2 USDC per SUI + 10 * constants::float_scaling(), // quantity: 10 SUI + false, // is_bid = false (sell/ask) + true, // pay_with_deep + constants::max_u64(), + &mut test, + ); + + // Bob places a market buy order: buy 10 SUI (pays 20 USDC) + // This fills Alice's order, giving Alice 20 USDC in settled balances + place_market_order( + BOB, + pool_id, + balance_manager_id_bob, + 2, + constants::self_matching_allowed(), + 10 * constants::float_scaling(), // quantity: 10 SUI + true, // is_bid = true (buy) + true, // pay_with_deep + &mut test, + ); + + // Now test: Alice has 0 direct USDC, but has 20 USDC settled from the trade + // She should be able to place a bid order for 5 SUI at price 2 (needs 10 USDC) + test.next_tx(ALICE); + { + let pool = test.take_shared_by_id>(pool_id); + let balance_manager_alice = test.take_shared_by_id( + balance_manager_id_alice, + ); + let clock = clock::create_for_testing(test.ctx()); + + // Verify Alice has 0 direct USDC balance + let direct_usdc_balance = balance_manager_alice.balance(); + assert!(direct_usdc_balance == 0); + + // But can_place_limit_order should return true because of settled balances + // Bid for 5 SUI at price 2 = 10 USDC required (she has 20 USDC settled) + let can_place = pool.can_place_limit_order( + &balance_manager_alice, + 2 * constants::float_scaling(), // price: 2 USDC per SUI + 5 * constants::float_scaling(), // quantity: 5 SUI + true, // is_bid = true (buy) + true, // pay_with_deep + constants::max_u64(), + &clock, + ); + assert!(can_place); + + // Also verify that without enough settled balance, it would fail + // Bid for 15 SUI at price 2 = 30 USDC required (she only has 20 USDC settled) + let can_place_too_much = pool.can_place_limit_order( + &balance_manager_alice, + 2 * constants::float_scaling(), // price: 2 USDC per SUI + 15 * constants::float_scaling(), // quantity: 15 SUI + true, // is_bid = true (buy) + true, // pay_with_deep + constants::max_u64(), + &clock, + ); + assert!(!can_place_too_much); + + clock.destroy_for_testing(); + return_shared(pool); + return_shared(balance_manager_alice); + }; + + end(test); +} + +/// Test limit order with price = 0 (should fail min price check) +#[test] +fun test_can_place_limit_order_price_zero() { + let mut test = begin(OWNER); + let registry_id = setup_test(OWNER, &mut test); + let balance_manager_id_alice = create_acct_and_share_with_funds( + ALICE, + 1000000 * constants::float_scaling(), + &mut test, + ); + + let pool_id = setup_pool_with_default_fees( + OWNER, + registry_id, + true, + false, + &mut test, + ); + + test.next_tx(ALICE); + { + let pool = test.take_shared_by_id>(pool_id); + let balance_manager = test.take_shared_by_id(balance_manager_id_alice); + let clock = clock::create_for_testing(test.ctx()); + + // Test: price = 0 should return false (fails min price check) + let can_place = pool.can_place_limit_order( + &balance_manager, + 0, // price: 0 (below min_price) + 10 * constants::float_scaling(), // quantity: 10 SUI + true, // is_bid + true, // pay_with_deep + constants::max_u64(), // expire_timestamp + &clock, + ); + assert!(!can_place); + + clock.destroy_for_testing(); + return_shared(pool); + return_shared(balance_manager); + }; + + end(test); +} + +/// Test limit order with price = max_u64 (should fail max price check) +#[test] +fun test_can_place_limit_order_price_max_u64() { + let mut test = begin(OWNER); + let registry_id = setup_test(OWNER, &mut test); + let balance_manager_id_alice = create_acct_and_share_with_funds( + ALICE, + 1000000 * constants::float_scaling(), + &mut test, + ); + + let pool_id = setup_pool_with_default_fees( + OWNER, + registry_id, + true, + false, + &mut test, + ); + + test.next_tx(ALICE); + { + let pool = test.take_shared_by_id>(pool_id); + let balance_manager = test.take_shared_by_id(balance_manager_id_alice); + let clock = clock::create_for_testing(test.ctx()); + + // Test: price = max_u64 should return false (exceeds max_price) + let can_place = pool.can_place_limit_order( + &balance_manager, + constants::max_u64(), // price: max_u64 (above max_price) + 10 * constants::float_scaling(), // quantity: 10 SUI + true, // is_bid + true, // pay_with_deep + constants::max_u64(), // expire_timestamp + &clock, + ); + assert!(!can_place); + + clock.destroy_for_testing(); + return_shared(pool); + return_shared(balance_manager); + }; + + end(test); +} + +/// Test that can_place_market_order includes settled balances +/// Without settled balances, Alice wouldn't have enough USDC to place a market bid. +/// With settled balances from a previous trade, she can place the order. +#[test] +fun test_can_place_market_order_with_settled_balances() { + let mut test = begin(OWNER); + let registry_id = setup_test(OWNER, &mut test); + + // Create Alice's balance manager with only SUI (no USDC) + test.next_tx(ALICE); + let balance_manager_id_alice; + { + let mut bm = balance_manager::new(test.ctx()); + // Alice has 100 SUI but NO USDC + bm.deposit( + mint_for_testing(100 * constants::float_scaling(), test.ctx()), + test.ctx(), + ); + bm.deposit( + mint_for_testing(1000 * constants::float_scaling(), test.ctx()), + test.ctx(), + ); + balance_manager_id_alice = bm.id(); + transfer::public_share_object(bm); + }; + + // Create Bob's balance manager with USDC to buy Alice's SUI + test.next_tx(BOB); + let balance_manager_id_bob; + { + let mut bm = balance_manager::new(test.ctx()); + // Bob has USDC to buy SUI + bm.deposit( + mint_for_testing(200 * constants::float_scaling(), test.ctx()), + test.ctx(), + ); + bm.deposit( + mint_for_testing(1000 * constants::float_scaling(), test.ctx()), + test.ctx(), + ); + balance_manager_id_bob = bm.id(); + transfer::public_share_object(bm); + }; + + // Create Carol's balance manager to provide liquidity (sell orders for Alice to buy) + test.next_tx(@0xCCCC); + let balance_manager_id_carol; + { + let mut bm = balance_manager::new(test.ctx()); + bm.deposit( + mint_for_testing(100 * constants::float_scaling(), test.ctx()), + test.ctx(), + ); + bm.deposit( + mint_for_testing(1000 * constants::float_scaling(), test.ctx()), + test.ctx(), + ); + balance_manager_id_carol = bm.id(); + transfer::public_share_object(bm); + }; + + // Setup whitelisted pool + let pool_id = setup_pool_with_default_fees( + OWNER, + registry_id, + true, + false, + &mut test, + ); + + // Alice places a limit sell order: sell 10 SUI at price 2 USDC per SUI + place_limit_order( + ALICE, + pool_id, + balance_manager_id_alice, + 1, + constants::no_restriction(), + constants::self_matching_allowed(), + 2 * constants::float_scaling(), + 10 * constants::float_scaling(), + false, // sell + true, + constants::max_u64(), + &mut test, + ); + + // Bob places a market buy order: buy 10 SUI (pays 20 USDC) + // This fills Alice's order, giving Alice 20 USDC in settled balances + place_market_order( + BOB, + pool_id, + balance_manager_id_bob, + 2, + constants::self_matching_allowed(), + 10 * constants::float_scaling(), + true, // buy + true, + &mut test, + ); + + // Carol places sell orders so Alice has liquidity to buy against + place_limit_order( + @0xCCCC, + pool_id, + balance_manager_id_carol, + 3, + constants::no_restriction(), + constants::self_matching_allowed(), + 2 * constants::float_scaling(), + 50 * constants::float_scaling(), + false, // sell + true, + constants::max_u64(), + &mut test, + ); + + // Now test: Alice has 0 direct USDC, but has 20 USDC settled + // She should be able to place a market bid order + test.next_tx(ALICE); + { + let pool = test.take_shared_by_id>(pool_id); + let balance_manager_alice = test.take_shared_by_id( + balance_manager_id_alice, + ); + let clock = clock::create_for_testing(test.ctx()); + + // Verify Alice has 0 direct USDC balance + let direct_usdc_balance = balance_manager_alice.balance(); + assert!(direct_usdc_balance == 0); + + // can_place_market_order should return true because of settled balances + // Market bid for 5 SUI (will need ~10 USDC, she has 20 settled) + let can_place = pool.can_place_market_order( + &balance_manager_alice, + 5 * constants::float_scaling(), // quantity: 5 SUI + true, // is_bid = true (buy) + true, // pay_with_deep + &clock, + ); + assert!(can_place); + + // Also verify that without enough settled balance, it would fail + // Market bid for 15 SUI (would need ~30 USDC, she only has 20 settled) + let can_place_too_much = pool.can_place_market_order( + &balance_manager_alice, + 15 * constants::float_scaling(), // quantity: 15 SUI + true, // is_bid = true (buy) + true, // pay_with_deep + &clock, + ); + assert!(!can_place_too_much); + + clock.destroy_for_testing(); + return_shared(pool); + return_shared(balance_manager_alice); + }; + + end(test); +} + +/// Test get_base_quantity_in with multiple price levels +/// Setup: Orders at $3 (qty 10), $2 (qty 5), $1 (qty 25) +/// Target: 50 USDC +/// Expected: Sell 10 SUI at $3 (30 USDC), 5 SUI at $2 (10 USDC), 10 SUI at $1 (10 USDC) +/// Result: 25 base_quantity_in, 50 actual_quote_quantity_out +#[test] +fun test_get_base_quantity_in_multiple_levels() { + let mut test = begin(OWNER); + let registry_id = setup_test(OWNER, &mut test); + let balance_manager_id_alice = create_acct_and_share_with_funds( + ALICE, + 1000000 * constants::float_scaling(), + &mut test, + ); + + // Setup pool with reference pool (non-whitelisted) so we can test DEEP fees + let pool_id = setup_pool_with_default_fees_and_reference_pool( + ALICE, + registry_id, + balance_manager_id_alice, + &mut test, + ); + + // Place bid orders at different price levels + // Order 1: Buy 10 SUI at $3 per SUI + place_limit_order( + ALICE, + pool_id, + balance_manager_id_alice, + 1, + constants::no_restriction(), + constants::self_matching_allowed(), + 3 * constants::float_scaling(), // price: $3 + 10 * constants::float_scaling(), // quantity: 10 SUI + true, // is_bid (buy order) + true, // pay_with_deep + constants::max_u64(), + &mut test, + ); + + // Order 2: Buy 5 SUI at $2 per SUI + place_limit_order( + ALICE, + pool_id, + balance_manager_id_alice, + 2, + constants::no_restriction(), + constants::self_matching_allowed(), + 2 * constants::float_scaling(), // price: $2 + 5 * constants::float_scaling(), // quantity: 5 SUI + true, // is_bid + true, + constants::max_u64(), + &mut test, + ); + + // Order 3: Buy 25 SUI at $1 per SUI + place_limit_order( + ALICE, + pool_id, + balance_manager_id_alice, + 3, + constants::no_restriction(), + constants::self_matching_allowed(), + 1 * constants::float_scaling(), // price: $1 + 25 * constants::float_scaling(), // quantity: 25 SUI + true, // is_bid + true, + constants::max_u64(), + &mut test, + ); -fun get_quantity_out( - pool_id: ID, - base_quantity: u64, - quote_quantity: u64, - test: &mut Scenario, -): (u64, u64, u64) { - test.next_tx(OWNER); + test.next_tx(ALICE); { - let pool = test.take_shared_by_id>(pool_id); + let pool = test.take_shared_by_id>(pool_id); let clock = test.take_shared(); - let (base_out, quote_out, deep_required) = pool.get_quantity_out( - base_quantity, - quote_quantity, + // Test 1: Get base quantity needed for 50 USDC with pay_with_deep = true + let (base_in, quote_out, deep_required) = pool.get_base_quantity_in( + 50 * constants::float_scaling(), // target: 50 USDC + true, // pay_with_deep &clock, ); - return_shared(pool); - return_shared(clock); - (base_out, quote_out, deep_required) - } -} + // Expected: Sell 10 at $3 (30), 5 at $2 (10), 10 at $1 (10) = 25 SUI for 50 USDC + assert!(base_in == 25 * constants::float_scaling(), 0); + assert!(quote_out == 50 * constants::float_scaling(), 1); -fun get_quantity_out_input_fee( - pool_id: ID, - base_quantity: u64, - quote_quantity: u64, - test: &mut Scenario, -): (u64, u64, u64) { - test.next_tx(OWNER); - { - let pool = test.take_shared_by_id>(pool_id); - let clock = test.take_shared(); + // DEEP fee calculation for sell order (is_bid = false): + // fee_balances = deep_price.fee_quantity(25 SUI, 50 USDC, false) + // Then multiply by taker_fee (0.001) + let expected_deep = math::mul( + constants::taker_fee(), + math::mul(25 * constants::float_scaling(), constants::deep_multiplier()), + ); + assert!(deep_required == expected_deep, 2); - let (base_out, quote_out, deep_required) = pool.get_quantity_out_input_fee< - BaseAsset, - QuoteAsset, + // Test 2: Get base quantity needed for 50 USDC with pay_with_deep = false + let (base_in_no_deep, quote_out_no_deep, deep_required_no_deep) = pool.get_base_quantity_in< + SUI, + USDC, >( - base_quantity, - quote_quantity, + 50 * constants::float_scaling(), // target: 50 USDC + false, // pay_with_deep = false (fees in base) &clock, ); - return_shared(pool); - return_shared(clock); - (base_out, quote_out, deep_required) - } -} + // With fees in base, need extra base to cover fees + // input_fee_rate = fee_penalty_multiplier (1.25) * taker_fee (0.001) = 0.00125 + // base_with_fee = base * (1 + 0.00125) = 25 * 1.00125 = 25.03125 + let input_fee_rate = math::mul( + constants::fee_penalty_multiplier(), + constants::taker_fee(), + ); + let expected_base_with_fee = math::mul( + 25 * constants::float_scaling(), + constants::float_scaling() + input_fee_rate, + ); -fun get_base_quantity_out( - pool_id: ID, - quote_quantity: u64, - test: &mut Scenario, -): (u64, u64, u64) { - test.next_tx(OWNER); - { - let pool = test.take_shared_by_id>(pool_id); - let clock = test.take_shared(); + assert!(base_in_no_deep == expected_base_with_fee, 3); + assert!(quote_out_no_deep == 50 * constants::float_scaling(), 4); + assert!(deep_required_no_deep == 0, 5); - let (base_out, quote_out, deep_required) = pool.get_base_quantity_out< - BaseAsset, - QuoteAsset, - >( - quote_quantity, + // Test 3: Target close to max liquidity + // Available: 10 at $3 (30) + 5 at $2 (10) + 25 at $1 (25) = 65 USDC max + let (base_in_partial, quote_out_partial, _) = pool.get_base_quantity_in( + 60 * constants::float_scaling(), // target: 60 USDC + true, &clock, ); - return_shared(pool); - return_shared(clock); - - (base_out, quote_out, deep_required) - } -} -fun get_quote_quantity_out( - pool_id: ID, - base_quantity: u64, - test: &mut Scenario, -): (u64, u64, u64) { - test.next_tx(OWNER); - { - let pool = test.take_shared_by_id>(pool_id); - let clock = test.take_shared(); + // Should use: 10 at $3 (30) + 5 at $2 (10) + 20 at $1 (20) = 35 SUI for 60 USDC + assert!(base_in_partial == 35 * constants::float_scaling(), 6); + assert!(quote_out_partial == 60 * constants::float_scaling(), 7); - let (base_out, quote_out, deep_required) = pool.get_quote_quantity_out< - BaseAsset, - QuoteAsset, - >( - base_quantity, + // Test 4: Target exceeding available liquidity + // Max available: 10*3 + 5*2 + 25*1 = 65 USDC + let (base_in_exceed, quote_out_exceed, deep_exceed) = pool.get_base_quantity_in( + 100 * constants::float_scaling(), // target: 100 USDC (more than 65 available) + true, &clock, ); - return_shared(pool); - return_shared(clock); - - (base_out, quote_out, deep_required) - } -} -fun get_base_quantity_out_input_fee( - pool_id: ID, - quote_quantity: u64, - test: &mut Scenario, -): (u64, u64, u64) { - test.next_tx(OWNER); - { - let pool = test.take_shared_by_id>(pool_id); - let clock = test.take_shared(); + // Should return (0, 0, 0) since we can't meet the target + assert!(base_in_exceed == 0, 8); + assert!(quote_out_exceed == 0, 9); + assert!(deep_exceed == 0, 10); - let (base_out, quote_out, deep_required) = pool.get_base_quantity_out_input_fee< - BaseAsset, - QuoteAsset, - >( - quote_quantity, + // Test 5: Target exactly at max liquidity (65 USDC, exactly available) + let (base_in_65, quote_out_65, deep_65) = pool.get_base_quantity_in( + 65 * constants::float_scaling(), // target: 65 USDC (exact match) + true, &clock, ); - return_shared(pool); - return_shared(clock); - - (base_out, quote_out, deep_required) - } -} -fun get_quote_quantity_out_input_fee( - pool_id: ID, - base_quantity: u64, - test: &mut Scenario, -): (u64, u64, u64) { - test.next_tx(OWNER); - { - let pool = test.take_shared_by_id>(pool_id); - let clock = test.take_shared(); + // Should use all: 10 at $3 (30) + 5 at $2 (10) + 25 at $1 (25) = 40 SUI for 65 USDC + assert!(base_in_65 == 40 * constants::float_scaling(), 11); + assert!(quote_out_65 == 65 * constants::float_scaling(), 12); - let (base_out, quote_out, deep_required) = pool.get_quote_quantity_out_input_fee< - BaseAsset, - QuoteAsset, - >( - base_quantity, - &clock, + let expected_deep_65 = math::mul( + constants::taker_fee(), + math::mul(40 * constants::float_scaling(), constants::deep_multiplier()), ); + assert!(deep_65 == expected_deep_65, 13); + return_shared(pool); return_shared(clock); + }; - (base_out, quote_out, deep_required) - } + end(test); } -fun test_cancel_orders(is_bid: bool) { +/// Test get_quote_quantity_in with multiple price levels +/// Setup: Sell orders at $1 (qty 25), $2 (qty 5), $3 (qty 10) +/// Target: 30 SUI +/// Expected: Buy 25 SUI at $1 (25 USDC), 5 SUI at $2 (10 USDC) = 30 SUI for 35 USDC +/// Result: 30 base_quantity_out, 35 quote_quantity_in +#[test] +fun test_get_quote_quantity_in_multiple_levels() { let mut test = begin(OWNER); let registry_id = setup_test(OWNER, &mut test); let balance_manager_id_alice = create_acct_and_share_with_funds( @@ -5117,6 +9213,8 @@ fun test_cancel_orders(is_bid: bool) { 1000000 * constants::float_scaling(), &mut test, ); + + // Setup pool with reference pool (non-whitelisted) so we can test DEEP fees let pool_id = setup_pool_with_default_fees_and_reference_pool( ALICE, registry_id, @@ -5124,72 +9222,227 @@ fun test_cancel_orders(is_bid: bool) { &mut test, ); - let client_order_id = 1; - let price = 2 * constants::float_scaling(); - let quantity = 1 * constants::float_scaling(); - let expire_timestamp = constants::max_u64(); - let pay_with_deep = true; + // Place ask (sell) orders at different price levels + // Order 1: Sell 25 SUI at $1 per SUI + place_limit_order( + ALICE, + pool_id, + balance_manager_id_alice, + 1, + constants::no_restriction(), + constants::self_matching_allowed(), + 1 * constants::float_scaling(), // price: $1 + 25 * constants::float_scaling(), // quantity: 25 SUI + false, // is_bid = false (sell order) + true, // pay_with_deep + constants::max_u64(), + &mut test, + ); - let order_info_1 = place_limit_order( + // Order 2: Sell 5 SUI at $2 per SUI + place_limit_order( ALICE, pool_id, balance_manager_id_alice, - client_order_id, + 2, constants::no_restriction(), constants::self_matching_allowed(), - price, - quantity, - is_bid, - pay_with_deep, - expire_timestamp, + 2 * constants::float_scaling(), // price: $2 + 5 * constants::float_scaling(), // quantity: 5 SUI + false, // is_bid = false + true, + constants::max_u64(), &mut test, ); - let order_info_2 = place_limit_order( + // Order 3: Sell 10 SUI at $3 per SUI + place_limit_order( ALICE, pool_id, balance_manager_id_alice, - client_order_id, + 3, constants::no_restriction(), constants::self_matching_allowed(), - price, - quantity, - is_bid, - pay_with_deep, - expire_timestamp, + 3 * constants::float_scaling(), // price: $3 + 10 * constants::float_scaling(), // quantity: 10 SUI + false, // is_bid = false + true, + constants::max_u64(), &mut test, ); - let mut orders_to_cancel = vector[]; - orders_to_cancel.push_back(order_info_1.order_id()); - orders_to_cancel.push_back(order_info_2.order_id()); + test.next_tx(ALICE); + { + let pool = test.take_shared_by_id>(pool_id); + let clock = test.take_shared(); - cancel_orders( + // Test 1: Get quote quantity needed for 30 SUI with pay_with_deep = true + let (base_out, quote_in, deep_required) = pool.get_quote_quantity_in( + 30 * constants::float_scaling(), // target: 30 SUI + true, // pay_with_deep + &clock, + ); + + // Expected: Buy 25 at $1 (25) + 5 at $2 (10) = 30 SUI for 35 USDC + assert!(base_out == 30 * constants::float_scaling(), 0); + assert!(quote_in == 35 * constants::float_scaling(), 1); + + // DEEP fee calculation for buy order (is_bid = true): + // fee_balances = deep_price.fee_quantity(30 SUI, 35 USDC, true) + // Then multiply by taker_fee (0.001) + let expected_deep = math::mul( + constants::taker_fee(), + math::mul(30 * constants::float_scaling(), constants::deep_multiplier()), + ); + assert!(deep_required == expected_deep, 2); + + // Test 2: Get quote quantity needed for 30 SUI with pay_with_deep = false + let ( + base_out_no_deep, + quote_in_no_deep, + deep_required_no_deep, + ) = pool.get_quote_quantity_in( + 30 * constants::float_scaling(), // target: 30 SUI + false, // pay_with_deep = false (fees in quote) + &clock, + ); + + // With fees in quote, need extra quote to cover fees + // input_fee_rate = fee_penalty_multiplier (1.25) * taker_fee (0.001) = 0.00125 + // quote_with_fee = quote * (1 + 0.00125) = 35 * 1.00125 = 35.04375 + let input_fee_rate = math::mul( + constants::fee_penalty_multiplier(), + constants::taker_fee(), + ); + let expected_quote_with_fee = math::mul( + 35 * constants::float_scaling(), + constants::float_scaling() + input_fee_rate, + ); + + assert!(base_out_no_deep == 30 * constants::float_scaling(), 3); + assert!(quote_in_no_deep == expected_quote_with_fee, 4); + assert!(deep_required_no_deep == 0, 5); + + // Test 3: Target that requires all liquidity (40 SUI total available) + let (base_out_all, quote_in_all, deep_all) = pool.get_quote_quantity_in( + 40 * constants::float_scaling(), // target: 40 SUI (exact match) + true, + &clock, + ); + + // Should use all: 25 at $1 (25) + 5 at $2 (10) + 10 at $3 (30) = 40 SUI for 65 USDC + assert!(base_out_all == 40 * constants::float_scaling(), 6); + assert!(quote_in_all == 65 * constants::float_scaling(), 7); + + let expected_deep_all = math::mul( + constants::taker_fee(), + math::mul(40 * constants::float_scaling(), constants::deep_multiplier()), + ); + assert!(deep_all == expected_deep_all, 8); + + // Test 4: Target exceeding available liquidity (50 SUI, only 40 available) + let (base_out_exceed, quote_in_exceed, deep_exceed) = pool.get_quote_quantity_in( + 50 * constants::float_scaling(), // target: 50 SUI (more than 40 available) + true, + &clock, + ); + + // Should return (0, 0, 0) since we can't meet the target + assert!(base_out_exceed == 0, 9); + assert!(quote_in_exceed == 0, 10); + assert!(deep_exceed == 0, 11); + + // Test 5: Small target (5 SUI) + let (base_out_small, quote_in_small, _) = pool.get_quote_quantity_in( + 5 * constants::float_scaling(), // target: 5 SUI + true, + &clock, + ); + + // Should buy 5 at $1 = 5 SUI for 5 USDC + assert!(base_out_small == 5 * constants::float_scaling(), 12); + assert!(quote_in_small == 5 * constants::float_scaling(), 13); + + return_shared(pool); + return_shared(clock); + }; + + end(test); +} + +// ============== Fractional target tests ============== + +/// Test get_base_quantity_in with fractional target (slightly above round number) +/// Target: 10.0000...01 USDC (10 * float_scaling + 1) +/// This tests the rounding behavior when target is not exactly divisible +#[test] +fun test_get_base_quantity_in_fractional_target() { + let mut test = begin(OWNER); + let registry_id = setup_test(OWNER, &mut test); + let balance_manager_id_alice = create_acct_and_share_with_funds( ALICE, - pool_id, + 1000000 * constants::float_scaling(), + &mut test, + ); + + let pool_id = setup_pool_with_default_fees_and_reference_pool( + ALICE, + registry_id, balance_manager_id_alice, - orders_to_cancel, &mut test, ); - borrow_and_verify_book_order( + // Place a bid order at $1 per SUI with plenty of liquidity + place_limit_order( + ALICE, pool_id, - order_info_1.order_id(), - is_bid, - client_order_id, - quantity, - 0, - order_info_1.order_deep_price().deep_per_asset(), - test.ctx().epoch(), - constants::canceled(), - expire_timestamp, + balance_manager_id_alice, + 1, + constants::no_restriction(), + constants::self_matching_allowed(), + 1 * constants::float_scaling(), // price: $1 + 100 * constants::float_scaling(), // quantity: 100 SUI + true, // is_bid + true, + constants::max_u64(), &mut test, ); + test.next_tx(ALICE); + { + let pool = test.take_shared_by_id>(pool_id); + let clock = test.take_shared(); + + // Target: 10 USDC + 1 unit (fractional) + // At price $1, we need slightly more than 10 base to get 10.0000...01 quote + // Due to lot_size rounding, we should get at least the target (possibly more) + let fractional_target = 10 * constants::float_scaling() + 1; + let (base_in, quote_out, _) = pool.get_base_quantity_in( + fractional_target, + true, + &clock, + ); + + // base_in should be rounded to lot_size and sufficient to cover target + // quote_out should be >= fractional_target + assert!(quote_out >= fractional_target, 0); + // base_in should be a multiple of lot_size + assert!(base_in % constants::lot_size() == 0, 1); + // At $1, base_in * price = quote_out, so base_in should cover the target + assert!(base_in >= 10 * constants::float_scaling(), 2); + + return_shared(pool); + return_shared(clock); + }; + end(test); } -fun test_update_pool_book_params(error: u8) { +/// Test get_quote_quantity_in with fractional target (slightly above round number) +/// Target: 10.0000...01 SUI (10 * float_scaling + 1) +/// This tests the rounding behavior when target is not exactly divisible +#[test] +fun test_get_quote_quantity_in_fractional_target() { let mut test = begin(OWNER); let registry_id = setup_test(OWNER, &mut test); let balance_manager_id_alice = create_acct_and_share_with_funds( @@ -5197,183 +9450,296 @@ fun test_update_pool_book_params(error: u8) { 1000000 * constants::float_scaling(), &mut test, ); - let pool_id = setup_pool( - OWNER, - constants::tick_size(), // tick size - 100_000, - 1_000_000, + + let pool_id = setup_pool_with_default_fees_and_reference_pool( + ALICE, registry_id, - true, - false, + balance_manager_id_alice, &mut test, ); - let alice_client_order_id = 1; - let alice_quantity_1 = 1_000_000; - let alice_quantity_2 = 1_010_000; - let alice_price = 2 * constants::float_scaling(); - let expire_timestamp = constants::max_u64(); - let pay_with_deep = true; - + // Place an ask (sell) order at $1 per SUI with plenty of liquidity place_limit_order( ALICE, pool_id, balance_manager_id_alice, - alice_client_order_id, + 1, constants::no_restriction(), constants::self_matching_allowed(), - alice_price, - alice_quantity_1, + 1 * constants::float_scaling(), // price: $1 + 100 * constants::float_scaling(), // quantity: 100 SUI + false, // is_bid = false (sell order) true, - pay_with_deep, - expire_timestamp, + constants::max_u64(), &mut test, ); - if (error == 0) { - adjust_min_lot_size_admin( - OWNER, - pool_id, - 1000, - 10000, - &mut test, + test.next_tx(ALICE); + { + let pool = test.take_shared_by_id>(pool_id); + let clock = test.take_shared(); + + // Target: 10 SUI + 1 unit (fractional) + // We want to buy slightly more than 10 SUI + // Due to lot_size rounding, we should get at least the target (possibly more) + let fractional_target = 10 * constants::float_scaling() + 1; + let (base_out, quote_in, _) = pool.get_quote_quantity_in( + fractional_target, + true, + &clock, ); + + // base_out should be >= fractional_target (we get at least what we asked for) + assert!(base_out >= fractional_target, 0); + // base_out should be a multiple of lot_size (rounded up from target) + assert!(base_out % constants::lot_size() == 0, 1); + // At $1, quote needed = base bought, so quote_in should match base_out + assert!(quote_in == base_out, 2); + + return_shared(pool); + return_shared(clock); }; - if (error == 2) { - adjust_min_lot_size_admin( - OWNER, - pool_id, - 6000, - 60000, - &mut test, - ); + end(test); +} + +#[test] +fun pool_referral_multiplier_ok() { + let mut test = begin(OWNER); + let pool_id = setup_everything(&mut test); + let referral_id; + test.next_tx(ALICE); + { + let mut pool = test.take_shared_by_id>(pool_id); + referral_id = pool.mint_referral(500_000_000, test.ctx()); + return_shared(pool); }; - if (error == 3) { - adjust_tick_size_admin( - OWNER, - pool_id, - 50, - &mut test, - ); + test.next_tx(ALICE); + { + let pool = test.take_shared_by_id>(pool_id); + let referral = test.take_shared_by_id(referral_id); + let multiplier = pool.pool_referral_multiplier(&referral); + assert_eq!(multiplier, 500_000_000); + return_shared(referral); + return_shared(pool); }; - if (error == 4) { - adjust_min_lot_size_admin( - OWNER, - pool_id, - 0, - 500, - &mut test, - ); + end(test); +} + +#[test] +fun pool_referral_multiplier_after_update() { + let mut test = begin(OWNER); + let pool_id = setup_everything(&mut test); + let referral_id; + test.next_tx(ALICE); + { + let mut pool = test.take_shared_by_id>(pool_id); + referral_id = pool.mint_referral(100_000_000, test.ctx()); + return_shared(pool); }; - place_limit_order( + test.next_tx(ALICE); + { + let pool = test.take_shared_by_id>(pool_id); + let referral = test.take_shared_by_id(referral_id); + let multiplier = pool.pool_referral_multiplier(&referral); + assert_eq!(multiplier, 100_000_000); + return_shared(referral); + return_shared(pool); + }; + + test.next_tx(ALICE); + { + let mut pool = test.take_shared_by_id>(pool_id); + let referral = test.take_shared_by_id(referral_id); + pool.update_pool_referral_multiplier(&referral, 2_000_000_000, test.ctx()); + return_shared(referral); + return_shared(pool); + }; + + test.next_tx(ALICE); + { + let pool = test.take_shared_by_id>(pool_id); + let referral = test.take_shared_by_id(referral_id); + let multiplier = pool.pool_referral_multiplier(&referral); + assert_eq!(multiplier, 2_000_000_000); + return_shared(referral); + return_shared(pool); + }; + + end(test); +} + +#[test, expected_failure(abort_code = ::deepbook::pool::EWrongPoolReferral)] +fun pool_referral_multiplier_wrong_pool() { + let mut test = begin(OWNER); + let pool_id_1 = setup_everything(&mut test); + let referral_id; + + test.next_tx(ALICE); + { + let mut pool = test.take_shared_by_id>(pool_id_1); + referral_id = pool.mint_referral(100_000_000, test.ctx()); + return_shared(pool); + }; + + test.next_tx(OWNER); + let pool_id_2; + { + let mut registry = test.take_shared(); + pool_id_2 = + pool::create_permissionless_pool( + &mut registry, + constants::tick_size(), + constants::lot_size(), + constants::min_size(), + mint_for_testing(constants::pool_creation_fee(), test.ctx()), + test.ctx(), + ); + return_shared(registry); + }; + + test.next_tx(ALICE); + { + let pool = test.take_shared_by_id>(pool_id_2); + let referral = test.take_shared_by_id(referral_id); + pool.pool_referral_multiplier(&referral); + }; + + abort +} + +#[test, expected_failure(abort_code = ::deepbook::pool::EWrongPoolReferral)] +fun get_pool_referral_balances_wrong_pool() { + let mut test = begin(OWNER); + let pool_id_1 = setup_everything(&mut test); + let referral_id; + + test.next_tx(ALICE); + { + let mut pool = test.take_shared_by_id>(pool_id_1); + referral_id = pool.mint_referral(100_000_000, test.ctx()); + return_shared(pool); + }; + + test.next_tx(OWNER); + let pool_id_2; + { + let mut registry = test.take_shared(); + pool_id_2 = + pool::create_permissionless_pool( + &mut registry, + constants::tick_size(), + constants::lot_size(), + constants::min_size(), + mint_for_testing(constants::pool_creation_fee(), test.ctx()), + test.ctx(), + ); + return_shared(registry); + }; + + test.next_tx(ALICE); + { + let pool = test.take_shared_by_id>(pool_id_2); + let referral = test.take_shared_by_id(referral_id); + pool.get_pool_referral_balances(&referral); + }; + + abort (0) +} + +/// Test that swap_exact_base_for_quote_with_manager and swap_exact_quote_for_base_with_manager +/// work correctly when the swap results in zero leftover (base_out = 0 or quote_out = 0). +/// This tests the fix for withdrawing 0 from balance manager. +fun test_swap_with_manager_zero_out(is_base_to_quote: bool) { + let mut test = begin(OWNER); + let registry_id = setup_test(OWNER, &mut test); + let balance_manager_id_alice = create_acct_and_share_with_funds( ALICE, - pool_id, - balance_manager_id_alice, - alice_client_order_id, - constants::no_restriction(), - constants::self_matching_allowed(), - alice_price, - alice_quantity_2, - true, - pay_with_deep, - expire_timestamp, + 1000000 * constants::float_scaling(), &mut test, ); - adjust_tick_size_admin( - OWNER, - pool_id, - 100, + let pool_id = setup_pool_with_default_fees_and_reference_pool( + ALICE, + registry_id, + balance_manager_id_alice, &mut test, ); + + let price = 2 * constants::float_scaling(); + let quantity = 10 * constants::float_scaling(); + let expire_timestamp = constants::max_u64(); + let pay_with_deep = true; + + // Place a maker order on the opposite side + // If we're swapping base to quote, we need a bid order to match against + // If we're swapping quote to base, we need an ask order to match against place_limit_order( ALICE, pool_id, balance_manager_id_alice, - alice_client_order_id, + 1, constants::no_restriction(), constants::self_matching_allowed(), - alice_price + 100, - alice_quantity_2, - true, + price, + quantity, + is_base_to_quote, pay_with_deep, expire_timestamp, &mut test, ); - end(test); -} -fun adjust_min_lot_size_admin( - sender: address, - pool_id: ID, - new_lot_size: u64, - new_min_size: u64, - test: &mut Scenario, -) { - test.next_tx(sender); - let mut pool = test.take_shared_by_id>(pool_id); - let admin_cap = registry::get_admin_cap_for_testing(test.ctx()); - let clock = test.take_shared(); - pool::adjust_min_lot_size_admin( - &mut pool, - new_lot_size, - new_min_size, - &admin_cap, - &clock, + // Create Bob's balance manager with caps + let bob_balance_manager_id = create_acct_only_deep_and_share_with_funds( + BOB, + 1000000 * constants::float_scaling(), + &mut test, ); - test_utils::destroy(admin_cap); - return_shared(pool); - return_shared(clock); -} + create_caps(BOB, bob_balance_manager_id, &mut test); -fun adjust_tick_size_admin( - sender: address, - pool_id: ID, - new_tick_size: u64, - test: &mut Scenario, -) { - test.next_tx(sender); - let mut pool = test.take_shared_by_id>(pool_id); - let admin_cap = registry::get_admin_cap_for_testing(test.ctx()); - let clock = test.take_shared(); - pool::adjust_tick_size_admin( - &mut pool, - new_tick_size, - &admin_cap, - &clock, - ); - test_utils::destroy(admin_cap); - return_shared(pool); - return_shared(clock); -} + // Use an exact lot-size multiple so there's no leftover + let swap_quantity = 5 * constants::float_scaling(); -fun add_stablecoin(sender: address, registry_id: ID, test: &mut Scenario) { - test.next_tx(sender); - let admin_cap = registry::get_admin_cap_for_testing(test.ctx()); - let mut registry = test.take_shared_by_id(registry_id); - { - registry::add_stablecoin( - &mut registry, - &admin_cap, + if (is_base_to_quote) { + // Swap exactly 5 SUI for USDC - should result in base_out = 0 + let (base_out, quote_out) = place_exact_base_for_quote_with_manager( + pool_id, + BOB, + bob_balance_manager_id, + swap_quantity, + 0, + &mut test, ); - }; - return_shared(registry); - test_utils::destroy(admin_cap); -} -fun remove_stablecoin(sender: address, registry_id: ID, test: &mut Scenario) { - test.next_tx(sender); - let admin_cap = registry::get_admin_cap_for_testing(test.ctx()); - let mut registry = test.take_shared_by_id(registry_id); - { - registry::remove_stablecoin( - &mut registry, - &admin_cap, + // base_out should be 0 (all base was swapped) + assert!(base_out.value() == 0); + // quote_out should be swap_quantity * price = 5 * 2 = 10 USDC + assert!(quote_out.value() == math::mul(swap_quantity, price)); + + base_out.burn_for_testing(); + quote_out.burn_for_testing(); + } else { + // Swap exactly 10 USDC for SUI - should result in quote_out = 0 + let quote_swap_quantity = 10 * constants::float_scaling(); + let (base_out, quote_out) = place_exact_quote_for_base_with_manager( + pool_id, + BOB, + bob_balance_manager_id, + quote_swap_quantity, + 0, + &mut test, ); + + // quote_out should be 0 (all quote was swapped) + assert!(quote_out.value() == 0); + // base_out should be quote_swap_quantity / price = 10 / 2 = 5 SUI + assert!(base_out.value() == math::div(quote_swap_quantity, price)); + + base_out.burn_for_testing(); + quote_out.burn_for_testing(); }; - return_shared(registry); - test_utils::destroy(admin_cap); + + end(test); } diff --git a/packages/deepbook/tests/state/account_tests.move b/packages/deepbook/tests/state/account_tests.move index d3919e5c3..2a9784ec1 100644 --- a/packages/deepbook/tests/state/account_tests.move +++ b/packages/deepbook/tests/state/account_tests.move @@ -5,7 +5,8 @@ module deepbook::account_tests; use deepbook::{account, balances, constants, deep_price, fill}; -use sui::{object::id_from_address, test_scenario::{next_tx, begin, end}, test_utils::assert_eq}; +use std::unit_test::assert_eq; +use sui::{object::id_from_address, test_scenario::{next_tx, begin, end}}; const OWNER: address = @0xF; const ALICE: address = @0xA; @@ -17,14 +18,14 @@ fun add_balances_ok() { test.next_tx(ALICE); let mut account = account::empty(test.ctx()); let (settled, owed) = account.settle(); - assert_eq(settled, balances::new(0, 0, 0)); - assert_eq(owed, balances::new(0, 0, 0)); + assert_eq!(settled, balances::new(0, 0, 0)); + assert_eq!(owed, balances::new(0, 0, 0)); account.add_settled_balances(balances::new(1, 2, 3)); account.add_owed_balances(balances::new(4, 5, 6)); let (settled, owed) = account.settle(); - assert_eq(settled, balances::new(1, 2, 3)); - assert_eq(owed, balances::new(4, 5, 6)); + assert_eq!(settled, balances::new(1, 2, 3)); + assert_eq!(owed, balances::new(4, 5, 6)); test.end(); } @@ -54,11 +55,11 @@ fun process_maker_fill_ok() { ); account.process_maker_fill(&fill); let (settled, owed) = account.settle(); - assert_eq(settled, balances::new(100, 0, 0)); - assert_eq(owed, balances::new(0, 0, 0)); + assert_eq!(settled, balances::new(100, 0, 0)); + assert_eq!(owed, balances::new(0, 0, 0)); assert!(account.total_volume() == 100, 0); - assert!(account.open_orders().size() == 1, 0); - assert!(account.open_orders().contains(&(1 as u128)), 0); + assert!(account.open_orders().length() == 1, 0); + assert!(account.open_orders().contains(&1u128), 0); account.add_order(2); let fill = fill::new( @@ -79,12 +80,12 @@ fun process_maker_fill_ok() { ); account.process_maker_fill(&fill); let (settled, owed) = account.settle(); - assert_eq(settled, balances::new(0, 500, 0)); - assert_eq(owed, balances::new(0, 0, 0)); + assert_eq!(settled, balances::new(0, 500, 0)); + assert_eq!(owed, balances::new(0, 0, 0)); assert!(account.total_volume() == 200, 0); - assert!(account.open_orders().size() == 1, 0); - assert!(account.open_orders().contains(&(1 as u128)), 0); - assert!(!account.open_orders().contains(&(2 as u128)), 0); + assert!(account.open_orders().length() == 1, 0); + assert!(account.open_orders().contains(&1u128), 0); + assert!(!account.open_orders().contains(&2u128), 0); account.add_order(3); let fill = fill::new( @@ -105,13 +106,13 @@ fun process_maker_fill_ok() { ); account.process_maker_fill(&fill); let (settled, owed) = account.settle(); - assert_eq(settled, balances::new(100, 0, 0)); - assert_eq(owed, balances::new(0, 0, 0)); + assert_eq!(settled, balances::new(100, 0, 0)); + assert_eq!(owed, balances::new(0, 0, 0)); assert!(account.total_volume() == 200, 0); - assert!(account.open_orders().size() == 1, 0); - assert!(account.open_orders().contains(&(1 as u128)), 0); - assert!(!account.open_orders().contains(&(2 as u128)), 0); - assert!(!account.open_orders().contains(&(3 as u128)), 0); + assert!(account.open_orders().length() == 1, 0); + assert!(account.open_orders().contains(&1u128), 0); + assert!(!account.open_orders().contains(&2u128), 0); + assert!(!account.open_orders().contains(&3u128), 0); account.add_order(4); let fill = fill::new( @@ -132,14 +133,14 @@ fun process_maker_fill_ok() { ); account.process_maker_fill(&fill); let (settled, owed) = account.settle(); - assert_eq(settled, balances::new(0, 500, 0)); - assert_eq(owed, balances::new(0, 0, 0)); + assert_eq!(settled, balances::new(0, 500, 0)); + assert_eq!(owed, balances::new(0, 0, 0)); assert!(account.total_volume() == 300, 0); - assert!(account.open_orders().size() == 1, 0); - assert!(account.open_orders().contains(&(1 as u128)), 0); - assert!(!account.open_orders().contains(&(2 as u128)), 0); - assert!(!account.open_orders().contains(&(3 as u128)), 0); - assert!(!account.open_orders().contains(&(4 as u128)), 0); + assert!(account.open_orders().length() == 1, 0); + assert!(account.open_orders().contains(&1u128), 0); + assert!(!account.open_orders().contains(&2u128), 0); + assert!(!account.open_orders().contains(&3u128), 0); + assert!(!account.open_orders().contains(&4u128), 0); test.end(); } @@ -162,15 +163,15 @@ fun add_remove_stake_ok() { assert!(account.active_stake() == 0, 0); assert!(account.inactive_stake() == 200, 0); let (settled, owed) = account.settle(); - assert_eq(settled, balances::new(0, 0, 0)); - assert_eq(owed, balances::new(0, 0, 200)); + assert_eq!(settled, balances::new(0, 0, 0)); + assert_eq!(owed, balances::new(0, 0, 200)); account.remove_stake(); assert!(account.active_stake() == 0, 0); assert!(account.inactive_stake() == 0, 0); let (settled, owed) = account.settle(); - assert_eq(settled, balances::new(0, 0, 200)); - assert_eq(owed, balances::new(0, 0, 0)); + assert_eq!(settled, balances::new(0, 0, 200)); + assert_eq!(owed, balances::new(0, 0, 0)); let (before, after) = account.add_stake(0); assert!(before == 0, 0); @@ -178,8 +179,8 @@ fun add_remove_stake_ok() { assert!(account.active_stake() == 0, 0); assert!(account.inactive_stake() == 0, 0); let (settled, owed) = account.settle(); - assert_eq(settled, balances::new(0, 0, 0)); - assert_eq(owed, balances::new(0, 0, 0)); + assert_eq!(settled, balances::new(0, 0, 0)); + assert_eq!(owed, balances::new(0, 0, 0)); test.end(); } @@ -273,14 +274,14 @@ fun claim_rebates_ok() { let mut account = account::empty(test.ctx()); account.claim_rebates(); let (settled, owed) = account.settle(); - assert_eq(settled, balances::new(0, 0, 0)); - assert_eq(owed, balances::new(0, 0, 0)); + assert_eq!(settled, balances::new(0, 0, 0)); + assert_eq!(owed, balances::new(0, 0, 0)); account.add_rebates(balances::new(50, 150, 100)); account.claim_rebates(); let (settled, owed) = account.settle(); - assert_eq(settled, balances::new(50, 150, 100)); - assert_eq(owed, balances::new(0, 0, 0)); + assert_eq!(settled, balances::new(50, 150, 100)); + assert_eq!(owed, balances::new(0, 0, 0)); // user owes 100 DEEP for staking account.add_stake(100); @@ -288,8 +289,8 @@ fun claim_rebates_ok() { account.add_rebates(balances::new(150, 50, 100)); account.claim_rebates(); let (settled, owed) = account.settle(); - assert_eq(settled, balances::new(150, 50, 100)); - assert_eq(owed, balances::new(0, 0, 100)); + assert_eq!(settled, balances::new(150, 50, 100)); + assert_eq!(owed, balances::new(0, 0, 100)); test.end(); } diff --git a/packages/deepbook/tests/state/ewma_tests.move b/packages/deepbook/tests/state/ewma_tests.move new file mode 100644 index 000000000..927a45376 --- /dev/null +++ b/packages/deepbook/tests/state/ewma_tests.move @@ -0,0 +1,413 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +#[test_only] +module deepbook::ewma_tests; + +use deepbook::{constants, ewma::{Self, EWMAState}}; +use std::unit_test::{assert_eq, destroy}; +use sui::{clock, test_scenario::{begin, end, Scenario}}; + +#[test_only] +const TEST_POOL_ID: address = @0x1234; + +#[test_only] +public fun test_init_ewma_state(ctx: &TxContext): EWMAState { + ewma::init_ewma_state(ctx) +} + +#[test] +fun test_init_ewma_init_values() { + let mut test = begin(@0xF); + let alice = @0xA; + test.next_tx(alice); + let mut ewma_state = test_init_ewma_state(test.ctx()); + assert!(ewma_state.enabled() == false); + assert!(ewma_state.mean() == test.ctx().gas_price()); + assert!(ewma_state.variance() == 0); + assert!(ewma_state.last_updated_timestamp() == 0); + assert!(ewma_state.enabled() == false); + + test.next_tx(alice); + ewma_state.set_alpha(1_000_000_000); + ewma_state.set_z_score_threshold(100_000_000); + ewma_state.set_additional_taker_fee(100_000_000); + ewma_state.enable(); + assert!(ewma_state.enabled() == true); + assert!(ewma_state.alpha() == 1_000_000_000); + assert!(ewma_state.z_score_threshold() == 100_000_000); + assert!(ewma_state.additional_taker_fee() == 100_000_000); + + test.next_tx(alice); + ewma_state.disable(); + assert!(ewma_state.enabled() == false); + + end(test); +} + +#[test] +fun test_update_ewma_state() { + let mut test = begin(@0xF); + let gas_price1 = 1_000; + let taker_fee = 100_000_000; + advance_scenario_with_gas_price(&mut test, gas_price1, 1000); + let mut ewma_state = test_init_ewma_state(test.ctx()); + assert_eq!(ewma_state.mean(), 1_000 * constants::float_scaling()); + assert_eq!(ewma_state.variance(), 0); + assert_eq!(ewma_state.last_updated_timestamp(), 0); + + // default alpha is 0.01, so the mean should be 0.99 * 1_000_000 + 0.01 * 2_000_000 = 1_010_000 + // difference 2000 - 1000 = 1000 (using old mean) + // diff squared = 1000000 + let gas_price2 = 2_000; + advance_scenario_with_gas_price(&mut test, gas_price2, 1000); + let mut clock = clock::create_for_testing(test.ctx()); + clock.set_for_testing(1000); + ewma_state.update(TEST_POOL_ID.to_id(), &clock, test.ctx()); + assert_eq!(ewma_state.mean(), 1_010 * constants::float_scaling()); + assert_eq!(ewma_state.variance(), 1000000 * constants::float_scaling()); + + ewma_state.enable(); + // mean = 1010, variance = 1000000, std_dev = sqrt(1000000) = 1000 + // z_score = (2000 - 1010) / 1000 = 0.99 + assert_eq!(ewma_state.z_score(test.ctx()), 990_000_000); + + let gas_price3 = 3_000; + advance_scenario_with_gas_price(&mut test, gas_price3, 1000); + clock.set_for_testing(1000 + 10); + ewma_state.update(TEST_POOL_ID.to_id(), &clock, test.ctx()); + // mean = 0.99 * 1_010_000_000_000 + 0.01 * 3_000_000_000_000 = 1_029_900_000_000 + // difference = 3_000_000_000_000 - 1_010_000_000_000 = 1_990_000_000_000 (1990, using old mean) + // diff squared = (1990 * 1990) = 3_960_100 * 10^9 + // variance = 0.99 * 1000000 + 0.01 * 3960100 = 1,029,601 * 10^9 + assert_eq!(ewma_state.mean(), 1_029_900_000_000); + assert_eq!(ewma_state.variance(), 1_029_601_000_000_000); + // diff = 3000 - 1029.9 = 1970.1 + // std_dev = sqrt(1_029_601) ≈ 1,014.692836 * 10^9 + // z_score = 1970.1 / 1,014.692836 ≈ 1.941573309 * 10^9 + assert_eq!(ewma_state.z_score(test.ctx()), 1_941_573_309); + let new_taker_fee = ewma_state.apply_taker_penalty(taker_fee, test.ctx()); + assert_eq!(new_taker_fee, taker_fee); + + let gas_price4 = 4_000; + advance_scenario_with_gas_price(&mut test, gas_price4, 1000); + clock.set_for_testing(1000 + 20); + ewma_state.update(TEST_POOL_ID.to_id(), &clock, test.ctx()); + // mean = 0.99 * 1_029_900_000_000 + 0.01 * 4_000_000_000_000 = 1059.601 * 10^9 + // difference = 4_000_000_000_000 - 1_029_900_000_000 = 2_970_100_000_000 (2970.1, using old mean) + // diff squared = (2970.1 * 2970.1) = 8,821,494.01 * 10^9 + // variance = 0.99 * 1_029_601_000_000_000 + 0.01 * 8_821_494_010_000_000 = 1,107,519.9301 * 10^9 + assert_eq!(ewma_state.mean(), 1_059_601_000_000); + assert_eq!(ewma_state.variance(), 1_107_519_930_100_000); + // diff = 4000 - 1059.601 = 2940.399 + // std_dev = sqrt(1_107_519.9301) ≈ 1,052.387388 * 10^9 + // z_score = 2940.399 / 1,052.387388 ≈ 2.794026309 * 10^9 + assert_eq!(ewma_state.z_score(test.ctx()), 2_794_026_309); + let new_taker_fee = ewma_state.apply_taker_penalty(taker_fee, test.ctx()); + assert_eq!(new_taker_fee, taker_fee); + + // lower z-score threshold + ewma_state.set_z_score_threshold(2_000_000_000); + assert!(ewma_state.enabled()); + assert!(test.ctx().gas_price() * constants::float_scaling() > ewma_state.mean()); + let new_taker_fee = ewma_state.apply_taker_penalty(taker_fee, test.ctx()); + assert_eq!(new_taker_fee, taker_fee + ewma_state.additional_taker_fee()); + + // increase taker fee + ewma_state.set_additional_taker_fee(200_000_000); + let new_taker_fee = ewma_state.apply_taker_penalty(taker_fee, test.ctx()); + assert_eq!(new_taker_fee, taker_fee + ewma_state.additional_taker_fee()); + + // lower gas fee + let low_gas_fee = 10; + advance_scenario_with_gas_price(&mut test, low_gas_fee, 1000); + clock.set_for_testing(1000 + 30); + ewma_state.update(TEST_POOL_ID.to_id(), &clock, test.ctx()); + let new_taker_fee = ewma_state.apply_taker_penalty(taker_fee, test.ctx()); + assert_eq!(new_taker_fee, taker_fee); + + // disable ewma + ewma_state.disable(); + let new_taker_fee = ewma_state.apply_taker_penalty(taker_fee, test.ctx()); + assert_eq!(new_taker_fee, taker_fee); + + destroy(clock); + end(test); +} + +fun advance_scenario_with_gas_price(test: &mut Scenario, gas_price: u64, timestamp_advance: u64) { + let ts = test.ctx().epoch_timestamp_ms() + timestamp_advance; + let ctx = test.ctx_builder().set_gas_price(gas_price).set_epoch_timestamp(ts); + test.next_with_context(ctx); +} + +#[test] +fun test_apply_taker_penalty_disabled_ewma() { + let mut test = begin(@0xF); + let alice = @0xA; + test.next_tx(alice); + + let base_taker_fee = 1_000_000; // 0.1% + let mut ewma_state = test_init_ewma_state(test.ctx()); + ewma_state.set_additional_taker_fee(500_000); // 0.05% additional + ewma_state.set_z_score_threshold(2_000_000_000); // 2 std devs + + // EWMA is disabled, so no penalty should be applied regardless of gas price + assert!(!ewma_state.enabled()); + + // Test with high gas price + let high_gas_price = 10_000; + advance_scenario_with_gas_price(&mut test, high_gas_price, 1000); + + let fee_with_penalty = ewma_state.apply_taker_penalty(base_taker_fee, test.ctx()); + assert_eq!(fee_with_penalty, base_taker_fee); // No penalty applied + + end(test); +} + +#[test] +fun test_apply_taker_penalty_gas_below_mean() { + let mut test = begin(@0xF); + + // Start with moderate gas price + let initial_gas = 1_000; + advance_scenario_with_gas_price(&mut test, initial_gas, 1000); + + let base_taker_fee = 1_000_000; // 0.1% + let mut ewma_state = test_init_ewma_state(test.ctx()); + ewma_state.set_additional_taker_fee(500_000); // 0.05% additional + ewma_state.set_z_score_threshold(1_000_000_000); // 1 std dev + ewma_state.enable(); + + let mut clock = clock::create_for_testing(test.ctx()); + clock.set_for_testing(1000); + ewma_state.update(TEST_POOL_ID.to_id(), &clock, test.ctx()); + + // Now use gas price below mean + let low_gas = 500; + advance_scenario_with_gas_price(&mut test, low_gas, 1000); + clock.set_for_testing(2000); + + // No penalty when gas is below mean + let fee_with_penalty = ewma_state.apply_taker_penalty(base_taker_fee, test.ctx()); + assert_eq!(fee_with_penalty, base_taker_fee); + + destroy(clock); + end(test); +} + +#[test] +fun test_apply_taker_penalty_gas_above_mean_below_threshold() { + let mut test = begin(@0xF); + + // Initialize with gas price = 1000 + let initial_gas = 1_000; + advance_scenario_with_gas_price(&mut test, initial_gas, 1000); + + let base_taker_fee = 2_000_000; // 0.2% + let additional_fee = 1_000_000; // 0.1% additional + let mut ewma_state = test_init_ewma_state(test.ctx()); + ewma_state.set_additional_taker_fee(additional_fee); + ewma_state.set_z_score_threshold(5_000_000_000); // 5 std devs (very high threshold) + ewma_state.enable(); + + let mut clock = clock::create_for_testing(test.ctx()); + clock.set_for_testing(1000); + ewma_state.update(TEST_POOL_ID.to_id(), &clock, test.ctx()); + + // Use gas price moderately above mean + let moderate_gas = 1_500; + advance_scenario_with_gas_price(&mut test, moderate_gas, 1000); + clock.set_for_testing(2000); + ewma_state.update(TEST_POOL_ID.to_id(), &clock, test.ctx()); + + // Gas is above mean but z-score is below threshold, no penalty + let fee_with_penalty = ewma_state.apply_taker_penalty(base_taker_fee, test.ctx()); + assert_eq!(fee_with_penalty, base_taker_fee); + + destroy(clock); + end(test); +} + +#[test] +fun test_apply_taker_penalty_z_score_above_threshold() { + let mut test = begin(@0xF); + + // Initialize with low gas price + let initial_gas = 100; + advance_scenario_with_gas_price(&mut test, initial_gas, 1000); + + let base_taker_fee = 1_000_000; // 0.1% + let additional_fee = 500_000; // 0.05% additional + let mut ewma_state = test_init_ewma_state(test.ctx()); + ewma_state.set_additional_taker_fee(additional_fee); + ewma_state.set_z_score_threshold(1_000_000_000); // 1 std dev + ewma_state.enable(); + + let mut clock = clock::create_for_testing(test.ctx()); + clock.set_for_testing(1000); + ewma_state.update(TEST_POOL_ID.to_id(), &clock, test.ctx()); + + // Gradually increase gas price to build up variance + let gas_price_2 = 200; + advance_scenario_with_gas_price(&mut test, gas_price_2, 1000); + clock.set_for_testing(2000); + ewma_state.update(TEST_POOL_ID.to_id(), &clock, test.ctx()); + + let gas_price_3 = 400; + advance_scenario_with_gas_price(&mut test, gas_price_3, 1000); + clock.set_for_testing(3000); + ewma_state.update(TEST_POOL_ID.to_id(), &clock, test.ctx()); + + // Now spike the gas price significantly + let spike_gas = 10_000; + advance_scenario_with_gas_price(&mut test, spike_gas, 1000); + clock.set_for_testing(4000); + ewma_state.update(TEST_POOL_ID.to_id(), &clock, test.ctx()); + + // Z-score should be high enough to trigger penalty + let z_score = ewma_state.z_score(test.ctx()); + assert!(z_score > ewma_state.z_score_threshold()); + + let fee_with_penalty = ewma_state.apply_taker_penalty(base_taker_fee, test.ctx()); + assert_eq!(fee_with_penalty, base_taker_fee + additional_fee); + + destroy(clock); + end(test); +} + +#[test] +fun test_dynamic_additional_taker_fee_changes() { + let mut test = begin(@0xF); + + let initial_gas = 100; + advance_scenario_with_gas_price(&mut test, initial_gas, 1000); + + let base_taker_fee = 1_000_000; // 0.1% + let mut ewma_state = test_init_ewma_state(test.ctx()); + ewma_state.set_additional_taker_fee(250_000); // 0.025% initially + ewma_state.set_z_score_threshold(500_000_000); // 0.5 std dev (low threshold for testing) + ewma_state.enable(); + + let mut clock = clock::create_for_testing(test.ctx()); + clock.set_for_testing(1000); + ewma_state.update(TEST_POOL_ID.to_id(), &clock, test.ctx()); + + // Spike gas price + let spike_gas = 5_000; + advance_scenario_with_gas_price(&mut test, spike_gas, 1000); + clock.set_for_testing(2000); + ewma_state.update(TEST_POOL_ID.to_id(), &clock, test.ctx()); + + // Apply penalty with first additional fee + let fee_1 = ewma_state.apply_taker_penalty(base_taker_fee, test.ctx()); + assert_eq!(fee_1, base_taker_fee + 250_000); + + // Change additional taker fee + ewma_state.set_additional_taker_fee(750_000); // 0.075% + + // Same conditions, different penalty + let fee_2 = ewma_state.apply_taker_penalty(base_taker_fee, test.ctx()); + assert_eq!(fee_2, base_taker_fee + 750_000); + + // Set to maximum allowed + ewma_state.set_additional_taker_fee(constants::max_additional_taker_fee()); + let fee_3 = ewma_state.apply_taker_penalty(base_taker_fee, test.ctx()); + assert_eq!(fee_3, base_taker_fee + constants::max_additional_taker_fee()); + + destroy(clock); + end(test); +} + +#[test] +fun test_ewma_state_timestamping() { + let mut test = begin(@0xF); + let alice = @0xA; + test.next_tx(alice); + + let mut ewma_state = test_init_ewma_state(test.ctx()); + assert_eq!(ewma_state.last_updated_timestamp(), 0); + + let mut clock = clock::create_for_testing(test.ctx()); + clock.set_for_testing(5000); + ewma_state.update(TEST_POOL_ID.to_id(), &clock, test.ctx()); + assert_eq!(ewma_state.last_updated_timestamp(), 5000); + + // Update at same timestamp should be no-op + let mean_before = ewma_state.mean(); + let variance_before = ewma_state.variance(); + ewma_state.update(TEST_POOL_ID.to_id(), &clock, test.ctx()); + assert_eq!(ewma_state.mean(), mean_before); + assert_eq!(ewma_state.variance(), variance_before); + assert_eq!(ewma_state.last_updated_timestamp(), 5000); + + // Update with new timestamp + clock.set_for_testing(10000); + ewma_state.update(TEST_POOL_ID.to_id(), &clock, test.ctx()); + assert_eq!(ewma_state.last_updated_timestamp(), 10000); + + destroy(clock); + end(test); +} + +#[test] +fun test_z_score_with_zero_variance() { + let mut test = begin(@0xF); + let alice = @0xA; + test.next_tx(alice); + + let ewma_state = test_init_ewma_state(test.ctx()); + // Initial variance is 0 + assert_eq!(ewma_state.variance(), 0); + + // Z-score should be 0 when variance is 0 + let z = ewma_state.z_score(test.ctx()); + assert_eq!(z, 0); + + end(test); +} + +#[test] +fun test_alpha_parameter_effect() { + let mut test = begin(@0xF); + + // Test with high alpha (more weight on current price) + let initial_gas = 1_000; + advance_scenario_with_gas_price(&mut test, initial_gas, 1000); + + let mut ewma_high_alpha = test_init_ewma_state(test.ctx()); + ewma_high_alpha.set_alpha(500_000_000); // 50% weight on current + + let mut clock = clock::create_for_testing(test.ctx()); + clock.set_for_testing(1000); + ewma_high_alpha.update(TEST_POOL_ID.to_id(), &clock, test.ctx()); + + let new_gas = 2_000; + advance_scenario_with_gas_price(&mut test, new_gas, 1000); + clock.set_for_testing(2000); + ewma_high_alpha.update(TEST_POOL_ID.to_id(), &clock, test.ctx()); + + // With alpha = 0.5: new_mean = 0.5 * 2000 + 0.5 * 1000 = 1500 * float_scaling + assert_eq!(ewma_high_alpha.mean(), 1_500 * constants::float_scaling()); + + // Test with low alpha (more weight on historical) + let initial_gas_2 = 1_000; + advance_scenario_with_gas_price(&mut test, initial_gas_2, 1000); + + let mut ewma_low_alpha = test_init_ewma_state(test.ctx()); + ewma_low_alpha.set_alpha(10_000_000); // 1% weight on current (default) + + clock.set_for_testing(3000); + ewma_low_alpha.update(TEST_POOL_ID.to_id(), &clock, test.ctx()); + + let new_gas_2 = 2_000; + advance_scenario_with_gas_price(&mut test, new_gas_2, 1000); + clock.set_for_testing(4000); + ewma_low_alpha.update(TEST_POOL_ID.to_id(), &clock, test.ctx()); + + // With alpha = 0.01: new_mean = 0.01 * 2000 + 0.99 * 1000 = 1010 * float_scaling + assert_eq!(ewma_low_alpha.mean(), 1_010 * constants::float_scaling()); + + destroy(clock); + end(test); +} diff --git a/packages/deepbook/tests/state/governance_tests.move b/packages/deepbook/tests/state/governance_tests.move index fb2a1a2a0..c1d34def2 100644 --- a/packages/deepbook/tests/state/governance_tests.move +++ b/packages/deepbook/tests/state/governance_tests.move @@ -5,12 +5,8 @@ module deepbook::governance_tests; use deepbook::{constants, governance}; -use sui::{ - address, - object::id_from_address, - test_scenario::{next_tx, begin, end}, - test_utils::{destroy, assert_eq} -}; +use std::unit_test::{assert_eq, destroy}; +use sui::{address, object::id_from_address, test_scenario::{next_tx, begin, end}}; const OWNER: address = @0xF; const ALICE: address = @0xA; @@ -24,10 +20,11 @@ fun add_proposal_volatile_ok() { let alice = ALICE; test.next_tx(alice); + let whitelisted = false; let stable_pool = false; - let mut gov = governance::empty(stable_pool, test.ctx()); + let mut gov = governance::empty(whitelisted, stable_pool, test.ctx()); gov.add_proposal(500000, 200000, 10000, 1000, id_from_address(alice)); - assert!(gov.proposals().size() == 1, 0); + assert!(gov.proposals().length() == 1, 0); let (taker_fee, maker_fee, stake_required) = gov .proposals() .get(&id_from_address(alice)) @@ -46,8 +43,9 @@ fun add_proposal_volatile_taker_not_multiple_e() { let alice = ALICE; test.next_tx(alice); + let whitelisted = false; let stable_pool = false; - let mut gov = governance::empty(stable_pool, test.ctx()); + let mut gov = governance::empty(whitelisted, stable_pool, test.ctx()); gov.add_proposal(500100, 200000, 10000, 1000, id_from_address(alice)); abort 0 } @@ -58,8 +56,9 @@ fun add_proposal_volatile_low_taker_e() { let alice = ALICE; test.next_tx(alice); + let whitelisted = false; let stable_pool = false; - let mut gov = governance::empty(stable_pool, test.ctx()); + let mut gov = governance::empty(whitelisted, stable_pool, test.ctx()); gov.add_proposal(99000, 200000, 10000, 1000, id_from_address(alice)); abort 0 } @@ -70,8 +69,9 @@ fun add_proposal_volatile_high_taker_e() { let alice = ALICE; test.next_tx(alice); + let whitelisted = false; let stable_pool = false; - let mut gov = governance::empty(stable_pool, test.ctx()); + let mut gov = governance::empty(whitelisted, stable_pool, test.ctx()); gov.add_proposal(1010000, 200000, 10000, 1000, id_from_address(alice)); abort 0 } @@ -82,8 +82,9 @@ fun add_proposal_volatile_high_maker_e() { let alice = ALICE; test.next_tx(alice); + let whitelisted = false; let stable_pool = false; - let mut gov = governance::empty(stable_pool, test.ctx()); + let mut gov = governance::empty(whitelisted, stable_pool, test.ctx()); gov.add_proposal(500000, 510000, 10000, 1000, id_from_address(alice)); abort 0 } @@ -94,12 +95,13 @@ fun add_proposal_stable_ok() { let alice = ALICE; test.next_tx(OWNER); + let whitelisted = false; let stable_pool = true; - let mut gov = governance::empty(stable_pool, test.ctx()); + let mut gov = governance::empty(whitelisted, stable_pool, test.ctx()); test.next_tx(alice); gov.add_proposal(50000, 20000, 10000, 1000, id_from_address(alice)); - assert!(gov.proposals().size() == 1, 0); + assert!(gov.proposals().length() == 1, 0); destroy(gov); end(test); @@ -111,8 +113,9 @@ fun add_proposal_stable_taker_e() { let alice = ALICE; test.next_tx(OWNER); + let whitelisted = false; let stable_pool = true; - let mut gov = governance::empty(stable_pool, test.ctx()); + let mut gov = governance::empty(whitelisted, stable_pool, test.ctx()); test.next_tx(alice); gov.add_proposal(500000, 20000, 10000, 1000, id_from_address(alice)); @@ -125,8 +128,9 @@ fun add_proposal_stable_low_taker_e() { let alice = ALICE; test.next_tx(OWNER); + let whitelisted = false; let stable_pool = true; - let mut gov = governance::empty(stable_pool, test.ctx()); + let mut gov = governance::empty(whitelisted, stable_pool, test.ctx()); test.next_tx(alice); gov.add_proposal(9000, 20000, 10000, 10000, id_from_address(alice)); @@ -139,8 +143,9 @@ fun add_proposal_stable_high_taker_e() { let alice = ALICE; test.next_tx(OWNER); + let whitelisted = false; let stable_pool = true; - let mut gov = governance::empty(stable_pool, test.ctx()); + let mut gov = governance::empty(whitelisted, stable_pool, test.ctx()); test.next_tx(alice); gov.add_proposal(110000, 20000, 10000, 10000, id_from_address(alice)); @@ -153,58 +158,23 @@ fun add_proposal_stable_maker_e() { let alice = ALICE; test.next_tx(OWNER); + let whitelisted = false; let stable_pool = true; - let mut gov = governance::empty(stable_pool, test.ctx()); + let mut gov = governance::empty(whitelisted, stable_pool, test.ctx()); test.next_tx(alice); gov.add_proposal(50000, 200000, 10000, 1000, id_from_address(alice)); abort 0 } -#[test] -fun set_whitelist_ok() { - let mut test = begin(OWNER); - - test.next_tx(OWNER); - let stable_pool = false; - let mut gov = governance::empty(stable_pool, test.ctx()); - gov.add_proposal(500000, 200000, 10000, 1000, id_from_address(OWNER)); - assert!(gov.proposals().size() == 1, 0); - - // Setting whitelist to true removes all proposals, - // and sets all trade params to 0. - gov.set_whitelist(true); - assert!(gov.whitelisted(), 0); - assert!(!gov.stable(), 0); - assert!(gov.proposals().size() == 0, 0); - let trade_params = gov.trade_params(); - assert!(trade_params.taker_fee() == 0, 0); - assert!(trade_params.maker_fee() == 0, 0); - assert!(trade_params.stake_required() == 0, 0); - assert_eq(trade_params, gov.next_trade_params()); - - // Setting whitelist to false resets params to default volatile values. - test.next_tx(OWNER); - gov.set_whitelist(false); - assert!(!gov.whitelisted(), 0); - assert!(!gov.stable(), 0); - let trade_params = gov.trade_params(); - assert!(trade_params.taker_fee() == 1000000, 0); - assert!(trade_params.maker_fee() == 500000, 0); - assert!(trade_params.stake_required() == 0, 0); - - destroy(gov); - end(test); -} - #[test, expected_failure(abort_code = governance::EWhitelistedPoolCannotChange)] fun add_proposal_whitelisted_e() { let mut test = begin(OWNER); let alice = ALICE; test.next_tx(OWNER); + let whitelisted = true; let stable_pool = false; - let mut gov = governance::empty(stable_pool, test.ctx()); - gov.set_whitelist(true); + let mut gov = governance::empty(whitelisted, stable_pool, test.ctx()); test.next_tx(ALICE); gov.add_proposal(500000, 200000, 10000, 1000, id_from_address(alice)); @@ -218,8 +188,9 @@ fun adjust_voting_power_ok() { let mut alice_stake = 0; test.next_tx(OWNER); + let whitelisted = false; let stable_pool = false; - let mut gov = governance::empty(stable_pool, test.ctx()); + let mut gov = governance::empty(whitelisted, stable_pool, test.ctx()); test.next_tx(alice); gov.adjust_voting_power(alice_stake, alice_stake + 1000); @@ -258,12 +229,13 @@ fun update_ok() { let alice = ALICE; test.next_tx(OWNER); + let whitelisted = false; let stable_pool = false; - let mut gov = governance::empty(stable_pool, test.ctx()); + let mut gov = governance::empty(whitelisted, stable_pool, test.ctx()); assert!(gov.voting_power() == 0, 0); assert!(gov.quorum() == 0, 0); - assert!(gov.proposals().size() == 0, 0); - assert_eq(gov.trade_params(), gov.next_trade_params()); + assert!(gov.proposals().length() == 0, 0); + assert_eq!(gov.trade_params(), gov.next_trade_params()); gov.adjust_voting_power(0, 1000); assert!(gov.voting_power() == 1000, 0); @@ -276,7 +248,7 @@ fun update_ok() { test.next_tx(alice); gov.add_proposal(500000, 200000, 10000, 1000, id_from_address(alice)); gov.adjust_vote(option::none(), option::some(id_from_address(alice)), 1000); - assert!(gov.proposals().size() == 1, 0); + assert!(gov.proposals().length() == 1, 0); assert!(gov.quorum() == 500, 0); let trade_params = gov.trade_params(); assert!(trade_params.taker_fee() == 1000000, 0); @@ -289,9 +261,9 @@ fun update_ok() { // update doesn't apply proposal yet since epoch hasn't changed gov.update(test.ctx()); - assert_eq(trade_params, gov.trade_params()); - assert_eq(next_trade_params, gov.next_trade_params()); - assert!(gov.proposals().size() == 1, 0); + assert_eq!(trade_params, gov.trade_params()); + assert_eq!(next_trade_params, gov.next_trade_params()); + assert!(gov.proposals().length() == 1, 0); assert!(gov.voting_power() == 1000, 0); assert!(gov.quorum() == 500, 0); @@ -302,8 +274,8 @@ fun update_ok() { assert!(trade_params.taker_fee() == 500000, 0); assert!(trade_params.maker_fee() == 200000, 0); assert!(trade_params.stake_required() == 10000, 0); - assert_eq(trade_params, gov.next_trade_params()); - assert!(gov.proposals().size() == 0, 0); + assert_eq!(trade_params, gov.next_trade_params()); + assert!(gov.proposals().length() == 0, 0); assert!(gov.voting_power() == 1000, 0); assert!(gov.quorum() == 500, 0); @@ -318,8 +290,9 @@ fun adjust_vote_ok() { let bob = BOB; test.next_tx(OWNER); + let whitelisted = false; let stable_pool = false; - let mut gov = governance::empty(stable_pool, test.ctx()); + let mut gov = governance::empty(whitelisted, stable_pool, test.ctx()); gov.adjust_voting_power(0, 500); assert!(gov.voting_power() == 500, 0); @@ -334,7 +307,7 @@ fun adjust_vote_ok() { gov.adjust_vote(option::none(), option::some(id_from_address(alice)), 200); assert!(gov.proposals().get(&id_from_address(alice)).votes() == 200, 0); assert!(gov.next_trade_params().taker_fee() == 1000000, 0); - assert_eq(gov.trade_params(), gov.next_trade_params()); + assert_eq!(gov.trade_params(), gov.next_trade_params()); // bob proposes proposal 1, votes with 300 votes, over quorum test.next_tx(bob); @@ -388,8 +361,9 @@ fun adjust_vote_e() { let alice = ALICE; test.next_tx(alice); + let whitelisted = false; let stable_pool = false; - let mut gov = governance::empty(stable_pool, test.ctx()); + let mut gov = governance::empty(whitelisted, stable_pool, test.ctx()); gov.adjust_vote(option::none(), option::some(id_from_address(alice)), 1000); abort 0 } @@ -401,8 +375,9 @@ fun adjust_vote2_e() { let bob = BOB; test.next_tx(alice); + let whitelisted = false; let stable_pool = false; - let mut gov = governance::empty(stable_pool, test.ctx()); + let mut gov = governance::empty(whitelisted, stable_pool, test.ctx()); gov.add_proposal(500000, 200000, 10000, 200, id_from_address(alice)); gov.adjust_vote(option::none(), option::some(id_from_address(alice)), 1000); gov.adjust_vote( @@ -420,8 +395,9 @@ fun adjust_vote_from_removed_proposal_ok() { let bob = BOB; test.next_tx(alice); + let whitelisted = false; let stable_pool = false; - let mut gov = governance::empty(stable_pool, test.ctx()); + let mut gov = governance::empty(whitelisted, stable_pool, test.ctx()); gov.add_proposal(500000, 200000, 10000, 200, id_from_address(alice)); gov.adjust_vote( option::some(id_from_address(bob)), @@ -448,8 +424,9 @@ fun remove_proposal_vote_e() { let charlie = CHARLIE; test.next_tx(OWNER); + let whitelisted = false; let stable_pool = false; - let mut gov = governance::empty(stable_pool, test.ctx()); + let mut gov = governance::empty(whitelisted, stable_pool, test.ctx()); gov.adjust_voting_power(0, 450000); test.next_epoch(OWNER); @@ -480,12 +457,12 @@ fun remove_proposal_vote_e() { option::some(id_from_address(alice)), 100000, ); - assert_eq(gov.trade_params(), gov.next_trade_params()); + assert_eq!(gov.trade_params(), gov.next_trade_params()); // Bob proposes and votes with 200000 stake, not enough to push proposal Bob // over quorum gov.add_proposal(600000, 300000, 20000, 200000, id_from_address(bob)); gov.adjust_vote(option::none(), option::some(id_from_address(bob)), 200000); - assert_eq(gov.trade_params(), gov.next_trade_params()); + assert_eq!(gov.trade_params(), gov.next_trade_params()); // Charlie votes with 150000 stake, enough to push proposal ALICE over // quorum @@ -500,7 +477,7 @@ fun remove_proposal_vote_e() { assert!(trade_params.maker_fee() == 200000, 0); assert!(trade_params.stake_required() == 10000, 0); - assert!(gov.proposals().size() == (100 as u64), 0); + assert!(gov.proposals().length() == 100u64, 0); // Charlie makes a new proposal, proposal ALICE should be removed, not BOB gov.adjust_vote( @@ -530,8 +507,9 @@ fun remove_proposal_stake_too_low_e() { let alice = ALICE; test.next_tx(OWNER); + let whitelisted = false; let stable_pool = false; - let mut gov = governance::empty(stable_pool, test.ctx()); + let mut gov = governance::empty(whitelisted, stable_pool, test.ctx()); let mut i = 0; while (i < MAX_PROPOSALS) { @@ -546,7 +524,7 @@ fun remove_proposal_stake_too_low_e() { i = i + 1; }; - assert!(gov.proposals().size() == (MAX_PROPOSALS as u64), 0); + assert!(gov.proposals().length() == (MAX_PROPOSALS as u64), 0); gov.add_proposal(500000, 200000, 10000, 1000, id_from_address(alice)); abort 0 @@ -559,8 +537,9 @@ fun adjust_votes_remove_from_removed_ok() { let bob = BOB; test.next_tx(OWNER); + let whitelisted = false; let stable_pool = false; - let mut gov = governance::empty(stable_pool, test.ctx()); + let mut gov = governance::empty(whitelisted, stable_pool, test.ctx()); gov.add_proposal(500000, 200000, 10000, 1000, id_from_address(alice)); gov.adjust_vote(option::none(), option::some(id_from_address(alice)), 1000); assert!(gov.proposals().get(&id_from_address(alice)).votes() == 1000, 0); @@ -576,7 +555,7 @@ fun adjust_votes_remove_from_removed_ok() { ); i = i + 1; }; - assert!(gov.proposals().size() == 100, 0); + assert!(gov.proposals().length() == 100, 0); test.next_tx(bob); gov.add_proposal(500000, 200000, 10000, 3000, id_from_address(bob)); @@ -598,8 +577,9 @@ fun adjust_voting_power_over_threshold_ok() { let mut test = begin(OWNER); test.next_tx(OWNER); + let whitelisted = false; let stable_pool = false; - let mut gov = governance::empty(stable_pool, test.ctx()); + let mut gov = governance::empty(whitelisted, stable_pool, test.ctx()); gov.adjust_voting_power(0, 100_000 * constants::deep_unit()); assert!(gov.voting_power() == 100_000 * constants::deep_unit(), 0); test.next_epoch(OWNER); diff --git a/packages/deepbook/tests/state/history_tests.move b/packages/deepbook/tests/state/history_tests.move index 639a643ea..f6935b095 100644 --- a/packages/deepbook/tests/state/history_tests.move +++ b/packages/deepbook/tests/state/history_tests.move @@ -5,7 +5,8 @@ module deepbook::history_tests; use deepbook::{balances, constants, history, trade_params}; -use sui::{test_scenario::{begin, end}, test_utils}; +use std::unit_test::destroy; +use sui::test_scenario::{begin, end}; const EWrongRebateAmount: u64 = 0; @@ -55,7 +56,7 @@ fun test_rebate_amount() { assert!(rebate.quote() == 450_000, EWrongRebateAmount); assert!(rebate.deep() == 180_000_000, EWrongRebateAmount); - test_utils::destroy(history); + destroy(history); end(test); } @@ -124,7 +125,7 @@ fun test_epoch_skipped() { assert!(rebate_epoch_1_bob.quote() == 450_000, EWrongRebateAmount); assert!(rebate_epoch_1_bob.deep() == 180_000_000, EWrongRebateAmount); - test_utils::destroy(history); + destroy(history); end(test); } @@ -173,7 +174,7 @@ fun test_other_maker_volume_above_phase_out() { assert!(rebate.quote() == 0, EWrongRebateAmount); assert!(rebate.deep() == 0, EWrongRebateAmount); - test_utils::destroy(history); + destroy(history); end(test); } @@ -238,6 +239,6 @@ fun test_rebate_edge_epoch_ok() { assert!(rebate.quote() == 180_000, EWrongRebateAmount); assert!(rebate.deep() == 180_000_000, EWrongRebateAmount); - test_utils::destroy(history); + destroy(history); end(test); } diff --git a/packages/deepbook/tests/state/state_tests.move b/packages/deepbook/tests/state/state_tests.move index dd7f74083..8a93bca43 100644 --- a/packages/deepbook/tests/state/state_tests.move +++ b/packages/deepbook/tests/state/state_tests.move @@ -7,15 +7,13 @@ module deepbook::state_tests; use deepbook::{ balances, constants, + ewma_tests::test_init_ewma_state, order_info_tests::{create_order_info_base, create_order_info}, state, utils }; -use sui::{ - object::id_from_address, - test_scenario::{next_tx, begin, end}, - test_utils::{assert_eq, destroy} -}; +use std::unit_test::{assert_eq, destroy}; +use sui::{object::id_from_address, test_scenario::{next_tx, begin, end}}; const OWNER: address = @0xF; const ALICE: address = @0xA; @@ -38,8 +36,9 @@ fun process_create_ok() { test.ctx().epoch(), ); + let whitelisted = false; let stable_pool = false; - let mut state = state::empty(stable_pool, test.ctx()); + let mut state = state::empty(whitelisted, stable_pool, test.ctx()); let price = 1 * constants::usdc_unit(); let quantity = 1 * constants::sui_unit(); let mut order_info1 = create_order_info_base( @@ -49,13 +48,15 @@ fun process_create_ok() { true, test.ctx().epoch(), ); + let ewma_state = test_init_ewma_state(test.ctx()); let (settled, owed) = state.process_create( &mut order_info1, + &ewma_state, object::id_from_address(@0x0), test.ctx(), ); - assert_eq(settled, balances::new(0, 0, 0)); - assert_eq(owed, balances::new(0, 1 * constants::usdc_unit(), 500_000)); + assert_eq!(settled, balances::new(0, 0, 0)); + assert_eq!(owed, balances::new(0, 1 * constants::usdc_unit(), 500_000)); taker_order.match_maker(&mut order_info1.to_order(), 0); test.next_tx(ALICE); @@ -70,11 +71,12 @@ fun process_create_ok() { ); let (settled, owed) = state.process_create( &mut order_info2, + &ewma_state, object::id_from_address(@0x0), test.ctx(), ); - assert_eq(settled, balances::new(0, 0, 0)); - assert_eq(owed, balances::new(0, 1_002_002, 500_500)); // rounds down + assert_eq!(settled, balances::new(0, 0, 0)); + assert_eq!(owed, balances::new(0, 1_002_002, 500_500)); // rounds down taker_order.match_maker(&mut order_info2.to_order(), 0); test.next_tx(ALICE); @@ -89,11 +91,12 @@ fun process_create_ok() { ); let (settled, owed) = state.process_create( &mut order_info3, + &ewma_state, object::id_from_address(@0x0), test.ctx(), ); - assert_eq(settled, balances::new(0, 0, 0)); - assert_eq(owed, balances::new(1_999_000_000, 0, 999_500)); + assert_eq!(settled, balances::new(0, 0, 0)); + assert_eq!(owed, balances::new(1_999_000_000, 0, 999_500)); // the taker order has filled the first two maker orders and has some // quantities remaining. @@ -107,29 +110,30 @@ fun process_create_ok() { // total fees = 0.002001001 + 0.003999499 = 0.0060005 = 6000500 let (settled, owed) = state.process_create( &mut taker_order, + &ewma_state, object::id_from_address(@0x0), test.ctx(), ); - assert_eq(settled, balances::new(0, 2_002_002, 0)); - assert_eq(owed, balances::new(10 * constants::sui_unit(), 0, 6_000_500)); + assert_eq!(settled, balances::new(0, 2_002_002, 0)); + assert_eq!(owed, balances::new(10 * constants::sui_unit(), 0, 6_000_500)); // Alice has 1 open order remaining. The first two orders have been filled. let alice = state.account(id_from_address(ALICE)); assert!(alice.total_volume() == 2_001_001_000, 0); - assert!(alice.open_orders().size() == 1, 0); + assert!(alice.open_orders().length() == 1, 0); assert!(alice.open_orders().contains(&order_info3.order_id()), 0); // she traded BOB for 2.001001 SUI - assert_eq(alice.settled_balances(), balances::new(2_001_001_000, 0, 0)); - assert_eq(alice.owed_balances(), balances::new(0, 0, 0)); + assert_eq!(alice.settled_balances(), balances::new(2_001_001_000, 0, 0)); + assert_eq!(alice.owed_balances(), balances::new(0, 0, 0)); // Bob has 1 open order after the partial fill. let bob = state.account(id_from_address(BOB)); assert!(bob.total_volume() == 2_001_001_000, 0); - assert!(bob.open_orders().size() == 1, 0); + assert!(bob.open_orders().length() == 1, 0); assert!(bob.open_orders().contains(&taker_order.order_id()), 0); // Bob's balances have been settled already - assert_eq(bob.settled_balances(), balances::new(0, 0, 0)); - assert_eq(bob.owed_balances(), balances::new(0, 0, 0)); + assert_eq!(bob.settled_balances(), balances::new(0, 0, 0)); + assert_eq!(bob.owed_balances(), balances::new(0, 0, 0)); destroy(state); test.end(); @@ -152,8 +156,9 @@ fun process_create_expired_ok() { test.ctx().epoch(), ); + let whitelisted = false; let stable_pool = false; - let mut state = state::empty(stable_pool, test.ctx()); + let mut state = state::empty(whitelisted, stable_pool, test.ctx()); let price = 1 * constants::usdc_unit(); let quantity = 10 * constants::sui_unit(); let balance_manager_id = id_from_address(ALICE); @@ -181,22 +186,25 @@ fun process_create_expired_ok() { fill_limit_reached, order_inserted, ); + let ewma_state = test_init_ewma_state(test.ctx()); let (settled, owed) = state.process_create( &mut order_info1, + &ewma_state, object::id_from_address(@0x0), test.ctx(), ); - assert_eq(settled, balances::new(0, 0, 0)); - assert_eq(owed, balances::new(0, 10 * constants::usdc_unit(), 5_000_000)); + assert_eq!(settled, balances::new(0, 0, 0)); + assert_eq!(owed, balances::new(0, 10 * constants::usdc_unit(), 5_000_000)); let mut order = order_info1.to_order(); taker_order.match_maker(&mut order, 0); let (settled, owed) = state.process_create( &mut taker_order, + &ewma_state, object::id_from_address(@0x0), test.ctx(), ); - assert_eq(settled, balances::new(0, 5 * constants::usdc_unit(), 0)); - assert_eq(owed, balances::new(5 * constants::sui_unit(), 0, 5_000_000)); + assert_eq!(settled, balances::new(0, 5 * constants::usdc_unit(), 0)); + assert_eq!(owed, balances::new(5 * constants::sui_unit(), 0, 5_000_000)); let mut taker_order2 = create_order_info_base( CHARLIE, @@ -208,17 +216,18 @@ fun process_create_expired_ok() { taker_order2.match_maker(&mut order, 10); let (settled, owed) = state.process_create( &mut taker_order2, + &ewma_state, object::id_from_address(@0x0), test.ctx(), ); - assert_eq(settled, balances::new(0, 0, 0)); - assert_eq(owed, balances::new(5 * constants::sui_unit(), 0, 2_500_000)); + assert_eq!(settled, balances::new(0, 0, 0)); + assert_eq!(owed, balances::new(5 * constants::sui_unit(), 0, 2_500_000)); // maker had 5 SUI filled, 5 SUI expired let (settled, owed) = state.withdraw_settled_amounts( id_from_address(ALICE), ); - assert_eq( + assert_eq!( settled, balances::new( 5 * constants::sui_unit(), @@ -226,7 +235,7 @@ fun process_create_expired_ok() { 2_500_000, ), ); - assert_eq(owed, balances::new(0, 0, 0)); + assert_eq!(owed, balances::new(0, 0, 0)); destroy(state); test.end(); @@ -267,8 +276,9 @@ fun process_create_deep_price_ok() { order_inserted, ); + let whitelisted = false; let stable_pool = false; - let mut state = state::empty(stable_pool, test.ctx()); + let mut state = state::empty(whitelisted, stable_pool, test.ctx()); let price = 13 * constants::usdc_unit(); let quantity = 13 * constants::sui_unit(); let mut order_info = create_order_info_base( @@ -278,25 +288,28 @@ fun process_create_deep_price_ok() { true, test.ctx().epoch(), ); + let ewma_state = test_init_ewma_state(test.ctx()); let (settled, owed) = state.process_create( &mut order_info, + &ewma_state, object::id_from_address(@0x0), test.ctx(), ); - assert_eq(settled, balances::new(0, 0, 0)); - assert_eq(owed, balances::new(0, 169 * constants::usdc_unit(), 6_500_000)); + assert_eq!(settled, balances::new(0, 0, 0)); + assert_eq!(owed, balances::new(0, 169 * constants::usdc_unit(), 6_500_000)); taker_order.match_maker(&mut order_info.to_order(), 0); let (settled, owed) = state.process_create( &mut taker_order, + &ewma_state, object::id_from_address(@0x0), test.ctx(), ); - assert_eq(settled, balances::new(0, 130 * constants::usdc_unit(), 0)); + assert_eq!(settled, balances::new(0, 130 * constants::usdc_unit(), 0)); // taker fee 0.001, quantity 10, deep_per_base 21 // 10 * 21 * 0.001 = 0.21 = 210000000 - assert_eq(owed, balances::new(10_000_000_000, 0, 210_000_000)); + assert_eq!(owed, balances::new(10_000_000_000, 0, 210_000_000)); destroy(state); test.end(); @@ -309,8 +322,9 @@ fun process_create_stake_req_ok() { let mut test = begin(OWNER); test.next_tx(ALICE); + let whitelisted = false; let stable_pool = false; - let mut state = state::empty(stable_pool, test.ctx()); + let mut state = state::empty(whitelisted, stable_pool, test.ctx()); state.process_stake( id_from_address(POOL_ID), id_from_address(ALICE), @@ -341,8 +355,10 @@ fun process_create_stake_req_ok() { true, test.ctx().epoch(), ); + let ewma_state = test_init_ewma_state(test.ctx()); state.process_create( &mut order_info, + &ewma_state, object::id_from_address(@0x0), test.ctx(), ); @@ -362,11 +378,12 @@ fun process_create_stake_req_ok() { taker_order.match_maker(&mut order_info.to_order(), 0); let (settled, owed) = state.process_create( &mut taker_order, + &ewma_state, object::id_from_address(@0x0), test.ctx(), ); - assert_eq(settled, balances::new(0, 1 * constants::usdc_unit(), 0)); - assert_eq(owed, balances::new(1 * constants::sui_unit(), 0, 500_000)); + assert_eq!(settled, balances::new(0, 1 * constants::usdc_unit(), 0)); + assert_eq!(owed, balances::new(1 * constants::sui_unit(), 0, 500_000)); destroy(state); test.end(); @@ -379,8 +396,9 @@ fun process_create_after_raising_steak_req_ok() { test.next_tx(ALICE); // alice and bob stake 100 DEEP each // default stake required is 100 + let whitelisted = false; let stable_pool = false; - let mut state = state::empty(stable_pool, test.ctx()); + let mut state = state::empty(whitelisted, stable_pool, test.ctx()); state.process_stake( id_from_address(POOL_ID), id_from_address(ALICE), @@ -409,8 +427,10 @@ fun process_create_after_raising_steak_req_ok() { true, test.ctx().epoch(), ); + let ewma_state = test_init_ewma_state(test.ctx()); state.process_create( &mut order_info, + &ewma_state, object::id_from_address(@0x0), test.ctx(), ); @@ -428,13 +448,14 @@ fun process_create_after_raising_steak_req_ok() { taker_order.match_maker(&mut order, 0); let (settled, owed) = state.process_create( &mut taker_order, + &ewma_state, object::id_from_address(@0x0), test.ctx(), ); // bob's first order // pays 1 SUI for the trade along with 0.001 DEEP in fees to receive 1 USDC - assert_eq(settled, balances::new(0, 100 * constants::usdc_unit(), 0)); - assert_eq(owed, balances::new(100 * constants::sui_unit(), 0, 100_000_000)); + assert_eq!(settled, balances::new(0, 100 * constants::usdc_unit(), 0)); + assert_eq!(owed, balances::new(100 * constants::sui_unit(), 0, 100_000_000)); // bob's second order, gets reduced taker fees test.next_tx(BOB); @@ -450,11 +471,12 @@ fun process_create_after_raising_steak_req_ok() { taker_order.match_maker(&mut order, 0); let (settled, owed) = state.process_create( &mut taker_order, + &ewma_state, object::id_from_address(@0x0), test.ctx(), ); - assert_eq(settled, balances::new(0, 100 * constants::usdc_unit(), 0)); - assert_eq(owed, balances::new(100 * constants::sui_unit(), 0, 50_000_000)); + assert_eq!(settled, balances::new(0, 100 * constants::usdc_unit(), 0)); + assert_eq!(owed, balances::new(100 * constants::sui_unit(), 0, 50_000_000)); // alice makes a proposal to raise the stake required to 200 and votes for // it @@ -485,11 +507,12 @@ fun process_create_after_raising_steak_req_ok() { taker_order.match_maker(&mut order, 0); let (settled, owed) = state.process_create( &mut taker_order, + &ewma_state, object::id_from_address(@0x0), test.ctx(), ); - assert_eq(settled, balances::new(0, 200 * constants::usdc_unit(), 0)); - assert_eq(owed, balances::new(200 * constants::sui_unit(), 0, 200_000_000)); + assert_eq!(settled, balances::new(0, 200 * constants::usdc_unit(), 0)); + assert_eq!(owed, balances::new(200 * constants::sui_unit(), 0, 200_000_000)); // even though bob has 200 volume, since he doesn't have 200 stake, he // doesn't get reduced fees @@ -506,11 +529,12 @@ fun process_create_after_raising_steak_req_ok() { taker_order.match_maker(&mut order, 0); let (settled, owed) = state.process_create( &mut taker_order, + &ewma_state, object::id_from_address(@0x0), test.ctx(), ); - assert_eq(settled, balances::new(0, 200 * constants::usdc_unit(), 0)); - assert_eq(owed, balances::new(200 * constants::sui_unit(), 0, 200_000_000)); + assert_eq!(settled, balances::new(0, 200 * constants::usdc_unit(), 0)); + assert_eq!(owed, balances::new(200 * constants::sui_unit(), 0, 200_000_000)); destroy(state); test.end(); @@ -524,8 +548,9 @@ fun process_create_after_lowering_steak_req_ok() { test.next_tx(ALICE); // alice and bob stake 50 DEEP each // default stake required is 100 + let whitelisted = false; let stable_pool = false; - let mut state = state::empty(stable_pool, test.ctx()); + let mut state = state::empty(whitelisted, stable_pool, test.ctx()); state.process_stake( id_from_address(POOL_ID), id_from_address(ALICE), @@ -553,8 +578,10 @@ fun process_create_after_lowering_steak_req_ok() { true, test.ctx().epoch(), ); + let ewma_state = test_init_ewma_state(test.ctx()); state.process_create( &mut order_info, + &ewma_state, object::id_from_address(@0x0), test.ctx(), ); @@ -572,13 +599,14 @@ fun process_create_after_lowering_steak_req_ok() { taker_order.match_maker(&mut order, 0); let (settled, owed) = state.process_create( &mut taker_order, + &ewma_state, object::id_from_address(@0x0), test.ctx(), ); // bob's first order // pays 1 SUI for the trade along with 0.001 DEEP in fees to receive 1 USDC - assert_eq(settled, balances::new(0, 50 * constants::usdc_unit(), 0)); - assert_eq(owed, balances::new(50 * constants::sui_unit(), 0, 50_000_000)); + assert_eq!(settled, balances::new(0, 50 * constants::usdc_unit(), 0)); + assert_eq!(owed, balances::new(50 * constants::sui_unit(), 0, 50_000_000)); // bob's second order, still no reduced fees test.next_tx(BOB); @@ -594,11 +622,12 @@ fun process_create_after_lowering_steak_req_ok() { taker_order.match_maker(&mut order, 0); let (settled, owed) = state.process_create( &mut taker_order, + &ewma_state, object::id_from_address(@0x0), test.ctx(), ); - assert_eq(settled, balances::new(0, 50 * constants::usdc_unit(), 0)); - assert_eq(owed, balances::new(50 * constants::sui_unit(), 0, 50_000_000)); + assert_eq!(settled, balances::new(0, 50 * constants::usdc_unit(), 0)); + assert_eq!(owed, balances::new(50 * constants::sui_unit(), 0, 50_000_000)); // bob's third order, still no reduced fees test.next_tx(BOB); @@ -614,11 +643,12 @@ fun process_create_after_lowering_steak_req_ok() { taker_order.match_maker(&mut order, 0); let (settled, owed) = state.process_create( &mut taker_order, + &ewma_state, object::id_from_address(@0x0), test.ctx(), ); - assert_eq(settled, balances::new(0, 50 * constants::usdc_unit(), 0)); - assert_eq(owed, balances::new(50 * constants::sui_unit(), 0, 50_000_000)); + assert_eq!(settled, balances::new(0, 50 * constants::usdc_unit(), 0)); + assert_eq!(owed, balances::new(50 * constants::sui_unit(), 0, 50_000_000)); // alice makes a proposal to lower the stake required to 50 and votes for it test.next_tx(ALICE); @@ -648,11 +678,12 @@ fun process_create_after_lowering_steak_req_ok() { taker_order.match_maker(&mut order, 0); let (settled, owed) = state.process_create( &mut taker_order, + &ewma_state, object::id_from_address(@0x0), test.ctx(), ); - assert_eq(settled, balances::new(0, 50 * constants::usdc_unit(), 0)); - assert_eq(owed, balances::new(50 * constants::sui_unit(), 0, 50_000_000)); + assert_eq!(settled, balances::new(0, 50 * constants::usdc_unit(), 0)); + assert_eq!(owed, balances::new(50 * constants::sui_unit(), 0, 50_000_000)); // bob is now over 50 volume and has the necessary stake, his taker fee is // reduced @@ -669,11 +700,12 @@ fun process_create_after_lowering_steak_req_ok() { taker_order.match_maker(&mut order, 0); let (settled, owed) = state.process_create( &mut taker_order, + &ewma_state, object::id_from_address(@0x0), test.ctx(), ); - assert_eq(settled, balances::new(0, 50 * constants::usdc_unit(), 0)); - assert_eq(owed, balances::new(50 * constants::sui_unit(), 0, 25_000_000)); + assert_eq!(settled, balances::new(0, 50 * constants::usdc_unit(), 0)); + assert_eq!(owed, balances::new(50 * constants::sui_unit(), 0, 25_000_000)); destroy(state); test.end(); @@ -693,18 +725,21 @@ fun process_cancel_ok() { true, test.ctx().epoch(), ); + let ewma_state = test_init_ewma_state(test.ctx()); + let whitelisted = false; let stable_pool = false; - let mut state = state::empty(stable_pool, test.ctx()); + let mut state = state::empty(whitelisted, stable_pool, test.ctx()); let (settled, owed) = state.process_create( &mut order_info, + &ewma_state, object::id_from_address(@0x0), test.ctx(), ); - assert_eq(settled, balances::new(0, 0, 0)); + assert_eq!(settled, balances::new(0, 0, 0)); // 10 * 10 = 100 // 10 * 0.0005 = 0.005 - assert_eq(owed, balances::new(0, 100 * constants::usdc_unit(), 5_000_000)); + assert_eq!(owed, balances::new(0, 100 * constants::usdc_unit(), 5_000_000)); let (settled, owed) = state.process_cancel( &mut order_info.to_order(), @@ -712,11 +747,8 @@ fun process_cancel_ok() { object::id_from_address(@0x0), test.ctx(), ); - assert_eq( - settled, - balances::new(0, 100 * constants::usdc_unit(), 5_000_000), - ); - assert_eq(owed, balances::new(0, 0, 0)); + assert_eq!(settled, balances::new(0, 100 * constants::usdc_unit(), 5_000_000)); + assert_eq!(owed, balances::new(0, 0, 0)); destroy(state); test.end(); @@ -737,10 +769,13 @@ fun process_cancel_after_partial_ok() { true, test.ctx().epoch(), ); + let ewma_state = test_init_ewma_state(test.ctx()); + let whitelisted = false; let stable_pool = false; - let mut state = state::empty(stable_pool, test.ctx()); + let mut state = state::empty(whitelisted, stable_pool, test.ctx()); state.process_create( &mut order_info, + &ewma_state, object::id_from_address(@0x0), test.ctx(), ); @@ -759,6 +794,7 @@ fun process_cancel_after_partial_ok() { taker_order.match_maker(&mut order, 0); state.process_create( &mut taker_order, + &ewma_state, object::id_from_address(@0x0), test.ctx(), ); @@ -772,7 +808,7 @@ fun process_cancel_after_partial_ok() { ); // paid 100 USDC to buy 10 SUI. 1 SUI filled. // returns 90 USDC and 1 SUI, along with 4_500_000 in DEEP - assert_eq( + assert_eq!( settled, balances::new( 1 * constants::sui_unit(), @@ -780,7 +816,7 @@ fun process_cancel_after_partial_ok() { 4_500_000, ), ); - assert_eq(owed, balances::new(0, 0, 0)); + assert_eq!(owed, balances::new(0, 0, 0)); destroy(state); test.end(); @@ -793,8 +829,9 @@ fun process_cancel_after_modify_epoch_change_ok() { test.next_tx(ALICE); // stake 100 DEEP + let whitelisted = false; let stable_pool = false; - let mut state = state::empty(stable_pool, test.ctx()); + let mut state = state::empty(whitelisted, stable_pool, test.ctx()); state.process_stake( id_from_address(POOL_ID), id_from_address(ALICE), @@ -812,8 +849,10 @@ fun process_cancel_after_modify_epoch_change_ok() { true, test.ctx().epoch(), ); + let ewma_state = test_init_ewma_state(test.ctx()); state.process_create( &mut order_info, + &ewma_state, object::id_from_address(@0x0), test.ctx(), ); @@ -844,11 +883,8 @@ fun process_cancel_after_modify_epoch_change_ok() { test.ctx(), ); // reduces quantity from 10 to 5. Get refund of 50 USDC and half of the fees - assert_eq( - settled, - balances::new(0, 50 * constants::usdc_unit(), 2_500_000), - ); - assert_eq(owed, balances::new(0, 0, 0)); + assert_eq!(settled, balances::new(0, 50 * constants::usdc_unit(), 2_500_000)); + assert_eq!(owed, balances::new(0, 0, 0)); test.next_tx(ALICE); // regardless of the fee change, when canceling the remaining amount, get @@ -859,11 +895,8 @@ fun process_cancel_after_modify_epoch_change_ok() { object::id_from_address(@0x0), test.ctx(), ); - assert_eq( - settled, - balances::new(0, 50 * constants::usdc_unit(), 2_500_000), - ); - assert_eq(owed, balances::new(0, 0, 0)); + assert_eq!(settled, balances::new(0, 50 * constants::usdc_unit(), 2_500_000)); + assert_eq!(owed, balances::new(0, 0, 0)); destroy(state); test.end(); @@ -875,16 +908,17 @@ fun process_stake_ok() { let mut test = begin(OWNER); test.next_tx(ALICE); + let whitelisted = false; let stable_pool = false; - let mut state = state::empty(stable_pool, test.ctx()); + let mut state = state::empty(whitelisted, stable_pool, test.ctx()); let (settled, owed) = state.process_stake( id_from_address(POOL_ID), id_from_address(ALICE), 1 * constants::sui_unit(), test.ctx(), ); - assert_eq(settled, balances::new(0, 0, 0)); - assert_eq(owed, balances::new(0, 0, 1 * constants::sui_unit())); + assert_eq!(settled, balances::new(0, 0, 0)); + assert_eq!(owed, balances::new(0, 0, 1 * constants::sui_unit())); assert!(state.governance().voting_power() == 1_000_000_000, 0); state.process_stake( id_from_address(POOL_ID), @@ -899,16 +933,16 @@ fun process_stake_ok() { id_from_address(ALICE), test.ctx(), ); - assert_eq(settled, balances::new(0, 0, 1 * constants::sui_unit())); - assert_eq(owed, balances::new(0, 0, 0)); + assert_eq!(settled, balances::new(0, 0, 1 * constants::sui_unit())); + assert_eq!(owed, balances::new(0, 0, 0)); assert!(state.governance().voting_power() == 1_000_000_000, 0); let (settled, owed) = state.process_unstake( id_from_address(POOL_ID), id_from_address(BOB), test.ctx(), ); - assert_eq(settled, balances::new(0, 0, 1 * constants::sui_unit())); - assert_eq(owed, balances::new(0, 0, 0)); + assert_eq!(settled, balances::new(0, 0, 1 * constants::sui_unit())); + assert_eq!(owed, balances::new(0, 0, 0)); assert!(state.governance().voting_power() == 0, 0); destroy(state); @@ -921,8 +955,9 @@ fun process_proposal_no_stake_e() { let mut test = begin(OWNER); test.next_tx(ALICE); + let whitelisted = false; let stable_pool = false; - let mut state = state::empty(stable_pool, test.ctx()); + let mut state = state::empty(whitelisted, stable_pool, test.ctx()); state.process_proposal( id_from_address(POOL_ID), id_from_address(ALICE), @@ -941,8 +976,9 @@ fun process_proposal_no_stake_e2() { let mut test = begin(OWNER); test.next_tx(ALICE); + let whitelisted = false; let stable_pool = false; - let mut state = state::empty(stable_pool, test.ctx()); + let mut state = state::empty(whitelisted, stable_pool, test.ctx()); state.process_stake( id_from_address(POOL_ID), id_from_address(ALICE), @@ -966,8 +1002,9 @@ fun process_proposal_already_proposed_e() { let mut test = begin(OWNER); test.next_tx(ALICE); + let whitelisted = false; let stable_pool = false; - let mut state = state::empty(stable_pool, test.ctx()); + let mut state = state::empty(whitelisted, stable_pool, test.ctx()); state.process_stake( id_from_address(POOL_ID), id_from_address(ALICE), @@ -1002,8 +1039,9 @@ fun process_proposal_already_proposed_next_epoch_ok() { let mut test = begin(OWNER); test.next_tx(ALICE); + let whitelisted = false; let stable_pool = false; - let mut state = state::empty(stable_pool, test.ctx()); + let mut state = state::empty(whitelisted, stable_pool, test.ctx()); state.process_stake( id_from_address(POOL_ID), id_from_address(ALICE), @@ -1042,8 +1080,9 @@ fun process_proposal_vote_ok() { let mut test = begin(OWNER); test.next_tx(ALICE); + let whitelisted = false; let stable_pool = false; - let mut state = state::empty(stable_pool, test.ctx()); + let mut state = state::empty(whitelisted, stable_pool, test.ctx()); state.process_stake( id_from_address(POOL_ID), id_from_address(ALICE), diff --git a/packages/deepbook/tests/state/trade_params_tests.move b/packages/deepbook/tests/state/trade_params_tests.move new file mode 100644 index 000000000..f4578511f --- /dev/null +++ b/packages/deepbook/tests/state/trade_params_tests.move @@ -0,0 +1,201 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +#[test_only] +module deepbook::trade_params_tests; + +use deepbook::{constants, trade_params}; +use std::unit_test::assert_eq; + +#[test] +fun test_taker_fee_basic() { + let taker_fee = 1_000_000; // 0.1% + let maker_fee = 500_000; // 0.05% + let stake_required = 100 * constants::deep_unit(); + + let params = trade_params::new(taker_fee, maker_fee, stake_required); + + assert_eq!(params.taker_fee(), taker_fee); + assert_eq!(params.maker_fee(), maker_fee); + assert_eq!(params.stake_required(), stake_required); +} + +#[test] +fun test_taker_fee_for_user_no_stake_no_volume() { + let taker_fee = 2_000_000; // 0.2% + let maker_fee = 500_000; // 0.05% + let stake_required = 100 * constants::deep_unit(); + + let params = trade_params::new(taker_fee, maker_fee, stake_required); + + // User has no stake and no volume + let active_stake = 0; + let volume_in_deep = 0; + + let user_taker_fee = params.taker_fee_for_user(active_stake, volume_in_deep); + + // Should get full taker fee + assert_eq!(user_taker_fee, taker_fee); +} + +#[test] +fun test_taker_fee_for_user_has_stake_no_volume() { + let taker_fee = 2_000_000; // 0.2% + let maker_fee = 500_000; // 0.05% + let stake_required = 100 * constants::deep_unit(); + + let params = trade_params::new(taker_fee, maker_fee, stake_required); + + // User has stake but no volume + let active_stake = 150 * constants::deep_unit(); + let volume_in_deep = 0; + + let user_taker_fee = params.taker_fee_for_user(active_stake, volume_in_deep); + + // Should still get full taker fee (needs both stake AND volume) + assert_eq!(user_taker_fee, taker_fee); +} + +#[test] +fun test_taker_fee_for_user_has_volume_no_stake() { + let taker_fee = 2_000_000; // 0.2% + let maker_fee = 500_000; // 0.05% + let stake_required = 100 * constants::deep_unit(); + + let params = trade_params::new(taker_fee, maker_fee, stake_required); + + // User has volume but no stake + let active_stake = 0; + let volume_in_deep = 200 * (constants::deep_unit() as u128); + + let user_taker_fee = params.taker_fee_for_user(active_stake, volume_in_deep); + + // Should still get full taker fee (needs both stake AND volume) + assert_eq!(user_taker_fee, taker_fee); +} + +#[test] +fun test_taker_fee_for_user_has_both_stake_and_volume() { + let taker_fee = 2_000_000; // 0.2% + let maker_fee = 500_000; // 0.05% + let stake_required = 100 * constants::deep_unit(); + + let params = trade_params::new(taker_fee, maker_fee, stake_required); + + // User has both stake and volume that meet requirements + let active_stake = 150 * constants::deep_unit(); + let volume_in_deep = 200 * (constants::deep_unit() as u128); + + let user_taker_fee = params.taker_fee_for_user(active_stake, volume_in_deep); + + // Should get reduced taker fee (halved) + assert_eq!(user_taker_fee, taker_fee / 2); +} + +#[test] +fun test_taker_fee_for_user_exactly_at_threshold() { + let taker_fee = 1_000_000; // 0.1% + let maker_fee = 500_000; // 0.05% + let stake_required = 100 * constants::deep_unit(); + + let params = trade_params::new(taker_fee, maker_fee, stake_required); + + // User has exactly the required stake and volume + let active_stake = 100 * constants::deep_unit(); + let volume_in_deep = 100 * (constants::deep_unit() as u128); + + let user_taker_fee = params.taker_fee_for_user(active_stake, volume_in_deep); + + // Should get reduced taker fee (halved) + assert_eq!(user_taker_fee, taker_fee / 2); +} + +#[test] +fun test_taker_fee_for_user_stake_just_below_threshold() { + let taker_fee = 1_000_000; // 0.1% + let maker_fee = 500_000; // 0.05% + let stake_required = 100 * constants::deep_unit(); + + let params = trade_params::new(taker_fee, maker_fee, stake_required); + + // User has stake just below threshold but volume above + let active_stake = 99 * constants::deep_unit(); + let volume_in_deep = 200 * (constants::deep_unit() as u128); + + let user_taker_fee = params.taker_fee_for_user(active_stake, volume_in_deep); + + // Should get full taker fee + assert_eq!(user_taker_fee, taker_fee); +} + +#[test] +fun test_taker_fee_for_user_volume_just_below_threshold() { + let taker_fee = 1_000_000; // 0.1% + let maker_fee = 500_000; // 0.05% + let stake_required = 100 * constants::deep_unit(); + + let params = trade_params::new(taker_fee, maker_fee, stake_required); + + // User has volume just below threshold but stake above + let active_stake = 200 * constants::deep_unit(); + let volume_in_deep = 99 * (constants::deep_unit() as u128); + + let user_taker_fee = params.taker_fee_for_user(active_stake, volume_in_deep); + + // Should get full taker fee + assert_eq!(user_taker_fee, taker_fee); +} + +#[test] +fun test_taker_fee_for_user_with_high_stake_requirement() { + let taker_fee = 4_000_000; // 0.4% + let maker_fee = 500_000; // 0.05% + let stake_required = 10_000 * constants::deep_unit(); // 10,000 DEEP + + let params = trade_params::new(taker_fee, maker_fee, stake_required); + + // User has high stake and volume + let active_stake = 15_000 * constants::deep_unit(); + let volume_in_deep = 20_000 * (constants::deep_unit() as u128); + + let user_taker_fee = params.taker_fee_for_user(active_stake, volume_in_deep); + + // Should get reduced taker fee (halved) + assert_eq!(user_taker_fee, taker_fee / 2); +} + +#[test] +fun test_taker_fee_for_user_odd_taker_fee() { + let taker_fee = 3_500_000; // 0.35% + let maker_fee = 500_000; // 0.05% + let stake_required = 50 * constants::deep_unit(); + + let params = trade_params::new(taker_fee, maker_fee, stake_required); + + // User qualifies for reduced fee + let active_stake = 100 * constants::deep_unit(); + let volume_in_deep = 100 * (constants::deep_unit() as u128); + + let user_taker_fee = params.taker_fee_for_user(active_stake, volume_in_deep); + + // Should get reduced taker fee (halved) + assert_eq!(user_taker_fee, 1_750_000); +} + +#[test] +fun test_taker_fee_for_user_zero_stake_requirement() { + let taker_fee = 1_000_000; // 0.1% + let maker_fee = 500_000; // 0.05% + let stake_required = 0; // No stake required + + let params = trade_params::new(taker_fee, maker_fee, stake_required); + + // User has no stake but has volume + let active_stake = 0; + let volume_in_deep = 100 * (constants::deep_unit() as u128); + + let user_taker_fee = params.taker_fee_for_user(active_stake, volume_in_deep); + + // Should get reduced taker fee since stake_required is 0 + assert_eq!(user_taker_fee, taker_fee / 2); +} diff --git a/packages/deepbook/tests/vault/vault_tests.move b/packages/deepbook/tests/vault/vault_tests.move index 09be9426f..5256a1592 100644 --- a/packages/deepbook/tests/vault/vault_tests.move +++ b/packages/deepbook/tests/vault/vault_tests.move @@ -11,7 +11,8 @@ use deepbook::{ constants, vault }; -use sui::{object::id_from_address, test_scenario::{next_tx, begin, end}, test_utils::destroy}; +use std::unit_test::destroy; +use sui::{object::id_from_address, test_scenario::{next_tx, begin, end}}; const OWNER: address = @0xF; const ALICE: address = @0xA; diff --git a/packages/deepbook_margin/Move.lock b/packages/deepbook_margin/Move.lock new file mode 100644 index 000000000..ba2a734d7 --- /dev/null +++ b/packages/deepbook_margin/Move.lock @@ -0,0 +1,113 @@ +# Generated by move; do not edit +# This file should be checked in. + +[move] +version = 4 + +[pinned.mainnet.MoveStdlib] +source = { git = "https://github.com/MystenLabs/sui.git", subdir = "crates/sui-framework/packages/move-stdlib", rev = "22f9fc9781732d651e18384c9a8eb1dabddf73a6" } +use_environment = "mainnet" +manifest_digest = "C4FE4C91DE74CBF223B2E380AE40F592177D21870DC2D7EB6227D2D694E05363" +deps = {} + +[pinned.mainnet.MoveStdlib_1] +source = { git = "https://github.com/MystenLabs/sui.git", subdir = "crates/sui-framework/packages/move-stdlib", rev = "041c5f2bae2fe52079e44b70514333532d69f4e6" } +use_environment = "mainnet" +manifest_digest = "C4FE4C91DE74CBF223B2E380AE40F592177D21870DC2D7EB6227D2D694E05363" +deps = {} + +[pinned.mainnet.Pyth] +source = { git = "https://github.com/pyth-network/pyth-crosschain.git", subdir = "target_chains/sui/contracts", rev = "3bd1262dcba9518a6901aa6a15f04072799bfb37" } +use_environment = "mainnet" +manifest_digest = "F2C5AF85C4B72C8F6A8132D05DE4F787F4EB80DBFF812331ED65AE599E7DF92A" +deps = { Sui = "Sui_1", Wormhole = "Wormhole" } + +[pinned.mainnet.Sui] +source = { git = "https://github.com/MystenLabs/sui.git", subdir = "crates/sui-framework/packages/sui-framework", rev = "22f9fc9781732d651e18384c9a8eb1dabddf73a6" } +use_environment = "mainnet" +manifest_digest = "CD547CB1ACCE0880C835DAED2D8FFCB91D56C833AE5240D3AA5B918398263195" +deps = { MoveStdlib = "MoveStdlib" } + +[pinned.mainnet.Sui_1] +source = { git = "https://github.com/MystenLabs/sui.git", subdir = "crates/sui-framework/packages/sui-framework", rev = "041c5f2bae2fe52079e44b70514333532d69f4e6" } +use_environment = "mainnet" +manifest_digest = "CD547CB1ACCE0880C835DAED2D8FFCB91D56C833AE5240D3AA5B918398263195" +deps = { MoveStdlib = "MoveStdlib_1" } + +[pinned.mainnet.Wormhole] +source = { git = "https://github.com/wormhole-foundation/wormhole.git", subdir = "sui/wormhole", rev = "b71be5cbb9537c4aac8e23e74371affa3825efcd" } +use_environment = "mainnet" +manifest_digest = "0D766A0380B75080707CFA6099AF469A2E502B0D9DC334A9E9983B391C5555D9" +deps = { Sui = "Sui_1" } + +[pinned.mainnet.deepbook] +source = { local = "../deepbook" } +use_environment = "mainnet" +manifest_digest = "F4948AC65D214ECC0561B7E94987B2AF2D7BF78658F6AE5CA5D0E1DA68873872" +deps = { std = "MoveStdlib", sui = "Sui", token = "token" } + +[pinned.mainnet.deepbook_margin] +source = { root = true } +use_environment = "mainnet" +manifest_digest = "8EEB68B2BFCFE40D54C0A445414D1789D77E8CC373F7AF15A97B01ADE4B29199" +deps = { Pyth = "Pyth", deepbook = "deepbook", std = "MoveStdlib", sui = "Sui", token = "token" } + +[pinned.mainnet.token] +source = { git = "https://github.com/MystenLabs/deepbookv3.git", subdir = "packages/token", rev = "87b5423bb0c08e5fcf9c4ed8cade1d2904a8dae9" } +use_environment = "mainnet" +manifest_digest = "E41BBD67BE8940D26C79D78B028477EF5B33BA217A1282C78ACB344CF8A5ECF6" +deps = { std = "MoveStdlib", sui = "Sui" } + +[pinned.testnet.MoveStdlib] +source = { git = "https://github.com/MystenLabs/sui.git", subdir = "crates/sui-framework/packages/move-stdlib", rev = "22f9fc9781732d651e18384c9a8eb1dabddf73a6" } +use_environment = "testnet" +manifest_digest = "C4FE4C91DE74CBF223B2E380AE40F592177D21870DC2D7EB6227D2D694E05363" +deps = {} + +[pinned.testnet.MoveStdlib_1] +source = { git = "https://github.com/MystenLabs/sui.git", subdir = "crates/sui-framework/packages/move-stdlib", rev = "041c5f2bae2fe52079e44b70514333532d69f4e6" } +use_environment = "testnet" +manifest_digest = "C4FE4C91DE74CBF223B2E380AE40F592177D21870DC2D7EB6227D2D694E05363" +deps = {} + +[pinned.testnet.Pyth] +source = { git = "https://github.com/pyth-network/pyth-crosschain.git", subdir = "target_chains/sui/contracts", rev = "3bd1262dcba9518a6901aa6a15f04072799bfb37" } +use_environment = "testnet" +manifest_digest = "9942AAB3725BC51DADB046B4440A3D008E554C9286ECC72B3DE5CC0309C0D63D" +deps = { Sui = "Sui_1", Wormhole = "Wormhole" } + +[pinned.testnet.Sui] +source = { git = "https://github.com/MystenLabs/sui.git", subdir = "crates/sui-framework/packages/sui-framework", rev = "22f9fc9781732d651e18384c9a8eb1dabddf73a6" } +use_environment = "testnet" +manifest_digest = "7AFB66695545775FBFBB2D3078ADFD084244D5002392E837FDE21D9EA1C6D01C" +deps = { MoveStdlib = "MoveStdlib" } + +[pinned.testnet.Sui_1] +source = { git = "https://github.com/MystenLabs/sui.git", subdir = "crates/sui-framework/packages/sui-framework", rev = "041c5f2bae2fe52079e44b70514333532d69f4e6" } +use_environment = "testnet" +manifest_digest = "7AFB66695545775FBFBB2D3078ADFD084244D5002392E837FDE21D9EA1C6D01C" +deps = { MoveStdlib = "MoveStdlib_1" } + +[pinned.testnet.Wormhole] +source = { git = "https://github.com/wormhole-foundation/wormhole.git", subdir = "sui/wormhole", rev = "b71be5cbb9537c4aac8e23e74371affa3825efcd" } +use_environment = "testnet" +manifest_digest = "6996F1AB8CD448FFBA142C085C488FE8A46B485E61E38A1891EFA52B30D9C12E" +deps = { Sui = "Sui_1" } + +[pinned.testnet.deepbook] +source = { local = "../deepbook" } +use_environment = "testnet" +manifest_digest = "3101923B9428545A4F52FFAD1C4F959F9BFFF84CD09CE4BCC1CB831286999B5A" +deps = { std = "MoveStdlib", sui = "Sui", token = "token" } + +[pinned.testnet.deepbook_margin] +source = { root = true } +use_environment = "testnet" +manifest_digest = "E6B112DF6F98D4596806AD90F385DD4D9DA82C556A2DF58997D5FEB00F983244" +deps = { Pyth = "Pyth", deepbook = "deepbook", std = "MoveStdlib", sui = "Sui", token = "token" } + +[pinned.testnet.token] +source = { git = "https://github.com/MystenLabs/deepbookv3.git", subdir = "packages/token", rev = "87b5423bb0c08e5fcf9c4ed8cade1d2904a8dae9" } +use_environment = "testnet" +manifest_digest = "5745706258F61D6CE210904B3E6AE87A73CE9D31A6F93BE4718C442529332A87" +deps = { std = "MoveStdlib", sui = "Sui" } diff --git a/packages/deepbook_margin/Move.toml b/packages/deepbook_margin/Move.toml new file mode 100644 index 000000000..f77bf84b1 --- /dev/null +++ b/packages/deepbook_margin/Move.toml @@ -0,0 +1,17 @@ +[package] +name = "deepbook_margin" +edition = "2024.alpha" +version = "0.0.1" + +[dependencies] +token = { git = "https://github.com/MystenLabs/deepbookv3.git", subdir = "packages/token", rev = "main"} +deepbook = { local = "../deepbook" } + +# Pyth dependency +Pyth = { git = "https://github.com/pyth-network/pyth-crosschain.git", subdir = "target_chains/sui/contracts", rev = "sui-contract-mainnet" } + +[dep-replacements.testnet] +pyth = { git = "https://github.com/pyth-network/pyth-crosschain.git", subdir = "target_chains/sui/contracts", rev = "sui-contract-testnet" } + +[addresses] +deepbook_margin = "0x0" diff --git a/packages/deepbook_margin/Published.toml b/packages/deepbook_margin/Published.toml new file mode 100644 index 000000000..c062e9662 --- /dev/null +++ b/packages/deepbook_margin/Published.toml @@ -0,0 +1,18 @@ +# Generated by Move +# This file contains metadata about published versions of this package in different environments +# This file SHOULD be committed to source control + +[published.mainnet] +chain-id = "35834a8a" +published-at = "0x97d9473771b01f77b0940c589484184b49f6444627ec121314fae6a6d36fb86b" +original-id = "0x97d9473771b01f77b0940c589484184b49f6444627ec121314fae6a6d36fb86b" +version = 1 +toolchain-version = "1.63.1" +build-config = { flavor = "sui", edition = "2024" } +upgrade-capability = "0xd57c7a41b31c0a1fab5e71d296a75daf2e9e09945df383d4745daa49f06d9c56" + +[published.testnet] +chain-id = "4c78adac" +published-at = "0xd6a42f4df4db73d68cbeb52be66698d2fe6a9464f45ad113ca52b0c6ebd918b6" +original-id = "0xb8620c24c9ea1a4a41e79613d2b3d1d93648d1bb6f6b789a7c8f261c94110e4b" +version = 13 diff --git a/packages/deepbook_margin/README.md b/packages/deepbook_margin/README.md new file mode 100644 index 000000000..1c2877ad8 --- /dev/null +++ b/packages/deepbook_margin/README.md @@ -0,0 +1,64 @@ +# DeepBook Margin + +DeepBook Margin is a decentralized margin trading protocol built on top of DeepBookV3 on Sui. It +enables leveraged trading by allowing users to borrow assets against their collateral, execute +margin orders on DeepBook's central limit order book, and manage risk with advanced features like +take profit and stop loss conditional orders. The protocol includes a lending pool where +suppliers can earn interest by providing liquidity for margin traders to borrow. + +## DeepBook Margin Information + +- [Package and Pools](https://docs.google.com/document/d/1uK4MNqYa0LdhVqBD4KqOcWG1N1nNNe3JwbeUZc1kH1I) +- [Contract Documentation](https://docs.sui.io/standards/deepbook-margin) +- [SDK Documentation](https://docs.sui.io/standards/deepbook-margin-sdk) +- [Example SDK Usage](https://github.com/MystenLabs/ts-sdks/tree/main/packages/deepbook-v3/examples) + +## Margin Manager + +The `MarginManager` is a shared object that represents a single margin trading account. It holds +collateral balances, tracks borrowed positions, and interfaces with both DeepBookV3 for trading +and the MarginPool for borrowing. Each MarginManager is linked to a specific DeepBook pool and +its corresponding MarginPool. + +Users can deposit collateral (base, quote, or DEEP tokens), borrow assets to increase their +trading position, and repay loans. The MarginManager tracks the user's risk ratio, which +determines liquidation eligibility. If a position becomes undercollateralized, it can be +liquidated by any party. + +## Margin Pool + +The `MarginPool` is a lending pool that provides liquidity for margin traders. It consists of: + +1. **Supply Side** - Users can supply assets to earn interest from borrowers. Suppliers receive + shares representing their portion of the pool. +2. **Borrow Side** - Margin traders borrow from the pool to leverage their positions. Interest + accrues based on utilization rate. +3. **Interest Model** - Dynamic interest rates adjust based on pool utilization to balance supply + and demand. + +Suppliers mint a `SupplierCap` to track their deposits and can withdraw their supplied assets +plus accrued interest at any time, subject to available liquidity. + +## Take Profit / Stop Loss (TPSL) + +DeepBook Margin supports conditional orders that automatically execute when price conditions are +met: + +- **Take Profit** - Automatically close a position when the price reaches a favorable target +- **Stop Loss** - Automatically close a position to limit losses when price moves against you + +Conditional orders are stored on-chain and can be executed by anyone (permissionlessly) once the +trigger price is reached. This enables automated risk management without requiring users to +monitor positions constantly. + +## Liquidation + +When a MarginManager's risk ratio exceeds the maximum threshold (position becomes +undercollateralized), it becomes eligible for liquidation. Any user can call the liquidate +function to: + +1. Repay the outstanding debt +2. Receive the collateral plus a liquidation bonus + +This mechanism ensures the protocol remains solvent and incentivizes liquidators to maintain +system health. diff --git a/packages/deepbook_margin/sources/helper/margin_constants.move b/packages/deepbook_margin/sources/helper/margin_constants.move new file mode 100644 index 000000000..310f7153d --- /dev/null +++ b/packages/deepbook_margin/sources/helper/margin_constants.move @@ -0,0 +1,85 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +module deepbook_margin::margin_constants; + +const MARGIN_VERSION: u64 = 1; +const MAX_RISK_RATIO: u64 = 1_000 * 1_000_000_000; // Risk ratio above 1000 will be considered as 1000 +const DEFAULT_USER_LIQUIDATION_REWARD: u64 = 10_000_000; // 1% +const DEFAULT_POOL_LIQUIDATION_REWARD: u64 = 40_000_000; // 4% +const MIN_LEVERAGE: u64 = 1_000_000_000; // 1x +const MAX_LEVERAGE: u64 = 20_000_000_000; // 20x +const YEAR_MS: u64 = 365 * 24 * 60 * 60 * 1000; +const DAY_MS: u64 = 24 * 60 * 60 * 1000; +const MIN_MIN_BORROW: u64 = 1000; +const MAX_MARGIN_MANAGERS: u64 = 100; +const DEFAULT_REFERRAL: address = @0x0; +const MAX_PROTOCOL_SPREAD: u64 = 200_000_000; // 20% +const MIN_LIQUIDATION_REPAY: u64 = 1000; +const MAX_CONF_BPS: u64 = 10_000; // 100% - maximum allowed confidence interval +const MAX_EWMA_DIFFERENCE_BPS: u64 = 10_000; // 100% - maximum allowed EWMA price difference +const MAX_CONDITIONAL_ORDERS: u64 = 10; + +public fun margin_version(): u64 { + MARGIN_VERSION +} + +public fun max_risk_ratio(): u64 { + MAX_RISK_RATIO +} + +public fun default_user_liquidation_reward(): u64 { + DEFAULT_USER_LIQUIDATION_REWARD +} + +public fun default_pool_liquidation_reward(): u64 { + DEFAULT_POOL_LIQUIDATION_REWARD +} + +public fun min_leverage(): u64 { + MIN_LEVERAGE +} + +public fun max_leverage(): u64 { + MAX_LEVERAGE +} + +public fun year_ms(): u64 { + YEAR_MS +} + +public fun min_min_borrow(): u64 { + MIN_MIN_BORROW +} + +public fun max_margin_managers(): u64 { + MAX_MARGIN_MANAGERS +} + +public fun default_referral(): ID { + DEFAULT_REFERRAL.to_id() +} + +public fun max_protocol_spread(): u64 { + MAX_PROTOCOL_SPREAD +} + +public fun min_liquidation_repay(): u64 { + MIN_LIQUIDATION_REPAY +} + +public fun max_conf_bps(): u64 { + MAX_CONF_BPS +} + +public fun max_ewma_difference_bps(): u64 { + MAX_EWMA_DIFFERENCE_BPS +} + +public fun max_conditional_orders(): u64 { + MAX_CONDITIONAL_ORDERS +} + +public fun day_ms(): u64 { + DAY_MS +} diff --git a/packages/deepbook_margin/sources/helper/oracle.move b/packages/deepbook_margin/sources/helper/oracle.move new file mode 100644 index 000000000..4a2ffb479 --- /dev/null +++ b/packages/deepbook_margin/sources/helper/oracle.move @@ -0,0 +1,366 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +/// Oracle module for margin trading. +module deepbook_margin::oracle; + +use deepbook::{constants, math}; +use deepbook_margin::{margin_constants, margin_registry::MarginRegistry}; +use pyth::{price_info::PriceInfoObject, pyth}; +use std::type_name::{Self, TypeName}; +use sui::{clock::Clock, coin_registry::Currency, vec_map::{Self, VecMap}}; + +use fun get_config_for_type as MarginRegistry.get_config_for_type; + +const EInvalidPythPrice: u64 = 1; +const ECurrencyNotSupported: u64 = 2; +const EPriceFeedIdMismatch: u64 = 3; +const EInvalidPythPriceConf: u64 = 4; +const EInvalidOracleConfig: u64 = 5; +const EInvalidPrice: u64 = 6; + +/// A buffer added to the exponent when doing currency conversions. +const BUFFER: u8 = 10; + +/// Holds a VecMap that determines the configuration for each currency. +public struct PythConfig has drop, store { + currencies: VecMap, + max_age_secs: u64, // max age tolerance for pyth prices in seconds +} + +/// Find price feed IDs here https://www.pyth.network/developers/price-feed-ids +public struct CoinTypeData has copy, drop, store { + decimals: u8, + price_feed_id: vector, // Make sure to omit the `0x` prefix. + type_name: TypeName, + max_conf_bps: u64, // max confidence interval tolerance + max_ewma_difference_bps: u64, // max difference between pyth price and ema price in bps +} + +public struct ConversionConfig has copy, drop { + target_decimals: u8, + base_decimals: u8, + pyth_price: u64, + pyth_decimals: u8, +} + +/// Creates a new CoinTypeData struct of type T. +/// Uses Currency to avoid any errors in decimals. +public fun new_coin_type_data_from_currency( + currency: &Currency, + price_feed_id: vector, + max_conf_bps: u64, + max_ewma_difference_bps: u64, +): CoinTypeData { + // Validate oracle configuration parameters + assert!(max_conf_bps <= margin_constants::max_conf_bps(), EInvalidOracleConfig); + assert!( + max_ewma_difference_bps <= margin_constants::max_ewma_difference_bps(), + EInvalidOracleConfig, + ); + + let type_name = type_name::with_defining_ids(); + CoinTypeData { + decimals: currency.decimals(), + price_feed_id, + type_name, + max_conf_bps, + max_ewma_difference_bps, + } +} + +/// Creates a new PythConfig struct. +/// Can be attached by the Admin to MarginRegistry to allow oracle to work. +public fun new_pyth_config(setups: vector, max_age_secs: u64): PythConfig { + let mut currencies: VecMap = vec_map::empty(); + + setups.do!(|coin_type| { + currencies.insert(coin_type.type_name, coin_type); + }); + + PythConfig { + currencies, + max_age_secs, + } +} + +/// Calculates the USD price of a given asset or debt amount. +/// 9 decimals are used for USD representation. +public(package) fun calculate_usd_price( + price_info_object: &PriceInfoObject, + registry: &MarginRegistry, + amount: u64, + clock: &Clock, +): u64 { + let config = price_config( + price_info_object, + registry, + true, + clock, + ); + + config.calculate_usd_currency_amount( + amount, + ) +} + +public(package) fun calculate_usd_currency_amount( + config: ConversionConfig, + base_currency_amount: u64, +): u64 { + assert!(config.pyth_price > 0, EInvalidPythPrice); + let exponent_with_buffer = BUFFER + config.base_decimals - config.target_decimals; + + let target_currency_amount = + ( + ((base_currency_amount as u128) * (config.pyth_price as u128)).divide_and_round_up( + 10u128.pow( + config.pyth_decimals, + )) * (10u128.pow(BUFFER)), + ).divide_and_round_up(10u128.pow( + exponent_with_buffer, + )) as u64; + + target_currency_amount +} + +/// Calculates the price of BaseAsset in QuoteAsset. +/// Returns the price accounting for the decimal difference between the two assets. +public(package) fun calculate_price( + registry: &MarginRegistry, + base_price_info_object: &PriceInfoObject, + quote_price_info_object: &PriceInfoObject, + clock: &Clock, +): u64 { + let base_decimals = get_decimals(registry); + let quote_decimals = get_decimals(registry); + + let base_amount = 10u64.pow(base_decimals); + let base_usd_price = calculate_usd_price( + base_price_info_object, + registry, + base_amount, + clock, + ); + + let quote_amount = 10u64.pow(quote_decimals); + let quote_usd_price = calculate_usd_price( + quote_price_info_object, + registry, + quote_amount, + clock, + ); + let price_ratio = math::div(base_usd_price, quote_usd_price); + + if (base_decimals > quote_decimals) { + let decimal_diff = base_decimals - quote_decimals; + let divisor = 10u128.pow(decimal_diff); + let price = (price_ratio as u128) / divisor; + assert!(price <= constants::max_price() as u128, EInvalidPrice); + + price as u64 + } else if (quote_decimals > base_decimals) { + let decimal_diff = quote_decimals - base_decimals; + let multiplier = 10u128.pow(decimal_diff); + let price = (price_ratio as u128) * multiplier; + assert!(price <= constants::max_price() as u128, EInvalidPrice); + + price as u64 + } else { + price_ratio + } +} + +/// Calculates the amount in target currency based on amount in asset A. +public(package) fun calculate_target_currency( + registry: &MarginRegistry, + price_info_object_a: &PriceInfoObject, + price_info_object_b: &PriceInfoObject, + amount: u64, + clock: &Clock, +): u64 { + let usd_value = calculate_usd_price( + price_info_object_a, + registry, + amount, + clock, + ); + let target_value = calculate_target_amount( + price_info_object_b, + registry, + usd_value, + clock, + ); + + target_value +} + +/// Calculates the amount in target currency based on usd amount +public(package) fun calculate_target_amount( + price_info_object: &PriceInfoObject, + registry: &MarginRegistry, + usd_amount: u64, + clock: &Clock, +): u64 { + let config = price_config( + price_info_object, + registry, + false, + clock, + ); + + calculate_target_currency_amount( + config, + usd_amount, + ) +} + +public(package) fun calculate_target_currency_amount( + config: ConversionConfig, + base_currency_amount: u64, +): u64 { + assert!(config.pyth_price > 0, EInvalidPythPrice); + + // We use a buffer in the edge case where target_decimals + pyth_decimals < + // base_decimals + let exponent_with_buffer = + BUFFER + config.target_decimals + config.pyth_decimals - config.base_decimals; + + // We cast to u128 to avoid overflow, which is very likely with the buffer + let target_currency_amount = + (base_currency_amount as u128 * 10u128.pow(exponent_with_buffer)) + .divide_and_round_up(config.pyth_price as u128) + .divide_and_round_up(10u128.pow(BUFFER)) as u64; + + target_currency_amount +} + +fun price_config( + price_info_object: &PriceInfoObject, + registry: &MarginRegistry, + is_usd_price_config: bool, + clock: &Clock, +): ConversionConfig { + let (pyth_price, pyth_decimals, pyth_conf, type_config) = get_validated_pyth_price( + price_info_object, + registry, + clock, + ); + + assert!( + (pyth_conf as u128) * 10_000 <= (type_config.max_conf_bps as u128) * (pyth_price as u128), + EInvalidPythPriceConf, + ); + + let target_decimals = if (is_usd_price_config) { + 9 + } else { + type_config.decimals + }; // Our target decimals + let base_decimals = if (is_usd_price_config) { + type_config.decimals + } else { + 9 + }; // Our starting decimals + + ConversionConfig { + target_decimals, + base_decimals, + pyth_price, + pyth_decimals, + } +} + +/// Gets the raw Pyth price for a given asset +/// Returns (pyth_price, pyth_decimals) +public(package) fun get_pyth_price( + price_info_object: &PriceInfoObject, + registry: &MarginRegistry, + clock: &Clock, +): (u64, u8) { + let (pyth_price, pyth_decimals, _, _) = get_validated_pyth_price( + price_info_object, + registry, + clock, + ); + + (pyth_price, pyth_decimals) +} + +/// Helper function to get and validate Pyth price data +/// Returns (pyth_price, pyth_decimals, pyth_conf, type_config) +fun get_validated_pyth_price( + price_info_object: &PriceInfoObject, + registry: &MarginRegistry, + clock: &Clock, +): (u64, u8, u64, CoinTypeData) { + let config = registry.get_config(); + let type_config = registry.get_config_for_type(); + + let price = pyth::get_price_no_older_than( + price_info_object, + clock, + config.max_age_secs, + ); + let price_info = price_info_object.get_price_info_from_price_info_object(); + + // verify that the price feed id matches the one we have in our config. + assert!( + price_info.get_price_identifier().get_bytes() == type_config.price_feed_id, + EPriceFeedIdMismatch, + ); + + let pyth_price = price.get_price().get_magnitude_if_positive(); + let pyth_decimals = price.get_expo().get_magnitude_if_negative() as u8; + let pyth_conf = price.get_conf(); + + // verify that the ewma price is not too different from the pyth price + let ewma_price_object = price_info.get_price_feed().get_ema_price(); + let ewma_price = ewma_price_object.get_price().get_magnitude_if_positive(); + assert!( + (pyth_price as u128) * 10_000 <= (ewma_price as u128) * ((10_000 + type_config.max_ewma_difference_bps) as u128) && + (pyth_price as u128) * 10_000 >= (ewma_price as u128) * ((10_000 - type_config.max_ewma_difference_bps) as u128), + EInvalidPythPrice, + ); + + (pyth_price, pyth_decimals, pyth_conf, type_config) +} + +/// Gets the configuration for a given currency type. +fun get_config_for_type(registry: &MarginRegistry): CoinTypeData { + let config = registry.get_config(); + let payment_type = type_name::with_defining_ids(); + assert!(config.currencies.contains(&payment_type), ECurrencyNotSupported); + *config.currencies.get(&payment_type) +} + +fun get_decimals(registry: &MarginRegistry): u8 { + registry.get_config_for_type().decimals +} + +#[test_only] +public fun test_conversion_config( + target_decimals: u8, + base_decimals: u8, + pyth_price: u64, + pyth_decimals: u8, +): ConversionConfig { + ConversionConfig { + target_decimals, + base_decimals, + pyth_price, + pyth_decimals, + } +} + +#[test_only] +/// Create a test CoinTypeData for testing without needing CoinMetadata +public fun test_coin_type_data(decimals: u8, price_feed_id: vector): CoinTypeData { + CoinTypeData { + decimals, + price_feed_id, + type_name: type_name::with_defining_ids(), + max_conf_bps: 1000, // 10% + max_ewma_difference_bps: 1500, // 15% + } +} diff --git a/packages/deepbook_margin/sources/margin_manager.move b/packages/deepbook_margin/sources/margin_manager.move new file mode 100644 index 000000000..2a3e6aa56 --- /dev/null +++ b/packages/deepbook_margin/sources/margin_manager.move @@ -0,0 +1,1541 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +module deepbook_margin::margin_manager; + +use deepbook::{ + balance_manager::{ + Self, + BalanceManager, + TradeCap, + DepositCap, + WithdrawCap, + TradeProof, + DeepBookPoolReferral + }, + constants, + math, + order_info::OrderInfo, + pool::Pool, + registry::Registry +}; +use deepbook_margin::{ + margin_constants, + margin_pool::MarginPool, + margin_registry::MarginRegistry, + oracle::{calculate_target_currency, get_pyth_price, calculate_price}, + tpsl::{Self, TakeProfitStopLoss, PendingOrder, Condition, ConditionalOrder} +}; +use pyth::price_info::PriceInfoObject; +use std::{string::String, type_name::{Self, TypeName}}; +use sui::{clock::Clock, coin::Coin, event, vec_map::{Self, VecMap}}; +use token::deep::DEEP; + +// === Errors === +const EInvalidDeposit: u64 = 1; +const EMarginTradingNotAllowedInPool: u64 = 2; +const EInvalidMarginManagerOwner: u64 = 3; +const ECannotHaveLoanInMoreThanOneMarginPool: u64 = 4; +const EIncorrectDeepBookPool: u64 = 5; +const EDeepbookPoolNotAllowedForLoan: u64 = 6; +const EBorrowRiskRatioExceeded: u64 = 7; +const EWithdrawRiskRatioExceeded: u64 = 8; +const ECannotLiquidate: u64 = 9; +const EIncorrectMarginPool: u64 = 10; +const EInvalidManagerForSharing: u64 = 11; +const EInvalidDebtAsset: u64 = 12; +const ERepayAmountTooLow: u64 = 13; +const ERepaySharesTooLow: u64 = 14; +const EPoolNotEnabledForMarginTrading: u64 = 15; +const EConditionalOrderNotFound: u64 = 16; +const EOutstandingDebt: u64 = 17; + +// === Structs === +/// Witness type for authorizing MarginManager to call protected features of the DeepBook +public struct MarginApp has drop {} + +/// A shared object that wraps a `BalanceManager` and provides the necessary capabilities to deposit, withdraw, and trade. +public struct MarginManager has key { + id: UID, + owner: address, + deepbook_pool: ID, + margin_pool_id: Option, // If none, margin manager has no current loans in any margin pool + balance_manager: BalanceManager, + deposit_cap: DepositCap, + withdraw_cap: WithdrawCap, + trade_cap: TradeCap, + borrowed_base_shares: u64, + borrowed_quote_shares: u64, + take_profit_stop_loss: TakeProfitStopLoss, + extra_fields: VecMap, +} + +/// Hot potato to ensure manager is shared during creation +public struct ManagerInitializer { + margin_manager_id: ID, +} + +// === Events === +/// Event emitted when a new margin manager is created. +public struct MarginManagerCreatedEvent has copy, drop { + margin_manager_id: ID, + balance_manager_id: ID, + deepbook_pool_id: ID, + owner: address, + timestamp: u64, +} + +/// Event emitted when loan is borrowed +public struct LoanBorrowedEvent has copy, drop { + margin_manager_id: ID, + margin_pool_id: ID, + loan_amount: u64, + loan_shares: u64, + timestamp: u64, +} + +/// Event emitted when loan is repaid +public struct LoanRepaidEvent has copy, drop { + margin_manager_id: ID, + margin_pool_id: ID, + repay_amount: u64, + repay_shares: u64, + timestamp: u64, +} + +/// Event emitted when margin manager is liquidated +public struct LiquidationEvent has copy, drop { + margin_manager_id: ID, + margin_pool_id: ID, + liquidation_amount: u64, + pool_reward: u64, + pool_default: u64, + risk_ratio: u64, + remaining_base_asset: u64, + remaining_quote_asset: u64, + remaining_base_debt: u64, + remaining_quote_debt: u64, + base_pyth_price: u64, + base_pyth_decimals: u8, + quote_pyth_price: u64, + quote_pyth_decimals: u8, + timestamp: u64, +} + +/// Event emitted when user deposits collateral asset (either base or quote) into margin manager +public struct DepositCollateralEvent has copy, drop { + margin_manager_id: ID, + amount: u64, + asset: TypeName, + pyth_price: u64, + pyth_decimals: u8, + timestamp: u64, +} + +/// Event emitted when user withdraws collateral asset (either base or quote) from margin manager +public struct WithdrawCollateralEvent has copy, drop { + margin_manager_id: ID, + amount: u64, + asset: TypeName, + withdraw_base_asset: bool, + remaining_base_asset: u64, + remaining_quote_asset: u64, + remaining_base_debt: u64, + remaining_quote_debt: u64, + base_pyth_price: u64, + base_pyth_decimals: u8, + quote_pyth_price: u64, + quote_pyth_decimals: u8, + timestamp: u64, +} + +// === Functions - Take Profit Stop Loss === +/// Add a conditional order. +/// Specifies the conditions under which the order is triggered and the pending order to be placed. +public fun add_conditional_order( + self: &mut MarginManager, + pool: &Pool, + base_price_info_object: &PriceInfoObject, + quote_price_info_object: &PriceInfoObject, + registry: &MarginRegistry, + conditional_order_id: u64, + condition: Condition, + pending_order: PendingOrder, + clock: &Clock, + ctx: &mut TxContext, +) { + self.validate_owner(ctx); + let manager_id = self.id(); + assert!(pool.id() == self.deepbook_pool(), EIncorrectDeepBookPool); + self + .take_profit_stop_loss + .add_conditional_order( + pool, + manager_id, + base_price_info_object, + quote_price_info_object, + registry, + conditional_order_id, + condition, + pending_order, + clock, + ); +} + +/// Cancel all conditional orders. +public fun cancel_all_conditional_orders( + self: &mut MarginManager, + clock: &Clock, + ctx: &TxContext, +) { + self.validate_owner(ctx); + let manager_id = self.id(); + self.take_profit_stop_loss.cancel_all_conditional_orders(manager_id, clock); +} + +/// Cancel a conditional order. +public fun cancel_conditional_order( + self: &mut MarginManager, + conditional_order_id: u64, + clock: &Clock, + ctx: &TxContext, +) { + self.validate_owner(ctx); + let manager_id = self.id(); + self.take_profit_stop_loss.cancel_conditional_order(manager_id, conditional_order_id, clock); +} + +/// Execute conditional orders and return the order infos. +/// This is a permissionless function that can be called by anyone. +public fun execute_conditional_orders( + self: &mut MarginManager, + pool: &mut Pool, + base_price_info_object: &PriceInfoObject, + quote_price_info_object: &PriceInfoObject, + registry: &MarginRegistry, + max_orders_to_execute: u64, + clock: &Clock, + ctx: &TxContext, +): vector { + assert!(pool.id() == self.deepbook_pool(), EIncorrectDeepBookPool); + let current_price = calculate_price( + registry, + base_price_info_object, + quote_price_info_object, + clock, + ); + + let mut order_infos = vector[]; + let mut executed_ids = vector[]; + let mut expired_ids = vector[]; + let mut insufficient_funds_ids = vector[]; + + // Collect orders to process (to avoid borrow conflicts) + let mut orders_to_process = vector[]; + + // Collect trigger_below orders (sorted high to low) + let mut i = 0; + while (i < self.take_profit_stop_loss.trigger_below().length()) { + let conditional_order = &self.take_profit_stop_loss.trigger_below()[i]; + + // Break early if price doesn't trigger + if (current_price >= conditional_order.condition().trigger_price()) { + break + }; + + orders_to_process.push_back(*conditional_order); + i = i + 1; + }; + + // Collect trigger_above orders (sorted low to high) + i = 0; + while (i < self.take_profit_stop_loss.trigger_above().length()) { + let conditional_order = &self.take_profit_stop_loss.trigger_above()[i]; + + // Break early if price doesn't trigger + if (current_price <= conditional_order.condition().trigger_price()) { + break + }; + + orders_to_process.push_back(*conditional_order); + i = i + 1; + }; + + // Process collected orders + self.process_collected_orders( + pool, + registry, + orders_to_process, + &mut order_infos, + &mut executed_ids, + &mut expired_ids, + &mut insufficient_funds_ids, + max_orders_to_execute, + clock, + ctx, + ); + + let manager_id = self.id(); + let pool_id = pool.id(); + + insufficient_funds_ids.do!(|id| { + self.take_profit_stop_loss.emit_insufficient_funds_event(manager_id, id, clock); + }); + + let mut cancelled_ids = expired_ids; + cancelled_ids.append(insufficient_funds_ids); + // Canceled orders will include both expired and insufficient funds orders + cancelled_ids.do!(|id| { + self.take_profit_stop_loss.cancel_conditional_order(manager_id, id, clock); + }); + + self + .take_profit_stop_loss + .remove_executed_conditional_orders( + manager_id, + pool_id, + executed_ids, + clock, + ); + + order_infos +} + +// === Public Functions - Margin Manager === +/// Creates a new margin manager and shares it. +public fun new( + pool: &Pool, + deepbook_registry: &Registry, + margin_registry: &mut MarginRegistry, + clock: &Clock, + ctx: &mut TxContext, +): ID { + let manager = new_margin_manager(pool, deepbook_registry, margin_registry, clock, ctx); + let margin_manager_id = manager.id(); + transfer::share_object(manager); + + margin_manager_id +} + +/// Creates a new margin manager and returns it along with an initializer. +/// The initializer is used to ensure the margin manager is shared after creation. +public fun new_with_initializer( + pool: &Pool, + deepbook_registry: &Registry, + margin_registry: &mut MarginRegistry, + clock: &Clock, + ctx: &mut TxContext, +): (MarginManager, ManagerInitializer) { + let manager = new_margin_manager(pool, deepbook_registry, margin_registry, clock, ctx); + let initializer = ManagerInitializer { + margin_manager_id: manager.id(), + }; + + (manager, initializer) +} + +/// Shares the margin manager. The initializer is dropped in the process. +public fun share( + manager: MarginManager, + initializer: ManagerInitializer, +) { + assert!(manager.id() == initializer.margin_manager_id, EInvalidManagerForSharing); + transfer::share_object(manager); + + let ManagerInitializer { + margin_manager_id: _, + } = initializer; +} + +/// Unregister the margin manager from the margin registry. +public fun unregister_margin_manager( + self: &mut MarginManager, + margin_registry: &mut MarginRegistry, + ctx: &mut TxContext, +) { + self.validate_owner(ctx); + assert!(self.borrowed_base_shares == 0, EOutstandingDebt); + assert!(self.borrowed_quote_shares == 0, EOutstandingDebt); + assert!(self.margin_pool_id.is_none(), EOutstandingDebt); + + margin_registry.remove_margin_manager(self.id(), ctx); +} + +/// Set the referral for the margin manager. +public fun set_margin_manager_referral( + self: &mut MarginManager, + referral_cap: &DeepBookPoolReferral, + ctx: &mut TxContext, +) { + self.validate_owner(ctx); + self.balance_manager.set_balance_manager_referral(referral_cap, &self.trade_cap); +} + +/// Unset the referral for the margin manager. +public fun unset_margin_manager_referral( + self: &mut MarginManager, + pool_id: ID, + ctx: &mut TxContext, +) { + self.validate_owner(ctx); + self.balance_manager.unset_balance_manager_referral(pool_id, &self.trade_cap); +} + +// === Public Functions - Margin Manager === +/// Deposit a coin into the margin manager. The coin must be of the same type as either the base, quote, or DEEP. +public fun deposit( + self: &mut MarginManager, + registry: &MarginRegistry, + base_oracle: &PriceInfoObject, + quote_oracle: &PriceInfoObject, + coin: Coin, + clock: &Clock, + ctx: &mut TxContext, +) { + registry.load_inner(); + self.validate_owner(ctx); + + let deposit_amount = coin.value(); + self.deposit_int(coin, ctx); + + let deposit_asset_type = type_name::with_defining_ids(); + let deposit_base_asset = deposit_asset_type == type_name::with_defining_ids(); + let deposit_quote_asset = deposit_asset_type == type_name::with_defining_ids(); + // We return early here, because there is no need to emit a deposit collateral event if neither the base asset + // nor the quote asset is deposited. This handles the case for DEEP deposits, when DEEP is not part of the base + // or quote assets. + if (!deposit_base_asset && !deposit_quote_asset) return; + + let (pyth_price, pyth_decimals) = if (deposit_base_asset) { + get_pyth_price(base_oracle, registry, clock) + } else { + get_pyth_price(quote_oracle, registry, clock) + }; + + event::emit(DepositCollateralEvent { + margin_manager_id: self.id(), + amount: deposit_amount, + asset: deposit_asset_type, + pyth_price, + pyth_decimals, + timestamp: clock.timestamp_ms(), + }); +} + +/// Withdraw a specified amount of an asset from the margin manager. The asset must be of the same type as either the base, quote, or DEEP. +/// The withdrawal is subject to the risk ratio limit. +public fun withdraw( + self: &mut MarginManager, + registry: &MarginRegistry, + base_margin_pool: &MarginPool, + quote_margin_pool: &MarginPool, + base_oracle: &PriceInfoObject, + quote_oracle: &PriceInfoObject, + pool: &Pool, + withdraw_amount: u64, + clock: &Clock, + ctx: &mut TxContext, +): Coin { + registry.load_inner(); + self.validate_owner(ctx); + assert!(pool.id() == self.deepbook_pool(), EIncorrectDeepBookPool); + + let balance_manager = &mut self.balance_manager; + let withdraw_cap = &self.withdraw_cap; + + let coin = balance_manager.withdraw_with_cap( + withdraw_cap, + withdraw_amount, + ctx, + ); + + if (self.margin_pool_id.contains(&base_margin_pool.id())) { + let risk_ratio = self.risk_ratio_int( + registry, + base_oracle, + quote_oracle, + pool, + base_margin_pool, + clock, + ); + assert!(registry.can_withdraw(pool.id(), risk_ratio), EWithdrawRiskRatioExceeded); + } else if (self.margin_pool_id.contains("e_margin_pool.id())) { + let risk_ratio = self.risk_ratio_int( + registry, + base_oracle, + quote_oracle, + pool, + quote_margin_pool, + clock, + ); + assert!(registry.can_withdraw(pool.id(), risk_ratio), EWithdrawRiskRatioExceeded); + }; + + let withdraw_asset_type = type_name::with_defining_ids(); + let withdraw_base_asset = withdraw_asset_type == type_name::with_defining_ids(); + let withdraw_quote_asset = withdraw_asset_type == type_name::with_defining_ids(); + // We return early here, because there is no need to emit a withdraw collateral event if neither the base asset + // nor the quote asset is withdrawn. This handles the case for DEEP withdrawals, when DEEP is not part of the base + // or quote assets. + if (!withdraw_base_asset && !withdraw_quote_asset) return coin; + + let ( + _, + _, + _, + remaining_base_asset, + remaining_quote_asset, + remaining_base_debt, + remaining_quote_debt, + base_pyth_price, + base_pyth_decimals, + quote_pyth_price, + quote_pyth_decimals, + _, + _, + _, + ) = self.manager_state( + registry, + base_oracle, + quote_oracle, + pool, + base_margin_pool, + quote_margin_pool, + clock, + ); + + event::emit(WithdrawCollateralEvent { + margin_manager_id: self.id(), + amount: withdraw_amount, + asset: withdraw_asset_type, + withdraw_base_asset, + remaining_base_asset, + remaining_quote_asset, + remaining_base_debt, + remaining_quote_debt, + base_pyth_price, + base_pyth_decimals, + quote_pyth_price, + quote_pyth_decimals, + timestamp: clock.timestamp_ms(), + }); + + coin +} + +/// Borrow the base asset using the margin manager. +public fun borrow_base( + self: &mut MarginManager, + registry: &MarginRegistry, + base_margin_pool: &mut MarginPool, + base_oracle: &PriceInfoObject, + quote_oracle: &PriceInfoObject, + pool: &Pool, + loan_amount: u64, + clock: &Clock, + ctx: &mut TxContext, +) { + registry.load_inner(); + self.validate_owner(ctx); + assert!(registry.pool_enabled(pool), EPoolNotEnabledForMarginTrading); + assert!(pool.id() == self.deepbook_pool, EIncorrectDeepBookPool); + assert!(self.can_borrow(base_margin_pool), ECannotHaveLoanInMoreThanOneMarginPool); + assert!( + base_margin_pool.deepbook_pool_allowed(self.deepbook_pool), + EDeepbookPoolNotAllowedForLoan, + ); + let (coin, borrowed_shares) = base_margin_pool.borrow(loan_amount, clock, ctx); + self.borrowed_base_shares = self.borrowed_base_shares + borrowed_shares; + self.margin_pool_id = option::some(base_margin_pool.id()); + self.deposit_int(coin, ctx); + let risk_ratio = self.risk_ratio_int( + registry, + base_oracle, + quote_oracle, + pool, + base_margin_pool, + clock, + ); + assert!(registry.can_borrow(pool.id(), risk_ratio), EBorrowRiskRatioExceeded); + + event::emit(LoanBorrowedEvent { + margin_manager_id: self.id(), + margin_pool_id: base_margin_pool.id(), + loan_amount, + loan_shares: borrowed_shares, + timestamp: clock.timestamp_ms(), + }); +} + +/// Borrow the quote asset using the margin manager. +public fun borrow_quote( + self: &mut MarginManager, + registry: &MarginRegistry, + quote_margin_pool: &mut MarginPool, + base_oracle: &PriceInfoObject, + quote_oracle: &PriceInfoObject, + pool: &Pool, + loan_amount: u64, + clock: &Clock, + ctx: &mut TxContext, +) { + registry.load_inner(); + self.validate_owner(ctx); + assert!(registry.pool_enabled(pool), EPoolNotEnabledForMarginTrading); + assert!(pool.id() == self.deepbook_pool, EIncorrectDeepBookPool); + assert!(self.can_borrow(quote_margin_pool), ECannotHaveLoanInMoreThanOneMarginPool); + assert!( + quote_margin_pool.deepbook_pool_allowed(self.deepbook_pool), + EDeepbookPoolNotAllowedForLoan, + ); + let (coin, borrowed_shares) = quote_margin_pool.borrow(loan_amount, clock, ctx); + self.borrowed_quote_shares = self.borrowed_quote_shares + borrowed_shares; + self.margin_pool_id = option::some(quote_margin_pool.id()); + self.deposit_int(coin, ctx); + let risk_ratio = self.risk_ratio_int( + registry, + base_oracle, + quote_oracle, + pool, + quote_margin_pool, + clock, + ); + assert!(registry.can_borrow(pool.id(), risk_ratio), EBorrowRiskRatioExceeded); + + event::emit(LoanBorrowedEvent { + margin_manager_id: self.id(), + margin_pool_id: quote_margin_pool.id(), + loan_amount, + loan_shares: borrowed_shares, + timestamp: clock.timestamp_ms(), + }); +} + +/// Repay the base asset loan using the margin manager. +/// Returns the total amount repaid +public fun repay_base( + self: &mut MarginManager, + registry: &MarginRegistry, + margin_pool: &mut MarginPool, + amount: Option, + clock: &Clock, + ctx: &mut TxContext, +): u64 { + registry.load_inner(); + self.validate_owner(ctx); + assert!(self.margin_pool_id.contains(&margin_pool.id()), EIncorrectMarginPool); + + self.repay( + margin_pool, + amount, + clock, + ctx, + ) +} + +/// Repay the quote asset loan using the margin manager. +/// Returns the total amount repaid +public fun repay_quote( + self: &mut MarginManager, + registry: &MarginRegistry, + margin_pool: &mut MarginPool, + amount: Option, + clock: &Clock, + ctx: &mut TxContext, +): u64 { + registry.load_inner(); + self.validate_owner(ctx); + assert!(self.margin_pool_id.contains(&margin_pool.id()), EIncorrectMarginPool); + + self.repay( + margin_pool, + amount, + clock, + ctx, + ) +} + +// === Public Functions - Liquidation - Receive Assets After Liquidation === +public fun liquidate( + self: &mut MarginManager, + registry: &MarginRegistry, + base_oracle: &PriceInfoObject, + quote_oracle: &PriceInfoObject, + margin_pool: &mut MarginPool, + pool: &mut Pool, + mut repay_coin: Coin, + clock: &Clock, + ctx: &mut TxContext, +): (Coin, Coin, Coin) { + // 1. Check that we can liquidate, cancel all open orders. + assert!(self.deepbook_pool == pool.id(), EIncorrectDeepBookPool); + assert!(self.margin_pool_id.contains(&margin_pool.id()), EIncorrectMarginPool); + let risk_ratio = self.risk_ratio_int( + registry, + base_oracle, + quote_oracle, + pool, + margin_pool, + clock, + ); + assert!(registry.can_liquidate(pool.id(), risk_ratio), ECannotLiquidate); + assert!(repay_coin.value() >= margin_constants::min_liquidation_repay(), ERepayAmountTooLow); + let trade_proof = self.trade_proof(ctx); + pool.cancel_all_orders(&mut self.balance_manager, &trade_proof, clock, ctx); + + // 2. Calculate the maximum debt that can be repaid. The margin manager can be in three scenarios: + // a) Assets <= Debt + user_reward: Full liquidation, repay as much debt as possible, lending pool may incur bad debt. + // b) Debt + user_reward < Assets <= Debt + user_reward + pool_reward: There are enough assets to cover the debt, but pool may not get full rewards. + // c) Debt + user_reward + pool_reward < Assets: There are enough assets to cover everything. We may not need to liquidate the full position. + let borrowed_shares = self.borrowed_base_shares.max(self.borrowed_quote_shares); + let debt = margin_pool.borrow_shares_to_amount(borrowed_shares, clock); // 350 USDC debt + let debt_is_base = + type_name::with_defining_ids() == type_name::with_defining_ids(); + let (assets_in_debt_unit, base_asset, quote_asset) = self.assets_in_debt_unit( + registry, + pool, + base_oracle, + quote_oracle, + clock, + ); // SUI/USDC pool. We have 90 SUI and 40 USDC, 350 USDC debt. This should be 400 USDC. (assume 1 SUI = 4 USDC) + + let liquidation_reward_with_user_pool = + constants::float_scaling() + registry.user_liquidation_reward(pool.id()) + registry.pool_liquidation_reward(pool.id()); // 1.05 + + let target_ratio = registry.target_liquidation_risk_ratio(pool.id()); // 1.25 + let numerator = math::mul(target_ratio, debt) - assets_in_debt_unit; // 1.25 * 350 - 400 = 437.5 - 400 = 37.5 + let denominator = target_ratio - liquidation_reward_with_user_pool; // 1.25 - 1.05 = 0.2 + let debt_repay = math::div(numerator, denominator); // 37.5 / 0.2 = 187.5 + // We have to pay the minimum between our current debt and the debt required to reach the target ratio. + // In other words, if our assets are low, we pay off all debt (full liquidation) + // if our assets are high, we pay off some of the debt (partial liquidation) + let debt_repay = debt_repay.min(debt); // 187.5 + let debt_with_reward = math::mul(debt_repay, liquidation_reward_with_user_pool); // 187.5 * 1.05 = 196.875 + let debt_can_repay_with_rewards = debt_with_reward.min(assets_in_debt_unit); // 196.875 + let max_repay = math::div(debt_can_repay_with_rewards, liquidation_reward_with_user_pool); // 196.875 / 1.05 = 187.5 + let liquidation_reward_with_pool = + constants::float_scaling() + registry.pool_liquidation_reward(pool.id()); // 1.03 (assume 3% pool reward, 2% user reward) + + let input_coin_without_pool_reward = math::div( + repay_coin.value(), + liquidation_reward_with_pool, + ); // 100 / 1.03 = 97.087 + let repay_amount = max_repay.min(input_coin_without_pool_reward); // 97.087 + let repay_amount_with_pool_reward = math::mul(repay_amount, liquidation_reward_with_pool); // 97.087 * 1.03 = 100 + + let repay_shares = if (risk_ratio < constants::float_scaling() && repay_amount == max_repay) { + borrowed_shares + } else { + math::mul( + borrowed_shares, + math::div(repay_amount, debt), + ) + }; // Assume index 2, so borrowed_shares = 350/2 = 175. 97.087 / 350 = 0.2774 * 175 = 48.545 shares being repaid (97.087 USDC is repayment) + assert!(repay_shares > 0, ERepaySharesTooLow); + let (debt_repaid, pool_reward, pool_default) = margin_pool.repay_liquidation( + repay_shares, + repay_coin.split(repay_amount_with_pool_reward, ctx), + clock, + ); + // 97.087 debt repaid, pool reward is 100 - 97.087 = 2.913 (3%), pool_default is 0 + // We only default if this is a full liquidation + + if (debt_is_base) { + self.borrowed_base_shares = self.borrowed_base_shares - repay_shares; + } else { + self.borrowed_quote_shares = self.borrowed_quote_shares - repay_shares; + }; + + // Clear margin_pool_id if fully liquidated + if (self.borrowed_base_shares == 0 && self.borrowed_quote_shares == 0) { + self.margin_pool_id = option::none(); + }; + + // repay_amount * 1.05 is what the user should receive back, since the user provided both the repayment and pool reward + // user should receive as much assets possible in the debt asset first, then the collateral asset + + let mut out_amount = math::mul(repay_amount, liquidation_reward_with_user_pool); // 97.087 * 1.05 = 101.941 + + let (base_coin, quote_coin) = if (debt_is_base) { + let base_out = out_amount.min(base_asset); + out_amount = out_amount - base_out; + let max_quote_out = calculate_target_currency( + registry, + base_oracle, + quote_oracle, + out_amount, + clock, + ); + let quote_out = max_quote_out.min(quote_asset); + let base_coin = self.liquidation_withdraw( + base_out, + ctx, + ); + let quote_coin = self.liquidation_withdraw( + quote_out, + ctx, + ); + (base_coin, quote_coin) + } else { + let quote_out = out_amount.min(quote_asset); + out_amount = out_amount - quote_out; // 101.941 - 40 = 61.941 + let max_base_out = calculate_target_currency( + registry, + quote_oracle, + base_oracle, + out_amount, + clock, + ); + let base_out = max_base_out.min(base_asset); + let base_coin = self.liquidation_withdraw( + base_out, + ctx, + ); + let quote_coin = self.liquidation_withdraw( + quote_out, + ctx, + ); + (base_coin, quote_coin) + }; + // We have 40 USDC which is used first in the second loop. Then SUI to reach the total of 101.941 USDC. + + let (remaining_base_asset, remaining_quote_asset) = self.calculate_assets(pool); + let (remaining_base_debt, remaining_quote_debt) = if (self.margin_pool_id.is_some()) { + self.calculate_debts(margin_pool, clock) + } else { + (0, 0) + }; + let (base_pyth_price, base_pyth_decimals) = get_pyth_price( + base_oracle, + registry, + clock, + ); + let (quote_pyth_price, quote_pyth_decimals) = get_pyth_price( + quote_oracle, + registry, + clock, + ); + + event::emit(LiquidationEvent { + margin_manager_id: self.id(), + margin_pool_id: margin_pool.id(), + liquidation_amount: debt_repaid, + pool_reward, + pool_default, + risk_ratio, + remaining_base_asset, + remaining_quote_asset, + remaining_base_debt, + remaining_quote_debt, + base_pyth_price, + base_pyth_decimals, + quote_pyth_price, + quote_pyth_decimals, + timestamp: clock.timestamp_ms(), + }); + + (base_coin, quote_coin, repay_coin) +} + +// Returns the risk ratio of the margin manager given the corresponding margin pools. +public fun risk_ratio( + self: &MarginManager, + registry: &MarginRegistry, + base_oracle: &PriceInfoObject, + quote_oracle: &PriceInfoObject, + pool: &Pool, + base_margin_pool: &MarginPool, + quote_margin_pool: &MarginPool, + clock: &Clock, +): u64 { + let debt_is_base = self.borrowed_base_shares > 0; + if (debt_is_base) { + self.risk_ratio_int( + registry, + base_oracle, + quote_oracle, + pool, + base_margin_pool, + clock, + ) + } else { + self.risk_ratio_int( + registry, + base_oracle, + quote_oracle, + pool, + quote_margin_pool, + clock, + ) + } +} + +// === Public Functions - Read Only === +public fun balance_manager( + self: &MarginManager, +): &BalanceManager { + &self.balance_manager +} + +public fun base_balance(self: &MarginManager): u64 { + self.balance_manager.balance() +} + +public fun quote_balance(self: &MarginManager): u64 { + self.balance_manager.balance() +} + +public fun deep_balance(self: &MarginManager): u64 { + self.balance_manager.balance() +} + +/// Returns (base_asset, quote_asset) for margin manager. +public fun calculate_assets( + self: &MarginManager, + pool: &Pool, +): (u64, u64) { + let balance_manager = self.balance_manager(); + let (mut base, mut quote, _) = pool.locked_balance(balance_manager); + base = base + balance_manager.balance(); + quote = quote + balance_manager.balance(); + + (base, quote) +} + +public fun calculate_debts( + self: &MarginManager, + margin_pool: &MarginPool, + clock: &Clock, +): (u64, u64) { + let margin_pool_id = margin_pool.id(); + assert!(self.margin_pool_id.contains(&margin_pool_id), EIncorrectMarginPool); + + let debt_is_base = self.has_base_debt(); + let debt_shares = if (debt_is_base) { + self.borrowed_base_shares + } else { + self.borrowed_quote_shares + }; + + let base_debt = if (debt_is_base) { + assert!( + type_name::with_defining_ids() == type_name::with_defining_ids(), + EInvalidDebtAsset, + ); + margin_pool.borrow_shares_to_amount(debt_shares, clock) + } else { + 0 + }; + let quote_debt = if (debt_is_base) { + 0 + } else { + assert!( + type_name::with_defining_ids() == type_name::with_defining_ids(), + EInvalidDebtAsset, + ); + margin_pool.borrow_shares_to_amount(debt_shares, clock) + }; + + (base_debt, quote_debt) +} + +/// Returns comprehensive state information for a margin manager. +/// Returns (manager_id, deepbook_pool_id, risk_ratio, base_asset, quote_asset, +/// base_debt, quote_debt, base_pyth_price, base_pyth_decimals, +/// quote_pyth_price, quote_pyth_decimals, current_price, +/// lowest_trigger_above_price, highest_trigger_below_price) +public fun manager_state( + self: &MarginManager, + registry: &MarginRegistry, + base_oracle: &PriceInfoObject, + quote_oracle: &PriceInfoObject, + pool: &Pool, + base_margin_pool: &MarginPool, + quote_margin_pool: &MarginPool, + clock: &Clock, +): (ID, ID, u64, u64, u64, u64, u64, u64, u8, u64, u8, u64, u64, u64) { + let manager_id = self.id(); + let deepbook_pool_id = self.deepbook_pool; + let (base_asset, quote_asset) = self.calculate_assets(pool); + let (base_debt, quote_debt) = if (self.margin_pool_id.is_some()) { + if (self.has_base_debt()) { + self.calculate_debts(base_margin_pool, clock) + } else { + self.calculate_debts(quote_margin_pool, clock) + } + } else { + (0, 0) + }; + let risk_ratio = self.risk_ratio( + registry, + base_oracle, + quote_oracle, + pool, + base_margin_pool, + quote_margin_pool, + clock, + ); + + // Get raw Pyth oracle prices and decimals + let (base_pyth_price, base_pyth_decimals) = get_pyth_price( + base_oracle, + registry, + clock, + ); + let (quote_pyth_price, quote_pyth_decimals) = get_pyth_price( + quote_oracle, + registry, + clock, + ); + + // Calculate current price + let current_price = calculate_price( + registry, + base_oracle, + quote_oracle, + clock, + ); + + // Get the lowest trigger above price and highest trigger below price + let lowest_trigger_above_price = self.lowest_trigger_above_price(); + let highest_trigger_below_price = self.highest_trigger_below_price(); + + ( + manager_id, + deepbook_pool_id, + risk_ratio, + base_asset, + quote_asset, + base_debt, + quote_debt, + base_pyth_price, + base_pyth_decimals, + quote_pyth_price, + quote_pyth_decimals, + current_price, + lowest_trigger_above_price, + highest_trigger_below_price, + ) +} + +public fun id(self: &MarginManager): ID { + self.id.to_inner() +} + +public fun owner(self: &MarginManager): address { + self.owner +} + +public fun deepbook_pool(self: &MarginManager): ID { + self.deepbook_pool +} + +public fun margin_pool_id( + self: &MarginManager, +): Option { + self.margin_pool_id +} + +public fun borrowed_shares( + self: &MarginManager, +): (u64, u64) { + (self.borrowed_base_shares, self.borrowed_quote_shares) +} + +public fun borrowed_base_shares( + self: &MarginManager, +): u64 { + self.borrowed_base_shares +} + +public fun borrowed_quote_shares( + self: &MarginManager, +): u64 { + self.borrowed_quote_shares +} + +public fun has_base_debt(self: &MarginManager): bool { + self.borrowed_base_shares > 0 +} + +public fun conditional_order_ids( + self: &MarginManager, +): vector { + let mut ids = vector::empty(); + + let trigger_below = self.take_profit_stop_loss.trigger_below_orders(); + let mut i = 0; + while (i < trigger_below.length()) { + ids.push_back(trigger_below[i].conditional_order_id()); + i = i + 1; + }; + + let trigger_above = self.take_profit_stop_loss.trigger_above_orders(); + i = 0; + while (i < trigger_above.length()) { + ids.push_back(trigger_above[i].conditional_order_id()); + i = i + 1; + }; + + ids +} + +public fun conditional_order( + self: &MarginManager, + conditional_order_id: u64, +): ConditionalOrder { + let conditional_order = self.take_profit_stop_loss.get_conditional_order(conditional_order_id); + assert!(conditional_order.is_some(), EConditionalOrderNotFound); + + conditional_order.destroy_some() +} + +/// Returns the lowest trigger price for trigger_above orders +/// Returns constants::max_u64() if there are no trigger_above orders +public fun lowest_trigger_above_price( + self: &MarginManager, +): u64 { + let trigger_above = self.take_profit_stop_loss.trigger_above_orders(); + if (trigger_above.is_empty()) { + constants::max_u64() + } else { + trigger_above[0].condition().trigger_price() + } +} + +/// Returns the highest trigger price for trigger_below orders +/// Returns 0 if there are no trigger_below orders +public fun highest_trigger_below_price( + self: &MarginManager, +): u64 { + let trigger_below = self.take_profit_stop_loss.trigger_below_orders(); + if (trigger_below.is_empty()) { + 0 + } else { + trigger_below[0].condition().trigger_price() + } +} + +// === Public-Package Functions === +/// Unwraps balance manager for trading in deepbook. +public(package) fun balance_manager_trading_mut( + self: &mut MarginManager, + ctx: &TxContext, +): &mut BalanceManager { + assert!(self.owner == ctx.sender(), EInvalidMarginManagerOwner); + + &mut self.balance_manager +} + +/// Withdraws settled amounts from the pool permissionlessly. +/// Anyone can call this via the pool_proxy to settle balances. +public(package) fun withdraw_settled_amounts_permissionless_int( + self: &mut MarginManager, + pool: &mut Pool, +) { + assert!(self.deepbook_pool == pool.id(), EIncorrectDeepBookPool); + pool.withdraw_settled_amounts_permissionless(&mut self.balance_manager); +} + +/// Unwraps balance manager for trading in deepbook. +public(package) fun trade_proof( + self: &mut MarginManager, + ctx: &TxContext, +): TradeProof { + self.balance_manager.generate_proof_as_trader(&self.trade_cap, ctx) +} + +/// Deposit a coin into the margin manager. The coin must be of the same type as either the base, quote, or DEEP. +public(package) fun deposit_int( + self: &mut MarginManager, + coin: Coin, + ctx: &TxContext, +) { + let deposit_asset_type = type_name::with_defining_ids(); + let base_asset_type = type_name::with_defining_ids(); + let quote_asset_type = type_name::with_defining_ids(); + let deep_asset_type = type_name::with_defining_ids(); + assert!( + deposit_asset_type == base_asset_type || deposit_asset_type == quote_asset_type || + deposit_asset_type == deep_asset_type, + EInvalidDeposit, + ); + + let balance_manager = &mut self.balance_manager; + let deposit_cap = &self.deposit_cap; + + balance_manager.deposit_with_cap(deposit_cap, coin, ctx); +} + +// === Private Functions === +// Get the risk ratio of the margin manager. +fun risk_ratio_int( + self: &MarginManager, + registry: &MarginRegistry, + base_oracle: &PriceInfoObject, + quote_oracle: &PriceInfoObject, + pool: &Pool, + margin_pool: &MarginPool, + clock: &Clock, +): u64 { + assert!( + self.margin_pool_id.contains(&margin_pool.id()) || self.margin_pool_id.is_none(), + EIncorrectMarginPool, + ); + let (assets_in_debt_unit, _, _) = self.assets_in_debt_unit( + registry, + pool, + base_oracle, + quote_oracle, + clock, + ); + let borrowed_shares = self.borrowed_base_shares.max(self.borrowed_quote_shares); + let debt = margin_pool.borrow_shares_to_amount(borrowed_shares, clock); + let max_risk_ratio = margin_constants::max_risk_ratio(); + if (assets_in_debt_unit >= math::mul(debt, max_risk_ratio)) { + max_risk_ratio + } else { + math::div(assets_in_debt_unit, debt) + } +} + +fun new_margin_manager( + pool: &Pool, + deepbook_registry: &Registry, + margin_registry: &mut MarginRegistry, + clock: &Clock, + ctx: &mut TxContext, +): MarginManager { + margin_registry.load_inner(); + assert!(margin_registry.pool_enabled(pool), EMarginTradingNotAllowedInPool); + + let id = object::new(ctx); + let margin_manager_id = id.to_inner(); + let owner = ctx.sender(); + + let ( + balance_manager, + deposit_cap, + withdraw_cap, + trade_cap, + ) = balance_manager::new_with_custom_owner_caps( + deepbook_registry, + id.to_address(), + ctx, + ); + margin_registry.add_margin_manager(id.to_inner(), ctx); + + event::emit(MarginManagerCreatedEvent { + margin_manager_id, + balance_manager_id: balance_manager.id(), + deepbook_pool_id: pool.id(), + owner, + timestamp: clock.timestamp_ms(), + }); + + MarginManager { + id, + owner, + deepbook_pool: pool.id(), + margin_pool_id: option::none(), + balance_manager, + deposit_cap, + withdraw_cap, + trade_cap, + borrowed_base_shares: 0, + borrowed_quote_shares: 0, + take_profit_stop_loss: tpsl::new(), + extra_fields: vec_map::empty(), + } +} + +fun validate_owner( + self: &MarginManager, + ctx: &TxContext, +) { + assert!(ctx.sender() == self.owner, EInvalidMarginManagerOwner); +} + +/// Repays the loan using the margin manager. +/// Returns the total amount repaid +fun repay( + self: &mut MarginManager, + margin_pool: &mut MarginPool, + amount: Option, + clock: &Clock, + ctx: &mut TxContext, +): u64 { + let borrowed_shares = self.borrowed_base_shares.max(self.borrowed_quote_shares); + let borrowed_amount = margin_pool.borrow_shares_to_amount(borrowed_shares, clock); + let available_balance = self.balance_manager().balance(); + let repay_amount = amount.destroy_with_default(available_balance); + let repay_amount = repay_amount.min(borrowed_amount); + let repay_shares = math::mul(borrowed_shares, math::div(repay_amount, borrowed_amount)); + + let coin: Coin = self.repay_withdraw(repay_amount, ctx); + margin_pool.repay(repay_shares, coin, clock); + + if (type_name::with_defining_ids() == type_name::with_defining_ids()) { + self.borrowed_base_shares = self.borrowed_base_shares - repay_shares; + } else { + self.borrowed_quote_shares = self.borrowed_quote_shares - repay_shares; + }; + + if (self.borrowed_base_shares == 0 && self.borrowed_quote_shares == 0) { + self.margin_pool_id = option::none(); + }; + + event::emit(LoanRepaidEvent { + margin_manager_id: self.id(), + margin_pool_id: margin_pool.id(), + repay_amount, + repay_shares, + timestamp: clock.timestamp_ms(), + }); + + repay_amount +} + +fun liquidation_withdraw( + self: &mut MarginManager, + withdraw_amount: u64, + ctx: &mut TxContext, +): Coin { + let balance_manager = &mut self.balance_manager; + + balance_manager.withdraw_with_cap( + &self.withdraw_cap, + withdraw_amount, + ctx, + ) +} + +/// This can only be called by the manager owner +fun repay_withdraw( + self: &mut MarginManager, + withdraw_amount: u64, + ctx: &mut TxContext, +): Coin { + validate_owner(self, ctx); + let balance_manager = &mut self.balance_manager; + + let coin = balance_manager.withdraw_with_cap( + &self.withdraw_cap, + withdraw_amount, + ctx, + ); + + coin +} + +/// Helper function to determine if margin manager can borrow from a margin pool +fun can_borrow( + self: &MarginManager, + margin_pool: &MarginPool, +): bool { + let no_current_loan = self.margin_pool_id.is_none(); + + self.margin_pool_id.contains(&margin_pool.id()) || no_current_loan +} + +/// Returns (assets_in_debt_unit, base_asset, quote_asset) +fun assets_in_debt_unit( + self: &MarginManager, + registry: &MarginRegistry, + pool: &Pool, + base_oracle: &PriceInfoObject, + quote_oracle: &PriceInfoObject, + clock: &Clock, +): (u64, u64, u64) { + let (base_asset, quote_asset) = self.calculate_assets(pool); + if (self.margin_pool_id.is_none()) { + return (0, base_asset, quote_asset) + }; + + let assets_in_debt_unit = if (self.borrowed_base_shares > 0) { + calculate_target_currency(registry, quote_oracle, base_oracle, quote_asset, clock) + base_asset + } else { + calculate_target_currency(registry, base_oracle, quote_oracle, base_asset, clock) + quote_asset + }; + (assets_in_debt_unit, base_asset, quote_asset) +} + +fun place_pending_order( + self: &mut MarginManager, + registry: &MarginRegistry, + pool: &mut Pool, + pending_order: &PendingOrder, + clock: &Clock, + ctx: &TxContext, +): OrderInfo { + if (pending_order.is_limit_order()) { + self.place_pending_limit_order( + registry, + pool, + pending_order.client_order_id(), + pending_order.order_type().destroy_some(), + pending_order.self_matching_option(), + pending_order.price().destroy_some(), + pending_order.quantity(), + pending_order.is_bid(), + pending_order.pay_with_deep(), + pending_order.expire_timestamp().destroy_some(), + clock, + ctx, + ) + } else { + self.place_market_order_conditional( + registry, + pool, + pending_order.client_order_id(), + pending_order.self_matching_option(), + pending_order.quantity(), + pending_order.is_bid(), + pending_order.pay_with_deep(), + clock, + ctx, + ) + } +} + +/// Only used for tpsl pending orders. +fun place_pending_limit_order( + self: &mut MarginManager, + registry: &MarginRegistry, + pool: &mut Pool, + client_order_id: u64, + order_type: u8, + self_matching_option: u8, + price: u64, + quantity: u64, + is_bid: bool, + pay_with_deep: bool, + expire_timestamp: u64, + clock: &Clock, + ctx: &TxContext, +): OrderInfo { + assert!(self.deepbook_pool() == pool.id(), EIncorrectDeepBookPool); + let trade_proof = self.trade_proof(ctx); + let balance_manager = self.balance_manager_unsafe_mut(); + assert!(registry.pool_enabled(pool), EPoolNotEnabledForMarginTrading); + + pool.place_limit_order( + balance_manager, + &trade_proof, + client_order_id, + order_type, + self_matching_option, + price, + quantity, + is_bid, + pay_with_deep, + expire_timestamp, + clock, + ctx, + ) +} + +/// Places a market order in the pool. +/// Only used for tpsl pending orders. +fun place_market_order_conditional( + self: &mut MarginManager, + registry: &MarginRegistry, + pool: &mut Pool, + client_order_id: u64, + self_matching_option: u8, + quantity: u64, + is_bid: bool, + pay_with_deep: bool, + clock: &Clock, + ctx: &TxContext, +): OrderInfo { + assert!(self.deepbook_pool() == pool.id(), EIncorrectDeepBookPool); + let trade_proof = self.trade_proof(ctx); + let balance_manager = self.balance_manager_unsafe_mut(); + assert!(registry.pool_enabled(pool), EPoolNotEnabledForMarginTrading); + + pool.place_market_order( + balance_manager, + &trade_proof, + client_order_id, + self_matching_option, + quantity, + is_bid, + pay_with_deep, + clock, + ctx, + ) +} + +/// Helper function to process collected conditional orders +fun process_collected_orders( + self: &mut MarginManager, + pool: &mut Pool, + registry: &MarginRegistry, + orders: vector, + order_infos: &mut vector, + executed_ids: &mut vector, + expired_ids: &mut vector, + insufficient_funds_ids: &mut vector, + max_orders_to_execute: u64, + clock: &Clock, + ctx: &TxContext, +) { + let mut i = 0; + while (i < orders.length() && order_infos.length() < max_orders_to_execute) { + let conditional_order = &orders[i]; + let conditional_order_id = conditional_order.conditional_order_id(); + let pending_order = conditional_order.pending_order(); + + let can_place = if (pending_order.is_limit_order()) { + pool.can_place_limit_order( + self.balance_manager(), + pending_order.price().destroy_some(), + pending_order.quantity(), + pending_order.is_bid(), + pending_order.pay_with_deep(), + pending_order.expire_timestamp().destroy_some(), + clock, + ) + } else { + pool.can_place_market_order( + self.balance_manager(), + pending_order.quantity(), + pending_order.is_bid(), + pending_order.pay_with_deep(), + clock, + ) + }; + + if (can_place) { + let order_info = self.place_pending_order( + registry, + pool, + &pending_order, + clock, + ctx, + ); + order_infos.push_back(order_info); + executed_ids.push_back(conditional_order_id); + } else { + if (pending_order.is_limit_order()) { + let expire_timestamp = *pending_order.expire_timestamp().borrow(); + if (expire_timestamp <= clock.timestamp_ms()) { + expired_ids.push_back(conditional_order_id); + } else { + insufficient_funds_ids.push_back(conditional_order_id); + } + } else { + insufficient_funds_ids.push_back(conditional_order_id); + } + }; + + i = i + 1; + } +} + +fun balance_manager_unsafe_mut( + self: &mut MarginManager, +): &mut BalanceManager { + &mut self.balance_manager +} diff --git a/packages/deepbook_margin/sources/margin_pool.move b/packages/deepbook_margin/sources/margin_pool.move new file mode 100644 index 000000000..b23d78f42 --- /dev/null +++ b/packages/deepbook_margin/sources/margin_pool.move @@ -0,0 +1,673 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +module deepbook_margin::margin_pool; + +use deepbook::{constants, math}; +use deepbook_margin::{ + margin_registry::{MarginRegistry, MaintainerCap, MarginAdminCap, MarginPoolCap}, + margin_state::{Self, State}, + position_manager::{Self, PositionManager}, + protocol_config::{InterestConfig, MarginPoolConfig, ProtocolConfig}, + protocol_fees::{Self, ProtocolFees, SupplyReferral}, + rate_limiter::{Self, RateLimiter} +}; +use std::{string::String, type_name::{Self, TypeName}}; +use sui::{ + balance::{Self, Balance}, + clock::Clock, + coin::Coin, + event, + vec_map::{Self, VecMap}, + vec_set::{Self, VecSet} +}; + +// === Errors === +const ENotEnoughAssetInPool: u64 = 1; +const ESupplyCapExceeded: u64 = 2; +const EMaxPoolBorrowPercentageExceeded: u64 = 3; +const EDeepbookPoolAlreadyAllowed: u64 = 4; +const EDeepbookPoolNotAllowed: u64 = 5; +const EInvalidMarginPoolCap: u64 = 6; +const EBorrowAmountTooLow: u64 = 7; +const ERateLimitExceeded: u64 = 8; + +// === Structs === +public struct MarginPool has key, store { + id: UID, + vault: Balance, + state: State, + config: ProtocolConfig, + protocol_fees: ProtocolFees, + positions: PositionManager, + allowed_deepbook_pools: VecSet, + rate_limiter: RateLimiter, + extra_fields: VecMap, +} + +/// A capability that allows a user to supply and withdraw from margin pools. +/// The SupplierCap represents ownership of the shares supplied to the margin pool. +public struct SupplierCap has key, store { + id: UID, +} + +// === Events === +public struct MarginPoolCreated has copy, drop { + margin_pool_id: ID, + maintainer_cap_id: ID, + asset_type: TypeName, + config: ProtocolConfig, + timestamp: u64, +} + +public struct MaintainerFeesWithdrawn has copy, drop { + margin_pool_id: ID, + margin_pool_cap_id: ID, + maintainer_fees: u64, + timestamp: u64, +} + +public struct ProtocolFeesWithdrawn has copy, drop { + margin_pool_id: ID, + protocol_fees: u64, + timestamp: u64, +} + +public struct DeepbookPoolUpdated has copy, drop { + margin_pool_id: ID, + deepbook_pool_id: ID, + pool_cap_id: ID, + enabled: bool, + timestamp: u64, +} + +public struct InterestParamsUpdated has copy, drop { + margin_pool_id: ID, + pool_cap_id: ID, + interest_config: InterestConfig, + timestamp: u64, +} + +public struct MarginPoolConfigUpdated has copy, drop { + margin_pool_id: ID, + pool_cap_id: ID, + margin_pool_config: MarginPoolConfig, + timestamp: u64, +} + +public struct AssetSupplied has copy, drop { + margin_pool_id: ID, + asset_type: TypeName, + supplier_cap_id: ID, + supply_amount: u64, + supply_shares: u64, + timestamp: u64, +} + +public struct AssetWithdrawn has copy, drop { + margin_pool_id: ID, + asset_type: TypeName, + supplier_cap_id: ID, + withdraw_amount: u64, + withdraw_shares: u64, + timestamp: u64, +} + +public struct SupplierCapMinted has copy, drop { + supplier_cap_id: ID, + timestamp: u64, +} + +public struct SupplyReferralMinted has copy, drop { + margin_pool_id: ID, + supply_referral_id: ID, + owner: address, + timestamp: u64, +} + +// === Public Functions * ADMIN *=== +/// Creates and registers a new margin pool. If a same asset pool already exists, abort. +/// Sends a `MarginPoolCap` to the pool creator. Returns the created margin pool id. +public fun create_margin_pool( + registry: &mut MarginRegistry, + config: ProtocolConfig, + maintainer_cap: &MaintainerCap, + clock: &Clock, + ctx: &mut TxContext, +): ID { + let id = object::new(ctx); + let margin_pool_id = id.to_inner(); + let margin_pool = MarginPool { + id, + vault: balance::zero(), + state: margin_state::default(clock), + config, + protocol_fees: protocol_fees::default_protocol_fees(ctx), + positions: position_manager::create_position_manager(ctx), + allowed_deepbook_pools: vec_set::empty(), + rate_limiter: rate_limiter::new( + config.rate_limit_capacity(), + config.rate_limit_refill_rate_per_ms(), + config.rate_limit_enabled(), + clock, + ), + extra_fields: vec_map::empty(), + }; + transfer::share_object(margin_pool); + + let asset_type = type_name::with_defining_ids(); + registry.register_margin_pool(asset_type, margin_pool_id, maintainer_cap, ctx); + + let maintainer_cap_id = maintainer_cap.maintainer_cap_id(); + event::emit(MarginPoolCreated { + margin_pool_id, + maintainer_cap_id, + asset_type, + config, + timestamp: clock.timestamp_ms(), + }); + + margin_pool_id +} + +/// Allow a margin manager tied to a deepbook pool to borrow from the margin pool. +public fun enable_deepbook_pool_for_loan( + self: &mut MarginPool, + registry: &MarginRegistry, + deepbook_pool_id: ID, + margin_pool_cap: &MarginPoolCap, + clock: &Clock, +) { + registry.load_inner(); + assert!(margin_pool_cap.margin_pool_id() == self.id(), EInvalidMarginPoolCap); + assert!(!self.allowed_deepbook_pools.contains(&deepbook_pool_id), EDeepbookPoolAlreadyAllowed); + self.allowed_deepbook_pools.insert(deepbook_pool_id); + + event::emit(DeepbookPoolUpdated { + margin_pool_id: self.id(), + pool_cap_id: margin_pool_cap.pool_cap_id(), + deepbook_pool_id, + enabled: true, + timestamp: clock.timestamp_ms(), + }); +} + +/// Disable a margin manager tied to a deepbook pool from borrowing from the margin pool. +public fun disable_deepbook_pool_for_loan( + self: &mut MarginPool, + registry: &MarginRegistry, + deepbook_pool_id: ID, + margin_pool_cap: &MarginPoolCap, + clock: &Clock, +) { + registry.load_inner(); + assert!(margin_pool_cap.margin_pool_id() == self.id(), EInvalidMarginPoolCap); + assert!(self.allowed_deepbook_pools.contains(&deepbook_pool_id), EDeepbookPoolNotAllowed); + self.allowed_deepbook_pools.remove(&deepbook_pool_id); + + event::emit(DeepbookPoolUpdated { + margin_pool_id: self.id(), + pool_cap_id: margin_pool_cap.pool_cap_id(), + deepbook_pool_id, + enabled: false, + timestamp: clock.timestamp_ms(), + }); +} + +/// Updates interest params for the margin pool +public fun update_interest_params( + self: &mut MarginPool, + registry: &MarginRegistry, + interest_config: InterestConfig, + margin_pool_cap: &MarginPoolCap, + clock: &Clock, +) { + registry.load_inner(); + assert!(margin_pool_cap.margin_pool_id() == self.id(), EInvalidMarginPoolCap); + let margin_pool_id = self.id(); + let protocol_fees = self.state.update(&self.config, clock); + self.protocol_fees.increase_fees_accrued(margin_pool_id, protocol_fees); + self.config.set_interest_config(interest_config); + + event::emit(InterestParamsUpdated { + margin_pool_id, + pool_cap_id: margin_pool_cap.pool_cap_id(), + interest_config, + timestamp: clock.timestamp_ms(), + }); +} + +/// Updates margin pool config +public fun update_margin_pool_config( + self: &mut MarginPool, + registry: &MarginRegistry, + margin_pool_config: MarginPoolConfig, + margin_pool_cap: &MarginPoolCap, + clock: &Clock, +) { + registry.load_inner(); + assert!(margin_pool_cap.margin_pool_id() == self.id(), EInvalidMarginPoolCap); + let margin_pool_id = self.id(); + let protocol_fees = self.state.update(&self.config, clock); + self.protocol_fees.increase_fees_accrued(margin_pool_id, protocol_fees); + self.config.set_margin_pool_config(margin_pool_config); + self + .rate_limiter + .update_config( + margin_pool_config.rate_limit_capacity_from_config(), + margin_pool_config.rate_limit_refill_rate_per_ms_from_config(), + margin_pool_config.rate_limit_enabled_from_config(), + clock, + ); + + event::emit(MarginPoolConfigUpdated { + margin_pool_id: self.id(), + pool_cap_id: margin_pool_cap.pool_cap_id(), + margin_pool_config, + timestamp: clock.timestamp_ms(), + }); +} + +// === Public Functions * LENDING * === +/// Mint a new SupplierCap, which is used to supply and withdraw from margin pools. +/// One SupplierCap can be used to supply and withdraw from multiple margin pools. +public fun mint_supplier_cap( + registry: &MarginRegistry, + clock: &Clock, + ctx: &mut TxContext, +): SupplierCap { + registry.load_inner(); + let id = object::new(ctx); + + event::emit(SupplierCapMinted { + supplier_cap_id: id.to_inner(), + timestamp: clock.timestamp_ms(), + }); + + SupplierCap { id } +} + +/// Supply to the margin pool using a SupplierCap. Returns the new supply shares. +/// The `referral` parameter should be the ID of a SupplyReferral object if referral tracking is desired. +public fun supply( + self: &mut MarginPool, + registry: &MarginRegistry, + supplier_cap: &SupplierCap, + coin: Coin, + referral: Option, + clock: &Clock, +): u64 { + registry.load_inner(); + let margin_pool_id = self.id(); + let supplier_cap_id = supplier_cap.id.to_inner(); + let supply_amount = coin.value(); + let (supply_shares, protocol_fees) = self + .state + .increase_supply(&self.config, supply_amount, clock); + self.protocol_fees.increase_fees_accrued(margin_pool_id, protocol_fees); + + let (total_user_supply_shares, previous_referral) = self + .positions + .increase_user_supply(supplier_cap_id, referral, supply_shares); + + self.protocol_fees.decrease_shares(previous_referral, total_user_supply_shares - supply_shares); + self.protocol_fees.increase_shares(referral, total_user_supply_shares); + + let balance = coin.into_balance(); + self.vault.join(balance); + self.rate_limiter.record_deposit(supply_amount, clock); + + assert!(self.state.total_supply() <= self.config.supply_cap(), ESupplyCapExceeded); + + event::emit(AssetSupplied { + margin_pool_id: self.id(), + asset_type: type_name::with_defining_ids(), + supplier_cap_id, + supply_amount, + supply_shares, + timestamp: clock.timestamp_ms(), + }); + + total_user_supply_shares +} + +/// Withdraw from the margin pool using a SupplierCap. Returns the withdrawn coin. +public fun withdraw( + self: &mut MarginPool, + registry: &MarginRegistry, + supplier_cap: &SupplierCap, + amount: Option, + clock: &Clock, + ctx: &mut TxContext, +): Coin { + registry.load_inner(); + let margin_pool_id = self.id(); + let supplier_cap_id = supplier_cap.id.to_inner(); + let supplied_shares = self.positions.user_supply_shares(supplier_cap_id); + let supplied_amount = self.state.supply_shares_to_amount(supplied_shares, &self.config, clock); + let withdraw_amount = amount.destroy_with_default(supplied_amount); + let withdraw_shares = math::mul_round_up( + supplied_shares, + math::div_round_up(withdraw_amount, supplied_amount), + ); + assert!( + self.rate_limiter.check_and_record_withdrawal(withdraw_amount, clock), + ERateLimitExceeded, + ); + + let (_, protocol_fees) = self + .state + .decrease_supply_shares(&self.config, withdraw_shares, clock); + self.protocol_fees.increase_fees_accrued(margin_pool_id, protocol_fees); + + let (_, previous_referral) = self + .positions + .decrease_user_supply(supplier_cap_id, withdraw_shares); + + self.protocol_fees.decrease_shares(previous_referral, withdraw_shares); + assert!(withdraw_amount <= self.vault.value(), ENotEnoughAssetInPool); + let coin = self.vault.split(withdraw_amount).into_coin(ctx); + + event::emit(AssetWithdrawn { + margin_pool_id: self.id(), + asset_type: type_name::with_defining_ids(), + supplier_cap_id, + withdraw_amount, + withdraw_shares, + timestamp: clock.timestamp_ms(), + }); + + coin +} + +/// Mint a supply referral. +public fun mint_supply_referral( + self: &mut MarginPool, + registry: &MarginRegistry, + clock: &Clock, + ctx: &mut TxContext, +): ID { + registry.load_inner(); + let supply_referral_id = self.protocol_fees.mint_supply_referral(ctx); + + event::emit(SupplyReferralMinted { + margin_pool_id: self.id(), + supply_referral_id, + owner: ctx.sender(), + timestamp: clock.timestamp_ms(), + }); + + supply_referral_id +} + +/// Withdraw the referral fees. +public fun withdraw_referral_fees( + self: &mut MarginPool, + registry: &MarginRegistry, + referral: &SupplyReferral, + ctx: &mut TxContext, +): Coin { + registry.load_inner(); + let referral_fees = self.protocol_fees.calculate_and_claim(referral, ctx); + let coin = self.vault.split(referral_fees).into_coin(ctx); + + coin +} + +/// Withdraw the default referral fees (admin only). +/// The default referral at 0x0 doesn't have a SupplyReferral object, +public fun admin_withdraw_default_referral_fees( + self: &mut MarginPool, + registry: &MarginRegistry, + _admin_cap: &MarginAdminCap, + ctx: &mut TxContext, +): Coin { + registry.load_inner(); + let referral_fees = self.protocol_fees.claim_default_referral_fees(); + let coin = self.vault.split(referral_fees).into_coin(ctx); + + coin +} + +/// Withdraw the maintainer fees. +/// The `margin_pool_cap` parameter is used to ensure the correct margin pool is being withdrawn from. +public fun withdraw_maintainer_fees( + self: &mut MarginPool, + registry: &MarginRegistry, + margin_pool_cap: &MarginPoolCap, + clock: &Clock, + ctx: &mut TxContext, +): Coin { + registry.load_inner(); + assert!(margin_pool_cap.margin_pool_id() == self.id(), EInvalidMarginPoolCap); + let maintainer_fees = self.protocol_fees.claim_maintainer_fees(); + let coin = self.vault.split(maintainer_fees).into_coin(ctx); + + event::emit(MaintainerFeesWithdrawn { + margin_pool_id: self.id(), + margin_pool_cap_id: margin_pool_cap.pool_cap_id(), + maintainer_fees, + timestamp: clock.timestamp_ms(), + }); + + coin +} + +/// Withdraw the protocol fees. +public fun withdraw_protocol_fees( + self: &mut MarginPool, + registry: &MarginRegistry, + _admin_cap: &MarginAdminCap, + clock: &Clock, + ctx: &mut TxContext, +): Coin { + registry.load_inner(); + let protocol_fees = self.protocol_fees.claim_protocol_fees(); + let coin = self.vault.split(protocol_fees).into_coin(ctx); + + event::emit(ProtocolFeesWithdrawn { + margin_pool_id: self.id(), + protocol_fees, + timestamp: clock.timestamp_ms(), + }); + + coin +} + +// === Public-View Functions === +/// Return the ID of the margin pool. +public fun id(self: &MarginPool): ID { + self.id.to_inner() +} + +/// Return whether a margin manager for a given deepbook pool is allowed to borrow from the margin pool. +public fun deepbook_pool_allowed(self: &MarginPool, deepbook_pool_id: ID): bool { + self.allowed_deepbook_pools.contains(&deepbook_pool_id) +} + +/// Return the current total supply of the margin pool. +public fun total_supply(self: &MarginPool): u64 { + self.state.total_supply() +} + +/// Return the current total supply of the margin pool including accrued interest. +public fun total_supply_with_interest(self: &MarginPool, clock: &Clock): u64 { + self.state.total_supply_with_interest(&self.config, clock) +} + +/// Return the current total supply shares of the margin pool. +public fun supply_shares(self: &MarginPool): u64 { + self.state.supply_shares() +} + +/// Return the current supply ratio of the margin pool. +public fun supply_ratio(self: &MarginPool): u64 { + self.state.supply_ratio() +} + +/// Return the current total borrow of the margin pool. +public fun total_borrow(self: &MarginPool): u64 { + self.state.total_borrow() +} + +/// Return the current total borrow shares of the margin pool. +public fun borrow_shares(self: &MarginPool): u64 { + self.state.borrow_shares() +} + +/// Return the current borrow ratio of the margin pool. +public fun borrow_ratio(self: &MarginPool): u64 { + self.state.borrow_ratio() +} + +/// Return the last update timestamp of the margin pool. +public fun last_update_timestamp(self: &MarginPool): u64 { + self.state.last_update_timestamp() +} + +/// Return the supply cap of the margin pool. +public fun supply_cap(self: &MarginPool): u64 { + self.config.supply_cap() +} + +/// Return the current protocol fees of the margin pool. +public fun protocol_fees(self: &MarginPool): &ProtocolFees { + &self.protocol_fees +} + +/// Return the current max utilization rate of the margin pool. +public fun max_utilization_rate(self: &MarginPool): u64 { + self.config.max_utilization_rate() +} + +/// Return the current protocol spread of the margin pool. +public fun protocol_spread(self: &MarginPool): u64 { + self.config.protocol_spread() +} + +/// Return the current min borrow of the margin pool. +public fun min_borrow(self: &MarginPool): u64 { + self.config.min_borrow() +} + +/// Return the current interest rate of the margin pool. Represented in 9 decimal places. +public fun interest_rate(self: &MarginPool): u64 { + self.config.interest_rate(self.state.utilization_rate()) +} + +public fun true_interest_rate(self: &MarginPool): u64 { + math::mul( + math::mul(self.interest_rate(), self.state.utilization_rate()), + constants::float_scaling() - self.protocol_spread(), + ) +} + +/// Return the current user supply shares of the margin pool. +public fun user_supply_shares(self: &MarginPool, supplier_cap_id: ID): u64 { + self.positions.user_supply_shares(supplier_cap_id) +} + +/// Return the current vault balance of the margin pool. +public fun vault_balance(self: &MarginPool): u64 { + self.vault.value() +} + +/// Return the current user supply amount of the margin pool. +public fun user_supply_amount( + self: &MarginPool, + supplier_cap_id: ID, + clock: &Clock, +): u64 { + self + .state + .supply_shares_to_amount(self.user_supply_shares(supplier_cap_id), &self.config, clock) +} + +// === Public-Package Functions === +/// Allows borrowing from the margin pool. Returns the borrowed coin, and individual borrow shares for this loan. +public(package) fun borrow( + self: &mut MarginPool, + amount: u64, + clock: &Clock, + ctx: &mut TxContext, +): (Coin, u64) { + assert!(amount <= self.vault.value(), ENotEnoughAssetInPool); + assert!(amount >= self.config.min_borrow(), EBorrowAmountTooLow); + let margin_pool_id = self.id(); + let (individual_borrow_shares, protocol_fees) = self + .state + .increase_borrow(&self.config, amount, clock); + self.protocol_fees.increase_fees_accrued(margin_pool_id, protocol_fees); + assert!( + self.state.utilization_rate() <= self.config.max_utilization_rate(), + EMaxPoolBorrowPercentageExceeded, + ); + + (self.vault.split(amount).into_coin(ctx), individual_borrow_shares) +} + +public(package) fun repay( + self: &mut MarginPool, + shares: u64, + coin: Coin, + clock: &Clock, +) { + let margin_pool_id = self.id(); + let (_, protocol_fees) = self.state.decrease_borrow_shares(&self.config, shares, clock); + self.protocol_fees.increase_fees_accrued(margin_pool_id, protocol_fees); + + self.vault.join(coin.into_balance()); +} + +// Repay a liquidation given some quantity of shares and a coin. If too much coin is given, then extra is used as reward. +// If not enough coin given, then the difference is recorded as default. +// Returns (applied amount repaid, reward given, and default recorded). +public(package) fun repay_liquidation( + self: &mut MarginPool, + shares: u64, + coin: Coin, + clock: &Clock, +): (u64, u64, u64) { + let margin_pool_id = self.id(); + let (amount, protocol_fees) = self.state.decrease_borrow_shares(&self.config, shares, clock); // decreased 48.545 shares, 97.087 USDC + self.protocol_fees.increase_fees_accrued(margin_pool_id, protocol_fees); + let coin_value = coin.value(); // 100 USDC + let (reward, default) = if (coin_value > amount) { + self.state.increase_supply_absolute(coin_value - amount); + (coin_value - amount, 0) + } else { + self.state.decrease_supply_absolute(amount - coin_value); + (0, amount - coin_value) + }; + self.vault.join(coin.into_balance()); + + (amount, reward, default) +} + +public(package) fun borrow_shares_to_amount( + self: &MarginPool, + shares: u64, + clock: &Clock, +): u64 { + self.state.borrow_shares_to_amount(shares, &self.config, clock) +} + +/// Returns the maximum amount that can be withdrawn without hitting rate limits +public fun get_available_withdrawal(self: &MarginPool, clock: &Clock): u64 { + self.rate_limiter.get_available_withdrawal(clock) +} + +/// Returns whether rate limiting is enabled +public fun is_rate_limit_enabled(self: &MarginPool): bool { + self.rate_limiter.is_enabled() +} + +/// Returns the rate limit capacity (max bucket size) +public fun rate_limit_capacity(self: &MarginPool): u64 { + self.rate_limiter.capacity() +} + +/// Returns the rate limit refill rate per millisecond +public fun rate_limit_refill_rate_per_ms(self: &MarginPool): u64 { + self.rate_limiter.refill_rate_per_ms() +} diff --git a/packages/deepbook_margin/sources/margin_pool/margin_state.move b/packages/deepbook_margin/sources/margin_pool/margin_state.move new file mode 100644 index 000000000..5a639742f --- /dev/null +++ b/packages/deepbook_margin/sources/margin_pool/margin_state.move @@ -0,0 +1,257 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +/// Margin state manages the total supply and borrow of the margin pool. +/// Whenever supply and borrow increases or decreases, +/// the interest and protocol fees are updated. +/// Shares represent the constant amount and are used to calculate +/// amounts after interest and protocol fees are applied. +module deepbook_margin::margin_state; + +use deepbook::{constants, math}; +use deepbook_margin::protocol_config::ProtocolConfig; +use std::string::String; +use sui::{clock::Clock, vec_map::{Self, VecMap}}; + +public struct State has drop, store { + total_supply: u64, + total_borrow: u64, + supply_shares: u64, + borrow_shares: u64, + last_update_timestamp: u64, + extra_fields: VecMap, +} + +// === Public-Package Functions === +/// Initialize the margin state with the default values. +public(package) fun default(clock: &Clock): State { + State { + total_supply: 0, + total_borrow: 0, + supply_shares: 0, + borrow_shares: 0, + last_update_timestamp: clock.timestamp_ms(), + extra_fields: vec_map::empty(), + } +} + +/// Increase the supply given an amount. Return the corresponding shares +/// and protocol fees accrued since last update. +public(package) fun increase_supply( + self: &mut State, + config: &ProtocolConfig, + amount: u64, + clock: &Clock, +): (u64, u64) { + let protocol_fees = self.update(config, clock); + let ratio = self.supply_ratio(); + let shares = math::div(amount, ratio); + self.supply_shares = self.supply_shares + shares; + self.total_supply = self.total_supply + amount; + + (shares, protocol_fees) +} + +/// Decrease the supply given some shares. Return the corresponding amount +/// and protocol fees accrued since last update. +public(package) fun decrease_supply_shares( + self: &mut State, + config: &ProtocolConfig, + shares: u64, + clock: &Clock, +): (u64, u64) { + let protocol_fees = self.update(config, clock); + let ratio = self.supply_ratio(); + let amount = math::mul(shares, ratio); + self.supply_shares = self.supply_shares - shares; + self.total_supply = self.total_supply - amount; + + (amount, protocol_fees) +} + +/// Increase the supply given an absolute amount. Used when the supply needs to be +/// increased without increasing shares. +public(package) fun increase_supply_absolute(self: &mut State, amount: u64) { + self.total_supply = self.total_supply + amount; +} + +/// Decrease the supply given an absolute amount. Used when the supply needs to be +/// decreased without decreasing shares. +public(package) fun decrease_supply_absolute(self: &mut State, amount: u64) { + self.total_supply = self.total_supply - amount; +} + +/// Increase the borrow given an amount. Return the individual borrow shares +/// and protocol fees accrued since last update. +public(package) fun increase_borrow( + self: &mut State, + config: &ProtocolConfig, + amount: u64, + clock: &Clock, +): (u64, u64) { + let protocol_fees = self.update(config, clock); + let ratio = self.borrow_ratio(); + let shares = math::div_round_up(amount, ratio); + self.borrow_shares = self.borrow_shares + shares; + self.total_borrow = self.total_borrow + amount; + + (shares, protocol_fees) +} + +/// Decrease the borrow given some shares. Return the corresponding amount +/// and protocol fees accrued since last update. +public(package) fun decrease_borrow_shares( + self: &mut State, + config: &ProtocolConfig, + shares: u64, + clock: &Clock, +): (u64, u64) { + let protocol_fees = self.update(config, clock); + let ratio = self.borrow_ratio(); + let amount = math::mul(shares, ratio); + self.borrow_shares = self.borrow_shares - shares; + self.total_borrow = self.total_borrow - amount; + + (amount, protocol_fees) +} + +/// Update the supply and borrow with the interest and protocol fees. +/// Returns the protocol fees accrued since last update. +public(package) fun update(self: &mut State, config: &ProtocolConfig, clock: &Clock): u64 { + let now = clock.timestamp_ms(); + let elapsed = now - self.last_update_timestamp; + + let interest = config.calculate_interest_with_borrow( + self.utilization_rate(), + elapsed, + self.total_borrow, + ); + let protocol_fees = math::mul(interest, config.protocol_spread()); + self.total_supply = self.total_supply + interest - protocol_fees; + self.total_borrow = self.total_borrow + interest; + self.last_update_timestamp = now; + + protocol_fees +} + +/// Return the utilization rate of the margin pool. +public(package) fun utilization_rate(self: &State): u64 { + if (self.total_supply == 0) { + 0 + } else { + math::div(self.total_borrow, self.total_supply) + } +} + +/// Convert the supply shares to the corresponding amount. +public(package) fun supply_shares_to_amount( + self: &State, + shares: u64, + config: &ProtocolConfig, + clock: &Clock, +): u64 { + let now = clock.timestamp_ms(); + let elapsed = now - self.last_update_timestamp; + + let interest = config.calculate_interest_with_borrow( + self.utilization_rate(), + elapsed, + self.total_borrow, + ); + let protocol_fees = math::mul(interest, config.protocol_spread()); + let supply = self.total_supply + interest - protocol_fees; + let ratio = if (self.supply_shares == 0) { + constants::float_scaling() + } else { + math::div(supply, self.supply_shares) + }; + + math::mul(shares, ratio) +} + +/// Convert the borrow shares to the corresponding amount. +public(package) fun borrow_shares_to_amount( + self: &State, + shares: u64, + config: &ProtocolConfig, + clock: &Clock, +): u64 { + let now = clock.timestamp_ms(); + let elapsed = now - self.last_update_timestamp; + + let interest = config.calculate_interest_with_borrow( + self.utilization_rate(), + elapsed, + self.total_borrow, + ); + let borrow = self.total_borrow + interest; + let ratio = if (self.borrow_shares == 0) { + constants::float_scaling() + } else { + math::div(borrow, self.borrow_shares) + }; + + math::mul_round_up(shares, ratio) +} + +/// Return the supply ratio of the margin pool. +public(package) fun supply_ratio(self: &State): u64 { + if (self.supply_shares == 0) { + constants::float_scaling() + } else { + math::div(self.total_supply, self.supply_shares) + } +} + +/// Return the borrow ratio of the margin pool. +public(package) fun borrow_ratio(self: &State): u64 { + if (self.borrow_shares == 0) { + constants::float_scaling() + } else { + math::div(self.total_borrow, self.borrow_shares) + } +} + +/// Return the total supply of the margin pool. +public(package) fun total_supply(self: &State): u64 { + self.total_supply +} + +/// Return the total supply including accrued interest without updating state. +public(package) fun total_supply_with_interest( + self: &State, + config: &ProtocolConfig, + clock: &Clock, +): u64 { + let now = clock.timestamp_ms(); + let elapsed = now - self.last_update_timestamp; + + let interest = config.calculate_interest_with_borrow( + self.utilization_rate(), + elapsed, + self.total_borrow, + ); + let protocol_fees = math::mul(interest, config.protocol_spread()); + + self.total_supply + interest - protocol_fees +} + +/// Return the total supply shares of the margin pool. +public(package) fun supply_shares(self: &State): u64 { + self.supply_shares +} + +/// Return the total borrow of the margin pool. +public(package) fun total_borrow(self: &State): u64 { + self.total_borrow +} + +/// Return the total borrow shares of the margin pool. +public(package) fun borrow_shares(self: &State): u64 { + self.borrow_shares +} + +/// Return the last update timestamp of the margin pool. +public(package) fun last_update_timestamp(self: &State): u64 { + self.last_update_timestamp +} diff --git a/packages/deepbook_margin/sources/margin_pool/position_manager.move b/packages/deepbook_margin/sources/margin_pool/position_manager.move new file mode 100644 index 000000000..f64abdc39 --- /dev/null +++ b/packages/deepbook_margin/sources/margin_pool/position_manager.move @@ -0,0 +1,83 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +/// Position manager is responsible for managing users' positions. +/// It is used to track the supply and loan shares of the users. +module deepbook_margin::position_manager; + +use std::string::String; +use sui::{table::{Self, Table}, vec_map::{Self, VecMap}}; + +public struct PositionManager has store { + positions: Table, + extra_fields: VecMap, +} + +public struct Position has store { + shares: u64, + referral: Option, +} + +// === Public-Package Functions === +/// Initialize the position manager. +public(package) fun create_position_manager(ctx: &mut TxContext): PositionManager { + PositionManager { + positions: table::new(ctx), + extra_fields: vec_map::empty(), + } +} + +/// Increase the supply shares of the user and return outstanding supply shares. +/// Returns the new total supply shares and the previous referral. +public(package) fun increase_user_supply( + self: &mut PositionManager, + supplier_cap_id: ID, + referral: Option, + supply_shares: u64, +): (u64, Option) { + self.add_supply_entry(supplier_cap_id, referral); + let user_position = self.positions.borrow_mut(supplier_cap_id); + let previous_referral = user_position.referral; + user_position.shares = user_position.shares + supply_shares; + user_position.referral = referral; + + (user_position.shares, previous_referral) +} + +/// Decrease the supply shares of the user and return outstanding supply shares. +public(package) fun decrease_user_supply( + self: &mut PositionManager, + supplier_cap_id: ID, + supply_shares: u64, +): (u64, Option) { + let user_position = self.positions.borrow_mut(supplier_cap_id); + user_position.shares = user_position.shares - supply_shares; + + (user_position.shares, user_position.referral) +} + +public(package) fun add_supply_entry( + self: &mut PositionManager, + supplier_cap_id: ID, + referral: Option, +) { + if (!self.positions.contains(supplier_cap_id)) { + self + .positions + .add( + supplier_cap_id, + Position { + shares: 0, + referral, + }, + ); + } +} + +public(package) fun user_supply_shares(self: &PositionManager, supplier_cap_id: ID): u64 { + if (self.positions.contains(supplier_cap_id)) { + self.positions.borrow(supplier_cap_id).shares + } else { + 0 + } +} diff --git a/packages/deepbook_margin/sources/margin_pool/protocol_config.move b/packages/deepbook_margin/sources/margin_pool/protocol_config.move new file mode 100644 index 000000000..463ed135b --- /dev/null +++ b/packages/deepbook_margin/sources/margin_pool/protocol_config.move @@ -0,0 +1,219 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +module deepbook_margin::protocol_config; + +use deepbook::{constants, math}; +use deepbook_margin::margin_constants; +use std::string::String; +use sui::vec_map::{Self, VecMap}; + +const EInvalidRiskParam: u64 = 1; + +public struct ProtocolConfig has copy, drop, store { + margin_pool_config: MarginPoolConfig, + interest_config: InterestConfig, + extra_fields: VecMap, +} + +public struct MarginPoolConfig has copy, drop, store { + supply_cap: u64, + max_utilization_rate: u64, + protocol_spread: u64, + min_borrow: u64, + rate_limit_capacity: u64, + rate_limit_refill_rate_per_ms: u64, + rate_limit_enabled: bool, +} + +public struct InterestConfig has copy, drop, store { + base_rate: u64, + base_slope: u64, + optimal_utilization: u64, + excess_slope: u64, +} + +public fun new_protocol_config( + margin_pool_config: MarginPoolConfig, + interest_config: InterestConfig, +): ProtocolConfig { + // Validate cross-config constraints + assert!( + margin_pool_config.max_utilization_rate >= interest_config.optimal_utilization, + EInvalidRiskParam, + ); + + ProtocolConfig { + margin_pool_config, + interest_config, + extra_fields: vec_map::empty(), + } +} + +public fun new_margin_pool_config( + supply_cap: u64, + max_utilization_rate: u64, + protocol_spread: u64, + min_borrow: u64, +): MarginPoolConfig { + // Validate margin pool config parameters + assert!(max_utilization_rate <= constants::float_scaling(), EInvalidRiskParam); + assert!(min_borrow >= margin_constants::min_min_borrow(), EInvalidRiskParam); + assert!(protocol_spread <= margin_constants::max_protocol_spread(), EInvalidRiskParam); + + let default_capacity = supply_cap / 10; // 10% of supply cap + let default_window_ms = margin_constants::day_ms(); + let default_refill_rate = default_capacity / default_window_ms; + + MarginPoolConfig { + supply_cap, + max_utilization_rate, + protocol_spread, + min_borrow, + rate_limit_capacity: default_capacity, + rate_limit_refill_rate_per_ms: default_refill_rate, + rate_limit_enabled: false, + } +} + +public fun new_margin_pool_config_with_rate_limit( + supply_cap: u64, + max_utilization_rate: u64, + protocol_spread: u64, + min_borrow: u64, + rate_limit_capacity: u64, + rate_limit_refill_rate_per_ms: u64, + rate_limit_enabled: bool, +): MarginPoolConfig { + MarginPoolConfig { + supply_cap, + max_utilization_rate, + protocol_spread, + min_borrow, + rate_limit_capacity, + rate_limit_refill_rate_per_ms, + rate_limit_enabled, + } +} + +public fun new_interest_config( + base_rate: u64, + base_slope: u64, + optimal_utilization: u64, + excess_slope: u64, +): InterestConfig { + // Validate interest config parameters + assert!(optimal_utilization <= constants::float_scaling(), EInvalidRiskParam); + + InterestConfig { + base_rate, + base_slope, + optimal_utilization, + excess_slope, + } +} + +public(package) fun set_interest_config(self: &mut ProtocolConfig, config: InterestConfig) { + assert!( + self.margin_pool_config.max_utilization_rate >= config.optimal_utilization, + EInvalidRiskParam, + ); + self.interest_config = config; +} + +public(package) fun set_margin_pool_config(self: &mut ProtocolConfig, config: MarginPoolConfig) { + assert!( + config.max_utilization_rate >= self.interest_config.optimal_utilization, + EInvalidRiskParam, + ); + self.margin_pool_config = config; +} + +/// Calculate interest directly with borrow amount to avoid precision loss +public(package) fun calculate_interest_with_borrow( + self: &ProtocolConfig, + utilization_rate: u64, + time_elapsed: u64, + total_borrow: u64, +): u64 { + let interest_rate = self.interest_rate(utilization_rate); + + math::mul( + math::mul(total_borrow, interest_rate), + math::div(time_elapsed, margin_constants::year_ms()), + ) +} + +public(package) fun interest_rate(self: &ProtocolConfig, utilization_rate: u64): u64 { + let base_rate = self.interest_config.base_rate; + let base_slope = self.interest_config.base_slope; + let optimal_utilization = self.interest_config.optimal_utilization; + let excess_slope = self.interest_config.excess_slope; + + if (utilization_rate < optimal_utilization) { + // Use base slope + math::mul(utilization_rate, base_slope) + base_rate + } else { + // Use base slope and excess slope + let excess_utilization = utilization_rate - optimal_utilization; + let excess_rate = math::mul(excess_utilization, excess_slope); + + base_rate + math::mul(optimal_utilization, base_slope) + excess_rate + } +} + +public(package) fun supply_cap(self: &ProtocolConfig): u64 { + self.margin_pool_config.supply_cap +} + +public(package) fun max_utilization_rate(self: &ProtocolConfig): u64 { + self.margin_pool_config.max_utilization_rate +} + +public(package) fun protocol_spread(self: &ProtocolConfig): u64 { + self.margin_pool_config.protocol_spread +} + +public(package) fun min_borrow(self: &ProtocolConfig): u64 { + self.margin_pool_config.min_borrow +} + +public(package) fun base_rate(self: &ProtocolConfig): u64 { + self.interest_config.base_rate +} + +public(package) fun base_slope(self: &ProtocolConfig): u64 { + self.interest_config.base_slope +} + +public(package) fun optimal_utilization(self: &ProtocolConfig): u64 { + self.interest_config.optimal_utilization +} + +public(package) fun excess_slope(self: &ProtocolConfig): u64 { + self.interest_config.excess_slope +} + +public(package) fun rate_limit_capacity(self: &ProtocolConfig): u64 { + self.margin_pool_config.rate_limit_capacity +} + +public(package) fun rate_limit_refill_rate_per_ms(self: &ProtocolConfig): u64 { + self.margin_pool_config.rate_limit_refill_rate_per_ms +} + +public(package) fun rate_limit_enabled(self: &ProtocolConfig): bool { + self.margin_pool_config.rate_limit_enabled +} + +public(package) fun rate_limit_capacity_from_config(config: &MarginPoolConfig): u64 { + config.rate_limit_capacity +} + +public(package) fun rate_limit_refill_rate_per_ms_from_config(config: &MarginPoolConfig): u64 { + config.rate_limit_refill_rate_per_ms +} + +public(package) fun rate_limit_enabled_from_config(config: &MarginPoolConfig): bool { + config.rate_limit_enabled +} diff --git a/packages/deepbook_margin/sources/margin_pool/protocol_fees.move b/packages/deepbook_margin/sources/margin_pool/protocol_fees.move new file mode 100644 index 000000000..c771031f5 --- /dev/null +++ b/packages/deepbook_margin/sources/margin_pool/protocol_fees.move @@ -0,0 +1,235 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +module deepbook_margin::protocol_fees; + +use deepbook::math; +use deepbook_margin::margin_constants; +use std::string::String; +use sui::{event, table::{Self, Table}, vec_map::{Self, VecMap}}; + +// === Errors === +const ENotOwner: u64 = 1; + +// === Structs === +public struct ProtocolFees has store { + referrals: Table, + total_shares: u64, + fees_per_share: u64, + maintainer_fees: u64, + protocol_fees: u64, + extra_fields: VecMap, +} + +public struct ReferralTracker has store { + current_shares: u64, + last_fees_per_share: u64, + unclaimed_fees: u64, +} + +public struct SupplyReferral has key { + id: UID, + owner: address, +} + +public struct ProtocolFeesIncreasedEvent has copy, drop { + margin_pool_id: ID, + total_shares: u64, + referral_fees: u64, + maintainer_fees: u64, + protocol_fees: u64, +} + +public struct ReferralFeesClaimedEvent has copy, drop { + referral_id: ID, + owner: address, + fees: u64, +} + +/// Get the maintainer fees. +public fun maintainer_fees(self: &ProtocolFees): u64 { + self.maintainer_fees +} + +/// Get the protocol fees. +public fun protocol_fees(self: &ProtocolFees): u64 { + self.protocol_fees +} + +public fun referral_tracker(self: &ProtocolFees, referral: ID): (u64, u64) { + let referral_tracker = self.referrals.borrow(referral); + let fees_per_share_delta = self.fees_per_share - referral_tracker.last_fees_per_share; + let unclaimed_fees = math::mul(referral_tracker.current_shares, fees_per_share_delta); + (referral_tracker.current_shares, referral_tracker.unclaimed_fees + unclaimed_fees) +} + +public fun total_shares(self: &ProtocolFees): u64 { + self.total_shares +} + +public fun fees_per_share(self: &ProtocolFees): u64 { + self.fees_per_share +} + +// Initialize the referral fees with the default referral. +public(package) fun default_protocol_fees(ctx: &mut TxContext): ProtocolFees { + let default_id = margin_constants::default_referral(); + let mut manager = ProtocolFees { + referrals: table::new(ctx), + total_shares: 0, + fees_per_share: 0, + maintainer_fees: 0, + protocol_fees: 0, + extra_fields: vec_map::empty(), + }; + manager + .referrals + .add( + default_id, + ReferralTracker { + current_shares: 0, + last_fees_per_share: 0, + unclaimed_fees: 0, + }, + ); + + manager +} + +/// Mint a referral object. +public(package) fun mint_supply_referral(self: &mut ProtocolFees, ctx: &mut TxContext): ID { + let id = object::new(ctx); + let id_inner = id.to_inner(); + self + .referrals + .add( + id_inner, + ReferralTracker { + current_shares: 0, + last_fees_per_share: self.fees_per_share, + unclaimed_fees: 0, + }, + ); + let referral = SupplyReferral { + id, + owner: ctx.sender(), + }; + transfer::share_object(referral); + + id_inner +} + +/// Increase the fees per share. Given the current fees earned, divide it by current outstanding shares. +/// Half of fees goes to referrals, quarter to maintainer, quarter to protocol. +/// If there are no shares (no suppliers), referral fees are redistributed to maintainer and protocol. +public(package) fun increase_fees_accrued( + self: &mut ProtocolFees, + margin_pool_id: ID, + fees_accrued: u64, +) { + if (fees_accrued == 0) return; + let protocol_fees = fees_accrued / 4; + let maintainer_fees = fees_accrued / 4; + let referral_fees = fees_accrued - protocol_fees - maintainer_fees; + + if (self.total_shares > 0) { + let fees_per_share_increase = math::div(referral_fees, self.total_shares); + self.fees_per_share = self.fees_per_share + fees_per_share_increase; + self.maintainer_fees = self.maintainer_fees + maintainer_fees; + self.protocol_fees = self.protocol_fees + protocol_fees; + } else { + self.maintainer_fees = self.maintainer_fees + maintainer_fees + referral_fees / 2; + self.protocol_fees = + self.protocol_fees + protocol_fees + (referral_fees - referral_fees / 2); + }; + + event::emit(ProtocolFeesIncreasedEvent { + margin_pool_id, + total_shares: self.total_shares, + referral_fees, + maintainer_fees, + protocol_fees, + }); +} + +/// Increase the shares for a referral. +public(package) fun increase_shares(self: &mut ProtocolFees, referral: Option, shares: u64) { + let referral_id = referral.destroy_with_default(margin_constants::default_referral()); + let referral_tracker = self.referrals.borrow_mut(referral_id); + referral_tracker.update_unclaimed_fees(self.fees_per_share); + + referral_tracker.current_shares = referral_tracker.current_shares + shares; + self.total_shares = self.total_shares + shares; +} + +/// Decrease the shares for a referral. +public(package) fun decrease_shares(self: &mut ProtocolFees, referral: Option, shares: u64) { + let referral_id = referral.destroy_with_default(margin_constants::default_referral()); + let referral_tracker = self.referrals.borrow_mut(referral_id); + referral_tracker.update_unclaimed_fees(self.fees_per_share); + + referral_tracker.current_shares = referral_tracker.current_shares - shares; + self.total_shares = self.total_shares - shares; +} + +/// Calculate the fees for a referral and claim them. Multiply the referred shares by the fees per share delta. +/// Referred fees is set to the minimum of the current and referred shares. +public(package) fun calculate_and_claim( + self: &mut ProtocolFees, + referral: &SupplyReferral, + ctx: &TxContext, +): u64 { + assert!(ctx.sender() == referral.owner, ENotOwner); + + let referral_tracker = self.referrals.borrow_mut(referral.id.to_inner()); + referral_tracker.update_unclaimed_fees(self.fees_per_share); + let fees = referral_tracker.unclaimed_fees; + referral_tracker.unclaimed_fees = 0; + + event::emit(ReferralFeesClaimedEvent { + referral_id: referral.id.to_inner(), + owner: referral.owner, + fees, + }); + + fees +} + +/// Claim the default referral fees (admin only). +/// The default referral at 0x0 doesn't have a SupplyReferral object, so admin must claim these fees. +public(package) fun claim_default_referral_fees(self: &mut ProtocolFees): u64 { + let default_id = margin_constants::default_referral(); + let referral_tracker = self.referrals.borrow_mut(default_id); + referral_tracker.update_unclaimed_fees(self.fees_per_share); + let fees = referral_tracker.unclaimed_fees; + referral_tracker.unclaimed_fees = 0; + + event::emit(ReferralFeesClaimedEvent { + referral_id: default_id, + owner: default_id.to_address(), + fees, + }); + + fees +} + +/// Claim the maintainer fees. +public(package) fun claim_maintainer_fees(self: &mut ProtocolFees): u64 { + let fees = self.maintainer_fees; + self.maintainer_fees = 0; + fees +} + +/// Claim the protocol fees. +public(package) fun claim_protocol_fees(self: &mut ProtocolFees): u64 { + let fees = self.protocol_fees; + self.protocol_fees = 0; + fees +} + +fun update_unclaimed_fees(referral: &mut ReferralTracker, fees_per_share: u64) { + let fees_per_share_delta = fees_per_share - referral.last_fees_per_share; + let unclaimed_fees = math::mul(referral.current_shares, fees_per_share_delta); + referral.unclaimed_fees = referral.unclaimed_fees + unclaimed_fees; + referral.last_fees_per_share = fees_per_share; +} diff --git a/packages/deepbook_margin/sources/margin_registry.move b/packages/deepbook_margin/sources/margin_registry.move new file mode 100644 index 000000000..9aa7fe98c --- /dev/null +++ b/packages/deepbook_margin/sources/margin_registry.move @@ -0,0 +1,719 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +/// Registry holds all margin pools. +module deepbook_margin::margin_registry; + +use deepbook::{constants, math, pool::Pool}; +use deepbook_margin::margin_constants; +use std::{string::String, type_name::{Self, TypeName}}; +use sui::{ + clock::Clock, + dynamic_field as df, + event, + table::{Self, Table}, + vec_map::{Self, VecMap}, + vec_set::{Self, VecSet}, + versioned::{Self, Versioned} +}; + +use fun df::add as UID.add; +use fun df::borrow as UID.borrow; +use fun df::remove as UID.remove; + +// === Errors === +const EInvalidRiskParam: u64 = 1; +const EPoolAlreadyRegistered: u64 = 2; +const EPoolNotRegistered: u64 = 3; +const EPoolNotEnabled: u64 = 4; +const EPoolAlreadyEnabled: u64 = 5; +const EPoolAlreadyDisabled: u64 = 6; +const EMarginPoolAlreadyExists: u64 = 7; +const EMarginPoolDoesNotExists: u64 = 8; +const EMaintainerCapNotValid: u64 = 9; +const EPackageVersionDisabled: u64 = 10; +const EVersionAlreadyEnabled: u64 = 11; +const EVersionNotEnabled: u64 = 12; +const EMaxMarginManagersReached: u64 = 13; +const EPauseCapNotValid: u64 = 14; +const EMarginManagerNotRegistered: u64 = 15; + +public struct MARGIN_REGISTRY has drop {} + +// === Structs === +public struct MarginRegistry has key { + id: UID, + inner: Versioned, +} + +public struct MarginRegistryInner has store { + registry_id: ID, + allowed_versions: VecSet, + pool_registry: Table, + margin_pools: Table, + margin_managers: Table>, + allowed_maintainers: VecSet, + allowed_pause_caps: VecSet, +} + +public struct PoolConfig has copy, drop, store { + base_margin_pool_id: ID, + quote_margin_pool_id: ID, + risk_ratios: RiskRatios, + user_liquidation_reward: u64, // fractional reward for liquidating a position, in 9 decimals + pool_liquidation_reward: u64, // fractional reward for the pool, in 9 decimals + enabled: bool, // whether the pool is enabled for margin trading + extra_fields: VecMap, +} + +public struct RiskRatios has copy, drop, store { + min_withdraw_risk_ratio: u64, + min_borrow_risk_ratio: u64, + liquidation_risk_ratio: u64, + target_liquidation_risk_ratio: u64, +} + +public struct ConfigKey has copy, drop, store {} + +// === Caps === +public struct MarginAdminCap has key, store { + id: UID, +} + +public struct MarginPauseCap has key, store { + id: UID, +} + +public struct MaintainerCap has key, store { + id: UID, +} + +public struct MarginPoolCap has key, store { + id: UID, + margin_pool_id: ID, +} + +// === Events === +public struct MaintainerCapUpdated has copy, drop { + maintainer_cap_id: ID, + allowed: bool, + timestamp: u64, +} + +public struct PauseCapUpdated has copy, drop { + pause_cap_id: ID, + allowed: bool, + timestamp: u64, +} + +public struct DeepbookPoolRegistered has copy, drop { + pool_id: ID, + config: PoolConfig, + timestamp: u64, +} + +public struct DeepbookPoolUpdated has copy, drop { + pool_id: ID, + enabled: bool, + timestamp: u64, +} + +public struct DeepbookPoolConfigUpdated has copy, drop { + pool_id: ID, + config: PoolConfig, + timestamp: u64, +} + +fun init(_: MARGIN_REGISTRY, ctx: &mut TxContext) { + let id = object::new(ctx); + let margin_registry_inner = MarginRegistryInner { + registry_id: id.to_inner(), + allowed_versions: vec_set::singleton(margin_constants::margin_version()), + pool_registry: table::new(ctx), + margin_pools: table::new(ctx), + margin_managers: table::new(ctx), + allowed_maintainers: vec_set::empty(), + allowed_pause_caps: vec_set::empty(), + }; + + let registry = MarginRegistry { + id, + inner: versioned::create(margin_constants::margin_version(), margin_registry_inner, ctx), + }; + let margin_admin_cap = MarginAdminCap { id: object::new(ctx) }; + transfer::share_object(registry); + transfer::public_transfer(margin_admin_cap, ctx.sender()); +} + +// === Public Functions * ADMIN * === +/// Mint a `MaintainerCap`, only admin can mint a `MaintainerCap`. +/// This function does not have version restrictions +public fun mint_maintainer_cap( + self: &mut MarginRegistry, + _admin_cap: &MarginAdminCap, + clock: &Clock, + ctx: &mut TxContext, +): MaintainerCap { + let self: &mut MarginRegistryInner = self.inner.load_value_mut(); + let id = object::new(ctx); + self.allowed_maintainers.insert(id.to_inner()); + + event::emit(MaintainerCapUpdated { + maintainer_cap_id: id.to_inner(), + allowed: true, + timestamp: clock.timestamp_ms(), + }); + + MaintainerCap { + id, + } +} + +/// Revoke a `MaintainerCap`. Only the admin can revoke a `MaintainerCap`. +/// This function does not have version restrictions +public fun revoke_maintainer_cap( + self: &mut MarginRegistry, + _admin_cap: &MarginAdminCap, + maintainer_cap_id: ID, + clock: &Clock, +) { + let self: &mut MarginRegistryInner = self.inner.load_value_mut(); + assert!(self.allowed_maintainers.contains(&maintainer_cap_id), EMaintainerCapNotValid); + self.allowed_maintainers.remove(&maintainer_cap_id); + + event::emit(MaintainerCapUpdated { + maintainer_cap_id, + allowed: false, + timestamp: clock.timestamp_ms(), + }); +} + +/// Register a margin pool for margin trading with existing margin pools +public fun register_deepbook_pool( + self: &mut MarginRegistry, + _admin_cap: &MarginAdminCap, + pool: &Pool, + pool_config: PoolConfig, + clock: &Clock, +) { + let inner = self.load_inner_mut(); + let pool_id = pool.id(); + assert!(!inner.pool_registry.contains(pool_id), EPoolAlreadyRegistered); + + inner.pool_registry.add(pool_id, pool_config); + + event::emit(DeepbookPoolRegistered { + pool_id, + config: pool_config, + timestamp: clock.timestamp_ms(), + }); +} + +/// Enables a deepbook pool for margin trading. +public fun enable_deepbook_pool( + self: &mut MarginRegistry, + _admin_cap: &MarginAdminCap, + pool: &mut Pool, + clock: &Clock, +) { + let inner = self.load_inner_mut(); + let pool_id = pool.id(); + assert!(inner.pool_registry.contains(pool_id), EPoolNotRegistered); + + let config = inner.pool_registry.borrow_mut(pool_id); + assert!(config.enabled == false, EPoolAlreadyEnabled); + config.enabled = true; + + event::emit(DeepbookPoolUpdated { + pool_id, + enabled: true, + timestamp: clock.timestamp_ms(), + }); +} + +/// Disables a deepbook pool from margin trading. Only reduce only orders, cancels, and withdraw settled amounts are allowed. +public fun disable_deepbook_pool( + self: &mut MarginRegistry, + _admin_cap: &MarginAdminCap, + pool: &mut Pool, + clock: &Clock, +) { + let inner = self.load_inner_mut(); + let pool_id = pool.id(); + assert!(inner.pool_registry.contains(pool_id), EPoolNotRegistered); + + let config = inner.pool_registry.borrow_mut(pool_id); + assert!(config.enabled == true, EPoolAlreadyDisabled); + config.enabled = false; + + event::emit(DeepbookPoolUpdated { + pool_id, + enabled: false, + timestamp: clock.timestamp_ms(), + }); +} + +/// Updates risk params for a deepbook pool as the admin. +public fun update_risk_params( + self: &mut MarginRegistry, + _admin_cap: &MarginAdminCap, + pool: &Pool, + pool_config: PoolConfig, + clock: &Clock, +) { + let inner = self.load_inner_mut(); + let pool_id = pool.id(); + assert!(inner.pool_registry.contains(pool_id), EPoolNotRegistered); + + let prev_config = inner.pool_registry.remove(pool_id); + assert!( + pool_config.risk_ratios.liquidation_risk_ratio <= prev_config + .risk_ratios + .liquidation_risk_ratio, + EInvalidRiskParam, + ); + assert!(prev_config.enabled, EPoolNotEnabled); + + // Validate new risk parameters + assert!( + pool_config.risk_ratios.min_borrow_risk_ratio < pool_config + .risk_ratios + .min_withdraw_risk_ratio, + EInvalidRiskParam, + ); + assert!( + pool_config.risk_ratios.liquidation_risk_ratio < pool_config + .risk_ratios + .min_borrow_risk_ratio, + EInvalidRiskParam, + ); + assert!( + pool_config.risk_ratios.liquidation_risk_ratio < pool_config + .risk_ratios + .target_liquidation_risk_ratio, + EInvalidRiskParam, + ); + assert!( + pool_config.risk_ratios.liquidation_risk_ratio >= constants::float_scaling(), + EInvalidRiskParam, + ); + + inner.pool_registry.add(pool_id, pool_config); + + event::emit(DeepbookPoolConfigUpdated { + pool_id, + config: pool_config, + timestamp: clock.timestamp_ms(), + }); +} + +/// Add Pyth Config to the MarginRegistry. +public fun add_config( + self: &mut MarginRegistry, + _admin_cap: &MarginAdminCap, + config: Config, +) { + self.load_inner(); + self.id.add(ConfigKey {}, config); +} + +/// Remove Pyth Config from the MarginRegistry. +public fun remove_config( + self: &mut MarginRegistry, + _admin_cap: &MarginAdminCap, +): Config { + self.load_inner(); + self.id.remove(ConfigKey {}) +} + +/// Enables a package version +/// Only Admin can enable a package version +/// This function does not have version restrictions +public fun enable_version(self: &mut MarginRegistry, version: u64, _admin_cap: &MarginAdminCap) { + let self: &mut MarginRegistryInner = self.inner.load_value_mut(); + assert!(!self.allowed_versions.contains(&version), EVersionAlreadyEnabled); + self.allowed_versions.insert(version); +} + +/// Disables a package version +/// Only Admin can disable a package version +/// This function does not have version restrictions +public fun disable_version(self: &mut MarginRegistry, version: u64, _admin_cap: &MarginAdminCap) { + let self: &mut MarginRegistryInner = self.inner.load_value_mut(); + assert!(self.allowed_versions.contains(&version), EVersionNotEnabled); + self.allowed_versions.remove(&version); +} + +/// Disables a package version +/// Pause Cap must be valid and can disable the version +/// This function does not have version restrictions +public fun disable_version_pause_cap( + self: &mut MarginRegistry, + version: u64, + pause_cap: &MarginPauseCap, +) { + let self: &mut MarginRegistryInner = self.inner.load_value_mut(); + assert!(self.allowed_pause_caps.contains(&pause_cap.id.to_inner()), EPauseCapNotValid); + assert!(self.allowed_versions.contains(&version), EVersionNotEnabled); + self.allowed_versions.remove(&version); +} + +/// Mint a pause cap +/// Only Admin can mint a pause cap +/// This function does not have version restrictions +public fun mint_pause_cap( + self: &mut MarginRegistry, + _admin_cap: &MarginAdminCap, + clock: &Clock, + ctx: &mut TxContext, +): MarginPauseCap { + let id = object::new(ctx); + let self: &mut MarginRegistryInner = self.inner.load_value_mut(); + self.allowed_pause_caps.insert(id.to_inner()); + + event::emit(PauseCapUpdated { + pause_cap_id: id.to_inner(), + allowed: true, + timestamp: clock.timestamp_ms(), + }); + MarginPauseCap { id } +} + +/// Revoke a pause cap +/// Only Admin can revoke a pause cap +/// This function does not have version restrictions +public fun revoke_pause_cap( + self: &mut MarginRegistry, + _admin_cap: &MarginAdminCap, + clock: &Clock, + pause_cap_id: ID, +) { + let self: &mut MarginRegistryInner = self.inner.load_value_mut(); + assert!(self.allowed_pause_caps.contains(&pause_cap_id), EPauseCapNotValid); + self.allowed_pause_caps.remove(&pause_cap_id); + + event::emit(PauseCapUpdated { + pause_cap_id, + allowed: false, + timestamp: clock.timestamp_ms(), + }); +} + +// === Public Helper Functions === +/// Create a PoolConfig with margin pool IDs and risk parameters +/// Enable is false by default, must be enabled after registration +public fun new_pool_config( + self: &MarginRegistry, + min_withdraw_risk_ratio: u64, + min_borrow_risk_ratio: u64, + liquidation_risk_ratio: u64, + target_liquidation_risk_ratio: u64, + user_liquidation_reward: u64, + pool_liquidation_reward: u64, +): PoolConfig { + assert!(min_borrow_risk_ratio < min_withdraw_risk_ratio, EInvalidRiskParam); + assert!(liquidation_risk_ratio < min_borrow_risk_ratio, EInvalidRiskParam); + assert!(liquidation_risk_ratio < target_liquidation_risk_ratio, EInvalidRiskParam); + assert!(liquidation_risk_ratio >= constants::float_scaling(), EInvalidRiskParam); + assert!(user_liquidation_reward <= constants::float_scaling(), EInvalidRiskParam); + assert!(pool_liquidation_reward <= constants::float_scaling(), EInvalidRiskParam); + assert!( + user_liquidation_reward + pool_liquidation_reward <= constants::float_scaling(), + EInvalidRiskParam, + ); + assert!( + target_liquidation_risk_ratio > + constants::float_scaling() + user_liquidation_reward + pool_liquidation_reward, + EInvalidRiskParam, + ); + + PoolConfig { + base_margin_pool_id: self.get_margin_pool_id(), + quote_margin_pool_id: self.get_margin_pool_id(), + risk_ratios: RiskRatios { + min_withdraw_risk_ratio, + min_borrow_risk_ratio, + liquidation_risk_ratio, + target_liquidation_risk_ratio, + }, + user_liquidation_reward, + pool_liquidation_reward, + enabled: false, + extra_fields: vec_map::empty(), + } +} + +/// Create a PoolConfig with default risk parameters based on leverage +public fun new_pool_config_with_leverage( + self: &MarginRegistry, + leverage: u64, +): PoolConfig { + self.load_inner(); + assert!(leverage > margin_constants::min_leverage(), EInvalidRiskParam); + assert!(leverage <= margin_constants::max_leverage(), EInvalidRiskParam); + + let factor = math::div(constants::float_scaling(), leverage - constants::float_scaling()); + let risk_ratios = calculate_risk_ratios(factor); + + self.new_pool_config( + risk_ratios.min_withdraw_risk_ratio, + risk_ratios.min_borrow_risk_ratio, + risk_ratios.liquidation_risk_ratio, + risk_ratios.target_liquidation_risk_ratio, + margin_constants::default_user_liquidation_reward(), + margin_constants::default_pool_liquidation_reward(), + ) +} + +// === Public-View Functions === +/// Check if a deepbook pool is registered for margin trading +public fun pool_enabled( + self: &MarginRegistry, + pool: &Pool, +): bool { + let inner = self.load_inner(); + let pool_id = pool.id(); + if (inner.pool_registry.contains(pool_id)) { + let config = inner.pool_registry.borrow(pool_id); + + config.enabled + } else { + false + } +} + +/// Get the margin pool id for the given asset. +public fun get_margin_pool_id(self: &MarginRegistry): ID { + let inner = self.load_inner(); + let key = type_name::with_defining_ids(); + assert!(inner.margin_pools.contains(key), EMarginPoolDoesNotExists); + + *inner.margin_pools.borrow(key) +} + +/// Get the margin pool IDs for a deepbook pool +public fun get_deepbook_pool_margin_pool_ids( + self: &MarginRegistry, + deepbook_pool_id: ID, +): (ID, ID) { + self.load_inner(); + let config = self.get_pool_config(deepbook_pool_id); + (config.base_margin_pool_id, config.quote_margin_pool_id) +} + +/// Get the margin manager IDs for a given owner +public fun get_margin_manager_ids(self: &MarginRegistry, owner: address): VecSet { + let inner = self.load_inner(); + if (inner.margin_managers.contains(owner)) { + *inner.margin_managers.borrow>(owner) + } else { + vec_set::empty() + } +} + +public fun can_liquidate(self: &MarginRegistry, deepbook_pool_id: ID, risk_ratio: u64): bool { + let config = self.get_pool_config(deepbook_pool_id); + risk_ratio < config.risk_ratios.liquidation_risk_ratio +} + +public fun base_margin_pool_id(self: &MarginRegistry, deepbook_pool_id: ID): ID { + let config = self.get_pool_config(deepbook_pool_id); + config.base_margin_pool_id +} + +public fun quote_margin_pool_id(self: &MarginRegistry, deepbook_pool_id: ID): ID { + let config = self.get_pool_config(deepbook_pool_id); + config.quote_margin_pool_id +} + +public fun min_withdraw_risk_ratio(self: &MarginRegistry, deepbook_pool_id: ID): u64 { + let config = self.get_pool_config(deepbook_pool_id); + config.risk_ratios.min_withdraw_risk_ratio +} + +public fun min_borrow_risk_ratio(self: &MarginRegistry, deepbook_pool_id: ID): u64 { + let config = self.get_pool_config(deepbook_pool_id); + config.risk_ratios.min_borrow_risk_ratio +} + +public fun liquidation_risk_ratio(self: &MarginRegistry, deepbook_pool_id: ID): u64 { + let config = self.get_pool_config(deepbook_pool_id); + config.risk_ratios.liquidation_risk_ratio +} + +public fun target_liquidation_risk_ratio(self: &MarginRegistry, deepbook_pool_id: ID): u64 { + let config = self.get_pool_config(deepbook_pool_id); + config.risk_ratios.target_liquidation_risk_ratio +} + +public fun user_liquidation_reward(self: &MarginRegistry, deepbook_pool_id: ID): u64 { + let config = self.get_pool_config(deepbook_pool_id); + config.user_liquidation_reward +} + +public fun pool_liquidation_reward(self: &MarginRegistry, deepbook_pool_id: ID): u64 { + let config = self.get_pool_config(deepbook_pool_id); + config.pool_liquidation_reward +} + +public fun allowed_maintainers(self: &MarginRegistry): VecSet { + let inner = self.load_inner(); + inner.allowed_maintainers +} + +public fun allowed_pause_caps(self: &MarginRegistry): VecSet { + let inner = self.load_inner(); + inner.allowed_pause_caps +} + +// === Public-Package Functions === +#[allow(lint(self_transfer))] +public(package) fun register_margin_pool( + self: &mut MarginRegistry, + key: TypeName, + margin_pool_id: ID, + maintainer_cap: &MaintainerCap, + ctx: &mut TxContext, +) { + self.assert_maintainer_cap_valid(maintainer_cap); + let inner = self.load_inner_mut(); + assert!(!inner.margin_pools.contains(key), EMarginPoolAlreadyExists); + inner.margin_pools.add(key, margin_pool_id); + + let margin_pool_cap = MarginPoolCap { + id: object::new(ctx), + margin_pool_id, + }; + + transfer::public_transfer(margin_pool_cap, ctx.sender()); +} + +public(package) fun add_margin_manager( + self: &mut MarginRegistry, + margin_manager_id: ID, + ctx: &TxContext, +) { + let owner = ctx.sender(); + let inner = self.load_inner_mut(); + if (!inner.margin_managers.contains(owner)) { + inner.margin_managers.add(owner, vec_set::empty()); + }; + let margin_manager_ids = inner.margin_managers.borrow_mut(owner); + margin_manager_ids.insert(margin_manager_id); + assert!( + margin_manager_ids.length() <= margin_constants::max_margin_managers(), + EMaxMarginManagersReached, + ); +} + +public(package) fun remove_margin_manager( + self: &mut MarginRegistry, + margin_manager_id: ID, + ctx: &TxContext, +) { + let owner = ctx.sender(); + let inner = self.load_inner_mut(); + let margin_manager_ids = inner.margin_managers.borrow_mut(owner); + assert!(margin_manager_ids.contains(&margin_manager_id), EMarginManagerNotRegistered); + margin_manager_ids.remove(&margin_manager_id); +} + +public(package) fun load_inner_mut(self: &mut MarginRegistry): &mut MarginRegistryInner { + let inner: &mut MarginRegistryInner = self.inner.load_value_mut(); + let package_version = margin_constants::margin_version(); + assert!(inner.allowed_versions.contains(&package_version), EPackageVersionDisabled); + + inner +} + +public(package) fun load_inner(self: &MarginRegistry): &MarginRegistryInner { + let inner: &MarginRegistryInner = self.inner.load_value(); + let package_version = margin_constants::margin_version(); + assert!(inner.allowed_versions.contains(&package_version), EPackageVersionDisabled); + + inner +} + +/// Get the pool configuration for a deepbook pool +public fun get_pool_config(self: &MarginRegistry, deepbook_pool_id: ID): &PoolConfig { + let inner = self.load_inner(); + assert!(inner.pool_registry.contains(deepbook_pool_id), EPoolNotRegistered); + inner.pool_registry.borrow(deepbook_pool_id) +} + +public(package) fun can_withdraw( + self: &MarginRegistry, + deepbook_pool_id: ID, + risk_ratio: u64, +): bool { + let config = self.get_pool_config(deepbook_pool_id); + risk_ratio >= config.risk_ratios.min_withdraw_risk_ratio +} + +public(package) fun can_borrow(self: &MarginRegistry, deepbook_pool_id: ID, risk_ratio: u64): bool { + let config = self.get_pool_config(deepbook_pool_id); + risk_ratio >= config.risk_ratios.min_borrow_risk_ratio +} + +public(package) fun get_config(self: &MarginRegistry): &Config { + self.id.borrow(ConfigKey {}) +} + +public(package) fun margin_pool_id(margin_pool_cap: &MarginPoolCap): ID { + margin_pool_cap.margin_pool_id +} + +public(package) fun pool_cap_id(margin_pool_cap: &MarginPoolCap): ID { + margin_pool_cap.id.to_inner() +} + +public(package) fun maintainer_cap_id(maintainer_cap: &MaintainerCap): ID { + maintainer_cap.id.to_inner() +} + +public(package) fun assert_maintainer_cap_valid( + self: &MarginRegistry, + maintainer_cap: &MaintainerCap, +) { + let inner = self.load_inner(); + assert!( + inner.allowed_maintainers.contains(&maintainer_cap.id.to_inner()), + EMaintainerCapNotValid, + ); +} + +/// Calculate risk parameters based on leverage factor +fun calculate_risk_ratios(leverage_factor: u64): RiskRatios { + RiskRatios { + min_withdraw_risk_ratio: constants::float_scaling() + 4 * leverage_factor, // 1 + 1 = 2x + min_borrow_risk_ratio: constants::float_scaling() + leverage_factor, // 1 + 0.25 = 1.25x + liquidation_risk_ratio: constants::float_scaling() + + leverage_factor / 2, // 1 + 0.125 = 1.125x + target_liquidation_risk_ratio: constants::float_scaling() + + leverage_factor, // 1 + 0.25 = 1.25x + } +} + +#[test_only] +public fun new_for_testing(ctx: &mut TxContext): MarginAdminCap { + let id = object::new(ctx); + let margin_registry_inner = MarginRegistryInner { + registry_id: id.to_inner(), + allowed_versions: vec_set::singleton(margin_constants::margin_version()), + pool_registry: table::new(ctx), + margin_pools: table::new(ctx), + margin_managers: table::new(ctx), + allowed_maintainers: vec_set::empty(), + allowed_pause_caps: vec_set::empty(), + }; + + let registry = MarginRegistry { + id, + inner: versioned::create(margin_constants::margin_version(), margin_registry_inner, ctx), + }; + let margin_admin_cap = MarginAdminCap { id: object::new(ctx) }; + + transfer::share_object(registry); + + margin_admin_cap +} diff --git a/packages/deepbook_margin/sources/pool_proxy.move b/packages/deepbook_margin/sources/pool_proxy.move new file mode 100644 index 000000000..bf073d8f3 --- /dev/null +++ b/packages/deepbook_margin/sources/pool_proxy.move @@ -0,0 +1,424 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +module deepbook_margin::pool_proxy; + +use deepbook::{math, order_info::OrderInfo, pool::Pool}; +use deepbook_margin::{ + margin_manager::MarginManager, + margin_pool::MarginPool, + margin_registry::MarginRegistry +}; +use std::type_name; +use sui::clock::Clock; +use token::deep::DEEP; + +// === Errors === +const ECannotStakeWithDeepMarginManager: u64 = 1; +const EPoolNotEnabledForMarginTrading: u64 = 2; +const ENotReduceOnlyOrder: u64 = 3; +const EIncorrectDeepBookPool: u64 = 4; + +// === Public Proxy Functions - Trading === +/// Places a limit order in the pool. +public fun place_limit_order( + registry: &MarginRegistry, + margin_manager: &mut MarginManager, + pool: &mut Pool, + client_order_id: u64, + order_type: u8, + self_matching_option: u8, + price: u64, + quantity: u64, + is_bid: bool, + pay_with_deep: bool, + expire_timestamp: u64, + clock: &Clock, + ctx: &TxContext, +): OrderInfo { + assert!(margin_manager.deepbook_pool() == pool.id(), EIncorrectDeepBookPool); + let trade_proof = margin_manager.trade_proof(ctx); + let balance_manager = margin_manager.balance_manager_trading_mut(ctx); + assert!(registry.pool_enabled(pool), EPoolNotEnabledForMarginTrading); + + pool.place_limit_order( + balance_manager, + &trade_proof, + client_order_id, + order_type, + self_matching_option, + price, + quantity, + is_bid, + pay_with_deep, + expire_timestamp, + clock, + ctx, + ) +} + +/// Places a market order in the pool. +public fun place_market_order( + registry: &MarginRegistry, + margin_manager: &mut MarginManager, + pool: &mut Pool, + client_order_id: u64, + self_matching_option: u8, + quantity: u64, + is_bid: bool, + pay_with_deep: bool, + clock: &Clock, + ctx: &TxContext, +): OrderInfo { + assert!(margin_manager.deepbook_pool() == pool.id(), EIncorrectDeepBookPool); + let trade_proof = margin_manager.trade_proof(ctx); + let balance_manager = margin_manager.balance_manager_trading_mut(ctx); + assert!(registry.pool_enabled(pool), EPoolNotEnabledForMarginTrading); + + pool.place_market_order( + balance_manager, + &trade_proof, + client_order_id, + self_matching_option, + quantity, + is_bid, + pay_with_deep, + clock, + ctx, + ) +} + +/// Places a reduce-only order in the pool. Used when margin trading is disabled. +public fun place_reduce_only_limit_order( + registry: &MarginRegistry, + margin_manager: &mut MarginManager, + pool: &mut Pool, + margin_pool: &MarginPool, + client_order_id: u64, + order_type: u8, + self_matching_option: u8, + price: u64, + quantity: u64, + is_bid: bool, + pay_with_deep: bool, + expire_timestamp: u64, + clock: &Clock, + ctx: &TxContext, +): OrderInfo { + registry.load_inner(); + assert!(margin_manager.deepbook_pool() == pool.id(), EIncorrectDeepBookPool); + let (base_debt, quote_debt) = margin_manager.calculate_debts( + margin_pool, + clock, + ); + let (base_asset, quote_asset) = margin_manager.calculate_assets( + pool, + ); + + assert!( + (is_bid && base_debt > base_asset && quantity <= base_debt - base_asset) || + (!is_bid && quote_debt > quote_asset && math::mul(quantity, price) <= quote_debt - quote_asset), + ENotReduceOnlyOrder, + ); + + let trade_proof = margin_manager.trade_proof(ctx); + let balance_manager = margin_manager.balance_manager_trading_mut(ctx); + + pool.place_limit_order( + balance_manager, + &trade_proof, + client_order_id, + order_type, + self_matching_option, + price, + quantity, + is_bid, + pay_with_deep, + expire_timestamp, + clock, + ctx, + ) +} + +/// Places a reduce-only market order in the pool. Used when margin trading is disabled. +public fun place_reduce_only_market_order( + registry: &MarginRegistry, + margin_manager: &mut MarginManager, + pool: &mut Pool, + margin_pool: &MarginPool, + client_order_id: u64, + self_matching_option: u8, + quantity: u64, + is_bid: bool, + pay_with_deep: bool, + clock: &Clock, + ctx: &TxContext, +): OrderInfo { + registry.load_inner(); + assert!(margin_manager.deepbook_pool() == pool.id(), EIncorrectDeepBookPool); + let (base_debt, quote_debt) = margin_manager.calculate_debts( + margin_pool, + clock, + ); + let (base_asset, quote_asset) = margin_manager.calculate_assets( + pool, + ); + + let (_, quote_quantity, _) = if (pay_with_deep) { + pool.get_quote_quantity_out(quantity, clock) + } else { + pool.get_quote_quantity_out_input_fee(quantity, clock) + }; + + // The order is a bid, and quantity is less than the net base debt. + // The order is a ask, and quote quantity is less than the net quote debt. + assert!( + (is_bid && base_debt > base_asset && quantity <= base_debt - base_asset) || + (!is_bid && quote_debt > quote_asset && quote_quantity <= quote_debt - quote_asset), + ENotReduceOnlyOrder, + ); + + let trade_proof = margin_manager.trade_proof(ctx); + let balance_manager = margin_manager.balance_manager_trading_mut(ctx); + + pool.place_market_order( + balance_manager, + &trade_proof, + client_order_id, + self_matching_option, + quantity, + is_bid, + pay_with_deep, + clock, + ctx, + ) +} + +/// Modifies an order +public fun modify_order( + registry: &MarginRegistry, + margin_manager: &mut MarginManager, + pool: &mut Pool, + order_id: u128, + new_quantity: u64, + clock: &Clock, + ctx: &TxContext, +) { + registry.load_inner(); + assert!(margin_manager.deepbook_pool() == pool.id(), EIncorrectDeepBookPool); + let trade_proof = margin_manager.trade_proof(ctx); + let balance_manager = margin_manager.balance_manager_trading_mut(ctx); + + pool.modify_order( + balance_manager, + &trade_proof, + order_id, + new_quantity, + clock, + ctx, + ) +} + +/// Cancels an order +public fun cancel_order( + registry: &MarginRegistry, + margin_manager: &mut MarginManager, + pool: &mut Pool, + order_id: u128, + clock: &Clock, + ctx: &TxContext, +) { + registry.load_inner(); + assert!(margin_manager.deepbook_pool() == pool.id(), EIncorrectDeepBookPool); + let trade_proof = margin_manager.trade_proof(ctx); + let balance_manager = margin_manager.balance_manager_trading_mut(ctx); + + pool.cancel_order( + balance_manager, + &trade_proof, + order_id, + clock, + ctx, + ); +} + +/// Cancel multiple orders within a vector. +public fun cancel_orders( + registry: &MarginRegistry, + margin_manager: &mut MarginManager, + pool: &mut Pool, + order_ids: vector, + clock: &Clock, + ctx: &TxContext, +) { + registry.load_inner(); + assert!(margin_manager.deepbook_pool() == pool.id(), EIncorrectDeepBookPool); + let trade_proof = margin_manager.trade_proof(ctx); + let balance_manager = margin_manager.balance_manager_trading_mut(ctx); + + pool.cancel_orders( + balance_manager, + &trade_proof, + order_ids, + clock, + ctx, + ); +} + +/// Cancels all orders for the given account. +public fun cancel_all_orders( + registry: &MarginRegistry, + margin_manager: &mut MarginManager, + pool: &mut Pool, + clock: &Clock, + ctx: &TxContext, +) { + registry.load_inner(); + assert!(margin_manager.deepbook_pool() == pool.id(), EIncorrectDeepBookPool); + let trade_proof = margin_manager.trade_proof(ctx); + let balance_manager = margin_manager.balance_manager_trading_mut(ctx); + + pool.cancel_all_orders( + balance_manager, + &trade_proof, + clock, + ctx, + ); +} + +/// Withdraw settled amounts to balance_manager. +public fun withdraw_settled_amounts( + registry: &MarginRegistry, + margin_manager: &mut MarginManager, + pool: &mut Pool, + ctx: &TxContext, +) { + registry.load_inner(); + assert!(margin_manager.deepbook_pool() == pool.id(), EIncorrectDeepBookPool); + let trade_proof = margin_manager.trade_proof(ctx); + let balance_manager = margin_manager.balance_manager_trading_mut(ctx); + + pool.withdraw_settled_amounts( + balance_manager, + &trade_proof, + ); +} + +/// Withdraw settled amounts to balance_manager permissionlessly. +/// Anyone can call this function to settle balances for a margin manager. +public fun withdraw_settled_amounts_permissionless( + registry: &MarginRegistry, + margin_manager: &mut MarginManager, + pool: &mut Pool, +) { + registry.load_inner(); + margin_manager.withdraw_settled_amounts_permissionless_int(pool); +} + +/// Stake DEEP tokens to the pool. +public fun stake( + registry: &MarginRegistry, + margin_manager: &mut MarginManager, + pool: &mut Pool, + amount: u64, + ctx: &TxContext, +) { + registry.load_inner(); + assert!(margin_manager.deepbook_pool() == pool.id(), EIncorrectDeepBookPool); + let base_asset_type = type_name::with_defining_ids(); + let quote_asset_type = type_name::with_defining_ids(); + let deep_asset_type = type_name::with_defining_ids(); + assert!( + base_asset_type != deep_asset_type && quote_asset_type != deep_asset_type, + ECannotStakeWithDeepMarginManager, + ); + + let trade_proof = margin_manager.trade_proof(ctx); + let balance_manager = margin_manager.balance_manager_trading_mut(ctx); + + pool.stake( + balance_manager, + &trade_proof, + amount, + ctx, + ); +} + +/// Unstake DEEP tokens from the pool. +public fun unstake( + registry: &MarginRegistry, + margin_manager: &mut MarginManager, + pool: &mut Pool, + ctx: &TxContext, +) { + registry.load_inner(); + assert!(margin_manager.deepbook_pool() == pool.id(), EIncorrectDeepBookPool); + let trade_proof = margin_manager.trade_proof(ctx); + let balance_manager = margin_manager.balance_manager_trading_mut(ctx); + + pool.unstake( + balance_manager, + &trade_proof, + ctx, + ); +} + +/// Submit proposal using the margin manager. +public fun submit_proposal( + registry: &MarginRegistry, + margin_manager: &mut MarginManager, + pool: &mut Pool, + taker_fee: u64, + maker_fee: u64, + stake_required: u64, + ctx: &TxContext, +) { + registry.load_inner(); + assert!(margin_manager.deepbook_pool() == pool.id(), EIncorrectDeepBookPool); + let trade_proof = margin_manager.trade_proof(ctx); + let balance_manager = margin_manager.balance_manager_trading_mut(ctx); + + pool.submit_proposal( + balance_manager, + &trade_proof, + taker_fee, + maker_fee, + stake_required, + ctx, + ); +} + +/// Vote on a proposal using the margin manager. +public fun vote( + registry: &MarginRegistry, + margin_manager: &mut MarginManager, + pool: &mut Pool, + proposal_id: ID, + ctx: &TxContext, +) { + registry.load_inner(); + assert!(margin_manager.deepbook_pool() == pool.id(), EIncorrectDeepBookPool); + let trade_proof = margin_manager.trade_proof(ctx); + let balance_manager = margin_manager.balance_manager_trading_mut(ctx); + + pool.vote( + balance_manager, + &trade_proof, + proposal_id, + ctx, + ); +} + +public fun claim_rebates( + registry: &MarginRegistry, + margin_manager: &mut MarginManager, + pool: &mut Pool, + ctx: &mut TxContext, +) { + registry.load_inner(); + assert!(margin_manager.deepbook_pool() == pool.id(), EIncorrectDeepBookPool); + let trade_proof = margin_manager.trade_proof(ctx); + let balance_manager = margin_manager.balance_manager_trading_mut(ctx); + + pool.claim_rebates(balance_manager, &trade_proof, ctx); +} diff --git a/packages/deepbook_margin/sources/rate_limiter.move b/packages/deepbook_margin/sources/rate_limiter.move new file mode 100644 index 000000000..06e75e9e6 --- /dev/null +++ b/packages/deepbook_margin/sources/rate_limiter.move @@ -0,0 +1,136 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +/// Token Bucket rate limiter for controlling withdrawal rates. +/// Reference: https://github.com/code-423n4/2024-11-chainlink/blob/main/contracts/src/ccip/libraries/RateLimiter.sol +module deepbook_margin::rate_limiter; + +use std::u128::min; +use sui::clock::Clock; + +public struct RateLimiter has store { + available: u64, + last_updated_ms: u64, + capacity: u64, + refill_rate_per_ms: u64, + enabled: bool, +} + +// === Public-Package Functions === + +public(package) fun new( + capacity: u64, + refill_rate_per_ms: u64, + enabled: bool, + clock: &Clock, +): RateLimiter { + RateLimiter { + available: capacity, + last_updated_ms: clock.timestamp_ms(), + capacity, + refill_rate_per_ms, + enabled, + } +} + +public(package) fun check_and_record_withdrawal( + self: &mut RateLimiter, + amount: u64, + clock: &Clock, +): bool { + if (!self.enabled) return true; + + self.refill(clock); + + if (amount > self.available) { + return false + }; + + self.available = self.available - amount; + true +} + +public(package) fun record_deposit(self: &mut RateLimiter, amount: u64, clock: &Clock) { + if (!self.enabled) return; + + self.refill(clock); + + let new_available = (self.available as u128) + (amount as u128); + self.available = min(new_available, self.capacity as u128) as u64; +} + +public(package) fun get_available_withdrawal(self: &RateLimiter, clock: &Clock): u64 { + if (!self.enabled) return std::u64::max_value!(); + + let current_time = clock.timestamp_ms(); + let elapsed = if (current_time > self.last_updated_ms) { + current_time - self.last_updated_ms + } else { + 0 + }; + let refill_amount = (elapsed as u128) * (self.refill_rate_per_ms as u128); + let new_available = (self.available as u128) + refill_amount; + + min(new_available, self.capacity as u128) as u64 +} + +public(package) fun update_config( + self: &mut RateLimiter, + capacity: u64, + refill_rate_per_ms: u64, + enabled: bool, + clock: &Clock, +) { + // Accumulate available using the old rate before updating config + self.refill(clock); + self.capacity = capacity; + self.refill_rate_per_ms = refill_rate_per_ms; + self.enabled = enabled; + if (self.available > capacity) { + self.available = capacity; + }; +} + +// === Public View Functions === + +public fun is_enabled(self: &RateLimiter): bool { + self.enabled +} + +public fun capacity(self: &RateLimiter): u64 { + self.capacity +} + +public fun refill_rate_per_ms(self: &RateLimiter): u64 { + self.refill_rate_per_ms +} + +// === Internal Functions === + +fun refill(self: &mut RateLimiter, clock: &Clock) { + let current_time = clock.timestamp_ms(); + let elapsed = if (current_time > self.last_updated_ms) { + current_time - self.last_updated_ms + } else { + 0 + }; + + if (elapsed > 0) { + let refill_amount = (elapsed as u128) * (self.refill_rate_per_ms as u128); + let new_available = (self.available as u128) + refill_amount; + self.available = min(new_available, self.capacity as u128) as u64; + self.last_updated_ms = current_time; + } +} + +// === Test-only Functions === + +#[test_only] +public fun available(self: &RateLimiter): u64 { + self.available +} + +#[test_only] +public fun last_updated_ms(self: &RateLimiter): u64 { + self.last_updated_ms +} diff --git a/packages/deepbook_margin/sources/tpsl.move b/packages/deepbook_margin/sources/tpsl.move new file mode 100644 index 000000000..1955a5331 --- /dev/null +++ b/packages/deepbook_margin/sources/tpsl.move @@ -0,0 +1,481 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +module deepbook_margin::tpsl; + +use deepbook::{constants, pool::Pool}; +use deepbook_margin::{margin_constants, margin_registry::MarginRegistry, oracle::calculate_price}; +use pyth::price_info::PriceInfoObject; +use sui::{clock::Clock, event}; + +// === Errors === +const EInvalidCondition: u64 = 1; +const EConditionalOrderNotFound: u64 = 2; +const EMaxConditionalOrdersReached: u64 = 3; +const EInvalidTPSLOrderType: u64 = 4; +const EDuplicateConditionalOrderIdentifier: u64 = 5; +const EInvalidOrderParams: u64 = 6; + +// === Structs === +/// Stores conditional orders in two sorted vectors for efficient execution. +/// trigger_below: Orders that trigger when price < trigger_price (sorted high to low) +/// trigger_above: Orders that trigger when price > trigger_price (sorted low to high) +public struct TakeProfitStopLoss has drop, store { + trigger_below: vector, + trigger_above: vector, +} + +public struct ConditionalOrder has copy, drop, store { + conditional_order_id: u64, + condition: Condition, + pending_order: PendingOrder, +} + +public struct Condition has copy, drop, store { + trigger_below_price: bool, + trigger_price: u64, +} + +public struct PendingOrder has copy, drop, store { + is_limit_order: bool, + client_order_id: u64, + order_type: Option, + self_matching_option: u8, + price: Option, + quantity: u64, + is_bid: bool, + pay_with_deep: bool, + expire_timestamp: Option, +} + +// === Events === +public struct ConditionalOrderAdded has copy, drop { + manager_id: ID, + conditional_order_id: u64, + conditional_order: ConditionalOrder, + timestamp: u64, +} + +public struct ConditionalOrderCancelled has copy, drop { + manager_id: ID, + conditional_order_id: u64, + conditional_order: ConditionalOrder, + timestamp: u64, +} + +public struct ConditionalOrderExecuted has copy, drop { + manager_id: ID, + pool_id: ID, + conditional_order_id: u64, + conditional_order: ConditionalOrder, + timestamp: u64, +} + +public struct ConditionalOrderInsufficientFunds has copy, drop { + manager_id: ID, + conditional_order_id: u64, + conditional_order: ConditionalOrder, + timestamp: u64, +} + +// === Public Functions === +public fun new_condition(trigger_below_price: bool, trigger_price: u64): Condition { + Condition { + trigger_below_price, + trigger_price, + } +} + +/// Creates a new pending limit order. +/// Order type must be no restriction or immediate or cancel. +public fun new_pending_limit_order( + client_order_id: u64, + order_type: u8, + self_matching_option: u8, + price: u64, + quantity: u64, + is_bid: bool, + pay_with_deep: bool, + expire_timestamp: u64, +): PendingOrder { + assert!( + order_type == constants::no_restriction() || order_type == constants::immediate_or_cancel(), + EInvalidTPSLOrderType, + ); + PendingOrder { + is_limit_order: true, + client_order_id, + order_type: option::some(order_type), + self_matching_option, + price: option::some(price), + quantity, + is_bid, + pay_with_deep, + expire_timestamp: option::some(expire_timestamp), + } +} + +public fun new_pending_market_order( + client_order_id: u64, + self_matching_option: u8, + quantity: u64, + is_bid: bool, + pay_with_deep: bool, +): PendingOrder { + PendingOrder { + is_limit_order: false, + client_order_id, + order_type: option::none(), + self_matching_option, + price: option::none(), + quantity, + is_bid, + pay_with_deep, + expire_timestamp: option::none(), + } +} + +// === Read-Only Functions === +public fun trigger_below_orders(self: &TakeProfitStopLoss): &vector { + &self.trigger_below +} + +public fun trigger_above_orders(self: &TakeProfitStopLoss): &vector { + &self.trigger_above +} + +public fun num_conditional_orders(self: &TakeProfitStopLoss): u64 { + (self.trigger_below.length() + self.trigger_above.length()) as u64 +} + +public fun conditional_order_id(conditional_order: &ConditionalOrder): u64 { + conditional_order.conditional_order_id +} + +public fun get_conditional_order( + self: &TakeProfitStopLoss, + conditional_order_id: u64, +): Option { + let mut i = 0; + while (i < self.trigger_below.length()) { + let order = &self.trigger_below[i]; + if (order.conditional_order_id == conditional_order_id) { + return option::some(*order) + }; + i = i + 1; + }; + + i = 0; + while (i < self.trigger_above.length()) { + let order = &self.trigger_above[i]; + if (order.conditional_order_id == conditional_order_id) { + return option::some(*order) + }; + i = i + 1; + }; + + option::none() +} + +public fun condition(conditional_order: &ConditionalOrder): Condition { + conditional_order.condition +} + +public fun pending_order(conditional_order: &ConditionalOrder): PendingOrder { + conditional_order.pending_order +} + +public fun trigger_below_price(condition: &Condition): bool { + condition.trigger_below_price +} + +public fun trigger_price(condition: &Condition): u64 { + condition.trigger_price +} + +public fun client_order_id(pending_order: &PendingOrder): u64 { + pending_order.client_order_id +} + +public fun order_type(pending_order: &PendingOrder): Option { + pending_order.order_type +} + +public fun self_matching_option(pending_order: &PendingOrder): u8 { + pending_order.self_matching_option +} + +public fun price(pending_order: &PendingOrder): Option { + pending_order.price +} + +public fun quantity(pending_order: &PendingOrder): u64 { + pending_order.quantity +} + +public fun is_bid(pending_order: &PendingOrder): bool { + pending_order.is_bid +} + +public fun pay_with_deep(pending_order: &PendingOrder): bool { + pending_order.pay_with_deep +} + +public fun expire_timestamp(pending_order: &PendingOrder): Option { + pending_order.expire_timestamp +} + +public fun is_limit_order(pending_order: &PendingOrder): bool { + pending_order.is_limit_order +} + +// === public(package) functions === +public(package) fun new(): TakeProfitStopLoss { + TakeProfitStopLoss { + trigger_below: vector::empty(), + trigger_above: vector::empty(), + } +} + +public(package) fun add_conditional_order( + self: &mut TakeProfitStopLoss, + pool: &Pool, + manager_id: ID, + base_price_info_object: &PriceInfoObject, + quote_price_info_object: &PriceInfoObject, + registry: &MarginRegistry, + conditional_order_id: u64, + condition: Condition, + pending_order: PendingOrder, + clock: &Clock, +) { + // Validate order parameters + if (pending_order.is_limit_order()) { + let price = *pending_order.price.borrow(); + let expire_timestamp = *pending_order.expire_timestamp.borrow(); + assert!( + pool.check_limit_order_params(price, pending_order.quantity, expire_timestamp, clock), + EInvalidOrderParams, + ); + } else { + assert!(pool.check_market_order_params(pending_order.quantity), EInvalidOrderParams); + }; + + let current_price = calculate_price( + registry, + base_price_info_object, + quote_price_info_object, + clock, + ); + + let trigger_below_price = condition.trigger_below_price; + let trigger_price = condition.trigger_price; + + // Validate trigger condition (use <= and >= for consistency with execute_conditional_orders) + assert!( + (trigger_below_price && trigger_price <= current_price) || + (!trigger_below_price && trigger_price >= current_price), + EInvalidCondition, + ); + + assert!( + self.num_conditional_orders() < margin_constants::max_conditional_orders(), + EMaxConditionalOrdersReached, + ); + + assert!( + self.get_conditional_order(conditional_order_id).is_none(), + EDuplicateConditionalOrderIdentifier, + ); + + let conditional_order = ConditionalOrder { + conditional_order_id, + condition, + pending_order, + }; + + // Insert in sorted order (using >= and <= for stable sort) + if (trigger_below_price) { + self.trigger_below.push_back(conditional_order); + self + .trigger_below + .insertion_sort_by!(|a, b| a.condition.trigger_price >= b.condition.trigger_price); + } else { + self.trigger_above.push_back(conditional_order); + self + .trigger_above + .insertion_sort_by!(|a, b| a.condition.trigger_price <= b.condition.trigger_price); + }; + + event::emit(ConditionalOrderAdded { + manager_id, + conditional_order_id, + conditional_order, + timestamp: clock.timestamp_ms(), + }); +} + +public(package) fun cancel_conditional_order( + self: &mut TakeProfitStopLoss, + manager_id: ID, + conditional_order_id: u64, + clock: &Clock, +) { + let conditional_order = self.find_and_remove_order(conditional_order_id); + assert!(conditional_order.is_some(), EConditionalOrderNotFound); + + event::emit(ConditionalOrderCancelled { + manager_id, + conditional_order_id, + conditional_order: conditional_order.destroy_some(), + timestamp: clock.timestamp_ms(), + }); +} + +public(package) fun cancel_all_conditional_orders( + self: &mut TakeProfitStopLoss, + manager_id: ID, + clock: &Clock, +) { + let timestamp = clock.timestamp_ms(); + + // Emit events for all trigger_below orders + self.trigger_below.do!(|conditional_order| { + event::emit(ConditionalOrderCancelled { + manager_id, + conditional_order_id: conditional_order.conditional_order_id, + conditional_order, + timestamp, + }); + }); + + // Emit events for all trigger_above orders + self.trigger_above.do!(|conditional_order| { + event::emit(ConditionalOrderCancelled { + manager_id, + conditional_order_id: conditional_order.conditional_order_id, + conditional_order, + timestamp, + }); + }); + + // Clear both vectors + self.trigger_below = vector[]; + self.trigger_above = vector[]; +} + +public(package) fun remove_executed_conditional_order( + self: &mut TakeProfitStopLoss, + manager_id: ID, + pool_id: ID, + conditional_order_id: u64, + clock: &Clock, +) { + let conditional_order = find_and_remove_order(self, conditional_order_id); + assert!(conditional_order.is_some(), EConditionalOrderNotFound); + + event::emit(ConditionalOrderExecuted { + manager_id, + pool_id, + conditional_order_id, + conditional_order: conditional_order.destroy_some(), + timestamp: clock.timestamp_ms(), + }); +} + +/// Batch remove multiple executed orders efficiently +public(package) fun remove_executed_conditional_orders( + self: &mut TakeProfitStopLoss, + manager_id: ID, + pool_id: ID, + conditional_order_ids: vector, + clock: &Clock, +) { + let timestamp = clock.timestamp_ms(); + + // Partition trigger_below into orders to keep vs remove + let (remove_below, keep_below) = self.trigger_below.partition!(|order| { + conditional_order_ids.contains(&order.conditional_order_id) + }); + self.trigger_below = keep_below; + + // Partition trigger_above into orders to keep vs remove + let (remove_above, keep_above) = self.trigger_above.partition!(|order| { + conditional_order_ids.contains(&order.conditional_order_id) + }); + self.trigger_above = keep_above; + + // Emit events for removed orders + remove_below.do!(|conditional_order| { + event::emit(ConditionalOrderExecuted { + manager_id, + pool_id, + conditional_order_id: conditional_order.conditional_order_id, + conditional_order, + timestamp, + }); + }); + + remove_above.do!(|conditional_order| { + event::emit(ConditionalOrderExecuted { + manager_id, + pool_id, + conditional_order_id: conditional_order.conditional_order_id, + conditional_order, + timestamp, + }); + }); +} + +public(package) fun emit_insufficient_funds_event( + self: &TakeProfitStopLoss, + manager_id: ID, + conditional_order_id: u64, + clock: &Clock, +) { + let conditional_order = self.get_conditional_order(conditional_order_id); + if (conditional_order.is_some()) { + event::emit(ConditionalOrderInsufficientFunds { + manager_id, + conditional_order_id, + conditional_order: conditional_order.destroy_some(), + timestamp: clock.timestamp_ms(), + }); + }; +} + +/// Returns reference to trigger_below vector (sorted high to low by trigger price) +public(package) fun trigger_below(self: &TakeProfitStopLoss): &vector { + &self.trigger_below +} + +/// Returns reference to trigger_above vector (sorted low to high by trigger price) +public(package) fun trigger_above(self: &TakeProfitStopLoss): &vector { + &self.trigger_above +} + +/// Find and remove an order by ID from either vector +fun find_and_remove_order( + self: &mut TakeProfitStopLoss, + conditional_order_id: u64, +): Option { + // Search in trigger_below + let mut i = 0; + while (i < self.trigger_below.length()) { + if (self.trigger_below[i].conditional_order_id == conditional_order_id) { + return option::some(self.trigger_below.remove(i)) + }; + i = i + 1; + }; + + // Search in trigger_above + i = 0; + while (i < self.trigger_above.length()) { + if (self.trigger_above[i].conditional_order_id == conditional_order_id) { + return option::some(self.trigger_above.remove(i)) + }; + i = i + 1; + }; + + option::none() +} diff --git a/packages/deepbook_margin/tests/helper/oracle_tests.move b/packages/deepbook_margin/tests/helper/oracle_tests.move new file mode 100644 index 000000000..77d8c9ac4 --- /dev/null +++ b/packages/deepbook_margin/tests/helper/oracle_tests.move @@ -0,0 +1,627 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +#[test_only] +module deepbook_margin::oracle_tests; + +use deepbook_margin::{ + margin_registry::{Self, MarginRegistry}, + oracle::{ + calculate_usd_currency_amount, + calculate_target_currency_amount, + calculate_usd_price, + calculate_target_amount, + test_conversion_config + }, + test_constants::{Self, USDC}, + test_helpers::{build_pyth_price_info_object, create_test_pyth_config} +}; +use pyth::{i64, price, price_feed, price_identifier, price_info::{Self, PriceInfoObject}}; +use std::unit_test::destroy; +use sui::{clock::{Self, Clock}, test_scenario::{Self, Scenario}}; + +#[test] +fun test_calculate_usd_currency() { + let target_decimals: u8 = 9; + let base_decimals: u8 = 9; + let pyth_price = 380000000; // SUI price 3.8 + let pyth_decimals: u8 = 8; + let base_currency_amount = 100 * 1_000_000_000; // 100 SUI + + let config = test_conversion_config( + target_decimals, + base_decimals, + pyth_price, + pyth_decimals, + ); + let target_currency_amount = calculate_usd_currency_amount( + config, + base_currency_amount, + ); + + assert!(target_currency_amount == 380 * 1_000_000_000); // 380 USDC +} + +#[test] +fun test_calculate_usd_currency_usdc() { + let target_decimals: u8 = 9; + let base_decimals: u8 = 6; + let pyth_price = 100000000; + let pyth_decimals: u8 = 8; + let base_currency_amount = 100 * 1_000_000; // 100 USDC + + let config = test_conversion_config( + target_decimals, + base_decimals, + pyth_price, + pyth_decimals, + ); + let target_currency_amount = calculate_usd_currency_amount( + config, + base_currency_amount, + ); + + assert!(target_currency_amount == 100 * 1_000_000_000); // 100 USDC +} + +#[test] +fun test_calculate_usd_currency_2() { + let target_decimals: u8 = 9; + let base_decimals: u8 = 0; // TOKEN has no decimals + let pyth_price = 3800; // TOKEN price 3.8 + let pyth_decimals: u8 = 3; + let base_currency_amount = 100; // 100 TOKEN + + let config = test_conversion_config( + target_decimals, + base_decimals, + pyth_price, + pyth_decimals, + ); + let target_currency_amount = calculate_usd_currency_amount( + config, + base_currency_amount, + ); + + assert!(target_currency_amount == 380 * 1_000_000_000); // 380 USDC +} + +#[test, expected_failure(abort_code = ::deepbook_margin::oracle::EInvalidPythPrice)] +fun test_calculate_usd_currency_invalid_pyth_price() { + let target_decimals: u8 = 9; + let base_decimals: u8 = 6; + let pyth_price = 0; // Price 0 + let pyth_decimals: u8 = 8; + let base_currency_amount = 100 * 1_000_000; + + let config = test_conversion_config( + target_decimals, + base_decimals, + pyth_price, + pyth_decimals, + ); + calculate_usd_currency_amount( + config, + base_currency_amount, + ); +} + +#[test] +fun test_calculate_target_currency() { + let target_decimals: u8 = 9; + let base_decimals: u8 = 9; + let pyth_price = 380000000; // SUI price 3.8 + let pyth_decimals: u8 = 8; + let base_currency_amount = 100 * 1_000_000_000; // 100 USDC + + let config = test_conversion_config( + target_decimals, + base_decimals, + pyth_price, + pyth_decimals, + ); + let target_currency_amount = calculate_target_currency_amount( + config, + base_currency_amount, + ); + + assert!(target_currency_amount == 26315789474); // 26.315789474 SUI +} + +#[test] +fun test_calculate_target_currency_2() { + let target_decimals: u8 = 0; // TOKEN has no decimals + let base_decimals: u8 = 9; + let pyth_price = 3800; // TOKEN price 3.8 + let pyth_decimals: u8 = 3; + + let base_currency_amount = 100 * 1_000_000_000; // 100 USDC + + let config = test_conversion_config( + target_decimals, + base_decimals, + pyth_price, + pyth_decimals, + ); + let target_currency_amount = calculate_target_currency_amount( + config, + base_currency_amount, + ); + + assert!(target_currency_amount == 27); // 27 TOKEN +} + +#[test, expected_failure(abort_code = ::deepbook_margin::oracle::EInvalidPythPrice)] +fun test_calculate_target_currency_invalid_pyth_price() { + let target_decimals: u8 = 9; + let base_decimals: u8 = 9; + let pyth_price = 0; // Price 0 + let pyth_decimals: u8 = 8; + let base_currency_amount = 100 * 1_000_000_000; + + let config = test_conversion_config( + target_decimals, + base_decimals, + pyth_price, + pyth_decimals, + ); + calculate_target_currency_amount( + config, + base_currency_amount, + ); +} + +#[test, expected_failure(abort_code = ::deepbook_margin::oracle::EInvalidPythPriceConf)] +fun test_calculate_usd_price_invalid_confidence_too_high() { + let mut scenario = test_scenario::begin(test_constants::admin()); + + // Setup registry and clock + scenario.next_tx(test_constants::admin()); + let admin_cap = margin_registry::new_for_testing(scenario.ctx()); + let mut clock = clock::create_for_testing(scenario.ctx()); + clock.set_for_testing(1000000); + clock.share_for_testing(); + + scenario.next_tx(test_constants::admin()); + let mut registry = scenario.take_shared(); + let clock = scenario.take_shared(); + let pyth_config = create_test_pyth_config(); // max_conf_bps = 1000 (10%) + registry.add_config(&admin_cap, pyth_config); + + // Create price info with confidence that exceeds 10% + // Price = $100 (10000000000 with 8 decimals: 100 * 10^8) + // Max allowed conf = 1000 * 10000000000 / 10_000 = 1_000_000_000 + // We set conf = 1_500_000_000 which is > 10% (15%) + let price_info = build_pyth_price_info_object( + &mut scenario, + test_constants::usdc_price_feed_id(), + 10000000000, // $100 price (100 * 10^8) + 1500000000, // 15% confidence (exceeds 10% threshold) + 8, // decimals + clock.timestamp_ms() / 1000, + ); + + // This should fail with EInvalidPythPriceConf + calculate_usd_price( + &price_info, + ®istry, + 1000000, // 1 USDC (6 decimals) + &clock, + ); + + destroy(admin_cap); + destroy(price_info); + test_scenario::return_shared(registry); + test_scenario::return_shared(clock); + scenario.end(); +} + +#[test] +fun test_calculate_usd_price_valid_confidence_at_limit() { + let mut scenario = test_scenario::begin(test_constants::admin()); + + // Setup registry and clock + scenario.next_tx(test_constants::admin()); + let admin_cap = margin_registry::new_for_testing(scenario.ctx()); + let mut clock = clock::create_for_testing(scenario.ctx()); + clock.set_for_testing(1000000); + clock.share_for_testing(); + + scenario.next_tx(test_constants::admin()); + let mut registry = scenario.take_shared(); + let clock = scenario.take_shared(); + let pyth_config = create_test_pyth_config(); // max_conf_bps = 1000 (10%) + registry.add_config(&admin_cap, pyth_config); + + // Create price info with confidence exactly at 10% limit + // Price = $100 (10000000000 with 8 decimals: 100 * 10^8) + // Max allowed conf = 1000 * 10000000000 / 10_000 = 1_000_000_000 + // We set conf = 1_000_000_000 which is exactly at 10% + let price_info = build_pyth_price_info_object( + &mut scenario, + test_constants::usdc_price_feed_id(), + 10000000000, // $100 price (100 * 10^8) + 1000000000, // 10% confidence (exactly at threshold) + 8, // decimals + clock.timestamp_ms() / 1000, + ); + + // This should succeed + let usd_price = calculate_usd_price( + &price_info, + ®istry, + 1000000, // 1 USDC (6 decimals) + &clock, + ); + + // 1 USDC at $100 = $100 (with 9 decimals for USD representation) + assert!(usd_price == 100_000_000_000); + + destroy(admin_cap); + destroy(price_info); + test_scenario::return_shared(registry); + test_scenario::return_shared(clock); + scenario.end(); +} + +#[test, expected_failure(abort_code = ::deepbook_margin::oracle::EInvalidPythPriceConf)] +fun test_calculate_target_amount_invalid_confidence() { + let mut scenario = test_scenario::begin(test_constants::admin()); + + // Setup registry and clock + scenario.next_tx(test_constants::admin()); + let admin_cap = margin_registry::new_for_testing(scenario.ctx()); + let mut clock = clock::create_for_testing(scenario.ctx()); + clock.set_for_testing(1000000); + clock.share_for_testing(); + + scenario.next_tx(test_constants::admin()); + let mut registry = scenario.take_shared(); + let clock = scenario.take_shared(); + let pyth_config = create_test_pyth_config(); // max_conf_bps = 1000 (10%) + registry.add_config(&admin_cap, pyth_config); + + // Create price info with high confidence + // Price = $50 (5000000000 with 8 decimals: 50 * 10^8) + // Max allowed conf = 1000 * 5000000000 / 10_000 = 500_000_000 + // We set conf = 750_000_000 which is 15% (exceeds 10% threshold) + let price_info = build_pyth_price_info_object( + &mut scenario, + test_constants::usdc_price_feed_id(), + 5000000000, // $50 price (50 * 10^8) + 750000000, // 15% confidence (exceeds 10% threshold) + 8, // decimals + clock.timestamp_ms() / 1000, + ); + + // This should fail with EInvalidPythPriceConf + calculate_target_amount( + &price_info, + ®istry, + 100_000_000_000, // $100 USD (9 decimals) + &clock, + ); + + destroy(admin_cap); + destroy(price_info); + test_scenario::return_shared(registry); + test_scenario::return_shared(clock); + scenario.end(); +} + +/// Helper to build a price info object with separate pyth price and EWMA price +fun build_pyth_price_info_with_ewma( + scenario: &mut Scenario, + id: vector, + price_value: u64, + ewma_price_value: u64, + conf_value: u64, + exp_value: u64, + timestamp: u64, +): PriceInfoObject { + let price_id = price_identifier::from_byte_vec(id); + let price = price::new( + i64::new(price_value, false), // positive price + conf_value, + i64::new(exp_value, true), // negative exponent + timestamp, + ); + let ewma_price = price::new( + i64::new(ewma_price_value, false), // positive EWMA price + conf_value, + i64::new(exp_value, true), // negative exponent + timestamp, + ); + let price_feed = price_feed::new(price_id, price, ewma_price); + let price_info = price_info::new_price_info( + timestamp - 2, // attestation_time + timestamp - 1, // arrival_time + price_feed, + ); + price_info::new_price_info_object_for_test(price_info, scenario.ctx()) +} + +#[test, expected_failure(abort_code = ::deepbook_margin::oracle::EInvalidPythPrice)] +fun test_ewma_price_difference_too_high() { + let mut scenario = test_scenario::begin(test_constants::admin()); + + // Setup registry and clock + scenario.next_tx(test_constants::admin()); + let admin_cap = margin_registry::new_for_testing(scenario.ctx()); + let mut clock = clock::create_for_testing(scenario.ctx()); + clock.set_for_testing(1000000); + clock.share_for_testing(); + + scenario.next_tx(test_constants::admin()); + let mut registry = scenario.take_shared(); + let clock = scenario.take_shared(); + let pyth_config = create_test_pyth_config(); // max_ewma_difference_bps = 1500 (15%) + registry.add_config(&admin_cap, pyth_config); + + // Create price info where pyth price is 20% higher than EWMA (exceeds 15% threshold) + // EWMA price = $100 (10000000000 with 8 decimals) + // Pyth price = $120 (12000000000 with 8 decimals) - 20% higher + let price_info = build_pyth_price_info_with_ewma( + &mut scenario, + test_constants::usdc_price_feed_id(), + 12000000000, // $120 pyth price + 10000000000, // $100 EWMA price + 50000, // 0.05% confidence + 8, // decimals + clock.timestamp_ms() / 1000, + ); + + // This should fail with EInvalidPythPrice + calculate_usd_price( + &price_info, + ®istry, + 1000000, // 1 USDC (6 decimals) + &clock, + ); + + destroy(admin_cap); + destroy(price_info); + test_scenario::return_shared(registry); + test_scenario::return_shared(clock); + scenario.end(); +} + +#[test, expected_failure(abort_code = ::deepbook_margin::oracle::EInvalidPythPrice)] +fun test_ewma_price_difference_too_low() { + let mut scenario = test_scenario::begin(test_constants::admin()); + + // Setup registry and clock + scenario.next_tx(test_constants::admin()); + let admin_cap = margin_registry::new_for_testing(scenario.ctx()); + let mut clock = clock::create_for_testing(scenario.ctx()); + clock.set_for_testing(1000000); + clock.share_for_testing(); + + scenario.next_tx(test_constants::admin()); + let mut registry = scenario.take_shared(); + let clock = scenario.take_shared(); + let pyth_config = create_test_pyth_config(); // max_ewma_difference_bps = 1500 (15%) + registry.add_config(&admin_cap, pyth_config); + + // Create price info where pyth price is 20% lower than EWMA (exceeds 15% threshold) + // EWMA price = $100 (10000000000 with 8 decimals) + // Pyth price = $80 (8000000000 with 8 decimals) - 20% lower + let price_info = build_pyth_price_info_with_ewma( + &mut scenario, + test_constants::usdc_price_feed_id(), + 8000000000, // $80 pyth price + 10000000000, // $100 EWMA price + 50000, // 0.05% confidence + 8, // decimals + clock.timestamp_ms() / 1000, + ); + + // This should fail with EInvalidPythPrice + calculate_usd_price( + &price_info, + ®istry, + 1000000, // 1 USDC (6 decimals) + &clock, + ); + + destroy(admin_cap); + destroy(price_info); + test_scenario::return_shared(registry); + test_scenario::return_shared(clock); + scenario.end(); +} + +#[test] +fun test_ewma_price_difference_at_upper_limit() { + let mut scenario = test_scenario::begin(test_constants::admin()); + + // Setup registry and clock + scenario.next_tx(test_constants::admin()); + let admin_cap = margin_registry::new_for_testing(scenario.ctx()); + let mut clock = clock::create_for_testing(scenario.ctx()); + clock.set_for_testing(1000000); + clock.share_for_testing(); + + scenario.next_tx(test_constants::admin()); + let mut registry = scenario.take_shared(); + let clock = scenario.take_shared(); + let pyth_config = create_test_pyth_config(); // max_ewma_difference_bps = 1500 (15%) + registry.add_config(&admin_cap, pyth_config); + + // Create price info where pyth price is exactly 15% higher than EWMA + // EWMA price = $100 (10000000000 with 8 decimals) + // Pyth price = $115 (11500000000 with 8 decimals) - exactly 15% higher + let price_info = build_pyth_price_info_with_ewma( + &mut scenario, + test_constants::usdc_price_feed_id(), + 11500000000, // $115 pyth price + 10000000000, // $100 EWMA price + 50000, // 0.05% confidence + 8, // decimals + clock.timestamp_ms() / 1000, + ); + + // This should succeed + let usd_price = calculate_usd_price( + &price_info, + ®istry, + 1000000, // 1 USDC (6 decimals) + &clock, + ); + + // 1 USDC at $115 = $115 (with 9 decimals for USD representation) + assert!(usd_price == 115_000_000_000); + + destroy(admin_cap); + destroy(price_info); + test_scenario::return_shared(registry); + test_scenario::return_shared(clock); + scenario.end(); +} + +#[test] +fun test_ewma_price_difference_at_lower_limit() { + let mut scenario = test_scenario::begin(test_constants::admin()); + + // Setup registry and clock + scenario.next_tx(test_constants::admin()); + let admin_cap = margin_registry::new_for_testing(scenario.ctx()); + let mut clock = clock::create_for_testing(scenario.ctx()); + clock.set_for_testing(1000000); + clock.share_for_testing(); + + scenario.next_tx(test_constants::admin()); + let mut registry = scenario.take_shared(); + let clock = scenario.take_shared(); + let pyth_config = create_test_pyth_config(); // max_ewma_difference_bps = 1500 (15%) + registry.add_config(&admin_cap, pyth_config); + + // Create price info where pyth price is exactly 15% lower than EWMA + // EWMA price = $100 (10000000000 with 8 decimals) + // Pyth price = $85 (8500000000 with 8 decimals) - exactly 15% lower + let price_info = build_pyth_price_info_with_ewma( + &mut scenario, + test_constants::usdc_price_feed_id(), + 8500000000, // $85 pyth price + 10000000000, // $100 EWMA price + 50000, // 0.05% confidence + 8, // decimals + clock.timestamp_ms() / 1000, + ); + + // This should succeed + let usd_price = calculate_usd_price( + &price_info, + ®istry, + 1000000, // 1 USDC (6 decimals) + &clock, + ); + + // 1 USDC at $85 = $85 (with 9 decimals for USD representation) + assert!(usd_price == 85_000_000_000); + + destroy(admin_cap); + destroy(price_info); + test_scenario::return_shared(registry); + test_scenario::return_shared(clock); + scenario.end(); +} + +#[test] +fun test_confidence_check_with_high_price_no_overflow() { + let mut scenario = test_scenario::begin(test_constants::admin()); + + // Setup registry and clock + scenario.next_tx(test_constants::admin()); + let admin_cap = margin_registry::new_for_testing(scenario.ctx()); + let mut clock = clock::create_for_testing(scenario.ctx()); + clock.set_for_testing(1000000); + clock.share_for_testing(); + + scenario.next_tx(test_constants::admin()); + let mut registry = scenario.take_shared(); + let clock = scenario.take_shared(); + let pyth_config = create_test_pyth_config(); // max_conf_bps = 1000 (10%) + registry.add_config(&admin_cap, pyth_config); + + // Test with very high price that could overflow with old u64 multiplication + // Price = $1,000,000 (100000000000000 with 8 decimals: 1M * 10^8) + // With u64, max_conf_bps * pyth_price = 1000 * 100000000000000 = 10^17 (safe) + // Max allowed conf = 1000 * 100000000000000 / 10_000 = 10_000_000_000_000 + // We set conf = 5_000_000_000_000 which is 5% (within 10% threshold) + let price_info = build_pyth_price_info_object( + &mut scenario, + test_constants::usdc_price_feed_id(), + 100000000000000, // $1M price (1M * 10^8) + 5000000000000, // 5% confidence + 8, // decimals + clock.timestamp_ms() / 1000, + ); + + // This should succeed with u128 casting preventing overflow + let usd_price = calculate_usd_price( + &price_info, + ®istry, + 1000000, // 1 USDC (6 decimals) + &clock, + ); + + // 1 USDC at $1M = $1M (with 9 decimals for USD representation) + assert!(usd_price == 1_000_000_000_000_000); + + destroy(admin_cap); + destroy(price_info); + test_scenario::return_shared(registry); + test_scenario::return_shared(clock); + scenario.end(); +} + +#[test] +fun test_ewma_check_with_high_price_no_overflow() { + let mut scenario = test_scenario::begin(test_constants::admin()); + + // Setup registry and clock + scenario.next_tx(test_constants::admin()); + let admin_cap = margin_registry::new_for_testing(scenario.ctx()); + let mut clock = clock::create_for_testing(scenario.ctx()); + clock.set_for_testing(1000000); + clock.share_for_testing(); + + scenario.next_tx(test_constants::admin()); + let mut registry = scenario.take_shared(); + let clock = scenario.take_shared(); + let pyth_config = create_test_pyth_config(); // max_ewma_difference_bps = 1500 (15%) + registry.add_config(&admin_cap, pyth_config); + + // Test with very high price that could overflow with old u64 multiplication + // EWMA price = $1,000,000 (100000000000000 with 8 decimals) + // With u64: ewma_price * (10_000 + 1500) = 100000000000000 * 11500 + // = 1.15 * 10^18 which exceeds u64 max (~1.8 * 10^19) but is close + // Pyth price = $1,100,000 (110000000000000) - 10% higher (within 15%) + let price_info = build_pyth_price_info_with_ewma( + &mut scenario, + test_constants::usdc_price_feed_id(), + 110000000000000, // $1.1M pyth price + 100000000000000, // $1M EWMA price + 50000, // 0.005% confidence + 8, // decimals + clock.timestamp_ms() / 1000, + ); + + // This should succeed with u128 casting preventing overflow + let usd_price = calculate_usd_price( + &price_info, + ®istry, + 1000000, // 1 USDC (6 decimals) + &clock, + ); + + // 1 USDC at $1.1M = $1.1M (with 9 decimals for USD representation) + assert!(usd_price == 1_100_000_000_000_000); + + destroy(admin_cap); + destroy(price_info); + test_scenario::return_shared(registry); + test_scenario::return_shared(clock); + scenario.end(); +} diff --git a/packages/deepbook_margin/tests/helper/price_info_ext.move b/packages/deepbook_margin/tests/helper/price_info_ext.move new file mode 100644 index 000000000..9a009c872 --- /dev/null +++ b/packages/deepbook_margin/tests/helper/price_info_ext.move @@ -0,0 +1,15 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +#[test_only] +extend module pyth::price_info; + +public fun new_price_info_object_for_test( + price_info: PriceInfo, + ctx: &mut TxContext, +): PriceInfoObject { + PriceInfoObject { + id: object::new(ctx), + price_info, + } +} diff --git a/packages/deepbook_margin/tests/helper/test_constants.move b/packages/deepbook_margin/tests/helper/test_constants.move new file mode 100644 index 000000000..3429c9243 --- /dev/null +++ b/packages/deepbook_margin/tests/helper/test_constants.move @@ -0,0 +1,178 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +#[test_only] +module deepbook_margin::test_constants; + +// === Test Addresses === +const USER1: address = @0xA; +const USER2: address = @0xB; +const ADMIN: address = @0x0; +const LIQUIDATOR: address = @0xC; +const TEST_MARGIN_POOL_ID: address = @0x1234; + +// === Test Coin Types === +public struct USDC has drop {} +public struct USDT has drop {} +public struct BTC has drop {} +public struct SUI has drop {} +public struct INVALID_ASSET has drop {} + +const USDC_MULTIPLIER: u64 = 1000000; +const USDT_MULTIPLIER: u64 = 1000000; +const DEEP_MULTIPLIER: u64 = 1000000; +const BTC_MULTIPLIER: u64 = 100000000; +const SUI_MULTIPLIER: u64 = 1000000000; // 9 decimals +const PYTH_DECIMALS: u64 = 8; + +// === Margin Pool Constants === +const SUPPLY_CAP: u64 = 1_000_000_000_000_000; // 1B tokens with 9 decimals +const MAX_UTILIZATION_RATE: u64 = 800_000_000; // 80% +const PROTOCOL_SPREAD: u64 = 100_000_000; // 10% +const MIN_BORROW: u64 = 1000; + +// === Interest Rate Constants === +const BASE_RATE: u64 = 50_000_000; // 5% +const BASE_SLOPE: u64 = 100_000_000; // 10% +const OPTIMAL_UTILIZATION: u64 = 800_000_000; // 80% +const EXCESS_SLOPE: u64 = 2_000_000_000; // 200% + +// === Pool Configuration Constants === +const MIN_WITHDRAW_RISK_RATIO: u64 = 2_000_000_000; // 200% +const MIN_BORROW_RISK_RATIO: u64 = 1_250_000_000; // 125% +const LIQUIDATION_RISK_RATIO: u64 = 1_100_000_000; // 110% +const TARGET_LIQUIDATION_RISK_RATIO: u64 = 1_250_000_000; // 125% +const USER_LIQUIDATION_REWARD: u64 = 20_000_000; // 2% +const POOL_LIQUIDATION_REWARD: u64 = 30_000_000; // 3% + +// === Pyth Price Feed IDs for Testing === +const USDC_PRICE_FEED_ID: vector = b"USDC0000000000000000000000000000"; +const USDT_PRICE_FEED_ID: vector = b"USDT0000000000000000000000000000"; +const BTC_PRICE_FEED_ID: vector = b"BTC00000000000000000000000000000"; +const SUI_PRICE_FEED_ID: vector = b"SUI00000000000000000000000000000"; + +public fun supply_cap(): u64 { + SUPPLY_CAP +} + +public fun max_utilization_rate(): u64 { + MAX_UTILIZATION_RATE +} + +public fun protocol_spread(): u64 { + PROTOCOL_SPREAD +} + +public fun protocol_spread_inverse(): u64 { + 1_000_000_000 - PROTOCOL_SPREAD +} + +public fun min_borrow(): u64 { + MIN_BORROW +} + +public fun base_rate(): u64 { + BASE_RATE +} + +public fun base_slope(): u64 { + BASE_SLOPE +} + +public fun optimal_utilization(): u64 { + OPTIMAL_UTILIZATION +} + +public fun excess_slope(): u64 { + EXCESS_SLOPE +} + +public fun user1(): address { + USER1 +} + +public fun user2(): address { + USER2 +} + +public fun admin(): address { + ADMIN +} + +public fun liquidator(): address { + LIQUIDATOR +} + +// === Pool Configuration Getters === +public fun min_withdraw_risk_ratio(): u64 { + MIN_WITHDRAW_RISK_RATIO +} + +public fun min_borrow_risk_ratio(): u64 { + MIN_BORROW_RISK_RATIO +} + +public fun liquidation_risk_ratio(): u64 { + LIQUIDATION_RISK_RATIO +} + +public fun target_liquidation_risk_ratio(): u64 { + TARGET_LIQUIDATION_RISK_RATIO +} + +public fun user_liquidation_reward(): u64 { + USER_LIQUIDATION_REWARD +} + +public fun pool_liquidation_reward(): u64 { + POOL_LIQUIDATION_REWARD +} + +// === Pyth Price Feed ID Getters === +public fun usdc_price_feed_id(): vector { + USDC_PRICE_FEED_ID +} + +public fun usdt_price_feed_id(): vector { + USDT_PRICE_FEED_ID +} + +public fun btc_price_feed_id(): vector { + BTC_PRICE_FEED_ID +} + +public fun sui_price_feed_id(): vector { + SUI_PRICE_FEED_ID +} + +public fun usdc_multiplier(): u64 { + USDC_MULTIPLIER +} + +public fun usdt_multiplier(): u64 { + USDT_MULTIPLIER +} + +public fun deep_multiplier(): u64 { + DEEP_MULTIPLIER +} + +public fun btc_multiplier(): u64 { + BTC_MULTIPLIER +} + +public fun sui_multiplier(): u64 { + SUI_MULTIPLIER +} + +public fun pyth_multiplier(): u64 { + 10u64.pow(PYTH_DECIMALS as u8) +} + +public fun pyth_decimals(): u64 { + PYTH_DECIMALS +} + +public fun test_margin_pool_id(): ID { + TEST_MARGIN_POOL_ID.to_id() +} diff --git a/packages/deepbook_margin/tests/helper/test_helpers.move b/packages/deepbook_margin/tests/helper/test_helpers.move new file mode 100644 index 000000000..c300b7bf8 --- /dev/null +++ b/packages/deepbook_margin/tests/helper/test_helpers.move @@ -0,0 +1,802 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +#[test_only] +module deepbook_margin::test_helpers; + +use deepbook::{constants, math, pool::{Self, Pool}, registry::{Self, Registry}}; +use deepbook_margin::{ + margin_manager::MarginApp, + margin_pool::{Self, MarginPool}, + margin_registry::{ + Self, + MarginRegistry, + MarginAdminCap, + MaintainerCap, + PoolConfig, + MarginPoolCap + }, + oracle::{Self, PythConfig}, + protocol_config::{Self, ProtocolConfig}, + test_constants::{Self, USDC, USDT, BTC, SUI} +}; +use pyth::{i64, price, price_feed, price_identifier, price_info::{Self, PriceInfoObject}}; +use std::unit_test::destroy; +use sui::{ + clock::{Self, Clock}, + coin::{Self, Coin}, + test_scenario::{Self as test, Scenario, begin, return_shared} +}; +use token::deep::DEEP; + +// === Cleanup helper functions === + +public macro fun destroy_all<$T>($vec: vector<$T>) { + let mut v = $vec; + v.do!(|item| destroy(item)); + v.destroy_empty(); +} + +public macro fun destroy_2<$T1, $T2>($obj1: $T1, $obj2: $T2) { + destroy($obj1); + destroy($obj2); +} + +public macro fun destroy_3<$T1, $T2, $T3>($obj1: $T1, $obj2: $T2, $obj3: $T3) { + destroy($obj1); + destroy($obj2); + destroy($obj3); +} + +public macro fun destroy_4<$T1, $T2, $T3, $T4>($obj1: $T1, $obj2: $T2, $obj3: $T3, $obj4: $T4) { + destroy($obj1); + destroy($obj2); + destroy($obj3); + destroy($obj4); +} + +public macro fun return_shared_2<$T1, $T2>($obj1: $T1, $obj2: $T2) { + return_shared($obj1); + return_shared($obj2); +} + +public macro fun return_shared_3<$T1, $T2, $T3>($obj1: $T1, $obj2: $T2, $obj3: $T3) { + return_shared($obj1); + return_shared($obj2); + return_shared($obj3); +} + +public macro fun return_shared_4<$T1, $T2, $T3, $T4>( + $obj1: $T1, + $obj2: $T2, + $obj3: $T3, + $obj4: $T4, +) { + return_shared($obj1); + return_shared($obj2); + return_shared($obj3); + return_shared($obj4); +} + +public macro fun return_to_sender_2<$T1, $T2>($scenario: &Scenario, $obj1: $T1, $obj2: $T2) { + let s = $scenario; + s.return_to_sender($obj1); + s.return_to_sender($obj2); +} + +public fun setup_test(): (Scenario, MarginAdminCap) { + let mut test = begin(test_constants::admin()); + let clock = clock::create_for_testing(test.ctx()); + + let admin_cap = margin_registry::new_for_testing(test.ctx()); + + clock.share_for_testing(); + + (test, admin_cap) +} + +public fun setup_margin_registry(): (Scenario, Clock, MarginAdminCap, MaintainerCap) { + let (mut scenario, admin_cap) = setup_test(); + let mut clock = clock::create_for_testing(scenario.ctx()); + clock.set_for_testing(1000000); + + scenario.next_tx(test_constants::admin()); + let mut registry = scenario.take_shared(); + let maintainer_cap = registry.mint_maintainer_cap(&admin_cap, &clock, scenario.ctx()); + let pyth_config = create_test_pyth_config(); + registry.add_config(&admin_cap, pyth_config); + return_shared(registry); + + (scenario, clock, admin_cap, maintainer_cap) +} + +/// Authorize MarginApp to create balance managers with custom owners +public fun authorize_margin_app(scenario: &mut Scenario, registry_id: ID) { + scenario.next_tx(test_constants::admin()); + let deepbook_admin_cap = registry::get_admin_cap_for_testing(scenario.ctx()); + let mut registry = scenario.take_shared_by_id(registry_id); + registry.authorize_app(&deepbook_admin_cap); + return_shared(registry); + destroy(deepbook_admin_cap); +} + +public fun create_margin_pool( + test: &mut Scenario, + maintainer_cap: &MaintainerCap, + protocol_config: ProtocolConfig, + clock: &Clock, +): ID { + test.next_tx(test_constants::admin()); + + let mut registry = test.take_shared(); + + let pool_id = margin_pool::create_margin_pool( + &mut registry, + protocol_config, + maintainer_cap, + clock, + test.ctx(), + ); + return_shared(registry); + + pool_id +} + +/// Helper function to retrieve two MarginPoolCaps and return them in the correct order +public fun get_margin_pool_caps( + scenario: &mut Scenario, + base_pool_id: ID, +): (MarginPoolCap, MarginPoolCap) { + scenario.next_tx(test_constants::admin()); + let cap1 = scenario.take_from_sender(); + let cap2 = scenario.take_from_sender(); + + if (cap1.margin_pool_id() == base_pool_id) { + (cap1, cap2) + } else { + (cap2, cap1) + } +} + +/// Helper function to retrieve a single MarginPoolCap for a specific pool +public fun get_margin_pool_cap(scenario: &mut Scenario, pool_id: ID): MarginPoolCap { + scenario.next_tx(test_constants::admin()); + let cap = scenario.take_from_sender(); + assert!(cap.margin_pool_id() == pool_id); + cap +} + +public fun default_protocol_config(): ProtocolConfig { + let margin_pool_config = protocol_config::new_margin_pool_config( + test_constants::supply_cap(), + test_constants::max_utilization_rate(), + test_constants::protocol_spread(), + test_constants::min_borrow(), + ); + let interest_config = protocol_config::new_interest_config( + test_constants::base_rate(), // base_rate: 5% with 9 decimals + test_constants::base_slope(), // base_slope: 10% with 9 decimals + test_constants::optimal_utilization(), // optimal_utilization: 80% with 9 decimals + test_constants::excess_slope(), // excess_slope: 200% with 9 decimals + ); + + protocol_config::new_protocol_config(margin_pool_config, interest_config) +} + +public fun create_pool_with_rate_limit( + registry: &mut MarginRegistry, + maintainer_cap: &MaintainerCap, + supply_cap: u64, + rate_limit_capacity: u64, + rate_limit_refill_rate_per_ms: u64, + rate_limit_enabled: bool, + clock: &Clock, + scenario: &mut Scenario, +): ID { + scenario.next_tx(test_constants::admin()); + + let margin_pool_config = protocol_config::new_margin_pool_config_with_rate_limit( + supply_cap, + test_constants::max_utilization_rate(), + test_constants::protocol_spread(), + test_constants::min_borrow(), + rate_limit_capacity, + rate_limit_refill_rate_per_ms, + rate_limit_enabled, + ); + let interest_config = protocol_config::new_interest_config( + test_constants::base_rate(), + test_constants::base_slope(), + test_constants::optimal_utilization(), + test_constants::excess_slope(), + ); + let config = protocol_config::new_protocol_config(margin_pool_config, interest_config); + + let pool_id = margin_pool::create_margin_pool( + registry, + config, + maintainer_cap, + clock, + scenario.ctx(), + ); + + pool_id +} + +public fun mint_coin(amount: u64, ctx: &mut TxContext): Coin { + coin::mint_for_testing(amount, ctx) +} + +/// Helper function to supply to a margin pool with a SupplierCap +/// Returns the SupplierCap which must be used for withdrawals and eventually destroyed +public fun supply_to_pool( + pool: &mut MarginPool, + registry: &MarginRegistry, + amount: u64, + clock: &Clock, + ctx: &mut TxContext, +): margin_pool::SupplierCap { + let supplier_cap = margin_pool::mint_supplier_cap(registry, clock, ctx); + let supply_coin = mint_coin(amount, ctx); + pool.supply(registry, &supplier_cap, supply_coin, option::none(), clock); + supplier_cap +} + +/// Create a DeepBook pool for testing. Returns (pool_id, registry_id). +public fun create_pool_for_testing(scenario: &mut Scenario): (ID, ID) { + let registry_id = registry::test_registry(scenario.ctx()); + + // Authorize MarginApp to create BalanceManagers with custom owners + authorize_margin_app(scenario, registry_id); + + scenario.next_tx(test_constants::admin()); + let mut registry = scenario.take_shared_by_id(registry_id); + + let pool_id = pool::create_permissionless_pool( + &mut registry, + constants::tick_size(), + constants::lot_size(), + constants::min_size(), + mint_coin(constants::pool_creation_fee(), scenario.ctx()), + scenario.ctx(), + ); + + return_shared(registry); + (pool_id, registry_id) +} + +/// Enable margin trading on a DeepBook pool +public fun enable_deepbook_margin_on_pool( + pool_id: ID, + margin_registry: &mut MarginRegistry, + admin_cap: &MarginAdminCap, + clock: &Clock, + scenario: &mut Scenario, +) { + scenario.next_tx(test_constants::admin()); + let mut pool = scenario.take_shared_by_id>(pool_id); + + let pool_config = create_test_pool_config(margin_registry); + margin_registry.register_deepbook_pool( + admin_cap, + &pool, + pool_config, + clock, + ); + + margin_registry.enable_deepbook_pool( + admin_cap, + &mut pool, + clock, + ); + return_shared(pool); +} + +/// Create a test pool configuration +public fun create_test_pool_config( + margin_registry: &MarginRegistry, +): PoolConfig { + margin_registry::new_pool_config( + margin_registry, + test_constants::min_withdraw_risk_ratio(), + test_constants::min_borrow_risk_ratio(), + test_constants::liquidation_risk_ratio(), + test_constants::target_liquidation_risk_ratio(), + test_constants::user_liquidation_reward(), + test_constants::pool_liquidation_reward(), + ) +} + +/// Cleanup test resources +public fun cleanup_margin_test( + registry: MarginRegistry, + admin_cap: MarginAdminCap, + maintainer_cap: MaintainerCap, + clock: Clock, + scenario: Scenario, +) { + destroy(registry); + destroy(admin_cap); + destroy(maintainer_cap); + destroy(clock); + scenario.end(); +} + +// === Pyth Oracle Test Utilities === + +/// Build a Pyth price info object for testing +public fun build_pyth_price_info_object( + scenario: &mut Scenario, + id: vector, + price_value: u64, + conf_value: u64, + exp_value: u64, + timestamp: u64, +): PriceInfoObject { + let price_id = price_identifier::from_byte_vec(id); + let price = price::new( + i64::new(price_value, false), // positive price + conf_value, + i64::new(exp_value, true), // negative exponent + timestamp, + ); + let price_feed = price_feed::new(price_id, price, price); + let price_info = price_info::new_price_info( + timestamp - 2, // attestation_time + timestamp - 1, // arrival_time + price_feed, + ); + price_info::new_price_info_object_for_test(price_info, scenario.ctx()) +} + +/// Build a demo USDC price info object at $1.00 +public fun build_demo_usdc_price_info_object( + scenario: &mut Scenario, + clock: &Clock, +): PriceInfoObject { + // USDC at exactly $1.00 + build_pyth_price_info_object( + scenario, + test_constants::usdc_price_feed_id(), + 1 * test_constants::pyth_multiplier(), + 50000, + test_constants::pyth_decimals(), + clock.timestamp_ms() / 1000, + ) +} + +/// Build a demo USDC price info object at $1.00 +public fun build_demo_usdc_price_info_object_with_price( + scenario: &mut Scenario, + price: u64, + clock: &Clock, +): PriceInfoObject { + build_pyth_price_info_object( + scenario, + test_constants::usdc_price_feed_id(), + price, + 50000, + test_constants::pyth_decimals(), + clock.timestamp_ms() / 1000, + ) +} + +/// Build a demo USDT price info object at $1.00 +public fun build_demo_usdt_price_info_object( + scenario: &mut Scenario, + clock: &Clock, +): PriceInfoObject { + // USDT at exactly $1.00 + build_pyth_price_info_object( + scenario, + test_constants::usdt_price_feed_id(), + 1 * test_constants::pyth_multiplier(), + 50000, + test_constants::pyth_decimals(), + clock.timestamp_ms() / 1000, + ) +} + +/// Build a BTC price info object at a given price +public fun build_btc_price_info_object( + scenario: &mut Scenario, + price_usd: u64, + clock: &Clock, +): PriceInfoObject { + build_pyth_price_info_object( + scenario, + test_constants::btc_price_feed_id(), + price_usd * test_constants::pyth_multiplier(), + 1000000, + test_constants::pyth_decimals(), + clock.timestamp_ms() / 1000, + ) +} + +/// Build a SUI price info object at a given price +public fun build_sui_price_info_object( + scenario: &mut Scenario, + price_usd: u64, + clock: &Clock, +): PriceInfoObject { + build_pyth_price_info_object( + scenario, + test_constants::sui_price_feed_id(), + price_usd * test_constants::pyth_multiplier(), + 100000, + test_constants::pyth_decimals(), + clock.timestamp_ms() / 1000, + ) +} + +/// Create a test PythConfig for all test coins +public fun create_test_pyth_config(): PythConfig { + let mut coin_data_vec = vector[]; + + // Add USDC configuration (6 decimals) + let usdc_data = oracle::test_coin_type_data( + 6, // decimals + test_constants::usdc_price_feed_id(), + ); + coin_data_vec.push_back(usdc_data); + + // Add USDT configuration (6 decimals) + let usdt_data = oracle::test_coin_type_data( + 6, // decimals + test_constants::usdt_price_feed_id(), + ); + coin_data_vec.push_back(usdt_data); + + // Add BTC configuration (8 decimals) + let btc_data = oracle::test_coin_type_data( + 8, // decimals + test_constants::btc_price_feed_id(), + ); + coin_data_vec.push_back(btc_data); + + // Add SUI configuration (9 decimals) + let sui_data = oracle::test_coin_type_data( + 9, // decimals + test_constants::sui_price_feed_id(), + ); + coin_data_vec.push_back(sui_data); + + oracle::new_pyth_config( + coin_data_vec, + 60, // max age 60 seconds + ) +} + +public fun setup_usdc_usdt_deepbook_margin(): ( + Scenario, + Clock, + MarginAdminCap, + MaintainerCap, + ID, + ID, + ID, + ID, +) { + let (mut scenario, mut clock, admin_cap, maintainer_cap) = setup_margin_registry(); + + clock.set_for_testing(1000000); + let usdc_pool_id = create_margin_pool( + &mut scenario, + &maintainer_cap, + default_protocol_config(), + &clock, + ); + let usdt_pool_id = create_margin_pool( + &mut scenario, + &maintainer_cap, + default_protocol_config(), + &clock, + ); + + scenario.next_tx(test_constants::admin()); + let (usdc_pool_cap, usdt_pool_cap) = get_margin_pool_caps(&mut scenario, usdc_pool_id); + + let (pool_id, registry_id) = create_pool_for_testing(&mut scenario); + scenario.next_tx(test_constants::admin()); + let mut registry = scenario.take_shared(); + enable_deepbook_margin_on_pool( + pool_id, + &mut registry, + &admin_cap, + &clock, + &mut scenario, + ); + return_shared(registry); + + scenario.next_tx(test_constants::admin()); + let mut usdt_pool = scenario.take_shared_by_id>(usdt_pool_id); + let mut usdc_pool = scenario.take_shared_by_id>(usdc_pool_id); + let registry = scenario.take_shared(); + let supplier_cap = margin_pool::mint_supplier_cap(®istry, &clock, scenario.ctx()); + + usdc_pool.supply( + ®istry, + &supplier_cap, + mint_coin(1_000_000 * test_constants::usdc_multiplier(), scenario.ctx()), + option::none(), + &clock, + ); + usdt_pool.supply( + ®istry, + &supplier_cap, + mint_coin(1_000_000 * test_constants::usdt_multiplier(), scenario.ctx()), + option::none(), + &clock, + ); + + usdt_pool.enable_deepbook_pool_for_loan(®istry, pool_id, &usdt_pool_cap, &clock); + usdc_pool.enable_deepbook_pool_for_loan(®istry, pool_id, &usdc_pool_cap, &clock); + + test::return_shared(usdc_pool); + test::return_shared(usdt_pool); + test::return_shared(registry); + scenario.return_to_sender(usdt_pool_cap); + scenario.return_to_sender(usdc_pool_cap); + destroy(supplier_cap); + + (scenario, clock, admin_cap, maintainer_cap, usdc_pool_id, usdt_pool_id, pool_id, registry_id) +} + +/// Helper function to set up a complete BTC/USD margin trading environment +/// Returns: (scenario, clock, admin_cap, maintainer_cap, btc_pool_id, usdc_pool_id, deepbook_pool_id, registry_id) +public fun setup_btc_usd_deepbook_margin(): ( + Scenario, + Clock, + MarginAdminCap, + MaintainerCap, + ID, + ID, + ID, + ID, +) { + let (mut scenario, mut clock, admin_cap, maintainer_cap) = setup_margin_registry(); + + clock.set_for_testing(1000000); + let btc_pool_id = create_margin_pool( + &mut scenario, + &maintainer_cap, + default_protocol_config(), + &clock, + ); + let usdc_pool_id = create_margin_pool( + &mut scenario, + &maintainer_cap, + default_protocol_config(), + &clock, + ); + + scenario.next_tx(test_constants::admin()); + let (btc_pool_cap, usdc_pool_cap) = get_margin_pool_caps(&mut scenario, btc_pool_id); + + let (pool_id, registry_id) = create_pool_for_testing(&mut scenario); + scenario.next_tx(test_constants::admin()); + let mut registry = scenario.take_shared(); + enable_deepbook_margin_on_pool( + pool_id, + &mut registry, + &admin_cap, + &clock, + &mut scenario, + ); + return_shared(registry); + + scenario.next_tx(test_constants::admin()); + let mut btc_pool = scenario.take_shared_by_id>(btc_pool_id); + let mut usdc_pool = scenario.take_shared_by_id>(usdc_pool_id); + let registry = scenario.take_shared(); + let supplier_cap = margin_pool::mint_supplier_cap(®istry, &clock, scenario.ctx()); + + btc_pool.supply( + ®istry, + &supplier_cap, + mint_coin(10 * test_constants::btc_multiplier(), scenario.ctx()), + option::none(), + &clock, + ); + usdc_pool.supply( + ®istry, + &supplier_cap, + mint_coin(1_000_000 * test_constants::usdc_multiplier(), scenario.ctx()), + option::none(), + &clock, + ); + + btc_pool.enable_deepbook_pool_for_loan(®istry, pool_id, &btc_pool_cap, &clock); + usdc_pool.enable_deepbook_pool_for_loan(®istry, pool_id, &usdc_pool_cap, &clock); + + test::return_shared(btc_pool); + test::return_shared(usdc_pool); + test::return_shared(registry); + scenario.return_to_sender(btc_pool_cap); + scenario.return_to_sender(usdc_pool_cap); + destroy(supplier_cap); + + (scenario, clock, admin_cap, maintainer_cap, btc_pool_id, usdc_pool_id, pool_id, registry_id) +} + +/// Helper function to set up a complete BTC/SUI margin trading environment +/// Returns: (scenario, clock, admin_cap, maintainer_cap, btc_pool_id, sui_pool_id, deepbook_pool_id, registry_id) +public fun setup_btc_sui_deepbook_margin(): ( + Scenario, + Clock, + MarginAdminCap, + MaintainerCap, + ID, + ID, + ID, + ID, +) { + let (mut scenario, mut clock, admin_cap, maintainer_cap) = setup_margin_registry(); + + clock.set_for_testing(1000000); + let btc_pool_id = create_margin_pool( + &mut scenario, + &maintainer_cap, + default_protocol_config(), + &clock, + ); + let sui_pool_id = create_margin_pool( + &mut scenario, + &maintainer_cap, + default_protocol_config(), + &clock, + ); + + scenario.next_tx(test_constants::admin()); + let (btc_pool_cap, sui_pool_cap) = get_margin_pool_caps(&mut scenario, btc_pool_id); + + let (pool_id, registry_id) = create_pool_for_testing(&mut scenario); + scenario.next_tx(test_constants::admin()); + let mut registry = scenario.take_shared(); + enable_deepbook_margin_on_pool( + pool_id, + &mut registry, + &admin_cap, + &clock, + &mut scenario, + ); + return_shared(registry); + + scenario.next_tx(test_constants::admin()); + let mut btc_pool = scenario.take_shared_by_id>(btc_pool_id); + let mut sui_pool = scenario.take_shared_by_id>(sui_pool_id); + let registry = scenario.take_shared(); + let supplier_cap = margin_pool::mint_supplier_cap(®istry, &clock, scenario.ctx()); + + btc_pool.supply( + ®istry, + &supplier_cap, + mint_coin(10 * test_constants::btc_multiplier(), scenario.ctx()), + option::none(), + &clock, + ); + sui_pool.supply( + ®istry, + &supplier_cap, + mint_coin(1_000_000 * test_constants::sui_multiplier(), scenario.ctx()), + option::none(), + &clock, + ); + + btc_pool.enable_deepbook_pool_for_loan(®istry, pool_id, &btc_pool_cap, &clock); + sui_pool.enable_deepbook_pool_for_loan(®istry, pool_id, &sui_pool_cap, &clock); + + test::return_shared(btc_pool); + test::return_shared(sui_pool); + test::return_shared(registry); + scenario.return_to_sender(btc_pool_cap); + scenario.return_to_sender(sui_pool_cap); + destroy(supplier_cap); + + (scenario, clock, admin_cap, maintainer_cap, btc_pool_id, sui_pool_id, pool_id, registry_id) +} + +public fun advance_time(clock: &mut Clock, ms: u64) { + let current_time = clock.timestamp_ms(); + clock.set_for_testing(current_time + ms); +} + +public fun interest_rate( + utilization_rate: u64, + base_rate: u64, + base_slope: u64, + optimal_utilization: u64, + excess_slope: u64, +): u64 { + if (utilization_rate < optimal_utilization) { + // Use base slope + math::mul(utilization_rate, base_slope) + base_rate + } else { + // Use base slope and excess slope + let excess_utilization = utilization_rate - optimal_utilization; + let excess_rate = math::mul(excess_utilization, excess_slope); + + base_rate + math::mul(optimal_utilization, base_slope) + excess_rate + } +} + +/// Setup a complete margin trading environment with margin manager for pool proxy testing +/// Returns: (scenario, clock, admin_cap, maintainer_cap, base_pool_id, quote_pool_id, deepbook_pool_id, registry_id) +public fun setup_pool_proxy_test_env(): ( + Scenario, + Clock, + MarginAdminCap, + MaintainerCap, + ID, + ID, + ID, + ID, +) { + let (mut scenario, mut clock, admin_cap, maintainer_cap) = setup_margin_registry(); + + clock.set_for_testing(1000000); + + // Create margin pools + let base_pool_id = create_margin_pool( + &mut scenario, + &maintainer_cap, + default_protocol_config(), + &clock, + ); + let quote_pool_id = create_margin_pool( + &mut scenario, + &maintainer_cap, + default_protocol_config(), + &clock, + ); + + // Get pool caps + let (base_pool_cap, quote_pool_cap) = get_margin_pool_caps(&mut scenario, base_pool_id); + + // Create DeepBook pool + let (pool_id, registry_id) = create_pool_for_testing(&mut scenario); + + // Enable margin trading + scenario.next_tx(test_constants::admin()); + let mut registry = scenario.take_shared(); + enable_deepbook_margin_on_pool( + pool_id, + &mut registry, + &admin_cap, + &clock, + &mut scenario, + ); + return_shared(registry); + + // Setup liquidity for margin pools + scenario.next_tx(test_constants::admin()); + let mut base_pool = scenario.take_shared_by_id>(base_pool_id); + let mut quote_pool = scenario.take_shared_by_id>(quote_pool_id); + let registry = scenario.take_shared(); + let supplier_cap = margin_pool::mint_supplier_cap(®istry, &clock, scenario.ctx()); + + base_pool.supply( + ®istry, + &supplier_cap, + mint_coin(1_000_000 * test_constants::usdc_multiplier(), scenario.ctx()), + option::none(), + &clock, + ); + quote_pool.supply( + ®istry, + &supplier_cap, + mint_coin(1_000_000 * test_constants::usdt_multiplier(), scenario.ctx()), + option::none(), + &clock, + ); + + base_pool.enable_deepbook_pool_for_loan(®istry, pool_id, &base_pool_cap, &clock); + quote_pool.enable_deepbook_pool_for_loan(®istry, pool_id, "e_pool_cap, &clock); + + return_shared_2!(base_pool, quote_pool); + return_shared(registry); + return_to_sender_2!(&scenario, base_pool_cap, quote_pool_cap); + destroy(supplier_cap); + + (scenario, clock, admin_cap, maintainer_cap, base_pool_id, quote_pool_id, pool_id, registry_id) +} diff --git a/packages/deepbook_margin/tests/margin_manager_borrow_share_tests.move b/packages/deepbook_margin/tests/margin_manager_borrow_share_tests.move new file mode 100644 index 000000000..ee85d3a28 --- /dev/null +++ b/packages/deepbook_margin/tests/margin_manager_borrow_share_tests.move @@ -0,0 +1,573 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +#[test_only] +module deepbook_margin::margin_manager_borrow_share_tests; + +use deepbook::{pool::Pool, registry::Registry}; +use deepbook_margin::{ + margin_manager::{Self, MarginManager}, + margin_pool::MarginPool, + margin_registry::MarginRegistry, + test_constants::{Self, USDC, BTC, btc_multiplier}, + test_helpers::{ + setup_btc_usd_deepbook_margin, + cleanup_margin_test, + mint_coin, + build_demo_usdc_price_info_object, + build_btc_price_info_object, + destroy_2, + return_shared_2, + return_shared_3, + supply_to_pool + } +}; +use std::unit_test::destroy; +use sui::test_scenario::return_shared; + +#[test] +fun test_multiple_borrows_accumulate_shares_base() { + let ( + mut scenario, + clock, + admin_cap, + maintainer_cap, + btc_pool_id, + usdc_pool_id, + _pool_id, + registry_id, + ) = setup_btc_usd_deepbook_margin(); + + // Supply liquidity to BTC pool + scenario.next_tx(test_constants::admin()); + let mut btc_pool = scenario.take_shared_by_id>(btc_pool_id); + let registry = scenario.take_shared(); + let supplier_cap = supply_to_pool( + &mut btc_pool, + ®istry, + 100 * btc_multiplier(), + &clock, + scenario.ctx(), + ); + return_shared_2!(btc_pool, registry); + destroy(supplier_cap); + + // Create margin manager and deposit collateral + scenario.next_tx(test_constants::user1()); + let pool = scenario.take_shared>(); + let mut registry = scenario.take_shared(); + let deepbook_registry = scenario.take_shared_by_id(registry_id); + margin_manager::new( + &pool, + &deepbook_registry, + &mut registry, + &clock, + scenario.ctx(), + ); + return_shared(deepbook_registry); + return_shared_2!(pool, registry); + + scenario.next_tx(test_constants::user1()); + let mut mm = scenario.take_shared>(); + let registry = scenario.take_shared(); + let btc_price = build_btc_price_info_object(&mut scenario, 50000, &clock); + let usdc_price = build_demo_usdc_price_info_object(&mut scenario, &clock); + // Deposit significant USDC as collateral + let deposit_coin = mint_coin( + 5_000_000 * test_constants::usdc_multiplier(), + scenario.ctx(), + ); + mm.deposit( + ®istry, + &btc_price, + &usdc_price, + deposit_coin, + &clock, + scenario.ctx(), + ); + destroy_2!(btc_price, usdc_price); + return_shared_2!(mm, registry); + + // First borrow: 10 BTC when ratio is 1 + scenario.next_tx(test_constants::user1()); + let mut mm = scenario.take_shared>(); + let registry = scenario.take_shared(); + let mut btc_pool = scenario.take_shared_by_id>(btc_pool_id); + let usdc_pool = scenario.take_shared_by_id>(usdc_pool_id); + let pool = scenario.take_shared>(); + let btc_price = build_btc_price_info_object(&mut scenario, 50000, &clock); + let usdc_price = build_demo_usdc_price_info_object(&mut scenario, &clock); + + mm.borrow_base( + ®istry, + &mut btc_pool, + &btc_price, + &usdc_price, + &pool, + 10 * btc_multiplier(), + &clock, + scenario.ctx(), + ); + + let borrowed_base_shares_after_first = mm.borrowed_base_shares(); + // At ratio 1, borrowing 10 should give us 10 shares + assert!(borrowed_base_shares_after_first == 10 * btc_multiplier()); + + // Second borrow: 15 BTC more + mm.borrow_base( + ®istry, + &mut btc_pool, + &btc_price, + &usdc_price, + &pool, + 15 * btc_multiplier(), + &clock, + scenario.ctx(), + ); + + let borrowed_base_shares_after_second = mm.borrowed_base_shares(); + // Total shares should be 10 + 15 = 25 + assert!(borrowed_base_shares_after_second == 25 * btc_multiplier()); + assert!(mm.borrowed_quote_shares() == 0); + + return_shared_3!(btc_pool, usdc_pool, pool); + return_shared(mm); + destroy_2!(btc_price, usdc_price); + cleanup_margin_test(registry, admin_cap, maintainer_cap, clock, scenario); +} + +#[test] +fun test_multiple_borrows_accumulate_shares_quote() { + let ( + mut scenario, + clock, + admin_cap, + maintainer_cap, + btc_pool_id, + usdc_pool_id, + _pool_id, + registry_id, + ) = setup_btc_usd_deepbook_margin(); + + // Supply liquidity to USDC pool + scenario.next_tx(test_constants::admin()); + let mut usdc_pool = scenario.take_shared_by_id>(usdc_pool_id); + let registry = scenario.take_shared(); + let supplier_cap = supply_to_pool( + &mut usdc_pool, + ®istry, + 1_000_000 * test_constants::usdc_multiplier(), + &clock, + scenario.ctx(), + ); + return_shared_2!(usdc_pool, registry); + destroy(supplier_cap); + + // Create margin manager and deposit collateral + scenario.next_tx(test_constants::user1()); + let pool = scenario.take_shared>(); + let mut registry = scenario.take_shared(); + let deepbook_registry = scenario.take_shared_by_id(registry_id); + margin_manager::new( + &pool, + &deepbook_registry, + &mut registry, + &clock, + scenario.ctx(), + ); + return_shared(deepbook_registry); + return_shared_2!(pool, registry); + + scenario.next_tx(test_constants::user1()); + let mut mm = scenario.take_shared>(); + let registry = scenario.take_shared(); + let btc_price = build_btc_price_info_object(&mut scenario, 50000, &clock); + let usdc_price = build_demo_usdc_price_info_object(&mut scenario, &clock); + // Deposit significant BTC as collateral + let deposit_coin = mint_coin(10 * btc_multiplier(), scenario.ctx()); + mm.deposit( + ®istry, + &btc_price, + &usdc_price, + deposit_coin, + &clock, + scenario.ctx(), + ); + destroy_2!(btc_price, usdc_price); + return_shared_2!(mm, registry); + + // First borrow: 10 USDC when ratio is 1 + scenario.next_tx(test_constants::user1()); + let mut mm = scenario.take_shared>(); + let registry = scenario.take_shared(); + let btc_pool = scenario.take_shared_by_id>(btc_pool_id); + let mut usdc_pool = scenario.take_shared_by_id>(usdc_pool_id); + let pool = scenario.take_shared>(); + let btc_price = build_btc_price_info_object(&mut scenario, 50000, &clock); + let usdc_price = build_demo_usdc_price_info_object(&mut scenario, &clock); + + mm.borrow_quote( + ®istry, + &mut usdc_pool, + &btc_price, + &usdc_price, + &pool, + 10 * test_constants::usdc_multiplier(), + &clock, + scenario.ctx(), + ); + + let borrowed_quote_shares_after_first = mm.borrowed_quote_shares(); + // At ratio 1, borrowing 10 should give us 10 shares + assert!(borrowed_quote_shares_after_first == 10 * test_constants::usdc_multiplier()); + + // Second borrow: 15 USDC more + mm.borrow_quote( + ®istry, + &mut usdc_pool, + &btc_price, + &usdc_price, + &pool, + 15 * test_constants::usdc_multiplier(), + &clock, + scenario.ctx(), + ); + + let borrowed_quote_shares_after_second = mm.borrowed_quote_shares(); + // Total shares should be 10 + 15 = 25 + assert!(borrowed_quote_shares_after_second == 25 * test_constants::usdc_multiplier()); + assert!(mm.borrowed_base_shares() == 0); + + return_shared_3!(btc_pool, usdc_pool, pool); + return_shared(mm); + destroy_2!(btc_price, usdc_price); + cleanup_margin_test(registry, admin_cap, maintainer_cap, clock, scenario); +} + +#[test] +fun test_user_shares_isolated_from_other_users_base() { + let ( + mut scenario, + clock, + admin_cap, + maintainer_cap, + btc_pool_id, + usdc_pool_id, + _pool_id, + registry_id, + ) = setup_btc_usd_deepbook_margin(); + + // Supply liquidity to BTC pool + scenario.next_tx(test_constants::admin()); + let mut btc_pool = scenario.take_shared_by_id>(btc_pool_id); + let registry = scenario.take_shared(); + let supplier_cap = supply_to_pool( + &mut btc_pool, + ®istry, + 100 * btc_multiplier(), + &clock, + scenario.ctx(), + ); + return_shared_2!(btc_pool, registry); + destroy(supplier_cap); + + // User1 creates margin manager and borrows first + scenario.next_tx(test_constants::user1()); + let pool = scenario.take_shared>(); + let mut registry = scenario.take_shared(); + let deepbook_registry = scenario.take_shared_by_id(registry_id); + margin_manager::new( + &pool, + &deepbook_registry, + &mut registry, + &clock, + scenario.ctx(), + ); + return_shared(deepbook_registry); + return_shared_2!(pool, registry); + + scenario.next_tx(test_constants::user1()); + let mut mm1 = scenario.take_shared>(); + let registry = scenario.take_shared(); + let btc_price = build_btc_price_info_object(&mut scenario, 50000, &clock); + let usdc_price = build_demo_usdc_price_info_object(&mut scenario, &clock); + let deposit_coin = mint_coin( + 5_000_000 * test_constants::usdc_multiplier(), + scenario.ctx(), + ); + mm1.deposit( + ®istry, + &btc_price, + &usdc_price, + deposit_coin, + &clock, + scenario.ctx(), + ); + destroy_2!(btc_price, usdc_price); + return_shared_2!(mm1, registry); + + // User1 borrows 20 BTC + scenario.next_tx(test_constants::user1()); + let mut mm1 = scenario.take_shared>(); + let registry = scenario.take_shared(); + let mut btc_pool = scenario.take_shared_by_id>(btc_pool_id); + let usdc_pool = scenario.take_shared_by_id>(usdc_pool_id); + let pool = scenario.take_shared>(); + let btc_price = build_btc_price_info_object(&mut scenario, 50000, &clock); + let usdc_price = build_demo_usdc_price_info_object(&mut scenario, &clock); + + mm1.borrow_base( + ®istry, + &mut btc_pool, + &btc_price, + &usdc_price, + &pool, + 20 * btc_multiplier(), + &clock, + scenario.ctx(), + ); + + // User1 should have 20 shares + assert!(mm1.borrowed_base_shares() == 20 * btc_multiplier()); + + // The pool now has total borrow shares of 20 + assert!(btc_pool.borrow_shares() == 20 * btc_multiplier()); + + return_shared_3!(btc_pool, usdc_pool, pool); + return_shared_2!(mm1, registry); + destroy_2!(btc_price, usdc_price); + + // User2 creates their own margin manager + scenario.next_tx(test_constants::user2()); + let pool = scenario.take_shared>(); + let mut registry = scenario.take_shared(); + let deepbook_registry = scenario.take_shared_by_id(registry_id); + margin_manager::new( + &pool, + &deepbook_registry, + &mut registry, + &clock, + scenario.ctx(), + ); + return_shared(deepbook_registry); + return_shared_2!(pool, registry); + + scenario.next_tx(test_constants::user2()); + let mut mm2 = scenario.take_shared>(); + let registry = scenario.take_shared(); + let btc_price = build_btc_price_info_object(&mut scenario, 50000, &clock); + let usdc_price = build_demo_usdc_price_info_object(&mut scenario, &clock); + let deposit_coin = mint_coin( + 5_000_000 * test_constants::usdc_multiplier(), + scenario.ctx(), + ); + mm2.deposit( + ®istry, + &btc_price, + &usdc_price, + deposit_coin, + &clock, + scenario.ctx(), + ); + destroy_2!(btc_price, usdc_price); + return_shared_2!(mm2, registry); + + // User2 borrows 10 BTC when ratio is still 1 + scenario.next_tx(test_constants::user2()); + let mut mm2 = scenario.take_shared>(); + let registry = scenario.take_shared(); + let mut btc_pool = scenario.take_shared_by_id>(btc_pool_id); + let usdc_pool = scenario.take_shared_by_id>(usdc_pool_id); + let pool = scenario.take_shared>(); + let btc_price = build_btc_price_info_object(&mut scenario, 50000, &clock); + let usdc_price = build_demo_usdc_price_info_object(&mut scenario, &clock); + + mm2.borrow_base( + ®istry, + &mut btc_pool, + &btc_price, + &usdc_price, + &pool, + 10 * btc_multiplier(), + &clock, + scenario.ctx(), + ); + + // User2 should have exactly 10 shares, NOT 30 (which would be the pool total) + assert!(mm2.borrowed_base_shares() == 10 * btc_multiplier()); + assert!(mm2.borrowed_quote_shares() == 0); + + // The pool should now have total borrow shares of 30 (20 + 10) + assert!(btc_pool.borrow_shares() == 30 * btc_multiplier()); + + return_shared_3!(btc_pool, usdc_pool, pool); + return_shared(mm2); + destroy_2!(btc_price, usdc_price); + + // The key verifications are: + // 1. mm2 has exactly 10 shares (not 30 which would be the bug) + // 2. The pool has 30 total shares (20 from mm1 + 10 from mm2) + + cleanup_margin_test(registry, admin_cap, maintainer_cap, clock, scenario); +} + +#[test] +fun test_user_shares_isolated_from_other_users_quote() { + let ( + mut scenario, + clock, + admin_cap, + maintainer_cap, + btc_pool_id, + usdc_pool_id, + _pool_id, + registry_id, + ) = setup_btc_usd_deepbook_margin(); + + // Supply liquidity to USDC pool + scenario.next_tx(test_constants::admin()); + let mut usdc_pool = scenario.take_shared_by_id>(usdc_pool_id); + let registry = scenario.take_shared(); + let supplier_cap = supply_to_pool( + &mut usdc_pool, + ®istry, + 1_000_000 * test_constants::usdc_multiplier(), + &clock, + scenario.ctx(), + ); + return_shared_2!(usdc_pool, registry); + destroy(supplier_cap); + + // User1 creates margin manager and borrows first + scenario.next_tx(test_constants::user1()); + let pool = scenario.take_shared>(); + let mut registry = scenario.take_shared(); + let deepbook_registry = scenario.take_shared_by_id(registry_id); + margin_manager::new( + &pool, + &deepbook_registry, + &mut registry, + &clock, + scenario.ctx(), + ); + return_shared(deepbook_registry); + return_shared_2!(pool, registry); + + scenario.next_tx(test_constants::user1()); + let mut mm1 = scenario.take_shared>(); + let registry = scenario.take_shared(); + let btc_price = build_btc_price_info_object(&mut scenario, 50000, &clock); + let usdc_price = build_demo_usdc_price_info_object(&mut scenario, &clock); + let deposit_coin = mint_coin(10 * btc_multiplier(), scenario.ctx()); + mm1.deposit( + ®istry, + &btc_price, + &usdc_price, + deposit_coin, + &clock, + scenario.ctx(), + ); + destroy_2!(btc_price, usdc_price); + return_shared_2!(mm1, registry); + + // User1 borrows 20 USDC + scenario.next_tx(test_constants::user1()); + let mut mm1 = scenario.take_shared>(); + let registry = scenario.take_shared(); + let btc_pool = scenario.take_shared_by_id>(btc_pool_id); + let mut usdc_pool = scenario.take_shared_by_id>(usdc_pool_id); + let pool = scenario.take_shared>(); + let btc_price = build_btc_price_info_object(&mut scenario, 50000, &clock); + let usdc_price = build_demo_usdc_price_info_object(&mut scenario, &clock); + + mm1.borrow_quote( + ®istry, + &mut usdc_pool, + &btc_price, + &usdc_price, + &pool, + 20 * test_constants::usdc_multiplier(), + &clock, + scenario.ctx(), + ); + + // User1 should have 20 shares + assert!(mm1.borrowed_quote_shares() == 20 * test_constants::usdc_multiplier()); + + // The pool now has total borrow shares of 20 + assert!(usdc_pool.borrow_shares() == 20 * test_constants::usdc_multiplier()); + + return_shared_3!(btc_pool, usdc_pool, pool); + return_shared_2!(mm1, registry); + destroy_2!(btc_price, usdc_price); + + // User2 creates their own margin manager + scenario.next_tx(test_constants::user2()); + let pool = scenario.take_shared>(); + let mut registry = scenario.take_shared(); + let deepbook_registry = scenario.take_shared_by_id(registry_id); + margin_manager::new( + &pool, + &deepbook_registry, + &mut registry, + &clock, + scenario.ctx(), + ); + return_shared(deepbook_registry); + return_shared_2!(pool, registry); + + scenario.next_tx(test_constants::user2()); + let mut mm2 = scenario.take_shared>(); + let registry = scenario.take_shared(); + let btc_price = build_btc_price_info_object(&mut scenario, 50000, &clock); + let usdc_price = build_demo_usdc_price_info_object(&mut scenario, &clock); + let deposit_coin = mint_coin(10 * btc_multiplier(), scenario.ctx()); + mm2.deposit( + ®istry, + &btc_price, + &usdc_price, + deposit_coin, + &clock, + scenario.ctx(), + ); + destroy_2!(btc_price, usdc_price); + return_shared_2!(mm2, registry); + + // User2 borrows 10 USDC when ratio is still 1 + scenario.next_tx(test_constants::user2()); + let mut mm2 = scenario.take_shared>(); + let registry = scenario.take_shared(); + let btc_pool = scenario.take_shared_by_id>(btc_pool_id); + let mut usdc_pool = scenario.take_shared_by_id>(usdc_pool_id); + let pool = scenario.take_shared>(); + let btc_price = build_btc_price_info_object(&mut scenario, 50000, &clock); + let usdc_price = build_demo_usdc_price_info_object(&mut scenario, &clock); + + mm2.borrow_quote( + ®istry, + &mut usdc_pool, + &btc_price, + &usdc_price, + &pool, + 10 * test_constants::usdc_multiplier(), + &clock, + scenario.ctx(), + ); + + // User2 should have exactly 10 shares, NOT 30 (which would be the pool total) + assert!(mm2.borrowed_quote_shares() == 10 * test_constants::usdc_multiplier()); + assert!(mm2.borrowed_base_shares() == 0); + + // The pool should now have total borrow shares of 30 (20 + 10) + assert!(usdc_pool.borrow_shares() == 30 * test_constants::usdc_multiplier()); + + return_shared_3!(btc_pool, usdc_pool, pool); + return_shared(mm2); + destroy_2!(btc_price, usdc_price); + + // The key verifications are: + // 1. mm2 has exactly 10 shares (not 30 which would be the bug) + // 2. The pool has 30 total shares (20 from mm1 + 10 from mm2) + + cleanup_margin_test(registry, admin_cap, maintainer_cap, clock, scenario); +} diff --git a/packages/deepbook_margin/tests/margin_manager_math_tests.move b/packages/deepbook_margin/tests/margin_manager_math_tests.move new file mode 100644 index 000000000..63cb01839 --- /dev/null +++ b/packages/deepbook_margin/tests/margin_manager_math_tests.move @@ -0,0 +1,856 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +#[test_only] +module deepbook_margin::margin_manager_math_tests; + +use deepbook::{pool::Pool, registry::Registry}; +use deepbook_margin::{ + margin_manager::{Self, MarginManager}, + margin_pool::MarginPool, + margin_registry::MarginRegistry, + test_constants::{Self, USDC, BTC, SUI, btc_multiplier, sui_multiplier, usdc_multiplier}, + test_helpers::{ + cleanup_margin_test, + mint_coin, + build_demo_usdc_price_info_object, + build_btc_price_info_object, + build_sui_price_info_object, + setup_btc_usd_deepbook_margin, + setup_btc_sui_deepbook_margin, + destroy_3, + return_shared_3, + return_shared_4 + } +}; +use std::unit_test::destroy; +use sui::test_scenario::return_shared; + +const ENoError: u64 = 0; +const ECannotLiquidate: u64 = 1; +const ECannotWithdraw: u64 = 2; + +#[test] +fun test_liquidation_ok() { + test_liquidation(ENoError); +} + +#[test, expected_failure(abort_code = margin_manager::ECannotLiquidate)] +fun test_liquidation_cannot_liquidate() { + test_liquidation(ECannotLiquidate); +} + +#[test] +fun test_liquidation_quote_debt_ok() { + test_liquidation_quote_debt(ENoError); +} + +#[test, expected_failure(abort_code = margin_manager::EWithdrawRiskRatioExceeded)] +fun test_liquidation_cannot_withdraw() { + test_liquidation_quote_debt(ECannotWithdraw); +} + +#[test] +fun test_liquidation_quote_debt_partial_ok() { + test_liquidation_quote_debt_partial(); +} + +#[test] +fun test_liquidation_base_debt_default_ok() { + test_liquidation_base_debt_default(); +} + +#[test] +fun test_liquidation_base_debt_ok() { + test_liquidation_base_debt(); +} + +#[test] +fun test_btc_sui_volatile_pair_ok() { + test_btc_sui_liquidation(ENoError); +} + +#[test, expected_failure(abort_code = margin_manager::ECannotLiquidate)] +fun test_btc_sui_cannot_liquidate() { + test_btc_sui_liquidation(ECannotLiquidate); +} + +fun test_liquidation(error_code: u64) { + let ( + mut scenario, + clock, + admin_cap, + maintainer_cap, + btc_pool_id, + usdc_pool_id, + _pool_id, + registry_id, + ) = setup_btc_usd_deepbook_margin(); + + let btc_price = build_btc_price_info_object(&mut scenario, 50, &clock); + let usdc_price = build_demo_usdc_price_info_object(&mut scenario, &clock); + + scenario.next_tx(test_constants::user1()); + let mut pool = scenario.take_shared>(); + let mut registry = scenario.take_shared(); + let deepbook_registry = scenario.take_shared_by_id(registry_id); + margin_manager::new( + &pool, + &deepbook_registry, + &mut registry, + &clock, + scenario.ctx(), + ); + return_shared(deepbook_registry); + + scenario.next_tx(test_constants::user1()); + let mut mm = scenario.take_shared>(); + let mut usdc_pool = scenario.take_shared_by_id>(usdc_pool_id); + let btc_pool = scenario.take_shared_by_id>(btc_pool_id); + + // Deposit 1 BTC worth $50 + mm.deposit( + ®istry, + &btc_price, + &usdc_price, + mint_coin(1 * btc_multiplier(), scenario.ctx()), + &clock, + scenario.ctx(), + ); + + // Borrow $200 USDC. Risk ratio = (50 + 200) / 200 = 1.25 + mm.borrow_quote( + ®istry, + &mut usdc_pool, + &btc_price, + &usdc_price, + &pool, + 200 * test_constants::usdc_multiplier(), + &clock, + scenario.ctx(), + ); + + assert!( + mm.risk_ratio(®istry, &btc_price, &usdc_price, &pool, &btc_pool,&usdc_pool, &clock) == 1_250_000_000, + 0, + ); + + // Perform liquidation and check rewards + scenario.next_tx(test_constants::liquidator()); + + if (error_code == ECannotLiquidate) { + // At BTC price 40, Risk ratio = (40 + 200) / 200 = 1.2, still cannot liquidate + let repay_coin = mint_coin(500 * test_constants::usdc_multiplier(), scenario.ctx()); + let btc_price_40 = build_btc_price_info_object(&mut scenario, 40, &clock); + assert!( + mm.risk_ratio(®istry, &btc_price_40, &usdc_price, &pool, &btc_pool, &usdc_pool, &clock) == 1_200_000_000, + 0, + ); + + let (_base_coin, _quote_coin, _remaining_repay_coin) = mm.liquidate( + ®istry, + &btc_price_40, + &usdc_price, + &mut usdc_pool, + &mut pool, + repay_coin, + &clock, + scenario.ctx(), + ); + abort + }; + + // At BTC price 10, Risk ratio = (18 + 200) / 200 = 218 / 200 = 1.09 < 1.1, can liquidate + let repay_coin = mint_coin(500 * test_constants::usdc_multiplier(), scenario.ctx()); + let btc_price_18 = build_btc_price_info_object(&mut scenario, 18, &clock); + assert!( + mm.risk_ratio(®istry, &btc_price_18, &usdc_price, &pool, &btc_pool, &usdc_pool, &clock) == 1_090_000_000, + 0, + ); + + // 164.8 USDC will be used to liquidate. 160 USDC for repayment of loan, 4.8 for pool liquidation fee. + // Since 160 USDC is used for repayment, the liquidator should receive 160 * 0.02 = 3.2 as a reward. + // Risk ratio after liquidation = (218 - 160 * 1.05) / (200 - 160) = 1.25 (our target liquidation) + // Remaining_repay_coin = 500 - 164.8 = 335.2 USDC + // The liquidator should receive 160 * 1.05 = 168 USDC. The net profit is 168 - 164.8 = 3.2 USDC + // 3.2 USDC / 160 USDC = 2% reward + let (base_coin, quote_coin, remaining_repay_coin) = mm.liquidate( + ®istry, + &btc_price_18, + &usdc_price, + &mut usdc_pool, + &mut pool, + repay_coin, + &clock, + scenario.ctx(), + ); + + assert!(base_coin.value() == 0); // 0 BTC + assert!(quote_coin.value() == 168 * test_constants::usdc_multiplier()); // 168 USDC + assert!(remaining_repay_coin.value() == 335_200_000); // 335.2 USDC + + destroy_3!(remaining_repay_coin, base_coin, quote_coin); + return_shared_4!(mm, usdc_pool, pool, btc_pool); + destroy_3!(btc_price, usdc_price, btc_price_18); + cleanup_margin_test(registry, admin_cap, maintainer_cap, clock, scenario); +} + +fun test_liquidation_quote_debt(error_code: u64) { + let ( + mut scenario, + clock, + admin_cap, + maintainer_cap, + btc_pool_id, + usdc_pool_id, + _pool_id, + registry_id, + ) = setup_btc_usd_deepbook_margin(); + + scenario.next_tx(test_constants::user1()); + let mut pool = scenario.take_shared>(); + let mut registry = scenario.take_shared(); + let deepbook_registry = scenario.take_shared_by_id(registry_id); + margin_manager::new( + &pool, + &deepbook_registry, + &mut registry, + &clock, + scenario.ctx(), + ); + return_shared(deepbook_registry); + + scenario.next_tx(test_constants::user1()); + let mut mm = scenario.take_shared>(); + let mut usdc_pool = scenario.take_shared_by_id>(usdc_pool_id); + let btc_pool = scenario.take_shared_by_id>(btc_pool_id); + let btc_price = build_btc_price_info_object(&mut scenario, 500, &clock); + let usdc_price = build_demo_usdc_price_info_object(&mut scenario, &clock); + + // Deposit 1 BTC worth $500 + mm.deposit( + ®istry, + &btc_price, + &usdc_price, + mint_coin(1 * btc_multiplier(), scenario.ctx()), + &clock, + scenario.ctx(), + ); + + // Borrow $200 USDC. Risk ratio = (500 + 200) / 200 = 3.5 + mm.borrow_quote( + ®istry, + &mut usdc_pool, + &btc_price, + &usdc_price, + &pool, + 200 * test_constants::usdc_multiplier(), + &clock, + scenario.ctx(), + ); + + assert!( + mm.risk_ratio(®istry, &btc_price, &usdc_price, &pool, &btc_pool, &usdc_pool, &clock) == 3_500_000_000, + 0, + ); + + // Now we withdraw 100 USDC. This should be allowed since risk ratio >= 2; + let withdraw_usdc = mm.withdraw( + ®istry, + &btc_pool, + &usdc_pool, + &btc_price, + &usdc_price, + &pool, + 100 * test_constants::usdc_multiplier(), + &clock, + scenario.ctx(), + ); + withdraw_usdc.burn_for_testing(); + + // Risk ratio is now (500 + 100) / 200 = 3.0 + assert!( + mm.risk_ratio(®istry, &btc_price, &usdc_price, &pool, &btc_pool, &usdc_pool, &clock) == 3_000_000_000, + 0, + ); + + if (error_code == ECannotWithdraw) { + // At BTC price 500, we try to withdraw half BTC. (250 + 100) / 200 = 1.75 < 2.0, cannot withdraw + let withdraw_usdc_2 = mm.withdraw( + ®istry, + &btc_pool, + &usdc_pool, + &btc_price, + &usdc_price, + &pool, + 5000_0000, + &clock, + scenario.ctx(), + ); + withdraw_usdc_2.burn_for_testing(); + abort + }; + + // Perform liquidation and check rewards + scenario.next_tx(test_constants::liquidator()); + + // At BTC price 115, Risk ratio = (115 + 100) / 200 = 1.075 < 1.1, can liquidate + let repay_coin = mint_coin(500 * test_constants::usdc_multiplier(), scenario.ctx()); + let btc_price_115 = build_btc_price_info_object(&mut scenario, 115, &clock); + assert!( + mm.risk_ratio(®istry, &btc_price_115, &usdc_price, &pool, &btc_pool, &usdc_pool, &clock) == 1_075_000_000, + 0, + ); + + // 180.25 USDC will be used to liquidate. 175 USDC for repayment of loan, 5.25 for pool liquidation fee. + // Since 175 USDC is used for repayment, the liquidator should receive 175 * 0.02 = 3.5 as a reward. + // Risk ratio after liquidation = (215 - 175 * 1.05) / (200 - 175) = 1.25 (our target liquidation) + // Remaining_repay_coin = 500 - 180.25 = 319.75 USDC + // The liquidator should receive 175 * 1.05 = 183.75 USDC. The net profit is 183.75 - 180.25 = 3.5 USDC + // 3.5 USDC / 175 USDC = 2% reward + // Since there's only 100 USDC in the manager, quote_coin will be 100 USDC + // The remaining 83.75 USDC will be taken as base_coin (in BTC). 83.75 / 115 = 0.728260869565217391 BTC + let (base_coin, quote_coin, remaining_repay_coin) = mm.liquidate( + ®istry, + &btc_price_115, + &usdc_price, + &mut usdc_pool, + &mut pool, + repay_coin, + &clock, + scenario.ctx(), + ); + + assert!(base_coin.value() == 72826087); // ~0.72826087 BTC + assert!(quote_coin.value() == 100 * test_constants::usdc_multiplier()); // 100 USDC + assert!(remaining_repay_coin.value() == 319_750_000); // 319.75 USDC + + destroy_3!(remaining_repay_coin, base_coin, quote_coin); + return_shared_3!(mm, usdc_pool, pool); + destroy_3!(btc_price, usdc_price, btc_price_115); + destroy(btc_pool); + cleanup_margin_test(registry, admin_cap, maintainer_cap, clock, scenario); +} + +fun test_liquidation_quote_debt_partial() { + let ( + mut scenario, + clock, + admin_cap, + maintainer_cap, + btc_pool_id, + usdc_pool_id, + _pool_id, + registry_id, + ) = setup_btc_usd_deepbook_margin(); + + scenario.next_tx(test_constants::user1()); + let mut pool = scenario.take_shared>(); + let mut registry = scenario.take_shared(); + let deepbook_registry = scenario.take_shared_by_id(registry_id); + margin_manager::new( + &pool, + &deepbook_registry, + &mut registry, + &clock, + scenario.ctx(), + ); + return_shared(deepbook_registry); + + scenario.next_tx(test_constants::user1()); + let mut mm = scenario.take_shared>(); + let mut usdc_pool = scenario.take_shared_by_id>(usdc_pool_id); + let btc_pool = scenario.take_shared_by_id>(btc_pool_id); + let btc_price = build_btc_price_info_object(&mut scenario, 500, &clock); + let usdc_price = build_demo_usdc_price_info_object(&mut scenario, &clock); + + // Deposit 1 BTC worth $500 + mm.deposit( + ®istry, + &btc_price, + &usdc_price, + mint_coin(1 * btc_multiplier(), scenario.ctx()), + &clock, + scenario.ctx(), + ); + + // Borrow $200 USDC. Risk ratio = (500 + 200) / 200 = 3.5 + mm.borrow_quote( + ®istry, + &mut usdc_pool, + &btc_price, + &usdc_price, + &pool, + 200 * test_constants::usdc_multiplier(), + &clock, + scenario.ctx(), + ); + + assert!( + mm.risk_ratio(®istry, &btc_price, &usdc_price, &pool, &btc_pool, &usdc_pool, &clock) == 3_500_000_000, + 0, + ); + + // Now we withdraw 100 USDC. This should be allowed since risk ratio >= 2; + let withdraw_usdc = mm.withdraw( + ®istry, + &btc_pool, + &usdc_pool, + &btc_price, + &usdc_price, + &pool, + 100 * test_constants::usdc_multiplier(), + &clock, + scenario.ctx(), + ); + withdraw_usdc.burn_for_testing(); + + // Risk ratio is now (500 + 100) / 200 = 3.0 + assert!( + mm.risk_ratio(®istry, &btc_price, &usdc_price, &pool, &btc_pool, &usdc_pool, &clock) == 3_000_000_000, + 0, + ); + + // Perform liquidation and check rewards + scenario.next_tx(test_constants::liquidator()); + + // At BTC price 115, Risk ratio = (115 + 100) / 200 = 1.075 < 1.1, can liquidate + let repay_coin = mint_coin(90_125_000, scenario.ctx()); + let btc_price_115 = build_btc_price_info_object(&mut scenario, 115, &clock); + assert!( + mm.risk_ratio(®istry, &btc_price_115, &usdc_price, &pool, &btc_pool, &usdc_pool, &clock) == 1_075_000_000, + 0, + ); + + // 90.125 USDC will be used to liquidate. 87.5 USDC for repayment of loan, 2.625 for pool liquidation fee. + // Since 87.5 USDC is used for repayment, the liquidator should receive 87.5 * 0.02 = 1.75 as a reward. + // Risk ratio after liquidation = (215 - 87.5 * 1.05) / (200 - 87.5) = 1.094 (not at target since this is a partial liquidation) + // Remaining_repay_coin = 0 USDC + // The liquidator should receive 87.5 * 1.05 = 91.875 USDC. The net profit is 91.875 - 90.125 = 1.75 USDC + // 1.75 USDC / 87.5 USDC = 2% reward + // Since there's 100 USDC in the manager, only USDC will be paid out + let (base_coin, quote_coin, remaining_repay_coin) = mm.liquidate( + ®istry, + &btc_price_115, + &usdc_price, + &mut usdc_pool, + &mut pool, + repay_coin, + &clock, + scenario.ctx(), + ); + + assert!(base_coin.value() == 0); // 0 BTC + assert!(quote_coin.value() == 91_875_000); // 91.875 USDC + assert!(remaining_repay_coin.value() == 0); // 0 USDC + destroy_3!(remaining_repay_coin, base_coin, quote_coin); + + // Since risk ratio still < 1.1, can liquidate again + let repay_coin = mint_coin(90_125_000, scenario.ctx()); + let (base_coin, quote_coin, remaining_repay_coin) = mm.liquidate( + ®istry, + &btc_price_115, + &usdc_price, + &mut usdc_pool, + &mut pool, + repay_coin, + &clock, + scenario.ctx(), + ); + + assert!(base_coin.value() == 72826087); // ~0.72826087 BTC + assert!(quote_coin.value() == 8_125_000); // 8.125 USDC + assert!(remaining_repay_coin.value() == 0); // 0 USDC + + destroy_3!(remaining_repay_coin, base_coin, quote_coin); + return_shared_3!(mm, usdc_pool, pool); + destroy_3!(btc_price, usdc_price, btc_price_115); + destroy(btc_pool); + cleanup_margin_test(registry, admin_cap, maintainer_cap, clock, scenario); +} + +fun test_liquidation_base_debt_default() { + let ( + mut scenario, + clock, + admin_cap, + maintainer_cap, + btc_pool_id, + usdc_pool_id, + _pool_id, + registry_id, + ) = setup_btc_usd_deepbook_margin(); + + let btc_price = build_btc_price_info_object(&mut scenario, 500, &clock); + let usdc_price = build_demo_usdc_price_info_object(&mut scenario, &clock); + + scenario.next_tx(test_constants::user1()); + let mut pool = scenario.take_shared>(); + let mut registry = scenario.take_shared(); + let deepbook_registry = scenario.take_shared_by_id(registry_id); + margin_manager::new( + &pool, + &deepbook_registry, + &mut registry, + &clock, + scenario.ctx(), + ); + return_shared(deepbook_registry); + + scenario.next_tx(test_constants::user1()); + let mut mm = scenario.take_shared>(); + let usdc_pool = scenario.take_shared_by_id>(usdc_pool_id); + let mut btc_pool = scenario.take_shared_by_id>(btc_pool_id); + + // Deposit 500 USDC + mm.deposit( + ®istry, + &btc_price, + &usdc_price, + mint_coin(500 * usdc_multiplier(), scenario.ctx()), + &clock, + scenario.ctx(), + ); + + // Borrow $200 BTC (0.4 BTC). Risk ratio = (500 + 200) / 200 = 3.5 + mm.borrow_base( + ®istry, + &mut btc_pool, + &btc_price, + &usdc_price, + &pool, + 40000000, + &clock, + scenario.ctx(), + ); + + assert!( + mm.risk_ratio(®istry, &btc_price, &usdc_price, &pool, &btc_pool, &usdc_pool, &clock) == 3_500_000_000, + 0, + ); + + // Now we withdraw 0.2 BTC. This should be allowed since risk ratio >= 2; + let withdraw_btc = mm.withdraw( + ®istry, + &btc_pool, + &usdc_pool, + &btc_price, + &usdc_price, + &pool, + 20000000, + &clock, + scenario.ctx(), + ); + withdraw_btc.burn_for_testing(); + + // Risk ratio is now (500 + 100) / 200 = 3.0 + assert!( + mm.risk_ratio(®istry, &btc_price, &usdc_price, &pool, &btc_pool, &usdc_pool, &clock) == 3_000_000_000, + 0, + ); + + // Perform liquidation and check rewards + scenario.next_tx(test_constants::liquidator()); + + // We now have 0.2 BTC ($100) and 500 USDC ($500), with a debt of 0.4 BTC ($200) + // At BTC price 3000, Risk ratio = (600 + 500) / 1200 = 0.916666666666666666 < 1.1, can liquidate + let repay_coin = mint_coin(1 * btc_multiplier(), scenario.ctx()); + let btc_price_3000 = build_btc_price_info_object(&mut scenario, 3000, &clock); + + // 0.3597 BTC will be used to liquidate. 0.3492 BTC for repayment of loan, 0.0105 BTC for pool liquidation fee. + // Since 0.3492 BTC is used for repayment, the liquidator should receive 0.3492 * 0.02 = 0.006984 as a reward. + // There should be a full liquidation of the margin manager since it's in default. + // Remaining_repay_coin = 1 - 0.3597 = 0.6403 BTC + // The liquidator should receive 0.3492 * 1.05 = 0.36666 BTC = 1100 USD. The net profit is 0.36666 - 0.3597 = 0.00696 BTC + // 0.00696 BTC / 0.3492 BTC = 2% reward + // The 0.2 BTC will be used first. 1100 - 0.2 * 3000 = 500 USD. Then the remaining 500 USD will be taken as USDC. + let (base_coin, quote_coin, remaining_repay_coin) = mm.liquidate( + ®istry, + &btc_price_3000, + &usdc_price, + &mut btc_pool, + &mut pool, + repay_coin, + &clock, + scenario.ctx(), + ); + + assert!(base_coin.value() == 20000000); // 0.2 BTC + assert!(quote_coin.value() == 499999980); // ~500 USDC. Rounding is due to conversion of BTC to USDC. + assert!(remaining_repay_coin.value() == 64031746); // 0.6403 BTC + + // The loans should be defaulted + assert!(mm.borrowed_base_shares() == 0); // 0 BTC + assert!(mm.borrowed_quote_shares() == 0); // 0 USDC + + destroy_3!(remaining_repay_coin, base_coin, quote_coin); + return_shared_3!(mm, usdc_pool, pool); + destroy_3!(btc_price, usdc_price, btc_price_3000); + destroy(btc_pool); + cleanup_margin_test(registry, admin_cap, maintainer_cap, clock, scenario); +} + +fun test_liquidation_base_debt() { + let ( + mut scenario, + clock, + admin_cap, + maintainer_cap, + btc_pool_id, + usdc_pool_id, + _pool_id, + registry_id, + ) = setup_btc_usd_deepbook_margin(); + + let btc_price = build_btc_price_info_object(&mut scenario, 500, &clock); + let usdc_price = build_demo_usdc_price_info_object(&mut scenario, &clock); + + scenario.next_tx(test_constants::user1()); + let mut pool = scenario.take_shared>(); + let mut registry = scenario.take_shared(); + let deepbook_registry = scenario.take_shared_by_id(registry_id); + margin_manager::new( + &pool, + &deepbook_registry, + &mut registry, + &clock, + scenario.ctx(), + ); + return_shared(deepbook_registry); + + scenario.next_tx(test_constants::user1()); + let mut mm = scenario.take_shared>(); + let usdc_pool = scenario.take_shared_by_id>(usdc_pool_id); + let mut btc_pool = scenario.take_shared_by_id>(btc_pool_id); + + // Deposit 500 USDC + mm.deposit( + ®istry, + &btc_price, + &usdc_price, + mint_coin(500 * usdc_multiplier(), scenario.ctx()), + &clock, + scenario.ctx(), + ); + + // Borrow $200 BTC (0.4 BTC). Risk ratio = (500 + 200) / 200 = 3.5 + mm.borrow_base( + ®istry, + &mut btc_pool, + &btc_price, + &usdc_price, + &pool, + 40000000, + &clock, + scenario.ctx(), + ); + + assert!( + mm.risk_ratio(®istry, &btc_price, &usdc_price, &pool, &btc_pool, &usdc_pool, &clock) == 3_500_000_000, + 0, + ); + + // Now we withdraw 0.2 BTC. This should be allowed since risk ratio >= 2; + let withdraw_btc = mm.withdraw( + ®istry, + &btc_pool, + &usdc_pool, + &btc_price, + &usdc_price, + &pool, + 20000000, + &clock, + scenario.ctx(), + ); + withdraw_btc.burn_for_testing(); + + // Risk ratio is now (500 + 100) / 200 = 3.0 + assert!( + mm.risk_ratio(®istry, &btc_price, &usdc_price, &pool, &btc_pool, &usdc_pool, &clock) == 3_000_000_000, + 0, + ); + + // Perform liquidation and check rewards + scenario.next_tx(test_constants::liquidator()); + + // We now have 0.2 BTC ($440) and 500 USDC ($500), with a debt of 0.4 BTC ($880) + // At BTC price 2200, Risk ratio = (440 + 500) / 880 = 1.0681818 < 1.1, can liquidate + let repay_coin = mint_coin(1 * btc_multiplier(), scenario.ctx()); + let btc_price_2200 = build_btc_price_info_object(&mut scenario, 2200, &clock); + + assert!( + mm.risk_ratio(®istry, &btc_price_2200, &usdc_price, &pool, &btc_pool, &usdc_pool, &clock) == 1_068_181_825, + 0, + ); + + // 0.37454 BTC will be used to liquidate. 0.3636 BTC for repayment of loan, 0.01094 BTC for pool liquidation fee. + // Since 0.3636 BTC is used for repayment, the liquidator should receive 0.3636 * 0.02 = 0.007272 as a reward. + // Risk ratio after liquidation = (940 - 824 - 16) / (880 - 800) = 1.25 + // Remaining_repay_coin = 1 - 0.37454 = 0.62546 BTC + // The liquidator should receive 0.3636 * 1.05 = 0.38178 BTC = 840 USD. + // The 0.2 BTC will be used first (0.2 BTC = 440 USD). Then the remaining 400 USD will be taken as USDC. + let (base_coin, quote_coin, remaining_repay_coin) = mm.liquidate( + ®istry, + &btc_price_2200, + &usdc_price, + &mut btc_pool, + &mut pool, + repay_coin, + &clock, + scenario.ctx(), + ); + + assert!(base_coin.value() == 20000000); // 0.2 BTC + assert!(quote_coin.value() == 399999930); // ~400 USDC + assert!(remaining_repay_coin.value() == 62545457); // 0.62545457 BTC + + destroy_3!(remaining_repay_coin, base_coin, quote_coin); + return_shared_3!(mm, usdc_pool, pool); + destroy_3!(btc_price, usdc_price, btc_price_2200); + destroy(btc_pool); + cleanup_margin_test(registry, admin_cap, maintainer_cap, clock, scenario); +} + +/// Test liquidation with BTC/SUI pair where both assets are volatile +/// BTC: 8 decimals, SUI: 9 decimals +fun test_btc_sui_liquidation(error_code: u64) { + let ( + mut scenario, + clock, + admin_cap, + maintainer_cap, + btc_pool_id, + sui_pool_id, + _pool_id, + registry_id, + ) = setup_btc_sui_deepbook_margin(); + + // BTC at $50,000, SUI at $20 + let btc_price = build_btc_price_info_object(&mut scenario, 50000, &clock); + let sui_price = build_sui_price_info_object(&mut scenario, 20, &clock); + + scenario.next_tx(test_constants::user1()); + let mut pool = scenario.take_shared>(); + let mut registry = scenario.take_shared(); + let deepbook_registry = scenario.take_shared_by_id(registry_id); + margin_manager::new(&pool, &deepbook_registry, &mut registry, &clock, scenario.ctx()); + return_shared(deepbook_registry); + + scenario.next_tx(test_constants::user1()); + let mut mm = scenario.take_shared>(); + let btc_pool = scenario.take_shared_by_id>(btc_pool_id); + let mut sui_pool = scenario.take_shared_by_id>(sui_pool_id); + + // Deposit 0.1 BTC worth $5,000 + mm.deposit( + ®istry, + &btc_price, + &sui_price, + mint_coin(10_000_000, scenario.ctx()), // 0.1 BTC (8 decimals) + &clock, + scenario.ctx(), + ); + + // Borrow 200 SUI worth $4,000. Risk ratio = (5000 + 4000) / 4000 = 2.25 + mm.borrow_quote( + ®istry, + &mut sui_pool, + &btc_price, + &sui_price, + &pool, + 200 * sui_multiplier(), // 200 SUI (9 decimals) + &clock, + scenario.ctx(), + ); + + // Calculate expected risk ratio: (5000 + 4000) / 4000 = 2.25 + let actual_risk_ratio = mm.risk_ratio( + ®istry, + &btc_price, + &sui_price, + &pool, + &btc_pool, + &sui_pool, + &clock, + ); + assert!(actual_risk_ratio == 2_250_000_000); + + // Perform liquidation test + scenario.next_tx(test_constants::liquidator()); + + if (error_code == ECannotLiquidate) { + // SUI price stays at $20, risk ratio should still be safe + // BTC value: $5,000, SUI borrowed value: 200 * $20 = $4,000 + // Risk ratio = (5000 + 4000) / 4000 = 2.25, still cannot liquidate + let repay_coin = mint_coin(3000 * sui_multiplier(), scenario.ctx()); + + let safe_risk_ratio = mm.risk_ratio( + ®istry, + &btc_price, + &sui_price, + &pool, + &btc_pool, + &sui_pool, + &clock, + ); + assert!(safe_risk_ratio > test_constants::liquidation_risk_ratio()); + + let (_base_coin, _quote_coin, _remaining_repay_coin) = mm.liquidate( + ®istry, + &btc_price, + &sui_price, + &mut sui_pool, + &mut pool, + repay_coin, + &clock, + scenario.ctx(), + ); + abort + }; + + // Create a liquidatable scenario: BTC drops to $15,000, SUI rises to $100 + // BTC value: 0.1 * $15,000 = $1500, SUI borrowed value: 200 * $100 = $20,000 + // Risk ratio = (1500 + 20000) / 20000 = 1.075 < 1.1, can liquidate + let btc_price_crash = build_btc_price_info_object(&mut scenario, 15000, &clock); + let sui_price_spike = build_sui_price_info_object(&mut scenario, 100, &clock); + let repay_coin = mint_coin(3000 * sui_multiplier(), scenario.ctx()); + + let liquidation_risk_ratio = mm.risk_ratio( + ®istry, + &btc_price_crash, + &sui_price_spike, + &pool, + &btc_pool, + &sui_pool, + &clock, + ); + assert!(liquidation_risk_ratio == 1_075_000_000); // Should be liquidatable + + // 180.25 SUI total is used. 175 SUI for repayment, 5.25 SUI for pool liquidation fee. + // The liquidator should receive 175 * 0.02 = 3.5 SUI as a reward. + // Risk ratio after liquidation = (21500 - 175 * 1.05 * 100) / (20000 - 175 * 100) = 1.25 (our target liquidation) + // Remaining_repay_coin = 3000 - 180.25 = 2819.75 SUI + // The liquidator should receive 175 * 1.05 = 183.75 SUI = 183.75 * 100 = 18375 USD. + // Since there's enough SUI, no BTC is paid out + let (base_coin, quote_coin, remaining_repay_coin) = mm.liquidate( + ®istry, + &btc_price_crash, + &sui_price_spike, + &mut sui_pool, + &mut pool, + repay_coin, + &clock, + scenario.ctx(), + ); + + assert!(base_coin.value() == 0); + assert!(quote_coin.value() == 183_750_000_000); + assert!(remaining_repay_coin.value() == 2819_750_000_000); + + destroy_3!(remaining_repay_coin, base_coin, quote_coin); + return_shared_3!(mm, sui_pool, pool); + destroy_3!(btc_price, sui_price, btc_price_crash); + destroy(sui_price_spike); + destroy(btc_pool); + cleanup_margin_test(registry, admin_cap, maintainer_cap, clock, scenario); +} diff --git a/packages/deepbook_margin/tests/margin_manager_tests.move b/packages/deepbook_margin/tests/margin_manager_tests.move new file mode 100644 index 000000000..90a8a1956 --- /dev/null +++ b/packages/deepbook_margin/tests/margin_manager_tests.move @@ -0,0 +1,2899 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +#[test_only] +module deepbook_margin::margin_manager_tests; + +use deepbook::{pool::Pool, registry::Registry}; +use deepbook_margin::{ + margin_constants, + margin_manager::{Self, MarginManager}, + margin_pool::{Self, MarginPool}, + margin_registry::{Self, MarginRegistry}, + test_constants::{Self, USDC, USDT, BTC, INVALID_ASSET, btc_multiplier}, + test_helpers::{ + Self, + setup_margin_registry, + create_margin_pool, + create_pool_for_testing, + enable_deepbook_margin_on_pool, + default_protocol_config, + cleanup_margin_test, + mint_coin, + build_demo_usdc_price_info_object, + build_demo_usdt_price_info_object, + build_btc_price_info_object, + setup_btc_usd_deepbook_margin, + setup_usdc_usdt_deepbook_margin, + destroy_2, + destroy_3, + return_shared_2, + return_shared_3, + return_shared_4, + advance_time, + get_margin_pool_caps, + return_to_sender_2 + } +}; +use std::unit_test::destroy; +use sui::test_scenario::return_shared; +use token::deep::DEEP; + +#[test] +fun test_margin_manager_creation() { + let (mut scenario, clock, admin_cap, maintainer_cap) = setup_margin_registry(); + + create_margin_pool( + &mut scenario, + &maintainer_cap, + default_protocol_config(), + &clock, + ); + create_margin_pool( + &mut scenario, + &maintainer_cap, + default_protocol_config(), + &clock, + ); + + // Create DeepBook pool and enable margin trading on it + let (pool_id, registry_id) = create_pool_for_testing(&mut scenario); + scenario.next_tx(test_constants::admin()); + let mut registry = scenario.take_shared(); + enable_deepbook_margin_on_pool( + pool_id, + &mut registry, + &admin_cap, + &clock, + &mut scenario, + ); + return_shared(registry); + + scenario.next_tx(test_constants::admin()); + let pool = scenario.take_shared>(); + let mut registry = scenario.take_shared(); + let deepbook_registry = scenario.take_shared_by_id(registry_id); + margin_manager::new( + &pool, + &deepbook_registry, + &mut registry, + &clock, + scenario.ctx(), + ); + return_shared_2!(deepbook_registry, pool); + cleanup_margin_test(registry, admin_cap, maintainer_cap, clock, scenario); +} + +#[test] +fun test_deepbook_margin_with_oracle() { + let ( + mut scenario, + clock, + admin_cap, + maintainer_cap, + usdc_pool_id, + _usdt_pool_id, + _pool_id, + registry_id, + ) = setup_usdc_usdt_deepbook_margin(); + + scenario.next_tx(test_constants::user1()); + let mut registry = scenario.take_shared(); + let pool = scenario.take_shared>(); + let deepbook_registry = scenario.take_shared_by_id(registry_id); + margin_manager::new( + &pool, + &deepbook_registry, + &mut registry, + &clock, + scenario.ctx(), + ); + return_shared(deepbook_registry); + + scenario.next_tx(test_constants::admin()); + let usdc_price = build_demo_usdc_price_info_object(&mut scenario, &clock); + let usdt_price = build_demo_usdt_price_info_object(&mut scenario, &clock); + + // Test borrowing with oracle prices + scenario.next_tx(test_constants::user1()); + let mut mm = scenario.take_shared>(); + let mut usdc_pool = scenario.take_shared_by_id>(usdc_pool_id); + + // User1 deposits 10k USDT as collateral + let deposit_coin = mint_coin(10_000_000_000, scenario.ctx()); // 10k USDT with 6 decimals + mm.deposit( + ®istry, + &usdt_price, + &usdc_price, + deposit_coin, + &clock, + scenario.ctx(), + ); + + // Borrow 5k USDC against the collateral (50% borrow ratio) + mm.borrow_quote( + ®istry, + &mut usdc_pool, + &usdt_price, + &usdc_price, + &pool, + 5_000_000_000, // 5k USDC with 6 decimals + &clock, + scenario.ctx(), + ); + + destroy_2!(usdc_price, usdt_price); + return_shared_3!(usdc_pool, pool, mm); + cleanup_margin_test(registry, admin_cap, maintainer_cap, clock, scenario); +} + +#[test] +fun test_btc_usd_deepbook_margin() { + let ( + mut scenario, + clock, + admin_cap, + maintainer_cap, + _btc_pool_id, + usdc_pool_id, + _pool_id, + registry_id, + ) = setup_btc_usd_deepbook_margin(); + + let btc_price = build_btc_price_info_object( + &mut scenario, + 60000, + &clock, + ); + let usdc_price = build_demo_usdc_price_info_object(&mut scenario, &clock); + + scenario.next_tx(test_constants::user1()); + let pool = scenario.take_shared>(); + let mut registry = scenario.take_shared(); + let deepbook_registry = scenario.take_shared_by_id(registry_id); + margin_manager::new( + &pool, + &deepbook_registry, + &mut registry, + &clock, + scenario.ctx(), + ); + return_shared(deepbook_registry); + + scenario.next_tx(test_constants::user1()); + let mut mm = scenario.take_shared>(); + let mut usdc_pool = scenario.take_shared_by_id>(usdc_pool_id); + + let deposit = mint_coin(btc_multiplier() / 2, scenario.ctx()); // 0.5 BTC + mm.deposit(®istry, &btc_price, &usdc_price, deposit, &clock, scenario.ctx()); + + mm.borrow_quote( + ®istry, + &mut usdc_pool, + &btc_price, + &usdc_price, + &pool, + 15_000_000000, // $15,000 + &clock, + scenario.ctx(), + ); + + return_shared_2!(usdc_pool, pool); + destroy_2!(btc_price, usdc_price); + return_shared(mm); + cleanup_margin_test(registry, admin_cap, maintainer_cap, clock, scenario); +} + +/// Test demonstrates depositing USD and borrowing BTC at near-max LTV +#[test] +fun test_usd_deposit_btc_borrow() { + let ( + mut scenario, + mut clock, + admin_cap, + maintainer_cap, + btc_pool_id, + _usdc_pool_id, + _pool_id, + registry_id, + ) = setup_btc_usd_deepbook_margin(); + + // Set initial prices + let btc_price = build_btc_price_info_object( + &mut scenario, + 100000, + &clock, + ); + let usdc_price = build_demo_usdc_price_info_object(&mut scenario, &clock); + + scenario.next_tx(test_constants::user1()); + let mut pool = scenario.take_shared>(); + let mut registry = scenario.take_shared(); + let deepbook_registry = scenario.take_shared_by_id(registry_id); + margin_manager::new( + &pool, + &deepbook_registry, + &mut registry, + &clock, + scenario.ctx(), + ); + return_shared(deepbook_registry); + + scenario.next_tx(test_constants::user1()); + let mut mm = scenario.take_shared>(); + let mut btc_pool = scenario.take_shared_by_id>(btc_pool_id); + + // Deposit 100000 USD + mm.deposit( + ®istry, + &btc_price, + &usdc_price, + mint_coin(100_000 * test_constants::usdc_multiplier(), scenario.ctx()), + &clock, + scenario.ctx(), + ); + + mm.borrow_base( + ®istry, + &mut btc_pool, + &btc_price, + &usdc_price, + &pool, + 2 * btc_multiplier(), + &clock, + scenario.ctx(), + ); + + advance_time(&mut clock, 1); + let btc_increased = build_btc_price_info_object( + &mut scenario, + 1_000_000, + &clock, + ); + + let debt_coin = mint_coin(10 * test_constants::btc_multiplier(), scenario.ctx()); + scenario.next_tx(test_constants::admin()); + let (base_coin, quote_coin, debt_coin) = mm.liquidate( + ®istry, + &btc_increased, + &usdc_price, + &mut btc_pool, + &mut pool, + debt_coin, + &clock, + scenario.ctx(), + ); + + destroy(debt_coin); + destroy(base_coin); + destroy(quote_coin); + + return_shared_2!(btc_pool, pool); + destroy_2!(btc_price, usdc_price); + return_shared(mm); + destroy(btc_increased); + cleanup_margin_test(registry, admin_cap, maintainer_cap, clock, scenario); +} + +// Creation tests +#[test] +fun test_margin_manager_creation_ok() { + let (mut scenario, clock, admin_cap, maintainer_cap) = setup_margin_registry(); + + scenario.next_tx(test_constants::user1()); + create_margin_pool(&mut scenario, &maintainer_cap, default_protocol_config(), &clock); + create_margin_pool(&mut scenario, &maintainer_cap, default_protocol_config(), &clock); + + let (pool_id, registry_id) = create_pool_for_testing(&mut scenario); + scenario.next_tx(test_constants::admin()); + let mut registry = scenario.take_shared(); + enable_deepbook_margin_on_pool( + pool_id, + &mut registry, + &admin_cap, + &clock, + &mut scenario, + ); + return_shared(registry); + + scenario.next_tx(test_constants::user1()); + let pool = scenario.take_shared>(); + let mut registry = scenario.take_shared(); + let deepbook_registry = scenario.take_shared_by_id(registry_id); + margin_manager::new( + &pool, + &deepbook_registry, + &mut registry, + &clock, + scenario.ctx(), + ); + return_shared(deepbook_registry); + + scenario.next_tx(test_constants::user1()); + let mm = scenario.take_shared>(); + + return_shared_2!(mm, pool); + cleanup_margin_test(registry, admin_cap, maintainer_cap, clock, scenario); +} + +#[test, expected_failure(abort_code = margin_manager::EMarginTradingNotAllowedInPool)] +fun test_margin_manager_creation_fails_when_not_enabled() { + let (mut scenario, clock, _admin_cap, maintainer_cap) = setup_margin_registry(); + + scenario.next_tx(test_constants::user1()); + create_margin_pool(&mut scenario, &maintainer_cap, default_protocol_config(), &clock); + create_margin_pool(&mut scenario, &maintainer_cap, default_protocol_config(), &clock); + + // Create pool without margin trading + let (_pool_id, registry_id) = create_pool_for_testing(&mut scenario); + + scenario.next_tx(test_constants::user1()); + let pool = scenario.take_shared>(); + let mut registry = scenario.take_shared(); + // should fail + let deepbook_registry = scenario.take_shared_by_id(registry_id); + margin_manager::new( + &pool, + &deepbook_registry, + &mut registry, + &clock, + scenario.ctx(), + ); + return_shared(deepbook_registry); + + abort +} + +// Deposit tests +#[test] +fun test_deposit_with_base_quote_deep_assets() { + let (mut scenario, clock, admin_cap, maintainer_cap) = setup_margin_registry(); + + scenario.next_tx(test_constants::user1()); + create_margin_pool(&mut scenario, &maintainer_cap, default_protocol_config(), &clock); + create_margin_pool(&mut scenario, &maintainer_cap, default_protocol_config(), &clock); + + let (pool_id, registry_id) = create_pool_for_testing(&mut scenario); + scenario.next_tx(test_constants::admin()); + let mut registry = scenario.take_shared(); + enable_deepbook_margin_on_pool( + pool_id, + &mut registry, + &admin_cap, + &clock, + &mut scenario, + ); + return_shared(registry); + + scenario.next_tx(test_constants::user1()); + let pool = scenario.take_shared>(); + let mut registry = scenario.take_shared(); + let deepbook_registry = scenario.take_shared_by_id(registry_id); + margin_manager::new( + &pool, + &deepbook_registry, + &mut registry, + &clock, + scenario.ctx(), + ); + return_shared(deepbook_registry); + + scenario.next_tx(test_constants::user1()); + let mut mm = scenario.take_shared>(); + let usdc_price = build_demo_usdc_price_info_object(&mut scenario, &clock); + let usdt_price = build_demo_usdt_price_info_object(&mut scenario, &clock); + + mm.deposit( + ®istry, + &usdc_price, + &usdt_price, + mint_coin(1000 * test_constants::usdc_multiplier(), scenario.ctx()), + &clock, + scenario.ctx(), + ); + + mm.deposit( + ®istry, + &usdc_price, + &usdt_price, + mint_coin(2000 * test_constants::usdt_multiplier(), scenario.ctx()), + &clock, + scenario.ctx(), + ); + + mm.deposit( + ®istry, + &usdc_price, + &usdt_price, + mint_coin(500 * 1_000_000_000, scenario.ctx()), + &clock, + scenario.ctx(), + ); + + destroy_2!(usdc_price, usdt_price); + return_shared_2!(mm, pool); + cleanup_margin_test(registry, admin_cap, maintainer_cap, clock, scenario); +} + +#[test, expected_failure(abort_code = margin_manager::EInvalidDeposit)] +fun test_deposit_with_invalid_asset_fails() { + let (mut scenario, clock, admin_cap, maintainer_cap) = setup_margin_registry(); + scenario.next_tx(test_constants::user1()); + create_margin_pool(&mut scenario, &maintainer_cap, default_protocol_config(), &clock); + create_margin_pool(&mut scenario, &maintainer_cap, default_protocol_config(), &clock); + + let (pool_id, registry_id) = create_pool_for_testing(&mut scenario); + scenario.next_tx(test_constants::admin()); + let mut registry = scenario.take_shared(); + enable_deepbook_margin_on_pool( + pool_id, + &mut registry, + &admin_cap, + &clock, + &mut scenario, + ); + return_shared(registry); + + scenario.next_tx(test_constants::user1()); + let pool = scenario.take_shared>(); + let mut registry = scenario.take_shared(); + let deepbook_registry = scenario.take_shared_by_id(registry_id); + margin_manager::new( + &pool, + &deepbook_registry, + &mut registry, + &clock, + scenario.ctx(), + ); + return_shared(deepbook_registry); + + scenario.next_tx(test_constants::user1()); + let mut mm = scenario.take_shared>(); + let usdc_price = build_demo_usdc_price_info_object(&mut scenario, &clock); + let usdt_price = build_demo_usdt_price_info_object(&mut scenario, &clock); + + mm.deposit( + ®istry, + &usdc_price, + &usdt_price, + mint_coin(1000 * test_constants::usdc_multiplier(), scenario.ctx()), + &clock, + scenario.ctx(), + ); + + abort +} + +// Withdrawal tests +#[test] +fun test_withdrawal_ok_when_risk_ratio_above_limit() { + let ( + mut scenario, + clock, + admin_cap, + maintainer_cap, + usdc_pool_id, + usdt_pool_id, + _pool_id, + registry_id, + ) = setup_usdc_usdt_deepbook_margin(); + + scenario.next_tx(test_constants::user1()); + let mut registry = scenario.take_shared(); + let pool = scenario.take_shared>(); + let deepbook_registry = scenario.take_shared_by_id(registry_id); + margin_manager::new( + &pool, + &deepbook_registry, + &mut registry, + &clock, + scenario.ctx(), + ); + return_shared(deepbook_registry); + + scenario.next_tx(test_constants::user1()); + let mut mm = scenario.take_shared>(); + let mut usdc_pool = scenario.take_shared_by_id>(usdc_pool_id); + let usdt_pool = scenario.take_shared_by_id>(usdt_pool_id); + let usdc_price = build_demo_usdc_price_info_object(&mut scenario, &clock); + let usdt_price = build_demo_usdt_price_info_object(&mut scenario, &clock); + + mm.deposit( + ®istry, + &usdt_price, + &usdc_price, + mint_coin(10_000 * test_constants::usdt_multiplier(), scenario.ctx()), + &clock, + scenario.ctx(), + ); + mm.borrow_quote( + ®istry, + &mut usdc_pool, + &usdt_price, + &usdc_price, + &pool, + 1000 * test_constants::usdc_multiplier(), + &clock, + scenario.ctx(), + ); + + // Now test withdrawal with existing loan (risk ratio should still be high) + let withdraw_amount = 100 * test_constants::usdt_multiplier(); + let withdrawn_coin = mm.withdraw( + ®istry, + &usdt_pool, + &usdc_pool, + &usdt_price, + &usdc_price, + &pool, + withdraw_amount, + &clock, + scenario.ctx(), + ); + + assert!(withdrawn_coin.value() == withdraw_amount); + destroy(withdrawn_coin); + + destroy_2!(usdc_price, usdt_price); + return_shared_4!(usdc_pool, pool, usdt_pool, mm); + cleanup_margin_test(registry, admin_cap, maintainer_cap, clock, scenario); +} + +#[test, expected_failure(abort_code = margin_manager::EWithdrawRiskRatioExceeded)] +fun test_withdrawal_fails_when_risk_ratio_goes_below_limit() { + let ( + mut scenario, + clock, + _admin_cap, + _maintainer_cap, + usdc_pool_id, + usdt_pool_id, + _pool_id, + registry_id, + ) = setup_usdc_usdt_deepbook_margin(); + + scenario.next_tx(test_constants::user1()); + let mut registry = scenario.take_shared(); + let pool = scenario.take_shared>(); + let deepbook_registry = scenario.take_shared_by_id(registry_id); + margin_manager::new( + &pool, + &deepbook_registry, + &mut registry, + &clock, + scenario.ctx(), + ); + return_shared(deepbook_registry); + + scenario.next_tx(test_constants::user1()); + let mut mm = scenario.take_shared>(); + let mut usdc_pool = scenario.take_shared_by_id>(usdc_pool_id); + let usdt_pool = scenario.take_shared_by_id>(usdt_pool_id); + + let usdc_price = build_demo_usdc_price_info_object(&mut scenario, &clock); + let usdt_price = build_demo_usdt_price_info_object(&mut scenario, &clock); + let usdt_deposit = mint_coin(10_000 * test_constants::usdt_multiplier(), scenario.ctx()); + mm.deposit( + ®istry, + &usdt_price, + &usdc_price, + usdt_deposit, + &clock, + scenario.ctx(), + ); + mm.borrow_quote( + ®istry, + &mut usdc_pool, + &usdt_price, + &usdc_price, + &pool, + 5000 * test_constants::usdc_multiplier(), + &clock, + scenario.ctx(), + ); + + let withdraw_amount = 9000 * test_constants::usdt_multiplier(); + let withdraw_coin = mm.withdraw( + ®istry, + &usdt_pool, + &usdc_pool, + &usdt_price, + &usdc_price, + &pool, + withdraw_amount, + &clock, + scenario.ctx(), + ); + destroy(withdraw_coin); + + abort +} + +// Borrow tests +#[test, expected_failure(abort_code = margin_manager::ECannotHaveLoanInMoreThanOneMarginPool)] +fun test_borrow_fails_from_both_pools() { + let ( + mut scenario, + clock, + _admin_cap, + _maintainer_cap, + usdc_pool_id, + usdt_pool_id, + _pool_id, + registry_id, + ) = setup_usdc_usdt_deepbook_margin(); + + scenario.next_tx(test_constants::user1()); + let mut registry = scenario.take_shared(); + let pool = scenario.take_shared>(); + let deepbook_registry = scenario.take_shared_by_id(registry_id); + margin_manager::new( + &pool, + &deepbook_registry, + &mut registry, + &clock, + scenario.ctx(), + ); + return_shared(deepbook_registry); + + scenario.next_tx(test_constants::user1()); + let mut mm = scenario.take_shared>(); + let mut usdc_pool = scenario.take_shared_by_id>(usdc_pool_id); + let mut usdt_pool = scenario.take_shared_by_id>(usdt_pool_id); + let usdc_price = build_demo_usdc_price_info_object(&mut scenario, &clock); + let usdt_price = build_demo_usdt_price_info_object(&mut scenario, &clock); + + mm.deposit( + ®istry, + &usdt_price, + &usdc_price, + mint_coin(10_000 * test_constants::usdt_multiplier(), scenario.ctx()), + &clock, + scenario.ctx(), + ); + + mm.borrow_quote( + ®istry, + &mut usdc_pool, + &usdt_price, + &usdc_price, + &pool, + 1000 * test_constants::usdc_multiplier(), + &clock, + scenario.ctx(), + ); + + mm.borrow_base( + ®istry, + &mut usdt_pool, + &usdt_price, + &usdc_price, + &pool, + 1000 * test_constants::usdc_multiplier(), + &clock, + scenario.ctx(), + ); + + abort +} + +#[test, expected_failure(abort_code = margin_pool::EBorrowAmountTooLow)] +fun test_borrow_fails_with_zero_amount() { + let ( + mut scenario, + clock, + _admin_cap, + _maintainer_cap, + usdc_pool_id, + _usdt_pool_id, + _pool_id, + registry_id, + ) = setup_usdc_usdt_deepbook_margin(); + + scenario.next_tx(test_constants::user1()); + let mut registry = scenario.take_shared(); + let pool = scenario.take_shared>(); + let deepbook_registry = scenario.take_shared_by_id(registry_id); + margin_manager::new( + &pool, + &deepbook_registry, + &mut registry, + &clock, + scenario.ctx(), + ); + return_shared(deepbook_registry); + + scenario.next_tx(test_constants::user1()); + let mut mm = scenario.take_shared>(); + let mut usdc_pool = scenario.take_shared_by_id>(usdc_pool_id); + let usdc_price = build_demo_usdc_price_info_object(&mut scenario, &clock); + let usdt_price = build_demo_usdt_price_info_object(&mut scenario, &clock); + + mm.deposit( + ®istry, + &usdt_price, + &usdc_price, + mint_coin(10_000 * test_constants::usdt_multiplier(), scenario.ctx()), + &clock, + scenario.ctx(), + ); + mm.borrow_quote( + ®istry, + &mut usdc_pool, + &usdt_price, + &usdc_price, + &pool, + 0, + &clock, + scenario.ctx(), + ); + + abort +} + +#[test, expected_failure(abort_code = margin_manager::EBorrowRiskRatioExceeded)] +fun test_borrow_fails_when_risk_ratio_below_150() { + let ( + mut scenario, + clock, + _admin_cap, + _maintainer_cap, + usdc_pool_id, + _usdt_pool_id, + _pool_id, + registry_id, + ) = setup_usdc_usdt_deepbook_margin(); + + scenario.next_tx(test_constants::user1()); + let mut registry = scenario.take_shared(); + let pool = scenario.take_shared>(); + let deepbook_registry = scenario.take_shared_by_id(registry_id); + margin_manager::new( + &pool, + &deepbook_registry, + &mut registry, + &clock, + scenario.ctx(), + ); + return_shared(deepbook_registry); + + scenario.next_tx(test_constants::user1()); + let mut mm = scenario.take_shared>(); + let mut usdc_pool = scenario.take_shared_by_id>(usdc_pool_id); + + // Deposit small collateral + let usdc_price = build_demo_usdc_price_info_object(&mut scenario, &clock); + let usdt_price = build_demo_usdt_price_info_object(&mut scenario, &clock); + mm.deposit( + ®istry, + &usdt_price, + &usdc_price, + mint_coin(1000 * test_constants::usdt_multiplier(), scenario.ctx()), + &clock, + scenario.ctx(), + ); + + // Try to borrow amount that would push risk ratio below 1.5 + // With $1000 collateral, borrowing $5000 would give ratio of 0.2 which is way below 1.5 + let borrow_amount = 5000 * test_constants::usdc_multiplier(); + mm.borrow_quote( + ®istry, + &mut usdc_pool, + &usdt_price, + &usdc_price, + &pool, + borrow_amount, + &clock, + scenario.ctx(), + ); + + abort +} + +// Repay tests +#[test, expected_failure(abort_code = margin_manager::EIncorrectMarginPool)] +fun test_repay_fails_wrong_pool() { + let ( + mut scenario, + clock, + _admin_cap, + _maintainer_cap, + usdc_pool_id, + usdt_pool_id, + _pool_id, + registry_id, + ) = setup_usdc_usdt_deepbook_margin(); + + scenario.next_tx(test_constants::user1()); + let mut registry = scenario.take_shared(); + let pool = scenario.take_shared>(); + let deepbook_registry = scenario.take_shared_by_id(registry_id); + margin_manager::new( + &pool, + &deepbook_registry, + &mut registry, + &clock, + scenario.ctx(), + ); + return_shared(deepbook_registry); + + scenario.next_tx(test_constants::user1()); + let mut mm = scenario.take_shared>(); + let mut usdc_pool = scenario.take_shared_by_id>(usdc_pool_id); + let mut usdt_pool = scenario.take_shared_by_id>(usdt_pool_id); + + let usdc_price = build_demo_usdc_price_info_object(&mut scenario, &clock); + let usdt_price = build_demo_usdt_price_info_object(&mut scenario, &clock); + mm.deposit( + ®istry, + &usdt_price, + &usdc_price, + mint_coin(10_000 * test_constants::usdt_multiplier(), scenario.ctx()), + &clock, + scenario.ctx(), + ); + let borrow_amount = 2000 * test_constants::usdc_multiplier(); + mm.borrow_quote( + ®istry, + &mut usdc_pool, + &usdt_price, + &usdc_price, + &pool, + borrow_amount, + &clock, + scenario.ctx(), + ); + + // Try to repay to wrong pool (USDT pool instead of USDC pool) + let repay_coin = mint_coin(1000 * test_constants::usdt_multiplier(), scenario.ctx()); + mm.deposit( + ®istry, + &usdt_price, + &usdc_price, + repay_coin, + &clock, + scenario.ctx(), + ); + mm.repay_base( + ®istry, + &mut usdt_pool, + option::none(), + &clock, + scenario.ctx(), + ); + + abort +} + +#[test] +fun test_repay_full_with_none() { + let ( + mut scenario, + clock, + admin_cap, + maintainer_cap, + usdc_pool_id, + _usdt_pool_id, + _pool_id, + registry_id, + ) = setup_usdc_usdt_deepbook_margin(); + + // Create margin manager and borrow + scenario.next_tx(test_constants::user1()); + let mut registry = scenario.take_shared(); + let pool = scenario.take_shared>(); + let deepbook_registry = scenario.take_shared_by_id(registry_id); + margin_manager::new( + &pool, + &deepbook_registry, + &mut registry, + &clock, + scenario.ctx(), + ); + return_shared(deepbook_registry); + + scenario.next_tx(test_constants::user1()); + let mut mm = scenario.take_shared>(); + let mut usdc_pool = scenario.take_shared_by_id>(usdc_pool_id); + + // Deposit and borrow + let usdc_price = build_demo_usdc_price_info_object(&mut scenario, &clock); + let usdt_price = build_demo_usdt_price_info_object(&mut scenario, &clock); + mm.deposit( + ®istry, + &usdt_price, + &usdc_price, + mint_coin(10_000 * test_constants::usdt_multiplier(), scenario.ctx()), + &clock, + scenario.ctx(), + ); + let borrow_amount = 2000 * test_constants::usdc_multiplier(); + mm.borrow_quote( + ®istry, + &mut usdc_pool, + &usdt_price, + &usdc_price, + &pool, + borrow_amount, + &clock, + scenario.ctx(), + ); + + // Repay full loan + let repay_coin = mint_coin(3000 * test_constants::usdc_multiplier(), scenario.ctx()); // More than enough + + // Deposit the repay coin margin manager's balance manager + mm.deposit( + ®istry, + &usdt_price, + &usdc_price, + repay_coin, + &clock, + scenario.ctx(), + ); + + let repaid_amount = mm.repay_quote( + ®istry, + &mut usdc_pool, + option::none(), + &clock, + scenario.ctx(), + ); + + assert!(repaid_amount > 0); + destroy_2!(usdc_price, usdt_price); + return_shared_3!(usdc_pool, pool, mm); + cleanup_margin_test(registry, admin_cap, maintainer_cap, clock, scenario); +} + +#[test] +fun test_repay_exact_amount_no_rounding_errors() { + let ( + mut scenario, + clock, + admin_cap, + maintainer_cap, + usdc_pool_id, + _usdt_pool_id, + _pool_id, + registry_id, + ) = setup_usdc_usdt_deepbook_margin(); + + scenario.next_tx(test_constants::user1()); + let mut registry = scenario.take_shared(); + let pool = scenario.take_shared>(); + let deepbook_registry = scenario.take_shared_by_id(registry_id); + margin_manager::new( + &pool, + &deepbook_registry, + &mut registry, + &clock, + scenario.ctx(), + ); + return_shared(deepbook_registry); + + scenario.next_tx(test_constants::user1()); + let mut mm = scenario.take_shared>(); + let mut usdc_pool = scenario.take_shared_by_id>(usdc_pool_id); + let usdc_price = build_demo_usdc_price_info_object(&mut scenario, &clock); + let usdt_price = build_demo_usdt_price_info_object(&mut scenario, &clock); + + mm.deposit( + ®istry, + &usdt_price, + &usdc_price, + mint_coin(100_000 * test_constants::usdt_multiplier(), scenario.ctx()), + &clock, + scenario.ctx(), + ); + + // testing for rounding errors when repaying shares * index + let test_amounts = vector[ + 100 * test_constants::usdc_multiplier(), // Small amount + 1234567890, // Odd amount + 999999999, // Just under 1 USDC + ]; + + test_amounts.do!(|borrow_amount| { + // Borrow + mm.borrow_quote( + ®istry, + &mut usdc_pool, + &usdt_price, + &usdc_price, + &pool, + borrow_amount, + &clock, + scenario.ctx(), + ); + + // Get the borrowed shares and calculate exact amount (shares * index) + let (_, borrowed_quote_shares) = mm.borrowed_shares(); + let exact_amount = usdc_pool.borrow_shares_to_amount(borrowed_quote_shares, &clock); + + // Deposit enough for repayment + let repay_coin = mint_coin(exact_amount + 1000, scenario.ctx()); // Add buffer + mm.deposit( + ®istry, + &usdt_price, + &usdc_price, + repay_coin, + &clock, + scenario.ctx(), + ); + + // Repay the exact amount equal to shares * index + let repaid_amount = mm.repay_quote( + ®istry, + &mut usdc_pool, + option::none(), + &clock, + scenario.ctx(), + ); + + // Verify no rounding error: repaid amount should equal calculated amount + assert!(repaid_amount == exact_amount); + + // Verify shares are zero + let borrowed_quote_shares = mm.borrowed_quote_shares(); + assert!(borrowed_quote_shares == 0); + + // Clean up any remaining debt + if (borrowed_quote_shares > 0) { + let remaining_amount = usdc_pool.borrow_shares_to_amount( + borrowed_quote_shares, + &clock, + ); + if (remaining_amount > 0) { + mm.deposit( + ®istry, + &usdt_price, + &usdc_price, + mint_coin(remaining_amount + 1, scenario.ctx()), + &clock, + scenario.ctx(), + ); + mm.repay_quote( + ®istry, + &mut usdc_pool, + option::none(), + &clock, + scenario.ctx(), + ); + }; + }; + }); + + destroy_2!(usdc_price, usdt_price); + return_shared_3!(mm, usdc_pool, pool); + cleanup_margin_test(registry, admin_cap, maintainer_cap, clock, scenario); +} + +#[test] +fun test_liquidation_reward_calculations() { + let ( + mut scenario, + clock, + admin_cap, + maintainer_cap, + _btc_pool_id, + usdc_pool_id, + _pool_id, + registry_id, + ) = setup_btc_usd_deepbook_margin(); + + scenario.next_tx(test_constants::user1()); + let mut pool = scenario.take_shared>(); + let mut registry = scenario.take_shared(); + let deepbook_registry = scenario.take_shared_by_id(registry_id); + margin_manager::new( + &pool, + &deepbook_registry, + &mut registry, + &clock, + scenario.ctx(), + ); + return_shared(deepbook_registry); + + scenario.next_tx(test_constants::user1()); + let mut mm = scenario.take_shared>(); + let mut usdc_pool = scenario.take_shared_by_id>(usdc_pool_id); + let btc_price = build_btc_price_info_object(&mut scenario, 50000, &clock); + let usdc_price = build_demo_usdc_price_info_object(&mut scenario, &clock); + + // Deposit 1 BTC worth $50k + mm.deposit( + ®istry, + &btc_price, + &usdc_price, + mint_coin(btc_multiplier(), scenario.ctx()), + &clock, + scenario.ctx(), + ); + + // Borrow $45k + mm.borrow_quote( + ®istry, + &mut usdc_pool, + &btc_price, + &usdc_price, + &pool, + 45_000_000_000, + &clock, + scenario.ctx(), + ); + + // Price drops severely to trigger liquidation + // At $10k BTC price: ($10k BTC + $45k USDC) / $45k debt = $55k / $45k = 122% (still above 110%) + // At $2k BTC price: ($2k BTC + $45k USDC) / $45k debt = $47k / $45k = 104.4% (below 110% - triggers liquidation!) + let btc_price_dropped = build_btc_price_info_object(&mut scenario, 2000, &clock); + + // Perform liquidation and check rewards + scenario.next_tx(test_constants::liquidator()); + let debt_coin = mint_coin(50_000_000_000, scenario.ctx()); + + let (base_coin, quote_coin, remaining_debt) = mm.liquidate( + ®istry, + &btc_price_dropped, + &usdc_price, + &mut usdc_pool, + &mut pool, + debt_coin, + &clock, + scenario.ctx(), + ); + + let liquidator_btc_reward = base_coin.value(); + let liquidator_usdc_reward = quote_coin.value(); + + // Verify liquidator received rewards (should be non-zero) + assert!(liquidator_btc_reward > 0 || liquidator_usdc_reward > 0); + + destroy_3!(remaining_debt, base_coin, quote_coin); + return_shared_3!(mm, usdc_pool, pool); + destroy_3!(btc_price, usdc_price, btc_price_dropped); + cleanup_margin_test(registry, admin_cap, maintainer_cap, clock, scenario); +} + +// === Risk Ratio Calculation Tests === + +#[test] +fun test_risk_ratio_with_zero_assets() { + let (mut scenario, clock, admin_cap, maintainer_cap) = setup_margin_registry(); + + scenario.next_tx(test_constants::user1()); + create_margin_pool(&mut scenario, &maintainer_cap, default_protocol_config(), &clock); + create_margin_pool(&mut scenario, &maintainer_cap, default_protocol_config(), &clock); + + let (pool_id, registry_id) = create_pool_for_testing(&mut scenario); + scenario.next_tx(test_constants::admin()); + let mut registry = scenario.take_shared(); + enable_deepbook_margin_on_pool( + pool_id, + &mut registry, + &admin_cap, + &clock, + &mut scenario, + ); + return_shared(registry); + + scenario.next_tx(test_constants::user1()); + let pool = scenario.take_shared>(); + let mut registry = scenario.take_shared(); + let deepbook_registry = scenario.take_shared_by_id(registry_id); + margin_manager::new( + &pool, + &deepbook_registry, + &mut registry, + &clock, + scenario.ctx(), + ); + return_shared(deepbook_registry); + + scenario.next_tx(test_constants::user1()); + let mm = scenario.take_shared>(); + + assert!(mm.borrowed_base_shares() == 0); + assert!(mm.borrowed_quote_shares() == 0); + + return_shared_2!(mm, pool); + cleanup_margin_test(registry, admin_cap, maintainer_cap, clock, scenario); +} + +#[test] +fun test_risk_ratio_with_multiple_assets() { + let (mut scenario, clock, admin_cap, maintainer_cap) = setup_margin_registry(); + let usdc_pool_id = create_margin_pool( + &mut scenario, + &maintainer_cap, + default_protocol_config(), + &clock, + ); + let usdt_pool_id = create_margin_pool( + &mut scenario, + &maintainer_cap, + default_protocol_config(), + &clock, + ); + let (usdc_pool_cap, usdt_pool_cap) = get_margin_pool_caps(&mut scenario, usdc_pool_id); + + let (pool_id, registry_id) = create_pool_for_testing(&mut scenario); + scenario.next_tx(test_constants::admin()); + let mut registry = scenario.take_shared(); + enable_deepbook_margin_on_pool( + pool_id, + &mut registry, + &admin_cap, + &clock, + &mut scenario, + ); + return_shared(registry); + + // Setup pools + scenario.next_tx(test_constants::admin()); + let mut usdc_pool = scenario.take_shared_by_id>(usdc_pool_id); + let mut usdt_pool = scenario.take_shared_by_id>(usdt_pool_id); + let mut registry = scenario.take_shared(); + + let usdc_supplier_cap = test_helpers::supply_to_pool( + &mut usdc_pool, + ®istry, + 1_000_000 * test_constants::usdc_multiplier(), + &clock, + scenario.ctx(), + ); + let usdt_supplier_cap = test_helpers::supply_to_pool( + &mut usdt_pool, + ®istry, + 1_000_000 * test_constants::usdt_multiplier(), + &clock, + scenario.ctx(), + ); + + usdc_pool.enable_deepbook_pool_for_loan(®istry, pool_id, &usdc_pool_cap, &clock); + usdt_pool.enable_deepbook_pool_for_loan(®istry, pool_id, &usdt_pool_cap, &clock); + + destroy(usdc_supplier_cap); + destroy(usdt_supplier_cap); + + return_shared_2!(usdc_pool, usdt_pool); + return_to_sender_2!(&scenario, usdc_pool_cap, usdt_pool_cap); + + scenario.next_tx(test_constants::user1()); + let pool = scenario.take_shared>(); + let deepbook_registry = scenario.take_shared_by_id(registry_id); + margin_manager::new( + &pool, + &deepbook_registry, + &mut registry, + &clock, + scenario.ctx(), + ); + return_shared(deepbook_registry); + + scenario.next_tx(test_constants::user1()); + let mut mm = scenario.take_shared>(); + let mut usdt_pool = scenario.take_shared_by_id>(usdt_pool_id); + let usdc_price = build_demo_usdc_price_info_object(&mut scenario, &clock); + let usdt_price = build_demo_usdt_price_info_object(&mut scenario, &clock); + + // Deposit multiple asset types + mm.deposit( + ®istry, + &usdc_price, + &usdt_price, + mint_coin(5_000 * test_constants::usdc_multiplier(), scenario.ctx()), + &clock, + scenario.ctx(), + ); + mm.deposit( + ®istry, + &usdc_price, + &usdt_price, + mint_coin(3_000 * test_constants::usdt_multiplier(), scenario.ctx()), + &clock, + scenario.ctx(), + ); + mm.deposit( + ®istry, + &usdc_price, + &usdt_price, + mint_coin(1_000 * 1_000_000_000, scenario.ctx()), + &clock, + scenario.ctx(), + ); + + // Borrow to create debt + mm.borrow_quote( + ®istry, + &mut usdt_pool, + &usdc_price, + &usdt_price, + &pool, + 2_000 * test_constants::usdt_multiplier(), + &clock, + scenario.ctx(), + ); + + // Risk ratio should account for all assets vs debt + // Total collateral value: $5000 USDC + $3000 USDT = $8000 + // Total debt: $2000 USDT + // Risk ratio should be approximately 4.0 (400%) + + return_shared_3!(usdt_pool, pool, mm); + destroy_2!(usdc_price, usdt_price); + cleanup_margin_test(registry, admin_cap, maintainer_cap, clock, scenario); +} + +#[test] +fun test_risk_ratio_with_oracle_price_changes() { + let ( + mut scenario, + mut clock, + admin_cap, + maintainer_cap, + _btc_pool_id, + usdc_pool_id, + _pool_id, + registry_id, + ) = setup_btc_usd_deepbook_margin(); + + scenario.next_tx(test_constants::user1()); + let pool = scenario.take_shared>(); + let mut registry = scenario.take_shared(); + let deepbook_registry = scenario.take_shared_by_id(registry_id); + margin_manager::new( + &pool, + &deepbook_registry, + &mut registry, + &clock, + scenario.ctx(), + ); + return_shared(deepbook_registry); + + scenario.next_tx(test_constants::user1()); + let mut mm = scenario.take_shared>(); + let btc_pool = scenario.take_shared_by_id>(_btc_pool_id); + let mut usdc_pool = scenario.take_shared_by_id>(usdc_pool_id); + let btc_price = build_btc_price_info_object(&mut scenario, 50000, &clock); + let usdc_price = build_demo_usdc_price_info_object(&mut scenario, &clock); + + // Deposit 1 BTC worth $50k + mm.deposit( + ®istry, + &btc_price, + &usdc_price, + mint_coin(btc_multiplier(), scenario.ctx()), + &clock, + scenario.ctx(), + ); + + // Borrow $20k (40% LTV initially) + mm.borrow_quote( + ®istry, + &mut usdc_pool, + &btc_price, + &usdc_price, + &pool, + 20_000_000000, + &clock, + scenario.ctx(), + ); + + // Initial risk ratio: $50k / $20k = 2.5 (250%) + + // BTC price increases to $60k + advance_time(&mut clock, 1000); + let btc_price_increased = build_btc_price_info_object(&mut scenario, 60000, &clock); + + // Try withdrawing - should succeed as risk ratio improved to $60k / $20k = 3.0 (300%) + let withdrawn = mm.withdraw( + ®istry, + &btc_pool, + &usdc_pool, + &btc_price_increased, + &usdc_price, + &pool, + btc_multiplier() / 10, // Withdraw 0.1 BTC + &clock, + scenario.ctx(), + ); + + destroy(withdrawn); + + // BTC price drops to $35k + advance_time(&mut clock, 1000); + let btc_price_dropped = build_btc_price_info_object(&mut scenario, 35000, &clock); + + // Risk ratio now: 0.9 BTC * $35k / $20k = $31.5k / $20k = 1.575 (157.5%) + // Still above liquidation threshold (120%) but close + + return_shared_2!(btc_pool, usdc_pool); + return_shared_2!(mm, pool); + destroy_3!(btc_price, usdc_price, btc_price_increased); + destroy(btc_price_dropped); + cleanup_margin_test(registry, admin_cap, maintainer_cap, clock, scenario); +} + +// === Position Limits Tests === + +#[test, expected_failure(abort_code = margin_manager::EBorrowRiskRatioExceeded)] +fun test_max_leverage_enforcement() { + let (mut scenario, clock, admin_cap, maintainer_cap) = setup_margin_registry(); + + let usdc_pool_id = create_margin_pool( + &mut scenario, + &maintainer_cap, + default_protocol_config(), + &clock, + ); + let usdt_pool_id = create_margin_pool( + &mut scenario, + &maintainer_cap, + default_protocol_config(), + &clock, + ); + + let (usdc_pool_cap, usdt_pool_cap) = get_margin_pool_caps(&mut scenario, usdc_pool_id); + + let (pool_id, registry_id) = create_pool_for_testing(&mut scenario); + scenario.next_tx(test_constants::admin()); + let mut registry = scenario.take_shared(); + enable_deepbook_margin_on_pool( + pool_id, + &mut registry, + &admin_cap, + &clock, + &mut scenario, + ); + return_shared(registry); + + scenario.next_tx(test_constants::admin()); + let mut usdt_pool = scenario.take_shared_by_id>(usdt_pool_id); + let mut registry = scenario.take_shared(); + let supplier_cap = margin_pool::mint_supplier_cap(®istry, &clock, scenario.ctx()); + + usdt_pool.supply( + ®istry, + &supplier_cap, + mint_coin(10_000_000 * test_constants::usdt_multiplier(), scenario.ctx()), + option::none(), + &clock, + ); + usdt_pool.enable_deepbook_pool_for_loan(®istry, pool_id, &usdt_pool_cap, &clock); + + return_shared(usdt_pool); + return_to_sender_2!(&scenario, usdc_pool_cap, usdt_pool_cap); + + scenario.next_tx(test_constants::user1()); + let pool = scenario.take_shared>(); + let deepbook_registry = scenario.take_shared_by_id(registry_id); + margin_manager::new( + &pool, + &deepbook_registry, + &mut registry, + &clock, + scenario.ctx(), + ); + return_shared(deepbook_registry); + + scenario.next_tx(test_constants::user1()); + let mut mm = scenario.take_shared>(); + let mut usdt_pool = scenario.take_shared_by_id>(usdt_pool_id); + let usdc_price = build_demo_usdc_price_info_object(&mut scenario, &clock); + let usdt_price = build_demo_usdt_price_info_object(&mut scenario, &clock); + + // Deposit small collateral + mm.deposit( + ®istry, + &usdc_price, + &usdt_price, + mint_coin(1_000 * test_constants::usdc_multiplier(), scenario.ctx()), + &clock, + scenario.ctx(), + ); + + // Try to borrow beyond max leverage (would require > 10x leverage) + let excessive_borrow = 10_000 * test_constants::usdt_multiplier(); + // This should fail due to exceeding max leverage + mm.borrow_quote( + ®istry, + &mut usdt_pool, + &usdc_price, + &usdt_price, + &pool, + excessive_borrow, + &clock, + scenario.ctx(), + ); + + abort +} + +#[test, expected_failure(abort_code = margin_pool::EBorrowAmountTooLow)] +fun test_min_position_size_requirement() { + let (mut scenario, clock, admin_cap, maintainer_cap) = setup_margin_registry(); + + let usdc_pool_id = create_margin_pool( + &mut scenario, + &maintainer_cap, + default_protocol_config(), + &clock, + ); + let usdt_pool_id = create_margin_pool( + &mut scenario, + &maintainer_cap, + default_protocol_config(), + &clock, + ); + + let (usdc_pool_cap, usdt_pool_cap) = get_margin_pool_caps(&mut scenario, usdc_pool_id); + + let (pool_id, registry_id) = create_pool_for_testing(&mut scenario); + scenario.next_tx(test_constants::admin()); + let mut registry = scenario.take_shared(); + enable_deepbook_margin_on_pool( + pool_id, + &mut registry, + &admin_cap, + &clock, + &mut scenario, + ); + return_shared(registry); + + scenario.next_tx(test_constants::admin()); + let mut usdt_pool = scenario.take_shared_by_id>(usdt_pool_id); + let mut registry = scenario.take_shared(); + + let usdt_supplier_cap = test_helpers::supply_to_pool( + &mut usdt_pool, + ®istry, + 1_000_000 * test_constants::usdt_multiplier(), + &clock, + scenario.ctx(), + ); + usdt_pool.enable_deepbook_pool_for_loan(®istry, pool_id, &usdt_pool_cap, &clock); + + destroy(usdt_supplier_cap); + return_shared(usdt_pool); + return_to_sender_2!(&scenario, usdc_pool_cap, usdt_pool_cap); + + scenario.next_tx(test_constants::user1()); + let pool = scenario.take_shared>(); + let deepbook_registry = scenario.take_shared_by_id(registry_id); + margin_manager::new( + &pool, + &deepbook_registry, + &mut registry, + &clock, + scenario.ctx(), + ); + return_shared(deepbook_registry); + + scenario.next_tx(test_constants::user1()); + let mut mm = scenario.take_shared>(); + let mut usdt_pool = scenario.take_shared_by_id>(usdt_pool_id); + let usdc_price = build_demo_usdc_price_info_object(&mut scenario, &clock); + let usdt_price = build_demo_usdt_price_info_object(&mut scenario, &clock); + + mm.deposit( + ®istry, + &usdc_price, + &usdt_price, + mint_coin(10_000 * test_constants::usdc_multiplier(), scenario.ctx()), + &clock, + scenario.ctx(), + ); + + // Try to borrow below minimum position size (default min_borrow is 10 * PRECISION_DECIMAL_9) + let tiny_borrow = 1; // 1 mist, way below minimum + mm.borrow_quote( + ®istry, + &mut usdt_pool, + &usdc_price, + &usdt_price, + &pool, + tiny_borrow, + &clock, + scenario.ctx(), + ); + + // This should never be reached due to expected failure + return_shared_2!(usdt_pool, pool); + destroy_2!(usdc_price, usdt_price); + return_shared(mm); + cleanup_margin_test(registry, admin_cap, maintainer_cap, clock, scenario); +} + +#[test] +fun test_repayment_rounding() { + let (mut scenario, mut clock, admin_cap, maintainer_cap) = setup_margin_registry(); + + let usdc_pool_id = create_margin_pool( + &mut scenario, + &maintainer_cap, + default_protocol_config(), + &clock, + ); + let usdt_pool_id = create_margin_pool( + &mut scenario, + &maintainer_cap, + default_protocol_config(), + &clock, + ); + + let (usdc_pool_cap, usdt_pool_cap) = get_margin_pool_caps(&mut scenario, usdc_pool_id); + + let (pool_id, registry_id) = create_pool_for_testing(&mut scenario); + scenario.next_tx(test_constants::admin()); + let mut registry = scenario.take_shared(); + enable_deepbook_margin_on_pool( + pool_id, + &mut registry, + &admin_cap, + &clock, + &mut scenario, + ); + return_shared(registry); + + scenario.next_tx(test_constants::admin()); + let mut usdc_pool = scenario.take_shared_by_id>(usdc_pool_id); + let mut usdt_pool = scenario.take_shared_by_id>(usdt_pool_id); + let mut registry = scenario.take_shared(); + + let usdc_supplier_cap = test_helpers::supply_to_pool( + &mut usdc_pool, + ®istry, + 1_000_000 * test_constants::usdc_multiplier(), + &clock, + scenario.ctx(), + ); + let usdt_supplier_cap = test_helpers::supply_to_pool( + &mut usdt_pool, + ®istry, + 1_000_000 * test_constants::usdt_multiplier(), + &clock, + scenario.ctx(), + ); + + usdc_pool.enable_deepbook_pool_for_loan(®istry, pool_id, &usdc_pool_cap, &clock); + usdt_pool.enable_deepbook_pool_for_loan(®istry, pool_id, &usdt_pool_cap, &clock); + + destroy(usdc_supplier_cap); + destroy(usdt_supplier_cap); + return_shared_2!(usdc_pool, usdt_pool); + return_to_sender_2!(&scenario, usdc_pool_cap, usdt_pool_cap); + + scenario.next_tx(test_constants::user1()); + let pool = scenario.take_shared>(); + let deepbook_registry = scenario.take_shared_by_id(registry_id); + margin_manager::new( + &pool, + &deepbook_registry, + &mut registry, + &clock, + scenario.ctx(), + ); + return_shared(deepbook_registry); + + scenario.next_tx(test_constants::user1()); + let mut mm = scenario.take_shared>(); + let mut usdt_pool = scenario.take_shared_by_id>(usdt_pool_id); + let usdc_price = build_demo_usdc_price_info_object(&mut scenario, &clock); + let usdt_price = build_demo_usdt_price_info_object(&mut scenario, &clock); + + // Setup position with debt + mm.deposit( + ®istry, + &usdc_price, + &usdt_price, + mint_coin(20_000 * test_constants::usdc_multiplier(), scenario.ctx()), + &clock, + scenario.ctx(), + ); + + mm.borrow_quote( + ®istry, + &mut usdt_pool, + &usdc_price, + &usdt_price, + &pool, + 5_000 * test_constants::usdt_multiplier(), + &clock, + scenario.ctx(), + ); + + advance_time(&mut clock, 1000 * 100); // 100 seconds later + + destroy_2!(usdc_price, usdt_price); + + // Recreate price objects after time advance (they become stale) + // Create new ones in a new transaction to avoid stale price errors + scenario.next_tx(test_constants::user1()); + let usdc_price = build_demo_usdc_price_info_object(&mut scenario, &clock); + let usdt_price = build_demo_usdt_price_info_object(&mut scenario, &clock); + + // Partial repayment + mm.deposit( + ®istry, + &usdc_price, + &usdt_price, + mint_coin(2_000 * test_constants::usdt_multiplier(), scenario.ctx()), + &clock, + scenario.ctx(), + ); + + let repaid_amount = mm.repay_quote( + ®istry, + &mut usdt_pool, + option::some(2_000 * test_constants::usdt_multiplier()), + &clock, + scenario.ctx(), + ); + + assert!(repaid_amount == 2_000 * test_constants::usdt_multiplier()); + + // Full repayment + mm.deposit( + ®istry, + &usdc_price, + &usdt_price, + mint_coin(5_000 * test_constants::usdt_multiplier(), scenario.ctx()), + &clock, + scenario.ctx(), + ); + + let final_repaid = mm.repay_quote( + ®istry, + &mut usdt_pool, + option::none(), // Repay all + &clock, + scenario.ctx(), + ); + + assert!(final_repaid > 0); + assert!(mm.borrowed_quote_shares() == 0); + + return_shared_2!(usdt_pool, pool); + destroy_2!(usdc_price, usdt_price); + return_shared(mm); + cleanup_margin_test(registry, admin_cap, maintainer_cap, clock, scenario); +} + +#[test] +fun test_asset_rebalancing_between_pools() { + let (mut scenario, clock, admin_cap, maintainer_cap) = setup_margin_registry(); + + let usdc_pool_id = create_margin_pool( + &mut scenario, + &maintainer_cap, + default_protocol_config(), + &clock, + ); + let usdt_pool_id = create_margin_pool( + &mut scenario, + &maintainer_cap, + default_protocol_config(), + &clock, + ); + + let (usdc_pool_cap, usdt_pool_cap) = get_margin_pool_caps(&mut scenario, usdc_pool_id); + + let (pool_id, registry_id) = create_pool_for_testing(&mut scenario); + scenario.next_tx(test_constants::admin()); + let mut registry = scenario.take_shared(); + enable_deepbook_margin_on_pool( + pool_id, + &mut registry, + &admin_cap, + &clock, + &mut scenario, + ); + return_shared(registry); + + scenario.next_tx(test_constants::admin()); + let mut usdc_pool = scenario.take_shared_by_id>(usdc_pool_id); + let mut usdt_pool = scenario.take_shared_by_id>(usdt_pool_id); + let mut registry = scenario.take_shared(); + + let usdc_supplier_cap = test_helpers::supply_to_pool( + &mut usdc_pool, + ®istry, + 1_000_000 * test_constants::usdc_multiplier(), + &clock, + scenario.ctx(), + ); + let usdt_supplier_cap = test_helpers::supply_to_pool( + &mut usdt_pool, + ®istry, + 1_000_000 * test_constants::usdt_multiplier(), + &clock, + scenario.ctx(), + ); + + usdc_pool.enable_deepbook_pool_for_loan(®istry, pool_id, &usdc_pool_cap, &clock); + usdt_pool.enable_deepbook_pool_for_loan(®istry, pool_id, &usdt_pool_cap, &clock); + + destroy(usdc_supplier_cap); + destroy(usdt_supplier_cap); + return_shared_2!(usdc_pool, usdt_pool); + return_to_sender_2!(&scenario, usdc_pool_cap, usdt_pool_cap); + + scenario.next_tx(test_constants::user1()); + let pool = scenario.take_shared>(); + let deepbook_registry = scenario.take_shared_by_id(registry_id); + margin_manager::new( + &pool, + &deepbook_registry, + &mut registry, + &clock, + scenario.ctx(), + ); + return_shared(deepbook_registry); + + scenario.next_tx(test_constants::user1()); + let mut mm = scenario.take_shared>(); + + // Get margin pools for withdraw API + let usdc_pool = scenario.take_shared_by_id>(usdc_pool_id); + let usdt_pool = scenario.take_shared_by_id>(usdt_pool_id); + let usdc_price = build_demo_usdc_price_info_object(&mut scenario, &clock); + let usdt_price = build_demo_usdt_price_info_object(&mut scenario, &clock); + + // Deposit assets in both base and quote + mm.deposit( + ®istry, + &usdc_price, + &usdt_price, + mint_coin(10_000 * test_constants::usdc_multiplier(), scenario.ctx()), + &clock, + scenario.ctx(), + ); + mm.deposit( + ®istry, + &usdc_price, + &usdt_price, + mint_coin(10_000 * test_constants::usdt_multiplier(), scenario.ctx()), + &clock, + scenario.ctx(), + ); + + // Withdraw from one type (using new API) + let usdc_withdrawn = mm.withdraw( + ®istry, + &usdc_pool, + &usdt_pool, + &usdc_price, + &usdt_price, + &pool, + 5_000 * test_constants::usdc_multiplier(), + &clock, + scenario.ctx(), + ); + + // No debt, so withdrawal should succeed + assert!(usdc_withdrawn.value() == 5_000 * test_constants::usdc_multiplier()); + + // Deposit back different asset + mm.deposit( + ®istry, + &usdc_price, + &usdt_price, + mint_coin(5_000 * test_constants::usdt_multiplier(), scenario.ctx()), + &clock, + scenario.ctx(), + ); + + destroy(usdc_withdrawn); + destroy_2!(usdc_price, usdt_price); + return_shared_3!(usdc_pool, usdt_pool, pool); + return_shared(mm); + cleanup_margin_test(registry, admin_cap, maintainer_cap, clock, scenario); +} + +#[test] +fun test_risk_ratio_returns_max_when_no_loan_but_has_assets() { + let ( + mut scenario, + clock, + admin_cap, + maintainer_cap, + btc_pool_id, + usdc_pool_id, + _pool_id, + registry_id, + ) = setup_btc_usd_deepbook_margin(); + + scenario.next_tx(test_constants::user1()); + let pool = scenario.take_shared>(); + let mut registry = scenario.take_shared(); + let deepbook_registry = scenario.take_shared_by_id(registry_id); + margin_manager::new( + &pool, + &deepbook_registry, + &mut registry, + &clock, + scenario.ctx(), + ); + return_shared(deepbook_registry); + + scenario.next_tx(test_constants::user1()); + let mut mm = scenario.take_shared>(); + let btc_pool = scenario.take_shared_by_id>(btc_pool_id); + let usdc_pool = scenario.take_shared_by_id>(usdc_pool_id); + let btc_price = build_btc_price_info_object(&mut scenario, 50000, &clock); + let usdc_price = build_demo_usdc_price_info_object(&mut scenario, &clock); + + // Deposit 1 BTC worth $50k (but don't borrow anything) + mm.deposit( + ®istry, + &btc_price, + &usdc_price, + mint_coin(btc_multiplier(), scenario.ctx()), + &clock, + scenario.ctx(), + ); + + assert!(mm.borrowed_base_shares() == 0); + assert!(mm.borrowed_quote_shares() == 0); + + let risk_ratio = mm.risk_ratio( + ®istry, + &btc_price, + &usdc_price, + &pool, + &btc_pool, + &usdc_pool, + &clock, + ); + + assert!(risk_ratio == margin_constants::max_risk_ratio()); + + destroy_2!(btc_price, usdc_price); + return_shared_3!(btc_pool, usdc_pool, pool); + return_shared(mm); + cleanup_margin_test(registry, admin_cap, maintainer_cap, clock, scenario); +} + +#[test] +fun test_risk_ratio_returns_max_when_completely_empty() { + let ( + mut scenario, + clock, + admin_cap, + maintainer_cap, + btc_pool_id, + usdc_pool_id, + _pool_id, + registry_id, + ) = setup_btc_usd_deepbook_margin(); + + let btc_price = build_btc_price_info_object(&mut scenario, 50000, &clock); + let usdc_price = build_demo_usdc_price_info_object(&mut scenario, &clock); + + scenario.next_tx(test_constants::user1()); + let pool = scenario.take_shared>(); + let mut registry = scenario.take_shared(); + let deepbook_registry = scenario.take_shared_by_id(registry_id); + margin_manager::new( + &pool, + &deepbook_registry, + &mut registry, + &clock, + scenario.ctx(), + ); + return_shared(deepbook_registry); + + scenario.next_tx(test_constants::user1()); + let mm = scenario.take_shared>(); + let btc_pool = scenario.take_shared_by_id>(btc_pool_id); + let usdc_pool = scenario.take_shared_by_id>(usdc_pool_id); + + assert!(mm.borrowed_base_shares() == 0); + assert!(mm.borrowed_quote_shares() == 0); + + let (base_assets, quote_assets) = mm.calculate_assets(&pool); + assert!(base_assets == 0); + assert!(quote_assets == 0); + + let risk_ratio = mm.risk_ratio( + ®istry, + &btc_price, + &usdc_price, + &pool, + &btc_pool, + &usdc_pool, + &clock, + ); + + assert!(risk_ratio == margin_constants::max_risk_ratio()); + + destroy_2!(btc_price, usdc_price); + return_shared_3!(btc_pool, usdc_pool, pool); + return_shared(mm); + cleanup_margin_test(registry, admin_cap, maintainer_cap, clock, scenario); +} + +#[test] +fun test_borrow_at_exact_min_risk_ratio_no_rounding_issues() { + let (mut scenario, mut clock, admin_cap, maintainer_cap) = setup_margin_registry(); + + clock.set_for_testing(1000000); + + // Create USDC margin pool + let usdc_pool_id = create_margin_pool( + &mut scenario, + &maintainer_cap, + default_protocol_config(), + &clock, + ); + + // Create USDT margin pool (needed for the DeepBook pool) + let usdt_pool_id = create_margin_pool( + &mut scenario, + &maintainer_cap, + default_protocol_config(), + &clock, + ); + + // Create DeepBook pool + let (pool_id, registry_id) = create_pool_for_testing(&mut scenario); + scenario.next_tx(test_constants::admin()); + let mut registry = scenario.take_shared(); + enable_deepbook_margin_on_pool( + pool_id, + &mut registry, + &admin_cap, + &clock, + &mut scenario, + ); + return_shared(registry); + + // Fund the USDC margin pool with exactly 10 USDC (10 * 10^6) + scenario.next_tx(test_constants::admin()); + let (usdc_pool_cap, usdt_pool_cap) = get_margin_pool_caps(&mut scenario, usdc_pool_id); + let mut usdc_pool = scenario.take_shared_by_id>(usdc_pool_id); + let mut usdt_pool = scenario.take_shared_by_id>(usdt_pool_id); + let registry = scenario.take_shared(); + let supplier_cap = margin_pool::mint_supplier_cap(®istry, &clock, scenario.ctx()); + + usdc_pool.supply( + ®istry, + &supplier_cap, + mint_coin(10 * test_constants::usdc_multiplier(), scenario.ctx()), + option::none(), + &clock, + ); + + // Also fund USDT pool for completeness + usdt_pool.supply( + ®istry, + &supplier_cap, + mint_coin(10_000 * test_constants::usdt_multiplier(), scenario.ctx()), + option::none(), + &clock, + ); + + usdc_pool.enable_deepbook_pool_for_loan(®istry, pool_id, &usdc_pool_cap, &clock); + usdt_pool.enable_deepbook_pool_for_loan(®istry, pool_id, &usdt_pool_cap, &clock); + + return_shared_2!(usdc_pool, usdt_pool); + return_shared(registry); + scenario.return_to_sender(usdt_pool_cap); + scenario.return_to_sender(usdc_pool_cap); + destroy(supplier_cap); + + // User creates margin manager + scenario.next_tx(test_constants::user1()); + let mut registry = scenario.take_shared(); + let pool = scenario.take_shared>(); + let deepbook_registry = scenario.take_shared_by_id(registry_id); + margin_manager::new( + &pool, + &deepbook_registry, + &mut registry, + &clock, + scenario.ctx(), + ); + return_shared_2!(deepbook_registry, pool); + return_shared(registry); + + // User deposits exactly 1 USDC (1 * 10^6) + scenario.next_tx(test_constants::user1()); + let mut mm = scenario.take_shared>(); + let registry = scenario.take_shared(); + let usdc_price = build_demo_usdc_price_info_object(&mut scenario, &clock); + let usdt_price = build_demo_usdt_price_info_object(&mut scenario, &clock); + + let deposit_coin = mint_coin(1 * test_constants::usdc_multiplier(), scenario.ctx()); + mm.deposit( + ®istry, + &usdt_price, + &usdc_price, + deposit_coin, + &clock, + scenario.ctx(), + ); + destroy_2!(usdc_price, usdt_price); + return_shared_2!(mm, registry); + + // User borrows exactly 4 USDC (4 * 10^6) + // Risk ratio should be (1 + 4) / 4 = 1.25, exactly at the minimum + scenario.next_tx(test_constants::user1()); + let mut mm = scenario.take_shared>(); + let mut usdc_pool = scenario.take_shared_by_id>(usdc_pool_id); + let registry = scenario.take_shared(); + let pool = scenario.take_shared>(); + let usdc_price = build_demo_usdc_price_info_object(&mut scenario, &clock); + let usdt_price = build_demo_usdt_price_info_object(&mut scenario, &clock); + + mm.borrow_quote( + ®istry, + &mut usdc_pool, + &usdt_price, + &usdc_price, + &pool, + 4 * test_constants::usdc_multiplier(), // 4 USDC + &clock, + scenario.ctx(), + ); + + // Verify risk ratio is exactly at the minimum (1.25 with 9 decimals = 1_250_000_000) + let base_pool = scenario.take_shared_by_id>(usdt_pool_id); + let risk_ratio = mm.risk_ratio( + ®istry, + &usdt_price, + &usdc_price, + &pool, + &base_pool, + &usdc_pool, + &clock, + ); + + // Risk ratio should be exactly the minimum borrow risk ratio (1.25) + assert!(risk_ratio == test_constants::min_borrow_risk_ratio()); + + return_shared(base_pool); + destroy_2!(usdc_price, usdt_price); + return_shared_3!(usdc_pool, pool, mm); + cleanup_margin_test(registry, admin_cap, maintainer_cap, clock, scenario); +} + +#[test] +fun test_borrow_at_exact_min_risk_ratio_with_custom_price() { + let (mut scenario, mut clock, admin_cap, maintainer_cap) = setup_margin_registry(); + + clock.set_for_testing(1000000); + + // Create USDC margin pool + let usdc_pool_id = create_margin_pool( + &mut scenario, + &maintainer_cap, + default_protocol_config(), + &clock, + ); + + // Create USDT margin pool (needed for the DeepBook pool) + let usdt_pool_id = create_margin_pool( + &mut scenario, + &maintainer_cap, + default_protocol_config(), + &clock, + ); + + // Create DeepBook pool + let (pool_id, registry_id) = create_pool_for_testing(&mut scenario); + scenario.next_tx(test_constants::admin()); + let mut registry = scenario.take_shared(); + enable_deepbook_margin_on_pool( + pool_id, + &mut registry, + &admin_cap, + &clock, + &mut scenario, + ); + return_shared(registry); + + // Fund the USDC margin pool with exactly 10 USDC (10 * 10^6) + scenario.next_tx(test_constants::admin()); + let (usdc_pool_cap, usdt_pool_cap) = get_margin_pool_caps(&mut scenario, usdc_pool_id); + let mut usdc_pool = scenario.take_shared_by_id>(usdc_pool_id); + let mut usdt_pool = scenario.take_shared_by_id>(usdt_pool_id); + let registry = scenario.take_shared(); + let supplier_cap = margin_pool::mint_supplier_cap(®istry, &clock, scenario.ctx()); + + usdc_pool.supply( + ®istry, + &supplier_cap, + mint_coin(10 * test_constants::usdc_multiplier(), scenario.ctx()), + option::none(), + &clock, + ); + + // Also fund USDT pool for completeness + usdt_pool.supply( + ®istry, + &supplier_cap, + mint_coin(10_000 * test_constants::usdt_multiplier(), scenario.ctx()), + option::none(), + &clock, + ); + + usdc_pool.enable_deepbook_pool_for_loan(®istry, pool_id, &usdc_pool_cap, &clock); + usdt_pool.enable_deepbook_pool_for_loan(®istry, pool_id, &usdt_pool_cap, &clock); + + return_shared_2!(usdc_pool, usdt_pool); + return_shared(registry); + scenario.return_to_sender(usdt_pool_cap); + scenario.return_to_sender(usdc_pool_cap); + destroy(supplier_cap); + + // User creates margin manager + scenario.next_tx(test_constants::user1()); + let mut registry = scenario.take_shared(); + let pool = scenario.take_shared>(); + let deepbook_registry = scenario.take_shared_by_id(registry_id); + margin_manager::new( + &pool, + &deepbook_registry, + &mut registry, + &clock, + scenario.ctx(), + ); + return_shared_2!(deepbook_registry, pool); + return_shared(registry); + + // User deposits exactly 1 USDC (1 * 10^6) + scenario.next_tx(test_constants::user1()); + let mut mm = scenario.take_shared>(); + let registry = scenario.take_shared(); + let usdc_price = build_demo_usdc_price_info_object(&mut scenario, &clock); + let usdt_price = build_demo_usdt_price_info_object(&mut scenario, &clock); + + let deposit_coin = mint_coin(1 * test_constants::usdc_multiplier(), scenario.ctx()); + mm.deposit( + ®istry, + &usdt_price, + &usdc_price, + deposit_coin, + &clock, + scenario.ctx(), + ); + destroy_2!(usdc_price, usdt_price); + return_shared_2!(mm, registry); + + // User borrows exactly 4 USDC (4 * 10^6) + // With USDC at 0.99984495 instead of 1.00, verify the operation still works + scenario.next_tx(test_constants::user1()); + let mut mm = scenario.take_shared>(); + let mut usdc_pool = scenario.take_shared_by_id>(usdc_pool_id); + let registry = scenario.take_shared(); + let pool = scenario.take_shared>(); + let usdc_price = test_helpers::build_demo_usdc_price_info_object_with_price( + &mut scenario, + 99984495, // $0.99984495 with 8 decimals + &clock, + ); + let usdt_price = build_demo_usdt_price_info_object(&mut scenario, &clock); + + mm.borrow_quote( + ®istry, + &mut usdc_pool, + &usdt_price, + &usdc_price, + &pool, + 4 * test_constants::usdc_multiplier(), // 4 USDC + &clock, + scenario.ctx(), + ); + + // Verify risk ratio + let base_pool = scenario.take_shared_by_id>(usdt_pool_id); + let risk_ratio = mm.risk_ratio( + ®istry, + &usdt_price, + &usdc_price, + &pool, + &base_pool, + &usdc_pool, + &clock, + ); + + // Risk ratio should still be approximately at the minimum + // With the price difference, it might be slightly different + // USDC at 0.99984495: (1 * 0.99984495 + 4 * 0.99984495) / (4 * 0.99984495) = 5/4 = 1.25 + // The ratio should still be exactly 1.25 since both assets use the same price + assert!(risk_ratio == test_constants::min_borrow_risk_ratio()); + + return_shared(base_pool); + destroy_2!(usdc_price, usdt_price); + return_shared_3!(usdc_pool, pool, mm); + cleanup_margin_test(registry, admin_cap, maintainer_cap, clock, scenario); +} + +#[test, expected_failure(abort_code = margin_manager::ERepayAmountTooLow)] +fun test_liquidate_fails_with_too_low_repay_amount() { + let ( + mut scenario, + mut clock, + _admin_cap, + _maintainer_cap, + _btc_pool_id, + usdc_pool_id, + _pool_id, + registry_id, + ) = setup_btc_usd_deepbook_margin(); + + scenario.next_tx(test_constants::user1()); + let mut pool = scenario.take_shared>(); + let mut registry = scenario.take_shared(); + let deepbook_registry = scenario.take_shared_by_id(registry_id); + margin_manager::new( + &pool, + &deepbook_registry, + &mut registry, + &clock, + scenario.ctx(), + ); + return_shared(deepbook_registry); + + scenario.next_tx(test_constants::user1()); + let mut mm = scenario.take_shared>(); + let mut usdc_pool = scenario.take_shared_by_id>(usdc_pool_id); + let btc_price = build_btc_price_info_object(&mut scenario, 50000, &clock); + let usdc_price = build_demo_usdc_price_info_object(&mut scenario, &clock); + + // Deposit 1 BTC worth $50k + mm.deposit( + ®istry, + &btc_price, + &usdc_price, + mint_coin(btc_multiplier(), scenario.ctx()), + &clock, + scenario.ctx(), + ); + + // Borrow $40k USDC (risk ratio = 50k/40k = 1.25) + mm.borrow_quote( + ®istry, + &mut usdc_pool, + &btc_price, + &usdc_price, + &pool, + 40_000_000_000, + &clock, + scenario.ctx(), + ); + + // Advance time by 1 day to accrue interest + advance_time(&mut clock, 86_400_000); // 1 day in milliseconds + + // Drop BTC price to $3k to make position underwater and liquidatable + // Assets: 1 BTC at $3k + $40k USDC = $43k + // Debt: $40k+ (with interest) + // Risk ratio: ~$43k / ~$40k = ~1.075 (107.5%) < 110% liquidation threshold + // Create new price object AFTER time advancement to ensure it's fresh + destroy(btc_price); + destroy(usdc_price); + scenario.next_tx(test_constants::admin()); + let btc_price_dropped = build_btc_price_info_object(&mut scenario, 3000, &clock); + let usdc_price_fresh = build_demo_usdc_price_info_object(&mut scenario, &clock); + + // Try to liquidate with an extremely small repay amount (1 unit) + // This should fail with ERepayAmountTooLow + scenario.next_tx(test_constants::liquidator()); + let debt_coin = mint_coin(1, scenario.ctx()); // Just 1 unit - way too low + + let (base_coin, quote_coin, remaining_debt) = mm.liquidate( + ®istry, + &btc_price_dropped, + &usdc_price_fresh, + &mut usdc_pool, + &mut pool, + debt_coin, + &clock, + scenario.ctx(), + ); + + // Should never reach here + destroy_3!(base_coin, quote_coin, remaining_debt); + abort (0) +} + +#[test] +fun test_unregister_margin_manager() { + let (mut scenario, clock, admin_cap, maintainer_cap) = setup_margin_registry(); + + create_margin_pool( + &mut scenario, + &maintainer_cap, + default_protocol_config(), + &clock, + ); + create_margin_pool( + &mut scenario, + &maintainer_cap, + default_protocol_config(), + &clock, + ); + + // Create DeepBook pool and enable margin trading on it + let (pool_id, registry_id) = create_pool_for_testing(&mut scenario); + scenario.next_tx(test_constants::admin()); + let mut registry = scenario.take_shared(); + enable_deepbook_margin_on_pool( + pool_id, + &mut registry, + &admin_cap, + &clock, + &mut scenario, + ); + return_shared(registry); + + // Create first margin manager + scenario.next_tx(test_constants::user1()); + let pool = scenario.take_shared>(); + let mut registry = scenario.take_shared(); + let deepbook_registry = scenario.take_shared_by_id(registry_id); + let manager1_id = margin_manager::new( + &pool, + &deepbook_registry, + &mut registry, + &clock, + scenario.ctx(), + ); + return_shared_3!(deepbook_registry, pool, registry); + + // Create second margin manager + scenario.next_tx(test_constants::user1()); + let pool = scenario.take_shared>(); + let mut registry = scenario.take_shared(); + let deepbook_registry = scenario.take_shared_by_id(registry_id); + let manager2_id = margin_manager::new( + &pool, + &deepbook_registry, + &mut registry, + &clock, + scenario.ctx(), + ); + return_shared_3!(deepbook_registry, pool, registry); + + // Verify both are registered + scenario.next_tx(test_constants::user1()); + let registry = scenario.take_shared(); + let manager_ids = margin_registry::get_margin_manager_ids(®istry, test_constants::user1()); + assert!(manager_ids.length() == 2); + assert!(manager_ids.contains(&manager1_id)); + assert!(manager_ids.contains(&manager2_id)); + return_shared(registry); + + // Unregister first manager + scenario.next_tx(test_constants::user1()); + let mut mm1 = scenario.take_shared_by_id>(manager1_id); + let mut registry = scenario.take_shared(); + margin_manager::unregister_margin_manager( + &mut mm1, + &mut registry, + scenario.ctx(), + ); + return_shared_2!(mm1, registry); + + // Verify only second manager remains + scenario.next_tx(test_constants::user1()); + let registry = scenario.take_shared(); + let manager_ids = margin_registry::get_margin_manager_ids(®istry, test_constants::user1()); + assert!(manager_ids.length() == 1); + assert!(!manager_ids.contains(&manager1_id)); + assert!(manager_ids.contains(&manager2_id)); + cleanup_margin_test(registry, admin_cap, maintainer_cap, clock, scenario); +} + +#[test] +#[expected_failure(abort_code = margin_manager::EPoolNotEnabledForMarginTrading)] +fun test_borrow_base_fails_when_pool_disabled() { + let ( + mut scenario, + clock, + admin_cap, + _maintainer_cap, + btc_pool_id, + _usdc_pool_id, + _pool_id, + registry_id, + ) = setup_btc_usd_deepbook_margin(); + + // Supply liquidity to BTC pool + scenario.next_tx(test_constants::admin()); + let mut btc_pool = scenario.take_shared_by_id>(btc_pool_id); + let registry = scenario.take_shared(); + let supplier_cap = test_helpers::supply_to_pool( + &mut btc_pool, + ®istry, + 100 * btc_multiplier(), + &clock, + scenario.ctx(), + ); + return_shared_2!(btc_pool, registry); + destroy(supplier_cap); + + // Create margin manager and deposit collateral + scenario.next_tx(test_constants::user1()); + let pool = scenario.take_shared>(); + let mut registry = scenario.take_shared(); + let deepbook_registry = scenario.take_shared_by_id(registry_id); + margin_manager::new( + &pool, + &deepbook_registry, + &mut registry, + &clock, + scenario.ctx(), + ); + return_shared(deepbook_registry); + return_shared_2!(pool, registry); + + scenario.next_tx(test_constants::user1()); + let mut mm = scenario.take_shared>(); + let registry = scenario.take_shared(); + let btc_price = build_btc_price_info_object(&mut scenario, 50000, &clock); + let usdc_price = build_demo_usdc_price_info_object(&mut scenario, &clock); + let deposit_coin = mint_coin( + 5_000_000 * test_constants::usdc_multiplier(), + scenario.ctx(), + ); + mm.deposit( + ®istry, + &btc_price, + &usdc_price, + deposit_coin, + &clock, + scenario.ctx(), + ); + destroy_2!(btc_price, usdc_price); + return_shared_2!(mm, registry); + + // Admin disables the deepbook pool + scenario.next_tx(test_constants::admin()); + let mut registry = scenario.take_shared(); + let mut pool = scenario.take_shared>(); + registry.disable_deepbook_pool(&admin_cap, &mut pool, &clock); + return_shared_2!(registry, pool); + + // User1 tries to borrow BTC - this should fail with EPoolNotEnabledForMarginTrading + scenario.next_tx(test_constants::user1()); + let mut mm = scenario.take_shared>(); + let registry = scenario.take_shared(); + let mut btc_pool = scenario.take_shared_by_id>(btc_pool_id); + let pool = scenario.take_shared>(); + let btc_price = build_btc_price_info_object(&mut scenario, 50000, &clock); + let usdc_price = build_demo_usdc_price_info_object(&mut scenario, &clock); + + mm.borrow_base( + ®istry, + &mut btc_pool, + &btc_price, + &usdc_price, + &pool, + 10 * btc_multiplier(), + &clock, + scenario.ctx(), + ); + + // Cleanup (unreachable due to expected failure above) + abort 0 +} + +#[test] +#[expected_failure(abort_code = margin_manager::EPoolNotEnabledForMarginTrading)] +fun test_borrow_quote_fails_when_pool_disabled() { + let ( + mut scenario, + clock, + admin_cap, + _maintainer_cap, + _btc_pool_id, + usdc_pool_id, + _pool_id, + registry_id, + ) = setup_btc_usd_deepbook_margin(); + + // Supply liquidity to USDC pool + scenario.next_tx(test_constants::admin()); + let mut usdc_pool = scenario.take_shared_by_id>(usdc_pool_id); + let registry = scenario.take_shared(); + let supplier_cap = test_helpers::supply_to_pool( + &mut usdc_pool, + ®istry, + 10_000_000 * test_constants::usdc_multiplier(), + &clock, + scenario.ctx(), + ); + return_shared_2!(usdc_pool, registry); + destroy(supplier_cap); + + // Create margin manager and deposit collateral + scenario.next_tx(test_constants::user1()); + let pool = scenario.take_shared>(); + let mut registry = scenario.take_shared(); + let deepbook_registry = scenario.take_shared_by_id(registry_id); + margin_manager::new( + &pool, + &deepbook_registry, + &mut registry, + &clock, + scenario.ctx(), + ); + return_shared(deepbook_registry); + return_shared_2!(pool, registry); + + scenario.next_tx(test_constants::user1()); + let mut mm = scenario.take_shared>(); + let registry = scenario.take_shared(); + let btc_price = build_btc_price_info_object(&mut scenario, 50000, &clock); + let usdc_price = build_demo_usdc_price_info_object(&mut scenario, &clock); + let deposit_coin = mint_coin( + 10 * btc_multiplier(), + scenario.ctx(), + ); + mm.deposit( + ®istry, + &btc_price, + &usdc_price, + deposit_coin, + &clock, + scenario.ctx(), + ); + destroy_2!(btc_price, usdc_price); + return_shared_2!(mm, registry); + + // Admin disables the deepbook pool + scenario.next_tx(test_constants::admin()); + let mut registry = scenario.take_shared(); + let mut pool = scenario.take_shared>(); + registry.disable_deepbook_pool(&admin_cap, &mut pool, &clock); + return_shared_2!(registry, pool); + + // User1 tries to borrow USDC - this should fail with EPoolNotEnabledForMarginTrading + scenario.next_tx(test_constants::user1()); + let mut mm = scenario.take_shared>(); + let registry = scenario.take_shared(); + let mut usdc_pool = scenario.take_shared_by_id>(usdc_pool_id); + let pool = scenario.take_shared>(); + let btc_price = build_btc_price_info_object(&mut scenario, 50000, &clock); + let usdc_price = build_demo_usdc_price_info_object(&mut scenario, &clock); + + mm.borrow_quote( + ®istry, + &mut usdc_pool, + &btc_price, + &usdc_price, + &pool, + 100_000 * test_constants::usdc_multiplier(), + &clock, + scenario.ctx(), + ); + + // Cleanup (unreachable due to expected failure above) + abort 0 +} + +#[test, expected_failure(abort_code = margin_manager::EOutstandingDebt)] +fun test_unregister_margin_manager_fails_with_outstanding_base_debt() { + let ( + mut scenario, + clock, + _admin_cap, + _maintainer_cap, + btc_pool_id, + _usdc_pool_id, + _pool_id, + registry_id, + ) = setup_btc_usd_deepbook_margin(); + + // Supply liquidity to BTC pool so we can borrow + scenario.next_tx(test_constants::admin()); + let mut btc_pool = scenario.take_shared_by_id>(btc_pool_id); + let registry = scenario.take_shared(); + let supplier_cap = margin_pool::mint_supplier_cap(®istry, &clock, scenario.ctx()); + btc_pool.supply( + ®istry, + &supplier_cap, + mint_coin(100 * btc_multiplier(), scenario.ctx()), + option::none(), + &clock, + ); + return_shared_2!(btc_pool, registry); + destroy(supplier_cap); + + // Create margin manager + scenario.next_tx(test_constants::user1()); + let pool = scenario.take_shared>(); + let mut registry = scenario.take_shared(); + let deepbook_registry = scenario.take_shared_by_id(registry_id); + margin_manager::new( + &pool, + &deepbook_registry, + &mut registry, + &clock, + scenario.ctx(), + ); + return_shared(deepbook_registry); + return_shared_2!(pool, registry); + + // Deposit collateral (USDC) + scenario.next_tx(test_constants::user1()); + let mut mm = scenario.take_shared>(); + let registry = scenario.take_shared(); + let btc_price = build_btc_price_info_object(&mut scenario, 50000, &clock); + let usdc_price = build_demo_usdc_price_info_object(&mut scenario, &clock); + mm.deposit( + ®istry, + &btc_price, + &usdc_price, + mint_coin(1_000_000 * test_constants::usdc_multiplier(), scenario.ctx()), + &clock, + scenario.ctx(), + ); + destroy_2!(btc_price, usdc_price); + return_shared_2!(mm, registry); + + // Borrow BTC (base asset) to create outstanding debt + scenario.next_tx(test_constants::user1()); + let mut mm = scenario.take_shared>(); + let registry = scenario.take_shared(); + let mut btc_pool = scenario.take_shared_by_id>(btc_pool_id); + let pool = scenario.take_shared>(); + let btc_price = build_btc_price_info_object(&mut scenario, 50000, &clock); + let usdc_price = build_demo_usdc_price_info_object(&mut scenario, &clock); + + mm.borrow_base( + ®istry, + &mut btc_pool, + &btc_price, + &usdc_price, + &pool, + 1 * btc_multiplier(), + &clock, + scenario.ctx(), + ); + + assert!(mm.borrowed_base_shares() > 0); + + destroy_2!(btc_price, usdc_price); + return_shared_3!(btc_pool, pool, registry); + return_shared(mm); + + // Try to unregister with outstanding base debt - should fail + scenario.next_tx(test_constants::user1()); + let mut mm = scenario.take_shared>(); + let mut registry = scenario.take_shared(); + + margin_manager::unregister_margin_manager( + &mut mm, + &mut registry, + scenario.ctx(), + ); + + // Cleanup (unreachable due to expected failure above) + abort 0 +} + +#[test, expected_failure(abort_code = margin_manager::EOutstandingDebt)] +fun test_unregister_margin_manager_fails_with_outstanding_quote_debt() { + let ( + mut scenario, + clock, + _admin_cap, + _maintainer_cap, + _btc_pool_id, + usdc_pool_id, + _pool_id, + registry_id, + ) = setup_btc_usd_deepbook_margin(); + + // Supply liquidity to USDC pool so we can borrow + scenario.next_tx(test_constants::admin()); + let mut usdc_pool = scenario.take_shared_by_id>(usdc_pool_id); + let registry = scenario.take_shared(); + let supplier_cap = margin_pool::mint_supplier_cap(®istry, &clock, scenario.ctx()); + usdc_pool.supply( + ®istry, + &supplier_cap, + mint_coin(1_000_000 * test_constants::usdc_multiplier(), scenario.ctx()), + option::none(), + &clock, + ); + return_shared_2!(usdc_pool, registry); + destroy(supplier_cap); + + // Create margin manager + scenario.next_tx(test_constants::user1()); + let pool = scenario.take_shared>(); + let mut registry = scenario.take_shared(); + let deepbook_registry = scenario.take_shared_by_id(registry_id); + margin_manager::new( + &pool, + &deepbook_registry, + &mut registry, + &clock, + scenario.ctx(), + ); + return_shared(deepbook_registry); + return_shared_2!(pool, registry); + + // Deposit collateral (BTC) + scenario.next_tx(test_constants::user1()); + let mut mm = scenario.take_shared>(); + let registry = scenario.take_shared(); + let btc_price = build_btc_price_info_object(&mut scenario, 50000, &clock); + let usdc_price = build_demo_usdc_price_info_object(&mut scenario, &clock); + mm.deposit( + ®istry, + &btc_price, + &usdc_price, + mint_coin(10 * btc_multiplier(), scenario.ctx()), + &clock, + scenario.ctx(), + ); + destroy_2!(btc_price, usdc_price); + return_shared_2!(mm, registry); + + // Borrow USDC (quote asset) to create outstanding debt + scenario.next_tx(test_constants::user1()); + let mut mm = scenario.take_shared>(); + let registry = scenario.take_shared(); + let mut usdc_pool = scenario.take_shared_by_id>(usdc_pool_id); + let pool = scenario.take_shared>(); + let btc_price = build_btc_price_info_object(&mut scenario, 50000, &clock); + let usdc_price = build_demo_usdc_price_info_object(&mut scenario, &clock); + + mm.borrow_quote( + ®istry, + &mut usdc_pool, + &btc_price, + &usdc_price, + &pool, + 10_000 * test_constants::usdc_multiplier(), + &clock, + scenario.ctx(), + ); + + assert!(mm.borrowed_quote_shares() > 0); + + destroy_2!(btc_price, usdc_price); + return_shared_3!(usdc_pool, pool, registry); + return_shared(mm); + + // Try to unregister with outstanding quote debt - should fail + scenario.next_tx(test_constants::user1()); + let mut mm = scenario.take_shared>(); + let mut registry = scenario.take_shared(); + + margin_manager::unregister_margin_manager( + &mut mm, + &mut registry, + scenario.ctx(), + ); + + // Cleanup (unreachable due to expected failure above) + abort 0 +} diff --git a/packages/deepbook_margin/tests/margin_pool/margin_state_tests.move b/packages/deepbook_margin/tests/margin_pool/margin_state_tests.move new file mode 100644 index 000000000..0eaeccdcb --- /dev/null +++ b/packages/deepbook_margin/tests/margin_pool/margin_state_tests.move @@ -0,0 +1,165 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +#[test_only] +module deepbook_margin::margin_state_tests; + +use deepbook::{constants, math}; +use deepbook_margin::{margin_constants, margin_state, protocol_config_tests, test_constants}; +use std::unit_test::{assert_eq, destroy}; +use sui::{clock, test_scenario::begin}; + +#[test] +fun margin_state_operations_work() { + let mut test = begin(test_constants::admin()); + let mut clock = clock::create_for_testing(test.ctx()); + let mut state = margin_state::default(&clock); + assert_eq!(state.total_supply(), 0); + assert_eq!(state.total_borrow(), 0); + assert_eq!(state.supply_shares(), 0); + assert_eq!(state.borrow_shares(), 0); + assert_eq!(state.last_update_timestamp(), clock.timestamp_ms()); + + let config = protocol_config_tests::create_test_protocol_config(); + + clock.increment_for_testing(1000); + state.increase_supply(&config, 1000 * constants::float_scaling(), &clock); + assert_eq!(state.total_supply(), 1000 * constants::float_scaling()); + assert_eq!(state.supply_shares(), 1000 * constants::float_scaling()); + assert_eq!(state.total_borrow(), 0); + assert_eq!(state.borrow_shares(), 0); + assert_eq!(state.last_update_timestamp(), clock.timestamp_ms()); + + clock.increment_for_testing(1000); + state.increase_supply(&config, 1000 * constants::float_scaling(), &clock); + assert_eq!(state.total_supply(), 2000 * constants::float_scaling()); + assert_eq!(state.supply_shares(), 2000 * constants::float_scaling()); + assert_eq!(state.total_borrow(), 0); + assert_eq!(state.borrow_shares(), 0); + assert_eq!(state.last_update_timestamp(), clock.timestamp_ms()); + + state.increase_supply_absolute(1000 * constants::float_scaling()); + assert_eq!(state.total_supply(), 3000 * constants::float_scaling()); + assert_eq!(state.supply_shares(), 2000 * constants::float_scaling()); + assert_eq!(state.total_borrow(), 0); + assert_eq!(state.borrow_shares(), 0); + + clock.increment_for_testing(1000); + let (withdraw_amount, protocol_fees) = state.decrease_supply_shares( + &config, + 1000 * constants::float_scaling(), + &clock, + ); + assert_eq!(withdraw_amount, 1500 * constants::float_scaling()); + assert_eq!(protocol_fees, 0); + assert_eq!(state.total_supply(), 1500 * constants::float_scaling()); + assert_eq!(state.supply_shares(), 1000 * constants::float_scaling()); + assert_eq!(state.total_borrow(), 0); + assert_eq!(state.borrow_shares(), 0); + assert_eq!(state.last_update_timestamp(), clock.timestamp_ms()); + + state.decrease_supply_absolute(1000 * constants::float_scaling()); + assert_eq!(state.total_supply(), 500 * constants::float_scaling()); + assert_eq!(state.supply_shares(), 1000 * constants::float_scaling()); + assert_eq!(state.total_borrow(), 0); + assert_eq!(state.borrow_shares(), 0); + + clock.increment_for_testing(1000); + let (withdraw_amount, protocol_fees) = state.decrease_supply_shares( + &config, + 1000 * constants::float_scaling(), + &clock, + ); + assert_eq!(withdraw_amount, 500 * constants::float_scaling()); + assert_eq!(protocol_fees, 0); + assert_eq!(state.total_supply(), 0); + assert_eq!(state.supply_shares(), 0); + assert_eq!(state.total_borrow(), 0); + assert_eq!(state.borrow_shares(), 0); + assert_eq!(state.last_update_timestamp(), clock.timestamp_ms()); + + destroy(clock); + test.end(); +} + +#[test] +fun margin_state_with_supply_and_borrow_accrues_interest() { + let mut test = begin(test_constants::admin()); + let mut clock = clock::create_for_testing(test.ctx()); + let mut state = margin_state::default(&clock); + + let config = protocol_config_tests::create_test_protocol_config(); + + clock.increment_for_testing(1000); + state.increase_supply(&config, 1000 * constants::float_scaling(), &clock); + assert_eq!(state.total_supply(), 1000 * constants::float_scaling()); + assert_eq!(state.supply_shares(), 1000 * constants::float_scaling()); + assert_eq!(state.total_borrow(), 0); + assert_eq!(state.borrow_shares(), 0); + assert_eq!(state.last_update_timestamp(), clock.timestamp_ms()); + + clock.increment_for_testing(1000); + state.increase_borrow(&config, 500 * constants::float_scaling(), &clock); + assert_eq!(state.total_supply(), 1000 * constants::float_scaling()); + assert_eq!(state.supply_shares(), 1000 * constants::float_scaling()); + assert_eq!(state.total_borrow(), 500 * constants::float_scaling()); + assert_eq!(state.borrow_shares(), 500 * constants::float_scaling()); + assert_eq!(state.last_update_timestamp(), clock.timestamp_ms()); + + // so far 1000 supplied, 500 borrowed. + // incremeent time by 30 days + let elapsed = 30 * 24 * 60 * 60 * 1000; + clock.increment_for_testing(elapsed); + let interest_rate = config.interest_rate(constants::half()); + assert_eq!(state.utilization_rate(), constants::half()); + assert_eq!(interest_rate, 100_000_000); // 10% when 50% utilization + + // 10% interest for 30 days = 500 * 0.1 * 30 / 365 = 4.1095890411 + let interest = math::mul( + math::mul(interest_rate, 500 * constants::float_scaling()), + math::div(elapsed, margin_constants::year_ms()), + ); + let protocol_fees = math::mul(interest, config.protocol_spread()); + assert_eq!(interest, 4_109_589_000); + assert_eq!(protocol_fees, 410_958_900); + + let supply_ratio = math::div( + 1000 * constants::float_scaling() + interest - protocol_fees, + 1000 * constants::float_scaling(), + ); + let borrow_ratio = math::div( + 500 * constants::float_scaling() + interest, + 500 * constants::float_scaling(), + ); + let calc_supply_amount = math::mul(state.supply_shares(), supply_ratio); + let calc_borrow_amount = math::mul(state.borrow_shares(), borrow_ratio); + let supply_amount = state.supply_shares_to_amount(state.supply_shares(), &config, &clock); + let borrow_amount = state.borrow_shares_to_amount(state.borrow_shares(), &config, &clock); + assert_eq!(supply_amount, calc_supply_amount); + assert_eq!(borrow_amount, calc_borrow_amount); + + let (withdraw_borrow_amount, withdraw_protocol_fees) = state.decrease_borrow_shares( + &config, + 500 * constants::float_scaling(), + &clock, + ); + assert_eq!(withdraw_borrow_amount, calc_borrow_amount); + assert_eq!(withdraw_protocol_fees, protocol_fees); + let (withdraw_supply_amount, withdraw_protocol_fees) = state.decrease_supply_shares( + &config, + 1000 * constants::float_scaling(), + &clock, + ); + assert_eq!(withdraw_supply_amount, calc_supply_amount); + assert_eq!(withdraw_protocol_fees, 0); + + // rounding leaves 100 in supply + assert_eq!(state.total_supply(), 100); + assert_eq!(state.total_borrow(), 0); + assert_eq!(state.supply_shares(), 0); + assert_eq!(state.borrow_shares(), 0); + assert_eq!(state.last_update_timestamp(), clock.timestamp_ms()); + + destroy(clock); + test.end(); +} diff --git a/packages/deepbook_margin/tests/margin_pool/protocol_config_tests.move b/packages/deepbook_margin/tests/margin_pool/protocol_config_tests.move new file mode 100644 index 000000000..f533ab3ea --- /dev/null +++ b/packages/deepbook_margin/tests/margin_pool/protocol_config_tests.move @@ -0,0 +1,623 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +#[test_only] +module deepbook_margin::protocol_config_tests; + +use deepbook::math; +use deepbook_margin::{ + margin_constants, + protocol_config::{Self, ProtocolConfig, MarginPoolConfig, InterestConfig}, + test_constants +}; +use std::unit_test::{assert_eq, destroy}; + +/// Create a test protocol config with default values +public fun create_test_protocol_config(): ProtocolConfig { + let margin_pool_config = protocol_config::new_margin_pool_config( + test_constants::supply_cap(), + test_constants::max_utilization_rate(), // 80% + test_constants::protocol_spread(), // 10% + test_constants::min_borrow(), + ); + let interest_config = protocol_config::new_interest_config( + test_constants::base_rate(), // 5% + test_constants::base_slope(), // 10% + test_constants::optimal_utilization(), // 80% + test_constants::excess_slope(), // 200% + ); + protocol_config::new_protocol_config(margin_pool_config, interest_config) +} + +/// Helper function to create custom interest config +fun create_custom_interest_config( + base_rate: u64, + base_slope: u64, + optimal_utilization: u64, + excess_slope: u64, +): InterestConfig { + protocol_config::new_interest_config(base_rate, base_slope, optimal_utilization, excess_slope) +} + +/// Helper function to create custom margin pool config +fun create_custom_margin_pool_config( + supply_cap: u64, + max_utilization_rate: u64, + protocol_spread: u64, + min_borrow: u64, +): MarginPoolConfig { + protocol_config::new_margin_pool_config( + supply_cap, + max_utilization_rate, + protocol_spread, + min_borrow, + ) +} + +// ===== Interest Rate Tests ===== + +#[test] +/// Test interest rate calculation when utilization is below optimal +fun test_interest_rate_below_optimal() { + let config = create_test_protocol_config(); + + // Test at 0% utilization - should be base rate only + let rate_at_0 = config.interest_rate(0); + assert_eq!(rate_at_0, test_constants::base_rate()); // 5% + + // Test at 40% utilization (half of optimal 80%) + // Formula: base_rate + (utilization * base_slope) + // 5% + (40% * 10%) = 5% + 4% = 9% + let rate_at_40 = config.interest_rate(400_000_000); + let expected_40 = + test_constants::base_rate() + math::mul(400_000_000, test_constants::base_slope()); + assert_eq!(rate_at_40, expected_40); + assert_eq!(rate_at_40, 90_000_000); // 9% + + // Test at 60% utilization (still below optimal) + let rate_at_60 = config.interest_rate(600_000_000); + let expected_60 = + test_constants::base_rate() + math::mul(600_000_000, test_constants::base_slope()); + assert_eq!(rate_at_60, expected_60); + assert_eq!(rate_at_60, 110_000_000); // 11% + + destroy(config); +} + +#[test] +/// Test interest rate calculation exactly at optimal utilization +fun test_interest_rate_at_optimal() { + let config = create_test_protocol_config(); + + // At 80% utilization (optimal) + // Formula: base_rate + (optimal_utilization * base_slope) + // 5% + (80% * 10%) = 5% + 8% = 13% + let rate_at_optimal = config.interest_rate(test_constants::optimal_utilization()); + let expected = + test_constants::base_rate() + + math::mul(test_constants::optimal_utilization(), test_constants::base_slope()); + assert_eq!(rate_at_optimal, expected); + assert_eq!(rate_at_optimal, 130_000_000); // 13% + + destroy(config); +} + +#[test] +/// Test interest rate calculation when utilization is above optimal +fun test_interest_rate_above_optimal() { + let config = create_test_protocol_config(); + + // Test at 90% utilization (10% above optimal) + // Formula: base_rate + (optimal_utilization * base_slope) + ((utilization - optimal) * excess_slope) + // 5% + (80% * 10%) + (10% * 200%) = 5% + 8% + 20% = 33% + let rate_at_90 = config.interest_rate(900_000_000); + let base_plus_optimal = + test_constants::base_rate() + + math::mul(test_constants::optimal_utilization(), test_constants::base_slope()); + let excess = math::mul(100_000_000, test_constants::excess_slope()); // 10% * 200% + let expected_90 = base_plus_optimal + excess; + assert_eq!(rate_at_90, expected_90); + assert_eq!(rate_at_90, 330_000_000); // 33% + + // Test at 100% utilization + // 5% + (80% * 10%) + (20% * 200%) = 5% + 8% + 40% = 53% + let rate_at_100 = config.interest_rate(1_000_000_000); + let excess_100 = math::mul(200_000_000, test_constants::excess_slope()); // 20% * 200% + let expected_100 = base_plus_optimal + excess_100; + assert_eq!(rate_at_100, expected_100); + assert_eq!(rate_at_100, 530_000_000); // 53% + + destroy(config); +} + +#[test] +/// Test edge case with zero base rate +fun test_interest_rate_zero_base_rate() { + let margin_pool_config = create_custom_margin_pool_config( + test_constants::supply_cap(), + test_constants::max_utilization_rate(), + test_constants::protocol_spread(), + test_constants::min_borrow(), + ); + let interest_config = create_custom_interest_config( + 0, // Zero base rate + 100_000_000, // 10% base slope + 800_000_000, // 80% optimal + 2_000_000_000, // 200% excess slope + ); + let config = protocol_config::new_protocol_config(margin_pool_config, interest_config); + + // At 0% utilization - should be 0 + assert_eq!(config.interest_rate(0), 0); + + // At 50% utilization - should be 50% * 10% = 5% + assert_eq!(config.interest_rate(500_000_000), 50_000_000); + + // At 90% utilization - 80% * 10% + 10% * 200% = 8% + 20% = 28% + assert_eq!(config.interest_rate(900_000_000), 280_000_000); + + destroy(config); +} + +#[test] +/// Test interest rate with different slopes +fun test_interest_rate_different_slopes() { + let margin_pool_config = create_custom_margin_pool_config( + test_constants::supply_cap(), + test_constants::max_utilization_rate(), + test_constants::protocol_spread(), + test_constants::min_borrow(), + ); + let interest_config = create_custom_interest_config( + 20_000_000, // 2% base rate + 50_000_000, // 5% base slope + 600_000_000, // 60% optimal + 3_000_000_000, // 300% excess slope + ); + let config = protocol_config::new_protocol_config(margin_pool_config, interest_config); + + // At 30% utilization - 2% + (30% * 5%) = 2% + 1.5% = 3.5% + assert_eq!(config.interest_rate(300_000_000), 35_000_000); + + // At 60% utilization (optimal) - 2% + (60% * 5%) = 2% + 3% = 5% + assert_eq!(config.interest_rate(600_000_000), 50_000_000); + + // At 80% utilization - 2% + (60% * 5%) + (20% * 300%) = 2% + 3% + 60% = 65% + assert_eq!(config.interest_rate(800_000_000), 650_000_000); + + destroy(config); +} + +#[test] +/// Test calculate_interest_with_borrow precision with small time intervals +fun test_calculate_interest_with_borrow_precision() { + let config = create_test_protocol_config(); + let year_ms = margin_constants::year_ms(); + let second_ms = 1000; // 1 second + let minute_ms = 60 * 1000; // 1 minute + let total_borrow = 1_000_000_000_000; // 1M tokens with 6 decimals + + // Test with high utilization for very short time periods + let utilization = 950_000_000; // 95% utilization + let interest_rate = config.interest_rate(utilization); + + // Test for 1 second + let interest_1_second = config.calculate_interest_with_borrow( + utilization, + second_ms, + total_borrow, + ); + let expected_1_second = math::mul( + math::mul(total_borrow, interest_rate), + math::div(second_ms, year_ms), + ); + assert_eq!(interest_1_second, expected_1_second); + + // Test for 1 minute + let interest_1_minute = config.calculate_interest_with_borrow( + utilization, + minute_ms, + total_borrow, + ); + let expected_1_minute = math::mul( + math::mul(total_borrow, interest_rate), + math::div(minute_ms, year_ms), + ); + assert_eq!(interest_1_minute, expected_1_minute); + + // Verify that 60 seconds equals 1 minute + let interest_60_seconds = config.calculate_interest_with_borrow( + utilization, + 60 * second_ms, + total_borrow, + ); + assert_eq!(interest_60_seconds, interest_1_minute); + + destroy(config); +} + +#[test] +/// Test calculate_interest_with_borrow with exact mathematical equivalence +fun test_calculate_interest_with_borrow_exact() { + let config = create_test_protocol_config(); + let utilization = 500_000_000; // 50% utilization + let time_elapsed = 3600000; // 1 hour in ms + let total_borrow = 1_000_000_000_000; // 1M tokens with 6 decimals + let interest_rate = config.interest_rate(utilization); + + let interest_result = config.calculate_interest_with_borrow( + utilization, + time_elapsed, + total_borrow, + ); + + // Expected calculation: (total_borrow * interest_rate) * (time_elapsed / year_ms) + let expected = math::mul( + math::mul(total_borrow, interest_rate), + math::div(time_elapsed, margin_constants::year_ms()), + ); + + assert_eq!(interest_result, expected); + + destroy(config); +} + +#[test] +/// Test calculate_interest_with_borrow with various time periods +fun test_calculate_interest_with_borrow_time_periods() { + let config = create_test_protocol_config(); + let utilization = 200_000_000; // 20% utilization + let total_borrow = 500_000_000_000; // 500K tokens + let interest_rate = config.interest_rate(utilization); + + // Test different time periods + let hour_ms = 3600000; // 1 hour + let day_ms = 86400000; // 1 day + let week_ms = 604800000; // 1 week + + // Test 1 hour + let interest_hour = config.calculate_interest_with_borrow(utilization, hour_ms, total_borrow); + let expected_hour = math::mul( + math::mul(total_borrow, interest_rate), + math::div(hour_ms, margin_constants::year_ms()), + ); + assert_eq!(interest_hour, expected_hour); + + // Test 1 day + let interest_day = config.calculate_interest_with_borrow(utilization, day_ms, total_borrow); + let expected_day = math::mul( + math::mul(total_borrow, interest_rate), + math::div(day_ms, margin_constants::year_ms()), + ); + assert_eq!(interest_day, expected_day); + + // Test 1 week + let interest_week = config.calculate_interest_with_borrow(utilization, week_ms, total_borrow); + let expected_week = math::mul( + math::mul(total_borrow, interest_rate), + math::div(week_ms, margin_constants::year_ms()), + ); + assert_eq!(interest_week, expected_week); + + // Verify proportional scaling: 24 hours should equal 1 day + let interest_24_hours = config.calculate_interest_with_borrow( + utilization, + 24 * hour_ms, + total_borrow, + ); + assert_eq!(interest_24_hours, interest_day); + + destroy(config); +} + +// ===== Getter Function Tests ===== + +#[test] +/// Test all getter functions return correct values +fun test_protocol_config_getters() { + let config = create_test_protocol_config(); + + // Test margin pool config getters + assert_eq!(config.supply_cap(), test_constants::supply_cap()); + assert_eq!(config.max_utilization_rate(), test_constants::max_utilization_rate()); + assert_eq!(config.protocol_spread(), test_constants::protocol_spread()); + assert_eq!(config.min_borrow(), test_constants::min_borrow()); + + // Test interest config getters + assert_eq!(config.base_rate(), test_constants::base_rate()); + assert_eq!(config.base_slope(), test_constants::base_slope()); + assert_eq!(config.optimal_utilization(), test_constants::optimal_utilization()); + assert_eq!(config.excess_slope(), test_constants::excess_slope()); + + destroy(config); +} + +// ===== Setter Function Tests ===== + +#[test, expected_failure(abort_code = protocol_config::EInvalidRiskParam)] +/// Test that setting interest config with optimal > max utilization fails +fun test_set_interest_config_invalid_optimal() { + let mut config = create_test_protocol_config(); + + // Try to set optimal utilization higher than max utilization (80%) + let invalid_interest_config = create_custom_interest_config( + 50_000_000, + 100_000_000, + 900_000_000, // 90% optimal > 80% max utilization + 2_000_000_000, + ); + + config.set_interest_config(invalid_interest_config); + destroy(config); +} + +#[test, expected_failure(abort_code = protocol_config::EInvalidRiskParam)] +/// Test that setting invalid protocol spread fails +fun test_set_margin_pool_config_invalid_spread() { + let mut config = create_test_protocol_config(); + + // Try to set protocol spread > 100% + let invalid_config = create_custom_margin_pool_config( + test_constants::supply_cap(), + test_constants::max_utilization_rate(), + 1_100_000_000, // 110% > 100% + test_constants::min_borrow(), + ); + + config.set_margin_pool_config(invalid_config); + destroy(config); +} + +#[test, expected_failure(abort_code = protocol_config::EInvalidRiskParam)] +/// Test that setting max utilization > 100% fails +fun test_set_margin_pool_config_invalid_utilization() { + let mut config = create_test_protocol_config(); + + let invalid_config = create_custom_margin_pool_config( + test_constants::supply_cap(), + 1_100_000_000, // 110% + test_constants::protocol_spread(), + test_constants::min_borrow(), + ); + + config.set_margin_pool_config(invalid_config); + destroy(config); +} + +#[test, expected_failure(abort_code = protocol_config::EInvalidRiskParam)] +/// Test that setting max utilization < optimal utilization fails +fun test_set_margin_pool_config_utilization_mismatch() { + let mut config = create_test_protocol_config(); + + // Current optimal utilization is 80%, try to set max to 70% + let invalid_config = create_custom_margin_pool_config( + test_constants::supply_cap(), + 700_000_000, // 70% < 80% optimal + test_constants::protocol_spread(), + test_constants::min_borrow(), + ); + + config.set_margin_pool_config(invalid_config); + destroy(config); +} + +#[test, expected_failure(abort_code = protocol_config::EInvalidRiskParam)] +/// Test that setting min_borrow below minimum fails +fun test_set_margin_pool_config_invalid_min_borrow() { + let mut config = create_test_protocol_config(); + + // Try to set min_borrow below the minimum allowed + let invalid_config = create_custom_margin_pool_config( + test_constants::supply_cap(), + test_constants::max_utilization_rate(), + test_constants::protocol_spread(), + 100, // Below MIN_MIN_BORROW (1000) + ); + + config.set_margin_pool_config(invalid_config); + destroy(config); +} + +#[test, expected_failure(abort_code = protocol_config::EInvalidRiskParam)] +/// Test sequential config updates +fun test_sequential_config_updates_violating_constraints() { + let mut config = create_test_protocol_config(); + + // First set optimal utilization to 75% + let interest_config = create_custom_interest_config( + 50_000_000, + 100_000_000, + 750_000_000, // 75% optimal + 2_000_000_000, + ); + config.set_interest_config(interest_config); + + // Now try to set max utilization to 70% (less than optimal) + let invalid_margin_config = create_custom_margin_pool_config( + test_constants::supply_cap(), + 700_000_000, // 70% < 75% optimal + test_constants::protocol_spread(), + test_constants::min_borrow(), + ); + + config.set_margin_pool_config(invalid_margin_config); + destroy(config); +} + +#[test, expected_failure(abort_code = protocol_config::EInvalidRiskParam)] +/// Test that protocol spread maximum +fun test_set_margin_pool_config_spread() { + let mut config = create_test_protocol_config(); + + // Try to set protocol spread to just over 100% + let invalid_config = create_custom_margin_pool_config( + test_constants::supply_cap(), + test_constants::max_utilization_rate(), + 1_000_000_001, // > 100% + test_constants::min_borrow(), + ); + + config.set_margin_pool_config(invalid_config); + destroy(config); +} + +// ===== Constructor Validation Tests ===== + +#[test, expected_failure(abort_code = protocol_config::EInvalidRiskParam)] +/// Test that creating margin pool config with protocol_spread > 100% fails +fun test_new_margin_pool_config_invalid_protocol_spread_too_high() { + let _config = protocol_config::new_margin_pool_config( + test_constants::supply_cap(), + test_constants::max_utilization_rate(), + 1_100_000_000, // 110% > 100% + test_constants::min_borrow(), + ); + abort 0 +} + +#[test, expected_failure(abort_code = protocol_config::EInvalidRiskParam)] +/// Test that creating margin pool config with max_utilization_rate > 100% fails +fun test_new_margin_pool_config_invalid_max_utilization_rate() { + let _config = protocol_config::new_margin_pool_config( + test_constants::supply_cap(), + 1_100_000_000, // 110% > 100% + test_constants::protocol_spread(), + test_constants::min_borrow(), + ); + abort 0 +} + +#[test, expected_failure(abort_code = protocol_config::EInvalidRiskParam)] +/// Test that creating margin pool config with min_borrow below minimum fails +fun test_new_margin_pool_config_invalid_min_borrow() { + let _config = protocol_config::new_margin_pool_config( + test_constants::supply_cap(), + test_constants::max_utilization_rate(), + test_constants::protocol_spread(), + 999, // < MIN_MIN_BORROW (1000) + ); + abort 0 +} + +#[test, expected_failure(abort_code = protocol_config::EInvalidRiskParam)] +/// Test that creating margin pool config with protocol_spread > max fails +fun test_new_margin_pool_config_invalid_protocol_spread_above_max() { + let _config = protocol_config::new_margin_pool_config( + test_constants::supply_cap(), + test_constants::max_utilization_rate(), + 201_000_000, // 20.1% > MAX_PROTOCOL_SPREAD (20%) + test_constants::min_borrow(), + ); + abort 0 +} + +#[test, expected_failure(abort_code = protocol_config::EInvalidRiskParam)] +/// Test that creating interest config with optimal_utilization > 100% fails +fun test_new_interest_config_invalid_optimal_utilization() { + let _config = protocol_config::new_interest_config( + test_constants::base_rate(), + test_constants::base_slope(), + 1_100_000_000, // 110% > 100% + test_constants::excess_slope(), + ); + abort 0 +} + +#[test, expected_failure(abort_code = protocol_config::EInvalidRiskParam)] +/// Test that creating protocol config with max_utilization < optimal_utilization fails +fun test_new_protocol_config_invalid_utilization_mismatch() { + let margin_pool_config = protocol_config::new_margin_pool_config( + test_constants::supply_cap(), + 700_000_000, // 70% max utilization + test_constants::protocol_spread(), + test_constants::min_borrow(), + ); + let interest_config = protocol_config::new_interest_config( + test_constants::base_rate(), + test_constants::base_slope(), + 800_000_000, // 80% optimal > 70% max + test_constants::excess_slope(), + ); + let _config = protocol_config::new_protocol_config(margin_pool_config, interest_config); + abort 0 +} + +#[test] +/// Test that creating valid configs succeeds +fun test_new_configs_valid() { + // Create valid margin pool config + let margin_pool_config = protocol_config::new_margin_pool_config( + test_constants::supply_cap(), + test_constants::max_utilization_rate(), + test_constants::protocol_spread(), + test_constants::min_borrow(), + ); + + // Create valid interest config + let interest_config = protocol_config::new_interest_config( + test_constants::base_rate(), + test_constants::base_slope(), + test_constants::optimal_utilization(), + test_constants::excess_slope(), + ); + + // Create valid protocol config + let config = protocol_config::new_protocol_config(margin_pool_config, interest_config); + + // Verify values + assert_eq!(config.supply_cap(), test_constants::supply_cap()); + assert_eq!(config.max_utilization_rate(), test_constants::max_utilization_rate()); + assert_eq!(config.protocol_spread(), test_constants::protocol_spread()); + assert_eq!(config.min_borrow(), test_constants::min_borrow()); + assert_eq!(config.base_rate(), test_constants::base_rate()); + assert_eq!(config.base_slope(), test_constants::base_slope()); + assert_eq!(config.optimal_utilization(), test_constants::optimal_utilization()); + assert_eq!(config.excess_slope(), test_constants::excess_slope()); + + destroy(config); +} + +#[test] +/// Test edge case: max_utilization exactly equals optimal_utilization (valid) +fun test_new_protocol_config_max_equals_optimal() { + let margin_pool_config = protocol_config::new_margin_pool_config( + test_constants::supply_cap(), + 800_000_000, // 80% + test_constants::protocol_spread(), + test_constants::min_borrow(), + ); + let interest_config = protocol_config::new_interest_config( + test_constants::base_rate(), + test_constants::base_slope(), + 800_000_000, // 80% - exactly equal + test_constants::excess_slope(), + ); + let config = protocol_config::new_protocol_config(margin_pool_config, interest_config); + destroy(config); +} + +#[test] +/// Test edge case: protocol_spread at exactly max allowed (valid) +fun test_new_margin_pool_config_spread_at_max() { + let config = protocol_config::new_margin_pool_config( + test_constants::supply_cap(), + test_constants::max_utilization_rate(), + 200_000_000, // Exactly MAX_PROTOCOL_SPREAD (20%) + test_constants::min_borrow(), + ); + // Should succeed + destroy(config); +} + +#[test] +/// Test edge case: min_borrow at exactly minimum allowed (valid) +fun test_new_margin_pool_config_min_borrow_at_min() { + let config = protocol_config::new_margin_pool_config( + test_constants::supply_cap(), + test_constants::max_utilization_rate(), + test_constants::protocol_spread(), + 1000, // Exactly MIN_MIN_BORROW + ); + // Should succeed + destroy(config); +} diff --git a/packages/deepbook_margin/tests/margin_pool/protocol_fees_tests.move b/packages/deepbook_margin/tests/margin_pool/protocol_fees_tests.move new file mode 100644 index 000000000..e162dd9fb --- /dev/null +++ b/packages/deepbook_margin/tests/margin_pool/protocol_fees_tests.move @@ -0,0 +1,393 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +#[test_only] +module deepbook_margin::protocol_fees_tests; + +use deepbook::constants; +use deepbook_margin::{protocol_fees::{Self, SupplyReferral}, test_constants, test_helpers}; +use std::unit_test::{assert_eq, destroy}; +use sui::test_scenario::return_shared; + +#[test] +fun test_referral_fees_setup() { + let (mut test, admin_cap) = test_helpers::setup_test(); + + // 100 shares increased, 1 reward earned + test.next_tx(test_constants::admin()); + let mut protocol_fees = protocol_fees::default_protocol_fees(test.ctx()); + protocol_fees.increase_shares(option::none(), 100 * constants::float_scaling()); + protocol_fees.increase_fees_accrued( + test_constants::test_margin_pool_id(), + 2 * constants::float_scaling(), + ); + assert_eq!(protocol_fees.total_shares(), 100 * constants::float_scaling()); + assert_eq!(protocol_fees.fees_per_share(), 10_000_000); + + protocol_fees.increase_shares(option::none(), 100 * constants::float_scaling()); + protocol_fees.increase_fees_accrued( + test_constants::test_margin_pool_id(), + 4 * constants::float_scaling(), + ); + assert_eq!(protocol_fees.total_shares(), 200 * constants::float_scaling()); + assert_eq!(protocol_fees.fees_per_share(), 20_000_000); + + // so far we have 200 shares and 0.02 rewards per share + // increase by 1000 and add 5 more rewards. 5 rewards distributed over 1200 total shares + protocol_fees.increase_shares(option::none(), 1000 * constants::float_scaling()); + protocol_fees.increase_fees_accrued( + test_constants::test_margin_pool_id(), + 10 * constants::float_scaling(), + ); + assert_eq!(protocol_fees.total_shares(), 1200 * constants::float_scaling()); + assert_eq!(protocol_fees.fees_per_share(), 24_166_666); + + // decrease shares by 1100, add 10 rewards + protocol_fees.decrease_shares(option::none(), 1100 * constants::float_scaling()); + protocol_fees.increase_fees_accrued( + test_constants::test_margin_pool_id(), + 20 * constants::float_scaling(), + ); + assert_eq!(protocol_fees.total_shares(), 100 * constants::float_scaling()); + assert_eq!(protocol_fees.fees_per_share(), 124_166_666); + + destroy(admin_cap); + destroy(protocol_fees); + test.end(); +} + +#[test] +fun test_referral_fees_ok() { + let (mut test, admin_cap) = test_helpers::setup_test(); + + test.next_tx(test_constants::admin()); + let mut protocol_fees = protocol_fees::default_protocol_fees(test.ctx()); + + let referral_id; + test.next_tx(test_constants::user1()); + { + referral_id = protocol_fees.mint_supply_referral(test.ctx()); + }; + + test.next_tx(test_constants::user2()); + { + protocol_fees.increase_shares( + option::some(referral_id), + 100 * constants::float_scaling(), + ); + protocol_fees.increase_fees_accrued( + test_constants::test_margin_pool_id(), + 200 * constants::float_scaling(), + ); + let (current_shares, unclaimed_fees) = protocol_fees.referral_tracker(referral_id); + assert_eq!(current_shares, 100 * constants::float_scaling()); + assert_eq!(unclaimed_fees, 100 * constants::float_scaling()); + assert_eq!(protocol_fees.fees_per_share(), 1_000_000_000); + }; + + test.next_tx(test_constants::user1()); + { + // claim fees + let referral = test.take_shared_by_id(referral_id); + let fees = protocol_fees.calculate_and_claim(&referral, test.ctx()); + assert_eq!(fees, 100 * constants::float_scaling()); + let (current_shares, unclaimed_fees) = protocol_fees.referral_tracker(referral_id); + assert_eq!(current_shares, 100 * constants::float_scaling()); + assert_eq!(unclaimed_fees, 0); + + // claim fees again + let fees = protocol_fees.calculate_and_claim(&referral, test.ctx()); + assert_eq!(fees, 0); + let (current_shares, unclaimed_fees) = protocol_fees.referral_tracker(referral_id); + assert_eq!(current_shares, 100 * constants::float_scaling()); + assert_eq!(unclaimed_fees, 0); + + return_shared(referral); + }; + + test.next_tx(test_constants::user2()); + { + // user2 adds more shares + protocol_fees.increase_shares( + option::some(referral_id), + 100 * constants::float_scaling(), + ); + protocol_fees.increase_fees_accrued( + test_constants::test_margin_pool_id(), + 200 * constants::float_scaling(), + ); + let (current_shares, unclaimed_fees) = protocol_fees.referral_tracker(referral_id); + assert_eq!(current_shares, 200 * constants::float_scaling()); + assert_eq!(unclaimed_fees, 100 * constants::float_scaling()); + }; + + test.next_tx(test_constants::user1()); + { + // user1 claims fees. current_shares is 200, last_fees_per_share is 1_000_000_000, fees_per_share is now 1_500_000_000 + // they get 200 shares * (1_500_000_000 - 1_000_000_000) = 200 * 500_000_000 = 100_000_000_000 + assert_eq!(protocol_fees.fees_per_share(), 1_500_000_000); + let referral = test.take_shared_by_id(referral_id); + let fees = protocol_fees.calculate_and_claim(&referral, test.ctx()); + assert_eq!(fees, 100_000_000_000); + let (current_shares, unclaimed_fees) = protocol_fees.referral_tracker(referral_id); + assert_eq!(current_shares, 200 * constants::float_scaling()); + assert_eq!(unclaimed_fees, 0); + + // if we try to claim again, it should be 0 + let fees = protocol_fees.calculate_and_claim(&referral, test.ctx()); + assert_eq!(fees, 0); + let (current_shares, unclaimed_fees) = protocol_fees.referral_tracker(referral_id); + assert_eq!(current_shares, 200 * constants::float_scaling()); + assert_eq!(unclaimed_fees, 0); + + return_shared(referral); + }; + + // increase shares, accrue fees, decrease shares before the claim. + // referrer should only have 200 shares exposed to fees. + test.next_tx(test_constants::user1()); + { + protocol_fees.increase_shares( + option::some(referral_id), + 100 * constants::float_scaling(), + ); + protocol_fees.increase_fees_accrued( + test_constants::test_margin_pool_id(), + 200 * constants::float_scaling(), + ); + protocol_fees.decrease_shares( + option::some(referral_id), + 100 * constants::float_scaling(), + ); + + // additional 100 rewards for 300 shares + assert_eq!(protocol_fees.fees_per_share(), 1_833_333_333); + }; + + test.next_tx(test_constants::user1()); + { + // fees_per_share went from 1.5 -> 1.833 since last claim. 200 shares exposed. 200 * (1.833 - 1.5) = 200 * 0.333 = 66.6 + // while current_shares was 300, fees_per_share went from 1.5 -> 1.833, so 300 shares * (1.833 - 1.5) = 300 * 0.333 = 99.9 + let referral = test.take_shared_by_id(referral_id); + let fees = protocol_fees.calculate_and_claim(&referral, test.ctx()); + assert_eq!(fees, 99_999_999_900); + let (current_shares, unclaimed_fees) = protocol_fees.referral_tracker(referral_id); + assert_eq!(current_shares, 200 * constants::float_scaling()); + assert_eq!(unclaimed_fees, 0); + + return_shared(referral); + }; + + // decrease referred shares to 0, then increase by 1000. Add 1000 rewards. + test.next_tx(test_constants::user1()); + { + protocol_fees.decrease_shares( + option::some(referral_id), + 200 * constants::float_scaling(), + ); + protocol_fees.increase_shares( + option::some(referral_id), + 1000 * constants::float_scaling(), + ); + // current_shares went from 200 to 0 then to 1000. Then 2000 fees were accrued. + protocol_fees.increase_fees_accrued( + test_constants::test_margin_pool_id(), + 2000 * constants::float_scaling(), + ); + // 1000 rewards for 1000 shares. 1.833 -> 2.833 + assert_eq!(protocol_fees.fees_per_share(), 2_833_333_333); + }; + + test.next_tx(test_constants::user1()); + { + // current_shares is 1000, fees_per_share 1.833 -> 2.833. unclaimed_fees is 1000 * (2.833 - 1.833) = 1000 * 1 = 1000 + let (current_shares, unclaimed_fees) = protocol_fees.referral_tracker(referral_id); + assert_eq!(current_shares, 1000 * constants::float_scaling()); + assert_eq!(unclaimed_fees, 1000 * constants::float_scaling()); + let referral = test.take_shared_by_id(referral_id); + let fees = protocol_fees.calculate_and_claim(&referral, test.ctx()); + assert_eq!(fees, 1000 * constants::float_scaling()); + let (current_shares, unclaimed_fees) = protocol_fees.referral_tracker(referral_id); + assert_eq!(current_shares, 1000 * constants::float_scaling()); + assert_eq!(unclaimed_fees, 0); + + return_shared(referral); + }; + + // add 1000 more rewards. 2.833 -> 3.833 + // referrer now has 1000 shares exposed. 1000 * (3.833 - 2.833) = 1000 * 1 = 1000 + test.next_tx(test_constants::user1()); + { + protocol_fees.increase_fees_accrued( + test_constants::test_margin_pool_id(), + 2000 * constants::float_scaling(), + ); + assert_eq!(protocol_fees.fees_per_share(), 3_833_333_333); + }; + + test.next_tx(test_constants::user1()); + { + let referral = test.take_shared_by_id(referral_id); + let fees = protocol_fees.calculate_and_claim(&referral, test.ctx()); + assert_eq!(fees, 1000 * constants::float_scaling()); + let (current_shares, unclaimed_fees) = protocol_fees.referral_tracker(referral_id); + assert_eq!(current_shares, 1000 * constants::float_scaling()); + assert_eq!(unclaimed_fees, 0); + + return_shared(referral); + }; + + destroy(admin_cap); + destroy(protocol_fees); + test.end(); +} + +#[test] +fun test_referra_fees_many() { + let (mut test, admin_cap) = test_helpers::setup_test(); + + test.next_tx(test_constants::admin()); + let mut protocol_fees = protocol_fees::default_protocol_fees(test.ctx()); + + // create 10 referrals, each with 1000 shares referred. + // total shares is 10 * 1000 = 10000 + let mut i = 0; + let mut referral_ids = vector::empty(); + while (i < 10) { + let referral_id = protocol_fees.mint_supply_referral(test.ctx()); + referral_ids.push_back(referral_id); + protocol_fees.increase_shares( + option::some(referral_id), + 1000 * constants::float_scaling(), + ); + let (current_shares, unclaimed_fees) = protocol_fees.referral_tracker(referral_id); + assert_eq!(current_shares, 1000 * constants::float_scaling()); + assert_eq!(unclaimed_fees, 0); + + i = i + 1; + }; + + // add 5000 rewards. 10000 shares. 0 -> 0.5 + test.next_tx(test_constants::admin()); + { + protocol_fees.increase_fees_accrued( + test_constants::test_margin_pool_id(), + 10000 * constants::float_scaling(), + ); + assert_eq!(protocol_fees.fees_per_share(), 500_000_000); + }; + + test.next_tx(test_constants::admin()); + { + i = 0; + while (i < 10) { + let referral = test.take_shared_by_id(referral_ids[i]); + let fees = protocol_fees.calculate_and_claim(&referral, test.ctx()); + assert_eq!(fees, 500 * constants::float_scaling()); + let (current_shares, unclaimed_fees) = protocol_fees.referral_tracker(referral_ids[i]); + assert_eq!(current_shares, 1000 * constants::float_scaling()); + assert_eq!(unclaimed_fees, 0); + return_shared(referral); + i = i + 1; + }; + }; + + // reduce all even referrer's shares by 1000, down to 0. + test.next_tx(test_constants::admin()); + { + i = 0; + while (i < 10) { + if (i % 2 == 0) { + protocol_fees.decrease_shares( + option::some(referral_ids[i]), + 1000 * constants::float_scaling(), + ); + }; + i = i + 1; + }; + }; + + // add 5000 rewards. 5000 outstanding shares. 0.5 -> 1.5 + test.next_tx(test_constants::admin()); + { + protocol_fees.increase_fees_accrued( + test_constants::test_margin_pool_id(), + 10000 * constants::float_scaling(), + ); + assert_eq!(protocol_fees.fees_per_share(), 1_500_000_000); + }; + + // referrers that were reduced to 0 shoul get 0 rewards. + // rest of them should get 1000 * (1.5 - 0.5) = 1000 * 1 = 1000 + test.next_tx(test_constants::admin()); + { + i = 0; + while (i < 10) { + let referral = test.take_shared_by_id(referral_ids[i]); + let fees = protocol_fees.calculate_and_claim(&referral, test.ctx()); + let (current_shares, unclaimed_fees) = protocol_fees.referral_tracker(referral_ids[i]); + if (i % 2 == 0) { + assert_eq!(fees, 0); + assert_eq!(unclaimed_fees, 0); + assert_eq!(current_shares, 0); + } else { + assert_eq!(fees, 1000 * constants::float_scaling()); + assert_eq!(unclaimed_fees, 0); + assert_eq!(current_shares, 1000 * constants::float_scaling()); + }; + return_shared(referral); + i = i + 1; + }; + }; + + destroy(admin_cap); + destroy(protocol_fees); + test.end(); +} + +#[test, expected_failure(abort_code = protocol_fees::ENotOwner)] +fun test_referral_fees_not_owner_e() { + let (mut test, _admin_cap) = test_helpers::setup_test(); + + test.next_tx(test_constants::admin()); + let mut protocol_fees = protocol_fees::default_protocol_fees(test.ctx()); + + let referral_id; + test.next_tx(test_constants::user1()); + { + referral_id = protocol_fees.mint_supply_referral(test.ctx()); + }; + + test.next_tx(test_constants::user2()); + { + let referral = test.take_shared_by_id(referral_id); + protocol_fees.calculate_and_claim(&referral, test.ctx()); + }; + + abort +} + +#[test] +fun test_referral_fees_redistributed_when_no_shares() { + let (mut test, _admin_cap) = test_helpers::setup_test(); + + test.next_tx(test_constants::admin()); + let mut protocol_fees = protocol_fees::default_protocol_fees(test.ctx()); + + let fees_accrued = 1000; + protocol_fees.increase_fees_accrued(test_constants::test_margin_pool_id(), fees_accrued); + + let expected_protocol = 500; + let expected_maintainer = 500; + + let actual_protocol = protocol_fees.protocol_fees(); + let actual_maintainer = protocol_fees.maintainer_fees(); + + assert_eq!(actual_protocol, expected_protocol); + assert_eq!(actual_maintainer, expected_maintainer); + assert_eq!(protocol_fees.total_shares(), 0); + + destroy(protocol_fees); + destroy(_admin_cap); + test.end(); +} diff --git a/packages/deepbook_margin/tests/margin_pool_math_tests.move b/packages/deepbook_margin/tests/margin_pool_math_tests.move new file mode 100644 index 000000000..43d98a979 --- /dev/null +++ b/packages/deepbook_margin/tests/margin_pool_math_tests.move @@ -0,0 +1,206 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +#[test_only] +module deepbook_margin::margin_pool_math_tests; + +use deepbook::{constants, math}; +use deepbook_margin::{ + margin_constants, + margin_pool::MarginPool, + margin_registry::{Self, MarginRegistry, MarginAdminCap, MaintainerCap}, + test_constants::{Self, USDC}, + test_helpers::{Self, mint_coin, advance_time, interest_rate} +}; +use std::unit_test::destroy; +use sui::{clock::Clock, test_scenario::{Self as test, Scenario, return_shared}}; + +fun setup_test(): (Scenario, Clock, MarginAdminCap, MaintainerCap, ID) { + let (mut scenario, admin_cap) = test_helpers::setup_test(); + + scenario.next_tx(test_constants::admin()); + let mut registry = scenario.take_shared(); + let clock = scenario.take_shared(); + let maintainer_cap = margin_registry::mint_maintainer_cap( + &mut registry, + &admin_cap, + &clock, + scenario.ctx(), + ); + test::return_shared(registry); + + let protocol_config = test_helpers::default_protocol_config(); + let pool_id = test_helpers::create_margin_pool( + &mut scenario, + &maintainer_cap, + protocol_config, + &clock, + ); + + (scenario, clock, admin_cap, maintainer_cap, pool_id) +} + +fun cleanup_test( + registry: MarginRegistry, + admin_cap: MarginAdminCap, + maintainer_cap: MaintainerCap, + clock: Clock, + scenario: Scenario, +) { + return_shared(registry); + destroy(admin_cap); + destroy(maintainer_cap); + destroy(clock); + scenario.end(); +} + +#[test] +fun test_borrow_supply_interest_ok() { + let duration = 1; + let borrow = 50 * test_constants::usdc_multiplier(); + let supply = 100 * test_constants::usdc_multiplier(); + test_borrow_supply(duration, borrow, supply); +} + +#[test] +fun test_borrow_supply_interest_ok_2() { + let duration = 5; + let borrow = 20 * test_constants::usdc_multiplier(); + let supply = 100 * test_constants::usdc_multiplier(); + test_borrow_supply(duration, borrow, supply); +} + +#[test] +fun test_borrow_supply_interest_ok_3() { + let duration = 2; + let borrow = 80 * test_constants::usdc_multiplier(); + let supply = 100 * test_constants::usdc_multiplier(); + test_borrow_supply(duration, borrow, supply); +} + +#[test] +fun test_borrow_supply_interest_ok_4() { + let duration = 3; + let borrow = 10 * test_constants::usdc_multiplier(); + let supply = 100 * test_constants::usdc_multiplier(); + test_borrow_supply(duration, borrow, supply); +} + +#[test] +fun test_borrow_supply_interest_ok_5() { + let duration = 10; + let borrow = 50 * test_constants::usdc_multiplier(); + let supply = 100 * test_constants::usdc_multiplier(); + test_borrow_supply(duration, borrow, supply); +} + +fun test_borrow_supply(duration: u64, borrow: u64, supply: u64) { + let duration_ms = duration * margin_constants::year_ms(); + let utilization_rate = math::div(borrow, supply); + // 100% + let interest_rate = + interest_rate( + utilization_rate, + test_constants::base_rate(), + test_constants::base_slope(), + test_constants::optimal_utilization(), + test_constants::excess_slope(), + ) * duration; + let borrow_multiplier = constants::float_scaling() + interest_rate; // 200% + // 1 + 1*0.5 = 1.5 + let supply_multiplier = + constants::float_scaling() + math::mul(test_constants::protocol_spread_inverse(), math::mul(interest_rate, utilization_rate)); + + let (mut scenario, mut clock, admin_cap, maintainer_cap, pool_id) = setup_test(); + scenario.next_tx(test_constants::admin()); + let mut pool = scenario.take_shared_by_id>(pool_id); + let registry = scenario.take_shared(); + + // At time 0, user1 supplies 100 USDC. User 2 borrows 50 USDC. + scenario.next_tx(test_constants::user1()); + let supplier_cap = test_helpers::supply_to_pool( + &mut pool, + ®istry, + supply, + &clock, + scenario.ctx(), + ); + + scenario.next_tx(test_constants::user2()); + let (borrowed_coin, shares) = pool.borrow( + borrow, + &clock, + scenario.ctx(), + ); + assert!(borrowed_coin.value() == borrow); + destroy(borrowed_coin); + + // 1 year passes + // Interest rate 100% on 50 USDC = 50 USDC interest + // Repayment should be 100 USDC for user 2 + advance_time(&mut clock, duration_ms); + scenario.next_tx(test_constants::user2()); + let repay_coin = mint_coin(math::mul(borrow, borrow_multiplier), scenario.ctx()); + pool.repay(shares, repay_coin, &clock); + + // User 1 withdraws his entire balance, receiving 150 USDC + scenario.next_tx(test_constants::user1()); + let withdrawn_coin = pool.withdraw( + ®istry, + &supplier_cap, + option::none(), + &clock, + scenario.ctx(), + ); + let expected_withdrawn_value = math::mul(supply, supply_multiplier); + assert!(withdrawn_coin.value() == expected_withdrawn_value); + destroy(withdrawn_coin); + destroy(supplier_cap); + + test::return_shared(pool); + cleanup_test(registry, admin_cap, maintainer_cap, clock, scenario); +} + +#[test] +fun test_zero_utilization() { + let (mut scenario, mut clock, admin_cap, maintainer_cap, pool_id) = setup_test(); + scenario.next_tx(test_constants::admin()); + let mut pool = scenario.take_shared_by_id>(pool_id); + let registry = scenario.take_shared(); + + scenario.next_tx(test_constants::user1()); + let supply = 100 * test_constants::usdc_multiplier(); + let supplier_cap = test_helpers::supply_to_pool( + &mut pool, + ®istry, + supply, + &clock, + scenario.ctx(), + ); + + advance_time(&mut clock, margin_constants::year_ms()); + + // Withdraw should give back same amount + scenario.next_tx(test_constants::user1()); + let withdrawn_coin = pool.withdraw( + ®istry, + &supplier_cap, + option::none(), + &clock, + scenario.ctx(), + ); + assert!(withdrawn_coin.value() == supply); + destroy(withdrawn_coin); + destroy(supplier_cap); + + test::return_shared(pool); + cleanup_test(registry, admin_cap, maintainer_cap, clock, scenario); +} + +#[test] +fun test_high_utilization_interest() { + let duration = 1; + let borrow = 79 * test_constants::usdc_multiplier(); // Just below optimal utilization + let supply = 100 * test_constants::usdc_multiplier(); + test_borrow_supply(duration, borrow, supply); +} diff --git a/packages/deepbook_margin/tests/margin_pool_tests.move b/packages/deepbook_margin/tests/margin_pool_tests.move new file mode 100644 index 000000000..bea40332c --- /dev/null +++ b/packages/deepbook_margin/tests/margin_pool_tests.move @@ -0,0 +1,1743 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +#[test_only] +module deepbook_margin::margin_pool_tests; + +use deepbook::{constants, math}; +use deepbook_margin::{ + margin_constants, + margin_pool::{Self, MarginPool}, + margin_registry::{Self, MarginRegistry, MarginAdminCap, MaintainerCap, MarginPoolCap}, + protocol_config, + protocol_fees, + test_constants::{Self, USDC, USDT}, + test_helpers::{Self, mint_coin, advance_time} +}; +use std::unit_test::{assert_eq, destroy}; +use sui::{clock::Clock, coin::Coin, test_scenario::{Self as test, Scenario, return_shared}}; + +fun setup_test(): (Scenario, Clock, MarginAdminCap, MaintainerCap, ID) { + let (mut scenario, admin_cap) = test_helpers::setup_test(); + + scenario.next_tx(test_constants::admin()); + let mut registry = scenario.take_shared(); + let clock = scenario.take_shared(); + let maintainer_cap = margin_registry::mint_maintainer_cap( + &mut registry, + &admin_cap, + &clock, + scenario.ctx(), + ); + test::return_shared(registry); + + let protocol_config = test_helpers::default_protocol_config(); + let pool_id = test_helpers::create_margin_pool( + &mut scenario, + &maintainer_cap, + protocol_config, + &clock, + ); + scenario.next_tx(test_constants::admin()); + + (scenario, clock, admin_cap, maintainer_cap, pool_id) +} + +fun cleanup_test( + registry: MarginRegistry, + admin_cap: MarginAdminCap, + maintainer_cap: MaintainerCap, + clock: Clock, + scenario: Scenario, +) { + return_shared(registry); + destroy(admin_cap); + destroy(maintainer_cap); + destroy(clock); + scenario.end(); +} + +fun setup_usdt_pool_with_cap( + scenario: &mut Scenario, + maintainer_cap: &MaintainerCap, + clock: &Clock, +): MarginPoolCap { + scenario.next_tx(test_constants::admin()); + let mut registry = scenario.take_shared(); + let config2 = test_helpers::default_protocol_config(); + let _pool_id2 = margin_pool::create_margin_pool( + &mut registry, + config2, + maintainer_cap, + clock, + scenario.ctx(), + ); + test::return_shared(registry); + + scenario.next_tx(test_constants::admin()); + scenario.take_from_sender() +} + +public fun test_borrow( + pool: &mut MarginPool, + amount: u64, + clock: &Clock, + ctx: &mut TxContext, +): Coin { + let (coin, _) = pool.borrow(amount, clock, ctx); + + coin +} + +#[test] +fun test_supply_and_withdraw_basic() { + let (mut scenario, clock, admin_cap, maintainer_cap, pool_id) = setup_test(); + + scenario.next_tx(test_constants::admin()); + let mut pool = scenario.take_shared_by_id>(pool_id); + let registry = scenario.take_shared(); + + scenario.next_tx(test_constants::user1()); + let supplier_cap = test_helpers::supply_to_pool( + &mut pool, + ®istry, + 100 * test_constants::usdc_multiplier(), + &clock, + scenario.ctx(), + ); + + let withdrawn = pool.withdraw( + ®istry, + &supplier_cap, + option::some(50 * test_constants::usdc_multiplier()), + &clock, + scenario.ctx(), + ); // 50 tokens + assert!(withdrawn.value() == 50 * test_constants::usdc_multiplier()); + + destroy(withdrawn); + destroy(supplier_cap); + test::return_shared(pool); + cleanup_test(registry, admin_cap, maintainer_cap, clock, scenario); +} + +#[test, expected_failure(abort_code = margin_pool::ESupplyCapExceeded)] +fun test_supply_cap_enforcement() { + let (mut scenario, clock, admin_cap, maintainer_cap, pool_id) = setup_test(); + + scenario.next_tx(test_constants::admin()); + let mut pool = scenario.take_shared_by_id>(pool_id); + let registry = scenario.take_shared(); + scenario.next_tx(test_constants::user1()); + let supplier_cap = test_helpers::supply_to_pool( + &mut pool, + ®istry, + test_constants::supply_cap() + 1, + &clock, + scenario.ctx(), + ); + + // This should fail due to supply cap + destroy(supplier_cap); + test::return_shared(pool); + cleanup_test(registry, admin_cap, maintainer_cap, clock, scenario); +} + +#[test] +fun test_multiple_users_supply_withdraw() { + let (mut scenario, clock, admin_cap, maintainer_cap, pool_id) = setup_test(); + let mut pool = scenario.take_shared_by_id>(pool_id); + let registry = scenario.take_shared(); + + // User1 supplies + scenario.next_tx(test_constants::user1()); + let supplier_cap1 = test_helpers::supply_to_pool( + &mut pool, + ®istry, + 50 * test_constants::usdc_multiplier(), + &clock, + scenario.ctx(), + ); + + // User2 supplies + scenario.next_tx(test_constants::user2()); + let supplier_cap2 = test_helpers::supply_to_pool( + &mut pool, + ®istry, + 30 * test_constants::usdc_multiplier(), + &clock, + scenario.ctx(), + ); + + scenario.next_tx(test_constants::user1()); + let withdrawn1 = pool.withdraw( + ®istry, + &supplier_cap1, + option::some(25 * test_constants::usdc_multiplier()), + &clock, + scenario.ctx(), + ); + assert!(withdrawn1.value() == 25 * test_constants::usdc_multiplier()); + + scenario.next_tx(test_constants::user2()); + let withdrawn2 = pool.withdraw( + ®istry, + &supplier_cap2, + option::some(15 * test_constants::usdc_multiplier()), + &clock, + scenario.ctx(), + ); + assert!(withdrawn2.value() == 15 * test_constants::usdc_multiplier()); + + destroy(withdrawn1); + destroy(withdrawn2); + destroy(supplier_cap1); + destroy(supplier_cap2); + test::return_shared(pool); + cleanup_test(registry, admin_cap, maintainer_cap, clock, scenario); +} + +#[test] +fun test_withdraw_all() { + let (mut scenario, clock, admin_cap, maintainer_cap, pool_id) = setup_test(); + let mut pool = scenario.take_shared_by_id>(pool_id); + let registry = scenario.take_shared(); + + scenario.next_tx(test_constants::user1()); + let supply_amount = 100 * test_constants::usdc_multiplier(); + let supplier_cap = test_helpers::supply_to_pool( + &mut pool, + ®istry, + supply_amount, + &clock, + scenario.ctx(), + ); + + let withdrawn = pool.withdraw( + ®istry, + &supplier_cap, + option::none(), + &clock, + scenario.ctx(), + ); + assert!(withdrawn.value() == supply_amount); + + destroy(withdrawn); + destroy(supplier_cap); + test::return_shared(pool); + cleanup_test(registry, admin_cap, maintainer_cap, clock, scenario); +} + +#[test] +fun test_create_margin_pool_with_config() { + let (scenario, clock, admin_cap, maintainer_cap, pool_id) = setup_test(); + let pool = scenario.take_shared_by_id>(pool_id); + let registry = scenario.take_shared(); + + test::return_shared(pool); + cleanup_test(registry, admin_cap, maintainer_cap, clock, scenario); +} + +#[test] +fun test_interest_accrual_over_time() { + let (mut scenario, mut clock, admin_cap, maintainer_cap, pool_id) = setup_test(); + + let mut pool = scenario.take_shared_by_id>(pool_id); + let registry = scenario.take_shared(); + scenario.next_tx(test_constants::user1()); + let supply_amount = 100 * test_constants::usdc_multiplier(); + let supplier_cap = test_helpers::supply_to_pool( + &mut pool, + ®istry, + supply_amount, + &clock, + scenario.ctx(), + ); + + clock.set_for_testing(margin_constants::year_ms()); + + let withdrawn = pool.withdraw( + ®istry, + &supplier_cap, + option::none(), + &clock, + scenario.ctx(), + ); + assert!(withdrawn.value() >= supply_amount); + + destroy(withdrawn); + destroy(supplier_cap); + test::return_shared(pool); + cleanup_test(registry, admin_cap, maintainer_cap, clock, scenario); +} + +#[test, expected_failure(abort_code = margin_pool::ENotEnoughAssetInPool)] +fun test_not_enough_asset_in_pool() { + let (mut scenario, clock, admin_cap, maintainer_cap, pool_id) = setup_test(); + let mut pool = scenario.take_shared_by_id>(pool_id); + let registry = scenario.take_shared(); + + scenario.next_tx(test_constants::user1()); + let supplier_cap = test_helpers::supply_to_pool( + &mut pool, + ®istry, + 100 * test_constants::usdc_multiplier(), + &clock, + scenario.ctx(), + ); + + scenario.next_tx(test_constants::user2()); + let borrowed_coin = test_borrow( + &mut pool, + 80 * test_constants::usdc_multiplier(), + &clock, + scenario.ctx(), + ); // 80 tokens + destroy(borrowed_coin); + + // Should fail due to outstanding loan + scenario.next_tx(test_constants::user1()); + let withdrawn = pool.withdraw( + ®istry, + &supplier_cap, + option::none(), + &clock, + scenario.ctx(), + ); + + destroy(withdrawn); + destroy(supplier_cap); + test::return_shared(pool); + cleanup_test(registry, admin_cap, maintainer_cap, clock, scenario); +} + +#[test, expected_failure(abort_code = margin_pool::EMaxPoolBorrowPercentageExceeded)] +fun test_max_pool_borrow_percentage_exceeded() { + let (mut scenario, clock, admin_cap, maintainer_cap, pool_id) = setup_test(); + let mut pool = scenario.take_shared_by_id>(pool_id); + let registry = scenario.take_shared(); + + scenario.next_tx(test_constants::user1()); + let supplier_cap = test_helpers::supply_to_pool( + &mut pool, + ®istry, + 100 * test_constants::usdc_multiplier(), + &clock, + scenario.ctx(), + ); + + // Above max utilization rate + scenario.next_tx(test_constants::user2()); + let borrowed_coin = test_borrow( + &mut pool, + 85 * test_constants::usdc_multiplier(), + &clock, + scenario.ctx(), + ); // 85 tokens > 80% + + destroy(borrowed_coin); + destroy(supplier_cap); + test::return_shared(pool); + cleanup_test(registry, admin_cap, maintainer_cap, clock, scenario); +} + +#[test, expected_failure(abort_code = margin_pool::EBorrowAmountTooLow)] +fun test_invalid_loan_quantity() { + let (mut scenario, clock, admin_cap, maintainer_cap, pool_id) = setup_test(); + let mut pool = scenario.take_shared_by_id>(pool_id); + let registry = scenario.take_shared(); + + scenario.next_tx(test_constants::user1()); + let supplier_cap = test_helpers::supply_to_pool( + &mut pool, + ®istry, + 100_000_000_000, + &clock, + scenario.ctx(), + ); + + scenario.next_tx(test_constants::user2()); + let borrowed_coin = test_borrow(&mut pool, 0, &clock, scenario.ctx()); + + destroy(borrowed_coin); + destroy(supplier_cap); + test::return_shared(pool); + cleanup_test(registry, admin_cap, maintainer_cap, clock, scenario); +} + +#[test, expected_failure(abort_code = margin_pool::EDeepbookPoolAlreadyAllowed)] +fun test_deepbook_pool_already_allowed() { + let (scenario, clock, admin_cap, maintainer_cap, pool_id) = setup_test(); + let mut pool = scenario.take_shared_by_id>(pool_id); + let registry = scenario.take_shared(); + + let margin_pool_cap = scenario.take_from_sender(); + + let deepbook_pool_id = object::id_from_address(@0x123); + + pool.enable_deepbook_pool_for_loan(®istry, deepbook_pool_id, &margin_pool_cap, &clock); + pool.enable_deepbook_pool_for_loan(®istry, deepbook_pool_id, &margin_pool_cap, &clock); + + scenario.return_to_sender(margin_pool_cap); + test::return_shared(pool); + cleanup_test(registry, admin_cap, maintainer_cap, clock, scenario); +} + +#[test, expected_failure(abort_code = margin_pool::EInvalidMarginPoolCap)] +fun test_invalid_margin_pool_cap() { + let (mut scenario, clock, admin_cap, maintainer_cap, pool_id) = setup_test(); + + let wrong_margin_pool_cap = setup_usdt_pool_with_cap(&mut scenario, &maintainer_cap, &clock); + let mut pool = scenario.take_shared_by_id>(pool_id); + let registry = scenario.take_shared(); + let deepbook_pool_id = object::id_from_address(@0x123); + + // Try to use wrong cap with the first pool (should fail) + pool.enable_deepbook_pool_for_loan(®istry, deepbook_pool_id, &wrong_margin_pool_cap, &clock); + + scenario.return_to_sender(wrong_margin_pool_cap); + test::return_shared(pool); + cleanup_test(registry, admin_cap, maintainer_cap, clock, scenario); +} + +#[test, expected_failure(abort_code = margin_pool::EInvalidMarginPoolCap)] +fun test_disable_with_invalid_margin_pool_cap() { + let (mut scenario, clock, admin_cap, maintainer_cap, pool_id) = setup_test(); + + let correct_cap = scenario.take_from_sender(); + let wrong_cap = setup_usdt_pool_with_cap(&mut scenario, &maintainer_cap, &clock); + + let mut pool = scenario.take_shared_by_id>(pool_id); + let registry = scenario.take_shared(); + let deepbook_pool_id = object::id_from_address(@0x123); + + pool.enable_deepbook_pool_for_loan(®istry, deepbook_pool_id, &correct_cap, &clock); + + pool.disable_deepbook_pool_for_loan(®istry, deepbook_pool_id, &wrong_cap, &clock); + + scenario.return_to_sender(correct_cap); + scenario.return_to_sender(wrong_cap); + test::return_shared(pool); + cleanup_test(registry, admin_cap, maintainer_cap, clock, scenario); +} + +#[test] +fun test_disable_deepbook_pool_for_loan() { + let (scenario, clock, admin_cap, maintainer_cap, pool_id) = setup_test(); + let mut pool = scenario.take_shared_by_id>(pool_id); + let registry = scenario.take_shared(); + let margin_pool_cap = scenario.take_from_sender(); + let deepbook_pool_id = object::id_from_address(@0x123); + + pool.enable_deepbook_pool_for_loan(®istry, deepbook_pool_id, &margin_pool_cap, &clock); + assert!(pool.deepbook_pool_allowed(deepbook_pool_id)); + + pool.disable_deepbook_pool_for_loan(®istry, deepbook_pool_id, &margin_pool_cap, &clock); + assert!(!pool.deepbook_pool_allowed(deepbook_pool_id)); + + scenario.return_to_sender(margin_pool_cap); + test::return_shared(pool); + cleanup_test(registry, admin_cap, maintainer_cap, clock, scenario); +} + +#[test, expected_failure(abort_code = margin_pool::EDeepbookPoolNotAllowed)] +fun test_disable_deepbook_pool_not_allowed() { + let (scenario, clock, admin_cap, maintainer_cap, pool_id) = setup_test(); + let mut pool = scenario.take_shared_by_id>(pool_id); + let registry = scenario.take_shared(); + let margin_pool_cap = scenario.take_from_sender(); + let deepbook_pool_id = object::id_from_address(@0x123); + + // disable without enabling first + pool.disable_deepbook_pool_for_loan(®istry, deepbook_pool_id, &margin_pool_cap, &clock); + + scenario.return_to_sender(margin_pool_cap); + test::return_shared(pool); + cleanup_test(registry, admin_cap, maintainer_cap, clock, scenario); +} + +#[test] +fun test_update_interest_params() { + let (scenario, clock, admin_cap, maintainer_cap, pool_id) = setup_test(); + let mut pool = scenario.take_shared_by_id>(pool_id); + let registry = scenario.take_shared(); + let margin_pool_cap = scenario.take_from_sender(); + + let new_interest_config = protocol_config::new_interest_config( + 100_000_000, // base_rate: 10% + 200_000_000, // base_slope: 20% + 700_000_000, // optimal_utilization: 70% + 3_000_000_000, // excess_slope: 300% + ); + + pool.update_interest_params(®istry, new_interest_config, &margin_pool_cap, &clock); + + scenario.return_to_sender(margin_pool_cap); + test::return_shared(pool); + cleanup_test(registry, admin_cap, maintainer_cap, clock, scenario); +} + +#[test, expected_failure(abort_code = margin_pool::EInvalidMarginPoolCap)] +fun test_update_interest_params_with_invalid_cap() { + let (mut scenario, clock, admin_cap, maintainer_cap, pool_id) = setup_test(); + let correct_cap = scenario.take_from_sender(); + let wrong_cap = setup_usdt_pool_with_cap(&mut scenario, &maintainer_cap, &clock); + + scenario.next_tx(test_constants::admin()); + let mut pool = scenario.take_shared_by_id>(pool_id); + let registry = scenario.take_shared(); + + // Create new interest config + let new_interest_config = protocol_config::new_interest_config( + 100_000_000, + 200_000_000, + 700_000_000, + 3_000_000_000, + ); + + pool.update_interest_params(®istry, new_interest_config, &wrong_cap, &clock); + + scenario.return_to_sender(correct_cap); + scenario.return_to_sender(wrong_cap); + test::return_shared(pool); + cleanup_test(registry, admin_cap, maintainer_cap, clock, scenario); +} + +#[test] +fun test_update_margin_pool_config() { + let (scenario, clock, admin_cap, maintainer_cap, pool_id) = setup_test(); + let mut pool = scenario.take_shared_by_id>(pool_id); + let registry = scenario.take_shared(); + let margin_pool_cap = scenario.take_from_sender(); + + let new_margin_pool_config = protocol_config::new_margin_pool_config( + 2_000_000_000_000_000, // supply_cap: 2M tokens + 900_000_000, // max_utilization_rate: 90% + 5_000_000, // protocol_spread: 0.5% + 100_000_000, // min_borrow: 0.1 token + ); + + pool.update_margin_pool_config(®istry, new_margin_pool_config, &margin_pool_cap, &clock); + + scenario.return_to_sender(margin_pool_cap); + test::return_shared(pool); + cleanup_test(registry, admin_cap, maintainer_cap, clock, scenario); +} + +#[test, expected_failure(abort_code = margin_pool::EInvalidMarginPoolCap)] +fun test_update_margin_pool_config_with_invalid_cap() { + let (mut scenario, clock, admin_cap, maintainer_cap, pool_id) = setup_test(); + + // Get the first pool's cap + let correct_cap = scenario.take_from_sender(); + let wrong_cap = setup_usdt_pool_with_cap(&mut scenario, &maintainer_cap, &clock); + + scenario.next_tx(test_constants::admin()); + let mut pool = scenario.take_shared_by_id>(pool_id); + let registry = scenario.take_shared(); + + // Create new margin pool config + let new_margin_pool_config = protocol_config::new_margin_pool_config( + 2_000_000_000_000_000, + 900_000_000, + 5_000_000, + 100_000_000, + ); + + // Try to update with wrong cap + pool.update_margin_pool_config(®istry, new_margin_pool_config, &wrong_cap, &clock); + + scenario.return_to_sender(correct_cap); + scenario.return_to_sender(wrong_cap); + test::return_shared(pool); + cleanup_test(registry, admin_cap, maintainer_cap, clock, scenario); +} + +#[test] +fun test_repay_liquidation_with_reward() { + let (mut scenario, clock, admin_cap, maintainer_cap, pool_id) = setup_test(); + let mut pool = scenario.take_shared_by_id>(pool_id); + let registry = scenario.take_shared(); + + // User1 supplies + scenario.next_tx(test_constants::user1()); + let supplier_cap = test_helpers::supply_to_pool( + &mut pool, + ®istry, + 100 * test_constants::usdc_multiplier(), + &clock, + scenario.ctx(), + ); + + // User2 borrows + scenario.next_tx(test_constants::user2()); + let (borrowed_coin, shares) = pool.borrow( + 50 * test_constants::usdc_multiplier(), + &clock, + scenario.ctx(), + ); + destroy(borrowed_coin); + + // Liquidation with extra amount (reward scenario) + let repay_amount = pool.borrow_shares_to_amount(shares, &clock); + let extra_amount = 5 * test_constants::usdc_multiplier(); + let liquidation_coin = mint_coin(repay_amount + extra_amount, scenario.ctx()); + let (amount, reward, default) = pool.repay_liquidation(shares, liquidation_coin, &clock); + + assert!(amount == repay_amount); + assert!(reward == extra_amount); + assert!(default == 0); + + destroy(supplier_cap); + test::return_shared(pool); + cleanup_test(registry, admin_cap, maintainer_cap, clock, scenario); +} + +#[test] +fun test_repay_liquidation_with_default() { + let (mut scenario, clock, admin_cap, maintainer_cap, pool_id) = setup_test(); + let mut pool = scenario.take_shared_by_id>(pool_id); + let registry = scenario.take_shared(); + + // User1 supplies + scenario.next_tx(test_constants::user1()); + let supplier_cap = test_helpers::supply_to_pool( + &mut pool, + ®istry, + 100 * test_constants::usdc_multiplier(), + &clock, + scenario.ctx(), + ); + + // User2 borrows + scenario.next_tx(test_constants::user2()); + let (borrowed_coin, shares) = pool.borrow( + 50 * test_constants::usdc_multiplier(), + &clock, + scenario.ctx(), + ); + destroy(borrowed_coin); + + // Liquidation with insufficient amount (default scenario) + let repay_amount = pool.borrow_shares_to_amount(shares, &clock); + let insufficient_amount = repay_amount - 10 * test_constants::usdc_multiplier(); + let liquidation_coin = mint_coin(insufficient_amount, scenario.ctx()); + let (amount, reward, default) = pool.repay_liquidation(shares, liquidation_coin, &clock); + + assert!(amount == repay_amount); + assert!(reward == 0); + assert!(default == 10 * test_constants::usdc_multiplier()); + + destroy(supplier_cap); + test::return_shared(pool); + cleanup_test(registry, admin_cap, maintainer_cap, clock, scenario); +} + +#[test] +fun test_multiple_deepbook_pools() { + let (scenario, clock, admin_cap, maintainer_cap, pool_id) = setup_test(); + let mut pool = scenario.take_shared_by_id>(pool_id); + let registry = scenario.take_shared(); + let margin_pool_cap = scenario.take_from_sender(); + + let deepbook_pool_id1 = object::id_from_address(@0x123); + let deepbook_pool_id2 = object::id_from_address(@0x456); + let deepbook_pool_id3 = object::id_from_address(@0x789); + + // Enable multiple pools + pool.enable_deepbook_pool_for_loan(®istry, deepbook_pool_id1, &margin_pool_cap, &clock); + pool.enable_deepbook_pool_for_loan(®istry, deepbook_pool_id2, &margin_pool_cap, &clock); + pool.enable_deepbook_pool_for_loan(®istry, deepbook_pool_id3, &margin_pool_cap, &clock); + + assert!(pool.deepbook_pool_allowed(deepbook_pool_id1)); + assert!(pool.deepbook_pool_allowed(deepbook_pool_id2)); + assert!(pool.deepbook_pool_allowed(deepbook_pool_id3)); + + // Disable one pool + pool.disable_deepbook_pool_for_loan(®istry, deepbook_pool_id2, &margin_pool_cap, &clock); + + assert!(pool.deepbook_pool_allowed(deepbook_pool_id1)); + assert!(!pool.deepbook_pool_allowed(deepbook_pool_id2)); + assert!(pool.deepbook_pool_allowed(deepbook_pool_id3)); + + scenario.return_to_sender(margin_pool_cap); + test::return_shared(pool); + cleanup_test(registry, admin_cap, maintainer_cap, clock, scenario); +} + +#[test, expected_failure(abort_code = margin_pool::ENotEnoughAssetInPool)] +fun test_borrow_exceeds_vault_balance() { + let (mut scenario, clock, admin_cap, maintainer_cap, pool_id) = setup_test(); + let mut pool = scenario.take_shared_by_id>(pool_id); + let registry = scenario.take_shared(); + + // User1 supplies 100 USDC + scenario.next_tx(test_constants::user1()); + let supply_amount = 100 * test_constants::usdc_multiplier(); + let supplier_cap = test_helpers::supply_to_pool( + &mut pool, + ®istry, + supply_amount, + &clock, + scenario.ctx(), + ); + + // User2 borrows 70 USDC + scenario.next_tx(test_constants::user2()); + let first_borrow = 70 * test_constants::usdc_multiplier(); + let (borrowed_coin1, _) = pool.borrow(first_borrow, &clock, scenario.ctx()); + destroy(borrowed_coin1); + + // User3 tries to borrow $1 more than what's left in the vault + scenario.next_tx(test_constants::liquidator()); + let second_borrow = 31 * test_constants::usdc_multiplier(); + let (borrowed_coin2, _) = pool.borrow(second_borrow, &clock, scenario.ctx()); + + destroy(borrowed_coin2); + destroy(supplier_cap); + test::return_shared(pool); + cleanup_test(registry, admin_cap, maintainer_cap, clock, scenario); +} + +#[test, expected_failure(abort_code = margin_pool::ENotEnoughAssetInPool)] +fun test_withdraw_exceeds_available_liquidity() { + let (mut scenario, clock, admin_cap, maintainer_cap, pool_id) = setup_test(); + let mut pool = scenario.take_shared_by_id>(pool_id); + let registry = scenario.take_shared(); + + // User1 and User2 both supply + scenario.next_tx(test_constants::user1()); + let supply1 = 60 * test_constants::usdc_multiplier(); + let supplier_cap1 = test_helpers::supply_to_pool( + &mut pool, + ®istry, + supply1, + &clock, + scenario.ctx(), + ); + + scenario.next_tx(test_constants::user2()); + let supply2 = 40 * test_constants::usdc_multiplier(); + let supplier_cap2 = test_helpers::supply_to_pool( + &mut pool, + ®istry, + supply2, + &clock, + scenario.ctx(), + ); + + // Someone borrows, reducing available liquidity + scenario.next_tx(test_constants::liquidator()); + let borrow_amount = 75 * test_constants::usdc_multiplier(); + let (borrowed_coin, _) = pool.borrow(borrow_amount, &clock, scenario.ctx()); + destroy(borrowed_coin); + + // Now only 25 USDC left in vault + // User1 tries to withdraw their full 60 USDC, but only 25 is available + scenario.next_tx(test_constants::user1()); + let withdrawn = pool.withdraw( + ®istry, + &supplier_cap1, + option::none(), // withdraw all + &clock, + scenario.ctx(), + ); + + destroy(withdrawn); + destroy(supplier_cap1); + destroy(supplier_cap2); + test::return_shared(pool); + cleanup_test(registry, admin_cap, maintainer_cap, clock, scenario); +} + +#[test] +fun test_liquidation_exact_amount() { + let (mut scenario, clock, admin_cap, maintainer_cap, pool_id) = setup_test(); + scenario.next_tx(test_constants::admin()); + let mut pool = scenario.take_shared_by_id>(pool_id); + let registry = scenario.take_shared(); + + scenario.next_tx(test_constants::user1()); + let supplier_cap = test_helpers::supply_to_pool( + &mut pool, + ®istry, + 1000 * test_constants::usdc_multiplier(), + &clock, + scenario.ctx(), + ); + + scenario.next_tx(test_constants::user2()); + let (borrowed_coin, shares) = pool.borrow( + 500 * test_constants::usdc_multiplier(), + &clock, + scenario.ctx(), + ); + destroy(borrowed_coin); + + let exact_amount = pool.borrow_shares_to_amount(shares, &clock); + let liquidation_coin = mint_coin(exact_amount, scenario.ctx()); + let (amount, reward, default) = pool.repay_liquidation(shares, liquidation_coin, &clock); + + assert!(amount == exact_amount); + assert!(reward == 0); + assert!(default == 0); + + destroy(supplier_cap); + test::return_shared(pool); + cleanup_test(registry, admin_cap, maintainer_cap, clock, scenario); +} + +#[test] +fun test_liquidation_zero_shares() { + let (mut scenario, clock, admin_cap, maintainer_cap, pool_id) = setup_test(); + scenario.next_tx(test_constants::admin()); + let mut pool = scenario.take_shared_by_id>(pool_id); + let registry = scenario.take_shared(); + + scenario.next_tx(test_constants::user1()); + let supplier_cap = test_helpers::supply_to_pool( + &mut pool, + ®istry, + 1000 * test_constants::usdc_multiplier(), + &clock, + scenario.ctx(), + ); + + let liquidation_coin = mint_coin(100 * test_constants::usdc_multiplier(), scenario.ctx()); + let (amount, reward, default) = pool.repay_liquidation(0, liquidation_coin, &clock); + + assert!(amount == 0); + assert!(reward == 100 * test_constants::usdc_multiplier()); // all reward + assert!(default == 0); + + destroy(supplier_cap); + test::return_shared(pool); + cleanup_test(registry, admin_cap, maintainer_cap, clock, scenario); +} + +#[test] +fun test_supply_withdrawal_with_interest() { + let (mut scenario, mut clock, admin_cap, maintainer_cap, pool_id) = setup_test(); + scenario.next_tx(test_constants::admin()); + let mut pool = scenario.take_shared_by_id>(pool_id); + let registry = scenario.take_shared(); + + // Two users supply + scenario.next_tx(test_constants::user1()); + let supply1 = 1000 * test_constants::usdc_multiplier(); + let supplier_cap1 = test_helpers::supply_to_pool( + &mut pool, + ®istry, + supply1, + &clock, + scenario.ctx(), + ); + + scenario.next_tx(test_constants::user2()); + let supply2 = 500 * test_constants::usdc_multiplier(); + let supplier_cap2 = test_helpers::supply_to_pool( + &mut pool, + ®istry, + supply2, + &clock, + scenario.ctx(), + ); + + scenario.next_tx(test_constants::liquidator()); + let (borrowed_coin, _) = pool.borrow( + 750 * test_constants::usdc_multiplier(), + &clock, + scenario.ctx(), + ); + destroy(borrowed_coin); + + advance_time(&mut clock, margin_constants::year_ms()); + + // User1 withdraws 20% of initial deposit + scenario.next_tx(test_constants::user1()); + let withdrawn1 = pool.withdraw( + ®istry, + &supplier_cap1, + option::some(200 * test_constants::usdc_multiplier()), + &clock, + scenario.ctx(), + ); + + assert_eq!(withdrawn1.value(), 200 * test_constants::usdc_multiplier()); + destroy(withdrawn1); + + // User2 tries to withdraw all + scenario.next_tx(test_constants::user2()); + let withdrawn2 = pool.withdraw( + ®istry, + &supplier_cap2, + option::none(), + &clock, + scenario.ctx(), + ); + + assert!(withdrawn2.value() > supply2); + destroy(withdrawn2); + destroy(supplier_cap1); + destroy(supplier_cap2); + + test::return_shared(pool); + cleanup_test(registry, admin_cap, maintainer_cap, clock, scenario); +} + +#[test] +fun test_partial_liquidation_half_shares() { + let (mut scenario, mut clock, admin_cap, maintainer_cap, pool_id) = setup_test(); + scenario.next_tx(test_constants::admin()); + let mut pool = scenario.take_shared_by_id>(pool_id); + let registry = scenario.take_shared(); + + scenario.next_tx(test_constants::user1()); + let supplier_cap = test_helpers::supply_to_pool( + &mut pool, + ®istry, + 10000 * test_constants::usdc_multiplier(), + &clock, + scenario.ctx(), + ); + + scenario.next_tx(test_constants::user2()); + let borrow_amount = 1000 * test_constants::usdc_multiplier(); + let (borrowed_coin, total_shares) = pool.borrow(borrow_amount, &clock, scenario.ctx()); + destroy(borrowed_coin); + + advance_time(&mut clock, margin_constants::year_ms()); + + let half_shares = total_shares / 2; + let half_amount = pool.borrow_shares_to_amount(half_shares, &clock); + let liquidation_coin = mint_coin(half_amount + 10000, scenario.ctx()); + let (amount, reward, default) = pool.repay_liquidation(half_shares, liquidation_coin, &clock); + + assert!(amount == half_amount); + assert!(reward == 10000); + assert!(default == 0); + + let remaining_shares = total_shares - half_shares; + let remaining_amount = pool.borrow_shares_to_amount(remaining_shares, &clock); + // remaining amount should include accrued interest + assert!(remaining_amount > borrow_amount / 2); + + destroy(supplier_cap); + test::return_shared(pool); + cleanup_test(registry, admin_cap, maintainer_cap, clock, scenario); +} + +#[test] +fun test_partial_liquidation_with_default() { + let (mut scenario, mut clock, admin_cap, maintainer_cap, pool_id) = setup_test(); + scenario.next_tx(test_constants::admin()); + let mut pool = scenario.take_shared_by_id>(pool_id); + let registry = scenario.take_shared(); + + scenario.next_tx(test_constants::user1()); + let supplier_cap = test_helpers::supply_to_pool( + &mut pool, + ®istry, + 10000 * test_constants::usdc_multiplier(), + &clock, + scenario.ctx(), + ); + + scenario.next_tx(test_constants::user2()); + let borrow_amount = 2000 * test_constants::usdc_multiplier(); + let (borrowed_coin, total_shares) = pool.borrow(borrow_amount, &clock, scenario.ctx()); + destroy(borrowed_coin); + + advance_time(&mut clock, margin_constants::year_ms() / 6); + + // Partial liquidation of 30% shares with insufficient payment + let partial_shares = total_shares / 2; + let required_amount = pool.borrow_shares_to_amount(partial_shares, &clock); + let insufficient_amount = (required_amount * 90) / 100; + let liquidation_coin = mint_coin(insufficient_amount, scenario.ctx()); + let (amount, reward, default) = pool.repay_liquidation( + partial_shares, + liquidation_coin, + &clock, + ); + + assert!(amount == required_amount); + assert!(reward == 0); + assert!(default == required_amount - insufficient_amount); + + let remaining_shares = total_shares - partial_shares; + let remaining_amount = pool.borrow_shares_to_amount(remaining_shares, &clock); + assert!(remaining_amount > 0); + + destroy(supplier_cap); + test::return_shared(pool); + cleanup_test(registry, admin_cap, maintainer_cap, clock, scenario); +} + +#[test] +fun test_full_liquidation_with_interest() { + let (mut scenario, mut clock, admin_cap, maintainer_cap, pool_id) = setup_test(); + scenario.next_tx(test_constants::admin()); + let mut pool = scenario.take_shared_by_id>(pool_id); + let registry = scenario.take_shared(); + + scenario.next_tx(test_constants::user1()); + let supply_amount = 10000 * test_constants::usdc_multiplier(); + let supplier_cap = test_helpers::supply_to_pool( + &mut pool, + ®istry, + supply_amount, + &clock, + scenario.ctx(), + ); + + scenario.next_tx(test_constants::user2()); + let borrow_amount = 3000 * test_constants::usdc_multiplier(); + let (borrowed_coin, borrow_shares) = pool.borrow(borrow_amount, &clock, scenario.ctx()); + destroy(borrowed_coin); + + let initial_debt = pool.borrow_shares_to_amount(borrow_shares, &clock); + assert!(initial_debt == borrow_amount); + + advance_time(&mut clock, margin_constants::year_ms()); + + // Check debt has grown substantially due to interest + let debt_after_interest = pool.borrow_shares_to_amount(borrow_shares, &clock); + assert!(debt_after_interest > initial_debt); + + scenario.next_tx(test_constants::liquidator()); + let liquidation_coin = mint_coin(debt_after_interest + 1000, scenario.ctx()); + let (_, reward, default) = pool.repay_liquidation( + borrow_shares, + liquidation_coin, + &clock, + ); + assert!(reward > 0); + assert!(default == 0); + + // User should be able to withdraw supply plus interest earned + scenario.next_tx(test_constants::user1()); + let withdrawn = pool.withdraw(®istry, &supplier_cap, option::none(), &clock, scenario.ctx()); + let interest_earned = withdrawn.value() - supply_amount; + assert!(withdrawn.value() > supply_amount); + assert!(interest_earned > 0); + + destroy(withdrawn); + destroy(supplier_cap); + test::return_shared(pool); + cleanup_test(registry, admin_cap, maintainer_cap, clock, scenario); +} + +#[test] +fun test_user_supply_shares_tracks_individual_users() { + let (mut scenario, clock, admin_cap, maintainer_cap, pool_id) = setup_test(); + + // User1 supplies 20 USDC + scenario.next_tx(test_constants::user1()); + let mut pool = scenario.take_shared_by_id>(pool_id); + let registry = scenario.take_shared(); + let supplier_cap_1 = margin_pool::mint_supplier_cap(®istry, &clock, scenario.ctx()); + let supplier_cap_1_id = object::id(&supplier_cap_1); + let supply_coin_1 = mint_coin(20 * test_constants::usdc_multiplier(), scenario.ctx()); + + let user1_shares = pool.supply( + ®istry, + &supplier_cap_1, + supply_coin_1, + option::none(), + &clock, + ); + + // Verify user1 shares via the new function + assert!(pool.user_supply_shares(supplier_cap_1_id) == user1_shares); + assert!(pool.user_supply_shares(supplier_cap_1_id) == 20 * test_constants::usdc_multiplier()); + + // Pool should have 20 total supply shares + assert!(pool.supply_shares() == 20 * test_constants::usdc_multiplier()); + + test::return_shared(pool); + return_shared(registry); + destroy(supplier_cap_1); + + // User2 supplies 10 USDC + scenario.next_tx(test_constants::user2()); + let mut pool = scenario.take_shared_by_id>(pool_id); + let registry = scenario.take_shared(); + let supplier_cap_2 = margin_pool::mint_supplier_cap(®istry, &clock, scenario.ctx()); + let supplier_cap_2_id = object::id(&supplier_cap_2); + let supply_coin_2 = mint_coin(10 * test_constants::usdc_multiplier(), scenario.ctx()); + + let user2_shares = pool.supply( + ®istry, + &supplier_cap_2, + supply_coin_2, + option::none(), + &clock, + ); + + // Verify user2 has exactly 10 shares (not 30) + assert!(pool.user_supply_shares(supplier_cap_2_id) == user2_shares); + assert!(pool.user_supply_shares(supplier_cap_2_id) == 10 * test_constants::usdc_multiplier()); + + // Pool should now have 30 total supply shares (20 + 10) + assert!(pool.supply_shares() == 30 * test_constants::usdc_multiplier()); + + test::return_shared(pool); + destroy(supplier_cap_2); + + cleanup_test(registry, admin_cap, maintainer_cap, clock, scenario); +} + +#[test] +fun test_user_supply_amount_reflects_shares_value() { + let (mut scenario, clock, admin_cap, maintainer_cap, pool_id) = setup_test(); + + // User supplies 100 USDC + scenario.next_tx(test_constants::user1()); + let mut pool = scenario.take_shared_by_id>(pool_id); + let registry = scenario.take_shared(); + let supplier_cap = margin_pool::mint_supplier_cap(®istry, &clock, scenario.ctx()); + let supplier_cap_id = object::id(&supplier_cap); + let supply_amount = 100 * test_constants::usdc_multiplier(); + let supply_coin = mint_coin(supply_amount, scenario.ctx()); + + let shares = pool.supply(®istry, &supplier_cap, supply_coin, option::none(), &clock); + + // At ratio 1, shares should equal amount + assert!(shares == supply_amount); + + // Verify user_supply_shares returns correct shares + assert!(pool.user_supply_shares(supplier_cap_id) == shares); + + // Verify user_supply_amount returns correct amount + let amount = pool.user_supply_amount(supplier_cap_id, &clock); + assert!(amount == supply_amount); + + // Shares and amount should be equal at ratio 1 + assert!(pool.user_supply_shares(supplier_cap_id) == amount); + + test::return_shared(pool); + destroy(supplier_cap); + cleanup_test(registry, admin_cap, maintainer_cap, clock, scenario); +} + +#[test] +fun test_user_supply_amount_with_interest_accrual() { + let (mut scenario, mut clock, admin_cap, maintainer_cap, pool_id) = setup_test(); + + // User supplies 1000 USDC + scenario.next_tx(test_constants::user1()); + let mut pool = scenario.take_shared_by_id>(pool_id); + let registry = scenario.take_shared(); + let supplier_cap = margin_pool::mint_supplier_cap(®istry, &clock, scenario.ctx()); + let supplier_cap_id = object::id(&supplier_cap); + let supply_amount = 1000 * test_constants::usdc_multiplier(); + let supply_coin = mint_coin(supply_amount, scenario.ctx()); + + pool.supply(®istry, &supplier_cap, supply_coin, option::none(), &clock); + + let initial_shares = pool.user_supply_shares(supplier_cap_id); + let initial_amount = pool.user_supply_amount(supplier_cap_id, &clock); + + assert!(initial_shares == supply_amount); + assert!(initial_amount == supply_amount); + + test::return_shared(pool); + return_shared(registry); + + // Someone borrows to generate interest + scenario.next_tx(test_constants::user2()); + let mut pool = scenario.take_shared_by_id>(pool_id); + let registry = scenario.take_shared(); + let (borrowed_coin, _) = pool.borrow( + 500 * test_constants::usdc_multiplier(), + &clock, + scenario.ctx(), + ); + + test::return_shared(pool); + return_shared(registry); + destroy(borrowed_coin); + + // Advance time to accrue interest + clock.increment_for_testing(30 * 24 * 60 * 60 * 1000); // 30 days + + // Check that amount increased but shares stayed the same + scenario.next_tx(test_constants::user1()); + let pool = scenario.take_shared_by_id>(pool_id); + let registry = scenario.take_shared(); + + let final_shares = pool.user_supply_shares(supplier_cap_id); + let final_amount = pool.user_supply_amount(supplier_cap_id, &clock); + + // Shares should remain unchanged + assert!(final_shares == initial_shares); + + // Amount should have increased due to interest + assert!(final_amount > initial_amount); + + test::return_shared(pool); + destroy(supplier_cap); + cleanup_test(registry, admin_cap, maintainer_cap, clock, scenario); +} + +#[test] +fun test_multiple_users_supply_amounts_independent() { + let (mut scenario, clock, admin_cap, maintainer_cap, pool_id) = setup_test(); + + // User1 supplies 50 USDC + scenario.next_tx(test_constants::user1()); + let mut pool = scenario.take_shared_by_id>(pool_id); + let registry = scenario.take_shared(); + let supplier_cap_1 = margin_pool::mint_supplier_cap(®istry, &clock, scenario.ctx()); + let supplier_cap_1_id = object::id(&supplier_cap_1); + let user1_supply_amount = 50 * test_constants::usdc_multiplier(); + let supply_coin_1 = mint_coin(user1_supply_amount, scenario.ctx()); + + pool.supply(®istry, &supplier_cap_1, supply_coin_1, option::none(), &clock); + + let user1_shares = pool.user_supply_shares(supplier_cap_1_id); + let user1_amount = pool.user_supply_amount(supplier_cap_1_id, &clock); + + assert!(user1_shares == user1_supply_amount); + assert!(user1_amount == user1_supply_amount); + + test::return_shared(pool); + return_shared(registry); + destroy(supplier_cap_1); + + // User2 supplies 30 USDC + scenario.next_tx(test_constants::user2()); + let mut pool = scenario.take_shared_by_id>(pool_id); + let registry = scenario.take_shared(); + let supplier_cap_2 = margin_pool::mint_supplier_cap(®istry, &clock, scenario.ctx()); + let supplier_cap_2_id = object::id(&supplier_cap_2); + let user2_supply_amount = 30 * test_constants::usdc_multiplier(); + let supply_coin_2 = mint_coin(user2_supply_amount, scenario.ctx()); + + pool.supply(®istry, &supplier_cap_2, supply_coin_2, option::none(), &clock); + + let user2_shares = pool.user_supply_shares(supplier_cap_2_id); + let user2_amount = pool.user_supply_amount(supplier_cap_2_id, &clock); + + // User2's shares should be 30, not 80 (pool total) + assert!(user2_shares == user2_supply_amount); + assert!(user2_amount == user2_supply_amount); + + // Pool total should be 50 + 30 = 80 + assert!(pool.total_supply() == user1_supply_amount + user2_supply_amount); + assert!(pool.supply_shares() == user1_shares + user2_shares); + + test::return_shared(pool); + destroy(supplier_cap_2); + + cleanup_test(registry, admin_cap, maintainer_cap, clock, scenario); +} + +#[test, expected_failure(abort_code = margin_pool::EInvalidMarginPoolCap)] +fun test_withdraw_maintainer_fees_with_wrong_cap() { + let (mut scenario, clock, admin_cap, maintainer_cap, pool_id) = setup_test(); + + // Create a second pool and get its cap (the wrong cap) + let wrong_cap = setup_usdt_pool_with_cap(&mut scenario, &maintainer_cap, &clock); + + scenario.next_tx(test_constants::admin()); + let mut pool = scenario.take_shared_by_id>(pool_id); + let registry = scenario.take_shared(); + + // Supply some funds to generate fees + scenario.next_tx(test_constants::user1()); + let supplier_cap = test_helpers::supply_to_pool( + &mut pool, + ®istry, + 100 * test_constants::usdc_multiplier(), + &clock, + scenario.ctx(), + ); + + // Try to withdraw maintainer fees with the wrong cap (should fail) + scenario.next_tx(test_constants::admin()); + let coin = pool.withdraw_maintainer_fees(®istry, &wrong_cap, &clock, scenario.ctx()); + + destroy(supplier_cap); + destroy(coin); + scenario.return_to_sender(wrong_cap); + test::return_shared(pool); + cleanup_test(registry, admin_cap, maintainer_cap, clock, scenario); +} + +#[test, expected_failure(abort_code = protocol_fees::ENotOwner)] +fun test_withdraw_referral_fees_not_owner() { + use deepbook_margin::protocol_fees::SupplyReferral; + + let (mut scenario, mut clock, admin_cap, maintainer_cap, pool_id) = setup_test(); + + scenario.next_tx(test_constants::admin()); + let mut pool = scenario.take_shared_by_id>(pool_id); + let registry = scenario.take_shared(); + + // User1 creates a supply referral + scenario.next_tx(test_constants::user1()); + let referral_id = pool.mint_supply_referral(®istry, &clock, scenario.ctx()); + + // Supply some funds with the referral to generate fees + scenario.next_tx(test_constants::user2()); + let supplier_cap = margin_pool::mint_supplier_cap(®istry, &clock, scenario.ctx()); + let supply_coin = mint_coin(100 * test_constants::usdc_multiplier(), scenario.ctx()); + pool.supply(®istry, &supplier_cap, supply_coin, option::some(referral_id), &clock); + + // Advance time and add some borrow to generate interest/fees + advance_time(&mut clock, 30 * 24 * 60 * 60 * 1000); // 30 days + + // User2 (not the owner) tries to withdraw referral fees (should fail) + scenario.next_tx(test_constants::user2()); + let referral = scenario.take_shared_by_id(referral_id); + let coin = pool.withdraw_referral_fees(®istry, &referral, scenario.ctx()); + + return_shared(referral); + destroy(supplier_cap); + destroy(coin); + test::return_shared(pool); + cleanup_test(registry, admin_cap, maintainer_cap, clock, scenario); +} + +#[test] +fun test_admin_withdraw_default_referral_fees() { + let (mut scenario, clock, admin_cap, maintainer_cap, pool_id) = setup_test(); + + scenario.next_tx(test_constants::admin()); + let mut pool = scenario.take_shared_by_id>(pool_id); + let registry = scenario.take_shared(); + + // User1 supplies WITHOUT a referral (goes to default 0x0) + scenario.next_tx(test_constants::user1()); + let supplier_cap1 = margin_pool::mint_supplier_cap(®istry, &clock, scenario.ctx()); + let supply_coin1 = mint_coin(1000 * test_constants::usdc_multiplier(), scenario.ctx()); + pool.supply(®istry, &supplier_cap1, supply_coin1, option::none(), &clock); + + // User2 also supplies WITHOUT a referral + scenario.next_tx(test_constants::user2()); + let supplier_cap2 = margin_pool::mint_supplier_cap(®istry, &clock, scenario.ctx()); + let supply_coin2 = mint_coin(500 * test_constants::usdc_multiplier(), scenario.ctx()); + pool.supply(®istry, &supplier_cap2, supply_coin2, option::none(), &clock); + + // Check that default referral has shares + let default_id = margin_constants::default_referral(); + let (current_shares, _unclaimed_fees) = protocol_fees::referral_tracker( + pool.protocol_fees(), + default_id, + ); + assert!(current_shares > 0); // Users supplied without referral, so default has shares + + // Admin can call the function to claim default referral fees (even if 0) + scenario.next_tx(test_constants::admin()); + let default_referral_coin = pool.admin_withdraw_default_referral_fees( + ®istry, + &admin_cap, + scenario.ctx(), + ); + + // Fees will be 0 initially since no borrows/interest yet, + // but the important thing is admin CAN claim them (not stuck) + let fees_claimed = default_referral_coin.value(); + assert_eq!(fees_claimed, 0); // No fees accrued yet + + // Verify default referral's unclaimed_fees reset after claim + let (current_shares_after, unclaimed_fees) = protocol_fees::referral_tracker( + pool.protocol_fees(), + default_id, + ); + assert_eq!(unclaimed_fees, 0); + assert_eq!(current_shares_after, current_shares); + + // Cleanup + destroy(supplier_cap1); + destroy(supplier_cap2); + destroy(default_referral_coin); + test::return_shared(pool); + cleanup_test(registry, admin_cap, maintainer_cap, clock, scenario); +} + +#[test] +fun test_withdraw_round_up_shares() { + let (mut scenario, mut clock, admin_cap, maintainer_cap, pool_id) = setup_test(); + + scenario.next_tx(test_constants::admin()); + let mut pool = scenario.take_shared_by_id>(pool_id); + let registry = scenario.take_shared(); + + scenario.next_tx(test_constants::user1()); + // Supply 10 tokens + let supplier_cap = test_helpers::supply_to_pool( + &mut pool, + ®istry, + 10 * test_constants::usdc_multiplier(), + &clock, + scenario.ctx(), + ); + + // Borrow to create interest accrual + scenario.next_tx(test_constants::user2()); + let borrow_coin = test_borrow( + &mut pool, + 5 * test_constants::usdc_multiplier(), + &clock, + scenario.ctx(), + ); + + // Advance time to accrue interest + advance_time(&mut clock, 365 * 24 * 60 * 60 * 1000); // 1 year + + // Now shares are worth more than initial amount (ratio > 1) + scenario.next_tx(test_constants::user1()); + let supplier_cap_id = object::id(&supplier_cap); + let shares_before = pool.user_supply_shares(supplier_cap_id); + let amount_before = pool.user_supply_amount(supplier_cap_id, &clock); + + // Verify interest accrued: amount > initial supply + assert!(amount_before > 10 * test_constants::usdc_multiplier()); + + // Try to withdraw 1 token (very small compared to total) + let withdrawn = pool.withdraw( + ®istry, + &supplier_cap, + option::some(1), + &clock, + scenario.ctx(), + ); + + // Verify we got exactly 1 token + assert_eq!(withdrawn.value(), 1); + + // Verify exactly 1 share was burned (rounded up from fractional share) + let shares_after = pool.user_supply_shares(supplier_cap_id); + assert_eq!(shares_after, shares_before - 1); + + // Cleanup + destroy(borrow_coin); + destroy(withdrawn); + destroy(supplier_cap); + test::return_shared(pool); + cleanup_test(registry, admin_cap, maintainer_cap, clock, scenario); +} + +#[test] +fun test_total_supply_with_interest_no_borrow() { + let (mut scenario, clock, admin_cap, maintainer_cap, pool_id) = setup_test(); + let mut pool = scenario.take_shared_by_id>(pool_id); + let registry = scenario.take_shared(); + + scenario.next_tx(test_constants::user1()); + let supply_amount = 100 * test_constants::usdc_multiplier(); + let supplier_cap = test_helpers::supply_to_pool( + &mut pool, + ®istry, + supply_amount, + &clock, + scenario.ctx(), + ); + + // With no borrows, total_supply should equal total_supply_with_interest + let raw_supply = pool.total_supply(); + let supply_with_interest = pool.total_supply_with_interest(&clock); + assert_eq!(raw_supply, supply_amount); + assert_eq!(supply_with_interest, supply_amount); + + destroy(supplier_cap); + test::return_shared(pool); + cleanup_test(registry, admin_cap, maintainer_cap, clock, scenario); +} + +#[test] +fun test_total_supply_with_interest_after_year() { + let (mut scenario, mut clock, admin_cap, maintainer_cap, pool_id) = setup_test(); + let mut pool = scenario.take_shared_by_id>(pool_id); + let registry = scenario.take_shared(); + + // Supply 100 USDC + scenario.next_tx(test_constants::user1()); + let supply_amount = 100 * test_constants::usdc_multiplier(); + let supplier_cap = test_helpers::supply_to_pool( + &mut pool, + ®istry, + supply_amount, + &clock, + scenario.ctx(), + ); + + // Borrow 50 USDC (50% utilization) + scenario.next_tx(test_constants::user2()); + let borrow_amount = 50 * test_constants::usdc_multiplier(); + let borrowed_coin = test_borrow( + &mut pool, + borrow_amount, + &clock, + scenario.ctx(), + ); + + // Record initial values + let initial_supply = pool.total_supply(); + assert_eq!(initial_supply, supply_amount); + + // Advance time by 1 year + advance_time(&mut clock, margin_constants::year_ms()); + + // total_supply should still be the raw supply (not updated yet) + let raw_supply = pool.total_supply(); + assert_eq!(raw_supply, initial_supply); + + // total_supply_with_interest should include accrued interest + let supply_with_interest = pool.total_supply_with_interest(&clock); + let true_interest_rate = pool.true_interest_rate(); + + // Verify that supply_with_interest > raw_supply (interest has accrued) + assert_eq!( + supply_with_interest, + math::mul(raw_supply, constants::float_scaling() + true_interest_rate), + ); + + destroy(borrowed_coin); + destroy(supplier_cap); + test::return_shared(pool); + cleanup_test(registry, admin_cap, maintainer_cap, clock, scenario); +} + +#[test] +fun test_total_supply_with_interest_high_utilization() { + let (mut scenario, mut clock, admin_cap, maintainer_cap, pool_id) = setup_test(); + let mut pool = scenario.take_shared_by_id>(pool_id); + let registry = scenario.take_shared(); + + // Supply 100 USDC + scenario.next_tx(test_constants::user1()); + let supply_amount = 100 * test_constants::usdc_multiplier(); + let supplier_cap = test_helpers::supply_to_pool( + &mut pool, + ®istry, + supply_amount, + &clock, + scenario.ctx(), + ); + + // Borrow 79 USDC (79% utilization, close to optimal 80%) + scenario.next_tx(test_constants::user2()); + let borrow_amount = 79 * test_constants::usdc_multiplier(); + let borrowed_coin = test_borrow( + &mut pool, + borrow_amount, + &clock, + scenario.ctx(), + ); + + // Record initial values + let initial_supply = pool.total_supply(); + + // Advance time by 1 year + advance_time(&mut clock, margin_constants::year_ms()); + + // Raw supply should not have changed + let raw_supply_after_year = pool.total_supply(); + assert_eq!(raw_supply_after_year, initial_supply); + + // total_supply_with_interest should include accrued interest + let supply_with_interest = pool.total_supply_with_interest(&clock); + let true_interest_rate = pool.true_interest_rate(); + + // Verify exact calculation with true interest rate + assert_eq!( + supply_with_interest, + math::mul(raw_supply_after_year, constants::float_scaling() + true_interest_rate), + ); + + destroy(borrowed_coin); + destroy(supplier_cap); + test::return_shared(pool); + cleanup_test(registry, admin_cap, maintainer_cap, clock, scenario); +} + +#[test] +fun test_total_supply_with_interest_vs_update() { + let (mut scenario, mut clock, admin_cap, maintainer_cap, pool_id) = setup_test(); + let mut pool = scenario.take_shared_by_id>(pool_id); + let registry = scenario.take_shared(); + + // Supply 100 USDC + scenario.next_tx(test_constants::user1()); + let supply_amount = 100 * test_constants::usdc_multiplier(); + let supplier_cap = test_helpers::supply_to_pool( + &mut pool, + ®istry, + supply_amount, + &clock, + scenario.ctx(), + ); + + // Borrow 60 USDC (60% utilization) + scenario.next_tx(test_constants::user2()); + let borrow_amount = 60 * test_constants::usdc_multiplier(); + let borrowed_coin = test_borrow( + &mut pool, + borrow_amount, + &clock, + scenario.ctx(), + ); + + // Advance time by 1 year + advance_time(&mut clock, margin_constants::year_ms()); + + // Get supply with interest (without updating state) + let raw_supply_before = pool.total_supply(); + let supply_with_interest_before_update = pool.total_supply_with_interest(&clock); + let true_interest_rate = pool.true_interest_rate(); + + // Verify exact calculation with true interest rate + assert_eq!( + supply_with_interest_before_update, + math::mul(raw_supply_before, constants::float_scaling() + true_interest_rate), + ); + + // Now actually update the state by withdrawing + scenario.next_tx(test_constants::user1()); + let withdrawn = pool.withdraw( + ®istry, + &supplier_cap, + option::some(1), // Withdraw minimal amount to trigger state update + &clock, + scenario.ctx(), + ); + + // After update, raw supply should now include the interest (minus withdrawn amount) + let raw_supply_after = pool.total_supply(); + let withdrawn_amount = withdrawn.value(); + let expected_supply_after = supply_with_interest_before_update - withdrawn_amount; + + // Verify the supply after update matches our prediction + assert_eq!(raw_supply_after, expected_supply_after); + + destroy(withdrawn); + destroy(borrowed_coin); + destroy(supplier_cap); + test::return_shared(pool); + cleanup_test(registry, admin_cap, maintainer_cap, clock, scenario); +} + +/// Test that withdrawing a tiny amount still burns at least 1 share. +#[test] +fun test_tiny_withdraw_burns_at_least_one_share() { + let (mut scenario, clock, admin_cap, maintainer_cap, pool_id) = setup_test(); + + scenario.next_tx(test_constants::admin()); + let mut pool = scenario.take_shared_by_id>(pool_id); + let registry = scenario.take_shared(); + + // User supplies a large amount: 1,000,000 USDC = 10^12 units + scenario.next_tx(test_constants::user1()); + let large_supply = 1_000_000 * test_constants::usdc_multiplier(); // 10^12 units + let supplier_cap = test_helpers::supply_to_pool( + &mut pool, + ®istry, + large_supply, + &clock, + scenario.ctx(), + ); + + let supplier_cap_id = object::id(&supplier_cap); + let shares_before = pool.user_supply_shares(supplier_cap_id); + + // Verify initial shares equal supply (1:1 ratio at start) + assert_eq!(shares_before, large_supply); + + // Withdraw just 1 unit - this would have burned 0 shares before the fix + // because: div(1, 10^12) = (1 * 10^9) / 10^12 = 0 (floor division) + let withdrawn = pool.withdraw( + ®istry, + &supplier_cap, + option::some(1), + &clock, + scenario.ctx(), + ); + + // Verify we received exactly 1 unit + assert_eq!(withdrawn.value(), 1); + + let shares_after = pool.user_supply_shares(supplier_cap_id); + let shares_burned = shares_before - shares_after; + + assert!(shares_burned >= 1); + + destroy(withdrawn); + destroy(supplier_cap); + test::return_shared(pool); + cleanup_test(registry, admin_cap, maintainer_cap, clock, scenario); +} + +#[test] +/// Test that update_margin_pool_config accrues interest using old params before applying new config. +fun test_update_margin_pool_config_accrues_interest_with_old_params() { + let (mut scenario, mut clock, admin_cap, maintainer_cap, pool_id) = setup_test(); + let mut pool = scenario.take_shared_by_id>(pool_id); + let registry = scenario.take_shared(); + let margin_pool_cap = scenario.take_from_sender(); + + // Verify initial protocol_spread is 10% + let old_protocol_spread = pool.protocol_spread(); + assert_eq!(old_protocol_spread, test_constants::protocol_spread()); + + // Supply 100 USDC + scenario.next_tx(test_constants::user1()); + let supply_amount = 100 * test_constants::usdc_multiplier(); + let supplier_cap = test_helpers::supply_to_pool( + &mut pool, + ®istry, + supply_amount, + &clock, + scenario.ctx(), + ); + + // Borrow 60 USDC (60% utilization) + scenario.next_tx(test_constants::user2()); + let borrow_amount = 60 * test_constants::usdc_multiplier(); + let borrowed_coin = test_borrow(&mut pool, borrow_amount, &clock, scenario.ctx()); + + // Advance time by 1 year to accrue interest + advance_time(&mut clock, margin_constants::year_ms()); + + // Get expected supply with interest before config update (using old protocol_spread) + let supply_with_interest_before = pool.total_supply_with_interest(&clock); + let raw_supply_before = pool.total_supply(); + let protocol_fees_before = pool.protocol_fees().protocol_fees(); + + // Supply with interest should be greater than raw supply (interest has accrued) + assert!(supply_with_interest_before > raw_supply_before); + + // Now update margin_pool_config with new protocol_spread (5% instead of 10%) + scenario.next_tx(test_constants::admin()); + let new_protocol_spread = 50_000_000; // 5% + let new_margin_pool_config = protocol_config::new_margin_pool_config( + test_constants::supply_cap(), + test_constants::max_utilization_rate(), + new_protocol_spread, + test_constants::min_borrow(), + ); + + pool.update_margin_pool_config(®istry, new_margin_pool_config, &margin_pool_cap, &clock); + + // After config update, interest should have been accrued using old protocol_spread + let raw_supply_after = pool.total_supply(); + let protocol_fees_after = pool.protocol_fees().protocol_fees(); + + // Verify state was updated: raw supply should now include accrued interest + assert_eq!(raw_supply_after, supply_with_interest_before); + + // Verify protocol fees increased (interest was calculated with old protocol_spread) + assert!(protocol_fees_after > protocol_fees_before); + + // Verify new protocol_spread is in effect + assert_eq!(pool.protocol_spread(), new_protocol_spread); + + scenario.return_to_sender(margin_pool_cap); + destroy(borrowed_coin); + destroy(supplier_cap); + test::return_shared(pool); + cleanup_test(registry, admin_cap, maintainer_cap, clock, scenario); +} diff --git a/packages/deepbook_margin/tests/margin_registry_tests.move b/packages/deepbook_margin/tests/margin_registry_tests.move new file mode 100644 index 000000000..2a198d3f9 --- /dev/null +++ b/packages/deepbook_margin/tests/margin_registry_tests.move @@ -0,0 +1,641 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +#[test_only] +module deepbook_margin::margin_registry_tests; + +use deepbook_margin::{ + margin_constants, + margin_registry::{Self, MarginRegistry, MarginAdminCap, MaintainerCap}, + oracle, + test_constants::{Self, USDC, USDT}, + test_helpers::{Self, default_protocol_config} +}; +use std::unit_test::destroy; +use sui::{clock::Clock, test_scenario::{Scenario, return_shared}}; + +fun setup_test_with_margin_pools(): (Scenario, Clock, MarginAdminCap, MaintainerCap, ID, ID) { + let (mut scenario, admin_cap) = test_helpers::setup_test(); + + scenario.next_tx(test_constants::admin()); + let mut registry = scenario.take_shared(); + let clock = scenario.take_shared(); + let maintainer_cap = margin_registry::mint_maintainer_cap( + &mut registry, + &admin_cap, + &clock, + scenario.ctx(), + ); + return_shared(registry); + + // Create margin pools for USDC and USDT + let protocol_config = default_protocol_config(); + let usdc_pool_id = test_helpers::create_margin_pool( + &mut scenario, + &maintainer_cap, + protocol_config, + &clock, + ); + let usdt_pool_id = test_helpers::create_margin_pool( + &mut scenario, + &maintainer_cap, + protocol_config, + &clock, + ); + + (scenario, clock, admin_cap, maintainer_cap, usdc_pool_id, usdt_pool_id) +} + +fun create_mock_deepbook_pool_id(): ID { + sui::object::id_from_address(@0x1234567890abcdef) +} + +fun cleanup_test( + registry: MarginRegistry, + admin_cap: MarginAdminCap, + maintainer_cap: MaintainerCap, + clock: Clock, + scenario: Scenario, +) { + destroy(registry); + destroy(admin_cap); + destroy(maintainer_cap); + destroy(clock); + scenario.end(); +} + +#[test] +fun test_mint_maintainer_cap_ok() { + let ( + mut scenario, + clock, + admin_cap, + maintainer_cap, + _usdc_pool_id, + _usdt_pool_id, + ) = setup_test_with_margin_pools(); + + scenario.next_tx(test_constants::admin()); + let mut registry = scenario.take_shared(); + + // Mint a new maintainer cap + let new_maintainer_cap = registry.mint_maintainer_cap(&admin_cap, &clock, scenario.ctx()); + + // Verify cap was created successfully (just ensure it doesn't abort) + destroy(new_maintainer_cap); + + cleanup_test(registry, admin_cap, maintainer_cap, clock, scenario); +} + +#[test] +fun test_revoke_maintainer_cap_ok() { + let ( + mut scenario, + clock, + admin_cap, + maintainer_cap, + _usdc_pool_id, + _usdt_pool_id, + ) = setup_test_with_margin_pools(); + + scenario.next_tx(test_constants::admin()); + let mut registry = scenario.take_shared(); + let maintainer_cap_id = sui::object::id(&maintainer_cap); + + // Revoke the maintainer cap + registry.revoke_maintainer_cap(&admin_cap, maintainer_cap_id, &clock); + + cleanup_test(registry, admin_cap, maintainer_cap, clock, scenario); +} + +#[test, expected_failure(abort_code = margin_registry::EMaintainerCapNotValid)] +fun test_revoke_random_cap_should_fail() { + let ( + mut scenario, + clock, + admin_cap, + maintainer_cap, + _usdc_pool_id, + _usdt_pool_id, + ) = setup_test_with_margin_pools(); + + scenario.next_tx(test_constants::admin()); + let mut registry = scenario.take_shared(); + + // Try to revoke a random ID that was never a maintainer cap + let random_id = sui::object::id_from_address(@0x123); + registry.revoke_maintainer_cap(&admin_cap, random_id, &clock); + + cleanup_test(registry, admin_cap, maintainer_cap, clock, scenario); +} + +#[test] +fun test_register_deepbook_pool_ok() { + let ( + mut scenario, + clock, + admin_cap, + maintainer_cap, + _usdc_pool_id, + _usdt_pool_id, + ) = setup_test_with_margin_pools(); + + scenario.next_tx(test_constants::admin()); + let registry = scenario.take_shared(); + let _mock_pool_id = create_mock_deepbook_pool_id(); + + // Create a valid pool config + let pool_config = registry.new_pool_config( + 2_000_000_000, // min_withdraw_risk_ratio: 2.0 + 1_500_000_000, // min_borrow_risk_ratio: 1.5 + 1_100_000_000, // liquidation_risk_ratio: 1.1 + 1_250_000_000, // target_liquidation_risk_ratio: 1.25 + 20_000_000, // user_liquidation_reward: 2% + 30_000_000, // pool_liquidation_reward: 3% + ); + + // Register the pool using mock pool (we can't create a real Pool object easily) + // This test verifies the pool config creation works + destroy(pool_config); + + cleanup_test(registry, admin_cap, maintainer_cap, clock, scenario); +} + +#[test] +fun test_new_pool_config_ok() { + let ( + mut scenario, + clock, + admin_cap, + maintainer_cap, + _usdc_pool_id, + _usdt_pool_id, + ) = setup_test_with_margin_pools(); + + scenario.next_tx(test_constants::admin()); + let registry = scenario.take_shared(); + + // Create a valid pool config + let pool_config = registry.new_pool_config( + 2_000_000_000, // min_withdraw_risk_ratio: 2.0 + 1_500_000_000, // min_borrow_risk_ratio: 1.5 + 1_100_000_000, // liquidation_risk_ratio: 1.1 + 1_250_000_000, // target_liquidation_risk_ratio: 1.25 + 20_000_000, // user_liquidation_reward: 2% + 30_000_000, // pool_liquidation_reward: 3% + ); + + // Verify config was created (it should not abort) + destroy(pool_config); + + cleanup_test(registry, admin_cap, maintainer_cap, clock, scenario); +} + +// Test all the invalid parameter scenarios for new_pool_config +#[test, expected_failure(abort_code = margin_registry::EInvalidRiskParam)] +fun test_new_pool_config_invalid_borrow_vs_withdraw() { + let ( + mut scenario, + clock, + admin_cap, + maintainer_cap, + _usdc_pool_id, + _usdt_pool_id, + ) = setup_test_with_margin_pools(); + + scenario.next_tx(test_constants::admin()); + let registry = scenario.take_shared(); + + // Invalid: min_borrow_risk_ratio >= min_withdraw_risk_ratio + let pool_config = registry.new_pool_config( + 1_500_000_000, // min_withdraw_risk_ratio: 1.5 + 1_500_000_000, // min_borrow_risk_ratio: 1.5 (should be < withdraw) + 1_100_000_000, + 1_250_000_000, + 20_000_000, + 30_000_000, + ); + + destroy(pool_config); + cleanup_test(registry, admin_cap, maintainer_cap, clock, scenario); +} + +#[test, expected_failure(abort_code = margin_registry::EInvalidRiskParam)] +fun test_new_pool_config_invalid_liquidation_vs_borrow() { + let ( + mut scenario, + clock, + admin_cap, + maintainer_cap, + _usdc_pool_id, + _usdt_pool_id, + ) = setup_test_with_margin_pools(); + + scenario.next_tx(test_constants::admin()); + let registry = scenario.take_shared(); + + // Invalid: liquidation_risk_ratio >= min_borrow_risk_ratio + let pool_config = registry.new_pool_config( + 2_000_000_000, + 1_500_000_000, + 1_500_000_000, // liquidation_risk_ratio: 1.5 (should be < borrow) + 1_600_000_000, + 20_000_000, + 30_000_000, + ); + + destroy(pool_config); + cleanup_test(registry, admin_cap, maintainer_cap, clock, scenario); +} + +#[test, expected_failure(abort_code = margin_registry::EInvalidRiskParam)] +fun test_new_pool_config_invalid_liquidation_vs_target() { + let ( + mut scenario, + clock, + admin_cap, + maintainer_cap, + _usdc_pool_id, + _usdt_pool_id, + ) = setup_test_with_margin_pools(); + + scenario.next_tx(test_constants::admin()); + let registry = scenario.take_shared(); + + // Invalid: liquidation_risk_ratio >= target_liquidation_risk_ratio + let pool_config = registry.new_pool_config( + 2_000_000_000, + 1_500_000_000, + 1_200_000_000, // liquidation_risk_ratio: 1.2 + 1_200_000_000, // target_liquidation_risk_ratio: 1.2 (should be > liquidation) + 20_000_000, + 30_000_000, + ); + + destroy(pool_config); + cleanup_test(registry, admin_cap, maintainer_cap, clock, scenario); +} + +#[test, expected_failure(abort_code = margin_registry::EInvalidRiskParam)] +fun test_new_pool_config_liquidation_too_low() { + let ( + mut scenario, + clock, + admin_cap, + maintainer_cap, + _usdc_pool_id, + _usdt_pool_id, + ) = setup_test_with_margin_pools(); + + scenario.next_tx(test_constants::admin()); + let registry = scenario.take_shared(); + + // Invalid: liquidation_risk_ratio < constants::float_scaling() (1.0) + let pool_config = registry.new_pool_config( + 2_000_000_000, + 1_500_000_000, + 900_000_000, // liquidation_risk_ratio: 0.9 (should be >= 1.0) + 1_250_000_000, + 20_000_000, + 30_000_000, + ); + + destroy(pool_config); + cleanup_test(registry, admin_cap, maintainer_cap, clock, scenario); +} + +#[test, expected_failure(abort_code = margin_registry::EInvalidRiskParam)] +fun test_new_pool_config_user_reward_too_high() { + let ( + mut scenario, + clock, + admin_cap, + maintainer_cap, + _usdc_pool_id, + _usdt_pool_id, + ) = setup_test_with_margin_pools(); + + scenario.next_tx(test_constants::admin()); + let registry = scenario.take_shared(); + + // Invalid: user_liquidation_reward > constants::float_scaling() (100%) + let pool_config = registry.new_pool_config( + 2_000_000_000, + 1_500_000_000, + 1_100_000_000, + 1_250_000_000, + 1_100_000_000, // user_liquidation_reward: 110% (should be <= 100%) + 30_000_000, + ); + + destroy(pool_config); + cleanup_test(registry, admin_cap, maintainer_cap, clock, scenario); +} + +#[test, expected_failure(abort_code = margin_registry::EInvalidRiskParam)] +fun test_new_pool_config_pool_reward_too_high() { + let ( + mut scenario, + clock, + admin_cap, + maintainer_cap, + _usdc_pool_id, + _usdt_pool_id, + ) = setup_test_with_margin_pools(); + + scenario.next_tx(test_constants::admin()); + let registry = scenario.take_shared(); + + // Invalid: pool_liquidation_reward > constants::float_scaling() (100%) + let pool_config = registry.new_pool_config( + 2_000_000_000, + 1_500_000_000, + 1_100_000_000, + 1_250_000_000, + 20_000_000, + 1_100_000_000, // pool_liquidation_reward: 110% (should be <= 100%) + ); + + destroy(pool_config); + cleanup_test(registry, admin_cap, maintainer_cap, clock, scenario); +} + +#[test, expected_failure(abort_code = margin_registry::EInvalidRiskParam)] +fun test_new_pool_config_combined_rewards_too_high() { + let ( + mut scenario, + clock, + admin_cap, + maintainer_cap, + _usdc_pool_id, + _usdt_pool_id, + ) = setup_test_with_margin_pools(); + + scenario.next_tx(test_constants::admin()); + let registry = scenario.take_shared(); + + // Invalid: user_liquidation_reward + pool_liquidation_reward > 100% + let pool_config = registry.new_pool_config( + 2_000_000_000, + 1_500_000_000, + 1_100_000_000, + 1_250_000_000, + 600_000_000, // user_liquidation_reward: 60% + 500_000_000, // pool_liquidation_reward: 50% (total 110% > 100%) + ); + + destroy(pool_config); + cleanup_test(registry, admin_cap, maintainer_cap, clock, scenario); +} + +#[test, expected_failure(abort_code = margin_registry::EInvalidRiskParam)] +fun test_new_pool_config_target_too_low() { + let ( + mut scenario, + clock, + admin_cap, + maintainer_cap, + _usdc_pool_id, + _usdt_pool_id, + ) = setup_test_with_margin_pools(); + + scenario.next_tx(test_constants::admin()); + let registry = scenario.take_shared(); + + // Invalid: target_liquidation_risk_ratio <= 1.0 + user_reward + pool_reward + let pool_config = registry.new_pool_config( + 2_000_000_000, + 1_500_000_000, + 1_100_000_000, + 1_040_000_000, // target: 1.04, but 1.0 + 0.02 + 0.03 = 1.05, so target should be > 1.05 + 20_000_000, + 30_000_000, + ); + + destroy(pool_config); + cleanup_test(registry, admin_cap, maintainer_cap, clock, scenario); +} + +#[test] +fun test_new_pool_config_with_leverage_ok() { + let ( + mut scenario, + clock, + admin_cap, + maintainer_cap, + _usdc_pool_id, + _usdt_pool_id, + ) = setup_test_with_margin_pools(); + + scenario.next_tx(test_constants::admin()); + let registry = scenario.take_shared(); + + // Create a valid pool config with 5x leverage + let pool_config = registry.new_pool_config_with_leverage(5_000_000_000); + + // Verify config was created (it should not abort) + destroy(pool_config); + + cleanup_test(registry, admin_cap, maintainer_cap, clock, scenario); +} + +#[test, expected_failure(abort_code = margin_registry::EInvalidRiskParam)] +fun test_new_pool_config_with_leverage_too_low() { + let ( + mut scenario, + clock, + admin_cap, + maintainer_cap, + _usdc_pool_id, + _usdt_pool_id, + ) = setup_test_with_margin_pools(); + + scenario.next_tx(test_constants::admin()); + let registry = scenario.take_shared(); + + // Invalid: leverage <= margin_constants::min_leverage() + let pool_config = registry.new_pool_config_with_leverage( + margin_constants::min_leverage(), // Should be > min_leverage + ); + + destroy(pool_config); + cleanup_test(registry, admin_cap, maintainer_cap, clock, scenario); +} + +#[test, expected_failure(abort_code = margin_registry::EInvalidRiskParam)] +fun test_new_pool_config_with_leverage_too_high() { + let ( + mut scenario, + clock, + admin_cap, + maintainer_cap, + _usdc_pool_id, + _usdt_pool_id, + ) = setup_test_with_margin_pools(); + + scenario.next_tx(test_constants::admin()); + let registry = scenario.take_shared(); + + // Invalid: leverage > margin_constants::max_leverage() + let pool_config = registry.new_pool_config_with_leverage( + margin_constants::max_leverage() + 1, // Should be <= max_leverage + ); + + destroy(pool_config); + cleanup_test(registry, admin_cap, maintainer_cap, clock, scenario); +} + +#[test, expected_failure(abort_code = pyth::pyth::E_STALE_PRICE_UPDATE)] +fun test_oracle_max_age_exceeded() { + let ( + mut scenario, + mut clock, + admin_cap, + maintainer_cap, + _usdc_pool_id, + _usdt_pool_id, + ) = setup_test_with_margin_pools(); + + scenario.next_tx(test_constants::admin()); + let mut registry = scenario.take_shared(); + + let pyth_config = test_helpers::create_test_pyth_config(); + registry.add_config(&admin_cap, pyth_config); + + let current_time_ms = 10000000; // 10 million milliseconds = 10,000 seconds + clock.set_for_testing(current_time_ms); + + // Create a price info object with timestamp that's older than 60 seconds + let old_timestamp_seconds = (current_time_ms / 1000) - 65; // 65 seconds ago + + let old_price_info = test_helpers::build_pyth_price_info_object( + &mut scenario, + test_constants::usdc_price_feed_id(), + 1 * test_constants::pyth_multiplier(), // $1.00 price + 50000, // confidence + test_constants::pyth_decimals(), // exponent + old_timestamp_seconds, // timestamp 70 seconds ago + ); + + // This should fail with Pyth error because price is older than 60 seconds + let _usd_value = oracle::calculate_usd_price( + &old_price_info, + ®istry, + 1000000, // 1 USDC (6 decimals) + &clock, + ); + + destroy(old_price_info); + cleanup_test(registry, admin_cap, maintainer_cap, clock, scenario); +} + +#[test] +fun test_oracle_max_age_within_limit() { + let ( + mut scenario, + mut clock, + admin_cap, + maintainer_cap, + _usdc_pool_id, + _usdt_pool_id, + ) = setup_test_with_margin_pools(); + + scenario.next_tx(test_constants::admin()); + let mut registry = scenario.take_shared(); + + let pyth_config = test_helpers::create_test_pyth_config(); + registry.add_config(&admin_cap, pyth_config); + + let current_time_ms = 10000000; // 10 million milliseconds = 10,000 seconds + clock.set_for_testing(current_time_ms); + + // Create a price info object with recent timestamp (30 seconds ago, within 60 second limit) + let recent_timestamp_seconds = (current_time_ms / 1000) - 30; // 30 seconds ago + + let recent_price_info = test_helpers::build_pyth_price_info_object( + &mut scenario, + test_constants::usdc_price_feed_id(), + 1 * test_constants::pyth_multiplier(), // $1.00 price + 50000, // confidence + test_constants::pyth_decimals(), // exponent + recent_timestamp_seconds, // timestamp 30 seconds ago + ); + + // This should succeed because price is within 60 second limit + let usd_value = oracle::calculate_usd_price( + &recent_price_info, + ®istry, + 1000000, // 1 USDC (6 decimals) + &clock, + ); + assert!(usd_value > 900_000_000 && usd_value < 1_100_000_000); + + destroy(recent_price_info); + cleanup_test(registry, admin_cap, maintainer_cap, clock, scenario); +} + +#[test] +fun test_disable_version_with_pause_cap_ok() { + let ( + mut scenario, + clock, + admin_cap, + maintainer_cap, + _usdc_pool_id, + _usdt_pool_id, + ) = setup_test_with_margin_pools(); + + scenario.next_tx(test_constants::admin()); + let mut registry = scenario.take_shared(); + + // Mint a pause cap + let pause_cap = registry.mint_pause_cap(&admin_cap, &clock, scenario.ctx()); + + // Enable a new version so we can disable it + let new_version = margin_constants::margin_version() + 1; + registry.enable_version(new_version, &admin_cap); + + // Should succeed: disable version with valid pause cap + registry.disable_version_pause_cap(new_version, &pause_cap); + + destroy(pause_cap); + cleanup_test(registry, admin_cap, maintainer_cap, clock, scenario); +} + +#[test, expected_failure(abort_code = margin_registry::EPauseCapNotValid)] +fun test_disable_version_with_revoked_pause_cap_fails() { + let ( + mut scenario, + clock, + admin_cap, + maintainer_cap, + _usdc_pool_id, + _usdt_pool_id, + ) = setup_test_with_margin_pools(); + + scenario.next_tx(test_constants::admin()); + let mut registry = scenario.take_shared(); + + // Mint a pause cap + let pause_cap = registry.mint_pause_cap(&admin_cap, &clock, scenario.ctx()); + let pause_cap_id = sui::object::id(&pause_cap); + + // Enable a new version so we can disable it + let new_version = margin_constants::margin_version() + 1; + registry.enable_version(new_version, &admin_cap); + + // First disable succeeds with valid pause cap + registry.disable_version_pause_cap(new_version, &pause_cap); + + // Re-enable the version so we can try to disable it again + registry.enable_version(new_version, &admin_cap); + + // Revoke the pause cap + registry.revoke_pause_cap(&admin_cap, &clock, pause_cap_id); + + // Should fail: trying to use a revoked pause cap + registry.disable_version_pause_cap(new_version, &pause_cap); + + destroy(pause_cap); + cleanup_test(registry, admin_cap, maintainer_cap, clock, scenario); +} diff --git a/packages/deepbook_margin/tests/pool_proxy_tests.move b/packages/deepbook_margin/tests/pool_proxy_tests.move new file mode 100644 index 000000000..0759726b8 --- /dev/null +++ b/packages/deepbook_margin/tests/pool_proxy_tests.move @@ -0,0 +1,1937 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +#[test_only] +module deepbook_margin::pool_proxy_tests; + +use deepbook::{constants, pool::Pool, registry::Registry}; +use deepbook_margin::{ + margin_manager::{Self, MarginManager}, + margin_pool::MarginPool, + margin_registry::MarginRegistry, + pool_proxy, + test_constants::{Self, USDC, USDT}, + test_helpers::{ + setup_pool_proxy_test_env, + setup_margin_registry, + create_margin_pool, + create_pool_for_testing, + enable_deepbook_margin_on_pool, + default_protocol_config, + cleanup_margin_test, + mint_coin, + destroy_2, + return_shared_2, + return_shared_3, + build_demo_usdc_price_info_object, + build_demo_usdt_price_info_object + } +}; +use std::unit_test::destroy; +use sui::test_scenario::return_shared; +use token::deep::DEEP; + +// === Place Limit Order Tests === +#[test] +fun test_place_limit_order_ok() { + let ( + mut scenario, + clock, + _admin_cap, + _maintainer_cap, + _base_pool_id, + _quote_pool_id, + pool_id, + registry_id, + ) = setup_pool_proxy_test_env(); + + scenario.next_tx(test_constants::user1()); + let mut pool = scenario.take_shared_by_id>(pool_id); + let mut registry = scenario.take_shared(); + let deepbook_registry = scenario.take_shared_by_id(registry_id); + margin_manager::new( + &pool, + &deepbook_registry, + &mut registry, + &clock, + scenario.ctx(), + ); + return_shared(deepbook_registry); + + scenario.next_tx(test_constants::user1()); + let mut mm = scenario.take_shared>(); + let usdc_price = build_demo_usdc_price_info_object(&mut scenario, &clock); + let usdt_price = build_demo_usdt_price_info_object(&mut scenario, &clock); + + // Deposit some collateral + mm.deposit( + ®istry, + &usdc_price, + &usdt_price, + mint_coin(10000 * test_constants::usdc_multiplier(), scenario.ctx()), + &clock, + scenario.ctx(), + ); + destroy_2!(usdc_price, usdt_price); + + // Place a limit order successfully + let order_info = pool_proxy::place_limit_order( + ®istry, + &mut mm, + &mut pool, + 1, // client_order_id + constants::no_restriction(), + constants::self_matching_allowed(), + 1_000_000, // price + 100 * test_constants::usdc_multiplier(), // quantity + false, // is_bid (sell USDC for USDT) + false, // pay_with_deep + 2000000, // expire_timestamp + &clock, + scenario.ctx(), + ); + + // Verify the order was placed (basic sanity check) + destroy(order_info); + + return_shared_2!(mm, pool); + cleanup_margin_test(registry, _admin_cap, _maintainer_cap, clock, scenario); +} + +#[test, expected_failure(abort_code = pool_proxy::EIncorrectDeepBookPool)] +fun test_place_limit_order_incorrect_pool() { + let ( + mut scenario, + clock, + _admin_cap, + _maintainer_cap, + _base_pool_id, + _quote_pool_id, + pool_id, + registry_id, + ) = setup_pool_proxy_test_env(); + + // Create a wrong pool + let (wrong_pool_id, _wrong_registry_id) = create_pool_for_testing(&mut scenario); + + scenario.next_tx(test_constants::user1()); + let pool = scenario.take_shared_by_id>(pool_id); + let mut wrong_pool = scenario.take_shared_by_id>(wrong_pool_id); + let mut registry = scenario.take_shared(); + let deepbook_registry = scenario.take_shared_by_id(registry_id); + margin_manager::new( + &pool, + &deepbook_registry, + &mut registry, + &clock, + scenario.ctx(), + ); + return_shared(deepbook_registry); + + scenario.next_tx(test_constants::user1()); + let mut mm = scenario.take_shared>(); + + // Try to place order with wrong pool - should fail + pool_proxy::place_limit_order( + ®istry, + &mut mm, + &mut wrong_pool, // Wrong pool! + 1, + constants::no_restriction(), + constants::self_matching_allowed(), + 1_000_000, + 100, + true, + false, + 0, + &clock, + scenario.ctx(), + ); + + abort +} + +#[test, expected_failure(abort_code = pool_proxy::EIncorrectDeepBookPool)] +fun test_place_limit_order_pool_not_enabled() { + let (mut scenario, clock, admin_cap, maintainer_cap) = setup_margin_registry(); + + // Create a margin pool + create_margin_pool(&mut scenario, &maintainer_cap, default_protocol_config(), &clock); + create_margin_pool(&mut scenario, &maintainer_cap, default_protocol_config(), &clock); + + // Create a pool that is NOT enabled for margin trading + let (non_margin_pool_id, _non_margin_registry_id) = create_pool_for_testing( + &mut scenario, + ); + + // Create another pool that IS enabled for margin trading + let (margin_pool_id, margin_registry_id) = create_pool_for_testing(&mut scenario); + + scenario.next_tx(test_constants::admin()); + let mut registry = scenario.take_shared(); + enable_deepbook_margin_on_pool( + margin_pool_id, + &mut registry, + &admin_cap, + &clock, + &mut scenario, + ); + return_shared(registry); + + // Create margin manager with the enabled pool + scenario.next_tx(test_constants::user1()); + let margin_pool = scenario.take_shared_by_id>(margin_pool_id); + let deepbook_registry = scenario.take_shared_by_id(margin_registry_id); + let mut registry = scenario.take_shared(); + margin_manager::new( + &margin_pool, + &deepbook_registry, + &mut registry, + &clock, + scenario.ctx(), + ); + return_shared(deepbook_registry); + + scenario.next_tx(test_constants::user1()); + let mut mm = scenario.take_shared>(); + let mut non_margin_pool = scenario.take_shared_by_id>(non_margin_pool_id); + let usdc_price = build_demo_usdc_price_info_object(&mut scenario, &clock); + let usdt_price = build_demo_usdt_price_info_object(&mut scenario, &clock); + + mm.deposit( + ®istry, + &usdc_price, + &usdt_price, + mint_coin(10000 * test_constants::usdc_multiplier(), scenario.ctx()), + &clock, + scenario.ctx(), + ); + destroy_2!(usdc_price, usdt_price); + + // Try to place order with non-enabled pool - should fail + pool_proxy::place_limit_order( + ®istry, + &mut mm, + &mut non_margin_pool, + 1, + constants::no_restriction(), + constants::self_matching_allowed(), + 1_000_000, + 100 * test_constants::usdc_multiplier(), + false, + false, + 2000000, + &clock, + scenario.ctx(), + ); + + abort +} + +// === Place Market Order Tests === +#[test] +fun test_place_market_order_ok() { + let ( + mut scenario, + clock, + _admin_cap, + _maintainer_cap, + _base_pool_id, + _quote_pool_id, + pool_id, + registry_id, + ) = setup_pool_proxy_test_env(); + + scenario.next_tx(test_constants::user1()); + let mut pool = scenario.take_shared_by_id>(pool_id); + let mut registry = scenario.take_shared(); + let deepbook_registry = scenario.take_shared_by_id(registry_id); + margin_manager::new( + &pool, + &deepbook_registry, + &mut registry, + &clock, + scenario.ctx(), + ); + return_shared(deepbook_registry); + + scenario.next_tx(test_constants::user1()); + let mut mm = scenario.take_shared>(); + let usdc_price = build_demo_usdc_price_info_object(&mut scenario, &clock); + let usdt_price = build_demo_usdt_price_info_object(&mut scenario, &clock); + + mm.deposit( + ®istry, + &usdc_price, + &usdt_price, + mint_coin(10000 * test_constants::usdc_multiplier(), scenario.ctx()), + &clock, + scenario.ctx(), + ); + destroy_2!(usdc_price, usdt_price); + + let order_info = pool_proxy::place_market_order( + ®istry, + &mut mm, + &mut pool, + 2, // client_order_id + constants::self_matching_allowed(), + 100 * test_constants::usdc_multiplier(), // quantity + true, // is_bid + false, // pay_with_deep + &clock, + scenario.ctx(), + ); + + destroy(order_info); + return_shared_2!(mm, pool); + cleanup_margin_test(registry, _admin_cap, _maintainer_cap, clock, scenario); +} + +#[test, expected_failure(abort_code = pool_proxy::EIncorrectDeepBookPool)] +fun test_place_market_order_incorrect_pool() { + let ( + mut scenario, + clock, + _admin_cap, + _maintainer_cap, + _base_pool_id, + _quote_pool_id, + pool_id, + registry_id, + ) = setup_pool_proxy_test_env(); + + let (wrong_pool_id, _wrong_registry_id) = create_pool_for_testing(&mut scenario); + + scenario.next_tx(test_constants::user1()); + let pool = scenario.take_shared_by_id>(pool_id); + let mut wrong_pool = scenario.take_shared_by_id>(wrong_pool_id); + let mut registry = scenario.take_shared(); + let deepbook_registry = scenario.take_shared_by_id(registry_id); + margin_manager::new( + &pool, + &deepbook_registry, + &mut registry, + &clock, + scenario.ctx(), + ); + return_shared(deepbook_registry); + + scenario.next_tx(test_constants::user1()); + let mut mm = scenario.take_shared>(); + + pool_proxy::place_market_order( + ®istry, + &mut mm, + &mut wrong_pool, // Wrong pool! + 2, + constants::self_matching_allowed(), + 100, + true, + false, + &clock, + scenario.ctx(), + ); + + abort +} + +#[test, expected_failure(abort_code = pool_proxy::EIncorrectDeepBookPool)] +fun test_place_market_order_pool_not_enabled() { + let (mut scenario, clock, admin_cap, maintainer_cap) = setup_margin_registry(); + + create_margin_pool(&mut scenario, &maintainer_cap, default_protocol_config(), &clock); + create_margin_pool(&mut scenario, &maintainer_cap, default_protocol_config(), &clock); + + let (non_margin_pool_id, _non_margin_registry_id) = create_pool_for_testing( + &mut scenario, + ); + let (margin_pool_id, margin_registry_id) = create_pool_for_testing(&mut scenario); + + scenario.next_tx(test_constants::admin()); + let mut registry = scenario.take_shared(); + enable_deepbook_margin_on_pool( + margin_pool_id, + &mut registry, + &admin_cap, + &clock, + &mut scenario, + ); + return_shared(registry); + + scenario.next_tx(test_constants::user1()); + let margin_pool = scenario.take_shared_by_id>(margin_pool_id); + let deepbook_registry = scenario.take_shared_by_id(margin_registry_id); + let mut registry = scenario.take_shared(); + margin_manager::new( + &margin_pool, + &deepbook_registry, + &mut registry, + &clock, + scenario.ctx(), + ); + return_shared(deepbook_registry); + + scenario.next_tx(test_constants::user1()); + let mut mm = scenario.take_shared>(); + let mut non_margin_pool = scenario.take_shared_by_id>(non_margin_pool_id); + let usdc_price = build_demo_usdc_price_info_object(&mut scenario, &clock); + let usdt_price = build_demo_usdt_price_info_object(&mut scenario, &clock); + + mm.deposit( + ®istry, + &usdc_price, + &usdt_price, + mint_coin(10000 * test_constants::usdc_multiplier(), scenario.ctx()), + &clock, + scenario.ctx(), + ); + destroy_2!(usdc_price, usdt_price); + + pool_proxy::place_market_order( + ®istry, + &mut mm, + &mut non_margin_pool, + 2, + constants::self_matching_allowed(), + 100 * test_constants::usdc_multiplier(), + false, + false, + &clock, + scenario.ctx(), + ); + + abort +} + +// === Place Reduce Only Limit Order Tests === + +#[test] +fun test_place_reduce_only_limit_order_ok() { + let ( + mut scenario, + clock, + _admin_cap, + _maintainer_cap, + base_pool_id, + quote_pool_id, + pool_id, + registry_id, + ) = setup_pool_proxy_test_env(); + + scenario.next_tx(test_constants::user1()); + let mut pool = scenario.take_shared_by_id>(pool_id); + let mut registry = scenario.take_shared(); + let mut base_pool = scenario.take_shared_by_id>(base_pool_id); + let quote_pool = scenario.take_shared_by_id>(quote_pool_id); + let deepbook_registry = scenario.take_shared_by_id(registry_id); + margin_manager::new( + &pool, + &deepbook_registry, + &mut registry, + &clock, + scenario.ctx(), + ); + return_shared(deepbook_registry); + + scenario.next_tx(test_constants::user1()); + let mut mm = scenario.take_shared>(); + let usdc_price = build_demo_usdc_price_info_object(&mut scenario, &clock); + let usdt_price = build_demo_usdt_price_info_object(&mut scenario, &clock); + + // Deposit USDT as collateral + mm.deposit( + ®istry, + &usdc_price, + &usdt_price, + mint_coin(10000 * test_constants::usdt_multiplier(), scenario.ctx()), + &clock, + scenario.ctx(), + ); + + // Borrow USDC to establish a base debt + mm.borrow_base( + ®istry, + &mut base_pool, + &usdc_price, + &usdt_price, + &pool, + 500 * test_constants::usdc_multiplier(), // Borrow 500 USDC + &clock, + scenario.ctx(), + ); + + // Withdraw some USDC so we have debt but less assets (creating a net debt position) + let withdrawn_coin = mm.withdraw( + ®istry, + &base_pool, + "e_pool, + &usdc_price, + &usdt_price, + &pool, + 300 * test_constants::usdc_multiplier(), // Withdraw 300 USDC + &clock, + scenario.ctx(), + ); + + // Destroy the withdrawn coin + destroy(withdrawn_coin); + + // Now place a reduce-only limit order to buy USDC (reducing the debt) + let order_info = pool_proxy::place_reduce_only_limit_order( + ®istry, + &mut mm, + &mut pool, + &base_pool, // Pass base_pool since we have USDC debt + 1, + constants::no_restriction(), + constants::self_matching_allowed(), + 1_100_000, // price + 100 * test_constants::usdc_multiplier(), // quantity (less than debt) + true, // is_bid = true (buying USDC to reduce debt) + false, + 2000000, // expire_timestamp + &clock, + scenario.ctx(), + ); + + // Verify the order was placed successfully + destroy(order_info); + return_shared_3!(mm, pool, base_pool); + return_shared(quote_pool); + destroy(usdc_price); + destroy(usdt_price); + cleanup_margin_test(registry, _admin_cap, _maintainer_cap, clock, scenario); +} + +#[test, expected_failure(abort_code = pool_proxy::EIncorrectDeepBookPool)] +fun test_place_reduce_only_limit_order_incorrect_pool() { + let ( + mut scenario, + clock, + _admin_cap, + _maintainer_cap, + _base_pool_id, + quote_pool_id, + pool_id, + registry_id, + ) = setup_pool_proxy_test_env(); + + let (wrong_pool_id, _wrong_registry_id) = create_pool_for_testing(&mut scenario); + + scenario.next_tx(test_constants::user1()); + let pool = scenario.take_shared_by_id>(pool_id); + let mut wrong_pool = scenario.take_shared_by_id>(wrong_pool_id); + let mut registry = scenario.take_shared(); + let quote_pool = scenario.take_shared_by_id>(quote_pool_id); + + let deepbook_registry = scenario.take_shared_by_id(registry_id); + margin_manager::new( + &pool, + &deepbook_registry, + &mut registry, + &clock, + scenario.ctx(), + ); + return_shared(deepbook_registry); + + scenario.next_tx(test_constants::user1()); + let mut mm = scenario.take_shared>(); + + pool_proxy::place_reduce_only_limit_order( + ®istry, + &mut mm, + &mut wrong_pool, // Wrong pool! + "e_pool, + 3, + constants::no_restriction(), + constants::self_matching_allowed(), + 1_000_000, + 500, + false, + false, + 0, + &clock, + scenario.ctx(), + ); + + abort +} + +#[test, expected_failure(abort_code = pool_proxy::ENotReduceOnlyOrder)] +fun test_place_reduce_only_limit_order_not_reduce_only() { + let ( + mut scenario, + clock, + _admin_cap, + _maintainer_cap, + _base_pool_id, + quote_pool_id, + pool_id, + registry_id, + ) = setup_pool_proxy_test_env(); + + scenario.next_tx(test_constants::user1()); + let mut pool = scenario.take_shared_by_id>(pool_id); + let mut registry = scenario.take_shared(); + let mut quote_pool = scenario.take_shared_by_id>(quote_pool_id); + let deepbook_registry = scenario.take_shared_by_id(registry_id); + margin_manager::new( + &pool, + &deepbook_registry, + &mut registry, + &clock, + scenario.ctx(), + ); + return_shared(deepbook_registry); + + scenario.next_tx(test_constants::user1()); + let mut mm = scenario.take_shared>(); + let usdc_price = build_demo_usdc_price_info_object(&mut scenario, &clock); + let usdt_price = build_demo_usdt_price_info_object(&mut scenario, &clock); + + // Deposit some USDT to use as collateral + mm.deposit( + ®istry, + &usdc_price, + &usdt_price, + mint_coin(10000 * test_constants::usdt_multiplier(), scenario.ctx()), + &clock, + scenario.ctx(), + ); + destroy_2!(usdc_price, usdt_price); + + let usdc_price = build_demo_usdc_price_info_object(&mut scenario, &clock); + let usdt_price = build_demo_usdt_price_info_object(&mut scenario, &clock); + // Borrow some USDT to establish relationship with quote pool + mm.borrow_quote( + ®istry, + &mut quote_pool, + &usdc_price, + &usdt_price, + &pool, + 100 * test_constants::usdt_multiplier(), // Small borrow amount + &clock, + scenario.ctx(), + ); + + // User has no USDC debt but has USDT debt, tries to buy USDC (is_bid = true) + // This should fail because it's not reducing any USDC position - user is increasing exposure + pool_proxy::place_reduce_only_limit_order( + ®istry, + &mut mm, + &mut pool, + "e_pool, // Pass quote_pool since we have USDT debt + 1, + constants::no_restriction(), + constants::self_matching_allowed(), + 1_100_000, // price + 100 * test_constants::usdc_multiplier(), // quantity + true, // is_bid = true (buying USDC) + false, + 2000000, // expire_timestamp + &clock, + scenario.ctx(), + ); + + return_shared_3!(mm, pool, quote_pool); + destroy(usdc_price); + destroy(usdt_price); + cleanup_margin_test(registry, _admin_cap, _maintainer_cap, clock, scenario); +} + +#[test, expected_failure(abort_code = pool_proxy::ENotReduceOnlyOrder)] +fun test_place_reduce_only_limit_order_not_reduce_only_quantity_bid() { + let ( + mut scenario, + clock, + _admin_cap, + _maintainer_cap, + base_pool_id, + quote_pool_id, + pool_id, + registry_id, + ) = setup_pool_proxy_test_env(); + + scenario.next_tx(test_constants::user1()); + let mut pool = scenario.take_shared_by_id>(pool_id); + let mut registry = scenario.take_shared(); + let mut base_pool = scenario.take_shared_by_id>(base_pool_id); + let quote_pool = scenario.take_shared_by_id>(quote_pool_id); + let deepbook_registry = scenario.take_shared_by_id(registry_id); + margin_manager::new( + &pool, + &deepbook_registry, + &mut registry, + &clock, + scenario.ctx(), + ); + return_shared(deepbook_registry); + + scenario.next_tx(test_constants::user1()); + let mut mm = scenario.take_shared>(); + let usdc_price = build_demo_usdc_price_info_object(&mut scenario, &clock); + let usdt_price = build_demo_usdt_price_info_object(&mut scenario, &clock); + + // Deposit some USDT to use as collateral + mm.deposit( + ®istry, + &usdc_price, + &usdt_price, + mint_coin(10000 * test_constants::usdt_multiplier(), scenario.ctx()), + &clock, + scenario.ctx(), + ); + destroy_2!(usdc_price, usdt_price); + + let usdc_price = build_demo_usdc_price_info_object(&mut scenario, &clock); + let usdt_price = build_demo_usdt_price_info_object(&mut scenario, &clock); + // Borrow some USDC + mm.borrow_base( + ®istry, + &mut base_pool, + &usdc_price, + &usdt_price, + &pool, + 100 * test_constants::usdc_multiplier(), // Small borrow amount + &clock, + scenario.ctx(), + ); + + let coin = mm.withdraw( + ®istry, + &base_pool, + "e_pool, // Pass quote_pool since we have USDT debt + &usdc_price, + &usdt_price, + &pool, + 100 * test_constants::usdc_multiplier(), // Withdraw some USDC so we have have net debt + &clock, + scenario.ctx(), + ); + destroy(coin); + + // User has USDC debt, tries to buy more USDC than debt + // This should fail because user is trying to buy more USDC than debt + pool_proxy::place_reduce_only_limit_order( + ®istry, + &mut mm, + &mut pool, + &base_pool, // Pass quote_pool since we have USDT debt + 1, + constants::no_restriction(), + constants::self_matching_allowed(), + constants::float_scaling(), // price + 101 * test_constants::usdc_multiplier(), // quantity + true, // is_bid = true (buying USDC) + false, + 2000000, // expire_timestamp + &clock, + scenario.ctx(), + ); + + return_shared_2!(mm, pool); + return_shared_2!(base_pool, quote_pool); + destroy(usdc_price); + destroy(usdt_price); + cleanup_margin_test(registry, _admin_cap, _maintainer_cap, clock, scenario); +} + +#[test, expected_failure(abort_code = pool_proxy::ENotReduceOnlyOrder)] +fun test_place_reduce_only_limit_order_not_reduce_only_quantity_ask() { + let ( + mut scenario, + clock, + _admin_cap, + _maintainer_cap, + base_pool_id, + quote_pool_id, + pool_id, + registry_id, + ) = setup_pool_proxy_test_env(); + + scenario.next_tx(test_constants::user1()); + let mut pool = scenario.take_shared_by_id>(pool_id); + let mut registry = scenario.take_shared(); + let base_pool = scenario.take_shared_by_id>(base_pool_id); + let mut quote_pool = scenario.take_shared_by_id>(quote_pool_id); + let deepbook_registry = scenario.take_shared_by_id(registry_id); + margin_manager::new( + &pool, + &deepbook_registry, + &mut registry, + &clock, + scenario.ctx(), + ); + return_shared(deepbook_registry); + + scenario.next_tx(test_constants::user1()); + let mut mm = scenario.take_shared>(); + let usdc_price = build_demo_usdc_price_info_object(&mut scenario, &clock); + let usdt_price = build_demo_usdt_price_info_object(&mut scenario, &clock); + + // Deposit some USDC to use as collateral + mm.deposit( + ®istry, + &usdc_price, + &usdt_price, + mint_coin(10000 * test_constants::usdt_multiplier(), scenario.ctx()), + &clock, + scenario.ctx(), + ); + // Borrow some USDT + mm.borrow_quote( + ®istry, + &mut quote_pool, + &usdc_price, + &usdt_price, + &pool, + 100 * test_constants::usdt_multiplier(), // Small borrow amount + &clock, + scenario.ctx(), + ); + + let coin = mm.withdraw( + ®istry, + &base_pool, + "e_pool, // Pass quote_pool since we have USDT debt + &usdc_price, + &usdt_price, + &pool, + 100 * test_constants::usdt_multiplier(), // Withdraw some USDT so we have net debt + &clock, + scenario.ctx(), + ); + destroy(coin); + + // User has USDC debt, tries to buy more USDC than debt + // This should fail because user is trying to buy more USDC than debt + pool_proxy::place_reduce_only_limit_order( + ®istry, + &mut mm, + &mut pool, + "e_pool, // Pass quote_pool since we have USDT debt + 1, + constants::no_restriction(), + constants::self_matching_allowed(), + constants::float_scaling(), // price + 101 * test_constants::usdc_multiplier(), // quantity + false, // is_bid = false (buying USDT) + false, + 2000000, // expire_timestamp + &clock, + scenario.ctx(), + ); + + return_shared_2!(mm, pool); + return_shared_2!(base_pool, quote_pool); + destroy(usdc_price); + destroy(usdt_price); + cleanup_margin_test(registry, _admin_cap, _maintainer_cap, clock, scenario); +} + +// === Place Reduce Only Market Order Tests === + +#[test] +fun test_place_reduce_only_market_order_ok() { + let ( + mut scenario, + clock, + _admin_cap, + _maintainer_cap, + base_pool_id, + quote_pool_id, + pool_id, + registry_id, + ) = setup_pool_proxy_test_env(); + + scenario.next_tx(test_constants::user1()); + let mut pool = scenario.take_shared_by_id>(pool_id); + let mut registry = scenario.take_shared(); + let mut base_pool = scenario.take_shared_by_id>(base_pool_id); + let quote_pool = scenario.take_shared_by_id>(quote_pool_id); + let deepbook_registry = scenario.take_shared_by_id(registry_id); + margin_manager::new( + &pool, + &deepbook_registry, + &mut registry, + &clock, + scenario.ctx(), + ); + return_shared(deepbook_registry); + + scenario.next_tx(test_constants::user1()); + let mut mm = scenario.take_shared>(); + let usdc_price = build_demo_usdc_price_info_object(&mut scenario, &clock); + let usdt_price = build_demo_usdt_price_info_object(&mut scenario, &clock); + + // Deposit USDT as collateral + mm.deposit( + ®istry, + &usdc_price, + &usdt_price, + mint_coin(10000 * test_constants::usdt_multiplier(), scenario.ctx()), + &clock, + scenario.ctx(), + ); + // Borrow USDC to establish a base debt + mm.borrow_base( + ®istry, + &mut base_pool, + &usdc_price, + &usdt_price, + &pool, + 500 * test_constants::usdc_multiplier(), // Borrow 500 USDC + &clock, + scenario.ctx(), + ); + + // Withdraw some USDC so we have debt but less assets (creating a net debt position) + let withdrawn_coin = mm.withdraw( + ®istry, + &base_pool, + "e_pool, + &usdc_price, + &usdt_price, + &pool, + 300 * test_constants::usdc_multiplier(), // Withdraw 300 USDC + &clock, + scenario.ctx(), + ); + + // Destroy the withdrawn coin + destroy(withdrawn_coin); + + // Now place a reduce-only market order to buy USDC (reducing the debt) + let order_info = pool_proxy::place_reduce_only_market_order( + ®istry, + &mut mm, + &mut pool, + &base_pool, // Pass base_pool since we have USDC debt + 2, + constants::self_matching_allowed(), + 100 * test_constants::usdc_multiplier(), // quantity (less than debt) + true, // is_bid = true (buying USDC to reduce debt) + false, + &clock, + scenario.ctx(), + ); + + // Verify the order was placed successfully + destroy(order_info); + return_shared_3!(mm, pool, base_pool); + return_shared(quote_pool); + destroy(usdc_price); + destroy(usdt_price); + cleanup_margin_test(registry, _admin_cap, _maintainer_cap, clock, scenario); +} + +#[test, expected_failure(abort_code = pool_proxy::EIncorrectDeepBookPool)] +fun test_place_reduce_only_market_order_incorrect_pool() { + let ( + mut scenario, + clock, + _admin_cap, + _maintainer_cap, + _base_pool_id, + quote_pool_id, + pool_id, + registry_id, + ) = setup_pool_proxy_test_env(); + + let (wrong_pool_id, _wrong_registry_id) = create_pool_for_testing(&mut scenario); + + scenario.next_tx(test_constants::user1()); + let pool = scenario.take_shared_by_id>(pool_id); + let mut wrong_pool = scenario.take_shared_by_id>(wrong_pool_id); + let mut registry = scenario.take_shared(); + let quote_pool = scenario.take_shared_by_id>(quote_pool_id); + + let deepbook_registry = scenario.take_shared_by_id(registry_id); + margin_manager::new( + &pool, + &deepbook_registry, + &mut registry, + &clock, + scenario.ctx(), + ); + return_shared(deepbook_registry); + + scenario.next_tx(test_constants::user1()); + let mut mm = scenario.take_shared>(); + + pool_proxy::place_reduce_only_market_order( + ®istry, + &mut mm, + &mut wrong_pool, // Wrong pool! + "e_pool, + 4, + constants::self_matching_allowed(), + 500, + false, + false, + &clock, + scenario.ctx(), + ); + + abort +} + +#[test, expected_failure(abort_code = pool_proxy::ENotReduceOnlyOrder)] +fun test_place_reduce_only_market_order_not_reduce_only() { + let ( + mut scenario, + clock, + _admin_cap, + _maintainer_cap, + _base_pool_id, + quote_pool_id, + pool_id, + registry_id, + ) = setup_pool_proxy_test_env(); + + scenario.next_tx(test_constants::user1()); + let mut pool = scenario.take_shared_by_id>(pool_id); + let mut registry = scenario.take_shared(); + let mut quote_pool = scenario.take_shared_by_id>(quote_pool_id); + let deepbook_registry = scenario.take_shared_by_id(registry_id); + margin_manager::new( + &pool, + &deepbook_registry, + &mut registry, + &clock, + scenario.ctx(), + ); + return_shared(deepbook_registry); + + scenario.next_tx(test_constants::user1()); + let mut mm = scenario.take_shared>(); + let usdc_price = build_demo_usdc_price_info_object(&mut scenario, &clock); + let usdt_price = build_demo_usdt_price_info_object(&mut scenario, &clock); + + // Deposit some USDT to use as collateral + mm.deposit( + ®istry, + &usdc_price, + &usdt_price, + mint_coin(10000 * test_constants::usdt_multiplier(), scenario.ctx()), + &clock, + scenario.ctx(), + ); + destroy_2!(usdc_price, usdt_price); + + let usdc_price = build_demo_usdc_price_info_object(&mut scenario, &clock); + let usdt_price = build_demo_usdt_price_info_object(&mut scenario, &clock); + // Borrow some USDT to establish relationship with quote pool + mm.borrow_quote( + ®istry, + &mut quote_pool, + &usdc_price, + &usdt_price, + &pool, + 100 * test_constants::usdt_multiplier(), // Small borrow amount + &clock, + scenario.ctx(), + ); + + // User has no USDC debt but has USDT debt, tries to buy USDC (is_bid = true) + // This should fail because it's not reducing any USDC position - user is increasing exposure + pool_proxy::place_reduce_only_market_order( + ®istry, + &mut mm, + &mut pool, + "e_pool, // Pass quote_pool since we have USDT debt + 3, + constants::self_matching_allowed(), + 100 * test_constants::usdc_multiplier(), // quantity + true, // is_bid = true (buying USDC) + false, + &clock, + scenario.ctx(), + ); + + return_shared_3!(mm, pool, quote_pool); + destroy(usdc_price); + destroy(usdt_price); + cleanup_margin_test(registry, _admin_cap, _maintainer_cap, clock, scenario); +} + +// === Stake Tests === +#[test] +fun test_stake_ok() { + let ( + mut scenario, + clock, + _admin_cap, + _maintainer_cap, + _base_pool_id, + _quote_pool_id, + pool_id, + registry_id, + ) = setup_pool_proxy_test_env(); + + scenario.next_tx(test_constants::user1()); + let mut pool = scenario.take_shared_by_id>(pool_id); + let mut registry = scenario.take_shared(); + let deepbook_registry = scenario.take_shared_by_id(registry_id); + margin_manager::new( + &pool, + &deepbook_registry, + &mut registry, + &clock, + scenario.ctx(), + ); + return_shared(deepbook_registry); + + scenario.next_tx(test_constants::user1()); + let mut mm = scenario.take_shared>(); + let usdc_price = build_demo_usdc_price_info_object(&mut scenario, &clock); + let usdt_price = build_demo_usdt_price_info_object(&mut scenario, &clock); + + // Deposit DEEP tokens + mm.deposit( + ®istry, + &usdc_price, + &usdt_price, + mint_coin(1000 * test_constants::deep_multiplier(), scenario.ctx()), + &clock, + scenario.ctx(), + ); + destroy_2!(usdc_price, usdt_price); + + // Stake DEEP tokens - should work since this is not a DEEP margin manager + pool_proxy::stake( + ®istry, + &mut mm, + &mut pool, + 100 * test_constants::deep_multiplier(), // 100 DEEP + scenario.ctx(), + ); + + return_shared_2!(mm, pool); + cleanup_margin_test(registry, _admin_cap, _maintainer_cap, clock, scenario); +} + +#[test, expected_failure(abort_code = pool_proxy::ECannotStakeWithDeepMarginManager)] +fun test_stake_with_deep_margin_manager() { + let ( + mut scenario, + clock, + _admin_cap, + _maintainer_cap, + _base_pool_id, + _quote_pool_id, + pool_id, + registry_id, + ) = setup_pool_proxy_test_env(); + + scenario.next_tx(test_constants::user1()); + let mut pool = scenario.take_shared_by_id>(pool_id); + let mut registry = scenario.take_shared(); + let deepbook_registry = scenario.take_shared_by_id(registry_id); + margin_manager::new( + &pool, + &deepbook_registry, + &mut registry, + &clock, + scenario.ctx(), + ); + return_shared(deepbook_registry); + + scenario.next_tx(test_constants::user1()); + let mut mm = scenario.take_shared>(); + + // Try to stake with DEEP margin manager - should fail + pool_proxy::stake( + ®istry, + &mut mm, + &mut pool, + 100 * test_constants::deep_multiplier(), + scenario.ctx(), + ); + + abort +} + +// === Other Function Tests === +#[test] +fun test_modify_order_ok() { + let ( + mut scenario, + clock, + _admin_cap, + _maintainer_cap, + _base_pool_id, + _quote_pool_id, + pool_id, + registry_id, + ) = setup_pool_proxy_test_env(); + + scenario.next_tx(test_constants::user1()); + let mut pool = scenario.take_shared_by_id>(pool_id); + let mut registry = scenario.take_shared(); + let deepbook_registry = scenario.take_shared_by_id(registry_id); + margin_manager::new( + &pool, + &deepbook_registry, + &mut registry, + &clock, + scenario.ctx(), + ); + return_shared(deepbook_registry); + + scenario.next_tx(test_constants::user1()); + let mut mm = scenario.take_shared>(); + let usdc_price = build_demo_usdc_price_info_object(&mut scenario, &clock); + let usdt_price = build_demo_usdt_price_info_object(&mut scenario, &clock); + + mm.deposit( + ®istry, + &usdc_price, + &usdt_price, + mint_coin(10000 * test_constants::usdc_multiplier(), scenario.ctx()), + &clock, + scenario.ctx(), + ); + destroy_2!(usdc_price, usdt_price); + + // First place an order + let order_info = pool_proxy::place_limit_order( + ®istry, + &mut mm, + &mut pool, + 1, + constants::no_restriction(), + constants::self_matching_allowed(), + 1_000_000, + 100 * test_constants::usdc_multiplier(), + false, // is_bid (sell USDC for USDT) + false, + 2000000, // expire_timestamp + &clock, + scenario.ctx(), + ); + + let order_id = order_info.order_id(); + + // Now modify the order (new quantity must be less than original) + pool_proxy::modify_order( + ®istry, + &mut mm, + &mut pool, + order_id, + 50 * test_constants::usdc_multiplier(), // new quantity (less than original) + &clock, + scenario.ctx(), + ); + + destroy(order_info); + return_shared_2!(mm, pool); + cleanup_margin_test(registry, _admin_cap, _maintainer_cap, clock, scenario); +} + +#[test] +fun test_cancel_order_ok() { + let ( + mut scenario, + clock, + _admin_cap, + _maintainer_cap, + _base_pool_id, + _quote_pool_id, + pool_id, + registry_id, + ) = setup_pool_proxy_test_env(); + + scenario.next_tx(test_constants::user1()); + let mut pool = scenario.take_shared_by_id>(pool_id); + let mut registry = scenario.take_shared(); + let deepbook_registry = scenario.take_shared_by_id(registry_id); + margin_manager::new( + &pool, + &deepbook_registry, + &mut registry, + &clock, + scenario.ctx(), + ); + return_shared(deepbook_registry); + + scenario.next_tx(test_constants::user1()); + let mut mm = scenario.take_shared>(); + let usdc_price = build_demo_usdc_price_info_object(&mut scenario, &clock); + let usdt_price = build_demo_usdt_price_info_object(&mut scenario, &clock); + + mm.deposit( + ®istry, + &usdc_price, + &usdt_price, + mint_coin(10000 * test_constants::usdc_multiplier(), scenario.ctx()), + &clock, + scenario.ctx(), + ); + destroy_2!(usdc_price, usdt_price); + + let order_info = pool_proxy::place_limit_order( + ®istry, + &mut mm, + &mut pool, + 1, + constants::no_restriction(), + constants::self_matching_allowed(), + 1_000_000, + 100 * test_constants::usdc_multiplier(), + false, // is_bid (sell USDC for USDT) + false, + 2000000, // expire_timestamp + &clock, + scenario.ctx(), + ); + + let order_id = order_info.order_id(); + + // Cancel the order + pool_proxy::cancel_order( + ®istry, + &mut mm, + &mut pool, + order_id, + &clock, + scenario.ctx(), + ); + + destroy(order_info); + return_shared_2!(mm, pool); + cleanup_margin_test(registry, _admin_cap, _maintainer_cap, clock, scenario); +} + +#[test] +fun test_cancel_orders_ok() { + let ( + mut scenario, + clock, + _admin_cap, + _maintainer_cap, + _base_pool_id, + _quote_pool_id, + pool_id, + registry_id, + ) = setup_pool_proxy_test_env(); + + scenario.next_tx(test_constants::user1()); + let mut pool = scenario.take_shared_by_id>(pool_id); + let mut registry = scenario.take_shared(); + let deepbook_registry = scenario.take_shared_by_id(registry_id); + margin_manager::new( + &pool, + &deepbook_registry, + &mut registry, + &clock, + scenario.ctx(), + ); + return_shared(deepbook_registry); + + scenario.next_tx(test_constants::user1()); + let mut mm = scenario.take_shared>(); + let usdc_price = build_demo_usdc_price_info_object(&mut scenario, &clock); + let usdt_price = build_demo_usdt_price_info_object(&mut scenario, &clock); + + mm.deposit( + ®istry, + &usdc_price, + &usdt_price, + mint_coin(10000 * test_constants::usdc_multiplier(), scenario.ctx()), + &clock, + scenario.ctx(), + ); + destroy_2!(usdc_price, usdt_price); + + let order_info1 = pool_proxy::place_limit_order( + ®istry, + &mut mm, + &mut pool, + 1, + constants::no_restriction(), + constants::self_matching_allowed(), + 1_000_000, + 1000 * test_constants::usdc_multiplier(), // Increased quantity to meet minimum size + false, // is_bid (sell USDC for USDT) + false, + 2000000, // expire_timestamp + &clock, + scenario.ctx(), + ); + let order_info2 = pool_proxy::place_limit_order( + ®istry, + &mut mm, + &mut pool, + 2, + constants::no_restriction(), + constants::self_matching_allowed(), + 1_100_000, + 1000 * test_constants::usdc_multiplier(), // Increased quantity to meet minimum size + false, // is_bid (sell USDC for USDT) + false, + 2000000, // expire_timestamp + &clock, + scenario.ctx(), + ); + + let order_ids = vector[order_info1.order_id(), order_info2.order_id()]; + + pool_proxy::cancel_orders( + ®istry, + &mut mm, + &mut pool, + order_ids, + &clock, + scenario.ctx(), + ); + + destroy_2!(order_info1, order_info2); + return_shared_2!(mm, pool); + cleanup_margin_test(registry, _admin_cap, _maintainer_cap, clock, scenario); +} + +#[test] +fun test_cancel_all_orders_ok() { + let ( + mut scenario, + clock, + _admin_cap, + _maintainer_cap, + _base_pool_id, + _quote_pool_id, + pool_id, + registry_id, + ) = setup_pool_proxy_test_env(); + + scenario.next_tx(test_constants::user1()); + let mut pool = scenario.take_shared_by_id>(pool_id); + let mut registry = scenario.take_shared(); + let deepbook_registry = scenario.take_shared_by_id(registry_id); + margin_manager::new( + &pool, + &deepbook_registry, + &mut registry, + &clock, + scenario.ctx(), + ); + return_shared(deepbook_registry); + + scenario.next_tx(test_constants::user1()); + let mut mm = scenario.take_shared>(); + let usdc_price = build_demo_usdc_price_info_object(&mut scenario, &clock); + let usdt_price = build_demo_usdt_price_info_object(&mut scenario, &clock); + + mm.deposit( + ®istry, + &usdc_price, + &usdt_price, + mint_coin(10000 * test_constants::usdc_multiplier(), scenario.ctx()), + &clock, + scenario.ctx(), + ); + destroy_2!(usdc_price, usdt_price); + + pool_proxy::cancel_all_orders( + ®istry, + &mut mm, + &mut pool, + &clock, + scenario.ctx(), + ); + + return_shared_2!(mm, pool); + cleanup_margin_test(registry, _admin_cap, _maintainer_cap, clock, scenario); +} + +#[test] +fun test_withdraw_settled_amounts_ok() { + let ( + mut scenario, + clock, + _admin_cap, + _maintainer_cap, + _base_pool_id, + _quote_pool_id, + pool_id, + registry_id, + ) = setup_pool_proxy_test_env(); + + scenario.next_tx(test_constants::user1()); + let mut pool = scenario.take_shared_by_id>(pool_id); + let mut registry = scenario.take_shared(); + let deepbook_registry = scenario.take_shared_by_id(registry_id); + margin_manager::new( + &pool, + &deepbook_registry, + &mut registry, + &clock, + scenario.ctx(), + ); + return_shared(deepbook_registry); + + scenario.next_tx(test_constants::user1()); + let mut mm = scenario.take_shared>(); + + pool_proxy::withdraw_settled_amounts( + ®istry, + &mut mm, + &mut pool, + scenario.ctx(), + ); + + return_shared_2!(mm, pool); + cleanup_margin_test(registry, _admin_cap, _maintainer_cap, clock, scenario); +} + +#[test] +fun test_unstake_ok() { + let ( + mut scenario, + clock, + _admin_cap, + _maintainer_cap, + _base_pool_id, + _quote_pool_id, + pool_id, + registry_id, + ) = setup_pool_proxy_test_env(); + + scenario.next_tx(test_constants::user1()); + let mut pool = scenario.take_shared_by_id>(pool_id); + let mut registry = scenario.take_shared(); + let deepbook_registry = scenario.take_shared_by_id(registry_id); + margin_manager::new( + &pool, + &deepbook_registry, + &mut registry, + &clock, + scenario.ctx(), + ); + return_shared(deepbook_registry); + + scenario.next_tx(test_constants::user1()); + let mut mm = scenario.take_shared>(); + + pool_proxy::unstake( + ®istry, + &mut mm, + &mut pool, + scenario.ctx(), + ); + + return_shared_2!(mm, pool); + cleanup_margin_test(registry, _admin_cap, _maintainer_cap, clock, scenario); +} + +#[test] +fun test_submit_proposal_ok() { + let ( + mut scenario, + clock, + admin_cap, + _maintainer_cap, + _base_pool_id, + _quote_pool_id, + pool_id, + registry_id, + ) = setup_pool_proxy_test_env(); + + scenario.next_tx(test_constants::user1()); + let mut pool = scenario.take_shared_by_id>(pool_id); + let mut registry = scenario.take_shared(); + let deepbook_registry = scenario.take_shared_by_id(registry_id); + margin_manager::new( + &pool, + &deepbook_registry, + &mut registry, + &clock, + scenario.ctx(), + ); + return_shared(deepbook_registry); + + scenario.next_tx(test_constants::user1()); + let mut mm = scenario.take_shared>(); + let usdc_price = build_demo_usdc_price_info_object(&mut scenario, &clock); + let usdt_price = build_demo_usdt_price_info_object(&mut scenario, &clock); + + // Deposit DEEP tokens + mm.deposit( + ®istry, + &usdc_price, + &usdt_price, + mint_coin( + 20000 * test_constants::deep_multiplier(), + scenario.ctx(), + ), // 20000 DEEP with 6 decimals + &clock, + scenario.ctx(), + ); + destroy_2!(usdc_price, usdt_price); + + // Stake DEEP tokens (10000 DEEP to be safe) + pool_proxy::stake( + ®istry, + &mut mm, + &mut pool, + 10000 * test_constants::deep_multiplier(), // 10000 DEEP stake amount + scenario.ctx(), + ); + + // Transition to next epoch for stake to become active + scenario.next_epoch(test_constants::admin()); + + // Continue the transaction as user1 + scenario.next_tx(test_constants::user1()); + + // Now submit a proposal + pool_proxy::submit_proposal( + ®istry, + &mut mm, + &mut pool, + 600000, // taker_fee + 200000, // maker_fee + 10000 * test_constants::deep_multiplier(), // stake_required + scenario.ctx(), + ); + + return_shared_2!(mm, pool); + cleanup_margin_test(registry, admin_cap, _maintainer_cap, clock, scenario); +} + +#[test] +fun test_vote_ok() { + let ( + mut scenario, + clock, + admin_cap, + _maintainer_cap, + _base_pool_id, + _quote_pool_id, + pool_id, + registry_id, + ) = setup_pool_proxy_test_env(); + + scenario.next_tx(test_constants::user1()); + let mut pool = scenario.take_shared_by_id>(pool_id); + let mut registry = scenario.take_shared(); + let deepbook_registry = scenario.take_shared_by_id(registry_id); + margin_manager::new( + &pool, + &deepbook_registry, + &mut registry, + &clock, + scenario.ctx(), + ); + return_shared(deepbook_registry); + + scenario.next_tx(test_constants::user1()); + let mut mm = scenario.take_shared>(); + let usdc_price = build_demo_usdc_price_info_object(&mut scenario, &clock); + let usdt_price = build_demo_usdt_price_info_object(&mut scenario, &clock); + + // Deposit DEEP tokens + mm.deposit( + ®istry, + &usdc_price, + &usdt_price, + mint_coin( + 20000 * test_constants::deep_multiplier(), + scenario.ctx(), + ), // 20000 DEEP with 6 decimals + &clock, + scenario.ctx(), + ); + destroy_2!(usdc_price, usdt_price); + + // Stake DEEP tokens (10000 DEEP to be safe) + pool_proxy::stake( + ®istry, + &mut mm, + &mut pool, + 10000 * test_constants::deep_multiplier(), // 10000 DEEP stake amount + scenario.ctx(), + ); + + // Transition to next epoch for stake to become active + scenario.next_epoch(test_constants::admin()); + + // Continue the transaction as user1 + scenario.next_tx(test_constants::user1()); + + // Get the balance manager ID to use as proposal ID + let balance_manager = mm.balance_manager(); + let balance_manager_id = object::id(balance_manager); + + // First submit a proposal (this creates a proposal with balance_manager_id as the key) + pool_proxy::submit_proposal( + ®istry, + &mut mm, + &mut pool, + 600000, // taker_fee + 200000, // maker_fee + 10000 * test_constants::deep_multiplier(), // stake_required + scenario.ctx(), + ); + + // Vote on the proposal using balance manager ID as proposal ID + pool_proxy::vote( + ®istry, + &mut mm, + &mut pool, + balance_manager_id, + scenario.ctx(), + ); + + return_shared_2!(mm, pool); + cleanup_margin_test(registry, admin_cap, _maintainer_cap, clock, scenario); +} + +#[test] +fun test_claim_rebates_ok() { + let ( + mut scenario, + clock, + _admin_cap, + _maintainer_cap, + _base_pool_id, + _quote_pool_id, + pool_id, + registry_id, + ) = setup_pool_proxy_test_env(); + + scenario.next_tx(test_constants::user1()); + let mut pool = scenario.take_shared_by_id>(pool_id); + let mut registry = scenario.take_shared(); + let deepbook_registry = scenario.take_shared_by_id(registry_id); + margin_manager::new( + &pool, + &deepbook_registry, + &mut registry, + &clock, + scenario.ctx(), + ); + return_shared(deepbook_registry); + + scenario.next_tx(test_constants::user1()); + let mut mm = scenario.take_shared>(); + + pool_proxy::claim_rebates( + ®istry, + &mut mm, + &mut pool, + scenario.ctx(), + ); + + return_shared_2!(mm, pool); + cleanup_margin_test(registry, _admin_cap, _maintainer_cap, clock, scenario); +} + +// === Permissionless Settlement Tests === +#[test] +fun test_withdraw_settled_amounts_permissionless_ok() { + let ( + mut scenario, + clock, + _admin_cap, + _maintainer_cap, + _base_pool_id, + _quote_pool_id, + pool_id, + registry_id, + ) = setup_pool_proxy_test_env(); + + // User1 creates margin manager and places an order + scenario.next_tx(test_constants::user1()); + let mut pool = scenario.take_shared_by_id>(pool_id); + let mut registry = scenario.take_shared(); + let deepbook_registry = scenario.take_shared_by_id(registry_id); + margin_manager::new( + &pool, + &deepbook_registry, + &mut registry, + &clock, + scenario.ctx(), + ); + return_shared(deepbook_registry); + + scenario.next_tx(test_constants::user1()); + let mut mm = scenario.take_shared>(); + let usdc_price = build_demo_usdc_price_info_object(&mut scenario, &clock); + let usdt_price = build_demo_usdt_price_info_object(&mut scenario, &clock); + + // Deposit USDC + mm.deposit( + ®istry, + &usdc_price, + &usdt_price, + mint_coin(10000 * test_constants::usdc_multiplier(), scenario.ctx()), + &clock, + scenario.ctx(), + ); + destroy_2!(usdc_price, usdt_price); + + // Place a sell order + let order_info = pool_proxy::place_limit_order( + ®istry, + &mut mm, + &mut pool, + 1, // client_order_id + constants::no_restriction(), + constants::self_matching_allowed(), + 1_000_000, // price + 100 * test_constants::usdc_multiplier(), // quantity + false, // is_bid (sell USDC for USDT) + false, // pay_with_deep + 2000000, // expire_timestamp + &clock, + scenario.ctx(), + ); + + destroy(order_info); + + // User2 places a matching buy order to fill user1's order + scenario.next_tx(test_constants::user2()); + let deepbook_registry = scenario.take_shared_by_id(registry_id); + margin_manager::new( + &pool, + &deepbook_registry, + &mut registry, + &clock, + scenario.ctx(), + ); + return_shared(deepbook_registry); + + scenario.next_tx(test_constants::user2()); + let mut mm2 = scenario.take_shared>(); + let usdc_price = build_demo_usdc_price_info_object(&mut scenario, &clock); + let usdt_price = build_demo_usdt_price_info_object(&mut scenario, &clock); + + // Deposit USDT for user2 + mm2.deposit( + ®istry, + &usdc_price, + &usdt_price, + mint_coin(10000 * test_constants::usdt_multiplier(), scenario.ctx()), + &clock, + scenario.ctx(), + ); + destroy_2!(usdc_price, usdt_price); + + // Place a buy order that matches user1's sell order + let order_info2 = pool_proxy::place_limit_order( + ®istry, + &mut mm2, + &mut pool, + 2, // client_order_id + constants::no_restriction(), + constants::self_matching_allowed(), + 1_000_000, // same price + 100 * test_constants::usdc_multiplier(), // same quantity + true, // is_bid (buy USDC with USDT) + false, // pay_with_deep + 2000000, // expire_timestamp + &clock, + scenario.ctx(), + ); + destroy(order_info2); + + // Now user1 has settled balances (received USDT from the trade) + // User2 (not the owner) calls withdraw_settled_amounts_permissionless for user1 + scenario.next_tx(test_constants::user2()); + pool_proxy::withdraw_settled_amounts_permissionless( + ®istry, + &mut mm, + &mut pool, + ); + + // Verify that the settlement succeeded (if it failed, we would have aborted) + return_shared_3!(mm, mm2, pool); + cleanup_margin_test(registry, _admin_cap, _maintainer_cap, clock, scenario); +} + +#[test, expected_failure(abort_code = ::deepbook::vault::ENoBalanceToSettle)] +fun test_withdraw_settled_amounts_permissionless_no_balance_e() { + let ( + mut scenario, + clock, + _admin_cap, + _maintainer_cap, + _base_pool_id, + _quote_pool_id, + pool_id, + registry_id, + ) = setup_pool_proxy_test_env(); + + // User1 creates margin manager but doesn't trade + scenario.next_tx(test_constants::user1()); + let mut pool = scenario.take_shared_by_id>(pool_id); + let mut registry = scenario.take_shared(); + let deepbook_registry = scenario.take_shared_by_id(registry_id); + margin_manager::new( + &pool, + &deepbook_registry, + &mut registry, + &clock, + scenario.ctx(), + ); + return_shared(deepbook_registry); + + scenario.next_tx(test_constants::user1()); + let mut mm = scenario.take_shared>(); + + // Try to settle when there's nothing to settle - should fail + scenario.next_tx(test_constants::user2()); + pool_proxy::withdraw_settled_amounts_permissionless( + ®istry, + &mut mm, + &mut pool, + ); + + abort 0 +} + +#[test, expected_failure(abort_code = margin_manager::EIncorrectDeepBookPool)] +fun test_withdraw_settled_amounts_permissionless_incorrect_pool_e() { + let ( + mut scenario, + clock, + _admin_cap, + _maintainer_cap, + _base_pool_id, + _quote_pool_id, + pool_id, + registry_id, + ) = setup_pool_proxy_test_env(); + + // Create a wrong pool + let (wrong_pool_id, _wrong_registry_id) = create_pool_for_testing(&mut scenario); + + scenario.next_tx(test_constants::user1()); + let pool = scenario.take_shared_by_id>(pool_id); + let mut wrong_pool = scenario.take_shared_by_id>(wrong_pool_id); + let mut registry = scenario.take_shared(); + let deepbook_registry = scenario.take_shared_by_id(registry_id); + margin_manager::new( + &pool, + &deepbook_registry, + &mut registry, + &clock, + scenario.ctx(), + ); + return_shared(deepbook_registry); + + scenario.next_tx(test_constants::user1()); + let mut mm = scenario.take_shared>(); + + // Try to settle with wrong pool - should fail + scenario.next_tx(test_constants::user2()); + pool_proxy::withdraw_settled_amounts_permissionless( + ®istry, + &mut mm, + &mut wrong_pool, // Wrong pool! + ); + + abort 0 +} diff --git a/packages/deepbook_margin/tests/rate_limiter_tests.move b/packages/deepbook_margin/tests/rate_limiter_tests.move new file mode 100644 index 000000000..db15a1fa7 --- /dev/null +++ b/packages/deepbook_margin/tests/rate_limiter_tests.move @@ -0,0 +1,912 @@ +#[test_only] +module deepbook_margin::rate_limiter_tests; + +use deepbook_margin::{ + margin_pool::{Self, MarginPool}, + margin_registry::MarginRegistry, + rate_limiter, + test_constants::{USDC, admin, user1}, + test_helpers::{Self, return_shared_2, destroy_3, destroy_4} +}; +use std::unit_test::destroy; +use sui::{clock, coin, test_scenario}; + +const HOUR_MS: u64 = 3_600_000; +const CAPACITY: u64 = 100_000_000_000; +const RATE: u64 = 100_000_000; + +// === Unit Tests === + +#[test] +fun constructor_works() { + let ctx = &mut tx_context::dummy(); + let clock = clock::create_for_testing(ctx); + let limiter = rate_limiter::new(CAPACITY, RATE, true, &clock); + + assert!(limiter.available() == CAPACITY); + assert!(limiter.capacity() == CAPACITY); + assert!(limiter.refill_rate_per_ms() == RATE); + assert!(limiter.is_enabled() == true); + assert!(limiter.last_updated_ms() == 0); + + clock.destroy_for_testing(); + destroy(limiter); +} + +#[test] +fun constructor_disabled() { + let ctx = &mut tx_context::dummy(); + let clock = clock::create_for_testing(ctx); + let limiter = rate_limiter::new(CAPACITY, RATE, false, &clock); + + assert!(limiter.is_enabled() == false); + assert!(limiter.available() == CAPACITY); + + clock.destroy_for_testing(); + destroy(limiter); +} + +#[test] +fun get_available_withdrawal_returns_capacity_initially() { + let ctx = &mut tx_context::dummy(); + let clock = clock::create_for_testing(ctx); + let limiter = rate_limiter::new(CAPACITY, RATE, true, &clock); + + let available = limiter.get_available_withdrawal(&clock); + assert!(available == CAPACITY); + + clock.destroy_for_testing(); + destroy(limiter); +} + +#[test] +fun refill_works() { + let ctx = &mut tx_context::dummy(); + let mut clock = clock::create_for_testing(ctx); + clock::set_for_testing(&mut clock, 1000); + let mut limiter = rate_limiter::new(CAPACITY, RATE, true, &clock); + + let success = limiter.check_and_record_withdrawal(CAPACITY, &clock); + assert!(success == true); + assert!(limiter.available() == 0); + + clock::set_for_testing(&mut clock, 1500); + let available = limiter.get_available_withdrawal(&clock); + assert!(available == 500 * RATE); + + clock::set_for_testing(&mut clock, 2000); + let available_full = limiter.get_available_withdrawal(&clock); + assert!(available_full == CAPACITY); + + clock.destroy_for_testing(); + destroy(limiter); +} + +#[test] +fun refill_caps_at_capacity() { + let ctx = &mut tx_context::dummy(); + let mut clock = clock::create_for_testing(ctx); + clock::set_for_testing(&mut clock, 1000); + let mut limiter = rate_limiter::new(CAPACITY, RATE, true, &clock); + + let success = limiter.check_and_record_withdrawal(CAPACITY / 2, &clock); + assert!(success == true); + assert!(limiter.available() == CAPACITY / 2); + + clock::set_for_testing(&mut clock, 1_000_000_000); + let available = limiter.get_available_withdrawal(&clock); + assert!(available == CAPACITY); + + clock.destroy_for_testing(); + destroy(limiter); +} + +#[test] +fun consume_works() { + let ctx = &mut tx_context::dummy(); + let mut clock = clock::create_for_testing(ctx); + clock::set_for_testing(&mut clock, 1000); + let mut limiter = rate_limiter::new(CAPACITY, RATE, true, &clock); + + let consume_amount = CAPACITY / 4; + let success = limiter.check_and_record_withdrawal(consume_amount, &clock); + assert!(success == true); + assert!(limiter.available() == CAPACITY - consume_amount); + + clock.destroy_for_testing(); + destroy(limiter); +} + +#[test] +fun consume_exact_capacity() { + let ctx = &mut tx_context::dummy(); + let mut clock = clock::create_for_testing(ctx); + clock::set_for_testing(&mut clock, 1000); + let mut limiter = rate_limiter::new(CAPACITY, RATE, true, &clock); + + let success = limiter.check_and_record_withdrawal(CAPACITY, &clock); + assert!(success == true); + assert!(limiter.available() == 0); + + clock.destroy_for_testing(); + destroy(limiter); +} + +#[test] +fun consume_fails_when_exceeds_available() { + let ctx = &mut tx_context::dummy(); + let mut clock = clock::create_for_testing(ctx); + clock::set_for_testing(&mut clock, 1000); + let mut limiter = rate_limiter::new(CAPACITY, RATE, true, &clock); + + let success = limiter.check_and_record_withdrawal(CAPACITY + 1, &clock); + assert!(success == false); + assert!(limiter.available() == CAPACITY); + + clock.destroy_for_testing(); + destroy(limiter); +} + +#[test] +fun consume_fails_when_exceeds_capacity() { + let ctx = &mut tx_context::dummy(); + let mut clock = clock::create_for_testing(ctx); + clock::set_for_testing(&mut clock, 1000); + let mut limiter = rate_limiter::new(CAPACITY, RATE, true, &clock); + + let success = limiter.check_and_record_withdrawal(CAPACITY * 2, &clock); + assert!(success == false); + + clock.destroy_for_testing(); + destroy(limiter); +} + +#[test] +fun multiple_consumptions_with_refill() { + let ctx = &mut tx_context::dummy(); + let mut clock = clock::create_for_testing(ctx); + clock::set_for_testing(&mut clock, 1000); + let mut limiter = rate_limiter::new(CAPACITY, RATE, true, &clock); + + let success1 = limiter.check_and_record_withdrawal(CAPACITY / 2, &clock); + assert!(success1 == true); + assert!(limiter.available() == CAPACITY / 2); + + clock::set_for_testing(&mut clock, 1250); + + let success2 = limiter.check_and_record_withdrawal(CAPACITY / 4, &clock); + assert!(success2 == true); + + let expected = CAPACITY / 2 + (250 * RATE) - CAPACITY / 4; + assert!(limiter.available() == expected); + + clock.destroy_for_testing(); + destroy(limiter); +} + +#[test] +fun consume_then_wait_then_consume_again() { + let ctx = &mut tx_context::dummy(); + let mut clock = clock::create_for_testing(ctx); + clock::set_for_testing(&mut clock, 1000); + let mut limiter = rate_limiter::new(CAPACITY, RATE, true, &clock); + + let success1 = limiter.check_and_record_withdrawal(CAPACITY, &clock); + assert!(success1 == true); + assert!(limiter.available() == 0); + + let success2 = limiter.check_and_record_withdrawal(1, &clock); + assert!(success2 == false); + + clock::set_for_testing(&mut clock, 2000); + + let success3 = limiter.check_and_record_withdrawal(CAPACITY, &clock); + assert!(success3 == true); + + clock.destroy_for_testing(); + destroy(limiter); +} + +#[test] +fun disabled_allows_any_amount() { + let ctx = &mut tx_context::dummy(); + let mut clock = clock::create_for_testing(ctx); + clock::set_for_testing(&mut clock, 1000); + let mut limiter = rate_limiter::new(CAPACITY, RATE, false, &clock); + + let success = limiter.check_and_record_withdrawal(CAPACITY * 10, &clock); + assert!(success == true); + + assert!(limiter.get_available_withdrawal(&clock) == std::u64::max_value!()); + + clock.destroy_for_testing(); + destroy(limiter); +} + +#[test] +fun update_config_increases_capacity() { + let ctx = &mut tx_context::dummy(); + let clock = clock::create_for_testing(ctx); + let mut limiter = rate_limiter::new(CAPACITY, RATE, true, &clock); + + let new_capacity = CAPACITY * 2; + limiter.update_config(new_capacity, RATE, true, &clock); + + assert!(limiter.capacity() == new_capacity); + assert!(limiter.available() == CAPACITY); + + clock.destroy_for_testing(); + destroy(limiter); +} + +#[test] +fun update_config_decreases_capacity_caps_available() { + let ctx = &mut tx_context::dummy(); + let clock = clock::create_for_testing(ctx); + let mut limiter = rate_limiter::new(CAPACITY, RATE, true, &clock); + + let new_capacity = CAPACITY / 2; + limiter.update_config(new_capacity, RATE, true, &clock); + + assert!(limiter.capacity() == new_capacity); + assert!(limiter.available() == new_capacity); + + clock.destroy_for_testing(); + destroy(limiter); +} + +#[test] +fun update_config_changes_rate() { + let ctx = &mut tx_context::dummy(); + let mut clock = clock::create_for_testing(ctx); + clock::set_for_testing(&mut clock, 1000); + let mut limiter = rate_limiter::new(CAPACITY, RATE, true, &clock); + + limiter.check_and_record_withdrawal(CAPACITY, &clock); + assert!(limiter.available() == 0); + + let new_rate = RATE * 2; + limiter.update_config(CAPACITY, new_rate, true, &clock); + + clock::set_for_testing(&mut clock, 1500); + let available = limiter.get_available_withdrawal(&clock); + assert!(available == CAPACITY); + + clock.destroy_for_testing(); + destroy(limiter); +} + +#[test] +fun update_config_enables() { + let ctx = &mut tx_context::dummy(); + let clock = clock::create_for_testing(ctx); + let mut limiter = rate_limiter::new(CAPACITY, RATE, false, &clock); + assert!(limiter.is_enabled() == false); + + limiter.update_config(CAPACITY, RATE, true, &clock); + assert!(limiter.is_enabled() == true); + + clock.destroy_for_testing(); + destroy(limiter); +} + +#[test] +fun update_config_disables() { + let ctx = &mut tx_context::dummy(); + let clock = clock::create_for_testing(ctx); + let mut limiter = rate_limiter::new(CAPACITY, RATE, true, &clock); + assert!(limiter.is_enabled() == true); + + limiter.update_config(CAPACITY, RATE, false, &clock); + assert!(limiter.is_enabled() == false); + + clock.destroy_for_testing(); + destroy(limiter); +} + +#[test] +fun zero_consumption_succeeds() { + let ctx = &mut tx_context::dummy(); + let mut clock = clock::create_for_testing(ctx); + clock::set_for_testing(&mut clock, 1000); + let mut limiter = rate_limiter::new(CAPACITY, RATE, true, &clock); + + let success = limiter.check_and_record_withdrawal(0, &clock); + assert!(success == true); + assert!(limiter.available() == CAPACITY); + + clock.destroy_for_testing(); + destroy(limiter); +} + +#[test] +fun timestamp_at_zero_works() { + let ctx = &mut tx_context::dummy(); + let clock = clock::create_for_testing(ctx); + let limiter = rate_limiter::new(CAPACITY, RATE, true, &clock); + + let available = limiter.get_available_withdrawal(&clock); + assert!(available == CAPACITY); + + clock.destroy_for_testing(); + destroy(limiter); +} + +#[test] +fun partial_refill_precision() { + let capacity: u64 = 1_000_000; + let rate: u64 = 100; + + let ctx = &mut tx_context::dummy(); + let mut clock = clock::create_for_testing(ctx); + clock::set_for_testing(&mut clock, 1000); + let mut limiter = rate_limiter::new(capacity, rate, true, &clock); + + limiter.check_and_record_withdrawal(capacity, &clock); + + clock::set_for_testing(&mut clock, 1001); + let available = limiter.get_available_withdrawal(&clock); + assert!(available == 100); + + clock::set_for_testing(&mut clock, 1010); + let available2 = limiter.get_available_withdrawal(&clock); + assert!(available2 == 1000); + + clock.destroy_for_testing(); + destroy(limiter); +} + +#[test] +fun large_values_no_overflow() { + let capacity: u64 = 18_446_744_073_709_551_615; + let rate: u64 = 1_000_000_000_000; + + let ctx = &mut tx_context::dummy(); + let mut clock = clock::create_for_testing(ctx); + clock::set_for_testing(&mut clock, 1000); + let mut limiter = rate_limiter::new(capacity, rate, true, &clock); + + let success = limiter.check_and_record_withdrawal(capacity, &clock); + assert!(success == true); + + clock::set_for_testing(&mut clock, 1_000_000_000); + let available = limiter.get_available_withdrawal(&clock); + assert!(available == capacity); + + clock.destroy_for_testing(); + destroy(limiter); +} + +#[test] +fun same_timestamp_no_double_refill() { + let ctx = &mut tx_context::dummy(); + let mut clock = clock::create_for_testing(ctx); + clock::set_for_testing(&mut clock, 1000); + let mut limiter = rate_limiter::new(CAPACITY, RATE, true, &clock); + + limiter.check_and_record_withdrawal(CAPACITY / 2, &clock); + let after_first = limiter.available(); + + limiter.check_and_record_withdrawal(CAPACITY / 4, &clock); + let after_second = limiter.available(); + + assert!(after_second == after_first - CAPACITY / 4); + + clock.destroy_for_testing(); + destroy(limiter); +} + +#[test] +fun last_updated_changes_on_consumption() { + let ctx = &mut tx_context::dummy(); + let mut clock = clock::create_for_testing(ctx); + let mut limiter = rate_limiter::new(CAPACITY, RATE, true, &clock); + + assert!(limiter.last_updated_ms() == 0); + + clock::set_for_testing(&mut clock, 5000); + limiter.check_and_record_withdrawal(1000, &clock); + + assert!(limiter.last_updated_ms() == 5000); + + clock.destroy_for_testing(); + destroy(limiter); +} + +#[test] +fun wait_time_for_refill() { + let ctx = &mut tx_context::dummy(); + let mut clock = clock::create_for_testing(ctx); + clock::set_for_testing(&mut clock, 1000); + let mut limiter = rate_limiter::new(CAPACITY, RATE, true, &clock); + + limiter.check_and_record_withdrawal(CAPACITY, &clock); + assert!(limiter.available() == 0); + + clock::set_for_testing(&mut clock, 1100); + assert!(limiter.get_available_withdrawal(&clock) == 10_000_000_000); + + clock::set_for_testing(&mut clock, 1500); + assert!(limiter.get_available_withdrawal(&clock) == 50_000_000_000); + + clock::set_for_testing(&mut clock, 2000); + assert!(limiter.get_available_withdrawal(&clock) == CAPACITY); + + clock.destroy_for_testing(); + destroy(limiter); +} + +#[test] +fun consume_after_partial_refill() { + let ctx = &mut tx_context::dummy(); + let mut clock = clock::create_for_testing(ctx); + clock::set_for_testing(&mut clock, 1000); + let mut limiter = rate_limiter::new(CAPACITY, RATE, true, &clock); + + limiter.check_and_record_withdrawal(CAPACITY, &clock); + + clock::set_for_testing(&mut clock, 1200); + + let success1 = limiter.check_and_record_withdrawal(30_000_000_000, &clock); + assert!(success1 == false); + + let success2 = limiter.check_and_record_withdrawal(20_000_000_000, &clock); + assert!(success2 == true); + assert!(limiter.available() == 0); + + clock.destroy_for_testing(); + destroy(limiter); +} + +#[test] +fun burst_then_steady_consumption() { + let ctx = &mut tx_context::dummy(); + let mut clock = clock::create_for_testing(ctx); + clock::set_for_testing(&mut clock, 1000); + let mut limiter = rate_limiter::new(CAPACITY, RATE, true, &clock); + + let success1 = limiter.check_and_record_withdrawal(CAPACITY, &clock); + assert!(success1 == true); + + let mut i = 0u64; + while (i < 10) { + clock::increment_for_testing(&mut clock, 1); + let success = limiter.check_and_record_withdrawal(RATE, &clock); + assert!(success == true); + i = i + 1; + }; + + assert!(limiter.available() == 0); + + clock.destroy_for_testing(); + destroy(limiter); +} + +// === Integration Tests === + +#[test] +fun pool_basic_rate_limiting() { + let (mut scenario, clock, admin_cap, maintainer_cap) = test_helpers::setup_margin_registry(); + + scenario.next_tx(admin()); + let mut registry = scenario.take_shared(); + let _pool_id = test_helpers::create_pool_with_rate_limit( + &mut registry, + &maintainer_cap, + 1_000_000, + 10_000, + 10, + true, + &clock, + &mut scenario, + ); + test_scenario::return_shared(registry); + + scenario.next_tx(user1()); + let mut pool = scenario.take_shared>(); + let registry = scenario.take_shared(); + let supplier_cap = margin_pool::mint_supplier_cap(®istry, &clock, scenario.ctx()); + + let supply_coin = coin::mint_for_testing(20_000, scenario.ctx()); + pool.supply(®istry, &supplier_cap, supply_coin, option::none(), &clock); + + let withdrawn1 = pool.withdraw( + ®istry, + &supplier_cap, + option::some(10_000), + &clock, + scenario.ctx(), + ); + assert!(withdrawn1.value() == 10_000); + + let available = pool.get_available_withdrawal(&clock); + assert!(available == 0); + + destroy_3!(withdrawn1, supplier_cap, maintainer_cap); + destroy(admin_cap); + return_shared_2!(pool, registry); + clock.destroy_for_testing(); + scenario.end(); +} + +#[test] +fun pool_capacity_refills_over_time() { + let ( + mut scenario, + mut clock, + admin_cap, + maintainer_cap, + ) = test_helpers::setup_margin_registry(); + + scenario.next_tx(admin()); + let mut registry = scenario.take_shared(); + let _pool_id = test_helpers::create_pool_with_rate_limit( + &mut registry, + &maintainer_cap, + 1_000_000, + 10_000, + 10, + true, + &clock, + &mut scenario, + ); + test_scenario::return_shared(registry); + + scenario.next_tx(user1()); + let mut pool = scenario.take_shared>(); + let registry = scenario.take_shared(); + let supplier_cap = margin_pool::mint_supplier_cap(®istry, &clock, scenario.ctx()); + + let supply_coin = coin::mint_for_testing(30_000, scenario.ctx()); + pool.supply(®istry, &supplier_cap, supply_coin, option::none(), &clock); + + let withdrawn1 = pool.withdraw( + ®istry, + &supplier_cap, + option::some(10_000), + &clock, + scenario.ctx(), + ); + assert!(withdrawn1.value() == 10_000); + + assert!(pool.get_available_withdrawal(&clock) == 0); + + clock::increment_for_testing(&mut clock, 500); + + let available = pool.get_available_withdrawal(&clock); + assert!(available == 5_000); + + let withdrawn2 = pool.withdraw( + ®istry, + &supplier_cap, + option::some(5_000), + &clock, + scenario.ctx(), + ); + assert!(withdrawn2.value() == 5_000); + + clock::increment_for_testing(&mut clock, 1_000); + let available_after = pool.get_available_withdrawal(&clock); + assert!(available_after == 10_000); + + destroy_4!(withdrawn1, withdrawn2, supplier_cap, maintainer_cap); + destroy(admin_cap); + return_shared_2!(pool, registry); + clock.destroy_for_testing(); + scenario.end(); +} + +#[test] +fun pool_capacity_caps_at_max() { + let ( + mut scenario, + mut clock, + admin_cap, + maintainer_cap, + ) = test_helpers::setup_margin_registry(); + + scenario.next_tx(admin()); + let mut registry = scenario.take_shared(); + let _pool_id = test_helpers::create_pool_with_rate_limit( + &mut registry, + &maintainer_cap, + 1_000_000, + 10_000, + 10, + true, + &clock, + &mut scenario, + ); + test_scenario::return_shared(registry); + + scenario.next_tx(user1()); + let mut pool = scenario.take_shared>(); + let registry = scenario.take_shared(); + let supplier_cap = margin_pool::mint_supplier_cap(®istry, &clock, scenario.ctx()); + + let supply_coin = coin::mint_for_testing(20_000, scenario.ctx()); + pool.supply(®istry, &supplier_cap, supply_coin, option::none(), &clock); + + clock::increment_for_testing(&mut clock, 10 * HOUR_MS); + + let available = pool.get_available_withdrawal(&clock); + assert!(available == 10_000); + + let withdrawn = pool.withdraw( + ®istry, + &supplier_cap, + option::some(10_000), + &clock, + scenario.ctx(), + ); + assert!(withdrawn.value() == 10_000); + + destroy_3!(withdrawn, supplier_cap, maintainer_cap); + destroy(admin_cap); + return_shared_2!(pool, registry); + clock.destroy_for_testing(); + scenario.end(); +} + +#[test, expected_failure(abort_code = deepbook_margin::margin_pool::ERateLimitExceeded)] +fun pool_withdrawal_exceeds_limit_fails() { + let (mut scenario, clock, _admin_cap, maintainer_cap) = test_helpers::setup_margin_registry(); + + scenario.next_tx(admin()); + let mut registry = scenario.take_shared(); + let _pool_id = test_helpers::create_pool_with_rate_limit( + &mut registry, + &maintainer_cap, + 1_000_000, + 10_000, + 10, + true, + &clock, + &mut scenario, + ); + test_scenario::return_shared(registry); + + scenario.next_tx(user1()); + let mut pool = scenario.take_shared>(); + let registry = scenario.take_shared(); + let supplier_cap = margin_pool::mint_supplier_cap(®istry, &clock, scenario.ctx()); + + let supply_coin = coin::mint_for_testing(20_000, scenario.ctx()); + pool.supply(®istry, &supplier_cap, supply_coin, option::none(), &clock); + + let _withdrawn = pool.withdraw( + ®istry, + &supplier_cap, + option::some(15_000), + &clock, + scenario.ctx(), + ); + + abort +} + +#[test] +fun pool_disabled_rate_limiter() { + let (mut scenario, clock, admin_cap, maintainer_cap) = test_helpers::setup_margin_registry(); + + scenario.next_tx(admin()); + let mut registry = scenario.take_shared(); + let _pool_id = test_helpers::create_pool_with_rate_limit( + &mut registry, + &maintainer_cap, + 1_000_000, + 10_000, + 10, + false, + &clock, + &mut scenario, + ); + test_scenario::return_shared(registry); + + scenario.next_tx(user1()); + let mut pool = scenario.take_shared>(); + let registry = scenario.take_shared(); + let supplier_cap = margin_pool::mint_supplier_cap(®istry, &clock, scenario.ctx()); + + let supply_coin = coin::mint_for_testing(100_000, scenario.ctx()); + pool.supply(®istry, &supplier_cap, supply_coin, option::none(), &clock); + + let withdrawn = pool.withdraw( + ®istry, + &supplier_cap, + option::some(50_000), + &clock, + scenario.ctx(), + ); + assert!(withdrawn.value() == 50_000); + + destroy_3!(withdrawn, supplier_cap, maintainer_cap); + destroy(admin_cap); + return_shared_2!(pool, registry); + clock.destroy_for_testing(); + scenario.end(); +} + +#[test] +/// Test that deposit increases rate limit available, so deposit/withdraw cycles don't consume the bucket. +/// This prevents griefing attacks where someone deposits and withdraws to block other users. +fun deposit_refills_rate_limit_bucket() { + let ctx = &mut tx_context::dummy(); + let mut clock = clock::create_for_testing(ctx); + clock::set_for_testing(&mut clock, 1000); + + let capacity: u64 = 100_000; + let rate: u64 = 1; // slow refill rate + let mut limiter = rate_limiter::new(capacity, rate, true, &clock); + + // First drain the bucket partially so deposits can refill it + let success = limiter.check_and_record_withdrawal(50_000, &clock); + assert!(success == true); + assert!(limiter.available() == 50_000); + + // Now simulate 20 deposit/withdraw cycles of 10_000 each + let cycle_amount: u64 = 10_000; + let mut i = 0u64; + while (i < 20) { + // Deposit increases available (capped at capacity) + limiter.record_deposit(cycle_amount, &clock); + + // Withdraw decreases available + let success = limiter.check_and_record_withdrawal(cycle_amount, &clock); + assert!(success == true); + + i = i + 1; + }; + + // After 20 cycles, bucket should be at 50k (where we started after initial drain) + // Each deposit→withdraw cycle nets to zero when bucket is below capacity + assert!(limiter.available() == 50_000); + + clock.destroy_for_testing(); + destroy(limiter); +} + +#[test] +/// Test that pure withdrawals without deposits will eventually hit the rate limit. +fun pure_withdrawals_hit_rate_limit() { + let ctx = &mut tx_context::dummy(); + let mut clock = clock::create_for_testing(ctx); + clock::set_for_testing(&mut clock, 1000); + + let capacity: u64 = 100_000; + let rate: u64 = 1; + let mut limiter = rate_limiter::new(capacity, rate, true, &clock); + + // First 10 withdrawals of 10_000 should succeed (total 100k = capacity) + let mut i = 0u64; + while (i < 10) { + let success = limiter.check_and_record_withdrawal(10_000, &clock); + assert!(success == true); + i = i + 1; + }; + + // 11th withdrawal should fail (bucket exhausted) + let success = limiter.check_and_record_withdrawal(10_000, &clock); + assert!(success == false); + + clock.destroy_for_testing(); + destroy(limiter); +} + +#[test] +/// Integration test: Pool with 400k funds, 500k max cap, 100k rate limit. +/// 20 cycles of deposit 10k / withdraw 10k should not hit rate limit. +/// Key: Each deposit refills bucket, allowing subsequent withdraw to succeed. +fun pool_deposit_withdraw_cycles_no_rate_limit() { + let (mut scenario, clock, admin_cap, maintainer_cap) = test_helpers::setup_margin_registry(); + + scenario.next_tx(admin()); + let mut registry = scenario.take_shared(); + + // Create pool with 500k supply cap, 100k rate limit capacity, slow refill + let _pool_id = test_helpers::create_pool_with_rate_limit( + &mut registry, + &maintainer_cap, + 500_000, // supply cap + 100_000, // rate limit capacity + 1, // very slow refill rate (1 per ms) + true, // enabled + &clock, + &mut scenario, + ); + test_scenario::return_shared(registry); + + // Initial supply of 400k to the pool + scenario.next_tx(user1()); + let mut pool = scenario.take_shared>(); + let registry = scenario.take_shared(); + let supplier_cap = margin_pool::mint_supplier_cap(®istry, &clock, scenario.ctx()); + + let initial_supply = coin::mint_for_testing(400_000, scenario.ctx()); + pool.supply(®istry, &supplier_cap, initial_supply, option::none(), &clock); + + // Verify initial state - bucket at capacity after initial supply + assert!(pool.total_supply() == 400_000); + assert!(pool.get_available_withdrawal(&clock) == 100_000); + + // Perform 20 cycles of deposit 10k, withdraw 10k + // All 20 withdrawals should succeed because each deposit refills what the withdraw takes + let cycle_amount: u64 = 10_000; + let mut i = 0u64; + while (i < 20) { + // Deposit 10k - refills bucket (capped at capacity) + let deposit_coin = coin::mint_for_testing(cycle_amount, scenario.ctx()); + pool.supply(®istry, &supplier_cap, deposit_coin, option::none(), &clock); + + // Withdraw 10k - should succeed + let withdrawn = pool.withdraw( + ®istry, + &supplier_cap, + option::some(cycle_amount), + &clock, + scenario.ctx(), + ); + assert!(withdrawn.value() == cycle_amount); + withdrawn.burn_for_testing(); + + i = i + 1; + }; + + // After 20 cycles: + // - Bucket ends at 90k (first deposit was wasted since bucket was at cap, then withdraw took 10k) + // - Each subsequent cycle: deposit refills to 100k, withdraw takes to 90k + assert!(pool.get_available_withdrawal(&clock) == 90_000); + + // Vault should still have 400k (net zero change from cycles) + // Note: We check vault_balance instead of total_supply because share calculations can have rounding + assert!(pool.vault_balance() == 400_000); + + destroy(supplier_cap); + destroy(maintainer_cap); + destroy(admin_cap); + return_shared_2!(pool, registry); + clock.destroy_for_testing(); + scenario.end(); +} + +#[test, expected_failure(abort_code = deepbook_margin::margin_pool::ERateLimitExceeded)] +/// Integration test: Without deposits, pure withdrawals should hit rate limit. +fun pool_pure_withdrawals_hit_rate_limit() { + let (mut scenario, clock, _admin_cap, maintainer_cap) = test_helpers::setup_margin_registry(); + + scenario.next_tx(admin()); + let mut registry = scenario.take_shared(); + + // Create pool with 500k supply cap, 100k rate limit capacity + let _pool_id = test_helpers::create_pool_with_rate_limit( + &mut registry, + &maintainer_cap, + 500_000, // supply cap + 100_000, // rate limit capacity + 1, // very slow refill + true, // enabled + &clock, + &mut scenario, + ); + test_scenario::return_shared(registry); + + scenario.next_tx(user1()); + let mut pool = scenario.take_shared>(); + let registry = scenario.take_shared(); + let supplier_cap = margin_pool::mint_supplier_cap(®istry, &clock, scenario.ctx()); + + // Supply 400k initially + let initial_supply = coin::mint_for_testing(400_000, scenario.ctx()); + pool.supply(®istry, &supplier_cap, initial_supply, option::none(), &clock); + + // Try to withdraw 110k (exceeds 100k rate limit) - should fail + let _withdrawn = pool.withdraw( + ®istry, + &supplier_cap, + option::some(110_000), + &clock, + scenario.ctx(), + ); + + abort +} diff --git a/packages/deepbook_margin/tests/tpsl_tests.move b/packages/deepbook_margin/tests/tpsl_tests.move new file mode 100644 index 000000000..fe478fe79 --- /dev/null +++ b/packages/deepbook_margin/tests/tpsl_tests.move @@ -0,0 +1,3130 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +#[test_only] +module deepbook_margin::tpsl_tests; + +use deepbook::{constants, pool::Pool, registry::Registry}; +use deepbook_margin::{ + margin_manager::{Self, MarginManager}, + margin_pool, + margin_registry::{MarginRegistry, MarginAdminCap, MaintainerCap}, + test_constants::{Self, SUI, USDC}, + test_helpers::{ + setup_margin_registry, + create_margin_pool, + default_protocol_config, + get_margin_pool_caps, + create_pool_for_testing, + enable_deepbook_margin_on_pool, + cleanup_margin_test, + mint_coin, + build_pyth_price_info_object, + destroy_2, + return_shared_2 + }, + tpsl +}; +use std::unit_test::destroy; +use sui::test_scenario::{Self, return_shared}; + +// Helper to create a SUI/USDC margin trading environment +// SUI has 9 decimals, USDC has 6 decimals +// Price of $1 = 10^6 (since SUI has 9 decimals and USDC has 6 decimals, price = USD * 10^9 / 10^3) +fun setup_sui_usdc_deepbook_margin(): ( + test_scenario::Scenario, + sui::clock::Clock, + MarginAdminCap, + MaintainerCap, + ID, + ID, + ID, + ID, +) { + let (mut scenario, mut clock, admin_cap, maintainer_cap) = setup_margin_registry(); + + clock.set_for_testing(1000000); + let usdc_pool_id = create_margin_pool( + &mut scenario, + &maintainer_cap, + default_protocol_config(), + &clock, + ); + let sui_pool_id = create_margin_pool( + &mut scenario, + &maintainer_cap, + default_protocol_config(), + &clock, + ); + + scenario.next_tx(test_constants::admin()); + let (usdc_pool_cap, sui_pool_cap) = get_margin_pool_caps(&mut scenario, usdc_pool_id); + + let (pool_id, registry_id) = create_pool_for_testing(&mut scenario); + scenario.next_tx(test_constants::admin()); + let mut registry = scenario.take_shared(); + enable_deepbook_margin_on_pool( + pool_id, + &mut registry, + &admin_cap, + &clock, + &mut scenario, + ); + return_shared(registry); + + scenario.next_tx(test_constants::admin()); + let mut sui_pool = scenario.take_shared_by_id>(sui_pool_id); + let mut usdc_pool = scenario.take_shared_by_id>(usdc_pool_id); + let registry = scenario.take_shared(); + let supplier_cap = margin_pool::mint_supplier_cap(®istry, &clock, scenario.ctx()); + + usdc_pool.supply( + ®istry, + &supplier_cap, + mint_coin(1_000_000 * test_constants::usdc_multiplier(), scenario.ctx()), + option::none(), + &clock, + ); + sui_pool.supply( + ®istry, + &supplier_cap, + mint_coin(1_000_000 * test_constants::sui_multiplier(), scenario.ctx()), + option::none(), + &clock, + ); + + sui_pool.enable_deepbook_pool_for_loan(®istry, pool_id, &sui_pool_cap, &clock); + usdc_pool.enable_deepbook_pool_for_loan(®istry, pool_id, &usdc_pool_cap, &clock); + + test_scenario::return_shared(usdc_pool); + test_scenario::return_shared(sui_pool); + test_scenario::return_shared(registry); + scenario.return_to_sender(sui_pool_cap); + scenario.return_to_sender(usdc_pool_cap); + destroy(supplier_cap); + + (scenario, clock, admin_cap, maintainer_cap, usdc_pool_id, sui_pool_id, pool_id, registry_id) +} + +// Helper to set up orderbook liquidity +fun setup_orderbook_liquidity( + scenario: &mut test_scenario::Scenario, + pool_id: ID, + clock: &sui::clock::Clock, +) { + use deepbook::balance_manager; + use token::deep::DEEP; + + scenario.next_tx(test_constants::user2()); + let mut pool = scenario.take_shared_by_id>(pool_id); + let mut balance_manager = balance_manager::new(scenario.ctx()); + + // Deposit plenty of assets for liquidity provision + balance_manager.deposit( + mint_coin(1000 * test_constants::sui_multiplier(), scenario.ctx()), + scenario.ctx(), + ); + balance_manager.deposit( + mint_coin( + 1_000_000_000 * test_constants::usdc_multiplier(), + scenario.ctx(), + ), // 1B USDC + scenario.ctx(), + ); + balance_manager.deposit( + mint_coin(10000 * test_constants::deep_multiplier(), scenario.ctx()), + scenario.ctx(), + ); + + let trade_proof = balance_manager.generate_proof_as_owner(scenario.ctx()); + + // Place ask orders (sell SUI) at different prices + // Price in oracle terms: (USD_price / USDC_price) * 10^9 / 10^3 + pool.place_limit_order( + &mut balance_manager, + &trade_proof, + 1, + constants::no_restriction(), + constants::self_matching_allowed(), + 2_500_000, // $2.50 + 100 * test_constants::sui_multiplier(), + false, // is_bid = false (ask) + false, + constants::max_u64(), + clock, + scenario.ctx(), + ); + + pool.place_limit_order( + &mut balance_manager, + &trade_proof, + 2, + constants::no_restriction(), + constants::self_matching_allowed(), + 3_000_000, // $3.00 + 100 * test_constants::sui_multiplier(), + false, // is_bid = false (ask) + false, + constants::max_u64(), + clock, + scenario.ctx(), + ); + + // Place bid orders (buy SUI) at different prices + pool.place_limit_order( + &mut balance_manager, + &trade_proof, + 3, + constants::no_restriction(), + constants::self_matching_allowed(), + 1_500_000, // $1.50 + 100 * test_constants::sui_multiplier(), + true, // is_bid = true + false, + constants::max_u64(), + clock, + scenario.ctx(), + ); + + pool.place_limit_order( + &mut balance_manager, + &trade_proof, + 4, + constants::no_restriction(), + constants::self_matching_allowed(), + 1_000_000, // $1.00 + 100 * test_constants::sui_multiplier(), + true, // is_bid = true + false, + constants::max_u64(), + clock, + scenario.ctx(), + ); + + let _balance_manager_id = balance_manager.id(); + transfer::public_share_object(balance_manager); + return_shared(pool); +} + +// Helper to build price info objects with specific prices +// For SUI: price_usd is in cents (e.g., 100 = $1.00, 95 = $0.95, 200 = $2.00) +fun build_sui_price_info_object_with_price( + scenario: &mut test_scenario::Scenario, + price_cents: u64, + clock: &sui::clock::Clock, +): pyth::price_info::PriceInfoObject { + build_pyth_price_info_object( + scenario, + test_constants::sui_price_feed_id(), + price_cents * test_constants::pyth_multiplier() / 100, // Convert cents to Pyth format + 50000, + test_constants::pyth_decimals(), + clock.timestamp_ms() / 1000, + ) +} + +// Helper to build USDC price info object (always $1.00) +fun build_usdc_price_info_object( + scenario: &mut test_scenario::Scenario, + clock: &sui::clock::Clock, +): pyth::price_info::PriceInfoObject { + build_pyth_price_info_object( + scenario, + test_constants::usdc_price_feed_id(), + 1 * test_constants::pyth_multiplier(), // $1.00 + 50000, + test_constants::pyth_decimals(), + clock.timestamp_ms() / 1000, + ) +} + +#[test] +fun test_tpsl_trigger_below_executed() { + // This test demonstrates a stop-loss scenario where ALICE sets up a conditional order + // to sell SUI when its price drops below a trigger price. + // + // Setup: + // - ALICE deposits 10,000 SUI as collateral when SUI = $2.00 + // - ALICE creates a stop-loss order: if SUI price drops below $1.50, sell 100 SUI at $0.80 + // - BOB triggers the order execution when SUI price drops to $0.95 + // + // Price calculations (SUI has 9 decimals, USDC has 6 decimals): + // - Oracle price = (SUI_USD_price / USDC_USD_price) * float_scaling * 10^(9-6) + // - $2.00 SUI = 2.0 * 10^9 / 10^3 = 2_000_000 + // - $1.50 trigger = 1.5 * 10^9 / 10^3 = 1_500_000 + // - $0.95 SUI = 0.95 * 10^9 / 10^3 = 950_000 + + let ( + mut scenario, + clock, + admin_cap, + maintainer_cap, + _usdc_pool_id, + _sui_pool_id, + _pool_id, + registry_id, + ) = setup_sui_usdc_deepbook_margin(); + + // USER1 = ALICE creates a margin manager + scenario.next_tx(test_constants::user1()); + let mut margin_registry = scenario.take_shared(); + let pool = scenario.take_shared>(); + let deepbook_registry = scenario.take_shared_by_id(registry_id); + margin_manager::new( + &pool, + &deepbook_registry, + &mut margin_registry, + &clock, + scenario.ctx(), + ); + return_shared(deepbook_registry); + return_shared(pool); + + scenario.next_tx(test_constants::user1()); + let mut mm = scenario.take_shared>(); + let pool = scenario.take_shared>(); + + // Initial prices: SUI = $2.00, USDC = $1.00 + // Oracle price calculation: + // Price = (base_USD / quote_USD) * float_scaling * 10^(base_decimals - quote_decimals) + // = (2.00 / 1.00) * 10^9 / 10^3 = 2 * 10^6 = 2_000_000 + let sui_price_high = build_sui_price_info_object_with_price(&mut scenario, 200, &clock); // $2.00 + let usdc_price = build_usdc_price_info_object(&mut scenario, &clock); // $1.00 + + // Deposit collateral (SUI) + mm.deposit( + &margin_registry, + &sui_price_high, + &usdc_price, + mint_coin(10000 * test_constants::sui_multiplier(), scenario.ctx()), + &clock, + scenario.ctx(), + ); + + // Add conditional order: trigger_is_below = true, trigger_price = $1.50 + // This means: trigger when SUI price drops below $1.50 + // When triggered, SELL SUI (is_bid = false) to protect against further losses + // Trigger price = (1.50 / 1.00) * 10^9 / 10^3 = 1.5 * 10^6 = 1_500_000 + let condition = tpsl::new_condition( + true, // trigger_is_below + 1_500_000, // trigger price: $1.50 + ); + let pending_order = tpsl::new_pending_limit_order( + 1, // client_order_id + constants::no_restriction(), + constants::self_matching_allowed(), + 800_000, // price: $0.80 (sell when price drops) + 100 * test_constants::sui_multiplier(), // quantity: 100 SUI + false, // is_bid = false (SELL SUI for USDC) + false, // pay_with_deep + constants::max_u64(), // expire_timestamp + ); + + mm.add_conditional_order( + &pool, + &sui_price_high, + &usdc_price, + &margin_registry, + 1, // conditional_order_identifier + condition, + pending_order, + &clock, + scenario.ctx(), + ); + + // Verify conditional order was added + assert!(mm.conditional_order_ids().length() == 1); + + destroy_2!(sui_price_high, usdc_price); + return_shared(pool); + return_shared(margin_registry); + + // USER2 = BOB executes conditional orders with oracle price that triggers the condition + // Update price to trigger: SUI drops to $0.95 < $1.50 trigger + // Oracle price = (0.95 / 1.00) * 10^9 / 10^3 = 0.95 * 10^6 = 950_000 + scenario.next_tx(test_constants::user2()); + let sui_price_low = build_sui_price_info_object_with_price(&mut scenario, 95, &clock); // $0.95 + let usdc_price = build_usdc_price_info_object(&mut scenario, &clock); + + let mut pool = scenario.take_shared>(); + let margin_registry = scenario.take_shared(); + + // Execute conditional orders - should trigger and place order + let order_infos = mm.execute_conditional_orders( + &mut pool, + &sui_price_low, + &usdc_price, + &margin_registry, + 10, // max_orders_to_execute + &clock, + scenario.ctx(), + ); + + // Verify order was executed with accurate data + assert!(order_infos.length() == 1); + let order_info = &order_infos[0]; + + // Validate order details + assert!(order_info.client_order_id() == 1); // client_order_id from pending_order + assert!(order_info.price() == 800_000); // price: $0.80 + assert!(order_info.original_quantity() == 100 * test_constants::sui_multiplier()); // 100 SUI + assert!(order_info.is_bid() == false); // Sell order + assert!(order_info.balance_manager_id() == object::id(mm.balance_manager())); + + destroy(order_infos[0]); + + // Verify conditional order was removed after execution + assert!(mm.conditional_order_ids().length() == 0); + + destroy_2!(sui_price_low, usdc_price); + return_shared_2!(mm, pool); + + let deepbook_registry = scenario.take_shared_by_id(registry_id); + return_shared(deepbook_registry); + cleanup_margin_test(margin_registry, admin_cap, maintainer_cap, clock, scenario); +} + +#[test] +fun test_tpsl_trigger_above_executed() { + // This test demonstrates a take-profit scenario where ALICE sets up a conditional order + // to sell SUI when its price rises above a trigger price. + // + // Setup: + // - ALICE deposits 10,000 SUI as collateral when SUI = $1.50 + // - ALICE creates a take-profit order: if SUI price rises above $2.00, sell 100 SUI at $2.50 + // - BOB triggers the order execution when SUI price rises to $2.10 + // + // Price calculations (SUI has 9 decimals, USDC has 6 decimals): + // - Oracle price = (SUI_USD_price / USDC_USD_price) * float_scaling * 10^(9-6) + // - $1.50 SUI = 1.5 * 10^9 / 10^3 = 1_500_000 + // - $2.00 trigger = 2.0 * 10^9 / 10^3 = 2_000_000 + // - $2.10 SUI = 2.1 * 10^9 / 10^3 = 2_100_000 + + let ( + mut scenario, + clock, + admin_cap, + maintainer_cap, + _usdc_pool_id, + _sui_pool_id, + _pool_id, + registry_id, + ) = setup_sui_usdc_deepbook_margin(); + + // USER1 = ALICE creates a margin manager + scenario.next_tx(test_constants::user1()); + let mut margin_registry = scenario.take_shared(); + let pool = scenario.take_shared>(); + let deepbook_registry = scenario.take_shared_by_id(registry_id); + margin_manager::new( + &pool, + &deepbook_registry, + &mut margin_registry, + &clock, + scenario.ctx(), + ); + return_shared(deepbook_registry); + return_shared(pool); + + scenario.next_tx(test_constants::user1()); + let mut mm = scenario.take_shared>(); + let pool = scenario.take_shared>(); + + // Initial prices: SUI = $1.50, USDC = $1.00 + // Oracle price calculation: + // Price = (base_USD / quote_USD) * float_scaling * 10^(base_decimals - quote_decimals) + // = (1.50 / 1.00) * 10^9 / 10^3 = 1.5 * 10^6 = 1_500_000 + let sui_price_low = build_sui_price_info_object_with_price(&mut scenario, 150, &clock); // $1.50 + let usdc_price = build_usdc_price_info_object(&mut scenario, &clock); // $1.00 + + // Deposit collateral (SUI) + mm.deposit( + &margin_registry, + &sui_price_low, + &usdc_price, + mint_coin(10000 * test_constants::sui_multiplier(), scenario.ctx()), + &clock, + scenario.ctx(), + ); + + // Add conditional order: trigger_is_below = false, trigger_price = $2.00 + // This means: trigger when SUI price rises above $2.00 + // When triggered, SELL SUI (is_bid = false) to take profits + // Trigger price = (2.00 / 1.00) * 10^9 / 10^3 = 2.0 * 10^6 = 2_000_000 + let condition = tpsl::new_condition( + false, // trigger_is_below = false (trigger_above) + 2_000_000, // trigger price: $2.00 + ); + let pending_order = tpsl::new_pending_limit_order( + 1, // client_order_id + constants::no_restriction(), + constants::self_matching_allowed(), + 2_500_000, // price: $2.50 (sell at higher price) + 100 * test_constants::sui_multiplier(), // quantity: 100 SUI + false, // is_bid = false (SELL SUI for USDC) + false, // pay_with_deep + constants::max_u64(), // expire_timestamp + ); + + mm.add_conditional_order( + &pool, + &sui_price_low, + &usdc_price, + &margin_registry, + 1, // conditional_order_identifier + condition, + pending_order, + &clock, + scenario.ctx(), + ); + + // Verify conditional order was added + assert!(mm.conditional_order_ids().length() == 1); + + destroy_2!(sui_price_low, usdc_price); + return_shared(pool); + return_shared(margin_registry); + + // USER2 = BOB executes conditional orders with oracle price that triggers the condition + // Update price to trigger: SUI rises to $2.10 > $2.00 trigger + // Oracle price = (2.10 / 1.00) * 10^9 / 10^3 = 2.1 * 10^6 = 2_100_000 + scenario.next_tx(test_constants::user2()); + let sui_price_high = build_sui_price_info_object_with_price(&mut scenario, 210, &clock); // $2.10 + let usdc_price = build_usdc_price_info_object(&mut scenario, &clock); + + let mut pool = scenario.take_shared>(); + let margin_registry = scenario.take_shared(); + + // Execute conditional orders - should trigger and place order + let order_infos = mm.execute_conditional_orders( + &mut pool, + &sui_price_high, + &usdc_price, + &margin_registry, + 10, // max_orders_to_execute + &clock, + scenario.ctx(), + ); + + // Verify order was executed with accurate data + assert!(order_infos.length() == 1); + let order_info = &order_infos[0]; + + // Validate order details + assert!(order_info.client_order_id() == 1); // client_order_id from pending_order + assert!(order_info.price() == 2_500_000); // price: $2.50 + assert!(order_info.original_quantity() == 100 * test_constants::sui_multiplier()); // 100 SUI + assert!(order_info.is_bid() == false); // Sell order + assert!(order_info.balance_manager_id() == object::id(mm.balance_manager())); + + destroy(order_infos[0]); + + // Verify conditional order was removed after execution + assert!(mm.conditional_order_ids().length() == 0); + + destroy_2!(sui_price_high, usdc_price); + return_shared_2!(mm, pool); + + let deepbook_registry = scenario.take_shared_by_id(registry_id); + return_shared(deepbook_registry); + cleanup_margin_test(margin_registry, admin_cap, maintainer_cap, clock, scenario); +} + +#[test] +fun test_tpsl_orders_sorted_correctly() { + // This test verifies that conditional orders are correctly sorted: + // - trigger_below orders: sorted high to low by trigger_price + // - trigger_above orders: sorted low to high by trigger_price + // + // ALICE adds 8 conditional orders at different trigger prices and verifies the sorting. + + let ( + mut scenario, + clock, + admin_cap, + maintainer_cap, + _usdc_pool_id, + _sui_pool_id, + _pool_id, + registry_id, + ) = setup_sui_usdc_deepbook_margin(); + + // USER1 = ALICE creates a margin manager + scenario.next_tx(test_constants::user1()); + let mut margin_registry = scenario.take_shared(); + let pool = scenario.take_shared>(); + let deepbook_registry = scenario.take_shared_by_id(registry_id); + margin_manager::new( + &pool, + &deepbook_registry, + &mut margin_registry, + &clock, + scenario.ctx(), + ); + return_shared(deepbook_registry); + return_shared(pool); + + scenario.next_tx(test_constants::user1()); + let mut mm = scenario.take_shared>(); + let pool = scenario.take_shared>(); + + // Initial prices: SUI = $2.00, USDC = $1.00 + let sui_price = build_sui_price_info_object_with_price(&mut scenario, 200, &clock); // $2.00 + let usdc_price = build_usdc_price_info_object(&mut scenario, &clock); // $1.00 + + // Deposit collateral (SUI) + mm.deposit( + &margin_registry, + &sui_price, + &usdc_price, + mint_coin(10000 * test_constants::sui_multiplier(), scenario.ctx()), + &clock, + scenario.ctx(), + ); + + // Add 4 trigger_below orders at different prices (intentionally out of order) + // Expected sorted order (high to low): 1.8, 1.5, 1.2, 0.9 + let trigger_prices_below = vector[ + 1_500_000, // $1.50 - ID 1 + 900_000, // $0.90 - ID 2 + 1_800_000, // $1.80 - ID 3 + 1_200_000, // $1.20 - ID 4 + ]; + + let mut i = 0; + while (i < trigger_prices_below.length()) { + let condition = tpsl::new_condition( + true, // trigger_is_below + trigger_prices_below[i], + ); + let pending_order = tpsl::new_pending_limit_order( + i + 1, // client_order_id + constants::no_restriction(), + constants::self_matching_allowed(), + 800_000, // price: $0.80 + 100 * test_constants::sui_multiplier(), + false, // is_bid = false (SELL) + false, + constants::max_u64(), + ); + + mm.add_conditional_order( + &pool, + &sui_price, + &usdc_price, + &margin_registry, + i + 1, // conditional_order_id + condition, + pending_order, + &clock, + scenario.ctx(), + ); + i = i + 1; + }; + + // Add 4 trigger_above orders at different prices (intentionally out of order) + // Expected sorted order (low to high): 2.2, 2.5, 2.8, 3.1 + let trigger_prices_above = vector[ + 2_500_000, // $2.50 - ID 5 + 3_100_000, // $3.10 - ID 6 + 2_200_000, // $2.20 - ID 7 + 2_800_000, // $2.80 - ID 8 + ]; + + i = 0; + while (i < trigger_prices_above.length()) { + let condition = tpsl::new_condition( + false, // trigger_is_below = false (trigger_above) + trigger_prices_above[i], + ); + let pending_order = tpsl::new_pending_limit_order( + i + 5, // client_order_id (5, 6, 7, 8) + constants::no_restriction(), + constants::self_matching_allowed(), + 3_500_000, // price: $3.50 + 100 * test_constants::sui_multiplier(), + false, // is_bid = false (SELL) + false, + constants::max_u64(), + ); + + mm.add_conditional_order( + &pool, + &sui_price, + &usdc_price, + &margin_registry, + i + 5, // conditional_order_id (5, 6, 7, 8) + condition, + pending_order, + &clock, + scenario.ctx(), + ); + i = i + 1; + }; + + // Verify all 8 orders were added + let order_ids = mm.conditional_order_ids(); + assert!(order_ids.length() == 8); + + // Verify trigger_below orders are sorted high to low + // Expected order: ID 3 ($1.80), ID 1 ($1.50), ID 4 ($1.20), ID 2 ($0.90) + let order_1 = mm.conditional_order(order_ids[0]); + let order_2 = mm.conditional_order(order_ids[1]); + let order_3 = mm.conditional_order(order_ids[2]); + let order_4 = mm.conditional_order(order_ids[3]); + + assert!(order_1.condition().trigger_below_price() == true); + assert!(order_2.condition().trigger_below_price() == true); + assert!(order_3.condition().trigger_below_price() == true); + assert!(order_4.condition().trigger_below_price() == true); + + assert!(order_1.condition().trigger_price() == 1_800_000); // $1.80 (highest) + assert!(order_2.condition().trigger_price() == 1_500_000); // $1.50 + assert!(order_3.condition().trigger_price() == 1_200_000); // $1.20 + assert!(order_4.condition().trigger_price() == 900_000); // $0.90 (lowest) + + // Verify decreasing order (high to low) + assert!(order_1.condition().trigger_price() > order_2.condition().trigger_price()); + assert!(order_2.condition().trigger_price() > order_3.condition().trigger_price()); + assert!(order_3.condition().trigger_price() > order_4.condition().trigger_price()); + + // Verify trigger_above orders are sorted low to high + // Expected order: ID 7 ($2.20), ID 5 ($2.50), ID 8 ($2.80), ID 6 ($3.10) + let order_5 = mm.conditional_order(order_ids[4]); + let order_6 = mm.conditional_order(order_ids[5]); + let order_7 = mm.conditional_order(order_ids[6]); + let order_8 = mm.conditional_order(order_ids[7]); + + assert!(order_5.condition().trigger_below_price() == false); + assert!(order_6.condition().trigger_below_price() == false); + assert!(order_7.condition().trigger_below_price() == false); + assert!(order_8.condition().trigger_below_price() == false); + + assert!(order_5.condition().trigger_price() == 2_200_000); // $2.20 (lowest) + assert!(order_6.condition().trigger_price() == 2_500_000); // $2.50 + assert!(order_7.condition().trigger_price() == 2_800_000); // $2.80 + assert!(order_8.condition().trigger_price() == 3_100_000); // $3.10 (highest) + + // Verify increasing order (low to high) + assert!(order_5.condition().trigger_price() < order_6.condition().trigger_price()); + assert!(order_6.condition().trigger_price() < order_7.condition().trigger_price()); + assert!(order_7.condition().trigger_price() < order_8.condition().trigger_price()); + + destroy_2!(sui_price, usdc_price); + return_shared_2!(mm, pool); + + scenario.next_tx(test_constants::user1()); + let deepbook_registry = scenario.take_shared_by_id(registry_id); + return_shared(deepbook_registry); + cleanup_margin_test(margin_registry, admin_cap, maintainer_cap, clock, scenario); +} + +#[test] +fun test_tpsl_orders_with_same_trigger_price_maintain_fifo_order() { + // This test verifies that the sort is stable: orders with the same trigger price + // maintain their insertion order (FIFO - first in, first out). + // The insertion_sort_by! macro requires >= and <= comparisons for stability. + + let ( + mut scenario, + clock, + admin_cap, + maintainer_cap, + _usdc_pool_id, + _sui_pool_id, + _pool_id, + registry_id, + ) = setup_sui_usdc_deepbook_margin(); + + scenario.next_tx(test_constants::user1()); + let mut margin_registry = scenario.take_shared(); + let pool = scenario.take_shared>(); + let deepbook_registry = scenario.take_shared_by_id(registry_id); + margin_manager::new( + &pool, + &deepbook_registry, + &mut margin_registry, + &clock, + scenario.ctx(), + ); + return_shared(deepbook_registry); + return_shared(pool); + + scenario.next_tx(test_constants::user1()); + let mut mm = scenario.take_shared>(); + let pool = scenario.take_shared>(); + + let sui_price = build_sui_price_info_object_with_price(&mut scenario, 200, &clock); // $2.00 + let usdc_price = build_usdc_price_info_object(&mut scenario, &clock); // $1.00 + + mm.deposit( + &margin_registry, + &sui_price, + &usdc_price, + mint_coin(10000 * test_constants::sui_multiplier(), scenario.ctx()), + &clock, + scenario.ctx(), + ); + + // Add two trigger_below orders with the SAME trigger price + // Order ID 1 added first, Order ID 2 added second + let same_trigger_below_price = 1_500_000; // $1.50 + + let condition_1 = tpsl::new_condition(true, same_trigger_below_price); + let pending_order_1 = tpsl::new_pending_limit_order( + 1, + constants::no_restriction(), + constants::self_matching_allowed(), + 800_000, + 100 * test_constants::sui_multiplier(), + false, + false, + constants::max_u64(), + ); + mm.add_conditional_order( + &pool, + &sui_price, + &usdc_price, + &margin_registry, + 1, // conditional_order_id + condition_1, + pending_order_1, + &clock, + scenario.ctx(), + ); + + let condition_2 = tpsl::new_condition(true, same_trigger_below_price); + let pending_order_2 = tpsl::new_pending_limit_order( + 2, + constants::no_restriction(), + constants::self_matching_allowed(), + 800_000_000_000, + 100 * test_constants::sui_multiplier(), + false, + false, + constants::max_u64(), + ); + mm.add_conditional_order( + &pool, + &sui_price, + &usdc_price, + &margin_registry, + 2, // conditional_order_id + condition_2, + pending_order_2, + &clock, + scenario.ctx(), + ); + + // Add two trigger_above orders with the SAME trigger price + // Order ID 3 added first, Order ID 4 added second + let same_trigger_above_price = 2_500_000_000_000; // $2.50 + + let condition_3 = tpsl::new_condition(false, same_trigger_above_price); + let pending_order_3 = tpsl::new_pending_limit_order( + 3, + constants::no_restriction(), + constants::self_matching_allowed(), + 3_000_000_000_000, + 100 * test_constants::sui_multiplier(), + false, + false, + constants::max_u64(), + ); + mm.add_conditional_order( + &pool, + &sui_price, + &usdc_price, + &margin_registry, + 3, // conditional_order_id + condition_3, + pending_order_3, + &clock, + scenario.ctx(), + ); + + let condition_4 = tpsl::new_condition(false, same_trigger_above_price); + let pending_order_4 = tpsl::new_pending_limit_order( + 4, + constants::no_restriction(), + constants::self_matching_allowed(), + 3_000_000_000_000, + 100 * test_constants::sui_multiplier(), + false, + false, + constants::max_u64(), + ); + mm.add_conditional_order( + &pool, + &sui_price, + &usdc_price, + &margin_registry, + 4, // conditional_order_id + condition_4, + pending_order_4, + &clock, + scenario.ctx(), + ); + + // Verify all 4 orders were added + let order_ids = mm.conditional_order_ids(); + assert!(order_ids.length() == 4); + + // Verify trigger_below orders maintain FIFO order (order 1 before order 2) + let below_order_1 = mm.conditional_order(order_ids[0]); + let below_order_2 = mm.conditional_order(order_ids[1]); + + assert!(below_order_1.condition().trigger_below_price() == true); + assert!(below_order_2.condition().trigger_below_price() == true); + assert!(below_order_1.condition().trigger_price() == same_trigger_below_price); + assert!(below_order_2.condition().trigger_price() == same_trigger_below_price); + // Order 1 (added first) should appear before Order 2 (added second) + assert!(below_order_1.conditional_order_id() == 1); + assert!(below_order_2.conditional_order_id() == 2); + + // Verify trigger_above orders maintain FIFO order (order 3 before order 4) + let above_order_1 = mm.conditional_order(order_ids[2]); + let above_order_2 = mm.conditional_order(order_ids[3]); + + assert!(above_order_1.condition().trigger_below_price() == false); + assert!(above_order_2.condition().trigger_below_price() == false); + assert!(above_order_1.condition().trigger_price() == same_trigger_above_price); + assert!(above_order_2.condition().trigger_price() == same_trigger_above_price); + // Order 3 (added first) should appear before Order 4 (added second) + assert!(above_order_1.conditional_order_id() == 3); + assert!(above_order_2.conditional_order_id() == 4); + + destroy_2!(sui_price, usdc_price); + return_shared_2!(mm, pool); + + scenario.next_tx(test_constants::user1()); + let deepbook_registry = scenario.take_shared_by_id(registry_id); + return_shared(deepbook_registry); + cleanup_margin_test(margin_registry, admin_cap, maintainer_cap, clock, scenario); +} + +#[test] +fun test_tpsl_trigger_price_getters() { + // This test verifies the lowest_trigger_above_price and highest_trigger_below_price functions: + // - Returns default values when no orders exist + // - Returns correct values from the first element of each sorted vector + + let ( + mut scenario, + clock, + admin_cap, + maintainer_cap, + _usdc_pool_id, + _sui_pool_id, + _pool_id, + registry_id, + ) = setup_sui_usdc_deepbook_margin(); + + // USER1 = ALICE creates a margin manager + scenario.next_tx(test_constants::user1()); + let mut margin_registry = scenario.take_shared(); + let pool = scenario.take_shared>(); + let deepbook_registry = scenario.take_shared_by_id(registry_id); + margin_manager::new( + &pool, + &deepbook_registry, + &mut margin_registry, + &clock, + scenario.ctx(), + ); + return_shared(deepbook_registry); + return_shared(pool); + + scenario.next_tx(test_constants::user1()); + let mut mm = scenario.take_shared>(); + let pool = scenario.take_shared>(); + + // Verify default values when no orders exist + assert!(mm.lowest_trigger_above_price() == constants::max_u64()); + assert!(mm.highest_trigger_below_price() == 0); + + // Initial prices: SUI = $2.00, USDC = $1.00 + let sui_price = build_sui_price_info_object_with_price(&mut scenario, 200, &clock); // $2.00 + let usdc_price = build_usdc_price_info_object(&mut scenario, &clock); // $1.00 + + // Deposit collateral (SUI) + mm.deposit( + &margin_registry, + &sui_price, + &usdc_price, + mint_coin(10000 * test_constants::sui_multiplier(), scenario.ctx()), + &clock, + scenario.ctx(), + ); + + // Add 4 trigger_below orders at different prices (intentionally out of order) + // After insertion, they will be sorted high to low: $1.80, $1.50, $1.20, $0.90 + // highest_trigger_below_price should return the first element: $1.80 + let trigger_prices_below = vector[ + 1_500_000, // $1.50 + 900_000, // $0.90 + 1_800_000, // $1.80 (this will be first after sorting) + 1_200_000, // $1.20 + ]; + + let mut i = 0; + while (i < trigger_prices_below.length()) { + let condition = tpsl::new_condition( + true, // trigger_is_below + trigger_prices_below[i], + ); + let pending_order = tpsl::new_pending_limit_order( + i + 1, // client_order_id + constants::no_restriction(), + constants::self_matching_allowed(), + 800_000, // price: $0.80 + 100 * test_constants::sui_multiplier(), + false, // is_bid = false (SELL) + false, + constants::max_u64(), + ); + + mm.add_conditional_order( + &pool, + &sui_price, + &usdc_price, + &margin_registry, + i + 1, // conditional_order_id + condition, + pending_order, + &clock, + scenario.ctx(), + ); + i = i + 1; + }; + + // Verify highest_trigger_below_price returns the highest price (first element) + assert!(mm.highest_trigger_below_price() == 1_800_000); // $1.80 + // lowest_trigger_above_price should still be default (no trigger_above orders yet) + assert!(mm.lowest_trigger_above_price() == constants::max_u64()); + + // Add 4 trigger_above orders at different prices (intentionally out of order) + // After insertion, they will be sorted low to high: $2.20, $2.50, $2.80, $3.10 + // lowest_trigger_above_price should return the first element: $2.20 + let trigger_prices_above = vector[ + 2_500_000, // $2.50 + 3_100_000, // $3.10 + 2_200_000, // $2.20 (this will be first after sorting) + 2_800_000, // $2.80 + ]; + + i = 0; + while (i < trigger_prices_above.length()) { + let condition = tpsl::new_condition( + false, // trigger_is_below = false (trigger_above) + trigger_prices_above[i], + ); + let pending_order = tpsl::new_pending_limit_order( + i + 5, // client_order_id (5, 6, 7, 8) + constants::no_restriction(), + constants::self_matching_allowed(), + 3_500_000, // price: $3.50 + 100 * test_constants::sui_multiplier(), + false, // is_bid = false (SELL) + false, + constants::max_u64(), + ); + + mm.add_conditional_order( + &pool, + &sui_price, + &usdc_price, + &margin_registry, + i + 5, // conditional_order_id (5, 6, 7, 8) + condition, + pending_order, + &clock, + scenario.ctx(), + ); + i = i + 1; + }; + + // Verify both getters return the correct first elements + assert!(mm.highest_trigger_below_price() == 1_800_000); // $1.80 (highest in trigger_below) + assert!(mm.lowest_trigger_above_price() == 2_200_000); // $2.20 (lowest in trigger_above) + + // Verify all orders are present + assert!(mm.conditional_order_ids().length() == 8); + + destroy_2!(sui_price, usdc_price); + return_shared_2!(mm, pool); + + scenario.next_tx(test_constants::user1()); + let deepbook_registry = scenario.take_shared_by_id(registry_id); + return_shared(deepbook_registry); + cleanup_margin_test(margin_registry, admin_cap, maintainer_cap, clock, scenario); +} + +#[test] +fun test_tpsl_trigger_below_market_order_executed() { + // This test demonstrates a stop-loss with MARKET ORDER where ALICE sets up a conditional order + // to sell SUI at market price when price drops below a trigger. + // + // Setup: + // - Orderbook has bid liquidity at $1.50 (100 SUI) and $1.00 (100 SUI) + // - Orderbook has ask liquidity at $2.50 (100 SUI) and $3.00 (100 SUI) + // - ALICE deposits 10,000 SUI when SUI = $2.00 + // - ALICE creates stop-loss: if price drops below $1.50, sell 150 SUI at market + // - BOB triggers when price drops to $0.95 + // + // Expected: Market sell (is_bid=false) fills against bids + // - 100 SUI at $1.50 = 150 USDC received + // - 50 SUI at $1.00 = 50 USDC received + // - Total: 150 SUI sold for 200 USDC + + let ( + mut scenario, + clock, + admin_cap, + maintainer_cap, + _usdc_pool_id, + _sui_pool_id, + pool_id, + registry_id, + ) = setup_sui_usdc_deepbook_margin(); + + // Set up orderbook liquidity + setup_orderbook_liquidity(&mut scenario, pool_id, &clock); + + // USER1 = ALICE creates a margin manager + scenario.next_tx(test_constants::user1()); + let mut margin_registry = scenario.take_shared(); + let pool = scenario.take_shared>(); + let deepbook_registry = scenario.take_shared_by_id(registry_id); + margin_manager::new( + &pool, + &deepbook_registry, + &mut margin_registry, + &clock, + scenario.ctx(), + ); + return_shared(deepbook_registry); + return_shared(pool); + + scenario.next_tx(test_constants::user1()); + let mut mm = scenario.take_shared>(); + let pool = scenario.take_shared>(); + + // Initial prices: SUI = $2.00, USDC = $1.00 + let sui_price_high = build_sui_price_info_object_with_price(&mut scenario, 200, &clock); // $2.00 + let usdc_price = build_usdc_price_info_object(&mut scenario, &clock); // $1.00 + + // Deposit collateral (SUI) + mm.deposit( + &margin_registry, + &sui_price_high, + &usdc_price, + mint_coin(10000 * test_constants::sui_multiplier(), scenario.ctx()), + &clock, + scenario.ctx(), + ); + + // Add conditional order: trigger_is_below = true, trigger_price = $1.50 + // When triggered, execute MARKET order to sell 150 SUI (< 200 bid liquidity available) + let condition = tpsl::new_condition( + true, // trigger_is_below + 1_500_000, // trigger price: $1.50 + ); + let pending_order = tpsl::new_pending_market_order( + 1, // client_order_id + constants::self_matching_allowed(), + 150 * test_constants::sui_multiplier(), // quantity: 150 SUI (< 200 bid liquidity) + false, // is_bid = false (SELL at market, fills against bids) + false, // pay_with_deep + ); + + mm.add_conditional_order( + &pool, + &sui_price_high, + &usdc_price, + &margin_registry, + 1, // conditional_order_identifier + condition, + pending_order, + &clock, + scenario.ctx(), + ); + + // Verify conditional order was added + assert!(mm.conditional_order_ids().length() == 1); + + destroy_2!(sui_price_high, usdc_price); + return_shared(pool); + return_shared(margin_registry); + + // USER2 = BOB executes conditional orders when price drops + scenario.next_tx(test_constants::user2()); + let sui_price_low = build_sui_price_info_object_with_price(&mut scenario, 95, &clock); // $0.95 + let usdc_price = build_usdc_price_info_object(&mut scenario, &clock); + + let mut pool = scenario.take_shared>(); + let margin_registry = scenario.take_shared(); + + // Execute conditional orders - should trigger and place market order + let order_infos = mm.execute_conditional_orders( + &mut pool, + &sui_price_low, + &usdc_price, + &margin_registry, + 10, // max_orders_to_execute + &clock, + scenario.ctx(), + ); + + // Verify order was executed with accurate data + assert!(order_infos.length() == 1); + let order_info = &order_infos[0]; + + // Validate order details + assert!(order_info.client_order_id() == 1); + assert!(order_info.original_quantity() == 150 * test_constants::sui_multiplier()); // 150 SUI + assert!(order_info.is_bid() == false); // Sell order + assert!(order_info.balance_manager_id() == object::id(mm.balance_manager())); + + // Validate fills - market sell fills against bid orders + let fills = order_info.fills(); + assert!(fills.length() == 2); // Two fills: 100 at $1.50, 50 at $1.00 + + // First fill: 100 SUI at $1.50 + assert!(fills[0].base_quantity() == 100 * test_constants::sui_multiplier()); + assert!(fills[0].quote_quantity() == 150_000_000); // 100 * 1.5 in pool units + + // Second fill: 50 SUI at $1.00 + assert!(fills[1].base_quantity() == 50 * test_constants::sui_multiplier()); + assert!(fills[1].quote_quantity() == 50_000_000); // 50 * 1.0 in pool units + + // Total executed quantity should be 150 SUI + assert!(order_info.executed_quantity() == 150 * test_constants::sui_multiplier()); + + // Total quote in pool units + assert!(order_info.cumulative_quote_quantity() == 200_000_000); + + destroy(order_infos[0]); + + // Verify conditional order was removed after execution + assert!(mm.conditional_order_ids().length() == 0); + + destroy_2!(sui_price_low, usdc_price); + return_shared_2!(mm, pool); + + let deepbook_registry = scenario.take_shared_by_id(registry_id); + return_shared(deepbook_registry); + cleanup_margin_test(margin_registry, admin_cap, maintainer_cap, clock, scenario); +} + +#[test] +fun test_tpsl_trigger_above_market_order_executed() { + // This test demonstrates a take-profit with MARKET ORDER where ALICE sets up a conditional order + // to sell SUI at market price when price rises above a trigger. + // + // Setup: + // - Orderbook has bid liquidity at $1.50 (100 SUI) and $1.00 (100 SUI) + // - Orderbook has ask liquidity at $2.50 (100 SUI) and $3.00 (100 SUI) + // - ALICE deposits 10,000 SUI when SUI = $1.50 + // - ALICE creates take-profit: if price rises above $2.00, sell 150 SUI at market + // - BOB triggers when price rises to $2.10 + // + // Expected: Market sell (is_bid=false) fills against bids + // - 100 SUI at $1.50 = 150 USDC received + // - 50 SUI at $1.00 = 50 USDC received + // - Total: 150 SUI sold for 200 USDC + + let ( + mut scenario, + clock, + admin_cap, + maintainer_cap, + _usdc_pool_id, + _sui_pool_id, + pool_id, + registry_id, + ) = setup_sui_usdc_deepbook_margin(); + + // Set up orderbook liquidity + setup_orderbook_liquidity(&mut scenario, pool_id, &clock); + + // USER1 = ALICE creates a margin manager + scenario.next_tx(test_constants::user1()); + let mut margin_registry = scenario.take_shared(); + let pool = scenario.take_shared>(); + let deepbook_registry = scenario.take_shared_by_id(registry_id); + margin_manager::new( + &pool, + &deepbook_registry, + &mut margin_registry, + &clock, + scenario.ctx(), + ); + return_shared(deepbook_registry); + return_shared(pool); + + scenario.next_tx(test_constants::user1()); + let mut mm = scenario.take_shared>(); + let pool = scenario.take_shared>(); + + // Initial prices: SUI = $1.50, USDC = $1.00 + let sui_price_low = build_sui_price_info_object_with_price(&mut scenario, 150, &clock); // $1.50 + let usdc_price = build_usdc_price_info_object(&mut scenario, &clock); // $1.00 + + // Deposit collateral (SUI) + mm.deposit( + &margin_registry, + &sui_price_low, + &usdc_price, + mint_coin(10000 * test_constants::sui_multiplier(), scenario.ctx()), + &clock, + scenario.ctx(), + ); + + // Add conditional order: trigger_is_below = false, trigger_price = $2.00 + // When triggered, execute MARKET order to sell 150 SUI (< 200 total available) + let condition = tpsl::new_condition( + false, // trigger_is_below = false (trigger_above) + 2_000_000, // trigger price: $2.00 + ); + let pending_order = tpsl::new_pending_market_order( + 1, // client_order_id + constants::self_matching_allowed(), + 150 * test_constants::sui_multiplier(), // quantity: 150 SUI (< 200 available) + false, // is_bid = false (SELL at market, crosses to fill against asks) + false, // pay_with_deep + ); + + mm.add_conditional_order( + &pool, + &sui_price_low, + &usdc_price, + &margin_registry, + 1, // conditional_order_identifier + condition, + pending_order, + &clock, + scenario.ctx(), + ); + + // Verify conditional order was added + assert!(mm.conditional_order_ids().length() == 1); + + destroy_2!(sui_price_low, usdc_price); + return_shared(pool); + return_shared(margin_registry); + + // USER2 = BOB executes conditional orders when price rises + scenario.next_tx(test_constants::user2()); + let sui_price_high = build_sui_price_info_object_with_price(&mut scenario, 210, &clock); // $2.10 + let usdc_price = build_usdc_price_info_object(&mut scenario, &clock); + + let mut pool = scenario.take_shared>(); + let margin_registry = scenario.take_shared(); + + // Execute conditional orders - should trigger and place market order + let order_infos = mm.execute_conditional_orders( + &mut pool, + &sui_price_high, + &usdc_price, + &margin_registry, + 10, // max_orders_to_execute + &clock, + scenario.ctx(), + ); + + // Verify order was executed with accurate data + assert!(order_infos.length() == 1); + let order_info = &order_infos[0]; + + // Validate order details + assert!(order_info.client_order_id() == 1); + assert!(order_info.original_quantity() == 150 * test_constants::sui_multiplier()); // 150 SUI + assert!(order_info.is_bid() == false); // Sell order + assert!(order_info.balance_manager_id() == object::id(mm.balance_manager())); + + // Validate fills - market sell fills against bid orders (same as trigger_below) + let fills = order_info.fills(); + assert!(fills.length() == 2); // Two fills: 100 at $1.50, 50 at $1.00 + + // First fill: 100 SUI at $1.50 + assert!(fills[0].base_quantity() == 100 * test_constants::sui_multiplier()); + assert!(fills[0].quote_quantity() == 150_000_000); // 100 * 1.5 in pool units + + // Second fill: 50 SUI at $1.00 + assert!(fills[1].base_quantity() == 50 * test_constants::sui_multiplier()); + assert!(fills[1].quote_quantity() == 50_000_000); // 50 * 1.0 in pool units + + // Total executed quantity should be 150 SUI + assert!(order_info.executed_quantity() == 150 * test_constants::sui_multiplier()); + + // Total quote in pool units (150 + 50 = 200 in pool units) + assert!(order_info.cumulative_quote_quantity() == 200_000_000); + + destroy(order_infos[0]); + + // Verify conditional order was removed after execution + assert!(mm.conditional_order_ids().length() == 0); + + destroy_2!(sui_price_high, usdc_price); + return_shared_2!(mm, pool); + + let deepbook_registry = scenario.take_shared_by_id(registry_id); + return_shared(deepbook_registry); + cleanup_margin_test(margin_registry, admin_cap, maintainer_cap, clock, scenario); +} + +#[test] +fun test_tpsl_cancel_conditional_order() { + // This test verifies canceling specific conditional orders + // - ALICE adds 8 conditional orders (4 trigger_below, 4 trigger_above) + // - ALICE cancels 2 orders (1 from each vector) + // - Verifies remaining 6 orders are still correctly sorted + + let ( + mut scenario, + clock, + admin_cap, + maintainer_cap, + _usdc_pool_id, + _sui_pool_id, + _pool_id, + registry_id, + ) = setup_sui_usdc_deepbook_margin(); + + // USER1 = ALICE creates a margin manager + scenario.next_tx(test_constants::user1()); + let mut margin_registry = scenario.take_shared(); + let pool = scenario.take_shared>(); + let deepbook_registry = scenario.take_shared_by_id(registry_id); + margin_manager::new( + &pool, + &deepbook_registry, + &mut margin_registry, + &clock, + scenario.ctx(), + ); + return_shared(deepbook_registry); + return_shared(pool); + + scenario.next_tx(test_constants::user1()); + let mut mm = scenario.take_shared>(); + let pool = scenario.take_shared>(); + + let sui_price = build_sui_price_info_object_with_price(&mut scenario, 200, &clock); // $2.00 + let usdc_price = build_usdc_price_info_object(&mut scenario, &clock); // $1.00 + + // Deposit collateral + mm.deposit( + &margin_registry, + &sui_price, + &usdc_price, + mint_coin(10000 * test_constants::sui_multiplier(), scenario.ctx()), + &clock, + scenario.ctx(), + ); + + // Add 4 trigger_below orders + let trigger_prices_below = vector[ + 1_500_000, // $1.50 - ID 1 + 900_000, // $0.90 - ID 2 + 1_800_000, // $1.80 - ID 3 + 1_200_000, // $1.20 - ID 4 + ]; + + let mut i = 0; + while (i < trigger_prices_below.length()) { + let condition = tpsl::new_condition(true, trigger_prices_below[i]); + let pending_order = tpsl::new_pending_limit_order( + i + 1, + constants::no_restriction(), + constants::self_matching_allowed(), + 800_000, + 100 * test_constants::sui_multiplier(), + false, + false, + constants::max_u64(), + ); + + mm.add_conditional_order( + &pool, + &sui_price, + &usdc_price, + &margin_registry, + i + 1, + condition, + pending_order, + &clock, + scenario.ctx(), + ); + i = i + 1; + }; + + // Add 4 trigger_above orders + let trigger_prices_above = vector[ + 2_500_000, // $2.50 - ID 5 + 3_100_000, // $3.10 - ID 6 + 2_200_000, // $2.20 - ID 7 + 2_800_000, // $2.80 - ID 8 + ]; + + i = 0; + while (i < trigger_prices_above.length()) { + let condition = tpsl::new_condition(false, trigger_prices_above[i]); + let pending_order = tpsl::new_pending_limit_order( + i + 5, + constants::no_restriction(), + constants::self_matching_allowed(), + 3_500_000, + 100 * test_constants::sui_multiplier(), + false, + false, + constants::max_u64(), + ); + + mm.add_conditional_order( + &pool, + &sui_price, + &usdc_price, + &margin_registry, + i + 5, + condition, + pending_order, + &clock, + scenario.ctx(), + ); + i = i + 1; + }; + + // Verify all 8 orders were added + assert!(mm.conditional_order_ids().length() == 8); + + // Cancel 2 orders: ID 3 from trigger_below ($1.80) and ID 5 from trigger_above ($2.50) + mm.cancel_conditional_order(3, &clock, scenario.ctx()); + mm.cancel_conditional_order(5, &clock, scenario.ctx()); + + // Verify 6 orders remain + let order_ids = mm.conditional_order_ids(); + assert!(order_ids.length() == 6); + + // Verify trigger_below orders are still sorted correctly (high to low) + // After canceling ID 3 ($1.80), should be: ID 1 ($1.50), ID 4 ($1.20), ID 2 ($0.90) + let order_1 = mm.conditional_order(order_ids[0]); + let order_2 = mm.conditional_order(order_ids[1]); + let order_3 = mm.conditional_order(order_ids[2]); + + assert!(order_1.condition().trigger_below_price() == true); + assert!(order_1.condition().trigger_price() == 1_500_000); // $1.50 (highest remaining) + assert!(order_2.condition().trigger_price() == 1_200_000); // $1.20 + assert!(order_3.condition().trigger_price() == 900_000); // $0.90 (lowest) + + // Verify trigger_above orders are still sorted correctly (low to high) + // After canceling ID 5 ($2.50), should be: ID 7 ($2.20), ID 8 ($2.80), ID 6 ($3.10) + let order_4 = mm.conditional_order(order_ids[3]); + let order_5 = mm.conditional_order(order_ids[4]); + let order_6 = mm.conditional_order(order_ids[5]); + + assert!(order_4.condition().trigger_below_price() == false); + assert!(order_4.condition().trigger_price() == 2_200_000); // $2.20 (lowest remaining) + assert!(order_5.condition().trigger_price() == 2_800_000); // $2.80 + assert!(order_6.condition().trigger_price() == 3_100_000); // $3.10 (highest) + + destroy_2!(sui_price, usdc_price); + return_shared_2!(mm, pool); + + scenario.next_tx(test_constants::user1()); + let deepbook_registry = scenario.take_shared_by_id(registry_id); + return_shared(deepbook_registry); + cleanup_margin_test(margin_registry, admin_cap, maintainer_cap, clock, scenario); +} + +#[test] +fun test_tpsl_cancel_all_conditional_orders() { + // This test verifies canceling all conditional orders at once + // - ALICE adds 8 conditional orders (4 trigger_below, 4 trigger_above) + // - ALICE calls cancel_all_conditional_orders + // - Verifies no orders remain + + let ( + mut scenario, + clock, + admin_cap, + maintainer_cap, + _usdc_pool_id, + _sui_pool_id, + _pool_id, + registry_id, + ) = setup_sui_usdc_deepbook_margin(); + + // USER1 = ALICE creates a margin manager + scenario.next_tx(test_constants::user1()); + let mut margin_registry = scenario.take_shared(); + let pool = scenario.take_shared>(); + let deepbook_registry = scenario.take_shared_by_id(registry_id); + margin_manager::new( + &pool, + &deepbook_registry, + &mut margin_registry, + &clock, + scenario.ctx(), + ); + return_shared(deepbook_registry); + return_shared(pool); + + scenario.next_tx(test_constants::user1()); + let mut mm = scenario.take_shared>(); + let pool = scenario.take_shared>(); + + let sui_price = build_sui_price_info_object_with_price(&mut scenario, 200, &clock); // $2.00 + let usdc_price = build_usdc_price_info_object(&mut scenario, &clock); // $1.00 + + // Deposit collateral + mm.deposit( + &margin_registry, + &sui_price, + &usdc_price, + mint_coin(10000 * test_constants::sui_multiplier(), scenario.ctx()), + &clock, + scenario.ctx(), + ); + + // Add 4 trigger_below orders + let trigger_prices_below = vector[1_500_000, 900_000, 1_800_000, 1_200_000]; + + let mut i = 0; + while (i < trigger_prices_below.length()) { + let condition = tpsl::new_condition(true, trigger_prices_below[i]); + let pending_order = tpsl::new_pending_limit_order( + i + 1, + constants::no_restriction(), + constants::self_matching_allowed(), + 800_000, + 100 * test_constants::sui_multiplier(), + false, + false, + constants::max_u64(), + ); + + mm.add_conditional_order( + &pool, + &sui_price, + &usdc_price, + &margin_registry, + i + 1, + condition, + pending_order, + &clock, + scenario.ctx(), + ); + i = i + 1; + }; + + // Add 4 trigger_above orders + let trigger_prices_above = vector[2_500_000, 3_100_000, 2_200_000, 2_800_000]; + + i = 0; + while (i < trigger_prices_above.length()) { + let condition = tpsl::new_condition(false, trigger_prices_above[i]); + let pending_order = tpsl::new_pending_limit_order( + i + 5, + constants::no_restriction(), + constants::self_matching_allowed(), + 3_500_000, + 100 * test_constants::sui_multiplier(), + false, + false, + constants::max_u64(), + ); + + mm.add_conditional_order( + &pool, + &sui_price, + &usdc_price, + &margin_registry, + i + 5, + condition, + pending_order, + &clock, + scenario.ctx(), + ); + i = i + 1; + }; + + // Verify all 8 orders were added + assert!(mm.conditional_order_ids().length() == 8); + + // Cancel all conditional orders + mm.cancel_all_conditional_orders(&clock, scenario.ctx()); + + // Verify no orders remain + assert!(mm.conditional_order_ids().length() == 0); + + // Verify trigger price getters return default values + assert!(mm.lowest_trigger_above_price() == constants::max_u64()); + assert!(mm.highest_trigger_below_price() == 0); + + destroy_2!(sui_price, usdc_price); + return_shared_2!(mm, pool); + + scenario.next_tx(test_constants::user1()); + let deepbook_registry = scenario.take_shared_by_id(registry_id); + return_shared(deepbook_registry); + cleanup_margin_test(margin_registry, admin_cap, maintainer_cap, clock, scenario); +} + +// === Error Code Tests === + +#[test] +#[expected_failure(abort_code = deepbook_margin::tpsl::EInvalidCondition)] +fun test_error_invalid_condition() { + // Test EInvalidCondition: trigger_below price must be < current price + // Current price is $2.00, but trigger is set to $2.50 (above current price) + + let ( + mut scenario, + clock, + admin_cap, + maintainer_cap, + _usdc_pool_id, + _sui_pool_id, + _pool_id, + registry_id, + ) = setup_sui_usdc_deepbook_margin(); + + scenario.next_tx(test_constants::user1()); + let mut margin_registry = scenario.take_shared(); + let pool = scenario.take_shared>(); + let deepbook_registry = scenario.take_shared_by_id(registry_id); + margin_manager::new( + &pool, + &deepbook_registry, + &mut margin_registry, + &clock, + scenario.ctx(), + ); + return_shared(deepbook_registry); + return_shared(pool); + + scenario.next_tx(test_constants::user1()); + let mut mm = scenario.take_shared>(); + let pool = scenario.take_shared>(); + + let sui_price = build_sui_price_info_object_with_price(&mut scenario, 200, &clock); // $2.00 + let usdc_price = build_usdc_price_info_object(&mut scenario, &clock); + + mm.deposit( + &margin_registry, + &sui_price, + &usdc_price, + mint_coin(10000 * test_constants::sui_multiplier(), scenario.ctx()), + &clock, + scenario.ctx(), + ); + + // Invalid: trigger_below with trigger price $2.50 > current price $2.00 + let condition = tpsl::new_condition(true, 2_500_000); // $2.50 + let pending_order = tpsl::new_pending_limit_order( + 1, + constants::no_restriction(), + constants::self_matching_allowed(), + 800_000, + 100 * test_constants::sui_multiplier(), + false, + false, + constants::max_u64(), + ); + + mm.add_conditional_order( + &pool, + &sui_price, + &usdc_price, + &margin_registry, + 1, + condition, + pending_order, + &clock, + scenario.ctx(), + ); + + destroy_2!(sui_price, usdc_price); + return_shared_2!(mm, pool); + + cleanup_margin_test(margin_registry, admin_cap, maintainer_cap, clock, scenario); +} + +#[test] +#[expected_failure(abort_code = deepbook_margin::tpsl::EInvalidCondition)] +fun test_error_invalid_condition_trigger_above() { + // Test EInvalidCondition: trigger_above price must be > current price + // Current price is $2.00, but trigger is set to $1.50 (below current price) + + let ( + mut scenario, + clock, + admin_cap, + maintainer_cap, + _usdc_pool_id, + _sui_pool_id, + _pool_id, + registry_id, + ) = setup_sui_usdc_deepbook_margin(); + + scenario.next_tx(test_constants::user1()); + let mut margin_registry = scenario.take_shared(); + let pool = scenario.take_shared>(); + let deepbook_registry = scenario.take_shared_by_id(registry_id); + margin_manager::new( + &pool, + &deepbook_registry, + &mut margin_registry, + &clock, + scenario.ctx(), + ); + return_shared(deepbook_registry); + return_shared(pool); + + scenario.next_tx(test_constants::user1()); + let mut mm = scenario.take_shared>(); + let pool = scenario.take_shared>(); + + let sui_price = build_sui_price_info_object_with_price(&mut scenario, 200, &clock); // $2.00 + let usdc_price = build_usdc_price_info_object(&mut scenario, &clock); + + mm.deposit( + &margin_registry, + &sui_price, + &usdc_price, + mint_coin(10000 * test_constants::sui_multiplier(), scenario.ctx()), + &clock, + scenario.ctx(), + ); + + // Invalid: trigger_above (false) with trigger price $1.50 < current price $2.00 + let condition = tpsl::new_condition(false, 1_500_000); // $1.50 + let pending_order = tpsl::new_pending_limit_order( + 1, + constants::no_restriction(), + constants::self_matching_allowed(), + 2_500_000, + 100 * test_constants::sui_multiplier(), + false, + false, + constants::max_u64(), + ); + + mm.add_conditional_order( + &pool, + &sui_price, + &usdc_price, + &margin_registry, + 1, + condition, + pending_order, + &clock, + scenario.ctx(), + ); + + destroy_2!(sui_price, usdc_price); + return_shared_2!(mm, pool); + + cleanup_margin_test(margin_registry, admin_cap, maintainer_cap, clock, scenario); +} + +#[test] +#[expected_failure(abort_code = deepbook_margin::tpsl::EConditionalOrderNotFound)] +fun test_error_conditional_order_not_found() { + // Test EConditionalOrderNotFound: trying to cancel a non-existent order + + let ( + mut scenario, + clock, + admin_cap, + maintainer_cap, + _usdc_pool_id, + _sui_pool_id, + _pool_id, + registry_id, + ) = setup_sui_usdc_deepbook_margin(); + + scenario.next_tx(test_constants::user1()); + let mut margin_registry = scenario.take_shared(); + let pool = scenario.take_shared>(); + let deepbook_registry = scenario.take_shared_by_id(registry_id); + margin_manager::new( + &pool, + &deepbook_registry, + &mut margin_registry, + &clock, + scenario.ctx(), + ); + return_shared(deepbook_registry); + return_shared(pool); + + scenario.next_tx(test_constants::user1()); + let mut mm = scenario.take_shared>(); + + // Try to cancel non-existent order ID 999 + mm.cancel_conditional_order(999, &clock, scenario.ctx()); + + return_shared(mm); + cleanup_margin_test(margin_registry, admin_cap, maintainer_cap, clock, scenario); +} + +#[test] +#[expected_failure(abort_code = deepbook_margin::tpsl::EMaxConditionalOrdersReached)] +fun test_error_max_conditional_orders_reached() { + // Test EMaxConditionalOrdersReached: trying to add more than 10 orders (max is 10) + + let ( + mut scenario, + clock, + admin_cap, + maintainer_cap, + _usdc_pool_id, + _sui_pool_id, + _pool_id, + registry_id, + ) = setup_sui_usdc_deepbook_margin(); + + scenario.next_tx(test_constants::user1()); + let mut margin_registry = scenario.take_shared(); + let pool = scenario.take_shared>(); + let deepbook_registry = scenario.take_shared_by_id(registry_id); + margin_manager::new( + &pool, + &deepbook_registry, + &mut margin_registry, + &clock, + scenario.ctx(), + ); + return_shared(deepbook_registry); + return_shared(pool); + + scenario.next_tx(test_constants::user1()); + let mut mm = scenario.take_shared>(); + let pool = scenario.take_shared>(); + + let sui_price = build_sui_price_info_object_with_price(&mut scenario, 200, &clock); + let usdc_price = build_usdc_price_info_object(&mut scenario, &clock); + + mm.deposit( + &margin_registry, + &sui_price, + &usdc_price, + mint_coin(10000 * test_constants::sui_multiplier(), scenario.ctx()), + &clock, + scenario.ctx(), + ); + + // Add 11 orders (max is 10) + let mut i = 0; + while (i < 11) { + let condition = tpsl::new_condition(true, 1_500_000); + let pending_order = tpsl::new_pending_limit_order( + i + 1, + constants::no_restriction(), + constants::self_matching_allowed(), + 800_000, + 100 * test_constants::sui_multiplier(), + false, + false, + constants::max_u64(), + ); + + mm.add_conditional_order( + &pool, + &sui_price, + &usdc_price, + &margin_registry, + i + 1, + condition, + pending_order, + &clock, + scenario.ctx(), + ); + i = i + 1; + }; + + destroy_2!(sui_price, usdc_price); + return_shared_2!(mm, pool); + + cleanup_margin_test(margin_registry, admin_cap, maintainer_cap, clock, scenario); +} + +#[test] +#[expected_failure(abort_code = deepbook_margin::tpsl::EInvalidTPSLOrderType)] +fun test_error_invalid_tpsl_order_type() { + // Test EInvalidTPSLOrderType: only no_restriction and immediate_or_cancel are allowed + // fill_or_kill is not allowed + + let _condition = tpsl::new_condition(true, 1_500_000); + let _pending_order = tpsl::new_pending_limit_order( + 1, + constants::fill_or_kill(), // This should fail + constants::self_matching_allowed(), + 800_000, + 100 * test_constants::sui_multiplier(), + false, + false, + constants::max_u64(), + ); +} + +#[test] +#[expected_failure(abort_code = deepbook_margin::tpsl::EDuplicateConditionalOrderIdentifier)] +fun test_error_duplicate_conditional_order_identifier() { + // Test EDuplicateConditionalOrderIdentifier: trying to add order with existing ID + + let ( + mut scenario, + clock, + admin_cap, + maintainer_cap, + _usdc_pool_id, + _sui_pool_id, + _pool_id, + registry_id, + ) = setup_sui_usdc_deepbook_margin(); + + scenario.next_tx(test_constants::user1()); + let mut margin_registry = scenario.take_shared(); + let pool = scenario.take_shared>(); + let deepbook_registry = scenario.take_shared_by_id(registry_id); + margin_manager::new( + &pool, + &deepbook_registry, + &mut margin_registry, + &clock, + scenario.ctx(), + ); + return_shared(deepbook_registry); + return_shared(pool); + + scenario.next_tx(test_constants::user1()); + let mut mm = scenario.take_shared>(); + let pool = scenario.take_shared>(); + + let sui_price = build_sui_price_info_object_with_price(&mut scenario, 200, &clock); + let usdc_price = build_usdc_price_info_object(&mut scenario, &clock); + + mm.deposit( + &margin_registry, + &sui_price, + &usdc_price, + mint_coin(10000 * test_constants::sui_multiplier(), scenario.ctx()), + &clock, + scenario.ctx(), + ); + + // Add first order with ID 1 + let condition = tpsl::new_condition(true, 1_500_000); + let pending_order = tpsl::new_pending_limit_order( + 1, + constants::no_restriction(), + constants::self_matching_allowed(), + 800_000, + 100 * test_constants::sui_multiplier(), + false, + false, + constants::max_u64(), + ); + + mm.add_conditional_order( + &pool, + &sui_price, + &usdc_price, + &margin_registry, + 1, + condition, + pending_order, + &clock, + scenario.ctx(), + ); + + // Try to add another order with same ID 1 + let condition2 = tpsl::new_condition(true, 1_000_000); + let pending_order2 = tpsl::new_pending_limit_order( + 2, + constants::no_restriction(), + constants::self_matching_allowed(), + 700_000_000_000, + 100 * test_constants::sui_multiplier(), + false, + false, + constants::max_u64(), + ); + + mm.add_conditional_order( + &pool, + &sui_price, + &usdc_price, + &margin_registry, + 1, // Duplicate ID + condition2, + pending_order2, + &clock, + scenario.ctx(), + ); + + destroy_2!(sui_price, usdc_price); + return_shared_2!(mm, pool); + + cleanup_margin_test(margin_registry, admin_cap, maintainer_cap, clock, scenario); +} + +#[test] +#[expected_failure(abort_code = deepbook_margin::tpsl::EInvalidOrderParams)] +fun test_error_invalid_order_params_quantity_too_small() { + // Test EInvalidOrderParams: quantity below min_size + + let ( + mut scenario, + clock, + admin_cap, + maintainer_cap, + _usdc_pool_id, + _sui_pool_id, + _pool_id, + registry_id, + ) = setup_sui_usdc_deepbook_margin(); + + scenario.next_tx(test_constants::user1()); + let mut margin_registry = scenario.take_shared(); + let pool = scenario.take_shared>(); + let deepbook_registry = scenario.take_shared_by_id(registry_id); + margin_manager::new( + &pool, + &deepbook_registry, + &mut margin_registry, + &clock, + scenario.ctx(), + ); + return_shared(deepbook_registry); + return_shared(pool); + + scenario.next_tx(test_constants::user1()); + let mut mm = scenario.take_shared>(); + let pool = scenario.take_shared>(); + + let sui_price = build_sui_price_info_object_with_price(&mut scenario, 200, &clock); + let usdc_price = build_usdc_price_info_object(&mut scenario, &clock); + + mm.deposit( + &margin_registry, + &sui_price, + &usdc_price, + mint_coin(10000 * test_constants::sui_multiplier(), scenario.ctx()), + &clock, + scenario.ctx(), + ); + + // Invalid: quantity = 0 (below min_size) + let condition = tpsl::new_condition(true, 1_500_000); + let pending_order = tpsl::new_pending_limit_order( + 1, + constants::no_restriction(), + constants::self_matching_allowed(), + 800_000, + 0, // Invalid quantity + false, + false, + constants::max_u64(), + ); + + mm.add_conditional_order( + &pool, + &sui_price, + &usdc_price, + &margin_registry, + 1, + condition, + pending_order, + &clock, + scenario.ctx(), + ); + + destroy_2!(sui_price, usdc_price); + return_shared_2!(mm, pool); + + cleanup_margin_test(margin_registry, admin_cap, maintainer_cap, clock, scenario); +} + +#[test] +#[expected_failure(abort_code = deepbook_margin::tpsl::EInvalidOrderParams)] +fun test_error_invalid_order_params_quantity_not_lot_size_multiple() { + // Test EInvalidOrderParams: quantity not a multiple of lot_size + + let ( + mut scenario, + clock, + admin_cap, + maintainer_cap, + _usdc_pool_id, + _sui_pool_id, + _pool_id, + registry_id, + ) = setup_sui_usdc_deepbook_margin(); + + scenario.next_tx(test_constants::user1()); + let mut margin_registry = scenario.take_shared(); + let pool = scenario.take_shared>(); + let deepbook_registry = scenario.take_shared_by_id(registry_id); + margin_manager::new( + &pool, + &deepbook_registry, + &mut margin_registry, + &clock, + scenario.ctx(), + ); + return_shared(deepbook_registry); + return_shared(pool); + + scenario.next_tx(test_constants::user1()); + let mut mm = scenario.take_shared>(); + let pool = scenario.take_shared>(); + + let sui_price = build_sui_price_info_object_with_price(&mut scenario, 200, &clock); + let usdc_price = build_usdc_price_info_object(&mut scenario, &clock); + + mm.deposit( + &margin_registry, + &sui_price, + &usdc_price, + mint_coin(10000 * test_constants::sui_multiplier(), scenario.ctx()), + &clock, + scenario.ctx(), + ); + + // Invalid: quantity = 1.5 * lot_size + 1 (not a multiple of lot_size) + // lot_size is typically 1 * base_multiplier (1 SUI = 1_000_000_000) + let condition = tpsl::new_condition(true, 1_500_000); + let pending_order = tpsl::new_pending_limit_order( + 1, + constants::no_restriction(), + constants::self_matching_allowed(), + 800_000, + test_constants::sui_multiplier() + 1, // 1 SUI + 1 nano (not a lot_size multiple) + false, + false, + constants::max_u64(), + ); + + mm.add_conditional_order( + &pool, + &sui_price, + &usdc_price, + &margin_registry, + 1, + condition, + pending_order, + &clock, + scenario.ctx(), + ); + + destroy_2!(sui_price, usdc_price); + return_shared_2!(mm, pool); + + cleanup_margin_test(margin_registry, admin_cap, maintainer_cap, clock, scenario); +} + +#[test] +#[expected_failure(abort_code = deepbook_margin::tpsl::EInvalidOrderParams)] +fun test_error_invalid_order_params_price_not_tick_size_multiple() { + // Test EInvalidOrderParams: price not a multiple of tick_size for limit orders + + let ( + mut scenario, + clock, + admin_cap, + maintainer_cap, + _usdc_pool_id, + _sui_pool_id, + _pool_id, + registry_id, + ) = setup_sui_usdc_deepbook_margin(); + + scenario.next_tx(test_constants::user1()); + let mut margin_registry = scenario.take_shared(); + let pool = scenario.take_shared>(); + let deepbook_registry = scenario.take_shared_by_id(registry_id); + margin_manager::new( + &pool, + &deepbook_registry, + &mut margin_registry, + &clock, + scenario.ctx(), + ); + return_shared(deepbook_registry); + return_shared(pool); + + scenario.next_tx(test_constants::user1()); + let mut mm = scenario.take_shared>(); + let pool = scenario.take_shared>(); + + let sui_price = build_sui_price_info_object_with_price(&mut scenario, 200, &clock); + let usdc_price = build_usdc_price_info_object(&mut scenario, &clock); + + mm.deposit( + &margin_registry, + &sui_price, + &usdc_price, + mint_coin(10000 * test_constants::sui_multiplier(), scenario.ctx()), + &clock, + scenario.ctx(), + ); + + // Invalid: price = 12345 (not a multiple of tick_size) + let condition = tpsl::new_condition(true, 1_500_000); + let pending_order = tpsl::new_pending_limit_order( + 1, + constants::no_restriction(), + constants::self_matching_allowed(), + 12345, // Invalid price (not tick_size multiple) + 100 * test_constants::sui_multiplier(), + false, + false, + constants::max_u64(), + ); + + mm.add_conditional_order( + &pool, + &sui_price, + &usdc_price, + &margin_registry, + 1, + condition, + pending_order, + &clock, + scenario.ctx(), + ); + + destroy_2!(sui_price, usdc_price); + return_shared_2!(mm, pool); + + cleanup_margin_test(margin_registry, admin_cap, maintainer_cap, clock, scenario); +} + +#[test] +#[expected_failure(abort_code = deepbook_margin::tpsl::EInvalidOrderParams)] +fun test_error_invalid_order_params_price_below_min() { + // Test EInvalidOrderParams: price < min_price for limit orders + + let ( + mut scenario, + clock, + admin_cap, + maintainer_cap, + _usdc_pool_id, + _sui_pool_id, + _pool_id, + registry_id, + ) = setup_sui_usdc_deepbook_margin(); + + scenario.next_tx(test_constants::user1()); + let mut margin_registry = scenario.take_shared(); + let pool = scenario.take_shared>(); + let deepbook_registry = scenario.take_shared_by_id(registry_id); + margin_manager::new( + &pool, + &deepbook_registry, + &mut margin_registry, + &clock, + scenario.ctx(), + ); + return_shared(deepbook_registry); + return_shared(pool); + + scenario.next_tx(test_constants::user1()); + let mut mm = scenario.take_shared>(); + let pool = scenario.take_shared>(); + + let sui_price = build_sui_price_info_object_with_price(&mut scenario, 200, &clock); + let usdc_price = build_usdc_price_info_object(&mut scenario, &clock); + + mm.deposit( + &margin_registry, + &sui_price, + &usdc_price, + mint_coin(10000 * test_constants::sui_multiplier(), scenario.ctx()), + &clock, + scenario.ctx(), + ); + + // Invalid: price = 0 (< min_price) + let condition = tpsl::new_condition(true, 1_500_000); + let pending_order = tpsl::new_pending_limit_order( + 1, + constants::no_restriction(), + constants::self_matching_allowed(), + 0, // Invalid: price = 0 + 100 * test_constants::sui_multiplier(), + false, + false, + constants::max_u64(), + ); + + mm.add_conditional_order( + &pool, + &sui_price, + &usdc_price, + &margin_registry, + 1, + condition, + pending_order, + &clock, + scenario.ctx(), + ); + + destroy_2!(sui_price, usdc_price); + return_shared_2!(mm, pool); + + cleanup_margin_test(margin_registry, admin_cap, maintainer_cap, clock, scenario); +} + +#[test] +#[expected_failure(abort_code = deepbook_margin::tpsl::EInvalidOrderParams)] +fun test_error_invalid_order_params_expired_timestamp() { + // Test EInvalidOrderParams: expire_timestamp in the past + + let ( + mut scenario, + clock, + admin_cap, + maintainer_cap, + _usdc_pool_id, + _sui_pool_id, + _pool_id, + registry_id, + ) = setup_sui_usdc_deepbook_margin(); + + scenario.next_tx(test_constants::user1()); + let mut margin_registry = scenario.take_shared(); + let pool = scenario.take_shared>(); + let deepbook_registry = scenario.take_shared_by_id(registry_id); + margin_manager::new( + &pool, + &deepbook_registry, + &mut margin_registry, + &clock, + scenario.ctx(), + ); + return_shared(deepbook_registry); + return_shared(pool); + + scenario.next_tx(test_constants::user1()); + let mut mm = scenario.take_shared>(); + let pool = scenario.take_shared>(); + + let sui_price = build_sui_price_info_object_with_price(&mut scenario, 200, &clock); + let usdc_price = build_usdc_price_info_object(&mut scenario, &clock); + + mm.deposit( + &margin_registry, + &sui_price, + &usdc_price, + mint_coin(10000 * test_constants::sui_multiplier(), scenario.ctx()), + &clock, + scenario.ctx(), + ); + + // Invalid: expire_timestamp = 100 (< current clock time which is 1000000) + let condition = tpsl::new_condition(true, 1_500_000); + let pending_order = tpsl::new_pending_limit_order( + 1, + constants::no_restriction(), + constants::self_matching_allowed(), + 800_000, + 100 * test_constants::sui_multiplier(), + false, + false, + 100, // Already expired + ); + + mm.add_conditional_order( + &pool, + &sui_price, + &usdc_price, + &margin_registry, + 1, + condition, + pending_order, + &clock, + scenario.ctx(), + ); + + destroy_2!(sui_price, usdc_price); + return_shared_2!(mm, pool); + + cleanup_margin_test(margin_registry, admin_cap, maintainer_cap, clock, scenario); +} + +#[test] +#[expected_failure(abort_code = deepbook_margin::tpsl::EInvalidOrderParams)] +fun test_error_invalid_order_params_market_order_quantity_too_small() { + // Test EInvalidOrderParams: market order quantity below min_size + + let ( + mut scenario, + clock, + admin_cap, + maintainer_cap, + _usdc_pool_id, + _sui_pool_id, + _pool_id, + registry_id, + ) = setup_sui_usdc_deepbook_margin(); + + scenario.next_tx(test_constants::user1()); + let mut margin_registry = scenario.take_shared(); + let pool = scenario.take_shared>(); + let deepbook_registry = scenario.take_shared_by_id(registry_id); + margin_manager::new( + &pool, + &deepbook_registry, + &mut margin_registry, + &clock, + scenario.ctx(), + ); + return_shared(deepbook_registry); + return_shared(pool); + + scenario.next_tx(test_constants::user1()); + let mut mm = scenario.take_shared>(); + let pool = scenario.take_shared>(); + + let sui_price = build_sui_price_info_object_with_price(&mut scenario, 200, &clock); + let usdc_price = build_usdc_price_info_object(&mut scenario, &clock); + + mm.deposit( + &margin_registry, + &sui_price, + &usdc_price, + mint_coin(10000 * test_constants::sui_multiplier(), scenario.ctx()), + &clock, + scenario.ctx(), + ); + + // Invalid: market order quantity = 0 (below min_size) + let condition = tpsl::new_condition(true, 1_500_000); + let pending_order = tpsl::new_pending_market_order( + 1, + constants::self_matching_allowed(), + 0, // Invalid quantity + false, + false, + ); + + mm.add_conditional_order( + &pool, + &sui_price, + &usdc_price, + &margin_registry, + 1, + condition, + pending_order, + &clock, + scenario.ctx(), + ); + + destroy_2!(sui_price, usdc_price); + return_shared_2!(mm, pool); + + cleanup_margin_test(margin_registry, admin_cap, maintainer_cap, clock, scenario); +} + +#[test] +fun test_tpsl_insufficient_funds_second_order() { + // Test insufficient funds scenario: + // - ALICE adds 2 trigger_below orders at different trigger prices + // - Both orders get triggered simultaneously + // - Only enough collateral to execute the first order (sorted high to low) + // - Second order fails due to insufficient funds and is removed + + let ( + mut scenario, + clock, + admin_cap, + maintainer_cap, + _usdc_pool_id, + _sui_pool_id, + _pool_id, + registry_id, + ) = setup_sui_usdc_deepbook_margin(); + + // USER1 = ALICE creates a margin manager + scenario.next_tx(test_constants::user1()); + let mut margin_registry = scenario.take_shared(); + let pool = scenario.take_shared>(); + let deepbook_registry = scenario.take_shared_by_id(registry_id); + margin_manager::new( + &pool, + &deepbook_registry, + &mut margin_registry, + &clock, + scenario.ctx(), + ); + return_shared(deepbook_registry); + return_shared(pool); + + scenario.next_tx(test_constants::user1()); + let mut mm = scenario.take_shared>(); + let pool = scenario.take_shared>(); + + // Initial prices: SUI = $2.00, USDC = $1.00 + let sui_price_high = build_sui_price_info_object_with_price(&mut scenario, 200, &clock); // $2.00 + let usdc_price = build_usdc_price_info_object(&mut scenario, &clock); // $1.00 + + // Deposit limited collateral: only 150 SUI + mm.deposit( + &margin_registry, + &sui_price_high, + &usdc_price, + mint_coin(150 * test_constants::sui_multiplier(), scenario.ctx()), + &clock, + scenario.ctx(), + ); + + // Add first order: trigger_below at $1.80, sell 100 SUI (this will succeed) + let condition1 = tpsl::new_condition( + true, // trigger_is_below + 1_800_000, // $1.80 + ); + let pending_order1 = tpsl::new_pending_limit_order( + 1, + constants::no_restriction(), + constants::self_matching_allowed(), + 800_000, + 100 * test_constants::sui_multiplier(), // 100 SUI + false, + false, + constants::max_u64(), + ); + + mm.add_conditional_order( + &pool, + &sui_price_high, + &usdc_price, + &margin_registry, + 1, + condition1, + pending_order1, + &clock, + scenario.ctx(), + ); + + // Add second order: trigger_below at $1.50, sell 100 SUI (this will fail due to insufficient funds) + let condition2 = tpsl::new_condition( + true, // trigger_is_below + 1_500_000, // $1.50 + ); + let pending_order2 = tpsl::new_pending_limit_order( + 2, + constants::no_restriction(), + constants::self_matching_allowed(), + 700_000_000_000, + 100 * test_constants::sui_multiplier(), // 100 SUI + false, + false, + constants::max_u64(), + ); + + mm.add_conditional_order( + &pool, + &sui_price_high, + &usdc_price, + &margin_registry, + 2, + condition2, + pending_order2, + &clock, + scenario.ctx(), + ); + + // Verify both orders were added + assert!(mm.conditional_order_ids().length() == 2); + + destroy_2!(sui_price_high, usdc_price); + return_shared(pool); + return_shared(margin_registry); + + // USER2 = BOB executes conditional orders when price drops below both triggers + scenario.next_tx(test_constants::user2()); + let sui_price_low = build_sui_price_info_object_with_price(&mut scenario, 95, &clock); // $0.95 (below both $1.80 and $1.50) + let usdc_price = build_usdc_price_info_object(&mut scenario, &clock); + + let mut pool = scenario.take_shared>(); + let margin_registry = scenario.take_shared(); + + // Execute conditional orders - both are triggered, but only first succeeds + let order_infos = mm.execute_conditional_orders( + &mut pool, + &sui_price_low, + &usdc_price, + &margin_registry, + 10, // max_orders_to_execute + &clock, + scenario.ctx(), + ); + + // Only the first order should have been executed successfully + assert!(order_infos.length() == 1); + + let order_info = &order_infos[0]; + assert!(order_info.client_order_id() == 1); // First order + assert!(order_info.original_quantity() == 100 * test_constants::sui_multiplier()); + + destroy(order_infos[0]); + + // Both conditional orders should be removed (first executed, second insufficient funds) + assert!(mm.conditional_order_ids().length() == 0); + + destroy_2!(sui_price_low, usdc_price); + return_shared_2!(mm, pool); + + let deepbook_registry = scenario.take_shared_by_id(registry_id); + return_shared(deepbook_registry); + cleanup_margin_test(margin_registry, admin_cap, maintainer_cap, clock, scenario); +} + +#[test] +fun test_tpsl_expired_order_during_execution() { + // Test expired order scenario: + // - ALICE adds a trigger_below order with expiration timestamp + // - Time passes and the order expires + // - Price triggers the condition + // - Order should be removed due to expiration, not executed + + let ( + mut scenario, + mut clock, + admin_cap, + maintainer_cap, + _usdc_pool_id, + _sui_pool_id, + _pool_id, + registry_id, + ) = setup_sui_usdc_deepbook_margin(); + + // USER1 = ALICE creates a margin manager + scenario.next_tx(test_constants::user1()); + let mut margin_registry = scenario.take_shared(); + let pool = scenario.take_shared>(); + let deepbook_registry = scenario.take_shared_by_id(registry_id); + margin_manager::new( + &pool, + &deepbook_registry, + &mut margin_registry, + &clock, + scenario.ctx(), + ); + return_shared(deepbook_registry); + return_shared(pool); + + scenario.next_tx(test_constants::user1()); + let mut mm = scenario.take_shared>(); + let pool = scenario.take_shared>(); + + // Initial prices: SUI = $2.00, USDC = $1.00 + let sui_price_high = build_sui_price_info_object_with_price(&mut scenario, 200, &clock); // $2.00 + let usdc_price = build_usdc_price_info_object(&mut scenario, &clock); // $1.00 + + // Deposit collateral + mm.deposit( + &margin_registry, + &sui_price_high, + &usdc_price, + mint_coin(10000 * test_constants::sui_multiplier(), scenario.ctx()), + &clock, + scenario.ctx(), + ); + + // Add order with short expiration (current time + 100ms) + let expire_timestamp = clock.timestamp_ms() + 100; + let condition = tpsl::new_condition( + true, // trigger_is_below + 1_500_000, // $1.50 + ); + let pending_order = tpsl::new_pending_limit_order( + 1, + constants::no_restriction(), + constants::self_matching_allowed(), + 800_000, + 100 * test_constants::sui_multiplier(), + false, + false, + expire_timestamp, + ); + + mm.add_conditional_order( + &pool, + &sui_price_high, + &usdc_price, + &margin_registry, + 1, + condition, + pending_order, + &clock, + scenario.ctx(), + ); + + // Verify order was added + assert!(mm.conditional_order_ids().length() == 1); + + destroy_2!(sui_price_high, usdc_price); + return_shared(pool); + return_shared(margin_registry); + + // Advance time past expiration (current time + 200ms) + clock.increment_for_testing(200); + + // USER2 = BOB tries to execute when price drops (after expiration) + scenario.next_tx(test_constants::user2()); + let sui_price_low = build_sui_price_info_object_with_price(&mut scenario, 95, &clock); // $0.95 + let usdc_price = build_usdc_price_info_object(&mut scenario, &clock); + + let mut pool = scenario.take_shared>(); + let margin_registry = scenario.take_shared(); + + // Execute conditional orders - order is triggered but expired + let order_infos = mm.execute_conditional_orders( + &mut pool, + &sui_price_low, + &usdc_price, + &margin_registry, + 10, + &clock, + scenario.ctx(), + ); + + // No orders should have been executed (order was expired) + assert!(order_infos.length() == 0); + + // Conditional order should be removed due to expiration + assert!(mm.conditional_order_ids().length() == 0); + + destroy_2!(sui_price_low, usdc_price); + return_shared_2!(mm, pool); + + let deepbook_registry = scenario.take_shared_by_id(registry_id); + return_shared(deepbook_registry); + cleanup_margin_test(margin_registry, admin_cap, maintainer_cap, clock, scenario); +} + +#[test] +fun test_tpsl_early_exit_optimization() { + // Test early exit optimization: + // - ALICE adds 5 trigger_below orders at different prices + // - Price only crosses 2 of them (highest 2) + // - Only 2 orders should execute, 3 should remain + // - Tests that the early exit optimization works (breaks when condition not met) + + let ( + mut scenario, + clock, + admin_cap, + maintainer_cap, + _usdc_pool_id, + _sui_pool_id, + _pool_id, + registry_id, + ) = setup_sui_usdc_deepbook_margin(); + + // USER1 = ALICE creates a margin manager + scenario.next_tx(test_constants::user1()); + let mut margin_registry = scenario.take_shared(); + let pool = scenario.take_shared>(); + let deepbook_registry = scenario.take_shared_by_id(registry_id); + margin_manager::new( + &pool, + &deepbook_registry, + &mut margin_registry, + &clock, + scenario.ctx(), + ); + return_shared(deepbook_registry); + return_shared(pool); + + scenario.next_tx(test_constants::user1()); + let mut mm = scenario.take_shared>(); + let pool = scenario.take_shared>(); + + // Initial prices: SUI = $2.00, USDC = $1.00 + let sui_price_high = build_sui_price_info_object_with_price(&mut scenario, 200, &clock); // $2.00 + let usdc_price = build_usdc_price_info_object(&mut scenario, &clock); + + // Deposit collateral + mm.deposit( + &margin_registry, + &sui_price_high, + &usdc_price, + mint_coin(10000 * test_constants::sui_multiplier(), scenario.ctx()), + &clock, + scenario.ctx(), + ); + + // Add 5 trigger_below orders at different prices (will be sorted high to low) + let trigger_prices = vector[ + 1_800_000, // $1.80 - ID 1 - Will trigger + 1_600_000, // $1.60 - ID 2 - Will trigger + 1_400_000, // $1.40 - ID 3 - Won't trigger (price = $1.50) + 1_200_000, // $1.20 - ID 4 - Won't trigger + 1_000_000, // $1.00 - ID 5 - Won't trigger + ]; + + let mut i = 0; + while (i < trigger_prices.length()) { + let condition = tpsl::new_condition(true, trigger_prices[i]); + let pending_order = tpsl::new_pending_limit_order( + i + 1, + constants::no_restriction(), + constants::self_matching_allowed(), + 800_000, + 10 * test_constants::sui_multiplier(), // Small amounts to ensure all can execute + false, + false, + constants::max_u64(), + ); + + mm.add_conditional_order( + &pool, + &sui_price_high, + &usdc_price, + &margin_registry, + i + 1, + condition, + pending_order, + &clock, + scenario.ctx(), + ); + i = i + 1; + }; + + // Verify all 5 orders were added + assert!(mm.conditional_order_ids().length() == 5); + + destroy_2!(sui_price_high, usdc_price); + return_shared(pool); + return_shared(margin_registry); + + // USER2 = BOB executes when price drops to $1.50 (only crosses $1.80 and $1.60) + scenario.next_tx(test_constants::user2()); + let sui_price_mid = build_sui_price_info_object_with_price(&mut scenario, 150, &clock); // $1.50 + let usdc_price = build_usdc_price_info_object(&mut scenario, &clock); + + let mut pool = scenario.take_shared>(); + let margin_registry = scenario.take_shared(); + + // Execute conditional orders + let order_infos = mm.execute_conditional_orders( + &mut pool, + &sui_price_mid, + &usdc_price, + &margin_registry, + 10, + &clock, + scenario.ctx(), + ); + + // Only 2 orders should have been executed (ID 1 and ID 2) + assert!(order_infos.length() == 2); + assert!(order_infos[0].client_order_id() == 1); // $1.80 + assert!(order_infos[1].client_order_id() == 2); // $1.60 + + destroy(order_infos[0]); + destroy(order_infos[1]); + + // 3 orders should remain (ID 3, 4, 5) + let remaining_ids = mm.conditional_order_ids(); + assert!(remaining_ids.length() == 3); + + // Verify remaining orders are the correct ones (sorted high to low) + let order_3 = mm.conditional_order(remaining_ids[0]); + let order_4 = mm.conditional_order(remaining_ids[1]); + let order_5 = mm.conditional_order(remaining_ids[2]); + + assert!(order_3.condition().trigger_price() == 1_400_000); // $1.40 + assert!(order_4.condition().trigger_price() == 1_200_000); // $1.20 + assert!(order_5.condition().trigger_price() == 1_000_000); // $1.00 + + destroy_2!(sui_price_mid, usdc_price); + return_shared_2!(mm, pool); + + let deepbook_registry = scenario.take_shared_by_id(registry_id); + return_shared(deepbook_registry); + cleanup_margin_test(margin_registry, admin_cap, maintainer_cap, clock, scenario); +} + +#[test] +fun test_tpsl_max_orders_to_execute_limit() { + // Test max_orders_to_execute limit with multiple execution calls: + // - ALICE adds 5 trigger_below orders + // - All 5 are triggered by price movement + // - First execution: max_orders_to_execute = 2 (executes 2, 3 remain) + // - Second execution: max_orders_to_execute = 2 (executes 2 more, 1 remains) + // - Tests batched execution across multiple calls + + let ( + mut scenario, + clock, + admin_cap, + maintainer_cap, + _usdc_pool_id, + _sui_pool_id, + _pool_id, + registry_id, + ) = setup_sui_usdc_deepbook_margin(); + + // USER1 = ALICE creates a margin manager + scenario.next_tx(test_constants::user1()); + let mut margin_registry = scenario.take_shared(); + let pool = scenario.take_shared>(); + let deepbook_registry = scenario.take_shared_by_id(registry_id); + margin_manager::new( + &pool, + &deepbook_registry, + &mut margin_registry, + &clock, + scenario.ctx(), + ); + return_shared(deepbook_registry); + return_shared(pool); + + scenario.next_tx(test_constants::user1()); + let mut mm = scenario.take_shared>(); + let pool = scenario.take_shared>(); + + // Initial prices: SUI = $2.00, USDC = $1.00 + let sui_price_high = build_sui_price_info_object_with_price(&mut scenario, 200, &clock); // $2.00 + let usdc_price = build_usdc_price_info_object(&mut scenario, &clock); + + // Deposit collateral + mm.deposit( + &margin_registry, + &sui_price_high, + &usdc_price, + mint_coin(10000 * test_constants::sui_multiplier(), scenario.ctx()), + &clock, + scenario.ctx(), + ); + + // Add 5 trigger_below orders at different prices (all will trigger when price drops to $0.50) + let trigger_prices = vector[ + 1_800_000, // $1.80 - ID 1 + 1_600_000, // $1.60 - ID 2 + 1_400_000, // $1.40 - ID 3 + 1_200_000, // $1.20 - ID 4 + 1_000_000, // $1.00 - ID 5 + ]; + + let mut i = 0; + while (i < trigger_prices.length()) { + let condition = tpsl::new_condition(true, trigger_prices[i]); + let pending_order = tpsl::new_pending_limit_order( + i + 1, + constants::no_restriction(), + constants::self_matching_allowed(), + 400_000_000_000, // $0.40 + 10 * test_constants::sui_multiplier(), + false, + false, + constants::max_u64(), + ); + + mm.add_conditional_order( + &pool, + &sui_price_high, + &usdc_price, + &margin_registry, + i + 1, + condition, + pending_order, + &clock, + scenario.ctx(), + ); + i = i + 1; + }; + + // Verify all 5 orders were added + assert!(mm.conditional_order_ids().length() == 5); + + destroy_2!(sui_price_high, usdc_price); + return_shared(pool); + return_shared(margin_registry); + + // USER2 = BOB executes when price drops to $0.50 (triggers all 5 orders) + // First execution call with max = 2 + scenario.next_tx(test_constants::user2()); + let sui_price_low = build_sui_price_info_object_with_price(&mut scenario, 50, &clock); // $0.50 + let usdc_price = build_usdc_price_info_object(&mut scenario, &clock); + + let mut pool = scenario.take_shared>(); + let margin_registry = scenario.take_shared(); + + // First execution: max_orders_to_execute = 2 + let order_infos_1 = mm.execute_conditional_orders( + &mut pool, + &sui_price_low, + &usdc_price, + &margin_registry, + 2, // Execute only 2 orders + &clock, + scenario.ctx(), + ); + + // First batch: 2 orders executed (ID 1, 2) + assert!(order_infos_1.length() == 2); + assert!(order_infos_1[0].client_order_id() == 1); + assert!(order_infos_1[1].client_order_id() == 2); + + destroy(order_infos_1[0]); + destroy(order_infos_1[1]); + + // 3 orders should remain (ID 3, 4, 5) + assert!(mm.conditional_order_ids().length() == 3); + + destroy_2!(sui_price_low, usdc_price); + return_shared(pool); + return_shared(margin_registry); + + // Second execution call with max = 2 + scenario.next_tx(test_constants::user2()); + let sui_price_low2 = build_sui_price_info_object_with_price(&mut scenario, 50, &clock); // $0.50 + let usdc_price2 = build_usdc_price_info_object(&mut scenario, &clock); + + let mut pool = scenario.take_shared>(); + let margin_registry = scenario.take_shared(); + + // Second execution: max_orders_to_execute = 2 + let order_infos_2 = mm.execute_conditional_orders( + &mut pool, + &sui_price_low2, + &usdc_price2, + &margin_registry, + 2, // Execute 2 more orders + &clock, + scenario.ctx(), + ); + + // Second batch: 2 more orders executed (ID 3, 4) + assert!(order_infos_2.length() == 2); + assert!(order_infos_2[0].client_order_id() == 3); + assert!(order_infos_2[1].client_order_id() == 4); + + destroy(order_infos_2[0]); + destroy(order_infos_2[1]); + + // Only 1 order should remain (ID 5) + let remaining_ids = mm.conditional_order_ids(); + assert!(remaining_ids.length() == 1); + + // Verify the remaining order + let order_5 = mm.conditional_order(remaining_ids[0]); + assert!(order_5.condition().trigger_price() == 1_000_000); // $1.00 + + destroy_2!(sui_price_low2, usdc_price2); + return_shared_2!(mm, pool); + + let deepbook_registry = scenario.take_shared_by_id(registry_id); + return_shared(deepbook_registry); + cleanup_margin_test(margin_registry, admin_cap, maintainer_cap, clock, scenario); +} diff --git a/packages/margin_liquidation/Move.lock b/packages/margin_liquidation/Move.lock new file mode 100644 index 000000000..2a4e73865 --- /dev/null +++ b/packages/margin_liquidation/Move.lock @@ -0,0 +1,125 @@ +# Generated by move; do not edit +# This file should be checked in. + +[move] +version = 4 + +[pinned.mainnet.MoveStdlib] +source = { git = "https://github.com/MystenLabs/sui.git", subdir = "crates/sui-framework/packages/move-stdlib", rev = "22f9fc9781732d651e18384c9a8eb1dabddf73a6" } +use_environment = "mainnet" +manifest_digest = "C4FE4C91DE74CBF223B2E380AE40F592177D21870DC2D7EB6227D2D694E05363" +deps = {} + +[pinned.mainnet.MoveStdlib_1] +source = { git = "https://github.com/MystenLabs/sui.git", subdir = "crates/sui-framework/packages/move-stdlib", rev = "041c5f2bae2fe52079e44b70514333532d69f4e6" } +use_environment = "mainnet" +manifest_digest = "C4FE4C91DE74CBF223B2E380AE40F592177D21870DC2D7EB6227D2D694E05363" +deps = {} + +[pinned.mainnet.Pyth] +source = { git = "https://github.com/pyth-network/pyth-crosschain.git", subdir = "target_chains/sui/contracts", rev = "3bd1262dcba9518a6901aa6a15f04072799bfb37" } +use_environment = "mainnet" +manifest_digest = "F2C5AF85C4B72C8F6A8132D05DE4F787F4EB80DBFF812331ED65AE599E7DF92A" +deps = { Sui = "Sui_1", Wormhole = "Wormhole" } + +[pinned.mainnet.Sui] +source = { git = "https://github.com/MystenLabs/sui.git", subdir = "crates/sui-framework/packages/sui-framework", rev = "22f9fc9781732d651e18384c9a8eb1dabddf73a6" } +use_environment = "mainnet" +manifest_digest = "CD547CB1ACCE0880C835DAED2D8FFCB91D56C833AE5240D3AA5B918398263195" +deps = { MoveStdlib = "MoveStdlib" } + +[pinned.mainnet.Sui_1] +source = { git = "https://github.com/MystenLabs/sui.git", subdir = "crates/sui-framework/packages/sui-framework", rev = "041c5f2bae2fe52079e44b70514333532d69f4e6" } +use_environment = "mainnet" +manifest_digest = "CD547CB1ACCE0880C835DAED2D8FFCB91D56C833AE5240D3AA5B918398263195" +deps = { MoveStdlib = "MoveStdlib_1" } + +[pinned.mainnet.Wormhole] +source = { git = "https://github.com/wormhole-foundation/wormhole.git", subdir = "sui/wormhole", rev = "b71be5cbb9537c4aac8e23e74371affa3825efcd" } +use_environment = "mainnet" +manifest_digest = "0D766A0380B75080707CFA6099AF469A2E502B0D9DC334A9E9983B391C5555D9" +deps = { Sui = "Sui_1" } + +[pinned.mainnet.deepbook] +source = { local = "../deepbook" } +use_environment = "mainnet" +manifest_digest = "F4948AC65D214ECC0561B7E94987B2AF2D7BF78658F6AE5CA5D0E1DA68873872" +deps = { std = "MoveStdlib", sui = "Sui", token = "token" } + +[pinned.mainnet.deepbook_margin] +source = { local = "../deepbook_margin" } +use_environment = "mainnet" +manifest_digest = "8EEB68B2BFCFE40D54C0A445414D1789D77E8CC373F7AF15A97B01ADE4B29199" +deps = { Pyth = "Pyth", deepbook = "deepbook", std = "MoveStdlib", sui = "Sui", token = "token" } + +[pinned.mainnet.margin_liquidation] +source = { root = true } +use_environment = "mainnet" +manifest_digest = "ABA31C522B44E535251E4A8910C48B4384FA8BCC3670A0F77E50CF53A9FA66E7" +deps = { deepbook_margin = "deepbook_margin", std = "MoveStdlib", sui = "Sui" } + +[pinned.mainnet.token] +source = { git = "https://github.com/MystenLabs/deepbookv3.git", subdir = "packages/token", rev = "87b5423bb0c08e5fcf9c4ed8cade1d2904a8dae9" } +use_environment = "mainnet" +manifest_digest = "E41BBD67BE8940D26C79D78B028477EF5B33BA217A1282C78ACB344CF8A5ECF6" +deps = { std = "MoveStdlib", sui = "Sui" } + +[pinned.testnet.MoveStdlib] +source = { git = "https://github.com/MystenLabs/sui.git", subdir = "crates/sui-framework/packages/move-stdlib", rev = "22f9fc9781732d651e18384c9a8eb1dabddf73a6" } +use_environment = "testnet" +manifest_digest = "C4FE4C91DE74CBF223B2E380AE40F592177D21870DC2D7EB6227D2D694E05363" +deps = {} + +[pinned.testnet.MoveStdlib_1] +source = { git = "https://github.com/MystenLabs/sui.git", subdir = "crates/sui-framework/packages/move-stdlib", rev = "041c5f2bae2fe52079e44b70514333532d69f4e6" } +use_environment = "testnet" +manifest_digest = "C4FE4C91DE74CBF223B2E380AE40F592177D21870DC2D7EB6227D2D694E05363" +deps = {} + +[pinned.testnet.Pyth] +source = { git = "https://github.com/pyth-network/pyth-crosschain.git", subdir = "target_chains/sui/contracts", rev = "62c7a5bc0fc857ba6417ad780190552d4919ceca" } +use_environment = "testnet" +manifest_digest = "EE442EEB0E1F71B244DBB1E016B1D0D8F67061BDAFA606B6C86F093D15CA0755" +deps = { Sui = "Sui_1", Wormhole = "Wormhole" } + +[pinned.testnet.Sui] +source = { git = "https://github.com/MystenLabs/sui.git", subdir = "crates/sui-framework/packages/sui-framework", rev = "22f9fc9781732d651e18384c9a8eb1dabddf73a6" } +use_environment = "testnet" +manifest_digest = "7AFB66695545775FBFBB2D3078ADFD084244D5002392E837FDE21D9EA1C6D01C" +deps = { MoveStdlib = "MoveStdlib" } + +[pinned.testnet.Sui_1] +source = { git = "https://github.com/MystenLabs/sui.git", subdir = "crates/sui-framework/packages/sui-framework", rev = "041c5f2bae2fe52079e44b70514333532d69f4e6" } +use_environment = "testnet" +manifest_digest = "7AFB66695545775FBFBB2D3078ADFD084244D5002392E837FDE21D9EA1C6D01C" +deps = { MoveStdlib = "MoveStdlib_1" } + +[pinned.testnet.Wormhole] +source = { git = "https://github.com/wormhole-foundation/wormhole.git", subdir = "sui/wormhole", rev = "1b1cb69e809e0e7081cf1bf9b2779c41c14fc7f0" } +use_environment = "testnet" +manifest_digest = "6996F1AB8CD448FFBA142C085C488FE8A46B485E61E38A1891EFA52B30D9C12E" +deps = { Sui = "Sui_1" } + +[pinned.testnet.deepbook] +source = { local = "../deepbook" } +use_environment = "testnet" +manifest_digest = "3101923B9428545A4F52FFAD1C4F959F9BFFF84CD09CE4BCC1CB831286999B5A" +deps = { std = "MoveStdlib", sui = "Sui", token = "token" } + +[pinned.testnet.deepbook_margin] +source = { local = "../deepbook_margin" } +use_environment = "testnet" +manifest_digest = "78CC81AD9EDE9868D30B4247C4C73CA21D5CDBE172BC92F17CB7B2476858B104" +deps = { Pyth = "Pyth", deepbook = "deepbook", std = "MoveStdlib", sui = "Sui", token = "token" } + +[pinned.testnet.margin_liquidation] +source = { root = true } +use_environment = "testnet" +manifest_digest = "FF5DB9B8C02374468CDF7FEE90335D220C8E7D19B2C17F168618A83681B94488" +deps = { deepbook_margin = "deepbook_margin", std = "MoveStdlib", sui = "Sui" } + +[pinned.testnet.token] +source = { git = "https://github.com/MystenLabs/deepbookv3.git", subdir = "packages/token", rev = "c2db0897f9741928b1be0a766d8bdbd8715e5652" } +use_environment = "testnet" +manifest_digest = "5745706258F61D6CE210904B3E6AE87A73CE9D31A6F93BE4718C442529332A87" +deps = { std = "MoveStdlib", sui = "Sui" } diff --git a/packages/margin_liquidation/Move.toml b/packages/margin_liquidation/Move.toml new file mode 100644 index 000000000..112b99023 --- /dev/null +++ b/packages/margin_liquidation/Move.toml @@ -0,0 +1,10 @@ +[package] +name = "margin_liquidation" +edition = "2024.alpha" +version = "0.0.1" + +[dependencies] +deepbook_margin = { local = "../deepbook_margin" } + +[addresses] +margin_liquidation = "0x0" diff --git a/packages/margin_liquidation/Published.toml b/packages/margin_liquidation/Published.toml new file mode 100644 index 000000000..3a136857d --- /dev/null +++ b/packages/margin_liquidation/Published.toml @@ -0,0 +1,18 @@ +# Generated by Move +# This file contains metadata about published versions of this package in different environments +# This file SHOULD be committed to source control + +[published.mainnet] +chain-id = "35834a8a" +published-at = "0x73c593882cdb557703e903603f20bd373261fe6ba6e1a40515f4b62f10553e6a" +original-id = "0x73c593882cdb557703e903603f20bd373261fe6ba6e1a40515f4b62f10553e6a" +version = 1 +toolchain-version = "1.63.1" +build-config = { flavor = "sui", edition = "2024" } +upgrade-capability = "0xd1b0608d02814ad1e15ddf6d8bf50bc4b8f375cd99d36f523dadc50c93fff565" + +[published.testnet] +chain-id = "4c78adac" +published-at = "0x8d69c3ef3ef580e5bf87b933ce28de19a5d0323588d1a44b9c60b4001741aa24" +original-id = "0x829f19f7460c1f2a553724526dd3400acaff308a9e60ab47410b448f11eb252a" +version = 3 diff --git a/packages/margin_liquidation/sources/liquidation_vault.move b/packages/margin_liquidation/sources/liquidation_vault.move new file mode 100644 index 000000000..379681db0 --- /dev/null +++ b/packages/margin_liquidation/sources/liquidation_vault.move @@ -0,0 +1,244 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +module margin_liquidation::liquidation_vault; + +use deepbook::pool::Pool; +use deepbook_margin::{ + margin_manager::{MarginManager, liquidate}, + margin_pool::MarginPool, + margin_registry::MarginRegistry +}; +use pyth::price_info::PriceInfoObject; +use sui::{bag::{Self, Bag}, balance::{Self, Balance}, clock::Clock, coin::Coin, event}; + +// === Errors === +const ENotEnoughBalanceInVault: u64 = 1; + +public struct LIQUIDATION_VAULT has drop {} + +// === Structs === +public struct LiquidationVault has key { + id: UID, + vault: Bag, +} + +public struct BalanceKey has copy, drop, store {} + +// === Caps === +public struct LiquidationAdminCap has key, store { + id: UID, +} + +// === Events === +public struct LiquidationByVault has copy, drop { + vault_id: ID, + margin_manager_id: ID, + margin_pool_id: ID, + base_in: u64, + base_out: u64, + quote_in: u64, + quote_out: u64, + repay_balance_remaining: u64, + base_liquidation: bool, +} + +fun init(_: LIQUIDATION_VAULT, ctx: &mut TxContext) { + let id = object::new(ctx); + let liquidation_admin_cap = LiquidationAdminCap { id }; + transfer::public_transfer(liquidation_admin_cap, ctx.sender()); +} + +// === Public Functions * ADMIN * === +public fun deposit( + self: &mut LiquidationVault, + _liquidation_cap: &LiquidationAdminCap, + coin: Coin, +) { + let balance = coin.into_balance(); + self.deposit_int(balance); +} + +public fun withdraw( + self: &mut LiquidationVault, + _liquidation_cap: &LiquidationAdminCap, + amount: u64, + ctx: &mut TxContext, +): Coin { + let balance = self.withdraw_int(amount); + + balance.into_coin(ctx) +} + +public fun create_liquidation_vault(_liquidation_cap: &LiquidationAdminCap, ctx: &mut TxContext) { + let id = object::new(ctx); + let liquidation_vault = LiquidationVault { + id, + vault: bag::new(ctx), + }; + transfer::share_object(liquidation_vault); +} + +// === Public Functions * LIQUIDATION * === +public fun liquidate_base( + self: &mut LiquidationVault, + margin_manager: &mut MarginManager, + registry: &MarginRegistry, + base_oracle: &PriceInfoObject, + quote_oracle: &PriceInfoObject, + base_margin_pool: &mut MarginPool, + quote_margin_pool: &mut MarginPool, + pool: &mut Pool, + repay_amount: Option, + clock: &Clock, + ctx: &mut TxContext, +) { + let risk_ratio = margin_manager.risk_ratio( + registry, + base_oracle, + quote_oracle, + pool, + base_margin_pool, + quote_margin_pool, + clock, + ); + let base_balance = self.balance(); + if (!registry.can_liquidate(pool.id(), risk_ratio) || base_balance < 1000) { + return + }; + let amount = repay_amount.destroy_with_default(base_balance); + let balance = self.withdraw_int(amount); + let (mut base_coin, quote_coin, base_repay_coin) = margin_manager.liquidate< + BaseAsset, + QuoteAsset, + BaseAsset, + >( + registry, + base_oracle, + quote_oracle, + base_margin_pool, + pool, + balance.into_coin(ctx), + clock, + ctx, + ); + let repay_balance_remaining = base_repay_coin.value(); + let base_out = base_coin.value(); + let quote_out = quote_coin.value(); + event::emit(LiquidationByVault { + vault_id: self.id(), + margin_manager_id: margin_manager.id(), + margin_pool_id: base_margin_pool.id(), + base_in: amount, + quote_in: 0, + base_out, + quote_out, + repay_balance_remaining, + base_liquidation: true, + }); + + base_coin.join(base_repay_coin); + self.deposit_int(base_coin.into_balance()); + self.deposit_int(quote_coin.into_balance()); +} + +public fun liquidate_quote( + self: &mut LiquidationVault, + margin_manager: &mut MarginManager, + registry: &MarginRegistry, + base_oracle: &PriceInfoObject, + quote_oracle: &PriceInfoObject, + base_margin_pool: &mut MarginPool, + quote_margin_pool: &mut MarginPool, + pool: &mut Pool, + repay_amount: Option, + clock: &Clock, + ctx: &mut TxContext, +) { + let risk_ratio = margin_manager.risk_ratio( + registry, + base_oracle, + quote_oracle, + pool, + base_margin_pool, + quote_margin_pool, + clock, + ); + let quote_balance = self.balance(); + if (!registry.can_liquidate(pool.id(), risk_ratio) || quote_balance < 1000) { + return + }; + let amount = repay_amount.destroy_with_default(quote_balance); + let balance = self.withdraw_int(amount); + let (base_coin, mut quote_coin, quote_repay_coin) = margin_manager.liquidate< + BaseAsset, + QuoteAsset, + QuoteAsset, + >( + registry, + base_oracle, + quote_oracle, + quote_margin_pool, + pool, + balance.into_coin(ctx), + clock, + ctx, + ); + let repay_balance_remaining = quote_repay_coin.value(); + let base_out = base_coin.value(); + let quote_out = quote_coin.value(); + event::emit(LiquidationByVault { + vault_id: self.id(), + margin_manager_id: margin_manager.id(), + margin_pool_id: quote_margin_pool.id(), + base_in: 0, + quote_in: amount, + base_out, + quote_out, + repay_balance_remaining, + base_liquidation: false, + }); + + quote_coin.join(quote_repay_coin); + self.deposit_int(base_coin.into_balance()); + self.deposit_int(quote_coin.into_balance()); +} + +public fun balance(self: &LiquidationVault): u64 { + let key = BalanceKey {}; + + if (self.vault.contains(key)) { + let balance: &Balance = &self.vault[key]; + + balance.value() + } else { + 0 + } +} + +// === Private Functions === +fun deposit_int(self: &mut LiquidationVault, balance: Balance) { + let key = BalanceKey {}; + + if (self.vault.contains(key)) { + let vault: &mut Balance = &mut self.vault[key]; + vault.join(balance); + } else { + self.vault.add(key, balance); + } +} + +fun withdraw_int(self: &mut LiquidationVault, amount: u64): Balance { + let key = BalanceKey {}; + if (!self.vault.contains(key)) { + self.vault.add(key, balance::zero()); + }; + let balance: &mut Balance = &mut self.vault[key]; + assert!(balance.value() >= amount, ENotEnoughBalanceInVault); + + balance.split(amount) +} + +fun id(self: &LiquidationVault): ID { + self.id.to_inner() +} diff --git a/packages/margin_liquidation/tests/price_info_ext.move b/packages/margin_liquidation/tests/price_info_ext.move new file mode 100644 index 000000000..9a009c872 --- /dev/null +++ b/packages/margin_liquidation/tests/price_info_ext.move @@ -0,0 +1,15 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +#[test_only] +extend module pyth::price_info; + +public fun new_price_info_object_for_test( + price_info: PriceInfo, + ctx: &mut TxContext, +): PriceInfoObject { + PriceInfoObject { + id: object::new(ctx), + price_info, + } +} diff --git a/packages/token/Move.lock b/packages/token/Move.lock index 23b7bd78d..3d2be6c3e 100644 --- a/packages/token/Move.lock +++ b/packages/token/Move.lock @@ -1,27 +1,49 @@ # @generated by Move, please check-in and do not edit manually. [move] -version = 2 -manifest_digest = "FE608FA43B822AA81BBB797A11F98A1E10BB2519DDB34E6E0523917B70393EC2" -deps_digest = "F8BBB0CCB2491CA29A3DF03D6F92277A4F3574266507ACD77214D37ECA3F3082" +version = 3 +manifest_digest = "1F7B2B58D94BB850740DA09C84E9080D0AB3A4F9A52E24D64F8247B34733257C" +deps_digest = "F9B494B64F0615AED0E98FC12A85B85ECD2BC5185C22D30E7F67786BB52E507C" dependencies = [ - { name = "Sui" }, + { id = "Bridge", name = "Bridge" }, + { id = "MoveStdlib", name = "MoveStdlib" }, + { id = "Sui", name = "Sui" }, + { id = "SuiSystem", name = "SuiSystem" }, ] [[move.package]] -name = "MoveStdlib" -source = { git = "https://github.com/MystenLabs/sui.git", rev = "framework/mainnet", subdir = "crates/sui-framework/packages/move-stdlib" } +id = "Bridge" +source = { git = "https://github.com/MystenLabs/sui.git", rev = "f63c9fc78e2171fa174dc43e757ded416c204558", subdir = "crates/sui-framework/packages/bridge" } + +dependencies = [ + { id = "MoveStdlib", name = "MoveStdlib" }, + { id = "Sui", name = "Sui" }, + { id = "SuiSystem", name = "SuiSystem" }, +] + +[[move.package]] +id = "MoveStdlib" +source = { git = "https://github.com/MystenLabs/sui.git", rev = "f63c9fc78e2171fa174dc43e757ded416c204558", subdir = "crates/sui-framework/packages/move-stdlib" } + +[[move.package]] +id = "Sui" +source = { git = "https://github.com/MystenLabs/sui.git", rev = "f63c9fc78e2171fa174dc43e757ded416c204558", subdir = "crates/sui-framework/packages/sui-framework" } + +dependencies = [ + { id = "MoveStdlib", name = "MoveStdlib" }, +] [[move.package]] -name = "Sui" -source = { git = "https://github.com/MystenLabs/sui.git", rev = "framework/mainnet", subdir = "crates/sui-framework/packages/sui-framework" } +id = "SuiSystem" +source = { git = "https://github.com/MystenLabs/sui.git", rev = "f63c9fc78e2171fa174dc43e757ded416c204558", subdir = "crates/sui-framework/packages/sui-system" } dependencies = [ - { name = "MoveStdlib" }, + { id = "MoveStdlib", name = "MoveStdlib" }, + { id = "Sui", name = "Sui" }, ] [move.toolchain-version] -compiler-version = "1.34.0" +compiler-version = "1.57.2" edition = "2024.beta" flavor = "sui" diff --git a/packages/token/Move.toml b/packages/token/Move.toml index b52ff74ac..cfcee287d 100644 --- a/packages/token/Move.toml +++ b/packages/token/Move.toml @@ -3,7 +3,6 @@ name = "token" edition = "2024.beta" [dependencies] -Sui = { git = "https://github.com/MystenLabs/sui.git", subdir = "crates/sui-framework/packages/sui-framework", rev = "framework/mainnet" } [addresses] token = "0x0" diff --git a/packages/token/sources/deep.move b/packages/token/sources/deep.move index c36c50d3a..9d257c250 100644 --- a/packages/token/sources/deep.move +++ b/packages/token/sources/deep.move @@ -1,80 +1,80 @@ // Copyright (c) Mysten Labs, Inc. // SPDX-License-Identifier: Apache-2.0 -module token::deep { - public struct DEEP has drop {} +module token::deep; - public struct ProtectedTreasury has key { - id: UID, - } +public struct DEEP has drop {} - public struct TreasuryCapKey has copy, drop, store {} +public struct ProtectedTreasury has key { + id: UID, +} + +public struct TreasuryCapKey has copy, drop, store {} - public fun burn(arg0: &mut ProtectedTreasury, arg1: sui::coin::Coin) { - sui::coin::burn(borrow_cap_mut(arg0), arg1); - } +public fun burn(arg0: &mut ProtectedTreasury, arg1: sui::coin::Coin) { + sui::coin::burn(borrow_cap_mut(arg0), arg1); +} - public fun total_supply(arg0: &ProtectedTreasury): u64 { - sui::coin::total_supply(borrow_cap(arg0)) - } +public fun total_supply(arg0: &ProtectedTreasury): u64 { + sui::coin::total_supply(borrow_cap(arg0)) +} - fun borrow_cap(arg0: &ProtectedTreasury): &sui::coin::TreasuryCap { - let v0 = TreasuryCapKey {}; - sui::dynamic_object_field::borrow>( - &arg0.id, - v0, - ) - } +fun borrow_cap(arg0: &ProtectedTreasury): &sui::coin::TreasuryCap { + let v0 = TreasuryCapKey {}; + sui::dynamic_object_field::borrow>( + &arg0.id, + v0, + ) +} - fun borrow_cap_mut(arg0: &mut ProtectedTreasury): &mut sui::coin::TreasuryCap { - let v0 = TreasuryCapKey {}; - sui::dynamic_object_field::borrow_mut>( - &mut arg0.id, - v0, - ) - } +fun borrow_cap_mut(arg0: &mut ProtectedTreasury): &mut sui::coin::TreasuryCap { + let v0 = TreasuryCapKey {}; + sui::dynamic_object_field::borrow_mut>( + &mut arg0.id, + v0, + ) +} - fun create_coin( - arg0: DEEP, - arg1: u64, - arg2: &mut sui::tx_context::TxContext, - ): (ProtectedTreasury, sui::coin::Coin) { - let (v0, v1) = sui::coin::create_currency( - arg0, - 6, - b"DEEP", - b"DeepBook Token", - b"The DEEP token secures the DeepBook protocol, the premier wholesale liquidity venue for on-chain trading.", - std::option::some< - sui::url::Url, - >(sui::url::new_unsafe_from_bytes(b"https://images.deepbook.tech/icon.svg")), - arg2, - ); - let mut cap = v0; - sui::transfer::public_freeze_object>(v1); - let mut protected_treasury = ProtectedTreasury { id: sui::object::new(arg2) }; +fun create_coin( + arg0: DEEP, + arg1: u64, + arg2: &mut sui::tx_context::TxContext, +): (ProtectedTreasury, sui::coin::Coin) { + let (v0, v1) = sui::coin::create_currency( + arg0, + 6, + b"DEEP", + b"DeepBook Token", + b"The DEEP token secures the DeepBook protocol, the premier wholesale liquidity venue for on-chain trading.", + std::option::some( + sui::url::new_unsafe_from_bytes(b"https://images.deepbook.tech/icon.svg"), + ), + arg2, + ); + let mut cap = v0; + sui::transfer::public_freeze_object>(v1); + let mut protected_treasury = ProtectedTreasury { id: sui::object::new(arg2) }; - let coin = sui::coin::mint(&mut cap, arg1, arg2); - sui::dynamic_object_field::add>( - &mut protected_treasury.id, - TreasuryCapKey {}, - cap, - ); + let coin = sui::coin::mint(&mut cap, arg1, arg2); + sui::dynamic_object_field::add>( + &mut protected_treasury.id, + TreasuryCapKey {}, + cap, + ); - (protected_treasury, coin) - } + (protected_treasury, coin) +} - #[allow(lint(share_owned))] - fun init(arg0: DEEP, arg1: &mut TxContext) { - let (v0, v1) = create_coin(arg0, 10000000000000000, arg1); - sui::transfer::share_object(v0); - sui::transfer::public_transfer>(v1, sui::tx_context::sender(arg1)); - } +#[allow(lint(share_owned))] +fun init(arg0: DEEP, arg1: &mut TxContext) { + let (v0, v1) = create_coin(arg0, 10000000000000000, arg1); + sui::transfer::share_object(v0); + sui::transfer::public_transfer>(v1, sui::tx_context::sender(arg1)); +} - #[test_only] - public fun share_treasury_for_testing(ctx: &mut sui::tx_context::TxContext) { - let (v0, v1) = create_coin(DEEP {}, 10000000000000000, ctx); - sui::transfer::share_object(v0); - v1.burn_for_testing(); - } +#[test_only] +public fun share_treasury_for_testing(ctx: &mut sui::tx_context::TxContext) { + let (v0, v1) = create_coin(DEEP {}, 10000000000000000, ctx); + sui::transfer::share_object(v0); + v1.burn_for_testing(); } diff --git a/scripts/config/constants.ts b/scripts/config/constants.ts index 6c1ee1c19..9c675cf2e 100644 --- a/scripts/config/constants.ts +++ b/scripts/config/constants.ts @@ -2,21 +2,61 @@ // SPDX-License-Identifier: Apache-2.0 export const adminCapOwner = { - "mainnet": "0xd0ec0b201de6b4e7f425918bbd7151c37fc1b06c59b3961a2a00db74f6ea865e", - "testnet": "0xb3d277c50f7b846a5f609a8d13428ae482b5826bb98437997373f3a0d60d280e", + mainnet: "0xd0ec0b201de6b4e7f425918bbd7151c37fc1b06c59b3961a2a00db74f6ea865e", + testnet: "0xb3d277c50f7b846a5f609a8d13428ae482b5826bb98437997373f3a0d60d280e", }; export const upgradeCapOwner = { - "mainnet": "0x37f187e1e54e9c9b8c78b6c46a7281f644ebc62e75493623edcaa6d1dfcf64d2", - "testnet": "0xb3d277c50f7b846a5f609a8d13428ae482b5826bb98437997373f3a0d60d280e", + mainnet: "0x37f187e1e54e9c9b8c78b6c46a7281f644ebc62e75493623edcaa6d1dfcf64d2", + testnet: "0xb3d277c50f7b846a5f609a8d13428ae482b5826bb98437997373f3a0d60d280e", }; export const upgradeCapID = { - "mainnet": "0xdadf253cea3b91010e64651b03da6d56166a4f44b43bdd4e185c277658634483", - "testnet": "0x56632f4f1b707b5b0c1b99defe65a056af3f846d95700b4a1cbd3adc94bf7c96", + mainnet: "0xdadf253cea3b91010e64651b03da6d56166a4f44b43bdd4e185c277658634483", + testnet: "0x479467ad71ba0b7f93b38b26cb121fbd181ae2db8c91585d3572db4aaa764ffb", }; export const adminCapID = { - "mainnet": "0xada554b8b712556b8509be47ac1bc04db9505c3532049a543721aca0c010a840", - "testnet": "0x014e6a65f60936177820141ad64430290b6ad5e16421691dc9f3fa9907154b2e" + mainnet: "0xada554b8b712556b8509be47ac1bc04db9505c3532049a543721aca0c010a840", + testnet: "0x29a62a5385c549dd8e9565312265d2bda0b8700c1560b3e34941671325daae77", +}; + +export const marginAdminCapID = { + mainnet: "0x3ec65d06f0be30905cc1742b903aa497791c702820331db263176b74e74c95c8", + testnet: "0x42a2e769541d272e624c54fff72b878fb0be670776c2b34ef07be5308480650e", +}; + +export const marginMaintainerCapID = { + mainnet: "0xf44fb36ebfe03ff7696f8c17723bbc6af3db1e5eff7944aa65d092575851ca72", + testnet: "", +}; + +export const suiMarginPoolCapID = { + mainnet: "0x4894832150466e190359716e415f92d6260d4e86c5e29f919fff8b5afa6682cb", + testnet: "", +}; + +export const usdcMarginPoolCapID = { + mainnet: "0x3c6278f0b21ebf51cec6485e312123c1dbad6d89fca9bb7cfe027dad32c275d8", + testnet: "", +}; + +export const deepMarginPoolCapID = { + mainnet: "0xa9532986275e3eac41b8c59da91c29d591b017422b96071276e833f7c9ed855f", + testnet: "", +}; + +export const walMarginPoolCapID = { + mainnet: "0x8aa9345ea5b61e095e5de9dc5a498cf8c8cec9469d7916552b8772d313b41dc8", + testnet: "", +}; + +export const liquidationAdminCapID = { + mainnet: "0x21521b9ddc1cfc76b6f4c9462957b4d58a998a23eb100ab2821d27d55c60d0a9", + testnet: "", +}; + +export const supplierCapID = { + mainnet: "0xe0e64f2b0037304e29647fd5ef2c5ea758828a8e5aea73bb0bd5f227c1c20204", + testnet: "", }; diff --git a/scripts/package.json b/scripts/package.json index a9b9f0972..57d005065 100644 --- a/scripts/package.json +++ b/scripts/package.json @@ -1,21 +1,21 @@ { - "name": "scripts", - "version": "1.0.0", - "description": "", - "main": "index.js", - "scripts": { - "test": "echo \"Error: no test specified\" && exit 1", - "create-pool": "pnpm tsx transactions/createPool.ts" - }, - "keywords": [], - "author": "", - "license": "ISC", - "dependencies": { - "@mysten/deepbook-v3": "^0.14.0", - "@mysten/sui": "^1.27.1", - "dotenv": "^16.5.0", - "esbuild": "^0.20.2", - "ts-node": "^10.9.2", - "tsx": "^4.19.3" - } + "name": "scripts", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1", + "create-pool": "pnpm tsx transactions/createPool.ts" + }, + "keywords": [], + "author": "", + "license": "ISC", + "dependencies": { + "@mysten/deepbook-v3": "^0.27.0", + "@mysten/sui": "^1.45.2", + "dotenv": "^16.6.1", + "esbuild": "^0.20.2", + "ts-node": "^10.9.2", + "tsx": "^4.21.0" + } } diff --git a/scripts/pnpm-lock.yaml b/scripts/pnpm-lock.yaml index 7b794a77c..640f5a944 100644 --- a/scripts/pnpm-lock.yaml +++ b/scripts/pnpm-lock.yaml @@ -9,36 +9,36 @@ importers: .: dependencies: '@mysten/deepbook-v3': - specifier: ^0.14.0 - version: 0.14.0(typescript@5.6.2) + specifier: ^0.27.0 + version: 0.27.0(typescript@5.9.3) '@mysten/sui': - specifier: ^1.27.1 - version: 1.27.1(typescript@5.6.2) + specifier: ^1.45.2 + version: 1.45.2(typescript@5.9.3) dotenv: - specifier: ^16.5.0 - version: 16.5.0 + specifier: ^16.6.1 + version: 16.6.1 esbuild: specifier: ^0.20.2 version: 0.20.2 ts-node: specifier: ^10.9.2 - version: 10.9.2(@types/node@22.7.4)(typescript@5.6.2) + version: 10.9.2(@types/node@24.10.1)(typescript@5.9.3) tsx: - specifier: ^4.19.3 - version: 4.19.3 + specifier: ^4.21.0 + version: 4.21.0 packages: - '@0no-co/graphql.web@1.1.2': - resolution: {integrity: sha512-N2NGsU5FLBhT8NZ+3l2YrzZSHITjNXNuDhC4iDiikv0IujaJ0Xc6xIxQZ/Ek3Cb+rgPjnLHYyJm11tInuJn+cw==} + '@0no-co/graphql.web@1.2.0': + resolution: {integrity: sha512-/1iHy9TTr63gE1YcR5idjx8UREz1s0kFhydf3bBLCXyqjhkIc6igAzTOx3zPifCwFR87tsh/4Pa9cNts6d2otw==} peerDependencies: graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 peerDependenciesMeta: graphql: optional: true - '@0no-co/graphqlsp@1.12.16': - resolution: {integrity: sha512-B5pyYVH93Etv7xjT6IfB7QtMBdaaC07yjbhN6v8H7KgFStMkPvi+oWYBTibMFRMY89qwc9H8YixXg8SXDVgYWw==} + '@0no-co/graphqlsp@1.15.2': + resolution: {integrity: sha512-Ys031WnS3sTQQBtRTkQsYnw372OlW72ais4sp0oh2UMPRNyxxnq85zRfU4PIdoy9kWriysPT5BYAkgIxhbonFA==} peerDependencies: graphql: ^15.5.0 || ^16.0.0 || ^17.0.0 typescript: ^5.0.0 @@ -53,8 +53,8 @@ packages: cpu: [ppc64] os: [aix] - '@esbuild/aix-ppc64@0.25.2': - resolution: {integrity: sha512-wCIboOL2yXZym2cgm6mlA742s9QeJ8DjGVaL39dLN4rRwrOgOyYSnOaFPhKZGLb2ngj4EyfAFjsNJwPXZvseag==} + '@esbuild/aix-ppc64@0.27.1': + resolution: {integrity: sha512-HHB50pdsBX6k47S4u5g/CaLjqS3qwaOVE5ILsq64jyzgMhLuCuZ8rGzM9yhsAjfjkbgUPMzZEPa7DAp7yz6vuA==} engines: {node: '>=18'} cpu: [ppc64] os: [aix] @@ -65,8 +65,8 @@ packages: cpu: [arm64] os: [android] - '@esbuild/android-arm64@0.25.2': - resolution: {integrity: sha512-5ZAX5xOmTligeBaeNEPnPaeEuah53Id2tX4c2CVP3JaROTH+j4fnfHCkr1PjXMd78hMst+TlkfKcW/DlTq0i4w==} + '@esbuild/android-arm64@0.27.1': + resolution: {integrity: sha512-45fuKmAJpxnQWixOGCrS+ro4Uvb4Re9+UTieUY2f8AEc+t7d4AaZ6eUJ3Hva7dtrxAAWHtlEFsXFMAgNnGU9uQ==} engines: {node: '>=18'} cpu: [arm64] os: [android] @@ -77,8 +77,8 @@ packages: cpu: [arm] os: [android] - '@esbuild/android-arm@0.25.2': - resolution: {integrity: sha512-NQhH7jFstVY5x8CKbcfa166GoV0EFkaPkCKBQkdPJFvo5u+nGXLEH/ooniLb3QI8Fk58YAx7nsPLozUWfCBOJA==} + '@esbuild/android-arm@0.27.1': + resolution: {integrity: sha512-kFqa6/UcaTbGm/NncN9kzVOODjhZW8e+FRdSeypWe6j33gzclHtwlANs26JrupOntlcWmB0u8+8HZo8s7thHvg==} engines: {node: '>=18'} cpu: [arm] os: [android] @@ -89,8 +89,8 @@ packages: cpu: [x64] os: [android] - '@esbuild/android-x64@0.25.2': - resolution: {integrity: sha512-Ffcx+nnma8Sge4jzddPHCZVRvIfQ0kMsUsCMcJRHkGJ1cDmhe4SsrYIjLUKn1xpHZybmOqCWwB0zQvsjdEHtkg==} + '@esbuild/android-x64@0.27.1': + resolution: {integrity: sha512-LBEpOz0BsgMEeHgenf5aqmn/lLNTFXVfoWMUox8CtWWYK9X4jmQzWjoGoNb8lmAYml/tQ/Ysvm8q7szu7BoxRQ==} engines: {node: '>=18'} cpu: [x64] os: [android] @@ -101,8 +101,8 @@ packages: cpu: [arm64] os: [darwin] - '@esbuild/darwin-arm64@0.25.2': - resolution: {integrity: sha512-MpM6LUVTXAzOvN4KbjzU/q5smzryuoNjlriAIx+06RpecwCkL9JpenNzpKd2YMzLJFOdPqBpuub6eVRP5IgiSA==} + '@esbuild/darwin-arm64@0.27.1': + resolution: {integrity: sha512-veg7fL8eMSCVKL7IW4pxb54QERtedFDfY/ASrumK/SbFsXnRazxY4YykN/THYqFnFwJ0aVjiUrVG2PwcdAEqQQ==} engines: {node: '>=18'} cpu: [arm64] os: [darwin] @@ -113,8 +113,8 @@ packages: cpu: [x64] os: [darwin] - '@esbuild/darwin-x64@0.25.2': - resolution: {integrity: sha512-5eRPrTX7wFyuWe8FqEFPG2cU0+butQQVNcT4sVipqjLYQjjh8a8+vUTfgBKM88ObB85ahsnTwF7PSIt6PG+QkA==} + '@esbuild/darwin-x64@0.27.1': + resolution: {integrity: sha512-+3ELd+nTzhfWb07Vol7EZ+5PTbJ/u74nC6iv4/lwIU99Ip5uuY6QoIf0Hn4m2HoV0qcnRivN3KSqc+FyCHjoVQ==} engines: {node: '>=18'} cpu: [x64] os: [darwin] @@ -125,8 +125,8 @@ packages: cpu: [arm64] os: [freebsd] - '@esbuild/freebsd-arm64@0.25.2': - resolution: {integrity: sha512-mLwm4vXKiQ2UTSX4+ImyiPdiHjiZhIaE9QvC7sw0tZ6HoNMjYAqQpGyui5VRIi5sGd+uWq940gdCbY3VLvsO1w==} + '@esbuild/freebsd-arm64@0.27.1': + resolution: {integrity: sha512-/8Rfgns4XD9XOSXlzUDepG8PX+AVWHliYlUkFI3K3GB6tqbdjYqdhcb4BKRd7C0BhZSoaCxhv8kTcBrcZWP+xg==} engines: {node: '>=18'} cpu: [arm64] os: [freebsd] @@ -137,8 +137,8 @@ packages: cpu: [x64] os: [freebsd] - '@esbuild/freebsd-x64@0.25.2': - resolution: {integrity: sha512-6qyyn6TjayJSwGpm8J9QYYGQcRgc90nmfdUb0O7pp1s4lTY+9D0H9O02v5JqGApUyiHOtkz6+1hZNvNtEhbwRQ==} + '@esbuild/freebsd-x64@0.27.1': + resolution: {integrity: sha512-GITpD8dK9C+r+5yRT/UKVT36h/DQLOHdwGVwwoHidlnA168oD3uxA878XloXebK4Ul3gDBBIvEdL7go9gCUFzQ==} engines: {node: '>=18'} cpu: [x64] os: [freebsd] @@ -149,8 +149,8 @@ packages: cpu: [arm64] os: [linux] - '@esbuild/linux-arm64@0.25.2': - resolution: {integrity: sha512-gq/sjLsOyMT19I8obBISvhoYiZIAaGF8JpeXu1u8yPv8BE5HlWYobmlsfijFIZ9hIVGYkbdFhEqC0NvM4kNO0g==} + '@esbuild/linux-arm64@0.27.1': + resolution: {integrity: sha512-W9//kCrh/6in9rWIBdKaMtuTTzNj6jSeG/haWBADqLLa9P8O5YSRDzgD5y9QBok4AYlzS6ARHifAb75V6G670Q==} engines: {node: '>=18'} cpu: [arm64] os: [linux] @@ -161,8 +161,8 @@ packages: cpu: [arm] os: [linux] - '@esbuild/linux-arm@0.25.2': - resolution: {integrity: sha512-UHBRgJcmjJv5oeQF8EpTRZs/1knq6loLxTsjc3nxO9eXAPDLcWW55flrMVc97qFPbmZP31ta1AZVUKQzKTzb0g==} + '@esbuild/linux-arm@0.27.1': + resolution: {integrity: sha512-ieMID0JRZY/ZeCrsFQ3Y3NlHNCqIhTprJfDgSB3/lv5jJZ8FX3hqPyXWhe+gvS5ARMBJ242PM+VNz/ctNj//eA==} engines: {node: '>=18'} cpu: [arm] os: [linux] @@ -173,8 +173,8 @@ packages: cpu: [ia32] os: [linux] - '@esbuild/linux-ia32@0.25.2': - resolution: {integrity: sha512-bBYCv9obgW2cBP+2ZWfjYTU+f5cxRoGGQ5SeDbYdFCAZpYWrfjjfYwvUpP8MlKbP0nwZ5gyOU/0aUzZ5HWPuvQ==} + '@esbuild/linux-ia32@0.27.1': + resolution: {integrity: sha512-VIUV4z8GD8rtSVMfAj1aXFahsi/+tcoXXNYmXgzISL+KB381vbSTNdeZHHHIYqFyXcoEhu9n5cT+05tRv13rlw==} engines: {node: '>=18'} cpu: [ia32] os: [linux] @@ -185,8 +185,8 @@ packages: cpu: [loong64] os: [linux] - '@esbuild/linux-loong64@0.25.2': - resolution: {integrity: sha512-SHNGiKtvnU2dBlM5D8CXRFdd+6etgZ9dXfaPCeJtz+37PIUlixvlIhI23L5khKXs3DIzAn9V8v+qb1TRKrgT5w==} + '@esbuild/linux-loong64@0.27.1': + resolution: {integrity: sha512-l4rfiiJRN7sTNI//ff65zJ9z8U+k6zcCg0LALU5iEWzY+a1mVZ8iWC1k5EsNKThZ7XCQ6YWtsZ8EWYm7r1UEsg==} engines: {node: '>=18'} cpu: [loong64] os: [linux] @@ -197,8 +197,8 @@ packages: cpu: [mips64el] os: [linux] - '@esbuild/linux-mips64el@0.25.2': - resolution: {integrity: sha512-hDDRlzE6rPeoj+5fsADqdUZl1OzqDYow4TB4Y/3PlKBD0ph1e6uPHzIQcv2Z65u2K0kpeByIyAjCmjn1hJgG0Q==} + '@esbuild/linux-mips64el@0.27.1': + resolution: {integrity: sha512-U0bEuAOLvO/DWFdygTHWY8C067FXz+UbzKgxYhXC0fDieFa0kDIra1FAhsAARRJbvEyso8aAqvPdNxzWuStBnA==} engines: {node: '>=18'} cpu: [mips64el] os: [linux] @@ -209,8 +209,8 @@ packages: cpu: [ppc64] os: [linux] - '@esbuild/linux-ppc64@0.25.2': - resolution: {integrity: sha512-tsHu2RRSWzipmUi9UBDEzc0nLc4HtpZEI5Ba+Omms5456x5WaNuiG3u7xh5AO6sipnJ9r4cRWQB2tUjPyIkc6g==} + '@esbuild/linux-ppc64@0.27.1': + resolution: {integrity: sha512-NzdQ/Xwu6vPSf/GkdmRNsOfIeSGnh7muundsWItmBsVpMoNPVpM61qNzAVY3pZ1glzzAxLR40UyYM23eaDDbYQ==} engines: {node: '>=18'} cpu: [ppc64] os: [linux] @@ -221,8 +221,8 @@ packages: cpu: [riscv64] os: [linux] - '@esbuild/linux-riscv64@0.25.2': - resolution: {integrity: sha512-k4LtpgV7NJQOml/10uPU0s4SAXGnowi5qBSjaLWMojNCUICNu7TshqHLAEbkBdAszL5TabfvQ48kK84hyFzjnw==} + '@esbuild/linux-riscv64@0.27.1': + resolution: {integrity: sha512-7zlw8p3IApcsN7mFw0O1Z1PyEk6PlKMu18roImfl3iQHTnr/yAfYv6s4hXPidbDoI2Q0pW+5xeoM4eTCC0UdrQ==} engines: {node: '>=18'} cpu: [riscv64] os: [linux] @@ -233,8 +233,8 @@ packages: cpu: [s390x] os: [linux] - '@esbuild/linux-s390x@0.25.2': - resolution: {integrity: sha512-GRa4IshOdvKY7M/rDpRR3gkiTNp34M0eLTaC1a08gNrh4u488aPhuZOCpkF6+2wl3zAN7L7XIpOFBhnaE3/Q8Q==} + '@esbuild/linux-s390x@0.27.1': + resolution: {integrity: sha512-cGj5wli+G+nkVQdZo3+7FDKC25Uh4ZVwOAK6A06Hsvgr8WqBBuOy/1s+PUEd/6Je+vjfm6stX0kmib5b/O2Ykw==} engines: {node: '>=18'} cpu: [s390x] os: [linux] @@ -245,14 +245,14 @@ packages: cpu: [x64] os: [linux] - '@esbuild/linux-x64@0.25.2': - resolution: {integrity: sha512-QInHERlqpTTZ4FRB0fROQWXcYRD64lAoiegezDunLpalZMjcUcld3YzZmVJ2H/Cp0wJRZ8Xtjtj0cEHhYc/uUg==} + '@esbuild/linux-x64@0.27.1': + resolution: {integrity: sha512-z3H/HYI9MM0HTv3hQZ81f+AKb+yEoCRlUby1F80vbQ5XdzEMyY/9iNlAmhqiBKw4MJXwfgsh7ERGEOhrM1niMA==} engines: {node: '>=18'} cpu: [x64] os: [linux] - '@esbuild/netbsd-arm64@0.25.2': - resolution: {integrity: sha512-talAIBoY5M8vHc6EeI2WW9d/CkiO9MQJ0IOWX8hrLhxGbro/vBXJvaQXefW2cP0z0nQVTdQ/eNyGFV1GSKrxfw==} + '@esbuild/netbsd-arm64@0.27.1': + resolution: {integrity: sha512-wzC24DxAvk8Em01YmVXyjl96Mr+ecTPyOuADAvjGg+fyBpGmxmcr2E5ttf7Im8D0sXZihpxzO1isus8MdjMCXQ==} engines: {node: '>=18'} cpu: [arm64] os: [netbsd] @@ -263,14 +263,14 @@ packages: cpu: [x64] os: [netbsd] - '@esbuild/netbsd-x64@0.25.2': - resolution: {integrity: sha512-voZT9Z+tpOxrvfKFyfDYPc4DO4rk06qamv1a/fkuzHpiVBMOhpjK+vBmWM8J1eiB3OLSMFYNaOaBNLXGChf5tg==} + '@esbuild/netbsd-x64@0.27.1': + resolution: {integrity: sha512-1YQ8ybGi2yIXswu6eNzJsrYIGFpnlzEWRl6iR5gMgmsrR0FcNoV1m9k9sc3PuP5rUBLshOZylc9nqSgymI+TYg==} engines: {node: '>=18'} cpu: [x64] os: [netbsd] - '@esbuild/openbsd-arm64@0.25.2': - resolution: {integrity: sha512-dcXYOC6NXOqcykeDlwId9kB6OkPUxOEqU+rkrYVqJbK2hagWOMrsTGsMr8+rW02M+d5Op5NNlgMmjzecaRf7Tg==} + '@esbuild/openbsd-arm64@0.27.1': + resolution: {integrity: sha512-5Z+DzLCrq5wmU7RDaMDe2DVXMRm2tTDvX2KU14JJVBN2CT/qov7XVix85QoJqHltpvAOZUAc3ndU56HSMWrv8g==} engines: {node: '>=18'} cpu: [arm64] os: [openbsd] @@ -281,20 +281,26 @@ packages: cpu: [x64] os: [openbsd] - '@esbuild/openbsd-x64@0.25.2': - resolution: {integrity: sha512-t/TkWwahkH0Tsgoq1Ju7QfgGhArkGLkF1uYz8nQS/PPFlXbP5YgRpqQR3ARRiC2iXoLTWFxc6DJMSK10dVXluw==} + '@esbuild/openbsd-x64@0.27.1': + resolution: {integrity: sha512-Q73ENzIdPF5jap4wqLtsfh8YbYSZ8Q0wnxplOlZUOyZy7B4ZKW8DXGWgTCZmF8VWD7Tciwv5F4NsRf6vYlZtqg==} engines: {node: '>=18'} cpu: [x64] os: [openbsd] + '@esbuild/openharmony-arm64@0.27.1': + resolution: {integrity: sha512-ajbHrGM/XiK+sXM0JzEbJAen+0E+JMQZ2l4RR4VFwvV9JEERx+oxtgkpoKv1SevhjavK2z2ReHk32pjzktWbGg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + '@esbuild/sunos-x64@0.20.2': resolution: {integrity: sha512-2UyFtRC6cXLyejf/YEld4Hajo7UHILetzE1vsRcGL3earZEW77JxrFjH4Ez2qaTiEfMgAXxfAZCm1fvM/G/o8w==} engines: {node: '>=12'} cpu: [x64] os: [sunos] - '@esbuild/sunos-x64@0.25.2': - resolution: {integrity: sha512-cfZH1co2+imVdWCjd+D1gf9NjkchVhhdpgb1q5y6Hcv9TP6Zi9ZG/beI3ig8TvwT9lH9dlxLq5MQBBgwuj4xvA==} + '@esbuild/sunos-x64@0.27.1': + resolution: {integrity: sha512-IPUW+y4VIjuDVn+OMzHc5FV4GubIwPnsz6ubkvN8cuhEqH81NovB53IUlrlBkPMEPxvNnf79MGBoz8rZ2iW8HA==} engines: {node: '>=18'} cpu: [x64] os: [sunos] @@ -305,8 +311,8 @@ packages: cpu: [arm64] os: [win32] - '@esbuild/win32-arm64@0.25.2': - resolution: {integrity: sha512-7Loyjh+D/Nx/sOTzV8vfbB3GJuHdOQyrOryFdZvPHLf42Tk9ivBU5Aedi7iyX+x6rbn2Mh68T4qq1SDqJBQO5Q==} + '@esbuild/win32-arm64@0.27.1': + resolution: {integrity: sha512-RIVRWiljWA6CdVu8zkWcRmGP7iRRIIwvhDKem8UMBjPql2TXM5PkDVvvrzMtj1V+WFPB4K7zkIGM7VzRtFkjdg==} engines: {node: '>=18'} cpu: [arm64] os: [win32] @@ -317,8 +323,8 @@ packages: cpu: [ia32] os: [win32] - '@esbuild/win32-ia32@0.25.2': - resolution: {integrity: sha512-WRJgsz9un0nqZJ4MfhabxaD9Ft8KioqU3JMinOTvobbX6MOSUigSBlogP8QB3uxpJDsFS6yN+3FDBdqE5lg9kg==} + '@esbuild/win32-ia32@0.27.1': + resolution: {integrity: sha512-2BR5M8CPbptC1AK5JbJT1fWrHLvejwZidKx3UMSF0ecHMa+smhi16drIrCEggkgviBwLYd5nwrFLSl5Kho96RQ==} engines: {node: '>=18'} cpu: [ia32] os: [win32] @@ -329,14 +335,14 @@ packages: cpu: [x64] os: [win32] - '@esbuild/win32-x64@0.25.2': - resolution: {integrity: sha512-kM3HKb16VIXZyIeVrM1ygYmZBKybX8N4p754bw390wGO3Tf2j4L2/WYL+4suWujpgf6GBYs3jv7TyUivdd05JA==} + '@esbuild/win32-x64@0.27.1': + resolution: {integrity: sha512-d5X6RMYv6taIymSk8JBP+nxv8DQAMY6A51GPgusqLdK9wBz5wWIXy1KjTck6HnjE9hqJzJRdk+1p/t5soSbCtw==} engines: {node: '>=18'} cpu: [x64] os: [win32] - '@gql.tada/cli-utils@1.6.3': - resolution: {integrity: sha512-jFFSY8OxYeBxdKi58UzeMXG1tdm4FVjXa8WHIi66Gzu9JWtCE6mqom3a8xkmSw+mVaybFW5EN2WXf1WztJVNyQ==} + '@gql.tada/cli-utils@1.7.2': + resolution: {integrity: sha512-Qbc7hbLvCz6IliIJpJuKJa9p05b2Jona7ov7+qofCsMRxHRZE1kpAmZMvL8JCI4c0IagpIlWNaMizXEQUe8XjQ==} peerDependencies: '@0no-co/graphqlsp': ^1.12.13 '@gql.tada/svelte-support': 1.0.1 @@ -364,42 +370,54 @@ packages: resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} engines: {node: '>=6.0.0'} - '@jridgewell/sourcemap-codec@1.5.0': - resolution: {integrity: sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==} + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} '@jridgewell/trace-mapping@0.3.9': resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==} - '@mysten/bcs@1.6.0': - resolution: {integrity: sha512-ydDRYdIkIFCpHCcPvAkMC91fVwumjzbTgjqds0KsphDQI3jUlH3jFG5lfYNTmV6V3pkhOiRk1fupLBcsQsiszg==} + '@mysten/bcs@1.9.2': + resolution: {integrity: sha512-kBk5xrxV9OWR7i+JhL/plQrgQ2/KJhB2pB5gj+w6GXhbMQwS3DPpOvi/zN0Tj84jwPvHMllpEl0QHj6ywN7/eQ==} - '@mysten/deepbook-v3@0.14.0': - resolution: {integrity: sha512-A8DlBlkTHonHG2sjhFgY4NL88KB0bUL53zEI35oEBAJ+HuDKdt7eDyV9fKql8hxRVSEbyRlHCZUOn4U5rNw8eg==} + '@mysten/deepbook-v3@0.27.0': + resolution: {integrity: sha512-+hsjiAU7xsyhqtPzL17v1AMIbADe4luCmXHd3BY5pzKjGBnIkSh+xFK4/sKxNt4gJ6rcO8Mx4UQlP0mudjpCaQ==} engines: {node: '>=18'} - '@mysten/sui@1.27.1': - resolution: {integrity: sha512-ByckbAvDFhPTDT42QwbQHXbGOvCkS/GyJ4ns7OwKZ4r9bAH71SsOKPnchfLdJSDRp6N1N1FORQjq4VMrFfJcqw==} + '@mysten/sui@1.45.2': + resolution: {integrity: sha512-gftf7fNpFSiXyfXpbtP2afVEnhc7p2m/MEYc/SO5pov92dacGKOpQIF7etZsGDI1Wvhv+dpph+ulRNpnYSs7Bg==} engines: {node: '>=18'} - '@noble/curves@1.8.2': - resolution: {integrity: sha512-vnI7V6lFNe0tLAuJMu+2sX+FcL14TaCWy1qiczg1VwRmPrpQCdq5ESXQMqUc2tluRNf6irBXrWbl1mGN8uaU/g==} + '@mysten/utils@0.2.0': + resolution: {integrity: sha512-CM6kJcJHX365cK6aXfFRLBiuyXc5WSBHQ43t94jqlCAIRw8umgNcTb5EnEA9n31wPAQgLDGgbG/rCUISCTJ66w==} + + '@noble/curves@1.9.4': + resolution: {integrity: sha512-2bKONnuM53lINoDrSmK8qP8W271ms7pygDhZt4SiLOoLwBtoHqeCFi6RG42V8zd3mLHuJFhU/Bmaqo4nX0/kBw==} engines: {node: ^14.21.3 || >=16} - '@noble/hashes@1.7.2': - resolution: {integrity: sha512-biZ0NUSxyjLLqo6KxEJ1b+C2NAx0wtDoFvCaXHGgUkeHzf3Xc1xKumFKREuT7f7DARNZ/slvYUwFG6B0f2b6hQ==} + '@noble/hashes@1.8.0': + resolution: {integrity: sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==} engines: {node: ^14.21.3 || >=16} - '@scure/base@1.2.4': - resolution: {integrity: sha512-5Yy9czTO47mqz+/J8GM6GIId4umdCk1wc1q8rKERQulIoc8VP9pzDcghv10Tl2E7R96ZUx/PhND3ESYUQX8NuQ==} + '@protobuf-ts/grpcweb-transport@2.11.1': + resolution: {integrity: sha512-1W4utDdvOB+RHMFQ0soL4JdnxjXV+ddeGIUg08DvZrA8Ms6k5NN6GBFU2oHZdTOcJVpPrDJ02RJlqtaoCMNBtw==} + + '@protobuf-ts/runtime-rpc@2.11.1': + resolution: {integrity: sha512-4CqqUmNA+/uMz00+d3CYKgElXO9VrEbucjnBFEjqI4GuDrEQ32MaI3q+9qPBvIGOlL4PmHXrzM32vBPWRhQKWQ==} + + '@protobuf-ts/runtime@2.11.1': + resolution: {integrity: sha512-KuDaT1IfHkugM2pyz+FwiY80ejWrkH1pAtOBOZFuR6SXEFTsnb/jiQWQ1rCIrcKx2BtyxnxW6BWwsVSA/Ie+WQ==} - '@scure/bip32@1.6.2': - resolution: {integrity: sha512-t96EPDMbtGgtb7onKKqxRLfE5g05k7uHnHRM2xdE6BP/ZmxaLtPek4J4KfVn/90IQNrU1IOAqMgiDtUdtbe3nw==} + '@scure/base@1.2.6': + resolution: {integrity: sha512-g/nm5FgUa//MCj1gV09zTJTaM6KBAHqLN907YVQqf7zC49+DcO4B1so4ZX07Ef10Twr6nuqYEH9GEggFXA4Fmg==} - '@scure/bip39@1.5.4': - resolution: {integrity: sha512-TFM4ni0vKvCfBpohoh+/lY05i9gRbSwXWngAsF4CABQxoaOHijxuaZ2R6cStDQ5CHtHO9aGJTr4ksVJASRRyMA==} + '@scure/bip32@1.7.0': + resolution: {integrity: sha512-E4FFX/N3f4B80AKWp5dP6ow+flD1LQZo/w8UnLGYZO674jS6YnYeepycOOksv+vLPSpgN35wgKgy+ybfTb2SMw==} - '@tsconfig/node10@1.0.11': - resolution: {integrity: sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==} + '@scure/bip39@1.6.0': + resolution: {integrity: sha512-+lF0BbLiJNwVlev4eKelw1WWLaiKXw7sSl8T6FvBlWkdX+94aGJ4o8XjUdlyhTCjd8c+B3KT3JfS8P0bLRNU6A==} + + '@tsconfig/node10@1.0.12': + resolution: {integrity: sha512-UCYBaeFvM11aU2y3YPZ//O5Rhj+xKyzy7mvcIoAjASbigy8mHMryP5cK7dgjlz2hWxh1g5pLw084E0a/wlUSFQ==} '@tsconfig/node12@1.0.11': resolution: {integrity: sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==} @@ -410,66 +428,168 @@ packages: '@tsconfig/node16@1.0.4': resolution: {integrity: sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==} - '@types/node@22.7.4': - resolution: {integrity: sha512-y+NPi1rFzDs1NdQHHToqeiX2TIS79SWEAw9GYhkkx8bD0ChpfqC+n2j5OXOCpzfojBEBt6DnEnnG9MY0zk1XLg==} + '@types/node@24.10.1': + resolution: {integrity: sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ==} acorn-walk@8.3.4: resolution: {integrity: sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==} engines: {node: '>=0.4.0'} - acorn@8.14.1: - resolution: {integrity: sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==} + acorn@8.15.0: + resolution: {integrity: sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==} engines: {node: '>=0.4.0'} hasBin: true arg@4.1.3: resolution: {integrity: sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==} + asynckit@0.4.0: + resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} + + axios-retry@4.5.0: + resolution: {integrity: sha512-aR99oXhpEDGo0UuAlYcn2iGRds30k366Zfa05XWScR9QaQD4JYiP3/1Qt1u7YlefUOK+cn0CcwoL1oefavQUlQ==} + peerDependencies: + axios: 0.x || 1.x + + axios@1.13.2: + resolution: {integrity: sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==} + + call-bind-apply-helpers@1.0.2: + resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} + engines: {node: '>= 0.4'} + + combined-stream@1.0.8: + resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} + engines: {node: '>= 0.8'} + create-require@1.1.1: resolution: {integrity: sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==} + delayed-stream@1.0.0: + resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} + engines: {node: '>=0.4.0'} + diff@4.0.2: resolution: {integrity: sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==} engines: {node: '>=0.3.1'} - dotenv@16.5.0: - resolution: {integrity: sha512-m/C+AwOAr9/W1UOIZUo232ejMNnJAJtYQjUbHoNTBNTJSvqzzDh7vnrei3o3r3m9blf6ZoDkvcw0VmozNRFJxg==} + dotenv@16.6.1: + resolution: {integrity: sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==} engines: {node: '>=12'} + dunder-proto@1.0.1: + resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} + engines: {node: '>= 0.4'} + + es-define-property@1.0.1: + resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} + engines: {node: '>= 0.4'} + + es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + + es-object-atoms@1.1.1: + resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} + engines: {node: '>= 0.4'} + + es-set-tostringtag@2.1.0: + resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} + engines: {node: '>= 0.4'} + esbuild@0.20.2: resolution: {integrity: sha512-WdOOppmUNU+IbZ0PaDiTst80zjnrOkyJNHoKupIcVyU8Lvla3Ugx94VzkQ32Ijqd7UhHJy75gNWDMUekcrSJ6g==} engines: {node: '>=12'} hasBin: true - esbuild@0.25.2: - resolution: {integrity: sha512-16854zccKPnC+toMywC+uKNeYSv+/eXkevRAfwRD/G9Cleq66m8XFIrigkbvauLLlCfDL45Q2cWegSg53gGBnQ==} + esbuild@0.27.1: + resolution: {integrity: sha512-yY35KZckJJuVVPXpvjgxiCuVEJT67F6zDeVTv4rizyPrfGBUpZQsvmxnN+C371c2esD/hNMjj4tpBhuueLN7aA==} engines: {node: '>=18'} hasBin: true + follow-redirects@1.15.11: + resolution: {integrity: sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==} + engines: {node: '>=4.0'} + peerDependencies: + debug: '*' + peerDependenciesMeta: + debug: + optional: true + + form-data@4.0.5: + resolution: {integrity: sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==} + engines: {node: '>= 6'} + fsevents@2.3.3: resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} os: [darwin] - get-tsconfig@4.10.0: - resolution: {integrity: sha512-kGzZ3LWWQcGIAmg6iWvXn0ei6WDtV26wzHRMwDSzmAbcXrTEXxHy6IehI6/4eT6VRKyMP1eF1VqwrVUmE/LR7A==} + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + + get-intrinsic@1.3.0: + resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} + engines: {node: '>= 0.4'} + + get-proto@1.0.1: + resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} + engines: {node: '>= 0.4'} + + get-tsconfig@4.13.0: + resolution: {integrity: sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ==} - gql.tada@1.8.10: - resolution: {integrity: sha512-FrvSxgz838FYVPgZHGOSgbpOjhR+yq44rCzww3oOPJYi0OvBJjAgCiP6LEokZIYND2fUTXzQAyLgcvgw1yNP5A==} + gopd@1.2.0: + resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} + engines: {node: '>= 0.4'} + + gql.tada@1.9.0: + resolution: {integrity: sha512-1LMiA46dRs5oF7Qev6vMU32gmiNvM3+3nHoQZA9K9j2xQzH8xOAWnnJrLSbZOFHTSdFxqn86TL6beo1/7ja/aA==} hasBin: true peerDependencies: typescript: ^5.0.0 - graphql@16.10.0: - resolution: {integrity: sha512-AjqGKbDGUFRKIRCP9tCKiIGHyriz2oHEbPIbEtcSLSs4YjReZOIPQQWek4+6hjw62H9QShXHyaGivGiYVLeYFQ==} + graphql@16.12.0: + resolution: {integrity: sha512-DKKrynuQRne0PNpEbzuEdHlYOMksHSUI8Zc9Unei5gTsMNA2/vMpoMz/yKba50pejK56qj98qM0SjYxAKi13gQ==} engines: {node: ^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0} + has-symbols@1.1.0: + resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} + engines: {node: '>= 0.4'} + + has-tostringtag@1.0.2: + resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} + engines: {node: '>= 0.4'} + + hasown@2.0.2: + resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} + engines: {node: '>= 0.4'} + + is-retry-allowed@2.2.0: + resolution: {integrity: sha512-XVm7LOeLpTW4jV19QSH38vkswxoLud8sQ57YwJVTPWdiaI9I8keEhGFpBlslyVsgdQy4Opg8QOLb8YRgsyZiQg==} + engines: {node: '>=10'} + make-error@1.3.6: resolution: {integrity: sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==} + math-intrinsics@1.1.0: + resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} + engines: {node: '>= 0.4'} + + mime-db@1.52.0: + resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} + engines: {node: '>= 0.6'} + + mime-types@2.1.35: + resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} + engines: {node: '>= 0.6'} + poseidon-lite@0.2.1: resolution: {integrity: sha512-xIr+G6HeYfOhCuswdqcFpSX47SPhm0EpisWJ6h7fHlWwaVIvH3dLnejpatrtw6Xc6HaLrpq05y7VRfvDmDGIog==} + proxy-from-env@1.1.0: + resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} + resolve-pkg-maps@1.0.0: resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} @@ -487,24 +607,29 @@ packages: '@swc/wasm': optional: true - tsx@4.19.3: - resolution: {integrity: sha512-4H8vUNGNjQ4V2EOoGw005+c+dGuPSnhpPBPHBtsZdGZBk/iJb4kguGlPWaZTZ3q5nMtFOEsY0nRDlh9PJyd6SQ==} + tsx@4.21.0: + resolution: {integrity: sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==} engines: {node: '>=18.0.0'} hasBin: true - typescript@5.6.2: - resolution: {integrity: sha512-NW8ByodCSNCwZeghjN3o+JX5OFH0Ojg6sadjEKY4huZ52TqbJTJnDo5+Tw98lSy63NZvi4n+ez5m2u5d4PkZyw==} + typescript@5.9.3: + resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} engines: {node: '>=14.17'} hasBin: true - undici-types@6.19.8: - resolution: {integrity: sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==} + undici-types@7.16.0: + resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==} v8-compile-cache-lib@3.0.1: resolution: {integrity: sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==} - valibot@0.36.0: - resolution: {integrity: sha512-CjF1XN4sUce8sBK9TixrDqFM7RwNkuXdJu174/AwmQUB62QbCQADg5lLe8ldBalFgtj1uKj+pKwDJiNo4Mn+eQ==} + valibot@1.2.0: + resolution: {integrity: sha512-mm1rxUsmOxzrwnX5arGS+U4T25RdvpPjPN4yR0u9pUBov9+zGVtO84tif1eY4r6zWxVxu3KzIyknJy3rxfRZZg==} + peerDependencies: + typescript: '>=5' + peerDependenciesMeta: + typescript: + optional: true yn@3.1.1: resolution: {integrity: sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==} @@ -512,15 +637,15 @@ packages: snapshots: - '@0no-co/graphql.web@1.1.2(graphql@16.10.0)': + '@0no-co/graphql.web@1.2.0(graphql@16.12.0)': optionalDependencies: - graphql: 16.10.0 + graphql: 16.12.0 - '@0no-co/graphqlsp@1.12.16(graphql@16.10.0)(typescript@5.6.2)': + '@0no-co/graphqlsp@1.15.2(graphql@16.12.0)(typescript@5.9.3)': dependencies: - '@gql.tada/internal': 1.0.8(graphql@16.10.0)(typescript@5.6.2) - graphql: 16.10.0 - typescript: 5.6.2 + '@gql.tada/internal': 1.0.8(graphql@16.12.0)(typescript@5.9.3) + graphql: 16.12.0 + typescript: 5.9.3 '@cspotcode/source-map-support@0.8.1': dependencies: @@ -529,223 +654,251 @@ snapshots: '@esbuild/aix-ppc64@0.20.2': optional: true - '@esbuild/aix-ppc64@0.25.2': + '@esbuild/aix-ppc64@0.27.1': optional: true '@esbuild/android-arm64@0.20.2': optional: true - '@esbuild/android-arm64@0.25.2': + '@esbuild/android-arm64@0.27.1': optional: true '@esbuild/android-arm@0.20.2': optional: true - '@esbuild/android-arm@0.25.2': + '@esbuild/android-arm@0.27.1': optional: true '@esbuild/android-x64@0.20.2': optional: true - '@esbuild/android-x64@0.25.2': + '@esbuild/android-x64@0.27.1': optional: true '@esbuild/darwin-arm64@0.20.2': optional: true - '@esbuild/darwin-arm64@0.25.2': + '@esbuild/darwin-arm64@0.27.1': optional: true '@esbuild/darwin-x64@0.20.2': optional: true - '@esbuild/darwin-x64@0.25.2': + '@esbuild/darwin-x64@0.27.1': optional: true '@esbuild/freebsd-arm64@0.20.2': optional: true - '@esbuild/freebsd-arm64@0.25.2': + '@esbuild/freebsd-arm64@0.27.1': optional: true '@esbuild/freebsd-x64@0.20.2': optional: true - '@esbuild/freebsd-x64@0.25.2': + '@esbuild/freebsd-x64@0.27.1': optional: true '@esbuild/linux-arm64@0.20.2': optional: true - '@esbuild/linux-arm64@0.25.2': + '@esbuild/linux-arm64@0.27.1': optional: true '@esbuild/linux-arm@0.20.2': optional: true - '@esbuild/linux-arm@0.25.2': + '@esbuild/linux-arm@0.27.1': optional: true '@esbuild/linux-ia32@0.20.2': optional: true - '@esbuild/linux-ia32@0.25.2': + '@esbuild/linux-ia32@0.27.1': optional: true '@esbuild/linux-loong64@0.20.2': optional: true - '@esbuild/linux-loong64@0.25.2': + '@esbuild/linux-loong64@0.27.1': optional: true '@esbuild/linux-mips64el@0.20.2': optional: true - '@esbuild/linux-mips64el@0.25.2': + '@esbuild/linux-mips64el@0.27.1': optional: true '@esbuild/linux-ppc64@0.20.2': optional: true - '@esbuild/linux-ppc64@0.25.2': + '@esbuild/linux-ppc64@0.27.1': optional: true '@esbuild/linux-riscv64@0.20.2': optional: true - '@esbuild/linux-riscv64@0.25.2': + '@esbuild/linux-riscv64@0.27.1': optional: true '@esbuild/linux-s390x@0.20.2': optional: true - '@esbuild/linux-s390x@0.25.2': + '@esbuild/linux-s390x@0.27.1': optional: true '@esbuild/linux-x64@0.20.2': optional: true - '@esbuild/linux-x64@0.25.2': + '@esbuild/linux-x64@0.27.1': optional: true - '@esbuild/netbsd-arm64@0.25.2': + '@esbuild/netbsd-arm64@0.27.1': optional: true '@esbuild/netbsd-x64@0.20.2': optional: true - '@esbuild/netbsd-x64@0.25.2': + '@esbuild/netbsd-x64@0.27.1': optional: true - '@esbuild/openbsd-arm64@0.25.2': + '@esbuild/openbsd-arm64@0.27.1': optional: true '@esbuild/openbsd-x64@0.20.2': optional: true - '@esbuild/openbsd-x64@0.25.2': + '@esbuild/openbsd-x64@0.27.1': + optional: true + + '@esbuild/openharmony-arm64@0.27.1': optional: true '@esbuild/sunos-x64@0.20.2': optional: true - '@esbuild/sunos-x64@0.25.2': + '@esbuild/sunos-x64@0.27.1': optional: true '@esbuild/win32-arm64@0.20.2': optional: true - '@esbuild/win32-arm64@0.25.2': + '@esbuild/win32-arm64@0.27.1': optional: true '@esbuild/win32-ia32@0.20.2': optional: true - '@esbuild/win32-ia32@0.25.2': + '@esbuild/win32-ia32@0.27.1': optional: true '@esbuild/win32-x64@0.20.2': optional: true - '@esbuild/win32-x64@0.25.2': + '@esbuild/win32-x64@0.27.1': optional: true - '@gql.tada/cli-utils@1.6.3(@0no-co/graphqlsp@1.12.16(graphql@16.10.0)(typescript@5.6.2))(graphql@16.10.0)(typescript@5.6.2)': + '@gql.tada/cli-utils@1.7.2(@0no-co/graphqlsp@1.15.2(graphql@16.12.0)(typescript@5.9.3))(graphql@16.12.0)(typescript@5.9.3)': dependencies: - '@0no-co/graphqlsp': 1.12.16(graphql@16.10.0)(typescript@5.6.2) - '@gql.tada/internal': 1.0.8(graphql@16.10.0)(typescript@5.6.2) - graphql: 16.10.0 - typescript: 5.6.2 + '@0no-co/graphqlsp': 1.15.2(graphql@16.12.0)(typescript@5.9.3) + '@gql.tada/internal': 1.0.8(graphql@16.12.0)(typescript@5.9.3) + graphql: 16.12.0 + typescript: 5.9.3 - '@gql.tada/internal@1.0.8(graphql@16.10.0)(typescript@5.6.2)': + '@gql.tada/internal@1.0.8(graphql@16.12.0)(typescript@5.9.3)': dependencies: - '@0no-co/graphql.web': 1.1.2(graphql@16.10.0) - graphql: 16.10.0 - typescript: 5.6.2 + '@0no-co/graphql.web': 1.2.0(graphql@16.12.0) + graphql: 16.12.0 + typescript: 5.9.3 - '@graphql-typed-document-node/core@3.2.0(graphql@16.10.0)': + '@graphql-typed-document-node/core@3.2.0(graphql@16.12.0)': dependencies: - graphql: 16.10.0 + graphql: 16.12.0 '@jridgewell/resolve-uri@3.1.2': {} - '@jridgewell/sourcemap-codec@1.5.0': {} + '@jridgewell/sourcemap-codec@1.5.5': {} '@jridgewell/trace-mapping@0.3.9': dependencies: '@jridgewell/resolve-uri': 3.1.2 - '@jridgewell/sourcemap-codec': 1.5.0 + '@jridgewell/sourcemap-codec': 1.5.5 - '@mysten/bcs@1.6.0': + '@mysten/bcs@1.9.2': dependencies: - '@scure/base': 1.2.4 + '@mysten/utils': 0.2.0 + '@scure/base': 1.2.6 - '@mysten/deepbook-v3@0.14.0(typescript@5.6.2)': + '@mysten/deepbook-v3@0.27.0(typescript@5.9.3)': dependencies: - '@mysten/sui': 1.27.1(typescript@5.6.2) + '@mysten/bcs': 1.9.2 + '@mysten/sui': 1.45.2(typescript@5.9.3) + '@noble/hashes': 1.8.0 + axios: 1.13.2 + axios-retry: 4.5.0(axios@1.13.2) transitivePeerDependencies: - '@gql.tada/svelte-support' - '@gql.tada/vue-support' + - debug - typescript - '@mysten/sui@1.27.1(typescript@5.6.2)': - dependencies: - '@graphql-typed-document-node/core': 3.2.0(graphql@16.10.0) - '@mysten/bcs': 1.6.0 - '@noble/curves': 1.8.2 - '@noble/hashes': 1.7.2 - '@scure/base': 1.2.4 - '@scure/bip32': 1.6.2 - '@scure/bip39': 1.5.4 - gql.tada: 1.8.10(graphql@16.10.0)(typescript@5.6.2) - graphql: 16.10.0 + '@mysten/sui@1.45.2(typescript@5.9.3)': + dependencies: + '@graphql-typed-document-node/core': 3.2.0(graphql@16.12.0) + '@mysten/bcs': 1.9.2 + '@mysten/utils': 0.2.0 + '@noble/curves': 1.9.4 + '@noble/hashes': 1.8.0 + '@protobuf-ts/grpcweb-transport': 2.11.1 + '@protobuf-ts/runtime': 2.11.1 + '@protobuf-ts/runtime-rpc': 2.11.1 + '@scure/base': 1.2.6 + '@scure/bip32': 1.7.0 + '@scure/bip39': 1.6.0 + gql.tada: 1.9.0(graphql@16.12.0)(typescript@5.9.3) + graphql: 16.12.0 poseidon-lite: 0.2.1 - valibot: 0.36.0 + valibot: 1.2.0(typescript@5.9.3) transitivePeerDependencies: - '@gql.tada/svelte-support' - '@gql.tada/vue-support' - typescript - '@noble/curves@1.8.2': + '@mysten/utils@0.2.0': dependencies: - '@noble/hashes': 1.7.2 + '@scure/base': 1.2.6 - '@noble/hashes@1.7.2': {} + '@noble/curves@1.9.4': + dependencies: + '@noble/hashes': 1.8.0 - '@scure/base@1.2.4': {} + '@noble/hashes@1.8.0': {} - '@scure/bip32@1.6.2': + '@protobuf-ts/grpcweb-transport@2.11.1': dependencies: - '@noble/curves': 1.8.2 - '@noble/hashes': 1.7.2 - '@scure/base': 1.2.4 + '@protobuf-ts/runtime': 2.11.1 + '@protobuf-ts/runtime-rpc': 2.11.1 - '@scure/bip39@1.5.4': + '@protobuf-ts/runtime-rpc@2.11.1': dependencies: - '@noble/hashes': 1.7.2 - '@scure/base': 1.2.4 + '@protobuf-ts/runtime': 2.11.1 - '@tsconfig/node10@1.0.11': {} + '@protobuf-ts/runtime@2.11.1': {} + + '@scure/base@1.2.6': {} + + '@scure/bip32@1.7.0': + dependencies: + '@noble/curves': 1.9.4 + '@noble/hashes': 1.8.0 + '@scure/base': 1.2.6 + + '@scure/bip39@1.6.0': + dependencies: + '@noble/hashes': 1.8.0 + '@scure/base': 1.2.6 + + '@tsconfig/node10@1.0.12': {} '@tsconfig/node12@1.0.11': {} @@ -753,23 +906,70 @@ snapshots: '@tsconfig/node16@1.0.4': {} - '@types/node@22.7.4': + '@types/node@24.10.1': dependencies: - undici-types: 6.19.8 + undici-types: 7.16.0 acorn-walk@8.3.4: dependencies: - acorn: 8.14.1 + acorn: 8.15.0 - acorn@8.14.1: {} + acorn@8.15.0: {} arg@4.1.3: {} + asynckit@0.4.0: {} + + axios-retry@4.5.0(axios@1.13.2): + dependencies: + axios: 1.13.2 + is-retry-allowed: 2.2.0 + + axios@1.13.2: + dependencies: + follow-redirects: 1.15.11 + form-data: 4.0.5 + proxy-from-env: 1.1.0 + transitivePeerDependencies: + - debug + + call-bind-apply-helpers@1.0.2: + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + + combined-stream@1.0.8: + dependencies: + delayed-stream: 1.0.0 + create-require@1.1.1: {} + delayed-stream@1.0.0: {} + diff@4.0.2: {} - dotenv@16.5.0: {} + dotenv@16.6.1: {} + + dunder-proto@1.0.1: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-errors: 1.3.0 + gopd: 1.2.0 + + es-define-property@1.0.1: {} + + es-errors@1.3.0: {} + + es-object-atoms@1.1.1: + dependencies: + es-errors: 1.3.0 + + es-set-tostringtag@2.1.0: + dependencies: + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + has-tostringtag: 1.0.2 + hasown: 2.0.2 esbuild@0.20.2: optionalDependencies: @@ -797,92 +997,149 @@ snapshots: '@esbuild/win32-ia32': 0.20.2 '@esbuild/win32-x64': 0.20.2 - esbuild@0.25.2: + esbuild@0.27.1: optionalDependencies: - '@esbuild/aix-ppc64': 0.25.2 - '@esbuild/android-arm': 0.25.2 - '@esbuild/android-arm64': 0.25.2 - '@esbuild/android-x64': 0.25.2 - '@esbuild/darwin-arm64': 0.25.2 - '@esbuild/darwin-x64': 0.25.2 - '@esbuild/freebsd-arm64': 0.25.2 - '@esbuild/freebsd-x64': 0.25.2 - '@esbuild/linux-arm': 0.25.2 - '@esbuild/linux-arm64': 0.25.2 - '@esbuild/linux-ia32': 0.25.2 - '@esbuild/linux-loong64': 0.25.2 - '@esbuild/linux-mips64el': 0.25.2 - '@esbuild/linux-ppc64': 0.25.2 - '@esbuild/linux-riscv64': 0.25.2 - '@esbuild/linux-s390x': 0.25.2 - '@esbuild/linux-x64': 0.25.2 - '@esbuild/netbsd-arm64': 0.25.2 - '@esbuild/netbsd-x64': 0.25.2 - '@esbuild/openbsd-arm64': 0.25.2 - '@esbuild/openbsd-x64': 0.25.2 - '@esbuild/sunos-x64': 0.25.2 - '@esbuild/win32-arm64': 0.25.2 - '@esbuild/win32-ia32': 0.25.2 - '@esbuild/win32-x64': 0.25.2 + '@esbuild/aix-ppc64': 0.27.1 + '@esbuild/android-arm': 0.27.1 + '@esbuild/android-arm64': 0.27.1 + '@esbuild/android-x64': 0.27.1 + '@esbuild/darwin-arm64': 0.27.1 + '@esbuild/darwin-x64': 0.27.1 + '@esbuild/freebsd-arm64': 0.27.1 + '@esbuild/freebsd-x64': 0.27.1 + '@esbuild/linux-arm': 0.27.1 + '@esbuild/linux-arm64': 0.27.1 + '@esbuild/linux-ia32': 0.27.1 + '@esbuild/linux-loong64': 0.27.1 + '@esbuild/linux-mips64el': 0.27.1 + '@esbuild/linux-ppc64': 0.27.1 + '@esbuild/linux-riscv64': 0.27.1 + '@esbuild/linux-s390x': 0.27.1 + '@esbuild/linux-x64': 0.27.1 + '@esbuild/netbsd-arm64': 0.27.1 + '@esbuild/netbsd-x64': 0.27.1 + '@esbuild/openbsd-arm64': 0.27.1 + '@esbuild/openbsd-x64': 0.27.1 + '@esbuild/openharmony-arm64': 0.27.1 + '@esbuild/sunos-x64': 0.27.1 + '@esbuild/win32-arm64': 0.27.1 + '@esbuild/win32-ia32': 0.27.1 + '@esbuild/win32-x64': 0.27.1 + + follow-redirects@1.15.11: {} + + form-data@4.0.5: + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + es-set-tostringtag: 2.1.0 + hasown: 2.0.2 + mime-types: 2.1.35 fsevents@2.3.3: optional: true - get-tsconfig@4.10.0: + function-bind@1.1.2: {} + + get-intrinsic@1.3.0: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + function-bind: 1.1.2 + get-proto: 1.0.1 + gopd: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.2 + math-intrinsics: 1.1.0 + + get-proto@1.0.1: + dependencies: + dunder-proto: 1.0.1 + es-object-atoms: 1.1.1 + + get-tsconfig@4.13.0: dependencies: resolve-pkg-maps: 1.0.0 - gql.tada@1.8.10(graphql@16.10.0)(typescript@5.6.2): + gopd@1.2.0: {} + + gql.tada@1.9.0(graphql@16.12.0)(typescript@5.9.3): dependencies: - '@0no-co/graphql.web': 1.1.2(graphql@16.10.0) - '@0no-co/graphqlsp': 1.12.16(graphql@16.10.0)(typescript@5.6.2) - '@gql.tada/cli-utils': 1.6.3(@0no-co/graphqlsp@1.12.16(graphql@16.10.0)(typescript@5.6.2))(graphql@16.10.0)(typescript@5.6.2) - '@gql.tada/internal': 1.0.8(graphql@16.10.0)(typescript@5.6.2) - typescript: 5.6.2 + '@0no-co/graphql.web': 1.2.0(graphql@16.12.0) + '@0no-co/graphqlsp': 1.15.2(graphql@16.12.0)(typescript@5.9.3) + '@gql.tada/cli-utils': 1.7.2(@0no-co/graphqlsp@1.15.2(graphql@16.12.0)(typescript@5.9.3))(graphql@16.12.0)(typescript@5.9.3) + '@gql.tada/internal': 1.0.8(graphql@16.12.0)(typescript@5.9.3) + typescript: 5.9.3 transitivePeerDependencies: - '@gql.tada/svelte-support' - '@gql.tada/vue-support' - graphql - graphql@16.10.0: {} + graphql@16.12.0: {} + + has-symbols@1.1.0: {} + + has-tostringtag@1.0.2: + dependencies: + has-symbols: 1.1.0 + + hasown@2.0.2: + dependencies: + function-bind: 1.1.2 + + is-retry-allowed@2.2.0: {} make-error@1.3.6: {} + math-intrinsics@1.1.0: {} + + mime-db@1.52.0: {} + + mime-types@2.1.35: + dependencies: + mime-db: 1.52.0 + poseidon-lite@0.2.1: {} + proxy-from-env@1.1.0: {} + resolve-pkg-maps@1.0.0: {} - ts-node@10.9.2(@types/node@22.7.4)(typescript@5.6.2): + ts-node@10.9.2(@types/node@24.10.1)(typescript@5.9.3): dependencies: '@cspotcode/source-map-support': 0.8.1 - '@tsconfig/node10': 1.0.11 + '@tsconfig/node10': 1.0.12 '@tsconfig/node12': 1.0.11 '@tsconfig/node14': 1.0.3 '@tsconfig/node16': 1.0.4 - '@types/node': 22.7.4 - acorn: 8.14.1 + '@types/node': 24.10.1 + acorn: 8.15.0 acorn-walk: 8.3.4 arg: 4.1.3 create-require: 1.1.1 diff: 4.0.2 make-error: 1.3.6 - typescript: 5.6.2 + typescript: 5.9.3 v8-compile-cache-lib: 3.0.1 yn: 3.1.1 - tsx@4.19.3: + tsx@4.21.0: dependencies: - esbuild: 0.25.2 - get-tsconfig: 4.10.0 + esbuild: 0.27.1 + get-tsconfig: 4.13.0 optionalDependencies: fsevents: 2.3.3 - typescript@5.6.2: {} + typescript@5.9.3: {} - undici-types@6.19.8: {} + undici-types@7.16.0: {} v8-compile-cache-lib@3.0.1: {} - valibot@0.36.0: {} + valibot@1.2.0(typescript@5.9.3): + optionalDependencies: + typescript: 5.9.3 yn@3.1.1: {} diff --git a/scripts/transactions/allMvrSetup.ts b/scripts/transactions/allMvrSetup.ts new file mode 100644 index 000000000..0ab32b185 --- /dev/null +++ b/scripts/transactions/allMvrSetup.ts @@ -0,0 +1,259 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +import { namedPackagesPlugin, Transaction } from "@mysten/sui/transactions"; +import { prepareMultisigTx } from "../utils/utils"; + +export type Network = "mainnet" | "testnet" | "devnet" | "localnet"; + +const mainnetPlugin = namedPackagesPlugin({ + url: "https://mainnet.mvr.mystenlabs.com", +}); +(async () => { + const env = "mainnet"; + const transaction = new Transaction(); + transaction.addSerializationPlugin(mainnetPlugin); + + // appcap holding address + const holdingAddress = + "0x10a1fc2b9170c6bac858fdafc7d3cb1f4ea659fed748d18eff98d08debf82042"; + + const MVRAppCaps = { + // core: "0xf30a07fc1fadc8bd33ed4a9af5129967008201387b979a9899e52fbd852b29a9", + // payments: + // "0xcb44143e2921ed0fb82529ba58f5284ec77da63a8640e57c7fa8c12e87fa8baf", + subnames: + "0x969978eba35e57ad66856f137448da065bc27962a1bc4a6dd8b6cc229c899d5a", + // coupons: + // "0x4f3fa0d4da16578b8261175131bc7a24dcefe3ec83b45690e29cbc9bb3edc4de", + // discounts: + // "0x327702a5751c9582b152db81073e56c9201fad51ecbaf8bb522ae8df49f8dfd1", + // tempSubnameProxy: + // "0x3b2582036fe9aa17c059e7b3993b8dc97ae57d2ac9e1fe603884060c98385fb2", + // denylist: + // "0x8816fd949b3191040855a77a834d98aa822eb63bd2e63de2aaa0064586200882", + }; + + // for (const appCapObjectId of Object.values(MVRAppCaps)) { + // transaction.moveCall({ + // target: `@mvr/core::move_registry::set_metadata`, + // arguments: [ + // transaction.object( + // "0x0e5d473a055b6b7d014af557a13ad9075157fdc19b6d51562a18511afd397727" // Move registry + // ), + // transaction.object(appCapObjectId), + // transaction.pure.string("icon_url"), // key + // transaction.pure.string("https://docs.suins.io/logo.svg"), // value + // ], + // }); + // } + + // const kioskAppCap = + // "0x476cbd1df24cf590d675ddde59de4ec535f8aff9eea22fd83fed57001cfc9426"; + + // transaction.moveCall({ + // target: `@mvr/core::move_registry::set_metadata`, + // arguments: [ + // transaction.object( + // "0x0e5d473a055b6b7d014af557a13ad9075157fdc19b6d51562a18511afd397727" // Move registry + // ), + // transaction.object(kioskAppCap), + // transaction.pure.string("icon_url"), // key + // transaction.pure.string("https://svg-host.vercel.app/mystenlogo.svg"), + // ], + // }); + + const repository = "https://github.com/MystenLabs/suins-contracts"; + + const data = { + // core: { + // packageInfo: + // "0xf709e4075c19d9ab1ba5acb17dfbf08ddc1e328ab20eaa879454bf5f6b98758e", + // sha: latestSha, + // version: "4", + // path: "packages/suins", + // }, + // payments: { + // packageInfo: + // "0xa46d971d0e9298488605e1850d64fa067db9d66570dda8dad37bbf61ab2cca21", + // sha: latestSha, + // version: "1", + // path: "packages/payments", + // }, + subnames: { + packageInfo: + "0x9470cf5deaf2e22232244da9beeabb7b82d4a9f7b9b0784017af75c7641950ee", + sha: "releases/subdomains/2", + version: "2", + path: "packages/subdomains", + }, + // coupons: { + // packageInfo: + // "0xf7f29dce2246e6c79c8edd4094dc3039de478187b1b13e871a6a1a87775fe939", + // sha: latestSha, + // version: "2", + // path: "packages/coupons", + // }, + // discounts: { + // packageInfo: + // "0xcb8d0cefcda3949b3ff83c0014cb50ca2a7c7b2074a5a7c1f2fce68cb9ad7dd6", + // sha: latestSha, + // version: "1", + // path: "packages/discounts", + // }, + tempSubnameProxy: { + packageInfo: + "0x9accbc6d7c86abf91dcbe247fd44c6eb006d8f1864ff93b90faaeb09114d3b6f", + sha: "releases/temp_subdomain_proxy/2", + version: "2", + path: "packages/temp_subdomain_proxy", + }, + // denylist: { + // packageInfo: + // "0x5007c0681ff36e9efcb5d655af758c5eeb4825b39ef4ec2ccacd195f4f65d4f5", + // sha: latestSha, + // version: "1", + // path: "packages/denylist", + // }, + }; + + for (const [name, { packageInfo, sha, version, path }] of Object.entries( + data + )) { + const git = transaction.moveCall({ + target: `@mvr/metadata::git::new`, + arguments: [ + transaction.pure.string(repository), + transaction.pure.string(path), + transaction.pure.string(sha), + ], + }); + + transaction.moveCall({ + target: `@mvr/metadata::package_info::set_git_versioning`, + arguments: [ + transaction.object(packageInfo), + transaction.pure.u64(version), + git, + ], + }); + } + + // transaction.moveCall({ + // target: `@mvr/core::move_registry::set_metadata`, + // arguments: [ + // transaction.object( + // "0x0e5d473a055b6b7d014af557a13ad9075157fdc19b6d51562a18511afd397727" // Move registry + // ), + // transaction.object(MVRAppCaps.denylist), + // transaction.pure.string("description"), // key + // transaction.pure.string( + // "The SuiNS denylist package. Used to manage a list of disallowed names including banned names." + // ), // value + // ], + // }); + + // transaction.moveCall({ + // target: `@mvr/core::move_registry::set_metadata`, + // arguments: [ + // transaction.object( + // "0x0e5d473a055b6b7d014af557a13ad9075157fdc19b6d51562a18511afd397727" // Move registry + // ), + // transaction.object(MVRAppCaps.denylist), + // transaction.pure.string("documentation_url"), // key + // transaction.pure.string("https://docs.suins.io/"), // value + // ], + // }); + + // transaction.moveCall({ + // target: `@mvr/core::move_registry::set_metadata`, + // arguments: [ + // transaction.object( + // "0x0e5d473a055b6b7d014af557a13ad9075157fdc19b6d51562a18511afd397727" // Move registry + // ), + // transaction.object(MVRAppCaps.denylist), + // transaction.pure.string("homepage_url"), // key + // transaction.pure.string("https://suins.io/"), // value + // ], + // }); + + // transaction.moveCall({ + // target: "@mvr/metadata::package_info::set_metadata", + // arguments: [ + // transaction.object(data.denylist.packageInfo), + // transaction.pure.string("default"), + // transaction.pure.string("@suins/denylist"), + // ], + // }); + + // transaction.moveCall({ + // target: "@mvr/metadata::package_info::unset_metadata", + // arguments: [ + // transaction.object(data.tempSubnameProxy.packageInfo), + // transaction.pure.string("default"), + // ], + // }); + + // transaction.moveCall({ + // target: "@mvr/metadata::package_info::set_metadata", + // arguments: [ + // transaction.object(data.tempSubnameProxy.packageInfo), + // transaction.pure.string("default"), + // transaction.pure.string("@suins/temp-subnames-proxy"), + // ], + // }); + + const appInfo = transaction.moveCall({ + target: `@mvr/core::app_info::new`, + arguments: [ + transaction.pure.option( + "address", + "0xfb37e3fc36476472675083ff9990bad760545bd7a6c385da1e87dca58099e09b" // PackageInfo object on testnet + ), + transaction.pure.option( + "address", + "0x5afdc6b0c6c2821cd422f8985aea3c36acc6c76bf35520b3d7f47d1f5dc8bf54" // V1 of the subnames package on testnet + ), + transaction.pure.option("address", null), + ], + }); + + transaction.moveCall({ + target: `@mvr/core::move_registry::unset_network`, + arguments: [ + // the registry obj: Can also be resolved as `registry-obj@mvr` from mainnet SuiNS. + transaction.object( + "0x0e5d473a055b6b7d014af557a13ad9075157fdc19b6d51562a18511afd397727" + ), + transaction.object(MVRAppCaps.subnames), + transaction.pure.string("4c78adac"), // testnet + ], + }); + transaction.moveCall({ + target: `@mvr/core::move_registry::set_network`, + arguments: [ + // the registry obj: Can also be resolved as `registry-obj@mvr` from mainnet SuiNS. + transaction.object( + "0x0e5d473a055b6b7d014af557a13ad9075157fdc19b6d51562a18511afd397727" + ), + transaction.object(MVRAppCaps.subnames), + transaction.pure.string("4c78adac"), // testnet + appInfo, + ], + }); + + // transaction.moveCall({ + // target: `@mvr/core::move_registry::assign_package`, + // arguments: [ + // transaction.object( + // `0x0e5d473a055b6b7d014af557a13ad9075157fdc19b6d51562a18511afd397727` + // ), + // transaction.object(MVRAppCaps.denylist), + // transaction.object(data.denylist.packageInfo), + // ], + // }); + + let res = await prepareMultisigTx(transaction, env, holdingAddress); // Owner of all MVR caps + + console.dir(res, { depth: null }); +})(); diff --git a/scripts/transactions/createLiquidationVault.ts b/scripts/transactions/createLiquidationVault.ts new file mode 100644 index 000000000..9e475741e --- /dev/null +++ b/scripts/transactions/createLiquidationVault.ts @@ -0,0 +1,28 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +import { Transaction } from "@mysten/sui/transactions"; +import { prepareMultisigTx } from "../utils/utils"; +import { getFullnodeUrl, SuiClient } from "@mysten/sui/client"; +import { DeepBookClient } from "@mysten/deepbook-v3"; +import { adminCapOwner, liquidationAdminCapID } from "../config/constants"; + +(async () => { + const env = "mainnet"; + const tx = new Transaction(); + + const dbClient = new DeepBookClient({ + address: adminCapOwner[env], + env, + client: new SuiClient({ + url: getFullnodeUrl(env), + }), + }); + + dbClient.marginLiquidations.createLiquidationVault( + liquidationAdminCapID[env] + )(tx); + let res = await prepareMultisigTx(tx, env, adminCapOwner[env]); + + console.dir(res, { depth: null }); +})(); diff --git a/scripts/transactions/createPermissionlessPool.ts b/scripts/transactions/createPermissionlessPool.ts new file mode 100644 index 000000000..e8a2c1242 --- /dev/null +++ b/scripts/transactions/createPermissionlessPool.ts @@ -0,0 +1,44 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 +import { Transaction } from "@mysten/sui/transactions"; +import { DeepBookClient } from "@mysten/deepbook-v3"; +import { getFullnodeUrl, SuiClient } from "@mysten/sui/client"; + +(async () => { + // Update constant for env + const env = "mainnet"; + + const coinMap = { + // Define custom coins as needed + COIN_A: { + address: "", // ex: 0xdba34672e30cb065b1f93e3ab55318768fd6fef66c15942c9f7cb846e2f900e7 + type: "", // ex: 0xdba34672e30cb065b1f93e3ab55318768fd6fef66c15942c9f7cb846e2f900e7::usdc::USDC + scalar: 0, // scalar, 1000000 for 6 decimals as example + }, + COIN_B: { + address: "", // ex: 0xdba34672e30cb065b1f93e3ab55318768fd6fef66c15942c9f7cb846e2f900e7 + type: "", // ex: 0xdba34672e30cb065b1f93e3ab55318768fd6fef66c15942c9f7cb846e2f900e7::usdc::USDC + scalar: 0, // scalar, 1000000 for 6 decimals as example + }, + }; + + const dbClient = new DeepBookClient({ + address: "0x0", + env: env, + client: new SuiClient({ + url: getFullnodeUrl(env), + }), + coinMap, + }); + + const tx = new Transaction(); + + // follow conventions defined in https://docs.sui.io/standards/deepbookv3/permissionless-pool for tick/lot/min sizes + dbClient.deepBook.createPermissionlessPool({ + baseCoinKey: "COIN_A", + quoteCoinKey: "COIN_B", + tickSize: 0.00001, // true value of tick size + lotSize: 0.1, // true value of lot size + minSize: 1, // true value of min size + })(tx); +})(); diff --git a/scripts/transactions/createPool.ts b/scripts/transactions/createPool.ts index 3fc9924b2..98408b6cf 100644 --- a/scripts/transactions/createPool.ts +++ b/scripts/transactions/createPool.ts @@ -10,42 +10,23 @@ import { getFullnodeUrl, SuiClient } from "@mysten/sui/client"; // Update constant for env const env = "mainnet"; - // Initialize with balance managers if needed - const balanceManagers = { - MANAGER_1: { - address: "", - tradeCap: "", - }, - }; - const dbClient = new DeepBookClient({ address: "0x0", env: env, client: new SuiClient({ url: getFullnodeUrl(env), }), - balanceManagers: balanceManagers, adminCap: adminCapID[env], }); const tx = new Transaction(); dbClient.deepBookAdmin.createPoolAdmin({ - baseCoinKey: "WAL", - quoteCoinKey: "USDC", - tickSize: 0.000001, - lotSize: 0.1, - minSize: 1, - whitelisted: false, - stablePool: false, - })(tx); - - dbClient.deepBookAdmin.createPoolAdmin({ - baseCoinKey: "WAL", - quoteCoinKey: "SUI", - tickSize: 0.000001, - lotSize: 0.1, - minSize: 1, + baseCoinKey: "LZWBTC", // 8 + quoteCoinKey: "USDC", // 6 + tickSize: 1, + lotSize: 0.00001, // $1 + minSize: 0.00001, // $1 whitelisted: false, stablePool: false, })(tx); diff --git a/scripts/transactions/enableVersion.ts b/scripts/transactions/enableVersion.ts index 9bf9d0d77..d327e8db8 100644 --- a/scripts/transactions/enableVersion.ts +++ b/scripts/transactions/enableVersion.ts @@ -7,48 +7,43 @@ import { DeepBookClient } from "@mysten/deepbook-v3"; import { getFullnodeUrl, SuiClient } from "@mysten/sui/client"; (async () => { - // Update constant for env - const env = "mainnet"; - const versionToEnable = 2; + // Update constant for env + const env = "mainnet"; + const versionToEnable = 6; - // Initialize with balance managers if needed - const balanceManagers = { - MANAGER_1: { - address: "", - tradeCap: "", - }, - }; + const dbClient = new DeepBookClient({ + address: "0x0", + env: env, + client: new SuiClient({ + url: getFullnodeUrl(env), + }), + adminCap: adminCapID[env], + }); - const dbClient = new DeepBookClient({ - address: "0x0", - env: env, - client: new SuiClient({ - url: getFullnodeUrl(env), - }), - balanceManagers: balanceManagers, - adminCap: adminCapID[env], - }); + const tx = new Transaction(); - const tx = new Transaction(); + dbClient.deepBookAdmin.enableVersion(versionToEnable)(tx); + dbClient.deepBookAdmin.updateAllowedVersions("DEEP_SUI")(tx); + dbClient.deepBookAdmin.updateAllowedVersions("SUI_USDC")(tx); + dbClient.deepBookAdmin.updateAllowedVersions("DEEP_USDC")(tx); + dbClient.deepBookAdmin.updateAllowedVersions("WUSDT_USDC")(tx); + dbClient.deepBookAdmin.updateAllowedVersions("WUSDC_USDC")(tx); + dbClient.deepBookAdmin.updateAllowedVersions("BETH_USDC")(tx); + dbClient.deepBookAdmin.updateAllowedVersions("NS_USDC")(tx); + dbClient.deepBookAdmin.updateAllowedVersions("NS_SUI")(tx); + dbClient.deepBookAdmin.updateAllowedVersions("TYPUS_SUI")(tx); + dbClient.deepBookAdmin.updateAllowedVersions("SUI_AUSD")(tx); + dbClient.deepBookAdmin.updateAllowedVersions("AUSD_USDC")(tx); + dbClient.deepBookAdmin.updateAllowedVersions("DRF_SUI")(tx); + dbClient.deepBookAdmin.updateAllowedVersions("SEND_USDC")(tx); + dbClient.deepBookAdmin.updateAllowedVersions("WAL_USDC")(tx); + dbClient.deepBookAdmin.updateAllowedVersions("WAL_SUI")(tx); + dbClient.deepBookAdmin.updateAllowedVersions("XBTC_USDC")(tx); + dbClient.deepBookAdmin.updateAllowedVersions("IKA_USDC")(tx); + dbClient.deepBookAdmin.updateAllowedVersions("ALKIMI_SUI")(tx); + dbClient.deepBookAdmin.updateAllowedVersions("LZWBTC_USDC")(tx); - dbClient.deepBookAdmin.enableVersion(versionToEnable)(tx); - dbClient.deepBookAdmin.updateAllowedVersions("DEEP_SUI")(tx); - dbClient.deepBookAdmin.updateAllowedVersions("SUI_USDC")(tx); - dbClient.deepBookAdmin.updateAllowedVersions("DEEP_USDC")(tx); - dbClient.deepBookAdmin.updateAllowedVersions("WUSDT_USDC")(tx); - dbClient.deepBookAdmin.updateAllowedVersions("WUSDC_USDC")(tx); - dbClient.deepBookAdmin.updateAllowedVersions("BETH_USDC")(tx); - dbClient.deepBookAdmin.updateAllowedVersions("NS_USDC")(tx); - dbClient.deepBookAdmin.updateAllowedVersions("NS_SUI")(tx); - dbClient.deepBookAdmin.updateAllowedVersions("TYPUS_SUI")(tx); - dbClient.deepBookAdmin.updateAllowedVersions("SUI_AUSD")(tx); - dbClient.deepBookAdmin.updateAllowedVersions("AUSD_USDC")(tx); - dbClient.deepBookAdmin.updateAllowedVersions("DRF_SUI")(tx); - dbClient.deepBookAdmin.updateAllowedVersions("SEND_USDC")(tx); - dbClient.deepBookAdmin.updateAllowedVersions("WAL_USDC")(tx); - dbClient.deepBookAdmin.updateAllowedVersions("WAL_SUI")(tx); + let res = await prepareMultisigTx(tx, env, adminCapOwner[env]); - let res = await prepareMultisigTx(tx, env, adminCapOwner[env]); - - console.dir(res, { depth: null }); + console.dir(res, { depth: null }); })(); diff --git a/scripts/transactions/fundAbyssVault.ts b/scripts/transactions/fundAbyssVault.ts new file mode 100644 index 000000000..704db421f --- /dev/null +++ b/scripts/transactions/fundAbyssVault.ts @@ -0,0 +1,57 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +import { Transaction } from "@mysten/sui/transactions"; +import { prepareMultisigTx } from "../utils/utils"; +import { adminCapOwner, supplierCapID } from "../config/constants"; +import { getFullnodeUrl, SuiClient } from "@mysten/sui/client"; +import { DeepBookClient } from "@mysten/deepbook-v3"; + +const ABYSS_VAULT_PACKAGE = + "0x90a75f641859f4d77a4349d67e518e1dd9ecb4fac079e220fa46b7a7f164e0a5"; +const USDC_TYPE = + "0xdba34672e30cb065b1f93e3ab55318768fd6fef66c15942c9f7cb846e2f900e7::usdc::USDC"; +const ATOKEN_TYPE = `${ABYSS_VAULT_PACKAGE}::abyss_vault::AToken<${USDC_TYPE}>`; + +// Shared objects +const AbyssVault = + "0x86cd17116a5c1bc95c25296a901eb5ea91531cb8ba59d01f64ee2018a14d6fa5"; +const marginPool = + "0xba473d9ae278f10af75c50a8fa341e9c6a1c087dc91a3f23e8048baf67d0754f"; +const vaultRegistry = + "0xfac1800074e8ed8eb2baf1e631e8199ccce6b0f6bfd50b5143e1ff47c438aecf"; +const marginRegistry = + "0x0e40998b359a9ccbab22a98ed21bd4346abf19158bc7980c8291908086b3a742"; +const abyssSupplierCap = + "0x3d0faab3953525d243275b39cbed465cb310fe2d4dd2c15428b8f7cf5962c2c0"; + +(async () => { + const env = "mainnet"; + const tx = new Transaction(); + + // Supply 99k into abyss vault + const yieldTokens = tx.moveCall({ + target: `${ABYSS_VAULT_PACKAGE}::abyss_vault::supply`, + typeArguments: [USDC_TYPE, ATOKEN_TYPE], + arguments: [ + tx.object(AbyssVault), + tx.object(marginPool), + tx.object(vaultRegistry), + tx.object(marginRegistry), + tx.object( + "0x392b92d4a872bff969d9ef8d51c3d7a4223fe2b75da29d7befb2aee25c017562", + ), + tx.object(abyssSupplierCap), + tx.pure.option( + "id", + "0xba436b3f0e57600e9318c2e03c51b940612d8b0d4df18ad9f31c203f95cad122", + ), + tx.object.clock(), + ], + }); + tx.transferObjects([yieldTokens], adminCapOwner[env]); + + let res = await prepareMultisigTx(tx, env, adminCapOwner[env]); + + console.dir(res, { depth: null }); +})(); diff --git a/scripts/transactions/fundLiquidationVault.ts b/scripts/transactions/fundLiquidationVault.ts new file mode 100644 index 000000000..5343bcbf4 --- /dev/null +++ b/scripts/transactions/fundLiquidationVault.ts @@ -0,0 +1,65 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +import { Transaction } from "@mysten/sui/transactions"; +import { prepareMultisigTx } from "../utils/utils"; +import { getFullnodeUrl, SuiClient } from "@mysten/sui/client"; +import { DeepBookClient } from "@mysten/deepbook-v3"; +import { adminCapOwner, liquidationAdminCapID } from "../config/constants"; + +(async () => { + const env = "mainnet"; + const tx = new Transaction(); + const vaultId = + "0xae8e060630107720560d49e99f352b41a9f1696675021f087b69b57d35d814b6"; + + const dbClient = new DeepBookClient({ + address: adminCapOwner[env], + env, + client: new SuiClient({ + url: getFullnodeUrl(env), + }), + }); + + // Amounts to deposit + const suiAmount = 5_494; + const usdcAmount = 10_000; + const deepAmount = 190_000; + const walAmount = 73_000; + + dbClient.marginLiquidations.deposit( + vaultId, + liquidationAdminCapID[env], + "SUI", + suiAmount + )(tx); + + dbClient.marginLiquidations.deposit( + vaultId, + liquidationAdminCapID[env], + "USDC", + usdcAmount + )(tx); + + dbClient.marginLiquidations.deposit( + vaultId, + liquidationAdminCapID[env], + "DEEP", + deepAmount + )(tx); + + dbClient.marginLiquidations.deposit( + vaultId, + liquidationAdminCapID[env], + "WAL", + walAmount + )(tx); + + const supplierCap = dbClient.marginPool.mintSupplierCap()(tx); + dbClient.marginPool.supplyToMarginPool("USDC", supplierCap, 10_000)(tx); + tx.transferObjects([supplierCap], adminCapOwner[env]); + + let res = await prepareMultisigTx(tx, env, adminCapOwner[env]); + + console.dir(res, { depth: null }); +})(); diff --git a/scripts/transactions/initManagerMap.ts b/scripts/transactions/initManagerMap.ts new file mode 100644 index 000000000..5493e0a5e --- /dev/null +++ b/scripts/transactions/initManagerMap.ts @@ -0,0 +1,28 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 +import { Transaction } from "@mysten/sui/transactions"; +import { prepareMultisigTx } from "../utils/utils"; +import { adminCapOwner, adminCapID } from "../config/constants"; +import { DeepBookClient } from "@mysten/deepbook-v3"; +import { getFullnodeUrl, SuiClient } from "@mysten/sui/client"; + +(async () => { + const env = "mainnet"; + + const dbClient = new DeepBookClient({ + address: "0x0", + env: env, + client: new SuiClient({ + url: getFullnodeUrl(env), + }), + adminCap: adminCapID[env], + }); + + const tx = new Transaction(); + + dbClient.deepBookAdmin.initBalanceManagerMap()(tx); + + let res = await prepareMultisigTx(tx, env, adminCapOwner[env]); + + console.dir(res, { depth: null }); +})(); diff --git a/scripts/transactions/linkPackageInfo.ts b/scripts/transactions/linkPackageInfo.ts index 3764beaaf..3d24561bb 100644 --- a/scripts/transactions/linkPackageInfo.ts +++ b/scripts/transactions/linkPackageInfo.ts @@ -1,17 +1,22 @@ // Copyright (c) Mysten Labs, Inc. // SPDX-License-Identifier: Apache-2.0 -import { newTransaction } from "./transaction"; +import { namedPackagesPlugin, Transaction } from "@mysten/sui/transactions"; import { prepareMultisigTx } from "../utils/utils"; export type Network = "mainnet" | "testnet" | "devnet" | "localnet"; +const mainnetPlugin = namedPackagesPlugin({ + url: "https://mainnet.mvr.mystenlabs.com", +}); + (async () => { // Update constant for env const env = "mainnet"; - const transaction = newTransaction(); - const appCapObjectId = - "0xae2d10803aa2f22e3756235d0f98da17e3aa3e4de8dd0062822e2e899e901a04"; + const transaction = new Transaction(); + transaction.addSerializationPlugin(mainnetPlugin); + // const appCapObjectId = + // "0xae2d10803aa2f22e3756235d0f98da17e3aa3e4de8dd0062822e2e899e901a04"; const packageInfoId = "0x4874e126c490e495ff7490523841bdba57e2a01ed36db7610f07d417c8b5a988"; @@ -21,7 +26,7 @@ export type Network = "mainnet" | "testnet" | "devnet" | "localnet"; arguments: [ transaction.pure.string("https://github.com/MystenLabs/deepbookv3"), transaction.pure.string("packages/deepbook"), - transaction.pure.string("b9082548ee8181e118fcab618778cf2a9bae3b2e"), + transaction.pure.string("v3.0.0"), ], }); @@ -29,113 +34,113 @@ export type Network = "mainnet" | "testnet" | "devnet" | "localnet"; target: `@mvr/metadata::package_info::set_git_versioning`, arguments: [ transaction.object(packageInfoId), - transaction.pure.u64(`1`), + transaction.pure.u64(`3`), git, ], }); - // 2. Set metadata for mainnet (description, icon_url, documentation_url, homepage_url) - transaction.moveCall({ - target: `@mvr/core::move_registry::set_metadata`, - arguments: [ - transaction.object( - "0x0e5d473a055b6b7d014af557a13ad9075157fdc19b6d51562a18511afd397727" // Move registry - ), - transaction.object(appCapObjectId), - transaction.pure.string("description"), // key - transaction.pure.string( - "DeepBook V3 is a next-generation decentralized central limit order book (CLOB) built on Sui. DeepBook leverages Sui's parallel execution and low transaction fees to bring a highly performant, low-latency exchange on chain." - ), // value - ], - }); - - transaction.moveCall({ - target: `@mvr/core::move_registry::set_metadata`, - arguments: [ - transaction.object( - "0x0e5d473a055b6b7d014af557a13ad9075157fdc19b6d51562a18511afd397727" // Move registry - ), - transaction.object(appCapObjectId), - transaction.pure.string("icon_url"), // key - transaction.pure.string("https://images.deepbook.tech/icon.svg"), // value - ], - }); - - transaction.moveCall({ - target: `@mvr/core::move_registry::set_metadata`, - arguments: [ - transaction.object( - "0x0e5d473a055b6b7d014af557a13ad9075157fdc19b6d51562a18511afd397727" // Move registry - ), - transaction.object(appCapObjectId), - transaction.pure.string("documentation_url"), // key - transaction.pure.string("https://docs.sui.io/standards/deepbookv3"), // value - ], - }); - - transaction.moveCall({ - target: `@mvr/core::move_registry::set_metadata`, - arguments: [ - transaction.object( - "0x0e5d473a055b6b7d014af557a13ad9075157fdc19b6d51562a18511afd397727" // Move registry - ), - transaction.object(appCapObjectId), - transaction.pure.string("homepage_url"), // key - transaction.pure.string("https://deepbook.tech/"), // value - ], - }); - - // 3. Set default metadata for mainnet - - transaction.moveCall({ - target: "@mvr/metadata::package_info::set_metadata", - arguments: [ - transaction.object(packageInfoId), - transaction.pure.string("default"), - transaction.pure.string("@deepbook/core"), - ], - }); - - // 4. Links testnet packageInfo - const appInfo = transaction.moveCall({ - target: `@mvr/core::app_info::new`, - arguments: [ - transaction.pure.option( - "address", - "0x35f509124a4a34981e5b1ba279d1fdfc0af3502ae1edf101e49a2d724a4c1a34" // PackageInfo object on testnet - ), - transaction.pure.option( - "address", - "0x984757fc7c0e6dd5f15c2c66e881dd6e5aca98b725f3dbd83c445e057ebb790a" // V2 of the package on testnet - ), - transaction.pure.option("address", null), - ], - }); - - transaction.moveCall({ - target: `@mvr/core::move_registry::set_network`, - arguments: [ - // the registry obj: Can also be resolved as `registry-obj@mvr` from mainnet SuiNS. - transaction.object( - "0x0e5d473a055b6b7d014af557a13ad9075157fdc19b6d51562a18511afd397727" - ), - transaction.object(appCapObjectId), - transaction.pure.string("4c78adac"), // testnet - appInfo, - ], - }); - - // 5. Linked mainnet packageInfo with appCap - transaction.moveCall({ - target: `@mvr/core::move_registry::assign_package`, - arguments: [ - transaction.object( - `0x0e5d473a055b6b7d014af557a13ad9075157fdc19b6d51562a18511afd397727` - ), - transaction.object(appCapObjectId), - transaction.object(packageInfoId), - ], - }); + // // 2. Set metadata for mainnet (description, icon_url, documentation_url, homepage_url) + // transaction.moveCall({ + // target: `@mvr/core::move_registry::set_metadata`, + // arguments: [ + // transaction.object( + // "0x0e5d473a055b6b7d014af557a13ad9075157fdc19b6d51562a18511afd397727" // Move registry + // ), + // transaction.object(appCapObjectId), + // transaction.pure.string("description"), // key + // transaction.pure.string( + // "DeepBook V3 is a next-generation decentralized central limit order book (CLOB) built on Sui. DeepBook leverages Sui's parallel execution and low transaction fees to bring a highly performant, low-latency exchange on chain." + // ), // value + // ], + // }); + + // transaction.moveCall({ + // target: `@mvr/core::move_registry::set_metadata`, + // arguments: [ + // transaction.object( + // "0x0e5d473a055b6b7d014af557a13ad9075157fdc19b6d51562a18511afd397727" // Move registry + // ), + // transaction.object(appCapObjectId), + // transaction.pure.string("icon_url"), // key + // transaction.pure.string("https://images.deepbook.tech/icon.svg"), // value + // ], + // }); + + // transaction.moveCall({ + // target: `@mvr/core::move_registry::set_metadata`, + // arguments: [ + // transaction.object( + // "0x0e5d473a055b6b7d014af557a13ad9075157fdc19b6d51562a18511afd397727" // Move registry + // ), + // transaction.object(appCapObjectId), + // transaction.pure.string("documentation_url"), // key + // transaction.pure.string("https://docs.sui.io/standards/deepbookv3"), // value + // ], + // }); + + // transaction.moveCall({ + // target: `@mvr/core::move_registry::set_metadata`, + // arguments: [ + // transaction.object( + // "0x0e5d473a055b6b7d014af557a13ad9075157fdc19b6d51562a18511afd397727" // Move registry + // ), + // transaction.object(appCapObjectId), + // transaction.pure.string("homepage_url"), // key + // transaction.pure.string("https://deepbook.tech/"), // value + // ], + // }); + + // // 3. Set default metadata for mainnet + + // transaction.moveCall({ + // target: "@mvr/metadata::package_info::set_metadata", + // arguments: [ + // transaction.object(packageInfoId), + // transaction.pure.string("default"), + // transaction.pure.string("@deepbook/core"), + // ], + // }); + + // // 4. Links testnet packageInfo + // const appInfo = transaction.moveCall({ + // target: `@mvr/core::app_info::new`, + // arguments: [ + // transaction.pure.option( + // "address", + // "0x35f509124a4a34981e5b1ba279d1fdfc0af3502ae1edf101e49a2d724a4c1a34" // PackageInfo object on testnet + // ), + // transaction.pure.option( + // "address", + // "0x984757fc7c0e6dd5f15c2c66e881dd6e5aca98b725f3dbd83c445e057ebb790a" // V2 of the package on testnet + // ), + // transaction.pure.option("address", null), + // ], + // }); + + // transaction.moveCall({ + // target: `@mvr/core::move_registry::set_network`, + // arguments: [ + // // the registry obj: Can also be resolved as `registry-obj@mvr` from mainnet SuiNS. + // transaction.object( + // "0x0e5d473a055b6b7d014af557a13ad9075157fdc19b6d51562a18511afd397727" + // ), + // transaction.object(appCapObjectId), + // transaction.pure.string("4c78adac"), // testnet + // appInfo, + // ], + // }); + + // // 5. Linked mainnet packageInfo with appCap + // transaction.moveCall({ + // target: `@mvr/core::move_registry::assign_package`, + // arguments: [ + // transaction.object( + // `0x0e5d473a055b6b7d014af557a13ad9075157fdc19b6d51562a18511afd397727` + // ), + // transaction.object(appCapObjectId), + // transaction.object(packageInfoId), + // ], + // }); let res = await prepareMultisigTx( transaction, diff --git a/scripts/transactions/mainPackageUpgrade.ts b/scripts/transactions/mainPackageUpgrade.ts index 63c41b8f5..1242b1b74 100644 --- a/scripts/transactions/mainPackageUpgrade.ts +++ b/scripts/transactions/mainPackageUpgrade.ts @@ -1,34 +1,45 @@ // Copyright (c) Mysten Labs, Inc. // SPDX-License-Identifier: Apache-2.0 -import { execSync } from 'child_process'; -import { upgradeCapID } from '../config/constants'; +import { execSync } from "child_process"; +import { writeFileSync } from "fs"; +import { upgradeCapID } from "../config/constants"; -const network = 'mainnet'; +const network = "mainnet"; // Active env of sui has to be the same with the env we're publishing to. // if upgradeCap & gasObject is on mainnet, it has to be on mainnet. // Github actions are always on mainnet. const mainPackageUpgrade = async () => { - const gasObjectId = process.env.GAS_OBJECT; - - // Enabling the gas Object check only on mainnet, to allow testnet multisig tests. - if (!gasObjectId) throw new Error('No gas object supplied for a mainnet transaction'); - - const upgradeCall = `sui client upgrade --upgrade-capability ${upgradeCapID[network]} --gas-budget 3000000000 --gas ${gasObjectId} --skip-dependency-verification --serialize-unsigned-transaction`; - - try { - // Execute the command with the specified working directory and capture the output - execSync(`cd $PWD/../packages/deepbook && ${upgradeCall} > $PWD/../../scripts/tx/tx-data.txt`); - - console.log('Upgrade transaction successfully created and saved to tx-data.txt'); - } catch (error: any) { - console.error('Error during protocol upgrade:', error.message); - console.error('stderr:', error.stderr?.toString()); - console.error('stdout:', error.stdout?.toString()); - console.error('Command:', error.cmd); - process.exit(1); // Exit with an error code - } + const gasObjectId = process.env.GAS_OBJECT; + + // Enabling the gas Object check only on mainnet, to allow testnet multisig tests. + if (!gasObjectId) + throw new Error("No gas object supplied for a mainnet transaction"); + + const currentDir = process.cwd(); + const deepbookDir = `${currentDir}/../packages/deepbook`; + const txFilePath = `${currentDir}/tx/tx-data.txt`; + const upgradeCall = `sui client upgrade --upgrade-capability ${upgradeCapID[network]} --gas-budget 2000000000 --gas ${gasObjectId} --serialize-unsigned-transaction`; + + try { + // Execute the command with the specified working directory and capture the output + const output = execSync(upgradeCall, { + cwd: deepbookDir, + stdio: "pipe", + }).toString(); + + writeFileSync(txFilePath, output); + console.log( + "Upgrade transaction successfully created and saved to tx-data.txt" + ); + } catch (error: any) { + console.error("Error during protocol upgrade:", error.message); + console.error("stderr:", error.stderr?.toString()); + console.error("stdout:", error.stdout?.toString()); + console.error("Command:", error.cmd); + process.exit(1); // Exit with an error code + } }; mainPackageUpgrade(); diff --git a/scripts/transactions/marginPackageInfo.ts b/scripts/transactions/marginPackageInfo.ts new file mode 100644 index 000000000..cc8d8ac7e --- /dev/null +++ b/scripts/transactions/marginPackageInfo.ts @@ -0,0 +1,48 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +import { namedPackagesPlugin, Transaction } from "@mysten/sui/transactions"; +import { prepareMultisigTx } from "../utils/utils"; + +export type Network = "mainnet" | "testnet" | "devnet" | "localnet"; + +const mainnetPlugin = namedPackagesPlugin({ + url: "https://mainnet.mvr.mystenlabs.com", +}); + +(async () => { + // Update constant for env + const env = "mainnet"; + const transaction = new Transaction(); + transaction.addSerializationPlugin(mainnetPlugin); + + // appcap holding address + const holdingAddress = + "0xd0ec0b201de6b4e7f425918bbd7151c37fc1b06c59b3961a2a00db74f6ea865e"; + + const mainnetUpgradeCap = + "0xd57c7a41b31c0a1fab5e71d296a75daf2e9e09945df383d4745daa49f06d9c56"; + + const mainnetPackageInfo = transaction.moveCall({ + target: `@mvr/metadata::package_info::new`, + arguments: [ + transaction.object(mainnetUpgradeCap), // Margin UpgradeCap + ], + }); + + transaction.moveCall({ + target: `@mvr/metadata::package_info::transfer`, + arguments: [ + transaction.object(mainnetPackageInfo), + transaction.pure.address(holdingAddress), + ], + }); + + let res = await prepareMultisigTx( + transaction, + env, + "0x37f187e1e54e9c9b8c78b6c46a7281f644ebc62e75493623edcaa6d1dfcf64d2", + ); // multisig address + + console.dir(res, { depth: null }); +})(); diff --git a/scripts/transactions/marginPrep.ts b/scripts/transactions/marginPrep.ts new file mode 100644 index 000000000..bab9b2cdf --- /dev/null +++ b/scripts/transactions/marginPrep.ts @@ -0,0 +1,227 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 +import { Transaction } from "@mysten/sui/transactions"; +import { prepareMultisigTx } from "../utils/utils"; +import { + adminCapOwner, + adminCapID, + marginAdminCapID, + marginMaintainerCapID, + suiMarginPoolCapID, + usdcMarginPoolCapID, + deepMarginPoolCapID, + walMarginPoolCapID, +} from "../config/constants"; +import { DeepBookClient } from "@mysten/deepbook-v3"; +import { getFullnodeUrl, SuiClient } from "@mysten/sui/client"; + +(async () => { + // Update constant for env + const env = "mainnet"; + + const dbClient = new DeepBookClient({ + address: "0x0", + env: env, + client: new SuiClient({ + url: getFullnodeUrl(env), + }), + adminCap: adminCapID[env], + marginAdminCap: marginAdminCapID[env], + marginMaintainerCap: marginMaintainerCapID[env], + }); + + const tx = new Transaction(); + + // // 1. Enable Margin package in core deepbook + // dbClient.deepBookAdmin.authorizeMarginApp()(tx); + + // // 2. PauseCap distribution + // const pauseCap1 = dbClient.marginAdmin.mintPauseCap()(tx); + // const pauseCap2 = dbClient.marginAdmin.mintPauseCap()(tx); + // const pauseCap3 = dbClient.marginAdmin.mintPauseCap()(tx); + // tx.transferObjects( + // [pauseCap1], + // "0x517f822cd3c45a3ac3dbfab73c060d9a0d96bec7fffa204c341e7e0877c9787c" + // ); + // tx.transferObjects( + // [pauseCap2], + // "0x1b71380623813c8aee2ab9a68d96c19d0e45fc872e8c22dd70dfedfb76cbb192" + // ); + // tx.transferObjects( + // [pauseCap3], + // "0x7da4267928e568da4f64f5a80f5b63680f3c2e008f4f96f475b60ff1c48c0dcf" + // ); + + // // 3. Mint maintainerCap + // const maintainerCap = dbClient.marginAdmin.mintMaintainerCap()(tx); + // tx.transferObjects([maintainerCap], adminCapOwner[env]); + + // // 4. Pyth Config + // const maxAgeSeconds = 70; + // const pythConfig = dbClient.marginAdmin.newPythConfig( + // [ + // { coinKey: "SUI", maxConfBps: 300, maxEwmaDifferenceBps: 1500 }, // maxConfBps: 3%, maxEwmaDifferenceBps: 15% + // { coinKey: "USDC", maxConfBps: 100, maxEwmaDifferenceBps: 500 }, // maxConfBps: 1%, maxEwmaDifferenceBps: 5% + // { coinKey: "DEEP", maxConfBps: 500, maxEwmaDifferenceBps: 3000 }, // maxConfBps: 5%, maxEwmaDifferenceBps: 30% + // { coinKey: "WAL", maxConfBps: 500, maxEwmaDifferenceBps: 3000 }, // maxConfBps: 5%, maxEwmaDifferenceBps: 30% + // ], + // maxAgeSeconds // maxAgeSeconds: 70 seconds + // )(tx); + // dbClient.marginAdmin.removeConfig()(tx); + // dbClient.marginAdmin.addConfig(pythConfig)(tx); + + // // 5. Create margin pools + // const USDCprotocolConfig = dbClient.marginMaintainer.newProtocolConfig( + // "USDC", + // { + // supplyCap: 1_000_000, + // maxUtilizationRate: 0.8, + // referralSpread: 0.2, + // minBorrow: 0.1, + // rateLimitCapacity: 200_000, + // rateLimitRefillRatePerMs: 0.009259, // 200_000 / 21_600_000 (6 hours) + // rateLimitEnabled: true, + // }, + // { + // baseRate: 0.1, + // baseSlope: 0.15, + // optimalUtilization: 0.8, + // excessSlope: 5, + // } + // )(tx); + // dbClient.marginMaintainer.createMarginPool("USDC", USDCprotocolConfig)(tx); + + // const SUIprotocolConfig = dbClient.marginMaintainer.newProtocolConfig( + // "SUI", + // { + // supplyCap: 500_000, + // maxUtilizationRate: 0.8, + // referralSpread: 0.2, + // minBorrow: 0.1, + // rateLimitCapacity: 100_000, + // rateLimitRefillRatePerMs: 0.00462963, // 100_000 / 21_600_000 (6 hours) + // rateLimitEnabled: true, + // }, + // { + // baseRate: 0.1, + // baseSlope: 0.2, + // optimalUtilization: 0.8, + // excessSlope: 5, + // } + // )(tx); + // dbClient.marginMaintainer.createMarginPool("SUI", SUIprotocolConfig)(tx); + + // const DEEPprotocolConfig = dbClient.marginMaintainer.newProtocolConfig( + // "DEEP", + // { + // supplyCap: 20_000_000, + // maxUtilizationRate: 0.8, + // referralSpread: 0.2, + // minBorrow: 0.1, + // rateLimitCapacity: 4_000_000, + // rateLimitRefillRatePerMs: 0.185185, // 4_000_000 / 21_600_000 (6 hours) + // rateLimitEnabled: true, + // }, + // { + // baseRate: 0.15, + // baseSlope: 0.2, + // optimalUtilization: 0.8, + // excessSlope: 5, + // } + // )(tx); + // dbClient.marginMaintainer.createMarginPool("DEEP", DEEPprotocolConfig)(tx); + + // const WALprotocolConfig = dbClient.marginMaintainer.newProtocolConfig( + // "WAL", + // { + // supplyCap: 7_000_000, + // maxUtilizationRate: 0.8, + // referralSpread: 0.2, + // minBorrow: 0.1, + // rateLimitCapacity: 1_400_000, + // rateLimitRefillRatePerMs: 0.064814815, // 1_400_000 / 21_600_000 (6 hours) + // rateLimitEnabled: true, + // }, + // { + // baseRate: 0.15, + // baseSlope: 0.2, + // optimalUtilization: 0.8, + // excessSlope: 5, + // } + // )(tx); + // dbClient.marginMaintainer.createMarginPool("WAL", WALprotocolConfig)(tx); + + // // 3. Registering SUI_DBUSDC pool + // const PoolConfigSUIUSDC = dbClient.marginAdmin.newPoolConfig("SUI_USDC", { + // minWithdrawRiskRatio: 2, + // minBorrowRiskRatio: 1.2499, + // liquidationRiskRatio: 1.1, + // targetLiquidationRiskRatio: 1.25, + // userLiquidationReward: 0.02, + // poolLiquidationReward: 0.03, + // })(tx); + + // dbClient.marginAdmin.registerDeepbookPool("SUI_USDC", PoolConfigSUIUSDC)(tx); + // dbClient.marginAdmin.enableDeepbookPool("SUI_USDC")(tx); + + // const PoolConfigDEEPUSDC = dbClient.marginAdmin.newPoolConfig("DEEP_USDC", { + // minWithdrawRiskRatio: 2, + // minBorrowRiskRatio: 1.4999, + // liquidationRiskRatio: 1.2, + // targetLiquidationRiskRatio: 1.5, + // userLiquidationReward: 0.02, + // poolLiquidationReward: 0.03, + // })(tx); + // dbClient.marginAdmin.registerDeepbookPool( + // "DEEP_USDC", + // PoolConfigDEEPUSDC + // )(tx); + // dbClient.marginAdmin.enableDeepbookPool("DEEP_USDC")(tx); + + // const poolConfigWalUsdc = dbClient.marginAdmin.newPoolConfig("WAL_USDC", { + // minWithdrawRiskRatio: 2, + // minBorrowRiskRatio: 1.4999, + // liquidationRiskRatio: 1.2, + // targetLiquidationRiskRatio: 1.5, + // userLiquidationReward: 0.02, + // poolLiquidationReward: 0.03, + // })(tx); + // dbClient.marginAdmin.registerDeepbookPool("WAL_USDC", poolConfigWalUsdc)(tx); + // dbClient.marginAdmin.enableDeepbookPool("WAL_USDC")(tx); + + // // 4. Enable deepbook pool for loan + // dbClient.marginMaintainer.enableDeepbookPoolForLoan( + // "SUI_USDC", + // "USDC", + // tx.object(usdcMarginPoolCapID[env]) + // )(tx); + // dbClient.marginMaintainer.enableDeepbookPoolForLoan( + // "DEEP_USDC", + // "USDC", + // tx.object(usdcMarginPoolCapID[env]) + // )(tx); + // dbClient.marginMaintainer.enableDeepbookPoolForLoan( + // "WAL_USDC", + // "USDC", + // tx.object(usdcMarginPoolCapID[env]) + // )(tx); + // dbClient.marginMaintainer.enableDeepbookPoolForLoan( + // "DEEP_USDC", + // "DEEP", + // tx.object(deepMarginPoolCapID[env]) + // )(tx); + // dbClient.marginMaintainer.enableDeepbookPoolForLoan( + // "SUI_USDC", + // "SUI", + // tx.object(suiMarginPoolCapID[env]) + // )(tx); + // dbClient.marginMaintainer.enableDeepbookPoolForLoan( + // "WAL_USDC", + // "WAL", + // tx.object(walMarginPoolCapID[env]) + // )(tx); + + let res = await prepareMultisigTx(tx, env, adminCapOwner[env]); + + console.dir(res, { depth: null }); +})(); diff --git a/scripts/transactions/marginSetup.ts b/scripts/transactions/marginSetup.ts new file mode 100644 index 000000000..ee65e65d7 --- /dev/null +++ b/scripts/transactions/marginSetup.ts @@ -0,0 +1,69 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +import { namedPackagesPlugin, Transaction } from "@mysten/sui/transactions"; +import { prepareMultisigTx } from "../utils/utils"; + +export type Network = "mainnet" | "testnet" | "devnet" | "localnet"; + +const mainnetPlugin = namedPackagesPlugin({ + url: "https://mainnet.mvr.mystenlabs.com", +}); + +(async () => { + // Update constant for env + const env = "mainnet"; + const transaction = new Transaction(); + transaction.addSerializationPlugin(mainnetPlugin); + + const appCap = + "0x9e120e97c91434c8102024edc0a64c2d18ab702333da947791707dee6a45da2c"; // @deepbook/margin-trading appCap + + const repository = "https://github.com/MystenLabs/deepbookv3"; + + const data = { + packageInfo: "", // Fill in package info + sha: "margin-v1.0.0", + version: "1", + path: "packages/deepbook_margin", + }; + + const git = transaction.moveCall({ + target: `@mvr/metadata::git::new`, + arguments: [ + transaction.pure.string(repository), + transaction.pure.string(data.path), + transaction.pure.string(data.sha), + ], + }); + + transaction.moveCall({ + target: `@mvr/metadata::package_info::set_git_versioning`, + arguments: [ + transaction.object(data.packageInfo), + transaction.pure.u64(data.version), + git, + ], + }); + + // Link margin to correct packageInfo + // Important to check these two + transaction.moveCall({ + target: `@mvr/core::move_registry::assign_package`, + arguments: [ + transaction.object( + `0x0e5d473a055b6b7d014af557a13ad9075157fdc19b6d51562a18511afd397727`, + ), + transaction.object(appCap), + transaction.object(data.packageInfo), + ], + }); + + let res = await prepareMultisigTx( + transaction, + env, + "0xd0ec0b201de6b4e7f425918bbd7151c37fc1b06c59b3961a2a00db74f6ea865e", + ); // multisig address + + console.dir(res, { depth: null }); +})(); diff --git a/scripts/transactions/mvrFix.ts b/scripts/transactions/mvrFix.ts new file mode 100644 index 000000000..9f891f109 --- /dev/null +++ b/scripts/transactions/mvrFix.ts @@ -0,0 +1,257 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +import { namedPackagesPlugin, Transaction } from "@mysten/sui/transactions"; +import { prepareMultisigTx } from "../utils/utils"; + +export type Network = "mainnet" | "testnet" | "devnet" | "localnet"; + +const mainnetPlugin = namedPackagesPlugin({ + url: "https://mainnet.mvr.mystenlabs.com", +}); +(async () => { + const env = "mainnet"; + const transaction = new Transaction(); + transaction.addSerializationPlugin(mainnetPlugin); + + // appcap holding address + const holdingAddress = + "0x10a1fc2b9170c6bac858fdafc7d3cb1f4ea659fed748d18eff98d08debf82042"; + + // const MVRAppCaps = { + // core: "0xf30a07fc1fadc8bd33ed4a9af5129967008201387b979a9899e52fbd852b29a9", + // payments: + // "0xcb44143e2921ed0fb82529ba58f5284ec77da63a8640e57c7fa8c12e87fa8baf", + // subnames: + // "0x969978eba35e57ad66856f137448da065bc27962a1bc4a6dd8b6cc229c899d5a", + // coupons: + // "0x4f3fa0d4da16578b8261175131bc7a24dcefe3ec83b45690e29cbc9bb3edc4de", + // discounts: + // "0x327702a5751c9582b152db81073e56c9201fad51ecbaf8bb522ae8df49f8dfd1", + // tempSubnameProxy: + // "0x3b2582036fe9aa17c059e7b3993b8dc97ae57d2ac9e1fe603884060c98385fb2", + // denylist: + // "0x8816fd949b3191040855a77a834d98aa822eb63bd2e63de2aaa0064586200882", + // }; + + // for (const appCapObjectId of Object.values(MVRAppCaps)) { + // transaction.moveCall({ + // target: `@mvr/core::move_registry::set_metadata`, + // arguments: [ + // transaction.object( + // "0x0e5d473a055b6b7d014af557a13ad9075157fdc19b6d51562a18511afd397727" // Move registry + // ), + // transaction.object(appCapObjectId), + // transaction.pure.string("icon_url"), // key + // transaction.pure.string("https://docs.suins.io/logo.svg"), // value + // ], + // }); + // } + + // const kioskAppCap = + // "0x476cbd1df24cf590d675ddde59de4ec535f8aff9eea22fd83fed57001cfc9426"; + + // transaction.moveCall({ + // target: `@mvr/core::move_registry::set_metadata`, + // arguments: [ + // transaction.object( + // "0x0e5d473a055b6b7d014af557a13ad9075157fdc19b6d51562a18511afd397727" // Move registry + // ), + // transaction.object(kioskAppCap), + // transaction.pure.string("icon_url"), // key + // transaction.pure.string("https://svg-host.vercel.app/mystenlogo.svg"), + // ], + // }); + + const repository = "https://github.com/MystenLabs/suins-contracts"; + const latestSha = "releases/core/4"; + + const data = { + // core: { + // packageInfo: + // "0xf709e4075c19d9ab1ba5acb17dfbf08ddc1e328ab20eaa879454bf5f6b98758e", + // sha: latestSha, + // version: "4", + // path: "packages/suins", + // }, + // payments: { + // packageInfo: + // "0xa46d971d0e9298488605e1850d64fa067db9d66570dda8dad37bbf61ab2cca21", + // sha: latestSha, + // version: "1", + // path: "packages/payments", + // }, + // subnames: { + // packageInfo: + // "0x9470cf5deaf2e22232244da9beeabb7b82d4a9f7b9b0784017af75c7641950ee", + // sha: latestSha, + // version: "1", + // path: "packages/subdomains", + // }, + // coupons: { + // packageInfo: + // "0xf7f29dce2246e6c79c8edd4094dc3039de478187b1b13e871a6a1a87775fe939", + // sha: latestSha, + // version: "2", + // path: "packages/coupons", + // }, + // discounts: { + // packageInfo: + // "0xcb8d0cefcda3949b3ff83c0014cb50ca2a7c7b2074a5a7c1f2fce68cb9ad7dd6", + // sha: latestSha, + // version: "1", + // path: "packages/discounts", + // }, + tempSubnameProxy: { + packageInfo: + "0x9accbc6d7c86abf91dcbe247fd44c6eb006d8f1864ff93b90faaeb09114d3b6f", + sha: latestSha, + version: "1", + path: "packages/temp_subdomain_proxy", + }, + // denylist: { + // packageInfo: + // "0x5007c0681ff36e9efcb5d655af758c5eeb4825b39ef4ec2ccacd195f4f65d4f5", + // sha: latestSha, + // version: "1", + // path: "packages/denylist", + // }, + }; + + for (const [name, { packageInfo, sha, version, path }] of Object.entries( + data + )) { + transaction.moveCall({ + target: `@mvr/metadata::package_info::unset_git_versioning`, + arguments: [ + transaction.object(packageInfo), + transaction.pure.u64(version), + ], + }); + + const git = transaction.moveCall({ + target: `@mvr/metadata::git::new`, + arguments: [ + transaction.pure.string(repository), + transaction.pure.string(path), + transaction.pure.string(sha), + ], + }); + + transaction.moveCall({ + target: `@mvr/metadata::package_info::set_git_versioning`, + arguments: [ + transaction.object(packageInfo), + transaction.pure.u64(version), + git, + ], + }); + } + + // transaction.moveCall({ + // target: `@mvr/core::move_registry::set_metadata`, + // arguments: [ + // transaction.object( + // "0x0e5d473a055b6b7d014af557a13ad9075157fdc19b6d51562a18511afd397727" // Move registry + // ), + // transaction.object(MVRAppCaps.denylist), + // transaction.pure.string("description"), // key + // transaction.pure.string( + // "The SuiNS denylist package. Used to manage a list of disallowed names including banned names." + // ), // value + // ], + // }); + + // transaction.moveCall({ + // target: `@mvr/core::move_registry::set_metadata`, + // arguments: [ + // transaction.object( + // "0x0e5d473a055b6b7d014af557a13ad9075157fdc19b6d51562a18511afd397727" // Move registry + // ), + // transaction.object(MVRAppCaps.denylist), + // transaction.pure.string("documentation_url"), // key + // transaction.pure.string("https://docs.suins.io/"), // value + // ], + // }); + + // transaction.moveCall({ + // target: `@mvr/core::move_registry::set_metadata`, + // arguments: [ + // transaction.object( + // "0x0e5d473a055b6b7d014af557a13ad9075157fdc19b6d51562a18511afd397727" // Move registry + // ), + // transaction.object(MVRAppCaps.denylist), + // transaction.pure.string("homepage_url"), // key + // transaction.pure.string("https://suins.io/"), // value + // ], + // }); + + // transaction.moveCall({ + // target: "@mvr/metadata::package_info::set_metadata", + // arguments: [ + // transaction.object(data.denylist.packageInfo), + // transaction.pure.string("default"), + // transaction.pure.string("@suins/denylist"), + // ], + // }); + + // transaction.moveCall({ + // target: "@mvr/metadata::package_info::unset_metadata", + // arguments: [ + // transaction.object(data.tempSubnameProxy.packageInfo), + // transaction.pure.string("default"), + // ], + // }); + + // transaction.moveCall({ + // target: "@mvr/metadata::package_info::set_metadata", + // arguments: [ + // transaction.object(data.tempSubnameProxy.packageInfo), + // transaction.pure.string("default"), + // transaction.pure.string("@suins/temp-subnames-proxy"), + // ], + // }); + + // const appInfo = transaction.moveCall({ + // target: `@mvr/core::app_info::new`, + // arguments: [ + // transaction.pure.option( + // "address", + // "0xb82af529b54f90474e523467123c7e255903d0713ec8b7f0125794f94742c7bc" // PackageInfo object on testnet + // ), + // transaction.pure.option( + // "address", + // "0xa86c05fbc6371788eb31260dc5085f4bfeab8b95c95d9092c9eb86e63fae3d49" // V1 of the denylist package on testnet + // ), + // transaction.pure.option("address", null), + // ], + // }); + + // transaction.moveCall({ + // target: `@mvr/core::move_registry::set_network`, + // arguments: [ + // // the registry obj: Can also be resolved as `registry-obj@mvr` from mainnet SuiNS. + // transaction.object( + // "0x0e5d473a055b6b7d014af557a13ad9075157fdc19b6d51562a18511afd397727" + // ), + // transaction.object(MVRAppCaps.denylist), + // transaction.pure.string("4c78adac"), // testnet + // appInfo, + // ], + // }); + + // transaction.moveCall({ + // target: `@mvr/core::move_registry::assign_package`, + // arguments: [ + // transaction.object( + // `0x0e5d473a055b6b7d014af557a13ad9075157fdc19b6d51562a18511afd397727` + // ), + // transaction.object(MVRAppCaps.denylist), + // transaction.object(data.denylist.packageInfo), + // ], + // }); + + let res = await prepareMultisigTx(transaction, env, holdingAddress); // Owner of all MVR caps + + console.dir(res, { depth: null }); +})(); diff --git a/scripts/transactions/mvrPackageMetadata.ts b/scripts/transactions/mvrPackageMetadata.ts new file mode 100644 index 000000000..9ff89f24c --- /dev/null +++ b/scripts/transactions/mvrPackageMetadata.ts @@ -0,0 +1,83 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +import { namedPackagesPlugin, Transaction } from "@mysten/sui/transactions"; +import { prepareMultisigTx } from "../utils/utils"; + +export type Network = "mainnet" | "testnet" | "devnet" | "localnet"; + +const mainnetPlugin = namedPackagesPlugin({ + url: "https://mainnet.mvr.mystenlabs.com", +}); +(async () => { + const env = "mainnet"; + const transaction = new Transaction(); + transaction.addSerializationPlugin(mainnetPlugin); + + // appcap holding address + const holdingAddress = + "0x9a8859bbe68679bcc6dfd06ede1cce7309d59ef21bb0caf2e4c901320489a466"; + + const data = { + core: { + appCap: + "0x673bac45d749730e71c3ad2395c2942f7dd61167308752b564963228b147edc0", + description: + "The foundational component of the Move Registry (MVR). It provides essential on-chain functionality for application registration and resolution in the Sui ecosystem.", + documentation_url: "https://docs.suins.io/move-registry", + }, + "subnames-proxy": { + appCap: + "0xa24ad6dee0fa4b4a59839a78b638e3157638ac9774b6734af0250b372bf10881", + description: "Enables registering applications using SuiNS Subnames.", + documentation_url: "https://docs.suins.io/move-registry", + }, + metadata: { + appCap: + "0x8e5af7f91bcdbcb637eb6774fbb4b23022db864d125f7e74ab17f64646ac73da", + description: + "Defines PackageInfo objects, which contain metadata associated with registered Move packages. These objects track upgrade caps, package addresses, Git versioning metadata, and on-chain display configuration.", + documentation_url: "https://docs.suins.io/move-registry", + }, + "public-names": { + appCap: + "0x4e9264ba30222c1701457ed3d4745c74fd9d736c6609558aafd46ec734e60d78", + description: + "This package provides an open interface for creating and managing public names. Public names allow anyone to register apps under the namespace. The core use case for this is the global @pkg name supported on MVR.", + documentation_url: "https://docs.suins.io/move-registry", + }, + }; + + for (const [ + name, + { appCap, description, documentation_url }, + ] of Object.entries(data)) { + transaction.moveCall({ + target: "@mvr/core::move_registry::set_metadata", + arguments: [ + transaction.object( + "0x0e5d473a055b6b7d014af557a13ad9075157fdc19b6d51562a18511afd397727" // Move registry + ), + transaction.object(appCap), + transaction.pure.string("description"), // key + transaction.pure.string(description), // value + ], + }); + + transaction.moveCall({ + target: "@mvr/core::move_registry::set_metadata", + arguments: [ + transaction.object( + "0x0e5d473a055b6b7d014af557a13ad9075157fdc19b6d51562a18511afd397727" // Move registry + ), + transaction.object(appCap), + transaction.pure.string("documentation_url"), // key + transaction.pure.string(documentation_url), // value + ], + }); + } + + let res = await prepareMultisigTx(transaction, env, holdingAddress); // Owner of appcap for MVR + + console.dir(res, { depth: null }); +})(); diff --git a/scripts/transactions/mvrPackageReverseResolution.ts b/scripts/transactions/mvrPackageReverseResolution.ts new file mode 100644 index 000000000..5891d1702 --- /dev/null +++ b/scripts/transactions/mvrPackageReverseResolution.ts @@ -0,0 +1,107 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +import { namedPackagesPlugin, Transaction } from "@mysten/sui/transactions"; +import { prepareMultisigTx } from "../utils/utils"; + +export type Network = "mainnet" | "testnet" | "devnet" | "localnet"; + +const mainnetPlugin = namedPackagesPlugin({ + url: "https://mainnet.mvr.mystenlabs.com", +}); +(async () => { + const env = "mainnet"; + const transaction = new Transaction(); + transaction.addSerializationPlugin(mainnetPlugin); + + // appcap holding address + const holdingAddress = + "0x9a8859bbe68679bcc6dfd06ede1cce7309d59ef21bb0caf2e4c901320489a466"; + + // const MVRAppCaps = { + // core: "0x673bac45d749730e71c3ad2395c2942f7dd61167308752b564963228b147edc0", + // "subnames-proxy": + // "0xa24ad6dee0fa4b4a59839a78b638e3157638ac9774b6734af0250b372bf10881", + // metadata: + // "0x8e5af7f91bcdbcb637eb6774fbb4b23022db864d125f7e74ab17f64646ac73da", + // "public-names": + // "0x4e9264ba30222c1701457ed3d4745c74fd9d736c6609558aafd46ec734e60d78", + // }; + + const latestSha = "releases/metadata/2"; + const repository = "https://github.com/mystenlabs/mvr"; + + const data = { + // core: { + // packageInfo: + // "0xb68f1155b210ef649fa86c5a1b85d419b1593e08e2ee58d400d1090d36c93543", + // sha: latestSha, + // version: "3", + // path: "packages/mvr", + // }, + // "subnames-proxy": { + // packageInfo: + // "0x04de61f83f793aa89349263e04af8e186cffbbb4f4582422afd054a8bfb2c706", + // sha: latestSha, + // version: "1", + // path: "packages/proxy", + // }, + metadata: { + packageInfo: + "0x7ffeae2cd612960c7f208c68da064aa462e2fbb23fcf64faf2af9c2f67e7d4ca", + sha: latestSha, + version: "2", + path: "packages/package_info", + }, + // "public-names": { + // packageInfo: + // "0xe91836471642e44ba0c52b1f5223fcfa74686272192390295f7c8cbb2f44b51c", + // sha: latestSha, + // version: "1", + // path: "packages/public_names", + // }, + }; + + for (const [name, { packageInfo, sha, version, path }] of Object.entries( + data + )) { + // transaction.moveCall({ + // target: "@mvr/metadata::package_info::set_metadata", + // arguments: [ + // transaction.object(packageInfo), + // transaction.pure.string("default"), + // transaction.pure.string(`@mvr/${name}`), + // ], + // }); + + // transaction.moveCall({ + // target: `@mvr/metadata::package_info::unset_git_versioning`, + // arguments: [ + // transaction.object(packageInfo), + // transaction.pure.u64(version), + // ], + // }); + + const git = transaction.moveCall({ + target: `@mvr/metadata::git::new`, + arguments: [ + transaction.pure.string(repository), + transaction.pure.string(path), + transaction.pure.string(sha), + ], + }); + + transaction.moveCall({ + target: `@mvr/metadata::package_info::set_git_versioning`, + arguments: [ + transaction.object(packageInfo), + transaction.pure.u64(version), + git, + ], + }); + } + + let res = await prepareMultisigTx(transaction, env, holdingAddress); // Owner of appcap for MVR + + console.dir(res, { depth: null }); +})(); diff --git a/scripts/transactions/mvrPrep.ts b/scripts/transactions/mvrPrep.ts index e4274b815..7799d901d 100644 --- a/scripts/transactions/mvrPrep.ts +++ b/scripts/transactions/mvrPrep.ts @@ -1,41 +1,83 @@ // Copyright (c) Mysten Labs, Inc. // SPDX-License-Identifier: Apache-2.0 -import { newTransaction } from "./transaction"; import { prepareMultisigTx } from "../utils/utils"; +import { namedPackagesPlugin, Transaction } from "@mysten/sui/transactions"; export type Network = "mainnet" | "testnet" | "devnet" | "localnet"; +const mainnetPlugin = namedPackagesPlugin({ + url: "https://mainnet.mvr.mystenlabs.com", +}); + (async () => { // Update constant for env const env = "mainnet"; - const transaction = newTransaction(); + const transaction = new Transaction(); + transaction.addSerializationPlugin(mainnetPlugin); + + // appcap holding address + const holdingAddress = + "0x10a1fc2b9170c6bac858fdafc7d3cb1f4ea659fed748d18eff98d08debf82042"; + const appCapObjectId = + "0x5c05d47053b0b3126dc99ee97264bf0d8b52e5789ca33917b88d83eb63f0e434"; + + // const appCap = transaction.moveCall({ + // target: `@mvr/core::move_registry::register`, + // arguments: [ + // // the registry obj: Can also be resolved as `registry-obj@mvr` from mainnet SuiNS. + // transaction.object( + // "0x0e5d473a055b6b7d014af557a13ad9075157fdc19b6d51562a18511afd397727" + // ), + // transaction.object( + // "0x9dc2cd7decc92ec8a66ba32167fb7ec279b30bc36c3216096035db7d750aa89f" + // ), // mysten domain ID + // transaction.pure.string("nautilus"), // name + // transaction.object.clock(), + // ], + // }); - const appCap = transaction.moveCall({ - target: `@mvr/core::move_registry::register`, + // const appCap2 = transaction.moveCall({ + // target: `@mvr/core::move_registry::register`, + // arguments: [ + // // the registry obj: Can also be resolved as `registry-obj@mvr` from mainnet SuiNS. + // transaction.object( + // "0x0e5d473a055b6b7d014af557a13ad9075157fdc19b6d51562a18511afd397727" + // ), + // transaction.object( + // "0x9dc2cd7decc92ec8a66ba32167fb7ec279b30bc36c3216096035db7d750aa89f" + // ), // mysten domain ID + // transaction.pure.string("seal"), // name + // transaction.object.clock(), + // ], + // }); + + // transaction.transferObjects([appCap, appCap2], holdingAddress); + + transaction.moveCall({ + target: `@mvr/core::move_registry::unset_metadata`, arguments: [ - // the registry obj: Can also be resolved as `registry-obj@mvr` from mainnet SuiNS. transaction.object( - "0x0e5d473a055b6b7d014af557a13ad9075157fdc19b6d51562a18511afd397727" + "0x0e5d473a055b6b7d014af557a13ad9075157fdc19b6d51562a18511afd397727" // Move registry ), - transaction.object( - "0xd0815f9867a0a02690a9fe3b5be9a044bb381f96c660ba6aa28dfaaaeb76af76" - ), // deepbook domain ID - transaction.pure.string("core"), // name - transaction.object.clock(), + transaction.object(appCapObjectId), + transaction.pure.string("documentation_url"), // key ], }); - transaction.transferObjects( - [appCap], - "0xd0ec0b201de6b4e7f425918bbd7151c37fc1b06c59b3961a2a00db74f6ea865e" - ); // This is the deepbook adminCap owner + transaction.moveCall({ + target: `@mvr/core::move_registry::set_metadata`, + arguments: [ + transaction.object( + "0x0e5d473a055b6b7d014af557a13ad9075157fdc19b6d51562a18511afd397727" // Move registry + ), + transaction.object(appCapObjectId), + transaction.pure.string("documentation_url"), // key + transaction.pure.string("https://seal-docs.wal.app/UsingSeal/"), // value + ], + }); - let res = await prepareMultisigTx( - transaction, - env, - "0xb5b39d11ddbd0abb0166cd369c155409a2cca9868659bda6d9ce3804c510b949" - ); // Owner of @deepbook + let res = await prepareMultisigTx(transaction, env, holdingAddress); console.dir(res, { depth: null }); })(); diff --git a/scripts/transactions/mvrPrepKiosk.ts b/scripts/transactions/mvrPrepKiosk.ts index 97780c91a..79c93c469 100644 --- a/scripts/transactions/mvrPrepKiosk.ts +++ b/scripts/transactions/mvrPrepKiosk.ts @@ -1,15 +1,20 @@ // Copyright (c) Mysten Labs, Inc. // SPDX-License-Identifier: Apache-2.0 -import { newTransaction } from "./transaction"; +import { namedPackagesPlugin, Transaction } from "@mysten/sui/transactions"; import { prepareMultisigTx } from "../utils/utils"; export type Network = "mainnet" | "testnet" | "devnet" | "localnet"; +const mainnetPlugin = namedPackagesPlugin({ + url: "https://mainnet.mvr.mystenlabs.com", +}); + (async () => { // Update constant for env const env = "mainnet"; - const transaction = newTransaction(); + const transaction = new Transaction(); + transaction.addSerializationPlugin(mainnetPlugin); const appCap = transaction.moveCall({ target: `@mvr/core::move_registry::register`, diff --git a/scripts/transactions/nautilus-setup.ts b/scripts/transactions/nautilus-setup.ts new file mode 100644 index 000000000..4d1fc7bb6 --- /dev/null +++ b/scripts/transactions/nautilus-setup.ts @@ -0,0 +1,258 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +import { namedPackagesPlugin, Transaction } from "@mysten/sui/transactions"; +import { prepareMultisigTx } from "../utils/utils"; + +export type Network = "mainnet" | "testnet" | "devnet" | "localnet"; + +const mainnetPlugin = namedPackagesPlugin({ + url: "https://mainnet.mvr.mystenlabs.com", +}); +(async () => { + const env = "mainnet"; + const transaction = new Transaction(); + transaction.addSerializationPlugin(mainnetPlugin); + + // appcap holding address + const holdingAddress = + "0x10a1fc2b9170c6bac858fdafc7d3cb1f4ea659fed748d18eff98d08debf82042"; + + const MVRAppCaps = { + nautilus: + "0x8a159edc9ee8d809a980b3eb66510b6a6b608d8a79abb0576916430e4a7389b8", + seal: "0x5c05d47053b0b3126dc99ee97264bf0d8b52e5789ca33917b88d83eb63f0e434", + sites: "0x31bcfbe17957dae74f7a5dc7439f8e954870646317054ff880084c80d64f2390", + }; + + transaction.moveCall({ + target: `@mvr/core::move_registry::set_metadata`, + arguments: [ + transaction.object( + "0x0e5d473a055b6b7d014af557a13ad9075157fdc19b6d51562a18511afd397727" // Move registry + ), + transaction.object(MVRAppCaps.nautilus), + transaction.pure.string("icon_url"), // key + transaction.pure.string("https://svg-host.vercel.app/mystenlogo.svg"), + ], + }); + + transaction.moveCall({ + target: `@mvr/core::move_registry::set_metadata`, + arguments: [ + transaction.object( + "0x0e5d473a055b6b7d014af557a13ad9075157fdc19b6d51562a18511afd397727" // Move registry + ), + transaction.object(MVRAppCaps.seal), + transaction.pure.string("icon_url"), // key + transaction.pure.string( + "https://drive.google.com/file/d/1MwZmWh2GiEzxfw5zIeoNrst3v7fbnuO5/view?usp=sharing" + ), + ], + }); + + const data = { + nautilus: { + repository: "https://github.com/MystenLabs/nautilus", + packageInfo: + "0x427579e9f0f3200cc51a634b33088895879f38783655297f4ed2442351cd53d0", + sha: "d919402aadf15e21b3cf31515b3a46d1ca6965e4", + version: "1", + path: "move/enclave", + }, + // seal: { + // repository: "https://github.com/MystenLabs/seal", + // packageInfo: + // "0x78969731e1f29f996e24261a13dd78c6a0932bc099aa02e27965bbfb1a643d86", + // sha: "9aafac05433aa86c7ee1d6d971f253cc4f6e8edb", // TODO: update sha + // version: "1", + // path: "", + // }, + // deepbook: { + // repository: "https://github.com/MystenLabs/deepbookv3", + // packageInfo: "", // TODO + // sha: "v3.0.0", + // version: "3", + // path: "packages/deepbook", + // }, + }; + + for (const [ + name, + { repository, packageInfo, sha, version, path }, + ] of Object.entries(data)) { + const git = transaction.moveCall({ + target: `@mvr/metadata::git::new`, + arguments: [ + transaction.pure.string(repository), + transaction.pure.string(path), + transaction.pure.string(sha), + ], + }); + + transaction.moveCall({ + target: `@mvr/metadata::package_info::set_git_versioning`, + arguments: [ + transaction.object(packageInfo), + transaction.pure.u64(version), + git, + ], + }); + } + + transaction.moveCall({ + target: `@mvr/core::move_registry::set_metadata`, + arguments: [ + transaction.object( + "0x0e5d473a055b6b7d014af557a13ad9075157fdc19b6d51562a18511afd397727" // Move registry + ), + transaction.object(MVRAppCaps.nautilus), + transaction.pure.string("description"), // key + transaction.pure.string( + "Nautilus is a framework for secure and verifiable off chain computation on Sui." + ), // value + ], + }); + + transaction.moveCall({ + target: `@mvr/core::move_registry::set_metadata`, + arguments: [ + transaction.object( + "0x0e5d473a055b6b7d014af557a13ad9075157fdc19b6d51562a18511afd397727" // Move registry + ), + transaction.object(MVRAppCaps.nautilus), + transaction.pure.string("documentation_url"), // key + transaction.pure.string( + "https://docs.sui.io/concepts/cryptography/nautilus" + ), // value + ], + }); + + transaction.moveCall({ + target: `@mvr/core::move_registry::set_metadata`, + arguments: [ + transaction.object( + "0x0e5d473a055b6b7d014af557a13ad9075157fdc19b6d51562a18511afd397727" // Move registry + ), + transaction.object(MVRAppCaps.nautilus), + transaction.pure.string("homepage_url"), // key + transaction.pure.string("https://sui.io/nautilus"), // value + ], + }); + + transaction.moveCall({ + target: "@mvr/metadata::package_info::set_metadata", + arguments: [ + transaction.object(data.nautilus.packageInfo), + transaction.pure.string("default"), + transaction.pure.string("@mysten/nautilus"), + ], + }); + + transaction.moveCall({ + target: `@mvr/core::move_registry::set_metadata`, + arguments: [ + transaction.object( + "0x0e5d473a055b6b7d014af557a13ad9075157fdc19b6d51562a18511afd397727" // Move registry + ), + transaction.object(MVRAppCaps.seal), + transaction.pure.string("description"), // key + transaction.pure.string( + "Seal is a decentralized secrets management (DSM) service that relies on access control policies defined and validated on Sui." + ), // value + ], + }); + + transaction.moveCall({ + target: `@mvr/core::move_registry::set_metadata`, + arguments: [ + transaction.object( + "0x0e5d473a055b6b7d014af557a13ad9075157fdc19b6d51562a18511afd397727" // Move registry + ), + transaction.object(MVRAppCaps.seal), + transaction.pure.string("documentation_url"), // key + transaction.pure.string( + "https://github.com/MystenLabs/seal/blob/main/UsingSeal.md" + ), // value + ], + }); + + transaction.moveCall({ + target: `@mvr/core::move_registry::set_metadata`, + arguments: [ + transaction.object( + "0x0e5d473a055b6b7d014af557a13ad9075157fdc19b6d51562a18511afd397727" // Move registry + ), + transaction.object(MVRAppCaps.seal), + transaction.pure.string("homepage_url"), // key + transaction.pure.string("https://seal.mystenlabs.com"), // value + ], + }); + + const appInfo = transaction.moveCall({ + target: `@mvr/core::app_info::new`, + arguments: [ + transaction.pure.option( + "address", + "0xfe94e6c85433a1a933760d7111bf7e26dfef12403f7c8f90f2bd7f184715abeb" // PackageInfo object on testnet + ), + transaction.pure.option( + "address", + "0x0f16e84a49dec8425e6900cfdfe3730aaf1e8bc608d9f0500fcfa2c2267abfb4" // V1 of the seal package on testnet + ), + transaction.pure.option("address", null), + ], + }); + + transaction.moveCall({ + target: `@mvr/core::move_registry::set_network`, + arguments: [ + // the registry obj: Can also be resolved as `registry-obj@mvr` from mainnet SuiNS. + transaction.object( + "0x0e5d473a055b6b7d014af557a13ad9075157fdc19b6d51562a18511afd397727" + ), + transaction.object(MVRAppCaps.seal), + transaction.pure.string("4c78adac"), // testnet + appInfo, + ], + }); + + transaction.moveCall({ + target: `@mvr/core::move_registry::assign_package`, + arguments: [ + transaction.object( + `0x0e5d473a055b6b7d014af557a13ad9075157fdc19b6d51562a18511afd397727` + ), + transaction.object(MVRAppCaps.nautilus), + transaction.object(data.nautilus.packageInfo), + ], + }); + + // Sites changes + transaction.moveCall({ + target: `@mvr/core::move_registry::unset_metadata`, + arguments: [ + transaction.object( + "0x0e5d473a055b6b7d014af557a13ad9075157fdc19b6d51562a18511afd397727" // Move registry + ), + transaction.object(MVRAppCaps.sites), + transaction.pure.string("homepage_url"), // key + ], + }); + + transaction.moveCall({ + target: `@mvr/core::move_registry::set_metadata`, + arguments: [ + transaction.object( + "0x0e5d473a055b6b7d014af557a13ad9075157fdc19b6d51562a18511afd397727" // Move registry + ), + transaction.object(MVRAppCaps.sites), + transaction.pure.string("homepage_url"), // key + transaction.pure.string("https://wal.app/"), // value + ], + }); + + let res = await prepareMultisigTx(transaction, env, holdingAddress); // Owner of all MVR caps + + console.dir(res, { depth: null }); +})(); diff --git a/scripts/transactions/newDeepbookVersion.ts b/scripts/transactions/newDeepbookVersion.ts new file mode 100644 index 000000000..f5ea7bc86 --- /dev/null +++ b/scripts/transactions/newDeepbookVersion.ts @@ -0,0 +1,53 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +import { namedPackagesPlugin, Transaction } from "@mysten/sui/transactions"; +import { prepareMultisigTx } from "../utils/utils"; + +export type Network = "mainnet" | "testnet" | "devnet" | "localnet"; + +const mainnetPlugin = namedPackagesPlugin({ + url: "https://mainnet.mvr.mystenlabs.com", +}); +(async () => { + const env = "mainnet"; + const transaction = new Transaction(); + transaction.addSerializationPlugin(mainnetPlugin); + + // appcap holding address + const holdingAddress = + "0xd0ec0b201de6b4e7f425918bbd7151c37fc1b06c59b3961a2a00db74f6ea865e"; + + const deepbookPackageInfo = + "0x4874e126c490e495ff7490523841bdba57e2a01ed36db7610f07d417c8b5a988"; + + const data = { + repository: "https://github.com/MystenLabs/deepbookv3", + packageInfo: deepbookPackageInfo, + sha: "v6.0.0", + version: "6", + path: "packages/deepbook", + }; + + const git = transaction.moveCall({ + target: `@mvr/metadata::git::new`, + arguments: [ + transaction.pure.string(data.repository), + transaction.pure.string(data.path), + transaction.pure.string(data.sha), + ], + }); + + transaction.moveCall({ + target: `@mvr/metadata::package_info::set_git_versioning`, + arguments: [ + transaction.object(data.packageInfo), + transaction.pure.u64(data.version), + git, + ], + }); + + let res = await prepareMultisigTx(transaction, env, holdingAddress); + + console.dir(res, { depth: null }); +})(); diff --git a/scripts/transactions/packageInfoCreation.ts b/scripts/transactions/packageInfoCreation.ts index ad43e270a..8af2268c0 100644 --- a/scripts/transactions/packageInfoCreation.ts +++ b/scripts/transactions/packageInfoCreation.ts @@ -1,23 +1,32 @@ // Copyright (c) Mysten Labs, Inc. // SPDX-License-Identifier: Apache-2.0 -import { newTransaction } from "./transaction"; +import { namedPackagesPlugin, Transaction } from "@mysten/sui/transactions"; import { prepareMultisigTx } from "../utils/utils"; export type Network = "mainnet" | "testnet" | "devnet" | "localnet"; +const mainnetPlugin = namedPackagesPlugin({ + url: "https://mainnet.mvr.mystenlabs.com", +}); + (async () => { // Update constant for env const env = "mainnet"; - const transaction = newTransaction(); + const transaction = new Transaction(); + transaction.addSerializationPlugin(mainnetPlugin); + + // appcap holding address + const holdingAddress = + "0x10a1fc2b9170c6bac858fdafc7d3cb1f4ea659fed748d18eff98d08debf82042"; /// We pass in our UpgradeCap const packageInfo = transaction.moveCall({ target: `@mvr/metadata::package_info::new`, arguments: [ transaction.object( - "0xdadf253cea3b91010e64651b03da6d56166a4f44b43bdd4e185c277658634483" - ), // Deepbook UpgradeCap + "0x1cab3c76c48c023b60db0a56696d197569f006e406fb9627a8a8d1a119b1c23c" + ), // Walrus Sites Package UpgradeCap ], }); @@ -26,7 +35,7 @@ export type Network = "mainnet" | "testnet" | "devnet" | "localnet"; // that allows customizing the colors of your metadata object! const display = transaction.moveCall({ target: `@mvr/metadata::display::default`, - arguments: [transaction.pure.string("DeepbookV3")], + arguments: [transaction.pure.string("Walrus - Sites Metadata")], }); // Set that display object to our info object. @@ -40,17 +49,61 @@ export type Network = "mainnet" | "testnet" | "devnet" | "localnet"; target: `@mvr/metadata::package_info::transfer`, arguments: [ transaction.object(packageInfo), - transaction.pure.address( - "0xd0ec0b201de6b4e7f425918bbd7151c37fc1b06c59b3961a2a00db74f6ea865e" - ), + transaction.pure.address(holdingAddress), ], - }); // PackageInfo transferred to Admincap Owner + }); // PackageInfo transferred to MVR account + + // const MVRAppCaps = { + // core: "0xf30a07fc1fadc8bd33ed4a9af5129967008201387b979a9899e52fbd852b29a9", + // payments: + // "0xcb44143e2921ed0fb82529ba58f5284ec77da63a8640e57c7fa8c12e87fa8baf", + // subnames: + // "0x969978eba35e57ad66856f137448da065bc27962a1bc4a6dd8b6cc229c899d5a", + // coupons: + // "0x4f3fa0d4da16578b8261175131bc7a24dcefe3ec83b45690e29cbc9bb3edc4de", + // discounts: + // "0x327702a5751c9582b152db81073e56c9201fad51ecbaf8bb522ae8df49f8dfd1", + // tempSubnameProxy: + // "0x3b2582036fe9aa17c059e7b3993b8dc97ae57d2ac9e1fe603884060c98385fb2", + // }; + + // const packageInfos = { + // core: "0xf709e4075c19d9ab1ba5acb17dfbf08ddc1e328ab20eaa879454bf5f6b98758e", + // payments: + // "0xa46d971d0e9298488605e1850d64fa067db9d66570dda8dad37bbf61ab2cca21", + // subnames: + // "0x9470cf5deaf2e22232244da9beeabb7b82d4a9f7b9b0784017af75c7641950ee", + // coupons: + // "0xf7f29dce2246e6c79c8edd4094dc3039de478187b1b13e871a6a1a87775fe939", + // discounts: + // "0xcb8d0cefcda3949b3ff83c0014cb50ca2a7c7b2074a5a7c1f2fce68cb9ad7dd6", + // tempSubnameProxy: + // "0x9accbc6d7c86abf91dcbe247fd44c6eb006d8f1864ff93b90faaeb09114d3b6f", + // }; + + // // Transfer all app cap + package info objects + // const allAppCaps: string[] = []; + + // for (const value of Object.values(MVRAppCaps)) { + // allAppCaps.push(value); + // } + // transaction.transferObjects(allAppCaps, holdingAddress); + + // for (const packageInfoId of Object.values(packageInfos)) { + // transaction.moveCall({ + // target: `@mvr/metadata::package_info::transfer`, + // arguments: [ + // transaction.object(packageInfoId), + // transaction.pure.address(holdingAddress), + // ], + // }); // PackageInfo transferred to MVR account + // } let res = await prepareMultisigTx( transaction, env, - "0x37f187e1e54e9c9b8c78b6c46a7281f644ebc62e75493623edcaa6d1dfcf64d2" - ); // Owner of UpgradeCap + "0x23eb7ccbbb4a21afea8b1256475e255b3cd84083ca79fa1f1a9435ab93d2b71b" + ); // Owner of walrus sites UpgradeCap console.dir(res, { depth: null }); })(); diff --git a/scripts/transactions/packageInfoNautilus.ts b/scripts/transactions/packageInfoNautilus.ts new file mode 100644 index 000000000..9bae4647f --- /dev/null +++ b/scripts/transactions/packageInfoNautilus.ts @@ -0,0 +1,61 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +import { namedPackagesPlugin, Transaction } from "@mysten/sui/transactions"; +import { prepareMultisigTx } from "../utils/utils"; + +export type Network = "mainnet" | "testnet" | "devnet" | "localnet"; + +const mainnetPlugin = namedPackagesPlugin({ + url: "https://mainnet.mvr.mystenlabs.com", +}); + +(async () => { + const env = "mainnet"; + const transaction = new Transaction(); + transaction.addSerializationPlugin(mainnetPlugin); + + // appcap holding address + const holdingAddress = + "0x10a1fc2b9170c6bac858fdafc7d3cb1f4ea659fed748d18eff98d08debf82042"; + + const packageInfo = transaction.moveCall({ + target: `@mvr/metadata::package_info::new`, + arguments: [ + transaction.object( + "0xf8083707981031b003db9b0fcd074664efe366ba6926ce5859412495860cf9a9" + ), // Nautilus Package UpgradeCap + ], + }); + + // We also need to create the visual representation of our "info" object. + // You can also call `@mvr/metadata::display::new` instead, + // that allows customizing the colors of your metadata object! + const display = transaction.moveCall({ + target: `@mvr/metadata::display::default`, + arguments: [transaction.pure.string("Mysten - Nautilus Metadata")], + }); + + // Set that display object to our info object. + transaction.moveCall({ + target: `@mvr/metadata::package_info::set_display`, + arguments: [transaction.object(packageInfo), display], + }); + + // transfer the `PackageInfo` object to a safe address. + transaction.moveCall({ + target: `@mvr/metadata::package_info::transfer`, + arguments: [ + transaction.object(packageInfo), + transaction.pure.address(holdingAddress), + ], + }); // PackageInfo transferred to MVR account + + let res = await prepareMultisigTx( + transaction, + env, + "0xfa469d15a399f7a000214f4630712c6e6207430499278e1c2e19a63d5dd821e5" + ); // Owner of nautilus UpgradeCap + + console.dir(res, { depth: null }); +})(); diff --git a/scripts/transactions/pauseCap.ts b/scripts/transactions/pauseCap.ts new file mode 100644 index 000000000..3e45dabd1 --- /dev/null +++ b/scripts/transactions/pauseCap.ts @@ -0,0 +1,106 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 +import { execSync } from "child_process"; +import { readFileSync } from "fs"; +import { homedir } from "os"; +import path from "path"; +import { getFullnodeUrl, SuiClient } from "@mysten/sui/client"; +import { decodeSuiPrivateKey } from "@mysten/sui/cryptography"; +import { Ed25519Keypair } from "@mysten/sui/keypairs/ed25519"; +import { Secp256k1Keypair } from "@mysten/sui/keypairs/secp256k1"; +import { Secp256r1Keypair } from "@mysten/sui/keypairs/secp256r1"; +import { Transaction } from "@mysten/sui/transactions"; +import { fromBase64 } from "@mysten/sui/utils"; +import { namedPackagesPlugin } from "@mysten/sui/transactions"; +import { DeepBookClient } from "@mysten/deepbook-v3"; + +const SUI = process.env.SUI_BINARY ?? `sui`; + +export const getActiveAddress = () => { + return execSync(`${SUI} client active-address`, { encoding: "utf8" }).trim(); +}; + +export const getSigner = () => { + if (process.env.PRIVATE_KEY) { + console.log("Using supplied private key."); + const { schema, secretKey } = decodeSuiPrivateKey(process.env.PRIVATE_KEY); + + if (schema === "ED25519") return Ed25519Keypair.fromSecretKey(secretKey); + if (schema === "Secp256k1") + return Secp256k1Keypair.fromSecretKey(secretKey); + if (schema === "Secp256r1") + return Secp256r1Keypair.fromSecretKey(secretKey); + + throw new Error("Keypair not supported."); + } + + const sender = getActiveAddress(); + + const keystore = JSON.parse( + readFileSync( + path.join(homedir(), ".sui", "sui_config", "sui.keystore"), + "utf8" + ) + ); + + for (const priv of keystore) { + const raw = fromBase64(priv); + if (raw[0] !== 0) { + continue; + } + + const pair = Ed25519Keypair.fromSecretKey(raw.slice(1)); + if (pair.getPublicKey().toSuiAddress() === sender) { + return pair; + } + } + + throw new Error(`keypair not found for sender: ${sender}`); +}; + +export const signAndExecute = async (txb: Transaction, network: Network) => { + const client = getClient(network); + const signer = getSigner(); + + return client.signAndExecuteTransaction({ + transaction: txb, + signer, + options: { + showEffects: true, + showObjectChanges: true, + }, + }); +}; +export const getClient = (network: Network) => { + const url = process.env.RPC_URL || getFullnodeUrl(network); + return new SuiClient({ url }); +}; + +export type Network = "mainnet" | "testnet" | "devnet" | "localnet"; + +const mainnetPlugin = namedPackagesPlugin({ + url: "https://mainnet.mvr.mystenlabs.com", +}); + +(async () => { + // Update constant for env + const env = "mainnet"; + const version = 1; // Version to pause + const pauseCapID = ""; // Fill in the pause cap ID + const tx = new Transaction(); + tx.addSerializationPlugin(mainnetPlugin); + + const dbClient = new DeepBookClient({ + address: getActiveAddress(), + env: env, + client: new SuiClient({ + url: getFullnodeUrl(env), + }), + }); + + dbClient.marginAdmin.disableVersionPauseCap(version, pauseCapID)(tx); + + let res = await signAndExecute(tx, env); + + console.dir(res, { depth: null }); +})(); diff --git a/scripts/transactions/paymentSetup.ts b/scripts/transactions/paymentSetup.ts new file mode 100644 index 000000000..207c6f004 --- /dev/null +++ b/scripts/transactions/paymentSetup.ts @@ -0,0 +1,156 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +import { namedPackagesPlugin, Transaction } from "@mysten/sui/transactions"; +import { prepareMultisigTx } from "../utils/utils"; + +export type Network = "mainnet" | "testnet" | "devnet" | "localnet"; + +const mainnetPlugin = namedPackagesPlugin({ + url: "https://mainnet.mvr.mystenlabs.com", +}); + +(async () => { + // Update constant for env + const env = "mainnet"; + const transaction = new Transaction(); + transaction.addSerializationPlugin(mainnetPlugin); + + // appcap holding address + const holdingAddress = + "0x10a1fc2b9170c6bac858fdafc7d3cb1f4ea659fed748d18eff98d08debf82042"; + + const mainnetPackageInfo = + "0xa7fe44196e9d3c130643250d8742b6b886c0d297fa2febf11858fe4f3787eb3a"; + + const testnetPackageInfo = + "0x5ddfe36e164a18927ca50bb9f9cf797f2f557462a93f028bdca9f47acf12f69c"; + + const testnetPackageId = + "0x7e069abe383e80d32f2aec17b3793da82aabc8c2edf84abbf68dd7b719e71497"; + + const appCap = transaction.moveCall({ + target: `@mvr/core::move_registry::register`, + arguments: [ + // the registry obj: Can also be resolved as `registry-obj@mvr` from mainnet SuiNS. + transaction.object( + "0x0e5d473a055b6b7d014af557a13ad9075157fdc19b6d51562a18511afd397727" + ), + transaction.object( + "0x9dc2cd7decc92ec8a66ba32167fb7ec279b30bc36c3216096035db7d750aa89f" + ), // mysten domain ID + transaction.pure.string("payment-kit"), // name + transaction.object.clock(), + ], + }); + + // Set all metadata for payment-kit + transaction.moveCall({ + target: `@mvr/core::move_registry::set_metadata`, + arguments: [ + transaction.object( + "0x0e5d473a055b6b7d014af557a13ad9075157fdc19b6d51562a18511afd397727" // Move registry + ), + appCap, + transaction.pure.string("description"), // key + transaction.pure.string( + "A robust, open-source payment processing toolkit for the Sui blockchain that provides secure payment verification, receipt management, and duplicate prevention." + ), // value + ], + }); + + transaction.moveCall({ + target: `@mvr/core::move_registry::set_metadata`, + arguments: [ + transaction.object( + "0x0e5d473a055b6b7d014af557a13ad9075157fdc19b6d51562a18511afd397727" // Move registry + ), + appCap, + transaction.pure.string("documentation_url"), // key + transaction.pure.string("https://github.com/MystenLabs/sui-payment-kit"), // value + ], + }); + + transaction.moveCall({ + target: `@mvr/core::move_registry::set_metadata`, + arguments: [ + transaction.object( + "0x0e5d473a055b6b7d014af557a13ad9075157fdc19b6d51562a18511afd397727" // Move registry + ), + appCap, + transaction.pure.string("homepage_url"), // key + transaction.pure.string("https://github.com/MystenLabs/sui-payment-kit"), // value + ], + }); + + transaction.moveCall({ + target: `@mvr/core::move_registry::set_metadata`, + arguments: [ + transaction.object( + "0x0e5d473a055b6b7d014af557a13ad9075157fdc19b6d51562a18511afd397727" // Move registry + ), + appCap, + transaction.pure.string("icon_url"), // key + transaction.pure.string("https://svg-host.vercel.app/mystenlogo.svg"), // value + ], + }); + + // Set testnet information for payment-kit + const appInfo = transaction.moveCall({ + target: `@mvr/core::app_info::new`, + arguments: [ + transaction.pure.option( + "address", + testnetPackageInfo // PackageInfo object on testnet + ), + transaction.pure.option( + "address", + testnetPackageId // V1 of the payment-kit package on testnet + ), + transaction.pure.option("address", null), + ], + }); + + transaction.moveCall({ + target: `@mvr/core::move_registry::set_network`, + arguments: [ + // the registry obj: Can also be resolved as `registry-obj@mvr` from mainnet SuiNS. + transaction.object( + "0x0e5d473a055b6b7d014af557a13ad9075157fdc19b6d51562a18511afd397727" + ), + appCap, + transaction.pure.string("4c78adac"), // testnet + appInfo, + ], + }); + + // Link payment-kit to correct packageInfo + // Important to check these two + transaction.moveCall({ + target: `@mvr/core::move_registry::assign_package`, + arguments: [ + transaction.object( + `0x0e5d473a055b6b7d014af557a13ad9075157fdc19b6d51562a18511afd397727` + ), + appCap, + transaction.object(mainnetPackageInfo), + ], + }); + + transaction.transferObjects([appCap], holdingAddress); + transaction.moveCall({ + target: `@mvr/metadata::package_info::transfer`, + arguments: [ + transaction.object(mainnetPackageInfo), + transaction.pure.address(holdingAddress), + ], + }); + + let res = await prepareMultisigTx( + transaction, + env, + "0xa81a2328b7bbf70ab196d6aca400b5b0721dec7615bf272d95e0b0df04517e72" + ); // multisig address + + console.dir(res, { depth: null }); +})(); diff --git a/scripts/transactions/setupDenylist.ts b/scripts/transactions/setupDenylist.ts new file mode 100644 index 000000000..3e4697bfd --- /dev/null +++ b/scripts/transactions/setupDenylist.ts @@ -0,0 +1,197 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +import { namedPackagesPlugin, Transaction } from "@mysten/sui/transactions"; +import { prepareMultisigTx } from "../utils/utils"; + +export type Network = "mainnet" | "testnet" | "devnet" | "localnet"; + +const mainnetPlugin = namedPackagesPlugin({ + url: "https://mainnet.mvr.mystenlabs.com", +}); +(async () => { + const env = "mainnet"; + const transaction = new Transaction(); + transaction.addSerializationPlugin(mainnetPlugin); + + // appcap holding address + const holdingAddress = + "0x10a1fc2b9170c6bac858fdafc7d3cb1f4ea659fed748d18eff98d08debf82042"; + + // const MVRAppCaps = { + // denylist: + // "0x8816fd949b3191040855a77a834d98aa822eb63bd2e63de2aaa0064586200882", + // // "deny-list": + // // "0xbfa432f8d0424e61b175137135e2f5ee533609ee9039534f9109784be9aa7f7e", + // }; + + // // Set metadata for deny-list appcap + // transaction.moveCall({ + // target: `@mvr/core::move_registry::set_metadata`, + // arguments: [ + // transaction.object( + // "0x0e5d473a055b6b7d014af557a13ad9075157fdc19b6d51562a18511afd397727" // Move registry + // ), + // transaction.object(MVRAppCaps["deny-list"]), + // transaction.pure.string("icon_url"), // key + // transaction.pure.string("https://docs.suins.io/logo.svg"), // value + // ], + // }); + + const repository = "https://github.com/MystenLabs/suins-contracts"; + const latestSha = "releases/core/4"; + + const data = { + denylist: { + packageInfo: + "0x5007c0681ff36e9efcb5d655af758c5eeb4825b39ef4ec2ccacd195f4f65d4f5", + sha: latestSha, + version: "1", + path: "packages/redirect-denylist", + }, + // "deny-list": { + // packageInfo: + // "0x8db617063bf735f1c265800f0f48dcb7a98f542553a89b8f8ada11bd37729134", + // sha: latestSha, + // version: "1", + // path: "packages/denylist", + // }, + }; + + // Set git versioning for deny-list, unset for denylist + for (const [name, { packageInfo, sha, version, path }] of Object.entries( + data + )) { + // transaction.moveCall({ + // target: `@mvr/metadata::package_info::unset_git_versioning`, + // arguments: [ + // transaction.object(packageInfo), + // transaction.pure.u64(version), + // ], + // }); + + const git = transaction.moveCall({ + target: `@mvr/metadata::git::new`, + arguments: [ + transaction.pure.string(repository), + transaction.pure.string(path), + transaction.pure.string(sha), + ], + }); + + transaction.moveCall({ + target: `@mvr/metadata::package_info::set_git_versioning`, + arguments: [ + transaction.object(packageInfo), + transaction.pure.u64(version), + git, + ], + }); + } + + // // Set all metadata for deny-list + // transaction.moveCall({ + // target: `@mvr/core::move_registry::set_metadata`, + // arguments: [ + // transaction.object( + // "0x0e5d473a055b6b7d014af557a13ad9075157fdc19b6d51562a18511afd397727" // Move registry + // ), + // transaction.object(MVRAppCaps["deny-list"]), + // transaction.pure.string("description"), // key + // transaction.pure.string( + // "The SuiNS denylist package. Used to manage a list of disallowed names including banned names." + // ), // value + // ], + // }); + + // transaction.moveCall({ + // target: `@mvr/core::move_registry::set_metadata`, + // arguments: [ + // transaction.object( + // "0x0e5d473a055b6b7d014af557a13ad9075157fdc19b6d51562a18511afd397727" // Move registry + // ), + // transaction.object(MVRAppCaps["deny-list"]), + // transaction.pure.string("documentation_url"), // key + // transaction.pure.string("https://docs.suins.io/"), // value + // ], + // }); + + // transaction.moveCall({ + // target: `@mvr/core::move_registry::set_metadata`, + // arguments: [ + // transaction.object( + // "0x0e5d473a055b6b7d014af557a13ad9075157fdc19b6d51562a18511afd397727" // Move registry + // ), + // transaction.object(MVRAppCaps["deny-list"]), + // transaction.pure.string("homepage_url"), // key + // transaction.pure.string("https://suins.io/"), // value + // ], + // }); + + // Unset reverse resolution for denylist + // transaction.moveCall({ + // target: `@mvr/metadata::package_info::unset_metadata`, + // arguments: [ + // transaction.object(data.denylist.packageInfo), + // transaction.pure.string("default"), // key + // ], + // }); + + // // Set reverse resolution for deny-list + // transaction.moveCall({ + // target: "@mvr/metadata::package_info::set_metadata", + // arguments: [ + // transaction.object(data["deny-list"].packageInfo), + // transaction.pure.string("default"), + // transaction.pure.string("@suins/deny-list"), + // ], + // }); + + // Set testnet information for deny-list + // const appInfo = transaction.moveCall({ + // target: `@mvr/core::app_info::new`, + // arguments: [ + // transaction.pure.option( + // "address", + // "0xb82af529b54f90474e523467123c7e255903d0713ec8b7f0125794f94742c7bc" // PackageInfo object on testnet + // ), + // transaction.pure.option( + // "address", + // "0xa86c05fbc6371788eb31260dc5085f4bfeab8b95c95d9092c9eb86e63fae3d49" // V1 of the denylist package on testnet + // ), + // transaction.pure.option("address", null), + // ], + // }); + + // transaction.moveCall({ + // target: `@mvr/core::move_registry::set_network`, + // arguments: [ + // // the registry obj: Can also be resolved as `registry-obj@mvr` from mainnet SuiNS. + // transaction.object( + // "0x0e5d473a055b6b7d014af557a13ad9075157fdc19b6d51562a18511afd397727" + // ), + // transaction.object(MVRAppCaps["deny-list"]), + // transaction.pure.string("4c78adac"), // testnet + // appInfo, + // ], + // }); + + // Link deny-list to correct packageInfo + // Important to check these two + // 0xbfa432f8d0424e61b175137135e2f5ee533609ee9039534f9109784be9aa7f7e + // 0x8db617063bf735f1c265800f0f48dcb7a98f542553a89b8f8ada11bd37729134 + // transaction.moveCall({ + // target: `@mvr/core::move_registry::assign_package`, + // arguments: [ + // transaction.object( + // `0x0e5d473a055b6b7d014af557a13ad9075157fdc19b6d51562a18511afd397727` + // ), + // transaction.object(MVRAppCaps["deny-list"]), + // transaction.object(data["deny-list"].packageInfo), + // ], + // }); + + let res = await prepareMultisigTx(transaction, env, holdingAddress); // Owner of all MVR caps + + console.dir(res, { depth: null }); +})(); diff --git a/scripts/transactions/supplyToMarginPool.ts b/scripts/transactions/supplyToMarginPool.ts new file mode 100644 index 000000000..c6dce44d7 --- /dev/null +++ b/scripts/transactions/supplyToMarginPool.ts @@ -0,0 +1,32 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +import { Transaction } from "@mysten/sui/transactions"; +import { prepareMultisigTx } from "../utils/utils"; +import { getFullnodeUrl, SuiClient } from "@mysten/sui/client"; +import { DeepBookClient } from "@mysten/deepbook-v3"; +import { adminCapOwner, supplierCapID } from "../config/constants"; + +(async () => { + const env = "mainnet"; + const tx = new Transaction(); + const vaultId = + "0xae8e060630107720560d49e99f352b41a9f1696675021f087b69b57d35d814b6"; + + const dbClient = new DeepBookClient({ + address: adminCapOwner[env], + env, + client: new SuiClient({ + url: getFullnodeUrl(env), + }), + }); + + // Amounts to deposit + const usdcAmount = 90_000; + + dbClient.marginPool.supplyToMarginPool("USDC", tx.object(supplierCapID[env]), usdcAmount)(tx); + + let res = await prepareMultisigTx(tx, env, adminCapOwner[env]); + + console.dir(res, { depth: null }); +})(); diff --git a/scripts/transactions/transaction.ts b/scripts/transactions/transaction.ts deleted file mode 100644 index 36586533a..000000000 --- a/scripts/transactions/transaction.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { Transaction } from "@mysten/sui/transactions"; -import { namedPackagesPlugin } from "@mysten/sui/transactions"; -import { SuiGraphQLClient } from "@mysten/sui/graphql"; - -Transaction.registerGlobalSerializationPlugin( - "namedPackagesPlugin", - namedPackagesPlugin({ - suiGraphQLClient: new SuiGraphQLClient({ - url: `https://mvr-rpc.sui-mainnet.mystenlabs.com/graphql`, - }), - }) -); - -export const newTransaction = () => { - return new Transaction(); -}; diff --git a/scripts/transactions/transferFunds.ts b/scripts/transactions/transferFunds.ts new file mode 100644 index 000000000..d5c3c84ed --- /dev/null +++ b/scripts/transactions/transferFunds.ts @@ -0,0 +1,44 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +import { Transaction, coinWithBalance } from "@mysten/sui/transactions"; +import { prepareMultisigTx } from "../utils/utils"; + +export type Network = "mainnet" | "testnet" | "devnet" | "localnet"; + +(async () => { + const env = "mainnet"; + const transaction = new Transaction(); + const config = { + SUI: { + type: "0x0000000000000000000000000000000000000000000000000000000000000002::sui::SUI", + scalar: 1_000_000_000, + }, + DEEP: { + type: "0xdeeb7a4662eec9f2f3def03fb937a663dddaa2e215b8078a284d026b7946c270::deep::DEEP", + scalar: 1_000_000, + }, + }; + + // admin address + const adminAddress = + "0xd0ec0b201de6b4e7f425918bbd7151c37fc1b06c59b3961a2a00db74f6ea865e"; + + // Update receiving address as needed + const recevingAddress = + "0x0f97e5774fa2d0ad786ee0a562c4f65762e141397e469a736703351df85383cc"; + const coinType = "SUI"; // "SUI" or "DEEP" + const amount = 1_000; + + const totalAmount = amount * config[coinType].scalar; + const coin = coinWithBalance({ + balance: totalAmount, + type: config[coinType].type, + useGasCoin: false, + })(transaction); + + transaction.transferObjects([coin], recevingAddress); + let res = await prepareMultisigTx(transaction, env, adminAddress); + + console.dir(res, { depth: null }); +})(); diff --git a/scripts/transactions/transferMvrObjectsKiosk.ts b/scripts/transactions/transferMvrObjectsKiosk.ts new file mode 100644 index 000000000..03cdda0a5 --- /dev/null +++ b/scripts/transactions/transferMvrObjectsKiosk.ts @@ -0,0 +1,53 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +import { namedPackagesPlugin, Transaction } from "@mysten/sui/transactions"; +import { prepareMultisigTx } from "../utils/utils"; + +export type Network = "mainnet" | "testnet" | "devnet" | "localnet"; + +const mainnetPlugin = namedPackagesPlugin({ + url: "https://mainnet.mvr.mystenlabs.com", +}); + +(async () => { + // Update constant for env + const env = "mainnet"; + const transaction = new Transaction(); + transaction.addSerializationPlugin(mainnetPlugin); + + const holdingAddress = + "0x10a1fc2b9170c6bac858fdafc7d3cb1f4ea659fed748d18eff98d08debf82042"; + + const MVRAppCaps = { + kiosk: "0x476cbd1df24cf590d675ddde59de4ec535f8aff9eea22fd83fed57001cfc9426", + }; + + const packageInfos = { + kiosk: "0xa364dd21f5eb43fdd4e502be52f450c09529dfc94dea12412a6d587f17ec7f24", + }; + + // Transfer all app cap + package info objects + const allObjects: string[] = []; + + for (const value of Object.values(MVRAppCaps)) { + allObjects.push(value); + } + + transaction.transferObjects(allObjects, holdingAddress); + transaction.moveCall({ + target: `@mvr/metadata::package_info::transfer`, + arguments: [ + transaction.object(packageInfos.kiosk), + transaction.pure.address(holdingAddress), + ], + }); + + let res = await prepareMultisigTx( + transaction, + env, + "0xcb6a5c15cba57e5033cf3c2b8dc56eafa8a0564a1810f1f2f1341a663b575d54" + ); // Suins account + + console.dir(res, { depth: null }); +})(); diff --git a/scripts/transactions/updateInterestRates.ts b/scripts/transactions/updateInterestRates.ts new file mode 100644 index 000000000..5b802b39b --- /dev/null +++ b/scripts/transactions/updateInterestRates.ts @@ -0,0 +1,142 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 +import { Transaction } from "@mysten/sui/transactions"; +import { prepareMultisigTx } from "../utils/utils"; +import { + adminCapOwner, + adminCapID, + marginAdminCapID, + marginMaintainerCapID, + suiMarginPoolCapID, + usdcMarginPoolCapID, + deepMarginPoolCapID, + walMarginPoolCapID, +} from "../config/constants"; +import { DeepBookClient } from "@mysten/deepbook-v3"; +import { getFullnodeUrl, SuiClient } from "@mysten/sui/client"; + +(async () => { + const env = "mainnet"; + + const dbClient = new DeepBookClient({ + address: "0x0", + env: env, + client: new SuiClient({ + url: getFullnodeUrl(env), + }), + adminCap: adminCapID[env], + marginAdminCap: marginAdminCapID[env], + marginMaintainerCap: marginMaintainerCapID[env], + }); + + const tx = new Transaction(); + + // dbClient.marginMaintainer.updateInterestParams( + // "USDC", + // tx.object(usdcMarginPoolCapID[env]), + // { + // baseRate: 0, + // baseSlope: 0.15, + // optimalUtilization: 0.8, + // excessSlope: 5, + // }, + // )(tx); + + dbClient.marginMaintainer.updateMarginPoolConfig( + "USDC", + tx.object(usdcMarginPoolCapID[env]), + { + supplyCap: 2_000_000, + maxUtilizationRate: 0.9, + referralSpread: 0.2, + minBorrow: 0.1, + rateLimitCapacity: 400_000, + rateLimitRefillRatePerMs: 0.018518, // 400_000 / 21_600_000 (6 hours) + rateLimitEnabled: true, + }, + )(tx); + + // dbClient.marginMaintainer.updateInterestParams( + // "SUI", + // tx.object(suiMarginPoolCapID[env]), + // { + // baseRate: 0.03, + // baseSlope: 0.2, + // optimalUtilization: 0.8, + // excessSlope: 5, + // }, + // )(tx); + + dbClient.marginMaintainer.updateMarginPoolConfig( + "SUI", + tx.object(suiMarginPoolCapID[env]), + { + supplyCap: 1_000_000, + maxUtilizationRate: 0.9, + referralSpread: 0.2, + minBorrow: 0.1, + rateLimitCapacity: 200_000, + rateLimitRefillRatePerMs: 0.00925926, // 200_000 / 21_600_000 (6 hours) + rateLimitEnabled: true, + }, + )(tx); + + // dbClient.marginMaintainer.updateInterestParams( + // "DEEP", + // tx.object(deepMarginPoolCapID[env]), + // { + // baseRate: 0.05, + // baseSlope: 0.25, + // optimalUtilization: 0.8, + // excessSlope: 5, + // }, + // )(tx); + + // dbClient.marginMaintainer.updateMarginPoolConfig( + // "DEEP", + // tx.object(deepMarginPoolCapID[env]), + // { + // supplyCap: 20_000_000, + // maxUtilizationRate: 0.9, + // referralSpread: 0.2, + // minBorrow: 0.1, + // rateLimitCapacity: 4_000_000, + // rateLimitRefillRatePerMs: 0.185185, // 4_000_000 / 21_600_000 (6 hours) + // rateLimitEnabled: true, + // }, + // )(tx); + + // dbClient.marginMaintainer.updateInterestParams( + // "WAL", + // tx.object(walMarginPoolCapID[env]), + // { + // baseRate: 0.05, + // baseSlope: 0.25, + // optimalUtilization: 0.8, + // excessSlope: 5, + // }, + // )(tx); + + // dbClient.marginMaintainer.updateMarginPoolConfig( + // "WAL", + // tx.object(walMarginPoolCapID[env]), + // { + // supplyCap: 7_000_000, + // maxUtilizationRate: 0.9, + // referralSpread: 0.2, + // minBorrow: 0.1, + // rateLimitCapacity: 1_400_000, + // rateLimitRefillRatePerMs: 0.064814815, // 1_400_000 / 21_600_000 (6 hours) + // rateLimitEnabled: true, + // }, + // )(tx); + const maintainerCap = dbClient.marginAdmin.mintMaintainerCap()(tx); + tx.transferObjects( + [maintainerCap], + "0x6b9f717104d04a5e53cbcd95213c6fbe3616809d396ec4c31076f7dddc497362", + ); + + let res = await prepareMultisigTx(tx, env, adminCapOwner[env]); + + console.dir(res, { depth: null }); +})(); diff --git a/scripts/transactions/updatePoolMinLotSize.ts b/scripts/transactions/updatePoolMinLotSize.ts new file mode 100644 index 000000000..68dcdc01e --- /dev/null +++ b/scripts/transactions/updatePoolMinLotSize.ts @@ -0,0 +1,30 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 +import { Transaction } from "@mysten/sui/transactions"; +import { prepareMultisigTx } from "../utils/utils"; +import { adminCapOwner, adminCapID } from "../config/constants"; +import { DeepBookClient } from "@mysten/deepbook-v3"; +import { getFullnodeUrl, SuiClient } from "@mysten/sui/client"; + +(async () => { + // Update constant for env + const env = "mainnet"; + + const dbClient = new DeepBookClient({ + address: "0x0", + env: env, + client: new SuiClient({ + url: getFullnodeUrl(env), + }), + adminCap: adminCapID[env], + }); + + const tx = new Transaction(); + + dbClient.deepBookAdmin.adjustMinLotSize("DEEP_USDC", 1, 10)(tx); + dbClient.deepBookAdmin.adjustMinLotSize("DEEP_SUI", 1, 10)(tx); + + let res = await prepareMultisigTx(tx, env, adminCapOwner[env]); + + console.dir(res, { depth: null }); +})(); diff --git a/scripts/transactions/updatePoolTickSize.ts b/scripts/transactions/updatePoolTickSize.ts new file mode 100644 index 000000000..d180175ea --- /dev/null +++ b/scripts/transactions/updatePoolTickSize.ts @@ -0,0 +1,29 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 +import { Transaction } from "@mysten/sui/transactions"; +import { prepareMultisigTx } from "../utils/utils"; +import { adminCapOwner, adminCapID } from "../config/constants"; +import { DeepBookClient } from "@mysten/deepbook-v3"; +import { getFullnodeUrl, SuiClient } from "@mysten/sui/client"; + +(async () => { + // Update constant for env + const env = "mainnet"; + + const dbClient = new DeepBookClient({ + address: "0x0", + env: env, + client: new SuiClient({ + url: getFullnodeUrl(env), + }), + adminCap: adminCapID[env], + }); + + const tx = new Transaction(); + + dbClient.deepBookAdmin.adjustTickSize("SUI_USDC", 0.0001)(tx); + + let res = await prepareMultisigTx(tx, env, adminCapOwner[env]); + + console.dir(res, { depth: null }); +})(); diff --git a/scripts/transactions/walrusSitesSetup.ts b/scripts/transactions/walrusSitesSetup.ts new file mode 100644 index 000000000..03d1b56fa --- /dev/null +++ b/scripts/transactions/walrusSitesSetup.ts @@ -0,0 +1,213 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +import { namedPackagesPlugin, Transaction } from "@mysten/sui/transactions"; +import { prepareMultisigTx } from "../utils/utils"; + +export type Network = "mainnet" | "testnet" | "devnet" | "localnet"; + +const mainnetPlugin = namedPackagesPlugin({ + url: "https://mainnet.mvr.mystenlabs.com", +}); +(async () => { + const env = "mainnet"; + const transaction = new Transaction(); + transaction.addSerializationPlugin(mainnetPlugin); + + // appcap holding address + const holdingAddress = + "0x10a1fc2b9170c6bac858fdafc7d3cb1f4ea659fed748d18eff98d08debf82042"; + + const MVRAppCaps = { + // oldsite: + // "0xac82d5c6d183087007b1101ff71c7982c6365c2cd1a36fc9a1b3ea8fe966f545", + site: "0x31bcfbe17957dae74f7a5dc7439f8e954870646317054ff880084c80d64f2390", + }; + + // transaction.moveCall({ + // target: `@mvr/core::move_registry::set_metadata`, + // arguments: [ + // transaction.object( + // "0x0e5d473a055b6b7d014af557a13ad9075157fdc19b6d51562a18511afd397727" // Move registry + // ), + // transaction.object(MVRAppCaps.site), + // transaction.pure.string("icon_url"), // key + // transaction.pure.string( + // "https://cdn.prod.website-files.com/67bf314c789da9e4d7c30c50/67e506a7980c586cba295748_67c20e44c97b05da454f35f3_walrus-site.svg" + // ), + // ], + // }); + + const repository = "https://github.com/MystenLabs/walrus-sites"; + const latestSha = "walrus_sites_v0.1.0_1750151671_main_ci"; + + const data = { + site: { + packageInfo: + "0x78969731e1f29f996e24261a13dd78c6a0932bc099aa02e27965bbfb1a643d86", + sha: latestSha, + version: "1", + path: "move/walrus_site", + }, + }; + + for (const [name, { packageInfo, sha, version, path }] of Object.entries( + data + )) { + transaction.moveCall({ + target: `@mvr/metadata::package_info::unset_git_versioning`, + arguments: [ + transaction.object(packageInfo), + transaction.pure.u64(version), + ], + }); + const git = transaction.moveCall({ + target: `@mvr/metadata::git::new`, + arguments: [ + transaction.pure.string(repository), + transaction.pure.string(path), + transaction.pure.string(sha), + ], + }); + + transaction.moveCall({ + target: `@mvr/metadata::package_info::set_git_versioning`, + arguments: [ + transaction.object(packageInfo), + transaction.pure.u64(version), + git, + ], + }); + } + + // transaction.moveCall({ + // target: `@mvr/core::move_registry::set_metadata`, + // arguments: [ + // transaction.object( + // "0x0e5d473a055b6b7d014af557a13ad9075157fdc19b6d51562a18511afd397727" // Move registry + // ), + // transaction.object(MVRAppCaps.site), + // transaction.pure.string("description"), // key + // transaction.pure.string( + // "The Walrus sites package. Walrus Sites are websites built using decentralized tech such as Walrus, a decentralized storage network, and the Sui blockchain." + // ), // value + // ], + // }); + + // transaction.moveCall({ + // target: `@mvr/core::move_registry::set_metadata`, + // arguments: [ + // transaction.object( + // "0x0e5d473a055b6b7d014af557a13ad9075157fdc19b6d51562a18511afd397727" // Move registry + // ), + // transaction.object(MVRAppCaps.site), + // transaction.pure.string("documentation_url"), // key + // transaction.pure.string("https://docs.wal.app/walrus-sites/intro.html"), // value + // ], + // }); + + // transaction.moveCall({ + // target: `@mvr/core::move_registry::set_metadata`, + // arguments: [ + // transaction.object( + // "0x0e5d473a055b6b7d014af557a13ad9075157fdc19b6d51562a18511afd397727" // Move registry + // ), + // transaction.object(MVRAppCaps.site), + // transaction.pure.string("homepage_url"), // key + // transaction.pure.string("https://walrus.site/"), // value + // ], + // }); + + // transaction.moveCall({ + // target: `@mvr/core::move_registry::unset_metadata`, + // arguments: [ + // transaction.object( + // "0x0e5d473a055b6b7d014af557a13ad9075157fdc19b6d51562a18511afd397727" // Move registry + // ), + // transaction.object(MVRAppCaps.oldsite), + // transaction.pure.string("description"), // key + // ], + // }); + + // transaction.moveCall({ + // target: `@mvr/core::move_registry::unset_metadata`, + // arguments: [ + // transaction.object( + // "0x0e5d473a055b6b7d014af557a13ad9075157fdc19b6d51562a18511afd397727" // Move registry + // ), + // transaction.object(MVRAppCaps.oldsite), + // transaction.pure.string("documentation_url"), // key + // ], + // }); + + // transaction.moveCall({ + // target: `@mvr/core::move_registry::unset_metadata`, + // arguments: [ + // transaction.object( + // "0x0e5d473a055b6b7d014af557a13ad9075157fdc19b6d51562a18511afd397727" // Move registry + // ), + // transaction.object(MVRAppCaps.oldsite), + // transaction.pure.string("homepage_url"), // key + // ], + // }); + + // transaction.moveCall({ + // target: "@mvr/metadata::package_info::unset_metadata", + // arguments: [ + // transaction.object(data.oldsite.packageInfo), + // transaction.pure.string("default"), + // ], + // }); + + // transaction.moveCall({ + // target: "@mvr/metadata::package_info::set_metadata", + // arguments: [ + // transaction.object(data.site.packageInfo), + // transaction.pure.string("default"), + // transaction.pure.string("@walrus/sites"), + // ], + // }); + + const appInfo = transaction.moveCall({ + target: `@mvr/core::app_info::new`, + arguments: [ + transaction.pure.option( + "address", + "0x97be021af63c8b6c5e668f4d398b3a7457ff4c87cf9c347a1da3618e6a0223e4" // PackageInfo object on testnet + ), + transaction.pure.option( + "address", + "0xf99aee9f21493e1590e7e5a9aea6f343a1f381031a04a732724871fc294be799" // V1 of the walrus sites package on testnet + ), + transaction.pure.option("address", null), + ], + }); + + transaction.moveCall({ + target: `@mvr/core::move_registry::set_network`, + arguments: [ + // the registry obj: Can also be resolved as `registry-obj@mvr` from mainnet SuiNS. + transaction.object( + "0x0e5d473a055b6b7d014af557a13ad9075157fdc19b6d51562a18511afd397727" + ), + transaction.object(MVRAppCaps.site), + transaction.pure.string("4c78adac"), // testnet + appInfo, + ], + }); + + // transaction.moveCall({ + // target: `@mvr/core::move_registry::assign_package`, + // arguments: [ + // transaction.object( + // `0x0e5d473a055b6b7d014af557a13ad9075157fdc19b6d51562a18511afd397727` + // ), + // transaction.object(MVRAppCaps.site), + // transaction.object(data.site.packageInfo), + // ], + // }); + + let res = await prepareMultisigTx(transaction, env, holdingAddress); // Owner of all MVR caps + + console.dir(res, { depth: null }); +})();