├── .cargo └── audit.toml ├── .dockerignore ├── .env.example ├── .github ├── codecov.yml ├── dependabot.yml └── workflows │ ├── audit.yml │ ├── docker-ghcr.yml │ ├── dockerhub.yml │ ├── release.yml │ ├── rust.yml │ ├── spelling.yml │ └── wasm.yml ├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── Dockerfile ├── Dockerfile.alpine ├── LICENSE ├── README.md ├── conf ├── grafana-datasources.yaml ├── prometheus.yaml └── tempo.yaml ├── docker-compose.yml ├── entrypoint.sh ├── flake.lock ├── flake.nix ├── integrationtests ├── Cargo.toml ├── README.md ├── src │ ├── bitcoin_client.rs │ ├── lib.rs │ ├── lnbitsmock.rs │ ├── lnd_client.rs │ └── setup.rs └── tests │ ├── fixtures │ ├── invoice_1000.txt │ └── token_10.cashu │ ├── tests_lnbitsmock.rs │ ├── tests_lnd.rs │ ├── tests_moksha_cli.rs │ └── tests_nutshell_compatibility.rs ├── justfile ├── k8s └── moksha-mint │ ├── .helmignore │ ├── Chart.yaml │ ├── templates │ ├── NOTES.txt │ ├── _helpers.tpl │ ├── deployment.yaml │ ├── hpa.yaml │ ├── ingress.yaml │ ├── service.yaml │ ├── serviceaccount.yaml │ └── tests │ │ └── test-connection.yaml │ └── values.yaml ├── moksha-cli ├── Cargo.toml └── src │ ├── bin │ └── moksha-cli.rs │ ├── cli.rs │ └── lib.rs ├── moksha-core ├── Cargo.toml ├── README.md ├── benches │ └── dhke_benchmarks.rs └── src │ ├── amount.rs │ ├── blind.rs │ ├── dhke.rs │ ├── error.rs │ ├── fixture.rs │ ├── fixtures │ ├── incomplete_mint_info.json │ ├── nutshell_mint_info.json │ ├── nutshell_mint_info_v0.16.0.json │ ├── token_60.cashu │ ├── token_invalid.cashu │ ├── token_no_pad60.cashu │ └── token_nut_example.cashu │ ├── keyset.rs │ ├── lib.rs │ ├── primitives.rs │ ├── proof.rs │ └── token.rs ├── moksha-mint ├── .sqlx │ ├── query-2cb95b0c3011a332322132339e6023035e4a81824bef6a0ad47215f851fb1100.json │ ├── query-3a797dad2f155d90b625ffcd3d89e5b4b2050a52645bd8d8270cc0d8946eb249.json │ ├── query-51048c44648c7a3140c37027765ef0a9bdf7c8a050d8353f3f4083c80bbc03f7.json │ ├── query-594c0ed8b964bdf16208ab5909c05bbfe15c245f667646b2450b5bd649cf219c.json │ ├── query-5d14a8fcd6f0e680a3868e48a513d93eefb73461b7fc7cdf17996e8d979b1abf.json │ ├── query-6d6efea17e2e8799c4e2e09c5eabf8e0e59da3646de7f1471b4507df7168ed32.json │ ├── query-76f0b81d265374f59b1222f1c07d814093c9d225334f39549b0c05e0b733dcc6.json │ ├── query-87908c797aa3343256255edb331044d890d22d06adc0313823dc8448e21e3f1c.json │ ├── query-9b7392f6768ac112b9cfd7e4e08ca5c3949884a3f7b8139b61cd7446d4983a05.json │ ├── query-9db8c4f6f5c71b2a0e4476429931112334053812decdbc7869404a7806d33856.json │ ├── query-9deb32cf2da6eb3d4b099556891f4ef979dde0f4c3ae901c7617d2e3c602a691.json │ ├── query-aa1d3252354b9468831dfd2a3a499b5a52850bf3ec7da7a6c1a74b43d4970649.json │ ├── query-b1fd2ab2863004ad03ddfd0a0eb8084456f0dd28aff814a440d29edf3574028f.json │ ├── query-bbb5353af44cc7ef7e5d91a10a5bd0422c6a2f0e52a079dd41f012994cf3b4ea.json │ ├── query-bc67cae55e8dffda179773dcaec8275b37ff9e286e3d813d7aaad9a90cf793af.json │ ├── query-ce99d546aa2c36df35aa38a29e50e46c5ba5ad037d75315d04fb456c5201a924.json │ ├── query-d06538e9bb5466ea92afcee20108bc69b1d0771deea19e701c391c9bd6f38362.json │ ├── query-d8fa5a9f70c2a2d00433b2a9e2ddb1b9e55255e9c5409d9f3c4d33dd9338455e.json │ ├── query-dc1215b1d311ad0d1386f5058ff1ab8d4edf6314b4ab02ed2045b72603d01703.json │ ├── query-e4c704c7bf2a67103c56420f4c3388536ee546962368c41a898abaa9bf6f90fe.json │ └── query-eb5c7406dcca5d043c2cd3fd1c2618a47191cde482b2071246e2f007195cd3b4.json ├── Cargo.toml ├── migrations │ ├── 20231213080237_init.sql │ ├── 20240112101746_onchain.sql │ ├── 20240408071034_btconchain_melt_description.sql │ ├── 20240729115553_btconchain_melt_description_optional.sql │ ├── 20240801112647_btconchain_melt_state.sql │ └── 20240801124852_btconchain_mint_state.sql └── src │ ├── bin │ └── moksha-mint.rs │ ├── btconchain │ ├── lnd.rs │ └── mod.rs │ ├── config.rs │ ├── database │ ├── mod.rs │ └── postgres.rs │ ├── error.rs │ ├── fixtures │ ├── blinded_messages_40.json │ ├── blinded_messages_blank_4000.json │ ├── post_swap_request_64_20.json │ ├── post_swap_request_duplicate_key.json │ └── token_60.cashu │ ├── lib.rs │ ├── lightning │ ├── alby.rs │ ├── cln.rs │ ├── error.rs │ ├── lnbits.rs │ ├── lnd.rs │ ├── mod.rs │ └── strike.rs │ ├── mint.rs │ ├── model.rs │ ├── routes │ ├── btconchain.rs │ ├── default.rs │ └── mod.rs │ ├── server.rs │ └── url_serialize.rs ├── moksha-wallet ├── .sqlx │ ├── query-0913842a79a647c1190faa24d948ff27f74e24d8c2ca37c10861c2e128905013.json │ ├── query-1aa7a37640cde629ef99ebb746875208743b9fc168fa8cffd848b671014b8bf1.json │ ├── query-609877b640a209756b504ef93f9d0a41bca0c9c0eece9ec91a50195412d9adae.json │ ├── query-6caae4e19877ee8ab21df304f5d0945ae771fe2b59169958bdc9ce7c0eb73090.json │ ├── query-7373a18c132ed16ac8ae383f008ebd53e8d588375b06e40335e91da39c680ad3.json │ ├── query-c08a68cb103a8fe16fddbe23d163a04db5b9639ea87a8a1472ccca5d5194afc6.json │ └── query-e99d7276a6bb8d0b0f456e8d00ec38e1192ef3f25f9204550ed30aa3a0bd7e3f.json ├── Cargo.toml ├── examples │ └── receive_tokens.rs ├── migrations │ ├── 20230530061910_init.sql │ └── 20240329082342_deterministic_secrets.sql └── src │ ├── client │ ├── crossplatform.rs │ └── mod.rs │ ├── config_path.rs │ ├── error.rs │ ├── fixtures │ ├── blinded_messages_40.json │ ├── post_melt_quote_response.json │ ├── post_melt_response_21.json │ ├── post_melt_response_not_paid.json │ ├── post_mint_response_20.json │ ├── post_swap_response_24_40.json │ ├── pub_keys.json │ ├── token_60.cashu │ └── token_64.cashu │ ├── http │ ├── mod.rs │ ├── reqwest.rs │ └── wasm.rs │ ├── lib.rs │ ├── localstore │ ├── mod.rs │ ├── rexie.rs │ └── sqlite.rs │ ├── secret.rs │ └── wallet.rs ├── rust-toolchain.toml └── typos.toml /.cargo/audit.toml: -------------------------------------------------------------------------------- 1 | [advisories] 2 | ignore = ["RUSTSEC-2023-0071"] 3 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | **/target 2 | data 3 | tmp 4 | conf 5 | .git 6 | 7 | 8 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | #### enviroment variables for the mint 2 | RUST_LOG=debug 3 | 4 | # if set to 'dev' env variables from the .env file will be used 5 | MINT_APP_ENV=dev 6 | # connection string for the postgres database 7 | MINT_DB_URL=postgres://postgres:postgres@127.0.0.1/moksha-mint 8 | # Set the maximum number of connections that the pool should maintain (default 5) (optional) 9 | MINT_DB_MAX_CONNECTIONS=5 10 | # the private key of the mint 11 | MINT_PRIVATE_KEY=superprivatesecretkey 12 | 13 | # the derivation path for the mint (optional) 14 | MINT_DERIVATION_PATH="/0/0/0/0" 15 | 16 | 17 | # the host and port the mint will listen on int the format https://doc.rust-lang.org/std/net/enum.SocketAddr.html 18 | # if the variable is not set the mint will listen on all interfaces on port 3338 19 | MINT_HOST_PORT="[::]:3338" 20 | 21 | # optional prefix for the api. If set the api will be served under the given prefix. 22 | # This is useful if the mint is served behind a reverse proxy 23 | # (optional) 24 | #MINT_API_PREFIX=/api 25 | 26 | # if set will serve the wallet from the given path 27 | #MINT_SERVE_WALLET_PATH=./flutter/build/web 28 | 29 | # mint info (optional) 30 | MINT_INFO_NAME=moksha-mint 31 | # If set to true the version of the mint crate will be displayed in the mint info 32 | MINT_INFO_VERSION=true 33 | MINT_INFO_DESCRIPTION="mint description" 34 | MINT_INFO_DESCRIPTION_LONG="mint description long" 35 | MINT_INFO_MOTD="some message of the day" 36 | MINT_INFO_CONTACT_EMAIL="contact@me.com" 37 | MINT_INFO_CONTACT_TWITTER="@me" 38 | MINT_INFO_CONTACT_NOSTR="npub123" 39 | 40 | # fee configuration (optional) defaults to 1.0 / 4000 41 | MINT_LIGHTNING_FEE_PERCENT=1.0 42 | MINT_LIGHTNING_RESERVE_FEE_MIN=4000 43 | 44 | # configure the lightning backend. 45 | # currently supported backends are: 46 | # - Lnbits 47 | # - Alby 48 | # - Strike 49 | # - Lnd 50 | # you are required to set the corresponding environment variables for the backend you want to use 51 | MINT_LIGHTNING_BACKEND=Lnbits 52 | MINT_LNBITS_URL=https://.com 53 | MINT_LNBITS_ADMIN_KEY=YOUR_ADMIN_KEY 54 | 55 | #MINT_LIGHTNING_BACKEND=Alby 56 | MINT_ALBY_API_KEY=YOUR_API_KEY 57 | 58 | #MINT_LIGHTNING_BACKEND=Strike 59 | MINT_STRIKE_API_KEY=YOUR_API_KEY 60 | 61 | #MINT_LIGHTNING_BACKEND=Lnd 62 | # absolute path to the lnd macaroon file 63 | MINT_LND_MACAROON_PATH="/.../admin.macaroon" 64 | # absolute path to the tls certificate 65 | MINT_LND_TLS_CERT_PATH="/../tls.cert" 66 | # the host and port of the lnd grpc api 67 | MINT_LND_GRPC_HOST="https://localhost:10004" 68 | 69 | # (optional) base64 encoded macaroon and tls certificate instead of the file paths 70 | MINT_LND_MACAROON_BASE64="base64 encoded macaroon" 71 | MINT_LND_TLS_CERT_BASE64="base64 encoded tls cert" 72 | 73 | 74 | # (optional) onchain backend for the mint. Uses the same configuration as the lnd lightning backend 75 | #MINT_BTC_ONCHAIN_BACKEND=Lnd 76 | #MINT_BTC_ONCHAIN_BACKEND_MIN_AMOUNT=10000 77 | #MINT_BTC_ONCHAIN_BACKEND_MAX_AMOUNT=1000000 78 | #MINT_BTC_ONCHAIN_BACKEND_MIN_CONFIRMATIONS=1 79 | 80 | # (optional) enable tracing with open telemetry 81 | #MINT_TRACING_ENDPOINT="http://127.0.0.1:4318" 82 | -------------------------------------------------------------------------------- /.github/codecov.yml: -------------------------------------------------------------------------------- 1 | # ref: https://docs.codecov.com/docs/codecovyml-reference 2 | coverage: 3 | status: 4 | project: 5 | default: 6 | informational: true 7 | patch: 8 | default: 9 | informational: true 10 | comment: false 11 | 12 | # Test files aren't important for coverage 13 | ignore: 14 | - "tests" 15 | - "**/*generated*.rs" 16 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "cargo" 9 | directory: "/" 10 | schedule: 11 | interval: "daily" 12 | - package-ecosystem: github-actions 13 | directory: "/" 14 | schedule: 15 | interval: daily 16 | -------------------------------------------------------------------------------- /.github/workflows/audit.yml: -------------------------------------------------------------------------------- 1 | name: Audit 2 | on: 3 | pull_request: 4 | branches: [master] 5 | 6 | jobs: 7 | security_audit: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v4 11 | - uses: actions-rs/audit-check@v1 12 | with: 13 | token: ${{ secrets.GITHUB_TOKEN }} 14 | -------------------------------------------------------------------------------- /.github/workflows/docker-ghcr.yml: -------------------------------------------------------------------------------- 1 | name: docker-push-ghcr 2 | 3 | on: 4 | workflow_dispatch: 5 | 6 | jobs: 7 | build-and-push: 8 | runs-on: ubuntu-latest 9 | 10 | steps: 11 | - name: Checkout code 12 | uses: actions/checkout@v4 13 | 14 | - name: Log in to GHCR 15 | uses: docker/login-action@v3.3.0 16 | with: 17 | registry: ghcr.io 18 | username: ${{ github.actor }} 19 | password: ${{ secrets.GHCR_RW }} 20 | 21 | - name: Build and push Docker image 22 | uses: docker/build-push-action@v6 23 | with: 24 | context: . 25 | push: true 26 | tags: ghcr.io/${{ github.repository_owner }}/moksha-mint:latest 27 | secrets: | 28 | GIT_AUTH_TOKEN=${{ secrets.GHCR_RW }} 29 | -------------------------------------------------------------------------------- /.github/workflows/dockerhub.yml: -------------------------------------------------------------------------------- 1 | name: docker-push-dockerhub 2 | 3 | on: 4 | push: 5 | branches: ["master"] 6 | 7 | jobs: 8 | build-and-push: 9 | runs-on: ubuntu-latest 10 | 11 | steps: 12 | - name: Checkout code 13 | uses: actions/checkout@v4 14 | 15 | - name: Log in to docker.io 16 | uses: docker/login-action@v3.3.0 17 | with: 18 | registry: docker.io 19 | username: ${{ secrets.DOCKER_NAME }} 20 | password: ${{ secrets.DOCKER_PAT }} 21 | - name: Get current date 22 | id: date 23 | run: echo "::set-output name=date::$(date -u '+%F-%T')" 24 | 25 | - name: Get short SHA 26 | id: sha 27 | run: echo "::set-output name=sha::$(git rev-parse --short HEAD)" 28 | 29 | - name: Build and push Docker image bullseye 30 | uses: docker/build-push-action@v6 31 | with: 32 | context: . 33 | file: ./Dockerfile 34 | push: true 35 | build-args: | 36 | COMMITHASH=${{ steps.sha.outputs.sha }} 37 | BUILDTIME=${{ steps.date.outputs.date }} 38 | tags: docker.io/${{ secrets.DOCKER_NAME }}/moksha-mint:${{ steps.sha.outputs.sha }}-bullseye, docker.io/${{ secrets.DOCKER_NAME }}/moksha-mint:bullseye 39 | 40 | - name: Build and push Docker image alpine 41 | uses: docker/build-push-action@v6 42 | with: 43 | context: . 44 | file: ./Dockerfile.alpine 45 | push: true 46 | build-args: | 47 | COMMITHASH=${{ steps.sha.outputs.sha }} 48 | BUILDTIME=${{ steps.date.outputs.date }} 49 | tags: docker.io/${{ secrets.DOCKER_NAME }}/moksha-mint:latest, docker.io/${{ secrets.DOCKER_NAME }}/moksha-mint:${{ steps.sha.outputs.sha }}-alpine 50 | deploy: 51 | runs-on: ubuntu-latest 52 | needs: build-and-push 53 | steps: 54 | - name: DigitalOcean App Platform deployment 55 | uses: digitalocean/app_action/deploy@v2 56 | with: 57 | app_name: moksha-mint 58 | token: ${{ secrets.DO_TOKEN }} 59 | -------------------------------------------------------------------------------- /.github/workflows/rust.yml: -------------------------------------------------------------------------------- 1 | name: Rust 2 | 3 | on: 4 | push: 5 | branches: ["master"] 6 | pull_request: 7 | branches: ["master"] 8 | 9 | env: 10 | CARGO_TERM_COLOR: always 11 | 12 | jobs: 13 | build: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v4 17 | - name: Install protobuf-compiler 18 | run: sudo apt-get update && sudo apt-get install -y protobuf-compiler 19 | - name: Set up Docker Buildx 20 | uses: docker/setup-buildx-action@v3 21 | - name: Install just 22 | run: cargo install just 23 | - name: Run Docker Compose 24 | run: docker compose --profile itest up -d 25 | - name: check formatting 26 | run: cargo fmt --all -- --check 27 | - name: Build 28 | run: cargo build --verbose 29 | - name: chmod data-dir 30 | run: sudo chmod -R a+rwx ./data 31 | - name: Run tests 32 | run: just run-tests 33 | - name: Run itests 34 | run: just run-itests 35 | coverage: 36 | runs-on: ubuntu-latest 37 | name: coverage 38 | 39 | steps: 40 | - uses: actions/checkout@v4 41 | with: 42 | submodules: true 43 | - name: Run Docker Compose 44 | run: docker compose --profile itest up -d 45 | - name: Install just 46 | run: cargo install just 47 | - name: Install grcov 48 | run: cargo install grcov 49 | - name: Install protobuf-compiler 50 | run: sudo apt-get update && sudo apt-get install -y protobuf-compiler 51 | - name: Install stable 52 | uses: dtolnay/rust-toolchain@stable 53 | with: 54 | toolchain: stable 55 | - name: Install llvm-tools-preview 56 | run: rustup component add llvm-tools-preview 57 | - name: Build 58 | run: cargo build --verbose 59 | - name: chmod data-dir 60 | run: sudo chmod -R a+rwx ./data 61 | - name: run coverage test 62 | run: just run-coverage-tests 63 | - name: delete data-dir 64 | run: sudo rm -rf ./data 65 | - name: run coverage-report 66 | run: just run-coverage-report 67 | - name: Upload to codecov.io 68 | uses: codecov/codecov-action@v5 69 | with: 70 | token: ${{ secrets.CODECOV_TOKEN }} 71 | fail_ci_if_error: true 72 | files: ./target/coverage/lcov 73 | -------------------------------------------------------------------------------- /.github/workflows/spelling.yml: -------------------------------------------------------------------------------- 1 | name: Spelling 2 | on: 3 | push: 4 | branches: ["master"] 5 | pull_request: 6 | branches: ["master"] 7 | 8 | jobs: 9 | spelling: 10 | name: Spell Check with Typos 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Checkout Actions Repository 14 | uses: actions/checkout@v4 15 | - name: Spell Check Repo 16 | uses: crate-ci/typos@master 17 | with: 18 | config: typos.toml 19 | -------------------------------------------------------------------------------- /.github/workflows/wasm.yml: -------------------------------------------------------------------------------- 1 | name: wasm 2 | 3 | on: 4 | push: 5 | branches: ["master"] 6 | pull_request: 7 | branches: ["master"] 8 | 9 | env: 10 | CARGO_TERM_COLOR: always 11 | 12 | jobs: 13 | build: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v4 17 | - name: Set up Rust 18 | uses: dtolnay/rust-toolchain@stable 19 | with: 20 | toolchain: nightly 21 | - name: Add wasm32-unknown-unknown target 22 | run: rustup target add wasm32-unknown-unknown 23 | - name: Add nightly toolchain 24 | run: rustup component add rust-src --toolchain nightly 25 | - name: Install just 26 | run: cargo install just 27 | - name: Build 28 | run: just build-wasm 29 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | tmp 2 | target 3 | .env 4 | .idea 5 | .vscode 6 | data 7 | .DS_Store 8 | *.profraw 9 | build 10 | **/node_modules 11 | *.tgz 12 | docker-compose.override.yml -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | resolver = "2" 3 | 4 | members = [ 5 | "moksha-core", 6 | "moksha-cli", 7 | "moksha-wallet", 8 | "moksha-mint", 9 | "integrationtests", 10 | ] 11 | 12 | [profile.dev] 13 | split-debuginfo = "packed" 14 | 15 | [profile.release] 16 | strip = true # Automatically strip symbols from the binary. 17 | lto = true # Enable link-time optimization. 18 | codegen-units = 1 # Reduce the number of object files to speed up compilation. 19 | 20 | # The profile that 'cargo dist' will build with 21 | [profile.dist] 22 | inherits = "release" 23 | lto = "thin" 24 | strip = true 25 | codegen-units = 1 26 | 27 | [workspace.metadata] 28 | authors = ["The moksha Developers"] 29 | edition = "2021" 30 | description = "moksha is a cashu wallet and mint" 31 | readme = "README.md" 32 | repository = "https://github.com/ngutech21/moksha" 33 | license-file = "LICENSE" 34 | keywords = ["bitcoin", "e-cash"] 35 | 36 | # Config for 'cargo dist' 37 | [workspace.metadata.dist] 38 | # The preferred cargo-dist version to use in CI (Cargo.toml SemVer syntax) 39 | cargo-dist-version = "0.10.0" 40 | # CI backends to support 41 | ci = ["github"] 42 | # The installers to generate for each app 43 | installers = [] 44 | # Target platforms to build apps for (Rust target-triple syntax) 45 | targets = ["x86_64-apple-darwin", "x86_64-unknown-linux-gnu", "x86_64-pc-windows-msvc"] 46 | # Publish jobs to run in CI 47 | pr-run-mode = "plan" 48 | allow-dirty = ["ci"] 49 | 50 | [workspace.metadata.dist.dependencies.apt] 51 | protobuf-compiler = '*' 52 | 53 | [workspace.metadata.dist.dependencies.homebrew] 54 | protobuf = '*' 55 | sqlite = '*' 56 | 57 | [workspace.metadata.dist.dependencies.chocolatey] 58 | protoc = '*' 59 | nasm = '*' 60 | activeperl = "*" 61 | 62 | [workspace.metadata.dist.github-custom-runners] 63 | x86_64-unknown-linux-gnu = "ubuntu-22.04" 64 | 65 | [workspace.dependencies] 66 | anyhow = "1.0.94" 67 | assert_cmd = "2.0.14" 68 | async-trait = "0.1.80" 69 | axum = "0.7.9" 70 | base64 = "0.22.1" 71 | bip32 = "0.5.1" 72 | bip39 = "2.0.0" 73 | bitcoincore-rpc = "0.18.0" 74 | chrono = "0.4.39" 75 | clap = "4.5.23" 76 | cln-grpc = "=0.1.8" 77 | console = "0.15.8" 78 | dialoguer = "0.11.0" 79 | dirs = "5.0.1" 80 | dotenvy = "0.15.7" 81 | fedimint-tonic-lnd = "0.2.0" 82 | hex = "0.4.3" 83 | http-body-util = "0.1.0" 84 | hyper = "1" 85 | indicatif = "0.17.9" 86 | itertools = "0.13.0" 87 | mockall = "0.13.1" 88 | num-format = "0.4.4" 89 | opentelemetry = "0.22.0" 90 | opentelemetry-otlp = "0.15.0" 91 | opentelemetry_sdk = "0.22.1" 92 | pretty_assertions = "1.4.0" 93 | qrcode = "0.14.0" 94 | rand = "0.8.5" 95 | reqwest = { version = "0.12.9", default-features = false } 96 | serde = "1.0.216" 97 | serde_json = "1.0.133" 98 | serde_with = "3.11.0" 99 | sqlx = { version = "0.8.2", default-features = false } 100 | tempfile = "3.14.0" 101 | testcontainers = "0.23.1" 102 | testcontainers-modules = "0.11.4" 103 | thiserror = "2.0.7" 104 | tokio = "1.42.0" 105 | tonic = "0.8" 106 | tower = "0.5.2" 107 | tower-http = "0.6.2" 108 | tracing = "0.1.40" 109 | tracing-opentelemetry = "0.23.0" 110 | tracing-subscriber = "0.3.18" 111 | url = "2.5.4" 112 | utoipa = "5.2.0" 113 | utoipa-swagger-ui = "8.0.3" 114 | uuid = "1" 115 | secp256k1 = { version = "0.29.0", default-features = false } 116 | 117 | [profile.dev.package] 118 | secp256k1 = { opt-level = 3 } 119 | secp256k1-sys = { opt-level = 3 } 120 | bitcoin_hashes = { opt-level = 3 } 121 | rand_core = { opt-level = 3 } 122 | byteorder = { opt-level = 3 } 123 | zeroize = { opt-level = 3 } 124 | subtle = { opt-level = 3 } 125 | ring = { opt-level = 3 } 126 | sqlx-macros = { opt-level = 3 } 127 | 128 | 129 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # build backend 2 | FROM rust:1.83.0-slim-bullseye as rust-builder 3 | RUN apt update && apt install -y make clang pkg-config protobuf-compiler curl 4 | 5 | WORKDIR /rust-app 6 | COPY . /rust-app 7 | RUN cargo build --package moksha-mint --release 8 | 9 | 10 | FROM bitnami/minideb:bullseye 11 | COPY --from=rust-builder /rust-app/target/release/moksha-mint /app/ 12 | 13 | COPY --chmod=755 ./entrypoint.sh /app/entrypoint.sh 14 | 15 | USER 1000 16 | WORKDIR /app 17 | ENTRYPOINT ["/app/entrypoint.sh"] 18 | 19 | ARG BUILDTIME 20 | ARG COMMITHASH 21 | ENV BUILDTIME ${BUILDTIME} 22 | ENV COMMITHASH ${COMMITHASH} 23 | 24 | CMD ["/app/moksha-mint"] -------------------------------------------------------------------------------- /Dockerfile.alpine: -------------------------------------------------------------------------------- 1 | # build backend 2 | FROM rust:1.83.0-slim-bullseye as rust-builder 3 | RUN apt update && apt install -y musl-tools musl-dev make clang pkg-config protobuf-compiler curl 4 | RUN update-ca-certificates 5 | RUN rustup target add x86_64-unknown-linux-musl 6 | 7 | 8 | WORKDIR /rust-app 9 | COPY . /rust-app 10 | RUN cargo build --package moksha-mint --release --target x86_64-unknown-linux-musl 11 | 12 | 13 | FROM alpine:3.20.1 14 | RUN apk --no-cache add ca-certificates 15 | COPY --from=rust-builder /rust-app/target/x86_64-unknown-linux-musl/release/moksha-mint /app/ 16 | 17 | USER 1000 18 | WORKDIR /app 19 | 20 | ARG BUILDTIME 21 | ARG COMMITHASH 22 | ENV BUILDTIME ${BUILDTIME} 23 | ENV COMMITHASH ${COMMITHASH} 24 | 25 | CMD ["/app/moksha-mint"] -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Steffen 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /conf/grafana-datasources.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: 1 3 | 4 | datasources: 5 | - name: "Prometheus" 6 | type: "prometheus" 7 | uid: "prometheus" 8 | access: "proxy" 9 | orgId: 1 10 | url: "http://prometheus:9090" 11 | basicAuth: false 12 | isDefault: false 13 | version: 1 14 | editable: false 15 | jsonData: 16 | httpMethod: "GET" 17 | - name: "Tempo" 18 | type: "tempo" 19 | uid: "tempo" 20 | access: "proxy" 21 | orgId: 1 22 | url: "http://tempo:3200" 23 | basicAuth: false 24 | isDefault: true 25 | version: 1 26 | editable: false 27 | apiVersion: 1 28 | jsonData: 29 | httpMethod: "GET" 30 | serviceMap: 31 | datasourceUid: "prometheus" 32 | -------------------------------------------------------------------------------- /conf/prometheus.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | global: 3 | scrape_interval: "15s" 4 | evaluation_interval: "15s" 5 | 6 | scrape_configs: 7 | - job_name: "prometheus" 8 | static_configs: 9 | - targets: 10 | - "localhost:9090" 11 | - job_name: "tempo" 12 | static_configs: 13 | - targets: 14 | - "tempo:3200" 15 | -------------------------------------------------------------------------------- /conf/tempo.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | server: 3 | http_listen_port: 3200 4 | 5 | distributor: 6 | receivers: 7 | otlp: # for microservices 8 | protocols: 9 | grpc: 10 | http: 11 | ingester: 12 | max_block_duration: "5m" 13 | 14 | compactor: 15 | compaction: 16 | block_retention: "1h" 17 | 18 | metrics_generator: 19 | registry: 20 | external_labels: 21 | source: "tempo" 22 | cluster: "docker-compose" 23 | storage: 24 | path: "/tmp/tempo/generator/wal" 25 | remote_write: 26 | - url: "http://prometheus:9090/api/v1/write" 27 | send_exemplars: true 28 | 29 | storage: 30 | trace: 31 | backend: "local" 32 | wal: 33 | path: "/tmp/tempo/wal" 34 | local: 35 | path: "/tmp/tempo/blocks" 36 | 37 | overrides: 38 | metrics_generator_processors: 39 | - "service-graphs" 40 | - "span-metrics" 41 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | database: 3 | image: "postgres:16.6-alpine" 4 | container_name: moksha-mint-db 5 | ports: 6 | - 5432:5432 7 | environment: 8 | POSTGRES_USER: postgres 9 | POSTGRES_PASSWORD: postgres 10 | POSTGRES_DB: moksha-mint 11 | profiles: 12 | - tracing 13 | bitcoind: 14 | stop_grace_period: 5m 15 | image: btcpayserver/bitcoin:0.21.0 16 | container_name: bitcoind 17 | hostname: bitcoind 18 | command: >- 19 | bitcoind 20 | -server=1 21 | -regtest=1 22 | -rpcauth=polaruser:5e5e98c21f5c814568f8b55d83b23c1c$$066b03f92df30b11de8e4b1b1cd5b1b4281aa25205bd57df9be82caf97a05526 23 | -debug=1 24 | -zmqpubrawblock=tcp://0.0.0.0:28334 25 | -zmqpubrawtx=tcp://0.0.0.0:28335 26 | -zmqpubhashblock=tcp://0.0.0.0:28336 27 | -txindex=1 28 | -dnsseed=0 29 | -upnp=0 30 | -rpcbind=0.0.0.0 31 | -rpcallowip=0.0.0.0/0 32 | -rpcport=18443 33 | -listen=1 34 | -listenonion=0 35 | -fallbackfee=0.0002 36 | -blockfilterindex=1 37 | 38 | volumes: 39 | - ${HOST_PROJECT_PATH:-.}/data/bitcoind:/home/bitcoin/.bitcoin 40 | expose: 41 | - "18443" 42 | - "18444" 43 | - "28334" 44 | - "28335" 45 | ports: 46 | - "18453:18443" #json rpc 47 | profiles: 48 | - itest 49 | 50 | lnd-mint: 51 | depends_on: 52 | - bitcoind 53 | stop_grace_period: 2m 54 | image: lightninglabs/lnd:v0.17.4-beta 55 | container_name: lnd-mint 56 | hostname: lnd-mint 57 | command: >- 58 | lnd 59 | --noseedbackup 60 | --trickledelay=5000 61 | --alias=lnd-mint 62 | --externalip=lnd-mint 63 | --tlsextradomain=lnd-mint 64 | --tlsextradomain=lnd-mint 65 | --tlsextradomain=host.docker.internal 66 | --listen=0.0.0.0:9735 67 | --rpclisten=0.0.0.0:10009 68 | --bitcoin.active 69 | --bitcoin.regtest 70 | --bitcoin.node=bitcoind 71 | --bitcoind.rpchost=bitcoind 72 | --bitcoind.rpcuser=polaruser 73 | --bitcoind.rpcpass=polarpass 74 | --bitcoind.zmqpubrawblock=tcp://bitcoind:28334 75 | --bitcoind.zmqpubrawtx=tcp://bitcoind:28335 76 | restart: always 77 | volumes: 78 | - ${HOST_PROJECT_PATH:-.}/data/lnd-mint:/root/.lnd 79 | expose: 80 | - "10009" 81 | ports: 82 | - "11001:10009" 83 | profiles: 84 | - itest 85 | 86 | lnd-wallet: 87 | depends_on: 88 | - bitcoind 89 | stop_grace_period: 2m 90 | image: lightninglabs/lnd:v0.17.4-beta 91 | container_name: lnd-wallet 92 | hostname: lnd-wallet 93 | command: >- 94 | lnd 95 | --noseedbackup 96 | --trickledelay=5000 97 | --alias=lnd-wallet 98 | --externalip=lnd-wallet 99 | --tlsextradomain=lnd-wallet 100 | --tlsextradomain=lnd-wallet 101 | --tlsextradomain=host.docker.internal 102 | --listen=0.0.0.0:9735 103 | --rpclisten=0.0.0.0:10009 104 | --bitcoin.active 105 | --bitcoin.regtest 106 | --bitcoin.node=bitcoind 107 | --bitcoind.rpchost=bitcoind 108 | --bitcoind.rpcuser=polaruser 109 | --bitcoind.rpcpass=polarpass 110 | --bitcoind.zmqpubrawblock=tcp://bitcoind:28334 111 | --bitcoind.zmqpubrawtx=tcp://bitcoind:28335 112 | restart: always 113 | volumes: 114 | - ${HOST_PROJECT_PATH:-.}/data/lnd-wallet:/root/.lnd 115 | expose: 116 | - "10009" 117 | ports: 118 | - "12001:10009" 119 | profiles: 120 | - itest 121 | 122 | nutshell: 123 | image: cashubtc/nutshell:0.16.3 124 | container_name: nutshell 125 | ports: 126 | - "2228:3338" 127 | environment: 128 | - MINT_DERIVATION_PATH_LIST=["m/0'/0'/0'", "m/0'/0'/1'", "m/0'/1'/0'", "m/0'/2'/0'"] 129 | - MINT_BACKEND_BOLT11_SAT=FakeWallet 130 | - MINT_BACKEND_BOLT11_USD=FakeWallet 131 | - MINT_LISTEN_HOST=0.0.0.0 132 | - MINT_LISTEN_PORT=3338 133 | - MINT_PRIVATE_KEY=TEST_PRIVATE_KEY 134 | - MINT_INFO_NAME=nutshell 135 | command: ["poetry", "run", "mint"] 136 | profiles: 137 | - itest 138 | 139 | prometheus: 140 | image: "prom/prometheus:v2.45.0" 141 | command: 142 | - "--config.file=/etc/prometheus.yaml" 143 | - "--enable-feature=exemplar-storage" 144 | - "--web.enable-remote-write-receiver" 145 | volumes: 146 | - "./conf/prometheus.yaml:/etc/prometheus.yaml" 147 | ports: 148 | - "127.0.0.1:9090:9090" 149 | profiles: 150 | - tracing 151 | 152 | tempo: 153 | image: "grafana/tempo:2.4.0" 154 | command: 155 | - "-config.file=/etc/tempo.yaml" 156 | volumes: 157 | - "./conf/tempo.yaml:/etc/tempo.yaml" 158 | ports: 159 | - "127.0.0.1:3200:3200" # Tempo 160 | - "127.0.0.1:4317:4317" # OTLP GRPC 161 | - "127.0.0.1:4318:4318" # OTLP HTTP 162 | profiles: 163 | - tracing 164 | 165 | grafana: 166 | image: "grafana/grafana:10.2.4" 167 | volumes: 168 | - "./conf/grafana-datasources.yaml:/etc/grafana/provisioning/datasources/datasources.yaml" 169 | environment: 170 | - "GF_AUTH_ANONYMOUS_ENABLED=true" 171 | - "GF_AUTH_ANONYMOUS_ORG_ROLE=Admin" 172 | - "GF_AUTH_DISABLE_LOGIN_FORM=true" 173 | - "GF_FEATURE_TOGGLES_ENABLE=traceqlEditor" 174 | ports: 175 | - "127.0.0.1:3000:3000" 176 | profiles: 177 | - tracing 178 | 179 | app: 180 | #image: "docker.io/ngutech21/moksha-mint:latest" 181 | image: "moksha-mint:latest" # for local testing 182 | container_name: moksha-mint 183 | ports: 184 | - 3338:3338 185 | environment: 186 | - MINT_DB_URL=postgres://postgres:postgres@moksha-mint-db/moksha-mint 187 | - MINT_LIGHTNING_BACKEND=Lnd 188 | - MINT_PRIVATE_KEY=supersecretkey 189 | depends_on: 190 | - database 191 | profiles: 192 | - app 193 | -------------------------------------------------------------------------------- /entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | if [ -z "$MINT_LND_MACAROON_BASE64" ] || [ -z "$MINT_LND_TLS_CERT_BASE64" ]; then 4 | echo "Warning: MINT_LND_MACAROON_BASE64 and MINT_LND_TLS_CERT_BASE64 not set" >&2 5 | exec "$@" 6 | exit 0 7 | fi 8 | 9 | # Decode the base64 environment variables and write them to files 10 | mkdir -p /tmp/lndconf 11 | echo "$MINT_LND_MACAROON_BASE64" | base64 -d > /tmp/lndconf/admin.macaroon 12 | if [ $? -ne 0 ]; then 13 | echo "MINT_LND_MACAROON_BASE64 is not valid base64" 14 | exit 1 15 | fi 16 | 17 | echo "$MINT_LND_TLS_CERT_BASE64" | base64 -d > /tmp/lndconf/tls.cert 18 | if [ $? -ne 0 ]; then 19 | echo "MINT_LND_TLS_CERT_BASE64 is not valid base64" 20 | exit 1 21 | fi 22 | 23 | # Restrict permissions of the files 24 | chmod 700 /tmp/lndconf 25 | chmod 400 /tmp/lndconf/admin.macaroon 26 | chmod 400 /tmp/lndconf/tls.cert 27 | 28 | # Start your application 29 | exec "$@" -------------------------------------------------------------------------------- /flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "flake-utils": { 4 | "inputs": { 5 | "systems": "systems" 6 | }, 7 | "locked": { 8 | "lastModified": 1705309234, 9 | "narHash": "sha256-uNRRNRKmJyCRC/8y1RqBkqWBLM034y4qN7EprSdmgyA=", 10 | "owner": "numtide", 11 | "repo": "flake-utils", 12 | "rev": "1ef2e671c3b0c19053962c07dbda38332dcebf26", 13 | "type": "github" 14 | }, 15 | "original": { 16 | "owner": "numtide", 17 | "repo": "flake-utils", 18 | "type": "github" 19 | } 20 | }, 21 | "flake-utils_2": { 22 | "inputs": { 23 | "systems": "systems_2" 24 | }, 25 | "locked": { 26 | "lastModified": 1705309234, 27 | "narHash": "sha256-uNRRNRKmJyCRC/8y1RqBkqWBLM034y4qN7EprSdmgyA=", 28 | "owner": "numtide", 29 | "repo": "flake-utils", 30 | "rev": "1ef2e671c3b0c19053962c07dbda38332dcebf26", 31 | "type": "github" 32 | }, 33 | "original": { 34 | "owner": "numtide", 35 | "repo": "flake-utils", 36 | "type": "github" 37 | } 38 | }, 39 | "nixpkgs": { 40 | "locked": { 41 | "lastModified": 1706732774, 42 | "narHash": "sha256-hqJlyJk4MRpcItGYMF+3uHe8HvxNETWvlGtLuVpqLU0=", 43 | "owner": "NixOS", 44 | "repo": "nixpkgs", 45 | "rev": "b8b232ae7b8b144397fdb12d20f592e5e7c1a64d", 46 | "type": "github" 47 | }, 48 | "original": { 49 | "id": "nixpkgs", 50 | "ref": "nixos-unstable", 51 | "type": "indirect" 52 | } 53 | }, 54 | "root": { 55 | "inputs": { 56 | "flake-utils": "flake-utils", 57 | "nixpkgs": "nixpkgs", 58 | "rust-overlay": "rust-overlay" 59 | } 60 | }, 61 | "rust-overlay": { 62 | "inputs": { 63 | "flake-utils": "flake-utils_2", 64 | "nixpkgs": [ 65 | "nixpkgs" 66 | ] 67 | }, 68 | "locked": { 69 | "lastModified": 1706926144, 70 | "narHash": "sha256-2ausVOGPuTIHQtodO36jguE5giMl5aT6NNuG91PiAYE=", 71 | "owner": "oxalica", 72 | "repo": "rust-overlay", 73 | "rev": "130b1b89799ae76c0ad1bf4b9019470403d9fbe1", 74 | "type": "github" 75 | }, 76 | "original": { 77 | "owner": "oxalica", 78 | "repo": "rust-overlay", 79 | "type": "github" 80 | } 81 | }, 82 | "systems": { 83 | "locked": { 84 | "lastModified": 1681028828, 85 | "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", 86 | "owner": "nix-systems", 87 | "repo": "default", 88 | "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", 89 | "type": "github" 90 | }, 91 | "original": { 92 | "owner": "nix-systems", 93 | "repo": "default", 94 | "type": "github" 95 | } 96 | }, 97 | "systems_2": { 98 | "locked": { 99 | "lastModified": 1681028828, 100 | "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", 101 | "owner": "nix-systems", 102 | "repo": "default", 103 | "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", 104 | "type": "github" 105 | }, 106 | "original": { 107 | "owner": "nix-systems", 108 | "repo": "default", 109 | "type": "github" 110 | } 111 | } 112 | }, 113 | "root": "root", 114 | "version": 7 115 | } 116 | -------------------------------------------------------------------------------- /flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | description = "Moksha flake"; 3 | 4 | inputs = { 5 | flake-utils.url = "github:numtide/flake-utils"; 6 | rust-overlay = { 7 | url = "github:oxalica/rust-overlay"; 8 | inputs.nixpkgs.follows = "nixpkgs"; 9 | }; 10 | nixpkgs.url = "nixpkgs/nixos-unstable"; 11 | }; 12 | 13 | outputs = { self, nixpkgs, flake-utils, rust-overlay }: 14 | flake-utils.lib.eachDefaultSystem (system: 15 | let 16 | overlays = [ rust-overlay.overlays.default ]; 17 | pkgs = import nixpkgs { inherit system overlays; }; 18 | rust = pkgs.rust-bin.fromRustupToolchainFile ./rust-toolchain.toml; 19 | inputs = [ 20 | rust 21 | pkgs.rust-analyzer 22 | pkgs.typos 23 | pkgs.sqlx-cli 24 | pkgs.grcov 25 | pkgs.protobuf 26 | pkgs.openssl 27 | pkgs.zlib 28 | pkgs.gcc 29 | pkgs.pkg-config 30 | pkgs.just 31 | pkgs.wasm-pack 32 | pkgs.wasm-bindgen-cli 33 | pkgs.binaryen 34 | pkgs.clang 35 | ]; 36 | in { 37 | defaultPackage = pkgs.rustPlatform.buildRustPackage { 38 | name = "moksha"; 39 | src = ./.; 40 | 41 | cargoLock = { 42 | lockFile = ./Cargo.lock; 43 | }; 44 | 45 | nativeBuildInputs = inputs; 46 | }; 47 | 48 | 49 | devShell = pkgs.mkShell { 50 | packages = inputs; 51 | shellHook = '' 52 | export LIBCLANG_PATH=${pkgs.libclang.lib}/lib/ 53 | export LD_LIBRARY_PATH=${pkgs.openssl}/lib:$LD_LIBRARY_PATH 54 | export CC_wasm32_unknown_unknown=${pkgs.llvmPackages_14.clang-unwrapped}/bin/clang-14 55 | export CFLAGS_wasm32_unknown_unknown="-I ${pkgs.llvmPackages_14.libclang.lib}/lib/clang/14.0.6/include/" 56 | ''; 57 | }; 58 | } 59 | ); 60 | } 61 | -------------------------------------------------------------------------------- /integrationtests/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "integrationtests" 3 | version = "0.2.1" 4 | edition = "2021" 5 | repository = "https://github.com/ngutech21/moksha" 6 | license = "MIT" 7 | description = "Integrationtests for moksha" 8 | publish = false 9 | 10 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 11 | 12 | [lib] 13 | name = "itests" 14 | path = "src/lib.rs" 15 | 16 | [dependencies] 17 | rand = { workspace = true } 18 | axum = { workspace = true } 19 | anyhow = { workspace = true, features = ["backtrace"] } 20 | serde = { workspace = true, features = ["derive"] } 21 | lightning-invoice = "0.31.0" 22 | bitcoin = { version = "0.30.2", default-features = false } 23 | secp256k1 = { version = "0.27.0", features = ["recovery", "alloc", "rand"] } 24 | testcontainers = { workspace = true } 25 | testcontainers-modules = { workspace = true, features = ["postgres"] } 26 | bitcoincore-rpc = { workspace = true } 27 | fedimint-tonic-lnd = { workspace = true, features = ["lightningrpc", "walletrpc"] } 28 | url = { workspace = true } 29 | moksha-mint = { path = "../moksha-mint" } 30 | hex = { workspace = true } 31 | reqwest = { workspace = true, features = ["json", "rustls-tls"] } 32 | 33 | [target.'cfg(not(target_family="wasm"))'.dependencies] 34 | tokio = { version = "1.38.0", features = ["sync", "rt-multi-thread"] } 35 | 36 | [dev-dependencies] 37 | anyhow = { workspace = true, features = ["backtrace"] } 38 | tokio = { workspace = true, features = ["full"] } 39 | reqwest = { workspace = true, features = ["json", "rustls-tls"] } 40 | moksha-mint = { path = "../moksha-mint" } 41 | moksha-wallet = { path = "../moksha-wallet" } 42 | moksha-core = { path = "../moksha-core" } 43 | tempfile = { workspace = true } 44 | assert_cmd = { workspace = true } 45 | -------------------------------------------------------------------------------- /integrationtests/README.md: -------------------------------------------------------------------------------- 1 | # Integrationtests 2 | 3 | This crate contains integration tests for moksha. 4 | 5 | ## Prerequisites 6 | 7 | Before running the tests, ensure that you have the following software installed on your machine: 8 | 9 | - Docker: Our tests use Docker to create isolated, reproducible environments. You can download Docker from the [official website](https://www.docker.com/products/docker-desktop). 10 | - Docker Compose: This is a tool for defining and running multi-container Docker applications. It's included in the Docker Desktop installation for Windows and Mac. For Linux, you can follow the instructions on the [official documentation](https://docs.docker.com/compose/install/). 11 | 12 | ## Running the Tests 13 | 14 | To run the integration tests, use the `itests` command in your terminal. This command will start the required services using Docker Compose and run the tests. 15 | 16 | ```bash 17 | just run-itests 18 | ``` 19 | 20 | Please note that the first time you run the tests, Docker may need to download the required images. This can take some time, but the images will be cached for future runs. 21 | -------------------------------------------------------------------------------- /integrationtests/src/bitcoin_client.rs: -------------------------------------------------------------------------------- 1 | use bitcoincore_rpc::{ 2 | bitcoin::{Address, Amount}, 3 | json::AddressType, 4 | Auth, Client, RpcApi, 5 | }; 6 | use std::{str::FromStr, time::Duration}; 7 | 8 | pub struct BitcoinClient { 9 | pub client: Client, 10 | } 11 | 12 | impl BitcoinClient { 13 | pub async fn new_local() -> anyhow::Result { 14 | let client = Client::new( 15 | "http://localhost:18453/", 16 | Auth::UserPass("polaruser".to_string(), "polarpass".to_string()), 17 | )?; 18 | 19 | let wallets = client.list_wallets()?; 20 | if wallets.is_empty() { 21 | Self::create_wallet_autoload().await?; 22 | } 23 | Ok(Self { client }) 24 | } 25 | 26 | // creates a wallet and loads it on startup 27 | pub async fn create_wallet_autoload() -> anyhow::Result<()> { 28 | let _ = reqwest::Client::new() 29 | .post("http://localhost:18453/") 30 | .basic_auth("polaruser", Some("polarpass")) 31 | .body( 32 | r#"{"jsonrpc": "1.0", "method": "createwallet", "params": ["testwallet", null, null, null, null, null, true]}"# 33 | .to_string(), 34 | ) 35 | .send() 36 | .await?; 37 | Ok(()) 38 | } 39 | 40 | pub fn get_block_height(&self) -> anyhow::Result { 41 | Ok(self.client.get_block_count()?) 42 | } 43 | 44 | pub fn get_new_address(&self) -> anyhow::Result { 45 | let new_address = self 46 | .client 47 | .get_new_address(None, Some(AddressType::Bech32))?; 48 | Ok(new_address.assume_checked().to_string()) 49 | } 50 | 51 | pub async fn mine_blocks(&self, block_num: u64) -> anyhow::Result<()> { 52 | let new_adr = self.get_new_address()?; 53 | self.generate_to_address(block_num, &new_adr).await?; 54 | Ok(()) 55 | } 56 | 57 | pub async fn generate_to_address(&self, block_num: u64, address: &str) -> anyhow::Result<()> { 58 | let adr = Address::from_str(address)?; 59 | let adr = adr.require_network(bitcoincore_rpc::bitcoin::Network::Regtest)?; 60 | let _old_block_height = self.client.get_block_count()?; 61 | let _hashes = self.client.generate_to_address(block_num, &adr)?; 62 | tokio::time::sleep(Duration::from_secs(5)).await; 63 | Ok(()) 64 | } 65 | 66 | pub async fn send_to_address(&self, address: &str, amount: Amount) -> anyhow::Result<()> { 67 | let adr = Address::from_str(address)?; 68 | let adr = adr.require_network(bitcoincore_rpc::bitcoin::Network::Regtest)?; 69 | self.client.send_to_address( 70 | &adr, 71 | amount, 72 | None, 73 | None, 74 | Some(false), 75 | Some(false), 76 | None, 77 | None, 78 | )?; 79 | 80 | self.mine_blocks(10).await?; 81 | Ok(()) 82 | } 83 | 84 | pub fn get_balance(&self) -> anyhow::Result { 85 | Ok(self.client.get_balance(None, None)?) 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /integrationtests/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod bitcoin_client; 2 | pub mod lnbitsmock; 3 | pub mod lnd_client; 4 | pub mod setup; 5 | -------------------------------------------------------------------------------- /integrationtests/src/lnbitsmock.rs: -------------------------------------------------------------------------------- 1 | use axum::extract::State; 2 | use axum::http::StatusCode; 3 | use axum::Json; 4 | use axum::{response::IntoResponse, routing::get, routing::post, Router}; 5 | use bitcoin::hashes::sha256; 6 | use lightning_invoice::{Currency, InvoiceBuilder, PaymentSecret}; 7 | use secp256k1::Secp256k1; 8 | use secp256k1::SecretKey; 9 | use serde::{Deserialize, Serialize}; 10 | use std::net::SocketAddr; 11 | 12 | use std::str::FromStr; 13 | 14 | #[derive(Default, Debug, Deserialize, Serialize)] 15 | struct CreateInvoiceRequest { 16 | out: bool, 17 | amount: Option, 18 | bolt11: Option, 19 | memo: Option, 20 | expiry: Option, 21 | unit: Option, 22 | webhook: Option, 23 | internal: Option, 24 | } 25 | 26 | #[derive(Default, Debug, Deserialize, Serialize)] 27 | struct CreateInvoiceResponse { 28 | payment_hash: String, 29 | payment_request: Option, 30 | checking_id: Option, 31 | lnurl_response: Option, 32 | } 33 | 34 | #[derive(Debug, Deserialize, Serialize)] 35 | struct PaymentStatus { 36 | paid: bool, 37 | } 38 | 39 | async fn post_invoice( 40 | State(private_key): State, 41 | params: axum::Json, 42 | ) -> Result { 43 | if !params.out { 44 | let payment_secret = PaymentSecret([42u8; 32]); 45 | let invoice = InvoiceBuilder::new(Currency::Regtest) 46 | .description(params.memo.clone().unwrap_or("".to_string())) 47 | .amount_milli_satoshis(params.amount.expect("amount is not set")) 48 | .payment_hash( 49 | sha256::Hash::from_str( 50 | "0001020304050607080900010203040506070809000102030405060708090102", 51 | ) 52 | .unwrap(), 53 | ) 54 | .payment_secret(payment_secret) 55 | .current_timestamp() 56 | .min_final_cltv_expiry_delta(144) 57 | .build_signed(|hash| Secp256k1::new().sign_ecdsa_recoverable(hash, &private_key)) 58 | .expect("Can't create invoice"); 59 | 60 | let payment_hash = invoice.payment_hash().to_string(); 61 | let payment_request = invoice.to_string(); 62 | let response = CreateInvoiceResponse { 63 | payment_hash, 64 | payment_request: Some(payment_request), 65 | checking_id: Some( 66 | "caf224bb1dc543a3da2783c431d096428b7ea35807361a92868bdd0ac6de0f22".to_owned(), 67 | ), 68 | ..Default::default() 69 | }; 70 | Ok(Json(response)) 71 | } else { 72 | let payment_hash = "1234567890abcdef".to_string(); 73 | let response = CreateInvoiceResponse { 74 | payment_hash, 75 | ..Default::default() 76 | }; 77 | Ok(Json(response)) 78 | } 79 | } 80 | 81 | async fn get_payment( 82 | _payment_hash: axum::extract::Path, 83 | ) -> Result { 84 | Ok(Json(PaymentStatus { paid: true })) 85 | } 86 | 87 | pub async fn run_server(port: u16) -> anyhow::Result<()> { 88 | let private_key = SecretKey::new(&mut rand::thread_rng()); 89 | let app = Router::new() 90 | .route("/api/v1/payments/:payment_hash", get(get_payment)) 91 | .route("/api/v1/payments", post(post_invoice)) 92 | .with_state(private_key); 93 | 94 | let listener = tokio::net::TcpListener::bind(&SocketAddr::from(([127, 0, 0, 1], port))).await?; 95 | axum::serve(listener, app.into_make_service()).await?; 96 | 97 | Ok(()) 98 | } 99 | -------------------------------------------------------------------------------- /integrationtests/src/setup.rs: -------------------------------------------------------------------------------- 1 | use std::time::Duration; 2 | 3 | use crate::{bitcoin_client::BitcoinClient, lnd_client::LndClient}; 4 | use mokshamint::{ 5 | config::{BtcOnchainConfig, DatabaseConfig, ServerConfig}, 6 | lightning::LightningType, 7 | mint::MintBuilder, 8 | }; 9 | 10 | pub async fn fund_mint_lnd(amount: u64) -> anyhow::Result<()> { 11 | let btc_client = BitcoinClient::new_local().await?; 12 | btc_client.mine_blocks(108).await?; 13 | 14 | let lnd_wallet = LndClient::new_wallet_lnd().await?; 15 | lnd_wallet.wait_for_node_sync().await?; 16 | 17 | let lnd_mint = LndClient::new_mint_lnd().await?; 18 | lnd_mint.wait_for_node_sync().await?; 19 | 20 | let lnd_address = lnd_mint.new_address().await?; 21 | btc_client 22 | .send_to_address( 23 | &lnd_address, 24 | bitcoincore_rpc::bitcoin::Amount::from_sat(amount), 25 | ) 26 | .await?; 27 | tokio::time::sleep(Duration::from_millis(3_000)).await; 28 | Ok(()) 29 | } 30 | 31 | pub async fn open_channel_with_wallet(amount: u64) -> anyhow::Result<()> { 32 | let wallet_lnd = LndClient::new_wallet_lnd().await?; 33 | let wallet_pubkey = wallet_lnd.get_pubkey().await?; 34 | 35 | let mint_lnd = LndClient::new_mint_lnd().await?; 36 | mint_lnd 37 | .connect_to_peer(&wallet_pubkey, "lnd-wallet:9735") 38 | .await?; 39 | let mine_blocks = mint_lnd.open_channel(&wallet_pubkey, amount).await?; 40 | if mine_blocks { 41 | let btc_client = BitcoinClient::new_local().await?; 42 | btc_client.mine_blocks(5).await?; 43 | } 44 | Ok(()) 45 | } 46 | 47 | pub async fn start_mint( 48 | host_port: u16, 49 | ln: LightningType, 50 | btc_onchain: Option, 51 | ) -> anyhow::Result<()> { 52 | let db_config = DatabaseConfig { 53 | db_url: format!( 54 | "postgres://postgres:postgres@localhost:{}/postgres", 55 | host_port 56 | ), 57 | ..Default::default() 58 | }; 59 | 60 | let mint = MintBuilder::new() 61 | .with_private_key("my_private_key".to_string()) 62 | .with_server(Some(ServerConfig { 63 | host_port: "127.0.0.1:8686".parse()?, 64 | ..Default::default() 65 | })) 66 | .with_db(Some(db_config)) 67 | .with_lightning(ln) 68 | .with_btc_onchain(btc_onchain) 69 | .with_fee(Some((0.0, 0).into())) 70 | .build(); 71 | 72 | mokshamint::server::run_server(mint.await.expect("Can not connect to lightning backend")) 73 | .await?; 74 | Ok(()) 75 | } 76 | 77 | pub fn read_fixture(name: &str) -> anyhow::Result { 78 | let base_dir = std::env::var("CARGO_MANIFEST_DIR")?; 79 | let raw_token = std::fs::read_to_string(format!("{base_dir}/tests/fixtures/{name}"))?; 80 | Ok(raw_token.trim().to_string()) 81 | } 82 | -------------------------------------------------------------------------------- /integrationtests/tests/fixtures/invoice_1000.txt: -------------------------------------------------------------------------------- 1 | lnbcrt10u1pjfzuugpp5hmgp79w40upjw5l5n2x2ne4rrj7w2t6r0ksmyszadxk2wg0g7xhqdqqcqzzsxqyz5vqsp5x7zaf09wc8nvz50udasvmhx872xgh0a6g34sj0q6lea0c4emsufq9qyyssq4tzagx79z4yez48t6sg8df98g2rtqmtq2frdf7cedfltn4qpfwu3rrr7774skcxajzshg2vhfkl26cax4r0rcsqg8l33rd4yg7pz0ecqdjpp9r -------------------------------------------------------------------------------- /integrationtests/tests/fixtures/token_10.cashu: -------------------------------------------------------------------------------- 1 | cashuAeyJ0b2tlbiI6W3sibWludCI6Imh0dHA6Ly8xMjcuMC4wLjE6MzMzOC8iLCJwcm9vZnMiOlt7ImFtb3VudCI6Miwic2VjcmV0IjoiUjJvMmVWV2xXZU9VSDZodDd1SVRYSHpBIiwiQyI6IjAyZjdlNGNmODAyZmVjN2EzMDAyZGI4MjM2NjhmODYzMjFlNWUzYmU0ZDhiYjEwYjI2MGQ0YjBmNTUxMmRjYzE1ZSIsImlkIjoibVI5UEozTXpqTDF5In0seyJhbW91bnQiOjgsInNlY3JldCI6IjFCQnpUVTVaYWhWMER5clZtb042ZE9MQyIsIkMiOiIwMjg4ODY1YzU1OWUxYmNjNTUyNjczMDRiMmEwNzRlY2YxZDU4YmM0MWU3M2NjMGQwYjRmMzYyZTJmMjE3ZWViODUiLCJpZCI6Im1SOVBKM016akwxeSJ9XX1dfQ== -------------------------------------------------------------------------------- /integrationtests/tests/tests_lnbitsmock.rs: -------------------------------------------------------------------------------- 1 | use itests::setup::{read_fixture, start_mint}; 2 | use moksha_core::primitives::{CurrencyUnit, PaymentMethod}; 3 | 4 | use moksha_wallet::client::CashuClient; 5 | use moksha_wallet::http::CrossPlatformHttpClient; 6 | use moksha_wallet::localstore::sqlite::SqliteLocalStore; 7 | use moksha_wallet::wallet::WalletBuilder; 8 | 9 | use mokshamint::lightning::{lnbits::LnbitsLightningSettings, LightningType}; 10 | use reqwest::Url; 11 | use std::time::Duration; 12 | use testcontainers::runners::AsyncRunner; 13 | use testcontainers::ImageExt; 14 | use testcontainers_modules::postgres::Postgres; 15 | use tokio::time::{sleep_until, Instant}; 16 | 17 | #[tokio::test(flavor = "multi_thread", worker_threads = 1)] 18 | pub async fn test_bolt11_lnbitsmock() -> anyhow::Result<()> { 19 | // create postgres container that will be destroyed after the test is done 20 | 21 | let node = Postgres::default() 22 | .with_host_auth() 23 | .with_tag("16.6-alpine") 24 | .start() 25 | .await?; 26 | let host_port = node.get_host_port_ipv4(5432).await?; 27 | 28 | // start lnbits 29 | let _lnbits_thread = tokio::spawn(async { 30 | let _ = itests::lnbitsmock::run_server(6100).await; 31 | }); 32 | 33 | let _server_thread = tokio::spawn(async move { 34 | let ln = LightningType::Lnbits(LnbitsLightningSettings::new( 35 | "my_admin_key", 36 | "http://127.0.0.1:6100", 37 | )); 38 | 39 | start_mint(host_port, ln, None) 40 | .await 41 | .expect("Could not start mint server"); 42 | }); 43 | 44 | // Wait for the server to start 45 | tokio::time::sleep(Duration::from_millis(800)).await; 46 | 47 | let client = CrossPlatformHttpClient::new(); 48 | let mint_url = Url::parse("http://127.0.0.1:8686")?; 49 | let keys = client.get_keys(&mint_url).await; 50 | assert!(keys.is_ok()); 51 | 52 | let keysets = client.get_keysets(&mint_url).await; 53 | assert!(keysets.is_ok()); 54 | // create wallet 55 | let localstore = SqliteLocalStore::with_in_memory().await?; 56 | let wallet = WalletBuilder::default() 57 | .with_client(client) 58 | .with_localstore(localstore) 59 | .build() 60 | .await?; 61 | let wallet_keysets = wallet.add_mint_keysets(&mint_url).await?; 62 | let wallet_keyset = wallet_keysets.first().unwrap(); // FIXME 63 | 64 | // get initial balance 65 | let balance = wallet.get_balance().await?; 66 | assert_eq!(0, balance, "Initial balance should be 0"); 67 | 68 | // mint some tokens 69 | let mint_amount = 6_000; 70 | let mint_quote = wallet.create_quote_bolt11(&mint_url, mint_amount).await?; 71 | let hash = mint_quote.clone().quote; 72 | 73 | sleep_until(Instant::now() + Duration::from_millis(1_000)).await; 74 | let mint_result = wallet 75 | .mint_tokens( 76 | wallet_keyset, 77 | &PaymentMethod::Bolt11, 78 | mint_amount.into(), 79 | hash.clone(), 80 | ) 81 | .await?; 82 | assert_eq!(6_000, mint_result.total_amount()); 83 | 84 | let balance = wallet.get_balance().await?; 85 | assert_eq!(6_000, balance); 86 | 87 | // pay ln-invoice 88 | let invoice_1000 = read_fixture("invoice_1000.txt")?; 89 | let quote = wallet 90 | .get_melt_quote_bolt11(&mint_url, invoice_1000.clone(), CurrencyUnit::Sat) 91 | .await?; 92 | let result_pay_invoice = wallet 93 | .pay_invoice(wallet_keyset, "e, invoice_1000) 94 | .await; 95 | if result_pay_invoice.is_err() { 96 | println!("error in pay_invoice{:?}", result_pay_invoice); 97 | } 98 | assert!(result_pay_invoice.is_ok()); 99 | let balance = wallet.get_balance().await?; 100 | assert_eq!(5_000, balance); 101 | 102 | // receive 10 sats 103 | let token_10: moksha_core::token::TokenV3 = read_fixture("token_10.cashu")?.try_into()?; 104 | let result_receive = wallet.receive_tokens(wallet_keyset, &token_10).await; 105 | assert!(result_receive.is_ok()); 106 | let balance = wallet.get_balance().await?; 107 | assert_eq!(5_010, balance); 108 | 109 | // send 10 tokens 110 | let result_send = wallet.send_tokens(wallet_keyset, 10).await; 111 | assert!(result_send.is_ok()); 112 | assert_eq!(10, result_send.unwrap().total_amount()); 113 | let balance = wallet.get_balance().await?; 114 | assert_eq!(5_000, balance); 115 | 116 | // get info 117 | let info = wallet.get_mint_info(&mint_url).await?; 118 | assert!(!info.nuts.nut4.disabled); 119 | Ok(()) 120 | } 121 | -------------------------------------------------------------------------------- /integrationtests/tests/tests_moksha_cli.rs: -------------------------------------------------------------------------------- 1 | use std::time::Duration; 2 | 3 | use assert_cmd::Command; 4 | use itests::{ 5 | lnd_client, 6 | setup::{fund_mint_lnd, start_mint}, 7 | }; 8 | use mokshamint::lightning::{lnd::LndLightningSettings, LightningType}; 9 | 10 | use testcontainers::runners::AsyncRunner; 11 | use testcontainers::ImageExt; 12 | use testcontainers_modules::postgres::Postgres; 13 | 14 | #[tokio::test(flavor = "multi_thread", worker_threads = 1)] 15 | async fn test_cli() -> anyhow::Result<()> { 16 | let node = Postgres::default() 17 | .with_host_auth() 18 | .with_tag("16.6-alpine") 19 | .start() 20 | .await?; 21 | let host_port = node.get_host_port_ipv4(5432).await?; 22 | 23 | fund_mint_lnd(2_000_000).await?; 24 | 25 | // start mint server 26 | tokio::spawn(async move { 27 | let lnd_settings = LndLightningSettings::new( 28 | lnd_client::LND_MINT_ADDRESS.parse().expect("invalid url"), 29 | "../data/lnd-mint/tls.cert".into(), 30 | "../data/lnd-mint/data/chain/bitcoin/regtest/admin.macaroon".into(), 31 | ); 32 | 33 | let ln_type = LightningType::Lnd(lnd_settings.clone()); 34 | 35 | start_mint(host_port, ln_type, None) 36 | .await 37 | .expect("Could not start mint server"); 38 | }); 39 | 40 | // Wait for the server to start 41 | tokio::time::sleep(Duration::from_millis(800)).await; 42 | 43 | // compile the moksha-cli binary and run it 44 | let mut cmd = Command::cargo_bin("moksha-cli")?; 45 | cmd.arg("info"); 46 | let output = cmd.unwrap(); 47 | assert!(output.status.success()); 48 | Ok(()) 49 | } 50 | -------------------------------------------------------------------------------- /integrationtests/tests/tests_nutshell_compatibility.rs: -------------------------------------------------------------------------------- 1 | use itests::setup::read_fixture; 2 | use moksha_core::primitives::{CurrencyUnit, PaymentMethod}; 3 | use moksha_wallet::client::CashuClient; 4 | use moksha_wallet::http::CrossPlatformHttpClient; 5 | use moksha_wallet::localstore::sqlite::SqliteLocalStore; 6 | use moksha_wallet::wallet::WalletBuilder; 7 | use std::time::Duration; 8 | 9 | use reqwest::Url; 10 | 11 | use tokio::time::{sleep_until, Instant}; 12 | 13 | #[tokio::test(flavor = "multi_thread", worker_threads = 1)] 14 | pub async fn test_nutshell_compatibility() -> anyhow::Result<()> { 15 | let client = CrossPlatformHttpClient::new(); 16 | let mint_url = Url::parse("http://127.0.0.1:2228")?; 17 | let keys = client.get_keys(&mint_url).await; 18 | assert!(keys.is_ok()); 19 | 20 | let keysets = client.get_keysets(&mint_url).await; 21 | assert!(keysets.is_ok()); 22 | // create wallet 23 | let localstore = SqliteLocalStore::with_in_memory().await?; 24 | let wallet = WalletBuilder::default() 25 | .with_client(client) 26 | .with_localstore(localstore) 27 | .build() 28 | .await?; 29 | let wallet_keysets = wallet.add_mint_keysets(&mint_url).await?; 30 | let wallet_keyset = wallet_keysets.first().unwrap(); // FIXME 31 | 32 | // check if mint info is correct 33 | let mint_info = wallet.get_mint_info(&mint_url).await?; 34 | assert_eq!(Some("nutshell".to_owned()), mint_info.name); 35 | 36 | // get initial balance 37 | let balance = wallet.get_balance().await?; 38 | assert_eq!(0, balance, "Initial balance should be 0"); 39 | 40 | // mint some tokens 41 | let mint_amount = 6_000; 42 | let mint_quote = wallet.create_quote_bolt11(&mint_url, mint_amount).await?; 43 | let hash = mint_quote.clone().quote; 44 | 45 | sleep_until(Instant::now() + Duration::from_millis(1_000)).await; 46 | let mint_result = wallet 47 | .mint_tokens( 48 | wallet_keyset, 49 | &PaymentMethod::Bolt11, 50 | mint_amount.into(), 51 | hash.clone(), 52 | ) 53 | .await?; 54 | assert_eq!(6_000, mint_result.total_amount()); 55 | 56 | let balance = wallet.get_balance().await?; 57 | assert_eq!(6_000, balance); 58 | 59 | // pay ln-invoice (10_000 invoice + 10 sats fee_reserve / 9 sats get returned) 60 | let invoice_1000 = read_fixture("invoice_1000.txt")?; 61 | let quote = wallet 62 | .get_melt_quote_bolt11(&mint_url, invoice_1000.clone(), CurrencyUnit::Sat) 63 | .await?; 64 | assert_eq!(10, quote.fee_reserve); 65 | let result_pay_invoice = wallet 66 | .pay_invoice(wallet_keyset, "e, invoice_1000) 67 | .await; 68 | 69 | if result_pay_invoice.is_err() { 70 | println!("error in pay_invoice{:?}", result_pay_invoice); 71 | } 72 | assert!(result_pay_invoice.is_ok()); 73 | assert_eq!(9, result_pay_invoice?.1); 74 | let balance = wallet.get_balance().await?; 75 | assert_eq!(4_999, balance); 76 | Ok(()) 77 | } 78 | -------------------------------------------------------------------------------- /justfile: -------------------------------------------------------------------------------- 1 | DB_URL := "postgres://postgres:postgres@localhost/moksha-mint" 2 | 3 | # list all tasks 4 | default: 5 | @just --list 6 | 7 | # install all dependencies 8 | deps: 9 | cargo install sqlx-cli typos-cli grcov wasm-pack wasm-opt just 10 | 11 | 12 | # clean cargo 13 | clean: 14 | cargo clean 15 | 16 | 17 | # check code for typos 18 | [no-exit-message] 19 | typos: 20 | #!/usr/bin/env bash 21 | >&2 echo '💡 Valid new words can be added to `typos.toml`' 22 | typos 23 | 24 | 25 | # fix all typos 26 | [no-exit-message] 27 | typos-fix-all: 28 | #!/usr/bin/env bash 29 | >&2 echo '💡 Valid new words can be added to `typos.toml`' 30 | typos --write-changes 31 | 32 | 33 | # format code, check typos and run tests 34 | final-check: 35 | cargo fmt --all 36 | just typos 37 | RUST_BACKTRACE=1 cargo test --workspace --exclude integrationtests 38 | just run-itests 39 | just build-wasm 40 | 41 | 42 | # run coverage and create a report in html and lcov format 43 | run-coverage: 44 | just run-coverage-tests 45 | just run-coverage-report 46 | 47 | # runs all tests with coverage instrumentation 48 | run-coverage-tests: 49 | docker compose --profile itest up -d 50 | RUST_BACKTRACE=1 CARGO_INCREMENTAL=0 RUSTFLAGS='-Cinstrument-coverage' LLVM_PROFILE_FILE='cargo-test-%p-%m.profraw' cargo test -- --test-threads=1 51 | docker compose --profile itest down 52 | 53 | # creates a coverage report in html and lcov format 54 | run-coverage-report: 55 | #!/usr/bin/env bash 56 | mkdir -p target/coverage 57 | grcov . --binary-path ./target/debug/ -s . -t lcov,html --branch --ignore-not-existing --ignore "*cargo*" --ignore "./data/*" --ignore "*/examples/*" -o target/coverage/ 58 | find . -name '*.profraw' -exec rm -r {} \; 59 | >&2 echo '💡 Created the report in html-format target/coverage/html/index.html' 60 | 61 | 62 | # run the cashu-mint 63 | run-mint *ARGS: 64 | RUST_BACKTRACE=1 MINT_APP_ENV=dev cargo run --bin moksha-mint -- {{ARGS}} 65 | 66 | # run cli-wallet with the given args 67 | run-cli *ARGS: 68 | RUST_BACKTRACE=1 cargo run --bin moksha-cli -- --db-dir ./data/wallet {{ARGS}} 69 | 70 | 71 | # runs all tests 72 | run-tests: 73 | RUST_BACKTRACE=1 cargo test --workspace --exclude integrationtests 74 | 75 | 76 | # checks if docker and docker compose is installed and running 77 | _check-docker: 78 | #!/usr/bin/env bash 79 | if ! command -v docker &> /dev/null; then 80 | >&2 echo 'Error: Docker is not installed.'; 81 | exit 1; 82 | fi 83 | 84 | if ! command -v docker compose &> /dev/null; then 85 | >&2 echo 'Error: Docker Compose is not installed.' >&2; 86 | exit 1; 87 | fi 88 | 89 | if ! command docker info &> /dev/null; then 90 | >&2 echo 'Error: Docker is not running.'; 91 | exit 1; 92 | fi 93 | 94 | # starts bitcoind, nutshell, 2 lnd nodes via docker and runs the integration tests 95 | run-itests: _check-docker 96 | cargo build -p moksha-cli 97 | docker compose --profile itest up -d 98 | RUST_BACKTRACE=1 cargo test -p integrationtests -- --test-threads=1 99 | docker compose --profile itest down 100 | 101 | # build the mint docker-image 102 | build-docker: 103 | docker build --file Dockerfile.alpine --build-arg COMMITHASH=$(git rev-parse HEAD) --build-arg BUILDTIME=$(date -u '+%F-%T') -t moksha-mint:latest . 104 | 105 | 106 | # compile all rust crates, that are relevant for the client, to wasm 107 | build-wasm: 108 | cargo +nightly build -p moksha-core -p moksha-wallet \ 109 | --target wasm32-unknown-unknown \ 110 | -Z build-std=std,panic_abort 111 | 112 | 113 | # runs sqlx prepare 114 | db-prepare: 115 | cd moksha-mint && \ 116 | cargo sqlx prepare --database-url {{ DB_URL }} 117 | 118 | # runs sqlx prepare 119 | db-migrate: 120 | cd moksha-mint && \ 121 | cargo sqlx migrate run --database-url {{ DB_URL }} 122 | 123 | # creates the postgres database 124 | db-create: 125 | cd moksha-mint && \ 126 | cargo sqlx database create --database-url {{ DB_URL }} 127 | 128 | 129 | # publish everything on crates.io 130 | publish: 131 | cargo publish -p moksha-core 132 | cargo publish -p moksha-wallet 133 | cargo publish -p moksha-mint 134 | cargo publish -p moksha-cli 135 | 136 | 137 | -------------------------------------------------------------------------------- /k8s/moksha-mint/.helmignore: -------------------------------------------------------------------------------- 1 | # Patterns to ignore when building packages. 2 | # This supports shell glob matching, relative path matching, and 3 | # negation (prefixed with !). Only one pattern per line. 4 | .DS_Store 5 | # Common VCS dirs 6 | .git/ 7 | .gitignore 8 | .bzr/ 9 | .bzrignore 10 | .hg/ 11 | .hgignore 12 | .svn/ 13 | # Common backup files 14 | *.swp 15 | *.bak 16 | *.tmp 17 | *.orig 18 | *~ 19 | # Various IDEs 20 | .project 21 | .idea/ 22 | *.tmproj 23 | .vscode/ 24 | -------------------------------------------------------------------------------- /k8s/moksha-mint/Chart.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v2 2 | name: moksha-mint 3 | description: A Helm chart for moksha-mint 4 | 5 | # A chart can be either an 'application' or a 'library' chart. 6 | # 7 | # Application charts are a collection of templates that can be packaged into versioned archives 8 | # to be deployed. 9 | # 10 | # Library charts provide useful utilities or functions for the chart developer. They're included as 11 | # a dependency of application charts to inject those utilities and functions into the rendering 12 | # pipeline. Library charts do not define any templates and therefore cannot be deployed. 13 | type: application 14 | 15 | # This is the chart version. This version number should be incremented each time you make changes 16 | # to the chart and its templates, including the app version. 17 | # Versions are expected to follow Semantic Versioning (https://semver.org/) 18 | version: 0.1.0 19 | 20 | # This is the version number of the application being deployed. This version number should be 21 | # incremented each time you make changes to the application. Versions are not expected to 22 | # follow Semantic Versioning. They should reflect the version the application is using. 23 | # It is recommended to use it with quotes. 24 | appVersion: "0.2.0" 25 | -------------------------------------------------------------------------------- /k8s/moksha-mint/templates/NOTES.txt: -------------------------------------------------------------------------------- 1 | 1. Get the application URL by running these commands: 2 | {{- if .Values.ingress.enabled }} 3 | {{- range $host := .Values.ingress.hosts }} 4 | {{- range .paths }} 5 | http{{ if $.Values.ingress.tls }}s{{ end }}://{{ $host.host }}{{ .path }} 6 | {{- end }} 7 | {{- end }} 8 | {{- else if contains "NodePort" .Values.service.type }} 9 | export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "moksha-mint.fullname" . }}) 10 | export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}") 11 | echo http://$NODE_IP:$NODE_PORT 12 | {{- else if contains "LoadBalancer" .Values.service.type }} 13 | NOTE: It may take a few minutes for the LoadBalancer IP to be available. 14 | You can watch its status by running 'kubectl get --namespace {{ .Release.Namespace }} svc -w {{ include "moksha-mint.fullname" . }}' 15 | export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "moksha-mint.fullname" . }} --template "{{"{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}"}}") 16 | echo http://$SERVICE_IP:{{ .Values.service.port }} 17 | {{- else if contains "ClusterIP" .Values.service.type }} 18 | export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "moksha-mint.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}") 19 | export CONTAINER_PORT=$(kubectl get pod --namespace {{ .Release.Namespace }} $POD_NAME -o jsonpath="{.spec.containers[0].ports[0].containerPort}") 20 | echo "Visit http://127.0.0.1:8080 to use your application" 21 | kubectl --namespace {{ .Release.Namespace }} port-forward $POD_NAME 8080:$CONTAINER_PORT 22 | {{- end }} 23 | -------------------------------------------------------------------------------- /k8s/moksha-mint/templates/_helpers.tpl: -------------------------------------------------------------------------------- 1 | {{/* 2 | Expand the name of the chart. 3 | */}} 4 | {{- define "moksha-mint.name" -}} 5 | {{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} 6 | {{- end }} 7 | 8 | {{/* 9 | Create a default fully qualified app name. 10 | We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). 11 | If release name contains chart name it will be used as a full name. 12 | */}} 13 | {{- define "moksha-mint.fullname" -}} 14 | {{- if .Values.fullnameOverride }} 15 | {{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} 16 | {{- else }} 17 | {{- $name := default .Chart.Name .Values.nameOverride }} 18 | {{- if contains $name .Release.Name }} 19 | {{- .Release.Name | trunc 63 | trimSuffix "-" }} 20 | {{- else }} 21 | {{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} 22 | {{- end }} 23 | {{- end }} 24 | {{- end }} 25 | 26 | {{/* 27 | Create chart name and version as used by the chart label. 28 | */}} 29 | {{- define "moksha-mint.chart" -}} 30 | {{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} 31 | {{- end }} 32 | 33 | {{/* 34 | Common labels 35 | */}} 36 | {{- define "moksha-mint.labels" -}} 37 | helm.sh/chart: {{ include "moksha-mint.chart" . }} 38 | {{ include "moksha-mint.selectorLabels" . }} 39 | {{- if .Chart.AppVersion }} 40 | app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} 41 | {{- end }} 42 | app.kubernetes.io/managed-by: {{ .Release.Service }} 43 | {{- end }} 44 | 45 | {{/* 46 | Selector labels 47 | */}} 48 | {{- define "moksha-mint.selectorLabels" -}} 49 | app.kubernetes.io/name: {{ include "moksha-mint.name" . }} 50 | app.kubernetes.io/instance: {{ .Release.Name }} 51 | {{- end }} 52 | 53 | {{/* 54 | Create the name of the service account to use 55 | */}} 56 | {{- define "moksha-mint.serviceAccountName" -}} 57 | {{- if .Values.serviceAccount.create }} 58 | {{- default (include "moksha-mint.fullname" .) .Values.serviceAccount.name }} 59 | {{- else }} 60 | {{- default "default" .Values.serviceAccount.name }} 61 | {{- end }} 62 | {{- end }} 63 | -------------------------------------------------------------------------------- /k8s/moksha-mint/templates/deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: {{ include "moksha-mint.fullname" . }} 5 | labels: 6 | {{- include "moksha-mint.labels" . | nindent 4 }} 7 | spec: 8 | {{- if not .Values.autoscaling.enabled }} 9 | replicas: {{ .Values.replicaCount }} 10 | {{- end }} 11 | selector: 12 | matchLabels: 13 | {{- include "moksha-mint.selectorLabels" . | nindent 6 }} 14 | template: 15 | metadata: 16 | {{- with .Values.podAnnotations }} 17 | annotations: 18 | {{- toYaml . | nindent 8 }} 19 | {{- end }} 20 | labels: 21 | {{- include "moksha-mint.labels" . | nindent 8 }} 22 | {{- with .Values.podLabels }} 23 | {{- toYaml . | nindent 8 }} 24 | {{- end }} 25 | spec: 26 | {{- with .Values.imagePullSecrets }} 27 | imagePullSecrets: 28 | {{- toYaml . | nindent 8 }} 29 | {{- end }} 30 | serviceAccountName: {{ include "moksha-mint.serviceAccountName" . }} 31 | securityContext: 32 | {{- toYaml .Values.podSecurityContext | nindent 8 }} 33 | containers: 34 | - name: {{ .Chart.Name }} 35 | securityContext: 36 | {{- toYaml .Values.securityContext | nindent 12 }} 37 | image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" 38 | imagePullPolicy: {{ .Values.image.pullPolicy }} 39 | ports: 40 | - name: http 41 | containerPort: {{ .Values.service.port }} 42 | protocol: TCP 43 | env: 44 | - name: MINT_HOST_PORT 45 | value: "[::]:8080" 46 | {{- range .Values.mokshaMintEnv }} 47 | - name: {{ .name }} 48 | value: {{ .value | quote }} 49 | {{- end }} 50 | livenessProbe: 51 | {{- toYaml .Values.livenessProbe | nindent 12 }} 52 | readinessProbe: 53 | {{- toYaml .Values.readinessProbe | nindent 12 }} 54 | resources: 55 | {{- toYaml .Values.resources | nindent 12 }} 56 | {{- with .Values.nodeSelector }} 57 | nodeSelector: 58 | {{- toYaml . | nindent 8 }} 59 | {{- end }} 60 | {{- with .Values.affinity }} 61 | affinity: 62 | {{- toYaml . | nindent 8 }} 63 | {{- end }} 64 | {{- with .Values.tolerations }} 65 | tolerations: 66 | {{- toYaml . | nindent 8 }} 67 | {{- end }} 68 | -------------------------------------------------------------------------------- /k8s/moksha-mint/templates/hpa.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.autoscaling.enabled }} 2 | apiVersion: autoscaling/v2 3 | kind: HorizontalPodAutoscaler 4 | metadata: 5 | name: {{ include "moksha-mint.fullname" . }} 6 | labels: 7 | {{- include "moksha-mint.labels" . | nindent 4 }} 8 | spec: 9 | scaleTargetRef: 10 | apiVersion: apps/v1 11 | kind: Deployment 12 | name: {{ include "moksha-mint.fullname" . }} 13 | minReplicas: {{ .Values.autoscaling.minReplicas }} 14 | maxReplicas: {{ .Values.autoscaling.maxReplicas }} 15 | metrics: 16 | {{- if .Values.autoscaling.targetCPUUtilizationPercentage }} 17 | - type: Resource 18 | resource: 19 | name: cpu 20 | target: 21 | type: Utilization 22 | averageUtilization: {{ .Values.autoscaling.targetCPUUtilizationPercentage }} 23 | {{- end }} 24 | {{- if .Values.autoscaling.targetMemoryUtilizationPercentage }} 25 | - type: Resource 26 | resource: 27 | name: memory 28 | target: 29 | type: Utilization 30 | averageUtilization: {{ .Values.autoscaling.targetMemoryUtilizationPercentage }} 31 | {{- end }} 32 | {{- end }} 33 | -------------------------------------------------------------------------------- /k8s/moksha-mint/templates/ingress.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.ingress.enabled -}} 2 | {{- $fullName := include "moksha-mint.fullname" . -}} 3 | {{- $svcPort := .Values.service.port -}} 4 | {{- if and .Values.ingress.className (not (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion)) }} 5 | {{- if not (hasKey .Values.ingress.annotations "kubernetes.io/ingress.class") }} 6 | {{- $_ := set .Values.ingress.annotations "kubernetes.io/ingress.class" .Values.ingress.className}} 7 | {{- end }} 8 | {{- end }} 9 | {{- if semverCompare ">=1.19-0" .Capabilities.KubeVersion.GitVersion -}} 10 | apiVersion: networking.k8s.io/v1 11 | {{- else if semverCompare ">=1.14-0" .Capabilities.KubeVersion.GitVersion -}} 12 | apiVersion: networking.k8s.io/v1beta1 13 | {{- else -}} 14 | apiVersion: extensions/v1beta1 15 | {{- end }} 16 | kind: Ingress 17 | metadata: 18 | name: {{ $fullName }} 19 | labels: 20 | {{- include "moksha-mint.labels" . | nindent 4 }} 21 | {{- with .Values.ingress.annotations }} 22 | annotations: 23 | {{- toYaml . | nindent 4 }} 24 | {{- end }} 25 | spec: 26 | {{- if and .Values.ingress.className (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion) }} 27 | ingressClassName: {{ .Values.ingress.className }} 28 | {{- end }} 29 | {{- if .Values.ingress.tls }} 30 | tls: 31 | {{- range .Values.ingress.tls }} 32 | - hosts: 33 | {{- range .hosts }} 34 | - {{ . | quote }} 35 | {{- end }} 36 | secretName: {{ .secretName }} 37 | {{- end }} 38 | {{- end }} 39 | rules: 40 | {{- range .Values.ingress.hosts }} 41 | - host: {{ .host | quote }} 42 | http: 43 | paths: 44 | {{- range .paths }} 45 | - path: {{ .path }} 46 | {{- if and .pathType (semverCompare ">=1.18-0" $.Capabilities.KubeVersion.GitVersion) }} 47 | pathType: {{ .pathType }} 48 | {{- end }} 49 | backend: 50 | {{- if semverCompare ">=1.19-0" $.Capabilities.KubeVersion.GitVersion }} 51 | service: 52 | name: {{ $fullName }} 53 | port: 54 | number: {{ $svcPort }} 55 | {{- else }} 56 | serviceName: {{ $fullName }} 57 | servicePort: {{ $svcPort }} 58 | {{- end }} 59 | {{- end }} 60 | {{- end }} 61 | {{- end }} 62 | -------------------------------------------------------------------------------- /k8s/moksha-mint/templates/service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: {{ include "moksha-mint.fullname" . }} 5 | labels: {{- include "moksha-mint.labels" . | nindent 4 }} 6 | spec: 7 | type: {{ .Values.service.type }} 8 | ports: 9 | - port: {{ .Values.service.port }} 10 | targetPort: {{ .Values.service.targetPort }} 11 | protocol: TCP 12 | selector: {{- include "moksha-mint.selectorLabels" . | nindent 4 }} 13 | -------------------------------------------------------------------------------- /k8s/moksha-mint/templates/serviceaccount.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.serviceAccount.create -}} 2 | apiVersion: v1 3 | kind: ServiceAccount 4 | metadata: 5 | name: {{ include "moksha-mint.serviceAccountName" . }} 6 | labels: 7 | {{- include "moksha-mint.labels" . | nindent 4 }} 8 | {{- with .Values.serviceAccount.annotations }} 9 | annotations: 10 | {{- toYaml . | nindent 4 }} 11 | {{- end }} 12 | automountServiceAccountToken: {{ .Values.serviceAccount.automount }} 13 | {{- end }} 14 | -------------------------------------------------------------------------------- /k8s/moksha-mint/templates/tests/test-connection.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Pod 3 | metadata: 4 | name: "{{ include "moksha-mint.fullname" . }}-test-connection" 5 | labels: 6 | {{- include "moksha-mint.labels" . | nindent 4 }} 7 | annotations: 8 | "helm.sh/hook": test 9 | spec: 10 | containers: 11 | - name: wget 12 | image: busybox 13 | command: ['wget'] 14 | args: ['{{ include "moksha-mint.fullname" . }}:{{ .Values.service.port }}'] 15 | restartPolicy: Never 16 | -------------------------------------------------------------------------------- /k8s/moksha-mint/values.yaml: -------------------------------------------------------------------------------- 1 | # Default values for moksha-mint. 2 | # This is a YAML-formatted file. 3 | # Declare variables to be passed into your templates. 4 | 5 | replicaCount: 1 6 | 7 | image: 8 | repository: docker.io/ngutech21/moksha-mint 9 | pullPolicy: IfNotPresent 10 | # Overrides the image tag whose default is the chart appVersion. 11 | tag: "latest" 12 | 13 | imagePullSecrets: [] 14 | nameOverride: "" 15 | fullnameOverride: "" 16 | 17 | serviceAccount: 18 | # Specifies whether a service account should be created 19 | create: false 20 | # Automatically mount a ServiceAccount's API credentials? 21 | automount: true 22 | # Annotations to add to the service account 23 | annotations: {} 24 | # The name of the service account to use. 25 | # If not set and create is true, a name is generated using the fullname template 26 | name: "" 27 | 28 | podAnnotations: {} 29 | podLabels: {} 30 | 31 | podSecurityContext: 32 | {} 33 | # fsGroup: 2000 34 | 35 | securityContext: 36 | {} 37 | # capabilities: 38 | # drop: 39 | # - ALL 40 | # readOnlyRootFilesystem: true 41 | # runAsNonRoot: true 42 | # runAsUser: 1000 43 | 44 | service: 45 | type: ClusterIP 46 | port: 80 47 | targetPort: 8080 48 | ingress: 49 | enabled: false 50 | className: "" 51 | annotations: 52 | {} 53 | # kubernetes.io/ingress.class: nginx 54 | # kubernetes.io/tls-acme: "true" 55 | hosts: 56 | - host: moksha-mint.local 57 | paths: 58 | - path: / 59 | pathType: Prefix 60 | tls: [] 61 | # - secretName: chart-example-tls 62 | # hosts: 63 | # - chart-example.local 64 | 65 | resources: 66 | {} 67 | # We usually recommend not to specify default resources and to leave this as a conscious 68 | # choice for the user. This also increases chances charts run on environments with little 69 | # resources, such as Minikube. If you do want to specify resources, uncomment the following 70 | # lines, adjust them as necessary, and remove the curly braces after 'resources:'. 71 | # limits: 72 | # cpu: 100m 73 | # memory: 128Mi 74 | # requests: 75 | # cpu: 100m 76 | # memory: 128Mi 77 | 78 | livenessProbe: 79 | httpGet: 80 | path: /health 81 | port: 8080 82 | readinessProbe: 83 | httpGet: 84 | path: /health 85 | port: 8080 86 | 87 | autoscaling: 88 | enabled: false 89 | minReplicas: 1 90 | maxReplicas: 100 91 | targetCPUUtilizationPercentage: 80 92 | # targetMemoryUtilizationPercentage: 80 93 | 94 | 95 | nodeSelector: {} 96 | 97 | tolerations: [] 98 | 99 | affinity: {} 100 | 101 | 102 | # environment variables for moksha-mint. For running the mint you have to set the MINT_DB_URL and MINT_LIGHTNING_BACKEND. See .env.example for more documentation 103 | mokshaMintEnv: 104 | - name: RUST_LOG 105 | value: debug 106 | -------------------------------------------------------------------------------- /moksha-cli/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "moksha-cli" 3 | version = "0.2.1" 4 | edition = "2021" 5 | resolver = "2" 6 | repository = "https://github.com/ngutech21/moksha" 7 | license = "MIT" 8 | description = "cashu-cli wallet" 9 | 10 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 11 | 12 | [[bin]] 13 | name = "moksha-cli" 14 | path = "src/bin/moksha-cli.rs" 15 | 16 | [lib] 17 | name = "mokshacli" 18 | path = "src/lib.rs" 19 | 20 | [dependencies] 21 | moksha-wallet = { version = "0.2.1", path = "../moksha-wallet" } 22 | moksha-core = { version = "0.2.1", path = "../moksha-core" } 23 | console = { workspace = true } 24 | clap = { workspace = true, features = ["derive"] } 25 | tokio = { workspace = true, features = ["rt", "rt-multi-thread", "macros"] } 26 | url = { workspace = true } 27 | anyhow = { workspace = true, features = ["backtrace"] } 28 | dialoguer = { workspace = true } 29 | num-format = { workspace = true } 30 | qrcode = { workspace = true } 31 | indicatif = { workspace = true } 32 | -------------------------------------------------------------------------------- /moksha-cli/src/cli.rs: -------------------------------------------------------------------------------- 1 | use std::{process::exit, time::Duration}; 2 | 3 | use console::{style, Term}; 4 | use dialoguer::{theme::ColorfulTheme, Select}; 5 | use indicatif::{ProgressBar, ProgressStyle}; 6 | 7 | use moksha_core::primitives::CurrencyUnit; 8 | use moksha_wallet::{ 9 | error::MokshaWalletError, http::CrossPlatformHttpClient, localstore::sqlite::SqliteLocalStore, 10 | wallet::Wallet, 11 | }; 12 | use num_format::Locale; 13 | use num_format::ToFormattedString; 14 | use url::Url; 15 | 16 | pub fn progress_bar() -> anyhow::Result { 17 | let pb = ProgressBar::new_spinner(); 18 | pb.enable_steady_tick(Duration::from_millis(100)); 19 | pb.set_style(ProgressStyle::default_spinner().template("{spinner:.cyan} {msg}")?); 20 | Ok(pb) 21 | } 22 | 23 | pub async fn choose_mint( 24 | wallet: &Wallet, 25 | currency_unit: &CurrencyUnit, 26 | ) -> Result<(Url, u64), MokshaWalletError> { 27 | let mints = get_mints_with_balance(wallet, currency_unit).await?; 28 | 29 | if mints.is_empty() { 30 | println!("No mints found. Add a mint first with 'moksha-cli add-mint '"); 31 | exit(0) 32 | } 33 | 34 | if mints.len() == 1 { 35 | return Ok(mints[0].clone()); 36 | } 37 | 38 | let mints_display = mints 39 | .iter() 40 | .map(|(url, balance)| { 41 | format!( 42 | "{} - {} (sat)", 43 | url, 44 | balance.to_formatted_string(&Locale::en) 45 | ) 46 | }) 47 | .collect::>(); 48 | 49 | let selection = Select::with_theme(&ColorfulTheme::default()) 50 | .with_prompt("Choose a mint:") 51 | .default(0) 52 | .items(&mints_display[..]) 53 | .interact() 54 | .unwrap(); 55 | Ok(mints[selection].clone()) 56 | } 57 | 58 | pub async fn get_mints_with_balance( 59 | wallet: &Wallet, 60 | currency_unit: &CurrencyUnit, 61 | ) -> Result, MokshaWalletError> { 62 | let all_proofs = wallet.get_proofs().await?; 63 | 64 | let keysets = wallet.get_wallet_keysets().await?; 65 | if keysets.is_empty() { 66 | println!("No mints found. Add a mint first with 'moksha-cli add-mint '"); 67 | exit(0) 68 | } 69 | Ok(keysets 70 | .into_iter() 71 | .filter(|k| &k.currency_unit == currency_unit && k.active) 72 | .map(|k| { 73 | ( 74 | k.mint_url, 75 | all_proofs.proofs_by_keyset(&k.keyset_id).total_amount(), 76 | ) 77 | }) 78 | .collect::>()) 79 | } 80 | 81 | pub async fn show_total_balance( 82 | wallet: &Wallet, 83 | ) -> anyhow::Result<()> { 84 | let term = Term::stdout(); 85 | term.write_line(&format!( 86 | "New total balance {} (sat)", 87 | style(wallet.get_balance().await?.to_formatted_string(&Locale::en)).cyan() 88 | ))?; 89 | Ok(()) 90 | } 91 | -------------------------------------------------------------------------------- /moksha-cli/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod cli; 2 | -------------------------------------------------------------------------------- /moksha-core/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "moksha-core" 3 | version = "0.2.1" 4 | edition = "2021" 5 | repository = "https://github.com/ngutech21/moksha" 6 | license = "MIT" 7 | description = "A library for building cashu applications" 8 | 9 | [lib] 10 | name = "moksha_core" 11 | path = "src/lib.rs" 12 | 13 | [dependencies] 14 | anyhow = { workspace = true } 15 | url = { workspace = true } 16 | base64 = { workspace = true } 17 | bitcoin_hashes = "0.14.0" 18 | secp256k1 = { workspace = true, default-features = true, features = [ 19 | "rand", 20 | "serde", 21 | ] } 22 | serde = { workspace = true, features = ["derive"] } 23 | serde_json = { workspace = true } 24 | hex = { workspace = true } 25 | serde_with = { workspace = true } 26 | thiserror = { workspace = true } 27 | itertools = { workspace = true } 28 | uuid = { workspace = true, features = ["serde", "v4"] } 29 | utoipa = { workspace = true } 30 | 31 | [target.'cfg(target_family = "wasm")'.dependencies] 32 | # getrandom is transitive dependency of rand 33 | # on wasm, we need to enable the js backend 34 | # see https://docs.rs/getrandom/latest/getrandom/#indirect-dependencies and https://docs.rs/getrandom/latest/getrandom/#webassembly-support 35 | getrandom = { version = "0.2.14", features = ["js"] } 36 | 37 | [dev-dependencies] 38 | anyhow = { workspace = true } 39 | pretty_assertions = { workspace = true } 40 | criterion = "0.5.1" 41 | 42 | [[bench]] 43 | name = "dhke_benchmarks" 44 | harness = false 45 | -------------------------------------------------------------------------------- /moksha-core/README.md: -------------------------------------------------------------------------------- 1 | # moksha-core 2 | 3 | moksha-core is a Rust library for building Cashu apps. 4 | 5 | ## Getting Started 6 | 7 | To use Moksha Core in your Rust project, simply add it as a dependency in your `Cargo.toml` file: 8 | 9 | ```toml 10 | [dependencies] 11 | moksha-core = "0.2.1" 12 | ``` 13 | 14 | ## Benchmarks 15 | 16 | To run the DHKE benchmarks: 17 | 18 | ```sh 19 | cargo bench -p moksha-core 20 | ``` 21 | -------------------------------------------------------------------------------- /moksha-core/benches/dhke_benchmarks.rs: -------------------------------------------------------------------------------- 1 | use criterion::{criterion_group, criterion_main, Criterion}; 2 | use moksha_core::dhke::Dhke; 3 | use secp256k1::{Secp256k1, SecretKey}; 4 | 5 | fn bench_dhke(c: &mut Criterion) { 6 | let secp = Secp256k1::new(); 7 | let dhke = Dhke::new(); 8 | let secret_msg = "test_message"; 9 | let a = SecretKey::from_slice(&[1; 32]).unwrap(); 10 | let blinding_factor = SecretKey::from_slice(&[1; 32]).unwrap(); 11 | 12 | c.bench_function("hashToPoint", |b| { 13 | b.iter(|| Dhke::hash_to_curve(secret_msg.as_bytes()).unwrap()) 14 | }); 15 | 16 | c.bench_function("step1Alice", |b| { 17 | b.iter(|| { 18 | dhke.step1_alice(secret_msg, &blinding_factor.into()) 19 | .unwrap() 20 | }) 21 | }); 22 | 23 | let b_ = dhke 24 | .step1_alice(secret_msg, &blinding_factor.into()) 25 | .unwrap(); 26 | c.bench_function("step2Bob", |b| b.iter(|| dhke.step2_bob(b_, &a).unwrap())); 27 | 28 | let c_ = dhke.step2_bob(b_, &a).unwrap(); 29 | c.bench_function("step3Alice", |b| { 30 | b.iter(|| { 31 | dhke.step3_alice(c_, blinding_factor.into(), a.public_key(&secp)) 32 | .unwrap() 33 | }) 34 | }); 35 | 36 | let step3_c = dhke 37 | .step3_alice(c_, blinding_factor.into(), a.public_key(&secp)) 38 | .unwrap(); 39 | c.bench_function("verify", |b| { 40 | b.iter(|| dhke.verify(a, step3_c, secret_msg).unwrap()) 41 | }); 42 | 43 | c.bench_function("End-to-End BDHKE", |b| { 44 | b.iter(|| { 45 | let b_ = dhke 46 | .step1_alice(secret_msg, &blinding_factor.into()) 47 | .unwrap(); 48 | let c_ = dhke.step2_bob(b_, &a).unwrap(); 49 | let c = dhke 50 | .step3_alice(c_, blinding_factor.into(), a.public_key(&secp)) 51 | .unwrap(); 52 | dhke.verify(a, c, secret_msg).unwrap() 53 | }) 54 | }); 55 | } 56 | 57 | criterion_group!(benches, bench_dhke); 58 | criterion_main!(benches); 59 | -------------------------------------------------------------------------------- /moksha-core/src/amount.rs: -------------------------------------------------------------------------------- 1 | //! This module defines the `Amount` and `SplitAmount` structs, which are used for representing and splitting amounts in Cashu. 2 | //! 3 | //! The `Amount` struct represents an amount in satoshis, with a single `u64` field for the amount. The struct provides a `split` method that splits the amount into a `SplitAmount` struct. 4 | //! 5 | //! The `SplitAmount` struct represents a split amount, with a `Vec` field for the split amounts. The struct provides a `create_secrets` method that generates a vector of random strings for use as secrets in the split transaction. The struct also implements the `IntoIterator` trait, which allows it to be iterated over as a vector of `u64` values. 6 | //! 7 | //! Both the `Amount` and `SplitAmount` structs are serializable and deserializable using serde. 8 | 9 | #[derive(Debug, Clone)] 10 | pub struct Amount(pub u64); 11 | 12 | impl Amount { 13 | pub fn split(&self) -> SplitAmount { 14 | split_amount(self.0).into() 15 | } 16 | } 17 | 18 | #[derive(Debug, Clone)] 19 | pub struct SplitAmount(Vec); 20 | 21 | impl From> for SplitAmount { 22 | fn from(from: Vec) -> Self { 23 | Self(from) 24 | } 25 | } 26 | 27 | impl SplitAmount { 28 | pub fn len(&self) -> usize { 29 | self.0.len() 30 | } 31 | 32 | pub fn is_empty(&self) -> bool { 33 | self.0.is_empty() 34 | } 35 | } 36 | 37 | impl From for Amount { 38 | fn from(amount: u64) -> Self { 39 | Self(amount) 40 | } 41 | } 42 | 43 | impl IntoIterator for SplitAmount { 44 | type Item = u64; 45 | type IntoIter = std::vec::IntoIter; 46 | 47 | fn into_iter(self) -> Self::IntoIter { 48 | self.0.into_iter() 49 | } 50 | } 51 | 52 | /// split a decimal amount into a vector of powers of 2 53 | fn split_amount(amount: u64) -> Vec { 54 | format!("{amount:b}") 55 | .chars() 56 | .rev() 57 | .enumerate() 58 | .filter_map(|(i, c)| { 59 | if c == '1' { 60 | return Some(2_u64.pow(i as u32)); 61 | } 62 | None 63 | }) 64 | .collect::>() 65 | } 66 | 67 | #[cfg(test)] 68 | mod tests { 69 | use pretty_assertions::assert_eq; 70 | 71 | #[test] 72 | fn test_split_amount() -> anyhow::Result<()> { 73 | let bits = super::split_amount(13); 74 | assert_eq!(bits, vec![1, 4, 8]); 75 | 76 | let bits = super::split_amount(63); 77 | assert_eq!(bits, vec![1, 2, 4, 8, 16, 32]); 78 | 79 | let bits = super::split_amount(64); 80 | assert_eq!(bits, vec![64]); 81 | Ok(()) 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /moksha-core/src/blind.rs: -------------------------------------------------------------------------------- 1 | //! This module defines the `BlindedMessage` and `BlindedSignature` structs, which are used for representing blinded messages and signatures in Cashu as described in [Nut-00](https://github.com/cashubtc/nuts/blob/main/00.md) 2 | //! 3 | //! The `BlindedMessage` struct represents a blinded message, with an `amount` field for the amount in satoshis and a `b_` field for the public key of the blinding factor. 4 | //! 5 | //! The `BlindedSignature` struct represents a blinded signature, with an `amount` field for the amount in satoshis, a `c_` field for the public key of the blinding factor, and an optional `id` field for the ID of the signature. 6 | //! 7 | //! Both the `BlindedMessage` and `BlindedSignature` structs are serializable and deserializable using serde. 8 | //! 9 | //! The `TotalAmount` trait is also defined in this module, which provides a `total_amount` method for calculating the total amount of a vector of `BlindedMessage` or `BlindedSignature` structs. The trait is implemented for both `Vec` and `Vec`. 10 | 11 | use secp256k1::{PublicKey, SecretKey}; 12 | use serde::{Deserialize, Serialize}; 13 | use utoipa::ToSchema; 14 | 15 | use crate::error::MokshaCoreError; 16 | 17 | #[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] 18 | pub struct BlindedSignature { 19 | pub amount: u64, 20 | #[serde(rename = "C_")] 21 | #[schema(value_type=String)] 22 | pub c_: PublicKey, 23 | pub id: String, 24 | } 25 | 26 | #[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] 27 | pub struct BlindedMessage { 28 | pub amount: u64, 29 | #[serde(rename = "B_")] 30 | #[schema(value_type=String)] 31 | pub b_: PublicKey, 32 | // FIXME use KeysetId 33 | pub id: String, 34 | } 35 | 36 | #[derive(Debug, Clone)] 37 | pub struct BlindingFactor(SecretKey); 38 | 39 | impl From for BlindingFactor { 40 | fn from(sk: SecretKey) -> Self { 41 | BlindingFactor(sk) 42 | } 43 | } 44 | 45 | impl TryFrom<&str> for BlindingFactor { 46 | type Error = MokshaCoreError; 47 | 48 | fn try_from(hex: &str) -> Result { 49 | use std::str::FromStr; 50 | Ok(secp256k1::SecretKey::from_str(hex)?.into()) 51 | } 52 | } 53 | 54 | impl BlindingFactor { 55 | pub fn as_hex(&self) -> String { 56 | hex::encode(&self.0[..]) 57 | } 58 | 59 | pub fn to_secret_key(&self) -> SecretKey { 60 | self.0 61 | } 62 | } 63 | 64 | pub trait TotalAmount { 65 | fn total_amount(&self) -> u64; 66 | } 67 | 68 | impl TotalAmount for Vec { 69 | fn total_amount(&self) -> u64 { 70 | self.iter().fold(0, |acc, x| acc + x.amount) 71 | } 72 | } 73 | 74 | impl TotalAmount for Vec { 75 | fn total_amount(&self) -> u64 { 76 | self.iter().fold(0, |acc, x| acc + x.amount) 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /moksha-core/src/error.rs: -------------------------------------------------------------------------------- 1 | //! This module defines the `MokshaCoreError` enum, which represents the possible errors that can occur in the Moksha Core library. 2 | //! 3 | //! The `MokshaCoreError` enum is derived from the `Error` trait using the `thiserror` crate, which allows for easy definition of custom error types with automatic conversion to and from other error types. 4 | //! All of the variants in the `MokshaCoreError` enum implement the `Error` trait, which allows them to be used with the `?` operator for easy error propagation. The enum is also serializable and deserializable using serde. 5 | 6 | use base64::DecodeError; 7 | use thiserror::Error; 8 | 9 | #[derive(Error, Debug)] 10 | pub enum MokshaCoreError { 11 | #[error("Secp256k1Error {0}")] 12 | Secp256k1Error(#[from] secp256k1::Error), 13 | 14 | #[error("InvalidTokenType")] 15 | InvalidTokenPrefix, 16 | 17 | #[error("Base64DecodeError {0}")] 18 | Base64DecodeError(#[from] DecodeError), 19 | 20 | #[error("SerdeJsonError {0}")] 21 | SerdeJsonError(#[from] serde_json::Error), 22 | 23 | #[error("Invalid Keysetid")] 24 | InvalidKeysetid, 25 | 26 | #[error("Not enough tokens")] 27 | NotEnoughTokens, 28 | 29 | #[error("Invalid token")] 30 | InvalidToken, 31 | 32 | #[error("No valid point on curve secp256k1 found")] 33 | NoValidPointFound, 34 | 35 | #[error("Invalid hex string")] 36 | Hex(#[from] hex::FromHexError), 37 | 38 | #[error("Invalid Keyset-ID")] 39 | Slice(#[from] std::array::TryFromSliceError), 40 | } 41 | -------------------------------------------------------------------------------- /moksha-core/src/fixture.rs: -------------------------------------------------------------------------------- 1 | //! This module defines helper functions for loading fixtures in tests. 2 | //! 3 | //! The `read_fixture` function reads a fixture file from the `src/fixtures` directory relative to the Cargo manifest directory. The function takes a `name` argument that specifies the name of the fixture file to read. The function returns a `Result` containing the contents of the fixture file as a `String`. 4 | //! 5 | //! The `read_fixture_as` function is a generic function that reads a fixture file and deserializes its contents into a value of type `T`. The function takes a `name` argument that specifies the name of the fixture file to read, and a type parameter `T` that specifies the type to deserialize the fixture contents into. The function returns a `Result` containing the deserialized value. 6 | //! 7 | //! Both functions return an `anyhow::Result`, which allows for easy error handling using the `?` operator. The functions are intended to be used in tests to load fixture data for testing purposes. 8 | pub fn read_fixture(name: &str) -> anyhow::Result { 9 | let base_dir = std::env::var("CARGO_MANIFEST_DIR")?; 10 | let raw_token = std::fs::read_to_string(format!("{base_dir}/src/fixtures/{name}"))?; 11 | Ok(raw_token.trim().to_string()) 12 | } 13 | 14 | pub fn read_fixture_as(name: &str) -> anyhow::Result 15 | where 16 | T: serde::de::DeserializeOwned, 17 | { 18 | Ok(serde_json::from_str::(&read_fixture(name)?)?) 19 | } 20 | -------------------------------------------------------------------------------- /moksha-core/src/fixtures/incomplete_mint_info.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "My Cashu mint", 3 | "pubkey": "03a2118b421e6b47f0656b97bb7eeea43c41096adbc0d0e511ff70de7d94dbd990", 4 | "version": "MyMint", 5 | "description": "The short mint description", 6 | "description_long": "A long mint description that can be a long piece of text.", 7 | "contact": [ 8 | ["email", "contact@me.com"], 9 | ["twitter", "@me"], 10 | ["nostr", "npub..."] 11 | ], 12 | "motd": "Message to users", 13 | "nuts": { 14 | "4": { 15 | "methods": [ 16 | { 17 | "method": "bolt11", 18 | "unit": "sat", 19 | "min_amount": 1, 20 | "max_amount": 21 21 | } 22 | ], 23 | "disabled": false 24 | }, 25 | "5": { 26 | "methods": [ 27 | { 28 | "method": "bolt11", 29 | "unit": "sat", 30 | "min_amount": 1, 31 | "max_amount": 42 32 | } 33 | ], 34 | "disabled": false 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /moksha-core/src/fixtures/nutshell_mint_info.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "My Cashu mint", 3 | "pubkey": "03a2118b421e6b47f0656b97bb7eeea43c41096adbc0d0e511ff70de7d94dbd990", 4 | "version": "Nutshell/0.15.0", 5 | "description": "The short mint description", 6 | "description_long": "A long mint description that can be a long piece of text.", 7 | "contact": [ 8 | { 9 | "method": "email", 10 | "info": "contact@me.com" 11 | }, 12 | { 13 | "method": "twitter", 14 | "info": "@me" 15 | }, 16 | { 17 | "method": "nostr", 18 | "info": "npub..." 19 | } 20 | ], 21 | "motd": "Message to users", 22 | "nuts": { 23 | "4": { 24 | "methods": [ 25 | { 26 | "method": "bolt11", 27 | "unit": "sat", 28 | "min_amount": 1, 29 | "max_amount": 21 30 | } 31 | ], 32 | "disabled": false 33 | }, 34 | "5": { 35 | "methods": [ 36 | { 37 | "method": "bolt11", 38 | "unit": "sat", 39 | "min_amount": 1, 40 | "max_amount": 42 41 | } 42 | ], 43 | "disabled": false 44 | }, 45 | "7": { "supported": true }, 46 | "8": { "supported": true }, 47 | "9": { "supported": true }, 48 | "10": { "supported": true }, 49 | "11": { "supported": true }, 50 | "12": { "supported": true } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /moksha-core/src/fixtures/nutshell_mint_info_v0.16.0.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nutshell", 3 | "pubkey": "0296d0aa13b6a31cf0cd974249f28c7b7176d7274712c95a41c7d8066d3f29d679", 4 | "version": "Nutshell/0.16.0", 5 | "contact": [], 6 | "nuts": { 7 | "4": { 8 | "methods": [ 9 | { "method": "bolt11", "unit": "sat" }, 10 | { "method": "bolt11", "unit": "usd" } 11 | ], 12 | "disabled": false 13 | }, 14 | "5": { 15 | "methods": [ 16 | { "method": "bolt11", "unit": "sat" }, 17 | { "method": "bolt11", "unit": "usd" } 18 | ], 19 | "disabled": false 20 | }, 21 | "7": { "supported": true }, 22 | "8": { "supported": true }, 23 | "9": { "supported": true }, 24 | "10": { "supported": true }, 25 | "11": { "supported": true }, 26 | "12": { "supported": true }, 27 | "17": { 28 | "supported": [ 29 | { 30 | "method": "bolt11", 31 | "unit": "sat", 32 | "commands": ["bolt11_melt_quote", "proof_state", "bolt11_mint_quote"] 33 | }, 34 | { 35 | "method": "bolt11", 36 | "unit": "usd", 37 | "commands": ["bolt11_melt_quote", "proof_state", "bolt11_mint_quote"] 38 | } 39 | ] 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /moksha-core/src/fixtures/token_60.cashu: -------------------------------------------------------------------------------- 1 | cashuAeyJ0b2tlbiI6W3sibWludCI6Imh0dHA6Ly8xMjcuMC4wLjE6MzMzOCIsInByb29mcyI6W3siYW1vdW50Ijo0LCJzZWNyZXQiOiJzR3Z3OVZwalpqNGQ0YnFFU3FvQzdwTWEiLCJDIjoiMDM3YmQ2MGY2YWE1ZTE5ZjZhOWVjMzU5MjlkOGViN2E2Yzk1Y2YyOTM5NTlmMzMzNTQzYWQ5MWIxNTkyNWU2OTE1IiwiaWQiOiJtUjlQSjNNempMMXkifSx7ImFtb3VudCI6OCwic2VjcmV0IjoiQjJqNmw4Z1VUYjIxR0hqMFRnbUNRUjZHIiwiQyI6IjAyOTQzYmI0MWY4MmY3MGE2MWIwMzM0ZGU1YjJjZjNmYzc0YmI2ZTlhZTY5OWVlMzc4YjYyMzc3ZTVhMWJiZmM5ZCIsImlkIjoibVI5UEozTXpqTDF5In0seyJhbW91bnQiOjE2LCJzZWNyZXQiOiJ2SFRHbGJoRXFBQUdEUVBteFBkczc1MFkiLCJDIjoiMDI4NDU0OGJkN2FiNjhmNTIyNzdkOTQxYTgwN2JmZjJlZWI4ZjNmY2EzYmVlODY2ODgxN2RjYTg3MGJhOGQxYWJkIiwiaWQiOiJtUjlQSjNNempMMXkifSx7ImFtb3VudCI6MzIsInNlY3JldCI6IldSajZCTXVQNTQyTFpmWXdiTldlbTJLaCIsIkMiOiIwMzc5NWE0NGUwNGY1YWU5MGYyZGIwZTkzYzc3MzJkMDJkYTQ0ZGIxZmRkMWYzNDlkN2EwMzJmN2U5OGZkYzZjYzQiLCJpZCI6Im1SOVBKM016akwxeSJ9XX1dfQ== -------------------------------------------------------------------------------- /moksha-core/src/fixtures/token_invalid.cashu: -------------------------------------------------------------------------------- 1 | cashuAeyJ0b2tlbiI6W3sibWludCI6Imh0dHA6Ly8xMjcuMC4wLjE6MzMzOCIsInByb29mcyI6W3siYW1vdW50Ijo0LCJzZWNyZXQiOiJzR3Z3OVZwalpqNGQ0YnFFU3FvQzdwTWEiLCJDIjoiMDM3YmQ2MGY2YWE1ZTE5ZjZhOWVjMzU5MjlkOGViN2E2Yzk1Y2YyOTM5NTlmMzMzNTQzYWQ5MWIxNTkyNWU2OTE1IiwiaWQiOiJtUjlQSjNNempMMXkifSx7ImFtb3VudCI6OCwic2VjcmV0IjoiQjJqNmw4Z1VUYjIxR0hqMFRnbUNRUjZHIiwiQyI6IjAyOTQzYmI0MWY4MmY3MGE2MWIwMzM0ZGU1YjJjZjNmYzc0YmI2ZTlhZTY5OWVlMzc4YjYyMzc3ZTVhMWJiZmM5ZCIsImlkIjoibVI5UEozTXpqTDF5In0seyJhbW91bnQiOjE2LCJzZWNyZXQiOiJ2SFRHbGJoRXFBQUdEUVBteFBkczc1MFkiLCJDIjoiMDI4NDU0OGJkN2FiNjhmNTIyNzdkOTQxYTgwN2JmZjJlZWI4ZjNmY2EzYmVlODY2ODgxN2RjYTg3MGJhOGQxYWJkIiwiaWQiOiJtUjlQSjNNempMMXkifSx7ImFtb3VudCI6MzIsInNlY3JldCI6IldSajZCTXVQNTQyTFpmWXdiTldlbTJLaCIsIkMiOiIwMzc5NWE0NGUwNGY1YWU5MGYyZGIwZTkzYzc3MzJkMDJkYTQ0ZGIxZmRkMWYzNDlkN2EwMzJmN2U5OGZkYzZjYzQiLCJpZCI6Im1SOVBKM016akwxeSJ9XX1dfQVT -------------------------------------------------------------------------------- /moksha-core/src/fixtures/token_no_pad60.cashu: -------------------------------------------------------------------------------- 1 | cashuAeyJ0b2tlbiI6W3sibWludCI6Imh0dHA6Ly8xMjcuMC4wLjE6MzMzOCIsInByb29mcyI6W3siYW1vdW50Ijo0LCJzZWNyZXQiOiJzR3Z3OVZwalpqNGQ0YnFFU3FvQzdwTWEiLCJDIjoiMDM3YmQ2MGY2YWE1ZTE5ZjZhOWVjMzU5MjlkOGViN2E2Yzk1Y2YyOTM5NTlmMzMzNTQzYWQ5MWIxNTkyNWU2OTE1IiwiaWQiOiJtUjlQSjNNempMMXkifSx7ImFtb3VudCI6OCwic2VjcmV0IjoiQjJqNmw4Z1VUYjIxR0hqMFRnbUNRUjZHIiwiQyI6IjAyOTQzYmI0MWY4MmY3MGE2MWIwMzM0ZGU1YjJjZjNmYzc0YmI2ZTlhZTY5OWVlMzc4YjYyMzc3ZTVhMWJiZmM5ZCIsImlkIjoibVI5UEozTXpqTDF5In0seyJhbW91bnQiOjE2LCJzZWNyZXQiOiJ2SFRHbGJoRXFBQUdEUVBteFBkczc1MFkiLCJDIjoiMDI4NDU0OGJkN2FiNjhmNTIyNzdkOTQxYTgwN2JmZjJlZWI4ZjNmY2EzYmVlODY2ODgxN2RjYTg3MGJhOGQxYWJkIiwiaWQiOiJtUjlQSjNNempMMXkifSx7ImFtb3VudCI6MzIsInNlY3JldCI6IldSajZCTXVQNTQyTFpmWXdiTldlbTJLaCIsIkMiOiIwMzc5NWE0NGUwNGY1YWU5MGYyZGIwZTkzYzc3MzJkMDJkYTQ0ZGIxZmRkMWYzNDlkN2EwMzJmN2U5OGZkYzZjYzQiLCJpZCI6Im1SOVBKM016akwxeSJ9XX1dfQ -------------------------------------------------------------------------------- /moksha-core/src/fixtures/token_nut_example.cashu: -------------------------------------------------------------------------------- 1 | cashuAeyJ0b2tlbiI6W3sibWludCI6Imh0dHBzOi8vODMzMy5zcGFjZTozMzM4IiwicHJvb2ZzIjpbeyJhbW91bnQiOjIsImlkIjoiMDA5YTFmMjkzMjUzZTQxZSIsInNlY3JldCI6IjQwNzkxNWJjMjEyYmU2MWE3N2UzZTZkMmFlYjRjNzI3OTgwYmRhNTFjZDA2YTZhZmMyOWUyODYxNzY4YTc4MzciLCJDIjoiMDJiYzkwOTc5OTdkODFhZmIyY2M3MzQ2YjVlNDM0NWE5MzQ2YmQyYTUwNmViNzk1ODU5OGE3MmYwY2Y4NTE2M2VhIn0seyJhbW91bnQiOjgsImlkIjoiMDA5YTFmMjkzMjUzZTQxZSIsInNlY3JldCI6ImZlMTUxMDkzMTRlNjFkNzc1NmIwZjhlZTBmMjNhNjI0YWNhYTNmNGUwNDJmNjE0MzNjNzI4YzcwNTdiOTMxYmUiLCJDIjoiMDI5ZThlNTA1MGI4OTBhN2Q2YzA5NjhkYjE2YmMxZDVkNWZhMDQwZWExZGUyODRmNmVjNjlkNjEyOTlmNjcxMDU5In1dfV0sInVuaXQiOiJzYXQiLCJtZW1vIjoiVGhhbmsgeW91LiJ9 -------------------------------------------------------------------------------- /moksha-core/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod amount; 2 | pub mod blind; 3 | pub mod dhke; 4 | pub mod error; 5 | pub mod fixture; 6 | pub mod keyset; 7 | pub mod primitives; 8 | pub mod proof; 9 | pub mod token; 10 | -------------------------------------------------------------------------------- /moksha-core/src/proof.rs: -------------------------------------------------------------------------------- 1 | //! This module defines the `Proof`, `P2SHScript`, and `Proofs` structs, which are used for representing proofs in the Moksha Core library as described in [Nut-00](https://github.com/cashubtc/nuts/blob/main/00.md) 2 | //! 3 | //! The `Proof` struct represents a proof, with an `amount` field for the amount in satoshis, a `secret` field for the secret string, a `c` field for the public key of the blinding factor, an `id` field for the ID of the proof, and an optional `script` field for the P2SH script. 4 | //! 5 | //! The `Proof` struct provides a `new` method for creating a new proof from its constituent fields. 6 | //! 7 | //! The `P2SHScript` struct represents a P2SH script, and is currently not implemented. 8 | //! 9 | //! The `Proofs` struct represents a collection of proofs, with a `Vec` field for the proofs. 10 | //! 11 | //! Both the `Proof` and `Proofs` structs are serializable and deserializable using serde. 12 | 13 | use secp256k1::PublicKey; 14 | use serde::{Deserialize, Serialize}; 15 | use serde_with::skip_serializing_none; 16 | use utoipa::ToSchema; 17 | 18 | use crate::{error::MokshaCoreError, keyset::KeysetId}; 19 | 20 | #[skip_serializing_none] 21 | #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, ToSchema)] 22 | pub struct Proof { 23 | pub amount: u64, 24 | #[serde(rename = "id")] 25 | pub keyset_id: String, // FIXME use keysetID as specific type 26 | pub secret: String, 27 | #[serde(rename = "C")] 28 | #[schema(value_type = String)] 29 | pub c: PublicKey, 30 | pub script: Option, 31 | } 32 | 33 | impl Proof { 34 | pub const fn new(amount: u64, secret: String, c: PublicKey, id: String) -> Self { 35 | Self { 36 | amount, 37 | secret, 38 | c, 39 | keyset_id: id, 40 | script: None, 41 | } 42 | } 43 | } 44 | 45 | #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, ToSchema)] 46 | pub struct P2SHScript; 47 | 48 | #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, ToSchema)] 49 | pub struct Proofs(pub(super) Vec); 50 | 51 | impl Proofs { 52 | pub fn new(proofs: Vec) -> Self { 53 | Self(proofs) 54 | } 55 | 56 | pub fn with_proof(proof: Proof) -> Self { 57 | Self(vec![proof]) 58 | } 59 | 60 | pub const fn empty() -> Self { 61 | Self(vec![]) 62 | } 63 | 64 | pub fn total_amount(&self) -> u64 { 65 | self.0.iter().map(|proof| proof.amount).sum() 66 | } 67 | 68 | pub fn proofs(&self) -> Vec { 69 | self.0.clone() 70 | } 71 | 72 | pub fn len(&self) -> usize { 73 | self.0.len() 74 | } 75 | 76 | pub fn is_empty(&self) -> bool { 77 | self.0.is_empty() 78 | } 79 | 80 | pub fn proofs_by_keyset(&self, keyset_id: &KeysetId) -> Self { 81 | self.0 82 | .iter() 83 | .filter(|proof| proof.keyset_id == keyset_id.to_string()) 84 | .cloned() 85 | .collect::>() 86 | .into() 87 | } 88 | 89 | pub fn proofs_for_amount(&self, amount: u64) -> Result { 90 | let mut all_proofs = self.0.clone(); 91 | if amount > self.total_amount() { 92 | return Err(MokshaCoreError::NotEnoughTokens); 93 | } 94 | 95 | all_proofs.sort_by(|a, b| a.amount.cmp(&b.amount)); 96 | 97 | let mut selected_proofs = vec![]; 98 | let mut selected_amount = 0; 99 | 100 | while selected_amount < amount { 101 | if all_proofs.is_empty() { 102 | break; 103 | } 104 | 105 | let proof = all_proofs.pop().expect("proofs is empty"); 106 | selected_amount += proof.amount; 107 | selected_proofs.push(proof); 108 | } 109 | 110 | Ok(selected_proofs.into()) 111 | } 112 | } 113 | 114 | impl From> for Proofs { 115 | fn from(from: Vec) -> Self { 116 | Self(from) 117 | } 118 | } 119 | 120 | impl From for Proofs { 121 | fn from(from: Proof) -> Self { 122 | Self(vec![from]) 123 | } 124 | } 125 | 126 | #[cfg(test)] 127 | mod tests { 128 | use serde_json::json; 129 | 130 | use crate::{ 131 | fixture::read_fixture, 132 | proof::{Proof, Proofs}, 133 | token::TokenV3, 134 | }; 135 | use pretty_assertions::assert_eq; 136 | 137 | #[test] 138 | fn test_proofs_for_amount_empty() -> anyhow::Result<()> { 139 | let proofs = Proofs::empty(); 140 | 141 | let result = proofs.proofs_for_amount(10); 142 | 143 | assert!(result.is_err()); 144 | assert!(result 145 | .err() 146 | .unwrap() 147 | .to_string() 148 | .contains("Not enough tokens")); 149 | Ok(()) 150 | } 151 | 152 | #[test] 153 | fn test_proofs_for_amount_valid() -> anyhow::Result<()> { 154 | let fixture = read_fixture("token_60.cashu")?; // 60 tokens (4,8,16,32) 155 | let token: TokenV3 = fixture.try_into()?; 156 | 157 | let result = token.proofs().proofs_for_amount(10)?; 158 | assert_eq!(32, result.total_amount()); 159 | assert_eq!(1, result.len()); 160 | Ok(()) 161 | } 162 | 163 | #[test] 164 | fn test_proof() -> anyhow::Result<()> { 165 | let js = json!( 166 | { 167 | "id": "DSAl9nvvyfva", 168 | "amount": 2, 169 | "secret": "EhpennC9qB3iFlW8FZ_pZw", 170 | "C": "02c020067db727d586bc3183aecf97fcb800c3f4cc4759f69c626c9db5d8f5b5d4" 171 | } 172 | ); 173 | 174 | let proof = serde_json::from_value::(js)?; 175 | assert_eq!(proof.amount, 2); 176 | assert_eq!(proof.keyset_id, "DSAl9nvvyfva".to_string()); 177 | assert_eq!(proof.secret, "EhpennC9qB3iFlW8FZ_pZw".to_string()); 178 | assert_eq!( 179 | proof.c.to_string(), 180 | "02c020067db727d586bc3183aecf97fcb800c3f4cc4759f69c626c9db5d8f5b5d4".to_string() 181 | ); 182 | Ok(()) 183 | } 184 | } 185 | -------------------------------------------------------------------------------- /moksha-mint/.sqlx/query-2cb95b0c3011a332322132339e6023035e4a81824bef6a0ad47215f851fb1100.json: -------------------------------------------------------------------------------- 1 | { 2 | "db_name": "PostgreSQL", 3 | "query": "INSERT INTO onchain_mint_quotes (id, address, amount, expiry, state) VALUES ($1, $2, $3, $4, $5)", 4 | "describe": { 5 | "columns": [], 6 | "parameters": { 7 | "Left": [ 8 | "Uuid", 9 | "Text", 10 | "Int8", 11 | "Int8", 12 | "Text" 13 | ] 14 | }, 15 | "nullable": [] 16 | }, 17 | "hash": "2cb95b0c3011a332322132339e6023035e4a81824bef6a0ad47215f851fb1100" 18 | } 19 | -------------------------------------------------------------------------------- /moksha-mint/.sqlx/query-3a797dad2f155d90b625ffcd3d89e5b4b2050a52645bd8d8270cc0d8946eb249.json: -------------------------------------------------------------------------------- 1 | { 2 | "db_name": "PostgreSQL", 3 | "query": "DELETE FROM bolt11_mint_quotes WHERE id = $1", 4 | "describe": { 5 | "columns": [], 6 | "parameters": { 7 | "Left": [ 8 | "Uuid" 9 | ] 10 | }, 11 | "nullable": [] 12 | }, 13 | "hash": "3a797dad2f155d90b625ffcd3d89e5b4b2050a52645bd8d8270cc0d8946eb249" 14 | } 15 | -------------------------------------------------------------------------------- /moksha-mint/.sqlx/query-51048c44648c7a3140c37027765ef0a9bdf7c8a050d8353f3f4083c80bbc03f7.json: -------------------------------------------------------------------------------- 1 | { 2 | "db_name": "PostgreSQL", 3 | "query": "INSERT INTO onchain_melt_quotes (id, amount, address, fee_total, fee_sat_per_vbyte, expiry, state, description) VALUES ($1, $2, $3, $4, $5, $6, $7, $8)", 4 | "describe": { 5 | "columns": [], 6 | "parameters": { 7 | "Left": [ 8 | "Uuid", 9 | "Int8", 10 | "Text", 11 | "Int8", 12 | "Int8", 13 | "Int8", 14 | "Text", 15 | "Text" 16 | ] 17 | }, 18 | "nullable": [] 19 | }, 20 | "hash": "51048c44648c7a3140c37027765ef0a9bdf7c8a050d8353f3f4083c80bbc03f7" 21 | } 22 | -------------------------------------------------------------------------------- /moksha-mint/.sqlx/query-594c0ed8b964bdf16208ab5909c05bbfe15c245f667646b2450b5bd649cf219c.json: -------------------------------------------------------------------------------- 1 | { 2 | "db_name": "PostgreSQL", 3 | "query": "SELECT * FROM used_proofs", 4 | "describe": { 5 | "columns": [ 6 | { 7 | "ordinal": 0, 8 | "name": "amount", 9 | "type_info": "Int8" 10 | }, 11 | { 12 | "ordinal": 1, 13 | "name": "secret", 14 | "type_info": "Text" 15 | }, 16 | { 17 | "ordinal": 2, 18 | "name": "c", 19 | "type_info": "Text" 20 | }, 21 | { 22 | "ordinal": 3, 23 | "name": "keyset_id", 24 | "type_info": "Text" 25 | } 26 | ], 27 | "parameters": { 28 | "Left": [] 29 | }, 30 | "nullable": [ 31 | false, 32 | false, 33 | false, 34 | false 35 | ] 36 | }, 37 | "hash": "594c0ed8b964bdf16208ab5909c05bbfe15c245f667646b2450b5bd649cf219c" 38 | } 39 | -------------------------------------------------------------------------------- /moksha-mint/.sqlx/query-5d14a8fcd6f0e680a3868e48a513d93eefb73461b7fc7cdf17996e8d979b1abf.json: -------------------------------------------------------------------------------- 1 | { 2 | "db_name": "PostgreSQL", 3 | "query": "DELETE FROM bolt11_melt_quotes WHERE id = $1", 4 | "describe": { 5 | "columns": [], 6 | "parameters": { 7 | "Left": [ 8 | "Uuid" 9 | ] 10 | }, 11 | "nullable": [] 12 | }, 13 | "hash": "5d14a8fcd6f0e680a3868e48a513d93eefb73461b7fc7cdf17996e8d979b1abf" 14 | } 15 | -------------------------------------------------------------------------------- /moksha-mint/.sqlx/query-6d6efea17e2e8799c4e2e09c5eabf8e0e59da3646de7f1471b4507df7168ed32.json: -------------------------------------------------------------------------------- 1 | { 2 | "db_name": "PostgreSQL", 3 | "query": "UPDATE onchain_mint_quotes SET state = $1 WHERE id = $2", 4 | "describe": { 5 | "columns": [], 6 | "parameters": { 7 | "Left": [ 8 | "Text", 9 | "Uuid" 10 | ] 11 | }, 12 | "nullable": [] 13 | }, 14 | "hash": "6d6efea17e2e8799c4e2e09c5eabf8e0e59da3646de7f1471b4507df7168ed32" 15 | } 16 | -------------------------------------------------------------------------------- /moksha-mint/.sqlx/query-76f0b81d265374f59b1222f1c07d814093c9d225334f39549b0c05e0b733dcc6.json: -------------------------------------------------------------------------------- 1 | { 2 | "db_name": "PostgreSQL", 3 | "query": "SELECT id, amount,address, fee_total, fee_sat_per_vbyte, expiry, state, description FROM onchain_melt_quotes WHERE id = $1", 4 | "describe": { 5 | "columns": [ 6 | { 7 | "ordinal": 0, 8 | "name": "id", 9 | "type_info": "Uuid" 10 | }, 11 | { 12 | "ordinal": 1, 13 | "name": "amount", 14 | "type_info": "Int8" 15 | }, 16 | { 17 | "ordinal": 2, 18 | "name": "address", 19 | "type_info": "Text" 20 | }, 21 | { 22 | "ordinal": 3, 23 | "name": "fee_total", 24 | "type_info": "Int8" 25 | }, 26 | { 27 | "ordinal": 4, 28 | "name": "fee_sat_per_vbyte", 29 | "type_info": "Int8" 30 | }, 31 | { 32 | "ordinal": 5, 33 | "name": "expiry", 34 | "type_info": "Int8" 35 | }, 36 | { 37 | "ordinal": 6, 38 | "name": "state", 39 | "type_info": "Text" 40 | }, 41 | { 42 | "ordinal": 7, 43 | "name": "description", 44 | "type_info": "Text" 45 | } 46 | ], 47 | "parameters": { 48 | "Left": [ 49 | "Uuid" 50 | ] 51 | }, 52 | "nullable": [ 53 | false, 54 | false, 55 | false, 56 | false, 57 | false, 58 | false, 59 | false, 60 | true 61 | ] 62 | }, 63 | "hash": "76f0b81d265374f59b1222f1c07d814093c9d225334f39549b0c05e0b733dcc6" 64 | } 65 | -------------------------------------------------------------------------------- /moksha-mint/.sqlx/query-87908c797aa3343256255edb331044d890d22d06adc0313823dc8448e21e3f1c.json: -------------------------------------------------------------------------------- 1 | { 2 | "db_name": "PostgreSQL", 3 | "query": "SELECT id, address, amount, expiry, state FROM onchain_mint_quotes WHERE id = $1", 4 | "describe": { 5 | "columns": [ 6 | { 7 | "ordinal": 0, 8 | "name": "id", 9 | "type_info": "Uuid" 10 | }, 11 | { 12 | "ordinal": 1, 13 | "name": "address", 14 | "type_info": "Text" 15 | }, 16 | { 17 | "ordinal": 2, 18 | "name": "amount", 19 | "type_info": "Int8" 20 | }, 21 | { 22 | "ordinal": 3, 23 | "name": "expiry", 24 | "type_info": "Int8" 25 | }, 26 | { 27 | "ordinal": 4, 28 | "name": "state", 29 | "type_info": "Text" 30 | } 31 | ], 32 | "parameters": { 33 | "Left": [ 34 | "Uuid" 35 | ] 36 | }, 37 | "nullable": [ 38 | false, 39 | false, 40 | false, 41 | false, 42 | false 43 | ] 44 | }, 45 | "hash": "87908c797aa3343256255edb331044d890d22d06adc0313823dc8448e21e3f1c" 46 | } 47 | -------------------------------------------------------------------------------- /moksha-mint/.sqlx/query-9b7392f6768ac112b9cfd7e4e08ca5c3949884a3f7b8139b61cd7446d4983a05.json: -------------------------------------------------------------------------------- 1 | { 2 | "db_name": "PostgreSQL", 3 | "query": "DELETE FROM pending_invoices WHERE key = $1", 4 | "describe": { 5 | "columns": [], 6 | "parameters": { 7 | "Left": [ 8 | "Text" 9 | ] 10 | }, 11 | "nullable": [] 12 | }, 13 | "hash": "9b7392f6768ac112b9cfd7e4e08ca5c3949884a3f7b8139b61cd7446d4983a05" 14 | } 15 | -------------------------------------------------------------------------------- /moksha-mint/.sqlx/query-9db8c4f6f5c71b2a0e4476429931112334053812decdbc7869404a7806d33856.json: -------------------------------------------------------------------------------- 1 | { 2 | "db_name": "PostgreSQL", 3 | "query": "SELECT amount, payment_request FROM pending_invoices WHERE key = $1", 4 | "describe": { 5 | "columns": [ 6 | { 7 | "ordinal": 0, 8 | "name": "amount", 9 | "type_info": "Int8" 10 | }, 11 | { 12 | "ordinal": 1, 13 | "name": "payment_request", 14 | "type_info": "Text" 15 | } 16 | ], 17 | "parameters": { 18 | "Left": [ 19 | "Text" 20 | ] 21 | }, 22 | "nullable": [ 23 | false, 24 | false 25 | ] 26 | }, 27 | "hash": "9db8c4f6f5c71b2a0e4476429931112334053812decdbc7869404a7806d33856" 28 | } 29 | -------------------------------------------------------------------------------- /moksha-mint/.sqlx/query-9deb32cf2da6eb3d4b099556891f4ef979dde0f4c3ae901c7617d2e3c602a691.json: -------------------------------------------------------------------------------- 1 | { 2 | "db_name": "PostgreSQL", 3 | "query": "SELECT id, payment_request, expiry, paid, amount, fee_reserve FROM bolt11_melt_quotes WHERE id = $1", 4 | "describe": { 5 | "columns": [ 6 | { 7 | "ordinal": 0, 8 | "name": "id", 9 | "type_info": "Uuid" 10 | }, 11 | { 12 | "ordinal": 1, 13 | "name": "payment_request", 14 | "type_info": "Text" 15 | }, 16 | { 17 | "ordinal": 2, 18 | "name": "expiry", 19 | "type_info": "Int8" 20 | }, 21 | { 22 | "ordinal": 3, 23 | "name": "paid", 24 | "type_info": "Bool" 25 | }, 26 | { 27 | "ordinal": 4, 28 | "name": "amount", 29 | "type_info": "Int8" 30 | }, 31 | { 32 | "ordinal": 5, 33 | "name": "fee_reserve", 34 | "type_info": "Int8" 35 | } 36 | ], 37 | "parameters": { 38 | "Left": [ 39 | "Uuid" 40 | ] 41 | }, 42 | "nullable": [ 43 | false, 44 | false, 45 | false, 46 | false, 47 | false, 48 | false 49 | ] 50 | }, 51 | "hash": "9deb32cf2da6eb3d4b099556891f4ef979dde0f4c3ae901c7617d2e3c602a691" 52 | } 53 | -------------------------------------------------------------------------------- /moksha-mint/.sqlx/query-aa1d3252354b9468831dfd2a3a499b5a52850bf3ec7da7a6c1a74b43d4970649.json: -------------------------------------------------------------------------------- 1 | { 2 | "db_name": "PostgreSQL", 3 | "query": "INSERT INTO used_proofs (amount, secret, c, keyset_id) VALUES ($1, $2, $3, $4)", 4 | "describe": { 5 | "columns": [], 6 | "parameters": { 7 | "Left": [ 8 | "Int8", 9 | "Text", 10 | "Text", 11 | "Text" 12 | ] 13 | }, 14 | "nullable": [] 15 | }, 16 | "hash": "aa1d3252354b9468831dfd2a3a499b5a52850bf3ec7da7a6c1a74b43d4970649" 17 | } 18 | -------------------------------------------------------------------------------- /moksha-mint/.sqlx/query-b1fd2ab2863004ad03ddfd0a0eb8084456f0dd28aff814a440d29edf3574028f.json: -------------------------------------------------------------------------------- 1 | { 2 | "db_name": "PostgreSQL", 3 | "query": "DELETE FROM onchain_melt_quotes WHERE id = $1", 4 | "describe": { 5 | "columns": [], 6 | "parameters": { 7 | "Left": [ 8 | "Uuid" 9 | ] 10 | }, 11 | "nullable": [] 12 | }, 13 | "hash": "b1fd2ab2863004ad03ddfd0a0eb8084456f0dd28aff814a440d29edf3574028f" 14 | } 15 | -------------------------------------------------------------------------------- /moksha-mint/.sqlx/query-bbb5353af44cc7ef7e5d91a10a5bd0422c6a2f0e52a079dd41f012994cf3b4ea.json: -------------------------------------------------------------------------------- 1 | { 2 | "db_name": "PostgreSQL", 3 | "query": "INSERT INTO bolt11_mint_quotes (id, payment_request, expiry, paid) VALUES ($1, $2, $3, $4)", 4 | "describe": { 5 | "columns": [], 6 | "parameters": { 7 | "Left": [ 8 | "Uuid", 9 | "Text", 10 | "Int8", 11 | "Bool" 12 | ] 13 | }, 14 | "nullable": [] 15 | }, 16 | "hash": "bbb5353af44cc7ef7e5d91a10a5bd0422c6a2f0e52a079dd41f012994cf3b4ea" 17 | } 18 | -------------------------------------------------------------------------------- /moksha-mint/.sqlx/query-bc67cae55e8dffda179773dcaec8275b37ff9e286e3d813d7aaad9a90cf793af.json: -------------------------------------------------------------------------------- 1 | { 2 | "db_name": "PostgreSQL", 3 | "query": "UPDATE onchain_melt_quotes SET state = $1 WHERE id = $2", 4 | "describe": { 5 | "columns": [], 6 | "parameters": { 7 | "Left": [ 8 | "Text", 9 | "Uuid" 10 | ] 11 | }, 12 | "nullable": [] 13 | }, 14 | "hash": "bc67cae55e8dffda179773dcaec8275b37ff9e286e3d813d7aaad9a90cf793af" 15 | } 16 | -------------------------------------------------------------------------------- /moksha-mint/.sqlx/query-ce99d546aa2c36df35aa38a29e50e46c5ba5ad037d75315d04fb456c5201a924.json: -------------------------------------------------------------------------------- 1 | { 2 | "db_name": "PostgreSQL", 3 | "query": "SELECT id, payment_request, expiry, paid FROM bolt11_mint_quotes WHERE id = $1", 4 | "describe": { 5 | "columns": [ 6 | { 7 | "ordinal": 0, 8 | "name": "id", 9 | "type_info": "Uuid" 10 | }, 11 | { 12 | "ordinal": 1, 13 | "name": "payment_request", 14 | "type_info": "Text" 15 | }, 16 | { 17 | "ordinal": 2, 18 | "name": "expiry", 19 | "type_info": "Int8" 20 | }, 21 | { 22 | "ordinal": 3, 23 | "name": "paid", 24 | "type_info": "Bool" 25 | } 26 | ], 27 | "parameters": { 28 | "Left": [ 29 | "Uuid" 30 | ] 31 | }, 32 | "nullable": [ 33 | false, 34 | false, 35 | false, 36 | false 37 | ] 38 | }, 39 | "hash": "ce99d546aa2c36df35aa38a29e50e46c5ba5ad037d75315d04fb456c5201a924" 40 | } 41 | -------------------------------------------------------------------------------- /moksha-mint/.sqlx/query-d06538e9bb5466ea92afcee20108bc69b1d0771deea19e701c391c9bd6f38362.json: -------------------------------------------------------------------------------- 1 | { 2 | "db_name": "PostgreSQL", 3 | "query": "INSERT INTO bolt11_melt_quotes (id, payment_request, expiry, paid, amount, fee_reserve) VALUES ($1, $2, $3, $4, $5, $6)", 4 | "describe": { 5 | "columns": [], 6 | "parameters": { 7 | "Left": [ 8 | "Uuid", 9 | "Text", 10 | "Int8", 11 | "Bool", 12 | "Int8", 13 | "Int8" 14 | ] 15 | }, 16 | "nullable": [] 17 | }, 18 | "hash": "d06538e9bb5466ea92afcee20108bc69b1d0771deea19e701c391c9bd6f38362" 19 | } 20 | -------------------------------------------------------------------------------- /moksha-mint/.sqlx/query-d8fa5a9f70c2a2d00433b2a9e2ddb1b9e55255e9c5409d9f3c4d33dd9338455e.json: -------------------------------------------------------------------------------- 1 | { 2 | "db_name": "PostgreSQL", 3 | "query": "INSERT INTO pending_invoices (key, amount, payment_request) VALUES ($1, $2, $3)", 4 | "describe": { 5 | "columns": [], 6 | "parameters": { 7 | "Left": [ 8 | "Text", 9 | "Int8", 10 | "Text" 11 | ] 12 | }, 13 | "nullable": [] 14 | }, 15 | "hash": "d8fa5a9f70c2a2d00433b2a9e2ddb1b9e55255e9c5409d9f3c4d33dd9338455e" 16 | } 17 | -------------------------------------------------------------------------------- /moksha-mint/.sqlx/query-dc1215b1d311ad0d1386f5058ff1ab8d4edf6314b4ab02ed2045b72603d01703.json: -------------------------------------------------------------------------------- 1 | { 2 | "db_name": "PostgreSQL", 3 | "query": "UPDATE bolt11_melt_quotes SET paid = $1 WHERE id = $2", 4 | "describe": { 5 | "columns": [], 6 | "parameters": { 7 | "Left": [ 8 | "Bool", 9 | "Uuid" 10 | ] 11 | }, 12 | "nullable": [] 13 | }, 14 | "hash": "dc1215b1d311ad0d1386f5058ff1ab8d4edf6314b4ab02ed2045b72603d01703" 15 | } 16 | -------------------------------------------------------------------------------- /moksha-mint/.sqlx/query-e4c704c7bf2a67103c56420f4c3388536ee546962368c41a898abaa9bf6f90fe.json: -------------------------------------------------------------------------------- 1 | { 2 | "db_name": "PostgreSQL", 3 | "query": "DELETE FROM onchain_mint_quotes WHERE id = $1", 4 | "describe": { 5 | "columns": [], 6 | "parameters": { 7 | "Left": [ 8 | "Uuid" 9 | ] 10 | }, 11 | "nullable": [] 12 | }, 13 | "hash": "e4c704c7bf2a67103c56420f4c3388536ee546962368c41a898abaa9bf6f90fe" 14 | } 15 | -------------------------------------------------------------------------------- /moksha-mint/.sqlx/query-eb5c7406dcca5d043c2cd3fd1c2618a47191cde482b2071246e2f007195cd3b4.json: -------------------------------------------------------------------------------- 1 | { 2 | "db_name": "PostgreSQL", 3 | "query": "UPDATE bolt11_mint_quotes SET paid = $1 WHERE id = $2", 4 | "describe": { 5 | "columns": [], 6 | "parameters": { 7 | "Left": [ 8 | "Bool", 9 | "Uuid" 10 | ] 11 | }, 12 | "nullable": [] 13 | }, 14 | "hash": "eb5c7406dcca5d043c2cd3fd1c2618a47191cde482b2071246e2f007195cd3b4" 15 | } 16 | -------------------------------------------------------------------------------- /moksha-mint/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "moksha-mint" 3 | version = "0.2.1" 4 | edition = "2021" 5 | repository = "https://github.com/ngutech21/moksha" 6 | license = "MIT" 7 | description = "A cashu-mint written in Rust" 8 | 9 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 10 | 11 | [[bin]] 12 | name = "moksha-mint" 13 | path = "src/bin/moksha-mint.rs" 14 | 15 | [lib] 16 | name = "mokshamint" 17 | path = "src/lib.rs" 18 | 19 | [dependencies] 20 | clap = { workspace = true, features = ["env", "derive"] } 21 | hex = { workspace = true } 22 | async-trait = { workspace = true } 23 | anyhow = { workspace = true, features = ["backtrace"] } 24 | axum = { workspace = true, features = ["http2"] } 25 | hyper = { workspace = true } 26 | serde = { workspace = true, features = ["derive"] } 27 | serde_json = { workspace = true } 28 | tokio = { workspace = true, features = ["full"] } 29 | tower-http = { workspace = true, features = ["cors", "fs", "set-header"] } 30 | secp256k1 = { workspace = true, features = ["rand", "serde"] } 31 | thiserror = { workspace = true } 32 | moksha-core = { path = "../moksha-core", version = "0.2.1" } 33 | lightning-invoice = "0.31.0" 34 | reqwest = { workspace = true, features = ["json", "rustls-tls", "socks"] } 35 | url = { workspace = true } 36 | dotenvy = { workspace = true } 37 | fedimint-tonic-lnd = { workspace = true, features = ["lightningrpc", "walletrpc"] } 38 | uuid = { workspace = true, features = ["serde", "v4"] } 39 | utoipa = { workspace = true, features = ["axum_extras"] } 40 | utoipa-swagger-ui = { workspace = true, features = ["axum"] } 41 | sqlx = { workspace = true, features = ["postgres", "runtime-tokio", "tls-rustls", "migrate", "macros", "uuid"] } 42 | chrono = { workspace = true } 43 | cln-grpc = { workspace = true } 44 | tonic = { workspace = true, features = ["transport", "tls"] } 45 | 46 | tracing = { workspace = true } 47 | tracing-subscriber = { workspace = true, features = ["env-filter", "json"] } 48 | tracing-opentelemetry = { workspace = true } 49 | opentelemetry_sdk = { workspace = true, features = ["rt-tokio"] } 50 | opentelemetry = { workspace = true } 51 | opentelemetry-otlp = { workspace = true, features = ["http-proto", "reqwest-client"] } 52 | 53 | [dev-dependencies] 54 | tempfile = { workspace = true } 55 | tower = { workspace = true, features = ["util"] } 56 | mockall = { workspace = true } 57 | hex = { workspace = true } 58 | http-body-util = { workspace = true } 59 | testcontainers = { workspace = true } 60 | testcontainers-modules = { workspace = true, features = ["postgres"] } 61 | pretty_assertions = { workspace = true } 62 | -------------------------------------------------------------------------------- /moksha-mint/migrations/20231213080237_init.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE used_proofs ( 2 | amount BIGINT NOT NULL, 3 | secret TEXT NOT NULL PRIMARY KEY, 4 | c TEXT NOT NULL, 5 | keyset_id TEXT NOT NULL 6 | ); 7 | 8 | CREATE TABLE pending_invoices ( 9 | key TEXT NOT NULL PRIMARY KEY, 10 | payment_request TEXT NOT NULL, 11 | amount BIGINT NOT NULL 12 | ); 13 | 14 | CREATE TABLE bolt11_mint_quotes ( 15 | id UUID PRIMARY KEY NOT NULL, 16 | payment_request TEXT NOT NULL, 17 | expiry BIGINT NOT NULL, 18 | paid BOOLEAN NOT NULL 19 | ); 20 | 21 | CREATE TABLE bolt11_melt_quotes ( 22 | id UUID PRIMARY KEY NOT NULL, 23 | payment_request TEXT NOT NULL, 24 | expiry BIGINT NOT NULL, 25 | paid BOOLEAN NOT NULL, 26 | amount BIGINT NOT NULL, 27 | fee_reserve BIGINT NOT NULL 28 | ); -------------------------------------------------------------------------------- /moksha-mint/migrations/20240112101746_onchain.sql: -------------------------------------------------------------------------------- 1 | -- Add migration script here 2 | CREATE TABLE IF NOT EXISTS onchain_mint_quotes 3 | ( 4 | id uuid NOT NULL, 5 | address text COLLATE pg_catalog."default" NOT NULL, 6 | amount bigint NOT NULL, 7 | expiry bigint NOT NULL, 8 | paid boolean NOT NULL, 9 | CONSTRAINT onchain_mint_quotes_pkey PRIMARY KEY (id) 10 | ); 11 | 12 | 13 | CREATE TABLE IF NOT EXISTS onchain_melt_quotes 14 | ( 15 | id uuid NOT NULL, 16 | amount bigint NOT NULL, 17 | address text COLLATE pg_catalog."default" NOT NULL, 18 | fee_total bigint NOT NULL, 19 | fee_sat_per_vbyte bigint NOT NULL, 20 | expiry bigint NOT NULL, 21 | paid boolean NOT NULL, 22 | CONSTRAINT onchain_melt_quotes_pkey PRIMARY KEY (id) 23 | ); 24 | -------------------------------------------------------------------------------- /moksha-mint/migrations/20240408071034_btconchain_melt_description.sql: -------------------------------------------------------------------------------- 1 | -- Add the column without the NOT NULL constraint 2 | ALTER TABLE onchain_melt_quotes 3 | ADD COLUMN description text; 4 | 5 | -- Set all existing rows to an empty string 6 | UPDATE onchain_melt_quotes 7 | SET description = ''; 8 | 9 | -- Add the NOT NULL constraint 10 | ALTER TABLE onchain_melt_quotes 11 | ALTER COLUMN description SET NOT NULL; -------------------------------------------------------------------------------- /moksha-mint/migrations/20240729115553_btconchain_melt_description_optional.sql: -------------------------------------------------------------------------------- 1 | -- Add migration script here 2 | ALTER TABLE onchain_melt_quotes 3 | ALTER COLUMN description DROP NOT NULL; -------------------------------------------------------------------------------- /moksha-mint/migrations/20240801112647_btconchain_melt_state.sql: -------------------------------------------------------------------------------- 1 | -- Add migration script here 2 | ALTER TABLE onchain_melt_quotes 3 | ADD COLUMN state TEXT; 4 | 5 | UPDATE onchain_melt_quotes 6 | SET state = CASE 7 | WHEN paid = true THEN 'PAID' 8 | WHEN paid = false THEN 'UNPAID' 9 | END; 10 | 11 | ALTER TABLE onchain_melt_quotes 12 | DROP COLUMN paid; 13 | 14 | ALTER TABLE onchain_melt_quotes 15 | ALTER COLUMN state SET NOT NULL; -------------------------------------------------------------------------------- /moksha-mint/migrations/20240801124852_btconchain_mint_state.sql: -------------------------------------------------------------------------------- 1 | -- Add migration script here 2 | ALTER TABLE onchain_mint_quotes 3 | ADD COLUMN state TEXT; 4 | 5 | UPDATE onchain_mint_quotes 6 | SET state = CASE 7 | WHEN paid = true THEN 'PAID' 8 | WHEN paid = false THEN 'UNPAID' 9 | END; 10 | 11 | ALTER TABLE onchain_mint_quotes 12 | DROP COLUMN paid; 13 | 14 | ALTER TABLE onchain_mint_quotes 15 | ALTER COLUMN state SET NOT NULL; -------------------------------------------------------------------------------- /moksha-mint/src/bin/moksha-mint.rs: -------------------------------------------------------------------------------- 1 | use mokshamint::{ 2 | config::{MintConfig, TracingConfig}, 3 | mint::MintBuilder, 4 | }; 5 | use std::env; 6 | use tracing_subscriber::{filter::EnvFilter, fmt, layer::SubscriberExt, util::SubscriberInitExt}; 7 | 8 | use opentelemetry::KeyValue; 9 | use opentelemetry_otlp::WithExportConfig; 10 | use opentelemetry_sdk::trace::Sampler; 11 | 12 | #[tokio::main] 13 | pub async fn main() -> anyhow::Result<()> { 14 | let app_env = match env::var("MINT_APP_ENV") { 15 | Ok(v) if v.trim() == "dev" => AppEnv::Dev, 16 | _ => AppEnv::Prod, 17 | }; 18 | 19 | println!("Running in {app_env} mode"); 20 | 21 | if app_env == AppEnv::Dev { 22 | match dotenvy::dotenv() { 23 | Ok(path) => println!(".env read successfully from {}", path.display()), 24 | Err(e) => panic!("Could not load .env file: {e}"), 25 | }; 26 | } 27 | 28 | let MintConfig { 29 | privatekey, 30 | derivation_path, 31 | info, 32 | lightning_fee, 33 | server, 34 | btconchain_backend, 35 | lightning_backend, 36 | tracing, 37 | database, 38 | } = MintConfig::read_config_with_defaults(); 39 | 40 | init_tracing(tracing.clone())?; 41 | 42 | let mint = MintBuilder::new() 43 | .with_mint_info(Some(info)) 44 | .with_server(Some(server)) 45 | .with_private_key(privatekey) 46 | .with_derivation_path(derivation_path) 47 | .with_db(Some(database)) 48 | .with_lightning(lightning_backend.expect("lightning not set")) 49 | .with_btc_onchain(btconchain_backend) 50 | .with_fee(Some(lightning_fee)) 51 | .with_tracing(tracing) 52 | .build() 53 | .await; 54 | 55 | mokshamint::server::run_server(mint?).await 56 | } 57 | 58 | fn init_tracing(tr: Option) -> anyhow::Result<()> { 59 | let otlp_tracer = if tr.is_some() { 60 | let tracer = opentelemetry_otlp::new_pipeline() 61 | .tracing() 62 | .with_exporter( 63 | opentelemetry_otlp::new_exporter().http().with_endpoint( 64 | tr.unwrap_or_default() 65 | .endpoint 66 | .expect("No endpoint for tracing found"), 67 | ), 68 | ) 69 | .with_trace_config( 70 | opentelemetry_sdk::trace::config() 71 | .with_sampler(Sampler::AlwaysOn) 72 | .with_resource(opentelemetry_sdk::Resource::new(vec![KeyValue::new( 73 | "service.name", 74 | "moksha-mint", 75 | )])), 76 | ) 77 | .install_batch(opentelemetry_sdk::runtime::Tokio)?; 78 | Some(tracing_opentelemetry::layer().with_tracer(tracer)) 79 | } else { 80 | None 81 | }; 82 | 83 | tracing_subscriber::registry() 84 | .with(fmt::layer()) 85 | .with(EnvFilter::from_default_env()) 86 | .with(otlp_tracer) 87 | .try_init()?; 88 | Ok(()) 89 | } 90 | 91 | #[derive(Debug, PartialEq, Eq)] 92 | pub enum AppEnv { 93 | Dev, 94 | Prod, 95 | } 96 | 97 | impl core::fmt::Display for AppEnv { 98 | fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { 99 | match self { 100 | Self::Dev => write!(f, "dev"), 101 | Self::Prod => write!(f, "prod"), 102 | } 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /moksha-mint/src/btconchain/lnd.rs: -------------------------------------------------------------------------------- 1 | #![allow(clippy::blocks_in_conditions)] 2 | use super::{BtcOnchain, EstimateFeeResult, SendCoinsResult}; 3 | use crate::error::MokshaMintError; 4 | use async_trait::async_trait; 5 | use fedimint_tonic_lnd::{ 6 | lnrpc::{AddressType, EstimateFeeRequest, NewAddressRequest, SendCoinsRequest}, 7 | walletrpc::ListUnspentRequest, 8 | Client, 9 | }; 10 | use std::{collections::HashMap, path::PathBuf, sync::Arc}; 11 | use tokio::sync::{MappedMutexGuard, Mutex, MutexGuard}; 12 | use tracing::instrument; 13 | use url::Url; 14 | 15 | pub struct LndBtcOnchain(Arc>); 16 | 17 | impl LndBtcOnchain { 18 | pub async fn new( 19 | address: Url, 20 | cert_file: &PathBuf, 21 | macaroon_file: &PathBuf, 22 | ) -> Result { 23 | let client = 24 | fedimint_tonic_lnd::connect(address.to_string(), cert_file, &macaroon_file).await; 25 | 26 | Ok(Self(Arc::new(Mutex::new( 27 | client.map_err(MokshaMintError::ConnectError)?, 28 | )))) 29 | } 30 | 31 | pub async fn client_lock( 32 | &self, 33 | ) -> Result, MokshaMintError> { 34 | let guard = self.0.lock().await; 35 | Ok(MutexGuard::map(guard, |client| client.lightning())) 36 | } 37 | 38 | pub async fn wallet_lock( 39 | &self, 40 | ) -> Result, MokshaMintError> { 41 | let guard = self.0.lock().await; 42 | Ok(MutexGuard::map(guard, |client| client.wallet())) 43 | } 44 | } 45 | 46 | #[async_trait] 47 | impl BtcOnchain for LndBtcOnchain { 48 | #[instrument(level = "debug", skip(self), err)] 49 | async fn is_transaction_paid(&self, txid: &str) -> Result { 50 | let request = ListUnspentRequest { 51 | min_confs: 0, 52 | max_confs: i32::MAX, 53 | ..Default::default() 54 | }; 55 | 56 | let response = self.wallet_lock().await?.list_unspent(request).await?; 57 | 58 | Ok(response.get_ref().utxos.iter().any(|utxo| { 59 | utxo.outpoint.clone().expect("No outpoint found").txid_str == txid 60 | && utxo.confirmations > 0 61 | })) 62 | } 63 | 64 | #[instrument(level = "debug", skip(self), err)] 65 | async fn is_paid( 66 | &self, 67 | address: &str, 68 | amount: u64, 69 | min_confirmations: u8, 70 | ) -> Result { 71 | let request = ListUnspentRequest { 72 | min_confs: 0, 73 | max_confs: i32::MAX, 74 | ..Default::default() 75 | }; 76 | 77 | let response = self.wallet_lock().await?.list_unspent(request).await?; 78 | 79 | let amount_in_sat = response 80 | .get_ref() 81 | .utxos 82 | .iter() 83 | .filter(|utxo| { 84 | utxo.address == address && utxo.confirmations >= min_confirmations as i64 85 | }) 86 | .map(|utxo| utxo.amount_sat) 87 | .sum::(); 88 | // allow overpaying for privacy reasons 89 | Ok(amount_in_sat as u64 >= amount) 90 | } 91 | 92 | #[instrument(level = "debug", skip(self), err)] 93 | async fn new_address(&self) -> Result { 94 | let mut client = self.client_lock().await?; 95 | let response = client 96 | .new_address(NewAddressRequest { 97 | r#type: AddressType::WitnessPubkeyHash as i32, 98 | ..Default::default() 99 | }) 100 | .await? 101 | .into_inner(); 102 | 103 | Ok(response.address) 104 | } 105 | 106 | #[instrument(level = "debug", skip(self), err)] 107 | async fn send_coins( 108 | &self, 109 | address: &str, 110 | amount: u64, 111 | sat_per_vbyte: u32, 112 | ) -> Result { 113 | let response = self 114 | .client_lock() 115 | .await? 116 | .send_coins(SendCoinsRequest { 117 | addr: address.to_owned(), 118 | amount: amount as i64, 119 | sat_per_vbyte: sat_per_vbyte as u64, 120 | ..Default::default() 121 | }) 122 | .await? 123 | .into_inner(); 124 | 125 | Ok(SendCoinsResult { 126 | txid: response.txid, 127 | }) 128 | } 129 | 130 | #[instrument(level = "debug", skip(self), err)] 131 | async fn estimate_fee( 132 | &self, 133 | address: &str, 134 | amount: u64, 135 | ) -> Result { 136 | let response = self 137 | .client_lock() 138 | .await? 139 | .estimate_fee(EstimateFeeRequest { 140 | addr_to_amount: std::iter::once(&(address.to_owned(), amount as i64)) 141 | .cloned() 142 | .collect::>(), 143 | target_conf: 1, 144 | ..Default::default() 145 | }) 146 | .await? 147 | .into_inner(); 148 | 149 | Ok(EstimateFeeResult { 150 | fee_in_sat: response.fee_sat as u64, 151 | sat_per_vbyte: response.sat_per_vbyte as u32, 152 | }) 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /moksha-mint/src/btconchain/mod.rs: -------------------------------------------------------------------------------- 1 | use async_trait::async_trait; 2 | 3 | use crate::error::MokshaMintError; 4 | 5 | pub mod lnd; 6 | 7 | #[cfg(test)] 8 | use mockall::automock; 9 | 10 | #[cfg_attr(test, automock)] 11 | #[async_trait] 12 | pub trait BtcOnchain: Send + Sync { 13 | async fn new_address(&self) -> Result; 14 | async fn send_coins( 15 | &self, 16 | address: &str, 17 | amount: u64, 18 | sat_per_vbyte: u32, 19 | ) -> Result; 20 | 21 | async fn estimate_fee( 22 | &self, 23 | address: &str, 24 | amount: u64, 25 | ) -> Result; 26 | 27 | async fn is_paid( 28 | &self, 29 | address: &str, 30 | amount: u64, 31 | min_confirmations: u8, 32 | ) -> Result; 33 | 34 | async fn is_transaction_paid(&self, txid: &str) -> Result; 35 | } 36 | 37 | #[derive(Debug, Clone)] 38 | pub struct EstimateFeeResult { 39 | pub fee_in_sat: u64, 40 | pub sat_per_vbyte: u32, 41 | } 42 | 43 | #[derive(Debug, Clone)] 44 | pub struct SendCoinsResult { 45 | pub txid: String, 46 | } 47 | -------------------------------------------------------------------------------- /moksha-mint/src/database/mod.rs: -------------------------------------------------------------------------------- 1 | use async_trait::async_trait; 2 | use moksha_core::{ 3 | primitives::{Bolt11MeltQuote, Bolt11MintQuote, BtcOnchainMeltQuote, BtcOnchainMintQuote}, 4 | proof::Proofs, 5 | }; 6 | use uuid::Uuid; 7 | 8 | use crate::{error::MokshaMintError, model::Invoice}; 9 | 10 | pub mod postgres; 11 | 12 | #[async_trait] 13 | pub trait Database { 14 | type DB: sqlx::Database; 15 | async fn begin_tx(&self) -> Result, sqlx::Error>; 16 | async fn get_used_proofs( 17 | &self, 18 | tx: &mut sqlx::Transaction, 19 | ) -> Result; 20 | async fn add_used_proofs( 21 | &self, 22 | tx: &mut sqlx::Transaction, 23 | proofs: &Proofs, 24 | ) -> Result<(), MokshaMintError>; 25 | 26 | async fn get_pending_invoice( 27 | &self, 28 | tx: &mut sqlx::Transaction, 29 | key: String, 30 | ) -> Result; 31 | async fn add_pending_invoice( 32 | &self, 33 | tx: &mut sqlx::Transaction, 34 | key: String, 35 | invoice: &Invoice, 36 | ) -> Result<(), MokshaMintError>; 37 | async fn delete_pending_invoice( 38 | &self, 39 | tx: &mut sqlx::Transaction, 40 | key: String, 41 | ) -> Result<(), MokshaMintError>; 42 | 43 | async fn get_bolt11_mint_quote( 44 | &self, 45 | tx: &mut sqlx::Transaction, 46 | key: &Uuid, 47 | ) -> Result; 48 | async fn add_bolt11_mint_quote( 49 | &self, 50 | tx: &mut sqlx::Transaction, 51 | quote: &Bolt11MintQuote, 52 | ) -> Result<(), MokshaMintError>; 53 | async fn update_bolt11_mint_quote( 54 | &self, 55 | tx: &mut sqlx::Transaction, 56 | quote: &Bolt11MintQuote, 57 | ) -> Result<(), MokshaMintError>; 58 | async fn delete_bolt11_mint_quote( 59 | &self, 60 | tx: &mut sqlx::Transaction, 61 | quote: &Bolt11MintQuote, 62 | ) -> Result<(), MokshaMintError>; 63 | 64 | async fn get_bolt11_melt_quote( 65 | &self, 66 | tx: &mut sqlx::Transaction, 67 | key: &Uuid, 68 | ) -> Result; 69 | async fn add_bolt11_melt_quote( 70 | &self, 71 | tx: &mut sqlx::Transaction, 72 | quote: &Bolt11MeltQuote, 73 | ) -> Result<(), MokshaMintError>; 74 | async fn update_bolt11_melt_quote( 75 | &self, 76 | tx: &mut sqlx::Transaction, 77 | quote: &Bolt11MeltQuote, 78 | ) -> Result<(), MokshaMintError>; 79 | 80 | async fn delete_bolt11_melt_quote( 81 | &self, 82 | tx: &mut sqlx::Transaction, 83 | quote: &Bolt11MeltQuote, 84 | ) -> Result<(), MokshaMintError>; 85 | 86 | async fn get_onchain_mint_quote( 87 | &self, 88 | tx: &mut sqlx::Transaction, 89 | key: &Uuid, 90 | ) -> Result; 91 | 92 | async fn add_onchain_mint_quote( 93 | &self, 94 | tx: &mut sqlx::Transaction, 95 | quote: &BtcOnchainMintQuote, 96 | ) -> Result<(), MokshaMintError>; 97 | 98 | async fn update_onchain_mint_quote( 99 | &self, 100 | tx: &mut sqlx::Transaction, 101 | quote: &BtcOnchainMintQuote, 102 | ) -> Result<(), MokshaMintError>; 103 | 104 | async fn delete_onchain_mint_quote( 105 | &self, 106 | tx: &mut sqlx::Transaction, 107 | quote: &BtcOnchainMintQuote, 108 | ) -> Result<(), MokshaMintError>; 109 | 110 | async fn get_onchain_melt_quote( 111 | &self, 112 | tx: &mut sqlx::Transaction, 113 | key: &Uuid, 114 | ) -> Result; 115 | 116 | async fn add_onchain_melt_quote( 117 | &self, 118 | tx: &mut sqlx::Transaction, 119 | quote: &BtcOnchainMeltQuote, 120 | ) -> Result<(), MokshaMintError>; 121 | 122 | async fn update_onchain_melt_quote( 123 | &self, 124 | tx: &mut sqlx::Transaction, 125 | quote: &BtcOnchainMeltQuote, 126 | ) -> Result<(), MokshaMintError>; 127 | 128 | async fn delete_onchain_melt_quote( 129 | &self, 130 | tx: &mut sqlx::Transaction, 131 | quote: &BtcOnchainMeltQuote, 132 | ) -> Result<(), MokshaMintError>; 133 | } 134 | -------------------------------------------------------------------------------- /moksha-mint/src/error.rs: -------------------------------------------------------------------------------- 1 | use std::string::FromUtf8Error; 2 | 3 | use axum::{ 4 | http::StatusCode, 5 | response::{IntoResponse, Response}, 6 | Json, 7 | }; 8 | use fedimint_tonic_lnd::{tonic::Status, ConnectError}; 9 | 10 | use lightning_invoice::ParseOrSemanticError; 11 | use moksha_core::primitives::CurrencyUnit; 12 | use serde_json::json; 13 | use thiserror::Error; 14 | use tracing::{event, Level}; 15 | 16 | use crate::lightning::error::LightningError; 17 | 18 | #[derive(Error, Debug)] 19 | pub enum MokshaMintError { 20 | #[error("LndConnectError - {0}")] 21 | ConnectError(ConnectError), 22 | 23 | #[error("ClnConnectError - {0}")] 24 | ClnConnectError(anyhow::Error), 25 | 26 | #[error("Failed to decode payment request {0} - Error {1}")] 27 | DecodeInvoice(String, ParseOrSemanticError), 28 | 29 | #[error("Failed to pay invoice {0} - Error {1}")] 30 | PayInvoice(String, LightningError), 31 | 32 | #[error("DB Error {0}")] 33 | Db(#[from] sqlx::Error), 34 | 35 | #[error("Utf8 Error {0}")] 36 | Utf8(#[from] FromUtf8Error), 37 | 38 | #[error("Serde Error {0}")] 39 | Serialization(#[from] serde_json::Error), 40 | 41 | #[error("Invoice amount is too low {0}")] 42 | InvoiceAmountTooLow(String), 43 | 44 | #[error("Invoice not found for hash {0}")] 45 | InvoiceNotFound(String), 46 | 47 | #[error("Lightning invoice not paid yet.")] 48 | InvoiceNotPaidYet, 49 | 50 | #[error("BTC-Onchain not paid yet.")] 51 | BtcOnchainNotPaidYet, 52 | 53 | #[error("Proof already used {0}")] 54 | ProofAlreadyUsed(String), 55 | 56 | #[error("{0}")] 57 | SwapAmountMismatch(String), 58 | 59 | #[error("duplicate promises.")] 60 | SwapHasDuplicatePromises, 61 | 62 | #[error("Invalid amount: {0}")] 63 | InvalidAmount(String), 64 | 65 | #[error("Lightning Error {0}")] 66 | Lightning(#[from] LightningError), 67 | 68 | #[error("Invalid quote {0}")] 69 | InvalidQuote(String), 70 | 71 | #[error("Invalid quote uuid {0}")] 72 | InvalidUuid(#[from] uuid::Error), 73 | 74 | #[error("Keyset not found {0}")] 75 | KeysetNotFound(String), 76 | 77 | #[error("Currency not supported {0}")] 78 | CurrencyNotSupported(CurrencyUnit), 79 | 80 | #[error("Not Enough tokens. Required amount {0}")] 81 | NotEnoughTokens(u64), 82 | 83 | #[error("Lnd error: {0}")] 84 | Lnd(#[from] Status), 85 | 86 | #[error("PrivateKey in keyset not found")] 87 | PrivateKeyNotFound, 88 | 89 | #[error("MokshaCoreError: {0}")] 90 | MokshaCore(#[from] moksha_core::error::MokshaCoreError), 91 | } 92 | 93 | impl IntoResponse for MokshaMintError { 94 | fn into_response(self) -> Response { 95 | event!(Level::ERROR, "error in mint: {:?}", self); 96 | 97 | let body = Json(json!({ 98 | "code": 0, 99 | "detail": self.to_string(), 100 | })); 101 | 102 | (StatusCode::BAD_REQUEST, body).into_response() 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /moksha-mint/src/fixtures/blinded_messages_40.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "amount": 8, 4 | "B_": "031a392b384f8e2184ff77d0560e416b07f0cfe45c6e15273ddcbe84e6f87c4727", 5 | "id": "00ffd48b8f5ecf80" 6 | }, 7 | { 8 | "amount": 32, 9 | "B_": "03d763081ef9afdf11fe9d6114f4c494d53c28a78e9471faa1814ad336a6c234de", 10 | "id": "00ffd48b8f5ecf80" 11 | } 12 | ] 13 | -------------------------------------------------------------------------------- /moksha-mint/src/fixtures/blinded_messages_blank_4000.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "amount": 1, 4 | "B_": "036a08d1174c0b0405cf21e2aa31de36085dcdd527f38aa701728ac36ece40099e", 5 | "id": "00ffd48b8f5ecf80" 6 | }, 7 | { 8 | "amount": 1, 9 | "B_": "024153ab08aaca0622453504076fbac48d7db691582a2b1ae41486038ed96809ab", 10 | "id": "00ffd48b8f5ecf80" 11 | }, 12 | { 13 | "amount": 1, 14 | "B_": "02f7c3636710cbad8702778067cc376368884878c12093272d8567aebe7ef681bd", 15 | "id": "00ffd48b8f5ecf80" 16 | }, 17 | { 18 | "amount": 1, 19 | "B_": "02100e486e2742356d5d8f19391416f2eea9d4894f157743ea1c8300bc48eb8a65", 20 | "id": "00ffd48b8f5ecf80" 21 | }, 22 | { 23 | "amount": 1, 24 | "B_": "03a6e88ff14bc5db30ed8e7b5c076299cc17db94aef0455d04f4bd8c7982759722", 25 | "id": "00ffd48b8f5ecf80" 26 | }, 27 | { 28 | "amount": 1, 29 | "B_": "030ab2c23379d807f22c926a4ab09aabd0a1db55bd96cacf9e229530578eff6132", 30 | "id": "00ffd48b8f5ecf80" 31 | }, 32 | { 33 | "amount": 1, 34 | "B_": "031e66678307276ed027f172b952107579f6e9c7b22cb685c4347d5a4a9036a537", 35 | "id": "00ffd48b8f5ecf80" 36 | }, 37 | { 38 | "amount": 1, 39 | "B_": "02b267d1806d7ec893db2066115823e0018e5a7a3452ab7a06f95904567d484211", 40 | "id": "00ffd48b8f5ecf80" 41 | }, 42 | { 43 | "amount": 1, 44 | "B_": "021be658384127f40360f2370e3bd3d99440a87bebdbb2c2e144d9bac67a6321f4", 45 | "id": "00ffd48b8f5ecf80" 46 | }, 47 | { 48 | "amount": 1, 49 | "B_": "02b6eb0a2efff85f90781eebd3319d917c3c36bf4e984c81b3bf680fcdc1afc83b", 50 | "id": "00ffd48b8f5ecf80" 51 | }, 52 | { 53 | "amount": 1, 54 | "B_": "0202311c8541f880514d2fa608a2de6f579fec007fc0b2c92aa52e524fb612ec20", 55 | "id": "00ffd48b8f5ecf80" 56 | }, 57 | { 58 | "amount": 1, 59 | "B_": "03dfa943f4287e83e270aad5d5cbd91c1ed2e28aaa124d67501f5f74554b6fddac", 60 | "id": "00ffd48b8f5ecf80" 61 | } 62 | ] 63 | -------------------------------------------------------------------------------- /moksha-mint/src/fixtures/post_swap_request_64_20.json: -------------------------------------------------------------------------------- 1 | { 2 | "inputs": [ 3 | { 4 | "amount": 64, 5 | "secret": "sYYrrhUD3IwJzGFCGsUqqXXa", 6 | "C": "0359760ad29ae24cd8535d83d9dcf09b585d36e0649235354aa7001e60206b3a66", 7 | "id": "paFbO142_sui" 8 | } 9 | ], 10 | "outputs": [ 11 | { 12 | "amount": 4, 13 | "B_": "021b20f742d4735760e8dc9e89c99dbd9be9b6ec3edb4b8424c5b5a2c08063f96c", 14 | "id": "00ffd48b8f5ecf80" 15 | }, 16 | { 17 | "amount": 8, 18 | "B_": "031a392b384f8e2184ff77d0560e416b07f0cfe45c6e15273ddcbe84e6f87c4727", 19 | "id": "00ffd48b8f5ecf80" 20 | }, 21 | { 22 | "amount": 32, 23 | "B_": "03d763081ef9afdf11fe9d6114f4c494d53c28a78e9471faa1814ad336a6c234de", 24 | "id": "00ffd48b8f5ecf80" 25 | }, 26 | { 27 | "amount": 4, 28 | "B_": "03e997c205b170ed2f88c26f61559733144a55da6e66334b0f4a030b708a49e5ab", 29 | "id": "00ffd48b8f5ecf80" 30 | }, 31 | { 32 | "amount": 16, 33 | "B_": "02364fe16667a049eb6dbdf4a8db23c250822fb8bc9806f4b82cc100ab00872959", 34 | "id": "00ffd48b8f5ecf80" 35 | } 36 | ] 37 | } 38 | -------------------------------------------------------------------------------- /moksha-mint/src/fixtures/post_swap_request_duplicate_key.json: -------------------------------------------------------------------------------- 1 | { 2 | "inputs": [ 3 | { 4 | "amount": 64, 5 | "secret": "sYYrrhUD3IwJzGFCGsUqqXXa", 6 | "C": "0359760ad29ae24cd8535d83d9dcf09b585d36e0649235354aa7001e60206b3a66", 7 | "id": "paFbO142_sui" 8 | } 9 | ], 10 | "outputs": [ 11 | { 12 | "amount": 4, 13 | "B_": "021b20f742d4735760e8dc9e89c99dbd9be9b6ec3edb4b8424c5b5a2c08063f96c", 14 | "id": "00ffd48b8f5ecf80" 15 | }, 16 | { 17 | "amount": 8, 18 | "B_": "031a392b384f8e2184ff77d0560e416b07f0cfe45c6e15273ddcbe84e6f87c4727", 19 | "id": "00ffd48b8f5ecf80" 20 | }, 21 | { 22 | "amount": 32, 23 | "B_": "03d763081ef9afdf11fe9d6114f4c494d53c28a78e9471faa1814ad336a6c234de", 24 | "id": "00ffd48b8f5ecf80" 25 | }, 26 | { 27 | "amount": 4, 28 | "B_": "03e997c205b170ed2f88c26f61559733144a55da6e66334b0f4a030b708a49e5ab", 29 | "id": "00ffd48b8f5ecf80" 30 | }, 31 | { 32 | "amount": 16, 33 | "B_": "03e997c205b170ed2f88c26f61559733144a55da6e66334b0f4a030b708a49e5ab", 34 | "id": "00ffd48b8f5ecf80" 35 | } 36 | ] 37 | } 38 | -------------------------------------------------------------------------------- /moksha-mint/src/fixtures/token_60.cashu: -------------------------------------------------------------------------------- 1 | cashuAeyJ0b2tlbiI6W3sibWludCI6Imh0dHA6Ly8xMjcuMC4wLjE6MzMzOCIsInByb29mcyI6W3siYW1vdW50Ijo0LCJzZWNyZXQiOiJzR3Z3OVZwalpqNGQ0YnFFU3FvQzdwTWEiLCJDIjoiMDM3YmQ2MGY2YWE1ZTE5ZjZhOWVjMzU5MjlkOGViN2E2Yzk1Y2YyOTM5NTlmMzMzNTQzYWQ5MWIxNTkyNWU2OTE1IiwiaWQiOiJtUjlQSjNNempMMXkifSx7ImFtb3VudCI6OCwic2VjcmV0IjoiQjJqNmw4Z1VUYjIxR0hqMFRnbUNRUjZHIiwiQyI6IjAyOTQzYmI0MWY4MmY3MGE2MWIwMzM0ZGU1YjJjZjNmYzc0YmI2ZTlhZTY5OWVlMzc4YjYyMzc3ZTVhMWJiZmM5ZCIsImlkIjoibVI5UEozTXpqTDF5In0seyJhbW91bnQiOjE2LCJzZWNyZXQiOiJ2SFRHbGJoRXFBQUdEUVBteFBkczc1MFkiLCJDIjoiMDI4NDU0OGJkN2FiNjhmNTIyNzdkOTQxYTgwN2JmZjJlZWI4ZjNmY2EzYmVlODY2ODgxN2RjYTg3MGJhOGQxYWJkIiwiaWQiOiJtUjlQSjNNempMMXkifSx7ImFtb3VudCI6MzIsInNlY3JldCI6IldSajZCTXVQNTQyTFpmWXdiTldlbTJLaCIsIkMiOiIwMzc5NWE0NGUwNGY1YWU5MGYyZGIwZTkzYzc3MzJkMDJkYTQ0ZGIxZmRkMWYzNDlkN2EwMzJmN2U5OGZkYzZjYzQiLCJpZCI6Im1SOVBKM016akwxeSJ9XX1dfQ== -------------------------------------------------------------------------------- /moksha-mint/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod btconchain; 2 | pub mod config; 3 | pub mod database; 4 | pub mod error; 5 | pub mod lightning; 6 | pub mod mint; 7 | pub mod model; 8 | mod routes; 9 | pub mod server; 10 | pub mod url_serialize; 11 | -------------------------------------------------------------------------------- /moksha-mint/src/lightning/alby.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::{self, Formatter}; 2 | 3 | use async_trait::async_trait; 4 | use clap::Parser; 5 | use hyper::{header::CONTENT_TYPE, http::HeaderValue}; 6 | use serde::{Deserialize, Serialize}; 7 | use url::Url; 8 | 9 | use crate::{ 10 | error::MokshaMintError, 11 | model::{CreateInvoiceParams, CreateInvoiceResult, PayInvoiceResult}, 12 | }; 13 | 14 | use super::{error::LightningError, Lightning}; 15 | 16 | #[derive(Deserialize, Serialize, Debug, Clone, Default, Parser)] 17 | pub struct AlbyLightningSettings { 18 | #[clap(long, env = "MINT_ALBY_API_KEY")] 19 | pub api_key: Option, 20 | } 21 | 22 | impl fmt::Display for AlbyLightningSettings { 23 | fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { 24 | write!(f, "api_key: {}", self.api_key.as_ref().unwrap(),) 25 | } 26 | } 27 | 28 | impl AlbyLightningSettings { 29 | pub fn new(api_key: &str) -> Self { 30 | Self { 31 | api_key: Some(api_key.to_owned()), 32 | } 33 | } 34 | } 35 | 36 | #[derive(Clone)] 37 | pub struct AlbyLightning { 38 | pub client: AlbyClient, 39 | } 40 | 41 | impl AlbyLightning { 42 | pub fn new(api_key: String) -> Self { 43 | Self { 44 | client: AlbyClient::new(&api_key).expect("Can not create Alby client"), 45 | } 46 | } 47 | } 48 | #[async_trait] 49 | impl Lightning for AlbyLightning { 50 | async fn is_invoice_paid(&self, invoice: String) -> Result { 51 | let decoded_invoice = self.decode_invoice(invoice).await?; 52 | Ok(self 53 | .client 54 | .is_invoice_paid(&decoded_invoice.payment_hash().to_string()) 55 | .await?) 56 | } 57 | 58 | async fn create_invoice(&self, amount: u64) -> Result { 59 | Ok(self 60 | .client 61 | .create_invoice(&CreateInvoiceParams { 62 | amount, 63 | unit: "sat".to_string(), 64 | memo: None, 65 | expiry: Some(10000), 66 | webhook: None, 67 | internal: None, 68 | }) 69 | .await?) 70 | } 71 | 72 | async fn pay_invoice( 73 | &self, 74 | payment_request: String, 75 | ) -> Result { 76 | self.client 77 | .pay_invoice(&payment_request) 78 | .await 79 | .map_err(|err| MokshaMintError::PayInvoice(payment_request, err)) 80 | } 81 | } 82 | 83 | #[derive(Clone)] 84 | pub struct AlbyClient { 85 | api_key: String, 86 | alby_url: Url, 87 | reqwest_client: reqwest::Client, 88 | } 89 | 90 | impl AlbyClient { 91 | pub fn new(api_key: &str) -> Result { 92 | let alby_url = Url::parse("https://api.getalby.com")?; 93 | 94 | let reqwest_client = reqwest::Client::builder().build()?; 95 | 96 | Ok(Self { 97 | api_key: api_key.to_owned(), 98 | alby_url, 99 | reqwest_client, 100 | }) 101 | } 102 | } 103 | 104 | impl AlbyClient { 105 | pub async fn make_get(&self, endpoint: &str) -> Result { 106 | let url = self.alby_url.join(endpoint)?; 107 | let response = self 108 | .reqwest_client 109 | .get(url) 110 | .bearer_auth(self.api_key.clone()) 111 | .send() 112 | .await?; 113 | 114 | // Alby API returns a 404 for invoices that aren't settled yet 115 | // if response.status() == reqwest::StatusCode::NOT_FOUND { 116 | // return Err(LightningError::NotFound); 117 | // } 118 | 119 | Ok(response.text().await?) 120 | } 121 | 122 | pub async fn make_post(&self, endpoint: &str, body: &str) -> Result { 123 | let url = self.alby_url.join(endpoint)?; 124 | let response = self 125 | .reqwest_client 126 | .post(url) 127 | .bearer_auth(self.api_key.clone()) 128 | .header( 129 | CONTENT_TYPE, 130 | HeaderValue::from_str("application/json").expect("Invalid header value"), 131 | ) 132 | .body(body.to_string()) 133 | .send() 134 | .await?; 135 | 136 | if response.status() == reqwest::StatusCode::NOT_FOUND { 137 | return Err(LightningError::NotFound); 138 | } 139 | 140 | if response.status() == reqwest::StatusCode::UNAUTHORIZED { 141 | return Err(LightningError::Unauthorized); 142 | } 143 | 144 | Ok(response.text().await?) 145 | } 146 | } 147 | 148 | impl AlbyClient { 149 | pub async fn create_invoice( 150 | &self, 151 | params: &CreateInvoiceParams, 152 | ) -> Result { 153 | let params = serde_json::json!({ 154 | "amount": params.amount, 155 | "description": params.memo, 156 | }); 157 | 158 | let body = self 159 | .make_post("invoices", &serde_json::to_string(¶ms)?) 160 | .await?; 161 | 162 | let response: serde_json::Value = serde_json::from_str(&body)?; 163 | let payment_request = response["payment_request"] 164 | .as_str() 165 | .expect("payment_request is empty") 166 | .to_owned(); 167 | let payment_hash = response["payment_hash"] 168 | .as_str() 169 | .expect("payment_hash is empty") 170 | .to_owned(); 171 | 172 | Ok(CreateInvoiceResult { 173 | payment_hash: payment_hash.as_bytes().to_vec(), 174 | payment_request, 175 | }) 176 | } 177 | 178 | pub async fn pay_invoice(&self, bolt11: &str) -> Result { 179 | let body = self 180 | .make_post( 181 | "payments/bolt11", 182 | &serde_json::to_string(&serde_json::json!({ "invoice": bolt11 }))?, 183 | ) 184 | .await?; 185 | 186 | let response: serde_json::Value = serde_json::from_str(&body)?; 187 | 188 | Ok(PayInvoiceResult { 189 | payment_hash: response["payment_hash"] 190 | .as_str() 191 | .expect("payment_hash is empty") 192 | .to_owned(), 193 | total_fees: 0, // FIXME alby does not return fees at the moment 194 | }) 195 | } 196 | 197 | pub async fn is_invoice_paid(&self, payment_hash: &str) -> Result { 198 | let body = self.make_get(&format!("invoices/{payment_hash}")).await?; 199 | Ok(serde_json::from_str::(&body)?["settled"] 200 | .as_bool() 201 | .unwrap_or(false)) 202 | } 203 | } 204 | -------------------------------------------------------------------------------- /moksha-mint/src/lightning/error.rs: -------------------------------------------------------------------------------- 1 | #[derive(Debug, thiserror::Error)] 2 | pub enum LightningError { 3 | #[error("reqwest error: {0}")] 4 | ReqwestError(#[from] reqwest::Error), 5 | 6 | #[error("url error: {0}")] 7 | UrlError(#[from] url::ParseError), 8 | 9 | #[error("serde error: {0}")] 10 | SerdeError(#[from] serde_json::Error), 11 | 12 | #[error("Not found")] 13 | NotFound, 14 | 15 | #[error("Unauthorized")] 16 | Unauthorized, 17 | 18 | #[error("Payment failed")] 19 | PaymentFailed, 20 | } 21 | -------------------------------------------------------------------------------- /moksha-mint/src/lightning/lnd.rs: -------------------------------------------------------------------------------- 1 | #![allow(clippy::blocks_in_conditions)] 2 | use std::{ 3 | fmt::{self, Formatter}, 4 | path::PathBuf, 5 | sync::Arc, 6 | }; 7 | 8 | use crate::{ 9 | error::MokshaMintError, 10 | model::{CreateInvoiceResult, PayInvoiceResult}, 11 | url_serialize::{deserialize_url, serialize_url}, 12 | }; 13 | use async_trait::async_trait; 14 | use clap::Parser; 15 | use fedimint_tonic_lnd::Client; 16 | use serde::{Deserialize, Serialize}; 17 | use tokio::sync::{MappedMutexGuard, Mutex, MutexGuard}; 18 | use tracing::{debug, instrument}; 19 | use url::Url; 20 | 21 | use super::Lightning; 22 | 23 | #[derive(Deserialize, Serialize, Debug, Clone, Default, Parser)] 24 | pub struct LndLightningSettings { 25 | #[clap(long, env = "MINT_LND_GRPC_HOST")] 26 | #[serde(serialize_with = "serialize_url", deserialize_with = "deserialize_url")] 27 | pub grpc_host: Option, 28 | 29 | #[clap(long, env = "MINT_LND_TLS_CERT_PATH")] 30 | pub tls_cert_path: Option, 31 | 32 | #[clap(long, env = "MINT_LND_MACAROON_PATH")] 33 | pub macaroon_path: Option, 34 | } 35 | 36 | impl LndLightningSettings { 37 | pub fn new(grpc_host: Url, tls_cert_path: PathBuf, macaroon_path: PathBuf) -> Self { 38 | Self { 39 | grpc_host: Some(grpc_host), 40 | tls_cert_path: Some(tls_cert_path), 41 | macaroon_path: Some(macaroon_path), 42 | } 43 | } 44 | } 45 | 46 | impl fmt::Display for LndLightningSettings { 47 | fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { 48 | write!( 49 | f, 50 | "grpc_host: {}, tls_cert_path: {}, macaroon_path: {}", 51 | self.grpc_host.as_ref().unwrap(), 52 | self.tls_cert_path 53 | .as_ref() 54 | .unwrap() // FIXME unwrap 55 | .to_str() 56 | .unwrap_or_default(), 57 | self.macaroon_path 58 | .as_ref() 59 | .unwrap() 60 | .to_str() 61 | .unwrap_or_default() 62 | ) 63 | } 64 | } 65 | 66 | pub struct LndLightning(Arc>); 67 | 68 | impl LndLightning { 69 | pub async fn new( 70 | address: Url, 71 | cert_file: &PathBuf, 72 | macaroon_file: &PathBuf, 73 | ) -> Result { 74 | let client = 75 | fedimint_tonic_lnd::connect(address.to_string(), cert_file, &macaroon_file).await; 76 | 77 | Ok(Self(Arc::new(Mutex::new( 78 | client.map_err(MokshaMintError::ConnectError)?, 79 | )))) 80 | } 81 | 82 | pub async fn client_lock( 83 | &self, 84 | ) -> Result, MokshaMintError> { 85 | let guard = self.0.lock().await; 86 | Ok(MutexGuard::map(guard, |client| client.lightning())) 87 | } 88 | } 89 | 90 | #[async_trait] 91 | impl Lightning for LndLightning { 92 | #[instrument(skip(self), err)] 93 | async fn is_invoice_paid(&self, payment_request: String) -> Result { 94 | let invoice = self.decode_invoice(payment_request).await?; 95 | let payment_hash: &[u8] = invoice.payment_hash().as_ref(); 96 | 97 | let invoice_request = fedimint_tonic_lnd::lnrpc::PaymentHash { 98 | r_hash: payment_hash.to_vec(), 99 | ..Default::default() 100 | }; 101 | 102 | let invoice = self 103 | .client_lock() 104 | .await? 105 | .lookup_invoice(fedimint_tonic_lnd::tonic::Request::new(invoice_request)) 106 | .await? 107 | .into_inner(); 108 | 109 | Ok(invoice.state == fedimint_tonic_lnd::lnrpc::invoice::InvoiceState::Settled as i32) 110 | } 111 | 112 | #[instrument(skip(self), err)] 113 | async fn create_invoice(&self, amount: u64) -> Result { 114 | let invoice_request = fedimint_tonic_lnd::lnrpc::Invoice { 115 | value: amount as i64, 116 | ..Default::default() 117 | }; 118 | 119 | let invoice = self 120 | .client_lock() 121 | .await? 122 | .add_invoice(fedimint_tonic_lnd::tonic::Request::new(invoice_request)) 123 | .await? 124 | .into_inner(); 125 | 126 | Ok(CreateInvoiceResult { 127 | payment_hash: invoice.r_hash, 128 | payment_request: invoice.payment_request, 129 | }) 130 | } 131 | 132 | #[instrument(skip(self), err)] 133 | async fn pay_invoice( 134 | &self, 135 | payment_request: String, 136 | ) -> Result { 137 | let pay_req = fedimint_tonic_lnd::lnrpc::SendRequest { 138 | payment_request, 139 | ..Default::default() 140 | }; 141 | let payment_response = self 142 | .client_lock() 143 | .await? 144 | .send_payment_sync(fedimint_tonic_lnd::tonic::Request::new(pay_req)) 145 | .await? 146 | .into_inner(); 147 | 148 | let total_fees = payment_response 149 | .payment_route 150 | .map_or(0, |route| route.total_fees_msat / 1_000) as u64; 151 | 152 | debug!("lnd total_fees: {}", total_fees); 153 | 154 | Ok(PayInvoiceResult { 155 | payment_hash: hex::encode(payment_response.payment_hash), 156 | total_fees, 157 | }) 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /moksha-mint/src/lightning/mod.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | error::MokshaMintError, 3 | model::{CreateInvoiceResult, PayInvoiceResult}, 4 | }; 5 | use async_trait::async_trait; 6 | use lightning_invoice::Bolt11Invoice as LNInvoice; 7 | use serde::{Deserialize, Serialize}; 8 | use std::fmt::{self, Formatter}; 9 | 10 | pub mod alby; 11 | pub mod cln; 12 | pub mod error; 13 | pub mod lnbits; 14 | pub mod lnd; 15 | pub mod strike; 16 | 17 | #[cfg(test)] 18 | use mockall::automock; 19 | use std::str::FromStr; 20 | 21 | use self::lnd::LndLightningSettings; 22 | use self::{ 23 | alby::AlbyLightningSettings, cln::ClnLightningSettings, lnbits::LnbitsLightningSettings, 24 | strike::StrikeLightningSettings, 25 | }; 26 | 27 | #[derive(Debug, Clone, Serialize, Deserialize)] 28 | #[serde(tag = "type")] 29 | pub enum LightningType { 30 | Lnbits(LnbitsLightningSettings), 31 | Alby(AlbyLightningSettings), 32 | Strike(StrikeLightningSettings), 33 | Lnd(LndLightningSettings), 34 | Cln(ClnLightningSettings), 35 | } 36 | 37 | impl fmt::Display for LightningType { 38 | fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { 39 | match self { 40 | Self::Lnbits(settings) => write!(f, "Lnbits: {}", settings), 41 | Self::Alby(settings) => write!(f, "Alby: {}", settings), 42 | Self::Strike(settings) => write!(f, "Strike: {}", settings), 43 | Self::Lnd(settings) => write!(f, "Lnd: {}", settings), 44 | Self::Cln(settings) => write!(f, "Cln: {}", settings), 45 | } 46 | } 47 | } 48 | 49 | #[cfg_attr(test, automock)] 50 | #[async_trait] 51 | pub trait Lightning: Send + Sync { 52 | async fn is_invoice_paid(&self, invoice: String) -> Result; 53 | async fn create_invoice(&self, amount: u64) -> Result; 54 | async fn pay_invoice( 55 | &self, 56 | payment_request: String, 57 | ) -> Result; 58 | 59 | async fn decode_invoice(&self, payment_request: String) -> Result { 60 | LNInvoice::from_str(&payment_request) 61 | .map_err(|err| MokshaMintError::DecodeInvoice(payment_request, err)) 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /moksha-mint/src/model.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | 3 | #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] 4 | pub struct Invoice { 5 | pub amount: u64, 6 | pub payment_request: String, 7 | } 8 | 9 | impl Invoice { 10 | pub const fn new(amount: u64, payment_request: String) -> Self { 11 | Self { 12 | amount, 13 | payment_request, 14 | } 15 | } 16 | } 17 | 18 | #[derive(Debug, Serialize, Deserialize)] 19 | pub struct CreateInvoiceResult { 20 | pub payment_hash: Vec, 21 | pub payment_request: String, 22 | } 23 | 24 | #[derive(Debug, Serialize, Deserialize)] 25 | pub struct PayInvoiceResult { 26 | pub payment_hash: String, 27 | /// total fees in sat 28 | pub total_fees: u64, 29 | } 30 | 31 | #[derive(Debug, Serialize, Deserialize)] 32 | pub struct CreateInvoiceParams { 33 | pub amount: u64, 34 | pub unit: String, 35 | pub memo: Option, 36 | pub expiry: Option, 37 | pub webhook: Option, 38 | pub internal: Option, 39 | } 40 | -------------------------------------------------------------------------------- /moksha-mint/src/routes/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod btconchain; 2 | pub mod default; 3 | -------------------------------------------------------------------------------- /moksha-mint/src/url_serialize.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Deserializer, Serializer}; 2 | use url::Url; 3 | 4 | pub fn deserialize_url<'de, D>(deserializer: D) -> Result, D::Error> 5 | where 6 | D: Deserializer<'de>, 7 | { 8 | let url_str: Option = Option::deserialize(deserializer)?; 9 | url_str.map_or_else( 10 | || Ok(None), 11 | |s| Url::parse(&s).map_err(serde::de::Error::custom).map(Some), 12 | ) 13 | } 14 | 15 | pub fn serialize_url(url: &Option, serializer: S) -> Result 16 | where 17 | S: Serializer, 18 | { 19 | match url { 20 | Some(url) => serializer.serialize_str(url.as_str()), 21 | None => serializer.serialize_none(), 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /moksha-wallet/.sqlx/query-0913842a79a647c1190faa24d948ff27f74e24d8c2ca37c10861c2e128905013.json: -------------------------------------------------------------------------------- 1 | { 2 | "db_name": "SQLite", 3 | "query": "INSERT INTO keysets (keyset_id, mint_url, currency_unit, last_index, public_keys, active) VALUES ($1, $2, $3, $4, $5, $6)\n ON CONFLICT(keyset_id, mint_url) DO UPDATE SET currency_unit = $3, public_keys = $5, active = $6;\n ", 4 | "describe": { 5 | "columns": [], 6 | "parameters": { 7 | "Right": 6 8 | }, 9 | "nullable": [] 10 | }, 11 | "hash": "0913842a79a647c1190faa24d948ff27f74e24d8c2ca37c10861c2e128905013" 12 | } 13 | -------------------------------------------------------------------------------- /moksha-wallet/.sqlx/query-1aa7a37640cde629ef99ebb746875208743b9fc168fa8cffd848b671014b8bf1.json: -------------------------------------------------------------------------------- 1 | { 2 | "db_name": "SQLite", 3 | "query": "SELECT keyset_id, amount, C, secret FROM proofs;", 4 | "describe": { 5 | "columns": [ 6 | { 7 | "name": "keyset_id", 8 | "ordinal": 0, 9 | "type_info": "Text" 10 | }, 11 | { 12 | "name": "amount", 13 | "ordinal": 1, 14 | "type_info": "Integer" 15 | }, 16 | { 17 | "name": "C", 18 | "ordinal": 2, 19 | "type_info": "Text" 20 | }, 21 | { 22 | "name": "secret", 23 | "ordinal": 3, 24 | "type_info": "Text" 25 | } 26 | ], 27 | "parameters": { 28 | "Right": 0 29 | }, 30 | "nullable": [ 31 | false, 32 | false, 33 | false, 34 | false 35 | ] 36 | }, 37 | "hash": "1aa7a37640cde629ef99ebb746875208743b9fc168fa8cffd848b671014b8bf1" 38 | } 39 | -------------------------------------------------------------------------------- /moksha-wallet/.sqlx/query-609877b640a209756b504ef93f9d0a41bca0c9c0eece9ec91a50195412d9adae.json: -------------------------------------------------------------------------------- 1 | { 2 | "db_name": "SQLite", 3 | "query": "SELECT id, mint_url, keyset_id, currency_unit, active, last_index, public_keys FROM keysets;", 4 | "describe": { 5 | "columns": [ 6 | { 7 | "name": "id", 8 | "ordinal": 0, 9 | "type_info": "Integer" 10 | }, 11 | { 12 | "name": "mint_url", 13 | "ordinal": 1, 14 | "type_info": "Text" 15 | }, 16 | { 17 | "name": "keyset_id", 18 | "ordinal": 2, 19 | "type_info": "Text" 20 | }, 21 | { 22 | "name": "currency_unit", 23 | "ordinal": 3, 24 | "type_info": "Text" 25 | }, 26 | { 27 | "name": "active", 28 | "ordinal": 4, 29 | "type_info": "Bool" 30 | }, 31 | { 32 | "name": "last_index", 33 | "ordinal": 5, 34 | "type_info": "Integer" 35 | }, 36 | { 37 | "name": "public_keys", 38 | "ordinal": 6, 39 | "type_info": "Text" 40 | } 41 | ], 42 | "parameters": { 43 | "Right": 0 44 | }, 45 | "nullable": [ 46 | false, 47 | false, 48 | false, 49 | false, 50 | false, 51 | false, 52 | false 53 | ] 54 | }, 55 | "hash": "609877b640a209756b504ef93f9d0a41bca0c9c0eece9ec91a50195412d9adae" 56 | } 57 | -------------------------------------------------------------------------------- /moksha-wallet/.sqlx/query-6caae4e19877ee8ab21df304f5d0945ae771fe2b59169958bdc9ce7c0eb73090.json: -------------------------------------------------------------------------------- 1 | { 2 | "db_name": "SQLite", 3 | "query": "INSERT INTO proofs (keyset_id, amount, C, secret, time_created) VALUES ($1, $2, $3, $4, CURRENT_TIMESTAMP);", 4 | "describe": { 5 | "columns": [], 6 | "parameters": { 7 | "Right": 4 8 | }, 9 | "nullable": [] 10 | }, 11 | "hash": "6caae4e19877ee8ab21df304f5d0945ae771fe2b59169958bdc9ce7c0eb73090" 12 | } 13 | -------------------------------------------------------------------------------- /moksha-wallet/.sqlx/query-7373a18c132ed16ac8ae383f008ebd53e8d588375b06e40335e91da39c680ad3.json: -------------------------------------------------------------------------------- 1 | { 2 | "db_name": "SQLite", 3 | "query": "SELECT seed_words FROM seed;", 4 | "describe": { 5 | "columns": [ 6 | { 7 | "name": "seed_words", 8 | "ordinal": 0, 9 | "type_info": "Text" 10 | } 11 | ], 12 | "parameters": { 13 | "Right": 0 14 | }, 15 | "nullable": [ 16 | false 17 | ] 18 | }, 19 | "hash": "7373a18c132ed16ac8ae383f008ebd53e8d588375b06e40335e91da39c680ad3" 20 | } 21 | -------------------------------------------------------------------------------- /moksha-wallet/.sqlx/query-c08a68cb103a8fe16fddbe23d163a04db5b9639ea87a8a1472ccca5d5194afc6.json: -------------------------------------------------------------------------------- 1 | { 2 | "db_name": "SQLite", 3 | "query": "UPDATE keysets SET last_index = $1 WHERE id = $2;", 4 | "describe": { 5 | "columns": [], 6 | "parameters": { 7 | "Right": 2 8 | }, 9 | "nullable": [] 10 | }, 11 | "hash": "c08a68cb103a8fe16fddbe23d163a04db5b9639ea87a8a1472ccca5d5194afc6" 12 | } 13 | -------------------------------------------------------------------------------- /moksha-wallet/.sqlx/query-e99d7276a6bb8d0b0f456e8d00ec38e1192ef3f25f9204550ed30aa3a0bd7e3f.json: -------------------------------------------------------------------------------- 1 | { 2 | "db_name": "SQLite", 3 | "query": "INSERT INTO seed (seed_words) VALUES ($1);", 4 | "describe": { 5 | "columns": [], 6 | "parameters": { 7 | "Right": 1 8 | }, 9 | "nullable": [] 10 | }, 11 | "hash": "e99d7276a6bb8d0b0f456e8d00ec38e1192ef3f25f9204550ed30aa3a0bd7e3f" 12 | } 13 | -------------------------------------------------------------------------------- /moksha-wallet/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "moksha-wallet" 3 | version = "0.2.1" 4 | edition = "2021" 5 | resolver = "2" 6 | repository = "https://github.com/ngutech21/moksha" 7 | license = "MIT" 8 | description = "cashu-wallet library" 9 | 10 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 11 | 12 | [lib] 13 | name = "moksha_wallet" 14 | path = "src/lib.rs" 15 | 16 | [dependencies] 17 | anyhow = { workspace = true, features = ["backtrace"] } 18 | secp256k1 = { version = "0.29.0", default-features = false, features = ["serde"] } 19 | moksha-core = { version = "0.2.1", path = "../moksha-core" } 20 | serde_json = { workspace = true } 21 | serde = { workspace = true } 22 | thiserror = { workspace = true } 23 | async-trait = { workspace = true } 24 | lightning-invoice = "0.31.0" 25 | url = { workspace = true } 26 | dirs = { workspace = true } 27 | bip32 = { workspace = true, features = ["secp256k1", "std"] } 28 | bip39 = { workspace = true } 29 | hex = { workspace = true } 30 | rand = { workspace = true } 31 | 32 | [target.'cfg(target_family = "wasm")'.dependencies] 33 | gloo-net = { version = "0.5.0" } 34 | serde-wasm-bindgen = "0.6.5" 35 | wasm-bindgen = "0.2.92" 36 | rexie = "0.5.0" 37 | tokio = { workspace = true, features = ["rt", "sync"] } 38 | 39 | [target.'cfg(not(target_family="wasm"))'.dependencies] 40 | reqwest = { workspace = true, features = ["json", "rustls-tls"], default-features = false } 41 | tokio = { workspace = true, features = ["rt", "rt-multi-thread", "macros"] } 42 | sqlx = { workspace = true, default-features = false, features = ["sqlite", "runtime-tokio", "tls-rustls", "migrate", "macros", "json"] } 43 | 44 | [dev-dependencies] 45 | tempfile = { workspace = true } 46 | mockall = { workspace = true } 47 | -------------------------------------------------------------------------------- /moksha-wallet/examples/receive_tokens.rs: -------------------------------------------------------------------------------- 1 | use std::env::temp_dir; 2 | 3 | use moksha_core::token::TokenV3; 4 | use moksha_wallet::{ 5 | http::CrossPlatformHttpClient, localstore::sqlite::SqliteLocalStore, wallet::Wallet, 6 | }; 7 | use std::str::FromStr; 8 | use url::Url; 9 | 10 | #[tokio::main] 11 | async fn main() -> anyhow::Result<()> { 12 | let db_path = temp_dir().join("wallet.db").to_str().unwrap().to_string(); 13 | let localstore = SqliteLocalStore::with_path(db_path).await?; 14 | 15 | let wallet: Wallet<_, CrossPlatformHttpClient> = Wallet::builder() 16 | .with_localstore(localstore) 17 | .build() 18 | .await?; 19 | let wallet_keysets = wallet 20 | .add_mint_keysets(&Url::parse("https://mint.mutinynet.moksha.cash")?) 21 | .await?; 22 | let wallet_keyset = wallet_keysets.first().unwrap(); 23 | // FIXME add better filtering by CurrencyUnit 24 | 25 | let tokens = TokenV3::from_str("cashuAeyJ0b2tlbiI6IFt7InByb29mcyI6IFt7ImlkIjogIjAwOTkxZjRmMjc3MzMzOGMiLCAiYW1vdW50IjogMiwgInNlY3JldCI6ICI5ZmFjZWE0Y2QzN2I3ZWRlOGE4NmQzYWY1ZWIxZTczNzIxMDNmZDE2YTQ1M2E5NDQ5YjE0MDFkZDhhMzAzMWJiIiwgIkMiOiAiMDM2ZTVhOWJhOWE1ZjYxZmQ5MTk3YzM2OTgzZjc1YzAzYTUyYzc0YTJmZmM2NTBmNzg5MjJlMDcyZWY1MTI0YjZlIn1dLCAibWludCI6ICJodHRwczovL21pbnQubXV0aW55bmV0Lm1va3NoYS5jYXNoOjMzMzgifV19")?; 26 | wallet.receive_tokens(wallet_keyset, &tokens).await?; 27 | let balance = wallet.get_balance().await?; 28 | println!("New balance: {} sats", balance); 29 | Ok(()) 30 | } 31 | -------------------------------------------------------------------------------- /moksha-wallet/migrations/20230530061910_init.sql: -------------------------------------------------------------------------------- 1 | -- Add migration script here 2 | CREATE TABLE IF NOT EXISTS proofs ( 3 | keyset_id TEXT NOT NULL, 4 | amount INTEGER NOT NULL, 5 | C TEXT NOT NULL, 6 | secret TEXT NOT NULL, 7 | time_created TIMESTAMP, 8 | UNIQUE (secret) 9 | ); 10 | 11 | CREATE TABLE IF NOT EXISTS keysets ( 12 | id INTEGER PRIMARY KEY AUTOINCREMENT, 13 | mint_url TEXT NOT NULL, 14 | keyset_id TEXT NOT NULL, 15 | currency_unit TEXT NOT NULL, 16 | active BOOL NOT NULL DEFAULT TRUE, 17 | last_index INTEGER NOT NULL, 18 | public_keys TEXT NOT NULL CHECK (json_valid(public_keys)), 19 | UNIQUE (keyset_id, mint_url) 20 | ); -------------------------------------------------------------------------------- /moksha-wallet/migrations/20240329082342_deterministic_secrets.sql: -------------------------------------------------------------------------------- 1 | -- Add migration script here 2 | CREATE TABLE seed ( 3 | id INTEGER PRIMARY KEY CHECK (id = 1), 4 | seed_words TEXT NOT NULL 5 | -- other columns 6 | ); -------------------------------------------------------------------------------- /moksha-wallet/src/client/mod.rs: -------------------------------------------------------------------------------- 1 | use async_trait::async_trait; 2 | use moksha_core::{ 3 | blind::BlindedMessage, 4 | keyset::Keysets, 5 | primitives::{ 6 | CurrencyUnit, KeysResponse, MintInfoResponse, PostMeltBolt11Response, 7 | PostMeltBtcOnchainResponse, PostMeltQuoteBolt11Response, PostMeltQuoteBtcOnchainResponse, 8 | PostMintBolt11Response, PostMintBtcOnchainResponse, PostMintQuoteBolt11Response, 9 | PostMintQuoteBtcOnchainResponse, PostSwapResponse, 10 | }, 11 | proof::Proofs, 12 | }; 13 | 14 | use url::Url; 15 | 16 | use crate::error::MokshaWalletError; 17 | 18 | pub mod crossplatform; 19 | 20 | #[cfg(test)] 21 | use mockall::automock; 22 | 23 | #[cfg_attr(test, automock)] 24 | #[async_trait(?Send)] 25 | pub trait CashuClient { 26 | async fn get_keys(&self, mint_url: &Url) -> Result; 27 | 28 | async fn get_keys_by_id( 29 | &self, 30 | mint_url: &Url, 31 | keyset_id: String, 32 | ) -> Result; 33 | 34 | async fn get_keysets(&self, mint_url: &Url) -> Result; 35 | 36 | async fn post_swap( 37 | &self, 38 | mint_url: &Url, 39 | proofs: Proofs, 40 | output: Vec, 41 | ) -> Result; 42 | 43 | async fn post_melt_bolt11( 44 | &self, 45 | mint_url: &Url, 46 | proofs: Proofs, 47 | quote: String, 48 | outputs: Vec, 49 | ) -> Result; 50 | 51 | async fn post_melt_quote_bolt11( 52 | &self, 53 | mint_url: &Url, 54 | payment_request: String, 55 | unit: CurrencyUnit, 56 | ) -> Result; 57 | 58 | async fn get_melt_quote_bolt11( 59 | &self, 60 | mint_url: &Url, 61 | quote: String, 62 | ) -> Result; 63 | 64 | async fn post_mint_bolt11( 65 | &self, 66 | mint_url: &Url, 67 | quote: String, 68 | blinded_messages: Vec, 69 | ) -> Result; 70 | 71 | async fn post_mint_quote_bolt11( 72 | &self, 73 | mint_url: &Url, 74 | amount: u64, 75 | unit: CurrencyUnit, 76 | ) -> Result; 77 | 78 | async fn get_mint_quote_bolt11( 79 | &self, 80 | mint_url: &Url, 81 | quote: String, 82 | ) -> Result; 83 | 84 | async fn get_info(&self, mint_url: &Url) -> Result; 85 | 86 | async fn is_v1_supported(&self, mint_url: &Url) -> Result; 87 | 88 | async fn post_mint_onchain( 89 | &self, 90 | mint_url: &Url, 91 | quote: String, 92 | blinded_messages: Vec, 93 | ) -> Result; 94 | 95 | async fn post_mint_quote_onchain( 96 | &self, 97 | mint_url: &Url, 98 | amount: u64, 99 | unit: CurrencyUnit, 100 | ) -> Result; 101 | 102 | async fn get_mint_quote_onchain( 103 | &self, 104 | mint_url: &Url, 105 | quote: String, 106 | ) -> Result; 107 | 108 | async fn post_melt_onchain( 109 | &self, 110 | mint_url: &Url, 111 | proofs: Proofs, 112 | quote: String, 113 | ) -> Result; 114 | 115 | async fn post_melt_quote_onchain( 116 | &self, 117 | mint_url: &Url, 118 | address: String, 119 | amount: u64, 120 | unit: CurrencyUnit, 121 | ) -> Result, MokshaWalletError>; 122 | 123 | async fn get_melt_quote_onchain( 124 | &self, 125 | mint_url: &Url, 126 | quote: String, 127 | ) -> Result; 128 | } 129 | -------------------------------------------------------------------------------- /moksha-wallet/src/config_path.rs: -------------------------------------------------------------------------------- 1 | use dirs::home_dir; 2 | use std::{fs::create_dir, path::PathBuf}; 3 | 4 | pub const ENV_DB_PATH: &str = "WALLET_DB_PATH"; 5 | 6 | /// Returns the path to the wallet database file. 7 | /// 8 | /// The path is determined by the value of the `WALLET_DB_PATH` environment variable. If the 9 | /// variable is not set, the function creates a `.moksha` directory in the user's home directory 10 | /// and returns a path to a `wallet.db` file in that directory. 11 | /// 12 | /// # Examples 13 | /// 14 | /// ``` 15 | /// let db_path = moksha_wallet::config_path::db_path(); 16 | /// println!("Database path: {}", db_path); 17 | /// ``` 18 | pub fn db_path() -> String { 19 | std::env::var(ENV_DB_PATH).unwrap_or_else(|_| { 20 | let home = home_dir() 21 | .expect("home dir not found") 22 | .to_str() 23 | .expect("home dir is invalid") 24 | .to_owned(); 25 | // in a sandboxed environment on mac the path looks like 26 | // /Users/$USER_NAME/Library/Containers/..... so we have are just ising the first 2 parts 27 | let home = home 28 | .split('/') 29 | .take(3) 30 | .collect::>() 31 | .join(std::path::MAIN_SEPARATOR_STR); 32 | let moksha_dir = format!("{}{}.moksha", home, std::path::MAIN_SEPARATOR); 33 | 34 | if !std::path::Path::new(&moksha_dir).exists() { 35 | create_dir(std::path::Path::new(&moksha_dir)).expect("failed to create .moksha dir"); 36 | } 37 | 38 | format!("{moksha_dir}/wallet.db") 39 | }) 40 | } 41 | 42 | pub fn config_dir() -> PathBuf { 43 | let home = home_dir() 44 | .expect("home dir not found") 45 | .to_str() 46 | .expect("home dir is invalid") 47 | .to_owned(); 48 | // in a sandboxed environment on mac the path looks like 49 | // /Users/$USER_NAME/Library/Containers/..... so we have are just ising the first 2 parts 50 | let home = home 51 | .split('/') 52 | .take(3) 53 | .collect::>() 54 | .join(std::path::MAIN_SEPARATOR_STR); 55 | let moksha_dir = format!("{}{}.moksha", home, std::path::MAIN_SEPARATOR); 56 | 57 | if !std::path::Path::new(&moksha_dir).exists() { 58 | create_dir(std::path::Path::new(&moksha_dir)).expect("failed to create .moksha dir"); 59 | } 60 | PathBuf::from(moksha_dir) 61 | } 62 | -------------------------------------------------------------------------------- /moksha-wallet/src/error.rs: -------------------------------------------------------------------------------- 1 | use std::string::FromUtf8Error; 2 | 3 | use lightning_invoice::ParseOrSemanticError; 4 | use thiserror::Error; 5 | 6 | #[derive(Error, Debug)] 7 | pub enum MokshaWalletError { 8 | #[cfg(target_arch = "wasm32")] 9 | #[error("GlooNetError - {0}")] 10 | GlooNet(#[from] gloo_net::Error), 11 | 12 | #[error("SerdeJsonError - {0}")] 13 | Json(#[from] serde_json::Error), 14 | 15 | #[cfg(not(target_arch = "wasm32"))] 16 | #[error("ReqwestError - {0}")] 17 | Reqwest(#[from] reqwest::Error), 18 | 19 | #[cfg(not(target_arch = "wasm32"))] 20 | #[error("InvalidHeaderValueError - {0}")] 21 | InvalidHeaderValue(#[from] reqwest::header::InvalidHeaderValue), 22 | #[error("{0}")] 23 | MintError(String), 24 | 25 | #[error("{1}")] 26 | InvoiceNotPaidYet(u64, String), 27 | 28 | #[error("UnexpectedResponse - {0}")] 29 | UnexpectedResponse(String), 30 | 31 | #[error("MokshaCoreError - {0}")] 32 | MokshaCore(#[from] moksha_core::error::MokshaCoreError), 33 | 34 | #[cfg(not(target_arch = "wasm32"))] 35 | #[error("DB Error {0}")] 36 | Db(#[from] sqlx::Error), 37 | 38 | #[cfg(not(target_arch = "wasm32"))] 39 | #[error("Migrate Error {0}")] 40 | Migrate(#[from] sqlx::migrate::MigrateError), 41 | 42 | #[cfg(not(target_arch = "wasm32"))] 43 | #[error("Sqlite Error {0}")] 44 | Sqlite(#[from] sqlx::sqlite::SqliteError), 45 | 46 | #[error("Utf8 Error {0}")] 47 | Utf8(#[from] FromUtf8Error), 48 | 49 | #[error("Invalid Proofs")] 50 | InvalidProofs, 51 | 52 | #[error("Not enough tokens")] 53 | NotEnoughTokens, 54 | 55 | #[error("Failed to decode payment request {0} - Error {1}")] 56 | DecodeInvoice(String, ParseOrSemanticError), 57 | 58 | #[error("Invalid invoice {0}")] 59 | InvalidInvoice(String), 60 | 61 | #[error("URLParseError - {0}")] 62 | Url(#[from] url::ParseError), 63 | 64 | #[error("Unsupported version: Only mints with /v1 api are supported")] 65 | UnsupportedApiVersion, 66 | 67 | #[error("Bip32Error {0}")] 68 | Bip32(#[from] bip32::Error), 69 | 70 | #[error("Bip39Error {0}")] 71 | Bip39(#[from] bip39::Error), 72 | 73 | #[error("Secp256k1 {0}")] 74 | Secp256k1(#[from] secp256k1::Error), 75 | 76 | #[error("Primarykey not set for keyset")] 77 | IdNotSet, 78 | 79 | #[error("Found multiple seeds in the database. This is not supported.")] 80 | MultipleSeeds, 81 | 82 | #[error("Not valid hex string")] 83 | Hex(#[from] hex::FromHexError), 84 | 85 | #[error("Invalid Keyset-ID")] 86 | Slice(#[from] std::array::TryFromSliceError), 87 | 88 | #[error("Pubkey not found")] 89 | PubkeyNotFound, 90 | } 91 | -------------------------------------------------------------------------------- /moksha-wallet/src/fixtures/blinded_messages_40.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "amount": 4, 4 | "B_": "021b20f742d4735760e8dc9e89c99dbd9be9b6ec3edb4b8424c5b5a2c08063f96c" 5 | }, 6 | { 7 | "amount": 8, 8 | "B_": "031a392b384f8e2184ff77d0560e416b07f0cfe45c6e15273ddcbe84e6f87c4727" 9 | }, 10 | { 11 | "amount": 32, 12 | "B_": "03d763081ef9afdf11fe9d6114f4c494d53c28a78e9471faa1814ad336a6c234de" 13 | }, 14 | { 15 | "amount": 4, 16 | "B_": "03e997c205b170ed2f88c26f61559733144a55da6e66334b0f4a030b708a49e5ab" 17 | }, 18 | { 19 | "amount": 16, 20 | "B_": "02364fe16667a049eb6dbdf4a8db23c250822fb8bc9806f4b82cc100ab00872959" 21 | } 22 | ] 23 | -------------------------------------------------------------------------------- /moksha-wallet/src/fixtures/post_melt_quote_response.json: -------------------------------------------------------------------------------- 1 | { 2 | "quote": "721883C1-55D4-4F50-9717-8F12624FCAAC", 3 | "amount": 21, 4 | "fee_reserve": 0, 5 | "paid": false, 6 | "expiry": 1704786797 7 | } 8 | -------------------------------------------------------------------------------- /moksha-wallet/src/fixtures/post_melt_response_21.json: -------------------------------------------------------------------------------- 1 | { 2 | "paid": true, 3 | "payment_preimage": "08bb470aefc4252e3121905951365b445eccb6452619459139814de684fd311f", 4 | "change": [] 5 | } 6 | -------------------------------------------------------------------------------- /moksha-wallet/src/fixtures/post_melt_response_not_paid.json: -------------------------------------------------------------------------------- 1 | { 2 | "paid": false, 3 | "payment_preimage": "", 4 | "change": [] 5 | } 6 | -------------------------------------------------------------------------------- /moksha-wallet/src/fixtures/post_mint_response_20.json: -------------------------------------------------------------------------------- 1 | { 2 | "signatures": [ 3 | { 4 | "amount": 4, 5 | "C_": "02feef7133bc63fd6f7f82093b20afe83324ccfd78db928db5d1c4e9b2665880c7", 6 | "id": "mR9PJ3MzjL1y" 7 | }, 8 | { 9 | "amount": 16, 10 | "C_": "02d139b22bf0ad547eca2f29cc22975d8ee0dede388b51a562fd9f3de3ddd0c787", 11 | "id": "mR9PJ3MzjL1y" 12 | } 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /moksha-wallet/src/fixtures/post_swap_response_24_40.json: -------------------------------------------------------------------------------- 1 | { 2 | "signatures": [ 3 | { 4 | "amount": 4, 5 | "C_": "03b2e736e1280f1e64eedf3fc53eeb5fc74e6f1d1664e3b2b7b8e2934afd908673", 6 | "id": "mR9PJ3MzjL1y" 7 | }, 8 | { 9 | "amount": 16, 10 | "C_": "02aa7c77dad18fb2c18107b32f0f725b7075c9c6c5be049941b18c7b497a1ea21a", 11 | "id": "mR9PJ3MzjL1y" 12 | }, 13 | { 14 | "amount": 4, 15 | "C_": "036614b4844efe234e0ec2293938a84a42b6e803126b365074943dd338f813421f", 16 | "id": "mR9PJ3MzjL1y" 17 | }, 18 | { 19 | "amount": 8, 20 | "C_": "03859164602a27319bcd5c377bec90eda6f9d5d9e9c7987fce5bff4b69151a122f", 21 | "id": "mR9PJ3MzjL1y" 22 | }, 23 | { 24 | "amount": 32, 25 | "C_": "03eb55564312308159f1dbc0a48d05f669b9f9ad370b3cd1ea1d714c99de67aa8a", 26 | "id": "mR9PJ3MzjL1y" 27 | } 28 | ] 29 | } 30 | -------------------------------------------------------------------------------- /moksha-wallet/src/fixtures/pub_keys.json: -------------------------------------------------------------------------------- 1 | { 2 | "274877906944": "0231a0f33fd88070476f532aa2683a8fb39b32b8dc985cf25a2b743a9fc639d3cc", 3 | "288230376151711744": "03d8fa072b61c0d8aeba16cd8b0c059d75b23a5bed807b5b761d3619f8359777fe", 4 | "137438953472": "03771cb3626fd30e3f0c3298d2c7cbca3d2c8ab1efbbde64a3a8cf2cc7b96dd021", 5 | "1099511627776": "029048f5b684b123c0a0f34c7068ec17fc9dbd643660f5af1534a00ef72d3f5ce8", 6 | "256": "02b9e0be7b423bded7d60ff7114549de8d2d5b9c099edf8887aff474037e4bc0cf", 7 | "1125899906842624": "03112f972fa86340a41e97688654a385eae3f75590c98d51faea8d8b6e6608c0c6", 8 | "16384": "024fad3b0b60c6b71d848deac173183fae8ddde31bbde531f18ab23473ddff541d", 9 | "131072": "032ff6491cdeff0bf9b34acd5deceef3cca7682b5f94dbe3068af8bb3b5aa34b81", 10 | "4194304": "0359023fb85f6e6ede0141ab8f4a1277c19ed62b49b8ef5c5e2c8ca7fefe9b2f91", 11 | "1024": "0267fc1dabac016f46b3d1a650d97b56f3e56540106720f3d24ff7a6e9cd7183e9", 12 | "144115188075855872": "0236aea75081004b34945d0595204c1a7aa83f3b2d2eb34d384adfcddacf272dfa", 13 | "4611686018427387904": "02fd5ea0eaf57cf6457e381651b47c6ce33f738b1f9e43abe4aeea8d502ad188d8", 14 | "1073741824": "02d17d61027602432a8484b65e6d6063ed9157c51ce92099d61ac2820411c59f9f", 15 | "268435456": "032cba9068638965ccc3870c140c72a1b028a820851f36fe59639e7ab3093a8ffd", 16 | "34359738368": "0268b289c13f1a11932f04f0b5868a72f603d48cfe82a2681957adaeb29b893bac", 17 | "72057594037927936": "025fd8187973f1afd4e0a80938e0ce45d500af53850171362d57764717251465aa", 18 | "18014398509481984": "02ce56a1a7cf7a647957ab24aa704bb4632a4db12c30a96bc0f00f00e5d4a5ac2e", 19 | "1152921504606846976": "0309790f8a3f1bcfb636ae895e3cd64068d554f40e5a14c36acb184dfba03519d7", 20 | "2251799813685248": "02d6b6ae9c10b9c9f8fb869be051e3516d2f379270ff8c3d7c1c7df7155162e76e", 21 | "140737488355328": "02f955aee6d21ecd5b61b436f541c9cdb7063f8145a0b14cb57ded3f051c911b26", 22 | "1": "02f71e2d93aa95fc52b938735a24774ad926406c81e9dc9d2aa699fb89281548fd", 23 | "4096": "02f607a9eed310825c2d2e66d6e64fb237fe21b640b9a66cc7646b2a6480d91457", 24 | "70368744177664": "03b182854b9ca812db91b81c18c8198b791f03bb2667b7ba518952c88ea5b3e1e5", 25 | "8": "020fd24fbd552445df70c244be2af77da2b2f634ccfda9e9620b347b5cd50dbdd8", 26 | "8796093022208": "0340a7e3fce8ebc7e5d854dcf043ef2bdc4fcba61765eea44d8e516b6ff4e2eddc", 27 | "512": "0320454cc41e646f49e1ac0a62b9667c80dee45545b045575f2a26f01770dc2521", 28 | "524288": "029d3c751c7d1c3e1d3e4b7791e1e809f6dedf2c28e172a82967d49a14b7c26ce2", 29 | "562949953421312": "03e68ced3e52805b6c9962e57e1c80db26785f7f3d71c53accdb9af9d4a324c6e9", 30 | "4294967296": "0396d1924256cf94b7972b4f4aba70c18cb7547724b78edbd1f3f2a89add2623c8", 31 | "16": "03ef9ef2515df5c0d0851ed9419a24a571ef5e03206d9d2fc6572ac050c5afe1aa", 32 | "2": "03b28dd9c19aaf1ec847be31b60c6a5e1a6cb6f87434afcdb0d9348ba0e2bdb150", 33 | "134217728": "023834651da0737f484a77204c2d06543fb65ad2dd8d095a2be48ca12ebf2664ec", 34 | "8589934592": "023c132cf942ba0307fbc4e7e0c70f6d1e29739cbb0feef8b78a05c11fef322f3d", 35 | "2305843009213693952": "033021e71a54be133ffddc6011f8498180bd628b0e9da7c19bc374138b36634a32", 36 | "32768": "020d195466819d96d8c7eee9150565b7bd37196c7d12d0e96e389f56be8aebb44b", 37 | "33554432": "028a673a53e78aa8c992128e21efb3b33fbd54de20afcf81a67e69eaf2bab7e0e9", 38 | "2048": "035a9a25251a4da56f49667ca50677470fc6d8e186a875ab7b32aa064eb9e9e948", 39 | "8388608": "0353d3ae1dad05e1b46ab85a366bfcdb7a645e3457f7714003e0fb06f4d75f4d87", 40 | "32": "02dbd455474176b30234c178573e874cc79d0c2fc1920cf0e9f133204cf43299c1", 41 | "17179869184": "03f2f6d0b1278c7230aa3c8701f79264f8df133d6f2100d780a94e4b91c03172f6", 42 | "1048576": "03b4a41d39cc6f2a8925f694c514e107b87d7ddb8f5ac55c9e4b7895139d0decd8", 43 | "64": "0237c1eb11b8a214cca3e0104684227952188039a05cd55c1ad3896a572c70a7c3", 44 | "35184372088832": "02c80da12de468c3265e0b81d28678e390e63af1206128b0771e5136789f77c7ea", 45 | "2147483648": "0236870e39b3a739d5caa04988dce432e3d7988420f04d9b415125af22672e2726", 46 | "2199023255552": "02ac666c376f146ff5cd334266f2559dfda7cd8df64d737c9100f88126048c711d", 47 | "2097152": "02d4abbce491f87656eb0d2e66ef18eb009f6320169ef12e66703298d5395f2b91", 48 | "4398046511104": "03619b9a25fd26ff0aa3f36ed1a436752a6ec080caca6500a1cd56ec3934547511", 49 | "16777216": "032d0847606465b97f15aca30c69f5baeeb43bf6188b4679f723119ce6fb9708c5", 50 | "9007199254740992": "0207ebe787eca4e1d3caea3dd5622ad7ad0eb60e489d50ba6a96cd34f29b75da75", 51 | "4503599627370496": "035bfe141afcc58708bbd0c3048f7d833a8b087e62082b064da0affdaa1d4eb0e1", 52 | "549755813888": "03ef599e761c4b6301efeb10b1584a89d1efa70e434c93a837e918026fc8ad5c9f", 53 | "36028797018963968": "026ef25bde77d66e1109dc36e4273ea74d73e0d05b8e308bccc1ff948b88d7980f", 54 | "68719476736": "02a982ca9bac0c2df5addac9758be0016ae583cdb200fa0033c419b44194ab44ff", 55 | "576460752303423488": "03b69e3a23c017ad1fd150124cd831c0477ed39993b9eef6bbb1b6c4d26ed27aac", 56 | "536870912": "03eae5e4b22dfa5ad77476c925717dc4e005da78142e75b47fb28569d745483af3", 57 | "281474976710656": "02996d995371da914023905f2389c26a937f348c8bafc95565b526d1fc56d919f8", 58 | "8192": "033346f7dce2ef71a80c5d657a8930bdd19c7c1708d03829daf43f20eaeda76768", 59 | "65536": "038c9bf295a745726c38d14988851d68d201296a802c296faa838000c2f44d25e0", 60 | "262144": "02570090f5b6900955fd794d8f22c23fb35fc87fa03069b9b16bea63ea7cda419a", 61 | "4": "03ede0e704e223e764a82f73984b0fec0fdbde15ef57b4de95b527f7182af7487e", 62 | "17592186044416": "038bfb34eac31626b5bc0f17d99b7f36047ad1b63c26a6895e2a7156a46a536672", 63 | "9223372036854775808": "0391810b7edacc6c9e88722c3aa4e1e6f94dc135d358e5383d72ac3c81e549e505", 64 | "128": "02655041771766b94a269f9f1ec1860f2eade55bb472c4db74ac1257ef54aac52b", 65 | "67108864": "0278b66e140559352bb5aeca854a6466bc439ee206a9f349ed7926aae4335269b7" 66 | } 67 | -------------------------------------------------------------------------------- /moksha-wallet/src/fixtures/token_60.cashu: -------------------------------------------------------------------------------- 1 | cashuAeyJ0b2tlbiI6W3sibWludCI6Imh0dHA6Ly8xMjcuMC4wLjE6MzMzOCIsInByb29mcyI6W3siYW1vdW50Ijo0LCJzZWNyZXQiOiJzR3Z3OVZwalpqNGQ0YnFFU3FvQzdwTWEiLCJDIjoiMDM3YmQ2MGY2YWE1ZTE5ZjZhOWVjMzU5MjlkOGViN2E2Yzk1Y2YyOTM5NTlmMzMzNTQzYWQ5MWIxNTkyNWU2OTE1IiwiaWQiOiJtUjlQSjNNempMMXkifSx7ImFtb3VudCI6OCwic2VjcmV0IjoiQjJqNmw4Z1VUYjIxR0hqMFRnbUNRUjZHIiwiQyI6IjAyOTQzYmI0MWY4MmY3MGE2MWIwMzM0ZGU1YjJjZjNmYzc0YmI2ZTlhZTY5OWVlMzc4YjYyMzc3ZTVhMWJiZmM5ZCIsImlkIjoibVI5UEozTXpqTDF5In0seyJhbW91bnQiOjE2LCJzZWNyZXQiOiJ2SFRHbGJoRXFBQUdEUVBteFBkczc1MFkiLCJDIjoiMDI4NDU0OGJkN2FiNjhmNTIyNzdkOTQxYTgwN2JmZjJlZWI4ZjNmY2EzYmVlODY2ODgxN2RjYTg3MGJhOGQxYWJkIiwiaWQiOiJtUjlQSjNNempMMXkifSx7ImFtb3VudCI6MzIsInNlY3JldCI6IldSajZCTXVQNTQyTFpmWXdiTldlbTJLaCIsIkMiOiIwMzc5NWE0NGUwNGY1YWU5MGYyZGIwZTkzYzc3MzJkMDJkYTQ0ZGIxZmRkMWYzNDlkN2EwMzJmN2U5OGZkYzZjYzQiLCJpZCI6Im1SOVBKM016akwxeSJ9XX1dfQ== -------------------------------------------------------------------------------- /moksha-wallet/src/fixtures/token_64.cashu: -------------------------------------------------------------------------------- 1 | cashuAeyJ0b2tlbiI6W3sibWludCI6Imh0dHA6Ly8xMjcuMC4wLjE6MzMzOCIsInByb29mcyI6W3siYW1vdW50Ijo2NCwic2VjcmV0IjoibXpkdzJFRUszOGptSXdGQ0x6OWJISGZEIiwiQyI6IjAzNGRiOTU2Zjg0OTE3ZGRhMmRhMDgzNTc2OGFkZTUzOWFjMzhjZjA0MmZhYWY4NDk3NTJjNWE3N2I5YmIwOGQ2ZCIsImlkIjoicGFGYk8xNDJfc3VpIn1dfV19 -------------------------------------------------------------------------------- /moksha-wallet/src/http/mod.rs: -------------------------------------------------------------------------------- 1 | #[cfg(not(target_arch = "wasm32"))] 2 | pub mod reqwest; 3 | 4 | #[cfg(target_arch = "wasm32")] 5 | pub mod wasm; 6 | 7 | #[derive(Debug, Clone)] 8 | pub struct CrossPlatformHttpClient { 9 | #[cfg(not(target_arch = "wasm32"))] 10 | client: ::reqwest::Client, 11 | } 12 | 13 | impl Default for CrossPlatformHttpClient { 14 | fn default() -> Self { 15 | Self::new() 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /moksha-wallet/src/http/reqwest.rs: -------------------------------------------------------------------------------- 1 | use super::CrossPlatformHttpClient; 2 | use crate::error::MokshaWalletError; 3 | use reqwest::{ 4 | header::{HeaderValue, CONTENT_TYPE}, 5 | Response, StatusCode, 6 | }; 7 | use serde_json::Value; 8 | use url::Url; 9 | 10 | impl CrossPlatformHttpClient { 11 | pub fn new() -> Self { 12 | Self { 13 | client: reqwest::Client::new(), 14 | } 15 | } 16 | 17 | async fn extract_response_data( 18 | response: Response, 19 | ) -> Result { 20 | match response.status() { 21 | StatusCode::OK => { 22 | let response_text = response.text().await?; 23 | match serde_json::from_str::(&response_text) { 24 | Ok(data) => Ok(data), 25 | Err(_) => { 26 | // FIXME cleanup code 27 | let data: Value = serde_json::from_str(&response_text) 28 | .map_err(|_| MokshaWalletError::UnexpectedResponse(response_text)) 29 | .expect("invalid value"); 30 | let detail = data["detail"].as_str().expect("detail not found"); 31 | // let data = serde_json::from_str::(&response_text) 32 | // .map_err(|_| MokshaWalletError::UnexpectedResponse(response_text)) 33 | // .unwrap(); 34 | 35 | // FIXME: use the error code to return a proper error 36 | match detail { 37 | "Lightning invoice not paid yet." => { 38 | Err(MokshaWalletError::InvoiceNotPaidYet(0, detail.to_owned())) 39 | } 40 | _ => Err(MokshaWalletError::MintError(detail.to_owned())), 41 | } 42 | } 43 | } 44 | } 45 | _ => { 46 | let response_text = response.text().await?; 47 | let data: Value = serde_json::from_str(&response_text) 48 | .map_err(|_| MokshaWalletError::UnexpectedResponse(response_text)) 49 | .expect("invalid value"); 50 | let detail = data["detail"].as_str().expect("detail not found"); 51 | 52 | // FIXME: use the error code to return a proper error 53 | match detail { 54 | "Lightning invoice not paid yet." => { 55 | Err(MokshaWalletError::InvoiceNotPaidYet(0, detail.to_owned())) 56 | } 57 | _ => Err(MokshaWalletError::MintError(detail.to_owned())), 58 | } 59 | } 60 | } 61 | } 62 | 63 | pub async fn do_get( 64 | &self, 65 | url: &Url, 66 | ) -> Result { 67 | let resp = self.client.get(url.clone()).send().await?; 68 | Self::extract_response_data::(resp).await 69 | } 70 | 71 | pub async fn do_post( 72 | &self, 73 | url: &Url, 74 | body: &B, 75 | ) -> Result { 76 | let resp = self 77 | .client 78 | .post(url.clone()) 79 | .header(CONTENT_TYPE, HeaderValue::from_str("application/json")?) 80 | .body(serde_json::to_string(body)?) 81 | .send() 82 | .await?; 83 | Self::extract_response_data::(resp).await 84 | } 85 | 86 | pub async fn get_status(&self, url: &Url) -> Result { 87 | let resp = self.client.get(url.to_owned()).send().await?; 88 | Ok(resp.status().as_u16()) 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /moksha-wallet/src/http/wasm.rs: -------------------------------------------------------------------------------- 1 | use moksha_core::primitives::CashuErrorResponse; 2 | 3 | use crate::error::MokshaWalletError; 4 | use url::Url; 5 | 6 | use super::CrossPlatformHttpClient; 7 | use gloo_net::http::{Request, Response}; 8 | 9 | impl CrossPlatformHttpClient { 10 | pub fn new() -> Self { 11 | Self {} 12 | } 13 | 14 | pub async fn do_get( 15 | &self, 16 | url: &Url, 17 | ) -> Result { 18 | let resp = Request::get(url.as_str()).send().await?; 19 | Self::extract_response_data::(resp).await 20 | } 21 | 22 | pub async fn do_post( 23 | &self, 24 | url: &Url, 25 | body: &B, 26 | ) -> Result { 27 | let resp = Request::post(url.as_str()) 28 | .header("content-type", "application/json") 29 | .json(body)? 30 | .send() 31 | .await?; 32 | Self::extract_response_data::(resp).await 33 | } 34 | 35 | pub async fn get_status(&self, url: &Url) -> Result { 36 | let resp = Request::get(url.as_str()).send().await?; 37 | 38 | Ok(resp.status()) 39 | } 40 | 41 | async fn extract_response_data( 42 | response: Response, 43 | ) -> Result { 44 | match response.status() { 45 | 200 => { 46 | let response_text = response.text().await.unwrap(); // FIXME handle error 47 | match serde_json::from_str::(&response_text) { 48 | Ok(data) => Ok(data), 49 | Err(_) => { 50 | let data = serde_json::from_str::(&response_text) 51 | .map_err(|_| MokshaWalletError::UnexpectedResponse(response_text)) 52 | .unwrap(); 53 | 54 | // FIXME: use the error code to return a proper error 55 | match data.detail.as_str() { 56 | "Lightning invoice not paid yet." => { 57 | Err(MokshaWalletError::InvoiceNotPaidYet(data.code, data.detail)) 58 | } 59 | _ => Err(MokshaWalletError::MintError(data.detail)), 60 | } 61 | } 62 | } 63 | } 64 | _ => { 65 | let txt = response.text().await.unwrap(); // FIXME handle error 66 | let data = serde_json::from_str::(&txt) 67 | .map_err(|_| MokshaWalletError::UnexpectedResponse(txt)) 68 | .unwrap(); 69 | 70 | // FIXME: use the error code to return a proper error 71 | match data.detail.as_str() { 72 | "Lightning invoice not paid yet." => { 73 | Err(MokshaWalletError::InvoiceNotPaidYet(data.code, data.detail)) 74 | } 75 | _ => Err(MokshaWalletError::MintError(data.detail)), 76 | } 77 | } 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /moksha-wallet/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod client; 2 | pub mod config_path; 3 | pub mod error; 4 | pub mod http; 5 | pub mod localstore; 6 | pub mod secret; 7 | pub mod wallet; 8 | -------------------------------------------------------------------------------- /moksha-wallet/src/localstore/mod.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | use async_trait::async_trait; 4 | use moksha_core::{keyset::KeysetId, primitives::CurrencyUnit, proof::Proofs}; 5 | use secp256k1::PublicKey; 6 | use url::Url; 7 | 8 | use crate::error::MokshaWalletError; 9 | 10 | #[cfg(not(target_arch = "wasm32"))] 11 | pub mod sqlite; 12 | 13 | #[cfg(target_arch = "wasm32")] 14 | pub mod rexie; 15 | 16 | #[derive(Debug, Clone)] 17 | pub struct WalletKeyset { 18 | /// primary key 19 | pub id: Option, 20 | pub keyset_id: KeysetId, 21 | pub mint_url: Url, 22 | pub currency_unit: CurrencyUnit, 23 | /// last index used for deriving keys from the master key 24 | pub last_index: u64, 25 | pub public_keys: HashMap, 26 | pub active: bool, 27 | } 28 | 29 | impl WalletKeysetFilter for Vec { 30 | fn get_active(&self, mint_url: &Url, currency_unit: &CurrencyUnit) -> Option<&WalletKeyset> { 31 | self.iter() 32 | .find(|k| k.mint_url == *mint_url && k.currency_unit == *currency_unit && k.active) 33 | } 34 | } 35 | 36 | pub trait WalletKeysetFilter { 37 | fn get_active(&self, mint_url: &Url, currency_unit: &CurrencyUnit) -> Option<&WalletKeyset>; 38 | } 39 | 40 | impl WalletKeyset { 41 | pub fn new( 42 | keyset_id: &KeysetId, 43 | mint_url: &Url, 44 | currency_unit: &CurrencyUnit, 45 | last_index: u64, 46 | public_keys: HashMap, 47 | active: bool, 48 | ) -> Self { 49 | Self { 50 | id: None, 51 | keyset_id: keyset_id.to_owned(), 52 | mint_url: mint_url.to_owned(), 53 | currency_unit: currency_unit.clone(), 54 | last_index, 55 | public_keys, 56 | active, 57 | } 58 | } 59 | } 60 | 61 | #[cfg(not(target_arch = "wasm32"))] 62 | #[async_trait(?Send)] 63 | pub trait LocalStore { 64 | type DB: sqlx::Database; 65 | async fn begin_tx(&self) -> Result, MokshaWalletError>; 66 | async fn delete_proofs( 67 | &self, 68 | tx: &mut sqlx::Transaction, 69 | proofs: &Proofs, 70 | ) -> Result<(), MokshaWalletError>; 71 | async fn add_proofs( 72 | &self, 73 | tx: &mut sqlx::Transaction, 74 | proofs: &Proofs, 75 | ) -> Result<(), MokshaWalletError>; 76 | async fn get_proofs( 77 | &self, 78 | tx: &mut sqlx::Transaction, 79 | ) -> Result; 80 | 81 | async fn get_keysets( 82 | &self, 83 | tx: &mut sqlx::Transaction, 84 | ) -> Result, MokshaWalletError>; 85 | async fn upsert_keyset( 86 | &self, 87 | tx: &mut sqlx::Transaction, 88 | keyset: &WalletKeyset, 89 | ) -> Result<(), MokshaWalletError>; 90 | 91 | async fn update_keyset_last_index( 92 | &self, 93 | tx: &mut sqlx::Transaction, 94 | keyset: &WalletKeyset, 95 | ) -> Result<(), MokshaWalletError>; 96 | 97 | async fn add_seed( 98 | &self, 99 | tx: &mut sqlx::Transaction, 100 | seed_words: &str, 101 | ) -> Result<(), MokshaWalletError>; 102 | 103 | async fn get_seed( 104 | &self, 105 | tx: &mut sqlx::Transaction, 106 | ) -> Result, MokshaWalletError>; 107 | } 108 | 109 | #[cfg(target_arch = "wasm32")] 110 | pub struct RexieTransaction {} 111 | 112 | #[cfg(target_arch = "wasm32")] 113 | impl RexieTransaction { 114 | pub async fn commit(&self) -> Result<(), MokshaWalletError> { 115 | Ok(()) 116 | } 117 | } 118 | 119 | #[cfg(target_arch = "wasm32")] 120 | #[async_trait(?Send)] 121 | pub trait LocalStore { 122 | async fn begin_tx(&self) -> Result { 123 | Ok(RexieTransaction {}) 124 | } 125 | 126 | async fn delete_proofs( 127 | &self, 128 | tx: &mut RexieTransaction, 129 | proofs: &Proofs, 130 | ) -> Result<(), MokshaWalletError>; 131 | async fn add_proofs( 132 | &self, 133 | tx: &mut RexieTransaction, 134 | proofs: &Proofs, 135 | ) -> Result<(), MokshaWalletError>; 136 | async fn get_proofs(&self, tx: &mut RexieTransaction) -> Result; 137 | 138 | async fn get_keysets( 139 | &self, 140 | _tx: &mut RexieTransaction, 141 | ) -> Result, MokshaWalletError>; 142 | 143 | async fn upsert_keyset( 144 | &self, 145 | _tx: &mut RexieTransaction, 146 | keyset: &WalletKeyset, 147 | ) -> Result<(), MokshaWalletError>; 148 | 149 | async fn update_keyset_last_index( 150 | &self, 151 | _tx: &mut RexieTransaction, 152 | keyset: &WalletKeyset, 153 | ) -> Result<(), MokshaWalletError>; 154 | 155 | async fn add_seed( 156 | &self, 157 | _tx: &mut RexieTransaction, 158 | seed_words: &str, 159 | ) -> Result<(), MokshaWalletError>; 160 | 161 | async fn get_seed( 162 | &self, 163 | _tx: &mut RexieTransaction, 164 | ) -> Result, MokshaWalletError>; 165 | } 166 | 167 | #[cfg(test)] 168 | mod tests { 169 | use std::collections::HashMap; 170 | 171 | use secp256k1::PublicKey; 172 | 173 | fn generate_test_map() -> HashMap { 174 | let mut map = HashMap::new(); 175 | let secp = secp256k1::Secp256k1::new(); 176 | 177 | for i in 0..10 { 178 | let secret_key = secp256k1::SecretKey::new(&mut secp256k1::rand::thread_rng()); 179 | let public_key = PublicKey::from_secret_key(&secp, &secret_key); 180 | map.insert(i, public_key); 181 | } 182 | 183 | map 184 | } 185 | 186 | #[test] 187 | fn test_() { 188 | //let x: HashMap, RandomState>; 189 | let data = generate_test_map(); 190 | let json = serde_json::to_string(&data).unwrap(); 191 | println!("{:?}", json); 192 | } 193 | } 194 | -------------------------------------------------------------------------------- /moksha-wallet/src/localstore/rexie.rs: -------------------------------------------------------------------------------- 1 | use super::{LocalStore, RexieTransaction, WalletKeyset}; 2 | use crate::error::MokshaWalletError; 3 | use async_trait::async_trait; 4 | use moksha_core::proof::{Proof, Proofs}; 5 | use rexie::*; 6 | use wasm_bindgen::JsValue; 7 | 8 | #[derive(Clone, Default)] 9 | pub struct RexieLocalStore; 10 | 11 | const STORE_NAME: &str = "proofs"; 12 | 13 | impl RexieLocalStore { 14 | pub async fn new() -> Self { 15 | Self {} 16 | } 17 | } 18 | 19 | impl RexieLocalStore { 20 | async fn get_rexie() -> Rexie { 21 | Rexie::builder("moksha") 22 | .version(1) 23 | .add_object_store(ObjectStore::new(STORE_NAME)) 24 | .build() 25 | .await 26 | .unwrap() 27 | } 28 | 29 | fn get_key(proof: &Proof) -> JsValue { 30 | let key = serde_json::json!({ 31 | "key": proof.secret, 32 | }); 33 | let key = serde_json::to_string(&key).unwrap(); 34 | serde_wasm_bindgen::to_value(&key).unwrap() 35 | } 36 | } 37 | 38 | #[async_trait(?Send)] 39 | impl LocalStore for RexieLocalStore { 40 | // FIXME implement tx-handling for Rexie 41 | async fn add_proofs( 42 | &self, 43 | _tx: &mut RexieTransaction, 44 | proofs: &Proofs, 45 | ) -> std::result::Result<(), MokshaWalletError> { 46 | let db = Self::get_rexie().await; 47 | 48 | for proof in proofs.proofs() { 49 | let transaction = db 50 | .transaction(&[STORE_NAME], rexie::TransactionMode::ReadWrite) 51 | .expect("db error"); 52 | let store = transaction.store(STORE_NAME).expect("db error"); 53 | let json = serde_json::to_string(&proof).unwrap(); 54 | let js_value = serde_wasm_bindgen::to_value(&json).unwrap(); 55 | 56 | store 57 | .add(&js_value, Some(&Self::get_key(&proof))) 58 | .await 59 | .expect("db store error"); 60 | transaction.done().await.expect("db error"); 61 | } 62 | 63 | Ok(()) 64 | } 65 | 66 | async fn get_proofs( 67 | &self, 68 | _tx: &mut RexieTransaction, 69 | ) -> std::result::Result { 70 | let db = Self::get_rexie().await; 71 | let transaction = db 72 | .transaction(&[STORE_NAME], rexie::TransactionMode::ReadOnly) 73 | .expect("db error"); 74 | let store = transaction.store(STORE_NAME).expect("db error"); 75 | let all = store.get_all(None, None, None, None).await; 76 | match all { 77 | Ok(all) => { 78 | let mut proofs = vec![]; 79 | for (_, proof) in all { 80 | let proof: String = serde_wasm_bindgen::from_value(proof).unwrap(); 81 | let proof = serde_json::from_str::(&proof).unwrap(); 82 | proofs.push(proof); 83 | } 84 | Ok(Proofs::new(proofs)) 85 | } 86 | Err(_) => Ok(Proofs::new(vec![])), 87 | } 88 | } 89 | 90 | async fn delete_proofs( 91 | &self, 92 | _tx: &mut RexieTransaction, 93 | proofs_to_delete: &Proofs, 94 | ) -> std::result::Result<(), MokshaWalletError> { 95 | let db = Self::get_rexie().await; 96 | 97 | for proof in proofs_to_delete.proofs() { 98 | let transaction = db 99 | .transaction(&[STORE_NAME], rexie::TransactionMode::ReadWrite) 100 | .expect("db error"); 101 | let store = transaction.store(STORE_NAME).expect("db error"); 102 | 103 | store 104 | .delete(&Self::get_key(&proof)) 105 | .await 106 | .expect("db error"); 107 | transaction.done().await.expect("db error"); 108 | } 109 | 110 | Ok(()) 111 | } 112 | 113 | async fn get_keysets( 114 | &self, 115 | _tx: &mut RexieTransaction, 116 | ) -> std::result::Result, MokshaWalletError> { 117 | todo!() 118 | } 119 | 120 | async fn upsert_keyset( 121 | &self, 122 | _tx: &mut RexieTransaction, 123 | _keyset: &WalletKeyset, 124 | ) -> std::result::Result<(), MokshaWalletError> { 125 | todo!() 126 | } 127 | 128 | async fn update_keyset_last_index( 129 | &self, 130 | _tx: &mut RexieTransaction, 131 | _keyset: &WalletKeyset, 132 | ) -> std::result::Result<(), MokshaWalletError> { 133 | todo!() 134 | } 135 | 136 | async fn add_seed( 137 | &self, 138 | _tx: &mut RexieTransaction, 139 | _seed_words: &str, 140 | ) -> std::result::Result<(), MokshaWalletError> { 141 | todo!() 142 | } 143 | 144 | async fn get_seed( 145 | &self, 146 | _tx: &mut RexieTransaction, 147 | ) -> std::result::Result, MokshaWalletError> { 148 | todo!() 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /rust-toolchain.toml: -------------------------------------------------------------------------------- 1 | [toolchain] 2 | channel = "1.83.0" 3 | components = ["clippy", "rustfmt"] 4 | -------------------------------------------------------------------------------- /typos.toml: -------------------------------------------------------------------------------- 1 | [type.cashu] 2 | extend-glob = ["*.cashu"] 3 | check-file = false 4 | 5 | [files] 6 | extend-exclude = ["build/*", "moksha-wallet/examples/receive_tokens.rs"] 7 | --------------------------------------------------------------------------------