mirror of
https://github.com/logos-blockchain/lssa.git
synced 2026-02-19 12:53:31 +00:00
Merge branch 'arjentix/fix-sequencer-msg-id' into Pravdyvy/indexer-state-management
This commit is contained in:
commit
5d228c6d80
32
.github/workflows/ci.yml
vendored
32
.github/workflows/ci.yml
vendored
@ -100,7 +100,7 @@ jobs:
|
|||||||
RISC0_SKIP_BUILD: "1"
|
RISC0_SKIP_BUILD: "1"
|
||||||
run: cargo clippy -p "*programs" -- -D warnings
|
run: cargo clippy -p "*programs" -- -D warnings
|
||||||
|
|
||||||
tests:
|
unit-tests:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
timeout-minutes: 60
|
timeout-minutes: 60
|
||||||
steps:
|
steps:
|
||||||
@ -126,7 +126,35 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
RISC0_DEV_MODE: "1"
|
RISC0_DEV_MODE: "1"
|
||||||
RUST_LOG: "info"
|
RUST_LOG: "info"
|
||||||
run: cargo nextest run --no-fail-fast -- --skip tps_test
|
run: cargo nextest run --workspace --exclude integration_tests
|
||||||
|
|
||||||
|
integration-tests:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 60
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v5
|
||||||
|
with:
|
||||||
|
ref: ${{ github.head_ref }}
|
||||||
|
|
||||||
|
- uses: ./.github/actions/install-system-deps
|
||||||
|
|
||||||
|
- uses: ./.github/actions/install-risc0
|
||||||
|
|
||||||
|
- uses: ./.github/actions/install-logos-blockchain-circuits
|
||||||
|
with:
|
||||||
|
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Install active toolchain
|
||||||
|
run: rustup install
|
||||||
|
|
||||||
|
- name: Install nextest
|
||||||
|
run: cargo install --locked cargo-nextest
|
||||||
|
|
||||||
|
- name: Run tests
|
||||||
|
env:
|
||||||
|
RISC0_DEV_MODE: "1"
|
||||||
|
RUST_LOG: "info"
|
||||||
|
run: cargo nextest run -p integration_tests -- --skip tps_test
|
||||||
|
|
||||||
valid-proof-test:
|
valid-proof-test:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|||||||
2
.gitignore
vendored
2
.gitignore
vendored
@ -9,3 +9,5 @@ rocksdb
|
|||||||
sequencer_runner/data/
|
sequencer_runner/data/
|
||||||
storage.json
|
storage.json
|
||||||
result
|
result
|
||||||
|
wallet-ffi/wallet_ffi.h
|
||||||
|
bedrock_signing_key
|
||||||
|
|||||||
332
Cargo.lock
generated
332
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@ -13,6 +13,10 @@ members = [
|
|||||||
"common",
|
"common",
|
||||||
"nssa",
|
"nssa",
|
||||||
"nssa/core",
|
"nssa/core",
|
||||||
|
"programs/amm/core",
|
||||||
|
"programs/amm",
|
||||||
|
"programs/token/core",
|
||||||
|
"programs/token",
|
||||||
"sequencer_core",
|
"sequencer_core",
|
||||||
"sequencer_rpc",
|
"sequencer_rpc",
|
||||||
"sequencer_runner",
|
"sequencer_runner",
|
||||||
@ -51,6 +55,8 @@ wallet = { path = "wallet" }
|
|||||||
wallet-ffi = { path = "wallet-ffi" }
|
wallet-ffi = { path = "wallet-ffi" }
|
||||||
token_core = { path = "programs/token/core" }
|
token_core = { path = "programs/token/core" }
|
||||||
token_program = { path = "programs/token" }
|
token_program = { path = "programs/token" }
|
||||||
|
amm_core = { path = "programs/amm/core" }
|
||||||
|
amm_program = { path = "programs/amm" }
|
||||||
test_program_methods = { path = "test_program_methods" }
|
test_program_methods = { path = "test_program_methods" }
|
||||||
bedrock_client = { path = "bedrock_client" }
|
bedrock_client = { path = "bedrock_client" }
|
||||||
|
|
||||||
|
|||||||
639
README.md
639
README.md
@ -42,10 +42,16 @@ To our knowledge, this design is unique to LEZ. Other privacy-focused programmab
|
|||||||
- The execution is handled on-chain without ZKPs involved.
|
- The execution is handled on-chain without ZKPs involved.
|
||||||
- Alice's and Charlie's accounts are modified according to the transaction.
|
- Alice's and Charlie's accounts are modified according to the transaction.
|
||||||
|
|
||||||
#### Key points:
|
4. Transfer from public to public (public execution)
|
||||||
- The same token program is used in all executions.
|
- Alice submits an on-chain transaction to run `Transfer`, sending to Charlie’s public account.
|
||||||
- The difference lies in execution mode: public executions update visible accounts on-chain, while private executions rely on ZKPs.
|
- Execution is handled fully on-chain without ZKPs.
|
||||||
- Validators only need to verify proofs for privacy-preserving transactions, keeping processing efficient.
|
- Alice’s and Charlie’s public balances are updated.
|
||||||
|
|
||||||
|
|
||||||
|
### Key points:
|
||||||
|
- The same token program is used in every execution.
|
||||||
|
- The only difference is execution mode: public execution updates visible state on-chain, while private execution relies on ZKPs.
|
||||||
|
- Validators verify proofs only for privacy-preserving transactions, keeping processing efficient.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@ -125,21 +131,26 @@ RUST_LOG=info RISC0_DEV_MODE=1 cargo run $(pwd)/configs/debug all
|
|||||||
|
|
||||||
# Run the sequencer and node
|
# Run the sequencer and node
|
||||||
|
|
||||||
|
|
||||||
|
## Running Manually
|
||||||
|
|
||||||
The sequencer and node can be run locally:
|
The sequencer and node can be run locally:
|
||||||
|
|
||||||
1. On one terminal go to the `logos-blockchain/logos-blockchain` repo and run a local logos blockchain node:
|
1. On one terminal go to the `logos-blockchain/logos-blockchain` repo and run a local logos blockchain node:
|
||||||
- `git checkout master; git pull`
|
- `git checkout master; git pull`
|
||||||
- `cargo clean`
|
- `cargo clean`
|
||||||
- `rm ~/.logos-blockchain-circuits`
|
- `rm -r ~/.logos-blockchain-circuits`
|
||||||
- `./scripts/setup-logos-blockchain-circuits.sh`
|
- `./scripts/setup-logos-blockchain-circuits.sh`
|
||||||
- `cargo build --all-features`
|
- `cargo build --all-features`
|
||||||
- `./target/debug/logos-blockchain-node nodes/node/config-one-node.yaml`
|
- `./target/debug/logos-blockchain-node --deployment nodes/node/standalone-deployment-config.yaml nodes/node/standalone-node-config.yaml`
|
||||||
|
|
||||||
2. On another terminal go to the `logos-blockchain/lssa` repo and run indexer service:
|
2. On another terminal go to the `logos-blockchain/lssa` repo and run indexer service:
|
||||||
- `git checkout schouhy/full-bedrock-integration`
|
- `RUST_LOG=info cargo run --release -p indexer_service indexer/service/configs/indexer_config.json`
|
||||||
- `RUST_LOG=info cargo run --release -p indexer_service $(pwd)/integration_tests/configs/indexer/indexer_config.json`
|
|
||||||
|
|
||||||
# Running with Docker
|
3. On another terminal go to the `logos-blockchain/lssa` repo and run the sequencer:
|
||||||
|
- `RUST_LOG=info RISC0_DEV_MODE=1 cargo run --release -p sequencer_runner sequencer_runner/configs/debug`
|
||||||
|
|
||||||
|
## Running with Docker
|
||||||
|
|
||||||
You can run the whole setup with Docker:
|
You can run the whole setup with Docker:
|
||||||
|
|
||||||
@ -147,617 +158,9 @@ You can run the whole setup with Docker:
|
|||||||
docker compose up
|
docker compose up
|
||||||
```
|
```
|
||||||
|
|
||||||
With that you can send transactions from local wallet to the Sequencer running inside Docker using `wallet/configs/debug` as well as exploring block by opening `http://localhost:8080`.
|
With that you can send transactions from local wallet to the Sequencer running inside Docker using `wallet/configs/debug` as well as exploring blocks by opening `http://localhost:8080`.
|
||||||
|
|
||||||
## Caution for local image builds
|
## Caution for local image builds
|
||||||
|
|
||||||
If you're going to build sequencer image locally you should better adjust default docker settings and set `defaultKeepStorage` at least `25GB` so that it can keep layers properly cached.
|
If you're going to build sequencer image locally you should better adjust default docker settings and set `defaultKeepStorage` at least `25GB` so that it can keep layers properly cached.
|
||||||
|
|
||||||
# Try the Wallet CLI
|
|
||||||
|
|
||||||
## Install
|
|
||||||
|
|
||||||
This repository includes a CLI for interacting with the Nescience sequencer. To install it, run the following command from the root of the repository:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cargo install --path wallet --force
|
|
||||||
```
|
|
||||||
|
|
||||||
Run `wallet help` to check everything went well.
|
|
||||||
|
|
||||||
Some completion scripts exists, see the [completions](./completions/README.md) folder.
|
|
||||||
|
|
||||||
## Tutorial
|
|
||||||
|
|
||||||
This tutorial walks you through creating accounts and executing NSSA programs in both public and private contexts.
|
|
||||||
|
|
||||||
> [!NOTE]
|
|
||||||
> The NSSA state is split into two separate but interconnected components: the public state and the private state.
|
|
||||||
> The public state is an on-chain, publicly visible record of accounts indexed by their Account IDs
|
|
||||||
> The private state mirrors this, but the actual account values are stored locally by each account owner. On-chain, only a hidden commitment to each private account state is recorded. This allows the chain to enforce freshness (i.e., prevent the reuse of stale private states) while preserving privacy and unlinkability across executions and private accounts.
|
|
||||||
>
|
|
||||||
> Every piece of state in NSSA is stored in an account (public or private). Accounts are either uninitialized or are owned by a program, and programs can only modify the accounts they own.
|
|
||||||
>
|
|
||||||
> In NSSA, accounts can only be modified through program execution. A program is the sole mechanism that can change an account’s value.
|
|
||||||
> Programs run publicly when all involved accounts are public, and privately when at least one private account participates.
|
|
||||||
|
|
||||||
### Health-check
|
|
||||||
|
|
||||||
Verify that the node is running and that the wallet can connect to it:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
wallet check-health
|
|
||||||
```
|
|
||||||
|
|
||||||
You should see `✅ All looks good!`.
|
|
||||||
|
|
||||||
### The commands
|
|
||||||
|
|
||||||
The wallet provides several commands to interact with the node and query state. To see the full list, run `wallet help`:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
Commands:
|
|
||||||
auth-transfer Authenticated transfer subcommand
|
|
||||||
chain-info Generic chain info subcommand
|
|
||||||
account Account view and sync subcommand
|
|
||||||
pinata Pinata program interaction subcommand
|
|
||||||
token Token program interaction subcommand
|
|
||||||
amm AMM program interaction subcommand
|
|
||||||
check-health Check the wallet can connect to the node and builtin local programs match the remote versions
|
|
||||||
```
|
|
||||||
|
|
||||||
### Accounts
|
|
||||||
|
|
||||||
> [!NOTE]
|
|
||||||
> Accounts are the basic unit of state in NSSA. They essentially hold native tokens and arbitrary data managed by some program.
|
|
||||||
|
|
||||||
The CLI provides commands to manage accounts. Run `wallet account` to see the options available:
|
|
||||||
```bash
|
|
||||||
Commands:
|
|
||||||
get Get account data
|
|
||||||
new Produce new public or private account
|
|
||||||
sync-private Sync private accounts
|
|
||||||
help Print this message or the help of the given subcommand(s)
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Create a new public account
|
|
||||||
|
|
||||||
You can create both public and private accounts through the CLI. For example:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
wallet account new public
|
|
||||||
|
|
||||||
# Output:
|
|
||||||
Generated new account with account_id Public/9ypzv6GGr3fwsgxY7EZezg5rz6zj52DPCkmf1vVujEiJ
|
|
||||||
```
|
|
||||||
|
|
||||||
This id is required when executing any program that interacts with the account.
|
|
||||||
|
|
||||||
> [!NOTE]
|
|
||||||
> Public accounts live on-chain and are identified by a 32-byte Account ID.
|
|
||||||
> Running `wallet account new public` generates a fresh keypair for the signature scheme used in NSSA.
|
|
||||||
> The account ID is derived from the public key. The private key is used to sign transactions and to authorize the account in program executions.
|
|
||||||
|
|
||||||
#### Account initialization
|
|
||||||
|
|
||||||
To query the account’s current status, run:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Replace the id with yours
|
|
||||||
wallet account get --account-id Public/9ypzv6GGr3fwsgxY7EZezg5rz6zj52DPCkmf1vVujEiJ
|
|
||||||
|
|
||||||
# Output:
|
|
||||||
Account is Uninitialized
|
|
||||||
```
|
|
||||||
|
|
||||||
> [!NOTE]
|
|
||||||
> New accounts begin in an uninitialized state, meaning they are not yet owned by any program. A program may claim an uninitialized account; once claimed, the account becomes owned by that program.
|
|
||||||
> Owned accounts can only be modified through executions of the owning program. The only exception is native-token credits: any program may credit native tokens to any account.
|
|
||||||
> However, debiting native tokens from an account must always be performed by its owning program.
|
|
||||||
|
|
||||||
In this example, we will initialize the account for the Authenticated transfer program, which securely manages native token transfers by requiring authentication for debits.
|
|
||||||
|
|
||||||
Initialize the account by running:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# This command submits a public transaction executing the `init` function of the
|
|
||||||
# Authenticated-transfer program. The wallet polls the sequencer until the
|
|
||||||
# transaction is included in a block, which may take several seconds.
|
|
||||||
wallet auth-transfer init --account-id Public/9ypzv6GGr3fwsgxY7EZezg5rz6zj52DPCkmf1vVujEiJ
|
|
||||||
```
|
|
||||||
|
|
||||||
After it completes, check the updated account status:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
wallet account get --account-id Public/9ypzv6GGr3fwsgxY7EZezg5rz6zj52DPCkmf1vVujEiJ
|
|
||||||
|
|
||||||
# Output:
|
|
||||||
Account owned by authenticated transfer program
|
|
||||||
{"balance":0}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Funding the account: executing the Piñata program
|
|
||||||
|
|
||||||
Now that we have a public account initialized by the authenticated transfer program, we need to fund it. For that, the testnet provides the Piñata program.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Complete with your id
|
|
||||||
wallet pinata claim --to Public/9ypzv6GGr3fwsgxY7EZezg5rz6zj52DPCkmf1vVujEiJ
|
|
||||||
```
|
|
||||||
|
|
||||||
After the claim succeeds, the account will be funded with some tokens:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
wallet account get --account-id Public/9ypzv6GGr3fwsgxY7EZezg5rz6zj52DPCkmf1vVujEiJ
|
|
||||||
|
|
||||||
# Output:
|
|
||||||
Account owned by authenticated transfer program
|
|
||||||
{"balance":150}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Native token transfers: executing the Authenticated transfers program
|
|
||||||
|
|
||||||
NSSA comes with a program for managing and transferring native tokens. Run `wallet auth-transfer` to see the options available:
|
|
||||||
```bash
|
|
||||||
Commands:
|
|
||||||
init Initialize account under authenticated transfer program
|
|
||||||
send Send native tokens from one account to another with variable privacy
|
|
||||||
help Print this message or the help of the given subcommand(s)
|
|
||||||
```
|
|
||||||
|
|
||||||
We have already used the `init` command. The `send` command is used to execute the `Transfer` function of the authenticated program.
|
|
||||||
Let's try it. For that we need to create another account for the recipient of the transfer.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
wallet account new public
|
|
||||||
|
|
||||||
# Output:
|
|
||||||
Generated new account with account_id Public/Ev1JprP9BmhbFVQyBcbznU8bAXcwrzwRoPTetXdQPAWS
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
> [!NOTE]
|
|
||||||
> The new account is uninitialized. The authenticated transfers program will claim any uninitialized account used in a transfer. So we don't need to manually initialize the recipient account.
|
|
||||||
|
|
||||||
Let's send 37 tokens to the new account.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
wallet auth-transfer send \
|
|
||||||
--from Public/9ypzv6GGr3fwsgxY7EZezg5rz6zj52DPCkmf1vVujEiJ \
|
|
||||||
--to Public/Ev1JprP9BmhbFVQyBcbznU8bAXcwrzwRoPTetXdQPAWS \
|
|
||||||
--amount 37
|
|
||||||
```
|
|
||||||
|
|
||||||
Once that succeeds we can check the states.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Sender account
|
|
||||||
wallet account get --account-id Public/HrA8TVjBS8UVf9akV7LRhyh6k4c7F6PS7PvqgtPmKAT8
|
|
||||||
|
|
||||||
# Output:
|
|
||||||
Account owned by authenticated transfer program
|
|
||||||
{"balance":113}
|
|
||||||
```
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Recipient account
|
|
||||||
wallet account get --account-id Public/Ev1JprP9BmhbFVQyBcbznU8bAXcwrzwRoPTetXdQPAWS
|
|
||||||
|
|
||||||
# Output:
|
|
||||||
Account owned by authenticated transfer program
|
|
||||||
{"balance":37}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Create a new private account
|
|
||||||
|
|
||||||
> [!NOTE]
|
|
||||||
> Private accounts are structurally identical to public accounts; they differ only in how their state is stored off-chain and represented on-chain.
|
|
||||||
> The raw values of a private account are never stored on-chain. Instead, the chain only holds a 32-byte commitment (a hash-like binding to the actual values). Transactions include encrypted versions of the private values so that users can recover them from the blockchain. The decryption keys are known only to the user and are never shared.
|
|
||||||
> Private accounts are not managed through the usual signature mechanism used for public accounts. Instead, each private account is associated with two keypairs:
|
|
||||||
> - *Nullifier keys*, for using the corresponding private account in privacy preserving executions.
|
|
||||||
> - *Viewing keys*, used for encrypting and decrypting the values included in transactions.
|
|
||||||
>
|
|
||||||
> Private accounts also have a 32-byte identifier, derived from the nullifier public key.
|
|
||||||
>
|
|
||||||
> Just like public accounts, private accounts can only be initialized once. Any user can initialize them without knowing the owner's secret keys. However, modifying an initialized private account through an off-chain program execution requires knowledge of the owner’s secret keys.
|
|
||||||
>
|
|
||||||
> Transactions that modify the values of a private account include a commitment to the new values, which will be added to the on-chain commitment set. They also include a nullifier that marks the previous version as old.
|
|
||||||
> The nullifier is constructed so that it cannot be linked to any prior commitment, ensuring that updates to the same private account cannot be correlated.
|
|
||||||
|
|
||||||
Now let’s switch to the private state and create a private account.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
wallet account new private
|
|
||||||
|
|
||||||
# Output:
|
|
||||||
Generated new account with account_id Private/HacPU3hakLYzWtSqUPw6TUr8fqoMieVWovsUR6sJf7cL
|
|
||||||
With npk e6366f79d026c8bd64ae6b3d601f0506832ec682ab54897f205fffe64ec0d951
|
|
||||||
With ipk 02ddc96d0eb56e00ce14994cfdaec5ae1f76244180a919545983156e3519940a17
|
|
||||||
```
|
|
||||||
|
|
||||||
For now, focus only on the account id. Ignore the `npk` and `ipk` values. These are the Nullifier public key and the Viewing public key. They are stored locally in the wallet and are used internally to build privacy-preserving transactions.
|
|
||||||
Also, the account id for private accounts is derived from the `npk` value. But we won't need them now.
|
|
||||||
|
|
||||||
Just like public accounts, new private accounts start out uninitialized:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
wallet account get --account-id Private/HacPU3hakLYzWtSqUPw6TUr8fqoMieVWovsUR6sJf7cL
|
|
||||||
|
|
||||||
# Output:
|
|
||||||
Account is Uninitialized
|
|
||||||
```
|
|
||||||
Unlike public accounts, private accounts are never visible to the network. They exist only in your local wallet storage.
|
|
||||||
|
|
||||||
#### Sending tokens from the public account to the private account
|
|
||||||
|
|
||||||
Sending tokens to an uninitialized private account causes the Authenticated-Transfers program to claim it. Just like with public accounts.
|
|
||||||
This happens because program execution logic does not depend on whether the involved accounts are public or private.
|
|
||||||
|
|
||||||
Let’s send 17 tokens to the new private account.
|
|
||||||
|
|
||||||
The syntax is identical to the public-to-public transfer; just set the private ID as the recipient.
|
|
||||||
|
|
||||||
This command will run the Authenticated-Transfer program locally, generate a proof, and submit it to the sequencer. Depending on your machine, this can take from 30 seconds to 4 minutes.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
wallet auth-transfer send \
|
|
||||||
--from Public/Ev1JprP9BmhbFVQyBcbznU8bAXcwrzwRoPTetXdQPAWS \
|
|
||||||
--to Private/HacPU3hakLYzWtSqUPw6TUr8fqoMieVWovsUR6sJf7cL \
|
|
||||||
--amount 17
|
|
||||||
```
|
|
||||||
|
|
||||||
After it succeeds, check both accounts:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Public sender account
|
|
||||||
wallet account get --account-id Public/Ev1JprP9BmhbFVQyBcbznU8bAXcwrzwRoPTetXdQPAWS
|
|
||||||
|
|
||||||
# Output:
|
|
||||||
Account owned by authenticated transfer program
|
|
||||||
{"balance":20}
|
|
||||||
```
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Private recipient account
|
|
||||||
wallet account get --account-id Private/HacPU3hakLYzWtSqUPw6TUr8fqoMieVWovsUR6sJf7cL
|
|
||||||
|
|
||||||
# Output:
|
|
||||||
Account owned by authenticated transfer program
|
|
||||||
{"balance":17}
|
|
||||||
```
|
|
||||||
|
|
||||||
> [!NOTE]
|
|
||||||
> The last command does not query the network.
|
|
||||||
> It works even offline because private account data lives only in your wallet storage. Other users cannot read your private balances.
|
|
||||||
|
|
||||||
#### Digression: modifying private accounts
|
|
||||||
|
|
||||||
As a general rule, private accounts can only be modified through a program execution performed by their owner. That is, the person who holds the private key for that account. There is one exception: an uninitialized private account may be initialized by any user, without requiring the private key. After initialization, only the owner can modify it.
|
|
||||||
|
|
||||||
This mechanism enables a common use case: transferring funds from any account (public or private) to a private account owned by someone else. For such transfers, the recipient’s private account must be uninitialized.
|
|
||||||
|
|
||||||
|
|
||||||
#### Sending tokens from the public account to a private account owned by someone else
|
|
||||||
|
|
||||||
For this tutorial, we’ll simulate that scenario by creating a new private account that we own, but we’ll treat it as if it belonged to someone else.
|
|
||||||
|
|
||||||
Let's create a new (uninitialized) private account like before:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
wallet account new private
|
|
||||||
|
|
||||||
# Output:
|
|
||||||
Generated new account with account_id Private/AukXPRBmrYVqoqEW2HTs7N3hvTn3qdNFDcxDHVr5hMm5
|
|
||||||
With npk 0c95ebc4b3830f53da77bb0b80a276a776cdcf6410932acc718dcdb3f788a00e
|
|
||||||
With ipk 039fd12a3674a880d3e917804129141e4170d419d1f9e28a3dcf979c1f2369cb72
|
|
||||||
```
|
|
||||||
|
|
||||||
Now we'll ignore the private account ID and focus on the `npk` and `ipk` values. We'll need this to send tokens to a foreign private account. Syntax is very similar.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
wallet auth-transfer send \
|
|
||||||
--from Public/Ev1JprP9BmhbFVQyBcbznU8bAXcwrzwRoPTetXdQPAWS \
|
|
||||||
--to-npk 0c95ebc4b3830f53da77bb0b80a276a776cdcf6410932acc718dcdb3f788a00e \
|
|
||||||
--to-ipk 039fd12a3674a880d3e917804129141e4170d419d1f9e28a3dcf979c1f2369cb72 \
|
|
||||||
--amount 3
|
|
||||||
```
|
|
||||||
|
|
||||||
The command above produces a privacy-preserving transaction, which may take a few minutes to complete. The updated values of the private account are encrypted and included in the transaction.
|
|
||||||
|
|
||||||
Once the transaction is accepted, the recipient must run `wallet account sync-private`. This command scans the chain for encrypted values that belong to their private accounts and updates the local versions accordingly.
|
|
||||||
|
|
||||||
|
|
||||||
#### Transfers in other combinations of public and private accounts
|
|
||||||
|
|
||||||
We’ve shown how to use the authenticated-transfers program for transfers between two public accounts, and for transfers from a public sender to a private recipient. Sending tokens from a private account (whether to a public account or to another private account) works in essentially the same way.
|
|
||||||
|
|
||||||
### The token program
|
|
||||||
|
|
||||||
So far, we’ve made transfers using the authenticated-transfers program, which handles native token transfers. The Token program, on the other hand, is used for creating and managing custom tokens.
|
|
||||||
|
|
||||||
> [!NOTE]
|
|
||||||
> The token program is a single program responsible for creating and managing all tokens. There is no need to deploy new programs to introduce new tokens. All token-related operations are performed by invoking the appropriate functions of the token program.
|
|
||||||
|
|
||||||
The CLI provides commands to execute the token program. To see the options available run `wallet token`:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
Commands:
|
|
||||||
new Produce a new token
|
|
||||||
send Send tokens from one account to another with variable privacy
|
|
||||||
help Print this message or the help of the given subcommand(s)
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
> [!NOTE]
|
|
||||||
> The Token program manages its accounts in two categories. Meaning, all accounts owned by the Token program fall into one of these types.
|
|
||||||
> - Token definition accounts: these accounts store metadata about a token, such as its name, total supply, and other identifying properties. They act as the token’s unique identifier.
|
|
||||||
> - Token holding accounts: these accounts hold actual token balances. In addition to the balance, they also record which token definition they belong to.
|
|
||||||
|
|
||||||
#### Creating a new token
|
|
||||||
|
|
||||||
To create a new token, simply run `wallet token new`. This will create a transaction to execute the `New` function of the token program.
|
|
||||||
The command expects a name, the desired total supply, and two uninitialized accounts:
|
|
||||||
- One that will be initialized as the token definition account for the new token.
|
|
||||||
- Another that will be initialized as a token holding account and receive the token’s entire initial supply.
|
|
||||||
|
|
||||||
|
|
||||||
##### New token with both definition and supply accounts set as public
|
|
||||||
|
|
||||||
For example, let's create two new (uninitialized) public accounts and then use them to create a new token.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
wallet account new public
|
|
||||||
|
|
||||||
# Output:
|
|
||||||
Generated new account with account_id Public/4X9kAcnCZ1Ukkbm3nywW9xfCNPK8XaMWCk3zfs1sP4J7
|
|
||||||
```
|
|
||||||
|
|
||||||
```bash
|
|
||||||
wallet account new public
|
|
||||||
|
|
||||||
# Output:
|
|
||||||
Generated new account with account_id Public/9RRSMm3w99uCD2Jp2Mqqf6dfc8me2tkFRE9HeU2DFftw
|
|
||||||
```
|
|
||||||
|
|
||||||
Now we use them to create a new token. Let's call it the "Token A"
|
|
||||||
|
|
||||||
```bash
|
|
||||||
wallet token new \
|
|
||||||
--name TOKENA \
|
|
||||||
--total-supply 1337 \
|
|
||||||
--definition-account-id Public/4X9kAcnCZ1Ukkbm3nywW9xfCNPK8XaMWCk3zfs1sP4J7 \
|
|
||||||
--supply-account-id Public/9RRSMm3w99uCD2Jp2Mqqf6dfc8me2tkFRE9HeU2DFftw
|
|
||||||
```
|
|
||||||
|
|
||||||
After it succeeds, we can inspect the two accounts to see how they were initialized.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
wallet account get --account-id Public/4X9kAcnCZ1Ukkbm3nywW9xfCNPK8XaMWCk3zfs1sP4J7
|
|
||||||
|
|
||||||
# Output:
|
|
||||||
Definition account owned by token program
|
|
||||||
{"account_type":"Token definition","name":"TOKENA","total_supply":1337}
|
|
||||||
```
|
|
||||||
|
|
||||||
```bash
|
|
||||||
wallet account get --account-id Public/9RRSMm3w99uCD2Jp2Mqqf6dfc8me2tkFRE9HeU2DFftw
|
|
||||||
|
|
||||||
# Output:
|
|
||||||
Holding account owned by token program
|
|
||||||
{"account_type":"Token holding","definition_id":"4X9kAcnCZ1Ukkbm3nywW9xfCNPK8XaMWCk3zfs1sP4J7","balance":1337}
|
|
||||||
```
|
|
||||||
|
|
||||||
##### New token with public account definition but private holding account for initial supply
|
|
||||||
|
|
||||||
Let’s create a new token, but this time using a public definition account and a private holding account to store the entire supply.
|
|
||||||
|
|
||||||
Since we can’t reuse the accounts from the previous example, we need to create fresh ones for this case.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
wallet account new public
|
|
||||||
|
|
||||||
# Output:
|
|
||||||
Generated new account with account_id Public/GQ3C8rbprTtQUCvkuVBRu3v9wvUvjafCMFqoSPvTEVii
|
|
||||||
```
|
|
||||||
|
|
||||||
```bash
|
|
||||||
wallet account new private
|
|
||||||
|
|
||||||
|
|
||||||
# Output:
|
|
||||||
Generated new account with account_id Private/HMRHZdPw4pbyPVZHNGrV6K5AA95wACFsHTRST84fr3CF
|
|
||||||
With npk 6a2dfe433cf28e525aa0196d719be3c16146f7ee358ca39595323f94fde38f93
|
|
||||||
With ipk 03d59abf4bee974cc12ddb44641c19f0b5441fef39191f047c988c29a77252a577
|
|
||||||
```
|
|
||||||
|
|
||||||
And we use them to create the token.
|
|
||||||
|
|
||||||
Now we use them to create a new token. Let's call it "Token B".
|
|
||||||
|
|
||||||
```bash
|
|
||||||
wallet token new \
|
|
||||||
--name TOKENB \
|
|
||||||
--total-supply 7331 \
|
|
||||||
--definition-account-id Public/GQ3C8rbprTtQUCvkuVBRu3v9wvUvjafCMFqoSPvTEVii \
|
|
||||||
--supply-account-id Private/HMRHZdPw4pbyPVZHNGrV6K5AA95wACFsHTRST84fr3CF
|
|
||||||
```
|
|
||||||
|
|
||||||
After it succeeds, we can check their values
|
|
||||||
|
|
||||||
```bash
|
|
||||||
wallet account get --account-id Public/GQ3C8rbprTtQUCvkuVBRu3v9wvUvjafCMFqoSPvTEVii
|
|
||||||
|
|
||||||
# Output:
|
|
||||||
Definition account owned by token program
|
|
||||||
{"account_type":"Token definition","name":"TOKENB","total_supply":7331}
|
|
||||||
```
|
|
||||||
|
|
||||||
```bash
|
|
||||||
wallet account get --account-id Private/HMRHZdPw4pbyPVZHNGrV6K5AA95wACFsHTRST84fr3CF
|
|
||||||
|
|
||||||
# Output:
|
|
||||||
Holding account owned by token program
|
|
||||||
{"account_type":"Token holding","definition_id":"GQ3C8rbprTtQUCvkuVBRu3v9wvUvjafCMFqoSPvTEVii","balance":7331}
|
|
||||||
```
|
|
||||||
|
|
||||||
Like any other private account owned by us, it cannot be seen by other users.
|
|
||||||
|
|
||||||
#### Custom token transfers
|
|
||||||
|
|
||||||
The Token program has a function to move funds from one token holding account to another one. If executed with an uninitialized account as the recipient, this will be automatically claimed by the token program.
|
|
||||||
|
|
||||||
The transfer function can be executed with the `wallet token send` command.
|
|
||||||
|
|
||||||
Let's create a new public account for the recipient.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
wallet account new public
|
|
||||||
|
|
||||||
# Output:
|
|
||||||
Generated new account with account_id Public/88f2zeTgiv9LUthQwPJbrmufb9SiDfmpCs47B7vw6Gd6
|
|
||||||
```
|
|
||||||
|
|
||||||
Let's send 1000 B tokens to this new account. We'll debit this from the supply account used in the creation of the token.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
wallet token send \
|
|
||||||
--from Private/HMRHZdPw4pbyPVZHNGrV6K5AA95wACFsHTRST84fr3CF \
|
|
||||||
--to Public/88f2zeTgiv9LUthQwPJbrmufb9SiDfmpCs47B7vw6Gd6 \
|
|
||||||
--amount 1000
|
|
||||||
```
|
|
||||||
|
|
||||||
Let's inspect the public account:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
wallet account get --account-id Public/88f2zeTgiv9LUthQwPJbrmufb9SiDfmpCs47B7vw6Gd6
|
|
||||||
|
|
||||||
# Output:
|
|
||||||
Holding account owned by token program
|
|
||||||
{"account_type":"Token holding","definition_id":"GQ3C8rbprTtQUCvkuVBRu3v9wvUvjafCMFqoSPvTEVii","balance":1000}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Chain information
|
|
||||||
|
|
||||||
The wallet provides some commands to query information about the chain. These are under the `wallet chain-info` command.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
Commands:
|
|
||||||
current-block-id Get current block id from sequencer
|
|
||||||
block Get block at id from sequencer
|
|
||||||
transaction Get transaction at hash from sequencer
|
|
||||||
```
|
|
||||||
|
|
||||||
For example, run this to find the current block id.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
wallet chain-info current-block-id
|
|
||||||
|
|
||||||
# Output:
|
|
||||||
Last block id is 65537
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
### Automated Market Maker (AMM)
|
|
||||||
|
|
||||||
NSSA includes an AMM program that manages liquidity pools and enables swaps between custom tokens. To test this functionality, we first need to create a liquidity pool.
|
|
||||||
|
|
||||||
#### Creating a liquidity pool for a token pair
|
|
||||||
|
|
||||||
We start by creating a new pool for the tokens previously created. In return for providing liquidity, we will receive liquidity provider (LP) tokens, which represent our share of the pool and are required to withdraw liquidity later.
|
|
||||||
|
|
||||||
>[!NOTE]
|
|
||||||
> The AMM program does not currently charge swap fees or distribute rewards to liquidity providers. LP tokens therefore only represent a proportional share of the pool reserves and do not provide additional value from swap activity. Fee support for liquidity providers will be added in future versions of the AMM program.
|
|
||||||
|
|
||||||
To hold these LP tokens, we first create a new account:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
wallet account new public
|
|
||||||
|
|
||||||
# Output:
|
|
||||||
Generated new account with account_id Public/FHgLW9jW4HXMV6egLWbwpTqVAGiCHw2vkg71KYSuimVf
|
|
||||||
```
|
|
||||||
|
|
||||||
Next, we initialize the liquidity pool by depositing tokens A and B and specifying the account that will receive the LP tokens:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
wallet amm new \
|
|
||||||
--user-holding-a Public/9RRSMm3w99uCD2Jp2Mqqf6dfc8me2tkFRE9HeU2DFftw \
|
|
||||||
--user-holding-b Public/88f2zeTgiv9LUthQwPJbrmufb9SiDfmpCs47B7vw6Gd6 \
|
|
||||||
--user-holding-lp Public/FHgLW9jW4HXMV6egLWbwpTqVAGiCHw2vkg71KYSuimVf \
|
|
||||||
--balance-a 100 \
|
|
||||||
--balance-b 200
|
|
||||||
```
|
|
||||||
|
|
||||||
The newly created account is owned by the token program, meaning that LP tokens are managed by the same token infrastructure as regular tokens.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
wallet account get --account-id Public/FHgLW9jW4HXMV6egLWbwpTqVAGiCHw2vkg71KYSuimVf
|
|
||||||
|
|
||||||
# Output:
|
|
||||||
Holding account owned by token program
|
|
||||||
{"account_type":"Token holding","definition_id":"7BeDS3e28MA5Err7gBswmR1fUKdHXqmUpTefNPu3pJ9i","balance":100}
|
|
||||||
```
|
|
||||||
|
|
||||||
If you inspect the `user-holding-a` and `user-holding-b` accounts passed to the `wallet amm new` command, you will see that 100 and 200 tokens were deducted, respectively. These tokens now reside in the liquidity pool and are available for swaps by any user.
|
|
||||||
|
|
||||||
|
|
||||||
#### Swaping
|
|
||||||
|
|
||||||
Token swaps can be performed using the wallet amm swap command:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
wallet amm swap \
|
|
||||||
--user-holding-a Public/9RRSMm3w99uCD2Jp2Mqqf6dfc8me2tkFRE9HeU2DFftw \
|
|
||||||
--user-holding-b Public/88f2zeTgiv9LUthQwPJbrmufb9SiDfmpCs47B7vw6Gd6 \
|
|
||||||
# The amount of tokens to swap
|
|
||||||
--amount-in 5 \
|
|
||||||
# The minimum number of tokens expected in return
|
|
||||||
--min-amount-out 8 \
|
|
||||||
# The definition ID of the token being provided to the swap
|
|
||||||
# In this case, we are swapping from TOKENA to TOKENB, and so this is the definition ID of TOKENA
|
|
||||||
--token-definition 4X9kAcnCZ1Ukkbm3nywW9xfCNPK8XaMWCk3zfs1sP4J7
|
|
||||||
```
|
|
||||||
|
|
||||||
Once executed, 5 tokens are deducted from the Token A holding account and the corresponding amount (determined by the pool’s pricing function) is credited to the Token B holding account.
|
|
||||||
|
|
||||||
|
|
||||||
#### Withdrawing liquidity from the pool
|
|
||||||
|
|
||||||
Liquidity providers can withdraw assets from the pool by redeeming (burning) LP tokens. The amount of tokens received is proportional to the share of LP tokens being redeemed relative to the total LP supply.
|
|
||||||
|
|
||||||
This operation is performed using the `wallet amm remove-liquidity` command:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
wallet amm remove-liquidity \
|
|
||||||
--user-holding-a Public/9RRSMm3w99uCD2Jp2Mqqf6dfc8me2tkFRE9HeU2DFftw \
|
|
||||||
--user-holding-b Public/88f2zeTgiv9LUthQwPJbrmufb9SiDfmpCs47B7vw6Gd6 \
|
|
||||||
--user-holding-lp Public/FHgLW9jW4HXMV6egLWbwpTqVAGiCHw2vkg71KYSuimVf \
|
|
||||||
--balance-lp 20 \
|
|
||||||
--min-amount-a 1 \
|
|
||||||
--min-amount-b 1
|
|
||||||
```
|
|
||||||
|
|
||||||
This instruction burns `balance-lp` LP tokens from the user’s LP holding account. In exchange, the AMM transfers tokens A and B from the pool’s vault accounts to the user’s holding accounts, according to the current pool reserves.
|
|
||||||
|
|
||||||
The `min-amount-a` and `min-amount-b` parameters specify the minimum acceptable amounts of tokens A and B to be received. If the computed outputs fall below either threshold, the instruction fails, protecting the user against unfavorable pool state changes.
|
|
||||||
|
|
||||||
#### Adding liquidity to the pool
|
|
||||||
|
|
||||||
Additional liquidity can be added to an existing pool by depositing tokens A and B in the ratio implied by the current pool reserves. In return, new LP tokens are minted to represent the user’s proportional share of the pool.
|
|
||||||
|
|
||||||
This is done using the `wallet amm add-liquidity` command:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
wallet amm add-liquidity \
|
|
||||||
--user-holding-a Public/9RRSMm3w99uCD2Jp2Mqqf6dfc8me2tkFRE9HeU2DFftw \
|
|
||||||
--user-holding-b Public/88f2zeTgiv9LUthQwPJbrmufb9SiDfmpCs47B7vw6Gd6 \
|
|
||||||
--user-holding-lp Public/FHgLW9jW4HXMV6egLWbwpTqVAGiCHw2vkg71KYSuimVf \
|
|
||||||
--min-amount-lp 1 \
|
|
||||||
--max-amount-a 10 \
|
|
||||||
--max-amount-b 10
|
|
||||||
```
|
|
||||||
|
|
||||||
In this instruction, `max-amount-a` and `max-amount-b` define upper bounds on the number of tokens A and B that may be withdrawn from the user’s accounts. The AMM computes the actual required amounts based on the pool’s reserve ratio.
|
|
||||||
|
|
||||||
The `min-amount-lp` parameter specifies the minimum number of LP tokens that must be minted for the transaction to succeed. If the resulting LP token amount is below this threshold, the instruction fails.
|
|
||||||
|
|
||||||
|
|||||||
@ -62,7 +62,6 @@ impl BedrockClient {
|
|||||||
Retry::spawn(self.backoff_strategy(), || {
|
Retry::spawn(self.backoff_strategy(), || {
|
||||||
self.http_client
|
self.http_client
|
||||||
.post_transaction(self.node_url.clone(), tx.clone())
|
.post_transaction(self.node_url.clone(), tx.clone())
|
||||||
.inspect_err(|err| warn!("Transaction posting failed with error: {err:#}"))
|
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|||||||
@ -6,6 +6,16 @@ use sha2::{Digest, Sha256, digest::FixedOutput};
|
|||||||
use crate::{HashType, transaction::NSSATransaction};
|
use crate::{HashType, transaction::NSSATransaction};
|
||||||
|
|
||||||
pub type MantleMsgId = [u8; 32];
|
pub type MantleMsgId = [u8; 32];
|
||||||
|
pub type BlockHash = HashType;
|
||||||
|
pub type BlockId = u64;
|
||||||
|
pub type TimeStamp = u64;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, BorshSerialize, BorshDeserialize)]
|
||||||
|
pub struct BlockMeta {
|
||||||
|
pub id: BlockId,
|
||||||
|
pub hash: BlockHash,
|
||||||
|
pub msg_id: MantleMsgId,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
/// Our own hasher.
|
/// Our own hasher.
|
||||||
@ -21,10 +31,6 @@ impl OwnHasher {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub type BlockHash = HashType;
|
|
||||||
pub type BlockId = u64;
|
|
||||||
pub type TimeStamp = u64;
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, BorshSerialize, BorshDeserialize)]
|
#[derive(Debug, Clone, BorshSerialize, BorshDeserialize)]
|
||||||
pub struct BlockHeader {
|
pub struct BlockHeader {
|
||||||
pub block_id: BlockId,
|
pub block_id: BlockId,
|
||||||
|
|||||||
@ -13,25 +13,13 @@ pub struct SequencerRpcError {
|
|||||||
#[derive(thiserror::Error, Debug)]
|
#[derive(thiserror::Error, Debug)]
|
||||||
pub enum SequencerClientError {
|
pub enum SequencerClientError {
|
||||||
#[error("HTTP error")]
|
#[error("HTTP error")]
|
||||||
HTTPError(reqwest::Error),
|
HTTPError(#[from] reqwest::Error),
|
||||||
#[error("Serde error")]
|
#[error("Serde error")]
|
||||||
SerdeError(serde_json::Error),
|
SerdeError(#[from] serde_json::Error),
|
||||||
#[error("Internal error")]
|
#[error("Internal error: {0:?}")]
|
||||||
InternalError(SequencerRpcError),
|
InternalError(SequencerRpcError),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<reqwest::Error> for SequencerClientError {
|
|
||||||
fn from(value: reqwest::Error) -> Self {
|
|
||||||
SequencerClientError::HTTPError(value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<serde_json::Error> for SequencerClientError {
|
|
||||||
fn from(value: serde_json::Error) -> Self {
|
|
||||||
SequencerClientError::SerdeError(value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<SequencerRpcError> for SequencerClientError {
|
impl From<SequencerRpcError> for SequencerClientError {
|
||||||
fn from(value: SequencerRpcError) -> Self {
|
fn from(value: SequencerRpcError) -> Self {
|
||||||
SequencerClientError::InternalError(value)
|
SequencerClientError::InternalError(value)
|
||||||
|
|||||||
@ -7,7 +7,7 @@ use serde::{Deserialize, Serialize};
|
|||||||
pub struct SearchResults {
|
pub struct SearchResults {
|
||||||
pub blocks: Vec<Block>,
|
pub blocks: Vec<Block>,
|
||||||
pub transactions: Vec<Transaction>,
|
pub transactions: Vec<Transaction>,
|
||||||
pub accounts: Vec<(AccountId, Option<Account>)>,
|
pub accounts: Vec<(AccountId, Account)>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// RPC client type
|
/// RPC client type
|
||||||
@ -60,14 +60,8 @@ pub async fn search(query: String) -> Result<SearchResults, ServerFnError> {
|
|||||||
|
|
||||||
// Try as account ID
|
// Try as account ID
|
||||||
let account_id = AccountId { value: hash_array };
|
let account_id = AccountId { value: hash_array };
|
||||||
match client.get_account(account_id).await {
|
if let Ok(account) = client.get_account(account_id).await {
|
||||||
Ok(account) => {
|
accounts.push((account_id, account));
|
||||||
accounts.push((account_id, Some(account)));
|
|
||||||
}
|
|
||||||
Err(_) => {
|
|
||||||
// Account might not exist yet, still add it to results
|
|
||||||
accounts.push((account_id, None));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -6,7 +6,7 @@ use crate::format_utils;
|
|||||||
|
|
||||||
/// Account preview component
|
/// Account preview component
|
||||||
#[component]
|
#[component]
|
||||||
pub fn AccountPreview(account_id: AccountId, account: Option<Account>) -> impl IntoView {
|
pub fn AccountPreview(account_id: AccountId, account: Account) -> impl IntoView {
|
||||||
let account_id_str = format_utils::format_account_id(&account_id);
|
let account_id_str = format_utils::format_account_id(&account_id);
|
||||||
|
|
||||||
view! {
|
view! {
|
||||||
@ -19,42 +19,31 @@ pub fn AccountPreview(account_id: AccountId, account: Option<Account>) -> impl I
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{move || {
|
{move || {
|
||||||
account
|
let Account { program_owner, balance, data, nonce } = &account;
|
||||||
.as_ref()
|
let program_id = format_utils::format_program_id(program_owner);
|
||||||
.map(|Account { program_owner, balance, data, nonce }| {
|
view! {
|
||||||
let program_id = format_utils::format_program_id(program_owner);
|
<div class="account-preview-body">
|
||||||
view! {
|
<div class="account-field">
|
||||||
<div class="account-preview-body">
|
<span class="field-label">"Balance: "</span>
|
||||||
<div class="account-field">
|
<span class="field-value">{balance.to_string()}</span>
|
||||||
<span class="field-label">"Balance: "</span>
|
</div>
|
||||||
<span class="field-value">{balance.to_string()}</span>
|
<div class="account-field">
|
||||||
</div>
|
<span class="field-label">"Program: "</span>
|
||||||
<div class="account-field">
|
<span class="field-value hash">{program_id}</span>
|
||||||
<span class="field-label">"Program: "</span>
|
</div>
|
||||||
<span class="field-value hash">{program_id}</span>
|
<div class="account-field">
|
||||||
</div>
|
<span class="field-label">"Nonce: "</span>
|
||||||
<div class="account-field">
|
<span class="field-value">{nonce.to_string()}</span>
|
||||||
<span class="field-label">"Nonce: "</span>
|
</div>
|
||||||
<span class="field-value">{nonce.to_string()}</span>
|
<div class="account-field">
|
||||||
</div>
|
<span class="field-label">"Data: "</span>
|
||||||
<div class="account-field">
|
<span class="field-value">
|
||||||
<span class="field-label">"Data: "</span>
|
{format!("{} bytes", data.0.len())}
|
||||||
<span class="field-value">
|
</span>
|
||||||
{format!("{} bytes", data.0.len())}
|
</div>
|
||||||
</span>
|
</div>
|
||||||
</div>
|
}
|
||||||
</div>
|
.into_any()
|
||||||
}
|
|
||||||
.into_any()
|
|
||||||
})
|
|
||||||
.unwrap_or_else(|| {
|
|
||||||
view! {
|
|
||||||
<div class="account-preview-body">
|
|
||||||
<div class="account-not-found">"Account not found"</div>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
.into_any()
|
|
||||||
})
|
|
||||||
}}
|
}}
|
||||||
|
|
||||||
</A>
|
</A>
|
||||||
|
|||||||
@ -24,7 +24,7 @@ pub struct IndexerCore {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl IndexerCore {
|
impl IndexerCore {
|
||||||
pub async fn new(config: IndexerConfig) -> Result<Self> {
|
pub fn new(config: IndexerConfig) -> Result<Self> {
|
||||||
// ToDo: replace with correct startup
|
// ToDo: replace with correct startup
|
||||||
let hashable_data = HashableBlockData {
|
let hashable_data = HashableBlockData {
|
||||||
block_id: 1,
|
block_id: 1,
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"resubscribe_interval_millis": 1000,
|
"resubscribe_interval_millis": 1000,
|
||||||
"bedrock_client_config": {
|
"bedrock_client_config": {
|
||||||
"addr": "http://localhost:18080",
|
"addr": "http://localhost:8080",
|
||||||
"backoff": {
|
"backoff": {
|
||||||
"start_delay_millis": 100,
|
"start_delay_millis": 100,
|
||||||
"max_retries": 5
|
"max_retries": 5
|
||||||
|
|||||||
@ -10,7 +10,7 @@ compile_error!("At least one of `server` or `client` features must be enabled.")
|
|||||||
#[cfg_attr(all(feature = "client", not(feature = "server")), rpc(client))]
|
#[cfg_attr(all(feature = "client", not(feature = "server")), rpc(client))]
|
||||||
#[cfg_attr(all(feature = "server", feature = "client"), rpc(server, client))]
|
#[cfg_attr(all(feature = "server", feature = "client"), rpc(server, client))]
|
||||||
pub trait Rpc {
|
pub trait Rpc {
|
||||||
#[method(name = "get_schema")]
|
#[method(name = "getSchema")]
|
||||||
fn get_schema(&self) -> Result<serde_json::Value, ErrorObjectOwned> {
|
fn get_schema(&self) -> Result<serde_json::Value, ErrorObjectOwned> {
|
||||||
// TODO: Canonical solution would be to provide `describe` method returning OpenRPC spec,
|
// TODO: Canonical solution would be to provide `describe` method returning OpenRPC spec,
|
||||||
// But for now it's painful to implement, although can be done if really needed.
|
// But for now it's painful to implement, although can be done if really needed.
|
||||||
|
|||||||
@ -77,9 +77,8 @@ pub async fn run_server(config: IndexerConfig, port: u16) -> Result<IndexerHandl
|
|||||||
|
|
||||||
#[cfg(not(feature = "mock-responses"))]
|
#[cfg(not(feature = "mock-responses"))]
|
||||||
let handle = {
|
let handle = {
|
||||||
let service = service::IndexerService::new(config)
|
let service =
|
||||||
.await
|
service::IndexerService::new(config).context("Failed to initialize indexer service")?;
|
||||||
.context("Failed to initialize indexer service")?;
|
|
||||||
server.start(service.into_rpc())
|
server.start(service.into_rpc())
|
||||||
};
|
};
|
||||||
#[cfg(feature = "mock-responses")]
|
#[cfg(feature = "mock-responses")]
|
||||||
|
|||||||
@ -211,13 +211,6 @@ impl indexer_service_rpc::RpcServer for MockIndexerService {
|
|||||||
.ok_or_else(|| ErrorObjectOwned::owned(-32001, "Block with hash not found", None::<()>))
|
.ok_or_else(|| ErrorObjectOwned::owned(-32001, "Block with hash not found", None::<()>))
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn get_last_block_id(&self) -> Result<BlockId, ErrorObjectOwned> {
|
|
||||||
self.blocks
|
|
||||||
.last()
|
|
||||||
.map(|b| b.header.block_id)
|
|
||||||
.ok_or_else(|| ErrorObjectOwned::owned(-32001, "No blocks available", None::<()>))
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn get_account(&self, account_id: AccountId) -> Result<Account, ErrorObjectOwned> {
|
async fn get_account(&self, account_id: AccountId) -> Result<Account, ErrorObjectOwned> {
|
||||||
self.accounts
|
self.accounts
|
||||||
.get(&account_id)
|
.get(&account_id)
|
||||||
|
|||||||
@ -19,8 +19,8 @@ pub struct IndexerService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl IndexerService {
|
impl IndexerService {
|
||||||
pub async fn new(config: IndexerConfig) -> Result<Self> {
|
pub fn new(config: IndexerConfig) -> Result<Self> {
|
||||||
let indexer = IndexerCore::new(config).await?;
|
let indexer = IndexerCore::new(config)?;
|
||||||
let subscription_service = SubscriptionService::spawn_new(indexer.clone());
|
let subscription_service = SubscriptionService::spawn_new(indexer.clone());
|
||||||
|
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
|
|||||||
@ -16,6 +16,8 @@ indexer_service.workspace = true
|
|||||||
serde_json.workspace = true
|
serde_json.workspace = true
|
||||||
token_core.workspace = true
|
token_core.workspace = true
|
||||||
indexer_service_rpc.workspace = true
|
indexer_service_rpc.workspace = true
|
||||||
|
wallet-ffi.workspace = true
|
||||||
|
|
||||||
url.workspace = true
|
url.workspace = true
|
||||||
|
|
||||||
anyhow.workspace = true
|
anyhow.workspace = true
|
||||||
@ -27,4 +29,4 @@ hex.workspace = true
|
|||||||
tempfile.workspace = true
|
tempfile.workspace = true
|
||||||
borsh.workspace = true
|
borsh.workspace = true
|
||||||
futures.workspace = true
|
futures.workspace = true
|
||||||
testcontainers = { version = "0.26.3", features = ["docker-compose"] }
|
testcontainers = { version = "0.27.0", features = ["docker-compose"] }
|
||||||
|
|||||||
@ -1,132 +0,0 @@
|
|||||||
{
|
|
||||||
"home": "",
|
|
||||||
"initial_accounts": [
|
|
||||||
{
|
|
||||||
"account_id": "BLgCRDXYdQPMMWVHYRFGQZbgeHx9frkipa8GtpG2Syqy",
|
|
||||||
"balance": 10000
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"account_id": "Gj1mJy5W7J5pfmLRujmQaLfLMWidNxQ6uwnhb666ZwHw",
|
|
||||||
"balance": 20000
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"initial_commitments": [
|
|
||||||
{
|
|
||||||
"npk": [
|
|
||||||
63,
|
|
||||||
202,
|
|
||||||
178,
|
|
||||||
231,
|
|
||||||
183,
|
|
||||||
82,
|
|
||||||
237,
|
|
||||||
212,
|
|
||||||
216,
|
|
||||||
221,
|
|
||||||
215,
|
|
||||||
255,
|
|
||||||
153,
|
|
||||||
101,
|
|
||||||
177,
|
|
||||||
161,
|
|
||||||
254,
|
|
||||||
210,
|
|
||||||
128,
|
|
||||||
122,
|
|
||||||
54,
|
|
||||||
190,
|
|
||||||
230,
|
|
||||||
151,
|
|
||||||
183,
|
|
||||||
64,
|
|
||||||
225,
|
|
||||||
229,
|
|
||||||
113,
|
|
||||||
1,
|
|
||||||
228,
|
|
||||||
97
|
|
||||||
],
|
|
||||||
"account": {
|
|
||||||
"program_owner": [
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
"balance": 10000,
|
|
||||||
"data": [],
|
|
||||||
"nonce": 0
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"npk": [
|
|
||||||
192,
|
|
||||||
251,
|
|
||||||
166,
|
|
||||||
243,
|
|
||||||
167,
|
|
||||||
236,
|
|
||||||
84,
|
|
||||||
249,
|
|
||||||
35,
|
|
||||||
136,
|
|
||||||
130,
|
|
||||||
172,
|
|
||||||
219,
|
|
||||||
225,
|
|
||||||
161,
|
|
||||||
139,
|
|
||||||
229,
|
|
||||||
89,
|
|
||||||
243,
|
|
||||||
125,
|
|
||||||
194,
|
|
||||||
213,
|
|
||||||
209,
|
|
||||||
30,
|
|
||||||
23,
|
|
||||||
174,
|
|
||||||
100,
|
|
||||||
244,
|
|
||||||
124,
|
|
||||||
74,
|
|
||||||
140,
|
|
||||||
47
|
|
||||||
],
|
|
||||||
"account": {
|
|
||||||
"program_owner": [
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
"balance": 20000,
|
|
||||||
"data": [],
|
|
||||||
"nonce": 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"bedrock_client_config": {
|
|
||||||
"addr": "http://127.0.0.1:8080",
|
|
||||||
"auth": {
|
|
||||||
"username": "user"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"channel_id": "0101010101010101010101010101010101010101010101010101010101010101",
|
|
||||||
"backoff": {
|
|
||||||
"max_retries": 10,
|
|
||||||
"start_delay_millis": 100
|
|
||||||
},
|
|
||||||
"resubscribe_interval_millis": 1000,
|
|
||||||
"sequencer_client_config": {
|
|
||||||
"addr": "http://0.0.0.0"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -94,7 +94,6 @@ pub fn sequencer_config(
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO #312: Remove account id and key hardcoding
|
|
||||||
pub fn wallet_config(
|
pub fn wallet_config(
|
||||||
sequencer_addr: SocketAddr,
|
sequencer_addr: SocketAddr,
|
||||||
initial_data: &InitialData,
|
initial_data: &InitialData,
|
||||||
|
|||||||
@ -36,6 +36,7 @@ pub struct TestContext {
|
|||||||
sequencer_client: SequencerClient,
|
sequencer_client: SequencerClient,
|
||||||
indexer_client: IndexerClient,
|
indexer_client: IndexerClient,
|
||||||
wallet: WalletCore,
|
wallet: WalletCore,
|
||||||
|
wallet_password: String,
|
||||||
sequencer_handle: SequencerHandle,
|
sequencer_handle: SequencerHandle,
|
||||||
indexer_handle: IndexerHandle,
|
indexer_handle: IndexerHandle,
|
||||||
bedrock_compose: DockerCompose,
|
bedrock_compose: DockerCompose,
|
||||||
@ -78,9 +79,10 @@ impl TestContext {
|
|||||||
.await
|
.await
|
||||||
.context("Failed to setup Sequencer")?;
|
.context("Failed to setup Sequencer")?;
|
||||||
|
|
||||||
let (wallet, temp_wallet_dir) = Self::setup_wallet(sequencer_handle.addr(), &initial_data)
|
let (wallet, temp_wallet_dir, wallet_password) =
|
||||||
.await
|
Self::setup_wallet(sequencer_handle.addr(), &initial_data)
|
||||||
.context("Failed to setup wallet")?;
|
.await
|
||||||
|
.context("Failed to setup wallet")?;
|
||||||
|
|
||||||
let sequencer_url = config::addr_to_url(config::UrlProtocol::Http, sequencer_handle.addr())
|
let sequencer_url = config::addr_to_url(config::UrlProtocol::Http, sequencer_handle.addr())
|
||||||
.context("Failed to convert sequencer addr to URL")?;
|
.context("Failed to convert sequencer addr to URL")?;
|
||||||
@ -96,6 +98,7 @@ impl TestContext {
|
|||||||
sequencer_client,
|
sequencer_client,
|
||||||
indexer_client,
|
indexer_client,
|
||||||
wallet,
|
wallet,
|
||||||
|
wallet_password,
|
||||||
bedrock_compose,
|
bedrock_compose,
|
||||||
sequencer_handle,
|
sequencer_handle,
|
||||||
indexer_handle,
|
indexer_handle,
|
||||||
@ -221,7 +224,7 @@ impl TestContext {
|
|||||||
async fn setup_wallet(
|
async fn setup_wallet(
|
||||||
sequencer_addr: SocketAddr,
|
sequencer_addr: SocketAddr,
|
||||||
initial_data: &config::InitialData,
|
initial_data: &config::InitialData,
|
||||||
) -> Result<(WalletCore, TempDir)> {
|
) -> Result<(WalletCore, TempDir, String)> {
|
||||||
let config = config::wallet_config(sequencer_addr, initial_data)
|
let config = config::wallet_config(sequencer_addr, initial_data)
|
||||||
.context("Failed to create Wallet config")?;
|
.context("Failed to create Wallet config")?;
|
||||||
let config_serialized =
|
let config_serialized =
|
||||||
@ -250,7 +253,7 @@ impl TestContext {
|
|||||||
.await
|
.await
|
||||||
.context("Failed to store wallet persistent data")?;
|
.context("Failed to store wallet persistent data")?;
|
||||||
|
|
||||||
Ok((wallet, temp_wallet_dir))
|
Ok((wallet, temp_wallet_dir, wallet_password))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get reference to the wallet.
|
/// Get reference to the wallet.
|
||||||
@ -258,6 +261,10 @@ impl TestContext {
|
|||||||
&self.wallet
|
&self.wallet
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn wallet_password(&self) -> &str {
|
||||||
|
&self.wallet_password
|
||||||
|
}
|
||||||
|
|
||||||
/// Get mutable reference to the wallet.
|
/// Get mutable reference to the wallet.
|
||||||
pub fn wallet_mut(&mut self) -> &mut WalletCore {
|
pub fn wallet_mut(&mut self) -> &mut WalletCore {
|
||||||
&mut self.wallet
|
&mut self.wallet
|
||||||
@ -304,6 +311,7 @@ impl Drop for TestContext {
|
|||||||
sequencer_client: _,
|
sequencer_client: _,
|
||||||
indexer_client: _,
|
indexer_client: _,
|
||||||
wallet: _,
|
wallet: _,
|
||||||
|
wallet_password: _,
|
||||||
} = self;
|
} = self;
|
||||||
|
|
||||||
if sequencer_handle.is_finished() {
|
if sequencer_handle.is_finished() {
|
||||||
@ -341,6 +349,27 @@ impl Drop for TestContext {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A test context to be used in normal #[test] tests
|
||||||
|
pub struct BlockingTestContext {
|
||||||
|
ctx: Option<TestContext>,
|
||||||
|
runtime: tokio::runtime::Runtime,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BlockingTestContext {
|
||||||
|
pub fn new() -> Result<Self> {
|
||||||
|
let runtime = tokio::runtime::Runtime::new().unwrap();
|
||||||
|
let ctx = runtime.block_on(TestContext::new())?;
|
||||||
|
Ok(Self {
|
||||||
|
ctx: Some(ctx),
|
||||||
|
runtime,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn ctx(&self) -> &TestContext {
|
||||||
|
self.ctx.as_ref().expect("TestContext is set")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub struct TestContextBuilder {
|
pub struct TestContextBuilder {
|
||||||
initial_data: Option<config::InitialData>,
|
initial_data: Option<config::InitialData>,
|
||||||
sequencer_partial_config: Option<config::SequencerPartialConfig>,
|
sequencer_partial_config: Option<config::SequencerPartialConfig>,
|
||||||
@ -378,6 +407,19 @@ impl TestContextBuilder {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Drop for BlockingTestContext {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
let Self { ctx, runtime } = self;
|
||||||
|
|
||||||
|
// Ensure async cleanup of TestContext by blocking on its drop in the runtime.
|
||||||
|
runtime.block_on(async {
|
||||||
|
if let Some(ctx) = ctx.take() {
|
||||||
|
drop(ctx);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn format_public_account_id(account_id: AccountId) -> String {
|
pub fn format_public_account_id(account_id: AccountId) -> String {
|
||||||
format!("Public/{account_id}")
|
format!("Public/{account_id}")
|
||||||
}
|
}
|
||||||
|
|||||||
@ -37,13 +37,13 @@ async fn private_transfer_to_owned_account() -> Result<()> {
|
|||||||
|
|
||||||
let new_commitment1 = ctx
|
let new_commitment1 = ctx
|
||||||
.wallet()
|
.wallet()
|
||||||
.get_private_account_commitment(&from)
|
.get_private_account_commitment(from)
|
||||||
.context("Failed to get private account commitment for sender")?;
|
.context("Failed to get private account commitment for sender")?;
|
||||||
assert!(verify_commitment_is_in_state(new_commitment1, ctx.sequencer_client()).await);
|
assert!(verify_commitment_is_in_state(new_commitment1, ctx.sequencer_client()).await);
|
||||||
|
|
||||||
let new_commitment2 = ctx
|
let new_commitment2 = ctx
|
||||||
.wallet()
|
.wallet()
|
||||||
.get_private_account_commitment(&to)
|
.get_private_account_commitment(to)
|
||||||
.context("Failed to get private account commitment for receiver")?;
|
.context("Failed to get private account commitment for receiver")?;
|
||||||
assert!(verify_commitment_is_in_state(new_commitment2, ctx.sequencer_client()).await);
|
assert!(verify_commitment_is_in_state(new_commitment2, ctx.sequencer_client()).await);
|
||||||
|
|
||||||
@ -79,7 +79,7 @@ async fn private_transfer_to_foreign_account() -> Result<()> {
|
|||||||
|
|
||||||
let new_commitment1 = ctx
|
let new_commitment1 = ctx
|
||||||
.wallet()
|
.wallet()
|
||||||
.get_private_account_commitment(&from)
|
.get_private_account_commitment(from)
|
||||||
.context("Failed to get private account commitment for sender")?;
|
.context("Failed to get private account commitment for sender")?;
|
||||||
|
|
||||||
let tx = fetch_privacy_preserving_tx(ctx.sequencer_client(), tx_hash).await;
|
let tx = fetch_privacy_preserving_tx(ctx.sequencer_client(), tx_hash).await;
|
||||||
@ -105,7 +105,7 @@ async fn deshielded_transfer_to_public_account() -> Result<()> {
|
|||||||
// Check initial balance of the private sender
|
// Check initial balance of the private sender
|
||||||
let from_acc = ctx
|
let from_acc = ctx
|
||||||
.wallet()
|
.wallet()
|
||||||
.get_account_private(&from)
|
.get_account_private(from)
|
||||||
.context("Failed to get sender's private account")?;
|
.context("Failed to get sender's private account")?;
|
||||||
assert_eq!(from_acc.balance, 10000);
|
assert_eq!(from_acc.balance, 10000);
|
||||||
|
|
||||||
@ -124,11 +124,11 @@ async fn deshielded_transfer_to_public_account() -> Result<()> {
|
|||||||
|
|
||||||
let from_acc = ctx
|
let from_acc = ctx
|
||||||
.wallet()
|
.wallet()
|
||||||
.get_account_private(&from)
|
.get_account_private(from)
|
||||||
.context("Failed to get sender's private account")?;
|
.context("Failed to get sender's private account")?;
|
||||||
let new_commitment = ctx
|
let new_commitment = ctx
|
||||||
.wallet()
|
.wallet()
|
||||||
.get_private_account_commitment(&from)
|
.get_private_account_commitment(from)
|
||||||
.context("Failed to get private account commitment")?;
|
.context("Failed to get private account commitment")?;
|
||||||
assert!(verify_commitment_is_in_state(new_commitment, ctx.sequencer_client()).await);
|
assert!(verify_commitment_is_in_state(new_commitment, ctx.sequencer_client()).await);
|
||||||
|
|
||||||
@ -164,7 +164,7 @@ async fn private_transfer_to_owned_account_using_claiming_path() -> Result<()> {
|
|||||||
.wallet()
|
.wallet()
|
||||||
.storage()
|
.storage()
|
||||||
.user_data
|
.user_data
|
||||||
.get_private_account(&to_account_id)
|
.get_private_account(to_account_id)
|
||||||
.cloned()
|
.cloned()
|
||||||
.context("Failed to get private account")?;
|
.context("Failed to get private account")?;
|
||||||
|
|
||||||
@ -190,7 +190,7 @@ async fn private_transfer_to_owned_account_using_claiming_path() -> Result<()> {
|
|||||||
|
|
||||||
let new_commitment1 = ctx
|
let new_commitment1 = ctx
|
||||||
.wallet()
|
.wallet()
|
||||||
.get_private_account_commitment(&from)
|
.get_private_account_commitment(from)
|
||||||
.context("Failed to get private account commitment for sender")?;
|
.context("Failed to get private account commitment for sender")?;
|
||||||
assert_eq!(tx.message.new_commitments[0], new_commitment1);
|
assert_eq!(tx.message.new_commitments[0], new_commitment1);
|
||||||
|
|
||||||
@ -201,7 +201,7 @@ async fn private_transfer_to_owned_account_using_claiming_path() -> Result<()> {
|
|||||||
|
|
||||||
let to_res_acc = ctx
|
let to_res_acc = ctx
|
||||||
.wallet()
|
.wallet()
|
||||||
.get_account_private(&to_account_id)
|
.get_account_private(to_account_id)
|
||||||
.context("Failed to get recipient's private account")?;
|
.context("Failed to get recipient's private account")?;
|
||||||
assert_eq!(to_res_acc.balance, 100);
|
assert_eq!(to_res_acc.balance, 100);
|
||||||
|
|
||||||
@ -232,11 +232,11 @@ async fn shielded_transfer_to_owned_private_account() -> Result<()> {
|
|||||||
|
|
||||||
let acc_to = ctx
|
let acc_to = ctx
|
||||||
.wallet()
|
.wallet()
|
||||||
.get_account_private(&to)
|
.get_account_private(to)
|
||||||
.context("Failed to get receiver's private account")?;
|
.context("Failed to get receiver's private account")?;
|
||||||
let new_commitment = ctx
|
let new_commitment = ctx
|
||||||
.wallet()
|
.wallet()
|
||||||
.get_private_account_commitment(&to)
|
.get_private_account_commitment(to)
|
||||||
.context("Failed to get receiver's commitment")?;
|
.context("Failed to get receiver's commitment")?;
|
||||||
assert!(verify_commitment_is_in_state(new_commitment, ctx.sequencer_client()).await);
|
assert!(verify_commitment_is_in_state(new_commitment, ctx.sequencer_client()).await);
|
||||||
|
|
||||||
@ -321,7 +321,7 @@ async fn private_transfer_to_owned_account_continuous_run_path() -> Result<()> {
|
|||||||
.wallet()
|
.wallet()
|
||||||
.storage()
|
.storage()
|
||||||
.user_data
|
.user_data
|
||||||
.get_private_account(&to_account_id)
|
.get_private_account(to_account_id)
|
||||||
.cloned()
|
.cloned()
|
||||||
.context("Failed to get private account")?;
|
.context("Failed to get private account")?;
|
||||||
|
|
||||||
@ -354,7 +354,7 @@ async fn private_transfer_to_owned_account_continuous_run_path() -> Result<()> {
|
|||||||
// Verify receiver account balance
|
// Verify receiver account balance
|
||||||
let to_res_acc = ctx
|
let to_res_acc = ctx
|
||||||
.wallet()
|
.wallet()
|
||||||
.get_account_private(&to_account_id)
|
.get_account_private(to_account_id)
|
||||||
.context("Failed to get receiver account")?;
|
.context("Failed to get receiver account")?;
|
||||||
|
|
||||||
assert_eq!(to_res_acc.balance, 100);
|
assert_eq!(to_res_acc.balance, 100);
|
||||||
@ -385,13 +385,13 @@ async fn initialize_private_account() -> Result<()> {
|
|||||||
|
|
||||||
let new_commitment = ctx
|
let new_commitment = ctx
|
||||||
.wallet()
|
.wallet()
|
||||||
.get_private_account_commitment(&account_id)
|
.get_private_account_commitment(account_id)
|
||||||
.context("Failed to get private account commitment")?;
|
.context("Failed to get private account commitment")?;
|
||||||
assert!(verify_commitment_is_in_state(new_commitment, ctx.sequencer_client()).await);
|
assert!(verify_commitment_is_in_state(new_commitment, ctx.sequencer_client()).await);
|
||||||
|
|
||||||
let account = ctx
|
let account = ctx
|
||||||
.wallet()
|
.wallet()
|
||||||
.get_account_private(&account_id)
|
.get_account_private(account_id)
|
||||||
.context("Failed to get private account")?;
|
.context("Failed to get private account")?;
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
|
|||||||
@ -12,7 +12,7 @@ use tokio::test;
|
|||||||
use wallet::cli::{Command, programs::native_token_transfer::AuthTransferSubcommand};
|
use wallet::cli::{Command, programs::native_token_transfer::AuthTransferSubcommand};
|
||||||
|
|
||||||
/// Timeout in milliseconds to reliably await for block finalization
|
/// Timeout in milliseconds to reliably await for block finalization
|
||||||
const L2_TO_L1_TIMEOUT_MILLIS: u64 = 100000;
|
const L2_TO_L1_TIMEOUT_MILLIS: u64 = 220000;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
async fn indexer_test_run() -> Result<()> {
|
async fn indexer_test_run() -> Result<()> {
|
||||||
@ -130,13 +130,13 @@ async fn indexer_state_consistency() -> Result<()> {
|
|||||||
|
|
||||||
let new_commitment1 = ctx
|
let new_commitment1 = ctx
|
||||||
.wallet()
|
.wallet()
|
||||||
.get_private_account_commitment(&from)
|
.get_private_account_commitment(from)
|
||||||
.context("Failed to get private account commitment for sender")?;
|
.context("Failed to get private account commitment for sender")?;
|
||||||
assert!(verify_commitment_is_in_state(new_commitment1, ctx.sequencer_client()).await);
|
assert!(verify_commitment_is_in_state(new_commitment1, ctx.sequencer_client()).await);
|
||||||
|
|
||||||
let new_commitment2 = ctx
|
let new_commitment2 = ctx
|
||||||
.wallet()
|
.wallet()
|
||||||
.get_private_account_commitment(&to)
|
.get_private_account_commitment(to)
|
||||||
.context("Failed to get private account commitment for receiver")?;
|
.context("Failed to get private account commitment for receiver")?;
|
||||||
assert!(verify_commitment_is_in_state(new_commitment2, ctx.sequencer_client()).await);
|
assert!(verify_commitment_is_in_state(new_commitment2, ctx.sequencer_client()).await);
|
||||||
|
|
||||||
|
|||||||
@ -188,11 +188,11 @@ async fn restore_keys_from_seed() -> Result<()> {
|
|||||||
// Verify commitments exist for private accounts
|
// Verify commitments exist for private accounts
|
||||||
let comm1 = ctx
|
let comm1 = ctx
|
||||||
.wallet()
|
.wallet()
|
||||||
.get_private_account_commitment(&to_account_id1)
|
.get_private_account_commitment(to_account_id1)
|
||||||
.expect("Acc 1 commitment should exist");
|
.expect("Acc 1 commitment should exist");
|
||||||
let comm2 = ctx
|
let comm2 = ctx
|
||||||
.wallet()
|
.wallet()
|
||||||
.get_private_account_commitment(&to_account_id2)
|
.get_private_account_commitment(to_account_id2)
|
||||||
.expect("Acc 2 commitment should exist");
|
.expect("Acc 2 commitment should exist");
|
||||||
|
|
||||||
assert!(verify_commitment_is_in_state(comm1, ctx.sequencer_client()).await);
|
assert!(verify_commitment_is_in_state(comm1, ctx.sequencer_client()).await);
|
||||||
|
|||||||
@ -86,7 +86,7 @@ async fn claim_pinata_to_existing_private_account() -> Result<()> {
|
|||||||
|
|
||||||
let new_commitment = ctx
|
let new_commitment = ctx
|
||||||
.wallet()
|
.wallet()
|
||||||
.get_private_account_commitment(&ctx.existing_private_accounts()[0])
|
.get_private_account_commitment(ctx.existing_private_accounts()[0])
|
||||||
.context("Failed to get private account commitment")?;
|
.context("Failed to get private account commitment")?;
|
||||||
assert!(verify_commitment_is_in_state(new_commitment, ctx.sequencer_client()).await);
|
assert!(verify_commitment_is_in_state(new_commitment, ctx.sequencer_client()).await);
|
||||||
|
|
||||||
@ -135,7 +135,7 @@ async fn claim_pinata_to_new_private_account() -> Result<()> {
|
|||||||
|
|
||||||
let new_commitment = ctx
|
let new_commitment = ctx
|
||||||
.wallet()
|
.wallet()
|
||||||
.get_private_account_commitment(&winner_account_id)
|
.get_private_account_commitment(winner_account_id)
|
||||||
.context("Failed to get private account commitment")?;
|
.context("Failed to get private account commitment")?;
|
||||||
assert!(verify_commitment_is_in_state(new_commitment, ctx.sequencer_client()).await);
|
assert!(verify_commitment_is_in_state(new_commitment, ctx.sequencer_client()).await);
|
||||||
|
|
||||||
@ -157,7 +157,7 @@ async fn claim_pinata_to_new_private_account() -> Result<()> {
|
|||||||
|
|
||||||
let new_commitment = ctx
|
let new_commitment = ctx
|
||||||
.wallet()
|
.wallet()
|
||||||
.get_private_account_commitment(&winner_account_id)
|
.get_private_account_commitment(winner_account_id)
|
||||||
.context("Failed to get private account commitment")?;
|
.context("Failed to get private account commitment")?;
|
||||||
assert!(verify_commitment_is_in_state(new_commitment, ctx.sequencer_client()).await);
|
assert!(verify_commitment_is_in_state(new_commitment, ctx.sequencer_client()).await);
|
||||||
|
|
||||||
|
|||||||
@ -65,8 +65,8 @@ async fn create_and_transfer_public_token() -> Result<()> {
|
|||||||
let subcommand = TokenProgramAgnosticSubcommand::New {
|
let subcommand = TokenProgramAgnosticSubcommand::New {
|
||||||
definition_account_id: format_public_account_id(definition_account_id),
|
definition_account_id: format_public_account_id(definition_account_id),
|
||||||
supply_account_id: format_public_account_id(supply_account_id),
|
supply_account_id: format_public_account_id(supply_account_id),
|
||||||
name: "A NAME".to_string(),
|
name: name.clone(),
|
||||||
total_supply: 37,
|
total_supply,
|
||||||
};
|
};
|
||||||
wallet::cli::execute_subcommand(ctx.wallet_mut(), Command::Token(subcommand)).await?;
|
wallet::cli::execute_subcommand(ctx.wallet_mut(), Command::Token(subcommand)).await?;
|
||||||
|
|
||||||
@ -161,7 +161,7 @@ async fn create_and_transfer_public_token() -> Result<()> {
|
|||||||
let subcommand = TokenProgramAgnosticSubcommand::Burn {
|
let subcommand = TokenProgramAgnosticSubcommand::Burn {
|
||||||
definition: format_public_account_id(definition_account_id),
|
definition: format_public_account_id(definition_account_id),
|
||||||
holder: format_public_account_id(recipient_account_id),
|
holder: format_public_account_id(recipient_account_id),
|
||||||
amount: 3,
|
amount: burn_amount,
|
||||||
};
|
};
|
||||||
|
|
||||||
wallet::cli::execute_subcommand(ctx.wallet_mut(), Command::Token(subcommand)).await?;
|
wallet::cli::execute_subcommand(ctx.wallet_mut(), Command::Token(subcommand)).await?;
|
||||||
@ -304,8 +304,8 @@ async fn create_and_transfer_token_with_private_supply() -> Result<()> {
|
|||||||
let subcommand = TokenProgramAgnosticSubcommand::New {
|
let subcommand = TokenProgramAgnosticSubcommand::New {
|
||||||
definition_account_id: format_public_account_id(definition_account_id),
|
definition_account_id: format_public_account_id(definition_account_id),
|
||||||
supply_account_id: format_private_account_id(supply_account_id),
|
supply_account_id: format_private_account_id(supply_account_id),
|
||||||
name: "A NAME".to_string(),
|
name: name.clone(),
|
||||||
total_supply: 37,
|
total_supply,
|
||||||
};
|
};
|
||||||
|
|
||||||
wallet::cli::execute_subcommand(ctx.wallet_mut(), Command::Token(subcommand)).await?;
|
wallet::cli::execute_subcommand(ctx.wallet_mut(), Command::Token(subcommand)).await?;
|
||||||
@ -333,7 +333,7 @@ async fn create_and_transfer_token_with_private_supply() -> Result<()> {
|
|||||||
|
|
||||||
let new_commitment1 = ctx
|
let new_commitment1 = ctx
|
||||||
.wallet()
|
.wallet()
|
||||||
.get_private_account_commitment(&supply_account_id)
|
.get_private_account_commitment(supply_account_id)
|
||||||
.context("Failed to get supply account commitment")?;
|
.context("Failed to get supply account commitment")?;
|
||||||
assert!(verify_commitment_is_in_state(new_commitment1, ctx.sequencer_client()).await);
|
assert!(verify_commitment_is_in_state(new_commitment1, ctx.sequencer_client()).await);
|
||||||
|
|
||||||
@ -354,13 +354,13 @@ async fn create_and_transfer_token_with_private_supply() -> Result<()> {
|
|||||||
|
|
||||||
let new_commitment1 = ctx
|
let new_commitment1 = ctx
|
||||||
.wallet()
|
.wallet()
|
||||||
.get_private_account_commitment(&supply_account_id)
|
.get_private_account_commitment(supply_account_id)
|
||||||
.context("Failed to get supply account commitment")?;
|
.context("Failed to get supply account commitment")?;
|
||||||
assert!(verify_commitment_is_in_state(new_commitment1, ctx.sequencer_client()).await);
|
assert!(verify_commitment_is_in_state(new_commitment1, ctx.sequencer_client()).await);
|
||||||
|
|
||||||
let new_commitment2 = ctx
|
let new_commitment2 = ctx
|
||||||
.wallet()
|
.wallet()
|
||||||
.get_private_account_commitment(&recipient_account_id)
|
.get_private_account_commitment(recipient_account_id)
|
||||||
.context("Failed to get recipient account commitment")?;
|
.context("Failed to get recipient account commitment")?;
|
||||||
assert!(verify_commitment_is_in_state(new_commitment2, ctx.sequencer_client()).await);
|
assert!(verify_commitment_is_in_state(new_commitment2, ctx.sequencer_client()).await);
|
||||||
|
|
||||||
@ -369,7 +369,7 @@ async fn create_and_transfer_token_with_private_supply() -> Result<()> {
|
|||||||
let subcommand = TokenProgramAgnosticSubcommand::Burn {
|
let subcommand = TokenProgramAgnosticSubcommand::Burn {
|
||||||
definition: format_public_account_id(definition_account_id),
|
definition: format_public_account_id(definition_account_id),
|
||||||
holder: format_private_account_id(recipient_account_id),
|
holder: format_private_account_id(recipient_account_id),
|
||||||
amount: 3,
|
amount: burn_amount,
|
||||||
};
|
};
|
||||||
|
|
||||||
wallet::cli::execute_subcommand(ctx.wallet_mut(), Command::Token(subcommand)).await?;
|
wallet::cli::execute_subcommand(ctx.wallet_mut(), Command::Token(subcommand)).await?;
|
||||||
@ -396,14 +396,14 @@ async fn create_and_transfer_token_with_private_supply() -> Result<()> {
|
|||||||
|
|
||||||
let new_commitment2 = ctx
|
let new_commitment2 = ctx
|
||||||
.wallet()
|
.wallet()
|
||||||
.get_private_account_commitment(&recipient_account_id)
|
.get_private_account_commitment(recipient_account_id)
|
||||||
.context("Failed to get recipient account commitment")?;
|
.context("Failed to get recipient account commitment")?;
|
||||||
assert!(verify_commitment_is_in_state(new_commitment2, ctx.sequencer_client()).await);
|
assert!(verify_commitment_is_in_state(new_commitment2, ctx.sequencer_client()).await);
|
||||||
|
|
||||||
// Check the recipient account balance after burn
|
// Check the recipient account balance after burn
|
||||||
let recipient_acc = ctx
|
let recipient_acc = ctx
|
||||||
.wallet()
|
.wallet()
|
||||||
.get_account_private(&recipient_account_id)
|
.get_account_private(recipient_account_id)
|
||||||
.context("Failed to get recipient account")?;
|
.context("Failed to get recipient account")?;
|
||||||
let token_holding = TokenHolding::try_from(&recipient_acc.data)?;
|
let token_holding = TokenHolding::try_from(&recipient_acc.data)?;
|
||||||
|
|
||||||
@ -460,8 +460,8 @@ async fn create_token_with_private_definition() -> Result<()> {
|
|||||||
let subcommand = TokenProgramAgnosticSubcommand::New {
|
let subcommand = TokenProgramAgnosticSubcommand::New {
|
||||||
definition_account_id: format_private_account_id(definition_account_id),
|
definition_account_id: format_private_account_id(definition_account_id),
|
||||||
supply_account_id: format_public_account_id(supply_account_id),
|
supply_account_id: format_public_account_id(supply_account_id),
|
||||||
name: "A NAME".to_string(),
|
name: name.clone(),
|
||||||
total_supply: 37,
|
total_supply,
|
||||||
};
|
};
|
||||||
|
|
||||||
wallet::cli::execute_subcommand(ctx.wallet_mut(), Command::Token(subcommand)).await?;
|
wallet::cli::execute_subcommand(ctx.wallet_mut(), Command::Token(subcommand)).await?;
|
||||||
@ -472,7 +472,7 @@ async fn create_token_with_private_definition() -> Result<()> {
|
|||||||
// Verify private definition commitment
|
// Verify private definition commitment
|
||||||
let new_commitment = ctx
|
let new_commitment = ctx
|
||||||
.wallet()
|
.wallet()
|
||||||
.get_private_account_commitment(&definition_account_id)
|
.get_private_account_commitment(definition_account_id)
|
||||||
.context("Failed to get definition commitment")?;
|
.context("Failed to get definition commitment")?;
|
||||||
assert!(verify_commitment_is_in_state(new_commitment, ctx.sequencer_client()).await);
|
assert!(verify_commitment_is_in_state(new_commitment, ctx.sequencer_client()).await);
|
||||||
|
|
||||||
@ -537,7 +537,7 @@ async fn create_token_with_private_definition() -> Result<()> {
|
|||||||
// Verify definition account has updated supply
|
// Verify definition account has updated supply
|
||||||
let definition_acc = ctx
|
let definition_acc = ctx
|
||||||
.wallet()
|
.wallet()
|
||||||
.get_account_private(&definition_account_id)
|
.get_account_private(definition_account_id)
|
||||||
.context("Failed to get definition account")?;
|
.context("Failed to get definition account")?;
|
||||||
let token_definition = TokenDefinition::try_from(&definition_acc.data)?;
|
let token_definition = TokenDefinition::try_from(&definition_acc.data)?;
|
||||||
|
|
||||||
@ -584,14 +584,14 @@ async fn create_token_with_private_definition() -> Result<()> {
|
|||||||
// Verify private recipient commitment
|
// Verify private recipient commitment
|
||||||
let new_commitment = ctx
|
let new_commitment = ctx
|
||||||
.wallet()
|
.wallet()
|
||||||
.get_private_account_commitment(&recipient_account_id_private)
|
.get_private_account_commitment(recipient_account_id_private)
|
||||||
.context("Failed to get recipient commitment")?;
|
.context("Failed to get recipient commitment")?;
|
||||||
assert!(verify_commitment_is_in_state(new_commitment, ctx.sequencer_client()).await);
|
assert!(verify_commitment_is_in_state(new_commitment, ctx.sequencer_client()).await);
|
||||||
|
|
||||||
// Verify private recipient balance
|
// Verify private recipient balance
|
||||||
let recipient_acc_private = ctx
|
let recipient_acc_private = ctx
|
||||||
.wallet()
|
.wallet()
|
||||||
.get_account_private(&recipient_account_id_private)
|
.get_account_private(recipient_account_id_private)
|
||||||
.context("Failed to get private recipient account")?;
|
.context("Failed to get private recipient account")?;
|
||||||
let token_holding = TokenHolding::try_from(&recipient_acc_private.data)?;
|
let token_holding = TokenHolding::try_from(&recipient_acc_private.data)?;
|
||||||
|
|
||||||
@ -639,12 +639,13 @@ async fn create_token_with_private_definition_and_supply() -> Result<()> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Create token with both private definition and supply
|
// Create token with both private definition and supply
|
||||||
|
let name = "A NAME".to_string();
|
||||||
let total_supply = 37;
|
let total_supply = 37;
|
||||||
let subcommand = TokenProgramAgnosticSubcommand::New {
|
let subcommand = TokenProgramAgnosticSubcommand::New {
|
||||||
definition_account_id: format_private_account_id(definition_account_id),
|
definition_account_id: format_private_account_id(definition_account_id),
|
||||||
supply_account_id: format_private_account_id(supply_account_id),
|
supply_account_id: format_private_account_id(supply_account_id),
|
||||||
name: "A NAME".to_string(),
|
name,
|
||||||
total_supply: 37,
|
total_supply,
|
||||||
};
|
};
|
||||||
|
|
||||||
wallet::cli::execute_subcommand(ctx.wallet_mut(), Command::Token(subcommand)).await?;
|
wallet::cli::execute_subcommand(ctx.wallet_mut(), Command::Token(subcommand)).await?;
|
||||||
@ -655,21 +656,21 @@ async fn create_token_with_private_definition_and_supply() -> Result<()> {
|
|||||||
// Verify definition commitment
|
// Verify definition commitment
|
||||||
let definition_commitment = ctx
|
let definition_commitment = ctx
|
||||||
.wallet()
|
.wallet()
|
||||||
.get_private_account_commitment(&definition_account_id)
|
.get_private_account_commitment(definition_account_id)
|
||||||
.context("Failed to get definition commitment")?;
|
.context("Failed to get definition commitment")?;
|
||||||
assert!(verify_commitment_is_in_state(definition_commitment, ctx.sequencer_client()).await);
|
assert!(verify_commitment_is_in_state(definition_commitment, ctx.sequencer_client()).await);
|
||||||
|
|
||||||
// Verify supply commitment
|
// Verify supply commitment
|
||||||
let supply_commitment = ctx
|
let supply_commitment = ctx
|
||||||
.wallet()
|
.wallet()
|
||||||
.get_private_account_commitment(&supply_account_id)
|
.get_private_account_commitment(supply_account_id)
|
||||||
.context("Failed to get supply commitment")?;
|
.context("Failed to get supply commitment")?;
|
||||||
assert!(verify_commitment_is_in_state(supply_commitment, ctx.sequencer_client()).await);
|
assert!(verify_commitment_is_in_state(supply_commitment, ctx.sequencer_client()).await);
|
||||||
|
|
||||||
// Verify supply balance
|
// Verify supply balance
|
||||||
let supply_acc = ctx
|
let supply_acc = ctx
|
||||||
.wallet()
|
.wallet()
|
||||||
.get_account_private(&supply_account_id)
|
.get_account_private(supply_account_id)
|
||||||
.context("Failed to get supply account")?;
|
.context("Failed to get supply account")?;
|
||||||
let token_holding = TokenHolding::try_from(&supply_acc.data)?;
|
let token_holding = TokenHolding::try_from(&supply_acc.data)?;
|
||||||
|
|
||||||
@ -712,20 +713,20 @@ async fn create_token_with_private_definition_and_supply() -> Result<()> {
|
|||||||
// Verify both commitments updated
|
// Verify both commitments updated
|
||||||
let supply_commitment = ctx
|
let supply_commitment = ctx
|
||||||
.wallet()
|
.wallet()
|
||||||
.get_private_account_commitment(&supply_account_id)
|
.get_private_account_commitment(supply_account_id)
|
||||||
.context("Failed to get supply commitment")?;
|
.context("Failed to get supply commitment")?;
|
||||||
assert!(verify_commitment_is_in_state(supply_commitment, ctx.sequencer_client()).await);
|
assert!(verify_commitment_is_in_state(supply_commitment, ctx.sequencer_client()).await);
|
||||||
|
|
||||||
let recipient_commitment = ctx
|
let recipient_commitment = ctx
|
||||||
.wallet()
|
.wallet()
|
||||||
.get_private_account_commitment(&recipient_account_id)
|
.get_private_account_commitment(recipient_account_id)
|
||||||
.context("Failed to get recipient commitment")?;
|
.context("Failed to get recipient commitment")?;
|
||||||
assert!(verify_commitment_is_in_state(recipient_commitment, ctx.sequencer_client()).await);
|
assert!(verify_commitment_is_in_state(recipient_commitment, ctx.sequencer_client()).await);
|
||||||
|
|
||||||
// Verify balances
|
// Verify balances
|
||||||
let supply_acc = ctx
|
let supply_acc = ctx
|
||||||
.wallet()
|
.wallet()
|
||||||
.get_account_private(&supply_account_id)
|
.get_account_private(supply_account_id)
|
||||||
.context("Failed to get supply account")?;
|
.context("Failed to get supply account")?;
|
||||||
let token_holding = TokenHolding::try_from(&supply_acc.data)?;
|
let token_holding = TokenHolding::try_from(&supply_acc.data)?;
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
@ -738,7 +739,7 @@ async fn create_token_with_private_definition_and_supply() -> Result<()> {
|
|||||||
|
|
||||||
let recipient_acc = ctx
|
let recipient_acc = ctx
|
||||||
.wallet()
|
.wallet()
|
||||||
.get_account_private(&recipient_account_id)
|
.get_account_private(recipient_account_id)
|
||||||
.context("Failed to get recipient account")?;
|
.context("Failed to get recipient account")?;
|
||||||
let token_holding = TokenHolding::try_from(&recipient_acc.data)?;
|
let token_holding = TokenHolding::try_from(&recipient_acc.data)?;
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
@ -798,12 +799,13 @@ async fn shielded_token_transfer() -> Result<()> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Create token
|
// Create token
|
||||||
|
let name = "A NAME".to_string();
|
||||||
let total_supply = 37;
|
let total_supply = 37;
|
||||||
let subcommand = TokenProgramAgnosticSubcommand::New {
|
let subcommand = TokenProgramAgnosticSubcommand::New {
|
||||||
definition_account_id: format_public_account_id(definition_account_id),
|
definition_account_id: format_public_account_id(definition_account_id),
|
||||||
supply_account_id: format_public_account_id(supply_account_id),
|
supply_account_id: format_public_account_id(supply_account_id),
|
||||||
name: "A NAME".to_string(),
|
name,
|
||||||
total_supply: 37,
|
total_supply,
|
||||||
};
|
};
|
||||||
|
|
||||||
wallet::cli::execute_subcommand(ctx.wallet_mut(), Command::Token(subcommand)).await?;
|
wallet::cli::execute_subcommand(ctx.wallet_mut(), Command::Token(subcommand)).await?;
|
||||||
@ -844,14 +846,14 @@ async fn shielded_token_transfer() -> Result<()> {
|
|||||||
// Verify recipient commitment exists
|
// Verify recipient commitment exists
|
||||||
let new_commitment = ctx
|
let new_commitment = ctx
|
||||||
.wallet()
|
.wallet()
|
||||||
.get_private_account_commitment(&recipient_account_id)
|
.get_private_account_commitment(recipient_account_id)
|
||||||
.context("Failed to get recipient commitment")?;
|
.context("Failed to get recipient commitment")?;
|
||||||
assert!(verify_commitment_is_in_state(new_commitment, ctx.sequencer_client()).await);
|
assert!(verify_commitment_is_in_state(new_commitment, ctx.sequencer_client()).await);
|
||||||
|
|
||||||
// Verify recipient balance
|
// Verify recipient balance
|
||||||
let recipient_acc = ctx
|
let recipient_acc = ctx
|
||||||
.wallet()
|
.wallet()
|
||||||
.get_account_private(&recipient_account_id)
|
.get_account_private(recipient_account_id)
|
||||||
.context("Failed to get recipient account")?;
|
.context("Failed to get recipient account")?;
|
||||||
let token_holding = TokenHolding::try_from(&recipient_acc.data)?;
|
let token_holding = TokenHolding::try_from(&recipient_acc.data)?;
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
@ -911,12 +913,13 @@ async fn deshielded_token_transfer() -> Result<()> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Create token with private supply
|
// Create token with private supply
|
||||||
|
let name = "A NAME".to_string();
|
||||||
let total_supply = 37;
|
let total_supply = 37;
|
||||||
let subcommand = TokenProgramAgnosticSubcommand::New {
|
let subcommand = TokenProgramAgnosticSubcommand::New {
|
||||||
definition_account_id: format_public_account_id(definition_account_id),
|
definition_account_id: format_public_account_id(definition_account_id),
|
||||||
supply_account_id: format_private_account_id(supply_account_id),
|
supply_account_id: format_private_account_id(supply_account_id),
|
||||||
name: "A NAME".to_string(),
|
name,
|
||||||
total_supply: 37,
|
total_supply,
|
||||||
};
|
};
|
||||||
|
|
||||||
wallet::cli::execute_subcommand(ctx.wallet_mut(), Command::Token(subcommand)).await?;
|
wallet::cli::execute_subcommand(ctx.wallet_mut(), Command::Token(subcommand)).await?;
|
||||||
@ -942,14 +945,14 @@ async fn deshielded_token_transfer() -> Result<()> {
|
|||||||
// Verify supply account commitment exists
|
// Verify supply account commitment exists
|
||||||
let new_commitment = ctx
|
let new_commitment = ctx
|
||||||
.wallet()
|
.wallet()
|
||||||
.get_private_account_commitment(&supply_account_id)
|
.get_private_account_commitment(supply_account_id)
|
||||||
.context("Failed to get supply commitment")?;
|
.context("Failed to get supply commitment")?;
|
||||||
assert!(verify_commitment_is_in_state(new_commitment, ctx.sequencer_client()).await);
|
assert!(verify_commitment_is_in_state(new_commitment, ctx.sequencer_client()).await);
|
||||||
|
|
||||||
// Verify supply balance
|
// Verify supply balance
|
||||||
let supply_acc = ctx
|
let supply_acc = ctx
|
||||||
.wallet()
|
.wallet()
|
||||||
.get_account_private(&supply_account_id)
|
.get_account_private(supply_account_id)
|
||||||
.context("Failed to get supply account")?;
|
.context("Failed to get supply account")?;
|
||||||
let token_holding = TokenHolding::try_from(&supply_acc.data)?;
|
let token_holding = TokenHolding::try_from(&supply_acc.data)?;
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
@ -1011,11 +1014,13 @@ async fn token_claiming_path_with_private_accounts() -> Result<()> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Create token
|
// Create token
|
||||||
|
let name = "A NAME".to_string();
|
||||||
|
let total_supply = 37;
|
||||||
let subcommand = TokenProgramAgnosticSubcommand::New {
|
let subcommand = TokenProgramAgnosticSubcommand::New {
|
||||||
definition_account_id: format_private_account_id(definition_account_id),
|
definition_account_id: format_private_account_id(definition_account_id),
|
||||||
supply_account_id: format_private_account_id(supply_account_id),
|
supply_account_id: format_private_account_id(supply_account_id),
|
||||||
name: "A NAME".to_string(),
|
name,
|
||||||
total_supply: 37,
|
total_supply,
|
||||||
};
|
};
|
||||||
|
|
||||||
wallet::cli::execute_subcommand(ctx.wallet_mut(), Command::Token(subcommand)).await?;
|
wallet::cli::execute_subcommand(ctx.wallet_mut(), Command::Token(subcommand)).await?;
|
||||||
@ -1041,7 +1046,7 @@ async fn token_claiming_path_with_private_accounts() -> Result<()> {
|
|||||||
.wallet()
|
.wallet()
|
||||||
.storage()
|
.storage()
|
||||||
.user_data
|
.user_data
|
||||||
.get_private_account(&recipient_account_id)
|
.get_private_account(recipient_account_id)
|
||||||
.cloned()
|
.cloned()
|
||||||
.context("Failed to get private account keys")?;
|
.context("Failed to get private account keys")?;
|
||||||
|
|
||||||
@ -1067,14 +1072,14 @@ async fn token_claiming_path_with_private_accounts() -> Result<()> {
|
|||||||
// Verify commitment exists
|
// Verify commitment exists
|
||||||
let recipient_commitment = ctx
|
let recipient_commitment = ctx
|
||||||
.wallet()
|
.wallet()
|
||||||
.get_private_account_commitment(&recipient_account_id)
|
.get_private_account_commitment(recipient_account_id)
|
||||||
.context("Failed to get recipient commitment")?;
|
.context("Failed to get recipient commitment")?;
|
||||||
assert!(verify_commitment_is_in_state(recipient_commitment, ctx.sequencer_client()).await);
|
assert!(verify_commitment_is_in_state(recipient_commitment, ctx.sequencer_client()).await);
|
||||||
|
|
||||||
// Verify balance
|
// Verify balance
|
||||||
let recipient_acc = ctx
|
let recipient_acc = ctx
|
||||||
.wallet()
|
.wallet()
|
||||||
.get_account_private(&recipient_account_id)
|
.get_account_private(recipient_account_id)
|
||||||
.context("Failed to get recipient account")?;
|
.context("Failed to get recipient account")?;
|
||||||
let token_holding = TokenHolding::try_from(&recipient_acc.data)?;
|
let token_holding = TokenHolding::try_from(&recipient_acc.data)?;
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@ -5,6 +5,8 @@ edition = "2024"
|
|||||||
license = { workspace = true }
|
license = { workspace = true }
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
secp256k1 = "0.31.1"
|
||||||
|
|
||||||
nssa.workspace = true
|
nssa.workspace = true
|
||||||
nssa_core.workspace = true
|
nssa_core.workspace = true
|
||||||
common.workspace = true
|
common.workspace = true
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
use secp256k1::Scalar;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use crate::key_management::key_tree::traits::KeyNode;
|
use crate::key_management::key_tree::traits::KeyNode;
|
||||||
@ -11,9 +12,32 @@ pub struct ChildKeysPublic {
|
|||||||
pub cci: Option<u32>,
|
pub cci: Option<u32>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl ChildKeysPublic {
|
||||||
|
fn compute_hash_value(&self, cci: u32) -> [u8; 64] {
|
||||||
|
let mut hash_input = vec![];
|
||||||
|
|
||||||
|
match ((2u32).pow(31)).cmp(&cci) {
|
||||||
|
// Non-harden
|
||||||
|
std::cmp::Ordering::Greater => {
|
||||||
|
hash_input.extend_from_slice(self.cpk.value());
|
||||||
|
hash_input.extend_from_slice(&cci.to_le_bytes());
|
||||||
|
|
||||||
|
hmac_sha512::HMAC::mac(hash_input, self.ccc)
|
||||||
|
}
|
||||||
|
// Harden
|
||||||
|
_ => {
|
||||||
|
hash_input.extend_from_slice(self.csk.value());
|
||||||
|
hash_input.extend_from_slice(&(cci).to_le_bytes());
|
||||||
|
|
||||||
|
hmac_sha512::HMAC::mac(hash_input, self.ccc)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl KeyNode for ChildKeysPublic {
|
impl KeyNode for ChildKeysPublic {
|
||||||
fn root(seed: [u8; 64]) -> Self {
|
fn root(seed: [u8; 64]) -> Self {
|
||||||
let hash_value = hmac_sha512::HMAC::mac(seed, "NSSA_master_pub");
|
let hash_value = hmac_sha512::HMAC::mac(seed, "LEE_master_pub");
|
||||||
|
|
||||||
let csk = nssa::PrivateKey::try_new(*hash_value.first_chunk::<32>().unwrap()).unwrap();
|
let csk = nssa::PrivateKey::try_new(*hash_value.first_chunk::<32>().unwrap()).unwrap();
|
||||||
let ccc = *hash_value.last_chunk::<32>().unwrap();
|
let ccc = *hash_value.last_chunk::<32>().unwrap();
|
||||||
@ -28,21 +52,30 @@ impl KeyNode for ChildKeysPublic {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn nth_child(&self, cci: u32) -> Self {
|
fn nth_child(&self, cci: u32) -> Self {
|
||||||
let mut hash_input = vec![];
|
let hash_value = self.compute_hash_value(cci);
|
||||||
hash_input.extend_from_slice(self.csk.value());
|
|
||||||
hash_input.extend_from_slice(&cci.to_le_bytes());
|
|
||||||
|
|
||||||
let hash_value = hmac_sha512::HMAC::mac(&hash_input, self.ccc);
|
let csk = secp256k1::SecretKey::from_byte_array(
|
||||||
|
|
||||||
let csk = nssa::PrivateKey::try_new(
|
|
||||||
*hash_value
|
*hash_value
|
||||||
.first_chunk::<32>()
|
.first_chunk::<32>()
|
||||||
.expect("hash_value is 64 bytes, must be safe to get first 32"),
|
.expect("hash_value is 64 bytes, must be safe to get first 32"),
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
|
let csk = nssa::PrivateKey::try_new(
|
||||||
|
csk.add_tweak(&Scalar::from_le_bytes(*self.csk.value()).unwrap())
|
||||||
|
.expect("Expect a valid Scalar")
|
||||||
|
.secret_bytes(),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
if secp256k1::constants::CURVE_ORDER < *csk.value() {
|
||||||
|
panic!("Secret key cannot exceed curve order");
|
||||||
|
}
|
||||||
|
|
||||||
let ccc = *hash_value
|
let ccc = *hash_value
|
||||||
.last_chunk::<32>()
|
.last_chunk::<32>()
|
||||||
.expect("hash_value is 64 bytes, must be safe to get last 32");
|
.expect("hash_value is 64 bytes, must be safe to get last 32");
|
||||||
|
|
||||||
let cpk = nssa::PublicKey::new_from_private_key(&csk);
|
let cpk = nssa::PublicKey::new_from_private_key(&csk);
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
@ -74,59 +107,152 @@ impl<'a> From<&'a ChildKeysPublic> for &'a nssa::PrivateKey {
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
|
use nssa::{PrivateKey, PublicKey};
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_keys_deterministic_generation() {
|
fn test_master_keys_generation() {
|
||||||
let root_keys = ChildKeysPublic::root([42; 64]);
|
let seed = [
|
||||||
let child_keys = root_keys.nth_child(5);
|
88, 189, 37, 237, 199, 125, 151, 226, 69, 153, 165, 113, 191, 69, 188, 221, 9, 34, 173,
|
||||||
|
134, 61, 109, 34, 103, 121, 39, 237, 14, 107, 194, 24, 194, 191, 14, 237, 185, 12, 87,
|
||||||
|
22, 227, 38, 71, 17, 144, 251, 118, 217, 115, 33, 222, 201, 61, 203, 246, 121, 214, 6,
|
||||||
|
187, 148, 92, 44, 253, 210, 37,
|
||||||
|
];
|
||||||
|
let keys = ChildKeysPublic::root(seed);
|
||||||
|
|
||||||
assert_eq!(root_keys.cci, None);
|
let expected_ccc = [
|
||||||
assert_eq!(child_keys.cci, Some(5));
|
238, 94, 84, 154, 56, 224, 80, 218, 133, 249, 179, 222, 9, 24, 17, 252, 120, 127, 222,
|
||||||
|
13, 146, 126, 232, 239, 113, 9, 194, 219, 190, 48, 187, 155,
|
||||||
|
];
|
||||||
|
|
||||||
assert_eq!(
|
let expected_csk: PrivateKey = PrivateKey::try_new([
|
||||||
root_keys.ccc,
|
40, 35, 239, 19, 53, 178, 250, 55, 115, 12, 34, 3, 153, 153, 72, 170, 190, 36, 172, 36,
|
||||||
[
|
202, 148, 181, 228, 35, 222, 58, 84, 156, 24, 146, 86,
|
||||||
61, 30, 91, 26, 133, 91, 236, 192, 231, 53, 186, 139, 11, 221, 202, 11, 178, 215,
|
])
|
||||||
254, 103, 191, 60, 117, 112, 1, 226, 31, 156, 83, 104, 150, 224
|
.unwrap();
|
||||||
]
|
let expected_cpk: PublicKey = PublicKey::try_new([
|
||||||
);
|
219, 141, 130, 105, 11, 203, 187, 124, 112, 75, 223, 22, 11, 164, 153, 127, 59, 247,
|
||||||
assert_eq!(
|
244, 166, 75, 66, 242, 224, 35, 156, 161, 75, 41, 51, 76, 245,
|
||||||
child_keys.ccc,
|
])
|
||||||
[
|
.unwrap();
|
||||||
67, 26, 102, 68, 189, 155, 102, 80, 199, 188, 112, 142, 207, 157, 36, 210, 48, 224,
|
|
||||||
35, 6, 112, 180, 11, 190, 135, 218, 9, 14, 84, 231, 58, 98
|
assert!(expected_ccc == keys.ccc);
|
||||||
]
|
assert!(expected_csk == keys.csk);
|
||||||
|
assert!(expected_cpk == keys.cpk);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_harden_child_keys_generation() {
|
||||||
|
let seed = [
|
||||||
|
88, 189, 37, 237, 199, 125, 151, 226, 69, 153, 165, 113, 191, 69, 188, 221, 9, 34, 173,
|
||||||
|
134, 61, 109, 34, 103, 121, 39, 237, 14, 107, 194, 24, 194, 191, 14, 237, 185, 12, 87,
|
||||||
|
22, 227, 38, 71, 17, 144, 251, 118, 217, 115, 33, 222, 201, 61, 203, 246, 121, 214, 6,
|
||||||
|
187, 148, 92, 44, 253, 210, 37,
|
||||||
|
];
|
||||||
|
let root_keys = ChildKeysPublic::root(seed);
|
||||||
|
let cci = (2u32).pow(31) + 13;
|
||||||
|
let child_keys = ChildKeysPublic::nth_child(&root_keys, cci);
|
||||||
|
|
||||||
|
print!(
|
||||||
|
"{} {}",
|
||||||
|
child_keys.csk.value()[0],
|
||||||
|
child_keys.csk.value()[1]
|
||||||
);
|
);
|
||||||
|
|
||||||
assert_eq!(
|
let expected_ccc = [
|
||||||
root_keys.csk.value(),
|
126, 175, 244, 41, 41, 173, 134, 103, 139, 140, 195, 86, 194, 147, 116, 48, 71, 107,
|
||||||
&[
|
253, 235, 114, 139, 60, 115, 226, 205, 215, 248, 240, 190, 196, 6,
|
||||||
241, 82, 246, 237, 62, 130, 116, 47, 189, 112, 99, 67, 178, 40, 115, 245, 141, 193,
|
];
|
||||||
77, 164, 243, 76, 222, 64, 50, 146, 23, 145, 91, 164, 92, 116
|
|
||||||
]
|
let expected_csk: PrivateKey = PrivateKey::try_new([
|
||||||
);
|
128, 148, 53, 165, 222, 155, 163, 108, 186, 182, 124, 67, 90, 86, 59, 123, 95, 224,
|
||||||
assert_eq!(
|
171, 4, 51, 131, 254, 57, 241, 178, 82, 161, 204, 206, 79, 107,
|
||||||
child_keys.csk.value(),
|
])
|
||||||
&[
|
.unwrap();
|
||||||
11, 151, 27, 212, 167, 26, 77, 234, 103, 145, 53, 191, 184, 25, 240, 191, 156, 25,
|
|
||||||
60, 144, 65, 22, 193, 163, 246, 227, 212, 81, 49, 170, 33, 158
|
let expected_cpk: PublicKey = PublicKey::try_new([
|
||||||
]
|
149, 240, 55, 15, 178, 67, 245, 254, 44, 141, 95, 223, 238, 62, 85, 11, 248, 9, 11, 40,
|
||||||
|
69, 211, 116, 13, 189, 35, 8, 95, 233, 154, 129, 58,
|
||||||
|
])
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert!(expected_ccc == child_keys.ccc);
|
||||||
|
assert!(expected_csk == child_keys.csk);
|
||||||
|
assert!(expected_cpk == child_keys.cpk);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_nonharden_child_keys_generation() {
|
||||||
|
let seed = [
|
||||||
|
88, 189, 37, 237, 199, 125, 151, 226, 69, 153, 165, 113, 191, 69, 188, 221, 9, 34, 173,
|
||||||
|
134, 61, 109, 34, 103, 121, 39, 237, 14, 107, 194, 24, 194, 191, 14, 237, 185, 12, 87,
|
||||||
|
22, 227, 38, 71, 17, 144, 251, 118, 217, 115, 33, 222, 201, 61, 203, 246, 121, 214, 6,
|
||||||
|
187, 148, 92, 44, 253, 210, 37,
|
||||||
|
];
|
||||||
|
let root_keys = ChildKeysPublic::root(seed);
|
||||||
|
let cci = 13;
|
||||||
|
let child_keys = ChildKeysPublic::nth_child(&root_keys, cci);
|
||||||
|
|
||||||
|
print!(
|
||||||
|
"{} {}",
|
||||||
|
child_keys.csk.value()[0],
|
||||||
|
child_keys.csk.value()[1]
|
||||||
);
|
);
|
||||||
|
|
||||||
assert_eq!(
|
let expected_ccc = [
|
||||||
root_keys.cpk.value(),
|
50, 29, 113, 102, 49, 130, 64, 0, 247, 95, 135, 187, 118, 162, 65, 65, 194, 53, 189,
|
||||||
&[
|
242, 66, 178, 168, 2, 51, 193, 155, 72, 209, 2, 207, 251,
|
||||||
220, 170, 95, 177, 121, 37, 86, 166, 56, 238, 232, 72, 21, 106, 107, 217, 158, 74,
|
];
|
||||||
133, 91, 143, 244, 155, 15, 2, 230, 223, 169, 13, 20, 163, 138
|
|
||||||
]
|
let expected_csk: PrivateKey = PrivateKey::try_new([
|
||||||
);
|
162, 32, 211, 190, 180, 74, 151, 246, 189, 93, 8, 57, 182, 239, 125, 245, 192, 255, 24,
|
||||||
assert_eq!(
|
186, 251, 23, 194, 186, 252, 121, 190, 54, 147, 199, 1, 109,
|
||||||
child_keys.cpk.value(),
|
])
|
||||||
&[
|
.unwrap();
|
||||||
152, 249, 236, 111, 132, 96, 184, 122, 21, 179, 240, 15, 234, 155, 164, 144, 108,
|
|
||||||
110, 120, 74, 176, 147, 196, 168, 243, 186, 203, 79, 97, 17, 194, 52
|
let expected_cpk: PublicKey = PublicKey::try_new([
|
||||||
]
|
183, 48, 207, 170, 221, 111, 118, 9, 40, 67, 123, 162, 159, 169, 34, 157, 23, 37, 232,
|
||||||
);
|
102, 231, 187, 199, 191, 205, 146, 159, 22, 79, 100, 10, 223,
|
||||||
|
])
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert!(expected_ccc == child_keys.ccc);
|
||||||
|
assert!(expected_csk == child_keys.csk);
|
||||||
|
assert!(expected_cpk == child_keys.cpk);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_edge_case_child_keys_generation_2_power_31() {
|
||||||
|
let seed = [
|
||||||
|
88, 189, 37, 237, 199, 125, 151, 226, 69, 153, 165, 113, 191, 69, 188, 221, 9, 34, 173,
|
||||||
|
134, 61, 109, 34, 103, 121, 39, 237, 14, 107, 194, 24, 194, 191, 14, 237, 185, 12, 87,
|
||||||
|
22, 227, 38, 71, 17, 144, 251, 118, 217, 115, 33, 222, 201, 61, 203, 246, 121, 214, 6,
|
||||||
|
187, 148, 92, 44, 253, 210, 37,
|
||||||
|
];
|
||||||
|
let root_keys = ChildKeysPublic::root(seed);
|
||||||
|
let cci = (2u32).pow(31); //equivant to 0, thus non-harden.
|
||||||
|
let child_keys = ChildKeysPublic::nth_child(&root_keys, cci);
|
||||||
|
|
||||||
|
let expected_ccc = [
|
||||||
|
101, 15, 69, 152, 144, 22, 105, 89, 175, 21, 13, 50, 160, 167, 93, 80, 94, 99, 192,
|
||||||
|
252, 1, 126, 196, 217, 149, 164, 60, 75, 237, 90, 104, 83,
|
||||||
|
];
|
||||||
|
|
||||||
|
let expected_csk: PrivateKey = PrivateKey::try_new([
|
||||||
|
46, 196, 131, 199, 190, 180, 250, 222, 41, 188, 221, 156, 255, 239, 251, 207, 239, 202,
|
||||||
|
166, 216, 107, 236, 195, 48, 167, 69, 97, 13, 132, 117, 76, 89,
|
||||||
|
])
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let expected_cpk: PublicKey = PublicKey::try_new([
|
||||||
|
93, 151, 154, 238, 175, 198, 53, 146, 255, 43, 37, 52, 214, 165, 69, 161, 38, 20, 68,
|
||||||
|
166, 143, 80, 149, 216, 124, 203, 240, 114, 168, 111, 33, 83,
|
||||||
|
])
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert!(expected_ccc == child_keys.ccc);
|
||||||
|
assert!(expected_csk == child_keys.csk);
|
||||||
|
assert!(expected_cpk == child_keys.cpk);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -345,8 +345,8 @@ mod tests {
|
|||||||
|
|
||||||
assert!(tree.key_map.contains_key(&ChainIndex::root()));
|
assert!(tree.key_map.contains_key(&ChainIndex::root()));
|
||||||
assert!(tree.account_id_map.contains_key(&AccountId::new([
|
assert!(tree.account_id_map.contains_key(&AccountId::new([
|
||||||
46, 223, 229, 177, 59, 18, 189, 219, 153, 31, 249, 90, 112, 230, 180, 164, 80, 25, 106,
|
172, 82, 222, 249, 164, 16, 148, 184, 219, 56, 92, 145, 203, 220, 251, 89, 214, 178,
|
||||||
159, 14, 238, 1, 192, 91, 8, 210, 165, 199, 41, 60, 104,
|
38, 30, 108, 202, 251, 241, 148, 200, 125, 185, 93, 227, 189, 247
|
||||||
])));
|
])));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -139,14 +139,14 @@ impl NSSAUserData {
|
|||||||
/// Returns the signing key for public transaction signatures
|
/// Returns the signing key for public transaction signatures
|
||||||
pub fn get_private_account(
|
pub fn get_private_account(
|
||||||
&self,
|
&self,
|
||||||
account_id: &nssa::AccountId,
|
account_id: nssa::AccountId,
|
||||||
) -> Option<&(KeyChain, nssa_core::account::Account)> {
|
) -> Option<&(KeyChain, nssa_core::account::Account)> {
|
||||||
// First seek in defaults
|
// First seek in defaults
|
||||||
if let Some(key) = self.default_user_private_accounts.get(account_id) {
|
if let Some(key) = self.default_user_private_accounts.get(&account_id) {
|
||||||
Some(key)
|
Some(key)
|
||||||
// Then seek in tree
|
// Then seek in tree
|
||||||
} else {
|
} else {
|
||||||
self.private_key_tree.get_node(*account_id).map(Into::into)
|
self.private_key_tree.get_node(account_id).map(Into::into)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -208,16 +208,13 @@ mod tests {
|
|||||||
let (account_id_private, _) = user_data
|
let (account_id_private, _) = user_data
|
||||||
.generate_new_privacy_preserving_transaction_key_chain(Some(ChainIndex::root()));
|
.generate_new_privacy_preserving_transaction_key_chain(Some(ChainIndex::root()));
|
||||||
|
|
||||||
let is_key_chain_generated = user_data.get_private_account(&account_id_private).is_some();
|
let is_key_chain_generated = user_data.get_private_account(account_id_private).is_some();
|
||||||
|
|
||||||
assert!(is_key_chain_generated);
|
assert!(is_key_chain_generated);
|
||||||
|
|
||||||
let account_id_private_str = account_id_private.to_string();
|
let account_id_private_str = account_id_private.to_string();
|
||||||
println!("{account_id_private_str:#?}");
|
println!("{account_id_private_str:#?}");
|
||||||
let key_chain = &user_data
|
let key_chain = &user_data.get_private_account(account_id_private).unwrap().0;
|
||||||
.get_private_account(&account_id_private)
|
|
||||||
.unwrap()
|
|
||||||
.0;
|
|
||||||
println!("{key_chain:#?}");
|
println!("{key_chain:#?}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -16,7 +16,6 @@ borsh.workspace = true
|
|||||||
hex.workspace = true
|
hex.workspace = true
|
||||||
secp256k1 = "0.31.1"
|
secp256k1 = "0.31.1"
|
||||||
risc0-binfmt = "3.0.2"
|
risc0-binfmt = "3.0.2"
|
||||||
bytemuck = "1.24.0"
|
|
||||||
log.workspace = true
|
log.workspace = true
|
||||||
|
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
@ -25,6 +24,7 @@ risc0-binfmt = "3.0.2"
|
|||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
token_core.workspace = true
|
token_core.workspace = true
|
||||||
|
amm_core.workspace = true
|
||||||
test_program_methods.workspace = true
|
test_program_methods.workspace = true
|
||||||
|
|
||||||
env_logger.workspace = true
|
env_logger.workspace = true
|
||||||
|
|||||||
@ -298,12 +298,12 @@ pub mod tests {
|
|||||||
let tx = transaction_for_tests();
|
let tx = transaction_for_tests();
|
||||||
let expected_signer_account_ids = vec![
|
let expected_signer_account_ids = vec![
|
||||||
AccountId::new([
|
AccountId::new([
|
||||||
208, 122, 210, 232, 75, 39, 250, 0, 194, 98, 240, 161, 238, 160, 255, 53, 202, 9,
|
148, 179, 206, 253, 199, 51, 82, 86, 232, 2, 152, 122, 80, 243, 54, 207, 237, 112,
|
||||||
115, 84, 126, 106, 16, 111, 114, 241, 147, 194, 220, 131, 139, 68,
|
83, 153, 44, 59, 204, 49, 128, 84, 160, 227, 216, 149, 97, 102,
|
||||||
]),
|
]),
|
||||||
AccountId::new([
|
AccountId::new([
|
||||||
231, 174, 119, 197, 239, 26, 5, 153, 147, 68, 175, 73, 159, 199, 138, 23, 5, 57,
|
30, 145, 107, 3, 207, 73, 192, 230, 160, 63, 238, 207, 18, 69, 54, 216, 103, 244,
|
||||||
141, 98, 237, 6, 207, 46, 20, 121, 246, 222, 248, 154, 57, 188,
|
92, 94, 124, 248, 42, 16, 141, 19, 119, 18, 14, 226, 140, 204,
|
||||||
]),
|
]),
|
||||||
];
|
];
|
||||||
let signer_account_ids = tx.signer_account_ids();
|
let signer_account_ids = tx.signer_account_ids();
|
||||||
|
|||||||
@ -48,7 +48,8 @@ impl PublicKey {
|
|||||||
|
|
||||||
impl From<&PublicKey> for AccountId {
|
impl From<&PublicKey> for AccountId {
|
||||||
fn from(key: &PublicKey) -> Self {
|
fn from(key: &PublicKey) -> Self {
|
||||||
const PUBLIC_ACCOUNT_ID_PREFIX: &[u8; 32] = b"/NSSA/v0.2/AccountId/Public/\x00\x00\x00\x00";
|
const PUBLIC_ACCOUNT_ID_PREFIX: &[u8; 32] =
|
||||||
|
b"/LEE/v0.3/AccountId/Public/\x00\x00\x00\x00\x00";
|
||||||
|
|
||||||
let mut hasher = Sha256::new();
|
let mut hasher = Sha256::new();
|
||||||
hasher.update(PUBLIC_ACCOUNT_ID_PREFIX);
|
hasher.update(PUBLIC_ACCOUNT_ID_PREFIX);
|
||||||
|
|||||||
@ -311,6 +311,7 @@ pub mod tests {
|
|||||||
|
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
use amm_core::PoolDefinition;
|
||||||
use nssa_core::{
|
use nssa_core::{
|
||||||
Commitment, Nullifier, NullifierPublicKey, NullifierSecretKey, SharedSecretKey,
|
Commitment, Nullifier, NullifierPublicKey, NullifierSecretKey, SharedSecretKey,
|
||||||
account::{Account, AccountId, AccountWithMetadata, Nonce, data::Data},
|
account::{Account, AccountId, AccountWithMetadata, Nonce, data::Data},
|
||||||
@ -2327,137 +2328,6 @@ pub mod tests {
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO repeated code should ultimately be removed;
|
|
||||||
fn compute_pool_pda(
|
|
||||||
amm_program_id: ProgramId,
|
|
||||||
definition_token_a_id: AccountId,
|
|
||||||
definition_token_b_id: AccountId,
|
|
||||||
) -> AccountId {
|
|
||||||
AccountId::from((
|
|
||||||
&amm_program_id,
|
|
||||||
&compute_pool_pda_seed(definition_token_a_id, definition_token_b_id),
|
|
||||||
))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn compute_pool_pda_seed(
|
|
||||||
definition_token_a_id: AccountId,
|
|
||||||
definition_token_b_id: AccountId,
|
|
||||||
) -> PdaSeed {
|
|
||||||
use risc0_zkvm::sha::{Impl, Sha256};
|
|
||||||
|
|
||||||
let mut i: usize = 0;
|
|
||||||
let (token_1, token_2) = loop {
|
|
||||||
if definition_token_a_id.value()[i] > definition_token_b_id.value()[i] {
|
|
||||||
let token_1 = definition_token_a_id;
|
|
||||||
let token_2 = definition_token_b_id;
|
|
||||||
break (token_1, token_2);
|
|
||||||
} else if definition_token_a_id.value()[i] < definition_token_b_id.value()[i] {
|
|
||||||
let token_1 = definition_token_b_id;
|
|
||||||
let token_2 = definition_token_a_id;
|
|
||||||
break (token_1, token_2);
|
|
||||||
}
|
|
||||||
|
|
||||||
if i == 32 {
|
|
||||||
panic!("Definitions match");
|
|
||||||
} else {
|
|
||||||
i += 1;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut bytes = [0; 64];
|
|
||||||
bytes[0..32].copy_from_slice(&token_1.to_bytes());
|
|
||||||
bytes[32..].copy_from_slice(&token_2.to_bytes());
|
|
||||||
|
|
||||||
PdaSeed::new(
|
|
||||||
Impl::hash_bytes(&bytes)
|
|
||||||
.as_bytes()
|
|
||||||
.try_into()
|
|
||||||
.expect("Hash output must be exactly 32 bytes long"),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn compute_vault_pda(
|
|
||||||
amm_program_id: ProgramId,
|
|
||||||
pool_id: AccountId,
|
|
||||||
definition_token_id: AccountId,
|
|
||||||
) -> AccountId {
|
|
||||||
AccountId::from((
|
|
||||||
&amm_program_id,
|
|
||||||
&compute_vault_pda_seed(pool_id, definition_token_id),
|
|
||||||
))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn compute_vault_pda_seed(pool_id: AccountId, definition_token_id: AccountId) -> PdaSeed {
|
|
||||||
use risc0_zkvm::sha::{Impl, Sha256};
|
|
||||||
|
|
||||||
let mut bytes = [0; 64];
|
|
||||||
bytes[0..32].copy_from_slice(&pool_id.to_bytes());
|
|
||||||
bytes[32..].copy_from_slice(&definition_token_id.to_bytes());
|
|
||||||
|
|
||||||
PdaSeed::new(
|
|
||||||
Impl::hash_bytes(&bytes)
|
|
||||||
.as_bytes()
|
|
||||||
.try_into()
|
|
||||||
.expect("Hash output must be exactly 32 bytes long"),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn compute_liquidity_token_pda(amm_program_id: ProgramId, pool_id: AccountId) -> AccountId {
|
|
||||||
AccountId::from((&amm_program_id, &compute_liquidity_token_pda_seed(pool_id)))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn compute_liquidity_token_pda_seed(pool_id: AccountId) -> PdaSeed {
|
|
||||||
use risc0_zkvm::sha::{Impl, Sha256};
|
|
||||||
|
|
||||||
let mut bytes = [0; 64];
|
|
||||||
bytes[0..32].copy_from_slice(&pool_id.to_bytes());
|
|
||||||
bytes[32..].copy_from_slice(&[0; 32]);
|
|
||||||
|
|
||||||
PdaSeed::new(
|
|
||||||
Impl::hash_bytes(&bytes)
|
|
||||||
.as_bytes()
|
|
||||||
.try_into()
|
|
||||||
.expect("Hash output must be exactly 32 bytes long"),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const POOL_DEFINITION_DATA_SIZE: usize = 225;
|
|
||||||
|
|
||||||
#[derive(Default)]
|
|
||||||
struct PoolDefinition {
|
|
||||||
definition_token_a_id: AccountId,
|
|
||||||
definition_token_b_id: AccountId,
|
|
||||||
vault_a_id: AccountId,
|
|
||||||
vault_b_id: AccountId,
|
|
||||||
liquidity_pool_id: AccountId,
|
|
||||||
liquidity_pool_supply: u128,
|
|
||||||
reserve_a: u128,
|
|
||||||
reserve_b: u128,
|
|
||||||
fees: u128,
|
|
||||||
active: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl PoolDefinition {
|
|
||||||
fn into_data(self) -> Data {
|
|
||||||
let mut bytes = [0; POOL_DEFINITION_DATA_SIZE];
|
|
||||||
bytes[0..32].copy_from_slice(&self.definition_token_a_id.to_bytes());
|
|
||||||
bytes[32..64].copy_from_slice(&self.definition_token_b_id.to_bytes());
|
|
||||||
bytes[64..96].copy_from_slice(&self.vault_a_id.to_bytes());
|
|
||||||
bytes[96..128].copy_from_slice(&self.vault_b_id.to_bytes());
|
|
||||||
bytes[128..160].copy_from_slice(&self.liquidity_pool_id.to_bytes());
|
|
||||||
bytes[160..176].copy_from_slice(&self.liquidity_pool_supply.to_le_bytes());
|
|
||||||
bytes[176..192].copy_from_slice(&self.reserve_a.to_le_bytes());
|
|
||||||
bytes[192..208].copy_from_slice(&self.reserve_b.to_le_bytes());
|
|
||||||
bytes[208..224].copy_from_slice(&self.fees.to_le_bytes());
|
|
||||||
bytes[224] = self.active as u8;
|
|
||||||
|
|
||||||
bytes
|
|
||||||
.to_vec()
|
|
||||||
.try_into()
|
|
||||||
.expect("225 bytes should fit into Data")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct PrivateKeysForTests;
|
struct PrivateKeysForTests;
|
||||||
|
|
||||||
impl PrivateKeysForTests {
|
impl PrivateKeysForTests {
|
||||||
@ -2638,7 +2508,7 @@ pub mod tests {
|
|||||||
|
|
||||||
impl IdForTests {
|
impl IdForTests {
|
||||||
fn pool_definition_id() -> AccountId {
|
fn pool_definition_id() -> AccountId {
|
||||||
compute_pool_pda(
|
amm_core::compute_pool_pda(
|
||||||
Program::amm().id(),
|
Program::amm().id(),
|
||||||
IdForTests::token_a_definition_id(),
|
IdForTests::token_a_definition_id(),
|
||||||
IdForTests::token_b_definition_id(),
|
IdForTests::token_b_definition_id(),
|
||||||
@ -2646,7 +2516,10 @@ pub mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn token_lp_definition_id() -> AccountId {
|
fn token_lp_definition_id() -> AccountId {
|
||||||
compute_liquidity_token_pda(Program::amm().id(), IdForTests::pool_definition_id())
|
amm_core::compute_liquidity_token_pda(
|
||||||
|
Program::amm().id(),
|
||||||
|
IdForTests::pool_definition_id(),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn token_a_definition_id() -> AccountId {
|
fn token_a_definition_id() -> AccountId {
|
||||||
@ -2676,7 +2549,7 @@ pub mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn vault_a_id() -> AccountId {
|
fn vault_a_id() -> AccountId {
|
||||||
compute_vault_pda(
|
amm_core::compute_vault_pda(
|
||||||
Program::amm().id(),
|
Program::amm().id(),
|
||||||
IdForTests::pool_definition_id(),
|
IdForTests::pool_definition_id(),
|
||||||
IdForTests::token_a_definition_id(),
|
IdForTests::token_a_definition_id(),
|
||||||
@ -2684,7 +2557,7 @@ pub mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn vault_b_id() -> AccountId {
|
fn vault_b_id() -> AccountId {
|
||||||
compute_vault_pda(
|
amm_core::compute_vault_pda(
|
||||||
Program::amm().id(),
|
Program::amm().id(),
|
||||||
IdForTests::pool_definition_id(),
|
IdForTests::pool_definition_id(),
|
||||||
IdForTests::token_b_definition_id(),
|
IdForTests::token_b_definition_id(),
|
||||||
@ -2723,7 +2596,7 @@ pub mod tests {
|
|||||||
Account {
|
Account {
|
||||||
program_owner: Program::amm().id(),
|
program_owner: Program::amm().id(),
|
||||||
balance: 0u128,
|
balance: 0u128,
|
||||||
data: PoolDefinition::into_data(PoolDefinition {
|
data: Data::from(&PoolDefinition {
|
||||||
definition_token_a_id: IdForTests::token_a_definition_id(),
|
definition_token_a_id: IdForTests::token_a_definition_id(),
|
||||||
definition_token_b_id: IdForTests::token_b_definition_id(),
|
definition_token_b_id: IdForTests::token_b_definition_id(),
|
||||||
vault_a_id: IdForTests::vault_a_id(),
|
vault_a_id: IdForTests::vault_a_id(),
|
||||||
@ -2842,7 +2715,7 @@ pub mod tests {
|
|||||||
Account {
|
Account {
|
||||||
program_owner: Program::amm().id(),
|
program_owner: Program::amm().id(),
|
||||||
balance: 0u128,
|
balance: 0u128,
|
||||||
data: PoolDefinition::into_data(PoolDefinition {
|
data: Data::from(&PoolDefinition {
|
||||||
definition_token_a_id: IdForTests::token_a_definition_id(),
|
definition_token_a_id: IdForTests::token_a_definition_id(),
|
||||||
definition_token_b_id: IdForTests::token_b_definition_id(),
|
definition_token_b_id: IdForTests::token_b_definition_id(),
|
||||||
vault_a_id: IdForTests::vault_a_id(),
|
vault_a_id: IdForTests::vault_a_id(),
|
||||||
@ -2910,7 +2783,7 @@ pub mod tests {
|
|||||||
Account {
|
Account {
|
||||||
program_owner: Program::amm().id(),
|
program_owner: Program::amm().id(),
|
||||||
balance: 0u128,
|
balance: 0u128,
|
||||||
data: PoolDefinition::into_data(PoolDefinition {
|
data: Data::from(&PoolDefinition {
|
||||||
definition_token_a_id: IdForTests::token_a_definition_id(),
|
definition_token_a_id: IdForTests::token_a_definition_id(),
|
||||||
definition_token_b_id: IdForTests::token_b_definition_id(),
|
definition_token_b_id: IdForTests::token_b_definition_id(),
|
||||||
vault_a_id: IdForTests::vault_a_id(),
|
vault_a_id: IdForTests::vault_a_id(),
|
||||||
@ -2978,7 +2851,7 @@ pub mod tests {
|
|||||||
Account {
|
Account {
|
||||||
program_owner: Program::amm().id(),
|
program_owner: Program::amm().id(),
|
||||||
balance: 0u128,
|
balance: 0u128,
|
||||||
data: PoolDefinition::into_data(PoolDefinition {
|
data: Data::from(&PoolDefinition {
|
||||||
definition_token_a_id: IdForTests::token_a_definition_id(),
|
definition_token_a_id: IdForTests::token_a_definition_id(),
|
||||||
definition_token_b_id: IdForTests::token_b_definition_id(),
|
definition_token_b_id: IdForTests::token_b_definition_id(),
|
||||||
vault_a_id: IdForTests::vault_a_id(),
|
vault_a_id: IdForTests::vault_a_id(),
|
||||||
@ -3071,7 +2944,7 @@ pub mod tests {
|
|||||||
Account {
|
Account {
|
||||||
program_owner: Program::amm().id(),
|
program_owner: Program::amm().id(),
|
||||||
balance: 0u128,
|
balance: 0u128,
|
||||||
data: PoolDefinition::into_data(PoolDefinition {
|
data: Data::from(&PoolDefinition {
|
||||||
definition_token_a_id: IdForTests::token_a_definition_id(),
|
definition_token_a_id: IdForTests::token_a_definition_id(),
|
||||||
definition_token_b_id: IdForTests::token_b_definition_id(),
|
definition_token_b_id: IdForTests::token_b_definition_id(),
|
||||||
vault_a_id: IdForTests::vault_a_id(),
|
vault_a_id: IdForTests::vault_a_id(),
|
||||||
@ -3177,7 +3050,7 @@ pub mod tests {
|
|||||||
Account {
|
Account {
|
||||||
program_owner: Program::amm().id(),
|
program_owner: Program::amm().id(),
|
||||||
balance: 0u128,
|
balance: 0u128,
|
||||||
data: PoolDefinition::into_data(PoolDefinition {
|
data: Data::from(&PoolDefinition {
|
||||||
definition_token_a_id: IdForTests::token_a_definition_id(),
|
definition_token_a_id: IdForTests::token_a_definition_id(),
|
||||||
definition_token_b_id: IdForTests::token_b_definition_id(),
|
definition_token_b_id: IdForTests::token_b_definition_id(),
|
||||||
vault_a_id: IdForTests::vault_a_id(),
|
vault_a_id: IdForTests::vault_a_id(),
|
||||||
@ -3246,7 +3119,7 @@ pub mod tests {
|
|||||||
Account {
|
Account {
|
||||||
program_owner: Program::amm().id(),
|
program_owner: Program::amm().id(),
|
||||||
balance: 0u128,
|
balance: 0u128,
|
||||||
data: PoolDefinition::into_data(PoolDefinition {
|
data: Data::from(&PoolDefinition {
|
||||||
definition_token_a_id: IdForTests::token_a_definition_id(),
|
definition_token_a_id: IdForTests::token_a_definition_id(),
|
||||||
definition_token_b_id: IdForTests::token_b_definition_id(),
|
definition_token_b_id: IdForTests::token_b_definition_id(),
|
||||||
vault_a_id: IdForTests::vault_a_id(),
|
vault_a_id: IdForTests::vault_a_id(),
|
||||||
@ -3275,11 +3148,6 @@ pub mod tests {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const AMM_NEW_DEFINITION: u8 = 0;
|
|
||||||
const AMM_SWAP: u8 = 1;
|
|
||||||
const AMM_ADD_LIQUIDITY: u8 = 2;
|
|
||||||
const AMM_REMOVE_LIQUIDITY: u8 = 3;
|
|
||||||
|
|
||||||
fn state_for_amm_tests() -> V02State {
|
fn state_for_amm_tests() -> V02State {
|
||||||
let initial_data = [];
|
let initial_data = [];
|
||||||
let mut state =
|
let mut state =
|
||||||
@ -3345,11 +3213,11 @@ pub mod tests {
|
|||||||
fn test_simple_amm_remove() {
|
fn test_simple_amm_remove() {
|
||||||
let mut state = state_for_amm_tests();
|
let mut state = state_for_amm_tests();
|
||||||
|
|
||||||
let mut instruction: Vec<u8> = Vec::new();
|
let instruction = amm_core::Instruction::RemoveLiquidity {
|
||||||
instruction.push(AMM_REMOVE_LIQUIDITY);
|
remove_liquidity_amount: BalanceForTests::remove_lp(),
|
||||||
instruction.extend_from_slice(&BalanceForTests::remove_lp().to_le_bytes());
|
min_amount_to_remove_token_a: BalanceForTests::remove_min_amount_a(),
|
||||||
instruction.extend_from_slice(&BalanceForTests::remove_min_amount_a().to_le_bytes());
|
min_amount_to_remove_token_b: BalanceForTests::remove_min_amount_b(),
|
||||||
instruction.extend_from_slice(&BalanceForTests::remove_min_amount_b().to_le_bytes());
|
};
|
||||||
|
|
||||||
let message = public_transaction::Message::try_new(
|
let message = public_transaction::Message::try_new(
|
||||||
Program::amm().id(),
|
Program::amm().id(),
|
||||||
@ -3422,12 +3290,11 @@ pub mod tests {
|
|||||||
AccountForTests::token_lp_definition_init_inactive(),
|
AccountForTests::token_lp_definition_init_inactive(),
|
||||||
);
|
);
|
||||||
|
|
||||||
let mut instruction: Vec<u8> = Vec::new();
|
let instruction = amm_core::Instruction::NewDefinition {
|
||||||
instruction.push(AMM_NEW_DEFINITION);
|
token_a_amount: BalanceForTests::vault_a_balance_init(),
|
||||||
instruction.extend_from_slice(&BalanceForTests::vault_a_balance_init().to_le_bytes());
|
token_b_amount: BalanceForTests::vault_b_balance_init(),
|
||||||
instruction.extend_from_slice(&BalanceForTests::vault_b_balance_init().to_le_bytes());
|
amm_program_id: Program::amm().id(),
|
||||||
let amm_program_u8: [u8; 32] = bytemuck::cast(Program::amm().id());
|
};
|
||||||
instruction.extend_from_slice(&amm_program_u8);
|
|
||||||
|
|
||||||
let message = public_transaction::Message::try_new(
|
let message = public_transaction::Message::try_new(
|
||||||
Program::amm().id(),
|
Program::amm().id(),
|
||||||
@ -3507,12 +3374,11 @@ pub mod tests {
|
|||||||
AccountForTests::user_token_lp_holding_init_zero(),
|
AccountForTests::user_token_lp_holding_init_zero(),
|
||||||
);
|
);
|
||||||
|
|
||||||
let mut instruction: Vec<u8> = Vec::new();
|
let instruction = amm_core::Instruction::NewDefinition {
|
||||||
instruction.push(AMM_NEW_DEFINITION);
|
token_a_amount: BalanceForTests::vault_a_balance_init(),
|
||||||
instruction.extend_from_slice(&BalanceForTests::vault_a_balance_init().to_le_bytes());
|
token_b_amount: BalanceForTests::vault_b_balance_init(),
|
||||||
instruction.extend_from_slice(&BalanceForTests::vault_b_balance_init().to_le_bytes());
|
amm_program_id: Program::amm().id(),
|
||||||
let amm_program_u8: [u8; 32] = bytemuck::cast(Program::amm().id());
|
};
|
||||||
instruction.extend_from_slice(&amm_program_u8);
|
|
||||||
|
|
||||||
let message = public_transaction::Message::try_new(
|
let message = public_transaction::Message::try_new(
|
||||||
Program::amm().id(),
|
Program::amm().id(),
|
||||||
@ -3580,12 +3446,11 @@ pub mod tests {
|
|||||||
AccountForTests::vault_b_init_inactive(),
|
AccountForTests::vault_b_init_inactive(),
|
||||||
);
|
);
|
||||||
|
|
||||||
let mut instruction: Vec<u8> = Vec::new();
|
let instruction = amm_core::Instruction::NewDefinition {
|
||||||
instruction.push(AMM_NEW_DEFINITION);
|
token_a_amount: BalanceForTests::vault_a_balance_init(),
|
||||||
instruction.extend_from_slice(&BalanceForTests::vault_a_balance_init().to_le_bytes());
|
token_b_amount: BalanceForTests::vault_b_balance_init(),
|
||||||
instruction.extend_from_slice(&BalanceForTests::vault_b_balance_init().to_le_bytes());
|
amm_program_id: Program::amm().id(),
|
||||||
let amm_program_u8: [u8; 32] = bytemuck::cast(Program::amm().id());
|
};
|
||||||
instruction.extend_from_slice(&amm_program_u8);
|
|
||||||
|
|
||||||
let message = public_transaction::Message::try_new(
|
let message = public_transaction::Message::try_new(
|
||||||
Program::amm().id(),
|
Program::amm().id(),
|
||||||
@ -3644,11 +3509,11 @@ pub mod tests {
|
|||||||
env_logger::init();
|
env_logger::init();
|
||||||
let mut state = state_for_amm_tests();
|
let mut state = state_for_amm_tests();
|
||||||
|
|
||||||
let mut instruction: Vec<u8> = Vec::new();
|
let instruction = amm_core::Instruction::AddLiquidity {
|
||||||
instruction.push(AMM_ADD_LIQUIDITY);
|
min_amount_liquidity: BalanceForTests::add_min_amount_lp(),
|
||||||
instruction.extend_from_slice(&BalanceForTests::add_min_amount_lp().to_le_bytes());
|
max_amount_to_add_token_a: BalanceForTests::add_max_amount_a(),
|
||||||
instruction.extend_from_slice(&BalanceForTests::add_max_amount_a().to_le_bytes());
|
max_amount_to_add_token_b: BalanceForTests::add_max_amount_b(),
|
||||||
instruction.extend_from_slice(&BalanceForTests::add_max_amount_b().to_le_bytes());
|
};
|
||||||
|
|
||||||
let message = public_transaction::Message::try_new(
|
let message = public_transaction::Message::try_new(
|
||||||
Program::amm().id(),
|
Program::amm().id(),
|
||||||
@ -3706,11 +3571,11 @@ pub mod tests {
|
|||||||
fn test_simple_amm_swap_1() {
|
fn test_simple_amm_swap_1() {
|
||||||
let mut state = state_for_amm_tests();
|
let mut state = state_for_amm_tests();
|
||||||
|
|
||||||
let mut instruction: Vec<u8> = Vec::new();
|
let instruction = amm_core::Instruction::Swap {
|
||||||
instruction.push(AMM_SWAP);
|
swap_amount_in: BalanceForTests::swap_amount_in(),
|
||||||
instruction.extend_from_slice(&BalanceForTests::swap_amount_in().to_le_bytes());
|
min_amount_out: BalanceForTests::swap_min_amount_out(),
|
||||||
instruction.extend_from_slice(&BalanceForTests::swap_min_amount_out().to_le_bytes());
|
token_definition_id_in: IdForTests::token_b_definition_id(),
|
||||||
instruction.extend_from_slice(&IdForTests::token_b_definition_id().to_bytes());
|
};
|
||||||
|
|
||||||
let message = public_transaction::Message::try_new(
|
let message = public_transaction::Message::try_new(
|
||||||
Program::amm().id(),
|
Program::amm().id(),
|
||||||
@ -3757,12 +3622,11 @@ pub mod tests {
|
|||||||
fn test_simple_amm_swap_2() {
|
fn test_simple_amm_swap_2() {
|
||||||
let mut state = state_for_amm_tests();
|
let mut state = state_for_amm_tests();
|
||||||
|
|
||||||
let mut instruction: Vec<u8> = Vec::new();
|
let instruction = amm_core::Instruction::Swap {
|
||||||
instruction.push(AMM_SWAP);
|
swap_amount_in: BalanceForTests::swap_amount_in(),
|
||||||
instruction.extend_from_slice(&BalanceForTests::swap_amount_in().to_le_bytes());
|
min_amount_out: BalanceForTests::swap_min_amount_out(),
|
||||||
instruction.extend_from_slice(&BalanceForTests::swap_min_amount_out().to_le_bytes());
|
token_definition_id_in: IdForTests::token_a_definition_id(),
|
||||||
instruction.extend_from_slice(&IdForTests::token_a_definition_id().to_bytes());
|
};
|
||||||
|
|
||||||
let message = public_transaction::Message::try_new(
|
let message = public_transaction::Message::try_new(
|
||||||
Program::amm().id(),
|
Program::amm().id(),
|
||||||
vec![
|
vec![
|
||||||
|
|||||||
@ -8,5 +8,7 @@ license = { workspace = true }
|
|||||||
nssa_core.workspace = true
|
nssa_core.workspace = true
|
||||||
token_core.workspace = true
|
token_core.workspace = true
|
||||||
token_program.workspace = true
|
token_program.workspace = true
|
||||||
|
amm_core.workspace = true
|
||||||
|
amm_program.workspace = true
|
||||||
risc0-zkvm.workspace = true
|
risc0-zkvm.workspace = true
|
||||||
serde = { workspace = true, default-features = false }
|
serde = { workspace = true, default-features = false }
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
10
programs/amm/Cargo.toml
Normal file
10
programs/amm/Cargo.toml
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
[package]
|
||||||
|
name = "amm_program"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2024"
|
||||||
|
license = { workspace = true }
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
nssa_core.workspace = true
|
||||||
|
token_core.workspace = true
|
||||||
|
amm_core.workspace = true
|
||||||
11
programs/amm/core/Cargo.toml
Normal file
11
programs/amm/core/Cargo.toml
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
[package]
|
||||||
|
name = "amm_core"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2024"
|
||||||
|
license = { workspace = true }
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
nssa_core.workspace = true
|
||||||
|
serde.workspace = true
|
||||||
|
risc0-zkvm.workspace = true
|
||||||
|
borsh.workspace = true
|
||||||
197
programs/amm/core/src/lib.rs
Normal file
197
programs/amm/core/src/lib.rs
Normal file
@ -0,0 +1,197 @@
|
|||||||
|
//! This crate contains core data structures and utilities for the AMM Program.
|
||||||
|
|
||||||
|
use borsh::{BorshDeserialize, BorshSerialize};
|
||||||
|
use nssa_core::{
|
||||||
|
account::{AccountId, Data},
|
||||||
|
program::{PdaSeed, ProgramId},
|
||||||
|
};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
/// AMM Program Instruction.
|
||||||
|
#[derive(Serialize, Deserialize)]
|
||||||
|
pub enum Instruction {
|
||||||
|
/// Initializes a new Pool (or re-initializes an inactive Pool).
|
||||||
|
///
|
||||||
|
/// Required accounts:
|
||||||
|
/// - AMM Pool
|
||||||
|
/// - Vault Holding Account for Token A
|
||||||
|
/// - Vault Holding Account for Token B
|
||||||
|
/// - Pool Liquidity Token Definition
|
||||||
|
/// - User Holding Account for Token A (authorized)
|
||||||
|
/// - User Holding Account for Token B (authorized)
|
||||||
|
/// - User Holding Account for Pool Liquidity
|
||||||
|
NewDefinition {
|
||||||
|
token_a_amount: u128,
|
||||||
|
token_b_amount: u128,
|
||||||
|
amm_program_id: ProgramId,
|
||||||
|
},
|
||||||
|
|
||||||
|
/// Adds liquidity to the Pool
|
||||||
|
///
|
||||||
|
/// Required accounts:
|
||||||
|
/// - AMM Pool (initialized)
|
||||||
|
/// - Vault Holding Account for Token A (initialized)
|
||||||
|
/// - Vault Holding Account for Token B (initialized)
|
||||||
|
/// - Pool Liquidity Token Definition (initialized)
|
||||||
|
/// - User Holding Account for Token A (authorized)
|
||||||
|
/// - User Holding Account for Token B (authorized)
|
||||||
|
/// - User Holding Account for Pool Liquidity
|
||||||
|
AddLiquidity {
|
||||||
|
min_amount_liquidity: u128,
|
||||||
|
max_amount_to_add_token_a: u128,
|
||||||
|
max_amount_to_add_token_b: u128,
|
||||||
|
},
|
||||||
|
|
||||||
|
/// Removes liquidity from the Pool
|
||||||
|
///
|
||||||
|
/// Required accounts:
|
||||||
|
/// - AMM Pool (initialized)
|
||||||
|
/// - Vault Holding Account for Token A (initialized)
|
||||||
|
/// - Vault Holding Account for Token B (initialized)
|
||||||
|
/// - Pool Liquidity Token Definition (initialized)
|
||||||
|
/// - User Holding Account for Token A (initialized)
|
||||||
|
/// - User Holding Account for Token B (initialized)
|
||||||
|
/// - User Holding Account for Pool Liquidity (authorized)
|
||||||
|
RemoveLiquidity {
|
||||||
|
remove_liquidity_amount: u128,
|
||||||
|
min_amount_to_remove_token_a: u128,
|
||||||
|
min_amount_to_remove_token_b: u128,
|
||||||
|
},
|
||||||
|
|
||||||
|
/// Swap some quantity of Tokens (either Token A or Token B)
|
||||||
|
/// while maintaining the Pool constant product.
|
||||||
|
///
|
||||||
|
/// Required accounts:
|
||||||
|
/// - AMM Pool (initialized)
|
||||||
|
/// - Vault Holding Account for Token A (initialized)
|
||||||
|
/// - Vault Holding Account for Token B (initialized)
|
||||||
|
/// - User Holding Account for Token A
|
||||||
|
/// - User Holding Account for Token B Either User Holding Account for Token A or Token B is
|
||||||
|
/// authorized.
|
||||||
|
Swap {
|
||||||
|
swap_amount_in: u128,
|
||||||
|
min_amount_out: u128,
|
||||||
|
token_definition_id_in: AccountId,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Default, Serialize, Deserialize, BorshSerialize, BorshDeserialize)]
|
||||||
|
pub struct PoolDefinition {
|
||||||
|
pub definition_token_a_id: AccountId,
|
||||||
|
pub definition_token_b_id: AccountId,
|
||||||
|
pub vault_a_id: AccountId,
|
||||||
|
pub vault_b_id: AccountId,
|
||||||
|
pub liquidity_pool_id: AccountId,
|
||||||
|
pub liquidity_pool_supply: u128,
|
||||||
|
pub reserve_a: u128,
|
||||||
|
pub reserve_b: u128,
|
||||||
|
/// Fees are currently not used
|
||||||
|
pub fees: u128,
|
||||||
|
/// A pool becomes inactive (active = false)
|
||||||
|
/// once all of its liquidity has been removed (e.g., reserves are emptied and
|
||||||
|
/// liquidity_pool_supply = 0)
|
||||||
|
pub active: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<&Data> for PoolDefinition {
|
||||||
|
type Error = std::io::Error;
|
||||||
|
|
||||||
|
fn try_from(data: &Data) -> Result<Self, Self::Error> {
|
||||||
|
PoolDefinition::try_from_slice(data.as_ref())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<&PoolDefinition> for Data {
|
||||||
|
fn from(definition: &PoolDefinition) -> Self {
|
||||||
|
// Using size_of_val as size hint for Vec allocation
|
||||||
|
let mut data = Vec::with_capacity(std::mem::size_of_val(definition));
|
||||||
|
|
||||||
|
BorshSerialize::serialize(definition, &mut data)
|
||||||
|
.expect("Serialization to Vec should not fail");
|
||||||
|
|
||||||
|
Data::try_from(data).expect("Token definition encoded data should fit into Data")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn compute_pool_pda(
|
||||||
|
amm_program_id: ProgramId,
|
||||||
|
definition_token_a_id: AccountId,
|
||||||
|
definition_token_b_id: AccountId,
|
||||||
|
) -> AccountId {
|
||||||
|
AccountId::from((
|
||||||
|
&amm_program_id,
|
||||||
|
&compute_pool_pda_seed(definition_token_a_id, definition_token_b_id),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn compute_pool_pda_seed(
|
||||||
|
definition_token_a_id: AccountId,
|
||||||
|
definition_token_b_id: AccountId,
|
||||||
|
) -> PdaSeed {
|
||||||
|
use risc0_zkvm::sha::{Impl, Sha256};
|
||||||
|
|
||||||
|
let (token_1, token_2) = match definition_token_a_id
|
||||||
|
.value()
|
||||||
|
.cmp(definition_token_b_id.value())
|
||||||
|
{
|
||||||
|
std::cmp::Ordering::Less => (definition_token_b_id, definition_token_a_id),
|
||||||
|
std::cmp::Ordering::Greater => (definition_token_a_id, definition_token_b_id),
|
||||||
|
std::cmp::Ordering::Equal => panic!("Definitions match"),
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut bytes = [0; 64];
|
||||||
|
bytes[0..32].copy_from_slice(&token_1.to_bytes());
|
||||||
|
bytes[32..].copy_from_slice(&token_2.to_bytes());
|
||||||
|
|
||||||
|
PdaSeed::new(
|
||||||
|
Impl::hash_bytes(&bytes)
|
||||||
|
.as_bytes()
|
||||||
|
.try_into()
|
||||||
|
.expect("Hash output must be exactly 32 bytes long"),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn compute_vault_pda(
|
||||||
|
amm_program_id: ProgramId,
|
||||||
|
pool_id: AccountId,
|
||||||
|
definition_token_id: AccountId,
|
||||||
|
) -> AccountId {
|
||||||
|
AccountId::from((
|
||||||
|
&amm_program_id,
|
||||||
|
&compute_vault_pda_seed(pool_id, definition_token_id),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn compute_vault_pda_seed(pool_id: AccountId, definition_token_id: AccountId) -> PdaSeed {
|
||||||
|
use risc0_zkvm::sha::{Impl, Sha256};
|
||||||
|
|
||||||
|
let mut bytes = [0; 64];
|
||||||
|
bytes[0..32].copy_from_slice(&pool_id.to_bytes());
|
||||||
|
bytes[32..].copy_from_slice(&definition_token_id.to_bytes());
|
||||||
|
|
||||||
|
PdaSeed::new(
|
||||||
|
Impl::hash_bytes(&bytes)
|
||||||
|
.as_bytes()
|
||||||
|
.try_into()
|
||||||
|
.expect("Hash output must be exactly 32 bytes long"),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn compute_liquidity_token_pda(amm_program_id: ProgramId, pool_id: AccountId) -> AccountId {
|
||||||
|
AccountId::from((&amm_program_id, &compute_liquidity_token_pda_seed(pool_id)))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn compute_liquidity_token_pda_seed(pool_id: AccountId) -> PdaSeed {
|
||||||
|
use risc0_zkvm::sha::{Impl, Sha256};
|
||||||
|
|
||||||
|
let mut bytes = [0; 64];
|
||||||
|
bytes[0..32].copy_from_slice(&pool_id.to_bytes());
|
||||||
|
bytes[32..].copy_from_slice(&[0; 32]);
|
||||||
|
|
||||||
|
PdaSeed::new(
|
||||||
|
Impl::hash_bytes(&bytes)
|
||||||
|
.as_bytes()
|
||||||
|
.try_into()
|
||||||
|
.expect("Hash output must be exactly 32 bytes long"),
|
||||||
|
)
|
||||||
|
}
|
||||||
178
programs/amm/src/add.rs
Normal file
178
programs/amm/src/add.rs
Normal file
@ -0,0 +1,178 @@
|
|||||||
|
use std::num::NonZeroU128;
|
||||||
|
|
||||||
|
use amm_core::{PoolDefinition, compute_liquidity_token_pda_seed};
|
||||||
|
use nssa_core::{
|
||||||
|
account::{AccountWithMetadata, Data},
|
||||||
|
program::{AccountPostState, ChainedCall},
|
||||||
|
};
|
||||||
|
|
||||||
|
#[expect(clippy::too_many_arguments, reason = "TODO: Fix later")]
|
||||||
|
pub fn add_liquidity(
|
||||||
|
pool: AccountWithMetadata,
|
||||||
|
vault_a: AccountWithMetadata,
|
||||||
|
vault_b: AccountWithMetadata,
|
||||||
|
pool_definition_lp: AccountWithMetadata,
|
||||||
|
user_holding_a: AccountWithMetadata,
|
||||||
|
user_holding_b: AccountWithMetadata,
|
||||||
|
user_holding_lp: AccountWithMetadata,
|
||||||
|
min_amount_liquidity: NonZeroU128,
|
||||||
|
max_amount_to_add_token_a: u128,
|
||||||
|
max_amount_to_add_token_b: u128,
|
||||||
|
) -> (Vec<AccountPostState>, Vec<ChainedCall>) {
|
||||||
|
// 1. Fetch Pool state
|
||||||
|
let pool_def_data = PoolDefinition::try_from(&pool.account.data)
|
||||||
|
.expect("Add liquidity: AMM Program expects valid Pool Definition Account");
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
vault_a.account_id, pool_def_data.vault_a_id,
|
||||||
|
"Vault A was not provided"
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
pool_def_data.liquidity_pool_id, pool_definition_lp.account_id,
|
||||||
|
"LP definition mismatch"
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
vault_b.account_id, pool_def_data.vault_b_id,
|
||||||
|
"Vault B was not provided"
|
||||||
|
);
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
max_amount_to_add_token_a != 0 && max_amount_to_add_token_b != 0,
|
||||||
|
"Both max-balances must be nonzero"
|
||||||
|
);
|
||||||
|
|
||||||
|
// 2. Determine deposit amount
|
||||||
|
let vault_b_token_holding = token_core::TokenHolding::try_from(&vault_b.account.data)
|
||||||
|
.expect("Add liquidity: AMM Program expects valid Token Holding Account for Vault B");
|
||||||
|
let token_core::TokenHolding::Fungible {
|
||||||
|
definition_id: _,
|
||||||
|
balance: vault_b_balance,
|
||||||
|
} = vault_b_token_holding
|
||||||
|
else {
|
||||||
|
panic!(
|
||||||
|
"Add liquidity: AMM Program expects valid Fungible Token Holding Account for Vault B"
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
let vault_a_token_holding = token_core::TokenHolding::try_from(&vault_a.account.data)
|
||||||
|
.expect("Add liquidity: AMM Program expects valid Token Holding Account for Vault A");
|
||||||
|
let token_core::TokenHolding::Fungible {
|
||||||
|
definition_id: _,
|
||||||
|
balance: vault_a_balance,
|
||||||
|
} = vault_a_token_holding
|
||||||
|
else {
|
||||||
|
panic!(
|
||||||
|
"Add liquidity: AMM Program expects valid Fungible Token Holding Account for Vault A"
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
assert!(pool_def_data.reserve_a != 0, "Reserves must be nonzero");
|
||||||
|
assert!(pool_def_data.reserve_b != 0, "Reserves must be nonzero");
|
||||||
|
assert!(
|
||||||
|
vault_a_balance >= pool_def_data.reserve_a,
|
||||||
|
"Vaults' balances must be at least the reserve amounts"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
vault_b_balance >= pool_def_data.reserve_b,
|
||||||
|
"Vaults' balances must be at least the reserve amounts"
|
||||||
|
);
|
||||||
|
|
||||||
|
// Calculate actual_amounts
|
||||||
|
let ideal_a: u128 =
|
||||||
|
(pool_def_data.reserve_a * max_amount_to_add_token_b) / pool_def_data.reserve_b;
|
||||||
|
let ideal_b: u128 =
|
||||||
|
(pool_def_data.reserve_b * max_amount_to_add_token_a) / pool_def_data.reserve_a;
|
||||||
|
|
||||||
|
let actual_amount_a = if ideal_a > max_amount_to_add_token_a {
|
||||||
|
max_amount_to_add_token_a
|
||||||
|
} else {
|
||||||
|
ideal_a
|
||||||
|
};
|
||||||
|
let actual_amount_b = if ideal_b > max_amount_to_add_token_b {
|
||||||
|
max_amount_to_add_token_b
|
||||||
|
} else {
|
||||||
|
ideal_b
|
||||||
|
};
|
||||||
|
|
||||||
|
// 3. Validate amounts
|
||||||
|
assert!(
|
||||||
|
max_amount_to_add_token_a >= actual_amount_a,
|
||||||
|
"Actual trade amounts cannot exceed max_amounts"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
max_amount_to_add_token_b >= actual_amount_b,
|
||||||
|
"Actual trade amounts cannot exceed max_amounts"
|
||||||
|
);
|
||||||
|
|
||||||
|
assert!(actual_amount_a != 0, "A trade amount is 0");
|
||||||
|
assert!(actual_amount_b != 0, "A trade amount is 0");
|
||||||
|
|
||||||
|
// 4. Calculate LP to mint
|
||||||
|
let delta_lp = std::cmp::min(
|
||||||
|
pool_def_data.liquidity_pool_supply * actual_amount_a / pool_def_data.reserve_a,
|
||||||
|
pool_def_data.liquidity_pool_supply * actual_amount_b / pool_def_data.reserve_b,
|
||||||
|
);
|
||||||
|
|
||||||
|
assert!(delta_lp != 0, "Payable LP must be nonzero");
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
delta_lp >= min_amount_liquidity.into(),
|
||||||
|
"Payable LP is less than provided minimum LP amount"
|
||||||
|
);
|
||||||
|
|
||||||
|
// 5. Update pool account
|
||||||
|
let mut pool_post = pool.account.clone();
|
||||||
|
let pool_post_definition = PoolDefinition {
|
||||||
|
liquidity_pool_supply: pool_def_data.liquidity_pool_supply + delta_lp,
|
||||||
|
reserve_a: pool_def_data.reserve_a + actual_amount_a,
|
||||||
|
reserve_b: pool_def_data.reserve_b + actual_amount_b,
|
||||||
|
..pool_def_data
|
||||||
|
};
|
||||||
|
|
||||||
|
pool_post.data = Data::from(&pool_post_definition);
|
||||||
|
let token_program_id = user_holding_a.account.program_owner;
|
||||||
|
|
||||||
|
// Chain call for Token A (UserHoldingA -> Vault_A)
|
||||||
|
let call_token_a = ChainedCall::new(
|
||||||
|
token_program_id,
|
||||||
|
vec![user_holding_a.clone(), vault_a.clone()],
|
||||||
|
&token_core::Instruction::Transfer {
|
||||||
|
amount_to_transfer: actual_amount_a,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
// Chain call for Token B (UserHoldingB -> Vault_B)
|
||||||
|
let call_token_b = ChainedCall::new(
|
||||||
|
token_program_id,
|
||||||
|
vec![user_holding_b.clone(), vault_b.clone()],
|
||||||
|
&token_core::Instruction::Transfer {
|
||||||
|
amount_to_transfer: actual_amount_b,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
// Chain call for LP (mint new tokens for user_holding_lp)
|
||||||
|
let mut pool_definition_lp_auth = pool_definition_lp.clone();
|
||||||
|
pool_definition_lp_auth.is_authorized = true;
|
||||||
|
let call_token_lp = ChainedCall::new(
|
||||||
|
token_program_id,
|
||||||
|
vec![pool_definition_lp_auth.clone(), user_holding_lp.clone()],
|
||||||
|
&token_core::Instruction::Mint {
|
||||||
|
amount_to_mint: delta_lp,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.with_pda_seeds(vec![compute_liquidity_token_pda_seed(pool.account_id)]);
|
||||||
|
|
||||||
|
let chained_calls = vec![call_token_lp, call_token_b, call_token_a];
|
||||||
|
|
||||||
|
let post_states = vec![
|
||||||
|
AccountPostState::new(pool_post),
|
||||||
|
AccountPostState::new(vault_a.account.clone()),
|
||||||
|
AccountPostState::new(vault_b.account.clone()),
|
||||||
|
AccountPostState::new(pool_definition_lp.account.clone()),
|
||||||
|
AccountPostState::new(user_holding_a.account.clone()),
|
||||||
|
AccountPostState::new(user_holding_b.account.clone()),
|
||||||
|
AccountPostState::new(user_holding_lp.account.clone()),
|
||||||
|
];
|
||||||
|
|
||||||
|
(post_states, chained_calls)
|
||||||
|
}
|
||||||
10
programs/amm/src/lib.rs
Normal file
10
programs/amm/src/lib.rs
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
//! The AMM Program implementation.
|
||||||
|
|
||||||
|
pub use amm_core as core;
|
||||||
|
|
||||||
|
pub mod add;
|
||||||
|
pub mod new_definition;
|
||||||
|
pub mod remove;
|
||||||
|
pub mod swap;
|
||||||
|
|
||||||
|
mod tests;
|
||||||
158
programs/amm/src/new_definition.rs
Normal file
158
programs/amm/src/new_definition.rs
Normal file
@ -0,0 +1,158 @@
|
|||||||
|
use std::num::NonZeroU128;
|
||||||
|
|
||||||
|
use amm_core::{
|
||||||
|
PoolDefinition, compute_liquidity_token_pda, compute_liquidity_token_pda_seed,
|
||||||
|
compute_pool_pda, compute_vault_pda,
|
||||||
|
};
|
||||||
|
use nssa_core::{
|
||||||
|
account::{Account, AccountWithMetadata, Data},
|
||||||
|
program::{AccountPostState, ChainedCall, ProgramId},
|
||||||
|
};
|
||||||
|
|
||||||
|
#[expect(clippy::too_many_arguments, reason = "TODO: Fix later")]
|
||||||
|
pub fn new_definition(
|
||||||
|
pool: AccountWithMetadata,
|
||||||
|
vault_a: AccountWithMetadata,
|
||||||
|
vault_b: AccountWithMetadata,
|
||||||
|
pool_definition_lp: AccountWithMetadata,
|
||||||
|
user_holding_a: AccountWithMetadata,
|
||||||
|
user_holding_b: AccountWithMetadata,
|
||||||
|
user_holding_lp: AccountWithMetadata,
|
||||||
|
token_a_amount: NonZeroU128,
|
||||||
|
token_b_amount: NonZeroU128,
|
||||||
|
amm_program_id: ProgramId,
|
||||||
|
) -> (Vec<AccountPostState>, Vec<ChainedCall>) {
|
||||||
|
// Verify token_a and token_b are different
|
||||||
|
let definition_token_a_id = token_core::TokenHolding::try_from(&user_holding_a.account.data)
|
||||||
|
.expect("New definition: AMM Program expects valid Token Holding account for Token A")
|
||||||
|
.definition_id();
|
||||||
|
let definition_token_b_id = token_core::TokenHolding::try_from(&user_holding_b.account.data)
|
||||||
|
.expect("New definition: AMM Program expects valid Token Holding account for Token B")
|
||||||
|
.definition_id();
|
||||||
|
|
||||||
|
// both instances of the same token program
|
||||||
|
let token_program = user_holding_a.account.program_owner;
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
user_holding_b.account.program_owner, token_program,
|
||||||
|
"User Token holdings must use the same Token Program"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
definition_token_a_id != definition_token_b_id,
|
||||||
|
"Cannot set up a swap for a token with itself"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
pool.account_id,
|
||||||
|
compute_pool_pda(amm_program_id, definition_token_a_id, definition_token_b_id),
|
||||||
|
"Pool Definition Account ID does not match PDA"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
vault_a.account_id,
|
||||||
|
compute_vault_pda(amm_program_id, pool.account_id, definition_token_a_id),
|
||||||
|
"Vault ID does not match PDA"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
vault_b.account_id,
|
||||||
|
compute_vault_pda(amm_program_id, pool.account_id, definition_token_b_id),
|
||||||
|
"Vault ID does not match PDA"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
pool_definition_lp.account_id,
|
||||||
|
compute_liquidity_token_pda(amm_program_id, pool.account_id),
|
||||||
|
"Liquidity pool Token Definition Account ID does not match PDA"
|
||||||
|
);
|
||||||
|
|
||||||
|
// TODO: return here
|
||||||
|
// Verify that Pool Account is not active
|
||||||
|
let pool_account_data = if pool.account == Account::default() {
|
||||||
|
PoolDefinition::default()
|
||||||
|
} else {
|
||||||
|
PoolDefinition::try_from(&pool.account.data)
|
||||||
|
.expect("AMM program expects a valid Pool account")
|
||||||
|
};
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
!pool_account_data.active,
|
||||||
|
"Cannot initialize an active Pool Definition"
|
||||||
|
);
|
||||||
|
|
||||||
|
// LP Token minting calculation
|
||||||
|
// We assume LP is based on the initial deposit amount for Token_A.
|
||||||
|
|
||||||
|
// Update pool account
|
||||||
|
let mut pool_post = pool.account.clone();
|
||||||
|
let pool_post_definition = PoolDefinition {
|
||||||
|
definition_token_a_id,
|
||||||
|
definition_token_b_id,
|
||||||
|
vault_a_id: vault_a.account_id,
|
||||||
|
vault_b_id: vault_b.account_id,
|
||||||
|
liquidity_pool_id: pool_definition_lp.account_id,
|
||||||
|
liquidity_pool_supply: token_a_amount.into(),
|
||||||
|
reserve_a: token_a_amount.into(),
|
||||||
|
reserve_b: token_b_amount.into(),
|
||||||
|
fees: 0u128, // TODO: we assume all fees are 0 for now.
|
||||||
|
active: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
pool_post.data = Data::from(&pool_post_definition);
|
||||||
|
let pool_post: AccountPostState = if pool.account == Account::default() {
|
||||||
|
AccountPostState::new_claimed(pool_post.clone())
|
||||||
|
} else {
|
||||||
|
AccountPostState::new(pool_post.clone())
|
||||||
|
};
|
||||||
|
|
||||||
|
let token_program_id = user_holding_a.account.program_owner;
|
||||||
|
|
||||||
|
// Chain call for Token A (user_holding_a -> Vault_A)
|
||||||
|
let call_token_a = ChainedCall::new(
|
||||||
|
token_program_id,
|
||||||
|
vec![user_holding_a.clone(), vault_a.clone()],
|
||||||
|
&token_core::Instruction::Transfer {
|
||||||
|
amount_to_transfer: token_a_amount.into(),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
// Chain call for Token B (user_holding_b -> Vault_B)
|
||||||
|
let call_token_b = ChainedCall::new(
|
||||||
|
token_program_id,
|
||||||
|
vec![user_holding_b.clone(), vault_b.clone()],
|
||||||
|
&token_core::Instruction::Transfer {
|
||||||
|
amount_to_transfer: token_b_amount.into(),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// Chain call for liquidity token (TokenLP definition -> User LP Holding)
|
||||||
|
let instruction = if pool.account == Account::default() {
|
||||||
|
token_core::Instruction::NewFungibleDefinition {
|
||||||
|
name: String::from("LP Token"),
|
||||||
|
total_supply: token_a_amount.into(),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
token_core::Instruction::Mint {
|
||||||
|
amount_to_mint: token_a_amount.into(),
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut pool_lp_auth = pool_definition_lp.clone();
|
||||||
|
pool_lp_auth.is_authorized = true;
|
||||||
|
|
||||||
|
let call_token_lp = ChainedCall::new(
|
||||||
|
token_program_id,
|
||||||
|
vec![pool_lp_auth.clone(), user_holding_lp.clone()],
|
||||||
|
&instruction,
|
||||||
|
)
|
||||||
|
.with_pda_seeds(vec![compute_liquidity_token_pda_seed(pool.account_id)]);
|
||||||
|
|
||||||
|
let chained_calls = vec![call_token_lp, call_token_b, call_token_a];
|
||||||
|
|
||||||
|
let post_states = vec![
|
||||||
|
pool_post.clone(),
|
||||||
|
AccountPostState::new(vault_a.account.clone()),
|
||||||
|
AccountPostState::new(vault_b.account.clone()),
|
||||||
|
AccountPostState::new(pool_definition_lp.account.clone()),
|
||||||
|
AccountPostState::new(user_holding_a.account.clone()),
|
||||||
|
AccountPostState::new(user_holding_b.account.clone()),
|
||||||
|
AccountPostState::new(user_holding_lp.account.clone()),
|
||||||
|
];
|
||||||
|
|
||||||
|
(post_states.clone(), chained_calls)
|
||||||
|
}
|
||||||
166
programs/amm/src/remove.rs
Normal file
166
programs/amm/src/remove.rs
Normal file
@ -0,0 +1,166 @@
|
|||||||
|
use std::num::NonZeroU128;
|
||||||
|
|
||||||
|
use amm_core::{PoolDefinition, compute_liquidity_token_pda_seed, compute_vault_pda_seed};
|
||||||
|
use nssa_core::{
|
||||||
|
account::{AccountWithMetadata, Data},
|
||||||
|
program::{AccountPostState, ChainedCall},
|
||||||
|
};
|
||||||
|
|
||||||
|
#[expect(clippy::too_many_arguments, reason = "TODO: Fix later")]
|
||||||
|
pub fn remove_liquidity(
|
||||||
|
pool: AccountWithMetadata,
|
||||||
|
vault_a: AccountWithMetadata,
|
||||||
|
vault_b: AccountWithMetadata,
|
||||||
|
pool_definition_lp: AccountWithMetadata,
|
||||||
|
user_holding_a: AccountWithMetadata,
|
||||||
|
user_holding_b: AccountWithMetadata,
|
||||||
|
user_holding_lp: AccountWithMetadata,
|
||||||
|
remove_liquidity_amount: NonZeroU128,
|
||||||
|
min_amount_to_remove_token_a: u128,
|
||||||
|
min_amount_to_remove_token_b: u128,
|
||||||
|
) -> (Vec<AccountPostState>, Vec<ChainedCall>) {
|
||||||
|
let remove_liquidity_amount: u128 = remove_liquidity_amount.into();
|
||||||
|
|
||||||
|
// 1. Fetch Pool state
|
||||||
|
let pool_def_data = PoolDefinition::try_from(&pool.account.data)
|
||||||
|
.expect("Remove liquidity: AMM Program expects a valid Pool Definition Account");
|
||||||
|
|
||||||
|
assert!(pool_def_data.active, "Pool is inactive");
|
||||||
|
assert_eq!(
|
||||||
|
pool_def_data.liquidity_pool_id, pool_definition_lp.account_id,
|
||||||
|
"LP definition mismatch"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
vault_a.account_id, pool_def_data.vault_a_id,
|
||||||
|
"Vault A was not provided"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
vault_b.account_id, pool_def_data.vault_b_id,
|
||||||
|
"Vault B was not provided"
|
||||||
|
);
|
||||||
|
|
||||||
|
// Vault addresses do not need to be checked with PDA
|
||||||
|
// calculation for setting authorization since stored
|
||||||
|
// in the Pool Definition.
|
||||||
|
let mut running_vault_a = vault_a.clone();
|
||||||
|
let mut running_vault_b = vault_b.clone();
|
||||||
|
running_vault_a.is_authorized = true;
|
||||||
|
running_vault_b.is_authorized = true;
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
min_amount_to_remove_token_a != 0,
|
||||||
|
"Minimum withdraw amount must be nonzero"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
min_amount_to_remove_token_b != 0,
|
||||||
|
"Minimum withdraw amount must be nonzero"
|
||||||
|
);
|
||||||
|
|
||||||
|
// 2. Compute withdrawal amounts
|
||||||
|
let user_holding_lp_data = token_core::TokenHolding::try_from(&user_holding_lp.account.data)
|
||||||
|
.expect("Remove liquidity: AMM Program expects a valid Token Account for liquidity token");
|
||||||
|
let token_core::TokenHolding::Fungible {
|
||||||
|
definition_id: _,
|
||||||
|
balance: user_lp_balance,
|
||||||
|
} = user_holding_lp_data
|
||||||
|
else {
|
||||||
|
panic!(
|
||||||
|
"Remove liquidity: AMM Program expects a valid Fungible Token Holding Account for liquidity token"
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
user_lp_balance <= pool_def_data.liquidity_pool_supply,
|
||||||
|
"Invalid liquidity account provided"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
user_holding_lp_data.definition_id(),
|
||||||
|
pool_def_data.liquidity_pool_id,
|
||||||
|
"Invalid liquidity account provided"
|
||||||
|
);
|
||||||
|
|
||||||
|
let withdraw_amount_a =
|
||||||
|
(pool_def_data.reserve_a * remove_liquidity_amount) / pool_def_data.liquidity_pool_supply;
|
||||||
|
let withdraw_amount_b =
|
||||||
|
(pool_def_data.reserve_b * remove_liquidity_amount) / pool_def_data.liquidity_pool_supply;
|
||||||
|
|
||||||
|
// 3. Validate and slippage check
|
||||||
|
assert!(
|
||||||
|
withdraw_amount_a >= min_amount_to_remove_token_a,
|
||||||
|
"Insufficient minimal withdraw amount (Token A) provided for liquidity amount"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
withdraw_amount_b >= min_amount_to_remove_token_b,
|
||||||
|
"Insufficient minimal withdraw amount (Token B) provided for liquidity amount"
|
||||||
|
);
|
||||||
|
|
||||||
|
// 4. Calculate LP to reduce cap by
|
||||||
|
let delta_lp: u128 = (pool_def_data.liquidity_pool_supply * remove_liquidity_amount)
|
||||||
|
/ pool_def_data.liquidity_pool_supply;
|
||||||
|
|
||||||
|
let active: bool = pool_def_data.liquidity_pool_supply - delta_lp != 0;
|
||||||
|
|
||||||
|
// 5. Update pool account
|
||||||
|
let mut pool_post = pool.account.clone();
|
||||||
|
let pool_post_definition = PoolDefinition {
|
||||||
|
liquidity_pool_supply: pool_def_data.liquidity_pool_supply - delta_lp,
|
||||||
|
reserve_a: pool_def_data.reserve_a - withdraw_amount_a,
|
||||||
|
reserve_b: pool_def_data.reserve_b - withdraw_amount_b,
|
||||||
|
active,
|
||||||
|
..pool_def_data.clone()
|
||||||
|
};
|
||||||
|
|
||||||
|
pool_post.data = Data::from(&pool_post_definition);
|
||||||
|
|
||||||
|
let token_program_id = user_holding_a.account.program_owner;
|
||||||
|
|
||||||
|
// Chaincall for Token A withdraw
|
||||||
|
let call_token_a = ChainedCall::new(
|
||||||
|
token_program_id,
|
||||||
|
vec![running_vault_a, user_holding_a.clone()],
|
||||||
|
&token_core::Instruction::Transfer {
|
||||||
|
amount_to_transfer: withdraw_amount_a,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.with_pda_seeds(vec![compute_vault_pda_seed(
|
||||||
|
pool.account_id,
|
||||||
|
pool_def_data.definition_token_a_id,
|
||||||
|
)]);
|
||||||
|
// Chaincall for Token B withdraw
|
||||||
|
let call_token_b = ChainedCall::new(
|
||||||
|
token_program_id,
|
||||||
|
vec![running_vault_b, user_holding_b.clone()],
|
||||||
|
&token_core::Instruction::Transfer {
|
||||||
|
amount_to_transfer: withdraw_amount_b,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.with_pda_seeds(vec![compute_vault_pda_seed(
|
||||||
|
pool.account_id,
|
||||||
|
pool_def_data.definition_token_b_id,
|
||||||
|
)]);
|
||||||
|
// Chaincall for LP adjustment
|
||||||
|
let mut pool_definition_lp_auth = pool_definition_lp.clone();
|
||||||
|
pool_definition_lp_auth.is_authorized = true;
|
||||||
|
let call_token_lp = ChainedCall::new(
|
||||||
|
token_program_id,
|
||||||
|
vec![pool_definition_lp_auth, user_holding_lp.clone()],
|
||||||
|
&token_core::Instruction::Burn {
|
||||||
|
amount_to_burn: delta_lp,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.with_pda_seeds(vec![compute_liquidity_token_pda_seed(pool.account_id)]);
|
||||||
|
|
||||||
|
let chained_calls = vec![call_token_lp, call_token_b, call_token_a];
|
||||||
|
|
||||||
|
let post_states = vec![
|
||||||
|
AccountPostState::new(pool_post.clone()),
|
||||||
|
AccountPostState::new(vault_a.account.clone()),
|
||||||
|
AccountPostState::new(vault_b.account.clone()),
|
||||||
|
AccountPostState::new(pool_definition_lp.account.clone()),
|
||||||
|
AccountPostState::new(user_holding_a.account.clone()),
|
||||||
|
AccountPostState::new(user_holding_b.account.clone()),
|
||||||
|
AccountPostState::new(user_holding_lp.account.clone()),
|
||||||
|
];
|
||||||
|
|
||||||
|
(post_states, chained_calls)
|
||||||
|
}
|
||||||
176
programs/amm/src/swap.rs
Normal file
176
programs/amm/src/swap.rs
Normal file
@ -0,0 +1,176 @@
|
|||||||
|
pub use amm_core::{PoolDefinition, compute_liquidity_token_pda_seed, compute_vault_pda_seed};
|
||||||
|
use nssa_core::{
|
||||||
|
account::{AccountId, AccountWithMetadata, Data},
|
||||||
|
program::{AccountPostState, ChainedCall},
|
||||||
|
};
|
||||||
|
|
||||||
|
#[expect(clippy::too_many_arguments, reason = "TODO: Fix later")]
|
||||||
|
pub fn swap(
|
||||||
|
pool: AccountWithMetadata,
|
||||||
|
vault_a: AccountWithMetadata,
|
||||||
|
vault_b: AccountWithMetadata,
|
||||||
|
user_holding_a: AccountWithMetadata,
|
||||||
|
user_holding_b: AccountWithMetadata,
|
||||||
|
swap_amount_in: u128,
|
||||||
|
min_amount_out: u128,
|
||||||
|
token_in_id: AccountId,
|
||||||
|
) -> (Vec<AccountPostState>, Vec<ChainedCall>) {
|
||||||
|
// Verify vaults are in fact vaults
|
||||||
|
let pool_def_data = PoolDefinition::try_from(&pool.account.data)
|
||||||
|
.expect("Swap: AMM Program expects a valid Pool Definition Account");
|
||||||
|
|
||||||
|
assert!(pool_def_data.active, "Pool is inactive");
|
||||||
|
assert_eq!(
|
||||||
|
vault_a.account_id, pool_def_data.vault_a_id,
|
||||||
|
"Vault A was not provided"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
vault_b.account_id, pool_def_data.vault_b_id,
|
||||||
|
"Vault B was not provided"
|
||||||
|
);
|
||||||
|
|
||||||
|
// fetch pool reserves
|
||||||
|
// validates reserves is at least the vaults' balances
|
||||||
|
let vault_a_token_holding = token_core::TokenHolding::try_from(&vault_a.account.data)
|
||||||
|
.expect("Swap: AMM Program expects a valid Token Holding Account for Vault A");
|
||||||
|
let token_core::TokenHolding::Fungible {
|
||||||
|
definition_id: _,
|
||||||
|
balance: vault_a_balance,
|
||||||
|
} = vault_a_token_holding
|
||||||
|
else {
|
||||||
|
panic!("Swap: AMM Program expects a valid Fungible Token Holding Account for Vault A");
|
||||||
|
};
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
vault_a_balance >= pool_def_data.reserve_a,
|
||||||
|
"Reserve for Token A exceeds vault balance"
|
||||||
|
);
|
||||||
|
|
||||||
|
let vault_b_token_holding = token_core::TokenHolding::try_from(&vault_b.account.data)
|
||||||
|
.expect("Swap: AMM Program expects a valid Token Holding Account for Vault B");
|
||||||
|
let token_core::TokenHolding::Fungible {
|
||||||
|
definition_id: _,
|
||||||
|
balance: vault_b_balance,
|
||||||
|
} = vault_b_token_holding
|
||||||
|
else {
|
||||||
|
panic!("Swap: AMM Program expects a valid Fungible Token Holding Account for Vault B");
|
||||||
|
};
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
vault_b_balance >= pool_def_data.reserve_b,
|
||||||
|
"Reserve for Token B exceeds vault balance"
|
||||||
|
);
|
||||||
|
|
||||||
|
let (chained_calls, [deposit_a, withdraw_a], [deposit_b, withdraw_b]) =
|
||||||
|
if token_in_id == pool_def_data.definition_token_a_id {
|
||||||
|
let (chained_calls, deposit_a, withdraw_b) = swap_logic(
|
||||||
|
user_holding_a.clone(),
|
||||||
|
vault_a.clone(),
|
||||||
|
vault_b.clone(),
|
||||||
|
user_holding_b.clone(),
|
||||||
|
swap_amount_in,
|
||||||
|
min_amount_out,
|
||||||
|
pool_def_data.reserve_a,
|
||||||
|
pool_def_data.reserve_b,
|
||||||
|
pool.account_id,
|
||||||
|
);
|
||||||
|
|
||||||
|
(chained_calls, [deposit_a, 0], [0, withdraw_b])
|
||||||
|
} else if token_in_id == pool_def_data.definition_token_b_id {
|
||||||
|
let (chained_calls, deposit_b, withdraw_a) = swap_logic(
|
||||||
|
user_holding_b.clone(),
|
||||||
|
vault_b.clone(),
|
||||||
|
vault_a.clone(),
|
||||||
|
user_holding_a.clone(),
|
||||||
|
swap_amount_in,
|
||||||
|
min_amount_out,
|
||||||
|
pool_def_data.reserve_b,
|
||||||
|
pool_def_data.reserve_a,
|
||||||
|
pool.account_id,
|
||||||
|
);
|
||||||
|
|
||||||
|
(chained_calls, [0, withdraw_a], [deposit_b, 0])
|
||||||
|
} else {
|
||||||
|
panic!("AccountId is not a token type for the pool");
|
||||||
|
};
|
||||||
|
|
||||||
|
// Update pool account
|
||||||
|
let mut pool_post = pool.account.clone();
|
||||||
|
let pool_post_definition = PoolDefinition {
|
||||||
|
reserve_a: pool_def_data.reserve_a + deposit_a - withdraw_a,
|
||||||
|
reserve_b: pool_def_data.reserve_b + deposit_b - withdraw_b,
|
||||||
|
..pool_def_data
|
||||||
|
};
|
||||||
|
|
||||||
|
pool_post.data = Data::from(&pool_post_definition);
|
||||||
|
|
||||||
|
let post_states = vec![
|
||||||
|
AccountPostState::new(pool_post.clone()),
|
||||||
|
AccountPostState::new(vault_a.account.clone()),
|
||||||
|
AccountPostState::new(vault_b.account.clone()),
|
||||||
|
AccountPostState::new(user_holding_a.account.clone()),
|
||||||
|
AccountPostState::new(user_holding_b.account.clone()),
|
||||||
|
];
|
||||||
|
|
||||||
|
(post_states, chained_calls)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[expect(clippy::too_many_arguments, reason = "TODO: Fix later")]
|
||||||
|
fn swap_logic(
|
||||||
|
user_deposit: AccountWithMetadata,
|
||||||
|
vault_deposit: AccountWithMetadata,
|
||||||
|
vault_withdraw: AccountWithMetadata,
|
||||||
|
user_withdraw: AccountWithMetadata,
|
||||||
|
swap_amount_in: u128,
|
||||||
|
min_amount_out: u128,
|
||||||
|
reserve_deposit_vault_amount: u128,
|
||||||
|
reserve_withdraw_vault_amount: u128,
|
||||||
|
pool_id: AccountId,
|
||||||
|
) -> (Vec<ChainedCall>, u128, u128) {
|
||||||
|
// Compute withdraw amount
|
||||||
|
// Maintains pool constant product
|
||||||
|
// k = pool_def_data.reserve_a * pool_def_data.reserve_b;
|
||||||
|
let withdraw_amount = (reserve_withdraw_vault_amount * swap_amount_in)
|
||||||
|
/ (reserve_deposit_vault_amount + swap_amount_in);
|
||||||
|
|
||||||
|
// Slippage check
|
||||||
|
assert!(
|
||||||
|
min_amount_out <= withdraw_amount,
|
||||||
|
"Withdraw amount is less than minimal amount out"
|
||||||
|
);
|
||||||
|
assert!(withdraw_amount != 0, "Withdraw amount should be nonzero");
|
||||||
|
|
||||||
|
let token_program_id = user_deposit.account.program_owner;
|
||||||
|
|
||||||
|
let mut chained_calls = Vec::new();
|
||||||
|
chained_calls.push(ChainedCall::new(
|
||||||
|
token_program_id,
|
||||||
|
vec![user_deposit, vault_deposit],
|
||||||
|
&token_core::Instruction::Transfer {
|
||||||
|
amount_to_transfer: swap_amount_in,
|
||||||
|
},
|
||||||
|
));
|
||||||
|
|
||||||
|
let mut vault_withdraw = vault_withdraw.clone();
|
||||||
|
vault_withdraw.is_authorized = true;
|
||||||
|
|
||||||
|
let pda_seed = compute_vault_pda_seed(
|
||||||
|
pool_id,
|
||||||
|
token_core::TokenHolding::try_from(&vault_withdraw.account.data)
|
||||||
|
.expect("Swap Logic: AMM Program expects valid token data")
|
||||||
|
.definition_id(),
|
||||||
|
);
|
||||||
|
|
||||||
|
chained_calls.push(
|
||||||
|
ChainedCall::new(
|
||||||
|
token_program_id,
|
||||||
|
vec![vault_withdraw, user_withdraw],
|
||||||
|
&token_core::Instruction::Transfer {
|
||||||
|
amount_to_transfer: withdraw_amount,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.with_pda_seeds(vec![pda_seed]),
|
||||||
|
);
|
||||||
|
|
||||||
|
(chained_calls, swap_amount_in, withdraw_amount)
|
||||||
|
}
|
||||||
1719
programs/amm/src/tests.rs
Normal file
1719
programs/amm/src/tests.rs
Normal file
File diff suppressed because it is too large
Load Diff
@ -23,7 +23,7 @@ pub trait BlockSettlementClientTrait: Clone {
|
|||||||
fn bedrock_signing_key(&self) -> &Ed25519Key;
|
fn bedrock_signing_key(&self) -> &Ed25519Key;
|
||||||
|
|
||||||
/// Post a transaction to the node.
|
/// Post a transaction to the node.
|
||||||
async fn submit_block_to_bedrock(&self, block: &Block) -> Result<MsgId>;
|
async fn submit_inscribe_tx_to_bedrock(&self, tx: SignedMantleTx) -> Result<()>;
|
||||||
|
|
||||||
/// Create and sign a transaction for inscribing data.
|
/// Create and sign a transaction for inscribing data.
|
||||||
fn create_inscribe_tx(&self, block: &Block) -> Result<(SignedMantleTx, MsgId)> {
|
fn create_inscribe_tx(&self, block: &Block) -> Result<(SignedMantleTx, MsgId)> {
|
||||||
@ -89,16 +89,13 @@ impl BlockSettlementClientTrait for BlockSettlementClient {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn submit_block_to_bedrock(&self, block: &Block) -> Result<MsgId> {
|
async fn submit_inscribe_tx_to_bedrock(&self, tx: SignedMantleTx) -> Result<()> {
|
||||||
let (tx, new_msg_id) = self.create_inscribe_tx(block)?;
|
|
||||||
|
|
||||||
// Post the transaction
|
|
||||||
self.bedrock_client
|
self.bedrock_client
|
||||||
.post_transaction(tx)
|
.post_transaction(tx)
|
||||||
.await
|
.await
|
||||||
.context("Failed to post transaction to Bedrock")?;
|
.context("Failed to post transaction to Bedrock")?;
|
||||||
|
|
||||||
Ok(new_msg_id)
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn bedrock_channel_id(&self) -> ChannelId {
|
fn bedrock_channel_id(&self) -> ChannelId {
|
||||||
|
|||||||
@ -1,7 +1,11 @@
|
|||||||
use std::{collections::HashMap, path::Path};
|
use std::{collections::HashMap, path::Path};
|
||||||
|
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use common::{HashType, block::Block, transaction::NSSATransaction};
|
use common::{
|
||||||
|
HashType,
|
||||||
|
block::{Block, BlockMeta, MantleMsgId},
|
||||||
|
transaction::NSSATransaction,
|
||||||
|
};
|
||||||
use nssa::V02State;
|
use nssa::V02State;
|
||||||
use storage::sequencer::RocksDBIO;
|
use storage::sequencer::RocksDBIO;
|
||||||
|
|
||||||
@ -20,10 +24,10 @@ impl SequencerStore {
|
|||||||
/// ATTENTION: Will overwrite genesis block.
|
/// ATTENTION: Will overwrite genesis block.
|
||||||
pub fn open_db_with_genesis(
|
pub fn open_db_with_genesis(
|
||||||
location: &Path,
|
location: &Path,
|
||||||
genesis_block: Option<&Block>,
|
genesis_block: Option<(&Block, MantleMsgId)>,
|
||||||
signing_key: nssa::PrivateKey,
|
signing_key: nssa::PrivateKey,
|
||||||
) -> Result<Self> {
|
) -> Result<Self> {
|
||||||
let tx_hash_to_block_map = if let Some(block) = &genesis_block {
|
let tx_hash_to_block_map = if let Some((block, _msg_id)) = &genesis_block {
|
||||||
block_to_transactions_map(block)
|
block_to_transactions_map(block)
|
||||||
} else {
|
} else {
|
||||||
HashMap::new()
|
HashMap::new()
|
||||||
@ -54,6 +58,10 @@ impl SequencerStore {
|
|||||||
Ok(self.dbio.delete_block(block_id)?)
|
Ok(self.dbio.delete_block(block_id)?)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn mark_block_as_finalized(&mut self, block_id: u64) -> Result<()> {
|
||||||
|
Ok(self.dbio.mark_block_as_finalized(block_id)?)
|
||||||
|
}
|
||||||
|
|
||||||
/// Returns the transaction corresponding to the given hash, if it exists in the blockchain.
|
/// Returns the transaction corresponding to the given hash, if it exists in the blockchain.
|
||||||
pub fn get_transaction_by_hash(&self, hash: HashType) -> Option<NSSATransaction> {
|
pub fn get_transaction_by_hash(&self, hash: HashType) -> Option<NSSATransaction> {
|
||||||
let block_id = self.tx_hash_to_block_map.get(&hash);
|
let block_id = self.tx_hash_to_block_map.get(&hash);
|
||||||
@ -68,8 +76,8 @@ impl SequencerStore {
|
|||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn insert(&mut self, tx: &NSSATransaction, block_id: u64) {
|
pub fn latest_block_meta(&self) -> Result<BlockMeta> {
|
||||||
self.tx_hash_to_block_map.insert(tx.hash(), block_id);
|
Ok(self.dbio.latest_block_meta()?)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn genesis_id(&self) -> u64 {
|
pub fn genesis_id(&self) -> u64 {
|
||||||
@ -84,9 +92,14 @@ impl SequencerStore {
|
|||||||
self.dbio.get_all_blocks().map(|res| Ok(res?))
|
self.dbio.get_all_blocks().map(|res| Ok(res?))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn update(&mut self, block: &Block, state: &V02State) -> Result<()> {
|
pub(crate) fn update(
|
||||||
|
&mut self,
|
||||||
|
block: &Block,
|
||||||
|
msg_id: MantleMsgId,
|
||||||
|
state: &V02State,
|
||||||
|
) -> Result<()> {
|
||||||
let new_transactions_map = block_to_transactions_map(block);
|
let new_transactions_map = block_to_transactions_map(block);
|
||||||
self.dbio.atomic_update(block, state)?;
|
self.dbio.atomic_update(block, msg_id, state)?;
|
||||||
self.tx_hash_to_block_map.extend(new_transactions_map);
|
self.tx_hash_to_block_map.extend(new_transactions_map);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@ -128,8 +141,12 @@ mod tests {
|
|||||||
|
|
||||||
let genesis_block = genesis_block_hashable_data.into_pending_block(&signing_key, [0; 32]);
|
let genesis_block = genesis_block_hashable_data.into_pending_block(&signing_key, [0; 32]);
|
||||||
// Start an empty node store
|
// Start an empty node store
|
||||||
let mut node_store =
|
let mut node_store = SequencerStore::open_db_with_genesis(
|
||||||
SequencerStore::open_db_with_genesis(path, Some(&genesis_block), signing_key).unwrap();
|
path,
|
||||||
|
Some((&genesis_block, [0; 32])),
|
||||||
|
signing_key,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
let tx = common::test_utils::produce_dummy_empty_transaction();
|
let tx = common::test_utils::produce_dummy_empty_transaction();
|
||||||
let block = common::test_utils::produce_dummy_block(1, None, vec![tx.clone()]);
|
let block = common::test_utils::produce_dummy_block(1, None, vec![tx.clone()]);
|
||||||
@ -139,9 +156,126 @@ mod tests {
|
|||||||
assert_eq!(None, retrieved_tx);
|
assert_eq!(None, retrieved_tx);
|
||||||
// Add the block with the transaction
|
// Add the block with the transaction
|
||||||
let dummy_state = V02State::new_with_genesis_accounts(&[], &[]);
|
let dummy_state = V02State::new_with_genesis_accounts(&[], &[]);
|
||||||
node_store.update(&block, &dummy_state).unwrap();
|
node_store.update(&block, [1; 32], &dummy_state).unwrap();
|
||||||
// Try again
|
// Try again
|
||||||
let retrieved_tx = node_store.get_transaction_by_hash(tx.hash());
|
let retrieved_tx = node_store.get_transaction_by_hash(tx.hash());
|
||||||
assert_eq!(Some(tx), retrieved_tx);
|
assert_eq!(Some(tx), retrieved_tx);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_latest_block_meta_returns_genesis_meta_initially() {
|
||||||
|
let temp_dir = tempdir().unwrap();
|
||||||
|
let path = temp_dir.path();
|
||||||
|
|
||||||
|
let signing_key = sequencer_sign_key_for_testing();
|
||||||
|
|
||||||
|
let genesis_block_hashable_data = HashableBlockData {
|
||||||
|
block_id: 0,
|
||||||
|
prev_block_hash: HashType([0; 32]),
|
||||||
|
timestamp: 0,
|
||||||
|
transactions: vec![],
|
||||||
|
};
|
||||||
|
|
||||||
|
let genesis_block = genesis_block_hashable_data.into_pending_block(&signing_key, [0; 32]);
|
||||||
|
let genesis_hash = genesis_block.header.hash;
|
||||||
|
|
||||||
|
let node_store = SequencerStore::open_db_with_genesis(
|
||||||
|
path,
|
||||||
|
Some((&genesis_block, [0; 32])),
|
||||||
|
signing_key,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// Verify that initially the latest block hash equals genesis hash
|
||||||
|
let latest_meta = node_store.latest_block_meta().unwrap();
|
||||||
|
assert_eq!(latest_meta.hash, genesis_hash);
|
||||||
|
assert_eq!(latest_meta.msg_id, [0; 32]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_latest_block_meta_updates_after_new_block() {
|
||||||
|
let temp_dir = tempdir().unwrap();
|
||||||
|
let path = temp_dir.path();
|
||||||
|
|
||||||
|
let signing_key = sequencer_sign_key_for_testing();
|
||||||
|
|
||||||
|
let genesis_block_hashable_data = HashableBlockData {
|
||||||
|
block_id: 0,
|
||||||
|
prev_block_hash: HashType([0; 32]),
|
||||||
|
timestamp: 0,
|
||||||
|
transactions: vec![],
|
||||||
|
};
|
||||||
|
|
||||||
|
let genesis_block = genesis_block_hashable_data.into_pending_block(&signing_key, [0; 32]);
|
||||||
|
let mut node_store = SequencerStore::open_db_with_genesis(
|
||||||
|
path,
|
||||||
|
Some((&genesis_block, [0; 32])),
|
||||||
|
signing_key,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// Add a new block
|
||||||
|
let tx = common::test_utils::produce_dummy_empty_transaction();
|
||||||
|
let block = common::test_utils::produce_dummy_block(1, None, vec![tx.clone()]);
|
||||||
|
let block_hash = block.header.hash;
|
||||||
|
let block_msg_id = [1; 32];
|
||||||
|
|
||||||
|
let dummy_state = V02State::new_with_genesis_accounts(&[], &[]);
|
||||||
|
node_store
|
||||||
|
.update(&block, block_msg_id, &dummy_state)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// Verify that the latest block meta now equals the new block's hash and msg_id
|
||||||
|
let latest_meta = node_store.latest_block_meta().unwrap();
|
||||||
|
assert_eq!(latest_meta.hash, block_hash);
|
||||||
|
assert_eq!(latest_meta.msg_id, block_msg_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_mark_block_finalized() {
|
||||||
|
let temp_dir = tempdir().unwrap();
|
||||||
|
let path = temp_dir.path();
|
||||||
|
|
||||||
|
let signing_key = sequencer_sign_key_for_testing();
|
||||||
|
|
||||||
|
let genesis_block_hashable_data = HashableBlockData {
|
||||||
|
block_id: 0,
|
||||||
|
prev_block_hash: HashType([0; 32]),
|
||||||
|
timestamp: 0,
|
||||||
|
transactions: vec![],
|
||||||
|
};
|
||||||
|
|
||||||
|
let genesis_block = genesis_block_hashable_data.into_pending_block(&signing_key, [0; 32]);
|
||||||
|
let mut node_store = SequencerStore::open_db_with_genesis(
|
||||||
|
path,
|
||||||
|
Some((&genesis_block, [0; 32])),
|
||||||
|
signing_key,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// Add a new block with Pending status
|
||||||
|
let tx = common::test_utils::produce_dummy_empty_transaction();
|
||||||
|
let block = common::test_utils::produce_dummy_block(1, None, vec![tx.clone()]);
|
||||||
|
let block_id = block.header.block_id;
|
||||||
|
|
||||||
|
let dummy_state = V02State::new_with_genesis_accounts(&[], &[]);
|
||||||
|
node_store.update(&block, [1; 32], &dummy_state).unwrap();
|
||||||
|
|
||||||
|
// Verify initial status is Pending
|
||||||
|
let retrieved_block = node_store.get_block_at_id(block_id).unwrap();
|
||||||
|
assert!(matches!(
|
||||||
|
retrieved_block.bedrock_status,
|
||||||
|
common::block::BedrockStatus::Pending
|
||||||
|
));
|
||||||
|
|
||||||
|
// Mark block as finalized
|
||||||
|
node_store.mark_block_as_finalized(block_id).unwrap();
|
||||||
|
|
||||||
|
// Verify status is now Finalized
|
||||||
|
let finalized_block = node_store.get_block_at_id(block_id).unwrap();
|
||||||
|
assert!(matches!(
|
||||||
|
finalized_block.bedrock_status,
|
||||||
|
common::block::BedrockStatus::Finalized
|
||||||
|
));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,11 +1,12 @@
|
|||||||
use std::{fmt::Display, path::Path, time::Instant};
|
use std::{fmt::Display, path::Path, time::Instant};
|
||||||
|
|
||||||
use anyhow::{Result, anyhow};
|
use anyhow::{Context as _, Result, anyhow};
|
||||||
|
use bedrock_client::SignedMantleTx;
|
||||||
#[cfg(feature = "testnet")]
|
#[cfg(feature = "testnet")]
|
||||||
use common::PINATA_BASE58;
|
use common::PINATA_BASE58;
|
||||||
use common::{
|
use common::{
|
||||||
HashType,
|
HashType,
|
||||||
block::{BedrockStatus, Block, HashableBlockData, MantleMsgId},
|
block::{BedrockStatus, Block, HashableBlockData},
|
||||||
transaction::NSSATransaction,
|
transaction::NSSATransaction,
|
||||||
};
|
};
|
||||||
use config::SequencerConfig;
|
use config::SequencerConfig;
|
||||||
@ -15,7 +16,7 @@ use mempool::{MemPool, MemPoolHandle};
|
|||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
block_settlement_client::{BlockSettlementClient, BlockSettlementClientTrait},
|
block_settlement_client::{BlockSettlementClient, BlockSettlementClientTrait, MsgId},
|
||||||
block_store::SequencerStore,
|
block_store::SequencerStore,
|
||||||
indexer_client::{IndexerClient, IndexerClientTrait},
|
indexer_client::{IndexerClient, IndexerClientTrait},
|
||||||
};
|
};
|
||||||
@ -38,7 +39,6 @@ pub struct SequencerCore<
|
|||||||
chain_height: u64,
|
chain_height: u64,
|
||||||
block_settlement_client: BC,
|
block_settlement_client: BC,
|
||||||
indexer_client: IC,
|
indexer_client: IC,
|
||||||
last_bedrock_msg_id: MantleMsgId,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||||
@ -72,17 +72,35 @@ impl<BC: BlockSettlementClientTrait, IC: IndexerClientTrait> SequencerCore<BC, I
|
|||||||
};
|
};
|
||||||
|
|
||||||
let signing_key = nssa::PrivateKey::try_new(config.signing_key).unwrap();
|
let signing_key = nssa::PrivateKey::try_new(config.signing_key).unwrap();
|
||||||
let channel_genesis_msg_id = [0; 32];
|
let genesis_parent_msg_id = [0; 32];
|
||||||
let genesis_block = hashable_data.into_pending_block(&signing_key, channel_genesis_msg_id);
|
let genesis_block = hashable_data.into_pending_block(&signing_key, genesis_parent_msg_id);
|
||||||
|
|
||||||
|
let bedrock_signing_key =
|
||||||
|
load_or_create_signing_key(&config.home.join("bedrock_signing_key"))
|
||||||
|
.expect("Failed to load or create bedrock signing key");
|
||||||
|
|
||||||
|
let block_settlement_client = BC::new(&config.bedrock_config, bedrock_signing_key)
|
||||||
|
.expect("Failed to initialize Block Settlement Client");
|
||||||
|
|
||||||
|
let indexer_client = IC::new(&config.indexer_rpc_url)
|
||||||
|
.await
|
||||||
|
.expect("Failed to create Indexer Client");
|
||||||
|
|
||||||
|
let (_tx, genesis_msg_id) = block_settlement_client
|
||||||
|
.create_inscribe_tx(&genesis_block)
|
||||||
|
.expect("Failed to create inscribe tx for genesis block");
|
||||||
|
|
||||||
// Sequencer should panic if unable to open db,
|
// Sequencer should panic if unable to open db,
|
||||||
// as fixing this issue may require actions non-native to program scope
|
// as fixing this issue may require actions non-native to program scope
|
||||||
let store = SequencerStore::open_db_with_genesis(
|
let store = SequencerStore::open_db_with_genesis(
|
||||||
&config.home.join("rocksdb"),
|
&config.home.join("rocksdb"),
|
||||||
Some(&genesis_block),
|
Some((&genesis_block, genesis_msg_id.into())),
|
||||||
signing_key,
|
signing_key,
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
let latest_block_meta = store
|
||||||
|
.latest_block_meta()
|
||||||
|
.expect("Failed to read latest block meta from store");
|
||||||
|
|
||||||
#[cfg_attr(not(feature = "testnet"), allow(unused_mut))]
|
#[cfg_attr(not(feature = "testnet"), allow(unused_mut))]
|
||||||
let mut state = match store.get_nssa_state() {
|
let mut state = match store.get_nssa_state() {
|
||||||
@ -123,30 +141,15 @@ impl<BC: BlockSettlementClientTrait, IC: IndexerClientTrait> SequencerCore<BC, I
|
|||||||
state.add_pinata_program(PINATA_BASE58.parse().unwrap());
|
state.add_pinata_program(PINATA_BASE58.parse().unwrap());
|
||||||
|
|
||||||
let (mempool, mempool_handle) = MemPool::new(config.mempool_max_size);
|
let (mempool, mempool_handle) = MemPool::new(config.mempool_max_size);
|
||||||
let bedrock_signing_key =
|
|
||||||
load_or_create_signing_key(&config.home.join("bedrock_signing_key"))
|
|
||||||
.expect("Failed to load or create signing key");
|
|
||||||
let block_settlement_client = BC::new(&config.bedrock_config, bedrock_signing_key)
|
|
||||||
.expect("Failed to initialize Block Settlement Client");
|
|
||||||
|
|
||||||
// let (_, msg_id) = block_settlement_client
|
|
||||||
// .create_inscribe_tx(&genesis_block)
|
|
||||||
// .expect("Inscription transaction with genesis block should be constructible");
|
|
||||||
// let last_bedrock_msg_id = msg_id.into();
|
|
||||||
|
|
||||||
let indexer_client = IC::new(&config.indexer_rpc_url)
|
|
||||||
.await
|
|
||||||
.expect("Failed to create Indexer Client");
|
|
||||||
|
|
||||||
let sequencer_core = Self {
|
let sequencer_core = Self {
|
||||||
state,
|
state,
|
||||||
store,
|
store,
|
||||||
mempool,
|
mempool,
|
||||||
chain_height: config.genesis_id,
|
chain_height: latest_block_meta.id,
|
||||||
sequencer_config: config,
|
sequencer_config: config,
|
||||||
block_settlement_client,
|
block_settlement_client,
|
||||||
indexer_client,
|
indexer_client,
|
||||||
last_bedrock_msg_id: channel_genesis_msg_id,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
(sequencer_core, mempool_handle)
|
(sequencer_core, mempool_handle)
|
||||||
@ -172,14 +175,15 @@ impl<BC: BlockSettlementClientTrait, IC: IndexerClientTrait> SequencerCore<BC, I
|
|||||||
|
|
||||||
pub async fn produce_new_block_and_post_to_settlement_layer(&mut self) -> Result<u64> {
|
pub async fn produce_new_block_and_post_to_settlement_layer(&mut self) -> Result<u64> {
|
||||||
{
|
{
|
||||||
let block = self.produce_new_block_with_mempool_transactions()?;
|
let (tx, msg_id) = self
|
||||||
|
.produce_new_block_with_mempool_transactions()
|
||||||
|
.context("Failed to produce new block with mempool transactions")?;
|
||||||
match self
|
match self
|
||||||
.block_settlement_client
|
.block_settlement_client
|
||||||
.submit_block_to_bedrock(&block)
|
.submit_inscribe_tx_to_bedrock(tx)
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
Ok(msg_id) => {
|
Ok(()) => {
|
||||||
self.last_bedrock_msg_id = msg_id.into();
|
|
||||||
info!("Posted block data to Bedrock, msg_id: {msg_id:?}");
|
info!("Posted block data to Bedrock, msg_id: {msg_id:?}");
|
||||||
}
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
@ -191,8 +195,10 @@ impl<BC: BlockSettlementClientTrait, IC: IndexerClientTrait> SequencerCore<BC, I
|
|||||||
Ok(self.chain_height)
|
Ok(self.chain_height)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Produces new block from transactions in mempool
|
/// Produces new block from transactions in mempool and packs it into a SignedMantleTx.
|
||||||
pub fn produce_new_block_with_mempool_transactions(&mut self) -> Result<Block> {
|
pub fn produce_new_block_with_mempool_transactions(
|
||||||
|
&mut self,
|
||||||
|
) -> Result<(SignedMantleTx, MsgId)> {
|
||||||
let now = Instant::now();
|
let now = Instant::now();
|
||||||
|
|
||||||
let new_block_height = self.chain_height + 1;
|
let new_block_height = self.chain_height + 1;
|
||||||
@ -219,41 +225,44 @@ impl<BC: BlockSettlementClientTrait, IC: IndexerClientTrait> SequencerCore<BC, I
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let prev_block_hash = self.store.get_block_at_id(self.chain_height)?.header.hash;
|
let latest_block_meta = self
|
||||||
|
.store
|
||||||
|
.latest_block_meta()
|
||||||
|
.context("Failed to get latest block meta from store")?;
|
||||||
|
|
||||||
let curr_time = chrono::Utc::now().timestamp_millis() as u64;
|
let curr_time = chrono::Utc::now().timestamp_millis() as u64;
|
||||||
|
|
||||||
let hashable_data = HashableBlockData {
|
let hashable_data = HashableBlockData {
|
||||||
block_id: new_block_height,
|
block_id: new_block_height,
|
||||||
transactions: valid_transactions,
|
transactions: valid_transactions,
|
||||||
prev_block_hash,
|
prev_block_hash: latest_block_meta.hash,
|
||||||
timestamp: curr_time,
|
timestamp: curr_time,
|
||||||
};
|
};
|
||||||
|
|
||||||
let block = hashable_data
|
let block = hashable_data
|
||||||
.clone()
|
.clone()
|
||||||
.into_pending_block(self.store.signing_key(), self.last_bedrock_msg_id);
|
.into_pending_block(self.store.signing_key(), latest_block_meta.msg_id);
|
||||||
|
|
||||||
self.store.update(&block, &self.state)?;
|
let (tx, msg_id) = self
|
||||||
|
.block_settlement_client
|
||||||
|
.create_inscribe_tx(&block)
|
||||||
|
.with_context(|| {
|
||||||
|
format!(
|
||||||
|
"Failed to create inscribe transaction for block with id {}",
|
||||||
|
block.header.block_id
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
self.store.update(&block, msg_id.into(), &self.state)?;
|
||||||
|
|
||||||
self.chain_height = new_block_height;
|
self.chain_height = new_block_height;
|
||||||
|
|
||||||
// TODO: Consider switching to `tracing` crate to have more structured and consistent logs
|
|
||||||
// e.g.
|
|
||||||
//
|
|
||||||
// ```
|
|
||||||
// info!(
|
|
||||||
// num_txs = num_txs_in_block,
|
|
||||||
// time = now.elapsed(),
|
|
||||||
// "Created block"
|
|
||||||
// );
|
|
||||||
// ```
|
|
||||||
log::info!(
|
log::info!(
|
||||||
"Created block with {} transactions in {} seconds",
|
"Created block with {} transactions in {} seconds",
|
||||||
hashable_data.transactions.len(),
|
hashable_data.transactions.len(),
|
||||||
now.elapsed().as_secs()
|
now.elapsed().as_secs()
|
||||||
);
|
);
|
||||||
Ok(block)
|
Ok((tx, msg_id))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn state(&self) -> &nssa::V02State {
|
pub fn state(&self) -> &nssa::V02State {
|
||||||
@ -287,8 +296,10 @@ impl<BC: BlockSettlementClientTrait, IC: IndexerClientTrait> SequencerCore<BC, I
|
|||||||
"Clearing pending blocks up to id: {}",
|
"Clearing pending blocks up to id: {}",
|
||||||
last_finalized_block_id
|
last_finalized_block_id
|
||||||
);
|
);
|
||||||
|
// TODO: Delete blocks instead of marking them as finalized.
|
||||||
|
// Current approach is used because we still have `GetBlockDataRequest`.
|
||||||
(first_pending_block_id..=last_finalized_block_id)
|
(first_pending_block_id..=last_finalized_block_id)
|
||||||
.try_for_each(|id| self.store.delete_block_at_id(id))
|
.try_for_each(|id| self.store.mark_block_as_finalized(id))
|
||||||
} else {
|
} else {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@ -325,6 +336,10 @@ fn load_or_create_signing_key(path: &Path) -> Result<Ed25519Key> {
|
|||||||
} else {
|
} else {
|
||||||
let mut key_bytes = [0u8; ED25519_SECRET_KEY_SIZE];
|
let mut key_bytes = [0u8; ED25519_SECRET_KEY_SIZE];
|
||||||
rand::RngCore::fill_bytes(&mut rand::thread_rng(), &mut key_bytes);
|
rand::RngCore::fill_bytes(&mut rand::thread_rng(), &mut key_bytes);
|
||||||
|
// Create parent directory if it doesn't exist
|
||||||
|
if let Some(parent) = path.parent() {
|
||||||
|
std::fs::create_dir_all(parent)?;
|
||||||
|
}
|
||||||
std::fs::write(path, key_bytes)?;
|
std::fs::write(path, key_bytes)?;
|
||||||
Ok(Ed25519Key::from_bytes(&key_bytes))
|
Ok(Ed25519Key::from_bytes(&key_bytes))
|
||||||
}
|
}
|
||||||
@ -336,11 +351,7 @@ mod tests {
|
|||||||
|
|
||||||
use base58::ToBase58;
|
use base58::ToBase58;
|
||||||
use bedrock_client::BackoffConfig;
|
use bedrock_client::BackoffConfig;
|
||||||
use common::{
|
use common::{block::AccountInitialData, test_utils::sequencer_sign_key_for_testing, transaction::{NSSATransaction, transaction_pre_check}};
|
||||||
block::AccountInitialData,
|
|
||||||
test_utils::sequencer_sign_key_for_testing,
|
|
||||||
transaction::{NSSATransaction, transaction_pre_check},
|
|
||||||
};
|
|
||||||
use logos_blockchain_core::mantle::ops::channel::ChannelId;
|
use logos_blockchain_core::mantle::ops::channel::ChannelId;
|
||||||
use mempool::MemPoolHandle;
|
use mempool::MemPoolHandle;
|
||||||
use nssa::{AccountId, PrivateKey};
|
use nssa::{AccountId, PrivateKey};
|
||||||
@ -384,13 +395,13 @@ mod tests {
|
|||||||
|
|
||||||
fn setup_sequencer_config() -> SequencerConfig {
|
fn setup_sequencer_config() -> SequencerConfig {
|
||||||
let acc1_account_id: Vec<u8> = vec![
|
let acc1_account_id: Vec<u8> = vec![
|
||||||
208, 122, 210, 232, 75, 39, 250, 0, 194, 98, 240, 161, 238, 160, 255, 53, 202, 9, 115,
|
148, 179, 206, 253, 199, 51, 82, 86, 232, 2, 152, 122, 80, 243, 54, 207, 237, 112, 83,
|
||||||
84, 126, 106, 16, 111, 114, 241, 147, 194, 220, 131, 139, 68,
|
153, 44, 59, 204, 49, 128, 84, 160, 227, 216, 149, 97, 102,
|
||||||
];
|
];
|
||||||
|
|
||||||
let acc2_account_id: Vec<u8> = vec![
|
let acc2_account_id: Vec<u8> = vec![
|
||||||
231, 174, 119, 197, 239, 26, 5, 153, 147, 68, 175, 73, 159, 199, 138, 23, 5, 57, 141,
|
30, 145, 107, 3, 207, 73, 192, 230, 160, 63, 238, 207, 18, 69, 54, 216, 103, 244, 92,
|
||||||
98, 237, 6, 207, 46, 20, 121, 246, 222, 248, 154, 57, 188,
|
94, 124, 248, 42, 16, 141, 19, 119, 18, 14, 226, 140, 204,
|
||||||
];
|
];
|
||||||
|
|
||||||
let initial_acc1 = AccountInitialData {
|
let initial_acc1 = AccountInitialData {
|
||||||
@ -632,9 +643,9 @@ mod tests {
|
|||||||
let tx = common::test_utils::produce_dummy_empty_transaction();
|
let tx = common::test_utils::produce_dummy_empty_transaction();
|
||||||
mempool_handle.push(tx).await.unwrap();
|
mempool_handle.push(tx).await.unwrap();
|
||||||
|
|
||||||
let block = sequencer.produce_new_block_with_mempool_transactions();
|
let result = sequencer.produce_new_block_with_mempool_transactions();
|
||||||
assert!(block.is_ok());
|
assert!(result.is_ok());
|
||||||
assert_eq!(block.unwrap().header.block_id, genesis_height + 1);
|
assert_eq!(sequencer.chain_height, genesis_height + 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
@ -657,12 +668,13 @@ mod tests {
|
|||||||
mempool_handle.push(tx_replay).await.unwrap();
|
mempool_handle.push(tx_replay).await.unwrap();
|
||||||
|
|
||||||
// Create block
|
// Create block
|
||||||
let current_height = sequencer
|
sequencer
|
||||||
.produce_new_block_with_mempool_transactions()
|
.produce_new_block_with_mempool_transactions()
|
||||||
.unwrap()
|
.unwrap();
|
||||||
.header
|
let block = sequencer
|
||||||
.block_id;
|
.store
|
||||||
let block = sequencer.store.get_block_at_id(current_height).unwrap();
|
.get_block_at_id(sequencer.chain_height)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
// Only one should be included in the block
|
// Only one should be included in the block
|
||||||
assert_eq!(block.body.transactions, vec![tx.clone()]);
|
assert_eq!(block.body.transactions, vec![tx.clone()]);
|
||||||
@ -683,22 +695,24 @@ mod tests {
|
|||||||
|
|
||||||
// The transaction should be included the first time
|
// The transaction should be included the first time
|
||||||
mempool_handle.push(tx.clone()).await.unwrap();
|
mempool_handle.push(tx.clone()).await.unwrap();
|
||||||
let current_height = sequencer
|
sequencer
|
||||||
.produce_new_block_with_mempool_transactions()
|
.produce_new_block_with_mempool_transactions()
|
||||||
.unwrap()
|
.unwrap();
|
||||||
.header
|
let block = sequencer
|
||||||
.block_id;
|
.store
|
||||||
let block = sequencer.store.get_block_at_id(current_height).unwrap();
|
.get_block_at_id(sequencer.chain_height)
|
||||||
|
.unwrap();
|
||||||
assert_eq!(block.body.transactions, vec![tx.clone()]);
|
assert_eq!(block.body.transactions, vec![tx.clone()]);
|
||||||
|
|
||||||
// Add same transaction should fail
|
// Add same transaction should fail
|
||||||
mempool_handle.push(tx.clone()).await.unwrap();
|
mempool_handle.push(tx.clone()).await.unwrap();
|
||||||
let current_height = sequencer
|
sequencer
|
||||||
.produce_new_block_with_mempool_transactions()
|
.produce_new_block_with_mempool_transactions()
|
||||||
.unwrap()
|
.unwrap();
|
||||||
.header
|
let block = sequencer
|
||||||
.block_id;
|
.store
|
||||||
let block = sequencer.store.get_block_at_id(current_height).unwrap();
|
.get_block_at_id(sequencer.chain_height)
|
||||||
|
.unwrap();
|
||||||
assert!(block.body.transactions.is_empty());
|
assert!(block.body.transactions.is_empty());
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -726,12 +740,13 @@ mod tests {
|
|||||||
);
|
);
|
||||||
|
|
||||||
mempool_handle.push(tx.clone()).await.unwrap();
|
mempool_handle.push(tx.clone()).await.unwrap();
|
||||||
let current_height = sequencer
|
sequencer
|
||||||
.produce_new_block_with_mempool_transactions()
|
.produce_new_block_with_mempool_transactions()
|
||||||
.unwrap()
|
.unwrap();
|
||||||
.header
|
let block = sequencer
|
||||||
.block_id;
|
.store
|
||||||
let block = sequencer.store.get_block_at_id(current_height).unwrap();
|
.get_block_at_id(sequencer.chain_height)
|
||||||
|
.unwrap();
|
||||||
assert_eq!(block.body.transactions, vec![tx.clone()]);
|
assert_eq!(block.body.transactions, vec![tx.clone()]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -792,4 +807,126 @@ mod tests {
|
|||||||
|
|
||||||
assert_eq!(sequencer.get_pending_blocks().unwrap().len(), 1);
|
assert_eq!(sequencer.get_pending_blocks().unwrap().len(), 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_produce_block_with_correct_prev_meta_after_restart() {
|
||||||
|
let config = setup_sequencer_config();
|
||||||
|
let acc1_account_id = config.initial_accounts[0].account_id;
|
||||||
|
let acc2_account_id = config.initial_accounts[1].account_id;
|
||||||
|
|
||||||
|
// Step 1: Create initial database with some block metadata
|
||||||
|
let expected_prev_meta = {
|
||||||
|
let (mut sequencer, mempool_handle) =
|
||||||
|
SequencerCoreWithMockClients::start_from_config(config.clone()).await;
|
||||||
|
|
||||||
|
let signing_key = PrivateKey::try_new([1; 32]).unwrap();
|
||||||
|
|
||||||
|
// Add a transaction and produce a block to set up block metadata
|
||||||
|
let tx = common::test_utils::create_transaction_native_token_transfer(
|
||||||
|
acc1_account_id,
|
||||||
|
0,
|
||||||
|
acc2_account_id,
|
||||||
|
100,
|
||||||
|
signing_key,
|
||||||
|
);
|
||||||
|
|
||||||
|
mempool_handle.push(tx).await.unwrap();
|
||||||
|
sequencer
|
||||||
|
.produce_new_block_with_mempool_transactions()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// Get the metadata of the last block produced
|
||||||
|
sequencer.store.latest_block_meta().unwrap()
|
||||||
|
};
|
||||||
|
|
||||||
|
// Step 2: Restart sequencer from the same storage
|
||||||
|
let (mut sequencer, mempool_handle) =
|
||||||
|
SequencerCoreWithMockClients::start_from_config(config.clone()).await;
|
||||||
|
|
||||||
|
// Step 3: Submit a new transaction
|
||||||
|
let signing_key = PrivateKey::try_new([1; 32]).unwrap();
|
||||||
|
let tx = common::test_utils::create_transaction_native_token_transfer(
|
||||||
|
acc1_account_id,
|
||||||
|
1, // Next nonce
|
||||||
|
acc2_account_id,
|
||||||
|
50,
|
||||||
|
signing_key,
|
||||||
|
);
|
||||||
|
|
||||||
|
mempool_handle.push(tx.clone()).await.unwrap();
|
||||||
|
|
||||||
|
// Step 4: Produce new block
|
||||||
|
sequencer
|
||||||
|
.produce_new_block_with_mempool_transactions()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// Step 5: Verify the new block has correct previous block metadata
|
||||||
|
let new_block = sequencer
|
||||||
|
.store
|
||||||
|
.get_block_at_id(sequencer.chain_height)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
new_block.header.prev_block_hash, expected_prev_meta.hash,
|
||||||
|
"New block's prev_block_hash should match the stored metadata hash"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
new_block.bedrock_parent_id, expected_prev_meta.msg_id,
|
||||||
|
"New block's bedrock_parent_id should match the stored metadata msg_id"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
new_block.body.transactions,
|
||||||
|
vec![tx],
|
||||||
|
"New block should contain the submitted transaction"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_start_from_config_uses_db_height_not_config_genesis() {
|
||||||
|
let mut config = setup_sequencer_config();
|
||||||
|
let original_genesis_id = config.genesis_id;
|
||||||
|
|
||||||
|
// Step 1: Create initial database and produce some blocks
|
||||||
|
let expected_chain_height = {
|
||||||
|
let (mut sequencer, mempool_handle) =
|
||||||
|
SequencerCoreWithMockClients::start_from_config(config.clone()).await;
|
||||||
|
|
||||||
|
// Verify we start with the genesis_id from config
|
||||||
|
assert_eq!(sequencer.chain_height, original_genesis_id);
|
||||||
|
|
||||||
|
// Produce multiple blocks to advance chain height
|
||||||
|
let tx = common::test_utils::produce_dummy_empty_transaction();
|
||||||
|
mempool_handle.push(tx).await.unwrap();
|
||||||
|
sequencer
|
||||||
|
.produce_new_block_with_mempool_transactions()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let tx = common::test_utils::produce_dummy_empty_transaction();
|
||||||
|
mempool_handle.push(tx).await.unwrap();
|
||||||
|
sequencer
|
||||||
|
.produce_new_block_with_mempool_transactions()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// Return the current chain height (should be genesis_id + 2)
|
||||||
|
sequencer.chain_height
|
||||||
|
};
|
||||||
|
|
||||||
|
// Step 2: Modify the config to have a DIFFERENT genesis_id
|
||||||
|
let different_genesis_id = original_genesis_id + 100;
|
||||||
|
config.genesis_id = different_genesis_id;
|
||||||
|
|
||||||
|
// Step 3: Restart sequencer with the modified config (different genesis_id)
|
||||||
|
let (sequencer, _mempool_handle) =
|
||||||
|
SequencerCoreWithMockClients::start_from_config(config.clone()).await;
|
||||||
|
|
||||||
|
// Step 4: Verify chain_height comes from database, NOT from the new config.genesis_id
|
||||||
|
assert_eq!(
|
||||||
|
sequencer.chain_height, expected_chain_height,
|
||||||
|
"Chain height should be loaded from database metadata, not config.genesis_id"
|
||||||
|
);
|
||||||
|
assert_ne!(
|
||||||
|
sequencer.chain_height, different_genesis_id,
|
||||||
|
"Chain height should NOT match the modified config.genesis_id"
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
use anyhow::Result;
|
use anyhow::{Result, anyhow};
|
||||||
use common::block::Block;
|
use bedrock_client::SignedMantleTx;
|
||||||
use logos_blockchain_core::mantle::ops::channel::{ChannelId, MsgId};
|
use logos_blockchain_core::mantle::ops::channel::ChannelId;
|
||||||
use logos_blockchain_key_management_system_service::keys::Ed25519Key;
|
use logos_blockchain_key_management_system_service::keys::Ed25519Key;
|
||||||
use url::Url;
|
use url::Url;
|
||||||
|
|
||||||
@ -34,8 +34,35 @@ impl BlockSettlementClientTrait for MockBlockSettlementClient {
|
|||||||
&self.bedrock_signing_key
|
&self.bedrock_signing_key
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn submit_block_to_bedrock(&self, block: &Block) -> Result<MsgId> {
|
async fn submit_inscribe_tx_to_bedrock(&self, _tx: SignedMantleTx) -> Result<()> {
|
||||||
self.create_inscribe_tx(block).map(|(_, msg_id)| msg_id)
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct MockBlockSettlementClientWithError {
|
||||||
|
bedrock_channel_id: ChannelId,
|
||||||
|
bedrock_signing_key: Ed25519Key,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BlockSettlementClientTrait for MockBlockSettlementClientWithError {
|
||||||
|
fn new(config: &BedrockConfig, bedrock_signing_key: Ed25519Key) -> Result<Self> {
|
||||||
|
Ok(Self {
|
||||||
|
bedrock_channel_id: config.channel_id,
|
||||||
|
bedrock_signing_key,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn bedrock_channel_id(&self) -> ChannelId {
|
||||||
|
self.bedrock_channel_id
|
||||||
|
}
|
||||||
|
|
||||||
|
fn bedrock_signing_key(&self) -> &Ed25519Key {
|
||||||
|
&self.bedrock_signing_key
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn submit_inscribe_tx_to_bedrock(&self, _tx: SignedMantleTx) -> Result<()> {
|
||||||
|
Err(anyhow!("Mock error"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -26,7 +26,8 @@ use itertools::Itertools as _;
|
|||||||
use log::warn;
|
use log::warn;
|
||||||
use nssa::{self, program::Program};
|
use nssa::{self, program::Program};
|
||||||
use sequencer_core::{
|
use sequencer_core::{
|
||||||
block_settlement_client::BlockSettlementClientTrait, indexer_client::IndexerClientTrait,
|
block_settlement_client::BlockSettlementClientTrait,
|
||||||
|
indexer_client::IndexerClientTrait,
|
||||||
};
|
};
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
|
|
||||||
@ -94,8 +95,8 @@ impl<BC: BlockSettlementClientTrait, IC: IndexerClientTrait> JsonHandler<BC, IC>
|
|||||||
let tx = borsh::from_slice::<NSSATransaction>(&send_tx_req.transaction).unwrap();
|
let tx = borsh::from_slice::<NSSATransaction>(&send_tx_req.transaction).unwrap();
|
||||||
let tx_hash = tx.hash();
|
let tx_hash = tx.hash();
|
||||||
|
|
||||||
let authenticated_tx =
|
let authenticated_tx = transaction_pre_check(tx)
|
||||||
transaction_pre_check(tx).inspect_err(|err| warn!("Error at pre_check {err:#?}"))?;
|
.inspect_err(|err| warn!("Error at pre_check {err:#?}"))?;
|
||||||
|
|
||||||
// TODO: Do we need a timeout here? It will be usable if we have too many transactions to
|
// TODO: Do we need a timeout here? It will be usable if we have too many transactions to
|
||||||
// process
|
// process
|
||||||
@ -327,8 +328,7 @@ mod tests {
|
|||||||
use base64::{Engine, engine::general_purpose};
|
use base64::{Engine, engine::general_purpose};
|
||||||
use bedrock_client::BackoffConfig;
|
use bedrock_client::BackoffConfig;
|
||||||
use common::{
|
use common::{
|
||||||
block::AccountInitialData, config::BasicAuth, test_utils::sequencer_sign_key_for_testing,
|
block::AccountInitialData, config::BasicAuth, test_utils::sequencer_sign_key_for_testing, transaction::NSSATransaction
|
||||||
transaction::NSSATransaction,
|
|
||||||
};
|
};
|
||||||
use nssa::AccountId;
|
use nssa::AccountId;
|
||||||
use sequencer_core::{
|
use sequencer_core::{
|
||||||
@ -348,13 +348,13 @@ mod tests {
|
|||||||
let tempdir = tempdir().unwrap();
|
let tempdir = tempdir().unwrap();
|
||||||
let home = tempdir.path().to_path_buf();
|
let home = tempdir.path().to_path_buf();
|
||||||
let acc1_id: Vec<u8> = vec![
|
let acc1_id: Vec<u8> = vec![
|
||||||
208, 122, 210, 232, 75, 39, 250, 0, 194, 98, 240, 161, 238, 160, 255, 53, 202, 9, 115,
|
148, 179, 206, 253, 199, 51, 82, 86, 232, 2, 152, 122, 80, 243, 54, 207, 237, 112, 83,
|
||||||
84, 126, 106, 16, 111, 114, 241, 147, 194, 220, 131, 139, 68,
|
153, 44, 59, 204, 49, 128, 84, 160, 227, 216, 149, 97, 102,
|
||||||
];
|
];
|
||||||
|
|
||||||
let acc2_id: Vec<u8> = vec![
|
let acc2_id: Vec<u8> = vec![
|
||||||
231, 174, 119, 197, 239, 26, 5, 153, 147, 68, 175, 73, 159, 199, 138, 23, 5, 57, 141,
|
30, 145, 107, 3, 207, 73, 192, 230, 160, 63, 238, 207, 18, 69, 54, 216, 103, 244, 92,
|
||||||
98, 237, 6, 207, 46, 20, 121, 246, 222, 248, 154, 57, 188,
|
94, 124, 248, 42, 16, 141, 19, 119, 18, 14, 226, 140, 204,
|
||||||
];
|
];
|
||||||
|
|
||||||
let initial_acc1 = AccountInitialData {
|
let initial_acc1 = AccountInitialData {
|
||||||
@ -414,8 +414,8 @@ mod tests {
|
|||||||
let tx = common::test_utils::create_transaction_native_token_transfer(
|
let tx = common::test_utils::create_transaction_native_token_transfer(
|
||||||
AccountId::from_str(
|
AccountId::from_str(
|
||||||
&[
|
&[
|
||||||
208, 122, 210, 232, 75, 39, 250, 0, 194, 98, 240, 161, 238, 160, 255, 53, 202,
|
148, 179, 206, 253, 199, 51, 82, 86, 232, 2, 152, 122, 80, 243, 54, 207, 237,
|
||||||
9, 115, 84, 126, 106, 16, 111, 114, 241, 147, 194, 220, 131, 139, 68,
|
112, 83, 153, 44, 59, 204, 49, 128, 84, 160, 227, 216, 149, 97, 102,
|
||||||
]
|
]
|
||||||
.to_base58(),
|
.to_base58(),
|
||||||
)
|
)
|
||||||
|
|||||||
@ -77,9 +77,6 @@ COPY --from=builder --chown=sequencer_user:sequencer_user /root/.logos-blockchai
|
|||||||
COPY sequencer_runner/docker-entrypoint.sh /docker-entrypoint.sh
|
COPY sequencer_runner/docker-entrypoint.sh /docker-entrypoint.sh
|
||||||
RUN chmod +x /docker-entrypoint.sh
|
RUN chmod +x /docker-entrypoint.sh
|
||||||
|
|
||||||
# Volume for configuration directory
|
|
||||||
VOLUME ["/etc/sequencer_runner"]
|
|
||||||
|
|
||||||
# Expose default port
|
# Expose default port
|
||||||
EXPOSE 3040
|
EXPOSE 3040
|
||||||
|
|
||||||
|
|||||||
@ -14,7 +14,7 @@
|
|||||||
"max_retries": 5
|
"max_retries": 5
|
||||||
},
|
},
|
||||||
"channel_id": "0101010101010101010101010101010101010101010101010101010101010101",
|
"channel_id": "0101010101010101010101010101010101010101010101010101010101010101",
|
||||||
"node_url": "http://localhost:18080"
|
"node_url": "http://localhost:8080"
|
||||||
},
|
},
|
||||||
"indexer_rpc_url": "ws://localhost:8779",
|
"indexer_rpc_url": "ws://localhost:8779",
|
||||||
"initial_accounts": [
|
"initial_accounts": [
|
||||||
|
|||||||
@ -19,11 +19,11 @@
|
|||||||
"indexer_rpc_url": "ws://localhost:8779",
|
"indexer_rpc_url": "ws://localhost:8779",
|
||||||
"initial_accounts": [
|
"initial_accounts": [
|
||||||
{
|
{
|
||||||
"account_id": "BLgCRDXYdQPMMWVHYRFGQZbgeHx9frkipa8GtpG2Syqy",
|
"account_id": "6iArKUXxhUJqS7kCaPNhwMWt3ro71PDyBj7jwAyE2VQV",
|
||||||
"balance": 10000
|
"balance": 10000
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"account_id": "Gj1mJy5W7J5pfmLRujmQaLfLMWidNxQ6uwnhb666ZwHw",
|
"account_id": "7wHg9sbJwc6h3NP1S9bekfAzB8CHifEcxKswCKUt3YQo",
|
||||||
"balance": 20000
|
"balance": 20000
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
|||||||
@ -176,10 +176,19 @@ async fn retry_pending_blocks_loop(
|
|||||||
|
|
||||||
info!("Resubmitting {} pending blocks", pending_blocks.len());
|
info!("Resubmitting {} pending blocks", pending_blocks.len());
|
||||||
for block in &pending_blocks {
|
for block in &pending_blocks {
|
||||||
if let Err(e) = block_settlement_client.submit_block_to_bedrock(block).await {
|
// TODO: We could cache the inscribe tx for each pending block to avoid re-creating it
|
||||||
|
// on every retry.
|
||||||
|
let (tx, _msg_id) = block_settlement_client
|
||||||
|
.create_inscribe_tx(block)
|
||||||
|
.context("Failed to create inscribe tx for pending block")?;
|
||||||
|
|
||||||
|
if let Err(e) = block_settlement_client
|
||||||
|
.submit_inscribe_tx_to_bedrock(tx)
|
||||||
|
.await
|
||||||
|
{
|
||||||
warn!(
|
warn!(
|
||||||
"Failed to resubmit block with id {} with error {}",
|
"Failed to resubmit block with id {} with error {e:#}",
|
||||||
block.header.block_id, e
|
block.header.block_id
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,16 +1,18 @@
|
|||||||
#[derive(thiserror::Error, Debug)]
|
#[derive(thiserror::Error, Debug)]
|
||||||
pub enum DbError {
|
pub enum DbError {
|
||||||
#[error("RocksDb error {additional_info:#?}")]
|
#[error("RocksDb error: {}", additional_info.as_deref().unwrap_or("No additional info"))]
|
||||||
RocksDbError {
|
RocksDbError {
|
||||||
|
#[source]
|
||||||
error: rocksdb::Error,
|
error: rocksdb::Error,
|
||||||
additional_info: Option<String>,
|
additional_info: Option<String>,
|
||||||
},
|
},
|
||||||
#[error("Serialization error {additional_info:#?}")]
|
#[error("Serialization error: {}", additional_info.as_deref().unwrap_or("No additional info"))]
|
||||||
SerializationError {
|
SerializationError {
|
||||||
|
#[source]
|
||||||
error: borsh::io::Error,
|
error: borsh::io::Error,
|
||||||
additional_info: Option<String>,
|
additional_info: Option<String>,
|
||||||
},
|
},
|
||||||
#[error("Logic Error {additional_info:#?}")]
|
#[error("Logic Error: {additional_info}")]
|
||||||
DbInteractionError { additional_info: String },
|
DbInteractionError { additional_info: String },
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
use std::{path::Path, sync::Arc};
|
use std::{path::Path, sync::Arc};
|
||||||
|
|
||||||
use common::block::Block;
|
use common::block::{BedrockStatus, Block, BlockMeta, MantleMsgId};
|
||||||
use nssa::V02State;
|
use nssa::V02State;
|
||||||
use rocksdb::{
|
use rocksdb::{
|
||||||
BoundColumnFamily, ColumnFamilyDescriptor, DBWithThreadMode, MultiThreaded, Options, WriteBatch,
|
BoundColumnFamily, ColumnFamilyDescriptor, DBWithThreadMode, MultiThreaded, Options, WriteBatch,
|
||||||
@ -28,6 +28,8 @@ pub const DB_META_LAST_BLOCK_IN_DB_KEY: &str = "last_block_in_db";
|
|||||||
pub const DB_META_FIRST_BLOCK_SET_KEY: &str = "first_block_set";
|
pub const DB_META_FIRST_BLOCK_SET_KEY: &str = "first_block_set";
|
||||||
/// Key base for storing metainformation about the last finalized block on Bedrock
|
/// Key base for storing metainformation about the last finalized block on Bedrock
|
||||||
pub const DB_META_LAST_FINALIZED_BLOCK_ID: &str = "last_finalized_block_id";
|
pub const DB_META_LAST_FINALIZED_BLOCK_ID: &str = "last_finalized_block_id";
|
||||||
|
/// Key base for storing metainformation about the latest block meta
|
||||||
|
pub const DB_META_LATEST_BLOCK_META_KEY: &str = "latest_block_meta";
|
||||||
|
|
||||||
/// Key base for storing the NSSA state
|
/// Key base for storing the NSSA state
|
||||||
pub const DB_NSSA_STATE_KEY: &str = "nssa_state";
|
pub const DB_NSSA_STATE_KEY: &str = "nssa_state";
|
||||||
@ -46,7 +48,10 @@ pub struct RocksDBIO {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl RocksDBIO {
|
impl RocksDBIO {
|
||||||
pub fn open_or_create(path: &Path, start_block: Option<&Block>) -> DbResult<Self> {
|
pub fn open_or_create(
|
||||||
|
path: &Path,
|
||||||
|
start_block: Option<(&Block, MantleMsgId)>,
|
||||||
|
) -> DbResult<Self> {
|
||||||
let mut cf_opts = Options::default();
|
let mut cf_opts = Options::default();
|
||||||
cf_opts.set_max_write_buffer_number(16);
|
cf_opts.set_max_write_buffer_number(16);
|
||||||
// ToDo: Add more column families for different data
|
// ToDo: Add more column families for different data
|
||||||
@ -72,12 +77,17 @@ impl RocksDBIO {
|
|||||||
|
|
||||||
if is_start_set {
|
if is_start_set {
|
||||||
Ok(dbio)
|
Ok(dbio)
|
||||||
} else if let Some(block) = start_block {
|
} else if let Some((block, msg_id)) = start_block {
|
||||||
let block_id = block.header.block_id;
|
let block_id = block.header.block_id;
|
||||||
dbio.put_meta_first_block_in_db(block)?;
|
dbio.put_meta_first_block_in_db(block, msg_id)?;
|
||||||
dbio.put_meta_is_first_block_set()?;
|
dbio.put_meta_is_first_block_set()?;
|
||||||
dbio.put_meta_last_block_in_db(block_id)?;
|
dbio.put_meta_last_block_in_db(block_id)?;
|
||||||
dbio.put_meta_last_finalized_block_id(None)?;
|
dbio.put_meta_last_finalized_block_id(None)?;
|
||||||
|
dbio.put_meta_latest_block_meta(&BlockMeta {
|
||||||
|
id: block.header.block_id,
|
||||||
|
hash: block.header.hash,
|
||||||
|
msg_id,
|
||||||
|
})?;
|
||||||
|
|
||||||
Ok(dbio)
|
Ok(dbio)
|
||||||
} else {
|
} else {
|
||||||
@ -207,7 +217,7 @@ impl RocksDBIO {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn put_meta_first_block_in_db(&self, block: &Block) -> DbResult<()> {
|
pub fn put_meta_first_block_in_db(&self, block: &Block, msg_id: MantleMsgId) -> DbResult<()> {
|
||||||
let cf_meta = self.meta_column();
|
let cf_meta = self.meta_column();
|
||||||
self.db
|
self.db
|
||||||
.put_cf(
|
.put_cf(
|
||||||
@ -228,7 +238,7 @@ impl RocksDBIO {
|
|||||||
.map_err(|rerr| DbError::rocksdb_cast_message(rerr, None))?;
|
.map_err(|rerr| DbError::rocksdb_cast_message(rerr, None))?;
|
||||||
|
|
||||||
let mut batch = WriteBatch::default();
|
let mut batch = WriteBatch::default();
|
||||||
self.put_block(block, true, &mut batch)?;
|
self.put_block(block, msg_id, true, &mut batch)?;
|
||||||
self.db.write(batch).map_err(|rerr| {
|
self.db.write(batch).map_err(|rerr| {
|
||||||
DbError::rocksdb_cast_message(
|
DbError::rocksdb_cast_message(
|
||||||
rerr,
|
rerr,
|
||||||
@ -261,6 +271,30 @@ impl RocksDBIO {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn put_meta_last_block_in_db_batch(
|
||||||
|
&self,
|
||||||
|
block_id: u64,
|
||||||
|
batch: &mut WriteBatch,
|
||||||
|
) -> DbResult<()> {
|
||||||
|
let cf_meta = self.meta_column();
|
||||||
|
batch.put_cf(
|
||||||
|
&cf_meta,
|
||||||
|
borsh::to_vec(&DB_META_LAST_BLOCK_IN_DB_KEY).map_err(|err| {
|
||||||
|
DbError::borsh_cast_message(
|
||||||
|
err,
|
||||||
|
Some("Failed to serialize DB_META_LAST_BLOCK_IN_DB_KEY".to_string()),
|
||||||
|
)
|
||||||
|
})?,
|
||||||
|
borsh::to_vec(&block_id).map_err(|err| {
|
||||||
|
DbError::borsh_cast_message(
|
||||||
|
err,
|
||||||
|
Some("Failed to serialize last block id".to_string()),
|
||||||
|
)
|
||||||
|
})?,
|
||||||
|
);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
pub fn put_meta_last_finalized_block_id(&self, block_id: Option<u64>) -> DbResult<()> {
|
pub fn put_meta_last_finalized_block_id(&self, block_id: Option<u64>) -> DbResult<()> {
|
||||||
let cf_meta = self.meta_column();
|
let cf_meta = self.meta_column();
|
||||||
self.db
|
self.db
|
||||||
@ -300,14 +334,103 @@ impl RocksDBIO {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn put_block(&self, block: &Block, first: bool, batch: &mut WriteBatch) -> DbResult<()> {
|
fn put_meta_latest_block_meta(&self, block_meta: &BlockMeta) -> DbResult<()> {
|
||||||
|
let cf_meta = self.meta_column();
|
||||||
|
self.db
|
||||||
|
.put_cf(
|
||||||
|
&cf_meta,
|
||||||
|
borsh::to_vec(&DB_META_LATEST_BLOCK_META_KEY).map_err(|err| {
|
||||||
|
DbError::borsh_cast_message(
|
||||||
|
err,
|
||||||
|
Some("Failed to serialize DB_META_LATEST_BLOCK_META_KEY".to_string()),
|
||||||
|
)
|
||||||
|
})?,
|
||||||
|
borsh::to_vec(&block_meta).map_err(|err| {
|
||||||
|
DbError::borsh_cast_message(
|
||||||
|
err,
|
||||||
|
Some("Failed to serialize latest block meta".to_string()),
|
||||||
|
)
|
||||||
|
})?,
|
||||||
|
)
|
||||||
|
.map_err(|rerr| DbError::rocksdb_cast_message(rerr, None))?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn put_meta_latest_block_meta_batch(
|
||||||
|
&self,
|
||||||
|
block_meta: &BlockMeta,
|
||||||
|
batch: &mut WriteBatch,
|
||||||
|
) -> DbResult<()> {
|
||||||
|
let cf_meta = self.meta_column();
|
||||||
|
batch.put_cf(
|
||||||
|
&cf_meta,
|
||||||
|
borsh::to_vec(&DB_META_LATEST_BLOCK_META_KEY).map_err(|err| {
|
||||||
|
DbError::borsh_cast_message(
|
||||||
|
err,
|
||||||
|
Some("Failed to serialize DB_META_LATEST_BLOCK_META_KEY".to_string()),
|
||||||
|
)
|
||||||
|
})?,
|
||||||
|
borsh::to_vec(&block_meta).map_err(|err| {
|
||||||
|
DbError::borsh_cast_message(
|
||||||
|
err,
|
||||||
|
Some("Failed to serialize latest block meta".to_string()),
|
||||||
|
)
|
||||||
|
})?,
|
||||||
|
);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn latest_block_meta(&self) -> DbResult<BlockMeta> {
|
||||||
|
let cf_meta = self.meta_column();
|
||||||
|
let res = self
|
||||||
|
.db
|
||||||
|
.get_cf(
|
||||||
|
&cf_meta,
|
||||||
|
borsh::to_vec(&DB_META_LATEST_BLOCK_META_KEY).map_err(|err| {
|
||||||
|
DbError::borsh_cast_message(
|
||||||
|
err,
|
||||||
|
Some("Failed to serialize DB_META_LATEST_BLOCK_META_KEY".to_string()),
|
||||||
|
)
|
||||||
|
})?,
|
||||||
|
)
|
||||||
|
.map_err(|rerr| DbError::rocksdb_cast_message(rerr, None))?;
|
||||||
|
|
||||||
|
if let Some(data) = res {
|
||||||
|
Ok(borsh::from_slice::<BlockMeta>(&data).map_err(|err| {
|
||||||
|
DbError::borsh_cast_message(
|
||||||
|
err,
|
||||||
|
Some("Failed to deserialize latest block meta".to_string()),
|
||||||
|
)
|
||||||
|
})?)
|
||||||
|
} else {
|
||||||
|
Err(DbError::db_interaction_error(
|
||||||
|
"Latest block meta not found".to_string(),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn put_block(
|
||||||
|
&self,
|
||||||
|
block: &Block,
|
||||||
|
msg_id: MantleMsgId,
|
||||||
|
first: bool,
|
||||||
|
batch: &mut WriteBatch,
|
||||||
|
) -> DbResult<()> {
|
||||||
let cf_block = self.block_column();
|
let cf_block = self.block_column();
|
||||||
|
|
||||||
if !first {
|
if !first {
|
||||||
let last_curr_block = self.get_meta_last_block_in_db()?;
|
let last_curr_block = self.get_meta_last_block_in_db()?;
|
||||||
|
|
||||||
if block.header.block_id > last_curr_block {
|
if block.header.block_id > last_curr_block {
|
||||||
self.put_meta_last_block_in_db(block.header.block_id)?;
|
self.put_meta_last_block_in_db_batch(block.header.block_id, batch)?;
|
||||||
|
self.put_meta_latest_block_meta_batch(
|
||||||
|
&BlockMeta {
|
||||||
|
id: block.header.block_id,
|
||||||
|
hash: block.header.hash,
|
||||||
|
msg_id,
|
||||||
|
},
|
||||||
|
batch,
|
||||||
|
)?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -405,6 +528,37 @@ impl RocksDBIO {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn mark_block_as_finalized(&self, block_id: u64) -> DbResult<()> {
|
||||||
|
let mut block = self.get_block(block_id)?;
|
||||||
|
block.bedrock_status = BedrockStatus::Finalized;
|
||||||
|
|
||||||
|
let cf_block = self.block_column();
|
||||||
|
self.db
|
||||||
|
.put_cf(
|
||||||
|
&cf_block,
|
||||||
|
borsh::to_vec(&block_id).map_err(|err| {
|
||||||
|
DbError::borsh_cast_message(
|
||||||
|
err,
|
||||||
|
Some("Failed to serialize block id".to_string()),
|
||||||
|
)
|
||||||
|
})?,
|
||||||
|
borsh::to_vec(&block).map_err(|err| {
|
||||||
|
DbError::borsh_cast_message(
|
||||||
|
err,
|
||||||
|
Some("Failed to serialize block data".to_string()),
|
||||||
|
)
|
||||||
|
})?,
|
||||||
|
)
|
||||||
|
.map_err(|rerr| {
|
||||||
|
DbError::rocksdb_cast_message(
|
||||||
|
rerr,
|
||||||
|
Some(format!("Failed to mark block {block_id} as finalized")),
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
pub fn get_all_blocks(&self) -> impl Iterator<Item = DbResult<Block>> {
|
pub fn get_all_blocks(&self) -> impl Iterator<Item = DbResult<Block>> {
|
||||||
let cf_block = self.block_column();
|
let cf_block = self.block_column();
|
||||||
self.db
|
self.db
|
||||||
@ -426,10 +580,15 @@ impl RocksDBIO {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn atomic_update(&self, block: &Block, state: &V02State) -> DbResult<()> {
|
pub fn atomic_update(
|
||||||
|
&self,
|
||||||
|
block: &Block,
|
||||||
|
msg_id: MantleMsgId,
|
||||||
|
state: &V02State,
|
||||||
|
) -> DbResult<()> {
|
||||||
let block_id = block.header.block_id;
|
let block_id = block.header.block_id;
|
||||||
let mut batch = WriteBatch::default();
|
let mut batch = WriteBatch::default();
|
||||||
self.put_block(block, false, &mut batch)?;
|
self.put_block(block, msg_id, false, &mut batch)?;
|
||||||
self.put_nssa_state_in_db(state, &mut batch)?;
|
self.put_nssa_state_in_db(state, &mut batch)?;
|
||||||
self.db.write(batch).map_err(|rerr| {
|
self.db.write(batch).map_err(|rerr| {
|
||||||
DbError::rocksdb_cast_message(
|
DbError::rocksdb_cast_message(
|
||||||
|
|||||||
@ -278,7 +278,7 @@ pub unsafe extern "C" fn wallet_ffi_get_balance(
|
|||||||
Err(e) => return e,
|
Err(e) => return e,
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
match wallet.get_account_private(&account_id) {
|
match wallet.get_account_private(account_id) {
|
||||||
Some(account) => account.balance,
|
Some(account) => account.balance,
|
||||||
None => {
|
None => {
|
||||||
print_error("Private account not found");
|
print_error("Private account not found");
|
||||||
|
|||||||
@ -119,7 +119,7 @@ pub unsafe extern "C" fn wallet_ffi_get_private_account_keys(
|
|||||||
|
|
||||||
let account_id = AccountId::new(unsafe { (*account_id).data });
|
let account_id = AccountId::new(unsafe { (*account_id).data });
|
||||||
|
|
||||||
let (key_chain, _account) = match wallet.storage().user_data.get_private_account(&account_id) {
|
let (key_chain, _account) = match wallet.storage().user_data.get_private_account(account_id) {
|
||||||
Some(k) => k,
|
Some(k) => k,
|
||||||
None => {
|
None => {
|
||||||
print_error("Private account not found in wallet");
|
print_error("Private account not found in wallet");
|
||||||
|
|||||||
@ -10,6 +10,7 @@ nssa.workspace = true
|
|||||||
common.workspace = true
|
common.workspace = true
|
||||||
key_protocol.workspace = true
|
key_protocol.workspace = true
|
||||||
token_core.workspace = true
|
token_core.workspace = true
|
||||||
|
amm_core.workspace = true
|
||||||
|
|
||||||
anyhow.workspace = true
|
anyhow.workspace = true
|
||||||
serde_json.workspace = true
|
serde_json.workspace = true
|
||||||
|
|||||||
@ -8,7 +8,7 @@
|
|||||||
"initial_accounts": [
|
"initial_accounts": [
|
||||||
{
|
{
|
||||||
"Public": {
|
"Public": {
|
||||||
"account_id": "BLgCRDXYdQPMMWVHYRFGQZbgeHx9frkipa8GtpG2Syqy",
|
"account_id": "6iArKUXxhUJqS7kCaPNhwMWt3ro71PDyBj7jwAyE2VQV",
|
||||||
"pub_sign_key": [
|
"pub_sign_key": [
|
||||||
16,
|
16,
|
||||||
162,
|
162,
|
||||||
@ -47,7 +47,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"Public": {
|
"Public": {
|
||||||
"account_id": "Gj1mJy5W7J5pfmLRujmQaLfLMWidNxQ6uwnhb666ZwHw",
|
"account_id": "7wHg9sbJwc6h3NP1S9bekfAzB8CHifEcxKswCKUt3YQo",
|
||||||
"pub_sign_key": [
|
"pub_sign_key": [
|
||||||
113,
|
113,
|
||||||
121,
|
121,
|
||||||
|
|||||||
@ -170,7 +170,7 @@ mod tests {
|
|||||||
let initial_acc1 = serde_json::from_str(
|
let initial_acc1 = serde_json::from_str(
|
||||||
r#"{
|
r#"{
|
||||||
"Public": {
|
"Public": {
|
||||||
"account_id": "BLgCRDXYdQPMMWVHYRFGQZbgeHx9frkipa8GtpG2Syqy",
|
"account_id": "6iArKUXxhUJqS7kCaPNhwMWt3ro71PDyBj7jwAyE2VQV",
|
||||||
"pub_sign_key": [
|
"pub_sign_key": [
|
||||||
16,
|
16,
|
||||||
162,
|
162,
|
||||||
@ -213,7 +213,7 @@ mod tests {
|
|||||||
let initial_acc2 = serde_json::from_str(
|
let initial_acc2 = serde_json::from_str(
|
||||||
r#"{
|
r#"{
|
||||||
"Public": {
|
"Public": {
|
||||||
"account_id": "Gj1mJy5W7J5pfmLRujmQaLfLMWidNxQ6uwnhb666ZwHw",
|
"account_id": "7wHg9sbJwc6h3NP1S9bekfAzB8CHifEcxKswCKUt3YQo",
|
||||||
"pub_sign_key": [
|
"pub_sign_key": [
|
||||||
113,
|
113,
|
||||||
121,
|
121,
|
||||||
|
|||||||
@ -100,7 +100,7 @@ impl WalletSubcommand for NewSubcommand {
|
|||||||
let (key, _) = wallet_core
|
let (key, _) = wallet_core
|
||||||
.storage
|
.storage
|
||||||
.user_data
|
.user_data
|
||||||
.get_private_account(&account_id)
|
.get_private_account(account_id)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
println!(
|
println!(
|
||||||
@ -184,7 +184,7 @@ impl WalletSubcommand for AccountSubcommand {
|
|||||||
wallet_core.get_account_public(account_id).await?
|
wallet_core.get_account_public(account_id).await?
|
||||||
}
|
}
|
||||||
AccountPrivacyKind::Private => wallet_core
|
AccountPrivacyKind::Private => wallet_core
|
||||||
.get_account_private(&account_id)
|
.get_account_private(account_id)
|
||||||
.ok_or(anyhow::anyhow!("Private account not found in storage"))?,
|
.ok_or(anyhow::anyhow!("Private account not found in storage"))?,
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -205,7 +205,7 @@ impl WalletSubcommand for AccountSubcommand {
|
|||||||
let (key, _) = wallet_core
|
let (key, _) = wallet_core
|
||||||
.storage
|
.storage
|
||||||
.user_data
|
.user_data
|
||||||
.get_private_account(&account_id)
|
.get_private_account(account_id)
|
||||||
.ok_or(anyhow::anyhow!("Private account not found in storage"))?;
|
.ok_or(anyhow::anyhow!("Private account not found in storage"))?;
|
||||||
|
|
||||||
println!("npk {}", hex::encode(key.nullifer_public_key.0));
|
println!("npk {}", hex::encode(key.nullifer_public_key.0));
|
||||||
@ -275,7 +275,7 @@ impl WalletSubcommand for AccountSubcommand {
|
|||||||
let user_data = &wallet_core.storage.user_data;
|
let user_data = &wallet_core.storage.user_data;
|
||||||
let labels = &wallet_core.storage.labels;
|
let labels = &wallet_core.storage.labels;
|
||||||
|
|
||||||
let format_with_label = |prefix: &str, id: &nssa::AccountId| {
|
let format_with_label = |prefix: &str, id: nssa::AccountId| {
|
||||||
let id_str = id.to_string();
|
let id_str = id.to_string();
|
||||||
if let Some(label) = labels.get(&id_str) {
|
if let Some(label) = labels.get(&id_str) {
|
||||||
format!("{prefix} [{label}]")
|
format!("{prefix} [{label}]")
|
||||||
@ -285,24 +285,26 @@ impl WalletSubcommand for AccountSubcommand {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if !long {
|
if !long {
|
||||||
let accounts = user_data
|
let accounts =
|
||||||
.default_pub_account_signing_keys
|
user_data
|
||||||
.keys()
|
.default_pub_account_signing_keys
|
||||||
.map(|id| format_with_label(&format!("Preconfigured Public/{id}"), id))
|
.keys()
|
||||||
.chain(user_data.default_user_private_accounts.keys().map(|id| {
|
.copied()
|
||||||
format_with_label(&format!("Preconfigured Private/{id}"), id)
|
.map(|id| format_with_label(&format!("Preconfigured Public/{id}"), id))
|
||||||
}))
|
.chain(user_data.default_user_private_accounts.keys().copied().map(
|
||||||
.chain(user_data.public_key_tree.account_id_map.iter().map(
|
|id| format_with_label(&format!("Preconfigured Private/{id}"), id),
|
||||||
|(id, chain_index)| {
|
))
|
||||||
format_with_label(&format!("{chain_index} Public/{id}"), id)
|
.chain(user_data.public_key_tree.account_id_map.iter().map(
|
||||||
},
|
|(id, chain_index)| {
|
||||||
))
|
format_with_label(&format!("{chain_index} Public/{id}"), *id)
|
||||||
.chain(user_data.private_key_tree.account_id_map.iter().map(
|
},
|
||||||
|(id, chain_index)| {
|
))
|
||||||
format_with_label(&format!("{chain_index} Private/{id}"), id)
|
.chain(user_data.private_key_tree.account_id_map.iter().map(
|
||||||
},
|
|(id, chain_index)| {
|
||||||
))
|
format_with_label(&format!("{chain_index} Private/{id}"), *id)
|
||||||
.format("\n");
|
},
|
||||||
|
))
|
||||||
|
.format("\n");
|
||||||
|
|
||||||
println!("{accounts}");
|
println!("{accounts}");
|
||||||
return Ok(SubcommandReturnValue::Empty);
|
return Ok(SubcommandReturnValue::Empty);
|
||||||
@ -310,12 +312,12 @@ impl WalletSubcommand for AccountSubcommand {
|
|||||||
|
|
||||||
// Detailed listing with --long flag
|
// Detailed listing with --long flag
|
||||||
// Preconfigured public accounts
|
// Preconfigured public accounts
|
||||||
for id in user_data.default_pub_account_signing_keys.keys() {
|
for id in user_data.default_pub_account_signing_keys.keys().copied() {
|
||||||
println!(
|
println!(
|
||||||
"{}",
|
"{}",
|
||||||
format_with_label(&format!("Preconfigured Public/{id}"), id)
|
format_with_label(&format!("Preconfigured Public/{id}"), id)
|
||||||
);
|
);
|
||||||
match wallet_core.get_account_public(*id).await {
|
match wallet_core.get_account_public(id).await {
|
||||||
Ok(account) if account != Account::default() => {
|
Ok(account) if account != Account::default() => {
|
||||||
let (description, json_view) = format_account_details(&account);
|
let (description, json_view) = format_account_details(&account);
|
||||||
println!(" {description}");
|
println!(" {description}");
|
||||||
@ -327,7 +329,7 @@ impl WalletSubcommand for AccountSubcommand {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Preconfigured private accounts
|
// Preconfigured private accounts
|
||||||
for id in user_data.default_user_private_accounts.keys() {
|
for id in user_data.default_user_private_accounts.keys().copied() {
|
||||||
println!(
|
println!(
|
||||||
"{}",
|
"{}",
|
||||||
format_with_label(&format!("Preconfigured Private/{id}"), id)
|
format_with_label(&format!("Preconfigured Private/{id}"), id)
|
||||||
@ -347,7 +349,7 @@ impl WalletSubcommand for AccountSubcommand {
|
|||||||
for (id, chain_index) in user_data.public_key_tree.account_id_map.iter() {
|
for (id, chain_index) in user_data.public_key_tree.account_id_map.iter() {
|
||||||
println!(
|
println!(
|
||||||
"{}",
|
"{}",
|
||||||
format_with_label(&format!("{chain_index} Public/{id}"), id)
|
format_with_label(&format!("{chain_index} Public/{id}"), *id)
|
||||||
);
|
);
|
||||||
match wallet_core.get_account_public(*id).await {
|
match wallet_core.get_account_public(*id).await {
|
||||||
Ok(account) if account != Account::default() => {
|
Ok(account) if account != Account::default() => {
|
||||||
@ -364,9 +366,9 @@ impl WalletSubcommand for AccountSubcommand {
|
|||||||
for (id, chain_index) in user_data.private_key_tree.account_id_map.iter() {
|
for (id, chain_index) in user_data.private_key_tree.account_id_map.iter() {
|
||||||
println!(
|
println!(
|
||||||
"{}",
|
"{}",
|
||||||
format_with_label(&format!("{chain_index} Private/{id}"), id)
|
format_with_label(&format!("{chain_index} Private/{id}"), *id)
|
||||||
);
|
);
|
||||||
match wallet_core.get_account_private(id) {
|
match wallet_core.get_account_private(*id) {
|
||||||
Some(account) if account != Account::default() => {
|
Some(account) if account != Account::default() => {
|
||||||
let (description, json_view) = format_account_details(&account);
|
let (description, json_view) = format_account_details(&account);
|
||||||
println!(" {description}");
|
println!(" {description}");
|
||||||
|
|||||||
@ -214,7 +214,7 @@ impl Default for WalletConfig {
|
|||||||
[
|
[
|
||||||
{
|
{
|
||||||
"Public": {
|
"Public": {
|
||||||
"account_id": "BLgCRDXYdQPMMWVHYRFGQZbgeHx9frkipa8GtpG2Syqy",
|
"account_id": "6iArKUXxhUJqS7kCaPNhwMWt3ro71PDyBj7jwAyE2VQV",
|
||||||
"pub_sign_key": [
|
"pub_sign_key": [
|
||||||
16,
|
16,
|
||||||
162,
|
162,
|
||||||
@ -253,7 +253,7 @@ impl Default for WalletConfig {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"Public": {
|
"Public": {
|
||||||
"account_id": "Gj1mJy5W7J5pfmLRujmQaLfLMWidNxQ6uwnhb666ZwHw",
|
"account_id": "7wHg9sbJwc6h3NP1S9bekfAzB8CHifEcxKswCKUt3YQo",
|
||||||
"pub_sign_key": [
|
"pub_sign_key": [
|
||||||
113,
|
113,
|
||||||
121,
|
121,
|
||||||
|
|||||||
@ -225,14 +225,14 @@ impl WalletCore {
|
|||||||
.get_pub_account_signing_key(account_id)
|
.get_pub_account_signing_key(account_id)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_account_private(&self, account_id: &AccountId) -> Option<Account> {
|
pub fn get_account_private(&self, account_id: AccountId) -> Option<Account> {
|
||||||
self.storage
|
self.storage
|
||||||
.user_data
|
.user_data
|
||||||
.get_private_account(account_id)
|
.get_private_account(account_id)
|
||||||
.map(|value| value.1.clone())
|
.map(|value| value.1.clone())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_private_account_commitment(&self, account_id: &AccountId) -> Option<Commitment> {
|
pub fn get_private_account_commitment(&self, account_id: AccountId) -> Option<Commitment> {
|
||||||
let (keys, account) = self.storage.user_data.get_private_account(account_id)?;
|
let (keys, account) = self.storage.user_data.get_private_account(account_id)?;
|
||||||
Some(Commitment::new(&keys.nullifer_public_key, account))
|
Some(Commitment::new(&keys.nullifer_public_key, account))
|
||||||
}
|
}
|
||||||
@ -248,7 +248,7 @@ impl WalletCore {
|
|||||||
|
|
||||||
pub async fn check_private_account_initialized(
|
pub async fn check_private_account_initialized(
|
||||||
&self,
|
&self,
|
||||||
account_id: &AccountId,
|
account_id: AccountId,
|
||||||
) -> Result<Option<MembershipProof>> {
|
) -> Result<Option<MembershipProof>> {
|
||||||
if let Some(acc_comm) = self.get_private_account_commitment(account_id) {
|
if let Some(acc_comm) = self.get_private_account_commitment(account_id) {
|
||||||
self.sequencer_client
|
self.sequencer_client
|
||||||
|
|||||||
@ -204,7 +204,7 @@ async fn private_acc_preparation(
|
|||||||
let Some((from_keys, from_acc)) = wallet
|
let Some((from_keys, from_acc)) = wallet
|
||||||
.storage
|
.storage
|
||||||
.user_data
|
.user_data
|
||||||
.get_private_account(&account_id)
|
.get_private_account(account_id)
|
||||||
.cloned()
|
.cloned()
|
||||||
else {
|
else {
|
||||||
return Err(ExecutionFailureKind::KeyNotFoundError);
|
return Err(ExecutionFailureKind::KeyNotFoundError);
|
||||||
@ -217,7 +217,7 @@ async fn private_acc_preparation(
|
|||||||
|
|
||||||
// TODO: Remove this unwrap, error types must be compatible
|
// TODO: Remove this unwrap, error types must be compatible
|
||||||
let proof = wallet
|
let proof = wallet
|
||||||
.check_private_account_initialized(&account_id)
|
.check_private_account_initialized(account_id)
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
|
|||||||
@ -1,103 +1,9 @@
|
|||||||
|
use amm_core::{compute_liquidity_token_pda, compute_pool_pda, compute_vault_pda};
|
||||||
use common::{error::ExecutionFailureKind, rpc_primitives::requests::SendTxResponse};
|
use common::{error::ExecutionFailureKind, rpc_primitives::requests::SendTxResponse};
|
||||||
use nssa::{AccountId, ProgramId, program::Program};
|
use nssa::{AccountId, program::Program};
|
||||||
use nssa_core::program::PdaSeed;
|
|
||||||
use token_core::TokenHolding;
|
use token_core::TokenHolding;
|
||||||
|
|
||||||
use crate::WalletCore;
|
use crate::WalletCore;
|
||||||
|
|
||||||
fn compute_pool_pda(
|
|
||||||
amm_program_id: ProgramId,
|
|
||||||
definition_token_a_id: AccountId,
|
|
||||||
definition_token_b_id: AccountId,
|
|
||||||
) -> AccountId {
|
|
||||||
AccountId::from((
|
|
||||||
&amm_program_id,
|
|
||||||
&compute_pool_pda_seed(definition_token_a_id, definition_token_b_id),
|
|
||||||
))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn compute_pool_pda_seed(
|
|
||||||
definition_token_a_id: AccountId,
|
|
||||||
definition_token_b_id: AccountId,
|
|
||||||
) -> PdaSeed {
|
|
||||||
use risc0_zkvm::sha::{Impl, Sha256};
|
|
||||||
|
|
||||||
let mut i: usize = 0;
|
|
||||||
let (token_1, token_2) = loop {
|
|
||||||
if definition_token_a_id.value()[i] > definition_token_b_id.value()[i] {
|
|
||||||
let token_1 = definition_token_a_id;
|
|
||||||
let token_2 = definition_token_b_id;
|
|
||||||
break (token_1, token_2);
|
|
||||||
} else if definition_token_a_id.value()[i] < definition_token_b_id.value()[i] {
|
|
||||||
let token_1 = definition_token_b_id;
|
|
||||||
let token_2 = definition_token_a_id;
|
|
||||||
break (token_1, token_2);
|
|
||||||
}
|
|
||||||
|
|
||||||
if i == 32 {
|
|
||||||
panic!("Definitions match");
|
|
||||||
} else {
|
|
||||||
i += 1;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut bytes = [0; 64];
|
|
||||||
bytes[0..32].copy_from_slice(&token_1.to_bytes());
|
|
||||||
bytes[32..].copy_from_slice(&token_2.to_bytes());
|
|
||||||
|
|
||||||
PdaSeed::new(
|
|
||||||
Impl::hash_bytes(&bytes)
|
|
||||||
.as_bytes()
|
|
||||||
.try_into()
|
|
||||||
.expect("Hash output must be exactly 32 bytes long"),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn compute_vault_pda(
|
|
||||||
amm_program_id: ProgramId,
|
|
||||||
pool_id: AccountId,
|
|
||||||
definition_token_id: AccountId,
|
|
||||||
) -> AccountId {
|
|
||||||
AccountId::from((
|
|
||||||
&amm_program_id,
|
|
||||||
&compute_vault_pda_seed(pool_id, definition_token_id),
|
|
||||||
))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn compute_vault_pda_seed(pool_id: AccountId, definition_token_id: AccountId) -> PdaSeed {
|
|
||||||
use risc0_zkvm::sha::{Impl, Sha256};
|
|
||||||
|
|
||||||
let mut bytes = [0; 64];
|
|
||||||
bytes[0..32].copy_from_slice(&pool_id.to_bytes());
|
|
||||||
bytes[32..].copy_from_slice(&definition_token_id.to_bytes());
|
|
||||||
|
|
||||||
PdaSeed::new(
|
|
||||||
Impl::hash_bytes(&bytes)
|
|
||||||
.as_bytes()
|
|
||||||
.try_into()
|
|
||||||
.expect("Hash output must be exactly 32 bytes long"),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn compute_liquidity_token_pda(amm_program_id: ProgramId, pool_id: AccountId) -> AccountId {
|
|
||||||
AccountId::from((&amm_program_id, &compute_liquidity_token_pda_seed(pool_id)))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn compute_liquidity_token_pda_seed(pool_id: AccountId) -> PdaSeed {
|
|
||||||
use risc0_zkvm::sha::{Impl, Sha256};
|
|
||||||
|
|
||||||
let mut bytes = [0; 64];
|
|
||||||
bytes[0..32].copy_from_slice(&pool_id.to_bytes());
|
|
||||||
bytes[32..].copy_from_slice(&[0; 32]);
|
|
||||||
|
|
||||||
PdaSeed::new(
|
|
||||||
Impl::hash_bytes(&bytes)
|
|
||||||
.as_bytes()
|
|
||||||
.try_into()
|
|
||||||
.expect("Hash output must be exactly 32 bytes long"),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct Amm<'w>(pub &'w WalletCore);
|
pub struct Amm<'w>(pub &'w WalletCore);
|
||||||
|
|
||||||
impl Amm<'_> {
|
impl Amm<'_> {
|
||||||
@ -109,9 +15,13 @@ impl Amm<'_> {
|
|||||||
balance_a: u128,
|
balance_a: u128,
|
||||||
balance_b: u128,
|
balance_b: u128,
|
||||||
) -> Result<SendTxResponse, ExecutionFailureKind> {
|
) -> Result<SendTxResponse, ExecutionFailureKind> {
|
||||||
let (instruction, program) = amm_program_preparation_definition(balance_a, balance_b);
|
let program = Program::amm();
|
||||||
|
|
||||||
let amm_program_id = Program::amm().id();
|
let amm_program_id = Program::amm().id();
|
||||||
|
let instruction = amm_core::Instruction::NewDefinition {
|
||||||
|
token_a_amount: balance_a,
|
||||||
|
token_b_amount: balance_b,
|
||||||
|
amm_program_id,
|
||||||
|
};
|
||||||
|
|
||||||
let user_a_acc = self
|
let user_a_acc = self
|
||||||
.0
|
.0
|
||||||
@ -189,13 +99,16 @@ impl Amm<'_> {
|
|||||||
&self,
|
&self,
|
||||||
user_holding_a: AccountId,
|
user_holding_a: AccountId,
|
||||||
user_holding_b: AccountId,
|
user_holding_b: AccountId,
|
||||||
amount_in: u128,
|
swap_amount_in: u128,
|
||||||
min_amount_out: u128,
|
min_amount_out: u128,
|
||||||
token_definition_id: AccountId,
|
token_definition_id_in: AccountId,
|
||||||
) -> Result<SendTxResponse, ExecutionFailureKind> {
|
) -> Result<SendTxResponse, ExecutionFailureKind> {
|
||||||
let (instruction, program) =
|
let instruction = amm_core::Instruction::Swap {
|
||||||
amm_program_preparation_swap(amount_in, min_amount_out, token_definition_id);
|
swap_amount_in,
|
||||||
|
min_amount_out,
|
||||||
|
token_definition_id_in,
|
||||||
|
};
|
||||||
|
let program = Program::amm();
|
||||||
let amm_program_id = Program::amm().id();
|
let amm_program_id = Program::amm().id();
|
||||||
|
|
||||||
let user_a_acc = self
|
let user_a_acc = self
|
||||||
@ -248,12 +161,14 @@ impl Amm<'_> {
|
|||||||
let token_holder_b = TokenHolding::try_from(&token_holder_acc_b.data)
|
let token_holder_b = TokenHolding::try_from(&token_holder_acc_b.data)
|
||||||
.map_err(|_| ExecutionFailureKind::AccountDataError(user_holding_b))?;
|
.map_err(|_| ExecutionFailureKind::AccountDataError(user_holding_b))?;
|
||||||
|
|
||||||
if token_holder_a.definition_id() == token_definition_id {
|
if token_holder_a.definition_id() == token_definition_id_in {
|
||||||
account_id_auth = user_holding_a;
|
account_id_auth = user_holding_a;
|
||||||
} else if token_holder_b.definition_id() == token_definition_id {
|
} else if token_holder_b.definition_id() == token_definition_id_in {
|
||||||
account_id_auth = user_holding_b;
|
account_id_auth = user_holding_b;
|
||||||
} else {
|
} else {
|
||||||
return Err(ExecutionFailureKind::AccountDataError(token_definition_id));
|
return Err(ExecutionFailureKind::AccountDataError(
|
||||||
|
token_definition_id_in,
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
let nonces = self
|
let nonces = self
|
||||||
@ -290,13 +205,16 @@ impl Amm<'_> {
|
|||||||
user_holding_a: AccountId,
|
user_holding_a: AccountId,
|
||||||
user_holding_b: AccountId,
|
user_holding_b: AccountId,
|
||||||
user_holding_lp: AccountId,
|
user_holding_lp: AccountId,
|
||||||
min_amount_lp: u128,
|
min_amount_liquidity: u128,
|
||||||
max_amount_a: u128,
|
max_amount_to_add_token_a: u128,
|
||||||
max_amount_b: u128,
|
max_amount_to_add_token_b: u128,
|
||||||
) -> Result<SendTxResponse, ExecutionFailureKind> {
|
) -> Result<SendTxResponse, ExecutionFailureKind> {
|
||||||
let (instruction, program) =
|
let instruction = amm_core::Instruction::AddLiquidity {
|
||||||
amm_program_preparation_add_liq(min_amount_lp, max_amount_a, max_amount_b);
|
min_amount_liquidity,
|
||||||
|
max_amount_to_add_token_a,
|
||||||
|
max_amount_to_add_token_b,
|
||||||
|
};
|
||||||
|
let program = Program::amm();
|
||||||
let amm_program_id = Program::amm().id();
|
let amm_program_id = Program::amm().id();
|
||||||
|
|
||||||
let user_a_acc = self
|
let user_a_acc = self
|
||||||
@ -376,13 +294,16 @@ impl Amm<'_> {
|
|||||||
user_holding_a: AccountId,
|
user_holding_a: AccountId,
|
||||||
user_holding_b: AccountId,
|
user_holding_b: AccountId,
|
||||||
user_holding_lp: AccountId,
|
user_holding_lp: AccountId,
|
||||||
balance_lp: u128,
|
remove_liquidity_amount: u128,
|
||||||
min_amount_a: u128,
|
min_amount_to_remove_token_a: u128,
|
||||||
min_amount_b: u128,
|
min_amount_to_remove_token_b: u128,
|
||||||
) -> Result<SendTxResponse, ExecutionFailureKind> {
|
) -> Result<SendTxResponse, ExecutionFailureKind> {
|
||||||
let (instruction, program) =
|
let instruction = amm_core::Instruction::RemoveLiquidity {
|
||||||
amm_program_preparation_remove_liq(balance_lp, min_amount_a, min_amount_b);
|
remove_liquidity_amount,
|
||||||
|
min_amount_to_remove_token_a,
|
||||||
|
min_amount_to_remove_token_b,
|
||||||
|
};
|
||||||
|
let program = Program::amm();
|
||||||
let amm_program_id = Program::amm().id();
|
let amm_program_id = Program::amm().id();
|
||||||
|
|
||||||
let user_a_acc = self
|
let user_a_acc = self
|
||||||
@ -448,93 +369,3 @@ impl Amm<'_> {
|
|||||||
Ok(self.0.sequencer_client.send_tx_public(tx).await?)
|
Ok(self.0.sequencer_client.send_tx_public(tx).await?)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn amm_program_preparation_definition(balance_a: u128, balance_b: u128) -> (Vec<u8>, Program) {
|
|
||||||
// An instruction data of 65-bytes, indicating the initial amm reserves' balances and
|
|
||||||
// token_program_id with the following layout:
|
|
||||||
// [0x00 || array of balances (little-endian 16 bytes) || AMM_PROGRAM_ID)]
|
|
||||||
let amm_program_id = Program::amm().id();
|
|
||||||
|
|
||||||
let mut instruction = [0; 65];
|
|
||||||
instruction[1..17].copy_from_slice(&balance_a.to_le_bytes());
|
|
||||||
instruction[17..33].copy_from_slice(&balance_b.to_le_bytes());
|
|
||||||
|
|
||||||
// This can be done less verbose, but it is better to use same way, as in amm program
|
|
||||||
instruction[33..37].copy_from_slice(&amm_program_id[0].to_le_bytes());
|
|
||||||
instruction[37..41].copy_from_slice(&amm_program_id[1].to_le_bytes());
|
|
||||||
instruction[41..45].copy_from_slice(&amm_program_id[2].to_le_bytes());
|
|
||||||
instruction[45..49].copy_from_slice(&amm_program_id[3].to_le_bytes());
|
|
||||||
instruction[49..53].copy_from_slice(&amm_program_id[4].to_le_bytes());
|
|
||||||
instruction[53..57].copy_from_slice(&amm_program_id[5].to_le_bytes());
|
|
||||||
instruction[57..61].copy_from_slice(&amm_program_id[6].to_le_bytes());
|
|
||||||
instruction[61..].copy_from_slice(&amm_program_id[7].to_le_bytes());
|
|
||||||
|
|
||||||
let instruction_data = instruction.to_vec();
|
|
||||||
let program = Program::amm();
|
|
||||||
|
|
||||||
(instruction_data, program)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn amm_program_preparation_swap(
|
|
||||||
amount_in: u128,
|
|
||||||
min_amount_out: u128,
|
|
||||||
token_definition_id: AccountId,
|
|
||||||
) -> (Vec<u8>, Program) {
|
|
||||||
// An instruction data byte string of length 65, indicating which token type to swap, quantity
|
|
||||||
// of tokens put into the swap (of type TOKEN_DEFINITION_ID) and min_amount_out.
|
|
||||||
// [0x01 || amount (little-endian 16 bytes) || TOKEN_DEFINITION_ID].
|
|
||||||
let mut instruction = [0; 65];
|
|
||||||
instruction[0] = 0x01;
|
|
||||||
instruction[1..17].copy_from_slice(&amount_in.to_le_bytes());
|
|
||||||
instruction[17..33].copy_from_slice(&min_amount_out.to_le_bytes());
|
|
||||||
|
|
||||||
// This can be done less verbose, but it is better to use same way, as in amm program
|
|
||||||
instruction[33..].copy_from_slice(&token_definition_id.to_bytes());
|
|
||||||
|
|
||||||
let instruction_data = instruction.to_vec();
|
|
||||||
let program = Program::amm();
|
|
||||||
|
|
||||||
(instruction_data, program)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn amm_program_preparation_add_liq(
|
|
||||||
min_amount_lp: u128,
|
|
||||||
max_amount_a: u128,
|
|
||||||
max_amount_b: u128,
|
|
||||||
) -> (Vec<u8>, Program) {
|
|
||||||
// An instruction data byte string of length 49, amounts for minimum amount of liquidity from
|
|
||||||
// add (min_amount_lp), max amount added for each token (max_amount_a and max_amount_b);
|
|
||||||
// indicate [0x02 || array of of balances (little-endian 16 bytes)].
|
|
||||||
let mut instruction = [0; 49];
|
|
||||||
instruction[0] = 0x02;
|
|
||||||
|
|
||||||
instruction[1..17].copy_from_slice(&min_amount_lp.to_le_bytes());
|
|
||||||
instruction[17..33].copy_from_slice(&max_amount_a.to_le_bytes());
|
|
||||||
instruction[33..49].copy_from_slice(&max_amount_b.to_le_bytes());
|
|
||||||
|
|
||||||
let instruction_data = instruction.to_vec();
|
|
||||||
let program = Program::amm();
|
|
||||||
|
|
||||||
(instruction_data, program)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn amm_program_preparation_remove_liq(
|
|
||||||
balance_lp: u128,
|
|
||||||
min_amount_a: u128,
|
|
||||||
min_amount_b: u128,
|
|
||||||
) -> (Vec<u8>, Program) {
|
|
||||||
// An instruction data byte string of length 49, amounts for minimum amount of liquidity to
|
|
||||||
// redeem (balance_lp), minimum balance of each token to remove (min_amount_a and
|
|
||||||
// min_amount_b); indicate [0x03 || array of balances (little-endian 16 bytes)].
|
|
||||||
let mut instruction = [0; 49];
|
|
||||||
instruction[0] = 0x03;
|
|
||||||
|
|
||||||
instruction[1..17].copy_from_slice(&balance_lp.to_le_bytes());
|
|
||||||
instruction[17..33].copy_from_slice(&min_amount_a.to_le_bytes());
|
|
||||||
instruction[33..49].copy_from_slice(&min_amount_b.to_le_bytes());
|
|
||||||
|
|
||||||
let instruction_data = instruction.to_vec();
|
|
||||||
let program = Program::amm();
|
|
||||||
|
|
||||||
(instruction_data, program)
|
|
||||||
}
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user