From 6941617f5c7da2ef1fbac0c32b5a56964443a56b Mon Sep 17 00:00:00 2001 From: Brian Corbin Date: Thu, 13 Oct 2022 18:12:11 -0700 Subject: [PATCH] Release/v2.0.0 (#465) --- .github/ISSUE_TEMPLATE/bug-report.md | 32 + .github/ISSUE_TEMPLATE/feature_request.md | 20 + .github/dependabot.yml | 10 + .github/workflows/build.yml | 228 +- .github/workflows/docker-hub.yml | 2 +- .github/workflows/release.yml | 51 +- .gitignore | 6 + CONTRIBUTING.md | 2 +- Cargo.lock | 1188 +++-- Cargo.toml | 4 +- Dockerfile | 5 +- README.md | 50 +- SECURITY.md | 7 + cli/mobilecoin/cli.py | 134 +- cli/mobilecoin/client.py | 52 +- docs/SUMMARY.md | 206 +- .../export_view_only_account_package.md | 77 - docs/api-endpoints/account/README.md | 2 - docs/api-endpoints/transaction/README.md | 2 - .../api-endpoints/view-only-account/README.md | 2 - docs/dbdocs/database.dbml | 112 +- ...port_view_only_txouts_without_key_image.md | 40 - .../txo/get_txos_for_view_only_account.md | 74 - .../txo/set_view_only_txos_key_images.md | 40 - docs/tutorials/environment-setup.md | 10 +- docs/usage/no-wallet-db/no-wallet-db.md | 7 + .../accounts/account-secrets/README.md | 0 docs/{ => v1}/accounts/account/README.md | 0 docs/{ => v1}/accounts/address/README.md | 0 docs/{ => v1}/accounts/balance/README.md | 0 .../assign_address_for_account.md | 0 .../build_and_submit_transaction.md | 0 .../api-endpoints}/build_gift_code.md | 0 .../build_split_txo_transaction.md | 0 .../api-endpoints}/build_transaction.md | 0 .../api-endpoints}/check_b58_type.md | 0 .../api-endpoints}/check_gift_code_status.md | 0 .../check_receiver_receipt_status.md | 0 .../api-endpoints}/claim_gift_code.md | 0 .../api-endpoints}/create_account.md | 0 .../api-endpoints}/create_payment_request.md | 0 .../create_receiver_receipts.md | 0 .../api-endpoints}/export_account_secrets.md | 0 .../api-endpoints}/get_account.md | 0 .../api-endpoints}/get_account_status.md | 0 .../api-endpoints}/get_address_for_account.md | 0 .../get_addresses_for_account.md | 0 .../api-endpoints}/get_all_accounts.md | 0 .../api-endpoints}/get_all_gift_codes.md | 0 .../get_all_transaction_logs_for_block.md | 0 ...t_all_transaction_logs_ordered_by_block.md | 0 .../get_all_txos_for_address.md | 0 .../get_all_view_only_accounts.md | 0 .../api-endpoints}/get_balance_for_account.md | 0 .../api-endpoints}/get_balance_for_address.md | 0 .../block => v1/api-endpoints}/get_block.md | 0 .../api-endpoints}/get_confirmations.md | 0 .../api-endpoints}/get_gift_code.md | 0 .../get_mc_protocol_transaction.md | 0 .../api-endpoints}/get_mc_protocol_txo.md | 0 .../api-endpoints}/get_network_status.md | 0 .../api-endpoints}/get_transaction_log.md | 0 .../get_transaction_logs_for_account.md | 0 .../api-endpoints}/get_transaction_object.md | 0 .../txo => v1/api-endpoints}/get_txo.md | 0 .../api-endpoints}/get_txo_object.md | 0 .../api-endpoints}/get_txos_for_account.md | 0 .../api-endpoints}/get_wallet_status.md | 38 +- .../api-endpoints}/import_account.md | 0 ...unt_from_legacy_root_entropy-deprecated.md | 0 .../api-endpoints}/remove_account.md | 0 .../api-endpoints}/remove_gift_code.md | 0 .../api-endpoints}/submit_gift_code.md | 0 .../api-endpoints}/submit_transaction.md | 0 .../api-endpoints}/update_account_name.md | 0 .../api-endpoints}/validate_confirmation.md | 0 .../api-endpoints}/verify_address.md | 0 .../version => v1/api-endpoints}/version.md | 0 docs/{ => v1}/gift-codes/gift-code/README.md | 0 docs/{ => v1}/other/block/README.md | 0 docs/{ => v1}/other/network-status/README.md | 0 docs/{ => v1}/other/version/README.md | 0 docs/{ => v1}/other/wallet-status/README.md | 45 +- .../transactions/payment-request/README.md | 0 .../transaction-confirmation/README.md | 0 .../transactions/transaction-log/README.md | 0 .../transaction-receipt/README.md | 0 .../transactions/transaction/README.md | 0 docs/{ => v1}/transactions/txo/README.md | 0 docs/v2/accounts/account-secrets/README.md | 37 + docs/v2/accounts/account/README.md | 36 + docs/v2/accounts/address/README.md | 38 + docs/v2/accounts/balance/README.md | 32 + .../assign_address_for_account.md | 58 + .../build_and_submit_transaction.md | 137 + docs/v2/api-endpoints/build_transaction.md | 97 + .../build_unsigned_transaction.md | 35 +- docs/v2/api-endpoints/check_b58_type.md | 50 + .../check_receiver_receipt_status.md | 81 + docs/v2/api-endpoints/create_account.md | 53 + .../api-endpoints/create_payment_request.md | 54 + .../api-endpoints/create_receiver_receipts.md | 82 + ...create_view_only_account_import_request.md | 53 + .../create_view_only_account_sync_request.md | 6 +- .../api-endpoints/export_account_secrets.md | 67 + docs/v2/api-endpoints/get_account_status.md | 67 + docs/v2/api-endpoints/get_accounts.md | 69 + .../api-endpoints/get_address_for_account.md | 53 + docs/v2/api-endpoints/get_address_status.md | 69 + docs/v2/api-endpoints/get_addresses.md | 59 + docs/v2/api-endpoints/get_block.md | 94 + docs/v2/api-endpoints/get_confirmations.md | 57 + .../get_mc_protocol_transaction.md | 45 + docs/v2/api-endpoints/get_mc_protocol_txo.md | 67 + docs/v2/api-endpoints/get_network_status.md | 47 + docs/v2/api-endpoints/get_transaction_log.md | 63 + docs/v2/api-endpoints/get_transaction_logs.md | 125 + docs/v2/api-endpoints/get_txo.md | 64 + docs/v2/api-endpoints/get_txo_block_index.md | 60 + .../get_txo_membership_proofs.md | 237 + docs/v2/api-endpoints/get_txos.md | 151 + docs/v2/api-endpoints/get_wallet_status.md | 86 + docs/v2/api-endpoints/import_account.md | 67 + ...import_account_from_legacy_root_entropy.md | 73 + .../api-endpoints/import_view_only_account.md | 68 + .../api-endpoints/remove_account.md} | 10 +- docs/v2/api-endpoints/sample_mixins.md | 414 ++ docs/v2/api-endpoints/submit_transaction.md | 289 ++ .../api-endpoints/sync_view_only_account.md | 40 + docs/v2/api-endpoints/update_account_name.md | 55 + .../v2/api-endpoints/validate_confirmation.md | 47 + docs/v2/api-endpoints/verify_address.md | 45 + docs/v2/api-endpoints/version.md | 40 + docs/v2/other/block/README.md | 16 + docs/v2/other/network-status/README.md | 14 + docs/v2/other/version/README.md | 15 + docs/v2/other/wallet-status/README.md | 72 + .../v2/transactions/payment-request/README.md | 6 + .../transaction-confirmation/README.md | 29 + .../v2/transactions/transaction-log/README.md | 188 + .../transaction-receipt/README.md | 33 + docs/v2/transactions/transaction/README.md | 42 + docs/v2/transactions/txo/README.md | 106 + .../account-secrets/README.md | 26 - .../export_view_only_account_secrets.md | 54 - docs/view-only-accounts/account/README.md | 37 - .../account/get_view_only_account.md | 61 - .../account/import_view_only_account.md | 83 - .../account/update_view_only_account_name.md | 52 - docs/view-only-accounts/balance/README.md | 32 - .../get_balance_for_view_only_account.md | 55 - .../get_balance_for_view_only_address.md | 53 - .../create_new_subaddress_request.md | 41 - ...mport_subaddresses_to_view_only_account.md | 56 - docs/view-only-accounts/syncing/README.md | 0 .../syncing/sync_view_only_account.md | 55 - full-service/Cargo.toml | 36 +- .../2022-06-13-204000_api_v3/down.sql | 0 .../2022-06-13-204000_api_v3/up.sql | 107 + full-service/src/bin/main.rs | 67 +- full-service/src/config.rs | 22 +- full-service/src/db/account.rs | 451 +- full-service/src/db/assigned_subaddress.rs | 382 +- full-service/src/db/gift_code.rs | 25 +- .../db/migration_testing/migration_testing.rs | 59 - full-service/src/db/migration_testing/mod.rs | 5 - .../src/db/migration_testing/seed_accounts.rs | 48 - .../db/migration_testing/seed_gift_codes.rs | 165 - .../src/db/migration_testing/seed_txos.rs | 122 - full-service/src/db/mod.rs | 6 - full-service/src/db/models.rs | 387 +- full-service/src/db/schema.rs | 121 +- full-service/src/db/transaction_log.rs | 1237 +++-- full-service/src/db/txo.rs | 1635 ++++--- full-service/src/db/view_only_account.rs | 308 -- full-service/src/db/view_only_subaddress.rs | 163 - full-service/src/db/view_only_txo.rs | 745 --- full-service/src/db/wallet_db.rs | 93 +- full-service/src/db/wallet_db_error.rs | 15 + full-service/src/error.rs | 34 +- full-service/src/fog_resolver.rs | 56 - full-service/src/json_rpc/account_secrets.rs | 78 - full-service/src/json_rpc/amount.rs | 73 - full-service/src/json_rpc/e2e.rs | 4041 ----------------- full-service/src/json_rpc/json_rpc_request.rs | 307 -- .../src/json_rpc/json_rpc_response.rs | 256 +- full-service/src/json_rpc/mod.rs | 27 +- full-service/src/json_rpc/transaction_log.rs | 141 - full-service/src/json_rpc/v1/api/mod.rs | 6 + full-service/src/json_rpc/v1/api/request.rs | 241 + full-service/src/json_rpc/v1/api/response.rs | 195 + .../api/test_utils.rs} | 10 +- full-service/src/json_rpc/v1/api/wallet.rs | 1078 +++++ .../v1/e2e_tests/account/account_address.rs | 516 +++ .../v1/e2e_tests/account/account_balance.rs | 253 ++ .../v1/e2e_tests/account/account_other.rs | 187 + .../account/create_import/account_crud.rs | 159 + .../account/create_import/import_account.rs | 443 ++ .../v1/e2e_tests/account/create_import/mod.rs | 2 + .../src/json_rpc/v1/e2e_tests/account/mod.rs | 4 + .../src/json_rpc/v1/e2e_tests/gift_codes.rs | 213 + full-service/src/json_rpc/v1/e2e_tests/mod.rs | 4 + .../src/json_rpc/v1/e2e_tests/other.rs | 108 + .../build_submit/build_and_submit.rs | 193 + .../build_submit/build_then_submit.rs | 392 ++ .../build_submit/large_transaction.rs | 180 + .../e2e_tests/transaction/build_submit/mod.rs | 4 + .../build_submit/multiple_outlay.rs | 366 ++ .../json_rpc/v1/e2e_tests/transaction/mod.rs | 3 + .../transaction/transaction_other.rs | 560 +++ .../e2e_tests/transaction/transaction_txo.rs | 755 +++ full-service/src/json_rpc/v1/mod.rs | 5 + .../src/json_rpc/{ => v1/models}/account.rs | 33 +- .../json_rpc/{ => v1/models}/account_key.rs | 43 +- .../src/json_rpc/v1/models/account_secrets.rs | 115 + .../src/json_rpc/{ => v1/models}/address.rs | 16 +- full-service/src/json_rpc/v1/models/amount.rs | 103 + .../src/json_rpc/{ => v1/models}/balance.rs | 28 +- .../src/json_rpc/{ => v1/models}/block.rs | 4 +- .../{ => v1/models}/confirmation_number.rs | 0 .../src/json_rpc/{ => v1/models}/gift_code.rs | 0 full-service/src/json_rpc/v1/models/mod.rs | 16 + .../{ => v1/models}/network_status.rs | 8 +- .../{ => v1/models}/receiver_receipt.rs | 5 +- .../src/json_rpc/v1/models/transaction_log.rs | 253 ++ .../json_rpc/{ => v1/models}/tx_proposal.rs | 76 +- .../src/json_rpc/{ => v1/models}/txo.rs | 151 +- .../{ => v1/models}/unspent_tx_out.rs | 8 +- .../json_rpc/{ => v1/models}/wallet_status.rs | 63 +- full-service/src/json_rpc/v2/api/mod.rs | 6 + full-service/src/json_rpc/v2/api/request.rs | 245 + full-service/src/json_rpc/v2/api/response.rs | 192 + .../src/json_rpc/v2/api/test_utils.rs | 329 ++ full-service/src/json_rpc/v2/api/wallet.rs | 1002 ++++ .../v2/e2e_tests/account/account_address.rs | 515 +++ .../v2/e2e_tests/account/account_balance.rs | 190 + .../v2/e2e_tests/account/account_other.rs | 654 +++ .../account/create_import/account_crud.rs | 163 + .../account/create_import/import_account.rs | 451 ++ .../v2/e2e_tests/account/create_import/mod.rs | 3 + .../create_import/view_account_flow.rs | 218 + .../src/json_rpc/v2/e2e_tests/account/mod.rs | 4 + full-service/src/json_rpc/v2/e2e_tests/mod.rs | 3 + .../src/json_rpc/v2/e2e_tests/other.rs | 223 + .../build_submit/build_and_submit.rs | 381 ++ .../build_submit/build_then_submit.rs | 715 +++ .../build_submit/build_unsigned.rs | 174 + .../build_submit/large_transaction.rs | 143 + .../e2e_tests/transaction/build_submit/mod.rs | 5 + .../build_submit/multiple_outlay.rs | 319 ++ .../json_rpc/v2/e2e_tests/transaction/mod.rs | 3 + .../transaction/transaction_other.rs | 337 ++ .../e2e_tests/transaction/transaction_txo.rs | 595 +++ full-service/src/json_rpc/v2/mod.rs | 5 + .../src/json_rpc/v2/models/account.rs | 88 + .../src/json_rpc/v2/models/account_key.rs | 137 + .../src/json_rpc/v2/models/account_secrets.rs | 103 + .../src/json_rpc/v2/models/address.rs | 46 + full-service/src/json_rpc/v2/models/amount.rs | 50 + .../src/json_rpc/v2/models/balance.rs | 60 + full-service/src/json_rpc/v2/models/block.rs | 59 + .../json_rpc/v2/models/confirmation_number.rs | 34 + .../src/json_rpc/v2/models/masked_amount.rs | 97 + full-service/src/json_rpc/v2/models/mod.rs | 15 + .../src/json_rpc/v2/models/network_status.rs | 41 + .../json_rpc/v2/models/receiver_receipt.rs | 128 + .../src/json_rpc/v2/models/transaction_log.rs | 147 + .../src/json_rpc/v2/models/tx_proposal.rs | 172 + full-service/src/json_rpc/v2/models/txo.rs | 139 + .../src/json_rpc/v2/models/wallet_status.rs | 48 + .../src/json_rpc/view_only_account.rs | 146 - .../src/json_rpc/view_only_subaddress.rs | 64 - full-service/src/json_rpc/view_only_txo.rs | 64 - full-service/src/json_rpc/wallet.rs | 1258 +---- full-service/src/lib.rs | 2 - full-service/src/service/account.rs | 393 +- full-service/src/service/address.rs | 199 +- full-service/src/service/balance.rs | 622 +-- .../src/service/confirmation_number.rs | 15 +- full-service/src/service/gift_code.rs | 261 +- full-service/src/service/ledger.rs | 229 +- full-service/src/service/mod.rs | 3 +- full-service/src/service/models/mod.rs | 1 + .../src/service/models/tx_proposal.rs | 372 ++ full-service/src/service/payment_request.rs | 12 +- full-service/src/service/receipt.rs | 306 +- full-service/src/service/sync.rs | 457 +- full-service/src/service/transaction.rs | 584 ++- .../src/service/transaction_builder.rs | 836 ++-- full-service/src/service/transaction_log.rs | 181 +- full-service/src/service/txo.rs | 314 +- full-service/src/service/view_only_account.rs | 291 -- full-service/src/service/view_only_txo.rs | 219 - full-service/src/service/wallet_service.rs | 31 +- full-service/src/test_utils.rs | 178 +- full-service/src/unsigned_tx.rs | 203 - full-service/src/util/b58/mod.rs | 11 +- full-service/src/util/b58/tests.rs | 6 +- full-service/src/util/encoding_helpers.rs | 19 +- full-service/src/validator_ledger_sync.rs | 47 +- mobilecoin | 2 +- renovate.json | 31 + tools/build-fs.sh | 55 + tools/run-alphanet.sh | 16 - tools/run-fs.sh | 66 + tools/run-mainnet.sh | 22 - tools/run-testnet.sh | 22 - tools/test.sh | 6 +- transaction-signer/Cargo.toml | 31 + .../src/bin/main.rs | 214 +- validator/api/Cargo.toml | 6 +- validator/connection/Cargo.toml | 9 +- validator/connection/src/lib.rs | 44 +- validator/service/Cargo.toml | 6 +- validator/service/src/bin/main.rs | 8 +- validator/service/src/blockchain_api.rs | 75 +- validator/service/src/service.rs | 11 +- validator/service/src/validator_api.rs | 42 +- 318 files changed, 25891 insertions(+), 15398 deletions(-) create mode 100644 .github/ISSUE_TEMPLATE/bug-report.md create mode 100644 .github/ISSUE_TEMPLATE/feature_request.md create mode 100644 .github/dependabot.yml create mode 100644 SECURITY.md delete mode 100644 docs/accounts/account-secrets/export_view_only_account_package.md delete mode 100644 docs/api-endpoints/account/README.md delete mode 100644 docs/api-endpoints/transaction/README.md delete mode 100644 docs/api-endpoints/view-only-account/README.md delete mode 100644 docs/transactions/txo/export_view_only_txouts_without_key_image.md delete mode 100644 docs/transactions/txo/get_txos_for_view_only_account.md delete mode 100644 docs/transactions/txo/set_view_only_txos_key_images.md create mode 100644 docs/usage/no-wallet-db/no-wallet-db.md rename docs/{ => v1}/accounts/account-secrets/README.md (100%) rename docs/{ => v1}/accounts/account/README.md (100%) rename docs/{ => v1}/accounts/address/README.md (100%) rename docs/{ => v1}/accounts/balance/README.md (100%) rename docs/{accounts/address => v1/api-endpoints}/assign_address_for_account.md (100%) rename docs/{transactions/transaction => v1/api-endpoints}/build_and_submit_transaction.md (100%) rename docs/{gift-codes/gift-code => v1/api-endpoints}/build_gift_code.md (100%) rename docs/{transactions/transaction => v1/api-endpoints}/build_split_txo_transaction.md (100%) rename docs/{transactions/transaction => v1/api-endpoints}/build_transaction.md (100%) rename docs/{transactions/payment-request => v1/api-endpoints}/check_b58_type.md (100%) rename docs/{gift-codes/gift-code => v1/api-endpoints}/check_gift_code_status.md (100%) rename docs/{transactions/transaction-receipt => v1/api-endpoints}/check_receiver_receipt_status.md (100%) rename docs/{gift-codes/gift-code => v1/api-endpoints}/claim_gift_code.md (100%) rename docs/{accounts/account => v1/api-endpoints}/create_account.md (100%) rename docs/{transactions/payment-request => v1/api-endpoints}/create_payment_request.md (100%) rename docs/{transactions/transaction-receipt => v1/api-endpoints}/create_receiver_receipts.md (100%) rename docs/{accounts/account-secrets => v1/api-endpoints}/export_account_secrets.md (100%) rename docs/{accounts/account => v1/api-endpoints}/get_account.md (100%) rename docs/{accounts/account => v1/api-endpoints}/get_account_status.md (100%) rename docs/{accounts/address => v1/api-endpoints}/get_address_for_account.md (100%) rename docs/{accounts/address => v1/api-endpoints}/get_addresses_for_account.md (100%) rename docs/{accounts/account => v1/api-endpoints}/get_all_accounts.md (100%) rename docs/{gift-codes/gift-code => v1/api-endpoints}/get_all_gift_codes.md (100%) rename docs/{transactions/transaction-log => v1/api-endpoints}/get_all_transaction_logs_for_block.md (100%) rename docs/{transactions/transaction-log => v1/api-endpoints}/get_all_transaction_logs_ordered_by_block.md (100%) rename docs/{transactions/txo => v1/api-endpoints}/get_all_txos_for_address.md (100%) rename docs/{view-only-accounts/account => v1/api-endpoints}/get_all_view_only_accounts.md (100%) rename docs/{accounts/balance => v1/api-endpoints}/get_balance_for_account.md (100%) rename docs/{accounts/balance => v1/api-endpoints}/get_balance_for_address.md (100%) rename docs/{other/block => v1/api-endpoints}/get_block.md (100%) rename docs/{transactions/transaction-confirmation => v1/api-endpoints}/get_confirmations.md (100%) rename docs/{gift-codes/gift-code => v1/api-endpoints}/get_gift_code.md (100%) rename docs/{transactions/transaction-log => v1/api-endpoints}/get_mc_protocol_transaction.md (100%) rename docs/{transactions/txo => v1/api-endpoints}/get_mc_protocol_txo.md (100%) rename docs/{other/network-status => v1/api-endpoints}/get_network_status.md (100%) rename docs/{transactions/transaction-log => v1/api-endpoints}/get_transaction_log.md (100%) rename docs/{transactions/transaction-log => v1/api-endpoints}/get_transaction_logs_for_account.md (100%) rename docs/{transactions/transaction-log => v1/api-endpoints}/get_transaction_object.md (100%) rename docs/{transactions/txo => v1/api-endpoints}/get_txo.md (100%) rename docs/{transactions/txo => v1/api-endpoints}/get_txo_object.md (100%) rename docs/{transactions/txo => v1/api-endpoints}/get_txos_for_account.md (100%) rename docs/{other/wallet-status => v1/api-endpoints}/get_wallet_status.md (71%) rename docs/{accounts/account => v1/api-endpoints}/import_account.md (100%) rename docs/{accounts/account => v1/api-endpoints}/import_account_from_legacy_root_entropy-deprecated.md (100%) rename docs/{accounts/account => v1/api-endpoints}/remove_account.md (100%) rename docs/{gift-codes/gift-code => v1/api-endpoints}/remove_gift_code.md (100%) rename docs/{gift-codes/gift-code => v1/api-endpoints}/submit_gift_code.md (100%) rename docs/{transactions/transaction => v1/api-endpoints}/submit_transaction.md (100%) rename docs/{accounts/account => v1/api-endpoints}/update_account_name.md (100%) rename docs/{transactions/transaction-confirmation => v1/api-endpoints}/validate_confirmation.md (100%) rename docs/{accounts/address => v1/api-endpoints}/verify_address.md (100%) rename docs/{other/version => v1/api-endpoints}/version.md (100%) rename docs/{ => v1}/gift-codes/gift-code/README.md (100%) rename docs/{ => v1}/other/block/README.md (100%) rename docs/{ => v1}/other/network-status/README.md (100%) rename docs/{ => v1}/other/version/README.md (100%) rename docs/{ => v1}/other/wallet-status/README.md (54%) rename docs/{ => v1}/transactions/payment-request/README.md (100%) rename docs/{ => v1}/transactions/transaction-confirmation/README.md (100%) rename docs/{ => v1}/transactions/transaction-log/README.md (100%) rename docs/{ => v1}/transactions/transaction-receipt/README.md (100%) rename docs/{ => v1}/transactions/transaction/README.md (100%) rename docs/{ => v1}/transactions/txo/README.md (100%) create mode 100644 docs/v2/accounts/account-secrets/README.md create mode 100644 docs/v2/accounts/account/README.md create mode 100644 docs/v2/accounts/address/README.md create mode 100644 docs/v2/accounts/balance/README.md create mode 100644 docs/v2/api-endpoints/assign_address_for_account.md create mode 100644 docs/v2/api-endpoints/build_and_submit_transaction.md create mode 100644 docs/v2/api-endpoints/build_transaction.md rename docs/{transactions/transaction => v2/api-endpoints}/build_unsigned_transaction.md (80%) create mode 100644 docs/v2/api-endpoints/check_b58_type.md create mode 100644 docs/v2/api-endpoints/check_receiver_receipt_status.md create mode 100644 docs/v2/api-endpoints/create_account.md create mode 100644 docs/v2/api-endpoints/create_payment_request.md create mode 100644 docs/v2/api-endpoints/create_receiver_receipts.md create mode 100644 docs/v2/api-endpoints/create_view_only_account_import_request.md rename docs/{view-only-accounts/syncing => v2/api-endpoints}/create_view_only_account_sync_request.md (86%) create mode 100644 docs/v2/api-endpoints/export_account_secrets.md create mode 100644 docs/v2/api-endpoints/get_account_status.md create mode 100644 docs/v2/api-endpoints/get_accounts.md create mode 100644 docs/v2/api-endpoints/get_address_for_account.md create mode 100644 docs/v2/api-endpoints/get_address_status.md create mode 100644 docs/v2/api-endpoints/get_addresses.md create mode 100644 docs/v2/api-endpoints/get_block.md create mode 100644 docs/v2/api-endpoints/get_confirmations.md create mode 100644 docs/v2/api-endpoints/get_mc_protocol_transaction.md create mode 100644 docs/v2/api-endpoints/get_mc_protocol_txo.md create mode 100644 docs/v2/api-endpoints/get_network_status.md create mode 100644 docs/v2/api-endpoints/get_transaction_log.md create mode 100644 docs/v2/api-endpoints/get_transaction_logs.md create mode 100644 docs/v2/api-endpoints/get_txo.md create mode 100644 docs/v2/api-endpoints/get_txo_block_index.md create mode 100644 docs/v2/api-endpoints/get_txo_membership_proofs.md create mode 100644 docs/v2/api-endpoints/get_txos.md create mode 100644 docs/v2/api-endpoints/get_wallet_status.md create mode 100644 docs/v2/api-endpoints/import_account.md create mode 100644 docs/v2/api-endpoints/import_account_from_legacy_root_entropy.md create mode 100644 docs/v2/api-endpoints/import_view_only_account.md rename docs/{view-only-accounts/account/remove_view_only_account.md => v2/api-endpoints/remove_account.md} (68%) create mode 100644 docs/v2/api-endpoints/sample_mixins.md create mode 100644 docs/v2/api-endpoints/submit_transaction.md create mode 100644 docs/v2/api-endpoints/sync_view_only_account.md create mode 100644 docs/v2/api-endpoints/update_account_name.md create mode 100644 docs/v2/api-endpoints/validate_confirmation.md create mode 100644 docs/v2/api-endpoints/verify_address.md create mode 100644 docs/v2/api-endpoints/version.md create mode 100644 docs/v2/other/block/README.md create mode 100644 docs/v2/other/network-status/README.md create mode 100644 docs/v2/other/version/README.md create mode 100644 docs/v2/other/wallet-status/README.md create mode 100644 docs/v2/transactions/payment-request/README.md create mode 100644 docs/v2/transactions/transaction-confirmation/README.md create mode 100644 docs/v2/transactions/transaction-log/README.md create mode 100644 docs/v2/transactions/transaction-receipt/README.md create mode 100644 docs/v2/transactions/transaction/README.md create mode 100644 docs/v2/transactions/txo/README.md delete mode 100644 docs/view-only-accounts/account-secrets/README.md delete mode 100644 docs/view-only-accounts/account-secrets/export_view_only_account_secrets.md delete mode 100644 docs/view-only-accounts/account/README.md delete mode 100644 docs/view-only-accounts/account/get_view_only_account.md delete mode 100644 docs/view-only-accounts/account/import_view_only_account.md delete mode 100644 docs/view-only-accounts/account/update_view_only_account_name.md delete mode 100644 docs/view-only-accounts/balance/README.md delete mode 100644 docs/view-only-accounts/balance/get_balance_for_view_only_account.md delete mode 100644 docs/view-only-accounts/balance/get_balance_for_view_only_address.md delete mode 100644 docs/view-only-accounts/subaddress/create_new_subaddress_request.md delete mode 100644 docs/view-only-accounts/subaddress/import_subaddresses_to_view_only_account.md delete mode 100644 docs/view-only-accounts/syncing/README.md delete mode 100644 docs/view-only-accounts/syncing/sync_view_only_account.md rename docs/view-only-accounts/subaddress/README.md => full-service/migrations/2022-06-13-204000_api_v3/down.sql (100%) create mode 100644 full-service/migrations/2022-06-13-204000_api_v3/up.sql delete mode 100644 full-service/src/db/migration_testing/migration_testing.rs delete mode 100644 full-service/src/db/migration_testing/mod.rs delete mode 100644 full-service/src/db/migration_testing/seed_accounts.rs delete mode 100644 full-service/src/db/migration_testing/seed_gift_codes.rs delete mode 100644 full-service/src/db/migration_testing/seed_txos.rs delete mode 100644 full-service/src/db/view_only_account.rs delete mode 100644 full-service/src/db/view_only_subaddress.rs delete mode 100644 full-service/src/db/view_only_txo.rs delete mode 100644 full-service/src/fog_resolver.rs delete mode 100644 full-service/src/json_rpc/account_secrets.rs delete mode 100644 full-service/src/json_rpc/amount.rs delete mode 100644 full-service/src/json_rpc/e2e.rs delete mode 100644 full-service/src/json_rpc/transaction_log.rs create mode 100644 full-service/src/json_rpc/v1/api/mod.rs create mode 100644 full-service/src/json_rpc/v1/api/request.rs create mode 100644 full-service/src/json_rpc/v1/api/response.rs rename full-service/src/json_rpc/{api_test_utils.rs => v1/api/test_utils.rs} (97%) create mode 100644 full-service/src/json_rpc/v1/api/wallet.rs create mode 100644 full-service/src/json_rpc/v1/e2e_tests/account/account_address.rs create mode 100644 full-service/src/json_rpc/v1/e2e_tests/account/account_balance.rs create mode 100644 full-service/src/json_rpc/v1/e2e_tests/account/account_other.rs create mode 100644 full-service/src/json_rpc/v1/e2e_tests/account/create_import/account_crud.rs create mode 100644 full-service/src/json_rpc/v1/e2e_tests/account/create_import/import_account.rs create mode 100644 full-service/src/json_rpc/v1/e2e_tests/account/create_import/mod.rs create mode 100644 full-service/src/json_rpc/v1/e2e_tests/account/mod.rs create mode 100644 full-service/src/json_rpc/v1/e2e_tests/gift_codes.rs create mode 100644 full-service/src/json_rpc/v1/e2e_tests/mod.rs create mode 100644 full-service/src/json_rpc/v1/e2e_tests/other.rs create mode 100644 full-service/src/json_rpc/v1/e2e_tests/transaction/build_submit/build_and_submit.rs create mode 100644 full-service/src/json_rpc/v1/e2e_tests/transaction/build_submit/build_then_submit.rs create mode 100644 full-service/src/json_rpc/v1/e2e_tests/transaction/build_submit/large_transaction.rs create mode 100644 full-service/src/json_rpc/v1/e2e_tests/transaction/build_submit/mod.rs create mode 100644 full-service/src/json_rpc/v1/e2e_tests/transaction/build_submit/multiple_outlay.rs create mode 100644 full-service/src/json_rpc/v1/e2e_tests/transaction/mod.rs create mode 100644 full-service/src/json_rpc/v1/e2e_tests/transaction/transaction_other.rs create mode 100644 full-service/src/json_rpc/v1/e2e_tests/transaction/transaction_txo.rs create mode 100644 full-service/src/json_rpc/v1/mod.rs rename full-service/src/json_rpc/{ => v1/models}/account.rs (68%) rename full-service/src/json_rpc/{ => v1/models}/account_key.rs (67%) create mode 100644 full-service/src/json_rpc/v1/models/account_secrets.rs rename full-service/src/json_rpc/{ => v1/models}/address.rs (73%) create mode 100644 full-service/src/json_rpc/v1/models/amount.rs rename full-service/src/json_rpc/{ => v1/models}/balance.rs (74%) rename full-service/src/json_rpc/{ => v1/models}/block.rs (93%) rename full-service/src/json_rpc/{ => v1/models}/confirmation_number.rs (100%) rename full-service/src/json_rpc/{ => v1/models}/gift_code.rs (100%) create mode 100644 full-service/src/json_rpc/v1/models/mod.rs rename full-service/src/json_rpc/{ => v1/models}/network_status.rs (83%) rename full-service/src/json_rpc/{ => v1/models}/receiver_receipt.rs (96%) create mode 100644 full-service/src/json_rpc/v1/models/transaction_log.rs rename full-service/src/json_rpc/{ => v1/models}/tx_proposal.rs (60%) rename full-service/src/json_rpc/{ => v1/models}/txo.rs (65%) rename full-service/src/json_rpc/{ => v1/models}/unspent_tx_out.rs (91%) rename full-service/src/json_rpc/{ => v1/models}/wallet_status.rs (55%) create mode 100644 full-service/src/json_rpc/v2/api/mod.rs create mode 100644 full-service/src/json_rpc/v2/api/request.rs create mode 100644 full-service/src/json_rpc/v2/api/response.rs create mode 100644 full-service/src/json_rpc/v2/api/test_utils.rs create mode 100644 full-service/src/json_rpc/v2/api/wallet.rs create mode 100644 full-service/src/json_rpc/v2/e2e_tests/account/account_address.rs create mode 100644 full-service/src/json_rpc/v2/e2e_tests/account/account_balance.rs create mode 100644 full-service/src/json_rpc/v2/e2e_tests/account/account_other.rs create mode 100644 full-service/src/json_rpc/v2/e2e_tests/account/create_import/account_crud.rs create mode 100644 full-service/src/json_rpc/v2/e2e_tests/account/create_import/import_account.rs create mode 100644 full-service/src/json_rpc/v2/e2e_tests/account/create_import/mod.rs create mode 100644 full-service/src/json_rpc/v2/e2e_tests/account/create_import/view_account_flow.rs create mode 100644 full-service/src/json_rpc/v2/e2e_tests/account/mod.rs create mode 100644 full-service/src/json_rpc/v2/e2e_tests/mod.rs create mode 100644 full-service/src/json_rpc/v2/e2e_tests/other.rs create mode 100644 full-service/src/json_rpc/v2/e2e_tests/transaction/build_submit/build_and_submit.rs create mode 100644 full-service/src/json_rpc/v2/e2e_tests/transaction/build_submit/build_then_submit.rs create mode 100644 full-service/src/json_rpc/v2/e2e_tests/transaction/build_submit/build_unsigned.rs create mode 100644 full-service/src/json_rpc/v2/e2e_tests/transaction/build_submit/large_transaction.rs create mode 100644 full-service/src/json_rpc/v2/e2e_tests/transaction/build_submit/mod.rs create mode 100644 full-service/src/json_rpc/v2/e2e_tests/transaction/build_submit/multiple_outlay.rs create mode 100644 full-service/src/json_rpc/v2/e2e_tests/transaction/mod.rs create mode 100644 full-service/src/json_rpc/v2/e2e_tests/transaction/transaction_other.rs create mode 100644 full-service/src/json_rpc/v2/e2e_tests/transaction/transaction_txo.rs create mode 100644 full-service/src/json_rpc/v2/mod.rs create mode 100644 full-service/src/json_rpc/v2/models/account.rs create mode 100644 full-service/src/json_rpc/v2/models/account_key.rs create mode 100644 full-service/src/json_rpc/v2/models/account_secrets.rs create mode 100644 full-service/src/json_rpc/v2/models/address.rs create mode 100644 full-service/src/json_rpc/v2/models/amount.rs create mode 100644 full-service/src/json_rpc/v2/models/balance.rs create mode 100644 full-service/src/json_rpc/v2/models/block.rs create mode 100644 full-service/src/json_rpc/v2/models/confirmation_number.rs create mode 100644 full-service/src/json_rpc/v2/models/masked_amount.rs create mode 100644 full-service/src/json_rpc/v2/models/mod.rs create mode 100644 full-service/src/json_rpc/v2/models/network_status.rs create mode 100644 full-service/src/json_rpc/v2/models/receiver_receipt.rs create mode 100644 full-service/src/json_rpc/v2/models/transaction_log.rs create mode 100644 full-service/src/json_rpc/v2/models/tx_proposal.rs create mode 100644 full-service/src/json_rpc/v2/models/txo.rs create mode 100644 full-service/src/json_rpc/v2/models/wallet_status.rs delete mode 100644 full-service/src/json_rpc/view_only_account.rs delete mode 100644 full-service/src/json_rpc/view_only_subaddress.rs delete mode 100644 full-service/src/json_rpc/view_only_txo.rs create mode 100644 full-service/src/service/models/mod.rs create mode 100644 full-service/src/service/models/tx_proposal.rs delete mode 100644 full-service/src/service/view_only_account.rs delete mode 100644 full-service/src/service/view_only_txo.rs delete mode 100644 full-service/src/unsigned_tx.rs create mode 100644 renovate.json create mode 100755 tools/build-fs.sh delete mode 100755 tools/run-alphanet.sh create mode 100755 tools/run-fs.sh delete mode 100644 tools/run-mainnet.sh delete mode 100755 tools/run-testnet.sh create mode 100644 transaction-signer/Cargo.toml rename full-service/src/bin/transaction-signer.rs => transaction-signer/src/bin/main.rs (64%) diff --git a/.github/ISSUE_TEMPLATE/bug-report.md b/.github/ISSUE_TEMPLATE/bug-report.md new file mode 100644 index 000000000..dad8ef503 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug-report.md @@ -0,0 +1,32 @@ +--- +name: Bug Report +about: Create a report to help us improve +title: '' +labels: bug +assignees: '' + +--- + +**Describe the bug** +A clear and concise description of what the bug is. + +**To Reproduce** +Steps to reproduce the behavior: + +**Expected behavior** +A clear and concise description of what you expected to happen. + +**Environment (please complete the following information):** + - OS: [e.g. Ubuntu] + - Version [e.g. 20.04] + +**
Logs** + +``` +Copy/paste the relevant log(s) here, between the starting and ending backticks +``` + +
+ +**Additional context** +Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 000000000..11fc491ef --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,20 @@ +--- +name: Feature request +about: Suggest an idea for this project +title: '' +labels: enhancement +assignees: '' + +--- + +**Is your feature request related to a problem? Please describe.** +A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + +**Describe the solution you'd like** +A clear and concise description of what you want to happen. + +**Describe alternatives you've considered** +A clear and concise description of any alternative solutions or features you've considered. + +**Additional context** +Add any other context or screenshots about the feature request here. diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 000000000..68bf90396 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,10 @@ +version: 2 +updates: +- package-ecosystem: cargo + directory: "/" + schedule: + interval: daily + open-pull-requests-limit: 25 + labels: + - "dependencies" + - "rust" diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index a77fca621..3cd77b1cd 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -7,116 +7,35 @@ env: CONSENSUS_ENCLAVE_CSS: /var/tmp/consensus-enclave.css INGEST_ENCLAVE_CSS: /var/tmp/ingest-enclave.css -# only perform these build steps on pre-release +# only perform these build steps on pre-release or forced dev build on: push: tags: - 'v*-pre.*' - - '*-force-build*' + - '*.dev-build.*' jobs: - macos-x64: - runs-on: [self-hosted, macOS, X64] + build-and-pre-release: permissions: contents: write strategy: matrix: + runner-tags: [[self-hosted, macOS, X64, cargo], [self-hosted, macOS, ARM64, cargo], [self-hosted, Linux, large]] + namespace: [test, prod] include: + - runner-tags: [self-hosted, macOS, X64, cargo] + container: null + - runner-tags: [self-hosted, macOS, ARM64, cargo] + container: null + - runner-tags: [self-hosted, Linux, large] + container: mobilecoin/rust-sgx-base:latest - namespace: test network: testnet - namespace: prod network: mainnet - - steps: - - name: Checkout - uses: actions/checkout@v3 - with: - submodules: recursive - - - name: Brew Bundle - run: | - brew bundle - - - name: Git Submodule - run: | - git submodule update --checkout --init --recursive - - # CACHE_VERSION secret is 'date --iso-8601=minutes' and is used to invalidate cache if needed - - name: Cache Build Binaries - id: artifact_cache - uses: actions/cache@v3 - with: - path: | - build_artifacts - key: ${{ runner.os }}-x86-${{ matrix.network }}-${{ secrets.CACHE_VERSION }}-build-cargo-artifacts-${{ hashFiles('**/*.rs', '**/*.proto', '**/Cargo.toml')}} - - - name: Consensus SigStruct - if: steps.artifact_cache.outputs.cache-hit != 'true' - run: | - CONSENSUS_SIGSTRUCT_URI=$(curl -s https://enclave-distribution.${{ matrix.namespace }}.mobilecoin.com/production.json | grep consensus-enclave.css | awk '{print $2}' | tr -d \" | tr -d ,) - (cd /var/tmp && curl -O https://enclave-distribution.${{ matrix.namespace }}.mobilecoin.com/${CONSENSUS_SIGSTRUCT_URI}) - - - name: Ingest SigStruct - if: steps.artifact_cache.outputs.cache-hit != 'true' - run: | - INGEST_SIGSTRUCT_URI=$(curl -s https://enclave-distribution.${{ matrix.namespace }}.mobilecoin.com/production.json | grep ingest-enclave.css | awk '{print $2}' | tr -d \" | tr -d ,) - (cd /var/tmp && curl -O https://enclave-distribution.${{ matrix.namespace }}.mobilecoin.com/${INGEST_SIGSTRUCT_URI}) + runs-on: ${{ matrix.runner-tags }} + container: ${{ matrix.container }} - - name: Cargo Build - if: steps.artifact_cache.outputs.cache-hit != 'true' - run: | - export PATH="/usr/local/opt/openssl@3/bin:$PATH" - export LDFLAGS="-L/usr/local/opt/openssl@3/lib" - export CPPFLAGS="-I/usr/local/opt/openssl@3/include" - export PKG_CONFIG_PATH="/usr/local/opt/openssl@3/lib/pkgconfig" - cargo build --release - - - name: Copy binaries to cache folder - if: steps.artifact_cache.outputs.cache-hit != 'true' - run: | - mkdir -pv build_artifacts/${{ matrix.network }}/bin - cp /var/tmp/*.css build_artifacts/${{ matrix.network }} - cp target/release/full-service build_artifacts/${{ matrix.network }}/bin/ - cp target/release/transaction-signer build_artifacts/${{ matrix.network }}/bin/ - - - name: Create Artifact - run: | - mkdir -pv artifact - cd artifact && tar -czvf ${{ github.sha }}-${{ runner.os }}-x86-${{ matrix.network }}.tar.gz -C ../build_artifacts/${{ matrix.network }}/ . - - - name: Upload Artifact - uses: actions/upload-artifact@v3 - with: - name: full-service_${{ runner.os }}_${{ matrix.network }}_x86 - path: artifact/${{ github.sha }}-${{ runner.os }}-x86-${{ matrix.network }}.tar.gz - - - name: Create Release - if: startsWith(github.ref, 'refs/tags/v') - run: | - mkdir -pv release - cd release && tar -czvf ${{ github.ref_name }}-${{ runner.os }}-x86-${{ matrix.network }}.tar.gz -C ../build_artifacts/${{ matrix.network }}/ . - - - name: Upload Release - if: startsWith(github.ref, 'refs/tags/v') - uses: softprops/action-gh-release@v1 - with: - draft: true - prerelease: true - files: | - release/${{ github.ref_name }}-${{ runner.os }}-x86-${{ matrix.network }}.tar.gz - - macos-arm64: - runs-on: [self-hosted, macOS, ARM64] - permissions: - contents: write - strategy: - matrix: - include: - - namespace: test - network: testnet - - namespace: prod - network: mainnet - steps: - name: Checkout uses: actions/checkout@v3 @@ -124,153 +43,54 @@ jobs: submodules: recursive - name: Brew Bundle + if: runner.os == 'macOS' run: | brew bundle - - - name: Git Submodule - run: | - git submodule update --checkout --init --recursive - - # CACHE_VERSION secret is 'date --iso-8601=minutes' and is used to invalidate cache if needed - - name: Cache Build Binaries - id: artifact_cache - uses: actions/cache@v3 - with: - path: | - build_artifacts - key: ${{ runner.os }}-arm64-${{ matrix.network }}-${{ secrets.CACHE_VERSION }}-build-cargo-artifacts-${{ hashFiles('**/*.rs', '**/*.proto', '**/Cargo.toml')}} - - - name: Consensus SigStruct - if: steps.artifact_cache.outputs.cache-hit != 'true' - run: | - CONSENSUS_SIGSTRUCT_URI=$(curl -s https://enclave-distribution.${{ matrix.namespace }}.mobilecoin.com/production.json | grep consensus-enclave.css | awk '{print $2}' | tr -d \" | tr -d ,) - (cd /var/tmp && curl -O https://enclave-distribution.${{ matrix.namespace }}.mobilecoin.com/${CONSENSUS_SIGSTRUCT_URI}) - - - name: Ingest SigStruct - if: steps.artifact_cache.outputs.cache-hit != 'true' - run: | - INGEST_SIGSTRUCT_URI=$(curl -s https://enclave-distribution.${{ matrix.namespace }}.mobilecoin.com/production.json | grep ingest-enclave.css | awk '{print $2}' | tr -d \" | tr -d ,) - (cd /var/tmp && curl -O https://enclave-distribution.${{ matrix.namespace }}.mobilecoin.com/${INGEST_SIGSTRUCT_URI}) - - - name: Cargo Build - if: steps.artifact_cache.outputs.cache-hit != 'true' - run: | - export PATH="/opt/homebrew/opt/openssl@3/bin:$PATH" - export LDFLAGS="-L/opt/homebrew/opt/openssl@3/lib" - export CPPFLAGS="-I/opt/homebrew/opt/openssl@3/include" - export PKG_CONFIG_PATH="/opt/homebrew/opt/openssl@3/lib/pkgconfig" - cargo build --release - - - name: Copy binaries to cache folder - if: steps.artifact_cache.outputs.cache-hit != 'true' - run: | - mkdir -pv build_artifacts/${{ matrix.network }}/bin - cp /var/tmp/*.css build_artifacts/${{ matrix.network }} - cp target/release/full-service build_artifacts/${{ matrix.network }}/bin/ - cp target/release/transaction-signer build_artifacts/${{ matrix.network }}/bin/ - - - name: Create Artifact - run: | - mkdir -pv artifact - cd artifact && tar -czvf ${{ github.sha }}-${{ runner.os }}-arm64-${{ matrix.network }}.tar.gz -C ../build_artifacts/${{ matrix.network }}/ . - - - name: Upload Artifact - uses: actions/upload-artifact@v3 - with: - name: full-service_${{ runner.os }}_${{ matrix.network }}_arm64 - path: artifact/${{ github.sha }}-${{ runner.os }}-arm64-${{ matrix.network }}.tar.gz - - - name: Create Release - if: startsWith(github.ref, 'refs/tags/v') - run: | - mkdir -pv release - cd release && tar -czvf ${{ github.ref_name }}-${{ runner.os }}-arm64-${{ matrix.network }}.tar.gz -C ../build_artifacts/${{ matrix.network }}/ . - - - name: Upload Release - if: startsWith(github.ref, 'refs/tags/v') - uses: softprops/action-gh-release@v1 - with: - draft: true - prerelease: true - files: | - release/${{ github.ref_name }}-${{ runner.os }}-arm64-${{ matrix.network }}.tar.gz - - linux: - runs-on: [self-hosted, Linux, large] - permissions: - contents: write - container: - image: mobilecoin/rust-sgx-base:latest - strategy: - matrix: - include: - - namespace: test - network: testnet - - namespace: prod - network: mainnet - - steps: - - name: Checkout - uses: actions/checkout@v3 - with: - submodules: recursive - - # CACHE_VERSION secret is 'date --iso-8601=minutes' and is used to invalidate cache if needed - - name: Cache Build Binaries - id: artifact_cache - uses: actions/cache@v3 - with: - path: | - build_artifacts - key: ${{ runner.os }}-${{ matrix.network }}-${{ secrets.CACHE_VERSION }}-build-cargo-artifacts-${{ hashFiles('**/*.rs', '**/*.proto', '**/Cargo.toml')}} - name: Consensus SigStruct - if: steps.artifact_cache.outputs.cache-hit != 'true' run: | CONSENSUS_SIGSTRUCT_URI=$(curl -s https://enclave-distribution.${{ matrix.namespace }}.mobilecoin.com/production.json | grep consensus-enclave.css | awk '{print $2}' | tr -d \" | tr -d ,) (cd /var/tmp && curl -O https://enclave-distribution.${{ matrix.namespace }}.mobilecoin.com/${CONSENSUS_SIGSTRUCT_URI}) - name: Ingest SigStruct - if: steps.artifact_cache.outputs.cache-hit != 'true' run: | INGEST_SIGSTRUCT_URI=$(curl -s https://enclave-distribution.${{ matrix.namespace }}.mobilecoin.com/production.json | grep ingest-enclave.css | awk '{print $2}' | tr -d \" | tr -d ,) (cd /var/tmp && curl -O https://enclave-distribution.${{ matrix.namespace }}.mobilecoin.com/${INGEST_SIGSTRUCT_URI}) - name: Cargo Build - if: steps.artifact_cache.outputs.cache-hit != 'true' run: | cargo build --release - name: Copy binaries to cache folder - if: steps.artifact_cache.outputs.cache-hit != 'true' run: | mkdir -pv build_artifacts/${{ matrix.network }}/bin cp /var/tmp/*.css build_artifacts/${{ matrix.network }} - cp target/release/full-service build_artifacts/${{ matrix.network }}/bin/ - cp target/release/transaction-signer build_artifacts/${{ matrix.network }}/bin/ + cp target/release/full-service build_artifacts/${{ matrix.network }} + cp target/release/transaction-signer build_artifacts/${{ matrix.network }} + cp target/release/validator-service build_artifacts/${{ matrix.network }} - name: Create Artifact run: | mkdir -pv artifact - cd artifact && tar -czvf ${{ github.sha }}-${{ runner.os }}-${{ matrix.network }}.tar.gz -C ../build_artifacts/${{ matrix.network }}/ . + cd artifact && tar -czvf ${{ github.sha }}-${{ runner.os }}-${{ runner.arch }}-${{ matrix.network }}.tar.gz -C ../build_artifacts/${{ matrix.network }}/ . - name: Upload Artifact uses: actions/upload-artifact@v3 with: - name: full-service_${{ runner.os }}_${{ matrix.network }} - path: artifact/${{ github.sha }}-${{ runner.os }}-${{ matrix.network }}.tar.gz + name: full-service_${{ runner.os }}-${{ runner.arch }}-${{ matrix.network }} + path: artifact/${{ github.sha }}-${{ runner.os }}-${{ runner.arch }}-${{ matrix.network }}.tar.gz - - name: Create Release + - name: Create Prerelease if: startsWith(github.ref, 'refs/tags/v') run: | mkdir -pv release - cd release && tar -czvf ${{ github.ref_name }}-${{ runner.os }}-${{ matrix.network }}.tar.gz -C ../build_artifacts/${{ matrix.network }}/ . + cd release && tar -czvf ${{ github.ref_name }}-${{ runner.os }}-${{ runner.arch }}-${{ matrix.network }}.tar.gz -C ../build_artifacts/${{ matrix.network }}/ . - - name: Upload Release + - name: Upload Prerelease if: startsWith(github.ref, 'refs/tags/v') uses: softprops/action-gh-release@v1 with: draft: true prerelease: true files: | - release/${{ github.ref_name }}-${{ runner.os }}-${{ matrix.network }}.tar.gz + release/${{ github.ref_name }}-${{ runner.os }}-${{ runner.arch }}-${{ matrix.network }}.tar.gz \ No newline at end of file diff --git a/.github/workflows/docker-hub.yml b/.github/workflows/docker-hub.yml index 8e2c13074..aa45ec2ac 100644 --- a/.github/workflows/docker-hub.yml +++ b/.github/workflows/docker-hub.yml @@ -24,7 +24,7 @@ jobs: tags: ${{ steps.meta.outputs.tags }} steps: - name: Checkout - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: submodules: recursive diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index db7733872..dc5888af7 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -6,11 +6,10 @@ on: tags: - 'v*' - '!v*-pre*' - - '*-force-release*' jobs: release: - runs-on: [self-hosted, Linux, large] + runs-on: [self-hosted, Linux-X64, large] # Needs write permission for publishing release permissions: contents: write @@ -31,41 +30,41 @@ jobs: with: tag: ${{ steps.current_release.outputs.tag_name }} files: | - ${{ steps.current_release.outputs.tag_name }}-Linux-testnet.tar.gz - ${{ steps.current_release.outputs.tag_name }}-Linux-mainnet.tar.gz - ${{ steps.current_release.outputs.tag_name }}-macOS-x86-testnet.tar.gz - ${{ steps.current_release.outputs.tag_name }}-macOS-x86-mainnet.tar.gz - ${{ steps.current_release.outputs.tag_name }}-macOS-arm64-testnet.tar.gz - ${{ steps.current_release.outputs.tag_name }}-macOS-arm64-mainnet.tar.gz + ${{ steps.current_release.outputs.tag_name }}-Linux-X64-testnet.tar.gz + ${{ steps.current_release.outputs.tag_name }}-Linux-X64-mainnet.tar.gz + ${{ steps.current_release.outputs.tag_name }}-macOS-X64-testnet.tar.gz + ${{ steps.current_release.outputs.tag_name }}-macOS-X64-mainnet.tar.gz + ${{ steps.current_release.outputs.tag_name }}-macOS-ARM64-testnet.tar.gz + ${{ steps.current_release.outputs.tag_name }}-macOS-ARM64-mainnet.tar.gz target: /var/tmp/ - name: Extract Release run: | rm -rfv build_artifacts - mkdir -pv build_artifacts/Linux-testnet - mkdir -pv build_artifacts/Linux-mainnet - mkdir -pv build_artifacts/macOS-x86-testnet - mkdir -pv build_artifacts/macOS-x86-mainnet - mkdir -pv build_artifacts/macOS-arm64-testnet - mkdir -pv build_artifacts/macOS-arm64-mainnet - tar xzvf /var/tmp/${{ steps.current_release.outputs.tag_name }}-Linux-testnet.tar.gz -C build_artifacts/Linux-testnet - tar xzvf /var/tmp/${{ steps.current_release.outputs.tag_name }}-Linux-mainnet.tar.gz -C build_artifacts/Linux-mainnet - tar xzvf /var/tmp/${{ steps.current_release.outputs.tag_name }}-macOS-x86-testnet.tar.gz -C build_artifacts/macOS-x86-testnet - tar xzvf /var/tmp/${{ steps.current_release.outputs.tag_name }}-macOS-x86-mainnet.tar.gz -C build_artifacts/macOS-x86-mainnet - tar xzvf /var/tmp/${{ steps.current_release.outputs.tag_name }}-macOS-arm64-testnet.tar.gz -C build_artifacts/macOS-arm64-testnet - tar xzvf /var/tmp/${{ steps.current_release.outputs.tag_name }}-macOS-arm64-mainnet.tar.gz -C build_artifacts/macOS-arm64-mainnet + mkdir -pv build_artifacts/Linux-X64-testnet + mkdir -pv build_artifacts/Linux-X64-mainnet + mkdir -pv build_artifacts/macOS-X64-testnet + mkdir -pv build_artifacts/macOS-X64-mainnet + mkdir -pv build_artifacts/macOS-ARM64-testnet + mkdir -pv build_artifacts/macOS-ARM64-mainnet + tar xzvf /var/tmp/${{ steps.current_release.outputs.tag_name }}-Linux-X64-testnet.tar.gz -C build_artifacts/Linux-X64-testnet + tar xzvf /var/tmp/${{ steps.current_release.outputs.tag_name }}-Linux-X64-mainnet.tar.gz -C build_artifacts/Linux-X64-mainnet + tar xzvf /var/tmp/${{ steps.current_release.outputs.tag_name }}-macOS-X64-testnet.tar.gz -C build_artifacts/macOS-X64-testnet + tar xzvf /var/tmp/${{ steps.current_release.outputs.tag_name }}-macOS-X64-mainnet.tar.gz -C build_artifacts/macOS-X64-mainnet + tar xzvf /var/tmp/${{ steps.current_release.outputs.tag_name }}-macOS-ARM64-testnet.tar.gz -C build_artifacts/macOS-ARM64-testnet + tar xzvf /var/tmp/${{ steps.current_release.outputs.tag_name }}-macOS-ARM64-mainnet.tar.gz -C build_artifacts/macOS-ARM64-mainnet - name: Create Release if: startsWith(github.ref, 'refs/tags/v') run: | mkdir -pv release cd release - tar -czvf ${{ github.ref_name }}-Linux-testnet.tar.gz -C ../build_artifacts/Linux-testnet/ . - tar -czvf ${{ github.ref_name }}-Linux-mainnet.tar.gz -C ../build_artifacts/Linux-mainnet/ . - tar -czvf ${{ github.ref_name }}-macOS-x86-testnet.tar.gz -C ../build_artifacts/macOS-x86-testnet/ . - tar -czvf ${{ github.ref_name }}-macOS-x86-mainnet.tar.gz -C ../build_artifacts/macOS-x86-mainnet/ . - tar -czvf ${{ github.ref_name }}-macOS-arm64-testnet.tar.gz -C ../build_artifacts/macOS-arm64-testnet/ . - tar -czvf ${{ github.ref_name }}-macOS-arm64-mainnet.tar.gz -C ../build_artifacts/macOS-arm64-mainnet/ . + tar -czvf ${{ github.ref_name }}-Linux-X64-testnet.tar.gz -C ../build_artifacts/Linux-X64-testnet/ . + tar -czvf ${{ github.ref_name }}-Linux-X64-mainnet.tar.gz -C ../build_artifacts/Linux-X64-mainnet/ . + tar -czvf ${{ github.ref_name }}-macOS-X64-testnet.tar.gz -C ../build_artifacts/macOS-X64-testnet/ . + tar -czvf ${{ github.ref_name }}-macOS-X64-mainnet.tar.gz -C ../build_artifacts/macOS-X64-mainnet/ . + tar -czvf ${{ github.ref_name }}-macOS-ARM64-testnet.tar.gz -C ../build_artifacts/macOS-ARM64-testnet/ . + tar -czvf ${{ github.ref_name }}-macOS-ARM64-mainnet.tar.gz -C ../build_artifacts/macOS-ARM64-mainnet/ . - name: Upload Release if: startsWith(github.ref, 'refs/tags/v') diff --git a/.gitignore b/.gitignore index d54cbf97d..fe344c864 100644 --- a/.gitignore +++ b/.gitignore @@ -73,3 +73,9 @@ release/ cli/build/** dbml-error.log + +*.profraw + +__pycache__ + +/tools/*.bin \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index fa3dfe140..d79596612 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -12,7 +12,7 @@ Please report issues to https://github.com/mobilecoinofficial/full-service/issue Please feel free to submit PRs! -When you submit PRs, please use the folowing workflow: +When you submit PRs, please use the following workflow: 1. Install the settings that enable the git hooks: diff --git a/Cargo.lock b/Cargo.lock index 84d06b92b..3fe17b590 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -55,13 +55,22 @@ dependencies = [ [[package]] name = "aho-corasick" -version = "0.7.10" +version = "0.7.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8716408b8bc624ed7f65d223ddb9ac2d044c0547b6fa4b0d554f3a9540496ada" +checksum = "1e37cfd5e7657ada45f742d6e99ca5788580b5c529dc78faf11ece6dc702656f" dependencies = [ "memchr", ] +[[package]] +name = "android_system_properties" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7ed72e1635e121ca3e79420540282af22da58be50de153d36f81ddc6b83aa9e" +dependencies = [ + "libc", +] + [[package]] name = "ansi_term" version = "0.11.0" @@ -82,9 +91,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.56" +version = "1.0.65" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4361135be9122e0870de935d7c439aef945b9f9ddd4199a553b5270b49c82a27" +checksum = "98161a4e3e2184da77bb14f02184cdd111e83bbbcc9979dfee3c44b9a85f5602" [[package]] name = "arc-swap" @@ -144,8 +153,8 @@ version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "10f203db73a71dfa2fb6dd22763990fa26f3d2625a6da2da900d23b87d26be27" dependencies = [ - "proc-macro2 1.0.39", - "quote 1.0.10", + "proc-macro2 1.0.46", + "quote 1.0.21", "syn 1.0.96", ] @@ -155,8 +164,8 @@ version = "0.1.52" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "061a7acccaa286c011ddc30970520b98fa40e00c9d644633fb26b5fc63a265e3" dependencies = [ - "proc-macro2 1.0.39", - "quote 1.0.10", + "proc-macro2 1.0.46", + "quote 1.0.21", "syn 1.0.96", ] @@ -238,8 +247,8 @@ dependencies = [ "lazycell", "log 0.4.11", "peeking_take_while", - "proc-macro2 1.0.39", - "quote 1.0.10", + "proc-macro2 1.0.46", + "quote 1.0.21", "regex", "rustc-hash", "shlex", @@ -261,8 +270,8 @@ dependencies = [ "lazycell", "log 0.4.11", "peeking_take_while", - "proc-macro2 1.0.39", - "quote 1.0.10", + "proc-macro2 1.0.46", + "quote 1.0.21", "regex", "rustc-hash", "shlex", @@ -299,16 +308,7 @@ version = "0.10.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9cf849ee05b2ee5fba5e36f97ff8ec2533916700fc0758d40d92136a42f3388" dependencies = [ - "digest 0.10.3", -] - -[[package]] -name = "block-buffer" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4" -dependencies = [ - "generic-array", + "digest", ] [[package]] @@ -343,7 +343,7 @@ dependencies = [ "byteorder", "clear_on_drop", "curve25519-dalek", - "digest 0.10.3", + "digest", "merlin", "rand_core 0.6.3", "serde", @@ -396,9 +396,9 @@ checksum = "631ae5198c9be5e753e5cc215e1bd73c2b466a3565173db433f52bb9d3e66dba" [[package]] name = "camino" -version = "1.0.5" +version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52d74260d9bf6944e2208aa46841b4b8f0d7ffc0849a06837b2f510337f86b2b" +checksum = "869119e97797867fd90f5e22af7d0bd274bd4635ebb9eb68c04f3f513ae6c412" dependencies = [ "serde", ] @@ -433,13 +433,13 @@ dependencies = [ [[package]] name = "cargo_metadata" -version = "0.14.1" +version = "0.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba2ae6de944143141f6155a473a6b02f66c7c3f9f47316f802f80204ebfe6e12" +checksum = "3abb7553d5b9b8421c6de7cb02606ff15e0c6eea7d8eadd75ef013fd636bec36" dependencies = [ "camino", "cargo-platform", - "semver 1.0.4", + "semver 1.0.10", "serde", "serde_json", ] @@ -491,14 +491,16 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "chrono" -version = "0.4.19" +version = "0.4.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "670ad68c9088c2a963aaa298cb369688cf3f9465ce5e2d4ca10e6e0098a1ce73" +checksum = "bfd4d1b31faaa3a89d7934dbded3111da0d2ef28e3ebccdb4f0179f5929d1ef1" dependencies = [ - "libc", + "iana-time-zone", + "js-sys", "num-integer", "num-traits", "time 0.1.43", + "wasm-bindgen", "winapi 0.3.9", ] @@ -539,16 +541,16 @@ dependencies = [ [[package]] name = "clap" -version = "3.1.18" +version = "3.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2dbdf4bdacb33466e854ce889eee8dfd5729abf7ccd7664d0a2d60cd384440b" +checksum = "5b7b16274bb247b45177db843202209b12191b631a14a9d06e41b3777d6ecf14" dependencies = [ "atty", "bitflags", "clap_derive", "clap_lex", "indexmap", - "lazy_static", + "once_cell", "strsim 0.10.0", "termcolor", "textwrap 0.15.0", @@ -556,22 +558,22 @@ dependencies = [ [[package]] name = "clap_derive" -version = "3.1.18" +version = "3.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25320346e922cffe59c0bbc5410c8d8784509efb321488971081313cb1e1a33c" +checksum = "759bf187376e1afa7b85b959e6a664a3e7a95203415dba952ad19139e798f902" dependencies = [ "heck 0.4.0", "proc-macro-error", - "proc-macro2 1.0.39", - "quote 1.0.10", + "proc-macro2 1.0.46", + "quote 1.0.21", "syn 1.0.96", ] [[package]] name = "clap_lex" -version = "0.2.0" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a37c35f1112dad5e6e0b1adaff798507497a18fceeb30cceb3bae7d1427b9213" +checksum = "2850f2f5a82cbf437dd5af4d49848fbdfc27c157c3d010345776f952765261c5" dependencies = [ "os_str_bytes", ] @@ -608,7 +610,7 @@ version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5795cda0897252e34380a27baf884c53aa7ad9990329cdad96d4c5d027015d44" dependencies = [ - "percent-encoding 2.1.0", + "percent-encoding 2.2.0", "time 0.1.43", ] @@ -621,12 +623,12 @@ dependencies = [ "aes-gcm", "base64 0.13.0", "hkdf", - "hmac 0.12.1", - "percent-encoding 2.1.0", + "hmac", + "percent-encoding 2.2.0", "rand 0.8.5", - "sha2 0.10.2", + "sha2", "subtle", - "time 0.3.7", + "time 0.3.14", "version_check 0.9.3", ] @@ -729,16 +731,6 @@ dependencies = [ "typenum", ] -[[package]] -name = "crypto-mac" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b584a330336237c1eecd3e94266efb216c56ed91225d634cb2991c5f3fd1aeab" -dependencies = [ - "generic-array", - "subtle", -] - [[package]] name = "ctr" version = "0.8.0" @@ -784,7 +776,7 @@ version = "4.0.0-pre.2" source = "git+https://github.com/mobilecoinfoundation/curve25519-dalek.git?rev=8791722e0273762552c9a056eaccb7df6baf44d7#8791722e0273762552c9a056eaccb7df6baf44d7" dependencies = [ "byteorder", - "digest 0.10.3", + "digest", "packed_simd_2", "rand_core 0.6.3", "serde", @@ -794,22 +786,22 @@ dependencies = [ [[package]] name = "debugid" -version = "0.7.2" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f91cf5a8c2f2097e2a32627123508635d47ce10563d999ec1a95addf08b502ba" +checksum = "bef552e6f588e446098f6ba40d89ac146c8c7b64aade83c051ee00bb5d2bc18d" dependencies = [ "serde", - "uuid 0.8.1", + "uuid", ] [[package]] name = "devise" -version = "0.2.0" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74e04ba2d03c5fa0d954c061fc8c9c288badadffc272ebb87679a89846de3ed3" +checksum = "dd716c4a507adc5a2aa7c2a372d06c7497727e0892b243d3036bc7478a13e526" dependencies = [ - "devise_codegen 0.2.0", - "devise_core 0.2.0", + "devise_codegen 0.2.1", + "devise_core 0.2.1", ] [[package]] @@ -824,11 +816,11 @@ dependencies = [ [[package]] name = "devise_codegen" -version = "0.2.0" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "066ceb7928ca93a9bedc6d0e612a8a0424048b0ab1f75971b203d01420c055d7" +checksum = "ea7b8290d118127c08e3669da20b331bed56b09f20be5945b7da6c116d8fab53" dependencies = [ - "devise_core 0.2.0", + "devise_core 0.2.1", "quote 0.6.13", ] @@ -839,14 +831,14 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "123c73e7a6e51b05c75fe1a1b2f4e241399ea5740ed810b0e3e6cacd9db5e7b2" dependencies = [ "devise_core 0.3.1", - "quote 1.0.10", + "quote 1.0.21", ] [[package]] name = "devise_core" -version = "0.2.0" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf41c59b22b5e3ec0ea55c7847e5f358d340f3a8d6d53a5cf4f1564967f96487" +checksum = "d1053e9d5d5aade9bcedb5ab53b78df2b56ff9408a3138ce77eaaef87f932373" dependencies = [ "bitflags", "proc-macro2 0.4.30", @@ -861,9 +853,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "841ef46f4787d9097405cac4e70fb8644fc037b526e8c14054247c0263c400d0" dependencies = [ "bitflags", - "proc-macro2 1.0.39", + "proc-macro2 1.0.46", "proc-macro2-diagnostics", - "quote 1.0.10", + "quote 1.0.21", "syn 1.0.96", ] @@ -873,6 +865,7 @@ version = "1.4.8" source = "git+https://github.com/mobilecoinofficial/diesel?rev=026f6379715d27c8be48396e5ca9059f4a263198#026f6379715d27c8be48396e5ca9059f4a263198" dependencies = [ "byteorder", + "chrono", "diesel_derives", "libsqlite3-sys", "r2d2", @@ -885,8 +878,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "70806b70be328e646f243680a3fc93b3cfdd6db373faa5110660a5dd5af243bc" dependencies = [ "heck 0.3.1", - "proc-macro2 1.0.39", - "quote 1.0.10", + "proc-macro2 1.0.46", + "quote 1.0.21", "syn 1.0.96", ] @@ -896,8 +889,8 @@ version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "45f5098f628d02a7a0f68ddba586fb61e80edec3bdc1be3b921f4ceec60858d3" dependencies = [ - "proc-macro2 1.0.39", - "quote 1.0.10", + "proc-macro2 1.0.46", + "quote 1.0.21", "syn 1.0.96", ] @@ -917,22 +910,13 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6184e33543162437515c2e2b48714794e37845ec9851711914eec9d308f6ebe8" -[[package]] -name = "digest" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066" -dependencies = [ - "generic-array", -] - [[package]] name = "digest" version = "0.10.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2fb860ca6fafa5552fb6d0e816a69c8e49f0908bf524e30a90d97c85892d506" dependencies = [ - "block-buffer 0.10.2", + "block-buffer", "crypto-common", "subtle", ] @@ -964,8 +948,8 @@ version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3bf95dc3f046b9da4f2d51833c0d3547d8564ef6910f5c1ed130306a75b92886" dependencies = [ - "proc-macro2 1.0.39", - "quote 1.0.10", + "proc-macro2 1.0.46", + "quote 1.0.21", "syn 1.0.96", ] @@ -1001,7 +985,7 @@ dependencies = [ "rand 0.8.5", "serde", "serde_bytes", - "sha2 0.10.2", + "sha2", "zeroize", ] @@ -1022,21 +1006,21 @@ dependencies = [ [[package]] name = "enum-iterator" -version = "0.7.0" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4eeac5c5edb79e4e39fe8439ef35207780a11f69c52cbe424ce3dfad4cb78de6" +checksum = "45a0ac4aeb3a18f92eaf09c6bb9b3ac30ff61ca95514fc58cbead1c9a6bf5401" dependencies = [ "enum-iterator-derive", ] [[package]] name = "enum-iterator-derive" -version = "0.7.0" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c134c37760b27a871ba422106eedbb8247da973a09e82558bf26d619c882b159" +checksum = "828de45d0ca18782232dfb8f3ea9cc428e8ced380eb26a520baaacfc70de39ce" dependencies = [ - "proc-macro2 1.0.39", - "quote 1.0.10", + "proc-macro2 1.0.46", + "quote 1.0.21", "syn 1.0.96", ] @@ -1107,8 +1091,8 @@ version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aa4da3c766cd7a0db8242e326e9e4e081edd567072893ed320008189715366a4" dependencies = [ - "proc-macro2 1.0.39", - "quote 1.0.10", + "proc-macro2 1.0.46", + "quote 1.0.21", "syn 1.0.96", "synstructure", ] @@ -1189,12 +1173,11 @@ checksum = "2fad85553e09a6f881f739c29f0b00b0f01357c743266d478b68951ce23285f3" [[package]] name = "form_urlencoded" -version = "1.0.1" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5fc25a87fa4fd2094bffb06925852034d90a17f0d1e05197d4956d3555752191" +checksum = "a9c384f161156f5260c24a097c56119f9be8c798586aecc13afbcbe7b7e26bf8" dependencies = [ - "matches", - "percent-encoding 2.1.0", + "percent-encoding 2.2.0", ] [[package]] @@ -1321,8 +1304,8 @@ checksum = "18e4a4b95cea4b4ccbcf1c5675ca7c4ee4e9e75eb79944d07defde18068f79bb" dependencies = [ "autocfg", "proc-macro-hack", - "proc-macro2 1.0.39", - "quote 1.0.10", + "proc-macro2 1.0.46", + "quote 1.0.21", "syn 1.0.96", ] @@ -1401,7 +1384,6 @@ dependencies = [ "cfg-if 0.1.10", "libc", "wasi 0.9.0+wasi-snapshot-preview1", - "wasm-bindgen", ] [[package]] @@ -1422,8 +1404,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e45727250e75cc04ff2846a66397da8ef2b3db8e40e0cef4df67950a07621eb9" dependencies = [ "proc-macro-error", - "proc-macro2 1.0.39", - "quote 1.0.10", + "proc-macro2 1.0.46", + "quote 1.0.21", "syn 1.0.96", ] @@ -1445,15 +1427,15 @@ checksum = "81a03ce013ffccead76c11a15751231f777d9295b845cc1266ed4d34fcbd7977" [[package]] name = "git2" -version = "0.14.2" +version = "0.14.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3826a6e0e2215d7a41c2bfc7c9244123969273f3476b939a226aac0ab56e9e3c" +checksum = "d0155506aab710a86160ddb504a480d2964d7ab5b9e62419be69e0032bc5931c" dependencies = [ "bitflags", "libc", "libgit2-sys", "log 0.4.11", - "url 2.2.2", + "url 2.3.1", ] [[package]] @@ -1470,9 +1452,9 @@ checksum = "9b919933a397b79c37e33b77bb2aa3dc8eb6e165ad809e58ff75bc7db2e34574" [[package]] name = "grpcio" -version = "0.10.2" +version = "0.10.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86ef249d9cb1b1843767501ae7463b500542e7f9e72d9c2d61ed320fbefa6c79" +checksum = "f9bcdd3694fa08158334501af37bdf5b4f00b1865b602d917e3cd74ecf80cd0a" dependencies = [ "futures-executor", "futures-util", @@ -1494,9 +1476,9 @@ dependencies = [ [[package]] name = "grpcio-sys" -version = "0.10.1+1.44.0" +version = "0.10.3+1.44.0-patched" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "925586932dbbea927e913783da0be160ee74e0b0519d7b20cec35547a0a84631" +checksum = "f23adc509a3c4dea990e0ab8d2add4a65389ee69c288b7851d75dd1df7a6d6c6" dependencies = [ "bindgen 0.59.2", "boringssl-src", @@ -1541,9 +1523,9 @@ checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e" [[package]] name = "hashbrown" -version = "0.12.1" +version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db0d4cf898abf0081f964436dc980e96670a0f36863e4b83aaacdb65c9d7ccc3" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" dependencies = [ "serde", ] @@ -1590,17 +1572,7 @@ version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "791a029f6b9fc27657f6f188ec6e5e43f6911f6f878e0dc5501396e09809d437" dependencies = [ - "hmac 0.12.1", -] - -[[package]] -name = "hmac" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "126888268dcc288495a26bf004b38c5fdbb31682f992c84ceb046a1f0fe38840" -dependencies = [ - "crypto-mac", - "digest 0.9.0", + "hmac", ] [[package]] @@ -1609,7 +1581,7 @@ version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" dependencies = [ - "digest 0.10.3", + "digest", ] [[package]] @@ -1725,6 +1697,19 @@ dependencies = [ "tokio-rustls", ] +[[package]] +name = "iana-time-zone" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad2bfd338099682614d3ee3fe0cd72e0b6a41ca6a87f6a74a3bd593c91650501" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "js-sys", + "wasm-bindgen", + "winapi 0.3.9", +] + [[package]] name = "idna" version = "0.1.5" @@ -1738,11 +1723,10 @@ dependencies = [ [[package]] name = "idna" -version = "0.2.0" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02e2673c30ee86b5b96a9cb52ad15718aa1f966f5ab9ad54a8b95d5ca33120a9" +checksum = "e14ddfc70884202db2244c223200c204c2bda1bc6e0998d11b5e024d657209e6" dependencies = [ - "matches", "unicode-bidi", "unicode-normalization", ] @@ -1762,8 +1746,8 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "11d7a9f6330b71fea57921c9b61c47ee6e84f72d394754eff6163ae67e7395eb" dependencies = [ - "proc-macro2 1.0.39", - "quote 1.0.10", + "proc-macro2 1.0.46", + "quote 1.0.21", "syn 1.0.96", ] @@ -1855,7 +1839,7 @@ dependencies = [ "sluice", "tracing", "tracing-futures", - "url 2.2.2", + "url 2.3.1", "waker-fn", ] @@ -1891,9 +1875,9 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.55" +version = "0.3.59" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7cc9ffccd38c451a86bf13657df244e9c3f37493cce8e5e21e940963777acc84" +checksum = "258451ab10b34f8af53416d1fdab72c22e805f0c92a1136d59470ec0b11138b2" dependencies = [ "wasm-bindgen", ] @@ -1943,9 +1927,9 @@ checksum = "349d5a591cd28b49e1d1037471617a32ddcda5731b99419008085f72d5a53836" [[package]] name = "libgit2-sys" -version = "0.13.2+1.4.2" +version = "0.13.4+1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a42de9a51a5c12e00fc0e4ca6bc2ea43582fc6418488e8f615e905d886f258b" +checksum = "d0fa6563431ede25f5cc7f6d803c6afbc1c5d3ad3d4925d12c882bf2b526f5d1" dependencies = [ "cc", "libc", @@ -2114,17 +2098,18 @@ dependencies = [ "lazy_static", "libc", "libz-sys", - "quote 1.0.10", + "quote 1.0.21", "syn 1.0.96", ] [[package]] name = "mc-account-keys" -version = "1.2.2" +version = "2.0.0" dependencies = [ "curve25519-dalek", "displaydoc", "hkdf", + "mc-account-keys-types", "mc-crypto-digestible", "mc-crypto-hashes", "mc-crypto-keys", @@ -2140,22 +2125,29 @@ dependencies = [ [[package]] name = "mc-account-keys-slip10" -version = "1.2.2" +version = "2.0.0" dependencies = [ "curve25519-dalek", "displaydoc", "hkdf", "mc-account-keys", "mc-crypto-keys", - "sha2 0.10.2", + "sha2", "slip10_ed25519", "tiny-bip39", "zeroize", ] +[[package]] +name = "mc-account-keys-types" +version = "2.0.0" +dependencies = [ + "mc-crypto-keys", +] + [[package]] name = "mc-api" -version = "1.2.2" +version = "2.0.0" dependencies = [ "bs58", "cargo-emit", @@ -2163,10 +2155,14 @@ dependencies = [ "curve25519-dalek", "displaydoc", "mc-account-keys", - "mc-attest-core", + "mc-attest-verifier-types", + "mc-blockchain-types", + "mc-common", "mc-crypto-keys", "mc-crypto-multisig", + "mc-crypto-ring-signature-signer", "mc-transaction-core", + "mc-transaction-std", "mc-util-build-grpc", "mc-util-build-script", "mc-util-repr-bytes", @@ -2177,11 +2173,11 @@ dependencies = [ [[package]] name = "mc-attest-ake" -version = "1.2.2" +version = "2.0.0" dependencies = [ "aead", "cargo-emit", - "digest 0.10.3", + "digest", "displaydoc", "mc-attest-core", "mc-attest-verifier", @@ -2196,11 +2192,11 @@ dependencies = [ [[package]] name = "mc-attest-api" -version = "1.2.2" +version = "2.0.0" dependencies = [ "aead", "cargo-emit", - "digest 0.10.3", + "digest", "futures", "grpcio", "mc-attest-ake", @@ -2214,33 +2210,36 @@ dependencies = [ [[package]] name = "mc-attest-core" -version = "1.2.2" +version = "2.0.0" dependencies = [ - "binascii", + "base64 0.13.0", "bitflags", "cargo-emit", "chrono", - "digest 0.10.3", + "digest", "displaydoc", + "hex", "hex_fmt", + "mc-attest-verifier-types", "mc-common", "mc-crypto-digestible", - "mc-crypto-rand", "mc-sgx-css", "mc-sgx-types", "mc-util-build-script", "mc-util-build-sgx", "mc-util-encodings", + "mc-util-repr-bytes", "prost", + "rand_core 0.6.3", "rjson", "serde", - "sha2 0.10.2", + "sha2", "subtle", ] [[package]] name = "mc-attest-enclave-api" -version = "1.2.2" +version = "2.0.0" dependencies = [ "displaydoc", "mc-attest-ake", @@ -2253,13 +2252,14 @@ dependencies = [ [[package]] name = "mc-attest-verifier" -version = "1.2.2" +version = "2.0.0" dependencies = [ "cargo-emit", "cfg-if 1.0.0", "chrono", "displaydoc", "hex", + "hex_fmt", "lazy_static", "mbedtls", "mbedtls-sys-auto", @@ -2272,19 +2272,70 @@ dependencies = [ "rand 0.8.5", "rand_hc 0.3.1", "serde", - "sha2 0.10.2", + "sha2", +] + +[[package]] +name = "mc-attest-verifier-types" +version = "2.0.0" +dependencies = [ + "base64 0.13.0", + "displaydoc", + "hex", + "hex_fmt", + "mc-crypto-digestible", + "mc-util-encodings", + "prost", + "serde", +] + +[[package]] +name = "mc-blockchain-test-utils" +version = "2.0.0" +dependencies = [ + "mc-blockchain-types", + "mc-common", + "mc-consensus-scp-types", + "mc-crypto-keys", + "mc-transaction-core", + "mc-transaction-core-test-utils", + "mc-util-from-random", + "mc-util-test-helper", +] + +[[package]] +name = "mc-blockchain-types" +version = "2.0.0" +dependencies = [ + "displaydoc", + "hex_fmt", + "mc-account-keys", + "mc-attest-verifier-types", + "mc-common", + "mc-consensus-scp-types", + "mc-crypto-digestible", + "mc-crypto-digestible-signature", + "mc-crypto-keys", + "mc-crypto-ring-signature", + "mc-transaction-core", + "mc-transaction-types", + "mc-util-from-random", + "mc-util-repr-bytes", + "prost", + "serde", + "zeroize", ] [[package]] name = "mc-common" -version = "1.2.2" +version = "2.0.0" dependencies = [ "backtrace", - "binascii", "cfg-if 1.0.0", "chrono", "displaydoc", - "hashbrown 0.12.1", + "hashbrown 0.12.3", + "hex", "hex_fmt", "hostname", "lazy_static", @@ -2294,6 +2345,7 @@ dependencies = [ "mc-util-build-info", "mc-util-logger-macros", "mc-util-serial", + "prost", "rand_core 0.6.3", "sentry", "serde", @@ -2312,7 +2364,7 @@ dependencies = [ [[package]] name = "mc-connection" -version = "1.2.2" +version = "2.0.0" dependencies = [ "aes-gcm", "cookie 0.16.0", @@ -2322,6 +2374,7 @@ dependencies = [ "mc-attest-api", "mc-attest-core", "mc-attest-verifier", + "mc-blockchain-types", "mc-common", "mc-consensus-api", "mc-crypto-keys", @@ -2333,13 +2386,14 @@ dependencies = [ "mc-util-uri", "retry", "secrecy", - "sha2 0.10.2", + "sha2", ] [[package]] name = "mc-connection-test-utils" -version = "1.2.2" +version = "2.0.0" dependencies = [ + "mc-blockchain-types", "mc-connection", "mc-consensus-enclave-api", "mc-ledger-db", @@ -2349,7 +2403,7 @@ dependencies = [ [[package]] name = "mc-consensus-api" -version = "1.2.2" +version = "2.0.0" dependencies = [ "cargo-emit", "futures", @@ -2365,13 +2419,14 @@ dependencies = [ [[package]] name = "mc-consensus-enclave-api" -version = "1.2.2" +version = "2.0.0" dependencies = [ "displaydoc", "hex", "mc-attest-ake", "mc-attest-core", "mc-attest-enclave-api", + "mc-blockchain-types", "mc-common", "mc-crypto-digestible", "mc-crypto-keys", @@ -2386,7 +2441,7 @@ dependencies = [ [[package]] name = "mc-consensus-enclave-measurement" -version = "1.2.2" +version = "2.0.0" dependencies = [ "cargo-emit", "mc-attest-core", @@ -2399,10 +2454,11 @@ dependencies = [ [[package]] name = "mc-consensus-scp" -version = "1.2.2" +version = "2.0.0" dependencies = [ "maplit", "mc-common", + "mc-consensus-scp-types", "mc-crypto-digestible", "mc-crypto-keys", "mc-util-from-random", @@ -2415,12 +2471,27 @@ dependencies = [ "serde_json", ] +[[package]] +name = "mc-consensus-scp-types" +version = "2.0.0" +dependencies = [ + "mc-common", + "mc-crypto-digestible", + "mc-crypto-keys", + "mc-util-from-random", + "mc-util-test-helper", + "prost", + "rand 0.8.5", + "rand_hc 0.3.1", + "serde", +] + [[package]] name = "mc-crypto-box" -version = "1.2.2" +version = "2.0.0" dependencies = [ "aead", - "digest 0.10.3", + "digest", "displaydoc", "hkdf", "mc-crypto-hashes", @@ -2429,9 +2500,18 @@ dependencies = [ "rand_core 0.6.3", ] +[[package]] +name = "mc-crypto-dalek" +version = "2.0.0" +dependencies = [ + "curve25519-dalek", + "ed25519-dalek", + "x25519-dalek", +] + [[package]] name = "mc-crypto-digestible" -version = "1.2.2" +version = "2.0.0" dependencies = [ "cfg-if 1.0.0", "curve25519-dalek", @@ -2444,16 +2524,16 @@ dependencies = [ [[package]] name = "mc-crypto-digestible-derive" -version = "1.2.2" +version = "2.0.0" dependencies = [ - "proc-macro2 1.0.39", - "quote 1.0.10", + "proc-macro2 1.0.46", + "quote 1.0.21", "syn 1.0.96", ] [[package]] name = "mc-crypto-digestible-signature" -version = "1.2.2" +version = "2.0.0" dependencies = [ "mc-crypto-digestible", "schnorrkel-og", @@ -2462,24 +2542,26 @@ dependencies = [ [[package]] name = "mc-crypto-hashes" -version = "1.2.2" +version = "2.0.0" dependencies = [ "blake2", - "digest 0.10.3", + "digest", "mc-crypto-digestible", ] [[package]] name = "mc-crypto-keys" -version = "1.2.2" +version = "2.0.0" dependencies = [ - "binascii", + "base64 0.13.0", "curve25519-dalek", - "digest 0.10.3", + "digest", "displaydoc", "ed25519", "ed25519-dalek", + "hex", "hex_fmt", + "mc-crypto-dalek", "mc-crypto-digestible", "mc-crypto-digestible-signature", "mc-util-from-random", @@ -2488,8 +2570,9 @@ dependencies = [ "rand_hc 0.3.1", "schnorrkel-og", "serde", - "sha2 0.10.2", + "sha2", "signature", + "static_assertions", "subtle", "x25519-dalek", "zeroize", @@ -2497,7 +2580,7 @@ dependencies = [ [[package]] name = "mc-crypto-message-cipher" -version = "1.2.2" +version = "2.0.0" dependencies = [ "aes-gcm", "displaydoc", @@ -2510,7 +2593,7 @@ dependencies = [ [[package]] name = "mc-crypto-multisig" -version = "1.2.2" +version = "2.0.0" dependencies = [ "mc-crypto-digestible", "mc-crypto-keys", @@ -2520,11 +2603,11 @@ dependencies = [ [[package]] name = "mc-crypto-noise" -version = "1.2.2" +version = "2.0.0" dependencies = [ "aead", "aes-gcm", - "digest 0.10.3", + "digest", "displaydoc", "generic-array", "hkdf", @@ -2533,25 +2616,68 @@ dependencies = [ "rand_core 0.6.3", "secrecy", "serde", - "sha2 0.10.2", + "sha2", "subtle", "zeroize", ] [[package]] name = "mc-crypto-rand" -version = "1.2.2" +version = "2.0.0" dependencies = [ "cfg-if 1.0.0", - "getrandom 0.2.3", "rand 0.8.5", "rand_core 0.6.3", - "rand_hc 0.3.1", +] + +[[package]] +name = "mc-crypto-ring-signature" +version = "2.0.0" +dependencies = [ + "curve25519-dalek", + "displaydoc", + "hex_fmt", + "mc-account-keys", + "mc-account-keys-types", + "mc-crypto-dalek", + "mc-crypto-digestible", + "mc-crypto-hashes", + "mc-crypto-keys", + "mc-transaction-types", + "mc-util-from-random", + "mc-util-repr-bytes", + "mc-util-serial", + "prost", + "rand_core 0.6.3", + "serde", + "subtle", + "zeroize", +] + +[[package]] +name = "mc-crypto-ring-signature-signer" +version = "2.0.0" +dependencies = [ + "curve25519-dalek", + "displaydoc", + "generic-array", + "hex_fmt", + "mc-account-keys", + "mc-crypto-dalek", + "mc-crypto-keys", + "mc-crypto-ring-signature", + "mc-transaction-types", + "mc-util-serial", + "prost", + "rand_core 0.6.3", + "serde", + "subtle", + "zeroize", ] [[package]] name = "mc-crypto-x509-utils" -version = "1.2.2" +version = "2.0.0" dependencies = [ "displaydoc", "mc-crypto-keys", @@ -2559,9 +2685,21 @@ dependencies = [ "x509-signature", ] +[[package]] +name = "mc-fog-ingest-report" +version = "2.0.0" +dependencies = [ + "displaydoc", + "mc-attest-core", + "mc-attest-verifier", + "mc-crypto-keys", + "mc-util-encodings", + "serde", +] + [[package]] name = "mc-fog-report-api" -version = "1.2.2" +version = "2.0.0" dependencies = [ "cargo-emit", "futures", @@ -2577,7 +2715,7 @@ dependencies = [ [[package]] name = "mc-fog-report-connection" -version = "1.2.2" +version = "2.0.0" dependencies = [ "displaydoc", "grpcio", @@ -2592,9 +2730,23 @@ dependencies = [ "mc-util-uri", ] +[[package]] +name = "mc-fog-report-resolver" +version = "2.0.0" +dependencies = [ + "mc-account-keys", + "mc-attest-verifier", + "mc-fog-ingest-report", + "mc-fog-report-types", + "mc-fog-report-validation", + "mc-fog-sig", + "mc-util-uri", + "serde", +] + [[package]] name = "mc-fog-report-types" -version = "1.2.2" +version = "2.0.0" dependencies = [ "mc-attest-core", "mc-crypto-digestible", @@ -2604,25 +2756,20 @@ dependencies = [ [[package]] name = "mc-fog-report-validation" -version = "1.2.2" +version = "2.0.0" dependencies = [ "displaydoc", "mc-account-keys", - "mc-attest-core", - "mc-attest-verifier", "mc-crypto-keys", - "mc-fog-report-types", "mc-fog-sig", - "mc-util-encodings", "mc-util-serial", "mc-util-uri", "mockall", - "serde", ] [[package]] name = "mc-fog-report-validation-test-utils" -version = "1.2.2" +version = "2.0.0" dependencies = [ "mc-account-keys", "mc-fog-report-validation", @@ -2630,7 +2777,7 @@ dependencies = [ [[package]] name = "mc-fog-sig" -version = "1.2.2" +version = "2.0.0" dependencies = [ "displaydoc", "mc-account-keys", @@ -2646,7 +2793,7 @@ dependencies = [ [[package]] name = "mc-fog-sig-authority" -version = "1.2.2" +version = "2.0.0" dependencies = [ "mc-crypto-keys", "signature", @@ -2654,7 +2801,7 @@ dependencies = [ [[package]] name = "mc-fog-sig-report" -version = "1.2.2" +version = "2.0.0" dependencies = [ "displaydoc", "mc-attest-core", @@ -2666,7 +2813,7 @@ dependencies = [ [[package]] name = "mc-full-service" -version = "1.9.2" +version = "2.0.0" dependencies = [ "anyhow", "base64 0.13.0", @@ -2684,15 +2831,20 @@ dependencies = [ "mc-account-keys-slip10", "mc-api", "mc-attest-verifier", + "mc-blockchain-test-utils", + "mc-blockchain-types", "mc-common", "mc-connection", "mc-connection-test-utils", + "mc-consensus-enclave-api", "mc-consensus-enclave-measurement", "mc-consensus-scp", "mc-crypto-digestible", "mc-crypto-keys", "mc-crypto-rand", + "mc-crypto-ring-signature-signer", "mc-fog-report-connection", + "mc-fog-report-resolver", "mc-fog-report-validation", "mc-fog-report-validation-test-utils", "mc-ledger-db", @@ -2704,6 +2856,7 @@ dependencies = [ "mc-sgx-css", "mc-transaction-core", "mc-transaction-std", + "mc-transaction-types", "mc-util-from-random", "mc-util-parse", "mc-util-serial", @@ -2711,13 +2864,15 @@ dependencies = [ "mc-validator-api", "mc-validator-connection", "num_cpus", + "protobuf", "rand 0.8.5", "rayon", "reqwest", "retry", - "rocket 0.4.10", + "rocket 0.4.11", "rocket_contrib", "serde", + "serde-big-array", "serde_derive", "serde_json", "structopt", @@ -2725,37 +2880,42 @@ dependencies = [ "strum_macros", "tempdir", "tiny-bip39", - "uuid 1.1.1", + "uuid", "vergen", ] [[package]] name = "mc-ledger-db" -version = "1.2.2" +version = "2.0.0" dependencies = [ "displaydoc", "lazy_static", "lmdb-rkv", "mc-account-keys", + "mc-blockchain-test-utils", + "mc-blockchain-types", "mc-common", "mc-crypto-keys", "mc-transaction-core", + "mc-transaction-core-test-utils", + "mc-transaction-std", "mc-util-from-random", "mc-util-lmdb", "mc-util-metrics", "mc-util-serial", "mc-util-telemetry", + "mc-util-test-helper", "mockall", "prost", "rand 0.8.5", - "rand_core 0.6.3", + "tempdir", ] [[package]] name = "mc-ledger-migration" -version = "1.2.2" +version = "2.0.0" dependencies = [ - "clap 3.1.18", + "clap 3.2.7", "lmdb-rkv", "mc-common", "mc-ledger-db", @@ -2766,7 +2926,7 @@ dependencies = [ [[package]] name = "mc-ledger-sync" -version = "1.2.2" +version = "2.0.0" dependencies = [ "crossbeam-channel", "displaydoc", @@ -2774,6 +2934,8 @@ dependencies = [ "mc-account-keys", "mc-api", "mc-attest-verifier", + "mc-blockchain-test-utils", + "mc-blockchain-types", "mc-common", "mc-connection", "mc-consensus-enclave-measurement", @@ -2790,15 +2952,15 @@ dependencies = [ "retry", "serde", "tempdir", - "url 2.2.2", + "url 2.3.1", ] [[package]] name = "mc-mobilecoind" -version = "1.2.2" +version = "2.0.0" dependencies = [ "aes-gcm", - "clap 3.1.18", + "clap 3.2.7", "crossbeam-channel", "displaydoc", "grpcio", @@ -2810,16 +2972,20 @@ dependencies = [ "mc-api", "mc-attest-core", "mc-attest-verifier", + "mc-blockchain-types", "mc-common", "mc-connection", "mc-consensus-api", + "mc-consensus-enclave-api", "mc-consensus-enclave-measurement", "mc-consensus-scp", "mc-crypto-digestible", "mc-crypto-hashes", "mc-crypto-keys", "mc-crypto-rand", + "mc-crypto-ring-signature-signer", "mc-fog-report-connection", + "mc-fog-report-resolver", "mc-fog-report-validation", "mc-ledger-db", "mc-ledger-migration", @@ -2850,12 +3016,13 @@ dependencies = [ [[package]] name = "mc-mobilecoind-api" -version = "1.2.2" +version = "2.0.0" dependencies = [ "cargo-emit", "futures", "grpcio", "mc-api", + "mc-consensus-api", "mc-util-build-grpc", "mc-util-build-script", "mc-util-uri", @@ -2864,15 +3031,16 @@ dependencies = [ [[package]] name = "mc-mobilecoind-json" -version = "1.2.2" +version = "2.0.0" dependencies = [ - "clap 3.1.18", + "clap 3.2.7", "grpcio", "hex", "mc-api", "mc-common", "mc-mobilecoind-api", "mc-util-grpc", + "mc-util-serial", "protobuf", "rocket 0.5.0-rc.2", "serde", @@ -2896,7 +3064,7 @@ dependencies = [ [[package]] name = "mc-sgx-compat" -version = "1.2.2" +version = "2.0.0" dependencies = [ "cfg-if 1.0.0", "mc-sgx-types", @@ -2904,15 +3072,15 @@ dependencies = [ [[package]] name = "mc-sgx-css" -version = "1.2.2" +version = "2.0.0" dependencies = [ "displaydoc", - "sha2 0.10.2", + "sha2", ] [[package]] name = "mc-sgx-report-cache-api" -version = "1.2.2" +version = "2.0.0" dependencies = [ "displaydoc", "mc-attest-core", @@ -2923,11 +3091,11 @@ dependencies = [ [[package]] name = "mc-sgx-types" -version = "1.2.2" +version = "2.0.0" [[package]] name = "mc-transaction-core" -version = "1.2.2" +version = "2.0.0" dependencies = [ "aes", "bulletproofs-og", @@ -2945,63 +3113,102 @@ dependencies = [ "mc-crypto-hashes", "mc-crypto-keys", "mc-crypto-multisig", + "mc-crypto-ring-signature", + "mc-crypto-ring-signature-signer", + "mc-transaction-types", "mc-util-from-random", "mc-util-repr-bytes", "mc-util-serial", + "mc-util-zip-exact", "merlin", "prost", "rand_core 0.6.3", "serde", - "sha2 0.10.2", + "sha2", "subtle", "zeroize", ] [[package]] name = "mc-transaction-core-test-utils" -version = "1.2.2" +version = "2.0.0" dependencies = [ "mc-account-keys", "mc-crypto-keys", "mc-crypto-multisig", "mc-crypto-rand", + "mc-crypto-ring-signature-signer", "mc-fog-report-validation-test-utils", - "mc-ledger-db", "mc-transaction-core", - "mc-transaction-std", "mc-util-from-random", + "mc-util-serial", +] + +[[package]] +name = "mc-transaction-signer" +version = "2.0.0" +dependencies = [ + "base64 0.13.0", + "hex", + "mc-account-keys", + "mc-account-keys-slip10", + "mc-common", + "mc-crypto-keys", + "mc-crypto-ring-signature-signer", + "mc-full-service", + "mc-transaction-core", + "mc-transaction-std", + "mc-util-serial", "rand 0.8.5", - "tempdir", + "serde", + "serde_json", + "structopt", + "tiny-bip39", ] [[package]] name = "mc-transaction-std" -version = "1.2.2" +version = "2.0.0" dependencies = [ "cfg-if 1.0.0", "curve25519-dalek", "displaydoc", - "hmac 0.12.1", + "hmac", "mc-account-keys", + "mc-crypto-hashes", "mc-crypto-keys", + "mc-crypto-ring-signature-signer", "mc-fog-report-validation", "mc-transaction-core", + "mc-transaction-types", "mc-util-from-random", "mc-util-serial", "prost", "rand 0.8.5", "rand_core 0.6.3", - "sha2 0.10.2", + "serde", + "sha2", + "subtle", + "zeroize", +] + +[[package]] +name = "mc-transaction-types" +version = "2.0.0" +dependencies = [ + "displaydoc", + "mc-crypto-digestible", + "serde", "subtle", "zeroize", ] [[package]] name = "mc-util-build-enclave" -version = "1.2.2" +version = "2.0.0" dependencies = [ "cargo-emit", - "cargo_metadata 0.14.1", + "cargo_metadata 0.15.0", "displaydoc", "mbedtls", "mbedtls-sys-auto", @@ -3014,7 +3221,7 @@ dependencies = [ [[package]] name = "mc-util-build-grpc" -version = "1.2.2" +version = "2.0.0" dependencies = [ "mc-util-build-script", "protoc-grpcio", @@ -3022,25 +3229,25 @@ dependencies = [ [[package]] name = "mc-util-build-info" -version = "1.2.2" +version = "2.0.0" dependencies = [ "cargo-emit", ] [[package]] name = "mc-util-build-script" -version = "1.2.2" +version = "2.0.0" dependencies = [ "cargo-emit", "displaydoc", "lazy_static", - "url 2.2.2", + "url 2.3.1", "walkdir", ] [[package]] name = "mc-util-build-sgx" -version = "1.2.2" +version = "2.0.0" dependencies = [ "cargo-emit", "cc", @@ -3051,10 +3258,9 @@ dependencies = [ [[package]] name = "mc-util-encodings" -version = "1.2.2" +version = "2.0.0" dependencies = [ "base64 0.13.0", - "binascii", "displaydoc", "hex", "mc-util-repr-bytes", @@ -3063,24 +3269,24 @@ dependencies = [ [[package]] name = "mc-util-from-random" -version = "1.2.2" +version = "2.0.0" dependencies = [ "rand_core 0.6.3", ] [[package]] name = "mc-util-grpc" -version = "1.2.2" +version = "2.0.0" dependencies = [ "base64 0.13.0", - "clap 3.1.18", + "clap 3.2.7", "cookie 0.16.0", "displaydoc", "futures", "grpcio", "hex", "hex_fmt", - "hmac 0.12.1", + "hmac", "lazy_static", "mc-common", "mc-util-build-grpc", @@ -3093,7 +3299,7 @@ dependencies = [ "rand 0.8.5", "retry", "serde", - "sha2 0.10.2", + "sha2", "signal-hook", "subtle", "zeroize", @@ -3101,11 +3307,11 @@ dependencies = [ [[package]] name = "mc-util-host-cert" -version = "1.2.2" +version = "2.0.0" [[package]] name = "mc-util-lmdb" -version = "1.2.2" +version = "2.0.0" dependencies = [ "displaydoc", "lmdb-rkv", @@ -3115,16 +3321,16 @@ dependencies = [ [[package]] name = "mc-util-logger-macros" -version = "1.2.2" +version = "2.0.0" dependencies = [ - "proc-macro2 1.0.39", - "quote 1.0.10", + "proc-macro2 1.0.46", + "quote 1.0.21", "syn 1.0.96", ] [[package]] name = "mc-util-metrics" -version = "1.2.2" +version = "2.0.0" dependencies = [ "chrono", "grpcio", @@ -3137,7 +3343,7 @@ dependencies = [ [[package]] name = "mc-util-parse" -version = "1.2.2" +version = "2.0.0" dependencies = [ "itertools", "mc-sgx-css", @@ -3145,25 +3351,28 @@ dependencies = [ [[package]] name = "mc-util-repr-bytes" -version = "1.2.2" +version = "2.0.0" dependencies = [ "generic-array", + "hex_fmt", "prost", "serde", ] [[package]] name = "mc-util-serial" -version = "1.2.2" +version = "2.0.0" dependencies = [ "prost", + "protobuf", "serde", "serde_cbor", + "serde_with", ] [[package]] name = "mc-util-telemetry" -version = "1.2.2" +version = "2.0.0" dependencies = [ "cfg-if 1.0.0", "displaydoc", @@ -3172,9 +3381,20 @@ dependencies = [ "opentelemetry-jaeger", ] +[[package]] +name = "mc-util-test-helper" +version = "2.0.0" +dependencies = [ + "clap 3.2.7", + "lazy_static", + "mc-account-keys", + "rand 0.8.5", + "rand_hc 0.3.1", +] + [[package]] name = "mc-util-uri" -version = "1.2.2" +version = "2.0.0" dependencies = [ "base64 0.13.0", "displaydoc", @@ -3182,14 +3402,21 @@ dependencies = [ "mc-common", "mc-crypto-keys", "mc-util-host-cert", - "percent-encoding 2.1.0", + "percent-encoding 2.2.0", + "serde", + "url 2.3.1", +] + +[[package]] +name = "mc-util-zip-exact" +version = "2.0.0" +dependencies = [ "serde", - "url 2.2.2", ] [[package]] name = "mc-validator-api" -version = "1.0.0" +version = "2.0.0" dependencies = [ "cargo-emit", "futures", @@ -3205,15 +3432,16 @@ dependencies = [ [[package]] name = "mc-validator-connection" -version = "1.0.0" +version = "2.0.0" dependencies = [ "displaydoc", "futures", "grpcio", "mc-api", + "mc-blockchain-types", "mc-common", "mc-connection", - "mc-fog-report-validation", + "mc-fog-report-types", "mc-transaction-core", "mc-util-grpc", "mc-util-uri", @@ -3223,7 +3451,7 @@ dependencies = [ [[package]] name = "mc-validator-service" -version = "1.0.0" +version = "2.0.0" dependencies = [ "grpcio", "mc-attest-verifier", @@ -3245,9 +3473,9 @@ dependencies = [ [[package]] name = "mc-watcher" -version = "1.2.2" +version = "2.0.0" dependencies = [ - "clap 3.1.18", + "clap 3.2.7", "displaydoc", "futures", "grpcio", @@ -3257,14 +3485,13 @@ dependencies = [ "mc-api", "mc-attest-core", "mc-attest-verifier", + "mc-blockchain-types", "mc-common", "mc-connection", "mc-crypto-digestible", "mc-crypto-keys", "mc-ledger-db", "mc-ledger-sync", - "mc-transaction-core", - "mc-transaction-core-test-utils", "mc-util-from-random", "mc-util-grpc", "mc-util-lmdb", @@ -3278,12 +3505,12 @@ dependencies = [ "rayon", "serde", "toml 0.5.8", - "url 2.2.2", + "url 2.3.1", ] [[package]] name = "mc-watcher-api" -version = "1.2.2" +version = "2.0.0" dependencies = [ "displaydoc", "serde", @@ -3332,8 +3559,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9753f12909fd8d923f75ae5c3258cae1ed3c8ec052e1b38c93c21a6d157f789c" dependencies = [ "migrations_internals", - "proc-macro2 1.0.39", - "quote 1.0.10", + "proc-macro2 1.0.46", + "quote 1.0.21", "syn 1.0.96", ] @@ -3425,9 +3652,9 @@ dependencies = [ [[package]] name = "mockall" -version = "0.11.1" +version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5641e476bbaf592a3939a7485fa079f427b4db21407d5ebfd5bba4e07a1f6f4c" +checksum = "e2be9a9090bc1cac2930688fa9478092a64c6a92ddc6ae0692d46b37d9cab709" dependencies = [ "cfg-if 1.0.0", "downcast", @@ -3440,13 +3667,13 @@ dependencies = [ [[package]] name = "mockall_derive" -version = "0.11.1" +version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "262d56735932ee0240d515656e5a7667af3af2a5b0af4da558c4cff2b2aeb0c7" +checksum = "86d702a0530a0141cf4ed147cf5ec7be6f2c187d4e37fcbefc39cf34116bfe8f" dependencies = [ "cfg-if 1.0.0", - "proc-macro2 1.0.39", - "quote 1.0.10", + "proc-macro2 1.0.46", + "quote 1.0.21", "syn 1.0.96", ] @@ -3527,9 +3754,9 @@ dependencies = [ [[package]] name = "ntapi" -version = "0.3.7" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c28774a7fd2fbb4f0babd8237ce554b73af68021b5f695a3cebd6c59bac0980f" +checksum = "bc51db7b362b205941f71232e56c625156eb9a929f8cf74a428fd5bc094a4afc" dependencies = [ "winapi 0.3.9", ] @@ -3594,9 +3821,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.8.0" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "692fcb63b64b1758029e0a96ee63e049ce8c5948587f2f7208df04625e5f6b56" +checksum = "7709cef83f0c1f58f666e746a08b21e0085f7440fa6a29cc194d68aac97a4225" [[package]] name = "opaque-debug" @@ -3635,7 +3862,7 @@ dependencies = [ "futures-util", "js-sys", "lazy_static", - "percent-encoding 2.1.0", + "percent-encoding 2.2.0", "pin-project", "rand 0.8.5", "thiserror", @@ -3693,10 +3920,11 @@ checksum = "21326818e99cfe6ce1e524c2a805c189a99b5ae555a35d19f9a284b427d86afa" [[package]] name = "packed_simd_2" -version = "0.3.5" -source = "git+https://github.com/rust-lang/packed_simd.git?rev=f60e900f4ceb71303baa37ff8b41ee7d490c01bf#f60e900f4ceb71303baa37ff8b41ee7d490c01bf" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1914cd452d8fccd6f9db48147b29fd4ae05bea9dc5d9ad578509f72415de282" dependencies = [ - "cfg-if 0.1.10", + "cfg-if 1.0.0", "libm", ] @@ -3721,8 +3949,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c45ed1f39709f5a89338fab50e59816b2e8815f5bb58276e7ddf9afd495f73f8" dependencies = [ "proc-macro-crate", - "proc-macro2 1.0.39", - "quote 1.0.10", + "proc-macro2 1.0.46", + "quote 1.0.21", "syn 1.0.96", ] @@ -3782,18 +4010,18 @@ dependencies = [ [[package]] name = "pbkdf2" -version = "0.4.0" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "216eaa586a190f0a738f2f918511eecfa90f13295abec0e457cdebcceda80cbd" +checksum = "83a0692ec44e4cf1ef28ca317f14f8f07da2d95ec3fa01f86e4467b725e60917" dependencies = [ - "crypto-mac", + "digest", ] [[package]] name = "pear" -version = "0.1.4" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5320f212db967792b67cfe12bd469d08afd6318a249bd917d5c19bc92200ab8a" +checksum = "32dfa7458144c6af7f9ce6a137ef975466aa68ffa44d4d816ee5934018ba960a" dependencies = [ "pear_codegen 0.1.4", ] @@ -3828,9 +4056,9 @@ version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "82a5ca643c2303ecb740d506539deba189e16f2754040a42901cd8105d0282d0" dependencies = [ - "proc-macro2 1.0.39", + "proc-macro2 1.0.46", "proc-macro2-diagnostics", - "quote 1.0.10", + "quote 1.0.21", "syn 1.0.96", ] @@ -3842,13 +4070,11 @@ checksum = "19b17cddbe7ec3f8bc800887bab5e717348c95ea2ca0b1bf0837fb964dc67099" [[package]] name = "pem" -version = "1.0.1" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06673860db84d02a63942fa69cd9543f2624a5df3aea7f33173048fa7ad5cf1a" +checksum = "03c64931a1a212348ec4f3b4362585eca7159d0d09cbdf4a7f74f02173596fd4" dependencies = [ "base64 0.13.0", - "once_cell", - "regex", ] [[package]] @@ -3859,9 +4085,9 @@ checksum = "31010dd2e1ac33d5b46a5b413495239882813e0369f8ed8a5e266f173602f831" [[package]] name = "percent-encoding" -version = "2.1.0" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e" +checksum = "478c572c3d73181ff3c2539045f6eb99e5491218eae919370993b890cdbdd98e" [[package]] name = "pin-project" @@ -3878,8 +4104,8 @@ version = "1.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6e8fe8163d14ce7f0cdac2e040116f22eac817edabff0be91e8aff7e9accf389" dependencies = [ - "proc-macro2 1.0.39", - "quote 1.0.10", + "proc-macro2 1.0.46", + "quote 1.0.21", "syn 1.0.96", ] @@ -3990,8 +4216,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "98e9e4b82e0ef281812565ea4751049f1bdcdfccda7d3f459f2e138a40c08678" dependencies = [ "proc-macro-error-attr", - "proc-macro2 1.0.39", - "quote 1.0.10", + "proc-macro2 1.0.46", + "quote 1.0.21", "syn 1.0.96", "version_check 0.9.3", ] @@ -4002,8 +4228,8 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4f5444ead4e9935abd7f27dc51f7e852a0569ac888096d5ec2499470794e2e53" dependencies = [ - "proc-macro2 1.0.39", - "quote 1.0.10", + "proc-macro2 1.0.46", + "quote 1.0.21", "syn 1.0.96", "syn-mid", "version_check 0.9.3", @@ -4032,9 +4258,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.39" +version = "1.0.46" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c54b25569025b7fc9651de43004ae593a75ad88543b17178aa5e1b9c4f15f56f" +checksum = "94e2ef8dbfc347b10c094890f778ee2e36ca9bb4262e86dc99cd217e35f3470b" dependencies = [ "unicode-ident", ] @@ -4045,8 +4271,8 @@ version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4bf29726d67464d49fa6224a1d07936a8c08bb3fba727c7493f6cf1616fdaada" dependencies = [ - "proc-macro2 1.0.39", - "quote 1.0.10", + "proc-macro2 1.0.46", + "quote 1.0.21", "syn 1.0.96", "version_check 0.9.3", "yansi", @@ -4069,9 +4295,9 @@ dependencies = [ [[package]] name = "prost" -version = "0.10.4" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "71adf41db68aa0daaefc69bb30bcd68ded9b9abaad5d1fbb6304c4fb390e083e" +checksum = "399c3c31cdec40583bb68f0b18403400d01ec4289c383aa047560439952c4dd7" dependencies = [ "bytes 1.1.0", "prost-derive", @@ -4079,28 +4305,28 @@ dependencies = [ [[package]] name = "prost-derive" -version = "0.10.1" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b670f45da57fb8542ebdbb6105a925fe571b67f9e7ed9f47a06a84e72b4e7cc" +checksum = "7345d5f0e08c0536d7ac7229952590239e77abf0a0100a1b1d890add6ea96364" dependencies = [ "anyhow", "itertools", - "proc-macro2 1.0.39", - "quote 1.0.10", + "proc-macro2 1.0.46", + "quote 1.0.21", "syn 1.0.96", ] [[package]] name = "protobuf" -version = "2.27.1" +version = "2.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf7e6d18738ecd0902d30d1ad232c9125985a3422929b16c65517b38adc14f96" +checksum = "106dd99e98437432fed6519dedecfade6a06a73bb7b2a1e019fdd2bee5778d94" [[package]] name = "protobuf-codegen" -version = "2.27.1" +version = "2.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aec1632b7c8f2e620343439a7dfd1f3c47b18906c4be58982079911482b5d707" +checksum = "033460afb75cf755fcfc16dfaed20b86468082a2ea24e05ac35ab4a099a017d6" dependencies = [ "protobuf", ] @@ -4149,11 +4375,11 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.10" +version = "1.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38bc8cc6a5f2e3655e0899c1b848643b2562f853f114bfec7be120678e3ace05" +checksum = "bbe448f377a7d6961e30f5955f9b8d106c3f5e449d493ee1b125c1d43c2b5179" dependencies = [ - "proc-macro2 1.0.39", + "proc-macro2 1.0.46", ] [[package]] @@ -4355,21 +4581,20 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a043824e29c94169374ac5183ac0ed43f5724dc4556b19568007486bd840fa1f" dependencies = [ - "proc-macro2 1.0.39", - "quote 1.0.10", + "proc-macro2 1.0.46", + "quote 1.0.21", "syn 1.0.96", ] [[package]] name = "regex" -version = "1.3.7" +version = "1.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6020f034922e3194c711b82a627453881bc4682166cabb07134a10c26ba7692" +checksum = "d83f127d94bdbcda4c8cc2e50f6f84f4b611f69c902699ca385a39c3a75f9ff1" dependencies = [ "aho-corasick", "memchr", "regex-syntax", - "thread_local", ] [[package]] @@ -4383,9 +4608,9 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.6.17" +version = "0.6.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fe5bd57d1d7414c6b5ed48563a2c855d995ff777729dcd91c369ec7fea395ae" +checksum = "49b3de9ec5dc0a3417da371aab17d729997c15010e7fd24ff707773a33bddb64" [[package]] name = "remove_dir_all" @@ -4398,9 +4623,9 @@ dependencies = [ [[package]] name = "reqwest" -version = "0.11.10" +version = "0.11.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46a1f7aa4f35e5e8b4160449f51afc758f0ce6454315a9fa7d0d113e958c41eb" +checksum = "431949c384f4e2ae07605ccaa56d1d9d2ecdb5cadd4f9577ccfab29f2e5149fc" dependencies = [ "async-compression", "base64 0.13.0", @@ -4415,10 +4640,10 @@ dependencies = [ "hyper-rustls", "ipnet", "js-sys", - "lazy_static", "log 0.4.11", "mime 0.3.16", - "percent-encoding 2.1.0", + "once_cell", + "percent-encoding 2.2.0", "pin-project-lite", "rustls", "rustls-pemfile", @@ -4427,8 +4652,9 @@ dependencies = [ "serde_urlencoded", "tokio", "tokio-rustls", - "tokio-util 0.6.10", - "url 2.2.2", + "tokio-util 0.7.3", + "tower-service", + "url 2.3.1", "wasm-bindgen", "wasm-bindgen-futures", "web-sys", @@ -4468,18 +4694,18 @@ checksum = "5510dbde48c4c37bf69123b1f636b6dd5f8dffe1f4e358af03c46a4947dca219" [[package]] name = "rocket" -version = "0.4.10" +version = "0.4.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a7ab1dfdc75bb8bd2be381f37796b1b300c45a3c9145b34d86715e8dd90bf28" +checksum = "83b9d9dc08c5dcc1d8126a9dd615545e6a358f8c13c883c8dfed8c0376fa355e" dependencies = [ "atty", "base64 0.13.0", "log 0.4.11", "memchr", "num_cpus", - "pear 0.1.4", - "rocket_codegen 0.4.10", - "rocket_http 0.4.10", + "pear 0.1.5", + "rocket_codegen 0.4.11", + "rocket_http 0.4.11", "state 0.4.1", "time 0.1.43", "toml 0.4.10", @@ -4517,7 +4743,7 @@ dependencies = [ "serde_json", "state 0.5.3", "tempfile", - "time 0.3.7", + "time 0.3.14", "tokio", "tokio-stream", "tokio-util 0.7.3", @@ -4528,15 +4754,15 @@ dependencies = [ [[package]] name = "rocket_codegen" -version = "0.4.10" +version = "0.4.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1729e687d6d2cf434d174da84fb948f7fef4fac22d20ce94ca61c28b72dbcf9f" +checksum = "2810037b5820098af97bd4fdd309e76a8101ceb178147de775c835a2537284fe" dependencies = [ - "devise 0.2.0", + "devise 0.2.1", "glob 0.3.0", "indexmap", "quote 0.6.13", - "rocket_http 0.4.10", + "rocket_http 0.4.11", "version_check 0.9.3", "yansi", ] @@ -4550,8 +4776,8 @@ dependencies = [ "devise 0.3.1", "glob 0.3.0", "indexmap", - "proc-macro2 1.0.39", - "quote 1.0.10", + "proc-macro2 1.0.46", + "quote 1.0.21", "rocket_http 0.5.0-rc.2", "syn 1.0.96", "unicode-xid 0.2.0", @@ -4559,15 +4785,15 @@ dependencies = [ [[package]] name = "rocket_contrib" -version = "0.4.10" +version = "0.4.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b6303dccab46dce6c7ac26c9b9d8d8cde1b19614b027c3f913be6611bff6d9b" +checksum = "e20efbc6a211cb3df5375accf532d4186f224b623f39eca650b19b96240c596b" dependencies = [ "diesel", "log 0.4.11", "notify", "r2d2", - "rocket 0.4.10", + "rocket 0.4.11", "rocket_contrib_codegen", "serde", "serde_json", @@ -4575,11 +4801,11 @@ dependencies = [ [[package]] name = "rocket_contrib_codegen" -version = "0.4.10" +version = "0.4.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0f2cbcb6c09b3ac0acdf77682ff8c9d1f317361498a773ee50b32be7fddfe2b" +checksum = "a7b7eaf469d5d7b86e05b0f4ab71f98f094ef1a5a188cbe716ce31eabc2816ae" dependencies = [ - "devise 0.2.0", + "devise 0.2.1", "quote 0.6.13", "version_check 0.9.3", "yansi", @@ -4587,14 +4813,14 @@ dependencies = [ [[package]] name = "rocket_http" -version = "0.4.10" +version = "0.4.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6131e6e6d38a9817f4a494ff5da95971451c2eb56a53915579fc9c80f6ef0117" +checksum = "2bf9cbd128e1f321a2d0bebd2b7cf0aafd89ca43edf69e49b56a5c46e48eb19f" dependencies = [ "cookie 0.11.3", "hyper 0.10.16", "indexmap", - "pear 0.1.4", + "pear 0.1.5", "percent-encoding 1.0.1", "smallvec", "state 0.4.1", @@ -4617,14 +4843,14 @@ dependencies = [ "log 0.4.11", "memchr", "pear 0.2.3", - "percent-encoding 2.1.0", + "percent-encoding 2.2.0", "pin-project-lite", "ref-cast", "serde", "smallvec", "stable-pattern", "state 0.5.3", - "time 0.3.7", + "time 0.3.14", "tokio", "uncased", ] @@ -4662,7 +4888,7 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" dependencies = [ - "semver 1.0.4", + "semver 1.0.10", ] [[package]] @@ -4679,18 +4905,18 @@ dependencies = [ [[package]] name = "rustls-pemfile" -version = "0.3.0" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ee86d63972a7c661d1536fefe8c3c8407321c3df668891286de28abcd087360" +checksum = "0864aeff53f8c05aa08d86e5ef839d3dfcf07aeba2db32f12db0ef716e87bd55" dependencies = [ "base64 0.13.0", ] [[package]] name = "rustversion" -version = "1.0.5" +version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61b3909d758bb75c79f23d4736fac9433868679d3ad2ea7a61e3c25cfda9a088" +checksum = "97477e48b4cf8603ad5f7aaf897467cf42ab4218a38ef76fb14c2d6773a6d6a8" [[package]] name = "ryu" @@ -4742,7 +4968,7 @@ dependencies = [ "curve25519-dalek", "merlin", "rand_core 0.6.3", - "sha2 0.10.2", + "sha2", "subtle", "zeroize", ] @@ -4790,9 +5016,9 @@ dependencies = [ [[package]] name = "semver" -version = "1.0.4" +version = "1.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "568a8e6258aa33c13358f81fd834adb854c6f7c9468520910a9b1e8fac068012" +checksum = "a41d061efea015927ac527063765e73601444cdc344ba855bc7bd44578b25e1c" dependencies = [ "serde", ] @@ -4805,9 +5031,9 @@ checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3" [[package]] name = "sentry" -version = "0.25.0" +version = "0.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2d23af89cf3e40dffb53f974e9a21653353b3e21cf51633aa58006f2a0caf8a" +checksum = "73642819e7fa63eb264abc818a2f65ac8764afbe4870b5ee25bcecc491be0d4c" dependencies = [ "httpdate", "reqwest", @@ -4823,21 +5049,21 @@ dependencies = [ [[package]] name = "sentry-backtrace" -version = "0.25.0" +version = "0.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8158a446429420acdf6a4f75192ee8929da16a0c41c89a1c34b2e0f1eaebcc02" +checksum = "49bafa55eefc6dbc04c7dac91e8c8ab9e89e9414f3193c105cabd991bbc75134" dependencies = [ "backtrace", - "lazy_static", + "once_cell", "regex", "sentry-core", ] [[package]] name = "sentry-contexts" -version = "0.25.0" +version = "0.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a3bda8a1e3213f1944da2d42f3081ea9f3717105bb2a6b0a8fe4f5e603010a3" +checksum = "c63317c4051889e73f0b00ce4024cae3e6a225f2e18a27d2c1522eb9ce2743da" dependencies = [ "hostname", "libc", @@ -4848,11 +5074,11 @@ dependencies = [ [[package]] name = "sentry-core" -version = "0.25.0" +version = "0.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56333f11be3a78131c67637f7611339df8af7ad9af831226585a457df75f9e3b" +checksum = "5a4591a2d128af73b1b819ab95f143bc6a2fbe48cd23a4c45e1ee32177e66ae6" dependencies = [ - "lazy_static", + "once_cell", "rand 0.8.5", "sentry-types", "serde", @@ -4861,9 +5087,9 @@ dependencies = [ [[package]] name = "sentry-log" -version = "0.25.0" +version = "0.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91b56c287a5295358bd4a3481a32add1f3fb7131102e300f561f788e33b79efe" +checksum = "58a76b41861ebde9b0a689fa13080ad5508583e094c48acad461eec5acd7fc5f" dependencies = [ "log 0.4.11", "sentry-core", @@ -4871,9 +5097,9 @@ dependencies = [ [[package]] name = "sentry-panic" -version = "0.25.0" +version = "0.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b957b1965c450acd220a27806fe1f2dec998d393973ebae797936b12df1c7416" +checksum = "696c74c5882d5a0d5b4a31d0ff3989b04da49be7983b7f52a52c667da5b480bf" dependencies = [ "sentry-backtrace", "sentry-core", @@ -4881,9 +5107,9 @@ dependencies = [ [[package]] name = "sentry-slog" -version = "0.25.0" +version = "0.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11555d3582f504df2047d77460daa942a0a27228bf0803fb2aa040adfe17abb0" +checksum = "f855446c5f08db26a73b0c532b4354d33143982eadf84071d2a0102f9885a31e" dependencies = [ "sentry-core", "serde_json", @@ -4892,9 +5118,9 @@ dependencies = [ [[package]] name = "sentry-types" -version = "0.25.0" +version = "0.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "825fd3382e2397007499a910e0184e55f7837cb0df4af30ae62bd2123e2ebcd6" +checksum = "823923ae5f54a729159d720aa12181673044ee5c79cbda3be09e56f885e5468f" dependencies = [ "debugid", "getrandom 0.2.3", @@ -4902,20 +5128,29 @@ dependencies = [ "serde", "serde_json", "thiserror", - "time 0.3.7", - "url 2.2.2", - "uuid 0.8.1", + "time 0.3.14", + "url 2.3.1", + "uuid", ] [[package]] name = "serde" -version = "1.0.130" +version = "1.0.137" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f12d06de37cf59146fbdecab66aa99f9fe4f78722e3607577a5375d66bd0c913" +checksum = "61ea8d54c77f8315140a05f4c7237403bf38b72704d031543aa1d16abbf517d1" dependencies = [ "serde_derive", ] +[[package]] +name = "serde-big-array" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3323f09a748af288c3dc2474ea6803ee81f118321775bffa3ac8f7e65c5e90e7" +dependencies = [ + "serde", +] + [[package]] name = "serde_bytes" version = "0.11.5" @@ -4936,23 +5171,23 @@ dependencies = [ [[package]] name = "serde_derive" -version = "1.0.130" +version = "1.0.137" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7bc1a1ab1961464eae040d96713baa5a724a8152c1222492465b54322ec508b" +checksum = "1f26faba0c3959972377d3b2d306ee9f71faee9714294e41bb777f83f88578be" dependencies = [ - "proc-macro2 1.0.39", - "quote 1.0.10", + "proc-macro2 1.0.46", + "quote 1.0.21", "syn 1.0.96", ] [[package]] name = "serde_json" -version = "1.0.70" +version = "1.0.81" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e277c495ac6cd1a01a58d0a0c574568b4d1ddf14f59965c6a58b8d96400b54f3" +checksum = "9b7ce2b32a1aed03c558dc61a5cd328f15aff2dbc17daad8fb8af04d2100e15c" dependencies = [ "indexmap", - "itoa 0.4.5", + "itoa 1.0.1", "ryu", "serde", ] @@ -4970,16 +5205,12 @@ dependencies = [ ] [[package]] -name = "sha2" -version = "0.9.8" +name = "serde_with" +version = "1.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b69f9a4c9740d74c5baa3fd2e547f9525fa8088a8a958e0ca2409a514e33f5fa" +checksum = "678b5a069e50bf00ecd22d0cd8ddf7c236f68581b03db652061ed5eb13a312ff" dependencies = [ - "block-buffer 0.9.0", - "cfg-if 1.0.0", - "cpufeatures", - "digest 0.9.0", - "opaque-debug", + "serde", ] [[package]] @@ -4990,7 +5221,7 @@ checksum = "55deaec60f81eefe3cce0dc50bda92d6d8e88f2a27df7c5033b42afeb1ed2676" dependencies = [ "cfg-if 1.0.0", "cpufeatures", - "digest 0.10.3", + "digest", ] [[package]] @@ -4999,7 +5230,7 @@ version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "881bf8156c87b6301fc5ca6b27f11eeb2761224c7081e69b409d5a1951a70c86" dependencies = [ - "digest 0.10.3", + "digest", "keccak", ] @@ -5039,11 +5270,11 @@ dependencies = [ [[package]] name = "signature" -version = "1.5.0" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f054c6c1a6e95179d6f23ed974060dcefb2d9388bb7256900badad682c499de4" +checksum = "f0ea32af43239f0d353a7dd75a22d94c329c8cdaafdcb4c1c1335aa10c298a4a" dependencies = [ - "digest 0.10.3", + "digest", ] [[package]] @@ -5153,7 +5384,7 @@ dependencies = [ "serde", "serde_json", "slog", - "time 0.3.7", + "time 0.3.14", ] [[package]] @@ -5188,7 +5419,7 @@ dependencies = [ "slog", "term", "thread_local", - "time 0.3.7", + "time 0.3.14", ] [[package]] @@ -5297,29 +5528,29 @@ checksum = "dcb5ae327f9cc13b68763b5749770cb9e048a99bd9dfdfa58d0cf05d5f64afe0" dependencies = [ "heck 0.3.1", "proc-macro-error", - "proc-macro2 1.0.39", - "quote 1.0.10", + "proc-macro2 1.0.46", + "quote 1.0.21", "syn 1.0.96", ] [[package]] name = "strum" -version = "0.24.0" +version = "0.24.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e96acfc1b70604b8b2f1ffa4c57e59176c7dbb05d556c71ecd2f5498a1dee7f8" +checksum = "063e6045c0e62079840579a7e47a355ae92f60eb74daaf156fb1e84ba164e63f" dependencies = [ "strum_macros", ] [[package]] name = "strum_macros" -version = "0.24.0" +version = "0.24.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6878079b17446e4d3eba6192bb0a2950d5b14f0ed8424b852310e5a94345d0ef" +checksum = "1e385be0d24f186b4ce2f9982191e7101bb737312ad61c1f2f984f34bcf85d59" dependencies = [ "heck 0.4.0", - "proc-macro2 1.0.39", - "quote 1.0.10", + "proc-macro2 1.0.46", + "quote 1.0.21", "rustversion", "syn 1.0.96", ] @@ -5347,8 +5578,8 @@ version = "1.0.96" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0748dd251e24453cb8717f0354206b91557e4ec8703673a4b30208f2abaf1ebf" dependencies = [ - "proc-macro2 1.0.39", - "quote 1.0.10", + "proc-macro2 1.0.46", + "quote 1.0.21", "unicode-ident", ] @@ -5358,8 +5589,8 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7be3539f6c128a931cf19dcee741c1af532c7fd387baa739c03dd2e96479338a" dependencies = [ - "proc-macro2 1.0.39", - "quote 1.0.10", + "proc-macro2 1.0.46", + "quote 1.0.21", "syn 1.0.96", ] @@ -5369,17 +5600,17 @@ version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67656ea1dc1b41b1451851562ea232ec2e5a80242139f7e679ceccfb5d61f545" dependencies = [ - "proc-macro2 1.0.39", - "quote 1.0.10", + "proc-macro2 1.0.46", + "quote 1.0.21", "syn 1.0.96", "unicode-xid 0.2.0", ] [[package]] name = "sysinfo" -version = "0.23.5" +version = "0.26.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07fa4c84a5305909b0eedfcc8d1f2fafdbede645bb700a45ecaafe681a0ac5d6" +checksum = "7890fff842b8db56f2033ebee8f6efe1921475c3830c115995552914fb967580" dependencies = [ "cfg-if 1.0.0", "core-foundation-sys", @@ -5462,21 +5693,21 @@ checksum = "b1141d4d61095b28419e22cb0bbf02755f5e54e0526f97f1e3d1d160e60885fb" [[package]] name = "thiserror" -version = "1.0.24" +version = "1.0.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0f4a65597094d4483ddaed134f409b2cb7c1beccf25201a9f73c719254fa98e" +checksum = "10deb33631e3c9018b9baf9dcbbc4f737320d2b576bac10f6aefa048fa407e3e" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.24" +version = "1.0.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7765189610d8241a44529806d6fd1f2e0a08734313a35d5b3a556f92b381f3c0" +checksum = "982d17546b47146b28f7c22e3d08465f6b8903d0ea13c1660d9d84a6e7adcdbb" dependencies = [ - "proc-macro2 1.0.39", - "quote 1.0.10", + "proc-macro2 1.0.46", + "quote 1.0.21", "syn 1.0.96", ] @@ -5522,9 +5753,9 @@ dependencies = [ [[package]] name = "time" -version = "0.3.7" +version = "0.3.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "004cbc98f30fa233c61a38bc77e96a9106e65c88f2d3bef182ae952027e5753d" +checksum = "3c3f9a28b618c3a6b9251b6908e9c99e04b9e5c02e6581ccbb67d59c34ef7f9b" dependencies = [ "itoa 1.0.1", "libc", @@ -5534,23 +5765,23 @@ dependencies = [ [[package]] name = "time-macros" -version = "0.2.3" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25eb0ca3468fc0acc11828786797f6ef9aa1555e4a211a60d64cc8e4d1be47d6" +checksum = "42657b1a6f4d817cda8e7a0ace261fe0cc946cf3a80314390b22cc61ae080792" [[package]] name = "tiny-bip39" -version = "0.8.2" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ffc59cb9dfc85bb312c3a78fd6aa8a8582e310b0fa885d5bb877f6dcc601839d" +checksum = "62cc94d358b5a1e84a5cb9109f559aa3c4d634d2b1b4de3d0fa4adc7c78e2861" dependencies = [ "anyhow", - "hmac 0.8.1", + "hmac", "once_cell", "pbkdf2", - "rand 0.7.3", + "rand 0.8.5", "rustc-hash", - "sha2 0.9.8", + "sha2", "thiserror", "unicode-normalization", "wasm-bindgen", @@ -5597,8 +5828,8 @@ version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9724f9a975fb987ef7a3cd9be0350edcbe130698af5b8f7a631e23d42d052484" dependencies = [ - "proc-macro2 1.0.39", - "quote 1.0.10", + "proc-macro2 1.0.46", + "quote 1.0.21", "syn 1.0.96", ] @@ -5695,8 +5926,8 @@ version = "0.1.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f4f480b8f81512e825f337ad51e94c1eb5d3bbdf2b363dcd01e2b19a9ffe3f8e" dependencies = [ - "proc-macro2 1.0.39", - "quote 1.0.10", + "proc-macro2 1.0.46", + "quote 1.0.21", "syn 1.0.96", ] @@ -5844,9 +6075,9 @@ checksum = "d22af068fba1eb5edcb4aea19d382b2a3deb4c8f9d475c589b6ada9e0fd493ee" [[package]] name = "unicode-normalization" -version = "0.1.17" +version = "0.1.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07fbfce1c8a97d547e8b5334978438d9d6ec8c20e38f56d4a4374d181493eaef" +checksum = "81dee68f85cab8cf68dec42158baf3a79a1cdc065a8b103025965d6ccb7f6cbd" dependencies = [ "tinyvec", ] @@ -5904,32 +6135,21 @@ dependencies = [ [[package]] name = "url" -version = "2.2.2" +version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a507c383b2d33b5fc35d1861e77e6b383d158b2da5e14fe51b83dfedf6fd578c" +checksum = "0d68c799ae75762b8c3fe375feb6600ef5602c883c5d21eb51c09f22b83c4643" dependencies = [ "form_urlencoded", - "idna 0.2.0", - "matches", - "percent-encoding 2.1.0", - "serde", -] - -[[package]] -name = "uuid" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fde2f6a4bea1d6e007c4ad38c6839fa71cbb63b6dbf5b595aa38dc9b1093c11" -dependencies = [ - "rand 0.7.3", + "idna 0.3.0", + "percent-encoding 2.2.0", "serde", ] [[package]] name = "uuid" -version = "1.1.1" +version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6d5d669b51467dcf7b2f1a796ce0f955f05f01cafda6c19d6e95f730df29238" +checksum = "dd6469f4314d5f1ffec476e05f17cc9a78bc7a27a6a857842170bdf8d6f98d2f" dependencies = [ "getrandom 0.2.3", "serde", @@ -5949,9 +6169,9 @@ checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191" [[package]] name = "vergen" -version = "7.0.0" +version = "7.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4db743914c971db162f35bf46601c5a63ec4452e61461937b4c1ab817a60c12e" +checksum = "73ba753d713ec3844652ad2cb7eb56bc71e34213a14faddac7852a10ba88f61e" dependencies = [ "anyhow", "cfg-if 1.0.0", @@ -5962,7 +6182,7 @@ dependencies = [ "rustversion", "sysinfo", "thiserror", - "time 0.3.7", + "time 0.3.14", ] [[package]] @@ -6030,9 +6250,9 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasm-bindgen" -version = "0.2.78" +version = "0.2.82" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "632f73e236b219150ea279196e54e610f5dbafa5d61786303d4da54f84e47fce" +checksum = "fc7652e3f6c4706c8d9cd54832c4a4ccb9b5336e2c3bd154d5cccfbf1c1f5f7d" dependencies = [ "cfg-if 1.0.0", "wasm-bindgen-macro", @@ -6040,15 +6260,15 @@ dependencies = [ [[package]] name = "wasm-bindgen-backend" -version = "0.2.78" +version = "0.2.82" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a317bf8f9fba2476b4b2c85ef4c4af8ff39c3c7f0cdfeed4f82c34a880aa837b" +checksum = "662cd44805586bd52971b9586b1df85cdbbd9112e4ef4d8f41559c334dc6ac3f" dependencies = [ "bumpalo", - "lazy_static", "log 0.4.11", - "proc-macro2 1.0.39", - "quote 1.0.10", + "once_cell", + "proc-macro2 1.0.46", + "quote 1.0.21", "syn 1.0.96", "wasm-bindgen-shared", ] @@ -6067,22 +6287,22 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.78" +version = "0.2.82" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d56146e7c495528bf6587663bea13a8eb588d39b36b679d83972e1a2dbbdacf9" +checksum = "b260f13d3012071dfb1512849c033b1925038373aea48ced3012c09df952c602" dependencies = [ - "quote 1.0.10", + "quote 1.0.21", "wasm-bindgen-macro-support", ] [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.78" +version = "0.2.82" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7803e0eea25835f8abdc585cd3021b3deb11543c6fe226dcd30b228857c5c5ab" +checksum = "5be8e654bdd9b79216c2929ab90721aa82faf65c48cdf08bdc4e7f51357b80da" dependencies = [ - "proc-macro2 1.0.39", - "quote 1.0.10", + "proc-macro2 1.0.46", + "quote 1.0.21", "syn 1.0.96", "wasm-bindgen-backend", "wasm-bindgen-shared", @@ -6090,9 +6310,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.78" +version = "0.2.82" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0237232789cf037d5480773fe568aac745bfe2afbc11a863e97901780a6b47cc" +checksum = "6598dd0bd3c7d51095ff6531a5b23e02acdc81804e30d8f07afb77b7215a140a" [[package]] name = "web-sys" @@ -6304,9 +6524,9 @@ dependencies = [ [[package]] name = "zeroize" -version = "1.5.4" +version = "1.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7eb5728b8afd3f280a869ce1d4c554ffaed35f45c231fc41bfbd0381bef50317" +checksum = "94693807d016b2f2d2e14420eb3bfcca689311ff775dcf113d74ea624b7cdf07" dependencies = [ "zeroize_derive", ] @@ -6317,8 +6537,8 @@ version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f8f187641dad4f680d25c4bfc4225b418165984179f26ca76ec4fb6441d3a17" dependencies = [ - "proc-macro2 1.0.39", - "quote 1.0.10", + "proc-macro2 1.0.46", + "quote 1.0.21", "syn 1.0.96", "synstructure", ] diff --git a/Cargo.toml b/Cargo.toml index 93201abb5..09b155cb8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,6 +4,7 @@ cargo-features = ["resolver"] resolver = "2" members = [ "full-service", + "transaction-signer", "validator/api", "validator/connection", "validator/service", @@ -39,9 +40,6 @@ ed25519-dalek = { git = "https://github.com/mobilecoinfoundation/ed25519-dalek.g mbedtls = { git = "https://github.com/mobilecoinofficial/rust-mbedtls.git", rev = "49a293a5f4b1ef571c71174e3fa1f301925f3915" } mbedtls-sys-auto = { git = "https://github.com/mobilecoinofficial/rust-mbedtls.git", rev = "49a293a5f4b1ef571c71174e3fa1f301925f3915" } -# packed_simd_2 has unreleased fixes for build issues we're experiencing -packed_simd_2 = { git = "https://github.com/rust-lang/packed_simd.git", rev = "f60e900f4ceb71303baa37ff8b41ee7d490c01bf" } - # Override lmdb-rkv for a necessary bugfix (see https://github.com/mozilla/lmdb-rs/pull/80) lmdb-rkv = { git = "https://github.com/mozilla/lmdb-rs", rev = "df1c2f5" } diff --git a/Dockerfile b/Dockerfile index c59a040e5..04f06794a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -56,7 +56,7 @@ RUN --mount=type=cache,target=/root/.cargo/git \ --mount=type=cache,target=/root/.cargo/registry \ --mount=type=cache,target=/app/target \ cargo build --release -p mc-full-service ${BUILD_OPTS} \ - && cp /app/target/release/full-service /usr/local/bin/full-service + && cp /app/target/release/mc-full-service /usr/local/bin/mc-full-service # This is the runtime container. @@ -76,7 +76,8 @@ RUN apt-get update \ && mkdir -p /usr/share/grpc \ && ln -s /etc/ssl/certs/ca-certificates.crt /usr/share/grpc/roots.pem -COPY --from=builder /usr/local/bin/full-service /usr/local/bin/full-service +COPY --from=builder /usr/local/bin/mc-full-service /usr/local/bin/mc-full-service +RUN ln -s /usr/local/bin/mc-full-service /usr/local/bin/full-service COPY --from=builder /app/*.css /usr/local/bin/ USER app diff --git a/README.md b/README.md index 9aeb7a32a..76519e7a5 100644 --- a/README.md +++ b/README.md @@ -82,10 +82,10 @@ sudo xcode-select -s /Applications/.app/Contents/Deve ```sh NAMESPACE=test - + CONSENSUS_SIGSTRUCT_URI=$(curl -s https://enclave-distribution.${NAMESPACE}.mobilecoin.com/production.json | grep consensus-enclave.css | awk '{print $2}' | tr -d \" | tr -d ,) curl -O https://enclave-distribution.${NAMESPACE}.mobilecoin.com/${CONSENSUS_SIGSTRUCT_URI} - + INGEST_SIGSTRUCT_URI=$(curl -s https://enclave-distribution.${NAMESPACE}.mobilecoin.com/production.json | grep ingest-enclave.css | awk '{print $2}' | tr -d \" | tr -d ,) curl -O https://enclave-distribution.${NAMESPACE}.mobilecoin.com/${INGEST_SIGSTRUCT_URI} ``` @@ -99,15 +99,19 @@ sudo xcode-select -s /Applications/.app/Contents/Deve sudo ./sgx_linux_x64_sdk_2.9.101.2.bin --prefix=/opt/intel ``` - Put this line in your .bashrc: + Put this line in your .bashrc or .zhrc: ```sh source /opt/intel/sgxsdk/environment ``` This works on more recent Ubuntu distributions, even though it specifies 18.04. -7. Build +7. Put this line in your .bashrc or .zhrc: + ```sh + export OPENSSL_ROOT_DIR="/usr/local/opt/openssl@3" + ``` +8. Build ```sh SGX_MODE=HW \ IAS_MODE=PROD \ @@ -116,26 +120,27 @@ sudo xcode-select -s /Applications/.app/Contents/Deve cargo build --release -p mc-full-service ``` -1. Set database password if using encryption. +9. Set database password if using encryption. ```sh read -rs MC_PASSWORD export MC_PASSWORD=$MC_PASSWORD ``` -8. Run +10. Run TestNet Example ```sh mkdir -p /tmp/wallet-db/ - ./target/release/full-service \ + ./target/release/mc-full-service \ --wallet-db /tmp/wallet-db/wallet.db \ --ledger-db /tmp/ledger-db/ \ --peer mc://node1.test.mobilecoin.com/ \ --peer mc://node2.test.mobilecoin.com/ \ --tx-source-url https://s3-us-west-1.amazonaws.com/mobilecoin.chain/node1.test.mobilecoin.com/ \ --tx-source-url https://s3-us-west-1.amazonaws.com/mobilecoin.chain/node2.test.mobilecoin.com/ \ - --fog-ingest-enclave-css $(pwd)/ingest-enclave.css + --fog-ingest-enclave-css $(pwd)/ingest-enclave.css \ + --chain-id test ``` See [Parameters](#parameters) for full list of available options. @@ -185,9 +190,9 @@ sudo xcode-select -s /Applications/.app/Contents/Deve ```sh mkdir -p /opt/full-service/data - + chown 1000:1000 /opt/full-service/data - + docker run -it -p 127.0.0.1:9090:9090 \ -v /opt/full-service/data:data \ --name full-service \ @@ -195,7 +200,8 @@ sudo xcode-select -s /Applications/.app/Contents/Deve --peer mc://node1.test.mobilecoin.com/ \ --peer mc://node2.test.mobilecoin.com/ \ --tx-source-url https://s3-us-west-1.amazonaws.com/mobilecoin.chain/node1.test.mobilecoin.com/ \ - --tx-source-url https://s3-us-west-1.amazonaws.com/mobilecoin.chain/node2.test.mobilecoin.com/ + --tx-source-url https://s3-us-west-1.amazonaws.com/mobilecoin.chain/node2.test.mobilecoin.com/ \ + --chain-id test ``` **Listen and Port** @@ -215,7 +221,8 @@ sudo xcode-select -s /Applications/.app/Contents/Deve --peer mc://node1.test.mobilecoin.com/ \ --peer mc://node2.test.mobilecoin.com/ \ --tx-source-url https://s3-us-west-1.amazonaws.com/mobilecoin.chain/node1.test.mobilecoin.com/ \ - --tx-source-url https://s3-us-west-1.amazonaws.com/mobilecoin.chain/node2.test.mobilecoin.com/ + --tx-source-url https://s3-us-west-1.amazonaws.com/mobilecoin.chain/node2.test.mobilecoin.com/ \ + --chain-id test ``` See [Parameters](#parameters) for full list of available options. @@ -224,13 +231,14 @@ sudo xcode-select -s /Applications/.app/Contents/Deve | Param | Purpose | Requirements | | :--------------- | :----------------------- | :------------------------ | -| `wallet-db` | Path to wallet file | Created if does not exist | | `ledger-db` | Path to ledger directory | Created if does not exist | | `peer` | URI of consensus node. Used to submit
transactions and to check the network
block height. | MC URI format | | `tx-source-url` | S3 location of archived ledger. Used to
sync transactions to the local ledger. | S3 URI format | +| `chain-id` | The chain id of the network we expect to interact with | String | | Opional Param | Purpose | Requirements | | :------------ | :----------------------- | :------------------------ | +| `wallet-db` | Path to wallet file. If not set, will disable any endpoints that require a wallet_db | Created if does not exist | | `listen-host` | Host to listen on. | Default: 127.0.0.1 | | `listen-port` | Port to start webserver on. | Default: 9090 | | `ledger-db-bootstrap` | Path to existing ledger_db that contains the origin block,
used when initializing new ledger dbs. | | @@ -270,20 +278,21 @@ The recommended flow to get balance and submit transaction is the following: 1. *ONLINE MACHINE*: Sync ledger by running full service. ```sh - ./target/release/full-service \ + ./target/release/mc-full-service \ --wallet-db /tmp/wallet-db/wallet.db \ --ledger-db /tmp/ledger-db/ \ --peer mc://node1.test.mobilecoin.com/ \ --peer mc://node2.test.mobilecoin.com/ \ --tx-source-url https://s3-us-west-1.amazonaws.com/mobilecoin.chain/node1.test.mobilecoin.com/ \ - --tx-source-url https://s3-us-west-1.amazonaws.com/mobilecoin.chain/node2.test.mobilecoin.com/ + --tx-source-url https://s3-us-west-1.amazonaws.com/mobilecoin.chain/node2.test.mobilecoin.com/ \ + --chain-id test ``` 1. *ONLINE MACHINE and USB*: Copy the ledger and the full-service binary to USB. ```sh cp -r /tmp/ledger-db /media/ - cp ./target/release/full-service /media/ + cp ./target/release/mc-full-service /media/ ``` 1. *OFFLINE MACHINE*: Create a ramdisk to store sensitive material. @@ -309,16 +318,17 @@ The recommended flow to get balance and submit transaction is the following: ```sh cp /media/ledger-db /keyfs/ledger-db - cp /media/full-service /keyfs/full-service + cp /media/mc-full-service /keyfs/mc-full-service ``` 1. *OFFLINE MACHINE*: Run full service in offline mode. ```sh - ./target/release/full-service \ + ./target/release/mc-full-service \ --wallet-db /keyfs/wallet.db \ --ledger-db /keyfs/ledger-db/ \ - --offline + --offline \ + --chain-id test ``` 1. *OFFLINE MACHINE*: You can now [create](#create-account) or [import](#import-account) your @@ -342,7 +352,7 @@ The recommended flow to get balance and submit transaction is the following: } }' \ -X POST -H 'Content-type: application/json' | jq '.result' > /keyfs/tx_proposal.json - + cp /keyfs/tx_proposal.json /media/ ``` diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 000000000..cb83bdbec --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,7 @@ +# Security / Disclosure + +If you find any bug with Full Service that may be a security problem, then e-mail us at: [security@mobilecoin.com](mailto:security@mobilecoin.com). +This way we can evaluate the bug and hopefully fix it before it gets abused. +Please give us enough time to investigate the bug before you report it anywhere else. + +Please do not create GitHub issues for security-related doubts or problems. diff --git a/cli/mobilecoin/cli.py b/cli/mobilecoin/cli.py index d1a02f629..b791f672f 100644 --- a/cli/mobilecoin/cli.py +++ b/cli/mobilecoin/cli.py @@ -98,8 +98,6 @@ def _create_parsers(self): self.export_args.add_argument('account_id', help='ID of the account to export.') self.export_args.add_argument('-s', '--show', action='store_true', help='Only show the secret entropy mnemonic, do not write it to file.') - self.export_args.add_argument('-V', '--view', action='store_true', - help='Show the view-private-key only.') # Remove account. self.remove_args = command_sp.add_parser('remove', help='Remove an account from local storage.') @@ -179,8 +177,6 @@ def _create_parsers(self): def _load_account_prefix(self, prefix): accounts = self.client.get_all_accounts() - view_accounts = self.client.get_all_view_only_accounts() - accounts.update(view_accounts) matching_ids = [ a_id for a_id in accounts.keys() @@ -286,9 +282,7 @@ def status(self): def list(self): accounts = self.client.get_all_accounts() - view_accounts = self.client.get_all_view_only_accounts() - - if len(accounts) + len(view_accounts) == 0: + if len(accounts) == 0: print('No accounts.') return @@ -297,12 +291,7 @@ def list(self): balance = self.client.get_balance_for_account(account_id) account_list.append((account_id, account, balance)) - view_account_list = [] - for account_id, view_account in view_accounts.items(): - balance = self.client.get_balance_for_view_only_account(account_id) - view_account_list.append((account_id, view_account, balance)) - - for (account_id, account, balance) in account_list + view_account_list: + for (account_id, account, balance) in account_list: print() _print_account(account, balance) @@ -338,6 +327,7 @@ def import_(self, backup, name=None, block=None, key_derivation_version=2): account = self.client.import_view_only_account(data['params']) else: params = {} + for field in [ 'mnemonic', # Key derivation version 2+. 'entropy', # Key derivation version 1. @@ -348,6 +338,7 @@ def import_(self, backup, name=None, block=None, key_derivation_version=2): value = data.get(field) if value is not None: params[field] = value + if 'account_key' in data: params['fog_keys'] = {} for field in [ @@ -358,6 +349,10 @@ def import_(self, backup, name=None, block=None, key_derivation_version=2): value = data['account_key'].get(field) if value is not None: params['fog_keys'][field] = value + + if name is not None: + params['name'] = name + account = self.client.import_account(**params) else: @@ -384,11 +379,7 @@ def import_(self, backup, name=None, block=None, key_derivation_version=2): _print_account(account) print() - def export(self, account_id, show=False, view=False): - if view: - self._export_view_key(account_id, show) - return - + def export(self, account_id, show=False): account = self._load_account_prefix(account_id) account_id = account['account_id'] balance = self.client.get_balance_for_account(account_id) @@ -422,6 +413,7 @@ def export(self, account_id, show=False, view=False): else: filename = 'mobilecoin_secret_mnemonic_{}.json'.format(account_id[:6]) try: + print(secrets) _save_export(account, secrets, filename) except OSError as e: print('Could not write file: {}'.format(e)) @@ -429,53 +421,12 @@ def export(self, account_id, show=False, view=False): else: print(f'Wrote {filename}.') - def _export_view_key(self, account_id, show): - account = self._load_account_prefix(account_id) - account_id = account['account_id'] - balance = self.client.get_balance_for_account(account_id) - - print('You are about to export the private view key for this account:') - print() - _print_account(account, balance) - - print() - if show: - print('The private view key will display on your screen.') - print('Make sure your screen is not being viewed or recorded.') - else: - print('Keep the view key file safe and private!') - print('Anyone who has access to the view key can see all transactions for the account.') - - if show: - confirm_message = 'Really show account view key? (Y/N) ' - else: - confirm_message = 'Really write private view key to a file? (Y/N) ' - - if not self.confirm(confirm_message): - print('Cancelled.') - return - - secrets = self.client.export_account_secrets(account_id) - if show: - print() - print(secrets['account_key']['view_private_key']) - print() - else: - filename = 'mobilecoin_view_key_{}.json'.format(account_id[:6]) - try: - _save_view_key_export(account, secrets, filename) - except OSError as e: - print('Could not write file: {}'.format(e)) - exit(1) - else: - print(f'Wrote {filename}.') - def remove(self, account_id): account = self._load_account_prefix(account_id) account_id = account['account_id'] + balance = self.client.get_balance_for_account(account_id) - if account['object'] == 'view_only_account': - balance = self.client.get_balance_for_view_only_account(account_id) + if account['view_only']: print('You are about to remove this view key:') print() _print_account(account, balance) @@ -483,7 +434,6 @@ def remove(self, account_id): print('You will lose the ability to see related transactions unless you') print('restore it from backup.') else: - balance = self.client.get_balance_for_account(account_id) print('You are about to remove this account:') print() _print_account(account, balance) @@ -495,10 +445,7 @@ def remove(self, account_id): print('Cancelled.') return - if account['object'] == 'view_only_account': - self.client.remove_view_only_account(account_id) - else: - self.client.remove_account(account_id) + self.client.remove_account(account_id) print('Removed.') def history(self, account_id): @@ -551,11 +498,7 @@ def send(self, account_id, amount, to_address, build_only=False, fee=None): account = self._load_account_prefix(account_id) account_id = account['account_id'] - view_only = (account['object'] == 'view_only_account') - if view_only: - balance = self.client.get_balance_for_view_only_account(account_id) - else: - balance = self.client.get_balance_for_account(account_id) + balance = self.client.get_balance_for_account(account_id) unspent = pmob2mob(balance['unspent_pmob']) network_status = self.client.get_network_status() @@ -565,10 +508,6 @@ def send(self, account_id, amount, to_address, build_only=False, fee=None): else: fee = Decimal(fee) - if unspent <= fee: - print('There is not enough MOB in account {} to pay the transaction fee.'.format(account_id[:6])) - return - if amount == "all": amount = unspent - fee total_amount = unspent @@ -576,7 +515,11 @@ def send(self, account_id, amount, to_address, build_only=False, fee=None): amount = Decimal(amount) total_amount = amount + fee - if view_only: + if unspent < total_amount: + print('There is not enough MOB in account {} to pay for this transaction.'.format(account_id[:6])) + return + + if account['view_only']: verb = 'Building unsigned transaction for' elif build_only: verb = 'Building transaction for' @@ -584,10 +527,12 @@ def send(self, account_id, amount, to_address, build_only=False, fee=None): verb = 'Sending' print('\n'.join([ + '', '{} {}', - 'from account {}', - 'to address {}', + ' from account {}', + ' to address {}', 'Fee is {}, for a total amount of {}.', + '', ]).format( verb, _format_mob(amount), @@ -604,9 +549,9 @@ def send(self, account_id, amount, to_address, build_only=False, fee=None): ]).format(_format_mob(unspent))) return - if view_only: + if account['view_only']: response = self.client.build_unsigned_transaction(account_id, amount, to_address, fee=fee) - path = Path('unsigned_tx_proposal_{}_{}.json'.format( + path = Path('tx_proposal_{}_{}_unsigned.json'.format( account_id[:6], balance['local_block_height'], )) @@ -615,6 +560,9 @@ def send(self, account_id, amount, to_address, build_only=False, fee=None): else: _save_json_file(path, response) print(f'Wrote {path}.') + print() + print('This account is view-only, so its spend key is in an offline signer.') + print('Run `mob-transaction-signer sign`, then submit the result with `mobcli submit`') return if build_only: @@ -654,6 +602,7 @@ def submit(self, proposal, account_id=None, receipt=False): # Check whether this is an already built response from the offline transaction signer. if tx_proposal.get('method') == 'submit_transaction': + account_id = tx_proposal['params']['account_id'] tx_proposal = tx_proposal['params']['tx_proposal'] # Check that the tombstone block is within range. @@ -690,7 +639,7 @@ def submit(self, proposal, account_id=None, receipt=False): print('Cancelled.') return - self.client.submit_transaction(tx_proposal) + self.client.submit_transaction(tx_proposal, account_id) print('Submitted. The file {} is now unusable for sending transactions.'.format(proposal)) def qr(self, account_id): @@ -727,17 +676,8 @@ def address_list(self, account_id): print(_format_account_header(account)) addresses = self.client.get_addresses_for_account(account['account_id'], limit=1000) - address_balances = [] for address in addresses.values(): balance = self.client.get_balance_for_address(address['public_address']) - address_balances.append((address, balance)) - - view_addresses = self.client.get_addresses_for_view_only_account(account['account_id'], limit=1000) - for address in view_addresses.values(): - balance = self.client.get_balance_for_view_only_address(address['public_address']) - address_balances.append((address, balance)) - - for (address, balance) in address_balances: print(indent( '{} {}'.format(address['public_address'], address['metadata']), ' '*2, @@ -888,13 +828,13 @@ def _finish_sync(self, sync_response): with open(sync_response) as f: data = json.load(f) - r = self.client.sync_view_only_account(data['params']) + self.client.sync_view_only_account(data['params']) account_id = data['params']['account_id'] - account = self.client.get_view_only_account(account_id) - balance = self.client.get_balance_for_view_only_account(account_id) + account = self.client.get_account(account_id) + balance = self.client.get_balance_for_account(account_id) print() - print('Synced {} transaction outputs.'.format(len(data['completed_txos']))) + print('Synced {} transaction outputs.'.format(len(data['params']['completed_txos']))) print() _print_account(account, balance) @@ -928,7 +868,7 @@ def _format_account_header(account): output = account['account_id'][:6] if account['name']: output += ' ' + account['name'] - if account.get('object') == 'view_only_account': + if account['view_only']: output += ' [view-only]' return output @@ -1037,7 +977,7 @@ def _save_export(account, secrets, filename): export_data.update({ 'account_id': account['account_id'], - 'account_name': account['name'], + 'name': account['name'], 'account_key': secrets['account_key'], 'first_block_index': account['first_block_index'], 'next_subaddress_index': account['next_subaddress_index'], @@ -1050,7 +990,7 @@ def _save_view_key_export(account, secrets, filename): _save_json_file( filename, { - 'account_name': account['name'], + 'name': account['name'], 'view_private_key': secrets['account_key']['view_private_key'], 'first_block_index': account['first_block_index'], } diff --git a/cli/mobilecoin/client.py b/cli/mobilecoin/client.py index 4e8a85fbe..e83119c6f 100755 --- a/cli/mobilecoin/client.py +++ b/cli/mobilecoin/client.py @@ -121,7 +121,7 @@ def import_view_only_account(self, params): "method": "import_view_only_account", "params": params, }) - return r['view_only_account'] + return r['account'] def get_account(self, account_id): r = self._req({ @@ -134,17 +134,6 @@ def get_all_accounts(self): r = self._req({"method": "get_all_accounts"}) return r['account_map'] - def get_view_only_account(self, account_id): - r = self._req({ - "method": "get_view_only_account", - "params": {"account_id": account_id} - }) - return r['view_only_account'] - - def get_all_view_only_accounts(self): - r = self._req({"method": "get_all_view_only_accounts"}) - return r['account_map'] - def update_account_name(self, account_id, name): r = self._req({ "method": "update_account_name", @@ -161,12 +150,6 @@ def remove_account(self, account_id): "params": {"account_id": account_id} }) - def remove_view_only_account(self, account_id): - return self._req({ - "method": "remove_view_only_account", - "params": {"account_id": account_id} - }) - def export_account_secrets(self, account_id): r = self._req({ "method": "export_account_secrets", @@ -179,8 +162,8 @@ def get_txos_for_account(self, account_id, offset=0, limit=100): "method": "get_txos_for_account", "params": { "account_id": account_id, - "offset": offset, - "limit": limit, + "offset": str(int(offset)), + "limit": str(int(limit)), } }) return r['txo_map'] @@ -209,15 +192,6 @@ def get_balance_for_account(self, account_id): }) return r['balance'] - def get_balance_for_view_only_account(self, account_id): - r = self._req({ - "method": "get_balance_for_view_only_account", - "params": { - "account_id": account_id, - } - }) - return r['balance'] - def get_balance_for_address(self, address): r = self._req({ "method": "get_balance_for_address", @@ -227,15 +201,6 @@ def get_balance_for_address(self, address): }) return r['balance'] - def get_balance_for_view_only_address(self, address): - r = self._req({ - "method": "get_balance_for_view_only_address", - "params": { - "address": address, - } - }) - return r['balance'] - def assign_address_for_account(self, account_id, metadata=None): if metadata is None: metadata = '' @@ -260,17 +225,6 @@ def get_addresses_for_account(self, account_id, offset=0, limit=100): }) return r['address_map'] - def get_addresses_for_view_only_account(self, account_id, offset=0, limit=100): - r = self._req({ - "method": "get_addresses_for_view_only_account", - "params": { - "account_id": account_id, - "offset": str(int(offset)), - "limit": str(int(limit)), - }, - }) - return r['address_map'] - def build_and_submit_transaction(self, account_id, amount, to_address, fee=None): r = self._build_and_submit_transaction(account_id, amount, to_address, fee) return r['transaction_log'] diff --git a/docs/SUMMARY.md b/docs/SUMMARY.md index d2c4e5a3e..73e723ea2 100644 --- a/docs/SUMMARY.md +++ b/docs/SUMMARY.md @@ -3,90 +3,127 @@ * [Welcome!](README.md) ## API Endpoints - -* [Account](api-endpoints/account/README.md) - * [Account](accounts/account/README.md) - * [Create Account](accounts/account/create\_account.md) - * [Import Account](accounts/account/import\_account.md) - * [Import Account Legacy](accounts/account/import\_account\_from\_legacy\_root\_entropy-deprecated.md) - * [Get Account](accounts/account/get\_account.md) - * [Get All Accounts](accounts/account/get\_all\_accounts.md) - * [Get Account Status](accounts/account/get\_account\_status.md) - * [Update Account Name](accounts/account/update\_account\_name.md) - * [Remove Account](accounts/account/remove\_account.md) - * [Account Secrets](accounts/account-secrets/README.md) - * [Export Account Secrets](accounts/account-secrets/export\_account\_secrets.md) - * [Export View Only Account Package](accounts/account-secrets/export\_view\_only\_account\_package.md) - * [Address](accounts/address/README.md) - * [Assign Address For Account](accounts/address/assign\_address\_for\_account.md) - * [Get Addresses For Account](accounts/address/get\_addresses\_for\_account.md) - * [Verify Address](accounts/address/verify\_address.md) - * [Balance](accounts/balance/README.md) - * [Get Balance For Account](accounts/balance/get\_balance\_for\_account.md) - * [Get Balance For Address](accounts/balance/get\_balance\_for\_address.md) -* [View Only Account](api-endpoints/view-only-account/README.md) - * [Account](view-only-accounts/account/README.md) - * [Import](view-only-accounts/account/import\_view\_only\_account.md) - * [Get All](view-only-accounts/account/get\_all\_view\_only\_accounts.md) - * [Get](view-only-accounts/account/get\_view\_only\_account.md) - * [Update Name](view-only-accounts/account/update\_view\_only\_account\_name.md) - * [Remove](view-only-accounts/account/remove\_view\_only\_account.md) - * [Secrets](view-only-accounts/account-secrets/README.md) - * [Export Secrets](view-only-accounts/account-secrets/export\_view\_only\_account\_secrets.md) - * [Balance](view-only-accounts/balance/README.md) - * [Get Balance](view-only-accounts/balance/get\_balance\_for\_view\_only\_account.md) - * [Get Balance For Address](view-only-accounts/balance/get\_balance\_for\_view\_only\_address.md) - * [Syncing](view-only-accounts/syncing/README.md) - * [Create Account Sync Request](view-only-accounts/syncing/create\_view\_only\_account\_sync\_request.md) - * [Sync Account](view-only-accounts/syncing/sync\_view\_only\_account.md) - * [Subaddress](view-only-accounts/subaddress/README.md) - * [Create New Subaddress Request](view-only-accounts/subaddress/create\_new\_subaddress\_request.md) - * [Import Subaddresses](view-only-accounts/subaddress/import\_subaddresses\_to\_view\_only\_account.md) -* [Transaction](api-endpoints/transaction/README.md) - * [Transaction](transactions/transaction/README.md) - * [Build Transaction](transactions/transaction/build\_transaction.md) - * [Submit Transaction](transactions/transaction/submit\_transaction.md) - * [Build And Submit Transaction](transactions/transaction/build\_and\_submit\_transaction.md) - * [Build Split Txo Transaction](transactions/transaction/build\_split\_txo\_transaction.md) - * [Build Unsigned Transaction](transactions/transaction/build\_unsigned\_transaction.md) - * [Transaction Output TXO](transactions/txo/README.md) - * [Get TXO](transactions/txo/get\_txo.md) - * [Get MobileCoin Protocol TXO](transactions/txo/get\_mc\_protocol\_txo.md) - * [Get TXOs For Account](transactions/txo/get\_txos\_for\_account.md) - * [Get TXOs For View Only Account](transactions/txo/get\_txos\_for\_view\_only\_account.md) - * [Get All TXOs For Address](transactions/txo/get\_txo\_object.md) - * [Confirmation](transactions/transaction-confirmation/README.md) - * [Get Confirmations](transactions/transaction-confirmation/get\_confirmations.md) - * [Validate Confirmations](transactions/transaction-confirmation/validate\_confirmation.md) - * [Receiver Receipt](transactions/transaction-receipt/README.md) - * [Check Receiver Receipt Status](transactions/transaction-receipt/check\_receiver\_receipt\_status.md) - * [Create Receiver Receipts](transactions/transaction-receipt/create\_receiver\_receipts.md) - * [Transaction Log](transactions/transaction-log/README.md) - * [Get Transaction Object](transactions/transaction-log/get\_transaction\_object.md) - * [Get Transaction Log](transactions/transaction-log/get\_transaction\_log.md) - * [Get Transaction Logs For Account](transactions/transaction-log/get\_transaction\_logs\_for\_account.md) - * [Get All Transaction Logs For Block](transactions/transaction-log/get\_all\_transaction\_logs\_for\_block.md) - * [Get All Transaction Logs Ordered By Block](transactions/transaction-log/get\_all\_transaction\_logs\_ordered\_by\_block.md) - * [Get MobileCoin Protocol Transaction](transactions/transaction-log/get\_mc\_protocol\_transaction.md) - * [Payment Request](transactions/payment-request/README.md) - * [Create Payment Request](transactions/payment-request/create\_payment\_request.md) - * [Check B58 Type](transactions/payment-request/check\_b58\_type.md) -* [Gift Code](gift-codes/gift-code/README.md) - * [Build Gift Code](gift-codes/gift-code/build\_gift\_code.md) - * [Submit Gift Code](gift-codes/gift-code/submit\_gift\_code.md) - * [Get Gift Code](gift-codes/gift-code/get\_gift\_code.md) - * [Get All Gift Codes](gift-codes/gift-code/get\_all\_gift\_codes.md) - * [Check Gift Code Status](gift-codes/gift-code/check\_gift\_code\_status.md) - * [Claim Gift Code](gift-codes/gift-code/claim\_gift\_code.md) - * [Remove Gift Code](gift-codes/gift-code/remove\_gift\_code.md) -* [Block](other/block/README.md) - * [Get Block](other/block/get\_block.md) -* [Network Status](other/network-status/README.md) - * [Get Network Status](other/network-status/get\_network\_status.md) -* [Wallet Status](other/wallet-status/README.md) - * [Get Wallet Status](other/wallet-status/get\_wallet\_status.md) -* [Version](other/version/README.md) - * [Get Version](other/version/version.md) +* v2 + * Account + * [Account](v2/accounts/account/README.md) + * [Create Account](v2/api-endpoints/create_account.md) + * [Import Account](v2/api-endpoints/import_account.md) + * [Import Account Legacy](v2/api-endpoints/import_account_from_legacy_root_entropy.md) + * [Get Accounts](v2/api-endpoints/get_accounts.md) + * [Get Account Status](v2/api-endpoints/get_account_status.md) + * [Update Account Name](v2/api-endpoints/update_account_name.md) + * [Remove Account](v2/api-endpoints/remove_account.md) + * [Account Secrets](v2/accounts/account-secrets/README.md) + * [Export Account Secrets](v2/api-endpoints/export_account_secrets.md) + * [Address](v2/accounts/address/README.md) + * [Assign Address For Account](v2/api-endpoints/assign_address_for_account.md) + * [Get Address For Account](v2/api-endpoints/get_address_for_account.md) + * [Get Addresses](v2/api-endpoints/get_addresses.md) + * [Get Address Status](v2/api-endpoints/get_address_status.md) + * [Verify Address](v2/api-endpoints/verify_address.md) + * View Only Account + * [Import View Only Account](v2/api-endpoints/import_view_only_account.md) + * [Create View Only Account Import Request](v2/api-endpoints/create_view_only_account_import_request.md) + * [Create View Only Account Sync Request](v2/api-endpoints/create_view_only_account_sync_request.md) + * [Sync View Only Account](v2/api-endpoints/sync_view_only_account.md) + * Transaction + * [Transaction](v2/transactions/transaction/README.md) + * [Build Transaction](v2/api-endpoints/build_transaction.md) + * [Submit Transaction](v2/api-endpoints/submit_transaction.md) + * [Build And Submit Transaction](v2/api-endpoints/build_and_submit_transaction.md) + * [Build Unsigned Transaction](v2/api-endpoints/build_unsigned_transaction.md) + * [Transaction Output TXO](v2/transactions/txo/README.md) + * [Get TXO](v2/api-endpoints/get_txo.md) + * [Get TXOs](v2/api-endpoints/get_txos.md) + * [Get MobileCoin Protocol TXO](v2/api-endpoints/get_mc_protocol_txo.md) + * [Get TXO Membership Proofs](v2/api-endpoints/get_txo_membership_proofs.md) + * [Sample Mixins](v2/api-endpoints/sample_mixins.md) + * [Get TXO Block Index](v2/api-endpoints/get_txo_block_index.md) + * [Confirmation](v2/transactions/transaction-confirmation/README.md) + * [Get Confirmations](v2/api-endpoints/get_confirmations.md) + * [Validate Confirmations](v2/api-endpoints/validate_confirmation.md) + * [Receiver Receipt](v2/transactions/transaction-receipt/README.md) + * [Check Receiver Receipt Status](v2/api-endpoints/check_receiver_receipt_status.md) + * [Create Receiver Receipts](v2/api-endpoints/create_receiver_receipts.md) + * [Transaction Log](v2/transactions/transaction-log/README.md) + * [Get Transaction Log](v2/api-endpoints/get_transaction_log.md) + * [Get Transaction Logs](v2/api-endpoints/get_transaction_logs.md) + * [Get MobileCoin Protocol Transaction](v2/api-endpoints/get_mc_protocol_transaction.md) + * [Payment Request](v2/transactions/payment-request/README.md) + * [Create Payment Request](v2/api-endpoints/create_payment_request.md) + * [Check B58 Type](v2/api-endpoints/check_b58_type.md) + * [Block](v2/other/block/README.md) + * [Get Block](v2/api-endpoints/get_block.md) + * [Network Status](v2/other/network-status/README.md) + * [Get Network Status](v2/api-endpoints/get_network_status.md) + * [Wallet Status](v2/other/wallet-status/README.md) + * [Get Wallet Status](v2/api-endpoints/get_wallet_status.md) + * [Version](v2/other/version/README.md) + * [Get Version](v2/api-endpoints/version.md) +* v1 (deprecated) + * Account + * [Account](v1/accounts//account/README.md) + * [Create Account](v1/api-endpoints/create_account.md) + * [Import Account](v1/api-endpoints/import_account.md) + * [Import Account Legacy](v1/api-endpoints/import_account_from_legacy_root_entropy-deprecated.md) + * [Get Account](v1/api-endpoints/get_account.md) + * [Get All Accounts](v1/api-endpoints/get_all_accounts.md) + * [Get Account Status](v1/api-endpoints/get_account_status.md) + * [Update Account Name](v1/api-endpoints/update_account_name.md) + * [Remove Account](v1/api-endpoints/remove_account.md) + * [Account Secrets](v1/accounts/account-secrets/README.md) + * [Export Account Secrets](v1/api-endpoints/export_account_secrets.md) + * [Address](v1/accounts/address/README.md) + * [Assign Address For Account](v1/api-endpoints/assign_address_for_account.md) + * [Get Addresses For Account](v1/api-endpoints/get_addresses_for_account.md) + * [Verify Address](v1/api-endpoints/verify_address.md) + * [Balance](v1/accounts/balance/README.md) + * [Get Balance For Account](v1/api-endpoints/get_balance_for_account.md) + * [Get Balance For Address](v1/api-endpoints/get_balance_for_address.md) + * Transaction + * [Transaction](v1/transactions/transaction/README.md) + * [Build Transaction](v1/api-endpoints/build_transaction.md) + * [Submit Transaction](v1/api-endpoints/submit_transaction.md) + * [Build And Submit Transaction](v1/api-endpoints/build_and_submit_transaction.md) + * [Build Split Txo Transaction](v1/api-endpoints/build_split_txo_transaction.md) + * [Transaction Output TXO](v1/transactions/txo/README.md) + * [Get TXO](v1/api-endpoints/get_txo.md) + * [Get MobileCoin Protocol TXO](v1/api-endpoints/get_mc_protocol_txo.md) + * [Get TXOs For Account](v1/api-endpoints/get_txos_for_account.md) + * [Get TXOs For View Only Account](v1/api-endpoints/get_txos_for_view_only_account.md) + * [Get All TXOs For Address](v1/api-endpoints/get_txo_object.md) + * [Confirmation](v1/transactions/transaction-confirmation/README.md) + * [Get Confirmations](v1/api-endpoints/get_confirmations.md) + * [Validate Confirmations](v1/api-endpoints/validate_confirmation.md) + * [Receiver Receipt](v1/transactions/transaction-receipt/README.md) + * [Check Receiver Receipt Status](v1/api-endpoints/check_receiver_receipt_status.md) + * [Create Receiver Receipts](v1/api-endpoints/create_receiver_receipts.md) + * [Transaction Log](v1/transactions/transaction-log/README.md) + * [Get Transaction Object](v1/api-endpoints/get_transaction_object.md) + * [Get Transaction Log](v1/api-endpoints/get_transaction_log.md) + * [Get Transaction Logs For Account](v1/api-endpoints/get_transaction_logs_for_account.md) + * [Get All Transaction Logs For Block](v1/api-endpoints/get_all_transaction_logs_for_block.md) + * [Get All Transaction Logs Ordered By Block](v1/api-endpoints/get_all_transaction_logs_ordered_by_block.md) + * [Get MobileCoin Protocol Transaction](v1/api-endpoints/get_mc_protocol_transaction.md) + * [Payment Request](v1/transactions/payment-request/README.md) + * [Create Payment Request](v1/api-endpoints/create_payment_request.md) + * [Check B58 Type](v1/api-endpoints/check_b58_type.md) + * [Gift Code](v1/gift-codes/gift-code/README.md) + * [Build Gift Code](v1/api-endpoints/build_gift_code.md) + * [Submit Gift Code](v1/api-endpoints/submit_gift_code.md) + * [Get Gift Code](v1/api-endpoints/get_gift_code.md) + * [Get All Gift Codes](v1/api-endpoints/get_all_gift_codes.md) + * [Check Gift Code Status](v1/api-endpoints/check_gift_code_status.md) + * [Claim Gift Code](v1/api-endpoints/claim_gift_code.md) + * [Remove Gift Code](v1/api-endpoints/remove_gift_code.md) + * [Block](v1/other/block/README.md) + * [Get Block](v1/api-endpoints/get_block.md) + * [Network Status](v1/other/network-status/README.md) + * [Get Network Status](v1/api-endpoints/get_network_status.md) + * [Wallet Status](v1/other/wallet-status/README.md) + * [Get Wallet Status](v1/api-endpoints/get_wallet_status.md) + * [Version](v1/other/version/README.md) + * [Get Version](v1/api-endpoints/version.md) ## Usage @@ -96,5 +133,6 @@ * [Resolve Disputes](tutorials/resolve-disputes.md) * [View Only Account](usage/view-only-account/README.md) * [Transaction Signer](usage/view-only-account/transaction-signer.md) +* [No Wallet Mode](usage/no-wallet-db/no-wallet-db.md) ## Frequently Asked Questions diff --git a/docs/accounts/account-secrets/export_view_only_account_package.md b/docs/accounts/account-secrets/export_view_only_account_package.md deleted file mode 100644 index 833814495..000000000 --- a/docs/accounts/account-secrets/export_view_only_account_package.md +++ /dev/null @@ -1,77 +0,0 @@ -# Export View Only Account Package - -## Parameters - -| Required Param | Purpose | Requirements | -| -------------- | -------------------------------------------- | --------------------------------- | -| `account_id` | The account on which to perform this action. | Account must exist in the wallet. | - -## Example - -{% tabs %} -{% tab title="Request Body" %} -``` -{ - "method": "export_view_only_account_package", - "params": { - "account_id": "6d95067c5fcc0dd7bbcdd42d49cc3571fe1bb2597a9c397c75b7280eca534208" - }, - "jsonrpc": "2.0", - "id": 1 -} -``` -{% endtab %} - -{% tab title="Response" %} -``` -{ - "method": "export_view_only_account_package", - "result": { - "json_rpc_request": { - "method": "import_view_only_account", - "params": { - "account": { - "object": "view_only_account", - "account_id": "6d95067c5fcc0dd7bbcdd42d49cc3571fe1bb2597a9c397c75b7280eca534208", - "name": "testing", - "first_block_index": "661194", - "next_block_index": "693043", - "main_subaddress_index": "0", - "change_subaddress_index": "1", - "next_subaddress_index": "2" - }, - "secrets": { - "object": "view_only_account_secrets", - "view_private_key": "0a20ec42a30f81c5367042516bcbe499def7346f39870ef0f7d1a467e5325d845007", - "account_id": "6d95067c5fcc0dd7bbcdd42d49cc3571fe1bb2597a9c397c75b7280eca534208" - }, - "subaddresses": [ - { - "object": "view_only_subaddress", - "public_address": "3b63EnYDAaGCoeZ473YwcsoHk47qDcuFo6emkFKtiEfSrNy5NuzLpLCau7yJJ5WfavVjMsK8Qa7FKBDEQF5UkRadFVFKEBEaji2FvfLJRTh", - "account_id": "6d95067c5fcc0dd7bbcdd42d49cc3571fe1bb2597a9c397c75b7280eca534208", - "comment": "Main", - "subaddress_index": "0", - "public_spend_key": "0a203cbe82bc9af6cc20d485534f79c5cc41a887099f424d64b8d9ee3ae4599d7544" - }, - { - "object": "view_only_subaddress", - "public_address": "88hRd28N7srH1wtydh9hWBq8EFfgPy492prHXqvuF4kRu6i6rk6dMNNsGN7H8rdDUcTCCBGDzN14nDEvfWS8W5GytJuUVkD9emCYr9cX7Sr", - "account_id": "6d95067c5fcc0dd7bbcdd42d49cc3571fe1bb2597a9c397c75b7280eca534208", - "comment": "Change", - "subaddress_index": "1", - "public_spend_key": "0a20c61def43c7b62ca7caeec567c23c1fd62d8a627e385b4206f9f91e80af85ea53" - } - ] - }, - "jsonrpc": "2.0", - "id": 1 - } - }, - "jsonrpc": "2.0", - "id": 1 -} -``` -{% endtab %} -{% endtabs %} - diff --git a/docs/api-endpoints/account/README.md b/docs/api-endpoints/account/README.md deleted file mode 100644 index 9412fb6d7..000000000 --- a/docs/api-endpoints/account/README.md +++ /dev/null @@ -1,2 +0,0 @@ -# Account - diff --git a/docs/api-endpoints/transaction/README.md b/docs/api-endpoints/transaction/README.md deleted file mode 100644 index 1b1ccbc6c..000000000 --- a/docs/api-endpoints/transaction/README.md +++ /dev/null @@ -1,2 +0,0 @@ -# Transaction - diff --git a/docs/api-endpoints/view-only-account/README.md b/docs/api-endpoints/view-only-account/README.md deleted file mode 100644 index 1455e5888..000000000 --- a/docs/api-endpoints/view-only-account/README.md +++ /dev/null @@ -1,2 +0,0 @@ -# View Only Account - diff --git a/docs/dbdocs/database.dbml b/docs/dbdocs/database.dbml index 3493f3aa2..13ca096b6 100644 --- a/docs/dbdocs/database.dbml +++ b/docs/dbdocs/database.dbml @@ -3,119 +3,63 @@ Project FullService { } Table accounts { - id integer [pk, not null] - account_id_hex varchar [not null, unique] + id varchar [pk, not null, unique] account_key blob [not null] - entropy blob [not null] + entropy blob key_derivation_version integer [not null] - main_subaddress_index bigint [not null] - change_subaddress_index bigint [not null] - next_subaddress_index bigint [not null] first_block_index bigint [not null] next_block_index bigint [not null] import_block_index bigint name varchar [not null] - fog_enabled bool [not null] + fog_enabled boolean [not null] + view_only boolean [not null] } -Table assigned_subaddresses { - id integer [pk, not null] - assigned_subaddress_b58 varchar [not null, unique] - account_id_hex varchar [not null, ref: > accounts.account_id_hex] - address_book_entry bigint - public_address blob [not null] +Table subaddresses { + public_address_b58 varchar [pk, not null, unique] + account_id varchar [not null, ref: > accounts.id] subaddress_index bigint [not null] comment varchar [not null] - subaddress_spend_key blob [not null] -} - -Table gift_codes { - id integer [pk, not null] - gift_code_b58 varchar [not null] - entropy blob [not null] - txo_public_key blob [not null] - value bigint [not null] - memo text [not null] - account_id_hex varchar [not null, ref: > accounts.account_id_hex] - txo_id_hex varchar [not null, ref: > txos.txo_id_hex] + public_spend_key blob [not null] } Table transaction_logs { - id integer [pk, not null] - transaction_id_hex varchar [not null, unique] - account_id_hex varchar [not null, ref: > accounts.account_id_hex] - assigned_subaddress_b58 varchar [not null] - value bigint [not null] - fee bigint - status varchar(8) [not null] - sent_time bigint + id varchar [not null, unique] + account_id varchar [not null, ref: > accounts.id] + fee_value bigint [not null] + fee_token_id bigint [not null] submitted_block_index bigint + tombstone_block_index bigint finalized_block_index bigint + failed boolean [not null] comment text [not null] - direction varchar(8) [not null] tx blob } -Table transaction_txo_types { - transaction_id_hex varchar [pk, not null, ref: > transaction_logs.transaction_id_hex] - txo_id_hex varchar [pk, not null, ref: > txos.txo_id_hex] - transaction_txo_type varchar(6) [not null] +Table transaction_input_txos { + transaction_log_id varchar [pk, not null, ref: > transaction_logs.id] + txo_id varchar [pk, not null, ref: > txos.id] } -Table txos { - id integer [pk, not null] - txo_id_hex varchar [not null, unique] - value bigint [not null] - token_id integer [not null] - target_key blob [not null] - public_key blob [not null] - e_fog_hint blob [not null] - txo blob [not null] - subaddress_index bigint - key_image blob - received_block_index bigint - pending_tombstone_block_index bigint - spent_block_index bigint - confirmation blob +Table transaction_output_txos { + transaction_log_id varchar [pk, not null, ref: > transaction_logs.id] + txo_id varchar [pk, not null, ref: > txos.id] recipient_public_address_b58 varchar [not null] - minted_account_id_hex varchar [ref: > accounts.account_id_hex] - received_account_id_hex varchar [ref: > accounts.account_id_hex] + is_change boolean [not null] } -Table view_only_accounts { - id integer [pk, not null] - account_id_hex varchar [not null, unique] - view_private_key blob [not null] - first_block_index bigint [not null] - next_block_index bigint [not null] - main_subaddress_index bigint [not null] - change_subaddress_index bigint [not null] - next_subaddress_index bigint [not null] - import_block_index bigint - name varchar [not null] -} - -Table view_only_subaddresses { - id integer [pk, not null] - public_address_b58 varchar [not null, unique] - subaddress_index bigint [not null] - view_only_account_id_hex varchar [not null, ref: > view_only_accounts.account_id_hex] - comment varchar [not null] - public_spend_key blob [not null] -} - -Table view_only_txos { - id integer [pk, not null] - txo_id_hex varchar [not null, unique] +Table txos { + id varchar [not null, unique] + account_id varchar [ref: > accounts.id] value bigint [not null] token_id bigint [not null] + target_key blob [not null] public_key blob [not null] + e_fog_hint blob [not null] txo blob [not null] subaddress_index bigint key_image blob received_block_index bigint - pending_tombstone_block_index bigint - submitted_block_index bigint - spent_block_index bigint - view_only_account_id_hex varchar [ref: > view_only_accounts.account_id_hex] + shared_secret blob + output_transaction_log_id varchar [ref: > transaction_logs.id] } \ No newline at end of file diff --git a/docs/transactions/txo/export_view_only_txouts_without_key_image.md b/docs/transactions/txo/export_view_only_txouts_without_key_image.md deleted file mode 100644 index b029467ab..000000000 --- a/docs/transactions/txo/export_view_only_txouts_without_key_image.md +++ /dev/null @@ -1,40 +0,0 @@ ---- -description: Export the txouts for all view-only txos for a given account, where the key image field is null. Returns an array of serial encoded txouts. ---- - -# Export View Only Txouts Without Key Image - -## Parameters - -| Parameter | Purpose | Requirements | -| :--- | :--- | :--- | -| `account_id` | The target view only account id | The view only account must exist in the DB | - -## Example - -{% tabs %} -{% tab title="Request Body" %} -```text -{ - "method": "export_view_only_txouts_without_key_image", - "params": { - "account_id": "9d784d61ad46b9ee9133a06f1256949ea0fgccf86b167ff76490eb829e9c625f", - }, - "jsonrpc": "2.0", - "id": 1 -} -``` -{% endtab %} - -{% tab title="Response" %} -```text -{ - "method": "export_view_only_txouts_without_key_image", - "result":{"txouts":[[10,45,10,34,10,32,220,99,128,68,231,68,139,140,48,55,161,158,141,145,9,62,111,243,56,92,123,232,14,213,155,54,201,146,156,202,181,105,17,165,49,16,246,74,189,247,212,18,34,10,32,24,87,13,224,11,90,8,6,197,173,3,210,92,156,70,19,170,57,17,189,4,16,23,161,227,148,56,255,222,148,157,115,26,34,10,32,182,100,250,12,131,147,135,247,207,31,74,40,77,99,163,95,59,4,66,173,235,188,106,247,22,111,66,4,180,230,219,97,34,86,10,84,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,42,48,10,46,246,24,16,173,45,197,251,81,122,120,242,116,244,25,188,169,26,81,17,180,244,117,49,138,254,239,62,225,114,247,180,146,21,186,82,21,51,3,244,166,101,101,91,252,52,234],[10,45,10,34,10,32,12,95,230,208,121,122,158,29,8,36,177,4,54,34,158,239,224,133,98,243,191,228,143,62,200,201,182,31,181,104,111,102,17,251,185,105,184,230,192,197,36,18,34,10,32,208,38,58,249,42,15,144,191,80,135,222,9,63,230,138,176,229,40,215,84,68,50,47,56,71,57,94,52,40,52,111,67,26,34,10,32,6,87,226,62,54,243,166,208,127,198,101,211,79,253,246,198,63,228,199,53,153,86,221,44,71,163,148,153,52,149,33,51,34,86,10,84,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,42,48,10,46,123,81,18,229,102,234,90,11,37,35,166,3,245,59,127,17,19,133,77,10,180,105,202,174,97,29,150,156,103,164,176,224,1,99,237,21,12,155,185,242,21,80,182,98,66,187],[10,45,10,34,10,32,208,6,12,150,24,191,173,39,227,43,87,172,65,211,24,128,89,100,30,84,225,51,119,151,76,113,123,177,95,126,96,90,17,94,56,132,99,11,179,255,1,18,34,10,32,106,140,196,203,127,210,232,121,100,147,21,14,75,255,121,54,227,228,118,7,133,84,78,239,211,185,144,112,228,87,80,1,26,34,10,32,160,84,245,69,139,79,29,116,37,144,255,117,1,24,208,80,94,25,41,91,161,104,25,36,222,15,45,214,187,224,130,18,34,86,10,84,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,42,48,10,46,49,179,226,78,253,110,50,18,226,248,100,145,217,56,88,1,127,125,175,248,162,41,193,110,41,71,63,30,24,107,168,123,105,159,88,150,251,11,212,163,151,99,182,254,21,89]] - }, - "jsonrpc": "2.0", - "id": 1 -} -``` -{% endtab %} -{% endtabs %} \ No newline at end of file diff --git a/docs/transactions/txo/get_txos_for_view_only_account.md b/docs/transactions/txo/get_txos_for_view_only_account.md deleted file mode 100644 index 7f1357ec0..000000000 --- a/docs/transactions/txo/get_txos_for_view_only_account.md +++ /dev/null @@ -1,74 +0,0 @@ ---- -description: Get view only TXOs for a given view only account with offset and limit parameters ---- - -# Get TXOs For View Only Account - -## Parameters - -| Parameter | Purpose | Requirements | -| :--- | :--- | :--- | -| `account_id` | The account on which to perform this action. | Account must exist in the wallet. | -| `offset` | The pagination offset. Results start at the offset index. Optional, defaults to 0. | | -| `limit` | Limit for the number of results. Optional, defaults to 100 | | - -## Example - -{% tabs %} -{% tab title="Request Body" %} -```text -{ - "method": "get_txos_for_view_only_account", - "params": { - "account_id": "b59b3d0efd6840ace19cdc258f035cc87e6a63b6c24498763c478c417c1f44ca", - "offset": "2", - "limit": "8" - }, - "jsonrpc": "2.0", - "id": 1 -} -``` -{% endtab %} - -{% tab title="Response" %} -```text -{ - "method": "get_txos_for_view_only_account", - "result": { - "txo_ids": [ - "001cdcc1f0a22dc0ddcdaac6020cc03d919cbc3c36923f157b4a6bf0dc980167", - "00408833347550b046f0996afe92313745f76e307904686e93de5bab3590e9da", - "005b41a40be1401426f9a00965cc334e4703e4089adb8fa00616e7b25b92c6e5" - ], - "txo_map": { - "001cdcc1f0a22dc0ddcdaac6020cc03d919cbc3c36923f157b4a6bf0dc980167": { - "object": "view_only_txo", - "txo_id_hex": "84eab721b7eeb4dc6f6d73c0504182a06ccfb98e2d341acac2dfe22d831fae44", - "value_pmob": "10000000000000", - "public_key": "ef3e04533424fd181e8039ec4e2df0bc67c2f59dbbe55d660202d0fc588638d2", - "view_only_account_id_hex": "324a0969a356a81916eecb3aa002da2bbc79154a835c9f6df61d71f67dc5f632", - "spent": false - } - "001cdcc1f0a22dc0ddcdaac6020cc03d919cbc3c36923f157b4a6bf0dc980167": { - "object": "view_only_txo", - "txo_id_hex": "27eab721b7eeb4dc6f6d73c0504182a06ccfb98e2d341acac2dfe22d831fae44", - "value_pmob": "20000000000000", - "public_key": "222204533424fd181e8039ec4e2df0bc67c2f59dbbe55d660202d0fc588638d2", - "view_only_account_id_hex": "324a0969a356a81916eecb3aa002da2bbc79154a835c9f6df61d71f67dc5f632", - "spent": false - } - "005b41a40be1401426f9a00965cc334e4703e4089adb8fa00616e7b25b92c6e5": { - "object": "view_only_txo", - "txo_id_hex": "93eab721b7eeb4dc6f6d73c0504182a06ccfb98e2d341acac2dfe22d831fae44", - "value_pmob": "30000000000000", - "public_key": "123454533424fd181e8039ec4e2df0bc67c2f59dbbe55d660202d0fc588638d2", - "view_only_account_id_hex": "324a0969a356a81916eecb3aa002da2bbc79154a835c9f6df61d71f67dc5f632", - "spent": false - } - }, - "jsonrpc": "2.0", - "id": 1 -} -``` -{% endtab %} -{% endtabs %} diff --git a/docs/transactions/txo/set_view_only_txos_key_images.md b/docs/transactions/txo/set_view_only_txos_key_images.md deleted file mode 100644 index aa469a465..000000000 --- a/docs/transactions/txo/set_view_only_txos_key_images.md +++ /dev/null @@ -1,40 +0,0 @@ ---- -description: Set the key image field for view only txos. Useful for offline transaction signing and hot + cold wallet flows. Any of the view only txos for which the supplied key-image already exists on the blockchain will be marked as spent. ---- - -# Set View Only Txos Key Images - -## Parameters - -| Parameter | Purpose | Requirements | -| :--- | :--- | :--- | -| `txos_with_key_images` | An array of tuples, with the first element being the serial encoded txout and the second element being the serial encoded key image. | The view only txos must exist in the DB. | - -## Example - -{% tabs %} -{% tab title="Request Body" %} -```text -{ - "method": "set_view_only_txos_key_images", - "params": { - "txos_with_key_images": [[[10,45,10,34,10,32,220,99,128,68,231,68,139,140,48,55,161,158,141,145,9,62,111,243,56,92,123,232,14,213,155,54,201,146,156,202,181,105,17,165,49,16,246,74,189,247,212,18,34,10,32,24,87,13,224,11,90,8,6,197,173,3,210,92,156,70,19,170,57,17,189,4,16,23,161,227,148,56,255,222,148,157,115,26,34,10,32,182,100,250,12,131,147,135,247,207,31,74,40,77,99,163,95,59,4,66,173,235,188,106,247,22,111,66,4,180,230,219,97,34,86,10,84,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,42,48,10,46,246,24,16,173,45,197,251,81,122,120,242,116,244,25,188,169,26,81,17,180,244,117,49,138,254,239,62,225,114,247,180,146,21,186,82,21,51,3,244,166,101,101,91,252,52,234],[10,32,46,182,252,137,25,120,243,78,119,33,100,171,231,70,57,142,14,59,193,110,157,108,72,169,220,62,112,11,43,32,100,55]],[[10,45,10,34,10,32,12,95,230,208,121,122,158,29,8,36,177,4,54,34,158,239,224,133,98,243,191,228,143,62,200,201,182,31,181,104,111,102,17,251,185,105,184,230,192,197,36,18,34,10,32,208,38,58,249,42,15,144,191,80,135,222,9,63,230,138,176,229,40,215,84,68,50,47,56,71,57,94,52,40,52,111,67,26,34,10,32,6,87,226,62,54,243,166,208,127,198,101,211,79,253,246,198,63,228,199,53,153,86,221,44,71,163,148,153,52,149,33,51,34,86,10,84,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,42,48,10,46,123,81,18,229,102,234,90,11,37,35,166,3,245,59,127,17,19,133,77,10,180,105,202,174,97,29,150,156,103,164,176,224,1,99,237,21,12,155,185,242,21,80,182,98,66,187],[10,32,150,183,210,211,221,182,51,180,18,210,137,15,157,107,241,124,233,98,187,87,213,181,160,88,158,89,230,14,21,228,78,101]],[[10,45,10,34,10,32,208,6,12,150,24,191,173,39,227,43,87,172,65,211,24,128,89,100,30,84,225,51,119,151,76,113,123,177,95,126,96,90,17,94,56,132,99,11,179,255,1,18,34,10,32,106,140,196,203,127,210,232,121,100,147,21,14,75,255,121,54,227,228,118,7,133,84,78,239,211,185,144,112,228,87,80,1,26,34,10,32,160,84,245,69,139,79,29,116,37,144,255,117,1,24,208,80,94,25,41,91,161,104,25,36,222,15,45,214,187,224,130,18,34,86,10,84,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,42,48,10,46,49,179,226,78,253,110,50,18,226,248,100,145,217,56,88,1,127,125,175,248,162,41,193,110,41,71,63,30,24,107,168,123,105,159,88,150,251,11,212,163,151,99,182,254,21,89],[10,32,114,151,71,132,64,125,21,250,28,218,11,133,139,34,11,171,26,154,116,198,27,216,89,255,0,5,139,33,213,30,203,109]]], - }, - "jsonrpc": "2.0", - "id": 1 -} -``` -{% endtab %} - -{% tab title="Response" %} -```text -{ - "method": "set_view_only_txos_key_images", - "result":{"success":true}," - }, - "jsonrpc": "2.0", - "id": 1 -} -``` -{% endtab %} -{% endtabs %} \ No newline at end of file diff --git a/docs/tutorials/environment-setup.md b/docs/tutorials/environment-setup.md index 56eef683f..2c5a6d649 100644 --- a/docs/tutorials/environment-setup.md +++ b/docs/tutorials/environment-setup.md @@ -20,28 +20,30 @@ description: Set up your environment to run full service on Mac or Linux. ```text mkdir -p testnet-dbs - RUST_LOG=info,mc_connection=info,mc_ledger_sync=info ./full-service \ + RUST_LOG=info,mc_connection=info,mc_ledger_sync=info ./mc-full-service \ --wallet-db ./testnet-dbs/wallet.db \ --ledger-db ./testnet-dbs/ledger-db/ \ --peer mc://node1.test.mobilecoin.com/ \ --peer mc://node2.test.mobilecoin.com/ \ --tx-source-url https://s3-us-west-1.amazonaws.com/mobilecoin.chain/node1.test.mobilecoin.com/ \ --tx-source-url https://s3-us-west-1.amazonaws.com/mobilecoin.chain/node2.test.mobilecoin.com/ \ - --fog-ingest-enclave-css $(pwd)/ingest-enclave.css + --fog-ingest-enclave-css $(pwd)/ingest-enclave.css \ + --chain-id test ``` * If you downloaded MainNet, run: ```text mkdir -p mainnet-dbs - RUST_LOG=info,mc_connection=info,mc_ledger_sync=info ./full-service \ + RUST_LOG=info,mc_connection=info,mc_ledger_sync=info ./mc-full-service \ --wallet-db ./mainnet-dbs/wallet.db \ --ledger-db ./mainnet-dbs/ledger-db/ \ --peer mc://node1.prod.mobilecoinww.com/ \ --peer mc://node2.prod.mobilecoinww.com/ \ --tx-source-url https://ledger.mobilecoinww.com/node1.prod.mobilecoinww.com/ \ --tx-source-url https://ledger.mobilecoinww.com/node2.prod.mobilecoinww.com/ \ - --fog-ingest-enclave-css $(pwd)/ingest-enclave.css + --fog-ingest-enclave-css $(pwd)/ingest-enclave.css \ + --chain-id test ``` {% hint style="info" %} diff --git a/docs/usage/no-wallet-db/no-wallet-db.md b/docs/usage/no-wallet-db/no-wallet-db.md new file mode 100644 index 000000000..9e007f831 --- /dev/null +++ b/docs/usage/no-wallet-db/no-wallet-db.md @@ -0,0 +1,7 @@ +--- +description: How to start in no wallet mode +--- + +# No Wallet Mode (Partial-Service) + +You can optionally start Full Service without specifying a path for the wallet-db, which will cause it to start in No Wallet Mode. When this is true, any endpoints that require interaction with a wallet_db will be disabled. \ No newline at end of file diff --git a/docs/accounts/account-secrets/README.md b/docs/v1/accounts/account-secrets/README.md similarity index 100% rename from docs/accounts/account-secrets/README.md rename to docs/v1/accounts/account-secrets/README.md diff --git a/docs/accounts/account/README.md b/docs/v1/accounts/account/README.md similarity index 100% rename from docs/accounts/account/README.md rename to docs/v1/accounts/account/README.md diff --git a/docs/accounts/address/README.md b/docs/v1/accounts/address/README.md similarity index 100% rename from docs/accounts/address/README.md rename to docs/v1/accounts/address/README.md diff --git a/docs/accounts/balance/README.md b/docs/v1/accounts/balance/README.md similarity index 100% rename from docs/accounts/balance/README.md rename to docs/v1/accounts/balance/README.md diff --git a/docs/accounts/address/assign_address_for_account.md b/docs/v1/api-endpoints/assign_address_for_account.md similarity index 100% rename from docs/accounts/address/assign_address_for_account.md rename to docs/v1/api-endpoints/assign_address_for_account.md diff --git a/docs/transactions/transaction/build_and_submit_transaction.md b/docs/v1/api-endpoints/build_and_submit_transaction.md similarity index 100% rename from docs/transactions/transaction/build_and_submit_transaction.md rename to docs/v1/api-endpoints/build_and_submit_transaction.md diff --git a/docs/gift-codes/gift-code/build_gift_code.md b/docs/v1/api-endpoints/build_gift_code.md similarity index 100% rename from docs/gift-codes/gift-code/build_gift_code.md rename to docs/v1/api-endpoints/build_gift_code.md diff --git a/docs/transactions/transaction/build_split_txo_transaction.md b/docs/v1/api-endpoints/build_split_txo_transaction.md similarity index 100% rename from docs/transactions/transaction/build_split_txo_transaction.md rename to docs/v1/api-endpoints/build_split_txo_transaction.md diff --git a/docs/transactions/transaction/build_transaction.md b/docs/v1/api-endpoints/build_transaction.md similarity index 100% rename from docs/transactions/transaction/build_transaction.md rename to docs/v1/api-endpoints/build_transaction.md diff --git a/docs/transactions/payment-request/check_b58_type.md b/docs/v1/api-endpoints/check_b58_type.md similarity index 100% rename from docs/transactions/payment-request/check_b58_type.md rename to docs/v1/api-endpoints/check_b58_type.md diff --git a/docs/gift-codes/gift-code/check_gift_code_status.md b/docs/v1/api-endpoints/check_gift_code_status.md similarity index 100% rename from docs/gift-codes/gift-code/check_gift_code_status.md rename to docs/v1/api-endpoints/check_gift_code_status.md diff --git a/docs/transactions/transaction-receipt/check_receiver_receipt_status.md b/docs/v1/api-endpoints/check_receiver_receipt_status.md similarity index 100% rename from docs/transactions/transaction-receipt/check_receiver_receipt_status.md rename to docs/v1/api-endpoints/check_receiver_receipt_status.md diff --git a/docs/gift-codes/gift-code/claim_gift_code.md b/docs/v1/api-endpoints/claim_gift_code.md similarity index 100% rename from docs/gift-codes/gift-code/claim_gift_code.md rename to docs/v1/api-endpoints/claim_gift_code.md diff --git a/docs/accounts/account/create_account.md b/docs/v1/api-endpoints/create_account.md similarity index 100% rename from docs/accounts/account/create_account.md rename to docs/v1/api-endpoints/create_account.md diff --git a/docs/transactions/payment-request/create_payment_request.md b/docs/v1/api-endpoints/create_payment_request.md similarity index 100% rename from docs/transactions/payment-request/create_payment_request.md rename to docs/v1/api-endpoints/create_payment_request.md diff --git a/docs/transactions/transaction-receipt/create_receiver_receipts.md b/docs/v1/api-endpoints/create_receiver_receipts.md similarity index 100% rename from docs/transactions/transaction-receipt/create_receiver_receipts.md rename to docs/v1/api-endpoints/create_receiver_receipts.md diff --git a/docs/accounts/account-secrets/export_account_secrets.md b/docs/v1/api-endpoints/export_account_secrets.md similarity index 100% rename from docs/accounts/account-secrets/export_account_secrets.md rename to docs/v1/api-endpoints/export_account_secrets.md diff --git a/docs/accounts/account/get_account.md b/docs/v1/api-endpoints/get_account.md similarity index 100% rename from docs/accounts/account/get_account.md rename to docs/v1/api-endpoints/get_account.md diff --git a/docs/accounts/account/get_account_status.md b/docs/v1/api-endpoints/get_account_status.md similarity index 100% rename from docs/accounts/account/get_account_status.md rename to docs/v1/api-endpoints/get_account_status.md diff --git a/docs/accounts/address/get_address_for_account.md b/docs/v1/api-endpoints/get_address_for_account.md similarity index 100% rename from docs/accounts/address/get_address_for_account.md rename to docs/v1/api-endpoints/get_address_for_account.md diff --git a/docs/accounts/address/get_addresses_for_account.md b/docs/v1/api-endpoints/get_addresses_for_account.md similarity index 100% rename from docs/accounts/address/get_addresses_for_account.md rename to docs/v1/api-endpoints/get_addresses_for_account.md diff --git a/docs/accounts/account/get_all_accounts.md b/docs/v1/api-endpoints/get_all_accounts.md similarity index 100% rename from docs/accounts/account/get_all_accounts.md rename to docs/v1/api-endpoints/get_all_accounts.md diff --git a/docs/gift-codes/gift-code/get_all_gift_codes.md b/docs/v1/api-endpoints/get_all_gift_codes.md similarity index 100% rename from docs/gift-codes/gift-code/get_all_gift_codes.md rename to docs/v1/api-endpoints/get_all_gift_codes.md diff --git a/docs/transactions/transaction-log/get_all_transaction_logs_for_block.md b/docs/v1/api-endpoints/get_all_transaction_logs_for_block.md similarity index 100% rename from docs/transactions/transaction-log/get_all_transaction_logs_for_block.md rename to docs/v1/api-endpoints/get_all_transaction_logs_for_block.md diff --git a/docs/transactions/transaction-log/get_all_transaction_logs_ordered_by_block.md b/docs/v1/api-endpoints/get_all_transaction_logs_ordered_by_block.md similarity index 100% rename from docs/transactions/transaction-log/get_all_transaction_logs_ordered_by_block.md rename to docs/v1/api-endpoints/get_all_transaction_logs_ordered_by_block.md diff --git a/docs/transactions/txo/get_all_txos_for_address.md b/docs/v1/api-endpoints/get_all_txos_for_address.md similarity index 100% rename from docs/transactions/txo/get_all_txos_for_address.md rename to docs/v1/api-endpoints/get_all_txos_for_address.md diff --git a/docs/view-only-accounts/account/get_all_view_only_accounts.md b/docs/v1/api-endpoints/get_all_view_only_accounts.md similarity index 100% rename from docs/view-only-accounts/account/get_all_view_only_accounts.md rename to docs/v1/api-endpoints/get_all_view_only_accounts.md diff --git a/docs/accounts/balance/get_balance_for_account.md b/docs/v1/api-endpoints/get_balance_for_account.md similarity index 100% rename from docs/accounts/balance/get_balance_for_account.md rename to docs/v1/api-endpoints/get_balance_for_account.md diff --git a/docs/accounts/balance/get_balance_for_address.md b/docs/v1/api-endpoints/get_balance_for_address.md similarity index 100% rename from docs/accounts/balance/get_balance_for_address.md rename to docs/v1/api-endpoints/get_balance_for_address.md diff --git a/docs/other/block/get_block.md b/docs/v1/api-endpoints/get_block.md similarity index 100% rename from docs/other/block/get_block.md rename to docs/v1/api-endpoints/get_block.md diff --git a/docs/transactions/transaction-confirmation/get_confirmations.md b/docs/v1/api-endpoints/get_confirmations.md similarity index 100% rename from docs/transactions/transaction-confirmation/get_confirmations.md rename to docs/v1/api-endpoints/get_confirmations.md diff --git a/docs/gift-codes/gift-code/get_gift_code.md b/docs/v1/api-endpoints/get_gift_code.md similarity index 100% rename from docs/gift-codes/gift-code/get_gift_code.md rename to docs/v1/api-endpoints/get_gift_code.md diff --git a/docs/transactions/transaction-log/get_mc_protocol_transaction.md b/docs/v1/api-endpoints/get_mc_protocol_transaction.md similarity index 100% rename from docs/transactions/transaction-log/get_mc_protocol_transaction.md rename to docs/v1/api-endpoints/get_mc_protocol_transaction.md diff --git a/docs/transactions/txo/get_mc_protocol_txo.md b/docs/v1/api-endpoints/get_mc_protocol_txo.md similarity index 100% rename from docs/transactions/txo/get_mc_protocol_txo.md rename to docs/v1/api-endpoints/get_mc_protocol_txo.md diff --git a/docs/other/network-status/get_network_status.md b/docs/v1/api-endpoints/get_network_status.md similarity index 100% rename from docs/other/network-status/get_network_status.md rename to docs/v1/api-endpoints/get_network_status.md diff --git a/docs/transactions/transaction-log/get_transaction_log.md b/docs/v1/api-endpoints/get_transaction_log.md similarity index 100% rename from docs/transactions/transaction-log/get_transaction_log.md rename to docs/v1/api-endpoints/get_transaction_log.md diff --git a/docs/transactions/transaction-log/get_transaction_logs_for_account.md b/docs/v1/api-endpoints/get_transaction_logs_for_account.md similarity index 100% rename from docs/transactions/transaction-log/get_transaction_logs_for_account.md rename to docs/v1/api-endpoints/get_transaction_logs_for_account.md diff --git a/docs/transactions/transaction-log/get_transaction_object.md b/docs/v1/api-endpoints/get_transaction_object.md similarity index 100% rename from docs/transactions/transaction-log/get_transaction_object.md rename to docs/v1/api-endpoints/get_transaction_object.md diff --git a/docs/transactions/txo/get_txo.md b/docs/v1/api-endpoints/get_txo.md similarity index 100% rename from docs/transactions/txo/get_txo.md rename to docs/v1/api-endpoints/get_txo.md diff --git a/docs/transactions/txo/get_txo_object.md b/docs/v1/api-endpoints/get_txo_object.md similarity index 100% rename from docs/transactions/txo/get_txo_object.md rename to docs/v1/api-endpoints/get_txo_object.md diff --git a/docs/transactions/txo/get_txos_for_account.md b/docs/v1/api-endpoints/get_txos_for_account.md similarity index 100% rename from docs/transactions/txo/get_txos_for_account.md rename to docs/v1/api-endpoints/get_txos_for_account.md diff --git a/docs/other/wallet-status/get_wallet_status.md b/docs/v1/api-endpoints/get_wallet_status.md similarity index 71% rename from docs/other/wallet-status/get_wallet_status.md rename to docs/v1/api-endpoints/get_wallet_status.md index e59f45166..53918f9f4 100644 --- a/docs/other/wallet-status/get_wallet_status.md +++ b/docs/v1/api-endpoints/get_wallet_status.md @@ -42,34 +42,34 @@ description: Get the current status of a wallet. Note that pmob calculations do "account_id": "b0be5377a2f45b1573586ed530b2901a559d9952ea8a02f8c2dbb033a935ac17", "key_derivation_version:": "2", "main_address": "7JvajhkAZYGmrpCY7ZpEiXRK5yW1ooTV7EWfDNu3Eyt572mH1wNb37BWiU6JqRUvgopPqSVZRexhXXpjF3wqLQR7HaJrcdbHmULujgFmzav", - "name": "Carol", + "name": "Brady", "next_subaddress_index": "2", "first_block_index": "3500", "object": "account", "recovery_mode": false } }, - "view_only_account_ids": [ - "b0be5377a2f45b1573586ed530b2901a559d9952ea8a02f8c2dbb033a935ac17", - ], - "view_only_account_map": { - "6ed6b79004032fcfcfa65fa7a307dd004b8ec4ed77660d36d44b67452f62b470": { - "account_id": "6ed6b79004032fcfcfa65fa7a307dd004b8ec4ed77660d36d44b67452f62b470", - "name": "Look at these cats", - "first_block_index": "3500", - "last_block_index": "3500", - "object": "view_only_account", - } - }, "is_synced_all": false, "local_block_height": "152918", "network_block_height": "152918", - "object": "wallet_status", - "total_orphaned_pmob": "0", - "total_pending_pmob": "70148220000000000", - "total_secreted_pmob": "0", - "total_spent_pmob": "0", - "total_unspent_pmob": "220588320000000000" + "balance_per_token": { + "0": { + "orphaned": "0", + "pending": "70148220000000000", + "secreted": "0", + "spent": "0", + "unspent": "220588320000000000", + "unverified": "1300004044440000" + }, + "1": { + "orphaned": "0", + "pending": "70148220000000000", + "secreted": "0", + "spent": "0", + "unspent": "220588320000000000", + "unverified": "1300004044440000" + } + } } }, "error": null, diff --git a/docs/accounts/account/import_account.md b/docs/v1/api-endpoints/import_account.md similarity index 100% rename from docs/accounts/account/import_account.md rename to docs/v1/api-endpoints/import_account.md diff --git a/docs/accounts/account/import_account_from_legacy_root_entropy-deprecated.md b/docs/v1/api-endpoints/import_account_from_legacy_root_entropy-deprecated.md similarity index 100% rename from docs/accounts/account/import_account_from_legacy_root_entropy-deprecated.md rename to docs/v1/api-endpoints/import_account_from_legacy_root_entropy-deprecated.md diff --git a/docs/accounts/account/remove_account.md b/docs/v1/api-endpoints/remove_account.md similarity index 100% rename from docs/accounts/account/remove_account.md rename to docs/v1/api-endpoints/remove_account.md diff --git a/docs/gift-codes/gift-code/remove_gift_code.md b/docs/v1/api-endpoints/remove_gift_code.md similarity index 100% rename from docs/gift-codes/gift-code/remove_gift_code.md rename to docs/v1/api-endpoints/remove_gift_code.md diff --git a/docs/gift-codes/gift-code/submit_gift_code.md b/docs/v1/api-endpoints/submit_gift_code.md similarity index 100% rename from docs/gift-codes/gift-code/submit_gift_code.md rename to docs/v1/api-endpoints/submit_gift_code.md diff --git a/docs/transactions/transaction/submit_transaction.md b/docs/v1/api-endpoints/submit_transaction.md similarity index 100% rename from docs/transactions/transaction/submit_transaction.md rename to docs/v1/api-endpoints/submit_transaction.md diff --git a/docs/accounts/account/update_account_name.md b/docs/v1/api-endpoints/update_account_name.md similarity index 100% rename from docs/accounts/account/update_account_name.md rename to docs/v1/api-endpoints/update_account_name.md diff --git a/docs/transactions/transaction-confirmation/validate_confirmation.md b/docs/v1/api-endpoints/validate_confirmation.md similarity index 100% rename from docs/transactions/transaction-confirmation/validate_confirmation.md rename to docs/v1/api-endpoints/validate_confirmation.md diff --git a/docs/accounts/address/verify_address.md b/docs/v1/api-endpoints/verify_address.md similarity index 100% rename from docs/accounts/address/verify_address.md rename to docs/v1/api-endpoints/verify_address.md diff --git a/docs/other/version/version.md b/docs/v1/api-endpoints/version.md similarity index 100% rename from docs/other/version/version.md rename to docs/v1/api-endpoints/version.md diff --git a/docs/gift-codes/gift-code/README.md b/docs/v1/gift-codes/gift-code/README.md similarity index 100% rename from docs/gift-codes/gift-code/README.md rename to docs/v1/gift-codes/gift-code/README.md diff --git a/docs/other/block/README.md b/docs/v1/other/block/README.md similarity index 100% rename from docs/other/block/README.md rename to docs/v1/other/block/README.md diff --git a/docs/other/network-status/README.md b/docs/v1/other/network-status/README.md similarity index 100% rename from docs/other/network-status/README.md rename to docs/v1/other/network-status/README.md diff --git a/docs/other/version/README.md b/docs/v1/other/version/README.md similarity index 100% rename from docs/other/version/README.md rename to docs/v1/other/version/README.md diff --git a/docs/other/wallet-status/README.md b/docs/v1/other/wallet-status/README.md similarity index 54% rename from docs/other/wallet-status/README.md rename to docs/v1/other/wallet-status/README.md index 422fb65a9..f16eee28b 100644 --- a/docs/other/wallet-status/README.md +++ b/docs/v1/other/wallet-status/README.md @@ -8,19 +8,12 @@ description: The Wallet Status provides a quick overview of the contents of the | _Name_ | _Type_ | _Description_ | | :--- | :--- | :--- | -| `object` | string, value is "wallet\_status" | String representing the object's type. Objects of the same type share the same value. | | `network_block_height` | string \(uint64\) | The block count of MobileCoin's distributed ledger. | | `local_block_height` | string \(uint64\) | The local block count downloaded from the ledger. The local database is synced when the `local_block_height` reaches the `network_block_height`. The account_block_height can only sync up to `local_block_height`. | | `is_synced_all` | Boolean | Whether ALL accounts are synced with the `network_block_height`. Balances may not appear correct if any account is still syncing. | -| `total_unspent_pmob` | string \(uint64\) | Unspent pico mob for ALL accounts at the `account_block_height`. If the account is syncing, this value may change. | -| `total_pending_pmob` | string \(uint64\) | Pending outgoing pico mob from ALL accounts. Pending pico mobs will clear once the ledger processes the outgoing TXO. The `available_pmob` will reflect the change. | -| `total_spent_pmob` | string \(uint64\) | Spent pico MOB. This is the sum of all the TXOs in the wallet which have been spent. | -| `total_secreted_pmob` | string \(uint64\) | Secreted \(minted\) pico MOB. This is the sum of all the TXOs which have been created in the wallet for outgoing transactions. | -| `total_orphaned_pmob` | string \(uint64\) | Orphaned pico MOB. The orphaned value represents the TXOs which were view-key matched, but which can not be spent until their subaddress index is recovered. | +| `balance_per_token` | map \(string, Balance\) | Map of balances for each token that is present in the wallet | | `account_ids` | list | A list of all `account_ids` imported into the wallet in order of import. | | `account_map` | hash map | A normalized hash mapping `account_id` to account objects. | -| `view_only_account_ids` | list | A list of all `account_ids` for view only accounts imported into the wallet in order of import. | -| `view_only_account_map` | hash map | A normalized hash mapping view only `account_id` to view only account objects. | ## ​Example @@ -53,27 +46,27 @@ description: The Wallet Status provides a quick overview of the contents of the "recovery_mode": false } }, - "view_only_account_ids": [ - "b0be5377a2f45b1573586ed530b2901a559d9952ea8a02f8c2dbb033a935ac17", - ], - "view_only_account_map": { - "6ed6b79004032fcfcfa65fa7a307dd004b8ec4ed77660d36d44b67452f62b470": { - "account_id": "6ed6b79004032fcfcfa65fa7a307dd004b8ec4ed77660d36d44b67452f62b470", - "name": "Look at these cats", - "first_block_index": "3500", - "last_block_index": "3500", - "object": "view_only_account", - } - }, "is_synced_all": false, "local_block_height": "152918", "network_block_height": "152918", - "object": "wallet_status", - "total_orphaned_pmob": "0", - "total_pending_pmob": "70148220000000000", - "total_secreted_pmob": "0", - "total_spent_pmob": "0", - "total_unspent_pmob": "220588320000000000" + "balance_per_token": { + "0": { + "orphaned": "0", + "pending": "70148220000000000", + "secreted": "0", + "spent": "0", + "unspent": "220588320000000000", + "unverified": "1300004044440000" + }, + "1": { + "orphaned": "0", + "pending": "70148220000000000", + "secreted": "0", + "spent": "0", + "unspent": "220588320000000000", + "unverified": "1300004044440000" + } + } } ``` diff --git a/docs/transactions/payment-request/README.md b/docs/v1/transactions/payment-request/README.md similarity index 100% rename from docs/transactions/payment-request/README.md rename to docs/v1/transactions/payment-request/README.md diff --git a/docs/transactions/transaction-confirmation/README.md b/docs/v1/transactions/transaction-confirmation/README.md similarity index 100% rename from docs/transactions/transaction-confirmation/README.md rename to docs/v1/transactions/transaction-confirmation/README.md diff --git a/docs/transactions/transaction-log/README.md b/docs/v1/transactions/transaction-log/README.md similarity index 100% rename from docs/transactions/transaction-log/README.md rename to docs/v1/transactions/transaction-log/README.md diff --git a/docs/transactions/transaction-receipt/README.md b/docs/v1/transactions/transaction-receipt/README.md similarity index 100% rename from docs/transactions/transaction-receipt/README.md rename to docs/v1/transactions/transaction-receipt/README.md diff --git a/docs/transactions/transaction/README.md b/docs/v1/transactions/transaction/README.md similarity index 100% rename from docs/transactions/transaction/README.md rename to docs/v1/transactions/transaction/README.md diff --git a/docs/transactions/txo/README.md b/docs/v1/transactions/txo/README.md similarity index 100% rename from docs/transactions/txo/README.md rename to docs/v1/transactions/txo/README.md diff --git a/docs/v2/accounts/account-secrets/README.md b/docs/v2/accounts/account-secrets/README.md new file mode 100644 index 000000000..6ee2f3194 --- /dev/null +++ b/docs/v2/accounts/account-secrets/README.md @@ -0,0 +1,37 @@ +--- +description: >- + The secret keys for an account. The account secrets are returned separately + from other account information, to enable more careful handling of + cryptographically sensitive information. +--- + +# Account Secrets + +## Attributes + +| Name | Type | Description | +| :--- | :--- | :--- | +| `account_id` | string | The unique identifier for the account. | +| `mnemonic` | string | A BIP39-encoded mnemonic phrase used to generate the account key. | +| `key_derivation_version` | string \(uint64\) | The version number of the key derivation path used to generate the account key from the mnemonic. | +| `account_key` | account\_key | The view and spend keys used to transact on the MobileCoin network. Also may contain keys to connect to the Fog ledger scanning service. | +| `view_account_key` | view\_account\_key | The private view and public spend keys for this account | + +## Example + +```text +{ + "object": "account_secrets", + "account_id": "3407fbbc250799f5ce9089658380c5fe152403643a525f581f359917d8d59d52", + "mnemonic": "sheriff odor square mistake huge skate mouse shoot purity weapon proof stuff correct concert blanket neck own shift clay mistake air viable stick group", + "key_derivation_version": "2", + "account_key": { + "object": "account_key", + "view_private_key": "0a20be48e147741246f09adb195b110c4ec39302778c4554cd3c9ff877f8392ce605", + "spend_private_key": "0a201f33b194e13176341b4e696b70be5ba5c4e0021f5a79664ab9a8b128f0d6d40d", + "fog_report_url": "", + "fog_report_id": "", + "fog_authority_spki": "" + } +} +``` diff --git a/docs/v2/accounts/account/README.md b/docs/v2/accounts/account/README.md new file mode 100644 index 000000000..b39587ee3 --- /dev/null +++ b/docs/v2/accounts/account/README.md @@ -0,0 +1,36 @@ +--- +description: >- + An account in the wallet. An account is associated with one AccountKey, + containing a View keypair and a Spend keypair. +--- + +# Account + +## Attributes + +| Name | Type | Description | +| :--- | :--- | :--- | +| `account_id` | string | The unique identifier for the account. | +| `name` | string | The display name for the account. | +| `main_address` | string | The b58 address code for the account's main address. The main address is determined by the seed subaddress. It is not assigned to a single recipient and should be considered a free-for-all address. | +| `next_subaddress_index` | string \(uint64\) | This index represents the next subaddress to be assigned as an address. This is useful information in case the account is imported elsewhere. | +| `first_block_index` | string \(uint64\) | Index of the first block when this account may have received funds. Defaults to 0 if not provided on account import | +| `next_block_index` | string \(uint64\) | Index of the next block this account needs to sync. | +| `fog_enabled` | boolean | A flag that indicates whether or not this account has a fog address. | +| `view_only` | boolean | A flag that indicates whether or not htis account is view only. | + +## Example + +```text +{ + "object": "account", + "account_id": "gdc3fd37f1903aec5a12b12a580eb837e14f87e5936f92a0af4794219f00691d", + "name": "I love MobileCoin", + "main_address": "8vbEtknX7zNtmN5epTYU95do3fDfsmecDu9kUbW66XGkKBX87n8AyqiiH9CMrueo5H7yiBEPXPoQHhEBLFHZJLcB2g7DZJ3tUZ9ArVgBu3a", + "next_subaddress_index": "3", + "first_block_index": "3500", + "recovery_mode": false, + "fog_enabled": false, + "view_only": false +} +``` \ No newline at end of file diff --git a/docs/v2/accounts/address/README.md b/docs/v2/accounts/address/README.md new file mode 100644 index 000000000..67a22c49d --- /dev/null +++ b/docs/v2/accounts/address/README.md @@ -0,0 +1,38 @@ +--- +description: >- + An Address is a public address created from the Account Key. An Address + contains a public View key and a public Spend key, as well as optional Fog + materials, if the account is enabled for mobile. +--- + +# Address + +Addresses in the Full-service Wallet are useful to help distinguish incoming transactions from each other. Due to MobileCoin's privacy properties, without using "subaddresses," the wallet would be unable to disambiguate which transactions were from which sender. By creating a new address for each contact, and sharing the address with only that contact, you can be certain that when you receive funds at that address, it is from the assigned contact. + +The way this works under the hood is by using the "subaddress index" to perform a cryptographic operation to generate a new subaddress. + +Important: If you receive funds at a subaddress that has not yet been assigned, you will not be able to spend the funds until you assign the address. We call those funds "orphaned" until they have been "recovered" by assigning the subaddress in the wallet to which they were sent. + +## Attributes + +| Name | Type | Description | +| :--- | :--- | :--- | +| `public_address` | string | A shareable B58-encoded string representing the address. | +| `account_id` | string | A unique identifier for the assigned associated account. | +| `metadata` | string | An arbitrary string attached to the object. | +| `subaddress_index` | string \(uint64\) | The assigned subaddress index on the associated account. | + +## Example + +```text +{ + "object": "address", + "public_address": "3P4GtGkp5UVBXUzBqirgj7QFetWn4PsFPsHBXbC6A8AXw1a9CMej969jneiN1qKcwdn6e1VtD64EruGVSFQ8wHk5xuBHndpV9WUGQ78vV7Z", + "account_id": "3407fbbc250799f5ce9089658380c5fe152403643a525f581f359917d8d59d52", + "metadata": "", + "subaddress_index": "2", + "offset": "7", + "limit": "6" +} +``` + diff --git a/docs/v2/accounts/balance/README.md b/docs/v2/accounts/balance/README.md new file mode 100644 index 000000000..d198de3d3 --- /dev/null +++ b/docs/v2/accounts/balance/README.md @@ -0,0 +1,32 @@ +--- +description: >- + The balance for a token, separated by status +--- + +# Balance + +## Attributes + +| Name | Type | Description | +| :--- | :--- | :--- | +| `max_spendable` | string \(uint64\) | Max spendable of this token for this account at the current `account_block_height`. | +| `unverified` | string \(uint64\) | Unverified value for this account at the current `account_block_height`. Unverified means it has a known subaddress but not a known key image \(In the case of view only accounts\) If the account is syncing, this value may change. | +| `unspent` | string \(uint64\) | Unspent value for this account at the current `account_block_height`. If the account is syncing, this value may change. | +| `pending` | string \(uint64\) | The pending value will clear once the ledger processes the outgoing TXOs. The `pending` will reflect the change. | +| `spent` | string \(uint64\) | This is the sum of all the TXOs in the wallet which have been spent. | +| `secreted` | string \(uint64\) | This is the sum of all the TXOs which have been created in the wallet for outgoing transactions. | +| `orphaned` | string \(uint64\) | The orphaned value represents the TXOs which were view-key matched, but which can not be spent until their subaddress index is recovered. | + +## Example + +```text +{ + "max_spendable": "1009999960000000000" + "unverified": "0", + "unspent": "110000000000000000", + "pending": "0", + "spent": "0", + "secreted": "0", + "orphaned": "0" +} +``` \ No newline at end of file diff --git a/docs/v2/api-endpoints/assign_address_for_account.md b/docs/v2/api-endpoints/assign_address_for_account.md new file mode 100644 index 000000000..368db06a2 --- /dev/null +++ b/docs/v2/api-endpoints/assign_address_for_account.md @@ -0,0 +1,58 @@ +--- +description: Assign an address to a given account. +--- + +# Assign Address For Account + +## [Request](../../../full-service/src/json_rpc/v2/api/request.rs#L40-L43) + +### Required Params +| Param | Purpose | Requirements | +| :--- | :--- | :--- | +| `account_id` | The account on which to perform this action. | The account must exist in the wallet. | + +### Optional Params +| Param | Purpose | Requirements | +| :--- | :--- | :--- | +| ​`metadata` | The metadata for this address. | String; can contain stringified JSON. | + +## [Response](../../../full-service/src/json_rpc/v2/api/response.rs#L41-L43) + +## Examples + +{% tabs %} +{% tab title="Request Body" %} +```text +{ + "method": "assign_address_for_account", + "params": { + "account_id": "a8c9c7acb96cf4ad9154eec9384c09f2c75a340b441924847fe5f60a41805bde", + "metadata": "For transactions from Carol" + }, + "jsonrpc": "2.0", + "id": 1 +} +``` +{% endtab %} + +{% tab title="Response" %} +```text +{ + "method": "assign_address_for_account", + "result": { + "address": { + "object": "address", + "public_address": "3P4GtGkp5UVBXUzBqirgj7QFetWn4PsFPsHBXbC6A8AXw1a9CMej969jneiN1qKcwdn6e1VtD64EruGVSFQ8wHk5xuBHndpV9WUGQ78vV7Z", + "account_id": "a8c9c7acb96cf4ad9154eec9384c09f2c75a340b441924847fe5f60a41805bde", + "metadata": "", + "subaddress_index": "2" + } + }, + "error": null, + "jsonrpc": "2.0", + "id": 1, +} +``` +{% endtab %} +{% endtabs %} + diff --git a/docs/v2/api-endpoints/build_and_submit_transaction.md b/docs/v2/api-endpoints/build_and_submit_transaction.md new file mode 100644 index 000000000..6acc861c8 --- /dev/null +++ b/docs/v2/api-endpoints/build_and_submit_transaction.md @@ -0,0 +1,137 @@ +--- +description: >- + Sending a transaction is a convenience method that first builds and then + submits a transaction. +--- + +# Build And Submit Transaction + +## [Request](../../../full-service/src/json_rpc/v2/api/request.rs#L44-L55) + +### Required Params +| Param | Type | Description | +| :--- | :--- | :--- | +| `account_id` | string | The account on which to perform this action. Must exist in the wallet. | + +### Optional Params +| Param | Type | Description | +| :--- | :--- | :--- | +| `addresses_and_amounts` | (string, [Amount](../../../full-service/src/json_rpc/v2/models/amount.rs))[] | An array of public addresses and Amount object tuples | +| `recipient_public_address` | string | b58-encoded public address bytes of the recipient for this transaction. | +| `amount` | [Amount](../../../full-service/src/json_rpc/v2/models/amount.rs) | The Amount to send in this transaction | +| `input_txo_ids` | string[]] | Specific TXOs to use as inputs to this transaction | +| `fee_value` | string(u64) | The fee value to submit with this transaction. If not provided, uses `MINIMUM_FEE` of the first outputs token_id, if available, or defaults to MOB | +| `fee_token_id` | string(u64) | The fee token to submit with this transaction. If not provided, uses token_id of first output, if available, or defaults to MOB | +| `tombstone_block` | string(u64) | The block after which this transaction expires. If not provided, uses `cur_height` + 10 | +| `max_spendable_value` | string(u64) | The maximum amount for an input TXO selected for this transaction | +| `comment` | string | Comment to annotate this transaction in the transaction log | + +##[Response](../../../full-service/src/json_rpc/v2/api/response.rs#L44-L47) + +## Example + +{% tabs %} +{% tab title="Request Body" %} +```text +{ + "method": "build_and_submit_transaction", + "params": { + "account_id": "a8c9c7acb96cf4ad9154eec9384c09f2c75a340b441924847fe5f60a41805bde", + "recipient_public_address": "CaE5bdbQxLG2BqAYAz84mhND79iBSs13ycQqN8oZKZtHdr6KNr1DzoX93c6LQWYHEi5b7YLiJXcTRzqhDFB563Kr1uxD6iwERFbw7KLWA6", + "amount": { "value": "42000000000000", "token_id": "0" } + }, + "jsonrpc": "2.0", + "id": 1 +} +``` +{% endtab %} + +{% tab title="Response" %} +```text +{ + "method": "build_and_submit_transaction", + "result": { + "transaction_log": { + "id": "ab447d73553309ccaf60aedc1eaa67b47f65bee504872e4358682d76df486a87", + "account_id": "a8c9c7acb96cf4ad9154eec9384c09f2c75a340b441924847fe5f60a41805bde", + "value_map": { + "0": "42000000000000" + }, + "fee_value": "10000000000", + "fee_token_id": "0", + "submitted_block_index": "152950", + "finalized_block_index": null, + "status": "pending", + "input_txos": [ + { + "id": "eb735cafa6d8b14a69361cc05cb3a5970752d27d1265a1ffdfd22c0171c2b20d", + "value": "50000000000", + "token_id": "0" + } + ], + "payload_txos": [ + { + "id": "fd39b4e740cb302edf5da89c22c20bea0e4408df40e31c1dbb2ec0055435861c", + "value": "30000000000", + "token_id": "0" + "recipient_public_address_b58": "vrewh94jfm43m430nmv2084j3k230j3mfm4i3mv39nffrwv43" + } + ], + "change_txos": [ + { + "id": "bcb45b4fab868324003631b6490a0bf46aaf37078a8d366b490437513c6786e4", + "value": "10000000000", + "token_id": "0" + "recipient_public_address_b58": "grewmvn3990435vm032492v43mgkvocdajcl2icas" + } + ], + "sent_time": "2021-02-28 01:42:28 UTC", + "comment": "", + "failure_code": null, + "failure_message": null + }, + "tx_proposal": { + "input_txos": [ + "tx_out_proto": "439f9843vmtbgdrv5...", + "value": "10000000000", + "token_id": "0", + "key_image": "dfj42v03mn40c353v53vvjyh5tr...", + ], + "payload_txos": [ + "tx_out_proto": "vr243095b89nvrimwec...", + "value": "5000000000", + "token_id": "0", + "recipient_public_address_b58": "ewvr3m49350c932emr3cew2...", + ], + "change_txos": [ + "tx_out_proto": "defvr34v5t4b6b...", + "value": "4060000000", + "token_id": "0", + "recipient_public_address_b58": "n23924mtb89vck31...", + ] + "fee": "40000000", + "fee_token_id": "0", + "tombstone_block_index": "152700", + "tx_proto": "328fi4n94902cmjinrievn49jg9439nvr3v..." + } + }, + "error": null, + "jsonrpc": "2.0", + "id": 1 +} +``` +{% endtab %} +{% endtabs %} + +{% hint style="warning" %} +`If an account is not fully-synced, you may see the following error message:` + +```text +{ + "error": "Connection(Operation { error: TransactionValidation(ContainsSpentKeyImage), total_delay: 0ns, tries: 1 })" +} +``` + +Call `check_balance` for the account, and note the `synced_blocks` value. If that value is less than the `local_block_height` value, then your TXOs may not all be updated to their spent status. +{% endhint %} + diff --git a/docs/v2/api-endpoints/build_transaction.md b/docs/v2/api-endpoints/build_transaction.md new file mode 100644 index 000000000..30271216a --- /dev/null +++ b/docs/v2/api-endpoints/build_transaction.md @@ -0,0 +1,97 @@ +--- +description: >- + Build a transaction to confirm its contents before submitting it to the + network. +--- + +# Build Transaction + +## [Request](../../../full-service/src/json_rpc/v2/api/request.rs#L56-L66) + +| Required Param | Purpose | Requirements | +| :--- | :--- | :--- | +| `account_id` | The account on which to perform this action | Account must exist in the wallet | + +| Optional Param | Purpose | Requirements | +| :--- | :--- | :--- | +| `addresses_and_amounts` | An array of public addresses and [Amounts](../../../full-service/src/json_rpc/v2/models/amount.rs) as a tuple | addresses are b58-encoded public addresses | +| `recipient_public_address` | The recipient for this transaction | b58-encoded public address bytes | +| `amount` | The [Amount](../../../full-service/src/json_rpc/v2/models/amount.rs) to send in this transaction | | +| `input_txo_ids` | Specific TXOs to use as inputs to this transaction | TXO IDs \(obtain from `get_txos_for_account`\) | +| `fee_value` | The fee value to submit with this transaction | If not provided, uses `MINIMUM_FEE` of the first outputs token_id, if available, or defaults to MOB | +| `fee_token_id` | The fee token_id to submit with this transaction | If not provided, uses token_id of first output, if available, or defaults to MOB | +| `tombstone_block` | The block after which this transaction expires | If not provided, uses `cur_height` + 10 | +| `max_spendable_value` | The maximum amount for an input TXO selected for this transaction | | + +## [Response](../../../full-service/src/json_rpc/v2/api/response.rs#L48-51) + +## Example + +{% tabs %} +{% tab title="Request Body" %} +``` +{ + "method": "build_transaction", + "params": { + "account_id": "a8c9c7acb96cf4ad9154eec9384c09f2c75a340b441924847fe5f60a41805bde", + "recipient_public_address": "CaE5bdbQxLG2BqAYAz84mhND79iBSs13ycQqN8oZKZtHdr6KNr1DzoX93c6LQWYHEi5b7YLiJXcTRzqhDFB563Kr1uxD6iwERFbw7KLWA6", + "amount": { "value": "42000000000000", "token_id": "0" }, + }, + "jsonrpc": "2.0", + "id": 1 +} +``` +{% endtab %} + +{% tab title="Response" %} +``` +{ + "method": "build_transaction", + "result": { + "transaction_log_id": "ab447d73553309ccaf60aedc1eaa67b47f65bee504872e4358682d76df486a87", + "tx_proposal": { + "input_txos": [ + "tx_out_proto": "439f9843vmtbgdrv5...", + "value": "10000000000", + "token_id": "0", + "key_image": "dfj42v03mn40c353v53vvjyh5tr...", + ], + "payload_txos": [ + "tx_out_proto": "vr243095b89nvrimwec...", + "value": "5000000000", + "token_id": "0", + "recipient_public_address_b58": "ewvr3m49350c932emr3cew2...", + ], + "change_txos": [ + "tx_out_proto": "defvr34v5t4b6b...", + "value": "4060000000", + "token_id": "0", + "recipient_public_address_b58": "n23924mtb89vck31...", + ] + "fee": "40000000", + "fee_token_id": "0", + "tombstone_block_index": "152700", + "tx_proto": "328fi4n94902cmjinrievn49jg9439nvr3v..." + } + } +} +``` +{% endtab %} +{% endtabs %} + +{% hint style="info" %} +Since the `tx_proposal`JSON object is quite large, you may wish to write the result to a file for use in the `submit_transaction` call, such as: + +``` +{ + "method": "build_transaction", + "params": { + "account_id": "a8c9c7acb96cf4ad9154eec9384c09f2c75a340b441924847fe5f60a41805bde", + "recipient_public_address": "CaE5bdbQxLG2BqAYAz84mhND79iBSs13ycQqN8oZKZtHdr6KNr1DzoX93c6LQWYHEi5b7YLiJXcTRzqhDFB563Kr1uxD6iwERFbw7KLWA6", + "value_pmob": "42000000000000" + }, + "jsonrpc": "2.0", + "id": 1 +} +``` +{% endhint %} diff --git a/docs/transactions/transaction/build_unsigned_transaction.md b/docs/v2/api-endpoints/build_unsigned_transaction.md similarity index 80% rename from docs/transactions/transaction/build_unsigned_transaction.md rename to docs/v2/api-endpoints/build_unsigned_transaction.md index 94c527c25..d6474937f 100644 --- a/docs/transactions/transaction/build_unsigned_transaction.md +++ b/docs/v2/api-endpoints/build_unsigned_transaction.md @@ -4,24 +4,25 @@ description: >- --- # Build Unsigned Transaction - account_id: String, - recipient_public_address: Option, - value_pmob: Option, - fee: Option, - tombstone_block: Option, -## Parameters +## [Request](../../../full-service/src/json_rpc/v2/api/request.rs#L67-L74) -| Required Param | Purpose | Requirements | -| -------------- | ------------------------------------------- | -------------------------------- | -| `account_id` | The account on which to perform this action | Account must exist in the wallet | +| Required Param | Purpose | Requirements | +| :--- | :--- | :--- | +| `account_id` | The account on which to perform this action | Account must exist in the wallet | -| Optional Param | Purpose | Requirements | -| -------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------ | -| `recipient_public_address` | The recipient for this transaction | b58-encoded public address bytes | -| `value_pmob` | The amount of MOB to send in this transaction | | -| `fee` | The fee amount to submit with this transaction | If not provided, uses `MINIMUM_FEE` = .01 MOB | -| `tombstone_block` | The block after which this transaction expires | If not provided, uses `cur_height` + 10 | | +| Optional Param | Purpose | Requirements | +| :--- | :--- | :--- | +| `addresses_and_amounts` | An array of public addresses and [Amounts](../../../full-service/src/json_rpc/v2/models/amount.rs) as a tuple | addresses are b58-encoded public addresses | +| `recipient_public_address` | The recipient for this transaction | b58-encoded public address bytes | +| `amount` | The [Amount](../../../full-service/src/json_rpc/v2/models/amount.rs) to send in this transaction | | +| `input_txo_ids` | Specific TXOs to use as inputs to this transaction | TXO IDs \(obtain from `get_txos_for_account`\) | +| `fee_value` | The fee value to submit with this transaction | If not provided, uses `MINIMUM_FEE` of the first outputs token_id, if available, or defaults to MOB | +| `fee_token_id` | The fee token_id to submit with this transaction | If not provided, uses token_id of first output, if available, or defaults to MOB | +| `tombstone_block` | The block after which this transaction expires | If not provided, uses `cur_height` + 10 | +| `max_spendable_value` | The maximum amount for an input TXO selected for this transaction | | + +## [Response](../../../full-service/src/json_rpc/v2/api/response.rs#L52-L56) ## Example @@ -33,7 +34,7 @@ description: >- "params": { "account_id": "a8c9c7acb96cf4ad9154eec9384c09f2c75a340b441924847fe5f60a41805bde", "recipient_public_address": "CaE5bdbQxLG2BqAYAz84mhND79iBSs13ycQqN8oZKZtHdr6KNr1DzoX93c6LQWYHEi5b7YLiJXcTRzqhDFB563Kr1uxD6iwERFbw7KLWA6", - "value_pmob": "42000000000000", + "value": ["42000000000000", "0"] }, "jsonrpc": "2.0", "id": 1 @@ -50,7 +51,7 @@ Since the `tx_proposal`JSON object is quite large, you may wish to write the res "params": { "account_id": "a8c9c7acb96cf4ad9154eec9384c09f2c75a340b441924847fe5f60a41805bde", "recipient_public_address": "CaE5bdbQxLG2BqAYAz84mhND79iBSs13ycQqN8oZKZtHdr6KNr1DzoX93c6LQWYHEi5b7YLiJXcTRzqhDFB563Kr1uxD6iwERFbw7KLWA6", - "value_pmob": "42000000000000" + "value": ["42000000000000", "0"] }, "jsonrpc": "2.0", "id": 1 diff --git a/docs/v2/api-endpoints/check_b58_type.md b/docs/v2/api-endpoints/check_b58_type.md new file mode 100644 index 000000000..b995588af --- /dev/null +++ b/docs/v2/api-endpoints/check_b58_type.md @@ -0,0 +1,50 @@ +--- +description: Check the type of the b58 code +--- + +# Check B58 Type + +## [Request](../../../full-service/src/json_rpc/v2/api/request.rs#L75) + +| Required Param | Purpose | Requirements | +| :--- | :--- | :--- | +| `b58_code` | The code to check | `String` | + +## [Response](../../../full-service/src/json_rpc/v2/api/response.rs#L58) + +## Example + +{% tabs %} +{% tab title="Request Body" %} +```text +{ + "method": "check_b58_type", + "params": { + "b58_code": "3Th9MSyznKV8VWAHAYoF8ZnVVunaTcMjRTnXvtzqeJPfAY8c7uQn71d6McViyzjLaREg7AppT7quDmBRG5E48csVhhzF4TEn1tw9Ekwr2hrq57A8cqR6sqpNC47mF7kHe" + }, + "jsonrpc": "2.0", + "id": 1 +} +``` +{% endtab %} + +{% tab title="Response" %} +```text +{ + "method": "check_b58_code", + "result": { + "b58_type": "PaymentRequest", + "data": { + "value": "1000000000000", + "public_address_b58": "4BfAQbahn9Bs8on7RrWkpargtVUiGNnLrbsmCVFyeqFHHATbwV4CRtjQvhhzpyrkbWBU2HqWK8Fg6boZ235YLEzkGJNFBEVGTKAnCN6vNGV", + "memo": "testing testing" + } + }, + "error": null, + "jsonrpc": "2.0", + "id": 1 +} +``` +{% endtab %} +{% endtabs %} + diff --git a/docs/v2/api-endpoints/check_receiver_receipt_status.md b/docs/v2/api-endpoints/check_receiver_receipt_status.md new file mode 100644 index 000000000..a6b51ac6f --- /dev/null +++ b/docs/v2/api-endpoints/check_receiver_receipt_status.md @@ -0,0 +1,81 @@ +--- +description: Check the status of a receiver receipt. +--- + +# Check Receiver Receipt Status + +## [Request](../../../full-service/src/json_rpc/v2/api/request.rs#L78) + +| Required Param | Purpose | Requirements | +| :--- | :--- | :--- | +| `address` | The account's public address. | Must be a valid account address. | +| `receiver_receipt` | The receipt whose status is being checked. | | + +## [Response](../../../full-service/src/json_rpc/v2/api/response.rs#L61) + +## Example + +{% tabs %} +{% tab title="Request Body" %} +```text +{ + "method": "check_receiver_receipt_status", + "params": { + "address": "3Dg4iFavKJScgCUeqb1VnET5ADmKjZgWz15fN7jfeCCWb72serxKE7fqz7htQvRirN4yeU2xxtcHRAN2zbF6V9n7FomDm69VX3FghvkDfpq", + "receiver_receipt": { + "object": "receiver_receipt", + "public_key": "0a20d2118a065192f11e228e0fce39e90a878b5aa628b7613a4556c193461ebd4f67", + "confirmation": "0a205e5ca2fa40f837d7aff6d37e9314329d21bad03d5fac2ec1fc844a09368c33e5", + "tombstone_block": "154512", + "amount": { + "object": "amount", + "commitment": "782c575ed7d893245d10d7dd49dcffc3515a7ed252bcade74e719a17d639092d", + "masked_value": "12052895925511073331" + } + } + }, + "jsonrpc": "2.0", + "id": 1 +} +``` +{% endtab %} + +{% tab title="Response" %} +```text +{ + "method": "check_receiver_receipt_status", + "result": { + "receipts_transaction_status": "TransactionSuccess", + "txo": { + "object": "txo", + "txo_id": "fff4cae55a74e5ce852b79c31576f4041d510c26e59fec178b3e45705c5b35a7", + "value_pmob": "2960000000000", + "received_block_index": "8094", + "spent_block_index": "8180", + "is_spent_recovered": false, + "received_account_id": "a4db032dcedc14e39608fe6f26deadf57e306e8c03823b52065724fb4d274c10", + "minted_account_id": null, + "account_status_map": { + "a4db032dcedc14e39608fe6f26deadf57e306e8c03823b52065724fb4d274c10": { + "txo_status": "spent", + "txo_type": "received" + } + }, + "target_key": "0a209eefc082a656a34fae5cec81044d1b13bd8963c411afa28aecfce4839fc9f74e", + "public_key": "0a20f03f9684e5420d5410fe732f121626352d45e4e799d725432a0c61fa1343ac51", + "e_fog_hint": "0a544944e7527b7f09322651b7242663edf17478fd1804aeea24838a35ad3c66d5194763642ae1c1e0cd2bbe2571a97a8c0fb49e346d2fd5262113e7333c7f012e61114bd32d335b1a8183be8e1865b0a10199b60100", + "subaddress_index": "0", + "assigned_subaddress": "3Dg4iFavKJScgCUeqb1VnET5ADmKjZgWz15fN7jfeCCWb72serxKE7fqz7htQvRirN4yeU2xxtcHRAN2zbF6V9n7FomDm69VX3FghvkDfpq", + "key_image": "0a205445b406012d26baebb51cbcaaaceb0d56387a67353637d07265f4e886f33419", + "confirmation": null, + "offset_count": 25 + } + }, + "error": null, + "jsonrpc": "2.0", + "id": 1 +} +``` +{% endtab %} +{% endtabs %} + diff --git a/docs/v2/api-endpoints/create_account.md b/docs/v2/api-endpoints/create_account.md new file mode 100644 index 000000000..5d94acd90 --- /dev/null +++ b/docs/v2/api-endpoints/create_account.md @@ -0,0 +1,53 @@ +--- +description: Create a new account in the wallet. +--- + +# Create Account + +## [Request](../../../full-service/src/json_rpc/v2/api/request.rs#L82) + +| Optional Param | Purpose | Requirements | +| :--- | :--- | :--- | +| `name` | A label for this account. | A label can have duplicates, but it is not recommended. | +| `fog_info` | The [Fog Info](../../../full-service/src/json_rpc/v2/models/account_key.rs#L67) to include in public addresses | | + +## [Response](../../../full-service/src/json_rpc/v2/api/response.rs#L65) + +{% tabs %} +{% tab title="Request Body" %} +```text +{ + "method": "create_account", + "params": { + "name": "Alice" + }, + "jsonrpc": "2.0", + "id": 1 +} +``` +{% endtab %} + +{% tab title="Response" %} +```text +{ + "method": "create_account", + "result": { + "account": { + "object": "account", + "account_id": "3407fbbc250799f5ce9089658380c5fe152403643a525f581f359917d8d59d52", + "name": "Alice", + "main_address": "4bgkVAH1hs55dwLTGVpZER8ZayhqXbYqfuyisoRrmQPXoWcYQ3SQRTjsAytCiAgk21CRrVNysVw5qwzweURzDK9HL3rGXFmAAahb364kYe3", + "next_subaddress_index": "2", + "first_block_index": "3500", + "recovery_mode": false + } + }, + "error": null, + "jsonrpc": "2.0", + "id": 1, + } +} +``` +{% endtab %} +{% endtabs %} + diff --git a/docs/v2/api-endpoints/create_payment_request.md b/docs/v2/api-endpoints/create_payment_request.md new file mode 100644 index 000000000..87e4a864d --- /dev/null +++ b/docs/v2/api-endpoints/create_payment_request.md @@ -0,0 +1,54 @@ +--- +description: Create a payment request b58 code to give to someone else +--- + +# Create Payment Request + +## [Request](../../../full-service/src/json_rpc/v2/api/request.rs#L86) + +| Required Param | Purpose | Requirements | +| :--- | :--- | :--- | +| `account_id` | The account on which to perform this action. | Account must exist in the wallet. | +| `amount_pmob` | The amount of pMOB to send in this transaction. | `u64` | + +| Optional Param | Purpose | Requirements | +| :--- | :--- | :--- | +| `subaddress_index` | The subaddress index on the account to generate the request with | `i64` | +| `memo` | Memo for the payment request | | + +## [Response](../../../full-service/src/json_rpc/v2/api/response.rs#L41) + +## Example + +{% tabs %} +{% tab title="Request Body" %} +```text +{ + "method": "create_payment_request", + "params": { + "account_id": "a8c9c7acb96cf4ad9154eec9384c09f2c75a340b441924847fe5f60a41805bde", + "amount_pmob": 42000000000000, + "subaddress_index": 4, + "memo": "Payment for dinner with family" + }, + "jsonrpc": "2.0", + "id": 1 +} +``` +{% endtab %} + +{% tab title="Response" %} +```text +{ + "method": "create_payment_request", + "result": { + "payment_request_b58": "3Th9MSyznKV8VWAHAYoF8ZnVVunaTcMjRTnXvtzqeJPfAY8c7uQn71d6McViyzjLaREg7AppT7quDmBRG5E48csVhhzF4TEn1tw9Ekwr2hrq57A8cqR6sqpNC47mF7kHe", + }, + "error": null, + "jsonrpc": "2.0", + "id": 1 +} +``` +{% endtab %} +{% endtabs %} + diff --git a/docs/v2/api-endpoints/create_receiver_receipts.md b/docs/v2/api-endpoints/create_receiver_receipts.md new file mode 100644 index 000000000..60de47915 --- /dev/null +++ b/docs/v2/api-endpoints/create_receiver_receipts.md @@ -0,0 +1,82 @@ +--- +description: >- + After building a tx_proposal, you can get the receipts for that transaction + and provide it to the recipient so they can poll for the transaction status. +--- + +# Create Receiver Receipts + +## [Request](../../../full-service/src/json_rpc/v2/api/request.rs#L40) + +| Required Param | Purpose | Requirements | +| :--- | :--- | :--- | +| `tx_proposal` | | | + +## [Response](../../../full-service/src/json_rpc/v2/api/response.rs#L41) + +## Example + +{% tabs %} +{% tab title="Request Body" %} +```text +{ + "method": "create_receiver_receipts", + "params": { + "tx_proposal": { + "input_txos": [ + "tx_out_proto": "439f9843vmtbgdrv5...", + "value": "10000000000", + "token_id": "0", + "key_image": "dfj42v03mn40c353v53vvjyh5tr...", + ], + "payload_txos": [ + "tx_out_proto": "vr243095b89nvrimwec...", + "value": "5000000000", + "token_id": "0", + "recipient_public_address_b58": "ewvr3m49350c932emr3cew2...", + ], + "change_txos": [ + "tx_out_proto": "defvr34v5t4b6b...", + "value": "4060000000", + "token_id": "0", + "recipient_public_address_b58": "n23924mtb89vck31...", + ] + "fee": "40000000", + "fee_token_id": "0", + "tombstone_block_index": "152700", + "tx_proto": "328fi4n94902cmjinrievn49jg9439nvr3v..." + } + }, + "jsonrpc": "2.0", + "id": 1 +} +``` +{% endtab %} + +{% tab title="Response" %} +```text +{ + "method": "create_receiver_receipts", + "result": { + "receiver_receipts": [ + { + "object": "receiver_receipt", + "public_key": "0a20d2118a065192f11e228e0fce39e90a878b5aa628b7613a4556c193461ebd4f67", + "confirmation": "0a205e5ca2fa40f837d7aff6d37e9314329d21bad03d5fac2ec1fc844a09368c33e5", + "tombstone_block": "154512", + "amount": { + "object": "amount", + "commitment": "782c575ed7d893245d10d7dd49dcffc3515a7ed252bcade74e719a17d639092d", + "masked_value": "12052895925511073331" + } + } + ] + }, + "error": null, + "jsonrpc": "2.0", + "id": 1 +} +``` +{% endtab %} +{% endtabs %} + diff --git a/docs/v2/api-endpoints/create_view_only_account_import_request.md b/docs/v2/api-endpoints/create_view_only_account_import_request.md new file mode 100644 index 000000000..dce763f6d --- /dev/null +++ b/docs/v2/api-endpoints/create_view_only_account_import_request.md @@ -0,0 +1,53 @@ +# Export View Only Account Import Request + +## [Request](../../../full-service/src/json_rpc/v2/api/request.rs#L40) + +| Required Param | Purpose | Requirements | +| -------------- | -------------------------------------------- | --------------------------------- | +| `account_id` | The account on which to perform this action. | Account must exist in the wallet. | + +## [Response](../../../full-service/src/json_rpc/v2/api/response.rs#L41) + +## Example + +{% tabs %} +{% tab title="Request Body" %} +``` +{ + "method": "export_view_only_account_import_request", + "params": { + "account_id": "6d95067c5fcc0dd7bbcdd42d49cc3571fe1bb2597a9c397c75b7280eca534208" + }, + "jsonrpc": "2.0", + "id": 1 +} +``` +{% endtab %} + +{% tab title="Response" %} +``` +{ + "method": "export_view_only_account_import_request", + "result": { + "json_rpc_request": { + "method": "import_view_only_account", + "params": { + "account": { + "view_private_key": "6ed6b79004032fcfcfa65fa7a307dd004b8ec4ed77660d36d44b67452f62b470", + "spend_public_key": "fcewc434g5353v535323f43f43f43g5342v3b67n8576453f4dcv56b77n857b46", + "name": "Coins for cats", + "first_block_index": "3500", + "next_block_index": "4000", + } + }, + "jsonrpc": "2.0", + "id": 1 + } + }, + "jsonrpc": "2.0", + "id": 1 +} +``` +{% endtab %} +{% endtabs %} + diff --git a/docs/view-only-accounts/syncing/create_view_only_account_sync_request.md b/docs/v2/api-endpoints/create_view_only_account_sync_request.md similarity index 86% rename from docs/view-only-accounts/syncing/create_view_only_account_sync_request.md rename to docs/v2/api-endpoints/create_view_only_account_sync_request.md index 32843a8a2..9df3c089b 100644 --- a/docs/view-only-accounts/syncing/create_view_only_account_sync_request.md +++ b/docs/v2/api-endpoints/create_view_only_account_sync_request.md @@ -1,11 +1,13 @@ -# Create Account Sync Request +# Create View Only Account Sync Request -## Parameters +## [Request](../../../full-service/src/json_rpc/v2/api/request.rs#L40) | Required Param | Purpose | Requirements | | :--- | :--- | :--- | | `account_id` | The account on which to perform this action. | Account must exist in the wallet as a view only account. | +## [Response](../../../full-service/src/json_rpc/v2/api/response.rs#L41) + {% tabs %} {% tab title="Request" %} ``` diff --git a/docs/v2/api-endpoints/export_account_secrets.md b/docs/v2/api-endpoints/export_account_secrets.md new file mode 100644 index 000000000..de21fa49c --- /dev/null +++ b/docs/v2/api-endpoints/export_account_secrets.md @@ -0,0 +1,67 @@ +--- +description: >- + Exporting the secret mnemonic an account is the only way to recover it when + lost. +--- + +# Export Account Secrets + +## [Request](../../../full-service/src/json_rpc/v2/api/request.rs#L40) + +| Required Param | Purpose | Requirements | +| :--- | :--- | :--- | +| `account_id` | The account on which to perform this action. | Account must exist in the wallet. | + +## [Response](../../../full-service/src/json_rpc/v2/api/response.rs#L41) + +## Example + +{% tabs %} +{% tab title="Request Body" %} +```text +{ + "method": "export_account_secrets", + "params": { + "account_id": "3407fbbc250799f5ce9089658380c5fe152403643a525f581f359917d8d59d52" + }, + "jsonrpc": "2.0", + "id": 1 +} +``` +{% endtab %} + +{% tab title="Response" %} +```text +{ + "method": "export_account_secrets", + "result": { + "account_secrets": { + "object": "account_secrets", + "account_id": "3407fbbc250799f5ce9089658380c5fe152403643a525f581f359917d8d59d52", + "entropy": "c0b285cc589447c7d47f3yfdc466e7e946753fd412337bfc1a7008f0184b0479", + "mnemonic": "sheriff odor square mistake huge skate mouse shoot purity weapon proof stuff correct concert blanket neck own shift clay mistake air viable stick group", + "key_derivation_version": "2", + "account_key": { + "object": "account_key", + "view_private_key": "0a20be48e147741246f09adb195b110c4ec39302778c4554cd3c9ff877f8392ce605", + "spend_private_key": "0a201f33b194e13176341b4e696b70be5ba5c4e0021f5a79664ab9a8b128f0d6d40d", + "fog_report_url": "", + "fog_report_id": "", + "fog_authority_spki": "" + } + } + }, + "error": null, + "jsonrpc": "2.0", + "id": 1, +} +``` +{% endtab %} +{% endtabs %} + +## Outputs + +If the account was generated using version 1 of the key derivation, entropy will be provided as a hex-encoded string. + +If the account was generated using version 2 of the key derivation, mnemonic will be provided as a 24-word mnemonic string. + diff --git a/docs/v2/api-endpoints/get_account_status.md b/docs/v2/api-endpoints/get_account_status.md new file mode 100644 index 000000000..1a0e4a667 --- /dev/null +++ b/docs/v2/api-endpoints/get_account_status.md @@ -0,0 +1,67 @@ +--- +description: >- + Get the current status of a given account. The account status includes both + the account object and the balance object. +--- + +# Get Account Status + +## [Request](../../../full-service/src/json_rpc/v2/api/request.rs#L40) + +| Required Param | Purpose | Requirements | +| :--- | :--- | :--- | +| `account_id` | The account on which to perform this action. | Account must exist in the wallet. | + +## [Response](../../../full-service/src/json_rpc/v2/api/response.rs#L41) + +## Example + +{% tabs %} +{% tab title="Request Body" %} +```text +{ + "method": "get_account_status", + "params": { + "account_id": "a8c9c7acb96cf4ad9154eec9384c09f2c75a340b441924847fe5f60a41805bde" + }, + "jsonrpc": "2.0", + "id": 1 +} +``` +{% endtab %} + +{% tab title="Response" %} +```text +{ + "method": "get_account_status", + "result": { + "account": { + "account_id": "b0be5377a2f45b1573586ed530b2901a559d9952ea8a02f8c2dbb033a935ac17", + "main_address": "7JvajhkAZYGmrpCY7ZpEiXRK5yW1ooTV7EWfDNu3Eyt572mH1wNb37BWiU6JqRUvgopPqSVZRexhXXpjF3wqLQR7HaJrcdbHmULujgFmzav", + "name": "Brady", + "next_subaddress_index": "2", + "first_block_index": "3500", + "object": "account", + "recovery_mode": false + }, + "network_block_height": "2", + "local_block_height": "2", + "balance_per_token": { + "0": { + "orphaned": "0", + "pending": "2040016523222112112", + "secreted": "204273415999956272", + "spent": "0", + "unspent": "51080511222211091", + "unverified": "0" + } + } + }, + "error": null, + "jsonrpc": "2.0", + "id": 1, +} +``` +{% endtab %} +{% endtabs %} + diff --git a/docs/v2/api-endpoints/get_accounts.md b/docs/v2/api-endpoints/get_accounts.md new file mode 100644 index 000000000..0a444b5cf --- /dev/null +++ b/docs/v2/api-endpoints/get_accounts.md @@ -0,0 +1,69 @@ +--- +description: Get the details of all accounts in a given wallet. +--- + +# Get Accounts + +## [Request](../../../full-service/src/json_rpc/v2/api/request.rs#L40) + +| Optional Param | Purpose | Requirements | +| :--- | :--- | :--- | +| `offset` | | | +| `limit` | | | + +## [Response](../../../full-service/src/json_rpc/v2/api/response.rs#L41) + +## Example + +{% tabs %} +{% tab title="Request Body" %} +```text +{ + "method": "get_accounts", + "jsonrpc": "2.0", + "id": 1, + "params": {} +} +``` +{% endtab %} + +{% tab title="Response" %} +```text +{ + "method": "get_accounts", + "result": { + "account_ids": [ + "3407fbbc250799f5ce9089658380c5fe152403643a525f581f359917d8d59d52", + "b6c9f6f779372ae25e93d68a79d725d71f3767d1bfd1c5fe155f948a2cc5c0a0" + ], + "account_map": { + "3407fbbc250799f5ce9089658380c5fe152403643a525f581f359917d8d59d52": { + "account_id": "3407fbbc250799f5ce9089658380c5fe152403643a525f581f359917d8d59d52", + "key_derivation_version:": "1", + "main_address": "4bgkVAH1hs55dwLTGVpZER8ZayhqXbYqfuyisoRrmQPXoWcYQ3SQRTjsAytCiAgk21CRrVNysVw5qwzweURzDK9HL3rGXFmAAahb364kYe3", + "name": "Alice", + "next_subaddress_index": "2", + "first_block_index": "3500", + "object": "account", + "recovery_mode": false + }, + "b6c9f6f779372ae25e93d68a79d725d71f3767d1bfd1c5fe155f948a2cc5c0a0": { + "account_id": "b6c9f6f779372ae25e93d68a79d725d71f3767d1bfd1c5fe155f948a2cc5c0a0", + "key_derivation_version:": "2", + "main_address": "7EqduSDpM1R5AfQejbjAqFxpuCoh6zJECtvJB9AZFwjK13dCzZgYbyfLf4TfHcE8LVPjzDdpcxYLkdMBh694mHfftJmsFZuz6xUeRtmsUdc", + "name": "Alice", + "next_subaddress_index": "2", + "first_block_index": "3500", + "object": "account", + "recovery_mode": false + } + } + }, + "error": null, + "jsonrpc": "2.0", + "id": 1, +} +``` +{% endtab %} +{% endtabs %} + diff --git a/docs/v2/api-endpoints/get_address_for_account.md b/docs/v2/api-endpoints/get_address_for_account.md new file mode 100644 index 000000000..6cdf807c8 --- /dev/null +++ b/docs/v2/api-endpoints/get_address_for_account.md @@ -0,0 +1,53 @@ +--- +description: Get an assigned address by index for an account. +--- + +# Get Address For Account At Index + +## [Request](../../../full-service/src/json_rpc/v2/api/request.rs#L40) + +| Required Param | Purpose | Requirements | +| :--- | :--- | :--- | +| `account_id` | The account on which to perform this action. | The account must exist in the wallet. | +| `index` | The subaddress index to lookup | The address must have already been assigned. | + +## [Response](../../../full-service/src/json_rpc/v2/api/response.rs#L41) + +## Example + +{% tabs %} +{% tab title="Request Body" %} +```text +{ + "method": "get_address_for_account_at_index", + "params": { + "account_id": "a8c9c7acb96cf4ad9154eec9384c09f2c75a340b441924847fe5f60a41805bde", + "index": 1 + }, + "jsonrpc": "2.0", + "id": 1 +} +``` +{% endtab %} + +{% tab title="Response" %} +```text +{ + "method": "get_address_for_account_at_index", + "result": { + "address": { + "object": "address", + "public_address": "4bgkVAH1hs55dwLTGVpZER8ZayhqXbYqfuyisoRrmQPXoWcYQ3SQRTjsAytCiAgk21CRrVNysVw5qwzweURzDK9HL3rGXFmAAahb364kYe3", + "account_id": "3407fbbc250799f5ce9089658380c5fe152403643a525f581f359917d8d59d52", + "metadata": "Main", + "subaddress_index": "0" + } + }, + "error": null, + "jsonrpc": "2.0", + "id": 1, +} +``` +{% endtab %} +{% endtabs %} + diff --git a/docs/v2/api-endpoints/get_address_status.md b/docs/v2/api-endpoints/get_address_status.md new file mode 100644 index 000000000..746029f31 --- /dev/null +++ b/docs/v2/api-endpoints/get_address_status.md @@ -0,0 +1,69 @@ +--- +description: Get the current balance for a given address. The response will have a map of the total values for each token_id that is present at that address. If no tokens are found at that address, the map will be empty. Orphaned will always be 0 for addresses. +--- + +# Get Address Status + +## [Request](../../../full-service/src/json_rpc/v2/api/request.rs#L40) +| Required Param | Purpose | Requirements | +| :--- | :--- | :--- | +| `address` | The address on which to perform this action. | Address must be assigned for an account in the wallet. | + +| Optional Param | Purpose | Requirements | +| :--- | :--- | :--- | +| `min_block_index` | The minimum block index to filter on txos received | | +| `max_block_index` | The maximum block index to filter on txos received | | + +## [Response](../../../full-service/src/json_rpc/v2/api/response.rs#L41) + +{% tabs %} +{% tab title="Request Body" %} +```text +{ + "method": "get_balance_for_address", + "params": { + "address": "3P4GtGkp5UVBXUzBqirgj7QFetWn4PsFPsHBXbC6A8AXw1a9CMej969jneiN1qKcwdn6e1VtD64EruGVSFQ8wHk5xuBHndpV9WUGQ78vV7Z" + }, + "jsonrpc": "2.0", + "api_version": "2", + "id": 1 +} +``` +{% endtab %} + +{% tab title="Response" %} +```text +{ + "method": "get_balance_for_address", + "result": { + "account_block_height": "154320", + "network_block_height": "154320", + "local_block_height": "154320", + "balance_per_token": { + "0": { + "unverified": "0000000000" + "unspent": "110000000000000000", + "pending": "0", + "spent": "0", + "secreted": "0", + "orphaned": "0" + }, + "1": { + "unverified": "0000000000" + "unspent": "1100000000", + "pending": "0", + "spent": "0", + "secreted": "0", + "orphaned": "0" + } + } + }, + "error": null, + "jsonrpc": "2.0", + "id": 1, + "api_version": "2" +} +``` +{% endtab %} +{% endtabs %} + diff --git a/docs/v2/api-endpoints/get_addresses.md b/docs/v2/api-endpoints/get_addresses.md new file mode 100644 index 000000000..eaa2fd6bb --- /dev/null +++ b/docs/v2/api-endpoints/get_addresses.md @@ -0,0 +1,59 @@ +--- +description: Get assigned addresses for an account. +--- + +# Get Addresses + +## [Request](../../../full-service/src/json_rpc/v2/api/request.rs#L40) + +| Optional Param | Purpose | Requirements | +| :--- | :--- | :--- | +| `account_id` | The account on which to perform this action. | The account must exist in the wallet. | +| `offset` | The pagination offset. Results start at the offset index | | +| `limit` | Limit for the number of results | | + +## [Response](../../../full-service/src/json_rpc/v2/api/response.rs#L41) + +## Example + +{% tabs %} +{% tab title="Request Body" %} +```text +{ + "method": "get_addresses", + "params": { + "account_id": "b59b3d0efd6840ace19cdc258f035cc87e6a63b6c24498763c478c417c1f44ca", + "offset": 1, + "limit": 1 + }, + "jsonrpc": "2.0", + "id": 1 +} +``` +{% endtab %} + +{% tab title="Response" %} +```text +{ + "method": "get_addresses", + "result": { + "public_addresses": [ + "7RvvDmRa9CuB5Uf1aDeyKuyhjKtQhxHroAuDh8NFuwfRdQd1QvAhgA8E6Tg34nRo4sM6B1SbPEC8ffz86oYfDKziBw7xYVPKzZ4dvL8p961" + ], + "address_map": { + "7RvvDmRa9CuB5Uf1aDeyKuyhjKtQhxHroAuDh8NFuwfRdQd1QvAhgA8E6Tg34nRo4sM6B1SbPEC8ffz86oYfDKziBw7xYVPKzZ4dvL8p961": { + "object": "address", + "public_address": "7RvvDmRa9CuB5Uf1aDeyKuyhjKtQhxHroAuDh8NFuwfRdQd1QvAhgA8E6Tg34nRo4sM6B1SbPEC8ffz86oYfDKziBw7xYVPKzZ4dvL8p961", + "account_id": "b59b3d0efd6840ace19cdc258f035cc87e6a63b6c24498763c478c417c1f44ca", + "metadata": "Change", + "subaddress_index": "1" + } + } + }, + "jsonrpc": "2.0", + "id": 1 +} +``` +{% endtab %} +{% endtabs %} + diff --git a/docs/v2/api-endpoints/get_block.md b/docs/v2/api-endpoints/get_block.md new file mode 100644 index 000000000..d0cead4de --- /dev/null +++ b/docs/v2/api-endpoints/get_block.md @@ -0,0 +1,94 @@ +--- +description: Get the JSON representation of the "Block" object in the ledger. +--- + +# Get Block + +## [Request](../../../full-service/src/json_rpc/v2/api/request.rs#L40) + +| Required Param | Purpose | Requirements | +| :--- | :--- | :--- | +| `block_index` | The block on which to perform this action. | Block must exist in the wallet. | + +## [Response](../../../full-service/src/json_rpc/v2/api/response.rs#L41) + +## Example + +{% tabs %} +{% tab title="Body Request" %} +```text +{ + "method": "get_block", + "params": { + "block_index": "3204", + }, + "jsonrpc": "2.0", + "id": 1 +} +``` +{% endtab %} + +{% tab title="Response" %} +```text +{ + "method": "get_block", + "result": { + "block": { + "id": "7cb35994cfcddf6c1e807b178d97d26b1426b0c5e035870c1f847194d2974051", + "version": "0", + "parent_id": "66c93f21731a852b1c3779362b86f60c2df4569a0f3192c2606aab80abf97720", + "index": "3204", + "cumulative_txo_count": "9630", + "root_element": { + "range": { + "from": "0", + "to": "16383" + }, + "hash": "13578eb43225da9ccac84054ec53a35882c11ffa91fd3fddf83a3695e3da2d34" + }, + "contents_hash": "e6859fe30de1bdaca04da1f48c672a7efe2a20dc2f92f274beecd95335057f40" + }, + "block_contents": { + "key_images": [ + "0a2014af115d02757996dc9ffb9503147a1df116944bbcb7b7485d004207c3ed5148", + "0a20f2161a1f709490ba7916f2f9b1240a8dd6ae373cf53db830bd0cf72784517733" + ], + "outputs": [ + { + "amount": { + "commitment": "3aed988182291e60592193834c1785cc461770c88a923e92c46b5e0c739f7328", + "masked_value": "11758756470468044129" + }, + "target_key": "08f63701a50e70dfe5f83680e417f20da0d29cfcf5a06487dea6d9b610d6531c", + "public_key": "56fb0ba834264fff19f4228423c16f95aa48524f027e94ec95c4370ab92f4219", + "e_fog_hint": "5b00649093c46e1f47447810d9b57885ce6d1046582f800205c9a823aec01c30dcb09e3f808ece5701b05976209d2290ba10b049e14955ab9904e9aedd5ad6957234ebc0e56a7e23eb5f1c80699a2764334c0100" + }, + { + "amount": { + "commitment": "c6fe77aaf3718ee614514cb127628d067c72d7836ebdf0cf0aeb36e465b48033", + "masked_value": "2884679206729723147" + }, + "target_key": "343e3fd460a447e3576bdd4e7c461811693e3352da9f7db9c88ee5246f5c5a28", + "public_key": "805daef5b1d8363c1af964f2aeb2b42f1960c780a514c3c2ead8d07230ca9303", + "e_fog_hint": "cf544d5c7f78af198ad0cfe6ebb270d1342f9e2e9ceeadadd8c6a5a216f21c7f8989b0580d7cd73a7e32a7a4f48ad192cf9987fe4ffe734bbcf64e18fbb4f787fd62030c29274b576c68e85441b23374edb00100" + }, + { + "amount": { + "commitment": "e81a0fd37fec7efa411bcf2671714d2f9653cd5de8adf0d981f807d63938716e", + "masked_value": "8464075929622445691" + }, + "target_key": "a48206113129e42d8c5cc1122cd76e0a06985f666f504e144e3d45d45095de5e", + "public_key": "b633c32f91aea42de6b6cd88dc0f05af47861db24823cc485430f9bbb7a35b22", + "e_fog_hint": "000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" + } + ] + } + }, + "error": null, + "jsonrpc": "2.0", + "id": 1 +} +``` +{% endtab %} +{% endtabs %} + diff --git a/docs/v2/api-endpoints/get_confirmations.md b/docs/v2/api-endpoints/get_confirmations.md new file mode 100644 index 000000000..49083cb0d --- /dev/null +++ b/docs/v2/api-endpoints/get_confirmations.md @@ -0,0 +1,57 @@ +--- +description: >- + A TXO constructed by this wallet will contain a confirmation number, which can + be shared with the recipient to verify the association between the sender and + this TXO. +--- + +# Get Confirmations + +## [Request](../../../full-service/src/json_rpc/v2/api/request.rs#L40) + +| Param | Purpose | Requirements | +| :--- | :--- | :--- | +| `transaction_log_id` | The transaction log ID for which to get confirmation numbers. | The transaction log must exist in the wallet. | + +## [Response](../../../full-service/src/json_rpc/v2/api/response.rs#L41) + +## Example + +When calling `get_confirmations` for a transaction, only the confirmation numbers for the `output_txo_ids` are returned. + +{% tabs %} +{% tab title="Request Body" %} +```text +{ + "method": "get_confirmations", + "params": { + "transaction_log_id": "0db5ac892ed796bb11e52d3842f83c05f4993f2f9d7da5fc9f40c8628c7859a4" + }, + "jsonrpc": "2.0", + "id": 1 +} +``` +{% endtab %} + +{% tab title="Response" %} +```text +{ + "method": "get_confirmations", + "result": { + "confirmations": [ + { + "object": "confirmation", + "txo_id": "9e0de29bfee9a391e520a0b9411a91f094a454ebc70122bdc0e36889ab59d466", + "txo_index": "458865", + "confirmation": "0a20faca10509c32845041e49e009ddc4e35b61e7982a11aced50493b4b8aaab7a1f" + } + ] + }, + "error": null, + "jsonrpc": "2.0", + "id": 1, +} +``` +{% endtab %} +{% endtabs %} + diff --git a/docs/v2/api-endpoints/get_mc_protocol_transaction.md b/docs/v2/api-endpoints/get_mc_protocol_transaction.md new file mode 100644 index 000000000..f93067a1b --- /dev/null +++ b/docs/v2/api-endpoints/get_mc_protocol_transaction.md @@ -0,0 +1,45 @@ +--- +description: 'Get the transaction protocol for MobileCoin' +--- + +# Get MobileCoin Protocol Transaction + +## [Request](../../../full-service/src/json_rpc/v2/api/request.rs#L40) + +| Required Param | Purpose | Requirements | +| :--- | :--- | :--- | +| `transaction_log_id` | The id of the transaction log. | Must be a valid id for a transaction. | + +## [Response](../../../full-service/src/json_rpc/v2/api/response.rs#L41) + +## Example + +{% tabs %} +{% tab title="Request Body" %} +```text +{ + "method": "get_mc_protocol_transaction", + "params": { + "transaction_log_id": "4b4fd11738c03bf5179781aeb27d725002fb67d8a99992920d3654ac00ee1a2c" + }, + "jsonrpc": "2.0", + "id": 1 +} +``` +{% endtab %} + +{% tab title="Response" %} +```text +{ + "method": "get_mc_protocol_transaction", + "result": { + "transaction": ... + }, + "error": null, + "jsonrpc": "2.0", + "id": 1 +} +``` +{% endtab %} +{% endtabs %} + diff --git a/docs/v2/api-endpoints/get_mc_protocol_txo.md b/docs/v2/api-endpoints/get_mc_protocol_txo.md new file mode 100644 index 000000000..dc30fb50c --- /dev/null +++ b/docs/v2/api-endpoints/get_mc_protocol_txo.md @@ -0,0 +1,67 @@ +--- +description: 'Get the MobileCoin transaction TXO' +--- + +# Get MobileCoin Protocol TXO + +## [Request](../../../full-service/src/json_rpc/v2/api/request.rs#L40) + +| Required Param | Purpose | Requirements | +| :--- | :--- | :--- | +| `txo_id` | The id of the TXO. | Must be a valid id for a TXO. | + +## [Response](../../../full-service/src/json_rpc/v2/api/response.rs#L41) + +## Example + +{% tabs %} +{% tab title="Request Body" %} +```text +{ + "method": "get_mc_protocol_txo", + "params": { + "txo_id": "fff4cae55a74e5ce852b79c31576f4041d510c26e59fec178b3e45705c5b35a7" + }, + "jsonrpc": "2.0", + "id": 1 +} +``` +{% endtab %} + +{% tab title="Response" %} +```text +{ + "method": "get_mc_protocol_txo", + "result": { + "txo": { + "object": "txo", + "txo_id": "fff4cae55a74e5ce852b79c31576f4041d510c26e59fec178b3e45705c5b35a7", + "value_pmob": "2960000000000", + "received_block_index": "8094", + "spent_block_index": "8180", + "is_spent_recovered": false, + "received_account_id": "a4db032dcedc14e39608fe6f26deadf57e306e8c03823b52065724fb4d274c10", + "minted_account_id": null, + "account_status_map": { + "a4db032dcedc14e39608fe6f26deadf57e306e8c03823b52065724fb4d274c10": { + "txo_status": "spent", + "txo_type": "received" + } + }, + "target_key": "0a209eefc082a656a34fae5cec81044d1b13bd8963c411afa28aecfce4839fc9f74e", + "public_key": "0a20f03f9684e5420d5410fe732f121626352d45e4e799d725432a0c61fa1343ac51", + "e_fog_hint": "0a544944e7527b7f09322651b7242663edf17478fd1804aeea24838a35ad3c66d5194763642ae1c1e0cd2bbe2571a97a8c0fb49e346d2fd5262113e7333c7f012e61114bd32d335b1a8183be8e1865b0a10199b60100", + "subaddress_index": "0", + "assigned_subaddress": "7BeDc5jpZu72AuNavumc8qo8CRJijtQ7QJXyPo9dpnqULaPhe6GdaDNF7cjxkTrDfTcfMgWVgDzKzbvTTwp32KQ78qpx7bUnPYxAgy92caJ", + "key_image": "0a205445b406012d26baebb51cbcaaaceb0d56387a67353637d07265f4e886f33419", + "confirmation": null + } + }, + "error": null, + "jsonrpc": "2.0", + "id": 1 +} +``` +{% endtab %} +{% endtabs %} + diff --git a/docs/v2/api-endpoints/get_network_status.md b/docs/v2/api-endpoints/get_network_status.md new file mode 100644 index 000000000..3ed8fbf57 --- /dev/null +++ b/docs/v2/api-endpoints/get_network_status.md @@ -0,0 +1,47 @@ +--- +description: 'Get the current status of the network.' +--- + +# Get The Network Status + +## [Request](../../../full-service/src/json_rpc/v2/api/request.rs#L40) + +## [Response](../../../full-service/src/json_rpc/v2/api/response.rs#L41) + +## Example + +{% tabs %} +{% tab title="Request Body" %} +```text +{ + "method": "get_network_status", + "jsonrpc": "2.0", + "id": 1 +} +``` +{% endtab %} + +{% tab title="Response" %} +```text +{ + "method": "get_network_status", + "result": { + "network_status": { + object: "network_status", + "network_block_height": "152918", + "local_block_height": ""152918, + "fees": { + "0": "400000000", + "1": "2560" + }, + + } + }, + "error": null, + "jsonrpc": "2.0", + "id": 1, +} +``` +{% endtab %} +{% endtabs %} + diff --git a/docs/v2/api-endpoints/get_transaction_log.md b/docs/v2/api-endpoints/get_transaction_log.md new file mode 100644 index 000000000..69f79b444 --- /dev/null +++ b/docs/v2/api-endpoints/get_transaction_log.md @@ -0,0 +1,63 @@ +# Get Transaction Log + +## [Request](../../../full-service/src/json_rpc/v2/api/request.rs#L40) + +| Required Param | Purpose | Requirement | +| :--- | :--- | :--- | +| `transaction_log_id` | The transaction log ID to get. | Transaction log must exist in the wallet. | + +## [Response](../../../full-service/src/json_rpc/v2/api/response.rs#L41) + +## Example + +{% tabs %} +{% tab title="Request Body" %} +```text +{ + "method": "get_transaction_log", + "params": { + "transaction_log_id": "914e703b5b7bc44b61bb3657b4ee8a184d00e87a728e2fe6754a77a38598a800" + }, + "jsonrpc": "2.0", + "id": 1 +} +``` +{% endtab %} + +{% tab title="Response" %} +```text +{ + "method": "get_transaction_log", + "result": { + "transaction_log": { + "object": "transaction_log", + "transaction_log_id": "914e703b5b7bc44b61bb3657b4ee8a184d00e87a728e2fe6754a77a38598a800", + "direction": "tx_direction_received", + "is_sent_recovered": null, + "account_id": "a4db032dcedc14e39608fe6f26deadf57e306e8c03823b52065724fb4d274c10", + "recipient_address_id": null, + "assigned_address_id": null, + "value_pmob": "51068338999989068", + "fee_pmob": null, + "submitted_block_index": null, + "finalized_block_index": "152905", + "status": "tx_status_succeeded", + "input_txo_ids": [], + "output_txo_ids": [ + "914e703b5b7bc44b61bb3657b4ee8a184d00e87a728e2fe6754a77a38598a800" + ], + "change_txo_ids": [], + "sent_time": null, + "comment": "", + "failure_code": null, + "failure_message": null + } + }, + "error": null, + "jsonrpc": "2.0", + "id": 1, +} +``` +{% endtab %} +{% endtabs %} + diff --git a/docs/v2/api-endpoints/get_transaction_logs.md b/docs/v2/api-endpoints/get_transaction_logs.md new file mode 100644 index 000000000..5432974d4 --- /dev/null +++ b/docs/v2/api-endpoints/get_transaction_logs.md @@ -0,0 +1,125 @@ +# Get Transaction Logs For Account + +## [Request](../../../full-service/src/json_rpc/v2/api/request.rs#L40) + +| Optional Param | Purpose | Requirement | +| :--- | :--- | :--- | +| `account_id` | The account id to scan for transaction logs | Account must exist in the database | +| `min_block_index` | The minimum block index to find transaction logs from | | +| `max_block_index` | The maximum block index to find transaction logs from | | +| `offset` | The pagination offset. Results start at the offset index. | | +| `limit` | Limit for the number of results. | | + +## [Response](../../../full-service/src/json_rpc/v2/api/response.rs#L41) + +## Example + +{% tabs %} +{% tab title="Request Body" %} +```text +{ + "method": "get_transaction_logs", + "params": { + "account_id": "b59b3d0efd6840ace19cdc258f035cc87e6a63b6c24498763c478c417c1f44ca", + "offset": 2, + "limit": 1 + }, + "jsonrpc": "2.0", + "id": 1 +} +``` +{% endtab %} + +{% tab title="Response" %} +```text +{ + "method": "get_transaction_logs", + "result": { + "transaction_log_ids": [ + "ff1c85e7a488c2821110597ba75db30d913bb1595de549f83c6e8c56b06d70d1", + "58729797de0929eed37acb45225d3631235933b709c00015f46bfc002d5754fc", + "243494a0030bcbac40e87670b9288834047ef0727bcc6630a2fe2799439879ab" + ], + "transaction_log_map": { + "ff1c85e7a488c2821110597ba75db30d913bb1595de549f83c6e8c56b06d70d1": { + "object": "transaction_log", + "transaction_log_id": "ff1c85e7a488c2821110597ba75db30d913bb1595de549f83c6e8c56b06d70d1", + "direction": "tx_direction_sent", + "is_sent_recovered": null, + "account_id": "a4db032dcedc14e39608fe6f26deadf57e306e8c03823b52065724fb4d274c10", + "recipient_address_id": "7JvajhkAZYGmrpCY7ZpEiXRK5yW1ooTV7EWfDNu3Eyt572mH1wNb37BWiU6JqRUvgopPqSVZRexhXXpjF3wqLQR7HaJrcdbHmULujgFmzav", + "assigned_address_id": null, + "value_pmob": "8000000000008", + "fee_pmob": "10000000000", + "submitted_block_index": "152951", + "finalized_block_index": "152951", + "status": "tx_status_succeeded", + "input_txo_ids": [ + "135c3861be4034fccb8d0b329f86124cb6e2404cd4debf52a3c3a10cb4a7bdfb", + "c91b5f27e28460ef6c4f33229e70c4cfe6dc4bc1517a22122a86df9fb8e40815" + ], + "output_txo_ids": [ + "243494a0030bcbac40e87670b9288834047ef0727bcc6630a2fe2799439879ab" + ], + "change_txo_ids": [ + "58729797de0929eed37acb45225d3631235933b709c00015f46bfc002d5754fc" + ], + "sent_time": "2021-02-28 03:05:11 UTC", + "comment": "", + "failure_code": null, + "failure_message": null + }, + "58729797de0929eed37acb45225d3631235933b709c00015f46bfc002d5754fc": { + "object": "transaction_log", + "transaction_log_id": "58729797de0929eed37acb45225d3631235933b709c00015f46bfc002d5754fc", + "direction": "tx_direction_received", + "is_sent_recovered": null, + "account_id": "a4db032dcedc14e39608fe6f26deadf57e306e8c03823b52065724fb4d274c10", + "recipient_address_id": null, + "assigned_address_id": "2pW3CcHUmg4cafp9ePCpPg72mowC6NJZ1iHQxpkiAuPJuWDVUC9WEGRxychqFmKXx68VqerFKiHeEATwM5hZcf9SKC9Cub2GyMsztSqYdjY", + "value_pmob": "11891402222024", + "fee_pmob": null, + "submitted_block_index": null, + "finalized_block_index": "152951", + "status": "tx_status_succeeded", + "input_txo_ids": [], + "output_txo_ids": [ + "58729797de0929eed37acb45225d3631235933b709c00015f46bfc002d5754fc" + ], + "change_txo_ids": [], + "sent_time": null, + "comment": "", + "failure_code": null, + "failure_message": null + }, + "243494a0030bcbac40e87670b9288834047ef0727bcc6630a2fe2799439879ab": { + "object": "transaction_log", + "transaction_log_id": "243494a0030bcbac40e87670b9288834047ef0727bcc6630a2fe2799439879ab", + "direction": "tx_direction_received", + "is_sent_recovered": null, + "account_id": "a4db032dcedc14e39608fe6f26deadf57e306e8c03823b52065724fb4d274c10", + "recipient_address_id": null, + "assigned_address_id": "7JvajhkAZYGmrpCY7ZpEiXRK5yW1ooTV7EWfDNu3Eyt572mH1wNb37BWiU6JqRUvgopPqSVZRexhXXpjF3wqLQR7HaJrcdbHmULujgFmzav", + "value_pmob": "8000000000008", + "fee_pmob": null, + "submitted_block_index": null, + "finalized_block_index": "152951", + "status": "tx_status_succeeded", + "input_txo_ids": [], + "output_txo_ids": [ + "243494a0030bcbac40e87670b9288834047ef0727bcc6630a2fe2799439879ab" + ], + "change_txo_ids": [], + "sent_time": null, + "comment": "", + "failure_code": null, + "failure_message": null + } + } + }, + "jsonrpc": "2.0", + "id": 1 +} +``` +{% endtab %} +{% endtabs %} diff --git a/docs/v2/api-endpoints/get_txo.md b/docs/v2/api-endpoints/get_txo.md new file mode 100644 index 000000000..d5419e1a2 --- /dev/null +++ b/docs/v2/api-endpoints/get_txo.md @@ -0,0 +1,64 @@ +--- +description: Get details of a given TXO. +--- + +# Get TXO + +## [Request](../../../full-service/src/json_rpc/v2/api/request.rs#L40) + +| Parameter | Purpose | Requirements | +| :--- | :--- | :--- | +| `txo_id` | The TXO ID for which to get details. | | + +## [Response](../../../full-service/src/json_rpc/v2/api/response.rs#L41) + +## Example + +{% tabs %} +{% tab title="Request Body" %} +```text +{ + "method": "get_txo", + "params": { + "txo_id": "fff4cae55a74e5ce852b79c31576f4041d510c26e59fec178b3e45705c5b35a7" + }, + "jsonrpc": "2.0", + "id": 1 +} +``` +{% endtab %} + +{% tab title="Response" %} +```text +{ + "method": "get_txo", + "result": { + "txo": { + "object": "txo", + "txo_id": "fff4cae55a74e5ce852b79c31576f4041d510c26e59fec178b3e45705c5b35a7", + "value_pmob": "2960000000000", + "received_block_index": "8094", + "spent_block_index": "8180", + "is_spent_recovered": false, + "received_account_id": "a4db032dcedc14e39608fe6f26deadf57e306e8c03823b52065724fb4d274c10", + "minted_account_id": null, + "account_status_map": { + "a4db032dcedc14e39608fe6f26deadf57e306e8c03823b52065724fb4d274c10": { + "txo_status": "spent", + "txo_type": "received" + } + }, + "target_key": "0a209eefc082a656a34fae5cec81044d1b13bd8963c411afa28aecfce4839fc9f74e", + "public_key": "0a20f03f9684e5420d5410fe732f121626352d45e4e799d725432a0c61fa1343ac51", + "e_fog_hint": "0a544944e7527b7f09322651b7242663edf17478fd1804aeea24838a35ad3c66d5194763642ae1c1e0cd2bbe2571a97a8c0fb49e346d2fd5262113e7333c7f012e61114bd32d335b1a8183be8e1865b0a10199b60100", + "subaddress_index": "0", + "assigned_subaddress": "7BeDc5jpZu72AuNavumc8qo8CRJijtQ7QJXyPo9dpnqULaPhe6GdaDNF7cjxkTrDfTcfMgWVgDzKzbvTTwp32KQ78qpx7bUnPYxAgy92caJ", + "key_image": "0a205445b406012d26baebb51cbcaaaceb0d56387a67353637d07265f4e886f33419", + "confirmation": null + } + } +} +``` +{% endtab %} +{% endtabs %} + diff --git a/docs/v2/api-endpoints/get_txo_block_index.md b/docs/v2/api-endpoints/get_txo_block_index.md new file mode 100644 index 000000000..fa0028219 --- /dev/null +++ b/docs/v2/api-endpoints/get_txo_block_index.md @@ -0,0 +1,60 @@ +# Get TXO Block Index + +Allows the public key of a tx out to be checked against the ledger, and if it exists will return the block index + +## Request + +| Param | Description | | +| :--- | :--- | :--- | +| `public_key` | The public key of the txo. | public key is hex encoded bytes | + +## Response + +## Example + +{% tabs %} +{% tab title="Request Body" %} +```text +{ + "method": "get_txo_block_index", + "params": { + "public_key": "6607d6189a4dc24823f8da6d42884a046947d00d9400e7033d7425d9df152269" + }, + "jsonrpc": "2.0", + "id": 1 +} +``` +{% endtab %} + +{% tab title="Response Success" %} +```text +{ + "method": "get_txo_block_index", + "result": { + "block_index": "682053" + }, + "jsonrpc": "2.0", + "id": 1 +} +``` +{% endtab %} + +{% tab title="Response Failed" %} +```text +{ + "method": "get_txo_block_index", + "error": { + "code": -32603, + "message": "InternalError", + "data": { + "server_error": "LedgerDB(NotFound)", + "details": "Error with LedgerDB: Record not found" + } + }, + "jsonrpc": "2.0", + "id": 1 +} +``` +{% endtab %} +{% endtabs %} + diff --git a/docs/v2/api-endpoints/get_txo_membership_proofs.md b/docs/v2/api-endpoints/get_txo_membership_proofs.md new file mode 100644 index 000000000..5b149f341 --- /dev/null +++ b/docs/v2/api-endpoints/get_txo_membership_proofs.md @@ -0,0 +1,237 @@ +--- +description: Get the Tx Out Membership Proof for a selection of Tx Outs +--- + +# Get Txo Membership Proof + +## [Request](../../../full-service/src/json_rpc/v2/api/request.rs#L40) + +| Required Param | Purpose | Requirements | +| :--- | :--- | :--- | +| `outputs` | The TXOs to get the membership proofs for | TXO must exist in the ledger | + +## [Response](../../../full-service/src/json_rpc/v2/api/response.rs#L41) + +## Example + +{% tabs %} +{% tab title="Body Request" %} +```json +{ + "method": "get_txo_membership_proofs", + "params": { + "outputs": [ + { + "masked_amount": { + "commitment": "f6207c1952489634384434c230bac7eb72427d15742e2b43ce40fa9be21f6044", + "masked_value": "778515034541258781", + "masked_token_id": "" + }, + "target_key": "94f722c735c5d2ada2561717d7ce83a1ebf161d66d5ab0e13c8a189048629241", + "public_key": "eaaf989840dba9de8f825f7d11c01523ad46f7f581bafc5f9d2a37d35b4b9e2f", + "e_fog_hint": "7d806ff43d1b4ead24e63263932ef820e7ca5bc72c3b6a01eee42c5e814769eac6b78c72f7fe9cbe4b65dd0f3b70a63b1dcb5f3223430eb5890e388dfa6c8acf7c73f8eeeb3def9a6dd5b4b4a7d3150f8c1e0100", + "e_memo": "" + } + ], + }, + "jsonrpc": "2.0", + "id": 1 +} +``` +{% endtab %} + +{% tab title="Response" %} +```json +{ + "method": "get_txo_membership_proofs", + "result": { + "outputs": [ + { + "masked_amount": { + "commitment": "f6207c1952489634384434c230bac7eb72427d15742e2b43ce40fa9be21f6044", + "masked_value": "778515034541258781", + "masked_token_id": "" + }, + "target_key": "94f722c735c5d2ada2561717d7ce83a1ebf161d66d5ab0e13c8a189048629241", + "public_key": "eaaf989840dba9de8f825f7d11c01523ad46f7f581bafc5f9d2a37d35b4b9e2f", + "e_fog_hint": "7d806ff43d1b4ead24e63263932ef820e7ca5bc72c3b6a01eee42c5e814769eac6b78c72f7fe9cbe4b65dd0f3b70a63b1dcb5f3223430eb5890e388dfa6c8acf7c73f8eeeb3def9a6dd5b4b4a7d3150f8c1e0100", + "e_memo": "" + } + ], + "membership_proofs": [ + { + "index": "9636", + "highest_index": "2954613", + "elements": [ + { + "range": { + "from": "9636", + "to": "9636" + }, + "hash": "ba5fe09724623f2fb2dd769561aa0763dbc77efdba7ad8429a724949e8ac5180" + }, + { + "range": { + "from": "9637", + "to": "9637" + }, + "hash": "6845c1a6ec4543e8e045604b0573677872965972e4717c0fdfac038482671bbf" + }, + { + "range": { + "from": "9638", + "to": "9639" + }, + "hash": "68eab9e61bd72d68889d410f87c5d00356f103e367c5b0cdfc4bb7f70d5fdaa5" + }, + { + "range": { + "from": "9632", + "to": "9635" + }, + "hash": "6fc1d18c4593192e66e25ba7027c30a9a4e9ca188041bdad29524d26adfedc1e" + }, + { + "range": { + "from": "9640", + "to": "9647" + }, + "hash": "80e49cb0cf92cc5f14849b0d75461df291d88fd8a8db6dcc380e431419056aa4" + }, + { + "range": { + "from": "9648", + "to": "9663" + }, + "hash": "2a5b5cabea35d66b99ee4d348389b2a6f67e925d28a4fca66a4ebf72bfadabe6" + }, + { + "range": { + "from": "9600", + "to": "9631" + }, + "hash": "5360fea1cd5a0a56289f37d064765642841583f643c5f02056a5dc58206a9d4d" + }, + { + "range": { + "from": "9664", + "to": "9727" + }, + "hash": "b7c4ddf7d711f5393546e275a81a5e68a130bd789f0bf978a292838902dd4215" + }, + { + "range": { + "from": "9472", + "to": "9599" + }, + "hash": "1d0222a2289a66787c52ddd8346bf89807ebe5033afa952c90a31596720a0a4f" + }, + { + "range": { + "from": "9216", + "to": "9471" + }, + "hash": "40605f54922bfb35ca707773faa92e0f93f381980944f46e4074ca39a3647088" + }, + { + "range": { + "from": "9728", + "to": "10239" + }, + "hash": "4002e276511a4a94832e2dec52ca8ddf3e01371afb4035db06d5759a13f2a365" + }, + { + "range": { + "from": "8192", + "to": "9215" + }, + "hash": "1851bd61df6fdcef280e1e0f65700e1fcba4fcf71e492a3e0812b1e33b992fe5" + }, + { + "range": { + "from": "10240", + "to": "12287" + }, + "hash": "ac98ec9700c9a55eda01ea036d207778ce203e7e9b0fc53572a94b67ae6e7406" + }, + { + "range": { + "from": "12288", + "to": "16383" + }, + "hash": "fb103b9efbb385fb972a34c2e49dc3f8befbe84280236b07a6d3c7c140535ae7" + }, + { + "range": { + "from": "0", + "to": "8191" + }, + "hash": "e3414a20e668ca283fe1cc5f49a9e883234cfcff28bce60556c3e2102f908620" + }, + { + "range": { + "from": "16384", + "to": "32767" + }, + "hash": "d73181dc373033eced433a797aceda8da2664972198cc99c0e0c52851e6f7e90" + }, + { + "range": { + "from": "32768", + "to": "65535" + }, + "hash": "ea706f9b84f872c459e0e9e316705bc3a72bc683625b1279259d48d8a1d63633" + }, + { + "range": { + "from": "65536", + "to": "131071" + }, + "hash": "1ff8fea30828f2548877cc69ba12218c7c8a38969162cbcb9dc25e5e08a1ae7f" + }, + { + "range": { + "from": "131072", + "to": "262143" + }, + "hash": "72973b7fbb93e23b67f278721c951098f630c375aaaf5e16fc04fe6271485d2d" + }, + { + "range": { + "from": "262144", + "to": "524287" + }, + "hash": "ede9af86064b5b91edf646ebe6b8f0fbaa31344894c77ad06d9f79784d536bca" + }, + { + "range": { + "from": "524288", + "to": "1048575" + }, + "hash": "5027acb6de8ac0b4e8ed35e23af60b165960a5e02fc3a5cdef0fb476e2f6ffc9" + }, + { + "range": { + "from": "1048576", + "to": "2097151" + }, + "hash": "47771f1a5984fc3243e37869733db130f174967f9c81847ea64cccef937e1c7c" + }, + { + "range": { + "from": "2097152", + "to": "4194303" + }, + "hash": "616efa08e869e62bb7ba8a04523b1fc18ec7d0c71c524d08d24f54aa7383dbd3" + } + ] + } + ] + }, + "jsonrpc": "2.0", + "id": 1 +} +``` +{% endtab %} +{% endtabs %} + diff --git a/docs/v2/api-endpoints/get_txos.md b/docs/v2/api-endpoints/get_txos.md new file mode 100644 index 000000000..157b706d8 --- /dev/null +++ b/docs/v2/api-endpoints/get_txos.md @@ -0,0 +1,151 @@ +--- +description: Get TXOs for a given account with offset and limit parameters +--- + +# Get TXOs + +## [Request](../../../full-service/src/json_rpc/v2/api/request.rs#L40) + +| Optional Param | Purpose | Requirements | +| :--- | :--- | :--- | +| `account_id` | The account on which to perform this action. | Account must exist in the wallet. | +| `address` | The address b58 on which to perform this action. | Address must exist in the wallet. | +| `status` | Txo status filer. Available status': "unverified", "unspent", "spent", "orphaned", "pending", "secreted", | | +| `offset` | The pagination offset. Results start at the offset index. | | +| `limit` | Limit for the number of results.| | + +## [Response](../../../full-service/src/json_rpc/v2/api/response.rs#L41) + +## Example + +{% tabs %} +{% tab title="Request Body" %} +```text +{ + "method": "get_txos", + "params": { + "account_id": "b59b3d0efd6840ace19cdc258f035cc87e6a63b6c24498763c478c417c1f44ca", + "offset": 2, + "limit": 8 + }, + "jsonrpc": "2.0", + "id": 1 +} +``` +{% endtab %} + +{% tab title="Response" %} +```text +{ + "method": "get_txos", + "result": { + "txo_ids": [ + "001cdcc1f0a22dc0ddcdaac6020cc03d919cbc3c36923f157b4a6bf0dc980167", + "00408833347550b046f0996afe92313745f76e307904686e93de5bab3590e9da", + "005b41a40be1401426f9a00965cc334e4703e4089adb8fa00616e7b25b92c6e5" + ], + "txo_map": { + "001cdcc1f0a22dc0ddcdaac6020cc03d919cbc3c36923f157b4a6bf0dc980167": { + "account_status_map": { + "a4db032dcedc14e39608fe6f26deadf57e306e8c03823b52065724fb4d274c10": { + "txo_status": "spent", + "txo_type": "received" + } + }, + "assigned_subaddress": "7BeDc5jpZu72AuNavumc8qo8CRJijtQ7QJXyPo9dpnqULaPhe6GdaDNF7cjxkTrDfTcfMgWVgDzKzbvTTwp32KQ78qpx7bUnPYxAgy92caJ", + "e_fog_hint": "0a54bf0a5f37989b379b9db3e8937387c5033428b399d44ee524c02b53ce8b7fa7ffc7181a854255cefc68704f69eedd43a891d2ed65c9f6e4c0fc645c2bc156278395221100a4fc3a1d617d04f6eca8851e846a0100", + "is_spent_recovered": false, + "key_image": "0a20f041e3da520a6e3328d43a920b90bf87826a1602c9249cf6591dd32328a4544e", + "minted_account_id": null, + "object": "txo", + "confirmation": null, + "public_key": "0a201a592874a596aeb14cbeb1c7d3449cbd20dc8078ad7fff657e131d619145ef0a", + "received_account_id": "a4db032dcedc14e39608fe6f26deadf57e306e8c03823b52065724fb4d274c10", + "received_block_index": "128567", + "spent_block_index": "128569", + "subaddress_index": "0", + "target_key": "0a209e1067117870549a77a47de04bd810da052abfc23d60a0c433367bfc689b7428", + "txo_id": "001cdcc1f0a22dc0ddcdaac6020cc03d919cbc3c36923f157b4a6bf0dc980167", + "value_pmob": "990000000000" + }, + "84f30233774d728bb7844bed59d471fe55ee3680ab70ddc312840db0f978f3ba": { + "account_status_map": { + "36fdf8fbdaa35ad8e661209b8a7c7057f29bf16a1e399a34aa92c3873dfb853c": { + "txo_status": "unspent", + "txo_type": "received" + }, + "a4db032dcedc14e39608fe6f26deadf57e306e8c03823b52065724fb4d274c10": { + "txo_status": "secreted", + "txo_type": "minted" + } + }, + "assigned_subaddress": null, + "e_fog_hint": "0a5472b079a520696518cc7d7c3036e855cbbcf1a3e247db32ab2e62e835183077b862ef86ec4963a584650cc028eb645569f9de1392b88f8fd7fa07aa28c4e035fd5f4866f3db3d403a05d2adb5e4f2992c010b0100", + "is_spent_recovered": false, + "key_image": null, + "minted_account_id": "a4db032dcedc14e39608fe6f26deadf57e306e8c03823b52065724fb4d274c10", + "object": "txo", + "confirmation": "0a204488e153cce1e4bcdd4419eecb778f3d2d2b024b39aaa29532d2e47e238b2e31", + "public_key": "0a20e6736474f73e440686736bfd045d838c2b3bc056ffc647ad6b1c990f5a46b123", + "received_account_id": "36fdf8fbdaa35ad8e661209b8a7c7057f29bf16a1e399a34aa92c3873dfb853c", + "received_block_index": null, + "spent_block_index": null, + "subaddress_index": null, + "target_key": "0a20762d8a723aae2aa70cc11c62c91af715f957a7455b695641fe8c94210812cf1b", + "txo_id": "84f30233774d728bb7844bed59d471fe55ee3680ab70ddc312840db0f978f3ba", + "value_pmob": "200" + }, + "58c2c3780792ccf9c51014c7688a71f03732b633f8c5dfa49040fa7f51328280": { + "account_status_map": { + "a4db032dcedc14e39608fe6f26deadf57e306e8c03823b52065724fb4d274c10": { + "txo_status": "unspent", + "txo_type": "received" + } + }, + "assigned_subaddress": "7BeDc5jpZu72AuNavumc8qo8CRJijtQ7QJXyPo9dpnqULaPhe6GdaDNF7cjxkTrDfTcfMgWVgDzKzbvTTwp32KQ78qpx7bUnPYxAgy92caJ", + "e_fog_hint": "0a546f862ccf5e96a89b3ede770a70aa26ce8be704a7e5a73fff02d16ee1f694297b6c17d2e668d6181df047ae68730dfc7913b28aca66450ee1de0ca3b0bedb07664918899848f217bcbbe48be2ef40074ae5dd0100", + "is_spent_recovered": false, + "key_image": "0a20784ab38c4541ce23abbec6744431d6ae14101c49c6535b3e9bf3fd728db13848", + "minted_account_id": null, + "object": "txo", + "confirmation": null, + "public_key": "0a20d803a979c9ec0531f106363a885dde29101fcd70209f9ed686905512dfd14d5f", + "received_account_id": "a4db032dcedc14e39608fe6f26deadf57e306e8c03823b52065724fb4d274c10", + "received_block_index": "79", + "spent_block_index": null, + "subaddress_index": "0", + "target_key": "0a209abadbfcec6c81b3d184dc104e51cac4c4faa8bab4da21a3714901519810c20d", + "txo_id": "58c2c3780792ccf9c51014c7688a71f03732b633f8c5dfa49040fa7f51328280", + "value_pmob": "4000000000000" + }, + "b496f4f3ec3159bf48517aa7d9cda193ef8bfcac343f81eaed0e0a55849e4726": { + "account_status_map": { + "a4db032dcedc14e39608fe6f26deadf57e306e8c03823b52065724fb4d274c10": { + "txo_status": "secreted", + "txo_type": "minted" + } + }, + "assigned_subaddress": null, + "e_fog_hint": "0a54338fcf8609cf80dfe017bee2339b22b626af2957ef579ae8829f3d8e7fab6c20365b6a99727fcd5e3de7784fca7e1cbb77ec35e7f2c39ea47ef6121716119ba5a67f8a6026a6a6274e7262ea8ea8280782440100", + "is_spent_recovered": false, + "key_image": null, + "minted_account_id": "a4db032dcedc14e39608fe6f26deadf57e306e8c03823b52065724fb4d274c10", + "object": "txo", + "confirmation": null, + "public_key": "0a209432c589bb4e5101c26e935b70930dfe45c78417527fb994872ebd65fcb9c116", + "received_account_id": null, + "received_block_index": null, + "spent_block_index": null, + "subaddress_index": null, + "target_key": "0a208c75723e9b9a4af0c833bfe190c43900c3b41834cf37024f5fecfbe9919dff23", + "txo_id": "b496f4f3ec3159bf48517aa7d9cda193ef8bfcac343f81eaed0e0a55849e4726", + "value_pmob": "980000000000" + } + } + }, + "jsonrpc": "2.0", + "id": 1 +} +``` +{% endtab %} +{% endtabs %} diff --git a/docs/v2/api-endpoints/get_wallet_status.md b/docs/v2/api-endpoints/get_wallet_status.md new file mode 100644 index 000000000..93cf21d84 --- /dev/null +++ b/docs/v2/api-endpoints/get_wallet_status.md @@ -0,0 +1,86 @@ +--- +description: Get the current status of a wallet. Note that pmob calculations do not include view-only-accounts +--- + +# Get Wallet Status + +## [Request](../../../full-service/src/json_rpc/v2/api/request.rs#L40) + +## [Response](../../../full-service/src/json_rpc/v2/api/response.rs#L41) + +## Example + +{% tabs %} +{% tab title="Body Request" %} +```text +{ + "method": "get_wallet_status", + "jsonrpc": "2.0", + "id": 1 +} +``` +{% endtab %} + +{% tab title="Response" %} +```text +{ + "method": "get_wallet_status", + "result": { + "wallet_status": { + "account_ids": [ + "b0be5377a2f45b1573586ed530b2901a559d9952ea8a02f8c2dbb033a935ac17", + "6ed6b79004032fcfcfa65fa7a307dd004b8ec4ed77660d36d44b67452f62b470" + ], + "account_map": { + "6ed6b79004032fcfcfa65fa7a307dd004b8ec4ed77660d36d44b67452f62b470": { + "account_id": "6ed6b79004032fcfcfa65fa7a307dd004b8ec4ed77660d36d44b67452f62b470", + "key_derivation_version:": "2", + "main_address": "CaE5bdbQxLG2BqAYAz84mhND79iBSs13ycQqN8oZKZtHdr6KNr1DzoX93c6LQWYHEi5b7YLiJXcTRzqhDFB563Kr1uxD6iwERFbw7KLWA6", + "name": "Bob", + "next_subaddress_index": "2", + "first_block_index": "3500", + "object": "account", + "recovery_mode": false + }, + "b0be5377a2f45b1573586ed530b2901a559d9952ea8a02f8c2dbb033a935ac17": { + "account_id": "b0be5377a2f45b1573586ed530b2901a559d9952ea8a02f8c2dbb033a935ac17", + "key_derivation_version:": "2", + "main_address": "7JvajhkAZYGmrpCY7ZpEiXRK5yW1ooTV7EWfDNu3Eyt572mH1wNb37BWiU6JqRUvgopPqSVZRexhXXpjF3wqLQR7HaJrcdbHmULujgFmzav", + "name": "Brady", + "next_subaddress_index": "2", + "first_block_index": "3500", + "object": "account", + "recovery_mode": false + } + }, + "is_synced_all": false, + "local_block_height": "152918", + "network_block_height": "152918", + "balance_per_token": { + "0": { + "orphaned": "0", + "pending": "70148220000000000", + "secreted": "0", + "spent": "0", + "unspent": "220588320000000000", + "unverified": "1300004044440000" + }, + "1": { + "orphaned": "0", + "pending": "70148220000000000", + "secreted": "0", + "spent": "0", + "unspent": "220588320000000000", + "unverified": "1300004044440000" + } + } + } + }, + "error": null, + "jsonrpc": "2.0", + "id": 1, +} +``` +{% endtab %} +{% endtabs %} + diff --git a/docs/v2/api-endpoints/import_account.md b/docs/v2/api-endpoints/import_account.md new file mode 100644 index 000000000..d34990fbd --- /dev/null +++ b/docs/v2/api-endpoints/import_account.md @@ -0,0 +1,67 @@ +--- +description: Import an existing account from the secret entropy. +--- + +# Import Account + +## [Request](../../../full-service/src/json_rpc/v2/api/request.rs#L40) + +| Required Param | Purpose | Requirements | +| :--- | :--- | :--- | +| `mnemonic` | The secret mnemonic to recover the account. | The mnemonic must be 24 words. | +| `key_derivation_version` | The version number of the key derivation used to derive an account key from this mnemonic. The current version is 2. | | + +| Optional Param | Purpose | Requirements | +| :--- | :--- | :--- | +| `name` | A label for this account. | A label can have duplicates, but it is not recommended. | +| `next_subaddress_index` | The next known unused subaddress index for the account. | | +| `first_block_index` | The block from which to start scanning the ledger. | | +| `fog_report_url` | | | +| `fog_report_id` | | | +| `fog_authority_spki` | | | + +## [Response](../../../full-service/src/json_rpc/v2/api/response.rs#L41) + +## Example + +{% tabs %} +{% tab title="Request Body" %} +```text +{ + "method": "import_account", + "params": { + "mnemonic": "sheriff odor square mistake huge skate mouse shoot purity weapon proof stuff correct concert blanket neck own shift clay mistake air viable stick group", + "key_derivation_version": "2", + "name": "Bob" + "next_subaddress_index": 2, + "first_block_index": "3500" + }, + "jsonrpc": "2.0", + "id": 1 +} +``` +{% endtab %} + +{% tab title="Response" %} +```text +{ + "method": "import_account", + "result": { + "account": { + "object": "account", + "account_id": "6ed6b79004032fcfcfa65fa7a307dd004b8ec4ed77660d36d44b67452f62b470", + "name": "Bob", + "main_address": "CaE5bdbQxLG2BqAYAz84mhND79iBSs13ycQqN8oZKZtHdr6KNr1DzoX93c6LQWYHEi5b7YLiJXcTRzqhDFB563Kr1uxD6iwERFbw7KLWA6", + "next_subaddress_index": "2", + "first_block_index": "3500", + "recovery_mode": false + } + }, + "error": null, + "jsonrpc": "2.0", + "id": 1, +} +``` +{% endtab %} +{% endtabs %} + diff --git a/docs/v2/api-endpoints/import_account_from_legacy_root_entropy.md b/docs/v2/api-endpoints/import_account_from_legacy_root_entropy.md new file mode 100644 index 000000000..05880025b --- /dev/null +++ b/docs/v2/api-endpoints/import_account_from_legacy_root_entropy.md @@ -0,0 +1,73 @@ +--- +description: Import an existing account from the secret entropy. +--- + +# Import Account Legacy + +## [Request](../../../full-service/src/json_rpc/v2/api/request.rs#L40) + +| Required Param | Purpose | Requirements | +| :--- | :--- | :--- | +| `entropy` | The secret root entropy. | 32 bytes of randomness, hex-encoded. | + +| Optional Param | Purpose | Requirements | +| :--- | :--- | :--- | +| `name` | A label for this account. | A label can have duplicates, but it is not recommended. | +| `next_subaddress_index` | The next known unused subaddress index for the account. | | +| `first_block_index` | The block from which to start scanning the ledger. | | +| `fog_report_url` | | | +| `fog_report_id` | | | +| `fog_authority_spki` | | | + +## [Response](../../../full-service/src/json_rpc/v2/api/response.rs#L41) + +## Example + +{% tabs %} +{% tab title="Request Body" %} +```text +{ + "method": "import_account_from_legacy_root_entropy", + "params": { + "entropy": "c593274dc6f6eb94242e34ae5f0ab16bc3085d45d49d9e18b8a8c6f057e6b56b", + "name": "Bob" + "next_subaddress_index": 2, + "first_block_index": "3500", + }, + "jsonrpc": "2.0", + "id": 1 +} +``` +{% endtab %} + +{% tab title="Response" %} +```text +{ + "method": "import_account", + "result": { + "account": { + "object": "account", + "account_id": "6ed6b79004032fcfcfa65fa7a307dd004b8ec4ed77660d36d44b67452f62b470", + "name": "Bob", + "main_address": "CaE5bdbQxLG2BqAYAz84mhND79iBSs13ycQqN8oZKZtHdr6KNr1DzoX93c6LQWYHEi5b7YLiJXcTRzqhDFB563Kr1uxD6iwERFbw7KLWA6", + "next_subaddress_index": "2", + "first_block_index": "3500", + "recovery_mode": false + } + }, + "error": null, + "jsonrpc": "2.0", + "id": 1, +} +``` +{% endtab %} +{% endtabs %} + +{% hint style="warning" %} +`If you attempt to import an account already in the wallet, you will see the following error message:` + +```text +{"error": "Database(Diesel(DatabaseError(UniqueViolation, "UNIQUE constraint failed: accounts.account_id_hex")))"} +``` +{% endhint %} + diff --git a/docs/v2/api-endpoints/import_view_only_account.md b/docs/v2/api-endpoints/import_view_only_account.md new file mode 100644 index 000000000..95883336a --- /dev/null +++ b/docs/v2/api-endpoints/import_view_only_account.md @@ -0,0 +1,68 @@ +--- +description: >- + Create a view-only account by importing the private key from an existing + account. Note: a single wallet cannot have both the regular and view-only versions of an account. +--- + +# Import + +## [Request](../../../full-service/src/json_rpc/v2/api/request.rs#L40) + +| Required Param | Purpose | Requirements | +| -------------- | ---------------------------------------------------------------------------------- | ------------ | +| `view_private_key` | The view private key of this account | | +| `spend_public_key` | The spend public key of this account | | + +| Optional Param | Purpose | Requirements | +| -------------- | ---------------------------------------------------------------------------------- | ------------ | +| `name` | | | +| `first_block_index` | | | +| `next_subaddress_index` | | | + +## [Response](../../../full-service/src/json_rpc/v2/api/response.rs#L41) + +## Example + +{% tabs %} +{% tab title="Request Body" %} +``` +{ + "method": "import_view_only_account", + "result": { + "account": { + "view_private_key": "6ed6b79004032fcfcfa65fa7a307dd004b8ec4ed77660d36d44b67452f62b470", + "spend_public_key": "fcewc434g5353v535323f43f43f43g5342v3b67n8576453f4dcv56b77n857b46", + "name": "Coins for cats", + "first_block_index": "3500", + "next_block_index": "4000", + } + }, + "error": null, + "jsonrpc": "2.0", + "id": 1, +} +``` +{% endtab %} + +{% tab title="Response" %} +``` +{ + "method": "import_view_only_account", + "params": { + "account": { + "object": "account", + "account_id": "6ed6b79004032fcfcfa65fa7a307dd004b8ec4ed77660d36d44b67452f62b470", + "name": "Bob", + "main_address": "CaE5bdbQxLG2BqAYAz84mhND79iBSs13ycQqN8oZKZtHdr6KNr1DzoX93c6LQWYHEi5b7YLiJXcTRzqhDFB563Kr1uxD6iwERFbw7KLWA6", + "next_subaddress_index": "2", + "first_block_index": "3500", + "recovery_mode": false + } + }, + "jsonrpc": "2.0", + "api_version": "2", + "id": 1 +} +``` +{% endtab %} +{% endtabs %} diff --git a/docs/view-only-accounts/account/remove_view_only_account.md b/docs/v2/api-endpoints/remove_account.md similarity index 68% rename from docs/view-only-accounts/account/remove_view_only_account.md rename to docs/v2/api-endpoints/remove_account.md index dbb4d3e87..57dcc8768 100644 --- a/docs/view-only-accounts/account/remove_view_only_account.md +++ b/docs/v2/api-endpoints/remove_account.md @@ -1,22 +1,24 @@ --- -description: Remove a view only account from a given wallet. +description: Remove an account from a given wallet. --- # Remove Account -## Parameters +## [Request](../../../full-service/src/json_rpc/v2/api/request.rs#L40) | Required Param | Purpose | Requirements | | :--- | :--- | :--- | | `account_id` | The account on which to perform this action. | Account must exist in the wallet. | +## [Response](../../../full-service/src/json_rpc/v2/api/response.rs#L41) + ## Example {% tabs %} {% tab title="Request Body" %} ```text { - "method": "remove_view_only_account", + "method": "remove_account", "params": { "account_id": "3407fbbc250799f5ce9089658380c5fe152403643a525f581f359917d8d59d52" }, @@ -29,7 +31,7 @@ description: Remove a view only account from a given wallet. {% tab title="Response" %} ```text { - "method": "remove_view_only_account", + "method": "remove_account", "result": { "removed": true }, diff --git a/docs/v2/api-endpoints/sample_mixins.md b/docs/v2/api-endpoints/sample_mixins.md new file mode 100644 index 000000000..cded93b8c --- /dev/null +++ b/docs/v2/api-endpoints/sample_mixins.md @@ -0,0 +1,414 @@ +--- +description: Sample a desired number of mixins from the ledger, excluding a list of tx outs +--- + +# Sample Mixins + +## [Request](../../../full-service/src/json_rpc/v2/api/request.rs#L40) + +| Required Param | Purpose | Requirements | +| :--- | :--- | :--- | +| `outputs` | The TXOs to get the membership proofs for | TXO must exist in the ledger | + +## [Response](../../../full-service/src/json_rpc/v2/api/response.rs#L41) + +## Example + +{% tabs %} +{% tab title="Body Request" %} +```json +{ + "method": "sample_mixins", + "params": { + "num_mixins": 2, + "excluded_outputs": [{ + "masked_amount": { + "commitment": "f6207c1952489634384434c230bac7eb72427d15742e2b43ce40fa9be21f6044", + "masked_value": "778515034541258781", + "masked_token_id": "" + }, + "target_key": "94f722c735c5d2ada2561717d7ce83a1ebf161d66d5ab0e13c8a189048629241", + "public_key": "eaaf989840dba9de8f825f7d11c01523ad46f7f581bafc5f9d2a37d35b4b9e2f", + "e_fog_hint": "7d806ff43d1b4ead24e63263932ef820e7ca5bc72c3b6a01eee42c5e814769eac6b78c72f7fe9cbe4b65dd0f3b70a63b1dcb5f3223430eb5890e388dfa6c8acf7c73f8eeeb3def9a6dd5b4b4a7d3150f8c1e0100", + "e_memo": "" + }] + }, + "jsonrpc": "2.0", + "id": 1 +} +``` +{% endtab %} + +{% tab title="Response" %} +```json +{ + "method": "sample_mixins", + "result": { + "mixins": [ + { + "masked_amount": { + "commitment": "268bca85a8bf01d775f98952788ef2eaf48618e0ac4dbb642426ac270f63e501", + "masked_value": "4148226062671934601", + "masked_token_id": "" + }, + "target_key": "46e18441764ca38f669abd609cb04aa1961ba3c57855f363c45045117006260e", + "public_key": "8a28ebd659c9914343427121a3cc3b5b527a97805ce11ca1d6b16568326ffe22", + "e_fog_hint": "000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + "e_memo": "" + }, + { + "masked_amount": { + "commitment": "887a666054c0366bdf951ea84af07d25de6267301fb5b841be3fc412ed9a4470", + "masked_value": "1551464221591799897", + "masked_token_id": "" + }, + "target_key": "c68e49d858ff75d150f8441759a2e7bf3ff187306b7a03020104fcac34929439", + "public_key": "cc0d1969185915b0dfb1d6bef089d9ac3be214e5f40b8aa3332b60827a48ac45", + "e_fog_hint": "1d19b3e329b61410e42a20675473fa9583b3e4fbfa803f8933cf6a7b0d29dbf51a8e1a0f3bb5e7be496acdb9c4d0fcb4363a8d4a53601d59a2378d7cf7a3344e9414da8bac896edb0591d90bc51ea658c69a0100", + "e_memo": "" + } + ], + "membership_proofs": [ + { + "index": "643891", + "highest_index": "2954595", + "elements": [ + { + "range": { + "from": "643891", + "to": "643891" + }, + "hash": "16439812756f65a2bad5bd7df813aa318979b465220b3c3654b1474b5f42cf24" + }, + { + "range": { + "from": "643890", + "to": "643890" + }, + "hash": "88c2d7d82a4d45510651058fa8c4c0f7c4baa7ddb8fe101f31492586b089b0f0" + }, + { + "range": { + "from": "643888", + "to": "643889" + }, + "hash": "1252defa6d3289a7c9d24460d399fb7681e36a479c82076d0857448c9e206daf" + }, + { + "range": { + "from": "643892", + "to": "643895" + }, + "hash": "ad09a8f8ce75974c2237129f43360c2a357177886c48186183c60d96b625f29b" + }, + { + "range": { + "from": "643896", + "to": "643903" + }, + "hash": "119ed507664cfef1da5c17378b08b6b0e4974c20a08a213eb308e8540c907547" + }, + { + "range": { + "from": "643872", + "to": "643887" + }, + "hash": "8aa944350fb0d0595bda4035939f581c4fe25e8d0554aa8f2b9235b40fec0b63" + }, + { + "range": { + "from": "643840", + "to": "643871" + }, + "hash": "6da268a3f750752ea70fe9c49af2df27cecae71568541efe06bea5589e60148f" + }, + { + "range": { + "from": "643904", + "to": "643967" + }, + "hash": "ba4199316471f0c7fc262093a0b77ff027e7a983acc0cfef838e86c6f08942a0" + }, + { + "range": { + "from": "643968", + "to": "644095" + }, + "hash": "eb470ef71de14005d27d51be6774d123bea003dda316618e308ed5fba9a82e81" + }, + { + "range": { + "from": "643584", + "to": "643839" + }, + "hash": "3ad7566ffaa14b05695a30e25e916fdb8791318b32f3ca05360a5324391a8cee" + }, + { + "range": { + "from": "643072", + "to": "643583" + }, + "hash": "5ffbfbf22f33738dc5d5d3aa5f7ed2dcabc59bd662132ee7e660a23b6bef668e" + }, + { + "range": { + "from": "644096", + "to": "645119" + }, + "hash": "a6d774dec28e475b01dd3418c4f3698b303bcc22e89c73d74de0c4f4fb55fda6" + }, + { + "range": { + "from": "645120", + "to": "647167" + }, + "hash": "69855c4cd1af89d7fd3b498da26e9aafe25eaed453a6435c82a270cae786b6ed" + }, + { + "range": { + "from": "638976", + "to": "643071" + }, + "hash": "c0f365bb448b01c7c15aee1ed599929b26a93aa29eb277556d38522fc0e84816" + }, + { + "range": { + "from": "647168", + "to": "655359" + }, + "hash": "75698e4045eb48ad902a9eba22c1cf57e39172e37e3fb4f500c019b0f4753e9f" + }, + { + "range": { + "from": "622592", + "to": "638975" + }, + "hash": "1021ca2ad629be7f6939c123e868e08dd57dcdd3fcbf833a48cf1a50cb3d56f1" + }, + { + "range": { + "from": "589824", + "to": "622591" + }, + "hash": "4ae17019426c6648db5d618c7b8cd9597b98efb35b7fe0991bdc174ab7c4fe47" + }, + { + "range": { + "from": "524288", + "to": "589823" + }, + "hash": "91ac1d42a7874b1fa0cba1450ba5de987215d690d128aeda9b6841421149001a" + }, + { + "range": { + "from": "655360", + "to": "786431" + }, + "hash": "0e9ef9d0354896086a7943b076aa7ddd0d0dd94d30542b82d90735e9a080a32a" + }, + { + "range": { + "from": "786432", + "to": "1048575" + }, + "hash": "4d4dec78fc598560811d42adc2744d7f2499c0b684414c355f6f3b577bfe74fe" + }, + { + "range": { + "from": "0", + "to": "524287" + }, + "hash": "deee6f72a764e18887a16d96006b2c35f23c411d273805e4f10827151cba7a2a" + }, + { + "range": { + "from": "1048576", + "to": "2097151" + }, + "hash": "47771f1a5984fc3243e37869733db130f174967f9c81847ea64cccef937e1c7c" + }, + { + "range": { + "from": "2097152", + "to": "4194303" + }, + "hash": "ff083a72249806122533f32baa14253f6bf3801d7b1f9a804ab86cd15e7542a9" + } + ] + }, + { + "index": "1441542", + "highest_index": "2954595", + "elements": [ + { + "range": { + "from": "1441542", + "to": "1441542" + }, + "hash": "361dd7f0a49848ef8af280ac9ab0293edf017ec8bcf77ec386243621babaf1a8" + }, + { + "range": { + "from": "1441543", + "to": "1441543" + }, + "hash": "9fb233507554c39f480c07f95abf81efa26509060027f1fbe2fd8a3dfaba4b20" + }, + { + "range": { + "from": "1441540", + "to": "1441541" + }, + "hash": "03a6468c2ecc6470ef9afa8805f1d00ecab0bd837e2b9a8ddaa5a49bc4bf57d4" + }, + { + "range": { + "from": "1441536", + "to": "1441539" + }, + "hash": "767d515ad4f9c5c87acaa9375ddd71a219f710b4bc9b9df65f15e8de65e2f90d" + }, + { + "range": { + "from": "1441544", + "to": "1441551" + }, + "hash": "c8cb3365172d07041be5f233bb949ba3d7427d725b605f41c12a838108dcf380" + }, + { + "range": { + "from": "1441552", + "to": "1441567" + }, + "hash": "8e239fa9d67e4de48132720c4223df43d5ec94b696e36afa1d998c1208fd84a7" + }, + { + "range": { + "from": "1441568", + "to": "1441599" + }, + "hash": "506b80222a94a324b25ae59f54718c2c152310cc24779764feeb359fb212df7d" + }, + { + "range": { + "from": "1441600", + "to": "1441663" + }, + "hash": "11da0472aacf7228baa1d620a2112c85b36e2e52a089ae6ae67ba93fc015a277" + }, + { + "range": { + "from": "1441664", + "to": "1441791" + }, + "hash": "2c76256cbf719f32916a7c9ed7a343ca31f374347c0e8b42fc047dd132b36f06" + }, + { + "range": { + "from": "1441280", + "to": "1441535" + }, + "hash": "917cdf2fbdc8a18cffd6b2beaed2e75f884900cd9553725df18786898812c800" + }, + { + "range": { + "from": "1440768", + "to": "1441279" + }, + "hash": "50ed8c28966abff98d024cc14019fa64fe714c47ffae23c1cc2f0acb51b1c6fe" + }, + { + "range": { + "from": "1439744", + "to": "1440767" + }, + "hash": "c8bc95e8ab8380473b7d5727a58d859539066baffbf170b8f2f9e7b74245baf6" + }, + { + "range": { + "from": "1437696", + "to": "1439743" + }, + "hash": "66f969f7e412fb08bba0ccd891fe3b0047ad69c5519cf06f0d77add0132d6da0" + }, + { + "range": { + "from": "1433600", + "to": "1437695" + }, + "hash": "90a555645cfa3bd6d8e3e0d51960bcda6480ae639aa05b0facf1f5e8d837e636" + }, + { + "range": { + "from": "1425408", + "to": "1433599" + }, + "hash": "5f39939869191af4cf196da51cdf326dd43bb4626d7a8e50e81d57a30fdc4411" + }, + { + "range": { + "from": "1409024", + "to": "1425407" + }, + "hash": "ecde44bae54f53cf3caef36da0ec353a42903faee2dd7d3159b422fa6af8df0a" + }, + { + "range": { + "from": "1376256", + "to": "1409023" + }, + "hash": "b3ce002cc4940f56dbd8b29a875d8e0d81ecae2a4c6abf502d9fce01a1e77292" + }, + { + "range": { + "from": "1310720", + "to": "1376255" + }, + "hash": "6276cccce69ae18d2fb278371848d31af09764089700a27cf774dbcc71437995" + }, + { + "range": { + "from": "1441792", + "to": "1572863" + }, + "hash": "3838f438938a2d43ee2746b58cc2b7f5c41e4e2fadf3a44af20bece443152c24" + }, + { + "range": { + "from": "1048576", + "to": "1310719" + }, + "hash": "8256f43b4bf0cd9c67d6d753e3f96ad489eaab5adb225f8ce3909164c2faa279" + }, + { + "range": { + "from": "1572864", + "to": "2097151" + }, + "hash": "10804b01fd7da84d4a1378917a6e696b5b986080dbd827e04db44ead4da3beb9" + }, + { + "range": { + "from": "0", + "to": "1048575" + }, + "hash": "84c55b30648860a50aa0972473b8f24dd7dfb87ba94ddd2a2f23965a9d8e715c" + }, + { + "range": { + "from": "2097152", + "to": "4194303" + }, + "hash": "ff083a72249806122533f32baa14253f6bf3801d7b1f9a804ab86cd15e7542a9" + } + ] + } + ] + }, + "jsonrpc": "2.0", + "id": 1 +} +``` +{% endtab %} +{% endtabs %} + diff --git a/docs/v2/api-endpoints/submit_transaction.md b/docs/v2/api-endpoints/submit_transaction.md new file mode 100644 index 000000000..58eb1f5b6 --- /dev/null +++ b/docs/v2/api-endpoints/submit_transaction.md @@ -0,0 +1,289 @@ +--- +description: >- + Submit a transaction for an account with or without recording it in the + transaction log. +--- + +# Submit Transaction + +## [Request](../../../full-service/src/json_rpc/v2/api/request.rs#L40) + +| Required Param | Purpose | Requirements | +| -------------- | ------------------------------ | -------------------------------- | +| `tx_proposal` | Transaction proposal to submit | Created with `build_transaction` | + +| Optional Param | Purpose | Requirements | +| -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------ | +| `account_id` | Account ID for which to log the transaction. If omitted, the transaction is not logged and therefor the txos used will not be set to pending, if they exist. This could inadvertently cause an attempt to spend the same txo in multiple transactions. | | +| `comment` | Comment to annotate this transaction in the transaction log | | + +## [Response](../../../full-service/src/json_rpc/v2/api/response.rs#L41) + +## Examples + +### Submit with Log + +{% tabs %} +{% tab title="Request Body" %} +``` +{ + "method": "submit_transaction", + "params": { + "tx_proposal": { + "input_list": [ + { + "tx_out": { + "amount": { + "commitment": "629abf4112819dadfa27947e04ce37d279f568350506e4060e310a14131d3f69", + "masked_value": "17560205508454890368" + }, + "target_key": "eec9700ee08358842e16d43fe3df6e346c163b7f6007de4fcf3bafc954847174", + "public_key": "3209d365b449b577721430d6e0534f5a188dc4bdcefa02be2eeef45b2925bc1b", + "e_fog_hint": "ae39a969db8ef10daa4f70fa4859829e294ec704b0eb0a15f43ae91bb62bd9ff58ba622e5820b5cdfe28dde6306a6941d538d14c807f9045504619acaafbb684f2040107eb6868c8c99943d02077fa2d090d0100" + }, + "subaddress_index": "0", + "key_image": "2a14381de88c3fe2b827f6adaa771f620873009f55cc7743dca676b188508605", + "value": "1", + "attempted_spend_height": "0", + "attempted_spend_tombstone": "0", + "monitor_id": "" + }, + { + "tx_out": { + "amount": { + "commitment": "8ccbeaf28bad17ac6c64940aab010fedfdd44fb43c50c594c8fa6e8574b9b147", + "masked_value": "8257145351360856463" + }, + "target_key": "2c73db6b914847d124a93691884d2fb181dfcf4d9182686e53c0464cf1c9a711", + "public_key": "ce43370def13a97830cf6e2e73020b5190d673bd75e0692cd18c850030cc3f06", + "e_fog_hint": "6b24ceb038ed5c31bfa8f69c73be59eca46612ba8bfea7f53bc52c97cdf549c419fa5a0b2219b1434848197fdbac7880b3a20d92c59c67ec570c7d60e263b4c7c61164f0517c8f774321435c3ec600593d610100" + }, + "subaddress_index": "0", + "key_image": "a66fa1c3c35e2c2a56109a901bffddc1129625e4c4b381389f6be1b5bb3c7056", + "value": "97580449900010990", + "attempted_spend_height": "0", + "attempted_spend_tombstone": "0", + "monitor_id": "" + } + ], + "outlay_list": [ + { + "value": "42000000000000", + "receiver": { + "view_public_key": "5c04cc0de88725f811625b56844aacd789815d43d6df30354939aafd6e683d1a", + "spend_public_key": "aaf2937c73ef657a529d0f10aaaba394f41bf6f67d8da5ae13284afdb5bc657b", + "fog_report_url": "", + "fog_authority_fingerprint_sig": "", + "fog_report_id": "" + } + } + ], + "tx": { + "prefix": { + "inputs": [ + { + "ring": [ + { + "amount": { + "commitment": "3c90eb914a5fe5eb11fab745c9bebfd988de71fa777521099bd442d0eecb765a", + "masked_value": "5446626203987095523" + }, + "target_key": "f23c5dd112e5f453cf896294be705f52ee90e3cd15da5ea29a0ca0be410a592b", + "public_key": "084c6c6861146672eb2929a0dfc9b9087a49b6531964ca1892602a4e4d2b6d59", + "e_fog_hint": "000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" + }, + ... + ], + "proofs": [ + { + "index": "24296", + "highest_index": "335531", + "elements": [ + { + "range": { + "from": "24296", + "to": "24296" + }, + "hash": "f7217a219665b1dfa3f216191de1c79e7d62f520e83afe256b6b43c64ead7d3f" + }, + } + ... + ] + }, + ... + ] + }, + { + "ring": [ + { + "amount": { + "commitment": "50b46eef8d223824f87316e6f446d50530929c8a758195005fbe9d41ec7fc227", + "masked_value": "11687342289991185016" + }, + "target_key": "241d533daf32ed1523561c96c618808a2db9635075776ef42da32b34e7586058", + "public_key": "24725d8e47e4b03f6cb893369cc7582ea565dbd5e1914a5ecb3f4ed7910c5a03", + "e_fog_hint": "3fba73a6271141aae115148196ad59412b4d703847e0738c460c4d1831c6d44004c4deee4fabf6407c5f801703a31a13f1c70ed18a43a0d0a071b863a529dfbab51634fdf127ba2e7a7d426731ba59dbe3660100" + }, + ... + ], + "proofs": [ + { + "index": "173379", + "highest_index": "335531", + "elements": [ + { + "range": { + "from": "173379", + "to": "173379" + }, + "hash": "bcb26ff5d1104b8c0d7c9aed9b326c824151461257737e0fc4533d1a39e3a876" + }, + ... + ] + }, + ... + ] + } + ], + "outputs": [ + { + "amount": { + "commitment": "147113bbd5d4fdc5f9266ccdec6d6e6148e8dbc979d7d3bab1a91e99ab256518", + "masked_value": "3431426060591787774" + }, + "target_key": "2c6a9c23810e91d8c504dd4fe59f07c2872a8a866c160a58928750eab7328c64", + "public_key": "0049281368c270eb5a7291fb012e95e776a07c1ff4336be1aa6a61abb1868229", + "e_fog_hint": "eb5b104677df5bbc22f70027646a448dcffb61eb31580d50f41cb487a87a9545d507d4c5e13a22f7fe3b2daea3f951b8d9901e73794d24650176faca3251dd904d7cac97ee73f50a84701cb4c297b31cbdf80100" + }, + { + "amount": { + "commitment": "78083af2c1682f765c332c1c69af4260a410914962bddb9a30857a36aed75837", + "masked_value": "17824177895224156943" + }, + "target_key": "68a193eeb7614e3dec6e980dfab2b14aa9b2c3dcaaf1c52b077fbbf259081d36", + "public_key": "6cdfd36e11042adf904d89bcf9b2eba950ad25f48ed6e877589c40caa1a0d50d", + "e_fog_hint": "c0c9fe3a43e237ad2f4ab055532831b95f82141c69c75bc6e913d0f37633cb224ce162e59240ffab51054b13e451bfeccb5a09fa5bfbd477c5a8e809297a38a0cb5233cc5d875067cbd832947ae48555fbc00100" + } + ], + "fee": "10000000000", + "tombstone_block": "0" + }, + "signature": { + "ring_signatures": [ + { + "c_zero": "27a97dbbcf36257b31a1d64a6d133a5c246748c29e839c0f1661702a07a4960f", + "responses": [ + "bc703776fd8b6b1daadf7e4df7ca4cb5df2d6498a55e8ff15a4bceb0e808ca06", + ... + ], + "key_image": "a66fa1c3c35e2c2a56109a901bffddc1129625e4c4b381389f6be1b5bb3c7056" + }, + { + "c_zero": "421cc5527eae6519a8f20871996db99ffd91522ae7ed34e401249e262dfb2702", + "responses": [ + "322852fd40d5bbd0113a6e56d8d6692200bcedbc4a7f32d9911fae2e5170c50e", + ... + ], + "key_image": "2a14381de88c3fe2b827f6adaa771f620873009f55cc7743dca676b188508605" + } + ], + "pseudo_output_commitments": [ + "1a79f311e74027bdc11fb479ce3a5c8feed6794da40e6ccbe45d3931cb4a3239", + "5c3406600fbf8e93dbf5b7268dfc43273f93396b2d4976b73cb935d5619aed7a" + ], + "range_proofs": [ + ... + ] + } + }, + "fee": "10000000000", + "outlay_index_to_tx_out_index": [ + [ + "0", + "0" + ] + ], + "outlay_confirmation_numbers": [ + [...] + ] + }, + "account_id": "a8c9c7acb96cf4ad9154eec9384c09f2c75a340b441924847fe5f60a41805bde" + }, + "jsonrpc": "2.0", + "id": 1 +} +``` +{% endtab %} + +{% tab title="Response" %} +``` +{ + "method": "submit_transaction", + "result": { + "transaction_log": { + "object": "transaction_log", + "transaction_log_id": "ab447d73553309ccaf60aedc1eaa67b47f65bee504872e4358682d76df486a87", + "direction": "tx_direction_sent", + "is_sent_recovered": null, + "account_id": "a8c9c7acb96cf4ad9154eec9384c09f2c75a340b441924847fe5f60a41805bde", + "recipient_address_id": "CaE5bdbQxLG2BqAYAz84mhND79iBSs13ycQqN8oZKZtHdr6KNr1DzoX93c6LQWYHEi5b7YLiJXcTRzqhDFB563Kr1uxD6iwERFbw7KLWA6", + "assigned_address_id": null, + "value_pmob": "42000000000000", + "fee_pmob": "10000000000", + "submitted_block_index": "152950", + "finalized_block_index": null, + "status": "tx_status_pending", + "input_txo_ids": [ + "eb735cafa6d8b14a69361cc05cb3a5970752d27d1265a1ffdfd22c0171c2b20d" + ], + "output_txo_ids": [ + "fd39b4e740cb302edf5da89c22c20bea0e4408df40e31c1dbb2ec0055435861c" + ], + "change_txo_ids": [ + "bcb45b4fab868324003631b6490a0bf46aaf37078a8d366b490437513c6786e4" + ], + "sent_time": "2021-02-28 01:42:28 UTC", + "comment": "", + "failure_code": null, + "failure_message": null, + "offset_count": 2252 + } + }, + "error": null, + "jsonrpc": "2.0", + "id": 1, +} +``` +{% endtab %} +{% endtabs %} + +### Submit without Log + +{% tabs %} +{% tab title="Request Body" %} +``` +{ + "method": "submit_transaction", + "params": { + "tx_proposal": '$(cat test-tx-proposal.json)' + }, + "jsonrpc": "2.0", + "id": 1 +} +``` +{% endtab %} + +{% tab title="Response" %} +``` +{ + "method": "submit_transaction", + "result": { + "transaction_log": null + }, + "error": null, + "jsonrpc": "2.0", + "id": 1 +} +``` +{% endtab %} +{% endtabs %} diff --git a/docs/v2/api-endpoints/sync_view_only_account.md b/docs/v2/api-endpoints/sync_view_only_account.md new file mode 100644 index 000000000..bb4a62c36 --- /dev/null +++ b/docs/v2/api-endpoints/sync_view_only_account.md @@ -0,0 +1,40 @@ +# Sync View Only Account + +## [Request](../../../full-service/src/json_rpc/v2/api/request.rs#L40) + +| Required Param | Purpose | Requirements | +| :--- | :--- | :--- | +| `account_id` | The account on which to perform this action. | Account must exist in the wallet as a view only account. | +| `completed_txos` | signed txos. A array of tuples (txoID, KeyImage) | | +| `next_subaddress_index` | The updated next subaddress index to assign for this account | | + +## [Response](../../../full-service/src/json_rpc/v2/api/response.rs#L41) + +## Example + +{% tabs %} +{% tab title="Request" %} +``` +{ + "method": "sync_view_only_account", + "params": { + "account_id": "f85920dd83f69d8850799e28240e3d395f0ad46dec2561b71f4614dd90a3edb5", + "completed_txos": "[(asdasedeerwe..., sadjashdoauihdkahwk...)]", + "next_subaddress_index": "3" + }, + "jsonrpc": "2.0", + "id": 1 +} +``` +{% endtab %} + +{% tab title="Response" %} +``` +{ + "method": "sync_view_only_account", + "jsonrpc": "2.0", + "id": 1 +} +``` +{% endtab %} +{% endtabs %} diff --git a/docs/v2/api-endpoints/update_account_name.md b/docs/v2/api-endpoints/update_account_name.md new file mode 100644 index 000000000..612e26a7e --- /dev/null +++ b/docs/v2/api-endpoints/update_account_name.md @@ -0,0 +1,55 @@ +--- +description: Rename an account. +--- + +# Update Account Name + +## [Request](../../../full-service/src/json_rpc/v2/api/request.rs#L40) + +| Required Param | Purpose | Requirements | +| :--- | :--- | :--- | +| `account_id` | The account on which to perform this action. | Account must exist in the wallet. | +| `name` | The new name for this account. | | + +## [Response](../../../full-service/src/json_rpc/v2/api/response.rs#L41) + +## Example + +{% tabs %} +{% tab title="Request Body" %} +```text +{ + "method": "update_account_name", + "params": { + "acount_id": "3407fbbc250799f5ce9089658380c5fe152403643a525f581f359917d8d59d52", + "name": "Carol" + }, + "jsonrpc": "2.0", + "id": 1 +} +``` +{% endtab %} + +{% tab title="Response" %} +```text +{ + "method": "update_account_name", + "result": { + "account": { + "account_id": "3407fbbc250799f5ce9089658380c5fe152403643a525f581f359917d8d59d52", + "main_address": "7JvajhkAZYGmrpCY7ZpEiXRK5yW1ooTV7EWfDNu3Eyt572mH1wNb37BWiU6JqRUvgopPqSVZRexhXXpjF3wqLQR7HaJrcdbHmULujgFmzav", + "name": "Carol", + "next_subaddress_index": "2", + "first_block_index": "3500", + "object": "account", + "recovery_mode": false + } + }, + "error": null, + "jsonrpc": "2.0", + "id": 1 +} +``` +{% endtab %} +{% endtabs %} + diff --git a/docs/v2/api-endpoints/validate_confirmation.md b/docs/v2/api-endpoints/validate_confirmation.md new file mode 100644 index 000000000..e5ffae2b1 --- /dev/null +++ b/docs/v2/api-endpoints/validate_confirmation.md @@ -0,0 +1,47 @@ +# Validate Confirmations + +A sender can provide the confirmation numbers from a transaction to the recipient, who then verifies for a specific TXO ID \(note that TXO ID is specific to the TXO, and is consistent across wallets. Therefore the sender and receiver will have the same TXO ID for the same TXO which was minted by the sender, and received by the receiver\). + +## [Request](../../../full-service/src/json_rpc/v2/api/request.rs#L40) + +| Param | Description | | +| :--- | :--- | :--- | +| `account_id` | The account on which to perform this action. | Account must exist in the wallet. | +| `txo_id` | The ID of the TXO for which to validate the confirmation number. | TXO must be a received TXO. | +| `confirmation` | The confirmation number to validate. | The confirmation number should be delivered by the sender of the Txo in question. | + +## [Response](../../../full-service/src/json_rpc/v2/api/response.rs#L41) + +## Example + +{% tabs %} +{% tab title="Request Body" %} +```text +{ + "method": "validate_confirmation", + "params": { + "account_id": "4b4fd11738c03bf5179781aeb27d725002fb67d8a99992920d3654ac00ee1a2c", + "txo_id": "bbee8b70e80837fc3e10bde47f63de41768ee036263907325ef9a8d45d851f15", + "confirmation": "0a2005ba1d9d871c7fb0d5ba7df17391a1e14aad1b4aa2319c997538f8e338a670bb" + }, + "jsonrpc": "2.0", + "id": 1 +} +``` +{% endtab %} + +{% tab title="Response" %} +```text +{ + "method": "validate_confirmation", + "result": { + "validated": true + }, + "error": null, + "jsonrpc": "2.0", + "id": 1, +} +``` +{% endtab %} +{% endtabs %} + diff --git a/docs/v2/api-endpoints/verify_address.md b/docs/v2/api-endpoints/verify_address.md new file mode 100644 index 000000000..77a25a728 --- /dev/null +++ b/docs/v2/api-endpoints/verify_address.md @@ -0,0 +1,45 @@ +--- +description: Verify whether an address is correctly b58-encoded. +--- + +# Verify Address + +## [Request](../../../full-service/src/json_rpc/v2/api/request.rs#L40) + +| Required Param | Purpose | Requirements | +| :--- | :--- | :--- | +| `address` | The address on which to perform this action. | Address must be assigned for an account in the wallet. | + +## [Response](../../../full-service/src/json_rpc/v2/api/response.rs#L41) + +## Example + +{% tabs %} +{% tab title="Request Body" %} +```text +{ + "method": "verify_address", + "params": { + "address": "CaE5bdbQxLG2BqAYAz84mhND79iBSs13ycQqN8oZKZtHdr6KNr1DzoX93c6LQWYHEi5b7YLiJXcTRzqhDFB563Kr1uxD6iwERFbw7KLWA6", + }, + "jsonrpc": "2.0", + "id": 1 +} +``` +{% endtab %} + +{% tab title="Response" %} +```text +{ + "method": "verify_address", + "result": { + "verified": true + }, + "error": null, + "jsonrpc": "2.0", + "id": 1, +} +``` +{% endtab %} +{% endtabs %} + diff --git a/docs/v2/api-endpoints/version.md b/docs/v2/api-endpoints/version.md new file mode 100644 index 000000000..a567443ce --- /dev/null +++ b/docs/v2/api-endpoints/version.md @@ -0,0 +1,40 @@ +--- +description: 'Get the version number of the software.' +--- + +# Get Version Number + +## [Request](../../../full-service/src/json_rpc/v2/api/request.rs#L40) + +## [Response](../../../full-service/src/json_rpc/v2/api/response.rs#L41) + +## Example + +{% tabs %} +{% tab title="Request Body" %} +```text +{ + "method": "version", + "jsonrpc": "2.0", + "api_version": "2", + "id": 1 +} +``` +{% endtab %} + +{% tab title="Response" %} +```text +{ + "method": "version", + "result": { + "string": "1.6.0", + "number": ["1", "6", "0", ""], + "commit": "282982fb295dbe0bf6f9df829471055f02606f10" + }, + "jsonrpc": "2.0", + "id": 1 +} +``` +{% endtab %} +{% endtabs %} + diff --git a/docs/v2/other/block/README.md b/docs/v2/other/block/README.md new file mode 100644 index 000000000..12c1f2526 --- /dev/null +++ b/docs/v2/other/block/README.md @@ -0,0 +1,16 @@ +--- +description: >- + The Block is an important primitive in the MobileCoin blockchain, and consists + of TXOs and Key Images. +--- + +# Block + +## Attributes + +| _Name_ | _Type_ | _Description_ | +| :--- | :--- | :--- | +| `object` | string, value is "block" | String representing the object's type. Objects of the same type share the same value. | +| `block` | JSON object | Contains the block header information for the block | +| `block_contents` | JSON object | Contains the key\_images and TXOs \(outputs\) for the block. | + diff --git a/docs/v2/other/network-status/README.md b/docs/v2/other/network-status/README.md new file mode 100644 index 000000000..1d87bca17 --- /dev/null +++ b/docs/v2/other/network-status/README.md @@ -0,0 +1,14 @@ +--- +description: The current network fee and total number of blocks. +--- + +# Network Status + +## Attributes + +| _Name_ | _Type_ | _Description_ | +| :--- | :--- | :--- | +| `network_block_height` | string \(string\) | The block count of MobileCoin's distributed ledger. | +| `local_block_height` | string \(string\) | The local block count downloaded from the ledger. The local database is synced when the `local_block_height` reaches the `network_block_height`. | +| `fees` | Map \(string, string\) | Default fee for each token required to send a transaction. | +| `block_version` | string \(optional\) | The current block version of MobileCoin's blockchain. | diff --git a/docs/v2/other/version/README.md b/docs/v2/other/version/README.md new file mode 100644 index 000000000..2428efbe5 --- /dev/null +++ b/docs/v2/other/version/README.md @@ -0,0 +1,15 @@ +--- +description: 'The version number of the full-service software.' +--- + +# Version Number + +## Attributes + +| _Name_ | _Type_ | _Description_ | +| :--- | :--- | :--- | +| `object` | string, value is "version" | String representing the object's type. Objects of the same type share the same value. | + +| `string` | string | The version number as a string. | +| `number` | \[string \(integer\)\] | An array of four numbers representing the major, minor, patch, and prerelease version numbers. | +| `commit` | string | The commit hash of the current version. | diff --git a/docs/v2/other/wallet-status/README.md b/docs/v2/other/wallet-status/README.md new file mode 100644 index 000000000..f16eee28b --- /dev/null +++ b/docs/v2/other/wallet-status/README.md @@ -0,0 +1,72 @@ +--- +description: The Wallet Status provides a quick overview of the contents of the wallet. Note that pmob calculations do not include view-only-accounts +--- + +# Wallet Status + +## Attributes + +| _Name_ | _Type_ | _Description_ | +| :--- | :--- | :--- | +| `network_block_height` | string \(uint64\) | The block count of MobileCoin's distributed ledger. | +| `local_block_height` | string \(uint64\) | The local block count downloaded from the ledger. The local database is synced when the `local_block_height` reaches the `network_block_height`. The account_block_height can only sync up to `local_block_height`. | +| `is_synced_all` | Boolean | Whether ALL accounts are synced with the `network_block_height`. Balances may not appear correct if any account is still syncing. | +| `balance_per_token` | map \(string, Balance\) | Map of balances for each token that is present in the wallet | +| `account_ids` | list | A list of all `account_ids` imported into the wallet in order of import. | +| `account_map` | hash map | A normalized hash mapping `account_id` to account objects. | + +## ​Example + +```text +{ +"wallet_status": { + "account_ids": [ + "b0be5377a2f45b1573586ed530b2901a559d9952ea8a02f8c2dbb033a935ac17", + "6ed6b79004032fcfcfa65fa7a307dd004b8ec4ed77660d36d44b67452f62b470" + ], + "account_map": { + "6ed6b79004032fcfcfa65fa7a307dd004b8ec4ed77660d36d44b67452f62b470": { + "account_id": "6ed6b79004032fcfcfa65fa7a307dd004b8ec4ed77660d36d44b67452f62b470", + "key_derivation_version:": "2", + "main_address": "CaE5bdbQxLG2BqAYAz84mhND79iBSs13ycQqN8oZKZtHdr6KNr1DzoX93c6LQWYHEi5b7YLiJXcTRzqhDFB563Kr1uxD6iwERFbw7KLWA6", + "name": "Bob", + "next_subaddress_index": "2", + "first_block_index": "3500", + "object": "account", + "recovery_mode": false + }, + "b0be5377a2f45b1573586ed530b2901a559d9952ea8a02f8c2dbb033a935ac17": { + "account_id": "b0be5377a2f45b1573586ed530b2901a559d9952ea8a02f8c2dbb033a935ac17", + "key_derivation_version:": "2", + "main_address": "7JvajhkAZYGmrpCY7ZpEiXRK5yW1ooTV7EWfDNu3Eyt572mH1wNb37BWiU6JqRUvgopPqSVZRexhXXpjF3wqLQR7HaJrcdbHmULujgFmzav", + "name": "Brady", + "next_subaddress_index": "2", + "first_block_index": "3500", + "object": "account", + "recovery_mode": false + } + }, + "is_synced_all": false, + "local_block_height": "152918", + "network_block_height": "152918", + "balance_per_token": { + "0": { + "orphaned": "0", + "pending": "70148220000000000", + "secreted": "0", + "spent": "0", + "unspent": "220588320000000000", + "unverified": "1300004044440000" + }, + "1": { + "orphaned": "0", + "pending": "70148220000000000", + "secreted": "0", + "spent": "0", + "unspent": "220588320000000000", + "unverified": "1300004044440000" + } + } +} +``` + diff --git a/docs/v2/transactions/payment-request/README.md b/docs/v2/transactions/payment-request/README.md new file mode 100644 index 000000000..3bb0838f2 --- /dev/null +++ b/docs/v2/transactions/payment-request/README.md @@ -0,0 +1,6 @@ +--- +description: A payment request +--- + +# Payment Request + diff --git a/docs/v2/transactions/transaction-confirmation/README.md b/docs/v2/transactions/transaction-confirmation/README.md new file mode 100644 index 000000000..6aacb3fc1 --- /dev/null +++ b/docs/v2/transactions/transaction-confirmation/README.md @@ -0,0 +1,29 @@ +--- +description: >- + When constructing a transaction, the wallet produces a "confirmation number" + for each TXO minted by the transaction. +--- + +# Confirmation + +The confirmation number can be delivered to the recipient to prove that they received the TXO from that particular sender. + +## Attributes + +| _Name_ | _Type_ | _Description_ | +| :--- | :--- | :--- | +| `txo_id` | string | Unique identifier for the TXO. | +| `txo_index` | string | The index of the TXO in the ledger. | +| `confirmation` | string | A string with a confirmation number that can be validated to confirm that another party constructed or had knowledge of the construction of the associated TXO. | + +## Example + +```text +{ + "object": "confirmation", + "txo_id": "873dfb8c...", + "txo_index": "1276", + "confirmation": "984eacd..." +} +``` + diff --git a/docs/v2/transactions/transaction-log/README.md b/docs/v2/transactions/transaction-log/README.md new file mode 100644 index 000000000..3fdb210c4 --- /dev/null +++ b/docs/v2/transactions/transaction-log/README.md @@ -0,0 +1,188 @@ +--- +description: >- + A Transaction Log is a record of a MobileCoin transaction that was constructed + and sent from this wallet. +--- + +# Transaction Log + +Due to the privacy properties of the MobileCoin ledger, transactions are ephemeral. Once they have been created, they only exist until they are validated, and then only the outputs are written to the ledger. For this reason, the Full-service Wallet stores transactions in the `transaction_log` table in order to preserve transaction history. + +## Attributes + +| _Name_ | _Type_ | _Description_ | +| :--- | :--- | :--- | +| `id` | integer | Unique identifier for the transaction log. This value is not associated to the ledger. | +| `account_id` | string | Unique identifier for the assigned associated account. If the transaction is outgoing, this account is from whence the TXO came. If received, this is the receiving account. | +| `value_map` | map \(string, uint64\) | Total value per token associated to this transaction log. | +| `fee_value` | string \(uint64\) | Fee value associated to this transaction log. | +| `fee_token_id` | string \(uint64\) | Fee token id associated to this transaction log. | +| `submitted_block_index` | string \(uint64\) | The block index of the highest block on the network at the time the transaction was submitted. | +| `tombstone_block_index` | string \(uint64\) | The tombstone block index. | +| `finalized_block_index` | string \(uint64\) | The scanned block block index in which this transaction occurred. | +| `status` | string | String representing the transaction log status. Valid statuses are "built", "pending", "succeeded", "failed". | +| `input_txos` | \[InputTxo\] | A list of the TXOs which were inputs to this transaction. | +| `payload_txos` | \[OutputTxo\] | A list of the TXOs which were payloads of this transaction. | +| `change_txos` | \[OutputTxo\] | A list of the TXOs which were change in this transaction. | +| `sent_time` | Timestamp | Time at which sent transaction log was created. Only available if direction is "sent". This value is null if "received" or if the sent transactions were recovered from the ledger \(`is_sent_recovered = true`\). | +| `comment` | string | An arbitrary string attached to the object. | +| `failure_code` | integer | Code representing the cause of "failed" status. | +| `failure_message` | string | Human parsable explanation of "failed" status. | + +# Input Txo + +## Attributes + +| _Name_ | _Type_ | _Description_ | +| :--- | :--- | :--- | +| `id` | string | Unique identifier for the txo. | +| `value` | string \(uint64\) | Value of this txo. | +| `token_id` | string \(uint64\) | Token id of this txo. | + +# Output Txo + +## Attributes + +| _Name_ | _Type_ | _Description_ | +| :--- | :--- | :--- | +| `id` | string | Unique identifier for the txo. | +| `value` | string \(uint64\) | Value of this txo. | +| `token_id` | string \(uint64\) | Token id of this txo. | +| `recipient_public_address_b58` | string | Public address b58 of the recipient of this txo. | + +## Example + +{% tabs %} +{% tab title="Sent - Pending" %} +```text +{ + "id": "ab447d73553309ccaf60aedc1eaa67b47f65bee504872e4358682d76df486a87", + "account_id": "a8c9c7acb96cf4ad9154eec9384c09f2c75a340b441924847fe5f60a41805bde", + "value_map": { + "0": "42000000000000" + }, + "fee_value": "10000000000", + "fee_token_id": "0", + "submitted_block_index": "152950", + "finalized_block_index": null, + "status": "pending", + "input_txos": [ + { + "id": "eb735cafa6d8b14a69361cc05cb3a5970752d27d1265a1ffdfd22c0171c2b20d", + "value": "50000000000", + "token_id": "0" + } + ], + "payload_txos": [ + { + "id": "fd39b4e740cb302edf5da89c22c20bea0e4408df40e31c1dbb2ec0055435861c", + "value": "30000000000", + "token_id": "0" + "recipient_public_address_b58": "vrewh94jfm43m430nmv2084j3k230j3mfm4i3mv39nffrwv43" + } + ], + "change_txos": [ + { + "id": "bcb45b4fab868324003631b6490a0bf46aaf37078a8d366b490437513c6786e4", + "value": "10000000000", + "token_id": "0" + "recipient_public_address_b58": "grewmvn3990435vm032492v43mgkvocdajcl2icas" + } + ], + "sent_time": "2021-02-28 01:42:28 UTC", + "comment": "", + "failure_code": null, + "failure_message": null +} +``` +{% endtab %} + +{% tab title="Sent - Failed" %} +```text +{ + "id": "ab447d73553309ccaf60aedc1eaa67b47f65bee504872e4358682d76df486a87", + "account_id": "a8c9c7acb96cf4ad9154eec9384c09f2c75a340b441924847fe5f60a41805bde", + "value_map": { + "0": "42000000000000" + }, + "fee_value": "10000000000", + "fee_token_id": "0", + "submitted_block_index": "152950", + "finalized_block_index": null, + "status": "failed", + "input_txos": [ + { + "id": "eb735cafa6d8b14a69361cc05cb3a5970752d27d1265a1ffdfd22c0171c2b20d", + "value": "50000000000", + "token_id": "0" + } + ], + "payload_txos": [ + { + "id": "fd39b4e740cb302edf5da89c22c20bea0e4408df40e31c1dbb2ec0055435861c", + "value": "30000000000", + "token_id": "0" + "recipient_public_address_b58": "vrewh94jfm43m430nmv2084j3k230j3mfm4i3mv39nffrwv43" + } + ], + "change_txos": [ + { + "id": "bcb45b4fab868324003631b6490a0bf46aaf37078a8d366b490437513c6786e4", + "value": "10000000000", + "token_id": "0" + "recipient_public_address_b58": "grewmvn3990435vm032492v43mgkvocdajcl2icas" + } + ], + "sent_time": "2021-02-28 01:42:28 UTC", + "comment": "This is an example of a failed sent transaction log of 1.288 MOB and 0.01 MOB fee!", + "failure_code": 3, + "failure_message:": "Contains spent key image." +} +``` +{% endtab %} + +{% tab title="Sent - Success" %} +```text +{ + "id": "ab447d73553309ccaf60aedc1eaa67b47f65bee504872e4358682d76df486a87", + "account_id": "a8c9c7acb96cf4ad9154eec9384c09f2c75a340b441924847fe5f60a41805bde", + "value_map": { + "0": "42000000000000" + }, + "fee_value": "10000000000", + "fee_token_id": "0", + "submitted_block_index": "152950", + "finalized_block_index": "152951", + "status": "succeeded", + "input_txos": [ + { + "id": "eb735cafa6d8b14a69361cc05cb3a5970752d27d1265a1ffdfd22c0171c2b20d", + "value": "50000000000", + "token_id": "0" + } + ], + "payload_txos": [ + { + "id": "fd39b4e740cb302edf5da89c22c20bea0e4408df40e31c1dbb2ec0055435861c", + "value": "30000000000", + "token_id": "0" + "recipient_public_address_b58": "vrewh94jfm43m430nmv2084j3k230j3mfm4i3mv39nffrwv43" + } + ], + "change_txos": [ + { + "id": "bcb45b4fab868324003631b6490a0bf46aaf37078a8d366b490437513c6786e4", + "value": "10000000000", + "token_id": "0" + "recipient_public_address_b58": "grewmvn3990435vm032492v43mgkvocdajcl2icas" + } + ], + "sent_time": "2021-02-28 01:42:28 UTC", + "comment": "", + "failure_code": null, + "failure_message": null +} +``` +{% endtab %} +{% endtabs %} + diff --git a/docs/v2/transactions/transaction-receipt/README.md b/docs/v2/transactions/transaction-receipt/README.md new file mode 100644 index 000000000..48187ee26 --- /dev/null +++ b/docs/v2/transactions/transaction-receipt/README.md @@ -0,0 +1,33 @@ +--- +description: >- + A receiver receipt contains the confirmation number and recipients can poll + the receiver receipt for the status of the transaction. +--- + +# Receiver Receipt + +## Attributes + +| _Name_ | _Type_ | _Description_ | +| :--- | :--- | :--- | +| `public_key` | string | Hex-encoded public key for the TXO. | +| `tombstone_block` | string | The block index after which this TXO would be rejected by consensus. | +| `confirmation` | string | Hex-encoded confirmation that can be validated to confirm that another party constructed or had knowledge of the construction of the associated TXO. | +| `masked_amount` | MasketAmount | The encrypted amount in the TXO referenced by this receipt. | + +## Example + +```text +{ + "object": "receiver_receipt", + "public_key": "0a20d2118a065192f11e228e0fce39e90a878b5aa628b7613a4556c193461ebd4f67", + "confirmation": "0a205e5ca2fa40f837d7aff6d37e9314329d21bad03d5fac2ec1fc844a09368c33e5", + "tombstone_block": "154512", + "amount": { + "commitment": "782c575ed7d893245d10d7dd49dcffc3515a7ed252bcade74e719a17d639092d", + "masked_value": "12052895925511073331", + "masked_token_id": "123589105786482", + } +} +``` + diff --git a/docs/v2/transactions/transaction/README.md b/docs/v2/transactions/transaction/README.md new file mode 100644 index 000000000..9e4688e3e --- /dev/null +++ b/docs/v2/transactions/transaction/README.md @@ -0,0 +1,42 @@ +--- +description: >- + A MobileCoin Transaction consists of inputs which are spent in order to mint + new outputs for the recipient. +--- + +# Transaction + +Due to the privacy properties of the MobileCoin ledger, transactions are ephemeral. Once they have been created, they only exist until they are validated, and then only the outputs are written to the ledger. For this reason, the Full Service wallet stores transactions in the `transaction_log` table in order to preserve transaction history. + +## Attributes + +### Transaction Proposal + +| Name | Type | Description | +| :--- | :--- | :--- | +| `input_txos` | [InputTxo] | The collection of txos used as inputs | +| `payload_txos` | [OutputTxo] | The collection of txos used as payload outputs | +| `change_txos` | [OutputTxo] | The collection of txos used as change outputs | +| `fee` | string | Fee for this transaction | +| `fee_token_id` | string | TokenId of the fee for this transaction | +| `tombstone_block_index` | string | The tombstone block index of this transaction | +| `tx_proto` | string | The protobuff encoded data of the transaction that can be submitted to the mobilecoin network | + +### InputTxo + +| Name | Type | Description | +| :--- | :--- | :--- | +| `tx_out_proto` | string | Unique identifier for the txo | +| `value` | string | The value of this txo | +| `token_id` | string | The tokenId of this txo | +| `key_image` | string | The key image of this txo | + +### OutputTxo + +| Name | Type | Description | +| :--- | :--- | :--- | +| `tx_out_proto` | string | Unique identifier for the txo | +| `value` | string | The value of this txo | +| `token_id` | string | The tokenId of this txo | +| `recipient_public_address_b58` | string | The recipient that this txo belongs to | +| `confirmation_number` | string | The confirmation number of the txo that can be used to validate it by the recipient | diff --git a/docs/v2/transactions/txo/README.md b/docs/v2/transactions/txo/README.md new file mode 100644 index 000000000..4f7c78dc3 --- /dev/null +++ b/docs/v2/transactions/txo/README.md @@ -0,0 +1,106 @@ +--- +description: >- + A TXO is a "Transaction Output." MobileCoin is a ledger built on the "Unspent + Transaction Output" model (UTXO). +--- + +# Transaction Output TXO + +In order to construct a transaction, the wallet will select "Unspent Transaction Outputs" and perform a cryptographic operation to mark them as "spent" in the ledger. Then, it will mint new TXOs for the recipient. + +## Attributes + +| _Name_ | _Type_ | _Description_ | +| :--- | :--- | :--- | +| `id` | string | | +| `value` | string \(uint64\) | Available value for this account at the current `account_block_height`. If the account is syncing, this value may change. | +| `token_id` | string \(uint64\) | | +| `received_block_index` | string \(uint64\) | Block index in which the TXO was received by an account. | +| `spent_block_index` | string \(uint64\) | Block index in which the TXO was spent by an account. | +| `account_id` | string | The `account_id` for the account which has received this TXO. This account has spend authority. | +| `status` | string \(enum\) | With respect to this account, the TXO may be "unverified", "unspent", "pending", "spent", "secreted" or "orphaned". For received TXOs received as an assigned address, the lifecycle is "unspent" -> "pending" -> "spent", the TXO is considered "orphaned" until its address is calculated -- in this case, there are manual ways to discover the missing assigned address for orphaned TXOs or to recover an entire account. | +| `target_key` | string \(hex\) | A cryptographic key for this TXO. | +| `public_key` | string \(hex\) | The public key for this TXO, can be used as an identifier to find the TXO in the ledger. | +| `e_fog_hint` | string \(hex\) | The encrypted fog hint for this TXO. | +| `subaddress_index` | string \(uint64\) | The assigned subaddress index for this TXO with respect to its received account. | +| `key_image` \(only on pending/spent\) | string \(hex\) | A fingerprint of the TXO derived from your private spend key materials, required to spend a TXO | +| `confirmation` | string \(hex\) | A confirmation that the sender of the TXO can provide to validate that they participated in the construction of this TXO. | + +## Example + +### Received and Spent TXO + +```text +{ + "object": "txo", + "txo_id": "14ad2f88...", + "value_pmob": "8500000000000", + "received_block_index": "14152", + "spent_block_index": "20982", + "is_spent_recovered": false, + "received_account_id": "1916a9b3...", + "minted_account_id": null, + "account_status_map": { + "1916a9b3...": { + "txo_status": "spent", + "txo_type": "received" + } + }, + "target_key": "6d6f6f6e...", + "public_key": "6f20776f...", + "e_fog_hint": "726c6421...", + "subaddress_index": "20", + "assigned_subaddress": "7BeDc5jpZ...", + "key_image": "6d6f6269...", + "confirmation": "23fd34a..." +} +``` + +### TXO Spent Between Accounts in the Same Wallet + +```text +{ + "object": "txo", + "txo_id": "84f3023...", + "value_pmob": "200", + "received_block_index": null, + "spent_block_index": null, + "is_spent_recovered": false, + "received_account_id": "36fdf8...", + "minted_account_id": "a4db032...", + "account_status_map": { + "36fdf8...": { + "txo_status": "unspent", + "txo_type": "received" + }, + "a4db03...": { + "txo_status": "secreted", + "txo_type": "minted" + } + }, + "target_key": "0a2076...", + "public_key": "0a20e6...", + "e_fog_hint": "0a5472...", + "subaddress_index": null, + "assigned_subaddress": null, + "key_image": null, + "confirmation": "0a2044..." +} +``` + +# View Only Transaction Output ViewOnlyTXO + +a minimal txo entity useful for view-only-accounts + +## Attributes + +| _Name_ | _Type_ | _Description_ | +| :--- | :--- | :--- | +| `object` | string, value is "view_only_txo" | String representing the object's type. Objects of the same type share the same value. | +| `public_key` | string \(hex\) | The public key for this TXO, can be used as an identifier to find the TXO in the ledger. | +| `value_pmob` | string \(uint64\) | Available pico MOB for this account at the current `account_block_height`. If the account is syncing, this value may change. | +| `view_only_account_id_hex` | string | The local ID for view only account that has the private view key capable of decrypting this txo. | +| `spent` | string | Whether or not this txo has been manually marked as spent. | +| `txo_id_hex` | string | A synthetic ID created from properties of the TXO. This will be the same for a given TXO across systems. | + +## Example diff --git a/docs/view-only-accounts/account-secrets/README.md b/docs/view-only-accounts/account-secrets/README.md deleted file mode 100644 index b5ba8649f..000000000 --- a/docs/view-only-accounts/account-secrets/README.md +++ /dev/null @@ -1,26 +0,0 @@ ---- -description: >- - The secret keys for an account. The account secrets are returned separately - from other account information, to enable more careful handling of - cryptographically sensitive information. ---- - -# View Only Account Secrets - -## Attributes - -| Name | Type | Description | -| :--- | :--- | :--- | -| `object` | string, value is "view\_only\_account\_secrets" | String representing the object's type. Objects of the same type share the same value. | -| `view_private_key` | string | The private view key for with this account | - -## Example - -```text -{ - "object": "view_only_account_secrets", - "account_id": "3407fbbc250799f5ce9089658380c5fe152403643a525f581f359917d8d59d52", - "view_private_key": "0a207960bd832aae551ee03d6e5ab48baa229acd7ca4d2c6aaf7c8c4e77ac3e92307", -} -``` - diff --git a/docs/view-only-accounts/account-secrets/export_view_only_account_secrets.md b/docs/view-only-accounts/account-secrets/export_view_only_account_secrets.md deleted file mode 100644 index 7ca11cead..000000000 --- a/docs/view-only-accounts/account-secrets/export_view_only_account_secrets.md +++ /dev/null @@ -1,54 +0,0 @@ ---- -description: >- - Exporting the view private key for a view only account ---- - -# Export Account Secrets - -## Parameters - -| Required Param | Purpose | Requirements | -| :--- | :--- | :--- | -| `account_id` | The account on which to perform this action. | Account must exist in the wallet. | - -## Example - -{% tabs %} -{% tab title="Request Body" %} -```text -{ - "method": "export_view_only_account_secrets", - "params": { - "account_id": "3407fbbc250799f5ce9089658380c5fe152403643a525f581f359917d8d59d52" - }, - "jsonrpc": "2.0", - "id": 1 -} -``` -{% endtab %} - -{% tab title="Response" %} -```text -{ - "method": "export_view_only_account_secrets", - "result": { - "view_only_account_secrets": { - "object": "view_only_account_secrets", - "account_id": "3407fbbc250799f5ce9089658380c5fe152403643a525f581f359917d8d59d52", - "view_private_key": "c0b285cc589447c7d47f3yfdc466e7e946753fd412337bfc1a7008f0184b0479", - } - }, - "error": null, - "jsonrpc": "2.0", - "id": 1, -} -``` -{% endtab %} -{% endtabs %} - -## Outputs - -If the account was generated using version 1 of the key derivation, entropy will be provided as a hex-encoded string. - -If the account was generated using version 2 of the key derivation, mnemonic will be provided as a 24-word mnemonic string. - diff --git a/docs/view-only-accounts/account/README.md b/docs/view-only-accounts/account/README.md deleted file mode 100644 index 9bd4a891a..000000000 --- a/docs/view-only-accounts/account/README.md +++ /dev/null @@ -1,37 +0,0 @@ ---- -description: >- - An account in the wallet. An account is associated with one AccountKey, - containing a View keypair and a Spend keypair. ---- - -# Account - -A view-only-account in the wallet. An view-only-account is associated with one ViewPrivateKey. It can decode txos but it can not decode key images or create txos. - -## Attributes - -| Name | Type | Description | -| ------------------------- | -------------------------------------- | -------------------------------------------------------------------------------------------------------------------- | -| `object` | string, value is "view\_only\_account" | String representing the object's type. Objects of the same type share the same value. | -| `account_id` | string | The unique identifier for the account. | -| `name` | string | The display name for the account. | -| `first_block_index` | string (uint64) | Index of the first block when this account may have received funds. Defaults to 0 if not provided on account import. | -| `next_block_index` | string (uint64) | Index of the next block this account needs to sync. | -| `main_subaddress_index` | string (uint64) | | -| `change_subaddress_index` | string (uint64) | | -| `next_subaddress_index` | string (uint64) | | - -## Example - -``` -{ - "object": "view-only-account", - "account_id": "gdc3fd37f1903aec5a12b12a580eb837e14f87e5936f92a0af4794219f00691d", - "name": "I love MobileCoin", - "first_block_index": "0", - "next_block_index": "3500", - "main_subaddress_index": "0", - "change_subaddress_index": "1", - "next_subaddress_index": "2" -} -``` diff --git a/docs/view-only-accounts/account/get_view_only_account.md b/docs/view-only-accounts/account/get_view_only_account.md deleted file mode 100644 index caccc47fb..000000000 --- a/docs/view-only-accounts/account/get_view_only_account.md +++ /dev/null @@ -1,61 +0,0 @@ ---- -description: Get the details of a given view only account. ---- - -# Get - -## Parameters - -| Required Param | Purpose | Requirements | -| -------------- | ------------------------------------------------------ | --------------------------------- | -| `account_id` | The view only account on which to perform this action. | Account must exist in the wallet. | - -## Example - -{% tabs %} -{% tab title="Request Body" %} -``` -{ - "method": "get_view_only_account", - "params": { - "account_id": "f85920dd83f69d8850799e28240e3d395f0ad46dec2561b71f4614dd90a3edb5" - }, - "jsonrpc": "2.0", - "id": 1 -} -``` -{% endtab %} - -{% tab title="Response" %} -``` -{ - "method": "get_view_only_account", - "result": { - "view_only_account": { - "object": "view_only_account", - "account_id": "f85920dd83f69d8850799e28240e3d395f0ad46dec2561b71f4614dd90a3edb5", - "name": "ts-test-2", - "first_block_index": "0", - "next_block_index": "679739", - "main_subaddress_index": "0", - "change_subaddress_index": "1", - "next_subaddress_index": "2" - } - }, - "jsonrpc": "2.0", - "id": 1 -} -``` -{% endtab %} -{% endtabs %} - -{% hint style="warning" %} -If the account is not in the database, you will receive the following error message: - -``` -{ - "error": "Database(AccountNotFound(\"a4db032dcedc14e39608fe6f26deadf57e306e8c03823b52065724fb4d274c10\"))", - "details": "Error interacting with the database: Account Not Found: a4db032dcedc14e39608fe6f26deadf57e306e8c03823b52065724fb4d274c10" -} -``` -{% endhint %} diff --git a/docs/view-only-accounts/account/import_view_only_account.md b/docs/view-only-accounts/account/import_view_only_account.md deleted file mode 100644 index ab9fd90f0..000000000 --- a/docs/view-only-accounts/account/import_view_only_account.md +++ /dev/null @@ -1,83 +0,0 @@ ---- -description: >- - Create a view-only account by importing the private key from an existing - account. Note: a single wallet cannot have both the regular and view-only versions of an account. ---- - -# Import - -## Parameters - -| Required Param | Purpose | Requirements | -| -------------- | ---------------------------------------------------------------------------------- | ------------ | -| `package` | The view only account import package generated from the offline transaction signer | | - -## Example - -{% tabs %} -{% tab title="Request Body" %} -``` -{ - "method": "import_view_only_account", - "params": { - "account": { - "object": "view_only_account", - "account_id": "f85920dd83f69d8850799e28240e3d395f0ad46dec2561b71f4614dd90a3edb5", - "name": "ts-test-2", - "first_block_index": "0", - "next_block_index": "0", - "main_subaddress_index": "0", - "change_subaddress_index": "1", - "next_subaddress_index": "2" - }, - "secrets": { - "object": "view_only_account_secrets", - "view_private_key": "0a20f6fdc6e12fc60c39fe10be71a0ad7b2e6aaae98d56d59c6a71e3f4043b628b0c", - "account_id": "f85920dd83f69d8850799e28240e3d395f0ad46dec2561b71f4614dd90a3edb5" - }, - "subaddresses": [ - { - "object": "view_only_subaddress", - "public_address": "6MZ9Na9yC6upiE5BSe9gNsBX5zjwjuCASGNGmfvU8cCyWqo6xePySAU84zaMmSi3Zjrt2AKKXPcsy4J1CDmXmoZtFFo9QQ7cgpbUg8opX1y", - "account_id": "f85920dd83f69d8850799e28240e3d395f0ad46dec2561b71f4614dd90a3edb5", - "comment": "Main", - "subaddress_index": "0", - "public_spend_key": "0a208650eb2c525a41bcd88ce47dcf8f657bbe0882461ccace1afbc856e22e929348" - }, - { - "object": "view_only_subaddress", - "public_address": "6QYeh2h5WegDWGqFYgennj8vjaFzaFTmMZo5M84Ntcsnc69mLSdxrReKditxwLedBSktXznUrC4L3Q57vwiFzfHTXB2EgWQU8LHMB4UjBrj", - "account_id": "f85920dd83f69d8850799e28240e3d395f0ad46dec2561b71f4614dd90a3edb5", - "comment": "Change", - "subaddress_index": "1", - "public_spend_key": "0a20ee1adb69b3d6cb3173f712790ff1fe89a1312d678c82cb8e8c940ef9c9e8ed4c" - } - ] - }, - "jsonrpc": "2.0", - "api_version": "2", - "id": 1 -} -``` -{% endtab %} - -{% tab title="Response" %} -``` -{ - "method": "import_view_only_account", - "result": { - "account": { - "object": "view_only_account_account", - "account_id": "6ed6b79004032fcfcfa65fa7a307dd004b8ec4ed77660d36d44b67452f62b470", - "name": "Coins for cats", - "first_block_index": "3500", - "next_block_index": "4000", - } - }, - "error": null, - "jsonrpc": "2.0", - "id": 1, -} -``` -{% endtab %} -{% endtabs %} diff --git a/docs/view-only-accounts/account/update_view_only_account_name.md b/docs/view-only-accounts/account/update_view_only_account_name.md deleted file mode 100644 index 9a67f88b7..000000000 --- a/docs/view-only-accounts/account/update_view_only_account_name.md +++ /dev/null @@ -1,52 +0,0 @@ ---- -description: Rename a view only account. ---- - -# Update Name - -## Parameters - -| Required Param | Purpose | Requirements | -| -------------- | -------------------------------------------- | --------------------------------- | -| `account_id` | The account on which to perform this action. | Account must exist in the wallet. | -| `name` | The new name for this account. | | - -## Example - -{% tabs %} -{% tab title="Request Body" %} -``` -{ - "method": "update_view_only_account_name", - "params": { - "acount_id": "3407fbbc250799f5ce9089658380c5fe152403643a525f581f359917d8d59d52", - "name": "Coins for birds" - }, - "jsonrpc": "2.0", - "id": 1 -} -``` -{% endtab %} - -{% tab title="Response" %} -``` -{ - "method": "update_view_only_account_name", - "result": { - "view_only_account": { - "object": "view_only_account", - "account_id": "f85920dd83f69d8850799e28240e3d395f0ad46dec2561b71f4614dd90a3edb5", - "name": "test-2", - "first_block_index": "0", - "next_block_index": "679741", - "main_subaddress_index": "0", - "change_subaddress_index": "1", - "next_subaddress_index": "2" - } - }, - "jsonrpc": "2.0", - "id": 1 -} -``` -{% endtab %} -{% endtabs %} diff --git a/docs/view-only-accounts/balance/README.md b/docs/view-only-accounts/balance/README.md deleted file mode 100644 index 586bb7d88..000000000 --- a/docs/view-only-accounts/balance/README.md +++ /dev/null @@ -1,32 +0,0 @@ ---- -description: >- - The balance of an account, which includes additional information about the - syncing status needed to interpret the balance correctly. ---- - -# View Only Balance -The balance for a view-only-account. - -## Attributes - -| Name | Type | Description | -| :--- | :--- | :--- | -| `object` | string, value is "balance" | String representing the object's type. Objects of the same type share the same value. | -| `network_block_height` | string \(uint64\) | The block count of MobileCoin's distributed ledger. | -| `local_block_height` | string \(uint64\) | The local block count downloaded from the ledger. The local database is synced when the `local_block_height` reaches the `network_block_height`. The `account_block_height` can only sync up to `local_block_height`. | -| `account_block_height` | string \(uint64\) | The scanned local block count for this account. This value will never be greater than `local_block_height`. At fully synced, it will match `network_block_height`. -| `is_synced` | boolean | Whether the account is synced with the `network_block_height`. Balances may not appear correct if the account is still syncing. | -| `balance` | string \(uint64\) | total pico MOB for this account minus the pico MOB marked as spent for this account | - -## Example - -```text -{ - "object": "balance", - "balance": "10000000000000", - "network_block_height": "468847", - "local_block_height": "468847", - "account_block_height": "468847", - "is_synced": true -} -``` diff --git a/docs/view-only-accounts/balance/get_balance_for_view_only_account.md b/docs/view-only-accounts/balance/get_balance_for_view_only_account.md deleted file mode 100644 index 6b8cf764f..000000000 --- a/docs/view-only-accounts/balance/get_balance_for_view_only_account.md +++ /dev/null @@ -1,55 +0,0 @@ ---- -description: Get the current balance for a given account. ---- - -# Get Balance For View Only Account - -## Parameters - -| Required Param | Purpose | Requirements | -| :--- | :--- | :--- | -| `account_id` | The account on which to perform this action. | Account must exist in the wallet as a view only account. | - -## Example - -{% tabs %} -{% tab title="Request Body" %} -```text -{ - "method": "get_balance_for_view_only_account", - "params": { - "account_id": "a8c9c7acb96cf4ad9154eec9384c09f2c75a340b441924847fe5f60a41805bde" - }, - "jsonrpc": "2.0", - "id": 1 -} -``` -{% endtab %} - -{% tab title="Response" %} -```text -{ - "method": "get_balance_for_view_only_account", - "result": { - "balance": { - "object": "balance", - "network_block_height": "152918", - "local_block_height": "152918", - "account_block_height": "152003", - "is_synced": false, - "unspent_pmob": "110000000000000000", - "max_spendable_pmob": "110000000000000000", - "pending_pmob": "0", - "spent_pmob": "0", - "secreted_pmob": "0", - "orphaned_pmob": "0" - } - }, - "error": null, - "jsonrpc": "2.0", - "id": 1, -} -``` -{% endtab %} -{% endtabs %} - diff --git a/docs/view-only-accounts/balance/get_balance_for_view_only_address.md b/docs/view-only-accounts/balance/get_balance_for_view_only_address.md deleted file mode 100644 index f970fed0a..000000000 --- a/docs/view-only-accounts/balance/get_balance_for_view_only_address.md +++ /dev/null @@ -1,53 +0,0 @@ ---- -description: Get the current balance for a given address. ---- - -# Get Balance For Address - -| Required Param | Purpose | Requirements | -| :--- | :--- | :--- | -| `address` | The address on which to perform this action. | Address must be assigned for an account in the wallet. | - -{% tabs %} -{% tab title="Request Body" %} -```text -{ - "method": "get_balance_for_view_only_address", - "params": { - "address": "3P4GtGkp5UVBXUzBqirgj7QFetWn4PsFPsHBXbC6A8AXw1a9CMej969jneiN1qKcwdn6e1VtD64EruGVSFQ8wHk5xuBHndpV9WUGQ78vV7Z" - }, - "jsonrpc": "2.0", - "api_version": "2", - "id": 1 -} -``` -{% endtab %} - -{% tab title="Response" %} -```text -{ - "method": "get_balance_for_view_only_address", - "result": { - "balance": { - "object": "balance", - "network_block_height": "152961", - "local_block_height": "152961", - "account_block_height": "152961", - "is_synced": true, - "unspent_pmob": "11881402222024", - "max_spendable_pmob": "11881402222024", - "pending_pmob": "0", - "spent_pmob": "84493835554166", - "secreted_pmob": "0", - "orphaned_pmob": "0" - } - }, - "error": null, - "jsonrpc": "2.0", - "id": 1, - "api_version": "2" -} -``` -{% endtab %} -{% endtabs %} - diff --git a/docs/view-only-accounts/subaddress/create_new_subaddress_request.md b/docs/view-only-accounts/subaddress/create_new_subaddress_request.md deleted file mode 100644 index 448c8e015..000000000 --- a/docs/view-only-accounts/subaddress/create_new_subaddress_request.md +++ /dev/null @@ -1,41 +0,0 @@ -# Create New Subaddress Request - -## Parameters - -| Required Param | Purpose | Requirements | -| :--- | :--- | :--- | -| `account_id` | The account on which to perform this action. | Account must exist in the wallet as a view only account. | -| `num_subaddresses_to_generate` | The number of desired subaddress. | | - -## Example - -{% tabs %} -{% tab title="Request" %} -``` -{ - "method": "create_new_subaddresses_request", - "params": { - "account_id": "f85920dd83f69d8850799e28240e3d395f0ad46dec2561b71f4614dd90a3edb5", - "num_subaddresses_to_generate": "10" - }, - "jsonrpc": "2.0", - "id": 1 -} -``` -{% endtab %} - -{% tab title="Response" %} -``` -{ - "method": "create_new_subaddresses_request", - "result": { - "account_id": "f85920dd83f69d8850799e28240e3d395f0ad46dec2561b71f4614dd90a3edb5", - "next_subaddress_index": "10", - "num_subaddresses_to_generate": "10" - }, - "jsonrpc": "2.0", - "id": 1 -} -``` -{% endtab %} -{% endtabs %} diff --git a/docs/view-only-accounts/subaddress/import_subaddresses_to_view_only_account.md b/docs/view-only-accounts/subaddress/import_subaddresses_to_view_only_account.md deleted file mode 100644 index 040b1a5bd..000000000 --- a/docs/view-only-accounts/subaddress/import_subaddresses_to_view_only_account.md +++ /dev/null @@ -1,56 +0,0 @@ -# Import Subaddresses - -## Parameters - -| Required Param | Purpose | Requirements | -| :--- | :--- | :--- | -| `account_id` | The account on which to perform this action. | Account must exist in the wallet as a view only account. | -| `subaddresses` | List of subaddress in json format | | - -### subaddress import json fields: -| field | description (all strings) | -| :--- | :--- | -| `object` | "view_only_subaddress" | -| `public_address` |A b58 encoding of the public address materials | -| `account_id` | The account that owns this subaddress | -| `comment` | Additional data associated with this address. | -| `subaddress_index` | The index of this address in the subaddress space for the account | -| `public_spend_key` | The public spend key for this addres | - -## Example - -{% tabs %} -{% tab title="Request" %} -``` -{ - "method": "import_subaddresses_to_view_only_account", - "params": { - "account_id": "f85920dd83f69d8850799e28240e3d395f0ad46dec2561b71f4614dd90a3edb5", - "subaddresses": "[{ - object: "view_only_subaddress", - public_address: "USm3fpXnKG5EUBx2ndxBDMPVciP5hGey2Jh4NDv6gmeo1LkMeiKrLJUUBk6Z", - account_id: "f85920dd83f69d8850799e28240e3d395f0ad46dec2561b71f4614dd90a3edb5", - comment: "target address", - "subaddress_index: "5", - public_spend_key: "asdsfpXnKG5EUBx2ndxBDMPVciP5hGey2Jh4NDv6gmeo1LkMeiKrLJUUBk6Z" - }]" - }, - "jsonrpc": "2.0", - "id": 1 -} -``` -{% endtab %} - -{% tab title="Response" %} -``` -{ - "method": "import_subaddresses_to_view_only_account", - "result": { - "public_address_b58s": "["USm3fpXnKG5EUBx2ndxBDMPVciP5hGey2Jh4NDv6gmeo1LkMeiKrLJUUBk6Z"]", - }, - "jsonrpc": "2.0", - "id": 1 -} -``` -{% endtab %} -{% endtabs %} \ No newline at end of file diff --git a/docs/view-only-accounts/syncing/README.md b/docs/view-only-accounts/syncing/README.md deleted file mode 100644 index e69de29bb..000000000 diff --git a/docs/view-only-accounts/syncing/sync_view_only_account.md b/docs/view-only-accounts/syncing/sync_view_only_account.md deleted file mode 100644 index 6bfc264ee..000000000 --- a/docs/view-only-accounts/syncing/sync_view_only_account.md +++ /dev/null @@ -1,55 +0,0 @@ -# Sync View Only Account - -## Parameters - -| Required Param | Purpose | Requirements | -| :--- | :--- | :--- | -| `account_id` | The account on which to perform this action. | Account must exist in the wallet as a view only account. | -| `completed_txos` | signed txos. A array of tuples (txoID, KeyImage) | | -| `subaddresses` | The subaddress to sync | | - -### subaddress import json fields: -| field | description (all strings) | -| :--- | :--- | -| `object` | "view_only_subaddress" | -| `public_address` |A b58 encoding of the public address materials | -| `account_id` | The account that owns this subaddress | -| `comment` | Additional data associated with this address. | -| `subaddress_index` | The index of this address in the subaddress space for the account | -| `public_spend_key` | The public spend key for this addres | - -## Example - -{% tabs %} -{% tab title="Request" %} -``` -{ - "method": "sync_view_only_account", - "params": { - "account_id": "f85920dd83f69d8850799e28240e3d395f0ad46dec2561b71f4614dd90a3edb5", - "completed_txos": "[(asdasedeerwe..., sadjashdoauihdkahwk...)]", - "subaddresses": "[{ - object: "view_only_subaddress", - public_address: "USm3fpXnKG5EUBx2ndxBDMPVciP5hGey2Jh4NDv6gmeo1LkMeiKrLJUUBk6Z", - account_id: "f85920dd83f69d8850799e28240e3d395f0ad46dec2561b71f4614dd90a3edb5", - comment: "target address", - "subaddress_index: "5", - public_spend_key: "asdsfpXnKG5EUBx2ndxBDMPVciP5hGey2Jh4NDv6gmeo1LkMeiKrLJUUBk6Z" - }]" - }, - "jsonrpc": "2.0", - "id": 1 -} -``` -{% endtab %} - -{% tab title="Response" %} -``` -{ - "method": "sync_view_only_account", - "jsonrpc": "2.0", - "id": 1 -} -``` -{% endtab %} -{% endtabs %} diff --git a/full-service/Cargo.toml b/full-service/Cargo.toml index e95d01574..65ba06ce9 100644 --- a/full-service/Cargo.toml +++ b/full-service/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "mc-full-service" -version = "1.9.2" +version = "2.0.0" authors = ["MobileCoin"] edition = "2018" build = "build.rs" @@ -9,10 +9,6 @@ build = "build.rs" name = "full-service" path = "src/bin/main.rs" -[[bin]] -name = "transaction-signer" -path = "src/bin/transaction-signer.rs" - [dependencies] mc-validator-api = { path = "../validator/api" } mc-validator-connection = { path = "../validator/connection" } @@ -21,6 +17,7 @@ mc-account-keys = { path = "../mobilecoin/account-keys" } mc-account-keys-slip10 = { path = "../mobilecoin/account-keys/slip10" } mc-api = { path = "../mobilecoin/api" } mc-attest-verifier = { path = "../mobilecoin/attest/verifier", default-features = false } +mc-blockchain-types = { path = "../mobilecoin/blockchain/types" } mc-common = { path = "../mobilecoin/common", default-features = false, features = ["loggers"] } mc-connection = { path = "../mobilecoin/connection" } mc-consensus-enclave-measurement = { path = "../mobilecoin/consensus/enclave/measurement" } @@ -28,7 +25,9 @@ mc-consensus-scp = { path = "../mobilecoin/consensus/scp" } mc-crypto-digestible = { path = "../mobilecoin/crypto/digestible", features = ["derive"] } mc-crypto-keys = { path = "../mobilecoin/crypto/keys", default-features = false } mc-crypto-rand = { path = "../mobilecoin/crypto/rand", default-features = false } +mc-crypto-ring-signature-signer = { path = "../mobilecoin/crypto/ring-signature/signer" } mc-fog-report-connection = { path = "../mobilecoin/fog/report/connection" } +mc-fog-report-resolver = { path = "../mobilecoin/fog/report/resolver" } mc-fog-report-validation = { path = "../mobilecoin/fog/report/validation" } mc-ledger-db = { path = "../mobilecoin/ledger/db" } mc-ledger-migration = { path = "../mobilecoin/ledger/migration" } @@ -39,6 +38,7 @@ mc-mobilecoind-json = { path = "../mobilecoin/mobilecoind-json" } mc-sgx-css = { path = "../mobilecoin/sgx/css" } mc-transaction-core = { path = "../mobilecoin/transaction/core" } mc-transaction-std = { path = "../mobilecoin/transaction/std" } +mc-transaction-types = { path = "../mobilecoin/transaction/types" } mc-util-from-random = { path = "../mobilecoin/util/from-random" } mc-util-parse = { path = "../mobilecoin/util/parse" } mc-util-serial = { path = "../mobilecoin/util/serial", default-features = false } @@ -47,31 +47,35 @@ mc-util-uri = { path = "../mobilecoin/util/uri" } base64 = "0.13.0" chrono = { version = "0.4", default-features = false, features = ["alloc"] } crossbeam-channel = "0.5" -diesel = { version = "1.4.8", features = ["sqlcipher-bundled"] } +diesel = { version = "1.4.8", features = ["sqlcipher-bundled", "chrono"] } diesel-derive-enum = { version = "1", features = ["sqlite"] } diesel_migrations = { version = "1.4.0", features = ["sqlite"] } displaydoc = {version = "0.2", default-features = false } dotenv = "0.15.0" -grpcio = { version ="0.10.2", default-features = false, features = [ "openssl" ] } +grpcio = "0.10.3" hex = {version = "0.4", default-features = false } -num_cpus = "1.12" +num_cpus = "1.13" +protobuf = "2.28.0" rand = { version = "0.8", default-features = false } rayon = "1.5" -reqwest = { version = "0.11.10", default-features = false, features = ["rustls-tls", "gzip"] } +reqwest = { version = "0.11.12", default-features = false, features = ["rustls-tls", "gzip"] } retry = "1.3" -rocket = { version = "0.4.5", default-features = false } -rocket_contrib = { version = "0.4.5", default-features = false, features = ["json", "diesel_sqlite_pool"] } +rocket = { version = "0.4.11", default-features = false } +rocket_contrib = { version = "0.4.11", default-features = false, features = ["json", "diesel_sqlite_pool"] } serde = { version = "1.0", default-features = false, features = ["alloc", "derive"] } +serde-big-array = "0.4.1" serde_derive = "1.0" serde_json = { version = "1.0", features = ["preserve_order"] } structopt = "0.3" -strum = { version = "0.24.0", features = ["derive"] } -strum_macros = "0.24.0" -tiny-bip39 = "0.8.0" -uuid = { version = "1.0.0", features = ["serde", "v4"] } +strum = { version = "0.24.1", features = ["derive"] } +strum_macros = "0.24.3" +tiny-bip39 = "1.0" +uuid = { version = "1.1.2", features = ["serde", "v4"] } [dev-dependencies] +mc-blockchain-test-utils = { path = "../mobilecoin/blockchain/test-utils" } mc-connection-test-utils = { path = "../mobilecoin/connection/test-utils" } +mc-consensus-enclave-api = { path = "../mobilecoin/consensus/enclave/api" } mc-fog-report-validation = { path = "../mobilecoin/fog/report/validation", features = ["automock"] } mc-fog-report-validation-test-utils = { path = "../mobilecoin/fog/report/validation/test-utils"} tempdir = "0.3" @@ -80,5 +84,5 @@ bs58 = "0.4.0" [build-dependencies] # clippy fails to run without this. diesel = { version = "1.4.8", features = ["sqlcipher-bundled"] } -vergen = "7.0.0" +vergen = "7.4.2" anyhow = "1.0" diff --git a/docs/view-only-accounts/subaddress/README.md b/full-service/migrations/2022-06-13-204000_api_v3/down.sql similarity index 100% rename from docs/view-only-accounts/subaddress/README.md rename to full-service/migrations/2022-06-13-204000_api_v3/down.sql diff --git a/full-service/migrations/2022-06-13-204000_api_v3/up.sql b/full-service/migrations/2022-06-13-204000_api_v3/up.sql new file mode 100644 index 000000000..893351327 --- /dev/null +++ b/full-service/migrations/2022-06-13-204000_api_v3/up.sql @@ -0,0 +1,107 @@ +DROP TABLE view_only_txos; +DROP TABLE view_only_subaddresses; +DROP TABLE view_only_accounts; + +CREATE TABLE NEW_accounts ( + id VARCHAR NOT NULL PRIMARY KEY, + account_key BLOB NOT NULL, + entropy BLOB, + key_derivation_version INTEGER NOT NULL, + first_block_index UNSIGNED BIG INT NOT NULL, + next_block_index UNSIGNED BIG INT NOT NULL, + import_block_index UNSIGNED BIG INT NULL, + name VARCHAR NOT NULL DEFAULT '', + fog_enabled BOOLEAN NOT NULL, + view_only BOOLEAN NOT NULL DEFAULT FALSE +); + +INSERT INTO NEW_accounts SELECT account_id_hex, account_key, entropy, key_derivation_version, first_block_index, next_block_index, import_block_index, name, fog_enabled, FALSE FROM accounts; +DROP TABLE accounts; +ALTER TABLE NEW_accounts RENAME TO accounts; + +CREATE TABLE NEW_assigned_subaddresses ( + public_address_b58 VARCHAR NOT NULL PRIMARY KEY, + account_id VARCHAR NOT NULL, + subaddress_index UNSIGNED BIG INT NOT NULL, + comment VARCHAR NOT NULL DEFAULT '', + spend_public_key BLOB NOT NULL, + FOREIGN KEY (account_id) REFERENCES accounts(id) +); + +INSERT INTO NEW_assigned_subaddresses SELECT assigned_subaddress_b58, account_id_hex, subaddress_index, comment, subaddress_spend_key FROM assigned_subaddresses; +DROP TABLE assigned_subaddresses; +ALTER TABLE NEW_assigned_subaddresses RENAME TO assigned_subaddresses; + +CREATE TABLE transaction_input_txos ( + transaction_log_id VARCHAR NOT NULL, + txo_id VARCHAR NOT NULL, + PRIMARY KEY (transaction_log_id, txo_id), + FOREIGN KEY (transaction_log_id) REFERENCES transaction_logs(id), + FOREIGN KEY (txo_id) REFERENCES txos(id) +); + +INSERT INTO transaction_input_txos SELECT transaction_id_hex, txo_id_hex FROM transaction_txo_types WHERE transaction_txo_type = 'txo_used_as_input'; + +CREATE TABLE transaction_output_txos ( + transaction_log_id VARCHAR NOT NULL, + txo_id VARCHAR NOT NULL, + recipient_public_address_b58 VARCHAR NOT NULL, + is_change BOOLEAN NOT NULL, + PRIMARY KEY (transaction_log_id, txo_id), + FOREIGN KEY (transaction_log_id) REFERENCES transaction_logs(id), + FOREIGN KEY (txo_id) REFERENCES txos(id) +); + +INSERT INTO transaction_output_txos SELECT ttt.transaction_id_hex, ttt.txo_id_hex, txos.recipient_public_address_b58, FALSE +FROM transaction_txo_types ttt +INNER JOIN transaction_logs tl ON ttt.transaction_id_hex = tl.transaction_id_hex +INNER JOIN txos ON ttt.txo_id_hex = txos.txo_id_hex +WHERE ttt.transaction_txo_type = 'txo_used_as_output' AND tl.direction = 'tx_direction_sent'; + +INSERT INTO transaction_output_txos SELECT ttt.transaction_id_hex, ttt.txo_id_hex, asub.public_address_b58, TRUE +FROM transaction_txo_types ttt +INNER JOIN transaction_logs tl ON ttt.transaction_id_hex = tl.transaction_id_hex +INNER JOIN txos ON ttt.txo_id_hex = txos.txo_id_hex +INNER JOIN assigned_subaddresses asub ON asub.subaddress_index = txos.subaddress_index AND asub.account_id = txos.received_account_id_hex +WHERE ttt.transaction_txo_type = 'txo_used_as_change' AND tl.direction = 'tx_direction_sent'; + +DROP TABLE transaction_txo_types; + +CREATE TABLE NEW_txos ( + id VARCHAR NOT NULL PRIMARY KEY, + account_id VARCHAR, + value UNSIGNED BIG INT NOT NULL, + token_id UNSIGNED BIG INT NOT NULL, + target_key BLOB NOT NULL, + public_key BLOB NOT NULL, + e_fog_hint BLOB NOT NULL, + txo BLOB NOT NULL, + subaddress_index UNSIGNED BIG INT, + key_image BLOB, + received_block_index UNSIGNED BIG INT, + spent_block_index UNSIGNED BIG INT, + shared_secret BLOB, + FOREIGN KEY (account_id) REFERENCES accounts(id) +); + +INSERT INTO NEW_txos SELECT txo_id_hex, received_account_id_hex, value, token_id, target_key, public_key, e_fog_hint, txo, subaddress_index, key_image, received_block_index, spent_block_index, confirmation FROM txos; +DROP TABLE txos; +ALTER TABLE NEW_txos RENAME TO txos; + +CREATE TABLE NEW_transaction_logs ( + id VARCHAR NOT NULL PRIMARY KEY, + account_id VARCHAR NOT NULL, + fee_value UNSIGNED BIG INT NOT NULL, + fee_token_id UNSIGNED BIG INT NOT NULL, + submitted_block_index UNSIGNED BIG INT, + tombstone_block_index UNSIGNED BIG INT, + finalized_block_index UNSIGNED BIG INT, + comment TEXT NOT NULL DEFAULT '', + tx BLOB NOT NULL, + failed BOOLEAN NOT NULL, + FOREIGN KEY (account_id) REFERENCES accounts(id) +); + +INSERT INTO NEW_transaction_logs SELECT transaction_id_hex, account_id_hex, fee, 0, submitted_block_index, NULL, finalized_block_index, comment, tx, FALSE FROM transaction_logs WHERE direction = 'tx_direction_sent'; +DROP TABLE transaction_logs; +ALTER TABLE NEW_transaction_logs RENAME TO transaction_logs; \ No newline at end of file diff --git a/full-service/src/bin/main.rs b/full-service/src/bin/main.rs index 8127ef5b2..65ebfe55f 100644 --- a/full-service/src/bin/main.rs +++ b/full-service/src/bin/main.rs @@ -9,11 +9,10 @@ use mc_attest_verifier::{MrSignerVerifier, Verifier, DEBUG_ENCLAVE}; use mc_common::logger::{create_app_logger, log, o, Logger}; use mc_connection::ConnectionManager; use mc_consensus_scp::QuorumSet; -use mc_fog_report_validation::FogResolver; +use mc_fog_report_resolver::FogResolver; use mc_full_service::{ check_host, config::APIConfig, - service::address::AddressService, wallet::{consensus_backed_rocket, validator_backed_rocket, APIKeyState, WalletState}, ValidatorLedgerSyncThread, WalletDb, WalletService, }; @@ -62,29 +61,28 @@ fn main() { .port(config.listen_port) .unwrap(); - // Connect to the database and run the migrations - let conn = - SqliteConnection::establish(config.wallet_db.to_str().unwrap()).unwrap_or_else(|err| { - eprintln!("Cannot open database {:?}: {:?}", config.wallet_db, err); - exit(EXIT_NO_DATABASE_CONNECTION); - }); - WalletDb::set_db_encryption_key_from_env(&conn); - WalletDb::try_change_db_encryption_key_from_env(&conn); - if !WalletDb::check_database_connectivity(&conn) { - eprintln!("Incorrect password for database {:?}.", config.wallet_db); - exit(EXIT_WRONG_PASSWORD); + let wallet_db = match config.wallet_db { + Some(ref wallet_db_path_buf) => { + let wallet_db_path = wallet_db_path_buf.to_str().unwrap(); + // Connect to the database and run the migrations + let conn = SqliteConnection::establish(wallet_db_path).unwrap_or_else(|err| { + eprintln!("Cannot open database {:?}: {:?}", wallet_db_path, err); + exit(EXIT_NO_DATABASE_CONNECTION); + }); + WalletDb::set_db_encryption_key_from_env(&conn); + WalletDb::try_change_db_encryption_key_from_env(&conn); + if !WalletDb::check_database_connectivity(&conn) { + eprintln!("Incorrect password for database {:?}.", wallet_db_path); + exit(EXIT_WRONG_PASSWORD); + }; + WalletDb::run_migrations(&conn); + WalletDb::run_proto_conversions_if_necessary(&conn); + log::info!(logger, "Connected to database."); + + Some(WalletDb::new_from_url(wallet_db_path, 10).expect("Could not access wallet db")) + } + None => None, }; - WalletDb::run_migrations(&conn); - log::info!(logger, "Connected to database."); - - let wallet_db = WalletDb::new_from_url( - config - .wallet_db - .to_str() - .expect("Could not get wallet_db path"), - 10, - ) - .expect("Could not access wallet db"); // Start WalletService based on our configuration if let Some(validator_uri) = config.validator.as_ref() { @@ -96,7 +94,7 @@ fn main() { fn consensus_backed_full_service( config: &APIConfig, - wallet_db: WalletDb, + wallet_db: Option, rocket_config: rocket::Config, logger: Logger, ) { @@ -165,11 +163,6 @@ fn consensus_backed_full_service( config.offline, logger, ); - - service - .assign_missing_reserved_subaddresses_for_accounts() - .unwrap(); - let state = WalletState { service }; let rocket = consensus_backed_rocket(rocket_config, state); @@ -180,11 +173,15 @@ fn consensus_backed_full_service( fn validator_backed_full_service( validator_uri: &ValidatorUri, config: &APIConfig, - wallet_db: WalletDb, + wallet_db: Option, rocket_config: rocket::Config, logger: Logger, ) { - let validator_conn = ValidatorConnection::new(validator_uri, logger.clone()); + let validator_conn = ValidatorConnection::new( + validator_uri, + config.peers_config.chain_id.clone(), + logger.clone(), + ); // Create the ledger_db. let ledger_db = config.ledger_db_config.create_or_open_ledger_db( @@ -218,6 +215,7 @@ fn validator_backed_full_service( // Create the ledger sync thread. let _ledger_sync_thread = ValidatorLedgerSyncThread::new( validator_uri, + config.peers_config.chain_id.clone(), config.poll_interval, ledger_db.clone(), network_state.clone(), @@ -257,11 +255,6 @@ fn validator_backed_full_service( false, logger, ); - - service - .assign_missing_reserved_subaddresses_for_accounts() - .unwrap(); - let state = WalletState { service }; let rocket = validator_backed_rocket(rocket_config, state); diff --git a/full-service/src/config.rs b/full-service/src/config.rs index 195e46a0f..9214f3366 100644 --- a/full-service/src/config.rs +++ b/full-service/src/config.rs @@ -3,6 +3,7 @@ //! Config definition and processing for Wallet Service. use mc_attest_verifier::{MrSignerVerifier, Verifier, DEBUG_ENCLAVE}; +use mc_blockchain_types::BlockData; use mc_common::{ logger::{log, Logger}, ResponderId, @@ -10,10 +11,9 @@ use mc_common::{ use mc_connection::{ConnectionManager, HardcodedCredentialsProvider, ThickClient}; use mc_consensus_scp::QuorumSet; use mc_fog_report_connection::GrpcFogReportConnection; -use mc_fog_report_validation::FogResolver; +use mc_fog_report_resolver::FogResolver; use mc_ledger_db::{Ledger, LedgerDB}; use mc_sgx_css::Signature; -use mc_transaction_core::BlockData; use mc_util_parse::parse_duration_in_seconds; use mc_util_uri::{ConnectionUri, ConsensusClientUri, FogUri}; use mc_validator_api::ValidatorUri; @@ -40,7 +40,7 @@ pub struct APIConfig { /// Path to WalletDb. #[structopt(long, parse(from_os_str))] - pub wallet_db: PathBuf, + pub wallet_db: Option, #[structopt(flatten)] pub ledger_db_config: LedgerDbConfig, @@ -120,7 +120,8 @@ impl APIConfig { .build(), ); - let conn = GrpcFogReportConnection::new(env, logger.clone()); + let conn = + GrpcFogReportConnection::new(self.peers_config.chain_id.clone(), env, logger.clone()); let verifier = self.get_fog_ingest_verifier(); @@ -165,6 +166,10 @@ pub struct PeersConfig { /// For example: https://s3-us-west-1.amazonaws.com/mobilecoin.chain/node1.test.mobilecoin.com/ #[structopt(long = "tx-source-url", required_unless_one = &["offline", "validator"], conflicts_with_all = &["offline", "validator"])] pub tx_source_urls: Option>, + + /// Chain Id + #[structopt(default_value = "", long)] + pub chain_id: String, } impl PeersConfig { @@ -213,6 +218,7 @@ impl PeersConfig { .iter() .map(|client_uri| { ThickClient::new( + self.chain_id.clone(), client_uri.clone(), verifier.clone(), grpc_env.clone(), @@ -326,12 +332,8 @@ impl LedgerDbConfig { let block_data = get_origin_block_and_transactions() .expect("Failed to download initial transactions"); let mut db = LedgerDB::open(&self.ledger_db).expect("Could not open ledger_db"); - db.append_block( - block_data.block(), - block_data.contents(), - block_data.signature().clone(), - ) - .expect("Failed to appened initial transactions"); + db.append_block_data(&block_data) + .expect("Failed to appened initial transactions"); log::info!(logger, "Bootstrapping completed!"); } } diff --git a/full-service/src/db/account.rs b/full-service/src/db/account.rs index e060e52e3..1e827e104 100644 --- a/full-service/src/db/account.rs +++ b/full-service/src/db/account.rs @@ -5,10 +5,9 @@ use crate::{ db::{ assigned_subaddress::AssignedSubaddressModel, - models::{Account, AssignedSubaddress, NewAccount, TransactionLog, Txo, ViewOnlyAccount}, + models::{Account, AssignedSubaddress, NewAccount, TransactionLog, Txo}, transaction_log::TransactionLogModel, txo::TxoModel, - view_only_account::ViewOnlyAccountModel, Conn, WalletDbError, }, util::constants::{ @@ -20,19 +19,27 @@ use crate::{ use bip39::Mnemonic; use diesel::prelude::*; use mc_account_keys::{ - AccountKey, PublicAddress, RootEntropy, RootIdentity, CHANGE_SUBADDRESS_INDEX, + AccountKey, PublicAddress, RootEntropy, RootIdentity, ViewAccountKey, CHANGE_SUBADDRESS_INDEX, DEFAULT_SUBADDRESS_INDEX, }; use mc_account_keys_slip10::Slip10Key; use mc_crypto_digestible::{Digestible, MerlinTranscript}; -use mc_ledger_db::LedgerDB; +use mc_crypto_keys::{RistrettoPrivate, RistrettoPublic}; +use mc_transaction_core::TokenId; use std::fmt; #[derive(Debug, Clone, Eq, PartialEq, Hash)] pub struct AccountID(pub String); impl From<&AccountKey> for AccountID { - fn from(src: &AccountKey) -> AccountID { + fn from(src: &AccountKey) -> Self { + let main_subaddress = src.subaddress(DEFAULT_SUBADDRESS_INDEX); + AccountID::from(&main_subaddress) + } +} + +impl From<&ViewAccountKey> for AccountID { + fn from(src: &ViewAccountKey) -> Self { let main_subaddress = src.subaddress(DEFAULT_SUBADDRESS_INDEX); AccountID::from(&main_subaddress) } @@ -45,17 +52,18 @@ impl From<&PublicAddress> for AccountID { } } +impl From for AccountID { + fn from(src: String) -> Self { + Self(src) + } +} + impl fmt::Display for AccountID { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "{}", self.0) } } -pub struct ViewOnlyAccountImportPackage { - pub account: Account, - pub subaddresses: Vec, -} - pub trait AccountModel { /// Create an account. /// @@ -71,7 +79,6 @@ pub trait AccountModel { fog_report_url: String, fog_report_id: String, fog_authority_spki: String, - ledger_db: &LedgerDB, conn: &Conn, ) -> Result<(AccountID, String), WalletDbError>; @@ -89,7 +96,6 @@ pub trait AccountModel { fog_report_url: String, fog_report_id: String, fog_authority_spki: String, - ledger_db: &LedgerDB, conn: &Conn, ) -> Result<(AccountID, String), WalletDbError>; @@ -107,7 +113,6 @@ pub trait AccountModel { next_subaddress_index: Option, name: &str, fog_enabled: bool, - ledger_db: &LedgerDB, conn: &Conn, ) -> Result<(AccountID, String), WalletDbError>; @@ -122,7 +127,6 @@ pub trait AccountModel { fog_report_url: String, fog_report_id: String, fog_authority_spki: String, - ledger_db: &LedgerDB, conn: &Conn, ) -> Result; @@ -137,7 +141,17 @@ pub trait AccountModel { fog_report_url: String, fog_report_id: String, fog_authority_spki: String, - ledger_db: &LedgerDB, + conn: &Conn, + ) -> Result; + + /// Import a view only account. + fn import_view_only( + view_private_key: &RistrettoPrivate, + spend_public_key: &RistrettoPublic, + name: Option, + import_block_index: u64, + first_block_index: Option, + next_subaddress_index: Option, conn: &Conn, ) -> Result; @@ -145,7 +159,11 @@ pub trait AccountModel { /// /// Returns: /// * Vector of all Accounts in the DB - fn list_all(conn: &Conn) -> Result, WalletDbError>; + fn list_all( + conn: &Conn, + offset: Option, + limit: Option, + ) -> Result, WalletDbError>; /// Get a specific account. /// @@ -170,6 +188,25 @@ pub trait AccountModel { /// Delete an account. fn delete(self, conn: &Conn) -> Result<(), WalletDbError>; + + /// Get change public address + fn change_subaddress(self, conn: &Conn) -> Result; + + /// Get main public address + fn main_subaddress(self, conn: &Conn) -> Result; + + /// Get all of the token ids present for the account + fn get_token_ids(self, conn: &Conn) -> Result, WalletDbError>; + + /// Get the next sequentially unassigned subaddress index for the account + /// (reserved addresses are not included) + fn next_subaddress_index(self, conn: &Conn) -> Result; + + fn account_key(&self) -> Result, WalletDbError>; + + fn view_account_key(&self) -> Result; + + fn view_private_key(&self) -> Result; } impl AccountModel for Account { @@ -182,7 +219,6 @@ impl AccountModel for Account { fog_report_url: String, fog_report_id: String, fog_authority_spki: String, - ledger_db: &LedgerDB, conn: &Conn, ) -> Result<(AccountID, String), WalletDbError> { let fog_enabled = !fog_report_url.is_empty(); @@ -202,7 +238,6 @@ impl AccountModel for Account { next_subaddress_index, name, fog_enabled, - ledger_db, conn, ) } @@ -216,7 +251,6 @@ impl AccountModel for Account { fog_report_url: String, fog_report_id: String, fog_authority_spki: String, - ledger_db: &LedgerDB, conn: &Conn, ) -> Result<(AccountID, String), WalletDbError> { let fog_enabled = !fog_report_url.is_empty(); @@ -238,7 +272,6 @@ impl AccountModel for Account { next_subaddress_index, name, fog_enabled, - ledger_db, conn, ) } @@ -252,92 +285,54 @@ impl AccountModel for Account { next_subaddress_index: Option, name: &str, fog_enabled: bool, - ledger_db: &LedgerDB, conn: &Conn, ) -> Result<(AccountID, String), WalletDbError> { use crate::db::schema::accounts; let account_id = AccountID::from(account_key); - if ViewOnlyAccount::get(&account_id.to_string(), conn).is_ok() { - return Err(WalletDbError::ViewOnlyAccountAlreadyExists( - account_id.to_string(), - )); + if Account::get(&account_id, conn).is_ok() { + return Err(WalletDbError::AccountAlreadyExists(account_id.to_string())); } let first_block_index = first_block_index.unwrap_or(DEFAULT_FIRST_BLOCK_INDEX); let next_block_index = first_block_index; - let change_subaddress_index = if fog_enabled { - DEFAULT_SUBADDRESS_INDEX as i64 - } else { - CHANGE_SUBADDRESS_INDEX as i64 - }; - - let next_subaddress_index = if fog_enabled { - 1 - } else { - next_subaddress_index.unwrap_or(DEFAULT_NEXT_SUBADDRESS_INDEX) as i64 - }; + let next_subaddress_index = + next_subaddress_index.unwrap_or(DEFAULT_NEXT_SUBADDRESS_INDEX) as i64; let new_account = NewAccount { - account_id_hex: &account_id.to_string(), - account_key: &mc_util_serial::encode(account_key), /* FIXME: WS-6 - add - * encryption */ - entropy, + id: &account_id.to_string(), + account_key: &mc_util_serial::encode(account_key), + entropy: Some(entropy), key_derivation_version: key_derivation_version as i32, - main_subaddress_index: DEFAULT_SUBADDRESS_INDEX as i64, - change_subaddress_index, - next_subaddress_index, first_block_index: first_block_index as i64, next_block_index: next_block_index as i64, import_block_index: import_block_index.map(|i| i as i64), name, fog_enabled, + view_only: false, }; diesel::insert_into(accounts::table) .values(&new_account) .execute(conn)?; - let main_subaddress_b58 = AssignedSubaddress::create( - account_key, - None, /* FIXME: WS-8 - Address Book Entry if details provided, or None - * always for main? */ - DEFAULT_SUBADDRESS_INDEX, - "Main", - ledger_db, - conn, - )?; + let main_subaddress_b58 = + AssignedSubaddress::create(account_key, DEFAULT_SUBADDRESS_INDEX, "Main", conn)?; + + AssignedSubaddress::create(account_key, CHANGE_SUBADDRESS_INDEX, "Change", conn)?; + if !fog_enabled { AssignedSubaddress::create( account_key, - None, LEGACY_CHANGE_SUBADDRESS_INDEX, "Legacy Change", - ledger_db, conn, )?; - AssignedSubaddress::create( - account_key, - None, /* FIXME: WS-8 - Address Book Entry if details provided, or None - * always for main? */ - CHANGE_SUBADDRESS_INDEX, - "Change", - ledger_db, - conn, - )?; - - for subaddress_index in 2..next_subaddress_index { - AssignedSubaddress::create( - account_key, - None, - subaddress_index as u64, - "", - ledger_db, - conn, - )?; + for subaddress_index in DEFAULT_NEXT_SUBADDRESS_INDEX..next_subaddress_index as u64 { + AssignedSubaddress::create(account_key, subaddress_index as u64, "", conn)?; } } @@ -353,7 +348,6 @@ impl AccountModel for Account { fog_report_url: String, fog_report_id: String, fog_authority_spki: String, - ledger_db: &LedgerDB, conn: &Conn, ) -> Result { let (account_id, _public_address_b58) = Account::create_from_mnemonic( @@ -365,7 +359,6 @@ impl AccountModel for Account { fog_report_url, fog_report_id, fog_authority_spki, - ledger_db, conn, )?; Account::get(&account_id, conn) @@ -380,7 +373,6 @@ impl AccountModel for Account { fog_report_url: String, fog_report_id: String, fog_authority_spki: String, - ledger_db: &LedgerDB, conn: &Conn, ) -> Result { let (account_id, _public_address_b58) = Account::create_from_root_entropy( @@ -392,25 +384,106 @@ impl AccountModel for Account { fog_report_url, fog_report_id, fog_authority_spki, - ledger_db, conn, )?; Account::get(&account_id, conn) } - fn list_all(conn: &Conn) -> Result, WalletDbError> { + fn import_view_only( + view_private_key: &RistrettoPrivate, + spend_public_key: &RistrettoPublic, + name: Option, + import_block_index: u64, + first_block_index: Option, + next_subaddress_index: Option, + conn: &Conn, + ) -> Result { + use crate::db::schema::accounts; + + let view_account_key = ViewAccountKey::new(*view_private_key, *spend_public_key); + let account_id = AccountID::from(&view_account_key); + + if Account::get(&account_id, conn).is_ok() { + return Err(WalletDbError::AccountAlreadyExists(account_id.to_string())); + } + + let first_block_index = first_block_index.unwrap_or(DEFAULT_FIRST_BLOCK_INDEX) as i64; + let next_block_index = first_block_index; + + let next_subaddress_index = + next_subaddress_index.unwrap_or(DEFAULT_NEXT_SUBADDRESS_INDEX) as i64; + + let new_account = NewAccount { + id: &account_id.to_string(), + account_key: &mc_util_serial::encode(&view_account_key), + entropy: None, + key_derivation_version: MNEMONIC_KEY_DERIVATION_VERSION as i32, + first_block_index, + next_block_index, + import_block_index: Some(import_block_index as i64), + name: &name.unwrap_or_else(|| "".to_string()), + fog_enabled: false, + view_only: true, + }; + + diesel::insert_into(accounts::table) + .values(&new_account) + .execute(conn)?; + + AssignedSubaddress::create_for_view_only_account( + &view_account_key, + DEFAULT_SUBADDRESS_INDEX, + "Main", + conn, + )?; + + AssignedSubaddress::create_for_view_only_account( + &view_account_key, + LEGACY_CHANGE_SUBADDRESS_INDEX, + "Legacy Change", + conn, + )?; + + AssignedSubaddress::create_for_view_only_account( + &view_account_key, + CHANGE_SUBADDRESS_INDEX, + "Change", + conn, + )?; + + for subaddress_index in DEFAULT_NEXT_SUBADDRESS_INDEX..next_subaddress_index as u64 { + AssignedSubaddress::create_for_view_only_account( + &view_account_key, + subaddress_index as u64, + "", + conn, + )?; + } + + Account::get(&account_id, conn) + } + + fn list_all( + conn: &Conn, + offset: Option, + limit: Option, + ) -> Result, WalletDbError> { use crate::db::schema::accounts; - Ok(accounts::table - .select(accounts::all_columns) - .load::(conn)?) + let mut query = accounts::table.into_boxed(); + + if let (Some(offset), Some(limit)) = (offset, limit) { + query = query.limit(limit as i64).offset(offset as i64); + } + + Ok(query.load(conn)?) } fn get(account_id: &AccountID, conn: &Conn) -> Result { - use crate::db::schema::accounts::dsl::{account_id_hex as dsl_account_id_hex, accounts}; + use crate::db::schema::accounts; - match accounts - .filter(dsl_account_id_hex.eq(account_id.to_string())) + match accounts::table + .filter(accounts::id.eq(account_id.to_string())) .get_result::(conn) { Ok(a) => Ok(a), @@ -427,13 +500,8 @@ impl AccountModel for Account { let mut accounts: Vec = Vec::::new(); - if let Some(received_account_id_hex) = txo.received_account_id_hex { - let account = Account::get(&AccountID(received_account_id_hex), conn)?; - accounts.push(account); - } - - if let Some(minted_account_id_hex) = txo.minted_account_id_hex { - let account = Account::get(&AccountID(minted_account_id_hex), conn)?; + if let Some(account_id) = txo.account_id { + let account = Account::get(&AccountID(account_id), conn)?; accounts.push(account); } @@ -441,10 +509,10 @@ impl AccountModel for Account { } fn update_name(&self, new_name: String, conn: &Conn) -> Result<(), WalletDbError> { - use crate::db::schema::accounts::dsl::{account_id_hex, accounts}; + use crate::db::schema::accounts; - diesel::update(accounts.filter(account_id_hex.eq(&self.account_id_hex))) - .set(crate::db::schema::accounts::name.eq(new_name)) + diesel::update(accounts::table.filter(accounts::id.eq(&self.id))) + .set(accounts::name.eq(new_name)) .execute(conn)?; Ok(()) } @@ -454,38 +522,101 @@ impl AccountModel for Account { next_block_index: u64, conn: &Conn, ) -> Result<(), WalletDbError> { - use crate::db::schema::accounts::dsl::{account_id_hex, accounts}; - diesel::update(accounts.filter(account_id_hex.eq(&self.account_id_hex))) - .set(crate::db::schema::accounts::next_block_index.eq(next_block_index as i64)) + use crate::db::schema::accounts; + diesel::update(accounts::table.filter(accounts::id.eq(&self.id))) + .set(accounts::next_block_index.eq(next_block_index as i64)) .execute(conn)?; Ok(()) } fn delete(self, conn: &Conn) -> Result<(), WalletDbError> { - use crate::db::schema::accounts::dsl::{account_id_hex, accounts}; + use crate::db::schema::accounts; // Delete transaction logs associated with this account - TransactionLog::delete_all_for_account(&self.account_id_hex, conn)?; + TransactionLog::delete_all_for_account(&self.id, conn)?; // Delete associated assigned subaddresses - AssignedSubaddress::delete_all(&self.account_id_hex, conn)?; + AssignedSubaddress::delete_all(&self.id, conn)?; // Delete references to the account in the Txos table. - Txo::scrub_account(&self.account_id_hex, conn)?; + Txo::scrub_account(&self.id, conn)?; - diesel::delete(accounts.filter(account_id_hex.eq(&self.account_id_hex))).execute(conn)?; + diesel::delete(accounts::table.filter(accounts::id.eq(&self.id))).execute(conn)?; // Delete Txos with no references. Txo::delete_unreferenced(conn)?; Ok(()) } + + fn change_subaddress(self, conn: &Conn) -> Result { + AssignedSubaddress::get_for_account_by_index(&self.id, CHANGE_SUBADDRESS_INDEX as i64, conn) + } + + fn main_subaddress(self, conn: &Conn) -> Result { + AssignedSubaddress::get_for_account_by_index( + &self.id, + DEFAULT_SUBADDRESS_INDEX as i64, + conn, + ) + } + + fn get_token_ids(self, conn: &Conn) -> Result, WalletDbError> { + use crate::db::schema::txos; + + let distinct_token_ids = txos::table + .filter(txos::account_id.eq(&self.id)) + .select(txos::token_id) + .distinct() + .load::(conn)? + .into_iter() + .map(|i| TokenId::from(i as u64)) + .collect(); + + Ok(distinct_token_ids) + } + + fn next_subaddress_index(self, conn: &Conn) -> Result { + use crate::db::schema::assigned_subaddresses; + + let highest_subaddress_index: i64 = assigned_subaddresses::table + .filter(assigned_subaddresses::account_id.eq(&self.id)) + .order_by(assigned_subaddresses::subaddress_index.desc()) + .select(diesel::dsl::max(assigned_subaddresses::subaddress_index)) + .select(assigned_subaddresses::subaddress_index) + .first(conn)?; + + Ok(highest_subaddress_index as u64 + 1) + } + + fn account_key(&self) -> Result, WalletDbError> { + if self.view_only { + return Ok(None); + } + + let account_key: AccountKey = mc_util_serial::decode(&self.account_key)?; + Ok(Some(account_key)) + } + + fn view_account_key(&self) -> Result { + if self.view_only { + return Ok(mc_util_serial::decode(&self.account_key)?); + } + + let account_key: AccountKey = mc_util_serial::decode(&self.account_key)?; + let view_account_key = ViewAccountKey::from(&account_key); + Ok(view_account_key) + } + + fn view_private_key(&self) -> Result { + Ok(*self.view_account_key()?.view_private_key()) + } } #[cfg(test)] mod tests { use super::*; - use crate::test_utils::{get_test_ledger, WalletDbTestContext}; + use crate::test_utils::WalletDbTestContext; use mc_account_keys::RootIdentity; use mc_common::logger::{test_with_logger, Logger}; use mc_util_from_random::FromRandom; @@ -498,7 +629,6 @@ mod tests { let db_test_context = WalletDbTestContext::default(); let wallet_db = db_test_context.get_db_instance(logger); - let ledger_db = get_test_ledger(5, &[], 12, &mut rng); let root_id = RootIdentity::from_random(&mut rng); let account_key = AccountKey::from(&root_id); @@ -513,7 +643,6 @@ mod tests { "".to_string(), "".to_string(), "".to_string(), - &ledger_db, &conn, ) .unwrap(); @@ -522,31 +651,28 @@ mod tests { { let conn = wallet_db.get_conn().unwrap(); - let res = Account::list_all(&conn).unwrap(); + let res = Account::list_all(&conn, None, None).unwrap(); assert_eq!(res.len(), 1); } let acc = Account::get(&account_id_hex, &wallet_db.get_conn().unwrap()).unwrap(); let expected_account = Account { - id: 1, - account_id_hex: account_id_hex.to_string(), + id: account_id_hex.to_string(), account_key: mc_util_serial::encode(&account_key), - entropy: root_id.root_entropy.bytes.to_vec(), + entropy: Some(root_id.root_entropy.bytes.to_vec()), key_derivation_version: 1, - main_subaddress_index: 0, - change_subaddress_index: CHANGE_SUBADDRESS_INDEX as i64, - next_subaddress_index: 2, first_block_index: 0, next_block_index: 0, import_block_index: None, name: "Alice's Main Account".to_string(), fog_enabled: false, + view_only: false, }; assert_eq!(expected_account, acc); // Verify that the subaddress table entries were updated for main and change let subaddresses = AssignedSubaddress::list_all( - &account_id_hex.to_string(), + Some(account_id_hex.to_string()), None, None, &wallet_db.get_conn().unwrap(), @@ -584,29 +710,25 @@ mod tests { "".to_string(), "".to_string(), "".to_string(), - &ledger_db, &wallet_db.get_conn().unwrap(), ) .unwrap(); - let res = Account::list_all(&wallet_db.get_conn().unwrap()).unwrap(); + let res = Account::list_all(&wallet_db.get_conn().unwrap(), None, None).unwrap(); assert_eq!(res.len(), 2); let acc_secondary = Account::get(&account_id_hex_secondary, &wallet_db.get_conn().unwrap()).unwrap(); let mut expected_account_secondary = Account { - id: 2, - account_id_hex: account_id_hex_secondary.to_string(), + id: account_id_hex_secondary.to_string(), account_key: mc_util_serial::encode(&account_key_secondary), - entropy: root_id_secondary.root_entropy.bytes.to_vec(), + entropy: Some(root_id_secondary.root_entropy.bytes.to_vec()), key_derivation_version: 1, - main_subaddress_index: 0, - change_subaddress_index: CHANGE_SUBADDRESS_INDEX as i64, - next_subaddress_index: 2, first_block_index: 50, next_block_index: 50, import_block_index: Some(50), name: "".to_string(), fog_enabled: false, + view_only: false, }; assert_eq!(expected_account_secondary, acc_secondary); @@ -627,7 +749,7 @@ mod tests { .delete(&wallet_db.get_conn().unwrap()) .unwrap(); - let res = Account::list_all(&wallet_db.get_conn().unwrap()).unwrap(); + let res = Account::list_all(&wallet_db.get_conn().unwrap(), None, None).unwrap(); assert_eq!(res.len(), 1); // Attempt to get the deleted account @@ -648,7 +770,6 @@ mod tests { let db_test_context = WalletDbTestContext::default(); let wallet_db = db_test_context.get_db_instance(logger); - let ledger_db = get_test_ledger(5, &[], 12, &mut rng); // Test providing entropy. let root_id = RootIdentity::from_random(&mut rng); @@ -664,14 +785,13 @@ mod tests { "".to_string(), "".to_string(), "".to_string(), - &ledger_db, &conn, ) .unwrap(); account_id_hex }; let account = Account::get(&account_id, &wallet_db.get_conn().unwrap()).unwrap(); - let decoded_entropy = RootEntropy::try_from(account.entropy.as_slice()).unwrap(); + let decoded_entropy = RootEntropy::try_from(account.entropy.unwrap().as_slice()).unwrap(); assert_eq!(decoded_entropy, root_id.root_entropy); let decoded_account_key: AccountKey = mc_util_serial::decode(&account.account_key).unwrap(); assert_eq!(decoded_account_key, account_key); @@ -683,7 +803,6 @@ mod tests { let db_test_context = WalletDbTestContext::default(); let wallet_db = db_test_context.get_db_instance(logger); - let ledger_db = get_test_ledger(5, &[], 12, &mut rng); let root_id = RootIdentity::from_random(&mut rng); let account_id_hex = { @@ -697,7 +816,6 @@ mod tests { "fog//some.fog.url".to_string(), "".to_string(), "DefinitelyARealFOGAuthoritySPKI".to_string(), - &ledger_db, &conn, ) .unwrap(); @@ -706,36 +824,85 @@ mod tests { { let conn = wallet_db.get_conn().unwrap(); - let res = Account::list_all(&conn).unwrap(); + let res = Account::list_all(&conn, None, None).unwrap(); assert_eq!(res.len(), 1); } let acc = Account::get(&account_id_hex, &wallet_db.get_conn().unwrap()).unwrap(); let expected_account = Account { - id: 1, - account_id_hex: account_id_hex.to_string(), + id: account_id_hex.to_string(), account_key: [ - 10, 34, 10, 32, 135, 37, 92, 17, 29, 203, 205, 96, 6, 72, 229, 193, 11, 153, 50, 3, - 233, 116, 186, 132, 203, 190, 186, 77, 17, 15, 246, 254, 22, 4, 161, 13, 18, 34, - 10, 32, 207, 106, 86, 24, 232, 51, 148, 230, 19, 117, 228, 119, 164, 182, 133, 190, - 22, 197, 0, 66, 5, 57, 33, 188, 231, 11, 121, 197, 253, 113, 40, 3, 26, 17, 102, - 111, 103, 47, 47, 115, 111, 109, 101, 46, 102, 111, 103, 46, 117, 114, 108, 42, 23, - 13, 231, 226, 158, 43, 94, 151, 32, 17, 121, 169, 69, 56, 96, 46, 182, 26, 43, 138, - 220, 146, 60, 162, + 10, 34, 10, 32, 129, 223, 141, 215, 200, 104, 120, 117, 123, 154, 151, 210, 253, + 23, 148, 151, 2, 18, 182, 100, 83, 138, 144, 99, 225, 74, 214, 14, 175, 68, 167, 4, + 18, 34, 10, 32, 24, 98, 18, 92, 9, 50, 142, 184, 114, 99, 34, 125, 211, 54, 146, + 33, 98, 71, 179, 56, 136, 67, 98, 97, 230, 228, 31, 194, 119, 169, 189, 8, 26, 17, + 102, 111, 103, 47, 47, 115, 111, 109, 101, 46, 102, 111, 103, 46, 117, 114, 108, + 42, 23, 13, 231, 226, 158, 43, 94, 151, 32, 17, 121, 169, 69, 56, 96, 46, 182, 26, + 43, 138, 220, 146, 60, 162, ] .to_vec(), - entropy: root_id.root_entropy.bytes.to_vec(), + entropy: Some(root_id.root_entropy.bytes.to_vec()), key_derivation_version: 1, - main_subaddress_index: 0, - change_subaddress_index: 0, - next_subaddress_index: 1, + first_block_index: 0, next_block_index: 0, import_block_index: None, name: "Alice's FOG Account".to_string(), fog_enabled: true, + view_only: false, }; - assert_eq!(expected_account, acc); } + + #[test_with_logger] + fn test_import_view_only_account(logger: Logger) { + let mut rng: StdRng = SeedableRng::from_seed([20u8; 32]); + + let db_test_context = WalletDbTestContext::default(); + let wallet_db = db_test_context.get_db_instance(logger); + + let view_private_key = RistrettoPrivate::from_random(&mut rng); + let spend_public_key = RistrettoPublic::from_random(&mut rng); + + let account = { + let conn = wallet_db.get_conn().unwrap(); + let account = Account::import_view_only( + &view_private_key, + &spend_public_key, + Some("View Only Account".to_string()), + 12, + None, + None, + &conn, + ) + .unwrap(); + account + }; + + { + let conn = wallet_db.get_conn().unwrap(); + let res = Account::list_all(&conn, None, None).unwrap(); + assert_eq!(res.len(), 1); + } + + let expected_account = Account { + id: account.id.to_string(), + account_key: [ + 10, 34, 10, 32, 66, 186, 14, 57, 108, 119, 153, 172, 224, 25, 53, 237, 22, 219, + 222, 137, 26, 227, 37, 43, 122, 52, 71, 153, 60, 246, 90, 102, 123, 176, 139, 11, + 18, 34, 10, 32, 28, 19, 114, 110, 204, 131, 192, 90, 192, 83, 149, 201, 140, 112, + 168, 124, 195, 19, 252, 208, 160, 39, 44, 28, 108, 143, 40, 149, 53, 137, 20, 47, + ] + .to_vec(), + entropy: None, + key_derivation_version: 2, + first_block_index: 0, + next_block_index: 0, + import_block_index: Some(12), + name: "View Only Account".to_string(), + fog_enabled: false, + view_only: true, + }; + assert_eq!(expected_account, account); + } } diff --git a/full-service/src/db/assigned_subaddress.rs b/full-service/src/db/assigned_subaddress.rs index 86fd50d6e..eff2e0887 100644 --- a/full-service/src/db/assigned_subaddress.rs +++ b/full-service/src/db/assigned_subaddress.rs @@ -3,10 +3,13 @@ //! A subaddress assigned to a particular contact for the purpose of tracking //! funds received from that contact. -use crate::db::{ - account::{AccountID, AccountModel}, - models::{Account, AssignedSubaddress, NewAssignedSubaddress, Txo}, - txo::TxoModel, +use crate::{ + db::{ + account::{AccountID, AccountModel}, + models::{Account, AssignedSubaddress, NewAssignedSubaddress, Txo}, + txo::TxoModel, + }, + util::b58::b58_decode_public_address, }; use crate::util::b58::b58_encode_public_address; @@ -16,7 +19,7 @@ use mc_transaction_core::{ ring_signature::KeyImage, }; -use mc_account_keys::AccountKey; +use mc_account_keys::{AccountKey, PublicAddress, ViewAccountKey}; use mc_crypto_keys::{CompressedRistrettoPublic, RistrettoPublic}; use mc_ledger_db::{Ledger, LedgerDB}; @@ -36,20 +39,25 @@ pub trait AssignedSubaddressModel { /// * `conn` - /// /// # Returns - /// * assigned_subaddress_b58 + /// * public_address_b58 fn create( account_key: &AccountKey, - address_book_entry: Option, subaddress_index: u64, comment: &str, - ledger_db: &LedgerDB, + conn: &Conn, + ) -> Result; + + fn create_for_view_only_account( + account_key: &ViewAccountKey, + subaddress_index: u64, + comment: &str, conn: &Conn, ) -> Result; /// Create the next subaddress for a given account. /// /// Returns: - /// * (assigned_subaddress_b58, subaddress_index) + /// * (public_address_b58, subaddress_index) fn create_next_for_account( account_id_hex: &str, comment: &str, @@ -57,7 +65,7 @@ pub trait AssignedSubaddressModel { conn: &Conn, ) -> Result<(String, i64), WalletDbError>; - /// Get the AssignedSubaddress for a given assigned_subaddress_b58 + /// Get the AssignedSubaddress for a given public_address_b58 fn get(public_address_b58: &str, conn: &Conn) -> Result; /// Get the Assigned Subaddress for a given index in an account, if it @@ -71,7 +79,7 @@ pub trait AssignedSubaddressModel { /// Find an AssignedSubaddress by the subaddress spend public key /// /// Returns: - /// * (subaddress_index, assigned_subaddress_b58) + /// * (subaddress_index, public_address_b58) fn find_by_subaddress_spend_public_key( subaddress_spend_public_key: &RistrettoPublic, conn: &Conn, @@ -79,7 +87,7 @@ pub trait AssignedSubaddressModel { /// List all AssignedSubaddresses for a given account. fn list_all( - account_id_hex: &str, + account_id: Option, offset: Option, limit: Option, conn: &Conn, @@ -87,102 +95,66 @@ pub trait AssignedSubaddressModel { /// Delete all AssignedSubaddresses for a given account. fn delete_all(account_id_hex: &str, conn: &Conn) -> Result<(), WalletDbError>; + + /// Helper to get the public address out of the assigned subaddress + fn public_address(self) -> Result; } impl AssignedSubaddressModel for AssignedSubaddress { fn create( account_key: &AccountKey, - address_book_entry: Option, subaddress_index: u64, comment: &str, - ledger_db: &LedgerDB, conn: &Conn, ) -> Result { - use crate::db::schema::{ - assigned_subaddresses, - transaction_logs::dsl::{ - account_id_hex as tx_log_account_id_hex, - transaction_id_hex as tx_log_transaction_id_hex, transaction_logs, - }, - }; + use crate::db::schema::assigned_subaddresses; let account_id = AccountID::from(account_key); - let view_private_key = account_key.view_private_key(); - let subaddress = account_key.subaddress(subaddress_index); - let subaddress_b58 = b58_encode_public_address(&subaddress)?; + let subaddress = account_key.subaddress(subaddress_index as u64); + + let subaddress_b58 = b58_encode_public_address(&subaddress)?; let subaddress_entry = NewAssignedSubaddress { - assigned_subaddress_b58: &subaddress_b58, - account_id_hex: &account_id.to_string(), - address_book_entry, - public_address: &mc_util_serial::encode(&subaddress), + public_address_b58: &subaddress_b58, + account_id: &account_id.to_string(), subaddress_index: subaddress_index as i64, comment, - subaddress_spend_key: &mc_util_serial::encode(subaddress.spend_public_key()), + spend_public_key: &subaddress.spend_public_key().to_bytes(), }; diesel::insert_into(assigned_subaddresses::table) .values(&subaddress_entry) .execute(conn)?; - // Find and repair orphaned txos at this subaddress. - let orphaned_txos = Txo::list_orphaned(&account_id.to_string(), None, None, None, conn)?; + Ok(subaddress_b58) + } - for orphaned_txo in orphaned_txos.iter() { - let tx_out_target_key: RistrettoPublic = - mc_util_serial::decode(&orphaned_txo.target_key).unwrap(); - let tx_public_key: RistrettoPublic = - mc_util_serial::decode(&orphaned_txo.public_key).unwrap(); - let txo_public_key = CompressedRistrettoPublic::from(tx_public_key); + fn create_for_view_only_account( + account_key: &ViewAccountKey, + subaddress_index: u64, + comment: &str, + conn: &Conn, + ) -> Result { + use crate::db::schema::assigned_subaddresses; - let txo_subaddress_spk: RistrettoPublic = recover_public_subaddress_spend_key( - view_private_key, - &tx_out_target_key, - &tx_public_key, - ); + let account_id = AccountID::from(account_key); - if txo_subaddress_spk == *subaddress.spend_public_key() { - let onetime_private_key = recover_onetime_private_key( - &tx_public_key, - account_key.view_private_key(), - &account_key.subaddress_spend_private(subaddress_index), - ); + let subaddress = account_key.subaddress(subaddress_index); + let public_address_b58 = b58_encode_public_address(&subaddress)?; - let key_image = KeyImage::from(&onetime_private_key); + let subaddress_entry = NewAssignedSubaddress { + public_address_b58: &public_address_b58, + account_id: &account_id.to_string(), + subaddress_index: subaddress_index as i64, + comment, + spend_public_key: &subaddress.spend_public_key().to_bytes(), + }; - if ledger_db.contains_key_image(&key_image)? { - let txo_index = ledger_db.get_tx_out_index_by_public_key(&txo_public_key)?; - let block_index = ledger_db.get_block_index_by_tx_out_index(txo_index)?; - diesel::update(orphaned_txo) - .set( - crate::db::schema::txos::spent_block_index.eq(Some(block_index as i64)), - ) - .execute(conn)?; - } + diesel::insert_into(assigned_subaddresses::table) + .values(&subaddress_entry) + .execute(conn)?; - let key_image_bytes = mc_util_serial::encode(&key_image); - - // Update the account status mapping. - diesel::update(orphaned_txo) - .set(( - crate::db::schema::txos::subaddress_index.eq(subaddress_index as i64), - crate::db::schema::txos::key_image.eq(key_image_bytes), - )) - .execute(conn)?; - - diesel::update( - transaction_logs - .filter(tx_log_transaction_id_hex.eq(&orphaned_txo.txo_id_hex)) - .filter(tx_log_account_id_hex.eq(&account_id.to_string())), - ) - .set( - (crate::db::schema::transaction_logs::assigned_subaddress_b58 - .eq(&subaddress_b58),), - ) - .execute(conn)?; - } - } - Ok(subaddress_b58) + Ok(public_address_b58) } fn create_next_for_account( @@ -191,115 +163,120 @@ impl AssignedSubaddressModel for AssignedSubaddress { ledger_db: &LedgerDB, conn: &Conn, ) -> Result<(String, i64), WalletDbError> { - use crate::db::schema::{ - accounts::dsl::{account_id_hex as dsl_account_id_hex, accounts}, - assigned_subaddresses, - transaction_logs::dsl::{ - account_id_hex as tx_log_account_id_hex, - transaction_id_hex as tx_log_transaction_id_hex, transaction_logs, - }, - }; - let account = Account::get(&AccountID(account_id_hex.to_string()), conn)?; if account.fog_enabled { return Err(WalletDbError::SubaddressesNotSupportedForFOGEnabledAccounts); } - let account_key: AccountKey = mc_util_serial::decode(&account.account_key)?; - let view_private_key = account_key.view_private_key(); - let subaddress_index = account.next_subaddress_index; - let subaddress = account_key.subaddress(subaddress_index as u64); - - let subaddress_b58 = b58_encode_public_address(&subaddress)?; - let subaddress_entry = NewAssignedSubaddress { - assigned_subaddress_b58: &subaddress_b58, - account_id_hex, - address_book_entry: None, /* FIXME: WS-8 - Address Book Entry if details - * provided, or None always for main? */ - public_address: &mc_util_serial::encode(&subaddress), - subaddress_index: subaddress_index as i64, - comment, - subaddress_spend_key: &mc_util_serial::encode(subaddress.spend_public_key()), - }; + let (subaddress_b58, next_subaddress_index) = if account.view_only { + let view_account_key: ViewAccountKey = mc_util_serial::decode(&account.account_key)?; + let next_subaddress_index = account.next_subaddress_index(conn)?; + let subaddress_b58 = AssignedSubaddress::create_for_view_only_account( + &view_account_key, + next_subaddress_index, + comment, + conn, + )?; + + let subaddress = view_account_key.subaddress(next_subaddress_index); + + // Find and repair orphaned txos at this subaddress. + let orphaned_txos = + Txo::list_orphaned(Some(account_id_hex), None, None, None, None, None, conn)?; + + for orphaned_txo in orphaned_txos.iter() { + let tx_out_target_key: RistrettoPublic = + mc_util_serial::decode(&orphaned_txo.target_key)?; + let tx_public_key: RistrettoPublic = + mc_util_serial::decode(&orphaned_txo.public_key)?; + + let txo_subaddress_spk: RistrettoPublic = recover_public_subaddress_spend_key( + view_account_key.view_private_key(), + &tx_out_target_key, + &tx_public_key, + ); - diesel::insert_into(assigned_subaddresses::table) - .values(&subaddress_entry) - .execute(conn)?; + if txo_subaddress_spk == *subaddress.spend_public_key() { + // Update the account status mapping. + diesel::update(orphaned_txo) + .set((crate::db::schema::txos::subaddress_index + .eq(next_subaddress_index as i64),)) + .execute(conn)?; + } + } - // Update the next subaddress index for the account - diesel::update(accounts.filter(dsl_account_id_hex.eq(account_id_hex))) - .set((crate::db::schema::accounts::next_subaddress_index.eq(subaddress_index + 1),)) - .execute(conn)?; + (subaddress_b58, next_subaddress_index) + } else { + let account_key: AccountKey = mc_util_serial::decode(&account.account_key)?; + let next_subaddress_index = account.next_subaddress_index(conn)?; + let subaddress_b58 = + AssignedSubaddress::create(&account_key, next_subaddress_index, comment, conn)?; - // Find and repair orphaned txos at this subaddress. - let orphaned_txos = Txo::list_orphaned(account_id_hex, None, None, None, conn)?; + let subaddress = account_key.subaddress(next_subaddress_index); - for orphaned_txo in orphaned_txos.iter() { - let tx_out_target_key: RistrettoPublic = - mc_util_serial::decode(&orphaned_txo.target_key).unwrap(); - let tx_public_key: RistrettoPublic = - mc_util_serial::decode(&orphaned_txo.public_key).unwrap(); - let txo_public_key = CompressedRistrettoPublic::from(tx_public_key); + // Find and repair orphaned txos at this subaddress. + let orphaned_txos = + Txo::list_orphaned(Some(account_id_hex), None, None, None, None, None, conn)?; - let txo_subaddress_spk: RistrettoPublic = recover_public_subaddress_spend_key( - view_private_key, - &tx_out_target_key, - &tx_public_key, - ); + for orphaned_txo in orphaned_txos.iter() { + let tx_out_target_key: RistrettoPublic = + mc_util_serial::decode(&orphaned_txo.target_key)?; + let tx_public_key: RistrettoPublic = + mc_util_serial::decode(&orphaned_txo.public_key)?; + let txo_public_key = CompressedRistrettoPublic::from(tx_public_key); - if txo_subaddress_spk == *subaddress.spend_public_key() { - let onetime_private_key = recover_onetime_private_key( - &tx_public_key, + let txo_subaddress_spk: RistrettoPublic = recover_public_subaddress_spend_key( account_key.view_private_key(), - &account_key.subaddress_spend_private(subaddress_index as u64), + &tx_out_target_key, + &tx_public_key, ); - let key_image = KeyImage::from(&onetime_private_key); - - if ledger_db.contains_key_image(&key_image)? { - let txo_index = ledger_db.get_tx_out_index_by_public_key(&txo_public_key)?; - let block_index = ledger_db.get_block_index_by_tx_out_index(txo_index)?; + if txo_subaddress_spk == *subaddress.spend_public_key() { + let onetime_private_key = recover_onetime_private_key( + &tx_public_key, + account_key.view_private_key(), + &account_key.subaddress_spend_private(next_subaddress_index), + ); + + let key_image = KeyImage::from(&onetime_private_key); + + if ledger_db.contains_key_image(&key_image)? { + let txo_index = + ledger_db.get_tx_out_index_by_public_key(&txo_public_key)?; + let block_index = ledger_db.get_block_index_by_tx_out_index(txo_index)?; + diesel::update(orphaned_txo) + .set( + crate::db::schema::txos::spent_block_index + .eq(Some(block_index as i64)), + ) + .execute(conn)?; + } + + let key_image_bytes = mc_util_serial::encode(&key_image); + + // Update the account status mapping. diesel::update(orphaned_txo) - .set( - crate::db::schema::txos::spent_block_index.eq(Some(block_index as i64)), - ) + .set(( + crate::db::schema::txos::subaddress_index + .eq(next_subaddress_index as i64), + crate::db::schema::txos::key_image.eq(key_image_bytes), + )) .execute(conn)?; } - - let key_image_bytes = mc_util_serial::encode(&key_image); - - // Update the account status mapping. - diesel::update(orphaned_txo) - .set(( - crate::db::schema::txos::subaddress_index.eq(subaddress_index), - crate::db::schema::txos::key_image.eq(key_image_bytes), - )) - .execute(conn)?; - - diesel::update( - transaction_logs - .filter(tx_log_transaction_id_hex.eq(&orphaned_txo.txo_id_hex)) - .filter(tx_log_account_id_hex.eq(account_id_hex)), - ) - .set( - (crate::db::schema::transaction_logs::assigned_subaddress_b58 - .eq(&subaddress_b58),), - ) - .execute(conn)?; } - } - Ok((subaddress_b58, subaddress_index)) + (subaddress_b58, next_subaddress_index) + }; + + Ok((subaddress_b58, next_subaddress_index as i64)) } fn get(public_address_b58: &str, conn: &Conn) -> Result { - use crate::db::schema::assigned_subaddresses::dsl::{ - assigned_subaddress_b58, assigned_subaddresses, - }; + use crate::db::schema::assigned_subaddresses; - let assigned_subaddress: AssignedSubaddress = match assigned_subaddresses - .filter(assigned_subaddress_b58.eq(&public_address_b58)) + let assigned_subaddress: AssignedSubaddress = match assigned_subaddresses::table + .filter(assigned_subaddresses::public_address_b58.eq(&public_address_b58)) .get_result::(conn) { Ok(t) => t, @@ -321,26 +298,29 @@ impl AssignedSubaddressModel for AssignedSubaddress { index: i64, conn: &Conn, ) -> Result { - let account = Account::get(&AccountID(account_id_hex.to_string()), conn)?; + use crate::db::schema::assigned_subaddresses; - let account_key: AccountKey = mc_util_serial::decode(&account.account_key)?; - let subaddress = account_key.subaddress(index as u64); - - let subaddress_b58 = b58_encode_public_address(&subaddress)?; - Self::get(&subaddress_b58, conn) + Ok(assigned_subaddresses::table + .filter(assigned_subaddresses::account_id.eq(account_id_hex)) + .filter(assigned_subaddresses::subaddress_index.eq(index)) + .first(conn)?) } fn find_by_subaddress_spend_public_key( subaddress_spend_public_key: &RistrettoPublic, conn: &Conn, ) -> Result<(i64, String), WalletDbError> { - use crate::db::schema::assigned_subaddresses::{ - account_id_hex, dsl::assigned_subaddresses, subaddress_index, subaddress_spend_key, - }; - - let matches = assigned_subaddresses - .select((subaddress_index, account_id_hex)) - .filter(subaddress_spend_key.eq(mc_util_serial::encode(subaddress_spend_public_key))) + use crate::db::schema::assigned_subaddresses; + + let matches = assigned_subaddresses::table + .select(( + assigned_subaddresses::subaddress_index, + assigned_subaddresses::account_id, + )) + .filter( + assigned_subaddresses::spend_public_key + .eq(subaddress_spend_public_key.to_bytes().to_vec()), + ) .load::<(i64, String)>(conn)?; if matches.is_empty() { @@ -359,38 +339,40 @@ impl AssignedSubaddressModel for AssignedSubaddress { } fn list_all( - account_id_hex: &str, + account_id: Option, offset: Option, limit: Option, conn: &Conn, ) -> Result, WalletDbError> { - use crate::db::schema::assigned_subaddresses::{ - account_id_hex as schema_account_id_hex, all_columns, dsl::assigned_subaddresses, - }; + use crate::db::schema::assigned_subaddresses; - let addresses_query = assigned_subaddresses - .select(all_columns) - .filter(schema_account_id_hex.eq(account_id_hex)); + let mut addresses_query = assigned_subaddresses::table.into_boxed(); - let addresses: Vec = if let (Some(o), Some(l)) = (offset, limit) { - addresses_query - .offset(o as i64) - .limit(l as i64) - .load(conn)? - } else { - addresses_query.load(conn)? - }; + if let Some(account_id) = account_id { + addresses_query = + addresses_query.filter(assigned_subaddresses::account_id.eq(account_id)); + } + + if let (Some(offset), Some(limit)) = (offset, limit) { + addresses_query = addresses_query.offset(offset as i64).limit(limit as i64); + } - Ok(addresses) + Ok(addresses_query.load(conn)?) } fn delete_all(account_id_hex: &str, conn: &Conn) -> Result<(), WalletDbError> { - use crate::db::schema::assigned_subaddresses::dsl::{ - account_id_hex as schema_account_id_hex, assigned_subaddresses, - }; + use crate::db::schema::assigned_subaddresses; - diesel::delete(assigned_subaddresses.filter(schema_account_id_hex.eq(account_id_hex))) - .execute(conn)?; + diesel::delete( + assigned_subaddresses::table + .filter(assigned_subaddresses::account_id.eq(account_id_hex)), + ) + .execute(conn)?; Ok(()) } + + fn public_address(self) -> Result { + let public_address = b58_decode_public_address(&self.public_address_b58)?; + Ok(public_address) + } } diff --git a/full-service/src/db/gift_code.rs b/full-service/src/db/gift_code.rs index dc5b7c69b..fcee818dd 100644 --- a/full-service/src/db/gift_code.rs +++ b/full-service/src/db/gift_code.rs @@ -44,7 +44,11 @@ pub trait GiftCodeModel { fn get(gift_code_b58: &EncodedGiftCode, conn: &Conn) -> Result; /// Get all Gift Codes in this wallet. - fn list_all(conn: &Conn) -> Result, WalletDbError>; + fn list_all( + conn: &Conn, + offset: Option, + limit: Option, + ) -> Result, WalletDbError>; /// Delete a gift code. fn delete(self, conn: &Conn) -> Result<(), WalletDbError>; @@ -87,12 +91,20 @@ impl GiftCodeModel for GiftCode { } } - fn list_all(conn: &Conn) -> Result, WalletDbError> { + fn list_all( + conn: &Conn, + offset: Option, + limit: Option, + ) -> Result, WalletDbError> { use crate::db::schema::gift_codes; - Ok(gift_codes::table - .select(gift_codes::all_columns) - .load::(conn)?) + let mut query = gift_codes::table.into_boxed(); + + if let (Some(offset), Some(limit)) = (offset, limit) { + query = query.offset(offset as i64).limit(limit as i64); + } + + Ok(query.load(conn)?) } fn delete(self, conn: &Conn) -> Result<(), WalletDbError> { @@ -159,7 +171,8 @@ mod tests { }; assert_eq!(gotten, expected_gift_code); - let all_gift_codes = GiftCode::list_all(&wallet_db.get_conn().unwrap()).unwrap(); + let all_gift_codes = + GiftCode::list_all(&wallet_db.get_conn().unwrap(), None, None).unwrap(); assert_eq!(all_gift_codes.len(), 1); assert_eq!(all_gift_codes[0], expected_gift_code); } diff --git a/full-service/src/db/migration_testing/migration_testing.rs b/full-service/src/db/migration_testing/migration_testing.rs deleted file mode 100644 index e502d548f..000000000 --- a/full-service/src/db/migration_testing/migration_testing.rs +++ /dev/null @@ -1,59 +0,0 @@ -// #[cfg(test)] -// mod migration_testing { -// use crate::{ -// db::{ -// account::AccountID, -// migration_testing::{ -// seed_accounts::{seed_accounts, test_accounts}, -// seed_gift_codes::{seed_gift_codes, test_gift_codes}, -// seed_txos::{seed_txos, test_txos}, -// }, -// }, -// test_utils::{get_test_ledger, setup_wallet_service, -// WalletDbTestContext}, }; -// use diesel_migrations::{revert_latest_migration, run_pending_migrations}; -// use mc_account_keys::{AccountKey, PublicAddress}; -// use mc_common::logger::{test_with_logger, Logger}; -// use rand::{rngs::StdRng, SeedableRng}; - -// #[test_with_logger] -// fn test_latest_migration(logger: Logger) { -// // set up wallet and service. this will run all migrations -// let mut rng: StdRng = SeedableRng::from_seed([20u8; 32]); -// let known_recipients: Vec = Vec::new(); -// let mut ledger_db = get_test_ledger(5, &known_recipients, 12, &mut -// rng); let _db_test_context = WalletDbTestContext::default(); -// let service = setup_wallet_service(ledger_db.clone(), -// logger.clone()); let wallet_db = &service.wallet_db; -// let conn = wallet_db.get_conn().unwrap(); - -// // revert the last migration -// // revert_latest_migration(&conn).unwrap(); - -// // seed the entities -// let (txo_account, gift_code_account, gift_code_receiver_account) = -// seed_accounts(&service); seed_txos(&conn, &mut ledger_db, &wallet_db, -// &logger, &txo_account); let gift_codes = seed_gift_codes( -// &conn, -// &mut ledger_db, -// &wallet_db, -// &service, -// &logger, -// &gift_code_account, -// &gift_code_receiver_account, -// ); - -// let account_key: AccountKey = -// -// mc_util_serial::decode(txo_account.account_key.as_slice()).unwrap(); -// let txo_account_id = AccountID::from(&account_key); - -// // run the last migration -// // run_pending_migrations(&conn).unwrap(); - -// // validate expected state of entities in DB again, post-migration -// test_accounts(&service); -// test_txos(txo_account_id, &conn); -// test_gift_codes(&gift_codes, &service); -// } -// } diff --git a/full-service/src/db/migration_testing/mod.rs b/full-service/src/db/migration_testing/mod.rs deleted file mode 100644 index cd352da55..000000000 --- a/full-service/src/db/migration_testing/mod.rs +++ /dev/null @@ -1,5 +0,0 @@ -#[cfg(any(test))] -// pub mod migration_testing; -pub mod seed_accounts; -pub mod seed_gift_codes; -pub mod seed_txos; diff --git a/full-service/src/db/migration_testing/seed_accounts.rs b/full-service/src/db/migration_testing/seed_accounts.rs deleted file mode 100644 index f9c51a1e2..000000000 --- a/full-service/src/db/migration_testing/seed_accounts.rs +++ /dev/null @@ -1,48 +0,0 @@ -use crate::{ - db::models::Account, - service::{account::AccountService, WalletService}, -}; -use mc_connection_test_utils::MockBlockchainConnection; -use mc_fog_report_validation::MockFogPubkeyResolver; -use mc_ledger_db::LedgerDB; - -pub fn seed_accounts( - service: &WalletService, MockFogPubkeyResolver>, -) -> (Account, Account, Account) { - let txo_account = service - .create_account( - Some("txo_account".to_string()), - "".to_string(), - "".to_string(), - "".to_string(), - ) - .unwrap(); - - let gift_code_account = service - .create_account( - Some("gift_code_account".to_string()), - "".to_string(), - "".to_string(), - "".to_string(), - ) - .unwrap(); - - let gift_code_receiver_account = service - .create_account( - Some("gift_code_receiver_account".to_string()), - "".to_string(), - "".to_string(), - "".to_string(), - ) - .unwrap(); - - (txo_account, gift_code_account, gift_code_receiver_account) -} - -pub fn test_accounts( - service: &WalletService, MockFogPubkeyResolver>, -) { - let accounts = service.list_accounts().unwrap(); - - assert_eq!(accounts.len(), 3); -} diff --git a/full-service/src/db/migration_testing/seed_gift_codes.rs b/full-service/src/db/migration_testing/seed_gift_codes.rs deleted file mode 100644 index d78c3a6f2..000000000 --- a/full-service/src/db/migration_testing/seed_gift_codes.rs +++ /dev/null @@ -1,165 +0,0 @@ -use crate::{ - db::{account::AccountID, models::Account, WalletDb}, - service::{ - gift_code::{EncodedGiftCode, GiftCodeService, GiftCodeStatus}, - WalletService, - }, - test_utils::{ - add_block_to_ledger_db, add_block_with_tx, add_block_with_tx_proposal, - manually_sync_account, MOB, - }, -}; -use diesel::{ - r2d2::{ConnectionManager, PooledConnection}, - SqliteConnection, -}; -use mc_account_keys::AccountKey; -use mc_common::logger::Logger; -use mc_connection_test_utils::MockBlockchainConnection; -use mc_crypto_rand::RngCore; -use mc_fog_report_validation::MockFogPubkeyResolver; -use mc_ledger_db::LedgerDB; -use mc_transaction_core::ring_signature::KeyImage; -use rand::{rngs::StdRng, SeedableRng}; - -pub struct SeedGiftCodesResult { - unsubmitted: EncodedGiftCode, - submitted: EncodedGiftCode, - claimed: EncodedGiftCode, -} -pub fn seed_gift_codes( - _conn: &PooledConnection>, - ledger_db: &mut LedgerDB, - wallet_db: &WalletDb, - service: &WalletService, MockFogPubkeyResolver>, - logger: &Logger, - account: &Account, - receiver_account: &Account, -) -> SeedGiftCodesResult { - let mut rng: StdRng = SeedableRng::from_seed([20u8; 32]); - - // Add a block with a transaction for the gifter account - let gifter_account_key: AccountKey = mc_util_serial::decode(&account.account_key).unwrap(); - let gifter_public_address = - &gifter_account_key.subaddress(account.main_subaddress_index as u64); - let gifter_account_id = AccountID(account.account_id_hex.to_string()); - - add_block_to_ledger_db( - ledger_db, - &vec![gifter_public_address.clone()], - 100 * MOB as u64, - &vec![KeyImage::from(rng.next_u64())], - &mut rng, - ); - manually_sync_account(ledger_db, wallet_db, &gifter_account_id, logger); - - // Create 3 gift codes - let (tx_proposal, gift_code_b58) = service - .build_gift_code( - &gifter_account_id, - 2 * MOB as u64, - Some("Gift code".to_string()), - None, - None, - None, - None, - ) - .unwrap(); - - // going to submit but not claim this code - let gift_code_1_submitted = service - .submit_gift_code( - &gifter_account_id, - &gift_code_b58.clone(), - &tx_proposal.clone(), - ) - .unwrap(); - - add_block_with_tx_proposal(ledger_db, tx_proposal); - manually_sync_account(&ledger_db, &service.wallet_db, &gifter_account_id, &logger); - - let (tx_proposal, gift_code_b58) = service - .build_gift_code( - &gifter_account_id, - 2 * MOB as u64, - Some("Gift code".to_string()), - None, - None, - None, - None, - ) - .unwrap(); - - // going to submit and claim this one - let gift_code_2_claimed = service - .submit_gift_code( - &gifter_account_id, - &gift_code_b58.clone(), - &tx_proposal.clone(), - ) - .unwrap(); - - add_block_with_tx_proposal(ledger_db, tx_proposal); - manually_sync_account(&ledger_db, &service.wallet_db, &gifter_account_id, &logger); - - // leave this code as pending - let (_tx_proposal, gift_code_b58_pending) = service - .build_gift_code( - &gifter_account_id, - 2 * MOB as u64, - Some("Gift code".to_string()), - None, - None, - None, - None, - ) - .unwrap(); - - // Claim the gift code to another account - manually_sync_account( - &ledger_db, - &service.wallet_db, - &AccountID(receiver_account.account_id_hex.clone()), - &logger, - ); - - let tx = service - .claim_gift_code( - &EncodedGiftCode(gift_code_2_claimed.gift_code_b58.clone()), - &AccountID(receiver_account.account_id_hex.clone()), - None, - ) - .unwrap(); - add_block_with_tx(ledger_db, tx); - manually_sync_account( - &ledger_db, - &service.wallet_db, - &AccountID(receiver_account.account_id_hex.clone()), - &logger, - ); - - SeedGiftCodesResult { - unsubmitted: gift_code_b58_pending, - submitted: EncodedGiftCode(gift_code_1_submitted.gift_code_b58), - claimed: EncodedGiftCode(gift_code_2_claimed.gift_code_b58), - } -} - -pub fn test_gift_codes( - gift_codes: &SeedGiftCodesResult, - service: &WalletService, MockFogPubkeyResolver>, -) { - let (status, _gift_code_value_opt, _memo) = service - .check_gift_code_status(&gift_codes.unsubmitted) - .unwrap(); - assert_eq!(status, GiftCodeStatus::GiftCodeSubmittedPending); - - let (status, _gift_code_value_opt, _memo) = service - .check_gift_code_status(&gift_codes.submitted) - .unwrap(); - assert_eq!(status, GiftCodeStatus::GiftCodeAvailable); - - let (status, _gift_code_value_opt, _memo) = - service.check_gift_code_status(&gift_codes.claimed).unwrap(); - assert_eq!(status, GiftCodeStatus::GiftCodeClaimed); -} diff --git a/full-service/src/db/migration_testing/seed_txos.rs b/full-service/src/db/migration_testing/seed_txos.rs deleted file mode 100644 index 3290db6d4..000000000 --- a/full-service/src/db/migration_testing/seed_txos.rs +++ /dev/null @@ -1,122 +0,0 @@ -use crate::{ - db::{ - account::AccountID, - models::{Account, TransactionLog, Txo}, - transaction_log::TransactionLogModel, - txo::TxoModel, - WalletDb, - }, - test_utils::{ - add_block_with_db_txos, add_block_with_tx_outs, create_test_minted_and_change_txos, - create_test_txo_for_recipient, manually_sync_account, MOB, - }, -}; -use diesel::{ - r2d2::{ConnectionManager, PooledConnection}, - SqliteConnection, -}; -use mc_common::logger::Logger; -use mc_crypto_rand::RngCore; -use mc_ledger_db::LedgerDB; -use mc_transaction_core::{ring_signature::KeyImage, tokens::Mob, Amount, Token}; -use rand::{rngs::StdRng, SeedableRng}; - -// create 1 spent, 1 change (minted), and 1 orphaned txo -pub fn seed_txos( - _conn: &PooledConnection>, - ledger_db: &mut LedgerDB, - wallet_db: &WalletDb, - logger: &Logger, - account: &Account, -) { - let mut rng: StdRng = SeedableRng::from_seed([20u8; 32]); - // Create received txo for account - let account_key = mc_util_serial::decode(&account.account_key).unwrap(); - let (for_account_txo, for_account_key_image) = - create_test_txo_for_recipient(&account_key, 0, Amount::new(1000 * MOB, Mob::ID), &mut rng); - - // add this txo to the ledger - add_block_with_tx_outs( - ledger_db, - &[for_account_txo.clone()], - &[KeyImage::from(rng.next_u64())], - ); - - manually_sync_account( - &ledger_db, - &wallet_db, - &AccountID::from(&account_key), - &logger, - ); - - // "spend" the TXO by sending it to same account, but at a subaddress we - // have not yet assigned. At the DB layer, we accomplish this by - // constructing the output txos, then logging sent and received for this - // account. - let ((output_txo_id, _output_value), (change_txo_id, _change_value)) = - create_test_minted_and_change_txos( - account_key.clone(), - account_key.subaddress(4), - 33 * MOB, - wallet_db.clone(), - ledger_db.clone(), - logger.clone(), - ); - - add_block_with_db_txos( - ledger_db, - &wallet_db, - &[output_txo_id, change_txo_id], - &[KeyImage::from(for_account_key_image)], - ); - - manually_sync_account( - &ledger_db, - &wallet_db, - &AccountID::from(&account_key), - &logger, - ); -} - -pub fn test_txos( - account_id: AccountID, - conn: &PooledConnection>, -) { - // validate expected txo states - let txos = - Txo::list_for_account(&account_id.to_string(), None, None, None, Some(0), &conn).unwrap(); - assert_eq!(txos.len(), 3); - - // Check that we have 2 spendable (1 is orphaned) - let spendable: Vec<&Txo> = txos.iter().filter(|f| f.key_image.is_some()).collect(); - assert_eq!(spendable.len(), 2); - - // Check that we have one spent - went from [Received, Unspent] -> [Received, - // Spent] - let spent = Txo::list_spent(&account_id.to_string(), None, Some(0), None, None, &conn).unwrap(); - assert_eq!(spent.len(), 1); - assert_eq!(spent[0].spent_block_index.clone().unwrap(), 13); - assert_eq!(spent[0].minted_account_id_hex, None); - - // Check that we have one orphaned - went from [Minted, Secreted] -> [Minted, - // Orphaned] - let orphaned = Txo::list_orphaned(&account_id.to_string(), Some(0), None, None, &conn).unwrap(); - assert_eq!(orphaned.len(), 1); - assert!(orphaned[0].key_image.is_none()); - assert_eq!(orphaned[0].received_block_index.clone().unwrap(), 13); - assert!(orphaned[0].minted_account_id_hex.is_some()); - assert!(orphaned[0].received_account_id_hex.is_some()); - - // Check that we have one unspent (change) - went from [Minted, Secreted] -> - // [Minted, Unspent] - let unspent = - Txo::list_unspent(&account_id.to_string(), None, Some(0), None, None, &conn).unwrap(); - assert_eq!(unspent.len(), 1); - assert_eq!(unspent[0].received_block_index.clone().unwrap(), 13); - - // Check that a transaction log entry was created for each received TxOut (note: - // we are not creating submit logs in this test) - let transaction_logs = - TransactionLog::list_all(&account_id.to_string(), None, None, None, None, &conn).unwrap(); - assert_eq!(transaction_logs.len(), 3); -} diff --git a/full-service/src/db/mod.rs b/full-service/src/db/mod.rs index 72bb75353..84012d0c6 100644 --- a/full-service/src/db/mod.rs +++ b/full-service/src/db/mod.rs @@ -10,14 +10,8 @@ pub mod models; pub mod schema; pub mod transaction_log; pub mod txo; -pub mod view_only_account; -pub mod view_only_subaddress; -pub mod view_only_txo; mod wallet_db; mod wallet_db_error; pub use wallet_db::{transaction, Conn, WalletDb}; pub use wallet_db_error::WalletDbError; - -#[cfg(any(test))] -pub mod migration_testing; diff --git a/full-service/src/db/models.rs b/full-service/src/db/models.rs index 3be707bcf..5137522f4 100644 --- a/full-service/src/db/models.rs +++ b/full-service/src/db/models.rs @@ -3,93 +3,24 @@ //! DB Models use super::schema::{ - accounts, assigned_subaddresses, gift_codes, transaction_logs, transaction_txo_types, txos, - view_only_accounts, view_only_subaddresses, view_only_txos, + __diesel_schema_migrations, accounts, assigned_subaddresses, gift_codes, + transaction_input_txos, transaction_logs, transaction_output_txos, txos, }; +use mc_crypto_keys::CompressedRistrettoPublic; use serde::Serialize; -// The following string constants are used in lieu of proper enum plumbing for -// SQLite3 with Diesel at the time of authorship. Ideally, we will migrate to -// enums at some point. - -/// A TXO owned by an account in this wallet that has not yet been spent. -pub const TXO_STATUS_UNSPENT: &str = "txo_status_unspent"; - -/// A TXO owned by an account in this wallet that is used by a pending -/// transaction. -pub const TXO_STATUS_PENDING: &str = "txo_status_pending"; - -/// A TXO owned by an account in this wallet that has been spent. -pub const TXO_STATUS_SPENT: &str = "txo_status_spent"; - -/// A TXO created by an account in this wallet for use as an output in an -/// outgoing transaction. -pub const TXO_STATUS_SECRETED: &str = "txo_status_secreted"; - -/// The TXO is owned by this wallet, but not yet spendable (i.e., receiving -/// subaddress is unknown). -pub const TXO_STATUS_ORPHANED: &str = "txo_status_orphaned"; - -/// A Txo that has been created locally, but is not yet in the ledger. -pub const TXO_TYPE_MINTED: &str = "txo_type_minted"; - -/// A Txo in the ledger that belongs to an account in this wallet. -pub const TXO_TYPE_RECEIVED: &str = "txo_type_received"; - -/// A transaction that has been built locally. -pub const TX_STATUS_BUILT: &str = "tx_status_built"; - -/// A transaction that has been submitted to the MobileCoin network. -pub const TX_STATUS_PENDING: &str = "tx_status_pending"; - -/// A transaction that appears to have been processed by the MobileCoin network. -pub const TX_STATUS_SUCCEEDED: &str = "tx_status_succeeded"; - -/// A transaction that was rejected by the MobileCoin network, or that expired -/// before it could be processed. -pub const TX_STATUS_FAILED: &str = "tx_status_failed"; - -/// A transaction created by an account in this wallet. -pub const TX_DIRECTION_SENT: &str = "tx_direction_sent"; - -/// A TxOut received by an account in this wallet. -pub const TX_DIRECTION_RECEIVED: &str = "tx_direction_received"; - -/// A transaction output that is used as an input to a new transaction. -pub const TXO_USED_AS_INPUT: &str = "txo_used_as_input"; - -/// A transaction output that is used as an output of a new transaction. -pub const TXO_USED_AS_OUTPUT: &str = "txo_used_as_output"; - -/// A transaction output used as a change output of a new transaction. -pub const TXO_USED_AS_CHANGE: &str = "txo_used_as_change"; - /// An Account entity. /// /// Contains the account private keys, subaddress configuration, and ... #[derive(Clone, Serialize, Identifiable, Queryable, PartialEq, Debug)] #[primary_key(id)] pub struct Account { - /// Primary key - pub id: i32, - /// An additional ID, derived from the account data. - pub account_id_hex: String, - /// Private keys for viewing and spending the MobileCoin belonging to an - /// account. + /// Primary key, derived from the account data. + pub id: String, pub account_key: Vec, - /// The private entropy for this account, used to derive the view and send - /// keys which comprise the account_key. - pub entropy: Vec, - /// Which version of key derivation we are using. + pub entropy: Option>, pub key_derivation_version: i32, - /// Default subadress that is given out to refer to this account. - pub main_subaddress_index: i64, - /// Subaddress used to return transaction "change" to self. - pub change_subaddress_index: i64, - /// The next unused subaddress index. (Assumes indices are used sequentially - /// from 0). - pub next_subaddress_index: i64, /// Index of the first block where this account may have held funds. pub first_block_index: i64, /// Index of the next block to inspect for transactions related to this @@ -101,56 +32,8 @@ pub struct Account { pub import_block_index: Option, /// Name of this account. pub name: String, /* empty string for nullable */ - /// Fog enabled address pub fog_enabled: bool, -} - -/// A View Only Account entity. -/// -/// Contains the account view private key -#[derive(Clone, Serialize, Identifiable, Queryable, PartialEq, Debug)] -#[primary_key(id)] -pub struct ViewOnlyAccount { - /// Primary key - pub id: i32, - /// An additional ID, derived from the account data. - pub account_id_hex: String, - /// private key for viewing MobileCoin belonging to an account. - pub view_private_key: Vec, - /// Index of the first block where this account may have held funds. - pub first_block_index: i64, - /// Index of the next block to inspect for transactions related to this - /// account. - pub next_block_index: i64, - /// Default subadress that is given out to refer to this account. - pub main_subaddress_index: i64, - /// Subaddress used to return transaction "change" to self. - pub change_subaddress_index: i64, - /// The next unused subaddress index. (Assumes indices are used sequentially - /// from 0). - pub next_subaddress_index: i64, - /// account history prior to this block index is derived from the public - /// ledger, and does not reflect client-side - /// user events. - pub import_block_index: i64, - /// Name of this account. - pub name: String, /* empty string for nullable */ -} - -/// A structure that can be inserted to create a new entity in the -/// `view_only_accounts` table. -#[derive(Insertable)] -#[table_name = "view_only_accounts"] -pub struct NewViewOnlyAccount<'a> { - pub account_id_hex: &'a str, - pub view_private_key: &'a [u8], - pub first_block_index: i64, - pub next_block_index: i64, - pub main_subaddress_index: i64, - pub change_subaddress_index: i64, - pub next_subaddress_index: i64, - pub import_block_index: i64, - pub name: &'a str, + pub view_only: bool, } /// A structure that can be inserted to create a new entity in the `accounts` @@ -158,18 +41,16 @@ pub struct NewViewOnlyAccount<'a> { #[derive(Insertable)] #[table_name = "accounts"] pub struct NewAccount<'a> { - pub account_id_hex: &'a str, + pub id: &'a str, pub account_key: &'a [u8], - pub entropy: &'a [u8], + pub entropy: Option<&'a [u8]>, pub key_derivation_version: i32, - pub main_subaddress_index: i64, - pub change_subaddress_index: i64, - pub next_subaddress_index: i64, pub first_block_index: i64, pub next_block_index: i64, pub import_block_index: Option, pub name: &'a str, pub fog_enabled: bool, + pub view_only: bool, } /// A transaction output entity that either was received to an Account in this @@ -180,10 +61,9 @@ pub struct NewAccount<'a> { #[derive(Clone, Serialize, Identifiable, Queryable, PartialEq, Debug)] #[primary_key(id)] pub struct Txo { - /// Primary key - pub id: i32, - /// An additional ID derived from the contents of the ledger TxOut. - pub txo_id_hex: String, + /// Primary key derived from the contents of the ledger TxOut + pub id: String, + pub account_id: Option, /// The value of this transaction output, in picoMob. pub value: i64, /// The token of this transaction output. @@ -202,20 +82,23 @@ pub struct Txo { pub key_image: Option>, /// Block index containing this Txo. pub received_block_index: Option, - pub pending_tombstone_block_index: Option, pub spent_block_index: Option, - pub confirmation: Option>, - /// The recipient public address. Blank for unknown. - pub recipient_public_address_b58: String, - pub minted_account_id_hex: Option, - pub received_account_id_hex: Option, + pub shared_secret: Option>, +} + +impl Txo { + pub fn public_key(&self) -> Result { + let public_key: CompressedRistrettoPublic = mc_util_serial::decode(&self.public_key)?; + Ok(public_key) + } } /// A structure that can be inserted to create a new entity in the `txos` table. #[derive(Insertable)] #[table_name = "txos"] pub struct NewTxo<'a> { - pub txo_id_hex: &'a str, + pub id: &'a str, + pub account_id: Option, pub value: i64, pub token_id: i64, pub target_key: &'a [u8], @@ -225,192 +108,105 @@ pub struct NewTxo<'a> { pub subaddress_index: Option, pub key_image: Option<&'a [u8]>, pub received_block_index: Option, - pub pending_tombstone_block_index: Option, - pub spent_block_index: Option, - pub confirmation: Option<&'a [u8]>, - pub recipient_public_address_b58: String, - pub minted_account_id_hex: Option, - pub received_account_id_hex: Option, -} - -/// TXOs that can be decrypted with the view-private-key for a -/// view-only-account. -#[derive(Clone, Serialize, Identifiable, Queryable, PartialEq, Debug, Associations)] -#[belongs_to(ViewOnlyAccount, foreign_key = "view_only_account_id_hex")] -#[primary_key(id)] -pub struct ViewOnlyTxo { - /// Primary key - pub id: i32, - /// id derrived from txo contents - will be the same for a given txo across - /// databases - pub txo_id_hex: String, - /// The serialized TxOut. - pub txo: Vec, - /// Pre-computed key image for this Txo - pub key_image: Option>, - /// the subaddress index this txo belongs to - pub subaddress_index: Option, - /// The value of this transaction output, in picoMob. - pub value: i64, - /// The token of this transaction output. - pub token_id: i64, - /// The serialized public_key of the TxOut. - pub public_key: Vec, - /// account_id_hex of the view_only_account that received this txo - pub view_only_account_id_hex: String, - /// When this txo was submitted to consensus in a transaction - pub submitted_block_index: Option, - /// What tombstone block index this txo must be accepted by before - /// becoming invalid - pub pending_tombstone_block_index: Option, - /// What index this txo was received on the ledger - pub received_block_index: Option, - /// Which block this txo was spent at - pub spent_block_index: Option, -} - -/// A structure that can be inserted to create a new entity in the -/// `view_only_txos` table. -#[derive(Insertable)] -#[table_name = "view_only_txos"] -pub struct NewViewOnlyTxo<'a> { - pub txo: &'a [u8], - pub txo_id_hex: &'a str, - pub key_image: Option<&'a [u8]>, - pub subaddress_index: Option, - pub value: i64, - pub token_id: i64, - pub public_key: &'a [u8], - pub view_only_account_id_hex: &'a str, - pub submitted_block_index: Option, - pub pending_tombstone_block_index: Option, - pub received_block_index: Option, pub spent_block_index: Option, -} - -/// TXOs that can be decrypted with the view-private-key for a -/// view-only-account. -#[derive(Clone, Serialize, Identifiable, Queryable, PartialEq, Debug, Associations)] -#[belongs_to(ViewOnlyAccount, foreign_key = "view_only_account_id_hex")] -#[primary_key(id)] -#[table_name = "view_only_subaddresses"] -pub struct ViewOnlySubaddress { - /// Primary key - pub id: i32, - /// The pub address b58 string - pub public_address_b58: String, - /// The serialized TxOut. - pub subaddress_index: i64, - /// account_id_hex of the view_only_account that received this txo - pub view_only_account_id_hex: String, - /// comment - pub comment: String, - /// public spend key - pub public_spend_key: Vec, -} - -/// A structure that can be inserted to create a new entity in the -/// `view_only_subaddresses` table. -#[derive(Insertable)] -#[table_name = "view_only_subaddresses"] -pub struct NewViewOnlySubaddress<'a> { - pub public_address_b58: &'a str, - pub view_only_account_id_hex: &'a str, - pub subaddress_index: i64, - pub comment: &'a str, - pub public_spend_key: &'a [u8], + pub shared_secret: Option<&'a [u8]>, } /// A subaddress given to a particular contact, for the purpose of tracking /// funds received from that contact. #[derive(Clone, Serialize, Associations, Identifiable, Queryable, PartialEq, Debug)] -#[belongs_to(Account, foreign_key = "account_id_hex")] -#[primary_key(id)] +#[belongs_to(Account, foreign_key = "account_id")] +#[primary_key(public_address_b58)] #[table_name = "assigned_subaddresses"] pub struct AssignedSubaddress { - pub id: i32, - pub assigned_subaddress_b58: String, - pub account_id_hex: String, - pub address_book_entry: Option, - pub public_address: Vec, + pub public_address_b58: String, + pub account_id: String, pub subaddress_index: i64, - pub comment: String, // empty string for nullable - pub subaddress_spend_key: Vec, // FIXME: WS-28 - Index on subaddress_spend_key? + pub comment: String, + pub spend_public_key: Vec, } /// A structure that can be inserted to create a new AssignedSubaddress entity. #[derive(Insertable)] #[table_name = "assigned_subaddresses"] pub struct NewAssignedSubaddress<'a> { - pub assigned_subaddress_b58: &'a str, - pub account_id_hex: &'a str, - pub address_book_entry: Option, - pub public_address: &'a [u8], + pub public_address_b58: &'a str, + pub account_id: &'a str, pub subaddress_index: i64, pub comment: &'a str, - pub subaddress_spend_key: &'a [u8], + pub spend_public_key: &'a [u8], } /// The status of a sent transaction OR a received transaction output. #[derive(Clone, Serialize, Associations, Identifiable, Queryable, PartialEq, Debug)] -#[belongs_to(Account, foreign_key = "account_id_hex")] -#[belongs_to(AssignedSubaddress, foreign_key = "assigned_subaddress_b58")] +#[belongs_to(Account, foreign_key = "account_id")] #[primary_key(id)] #[table_name = "transaction_logs"] pub struct TransactionLog { - pub id: i32, - pub transaction_id_hex: String, - pub account_id_hex: String, - pub assigned_subaddress_b58: Option, - pub value: i64, - pub fee: Option, - // Statuses: built, pending, succeeded, failed - pub status: String, - pub sent_time: Option, + pub id: String, + pub account_id: String, + pub fee_value: i64, + pub fee_token_id: i64, pub submitted_block_index: Option, + pub tombstone_block_index: Option, pub finalized_block_index: Option, - pub comment: String, // empty string for nullable - // Directions: sent, received - pub direction: String, - pub tx: Option>, + pub comment: String, + pub tx: Vec, + pub failed: bool, } /// A structure that can be inserted to create a new TransactionLog entity. #[derive(Insertable)] #[table_name = "transaction_logs"] pub struct NewTransactionLog<'a> { - pub transaction_id_hex: &'a str, - pub account_id_hex: &'a str, - pub assigned_subaddress_b58: Option<&'a str>, - pub value: i64, - pub fee: Option, - pub status: &'a str, - pub sent_time: Option, + pub id: &'a str, + pub account_id: &'a str, + pub fee_value: i64, + pub fee_token_id: i64, pub submitted_block_index: Option, + pub tombstone_block_index: Option, pub finalized_block_index: Option, pub comment: &'a str, - pub direction: &'a str, - pub tx: Option<&'a [u8]>, + pub tx: &'a [u8], + pub failed: bool, } #[derive(Clone, Serialize, Associations, Identifiable, Queryable, PartialEq, Debug)] -#[belongs_to(TransactionLog, foreign_key = "transaction_id_hex")] -#[belongs_to(Txo, foreign_key = "txo_id_hex")] -#[table_name = "transaction_txo_types"] -#[primary_key(transaction_id_hex, txo_id_hex)] -pub struct TransactionTxoType { - pub transaction_id_hex: String, - pub txo_id_hex: String, - // Statuses: input, output, change - pub transaction_txo_type: String, +#[belongs_to(TransactionLog, foreign_key = "transaction_log_id")] +#[belongs_to(Txo, foreign_key = "txo_id")] +#[table_name = "transaction_input_txos"] +#[primary_key(transaction_log_id, txo_id)] +pub struct TransactionInputTxo { + pub transaction_log_id: String, + pub txo_id: String, } #[derive(Insertable)] -#[table_name = "transaction_txo_types"] -pub struct NewTransactionTxoType<'a> { - pub transaction_id_hex: &'a str, - pub txo_id_hex: &'a str, - pub transaction_txo_type: &'a str, +#[table_name = "transaction_input_txos"] +pub struct NewTransactionInputTxo<'a> { + pub transaction_log_id: &'a str, + pub txo_id: &'a str, +} + +#[derive(Clone, Serialize, Associations, Identifiable, Queryable, PartialEq, Debug)] +#[belongs_to(TransactionLog, foreign_key = "transaction_log_id")] +#[belongs_to(Txo, foreign_key = "txo_id")] +#[table_name = "transaction_output_txos"] +#[primary_key(transaction_log_id, txo_id)] +pub struct TransactionOutputTxo { + pub transaction_log_id: String, + pub txo_id: String, + pub recipient_public_address_b58: String, + pub is_change: bool, +} + +#[derive(Insertable)] +#[table_name = "transaction_output_txos"] +pub struct NewTransactionOutputTxo<'a> { + pub transaction_log_id: &'a str, + pub txo_id: &'a str, + pub recipient_public_address_b58: &'a str, + pub is_change: bool, } #[derive(Clone, Serialize, Associations, Identifiable, Queryable, PartialEq, Debug)] @@ -430,3 +226,24 @@ pub struct NewGiftCode<'a> { pub gift_code_b58: &'a str, pub value: i64, } + +#[derive(Queryable, Insertable)] +#[table_name = "__diesel_schema_migrations"] +pub struct Migration { + pub version: String, + pub run_on: chrono::NaiveDateTime, +} + +#[derive(Insertable)] +#[table_name = "__diesel_schema_migrations"] +pub struct NewMigration { + pub version: String, +} + +impl NewMigration { + pub fn new(version: &str) -> Self { + Self { + version: version.to_string(), + } + } +} diff --git a/full-service/src/db/schema.rs b/full-service/src/db/schema.rs index 10cace6ae..accc5b36c 100644 --- a/full-service/src/db/schema.rs +++ b/full-service/src/db/schema.rs @@ -1,116 +1,71 @@ table! { accounts (id) { - id -> Integer, - account_id_hex -> Text, + id -> Text, account_key -> Binary, - entropy -> Binary, + entropy -> Nullable, key_derivation_version -> Integer, - main_subaddress_index -> BigInt, - change_subaddress_index -> BigInt, - next_subaddress_index -> BigInt, first_block_index -> BigInt, next_block_index -> BigInt, import_block_index -> Nullable, name -> Text, fog_enabled -> Bool, + view_only -> Bool, } } table! { - view_only_accounts (id) { - id -> Integer, - account_id_hex -> Text, - view_private_key -> Binary, - first_block_index -> BigInt, - next_block_index -> BigInt, - main_subaddress_index -> BigInt, - change_subaddress_index -> BigInt, - next_subaddress_index -> BigInt, - import_block_index -> BigInt, - name -> Text, - } -} - -table! { - view_only_txos (id) { - id -> Integer, - txo_id_hex -> Text, - txo -> Binary, - key_image -> Nullable, - subaddress_index -> Nullable, - value -> BigInt, - token_id -> BigInt, - public_key -> Binary, - view_only_account_id_hex -> Text, - submitted_block_index -> Nullable, - pending_tombstone_block_index -> Nullable, - received_block_index -> Nullable, - spent_block_index -> Nullable, - } -} - -table! { - view_only_subaddresses (id) { - id -> Integer, + assigned_subaddresses (public_address_b58) { public_address_b58 -> Text, + account_id -> Text, subaddress_index -> BigInt, - view_only_account_id_hex -> Text, comment -> Text, - public_spend_key -> Binary, + spend_public_key -> Binary, } } table! { - assigned_subaddresses (id) { + gift_codes (id) { id -> Integer, - assigned_subaddress_b58 -> Text, - account_id_hex -> Text, - address_book_entry -> Nullable, - public_address -> Binary, - subaddress_index -> BigInt, - comment -> Text, - subaddress_spend_key -> Binary, + gift_code_b58 -> Text, + value -> BigInt, } } table! { - gift_codes (id) { - id -> Integer, - gift_code_b58 -> Text, - value -> BigInt, + transaction_input_txos (transaction_log_id, txo_id) { + transaction_log_id -> Text, + txo_id -> Text, } } table! { transaction_logs (id) { - id -> Integer, - transaction_id_hex -> Text, - account_id_hex -> Text, - assigned_subaddress_b58 -> Nullable, - value -> BigInt, - fee -> Nullable, - status -> Text, - sent_time -> Nullable, + id -> Text, + account_id -> Text, + fee_value -> BigInt, + fee_token_id -> BigInt, submitted_block_index -> Nullable, + tombstone_block_index -> Nullable, finalized_block_index -> Nullable, comment -> Text, - direction -> Text, - tx -> Nullable, + tx -> Binary, + failed -> Bool, } } table! { - transaction_txo_types (transaction_id_hex, txo_id_hex) { - transaction_id_hex -> Text, - txo_id_hex -> Text, - transaction_txo_type -> Text, + transaction_output_txos (transaction_log_id, txo_id) { + transaction_log_id -> Text, + txo_id -> Text, + recipient_public_address_b58 -> Text, + is_change -> Bool, } } table! { txos (id) { - id -> Integer, - txo_id_hex -> Text, + id -> Text, + account_id -> Nullable, value -> BigInt, token_id -> BigInt, target_key -> Binary, @@ -120,22 +75,32 @@ table! { subaddress_index -> Nullable, key_image -> Nullable, received_block_index -> Nullable, - pending_tombstone_block_index -> Nullable, spent_block_index -> Nullable, - confirmation -> Nullable, - recipient_public_address_b58 -> Text, - minted_account_id_hex -> Nullable, - received_account_id_hex -> Nullable, + shared_secret -> Nullable, + } +} + +table! { + __diesel_schema_migrations(version) { + version -> Text, + run_on -> Timestamp, } } -allow_tables_to_appear_in_same_query!(view_only_accounts, view_only_txos,); +joinable!(assigned_subaddresses -> accounts (account_id)); +joinable!(transaction_input_txos -> transaction_logs (transaction_log_id)); +joinable!(transaction_input_txos -> txos (txo_id)); +joinable!(transaction_logs -> accounts (account_id)); +joinable!(transaction_output_txos -> transaction_logs (transaction_log_id)); +joinable!(transaction_output_txos -> txos (txo_id)); +joinable!(txos -> accounts (account_id)); allow_tables_to_appear_in_same_query!( accounts, assigned_subaddresses, gift_codes, + transaction_input_txos, transaction_logs, - transaction_txo_types, + transaction_output_txos, txos, ); diff --git a/full-service/src/db/transaction_log.rs b/full-service/src/db/transaction_log.rs index 5e05747dc..6e7cabc7c 100644 --- a/full-service/src/db/transaction_log.rs +++ b/full-service/src/db/transaction_log.rs @@ -2,28 +2,39 @@ //! DB impl for the Transaction model. -use chrono::Utc; use diesel::prelude::*; use mc_common::HashMap; use mc_crypto_digestible::{Digestible, MerlinTranscript}; -use mc_mobilecoind::payments::TxProposal; -use mc_transaction_core::{tx::Tx, Amount}; +use mc_transaction_core::{tx::Tx, Amount, TokenId}; use std::fmt; -use crate::db::{ - account::{AccountID, AccountModel}, - models::{ - Account, NewTransactionLog, NewTransactionTxoType, TransactionLog, TransactionTxoType, Txo, - TXO_USED_AS_CHANGE, TXO_USED_AS_INPUT, TXO_USED_AS_OUTPUT, TX_DIRECTION_RECEIVED, - TX_DIRECTION_SENT, TX_STATUS_BUILT, TX_STATUS_FAILED, TX_STATUS_PENDING, - TX_STATUS_SUCCEEDED, +use crate::{ + db::{ + account::{AccountID, AccountModel}, + models::{ + Account, NewTransactionInputTxo, NewTransactionLog, TransactionInputTxo, + TransactionLog, TransactionOutputTxo, Txo, + }, + txo::{TxoID, TxoModel}, + Conn, WalletDbError, }, - txo::{TxoID, TxoModel}, - Conn, WalletDbError, + service::models::tx_proposal::TxProposal, }; #[derive(Debug)] -pub struct TransactionID(String); +pub struct TransactionID(pub String); + +impl From<&TransactionLog> for TransactionID { + fn from(tx_log: &TransactionLog) -> Self { + Self(tx_log.id.clone()) + } +} + +impl From<&TxProposal> for TransactionID { + fn from(_tx_proposal: &TxProposal) -> Self { + Self::from(&_tx_proposal.tx) + } +} // TransactionID is formed from the contents of the transaction when sent impl From<&Tx> for TransactionID { @@ -33,30 +44,79 @@ impl From<&Tx> for TransactionID { } } -// TransactionID is formed from the received TxoID when received -impl From for TransactionID { - fn from(src: String) -> TransactionID { - Self(src) - } -} - impl fmt::Display for TransactionID { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "{}", self.0) } } +#[derive(Debug, PartialEq)] +pub enum TxStatus { + // The transaction log has been built but not yet submitted to consensus + Built, + // The transaction log has been submitted to consensus + Pending, + // The txos associated with this transaction log have appeared on the ledger, indicating that + // the transaction was successful + Succeeded, + // Either consensus has rejected the tx proposal, or the tombstone block index has passed + // without the txos in this transaction showing on the ledger + Failed, +} + +impl fmt::Display for TxStatus { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + TxStatus::Built => write!(f, "built"), + TxStatus::Pending => write!(f, "pending"), + TxStatus::Succeeded => write!(f, "succeeded"), + TxStatus::Failed => write!(f, "failed"), + } + } +} + +#[derive(Debug, PartialEq)] +pub enum TxoType { + // used as an input in a transaction + Input, + // used as an output in a transaction that is not change + Payload, + // used as an output in a transaction that is change + Change, +} + +impl fmt::Display for TxoType { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + TxoType::Input => write!(f, "input"), + TxoType::Payload => write!(f, "payload"), + TxoType::Change => write!(f, "change"), + } + } +} + +#[derive(Debug)] +pub struct ValueMap(pub HashMap); + #[derive(Debug)] pub struct AssociatedTxos { pub inputs: Vec, - pub outputs: Vec, - pub change: Vec, + pub outputs: Vec<(Txo, String)>, + pub change: Vec<(Txo, String)>, +} + +impl TransactionLog { + pub fn fee_amount(&self) -> Amount { + Amount::new( + self.fee_value as u64, + TokenId::from(self.fee_token_id as u64), + ) + } } -const MOB_TOKEN_ID: i64 = 0; pub trait TransactionLogModel { /// Get a transaction log from the TransactionId. - fn get(transaction_id_hex: &str, conn: &Conn) -> Result; + fn get(id: &TransactionID, conn: &Conn) -> Result; /// Get all transaction logs for the given block index. fn get_all_for_block_index( @@ -74,31 +134,31 @@ pub trait TransactionLogModel { /// * AssoiatedTxos(inputs, outputs, change) fn get_associated_txos(&self, conn: &Conn) -> Result; - /// Select the TransactionLogs associated with a given TxoId. - fn select_for_txo(txo_id_hex: &str, conn: &Conn) -> Result, WalletDbError>; + fn update_submitted_block_index( + &self, + submitted_block_index: u64, + conn: &Conn, + ) -> Result<(), WalletDbError>; /// List all TransactionLogs and their associated Txos for a given account. /// /// Returns: /// * Vec(TransactionLog, AssociatedTxos(inputs, outputs, change)) fn list_all( - account_id_hex: &str, + account_id: Option, offset: Option, limit: Option, min_block_index: Option, max_block_index: Option, conn: &Conn, - ) -> Result, WalletDbError>; + ) -> Result, WalletDbError>; - /// Log a received transaction. - fn log_received( + fn log_built( + tx_proposal: TxProposal, + comment: String, account_id_hex: &str, - assigned_subaddress_b58: Option<&str>, - txo_id_hex: &str, - amount: Amount, - block_index: u64, conn: &Conn, - ) -> Result<(), WalletDbError>; + ) -> Result; /// Log a submitted transaction. /// @@ -111,7 +171,7 @@ pub trait TransactionLogModel { /// change. Other wallets may choose to behave differently, but /// our TransactionLogs Table assumes this behavior. fn log_submitted( - tx_proposal: TxProposal, + tx_proposal: &TxProposal, block_index: u64, comment: String, account_id_hex: &str, @@ -121,33 +181,49 @@ pub trait TransactionLogModel { /// Remove all logs for an account fn delete_all_for_account(account_id_hex: &str, conn: &Conn) -> Result<(), WalletDbError>; - fn update_tx_logs_associated_with_txo_to_succeeded( + fn update_pending_associated_with_txo_to_succeeded( txo_id_hex: &str, finalized_block_index: u64, conn: &Conn, ) -> Result<(), WalletDbError>; - fn update_tx_logs_associated_with_txos_to_failed( - txos: &[Txo], + fn update_pending_exceeding_tombstone_block_index_to_failed( + block_index: u64, conn: &Conn, ) -> Result<(), WalletDbError>; + + fn status(&self) -> TxStatus; + + fn value_for_token_id(&self, token_id: TokenId, conn: &Conn) -> Result; + + fn value_map(&self, conn: &Conn) -> Result; } impl TransactionLogModel for TransactionLog { - fn get(transaction_id_hex: &str, conn: &Conn) -> Result { - use crate::db::schema::transaction_logs::dsl::{ - transaction_id_hex as dsl_transaction_id_hex, transaction_logs, - }; + fn status(&self) -> TxStatus { + if self.failed { + TxStatus::Failed + } else if self.finalized_block_index.is_some() { + TxStatus::Succeeded + } else if self.submitted_block_index.is_some() { + TxStatus::Pending + } else { + TxStatus::Built + } + } + + fn get(id: &TransactionID, conn: &Conn) -> Result { + use crate::db::schema::transaction_logs::dsl::{id as dsl_id, transaction_logs}; match transaction_logs - .filter(dsl_transaction_id_hex.eq(transaction_id_hex)) + .filter(dsl_id.eq(id.to_string())) .get_result::(conn) { Ok(a) => Ok(a), // Match on NotFound to get a more informative NotFound Error - Err(diesel::result::Error::NotFound) => Err(WalletDbError::TransactionLogNotFound( - transaction_id_hex.to_string(), - )), + Err(diesel::result::Error::NotFound) => { + Err(WalletDbError::TransactionLogNotFound(id.to_string())) + } Err(e) => Err(e.into()), } } @@ -156,236 +232,180 @@ impl TransactionLogModel for TransactionLog { block_index: u64, conn: &Conn, ) -> Result, WalletDbError> { - use crate::db::schema::{transaction_logs, transaction_txo_types, txos}; + use crate::db::schema::transaction_logs::{ + all_columns, dsl::transaction_logs, finalized_block_index, + }; - let matches: Vec = transaction_logs::table - .inner_join(transaction_txo_types::table.on( - transaction_logs::transaction_id_hex.eq(transaction_txo_types::transaction_id_hex), - )) - .inner_join(txos::table.on(transaction_txo_types::txo_id_hex.eq(txos::txo_id_hex))) - .filter(txos::token_id.eq(MOB_TOKEN_ID)) - .filter(transaction_logs::finalized_block_index.eq(block_index as i64)) - .select(transaction_logs::all_columns) + let matches: Vec = transaction_logs + .select(all_columns) + .filter(finalized_block_index.eq(block_index as i64)) .load::(conn)?; Ok(matches) } fn get_all_ordered_by_block_index(conn: &Conn) -> Result, WalletDbError> { - use crate::db::schema::{transaction_logs, transaction_txo_types, txos}; + use crate::db::schema::transaction_logs::{ + all_columns, dsl::transaction_logs, finalized_block_index, + }; - let matches = transaction_logs::table - .inner_join(transaction_txo_types::table.on( - transaction_logs::transaction_id_hex.eq(transaction_txo_types::transaction_id_hex), - )) - .inner_join(txos::table.on(transaction_txo_types::txo_id_hex.eq(txos::txo_id_hex))) - .filter(txos::token_id.eq(MOB_TOKEN_ID)) - .select(transaction_logs::all_columns) - .order_by(transaction_logs::finalized_block_index.asc()) + let matches = transaction_logs + .select(all_columns) + .order_by(finalized_block_index.asc()) .load(conn)?; Ok(matches) } fn get_associated_txos(&self, conn: &Conn) -> Result { - use crate::db::schema::{transaction_txo_types, txos}; - - // FIXME: WS-29 - use group_by rather than the processing below: - // https://docs.diesel.rs/diesel/associations/trait.GroupedBy.html - let transaction_txos: Vec<(TransactionTxoType, Txo)> = transaction_txo_types::table - .inner_join(txos::table.on(transaction_txo_types::txo_id_hex.eq(txos::txo_id_hex))) - .filter(transaction_txo_types::transaction_id_hex.eq(&self.transaction_id_hex)) - .filter(txos::token_id.eq(MOB_TOKEN_ID)) - .select((transaction_txo_types::all_columns, txos::all_columns)) + use crate::db::schema::{transaction_input_txos, transaction_output_txos, txos}; + + let inputs: Vec = txos::table + .inner_join(transaction_input_txos::table) + .filter(transaction_input_txos::transaction_log_id.eq(&self.id)) + .select(txos::all_columns) .load(conn)?; - let mut inputs: Vec = Vec::new(); - let mut outputs: Vec = Vec::new(); - let mut change: Vec = Vec::new(); - - for (transaction_txo_type, txo) in transaction_txos { - match transaction_txo_type.transaction_txo_type.as_str() { - TXO_USED_AS_INPUT => inputs.push(txo), - TXO_USED_AS_OUTPUT => outputs.push(txo), - TXO_USED_AS_CHANGE => change.push(txo), - _ => { - return Err(WalletDbError::UnexpectedTransactionTxoType( - transaction_txo_type.transaction_txo_type, - )); - } - } - } + let payload: Vec<(Txo, String)> = txos::table + .inner_join(transaction_output_txos::table) + .filter(transaction_output_txos::transaction_log_id.eq(&self.id)) + .filter(transaction_output_txos::is_change.eq(false)) + .select(( + txos::all_columns, + transaction_output_txos::recipient_public_address_b58, + )) + .load(conn)?; + + let change: Vec<(Txo, String)> = txos::table + .inner_join(transaction_output_txos::table) + .filter(transaction_output_txos::transaction_log_id.eq(&self.id)) + .filter(transaction_output_txos::is_change.eq(true)) + .select(( + txos::all_columns, + transaction_output_txos::recipient_public_address_b58, + )) + .load(conn)?; Ok(AssociatedTxos { inputs, - outputs, + outputs: payload, change, }) } - fn select_for_txo(txo_id_hex: &str, conn: &Conn) -> Result, WalletDbError> { - use crate::db::schema::{transaction_logs, transaction_txo_types, txos}; + fn update_submitted_block_index( + &self, + submitted_block_index: u64, + conn: &Conn, + ) -> Result<(), WalletDbError> { + use crate::db::schema::transaction_logs; - Ok(transaction_logs::table - .inner_join(transaction_txo_types::table.on( - transaction_logs::transaction_id_hex.eq(transaction_txo_types::transaction_id_hex), - )) - .inner_join(txos::table.on(transaction_txo_types::txo_id_hex.eq(txos::txo_id_hex))) - .filter(transaction_txo_types::txo_id_hex.eq(txo_id_hex)) - .filter(txos::token_id.eq(MOB_TOKEN_ID)) - .select(transaction_logs::all_columns) - .load(conn)?) + diesel::update(self) + .set(transaction_logs::submitted_block_index.eq(Some(submitted_block_index as i64))) + .execute(conn)?; + + Ok(()) } fn list_all( - account_id_hex: &str, + account_id: Option, offset: Option, limit: Option, min_block_index: Option, max_block_index: Option, conn: &Conn, - ) -> Result, WalletDbError> { - use crate::db::schema::{transaction_logs, transaction_txo_types, txos}; - - // Query for all transaction logs for the account, as well as associated txos. - // This is accomplished via a double-join through the - // transaction_txo_types table. - // TODO: investigate simplifying the database structure around this. - let mut transactions_query = transaction_logs::table - .into_boxed() - .filter(transaction_logs::account_id_hex.eq(account_id_hex)) - .inner_join(transaction_txo_types::table.on( - transaction_logs::transaction_id_hex.eq(transaction_txo_types::transaction_id_hex), - )) - .inner_join(txos::table.on(transaction_txo_types::txo_id_hex.eq(txos::txo_id_hex))) - .filter(txos::token_id.eq(MOB_TOKEN_ID)) - .select(( - transaction_logs::all_columns, - transaction_txo_types::all_columns, - txos::all_columns, - )) - .order(transaction_logs::id); + ) -> Result, WalletDbError> { + use crate::db::schema::transaction_logs; + + let mut query = transaction_logs::table.into_boxed(); + + if let Some(account_id) = account_id { + query = query.filter(transaction_logs::account_id.eq(account_id)); + } if let (Some(o), Some(l)) = (offset, limit) { - transactions_query = transactions_query.offset(o as i64).limit(l as i64); + query = query.offset(o as i64).limit(l as i64); } if let Some(min_block_index) = min_block_index { - transactions_query = transactions_query - .filter(transaction_logs::finalized_block_index.ge(min_block_index as i64)); + query = + query.filter(transaction_logs::finalized_block_index.ge(min_block_index as i64)); } if let Some(max_block_index) = max_block_index { - transactions_query = transactions_query - .filter(transaction_logs::finalized_block_index.le(max_block_index as i64)); - } - - let transactions: Vec<(TransactionLog, TransactionTxoType, Txo)> = - transactions_query.load(conn)?; - - #[derive(Clone)] - struct TransactionContents { - transaction_log: TransactionLog, - inputs: Vec, - outputs: Vec, - change: Vec, + query = + query.filter(transaction_logs::finalized_block_index.le(max_block_index as i64)); } - let mut results: HashMap = HashMap::default(); - for (transaction, transaction_txo_type, txo) in transactions { - if results.get(&transaction.transaction_id_hex).is_none() { - results.insert( - transaction.transaction_id_hex.clone(), - TransactionContents { - transaction_log: transaction.clone(), - inputs: Vec::new(), - outputs: Vec::new(), - change: Vec::new(), - }, - ); - }; - - let entry = results.get_mut(&transaction.transaction_id_hex).unwrap(); - if entry.transaction_log != transaction { - return Err(WalletDbError::TransactionMismatch); - } + let transaction_logs: Vec = query.order(transaction_logs::id).load(conn)?; - match transaction_txo_type.transaction_txo_type.as_str() { - TXO_USED_AS_INPUT => entry.inputs.push(txo), - TXO_USED_AS_OUTPUT => entry.outputs.push(txo), - TXO_USED_AS_CHANGE => entry.change.push(txo), - _ => { - return Err(WalletDbError::UnexpectedTransactionTxoType( - transaction_txo_type.transaction_txo_type, - )); - } - } - } - - let mut results: Vec<(TransactionLog, AssociatedTxos)> = results - .values() - .cloned() - .map(|t| { - ( - t.transaction_log, - AssociatedTxos { - inputs: t.inputs, - outputs: t.outputs, - change: t.change, - }, - ) + let results = transaction_logs + .into_iter() + .map(|log| { + let associated_txos = log.get_associated_txos(conn)?; + let value_map = log.value_map(conn)?; + Ok((log, associated_txos, value_map)) }) - .collect(); + .collect::, WalletDbError>>()?; - results.sort_by_key(|r| r.0.id); Ok(results) } - fn log_received( + fn log_built( + tx_proposal: TxProposal, + comment: String, account_id_hex: &str, - assigned_subaddress_b58: Option<&str>, - txo_id_hex: &str, - amount: Amount, - block_index: u64, conn: &Conn, - ) -> Result<(), WalletDbError> { - use crate::db::schema::transaction_txo_types; + ) -> Result { + // Verify that the account exists. + Account::get(&AccountID(account_id_hex.to_string()), conn)?; + + let transaction_log_id = TransactionID::from(&tx_proposal); + let tx = mc_util_serial::encode(&tx_proposal.tx); let new_transaction_log = NewTransactionLog { - transaction_id_hex: txo_id_hex, - account_id_hex, - assigned_subaddress_b58, - value: amount.value as i64, // We store numbers between 2^63 and 2^64 as negative. - fee: None, // Impossible to recover fee from received transaction - status: TX_STATUS_SUCCEEDED, - sent_time: None, // NULL for received + id: &transaction_log_id.to_string(), + account_id: account_id_hex, + fee_value: tx_proposal.tx.prefix.fee as i64, + fee_token_id: tx_proposal.tx.prefix.fee_token_id as i64, submitted_block_index: None, - finalized_block_index: Some(block_index as i64), - comment: "", // NULL for received - direction: TX_DIRECTION_RECEIVED, - tx: None, // NULL for received + tombstone_block_index: Some(tx_proposal.tx.prefix.tombstone_block as i64), + finalized_block_index: None, + comment: &comment, + tx: &tx, + failed: false, }; diesel::insert_into(crate::db::schema::transaction_logs::table) .values(&new_transaction_log) .execute(conn)?; - // Create an entry per TXO for the TransactionTxoTypes - let new_transaction_txo = NewTransactionTxoType { - transaction_id_hex: txo_id_hex, - txo_id_hex, - transaction_txo_type: TXO_USED_AS_OUTPUT, - }; + for txo in tx_proposal.input_txos.iter() { + let txo_id = TxoID::from(&txo.tx_out); + Txo::update_key_image(&txo_id.to_string(), &txo.key_image, None, conn)?; + let transaction_input_txo = NewTransactionInputTxo { + transaction_log_id: &transaction_log_id.to_string(), + txo_id: &txo_id.to_string(), + }; - diesel::insert_into(transaction_txo_types::table) - .values(&new_transaction_txo) - .execute(conn)?; + diesel::insert_into(crate::db::schema::transaction_input_txos::table) + .values(&transaction_input_txo) + .execute(conn)?; + } - Ok(()) + for output_txo in tx_proposal.payload_txos.iter() { + Txo::create_new_output(output_txo, false, &transaction_log_id, conn)?; + } + + for change_txo in tx_proposal.change_txos.iter() { + Txo::create_new_output(change_txo, true, &transaction_log_id, conn)?; + } + + TransactionLog::get(&transaction_log_id, conn) } fn log_submitted( - tx_proposal: TxProposal, + tx_proposal: &TxProposal, block_index: u64, comment: String, account_id_hex: &str, @@ -394,183 +414,181 @@ impl TransactionLogModel for TransactionLog { // Verify that the account exists. Account::get(&AccountID(account_id_hex.to_string()), conn)?; - // Store the txo_id_hex -> transaction_txo_type - let mut txo_ids: Vec<(String, String)> = Vec::new(); - - // Verify that the TxProposal is well-formed according to our assumptions about - // how to store the sent data in our wallet (num_output_TXOs = num_outlays + - // change_TXO). - if tx_proposal.tx.prefix.outputs.len() - tx_proposal.outlays.len() > 1 { - return Err(WalletDbError::UnexpectedNumberOfChangeOutputs); - } - - // First update all inputs to "pending." They will remain pending until their - // key_image hits the ledger or their tombstone block is exceeded. - for utxo in tx_proposal.utxos.iter() { - let txo_id = TxoID::from(&utxo.tx_out); - let txo = Txo::get(&txo_id.to_string(), conn)?; - txo.update_to_pending(tx_proposal.tx.prefix.tombstone_block, conn)?; - txo_ids.push((txo_id.to_string(), TXO_USED_AS_INPUT.to_string())); - } + let transaction_log_id = TransactionID::from(&tx_proposal.tx); + let tx = mc_util_serial::encode(&tx_proposal.tx); - // Next, add all of our minted outputs to the Txo Table - for (i, output) in tx_proposal.tx.prefix.outputs.iter().enumerate() { - let processed_output = - Txo::create_minted(account_id_hex, output, &tx_proposal, i, conn)?; - txo_ids.push(( - processed_output.txo_id_hex, - processed_output.txo_type.to_string(), - )); - } + match TransactionLog::get(&transaction_log_id, conn) { + Ok(transaction_log) => { + transaction_log.update_submitted_block_index(block_index, conn)?; + } - // Enforce maximum value. - let transaction_value = tx_proposal - .outlays - .iter() - .map(|o| o.value as u128) - .sum::(); - if transaction_value > u64::MAX as u128 { - return Err(WalletDbError::TransactionValueExceedsMax); - } + Err(WalletDbError::TransactionLogNotFound(_)) => { + let new_transaction_log = NewTransactionLog { + id: &transaction_log_id.to_string(), + account_id: account_id_hex, + fee_value: tx_proposal.tx.prefix.fee as i64, + fee_token_id: tx_proposal.tx.prefix.fee_token_id as i64, + submitted_block_index: Some(block_index as i64), + tombstone_block_index: Some(tx_proposal.tx.prefix.tombstone_block as i64), + finalized_block_index: None, + comment: &comment, + tx: &tx, + failed: false, + }; + + diesel::insert_into(crate::db::schema::transaction_logs::table) + .values(&new_transaction_log) + .execute(conn)?; + + for input_txo in tx_proposal.input_txos.iter() { + let txo_id = TxoID::from(&input_txo.tx_out); + Txo::update_key_image(&txo_id.to_string(), &input_txo.key_image, None, conn)?; + let transaction_input_txo = NewTransactionInputTxo { + transaction_log_id: &transaction_log_id.to_string(), + txo_id: &txo_id.to_string(), + }; + + diesel::insert_into(crate::db::schema::transaction_input_txos::table) + .values(&transaction_input_txo) + .execute(conn)?; + } - let transaction_id = TransactionID::from(&tx_proposal.tx); - let tx = mc_util_serial::encode(&tx_proposal.tx); + for output_txo in tx_proposal.payload_txos.iter() { + Txo::create_new_output(output_txo, false, &transaction_log_id, conn)?; + } - // Create a TransactionLogs entry - let new_transaction_log = NewTransactionLog { - transaction_id_hex: &transaction_id.to_string(), - account_id_hex, // Can be null if submitting an "unowned" proposal. - assigned_subaddress_b58: None, // NULL for sent - value: transaction_value as i64, - fee: Some(tx_proposal.tx.prefix.fee as i64), - status: TX_STATUS_PENDING, - sent_time: Some(Utc::now().timestamp()), - submitted_block_index: Some(block_index as i64), - finalized_block_index: None, - comment: &comment, - direction: TX_DIRECTION_SENT, - tx: Some(&tx), - }; - diesel::insert_into(crate::db::schema::transaction_logs::table) - .values(&new_transaction_log) - .execute(conn)?; + for change_txo in tx_proposal.change_txos.iter() { + Txo::create_new_output(change_txo, true, &transaction_log_id, conn)?; + } + } - // Create an entry per TXO for the TransactionTxoTypes - for (txo_id_hex, transaction_txo_type) in txo_ids { - let new_transaction_txo = NewTransactionTxoType { - transaction_id_hex: &transaction_id.to_string(), - txo_id_hex: &txo_id_hex, - transaction_txo_type: &transaction_txo_type, - }; - diesel::insert_into(crate::db::schema::transaction_txo_types::table) - .values(&new_transaction_txo) - .execute(conn)?; + Err(e) => { + return Err(e); + } } - TransactionLog::get(&transaction_id.to_string(), conn) + + TransactionLog::get(&transaction_log_id, conn) } fn delete_all_for_account(account_id_hex: &str, conn: &Conn) -> Result<(), WalletDbError> { use crate::db::schema::{ - transaction_logs as cols, transaction_logs::dsl::transaction_logs, - transaction_txo_types as types_cols, transaction_txo_types::dsl::transaction_txo_types, + transaction_input_txos, transaction_logs, transaction_output_txos, }; - let results: Vec = transaction_logs - .filter(cols::account_id_hex.eq(account_id_hex)) - .select(cols::transaction_id_hex) + let transaction_input_txos: Vec = transaction_input_txos::table + .inner_join(transaction_logs::table) + .filter(transaction_logs::account_id.eq(account_id_hex)) + .select(transaction_input_txos::all_columns) .load(conn)?; - for transaction_id_hex in results.iter() { - diesel::delete( - transaction_txo_types.filter(types_cols::transaction_id_hex.eq(transaction_id_hex)), - ) - .execute(conn)?; + for transaction_input_txo in transaction_input_txos { + diesel::delete(&transaction_input_txo).execute(conn)?; } - diesel::delete(transaction_logs.filter(cols::account_id_hex.eq(account_id_hex))) - .execute(conn)?; + let transaction_output_txos: Vec = transaction_output_txos::table + .inner_join(transaction_logs::table) + .filter(transaction_logs::account_id.eq(account_id_hex)) + .select(transaction_output_txos::all_columns) + .load(conn)?; + + for transaction_output_txo in transaction_output_txos { + diesel::delete(&transaction_output_txo).execute(conn)?; + } + + diesel::delete( + transaction_logs::table.filter(transaction_logs::account_id.eq(account_id_hex)), + ) + .execute(conn)?; Ok(()) } - fn update_tx_logs_associated_with_txo_to_succeeded( + fn update_pending_associated_with_txo_to_succeeded( txo_id_hex: &str, finalized_block_index: u64, conn: &Conn, ) -> Result<(), WalletDbError> { - use crate::db::schema::{transaction_logs, transaction_txo_types}; - - // Find all transaction_logs that are BUILT or PENDING that are associated - // with the txo id when it is used as an input. - // Update the status to SUCCEEDED and update the finalized_block_index. + use crate::db::schema::{transaction_input_txos, transaction_logs}; + // Find all transaction logs associated with this txo that have not + // yet been finalized (there should only ever be one). + // TODO - WHY WON'T THIS WORK?!?!? let transaction_log_ids: Vec = transaction_logs::table - .inner_join(transaction_txo_types::table.on( - transaction_logs::transaction_id_hex.eq(transaction_txo_types::transaction_id_hex), - )) - .filter(transaction_txo_types::txo_id_hex.eq(txo_id_hex)) - .filter(transaction_logs::status.eq_any(vec![TX_STATUS_BUILT, TX_STATUS_PENDING])) - .select(transaction_logs::transaction_id_hex) + .inner_join(transaction_input_txos::table) + .filter(transaction_input_txos::txo_id.eq(txo_id_hex)) + .filter(transaction_logs::failed.eq(false)) + .filter(transaction_logs::finalized_block_index.is_null()) + .select(transaction_logs::id) .load(conn)?; diesel::update( - transaction_logs::table - .filter(transaction_logs::transaction_id_hex.eq_any(transaction_log_ids)), + transaction_logs::table.filter(transaction_logs::id.eq_any(transaction_log_ids)), ) - .set(( - transaction_logs::status.eq(TX_STATUS_SUCCEEDED), - transaction_logs::finalized_block_index.eq(finalized_block_index as i64), - )) + .set((transaction_logs::finalized_block_index.eq(finalized_block_index as i64),)) .execute(conn)?; Ok(()) } - fn update_tx_logs_associated_with_txos_to_failed( - txos: &[Txo], + fn update_pending_exceeding_tombstone_block_index_to_failed( + block_index: u64, conn: &Conn, ) -> Result<(), WalletDbError> { - use crate::db::schema::{transaction_logs, transaction_txo_types}; - - let txo_ids: Vec = txos.iter().map(|txo| txo.txo_id_hex.clone()).collect(); - - // Find all transaction_logs that are BUILT or PENDING that are associated - // with the txo id when it is used as an input. - // Update the status to FAILED - let transaction_log_ids: Vec = transaction_logs::table - .inner_join(transaction_txo_types::table.on( - transaction_logs::transaction_id_hex.eq(transaction_txo_types::transaction_id_hex), - )) - .filter(transaction_txo_types::txo_id_hex.eq_any(txo_ids)) - .filter(transaction_logs::status.eq_any(vec![TX_STATUS_BUILT, TX_STATUS_PENDING])) - .select(transaction_logs::transaction_id_hex) - .load(conn)?; + use crate::db::schema::transaction_logs; diesel::update( transaction_logs::table - .filter(transaction_logs::transaction_id_hex.eq_any(transaction_log_ids)), + .filter(transaction_logs::tombstone_block_index.lt(block_index as i64)) + .filter(transaction_logs::failed.eq(false)) + .filter(transaction_logs::finalized_block_index.is_null()), ) - .set((transaction_logs::status.eq(TX_STATUS_FAILED),)) + .set((transaction_logs::failed.eq(true),)) .execute(conn)?; Ok(()) } + + fn value_for_token_id(&self, token_id: TokenId, conn: &Conn) -> Result { + let associated_txos = self.get_associated_txos(conn)?; + + let output_total = associated_txos + .outputs + .iter() + .filter(|(txo, _)| txo.token_id as u64 == *token_id) + .map(|(txo, _)| txo.value as u64) + .sum::(); + + Ok(output_total) + } + + fn value_map(&self, conn: &Conn) -> Result { + let associated_txos = self.get_associated_txos(conn)?; + + let mut value_map: HashMap = HashMap::default(); + for (txo, _) in associated_txos.outputs.iter() { + let token_id = TokenId::from(txo.token_id as u64); + let value = value_map.entry(token_id).or_insert(0); + *value += txo.value as u64; + } + Ok(ValueMap(value_map)) + } } #[cfg(test)] mod tests { - use mc_account_keys::{AccountKey, PublicAddress, RootIdentity, CHANGE_SUBADDRESS_INDEX}; + use mc_account_keys::{PublicAddress, CHANGE_SUBADDRESS_INDEX}; use mc_common::logger::{test_with_logger, Logger}; - use mc_crypto_rand::RngCore; use mc_ledger_db::Ledger; - use mc_transaction_core::{ring_signature::KeyImage, tokens::Mob, Amount, Token, TokenId}; - use mc_util_from_random::FromRandom; + use mc_transaction_core::{tokens::Mob, Token}; use rand::{rngs::StdRng, SeedableRng}; use crate::{ - db::account::{AccountID, AccountModel}, - service::{sync::SyncThread, transaction_builder::WalletTransactionBuilder}, + db::{account::AccountID, transaction_log::TransactionID, txo::TxoStatus}, + service::{ + sync::SyncThread, transaction::TransactionMemo, + transaction_builder::WalletTransactionBuilder, + }, test_utils::{ - add_block_with_tx_outs, builder_for_random_recipient, create_test_received_txo, + add_block_from_transaction_log, add_block_with_tx_outs, builder_for_random_recipient, get_resolver_factory, get_test_ledger, manually_sync_account, random_account_with_seed_values, WalletDbTestContext, MOB, }, @@ -579,84 +597,6 @@ mod tests { use super::*; - #[test_with_logger] - fn test_log_received(logger: Logger) { - let mut rng: StdRng = SeedableRng::from_seed([20u8; 32]); - - let db_test_context = WalletDbTestContext::default(); - let wallet_db = db_test_context.get_db_instance(logger); - let ledger_db = get_test_ledger(5, &[], 12, &mut rng); - - let root_id = RootIdentity::from_random(&mut rng); - let account_key = AccountKey::from(&root_id); - let (account_id, _address) = Account::create_from_root_entropy( - &root_id.root_entropy, - Some(0), - None, - None, - "", - "".to_string(), - "".to_string(), - "".to_string(), - &ledger_db, - &wallet_db.get_conn().unwrap(), - ) - .unwrap(); - - // Populate our DB with some received txos in the same block - let mut synced: HashMap> = HashMap::default(); - let subaddress = account_key.subaddress(0); - let assigned_subaddress_b58 = Some(b58_encode_public_address(&subaddress).unwrap()); - - for i in 1..20 { - let (txo_id_hex, _txo, _key_image) = create_test_received_txo( - &account_key, - 0, // All to the same subaddress - Amount::new((100 * i * MOB) as u64, Mob::ID), - 144, - &mut rng, - &wallet_db, - ); - if synced.is_empty() { - synced.insert(0, Vec::new()); - } - synced.get_mut(&0).unwrap().push(txo_id_hex.clone()); - - TransactionLog::log_received( - &account_id.to_string(), - assigned_subaddress_b58.as_ref().map(|s| s.as_str()), - &txo_id_hex, - Amount::new(100 * i * MOB, Mob::ID), - 144, - &wallet_db.get_conn().unwrap(), - ) - .unwrap(); - } - - for (_subaddress, txos) in synced.iter() { - for txo_id_hex in txos { - let transaction_logs = - TransactionLog::select_for_txo(txo_id_hex, &wallet_db.get_conn().unwrap()) - .unwrap(); - // There should be one TransactionLog per received txo - assert_eq!(transaction_logs.len(), 1); - - assert_eq!(&transaction_logs[0].transaction_id_hex, txo_id_hex); - - let txo_details = Txo::get(txo_id_hex, &wallet_db.get_conn().unwrap()).unwrap(); - assert_eq!(transaction_logs[0].value, txo_details.value); - - // Make the sure the types are correct - all received should be TXO_OUTPUT - let associated = transaction_logs[0] - .get_associated_txos(&wallet_db.get_conn().unwrap()) - .unwrap(); - assert_eq!(associated.inputs.len(), 0); - assert_eq!(associated.outputs.len(), 1); - assert_eq!(associated.change.len(), 0); - } - } - } - #[test_with_logger] // Test the happy path for log_submitted. When a transaction is submitted to the // MobileCoin network, several things must happen for Full-Service to @@ -689,15 +629,18 @@ mod tests { // Build a transaction let conn = wallet_db.get_conn().unwrap(); let (recipient, mut builder) = - builder_for_random_recipient(&account_key, &ledger_db, &mut rng, &logger); - builder.add_recipient(recipient.clone(), 50 * MOB).unwrap(); + builder_for_random_recipient(&account_key, &ledger_db, &mut rng); + builder + .add_recipient(recipient.clone(), 50 * MOB, Mob::ID) + .unwrap(); builder.set_tombstone(0).unwrap(); - builder.select_txos(&conn, None, false).unwrap(); - let tx_proposal = builder.build(&conn).unwrap(); + builder.select_txos(&conn, None).unwrap(); + let unsigned_tx_proposal = builder.build(TransactionMemo::RTH, &conn).unwrap(); + let tx_proposal = unsigned_tx_proposal.sign(&account_key).unwrap(); // Log submitted transaction from tx_proposal let tx_log = TransactionLog::log_submitted( - tx_proposal.clone(), + &tx_proposal, ledger_db.num_blocks().unwrap(), "".to_string(), &AccountID::from(&account_key).to_string(), @@ -706,30 +649,20 @@ mod tests { .unwrap(); // The log's account ID matches the account_id which submitted the tx - assert_eq!( - tx_log.account_id_hex, - AccountID::from(&account_key).to_string() - ); - // No assigned subaddress for sent - assert_eq!(tx_log.assigned_subaddress_b58, None); - // Value is the amount sent, not including fee and change - assert_eq!(tx_log.value as u64, 50 * MOB); - // Fee exists for submitted - assert_eq!(tx_log.fee.unwrap() as u64, Mob::MINIMUM_FEE); - // Created and sent transaction is "pending" until it lands - assert_eq!(tx_log.status, TX_STATUS_PENDING); - assert!(tx_log.sent_time.unwrap() > 0); + assert_eq!(tx_log.account_id, AccountID::from(&account_key).to_string()); + assert_eq!(tx_log.value_for_token_id(Mob::ID, &conn).unwrap(), 50 * MOB); + assert_eq!(tx_log.fee_value as u64, Mob::MINIMUM_FEE); + assert_eq!(tx_log.fee_token_id as u64, *Mob::ID); + assert_eq!(tx_log.status(), TxStatus::Pending); assert_eq!( tx_log.submitted_block_index, Some(ledger_db.num_blocks().unwrap() as i64) ); // There is no comment for this submission assert_eq!(tx_log.comment, ""); - // Tx direction is "sent" - assert_eq!(tx_log.direction, TX_DIRECTION_SENT); // The tx in the log matches the tx in the proposal - let tx: Tx = mc_util_serial::decode(&tx_log.clone().tx.unwrap()).unwrap(); + let tx: Tx = mc_util_serial::decode(&tx_log.clone().tx).unwrap(); assert_eq!(tx, tx_proposal.tx); // Check the associated_txos for this transaction_log are as expected @@ -740,25 +673,28 @@ mod tests { // There is one associated input TXO to this transaction, and it is now pending. assert_eq!(associated_txos.inputs.len(), 1); let input_details = Txo::get( - &associated_txos.inputs[0].txo_id_hex, + &associated_txos.inputs[0].id, &wallet_db.get_conn().unwrap(), ) .unwrap(); assert_eq!(input_details.value as u64, 70 * MOB); - assert!(input_details.is_pending()); // Should now be pending - assert!(input_details.is_received()); + assert_eq!( + input_details + .status(&wallet_db.get_conn().unwrap()) + .unwrap(), + TxoStatus::Pending + ); assert_eq!(input_details.subaddress_index.unwrap(), 0); - assert!(!input_details.is_minted()); // There is one associated output TXO to this transaction, and its recipient // is the destination addr assert_eq!(associated_txos.outputs.len(), 1); assert_eq!( - associated_txos.outputs[0].recipient_public_address_b58, + associated_txos.outputs[0].1, b58_encode_public_address(&recipient).unwrap() ); let output_details = Txo::get( - &associated_txos.outputs[0].txo_id_hex, + &associated_txos.outputs[0].0.id, &wallet_db.get_conn().unwrap(), ) .unwrap(); @@ -766,14 +702,12 @@ mod tests { // We cannot know any details about the received_to_account for this TXO, as it // was sent out of the wallet - assert!(output_details.is_minted()); - assert!(!output_details.is_received()); assert!(output_details.subaddress_index.is_none()); // Assert change is as expected assert_eq!(associated_txos.change.len(), 1); let change_details = Txo::get( - &associated_txos.change[0].txo_id_hex, + &associated_txos.change[0].0.id, &wallet_db.get_conn().unwrap(), ) .unwrap(); @@ -781,37 +715,47 @@ mod tests { // Note, this will still be marked as not change until the txo // appears on the ledger and the account syncs. - // change becomes unspent once scanned - assert!(change_details.is_minted()); - assert!(!change_details.is_received()); - assert!(change_details.subaddress_index.is_none()); // this gets filled once scanned + // change becomes unspent once scanned. + // The subaddress will also be set once received. + assert_eq!(change_details.subaddress_index, None,); - // Now - we will add the change TXO to the ledger, so we can scan and verify - add_block_with_tx_outs( + add_block_from_transaction_log( &mut ledger_db, - &[mc_util_serial::decode(&change_details.txo).unwrap()], - &[KeyImage::from(rng.next_u64())], + &wallet_db.get_conn().unwrap(), + &tx_log, + &mut rng, ); + assert_eq!(ledger_db.num_blocks().unwrap(), 14); let _sync = manually_sync_account( &ledger_db, &wallet_db, - &AccountID(tx_log.account_id_hex.to_string()), + &AccountID(tx_log.account_id.to_string()), &logger, ); + let updated_tx_log = TransactionLog::get( + &TransactionID::from(&tx_log), + &wallet_db.get_conn().unwrap(), + ) + .unwrap(); + + assert_eq!(updated_tx_log.status(), TxStatus::Succeeded); + // Get the change txo again let updated_change_details = Txo::get( - &associated_txos.change[0].txo_id_hex, + &associated_txos.change[0].0.id, &wallet_db.get_conn().unwrap(), ) .unwrap(); - assert!(updated_change_details.is_minted()); - assert!(updated_change_details.is_unspent()); assert_eq!( - updated_change_details.received_account_id_hex.unwrap(), - tx_log.account_id_hex + updated_change_details.status(&conn).unwrap(), + TxoStatus::Unspent + ); + assert_eq!( + updated_change_details.account_id.unwrap(), + tx_log.account_id ); assert_eq!( updated_change_details.subaddress_index, @@ -842,17 +786,20 @@ mod tests { // Build a transaction let conn = wallet_db.get_conn().unwrap(); let (recipient, mut builder) = - builder_for_random_recipient(&account_key, &ledger_db, &mut rng, &logger); + builder_for_random_recipient(&account_key, &ledger_db, &mut rng); // Add outlays all to the same recipient, so that we exceed u64::MAX in this tx let value = 100 * MOB - Mob::MINIMUM_FEE; - builder.add_recipient(recipient.clone(), value).unwrap(); + builder + .add_recipient(recipient.clone(), value, Mob::ID) + .unwrap(); builder.set_tombstone(0).unwrap(); - builder.select_txos(&conn, None, false).unwrap(); - let tx_proposal = builder.build(&conn).unwrap(); + builder.select_txos(&conn, None).unwrap(); + let unsigned_tx_proposal = builder.build(TransactionMemo::RTH, &conn).unwrap(); + let tx_proposal = unsigned_tx_proposal.sign(&account_key).unwrap(); let tx_log = TransactionLog::log_submitted( - tx_proposal.clone(), + &tx_proposal, ledger_db.num_blocks().unwrap(), "".to_string(), &AccountID::from(&account_key).to_string(), @@ -860,34 +807,26 @@ mod tests { ) .unwrap(); - assert_eq!( - tx_log.account_id_hex, - AccountID::from(&account_key).to_string() - ); + assert_eq!(tx_log.account_id, AccountID::from(&account_key).to_string()); let associated_txos = tx_log .get_associated_txos(&wallet_db.get_conn().unwrap()) .unwrap(); assert_eq!(associated_txos.outputs.len(), 1); assert_eq!( - associated_txos.outputs[0].recipient_public_address_b58, + associated_txos.outputs[0].1, b58_encode_public_address(&recipient).unwrap() ); - // No assigned subaddress for sent - assert_eq!(tx_log.assigned_subaddress_b58, None); - // Value is the amount sent, not including fee and change - assert_eq!(tx_log.value as u64, value); - // Fee exists for submitted - assert_eq!(tx_log.fee.unwrap() as u64, Mob::MINIMUM_FEE); - // Created and sent transaction is "pending" until it lands - assert_eq!(tx_log.status, TX_STATUS_PENDING); - assert!(tx_log.sent_time.unwrap() > 0); + + assert_eq!(tx_log.value_for_token_id(Mob::ID, &conn).unwrap(), value); + assert_eq!(tx_log.fee_value as u64, Mob::MINIMUM_FEE); + assert_eq!(tx_log.fee_token_id as u64, *Mob::ID); + assert_eq!(tx_log.status(), TxStatus::Pending); assert_eq!( tx_log.submitted_block_index.unwrap() as u64, ledger_db.num_blocks().unwrap() ); assert_eq!(tx_log.comment, ""); - assert_eq!(tx_log.direction, TX_DIRECTION_SENT); - let tx: Tx = mc_util_serial::decode(&tx_log.clone().tx.unwrap()).unwrap(); + let tx: Tx = mc_util_serial::decode(&tx_log.clone().tx).unwrap(); assert_eq!(tx, tx_proposal.tx); // Get associated Txos @@ -901,94 +840,97 @@ mod tests { #[test_with_logger] fn test_delete_transaction_logs_for_account(logger: Logger) { - use crate::db::schema::{transaction_logs, transaction_txo_types}; + use crate::db::schema::{ + transaction_input_txos, transaction_logs, transaction_output_txos, + }; use diesel::dsl::count_star; let mut rng: StdRng = SeedableRng::from_seed([20u8; 32]); let db_test_context = WalletDbTestContext::default(); let wallet_db = db_test_context.get_db_instance(logger.clone()); - let ledger_db = get_test_ledger(5, &[], 12, &mut rng); - - // Populate our DB with some received txos in the same block. - // Do this for two different accounts. - let mut account_ids: Vec = Vec::new(); - for _ in 0..2 { - let root_id = RootIdentity::from_random(&mut rng); - let account_key = AccountKey::from(&root_id); - let (account_id, _address) = Account::create_from_root_entropy( - &root_id.root_entropy, - Some(0), - None, - None, - "", - "".to_string(), - "".to_string(), - "".to_string(), - &ledger_db, - &wallet_db.get_conn().unwrap(), - ) - .unwrap(); + let known_recipients: Vec = Vec::new(); + let mut ledger_db = get_test_ledger(5, &known_recipients, 12, &mut rng); - let subaddress = account_key.subaddress(0); - let assigned_subaddress_b58 = Some(b58_encode_public_address(&subaddress).unwrap()); - - // Ingest relevant txos. - for i in 1..=10 { - let (txo_id_hex, _txo, _key_image) = create_test_received_txo( - &account_key, - 0, // All to the same subaddress - Amount::new(100 * i * MOB, Mob::ID), - 144, - &mut rng, - &wallet_db, - ); - - TransactionLog::log_received( - &account_id.to_string(), - assigned_subaddress_b58.as_ref().map(|s| s.as_str()), - &txo_id_hex, - Amount::new(100 * i * MOB, Mob::ID), - 144, - &wallet_db.get_conn().unwrap(), - ) - .unwrap(); - } + // Start sync thread + let _sync_thread = SyncThread::start(ledger_db.clone(), wallet_db.clone(), logger.clone()); - account_ids.push(account_id); - } + let account_key = random_account_with_seed_values( + &wallet_db, + &mut ledger_db, + &vec![70 * MOB], + &mut rng, + &logger, + ); + + let account_id = AccountID::from(&account_key); + + // Build a transaction + let conn = wallet_db.get_conn().unwrap(); + let (recipient, mut builder) = + builder_for_random_recipient(&account_key, &ledger_db, &mut rng); + builder + .add_recipient(recipient.clone(), 50 * MOB, Mob::ID) + .unwrap(); + builder.set_tombstone(0).unwrap(); + builder.select_txos(&conn, None).unwrap(); + let unsigned_tx_proposal = builder.build(TransactionMemo::RTH, &conn).unwrap(); + let tx_proposal = unsigned_tx_proposal.sign(&account_key).unwrap(); + + // Log submitted transaction from tx_proposal + TransactionLog::log_submitted( + &tx_proposal, + ledger_db.num_blocks().unwrap(), + "".to_string(), + &AccountID::from(&account_key).to_string(), + &conn, + ) + .unwrap(); // Check that we created transaction_logs and transaction_txo_types entries. assert_eq!( - Ok(20), + Ok(1), transaction_logs::table .select(count_star()) .first(&wallet_db.get_conn().unwrap()) ); assert_eq!( - Ok(20), - transaction_txo_types::table + Ok(1), + transaction_input_txos::table + .select(count_star()) + .first(&wallet_db.get_conn().unwrap()) + ); + assert_eq!( + Ok(2), + transaction_output_txos::table .select(count_star()) .first(&wallet_db.get_conn().unwrap()) ); // Delete the transaction logs for one account. let result = TransactionLog::delete_all_for_account( - &account_ids[0].to_string(), + &account_id.to_string(), &wallet_db.get_conn().unwrap(), ); assert!(result.is_ok()); - // For the given account, the transaction logs and the txo types are deleted. + // For the given account, the transaction logs and the txo types are + // deleted. assert_eq!( - Ok(10), + Ok(0), transaction_logs::table .select(count_star()) .first(&wallet_db.get_conn().unwrap()) ); assert_eq!( - Ok(10), - transaction_txo_types::table + Ok(0), + transaction_input_txos::table + .select(count_star()) + .first(&wallet_db.get_conn().unwrap()) + ); + assert_eq!( + Ok(0), + transaction_output_txos::table .select(count_star()) .first(&wallet_db.get_conn().unwrap()) ); @@ -1024,19 +966,23 @@ mod tests { // Build a transaction for > i64::Max let conn = wallet_db.get_conn().unwrap(); let (recipient, mut builder) = - builder_for_random_recipient(&account_key, &ledger_db, &mut rng, &logger); + builder_for_random_recipient(&account_key, &ledger_db, &mut rng); builder - .add_recipient(recipient.clone(), 10_000_000 * MOB) + .add_recipient(recipient.clone(), 10_000_000 * MOB, Mob::ID) .unwrap(); builder.set_tombstone(0).unwrap(); - builder.select_txos(&conn, None, false).unwrap(); - let tx_proposal = builder.build(&conn).unwrap(); + builder.select_txos(&conn, None).unwrap(); + let unsigned_tx_proposal = builder.build(TransactionMemo::RTH, &conn).unwrap(); + let tx_proposal = unsigned_tx_proposal.sign(&account_key).unwrap(); - assert_eq!(tx_proposal.outlays[0].value, 10_000_000_000_000_000_000); + assert_eq!( + tx_proposal.payload_txos[0].amount.value, + 10_000_000_000_000_000_000 + ); // Log submitted transaction from tx_proposal let tx_log = TransactionLog::log_submitted( - tx_proposal.clone(), + &tx_proposal, ledger_db.num_blocks().unwrap(), "".to_string(), &AccountID::from(&account_key).to_string(), @@ -1044,7 +990,8 @@ mod tests { ) .unwrap(); - assert_eq!(tx_log.value as u64, 10_000_000 * MOB); + let pmob_value = tx_log.value_for_token_id(Mob::ID, &conn).unwrap(); + assert_eq!(pmob_value, 10_000_000 * MOB); } // Test that logging a submitted transaction to self results in the inputs, @@ -1085,19 +1032,19 @@ mod tests { AccountID::from(&account_key).to_string(), ledger_db.clone(), get_resolver_factory(&mut rng).unwrap(), - logger.clone(), ); // Add self at main subaddress as the recipient builder - .add_recipient(account_key.subaddress(0), 12 * MOB) + .add_recipient(account_key.subaddress(0), 12 * MOB, Mob::ID) .unwrap(); builder.set_tombstone(0).unwrap(); - builder.select_txos(&conn, None, false).unwrap(); - let tx_proposal = builder.build(&conn).unwrap(); + builder.select_txos(&conn, None).unwrap(); + let unsigned_tx_proposal = builder.build(TransactionMemo::RTH, &conn).unwrap(); + let tx_proposal = unsigned_tx_proposal.sign(&account_key).unwrap(); // Log submitted transaction from tx_proposal let tx_log = TransactionLog::log_submitted( - tx_proposal.clone(), + &tx_proposal, ledger_db.num_blocks().unwrap(), "".to_string(), &AccountID::from(&account_key).to_string(), @@ -1113,62 +1060,68 @@ mod tests { // There are two input TXOs to this transaction, and they are both now pending. assert_eq!(associated_txos.inputs.len(), 2); let input_details0 = Txo::get( - &associated_txos.inputs[0].txo_id_hex, + &associated_txos.inputs[0].id, &wallet_db.get_conn().unwrap(), ) .unwrap(); - assert_eq!(input_details0.value as u64, 8 * MOB); + assert_eq!(input_details0.value, associated_txos.inputs[0].value); - assert!(input_details0.is_pending()); - assert!(input_details0.is_received()); + assert_eq!( + input_details0 + .status(&wallet_db.get_conn().unwrap()) + .unwrap(), + TxoStatus::Pending + ); assert_eq!(input_details0.subaddress_index, Some(0)); - assert!(!input_details0.is_minted()); let input_details1 = Txo::get( - &associated_txos.inputs[1].txo_id_hex, + &associated_txos.inputs[1].id, &wallet_db.get_conn().unwrap(), ) .unwrap(); - assert_eq!(input_details1.value as u64, 7 * MOB); + assert_eq!(input_details1.value, associated_txos.inputs[1].value); - assert!(input_details1.is_pending()); - assert!(input_details1.is_received()); + assert_eq!( + input_details1 + .status(&wallet_db.get_conn().unwrap()) + .unwrap(), + TxoStatus::Pending + ); assert_eq!(input_details1.subaddress_index, Some(0)); - assert!(!input_details1.is_minted()); + + assert_eq!( + input_details0.value as u64 + input_details1.value as u64, + 15 * MOB + ); // There is one associated output TXO to this transaction, and its recipient // is our own address assert_eq!(associated_txos.outputs.len(), 1); assert_eq!( - associated_txos.outputs[0].recipient_public_address_b58, + associated_txos.outputs[0].1, b58_encode_public_address(&account_key.subaddress(0)).unwrap() ); let output_details = Txo::get( - &associated_txos.outputs[0].txo_id_hex, + &associated_txos.outputs[0].0.id, &wallet_db.get_conn().unwrap(), ) .unwrap(); assert_eq!(output_details.value as u64, 12 * MOB); - // The output type is "minted" - assert!(output_details.is_minted()); // We cannot know any details about the received_to_account for this TXO (until // it is scanned) - assert!(!output_details.is_received()); assert!(output_details.subaddress_index.is_none()); // Assert change is as expected assert_eq!(associated_txos.change.len(), 1); let change_details = Txo::get( - &associated_txos.change[0].txo_id_hex, + &associated_txos.change[0].0.id, &wallet_db.get_conn().unwrap(), ) .unwrap(); // Change = (8 + 7) - 12 - fee assert_eq!(change_details.value as u64, 3 * MOB - Mob::MINIMUM_FEE); - assert!(change_details.is_minted()); - assert!(!change_details.is_received()); - assert!(change_details.subaddress_index.is_none()); + assert_eq!(change_details.subaddress_index, None); // Now - we will add the spent Txos, outputs, and change to the ledger, so we // can scan and verify @@ -1182,67 +1135,74 @@ mod tests { mc_util_serial::decode(&input_details0.key_image.unwrap()).unwrap(), mc_util_serial::decode(&input_details1.key_image.unwrap()).unwrap(), ], + &mut rng, ); assert_eq!(ledger_db.num_blocks().unwrap(), 15); let _sync = manually_sync_account( &ledger_db, &wallet_db, - &AccountID(tx_log.account_id_hex.to_string()), + &AccountID(tx_log.account_id.to_string()), &logger, ); // Get the Input Txos again let updated_input_details0 = Txo::get( - &associated_txos.inputs[0].txo_id_hex, + &associated_txos.inputs[0].id, &wallet_db.get_conn().unwrap(), ) .unwrap(); let updated_input_details1 = Txo::get( - &associated_txos.inputs[1].txo_id_hex, + &associated_txos.inputs[1].id, &wallet_db.get_conn().unwrap(), ) .unwrap(); - // We cannot know where these inputs were minted from (unless we had sent them - // to ourselves, which we did not for this test). The outputs were sent - // to ourselves, so will be testing that case, in "output" form. - assert!(!updated_input_details0.is_minted()); - assert!(!updated_input_details1.is_minted()); - // The inputs are now spent - assert!(updated_input_details0.is_spent()); - assert!(updated_input_details1.is_spent()); + assert_eq!( + updated_input_details0 + .status(&wallet_db.get_conn().unwrap()) + .unwrap(), + TxoStatus::Spent + ); + assert_eq!( + updated_input_details1 + .status(&wallet_db.get_conn().unwrap()) + .unwrap(), + TxoStatus::Spent + ); // The received_to account is ourself, which is the same as the account - // account_id_hex in the transaction log. The type is "Received" + // account_id in the transaction log. The type is "Received" assert_eq!( - updated_input_details0.received_account_id_hex, - Some(tx_log.account_id_hex.clone()) + updated_input_details0.account_id, + Some(tx_log.account_id.clone()) ); - assert!(updated_input_details0.is_received()); assert_eq!(updated_input_details0.subaddress_index, Some(0 as i64)); assert_eq!( - updated_input_details1.received_account_id_hex, - Some(tx_log.account_id_hex.clone()) + updated_input_details1.account_id, + Some(tx_log.account_id.clone()) ); - assert!(updated_input_details1.is_received()); assert_eq!(updated_input_details1.subaddress_index, Some(0 as i64)); // Get the output txo again let updated_output_details = Txo::get( - &associated_txos.outputs[0].txo_id_hex, + &associated_txos.outputs[0].0.id, &wallet_db.get_conn().unwrap(), ) .unwrap(); // The minted from account is ourself, and it is unspent, minted - assert!(updated_output_details.is_unspent()); - assert!(updated_output_details.is_minted()); + assert_eq!( + updated_output_details + .status(&wallet_db.get_conn().unwrap()) + .unwrap(), + TxoStatus::Unspent + ); // The received to account is ourself, and it is unspent, minted assert_eq!( - updated_output_details.received_account_id_hex, - Some(tx_log.account_id_hex.clone()) + updated_output_details.account_id, + Some(tx_log.account_id.clone()) ); // Received to main subaddress @@ -1250,111 +1210,24 @@ mod tests { // Get the change txo again let updated_change_details = Txo::get( - &associated_txos.change[0].txo_id_hex, + &associated_txos.change[0].0.id, &wallet_db.get_conn().unwrap(), ) .unwrap(); - assert!(updated_change_details.is_unspent()); - assert!(updated_change_details.is_minted()); assert_eq!( - updated_change_details.received_account_id_hex, - Some(tx_log.account_id_hex) + updated_change_details + .status(&wallet_db.get_conn().unwrap()) + .unwrap(), + TxoStatus::Unspent ); + assert_eq!(updated_change_details.account_id, Some(tx_log.account_id)); assert_eq!( updated_change_details.subaddress_index, Some(CHANGE_SUBADDRESS_INDEX as i64) ); } - #[test_with_logger] - fn test_token_filtering(logger: Logger) { - let mut rng: StdRng = SeedableRng::from_seed([20u8; 32]); - - let db_test_context = WalletDbTestContext::default(); - let wallet_db = db_test_context.get_db_instance(logger); - let ledger_db = get_test_ledger(5, &[], 12, &mut rng); - - let root_id = RootIdentity::from_random(&mut rng); - let account_key = AccountKey::from(&root_id); - let (account_id, _address) = Account::create_from_root_entropy( - &root_id.root_entropy, - Some(0), - None, - None, - "", - "".to_string(), - "".to_string(), - "".to_string(), - &ledger_db, - &wallet_db.get_conn().unwrap(), - ) - .unwrap(); - - // Populate our DB with some received txos in the same block - let subaddress = account_key.subaddress(0); - let assigned_subaddress_b58 = Some(b58_encode_public_address(&subaddress).unwrap()); - - // add one non-mob token - let (non_mob_txo_id_hex, _txo, _key_image) = create_test_received_txo( - &account_key, - 0, // All to the same subaddress - Amount::new((100 * MOB) as u64, TokenId::from(2 as u64)), - 144, - &mut rng, - &wallet_db, - ); - - let conn = wallet_db.get_conn().unwrap(); - - TransactionLog::log_received( - &account_id.to_string(), - assigned_subaddress_b58.as_ref().map(|s| s.as_str()), - &non_mob_txo_id_hex, - Amount::new(100 * MOB, TokenId::from(2 as u64)), - 144, - &conn, - ) - .unwrap(); - - // add one mob token - let (mob_txo_id_hex, _txo, _key_image) = create_test_received_txo( - &account_key, - 0, // All to the same subaddress - Amount::new((100 * MOB) as u64, TokenId::from(MOB_TOKEN_ID as u64)), - 144, - &mut rng, - &wallet_db, - ); - - TransactionLog::log_received( - &account_id.to_string(), - assigned_subaddress_b58.as_ref().map(|s| s.as_str()), - &mob_txo_id_hex, - Amount::new(100 * MOB, TokenId::from(2 as u64)), - 144, - &conn, - ) - .unwrap(); - - let from_block = TransactionLog::get_all_for_block_index(144, &conn).unwrap(); - assert_eq!(from_block.len(), 1); - - let for_mob_txo = TransactionLog::select_for_txo(&mob_txo_id_hex, &conn).unwrap(); - assert_eq!(for_mob_txo.len(), 1); - - let for_non_mob_txo = TransactionLog::select_for_txo(&non_mob_txo_id_hex, &conn).unwrap(); - assert_eq!(for_non_mob_txo.len(), 0); - - let all_logs = - TransactionLog::list_all(&account_id.to_string(), None, None, None, None, &conn) - .unwrap(); - assert_eq!(all_logs.len(), 1); - - let all_logs = TransactionLog::get_all_ordered_by_block_index(&conn).unwrap(); - assert_eq!(all_logs.len(), 1); - } - // FIXME: test_log_submitted for recovered // FIXME: test_log_submitted offline flow } diff --git a/full-service/src/db/txo.rs b/full-service/src/db/txo.rs index 34e6cc783..f95668565 100644 --- a/full-service/src/db/txo.rs +++ b/full-service/src/db/txo.rs @@ -2,35 +2,80 @@ //! DB impl for the Txo model. -use diesel::prelude::*; -use mc_account_keys::{AccountKey, PublicAddress, CHANGE_SUBADDRESS_INDEX}; +use diesel::{ + dsl::{count, exists, not}, + prelude::*, +}; +use mc_account_keys::AccountKey; use mc_common::HashMap; use mc_crypto_digestible::{Digestible, MerlinTranscript}; use mc_crypto_keys::{CompressedRistrettoPublic, RistrettoPublic}; -use mc_mobilecoind::payments::TxProposal; +use mc_ledger_db::{Ledger, LedgerDB}; use mc_transaction_core::{ constants::MAX_INPUTS, ring_signature::KeyImage, - tokens::Mob, - tx::{TxOut, TxOutConfirmationNumber}, - Amount, Token, + tx::{TxOut, TxOutConfirmationNumber, TxOutMembershipProof}, + Amount, TokenId, }; -use std::fmt; +use std::{fmt, str::FromStr}; use crate::{ db::{ account::{AccountID, AccountModel}, assigned_subaddress::AssignedSubaddressModel, - models::{ - Account, AssignedSubaddress, NewTxo, Txo, TXO_STATUS_ORPHANED, TXO_STATUS_PENDING, - TXO_STATUS_SECRETED, TXO_STATUS_SPENT, TXO_STATUS_UNSPENT, TXO_USED_AS_CHANGE, - TXO_USED_AS_OUTPUT, - }, + models::{Account, AssignedSubaddress, NewTransactionOutputTxo, NewTxo, Txo}, + transaction_log::TransactionID, Conn, WalletDbError, }, + service::models::tx_proposal::OutputTxo, util::b58::b58_encode_public_address, }; +#[derive(Debug, PartialEq)] +pub enum TxoStatus { + // The txo has been received at a known subaddress index, but the key image cannot + // be derived (usually because this is a view only account) + Unverified, + // The txo has been received at a known subaddress index with a known key image, has not been + // spent, and is not part of a pending transaction + Unspent, + // The txo is part of a pending transaction + Pending, + // The txo has a known spent block index + Spent, + // The txo has been received but the subaddress index and key image cannot be determined. This + // happens typically when an account is imported but all subaddresses it was using were not + // recreated + Orphaned, +} + +impl fmt::Display for TxoStatus { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + TxoStatus::Unverified => write!(f, "unverified"), + TxoStatus::Unspent => write!(f, "unspent"), + TxoStatus::Pending => write!(f, "pending"), + TxoStatus::Spent => write!(f, "spent"), + TxoStatus::Orphaned => write!(f, "orphaned"), + } + } +} + +impl FromStr for TxoStatus { + type Err = WalletDbError; + + fn from_str(s: &str) -> Result { + match s { + "unverified" => Ok(TxoStatus::Unverified), + "unspent" => Ok(TxoStatus::Unspent), + "pending" => Ok(TxoStatus::Pending), + "spent" => Ok(TxoStatus::Spent), + "orphaned" => Ok(TxoStatus::Orphaned), + _ => Err(WalletDbError::InvalidTxoStatus(s.to_string())), + } + } +} + /// A unique ID derived from a TxOut in the ledger. #[derive(Debug)] pub struct TxoID(pub String); @@ -42,26 +87,29 @@ impl From<&TxOut> for TxoID { } } +impl From<&Txo> for TxoID { + fn from(src: &Txo) -> TxoID { + Self(src.id.clone()) + } +} + impl fmt::Display for TxoID { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "{}", self.0) } } -#[derive(Debug, Clone)] -pub struct ProcessedTxProposalOutput { - /// The recipient of this TxOut - None if change - pub recipient: Option, - pub txo_id_hex: String, - pub value: i64, - pub txo_type: String, -} - pub struct SpendableTxosResult { pub spendable_txos: Vec, pub max_spendable_in_wallet: u128, } +impl Txo { + pub fn amount(&self) -> Amount { + Amount::new(self.value as u64, TokenId::from(self.token_id as u64)) + } +} + pub trait TxoModel { /// Upserts a received Txo. /// @@ -90,79 +138,70 @@ pub trait TxoModel { conn: &Conn, ) -> Result; - /// Processes a TxProposal to create a new minted Txo and a change Txo. - /// - /// Returns: - /// * ProcessedTxProposalOutput - fn create_minted( - account_id_hex: &str, - txo: &TxOut, - tx_proposal: &TxProposal, - outlay_index: usize, - conn: &Conn, - ) -> Result; - - /// Update an existing Txo to spendable by including its subaddress_index - /// and key_image. - fn update_to_spendable( - &self, - received_account_id_hex: &str, - received_subaddress_index: Option, - received_key_image: Option, - block_index: u64, + fn create_new_output( + output_txo: &OutputTxo, + is_change: bool, + transaction_id: &TransactionID, conn: &Conn, ) -> Result<(), WalletDbError>; - /// Update a Txo's received block count. - fn update_received_block_index( + /// Update an existing Txo to spendable by including its subaddress_index + /// and optionally the key_image in the case of view only accounts. + fn update_as_received( &self, + account_id_hex: &str, + subaddress_index: Option, + key_image: Option, block_index: u64, conn: &Conn, ) -> Result<(), WalletDbError>; - /// Update a Txo's status to pending - fn update_to_pending( - &self, - pending_tombstone_block_index: u64, - conn: &Conn, - ) -> Result<(), WalletDbError>; - /// Update a Txo's status to spent - fn update_to_spent( + fn update_spent_block_index( txo_id_hex: &str, spent_block_index: u64, conn: &Conn, ) -> Result<(), WalletDbError>; - /// Update all Txo's that are pending with a pending_tombstone_block_index - /// less than the target block index to unspent - fn update_txos_exceeding_pending_tombstone_block_index_to_unspent( - block_index: u64, + fn update_key_image( + txo_id_hex: &str, + key_image: &KeyImage, + spent_block_index: Option, conn: &Conn, ) -> Result<(), WalletDbError>; + fn list( + status: Option, + min_received_block_index: Option, + max_received_block_index: Option, + offset: Option, + limit: Option, + token_id: Option, + conn: &Conn, + ) -> Result, WalletDbError>; + /// Get all Txos associated with a given account. + #[allow(clippy::too_many_arguments)] fn list_for_account( account_id_hex: &str, - status: Option, + status: Option, + min_received_block_index: Option, + max_received_block_index: Option, offset: Option, limit: Option, token_id: Option, conn: &Conn, ) -> Result, WalletDbError>; + #[allow(clippy::too_many_arguments)] fn list_for_address( assigned_subaddress_b58: &str, - token_id: Option, - conn: &Conn, - ) -> Result, WalletDbError>; - - fn list_unspent( - account_id_hex: &str, - assigned_subaddress_b58: Option<&str>, - token_id: Option, + status: Option, + min_received_block_index: Option, + max_received_block_index: Option, offset: Option, limit: Option, + token_id: Option, conn: &Conn, ) -> Result, WalletDbError>; @@ -173,60 +212,72 @@ pub trait TxoModel { conn: &Conn, ) -> Result, WalletDbError>; - fn list_spent( - account_id_hex: &str, + #[allow(clippy::too_many_arguments)] + fn list_unspent( + account_id_hex: Option<&str>, assigned_subaddress_b58: Option<&str>, token_id: Option, + min_received_block_index: Option, + max_received_block_index: Option, offset: Option, limit: Option, conn: &Conn, ) -> Result, WalletDbError>; - fn list_spendable( - account_id_hex: &str, - max_spendable_value: Option, + #[allow(clippy::too_many_arguments)] + fn list_spent( + account_id_hex: Option<&str>, assigned_subaddress_b58: Option<&str>, token_id: Option, - conn: &Conn, - ) -> Result; - - fn list_secreted( - account_id_hex: &str, - token_id: Option, + min_received_block_index: Option, + max_received_block_index: Option, offset: Option, limit: Option, conn: &Conn, ) -> Result, WalletDbError>; fn list_orphaned( - account_id_hex: &str, + account_id_hex: Option<&str>, token_id: Option, + min_received_block_index: Option, + max_received_block_index: Option, offset: Option, limit: Option, conn: &Conn, ) -> Result, WalletDbError>; + #[allow(clippy::too_many_arguments)] fn list_pending( - account_id_hex: &str, + account_id_hex: Option<&str>, assigned_subaddress_b58: Option<&str>, token_id: Option, + min_received_block_index: Option, + max_received_block_index: Option, offset: Option, limit: Option, conn: &Conn, ) -> Result, WalletDbError>; - fn list_minted( - account_id_hex: &str, + #[allow(clippy::too_many_arguments)] + fn list_unverified( + account_id_hex: Option<&str>, + assigned_subaddress_b58: Option<&str>, token_id: Option, + min_received_block_index: Option, + max_received_block_index: Option, + offset: Option, + limit: Option, conn: &Conn, ) -> Result, WalletDbError>; - fn list_pending_exceeding_block_index( - account_id_hex: &str, - block_index: u64, - token_id: Option, + fn list_spendable( + account_id_hex: Option<&str>, + max_spendable_value: Option, + assigned_subaddress_b58: Option<&str>, + token_id: u64, + default_token_fee: u64, conn: &Conn, - ) -> Result, WalletDbError>; + ) -> Result; /// Get the details for a specific Txo. /// @@ -246,23 +297,19 @@ pub trait TxoModel { /// Select several Txos by their TxoIds /// /// Returns: - /// * Vec<(Txo, TxoStatus)> - fn select_by_id( - txo_ids: &[String], - pending_tombstone_block_index: Option, - conn: &Conn, - ) -> Result, WalletDbError>; + /// * Vec<(Txo)> + fn select_by_id(txo_ids: &[String], conn: &Conn) -> Result, WalletDbError>; /// Select a set of unspent Txos to reach a given value. /// /// Returns: /// * Vec - fn select_unspent_txos_for_value( + fn select_spendable_txos_for_value( account_id_hex: &str, target_value: u64, max_spendable_value: Option, - pending_tombstone_block_index: Option, - token_id: Option, + token_id: u64, + default_token_fee: u64, conn: &Conn, ) -> Result, WalletDbError>; @@ -282,19 +329,10 @@ pub trait TxoModel { /// Delete txos which are not referenced by any account or transaction. fn delete_unreferenced(conn: &Conn) -> Result<(), WalletDbError>; - fn is_change(&self) -> bool; - - fn is_minted(&self) -> bool; + fn status(&self, conn: &Conn) -> Result; - fn is_received(&self) -> bool; - - fn is_unspent(&self) -> bool; - - fn is_pending(&self) -> bool; - - fn is_spent(&self) -> bool; - - fn is_orphaned(&self) -> bool; + fn membership_proof(&self, ledger_db: &LedgerDB) + -> Result; } impl TxoModel for Txo { @@ -315,7 +353,7 @@ impl TxoModel for Txo { // If we already have this TXO for this account (e.g. from minting in a previous // transaction), we need to update it Ok(txo) => { - txo.update_to_spendable( + txo.update_as_received( account_id_hex, subaddress_index, key_image, @@ -328,7 +366,7 @@ impl TxoModel for Txo { Err(WalletDbError::TxoNotFound(_)) => { let key_image_bytes = key_image.map(|k| mc_util_serial::encode(&k)); let new_txo = NewTxo { - txo_id_hex: &txo_id.to_string(), + id: &txo_id.to_string(), value: amount.value as i64, token_id: *amount.token_id as i64, target_key: &mc_util_serial::encode(&txo.target_key), @@ -338,12 +376,9 @@ impl TxoModel for Txo { subaddress_index: subaddress_index.map(|i| i as i64), key_image: key_image_bytes.as_deref(), received_block_index: Some(received_block_index as i64), - pending_tombstone_block_index: None, spent_block_index: None, - confirmation: None, - recipient_public_address_b58: "".to_string(), - minted_account_id_hex: None, - received_account_id_hex: Some(account_id_hex.to_string()), + shared_secret: None, + account_id: Some(account_id_hex.to_string()), }; diesel::insert_into(crate::db::schema::txos::table) @@ -357,95 +392,55 @@ impl TxoModel for Txo { Ok(txo_id.to_string()) } - fn create_minted( - account_id_hex: &str, - output: &TxOut, - tx_proposal: &TxProposal, - output_index: usize, + fn create_new_output( + output_txo: &OutputTxo, + is_change: bool, + transaction_id: &TransactionID, conn: &Conn, - ) -> Result { + ) -> Result<(), WalletDbError> { use crate::db::schema::txos; - let txo_id = TxoID::from(output); - - let total_input_value: u64 = tx_proposal.utxos.iter().map(|u| u.value).sum(); - let total_output_value: u64 = tx_proposal.outlays.iter().map(|o| o.value).sum(); - let change_value: u64 = total_input_value - total_output_value - tx_proposal.fee(); - - // Determine whether this output is an outlay destination, or change. - let (value, confirmation, outlay_receiver) = if let Some(outlay_index) = tx_proposal - .outlay_index_to_tx_out_index - .iter() - .find_map(|(k, &v)| if v == output_index { Some(k) } else { None }) - { - let outlay = &tx_proposal.outlays[*outlay_index]; - ( - outlay.value, - Some(*outlay_index), - Some(outlay.receiver.clone()), - ) - } else { - // This is the change output. Note: there should only be one change output - // per transaction, based on how we construct transactions. If we change - // how we construct transactions, these assumptions will change, and should be - // reflected in the TxProposal. - (change_value, None, None) - }; - - // Update receiver, transaction_value, and transaction_txo_type, if outlay was - // found. - let (transaction_txo_type, log_value, recipient_public_address_b58) = - if let Some(r) = outlay_receiver.clone() { - ( - TXO_USED_AS_OUTPUT, - total_output_value, - b58_encode_public_address(&r)?, - ) - } else { - // If not in an outlay, this output is change, according to how we build - // transactions. - (TXO_USED_AS_CHANGE, change_value, "".to_string()) - }; - - let encoded_confirmation = confirmation - .map(|p| mc_util_serial::encode(&tx_proposal.outlay_confirmation_numbers[p])); + let txo_id = TxoID::from(&output_txo.tx_out); + let encoded_confirmation = mc_util_serial::encode(&output_txo.confirmation_number); - // TODO: Update this to use the txo id of the output we are minting, not - // defaulting to 0 let new_txo = NewTxo { - txo_id_hex: &txo_id.to_string(), - value: value as i64, - token_id: 0, - target_key: &mc_util_serial::encode(&output.target_key), - public_key: &mc_util_serial::encode(&output.public_key), - e_fog_hint: &mc_util_serial::encode(&output.e_fog_hint), - txo: &mc_util_serial::encode(output), + id: &txo_id.to_string(), + account_id: None, + value: output_txo.amount.value as i64, + token_id: *output_txo.amount.token_id as i64, + target_key: &mc_util_serial::encode(&output_txo.tx_out.target_key), + public_key: &mc_util_serial::encode(&output_txo.tx_out.public_key), + e_fog_hint: &mc_util_serial::encode(&output_txo.tx_out.e_fog_hint), + txo: &mc_util_serial::encode(&output_txo.tx_out), subaddress_index: None, - /* Minted set subaddress_index to None. If later - * received, updates. */ - key_image: None, // Only the recipient can calculate the KeyImage + key_image: None, received_block_index: None, - pending_tombstone_block_index: Some(tx_proposal.tx.prefix.tombstone_block as i64), spent_block_index: None, - confirmation: encoded_confirmation.as_deref(), - recipient_public_address_b58, - minted_account_id_hex: Some(account_id_hex.to_string()), - received_account_id_hex: None, + shared_secret: Some(&encoded_confirmation), }; diesel::insert_into(txos::table) .values(&new_txo) .execute(conn)?; - Ok(ProcessedTxProposalOutput { - recipient: outlay_receiver, - txo_id_hex: txo_id.to_string(), - value: log_value as i64, - txo_type: transaction_txo_type.to_string(), - }) + let recipient_public_address_b58 = + &b58_encode_public_address(&output_txo.recipient_public_address)?; + + let new_transaction_output_txo = NewTransactionOutputTxo { + transaction_log_id: &transaction_id.to_string(), + txo_id: &txo_id.to_string(), + recipient_public_address_b58, + is_change, + }; + + diesel::insert_into(crate::db::schema::transaction_output_txos::table) + .values(&new_transaction_output_txo) + .execute(conn)?; + + Ok(()) } - fn update_to_spendable( + fn update_as_received( &self, received_account_id_hex: &str, received_subaddress_index: Option, @@ -459,78 +454,149 @@ impl TxoModel for Txo { diesel::update(self) .set(( - txos::received_account_id_hex.eq(Some(received_account_id_hex)), + txos::account_id.eq(Some(received_account_id_hex)), txos::received_block_index.eq(Some(block_index as i64)), txos::subaddress_index.eq(received_subaddress_index.map(|i| i as i64)), txos::key_image.eq(encoded_key_image), - txos::pending_tombstone_block_index.eq::>(None), )) .execute(conn)?; Ok(()) } - fn update_received_block_index( - &self, - block_index: u64, - conn: &Conn, - ) -> Result<(), WalletDbError> { - use crate::db::schema::txos::received_block_index; - - diesel::update(self) - .set((received_block_index.eq(Some(block_index as i64)),)) - .execute(conn)?; - Ok(()) - } - - fn update_to_pending( - &self, - pending_tombstone_block_index: u64, + fn update_spent_block_index( + txo_id_hex: &str, + spent_block_index: u64, conn: &Conn, ) -> Result<(), WalletDbError> { use crate::db::schema::txos; - diesel::update(self) - .set(txos::pending_tombstone_block_index.eq(Some(pending_tombstone_block_index as i64))) + diesel::update(txos::table.filter(txos::id.eq(txo_id_hex))) + .set((txos::spent_block_index.eq(Some(spent_block_index as i64)),)) .execute(conn)?; Ok(()) } - fn update_to_spent( + fn update_key_image( txo_id_hex: &str, - spent_block_index: u64, + key_image: &KeyImage, + spent_block_index: Option, conn: &Conn, ) -> Result<(), WalletDbError> { use crate::db::schema::txos; - diesel::update(txos::table.filter(txos::txo_id_hex.eq(txo_id_hex))) + let encoded_key_image = mc_util_serial::encode(key_image); + + diesel::update(txos::table.filter(txos::id.eq(txo_id_hex))) .set(( - txos::spent_block_index.eq(Some(spent_block_index as i64)), - txos::pending_tombstone_block_index.eq::>(None), + txos::key_image.eq(Some(encoded_key_image)), + txos::spent_block_index.eq(spent_block_index.map(|i| i as i64)), )) .execute(conn)?; + Ok(()) } - fn update_txos_exceeding_pending_tombstone_block_index_to_unspent( - block_index: u64, + fn list( + status: Option, + min_received_block_index: Option, + max_received_block_index: Option, + offset: Option, + limit: Option, + token_id: Option, conn: &Conn, - ) -> Result<(), WalletDbError> { + ) -> Result, WalletDbError> { use crate::db::schema::txos; - diesel::update( - txos::table - .filter(txos::pending_tombstone_block_index.is_not_null()) - .filter(txos::pending_tombstone_block_index.lt(block_index as i64)), - ) - .set(txos::pending_tombstone_block_index.eq::>(None)) - .execute(conn)?; + if let Some(status) = status { + match status { + TxoStatus::Unverified => { + return Txo::list_unverified( + None, + None, + token_id, + min_received_block_index, + max_received_block_index, + offset, + limit, + conn, + ) + } + TxoStatus::Unspent => { + return Txo::list_unspent( + None, + None, + token_id, + min_received_block_index, + max_received_block_index, + offset, + limit, + conn, + ) + } + TxoStatus::Pending => { + return Txo::list_pending( + None, + None, + token_id, + min_received_block_index, + max_received_block_index, + offset, + limit, + conn, + ) + } + TxoStatus::Spent => { + return Txo::list_spent( + None, + None, + token_id, + min_received_block_index, + max_received_block_index, + offset, + limit, + conn, + ) + } + TxoStatus::Orphaned => { + return Txo::list_orphaned( + None, + token_id, + min_received_block_index, + max_received_block_index, + offset, + limit, + conn, + ) + } + } + } - Ok(()) + let mut query = txos::table.into_boxed(); + + if let (Some(o), Some(l)) = (offset, limit) { + query = query.offset(o as i64).limit(l as i64); + } + + if let Some(token_id) = token_id { + query = query.filter(txos::token_id.eq(token_id as i64)); + } + + if let Some(min_received_block_index) = min_received_block_index { + query = query.filter(txos::received_block_index.ge(min_received_block_index as i64)); + } + + if let Some(max_received_block_index) = max_received_block_index { + query = query.filter(txos::received_block_index.le(max_received_block_index as i64)); + } + + Ok(query.load(conn)?) } fn list_for_account( account_id_hex: &str, - status: Option, + status: Option, + min_received_block_index: Option, + max_received_block_index: Option, offset: Option, limit: Option, token_id: Option, @@ -538,11 +604,73 @@ impl TxoModel for Txo { ) -> Result, WalletDbError> { use crate::db::schema::txos; + if let Some(status) = status { + match status { + TxoStatus::Unverified => { + return Txo::list_unverified( + Some(account_id_hex), + None, + token_id, + min_received_block_index, + max_received_block_index, + offset, + limit, + conn, + ) + } + TxoStatus::Unspent => { + return Txo::list_unspent( + Some(account_id_hex), + None, + token_id, + min_received_block_index, + max_received_block_index, + offset, + limit, + conn, + ) + } + TxoStatus::Pending => { + return Txo::list_pending( + Some(account_id_hex), + None, + token_id, + min_received_block_index, + max_received_block_index, + offset, + limit, + conn, + ) + } + TxoStatus::Spent => { + return Txo::list_spent( + Some(account_id_hex), + None, + token_id, + min_received_block_index, + max_received_block_index, + offset, + limit, + conn, + ) + } + TxoStatus::Orphaned => { + return Txo::list_orphaned( + Some(account_id_hex), + token_id, + min_received_block_index, + max_received_block_index, + offset, + limit, + conn, + ) + } + } + } + let mut query = txos::table.into_boxed(); - query = query - .filter(txos::received_account_id_hex.eq(account_id_hex)) - .or_filter(txos::minted_account_id_hex.eq(account_id_hex)); + query = query.filter(txos::account_id.eq(account_id_hex)); if let (Some(o), Some(l)) = (offset, limit) { query = query.offset(o as i64).limit(l as i64); @@ -552,30 +680,12 @@ impl TxoModel for Txo { query = query.filter(txos::token_id.eq(token_id as i64)); } - if let Some(status) = status { - match status.as_str() { - TXO_STATUS_UNSPENT => { - return Txo::list_unspent(account_id_hex, None, token_id, offset, limit, conn) - } - TXO_STATUS_SPENT => { - return Txo::list_spent(account_id_hex, None, token_id, offset, limit, conn) - } - TXO_STATUS_ORPHANED => { - return Txo::list_orphaned(account_id_hex, token_id, offset, limit, conn) - } - TXO_STATUS_PENDING => { - return Txo::list_pending(account_id_hex, None, token_id, offset, limit, conn) - } - TXO_STATUS_SECRETED => { - return Txo::list_secreted(account_id_hex, token_id, offset, limit, conn) - } - _ => { - return Err(WalletDbError::InvalidArgument(format!( - "Invalid txo status: {:?}", - status - ))) - } - }; + if let Some(min_received_block_index) = min_received_block_index { + query = query.filter(txos::received_block_index.ge(min_received_block_index as i64)); + } + + if let Some(max_received_block_index) = max_received_block_index { + query = query.filter(txos::received_block_index.le(max_received_block_index as i64)); } Ok(query.load(conn)?) @@ -583,44 +693,208 @@ impl TxoModel for Txo { fn list_for_address( assigned_subaddress_b58: &str, + status: Option, + min_received_block_index: Option, + max_received_block_index: Option, + offset: Option, + limit: Option, token_id: Option, conn: &Conn, ) -> Result, WalletDbError> { use crate::db::schema::txos; + + if let Some(status) = status { + match status { + TxoStatus::Unverified => { + return Txo::list_unverified( + None, + Some(assigned_subaddress_b58), + token_id, + min_received_block_index, + max_received_block_index, + offset, + limit, + conn, + ) + } + TxoStatus::Unspent => { + return Txo::list_unspent( + None, + Some(assigned_subaddress_b58), + token_id, + min_received_block_index, + max_received_block_index, + offset, + limit, + conn, + ) + } + TxoStatus::Pending => { + return Txo::list_pending( + None, + Some(assigned_subaddress_b58), + token_id, + min_received_block_index, + max_received_block_index, + offset, + limit, + conn, + ) + } + TxoStatus::Spent => { + return Txo::list_spent( + None, + Some(assigned_subaddress_b58), + token_id, + min_received_block_index, + max_received_block_index, + offset, + limit, + conn, + ) + } + TxoStatus::Orphaned => { + return Ok(vec![]); + } + } + } + let subaddress = AssignedSubaddress::get(assigned_subaddress_b58, conn)?; let mut query = txos::table.into_boxed(); query = query .filter(txos::subaddress_index.eq(subaddress.subaddress_index)) - .filter(txos::received_account_id_hex.eq(subaddress.account_id_hex)); + .filter(txos::account_id.eq(subaddress.account_id)); if let Some(token_id) = token_id { query = query.filter(txos::token_id.eq(token_id as i64)); } + if let Some(min_received_block_index) = min_received_block_index { + query = query.filter(txos::received_block_index.ge(min_received_block_index as i64)); + } + + if let Some(max_received_block_index) = max_received_block_index { + query = query.filter(txos::received_block_index.le(max_received_block_index as i64)); + } + let txos: Vec = query.load(conn)?; Ok(txos) } fn list_unspent( - account_id_hex: &str, + account_id_hex: Option<&str>, assigned_subaddress_b58: Option<&str>, token_id: Option, + min_received_block_index: Option, + max_received_block_index: Option, offset: Option, limit: Option, conn: &Conn, ) -> Result, WalletDbError> { - use crate::db::schema::txos; + use crate::db::schema::{transaction_input_txos, transaction_logs, txos}; + + /* + SELECT * FROM txos + LEFT JOIN transaction_txos + ON txos.id = transaction_txos.txo_id + LEFT JOIN transaction_logs + ON transaction_txos.transaction_log_id = transaction_logs.id + WHERE (transaction_logs.id IS NULL + OR ((transaction_txos.used_as = "input" AND (transaction_logs.failed = 1 OR transaction_logs.submitted_block_index = null)) + OR (transaction_txos.used_as != "input" AND transaction_logs.failed = 0))) + AND txos.key_image IS NOT NULL + AND txos.spent_block_index IS NULL + */ + + let mut query = txos::table + .into_boxed() + .left_join(transaction_input_txos::table) + .left_join( + transaction_logs::table + .on(transaction_logs::id.eq(transaction_input_txos::transaction_log_id)), + ); - let mut query = txos::table.into_boxed(); + query = query.filter( + transaction_logs::id + .is_null() + .or(transaction_logs::failed.eq(true)) + .or(transaction_logs::id + .is_not_null() + .and(transaction_logs::submitted_block_index.is_null())), + ); + + query = query.filter(txos::received_block_index.is_not_null()); + query = query.filter(txos::key_image.is_not_null()); + query = query.filter(txos::spent_block_index.is_null()); + + if let Some(account_id_hex) = account_id_hex { + query = query.filter(txos::account_id.eq(account_id_hex)); + } + + if let (Some(o), Some(l)) = (offset, limit) { + query = query.offset(o as i64).limit(l as i64); + } + + if let Some(subaddress_b58) = assigned_subaddress_b58 { + let subaddress = AssignedSubaddress::get(subaddress_b58, conn)?; + query = query.filter(txos::subaddress_index.eq(subaddress.subaddress_index)); + } + + if let Some(token_id) = token_id { + query = query.filter(txos::token_id.eq(token_id as i64)); + } + + if let Some(min_received_block_index) = min_received_block_index { + query = query.filter(txos::received_block_index.ge(min_received_block_index as i64)); + } + + if let Some(max_received_block_index) = max_received_block_index { + query = query.filter(txos::received_block_index.le(max_received_block_index as i64)); + } + + Ok(query.select(txos::all_columns).distinct().load(conn)?) + } + + fn list_unverified( + account_id_hex: Option<&str>, + assigned_subaddress_b58: Option<&str>, + token_id: Option, + min_received_block_index: Option, + max_received_block_index: Option, + offset: Option, + limit: Option, + conn: &Conn, + ) -> Result, WalletDbError> { + use crate::db::schema::{transaction_input_txos, transaction_logs, txos}; + + let mut query = txos::table + .into_boxed() + .left_join(transaction_input_txos::table) + .left_join( + transaction_logs::table + .on(transaction_logs::id.eq(transaction_input_txos::transaction_log_id)), + ); + + query = query.filter( + transaction_logs::id + .is_null() + .or(transaction_logs::failed.eq(true)) + .or(transaction_logs::id + .is_not_null() + .and(transaction_logs::submitted_block_index.is_null())), + ); query = query - .filter(txos::received_account_id_hex.eq(account_id_hex)) + .filter(txos::received_block_index.is_not_null()) .filter(txos::subaddress_index.is_not_null()) - .filter(txos::pending_tombstone_block_index.is_null()) - .filter(txos::spent_block_index.is_null()); + .filter(txos::key_image.is_null()); + + if let Some(account_id_hex) = account_id_hex { + query = query.filter(txos::account_id.eq(account_id_hex)); + } if let (Some(o), Some(l)) = (offset, limit) { query = query.offset(o as i64).limit(l as i64); @@ -635,7 +909,15 @@ impl TxoModel for Txo { query = query.filter(txos::token_id.eq(token_id as i64)); } - Ok(query.load(conn)?) + if let Some(min_received_block_index) = min_received_block_index { + query = query.filter(txos::received_block_index.ge(min_received_block_index as i64)); + } + + if let Some(max_received_block_index) = max_received_block_index { + query = query.filter(txos::received_block_index.le(max_received_block_index as i64)); + } + + Ok(query.distinct().load(conn)?) } fn list_unspent_or_pending_key_images( @@ -649,7 +931,7 @@ impl TxoModel for Txo { query = query .filter(txos::key_image.is_not_null()) - .filter(txos::received_account_id_hex.eq(account_id_hex)) + .filter(txos::account_id.eq(account_id_hex)) .filter(txos::subaddress_index.is_not_null()) .filter(txos::spent_block_index.is_null()); @@ -657,9 +939,8 @@ impl TxoModel for Txo { query = query.filter(txos::token_id.eq(token_id as i64)); } - let results: Vec<(Option>, String)> = query - .select((txos::key_image, txos::txo_id_hex)) - .load(conn)?; + let results: Vec<(Option>, String)> = + query.select((txos::key_image, txos::id)).load(conn)?; Ok(results .into_iter() @@ -674,9 +955,11 @@ impl TxoModel for Txo { } fn list_spent( - account_id_hex: &str, + account_id_hex: Option<&str>, assigned_subaddress_b58: Option<&str>, token_id: Option, + min_received_block_index: Option, + max_received_block_index: Option, offset: Option, limit: Option, conn: &Conn, @@ -685,9 +968,11 @@ impl TxoModel for Txo { let mut query = txos::table.into_boxed(); - query = query - .filter(txos::received_account_id_hex.eq(account_id_hex)) - .filter(txos::spent_block_index.is_not_null()); + if let Some(account_id_hex) = account_id_hex { + query = query.filter(txos::account_id.eq(account_id_hex)); + } + + query = query.filter(txos::spent_block_index.is_not_null()); if let Some(subaddress_b58) = assigned_subaddress_b58 { let subaddress = AssignedSubaddress::get(subaddress_b58, conn)?; @@ -702,46 +987,22 @@ impl TxoModel for Txo { query = query.offset(o as i64).limit(l as i64); } - Ok(query.load(conn)?) - } - - fn list_secreted( - account_id_hex: &str, - token_id: Option, - offset: Option, - limit: Option, - conn: &Conn, - ) -> Result, WalletDbError> { - use crate::db::schema::txos; - - let mut query = txos::table.into_boxed(); - - // Secreted txos were minted by this account, but not received by this account, - // so they can no longer be decrypted. - query = query - .filter(txos::minted_account_id_hex.eq(account_id_hex)) - .filter( - txos::received_account_id_hex - .ne(account_id_hex) - .or(txos::received_account_id_hex.is_null()), - ); - - if let Some(token_id) = token_id { - query = query.filter(txos::token_id.eq(token_id as i64)); + if let Some(min_received_block_index) = min_received_block_index { + query = query.filter(txos::received_block_index.ge(min_received_block_index as i64)); } - if let (Some(o), Some(l)) = (offset, limit) { - query = query.offset(o as i64).limit(l as i64); + if let Some(max_received_block_index) = max_received_block_index { + query = query.filter(txos::received_block_index.le(max_received_block_index as i64)); } - let txos: Vec = query.load(conn)?; - - Ok(txos) + Ok(query.load(conn)?) } fn list_orphaned( - account_id_hex: &str, + account_id_hex: Option<&str>, token_id: Option, + min_received_block_index: Option, + max_received_block_index: Option, offset: Option, limit: Option, conn: &Conn, @@ -750,9 +1011,13 @@ impl TxoModel for Txo { let mut query = txos::table.into_boxed(); + if let Some(account_id_hex) = account_id_hex { + query = query.filter(txos::account_id.eq(account_id_hex)); + } + query = query - .filter(txos::received_account_id_hex.eq(account_id_hex)) - .filter(txos::subaddress_index.is_null()); + .filter(txos::subaddress_index.is_null()) + .filter(txos::key_image.is_null()); if let Some(token_id) = token_id { query = query.filter(txos::token_id.eq(token_id as i64)); @@ -762,29 +1027,52 @@ impl TxoModel for Txo { query = query.offset(o as i64).limit(l as i64); } + if let Some(min_received_block_index) = min_received_block_index { + query = query.filter(txos::received_block_index.ge(min_received_block_index as i64)); + } + + if let Some(max_received_block_index) = max_received_block_index { + query = query.filter(txos::received_block_index.le(max_received_block_index as i64)); + } + let txos: Vec = query.load(conn)?; Ok(txos) } fn list_pending( - account_id_hex: &str, + account_id_hex: Option<&str>, assigned_subaddress_b58: Option<&str>, token_id: Option, + min_received_block_index: Option, + max_received_block_index: Option, offset: Option, limit: Option, conn: &Conn, ) -> Result, WalletDbError> { - use crate::db::schema::txos; + use crate::db::schema::{transaction_input_txos, transaction_logs, txos}; + + let mut query = txos::table + .into_boxed() + .inner_join(transaction_input_txos::table) + .inner_join( + transaction_logs::table + .on(transaction_logs::id.eq(transaction_input_txos::transaction_log_id)), + ); - let mut query = txos::table.into_boxed(); + query = query + .filter(transaction_logs::failed.eq(false)) + .filter(transaction_logs::finalized_block_index.is_null()) + .filter(transaction_logs::submitted_block_index.is_not_null()); query = query - .filter(txos::received_account_id_hex.eq(account_id_hex)) .filter(txos::subaddress_index.is_not_null()) - .filter(txos::pending_tombstone_block_index.is_not_null()) .filter(txos::spent_block_index.is_null()); + if let Some(account_id_hex) = account_id_hex { + query = query.filter(txos::account_id.eq(account_id_hex)); + } + if let Some(subaddress_b58) = assigned_subaddress_b58 { let subaddress = AssignedSubaddress::get(subaddress_b58, conn)?; query = query.filter(txos::subaddress_index.eq(subaddress.subaddress_index)); @@ -798,60 +1086,24 @@ impl TxoModel for Txo { query = query.offset(o as i64).limit(l as i64); } - let txos: Vec = query.load(conn)?; - - Ok(txos) - } - - fn list_pending_exceeding_block_index( - account_id_hex: &str, - block_index: u64, - token_id: Option, - conn: &Conn, - ) -> Result, WalletDbError> { - use crate::db::schema::txos; - - let mut query = txos::table.into_boxed(); - - query = query - .filter(txos::received_account_id_hex.eq(account_id_hex)) - .filter(txos::subaddress_index.is_not_null()) - .filter(txos::pending_tombstone_block_index.is_not_null()) - .filter(txos::pending_tombstone_block_index.lt(block_index as i64)) - .filter(txos::spent_block_index.is_null()); + if let Some(min_received_block_index) = min_received_block_index { + query = query.filter(txos::received_block_index.ge(min_received_block_index as i64)); + } - if let Some(token_id) = token_id { - query = query.filter(txos::token_id.eq(token_id as i64)); + if let Some(max_received_block_index) = max_received_block_index { + query = query.filter(txos::received_block_index.le(max_received_block_index as i64)); } - let txos: Vec = query.load(conn)?; + let txos: Vec = query.select(txos::all_columns).distinct().load(conn)?; Ok(txos) } - fn list_minted( - account_id_hex: &str, - token_id: Option, - conn: &Conn, - ) -> Result, WalletDbError> { - use crate::db::schema::txos; - - let mut query = txos::table.into_boxed(); - - query = query.filter(txos::minted_account_id_hex.eq(account_id_hex)); - - if let Some(token_id) = token_id { - query = query.filter(txos::token_id.eq(token_id as i64)); - } - - Ok(query.load(conn)?) - } - fn get(txo_id_hex: &str, conn: &Conn) -> Result { use crate::db::schema::txos; let txo = match txos::table - .filter(txos::txo_id_hex.eq(txo_id_hex)) + .filter(txos::id.eq(txo_id_hex)) .get_result::(conn) { Ok(t) => t, @@ -882,67 +1134,65 @@ impl TxoModel for Txo { Ok(selected) } - fn select_by_id( - txo_ids: &[String], - pending_tombstone_block_index: Option, - conn: &Conn, - ) -> Result, WalletDbError> { + fn select_by_id(txo_ids: &[String], conn: &Conn) -> Result, WalletDbError> { use crate::db::schema::txos; - let txos: Vec = txos::table - .filter(txos::txo_id_hex.eq_any(txo_ids)) - .load(conn)?; - - if let Some(pending_tombstone_block_index) = pending_tombstone_block_index { - for txo in &txos { - txo.update_to_pending(pending_tombstone_block_index, conn)?; - } - } + let txos: Vec = txos::table.filter(txos::id.eq_any(txo_ids)).load(conn)?; Ok(txos) } fn list_spendable( - account_id_hex: &str, + account_id_hex: Option<&str>, max_spendable_value: Option, assigned_subaddress_b58: Option<&str>, - token_id: Option, + token_id: u64, + default_token_fee: u64, conn: &Conn, ) -> Result { - use crate::db::schema::txos; + use crate::db::schema::{transaction_input_txos, transaction_logs, txos}; + + let mut query = txos::table + .into_boxed() + .left_join(transaction_input_txos::table) + .left_join( + transaction_logs::table + .on(transaction_logs::id.eq(transaction_input_txos::transaction_log_id)), + ); + + query = query + .filter(transaction_logs::id.is_null()) + .or_filter(transaction_logs::failed.eq(true)) + .or_filter( + transaction_logs::id + .is_not_null() + .and(transaction_logs::submitted_block_index.is_null()), + ); - let mut query = txos::table.into_boxed(); - // The SQLite database cannot filter effectively on a u64 value, so filter for - // maximum value in memory. query = query + .filter(txos::received_block_index.is_not_null()) .filter(txos::spent_block_index.is_null()) - .filter(txos::pending_tombstone_block_index.is_null()) .filter(txos::subaddress_index.is_not_null()) - .filter(txos::key_image.is_not_null()) - .filter(txos::received_account_id_hex.eq(account_id_hex)); + .filter(txos::token_id.eq(token_id as i64)); - if let Some(token_id) = token_id { - query = query.filter(txos::token_id.eq(token_id as i64)); + if let Some(subaddress_b58) = assigned_subaddress_b58 { + let subaddress = AssignedSubaddress::get(subaddress_b58, conn)?; + query = query.filter(txos::subaddress_index.eq(subaddress.subaddress_index)); } - let spendable_txos: Vec = if let Some(subaddress_b58) = assigned_subaddress_b58 { - let subaddress = AssignedSubaddress::get(subaddress_b58, conn)?; - query - .filter(txos::subaddress_index.eq(subaddress.subaddress_index)) - .order_by(txos::value.desc()) - .load(conn)? - } else { - query.order_by(txos::value.desc()).load(conn)? - }; + if let Some(max_spendable_value) = max_spendable_value { + query = query.filter(txos::value.le(max_spendable_value as i64)); + } - let spendable_txos = if let Some(msv) = max_spendable_value { - spendable_txos - .into_iter() - .filter(|txo| (txo.value as u64) <= msv) - .collect() - } else { - spendable_txos - }; + if let Some(account_id_hex) = account_id_hex { + query = query.filter(txos::account_id.eq(account_id_hex)); + } + + let spendable_txos = query + .select(txos::all_columns) + .distinct() + .order_by(txos::value.desc()) + .load(conn)?; // The maximum spendable is limited by the maximal number of inputs we can use. // Since the txos are sorted by decreasing value, this is the maximum @@ -953,11 +1203,11 @@ impl TxoModel for Txo { let mut max_spendable_in_wallet: u128 = spendable_txos .iter() .take(MAX_INPUTS as usize) - .map(|utxo| (utxo.value as u64) as u128) + .map(|utxo: &Txo| (utxo.value as u64) as u128) .sum(); - if max_spendable_in_wallet > Mob::MINIMUM_FEE as u128 { - max_spendable_in_wallet -= Mob::MINIMUM_FEE as u128; + if max_spendable_in_wallet > default_token_fee as u128 { + max_spendable_in_wallet -= default_token_fee as u128; } else { max_spendable_in_wallet = 0; } @@ -968,19 +1218,25 @@ impl TxoModel for Txo { }) } - fn select_unspent_txos_for_value( + fn select_spendable_txos_for_value( account_id_hex: &str, - // target_value includes the network fee target_value: u64, max_spendable_value: Option, - pending_tombstone_block_index: Option, - token_id: Option, + token_id: u64, + default_token_fee: u64, conn: &Conn, ) -> Result, WalletDbError> { let SpendableTxosResult { mut spendable_txos, max_spendable_in_wallet, - } = Txo::list_spendable(account_id_hex, max_spendable_value, None, token_id, conn)?; + } = Txo::list_spendable( + Some(account_id_hex), + max_spendable_value, + None, + token_id, + default_token_fee, + conn, + )?; if spendable_txos.is_empty() { return Err(WalletDbError::NoSpendableTxos); @@ -988,14 +1244,14 @@ impl TxoModel for Txo { // If we're trying to spend more than we have in the wallet, we may need to // defrag - if target_value as u128 > max_spendable_in_wallet + Mob::MINIMUM_FEE as u128 { + if target_value as u128 > max_spendable_in_wallet + default_token_fee as u128 { // See if we merged the UTXOs we would be able to spend this amount. let total_unspent_value_in_wallet: u128 = spendable_txos .iter() .map(|utxo| (utxo.value as u64) as u128) .sum(); - if total_unspent_value_in_wallet >= (target_value + Mob::MINIMUM_FEE) as u128 { + if total_unspent_value_in_wallet >= (target_value + default_token_fee) as u128 { return Err(WalletDbError::InsufficientFundsFragmentedTxos); } else { return Err(WalletDbError::InsufficientFundsUnderMaxSpendable(format!( @@ -1041,11 +1297,6 @@ impl TxoModel for Txo { "Logic error. Could not select Txos despite having sufficient funds".to_string(), )); } - if let Some(pending_tombstone_block_index) = pending_tombstone_block_index { - for txo in &selected_utxos { - txo.update_to_pending(pending_tombstone_block_index, conn)?; - } - } Ok(selected_utxos) } @@ -1066,68 +1317,95 @@ impl TxoModel for Txo { fn scrub_account(account_id_hex: &str, conn: &Conn) -> Result<(), WalletDbError> { use crate::db::schema::txos; - let txos_received_by_account = - txos::table.filter(txos::received_account_id_hex.eq(account_id_hex)); + let txos_received_by_account = txos::table.filter(txos::account_id.eq(account_id_hex)); diesel::update(txos_received_by_account) - .set(txos::received_account_id_hex.eq::>(None)) - .execute(conn)?; - - let txos_minted_by_account = - txos::table.filter(txos::minted_account_id_hex.eq(account_id_hex)); - - diesel::update(txos_minted_by_account) - .set(txos::minted_account_id_hex.eq::>(None)) + .set(txos::account_id.eq::>(None)) .execute(conn)?; Ok(()) } fn delete_unreferenced(conn: &Conn) -> Result<(), WalletDbError> { - use crate::db::schema::txos; + use crate::db::schema::{transaction_input_txos, transaction_output_txos, txos}; + + /* + SELECT * FROM txos + WHERE NOT EXISTS (SELECT * FROM transaction_input_txos WHERE transaction_input_txos.txo_id = txos.id) + AND NOT EXISTS (SELECT * FROM transaction_output_txos WHERE transaction_output_txos.txo_id = txos.id) + AND txos.account_id_hex IS NULL + */ let unreferenced_txos = txos::table - .filter(txos::minted_account_id_hex.is_null()) - .filter(txos::received_account_id_hex.is_null()); + .filter(not(exists( + transaction_input_txos::table.filter(transaction_input_txos::txo_id.eq(txos::id)), + ))) + .filter(not(exists( + transaction_output_txos::table.filter(transaction_output_txos::txo_id.eq(txos::id)), + ))) + .filter(txos::account_id.is_null()); diesel::delete(unreferenced_txos).execute(conn)?; Ok(()) } - fn is_change(&self) -> bool { - self.minted_account_id_hex == self.received_account_id_hex - && self.subaddress_index == Some(CHANGE_SUBADDRESS_INDEX as i64) - } + fn status(&self, conn: &Conn) -> Result { + use crate::db::schema::{ + transaction_input_txos, transaction_logs, transaction_output_txos, + }; - fn is_minted(&self) -> bool { - self.minted_account_id_hex.is_some() - } + if self.spent_block_index.is_some() { + return Ok(TxoStatus::Spent); + } - fn is_received(&self) -> bool { - self.received_account_id_hex.is_some() - } + let num_pending_logs: i64 = transaction_logs::table + .inner_join(transaction_input_txos::table) + .inner_join(transaction_output_txos::table) + .filter( + transaction_input_txos::txo_id + .eq(&self.id) + .or(transaction_output_txos::txo_id.eq(&self.id)), + ) + .filter(transaction_logs::tombstone_block_index.is_not_null()) + .filter(transaction_logs::finalized_block_index.is_null()) + .filter(transaction_logs::failed.eq(false)) + .select(count(transaction_logs::id)) + .first(conn)?; - fn is_unspent(&self) -> bool { - !self.is_pending() && !self.is_spent() && !self.is_orphaned() - } + let pending = num_pending_logs > 0; - fn is_pending(&self) -> bool { - self.pending_tombstone_block_index.is_some() - } + if pending { + return Ok(TxoStatus::Pending); + } - fn is_spent(&self) -> bool { - self.spent_block_index.is_some() + if self.subaddress_index.is_some() && self.key_image.is_some() { + Ok(TxoStatus::Unspent) + } else if self.subaddress_index.is_some() { + Ok(TxoStatus::Unverified) + } else { + Ok(TxoStatus::Orphaned) + } } - fn is_orphaned(&self) -> bool { - self.subaddress_index.is_none() && self.is_received() + fn membership_proof( + &self, + ledger_db: &LedgerDB, + ) -> Result { + let index = ledger_db.get_tx_out_index_by_public_key(&self.public_key()?)?; + let membership_proof = ledger_db + .get_tx_out_proof_of_memberships(&[index])? + .first() + .ok_or_else(|| WalletDbError::MissingTxoMembershipProof(self.id.clone()))? + .clone(); + + Ok(membership_proof) } } #[cfg(test)] mod tests { - use mc_account_keys::{AccountKey, RootIdentity, CHANGE_SUBADDRESS_INDEX}; + use mc_account_keys::{AccountKey, PublicAddress, RootIdentity, CHANGE_SUBADDRESS_INDEX}; use mc_common::{ logger::{log, test_with_logger, Logger}, HashSet, @@ -1148,10 +1426,11 @@ mod tests { }, service::{ sync::{sync_account, SyncThread}, + transaction::TransactionMemo, transaction_builder::WalletTransactionBuilder, }, test_utils::{ - add_block_with_db_txos, add_block_with_tx_outs, add_block_with_tx_proposal, + add_block_with_db_txos, add_block_with_tx, add_block_with_tx_outs, create_test_minted_and_change_txos, create_test_received_txo, create_test_txo_for_recipient, get_resolver_factory, get_test_ledger, manually_sync_account, random_account_with_seed_values, WalletDbTestContext, MOB, @@ -1185,7 +1464,6 @@ mod tests { "".to_string(), "".to_string(), "".to_string(), - &ledger_db, &wallet_db.get_conn().unwrap(), ) .unwrap(); @@ -1203,6 +1481,7 @@ mod tests { &mut ledger_db, &[for_alice_txo.clone()], &[KeyImage::from(rng.next_u64())], + &mut rng, ); assert_eq!(ledger_db.num_blocks().unwrap(), 13); @@ -1214,6 +1493,8 @@ mod tests { None, None, None, + None, + None, Some(0), &wallet_db.get_conn().unwrap(), ) @@ -1222,8 +1503,7 @@ mod tests { // Verify that the Txo is what we expect let expected_txo = Txo { - id: 1, - txo_id_hex: TxoID::from(&for_alice_txo).to_string(), + id: TxoID::from(&for_alice_txo).to_string(), value: 1000 * MOB as i64, token_id: 0, target_key: mc_util_serial::encode(&for_alice_txo.target_key), @@ -1233,23 +1513,22 @@ mod tests { subaddress_index: Some(0), key_image: Some(mc_util_serial::encode(&for_alice_key_image)), received_block_index: Some(12), - pending_tombstone_block_index: None, spent_block_index: None, - confirmation: None, - recipient_public_address_b58: "".to_string(), - minted_account_id_hex: None, - received_account_id_hex: Some(alice_account_id.to_string()), + shared_secret: None, + account_id: Some(alice_account_id.to_string()), }; assert_eq!(expected_txo, txos[0]); // Verify that the status filter works as well let unspent = Txo::list_unspent( - &alice_account_id.to_string(), + Some(&alice_account_id.to_string()), None, Some(0), None, None, + None, + None, &wallet_db.get_conn().unwrap(), ) .unwrap(); @@ -1259,23 +1538,30 @@ mod tests { // have not yet assigned. At the DB layer, we accomplish this by // constructing the output txos, then logging sent and received for this // account. - let ((output_txo_id, output_value), (change_txo_id, change_value)) = - create_test_minted_and_change_txos( - alice_account_key.clone(), - alice_account_key.subaddress(4), - 33 * MOB, - wallet_db.clone(), - ledger_db.clone(), - logger.clone(), - ); - assert_eq!(output_value, 33 * MOB); - assert_eq!(change_value, 967 * MOB - Mob::MINIMUM_FEE); + let transaction_log = create_test_minted_and_change_txos( + alice_account_key.clone(), + alice_account_key.subaddress(4), + 33 * MOB, + wallet_db.clone(), + ledger_db.clone(), + ); + + let associated_txos = transaction_log + .get_associated_txos(&wallet_db.get_conn().unwrap()) + .unwrap(); + + let (minted_txo, _) = associated_txos.outputs.first().unwrap(); + let (change_txo, _) = associated_txos.change.first().unwrap(); + + assert_eq!(minted_txo.value as u64, 33 * MOB); + assert_eq!(change_txo.value as u64, 967 * MOB - Mob::MINIMUM_FEE); add_block_with_db_txos( &mut ledger_db, &wallet_db, - &[output_txo_id, change_txo_id], + &[minted_txo.id.clone(), change_txo.id.clone()], &[KeyImage::from(for_alice_key_image)], + &mut rng, ); assert_eq!(ledger_db.num_blocks().unwrap(), 14); @@ -1290,6 +1576,8 @@ mod tests { None, None, None, + None, + None, Some(0), &wallet_db.get_conn().unwrap(), ) @@ -1299,7 +1587,9 @@ mod tests { // test spent let spent_txos = Txo::list_for_account( &alice_account_id.to_string(), - Some(TXO_STATUS_SPENT.to_string()), + Some(TxoStatus::Spent), + None, + None, None, None, Some(0), @@ -1311,7 +1601,9 @@ mod tests { // test unspent let unspent_txos = Txo::list_for_account( &alice_account_id.to_string(), - Some(TXO_STATUS_UNSPENT.to_string()), + Some(TxoStatus::Unspent), + None, + None, None, None, Some(0), @@ -1320,7 +1612,6 @@ mod tests { .unwrap(); assert_eq!(unspent_txos.len(), 1); - // println!("{}", serde_json::to_string_pretty(&txos).unwrap()); // Check that we have 2 spendable (1 is orphaned) let spendable: Vec<&Txo> = txos.iter().filter(|f| f.key_image.is_some()).collect(); assert_eq!(spendable.len(), 2); @@ -1328,11 +1619,13 @@ mod tests { // Check that we have one spent - went from [Received, Unspent] -> [Received, // Spent] let spent = Txo::list_spent( - &alice_account_id.to_string(), + Some(&alice_account_id.to_string()), None, Some(0), None, None, + None, + None, &wallet_db.get_conn().unwrap(), ) .unwrap(); @@ -1342,32 +1635,34 @@ mod tests { Some(mc_util_serial::encode(&for_alice_key_image)) ); assert_eq!(spent[0].spent_block_index.clone().unwrap(), 13); - assert_eq!(spent[0].minted_account_id_hex, None); // Check that we have one orphaned - went from [Minted, Secreted] -> [Minted, // Orphaned] let orphaned = Txo::list_orphaned( - &alice_account_id.to_string(), + Some(&alice_account_id.to_string()), Some(0), None, None, + None, + None, &wallet_db.get_conn().unwrap(), ) .unwrap(); assert_eq!(orphaned.len(), 1); assert!(orphaned[0].key_image.is_none()); assert_eq!(orphaned[0].received_block_index.clone().unwrap(), 13); - assert!(orphaned[0].minted_account_id_hex.is_some()); - assert!(orphaned[0].received_account_id_hex.is_some()); + assert!(orphaned[0].account_id.is_some()); // Check that we have one unspent (change) - went from [Minted, Secreted] -> // [Minted, Unspent] let unspent = Txo::list_unspent( - &alice_account_id.to_string(), + Some(&alice_account_id.to_string()), None, Some(0), None, None, + None, + None, &wallet_db.get_conn().unwrap(), ) .unwrap(); @@ -1393,48 +1688,35 @@ mod tests { let alice_account = Account::get(&alice_account_id, &wallet_db.get_conn().unwrap()).unwrap(); assert_eq!(alice_account.next_block_index, 14); - assert_eq!(alice_account.next_subaddress_index, 5); - - // Check that a transaction log entry was created for each received TxOut (note: - // we are not creating submit logs in this test) - let transaction_logs = TransactionLog::list_all( - &alice_account_id.to_string(), - None, - None, - None, - None, - &wallet_db.get_conn().unwrap(), - ) - .unwrap(); - assert_eq!(transaction_logs.len(), 3); + assert_eq!( + alice_account + .next_subaddress_index(&wallet_db.get_conn().unwrap()) + .unwrap(), + 5 + ); // Verify that there are two unspent txos - the one that was previously // orphaned, and change. let unspent = Txo::list_unspent( - &alice_account_id.to_string(), + Some(&alice_account_id.to_string()), None, Some(0), None, None, + None, + None, &wallet_db.get_conn().unwrap(), ) .unwrap(); - println!("{}", serde_json::to_string_pretty(&unspent).unwrap()); assert_eq!(unspent.len(), 2); - let minted = Txo::list_minted( - &alice_account_id.to_string(), - Some(0), - &wallet_db.get_conn().unwrap(), - ) - .unwrap(); - assert_eq!(minted.len(), 2); - let updated_txos = Txo::list_for_account( &alice_account_id.to_string(), None, None, None, + None, + None, Some(0), &wallet_db.get_conn().unwrap(), ) @@ -1468,29 +1750,35 @@ mod tests { "".to_string(), "".to_string(), "".to_string(), - &ledger_db, &wallet_db.get_conn().unwrap(), ) .unwrap(); - let ((output_txo_id, output_value), (change_txo_id, change_value)) = - create_test_minted_and_change_txos( - alice_account_key.clone(), - bob_account_key.subaddress(0), - 72 * MOB, - wallet_db.clone(), - ledger_db.clone(), - logger.clone(), - ); - assert_eq!(output_value, 72 * MOB); - assert_eq!(change_value, 928 * MOB - (2 * Mob::MINIMUM_FEE)); + let transaction_log = create_test_minted_and_change_txos( + alice_account_key.clone(), + bob_account_key.subaddress(0), + 72 * MOB, + wallet_db.clone(), + ledger_db.clone(), + ); + + let associated_txos = transaction_log + .get_associated_txos(&wallet_db.get_conn().unwrap()) + .unwrap(); + + let (minted_txo, _) = associated_txos.outputs.first().unwrap(); + let (change_txo, _) = associated_txos.change.first().unwrap(); + + assert_eq!(minted_txo.value as u64, 72 * MOB); + assert_eq!(change_txo.value as u64, 928 * MOB - (2 * Mob::MINIMUM_FEE)); // Add the minted Txos to the ledger add_block_with_db_txos( &mut ledger_db, &wallet_db, - &[output_txo_id, change_txo_id], + &[minted_txo.id.clone(), change_txo.id.clone()], &[KeyImage::from(for_bob_key_image)], + &mut rng, ); // Process the latest block for Bob (note, Bob is starting to sync from block 0) @@ -1505,6 +1793,8 @@ mod tests { None, None, None, + None, + None, Some(0), &wallet_db.get_conn().unwrap(), ) @@ -1522,7 +1812,6 @@ mod tests { let db_test_context = WalletDbTestContext::default(); let wallet_db = db_test_context.get_db_instance(logger); - let ledger_db = get_test_ledger(5, &[], 12, &mut rng); let root_id = RootIdentity::from_random(&mut rng); let account_key = AccountKey::from(&root_id); @@ -1535,7 +1824,6 @@ mod tests { "".to_string(), "".to_string(), "".to_string(), - &ledger_db, &wallet_db.get_conn().unwrap(), ) .unwrap(); @@ -1554,12 +1842,12 @@ mod tests { } // Greedily take smallest to exact value - let txos_for_value = Txo::select_unspent_txos_for_value( + let txos_for_value = Txo::select_spendable_txos_for_value( &account_id_hex.to_string(), 300 * MOB, None, - None, - Some(0), + 0, + Mob::MINIMUM_FEE, &wallet_db.get_conn().unwrap(), ) .unwrap(); @@ -1567,12 +1855,12 @@ mod tests { assert_eq!(result_set, HashSet::from_iter([100 * MOB, 200 * MOB])); // Once we include the fee, we need another txo - let txos_for_value = Txo::select_unspent_txos_for_value( + let txos_for_value = Txo::select_spendable_txos_for_value( &account_id_hex.to_string(), 300 * MOB + Mob::MINIMUM_FEE, None, - None, - Some(0), + 0, + Mob::MINIMUM_FEE, &wallet_db.get_conn().unwrap(), ) .unwrap(); @@ -1583,12 +1871,12 @@ mod tests { ); // Setting max spendable value gives us insufficient funds - only allows 100 - let res = Txo::select_unspent_txos_for_value( + let res = Txo::select_spendable_txos_for_value( &account_id_hex.to_string(), 300 * MOB + Mob::MINIMUM_FEE, Some(200 * MOB), - None, - Some(0), + 0, + Mob::MINIMUM_FEE, &wallet_db.get_conn().unwrap(), ); @@ -1600,12 +1888,12 @@ mod tests { // sum(300..1800) to get a window where we had to increase past the smallest // txos, and also fill up all 16 input slots. - let txos_for_value = Txo::select_unspent_txos_for_value( + let txos_for_value = Txo::select_spendable_txos_for_value( &account_id_hex.to_string(), 16800 * MOB, None, - None, - Some(0), + 0, + Mob::MINIMUM_FEE, &wallet_db.get_conn().unwrap(), ) .unwrap(); @@ -1639,7 +1927,6 @@ mod tests { let db_test_context = WalletDbTestContext::default(); let wallet_db = db_test_context.get_db_instance(logger); - let ledger_db = get_test_ledger(5, &[], 12, &mut rng); let root_id = RootIdentity::from_random(&mut rng); let account_key = AccountKey::from(&root_id); @@ -1652,7 +1939,6 @@ mod tests { "".to_string(), "".to_string(), "".to_string(), - &ledger_db, &wallet_db.get_conn().unwrap(), ) .unwrap(); @@ -1672,29 +1958,32 @@ mod tests { // sum(300..1800) to get a window where we had to increase past the smallest // txos, and also fill up all 16 input slots. - Txo::select_unspent_txos_for_value( + Txo::select_spendable_txos_for_value( &account_id_hex.to_string(), 16800 * MOB, None, - Some(100), - Some(0), + 0, + Mob::MINIMUM_FEE, &wallet_db.get_conn().unwrap(), ) .unwrap(); - let res = Txo::select_unspent_txos_for_value( + let res = Txo::select_spendable_txos_for_value( &account_id_hex.to_string(), 16800 * MOB, - None, - Some(100), - Some(0), + Some(100 * MOB), + 0, + Mob::MINIMUM_FEE, &wallet_db.get_conn().unwrap(), ); match res { Err(WalletDbError::InsufficientFundsUnderMaxSpendable(_)) => {} - Ok(_) => panic!("Should error with InsufficientFundsUnderMaxSpendable"), - Err(_) => panic!("Should error with InsufficientFundsUnderMaxSpendable"), + Ok(_) => panic!("Should error with InsufficientFundsUnderMaxSpendable, but got Ok"), + Err(e) => panic!( + "Should error with InsufficientFundsUnderMaxSpendable, but got {:?}", + e + ), } } @@ -1704,7 +1993,6 @@ mod tests { let db_test_context = WalletDbTestContext::default(); let wallet_db = db_test_context.get_db_instance(logger); - let ledger_db = get_test_ledger(5, &[], 12, &mut rng); let root_id = RootIdentity::from_random(&mut rng); let account_key = AccountKey::from(&root_id); @@ -1717,7 +2005,6 @@ mod tests { "".to_string(), "".to_string(), "".to_string(), - &ledger_db, &wallet_db.get_conn().unwrap(), ) .unwrap(); @@ -1735,12 +2022,12 @@ mod tests { ); } - let res = Txo::select_unspent_txos_for_value( + let res = Txo::select_spendable_txos_for_value( &account_id_hex.to_string(), // FIXME: WS-11 - take AccountID 1800 * MOB, None, - None, - Some(0), + 0, + Mob::MINIMUM_FEE, &wallet_db.get_conn().unwrap(), ); match res { @@ -1776,7 +2063,6 @@ mod tests { "".to_string(), "".to_string(), "".to_string(), - &ledger_db, &wallet_db.get_conn().unwrap(), ) .unwrap(); @@ -1793,29 +2079,40 @@ mod tests { let recipient = AccountKey::from(&RootIdentity::from_random(&mut rng)).subaddress(rng.next_u64()); - let ((output_txo_id, output_value), (change_txo_id, change_value)) = - create_test_minted_and_change_txos( - src_account.clone(), - recipient, - 1 * MOB, - wallet_db.clone(), - ledger_db, - logger, - ); + let txos = Txo::list_for_account( + &AccountID::from(&src_account).to_string(), + None, + None, + None, + None, + None, + None, + &wallet_db.get_conn().unwrap(), + ) + .unwrap(); + + assert_eq!(txos.len(), 12); - assert_eq!(output_value, 1 * MOB); - let minted_txo = Txo::get(&output_txo_id, &wallet_db.get_conn().unwrap()).unwrap(); - assert_eq!(minted_txo.value as u64, output_value); - assert!(minted_txo.minted_account_id_hex.is_some()); - assert!(minted_txo.received_account_id_hex.is_none()); - - assert_eq!(change_value, 4999 * MOB - Mob::MINIMUM_FEE); - let change_txo = Txo::get(&change_txo_id, &wallet_db.get_conn().unwrap()).unwrap(); - assert_eq!(change_txo.value as u64, change_value); - assert!(change_txo.minted_account_id_hex.is_some()); - assert!(change_txo.received_account_id_hex.is_none()); // Note: This - // gets updated - // on sync + let transaction_log = create_test_minted_and_change_txos( + src_account.clone(), + recipient, + 1 * MOB, + wallet_db.clone(), + ledger_db, + ); + + let associated_txos = transaction_log + .get_associated_txos(&wallet_db.get_conn().unwrap()) + .unwrap(); + + let (minted_txo, _) = associated_txos.outputs.first().unwrap(); + let (change_txo, _) = associated_txos.change.first().unwrap(); + + assert_eq!(minted_txo.value as u64, 1 * MOB); + assert!(minted_txo.account_id.is_none()); + + assert_eq!(change_txo.value as u64, 4999 * MOB - Mob::MINIMUM_FEE); + assert!(change_txo.account_id.is_none()); } // Test that the confirmation number validates correctly. @@ -1842,7 +2139,6 @@ mod tests { "".to_string(), "".to_string(), "".to_string(), - &ledger_db, &wallet_db.get_conn().unwrap(), ) .unwrap(); @@ -1865,19 +2161,24 @@ mod tests { // Number log::info!(logger, "Creating transaction builder"); let conn = wallet_db.get_conn().unwrap(); + let mut builder: WalletTransactionBuilder = WalletTransactionBuilder::new( AccountID::from(&sender_account_key).to_string(), ledger_db.clone(), get_resolver_factory(&mut rng).unwrap(), - logger.clone(), ); builder - .add_recipient(recipient_account_key.default_subaddress(), 50 * MOB) + .add_recipient( + recipient_account_key.default_subaddress(), + 50 * MOB, + Mob::ID, + ) .unwrap(); - builder.select_txos(&conn, None, false).unwrap(); + builder.select_txos(&conn, None).unwrap(); builder.set_tombstone(0).unwrap(); - let proposal = builder.build(&conn).unwrap(); + let unsigned_tx_proposal = builder.build(TransactionMemo::RTH, &conn).unwrap(); + let proposal = unsigned_tx_proposal.sign(&sender_account_key).unwrap(); // Sleep to make sure that the foreign keys exist std::thread::sleep(Duration::from_secs(3)); @@ -1886,7 +2187,7 @@ mod tests { // sent Txo log::info!(logger, "Logging submitted transaction"); let tx_log = TransactionLog::log_submitted( - proposal.clone(), + &proposal, ledger_db.num_blocks().unwrap(), "".to_string(), &sender_account_id.to_string(), @@ -1897,7 +2198,7 @@ mod tests { // Now we need to let this txo hit the ledger, which will update sender and // receiver log::info!(logger, "Adding block from submitted"); - add_block_with_tx_proposal(&mut ledger_db, proposal.clone()); + add_block_with_tx(&mut ledger_db, proposal.tx.clone(), &mut rng); // Now let our sync thread catch up for both sender and receiver log::info!(logger, "Manually syncing account"); @@ -1911,6 +2212,8 @@ mod tests { None, None, None, + None, + None, Some(0), &wallet_db.get_conn().unwrap(), ) @@ -1922,7 +2225,7 @@ mod tests { // Note: Because this txo is both received and sent, between two different // accounts, its confirmation number does get updated. Typically, received txos // have None for the confirmation number. - assert!(received_txo.confirmation.is_some()); + assert!(received_txo.shared_secret.is_some()); // Get the txo from the sent perspective log::info!(logger, "Listing all Txos for sender account"); @@ -1931,14 +2234,15 @@ mod tests { None, None, None, + None, + None, Some(0), &wallet_db.get_conn().unwrap(), ) .unwrap(); - // We seeded with 3 received (70, 80, 90), we have a change txo, and a secreted - // Txo (50) - assert_eq!(sender_txos.len(), 5); + // We seeded with 3 received (70, 80, 90), and a change txo + assert_eq!(sender_txos.len(), 4); // Get the associated Txos with the transaction log log::info!(logger, "Getting associated Txos with the transaction"); @@ -1948,19 +2252,19 @@ mod tests { let sent_outputs = associated.outputs; assert_eq!(sent_outputs.len(), 1); let sent_txo_details = - Txo::get(&sent_outputs[0].txo_id_hex, &wallet_db.get_conn().unwrap()).unwrap(); + Txo::get(&sent_outputs[0].0.id, &wallet_db.get_conn().unwrap()).unwrap(); // These two txos should actually be the same txo, and the account_txo_status is // what differentiates them. assert_eq!(sent_txo_details, received_txo); - assert!(sent_txo_details.confirmation.is_some()); + assert!(sent_txo_details.shared_secret.is_some()); let confirmation: TxOutConfirmationNumber = - mc_util_serial::decode(&sent_txo_details.confirmation.unwrap()).unwrap(); + mc_util_serial::decode(&sent_txo_details.shared_secret.unwrap()).unwrap(); log::info!(logger, "Validating the confirmation number"); let verified = Txo::validate_confirmation( &AccountID::from(&recipient_account_key), - &received_txo.txo_id_hex, + &received_txo.id, &confirmation, &wallet_db.get_conn().unwrap(), ) @@ -1974,8 +2278,6 @@ mod tests { let db_test_context = WalletDbTestContext::default(); let wallet_db = db_test_context.get_db_instance(logger); - let known_recipients: Vec = Vec::new(); - let ledger_db = get_test_ledger(5, &known_recipients, 12, &mut rng); let root_id = RootIdentity::from_random(&mut rng); let account_key = AccountKey::from(&root_id); @@ -1988,7 +2290,6 @@ mod tests { "".to_string(), "".to_string(), "".to_string(), - &ledger_db, &wallet_db.get_conn().unwrap(), ) .unwrap(); @@ -2028,7 +2329,6 @@ mod tests { let db_test_context = WalletDbTestContext::default(); let wallet_db = db_test_context.get_db_instance(logger); - let ledger_db = get_test_ledger(5, &[], 12, &mut rng); let root_id = RootIdentity::from_random(&mut rng); let account_key = AccountKey::from(&root_id); @@ -2041,7 +2341,6 @@ mod tests { "".to_string(), "".to_string(), "".to_string(), - &ledger_db, &wallet_db.get_conn().unwrap(), ) .unwrap(); @@ -2050,7 +2349,7 @@ mod tests { // Create some txos. assert_eq!( txos::table - .select(count(txos::txo_id_hex)) + .select(count(txos::id)) .first::(&wallet_db.get_conn().unwrap()) .unwrap(), 0 @@ -2067,7 +2366,7 @@ mod tests { } assert_eq!( txos::table - .select(count(txos::txo_id_hex)) + .select(count(txos::id)) .first::(&wallet_db.get_conn().unwrap()) .unwrap(), 10 @@ -2078,6 +2377,8 @@ mod tests { None, None, None, + None, + None, Some(0), &wallet_db.get_conn().unwrap(), ) @@ -2092,6 +2393,8 @@ mod tests { None, None, None, + None, + None, Some(0), &wallet_db.get_conn().unwrap(), ) @@ -2100,7 +2403,7 @@ mod tests { assert_eq!( txos::table - .select(count(txos::txo_id_hex)) + .select(count(txos::id)) .first::(&wallet_db.get_conn().unwrap()) .unwrap(), 0 @@ -2114,7 +2417,6 @@ mod tests { let db_test_context = WalletDbTestContext::default(); let wallet_db = db_test_context.get_db_instance(logger); let conn = wallet_db.get_conn().unwrap(); - let ledger_db = get_test_ledger(5, &[], 12, &mut rng); let root_id = RootIdentity::from_random(&mut rng); let account_key = AccountKey::from(&root_id); @@ -2127,7 +2429,6 @@ mod tests { "".to_string(), "".to_string(), "".to_string(), - &ledger_db, &conn, ) .unwrap(); @@ -2148,7 +2449,15 @@ mod tests { let SpendableTxosResult { spendable_txos, max_spendable_in_wallet, - } = Txo::list_spendable(&account_id.to_string(), None, None, Some(0), &conn).unwrap(); + } = Txo::list_spendable( + Some(&account_id.to_string()), + None, + None, + 0, + Mob::MINIMUM_FEE, + &conn, + ) + .unwrap(); assert_eq!(spendable_txos.len(), 20); assert_eq!( @@ -2164,7 +2473,6 @@ mod tests { let db_test_context = WalletDbTestContext::default(); let wallet_db = db_test_context.get_db_instance(logger); let conn = wallet_db.get_conn().unwrap(); - let ledger_db = get_test_ledger(5, &[], 12, &mut rng); let root_id = RootIdentity::from_random(&mut rng); let account_key = AccountKey::from(&root_id); @@ -2177,7 +2485,6 @@ mod tests { "".to_string(), "".to_string(), "".to_string(), - &ledger_db, &conn, ) .unwrap(); @@ -2198,7 +2505,15 @@ mod tests { let SpendableTxosResult { spendable_txos, max_spendable_in_wallet, - } = Txo::list_spendable(&account_id.to_string(), None, None, Some(0), &conn).unwrap(); + } = Txo::list_spendable( + Some(&account_id.to_string()), + None, + None, + 0, + Mob::MINIMUM_FEE, + &conn, + ) + .unwrap(); assert_eq!(spendable_txos.len(), 10); assert_eq!(max_spendable_in_wallet as u64, 0); @@ -2211,7 +2526,6 @@ mod tests { let db_test_context = WalletDbTestContext::default(); let wallet_db = db_test_context.get_db_instance(logger); let conn = wallet_db.get_conn().unwrap(); - let ledger_db = get_test_ledger(5, &[], 12, &mut rng); let root_id = RootIdentity::from_random(&mut rng); let account_key = AccountKey::from(&root_id); @@ -2224,7 +2538,6 @@ mod tests { "".to_string(), "".to_string(), "".to_string(), - &ledger_db, &conn, ) .unwrap(); @@ -2278,10 +2591,11 @@ mod tests { spendable_txos, max_spendable_in_wallet, } = Txo::list_spendable( - &account_id.to_string(), + Some(&account_id.to_string()), Some(100 * MOB), None, - Some(0), + 0, + Mob::MINIMUM_FEE, &conn, ) .unwrap(); @@ -2293,12 +2607,116 @@ mod tests { ); } + #[test_with_logger] + fn test_unspent_txo_query(logger: Logger) { + // make sure it only includes txos with key image and subaddress + let mut rng: StdRng = SeedableRng::from_seed([20u8; 32]); + + let db_test_context = WalletDbTestContext::default(); + let wallet_db = db_test_context.get_db_instance(logger); + let conn = wallet_db.get_conn().unwrap(); + + let root_id = RootIdentity::from_random(&mut rng); + let account_key = AccountKey::from(&root_id); + let (account_id, _address) = Account::create_from_root_entropy( + &root_id.root_entropy, + Some(0), + None, + None, + "", + "".to_string(), + "".to_string(), + "".to_string(), + &wallet_db.get_conn().unwrap(), + ) + .unwrap(); + + let amount = Amount::new(28922973268924, Mob::ID); + + let (txo, key_image) = + create_test_txo_for_recipient(&account_key, 1, amount.clone(), &mut rng); + + // create 1 txo with no key image and no subaddress + Txo::create_received( + txo.clone(), + None, + None, + amount.clone(), + 15, + &account_id.to_string(), + &wallet_db.get_conn().unwrap(), + ) + .unwrap(); + + let txos = Txo::list_unspent( + Some(&account_id.to_string()), + None, + None, + None, + None, + None, + None, + &conn, + ) + .unwrap(); + assert_eq!(txos.len(), 0); + + // create 1 txo with subaddress, but not key image + Txo::create_received( + txo.clone(), + Some(1), + None, + amount.clone(), + 15, + &account_id.to_string(), + &wallet_db.get_conn().unwrap(), + ) + .unwrap(); + + let txos = Txo::list_unspent( + Some(&account_id.to_string()), + None, + None, + None, + None, + None, + None, + &conn, + ) + .unwrap(); + assert_eq!(txos.len(), 0); + + // create 1 txo with key image and subaddress + Txo::create_received( + txo.clone(), + Some(1), + Some(key_image), + amount.clone(), + 15, + &account_id.to_string(), + &wallet_db.get_conn().unwrap(), + ) + .unwrap(); + + let txos = Txo::list_unspent( + Some(&account_id.to_string()), + None, + None, + None, + None, + None, + None, + &conn, + ) + .unwrap(); + assert_eq!(txos.len(), 1); + } + fn setup_select_unspent_txos_tests(logger: Logger, fragmented: bool) -> (AccountID, WalletDb) { let mut rng: StdRng = SeedableRng::from_seed([20u8; 32]); let db_test_context = WalletDbTestContext::default(); let wallet_db = db_test_context.get_db_instance(logger); - let ledger_db = get_test_ledger(5, &[], 12, &mut rng); let root_id = RootIdentity::from_random(&mut rng); let account_key = AccountKey::from(&root_id); @@ -2311,7 +2729,6 @@ mod tests { "".to_string(), "".to_string(), "".to_string(), - &ledger_db, &wallet_db.get_conn().unwrap(), ) .unwrap(); @@ -2390,12 +2807,12 @@ mod tests { let target_value: u64 = 200 as u64 * MOB - Mob::MINIMUM_FEE; let (account_id, wallet_db) = setup_select_unspent_txos_tests(logger, false); - let result = Txo::select_unspent_txos_for_value( + let result = Txo::select_spendable_txos_for_value( &account_id.to_string(), target_value, None, - None, - Some(0), + 0, + Mob::MINIMUM_FEE, &wallet_db.get_conn().unwrap(), ) .unwrap(); @@ -2408,12 +2825,12 @@ mod tests { fn test_select_unspent_txos_target_value_over_max_spendable_in_account(logger: Logger) { let (account_id, wallet_db) = setup_select_unspent_txos_tests(logger, false); - let result = Txo::select_unspent_txos_for_value( + let result = Txo::select_spendable_txos_for_value( &account_id.to_string(), 201 as u64 * MOB, None, - None, - Some(0), + 0, + Mob::MINIMUM_FEE, &wallet_db.get_conn().unwrap(), ); @@ -2426,12 +2843,12 @@ mod tests { ) { let (account_id, wallet_db) = setup_select_unspent_txos_tests(logger, false); - let result = Txo::select_unspent_txos_for_value( + let result = Txo::select_spendable_txos_for_value( &account_id.to_string(), 3 as u64, None, - None, - Some(0), + 0, + Mob::MINIMUM_FEE, &wallet_db.get_conn().unwrap(), ) .unwrap(); @@ -2442,12 +2859,12 @@ mod tests { fn test_select_unspent_txos_target_value_over_total_mob_in_account(logger: Logger) { let (account_id, wallet_db) = setup_select_unspent_txos_tests(logger, false); - let result = Txo::select_unspent_txos_for_value( + let result = Txo::select_spendable_txos_for_value( &account_id.to_string(), 500 as u64 * MOB, None, - None, - Some(0), + 0, + Mob::MINIMUM_FEE, &wallet_db.get_conn().unwrap(), ); assert!(result.is_err()); @@ -2459,12 +2876,12 @@ mod tests { ) { let (account_id, wallet_db) = setup_select_unspent_txos_tests(logger, true); - let result = Txo::select_unspent_txos_for_value( + let result = Txo::select_spendable_txos_for_value( &account_id.to_string(), 12400000000 as u64, None, - None, - Some(0), + 0, + Mob::MINIMUM_FEE, &wallet_db.get_conn().unwrap(), ) .unwrap(); diff --git a/full-service/src/db/view_only_account.rs b/full-service/src/db/view_only_account.rs deleted file mode 100644 index cefeba2ed..000000000 --- a/full-service/src/db/view_only_account.rs +++ /dev/null @@ -1,308 +0,0 @@ -// Copyright (c) 2020-2022 MobileCoin Inc. - -//! DB impl for the View Only Account model. - -use crate::{ - db::{ - account::{AccountID, AccountModel}, - models::{Account, NewViewOnlyAccount, ViewOnlyAccount, ViewOnlySubaddress, ViewOnlyTxo}, - schema, - view_only_subaddress::ViewOnlySubaddressModel, - view_only_txo::ViewOnlyTxoModel, - Conn, WalletDbError, - }, - util::{b58::b58_decode_public_address, encoding_helpers::ristretto_to_vec}, -}; -use diesel::prelude::*; -use mc_account_keys::PublicAddress; -use mc_crypto_keys::RistrettoPrivate; -use std::str; - -pub trait ViewOnlyAccountModel { - // insert new view-only-account in the db\ - #[allow(clippy::too_many_arguments)] - fn create( - account_id_hex: &str, - view_private_key: &RistrettoPrivate, - first_block_index: u64, - import_block_index: u64, - main_subaddress_index: u64, - change_subaddress_index: u64, - next_subaddress_index: u64, - name: &str, - conn: &Conn, - ) -> Result; - - /// Get a specific account. - /// Returns: - /// * ViewOnlyAccount - fn get(account_id: &str, conn: &Conn) -> Result; - - /// List all view-only-accounts. - /// Returns: - /// * Vector of all View Only Accounts in the DB - fn list_all(conn: &Conn) -> Result, WalletDbError>; - - /// Update an view-only-account name. - /// The only updatable field is the name. Any other desired update requires - /// adding a new account, and deleting the existing if desired. - fn update_name(&self, new_name: &str, conn: &Conn) -> Result<(), WalletDbError>; - - /// Update the next block index this account will need to sync. - fn update_next_block_index( - &self, - next_block_index: u64, - conn: &Conn, - ) -> Result<(), WalletDbError>; - - fn update_next_subaddress_index( - &self, - next_subaddress_index: u64, - conn: &Conn, - ) -> Result<(), WalletDbError>; - - fn change_public_address(&self, conn: &Conn) -> Result; - - /// Delete a view-only-account. - fn delete(self, conn: &Conn) -> Result<(), WalletDbError>; -} - -impl ViewOnlyAccountModel for ViewOnlyAccount { - fn create( - account_id_hex: &str, - view_private_key: &RistrettoPrivate, - first_block_index: u64, - import_block_index: u64, - main_subaddress_index: u64, - change_subaddress_index: u64, - next_subaddress_index: u64, - name: &str, - conn: &Conn, - ) -> Result { - use schema::view_only_accounts; - - if Account::get(&AccountID(account_id_hex.to_string()), conn).is_ok() { - return Err(WalletDbError::ViewOnlyAccountAlreadyExists( - account_id_hex.to_string(), - )); - } - - let encoded_key = ristretto_to_vec(view_private_key); - - let new_view_only_account = NewViewOnlyAccount { - account_id_hex, - view_private_key: &encoded_key, - first_block_index: first_block_index as i64, - next_block_index: first_block_index as i64, - import_block_index: import_block_index as i64, - name, - next_subaddress_index: next_subaddress_index as i64, - main_subaddress_index: main_subaddress_index as i64, - change_subaddress_index: change_subaddress_index as i64, - }; - - diesel::insert_into(view_only_accounts::table) - .values(&new_view_only_account) - .execute(conn)?; - - ViewOnlyAccount::get(account_id_hex, conn) - } - - fn get(account_id: &str, conn: &Conn) -> Result { - use schema::view_only_accounts::dsl::{ - account_id_hex as dsl_account_id, view_only_accounts, - }; - - match view_only_accounts - .filter((dsl_account_id).eq(account_id.to_string())) - .get_result::(conn) - { - Ok(a) => Ok(a), - // Match on NotFound to get a more informative NotFound Error - Err(diesel::result::Error::NotFound) => { - Err(WalletDbError::AccountNotFound(account_id.to_string())) - } - Err(e) => Err(e.into()), - } - } - - fn list_all(conn: &Conn) -> Result, WalletDbError> { - use schema::view_only_accounts; - - Ok(view_only_accounts::table - .select(view_only_accounts::all_columns) - .load::(conn)?) - } - - fn update_name(&self, new_name: &str, conn: &Conn) -> Result<(), WalletDbError> { - use schema::view_only_accounts::dsl::{ - account_id_hex as dsl_account_id, name as dsl_name, view_only_accounts, - }; - - diesel::update(view_only_accounts.filter(dsl_account_id.eq(&self.account_id_hex))) - .set(dsl_name.eq(new_name)) - .execute(conn)?; - Ok(()) - } - - fn update_next_block_index( - &self, - next_block_index: u64, - conn: &Conn, - ) -> Result<(), WalletDbError> { - use schema::view_only_accounts::dsl::{ - account_id_hex as dsl_account_id, next_block_index as dsl_next_block, - view_only_accounts, - }; - diesel::update(view_only_accounts.filter(dsl_account_id.eq(&self.account_id_hex))) - .set(dsl_next_block.eq(next_block_index as i64)) - .execute(conn)?; - Ok(()) - } - - fn update_next_subaddress_index( - &self, - next_subaddress_index: u64, - conn: &Conn, - ) -> Result<(), WalletDbError> { - use crate::db::schema::view_only_accounts; - - diesel::update( - view_only_accounts::table - .filter(view_only_accounts::account_id_hex.eq(&self.account_id_hex)), - ) - .set(view_only_accounts::next_subaddress_index.eq(next_subaddress_index as i64)) - .execute(conn)?; - - Ok(()) - } - - fn change_public_address(&self, conn: &Conn) -> Result { - use crate::db::schema::view_only_subaddresses; - - let change_subaddress = view_only_subaddresses::table - .filter(view_only_subaddresses::view_only_account_id_hex.eq(&self.account_id_hex)) - .filter(view_only_subaddresses::subaddress_index.eq(self.change_subaddress_index)) - .first::(conn)?; - - let change_public_address = - b58_decode_public_address(&change_subaddress.public_address_b58)?; - - Ok(change_public_address) - } - - fn delete(self, conn: &Conn) -> Result<(), WalletDbError> { - use schema::view_only_accounts::dsl::{ - account_id_hex as dsl_account_id, view_only_accounts, - }; - - // delete associated view-only-txos - ViewOnlyTxo::delete_all_for_account(&self.account_id_hex, conn)?; - ViewOnlySubaddress::delete_all_for_account(&self.account_id_hex, conn)?; - diesel::delete(view_only_accounts.filter(dsl_account_id.eq(&self.account_id_hex))) - .execute(conn)?; - Ok(()) - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::test_utils::WalletDbTestContext; - use mc_account_keys::{CHANGE_SUBADDRESS_INDEX, DEFAULT_SUBADDRESS_INDEX}; - use mc_common::logger::{test_with_logger, Logger}; - use mc_crypto_keys::RistrettoPrivate; - use mc_util_from_random::FromRandom; - use rand::{rngs::StdRng, SeedableRng}; - - #[test_with_logger] - fn test_view_only_account_crud(logger: Logger) { - let db_test_context = WalletDbTestContext::default(); - let wallet_db = db_test_context.get_db_instance(logger); - let conn = wallet_db.get_conn().unwrap(); - - let mut rng: StdRng = SeedableRng::from_seed([20u8; 32]); - - // test account creation - - let name = "Coins for cats"; - let view_private_key = RistrettoPrivate::from_random(&mut rng); - let first_block_index: u64 = 25; - let import_block_index: u64 = 26; - let account_id_hex = "abcd"; - - let expected_account = ViewOnlyAccount { - id: 1, - account_id_hex: account_id_hex.to_string(), - view_private_key: ristretto_to_vec(&view_private_key), - first_block_index: first_block_index as i64, - next_block_index: first_block_index as i64, - import_block_index: import_block_index as i64, - name: name.to_string(), - main_subaddress_index: DEFAULT_SUBADDRESS_INDEX as i64, - change_subaddress_index: CHANGE_SUBADDRESS_INDEX as i64, - next_subaddress_index: 2, - }; - - let created = ViewOnlyAccount::create( - account_id_hex, - &view_private_key, - first_block_index, - import_block_index, - DEFAULT_SUBADDRESS_INDEX, - CHANGE_SUBADDRESS_INDEX, - 2, - &name, - &conn, - ) - .unwrap(); - assert_eq!(expected_account, created); - - // test account name update - - let new_name = "coins for dogs"; - - created.update_name(&new_name, &conn).unwrap(); - - // test updating next block index - - let new_next_block = 100; - - created - .update_next_block_index(new_next_block, &conn) - .unwrap(); - - // test getting an account - - let updated: ViewOnlyAccount = ViewOnlyAccount::get(&account_id_hex, &conn).unwrap(); - - assert_eq!(&updated.name, &new_name); - assert_eq!(updated.next_block_index as u64, new_next_block); - - // test getting all accounts - - ViewOnlyAccount::create( - "some_account_id", - &view_private_key, - first_block_index, - import_block_index, - DEFAULT_SUBADDRESS_INDEX, - CHANGE_SUBADDRESS_INDEX, - 2, - "catcoin_name", - &conn, - ) - .unwrap(); - - let all_accounts = ViewOnlyAccount::list_all(&conn).unwrap(); - - assert_eq!(all_accounts.len(), 2); - - // test deleting the account - - created.delete(&conn).unwrap(); - - let not_found = ViewOnlyAccount::get(&account_id_hex, &conn); - assert!(not_found.is_err()); - } -} diff --git a/full-service/src/db/view_only_subaddress.rs b/full-service/src/db/view_only_subaddress.rs deleted file mode 100644 index 8ba34f4f2..000000000 --- a/full-service/src/db/view_only_subaddress.rs +++ /dev/null @@ -1,163 +0,0 @@ -// Copyright (c) 2020-2021 MobileCoin Inc. - -//! A subaddress assigned to a particular contact for the purpose of tracking -//! funds received from that contact. - -use crate::db::{ - models::{NewViewOnlySubaddress, ViewOnlyAccount, ViewOnlySubaddress, ViewOnlyTxo}, - view_only_txo::ViewOnlyTxoModel, -}; - -use mc_crypto_keys::{RistrettoPrivate, RistrettoPublic}; -use mc_transaction_core::{onetime_keys::recover_public_subaddress_spend_key, tx::TxOut}; - -use crate::db::{Conn, WalletDbError}; -use diesel::prelude::*; -use std::convert::TryFrom; - -pub trait ViewOnlySubaddressModel { - fn create( - account: &ViewOnlyAccount, - public_address_b58: &str, - subaddress_index: u64, - comment: &str, - public_spend_key: &RistrettoPublic, - conn: &Conn, - ) -> Result; - - /// Get the Subaddress for a given subaddress_b58 - fn get(public_address_b58: &str, conn: &Conn) -> Result; - - fn get_for_account_by_index( - account_id: &str, - subaddress_index: u64, - conn: &Conn, - ) -> Result; - - fn list_all( - account_id_hex: &str, - offset: Option, - limit: Option, - conn: &Conn, - ) -> Result, WalletDbError>; - - fn delete_all_for_account(account_id_hex: &str, conn: &Conn) -> Result<(), WalletDbError>; -} - -impl ViewOnlySubaddressModel for ViewOnlySubaddress { - fn create( - account: &ViewOnlyAccount, - public_address_b58: &str, - subaddress_index: u64, - comment: &str, - public_spend_key: &RistrettoPublic, - conn: &Conn, - ) -> Result { - use crate::db::schema::view_only_subaddresses; - - let new_subaddress = NewViewOnlySubaddress { - view_only_account_id_hex: &account.account_id_hex, - public_address_b58, - subaddress_index: subaddress_index as i64, - comment, - public_spend_key: &public_spend_key.to_bytes(), - }; - - diesel::insert_into(view_only_subaddresses::table) - .values(&new_subaddress) - .execute(conn)?; - - let orphaned_txos_with_key_images = - ViewOnlyTxo::list_orphaned_with_key_images(&account.account_id_hex, None, conn)?; - - let view_private_key: RistrettoPrivate = mc_util_serial::decode(&account.view_private_key)?; - - for txo in orphaned_txos_with_key_images { - let tx_out: TxOut = mc_util_serial::decode(&txo.txo)?; - - let txo_subaddress_spk = recover_public_subaddress_spend_key( - &view_private_key, - &RistrettoPublic::try_from(&tx_out.target_key)?, - &RistrettoPublic::try_from(&tx_out.public_key)?, - ); - - if txo_subaddress_spk == *public_spend_key { - txo.update_subaddress_index(subaddress_index, conn)?; - } - } - - Ok(public_address_b58.to_string()) - } - - fn get(public_address_b58: &str, conn: &Conn) -> Result { - use crate::db::schema::view_only_subaddresses; - - let subaddress: ViewOnlySubaddress = match view_only_subaddresses::table - .filter(view_only_subaddresses::public_address_b58.eq(public_address_b58)) - .get_result::(conn) - { - Ok(t) => t, - // Match on NotFound to get a more informative NotFound Error - Err(diesel::result::Error::NotFound) => { - return Err(WalletDbError::AssignedSubaddressNotFound( - public_address_b58.to_string(), - )); - } - Err(e) => { - return Err(e.into()); - } - }; - - Ok(subaddress) - } - - fn get_for_account_by_index( - account_id: &str, - subaddress_index: u64, - conn: &Conn, - ) -> Result { - use crate::db::schema::view_only_subaddresses; - - let subaddress: ViewOnlySubaddress = view_only_subaddresses::table - .filter(view_only_subaddresses::view_only_account_id_hex.eq(account_id)) - .filter(view_only_subaddresses::subaddress_index.eq(subaddress_index as i64)) - .get_result::(conn)?; - - Ok(subaddress) - } - - fn list_all( - account_id_hex: &str, - offset: Option, - limit: Option, - conn: &Conn, - ) -> Result, WalletDbError> { - use crate::db::schema::view_only_subaddresses; - - let addresses_query = view_only_subaddresses::table - .filter(view_only_subaddresses::view_only_account_id_hex.eq(account_id_hex)) - .select(view_only_subaddresses::all_columns); - - let subaddresses: Vec = if let (Some(o), Some(l)) = (offset, limit) { - addresses_query - .offset(o as i64) - .limit(l as i64) - .load(conn)? - } else { - addresses_query.load(conn)? - }; - - Ok(subaddresses) - } - - fn delete_all_for_account(account_id_hex: &str, conn: &Conn) -> Result<(), WalletDbError> { - use crate::db::schema::view_only_subaddresses; - - diesel::delete( - view_only_subaddresses::table - .filter(view_only_subaddresses::view_only_account_id_hex.eq(account_id_hex)), - ) - .execute(conn)?; - Ok(()) - } -} diff --git a/full-service/src/db/view_only_txo.rs b/full-service/src/db/view_only_txo.rs deleted file mode 100644 index 43975855a..000000000 --- a/full-service/src/db/view_only_txo.rs +++ /dev/null @@ -1,745 +0,0 @@ -// Copyright (c) 2020-2022 MobileCoin Inc. - -//! DB impl for the view-only Txo model. - -use crate::db::{ - models::{NewViewOnlyTxo, ViewOnlyAccount, ViewOnlySubaddress, ViewOnlyTxo}, - schema, - txo::TxoID, - view_only_account::ViewOnlyAccountModel, - view_only_subaddress::ViewOnlySubaddressModel, - Conn, WalletDbError, -}; -use diesel::prelude::*; -use mc_common::HashMap; -use mc_transaction_core::{constants::MAX_INPUTS, ring_signature::KeyImage, tx::TxOut, Amount}; - -pub trait ViewOnlyTxoModel { - /// insert a new txo linked to a view-only-account - fn create( - tx_out: TxOut, - amount: Amount, - subaddress_index: Option, - received_block_index: Option, - view_only_account_id_hex: &str, - conn: &Conn, - ) -> Result; - - /// Get the details for a specific view only Txo. - /// - /// Returns: - /// * ViewOnlyTxo - fn get(txo_id_hex: &str, conn: &Conn) -> Result; - - /// list view only txos for a view only account - /// - /// Returns: - /// * Vec - fn list_for_account( - account_id_hex: &str, - offset: Option, - limit: Option, - token_id: Option, - conn: &Conn, - ) -> Result, WalletDbError>; - - /// list view only txos for a view only address - /// - /// Returns: - /// * Vec - fn list_for_address( - assigned_subaddress_b58: &str, - token_id: Option, - conn: &Conn, - ) -> Result, WalletDbError>; - - /// list view only txos that are unspent with key images for an account - fn list_unspent_with_key_images( - account_id_hex: &str, - token_id: Option, - conn: &Conn, - ) -> Result, WalletDbError>; - - fn list_orphaned_with_key_images( - account_id_hex: &str, - token_id: Option, - conn: &Conn, - ) -> Result, WalletDbError>; - - fn list_orphaned( - account_id_hex: &str, - token_id: Option, - conn: &Conn, - ) -> Result, WalletDbError>; - - fn list_unspent( - account_id_hex: &str, - assigned_subaddress_b58: Option<&str>, - token_id: Option, - conn: &Conn, - ) -> Result, WalletDbError>; - - fn list_pending( - account_id_hex: &str, - assigned_subaddress_b58: Option<&str>, - token_id: Option, - conn: &Conn, - ) -> Result, WalletDbError>; - - fn list_spent( - account_id_hex: &str, - assigned_subaddress_b58: Option<&str>, - token_id: Option, - conn: &Conn, - ) -> Result, WalletDbError>; - - /// Select a set of unspent view only Txos to reach a given value. - /// - /// Returns: - /// * Vec - fn select_unspent_view_only_txos_for_value( - account_id_hex: &str, - target_value: u64, - token_id: Option, - conn: &Conn, - ) -> Result, WalletDbError>; - - /// get all txouts with no key image or subaddress index for a given account - /// - /// Returns: - /// * Vec - fn export_txouts_without_key_image_or_subaddress_index( - account_id_hex: &str, - conn: &Conn, - ) -> Result, WalletDbError>; - - /// updates the key image for a given txo - /// - /// Returns: - /// * ViewOnlyTxo - fn update_key_image( - txo_id_hex: &str, - key_image: &KeyImage, - conn: &Conn, - ) -> Result<(), WalletDbError>; - - /// updates the spent block index for a given view only txo - fn update_spent_block_index( - txo_id_hex: &str, - spent_block_index: u64, - conn: &Conn, - ) -> Result<(), WalletDbError>; - - fn update_subaddress_index( - &self, - subaddress_index: u64, - conn: &Conn, - ) -> Result<(), WalletDbError>; - - fn update_for_pending_transaction( - txo_id_hex: &str, - subaddress_index: u64, - key_image: &KeyImage, - submitted_block_index: u64, - pending_tombstone_block_index: u64, - conn: &Conn, - ) -> Result<(), WalletDbError>; - - fn release_txos_with_expired_pending_tombstone_block_index( - account_id_hex: &str, - block_index: u64, - conn: &Conn, - ) -> Result<(), WalletDbError>; - - /// delete all view only txos for a view-only account - fn delete_all_for_account(account_id_hex: &str, conn: &Conn) -> Result<(), WalletDbError>; -} - -impl ViewOnlyTxoModel for ViewOnlyTxo { - // TODO: This needs to be updated for the new schema. - fn create( - tx_out: TxOut, - amount: Amount, - subaddress_index: Option, - received_block_index: Option, - view_only_account_id_hex: &str, - conn: &Conn, - ) -> Result { - use schema::view_only_txos; - - // Verify that the account exists. - ViewOnlyAccount::get(view_only_account_id_hex, conn)?; - - let txo_id = TxoID::from(&tx_out); - - let new_txo = NewViewOnlyTxo { - txo: &mc_util_serial::encode(&tx_out), - txo_id_hex: &txo_id.to_string(), - key_image: None, - value: amount.value as i64, - token_id: *amount.token_id as i64, - public_key: &mc_util_serial::encode(&tx_out.public_key), - view_only_account_id_hex, - subaddress_index: subaddress_index.map(|x| x as i64), - submitted_block_index: None, - pending_tombstone_block_index: None, - received_block_index: received_block_index.map(|x| x as i64), - spent_block_index: None, - }; - - diesel::insert_into(view_only_txos::table) - .values(&new_txo) - .execute(conn)?; - - ViewOnlyTxo::get(&txo_id.to_string(), conn) - } - - fn get(txo_id_hex: &str, conn: &Conn) -> Result { - use schema::view_only_txos; - - let txo = match view_only_txos::table - .filter(view_only_txos::txo_id_hex.eq(txo_id_hex)) - .get_result::(conn) - { - Ok(t) => t, - Err(diesel::result::Error::NotFound) => { - return Err(WalletDbError::TxoNotFound(txo_id_hex.to_string())); - } - Err(e) => { - return Err(e.into()); - } - }; - - Ok(txo) - } - - fn list_for_account( - account_id_hex: &str, - offset: Option, - limit: Option, - token_id: Option, - conn: &Conn, - ) -> Result, WalletDbError> { - use schema::view_only_txos; - - let mut query = view_only_txos::table.into_boxed(); - - query = query.filter(view_only_txos::view_only_account_id_hex.eq(account_id_hex)); - - if let (Some(o), Some(l)) = (offset, limit) { - query = query.offset(o as i64).limit(l as i64); - } - - if let Some(token_id) = token_id { - query = query.filter(view_only_txos::token_id.eq(token_id as i64)); - } - - Ok(query.load(conn)?) - } - - fn list_for_address( - assigned_subaddress_b58: &str, - token_id: Option, - conn: &Conn, - ) -> Result, WalletDbError> { - use schema::view_only_txos; - - let mut query = view_only_txos::table.into_boxed(); - - let subaddress = ViewOnlySubaddress::get(assigned_subaddress_b58, conn)?; - query = query - .filter(view_only_txos::subaddress_index.eq(subaddress.subaddress_index)) - .filter( - view_only_txos::view_only_account_id_hex.eq(subaddress.view_only_account_id_hex), - ); - - if let Some(token_id) = token_id { - query = query.filter(view_only_txos::token_id.eq(token_id as i64)); - } - - Ok(query.load(conn)?) - } - - fn list_unspent_with_key_images( - account_id_hex: &str, - token_id: Option, - conn: &Conn, - ) -> Result, WalletDbError> { - use schema::view_only_txos; - - let mut query = view_only_txos::table.into_boxed(); - - query = query - .filter(view_only_txos::view_only_account_id_hex.eq(account_id_hex)) - .filter(view_only_txos::key_image.is_not_null()) - .filter(view_only_txos::subaddress_index.is_not_null()) - .filter(view_only_txos::received_block_index.is_not_null()) - .filter(view_only_txos::spent_block_index.is_null()); - - if let Some(token_id) = token_id { - query = query.filter(view_only_txos::token_id.eq(token_id as i64)); - } - - let results: Vec<(Option>, String)> = query - .select((view_only_txos::key_image, view_only_txos::txo_id_hex)) - .load(conn)?; - - Ok(results - .into_iter() - .filter_map(|(key_image, txo_id_hex)| match key_image { - Some(key_image_encoded) => { - let key_image = mc_util_serial::decode(key_image_encoded.as_slice()).ok()?; - Some((key_image, txo_id_hex)) - } - None => None, - }) - .collect()) - } - - fn list_orphaned_with_key_images( - account_id_hex: &str, - token_id: Option, - conn: &Conn, - ) -> Result, WalletDbError> { - use schema::view_only_txos; - - let mut query = view_only_txos::table.into_boxed(); - - query = query - .filter(view_only_txos::view_only_account_id_hex.eq(account_id_hex)) - .filter(view_only_txos::key_image.is_not_null()) - .filter(view_only_txos::subaddress_index.is_not_null()) - .filter(view_only_txos::received_block_index.is_not_null()) - .filter(view_only_txos::spent_block_index.is_null()); - - if let Some(token_id) = token_id { - query = query.filter(view_only_txos::token_id.eq(token_id as i64)); - } - - Ok(query.load(conn)?) - } - - fn list_orphaned( - account_id_hex: &str, - token_id: Option, - conn: &Conn, - ) -> Result, WalletDbError> { - use schema::view_only_txos; - - let mut query = view_only_txos::table.into_boxed(); - - query = query - .filter(view_only_txos::view_only_account_id_hex.eq(account_id_hex)) - .filter(view_only_txos::key_image.is_null()) - .filter(view_only_txos::subaddress_index.is_null()); - - if let Some(token_id) = token_id { - query = query.filter(view_only_txos::token_id.eq(token_id as i64)); - } - - Ok(query.load(conn)?) - } - - fn list_unspent( - account_id_hex: &str, - assigned_subaddress_b58: Option<&str>, - token_id: Option, - conn: &Conn, - ) -> Result, WalletDbError> { - use schema::view_only_txos; - - let mut query = view_only_txos::table.into_boxed(); - - query = query - .filter(view_only_txos::view_only_account_id_hex.eq(account_id_hex)) - .filter(view_only_txos::received_block_index.is_not_null()) - .filter(view_only_txos::pending_tombstone_block_index.is_null()) - .filter(view_only_txos::spent_block_index.is_null()); - - if let Some(assigned_subaddress_b58) = assigned_subaddress_b58 { - let subaddress = ViewOnlySubaddress::get(assigned_subaddress_b58, conn)?; - query = query.filter(view_only_txos::subaddress_index.eq(subaddress.subaddress_index)); - } - - if let Some(token_id) = token_id { - query = query.filter(view_only_txos::token_id.eq(token_id as i64)); - } - - Ok(query.load(conn)?) - } - - fn list_pending( - account_id_hex: &str, - assigned_subaddress_b58: Option<&str>, - token_id: Option, - conn: &Conn, - ) -> Result, WalletDbError> { - use schema::view_only_txos; - - let mut query = view_only_txos::table.into_boxed(); - - query = query - .filter(view_only_txos::view_only_account_id_hex.eq(account_id_hex)) - .filter(view_only_txos::pending_tombstone_block_index.is_not_null()) - .filter(view_only_txos::spent_block_index.is_null()); - - if let Some(assigned_subaddress_b58) = assigned_subaddress_b58 { - let subaddress = ViewOnlySubaddress::get(assigned_subaddress_b58, conn)?; - query = query.filter(view_only_txos::subaddress_index.eq(subaddress.subaddress_index)); - } - - if let Some(token_id) = token_id { - query = query.filter(view_only_txos::token_id.eq(token_id as i64)); - } - - Ok(query.load(conn)?) - } - - fn list_spent( - account_id_hex: &str, - assigned_subaddress_b58: Option<&str>, - token_id: Option, - conn: &Conn, - ) -> Result, WalletDbError> { - use schema::view_only_txos; - - let mut query = view_only_txos::table.into_boxed(); - - query = query - .filter(view_only_txos::view_only_account_id_hex.eq(account_id_hex)) - .filter(view_only_txos::spent_block_index.is_not_null()); - - if let Some(assigned_subaddress_b58) = assigned_subaddress_b58 { - let subaddress = ViewOnlySubaddress::get(assigned_subaddress_b58, conn)?; - query = query.filter(view_only_txos::subaddress_index.eq(subaddress.subaddress_index)); - } - - if let Some(token_id) = token_id { - query = query.filter(view_only_txos::token_id.eq(token_id as i64)); - } - - Ok(query.load(conn)?) - } - - // This is a direct port of txo selection and - // the whole things needs a nice big refactor - // to make it happy. - fn select_unspent_view_only_txos_for_value( - account_id_hex: &str, - target_value: u64, - token_id: Option, - conn: &Conn, - ) -> Result, WalletDbError> { - use schema::view_only_txos; - - let mut query = view_only_txos::table.into_boxed(); - - query = query - .filter(view_only_txos::view_only_account_id_hex.eq(account_id_hex)) - .filter(view_only_txos::subaddress_index.is_not_null()) - .filter(view_only_txos::received_block_index.is_not_null()) - .filter(view_only_txos::pending_tombstone_block_index.is_null()) - .filter(view_only_txos::spent_block_index.is_null()); - - if let Some(token_id) = token_id { - query = query.filter(view_only_txos::token_id.eq(token_id as i64)); - } - - let mut spendable_txos: Vec = - query.order_by(view_only_txos::value.desc()).load(conn)?; - - if spendable_txos.is_empty() { - return Err(WalletDbError::NoSpendableTxos); - } - - let max_spendable_in_wallet: u128 = spendable_txos - .iter() - .take(MAX_INPUTS as usize) - .map(|utxo| (utxo.value as u64) as u128) - .sum(); - - if target_value as u128 > max_spendable_in_wallet { - // See if we merged the UTXOs we would be able to spend this amount. - let total_unspent_value_in_wallet: u128 = spendable_txos - .iter() - .map(|utxo| (utxo.value as u64) as u128) - .sum(); - if total_unspent_value_in_wallet >= target_value as u128 { - return Err(WalletDbError::InsufficientFundsFragmentedTxos); - } else { - return Err(WalletDbError::InsufficientFundsUnderMaxSpendable(format!( - "Max spendable value in wallet: {:?}, but target value: {:?}", - max_spendable_in_wallet, target_value - ))); - } - } - - let mut selected_utxos: Vec = Vec::new(); - let mut total: u64 = 0; - loop { - if total >= target_value { - break; - } - - // Grab the next (smallest) utxo, in order to opportunistically sweep up dust - let next_utxo = spendable_txos.pop().ok_or_else(|| { - WalletDbError::InsufficientFunds(format!( - "Not enough Txos to sum to target value: {:?}", - target_value - )) - })?; - selected_utxos.push(next_utxo.clone()); - total += next_utxo.value as u64; - - // Cap at maximum allowed inputs. - if selected_utxos.len() > MAX_INPUTS as usize { - // Remove the lowest utxo. - let removed = selected_utxos.remove(0); - total -= removed.value as u64; - } - } - - if selected_utxos.is_empty() || selected_utxos.len() > MAX_INPUTS as usize { - return Err(WalletDbError::InsufficientFunds( - "Logic error. Could not select Txos despite having sufficient funds".to_string(), - )); - } - - Ok(selected_utxos) - } - - fn update_key_image( - txo_id_hex: &str, - key_image: &KeyImage, - conn: &Conn, - ) -> Result<(), WalletDbError> { - use schema::view_only_txos::dsl::{ - key_image as dsl_key_image, txo_id_hex as dsl_txo_id, view_only_txos, - }; - - // assert txo exists - ViewOnlyTxo::get(txo_id_hex, conn)?; - - diesel::update(view_only_txos.filter(dsl_txo_id.eq(txo_id_hex))) - .set(dsl_key_image.eq(mc_util_serial::encode(key_image))) - .execute(conn)?; - Ok(()) - } - - fn update_spent_block_index( - txo_id_hex: &str, - spent_block_index: u64, - conn: &Conn, - ) -> Result<(), WalletDbError> { - use schema::view_only_txos; - - diesel::update(view_only_txos::table.filter(view_only_txos::txo_id_hex.eq(txo_id_hex))) - .set((view_only_txos::spent_block_index.eq(spent_block_index as i64),)) - .execute(conn)?; - Ok(()) - } - - fn update_subaddress_index( - &self, - subaddress_index: u64, - conn: &Conn, - ) -> Result<(), WalletDbError> { - use schema::view_only_txos; - - diesel::update( - view_only_txos::table.filter(view_only_txos::txo_id_hex.eq(&self.txo_id_hex)), - ) - .set((view_only_txos::subaddress_index.eq(subaddress_index as i64),)) - .execute(conn)?; - - Ok(()) - } - - fn update_for_pending_transaction( - txo_id_hex: &str, - subaddress_index: u64, - key_image: &KeyImage, - submitted_block_index: u64, - pending_tombstone_block_index: u64, - conn: &Conn, - ) -> Result<(), WalletDbError> { - use schema::view_only_txos::dsl::{ - key_image as dsl_key_image, - pending_tombstone_block_index as dsl_pending_tombstone_block_index, - subaddress_index as dsl_subaddress_index, - submitted_block_index as dsl_submitted_block_index, txo_id_hex as dsl_txo_id_hex, - }; - - diesel::update( - schema::view_only_txos::table.filter(dsl_txo_id_hex.eq(txo_id_hex.to_string())), - ) - .set(( - dsl_subaddress_index.eq(subaddress_index as i64), - dsl_key_image.eq(mc_util_serial::encode(key_image)), - dsl_submitted_block_index.eq(submitted_block_index as i64), - dsl_pending_tombstone_block_index.eq(pending_tombstone_block_index as i64), - )) - .execute(conn)?; - - Ok(()) - } - - fn export_txouts_without_key_image_or_subaddress_index( - account_id_hex: &str, - conn: &Conn, - ) -> Result, WalletDbError> { - use schema::view_only_txos::dsl::{ - key_image as dsl_key_image, subaddress_index as dsl_subaddress_index, token_id, - view_only_account_id_hex as dsl_account_id, - }; - - let txos: Vec = schema::view_only_txos::table - .filter(dsl_account_id.eq(account_id_hex)) - .filter(dsl_key_image.is_null().or(dsl_subaddress_index.is_null())) - .filter(token_id.eq(0)) - .load(conn)?; - - let mut txouts: Vec = Vec::new(); - - for txo in txos { - let txout: TxOut = mc_util_serial::decode(&txo.txo)?; - txouts.push(txout); - } - - Ok(txouts) - } - - fn release_txos_with_expired_pending_tombstone_block_index( - account_id_hex: &str, - block_index: u64, - conn: &Conn, - ) -> Result<(), WalletDbError> { - use schema::view_only_txos::dsl::{ - pending_tombstone_block_index as dsl_pending_tombstone_block_index, - spent_block_index as dsl_spent_block_index, - submitted_block_index as dsl_submitted_block_index, - view_only_account_id_hex as dsl_account_id, - }; - - diesel::update( - schema::view_only_txos::table - .filter(dsl_account_id.eq(account_id_hex)) - .filter(dsl_pending_tombstone_block_index.le(block_index as i64)) - .filter(dsl_spent_block_index.is_null()), - ) - .set(( - dsl_pending_tombstone_block_index.eq::>(None), - dsl_submitted_block_index.eq::>(None), - )) - .execute(conn)?; - - Ok(()) - } - - fn delete_all_for_account(account_id_hex: &str, conn: &Conn) -> Result<(), WalletDbError> { - use schema::view_only_txos::dsl::{ - view_only_account_id_hex as dsl_account_id, view_only_txos, - }; - - diesel::delete(view_only_txos.filter(dsl_account_id.eq(account_id_hex))).execute(conn)?; - Ok(()) - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::test_utils::WalletDbTestContext; - - use crate::db::models::ViewOnlyAccount; - - use mc_account_keys::{PublicAddress, CHANGE_SUBADDRESS_INDEX, DEFAULT_SUBADDRESS_INDEX}; - use mc_common::logger::{test_with_logger, Logger}; - use mc_crypto_keys::{RistrettoPrivate, RistrettoPublic}; - use mc_transaction_core::{encrypted_fog_hint::EncryptedFogHint, tokens::Mob, Amount, Token}; - use mc_util_from_random::FromRandom; - use rand::{rngs::StdRng, SeedableRng}; - - #[test_with_logger] - fn test_view_only_txo_crud(logger: Logger) { - let mut rng: StdRng = SeedableRng::from_seed([20u8; 32]); - let db_test_context = WalletDbTestContext::default(); - let wallet_db = db_test_context.get_db_instance(logger); - let conn = wallet_db.get_conn().unwrap(); - - // make fake txo - let value = 420; - let amount = Amount::new(value, Mob::ID); - let tx_private_key = RistrettoPrivate::from_random(&mut rng); - let hint = EncryptedFogHint::fake_onetime_hint(&mut rng); - let public_address = PublicAddress::new( - &RistrettoPublic::from_random(&mut rng), - &RistrettoPublic::from_random(&mut rng), - ); - let fake_tx_out = TxOut::new(amount, &public_address, &tx_private_key, hint).unwrap(); - - // make sure it fails if no matching account - - let view_only_account_id = "accountId"; - - let err = ViewOnlyTxo::create( - fake_tx_out.clone(), - amount, - None, - None, - view_only_account_id, - &conn, - ); - - assert!(err.is_err()); - - // make sure it passes with a matching account - - let view_only_account = ViewOnlyAccount::create( - view_only_account_id, - &RistrettoPrivate::from_random(&mut rng), - 0, - 0, - DEFAULT_SUBADDRESS_INDEX, - CHANGE_SUBADDRESS_INDEX, - 2, - "catcoin_name", - &conn, - ) - .unwrap(); - - let txo_id = TxoID::from(&fake_tx_out); - let expected = ViewOnlyTxo { - id: 1, - txo_id_hex: txo_id.to_string(), - view_only_account_id_hex: view_only_account.account_id_hex.to_string(), - txo: mc_util_serial::encode(&fake_tx_out), - key_image: None, - public_key: mc_util_serial::encode(&fake_tx_out.public_key), - value: value as i64, - token_id: 0, - subaddress_index: Some(DEFAULT_SUBADDRESS_INDEX as i64), - submitted_block_index: None, - pending_tombstone_block_index: None, - received_block_index: Some(1), - spent_block_index: None, - }; - - let created = ViewOnlyTxo::create( - fake_tx_out.clone(), - amount, - Some(DEFAULT_SUBADDRESS_INDEX), - Some(1), - &view_only_account.account_id_hex, - &conn, - ) - .unwrap(); - - assert_eq!(expected, created); - - // test marking as spent - ViewOnlyTxo::update_spent_block_index(&txo_id.to_string(), 2, &conn).unwrap(); - let updated = ViewOnlyTxo::get(&txo_id.to_string(), &conn).unwrap(); - assert_eq!(updated.spent_block_index, Some(2)); - } -} diff --git a/full-service/src/db/wallet_db.rs b/full-service/src/db/wallet_db.rs index 07207d96a..a9b4777e5 100644 --- a/full-service/src/db/wallet_db.rs +++ b/full-service/src/db/wallet_db.rs @@ -1,4 +1,8 @@ -use crate::db::WalletDbError; +use crate::db::{ + models::{AssignedSubaddress, Migration, NewMigration}, + schema::{__diesel_schema_migrations, assigned_subaddresses}, + WalletDbError, +}; use diesel::{ connection::SimpleConnection, prelude::*, @@ -7,6 +11,7 @@ use diesel::{ }; use diesel_migrations::embed_migrations; use mc_common::logger::global_log; +use mc_crypto_keys::RistrettoPublic; use std::{env, thread::sleep, time::Duration}; embed_migrations!("migrations/"); @@ -138,6 +143,44 @@ impl WalletDb { } pub fn run_migrations(conn: &SqliteConnection) { + // check for and retroactively insert any missing migrations if there is a later + // migration without the prior ones. + // We need to perform this first check in case this is a fresh database, in + // which case there will be no migrations table. + if let Ok(migrations) = __diesel_schema_migrations::table.load::(conn) { + global_log::info!("Number of migrations applied: {:?}", migrations.len()); + + if migrations.len() == 1 && migrations[0].version == "20220613204000" { + global_log::info!("Retroactively inserting missing migrations"); + let missing_migrations = vec![ + NewMigration::new("20202109165203"), + NewMigration::new("20210303035127"), + NewMigration::new("20210307192850"), + NewMigration::new("20210308031049"), + NewMigration::new("20210325042338"), + NewMigration::new("20210330021521"), + NewMigration::new("20210331220723"), + NewMigration::new("20210403183001"), + NewMigration::new("20210409050201"), + NewMigration::new("20210420182449"), + NewMigration::new("20210625225113"), + NewMigration::new("20211214005344"), + NewMigration::new("20220208225206"), + NewMigration::new("20220215200456"), + NewMigration::new("20220228190052"), + NewMigration::new("20220328194805"), + NewMigration::new("20220427170453"), + NewMigration::new("20220513170243"), + NewMigration::new("20220601162825"), + ]; + + diesel::insert_into(__diesel_schema_migrations::table) + .values(&missing_migrations) + .execute(conn) + .expect("failed inserting migration"); + } + } + // Our migrations sometimes violate foreign keys, so disable foreign key checks // while we apply them. // This has to happen outside the scope of a transaction. Quoting @@ -155,6 +198,54 @@ impl WalletDb { conn.batch_execute("PRAGMA foreign_keys = ON;") .expect("failed enabling foreign keys"); } + + pub fn run_proto_conversions_if_necessary(conn: &SqliteConnection) { + Self::run_assigned_subaddress_proto_conversions(conn); + } + + /// Prior to v2.0.0, the spend public key of a subaddress was stored as + /// protobuf bytes unnecessarily. This converts those to raw bytes instead, + /// which is what users will most likely be expecting. + /// + /// This is a one-time conversion, so we check if the conversion has already + /// happened, and if it has we do nothing. + fn run_assigned_subaddress_proto_conversions(conn: &SqliteConnection) { + global_log::info!("Checking for assigned subaddress proto conversions"); + let assigned_subaddresses = assigned_subaddresses::table + .load::(conn) + .expect("failed querying for assigned subaddresses"); + + for assigned_subaddress in assigned_subaddresses { + // Checking if the data is encoded as protobuf bytes, and if it is, we turn it + // into raw bytes instead. + // + // If the spend public key is already raw bytes, we can assume the rest of the + // subaddresses are too, so we can return early. + let spend_public_key_bytes = match mc_util_serial::decode::( + &assigned_subaddress.spend_public_key, + ) { + Ok(spend_public_key) => spend_public_key.to_bytes().to_vec(), + Err(_) => { + global_log::info!( + "Assigned subaddress proto conversion already done, skipping..." + ); + return; + } + }; + + diesel::update( + assigned_subaddresses::table.filter( + assigned_subaddresses::public_address_b58 + .eq(&assigned_subaddress.public_address_b58), + ), + ) + .set((assigned_subaddresses::spend_public_key.eq(&spend_public_key_bytes),)) + .execute(conn) + .expect("failed updating assigned subaddress"); + + global_log::info!("Assigned subaddress proto conversion done"); + } + } } /// Create an immediate SQLite transaction with retry. diff --git a/full-service/src/db/wallet_db_error.rs b/full-service/src/db/wallet_db_error.rs index 3de6e437b..647453025 100644 --- a/full-service/src/db/wallet_db_error.rs +++ b/full-service/src/db/wallet_db_error.rs @@ -6,6 +6,9 @@ use displaydoc::Display; #[derive(Display, Debug)] pub enum WalletDbError { + /// Wallet functions are currently disabled + WalletFunctionsDisabled, + /// View Only Account already exists: {0} ViewOnlyAccountAlreadyExists(String), @@ -137,6 +140,18 @@ pub enum WalletDbError { /// error converting keys KeyError(mc_crypto_keys::KeyError), + + /// invalid txo status + InvalidTxoStatus(String), + + /// Expected to find TxOut as an outlay + ExpectedTxOutAsOutlay, + + /// Expected to find a membership proof for txo with id: {0} + MissingTxoMembershipProof(String), + + /// Expected to find a key image for a txo with id: {0} + MissingKeyImageForInputTxo(String), } impl From for WalletDbError { diff --git a/full-service/src/error.rs b/full-service/src/error.rs index 3c0fd8a89..0c6f327fa 100644 --- a/full-service/src/error.rs +++ b/full-service/src/error.rs @@ -301,11 +301,29 @@ pub enum WalletTransactionBuilderError { /// Error with the b58 util: {0} B58(B58Error), - /// Error passed up from AmountError + /// Error passed up from AmountError: {0} AmountError(mc_transaction_core::AmountError), - /// Error passed up from KeyError + /// Error passed up from KeyError: {0} KeyError(mc_crypto_keys::KeyError), + + /// Transaction is missing inputs for outputs with token id {0} + MissingInputsForTokenId(String), + + /// Error decoding the hex string: {0} + FromHexError(hex::FromHexError), + + /// Burn Redemption Memo must be exactly 128 characters (64 bytes) long: {0} + InvalidBurnRedemptionMemo(String), + + /// Error converting a TxOut: {0} + TxOutConversion(mc_transaction_core::TxOutConversionError), + + /// RTH is currently unavailable for view only accounts. + RTHUnavailableForViewOnlyAccounts, + + /// Cannot use orphaned txo as an input: {0} + CannotUseOrphanedTxoAsInput(String), } impl From for WalletTransactionBuilderError { @@ -361,3 +379,15 @@ impl From for WalletTransactionBuilderError { Self::B58(src) } } + +impl From for WalletTransactionBuilderError { + fn from(src: hex::FromHexError) -> Self { + Self::FromHexError(src) + } +} + +impl From for WalletTransactionBuilderError { + fn from(src: mc_transaction_core::TxOutConversionError) -> Self { + Self::TxOutConversion(src) + } +} diff --git a/full-service/src/fog_resolver.rs b/full-service/src/fog_resolver.rs deleted file mode 100644 index 4afaba099..000000000 --- a/full-service/src/fog_resolver.rs +++ /dev/null @@ -1,56 +0,0 @@ -use mc_account_keys::PublicAddress; -use mc_common::HashMap; -use mc_crypto_keys::RistrettoPublic; -use mc_fog_report_validation::{FogPubkeyError, FogPubkeyResolver, FullyValidatedFogPubkey}; -use serde::{Deserialize, Serialize}; - -use crate::util::b58::b58_encode_public_address; - -use std::convert::TryFrom; - -#[derive(Clone, Debug, Deserialize, Serialize)] -pub struct FullServiceFogResolver(pub HashMap); - -impl FogPubkeyResolver for FullServiceFogResolver { - fn get_fog_pubkey( - &self, - address: &PublicAddress, - ) -> Result { - let b58_address = - b58_encode_public_address(address).map_err(|_| FogPubkeyError::NoFogReportUrl)?; - - let fs_fog_pubkey = match self.0.get(&b58_address) { - Some(pubkey) => Ok(pubkey.clone()), - None => Err(FogPubkeyError::NoFogReportUrl), - }?; - - FullyValidatedFogPubkey::try_from(fs_fog_pubkey) - } -} - -#[derive(Clone, Debug, Deserialize, Serialize)] -pub struct FullServiceFullyValidatedFogPubkey { - pub pubkey: [u8; 32], - pub pubkey_expiry: u64, -} - -impl From for FullServiceFullyValidatedFogPubkey { - fn from(fog_pubkey: FullyValidatedFogPubkey) -> Self { - Self { - pubkey: fog_pubkey.pubkey.to_bytes(), - pubkey_expiry: fog_pubkey.pubkey_expiry, - } - } -} - -impl TryFrom for FullyValidatedFogPubkey { - type Error = FogPubkeyError; - - fn try_from(fog_pubkey: FullServiceFullyValidatedFogPubkey) -> Result { - Ok(Self { - pubkey: RistrettoPublic::try_from(&fog_pubkey.pubkey) - .map_err(|_| FogPubkeyError::NoFogReportUrl)?, - pubkey_expiry: fog_pubkey.pubkey_expiry, - }) - } -} diff --git a/full-service/src/json_rpc/account_secrets.rs b/full-service/src/json_rpc/account_secrets.rs deleted file mode 100644 index 8a01d49d3..000000000 --- a/full-service/src/json_rpc/account_secrets.rs +++ /dev/null @@ -1,78 +0,0 @@ -// Copyright (c) 2020-2021 MobileCoin Inc. - -//! API definition for the Account Secrets object. - -use crate::{db::models::Account, json_rpc::account_key::AccountKey}; - -use bip39::{Language, Mnemonic}; -use serde_derive::{Deserialize, Serialize}; -use std::convert::TryFrom; - -/// The AccountSecrets contains the entropy and the account key derived from -/// that entropy. -#[derive(Deserialize, Serialize, Default, Debug, Clone)] -pub struct AccountSecrets { - /// String representing the object's type. Objects of the same type share - /// the same value. - pub object: String, - - /// The account ID for this account key in the wallet database. - pub account_id: String, - - /// The name of this account - pub name: String, - - /// The entropy from which this account key was derived, as a String - /// (version 1) - #[serde(skip_serializing_if = "Option::is_none")] - pub entropy: Option, - - /// The mnemonic from which this account key was derived, as a String - /// (version 2) - pub mnemonic: Option, - - /// The key derivation version that this mnemonic goes with - pub key_derivation_version: String, - - /// Private keys for receiving and spending MobileCoin. - pub account_key: AccountKey, -} - -impl TryFrom<&Account> for AccountSecrets { - type Error = String; - - fn try_from(src: &Account) -> Result { - let account_key: mc_account_keys::AccountKey = mc_util_serial::decode(&src.account_key) - .map_err(|err| format!("Could not decode account key from database: {:?}", err))?; - - let entropy = match src.key_derivation_version { - 1 => Some(hex::encode(&src.entropy)), - _ => None, - }; - - let mnemonic = match src.key_derivation_version { - 2 => Some( - Mnemonic::from_entropy(&src.entropy, Language::English) - .unwrap() - .phrase() - .to_string(), - ), - _ => None, - }; - - Ok(AccountSecrets { - object: "account_secrets".to_string(), - name: src.name.clone(), - account_id: src.account_id_hex.clone(), - entropy, - mnemonic, - key_derivation_version: src.key_derivation_version.to_string(), - account_key: AccountKey::try_from(&account_key).map_err(|err| { - format!( - "Could not convert account_key to json_rpc representation: {:?}", - err - ) - })?, - }) - } -} diff --git a/full-service/src/json_rpc/amount.rs b/full-service/src/json_rpc/amount.rs deleted file mode 100644 index 360809992..000000000 --- a/full-service/src/json_rpc/amount.rs +++ /dev/null @@ -1,73 +0,0 @@ -// Copyright (c) 2020-2021 MobileCoin Inc. - -//! API definition for the Account object. - -use mc_crypto_keys::ReprBytes; -use mc_transaction_core::CompressedCommitment; -use serde::{Deserialize, Serialize}; -use std::convert::TryFrom; - -/// The encrypted amount of pMOB in a Txo. -#[derive(Deserialize, Serialize, Default, Debug, Clone)] -pub struct MaskedAmount { - /// String representing the object's type. Objects of the same type share - /// the same value. - pub object: String, - - /// A Pedersen commitment `v*G + s*H` - pub commitment: String, - - /// The masked value of pMOB in a Txo. - /// - /// The private view key is required to decrypt the amount, via: - /// `masked_value = value XOR_8 Blake2B("value_mask" || shared_secret)` - pub masked_value: String, - - /// `masked_token_id = token_id XOR_8 Blake2B(token_id_mask | - /// shared_secret)` 8 bytes long when used, 0 bytes for older amounts - /// that don't have this. - pub masked_token_id: String, -} - -impl From<&mc_api::external::MaskedAmount> for MaskedAmount { - fn from(src: &mc_api::external::MaskedAmount) -> Self { - Self { - object: "amount".to_string(), - commitment: hex::encode(src.get_commitment().get_data()), - masked_value: src.get_masked_value().to_string(), - masked_token_id: hex::encode(&src.get_masked_token_id()), - } - } -} - -impl From<&mc_transaction_core::MaskedAmount> for MaskedAmount { - fn from(src: &mc_transaction_core::MaskedAmount) -> Self { - Self { - object: "amount".to_string(), - commitment: hex::encode(src.commitment.to_bytes()), - masked_value: src.masked_value.to_string(), - masked_token_id: hex::encode(&src.masked_token_id), - } - } -} - -impl TryFrom<&MaskedAmount> for mc_transaction_core::MaskedAmount { - type Error = String; - - fn try_from(src: &MaskedAmount) -> Result { - let mut commitment_bytes = [0u8; 32]; - commitment_bytes[0..32].copy_from_slice( - &hex::decode(&src.commitment) - .map_err(|err| format!("Could not decode hex for amount commitment: {:?}", err))?, - ); - Ok(Self { - commitment: CompressedCommitment::from(&commitment_bytes), - masked_value: src - .masked_value - .parse::() - .map_err(|err| format!("Could not parse masked value u64: {:?}", err))?, - masked_token_id: hex::decode(&src.masked_token_id) - .map_err(|err| format!("Could not decode hex for masked token id: {:?}", err))?, - }) - } -} diff --git a/full-service/src/json_rpc/e2e.rs b/full-service/src/json_rpc/e2e.rs deleted file mode 100644 index 2a7fabb81..000000000 --- a/full-service/src/json_rpc/e2e.rs +++ /dev/null @@ -1,4041 +0,0 @@ -// Copyright (c) 2020-2021 MobileCoin Inc. - -//! End-to-end tests for the Full Service Wallet API. - -#[cfg(test)] -mod e2e { - use crate::{ - db::{ - account::AccountID, - models::{TXO_STATUS_UNSPENT, TXO_TYPE_RECEIVED}, - }, - json_rpc, - json_rpc::api_test_utils::{ - dispatch, dispatch_expect_error, dispatch_with_header, - dispatch_with_header_expect_error, setup, setup_with_api_key, - }, - test_utils::{ - add_block_to_ledger_db, add_block_with_tx_proposal, manually_sync_account, - manually_sync_view_only_account, MOB, - }, - util::b58::b58_decode_public_address, - }; - use bip39::{Language, Mnemonic}; - use mc_account_keys::{AccountKey, RootEntropy, RootIdentity}; - use mc_account_keys_slip10::Slip10Key; - use mc_common::logger::{test_with_logger, Logger}; - use mc_crypto_rand::rand_core::RngCore; - use mc_ledger_db::Ledger; - use mc_transaction_core::{ring_signature::KeyImage, tokens::Mob, Token}; - use rand::{rngs::StdRng, SeedableRng}; - use rocket::http::{Header, Status}; - use std::convert::TryFrom; - - #[test_with_logger] - fn test_e2e_account_crud(logger: Logger) { - let mut rng: StdRng = SeedableRng::from_seed([20u8; 32]); - let (client, _ledger_db, _db_ctx, _network_state) = setup(&mut rng, logger.clone()); - - // Create Account - let body = json!({ - "jsonrpc": "2.0", - "id": 1, - "method": "create_account", - "params": { - "name": "Alice Main Account", - }, - }); - let res = dispatch(&client, body, &logger); - assert_eq!(res.get("jsonrpc").unwrap(), "2.0"); - - let result = res.get("result").unwrap(); - let account_obj = result.get("account").unwrap(); - assert!(account_obj.get("account_id").is_some()); - assert_eq!(account_obj.get("name").unwrap(), "Alice Main Account"); - assert!(account_obj.get("main_address").is_some()); - assert_eq!(account_obj.get("next_subaddress_index").unwrap(), "2"); - assert_eq!(account_obj.get("recovery_mode").unwrap(), false); - assert_eq!(account_obj.get("fog_enabled").unwrap(), false); - - let account_id = account_obj.get("account_id").unwrap(); - - // Read Accounts via Get All - let body = json!({ - "jsonrpc": "2.0", - "id": 2, - "method": "get_all_accounts", - }); - let res = dispatch(&client, body, &logger); - let result = res.get("result").unwrap(); - let accounts = result.get("account_ids").unwrap().as_array().unwrap(); - assert_eq!(accounts.len(), 1); - let account_map = result.get("account_map").unwrap().as_object().unwrap(); - assert_eq!( - account_map - .get(accounts[0].as_str().unwrap()) - .unwrap() - .get("account_id") - .unwrap(), - &account_id.clone() - ); - - let body = json!({ - "jsonrpc": "2.0", - "id": 2, - "method": "get_account", - "params": { - "account_id": *account_id, - } - }); - let res = dispatch(&client, body, &logger); - let result = res.get("result").unwrap(); - let name = result.get("account").unwrap().get("name").unwrap(); - assert_eq!("Alice Main Account", name.as_str().unwrap()); - - // FIXME: assert balance - - // Update Account - let body = json!({ - "jsonrpc": "2.0", - "id": 2, - "method": "update_account_name", - "params": { - "account_id": *account_id, - "name": "Eve Main Account", - } - }); - let res = dispatch(&client, body, &logger); - let result = res.get("result").unwrap(); - assert_eq!( - result.get("account").unwrap().get("name").unwrap(), - "Eve Main Account" - ); - - let body = json!({ - "jsonrpc": "2.0", - "id": 2, - "method": "get_account", - "params": { - "account_id": *account_id, - } - }); - let res = dispatch(&client, body, &logger); - let result = res.get("result").unwrap(); - let name = result.get("account").unwrap().get("name").unwrap(); - assert_eq!("Eve Main Account", name.as_str().unwrap()); - - // Remove Account - let body = json!({ - "jsonrpc": "2.0", - "id": 2, - "method": "remove_account", - "params": { - "account_id": *account_id, - } - }); - let res = dispatch(&client, body, &logger); - let result = res.get("result").unwrap(); - assert_eq!(result["removed"].as_bool().unwrap(), true,); - - let body = json!({ - "jsonrpc": "2.0", - "id": 2, - "method": "get_all_accounts", - }); - let res = dispatch(&client, body, &logger); - let result = res.get("result").unwrap(); - let accounts = result.get("account_ids").unwrap().as_array().unwrap(); - assert_eq!(accounts.len(), 0); - } - - #[test_with_logger] - fn test_e2e_create_account_with_fog(logger: Logger) { - let mut rng: StdRng = SeedableRng::from_seed([20u8; 32]); - let (client, _ledger_db, _db_ctx, _network_state) = setup(&mut rng, logger.clone()); - // Create Account - let body = json!({ - "jsonrpc": "2.0", - "id": 1, - "method": "create_account", - "params": { - "name": "Alice Main Account", - "fog_report_url": "fog://fog-report.example.com", - "fog_report_id": "", - "fog_authority_spki": "MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAvnB9wTbTOT5uoizRYaYbw7XIEkInl8E7MGOAQj+xnC+F1rIXiCnc/t1+5IIWjbRGhWzo7RAwI5sRajn2sT4rRn9NXbOzZMvIqE4hmhmEzy1YQNDnfALAWNQ+WBbYGW+Vqm3IlQvAFFjVN1YYIdYhbLjAPdkgeVsWfcLDforHn6rR3QBZYZIlSBQSKRMY/tywTxeTCvK2zWcS0kbbFPtBcVth7VFFVPAZXhPi9yy1AvnldO6n7KLiupVmojlEMtv4FQkk604nal+j/dOplTATV8a9AJBbPRBZ/yQg57EG2Y2MRiHOQifJx0S5VbNyMm9bkS8TD7Goi59aCW6OT1gyeotWwLg60JRZTfyJ7lYWBSOzh0OnaCytRpSWtNZ6barPUeOnftbnJtE8rFhF7M4F66et0LI/cuvXYecwVwykovEVBKRF4HOK9GgSm17mQMtzrD7c558TbaucOWabYR04uhdAc3s10MkuONWG0wIQhgIChYVAGnFLvSpp2/aQEq3xrRSETxsixUIjsZyWWROkuA0IFnc8d7AmcnUBvRW7FT/5thWyk5agdYUGZ+7C1o69ihR1YxmoGh69fLMPIEOhYh572+3ckgl2SaV4uo9Gvkz8MMGRBcMIMlRirSwhCfozV2RyT5Wn1NgPpyc8zJL7QdOhL7Qxb+5WjnCVrQYHI2cCAwEAAQ==" - }, - }); - - let res = dispatch(&client, body, &logger); - assert_eq!(res.get("jsonrpc").unwrap(), "2.0"); - - let result = res.get("result").unwrap(); - let account_obj = result.get("account").unwrap(); - assert!(account_obj.get("account_id").is_some()); - assert_eq!(account_obj.get("name").unwrap(), "Alice Main Account"); - assert_eq!(account_obj.get("recovery_mode").unwrap(), false); - assert!(account_obj.get("main_address").is_some()); - assert_eq!(account_obj.get("next_subaddress_index").unwrap(), "1"); - assert_eq!(account_obj.get("fog_enabled").unwrap(), true); - } - - #[test_with_logger] - fn test_e2e_import_account(logger: Logger) { - let mut rng: StdRng = SeedableRng::from_seed([20u8; 32]); - let (client, _ledger_db, _db_ctx, _network_state) = setup(&mut rng, logger.clone()); - - let body = json!({ - "jsonrpc": "2.0", - "id": 1, - "method": "import_account", - "params": { - "mnemonic": "sheriff odor square mistake huge skate mouse shoot purity weapon proof stuff correct concert blanket neck own shift clay mistake air viable stick group", - "key_derivation_version": "2", - "name": "Alice Main Account", - "first_block_index": "200", - } - }); - let res = dispatch(&client, body, &logger); - let result = res.get("result").unwrap(); - let account_obj = result.get("account").unwrap(); - let public_address = account_obj.get("main_address").unwrap().as_str().unwrap(); - assert_eq!(public_address, "3CnfxAc2LvKw4FDNRVgj3GndwAhgQDd7v2Cne66GTUJyzBr3WzSikk9nJ5sCAb1jgSSKaqpWQtcEjV1nhoadVKjq2Soa8p3XZy6u2tpHdor"); - let account_id = account_obj.get("account_id").unwrap().as_str().unwrap(); - assert_eq!( - account_id, - "7872edf0d4094643213aabc92aa0d07379cfb58eda0722b21a44868f22f75b4e" - ); - - assert_eq!( - *account_obj.get("first_block_index").unwrap(), - serde_json::json!("200") - ); - assert_eq!(account_obj.get("next_subaddress_index").unwrap(), "2"); - assert_eq!(account_obj.get("fog_enabled").unwrap(), false); - } - - #[test_with_logger] - fn test_e2e_import_account_unknown_version(logger: Logger) { - let mut rng: StdRng = SeedableRng::from_seed([20u8; 32]); - let (client, _ledger_db, _db_ctx, _network_state) = setup(&mut rng, logger.clone()); - - let body = json!({ - "jsonrpc": "2.0", - "id": 1, - "method": "import_account", - "params": { - "mnemonic": "sheriff odor square mistake huge skate mouse shoot purity weapon proof stuff correct concert blanket neck own shift clay mistake air viable stick group", - "key_derivation_version": "3", - "name": "", - } - }); - dispatch_expect_error( - &client, - body, - &logger, - json!({ - "method": "import_account", - "error": json!({ - "code": -32603, - "message": "InternalError", - "data": json!({ - "server_error": "UnknownKeyDerivation(3)", - "details": "Unknown key version version: 3", - }) - }), - "jsonrpc": "2.0", - "id": 1, - }) - .to_string(), - ); - } - - #[test_with_logger] - fn test_e2e_import_account_legacy(logger: Logger) { - let mut rng: StdRng = SeedableRng::from_seed([20u8; 32]); - let (client, _ledger_db, _db_ctx, _network_state) = setup(&mut rng, logger.clone()); - - let body = json!({ - "jsonrpc": "2.0", - "id": 1, - "method": "import_account_from_legacy_root_entropy", - "params": { - "entropy": "c593274dc6f6eb94242e34ae5f0ab16bc3085d45d49d9e18b8a8c6f057e6b56b", - "name": "Alice Main Account", - "first_block_index": "200", - } - }); - let res = dispatch(&client, body, &logger); - let result = res.get("result").unwrap(); - let account_obj = result.get("account").unwrap(); - let public_address = account_obj.get("main_address").unwrap().as_str().unwrap(); - assert_eq!(public_address, "8JtpPPh9mV2PTLrrDz4f2j4PtUpNWnrRg8HKpnuwkZbj5j8bGqtNMNLC9E3zjzcw456215yMjkCVYK4FPZTX4gijYHiuDT31biNHrHmQmsU"); - let account_id = account_obj.get("account_id").unwrap().as_str().unwrap(); - // Catches if a change results in changed accounts_ids, which should always be - // made to be backward compatible. - assert_eq!( - account_id, - "f9957a9d050ef8dff9d8ef6f66daa608081e631b2d918988311613343827b779" - ); - assert_eq!( - *account_obj.get("first_block_index").unwrap(), - serde_json::json!("200") - ); - assert_eq!(account_obj.get("next_subaddress_index").unwrap(), "2"); - assert_eq!(account_obj.get("fog_enabled").unwrap(), false); - } - - #[test_with_logger] - fn test_e2e_import_account_fog(logger: Logger) { - let mut rng: StdRng = SeedableRng::from_seed([20u8; 32]); - let (client, _ledger_db, _db_ctx, _network_state) = setup(&mut rng, logger.clone()); - - // Import an account with fog info. - let body = json!({ - "jsonrpc": "2.0", - "id": 1, - "method": "import_account", - "params": { - "mnemonic": "sheriff odor square mistake huge skate mouse shoot purity weapon proof stuff correct concert blanket neck own shift clay mistake air viable stick group", - "key_derivation_version": "2", - "name": "Alice Main Account", - "first_block_index": "200", - "fog_report_url": "fog://fog-report.example.com", - "fog_report_id": "", - "fog_authority_spki": "MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAvnB9wTbTOT5uoizRYaYbw7XIEkInl8E7MGOAQj+xnC+F1rIXiCnc/t1+5IIWjbRGhWzo7RAwI5sRajn2sT4rRn9NXbOzZMvIqE4hmhmEzy1YQNDnfALAWNQ+WBbYGW+Vqm3IlQvAFFjVN1YYIdYhbLjAPdkgeVsWfcLDforHn6rR3QBZYZIlSBQSKRMY/tywTxeTCvK2zWcS0kbbFPtBcVth7VFFVPAZXhPi9yy1AvnldO6n7KLiupVmojlEMtv4FQkk604nal+j/dOplTATV8a9AJBbPRBZ/yQg57EG2Y2MRiHOQifJx0S5VbNyMm9bkS8TD7Goi59aCW6OT1gyeotWwLg60JRZTfyJ7lYWBSOzh0OnaCytRpSWtNZ6barPUeOnftbnJtE8rFhF7M4F66et0LI/cuvXYecwVwykovEVBKRF4HOK9GgSm17mQMtzrD7c558TbaucOWabYR04uhdAc3s10MkuONWG0wIQhgIChYVAGnFLvSpp2/aQEq3xrRSETxsixUIjsZyWWROkuA0IFnc8d7AmcnUBvRW7FT/5thWyk5agdYUGZ+7C1o69ihR1YxmoGh69fLMPIEOhYh572+3ckgl2SaV4uo9Gvkz8MMGRBcMIMlRirSwhCfozV2RyT5Wn1NgPpyc8zJL7QdOhL7Qxb+5WjnCVrQYHI2cCAwEAAQ==" - } - }); - let res = dispatch(&client, body, &logger); - let result = res.get("result").unwrap(); - let account_obj = result.get("account").unwrap(); - let public_address = account_obj.get("main_address").unwrap().as_str().unwrap(); - assert_eq!(public_address, "2kD4vRp3DaBdRrNLNhJ5BKf5FsZxcAijoMt5pxjJpbk5jQRubngUXnd92vuXWkFyezuLgjCiKu4JHjpjNCnmzf1gAdW6PbqXsecQtp8Qr8uoeeDKrd1a5PtA6apXuDVtnrKsDCcHiJqdeSt3bRsPBvkBP4JqpGyAeKFsC7s2LQwuZ88BxFe2kyeZp5G3zENfvLaMripxTKkWGDopok2LCyA9NiCDf1vwjA5opLU7eqaRfh9"); - let account_id = account_obj.get("account_id").unwrap().as_str().unwrap(); - assert_eq!( - account_id, - "0b8a95253a7d57faf8510d8092ab55fb8610a9d691a7fa3bfafbf49945b845a2" - ); - - assert_eq!(account_obj.get("next_subaddress_index").unwrap(), "1"); - assert_eq!(account_obj.get("fog_enabled").unwrap(), true); - } - - #[test_with_logger] - fn test_e2e_import_account_legacy_fog(logger: Logger) { - let mut rng: StdRng = SeedableRng::from_seed([20u8; 32]); - let (client, _ledger_db, _db_ctx, _network_state) = setup(&mut rng, logger.clone()); - - let body = json!({ - "jsonrpc": "2.0", - "id": 1, - "method": "import_account_from_legacy_root_entropy", - "params": { - "entropy": "c593274dc6f6eb94242e34ae5f0ab16bc3085d45d49d9e18b8a8c6f057e6b56b", - "name": "Alice Main Account", - "first_block_index": "200", - "fog_report_url": "fog://fog-report.example.com", - "fog_report_id": "", - "fog_authority_spki": "MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAvnB9wTbTOT5uoizRYaYbw7XIEkInl8E7MGOAQj+xnC+F1rIXiCnc/t1+5IIWjbRGhWzo7RAwI5sRajn2sT4rRn9NXbOzZMvIqE4hmhmEzy1YQNDnfALAWNQ+WBbYGW+Vqm3IlQvAFFjVN1YYIdYhbLjAPdkgeVsWfcLDforHn6rR3QBZYZIlSBQSKRMY/tywTxeTCvK2zWcS0kbbFPtBcVth7VFFVPAZXhPi9yy1AvnldO6n7KLiupVmojlEMtv4FQkk604nal+j/dOplTATV8a9AJBbPRBZ/yQg57EG2Y2MRiHOQifJx0S5VbNyMm9bkS8TD7Goi59aCW6OT1gyeotWwLg60JRZTfyJ7lYWBSOzh0OnaCytRpSWtNZ6barPUeOnftbnJtE8rFhF7M4F66et0LI/cuvXYecwVwykovEVBKRF4HOK9GgSm17mQMtzrD7c558TbaucOWabYR04uhdAc3s10MkuONWG0wIQhgIChYVAGnFLvSpp2/aQEq3xrRSETxsixUIjsZyWWROkuA0IFnc8d7AmcnUBvRW7FT/5thWyk5agdYUGZ+7C1o69ihR1YxmoGh69fLMPIEOhYh572+3ckgl2SaV4uo9Gvkz8MMGRBcMIMlRirSwhCfozV2RyT5Wn1NgPpyc8zJL7QdOhL7Qxb+5WjnCVrQYHI2cCAwEAAQ==" - } - }); - let res = dispatch(&client, body, &logger); - let result = res.get("result").unwrap(); - let account_obj = result.get("account").unwrap(); - let public_address = account_obj.get("main_address").unwrap().as_str().unwrap(); - assert_eq!(public_address, "d3FhtyUQDYJFpEmzoXmRtF9VA5FTLycgQBKf1JEJJj8K6UXCuwzGD2uVYw1cxzZpbSivZLSxf9nZpMgUnuRxSpJA9qCDpDZd2qtc7j2N2x4758dQ91jrSCxzyuR1aJR7zgdcgdF2KwSShUhQ5n7M9uebf2HqiCWt8vttqESJ7aRNDwiW8TVmeKWviWunzYG46c8vo4DeZYK4wFfLNdwmeSn9HXKkQVpNgzsMz87cKpHRnzn"); - let account_id = account_obj.get("account_id").unwrap().as_str().unwrap(); - // Catches if a change results in changed accounts_ids, which should always be - // made to be backward compatible. - assert_eq!( - account_id, - "9111a17691a1eecb85bbeaa789c69471e7c8b9789e0068de02204f9d7264263d" - ); - assert_eq!(account_obj.get("next_subaddress_index").unwrap(), "1"); - assert_eq!(account_obj.get("fog_enabled").unwrap(), true); - } - - #[test_with_logger] - fn test_e2e_import_delete_import(logger: Logger) { - let mut rng: StdRng = SeedableRng::from_seed([20u8; 32]); - let (client, _ledger_db, _db_ctx, _network_state) = setup(&mut rng, logger.clone()); - - let body = json!({ - "jsonrpc": "2.0", - "id": 1, - "method": "import_account_from_legacy_root_entropy", - "params": { - "entropy": "c593274dc6f6eb94242e34ae5f0ab16bc3085d45d49d9e18b8a8c6f057e6b56b", - "name": "Alice Main Account", - "first_block_index": "200", - } - }); - let res = dispatch(&client, body, &logger); - let result = res.get("result").unwrap(); - let account_obj = result.get("account").unwrap(); - let public_address = account_obj.get("main_address").unwrap().as_str().unwrap(); - assert_eq!(public_address, "8JtpPPh9mV2PTLrrDz4f2j4PtUpNWnrRg8HKpnuwkZbj5j8bGqtNMNLC9E3zjzcw456215yMjkCVYK4FPZTX4gijYHiuDT31biNHrHmQmsU"); - let account_id = account_obj.get("account_id").unwrap().as_str().unwrap(); - // Catches if a change results in changed accounts_ids, which should always be - // made to be backward compatible. - assert_eq!( - account_id, - "f9957a9d050ef8dff9d8ef6f66daa608081e631b2d918988311613343827b779" - ); - - // Delete Account - let body = json!({ - "jsonrpc": "2.0", - "id": 2, - "method": "remove_account", - "params": { - "account_id": *account_id, - } - }); - let res = dispatch(&client, body, &logger); - let result = res.get("result").unwrap(); - assert_eq!(result["removed"].as_bool().unwrap(), true); - - // Import it again - should succeed. - let body = json!({ - "jsonrpc": "2.0", - "id": 1, - "method": "import_account_from_legacy_root_entropy", - "params": { - "entropy": "c593274dc6f6eb94242e34ae5f0ab16bc3085d45d49d9e18b8a8c6f057e6b56b", - "name": "Alice Main Account", - "first_block_index": "200", - } - }); - let res = dispatch(&client, body, &logger); - let result = res.get("result").unwrap(); - let account_obj = result.get("account").unwrap(); - let public_address = account_obj.get("main_address").unwrap().as_str().unwrap(); - assert_eq!(public_address, "8JtpPPh9mV2PTLrrDz4f2j4PtUpNWnrRg8HKpnuwkZbj5j8bGqtNMNLC9E3zjzcw456215yMjkCVYK4FPZTX4gijYHiuDT31biNHrHmQmsU"); - } - - #[test_with_logger] - fn test_export_account_secrets(logger: Logger) { - let mut rng: StdRng = SeedableRng::from_seed([20u8; 32]); - let (client, _ledger_db, _db_ctx, _network_state) = setup(&mut rng, logger.clone()); - - let body = json!({ - "jsonrpc": "2.0", - "id": 1, - "method": "import_account", - "params": { - "mnemonic": "sheriff odor square mistake huge skate mouse shoot purity weapon proof stuff correct concert blanket neck own shift clay mistake air viable stick group", - "key_derivation_version": "2", - "name": "Alice Main Account", - "first_block_index": "200", - } - }); - let res = dispatch(&client, body, &logger); - let account_obj = res["result"]["account"].clone(); - let account_id = account_obj["account_id"].clone(); - - let body = json!({ - "jsonrpc": "2.0", - "id": 1, - "method": "export_account_secrets", - "params": { - "account_id": account_id, - } - }); - let res = dispatch(&client, body, &logger); - let result = res.get("result").unwrap(); - let secrets = result.get("account_secrets").unwrap(); - let phrase = secrets["mnemonic"].as_str().unwrap(); - assert_eq!(secrets["account_id"], serde_json::json!(account_id)); - assert_eq!(secrets["key_derivation_version"], serde_json::json!("2")); - - // Test that the mnemonic serializes correctly back to an AccountKey object - let mnemonic = Mnemonic::from_phrase(phrase, Language::English).unwrap(); - let account_key = Slip10Key::from(mnemonic.clone()) - .try_into_account_key( - &"".to_string(), - &"".to_string(), - &hex::decode("".to_string()).expect("invalid spki"), - ) - .unwrap(); - - assert_eq!( - serde_json::json!(json_rpc::account_key::AccountKey::try_from(&account_key).unwrap()), - secrets["account_key"] - ); - } - - #[test_with_logger] - fn test_export_legacy_account_secrets(logger: Logger) { - let mut rng: StdRng = SeedableRng::from_seed([20u8; 32]); - let (client, _ledger_db, _db_ctx, _network_state) = setup(&mut rng, logger.clone()); - - let entropy = "c593274dc6f6eb94242e34ae5f0ab16bc3085d45d49d9e18b8a8c6f057e6b56b"; - let body = json!({ - "jsonrpc": "2.0", - "id": 1, - "method": "import_account_from_legacy_root_entropy", - "params": { - "entropy": entropy, - "name": "Alice Main Account", - "first_block_index": "200", - } - }); - let res = dispatch(&client, body, &logger); - let result = res.get("result").unwrap(); - let account_obj = result.get("account").unwrap(); - let account_id = account_obj.get("account_id").unwrap().as_str().unwrap(); - - let body = json!({ - "jsonrpc": "2.0", - "id": 1, - "method": "export_account_secrets", - "params": { - "account_id": account_id, - } - }); - let res = dispatch(&client, body, &logger); - let result = res.get("result").unwrap(); - let secrets = result.get("account_secrets").unwrap(); - - assert_eq!(secrets["account_id"], serde_json::json!(account_id)); - assert_eq!(secrets["entropy"], serde_json::json!(entropy)); - assert_eq!(secrets["key_derivation_version"], serde_json::json!("1")); - - // Test that the account_key serializes correctly back to an AccountKey object - let mut entropy_slice = [0u8; 32]; - entropy_slice[0..32].copy_from_slice(&hex::decode(&entropy).unwrap().as_slice()); - let account_key = AccountKey::from(&RootIdentity::from(&RootEntropy::from(&entropy_slice))); - assert_eq!( - serde_json::json!(json_rpc::account_key::AccountKey::try_from(&account_key).unwrap()), - secrets["account_key"] - ); - } - - #[test_with_logger] - fn test_e2e_get_balance(logger: Logger) { - let mut rng: StdRng = SeedableRng::from_seed([20u8; 32]); - let (client, mut ledger_db, db_ctx, _network_state) = setup(&mut rng, logger.clone()); - - // Add an account - let body = json!({ - "jsonrpc": "2.0", - "id": 1, - "method": "create_account", - "params": { - "name": "Alice Main Account", - } - }); - let res = dispatch(&client, body, &logger); - let result = res.get("result").unwrap(); - let account_obj = result.get("account").unwrap(); - let account_id = account_obj.get("account_id").unwrap().as_str().unwrap(); - let b58_public_address = account_obj.get("main_address").unwrap().as_str().unwrap(); - let public_address = b58_decode_public_address(b58_public_address).unwrap(); - - // Add a block with a txo for this address - add_block_to_ledger_db( - &mut ledger_db, - &vec![public_address], - 42 * MOB, - &vec![KeyImage::from(rng.next_u64())], - &mut rng, - ); - - manually_sync_account( - &ledger_db, - &db_ctx.get_db_instance(logger.clone()), - &AccountID(account_id.to_string()), - &logger, - ); - - let body = json!({ - "jsonrpc": "2.0", - "id": 1, - "method": "get_balance_for_account", - "params": { - "account_id": account_id, - } - }); - let res = dispatch(&client, body, &logger); - let result = res.get("result").unwrap(); - let balance = result.get("balance").unwrap(); - assert_eq!( - balance - .get("unspent_pmob") - .unwrap() - .as_str() - .unwrap() - .to_string(), - (42 * MOB).to_string() - ); - assert_eq!( - balance - .get("max_spendable_pmob") - .unwrap() - .as_str() - .unwrap() - .to_string(), - (42 * MOB - Mob::MINIMUM_FEE).to_string() - ); - } - - #[test_with_logger] - fn test_wallet_status(logger: Logger) { - let mut rng: StdRng = SeedableRng::from_seed([20u8; 32]); - let (client, _ledger_db, _db_ctx, _network_state) = setup(&mut rng, logger.clone()); - - let body = json!({ - "jsonrpc": "2.0", - "id": 1, - "method": "create_account", - "params": { - "name": "Alice Main Account", - } - }); - let _result = dispatch(&client, body, &logger).get("result").unwrap(); - - let body = json!({ - "jsonrpc": "2.0", - "id": 1, - "method": "get_wallet_status", - }); - let res = dispatch(&client, body, &logger); - let result = res.get("result").unwrap(); - let status = result.get("wallet_status").unwrap(); - assert_eq!(status.get("network_block_height").unwrap(), "12"); - assert_eq!(status.get("local_block_height").unwrap(), "12"); - // Syncing will have already started, so we can't determine what the min synced - // index is. - assert!(status.get("min_synced_block_index").is_some()); - assert_eq!(status.get("total_unspent_pmob").unwrap(), "0"); - assert_eq!(status.get("total_pending_pmob").unwrap(), "0"); - assert_eq!(status.get("total_spent_pmob").unwrap(), "0"); - assert_eq!(status.get("total_orphaned_pmob").unwrap(), "0"); - assert_eq!(status.get("total_secreted_pmob").unwrap(), "0"); - assert_eq!( - status.get("account_ids").unwrap().as_array().unwrap().len(), - 1 - ); - assert_eq!( - status - .get("account_map") - .unwrap() - .as_object() - .unwrap() - .len(), - 1 - ); - } - - #[test_with_logger] - fn test_account_status(logger: Logger) { - let mut rng: StdRng = SeedableRng::from_seed([20u8; 32]); - let (client, mut ledger_db, db_ctx, _network_state) = setup(&mut rng, logger.clone()); - - let body = json!({ - "jsonrpc": "2.0", - "id": 1, - "method": "create_account", - "params": { - "name": "Alice Main Account", - } - }); - let res = dispatch(&client, body, &logger); - let result = res.get("result").unwrap(); - let account_obj = result.get("account").unwrap(); - let account_id = account_obj.get("account_id").unwrap().as_str().unwrap(); - let b58_public_address = account_obj.get("main_address").unwrap().as_str().unwrap(); - let public_address = b58_decode_public_address(b58_public_address).unwrap(); - - // Add a block with a txo for this address - add_block_to_ledger_db( - &mut ledger_db, - &vec![public_address], - 42 * MOB, - &vec![KeyImage::from(rng.next_u64())], - &mut rng, - ); - - manually_sync_account( - &ledger_db, - &db_ctx.get_db_instance(logger.clone()), - &AccountID(account_id.to_string()), - &logger, - ); - - let body = json!({ - "jsonrpc": "2.0", - "id": 1, - "method": "get_account_status", - "params": { - "account_id": account_id, - } - }); - let res = dispatch(&client, body, &logger); - let result = res.get("result").unwrap(); - let balance = result.get("balance").unwrap(); - assert_eq!( - balance - .get("unspent_pmob") - .unwrap() - .as_str() - .unwrap() - .to_string(), - (42 * MOB).to_string() - ); - let _account = result.get("account").unwrap(); - } - - #[test_with_logger] - fn test_build_and_submit_transaction(logger: Logger) { - let mut rng: StdRng = SeedableRng::from_seed([20u8; 32]); - let (client, mut ledger_db, db_ctx, _network_state) = setup(&mut rng, logger.clone()); - - // Add an account - let body = json!({ - "jsonrpc": "2.0", - "id": 1, - "method": "create_account", - "params": { - "name": "Alice Main Account", - } - }); - let res = dispatch(&client, body, &logger); - let result = res.get("result").unwrap(); - let account_obj = result.get("account").unwrap(); - let account_id = account_obj.get("account_id").unwrap().as_str().unwrap(); - let b58_public_address = account_obj.get("main_address").unwrap().as_str().unwrap(); - let public_address = b58_decode_public_address(b58_public_address).unwrap(); - - // Add a block with a txo for this address (note that value is smaller than - // MINIMUM_FEE, so it is a "dust" TxOut that should get opportunistically swept - // up when we construct the transaction) - add_block_to_ledger_db( - &mut ledger_db, - &vec![public_address.clone()], - 100, - &vec![KeyImage::from(rng.next_u64())], - &mut rng, - ); - - manually_sync_account( - &ledger_db, - &db_ctx.get_db_instance(logger.clone()), - &AccountID(account_id.to_string()), - &logger, - ); - assert_eq!(ledger_db.num_blocks().unwrap(), 13); - - // Add a block with significantly more MOB - add_block_to_ledger_db( - &mut ledger_db, - &vec![public_address], - 100_000_000_000_000, // 100.0 MOB - &vec![KeyImage::from(rng.next_u64())], - &mut rng, - ); - - manually_sync_account( - &ledger_db, - &db_ctx.get_db_instance(logger.clone()), - &AccountID(account_id.to_string()), - &logger, - ); - assert_eq!(ledger_db.num_blocks().unwrap(), 14); - - // Create a tx proposal to ourselves - let body = json!({ - "jsonrpc": "2.0", - "id": 1, - "method": "build_and_submit_transaction", - "params": { - "account_id": account_id, - "recipient_public_address": b58_public_address, - "value_pmob": "42000000000000", // 42.0 MOB - } - }); - let res = dispatch(&client, body, &logger); - let result = res.get("result").unwrap(); - let tx_proposal = result.get("tx_proposal").unwrap(); - let tx = tx_proposal.get("tx").unwrap(); - let tx_prefix = tx.get("prefix").unwrap(); - - // Assert the fee is correct in both places - let prefix_fee = tx_prefix.get("fee").unwrap().as_str().unwrap(); - let fee = tx_proposal.get("fee").unwrap(); - // FIXME: WS-9 - Note, minimum fee does not fit into i32 - need to make sure we - // are not losing precision with the JsonTxProposal treating Fee as number - assert_eq!(fee, &Mob::MINIMUM_FEE.to_string()); - assert_eq!(fee, prefix_fee); - - // Transaction builder attempts to use as many inputs as we have txos - let inputs = tx_proposal.get("input_list").unwrap().as_array().unwrap(); - assert_eq!(inputs.len(), 2); - let prefix_inputs = tx_prefix.get("inputs").unwrap().as_array().unwrap(); - assert_eq!(prefix_inputs.len(), inputs.len()); - - // One destination - let outlays = tx_proposal.get("outlay_list").unwrap().as_array().unwrap(); - assert_eq!(outlays.len(), 1); - - // Map outlay -> tx_out, should have one entry for one outlay - let outlay_index_to_tx_out_index = tx_proposal - .get("outlay_index_to_tx_out_index") - .unwrap() - .as_array() - .unwrap(); - assert_eq!(outlay_index_to_tx_out_index.len(), 1); - - // Two outputs in the prefix, one for change - let prefix_outputs = tx_prefix.get("outputs").unwrap().as_array().unwrap(); - assert_eq!(prefix_outputs.len(), 2); - - // One outlay confirmation number for our one outlay (no receipt for change) - let outlay_confirmation_numbers = tx_proposal - .get("outlay_confirmation_numbers") - .unwrap() - .as_array() - .unwrap(); - assert_eq!(outlay_confirmation_numbers.len(), 1); - - // Tombstone block = ledger height (12 to start + 2 new blocks + 10 default - // tombstone) - let prefix_tombstone = tx_prefix.get("tombstone_block").unwrap(); - assert_eq!(prefix_tombstone, "24"); - - let json_tx_proposal: json_rpc::tx_proposal::TxProposal = - serde_json::from_value(tx_proposal.clone()).unwrap(); - let payments_tx_proposal = - mc_mobilecoind::payments::TxProposal::try_from(&json_tx_proposal).unwrap(); - - add_block_with_tx_proposal(&mut ledger_db, payments_tx_proposal); - manually_sync_account( - &ledger_db, - &db_ctx.get_db_instance(logger.clone()), - &AccountID(account_id.to_string()), - &logger, - ); - assert_eq!(ledger_db.num_blocks().unwrap(), 15); - - // Get balance after submission - let body = json!({ - "jsonrpc": "2.0", - "id": 1, - "method": "get_balance_for_account", - "params": { - "account_id": account_id, - } - }); - let res = dispatch(&client, body, &logger); - let result = res.get("result").unwrap(); - let balance_status = result.get("balance").unwrap(); - let unspent = balance_status - .get("unspent_pmob") - .unwrap() - .as_str() - .unwrap(); - let pending = balance_status - .get("pending_pmob") - .unwrap() - .as_str() - .unwrap(); - let spent = balance_status.get("spent_pmob").unwrap().as_str().unwrap(); - let secreted = balance_status - .get("secreted_pmob") - .unwrap() - .as_str() - .unwrap(); - let orphaned = balance_status - .get("orphaned_pmob") - .unwrap() - .as_str() - .unwrap(); - assert_eq!(unspent, &(100000000000100 - Mob::MINIMUM_FEE).to_string()); - assert_eq!(pending, "0"); - assert_eq!(spent, "100000000000100"); - assert_eq!(secreted, "0"); - assert_eq!(orphaned, "0"); - } - - #[test_with_logger] - fn test_large_transaction(logger: Logger) { - let mut rng: StdRng = SeedableRng::from_seed([20u8; 32]); - let (client, mut ledger_db, db_ctx, _network_state) = setup(&mut rng, logger.clone()); - - // Add an account - let body = json!({ - "jsonrpc": "2.0", - "id": 1, - "method": "create_account", - "params": { - "name": "Alice Main Account", - } - }); - let res = dispatch(&client, body, &logger); - let result = res.get("result").unwrap(); - let account_obj = result.get("account").unwrap(); - let account_id = account_obj.get("account_id").unwrap().as_str().unwrap(); - let b58_public_address = account_obj.get("main_address").unwrap().as_str().unwrap(); - let public_address = b58_decode_public_address(b58_public_address).unwrap(); - - // Add a block with a large txo for this address. - add_block_to_ledger_db( - &mut ledger_db, - &vec![public_address.clone()], - 11_000_000_000_000_000_000, // Eleven million MOB. - &vec![KeyImage::from(rng.next_u64())], - &mut rng, - ); - - manually_sync_account( - &ledger_db, - &db_ctx.get_db_instance(logger.clone()), - &AccountID(account_id.to_string()), - &logger, - ); - assert_eq!(ledger_db.num_blocks().unwrap(), 13); - - // Create a tx proposal to ourselves - let body = json!({ - "jsonrpc": "2.0", - "id": 1, - "method": "build_and_submit_transaction", - "params": { - "account_id": account_id, - "recipient_public_address": b58_public_address, - "value_pmob": "10000000000000000000", // Ten million MOB, which is larger than i64::MAX picomob. - } - }); - let res = dispatch(&client, body, &logger); - let result = res.get("result").unwrap(); - let tx_proposal = result.get("tx_proposal").unwrap(); - - // Check that the value was recorded correctly. - let transaction_log = result.get("transaction_log").unwrap(); - assert_eq!( - transaction_log.get("direction").unwrap().as_str().unwrap(), - "tx_direction_sent" - ); - assert_eq!( - transaction_log.get("value_pmob").unwrap().as_str().unwrap(), - "10000000000000000000", - ); - assert_eq!( - transaction_log - .get("input_txos") - .unwrap() - .get(0) - .unwrap() - .get("value_pmob") - .unwrap() - .as_str() - .unwrap(), - 11_000_000_000_000_000_000u64.to_string(), - ); - assert_eq!( - transaction_log - .get("output_txos") - .unwrap() - .get(0) - .unwrap() - .get("value_pmob") - .unwrap() - .as_str() - .unwrap(), - 10_000_000_000_000_000_000u64.to_string(), - ); - assert_eq!( - transaction_log - .get("change_txos") - .unwrap() - .get(0) - .unwrap() - .get("value_pmob") - .unwrap() - .as_str() - .unwrap(), - (1_000_000_000_000_000_000u64 - Mob::MINIMUM_FEE).to_string(), - ); - - // Sync the proposal. - let json_tx_proposal: json_rpc::tx_proposal::TxProposal = - serde_json::from_value(tx_proposal.clone()).unwrap(); - let payments_tx_proposal = - mc_mobilecoind::payments::TxProposal::try_from(&json_tx_proposal).unwrap(); - - add_block_with_tx_proposal(&mut ledger_db, payments_tx_proposal); - manually_sync_account( - &ledger_db, - &db_ctx.get_db_instance(logger.clone()), - &AccountID(account_id.to_string()), - &logger, - ); - assert_eq!(ledger_db.num_blocks().unwrap(), 14); - - // Get balance after submission - let body = json!({ - "jsonrpc": "2.0", - "id": 1, - "method": "get_balance_for_account", - "params": { - "account_id": account_id, - } - }); - let res = dispatch(&client, body, &logger); - let result = res.get("result").unwrap(); - let balance_status = result.get("balance").unwrap(); - let unspent = balance_status - .get("unspent_pmob") - .unwrap() - .as_str() - .unwrap(); - let pending = balance_status - .get("pending_pmob") - .unwrap() - .as_str() - .unwrap(); - let spent = balance_status.get("spent_pmob").unwrap().as_str().unwrap(); - let secreted = balance_status - .get("secreted_pmob") - .unwrap() - .as_str() - .unwrap(); - let orphaned = balance_status - .get("orphaned_pmob") - .unwrap() - .as_str() - .unwrap(); - assert_eq!( - unspent, - &(11_000_000_000_000_000_000u64 - Mob::MINIMUM_FEE).to_string() - ); - assert_eq!(pending, "0"); - assert_eq!(spent, 11_000_000_000_000_000_000u64.to_string()); - assert_eq!(secreted, "0"); - assert_eq!(orphaned, "0"); - } - - #[test_with_logger] - fn test_build_then_submit_transaction(logger: Logger) { - let mut rng: StdRng = SeedableRng::from_seed([20u8; 32]); - let (client, mut ledger_db, db_ctx, _network_state) = setup(&mut rng, logger.clone()); - - // Add an account - let body = json!({ - "jsonrpc": "2.0", - "id": 1, - "method": "create_account", - "params": { - "name": "Alice Main Account", - } - }); - let res = dispatch(&client, body, &logger); - let result = res.get("result").unwrap(); - let account_obj = result.get("account").unwrap(); - let account_id = account_obj.get("account_id").unwrap().as_str().unwrap(); - let b58_public_address = account_obj.get("main_address").unwrap().as_str().unwrap(); - let public_address = b58_decode_public_address(b58_public_address).unwrap(); - - // Add a block with a txo for this address (note that value is smaller than - // MINIMUM_FEE, so it is a "dust" TxOut that should get opportunistically swept - // up when we construct the transaction) - add_block_to_ledger_db( - &mut ledger_db, - &vec![public_address.clone()], - 100, - &vec![KeyImage::from(rng.next_u64())], - &mut rng, - ); - - manually_sync_account( - &ledger_db, - &db_ctx.get_db_instance(logger.clone()), - &AccountID(account_id.to_string()), - &logger, - ); - assert_eq!(ledger_db.num_blocks().unwrap(), 13); - - // Create a tx proposal to ourselves - let body = json!({ - "jsonrpc": "2.0", - "id": 1, - "method": "build_transaction", - "params": { - "account_id": account_id, - "recipient_public_address": b58_public_address, - "value_pmob": "42", - } - }); - // We will fail because we cannot afford the fee - dispatch_expect_error( - &client, - body, - &logger, - json!({ - "method": "build_transaction", - "error": json!({ - "code": -32603, - "message": "InternalError", - "data": json!({ - "server_error": format!("TransactionBuilder(WalletDb(InsufficientFundsUnderMaxSpendable(\"Max spendable value in wallet: 0, but target value: {}\")))", 42 + Mob::MINIMUM_FEE), - "details": format!("Error building transaction: Wallet DB Error: Insufficient funds from Txos under max_spendable_value: Max spendable value in wallet: 0, but target value: {}", 42 + Mob::MINIMUM_FEE), - }) - }), - "jsonrpc": "2.0", - "id": 1, - }).to_string(), - ); - - // Add a block with significantly more MOB - add_block_to_ledger_db( - &mut ledger_db, - &vec![public_address], - 100000000000000, // 100.0 MOB - &vec![KeyImage::from(rng.next_u64())], - &mut rng, - ); - - manually_sync_account( - &ledger_db, - &db_ctx.get_db_instance(logger.clone()), - &AccountID(account_id.to_string()), - &logger, - ); - assert_eq!(ledger_db.num_blocks().unwrap(), 14); - - // Create a tx proposal to ourselves - let body = json!({ - "jsonrpc": "2.0", - "id": 1, - "method": "build_transaction", - "params": { - "account_id": account_id, - "recipient_public_address": b58_public_address, - "value_pmob": "42000000000000", // 42.0 MOB - } - }); - let res = dispatch(&client, body, &logger); - let result = res.get("result").unwrap(); - let tx_proposal = result.get("tx_proposal").unwrap(); - let tx = tx_proposal.get("tx").unwrap(); - let tx_prefix = tx.get("prefix").unwrap(); - - // Assert the fee is correct in both places - let prefix_fee = tx_prefix.get("fee").unwrap().as_str().unwrap(); - let fee = tx_proposal.get("fee").unwrap(); - // FIXME: WS-9 - Note, minimum fee does not fit into i32 - need to make sure we - // are not losing precision with the JsonTxProposal treating Fee as number - assert_eq!(fee, &Mob::MINIMUM_FEE.to_string()); - assert_eq!(fee, prefix_fee); - - // Transaction builder attempts to use as many inputs as we have txos - let inputs = tx_proposal.get("input_list").unwrap().as_array().unwrap(); - assert_eq!(inputs.len(), 2); - let prefix_inputs = tx_prefix.get("inputs").unwrap().as_array().unwrap(); - assert_eq!(prefix_inputs.len(), inputs.len()); - - // One destination - let outlays = tx_proposal.get("outlay_list").unwrap().as_array().unwrap(); - assert_eq!(outlays.len(), 1); - - // Map outlay -> tx_out, should have one entry for one outlay - let outlay_index_to_tx_out_index = tx_proposal - .get("outlay_index_to_tx_out_index") - .unwrap() - .as_array() - .unwrap(); - assert_eq!(outlay_index_to_tx_out_index.len(), 1); - - // Two outputs in the prefix, one for change - let prefix_outputs = tx_prefix.get("outputs").unwrap().as_array().unwrap(); - assert_eq!(prefix_outputs.len(), 2); - - // One outlay confirmation number for our one outlay (no receipt for change) - let outlay_confirmation_numbers = tx_proposal - .get("outlay_confirmation_numbers") - .unwrap() - .as_array() - .unwrap(); - assert_eq!(outlay_confirmation_numbers.len(), 1); - - // Tombstone block = ledger height (12 to start + 2 new blocks + 10 default - // tombstone) - let prefix_tombstone = tx_prefix.get("tombstone_block").unwrap(); - assert_eq!(prefix_tombstone, "24"); - - // Get current balance - assert_eq!(ledger_db.num_blocks().unwrap(), 14); - let body = json!({ - "jsonrpc": "2.0", - "id": 1, - "method": "get_balance_for_account", - "params": { - "account_id": account_id, - } - }); - let res = dispatch(&client, body, &logger); - let result = res.get("result").unwrap(); - let balance_status = result.get("balance").unwrap(); - let unspent = balance_status - .get("unspent_pmob") - .unwrap() - .as_str() - .unwrap(); - assert_eq!(unspent, "100000000000100"); - - // Submit the tx_proposal - let body = json!({ - "jsonrpc": "2.0", - "id": 1, - "method": "submit_transaction", - "params": { - "tx_proposal": tx_proposal, - "account_id": account_id, - } - }); - let res = dispatch(&client, body, &logger); - let result = res.get("result").unwrap(); - let transaction_id = result - .get("transaction_log") - .unwrap() - .get("transaction_log_id") - .unwrap() - .as_str() - .unwrap(); - // Note - we cannot test here that the transaction ID is consistent, because - // there is randomness in the transaction creation. - - let json_tx_proposal: json_rpc::tx_proposal::TxProposal = - serde_json::from_value(tx_proposal.clone()).unwrap(); - let payments_tx_proposal = - mc_mobilecoind::payments::TxProposal::try_from(&json_tx_proposal).unwrap(); - - // The MockBlockchainConnection does not write to the ledger_db - add_block_with_tx_proposal(&mut ledger_db, payments_tx_proposal); - manually_sync_account( - &ledger_db, - &db_ctx.get_db_instance(logger.clone()), - &AccountID(account_id.to_string()), - &logger, - ); - assert_eq!(ledger_db.num_blocks().unwrap(), 15); - - // Get balance after submission - let body = json!({ - "jsonrpc": "2.0", - "id": 1, - "method": "get_balance_for_account", - "params": { - "account_id": account_id, - } - }); - let res = dispatch(&client, body, &logger); - let result = res.get("result").unwrap(); - let balance_status = result.get("balance").unwrap(); - let unspent = balance_status - .get("unspent_pmob") - .unwrap() - .as_str() - .unwrap(); - let pending = balance_status - .get("pending_pmob") - .unwrap() - .as_str() - .unwrap(); - let spent = balance_status.get("spent_pmob").unwrap().as_str().unwrap(); - let secreted = balance_status - .get("secreted_pmob") - .unwrap() - .as_str() - .unwrap(); - let orphaned = balance_status - .get("orphaned_pmob") - .unwrap() - .as_str() - .unwrap(); - assert_eq!(unspent, "99999600000100"); - assert_eq!(pending, "0"); - assert_eq!(spent, "100000000000100"); - assert_eq!(secreted, "0"); - assert_eq!(orphaned, "0"); - - // Get the transaction_id and verify it contains what we expect - let body = json!({ - "jsonrpc": "2.0", - "id": 1, - "method": "get_transaction_log", - "params": { - "transaction_log_id": transaction_id, - } - }); - let res = dispatch(&client, body, &logger); - let result = res.get("result").unwrap(); - let transaction_log = result.get("transaction_log").unwrap(); - assert_eq!( - transaction_log.get("direction").unwrap().as_str().unwrap(), - "tx_direction_sent" - ); - assert_eq!( - transaction_log.get("value_pmob").unwrap().as_str().unwrap(), - "42000000000000" - ); - assert_eq!( - transaction_log.get("output_txos").unwrap()[0] - .get("recipient_address_id") - .unwrap() - .as_str() - .unwrap(), - b58_public_address - ); - transaction_log.get("account_id").unwrap().as_str().unwrap(); - assert_eq!( - transaction_log.get("fee_pmob").unwrap().as_str().unwrap(), - &Mob::MINIMUM_FEE.to_string() - ); - assert_eq!( - transaction_log.get("status").unwrap().as_str().unwrap(), - "tx_status_succeeded" - ); - assert_eq!( - transaction_log - .get("submitted_block_index") - .unwrap() - .as_str() - .unwrap(), - "14" - ); - assert_eq!( - transaction_log - .get("transaction_log_id") - .unwrap() - .as_str() - .unwrap(), - transaction_id - ); - - // Get All Transaction Logs - let body = json!({ - "jsonrpc": "2.0", - "id": 1, - "method": "get_transaction_logs_for_account", - "params": { - "account_id": account_id, - } - }); - let res = dispatch(&client, body, &logger); - let result = res.get("result").unwrap(); - let transaction_log_ids = result - .get("transaction_log_ids") - .unwrap() - .as_array() - .unwrap(); - // We have a transaction log for each of the received, as well as the sent. - assert_eq!(transaction_log_ids.len(), 5); - - // Check the contents of the transaction log associated txos - let transaction_log_map = result.get("transaction_log_map").unwrap(); - let transaction_log = transaction_log_map.get(transaction_id).unwrap(); - assert_eq!( - transaction_log - .get("output_txos") - .unwrap() - .as_array() - .unwrap() - .len(), - 1 - ); - assert_eq!( - transaction_log - .get("input_txos") - .unwrap() - .as_array() - .unwrap() - .len(), - 2 - ); - assert_eq!( - transaction_log - .get("change_txos") - .unwrap() - .as_array() - .unwrap() - .len(), - 1 - ); - - assert_eq!( - transaction_log.get("status").unwrap().as_str().unwrap(), - "tx_status_succeeded" - ); - - // Get all Transaction Logs for a given Block - - let body = json!({ - "jsonrpc": "2.0", - "id": 1, - "method": "get_all_transaction_logs_ordered_by_block", - }); - let res = dispatch(&client, body, &logger); - let result = res.get("result").unwrap(); - let transaction_log_map = result - .get("transaction_log_map") - .unwrap() - .as_object() - .unwrap(); - assert_eq!(transaction_log_map.len(), 5); - } - - #[test_with_logger] - fn test_tx_status_failed_when_tombstone_block_index_exceeded(logger: Logger) { - let mut rng: StdRng = SeedableRng::from_seed([20u8; 32]); - let (client, mut ledger_db, db_ctx, _network_state) = setup(&mut rng, logger.clone()); - - // Add an account - let body = json!({ - "jsonrpc": "2.0", - "id": 1, - "method": "create_account", - "params": { - "name": "Alice Main Account", - } - }); - let res = dispatch(&client, body, &logger); - let result = res.get("result").unwrap(); - let account_obj = result.get("account").unwrap(); - let account_id = account_obj.get("account_id").unwrap().as_str().unwrap(); - let b58_public_address = account_obj.get("main_address").unwrap().as_str().unwrap(); - let public_address = b58_decode_public_address(b58_public_address).unwrap(); - - // Add a block with a txo for this address (note that value is smaller than - // MINIMUM_FEE, so it is a "dust" TxOut that should get opportunistically swept - // up when we construct the transaction) - add_block_to_ledger_db( - &mut ledger_db, - &vec![public_address.clone()], - 100, - &vec![KeyImage::from(rng.next_u64())], - &mut rng, - ); - - manually_sync_account( - &ledger_db, - &db_ctx.get_db_instance(logger.clone()), - &AccountID(account_id.to_string()), - &logger, - ); - assert_eq!(ledger_db.num_blocks().unwrap(), 13); - - // Add a block with significantly more MOB - add_block_to_ledger_db( - &mut ledger_db, - &vec![public_address.clone()], - 100000000000000, // 100.0 MOB - &vec![KeyImage::from(rng.next_u64())], - &mut rng, - ); - - manually_sync_account( - &ledger_db, - &db_ctx.get_db_instance(logger.clone()), - &AccountID(account_id.to_string()), - &logger, - ); - assert_eq!(ledger_db.num_blocks().unwrap(), 14); - - // Create a tx proposal to ourselves with a tombstone block of 1 - let body = json!({ - "jsonrpc": "2.0", - "id": 1, - "method": "build_and_submit_transaction", - "params": { - "account_id": account_id, - "recipient_public_address": b58_public_address, - "value_pmob": "42000000000000", // 42.0 MOB - "tombstone_block": "16", - } - }); - let res = dispatch(&client, body, &logger); - let result = res.get("result").unwrap(); - let tx_log = result.get("transaction_log").unwrap(); - let tx_log_status = tx_log.get("status").unwrap(); - let tx_log_id = tx_log.get("transaction_log_id").unwrap(); - - assert_eq!(tx_log_status, "tx_status_pending"); - - // Add a block with 1 MOB - add_block_to_ledger_db( - &mut ledger_db, - &vec![public_address.clone()], - 1, // 100.0 MOB - &vec![KeyImage::from(rng.next_u64())], - &mut rng, - ); - - manually_sync_account( - &ledger_db, - &db_ctx.get_db_instance(logger.clone()), - &AccountID(account_id.to_string()), - &logger, - ); - - // Get balance after submission - let body = json!({ - "jsonrpc": "2.0", - "id": 1, - "method": "get_balance_for_account", - "params": { - "account_id": account_id, - } - }); - let res = dispatch(&client, body, &logger); - let result = res.get("result").unwrap(); - let balance_status = result.get("balance").unwrap(); - let unspent = balance_status - .get("unspent_pmob") - .unwrap() - .as_str() - .unwrap(); - let pending = balance_status - .get("pending_pmob") - .unwrap() - .as_str() - .unwrap(); - assert_eq!(unspent, "1"); - assert_eq!(pending, "100000000000100"); - - // Add a block with 1 MOB to increment height 2 times, - // which should cause the previous transaction to - // become invalid and free up the TXO as well as mark - // the transaction log as TX_STATUS_FAILED - add_block_to_ledger_db( - &mut ledger_db, - &vec![public_address.clone()], - 1, // 100.0 MOB - &vec![KeyImage::from(rng.next_u64())], - &mut rng, - ); - add_block_to_ledger_db( - &mut ledger_db, - &vec![public_address.clone()], - 1, // 100.0 MOB - &vec![KeyImage::from(rng.next_u64())], - &mut rng, - ); - manually_sync_account( - &ledger_db, - &db_ctx.get_db_instance(logger.clone()), - &AccountID(account_id.to_string()), - &logger, - ); - - assert_eq!(ledger_db.num_blocks().unwrap(), 17); - - // Get tx log after syncing is finished - let body = json!({ - "jsonrpc": "2.0", - "id": 1, - "method": "get_transaction_log", - "params": { - "transaction_log_id": tx_log_id, - } - }); - let res = dispatch(&client, body, &logger); - let result = res.get("result").unwrap(); - let tx_log = result.get("transaction_log").unwrap(); - let tx_log_status = tx_log.get("status").unwrap(); - - assert_eq!(tx_log_status, "tx_status_failed"); - - // Get balance after submission - let body = json!({ - "jsonrpc": "2.0", - "id": 1, - "method": "get_balance_for_account", - "params": { - "account_id": account_id, - } - }); - let res = dispatch(&client, body, &logger); - let result = res.get("result").unwrap(); - let balance_status = result.get("balance").unwrap(); - let unspent = balance_status - .get("unspent_pmob") - .unwrap() - .as_str() - .unwrap(); - let pending = balance_status - .get("pending_pmob") - .unwrap() - .as_str() - .unwrap(); - let spent = balance_status.get("spent_pmob").unwrap().as_str().unwrap(); - assert_eq!(unspent, "100000000000103".to_string()); - assert_eq!(pending, "0"); - assert_eq!(spent, "0"); - } - - #[test_with_logger] - fn test_multiple_outlay_transaction(logger: Logger) { - let mut rng: StdRng = SeedableRng::from_seed([20u8; 32]); - let (client, mut ledger_db, db_ctx, _network_state) = setup(&mut rng, logger.clone()); - - // Add some accounts. - let body = json!({ - "jsonrpc": "2.0", - "id": 1, - "method": "create_account", - "params": { - "name": "Alice Main Account", - } - }); - let res = dispatch(&client, body, &logger); - let result = res.get("result").unwrap(); - let account_obj = result.get("account").unwrap(); - let alice_account_id = account_obj.get("account_id").unwrap().as_str().unwrap(); - let b58_public_address = account_obj.get("main_address").unwrap().as_str().unwrap(); - let alice_public_address = b58_decode_public_address(b58_public_address).unwrap(); - - let body = json!({ - "jsonrpc": "2.0", - "id": 1, - "method": "create_account", - "params": { - "name": "Bob Main Account", - } - }); - let res = dispatch(&client, body, &logger); - let result = res.get("result").unwrap(); - let account_obj = result.get("account").unwrap(); - let bob_account_id = account_obj.get("account_id").unwrap().as_str().unwrap(); - let bob_b58_public_address = account_obj.get("main_address").unwrap().as_str().unwrap(); - - let body = json!({ - "jsonrpc": "2.0", - "id": 1, - "method": "create_account", - "params": { - "name": "Charlie Main Account", - } - }); - let res = dispatch(&client, body, &logger); - let result = res.get("result").unwrap(); - let account_obj = result.get("account").unwrap(); - let charlie_account_id = account_obj.get("account_id").unwrap().as_str().unwrap(); - let charlie_b58_public_address = account_obj.get("main_address").unwrap().as_str().unwrap(); - - // Add some money to Alice's account. - add_block_to_ledger_db( - &mut ledger_db, - &vec![alice_public_address], - 100000000000000, // 100.0 MOB - &vec![KeyImage::from(rng.next_u64())], - &mut rng, - ); - - manually_sync_account( - &ledger_db, - &db_ctx.get_db_instance(logger.clone()), - &AccountID(alice_account_id.to_string()), - &logger, - ); - - // Create a two-output tx proposal to Bob and Charlie. - let body = json!({ - "jsonrpc": "2.0", - "id": 1, - "method": "build_transaction", - "params": { - "account_id": alice_account_id, - "addresses_and_values": [ - [bob_b58_public_address, "42000000000000"], // 42.0 MOB - [charlie_b58_public_address, "43000000000000"], // 43.0 MOB - ] - } - }); - let res = dispatch(&client, body, &logger); - let result = res.get("result").unwrap(); - - let tx_proposal = result.get("tx_proposal").unwrap(); - let tx = tx_proposal.get("tx").unwrap(); - let tx_prefix = tx.get("prefix").unwrap(); - - // Assert the fee is correct in both places - let prefix_fee = tx_prefix.get("fee").unwrap().as_str().unwrap(); - let fee = tx_proposal.get("fee").unwrap(); - // FIXME: WS-9 - Note, minimum fee does not fit into i32 - need to make sure we - // are not losing precision with the JsonTxProposal treating Fee as number - assert_eq!(fee, &Mob::MINIMUM_FEE.to_string()); - assert_eq!(fee, prefix_fee); - - // Two destinations. - let outlays = tx_proposal.get("outlay_list").unwrap().as_array().unwrap(); - assert_eq!(outlays.len(), 2); - - // Map outlay -> tx_out, should have one entry for one outlay - let outlay_index_to_tx_out_index = tx_proposal - .get("outlay_index_to_tx_out_index") - .unwrap() - .as_array() - .unwrap(); - assert_eq!(outlay_index_to_tx_out_index.len(), 2); - - // Three outputs in the prefix, one for change - let prefix_outputs = tx_prefix.get("outputs").unwrap().as_array().unwrap(); - assert_eq!(prefix_outputs.len(), 3); - - // Two outlay confirmation numbers for our two outlays (no receipt for change) - let outlay_confirmation_numbers = tx_proposal - .get("outlay_confirmation_numbers") - .unwrap() - .as_array() - .unwrap(); - assert_eq!(outlay_confirmation_numbers.len(), 2); - - // Get balances before submitting. - let body = json!({ - "jsonrpc": "2.0", - "id": 1, - "method": "get_balance_for_account", - "params": { - "account_id": alice_account_id, - } - }); - let res = dispatch(&client, body, &logger); - let result = res.get("result").unwrap(); - let balance_status = result.get("balance").unwrap(); - let alice_unspent = balance_status - .get("unspent_pmob") - .unwrap() - .as_str() - .unwrap(); - assert_eq!(alice_unspent, "100000000000000"); - - let body = json!({ - "jsonrpc": "2.0", - "id": 1, - "method": "get_balance_for_account", - "params": { - "account_id": bob_account_id, - } - }); - let res = dispatch(&client, body, &logger); - let result = res.get("result").unwrap(); - let balance_status = result.get("balance").unwrap(); - let bob_unspent = balance_status - .get("unspent_pmob") - .unwrap() - .as_str() - .unwrap(); - assert_eq!(bob_unspent, "0"); - - let body = json!({ - "jsonrpc": "2.0", - "id": 1, - "method": "get_balance_for_account", - "params": { - "account_id": charlie_account_id, - } - }); - let res = dispatch(&client, body, &logger); - let result = res.get("result").unwrap(); - let balance_status = result.get("balance").unwrap(); - let charlie_unspent = balance_status - .get("unspent_pmob") - .unwrap() - .as_str() - .unwrap(); - assert_eq!(charlie_unspent, "0"); - - // Submit the tx_proposal - assert_eq!(ledger_db.num_blocks().unwrap(), 13); - let body = json!({ - "jsonrpc": "2.0", - "id": 1, - "method": "submit_transaction", - "params": { - "tx_proposal": tx_proposal, - "account_id": alice_account_id, - } - }); - let res = dispatch(&client, body, &logger); - let result = res.get("result").unwrap(); - let transaction_id = result - .get("transaction_log") - .unwrap() - .get("transaction_log_id") - .unwrap() - .as_str() - .unwrap(); - - let json_tx_proposal: json_rpc::tx_proposal::TxProposal = - serde_json::from_value(tx_proposal.clone()).unwrap(); - let payments_tx_proposal = - mc_mobilecoind::payments::TxProposal::try_from(&json_tx_proposal).unwrap(); - - // The MockBlockchainConnection does not write to the ledger_db - add_block_with_tx_proposal(&mut ledger_db, payments_tx_proposal); - assert_eq!(ledger_db.num_blocks().unwrap(), 14); - - // Wait for accounts to sync. - manually_sync_account( - &ledger_db, - &db_ctx.get_db_instance(logger.clone()), - &AccountID(alice_account_id.to_string()), - &logger, - ); - manually_sync_account( - &ledger_db, - &db_ctx.get_db_instance(logger.clone()), - &AccountID(bob_account_id.to_string()), - &logger, - ); - manually_sync_account( - &ledger_db, - &db_ctx.get_db_instance(logger.clone()), - &AccountID(charlie_account_id.to_string()), - &logger, - ); - - // Get balances after submission - let body = json!({ - "jsonrpc": "2.0", - "id": 1, - "method": "get_balance_for_account", - "params": { - "account_id": alice_account_id, - } - }); - let res = dispatch(&client, body, &logger); - let result = res.get("result").unwrap(); - let balance_status = result.get("balance").unwrap(); - let unspent = balance_status - .get("unspent_pmob") - .unwrap() - .as_str() - .unwrap(); - assert_eq!(unspent, &(15 * MOB - Mob::MINIMUM_FEE).to_string()); - - let body = json!({ - "jsonrpc": "2.0", - "id": 1, - "method": "get_balance_for_account", - "params": { - "account_id": bob_account_id, - } - }); - let res = dispatch(&client, body, &logger); - let result = res.get("result").unwrap(); - let balance_status = result.get("balance").unwrap(); - let bob_unspent = balance_status - .get("unspent_pmob") - .unwrap() - .as_str() - .unwrap(); - assert_eq!(bob_unspent, "42000000000000"); - - let body = json!({ - "jsonrpc": "2.0", - "id": 1, - "method": "get_balance_for_account", - "params": { - "account_id": charlie_account_id, - } - }); - let res = dispatch(&client, body, &logger); - let result = res.get("result").unwrap(); - let balance_status = result.get("balance").unwrap(); - let charlie_unspent = balance_status - .get("unspent_pmob") - .unwrap() - .as_str() - .unwrap(); - assert_eq!(charlie_unspent, "43000000000000"); - - // Get the transaction log and verify it contains what we expect - let body = json!({ - "jsonrpc": "2.0", - "id": 1, - "method": "get_transaction_log", - "params": { - "transaction_log_id": transaction_id, - } - }); - let res = dispatch(&client, body, &logger); - let result = res.get("result").unwrap(); - let transaction_log = result.get("transaction_log").unwrap(); - assert_eq!( - transaction_log.get("direction").unwrap().as_str().unwrap(), - "tx_direction_sent" - ); - assert_eq!( - transaction_log.get("value_pmob").unwrap().as_str().unwrap(), - "85000000000000" - ); - - let mut output_addresses: Vec = transaction_log - .get("output_txos") - .unwrap() - .as_array() - .unwrap() - .iter() - .map(|t| { - t.get("recipient_address_id") - .unwrap() - .as_str() - .unwrap() - .into() - }) - .collect(); - output_addresses.sort(); - let mut target_addresses = vec![bob_b58_public_address, charlie_b58_public_address]; - target_addresses.sort(); - assert_eq!(output_addresses, target_addresses); - - transaction_log.get("account_id").unwrap().as_str().unwrap(); - assert_eq!( - transaction_log.get("fee_pmob").unwrap().as_str().unwrap(), - &Mob::MINIMUM_FEE.to_string() - ); - assert_eq!( - transaction_log.get("status").unwrap().as_str().unwrap(), - "tx_status_succeeded" - ); - assert_eq!( - transaction_log - .get("submitted_block_index") - .unwrap() - .as_str() - .unwrap(), - "13" - ); - assert_eq!( - transaction_log - .get("transaction_log_id") - .unwrap() - .as_str() - .unwrap(), - transaction_id - ); - } - - #[test_with_logger] - fn test_paginate_transactions(logger: Logger) { - let mut rng: StdRng = SeedableRng::from_seed([20u8; 32]); - let (client, mut ledger_db, db_ctx, _network_state) = setup(&mut rng, logger.clone()); - - // Add an account - let body = json!({ - "jsonrpc": "2.0", - "id": 1, - "method": "create_account", - "params": { - "name": "", - } - }); - let res = dispatch(&client, body, &logger); - let result = res.get("result").unwrap(); - let account_obj = result.get("account").unwrap(); - let account_id = account_obj.get("account_id").unwrap().as_str().unwrap(); - let b58_public_address = account_obj.get("main_address").unwrap().as_str().unwrap(); - let public_address = b58_decode_public_address(b58_public_address).unwrap(); - - // Add some transactions. - for _ in 0..10 { - add_block_to_ledger_db( - &mut ledger_db, - &vec![public_address.clone()], - 100, - &vec![KeyImage::from(rng.next_u64())], - &mut rng, - ); - } - - assert_eq!(ledger_db.num_blocks().unwrap(), 22); - manually_sync_account( - &ledger_db, - &db_ctx.get_db_instance(logger.clone()), - &AccountID(account_id.to_string()), - &logger, - ); - - // Check that we can paginate txo output. - let body = json!({ - "jsonrpc": "2.0", - "id": 1, - "method": "get_txos_for_account", - "params": { - "account_id": account_id, - } - }); - let res = dispatch(&client, body, &logger); - let result = res.get("result").unwrap(); - let txos_all = result.get("txo_ids").unwrap().as_array().unwrap(); - assert_eq!(txos_all.len(), 10); - - let body = json!({ - "jsonrpc": "2.0", - "id": 1, - "method": "get_txos_for_account", - "params": { - "account_id": account_id, - "offset": "2", - "limit": "5", - } - }); - let res = dispatch(&client, body, &logger); - let result = res.get("result").unwrap(); - let txos_page = result.get("txo_ids").unwrap().as_array().unwrap(); - assert_eq!(txos_page.len(), 5); - assert_eq!(txos_all[2..7].len(), 5); - assert_eq!(txos_page[..], txos_all[2..7]); - - // Check that we can paginate transaction log output. - let body = json!({ - "jsonrpc": "2.0", - "id": 1, - "method": "get_transaction_logs_for_account", - "params": { - "account_id": account_id, - } - }); - let res = dispatch(&client, body, &logger); - let result = res.get("result").unwrap(); - let tx_logs_all = result - .get("transaction_log_ids") - .unwrap() - .as_array() - .unwrap(); - assert_eq!(tx_logs_all.len(), 10); - - let body = json!({ - "jsonrpc": "2.0", - "id": 1, - "method": "get_transaction_logs_for_account", - "params": { - "account_id": account_id, - "offset": "3", - "limit": "6", - } - }); - let res = dispatch(&client, body, &logger); - let result = res.get("result").unwrap(); - let tx_logs_page = result - .get("transaction_log_ids") - .unwrap() - .as_array() - .unwrap(); - assert_eq!(tx_logs_page.len(), 6); - assert_eq!(tx_logs_all[3..9].len(), 6); - assert_eq!(tx_logs_page[..], tx_logs_all[3..9]); - } - - #[test_with_logger] - fn test_paginate_assigned_addresses(logger: Logger) { - let mut rng: StdRng = SeedableRng::from_seed([20u8; 32]); - let (client, _ledger_db, _db_ctx, _network_state) = setup(&mut rng, logger.clone()); - - // Add an account - let body = json!({ - "jsonrpc": "2.0", - "id": 1, - "method": "create_account", - "params": { - "name": "", - } - }); - let res = dispatch(&client, body, &logger); - let result = res.get("result").unwrap(); - let account_obj = result.get("account").unwrap(); - let account_id = account_obj.get("account_id").unwrap().as_str().unwrap(); - - // Assign some addresses. - for _ in 0..10 { - let body = json!({ - "jsonrpc": "2.0", - "id": 1, - "method": "assign_address_for_account", - "params": { - "account_id": account_id, - "metadata": "subaddress_index_2", - } - }); - dispatch(&client, body, &logger); - } - - // Check that we can paginate address output. - let body = json!({ - "jsonrpc": "2.0", - "id": 1, - "method": "get_addresses_for_account", - "params": { - "account_id": account_id, - }, - }); - let res = dispatch(&client, body, &logger); - let result = res.get("result").unwrap(); - let addresses_all = result.get("public_addresses").unwrap().as_array().unwrap(); - assert_eq!(addresses_all.len(), 13); // Accounts start with 3 addresses, then we created 10. - - let body = json!({ - "jsonrpc": "2.0", - "id": 1, - "method": "get_addresses_for_account", - "params": { - "account_id": account_id, - "offset": "1", - "limit": "4", - }, - }); - let res = dispatch(&client, body, &logger); - let result = res.get("result").unwrap(); - let addresses_page = result.get("public_addresses").unwrap().as_array().unwrap(); - assert_eq!(addresses_page.len(), 4); - assert_eq!(addresses_page[..], addresses_all[1..5]); - } - - #[test_with_logger] - fn test_next_subaddress_fails_with_fog(logger: Logger) { - use crate::db::WalletDbError::SubaddressesNotSupportedForFOGEnabledAccounts as subaddress_error; - - let mut rng: StdRng = SeedableRng::from_seed([20u8; 32]); - let (client, mut _ledger_db, _db_ctx, _network_state) = setup(&mut rng, logger.clone()); - - // Create Account - let body = json!({ - "jsonrpc": "2.0", - "id": 1, - "method": "create_account", - "params": { - "name": "Alice Main Account", - "fog_report_url": "fog://fog-report.example.com", - "fog_report_id": "", - "fog_authority_spki": "MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAvnB9wTbTOT5uoizRYaYbw7XIEkInl8E7MGOAQj+xnC+F1rIXiCnc/t1+5IIWjbRGhWzo7RAwI5sRajn2sT4rRn9NXbOzZMvIqE4hmhmEzy1YQNDnfALAWNQ+WBbYGW+Vqm3IlQvAFFjVN1YYIdYhbLjAPdkgeVsWfcLDforHn6rR3QBZYZIlSBQSKRMY/tywTxeTCvK2zWcS0kbbFPtBcVth7VFFVPAZXhPi9yy1AvnldO6n7KLiupVmojlEMtv4FQkk604nal+j/dOplTATV8a9AJBbPRBZ/yQg57EG2Y2MRiHOQifJx0S5VbNyMm9bkS8TD7Goi59aCW6OT1gyeotWwLg60JRZTfyJ7lYWBSOzh0OnaCytRpSWtNZ6barPUeOnftbnJtE8rFhF7M4F66et0LI/cuvXYecwVwykovEVBKRF4HOK9GgSm17mQMtzrD7c558TbaucOWabYR04uhdAc3s10MkuONWG0wIQhgIChYVAGnFLvSpp2/aQEq3xrRSETxsixUIjsZyWWROkuA0IFnc8d7AmcnUBvRW7FT/5thWyk5agdYUGZ+7C1o69ihR1YxmoGh69fLMPIEOhYh572+3ckgl2SaV4uo9Gvkz8MMGRBcMIMlRirSwhCfozV2RyT5Wn1NgPpyc8zJL7QdOhL7Qxb+5WjnCVrQYHI2cCAwEAAQ==" - }, - }); - - let creation_res = dispatch(&client, body, &logger); - let creation_result = creation_res.get("result").unwrap(); - let account_obj = creation_result.get("account").unwrap(); - let account_id = account_obj.get("account_id").unwrap().as_str().unwrap(); - assert_eq!(creation_res.get("jsonrpc").unwrap(), "2.0"); - - // assign next subaddress for account - let body = json!({ - "jsonrpc": "2.0", - "id": 1, - "method": "assign_address_for_account", - "params": { - "account_id": account_id, - "metadata": "subaddress_index_2", - } - }); - let res = dispatch(&client, body, &logger); - let error = res.get("error").unwrap(); - let data = error.get("data").unwrap(); - let details = data.get("details").unwrap(); - assert!(details.to_string().contains(&subaddress_error.to_string())); - } - - #[test_with_logger] - fn test_import_account_with_next_subaddress_index(logger: Logger) { - let mut rng: StdRng = SeedableRng::from_seed([20u8; 32]); - let (client, mut ledger_db, db_ctx, _network_state) = setup(&mut rng, logger.clone()); - - // create an account - let body = json!({ - "jsonrpc": "2.0", - "id": 1, - "method": "import_account_from_legacy_root_entropy", - "params": { - "entropy": "c593274dc6f6eb94242e34ae5f0ab16bc3085d45d49d9e18b8a8c6f057e6b56b", - "name": "Alice Main Account", - } - }); - let res = dispatch(&client, body, &logger); - let result = res.get("result").unwrap(); - let account_obj = result.get("account").unwrap(); - let account_id = account_obj.get("account_id").unwrap().as_str().unwrap(); - - // assign next subaddress for account - let body = json!({ - "jsonrpc": "2.0", - "id": 1, - "method": "assign_address_for_account", - "params": { - "account_id": account_id, - "metadata": "subaddress_index_2", - } - }); - let res = dispatch(&client, body, &logger); - let result = res.get("result").unwrap(); - let address = result.get("address").unwrap(); - let b58_public_address = address.get("public_address").unwrap().as_str().unwrap(); - let public_address = b58_decode_public_address(b58_public_address).unwrap(); - - // Add a block to fund account at the new subaddress. - add_block_to_ledger_db( - &mut ledger_db, - &vec![public_address], - 100000000000000, // 100.0 MOB - &vec![KeyImage::from(rng.next_u64())], - &mut rng, - ); - - manually_sync_account( - &ledger_db, - &db_ctx.get_db_instance(logger.clone()), - &AccountID(account_id.to_string()), - &logger, - ); - assert_eq!(ledger_db.num_blocks().unwrap(), 13); - - let body = json!({ - "jsonrpc": "2.0", - "id": 1, - "method": "get_balance_for_account", - "params": { - "account_id": account_id, - } - }); - let res = dispatch(&client, body, &logger); - let result = res.get("result").unwrap(); - let balance = result.get("balance").unwrap(); - let unspent_pmob = balance.get("unspent_pmob").unwrap().as_str().unwrap(); - - assert_eq!("100000000000000", unspent_pmob); - - let body = json!({ - "jsonrpc": "2.0", - "id": 1, - "method": "remove_account", - "params": { - "account_id": account_id, - } - }); - dispatch(&client, body, &logger); - - let body = json!({ - "jsonrpc": "2.0", - "id": 1, - "method": "import_account_from_legacy_root_entropy", - "params": { - "entropy": "c593274dc6f6eb94242e34ae5f0ab16bc3085d45d49d9e18b8a8c6f057e6b56b", - "name": "Alice Main Account", - } - }); - dispatch(&client, body, &logger); - - manually_sync_account( - &ledger_db, - &db_ctx.get_db_instance(logger.clone()), - &AccountID(account_id.to_string()), - &logger, - ); - let body = json!({ - "jsonrpc": "2.0", - "id": 1, - "method": "get_balance_for_account", - "params": { - "account_id": account_id, - } - }); - let res = dispatch(&client, body, &logger); - let result = res.get("result").unwrap(); - let balance = result.get("balance").unwrap(); - let unspent_pmob = balance.get("unspent_pmob").unwrap().as_str().unwrap(); - let orphaned_pmob = balance.get("orphaned_pmob").unwrap().as_str().unwrap(); - let spent_pmob = balance.get("spent_pmob").unwrap().as_str().unwrap(); - - assert_eq!("0", unspent_pmob); - assert_eq!("100000000000000", orphaned_pmob); - assert_eq!("0", spent_pmob); - - // assign next subaddress for account - let body = json!({ - "jsonrpc": "2.0", - "id": 1, - "method": "assign_address_for_account", - "params": { - "account_id": account_id, - "metadata": "subaddress_index_2", - } - }); - dispatch(&client, body, &logger); - - let body = json!({ - "jsonrpc": "2.0", - "id": 1, - "method": "get_balance_for_account", - "params": { - "account_id": account_id, - } - }); - let res = dispatch(&client, body, &logger); - let result = res.get("result").unwrap(); - let balance = result.get("balance").unwrap(); - let unspent_pmob = balance.get("unspent_pmob").unwrap().as_str().unwrap(); - let orphaned_pmob = balance.get("orphaned_pmob").unwrap().as_str().unwrap(); - - assert_eq!("100000000000000", unspent_pmob); - assert_eq!("0", orphaned_pmob); - - let body = json!({ - "jsonrpc": "2.0", - "id": 1, - "method": "remove_account", - "params": { - "account_id": account_id, - } - }); - dispatch(&client, body, &logger); - - let body = json!({ - "jsonrpc": "2.0", - "id": 1, - "method": "import_account_from_legacy_root_entropy", - "params": { - "entropy": "c593274dc6f6eb94242e34ae5f0ab16bc3085d45d49d9e18b8a8c6f057e6b56b", - "name": "Alice Main Account", - "next_subaddress_index": "3", - } - }); - dispatch(&client, body, &logger); - - manually_sync_account( - &ledger_db, - &db_ctx.get_db_instance(logger.clone()), - &AccountID(account_id.to_string()), - &logger, - ); - - let body = json!({ - "jsonrpc": "2.0", - "id": 1, - "method": "get_balance_for_account", - "params": { - "account_id": account_id, - } - }); - let res = dispatch(&client, body, &logger); - let result = res.get("result").unwrap(); - let balance = result.get("balance").unwrap(); - let unspent_pmob = balance.get("unspent_pmob").unwrap().as_str().unwrap(); - let orphaned_pmob = balance.get("orphaned_pmob").unwrap().as_str().unwrap(); - - assert_eq!("100000000000000", unspent_pmob); - assert_eq!("0", orphaned_pmob); - } - - #[test_with_logger] - fn test_send_txo_received_from_removed_account(logger: Logger) { - use crate::db::schema::txos; - use diesel::{dsl::count, prelude::*}; - - let mut rng: StdRng = SeedableRng::from_seed([20u8; 32]); - let (client, mut ledger_db, db_ctx, _network_state) = setup(&mut rng, logger.clone()); - - let wallet_db = db_ctx.get_db_instance(logger.clone()); - - // Add three accounts. - let body = json!({ - "jsonrpc": "2.0", - "id": 1, - "method": "create_account", - "params": { - "name": "account 1", - } - }); - let res = dispatch(&client, body, &logger); - let result = res.get("result").unwrap(); - let account_obj = result.get("account").unwrap(); - let account_id_1 = account_obj.get("account_id").unwrap().as_str().unwrap(); - let b58_public_address_1 = account_obj.get("main_address").unwrap().as_str().unwrap(); - let public_address_1 = b58_decode_public_address(b58_public_address_1).unwrap(); - - let body = json!({ - "jsonrpc": "2.0", - "id": 1, - "method": "create_account", - "params": { - "name": "account 2", - } - }); - let res = dispatch(&client, body, &logger); - let result = res.get("result").unwrap(); - let account_obj = result.get("account").unwrap(); - let account_id_2 = account_obj.get("account_id").unwrap().as_str().unwrap(); - let b58_public_address_2 = account_obj.get("main_address").unwrap().as_str().unwrap(); - - let body = json!({ - "jsonrpc": "2.0", - "id": 1, - "method": "create_account", - "params": { - "name": "account 3", - } - }); - let res = dispatch(&client, body, &logger); - let result = res.get("result").unwrap(); - let account_obj = result.get("account").unwrap(); - let account_id_3 = account_obj.get("account_id").unwrap().as_str().unwrap(); - let b58_public_address_3 = account_obj.get("main_address").unwrap().as_str().unwrap(); - - // Add a block to fund account 1. - assert_eq!( - txos::table - .select(count(txos::txo_id_hex)) - .first::(&wallet_db.get_conn().unwrap()) - .unwrap(), - 0 - ); - add_block_to_ledger_db( - &mut ledger_db, - &vec![public_address_1], - 100000000000000, // 100.0 MOB - &vec![KeyImage::from(rng.next_u64())], - &mut rng, - ); - - manually_sync_account( - &ledger_db, - &wallet_db, - &AccountID(account_id_1.to_string()), - &logger, - ); - assert_eq!( - txos::table - .select(count(txos::txo_id_hex)) - .first::(&wallet_db.get_conn().unwrap()) - .unwrap(), - 1 - ); - - // Send some coins to account 2. - let body = json!({ - "jsonrpc": "2.0", - "id": 1, - "method": "build_transaction", - "params": { - "account_id": account_id_1, - "recipient_public_address": b58_public_address_2, - "value_pmob": "84000000000000", // 84.0 MOB - } - }); - let res = dispatch(&client, body, &logger); - let result = res.get("result").unwrap(); - let tx_proposal = result.get("tx_proposal").unwrap(); - - let body = json!({ - "jsonrpc": "2.0", - "id": 1, - "method": "submit_transaction", - "params": { - "tx_proposal": tx_proposal, - "account_id": account_id_1, - } - }); - let res = dispatch(&client, body, &logger); - let result = res.get("result"); - assert!(result.is_some()); - - let json_tx_proposal: json_rpc::tx_proposal::TxProposal = - serde_json::from_value(tx_proposal.clone()).unwrap(); - let payments_tx_proposal = - mc_mobilecoind::payments::TxProposal::try_from(&json_tx_proposal).unwrap(); - - add_block_with_tx_proposal(&mut ledger_db, payments_tx_proposal); - - manually_sync_account( - &ledger_db, - &wallet_db, - &AccountID(account_id_2.to_string()), - &logger, - ); - assert_eq!( - txos::table - .select(count(txos::txo_id_hex)) - .first::(&wallet_db.get_conn().unwrap()) - .unwrap(), - 3 - ); - - // Remove account 1. - let body = json!({ - "jsonrpc": "2.0", - "id": 2, - "method": "remove_account", - "params": { - "account_id": account_id_1, - } - }); - let res = dispatch(&client, body, &logger); - let result = res.get("result").unwrap(); - assert_eq!(result["removed"].as_bool().unwrap(), true,); - assert_eq!( - txos::table - .select(count(txos::txo_id_hex)) - .first::(&wallet_db.get_conn().unwrap()) - .unwrap(), - 1 - ); - - // Send coins from account 2 to account 3. - let body = json!({ - "jsonrpc": "2.0", - "id": 1, - "method": "build_transaction", - "params": { - "account_id": account_id_2, - "recipient_public_address": b58_public_address_3, - "value_pmob": "42000000000000", // 42.0 MOB - } - }); - let res = dispatch(&client, body, &logger); - let result = res.get("result").unwrap(); - let tx_proposal = result.get("tx_proposal").unwrap(); - - let body = json!({ - "jsonrpc": "2.0", - "id": 1, - "method": "submit_transaction", - "params": { - "tx_proposal": tx_proposal, - "account_id": account_id_2, - } - }); - let res = dispatch(&client, body, &logger); - let result = res.get("result"); - assert!(result.is_some()); - - let json_tx_proposal: json_rpc::tx_proposal::TxProposal = - serde_json::from_value(tx_proposal.clone()).unwrap(); - let payments_tx_proposal = - mc_mobilecoind::payments::TxProposal::try_from(&json_tx_proposal).unwrap(); - - add_block_with_tx_proposal(&mut ledger_db, payments_tx_proposal); - - manually_sync_account( - &ledger_db, - &wallet_db, - &AccountID(account_id_3.to_string()), - &logger, - ); - assert_eq!( - txos::table - .select(count(txos::txo_id_hex)) - .first::(&wallet_db.get_conn().unwrap()) - .unwrap(), - 3 - ); - - // Check that account 3 received its coins. - let body = json!({ - "jsonrpc": "2.0", - "id": 1, - "method": "get_balance_for_account", - "params": { - "account_id": account_id_3, - } - }); - let res = dispatch(&client, body, &logger); - let result = res.get("result").unwrap(); - let balance_status = result.get("balance").unwrap(); - let unspent = balance_status["unspent_pmob"].as_str().unwrap(); - assert_eq!(unspent, "42000000000000"); // 42.0 MOB - } - - #[test_with_logger] - fn test_create_assigned_subaddress(logger: Logger) { - let mut rng: StdRng = SeedableRng::from_seed([20u8; 32]); - let (client, mut ledger_db, db_ctx, _network_state) = setup(&mut rng, logger.clone()); - - // Add an account - let body = json!({ - "jsonrpc": "2.0", - "id": 1, - "method": "create_account", - "params": { - "name": "Alice Main Account", - } - }); - let res = dispatch(&client, body, &logger); - let result = res.get("result").unwrap(); - let account_id = result - .get("account") - .unwrap() - .get("account_id") - .unwrap() - .as_str() - .unwrap(); - - // Create a subaddress - let body = json!({ - "jsonrpc": "2.0", - "id": 1, - "method": "assign_address_for_account", - "params": { - "account_id": account_id, - "comment": "For Bob", - } - }); - let res = dispatch(&client, body, &logger); - let result = res.get("result").unwrap(); - let b58_public_address = result - .get("address") - .unwrap() - .get("public_address") - .unwrap() - .as_str() - .unwrap(); - let from_bob_public_address = b58_decode_public_address(b58_public_address).unwrap(); - - // Add a block to the ledger with a transaction "From Bob" - add_block_to_ledger_db( - &mut ledger_db, - &vec![from_bob_public_address], - 42000000000000, - &vec![KeyImage::from(rng.next_u64())], - &mut rng, - ); - - manually_sync_account( - &ledger_db, - &db_ctx.get_db_instance(logger.clone()), - &AccountID(account_id.to_string()), - &logger, - ); - - let body = json!({ - "jsonrpc": "2.0", - "id": 1, - "method": "get_txos_for_account", - "params": { - "account_id": account_id, - } - }); - let res = dispatch(&client, body, &logger); - let result = res.get("result").unwrap(); - let txos = result.get("txo_ids").unwrap().as_array().unwrap(); - assert_eq!(txos.len(), 1); - let txo_map = result.get("txo_map").unwrap().as_object().unwrap(); - let txo = &txo_map.get(txos[0].as_str().unwrap()).unwrap(); - let status_map = txo - .get("account_status_map") - .unwrap() - .as_object() - .unwrap() - .get(account_id) - .unwrap(); - let txo_status = status_map.get("txo_status").unwrap().as_str().unwrap(); - assert_eq!(txo_status, TXO_STATUS_UNSPENT); - let txo_type = status_map.get("txo_type").unwrap().as_str().unwrap(); - assert_eq!(txo_type, TXO_TYPE_RECEIVED); - let value = txo.get("value_pmob").unwrap().as_str().unwrap(); - assert_eq!(value, "42000000000000"); - } - - #[test_with_logger] - fn test_get_address_for_account(logger: Logger) { - let mut rng: StdRng = SeedableRng::from_seed([20u8; 32]); - let (client, _ledger_db, _db_ctx, _network_state) = setup(&mut rng, logger.clone()); - - // Add an account - let body = json!({ - "jsonrpc": "2.0", - "id": 1, - "method": "create_account", - "params": { - "name": "Alice Main Account", - } - }); - let res = dispatch(&client, body, &logger); - let result = res.get("result").unwrap(); - let account_id = result - .get("account") - .unwrap() - .get("account_id") - .unwrap() - .as_str() - .unwrap(); - - let body = json!({ - "jsonrpc": "2.0", - "id": 1, - "method": "get_address_for_account", - "params": { - "account_id": account_id, - "index": 2, - } - }); - let res = dispatch(&client, body, &logger); - let error = res.get("error").unwrap(); - let code = error.get("code").unwrap(); - assert_eq!(code, -32603); - - // Create a subaddress - let body = json!({ - "jsonrpc": "2.0", - "id": 1, - "method": "assign_address_for_account", - "params": { - "account_id": account_id, - "comment": "test", - } - }); - dispatch(&client, body, &logger); - - let body = json!({ - "jsonrpc": "2.0", - "id": 1, - "method": "get_address_for_account", - "params": { - "account_id": account_id, - "index": 2, - } - }); - let res = dispatch(&client, body, &logger); - let result = res.get("result").unwrap(); - let address = result.get("address").unwrap(); - let subaddress_index = address.get("subaddress_index").unwrap().as_str().unwrap(); - - assert_eq!(subaddress_index, "2"); - } - - #[test_with_logger] - fn test_verify_address(logger: Logger) { - let mut rng: StdRng = SeedableRng::from_seed([20u8; 32]); - let (client, _ledger_db, _db_ctx, _network_state) = setup(&mut rng, logger.clone()); - - // Add an account - let body = json!({ - "jsonrpc": "2.0", - "id": 1, - "method": "verify_address", - "params": { - "address": "NOTVALIDB58", - } - }); - let res = dispatch(&client, body, &logger); - let result = res["result"]["verified"].as_bool().unwrap(); - assert!(!result); - - // Add an account - let body = json!({ - "jsonrpc": "2.0", - "id": 1, - "method": "create_account", - "params": { - "name": "Alice Main Account", - } - }); - let res = dispatch(&client, body, &logger); - let b58_public_address = res["result"]["account"]["main_address"].as_str().unwrap(); - - let body = json!({ - "jsonrpc": "2.0", - "id": 1, - "method": "verify_address", - "params": { - "address": b58_public_address, - } - }); - let res = dispatch(&client, body, &logger); - let result = res["result"]["verified"].as_bool().unwrap(); - assert!(result); - } - - #[test_with_logger] - fn test_balance_for_address(logger: Logger) { - let mut rng: StdRng = SeedableRng::from_seed([20u8; 32]); - let (client, mut ledger_db, db_ctx, _network_state) = setup(&mut rng, logger.clone()); - - // Add an account - let body = json!({ - "jsonrpc": "2.0", - "api_version": "2", - "id": 1, - "method": "create_account", - "params": { - "name": "Alice Main Account", - } - }); - let res = dispatch(&client, body, &logger); - let account_id = res["result"]["account"]["account_id"].as_str().unwrap(); - let b58_public_address = res["result"]["account"]["main_address"].as_str().unwrap(); - - let alice_public_address = b58_decode_public_address(&b58_public_address) - .expect("Could not b58_decode public address"); - add_block_to_ledger_db( - &mut ledger_db, - &vec![alice_public_address], - 42 * MOB, - &vec![KeyImage::from(rng.next_u64())], - &mut rng, - ); - // - manually_sync_account( - &ledger_db, - &db_ctx.get_db_instance(logger.clone()), - &AccountID(account_id.to_string()), - &logger, - ); - - let body = json!({ - "jsonrpc": "2.0", - "api_version": "2", - "id": 1, - "method": "get_balance_for_address", - "params": { - "address": b58_public_address, - } - }); - let res = dispatch(&client, body, &logger); - let balance = res["result"]["balance"].clone(); - assert_eq!( - balance["unspent_pmob"] - .as_str() - .unwrap() - .parse::() - .expect("Could not parse u64"), - 42 * MOB - ); - assert_eq!( - balance["pending_pmob"] - .as_str() - .unwrap() - .parse::() - .expect("Could not parse u64"), - 0 - ); - assert_eq!( - balance["spent_pmob"] - .as_str() - .unwrap() - .parse::() - .expect("Could not parse u64"), - 0 - ); - assert_eq!( - balance["secreted_pmob"] - .as_str() - .unwrap() - .parse::() - .expect("Could not parse u64"), - 0 - ); - assert_eq!( - balance["orphaned_pmob"] - .as_str() - .unwrap() - .parse::() - .expect("Could not parse u64"), - 0 - ); - - // Create a subaddress - let body = json!({ - "jsonrpc": "2.0", - "api_version": "2", - "id": 1, - "method": "assign_address_for_account", - "params": { - "account_id": account_id, - "comment": "For Bob", - } - }); - let res = dispatch(&client, body, &logger); - let result = res.get("result").unwrap(); - let from_bob_b58_public_address = result - .get("address") - .unwrap() - .get("public_address") - .unwrap() - .as_str() - .unwrap(); - let from_bob_public_address = - b58_decode_public_address(from_bob_b58_public_address).unwrap(); - - // Add a block to the ledger with a transaction "From Bob" - add_block_to_ledger_db( - &mut ledger_db, - &vec![from_bob_public_address], - 64 * MOB, - &vec![KeyImage::from(rng.next_u64())], - &mut rng, - ); - // - manually_sync_account( - &ledger_db, - &db_ctx.get_db_instance(logger.clone()), - &AccountID(account_id.to_string()), - &logger, - ); - - let body = json!({ - "jsonrpc": "2.0", - "api_version": "2", - "id": 1, - "method": "get_balance_for_address", - "params": { - "address": from_bob_b58_public_address, - } - }); - let res = dispatch(&client, body, &logger); - let balance = res["result"]["balance"].clone(); - assert_eq!( - balance["unspent_pmob"] - .as_str() - .unwrap() - .parse::() - .expect("Could not parse u64"), - 64 * MOB - ); - } - - /// This test is intended to make sure that when a subaddress is assigned - /// that it correctly generates and checks the key image against the ledger - /// db to see if the previously orphaned txo has been spent or not - #[test_with_logger] - fn test_mark_orphaned_txo_as_spent(logger: Logger) { - let mut rng: StdRng = SeedableRng::from_seed([20u8; 32]); - let (client, mut ledger_db, db_ctx, _network_state) = setup(&mut rng, logger.clone()); - - // Add an account - let body = json!({ - "jsonrpc": "2.0", - "id": 1, - "method": "import_account", - "params": { - "mnemonic": "sheriff odor square mistake huge skate mouse shoot purity weapon proof stuff correct concert blanket neck own shift clay mistake air viable stick group", - "key_derivation_version": "2", - "name": "Alice Main Account", - } - }); - let res = dispatch(&client, body, &logger); - let result = res.get("result").unwrap(); - let account_obj = result.get("account").unwrap(); - let account_id = account_obj.get("account_id").unwrap().as_str().unwrap(); - - // Assign next subaddress for account. - let body = json!({ - "jsonrpc": "2.0", - "id": 1, - "method": "assign_address_for_account", - "params": { - "account_id": account_id, - "metadata": "subaddress_index_2", - } - }); - let res = dispatch(&client, body, &logger); - let result = res.get("result").unwrap(); - let address = result.get("address").unwrap(); - let b58_public_address = address.get("public_address").unwrap().as_str().unwrap(); - let public_address = b58_decode_public_address(b58_public_address).unwrap(); - - // Add a block to fund account at the new subaddress. - add_block_to_ledger_db( - &mut ledger_db, - &vec![public_address.clone()], - 100000000000000, // 100.0 MOB - &vec![KeyImage::from(rng.next_u64())], - &mut rng, - ); - - add_block_to_ledger_db( - &mut ledger_db, - &vec![public_address.clone()], - 500000000000000, // 500.0 MOB - &vec![KeyImage::from(rng.next_u64())], - &mut rng, - ); - - manually_sync_account( - &ledger_db, - &db_ctx.get_db_instance(logger.clone()), - &AccountID(account_id.to_string()), - &logger, - ); - - // Remove the account. - let body = json!({ - "jsonrpc": "2.0", - "id": 2, - "method": "remove_account", - "params": { - "account_id": *account_id, - } - }); - let res = dispatch(&client, body, &logger); - let result = res.get("result").unwrap(); - assert_eq!(result["removed"].as_bool().unwrap(), true,); - - // Add the same account back. - let body = json!({ - "jsonrpc": "2.0", - "id": 1, - "method": "import_account", - "params": { - "mnemonic": "sheriff odor square mistake huge skate mouse shoot purity weapon proof stuff correct concert blanket neck own shift clay mistake air viable stick group", - "key_derivation_version": "2", - "name": "Alice Main Account", - } - }); - let res = dispatch(&client, body, &logger); - let result = res.get("result").unwrap(); - let account_obj = result.get("account").unwrap(); - let account_id = account_obj.get("account_id").unwrap().as_str().unwrap(); - - manually_sync_account( - &ledger_db, - &db_ctx.get_db_instance(logger.clone()), - &AccountID(account_id.to_string()), - &logger, - ); - - let body = json!({ - "jsonrpc": "2.0", - "id": 1, - "method": "get_balance_for_account", - "params": { - "account_id": *account_id, - } - }); - let res = dispatch(&client, body, &logger); - let result = res.get("result").unwrap(); - let balance = result.get("balance").unwrap(); - assert_eq!(balance.get("unspent_pmob").unwrap(), "0"); - assert_eq!(balance.get("spent_pmob").unwrap(), "0"); - assert_eq!(balance.get("orphaned_pmob").unwrap(), "600000000000000"); - - // Verify orphaned txos. - let body = json!({ - "jsonrpc": "2.0", - "id": 2, - "method": "get_txos_for_account", - "params": { - "account_id": *account_id, - "status": "txo_status_orphaned" - } - }); - let res = dispatch(&client, body, &logger); - let result = res.get("result").unwrap(); - assert_eq!(result["txo_ids"].as_array().unwrap().len(), 2,); - let body = json!({ - "jsonrpc": "2.0", - "id": 2, - "method": "get_txos_for_account", - "params": { - "account_id": *account_id, - "status": "txo_status_unspent" - } - }); - let res = dispatch(&client, body, &logger); - let result = res.get("result").unwrap(); - assert_eq!(result["txo_ids"].as_array().unwrap().len(), 0); - - // Add back next subaddress. Txos are detected as unspent. - let body = json!({ - "jsonrpc": "2.0", - "id": 1, - "method": "assign_address_for_account", - "params": { - "account_id": account_id, - "metadata": "subaddress_index_2", - } - }); - dispatch(&client, body, &logger); - - let body = json!({ - "jsonrpc": "2.0", - "id": 1, - "method": "get_balance_for_account", - "params": { - "account_id": *account_id, - } - }); - let res = dispatch(&client, body, &logger); - let result = res.get("result").unwrap(); - let balance = result.get("balance").unwrap(); - assert_eq!(balance.get("unspent_pmob").unwrap(), "600000000000000"); - assert_eq!(balance.get("spent_pmob").unwrap(), "0"); - assert_eq!(balance.get("orphaned_pmob").unwrap(), "0"); - - // Verify unspent txos. - let body = json!({ - "jsonrpc": "2.0", - "id": 2, - "method": "get_txos_for_account", - "params": { - "account_id": *account_id, - "status": "txo_status_unspent" - } - }); - let res = dispatch(&client, body, &logger); - let result = res.get("result").unwrap(); - assert_eq!(result["txo_ids"].as_array().unwrap().len(), 2,); - let body = json!({ - "jsonrpc": "2.0", - "id": 2, - "method": "get_txos_for_account", - "params": { - "account_id": *account_id, - "status": "txo_status_orphaned" - } - }); - let res = dispatch(&client, body, &logger); - let result = res.get("result").unwrap(); - assert_eq!(result["txo_ids"].as_array().unwrap().len(), 0); - - // Create a second account. - let body = json!({ - "jsonrpc": "2.0", - "id": 1, - "method": "create_account", - "params": { - "name": "account 2", - } - }); - let res = dispatch(&client, body, &logger); - let result = res.get("result").unwrap(); - let account_obj = result.get("account").unwrap(); - let account_id_2 = account_obj.get("account_id").unwrap().as_str().unwrap(); - let b58_public_address_2 = account_obj.get("main_address").unwrap().as_str().unwrap(); - - // Remove the second Account - let body = json!({ - "jsonrpc": "2.0", - "id": 2, - "method": "remove_account", - "params": { - "account_id": *account_id_2, - } - }); - let res = dispatch(&client, body, &logger); - let result = res.get("result").unwrap(); - assert_eq!(result["removed"].as_bool().unwrap(), true,); - - // Send some coins to the removed second account. - let body = json!({ - "jsonrpc": "2.0", - "id": 1, - "method": "build_transaction", - "params": { - "account_id": account_id, - "recipient_public_address": b58_public_address_2, - "value_pmob": "50000000000000", // 50.0 MOB - } - }); - let res = dispatch(&client, body, &logger); - let result = res.get("result").unwrap(); - let tx_proposal = result.get("tx_proposal").unwrap(); - - let body = json!({ - "jsonrpc": "2.0", - "id": 1, - "method": "submit_transaction", - "params": { - "tx_proposal": tx_proposal - } - }); - let res = dispatch(&client, body, &logger); - let result = res.get("result"); - assert!(result.is_some()); - - let json_tx_proposal: json_rpc::tx_proposal::TxProposal = - serde_json::from_value(tx_proposal.clone()).unwrap(); - let payments_tx_proposal = - mc_mobilecoind::payments::TxProposal::try_from(&json_tx_proposal).unwrap(); - - add_block_with_tx_proposal(&mut ledger_db, payments_tx_proposal); - - manually_sync_account( - &ledger_db, - &db_ctx.get_db_instance(logger.clone()), - &AccountID(account_id.to_string()), - &logger, - ); - - // The first account shows the coins are spent. - let body = json!({ - "jsonrpc": "2.0", - "id": 1, - "method": "get_balance_for_account", - "params": { - "account_id": *account_id, - } - }); - let res = dispatch(&client, body, &logger); - let result = res.get("result").unwrap(); - let balance = result.get("balance").unwrap(); - assert_eq!(balance.get("unspent_pmob").unwrap(), "549999600000000"); - assert_eq!(balance.get("spent_pmob").unwrap(), "100000000000000"); - assert_eq!(balance.get("orphaned_pmob").unwrap(), "0"); - - // Remove the first account and add it back again. - let body = json!({ - "jsonrpc": "2.0", - "id": 2, - "method": "remove_account", - "params": { - "account_id": *account_id, - } - }); - let res = dispatch(&client, body, &logger); - let result = res.get("result").unwrap(); - assert_eq!(result["removed"].as_bool().unwrap(), true,); - - let body = json!({ - "jsonrpc": "2.0", - "id": 1, - "method": "import_account", - "params": { - "mnemonic": "sheriff odor square mistake huge skate mouse shoot purity weapon proof stuff correct concert blanket neck own shift clay mistake air viable stick group", - "key_derivation_version": "2", - "name": "Alice Main Account", - } - }); - let res = dispatch(&client, body, &logger); - let result = res.get("result").unwrap(); - let account_obj = result.get("account").unwrap(); - let account_id = account_obj.get("account_id").unwrap().as_str().unwrap(); - - manually_sync_account( - &ledger_db, - &db_ctx.get_db_instance(logger.clone()), - &AccountID(account_id.to_string()), - &logger, - ); - - // The unspent pmob shows what wasn't sent to the second account. - // The orphaned pmob are because we haven't added back the next subaddress. - let body = json!({ - "jsonrpc": "2.0", - "id": 1, - "method": "get_balance_for_account", - "params": { - "account_id": *account_id, - } - }); - let res = dispatch(&client, body, &logger); - let result = res.get("result").unwrap(); - let balance = result.get("balance").unwrap(); - assert_eq!(balance.get("unspent_pmob").unwrap(), "49999600000000"); - assert_eq!(balance.get("spent_pmob").unwrap(), "0"); - assert_eq!(balance.get("orphaned_pmob").unwrap(), "600000000000000"); - } - - #[test_with_logger] - fn test_get_txos(logger: Logger) { - let mut rng: StdRng = SeedableRng::from_seed([20u8; 32]); - let (client, mut ledger_db, db_ctx, _network_state) = setup(&mut rng, logger.clone()); - - // Add an account - let body = json!({ - "jsonrpc": "2.0", - "id": 1, - "method": "create_account", - "params": { - "name": "Alice Main Account", - } - }); - let res = dispatch(&client, body, &logger); - let result = res.get("result").unwrap(); - let account_obj = result.get("account").unwrap(); - let account_id = account_obj.get("account_id").unwrap().as_str().unwrap(); - let b58_public_address = account_obj.get("main_address").unwrap().as_str().unwrap(); - let public_address = b58_decode_public_address(b58_public_address).unwrap(); - - // Add a block with a txo for this address - add_block_to_ledger_db( - &mut ledger_db, - &vec![public_address], - 100, - &vec![KeyImage::from(rng.next_u64())], - &mut rng, - ); - - manually_sync_account( - &ledger_db, - &db_ctx.get_db_instance(logger.clone()), - &AccountID(account_id.to_string()), - &logger, - ); - - let body = json!({ - "jsonrpc": "2.0", - "id": 1, - "method": "get_txos_for_account", - "params": { - "account_id": account_id, - } - }); - let res = dispatch(&client, body, &logger); - let result = res.get("result").unwrap(); - let txos = result.get("txo_ids").unwrap().as_array().unwrap(); - assert_eq!(txos.len(), 1); - let txo_map = result.get("txo_map").unwrap().as_object().unwrap(); - let txo = txo_map.get(txos[0].as_str().unwrap()).unwrap(); - let account_status_map = txo - .get("account_status_map") - .unwrap() - .as_object() - .unwrap() - .get(account_id) - .unwrap(); - let txo_status = account_status_map - .get("txo_status") - .unwrap() - .as_str() - .unwrap(); - assert_eq!(txo_status, TXO_STATUS_UNSPENT); - let txo_type = account_status_map - .get("txo_type") - .unwrap() - .as_str() - .unwrap(); - assert_eq!(txo_type, TXO_TYPE_RECEIVED); - let value = txo.get("value_pmob").unwrap().as_str().unwrap(); - assert_eq!(value, "100"); - - // Check the overall balance for the account - let body = json!({ - "jsonrpc": "2.0", - "id": 1, - "method": "get_balance_for_account", - "params": { - "account_id": account_id, - } - }); - let res = dispatch(&client, body, &logger); - let result = res.get("result").unwrap(); - let balance_status = result.get("balance").unwrap(); - let unspent = balance_status["unspent_pmob"].as_str().unwrap(); - assert_eq!(unspent, "100"); - } - - #[test_with_logger] - fn test_split_txo(logger: Logger) { - let mut rng: StdRng = SeedableRng::from_seed([20u8; 32]); - let (client, mut ledger_db, db_ctx, _network_state) = setup(&mut rng, logger.clone()); - - // Add an account - let body = json!({ - "jsonrpc": "2.0", - "id": 1, - "method": "create_account", - "params": { - "name": "Alice Main Account", - } - }); - let res = dispatch(&client, body, &logger); - let result = res.get("result").unwrap(); - let account_obj = result.get("account").unwrap(); - let account_id = account_obj.get("account_id").unwrap().as_str().unwrap(); - let b58_public_address = account_obj.get("main_address").unwrap().as_str().unwrap(); - let public_address = b58_decode_public_address(b58_public_address).unwrap(); - - // Add a block with a txo for this address - add_block_to_ledger_db( - &mut ledger_db, - &vec![public_address], - 250000000000, - &vec![KeyImage::from(rng.next_u64())], - &mut rng, - ); - - manually_sync_account( - &ledger_db, - &db_ctx.get_db_instance(logger.clone()), - &AccountID(account_id.to_string()), - &logger, - ); - - let body = json!({ - "jsonrpc": "2.0", - "id": 1, - "method": "get_txos_for_account", - "params": { - "account_id": account_id, - } - }); - let res = dispatch(&client, body, &logger); - let result = res.get("result").unwrap(); - let txos = result.get("txo_ids").unwrap().as_array().unwrap(); - assert_eq!(txos.len(), 1); - let txo_map = result.get("txo_map").unwrap().as_object().unwrap(); - let txo = txo_map.get(txos[0].as_str().unwrap()).unwrap(); - let account_status_map = txo - .get("account_status_map") - .unwrap() - .as_object() - .unwrap() - .get(account_id) - .unwrap(); - let txo_status = account_status_map - .get("txo_status") - .unwrap() - .as_str() - .unwrap(); - assert_eq!(txo_status, TXO_STATUS_UNSPENT); - let txo_type = account_status_map - .get("txo_type") - .unwrap() - .as_str() - .unwrap(); - assert_eq!(txo_type, TXO_TYPE_RECEIVED); - let value = txo.get("value_pmob").unwrap().as_str().unwrap(); - assert_eq!(value, "250000000000"); - let txo_id = &txos[0]; - - // Check the overall balance for the account - let body = json!({ - "jsonrpc": "2.0", - "id": 1, - "method": "get_balance_for_account", - "params": { - "account_id": account_id, - } - }); - let res = dispatch(&client, body, &logger); - let result = res.get("result").unwrap(); - let balance_status = result.get("balance").unwrap(); - let unspent = balance_status["unspent_pmob"].as_str().unwrap(); - assert_eq!(unspent, "250000000000"); - - let body = json!({ - "jsonrpc": "2.0", - "id": 1, - "method": "build_split_txo_transaction", - "params": { - "txo_id": txo_id, - "output_values": ["20000000000", "80000000000", "30000000000", "70000000000", "40000000000"], - "fee": "10000000000" - } - }); - let res = dispatch(&client, body, &logger); - let result = res.get("result").unwrap(); - let tx_proposal = result.get("tx_proposal").unwrap(); - - let body = json!({ - "jsonrpc": "2.0", - "id": 1, - "method": "submit_transaction", - "params": { - "tx_proposal": tx_proposal, - "account_id": account_id, - } - }); - let res = dispatch(&client, body, &logger); - let result = res.get("result"); - assert!(result.is_some()); - - let json_tx_proposal: json_rpc::tx_proposal::TxProposal = - serde_json::from_value(tx_proposal.clone()).unwrap(); - let payments_tx_proposal = - mc_mobilecoind::payments::TxProposal::try_from(&json_tx_proposal).unwrap(); - - add_block_with_tx_proposal(&mut ledger_db, payments_tx_proposal); - - manually_sync_account( - &ledger_db, - &db_ctx.get_db_instance(logger.clone()), - &AccountID(account_id.to_string()), - &logger, - ); - - // Check the overall balance for the account - let body = json!({ - "jsonrpc": "2.0", - "id": 1, - "method": "get_balance_for_account", - "params": { - "account_id": account_id, - } - }); - let res = dispatch(&client, body, &logger); - let result = res.get("result").unwrap(); - let balance_status = result.get("balance").unwrap(); - let unspent = balance_status["unspent_pmob"].as_str().unwrap(); - assert_eq!(unspent, "240000000000"); - } - - #[test_with_logger] - fn test_receipts(logger: Logger) { - let mut rng: StdRng = SeedableRng::from_seed([20u8; 32]); - let (client, mut ledger_db, db_ctx, _network_state) = setup(&mut rng, logger.clone()); - - // Add an account - let body = json!({ - "jsonrpc": "2.0", - "id": 1, - "method": "create_account", - "params": { - "name": "Alice Main Account", - } - }); - let res = dispatch(&client, body, &logger); - let result = res.get("result").unwrap(); - let account_obj = result.get("account").unwrap(); - let alice_account_id = account_obj.get("account_id").unwrap().as_str().unwrap(); - let alice_b58_public_address = account_obj.get("main_address").unwrap().as_str().unwrap(); - let alice_public_address = b58_decode_public_address(alice_b58_public_address).unwrap(); - - // Add a block with a txo for this address - add_block_to_ledger_db( - &mut ledger_db, - &vec![alice_public_address], - 100 * MOB, - &vec![KeyImage::from(rng.next_u64())], - &mut rng, - ); - - manually_sync_account( - &ledger_db, - &db_ctx.get_db_instance(logger.clone()), - &AccountID(alice_account_id.to_string()), - &logger, - ); - - // Add Bob's account to our wallet - let body = json!({ - "jsonrpc": "2.0", - "id": 1, - "method": "create_account", - "params": { - "name": "Bob Main Account", - } - }); - let res = dispatch(&client, body, &logger); - let result = res.get("result").unwrap(); - let bob_account_obj = result.get("account").unwrap(); - let bob_account_id = bob_account_obj.get("account_id").unwrap().as_str().unwrap(); - let bob_b58_public_address = bob_account_obj - .get("main_address") - .unwrap() - .as_str() - .unwrap(); - - // Construct a transaction proposal from Alice to Bob - let body = json!({ - "jsonrpc": "2.0", - "id": 1, - "method": "build_transaction", - "params": { - "account_id": alice_account_id, - "recipient_public_address": bob_b58_public_address, - "value_pmob": "42000000000000", // 42 MOB - } - }); - let res = dispatch(&client, body, &logger); - let result = res.get("result").unwrap(); - let tx_proposal = result.get("tx_proposal").unwrap(); - - // Get the receipts from the tx_proposal - let body = json!({ - "jsonrpc": "2.0", - "id": 1, - "method": "create_receiver_receipts", - "params": { - "tx_proposal": tx_proposal - } - }); - let res = dispatch(&client, body, &logger); - let result = res.get("result").unwrap(); - let receipts = result["receiver_receipts"].as_array().unwrap(); - assert_eq!(receipts.len(), 1); - let receipt = &receipts[0]; - - // Bob checks status (should be pending before the block is added to the ledger) - let body = json!({ - "jsonrpc": "2.0", - "id": 1, - "method": "check_receiver_receipt_status", - "params": { - "address": bob_b58_public_address, - "receiver_receipt": receipt, - } - }); - let res = dispatch(&client, body, &logger); - let result = res.get("result").unwrap(); - let status = result["receipt_transaction_status"].as_str().unwrap(); - assert_eq!(status, "TransactionPending"); - - // Add the block to the ledger with the tx proposal - let json_tx_proposal: json_rpc::tx_proposal::TxProposal = - serde_json::from_value(tx_proposal.clone()).unwrap(); - let payments_tx_proposal = - mc_mobilecoind::payments::TxProposal::try_from(&json_tx_proposal).unwrap(); - - // The MockBlockchainConnection does not write to the ledger_db - add_block_with_tx_proposal(&mut ledger_db, payments_tx_proposal); - - manually_sync_account( - &ledger_db, - &db_ctx.get_db_instance(logger.clone()), - &AccountID(alice_account_id.to_string()), - &logger, - ); - manually_sync_account( - &ledger_db, - &db_ctx.get_db_instance(logger.clone()), - &AccountID(bob_account_id.to_string()), - &logger, - ); - - // Bob checks status (should be successful after added to the ledger) - let body = json!({ - "jsonrpc": "2.0", - "id": 1, - "method": "check_receiver_receipt_status", - "params": { - "address": bob_b58_public_address, - "receiver_receipt": receipt, - } - }); - let res = dispatch(&client, body, &logger); - let result = res.get("result").unwrap(); - let status = result["receipt_transaction_status"].as_str().unwrap(); - assert_eq!(status, "TransactionSuccess"); - } - - #[test_with_logger] - fn test_gift_codes(logger: Logger) { - let mut rng: StdRng = SeedableRng::from_seed([20u8; 32]); - let (client, mut ledger_db, db_ctx, _network_state) = setup(&mut rng, logger.clone()); - - // Add an account - let body = json!({ - "jsonrpc": "2.0", - "id": 1, - "method": "create_account", - "params": { - "name": "Alice Main Account", - } - }); - let res = dispatch(&client, body, &logger); - let result = res.get("result").unwrap(); - let account_obj = result.get("account").unwrap(); - let alice_account_id = account_obj.get("account_id").unwrap().as_str().unwrap(); - let alice_b58_public_address = account_obj.get("main_address").unwrap().as_str().unwrap(); - let alice_public_address = b58_decode_public_address(alice_b58_public_address).unwrap(); - - // Add a block with a txo for this address - add_block_to_ledger_db( - &mut ledger_db, - &vec![alice_public_address], - 100 * MOB, - &vec![KeyImage::from(rng.next_u64())], - &mut rng, - ); - - manually_sync_account( - &ledger_db, - &db_ctx.get_db_instance(logger.clone()), - &AccountID(alice_account_id.to_string()), - &logger, - ); - // Create a gift code - let body = json!({ - "jsonrpc": "2.0", - "id": 1, - "method": "build_gift_code", - "params": { - "account_id": alice_account_id, - "value_pmob": "42000000000000", - "memo": "Happy Birthday!", - } - }); - let res = dispatch(&client, body, &logger); - let result = res["result"].clone(); - let gift_code_b58 = result["gift_code_b58"].as_str().unwrap(); - let tx_proposal = result["tx_proposal"].clone(); - - // Check the status of the gift code - let body = json!({ - "jsonrpc": "2.0", - "id": 1, - "method": "check_gift_code_status", - "params": { - "gift_code_b58": gift_code_b58, - } - }); - let res = dispatch(&client, body, &logger); - let status = res["result"]["gift_code_status"].as_str().unwrap(); - assert_eq!(status, "GiftCodeSubmittedPending"); - let memo = res["result"]["gift_code_memo"].as_str().unwrap(); - assert_eq!(memo, "Happy Birthday!"); - - // Submit the gift code and tx proposal - let body = json!({ - "jsonrpc": "2.0", - "id": 1, - "method": "submit_gift_code", - "params": { - "from_account_id": alice_account_id, - "gift_code_b58": gift_code_b58, - "tx_proposal": tx_proposal, - } - }); - dispatch(&client, body, &logger); - - // Add the TxProposal for the gift code - let json_tx_proposal: json_rpc::tx_proposal::TxProposal = - serde_json::from_value(tx_proposal.clone()).unwrap(); - let payments_tx_proposal = - mc_mobilecoind::payments::TxProposal::try_from(&json_tx_proposal).unwrap(); - - // The MockBlockchainConnection does not write to the ledger_db - add_block_with_tx_proposal(&mut ledger_db, payments_tx_proposal); - - manually_sync_account( - &ledger_db, - &db_ctx.get_db_instance(logger.clone()), - &AccountID(alice_account_id.to_string()), - &logger, - ); - - // Check the status of the gift code - let body = json!({ - "jsonrpc": "2.0", - "id": 1, - "method": "check_gift_code_status", - "params": { - "gift_code_b58": gift_code_b58, - } - }); - let res = dispatch(&client, body, &logger); - let status = res["result"]["gift_code_status"].as_str().unwrap(); - assert_eq!(status, "GiftCodeAvailable"); - let memo = res["result"]["gift_code_memo"].as_str().unwrap(); - assert_eq!(memo, "Happy Birthday!"); - - // Add Bob's account to our wallet - let body = json!({ - "jsonrpc": "2.0", - "id": 1, - "method": "create_account", - "params": { - "name": "Bob Main Account", - } - }); - let res = dispatch(&client, body, &logger); - let result = res.get("result").unwrap(); - let bob_account_obj = result.get("account").unwrap(); - let bob_account_id = bob_account_obj.get("account_id").unwrap().as_str().unwrap(); - - manually_sync_account( - &ledger_db, - &db_ctx.get_db_instance(logger.clone()), - &AccountID(bob_account_id.to_string()), - &logger, - ); - - // Get all the gift codes in the wallet - let body = json!({ - "jsonrpc": "2.0", - "id": 1, - "method": "get_all_gift_codes", - }); - let res = dispatch(&client, body, &logger); - let result = res["result"]["gift_codes"].as_array().unwrap(); - assert_eq!(result.len(), 1); - - // Get the specific gift code - let body = json!({ - "jsonrpc": "2.0", - "id": 1, - "method": "get_gift_code", - "params": { - "gift_code_b58": gift_code_b58, - } - }); - dispatch(&client, body, &logger); - - // Claim the gift code for bob - let body = json!({ - "jsonrpc": "2.0", - "id": 1, - "method": "claim_gift_code", - "params": { - "account_id": bob_account_id, - "gift_code_b58": gift_code_b58, - } - }); - let res = dispatch(&client, body, &logger); - let txo_id_hex = res["result"]["txo_id"].as_str().unwrap(); - assert_eq!(txo_id_hex.len(), 64); - - // Now remove that gift code - let body = json!({ - "jsonrpc": "2.0", - "id": 1, - "method": "remove_gift_code", - "params": { - "gift_code_b58": gift_code_b58, - } - }); - let res = dispatch(&client, body, &logger); - let result = res["result"]["removed"].as_bool().unwrap(); - assert!(result); - - // Get all the gift codes in the wallet again, should be 0 now - let body = json!({ - "jsonrpc": "2.0", - "id": 1, - "method": "get_all_gift_codes", - }); - let res = dispatch(&client, body, &logger); - let result = res["result"]["gift_codes"].as_array().unwrap(); - assert_eq!(result.len(), 0); - } - - #[test_with_logger] - fn test_request_with_correct_api_key(logger: Logger) { - let api_key = "mobilecats"; - - let mut rng: StdRng = SeedableRng::from_seed([20u8; 32]); - let (client, _ledger_db, _db_ctx, _network_state) = - setup_with_api_key(&mut rng, logger.clone(), api_key.to_string()); - - let body = json!({ - "jsonrpc": "2.0", - "id": 1, - "method": "create_account", - "params": { - "name": "Alice Main Account", - }, - }); - - let header = Header::new("X-API-KEY", api_key); - - dispatch_with_header(&client, body, header, &logger); - } - - #[test_with_logger] - fn test_request_with_bad_api_key(logger: Logger) { - let api_key = "mobilecats"; - - let mut rng: StdRng = SeedableRng::from_seed([20u8; 32]); - let (client, _ledger_db, _db_ctx, _network_state) = - setup_with_api_key(&mut rng, logger.clone(), api_key.to_string()); - - let body = json!({ - "jsonrpc": "2.0", - "id": 1, - "method": "create_account", - "params": { - "name": "Alice Main Account", - }, - }); - - let header = Header::new("X-API-KEY", "wrong-header"); - - dispatch_with_header_expect_error(&client, body, header, &logger, Status::Unauthorized); - } - - #[test_with_logger] - fn test_e2e_view_only_account_flow(logger: Logger) { - // create normal account - let mut rng: StdRng = SeedableRng::from_seed([20u8; 32]); - let (client, mut ledger_db, db_ctx, _network_state) = setup(&mut rng, logger.clone()); - let wallet_db = db_ctx.get_db_instance(logger.clone()); - - // Create Account - let body = json!({ - "jsonrpc": "2.0", - "id": 1, - "method": "create_account", - "params": { - "name": "Alice Main Account", - }, - }); - let res = dispatch(&client, body, &logger); - assert_eq!(res.get("jsonrpc").unwrap(), "2.0"); - - let result = res.get("result").unwrap(); - let account_obj = result.get("account").unwrap(); - assert!(account_obj.get("account_id").is_some()); - assert_eq!(account_obj.get("name").unwrap(), "Alice Main Account"); - let account_id = account_obj.get("account_id").unwrap(); - let main_address = account_obj.get("main_address").unwrap().as_str().unwrap(); - let main_account_address = b58_decode_public_address(main_address).unwrap(); - - // add some funds to that account - add_block_to_ledger_db( - &mut ledger_db, - &vec![main_account_address], - 100 * MOB, - &vec![KeyImage::from(rng.next_u64())], - &mut rng, - ); - manually_sync_account( - &ledger_db, - &db_ctx.get_db_instance(logger.clone()), - &AccountID(account_id.as_str().unwrap().to_string()), - &logger, - ); - - // confirm that the regular account has the correct balance - let body = json!({ - "jsonrpc": "2.0", - "id": 1, - "method": "get_balance_for_account", - "params": { - "account_id": account_id, - }, - }); - let res = dispatch(&client, body, &logger); - let result = res.get("result").unwrap(); - let balance_status = result.get("balance").unwrap(); - let unspent = balance_status["unspent_pmob"].as_str().unwrap(); - assert_eq!(unspent, "100000000000000"); - - // export view only import package - let body = json!({ - "jsonrpc": "2.0", - "id": 1, - "method": "export_view_only_account_package", - "params": { - "account_id": account_id, - }, - }); - let res = dispatch(&client, body, &logger); - assert_eq!(res.get("jsonrpc").unwrap(), "2.0"); - let result = res.get("result").unwrap(); - let request = result.get("json_rpc_request").unwrap(); - - // remove regular account (can't have both view only and regular in same wallet) - let body = json!({ - "jsonrpc": "2.0", - "id": 2, - "method": "remove_account", - "params": { - "account_id": account_id, - } - }); - let res = dispatch(&client, body, &logger); - let result = res.get("result").unwrap(); - assert_eq!(result["removed"].as_bool().unwrap(), true); - - // import vo account - let body = json!(request); - let res = dispatch(&client, body, &logger); - let result = res.get("result").unwrap(); - let account = result.get("view_only_account").unwrap(); - let vo_account_id = account.get("account_id").unwrap(); - assert_eq!(vo_account_id, account_id); - - // sync the view only account - manually_sync_view_only_account( - &ledger_db, - &wallet_db, - vo_account_id.as_str().unwrap(), - &logger, - ); - - // check balance for view only account - let body = json!({ - "jsonrpc": "2.0", - "id": 1, - "method": "get_balance_for_view_only_account", - "params": { - "account_id": account_id, - }, - }); - let res = dispatch(&client, body, &logger); - let result = res.get("result").unwrap(); - let balance_status = result.get("balance").unwrap(); - let unspent = balance_status["unspent_pmob"].as_str().unwrap(); - assert_eq!(unspent, "100000000000000"); - - // test get - let body = json!({ - "jsonrpc": "2.0", - "id": 2, - "method": "get_view_only_account", - "params": { - "account_id": account_id, - } - }); - let res = dispatch(&client, body, &logger); - let result = res.get("result").unwrap(); - let account = result.get("view_only_account").unwrap(); - let vo_account_id = account.get("account_id").unwrap(); - assert_eq!(vo_account_id, account_id); - - // test update name - let name = "Look at these coins"; - let body = json!({ - "jsonrpc": "2.0", - "id": 2, - "method": "update_view_only_account_name", - "params": { - "account_id": account_id, - "name": name, - } - }); - let res = dispatch(&client, body, &logger); - let result = res.get("result").unwrap(); - let account = result.get("view_only_account").unwrap(); - let account_name = account.get("name").unwrap(); - assert_eq!(name, account_name); - - // create new subaddress request - let body = json!({ - "jsonrpc": "2.0", - "id": 1, - "method": "create_new_subaddresses_request", - "params": { - "account_id": account_id, - "num_subaddresses_to_generate": "2", - }, - }); - let res = dispatch(&client, body, &logger); - let result = res.get("result").unwrap(); - let next_index = result - .get("next_subaddress_index") - .unwrap() - .as_str() - .unwrap(); - assert_eq!(next_index, "2"); - - // test creating unsigned tx - let body = json!({ - "jsonrpc": "2.0", - "id": 2, - "method": "build_unsigned_transaction", - "params": { - "account_id": account_id, - "recipient_public_address": main_address, - "value_pmob": "50000000000000", - } - }); - let res = dispatch(&client, body, &logger); - let result = res.get("result").unwrap(); - let _tx = result.get("unsigned_tx").unwrap(); - - // test remove - let body = json!({ - "jsonrpc": "2.0", - "id": 2, - "method": "remove_view_only_account", - "params": { - "account_id": account_id, - } - }); - let res = dispatch(&client, body, &logger); - let result = res.get("result").unwrap(); - let removed = result.get("removed").unwrap().as_bool().unwrap(); - assert!(removed); - - // test get-all - let body = json!({ - "jsonrpc": "2.0", - "id": 2, - "method": "get_all_view_only_accounts", - }); - let res = dispatch(&client, body, &logger); - let result = res.get("result").unwrap(); - let account_ids = result.get("account_ids").unwrap().as_array().unwrap(); - assert_eq!(account_ids.len(), 0); - } -} diff --git a/full-service/src/json_rpc/json_rpc_request.rs b/full-service/src/json_rpc/json_rpc_request.rs index 0adff9694..5bcb29386 100644 --- a/full-service/src/json_rpc/json_rpc_request.rs +++ b/full-service/src/json_rpc/json_rpc_request.rs @@ -2,27 +2,7 @@ //! The JSON RPC 2.0 Requests to the Wallet API for Full Service. -use crate::json_rpc::{ - tx_proposal::TxProposal, - view_only_account::{ViewOnlyAccountJSON, ViewOnlyAccountSecretsJSON}, - view_only_subaddress::ViewOnlySubaddressesJSON, -}; - -use crate::json_rpc::receiver_receipt::ReceiverReceipt; use serde::{Deserialize, Serialize}; -use std::convert::TryFrom; -use strum::IntoEnumIterator; -use strum_macros::EnumIter; - -// FIXME: Update -/// Help string when invoking GET on the wallet endpoint. -pub fn help_str() -> String { - let mut help_str = "Please use json data to choose wallet commands. For example, \n\ncurl -s localhost:9090/wallet -d '{\"method\": \"create_account\", \"params\": {\"name\": \"Alice\"}}' -X POST -H 'Content-type: application/json'\n\nAvailable commands are:\n\n".to_owned(); - for e in JsonCommandRequest::iter() { - help_str.push_str(&format!("{:?}\n\n", e)); - } - help_str -} /// JSON-RPC 2.0 Request. #[derive(Deserialize, Serialize, Debug, Clone)] @@ -44,290 +24,3 @@ pub struct JsonRPCRequest { /// not optional. pub id: serde_json::Value, } - -impl TryFrom<&JsonRPCRequest> for JsonCommandRequest { - type Error = String; - - fn try_from(src: &JsonRPCRequest) -> Result { - let mut src_json: serde_json::Value = serde_json::json!(src); - - // Resolve deprecated method names to an alias. - let method = src_json.get_mut("method").ok_or("Missing method")?; - *method = method_alias(method.as_str().ok_or("Method is not a string")?).into(); - - serde_json::from_value(src_json).map_err(|e| format!("Could not get value {:?}", e)) - } -} - -/// Requests to the Full Service Wallet Service. -#[derive(Deserialize, Serialize, EnumIter, Debug)] -#[serde(tag = "method", content = "params")] -#[allow(non_camel_case_types)] -pub enum JsonCommandRequest { - assign_address_for_account { - account_id: String, - metadata: Option, - }, - build_and_submit_transaction { - account_id: String, - addresses_and_values: Option>, - recipient_public_address: Option, - value_pmob: Option, - input_txo_ids: Option>, - fee: Option, - tombstone_block: Option, - max_spendable_value: Option, - comment: Option, - }, - build_gift_code { - account_id: String, - value_pmob: String, - memo: Option, - input_txo_ids: Option>, - fee: Option, - tombstone_block: Option, - max_spendable_value: Option, - }, - build_split_txo_transaction { - txo_id: String, - output_values: Vec, - destination_subaddress_index: Option, - fee: Option, - tombstone_block: Option, - }, - build_transaction { - account_id: String, - addresses_and_values: Option>, - recipient_public_address: Option, - value_pmob: Option, - input_txo_ids: Option>, - fee: Option, - tombstone_block: Option, - max_spendable_value: Option, - log_tx_proposal: Option, - }, - build_unsigned_transaction { - account_id: String, - recipient_public_address: Option, - value_pmob: Option, - fee: Option, - tombstone_block: Option, - }, - check_b58_type { - b58_code: String, - }, - check_gift_code_status { - gift_code_b58: String, - }, - check_receiver_receipt_status { - address: String, - receiver_receipt: ReceiverReceipt, - }, - claim_gift_code { - gift_code_b58: String, - account_id: String, - address: Option, - }, - create_account { - name: Option, - fog_report_url: Option, - fog_report_id: Option, - fog_authority_spki: Option, - }, - create_new_subaddresses_request { - account_id: String, - num_subaddresses_to_generate: String, - }, - create_payment_request { - account_id: String, - subaddress_index: Option, - amount_pmob: u64, - memo: Option, - }, - create_receiver_receipts { - tx_proposal: TxProposal, - }, - create_view_only_account_sync_request { - account_id: String, - }, - export_account_secrets { - account_id: String, - }, - export_spent_txo_ids { - account_id: String, - }, - export_view_only_account_package { - account_id: String, - }, - export_view_only_account_secrets { - account_id: String, - }, - get_account { - account_id: String, - }, - get_account_status { - account_id: String, - }, - get_address_for_account { - account_id: String, - index: i64, - }, - get_address_for_view_only_account { - account_id: String, - index: i64, - }, - get_addresses_for_account { - account_id: String, - offset: Option, - limit: Option, - }, - get_addresses_for_view_only_account { - account_id: String, - offset: Option, - limit: Option, - }, - get_all_accounts, - get_all_gift_codes, - get_all_transaction_logs_for_block { - block_index: String, - }, - get_all_transaction_logs_ordered_by_block, - get_all_txos_for_address { - address: String, - }, - get_all_view_only_accounts, - get_balance_for_account { - account_id: String, - }, - get_balance_for_address { - address: String, - }, - get_balance_for_view_only_account { - account_id: String, - }, - get_balance_for_view_only_address { - address: String, - }, - get_block { - block_index: String, - }, - get_confirmations { - transaction_log_id: String, - }, - get_gift_code { - gift_code_b58: String, - }, - get_mc_protocol_transaction { - transaction_log_id: String, - }, - get_mc_protocol_txo { - txo_id: String, - }, - get_network_status, - get_transaction_log { - transaction_log_id: String, - }, - get_transaction_logs_for_account { - account_id: String, - offset: Option, - limit: Option, - min_block_index: Option, - max_block_index: Option, - }, - get_txo { - txo_id: String, - }, - get_txos_for_account { - account_id: String, - status: Option, - offset: Option, - limit: Option, - }, - get_txos_for_view_only_account { - account_id: String, - offset: Option, - limit: Option, - }, - get_view_only_account { - account_id: String, - }, - get_wallet_status, - import_account { - mnemonic: String, - key_derivation_version: String, - name: Option, - first_block_index: Option, - next_subaddress_index: Option, - fog_report_url: Option, - fog_report_id: Option, - fog_authority_spki: Option, - }, - import_account_from_legacy_root_entropy { - entropy: String, - name: Option, - first_block_index: Option, - next_subaddress_index: Option, - fog_report_url: Option, - fog_report_id: Option, - fog_authority_spki: Option, - }, - import_subaddresses_to_view_only_account { - account_id: String, - subaddresses: ViewOnlySubaddressesJSON, - }, - import_view_only_account { - account: ViewOnlyAccountJSON, - secrets: ViewOnlyAccountSecretsJSON, - subaddresses: ViewOnlySubaddressesJSON, - }, - remove_account { - account_id: String, - }, - remove_gift_code { - gift_code_b58: String, - }, - remove_view_only_account { - account_id: String, - }, - submit_gift_code { - from_account_id: String, - gift_code_b58: String, - tx_proposal: TxProposal, - }, - submit_transaction { - tx_proposal: TxProposal, - comment: Option, - account_id: Option, - }, - sync_view_only_account { - account_id: String, - completed_txos: Vec<(String, String)>, - subaddresses: ViewOnlySubaddressesJSON, - }, - update_account_name { - account_id: String, - name: String, - }, - update_view_only_account_name { - account_id: String, - name: String, - }, - validate_confirmation { - account_id: String, - txo_id: String, - confirmation: String, - }, - verify_address { - address: String, - }, - version, -} - -fn method_alias(m: &str) -> &str { - match m { - "get_all_addresses_for_account" => "get_addresses_for_account", - "get_all_transaction_logs_for_account" => "get_transaction_logs_for_account", - "get_all_txos_for_account" => "get_txos_for_account", - _ => m, - } -} diff --git a/full-service/src/json_rpc/json_rpc_response.rs b/full-service/src/json_rpc/json_rpc_response.rs index a9066f3ff..b4671beec 100644 --- a/full-service/src/json_rpc/json_rpc_response.rs +++ b/full-service/src/json_rpc/json_rpc_response.rs @@ -3,40 +3,17 @@ //! JSON-RPC Responses from the Wallet API. //! //! API v2 - -use crate::{ - json_rpc::{ - account::Account, - account_secrets::AccountSecrets, - address::Address, - balance::Balance, - block::{Block, BlockContents}, - confirmation_number::Confirmation, - gift_code::GiftCode, - json_rpc_request::JsonRPCRequest, - network_status::NetworkStatus, - receiver_receipt::ReceiverReceipt, - transaction_log::TransactionLog, - tx_proposal::TxProposal, - txo::Txo, - view_only_account::{ViewOnlyAccountJSON, ViewOnlyAccountSecretsJSON}, - view_only_subaddress::ViewOnlySubaddressJSON, - wallet_status::WalletStatus, - }, - service::{gift_code::GiftCodeStatus, receipt::ReceiptTransactionStatus}, - util::b58::PrintableWrapperType, -}; -use mc_mobilecoind_json::data_types::{JsonTx, JsonTxOut}; use serde::{Deserialize, Serialize}; -use serde_json::Map; -use std::collections::HashMap; use strum::Display; -use crate::{fog_resolver::FullServiceFogResolver, unsigned_tx::UnsignedTx}; +pub trait JsonCommandResponse {} /// A JSON RPC 2.0 Response. #[derive(Deserialize, Serialize, Debug)] -pub struct JsonRPCResponse { +pub struct JsonRPCResponse +where + Response: JsonCommandResponse, +{ /// The method which was invoked on the server. /// /// Optional because JSON RPC does not require returning the method invoked, @@ -48,7 +25,7 @@ pub struct JsonRPCResponse { /// /// Optional: if error occurs, result is not returned. #[serde(skip_serializing_if = "Option::is_none")] - pub result: Option, + pub result: Option, /// The error that occurred when invoking the method on the server. /// @@ -123,224 +100,3 @@ pub fn format_invalid_request_error(e: T data, } } - -/// Responses from the Full Service Wallet. -#[derive(Deserialize, Serialize, Debug)] -#[serde(untagged)] -#[allow(non_camel_case_types)] -#[allow(clippy::large_enum_variant)] -pub enum JsonCommandResponse { - assign_address_for_account { - address: Address, - }, - build_and_submit_transaction { - transaction_log: TransactionLog, - tx_proposal: TxProposal, - }, - build_gift_code { - tx_proposal: TxProposal, - gift_code_b58: String, - }, - build_split_txo_transaction { - tx_proposal: TxProposal, - transaction_log_id: String, - }, - build_transaction { - tx_proposal: TxProposal, - transaction_log_id: String, - }, - build_unsigned_transaction { - account_id: String, - unsigned_tx: UnsignedTx, - fog_resolver: FullServiceFogResolver, - }, - check_b58_type { - b58_type: PrintableWrapperType, - data: HashMap, - }, - check_gift_code_status { - gift_code_status: GiftCodeStatus, - gift_code_value: Option, - gift_code_memo: String, - }, - check_receiver_receipt_status { - receipt_transaction_status: ReceiptTransactionStatus, - txo: Option, - }, - claim_gift_code { - txo_id: String, - }, - create_account { - account: Account, - }, - create_payment_request { - payment_request_b58: String, - }, - create_new_subaddresses_request { - account_id: String, - next_subaddress_index: String, - num_subaddresses_to_generate: String, - }, - create_receiver_receipts { - receiver_receipts: Vec, - }, - create_view_only_account_sync_request { - account_id: String, - incomplete_txos_encoded: Vec, - }, - export_account_secrets { - account_secrets: AccountSecrets, - }, - export_spent_txo_ids { - spent_txo_ids: Vec, - }, - export_view_only_account_package { - json_rpc_request: JsonRPCRequest, - }, - export_view_only_account_secrets { - view_only_account_secrets: ViewOnlyAccountSecretsJSON, - }, - get_account { - account: Account, - }, - get_account_status { - account: Account, - balance: Balance, - }, - get_address_for_account { - address: Address, - }, - get_address_for_view_only_account { - address: ViewOnlySubaddressJSON, - }, - get_addresses_for_account { - public_addresses: Vec, - address_map: Map, - }, - get_addresses_for_view_only_account { - public_addresses: Vec, - address_map: Map, - }, - get_all_accounts { - account_ids: Vec, - account_map: Map, - }, - get_all_gift_codes { - gift_codes: Vec, - }, - get_all_transaction_logs_for_block { - transaction_log_ids: Vec, - transaction_log_map: Map, - }, - get_all_transaction_logs_ordered_by_block { - transaction_log_map: Map, - }, - get_all_txos_for_address { - txo_ids: Vec, - txo_map: Map, - }, - get_all_view_only_accounts { - account_ids: Vec, - account_map: Map, - }, - get_balance_for_account { - balance: Balance, - }, - get_balance_for_address { - balance: Balance, - }, - get_balance_for_view_only_account { - balance: Balance, - }, - get_balance_for_view_only_address { - balance: Balance, - }, - get_block { - block: Block, - block_contents: BlockContents, - }, - get_confirmations { - confirmations: Vec, - }, - get_gift_code { - gift_code: GiftCode, - }, - get_mc_protocol_transaction { - transaction: JsonTx, - }, - get_mc_protocol_txo { - txo: JsonTxOut, - }, - get_network_status { - network_status: NetworkStatus, - }, - get_transaction_log { - transaction_log: TransactionLog, - }, - get_transaction_logs_for_account { - transaction_log_ids: Vec, - transaction_log_map: Map, - }, - get_txo { - txo: Txo, - }, - get_txos_for_account { - txo_ids: Vec, - txo_map: Map, - }, - get_txos_for_view_only_account { - txo_ids: Vec, - txo_map: Map, - }, - get_view_only_account { - view_only_account: ViewOnlyAccountJSON, - }, - get_wallet_status { - wallet_status: WalletStatus, - }, - import_account { - account: Account, - }, - import_account_from_legacy_root_entropy { - account: Account, - }, - import_subaddresses_to_view_only_account { - public_address_b58s: Vec, - }, - import_view_only_account { - view_only_account: ViewOnlyAccountJSON, - }, - remove_account { - removed: bool, - }, - remove_gift_code { - removed: bool, - }, - remove_view_only_account { - removed: bool, - }, - submit_gift_code { - gift_code: GiftCode, - }, - submit_transaction { - transaction_log: Option, - }, - sync_view_only_account, - update_account_name { - account: Account, - }, - update_view_only_account_name { - view_only_account: ViewOnlyAccountJSON, - }, - validate_confirmation { - validated: bool, - }, - verify_address { - verified: bool, - }, - version { - string: String, - number: (String, String, String, String), - commit: String, - }, -} diff --git a/full-service/src/json_rpc/mod.rs b/full-service/src/json_rpc/mod.rs index 845562182..2e2c7a3c0 100644 --- a/full-service/src/json_rpc/mod.rs +++ b/full-service/src/json_rpc/mod.rs @@ -2,31 +2,8 @@ //! JSON RPC 2.0 API specification for the Full Service wallet. -pub mod account; -pub mod account_key; -pub mod account_secrets; -mod address; -mod amount; -mod balance; -mod block; -mod confirmation_number; -mod gift_code; pub mod json_rpc_request; pub mod json_rpc_response; -mod network_status; -mod receiver_receipt; -mod transaction_log; -pub mod tx_proposal; -mod txo; -mod unspent_tx_out; -pub mod view_only_account; -pub mod view_only_subaddress; -pub mod view_only_txo; +pub mod v1; +pub mod v2; pub mod wallet; -mod wallet_status; - -#[cfg(any(test, feature = "test_utils"))] -pub mod api_test_utils; - -#[cfg(any(test))] -pub mod e2e; diff --git a/full-service/src/json_rpc/transaction_log.rs b/full-service/src/json_rpc/transaction_log.rs deleted file mode 100644 index 51007d702..000000000 --- a/full-service/src/json_rpc/transaction_log.rs +++ /dev/null @@ -1,141 +0,0 @@ -// Copyright (c) 2020-2021 MobileCoin Inc. - -//! API definition for the TransactionLog object. - -use chrono::{offset::TimeZone, Utc}; -use serde::{Deserialize, Serialize}; - -use crate::{db, db::transaction_log::AssociatedTxos}; - -/// A log of a transaction that occurred on the MobileCoin network, constructed -/// and/or submitted from an account in this wallet. -#[derive(Deserialize, Serialize, Default, Debug, Clone)] -pub struct TransactionLog { - /// String representing the object's type. Objects of the same type share - /// the same value. - pub object: String, - - /// Unique identifier for the transaction log. This value is not associated - /// to the ledger. - pub transaction_log_id: String, - - /// A string that identifies if this transaction log was sent or received. - /// Valid values are "sent" or "received". - pub direction: String, - - /// Flag that indicates if the sent transaction log was recovered from the - /// ledger. This value is null for "received" transaction logs. If true, - /// some information may not be available on the transaction log and its - /// txos without user input. If true, the fee receipient_address_id, fee, - /// and sent_time will be null without user input. - pub is_sent_recovered: Option, - - /// Unique identifier for the assigned associated account. If the - /// transaction is outgoing, this account is from whence the txo came. If - /// received, this is the receiving account. - pub account_id: String, - - /// A list of the Txos which were inputs to this transaction. - pub input_txos: Vec, - - /// A list of the Txos which were outputs from this transaction. - pub output_txos: Vec, - - /// A list of the Txos which were change in this transaction. - pub change_txos: Vec, - - /// Unique identifier for the assigned associated account. Only available if - /// direction is "received". - pub assigned_address_id: Option, - - /// Value in pico MOB associated to this transaction log. - pub value_pmob: String, - - /// Fee in pico MOB associated to this transaction log. Only on outgoing - /// transaction logs. Only available if direction is "sent". - pub fee_pmob: Option, - - /// The block index of the highest block on the network at the time the - /// transaction was submitted. - pub submitted_block_index: Option, - - /// The scanned block block index in which this transaction occurred. - pub finalized_block_index: Option, - - /// String representing the transaction log status. On "sent", valid - /// statuses are "built", "pending", "succeeded", "failed". On "received", - /// the status is "succeeded". - pub status: String, - - /// Time at which sent transaction log was created. Only available if - /// direction is "sent". This value is null if "received" or if the sent - /// transactions were recovered from the ledger (is_sent_recovered = true). - pub sent_time: Option, - - /// An arbitrary string attached to the object. - pub comment: String, - - /// Code representing the cause of "failed" status. - pub failure_code: Option, - - /// Human parsable explanation of "failed" status. - pub failure_message: Option, -} - -impl TransactionLog { - pub fn new( - transaction_log: &db::models::TransactionLog, - associated_txos: &AssociatedTxos, - ) -> Self { - let assigned_address_id = transaction_log.assigned_subaddress_b58.clone(); - Self { - object: "transaction_log".to_string(), - transaction_log_id: transaction_log.transaction_id_hex.clone(), - direction: transaction_log.direction.clone(), - is_sent_recovered: None, // FIXME: WS-16 "Is Sent Recovered" - account_id: transaction_log.account_id_hex.clone(), - assigned_address_id, - value_pmob: (transaction_log.value as u64).to_string(), - fee_pmob: transaction_log.fee.map(|x| (x as u64).to_string()), - submitted_block_index: transaction_log - .submitted_block_index - .map(|b| (b as u64).to_string()), - finalized_block_index: transaction_log - .finalized_block_index - .map(|b| (b as u64).to_string()), - status: transaction_log.status.clone(), - input_txos: associated_txos.inputs.iter().map(TxoAbbrev::new).collect(), - output_txos: associated_txos.outputs.iter().map(TxoAbbrev::new).collect(), - change_txos: associated_txos.change.iter().map(TxoAbbrev::new).collect(), - sent_time: transaction_log - .sent_time - .map(|t| Utc.timestamp(t, 0).to_string()), - comment: transaction_log.comment.clone(), - failure_code: None, // FIXME: WS-17 Failiure code - failure_message: None, // FIXME: WS-17 Failure message - } - } -} - -#[derive(Deserialize, Serialize, Default, Debug, Clone)] -pub struct TxoAbbrev { - pub txo_id_hex: String, - - /// Unique identifier for the recipient associated account. Blank unless - /// direction is "sent". - pub recipient_address_id: String, - - /// Available pico MOB for this Txo. - /// If the account is syncing, this value may change. - pub value_pmob: String, -} - -impl TxoAbbrev { - pub fn new(txo: &db::models::Txo) -> Self { - Self { - txo_id_hex: txo.txo_id_hex.clone(), - recipient_address_id: txo.recipient_public_address_b58.clone(), - value_pmob: (txo.value as u64).to_string(), - } - } -} diff --git a/full-service/src/json_rpc/v1/api/mod.rs b/full-service/src/json_rpc/v1/api/mod.rs new file mode 100644 index 000000000..bd19e428f --- /dev/null +++ b/full-service/src/json_rpc/v1/api/mod.rs @@ -0,0 +1,6 @@ +pub mod request; +pub mod response; +pub mod wallet; + +#[cfg(any(test, feature = "test_utils"))] +pub mod test_utils; diff --git a/full-service/src/json_rpc/v1/api/request.rs b/full-service/src/json_rpc/v1/api/request.rs new file mode 100644 index 000000000..f5bcf076c --- /dev/null +++ b/full-service/src/json_rpc/v1/api/request.rs @@ -0,0 +1,241 @@ +// Copyright (c) 2020-2021 MobileCoin Inc. + +//! The JSON RPC 2.0 Requests to the Wallet API for Full Service. + +use crate::json_rpc::{ + json_rpc_request::JsonRPCRequest, + v1::models::{receiver_receipt::ReceiverReceipt, tx_proposal::TxProposal}, +}; +use serde::{Deserialize, Serialize}; +use std::convert::TryFrom; +use strum::IntoEnumIterator; +use strum_macros::EnumIter; + +// FIXME: Update +/// Help string when invoking GET on the wallet endpoint. +pub fn help_str() -> String { + let mut help_str = "Please use json data to choose wallet commands. For example, \n\ncurl -s localhost:9090/wallet -d '{\"method\": \"create_account\", \"params\": {\"name\": \"Alice\"}}' -X POST -H 'Content-type: application/json'\n\nAvailable commands are:\n\n".to_owned(); + for e in JsonCommandRequest::iter() { + help_str.push_str(&format!("{:?}\n\n", e)); + } + help_str +} + +impl TryFrom<&JsonRPCRequest> for JsonCommandRequest { + type Error = String; + + fn try_from(src: &JsonRPCRequest) -> Result { + let mut src_json: serde_json::Value = serde_json::json!(src); + + // Resolve deprecated method names to an alias. + let method = src_json.get_mut("method").ok_or("Missing method")?; + *method = method_alias(method.as_str().ok_or("Method is not a string")?).into(); + + serde_json::from_value(src_json).map_err(|e| format!("Could not get value {:?}", e)) + } +} + +/// Requests to the Full Service Wallet Service. +#[derive(Deserialize, Serialize, EnumIter, Debug)] +#[serde(tag = "method", content = "params")] +#[allow(non_camel_case_types)] +pub enum JsonCommandRequest { + assign_address_for_account { + account_id: String, + metadata: Option, + }, + build_and_submit_transaction { + account_id: String, + addresses_and_values: Option>, + recipient_public_address: Option, + value_pmob: Option, + input_txo_ids: Option>, + fee: Option, + tombstone_block: Option, + max_spendable_value: Option, + comment: Option, + }, + build_gift_code { + account_id: String, + value_pmob: String, + memo: Option, + input_txo_ids: Option>, + fee: Option, + tombstone_block: Option, + max_spendable_value: Option, + }, + build_split_txo_transaction { + txo_id: String, + output_values: Vec, + destination_subaddress_index: Option, + fee: Option, + tombstone_block: Option, + }, + build_transaction { + account_id: String, + addresses_and_values: Option>, + recipient_public_address: Option, + value_pmob: Option, + input_txo_ids: Option>, + fee: Option, + tombstone_block: Option, + max_spendable_value: Option, + log_tx_proposal: Option, + }, + check_b58_type { + b58_code: String, + }, + check_gift_code_status { + gift_code_b58: String, + }, + check_receiver_receipt_status { + address: String, + receiver_receipt: ReceiverReceipt, + }, + claim_gift_code { + gift_code_b58: String, + account_id: String, + address: Option, + }, + create_account { + name: Option, + fog_report_url: Option, + fog_report_id: Option, + fog_authority_spki: Option, + }, + create_payment_request { + account_id: String, + subaddress_index: Option, + amount_pmob: String, + memo: Option, + }, + create_receiver_receipts { + tx_proposal: TxProposal, + }, + export_account_secrets { + account_id: String, + }, + get_account { + account_id: String, + }, + get_account_status { + account_id: String, + }, + get_address_for_account { + account_id: String, + index: i64, + }, + get_addresses_for_account { + account_id: String, + offset: Option, + limit: Option, + }, + get_all_accounts, + get_all_gift_codes, + get_all_transaction_logs_for_block { + block_index: String, + }, + get_all_transaction_logs_ordered_by_block, + get_all_txos_for_address { + address: String, + }, + get_balance_for_account { + account_id: String, + }, + get_balance_for_address { + address: String, + }, + get_block { + block_index: String, + }, + get_confirmations { + transaction_log_id: String, + }, + get_gift_code { + gift_code_b58: String, + }, + get_mc_protocol_transaction { + transaction_log_id: String, + }, + get_mc_protocol_txo { + txo_id: String, + }, + get_network_status, + get_transaction_log { + transaction_log_id: String, + }, + get_transaction_logs_for_account { + account_id: String, + offset: Option, + limit: Option, + min_block_index: Option, + max_block_index: Option, + }, + get_txo { + txo_id: String, + }, + get_txos_for_account { + account_id: String, + status: Option, + offset: Option, + limit: Option, + }, + get_wallet_status, + import_account { + mnemonic: String, + key_derivation_version: String, + name: Option, + first_block_index: Option, + next_subaddress_index: Option, + fog_report_url: Option, + fog_report_id: Option, + fog_authority_spki: Option, + }, + import_account_from_legacy_root_entropy { + entropy: String, + name: Option, + first_block_index: Option, + next_subaddress_index: Option, + fog_report_url: Option, + fog_report_id: Option, + fog_authority_spki: Option, + }, + remove_account { + account_id: String, + }, + remove_gift_code { + gift_code_b58: String, + }, + submit_gift_code { + from_account_id: String, + gift_code_b58: String, + tx_proposal: TxProposal, + }, + submit_transaction { + tx_proposal: TxProposal, + comment: Option, + account_id: Option, + }, + update_account_name { + account_id: String, + name: String, + }, + validate_confirmation { + account_id: String, + txo_id: String, + confirmation: String, + }, + verify_address { + address: String, + }, + version, +} + +fn method_alias(m: &str) -> &str { + match m { + "get_all_addresses_for_account" => "get_addresses_for_account", + "get_all_transaction_logs_for_account" => "get_transaction_logs_for_account", + "get_all_txos_for_account" => "get_txos_for_account", + _ => m, + } +} diff --git a/full-service/src/json_rpc/v1/api/response.rs b/full-service/src/json_rpc/v1/api/response.rs new file mode 100644 index 000000000..4a6c908f9 --- /dev/null +++ b/full-service/src/json_rpc/v1/api/response.rs @@ -0,0 +1,195 @@ +// Copyright (c) 2020-2021 MobileCoin Inc. + +//! JSON-RPC Responses from the Wallet API. +//! +//! API v2 + +use crate::{ + json_rpc::{ + json_rpc_response::JsonCommandResponse as JsonCommandResponseTrait, + v1::models::{ + account::Account, + account_secrets::AccountSecrets, + address::Address, + balance::Balance, + block::{Block, BlockContents}, + confirmation_number::Confirmation, + gift_code::GiftCode, + network_status::NetworkStatus, + receiver_receipt::ReceiverReceipt, + transaction_log::TransactionLog, + tx_proposal::TxProposal, + txo::Txo, + wallet_status::WalletStatus, + }, + }, + service::{gift_code::GiftCodeStatus, receipt::ReceiptTransactionStatus}, + util::b58::PrintableWrapperType, +}; +use mc_mobilecoind_json::data_types::{JsonTx, JsonTxOut}; +use serde::{Deserialize, Serialize}; +use serde_json::Map; +use std::collections::HashMap; + +/// Responses from the Full Service Wallet. +#[derive(Deserialize, Serialize, Debug)] +#[serde(untagged)] +#[allow(non_camel_case_types)] +#[allow(clippy::large_enum_variant)] +pub enum JsonCommandResponse { + assign_address_for_account { + address: Address, + }, + build_and_submit_transaction { + transaction_log: TransactionLog, + tx_proposal: TxProposal, + }, + build_gift_code { + tx_proposal: TxProposal, + gift_code_b58: String, + }, + build_split_txo_transaction { + tx_proposal: TxProposal, + transaction_log_id: String, + }, + build_transaction { + tx_proposal: TxProposal, + transaction_log_id: String, + }, + check_b58_type { + b58_type: PrintableWrapperType, + data: HashMap, + }, + check_gift_code_status { + gift_code_status: GiftCodeStatus, + gift_code_value: Option, + gift_code_memo: String, + }, + check_receiver_receipt_status { + receipt_transaction_status: ReceiptTransactionStatus, + txo: Option, + }, + claim_gift_code { + txo_id: String, + }, + create_account { + account: Account, + }, + create_payment_request { + payment_request_b58: String, + }, + create_receiver_receipts { + receiver_receipts: Vec, + }, + export_account_secrets { + account_secrets: AccountSecrets, + }, + get_account { + account: Account, + }, + get_account_status { + account: Account, + balance: Balance, + }, + get_address_for_account { + address: Address, + }, + get_addresses_for_account { + public_addresses: Vec, + address_map: Map, + }, + get_all_accounts { + account_ids: Vec, + account_map: Map, + }, + get_all_gift_codes { + gift_codes: Vec, + }, + get_all_transaction_logs_for_block { + transaction_log_ids: Vec, + transaction_log_map: Map, + }, + get_all_transaction_logs_ordered_by_block { + transaction_log_map: Map, + }, + get_all_txos_for_address { + txo_ids: Vec, + txo_map: Map, + }, + get_balance_for_account { + balance: Balance, + }, + get_balance_for_address { + balance: Balance, + }, + get_block { + block: Block, + block_contents: BlockContents, + }, + get_confirmations { + confirmations: Vec, + }, + get_gift_code { + gift_code: GiftCode, + }, + get_mc_protocol_transaction { + transaction: JsonTx, + }, + get_mc_protocol_txo { + txo: JsonTxOut, + }, + get_network_status { + network_status: NetworkStatus, + }, + get_transaction_log { + transaction_log: TransactionLog, + }, + get_transaction_logs_for_account { + transaction_log_ids: Vec, + transaction_log_map: Map, + }, + get_txo { + txo: Txo, + }, + get_txos_for_account { + txo_ids: Vec, + txo_map: Map, + }, + get_wallet_status { + wallet_status: WalletStatus, + }, + import_account { + account: Account, + }, + import_account_from_legacy_root_entropy { + account: Account, + }, + remove_account { + removed: bool, + }, + remove_gift_code { + removed: bool, + }, + submit_gift_code { + gift_code: GiftCode, + }, + submit_transaction { + transaction_log: Option, + }, + update_account_name { + account: Account, + }, + validate_confirmation { + validated: bool, + }, + verify_address { + verified: bool, + }, + version { + string: String, + number: (String, String, String, String), + commit: String, + }, +} + +impl JsonCommandResponseTrait for JsonCommandResponse {} diff --git a/full-service/src/json_rpc/api_test_utils.rs b/full-service/src/json_rpc/v1/api/test_utils.rs similarity index 97% rename from full-service/src/json_rpc/api_test_utils.rs rename to full-service/src/json_rpc/v1/api/test_utils.rs index fc78cab65..26eefdbcf 100644 --- a/full-service/src/json_rpc/api_test_utils.rs +++ b/full-service/src/json_rpc/v1/api/test_utils.rs @@ -2,9 +2,11 @@ use crate::{ json_rpc::{ - json_rpc_request::{JsonCommandRequest, JsonRPCRequest}, + json_rpc_request::JsonRPCRequest, json_rpc_response::JsonRPCResponse, - wallet::wallet_api_inner, + v1::api::{ + request::JsonCommandRequest, response::JsonCommandResponse, wallet::wallet_api_inner, + }, }, service::WalletService, test_utils::{ @@ -51,7 +53,7 @@ fn test_wallet_api( _guard: ApiKeyGuard, state: rocket::State, command: Json, -) -> Result, String> { +) -> Result>, String> { let req: JsonRPCRequest = command.0.clone(); let mut response = JsonRPCResponse { @@ -102,7 +104,7 @@ pub fn create_test_setup( setup_peer_manager_and_network_state(ledger_db.clone(), logger.clone(), false); let service = WalletService::new( - wallet_db, + Some(wallet_db), ledger_db.clone(), peer_manager, network_state.clone(), diff --git a/full-service/src/json_rpc/v1/api/wallet.rs b/full-service/src/json_rpc/v1/api/wallet.rs new file mode 100644 index 000000000..2c9a519a9 --- /dev/null +++ b/full-service/src/json_rpc/v1/api/wallet.rs @@ -0,0 +1,1078 @@ +use crate::{ + db::{ + account::AccountID, + transaction_log::TransactionID, + txo::{TxoID, TxoStatus}, + }, + json_rpc::{ + self, + json_rpc_request::JsonRPCRequest, + json_rpc_response::{ + format_error, format_invalid_request_error, JsonRPCError, JsonRPCResponse, + }, + v1::{ + api::{request::JsonCommandRequest, response::JsonCommandResponse}, + models::{ + account::Account, + account_secrets::AccountSecrets, + address::Address, + balance::Balance, + block::{Block, BlockContents}, + confirmation_number::Confirmation, + gift_code::GiftCode, + network_status::NetworkStatus, + receiver_receipt::ReceiverReceipt, + transaction_log::TransactionLog, + tx_proposal::TxProposal, + txo::Txo, + wallet_status::WalletStatus, + }, + }, + v2::models::amount::Amount, + wallet::{ApiKeyGuard, WalletState}, + }, + service::{ + self, + account::AccountService, + address::AddressService, + balance::BalanceService, + confirmation_number::ConfirmationService, + gift_code::{EncodedGiftCode, GiftCodeService}, + ledger::LedgerService, + payment_request::PaymentRequestService, + receipt::ReceiptService, + transaction::{TransactionMemo, TransactionService}, + transaction_log::TransactionLogService, + txo::TxoService, + WalletService, + }, + util::b58::{ + b58_decode_payment_request, b58_encode_public_address, b58_printable_wrapper_type, + PrintableWrapperType, + }, +}; +use mc_common::logger::global_log; +use mc_connection::{BlockchainConnection, UserTxConnection}; +use mc_fog_report_validation::FogPubkeyResolver; +use mc_mobilecoind_json::data_types::{JsonTx, JsonTxOut}; +use mc_transaction_core::{tokens::Mob, Amount as CoreAmount, Token}; +use rocket::{self}; +use rocket_contrib::json::Json; +use serde_json::Map; +use std::{collections::HashMap, convert::TryFrom, iter::FromIterator}; + +pub fn generic_wallet_api( + _api_key_guard: ApiKeyGuard, + state: rocket::State>, + command: Json, +) -> Result>, String> +where + T: BlockchainConnection + UserTxConnection + 'static, + FPR: FogPubkeyResolver + Send + Sync + 'static, +{ + let req: JsonRPCRequest = command.0.clone(); + + let mut response: JsonRPCResponse = JsonRPCResponse { + method: Some(command.0.method), + result: None, + error: None, + jsonrpc: "2.0".to_string(), + id: command.0.id, + }; + + let request = match JsonCommandRequest::try_from(&req) { + Ok(request) => request, + Err(error) => { + response.error = Some(format_invalid_request_error(error)); + return Ok(Json(response)); + } + }; + + match wallet_api_inner(&state.service, request) { + Ok(command_response) => { + response.result = Some(command_response); + } + Err(rpc_error) => { + response.error = Some(rpc_error); + } + }; + + Ok(Json(response)) +} + +/// The Wallet API inner method, which handles switching on the method enum. +/// +/// Note that this is structured this way so that the routes can be defined to +/// take explicit Rocket state, and then pass the service to the inner method. +/// This allows us to properly construct state with Mock Connection Objects in +/// tests. This also allows us to version the overall API easily. +pub fn wallet_api_inner( + service: &WalletService, + command: JsonCommandRequest, +) -> Result +where + T: BlockchainConnection + UserTxConnection + 'static, + FPR: FogPubkeyResolver + Send + Sync + 'static, +{ + global_log::trace!("Running command {:?}", command); + + let response = match command { + JsonCommandRequest::assign_address_for_account { + account_id, + metadata, + } => JsonCommandResponse::assign_address_for_account { + address: Address::from( + &service + .assign_address_for_account(&AccountID(account_id), metadata.as_deref()) + .map_err(format_error)?, + ), + }, + JsonCommandRequest::build_and_submit_transaction { + account_id, + addresses_and_values, + recipient_public_address, + value_pmob, + input_txo_ids, + fee, + tombstone_block, + max_spendable_value, + comment, + } => { + // The user can specify either a single address and a single value, + // or a list of addresses and values. + let mut addresses_and_values = addresses_and_values.unwrap_or_default(); + if let (Some(a), Some(v)) = (recipient_public_address, value_pmob) { + addresses_and_values.push((a, v)); + } + + let addresses_and_amounts: Vec<(String, Amount)> = addresses_and_values + .into_iter() + .map(|(a, v)| { + ( + a, + Amount { + value: v, + token_id: Mob::ID.to_string(), + }, + ) + }) + .collect(); + + let (transaction_log, associated_txos, _, tx_proposal) = service + .build_sign_and_submit_transaction( + &account_id, + &addresses_and_amounts, + input_txo_ids.as_ref(), + fee, + Some(Mob::ID.to_string()), + tombstone_block, + max_spendable_value, + comment, + TransactionMemo::RTH, + ) + .map_err(format_error)?; + + JsonCommandResponse::build_and_submit_transaction { + transaction_log: json_rpc::v1::models::transaction_log::TransactionLog::new( + &transaction_log, + &associated_txos, + ), + tx_proposal: TxProposal::try_from(&tx_proposal).map_err(format_error)?, + } + } + JsonCommandRequest::build_gift_code { + account_id, + value_pmob, + memo, + input_txo_ids, + fee, + tombstone_block, + max_spendable_value, + } => { + let (tx_proposal, gift_code_b58) = service + .build_gift_code( + &AccountID(account_id), + value_pmob.parse::().map_err(format_error)?, + memo, + input_txo_ids.as_ref(), + fee.map(|f| f.parse::()) + .transpose() + .map_err(format_error)?, + tombstone_block + .map(|t| t.parse::()) + .transpose() + .map_err(format_error)?, + max_spendable_value + .map(|m| m.parse::()) + .transpose() + .map_err(format_error)?, + ) + .map_err(format_error)?; + JsonCommandResponse::build_gift_code { + tx_proposal: TxProposal::try_from(&tx_proposal).map_err(format_error)?, + gift_code_b58: gift_code_b58.to_string(), + } + } + JsonCommandRequest::build_split_txo_transaction { + txo_id, + output_values, + destination_subaddress_index, + fee, + tombstone_block, + } => { + let tx_proposal = service + .split_txo( + &TxoID(txo_id), + &output_values, + destination_subaddress_index + .map(|f| f.parse::()) + .transpose() + .map_err(format_error)?, + fee, + Some(Mob::ID.to_string()), + tombstone_block, + ) + .map_err(format_error)?; + JsonCommandResponse::build_split_txo_transaction { + tx_proposal: TxProposal::try_from(&tx_proposal).map_err(format_error)?, + transaction_log_id: TransactionID::from(&tx_proposal.tx).to_string(), + } + } + JsonCommandRequest::build_transaction { + account_id, + addresses_and_values, + recipient_public_address, + value_pmob, + input_txo_ids, + fee, + tombstone_block, + max_spendable_value, + log_tx_proposal: _, + } => { + // The user can specify either a single address and a single value, + // or a list of addresses and values. + let mut addresses_and_values = addresses_and_values.unwrap_or_default(); + if let (Some(a), Some(v)) = (recipient_public_address, value_pmob) { + addresses_and_values.push((a, v)); + } + + let addresses_and_amounts: Vec<(String, Amount)> = addresses_and_values + .into_iter() + .map(|(a, v)| { + ( + a, + Amount { + value: v, + token_id: Mob::ID.to_string(), + }, + ) + }) + .collect(); + + let tx_proposal = service + .build_and_sign_transaction( + &account_id, + &addresses_and_amounts, + input_txo_ids.as_ref(), + fee, + Some(Mob::ID.to_string()), + tombstone_block, + max_spendable_value, + TransactionMemo::RTH, + ) + .map_err(format_error)?; + + JsonCommandResponse::build_transaction { + tx_proposal: TxProposal::try_from(&tx_proposal).map_err(format_error)?, + transaction_log_id: TransactionID::from(&tx_proposal.tx).to_string(), + } + } + JsonCommandRequest::check_b58_type { b58_code } => { + let b58_type = b58_printable_wrapper_type(b58_code.clone()).map_err(format_error)?; + let mut b58_data = HashMap::new(); + match b58_type { + PrintableWrapperType::PublicAddress => { + b58_data.insert("public_address_b58".to_string(), b58_code); + } + PrintableWrapperType::TransferPayload => {} + PrintableWrapperType::PaymentRequest => { + let payment_request = + b58_decode_payment_request(b58_code).map_err(format_error)?; + let public_address_b58 = + b58_encode_public_address(&payment_request.public_address) + .map_err(format_error)?; + b58_data.insert("public_address_b58".to_string(), public_address_b58); + b58_data.insert("value".to_string(), payment_request.value.to_string()); + b58_data.insert("memo".to_string(), payment_request.memo); + } + } + JsonCommandResponse::check_b58_type { + b58_type, + data: b58_data, + } + } + JsonCommandRequest::check_gift_code_status { gift_code_b58 } => { + let (status, value, memo) = service + .check_gift_code_status(&EncodedGiftCode(gift_code_b58)) + .map_err(format_error)?; + JsonCommandResponse::check_gift_code_status { + gift_code_status: status, + gift_code_value: value, + gift_code_memo: memo, + } + } + JsonCommandRequest::check_receiver_receipt_status { + address, + receiver_receipt, + } => { + let receipt = service::receipt::ReceiverReceipt::try_from(&receiver_receipt) + .map_err(format_error)?; + let (status, txo_and_status) = service + .check_receipt_status(&address, &receipt) + .map_err(format_error)?; + JsonCommandResponse::check_receiver_receipt_status { + receipt_transaction_status: status, + txo: txo_and_status.as_ref().map(|(t, s)| Txo::new(t, s)), + } + } + JsonCommandRequest::claim_gift_code { + gift_code_b58, + account_id, + address, + } => { + let tx = service + .claim_gift_code( + &EncodedGiftCode(gift_code_b58), + &AccountID(account_id), + address, + ) + .map_err(format_error)?; + JsonCommandResponse::claim_gift_code { + txo_id: TxoID::from(&tx.prefix.outputs[0]).to_string(), + } + } + JsonCommandRequest::create_account { + name, + fog_report_url, + fog_report_id, + fog_authority_spki, + } => { + let account = service + .create_account( + name, + fog_report_url.unwrap_or_default(), + fog_report_id.unwrap_or_default(), + fog_authority_spki.unwrap_or_default(), + ) + .map_err(format_error)?; + let next_subaddress_index = service + .get_next_subaddress_index_for_account(&AccountID(account.id.clone())) + .map_err(format_error)?; + + JsonCommandResponse::create_account { + account: Account::new(&account, next_subaddress_index).map_err(format_error)?, + } + } + JsonCommandRequest::create_payment_request { + account_id, + subaddress_index, + amount_pmob, + memo, + } => JsonCommandResponse::create_payment_request { + payment_request_b58: service + .create_payment_request( + account_id, + subaddress_index, + CoreAmount::new(amount_pmob.parse::().map_err(format_error)?, Mob::ID), + memo, + ) + .map_err(format_error)?, + }, + JsonCommandRequest::create_receiver_receipts { tx_proposal } => { + let receipts = service + .create_receiver_receipts( + &service::models::tx_proposal::TxProposal::try_from(&tx_proposal) + .map_err(format_error)?, + ) + .map_err(format_error)?; + let json_receipts: Vec = receipts + .iter() + .map(ReceiverReceipt::try_from) + .collect::, String>>() + .map_err(format_error)?; + JsonCommandResponse::create_receiver_receipts { + receiver_receipts: json_receipts, + } + } + JsonCommandRequest::export_account_secrets { account_id } => { + let account = service + .get_account(&AccountID(account_id)) + .map_err(format_error)?; + JsonCommandResponse::export_account_secrets { + account_secrets: AccountSecrets::try_from(&account).map_err(format_error)?, + } + } + JsonCommandRequest::get_account { account_id } => { + let account_id = AccountID(account_id); + let account = service.get_account(&account_id).map_err(format_error)?; + let next_subaddress_index = service + .get_next_subaddress_index_for_account(&account_id) + .map_err(format_error)?; + + JsonCommandResponse::get_account { + account: Account::new(&account, next_subaddress_index).map_err(format_error)?, + } + } + JsonCommandRequest::get_account_status { account_id } => { + let account_id = AccountID(account_id); + let account = &service.get_account(&account_id).map_err(format_error)?; + let next_subaddress_index = service + .get_next_subaddress_index_for_account(&account_id) + .map_err(format_error)?; + + let balance_map = service + .get_balance_for_account(&account_id) + .map_err(format_error)?; + let balance_mob = balance_map.get(&Mob::ID).unwrap_or_default(); + + let network_status = service.get_network_status().map_err(format_error)?; + + let balance = Balance::new( + balance_mob, + account.next_block_index as u64, + &network_status, + ); + + let account = Account::new(account, next_subaddress_index).map_err(format_error)?; + JsonCommandResponse::get_account_status { account, balance } + } + JsonCommandRequest::get_address_for_account { account_id, index } => { + let assigned_subaddress = service + .get_address_for_account(&AccountID(account_id), index) + .map_err(format_error)?; + JsonCommandResponse::get_address_for_account { + address: Address::from(&assigned_subaddress), + } + } + JsonCommandRequest::get_addresses_for_account { + account_id, + offset, + limit, + } => { + let (o, l) = page_helper(offset, limit)?; + let addresses = service + .get_addresses(Some(account_id), Some(o), Some(l)) + .map_err(format_error)?; + let address_map: Map = Map::from_iter( + addresses + .iter() + .map(|a| { + ( + a.public_address_b58.clone(), + serde_json::to_value(&(Address::from(a))) + .expect("Could not get json value"), + ) + }) + .collect::>(), + ); + + JsonCommandResponse::get_addresses_for_account { + public_addresses: addresses + .iter() + .map(|a| a.public_address_b58.clone()) + .collect(), + address_map, + } + } + JsonCommandRequest::get_all_accounts => { + let accounts = service.list_accounts(None, None).map_err(format_error)?; + let json_accounts: Vec<(String, serde_json::Value)> = accounts + .iter() + .map(|a| { + let next_subaddress_index = service + .get_next_subaddress_index_for_account(&AccountID(a.id.clone())) + .map_err(format_error)?; + let account_json = + Account::new(a, next_subaddress_index).map_err(format_error)?; + + serde_json::to_value(account_json) + .map(|v| (a.id.clone(), v)) + .map_err(format_error) + }) + .collect::, JsonRPCError>>()?; + let account_map: Map = Map::from_iter(json_accounts); + JsonCommandResponse::get_all_accounts { + account_ids: accounts.iter().map(|a| a.id.clone()).collect(), + account_map, + } + } + JsonCommandRequest::get_all_gift_codes {} => JsonCommandResponse::get_all_gift_codes { + gift_codes: service + .list_gift_codes(None, None) + .map_err(format_error)? + .iter() + .map(GiftCode::from) + .collect(), + }, + JsonCommandRequest::get_all_transaction_logs_for_block { block_index } => { + let transaction_logs_and_txos = service + .get_all_transaction_logs_for_block( + block_index.parse::().map_err(format_error)?, + ) + .map_err(format_error)?; + let transaction_log_map: Map = Map::from_iter( + transaction_logs_and_txos + .iter() + .map(|(t, a, _v)| { + ( + t.id.clone(), + serde_json::json!( + json_rpc::v1::models::transaction_log::TransactionLog::new(t, a) + ), + ) + }) + .collect::>(), + ); + + JsonCommandResponse::get_all_transaction_logs_for_block { + transaction_log_ids: transaction_logs_and_txos + .iter() + .map(|(t, _a, _v)| t.id.to_string()) + .collect(), + transaction_log_map, + } + } + JsonCommandRequest::get_all_transaction_logs_ordered_by_block => { + let transaction_logs_and_txos = service + .get_all_transaction_logs_ordered_by_block() + .map_err(format_error)?; + + let mut transaction_log_map: Map = Map::new(); + + let received_txos = service + .list_txos(None, None, None, Some(*Mob::ID), None, None, None, None) + .map_err(format_error)?; + + let received_tx_logs: Vec = received_txos + .iter() + .map(|(txo, _)| { + let subaddress_b58 = match (txo.subaddress_index, txo.account_id.as_ref()) { + (Some(subaddress_index), Some(account_id)) => service + .get_address_for_account( + &AccountID(account_id.clone()), + subaddress_index, + ) + .map(|assigned_sub| assigned_sub.public_address_b58) + .ok(), + _ => None, + }; + + TransactionLog::new_from_received_txo(txo, subaddress_b58) + }) + .collect::, _>>() + .map_err(format_error)?; + + for received_tx_log in received_tx_logs.iter() { + let tx_log_json = serde_json::to_value(received_tx_log).map_err(format_error)?; + transaction_log_map.insert(received_tx_log.transaction_log_id.clone(), tx_log_json); + } + + for (tx_log, associated_txos, _status) in transaction_logs_and_txos { + let tx_log_json = + serde_json::json!(json_rpc::v1::models::transaction_log::TransactionLog::new( + &tx_log, + &associated_txos + )); + transaction_log_map.insert(tx_log.id.clone(), tx_log_json); + } + + JsonCommandResponse::get_all_transaction_logs_ordered_by_block { + transaction_log_map, + } + } + JsonCommandRequest::get_all_txos_for_address { address } => { + let txos = service + .list_txos( + None, + Some(address), + None, + Some(*Mob::ID), + None, + None, + None, + None, + ) + .map_err(format_error)?; + let txo_map: Map = Map::from_iter( + txos.iter() + .map(|(t, s)| { + ( + t.id.clone(), + serde_json::to_value(Txo::new(t, s)).expect("Could not get json value"), + ) + }) + .collect::>(), + ); + + JsonCommandResponse::get_all_txos_for_address { + txo_ids: txos.iter().map(|(t, _s)| t.id.clone()).collect(), + txo_map, + } + } + JsonCommandRequest::get_balance_for_account { account_id } => { + let account_id = AccountID(account_id); + let account = &service.get_account(&account_id).map_err(format_error)?; + let balance_map = service + .get_balance_for_account(&account_id) + .map_err(format_error)?; + let balance_mob = balance_map.get(&Mob::ID).unwrap_or_default(); + + let network_status = service.get_network_status().map_err(format_error)?; + JsonCommandResponse::get_balance_for_account { + balance: Balance::new( + balance_mob, + account.next_block_index as u64, + &network_status, + ), + } + } + JsonCommandRequest::get_balance_for_address { address } => { + let assigned_subaddress = service.get_address(&address).map_err(format_error)?; + let account_id = AccountID(assigned_subaddress.account_id); + let account = &service.get_account(&account_id).map_err(format_error)?; + + let balance_map = service + .get_balance_for_address(&address) + .map_err(format_error)?; + + let balance_mob = balance_map.get(&Mob::ID).unwrap_or_default(); + + JsonCommandResponse::get_balance_for_address { + balance: Balance::new( + balance_mob, + account.next_block_index as u64, + &service.get_network_status().map_err(format_error)?, + ), + } + } + JsonCommandRequest::get_block { block_index } => { + let (block, block_contents) = service + .get_block_object(block_index.parse::().map_err(format_error)?) + .map_err(format_error)?; + JsonCommandResponse::get_block { + block: Block::new(&block), + block_contents: BlockContents::new(&block_contents), + } + } + JsonCommandRequest::get_confirmations { transaction_log_id } => { + JsonCommandResponse::get_confirmations { + confirmations: service + .get_confirmations(&transaction_log_id) + .map_err(format_error)? + .iter() + .map(Confirmation::from) + .collect(), + } + } + JsonCommandRequest::get_gift_code { gift_code_b58 } => JsonCommandResponse::get_gift_code { + gift_code: GiftCode::from( + &service + .get_gift_code(&EncodedGiftCode(gift_code_b58)) + .map_err(format_error)?, + ), + }, + JsonCommandRequest::get_mc_protocol_transaction { transaction_log_id } => { + let tx = service + .get_transaction_object(&transaction_log_id) + .map_err(format_error)?; + let proto_tx = mc_api::external::Tx::from(&tx); + let json_tx = JsonTx::from(&proto_tx); + JsonCommandResponse::get_mc_protocol_transaction { + transaction: json_tx, + } + } + JsonCommandRequest::get_mc_protocol_txo { txo_id } => { + let tx_out = service.get_txo_object(&txo_id).map_err(format_error)?; + let proto_txo = mc_api::external::TxOut::from(&tx_out); + let json_txo = JsonTxOut::from(&proto_txo); + JsonCommandResponse::get_mc_protocol_txo { txo: json_txo } + } + JsonCommandRequest::get_network_status => JsonCommandResponse::get_network_status { + network_status: NetworkStatus::try_from( + &service.get_network_status().map_err(format_error)?, + ) + .map_err(format_error)?, + }, + JsonCommandRequest::get_transaction_log { transaction_log_id } => { + let (transaction_log, associated_txos, _) = service + .get_transaction_log(&transaction_log_id) + .map_err(format_error)?; + + let json_tx_log = json_rpc::v1::models::transaction_log::TransactionLog::new( + &transaction_log, + &associated_txos, + ); + + JsonCommandResponse::get_transaction_log { + transaction_log: json_tx_log, + } + } + JsonCommandRequest::get_transaction_logs_for_account { + account_id, + offset, + limit, + min_block_index, + max_block_index, + } => { + let (o, l) = page_helper(offset, limit)?; + + let min_block_index = min_block_index + .map(|i| i.parse::()) + .transpose() + .map_err(format_error)?; + + let max_block_index = max_block_index + .map(|i| i.parse::()) + .transpose() + .map_err(format_error)?; + + let mut transaction_log_map: Map = Map::new(); + let mut transaction_log_ids: Vec = Vec::new(); + + let transaction_logs_and_txos = service + .list_transaction_logs( + Some(account_id.clone()), + None, + None, + min_block_index, + max_block_index, + ) + .map_err(format_error)?; + + let received_txos = service + .list_txos( + Some(account_id.clone()), + None, + None, + Some(*Mob::ID), + None, + None, + None, + None, + ) + .map_err(format_error)?; + + let received_tx_logs: Vec = received_txos + .iter() + .map(|(txo, _)| { + let subaddress_b58 = match txo.subaddress_index { + Some(subaddress_index) => service + .get_address_for_account( + &AccountID(account_id.clone()), + subaddress_index, + ) + .map(|assigned_sub| assigned_sub.public_address_b58) + .ok(), + None => None, + }; + + TransactionLog::new_from_received_txo(txo, subaddress_b58) + }) + .collect::, _>>() + .map_err(format_error)?; + + for received_tx_log in received_tx_logs.iter() { + let tx_log_json = serde_json::to_value(received_tx_log).map_err(format_error)?; + transaction_log_map.insert(received_tx_log.transaction_log_id.clone(), tx_log_json); + transaction_log_ids.push(received_tx_log.transaction_log_id.clone()); + } + + for (tx_log, associated_txos, _status) in transaction_logs_and_txos { + let tx_log_json = + serde_json::json!(json_rpc::v1::models::transaction_log::TransactionLog::new( + &tx_log, + &associated_txos + )); + transaction_log_map.insert(tx_log.id.clone(), tx_log_json); + transaction_log_ids.push(tx_log.id.clone()); + } + + transaction_log_ids.sort(); + + let transaction_log_ids_limitted; + + if l - o < transaction_log_ids.len() as u64 { + let mut max = (o + l) as usize; + if max > transaction_log_ids.len() { + max = transaction_log_ids.len(); + } + transaction_log_ids_limitted = transaction_log_ids[o as usize..max].to_vec(); + } else { + transaction_log_ids_limitted = transaction_log_ids.clone(); + } + + JsonCommandResponse::get_transaction_logs_for_account { + transaction_log_ids: transaction_log_ids_limitted, + transaction_log_map, + } + } + JsonCommandRequest::get_txo { txo_id } => { + let (txo, status) = service.get_txo(&TxoID(txo_id)).map_err(format_error)?; + JsonCommandResponse::get_txo { + txo: Txo::new(&txo, &status), + } + } + JsonCommandRequest::get_txos_for_account { + account_id, + status, + offset, + limit, + } => { + let status = if let Some(status) = status { + Some(status.parse::().map_err(format_error)?) + } else { + None + }; + + let (o, l) = page_helper(offset, limit)?; + let txos = service + .list_txos( + Some(account_id), + None, + status, + Some(*Mob::ID), + None, + None, + Some(o), + Some(l), + ) + .map_err(format_error)?; + let txo_map: Map = Map::from_iter( + txos.iter() + .map(|(t, s)| { + ( + t.id.clone(), + serde_json::to_value(Txo::new(t, s)).expect("Could not get json value"), + ) + }) + .collect::>(), + ); + + JsonCommandResponse::get_txos_for_account { + txo_ids: txos.iter().map(|(t, _s)| t.id.clone()).collect(), + txo_map, + } + } + JsonCommandRequest::get_wallet_status => { + let wallet_status = service.get_wallet_status().map_err(format_error)?; + + let account_mapped: Vec<(String, serde_json::Value)> = wallet_status + .account_map + .iter() + .map(|(i, a)| { + let next_subaddress_index = service + .get_next_subaddress_index_for_account(i) + .map_err(|_| { + ("Could not get next subaddress index for account").to_string() + })?; + let account = Account::new(a, next_subaddress_index)?; + serde_json::to_value(account) + .map(|v| (i.to_string(), v)) + .map_err(|e| format!("Coult not convert account map:{:?}", e)) + }) + .collect::, String>>() + .map_err(format_error)?; + let account_map = Map::from_iter(account_mapped); + + let wallet_status = + WalletStatus::new(&wallet_status, account_map).map_err(format_error)?; + + JsonCommandResponse::get_wallet_status { wallet_status } + } + JsonCommandRequest::import_account { + mnemonic, + key_derivation_version, + name, + first_block_index, + next_subaddress_index, + fog_report_url, + fog_report_id, + fog_authority_spki, + } => { + let fb = first_block_index + .map(|fb| fb.parse::()) + .transpose() + .map_err(format_error)?; + let ns = next_subaddress_index + .map(|ns| ns.parse::()) + .transpose() + .map_err(format_error)?; + let kdv = key_derivation_version.parse::().map_err(format_error)?; + + let account = service + .import_account( + mnemonic, + kdv, + name, + fb, + ns, + fog_report_url.unwrap_or_default(), + fog_report_id.unwrap_or_default(), + fog_authority_spki.unwrap_or_default(), + ) + .map_err(format_error)?; + + let next_subaddress_index = service + .get_next_subaddress_index_for_account(&AccountID(account.id.clone())) + .map_err(format_error)?; + + let account_json = + Account::new(&account, next_subaddress_index).map_err(format_error)?; + + JsonCommandResponse::import_account { + account: account_json, + } + } + JsonCommandRequest::import_account_from_legacy_root_entropy { + entropy, + name, + first_block_index, + next_subaddress_index, + fog_report_url, + fog_report_id, + fog_authority_spki, + } => { + let fb = first_block_index + .map(|fb| fb.parse::()) + .transpose() + .map_err(format_error)?; + let ns = next_subaddress_index + .map(|ns| ns.parse::()) + .transpose() + .map_err(format_error)?; + + let account = service + .import_account_from_legacy_root_entropy( + entropy, + name, + fb, + ns, + fog_report_url.unwrap_or_default(), + fog_report_id.unwrap_or_default(), + fog_authority_spki.unwrap_or_default(), + ) + .map_err(format_error)?; + + let next_subaddress_index = service + .get_next_subaddress_index_for_account(&AccountID(account.id.clone())) + .map_err(format_error)?; + + let account_json = + Account::new(&account, next_subaddress_index).map_err(format_error)?; + + JsonCommandResponse::import_account { + account: account_json, + } + } + JsonCommandRequest::remove_account { account_id } => JsonCommandResponse::remove_account { + removed: service + .remove_account(&AccountID(account_id)) + .map_err(format_error)?, + }, + JsonCommandRequest::remove_gift_code { gift_code_b58 } => { + JsonCommandResponse::remove_gift_code { + removed: service + .remove_gift_code(&EncodedGiftCode(gift_code_b58)) + .map_err(format_error)?, + } + } + JsonCommandRequest::submit_gift_code { + from_account_id, + gift_code_b58, + tx_proposal, + } => { + let gift_code = service + .submit_gift_code( + &AccountID(from_account_id), + &EncodedGiftCode(gift_code_b58), + &service::models::tx_proposal::TxProposal::try_from(&tx_proposal) + .map_err(format_error)?, + ) + .map_err(format_error)?; + JsonCommandResponse::submit_gift_code { + gift_code: GiftCode::from(&gift_code), + } + } + JsonCommandRequest::submit_transaction { + tx_proposal, + comment, + account_id, + } => { + let result = service + .submit_transaction( + &service::models::tx_proposal::TxProposal::try_from(&tx_proposal) + .map_err(format_error)?, + comment, + account_id, + ) + .map_err(format_error)? + .map(|(tx_log, associated_txos, _value_map)| { + TransactionLog::new(&tx_log, &associated_txos) + }); + JsonCommandResponse::submit_transaction { + transaction_log: result, + } + } + JsonCommandRequest::update_account_name { account_id, name } => { + let account_id = AccountID(account_id); + let next_subaddress_index = service + .get_next_subaddress_index_for_account(&account_id) + .map_err(format_error)?; + let account = service + .update_account_name(&account_id, name) + .map_err(format_error)?; + let account_json = + Account::new(&account, next_subaddress_index).map_err(format_error)?; + JsonCommandResponse::update_account_name { + account: account_json, + } + } + JsonCommandRequest::validate_confirmation { + account_id, + txo_id, + confirmation, + } => { + let result = service + .validate_confirmation(&AccountID(account_id), &TxoID(txo_id), &confirmation) + .map_err(format_error)?; + JsonCommandResponse::validate_confirmation { validated: result } + } + JsonCommandRequest::verify_address { address } => JsonCommandResponse::verify_address { + verified: service.verify_address(&address).map_err(format_error)?, + }, + JsonCommandRequest::version => JsonCommandResponse::version { + string: env!("CARGO_PKG_VERSION").to_string(), + number: ( + env!("CARGO_PKG_VERSION_MAJOR").to_string(), + env!("CARGO_PKG_VERSION_MINOR").to_string(), + env!("CARGO_PKG_VERSION_PATCH").to_string(), + env!("CARGO_PKG_VERSION_PRE").to_string(), + ), + commit: env!("VERGEN_GIT_SHA").to_string(), + }, + }; + + Ok(response) +} + +fn page_helper(offset: Option, limit: Option) -> Result<(u64, u64), JsonRPCError> { + let offset = match offset { + Some(o) => o.parse::().map_err(format_error)?, + None => 0, // Default offset is zero, at the start of the records. + }; + let limit = match limit { + Some(l) => l.parse::().map_err(format_error)?, + None => 100, // Default page size is one hundred records. + }; + Ok((offset, limit)) +} diff --git a/full-service/src/json_rpc/v1/e2e_tests/account/account_address.rs b/full-service/src/json_rpc/v1/e2e_tests/account/account_address.rs new file mode 100644 index 000000000..54c382e8b --- /dev/null +++ b/full-service/src/json_rpc/v1/e2e_tests/account/account_address.rs @@ -0,0 +1,516 @@ +// Copyright (c) 2020-2022 MobileCoin Inc. + +//! End-to-end tests for the Full Service Wallet API. + +#[cfg(test)] +mod e2e_account { + use crate::{ + db::account::AccountID, + json_rpc::v1::api::test_utils::{dispatch, setup}, + test_utils::{add_block_to_ledger_db, manually_sync_account}, + util::b58::b58_decode_public_address, + }; + + use mc_common::logger::{test_with_logger, Logger}; + use mc_crypto_rand::rand_core::RngCore; + use mc_ledger_db::Ledger; + use mc_transaction_core::ring_signature::KeyImage; + use rand::{rngs::StdRng, SeedableRng}; + + #[test_with_logger] + fn test_import_account_with_next_subaddress_index(logger: Logger) { + let mut rng: StdRng = SeedableRng::from_seed([20u8; 32]); + let (client, mut ledger_db, db_ctx, _network_state) = setup(&mut rng, logger.clone()); + + // create an account + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "import_account_from_legacy_root_entropy", + "params": { + "entropy": "c593274dc6f6eb94242e34ae5f0ab16bc3085d45d49d9e18b8a8c6f057e6b56b", + "name": "Alice Main Account", + } + }); + let res = dispatch(&client, body, &logger); + let result = res.get("result").unwrap(); + let account_obj = result.get("account").unwrap(); + let account_id = account_obj.get("account_id").unwrap().as_str().unwrap(); + + // assign next subaddress for account + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "assign_address_for_account", + "params": { + "account_id": account_id, + "metadata": "subaddress_index_2", + } + }); + let res = dispatch(&client, body, &logger); + let result = res.get("result").unwrap(); + let address = result.get("address").unwrap(); + let b58_public_address = address.get("public_address").unwrap().as_str().unwrap(); + let public_address = b58_decode_public_address(b58_public_address).unwrap(); + + // Add a block to fund account at the new subaddress. + add_block_to_ledger_db( + &mut ledger_db, + &vec![public_address], + 100000000000000, // 100.0 MOB + &vec![KeyImage::from(rng.next_u64())], + &mut rng, + ); + + manually_sync_account( + &ledger_db, + &db_ctx.get_db_instance(logger.clone()), + &AccountID(account_id.to_string()), + &logger, + ); + assert_eq!(ledger_db.num_blocks().unwrap(), 13); + + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "get_balance_for_account", + "params": { + "account_id": account_id, + } + }); + let res = dispatch(&client, body, &logger); + let result = res.get("result").unwrap(); + let balance = result.get("balance").unwrap(); + let unspent_pmob = balance.get("unspent_pmob").unwrap().as_str().unwrap(); + + assert_eq!("100000000000000", unspent_pmob); + + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "remove_account", + "params": { + "account_id": account_id, + } + }); + dispatch(&client, body, &logger); + + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "import_account_from_legacy_root_entropy", + "params": { + "entropy": "c593274dc6f6eb94242e34ae5f0ab16bc3085d45d49d9e18b8a8c6f057e6b56b", + "name": "Alice Main Account", + } + }); + dispatch(&client, body, &logger); + + manually_sync_account( + &ledger_db, + &db_ctx.get_db_instance(logger.clone()), + &AccountID(account_id.to_string()), + &logger, + ); + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "get_balance_for_account", + "params": { + "account_id": account_id, + } + }); + let res = dispatch(&client, body, &logger); + let result = res.get("result").unwrap(); + let balance = result.get("balance").unwrap(); + let unspent_pmob = balance.get("unspent_pmob").unwrap().as_str().unwrap(); + let orphaned_pmob = balance.get("orphaned_pmob").unwrap().as_str().unwrap(); + let spent_pmob = balance.get("spent_pmob").unwrap().as_str().unwrap(); + + assert_eq!("0", unspent_pmob); + assert_eq!("100000000000000", orphaned_pmob); + assert_eq!("0", spent_pmob); + + // assign next subaddress for account + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "assign_address_for_account", + "params": { + "account_id": account_id, + "metadata": "subaddress_index_2", + } + }); + dispatch(&client, body, &logger); + + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "get_balance_for_account", + "params": { + "account_id": account_id, + } + }); + let res = dispatch(&client, body, &logger); + let result = res.get("result").unwrap(); + let balance = result.get("balance").unwrap(); + let unspent_pmob = balance.get("unspent_pmob").unwrap().as_str().unwrap(); + let orphaned_pmob = balance.get("orphaned_pmob").unwrap().as_str().unwrap(); + + assert_eq!("100000000000000", unspent_pmob); + assert_eq!("0", orphaned_pmob); + + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "remove_account", + "params": { + "account_id": account_id, + } + }); + dispatch(&client, body, &logger); + + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "import_account_from_legacy_root_entropy", + "params": { + "entropy": "c593274dc6f6eb94242e34ae5f0ab16bc3085d45d49d9e18b8a8c6f057e6b56b", + "name": "Alice Main Account", + "next_subaddress_index": "3", + } + }); + dispatch(&client, body, &logger); + + manually_sync_account( + &ledger_db, + &db_ctx.get_db_instance(logger.clone()), + &AccountID(account_id.to_string()), + &logger, + ); + + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "get_balance_for_account", + "params": { + "account_id": account_id, + } + }); + let res = dispatch(&client, body, &logger); + let result = res.get("result").unwrap(); + let balance = result.get("balance").unwrap(); + let unspent_pmob = balance.get("unspent_pmob").unwrap().as_str().unwrap(); + let orphaned_pmob = balance.get("orphaned_pmob").unwrap().as_str().unwrap(); + + assert_eq!("100000000000000", unspent_pmob); + assert_eq!("0", orphaned_pmob); + } + + #[test_with_logger] + fn test_paginate_assigned_addresses(logger: Logger) { + let mut rng: StdRng = SeedableRng::from_seed([20u8; 32]); + let (client, _ledger_db, _db_ctx, _network_state) = setup(&mut rng, logger.clone()); + + // Add an account + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "create_account", + "params": { + "name": "", + } + }); + let res = dispatch(&client, body, &logger); + let result = res.get("result").unwrap(); + let account_obj = result.get("account").unwrap(); + let account_id = account_obj.get("account_id").unwrap().as_str().unwrap(); + + // Assign some addresses. + for _ in 0..10 { + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "assign_address_for_account", + "params": { + "account_id": account_id, + "metadata": "subaddress_index_2", + } + }); + dispatch(&client, body, &logger); + } + + // Check that we can paginate address output. + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "get_addresses_for_account", + "params": { + "account_id": account_id, + }, + }); + let res = dispatch(&client, body, &logger); + let result = res.get("result").unwrap(); + let addresses_all = result.get("public_addresses").unwrap().as_array().unwrap(); + assert_eq!(addresses_all.len(), 13); // Accounts start with 3 addresses, then we created 10. + + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "get_addresses_for_account", + "params": { + "account_id": account_id, + "offset": "1", + "limit": "4", + }, + }); + let res = dispatch(&client, body, &logger); + let result = res.get("result").unwrap(); + let addresses_page = result.get("public_addresses").unwrap().as_array().unwrap(); + assert_eq!(addresses_page.len(), 4); + assert_eq!(addresses_page[..], addresses_all[1..5]); + } + + #[test_with_logger] + fn test_next_subaddress_fails_with_fog(logger: Logger) { + use crate::db::WalletDbError::SubaddressesNotSupportedForFOGEnabledAccounts as subaddress_error; + + let mut rng: StdRng = SeedableRng::from_seed([20u8; 32]); + let (client, mut _ledger_db, _db_ctx, _network_state) = setup(&mut rng, logger.clone()); + + // Create Account + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "create_account", + "params": { + "name": "Alice Main Account", + "fog_report_url": "fog://fog-report.example.com", + "fog_report_id": "", + "fog_authority_spki": "MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAvnB9wTbTOT5uoizRYaYbw7XIEkInl8E7MGOAQj+xnC+F1rIXiCnc/t1+5IIWjbRGhWzo7RAwI5sRajn2sT4rRn9NXbOzZMvIqE4hmhmEzy1YQNDnfALAWNQ+WBbYGW+Vqm3IlQvAFFjVN1YYIdYhbLjAPdkgeVsWfcLDforHn6rR3QBZYZIlSBQSKRMY/tywTxeTCvK2zWcS0kbbFPtBcVth7VFFVPAZXhPi9yy1AvnldO6n7KLiupVmojlEMtv4FQkk604nal+j/dOplTATV8a9AJBbPRBZ/yQg57EG2Y2MRiHOQifJx0S5VbNyMm9bkS8TD7Goi59aCW6OT1gyeotWwLg60JRZTfyJ7lYWBSOzh0OnaCytRpSWtNZ6barPUeOnftbnJtE8rFhF7M4F66et0LI/cuvXYecwVwykovEVBKRF4HOK9GgSm17mQMtzrD7c558TbaucOWabYR04uhdAc3s10MkuONWG0wIQhgIChYVAGnFLvSpp2/aQEq3xrRSETxsixUIjsZyWWROkuA0IFnc8d7AmcnUBvRW7FT/5thWyk5agdYUGZ+7C1o69ihR1YxmoGh69fLMPIEOhYh572+3ckgl2SaV4uo9Gvkz8MMGRBcMIMlRirSwhCfozV2RyT5Wn1NgPpyc8zJL7QdOhL7Qxb+5WjnCVrQYHI2cCAwEAAQ==" + }, + }); + + let creation_res = dispatch(&client, body, &logger); + let creation_result = creation_res.get("result").unwrap(); + let account_obj = creation_result.get("account").unwrap(); + let account_id = account_obj.get("account_id").unwrap().as_str().unwrap(); + assert_eq!(creation_res.get("jsonrpc").unwrap(), "2.0"); + + // assign next subaddress for account + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "assign_address_for_account", + "params": { + "account_id": account_id, + "metadata": "subaddress_index_2", + } + }); + let res = dispatch(&client, body, &logger); + let error = res.get("error").unwrap(); + let data = error.get("data").unwrap(); + let details = data.get("details").unwrap(); + assert!(details.to_string().contains(&subaddress_error.to_string())); + } + + #[test_with_logger] + fn test_create_assigned_subaddress(logger: Logger) { + let mut rng: StdRng = SeedableRng::from_seed([20u8; 32]); + let (client, mut ledger_db, db_ctx, _network_state) = setup(&mut rng, logger.clone()); + + // Add an account + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "create_account", + "params": { + "name": "Alice Main Account", + } + }); + let res = dispatch(&client, body, &logger); + let result = res.get("result").unwrap(); + let account_id = result + .get("account") + .unwrap() + .get("account_id") + .unwrap() + .as_str() + .unwrap(); + + // Create a subaddress + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "assign_address_for_account", + "params": { + "account_id": account_id, + "comment": "For Bob", + } + }); + let res = dispatch(&client, body, &logger); + let result = res.get("result").unwrap(); + let b58_public_address = result + .get("address") + .unwrap() + .get("public_address") + .unwrap() + .as_str() + .unwrap(); + let from_bob_public_address = b58_decode_public_address(b58_public_address).unwrap(); + + // Add a block to the ledger with a transaction "From Bob" + add_block_to_ledger_db( + &mut ledger_db, + &vec![from_bob_public_address], + 42000000000000, + &vec![KeyImage::from(rng.next_u64())], + &mut rng, + ); + + manually_sync_account( + &ledger_db, + &db_ctx.get_db_instance(logger.clone()), + &AccountID(account_id.to_string()), + &logger, + ); + + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "get_txos_for_account", + "params": { + "account_id": account_id, + } + }); + let res = dispatch(&client, body, &logger); + let result = res.get("result").unwrap(); + let txos = result.get("txo_ids").unwrap().as_array().unwrap(); + assert_eq!(txos.len(), 1); + let txo_map = result.get("txo_map").unwrap().as_object().unwrap(); + let txo = &txo_map.get(txos[0].as_str().unwrap()).unwrap(); + let status_map = txo + .get("account_status_map") + .unwrap() + .as_object() + .unwrap() + .get(account_id) + .unwrap(); + let txo_status = status_map.get("txo_status").unwrap().as_str().unwrap(); + assert_eq!(txo_status, "txo_status_unspent"); + let txo_type = status_map.get("txo_type").unwrap().as_str().unwrap(); + assert_eq!(txo_type, "txo_type_received"); + let value = txo.get("value_pmob").unwrap().as_str().unwrap(); + assert_eq!(value, "42000000000000"); + } + + #[test_with_logger] + fn test_get_address_for_account(logger: Logger) { + let mut rng: StdRng = SeedableRng::from_seed([20u8; 32]); + let (client, _ledger_db, _db_ctx, _network_state) = setup(&mut rng, logger.clone()); + + // Add an account + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "create_account", + "params": { + "name": "Alice Main Account", + } + }); + let res = dispatch(&client, body, &logger); + let result = res.get("result").unwrap(); + let account_id = result + .get("account") + .unwrap() + .get("account_id") + .unwrap() + .as_str() + .unwrap(); + + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "get_address_for_account", + "params": { + "account_id": account_id, + "index": 2, + } + }); + let res = dispatch(&client, body, &logger); + let error = res.get("error").unwrap(); + let code = error.get("code").unwrap(); + assert_eq!(code, -32603); + + // Create a subaddress + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "assign_address_for_account", + "params": { + "account_id": account_id, + "comment": "test", + } + }); + dispatch(&client, body, &logger); + + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "get_address_for_account", + "params": { + "account_id": account_id, + "index": 2, + } + }); + let res = dispatch(&client, body, &logger); + let result = res.get("result").unwrap(); + let address = result.get("address").unwrap(); + let subaddress_index = address.get("subaddress_index").unwrap().as_str().unwrap(); + + assert_eq!(subaddress_index, "2"); + } + + #[test_with_logger] + fn test_verify_address(logger: Logger) { + let mut rng: StdRng = SeedableRng::from_seed([20u8; 32]); + let (client, _ledger_db, _db_ctx, _network_state) = setup(&mut rng, logger.clone()); + + // Add an account + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "verify_address", + "params": { + "address": "NOTVALIDB58", + } + }); + let res = dispatch(&client, body, &logger); + let result = res["result"]["verified"].as_bool().unwrap(); + assert!(!result); + + // Add an account + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "create_account", + "params": { + "name": "Alice Main Account", + } + }); + let res = dispatch(&client, body, &logger); + let b58_public_address = res["result"]["account"]["main_address"].as_str().unwrap(); + + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "verify_address", + "params": { + "address": b58_public_address, + } + }); + let res = dispatch(&client, body, &logger); + let result = res["result"]["verified"].as_bool().unwrap(); + assert!(result); + } +} diff --git a/full-service/src/json_rpc/v1/e2e_tests/account/account_balance.rs b/full-service/src/json_rpc/v1/e2e_tests/account/account_balance.rs new file mode 100644 index 000000000..777a87efc --- /dev/null +++ b/full-service/src/json_rpc/v1/e2e_tests/account/account_balance.rs @@ -0,0 +1,253 @@ +// Copyright (c) 2020-2022 MobileCoin Inc. + +//! End-to-end tests for the Full Service Wallet API. + +#[cfg(test)] +mod e2e_account { + use crate::{ + db::account::AccountID, + json_rpc::v1::api::test_utils::{dispatch, setup}, + test_utils::{add_block_to_ledger_db, manually_sync_account, MOB}, + util::b58::b58_decode_public_address, + }; + + use mc_common::logger::{test_with_logger, Logger}; + use mc_crypto_rand::rand_core::RngCore; + + use mc_transaction_core::{ring_signature::KeyImage, tokens::Mob, Token}; + use rand::{rngs::StdRng, SeedableRng}; + + #[test_with_logger] + fn test_e2e_get_balance(logger: Logger) { + let mut rng: StdRng = SeedableRng::from_seed([20u8; 32]); + let (client, mut ledger_db, db_ctx, _network_state) = setup(&mut rng, logger.clone()); + + // Add an account + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "create_account", + "params": { + "name": "Alice Main Account", + } + }); + let res = dispatch(&client, body, &logger); + let result = res.get("result").unwrap(); + let account_obj = result.get("account").unwrap(); + let account_id = account_obj.get("account_id").unwrap().as_str().unwrap(); + let b58_public_address = account_obj.get("main_address").unwrap().as_str().unwrap(); + let public_address = b58_decode_public_address(b58_public_address).unwrap(); + + // Add a block with a txo for this address + add_block_to_ledger_db( + &mut ledger_db, + &vec![public_address], + 42 * MOB, + &vec![KeyImage::from(rng.next_u64())], + &mut rng, + ); + + manually_sync_account( + &ledger_db, + &db_ctx.get_db_instance(logger.clone()), + &AccountID(account_id.to_string()), + &logger, + ); + + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "get_balance_for_account", + "params": { + "account_id": account_id, + } + }); + let res = dispatch(&client, body, &logger); + let result = res.get("result").unwrap(); + let balance = result.get("balance").unwrap(); + assert_eq!( + balance + .get("unspent_pmob") + .unwrap() + .as_str() + .unwrap() + .to_string(), + (42 * MOB).to_string() + ); + assert_eq!( + balance + .get("max_spendable_pmob") + .unwrap() + .as_str() + .unwrap() + .to_string(), + (42 * MOB - Mob::MINIMUM_FEE).to_string() + ); + + assert_eq!( + balance["account_block_height"] + .as_str() + .unwrap() + .parse::() + .expect("Could not parse u64"), + 13 + ); + } + + #[test_with_logger] + fn test_balance_for_address(logger: Logger) { + let mut rng: StdRng = SeedableRng::from_seed([20u8; 32]); + let (client, mut ledger_db, db_ctx, _network_state) = setup(&mut rng, logger.clone()); + + // Add an account + let body = json!({ + "jsonrpc": "2.0", + "api_version": "2", + "id": 1, + "method": "create_account", + "params": { + "name": "Alice Main Account", + } + }); + let res = dispatch(&client, body, &logger); + let account_id = res["result"]["account"]["account_id"].as_str().unwrap(); + let b58_public_address = res["result"]["account"]["main_address"].as_str().unwrap(); + + let alice_public_address = b58_decode_public_address(&b58_public_address) + .expect("Could not b58_decode public address"); + add_block_to_ledger_db( + &mut ledger_db, + &vec![alice_public_address], + 42 * MOB, + &vec![KeyImage::from(rng.next_u64())], + &mut rng, + ); + // + manually_sync_account( + &ledger_db, + &db_ctx.get_db_instance(logger.clone()), + &AccountID(account_id.to_string()), + &logger, + ); + + let body = json!({ + "jsonrpc": "2.0", + "api_version": "2", + "id": 1, + "method": "get_balance_for_address", + "params": { + "address": b58_public_address, + } + }); + let res = dispatch(&client, body, &logger); + let balance = res["result"]["balance"].clone(); + assert_eq!( + balance["unspent_pmob"] + .as_str() + .unwrap() + .parse::() + .expect("Could not parse u64"), + 42 * MOB + ); + assert_eq!( + balance["pending_pmob"] + .as_str() + .unwrap() + .parse::() + .expect("Could not parse u64"), + 0 + ); + assert_eq!( + balance["spent_pmob"] + .as_str() + .unwrap() + .parse::() + .expect("Could not parse u64"), + 0 + ); + assert_eq!( + balance["secreted_pmob"] + .as_str() + .unwrap() + .parse::() + .expect("Could not parse u64"), + 0 + ); + assert_eq!( + balance["orphaned_pmob"] + .as_str() + .unwrap() + .parse::() + .expect("Could not parse u64"), + 0 + ); + + assert_eq!( + balance["account_block_height"] + .as_str() + .unwrap() + .parse::() + .expect("Could not parse u64"), + 13 + ); + + // Create a subaddress + let body = json!({ + "jsonrpc": "2.0", + "api_version": "2", + "id": 1, + "method": "assign_address_for_account", + "params": { + "account_id": account_id, + "comment": "For Bob", + } + }); + let res = dispatch(&client, body, &logger); + let result = res.get("result").unwrap(); + let from_bob_b58_public_address = result + .get("address") + .unwrap() + .get("public_address") + .unwrap() + .as_str() + .unwrap(); + let from_bob_public_address = + b58_decode_public_address(from_bob_b58_public_address).unwrap(); + + // Add a block to the ledger with a transaction "From Bob" + add_block_to_ledger_db( + &mut ledger_db, + &vec![from_bob_public_address], + 64 * MOB, + &vec![KeyImage::from(rng.next_u64())], + &mut rng, + ); + // + manually_sync_account( + &ledger_db, + &db_ctx.get_db_instance(logger.clone()), + &AccountID(account_id.to_string()), + &logger, + ); + + let body = json!({ + "jsonrpc": "2.0", + "api_version": "2", + "id": 1, + "method": "get_balance_for_address", + "params": { + "address": from_bob_b58_public_address, + } + }); + let res = dispatch(&client, body, &logger); + let balance = res["result"]["balance"].clone(); + assert_eq!( + balance["unspent_pmob"] + .as_str() + .unwrap() + .parse::() + .expect("Could not parse u64"), + 64 * MOB + ); + } +} diff --git a/full-service/src/json_rpc/v1/e2e_tests/account/account_other.rs b/full-service/src/json_rpc/v1/e2e_tests/account/account_other.rs new file mode 100644 index 000000000..5e48efe11 --- /dev/null +++ b/full-service/src/json_rpc/v1/e2e_tests/account/account_other.rs @@ -0,0 +1,187 @@ +// Copyright (c) 2020-2022 MobileCoin Inc. + +//! End-to-end tests for the Full Service Wallet API. + +#[cfg(test)] +mod e2e_account { + use crate::{ + db::account::AccountID, + json_rpc, + json_rpc::v1::api::test_utils::{dispatch, setup}, + test_utils::{add_block_to_ledger_db, manually_sync_account, MOB}, + util::b58::b58_decode_public_address, + }; + use bip39::{Language, Mnemonic}; + use mc_account_keys::{AccountKey, RootEntropy, RootIdentity}; + use mc_account_keys_slip10::Slip10Key; + use mc_common::logger::{test_with_logger, Logger}; + use mc_crypto_rand::rand_core::RngCore; + + use mc_transaction_core::ring_signature::KeyImage; + use rand::{rngs::StdRng, SeedableRng}; + + use std::convert::TryFrom; + + #[test_with_logger] + fn test_export_account_secrets(logger: Logger) { + let mut rng: StdRng = SeedableRng::from_seed([20u8; 32]); + let (client, _ledger_db, _db_ctx, _network_state) = setup(&mut rng, logger.clone()); + + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "import_account", + "params": { + "mnemonic": "sheriff odor square mistake huge skate mouse shoot purity weapon proof stuff correct concert blanket neck own shift clay mistake air viable stick group", + "key_derivation_version": "2", + "name": "Alice Main Account", + "first_block_index": "200", + } + }); + let res = dispatch(&client, body, &logger); + let account_obj = res["result"]["account"].clone(); + let account_id = account_obj["account_id"].clone(); + + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "export_account_secrets", + "params": { + "account_id": account_id, + } + }); + let res = dispatch(&client, body, &logger); + let result = res.get("result").unwrap(); + let secrets = result.get("account_secrets").unwrap(); + let phrase = secrets["mnemonic"].as_str().unwrap(); + assert_eq!(secrets["account_id"], serde_json::json!(account_id)); + assert_eq!(secrets["key_derivation_version"], serde_json::json!("2")); + + // Test that the mnemonic serializes correctly back to an AccountKey object + let mnemonic = Mnemonic::from_phrase(phrase, Language::English).unwrap(); + let account_key = Slip10Key::from(mnemonic.clone()) + .try_into_account_key( + &"".to_string(), + &"".to_string(), + &hex::decode("".to_string()).expect("invalid spki"), + ) + .unwrap(); + + assert_eq!( + serde_json::json!(json_rpc::v1::models::account_key::AccountKey::try_from( + &account_key + ) + .unwrap()), + secrets["account_key"] + ); + } + + #[test_with_logger] + fn test_export_legacy_account_secrets(logger: Logger) { + let mut rng: StdRng = SeedableRng::from_seed([20u8; 32]); + let (client, _ledger_db, _db_ctx, _network_state) = setup(&mut rng, logger.clone()); + + let entropy = "c593274dc6f6eb94242e34ae5f0ab16bc3085d45d49d9e18b8a8c6f057e6b56b"; + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "import_account_from_legacy_root_entropy", + "params": { + "entropy": entropy, + "name": "Alice Main Account", + "first_block_index": "200", + } + }); + let res = dispatch(&client, body, &logger); + let result = res.get("result").unwrap(); + let account_obj = result.get("account").unwrap(); + let account_id = account_obj.get("account_id").unwrap().as_str().unwrap(); + + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "export_account_secrets", + "params": { + "account_id": account_id, + } + }); + let res = dispatch(&client, body, &logger); + let result = res.get("result").unwrap(); + let secrets = result.get("account_secrets").unwrap(); + + assert_eq!(secrets["account_id"], serde_json::json!(account_id)); + assert_eq!(secrets["entropy"], serde_json::json!(entropy)); + assert_eq!(secrets["key_derivation_version"], serde_json::json!("1")); + + // Test that the account_key serializes correctly back to an AccountKey object + let mut entropy_slice = [0u8; 32]; + entropy_slice[0..32].copy_from_slice(&hex::decode(&entropy).unwrap().as_slice()); + let account_key = AccountKey::from(&RootIdentity::from(&RootEntropy::from(&entropy_slice))); + assert_eq!( + serde_json::json!(json_rpc::v1::models::account_key::AccountKey::try_from( + &account_key + ) + .unwrap()), + secrets["account_key"] + ); + } + + #[test_with_logger] + fn test_account_status(logger: Logger) { + let mut rng: StdRng = SeedableRng::from_seed([20u8; 32]); + let (client, mut ledger_db, db_ctx, _network_state) = setup(&mut rng, logger.clone()); + + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "create_account", + "params": { + "name": "Alice Main Account", + } + }); + let res = dispatch(&client, body, &logger); + let result = res.get("result").unwrap(); + let account_obj = result.get("account").unwrap(); + let account_id = account_obj.get("account_id").unwrap().as_str().unwrap(); + let b58_public_address = account_obj.get("main_address").unwrap().as_str().unwrap(); + let public_address = b58_decode_public_address(b58_public_address).unwrap(); + + // Add a block with a txo for this address + add_block_to_ledger_db( + &mut ledger_db, + &vec![public_address], + 42 * MOB, + &vec![KeyImage::from(rng.next_u64())], + &mut rng, + ); + + manually_sync_account( + &ledger_db, + &db_ctx.get_db_instance(logger.clone()), + &AccountID(account_id.to_string()), + &logger, + ); + + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "get_account_status", + "params": { + "account_id": account_id, + } + }); + let res = dispatch(&client, body, &logger); + let result = res.get("result").unwrap(); + let balance = result.get("balance").unwrap(); + assert_eq!( + balance + .get("unspent_pmob") + .unwrap() + .as_str() + .unwrap() + .to_string(), + (42 * MOB).to_string() + ); + let _account = result.get("account").unwrap(); + } +} diff --git a/full-service/src/json_rpc/v1/e2e_tests/account/create_import/account_crud.rs b/full-service/src/json_rpc/v1/e2e_tests/account/create_import/account_crud.rs new file mode 100644 index 000000000..403b25ed3 --- /dev/null +++ b/full-service/src/json_rpc/v1/e2e_tests/account/create_import/account_crud.rs @@ -0,0 +1,159 @@ +// Copyright (c) 2020-2022 MobileCoin Inc. + +//! End-to-end tests for the Full Service Wallet API. + +#[cfg(test)] +mod e2e_account { + use crate::json_rpc::v1::api::test_utils::{dispatch, setup}; + + use mc_common::logger::{test_with_logger, Logger}; + + use rand::{rngs::StdRng, SeedableRng}; + + #[test_with_logger] + fn test_e2e_account_crud(logger: Logger) { + let mut rng: StdRng = SeedableRng::from_seed([20u8; 32]); + let (client, _ledger_db, _db_ctx, _network_state) = setup(&mut rng, logger.clone()); + + // Create Account + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "create_account", + "params": { + "name": "Alice Main Account", + }, + }); + let res = dispatch(&client, body, &logger); + assert_eq!(res.get("jsonrpc").unwrap(), "2.0"); + + let result = res.get("result").unwrap(); + let account_obj = result.get("account").unwrap(); + assert!(account_obj.get("account_id").is_some()); + assert_eq!(account_obj.get("name").unwrap(), "Alice Main Account"); + assert!(account_obj.get("main_address").is_some()); + assert_eq!(account_obj.get("next_subaddress_index").unwrap(), "2"); + assert_eq!(account_obj.get("recovery_mode").unwrap(), false); + assert_eq!(account_obj.get("fog_enabled").unwrap(), false); + + let account_id = account_obj.get("account_id").unwrap(); + + // Read Accounts via Get All + let body = json!({ + "jsonrpc": "2.0", + "id": 2, + "method": "get_all_accounts", + }); + let res = dispatch(&client, body, &logger); + let result = res.get("result").unwrap(); + let accounts = result.get("account_ids").unwrap().as_array().unwrap(); + assert_eq!(accounts.len(), 1); + let account_map = result.get("account_map").unwrap().as_object().unwrap(); + assert_eq!( + account_map + .get(accounts[0].as_str().unwrap()) + .unwrap() + .get("account_id") + .unwrap(), + &account_id.clone() + ); + + let body = json!({ + "jsonrpc": "2.0", + "id": 2, + "method": "get_account", + "params": { + "account_id": *account_id, + } + }); + let res = dispatch(&client, body, &logger); + let result = res.get("result").unwrap(); + let name = result.get("account").unwrap().get("name").unwrap(); + assert_eq!("Alice Main Account", name.as_str().unwrap()); + + // FIXME: assert balance + + // Update Account + let body = json!({ + "jsonrpc": "2.0", + "id": 2, + "method": "update_account_name", + "params": { + "account_id": *account_id, + "name": "Eve Main Account", + } + }); + let res = dispatch(&client, body, &logger); + let result = res.get("result").unwrap(); + assert_eq!( + result.get("account").unwrap().get("name").unwrap(), + "Eve Main Account" + ); + + let body = json!({ + "jsonrpc": "2.0", + "id": 2, + "method": "get_account", + "params": { + "account_id": *account_id, + } + }); + let res = dispatch(&client, body, &logger); + let result = res.get("result").unwrap(); + let name = result.get("account").unwrap().get("name").unwrap(); + assert_eq!("Eve Main Account", name.as_str().unwrap()); + + // Remove Account + let body = json!({ + "jsonrpc": "2.0", + "id": 2, + "method": "remove_account", + "params": { + "account_id": *account_id, + } + }); + let res = dispatch(&client, body, &logger); + let result = res.get("result").unwrap(); + assert_eq!(result["removed"].as_bool().unwrap(), true,); + + let body = json!({ + "jsonrpc": "2.0", + "id": 2, + "method": "get_all_accounts", + }); + let res = dispatch(&client, body, &logger); + let result = res.get("result").unwrap(); + let accounts = result.get("account_ids").unwrap().as_array().unwrap(); + assert_eq!(accounts.len(), 0); + } + + #[test_with_logger] + fn test_e2e_create_account_with_fog(logger: Logger) { + let mut rng: StdRng = SeedableRng::from_seed([20u8; 32]); + let (client, _ledger_db, _db_ctx, _network_state) = setup(&mut rng, logger.clone()); + // Create Account + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "create_account", + "params": { + "name": "Alice Main Account", + "fog_report_url": "fog://fog-report.example.com", + "fog_report_id": "", + "fog_authority_spki": "MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAvnB9wTbTOT5uoizRYaYbw7XIEkInl8E7MGOAQj+xnC+F1rIXiCnc/t1+5IIWjbRGhWzo7RAwI5sRajn2sT4rRn9NXbOzZMvIqE4hmhmEzy1YQNDnfALAWNQ+WBbYGW+Vqm3IlQvAFFjVN1YYIdYhbLjAPdkgeVsWfcLDforHn6rR3QBZYZIlSBQSKRMY/tywTxeTCvK2zWcS0kbbFPtBcVth7VFFVPAZXhPi9yy1AvnldO6n7KLiupVmojlEMtv4FQkk604nal+j/dOplTATV8a9AJBbPRBZ/yQg57EG2Y2MRiHOQifJx0S5VbNyMm9bkS8TD7Goi59aCW6OT1gyeotWwLg60JRZTfyJ7lYWBSOzh0OnaCytRpSWtNZ6barPUeOnftbnJtE8rFhF7M4F66et0LI/cuvXYecwVwykovEVBKRF4HOK9GgSm17mQMtzrD7c558TbaucOWabYR04uhdAc3s10MkuONWG0wIQhgIChYVAGnFLvSpp2/aQEq3xrRSETxsixUIjsZyWWROkuA0IFnc8d7AmcnUBvRW7FT/5thWyk5agdYUGZ+7C1o69ihR1YxmoGh69fLMPIEOhYh572+3ckgl2SaV4uo9Gvkz8MMGRBcMIMlRirSwhCfozV2RyT5Wn1NgPpyc8zJL7QdOhL7Qxb+5WjnCVrQYHI2cCAwEAAQ==" + }, + }); + + let res = dispatch(&client, body, &logger); + assert_eq!(res.get("jsonrpc").unwrap(), "2.0"); + + let result = res.get("result").unwrap(); + let account_obj = result.get("account").unwrap(); + assert!(account_obj.get("account_id").is_some()); + assert_eq!(account_obj.get("name").unwrap(), "Alice Main Account"); + assert_eq!(account_obj.get("recovery_mode").unwrap(), false); + assert!(account_obj.get("main_address").is_some()); + assert_eq!(account_obj.get("next_subaddress_index").unwrap(), "1"); + assert_eq!(account_obj.get("fog_enabled").unwrap(), true); + } +} diff --git a/full-service/src/json_rpc/v1/e2e_tests/account/create_import/import_account.rs b/full-service/src/json_rpc/v1/e2e_tests/account/create_import/import_account.rs new file mode 100644 index 000000000..49d132003 --- /dev/null +++ b/full-service/src/json_rpc/v1/e2e_tests/account/create_import/import_account.rs @@ -0,0 +1,443 @@ +// Copyright (c) 2020-2022 MobileCoin Inc. + +//! End-to-end tests for the Full Service Wallet API. + +#[cfg(test)] +mod e2e_account { + use crate::{ + db::account::AccountID, + json_rpc::v1::api::test_utils::{dispatch, dispatch_expect_error, setup}, + test_utils::{add_block_to_ledger_db, manually_sync_account}, + util::b58::b58_decode_public_address, + }; + + use mc_common::logger::{test_with_logger, Logger}; + use mc_crypto_rand::rand_core::RngCore; + use mc_ledger_db::Ledger; + use mc_transaction_core::ring_signature::KeyImage; + use rand::{rngs::StdRng, SeedableRng}; + + #[test_with_logger] + fn test_e2e_import_account(logger: Logger) { + let mut rng: StdRng = SeedableRng::from_seed([20u8; 32]); + let (client, _ledger_db, _db_ctx, _network_state) = setup(&mut rng, logger.clone()); + + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "import_account", + "params": { + "mnemonic": "sheriff odor square mistake huge skate mouse shoot purity weapon proof stuff correct concert blanket neck own shift clay mistake air viable stick group", + "key_derivation_version": "2", + "name": "Alice Main Account", + "first_block_index": "200", + } + }); + let res = dispatch(&client, body, &logger); + let result = res.get("result").unwrap(); + let account_obj = result.get("account").unwrap(); + let public_address = account_obj.get("main_address").unwrap().as_str().unwrap(); + assert_eq!(public_address, "3CnfxAc2LvKw4FDNRVgj3GndwAhgQDd7v2Cne66GTUJyzBr3WzSikk9nJ5sCAb1jgSSKaqpWQtcEjV1nhoadVKjq2Soa8p3XZy6u2tpHdor"); + let account_id = account_obj.get("account_id").unwrap().as_str().unwrap(); + assert_eq!( + account_id, + "7872edf0d4094643213aabc92aa0d07379cfb58eda0722b21a44868f22f75b4e" + ); + + assert_eq!( + *account_obj.get("first_block_index").unwrap(), + serde_json::json!("200") + ); + assert_eq!(account_obj.get("next_subaddress_index").unwrap(), "2"); + assert_eq!(account_obj.get("fog_enabled").unwrap(), false); + } + + #[test_with_logger] + fn test_e2e_import_account_unknown_version(logger: Logger) { + let mut rng: StdRng = SeedableRng::from_seed([20u8; 32]); + let (client, _ledger_db, _db_ctx, _network_state) = setup(&mut rng, logger.clone()); + + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "import_account", + "params": { + "mnemonic": "sheriff odor square mistake huge skate mouse shoot purity weapon proof stuff correct concert blanket neck own shift clay mistake air viable stick group", + "key_derivation_version": "3", + "name": "", + } + }); + dispatch_expect_error( + &client, + body, + &logger, + json!({ + "method": "import_account", + "error": json!({ + "code": -32603, + "message": "InternalError", + "data": json!({ + "server_error": "UnknownKeyDerivation(3)", + "details": "Unknown key version version: 3", + }) + }), + "jsonrpc": "2.0", + "id": 1, + }) + .to_string(), + ); + } + + #[test_with_logger] + fn test_e2e_import_account_legacy(logger: Logger) { + let mut rng: StdRng = SeedableRng::from_seed([20u8; 32]); + let (client, _ledger_db, _db_ctx, _network_state) = setup(&mut rng, logger.clone()); + + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "import_account_from_legacy_root_entropy", + "params": { + "entropy": "c593274dc6f6eb94242e34ae5f0ab16bc3085d45d49d9e18b8a8c6f057e6b56b", + "name": "Alice Main Account", + "first_block_index": "200", + } + }); + let res = dispatch(&client, body, &logger); + let result = res.get("result").unwrap(); + let account_obj = result.get("account").unwrap(); + let public_address = account_obj.get("main_address").unwrap().as_str().unwrap(); + assert_eq!(public_address, "8JtpPPh9mV2PTLrrDz4f2j4PtUpNWnrRg8HKpnuwkZbj5j8bGqtNMNLC9E3zjzcw456215yMjkCVYK4FPZTX4gijYHiuDT31biNHrHmQmsU"); + let account_id = account_obj.get("account_id").unwrap().as_str().unwrap(); + // Catches if a change results in changed accounts_ids, which should always be + // made to be backward compatible. + assert_eq!( + account_id, + "f9957a9d050ef8dff9d8ef6f66daa608081e631b2d918988311613343827b779" + ); + assert_eq!( + *account_obj.get("first_block_index").unwrap(), + serde_json::json!("200") + ); + assert_eq!(account_obj.get("next_subaddress_index").unwrap(), "2"); + assert_eq!(account_obj.get("fog_enabled").unwrap(), false); + } + + #[test_with_logger] + fn test_e2e_import_account_fog(logger: Logger) { + let mut rng: StdRng = SeedableRng::from_seed([20u8; 32]); + let (client, _ledger_db, _db_ctx, _network_state) = setup(&mut rng, logger.clone()); + + // Import an account with fog info. + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "import_account", + "params": { + "mnemonic": "sheriff odor square mistake huge skate mouse shoot purity weapon proof stuff correct concert blanket neck own shift clay mistake air viable stick group", + "key_derivation_version": "2", + "name": "Alice Main Account", + "first_block_index": "200", + "fog_report_url": "fog://fog-report.example.com", + "fog_report_id": "", + "fog_authority_spki": "MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAvnB9wTbTOT5uoizRYaYbw7XIEkInl8E7MGOAQj+xnC+F1rIXiCnc/t1+5IIWjbRGhWzo7RAwI5sRajn2sT4rRn9NXbOzZMvIqE4hmhmEzy1YQNDnfALAWNQ+WBbYGW+Vqm3IlQvAFFjVN1YYIdYhbLjAPdkgeVsWfcLDforHn6rR3QBZYZIlSBQSKRMY/tywTxeTCvK2zWcS0kbbFPtBcVth7VFFVPAZXhPi9yy1AvnldO6n7KLiupVmojlEMtv4FQkk604nal+j/dOplTATV8a9AJBbPRBZ/yQg57EG2Y2MRiHOQifJx0S5VbNyMm9bkS8TD7Goi59aCW6OT1gyeotWwLg60JRZTfyJ7lYWBSOzh0OnaCytRpSWtNZ6barPUeOnftbnJtE8rFhF7M4F66et0LI/cuvXYecwVwykovEVBKRF4HOK9GgSm17mQMtzrD7c558TbaucOWabYR04uhdAc3s10MkuONWG0wIQhgIChYVAGnFLvSpp2/aQEq3xrRSETxsixUIjsZyWWROkuA0IFnc8d7AmcnUBvRW7FT/5thWyk5agdYUGZ+7C1o69ihR1YxmoGh69fLMPIEOhYh572+3ckgl2SaV4uo9Gvkz8MMGRBcMIMlRirSwhCfozV2RyT5Wn1NgPpyc8zJL7QdOhL7Qxb+5WjnCVrQYHI2cCAwEAAQ==" + } + }); + let res = dispatch(&client, body, &logger); + let result = res.get("result").unwrap(); + let account_obj = result.get("account").unwrap(); + let public_address = account_obj.get("main_address").unwrap().as_str().unwrap(); + assert_eq!(public_address, "2kD4vRp3DaBdRrNLNhJ5BKf5FsZxcAijoMt5pxjJpbk5jQRubngUXnd92vuXWkFyezuLgjCiKu4JHjpjNCnmzf1gAdW6PbqXsecQtp8Qr8uoeeDKrd1a5PtA6apXuDVtnrKsDCcHiJqdeSt3bRsPBvkBP4JqpGyAeKFsC7s2LQwuZ88BxFe2kyeZp5G3zENfvLaMripxTKkWGDopok2LCyA9NiCDf1vwjA5opLU7eqaRfh9"); + let account_id = account_obj.get("account_id").unwrap().as_str().unwrap(); + assert_eq!( + account_id, + "0b8a95253a7d57faf8510d8092ab55fb8610a9d691a7fa3bfafbf49945b845a2" + ); + + assert_eq!(account_obj.get("next_subaddress_index").unwrap(), "1"); + assert_eq!(account_obj.get("fog_enabled").unwrap(), true); + } + + #[test_with_logger] + fn test_e2e_import_account_legacy_fog(logger: Logger) { + let mut rng: StdRng = SeedableRng::from_seed([20u8; 32]); + let (client, _ledger_db, _db_ctx, _network_state) = setup(&mut rng, logger.clone()); + + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "import_account_from_legacy_root_entropy", + "params": { + "entropy": "c593274dc6f6eb94242e34ae5f0ab16bc3085d45d49d9e18b8a8c6f057e6b56b", + "name": "Alice Main Account", + "first_block_index": "200", + "fog_report_url": "fog://fog-report.example.com", + "fog_report_id": "", + "fog_authority_spki": "MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAvnB9wTbTOT5uoizRYaYbw7XIEkInl8E7MGOAQj+xnC+F1rIXiCnc/t1+5IIWjbRGhWzo7RAwI5sRajn2sT4rRn9NXbOzZMvIqE4hmhmEzy1YQNDnfALAWNQ+WBbYGW+Vqm3IlQvAFFjVN1YYIdYhbLjAPdkgeVsWfcLDforHn6rR3QBZYZIlSBQSKRMY/tywTxeTCvK2zWcS0kbbFPtBcVth7VFFVPAZXhPi9yy1AvnldO6n7KLiupVmojlEMtv4FQkk604nal+j/dOplTATV8a9AJBbPRBZ/yQg57EG2Y2MRiHOQifJx0S5VbNyMm9bkS8TD7Goi59aCW6OT1gyeotWwLg60JRZTfyJ7lYWBSOzh0OnaCytRpSWtNZ6barPUeOnftbnJtE8rFhF7M4F66et0LI/cuvXYecwVwykovEVBKRF4HOK9GgSm17mQMtzrD7c558TbaucOWabYR04uhdAc3s10MkuONWG0wIQhgIChYVAGnFLvSpp2/aQEq3xrRSETxsixUIjsZyWWROkuA0IFnc8d7AmcnUBvRW7FT/5thWyk5agdYUGZ+7C1o69ihR1YxmoGh69fLMPIEOhYh572+3ckgl2SaV4uo9Gvkz8MMGRBcMIMlRirSwhCfozV2RyT5Wn1NgPpyc8zJL7QdOhL7Qxb+5WjnCVrQYHI2cCAwEAAQ==" + } + }); + let res = dispatch(&client, body, &logger); + let result = res.get("result").unwrap(); + let account_obj = result.get("account").unwrap(); + let public_address = account_obj.get("main_address").unwrap().as_str().unwrap(); + assert_eq!(public_address, "d3FhtyUQDYJFpEmzoXmRtF9VA5FTLycgQBKf1JEJJj8K6UXCuwzGD2uVYw1cxzZpbSivZLSxf9nZpMgUnuRxSpJA9qCDpDZd2qtc7j2N2x4758dQ91jrSCxzyuR1aJR7zgdcgdF2KwSShUhQ5n7M9uebf2HqiCWt8vttqESJ7aRNDwiW8TVmeKWviWunzYG46c8vo4DeZYK4wFfLNdwmeSn9HXKkQVpNgzsMz87cKpHRnzn"); + let account_id = account_obj.get("account_id").unwrap().as_str().unwrap(); + // Catches if a change results in changed accounts_ids, which should always be + // made to be backward compatible. + assert_eq!( + account_id, + "9111a17691a1eecb85bbeaa789c69471e7c8b9789e0068de02204f9d7264263d" + ); + assert_eq!(account_obj.get("next_subaddress_index").unwrap(), "1"); + assert_eq!(account_obj.get("fog_enabled").unwrap(), true); + } + + #[test_with_logger] + fn test_e2e_import_delete_import(logger: Logger) { + let mut rng: StdRng = SeedableRng::from_seed([20u8; 32]); + let (client, _ledger_db, _db_ctx, _network_state) = setup(&mut rng, logger.clone()); + + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "import_account_from_legacy_root_entropy", + "params": { + "entropy": "c593274dc6f6eb94242e34ae5f0ab16bc3085d45d49d9e18b8a8c6f057e6b56b", + "name": "Alice Main Account", + "first_block_index": "200", + } + }); + let res = dispatch(&client, body, &logger); + let result = res.get("result").unwrap(); + let account_obj = result.get("account").unwrap(); + let public_address = account_obj.get("main_address").unwrap().as_str().unwrap(); + assert_eq!(public_address, "8JtpPPh9mV2PTLrrDz4f2j4PtUpNWnrRg8HKpnuwkZbj5j8bGqtNMNLC9E3zjzcw456215yMjkCVYK4FPZTX4gijYHiuDT31biNHrHmQmsU"); + let account_id = account_obj.get("account_id").unwrap().as_str().unwrap(); + // Catches if a change results in changed accounts_ids, which should always be + // made to be backward compatible. + assert_eq!( + account_id, + "f9957a9d050ef8dff9d8ef6f66daa608081e631b2d918988311613343827b779" + ); + + // Delete Account + let body = json!({ + "jsonrpc": "2.0", + "id": 2, + "method": "remove_account", + "params": { + "account_id": *account_id, + } + }); + let res = dispatch(&client, body, &logger); + let result = res.get("result").unwrap(); + assert_eq!(result["removed"].as_bool().unwrap(), true); + + // Import it again - should succeed. + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "import_account_from_legacy_root_entropy", + "params": { + "entropy": "c593274dc6f6eb94242e34ae5f0ab16bc3085d45d49d9e18b8a8c6f057e6b56b", + "name": "Alice Main Account", + "first_block_index": "200", + } + }); + let res = dispatch(&client, body, &logger); + let result = res.get("result").unwrap(); + let account_obj = result.get("account").unwrap(); + let public_address = account_obj.get("main_address").unwrap().as_str().unwrap(); + assert_eq!(public_address, "8JtpPPh9mV2PTLrrDz4f2j4PtUpNWnrRg8HKpnuwkZbj5j8bGqtNMNLC9E3zjzcw456215yMjkCVYK4FPZTX4gijYHiuDT31biNHrHmQmsU"); + } + + #[test_with_logger] + fn test_import_account_with_next_subaddress_index(logger: Logger) { + let mut rng: StdRng = SeedableRng::from_seed([20u8; 32]); + let (client, mut ledger_db, db_ctx, _network_state) = setup(&mut rng, logger.clone()); + + // create an account + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "import_account_from_legacy_root_entropy", + "params": { + "entropy": "c593274dc6f6eb94242e34ae5f0ab16bc3085d45d49d9e18b8a8c6f057e6b56b", + "name": "Alice Main Account", + } + }); + let res = dispatch(&client, body, &logger); + let result = res.get("result").unwrap(); + let account_obj = result.get("account").unwrap(); + let account_id = account_obj.get("account_id").unwrap().as_str().unwrap(); + + // assign next subaddress for account + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "assign_address_for_account", + "params": { + "account_id": account_id, + "metadata": "subaddress_index_2", + } + }); + let res = dispatch(&client, body, &logger); + let result = res.get("result").unwrap(); + let address = result.get("address").unwrap(); + let b58_public_address = address.get("public_address").unwrap().as_str().unwrap(); + let public_address = b58_decode_public_address(b58_public_address).unwrap(); + + // Add a block to fund account at the new subaddress. + add_block_to_ledger_db( + &mut ledger_db, + &vec![public_address], + 100000000000000, // 100.0 MOB + &vec![KeyImage::from(rng.next_u64())], + &mut rng, + ); + + manually_sync_account( + &ledger_db, + &db_ctx.get_db_instance(logger.clone()), + &AccountID(account_id.to_string()), + &logger, + ); + assert_eq!(ledger_db.num_blocks().unwrap(), 13); + + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "get_balance_for_account", + "params": { + "account_id": account_id, + } + }); + let res = dispatch(&client, body, &logger); + let result = res.get("result").unwrap(); + let balance = result.get("balance").unwrap(); + let unspent_pmob = balance.get("unspent_pmob").unwrap().as_str().unwrap(); + + assert_eq!("100000000000000", unspent_pmob); + + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "remove_account", + "params": { + "account_id": account_id, + } + }); + dispatch(&client, body, &logger); + + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "import_account_from_legacy_root_entropy", + "params": { + "entropy": "c593274dc6f6eb94242e34ae5f0ab16bc3085d45d49d9e18b8a8c6f057e6b56b", + "name": "Alice Main Account", + } + }); + dispatch(&client, body, &logger); + + manually_sync_account( + &ledger_db, + &db_ctx.get_db_instance(logger.clone()), + &AccountID(account_id.to_string()), + &logger, + ); + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "get_balance_for_account", + "params": { + "account_id": account_id, + } + }); + let res = dispatch(&client, body, &logger); + let result = res.get("result").unwrap(); + let balance = result.get("balance").unwrap(); + let unspent_pmob = balance.get("unspent_pmob").unwrap().as_str().unwrap(); + let orphaned_pmob = balance.get("orphaned_pmob").unwrap().as_str().unwrap(); + let spent_pmob = balance.get("spent_pmob").unwrap().as_str().unwrap(); + + assert_eq!("0", unspent_pmob); + assert_eq!("100000000000000", orphaned_pmob); + assert_eq!("0", spent_pmob); + + // assign next subaddress for account + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "assign_address_for_account", + "params": { + "account_id": account_id, + "metadata": "subaddress_index_2", + } + }); + dispatch(&client, body, &logger); + + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "get_balance_for_account", + "params": { + "account_id": account_id, + } + }); + let res = dispatch(&client, body, &logger); + let result = res.get("result").unwrap(); + let balance = result.get("balance").unwrap(); + let unspent_pmob = balance.get("unspent_pmob").unwrap().as_str().unwrap(); + let orphaned_pmob = balance.get("orphaned_pmob").unwrap().as_str().unwrap(); + + assert_eq!("100000000000000", unspent_pmob); + assert_eq!("0", orphaned_pmob); + + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "remove_account", + "params": { + "account_id": account_id, + } + }); + dispatch(&client, body, &logger); + + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "import_account_from_legacy_root_entropy", + "params": { + "entropy": "c593274dc6f6eb94242e34ae5f0ab16bc3085d45d49d9e18b8a8c6f057e6b56b", + "name": "Alice Main Account", + "next_subaddress_index": "3", + } + }); + dispatch(&client, body, &logger); + + manually_sync_account( + &ledger_db, + &db_ctx.get_db_instance(logger.clone()), + &AccountID(account_id.to_string()), + &logger, + ); + + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "get_balance_for_account", + "params": { + "account_id": account_id, + } + }); + let res = dispatch(&client, body, &logger); + let result = res.get("result").unwrap(); + let balance = result.get("balance").unwrap(); + let unspent_pmob = balance.get("unspent_pmob").unwrap().as_str().unwrap(); + let orphaned_pmob = balance.get("orphaned_pmob").unwrap().as_str().unwrap(); + + assert_eq!("100000000000000", unspent_pmob); + assert_eq!("0", orphaned_pmob); + } +} diff --git a/full-service/src/json_rpc/v1/e2e_tests/account/create_import/mod.rs b/full-service/src/json_rpc/v1/e2e_tests/account/create_import/mod.rs new file mode 100644 index 000000000..e9a1fbe7f --- /dev/null +++ b/full-service/src/json_rpc/v1/e2e_tests/account/create_import/mod.rs @@ -0,0 +1,2 @@ +mod account_crud; +mod import_account; diff --git a/full-service/src/json_rpc/v1/e2e_tests/account/mod.rs b/full-service/src/json_rpc/v1/e2e_tests/account/mod.rs new file mode 100644 index 000000000..74e2f0931 --- /dev/null +++ b/full-service/src/json_rpc/v1/e2e_tests/account/mod.rs @@ -0,0 +1,4 @@ +mod account_address; +mod account_balance; +mod account_other; +mod create_import; diff --git a/full-service/src/json_rpc/v1/e2e_tests/gift_codes.rs b/full-service/src/json_rpc/v1/e2e_tests/gift_codes.rs new file mode 100644 index 000000000..040d1d952 --- /dev/null +++ b/full-service/src/json_rpc/v1/e2e_tests/gift_codes.rs @@ -0,0 +1,213 @@ +// Copyright (c) 2020-2022 MobileCoin Inc. + +//! End-to-end tests for the Full Service Wallet API. + +#[cfg(test)] +mod e2e_transaction { + use crate::{ + db::account::AccountID, + json_rpc, + json_rpc::v1::api::test_utils::{dispatch, setup}, + test_utils::{add_block_to_ledger_db, add_block_with_tx, manually_sync_account, MOB}, + util::b58::b58_decode_public_address, + }; + + use mc_common::logger::{test_with_logger, Logger}; + use mc_crypto_rand::rand_core::RngCore; + + use mc_transaction_core::ring_signature::KeyImage; + use rand::{rngs::StdRng, SeedableRng}; + + use std::convert::TryFrom; + + #[test_with_logger] + fn test_gift_codes(logger: Logger) { + let mut rng: StdRng = SeedableRng::from_seed([20u8; 32]); + let (client, mut ledger_db, db_ctx, _network_state) = setup(&mut rng, logger.clone()); + + // Add an account + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "create_account", + "params": { + "name": "Alice Main Account", + } + }); + let res = dispatch(&client, body, &logger); + let result = res.get("result").unwrap(); + let account_obj = result.get("account").unwrap(); + let alice_account_id = account_obj.get("account_id").unwrap().as_str().unwrap(); + let alice_b58_public_address = account_obj.get("main_address").unwrap().as_str().unwrap(); + let alice_public_address = b58_decode_public_address(alice_b58_public_address).unwrap(); + + // Add a block with a txo for this address + add_block_to_ledger_db( + &mut ledger_db, + &vec![alice_public_address], + 100 * MOB, + &vec![KeyImage::from(rng.next_u64())], + &mut rng, + ); + + manually_sync_account( + &ledger_db, + &db_ctx.get_db_instance(logger.clone()), + &AccountID(alice_account_id.to_string()), + &logger, + ); + // Create a gift code + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "build_gift_code", + "params": { + "account_id": alice_account_id, + "value_pmob": "42000000000000", + "memo": "Happy Birthday!", + } + }); + let res = dispatch(&client, body, &logger); + let result = res["result"].clone(); + let gift_code_b58 = result["gift_code_b58"].as_str().unwrap(); + let tx_proposal = result["tx_proposal"].clone(); + + // Check the status of the gift code + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "check_gift_code_status", + "params": { + "gift_code_b58": gift_code_b58, + } + }); + let res = dispatch(&client, body, &logger); + let status = res["result"]["gift_code_status"].as_str().unwrap(); + assert_eq!(status, "GiftCodeSubmittedPending"); + let memo = res["result"]["gift_code_memo"].as_str().unwrap(); + assert_eq!(memo, "Happy Birthday!"); + + // Submit the gift code and tx proposal + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "submit_gift_code", + "params": { + "from_account_id": alice_account_id, + "gift_code_b58": gift_code_b58, + "tx_proposal": tx_proposal, + } + }); + dispatch(&client, body, &logger); + + // Add the TxProposal for the gift code + let json_tx_proposal: json_rpc::v1::models::tx_proposal::TxProposal = + serde_json::from_value(tx_proposal.clone()).unwrap(); + let payments_tx_proposal = + mc_mobilecoind::payments::TxProposal::try_from(&json_tx_proposal).unwrap(); + + // The MockBlockchainConnection does not write to the ledger_db + add_block_with_tx(&mut ledger_db, payments_tx_proposal.tx, &mut rng); + + manually_sync_account( + &ledger_db, + &db_ctx.get_db_instance(logger.clone()), + &AccountID(alice_account_id.to_string()), + &logger, + ); + + // Check the status of the gift code + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "check_gift_code_status", + "params": { + "gift_code_b58": gift_code_b58, + } + }); + let res = dispatch(&client, body, &logger); + let status = res["result"]["gift_code_status"].as_str().unwrap(); + assert_eq!(status, "GiftCodeAvailable"); + let memo = res["result"]["gift_code_memo"].as_str().unwrap(); + assert_eq!(memo, "Happy Birthday!"); + + // Add Bob's account to our wallet + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "create_account", + "params": { + "name": "Bob Main Account", + } + }); + let res = dispatch(&client, body, &logger); + let result = res.get("result").unwrap(); + let bob_account_obj = result.get("account").unwrap(); + let bob_account_id = bob_account_obj.get("account_id").unwrap().as_str().unwrap(); + + manually_sync_account( + &ledger_db, + &db_ctx.get_db_instance(logger.clone()), + &AccountID(bob_account_id.to_string()), + &logger, + ); + + // Get all the gift codes in the wallet + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "get_all_gift_codes", + }); + let res = dispatch(&client, body, &logger); + let result = res["result"]["gift_codes"].as_array().unwrap(); + assert_eq!(result.len(), 1); + + // Get the specific gift code + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "get_gift_code", + "params": { + "gift_code_b58": gift_code_b58, + } + }); + dispatch(&client, body, &logger); + + // Claim the gift code for bob + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "claim_gift_code", + "params": { + "account_id": bob_account_id, + "gift_code_b58": gift_code_b58, + } + }); + let res = dispatch(&client, body, &logger); + let txo_id_hex = res["result"]["txo_id"].as_str().unwrap(); + assert_eq!(txo_id_hex.len(), 64); + + // Now remove that gift code + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "remove_gift_code", + "params": { + "gift_code_b58": gift_code_b58, + } + }); + let res = dispatch(&client, body, &logger); + let result = res["result"]["removed"].as_bool().unwrap(); + assert!(result); + + // Get all the gift codes in the wallet again, should be 0 now + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "get_all_gift_codes", + }); + let res = dispatch(&client, body, &logger); + let result = res["result"]["gift_codes"].as_array().unwrap(); + assert_eq!(result.len(), 0); + } +} diff --git a/full-service/src/json_rpc/v1/e2e_tests/mod.rs b/full-service/src/json_rpc/v1/e2e_tests/mod.rs new file mode 100644 index 000000000..8c71fd38e --- /dev/null +++ b/full-service/src/json_rpc/v1/e2e_tests/mod.rs @@ -0,0 +1,4 @@ +mod account; +mod gift_codes; +mod other; +mod transaction; diff --git a/full-service/src/json_rpc/v1/e2e_tests/other.rs b/full-service/src/json_rpc/v1/e2e_tests/other.rs new file mode 100644 index 000000000..a36e0d1b2 --- /dev/null +++ b/full-service/src/json_rpc/v1/e2e_tests/other.rs @@ -0,0 +1,108 @@ +// Copyright (c) 2020-2022 MobileCoin Inc. + +//! End-to-end tests for the Full Service Wallet API. + +#[cfg(test)] +mod e2e_misc { + use crate::json_rpc::v1::api::test_utils::{ + dispatch, dispatch_with_header, dispatch_with_header_expect_error, setup, + setup_with_api_key, + }; + + use mc_common::logger::{test_with_logger, Logger}; + + use rand::{rngs::StdRng, SeedableRng}; + use rocket::http::{Header, Status}; + + #[test_with_logger] + fn test_wallet_status(logger: Logger) { + let mut rng: StdRng = SeedableRng::from_seed([20u8; 32]); + let (client, _ledger_db, _db_ctx, _network_state) = setup(&mut rng, logger.clone()); + + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "create_account", + "params": { + "name": "Alice Main Account", + } + }); + let _result = dispatch(&client, body, &logger).get("result").unwrap(); + + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "get_wallet_status", + }); + let res = dispatch(&client, body, &logger); + let result = res.get("result").unwrap(); + let status = result.get("wallet_status").unwrap(); + assert_eq!(status.get("network_block_height").unwrap(), "12"); + assert_eq!(status.get("local_block_height").unwrap(), "12"); + // Syncing will have already started, so we can't determine what the min synced + // index is. + assert!(status.get("min_synced_block_index").is_some()); + assert_eq!(status.get("total_unspent_pmob").unwrap(), "0"); + assert_eq!(status.get("total_pending_pmob").unwrap(), "0"); + assert_eq!(status.get("total_spent_pmob").unwrap(), "0"); + assert_eq!(status.get("total_orphaned_pmob").unwrap(), "0"); + assert_eq!(status.get("total_secreted_pmob").unwrap(), "0"); + assert_eq!( + status.get("account_ids").unwrap().as_array().unwrap().len(), + 1 + ); + assert_eq!( + status + .get("account_map") + .unwrap() + .as_object() + .unwrap() + .len(), + 1 + ); + } + + #[test_with_logger] + fn test_request_with_correct_api_key(logger: Logger) { + let api_key = "mobilecats"; + + let mut rng: StdRng = SeedableRng::from_seed([20u8; 32]); + let (client, _ledger_db, _db_ctx, _network_state) = + setup_with_api_key(&mut rng, logger.clone(), api_key.to_string()); + + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "create_account", + "params": { + "name": "Alice Main Account", + }, + }); + + let header = Header::new("X-API-KEY", api_key); + + dispatch_with_header(&client, body, header, &logger); + } + + #[test_with_logger] + fn test_request_with_bad_api_key(logger: Logger) { + let api_key = "mobilecats"; + + let mut rng: StdRng = SeedableRng::from_seed([20u8; 32]); + let (client, _ledger_db, _db_ctx, _network_state) = + setup_with_api_key(&mut rng, logger.clone(), api_key.to_string()); + + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "create_account", + "params": { + "name": "Alice Main Account", + }, + }); + + let header = Header::new("X-API-KEY", "wrong-header"); + + dispatch_with_header_expect_error(&client, body, header, &logger, Status::Unauthorized); + } +} diff --git a/full-service/src/json_rpc/v1/e2e_tests/transaction/build_submit/build_and_submit.rs b/full-service/src/json_rpc/v1/e2e_tests/transaction/build_submit/build_and_submit.rs new file mode 100644 index 000000000..49dd15e8c --- /dev/null +++ b/full-service/src/json_rpc/v1/e2e_tests/transaction/build_submit/build_and_submit.rs @@ -0,0 +1,193 @@ +// Copyright (c) 2020-2022 MobileCoin Inc. + +//! End-to-end tests for the Full Service Wallet API. + +#[cfg(test)] +mod e2e_transaction { + use crate::{ + db::account::AccountID, + json_rpc, + json_rpc::v1::api::test_utils::{dispatch, setup}, + test_utils::{add_block_to_ledger_db, add_block_with_tx, manually_sync_account}, + util::b58::b58_decode_public_address, + }; + + use mc_common::logger::{test_with_logger, Logger}; + use mc_crypto_rand::rand_core::RngCore; + use mc_ledger_db::Ledger; + use mc_transaction_core::{ring_signature::KeyImage, tokens::Mob, Token}; + use rand::{rngs::StdRng, SeedableRng}; + + use std::convert::TryFrom; + + #[test_with_logger] + fn test_build_and_submit_transaction(logger: Logger) { + let mut rng: StdRng = SeedableRng::from_seed([20u8; 32]); + let (client, mut ledger_db, db_ctx, _network_state) = setup(&mut rng, logger.clone()); + + // Add an account + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "create_account", + "params": { + "name": "Alice Main Account", + } + }); + let res = dispatch(&client, body, &logger); + let result = res.get("result").unwrap(); + let account_obj = result.get("account").unwrap(); + let account_id = account_obj.get("account_id").unwrap().as_str().unwrap(); + let b58_public_address = account_obj.get("main_address").unwrap().as_str().unwrap(); + let public_address = b58_decode_public_address(b58_public_address).unwrap(); + + // Add a block with a txo for this address (note that value is smaller than + // MINIMUM_FEE, so it is a "dust" TxOut that should get opportunistically swept + // up when we construct the transaction) + add_block_to_ledger_db( + &mut ledger_db, + &vec![public_address.clone()], + 100, + &vec![KeyImage::from(rng.next_u64())], + &mut rng, + ); + + manually_sync_account( + &ledger_db, + &db_ctx.get_db_instance(logger.clone()), + &AccountID(account_id.to_string()), + &logger, + ); + assert_eq!(ledger_db.num_blocks().unwrap(), 13); + + // Add a block with significantly more MOB + add_block_to_ledger_db( + &mut ledger_db, + &vec![public_address], + 100_000_000_000_000, // 100.0 MOB + &vec![KeyImage::from(rng.next_u64())], + &mut rng, + ); + + manually_sync_account( + &ledger_db, + &db_ctx.get_db_instance(logger.clone()), + &AccountID(account_id.to_string()), + &logger, + ); + assert_eq!(ledger_db.num_blocks().unwrap(), 14); + + // Create a tx proposal to ourselves + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "build_and_submit_transaction", + "params": { + "account_id": account_id, + "recipient_public_address": b58_public_address, + "value_pmob": "42000000000000", // 42.0 MOB + } + }); + let res = dispatch(&client, body, &logger); + let result = res.get("result").unwrap(); + let tx_proposal = result.get("tx_proposal").unwrap(); + let tx = tx_proposal.get("tx").unwrap(); + let tx_prefix = tx.get("prefix").unwrap(); + + // Assert the fee is correct in both places + let prefix_fee = tx_prefix.get("fee").unwrap().as_str().unwrap(); + let fee = tx_proposal.get("fee").unwrap(); + // FIXME: WS-9 - Note, minimum fee does not fit into i32 - need to make sure we + // are not losing precision with the JsonTxProposal treating Fee as number + assert_eq!(fee, &Mob::MINIMUM_FEE.to_string()); + assert_eq!(fee, prefix_fee); + + // Transaction builder attempts to use as many inputs as we have txos + let inputs = tx_proposal.get("input_list").unwrap().as_array().unwrap(); + assert_eq!(inputs.len(), 2); + let prefix_inputs = tx_prefix.get("inputs").unwrap().as_array().unwrap(); + assert_eq!(prefix_inputs.len(), inputs.len()); + + // One destination + let outlays = tx_proposal.get("outlay_list").unwrap().as_array().unwrap(); + assert_eq!(outlays.len(), 1); + + // Map outlay -> tx_out, should have one entry for one outlay + let outlay_index_to_tx_out_index = tx_proposal + .get("outlay_index_to_tx_out_index") + .unwrap() + .as_array() + .unwrap(); + assert_eq!(outlay_index_to_tx_out_index.len(), 1); + + // Two outputs in the prefix, one for change + let prefix_outputs = tx_prefix.get("outputs").unwrap().as_array().unwrap(); + assert_eq!(prefix_outputs.len(), 2); + + // One outlay confirmation number for our one outlay (no receipt for change) + let outlay_confirmation_numbers = tx_proposal + .get("outlay_confirmation_numbers") + .unwrap() + .as_array() + .unwrap(); + assert_eq!(outlay_confirmation_numbers.len(), 1); + + // Tombstone block = ledger height (12 to start + 2 new blocks + 10 default + // tombstone) + let prefix_tombstone = tx_prefix.get("tombstone_block").unwrap(); + assert_eq!(prefix_tombstone, "24"); + + let json_tx_proposal: json_rpc::v1::models::tx_proposal::TxProposal = + serde_json::from_value(tx_proposal.clone()).unwrap(); + let payments_tx_proposal = + mc_mobilecoind::payments::TxProposal::try_from(&json_tx_proposal).unwrap(); + + add_block_with_tx(&mut ledger_db, payments_tx_proposal.tx, &mut rng); + manually_sync_account( + &ledger_db, + &db_ctx.get_db_instance(logger.clone()), + &AccountID(account_id.to_string()), + &logger, + ); + assert_eq!(ledger_db.num_blocks().unwrap(), 15); + + // Get balance after submission + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "get_balance_for_account", + "params": { + "account_id": account_id, + } + }); + let res = dispatch(&client, body, &logger); + let result = res.get("result").unwrap(); + let balance_status = result.get("balance").unwrap(); + let unspent = balance_status + .get("unspent_pmob") + .unwrap() + .as_str() + .unwrap(); + let pending = balance_status + .get("pending_pmob") + .unwrap() + .as_str() + .unwrap(); + let spent = balance_status.get("spent_pmob").unwrap().as_str().unwrap(); + let secreted = balance_status + .get("secreted_pmob") + .unwrap() + .as_str() + .unwrap(); + let orphaned = balance_status + .get("orphaned_pmob") + .unwrap() + .as_str() + .unwrap(); + assert_eq!(unspent, &(100000000000100 - Mob::MINIMUM_FEE).to_string()); + assert_eq!(pending, "0"); + assert_eq!(spent, "100000000000100"); + assert_eq!(secreted, "0"); + assert_eq!(orphaned, "0"); + } +} diff --git a/full-service/src/json_rpc/v1/e2e_tests/transaction/build_submit/build_then_submit.rs b/full-service/src/json_rpc/v1/e2e_tests/transaction/build_submit/build_then_submit.rs new file mode 100644 index 000000000..3217dd50a --- /dev/null +++ b/full-service/src/json_rpc/v1/e2e_tests/transaction/build_submit/build_then_submit.rs @@ -0,0 +1,392 @@ +// Copyright (c) 2020-2022 MobileCoin Inc. + +//! End-to-end tests for the Full Service Wallet API. + +#[cfg(test)] +mod e2e_transaction { + use crate::{ + db::account::AccountID, + json_rpc, + json_rpc::v1::api::test_utils::{dispatch, dispatch_expect_error, setup}, + test_utils::{add_block_to_ledger_db, add_block_with_tx, manually_sync_account}, + util::b58::b58_decode_public_address, + }; + + use mc_common::logger::{test_with_logger, Logger}; + use mc_crypto_rand::rand_core::RngCore; + use mc_ledger_db::Ledger; + use mc_transaction_core::{ring_signature::KeyImage, tokens::Mob, Token}; + use rand::{rngs::StdRng, SeedableRng}; + + use std::convert::TryFrom; + + #[test_with_logger] + fn test_build_then_submit_transaction(logger: Logger) { + let mut rng: StdRng = SeedableRng::from_seed([20u8; 32]); + let (client, mut ledger_db, db_ctx, _network_state) = setup(&mut rng, logger.clone()); + + // Add an account + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "create_account", + "params": { + "name": "Alice Main Account", + } + }); + let res = dispatch(&client, body, &logger); + let result = res.get("result").unwrap(); + let account_obj = result.get("account").unwrap(); + let account_id = account_obj.get("account_id").unwrap().as_str().unwrap(); + let b58_public_address = account_obj.get("main_address").unwrap().as_str().unwrap(); + let public_address = b58_decode_public_address(b58_public_address).unwrap(); + + // Add a block with a txo for this address (note that value is smaller than + // MINIMUM_FEE, so it is a "dust" TxOut that should get opportunistically swept + // up when we construct the transaction) + add_block_to_ledger_db( + &mut ledger_db, + &vec![public_address.clone()], + 100, + &vec![KeyImage::from(rng.next_u64())], + &mut rng, + ); + + manually_sync_account( + &ledger_db, + &db_ctx.get_db_instance(logger.clone()), + &AccountID(account_id.to_string()), + &logger, + ); + assert_eq!(ledger_db.num_blocks().unwrap(), 13); + + // Create a tx proposal to ourselves + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "build_transaction", + "params": { + "account_id": account_id, + "recipient_public_address": b58_public_address, + "value_pmob": "42", + } + }); + // We will fail because we cannot afford the fee + dispatch_expect_error( + &client, + body, + &logger, + json!({ + "method": "build_transaction", + "error": json!({ + "code": -32603, + "message": "InternalError", + "data": json!({ + "server_error": format!("TransactionBuilder(WalletDb(InsufficientFundsUnderMaxSpendable(\"Max spendable value in wallet: 0, but target value: {}\")))", 42 + Mob::MINIMUM_FEE), + "details": format!("Error building transaction: Wallet DB Error: Insufficient funds from Txos under max_spendable_value: Max spendable value in wallet: 0, but target value: {}", 42 + Mob::MINIMUM_FEE), + }) + }), + "jsonrpc": "2.0", + "id": 1, + }).to_string(), + ); + + // Add a block with significantly more MOB + add_block_to_ledger_db( + &mut ledger_db, + &vec![public_address], + 100000000000000, // 100.0 MOB + &vec![KeyImage::from(rng.next_u64())], + &mut rng, + ); + + manually_sync_account( + &ledger_db, + &db_ctx.get_db_instance(logger.clone()), + &AccountID(account_id.to_string()), + &logger, + ); + assert_eq!(ledger_db.num_blocks().unwrap(), 14); + + // Create a tx proposal to ourselves + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "build_transaction", + "params": { + "account_id": account_id, + "recipient_public_address": b58_public_address, + "value_pmob": "42000000000000", // 42.0 MOB + } + }); + let res = dispatch(&client, body, &logger); + let result = res.get("result").unwrap(); + let tx_proposal = result.get("tx_proposal").unwrap(); + let tx = tx_proposal.get("tx").unwrap(); + let tx_prefix = tx.get("prefix").unwrap(); + + // Assert the fee is correct in both places + let prefix_fee = tx_prefix.get("fee").unwrap().as_str().unwrap(); + let fee = tx_proposal.get("fee").unwrap(); + // FIXME: WS-9 - Note, minimum fee does not fit into i32 - need to make sure we + // are not losing precision with the JsonTxProposal treating Fee as number + assert_eq!(fee, &Mob::MINIMUM_FEE.to_string()); + assert_eq!(fee, prefix_fee); + + // Transaction builder attempts to use as many inputs as we have txos + let inputs = tx_proposal.get("input_list").unwrap().as_array().unwrap(); + assert_eq!(inputs.len(), 2); + let prefix_inputs = tx_prefix.get("inputs").unwrap().as_array().unwrap(); + assert_eq!(prefix_inputs.len(), inputs.len()); + + // One destination + let outlays = tx_proposal.get("outlay_list").unwrap().as_array().unwrap(); + assert_eq!(outlays.len(), 1); + + // Map outlay -> tx_out, should have one entry for one outlay + let outlay_index_to_tx_out_index = tx_proposal + .get("outlay_index_to_tx_out_index") + .unwrap() + .as_array() + .unwrap(); + assert_eq!(outlay_index_to_tx_out_index.len(), 1); + + // Two outputs in the prefix, one for change + let prefix_outputs = tx_prefix.get("outputs").unwrap().as_array().unwrap(); + assert_eq!(prefix_outputs.len(), 2); + + // One outlay confirmation number for our one outlay (no receipt for change) + let outlay_confirmation_numbers = tx_proposal + .get("outlay_confirmation_numbers") + .unwrap() + .as_array() + .unwrap(); + assert_eq!(outlay_confirmation_numbers.len(), 1); + + // Tombstone block = ledger height (12 to start + 2 new blocks + 10 default + // tombstone) + let prefix_tombstone = tx_prefix.get("tombstone_block").unwrap(); + assert_eq!(prefix_tombstone, "24"); + + // Get current balance + assert_eq!(ledger_db.num_blocks().unwrap(), 14); + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "get_balance_for_account", + "params": { + "account_id": account_id, + } + }); + let res = dispatch(&client, body, &logger); + let result = res.get("result").unwrap(); + let balance_status = result.get("balance").unwrap(); + let unspent = balance_status + .get("unspent_pmob") + .unwrap() + .as_str() + .unwrap(); + assert_eq!(unspent, "100000000000100"); + + // Submit the tx_proposal + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "submit_transaction", + "params": { + "tx_proposal": tx_proposal, + "account_id": account_id, + } + }); + let res = dispatch(&client, body, &logger); + let result = res.get("result").unwrap(); + let transaction_id = result + .get("transaction_log") + .unwrap() + .get("transaction_log_id") + .unwrap() + .as_str() + .unwrap(); + // Note - we cannot test here that the transaction ID is consistent, because + // there is randomness in the transaction creation. + + let json_tx_proposal: json_rpc::v1::models::tx_proposal::TxProposal = + serde_json::from_value(tx_proposal.clone()).unwrap(); + let payments_tx_proposal = + mc_mobilecoind::payments::TxProposal::try_from(&json_tx_proposal).unwrap(); + + // The MockBlockchainConnection does not write to the ledger_db + add_block_with_tx(&mut ledger_db, payments_tx_proposal.tx, &mut rng); + manually_sync_account( + &ledger_db, + &db_ctx.get_db_instance(logger.clone()), + &AccountID(account_id.to_string()), + &logger, + ); + assert_eq!(ledger_db.num_blocks().unwrap(), 15); + + // Get balance after submission + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "get_balance_for_account", + "params": { + "account_id": account_id, + } + }); + let res = dispatch(&client, body, &logger); + let result = res.get("result").unwrap(); + let balance_status = result.get("balance").unwrap(); + let unspent = balance_status + .get("unspent_pmob") + .unwrap() + .as_str() + .unwrap(); + let pending = balance_status + .get("pending_pmob") + .unwrap() + .as_str() + .unwrap(); + let spent = balance_status.get("spent_pmob").unwrap().as_str().unwrap(); + let secreted = balance_status + .get("secreted_pmob") + .unwrap() + .as_str() + .unwrap(); + let orphaned = balance_status + .get("orphaned_pmob") + .unwrap() + .as_str() + .unwrap(); + assert_eq!(unspent, "99999600000100"); + assert_eq!(pending, "0"); + assert_eq!(spent, "100000000000100"); + assert_eq!(secreted, "0"); + assert_eq!(orphaned, "0"); + + // Get the transaction_id and verify it contains what we expect + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "get_transaction_log", + "params": { + "transaction_log_id": transaction_id, + } + }); + let res = dispatch(&client, body, &logger); + let result = res.get("result").unwrap(); + let transaction_log = result.get("transaction_log").unwrap(); + assert_eq!( + transaction_log.get("direction").unwrap().as_str().unwrap(), + "tx_direction_sent" + ); + assert_eq!( + transaction_log.get("value_pmob").unwrap().as_str().unwrap(), + "42000000000000" + ); + assert_eq!( + transaction_log.get("output_txos").unwrap()[0] + .get("recipient_address_id") + .unwrap() + .as_str() + .unwrap(), + b58_public_address + ); + transaction_log.get("account_id").unwrap().as_str().unwrap(); + assert_eq!( + transaction_log.get("fee_pmob").unwrap().as_str().unwrap(), + &Mob::MINIMUM_FEE.to_string() + ); + assert_eq!( + transaction_log.get("status").unwrap().as_str().unwrap(), + "tx_status_succeeded" + ); + assert_eq!( + transaction_log + .get("submitted_block_index") + .unwrap() + .as_str() + .unwrap(), + "14" + ); + assert_eq!( + transaction_log + .get("transaction_log_id") + .unwrap() + .as_str() + .unwrap(), + transaction_id + ); + + // Get All Transaction Logs + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "get_transaction_logs_for_account", + "params": { + "account_id": account_id, + } + }); + let res = dispatch(&client, body, &logger); + let result = res.get("result").unwrap(); + let transaction_log_ids = result + .get("transaction_log_ids") + .unwrap() + .as_array() + .unwrap(); + // We have a transaction log for each of the received, as well as the sent. + assert_eq!(transaction_log_ids.len(), 5); + + // Check the contents of the transaction log associated txos + let transaction_log_map = result.get("transaction_log_map").unwrap(); + let transaction_log = transaction_log_map.get(transaction_id).unwrap(); + assert_eq!( + transaction_log + .get("output_txos") + .unwrap() + .as_array() + .unwrap() + .len(), + 1 + ); + assert_eq!( + transaction_log + .get("input_txos") + .unwrap() + .as_array() + .unwrap() + .len(), + 2 + ); + assert_eq!( + transaction_log + .get("change_txos") + .unwrap() + .as_array() + .unwrap() + .len(), + 1 + ); + + assert_eq!( + transaction_log.get("status").unwrap().as_str().unwrap(), + "tx_status_succeeded" + ); + + // Get all Transaction Logs for a given Block + + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "get_all_transaction_logs_ordered_by_block", + }); + let res = dispatch(&client, body, &logger); + let result = res.get("result").unwrap(); + let transaction_log_map = result + .get("transaction_log_map") + .unwrap() + .as_object() + .unwrap(); + assert_eq!(transaction_log_map.len(), 5); + } +} diff --git a/full-service/src/json_rpc/v1/e2e_tests/transaction/build_submit/large_transaction.rs b/full-service/src/json_rpc/v1/e2e_tests/transaction/build_submit/large_transaction.rs new file mode 100644 index 000000000..a2b749b61 --- /dev/null +++ b/full-service/src/json_rpc/v1/e2e_tests/transaction/build_submit/large_transaction.rs @@ -0,0 +1,180 @@ +// Copyright (c) 2020-2022 MobileCoin Inc. + +//! End-to-end tests for the Full Service Wallet API. + +#[cfg(test)] +mod e2e_transaction { + use crate::{ + db::account::AccountID, + json_rpc, + json_rpc::v1::api::test_utils::{dispatch, setup}, + test_utils::{add_block_to_ledger_db, add_block_with_tx, manually_sync_account}, + util::b58::b58_decode_public_address, + }; + + use mc_common::logger::{test_with_logger, Logger}; + use mc_crypto_rand::rand_core::RngCore; + use mc_ledger_db::Ledger; + use mc_transaction_core::{ring_signature::KeyImage, tokens::Mob, Token}; + use rand::{rngs::StdRng, SeedableRng}; + + use std::convert::TryFrom; + + #[test_with_logger] + fn test_large_transaction(logger: Logger) { + let mut rng: StdRng = SeedableRng::from_seed([20u8; 32]); + let (client, mut ledger_db, db_ctx, _network_state) = setup(&mut rng, logger.clone()); + + // Add an account + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "create_account", + "params": { + "name": "Alice Main Account", + } + }); + let res = dispatch(&client, body, &logger); + let result = res.get("result").unwrap(); + let account_obj = result.get("account").unwrap(); + let account_id = account_obj.get("account_id").unwrap().as_str().unwrap(); + let b58_public_address = account_obj.get("main_address").unwrap().as_str().unwrap(); + let public_address = b58_decode_public_address(b58_public_address).unwrap(); + + // Add a block with a large txo for this address. + add_block_to_ledger_db( + &mut ledger_db, + &vec![public_address.clone()], + 11_000_000_000_000_000_000, // Eleven million MOB. + &vec![KeyImage::from(rng.next_u64())], + &mut rng, + ); + + manually_sync_account( + &ledger_db, + &db_ctx.get_db_instance(logger.clone()), + &AccountID(account_id.to_string()), + &logger, + ); + assert_eq!(ledger_db.num_blocks().unwrap(), 13); + + // Create a tx proposal to ourselves + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "build_and_submit_transaction", + "params": { + "account_id": account_id, + "recipient_public_address": b58_public_address, + "value_pmob": "10000000000000000000", // Ten million MOB, which is larger than i64::MAX picomob. + } + }); + let res = dispatch(&client, body, &logger); + let result = res.get("result").unwrap(); + let tx_proposal = result.get("tx_proposal").unwrap(); + + // Check that the value was recorded correctly. + let transaction_log = result.get("transaction_log").unwrap(); + assert_eq!( + transaction_log.get("direction").unwrap().as_str().unwrap(), + "tx_direction_sent" + ); + assert_eq!( + transaction_log.get("value_pmob").unwrap().as_str().unwrap(), + "10000000000000000000", + ); + assert_eq!( + transaction_log + .get("input_txos") + .unwrap() + .get(0) + .unwrap() + .get("value_pmob") + .unwrap() + .as_str() + .unwrap(), + 11_000_000_000_000_000_000u64.to_string(), + ); + assert_eq!( + transaction_log + .get("output_txos") + .unwrap() + .get(0) + .unwrap() + .get("value_pmob") + .unwrap() + .as_str() + .unwrap(), + 10_000_000_000_000_000_000u64.to_string(), + ); + assert_eq!( + transaction_log + .get("change_txos") + .unwrap() + .get(0) + .unwrap() + .get("value_pmob") + .unwrap() + .as_str() + .unwrap(), + (1_000_000_000_000_000_000u64 - Mob::MINIMUM_FEE).to_string(), + ); + + // Sync the proposal. + let json_tx_proposal: json_rpc::v1::models::tx_proposal::TxProposal = + serde_json::from_value(tx_proposal.clone()).unwrap(); + let payments_tx_proposal = + mc_mobilecoind::payments::TxProposal::try_from(&json_tx_proposal).unwrap(); + + add_block_with_tx(&mut ledger_db, payments_tx_proposal.tx, &mut rng); + manually_sync_account( + &ledger_db, + &db_ctx.get_db_instance(logger.clone()), + &AccountID(account_id.to_string()), + &logger, + ); + assert_eq!(ledger_db.num_blocks().unwrap(), 14); + + // Get balance after submission + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "get_balance_for_account", + "params": { + "account_id": account_id, + } + }); + let res = dispatch(&client, body, &logger); + let result = res.get("result").unwrap(); + let balance_status = result.get("balance").unwrap(); + let unspent = balance_status + .get("unspent_pmob") + .unwrap() + .as_str() + .unwrap(); + let pending = balance_status + .get("pending_pmob") + .unwrap() + .as_str() + .unwrap(); + let spent = balance_status.get("spent_pmob").unwrap().as_str().unwrap(); + let secreted = balance_status + .get("secreted_pmob") + .unwrap() + .as_str() + .unwrap(); + let orphaned = balance_status + .get("orphaned_pmob") + .unwrap() + .as_str() + .unwrap(); + assert_eq!( + unspent, + &(11_000_000_000_000_000_000u64 - Mob::MINIMUM_FEE).to_string() + ); + assert_eq!(pending, "0"); + assert_eq!(spent, 11_000_000_000_000_000_000u64.to_string()); + assert_eq!(secreted, "0"); + assert_eq!(orphaned, "0"); + } +} diff --git a/full-service/src/json_rpc/v1/e2e_tests/transaction/build_submit/mod.rs b/full-service/src/json_rpc/v1/e2e_tests/transaction/build_submit/mod.rs new file mode 100644 index 000000000..09f20f9d8 --- /dev/null +++ b/full-service/src/json_rpc/v1/e2e_tests/transaction/build_submit/mod.rs @@ -0,0 +1,4 @@ +mod build_and_submit; +mod build_then_submit; +mod large_transaction; +mod multiple_outlay; diff --git a/full-service/src/json_rpc/v1/e2e_tests/transaction/build_submit/multiple_outlay.rs b/full-service/src/json_rpc/v1/e2e_tests/transaction/build_submit/multiple_outlay.rs new file mode 100644 index 000000000..c23c0af1f --- /dev/null +++ b/full-service/src/json_rpc/v1/e2e_tests/transaction/build_submit/multiple_outlay.rs @@ -0,0 +1,366 @@ +// Copyright (c) 2020-2022 MobileCoin Inc. + +//! End-to-end tests for the Full Service Wallet API. + +#[cfg(test)] +mod e2e_transaction { + use crate::{ + db::account::AccountID, + json_rpc, + json_rpc::v1::api::test_utils::{dispatch, setup}, + test_utils::{add_block_to_ledger_db, add_block_with_tx, manually_sync_account, MOB}, + util::b58::b58_decode_public_address, + }; + + use mc_common::logger::{test_with_logger, Logger}; + use mc_crypto_rand::rand_core::RngCore; + use mc_ledger_db::Ledger; + use mc_transaction_core::{ring_signature::KeyImage, tokens::Mob, Token}; + use rand::{rngs::StdRng, SeedableRng}; + + use std::convert::TryFrom; + + #[test_with_logger] + fn test_multiple_outlay_transaction(logger: Logger) { + let mut rng: StdRng = SeedableRng::from_seed([20u8; 32]); + let (client, mut ledger_db, db_ctx, _network_state) = setup(&mut rng, logger.clone()); + + // Add some accounts. + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "create_account", + "params": { + "name": "Alice Main Account", + } + }); + let res = dispatch(&client, body, &logger); + let result = res.get("result").unwrap(); + let account_obj = result.get("account").unwrap(); + let alice_account_id = account_obj.get("account_id").unwrap().as_str().unwrap(); + let b58_public_address = account_obj.get("main_address").unwrap().as_str().unwrap(); + let alice_public_address = b58_decode_public_address(b58_public_address).unwrap(); + + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "create_account", + "params": { + "name": "Bob Main Account", + } + }); + let res = dispatch(&client, body, &logger); + let result = res.get("result").unwrap(); + let account_obj = result.get("account").unwrap(); + let bob_account_id = account_obj.get("account_id").unwrap().as_str().unwrap(); + let bob_b58_public_address = account_obj.get("main_address").unwrap().as_str().unwrap(); + + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "create_account", + "params": { + "name": "Charlie Main Account", + } + }); + let res = dispatch(&client, body, &logger); + let result = res.get("result").unwrap(); + let account_obj = result.get("account").unwrap(); + let charlie_account_id = account_obj.get("account_id").unwrap().as_str().unwrap(); + let charlie_b58_public_address = account_obj.get("main_address").unwrap().as_str().unwrap(); + + // Add some money to Alice's account. + add_block_to_ledger_db( + &mut ledger_db, + &vec![alice_public_address], + 100000000000000, // 100.0 MOB + &vec![KeyImage::from(rng.next_u64())], + &mut rng, + ); + + manually_sync_account( + &ledger_db, + &db_ctx.get_db_instance(logger.clone()), + &AccountID(alice_account_id.to_string()), + &logger, + ); + + // Create a two-output tx proposal to Bob and Charlie. + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "build_transaction", + "params": { + "account_id": alice_account_id, + "addresses_and_values": [ + [bob_b58_public_address, "42000000000000"], // 42.0 MOB + [charlie_b58_public_address, "43000000000000"], // 43.0 MOB + ] + } + }); + let res = dispatch(&client, body, &logger); + let result = res.get("result").unwrap(); + + let tx_proposal = result.get("tx_proposal").unwrap(); + let tx = tx_proposal.get("tx").unwrap(); + let tx_prefix = tx.get("prefix").unwrap(); + + // Assert the fee is correct in both places + let prefix_fee = tx_prefix.get("fee").unwrap().as_str().unwrap(); + let fee = tx_proposal.get("fee").unwrap(); + // FIXME: WS-9 - Note, minimum fee does not fit into i32 - need to make sure we + // are not losing precision with the JsonTxProposal treating Fee as number + assert_eq!(fee, &Mob::MINIMUM_FEE.to_string()); + assert_eq!(fee, prefix_fee); + + // Two destinations. + let outlays = tx_proposal.get("outlay_list").unwrap().as_array().unwrap(); + assert_eq!(outlays.len(), 2); + + // Map outlay -> tx_out, should have one entry for one outlay + let outlay_index_to_tx_out_index = tx_proposal + .get("outlay_index_to_tx_out_index") + .unwrap() + .as_array() + .unwrap(); + assert_eq!(outlay_index_to_tx_out_index.len(), 2); + + // Three outputs in the prefix, one for change + let prefix_outputs = tx_prefix.get("outputs").unwrap().as_array().unwrap(); + assert_eq!(prefix_outputs.len(), 3); + + // Two outlay confirmation numbers for our two outlays (no receipt for change) + let outlay_confirmation_numbers = tx_proposal + .get("outlay_confirmation_numbers") + .unwrap() + .as_array() + .unwrap(); + assert_eq!(outlay_confirmation_numbers.len(), 2); + + // Get balances before submitting. + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "get_balance_for_account", + "params": { + "account_id": alice_account_id, + } + }); + let res = dispatch(&client, body, &logger); + let result = res.get("result").unwrap(); + let balance_status = result.get("balance").unwrap(); + let alice_unspent = balance_status + .get("unspent_pmob") + .unwrap() + .as_str() + .unwrap(); + assert_eq!(alice_unspent, "100000000000000"); + + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "get_balance_for_account", + "params": { + "account_id": bob_account_id, + } + }); + let res = dispatch(&client, body, &logger); + let result = res.get("result").unwrap(); + let balance_status = result.get("balance").unwrap(); + let bob_unspent = balance_status + .get("unspent_pmob") + .unwrap() + .as_str() + .unwrap(); + assert_eq!(bob_unspent, "0"); + + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "get_balance_for_account", + "params": { + "account_id": charlie_account_id, + } + }); + let res = dispatch(&client, body, &logger); + let result = res.get("result").unwrap(); + let balance_status = result.get("balance").unwrap(); + let charlie_unspent = balance_status + .get("unspent_pmob") + .unwrap() + .as_str() + .unwrap(); + assert_eq!(charlie_unspent, "0"); + + // Submit the tx_proposal + assert_eq!(ledger_db.num_blocks().unwrap(), 13); + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "submit_transaction", + "params": { + "tx_proposal": tx_proposal, + "account_id": alice_account_id, + } + }); + let res = dispatch(&client, body, &logger); + let result = res.get("result").unwrap(); + let transaction_id = result + .get("transaction_log") + .unwrap() + .get("transaction_log_id") + .unwrap() + .as_str() + .unwrap(); + + let json_tx_proposal: json_rpc::v1::models::tx_proposal::TxProposal = + serde_json::from_value(tx_proposal.clone()).unwrap(); + let payments_tx_proposal = + mc_mobilecoind::payments::TxProposal::try_from(&json_tx_proposal).unwrap(); + + // The MockBlockchainConnection does not write to the ledger_db + add_block_with_tx(&mut ledger_db, payments_tx_proposal.tx, &mut rng); + assert_eq!(ledger_db.num_blocks().unwrap(), 14); + + // Wait for accounts to sync. + manually_sync_account( + &ledger_db, + &db_ctx.get_db_instance(logger.clone()), + &AccountID(alice_account_id.to_string()), + &logger, + ); + manually_sync_account( + &ledger_db, + &db_ctx.get_db_instance(logger.clone()), + &AccountID(bob_account_id.to_string()), + &logger, + ); + manually_sync_account( + &ledger_db, + &db_ctx.get_db_instance(logger.clone()), + &AccountID(charlie_account_id.to_string()), + &logger, + ); + + // Get balances after submission + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "get_balance_for_account", + "params": { + "account_id": alice_account_id, + } + }); + let res = dispatch(&client, body, &logger); + let result = res.get("result").unwrap(); + let balance_status = result.get("balance").unwrap(); + let unspent = balance_status + .get("unspent_pmob") + .unwrap() + .as_str() + .unwrap(); + assert_eq!(unspent, &(15 * MOB - Mob::MINIMUM_FEE).to_string()); + + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "get_balance_for_account", + "params": { + "account_id": bob_account_id, + } + }); + let res = dispatch(&client, body, &logger); + let result = res.get("result").unwrap(); + let balance_status = result.get("balance").unwrap(); + let bob_unspent = balance_status + .get("unspent_pmob") + .unwrap() + .as_str() + .unwrap(); + assert_eq!(bob_unspent, "42000000000000"); + + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "get_balance_for_account", + "params": { + "account_id": charlie_account_id, + } + }); + let res = dispatch(&client, body, &logger); + let result = res.get("result").unwrap(); + let balance_status = result.get("balance").unwrap(); + let charlie_unspent = balance_status + .get("unspent_pmob") + .unwrap() + .as_str() + .unwrap(); + assert_eq!(charlie_unspent, "43000000000000"); + + // Get the transaction log and verify it contains what we expect + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "get_transaction_log", + "params": { + "transaction_log_id": transaction_id, + } + }); + let res = dispatch(&client, body, &logger); + let result = res.get("result").unwrap(); + let transaction_log = result.get("transaction_log").unwrap(); + assert_eq!( + transaction_log.get("direction").unwrap().as_str().unwrap(), + "tx_direction_sent" + ); + assert_eq!( + transaction_log.get("value_pmob").unwrap().as_str().unwrap(), + "85000000000000" + ); + + let mut output_addresses: Vec = transaction_log + .get("output_txos") + .unwrap() + .as_array() + .unwrap() + .iter() + .map(|t| { + t.get("recipient_address_id") + .unwrap() + .as_str() + .unwrap() + .into() + }) + .collect(); + output_addresses.sort(); + let mut target_addresses = vec![bob_b58_public_address, charlie_b58_public_address]; + target_addresses.sort(); + assert_eq!(output_addresses, target_addresses); + + transaction_log.get("account_id").unwrap().as_str().unwrap(); + assert_eq!( + transaction_log.get("fee_pmob").unwrap().as_str().unwrap(), + &Mob::MINIMUM_FEE.to_string() + ); + assert_eq!( + transaction_log.get("status").unwrap().as_str().unwrap(), + "tx_status_succeeded" + ); + assert_eq!( + transaction_log + .get("submitted_block_index") + .unwrap() + .as_str() + .unwrap(), + "13" + ); + assert_eq!( + transaction_log + .get("transaction_log_id") + .unwrap() + .as_str() + .unwrap(), + transaction_id + ); + } +} diff --git a/full-service/src/json_rpc/v1/e2e_tests/transaction/mod.rs b/full-service/src/json_rpc/v1/e2e_tests/transaction/mod.rs new file mode 100644 index 000000000..316941ae8 --- /dev/null +++ b/full-service/src/json_rpc/v1/e2e_tests/transaction/mod.rs @@ -0,0 +1,3 @@ +mod build_submit; +mod transaction_other; +mod transaction_txo; diff --git a/full-service/src/json_rpc/v1/e2e_tests/transaction/transaction_other.rs b/full-service/src/json_rpc/v1/e2e_tests/transaction/transaction_other.rs new file mode 100644 index 000000000..5446a6e0b --- /dev/null +++ b/full-service/src/json_rpc/v1/e2e_tests/transaction/transaction_other.rs @@ -0,0 +1,560 @@ +// Copyright (c) 2020-2022 MobileCoin Inc. + +//! End-to-end tests for the Full Service Wallet API. + +#[cfg(test)] +mod e2e_transaction { + use crate::{ + db::account::AccountID, + json_rpc, + json_rpc::v1::{ + api::test_utils::{dispatch, setup}, + models::transaction_log::{TransactionLog, TxoAbbrev}, + }, + test_utils::{add_block_to_ledger_db, add_block_with_tx, manually_sync_account, MOB}, + util::b58::b58_decode_public_address, + }; + + use mc_common::logger::{test_with_logger, Logger}; + use mc_crypto_rand::rand_core::RngCore; + use mc_ledger_db::Ledger; + use mc_transaction_core::ring_signature::KeyImage; + use rand::{rngs::StdRng, SeedableRng}; + + use std::convert::TryFrom; + + #[test_with_logger] + fn test_tx_status_failed_when_tombstone_block_index_exceeded(logger: Logger) { + let mut rng: StdRng = SeedableRng::from_seed([20u8; 32]); + let (client, mut ledger_db, db_ctx, _network_state) = setup(&mut rng, logger.clone()); + + // Add an account + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "create_account", + "params": { + "name": "Alice Main Account", + } + }); + let res = dispatch(&client, body, &logger); + let result = res.get("result").unwrap(); + let account_obj = result.get("account").unwrap(); + let account_id = account_obj.get("account_id").unwrap().as_str().unwrap(); + let b58_public_address = account_obj.get("main_address").unwrap().as_str().unwrap(); + let public_address = b58_decode_public_address(b58_public_address).unwrap(); + + // Add a block with a txo for this address (note that value is smaller than + // MINIMUM_FEE, so it is a "dust" TxOut that should get opportunistically swept + // up when we construct the transaction) + add_block_to_ledger_db( + &mut ledger_db, + &vec![public_address.clone()], + 100, + &vec![KeyImage::from(rng.next_u64())], + &mut rng, + ); + + manually_sync_account( + &ledger_db, + &db_ctx.get_db_instance(logger.clone()), + &AccountID(account_id.to_string()), + &logger, + ); + assert_eq!(ledger_db.num_blocks().unwrap(), 13); + + // Add a block with significantly more MOB + add_block_to_ledger_db( + &mut ledger_db, + &vec![public_address.clone()], + 100000000000000, // 100.0 MOB + &vec![KeyImage::from(rng.next_u64())], + &mut rng, + ); + + manually_sync_account( + &ledger_db, + &db_ctx.get_db_instance(logger.clone()), + &AccountID(account_id.to_string()), + &logger, + ); + assert_eq!(ledger_db.num_blocks().unwrap(), 14); + + // Create a tx proposal to ourselves with a tombstone block of 1 + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "build_and_submit_transaction", + "params": { + "account_id": account_id, + "recipient_public_address": b58_public_address, + "value_pmob": "42000000000000", // 42.0 MOB + "tombstone_block": "16", + } + }); + let res = dispatch(&client, body, &logger); + let result = res.get("result").unwrap(); + let tx_log = result.get("transaction_log").unwrap(); + let tx_log_status = tx_log.get("status").unwrap(); + let tx_log_id = tx_log.get("transaction_log_id").unwrap(); + + assert_eq!(tx_log_status, "tx_status_pending"); + + // Add a block with 1 MOB + add_block_to_ledger_db( + &mut ledger_db, + &vec![public_address.clone()], + 1, // 100.0 MOB + &vec![KeyImage::from(rng.next_u64())], + &mut rng, + ); + + manually_sync_account( + &ledger_db, + &db_ctx.get_db_instance(logger.clone()), + &AccountID(account_id.to_string()), + &logger, + ); + + // Get balance after submission + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "get_balance_for_account", + "params": { + "account_id": account_id, + } + }); + let res = dispatch(&client, body, &logger); + let result = res.get("result").unwrap(); + let balance_status = result.get("balance").unwrap(); + let unspent = balance_status + .get("unspent_pmob") + .unwrap() + .as_str() + .unwrap(); + let pending = balance_status + .get("pending_pmob") + .unwrap() + .as_str() + .unwrap(); + assert_eq!(unspent, "1"); + assert_eq!(pending, "100000000000100"); + + // Add a block with 1 MOB to increment height 2 times, + // which should cause the previous transaction to + // become invalid and free up the TXO as well as mark + // the transaction log as TX_STATUS_FAILED + add_block_to_ledger_db( + &mut ledger_db, + &vec![public_address.clone()], + 1, // 100.0 MOB + &vec![KeyImage::from(rng.next_u64())], + &mut rng, + ); + add_block_to_ledger_db( + &mut ledger_db, + &vec![public_address.clone()], + 1, // 100.0 MOB + &vec![KeyImage::from(rng.next_u64())], + &mut rng, + ); + manually_sync_account( + &ledger_db, + &db_ctx.get_db_instance(logger.clone()), + &AccountID(account_id.to_string()), + &logger, + ); + + assert_eq!(ledger_db.num_blocks().unwrap(), 17); + + // Get tx log after syncing is finished + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "get_transaction_log", + "params": { + "transaction_log_id": tx_log_id, + } + }); + let res = dispatch(&client, body, &logger); + let result = res.get("result").unwrap(); + let tx_log = result.get("transaction_log").unwrap(); + let tx_log_status = tx_log.get("status").unwrap(); + + assert_eq!(tx_log_status, "tx_status_failed"); + + // Get balance after submission + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "get_balance_for_account", + "params": { + "account_id": account_id, + } + }); + let res = dispatch(&client, body, &logger); + let result = res.get("result").unwrap(); + let balance_status = result.get("balance").unwrap(); + let unspent = balance_status + .get("unspent_pmob") + .unwrap() + .as_str() + .unwrap(); + let pending = balance_status + .get("pending_pmob") + .unwrap() + .as_str() + .unwrap(); + let spent = balance_status.get("spent_pmob").unwrap().as_str().unwrap(); + assert_eq!(unspent, "100000000000103".to_string()); + assert_eq!(pending, "0"); + assert_eq!(spent, "0"); + } + + #[test_with_logger] + fn test_received_transaction_log(logger: Logger) { + let mut rng: StdRng = SeedableRng::from_seed([20u8; 32]); + let (client, mut ledger_db, db_ctx, _network_state) = setup(&mut rng, logger.clone()); + + // Add an account + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "create_account", + "params": { + "name": "", + } + }); + let res = dispatch(&client, body, &logger); + let result = res.get("result").unwrap(); + let account_obj = result.get("account").unwrap(); + let account_id = account_obj.get("account_id").unwrap().as_str().unwrap(); + let b58_public_address = account_obj.get("main_address").unwrap().as_str().unwrap(); + let public_address = b58_decode_public_address(b58_public_address).unwrap(); + + // Add some transactions. + add_block_to_ledger_db( + &mut ledger_db, + &vec![public_address.clone()], + 100, + &vec![KeyImage::from(rng.next_u64())], + &mut rng, + ); + + assert_eq!(ledger_db.num_blocks().unwrap(), 13); + manually_sync_account( + &ledger_db, + &db_ctx.get_db_instance(logger.clone()), + &AccountID(account_id.to_string()), + &logger, + ); + + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "get_transaction_logs_for_account", + "params": { + "account_id": account_id, + } + }); + let res = dispatch(&client, body, &logger); + let result = res.get("result").unwrap(); + let tx_log_id = result + .get("transaction_log_ids") + .unwrap() + .as_array() + .unwrap() + .get(0) + .unwrap() + .as_str() + .unwrap(); + + let tx_log_json = result + .get("transaction_log_map") + .unwrap() + .get(tx_log_id) + .unwrap(); + + let txo_abbrev_expected = TxoAbbrev { + txo_id_hex: tx_log_id.to_string(), + recipient_address_id: "".to_string(), + value_pmob: 100.to_string(), + }; + + let tx_log_expected = TransactionLog { + object: "transaction_log".to_string(), + transaction_log_id: tx_log_id.to_string(), + direction: "tx_direction_received".to_string(), + is_sent_recovered: None, + account_id: account_id.to_string(), + input_txos: vec![], + output_txos: vec![txo_abbrev_expected], + change_txos: vec![], + assigned_address_id: Some(b58_public_address.to_string()), + value_pmob: 100.to_string(), + fee_pmob: None, + submitted_block_index: None, + finalized_block_index: Some(12.to_string()), + status: "tx_status_succeeded".to_string(), + sent_time: None, + comment: "".to_string(), + failure_code: None, + failure_message: None, + }; + + let tx_log: TransactionLog = serde_json::from_value(tx_log_json.clone()).unwrap(); + + assert_eq!(tx_log, tx_log_expected); + } + + #[test_with_logger] + fn test_paginate_transactions(logger: Logger) { + let mut rng: StdRng = SeedableRng::from_seed([20u8; 32]); + let (client, mut ledger_db, db_ctx, _network_state) = setup(&mut rng, logger.clone()); + + // Add an account + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "create_account", + "params": { + "name": "", + } + }); + let res = dispatch(&client, body, &logger); + let result = res.get("result").unwrap(); + let account_obj = result.get("account").unwrap(); + let account_id = account_obj.get("account_id").unwrap().as_str().unwrap(); + let b58_public_address = account_obj.get("main_address").unwrap().as_str().unwrap(); + let public_address = b58_decode_public_address(b58_public_address).unwrap(); + + // Add some transactions. + for _ in 0..10 { + add_block_to_ledger_db( + &mut ledger_db, + &vec![public_address.clone()], + 100, + &vec![KeyImage::from(rng.next_u64())], + &mut rng, + ); + } + + assert_eq!(ledger_db.num_blocks().unwrap(), 22); + manually_sync_account( + &ledger_db, + &db_ctx.get_db_instance(logger.clone()), + &AccountID(account_id.to_string()), + &logger, + ); + + // Check that we can paginate txo output. + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "get_txos_for_account", + "params": { + "account_id": account_id, + } + }); + let res = dispatch(&client, body, &logger); + let result = res.get("result").unwrap(); + let txos_all = result.get("txo_ids").unwrap().as_array().unwrap(); + assert_eq!(txos_all.len(), 10); + + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "get_txos_for_account", + "params": { + "account_id": account_id, + "offset": "2", + "limit": "5", + } + }); + let res = dispatch(&client, body, &logger); + let result = res.get("result").unwrap(); + let txos_page = result.get("txo_ids").unwrap().as_array().unwrap(); + assert_eq!(txos_page.len(), 5); + assert_eq!(txos_all[2..7].len(), 5); + assert_eq!(txos_page[..], txos_all[2..7]); + + // Check that we can paginate transaction log output. + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "get_transaction_logs_for_account", + "params": { + "account_id": account_id, + } + }); + let res = dispatch(&client, body, &logger); + let result = res.get("result").unwrap(); + let tx_logs_all = result + .get("transaction_log_ids") + .unwrap() + .as_array() + .unwrap(); + assert_eq!(tx_logs_all.len(), 10); + + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "get_transaction_logs_for_account", + "params": { + "account_id": account_id, + "offset": "3", + "limit": "6", + } + }); + let res = dispatch(&client, body, &logger); + let result = res.get("result").unwrap(); + let tx_logs_page = result + .get("transaction_log_ids") + .unwrap() + .as_array() + .unwrap(); + assert_eq!(tx_logs_page.len(), 6); + assert_eq!(tx_logs_all[3..9].len(), 6); + assert_eq!(tx_logs_page[..], tx_logs_all[3..9]); + } + + #[test_with_logger] + fn test_receipts(logger: Logger) { + let mut rng: StdRng = SeedableRng::from_seed([20u8; 32]); + let (client, mut ledger_db, db_ctx, _network_state) = setup(&mut rng, logger.clone()); + + // Add an account + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "create_account", + "params": { + "name": "Alice Main Account", + } + }); + let res = dispatch(&client, body, &logger); + let result = res.get("result").unwrap(); + let account_obj = result.get("account").unwrap(); + let alice_account_id = account_obj.get("account_id").unwrap().as_str().unwrap(); + let alice_b58_public_address = account_obj.get("main_address").unwrap().as_str().unwrap(); + let alice_public_address = b58_decode_public_address(alice_b58_public_address).unwrap(); + + // Add a block with a txo for this address + add_block_to_ledger_db( + &mut ledger_db, + &vec![alice_public_address], + 100 * MOB, + &vec![KeyImage::from(rng.next_u64())], + &mut rng, + ); + + manually_sync_account( + &ledger_db, + &db_ctx.get_db_instance(logger.clone()), + &AccountID(alice_account_id.to_string()), + &logger, + ); + + // Add Bob's account to our wallet + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "create_account", + "params": { + "name": "Bob Main Account", + } + }); + let res = dispatch(&client, body, &logger); + let result = res.get("result").unwrap(); + let bob_account_obj = result.get("account").unwrap(); + let bob_account_id = bob_account_obj.get("account_id").unwrap().as_str().unwrap(); + let bob_b58_public_address = bob_account_obj + .get("main_address") + .unwrap() + .as_str() + .unwrap(); + + // Construct a transaction proposal from Alice to Bob + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "build_transaction", + "params": { + "account_id": alice_account_id, + "recipient_public_address": bob_b58_public_address, + "value_pmob": "42000000000000", // 42 MOB + } + }); + let res = dispatch(&client, body, &logger); + let result = res.get("result").unwrap(); + let tx_proposal = result.get("tx_proposal").unwrap(); + + // Get the receipts from the tx_proposal + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "create_receiver_receipts", + "params": { + "tx_proposal": tx_proposal + } + }); + let res = dispatch(&client, body, &logger); + let result = res.get("result").unwrap(); + let receipts = result["receiver_receipts"].as_array().unwrap(); + assert_eq!(receipts.len(), 1); + let receipt = &receipts[0]; + + // Bob checks status (should be pending before the block is added to the ledger) + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "check_receiver_receipt_status", + "params": { + "address": bob_b58_public_address, + "receiver_receipt": receipt, + } + }); + let res = dispatch(&client, body, &logger); + let result = res.get("result").unwrap(); + let status = result["receipt_transaction_status"].as_str().unwrap(); + assert_eq!(status, "TransactionPending"); + + // Add the block to the ledger with the tx proposal + let json_tx_proposal: json_rpc::v1::models::tx_proposal::TxProposal = + serde_json::from_value(tx_proposal.clone()).unwrap(); + let payments_tx_proposal = + mc_mobilecoind::payments::TxProposal::try_from(&json_tx_proposal).unwrap(); + + // The MockBlockchainConnection does not write to the ledger_db + add_block_with_tx(&mut ledger_db, payments_tx_proposal.tx, &mut rng); + + manually_sync_account( + &ledger_db, + &db_ctx.get_db_instance(logger.clone()), + &AccountID(alice_account_id.to_string()), + &logger, + ); + manually_sync_account( + &ledger_db, + &db_ctx.get_db_instance(logger.clone()), + &AccountID(bob_account_id.to_string()), + &logger, + ); + + // Bob checks status (should be successful after added to the ledger) + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "check_receiver_receipt_status", + "params": { + "address": bob_b58_public_address, + "receiver_receipt": receipt, + } + }); + let res = dispatch(&client, body, &logger); + let result = res.get("result").unwrap(); + let status = result["receipt_transaction_status"].as_str().unwrap(); + assert_eq!(status, "TransactionSuccess"); + } +} diff --git a/full-service/src/json_rpc/v1/e2e_tests/transaction/transaction_txo.rs b/full-service/src/json_rpc/v1/e2e_tests/transaction/transaction_txo.rs new file mode 100644 index 000000000..25463310a --- /dev/null +++ b/full-service/src/json_rpc/v1/e2e_tests/transaction/transaction_txo.rs @@ -0,0 +1,755 @@ +// Copyright (c) 2020-2022 MobileCoin Inc. + +//! End-to-end tests for the Full Service Wallet API. + +#[cfg(test)] +mod e2e_transaction { + use crate::{ + db::account::AccountID, + json_rpc, + json_rpc::v1::api::test_utils::{dispatch, setup}, + test_utils::{add_block_to_ledger_db, add_block_with_tx, manually_sync_account}, + util::b58::b58_decode_public_address, + }; + + use mc_common::logger::{test_with_logger, Logger}; + use mc_crypto_rand::rand_core::RngCore; + + use mc_transaction_core::ring_signature::KeyImage; + use rand::{rngs::StdRng, SeedableRng}; + + use std::convert::TryFrom; + + #[test_with_logger] + fn test_send_txo_received_from_removed_account(logger: Logger) { + use crate::db::schema::txos; + use diesel::{dsl::count, prelude::*}; + + let mut rng: StdRng = SeedableRng::from_seed([20u8; 32]); + let (client, mut ledger_db, db_ctx, _network_state) = setup(&mut rng, logger.clone()); + + let wallet_db = db_ctx.get_db_instance(logger.clone()); + + // Add three accounts. + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "create_account", + "params": { + "name": "account 1", + } + }); + let res = dispatch(&client, body, &logger); + let result = res.get("result").unwrap(); + let account_obj = result.get("account").unwrap(); + let account_id_1 = account_obj.get("account_id").unwrap().as_str().unwrap(); + let b58_public_address_1 = account_obj.get("main_address").unwrap().as_str().unwrap(); + let public_address_1 = b58_decode_public_address(b58_public_address_1).unwrap(); + + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "create_account", + "params": { + "name": "account 2", + } + }); + let res = dispatch(&client, body, &logger); + let result = res.get("result").unwrap(); + let account_obj = result.get("account").unwrap(); + let account_id_2 = account_obj.get("account_id").unwrap().as_str().unwrap(); + let b58_public_address_2 = account_obj.get("main_address").unwrap().as_str().unwrap(); + + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "create_account", + "params": { + "name": "account 3", + } + }); + let res = dispatch(&client, body, &logger); + let result = res.get("result").unwrap(); + let account_obj = result.get("account").unwrap(); + let account_id_3 = account_obj.get("account_id").unwrap().as_str().unwrap(); + let b58_public_address_3 = account_obj.get("main_address").unwrap().as_str().unwrap(); + + // Add a block to fund account 1. + assert_eq!( + txos::table + .select(count(txos::id)) + .first::(&wallet_db.get_conn().unwrap()) + .unwrap(), + 0 + ); + add_block_to_ledger_db( + &mut ledger_db, + &vec![public_address_1], + 100000000000000, // 100.0 MOB + &vec![KeyImage::from(rng.next_u64())], + &mut rng, + ); + + manually_sync_account( + &ledger_db, + &wallet_db, + &AccountID(account_id_1.to_string()), + &logger, + ); + assert_eq!( + txos::table + .select(count(txos::id)) + .first::(&wallet_db.get_conn().unwrap()) + .unwrap(), + 1 + ); + + // Send some coins to account 2. + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "build_transaction", + "params": { + "account_id": account_id_1, + "recipient_public_address": b58_public_address_2, + "value_pmob": "84000000000000", // 84.0 MOB + } + }); + let res = dispatch(&client, body, &logger); + let result = res.get("result").unwrap(); + let tx_proposal = result.get("tx_proposal").unwrap(); + + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "submit_transaction", + "params": { + "tx_proposal": tx_proposal, + "account_id": account_id_1, + } + }); + let res = dispatch(&client, body, &logger); + let result = res.get("result"); + assert!(result.is_some()); + + let json_tx_proposal: json_rpc::v1::models::tx_proposal::TxProposal = + serde_json::from_value(tx_proposal.clone()).unwrap(); + let payments_tx_proposal = + mc_mobilecoind::payments::TxProposal::try_from(&json_tx_proposal).unwrap(); + + add_block_with_tx(&mut ledger_db, payments_tx_proposal.tx, &mut rng); + + manually_sync_account( + &ledger_db, + &wallet_db, + &AccountID(account_id_2.to_string()), + &logger, + ); + assert_eq!( + txos::table + .select(count(txos::id)) + .first::(&wallet_db.get_conn().unwrap()) + .unwrap(), + 3 + ); + + // Remove account 1. + let body = json!({ + "jsonrpc": "2.0", + "id": 2, + "method": "remove_account", + "params": { + "account_id": account_id_1, + } + }); + let res = dispatch(&client, body, &logger); + let result = res.get("result").unwrap(); + assert_eq!(result["removed"].as_bool().unwrap(), true,); + assert_eq!( + txos::table + .select(count(txos::id)) + .first::(&wallet_db.get_conn().unwrap()) + .unwrap(), + 1 + ); + + // Send coins from account 2 to account 3. + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "build_transaction", + "params": { + "account_id": account_id_2, + "recipient_public_address": b58_public_address_3, + "value_pmob": "42000000000000", // 42.0 MOB + } + }); + let res = dispatch(&client, body, &logger); + let result = res.get("result").unwrap(); + let tx_proposal = result.get("tx_proposal").unwrap(); + + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "submit_transaction", + "params": { + "tx_proposal": tx_proposal, + "account_id": account_id_2, + } + }); + let res = dispatch(&client, body, &logger); + let result = res.get("result"); + assert!(result.is_some()); + + let json_tx_proposal: json_rpc::v1::models::tx_proposal::TxProposal = + serde_json::from_value(tx_proposal.clone()).unwrap(); + let payments_tx_proposal = + mc_mobilecoind::payments::TxProposal::try_from(&json_tx_proposal).unwrap(); + + add_block_with_tx(&mut ledger_db, payments_tx_proposal.tx, &mut rng); + + manually_sync_account( + &ledger_db, + &wallet_db, + &AccountID(account_id_3.to_string()), + &logger, + ); + assert_eq!( + txos::table + .select(count(txos::id)) + .first::(&wallet_db.get_conn().unwrap()) + .unwrap(), + 3 + ); + + // Check that account 3 received its coins. + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "get_balance_for_account", + "params": { + "account_id": account_id_3, + } + }); + let res = dispatch(&client, body, &logger); + let result = res.get("result").unwrap(); + let balance_status = result.get("balance").unwrap(); + let unspent = balance_status["unspent_pmob"].as_str().unwrap(); + assert_eq!(unspent, "42000000000000"); // 42.0 MOB + } + + /// This test is intended to make sure that when a subaddress is assigned + /// that it correctly generates and checks the key image against the ledger + /// db to see if the previously orphaned txo has been spent or not + #[test_with_logger] + fn test_mark_orphaned_txo_as_spent(logger: Logger) { + let mut rng: StdRng = SeedableRng::from_seed([20u8; 32]); + let (client, mut ledger_db, db_ctx, _network_state) = setup(&mut rng, logger.clone()); + + // Add an account + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "import_account", + "params": { + "mnemonic": "sheriff odor square mistake huge skate mouse shoot purity weapon proof stuff correct concert blanket neck own shift clay mistake air viable stick group", + "key_derivation_version": "2", + "name": "Alice Main Account", + } + }); + let res = dispatch(&client, body, &logger); + let result = res.get("result").unwrap(); + let account_obj = result.get("account").unwrap(); + let account_id = account_obj.get("account_id").unwrap().as_str().unwrap(); + + // Assign next subaddress for account. + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "assign_address_for_account", + "params": { + "account_id": account_id, + "metadata": "subaddress_index_2", + } + }); + let res = dispatch(&client, body, &logger); + let result = res.get("result").unwrap(); + let address = result.get("address").unwrap(); + let b58_public_address = address.get("public_address").unwrap().as_str().unwrap(); + let public_address = b58_decode_public_address(b58_public_address).unwrap(); + + // Add a block to fund account at the new subaddress. + add_block_to_ledger_db( + &mut ledger_db, + &vec![public_address.clone()], + 100000000000000, // 100.0 MOB + &vec![KeyImage::from(rng.next_u64())], + &mut rng, + ); + + add_block_to_ledger_db( + &mut ledger_db, + &vec![public_address.clone()], + 500000000000000, // 500.0 MOB + &vec![KeyImage::from(rng.next_u64())], + &mut rng, + ); + + manually_sync_account( + &ledger_db, + &db_ctx.get_db_instance(logger.clone()), + &AccountID(account_id.to_string()), + &logger, + ); + + // Remove the account. + let body = json!({ + "jsonrpc": "2.0", + "id": 2, + "method": "remove_account", + "params": { + "account_id": *account_id, + } + }); + let res = dispatch(&client, body, &logger); + let result = res.get("result").unwrap(); + assert_eq!(result["removed"].as_bool().unwrap(), true,); + + // Add the same account back. + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "import_account", + "params": { + "mnemonic": "sheriff odor square mistake huge skate mouse shoot purity weapon proof stuff correct concert blanket neck own shift clay mistake air viable stick group", + "key_derivation_version": "2", + "name": "Alice Main Account", + } + }); + let res = dispatch(&client, body, &logger); + let result = res.get("result").unwrap(); + let account_obj = result.get("account").unwrap(); + let account_id = account_obj.get("account_id").unwrap().as_str().unwrap(); + + manually_sync_account( + &ledger_db, + &db_ctx.get_db_instance(logger.clone()), + &AccountID(account_id.to_string()), + &logger, + ); + + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "get_balance_for_account", + "params": { + "account_id": *account_id, + } + }); + let res = dispatch(&client, body, &logger); + let result = res.get("result").unwrap(); + let balance = result.get("balance").unwrap(); + assert_eq!(balance.get("unspent_pmob").unwrap(), "0"); + assert_eq!(balance.get("spent_pmob").unwrap(), "0"); + assert_eq!(balance.get("orphaned_pmob").unwrap(), "600000000000000"); + + // Add back next subaddress. Txos are detected as unspent. + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "assign_address_for_account", + "params": { + "account_id": account_id, + "metadata": "subaddress_index_2", + } + }); + dispatch(&client, body, &logger); + + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "get_balance_for_account", + "params": { + "account_id": *account_id, + } + }); + let res = dispatch(&client, body, &logger); + let result = res.get("result").unwrap(); + let balance = result.get("balance").unwrap(); + assert_eq!(balance.get("unspent_pmob").unwrap(), "600000000000000"); + assert_eq!(balance.get("spent_pmob").unwrap(), "0"); + assert_eq!(balance.get("orphaned_pmob").unwrap(), "0"); + + // Create a second account. + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "create_account", + "params": { + "name": "account 2", + } + }); + let res = dispatch(&client, body, &logger); + let result = res.get("result").unwrap(); + let account_obj = result.get("account").unwrap(); + let account_id_2 = account_obj.get("account_id").unwrap().as_str().unwrap(); + let b58_public_address_2 = account_obj.get("main_address").unwrap().as_str().unwrap(); + + // Remove the second Account + let body = json!({ + "jsonrpc": "2.0", + "id": 2, + "method": "remove_account", + "params": { + "account_id": *account_id_2, + } + }); + let res = dispatch(&client, body, &logger); + let result = res.get("result").unwrap(); + assert_eq!(result["removed"].as_bool().unwrap(), true,); + + // Send some coins to the removed second account. + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "build_transaction", + "params": { + "account_id": account_id, + "recipient_public_address": b58_public_address_2, + "value_pmob": "50000000000000", // 50.0 MOB + } + }); + let res = dispatch(&client, body, &logger); + let result = res.get("result").unwrap(); + let tx_proposal = result.get("tx_proposal").unwrap(); + + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "submit_transaction", + "params": { + "tx_proposal": tx_proposal + } + }); + let res = dispatch(&client, body, &logger); + let result = res.get("result"); + assert!(result.is_some()); + + let json_tx_proposal: json_rpc::v1::models::tx_proposal::TxProposal = + serde_json::from_value(tx_proposal.clone()).unwrap(); + let payments_tx_proposal = + mc_mobilecoind::payments::TxProposal::try_from(&json_tx_proposal).unwrap(); + + add_block_with_tx(&mut ledger_db, payments_tx_proposal.tx, &mut rng); + + manually_sync_account( + &ledger_db, + &db_ctx.get_db_instance(logger.clone()), + &AccountID(account_id.to_string()), + &logger, + ); + + // The first account shows the coins are spent. + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "get_balance_for_account", + "params": { + "account_id": *account_id, + } + }); + let res = dispatch(&client, body, &logger); + let result = res.get("result").unwrap(); + let balance = result.get("balance").unwrap(); + assert_eq!(balance.get("unspent_pmob").unwrap(), "549999600000000"); + assert_eq!(balance.get("spent_pmob").unwrap(), "100000000000000"); + assert_eq!(balance.get("orphaned_pmob").unwrap(), "0"); + + // Remove the first account and add it back again. + let body = json!({ + "jsonrpc": "2.0", + "id": 2, + "method": "remove_account", + "params": { + "account_id": *account_id, + } + }); + let res = dispatch(&client, body, &logger); + let result = res.get("result").unwrap(); + assert_eq!(result["removed"].as_bool().unwrap(), true,); + + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "import_account", + "params": { + "mnemonic": "sheriff odor square mistake huge skate mouse shoot purity weapon proof stuff correct concert blanket neck own shift clay mistake air viable stick group", + "key_derivation_version": "2", + "name": "Alice Main Account", + } + }); + let res = dispatch(&client, body, &logger); + let result = res.get("result").unwrap(); + let account_obj = result.get("account").unwrap(); + let account_id = account_obj.get("account_id").unwrap().as_str().unwrap(); + + manually_sync_account( + &ledger_db, + &db_ctx.get_db_instance(logger.clone()), + &AccountID(account_id.to_string()), + &logger, + ); + + // The unspent pmob shows what wasn't sent to the second account. + // The orphaned pmob are because we haven't added back the next subaddress. + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "get_balance_for_account", + "params": { + "account_id": *account_id, + } + }); + let res = dispatch(&client, body, &logger); + let result = res.get("result").unwrap(); + let balance = result.get("balance").unwrap(); + assert_eq!(balance.get("unspent_pmob").unwrap(), "49999600000000"); + assert_eq!(balance.get("spent_pmob").unwrap(), "0"); + assert_eq!(balance.get("orphaned_pmob").unwrap(), "600000000000000"); + } + + #[test_with_logger] + fn test_get_txos(logger: Logger) { + let mut rng: StdRng = SeedableRng::from_seed([20u8; 32]); + let (client, mut ledger_db, db_ctx, _network_state) = setup(&mut rng, logger.clone()); + + // Add an account + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "create_account", + "params": { + "name": "Alice Main Account", + } + }); + let res = dispatch(&client, body, &logger); + let result = res.get("result").unwrap(); + let account_obj = result.get("account").unwrap(); + let account_id = account_obj.get("account_id").unwrap().as_str().unwrap(); + let b58_public_address = account_obj.get("main_address").unwrap().as_str().unwrap(); + let public_address = b58_decode_public_address(b58_public_address).unwrap(); + + // Add a block with a txo for this address + add_block_to_ledger_db( + &mut ledger_db, + &vec![public_address], + 100, + &vec![KeyImage::from(rng.next_u64())], + &mut rng, + ); + + manually_sync_account( + &ledger_db, + &db_ctx.get_db_instance(logger.clone()), + &AccountID(account_id.to_string()), + &logger, + ); + + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "get_txos_for_account", + "params": { + "account_id": account_id, + } + }); + let res = dispatch(&client, body, &logger); + let result = res.get("result").unwrap(); + let txos = result.get("txo_ids").unwrap().as_array().unwrap(); + assert_eq!(txos.len(), 1); + let txo_map = result.get("txo_map").unwrap().as_object().unwrap(); + let txo = txo_map.get(txos[0].as_str().unwrap()).unwrap(); + let account_status_map = txo + .get("account_status_map") + .unwrap() + .as_object() + .unwrap() + .get(account_id) + .unwrap(); + let txo_status = account_status_map + .get("txo_status") + .unwrap() + .as_str() + .unwrap(); + assert_eq!(txo_status, "txo_status_unspent"); + let txo_type = account_status_map + .get("txo_type") + .unwrap() + .as_str() + .unwrap(); + assert_eq!(txo_type, "txo_type_received"); + let value = txo.get("value_pmob").unwrap().as_str().unwrap(); + assert_eq!(value, "100"); + + // Check the overall balance for the account + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "get_balance_for_account", + "params": { + "account_id": account_id, + } + }); + let res = dispatch(&client, body, &logger); + let result = res.get("result").unwrap(); + let balance_status = result.get("balance").unwrap(); + let unspent = balance_status["unspent_pmob"].as_str().unwrap(); + assert_eq!(unspent, "100"); + } + + #[test_with_logger] + fn test_split_txo(logger: Logger) { + let mut rng: StdRng = SeedableRng::from_seed([20u8; 32]); + let (client, mut ledger_db, db_ctx, _network_state) = setup(&mut rng, logger.clone()); + + // Add an account + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "create_account", + "params": { + "name": "Alice Main Account", + } + }); + let res = dispatch(&client, body, &logger); + let result = res.get("result").unwrap(); + let account_obj = result.get("account").unwrap(); + let account_id = account_obj.get("account_id").unwrap().as_str().unwrap(); + let b58_public_address = account_obj.get("main_address").unwrap().as_str().unwrap(); + let public_address = b58_decode_public_address(b58_public_address).unwrap(); + + // Add a block with a txo for this address + add_block_to_ledger_db( + &mut ledger_db, + &vec![public_address], + 250000000000, + &vec![KeyImage::from(rng.next_u64())], + &mut rng, + ); + + manually_sync_account( + &ledger_db, + &db_ctx.get_db_instance(logger.clone()), + &AccountID(account_id.to_string()), + &logger, + ); + + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "get_txos_for_account", + "params": { + "account_id": account_id, + } + }); + let res = dispatch(&client, body, &logger); + let result = res.get("result").unwrap(); + let txos = result.get("txo_ids").unwrap().as_array().unwrap(); + assert_eq!(txos.len(), 1); + let txo_map = result.get("txo_map").unwrap().as_object().unwrap(); + let txo = txo_map.get(txos[0].as_str().unwrap()).unwrap(); + let account_status_map = txo + .get("account_status_map") + .unwrap() + .as_object() + .unwrap() + .get(account_id) + .unwrap(); + let txo_status = account_status_map + .get("txo_status") + .unwrap() + .as_str() + .unwrap(); + assert_eq!(txo_status, "txo_status_unspent"); + let txo_type = account_status_map + .get("txo_type") + .unwrap() + .as_str() + .unwrap(); + assert_eq!(txo_type, "txo_type_received"); + let value = txo.get("value_pmob").unwrap().as_str().unwrap(); + assert_eq!(value, "250000000000"); + let txo_id = &txos[0]; + + // Check the overall balance for the account + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "get_balance_for_account", + "params": { + "account_id": account_id, + } + }); + let res = dispatch(&client, body, &logger); + let result = res.get("result").unwrap(); + let balance_status = result.get("balance").unwrap(); + let unspent = balance_status["unspent_pmob"].as_str().unwrap(); + assert_eq!(unspent, "250000000000"); + + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "build_split_txo_transaction", + "params": { + "txo_id": txo_id, + "output_values": ["20000000000", "80000000000", "30000000000", "70000000000", "40000000000"], + "fee": "10000000000" + } + }); + let res = dispatch(&client, body, &logger); + let result = res.get("result").unwrap(); + let tx_proposal = result.get("tx_proposal").unwrap(); + + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "submit_transaction", + "params": { + "tx_proposal": tx_proposal, + "account_id": account_id, + } + }); + let res = dispatch(&client, body, &logger); + let result = res.get("result"); + assert!(result.is_some()); + + let json_tx_proposal: json_rpc::v1::models::tx_proposal::TxProposal = + serde_json::from_value(tx_proposal.clone()).unwrap(); + let payments_tx_proposal = + mc_mobilecoind::payments::TxProposal::try_from(&json_tx_proposal).unwrap(); + + add_block_with_tx(&mut ledger_db, payments_tx_proposal.tx, &mut rng); + + manually_sync_account( + &ledger_db, + &db_ctx.get_db_instance(logger.clone()), + &AccountID(account_id.to_string()), + &logger, + ); + + // Check the overall balance for the account + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "get_balance_for_account", + "params": { + "account_id": account_id, + } + }); + let res = dispatch(&client, body, &logger); + let result = res.get("result").unwrap(); + let balance_status = result.get("balance").unwrap(); + let unspent = balance_status["unspent_pmob"].as_str().unwrap(); + assert_eq!(unspent, "240000000000"); + } +} diff --git a/full-service/src/json_rpc/v1/mod.rs b/full-service/src/json_rpc/v1/mod.rs new file mode 100644 index 000000000..c2352b062 --- /dev/null +++ b/full-service/src/json_rpc/v1/mod.rs @@ -0,0 +1,5 @@ +pub mod api; +pub mod models; + +#[cfg(any(test))] +pub mod e2e_tests; diff --git a/full-service/src/json_rpc/account.rs b/full-service/src/json_rpc/v1/models/account.rs similarity index 68% rename from full-service/src/json_rpc/account.rs rename to full-service/src/json_rpc/v1/models/account.rs index d918c6a09..cf6e9799f 100644 --- a/full-service/src/json_rpc/account.rs +++ b/full-service/src/json_rpc/v1/models/account.rs @@ -4,7 +4,6 @@ use crate::{db, util::b58::b58_encode_public_address}; use serde_derive::{Deserialize, Serialize}; -use std::convert::TryFrom; /// An account in the wallet. /// @@ -52,29 +51,39 @@ pub struct Account { /// the default change subaddress (index 1). It also generates /// PublicAddressB58's with fog credentials. pub fog_enabled: bool, + + /// A flag that indicates if this account is a watch only account. + pub view_only: bool, } -impl TryFrom<&db::models::Account> for Account { - type Error = String; +impl Account { + pub fn new(src: &db::models::Account, next_subaddress_index: u64) -> Result { + let main_public_address = if src.view_only { + let account_key: mc_account_keys::ViewAccountKey = + mc_util_serial::decode(&src.account_key) + .map_err(|e| format!("Failed to decode view account key: {}", e))?; + account_key.default_subaddress() + } else { + let account_key: mc_account_keys::AccountKey = mc_util_serial::decode(&src.account_key) + .map_err(|e| format!("Failed to decode account key: {}", e))?; + account_key.default_subaddress() + }; - fn try_from(src: &db::models::Account) -> Result { - let account_key: mc_account_keys::AccountKey = mc_util_serial::decode(&src.account_key) - .map_err(|e| format!("Could not decode account key: {:?}", e))?; - let main_address = - b58_encode_public_address(&account_key.subaddress(src.main_subaddress_index as u64)) - .map_err(|e| format!("Could not b58 encode public address {:?}", e))?; + let main_public_address_b58 = b58_encode_public_address(&main_public_address) + .map_err(|e| format!("Could not b58 encode public address {:?}", e))?; Ok(Account { object: "account".to_string(), - account_id: src.account_id_hex.clone(), + account_id: src.id.clone(), key_derivation_version: src.key_derivation_version.to_string(), name: src.name.clone(), - main_address, - next_subaddress_index: (src.next_subaddress_index as u64).to_string(), + main_address: main_public_address_b58, + next_subaddress_index: (next_subaddress_index).to_string(), first_block_index: (src.first_block_index as u64).to_string(), next_block_index: (src.next_block_index as u64).to_string(), recovery_mode: false, fog_enabled: src.fog_enabled, + view_only: src.view_only, }) } } diff --git a/full-service/src/json_rpc/account_key.rs b/full-service/src/json_rpc/v1/models/account_key.rs similarity index 67% rename from full-service/src/json_rpc/account_key.rs rename to full-service/src/json_rpc/v1/models/account_key.rs index b9385f53e..0db8baa33 100644 --- a/full-service/src/json_rpc/account_key.rs +++ b/full-service/src/json_rpc/v1/models/account_key.rs @@ -2,7 +2,10 @@ //! API definition for the Account Key object. -use crate::util::encoding_helpers::{hex_to_ristretto, hex_to_vec, ristretto_to_hex, vec_to_hex}; +use crate::util::encoding_helpers::{ + hex_to_ristretto, hex_to_ristretto_public, hex_to_vec, ristretto_public_to_hex, + ristretto_to_hex, vec_to_hex, +}; use serde_derive::{Deserialize, Serialize}; use std::convert::TryFrom; @@ -64,6 +67,44 @@ impl TryFrom<&AccountKey> for mc_account_keys::AccountKey { } } +/// The ViewAccountKey contains a View private key and a Spend public key +#[derive(Deserialize, Serialize, Default, Debug, Clone)] +pub struct ViewAccountKey { + /// String representing the object's type. Objects of the same type share + /// the same value. + pub object: String, + + /// Private key used for view-key matching, hex-encoded Ristretto bytes. + pub view_private_key: String, + + /// Public key, hex-encoded Ristretto bytes. + pub spend_public_key: String, +} + +impl From<&mc_account_keys::ViewAccountKey> for ViewAccountKey { + fn from(src: &mc_account_keys::ViewAccountKey) -> ViewAccountKey { + ViewAccountKey { + object: "view_account_key".to_string(), + view_private_key: ristretto_to_hex(src.view_private_key()), + spend_public_key: ristretto_public_to_hex(src.spend_public_key()), + } + } +} + +impl TryFrom<&ViewAccountKey> for mc_account_keys::ViewAccountKey { + type Error = String; + + fn try_from(src: &ViewAccountKey) -> Result { + let view_private_key = hex_to_ristretto(&src.view_private_key)?; + let spend_public_key = hex_to_ristretto_public(&src.spend_public_key)?; + + Ok(mc_account_keys::ViewAccountKey::new( + view_private_key, + spend_public_key, + )) + } +} + #[cfg(test)] mod account_key_tests { use super::*; diff --git a/full-service/src/json_rpc/v1/models/account_secrets.rs b/full-service/src/json_rpc/v1/models/account_secrets.rs new file mode 100644 index 000000000..53914e1bb --- /dev/null +++ b/full-service/src/json_rpc/v1/models/account_secrets.rs @@ -0,0 +1,115 @@ +// Copyright (c) 2020-2021 MobileCoin Inc. + +//! API definition for the Account Secrets object. + +use crate::{ + db::models::Account, + json_rpc::v1::models::account_key::{AccountKey, ViewAccountKey}, +}; + +use bip39::{Language, Mnemonic}; +use serde_derive::{Deserialize, Serialize}; +use std::convert::TryFrom; + +/// The AccountSecrets contains the entropy and the account key derived from +/// that entropy. +#[derive(Deserialize, Serialize, Default, Debug, Clone)] +pub struct AccountSecrets { + /// String representing the object's type. Objects of the same type share + /// the same value. + pub object: String, + + /// The account ID for this account key in the wallet database. + pub account_id: String, + + /// The name of this account + pub name: String, + + /// The entropy from which this account key was derived, as a String + /// (version 1) + #[serde(skip_serializing_if = "Option::is_none")] + pub entropy: Option, + + /// The mnemonic from which this account key was derived, as a String + /// (version 2) + #[serde(skip_serializing_if = "Option::is_none")] + pub mnemonic: Option, + + /// The key derivation version that this mnemonic goes with + pub key_derivation_version: String, + + /// Private keys for receiving and spending MobileCoin. + #[serde(skip_serializing_if = "Option::is_none")] + pub account_key: Option, + + /// Private keys for receiving and spending MobileCoin. + #[serde(skip_serializing_if = "Option::is_none")] + pub view_account_key: Option, +} + +impl TryFrom<&Account> for AccountSecrets { + type Error = String; + + fn try_from(src: &Account) -> Result { + if src.view_only { + let view_account_key: mc_account_keys::ViewAccountKey = + mc_util_serial::decode(&src.account_key).map_err(|err| { + format!("Could not decode account key from database: {:?}", err) + })?; + + Ok(AccountSecrets { + object: "account_secrets".to_string(), + account_id: src.id.clone(), + name: src.name.clone(), + entropy: None, + mnemonic: None, + key_derivation_version: src.key_derivation_version.to_string(), + account_key: None, + view_account_key: Some(ViewAccountKey::from(&view_account_key)), + }) + } else { + let account_key: mc_account_keys::AccountKey = mc_util_serial::decode(&src.account_key) + .map_err(|err| format!("Could not decode account key from database: {:?}", err))?; + + let entropy = match src.key_derivation_version { + 1 => { + let entropy = src.entropy.as_ref().ok_or("No entropy found")?; + Some(hex::encode(entropy)) + } + _ => None, + }; + + let mnemonic = match src.key_derivation_version { + 2 => { + let entropy = src.entropy.as_ref().ok_or("No entropy found")?; + Some( + Mnemonic::from_entropy(entropy, Language::English) + .map_err(|err| { + format!("Could not decode mnemonic from entropy: {:?}", err) + })? + .phrase() + .to_string(), + ) + } + _ => None, + }; + + Ok(AccountSecrets { + object: "account_secrets".to_string(), + name: src.name.clone(), + account_id: src.id.clone(), + entropy, + mnemonic, + key_derivation_version: src.key_derivation_version.to_string(), + account_key: Some(AccountKey::try_from(&account_key).map_err(|err| { + format!( + "Could not convert account_key to json_rpc + representation: {:?}", + err + ) + })?), + view_account_key: None, + }) + } + } +} diff --git a/full-service/src/json_rpc/address.rs b/full-service/src/json_rpc/v1/models/address.rs similarity index 73% rename from full-service/src/json_rpc/address.rs rename to full-service/src/json_rpc/v1/models/address.rs index 6086b2b3e..f18ec7238 100644 --- a/full-service/src/json_rpc/address.rs +++ b/full-service/src/json_rpc/v1/models/address.rs @@ -2,7 +2,7 @@ //! API definition for the Address object. -use crate::db::models::{AssignedSubaddress, ViewOnlySubaddress}; +use crate::db::models::AssignedSubaddress; use serde_derive::{Deserialize, Serialize}; /// An address for an account in the wallet. @@ -35,22 +35,10 @@ pub struct Address { impl From<&AssignedSubaddress> for Address { fn from(src: &AssignedSubaddress) -> Address { - Address { - object: "address".to_string(), - public_address: src.assigned_subaddress_b58.clone(), - account_id: src.account_id_hex.clone(), - metadata: src.comment.clone(), - subaddress_index: (src.subaddress_index as u64).to_string(), - } - } -} - -impl From<&ViewOnlySubaddress> for Address { - fn from(src: &ViewOnlySubaddress) -> Address { Address { object: "address".to_string(), public_address: src.public_address_b58.clone(), - account_id: src.view_only_account_id_hex.clone(), + account_id: src.account_id.clone(), metadata: src.comment.clone(), subaddress_index: (src.subaddress_index as u64).to_string(), } diff --git a/full-service/src/json_rpc/v1/models/amount.rs b/full-service/src/json_rpc/v1/models/amount.rs new file mode 100644 index 000000000..85712e2b7 --- /dev/null +++ b/full-service/src/json_rpc/v1/models/amount.rs @@ -0,0 +1,103 @@ +// Copyright (c) 2020-2021 MobileCoin Inc. + +//! API definition for the Account object. + +use mc_crypto_keys::ReprBytes; +use serde::{Deserialize, Serialize}; +use std::convert::TryFrom; + +#[derive(Clone, Debug, Deserialize, Serialize)] +pub enum MaskedAmountVersion { + V1, + V2, +} + +impl Default for MaskedAmountVersion { + fn default() -> Self { + MaskedAmountVersion::V1 + } +} + +/// The encrypted amount of pMOB in a Txo. +#[derive(Deserialize, Serialize, Default, Debug, Clone)] +pub struct MaskedAmount { + /// String representing the object's type. Objects of the same type share + /// the same value. + pub object: String, + + /// A Pedersen commitment `v*G + s*H` + pub commitment: String, + + /// The masked value of pMOB in a Txo. + /// + /// The private view key is required to decrypt the amount, via: + /// `masked_value = value XOR_8 Blake2B("value_mask" || shared_secret)` + pub masked_value: String, + + /// `masked_token_id = token_id XOR_8 Blake2B(token_id_mask | + /// shared_secret)` 8 bytes long when used, 0 bytes for older amounts + /// that don't have this. + pub masked_token_id: String, + + /// The version of the masked amount. + pub version: Option, +} + +impl From<&mc_transaction_core::MaskedAmount> for MaskedAmount { + fn from(src: &mc_transaction_core::MaskedAmount) -> Self { + let version = Some(match src { + mc_transaction_core::MaskedAmount::V1(_) => MaskedAmountVersion::V1, + mc_transaction_core::MaskedAmount::V2(_) => MaskedAmountVersion::V2, + }); + + Self { + object: "amount".to_string(), + commitment: hex::encode(src.commitment().to_bytes()), + masked_value: src.get_masked_value().to_string(), + masked_token_id: hex::encode(&src.masked_token_id()), + version, + } + } +} + +impl TryFrom<&MaskedAmount> for mc_transaction_core::MaskedAmount { + type Error = String; + + fn try_from(src: &MaskedAmount) -> Result { + let mut commitment_bytes = [0u8; 32]; + commitment_bytes[0..32].copy_from_slice( + &hex::decode(&src.commitment) + .map_err(|err| format!("Could not decode hex for amount commitment: {:?}", err))?, + ); + + let commitment = (&commitment_bytes).into(); + let masked_value = src + .masked_value + .parse::() + .map_err(|err| format!("Could not parse masked value u64: {:?}", err))?; + let masked_token_id = hex::decode(&src.masked_token_id) + .map_err(|err| format!("Could not decode hex for masked token id: {:?}", err))?; + + match src.version { + // If the version is not specified, assume V1. + Some(MaskedAmountVersion::V1) | None => { + let masked_amount = mc_transaction_core::MaskedAmountV1 { + commitment, + masked_value, + masked_token_id, + }; + + Ok(mc_transaction_core::MaskedAmount::V1(masked_amount)) + } + Some(MaskedAmountVersion::V2) => { + let masked_amount = mc_transaction_core::MaskedAmountV2 { + commitment, + masked_value, + masked_token_id, + }; + + Ok(mc_transaction_core::MaskedAmount::V2(masked_amount)) + } + } + } +} diff --git a/full-service/src/json_rpc/balance.rs b/full-service/src/json_rpc/v1/models/balance.rs similarity index 74% rename from full-service/src/json_rpc/balance.rs rename to full-service/src/json_rpc/v1/models/balance.rs index 8aa71da9f..f7374a506 100644 --- a/full-service/src/json_rpc/balance.rs +++ b/full-service/src/json_rpc/v1/models/balance.rs @@ -59,20 +59,24 @@ pub struct Balance { pub orphaned_pmob: String, } -impl From<&service::balance::Balance> for Balance { - fn from(src: &service::balance::Balance) -> Balance { +impl Balance { + pub fn new( + balance: &service::balance::Balance, + account_block_height: u64, + network_status: &service::balance::NetworkStatus, + ) -> Self { Balance { object: "balance".to_string(), - network_block_height: src.network_block_height.to_string(), - local_block_height: src.local_block_height.to_string(), - account_block_height: src.synced_blocks.to_string(), - is_synced: src.synced_blocks == src.network_block_height, - unspent_pmob: src.unspent.to_string(), - max_spendable_pmob: src.max_spendable.to_string(), - pending_pmob: src.pending.to_string(), - spent_pmob: src.spent.to_string(), - secreted_pmob: src.secreted.to_string(), - orphaned_pmob: src.orphaned.to_string(), + network_block_height: network_status.network_block_height.to_string(), + local_block_height: network_status.local_block_height.to_string(), + account_block_height: account_block_height.to_string(), + is_synced: account_block_height == network_status.network_block_height, + unspent_pmob: (balance.unspent + balance.unverified).to_string(), + max_spendable_pmob: balance.max_spendable.to_string(), + pending_pmob: balance.pending.to_string(), + spent_pmob: balance.spent.to_string(), + secreted_pmob: balance.secreted.to_string(), + orphaned_pmob: balance.orphaned.to_string(), } } } diff --git a/full-service/src/json_rpc/block.rs b/full-service/src/json_rpc/v1/models/block.rs similarity index 93% rename from full-service/src/json_rpc/block.rs rename to full-service/src/json_rpc/v1/models/block.rs index 22d1a878c..7185a67b7 100644 --- a/full-service/src/json_rpc/block.rs +++ b/full-service/src/json_rpc/v1/models/block.rs @@ -17,7 +17,7 @@ pub struct Block { } impl Block { - pub fn new(block: &mc_transaction_core::Block) -> Self { + pub fn new(block: &mc_blockchain_types::Block) -> Self { let membership_element_proto = mc_api::external::TxOutMembershipElement::from(&block.root_element); Self { @@ -39,7 +39,7 @@ pub struct BlockContents { } impl BlockContents { - pub fn new(block_contents: &mc_transaction_core::BlockContents) -> Self { + pub fn new(block_contents: &mc_blockchain_types::BlockContents) -> Self { Self { key_images: block_contents .key_images diff --git a/full-service/src/json_rpc/confirmation_number.rs b/full-service/src/json_rpc/v1/models/confirmation_number.rs similarity index 100% rename from full-service/src/json_rpc/confirmation_number.rs rename to full-service/src/json_rpc/v1/models/confirmation_number.rs diff --git a/full-service/src/json_rpc/gift_code.rs b/full-service/src/json_rpc/v1/models/gift_code.rs similarity index 100% rename from full-service/src/json_rpc/gift_code.rs rename to full-service/src/json_rpc/v1/models/gift_code.rs diff --git a/full-service/src/json_rpc/v1/models/mod.rs b/full-service/src/json_rpc/v1/models/mod.rs new file mode 100644 index 000000000..d5f5a812e --- /dev/null +++ b/full-service/src/json_rpc/v1/models/mod.rs @@ -0,0 +1,16 @@ +pub mod account; +pub mod account_key; +pub mod account_secrets; +pub mod address; +pub mod amount; +pub mod balance; +pub mod block; +pub mod confirmation_number; +pub mod gift_code; +pub mod network_status; +pub mod receiver_receipt; +pub mod transaction_log; +pub mod tx_proposal; +pub mod txo; +pub mod unspent_tx_out; +pub mod wallet_status; diff --git a/full-service/src/json_rpc/network_status.rs b/full-service/src/json_rpc/v1/models/network_status.rs similarity index 83% rename from full-service/src/json_rpc/network_status.rs rename to full-service/src/json_rpc/v1/models/network_status.rs index 66db1cd80..9b60b3ec7 100644 --- a/full-service/src/json_rpc/network_status.rs +++ b/full-service/src/json_rpc/v1/models/network_status.rs @@ -4,6 +4,7 @@ use crate::service; +use mc_transaction_core::{tokens::Mob, Token}; use serde_derive::{Deserialize, Serialize}; use std::convert::TryFrom; @@ -31,11 +32,16 @@ impl TryFrom<&service::balance::NetworkStatus> for NetworkStatus { type Error = String; fn try_from(src: &service::balance::NetworkStatus) -> Result { + let fee = match src.fees.get(&Mob::ID) { + Some(fee) => fee, + None => return Err(format!("Could not find fee for token {}", Mob::ID)), + }; + Ok(NetworkStatus { object: "network_status".to_string(), network_block_height: src.network_block_height.to_string(), local_block_height: src.local_block_height.to_string(), - fee_pmob: src.fee_pmob.to_string(), + fee_pmob: fee.to_string(), block_version: src.block_version.to_string(), }) } diff --git a/full-service/src/json_rpc/receiver_receipt.rs b/full-service/src/json_rpc/v1/models/receiver_receipt.rs similarity index 96% rename from full-service/src/json_rpc/receiver_receipt.rs rename to full-service/src/json_rpc/v1/models/receiver_receipt.rs index cff7ae157..36ada4a86 100644 --- a/full-service/src/json_rpc/receiver_receipt.rs +++ b/full-service/src/json_rpc/v1/models/receiver_receipt.rs @@ -2,7 +2,7 @@ //! API definition for the ReceiverReceipt object. -use crate::{json_rpc::amount::MaskedAmount, service}; +use crate::{json_rpc::v1::models::amount::MaskedAmount, service}; use mc_crypto_keys::CompressedRistrettoPublic; use mc_transaction_core::tx::TxOutConfirmationNumber; use serde_derive::{Deserialize, Serialize}; @@ -84,6 +84,7 @@ mod tests { use mc_crypto_keys::{RistrettoPrivate, RistrettoPublic}; use mc_crypto_rand::RngCore; use mc_transaction_core::{tokens::Mob, tx::TxOut, Amount, Token}; + use mc_transaction_types::BlockVersion; use mc_util_from_random::FromRandom; use rand::{rngs::StdRng, SeedableRng}; @@ -94,6 +95,7 @@ mod tests { let account_key = AccountKey::random(&mut rng); let public_address = account_key.default_subaddress(); let txo = TxOut::new( + BlockVersion::MAX, Amount::new(rng.next_u64(), Mob::ID), &public_address, &RistrettoPrivate::from_random(&mut rng), @@ -105,6 +107,7 @@ mod tests { rng.fill_bytes(&mut proof_bytes); let confirmation_number = TxOutConfirmationNumber::from(proof_bytes); let amount = mc_transaction_core::MaskedAmount::new( + BlockVersion::MAX, Amount::new(rng.next_u64(), Mob::ID), &RistrettoPublic::from_random(&mut rng), ) diff --git a/full-service/src/json_rpc/v1/models/transaction_log.rs b/full-service/src/json_rpc/v1/models/transaction_log.rs new file mode 100644 index 000000000..f54670ef6 --- /dev/null +++ b/full-service/src/json_rpc/v1/models/transaction_log.rs @@ -0,0 +1,253 @@ +// Copyright (c) 2020-2021 MobileCoin Inc. + +//! API definition for the TransactionLog object. + +use serde::{Deserialize, Serialize}; +use std::fmt; + +use crate::{ + db, + db::transaction_log::{AssociatedTxos, TransactionLogModel}, +}; + +pub enum TxStatus { + Built, + Pending, + Succeeded, + Failed, +} + +impl fmt::Display for TxStatus { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + TxStatus::Built => write!(f, "tx_status_built"), + TxStatus::Pending => write!(f, "tx_status_pending"), + TxStatus::Succeeded => write!(f, "tx_status_succeeded"), + TxStatus::Failed => write!(f, "tx_status_failed"), + } + } +} + +impl From<&db::transaction_log::TxStatus> for TxStatus { + fn from(tx_status: &db::transaction_log::TxStatus) -> Self { + match tx_status { + db::transaction_log::TxStatus::Built => TxStatus::Built, + db::transaction_log::TxStatus::Pending => TxStatus::Pending, + db::transaction_log::TxStatus::Succeeded => TxStatus::Succeeded, + db::transaction_log::TxStatus::Failed => TxStatus::Failed, + } + } +} + +impl From<&TxStatus> for db::transaction_log::TxStatus { + fn from(tx_status: &TxStatus) -> Self { + match tx_status { + TxStatus::Built => db::transaction_log::TxStatus::Built, + TxStatus::Pending => db::transaction_log::TxStatus::Pending, + TxStatus::Succeeded => db::transaction_log::TxStatus::Pending, + TxStatus::Failed => db::transaction_log::TxStatus::Pending, + } + } +} + +pub enum TxDirection { + Received, + Sent, +} + +impl fmt::Display for TxDirection { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + TxDirection::Received => write!(f, "tx_direction_received"), + TxDirection::Sent => write!(f, "tx_direction_sent"), + } + } +} + +/// A log of a transaction that occurred on the MobileCoin network, constructed +/// and/or submitted from an account in this wallet. +#[derive(Deserialize, PartialEq, Serialize, Default, Debug, Clone)] +pub struct TransactionLog { + /// String representing the object's type. Objects of the same type share + /// the same value. + pub object: String, + + /// Unique identifier for the transaction log. This value is not associated + /// to the ledger. + pub transaction_log_id: String, + + /// A string that identifies if this transaction log was sent or received. + /// Valid values are "sent" or "received". + pub direction: String, + + /// Flag that indicates if the sent transaction log was recovered from the + /// ledger. This value is null for "received" transaction logs. If true, + /// some information may not be available on the transaction log and its + /// txos without user input. If true, the fee receipient_address_id, fee, + /// and sent_time will be null without user input. + pub is_sent_recovered: Option, + + /// Unique identifier for the assigned associated account. If the + /// transaction is outgoing, this account is from whence the txo came. If + /// received, this is the receiving account. + pub account_id: String, + + /// A list of the Txos which were inputs to this transaction. + pub input_txos: Vec, + + /// A list of the Txos which were outputs from this transaction. + pub output_txos: Vec, + + /// A list of the Txos which were change in this transaction. + pub change_txos: Vec, + + /// Unique identifier for the assigned associated account. Only available if + /// direction is "received". + pub assigned_address_id: Option, + + /// Value in pico MOB associated to this transaction log. + pub value_pmob: String, + + /// Fee in pico MOB associated to this transaction log. Only on outgoing + /// transaction logs. Only available if direction is "sent". + pub fee_pmob: Option, + + /// The block index of the highest block on the network at the time the + /// transaction was submitted. + pub submitted_block_index: Option, + + /// The scanned block block index in which this transaction occurred. + pub finalized_block_index: Option, + + /// String representing the transaction log status. On "sent", valid + /// statuses are "built", "pending", "succeeded", "failed". On "received", + /// the status is "succeeded". + pub status: String, + + /// Time at which sent transaction log was created. Only available if + /// direction is "sent". This value is null if "received" or if the sent + /// transactions were recovered from the ledger (is_sent_recovered = true). + pub sent_time: Option, + + /// An arbitrary string attached to the object. + pub comment: String, + + /// Code representing the cause of "failed" status. + pub failure_code: Option, + + /// Human parsable explanation of "failed" status. + pub failure_message: Option, +} + +impl TransactionLog { + pub fn new_from_received_txo( + txo: &db::models::Txo, + assigned_address: Option, + ) -> Result { + Ok(TransactionLog { + object: "transaction_log".to_string(), + transaction_log_id: txo.id.clone(), + direction: TxDirection::Received.to_string(), + is_sent_recovered: None, + account_id: txo + .clone() + .account_id + .ok_or("Txo has no account_id but it is required for a transaction log")?, + input_txos: vec![], + output_txos: vec![TxoAbbrev { + txo_id_hex: txo.id.to_string(), + recipient_address_id: "".to_string(), + value_pmob: txo.value.to_string(), + }], + change_txos: vec![], + assigned_address_id: assigned_address, + value_pmob: txo.value.to_string(), + fee_pmob: None, + submitted_block_index: None, + finalized_block_index: txo.received_block_index.map(|index| index.to_string()), + status: TxStatus::Succeeded.to_string(), + sent_time: None, + comment: "".to_string(), + failure_code: None, + failure_message: None, + }) + } + + pub fn new( + transaction_log: &db::models::TransactionLog, + associated_txos: &AssociatedTxos, + ) -> Self { + let input_txos: Vec = associated_txos + .inputs + .iter() + .map(|txo| TxoAbbrev::new(txo, "".to_string())) + .collect(); + + let output_txos: Vec = associated_txos + .outputs + .iter() + .map(|(txo, recipient_address_id)| TxoAbbrev::new(txo, recipient_address_id.clone())) + .collect(); + + let value_pmob = associated_txos + .outputs + .iter() + .map(|(txo, _)| txo.value as u64) + .sum::() + .to_string(); + + let change_txos: Vec = associated_txos + .change + .iter() + .map(|(txo, recipient_address_id)| TxoAbbrev::new(txo, recipient_address_id.clone())) + .collect(); + + let assigned_address_id = output_txos + .first() + .map(|txo| txo.recipient_address_id.clone()); + + Self { + object: "transaction_log".to_string(), + transaction_log_id: transaction_log.id.clone(), + direction: TxDirection::Sent.to_string(), + is_sent_recovered: None, + account_id: transaction_log.account_id.clone(), + input_txos, + output_txos, + change_txos, + assigned_address_id, + value_pmob, + fee_pmob: Some(transaction_log.fee_value.to_string()), + submitted_block_index: transaction_log.submitted_block_index.map(|i| i.to_string()), + finalized_block_index: transaction_log.finalized_block_index.map(|i| i.to_string()), + status: TxStatus::from(&transaction_log.status()).to_string(), + sent_time: None, + comment: transaction_log.comment.clone(), + failure_code: None, + failure_message: None, + } + } +} + +#[derive(Deserialize, PartialEq, Serialize, Default, Debug, Clone)] +pub struct TxoAbbrev { + pub txo_id_hex: String, + + /// Unique identifier for the recipient associated account. Blank unless + /// direction is "sent". + pub recipient_address_id: String, + + /// Available pico MOB for this Txo. + /// If the account is syncing, this value may change. + pub value_pmob: String, +} + +impl TxoAbbrev { + pub fn new(txo: &db::models::Txo, recipient_address_id: String) -> Self { + Self { + txo_id_hex: txo.id.clone(), + recipient_address_id, + value_pmob: (txo.value as u64).to_string(), + } + } +} diff --git a/full-service/src/json_rpc/tx_proposal.rs b/full-service/src/json_rpc/v1/models/tx_proposal.rs similarity index 60% rename from full-service/src/json_rpc/tx_proposal.rs rename to full-service/src/json_rpc/v1/models/tx_proposal.rs index 62da53ef7..b8629e9b7 100644 --- a/full-service/src/json_rpc/tx_proposal.rs +++ b/full-service/src/json_rpc/v1/models/tx_proposal.rs @@ -2,9 +2,14 @@ //! API definition for the TxProposal object. -use crate::json_rpc::unspent_tx_out::UnspentTxOut; +use crate::{ + json_rpc::v1::models::unspent_tx_out::UnspentTxOut, + service::models::tx_proposal::TxProposal as TxProposalServiceModel, +}; +use mc_common::HashMap; use mc_mobilecoind_json::data_types::{JsonOutlay, JsonTx, JsonUnspentTxOut}; +use mc_transaction_core::tx::TxOutConfirmationNumber; use serde_derive::{Deserialize, Serialize}; use std::convert::TryFrom; @@ -18,6 +23,75 @@ pub struct TxProposal { pub outlay_confirmation_numbers: Vec>, } +impl TryFrom<&TxProposalServiceModel> for TxProposal { + type Error = String; + + fn try_from(src: &TxProposalServiceModel) -> Result { + let mcd_tx_proposal = mc_mobilecoind::payments::TxProposal::try_from(src)?; + + let tx_proposal = TxProposal::try_from(&mcd_tx_proposal)?; + + Ok(tx_proposal) + } +} + +impl TryFrom<&TxProposalServiceModel> for mc_mobilecoind::payments::TxProposal { + type Error = String; + + #[allow(clippy::bind_instead_of_map)] + fn try_from( + src: &TxProposalServiceModel, + ) -> Result { + let unspent_txos: Vec = src + .input_txos + .iter() + .map(|input_txo| mc_mobilecoind::UnspentTxOut { + tx_out: input_txo.tx_out.clone(), + subaddress_index: input_txo.subaddress_index, + key_image: input_txo.key_image, + value: input_txo.amount.value, + attempted_spend_height: 0, + attempted_spend_tombstone: 0, + token_id: *input_txo.amount.token_id, + }) + .collect(); + + let mut outlay_list: Vec = Vec::new(); + let mut outlay_map: HashMap = HashMap::default(); + let mut confirmation_numbers: Vec = Vec::new(); + + for (outlay_index, payload_txo) in src.payload_txos.iter().enumerate() { + let tx_out_index = src + .tx + .prefix + .outputs + .iter() + .enumerate() + .position(|(_outlay_index, tx_out)| { + payload_txo.tx_out.public_key == tx_out.public_key + }) + .ok_or("Could not find tx_out in tx")?; + + outlay_map.insert(outlay_index, tx_out_index); + confirmation_numbers.push(payload_txo.confirmation_number.clone()); + outlay_list.push(mc_mobilecoind::payments::Outlay { + value: payload_txo.amount.value, + receiver: payload_txo.recipient_public_address.clone(), + }); + } + + let res = mc_mobilecoind::payments::TxProposal { + utxos: unspent_txos, + outlays: outlay_list, + tx: src.tx.clone(), + outlay_index_to_tx_out_index: outlay_map, + outlay_confirmation_numbers: confirmation_numbers, + }; + + Ok(res) + } +} + impl TryFrom<&mc_mobilecoind::payments::TxProposal> for TxProposal { type Error = String; diff --git a/full-service/src/json_rpc/txo.rs b/full-service/src/json_rpc/v1/models/txo.rs similarity index 65% rename from full-service/src/json_rpc/txo.rs rename to full-service/src/json_rpc/v1/models/txo.rs index 6ea62f7f3..1fcb2a746 100644 --- a/full-service/src/json_rpc/txo.rs +++ b/full-service/src/json_rpc/v1/models/txo.rs @@ -2,18 +2,87 @@ //! API definition for the Txo object. -use crate::{ - db, - db::{ - models::{ - TXO_STATUS_ORPHANED, TXO_STATUS_PENDING, TXO_STATUS_SECRETED, TXO_STATUS_SPENT, - TXO_STATUS_UNSPENT, TXO_TYPE_MINTED, TXO_TYPE_RECEIVED, - }, - txo::TxoModel, - }, -}; +use crate::db; use serde_derive::{Deserialize, Serialize}; use serde_json::Map; +use std::{convert::TryFrom, fmt, str::FromStr}; + +pub enum TxoStatus { + Orphaned, + Pending, + Secreted, + Spent, + Unspent, +} + +impl fmt::Display for TxoStatus { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + TxoStatus::Orphaned => write!(f, "txo_status_orphaned"), + TxoStatus::Pending => write!(f, "txo_status_pending"), + TxoStatus::Secreted => write!(f, "txo_status_secreted"), + TxoStatus::Spent => write!(f, "txo_status_spent"), + TxoStatus::Unspent => write!(f, "txo_status_unspent"), + } + } +} + +impl FromStr for TxoStatus { + type Err = String; + + fn from_str(s: &str) -> Result { + match s { + "txo_status_secreted" => Ok(TxoStatus::Secreted), + "txo_status_unspent" => Ok(TxoStatus::Unspent), + "txo_status_pending" => Ok(TxoStatus::Pending), + "txo_status_spent" => Ok(TxoStatus::Spent), + "txo_status_orphaned" => Ok(TxoStatus::Orphaned), + _ => Err(format!("Unknown TxoStatus: {}", s)), + } + } +} + +impl TryFrom for db::txo::TxoStatus { + type Error = String; + + fn try_from(status: TxoStatus) -> Result { + match status { + TxoStatus::Orphaned => Ok(db::txo::TxoStatus::Orphaned), + TxoStatus::Pending => Ok(db::txo::TxoStatus::Pending), + TxoStatus::Secreted => { + Err("TxoStatus::Secreted is not a valid db::txo::TxoStatus".to_string()) + } + TxoStatus::Spent => Ok(db::txo::TxoStatus::Spent), + TxoStatus::Unspent => Ok(db::txo::TxoStatus::Unspent), + } + } +} + +impl From<&db::txo::TxoStatus> for TxoStatus { + fn from(src: &db::txo::TxoStatus) -> Self { + match src { + db::txo::TxoStatus::Orphaned => TxoStatus::Orphaned, + db::txo::TxoStatus::Pending => TxoStatus::Pending, + db::txo::TxoStatus::Spent => TxoStatus::Spent, + db::txo::TxoStatus::Unspent => TxoStatus::Unspent, + db::txo::TxoStatus::Unverified => TxoStatus::Unspent, + } + } +} + +pub enum TxoType { + Minted, + Received, +} + +impl fmt::Display for TxoType { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + TxoType::Minted => write!(f, "txo_type_minted"), + TxoType::Received => write!(f, "txo_type_received"), + } + } +} /// An Txo in the wallet. /// @@ -102,64 +171,37 @@ pub struct Txo { pub confirmation: Option, } -impl From<&db::models::Txo> for Txo { - fn from(txo: &db::models::Txo) -> Txo { +impl Txo { + pub fn new(txo: &db::models::Txo, status: &db::txo::TxoStatus) -> Txo { let mut account_status_map: Map = Map::new(); - if let Some(received_account_id_hex) = &txo.received_account_id_hex { - let txo_status = if txo.is_spent() { - TXO_STATUS_SPENT - } else if txo.is_pending() { - TXO_STATUS_PENDING - } else if txo.is_orphaned() { - TXO_STATUS_ORPHANED - } else { - TXO_STATUS_UNSPENT - }; - - account_status_map.insert( - received_account_id_hex.to_string(), - json!({"txo_type": TXO_TYPE_RECEIVED, "txo_status": txo_status}).into(), - ); - } - - if let Some(minted_account_id_hex) = &txo.minted_account_id_hex { - let txo_status = if Some(minted_account_id_hex.clone()) != txo.received_account_id_hex { - TXO_STATUS_SECRETED - } else if txo.is_spent() { - TXO_STATUS_SPENT - } else if txo.is_pending() { - TXO_STATUS_PENDING - } else if txo.is_orphaned() { - TXO_STATUS_ORPHANED - } else { - TXO_STATUS_UNSPENT - }; + let status = TxoStatus::from(status); + if let Some(account_id) = &txo.account_id { account_status_map.insert( - minted_account_id_hex.to_string(), - json!({"txo_type": TXO_TYPE_MINTED, "txo_status": txo_status}).into(), + account_id.clone(), + json!({"txo_type": TxoType::Received.to_string(), "txo_status": status.to_string()}).into(), ); } Txo { object: "txo".to_string(), - txo_id_hex: txo.txo_id_hex.clone(), + txo_id_hex: txo.id.clone(), value_pmob: (txo.value as u64).to_string(), recipient_address_id: None, - received_block_index: txo.received_block_index.map(|x| (x as u64).to_string()), - spent_block_index: txo.spent_block_index.map(|x| (x as u64).to_string()), + received_block_index: txo.received_block_index.map(|i| i.to_string()), + spent_block_index: txo.spent_block_index.map(|i| i.to_string()), is_spent_recovered: false, - received_account_id: txo.received_account_id_hex.clone(), - minted_account_id: txo.minted_account_id_hex.clone(), + received_account_id: txo.clone().account_id, + minted_account_id: None, + account_status_map, target_key: hex::encode(&txo.target_key), public_key: hex::encode(&txo.public_key), e_fog_hint: hex::encode(&txo.e_fog_hint), - subaddress_index: txo.subaddress_index.map(|s| (s as u64).to_string()), + subaddress_index: txo.subaddress_index.map(|i| i.to_string()), assigned_address: None, key_image: txo.key_image.as_ref().map(|k| hex::encode(&k)), - confirmation: txo.confirmation.as_ref().map(hex::encode), - account_status_map, + confirmation: txo.shared_secret.as_ref().map(hex::encode), } } } @@ -170,7 +212,7 @@ mod tests { use crate::{ db, db::{account::AccountModel, models::Account, txo::TxoModel}, - test_utils::{create_test_received_txo, get_test_ledger, WalletDbTestContext, MOB}, + test_utils::{create_test_received_txo, WalletDbTestContext, MOB}, }; use mc_account_keys::{AccountKey, RootIdentity}; use mc_common::logger::{test_with_logger, Logger}; @@ -184,7 +226,6 @@ mod tests { let db_test_context = WalletDbTestContext::default(); let wallet_db = db_test_context.get_db_instance(logger); - let ledger_db = get_test_ledger(5, &[], 12, &mut rng); let root_id = RootIdentity::from_random(&mut rng); let account_key = AccountKey::from(&root_id); @@ -197,7 +238,6 @@ mod tests { "".to_string(), "".to_string(), "".to_string(), - &ledger_db, &wallet_db.get_conn().unwrap(), ) .unwrap(); @@ -214,8 +254,9 @@ mod tests { let txo_details = db::models::Txo::get(&txo_hex, &wallet_db.get_conn().unwrap()) .expect("Could not get Txo"); + let txo_status = txo_details.status(&wallet_db.get_conn().unwrap()).unwrap(); assert_eq!(txo_details.value as u64, 15_625_000 * MOB as u64); - let json_txo = Txo::from(&txo_details); + let json_txo = Txo::new(&txo_details, &txo_status); assert_eq!(json_txo.value_pmob, "15625000000000000000"); } } diff --git a/full-service/src/json_rpc/unspent_tx_out.rs b/full-service/src/json_rpc/v1/models/unspent_tx_out.rs similarity index 91% rename from full-service/src/json_rpc/unspent_tx_out.rs rename to full-service/src/json_rpc/v1/models/unspent_tx_out.rs index 0af147360..2e1d72302 100644 --- a/full-service/src/json_rpc/unspent_tx_out.rs +++ b/full-service/src/json_rpc/v1/models/unspent_tx_out.rs @@ -3,6 +3,7 @@ //! API definition for the UnspentTxOut object. use mc_mobilecoind_json::data_types::JsonTxOut; +use mc_util_serial::JsonU64; use serde_derive::{Deserialize, Serialize}; use std::convert::TryFrom; @@ -26,10 +27,7 @@ impl TryFrom<&mc_mobilecoind_json::data_types::JsonUnspentTxOut> for UnspentTxOu tx_out: src.tx_out.clone(), subaddress_index: src.subaddress_index.to_string(), key_image: src.key_image.clone(), - value: src - .value - .parse::() - .map_err(|err| format!("Failed to parse u64 from value: {}", err))?, + value: src.value.into(), attempted_spend_height: src.attempted_spend_height.to_string(), attempted_spend_tombstone: src.attempted_spend_tombstone.to_string(), monitor_id: src.monitor_id.clone(), @@ -50,7 +48,7 @@ impl TryFrom<&UnspentTxOut> for mc_mobilecoind_json::data_types::JsonUnspentTxOu .parse::() .map_err(|err| format!("Failed to parse u64 from subaddress_index: {}", err))?, key_image: src.key_image.clone(), - value: src.value.to_string(), + value: JsonU64(src.value), attempted_spend_height: src.attempted_spend_height.parse::().map_err(|err| { format!("Failed to parse u64 from attempted_spend_height: {}", err) })?, diff --git a/full-service/src/json_rpc/wallet_status.rs b/full-service/src/json_rpc/v1/models/wallet_status.rs similarity index 55% rename from full-service/src/json_rpc/wallet_status.rs rename to full-service/src/json_rpc/v1/models/wallet_status.rs index 3ed1041ca..d619c0d77 100644 --- a/full-service/src/json_rpc/wallet_status.rs +++ b/full-service/src/json_rpc/v1/models/wallet_status.rs @@ -2,11 +2,11 @@ //! API definition for the Wallet Status object. -use crate::{json_rpc, service}; +use crate::service; +use mc_transaction_core::{tokens::Mob, Token}; use serde_derive::{Deserialize, Serialize}; use serde_json::Map; -use std::{convert::TryFrom, iter::FromIterator}; /// The status of the wallet, including the sum of the balances for all /// accounts. @@ -58,43 +58,14 @@ pub struct WalletStatus { /// A normalized hash mapping account_id to account objects. pub account_map: Map, - - /// A list of all view only account_ids imported into the wallet in order of - /// import. - pub view_only_account_ids: Vec, - - /// A normalized hash mapping view only account_id to view only account - /// objects. - pub view_only_account_map: Map, } -impl TryFrom<&service::balance::WalletStatus> for WalletStatus { - type Error = String; - - fn try_from(src: &service::balance::WalletStatus) -> Result { - let account_mapped: Vec<(String, serde_json::Value)> = src - .account_map - .iter() - .map(|(i, a)| { - json_rpc::account::Account::try_from(a).and_then(|a| { - serde_json::to_value(a) - .map(|v| (i.to_string(), v)) - .map_err(|e| format!("Could not convert account map: {:?}", e)) - }) - }) - .collect::, String>>()?; - - let view_only_account_mapped: Vec<(String, serde_json::Value)> = src - .view_only_account_map - .iter() - .map(|(i, a)| { - let view_only_account_json = - json_rpc::view_only_account::ViewOnlyAccountJSON::from(a); - serde_json::to_value(view_only_account_json) - .map(|v| (i.to_string(), v)) - .map_err(|e| format!("Could not convert account map: {:?}", e)) - }) - .collect::, String>>()?; +impl WalletStatus { + pub fn new( + src: &service::balance::WalletStatus, + account_map: Map, + ) -> Result { + let balance_mob = src.balance_per_token.get(&Mob::ID).unwrap_or_default(); Ok(WalletStatus { object: "wallet_status".to_string(), @@ -102,19 +73,13 @@ impl TryFrom<&service::balance::WalletStatus> for WalletStatus { local_block_height: src.local_block_height.to_string(), is_synced_all: src.min_synced_block_index + 1 >= src.network_block_height, min_synced_block_index: src.min_synced_block_index.to_string(), - total_unspent_pmob: src.unspent.to_string(), - total_pending_pmob: src.pending.to_string(), - total_spent_pmob: src.spent.to_string(), - total_secreted_pmob: src.secreted.to_string(), - total_orphaned_pmob: src.orphaned.to_string(), + total_unspent_pmob: (balance_mob.unspent + balance_mob.unverified).to_string(), + total_pending_pmob: balance_mob.pending.to_string(), + total_spent_pmob: balance_mob.spent.to_string(), + total_secreted_pmob: balance_mob.secreted.to_string(), + total_orphaned_pmob: balance_mob.orphaned.to_string(), account_ids: src.account_ids.iter().map(|a| a.to_string()).collect(), - account_map: Map::from_iter(account_mapped), - view_only_account_ids: src - .view_only_account_ids - .iter() - .map(|a| a.to_string()) - .collect(), - view_only_account_map: Map::from_iter(view_only_account_mapped), + account_map, }) } } diff --git a/full-service/src/json_rpc/v2/api/mod.rs b/full-service/src/json_rpc/v2/api/mod.rs new file mode 100644 index 000000000..bd19e428f --- /dev/null +++ b/full-service/src/json_rpc/v2/api/mod.rs @@ -0,0 +1,6 @@ +pub mod request; +pub mod response; +pub mod wallet; + +#[cfg(any(test, feature = "test_utils"))] +pub mod test_utils; diff --git a/full-service/src/json_rpc/v2/api/request.rs b/full-service/src/json_rpc/v2/api/request.rs new file mode 100644 index 000000000..4256ced6c --- /dev/null +++ b/full-service/src/json_rpc/v2/api/request.rs @@ -0,0 +1,245 @@ +// Copyright (c) 2020-2021 MobileCoin Inc. + +//! The JSON RPC 2.0 Requests to the Wallet API for Full Service. + +use crate::json_rpc::{ + json_rpc_request::JsonRPCRequest, + v2::models::{ + account_key::FogInfo, amount::Amount, receiver_receipt::ReceiverReceipt, + tx_proposal::TxProposal, + }, +}; + +use mc_mobilecoind_json::data_types::JsonTxOut; +use serde::{Deserialize, Serialize}; +use std::convert::TryFrom; +use strum::IntoEnumIterator; +use strum_macros::EnumIter; + +pub fn help_str() -> String { + let mut help_str = "Please use json data to choose wallet commands. For example, \n\ncurl -s localhost:9090/wallet/v2 -d '{\"method\": \"create_account\", \"params\": {\"name\": \"Alice\"}}' -X POST -H 'Content-type: application/json'\n\nAvailable commands are:\n\n".to_owned(); + for e in JsonCommandRequest::iter() { + help_str.push_str(&format!("{:?}\n\n", e)); + } + help_str +} + +impl TryFrom<&JsonRPCRequest> for JsonCommandRequest { + type Error = String; + + fn try_from(src: &JsonRPCRequest) -> Result { + let src_json: serde_json::Value = serde_json::json!(src); + serde_json::from_value(src_json).map_err(|e| format!("Could not get value {:?}", e)) + } +} + +/// Requests to the Full Service Wallet Service. +#[derive(Deserialize, Serialize, EnumIter, Debug)] +#[serde(tag = "method", content = "params")] +#[allow(non_camel_case_types)] +pub enum JsonCommandRequest { + assign_address_for_account { + account_id: String, + metadata: Option, + }, + build_and_submit_transaction { + account_id: String, + addresses_and_amounts: Option>, + recipient_public_address: Option, + amount: Option, + input_txo_ids: Option>, + fee_value: Option, + fee_token_id: Option, + tombstone_block: Option, + max_spendable_value: Option, + comment: Option, + }, + build_burn_transaction { + account_id: String, + amount: Amount, + redemption_memo_hex: Option, + input_txo_ids: Option>, + fee_value: Option, + fee_token_id: Option, + tombstone_block: Option, + max_spendable_value: Option, + }, + build_transaction { + account_id: String, + addresses_and_amounts: Option>, + recipient_public_address: Option, + amount: Option, + input_txo_ids: Option>, + fee_value: Option, + fee_token_id: Option, + tombstone_block: Option, + max_spendable_value: Option, + }, + build_unsigned_burn_transaction { + account_id: String, + amount: Amount, + redemption_memo_hex: Option, + input_txo_ids: Option>, + fee_value: Option, + fee_token_id: Option, + tombstone_block: Option, + max_spendable_value: Option, + }, + build_unsigned_transaction { + account_id: String, + addresses_and_amounts: Option>, + recipient_public_address: Option, + amount: Option, + input_txo_ids: Option>, + fee_value: Option, + fee_token_id: Option, + tombstone_block: Option, + max_spendable_value: Option, + }, + check_b58_type { + b58_code: String, + }, + check_receiver_receipt_status { + address: String, + receiver_receipt: ReceiverReceipt, + }, + create_account { + name: Option, + fog_info: Option, + }, + create_payment_request { + account_id: String, + subaddress_index: Option, + amount: Amount, + memo: Option, + }, + create_receiver_receipts { + tx_proposal: TxProposal, + }, + create_view_only_account_import_request { + account_id: String, + }, + create_view_only_account_sync_request { + account_id: String, + }, + export_account_secrets { + account_id: String, + }, + get_account_status { + account_id: String, + }, + get_accounts { + offset: Option, + limit: Option, + }, + get_address { + public_address_b58: String, + }, + get_address_for_account { + account_id: String, + index: i64, + }, + get_addresses { + account_id: Option, + offset: Option, + limit: Option, + }, + get_address_status { + address: String, + }, + get_block { + block_index: String, + }, + get_confirmations { + transaction_log_id: String, + }, + get_mc_protocol_transaction { + transaction_log_id: String, + }, + get_mc_protocol_txo { + txo_id: String, + }, + get_network_status, + get_transaction_log { + transaction_log_id: String, + }, + get_transaction_logs { + account_id: Option, + min_block_index: Option, + max_block_index: Option, + offset: Option, + limit: Option, + }, + get_txo { + txo_id: String, + }, + get_txo_block_index { + public_key: String, + }, + get_txos { + account_id: Option, + address: Option, + status: Option, + token_id: Option, + min_received_block_index: Option, + max_received_block_index: Option, + offset: Option, + limit: Option, + }, + get_txo_membership_proofs { + outputs: Vec, + }, + get_wallet_status, + import_account { + mnemonic: String, + key_derivation_version: String, + name: Option, + first_block_index: Option, + next_subaddress_index: Option, + fog_info: Option, + }, + import_account_from_legacy_root_entropy { + entropy: String, + name: Option, + first_block_index: Option, + next_subaddress_index: Option, + fog_info: Option, + }, + import_view_only_account { + view_private_key: String, + spend_public_key: String, + name: Option, + first_block_index: Option, + next_subaddress_index: Option, + }, + remove_account { + account_id: String, + }, + sample_mixins { + num_mixins: u64, + excluded_outputs: Vec, + }, + submit_transaction { + tx_proposal: TxProposal, + comment: Option, + account_id: Option, + }, + sync_view_only_account { + account_id: String, + completed_txos: Vec<(String, String)>, + next_subaddress_index: String, + }, + update_account_name { + account_id: String, + name: String, + }, + validate_confirmation { + account_id: String, + txo_id: String, + confirmation: String, + }, + verify_address { + address: String, + }, + version, +} diff --git a/full-service/src/json_rpc/v2/api/response.rs b/full-service/src/json_rpc/v2/api/response.rs new file mode 100644 index 000000000..7b75668e0 --- /dev/null +++ b/full-service/src/json_rpc/v2/api/response.rs @@ -0,0 +1,192 @@ +// Copyright (c) 2020-2021 MobileCoin Inc. + +//! JSON-RPC Responses from the Wallet API. +//! +//! API v2 + +use crate::{ + json_rpc::{ + json_rpc_request::JsonRPCRequest, + json_rpc_response::JsonCommandResponse as JsonCommandResponseTrait, + v2::models::{ + account::{Account, AccountMap}, + account_secrets::AccountSecrets, + address::{Address, AddressMap}, + balance::BalanceMap, + block::{Block, BlockContents}, + confirmation_number::Confirmation, + network_status::NetworkStatus, + receiver_receipt::ReceiverReceipt, + transaction_log::{TransactionLog, TransactionLogMap}, + tx_proposal::{TxProposal, UnsignedTxProposal}, + txo::{Txo, TxoMap}, + wallet_status::WalletStatus, + }, + }, + service::receipt::ReceiptTransactionStatus, + util::b58::PrintableWrapperType, +}; +use mc_mobilecoind_json::data_types::{JsonTx, JsonTxOut, JsonTxOutMembershipProof}; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; + +/// Responses from the Full Service Wallet. +#[derive(Deserialize, Serialize, Debug)] +#[serde(untagged)] +#[allow(non_camel_case_types)] +#[allow(clippy::large_enum_variant)] +pub enum JsonCommandResponse { + assign_address_for_account { + address: Address, + }, + build_and_submit_transaction { + transaction_log: TransactionLog, + tx_proposal: TxProposal, + }, + build_burn_transaction { + tx_proposal: TxProposal, + transaction_log_id: String, + }, + build_transaction { + tx_proposal: TxProposal, + transaction_log_id: String, + }, + build_unsigned_burn_transaction { + account_id: String, + unsigned_tx_proposal: UnsignedTxProposal, + }, + build_unsigned_transaction { + account_id: String, + unsigned_tx_proposal: UnsignedTxProposal, + }, + check_b58_type { + b58_type: PrintableWrapperType, + data: HashMap, + }, + check_receiver_receipt_status { + receipt_transaction_status: ReceiptTransactionStatus, + txo: Option, + }, + create_account { + account: Account, + }, + create_payment_request { + payment_request_b58: String, + }, + create_receiver_receipts { + receiver_receipts: Vec, + }, + create_view_only_account_import_request { + json_rpc_request: JsonRPCRequest, + }, + create_view_only_account_sync_request { + account_id: String, + incomplete_txos_encoded: Vec, + }, + export_account_secrets { + account_secrets: AccountSecrets, + }, + get_account_status { + account: Account, + network_block_height: String, + local_block_height: String, + balance_per_token: BalanceMap, + }, + get_accounts { + account_ids: Vec, + account_map: AccountMap, + }, + get_address { + address: Address, + }, + get_address_for_account { + address: Address, + }, + get_addresses { + public_addresses: Vec, + address_map: AddressMap, + }, + get_address_status { + address: Address, + account_block_height: String, + network_block_height: String, + local_block_height: String, + balance_per_token: BalanceMap, + }, + get_block { + block: Block, + block_contents: BlockContents, + }, + get_confirmations { + confirmations: Vec, + }, + get_mc_protocol_transaction { + transaction: JsonTx, + }, + get_mc_protocol_txo { + txo: JsonTxOut, + }, + get_network_status { + network_status: NetworkStatus, + }, + get_transaction_log { + transaction_log: TransactionLog, + }, + get_transaction_logs { + transaction_log_ids: Vec, + transaction_log_map: TransactionLogMap, + }, + get_txo { + txo: Txo, + }, + get_txo_block_index { + block_index: String, + }, + get_txos { + txo_ids: Vec, + txo_map: TxoMap, + }, + get_txo_membership_proofs { + outputs: Vec, + membership_proofs: Vec, + }, + get_wallet_status { + wallet_status: WalletStatus, + }, + import_account { + account: Account, + }, + import_account_from_legacy_root_entropy { + account: Account, + }, + import_view_only_account { + account: Account, + }, + remove_account { + removed: bool, + }, + sample_mixins { + mixins: Vec, + membership_proofs: Vec, + }, + submit_transaction { + transaction_log: Option, + }, + sync_view_only_account, + update_account_name { + account: Account, + }, + validate_confirmation { + validated: bool, + }, + verify_address { + verified: bool, + }, + version { + string: String, + number: (String, String, String, String), + commit: String, + }, +} + +impl JsonCommandResponseTrait for JsonCommandResponse {} diff --git a/full-service/src/json_rpc/v2/api/test_utils.rs b/full-service/src/json_rpc/v2/api/test_utils.rs new file mode 100644 index 000000000..9a11b68c9 --- /dev/null +++ b/full-service/src/json_rpc/v2/api/test_utils.rs @@ -0,0 +1,329 @@ +// Copyright (c) 2020-2021 MobileCoin Inc. + +use crate::{ + json_rpc::{ + json_rpc_request::JsonRPCRequest, + json_rpc_response::JsonRPCResponse, + v2::api::{ + request::JsonCommandRequest, response::JsonCommandResponse, wallet::wallet_api_inner, + }, + }, + service::WalletService, + test_utils::{ + get_resolver_factory, get_test_ledger, setup_peer_manager_and_network_state, + WalletDbTestContext, + }, + wallet::{APIKeyState, ApiKeyGuard}, +}; +use mc_account_keys::PublicAddress; +use mc_common::logger::{log, Logger}; +use mc_connection_test_utils::MockBlockchainConnection; +use mc_fog_report_validation::MockFogPubkeyResolver; +use mc_ledger_db::{Ledger, LedgerDB}; +use mc_ledger_sync::PollingNetworkState; +use rand::rngs::StdRng; +use rocket::{ + http::{ContentType, Header, Status}, + local::Client, + post, routes, +}; +use rocket_contrib::json::{Json, JsonValue}; +use std::{ + convert::TryFrom, + sync::{ + atomic::{AtomicUsize, Ordering::SeqCst}, + Arc, RwLock, + }, + time::Duration, +}; + +pub fn get_free_port() -> u16 { + static PORT_NR: AtomicUsize = AtomicUsize::new(0); + PORT_NR.fetch_add(1, SeqCst) as u16 + 30300 +} + +pub struct TestWalletState { + pub service: WalletService, MockFogPubkeyResolver>, +} + +// Note: the reason this is duplicated from wallet.rs is to be able to pass the +// TestWalletState, which handles Mock objects. +#[post("/wallet/v2", format = "json", data = "")] +fn test_wallet_api( + _guard: ApiKeyGuard, + state: rocket::State, + command: Json, +) -> Result>, String> { + let req: JsonRPCRequest = command.0.clone(); + + let mut response = JsonRPCResponse { + method: Some(command.0.method), + result: None, + error: None, + jsonrpc: "2.0".to_string(), + id: command.0.id, + }; + + match wallet_api_inner( + &state.service, + JsonCommandRequest::try_from(&req).map_err(|e| e)?, + ) { + Ok(command_response) => { + response.result = Some(command_response); + } + Err(rpc_error) => { + response.error = Some(rpc_error); + } + }; + + Ok(Json(response)) +} + +pub fn test_rocket(rocket_config: rocket::Config, state: TestWalletState) -> rocket::Rocket { + rocket::custom(rocket_config) + .mount("/", routes![test_wallet_api]) + .manage(state) +} + +pub const BASE_TEST_BLOCK_HEIGHT: usize = 12; + +pub fn create_test_setup( + mut rng: &mut StdRng, + use_wallet_db: bool, + logger: Logger, +) -> ( + rocket::Rocket, + LedgerDB, + WalletDbTestContext, + Arc>>>, +) { + let db_test_context = WalletDbTestContext::default(); + let wallet_db = match use_wallet_db { + true => Some(db_test_context.get_db_instance(logger.clone())), + false => None, + }; + let known_recipients: Vec = Vec::new(); + let ledger_db = get_test_ledger(5, &known_recipients, BASE_TEST_BLOCK_HEIGHT, &mut rng); + let (peer_manager, network_state) = + setup_peer_manager_and_network_state(ledger_db.clone(), logger.clone(), false); + + let service = WalletService::new( + wallet_db, + ledger_db.clone(), + peer_manager, + network_state.clone(), + get_resolver_factory(&mut rng).unwrap(), + false, + logger, + ); + + let rocket_config: rocket::Config = + rocket::Config::build(rocket::config::Environment::Development) + .port(get_free_port()) + .unwrap(); + + let rocket_instance = test_rocket(rocket_config, TestWalletState { service }); + + (rocket_instance, ledger_db, db_test_context, network_state) +} + +pub fn setup( + rng: &mut StdRng, + logger: Logger, +) -> ( + Client, + LedgerDB, + WalletDbTestContext, + Arc>>>, +) { + let (rocket_instance, ledger_db, db_test_context, network_state) = + create_test_setup(rng, true, logger); + + let rocket = rocket_instance.manage(APIKeyState("".to_string())); + ( + Client::new(rocket).expect("valid rocket instance"), + ledger_db, + db_test_context, + network_state, + ) +} + +pub fn setup_no_wallet_db( + rng: &mut StdRng, + logger: Logger, +) -> ( + Client, + LedgerDB, + WalletDbTestContext, + Arc>>>, +) { + let (rocket_instance, ledger_db, db_test_context, network_state) = + create_test_setup(rng, false, logger); + + let rocket = rocket_instance.manage(APIKeyState("".to_string())); + ( + Client::new(rocket).expect("valid rocket instance"), + ledger_db, + db_test_context, + network_state, + ) +} + +pub fn setup_with_api_key( + rng: &mut StdRng, + logger: Logger, + api_key: String, +) -> ( + Client, + LedgerDB, + WalletDbTestContext, + Arc>>>, +) { + let (rocket_instance, ledger_db, db_test_context, network_state) = + create_test_setup(rng, true, logger); + + let rocket = rocket_instance.manage(APIKeyState(api_key)); + + ( + Client::new(rocket).expect("valid rocket instance"), + ledger_db, + db_test_context, + network_state, + ) +} + +pub fn dispatch(client: &Client, request_body: JsonValue, logger: &Logger) -> JsonValue { + log::info!(logger, "Attempting dispatch of\n{:?}\n", request_body,); + let request_body = request_body.to_string(); + + let mut res = client + .post("/wallet/v2") + .header(ContentType::JSON) + .body(request_body) + .dispatch(); + assert_eq!(res.status(), Status::Ok); + + let response_body = res.body().unwrap().into_string().unwrap(); + log::info!(logger, "Got response\n{}\n", response_body); + + let res: JsonValue = serde_json::from_str(&response_body).unwrap(); + res +} + +pub fn dispatch_with_header( + client: &Client, + request_body: JsonValue, + header: Header<'static>, + logger: &Logger, +) -> JsonValue { + log::info!(logger, "Attempting dispatch of\n{:?}\n", request_body,); + let request_body = request_body.to_string(); + log::info!(logger, "Attempting dispatch of\n{}\n", request_body,); + + let mut res = client + .post("/wallet/v2") + .header(ContentType::JSON) + .header(header) + .body(request_body) + .dispatch(); + assert_eq!(res.status(), Status::Ok); + + let response_body = res.body().unwrap().into_string().unwrap(); + log::info!(logger, "Got response\n{}\n", response_body); + + let res: JsonValue = serde_json::from_str(&response_body).unwrap(); + res +} + +pub fn dispatch_with_header_expect_error( + client: &Client, + request_body: JsonValue, + header: Header<'static>, + _logger: &Logger, + expected_err: Status, +) { + let res = client + .post("/wallet/v2") + .header(ContentType::JSON) + .header(header) + .body(request_body.to_string()) + .dispatch(); + assert_eq!(res.status(), expected_err); +} + +pub fn dispatch_expect_error( + client: &Client, + request_body: JsonValue, + logger: &Logger, + expected_err: String, +) { + let mut res = client + .post("/wallet/v2") + .header(ContentType::JSON) + .body(request_body.to_string()) + .dispatch(); + assert_eq!(res.status(), Status::Ok); + let response_body = res.body().unwrap().into_string().unwrap(); + log::info!( + logger, + "Attempted dispatch of {:?} got response {:?}", + request_body, + response_body + ); + let response_json: serde_json::Value = serde_json::from_str(&response_body).unwrap(); + let expected_json: serde_json::Value = serde_json::from_str(&expected_err).unwrap(); + assert_eq!(response_json, expected_json); +} + +pub fn wait_for_sync( + client: &Client, + ledger_db: &LedgerDB, + network_state: &Arc>>>, + logger: &Logger, +) { + let mut count = 0; + loop { + // Sleep to let the sync thread process the txos + std::thread::sleep(Duration::from_millis(2000)); + + // Check that syncing is working + let body = json!({ + "jsonrpc": "2.0", + "method": "get_wallet_status", + "id": 1, + }); + let res = dispatch(&client, body, &logger); + let status = res["result"]["wallet_status"].clone(); + + let is_synced_all = status["is_synced_all"].as_bool().unwrap(); + if is_synced_all { + let local_height = status["local_block_height"] + .as_str() + .unwrap() + .parse::() + .unwrap(); + assert_eq!(local_height, ledger_db.num_blocks().unwrap()); + // In the test context, we often add a block manually locally before updating + // the network_state. In the wild, the local_height should never be + // greater than the network_height. + assert!( + status["network_block_height"] + .as_str() + .unwrap() + .parse::() + .unwrap() + <= local_height + ); + break; + } + + // Have to manually call poll() on network state to get it to update for these + // tests + network_state.write().unwrap().poll(); + + count += 1; + if count > 10 { + panic!("Service did not sync after 10 iterations"); + } + } +} diff --git a/full-service/src/json_rpc/v2/api/wallet.rs b/full-service/src/json_rpc/v2/api/wallet.rs new file mode 100644 index 000000000..cec768ed5 --- /dev/null +++ b/full-service/src/json_rpc/v2/api/wallet.rs @@ -0,0 +1,1002 @@ +use crate::{ + db::{ + account::AccountID, + transaction_log::TransactionID, + txo::{TxoID, TxoStatus}, + }, + json_rpc::{ + json_rpc_request::JsonRPCRequest, + json_rpc_response::{ + format_error, format_invalid_request_error, JsonRPCError, JsonRPCResponse, + }, + v2::{ + api::{request::JsonCommandRequest, response::JsonCommandResponse}, + models::{ + account::{Account, AccountMap}, + account_secrets::AccountSecrets, + address::{Address, AddressMap}, + balance::{Balance, BalanceMap}, + block::{Block, BlockContents}, + confirmation_number::Confirmation, + network_status::NetworkStatus, + receiver_receipt::ReceiverReceipt, + transaction_log::{TransactionLog, TransactionLogMap}, + tx_proposal::{TxProposal as TxProposalJSON, UnsignedTxProposal}, + txo::{Txo, TxoMap}, + wallet_status::WalletStatus, + }, + }, + wallet::{ApiKeyGuard, WalletState}, + }, + service::{ + self, + account::AccountService, + address::AddressService, + balance::BalanceService, + confirmation_number::ConfirmationService, + ledger::LedgerService, + models::tx_proposal::TxProposal, + payment_request::PaymentRequestService, + receipt::ReceiptService, + transaction::{TransactionMemo, TransactionService}, + transaction_log::TransactionLogService, + txo::TxoService, + WalletService, + }, + util::b58::{ + b58_decode_payment_request, b58_encode_public_address, b58_printable_wrapper_type, + PrintableWrapperType, + }, +}; +use mc_account_keys::burn_address; +use mc_common::logger::global_log; +use mc_connection::{BlockchainConnection, UserTxConnection}; +use mc_crypto_keys::CompressedRistrettoPublic; +use mc_fog_report_validation::FogPubkeyResolver; +use mc_mobilecoind_json::data_types::{JsonTx, JsonTxOut, JsonTxOutMembershipProof}; +use mc_transaction_core::Amount; +use mc_transaction_std::BurnRedemptionMemo; +use rocket::{self}; +use rocket_contrib::json::Json; +use std::{ + collections::HashMap, + convert::{TryFrom, TryInto}, + str::FromStr, +}; + +pub fn generic_wallet_api( + _api_key_guard: ApiKeyGuard, + state: rocket::State>, + command: Json, +) -> Result>, String> +where + T: BlockchainConnection + UserTxConnection + 'static, + FPR: FogPubkeyResolver + Send + Sync + 'static, +{ + let req: JsonRPCRequest = command.0.clone(); + + let mut response = JsonRPCResponse { + method: Some(command.0.method), + result: None, + error: None, + jsonrpc: "2.0".to_string(), + id: command.0.id, + }; + + let request = match JsonCommandRequest::try_from(&req) { + Ok(request) => request, + Err(error) => { + response.error = Some(format_invalid_request_error(error)); + return Ok(Json(response)); + } + }; + + match wallet_api_inner(&state.service, request) { + Ok(command_response) => { + response.result = Some(command_response); + } + Err(rpc_error) => { + response.error = Some(rpc_error); + } + }; + + Ok(Json(response)) +} + +/// The Wallet API inner method, which handles switching on the method enum. +/// +/// Note that this is structured this way so that the routes can be defined to +/// take explicit Rocket state, and then pass the service to the inner method. +/// This allows us to properly construct state with Mock Connection Objects in +/// tests. This also allows us to version the overall API easily. +pub fn wallet_api_inner( + service: &WalletService, + command: JsonCommandRequest, +) -> Result +where + T: BlockchainConnection + UserTxConnection + 'static, + FPR: FogPubkeyResolver + Send + Sync + 'static, +{ + global_log::trace!("Running command {:?}", command); + + let response = match command { + JsonCommandRequest::assign_address_for_account { + account_id, + metadata, + } => JsonCommandResponse::assign_address_for_account { + address: Address::from( + &service + .assign_address_for_account(&AccountID(account_id), metadata.as_deref()) + .map_err(format_error)?, + ), + }, + JsonCommandRequest::build_and_submit_transaction { + account_id, + addresses_and_amounts, + recipient_public_address, + amount, + input_txo_ids, + fee_value, + fee_token_id, + tombstone_block, + max_spendable_value, + comment, + } => { + // The user can specify a list of addresses and values, + // or a single address and a single value. + let mut addresses_and_amounts = addresses_and_amounts.unwrap_or_default(); + if let (Some(address), Some(amount)) = (recipient_public_address, amount) { + addresses_and_amounts.push((address, amount)); + } + + let (transaction_log, associated_txos, value_map, tx_proposal) = service + .build_sign_and_submit_transaction( + &account_id, + &addresses_and_amounts, + input_txo_ids.as_ref(), + fee_value, + fee_token_id, + tombstone_block, + max_spendable_value, + comment, + TransactionMemo::RTH, + ) + .map_err(format_error)?; + + JsonCommandResponse::build_and_submit_transaction { + transaction_log: TransactionLog::new( + &transaction_log, + &associated_txos, + &value_map, + ), + tx_proposal: TxProposalJSON::try_from(&tx_proposal).map_err(format_error)?, + } + } + JsonCommandRequest::build_burn_transaction { + account_id, + amount, + redemption_memo_hex, + input_txo_ids, + fee_value, + fee_token_id, + tombstone_block, + max_spendable_value, + } => { + let mut memo_data = [0; BurnRedemptionMemo::MEMO_DATA_LEN]; + if let Some(redemption_memo_hex) = redemption_memo_hex { + if redemption_memo_hex.len() != BurnRedemptionMemo::MEMO_DATA_LEN * 2 { + return Err(format_error(format!( + "Invalid redemption memo length: {}. Must be 128 characters (64 bytes).", + redemption_memo_hex.len() + ))); + } + + hex::decode_to_slice(&redemption_memo_hex, &mut memo_data).map_err(format_error)?; + } + + let tx_proposal = service + .build_and_sign_transaction( + &account_id, + &[( + b58_encode_public_address(&burn_address()).map_err(format_error)?, + amount, + )], + input_txo_ids.as_ref(), + fee_value, + fee_token_id, + tombstone_block, + max_spendable_value, + TransactionMemo::BurnRedemption(memo_data), + ) + .map_err(format_error)?; + + JsonCommandResponse::build_burn_transaction { + tx_proposal: TxProposalJSON::try_from(&tx_proposal).map_err(format_error)?, + transaction_log_id: TransactionID::from(&tx_proposal.tx).to_string(), + } + } + JsonCommandRequest::build_transaction { + account_id, + addresses_and_amounts, + recipient_public_address, + amount, + input_txo_ids, + fee_value, + fee_token_id, + tombstone_block, + max_spendable_value, + } => { + // The user can specify a list of addresses and values, + // or a single address and a single value. + let mut addresses_and_amounts = addresses_and_amounts.unwrap_or_default(); + if let (Some(address), Some(amount)) = (recipient_public_address, amount) { + addresses_and_amounts.push((address, amount)); + } + + let tx_proposal = service + .build_and_sign_transaction( + &account_id, + &addresses_and_amounts, + input_txo_ids.as_ref(), + fee_value, + fee_token_id, + tombstone_block, + max_spendable_value, + TransactionMemo::RTH, + ) + .map_err(format_error)?; + + JsonCommandResponse::build_transaction { + tx_proposal: TxProposalJSON::try_from(&tx_proposal).map_err(format_error)?, + transaction_log_id: TransactionID::from(&tx_proposal.tx).to_string(), + } + } + JsonCommandRequest::build_unsigned_burn_transaction { + account_id, + amount, + redemption_memo_hex, + input_txo_ids, + fee_value, + fee_token_id, + tombstone_block, + max_spendable_value, + } => { + let mut memo_data = [0; BurnRedemptionMemo::MEMO_DATA_LEN]; + if let Some(redemption_memo_hex) = redemption_memo_hex { + if redemption_memo_hex.len() != BurnRedemptionMemo::MEMO_DATA_LEN * 2 { + return Err(format_error(format!( + "Invalid redemption memo length: {}. Must be 128 characters (64 bytes).", + redemption_memo_hex.len() + ))); + } + + hex::decode_to_slice(&redemption_memo_hex, &mut memo_data).map_err(format_error)?; + } + + let unsigned_tx_proposal: UnsignedTxProposal = service + .build_transaction( + &account_id, + &[( + b58_encode_public_address(&burn_address()).map_err(format_error)?, + amount, + )], + input_txo_ids.as_ref(), + fee_value, + fee_token_id, + tombstone_block, + max_spendable_value, + TransactionMemo::BurnRedemption(memo_data), + ) + .map_err(format_error)? + .try_into() + .map_err(format_error)?; + + JsonCommandResponse::build_unsigned_transaction { + account_id, + unsigned_tx_proposal, + } + } + JsonCommandRequest::build_unsigned_transaction { + account_id, + recipient_public_address, + amount, + fee_value, + fee_token_id, + tombstone_block, + addresses_and_amounts, + input_txo_ids, + max_spendable_value, + } => { + let mut addresses_and_amounts = addresses_and_amounts.unwrap_or_default(); + if let (Some(address), Some(amount)) = (recipient_public_address, amount) { + addresses_and_amounts.push((address, amount)); + } + let unsigned_tx_proposal: UnsignedTxProposal = service + .build_transaction( + &account_id, + &addresses_and_amounts, + input_txo_ids.as_ref(), + fee_value, + fee_token_id, + tombstone_block, + max_spendable_value, + TransactionMemo::Empty, + ) + .map_err(format_error)? + .try_into() + .map_err(format_error)?; + + JsonCommandResponse::build_unsigned_transaction { + account_id, + unsigned_tx_proposal, + } + } + JsonCommandRequest::check_b58_type { b58_code } => { + let b58_type = b58_printable_wrapper_type(b58_code.clone()).map_err(format_error)?; + let mut b58_data = HashMap::new(); + match b58_type { + PrintableWrapperType::PublicAddress => { + b58_data.insert("public_address_b58".to_string(), b58_code); + } + PrintableWrapperType::TransferPayload => {} + PrintableWrapperType::PaymentRequest => { + let payment_request = + b58_decode_payment_request(b58_code).map_err(format_error)?; + let public_address_b58 = + b58_encode_public_address(&payment_request.public_address) + .map_err(format_error)?; + b58_data.insert("public_address_b58".to_string(), public_address_b58); + b58_data.insert("value".to_string(), payment_request.value.to_string()); + b58_data.insert("memo".to_string(), payment_request.memo); + } + } + JsonCommandResponse::check_b58_type { + b58_type, + data: b58_data, + } + } + JsonCommandRequest::check_receiver_receipt_status { + address, + receiver_receipt, + } => { + let receipt = service::receipt::ReceiverReceipt::try_from(&receiver_receipt) + .map_err(format_error)?; + let (status, txo_and_status) = service + .check_receipt_status(&address, &receipt) + .map_err(format_error)?; + JsonCommandResponse::check_receiver_receipt_status { + receipt_transaction_status: status, + txo: txo_and_status + .as_ref() + .map(|(txo, status)| Txo::new(txo, status)), + } + } + JsonCommandRequest::create_account { name, fog_info } => { + let fog_info = fog_info.unwrap_or_default(); + + let account = service + .create_account( + name, + fog_info.report_url, + fog_info.report_id, + fog_info.authority_spki, + ) + .map_err(format_error)?; + + let next_subaddress_index = service + .get_next_subaddress_index_for_account(&AccountID(account.id.clone())) + .map_err(format_error)?; + + let account = Account::new(&account, next_subaddress_index).map_err(format_error)?; + + JsonCommandResponse::create_account { account } + } + JsonCommandRequest::create_payment_request { + account_id, + subaddress_index, + amount, + memo, + } => JsonCommandResponse::create_payment_request { + payment_request_b58: service + .create_payment_request( + account_id, + subaddress_index, + Amount::try_from(&amount).map_err(format_error)?, + memo, + ) + .map_err(format_error)?, + }, + JsonCommandRequest::create_receiver_receipts { tx_proposal } => { + let receipts = service + .create_receiver_receipts( + &TxProposal::try_from(&tx_proposal).map_err(format_error)?, + ) + .map_err(format_error)?; + let json_receipts: Vec = receipts + .iter() + .map(ReceiverReceipt::try_from) + .collect::, String>>() + .map_err(format_error)?; + JsonCommandResponse::create_receiver_receipts { + receiver_receipts: json_receipts, + } + } + JsonCommandRequest::create_view_only_account_import_request { account_id } => { + JsonCommandResponse::create_view_only_account_import_request { + json_rpc_request: service + .get_view_only_account_import_request(&AccountID(account_id)) + .map_err(format_error)?, + } + } + JsonCommandRequest::create_view_only_account_sync_request { account_id } => { + let unverified_txos = service + .list_txos( + Some(account_id.clone()), + None, + Some(TxoStatus::Unverified), + None, + None, + None, + None, + None, + ) + .map_err(format_error)?; + + let unverified_txos_encoded: Vec = unverified_txos + .iter() + .map(|(txo, _)| hex::encode(&txo.txo)) + .collect(); + + JsonCommandResponse::create_view_only_account_sync_request { + account_id, + incomplete_txos_encoded: unverified_txos_encoded, + } + } + JsonCommandRequest::export_account_secrets { account_id } => { + let account = service + .get_account(&AccountID(account_id)) + .map_err(format_error)?; + JsonCommandResponse::export_account_secrets { + account_secrets: AccountSecrets::try_from(&account).map_err(format_error)?, + } + } + JsonCommandRequest::get_account_status { account_id } => { + let account = service + .get_account(&AccountID(account_id.clone())) + .map_err(format_error)?; + + let next_subaddress_index = service + .get_next_subaddress_index_for_account(&AccountID(account_id.clone())) + .map_err(format_error)?; + + let account = Account::new(&account, next_subaddress_index).map_err(format_error)?; + + let network_status = service.get_network_status().map_err(format_error)?; + + let balance = service + .get_balance_for_account(&AccountID(account_id)) + .map_err(format_error)?; + + let balance_formatted = BalanceMap( + balance + .iter() + .map(|(k, v)| (k.to_string(), Balance::from(v))) + .collect(), + ); + + JsonCommandResponse::get_account_status { + account, + network_block_height: network_status.network_block_height.to_string(), + local_block_height: network_status.local_block_height.to_string(), + balance_per_token: balance_formatted, + } + } + JsonCommandRequest::get_accounts { offset, limit } => { + let accounts = service.list_accounts(offset, limit).map_err(format_error)?; + let account_map = AccountMap( + accounts + .iter() + .map(|a| { + let next_subaddress_index = service + .get_next_subaddress_index_for_account(&AccountID(a.id.clone())) + .map_err(format_error)?; + Ok(( + a.id.to_string(), + Account::new(a, next_subaddress_index).map_err(format_error)?, + )) + }) + .collect::>()?, + ); + + JsonCommandResponse::get_accounts { + account_ids: accounts.iter().map(|a| a.id.clone()).collect(), + account_map, + } + } + JsonCommandRequest::get_address { public_address_b58 } => { + let assigned_address = service + .get_address(&public_address_b58) + .map_err(format_error)?; + JsonCommandResponse::get_address { + address: Address::from(&assigned_address), + } + } + JsonCommandRequest::get_address_for_account { account_id, index } => { + let assigned_subaddress = service + .get_address_for_account(&AccountID(account_id), index) + .map_err(format_error)?; + JsonCommandResponse::get_address_for_account { + address: Address::from(&assigned_subaddress), + } + } + JsonCommandRequest::get_addresses { + account_id, + offset, + limit, + } => { + let addresses = service + .get_addresses(account_id, offset, limit) + .map_err(format_error)?; + + let address_map = AddressMap( + addresses + .iter() + .map(|a| (a.public_address_b58.clone(), Address::from(a))) + .collect(), + ); + + JsonCommandResponse::get_addresses { + public_addresses: addresses + .iter() + .map(|a| a.public_address_b58.clone()) + .collect(), + address_map, + } + } + JsonCommandRequest::get_address_status { address } => { + let subaddress = service.get_address(&address).map_err(format_error)?; + let account_id = AccountID(subaddress.account_id.clone()); + let account = service.get_account(&account_id).map_err(format_error)?; + let network_status = service.get_network_status().map_err(format_error)?; + + let balance = service + .get_balance_for_address(&address) + .map_err(format_error)?; + + let balance_per_token = BalanceMap( + balance + .iter() + .map(|(a, b)| (a.to_string(), Balance::from(b))) + .collect(), + ); + + JsonCommandResponse::get_address_status { + address: Address::from(&subaddress), + account_block_height: account.next_block_index.to_string(), + network_block_height: network_status.network_block_height.to_string(), + local_block_height: network_status.local_block_height.to_string(), + balance_per_token, + } + } + JsonCommandRequest::get_block { block_index } => { + let (block, block_contents) = service + .get_block_object(block_index.parse::().map_err(format_error)?) + .map_err(format_error)?; + JsonCommandResponse::get_block { + block: Block::new(&block), + block_contents: BlockContents::new(&block_contents), + } + } + JsonCommandRequest::get_confirmations { transaction_log_id } => { + JsonCommandResponse::get_confirmations { + confirmations: service + .get_confirmations(&transaction_log_id) + .map_err(format_error)? + .iter() + .map(Confirmation::from) + .collect(), + } + } + JsonCommandRequest::get_mc_protocol_transaction { transaction_log_id } => { + let tx = service + .get_transaction_object(&transaction_log_id) + .map_err(format_error)?; + let proto_tx = mc_api::external::Tx::from(&tx); + let json_tx = JsonTx::from(&proto_tx); + JsonCommandResponse::get_mc_protocol_transaction { + transaction: json_tx, + } + } + JsonCommandRequest::get_mc_protocol_txo { txo_id } => { + let tx_out = service.get_txo_object(&txo_id).map_err(format_error)?; + let proto_txo = mc_api::external::TxOut::from(&tx_out); + let json_txo = JsonTxOut::from(&proto_txo); + JsonCommandResponse::get_mc_protocol_txo { txo: json_txo } + } + JsonCommandRequest::get_network_status => JsonCommandResponse::get_network_status { + network_status: NetworkStatus::try_from( + &service.get_network_status().map_err(format_error)?, + ) + .map_err(format_error)?, + }, + JsonCommandRequest::get_transaction_log { transaction_log_id } => { + let (transaction_log, associated_txos, value_map) = service + .get_transaction_log(&transaction_log_id) + .map_err(format_error)?; + JsonCommandResponse::get_transaction_log { + transaction_log: TransactionLog::new( + &transaction_log, + &associated_txos, + &value_map, + ), + } + } + JsonCommandRequest::get_transaction_logs { + account_id, + min_block_index, + max_block_index, + offset, + limit, + } => { + let min_block_index = min_block_index + .map(|i| i.parse::()) + .transpose() + .map_err(format_error)?; + + let max_block_index = max_block_index + .map(|i| i.parse::()) + .transpose() + .map_err(format_error)?; + + let transaction_logs_and_txos = service + .list_transaction_logs(account_id, offset, limit, min_block_index, max_block_index) + .map_err(format_error)?; + + let transaction_log_map = TransactionLogMap( + transaction_logs_and_txos + .iter() + .map(|(t, a, v)| (t.id.clone(), TransactionLog::new(t, a, v))) + .collect(), + ); + + JsonCommandResponse::get_transaction_logs { + transaction_log_ids: transaction_logs_and_txos + .iter() + .map(|(t, _, _)| t.id.clone()) + .collect(), + transaction_log_map, + } + } + JsonCommandRequest::get_txo { txo_id } => { + let (txo, status) = service.get_txo(&TxoID(txo_id)).map_err(format_error)?; + JsonCommandResponse::get_txo { + txo: Txo::new(&txo, &status), + } + } + JsonCommandRequest::get_txo_block_index { public_key } => { + let public_key_bytes = hex::decode(public_key).map_err(format_error)?; + let public_key: CompressedRistrettoPublic = public_key_bytes + .as_slice() + .try_into() + .map_err(format_error)?; + let block_index = service + .get_block_index_from_txo_public_key(&public_key) + .map_err(format_error)?; + JsonCommandResponse::get_txo_block_index { + block_index: block_index.to_string(), + } + } + JsonCommandRequest::get_txos { + account_id, + address, + status, + token_id, + min_received_block_index, + max_received_block_index, + offset, + limit, + } => { + let status = match status { + Some(s) => Some(TxoStatus::from_str(&s).map_err(format_error)?), + None => None, + }; + + let token_id = match token_id { + Some(t) => Some(t.parse::().map_err(format_error)?), + None => None, + }; + + let txos_and_statuses = service + .list_txos( + account_id, + address, + status, + token_id, + min_received_block_index, + max_received_block_index, + offset, + limit, + ) + .map_err(format_error)?; + + let txo_map = TxoMap( + txos_and_statuses + .iter() + .map(|(t, s)| (t.id.clone(), Txo::new(t, s))) + .collect(), + ); + + JsonCommandResponse::get_txos { + txo_ids: txos_and_statuses + .iter() + .map(|(t, _)| t.id.clone()) + .collect(), + txo_map, + } + } + JsonCommandRequest::get_txo_membership_proofs { outputs } => { + let public_keys = outputs + .clone() + .into_iter() + .map(|tx_out| { + let public_key_bytes = hex::decode(tx_out.public_key).map_err(format_error)?; + let public_key: CompressedRistrettoPublic = public_key_bytes + .as_slice() + .try_into() + .map_err(format_error)?; + Ok(public_key) + }) + .collect::, _>>()?; + let indices = service + .get_indices_from_txo_public_keys(&public_keys) + .map_err(format_error)?; + + let membership_proofs = service + .get_tx_out_proof_of_memberships(&indices) + .map_err(format_error)? + .iter() + .map(|proof| { + let proof: mc_api::external::TxOutMembershipProof = + proof.try_into().map_err(format_error)?; + let json_proof = JsonTxOutMembershipProof::from(&proof); + Ok(json_proof) + }) + .collect::, _>>()?; + + JsonCommandResponse::get_txo_membership_proofs { + outputs, + membership_proofs, + } + } + JsonCommandRequest::get_wallet_status => JsonCommandResponse::get_wallet_status { + wallet_status: WalletStatus::try_from( + &service.get_wallet_status().map_err(format_error)?, + ) + .map_err(format_error)?, + }, + JsonCommandRequest::import_account { + mnemonic, + key_derivation_version, + name, + first_block_index, + next_subaddress_index, + fog_info, + } => { + let fb = first_block_index + .map(|fb| fb.parse::()) + .transpose() + .map_err(format_error)?; + let ns = next_subaddress_index + .map(|ns| ns.parse::()) + .transpose() + .map_err(format_error)?; + let kdv = key_derivation_version.parse::().map_err(format_error)?; + + let fog_info = fog_info.unwrap_or_default(); + + let account = service + .import_account( + mnemonic, + kdv, + name, + fb, + ns, + fog_info.report_url, + fog_info.report_id, + fog_info.authority_spki, + ) + .map_err(format_error)?; + + let next_subaddress_index = service + .get_next_subaddress_index_for_account(&AccountID(account.id.clone())) + .map_err(format_error)?; + + let account = Account::new(&account, next_subaddress_index).map_err(format_error)?; + + JsonCommandResponse::import_account { account } + } + JsonCommandRequest::import_account_from_legacy_root_entropy { + entropy, + name, + first_block_index, + next_subaddress_index, + fog_info, + } => { + let fb = first_block_index + .map(|fb| fb.parse::()) + .transpose() + .map_err(format_error)?; + let ns = next_subaddress_index + .map(|ns| ns.parse::()) + .transpose() + .map_err(format_error)?; + + let fog_info = fog_info.unwrap_or_default(); + + let account = service + .import_account_from_legacy_root_entropy( + entropy, + name, + fb, + ns, + fog_info.report_url, + fog_info.report_id, + fog_info.authority_spki, + ) + .map_err(format_error)?; + + let next_subaddress_index = service + .get_next_subaddress_index_for_account(&AccountID(account.id.clone())) + .map_err(format_error)?; + + let account = Account::new(&account, next_subaddress_index).map_err(format_error)?; + + JsonCommandResponse::import_account { account } + } + JsonCommandRequest::import_view_only_account { + view_private_key, + spend_public_key, + name, + first_block_index, + next_subaddress_index, + } => { + let fb = first_block_index + .map(|fb| fb.parse::()) + .transpose() + .map_err(format_error)?; + let ns = next_subaddress_index + .map(|ns| ns.parse::()) + .transpose() + .map_err(format_error)?; + + let account = service + .import_view_only_account(view_private_key, spend_public_key, name, fb, ns) + .map_err(format_error)?; + let next_subaddress_index = service + .get_next_subaddress_index_for_account(&AccountID(account.id.clone())) + .map_err(format_error)?; + let account = Account::new(&account, next_subaddress_index).map_err(format_error)?; + + JsonCommandResponse::import_view_only_account { account } + } + JsonCommandRequest::remove_account { account_id } => JsonCommandResponse::remove_account { + removed: service + .remove_account(&AccountID(account_id)) + .map_err(format_error)?, + }, + JsonCommandRequest::sample_mixins { + num_mixins, + excluded_outputs, + } => { + let public_keys = excluded_outputs + .into_iter() + .map(|tx_out| { + let public_key_bytes = hex::decode(tx_out.public_key).map_err(format_error)?; + let public_key: CompressedRistrettoPublic = public_key_bytes + .as_slice() + .try_into() + .map_err(format_error)?; + Ok(public_key) + }) + .collect::, _>>()?; + let excluded_indices = service + .get_indices_from_txo_public_keys(&public_keys) + .map_err(format_error)?; + let (mixins, membership_proofs) = service + .sample_mixins(num_mixins as usize, &excluded_indices) + .map_err(format_error)?; + + let mixins = mixins + .iter() + .map(|tx_out| { + let tx_out: mc_api::external::TxOut = + tx_out.try_into().map_err(format_error)?; + let json_tx_out = JsonTxOut::from(&tx_out); + Ok(json_tx_out) + }) + .collect::, _>>()?; + + let membership_proofs = membership_proofs + .iter() + .map(|proof| { + let proof: mc_api::external::TxOutMembershipProof = + proof.try_into().map_err(format_error)?; + let json_proof = JsonTxOutMembershipProof::from(&proof); + Ok(json_proof) + }) + .collect::, _>>()?; + + JsonCommandResponse::sample_mixins { + mixins, + membership_proofs, + } + } + JsonCommandRequest::submit_transaction { + tx_proposal, + comment, + account_id, + } => { + let tx_proposal = TxProposal::try_from(&tx_proposal).map_err(format_error)?; + let result: Option = service + .submit_transaction(&tx_proposal, comment, account_id) + .map_err(format_error)? + .map(|(transaction_log, associated_txos, value_map)| { + TransactionLog::new(&transaction_log, &associated_txos, &value_map) + }); + JsonCommandResponse::submit_transaction { + transaction_log: result, + } + } + JsonCommandRequest::sync_view_only_account { + account_id, + completed_txos, + next_subaddress_index, + } => { + service + .sync_account( + &AccountID(account_id), + completed_txos, + next_subaddress_index.parse::().map_err(format_error)?, + ) + .map_err(format_error)?; + + JsonCommandResponse::sync_view_only_account + } + JsonCommandRequest::update_account_name { account_id, name } => { + let account_id = AccountID(account_id); + let account = service + .update_account_name(&account_id, name) + .map_err(format_error)?; + let next_subaddress_index = service + .get_next_subaddress_index_for_account(&account_id) + .map_err(format_error)?; + let account = Account::new(&account, next_subaddress_index).map_err(format_error)?; + JsonCommandResponse::update_account_name { account } + } + JsonCommandRequest::validate_confirmation { + account_id, + txo_id, + confirmation, + } => { + let result = service + .validate_confirmation(&AccountID(account_id), &TxoID(txo_id), &confirmation) + .map_err(format_error)?; + JsonCommandResponse::validate_confirmation { validated: result } + } + JsonCommandRequest::verify_address { address } => JsonCommandResponse::verify_address { + verified: service.verify_address(&address).map_err(format_error)?, + }, + JsonCommandRequest::version => JsonCommandResponse::version { + string: env!("CARGO_PKG_VERSION").to_string(), + number: ( + env!("CARGO_PKG_VERSION_MAJOR").to_string(), + env!("CARGO_PKG_VERSION_MINOR").to_string(), + env!("CARGO_PKG_VERSION_PATCH").to_string(), + env!("CARGO_PKG_VERSION_PRE").to_string(), + ), + commit: env!("VERGEN_GIT_SHA").to_string(), + }, + }; + + Ok(response) +} diff --git a/full-service/src/json_rpc/v2/e2e_tests/account/account_address.rs b/full-service/src/json_rpc/v2/e2e_tests/account/account_address.rs new file mode 100644 index 000000000..9bada6a56 --- /dev/null +++ b/full-service/src/json_rpc/v2/e2e_tests/account/account_address.rs @@ -0,0 +1,515 @@ +// Copyright (c) 2020-2022 MobileCoin Inc. + +//! End-to-end tests for the Full Service Wallet API. + +#[cfg(test)] +mod e2e_account { + use crate::{ + db::{account::AccountID, txo::TxoStatus}, + json_rpc::v2::api::test_utils::{dispatch, setup}, + test_utils::{add_block_to_ledger_db, manually_sync_account}, + util::b58::b58_decode_public_address, + }; + + use mc_common::logger::{test_with_logger, Logger}; + use mc_crypto_rand::rand_core::RngCore; + use mc_ledger_db::Ledger; + use mc_transaction_core::{ring_signature::KeyImage, tokens::Mob, Token}; + use rand::{rngs::StdRng, SeedableRng}; + + #[test_with_logger] + fn test_import_account_with_next_subaddress_index(logger: Logger) { + let mut rng: StdRng = SeedableRng::from_seed([20u8; 32]); + let (client, mut ledger_db, db_ctx, _network_state) = setup(&mut rng, logger.clone()); + + // create an account + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "import_account_from_legacy_root_entropy", + "params": { + "entropy": "c593274dc6f6eb94242e34ae5f0ab16bc3085d45d49d9e18b8a8c6f057e6b56b", + "name": "Alice Main Account", + } + }); + let res = dispatch(&client, body, &logger); + let result = res.get("result").unwrap(); + let account_obj = result.get("account").unwrap(); + let account_id = account_obj.get("id").unwrap().as_str().unwrap(); + + // assign next subaddress for account + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "assign_address_for_account", + "params": { + "account_id": account_id, + "metadata": "subaddress_index_2", + } + }); + let res = dispatch(&client, body, &logger); + let result = res.get("result").unwrap(); + let address = result.get("address").unwrap(); + let b58_public_address = address.get("public_address_b58").unwrap().as_str().unwrap(); + let public_address = b58_decode_public_address(b58_public_address).unwrap(); + + // Add a block to fund account at the new subaddress. + add_block_to_ledger_db( + &mut ledger_db, + &vec![public_address], + 100000000000000, // 100.0 MOB + &vec![KeyImage::from(rng.next_u64())], + &mut rng, + ); + + manually_sync_account( + &ledger_db, + &db_ctx.get_db_instance(logger.clone()), + &AccountID(account_id.to_string()), + &logger, + ); + assert_eq!(ledger_db.num_blocks().unwrap(), 13); + + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "get_account_status", + "params": { + "account_id": account_id, + } + }); + let res = dispatch(&client, body, &logger); + let result = res.get("result").unwrap(); + let balance_per_token = result.get("balance_per_token").unwrap(); + let balance_mob = balance_per_token.get(Mob::ID.to_string()).unwrap(); + let unspent = balance_mob["unspent"].as_str().unwrap(); + + assert_eq!("100000000000000", unspent); + + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "remove_account", + "params": { + "account_id": account_id, + } + }); + dispatch(&client, body, &logger); + + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "import_account_from_legacy_root_entropy", + "params": { + "entropy": "c593274dc6f6eb94242e34ae5f0ab16bc3085d45d49d9e18b8a8c6f057e6b56b", + "name": "Alice Main Account", + } + }); + dispatch(&client, body, &logger); + + manually_sync_account( + &ledger_db, + &db_ctx.get_db_instance(logger.clone()), + &AccountID(account_id.to_string()), + &logger, + ); + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "get_account_status", + "params": { + "account_id": account_id, + } + }); + let res = dispatch(&client, body, &logger); + let result = res.get("result").unwrap(); + let balance_per_token = result.get("balance_per_token").unwrap(); + let balance_mob = balance_per_token.get(Mob::ID.to_string()).unwrap(); + let unspent = balance_mob["unspent"].as_str().unwrap(); + let spent = balance_mob["spent"].as_str().unwrap(); + let orphaned = balance_mob["orphaned"].as_str().unwrap(); + + assert_eq!("0", unspent); + assert_eq!("100000000000000", orphaned); + assert_eq!("0", spent); + + // assign next subaddress for account + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "assign_address_for_account", + "params": { + "account_id": account_id, + "metadata": "subaddress_index_2", + } + }); + dispatch(&client, body, &logger); + + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "get_account_status", + "params": { + "account_id": account_id, + } + }); + let res = dispatch(&client, body, &logger); + let result = res.get("result").unwrap(); + let balance_per_token = result.get("balance_per_token").unwrap(); + let balance_mob = balance_per_token.get(Mob::ID.to_string()).unwrap(); + let unspent = balance_mob["unspent"].as_str().unwrap(); + let orphaned = balance_mob["orphaned"].as_str().unwrap(); + + assert_eq!("100000000000000", unspent); + assert_eq!("0", orphaned); + + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "remove_account", + "params": { + "account_id": account_id, + } + }); + dispatch(&client, body, &logger); + + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "import_account_from_legacy_root_entropy", + "params": { + "entropy": "c593274dc6f6eb94242e34ae5f0ab16bc3085d45d49d9e18b8a8c6f057e6b56b", + "name": "Alice Main Account", + "next_subaddress_index": "3", + } + }); + dispatch(&client, body, &logger); + + manually_sync_account( + &ledger_db, + &db_ctx.get_db_instance(logger.clone()), + &AccountID(account_id.to_string()), + &logger, + ); + + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "get_account_status", + "params": { + "account_id": account_id, + } + }); + let res = dispatch(&client, body, &logger); + let result = res.get("result").unwrap(); + let balance_per_token = result.get("balance_per_token").unwrap(); + let balance_mob = balance_per_token.get(Mob::ID.to_string()).unwrap(); + let unspent = balance_mob["unspent"].as_str().unwrap(); + let orphaned = balance_mob["orphaned"].as_str().unwrap(); + + assert_eq!("100000000000000", unspent); + assert_eq!("0", orphaned); + } + + #[test_with_logger] + fn test_paginate_assigned_addresses(logger: Logger) { + let mut rng: StdRng = SeedableRng::from_seed([20u8; 32]); + let (client, _ledger_db, _db_ctx, _network_state) = setup(&mut rng, logger.clone()); + + // Add an account + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "create_account", + "params": { + "name": "", + } + }); + let res = dispatch(&client, body, &logger); + let result = res.get("result").unwrap(); + let account_obj = result.get("account").unwrap(); + let account_id = account_obj.get("id").unwrap().as_str().unwrap(); + + // Assign some addresses. + for _ in 0..10 { + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "assign_address_for_account", + "params": { + "account_id": account_id, + "metadata": "subaddress_index_2", + } + }); + dispatch(&client, body, &logger); + } + + // Check that we can paginate address output. + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "get_addresses", + "params": { + "account_id": account_id, + }, + }); + let res = dispatch(&client, body, &logger); + let result = res.get("result").unwrap(); + let addresses_all = result.get("public_addresses").unwrap().as_array().unwrap(); + assert_eq!(addresses_all.len(), 13); // Accounts start with 3 addresses, then we created 10. + + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "get_addresses", + "params": { + "account_id": account_id, + "offset": 1, + "limit": 4, + }, + }); + let res = dispatch(&client, body, &logger); + let result = res.get("result").unwrap(); + let addresses_page = result.get("public_addresses").unwrap().as_array().unwrap(); + assert_eq!(addresses_page.len(), 4); + assert_eq!(addresses_page[..], addresses_all[1..5]); + } + + #[test_with_logger] + fn test_next_subaddress_fails_with_fog(logger: Logger) { + use crate::db::WalletDbError::SubaddressesNotSupportedForFOGEnabledAccounts as subaddress_error; + + let mut rng: StdRng = SeedableRng::from_seed([20u8; 32]); + let (client, mut _ledger_db, _db_ctx, _network_state) = setup(&mut rng, logger.clone()); + + // Create Account + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "create_account", + "params": { + "name": "Alice Main Account", + "fog_info": { + "report_url": "fog://fog-report.example.com", + "report_id": "", + "authority_spki": "MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAvnB9wTbTOT5uoizRYaYbw7XIEkInl8E7MGOAQj+xnC+F1rIXiCnc/t1+5IIWjbRGhWzo7RAwI5sRajn2sT4rRn9NXbOzZMvIqE4hmhmEzy1YQNDnfALAWNQ+WBbYGW+Vqm3IlQvAFFjVN1YYIdYhbLjAPdkgeVsWfcLDforHn6rR3QBZYZIlSBQSKRMY/tywTxeTCvK2zWcS0kbbFPtBcVth7VFFVPAZXhPi9yy1AvnldO6n7KLiupVmojlEMtv4FQkk604nal+j/dOplTATV8a9AJBbPRBZ/yQg57EG2Y2MRiHOQifJx0S5VbNyMm9bkS8TD7Goi59aCW6OT1gyeotWwLg60JRZTfyJ7lYWBSOzh0OnaCytRpSWtNZ6barPUeOnftbnJtE8rFhF7M4F66et0LI/cuvXYecwVwykovEVBKRF4HOK9GgSm17mQMtzrD7c558TbaucOWabYR04uhdAc3s10MkuONWG0wIQhgIChYVAGnFLvSpp2/aQEq3xrRSETxsixUIjsZyWWROkuA0IFnc8d7AmcnUBvRW7FT/5thWyk5agdYUGZ+7C1o69ihR1YxmoGh69fLMPIEOhYh572+3ckgl2SaV4uo9Gvkz8MMGRBcMIMlRirSwhCfozV2RyT5Wn1NgPpyc8zJL7QdOhL7Qxb+5WjnCVrQYHI2cCAwEAAQ==" + } + }, + }); + + let creation_res = dispatch(&client, body, &logger); + let creation_result = creation_res.get("result").unwrap(); + let account_obj = creation_result.get("account").unwrap(); + let account_id = account_obj.get("id").unwrap().as_str().unwrap(); + assert_eq!(creation_res.get("jsonrpc").unwrap(), "2.0"); + + // assign next subaddress for account + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "assign_address_for_account", + "params": { + "account_id": account_id, + "metadata": "subaddress_index_2", + } + }); + let res = dispatch(&client, body, &logger); + let error = res.get("error").unwrap(); + let data = error.get("data").unwrap(); + let details = data.get("details").unwrap(); + assert!(details.to_string().contains(&subaddress_error.to_string())); + } + + #[test_with_logger] + fn test_create_assigned_subaddress(logger: Logger) { + let mut rng: StdRng = SeedableRng::from_seed([20u8; 32]); + let (client, mut ledger_db, db_ctx, _network_state) = setup(&mut rng, logger.clone()); + + // Add an account + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "create_account", + "params": { + "name": "Alice Main Account", + } + }); + let res = dispatch(&client, body, &logger); + let result = res.get("result").unwrap(); + let account_id = result + .get("account") + .unwrap() + .get("id") + .unwrap() + .as_str() + .unwrap(); + + // Create a subaddress + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "assign_address_for_account", + "params": { + "account_id": account_id, + "comment": "For Bob", + } + }); + let res = dispatch(&client, body, &logger); + let result = res.get("result").unwrap(); + let b58_public_address = result + .get("address") + .unwrap() + .get("public_address_b58") + .unwrap() + .as_str() + .unwrap(); + let from_bob_public_address = b58_decode_public_address(b58_public_address).unwrap(); + + // Add a block to the ledger with a transaction "From Bob" + add_block_to_ledger_db( + &mut ledger_db, + &vec![from_bob_public_address], + 42000000000000, + &vec![KeyImage::from(rng.next_u64())], + &mut rng, + ); + + manually_sync_account( + &ledger_db, + &db_ctx.get_db_instance(logger.clone()), + &AccountID(account_id.to_string()), + &logger, + ); + + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "get_txos", + "params": { + "account_id": account_id, + } + }); + let res = dispatch(&client, body, &logger); + let result = res.get("result").unwrap(); + let txos = result.get("txo_ids").unwrap().as_array().unwrap(); + assert_eq!(txos.len(), 1); + let txo_map = result.get("txo_map").unwrap().as_object().unwrap(); + let txo = &txo_map.get(txos[0].as_str().unwrap()).unwrap(); + let txo_status = txo.get("status").unwrap().as_str().unwrap(); + assert_eq!(txo_status, TxoStatus::Unspent.to_string()); + let value = txo.get("value").unwrap(); + let token_id = txo.get("token_id").unwrap(); + assert_eq!(value, "42000000000000"); + assert_eq!(token_id, "0"); + } + + #[test_with_logger] + fn test_get_address_for_account(logger: Logger) { + let mut rng: StdRng = SeedableRng::from_seed([20u8; 32]); + let (client, _ledger_db, _db_ctx, _network_state) = setup(&mut rng, logger.clone()); + + // Add an account + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "create_account", + "params": { + "name": "Alice Main Account", + } + }); + let res = dispatch(&client, body, &logger); + let result = res.get("result").unwrap(); + let account_id = result + .get("account") + .unwrap() + .get("id") + .unwrap() + .as_str() + .unwrap(); + + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "get_address_for_account", + "params": { + "account_id": account_id, + "index": 2, + } + }); + let res = dispatch(&client, body, &logger); + let error = res.get("error").unwrap(); + let code = error.get("code").unwrap(); + assert_eq!(code, -32603); + + // Create a subaddress + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "assign_address_for_account", + "params": { + "account_id": account_id, + "comment": "test", + } + }); + dispatch(&client, body, &logger); + + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "get_address_for_account", + "params": { + "account_id": account_id, + "index": 2, + } + }); + let res = dispatch(&client, body, &logger); + let result = res.get("result").unwrap(); + let address = result.get("address").unwrap(); + let subaddress_index = address.get("subaddress_index").unwrap().as_str().unwrap(); + + assert_eq!(subaddress_index, "2"); + } + + #[test_with_logger] + fn test_verify_address(logger: Logger) { + let mut rng: StdRng = SeedableRng::from_seed([20u8; 32]); + let (client, _ledger_db, _db_ctx, _network_state) = setup(&mut rng, logger.clone()); + + // Add an account + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "verify_address", + "params": { + "address": "NOTVALIDB58", + } + }); + let res = dispatch(&client, body, &logger); + let result = res["result"]["verified"].as_bool().unwrap(); + assert!(!result); + + // Add an account + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "create_account", + "params": { + "name": "Alice Main Account", + } + }); + let res = dispatch(&client, body, &logger); + let b58_public_address = res["result"]["account"]["main_address"].as_str().unwrap(); + + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "verify_address", + "params": { + "address": b58_public_address, + } + }); + let res = dispatch(&client, body, &logger); + let result = res["result"]["verified"].as_bool().unwrap(); + assert!(result); + } +} diff --git a/full-service/src/json_rpc/v2/e2e_tests/account/account_balance.rs b/full-service/src/json_rpc/v2/e2e_tests/account/account_balance.rs new file mode 100644 index 000000000..7babb2d40 --- /dev/null +++ b/full-service/src/json_rpc/v2/e2e_tests/account/account_balance.rs @@ -0,0 +1,190 @@ +// Copyright (c) 2020-2022 MobileCoin Inc. + +//! End-to-end tests for the Full Service Wallet API. + +#[cfg(test)] +mod e2e_account { + use crate::{ + db::account::AccountID, + json_rpc::v2::api::test_utils::{dispatch, setup}, + test_utils::{add_block_to_ledger_db, manually_sync_account, MOB}, + util::b58::b58_decode_public_address, + }; + + use mc_common::logger::{test_with_logger, Logger}; + use mc_crypto_rand::rand_core::RngCore; + + use mc_transaction_core::{ring_signature::KeyImage, tokens::Mob, Token}; + use rand::{rngs::StdRng, SeedableRng}; + + #[test_with_logger] + fn test_e2e_get_balance(logger: Logger) { + let mut rng: StdRng = SeedableRng::from_seed([20u8; 32]); + let (client, mut ledger_db, db_ctx, _network_state) = setup(&mut rng, logger.clone()); + + // Add an account + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "create_account", + "params": { + "name": "Alice Main Account", + } + }); + let res = dispatch(&client, body, &logger); + let result = res.get("result").unwrap(); + let account_obj = result.get("account").unwrap(); + let account_id = account_obj.get("id").unwrap().as_str().unwrap(); + let b58_public_address = account_obj.get("main_address").unwrap().as_str().unwrap(); + let public_address = b58_decode_public_address(b58_public_address).unwrap(); + + // Add a block with a txo for this address + add_block_to_ledger_db( + &mut ledger_db, + &vec![public_address], + 42 * MOB, + &vec![KeyImage::from(rng.next_u64())], + &mut rng, + ); + + manually_sync_account( + &ledger_db, + &db_ctx.get_db_instance(logger.clone()), + &AccountID(account_id.to_string()), + &logger, + ); + + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "get_account_status", + "params": { + "account_id": account_id, + } + }); + let res = dispatch(&client, body, &logger); + let result = res.get("result").unwrap(); + let balance_per_token = result.get("balance_per_token").unwrap(); + let balance_mob = balance_per_token.get(Mob::ID.to_string()).unwrap(); + let unspent = balance_mob["unspent"].as_str().unwrap(); + let max_spendable = balance_mob["max_spendable"].as_str().unwrap(); + assert_eq!(unspent, (42 * MOB).to_string()); + assert_eq!(max_spendable, (42 * MOB - Mob::MINIMUM_FEE).to_string()); + } + + #[test_with_logger] + fn test_balance_for_address(logger: Logger) { + let mut rng: StdRng = SeedableRng::from_seed([20u8; 32]); + let (client, mut ledger_db, db_ctx, _network_state) = setup(&mut rng, logger.clone()); + + // Add an account + let body = json!({ + "jsonrpc": "2.0", + "api_version": "2", + "id": 1, + "method": "create_account", + "params": { + "name": "Alice Main Account", + } + }); + let res = dispatch(&client, body, &logger); + let account_id = res["result"]["account"]["id"].as_str().unwrap(); + let b58_public_address = res["result"]["account"]["main_address"].as_str().unwrap(); + + let alice_public_address = b58_decode_public_address(&b58_public_address) + .expect("Could not b58_decode public address"); + add_block_to_ledger_db( + &mut ledger_db, + &vec![alice_public_address], + 42 * MOB, + &vec![KeyImage::from(rng.next_u64())], + &mut rng, + ); + // + manually_sync_account( + &ledger_db, + &db_ctx.get_db_instance(logger.clone()), + &AccountID(account_id.to_string()), + &logger, + ); + + let body = json!({ + "jsonrpc": "2.0", + "api_version": "2", + "id": 1, + "method": "get_address_status", + "params": { + "address": b58_public_address, + } + }); + let res = dispatch(&client, body, &logger); + let result = res.get("result").unwrap(); + let balance_per_token = result.get("balance_per_token").unwrap(); + let balance_mob = balance_per_token.get(Mob::ID.to_string()).unwrap(); + let unspent = balance_mob["unspent"].as_str().unwrap(); + let pending = balance_mob["pending"].as_str().unwrap(); + let spent = balance_mob["spent"].as_str().unwrap(); + let secreted = balance_mob["secreted"].as_str().unwrap(); + let orphaned = balance_mob["orphaned"].as_str().unwrap(); + assert_eq!(unspent, (42 * MOB).to_string(),); + assert_eq!(pending, "0"); + assert_eq!(spent, "0"); + assert_eq!(secreted, "0"); + assert_eq!(orphaned, "0"); + + // Create a subaddress + let body = json!({ + "jsonrpc": "2.0", + "api_version": "2", + "id": 1, + "method": "assign_address_for_account", + "params": { + "account_id": account_id, + "comment": "For Bob", + } + }); + let res = dispatch(&client, body, &logger); + let result = res.get("result").unwrap(); + let from_bob_b58_public_address = result + .get("address") + .unwrap() + .get("public_address_b58") + .unwrap() + .as_str() + .unwrap(); + let from_bob_public_address = + b58_decode_public_address(from_bob_b58_public_address).unwrap(); + + // Add a block to the ledger with a transaction "From Bob" + add_block_to_ledger_db( + &mut ledger_db, + &vec![from_bob_public_address], + 64 * MOB, + &vec![KeyImage::from(rng.next_u64())], + &mut rng, + ); + // + manually_sync_account( + &ledger_db, + &db_ctx.get_db_instance(logger.clone()), + &AccountID(account_id.to_string()), + &logger, + ); + + let body = json!({ + "jsonrpc": "2.0", + "api_version": "2", + "id": 1, + "method": "get_address_status", + "params": { + "address": from_bob_b58_public_address, + } + }); + let res = dispatch(&client, body, &logger); + let result = res.get("result").unwrap(); + let balance_per_token = result.get("balance_per_token").unwrap(); + let balance_mob = balance_per_token.get(Mob::ID.to_string()).unwrap(); + let unspent = balance_mob["unspent"].as_str().unwrap(); + assert_eq!(unspent, (64 * MOB).to_string()); + } +} diff --git a/full-service/src/json_rpc/v2/e2e_tests/account/account_other.rs b/full-service/src/json_rpc/v2/e2e_tests/account/account_other.rs new file mode 100644 index 000000000..285ed279a --- /dev/null +++ b/full-service/src/json_rpc/v2/e2e_tests/account/account_other.rs @@ -0,0 +1,654 @@ +// Copyright (c) 2020-2022 MobileCoin Inc. + +//! End-to-end tests for the Full Service Wallet API. + +#[cfg(test)] +mod e2e_account { + use crate::{ + db::{account::AccountID, txo::TxoStatus}, + json_rpc, + json_rpc::v2::api::test_utils::{dispatch, setup}, + test_utils::{add_block_to_ledger_db, manually_sync_account, MOB}, + util::b58::b58_decode_public_address, + }; + use bip39::{Language, Mnemonic}; + use mc_account_keys::{AccountKey, RootEntropy, RootIdentity}; + use mc_account_keys_slip10::Slip10Key; + use mc_common::logger::{test_with_logger, Logger}; + use mc_crypto_rand::rand_core::RngCore; + + use mc_transaction_core::{ring_signature::KeyImage, tokens::Mob, Token}; + use rand::{rngs::StdRng, SeedableRng}; + + use std::convert::TryFrom; + + #[test_with_logger] + fn test_export_account_secrets(logger: Logger) { + let mut rng: StdRng = SeedableRng::from_seed([20u8; 32]); + let (client, _ledger_db, _db_ctx, _network_state) = setup(&mut rng, logger.clone()); + + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "import_account", + "params": { + "mnemonic": "sheriff odor square mistake huge skate mouse shoot purity weapon proof stuff correct concert blanket neck own shift clay mistake air viable stick group", + "key_derivation_version": "2", + "name": "Alice Main Account", + "first_block_index": "200", + } + }); + let res = dispatch(&client, body, &logger); + let account_obj = res["result"]["account"].clone(); + let account_id = account_obj["id"].clone(); + + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "export_account_secrets", + "params": { + "account_id": account_id, + } + }); + let res = dispatch(&client, body, &logger); + let result = res.get("result").unwrap(); + let secrets = result.get("account_secrets").unwrap(); + let phrase = secrets["mnemonic"].as_str().unwrap(); + assert_eq!(secrets["account_id"], serde_json::json!(account_id)); + assert_eq!(secrets["key_derivation_version"], serde_json::json!("2")); + + // Test that the mnemonic serializes correctly back to an AccountKey object + let mnemonic = Mnemonic::from_phrase(phrase, Language::English).unwrap(); + let account_key = Slip10Key::from(mnemonic.clone()) + .try_into_account_key( + &"".to_string(), + &"".to_string(), + &hex::decode("".to_string()).expect("invalid spki"), + ) + .unwrap(); + + assert_eq!( + serde_json::json!(json_rpc::v2::models::account_key::AccountKey::try_from( + &account_key + ) + .unwrap()), + secrets["account_key"] + ); + } + + #[test_with_logger] + fn test_export_legacy_account_secrets(logger: Logger) { + let mut rng: StdRng = SeedableRng::from_seed([20u8; 32]); + let (client, _ledger_db, _db_ctx, _network_state) = setup(&mut rng, logger.clone()); + + let entropy = "c593274dc6f6eb94242e34ae5f0ab16bc3085d45d49d9e18b8a8c6f057e6b56b"; + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "import_account_from_legacy_root_entropy", + "params": { + "entropy": entropy, + "name": "Alice Main Account", + "first_block_index": "200", + } + }); + let res = dispatch(&client, body, &logger); + let result = res.get("result").unwrap(); + let account_obj = result.get("account").unwrap(); + let account_id = account_obj.get("id").unwrap().as_str().unwrap(); + + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "export_account_secrets", + "params": { + "account_id": account_id, + } + }); + let res = dispatch(&client, body, &logger); + let result = res.get("result").unwrap(); + let secrets = result.get("account_secrets").unwrap(); + + assert_eq!(secrets["account_id"], serde_json::json!(account_id)); + assert_eq!(secrets["entropy"], serde_json::json!(entropy)); + assert_eq!(secrets["key_derivation_version"], serde_json::json!("1")); + + // Test that the account_key serializes correctly back to an AccountKey object + let mut entropy_slice = [0u8; 32]; + entropy_slice[0..32].copy_from_slice(&hex::decode(&entropy).unwrap().as_slice()); + let account_key = AccountKey::from(&RootIdentity::from(&RootEntropy::from(&entropy_slice))); + assert_eq!( + serde_json::json!(json_rpc::v2::models::account_key::AccountKey::try_from( + &account_key + ) + .unwrap()), + secrets["account_key"] + ); + } + + #[test_with_logger] + fn test_account_status(logger: Logger) { + let mut rng: StdRng = SeedableRng::from_seed([20u8; 32]); + let (client, mut ledger_db, db_ctx, _network_state) = setup(&mut rng, logger.clone()); + + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "create_account", + "params": { + "name": "Alice Main Account", + } + }); + let res = dispatch(&client, body, &logger); + let result = res.get("result").unwrap(); + let account_obj = result.get("account").unwrap(); + let account_id = account_obj.get("id").unwrap().as_str().unwrap(); + let b58_public_address = account_obj.get("main_address").unwrap().as_str().unwrap(); + let public_address = b58_decode_public_address(b58_public_address).unwrap(); + + // Add a block with a txo for this address + add_block_to_ledger_db( + &mut ledger_db, + &vec![public_address], + 42 * MOB, + &vec![KeyImage::from(rng.next_u64())], + &mut rng, + ); + + manually_sync_account( + &ledger_db, + &db_ctx.get_db_instance(logger.clone()), + &AccountID(account_id.to_string()), + &logger, + ); + + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "get_account_status", + "params": { + "account_id": account_id, + } + }); + let res = dispatch(&client, body, &logger); + let result = res.get("result").unwrap(); + let balance_per_token = result.get("balance_per_token").unwrap(); + let balance_mob = balance_per_token.get(Mob::ID.to_string()).unwrap(); + let unspent = balance_mob["unspent"].as_str().unwrap(); + assert_eq!(unspent, (42 * MOB).to_string()); + let _account = result.get("account").unwrap(); + } + + #[test_with_logger] + fn test_e2e_get_balance(logger: Logger) { + let mut rng: StdRng = SeedableRng::from_seed([20u8; 32]); + let (client, mut ledger_db, db_ctx, _network_state) = setup(&mut rng, logger.clone()); + + // Add an account + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "create_account", + "params": { + "name": "Alice Main Account", + } + }); + let res = dispatch(&client, body, &logger); + let result = res.get("result").unwrap(); + let account_obj = result.get("account").unwrap(); + let account_id = account_obj.get("id").unwrap().as_str().unwrap(); + let b58_public_address = account_obj.get("main_address").unwrap().as_str().unwrap(); + let public_address = b58_decode_public_address(b58_public_address).unwrap(); + + // Add a block with a txo for this address + add_block_to_ledger_db( + &mut ledger_db, + &vec![public_address], + 42 * MOB, + &vec![KeyImage::from(rng.next_u64())], + &mut rng, + ); + + manually_sync_account( + &ledger_db, + &db_ctx.get_db_instance(logger.clone()), + &AccountID(account_id.to_string()), + &logger, + ); + + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "get_account_status", + "params": { + "account_id": account_id, + } + }); + let res = dispatch(&client, body, &logger); + let result = res.get("result").unwrap(); + let balance_per_token = result.get("balance_per_token").unwrap(); + let balance_mob = balance_per_token.get(Mob::ID.to_string()).unwrap(); + let unspent = balance_mob["unspent"].as_str().unwrap(); + let max_spendable = balance_mob["max_spendable"].as_str().unwrap(); + assert_eq!(unspent, (42 * MOB).to_string()); + assert_eq!(max_spendable, (42 * MOB - Mob::MINIMUM_FEE).to_string()); + } + + #[test_with_logger] + fn test_paginate_assigned_addresses(logger: Logger) { + let mut rng: StdRng = SeedableRng::from_seed([20u8; 32]); + let (client, _ledger_db, _db_ctx, _network_state) = setup(&mut rng, logger.clone()); + + // Add an account + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "create_account", + "params": { + "name": "", + } + }); + let res = dispatch(&client, body, &logger); + let result = res.get("result").unwrap(); + let account_obj = result.get("account").unwrap(); + let account_id = account_obj.get("id").unwrap().as_str().unwrap(); + + // Assign some addresses. + for _ in 0..10 { + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "assign_address_for_account", + "params": { + "account_id": account_id, + "metadata": "subaddress_index_2", + } + }); + dispatch(&client, body, &logger); + } + + // Check that we can paginate address output. + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "get_addresses", + "params": { + "account_id": account_id, + }, + }); + let res = dispatch(&client, body, &logger); + let result = res.get("result").unwrap(); + let addresses_all = result.get("public_addresses").unwrap().as_array().unwrap(); + assert_eq!(addresses_all.len(), 13); // Accounts start with 3 addresses, then we created 10. + + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "get_addresses", + "params": { + "account_id": account_id, + "offset": 1, + "limit": 4, + }, + }); + let res = dispatch(&client, body, &logger); + let result = res.get("result").unwrap(); + let addresses_page = result.get("public_addresses").unwrap().as_array().unwrap(); + assert_eq!(addresses_page.len(), 4); + assert_eq!(addresses_page[..], addresses_all[1..5]); + } + + #[test_with_logger] + fn test_next_subaddress_fails_with_fog(logger: Logger) { + use crate::db::WalletDbError::SubaddressesNotSupportedForFOGEnabledAccounts as subaddress_error; + + let mut rng: StdRng = SeedableRng::from_seed([20u8; 32]); + let (client, mut _ledger_db, _db_ctx, _network_state) = setup(&mut rng, logger.clone()); + + // Create Account + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "create_account", + "params": { + "name": "Alice Main Account", + "fog_info": { + "report_url": "fog://fog-report.example.com", + "report_id": "", + "authority_spki": "MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAvnB9wTbTOT5uoizRYaYbw7XIEkInl8E7MGOAQj+xnC+F1rIXiCnc/t1+5IIWjbRGhWzo7RAwI5sRajn2sT4rRn9NXbOzZMvIqE4hmhmEzy1YQNDnfALAWNQ+WBbYGW+Vqm3IlQvAFFjVN1YYIdYhbLjAPdkgeVsWfcLDforHn6rR3QBZYZIlSBQSKRMY/tywTxeTCvK2zWcS0kbbFPtBcVth7VFFVPAZXhPi9yy1AvnldO6n7KLiupVmojlEMtv4FQkk604nal+j/dOplTATV8a9AJBbPRBZ/yQg57EG2Y2MRiHOQifJx0S5VbNyMm9bkS8TD7Goi59aCW6OT1gyeotWwLg60JRZTfyJ7lYWBSOzh0OnaCytRpSWtNZ6barPUeOnftbnJtE8rFhF7M4F66et0LI/cuvXYecwVwykovEVBKRF4HOK9GgSm17mQMtzrD7c558TbaucOWabYR04uhdAc3s10MkuONWG0wIQhgIChYVAGnFLvSpp2/aQEq3xrRSETxsixUIjsZyWWROkuA0IFnc8d7AmcnUBvRW7FT/5thWyk5agdYUGZ+7C1o69ihR1YxmoGh69fLMPIEOhYh572+3ckgl2SaV4uo9Gvkz8MMGRBcMIMlRirSwhCfozV2RyT5Wn1NgPpyc8zJL7QdOhL7Qxb+5WjnCVrQYHI2cCAwEAAQ==" + } + }, + }); + + let creation_res = dispatch(&client, body, &logger); + let creation_result = creation_res.get("result").unwrap(); + let account_obj = creation_result.get("account").unwrap(); + let account_id = account_obj.get("id").unwrap().as_str().unwrap(); + assert_eq!(creation_res.get("jsonrpc").unwrap(), "2.0"); + + // assign next subaddress for account + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "assign_address_for_account", + "params": { + "account_id": account_id, + "metadata": "subaddress_index_2", + } + }); + let res = dispatch(&client, body, &logger); + let error = res.get("error").unwrap(); + let data = error.get("data").unwrap(); + let details = data.get("details").unwrap(); + assert!(details.to_string().contains(&subaddress_error.to_string())); + } + + #[test_with_logger] + fn test_create_assigned_subaddress(logger: Logger) { + let mut rng: StdRng = SeedableRng::from_seed([20u8; 32]); + let (client, mut ledger_db, db_ctx, _network_state) = setup(&mut rng, logger.clone()); + + // Add an account + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "create_account", + "params": { + "name": "Alice Main Account", + } + }); + let res = dispatch(&client, body, &logger); + let result = res.get("result").unwrap(); + let account_id = result + .get("account") + .unwrap() + .get("id") + .unwrap() + .as_str() + .unwrap(); + + // Create a subaddress + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "assign_address_for_account", + "params": { + "account_id": account_id, + "comment": "For Bob", + } + }); + let res = dispatch(&client, body, &logger); + let result = res.get("result").unwrap(); + let b58_public_address = result + .get("address") + .unwrap() + .get("public_address_b58") + .unwrap() + .as_str() + .unwrap(); + let from_bob_public_address = b58_decode_public_address(b58_public_address).unwrap(); + + // Add a block to the ledger with a transaction "From Bob" + add_block_to_ledger_db( + &mut ledger_db, + &vec![from_bob_public_address], + 42000000000000, + &vec![KeyImage::from(rng.next_u64())], + &mut rng, + ); + + manually_sync_account( + &ledger_db, + &db_ctx.get_db_instance(logger.clone()), + &AccountID(account_id.to_string()), + &logger, + ); + + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "get_txos", + "params": { + "account_id": account_id, + } + }); + let res = dispatch(&client, body, &logger); + let result = res.get("result").unwrap(); + let txos = result.get("txo_ids").unwrap().as_array().unwrap(); + assert_eq!(txos.len(), 1); + let txo_map = result.get("txo_map").unwrap().as_object().unwrap(); + let txo = &txo_map.get(txos[0].as_str().unwrap()).unwrap(); + let txo_status = txo.get("status").unwrap().as_str().unwrap(); + assert_eq!(txo_status, TxoStatus::Unspent.to_string()); + let value = txo.get("value").unwrap(); + let token_id = txo.get("token_id").unwrap(); + assert_eq!(value, "42000000000000"); + assert_eq!(token_id, "0"); + } + + #[test_with_logger] + fn test_get_address_for_account(logger: Logger) { + let mut rng: StdRng = SeedableRng::from_seed([20u8; 32]); + let (client, _ledger_db, _db_ctx, _network_state) = setup(&mut rng, logger.clone()); + + // Add an account + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "create_account", + "params": { + "name": "Alice Main Account", + } + }); + let res = dispatch(&client, body, &logger); + let result = res.get("result").unwrap(); + let account_id = result + .get("account") + .unwrap() + .get("id") + .unwrap() + .as_str() + .unwrap(); + + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "get_address_for_account", + "params": { + "account_id": account_id, + "index": 2, + } + }); + let res = dispatch(&client, body, &logger); + let error = res.get("error").unwrap(); + let code = error.get("code").unwrap(); + assert_eq!(code, -32603); + + // Create a subaddress + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "assign_address_for_account", + "params": { + "account_id": account_id, + "comment": "test", + } + }); + dispatch(&client, body, &logger); + + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "get_address_for_account", + "params": { + "account_id": account_id, + "index": 2, + } + }); + let res = dispatch(&client, body, &logger); + let result = res.get("result").unwrap(); + let address = result.get("address").unwrap(); + let subaddress_index = address.get("subaddress_index").unwrap().as_str().unwrap(); + + assert_eq!(subaddress_index, "2"); + } + + #[test_with_logger] + fn test_verify_address(logger: Logger) { + let mut rng: StdRng = SeedableRng::from_seed([20u8; 32]); + let (client, _ledger_db, _db_ctx, _network_state) = setup(&mut rng, logger.clone()); + + // Add an account + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "verify_address", + "params": { + "address": "NOTVALIDB58", + } + }); + let res = dispatch(&client, body, &logger); + let result = res["result"]["verified"].as_bool().unwrap(); + assert!(!result); + + // Add an account + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "create_account", + "params": { + "name": "Alice Main Account", + } + }); + let res = dispatch(&client, body, &logger); + let b58_public_address = res["result"]["account"]["main_address"].as_str().unwrap(); + + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "verify_address", + "params": { + "address": b58_public_address, + } + }); + let res = dispatch(&client, body, &logger); + let result = res["result"]["verified"].as_bool().unwrap(); + assert!(result); + } + + #[test_with_logger] + fn test_balance_for_address(logger: Logger) { + let mut rng: StdRng = SeedableRng::from_seed([20u8; 32]); + let (client, mut ledger_db, db_ctx, _network_state) = setup(&mut rng, logger.clone()); + + // Add an account + let body = json!({ + "jsonrpc": "2.0", + "api_version": "2", + "id": 1, + "method": "create_account", + "params": { + "name": "Alice Main Account", + } + }); + let res = dispatch(&client, body, &logger); + let account_id = res["result"]["account"]["id"].as_str().unwrap(); + let b58_public_address = res["result"]["account"]["main_address"].as_str().unwrap(); + + let alice_public_address = b58_decode_public_address(&b58_public_address) + .expect("Could not b58_decode public address"); + add_block_to_ledger_db( + &mut ledger_db, + &vec![alice_public_address], + 42 * MOB, + &vec![KeyImage::from(rng.next_u64())], + &mut rng, + ); + // + manually_sync_account( + &ledger_db, + &db_ctx.get_db_instance(logger.clone()), + &AccountID(account_id.to_string()), + &logger, + ); + + let body = json!({ + "jsonrpc": "2.0", + "api_version": "2", + "id": 1, + "method": "get_address_status", + "params": { + "address": b58_public_address, + } + }); + let res = dispatch(&client, body, &logger); + let result = res.get("result").unwrap(); + let balance_per_token = result.get("balance_per_token").unwrap(); + let balance_mob = balance_per_token.get(Mob::ID.to_string()).unwrap(); + let unspent = balance_mob["unspent"].as_str().unwrap(); + let pending = balance_mob["pending"].as_str().unwrap(); + let spent = balance_mob["spent"].as_str().unwrap(); + let secreted = balance_mob["secreted"].as_str().unwrap(); + let orphaned = balance_mob["orphaned"].as_str().unwrap(); + assert_eq!(unspent, (42 * MOB).to_string()); + assert_eq!(pending, "0"); + assert_eq!(spent, "0"); + assert_eq!(secreted, "0"); + assert_eq!(orphaned, "0"); + + // Create a subaddress + let body = json!({ + "jsonrpc": "2.0", + "api_version": "2", + "id": 1, + "method": "assign_address_for_account", + "params": { + "account_id": account_id, + "comment": "For Bob", + } + }); + let res = dispatch(&client, body, &logger); + let result = res.get("result").unwrap(); + let from_bob_b58_public_address = result + .get("address") + .unwrap() + .get("public_address_b58") + .unwrap() + .as_str() + .unwrap(); + let from_bob_public_address = + b58_decode_public_address(from_bob_b58_public_address).unwrap(); + + // Add a block to the ledger with a transaction "From Bob" + add_block_to_ledger_db( + &mut ledger_db, + &vec![from_bob_public_address], + 64 * MOB, + &vec![KeyImage::from(rng.next_u64())], + &mut rng, + ); + // + manually_sync_account( + &ledger_db, + &db_ctx.get_db_instance(logger.clone()), + &AccountID(account_id.to_string()), + &logger, + ); + + let body = json!({ + "jsonrpc": "2.0", + "api_version": "2", + "id": 1, + "method": "get_address_status", + "params": { + "address": from_bob_b58_public_address, + } + }); + let res = dispatch(&client, body, &logger); + let result = res.get("result").unwrap(); + let balance_per_token = result.get("balance_per_token").unwrap(); + let balance_mob = balance_per_token.get(Mob::ID.to_string()).unwrap(); + let unspent = balance_mob["unspent"].as_str().unwrap(); + assert_eq!(unspent, (64 * MOB).to_string()); + } +} diff --git a/full-service/src/json_rpc/v2/e2e_tests/account/create_import/account_crud.rs b/full-service/src/json_rpc/v2/e2e_tests/account/create_import/account_crud.rs new file mode 100644 index 000000000..46dfa526d --- /dev/null +++ b/full-service/src/json_rpc/v2/e2e_tests/account/create_import/account_crud.rs @@ -0,0 +1,163 @@ +// Copyright (c) 2020-2022 MobileCoin Inc. + +//! End-to-end tests for the Full Service Wallet API. + +#[cfg(test)] +mod e2e_account { + use crate::json_rpc::v2::api::test_utils::{dispatch, setup}; + + use mc_common::logger::{test_with_logger, Logger}; + + use rand::{rngs::StdRng, SeedableRng}; + + #[test_with_logger] + fn test_e2e_account_crud(logger: Logger) { + let mut rng: StdRng = SeedableRng::from_seed([20u8; 32]); + let (client, _ledger_db, _db_ctx, _network_state) = setup(&mut rng, logger.clone()); + + // Create Account + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "create_account", + "params": { + "name": "Alice Main Account", + }, + }); + let res = dispatch(&client, body, &logger); + assert_eq!(res.get("jsonrpc").unwrap(), "2.0"); + + let result = res.get("result").unwrap(); + let account_obj = result.get("account").unwrap(); + assert!(account_obj.get("id").is_some()); + assert_eq!(account_obj.get("name").unwrap(), "Alice Main Account"); + assert!(account_obj.get("main_address").is_some()); + assert_eq!(account_obj.get("next_subaddress_index").unwrap(), "2"); + assert_eq!(account_obj.get("recovery_mode").unwrap(), false); + assert_eq!(account_obj.get("fog_enabled").unwrap(), false); + + let account_id = account_obj.get("id").unwrap(); + + // Read Accounts via Get All + let body = json!({ + "jsonrpc": "2.0", + "id": 2, + "method": "get_accounts", + "params": {} + }); + let res = dispatch(&client, body, &logger); + let result = res.get("result").unwrap(); + let accounts = result.get("account_ids").unwrap().as_array().unwrap(); + assert_eq!(accounts.len(), 1); + let account_map = result.get("account_map").unwrap().as_object().unwrap(); + assert_eq!( + account_map + .get(accounts[0].as_str().unwrap()) + .unwrap() + .get("id") + .unwrap(), + &account_id.clone() + ); + + let body = json!({ + "jsonrpc": "2.0", + "id": 2, + "method": "get_account_status", + "params": { + "account_id": *account_id, + } + }); + let res = dispatch(&client, body, &logger); + let result = res.get("result").unwrap(); + let name = result.get("account").unwrap().get("name").unwrap(); + assert_eq!("Alice Main Account", name.as_str().unwrap()); + + // FIXME: assert balance + + // Update Account + let body = json!({ + "jsonrpc": "2.0", + "id": 2, + "method": "update_account_name", + "params": { + "account_id": *account_id, + "name": "Eve Main Account", + } + }); + let res = dispatch(&client, body, &logger); + let result = res.get("result").unwrap(); + assert_eq!( + result.get("account").unwrap().get("name").unwrap(), + "Eve Main Account" + ); + + let body = json!({ + "jsonrpc": "2.0", + "id": 2, + "method": "get_account_status", + "params": { + "account_id": *account_id, + } + }); + let res = dispatch(&client, body, &logger); + let result = res.get("result").unwrap(); + let name = result.get("account").unwrap().get("name").unwrap(); + assert_eq!("Eve Main Account", name.as_str().unwrap()); + + // Remove Account + let body = json!({ + "jsonrpc": "2.0", + "id": 2, + "method": "remove_account", + "params": { + "account_id": *account_id, + } + }); + let res = dispatch(&client, body, &logger); + let result = res.get("result").unwrap(); + assert_eq!(result["removed"].as_bool().unwrap(), true,); + + let body = json!({ + "jsonrpc": "2.0", + "id": 2, + "method": "get_accounts", + "params": {} + }); + let res = dispatch(&client, body, &logger); + let result = res.get("result").unwrap(); + let accounts = result.get("account_ids").unwrap().as_array().unwrap(); + assert_eq!(accounts.len(), 0); + } + + #[test_with_logger] + fn test_e2e_create_account_with_fog(logger: Logger) { + let mut rng: StdRng = SeedableRng::from_seed([20u8; 32]); + let (client, _ledger_db, _db_ctx, _network_state) = setup(&mut rng, logger.clone()); + // Create Account + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "create_account", + "params": { + "name": "Alice Main Account", + "fog_info": { + "report_url": "fog://fog-report.example.com", + "report_id": "", + "authority_spki": "MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAvnB9wTbTOT5uoizRYaYbw7XIEkInl8E7MGOAQj+xnC+F1rIXiCnc/t1+5IIWjbRGhWzo7RAwI5sRajn2sT4rRn9NXbOzZMvIqE4hmhmEzy1YQNDnfALAWNQ+WBbYGW+Vqm3IlQvAFFjVN1YYIdYhbLjAPdkgeVsWfcLDforHn6rR3QBZYZIlSBQSKRMY/tywTxeTCvK2zWcS0kbbFPtBcVth7VFFVPAZXhPi9yy1AvnldO6n7KLiupVmojlEMtv4FQkk604nal+j/dOplTATV8a9AJBbPRBZ/yQg57EG2Y2MRiHOQifJx0S5VbNyMm9bkS8TD7Goi59aCW6OT1gyeotWwLg60JRZTfyJ7lYWBSOzh0OnaCytRpSWtNZ6barPUeOnftbnJtE8rFhF7M4F66et0LI/cuvXYecwVwykovEVBKRF4HOK9GgSm17mQMtzrD7c558TbaucOWabYR04uhdAc3s10MkuONWG0wIQhgIChYVAGnFLvSpp2/aQEq3xrRSETxsixUIjsZyWWROkuA0IFnc8d7AmcnUBvRW7FT/5thWyk5agdYUGZ+7C1o69ihR1YxmoGh69fLMPIEOhYh572+3ckgl2SaV4uo9Gvkz8MMGRBcMIMlRirSwhCfozV2RyT5Wn1NgPpyc8zJL7QdOhL7Qxb+5WjnCVrQYHI2cCAwEAAQ==" + } + }, + }); + + let res = dispatch(&client, body, &logger); + assert_eq!(res.get("jsonrpc").unwrap(), "2.0"); + + let result = res.get("result").unwrap(); + let account_obj = result.get("account").unwrap(); + assert!(account_obj.get("id").is_some()); + assert_eq!(account_obj.get("name").unwrap(), "Alice Main Account"); + assert_eq!(account_obj.get("recovery_mode").unwrap(), false); + assert!(account_obj.get("main_address").is_some()); + assert_eq!(account_obj.get("next_subaddress_index").unwrap(), "1"); + assert_eq!(account_obj.get("fog_enabled").unwrap(), true); + } +} diff --git a/full-service/src/json_rpc/v2/e2e_tests/account/create_import/import_account.rs b/full-service/src/json_rpc/v2/e2e_tests/account/create_import/import_account.rs new file mode 100644 index 000000000..1a144b058 --- /dev/null +++ b/full-service/src/json_rpc/v2/e2e_tests/account/create_import/import_account.rs @@ -0,0 +1,451 @@ +// Copyright (c) 2020-2022 MobileCoin Inc. + +//! End-to-end tests for the Full Service Wallet API. + +#[cfg(test)] +mod e2e_account { + use crate::{ + db::account::AccountID, + json_rpc::v2::api::test_utils::{dispatch, dispatch_expect_error, setup}, + test_utils::{add_block_to_ledger_db, manually_sync_account}, + util::b58::b58_decode_public_address, + }; + + use mc_common::logger::{test_with_logger, Logger}; + use mc_crypto_rand::rand_core::RngCore; + use mc_ledger_db::Ledger; + use mc_transaction_core::{ring_signature::KeyImage, tokens::Mob, Token}; + use rand::{rngs::StdRng, SeedableRng}; + + #[test_with_logger] + fn test_e2e_import_account(logger: Logger) { + let mut rng: StdRng = SeedableRng::from_seed([20u8; 32]); + let (client, _ledger_db, _db_ctx, _network_state) = setup(&mut rng, logger.clone()); + + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "import_account", + "params": { + "mnemonic": "sheriff odor square mistake huge skate mouse shoot purity weapon proof stuff correct concert blanket neck own shift clay mistake air viable stick group", + "key_derivation_version": "2", + "name": "Alice Main Account", + "first_block_index": "200", + } + }); + let res = dispatch(&client, body, &logger); + let result = res.get("result").unwrap(); + let account_obj = result.get("account").unwrap(); + let public_address = account_obj.get("main_address").unwrap().as_str().unwrap(); + assert_eq!(public_address, "3CnfxAc2LvKw4FDNRVgj3GndwAhgQDd7v2Cne66GTUJyzBr3WzSikk9nJ5sCAb1jgSSKaqpWQtcEjV1nhoadVKjq2Soa8p3XZy6u2tpHdor"); + let account_id = account_obj.get("id").unwrap().as_str().unwrap(); + assert_eq!( + account_id, + "7872edf0d4094643213aabc92aa0d07379cfb58eda0722b21a44868f22f75b4e" + ); + + assert_eq!( + *account_obj.get("first_block_index").unwrap(), + serde_json::json!("200") + ); + assert_eq!(account_obj.get("next_subaddress_index").unwrap(), "2"); + assert_eq!(account_obj.get("fog_enabled").unwrap(), false); + } + + #[test_with_logger] + fn test_e2e_import_account_unknown_version(logger: Logger) { + let mut rng: StdRng = SeedableRng::from_seed([20u8; 32]); + let (client, _ledger_db, _db_ctx, _network_state) = setup(&mut rng, logger.clone()); + + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "import_account", + "params": { + "mnemonic": "sheriff odor square mistake huge skate mouse shoot purity weapon proof stuff correct concert blanket neck own shift clay mistake air viable stick group", + "key_derivation_version": "3", + "name": "", + } + }); + dispatch_expect_error( + &client, + body, + &logger, + json!({ + "method": "import_account", + "error": json!({ + "code": -32603, + "message": "InternalError", + "data": json!({ + "server_error": "UnknownKeyDerivation(3)", + "details": "Unknown key version version: 3", + }) + }), + "jsonrpc": "2.0", + "id": 1, + }) + .to_string(), + ); + } + + #[test_with_logger] + fn test_e2e_import_account_legacy(logger: Logger) { + let mut rng: StdRng = SeedableRng::from_seed([20u8; 32]); + let (client, _ledger_db, _db_ctx, _network_state) = setup(&mut rng, logger.clone()); + + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "import_account_from_legacy_root_entropy", + "params": { + "entropy": "c593274dc6f6eb94242e34ae5f0ab16bc3085d45d49d9e18b8a8c6f057e6b56b", + "name": "Alice Main Account", + "first_block_index": "200", + } + }); + let res = dispatch(&client, body, &logger); + let result = res.get("result").unwrap(); + let account_obj = result.get("account").unwrap(); + let public_address = account_obj.get("main_address").unwrap().as_str().unwrap(); + assert_eq!(public_address, "8JtpPPh9mV2PTLrrDz4f2j4PtUpNWnrRg8HKpnuwkZbj5j8bGqtNMNLC9E3zjzcw456215yMjkCVYK4FPZTX4gijYHiuDT31biNHrHmQmsU"); + let account_id = account_obj.get("id").unwrap().as_str().unwrap(); + // Catches if a change results in changed accounts_ids, which should always be + // made to be backward compatible. + assert_eq!( + account_id, + "f9957a9d050ef8dff9d8ef6f66daa608081e631b2d918988311613343827b779" + ); + assert_eq!( + *account_obj.get("first_block_index").unwrap(), + serde_json::json!("200") + ); + assert_eq!(account_obj.get("next_subaddress_index").unwrap(), "2"); + assert_eq!(account_obj.get("fog_enabled").unwrap(), false); + } + + #[test_with_logger] + fn test_e2e_import_account_fog(logger: Logger) { + let mut rng: StdRng = SeedableRng::from_seed([20u8; 32]); + let (client, _ledger_db, _db_ctx, _network_state) = setup(&mut rng, logger.clone()); + + // Import an account with fog info. + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "import_account", + "params": { + "mnemonic": "sheriff odor square mistake huge skate mouse shoot purity weapon proof stuff correct concert blanket neck own shift clay mistake air viable stick group", + "key_derivation_version": "2", + "name": "Alice Main Account", + "first_block_index": "200", + "fog_info": { + "report_url": "fog://fog-report.example.com", + "report_id": "", + "authority_spki": "MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAvnB9wTbTOT5uoizRYaYbw7XIEkInl8E7MGOAQj+xnC+F1rIXiCnc/t1+5IIWjbRGhWzo7RAwI5sRajn2sT4rRn9NXbOzZMvIqE4hmhmEzy1YQNDnfALAWNQ+WBbYGW+Vqm3IlQvAFFjVN1YYIdYhbLjAPdkgeVsWfcLDforHn6rR3QBZYZIlSBQSKRMY/tywTxeTCvK2zWcS0kbbFPtBcVth7VFFVPAZXhPi9yy1AvnldO6n7KLiupVmojlEMtv4FQkk604nal+j/dOplTATV8a9AJBbPRBZ/yQg57EG2Y2MRiHOQifJx0S5VbNyMm9bkS8TD7Goi59aCW6OT1gyeotWwLg60JRZTfyJ7lYWBSOzh0OnaCytRpSWtNZ6barPUeOnftbnJtE8rFhF7M4F66et0LI/cuvXYecwVwykovEVBKRF4HOK9GgSm17mQMtzrD7c558TbaucOWabYR04uhdAc3s10MkuONWG0wIQhgIChYVAGnFLvSpp2/aQEq3xrRSETxsixUIjsZyWWROkuA0IFnc8d7AmcnUBvRW7FT/5thWyk5agdYUGZ+7C1o69ihR1YxmoGh69fLMPIEOhYh572+3ckgl2SaV4uo9Gvkz8MMGRBcMIMlRirSwhCfozV2RyT5Wn1NgPpyc8zJL7QdOhL7Qxb+5WjnCVrQYHI2cCAwEAAQ==" + } + } + }); + let res = dispatch(&client, body, &logger); + let result = res.get("result").unwrap(); + let account_obj = result.get("account").unwrap(); + let public_address = account_obj.get("main_address").unwrap().as_str().unwrap(); + assert_eq!(public_address, "2kD4vRp3DaBdRrNLNhJ5BKf5FsZxcAijoMt5pxjJpbk5jQRubngUXnd92vuXWkFyezuLgjCiKu4JHjpjNCnmzf1gAdW6PbqXsecQtp8Qr8uoeeDKrd1a5PtA6apXuDVtnrKsDCcHiJqdeSt3bRsPBvkBP4JqpGyAeKFsC7s2LQwuZ88BxFe2kyeZp5G3zENfvLaMripxTKkWGDopok2LCyA9NiCDf1vwjA5opLU7eqaRfh9"); + let account_id = account_obj.get("id").unwrap().as_str().unwrap(); + assert_eq!( + account_id, + "0b8a95253a7d57faf8510d8092ab55fb8610a9d691a7fa3bfafbf49945b845a2" + ); + + assert_eq!(account_obj.get("next_subaddress_index").unwrap(), "1"); + assert_eq!(account_obj.get("fog_enabled").unwrap(), true); + } + + #[test_with_logger] + fn test_e2e_import_account_legacy_fog(logger: Logger) { + let mut rng: StdRng = SeedableRng::from_seed([20u8; 32]); + let (client, _ledger_db, _db_ctx, _network_state) = setup(&mut rng, logger.clone()); + + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "import_account_from_legacy_root_entropy", + "params": { + "entropy": "c593274dc6f6eb94242e34ae5f0ab16bc3085d45d49d9e18b8a8c6f057e6b56b", + "name": "Alice Main Account", + "first_block_index": "200", + "fog_info": { + "report_url": "fog://fog-report.example.com", + "report_id": "", + "authority_spki": "MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAvnB9wTbTOT5uoizRYaYbw7XIEkInl8E7MGOAQj+xnC+F1rIXiCnc/t1+5IIWjbRGhWzo7RAwI5sRajn2sT4rRn9NXbOzZMvIqE4hmhmEzy1YQNDnfALAWNQ+WBbYGW+Vqm3IlQvAFFjVN1YYIdYhbLjAPdkgeVsWfcLDforHn6rR3QBZYZIlSBQSKRMY/tywTxeTCvK2zWcS0kbbFPtBcVth7VFFVPAZXhPi9yy1AvnldO6n7KLiupVmojlEMtv4FQkk604nal+j/dOplTATV8a9AJBbPRBZ/yQg57EG2Y2MRiHOQifJx0S5VbNyMm9bkS8TD7Goi59aCW6OT1gyeotWwLg60JRZTfyJ7lYWBSOzh0OnaCytRpSWtNZ6barPUeOnftbnJtE8rFhF7M4F66et0LI/cuvXYecwVwykovEVBKRF4HOK9GgSm17mQMtzrD7c558TbaucOWabYR04uhdAc3s10MkuONWG0wIQhgIChYVAGnFLvSpp2/aQEq3xrRSETxsixUIjsZyWWROkuA0IFnc8d7AmcnUBvRW7FT/5thWyk5agdYUGZ+7C1o69ihR1YxmoGh69fLMPIEOhYh572+3ckgl2SaV4uo9Gvkz8MMGRBcMIMlRirSwhCfozV2RyT5Wn1NgPpyc8zJL7QdOhL7Qxb+5WjnCVrQYHI2cCAwEAAQ==" + } + } + }); + let res = dispatch(&client, body, &logger); + let result = res.get("result").unwrap(); + let account_obj = result.get("account").unwrap(); + let public_address = account_obj.get("main_address").unwrap().as_str().unwrap(); + assert_eq!(public_address, "d3FhtyUQDYJFpEmzoXmRtF9VA5FTLycgQBKf1JEJJj8K6UXCuwzGD2uVYw1cxzZpbSivZLSxf9nZpMgUnuRxSpJA9qCDpDZd2qtc7j2N2x4758dQ91jrSCxzyuR1aJR7zgdcgdF2KwSShUhQ5n7M9uebf2HqiCWt8vttqESJ7aRNDwiW8TVmeKWviWunzYG46c8vo4DeZYK4wFfLNdwmeSn9HXKkQVpNgzsMz87cKpHRnzn"); + let account_id = account_obj.get("id").unwrap().as_str().unwrap(); + // Catches if a change results in changed accounts_ids, which should always be + // made to be backward compatible. + assert_eq!( + account_id, + "9111a17691a1eecb85bbeaa789c69471e7c8b9789e0068de02204f9d7264263d" + ); + assert_eq!(account_obj.get("next_subaddress_index").unwrap(), "1"); + assert_eq!(account_obj.get("fog_enabled").unwrap(), true); + } + + #[test_with_logger] + fn test_e2e_import_delete_import(logger: Logger) { + let mut rng: StdRng = SeedableRng::from_seed([20u8; 32]); + let (client, _ledger_db, _db_ctx, _network_state) = setup(&mut rng, logger.clone()); + + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "import_account_from_legacy_root_entropy", + "params": { + "entropy": "c593274dc6f6eb94242e34ae5f0ab16bc3085d45d49d9e18b8a8c6f057e6b56b", + "name": "Alice Main Account", + "first_block_index": "200", + } + }); + let res = dispatch(&client, body, &logger); + let result = res.get("result").unwrap(); + let account_obj = result.get("account").unwrap(); + let public_address = account_obj.get("main_address").unwrap().as_str().unwrap(); + assert_eq!(public_address, "8JtpPPh9mV2PTLrrDz4f2j4PtUpNWnrRg8HKpnuwkZbj5j8bGqtNMNLC9E3zjzcw456215yMjkCVYK4FPZTX4gijYHiuDT31biNHrHmQmsU"); + let account_id = account_obj.get("id").unwrap().as_str().unwrap(); + // Catches if a change results in changed accounts_ids, which should always be + // made to be backward compatible. + assert_eq!( + account_id, + "f9957a9d050ef8dff9d8ef6f66daa608081e631b2d918988311613343827b779" + ); + + // Delete Account + let body = json!({ + "jsonrpc": "2.0", + "id": 2, + "method": "remove_account", + "params": { + "account_id": *account_id, + } + }); + let res = dispatch(&client, body, &logger); + let result = res.get("result").unwrap(); + assert_eq!(result["removed"].as_bool().unwrap(), true); + + // Import it again - should succeed. + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "import_account_from_legacy_root_entropy", + "params": { + "entropy": "c593274dc6f6eb94242e34ae5f0ab16bc3085d45d49d9e18b8a8c6f057e6b56b", + "name": "Alice Main Account", + "first_block_index": "200", + } + }); + let res = dispatch(&client, body, &logger); + let result = res.get("result").unwrap(); + let account_obj = result.get("account").unwrap(); + let public_address = account_obj.get("main_address").unwrap().as_str().unwrap(); + assert_eq!(public_address, "8JtpPPh9mV2PTLrrDz4f2j4PtUpNWnrRg8HKpnuwkZbj5j8bGqtNMNLC9E3zjzcw456215yMjkCVYK4FPZTX4gijYHiuDT31biNHrHmQmsU"); + } + + #[test_with_logger] + fn test_import_account_with_next_subaddress_index(logger: Logger) { + let mut rng: StdRng = SeedableRng::from_seed([20u8; 32]); + let (client, mut ledger_db, db_ctx, _network_state) = setup(&mut rng, logger.clone()); + + // create an account + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "import_account_from_legacy_root_entropy", + "params": { + "entropy": "c593274dc6f6eb94242e34ae5f0ab16bc3085d45d49d9e18b8a8c6f057e6b56b", + "name": "Alice Main Account", + } + }); + let res = dispatch(&client, body, &logger); + let result = res.get("result").unwrap(); + let account_obj = result.get("account").unwrap(); + let account_id = account_obj.get("id").unwrap().as_str().unwrap(); + + // assign next subaddress for account + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "assign_address_for_account", + "params": { + "account_id": account_id, + "metadata": "subaddress_index_2", + } + }); + let res = dispatch(&client, body, &logger); + let result = res.get("result").unwrap(); + let address = result.get("address").unwrap(); + let b58_public_address = address.get("public_address_b58").unwrap().as_str().unwrap(); + let public_address = b58_decode_public_address(b58_public_address).unwrap(); + + // Add a block to fund account at the new subaddress. + add_block_to_ledger_db( + &mut ledger_db, + &vec![public_address], + 100000000000000, // 100.0 MOB + &vec![KeyImage::from(rng.next_u64())], + &mut rng, + ); + + manually_sync_account( + &ledger_db, + &db_ctx.get_db_instance(logger.clone()), + &AccountID(account_id.to_string()), + &logger, + ); + assert_eq!(ledger_db.num_blocks().unwrap(), 13); + + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "get_account_status", + "params": { + "account_id": account_id, + } + }); + let res = dispatch(&client, body, &logger); + let result = res.get("result").unwrap(); + let balance_per_token = result.get("balance_per_token").unwrap(); + let balance_mob = balance_per_token.get(Mob::ID.to_string()).unwrap(); + let unspent = balance_mob["unspent"].as_str().unwrap(); + + assert_eq!("100000000000000", unspent); + + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "remove_account", + "params": { + "account_id": account_id, + } + }); + dispatch(&client, body, &logger); + + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "import_account_from_legacy_root_entropy", + "params": { + "entropy": "c593274dc6f6eb94242e34ae5f0ab16bc3085d45d49d9e18b8a8c6f057e6b56b", + "name": "Alice Main Account", + } + }); + dispatch(&client, body, &logger); + + manually_sync_account( + &ledger_db, + &db_ctx.get_db_instance(logger.clone()), + &AccountID(account_id.to_string()), + &logger, + ); + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "get_account_status", + "params": { + "account_id": account_id, + } + }); + let res = dispatch(&client, body, &logger); + let result = res.get("result").unwrap(); + let balance_per_token = result.get("balance_per_token").unwrap(); + let balance_mob = balance_per_token.get(Mob::ID.to_string()).unwrap(); + let unspent = balance_mob["unspent"].as_str().unwrap(); + let orphaned = balance_mob["orphaned"].as_str().unwrap(); + let spent = balance_mob["spent"].as_str().unwrap(); + + assert_eq!("0", unspent); + assert_eq!("100000000000000", orphaned); + assert_eq!("0", spent); + + // assign next subaddress for account + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "assign_address_for_account", + "params": { + "account_id": account_id, + "metadata": "subaddress_index_2", + } + }); + dispatch(&client, body, &logger); + + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "get_account_status", + "params": { + "account_id": account_id, + } + }); + let res = dispatch(&client, body, &logger); + let result = res.get("result").unwrap(); + let balance_per_token = result.get("balance_per_token").unwrap(); + let balance_mob = balance_per_token.get(Mob::ID.to_string()).unwrap(); + let unspent = balance_mob["unspent"].as_str().unwrap(); + let orphaned = balance_mob["orphaned"].as_str().unwrap(); + + assert_eq!("100000000000000", unspent); + assert_eq!("0", orphaned); + + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "remove_account", + "params": { + "account_id": account_id, + } + }); + dispatch(&client, body, &logger); + + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "import_account_from_legacy_root_entropy", + "params": { + "entropy": "c593274dc6f6eb94242e34ae5f0ab16bc3085d45d49d9e18b8a8c6f057e6b56b", + "name": "Alice Main Account", + "next_subaddress_index": "3", + } + }); + dispatch(&client, body, &logger); + + manually_sync_account( + &ledger_db, + &db_ctx.get_db_instance(logger.clone()), + &AccountID(account_id.to_string()), + &logger, + ); + + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "get_account_status", + "params": { + "account_id": account_id, + } + }); + let res = dispatch(&client, body, &logger); + let result = res.get("result").unwrap(); + let balance_per_token = result.get("balance_per_token").unwrap(); + let balance_mob = balance_per_token.get(Mob::ID.to_string()).unwrap(); + let unspent = balance_mob["unspent"].as_str().unwrap(); + let orphaned = balance_mob["orphaned"].as_str().unwrap(); + + assert_eq!("100000000000000", unspent); + assert_eq!("0", orphaned); + } +} diff --git a/full-service/src/json_rpc/v2/e2e_tests/account/create_import/mod.rs b/full-service/src/json_rpc/v2/e2e_tests/account/create_import/mod.rs new file mode 100644 index 000000000..5e9f3f3db --- /dev/null +++ b/full-service/src/json_rpc/v2/e2e_tests/account/create_import/mod.rs @@ -0,0 +1,3 @@ +mod account_crud; +mod import_account; +mod view_account_flow; diff --git a/full-service/src/json_rpc/v2/e2e_tests/account/create_import/view_account_flow.rs b/full-service/src/json_rpc/v2/e2e_tests/account/create_import/view_account_flow.rs new file mode 100644 index 000000000..8ff83dcb4 --- /dev/null +++ b/full-service/src/json_rpc/v2/e2e_tests/account/create_import/view_account_flow.rs @@ -0,0 +1,218 @@ +// Copyright (c) 2020-2022 MobileCoin Inc. + +//! End-to-end tests for the Full Service Wallet API. + +#[cfg(test)] +mod e2e_account { + use crate::{ + db::account::AccountID, + json_rpc::v2::api::test_utils::{dispatch, setup}, + test_utils::{add_block_to_ledger_db, manually_sync_account, MOB}, + util::b58::b58_decode_public_address, + }; + + use mc_common::logger::{test_with_logger, Logger}; + use mc_crypto_rand::rand_core::RngCore; + + use mc_transaction_core::{ring_signature::KeyImage, tokens::Mob, Token}; + use rand::{rngs::StdRng, SeedableRng}; + + #[test_with_logger] + fn test_e2e_view_only_account_flow(logger: Logger) { + // create normal account + let mut rng: StdRng = SeedableRng::from_seed([20u8; 32]); + let (client, mut ledger_db, db_ctx, _network_state) = setup(&mut rng, logger.clone()); + let wallet_db = db_ctx.get_db_instance(logger.clone()); + + // Create Account + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "create_account", + "params": { + "name": "Alice Main Account", + }, + }); + let res = dispatch(&client, body, &logger); + assert_eq!(res.get("jsonrpc").unwrap(), "2.0"); + + let result = res.get("result").unwrap(); + let account_obj = result.get("account").unwrap(); + assert!(account_obj.get("id").is_some()); + assert_eq!(account_obj.get("name").unwrap(), "Alice Main Account"); + let account_id = account_obj.get("id").unwrap(); + let main_address = account_obj.get("main_address").unwrap().as_str().unwrap(); + let main_account_address = b58_decode_public_address(main_address).unwrap(); + + // add some funds to that account + add_block_to_ledger_db( + &mut ledger_db, + &vec![main_account_address], + 100 * MOB, + &vec![KeyImage::from(rng.next_u64())], + &mut rng, + ); + manually_sync_account( + &ledger_db, + &db_ctx.get_db_instance(logger.clone()), + &AccountID(account_id.as_str().unwrap().to_string()), + &logger, + ); + + // confirm that the regular account has the correct balance + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "get_account_status", + "params": { + "account_id": account_id, + }, + }); + let res = dispatch(&client, body, &logger); + let result = res.get("result").unwrap(); + let balance_per_token = result.get("balance_per_token").unwrap(); + let balance_mob = balance_per_token.get(Mob::ID.to_string()).unwrap(); + let unspent = balance_mob["unspent"].as_str().unwrap(); + assert_eq!(unspent, "100000000000000"); + + // export view only import package + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "create_view_only_account_import_request", + "params": { + "account_id": account_id, + }, + }); + let res = dispatch(&client, body, &logger); + assert_eq!(res.get("jsonrpc").unwrap(), "2.0"); + let result = res.get("result").unwrap(); + let request = result.get("json_rpc_request").unwrap(); + + let body = json!({ + "jsonrpc": "2.0", + "id": 2, + "method": "remove_account", + "params": { + "account_id": account_id, + } + }); + let res = dispatch(&client, body, &logger); + let result = res.get("result").unwrap(); + assert_eq!(result["removed"].as_bool().unwrap(), true); + + // import vo account + let body = json!(request); + let res = dispatch(&client, body, &logger); + let result = res.get("result").unwrap(); + let account = result.get("account").unwrap(); + let vo_account_id = account.get("id").unwrap(); + assert_eq!(vo_account_id, account_id); + + // sync the view only account + manually_sync_account( + &ledger_db, + &wallet_db, + &AccountID(vo_account_id.as_str().unwrap().to_string()), + &logger, + ); + + // confirm that the view only account has the correct balance + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "get_account_status", + "params": { + "account_id": vo_account_id, + }, + }); + let res = dispatch(&client, body, &logger); + let result = res.get("result").unwrap(); + let balance_per_token = result.get("balance_per_token").unwrap(); + let balance_mob = balance_per_token.get(Mob::ID.to_string()).unwrap(); + let unverified = balance_mob["unverified"].as_str().unwrap(); + let unspent = balance_mob["unspent"].as_str().unwrap(); + assert_eq!(unverified, "100000000000000"); + assert_eq!(unspent, "0"); + + let account = result.get("account").unwrap(); + let vo_account_id = account.get("id").unwrap(); + assert_eq!(vo_account_id, account_id); + + // test update name + let name = "Look at these coins"; + let body = json!({ + "jsonrpc": "2.0", + "id": 2, + "method": "update_account_name", + "params": { + "account_id": vo_account_id, + "name": name, + } + }); + let res = dispatch(&client, body, &logger); + let result = res.get("result").unwrap(); + let account = result.get("account").unwrap(); + let account_name = account.get("name").unwrap(); + assert_eq!(name, account_name); + + // test creating unsigned tx + let body = json!({ + "jsonrpc": "2.0", + "id": 2, + "method": "build_unsigned_transaction", + "params": { + "account_id": account_id, + "recipient_public_address": main_address, + "amount": { "value": "50000000000000", "token_id": "0"}, + } + }); + let res = dispatch(&client, body, &logger); + let result = res.get("result").unwrap(); + let _tx = result.get("unsigned_tx_proposal").unwrap(); + + // test create sync account request + let body = json!({ + "jsonrpc": "2.0", + "id": 2, + "method": "create_view_only_account_sync_request", + "params": { + "account_id": account_id, + } + }); + let res = dispatch(&client, body, &logger); + let result = res.get("result").unwrap(); + let unverified_txos = result + .get("incomplete_txos_encoded") + .unwrap() + .as_array() + .unwrap(); + assert_eq!(unverified_txos.len(), 1); + + // test remove + let body = json!({ + "jsonrpc": "2.0", + "id": 2, + "method": "remove_account", + "params": { + "account_id": vo_account_id, + } + }); + let res = dispatch(&client, body, &logger); + let result = res.get("result").unwrap(); + let removed = result.get("removed").unwrap().as_bool().unwrap(); + assert!(removed); + + // test get-all + let body = json!({ + "jsonrpc": "2.0", + "id": 2, + "method": "get_accounts", + "params": {} + }); + let res = dispatch(&client, body, &logger); + let result = res.get("result").unwrap(); + let account_ids = result.get("account_ids").unwrap().as_array().unwrap(); + assert_eq!(account_ids.len(), 0); + } +} diff --git a/full-service/src/json_rpc/v2/e2e_tests/account/mod.rs b/full-service/src/json_rpc/v2/e2e_tests/account/mod.rs new file mode 100644 index 000000000..74e2f0931 --- /dev/null +++ b/full-service/src/json_rpc/v2/e2e_tests/account/mod.rs @@ -0,0 +1,4 @@ +mod account_address; +mod account_balance; +mod account_other; +mod create_import; diff --git a/full-service/src/json_rpc/v2/e2e_tests/mod.rs b/full-service/src/json_rpc/v2/e2e_tests/mod.rs new file mode 100644 index 000000000..215cf4f51 --- /dev/null +++ b/full-service/src/json_rpc/v2/e2e_tests/mod.rs @@ -0,0 +1,3 @@ +mod account; +mod other; +mod transaction; diff --git a/full-service/src/json_rpc/v2/e2e_tests/other.rs b/full-service/src/json_rpc/v2/e2e_tests/other.rs new file mode 100644 index 000000000..fe5f9b43e --- /dev/null +++ b/full-service/src/json_rpc/v2/e2e_tests/other.rs @@ -0,0 +1,223 @@ +// Copyright (c) &2020-2022 MobileCoin Inc. + +//! End-to-end tests for the Full Service Wallet API. + +#[cfg(test)] +mod e2e_misc { + use crate::{ + json_rpc::v2::api::test_utils::{ + dispatch, dispatch_with_header, dispatch_with_header_expect_error, setup, + setup_no_wallet_db, setup_with_api_key, wait_for_sync, + }, + test_utils::{ + add_block_with_tx_outs, create_test_received_txo, random_account_with_seed_values, MOB, + }, + }; + use mc_common::logger::{test_with_logger, Logger}; + use mc_crypto_rand::RngCore; + use mc_transaction_core::{ring_signature::KeyImage, tokens::Mob, Amount, BlockVersion, Token}; + use rand::{rngs::StdRng, SeedableRng}; + use rocket::http::{Header, Status}; + + #[test_with_logger] + fn test_wallet_status(logger: Logger) { + let mut rng: StdRng = SeedableRng::from_seed([20u8; 32]); + let (client, _ledger_db, _db_ctx, _network_state) = setup(&mut rng, logger.clone()); + + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "create_account", + "params": { + "name": "Alice Main Account", + } + }); + let _result = dispatch(&client, body, &logger).get("result").unwrap(); + + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "get_wallet_status", + }); + let res = dispatch(&client, body, &logger); + let result = res.get("result").unwrap(); + let status = result.get("wallet_status").unwrap(); + assert_eq!(status.get("network_block_height").unwrap(), "12"); + assert_eq!(status.get("local_block_height").unwrap(), "12"); + // Syncing will have already started, so we can't determine what the min synced + // index is. + assert!(status.get("min_synced_block_index").is_some()); + let balance_per_token = status.get("balance_per_token").unwrap(); + let balance_mob = balance_per_token.get(Mob::ID.to_string()); + assert!(balance_mob.is_none()); + } + + #[test_with_logger] + fn test_request_with_correct_api_key(logger: Logger) { + let api_key = "mobilecats"; + + let mut rng: StdRng = SeedableRng::from_seed([20u8; 32]); + let (client, _ledger_db, _db_ctx, _network_state) = + setup_with_api_key(&mut rng, logger.clone(), api_key.to_string()); + + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "create_account", + "params": { + "name": "Alice Main Account", + }, + }); + + let header = Header::new("X-API-KEY", api_key); + + dispatch_with_header(&client, body, header, &logger); + } + + #[test_with_logger] + fn test_request_with_bad_api_key(logger: Logger) { + let api_key = "mobilecats"; + + let mut rng: StdRng = SeedableRng::from_seed([20u8; 32]); + let (client, _ledger_db, _db_ctx, _network_state) = + setup_with_api_key(&mut rng, logger.clone(), api_key.to_string()); + + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "create_account", + "params": { + "name": "Alice Main Account", + }, + }); + + let header = Header::new("X-API-KEY", "wrong-header"); + + dispatch_with_header_expect_error(&client, body, header, &logger, Status::Unauthorized); + } + + #[test_with_logger] + fn test_get_network_status(logger: Logger) { + let mut rng: StdRng = SeedableRng::from_seed([20u8; 32]); + let (client, _ledger_db, _db_ctx, _network_state) = setup(&mut rng, logger.clone()); + + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "get_network_status" + }); + let res = dispatch(&client, body, &logger); + let result = res.get("result").unwrap(); + let status = result.get("network_status").unwrap(); + assert_eq!(status.get("network_block_height").unwrap(), "12"); + assert_eq!(status.get("local_block_height").unwrap(), "12"); + assert_eq!( + status.get("block_version").unwrap(), + &BlockVersion::MAX.to_string() + ); + + let fees = status.get("fees").unwrap().as_object().unwrap(); + assert_eq!( + fees.get(&Mob::ID.to_string()).unwrap().as_str().unwrap(), + &Mob::MINIMUM_FEE.to_string() + ); + } + + #[test_with_logger] + fn test_get_txo_block_index(logger: Logger) { + let mut rng: StdRng = SeedableRng::from_seed([20u8; 32]); + let (client, mut ledger_db, db_ctx, network_state) = setup(&mut rng, logger.clone()); + let wallet_db = db_ctx.get_db_instance(logger.clone()); + let account_key = random_account_with_seed_values( + &wallet_db, + &mut ledger_db, + &vec![70 * MOB], + &mut rng, + &logger, + ); + + let (_, tx_out, _) = create_test_received_txo( + &account_key, + 0, + Amount::new(70, Mob::ID), + 13, + &mut rng, + &wallet_db, + ); + + add_block_with_tx_outs( + &mut ledger_db, + &[tx_out.clone()], + &[KeyImage::from(rng.next_u64())], + &mut rng, + ); + wait_for_sync(&client, &ledger_db, &network_state, &logger); + + // A valid public key on the ledger + let public_key = hex::encode(tx_out.public_key.as_bytes()); + + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "get_txo_block_index", + "params": { + "public_key": public_key + } + }); + let res = dispatch(&client, body, &logger); + let result = res.get("result").unwrap(); + assert_eq!(result.get("block_index").unwrap(), "13"); + + // An invalid public key on the ledger + let target_key = hex::encode(tx_out.target_key.as_bytes()); + + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "get_txo_block_index", + "params": { + "public_key": target_key + } + }); + let res = dispatch(&client, body, &logger); + let error = res.get("error").unwrap(); + assert_eq!( + error.get("data").unwrap().get("server_error").unwrap(), + "LedgerDB(NotFound)" + ); + } + + #[test_with_logger] + fn test_no_wallet_db(logger: Logger) { + let mut rng: StdRng = SeedableRng::from_seed([20u8; 32]); + let (client, _ledger_db, _db_ctx, _network_state) = + setup_no_wallet_db(&mut rng, logger.clone()); + + // Because we are not using a ledger_db, this should return an error! + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "get_accounts", + "params": {}, + }); + let res = dispatch(&client, body, &logger); + let error = res.get("error").unwrap(); + let data = error.get("data").unwrap(); + assert_eq!( + data.get("server_error").unwrap(), + "Database(WalletFunctionsDisabled)" + ); + + // This should work just fine since it doesn't interact with the wallet_db + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "get_network_status" + }); + let res = dispatch(&client, body, &logger); + + // Check that we got a result! (We don't really care what it is, just that it's + // working) + let _ = res.get("result").unwrap(); + } +} diff --git a/full-service/src/json_rpc/v2/e2e_tests/transaction/build_submit/build_and_submit.rs b/full-service/src/json_rpc/v2/e2e_tests/transaction/build_submit/build_and_submit.rs new file mode 100644 index 000000000..7d995d73f --- /dev/null +++ b/full-service/src/json_rpc/v2/e2e_tests/transaction/build_submit/build_and_submit.rs @@ -0,0 +1,381 @@ +// Copyright (c) 2020-2022 MobileCoin Inc. + +//! End-to-end tests for the Full Service Wallet API. + +#[cfg(test)] +mod e2e_transaction { + use crate::{ + db::account::AccountID, + json_rpc::v2::{ + api::test_utils::{dispatch, setup}, + models::{amount::Amount as AmountJSON, tx_proposal::TxProposal as TxProposalJSON}, + }, + service::models::tx_proposal::TxProposal, + test_utils::{ + add_block_to_ledger_db, add_block_with_tx, add_block_with_tx_outs, + manually_sync_account, + }, + util::b58::b58_decode_public_address, + }; + + use mc_blockchain_types::BlockVersion; + use mc_common::logger::{test_with_logger, Logger}; + use mc_crypto_keys::RistrettoPrivate; + use mc_crypto_rand::rand_core::RngCore; + use mc_ledger_db::Ledger; + use mc_transaction_core::{ + ring_signature::KeyImage, tokens::Mob, tx::TxOut, Amount, Token, TokenId, + }; + use mc_util_from_random::FromRandom; + use rand::{rngs::StdRng, SeedableRng}; + + use std::convert::TryFrom; + + #[test_with_logger] + fn test_build_and_submit_transaction(logger: Logger) { + let mut rng: StdRng = SeedableRng::from_seed([20u8; 32]); + let (client, mut ledger_db, db_ctx, _network_state) = setup(&mut rng, logger.clone()); + + // Add an account + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "create_account", + "params": { + "name": "Alice Main Account", + } + }); + let res = dispatch(&client, body, &logger); + let result = res.get("result").unwrap(); + let account_obj = result.get("account").unwrap(); + let account_id = account_obj.get("id").unwrap().as_str().unwrap(); + let b58_public_address = account_obj.get("main_address").unwrap().as_str().unwrap(); + let public_address = b58_decode_public_address(b58_public_address).unwrap(); + + // Add a block with a txo for this address (note that value is smaller than + // MINIMUM_FEE, so it is a "dust" TxOut that should get opportunistically swept + // up when we construct the transaction) + add_block_to_ledger_db( + &mut ledger_db, + &vec![public_address.clone()], + 100, + &vec![KeyImage::from(rng.next_u64())], + &mut rng, + ); + + manually_sync_account( + &ledger_db, + &db_ctx.get_db_instance(logger.clone()), + &AccountID(account_id.to_string()), + &logger, + ); + assert_eq!(ledger_db.num_blocks().unwrap(), 13); + + // Add a block with significantly more MOB + add_block_to_ledger_db( + &mut ledger_db, + &vec![public_address.clone()], + 100_000_000_000_000, // 100.0 MOB + &vec![KeyImage::from(rng.next_u64())], + &mut rng, + ); + + manually_sync_account( + &ledger_db, + &db_ctx.get_db_instance(logger.clone()), + &AccountID(account_id.to_string()), + &logger, + ); + assert_eq!(ledger_db.num_blocks().unwrap(), 14); + + // Get balance after submission + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "get_account_status", + "params": { + "account_id": account_id, + } + }); + let res = dispatch(&client, body, &logger); + let result = res.get("result").unwrap(); + let balance_per_token = result.get("balance_per_token").unwrap(); + let balance_mob = balance_per_token.get(Mob::ID.to_string()).unwrap(); + let unspent = balance_mob["unspent"].as_str().unwrap(); + let pending = balance_mob["pending"].as_str().unwrap(); + let spent = balance_mob["spent"].as_str().unwrap(); + let secreted = balance_mob["secreted"].as_str().unwrap(); + let orphaned = balance_mob["orphaned"].as_str().unwrap(); + assert_eq!(unspent, "100000000000100"); + assert_eq!(pending, "0"); + assert_eq!(spent, "0"); + assert_eq!(secreted, "0"); + assert_eq!(orphaned, "0"); + + // Create a tx proposal to ourselves + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "build_and_submit_transaction", + "params": { + "account_id": account_id, + "recipient_public_address": b58_public_address, + "amount": { "value": "42000000000000", "token_id": "0" }, // 42.0 MOB + } + }); + let res = dispatch(&client, body, &logger); + let result = res.get("result").unwrap(); + let tx_proposal: TxProposalJSON = + serde_json::from_value(result.get("tx_proposal").unwrap().clone()).unwrap(); + + assert_eq!( + tx_proposal.fee_amount, + AmountJSON::new(Mob::MINIMUM_FEE, Mob::ID) + ); + + // Transaction builder attempts to use as many inputs as we have txos + assert_eq!(tx_proposal.input_txos.len(), 2); + + // One destination + assert_eq!(tx_proposal.payload_txos.len(), 1); + + assert_eq!(tx_proposal.change_txos.len(), 1); + + // Tombstone block = ledger height (12 to start + 2 new blocks + 10 default + // tombstone) + assert_eq!(tx_proposal.tombstone_block_index, "24"); + + let payments_tx_proposal = TxProposal::try_from(&tx_proposal).unwrap(); + + add_block_with_tx(&mut ledger_db, payments_tx_proposal.tx, &mut rng); + manually_sync_account( + &ledger_db, + &db_ctx.get_db_instance(logger.clone()), + &AccountID(account_id.to_string()), + &logger, + ); + assert_eq!(ledger_db.num_blocks().unwrap(), 15); + + // Get balance after submission, since we are sending it to ourselves, the + // unspent balance should be the original balance - the fee + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "get_account_status", + "params": { + "account_id": account_id, + } + }); + let res = dispatch(&client, body, &logger); + let result = res.get("result").unwrap(); + let balance_per_token = result.get("balance_per_token").unwrap(); + let balance_mob = balance_per_token.get(Mob::ID.to_string()).unwrap(); + let unspent = balance_mob["unspent"].as_str().unwrap(); + let pending = balance_mob["pending"].as_str().unwrap(); + let spent = balance_mob["spent"].as_str().unwrap(); + let secreted = balance_mob["secreted"].as_str().unwrap(); + let orphaned = balance_mob["orphaned"].as_str().unwrap(); + assert_eq!(unspent, &(100000000000100 - Mob::MINIMUM_FEE).to_string()); + assert_eq!(pending, "0"); + assert_eq!(spent, "100000000000100"); + assert_eq!(secreted, "0"); + assert_eq!(orphaned, "0"); + + let txo = TxOut::new( + BlockVersion::MAX, + Amount::new(1000000000000, TokenId::from(1)), + &public_address, + &RistrettoPrivate::from_random(&mut rng), + Default::default(), + ) + .unwrap(); + + add_block_with_tx_outs( + &mut ledger_db, + &[txo], + &vec![KeyImage::from(rng.next_u64())], + &mut rng, + ); + + manually_sync_account( + &ledger_db, + &db_ctx.get_db_instance(logger.clone()), + &AccountID(account_id.to_string()), + &logger, + ); + + // Get balance after submission + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "get_account_status", + "params": { + "account_id": account_id, + } + }); + let res = dispatch(&client, body, &logger); + let result = res.get("result").unwrap(); + let balance_per_token = result.get("balance_per_token").unwrap(); + let balance_mob = balance_per_token.get(Mob::ID.to_string()).unwrap(); + let unspent = balance_mob["unspent"].as_str().unwrap(); + let pending = balance_mob["pending"].as_str().unwrap(); + let spent = balance_mob["spent"].as_str().unwrap(); + let secreted = balance_mob["secreted"].as_str().unwrap(); + let orphaned = balance_mob["orphaned"].as_str().unwrap(); + assert_eq!(unspent, &(100000000000100 - Mob::MINIMUM_FEE).to_string()); + assert_eq!(pending, "0"); + assert_eq!(spent, "100000000000100"); + assert_eq!(secreted, "0"); + assert_eq!(orphaned, "0"); + + let balance_1 = balance_per_token.get("1").unwrap(); + let unspent = balance_1["unspent"].as_str().unwrap(); + let pending = balance_1["pending"].as_str().unwrap(); + let spent = balance_1["spent"].as_str().unwrap(); + let secreted = balance_1["secreted"].as_str().unwrap(); + let orphaned = balance_1["orphaned"].as_str().unwrap(); + assert_eq!(unspent, "1000000000000".to_string()); + assert_eq!(pending, "0"); + assert_eq!(spent, "0"); + assert_eq!(secreted, "0"); + assert_eq!(orphaned, "0"); + + // Create a tx proposal to ourselves, but this should fail because we cannot yet + // do mixed token transactions + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "build_and_submit_transaction", + "params": { + "account_id": account_id, + "recipient_public_address": b58_public_address, + "amount": { "value": "500000000000", "token_id": "1" }, + "fee_token_id": "0", + } + }); + let res = dispatch(&client, body, &logger); + let err = res.get("error"); + assert!(err.is_some()); + + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "build_and_submit_transaction", + "params": { + "account_id": account_id, + "recipient_public_address": b58_public_address, + "amount": { "value": "500000000000", "token_id": "1" } + } + }); + let res = dispatch(&client, body, &logger); + let result = res.get("result").unwrap(); + let tx_proposal: TxProposalJSON = + serde_json::from_value(result.get("tx_proposal").unwrap().clone()).unwrap(); + + // 1024 is the known minimum fee for token id 1, it just isn't in the mobilecoin + // library anywhere yet as a const + assert_eq!( + tx_proposal.fee_amount, + AmountJSON::new(1024, TokenId::from(1)) + ); + + assert_eq!(tx_proposal.input_txos.len(), 1); + + // One destination + assert_eq!(tx_proposal.payload_txos.len(), 1); + + assert_eq!(tx_proposal.change_txos.len(), 1); + + // Tombstone block = ledger height (14 to start + 2 new blocks + 10 default + // tombstone) + assert_eq!(tx_proposal.tombstone_block_index, "26"); + + let payments_tx_proposal = TxProposal::try_from(&tx_proposal).unwrap(); + + add_block_with_tx(&mut ledger_db, payments_tx_proposal.tx, &mut rng); + + // Get balance after submission + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "get_account_status", + "params": { + "account_id": account_id, + } + }); + let res = dispatch(&client, body, &logger); + let result = res.get("result").unwrap(); + let balance_per_token = result.get("balance_per_token").unwrap(); + + // Balance of MOB should be unchanged + let balance_mob = balance_per_token.get(Mob::ID.to_string()).unwrap(); + let unspent = balance_mob["unspent"].as_str().unwrap(); + let pending = balance_mob["pending"].as_str().unwrap(); + let spent = balance_mob["spent"].as_str().unwrap(); + let secreted = balance_mob["secreted"].as_str().unwrap(); + let orphaned = balance_mob["orphaned"].as_str().unwrap(); + assert_eq!(unspent, &(100000000000100 - Mob::MINIMUM_FEE).to_string()); + assert_eq!(pending, "0"); + assert_eq!(spent, "100000000000100"); + assert_eq!(secreted, "0"); + assert_eq!(orphaned, "0"); + + // There should be a pending balance for this token now + let balance_1 = balance_per_token.get("1").unwrap(); + let unspent = balance_1["unspent"].as_str().unwrap(); + let pending = balance_1["pending"].as_str().unwrap(); + let spent = balance_1["spent"].as_str().unwrap(); + let secreted = balance_1["secreted"].as_str().unwrap(); + let orphaned = balance_1["orphaned"].as_str().unwrap(); + assert_eq!(unspent, "0"); + assert_eq!(pending, "1000000000000"); + assert_eq!(spent, "0"); + assert_eq!(secreted, "0"); + assert_eq!(orphaned, "0"); + + manually_sync_account( + &ledger_db, + &db_ctx.get_db_instance(logger.clone()), + &AccountID(account_id.to_string()), + &logger, + ); + + // Get balance after submission + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "get_account_status", + "params": { + "account_id": account_id, + } + }); + let res = dispatch(&client, body, &logger); + let result = res.get("result").unwrap(); + let balance_per_token = result.get("balance_per_token").unwrap(); + + // Balance of MOB should be unchanged + let balance_mob = balance_per_token.get(Mob::ID.to_string()).unwrap(); + let unspent = balance_mob["unspent"].as_str().unwrap(); + let pending = balance_mob["pending"].as_str().unwrap(); + let spent = balance_mob["spent"].as_str().unwrap(); + let secreted = balance_mob["secreted"].as_str().unwrap(); + let orphaned = balance_mob["orphaned"].as_str().unwrap(); + assert_eq!(unspent, &(100000000000100 - Mob::MINIMUM_FEE).to_string()); + assert_eq!(pending, "0"); + assert_eq!(spent, "100000000000100"); + assert_eq!(secreted, "0"); + assert_eq!(orphaned, "0"); + + let balance_1 = balance_per_token.get("1").unwrap(); + let unspent = balance_1["unspent"].as_str().unwrap(); + let pending = balance_1["pending"].as_str().unwrap(); + let spent = balance_1["spent"].as_str().unwrap(); + let secreted = balance_1["secreted"].as_str().unwrap(); + let orphaned = balance_1["orphaned"].as_str().unwrap(); + assert_eq!(unspent, "999999998976".to_string()); + assert_eq!(pending, "0"); + assert_eq!(spent, "1000000000000"); + assert_eq!(secreted, "0"); + assert_eq!(orphaned, "0"); + } +} diff --git a/full-service/src/json_rpc/v2/e2e_tests/transaction/build_submit/build_then_submit.rs b/full-service/src/json_rpc/v2/e2e_tests/transaction/build_submit/build_then_submit.rs new file mode 100644 index 000000000..40de9655e --- /dev/null +++ b/full-service/src/json_rpc/v2/e2e_tests/transaction/build_submit/build_then_submit.rs @@ -0,0 +1,715 @@ +// Copyright (c) 2020-2022 MobileCoin Inc. + +//! End-to-end tests for the Full Service Wallet API. + +#[cfg(test)] +mod e2e_transaction { + use crate::{ + db::{account::AccountID, transaction_log::TxStatus}, + json_rpc::v2::{ + api::test_utils::{dispatch, dispatch_expect_error, setup}, + models::{ + amount::Amount as AmountJSON, transaction_log::TransactionLog, + tx_proposal::TxProposal as TxProposalJSON, + }, + }, + service::models::tx_proposal::TxProposal, + test_utils::{ + add_block_to_ledger_db, add_block_with_tx, add_block_with_tx_outs, + manually_sync_account, + }, + util::b58::b58_decode_public_address, + }; + + use mc_blockchain_types::BlockVersion; + use mc_common::logger::{test_with_logger, Logger}; + use mc_crypto_keys::RistrettoPrivate; + use mc_crypto_rand::rand_core::RngCore; + use mc_ledger_db::Ledger; + use mc_transaction_core::{ + ring_signature::KeyImage, tokens::Mob, tx::TxOut, Amount, Token, TokenId, + }; + use mc_util_from_random::FromRandom; + use rand::{rngs::StdRng, SeedableRng}; + + use std::convert::TryFrom; + + #[test_with_logger] + fn test_build_then_submit_transaction(logger: Logger) { + let mut rng: StdRng = SeedableRng::from_seed([20u8; 32]); + let (client, mut ledger_db, db_ctx, _network_state) = setup(&mut rng, logger.clone()); + + // Add an account + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "create_account", + "params": { + "name": "Alice Main Account", + } + }); + let res = dispatch(&client, body, &logger); + let result = res.get("result").unwrap(); + let account_obj = result.get("account").unwrap(); + let account_id = account_obj.get("id").unwrap().as_str().unwrap(); + let b58_public_address = account_obj.get("main_address").unwrap().as_str().unwrap(); + let public_address = b58_decode_public_address(b58_public_address).unwrap(); + + // Add a block with a txo for this address (note that value is smaller than + // MINIMUM_FEE, so it is a "dust" TxOut that should get opportunistically swept + // up when we construct the transaction) + add_block_to_ledger_db( + &mut ledger_db, + &vec![public_address.clone()], + 100, + &vec![KeyImage::from(rng.next_u64())], + &mut rng, + ); + + manually_sync_account( + &ledger_db, + &db_ctx.get_db_instance(logger.clone()), + &AccountID(account_id.to_string()), + &logger, + ); + assert_eq!(ledger_db.num_blocks().unwrap(), 13); + + // Create a tx proposal to ourselves + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "build_transaction", + "params": { + "account_id": account_id, + "recipient_public_address": b58_public_address, + "amount": { "value": "42", "token_id": "0"}, + } + }); + // We will fail because we cannot afford the fee + dispatch_expect_error( + &client, + body, + &logger, + json!({ + "method": "build_transaction", + "error": json!({ + "code": -32603, + "message": "InternalError", + "data": json!({ + "server_error": format!("TransactionBuilder(WalletDb(InsufficientFundsUnderMaxSpendable(\"Max spendable value in wallet: 0, but target value: {}\")))", 42 + Mob::MINIMUM_FEE), + "details": format!("Error building transaction: Wallet DB Error: Insufficient funds from Txos under max_spendable_value: Max spendable value in wallet: 0, but target value: {}", 42 + Mob::MINIMUM_FEE), + }) + }), + "jsonrpc": "2.0", + "id": 1, + }).to_string(), + ); + + // Add a block with significantly more MOB + add_block_to_ledger_db( + &mut ledger_db, + &vec![public_address.clone()], + 100000000000000, // 100.0 MOB + &vec![KeyImage::from(rng.next_u64())], + &mut rng, + ); + + manually_sync_account( + &ledger_db, + &db_ctx.get_db_instance(logger.clone()), + &AccountID(account_id.to_string()), + &logger, + ); + assert_eq!(ledger_db.num_blocks().unwrap(), 14); + + // Create a tx proposal to ourselves + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "build_transaction", + "params": { + "account_id": account_id, + "recipient_public_address": b58_public_address, + "amount": { "value": "42000000000000", "token_id": "0"}, // 42.0 MOB + } + }); + let res = dispatch(&client, body, &logger); + let result = res.get("result").unwrap(); + let tx_proposal: TxProposalJSON = + serde_json::from_value(result.get("tx_proposal").unwrap().clone()).unwrap(); + + assert_eq!( + tx_proposal.fee_amount, + AmountJSON::new(Mob::MINIMUM_FEE, Mob::ID) + ); + + // Transaction builder attempts to use as many inputs as we have txos + assert_eq!(tx_proposal.input_txos.len(), 2); + + // One payload txo + assert_eq!(tx_proposal.payload_txos.len(), 1); + + assert_eq!(tx_proposal.change_txos.len(), 1); + + // Tombstone block = ledger height (12 to start + 2 new blocks + 10 default + // tombstone) + assert_eq!(tx_proposal.tombstone_block_index, "24"); + + // Get current balance + assert_eq!(ledger_db.num_blocks().unwrap(), 14); + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "get_account_status", + "params": { + "account_id": account_id, + } + }); + let res = dispatch(&client, body, &logger); + let result = res.get("result").unwrap(); + let balance_per_token = result.get("balance_per_token").unwrap(); + let balance_mob = balance_per_token.get(Mob::ID.to_string()).unwrap(); + let unspent = balance_mob["unspent"].as_str().unwrap(); + assert_eq!(unspent, "100000000000100"); + + // Submit the tx_proposal + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "submit_transaction", + "params": { + "tx_proposal": tx_proposal, + "account_id": account_id, + } + }); + let res = dispatch(&client, body, &logger); + let result = res.get("result").unwrap(); + let transaction_id = result + .get("transaction_log") + .unwrap() + .get("id") + .unwrap() + .as_str() + .unwrap(); + // Note - we cannot test here that the transaction ID is consistent, because + // there is randomness in the transaction creation. + + let payments_tx_proposal = TxProposal::try_from(&tx_proposal).unwrap(); + + // The MockBlockchainConnection does not write to the ledger_db + add_block_with_tx(&mut ledger_db, payments_tx_proposal.tx, &mut rng); + manually_sync_account( + &ledger_db, + &db_ctx.get_db_instance(logger.clone()), + &AccountID(account_id.to_string()), + &logger, + ); + assert_eq!(ledger_db.num_blocks().unwrap(), 15); + + // Get balance after submission + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "get_account_status", + "params": { + "account_id": account_id, + } + }); + let res = dispatch(&client, body, &logger); + let result = res.get("result").unwrap(); + let balance_per_token = result.get("balance_per_token").unwrap(); + let balance_mob = balance_per_token.get(Mob::ID.to_string()).unwrap(); + let unspent = balance_mob["unspent"].as_str().unwrap(); + let pending = balance_mob["pending"].as_str().unwrap(); + let spent = balance_mob["spent"].as_str().unwrap(); + let secreted = balance_mob["secreted"].as_str().unwrap(); + let orphaned = balance_mob["orphaned"].as_str().unwrap(); + assert_eq!(unspent, "99999600000100"); + assert_eq!(pending, "0"); + assert_eq!(spent, "100000000000100"); + assert_eq!(secreted, "0"); + assert_eq!(orphaned, "0"); + + // Get the transaction_id and verify it contains what we expect + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "get_transaction_log", + "params": { + "transaction_log_id": transaction_id, + } + }); + let res = dispatch(&client, body, &logger); + let result = res.get("result").unwrap(); + let transaction_log: TransactionLog = + serde_json::from_value(result.get("transaction_log").unwrap().clone()).unwrap(); + + let pmob_value = transaction_log.value_map.get(&Mob::ID.to_string()).unwrap(); + assert_eq!(pmob_value, "42000000000000"); + + assert_eq!( + transaction_log.output_txos[0].recipient_public_address_b58, + b58_public_address + ); + + assert_eq!( + transaction_log.fee_amount, + AmountJSON::new(Mob::MINIMUM_FEE, Mob::ID) + ); + + assert_eq!(transaction_log.status, TxStatus::Succeeded.to_string()); + assert_eq!(transaction_log.submitted_block_index.unwrap(), "14"); + assert_eq!(transaction_log.id, transaction_id); + + // Get All Transaction Logs + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "get_transaction_logs", + "params": { + "account_id": account_id, + } + }); + let res = dispatch(&client, body, &logger); + let result = res.get("result").unwrap(); + let transaction_log_ids = result + .get("transaction_log_ids") + .unwrap() + .as_array() + .unwrap(); + // We have a transaction log for the sent + assert_eq!(transaction_log_ids.len(), 1); + + // Check the contents of the transaction log associated txos + let transaction_log_map = result.get("transaction_log_map").unwrap(); + let transaction_log = transaction_log_map.get(transaction_id).unwrap(); + assert_eq!( + transaction_log + .get("output_txos") + .unwrap() + .as_array() + .unwrap() + .len(), + 1 + ); + assert_eq!( + transaction_log + .get("input_txos") + .unwrap() + .as_array() + .unwrap() + .len(), + 2 + ); + assert_eq!( + transaction_log + .get("change_txos") + .unwrap() + .as_array() + .unwrap() + .len(), + 1 + ); + + assert_eq!( + transaction_log.get("status").unwrap().as_str().unwrap(), + "succeeded" + ); + + // Get all Transaction Logs for a given Block + + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "get_transaction_logs", + "params": { + "min_block_index": "14", + "max_block_index": "14", + } + }); + let res = dispatch(&client, body, &logger); + let result = res.get("result").unwrap(); + let transaction_log_map = result + .get("transaction_log_map") + .unwrap() + .as_object() + .unwrap(); + assert_eq!(transaction_log_map.len(), 1); + + let txo = TxOut::new( + BlockVersion::MAX, + Amount::new(1000000000000, TokenId::from(1)), + &public_address, + &RistrettoPrivate::from_random(&mut rng), + Default::default(), + ) + .unwrap(); + + add_block_with_tx_outs( + &mut ledger_db, + &[txo], + &vec![KeyImage::from(rng.next_u64())], + &mut rng, + ); + + manually_sync_account( + &ledger_db, + &db_ctx.get_db_instance(logger.clone()), + &AccountID(account_id.to_string()), + &logger, + ); + + // Create a tx proposal to ourselves, but this should fail because we cannot yet + // do mixed token transactions + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "build_transaction", + "params": { + "account_id": account_id, + "recipient_public_address": b58_public_address, + "amount": { "value": "500000000000", "token_id": "1" }, + "fee_token_id": "0", + } + }); + let res = dispatch(&client, body, &logger); + let err = res.get("error"); + assert!(err.is_some()); + + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "build_transaction", + "params": { + "account_id": account_id, + "recipient_public_address": b58_public_address, + "amount": { "value": "500000000000", "token_id": "1" } + } + }); + let res = dispatch(&client, body, &logger); + let result = res.get("result").unwrap(); + let tx_proposal: TxProposalJSON = + serde_json::from_value(result.get("tx_proposal").unwrap().clone()).unwrap(); + + assert_eq!( + tx_proposal.fee_amount.token_id, + TokenId::from(1).to_string() + ); + + assert_eq!(tx_proposal.input_txos.len(), 1); + + assert_eq!(tx_proposal.payload_txos.len(), 1); + + assert_eq!(tx_proposal.change_txos.len(), 1); + + // Tombstone block = ledger height (12 to start + 4 new blocks + 10 default + // tombstone) + assert_eq!(tx_proposal.tombstone_block_index, "26"); + + // Get current balance + assert_eq!(ledger_db.num_blocks().unwrap(), 16); + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "get_account_status", + "params": { + "account_id": account_id, + } + }); + let res = dispatch(&client, body, &logger); + let result = res.get("result").unwrap(); + let balance_per_token = result.get("balance_per_token").unwrap(); + let balance_1 = balance_per_token.get("1").unwrap(); + let unspent = balance_1["unspent"].as_str().unwrap(); + assert_eq!(unspent, "1000000000000"); + + // Submit the tx_proposal + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "submit_transaction", + "params": { + "tx_proposal": tx_proposal, + "account_id": account_id, + } + }); + let res = dispatch(&client, body, &logger); + let _result = res.get("result").unwrap(); + let payments_tx_proposal = TxProposal::try_from(&tx_proposal).unwrap(); + + // Get balance after submission + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "get_account_status", + "params": { + "account_id": account_id, + } + }); + let res = dispatch(&client, body, &logger); + let result = res.get("result").unwrap(); + let balance_per_token = result.get("balance_per_token").unwrap(); + + // Balance of MOB should be unchanged + let balance_mob = balance_per_token.get(Mob::ID.to_string()).unwrap(); + let unspent = balance_mob["unspent"].as_str().unwrap(); + let pending = balance_mob["pending"].as_str().unwrap(); + let spent = balance_mob["spent"].as_str().unwrap(); + let secreted = balance_mob["secreted"].as_str().unwrap(); + let orphaned = balance_mob["orphaned"].as_str().unwrap(); + assert_eq!(unspent, &(100000000000100 - Mob::MINIMUM_FEE).to_string()); + assert_eq!(pending, "0"); + assert_eq!(spent, "100000000000100"); + assert_eq!(secreted, "0"); + assert_eq!(orphaned, "0"); + + // There should be a pending balance for this token now + let balance_1 = balance_per_token.get("1").unwrap(); + let unspent = balance_1["unspent"].as_str().unwrap(); + let pending = balance_1["pending"].as_str().unwrap(); + let spent = balance_1["spent"].as_str().unwrap(); + let secreted = balance_1["secreted"].as_str().unwrap(); + let orphaned = balance_1["orphaned"].as_str().unwrap(); + assert_eq!(unspent, "0"); + assert_eq!(pending, "1000000000000"); + assert_eq!(spent, "0"); + assert_eq!(secreted, "0"); + assert_eq!(orphaned, "0"); + + // The MockBlockchainConnection does not write to the ledger_db + add_block_with_tx(&mut ledger_db, payments_tx_proposal.tx, &mut rng); + + manually_sync_account( + &ledger_db, + &db_ctx.get_db_instance(logger.clone()), + &AccountID(account_id.to_string()), + &logger, + ); + assert_eq!(ledger_db.num_blocks().unwrap(), 17); + + // Get balance after submission + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "get_account_status", + "params": { + "account_id": account_id, + } + }); + let res = dispatch(&client, body, &logger); + let result = res.get("result").unwrap(); + let balance_per_token = result.get("balance_per_token").unwrap(); + + // Balance of MOB should be unchanged + let balance_mob = balance_per_token.get(Mob::ID.to_string()).unwrap(); + let unspent = balance_mob["unspent"].as_str().unwrap(); + let pending = balance_mob["pending"].as_str().unwrap(); + let spent = balance_mob["spent"].as_str().unwrap(); + let secreted = balance_mob["secreted"].as_str().unwrap(); + let orphaned = balance_mob["orphaned"].as_str().unwrap(); + assert_eq!(unspent, &(100000000000100 - Mob::MINIMUM_FEE).to_string()); + assert_eq!(pending, "0"); + assert_eq!(spent, "100000000000100"); + assert_eq!(secreted, "0"); + assert_eq!(orphaned, "0"); + + let balance_1 = balance_per_token.get("1").unwrap(); + let unspent = balance_1["unspent"].as_str().unwrap(); + let pending = balance_1["pending"].as_str().unwrap(); + let spent = balance_1["spent"].as_str().unwrap(); + let secreted = balance_1["secreted"].as_str().unwrap(); + let orphaned = balance_1["orphaned"].as_str().unwrap(); + assert_eq!(unspent, "999999998976".to_string()); + assert_eq!(pending, "0"); + assert_eq!(spent, "1000000000000"); + assert_eq!(secreted, "0"); + assert_eq!(orphaned, "0"); + } + + #[test_with_logger] + fn test_build_then_submit_transaction_multiple_accounts(logger: Logger) { + let mut rng: StdRng = SeedableRng::from_seed([20u8; 32]); + let (client, mut ledger_db, db_ctx, _network_state) = setup(&mut rng, logger.clone()); + + // Add an account + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "create_account", + "params": { + "name": "Alice Main Account", + } + }); + let res = dispatch(&client, body, &logger); + let result = res.get("result").unwrap(); + let account_obj = result.get("account").unwrap(); + let alice_account_id = account_obj.get("id").unwrap().as_str().unwrap(); + let alice_b58_public_address = account_obj.get("main_address").unwrap().as_str().unwrap(); + let alice_public_address = b58_decode_public_address(alice_b58_public_address).unwrap(); + + // Add a second account + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "create_account", + "params": { + "name": "Bob Main Account", + } + }); + let res = dispatch(&client, body, &logger); + let result = res.get("result").unwrap(); + let account_obj = result.get("account").unwrap(); + let bob_account_id = account_obj.get("id").unwrap().as_str().unwrap(); + let bob_b58_public_address = account_obj.get("main_address").unwrap().as_str().unwrap(); + + // Add a block with a txo for this address (note that value is smaller than + // MINIMUM_FEE, so it is a "dust" TxOut that should get opportunistically swept + // up when we construct the transaction) + add_block_to_ledger_db( + &mut ledger_db, + &vec![ + alice_public_address.clone(), + alice_public_address.clone(), + alice_public_address.clone(), + alice_public_address.clone(), + alice_public_address.clone(), + ], + 100000000000000, + &vec![KeyImage::from(rng.next_u64())], + &mut rng, + ); + + manually_sync_account( + &ledger_db, + &db_ctx.get_db_instance(logger.clone()), + &AccountID(alice_account_id.to_string()), + &logger, + ); + + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "build_transaction", + "params": { + "account_id": alice_account_id, + "recipient_public_address": bob_b58_public_address, + "amount": { "value": "42000000000000", "token_id": "0"}, // 42.0 MOB + } + }); + let res = dispatch(&client, body, &logger); + let result = res.get("result").unwrap(); + let tx_proposal: TxProposalJSON = + serde_json::from_value(result.get("tx_proposal").unwrap().clone()).unwrap(); + + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "get_account_status", + "params": { + "account_id": alice_account_id, + } + }); + let res = dispatch(&client, body, &logger); + let result = res.get("result").unwrap(); + let balance_per_token = result.get("balance_per_token").unwrap(); + let balance_mob = balance_per_token.get(Mob::ID.to_string()).unwrap(); + let unspent = balance_mob["unspent"].as_str().unwrap(); + assert_eq!(unspent, "500000000000000"); + + // Submit the tx_proposal + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "submit_transaction", + "params": { + "tx_proposal": tx_proposal, + "account_id": alice_account_id, + } + }); + let _res = dispatch(&client, body, &logger); + + let payments_tx_proposal = TxProposal::try_from(&tx_proposal).unwrap(); + + // The MockBlockchainConnection does not write to the ledger_db + add_block_with_tx(&mut ledger_db, payments_tx_proposal.tx, &mut rng); + manually_sync_account( + &ledger_db, + &db_ctx.get_db_instance(logger.clone()), + &AccountID(alice_account_id.to_string()), + &logger, + ); + manually_sync_account( + &ledger_db, + &db_ctx.get_db_instance(logger.clone()), + &AccountID(bob_account_id.to_string()), + &logger, + ); + + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "build_transaction", + "params": { + "account_id": bob_account_id, + "recipient_public_address": bob_b58_public_address, + "amount": { "value": "10000000000000", "token_id": "0"}, // 42.0 MOB + } + }); + let _res = dispatch(&client, body, &logger); + + // Get balance after submission + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "get_account_status", + "params": { + "account_id": alice_account_id, + } + }); + let res = dispatch(&client, body, &logger); + let result = res.get("result").unwrap(); + let balance_per_token = result.get("balance_per_token").unwrap(); + let balance_mob = balance_per_token.get(Mob::ID.to_string()).unwrap(); + let max_spendable = balance_mob["max_spendable"].as_str().unwrap(); + let unspent = balance_mob["unspent"].as_str().unwrap(); + let pending = balance_mob["pending"].as_str().unwrap(); + let spent = balance_mob["spent"].as_str().unwrap(); + let secreted = balance_mob["secreted"].as_str().unwrap(); + let orphaned = balance_mob["orphaned"].as_str().unwrap(); + assert_eq!(max_spendable, "457999200000000"); + assert_eq!(unspent, "457999600000000"); + assert_eq!(pending, "0"); + assert_eq!(spent, "100000000000000"); + assert_eq!(secreted, "0"); + assert_eq!(orphaned, "0"); + + // Get balance after submission + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "get_account_status", + "params": { + "account_id": bob_account_id, + } + }); + let res = dispatch(&client, body, &logger); + let result = res.get("result").unwrap(); + let balance_per_token = result.get("balance_per_token").unwrap(); + let balance_mob = balance_per_token.get(Mob::ID.to_string()).unwrap(); + let max_spendable = balance_mob["max_spendable"].as_str().unwrap(); + let unspent = balance_mob["unspent"].as_str().unwrap(); + let pending = balance_mob["pending"].as_str().unwrap(); + let spent = balance_mob["spent"].as_str().unwrap(); + let secreted = balance_mob["secreted"].as_str().unwrap(); + let orphaned = balance_mob["orphaned"].as_str().unwrap(); + assert_eq!( + max_spendable, + (42000000000000 - Mob::MINIMUM_FEE).to_string() + ); + assert_eq!(unspent, "42000000000000"); + assert_eq!(pending, "0"); + assert_eq!(spent, "0"); + assert_eq!(secreted, "0"); + assert_eq!(orphaned, "0"); + } +} diff --git a/full-service/src/json_rpc/v2/e2e_tests/transaction/build_submit/build_unsigned.rs b/full-service/src/json_rpc/v2/e2e_tests/transaction/build_submit/build_unsigned.rs new file mode 100644 index 000000000..dc65e372b --- /dev/null +++ b/full-service/src/json_rpc/v2/e2e_tests/transaction/build_submit/build_unsigned.rs @@ -0,0 +1,174 @@ +// Copyright (c) 2020-2022 MobileCoin Inc. + +//! End-to-end tests for the Full Service Wallet API. + +#[cfg(test)] +mod e2e_transaction { + use crate::{ + db::account::AccountID, + json_rpc::v2::{ + api::test_utils::{dispatch, setup}, + models::tx_proposal::UnsignedTxProposal, + }, + test_utils::{add_block_to_ledger_db, manually_sync_account, MOB}, + util::b58::b58_decode_public_address, + }; + + use mc_common::logger::{test_with_logger, Logger}; + use mc_crypto_rand::rand_core::RngCore; + use mc_transaction_core::{ring_signature::KeyImage, tokens::Mob, Token}; + use rand::{rngs::StdRng, SeedableRng}; + + #[test_with_logger] + fn test_build_unsigned_transaction(logger: Logger) { + let mut rng: StdRng = SeedableRng::from_seed([20u8; 32]); + let (client, mut ledger_db, db_ctx, _network_state) = setup(&mut rng, logger.clone()); + let wallet_db = db_ctx.get_db_instance(logger.clone()); + + // Create Account + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "create_account", + "params": { + "name": "Alice Main Account", + }, + }); + let res = dispatch(&client, body, &logger); + assert_eq!(res.get("jsonrpc").unwrap(), "2.0"); + + let result = res.get("result").unwrap(); + let account_obj = result.get("account").unwrap(); + assert!(account_obj.get("id").is_some()); + assert_eq!(account_obj.get("name").unwrap(), "Alice Main Account"); + let account_id = account_obj.get("id").unwrap(); + let main_address = account_obj.get("main_address").unwrap().as_str().unwrap(); + let main_account_address = b58_decode_public_address(main_address).unwrap(); + + // add some funds to that account + add_block_to_ledger_db( + &mut ledger_db, + &vec![main_account_address], + 100 * MOB, + &vec![KeyImage::from(rng.next_u64())], + &mut rng, + ); + manually_sync_account( + &ledger_db, + &db_ctx.get_db_instance(logger.clone()), + &AccountID(account_id.as_str().unwrap().to_string()), + &logger, + ); + + // confirm that the regular account has the correct balance + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "get_account_status", + "params": { + "account_id": account_id, + }, + }); + let res = dispatch(&client, body, &logger); + let result = res.get("result").unwrap(); + let balance_per_token = result.get("balance_per_token").unwrap(); + let balance_mob = balance_per_token.get(Mob::ID.to_string()).unwrap(); + let unspent = balance_mob["unspent"].as_str().unwrap(); + assert_eq!(unspent, "100000000000000"); + + // export view only import package + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "create_view_only_account_import_request", + "params": { + "account_id": account_id, + }, + }); + let res = dispatch(&client, body, &logger); + assert_eq!(res.get("jsonrpc").unwrap(), "2.0"); + let result = res.get("result").unwrap(); + let request = result.get("json_rpc_request").unwrap(); + + let body = json!({ + "jsonrpc": "2.0", + "id": 2, + "method": "remove_account", + "params": { + "account_id": account_id, + } + }); + let res = dispatch(&client, body, &logger); + let result = res.get("result").unwrap(); + assert_eq!(result["removed"].as_bool().unwrap(), true); + + // import vo account + let body = json!(request); + let res = dispatch(&client, body, &logger); + let result = res.get("result").unwrap(); + let account = result.get("account").unwrap(); + let vo_account_id = account.get("id").unwrap(); + assert_eq!(vo_account_id, account_id); + + // sync the view only account + manually_sync_account( + &ledger_db, + &wallet_db, + &AccountID(vo_account_id.as_str().unwrap().to_string()), + &logger, + ); + + // confirm that the view only account has the correct balance + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "get_account_status", + "params": { + "account_id": vo_account_id, + }, + }); + let res = dispatch(&client, body, &logger); + let result = res.get("result").unwrap(); + let balance_per_token = result.get("balance_per_token").unwrap(); + let balance_mob = balance_per_token.get(Mob::ID.to_string()).unwrap(); + let unverified = balance_mob["unverified"].as_str().unwrap(); + let unspent = balance_mob["unspent"].as_str().unwrap(); + assert_eq!(unverified, "100000000000000"); + assert_eq!(unspent, "0"); + + let account = result.get("account").unwrap(); + let vo_account_id = account.get("id").unwrap(); + assert_eq!(vo_account_id, account_id); + + // test creating unsigned tx with recipient public address and amount + let body = json!({ + "jsonrpc": "2.0", + "id": 2, + "method": "build_unsigned_transaction", + "params": { + "account_id": account_id, + "recipient_public_address": main_address, + "amount": { "value": "50000000000000", "token_id": "0"}, + } + }); + let res = dispatch(&client, body, &logger); + let result = res.get("result").unwrap(); + let _: UnsignedTxProposal = + serde_json::from_value(result.get("unsigned_tx_proposal").unwrap().clone()).unwrap(); + + // test creating unsigned tx with addresses_and_amounts + let body = json!({ + "jsonrpc": "2.0", + "id": 2, + "method": "build_unsigned_transaction", + "params": { + "account_id": account_id, + "addresses_and_amounts": [[main_address, { "value": "50000000000000", "token_id": "0"}]] + } + }); + let res = dispatch(&client, body, &logger); + let result = res.get("result").unwrap(); + let _: UnsignedTxProposal = + serde_json::from_value(result.get("unsigned_tx_proposal").unwrap().clone()).unwrap(); + } +} diff --git a/full-service/src/json_rpc/v2/e2e_tests/transaction/build_submit/large_transaction.rs b/full-service/src/json_rpc/v2/e2e_tests/transaction/build_submit/large_transaction.rs new file mode 100644 index 000000000..4569d24b0 --- /dev/null +++ b/full-service/src/json_rpc/v2/e2e_tests/transaction/build_submit/large_transaction.rs @@ -0,0 +1,143 @@ +// Copyright (c) 2020-2022 MobileCoin Inc. + +//! End-to-end tests for the Full Service Wallet API. + +#[cfg(test)] +mod e2e_transaction { + use crate::{ + db::account::AccountID, + json_rpc::v2::{ + api::test_utils::{dispatch, setup}, + models::{ + amount::Amount, transaction_log::TransactionLog, + tx_proposal::TxProposal as TxProposalJSON, + }, + }, + service::models::tx_proposal::TxProposal, + test_utils::{add_block_to_ledger_db, add_block_with_tx, manually_sync_account}, + util::b58::b58_decode_public_address, + }; + + use mc_common::logger::{test_with_logger, Logger}; + use mc_crypto_rand::rand_core::RngCore; + use mc_ledger_db::Ledger; + use mc_transaction_core::{ring_signature::KeyImage, tokens::Mob, Token}; + use rand::{rngs::StdRng, SeedableRng}; + + use std::convert::TryFrom; + + #[test_with_logger] + fn test_large_transaction(logger: Logger) { + let mut rng: StdRng = SeedableRng::from_seed([20u8; 32]); + let (client, mut ledger_db, db_ctx, _network_state) = setup(&mut rng, logger.clone()); + + // Add an account + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "create_account", + "params": { + "name": "Alice Main Account", + } + }); + let res = dispatch(&client, body, &logger); + let result = res.get("result").unwrap(); + let account_obj = result.get("account").unwrap(); + let account_id = account_obj.get("id").unwrap().as_str().unwrap(); + let b58_public_address = account_obj.get("main_address").unwrap().as_str().unwrap(); + let public_address = b58_decode_public_address(b58_public_address).unwrap(); + + // Add a block with a large txo for this address. + add_block_to_ledger_db( + &mut ledger_db, + &vec![public_address.clone()], + 11_000_000_000_000_000_000, // Eleven million MOB. + &vec![KeyImage::from(rng.next_u64())], + &mut rng, + ); + + manually_sync_account( + &ledger_db, + &db_ctx.get_db_instance(logger.clone()), + &AccountID(account_id.to_string()), + &logger, + ); + assert_eq!(ledger_db.num_blocks().unwrap(), 13); + + // Create a tx proposal to ourselves + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "build_and_submit_transaction", + "params": { + "account_id": account_id, + "recipient_public_address": b58_public_address, + "amount": { "value": "10000000000000000000", "token_id": "0"}, // Ten million MOB, which is larger than i64::MAX picomob. + } + }); + let res = dispatch(&client, body, &logger); + let result = res.get("result").unwrap(); + let tx_proposal = result.get("tx_proposal").unwrap(); + + // Check that the value was recorded correctly. + let transaction_log: TransactionLog = + serde_json::from_value(result.get("transaction_log").unwrap().clone()).unwrap(); + let value_pmob = transaction_log.value_map.get(&Mob::ID.to_string()).unwrap(); + assert_eq!(value_pmob, "10000000000000000000"); + + assert_eq!( + transaction_log.input_txos[0].amount, + Amount::new(11_000_000_000_000_000_000u64, Mob::ID), + ); + + assert_eq!( + transaction_log.output_txos[0].amount, + Amount::new(10_000_000_000_000_000_000u64, Mob::ID), + ); + + assert_eq!( + transaction_log.change_txos[0].amount, + Amount::new(1_000_000_000_000_000_000u64 - Mob::MINIMUM_FEE, Mob::ID), + ); + + // Sync the proposal. + let json_tx_proposal: TxProposalJSON = serde_json::from_value(tx_proposal.clone()).unwrap(); + let payments_tx_proposal = TxProposal::try_from(&json_tx_proposal).unwrap(); + + add_block_with_tx(&mut ledger_db, payments_tx_proposal.tx, &mut rng); + manually_sync_account( + &ledger_db, + &db_ctx.get_db_instance(logger.clone()), + &AccountID(account_id.to_string()), + &logger, + ); + assert_eq!(ledger_db.num_blocks().unwrap(), 14); + + // Get balance after submission + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "get_account_status", + "params": { + "account_id": account_id, + } + }); + let res = dispatch(&client, body, &logger); + let result = res.get("result").unwrap(); + let balance_per_token = result.get("balance_per_token").unwrap(); + let balance_mob = balance_per_token.get(Mob::ID.to_string()).unwrap(); + let unspent = balance_mob["unspent"].as_str().unwrap(); + let pending = balance_mob["pending"].as_str().unwrap(); + let spent = balance_mob["spent"].as_str().unwrap(); + let secreted = balance_mob["secreted"].as_str().unwrap(); + let orphaned = balance_mob["orphaned"].as_str().unwrap(); + assert_eq!( + unspent, + &(11_000_000_000_000_000_000u64 - Mob::MINIMUM_FEE).to_string() + ); + assert_eq!(pending, "0"); + assert_eq!(spent, 11_000_000_000_000_000_000u64.to_string()); + assert_eq!(secreted, "0"); + assert_eq!(orphaned, "0"); + } +} diff --git a/full-service/src/json_rpc/v2/e2e_tests/transaction/build_submit/mod.rs b/full-service/src/json_rpc/v2/e2e_tests/transaction/build_submit/mod.rs new file mode 100644 index 000000000..4bb3df8d5 --- /dev/null +++ b/full-service/src/json_rpc/v2/e2e_tests/transaction/build_submit/mod.rs @@ -0,0 +1,5 @@ +mod build_and_submit; +mod build_then_submit; +mod build_unsigned; +mod large_transaction; +mod multiple_outlay; diff --git a/full-service/src/json_rpc/v2/e2e_tests/transaction/build_submit/multiple_outlay.rs b/full-service/src/json_rpc/v2/e2e_tests/transaction/build_submit/multiple_outlay.rs new file mode 100644 index 000000000..4e2db2957 --- /dev/null +++ b/full-service/src/json_rpc/v2/e2e_tests/transaction/build_submit/multiple_outlay.rs @@ -0,0 +1,319 @@ +// Copyright (c) 2020-2022 MobileCoin Inc. + +//! End-to-end tests for the Full Service Wallet API. + +#[cfg(test)] +mod e2e_transaction { + use crate::{ + db::account::AccountID, + json_rpc::v2::{ + api::test_utils::{dispatch, setup}, + models::{amount::Amount, tx_proposal::TxProposal as TxProposalJSON}, + }, + service::models::tx_proposal::TxProposal, + test_utils::{add_block_to_ledger_db, add_block_with_tx, manually_sync_account, MOB}, + util::b58::b58_decode_public_address, + }; + + use mc_common::logger::{test_with_logger, Logger}; + use mc_crypto_rand::rand_core::RngCore; + use mc_ledger_db::Ledger; + use mc_transaction_core::{ring_signature::KeyImage, tokens::Mob, Token}; + use rand::{rngs::StdRng, SeedableRng}; + + use std::convert::TryFrom; + + #[test_with_logger] + fn test_multiple_outlay_transaction(logger: Logger) { + let mut rng: StdRng = SeedableRng::from_seed([20u8; 32]); + let (client, mut ledger_db, db_ctx, _network_state) = setup(&mut rng, logger.clone()); + + // Add some accounts. + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "create_account", + "params": { + "name": "Alice Main Account", + } + }); + let res = dispatch(&client, body, &logger); + let result = res.get("result").unwrap(); + let account_obj = result.get("account").unwrap(); + let alice_account_id = account_obj.get("id").unwrap().as_str().unwrap(); + let b58_public_address = account_obj.get("main_address").unwrap().as_str().unwrap(); + let alice_public_address = b58_decode_public_address(b58_public_address).unwrap(); + + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "create_account", + "params": { + "name": "Bob Main Account", + } + }); + let res = dispatch(&client, body, &logger); + let result = res.get("result").unwrap(); + let account_obj = result.get("account").unwrap(); + let bob_account_id = account_obj.get("id").unwrap().as_str().unwrap(); + let bob_b58_public_address = account_obj.get("main_address").unwrap().as_str().unwrap(); + + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "create_account", + "params": { + "name": "Charlie Main Account", + } + }); + let res = dispatch(&client, body, &logger); + let result = res.get("result").unwrap(); + let account_obj = result.get("account").unwrap(); + let charlie_account_id = account_obj.get("id").unwrap().as_str().unwrap(); + let charlie_b58_public_address = account_obj.get("main_address").unwrap().as_str().unwrap(); + + // Add some money to Alice's account. + add_block_to_ledger_db( + &mut ledger_db, + &vec![alice_public_address], + 100000000000000, // 100.0 MOB + &vec![KeyImage::from(rng.next_u64())], + &mut rng, + ); + + manually_sync_account( + &ledger_db, + &db_ctx.get_db_instance(logger.clone()), + &AccountID(alice_account_id.to_string()), + &logger, + ); + + // Create a two-output tx proposal to Bob and Charlie. + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "build_transaction", + "params": { + "account_id": alice_account_id, + "addresses_and_amounts": [ + [bob_b58_public_address, {"value": "42000000000000", "token_id": "0"}], // 42.0 MOB + [charlie_b58_public_address, {"value": "43000000000000", "token_id": "0"}], // 43.0 MOB + ] + } + }); + let res = dispatch(&client, body, &logger); + let result = res.get("result").unwrap(); + + let tx_proposal = result.get("tx_proposal").unwrap(); + + let fee_amount: Amount = + serde_json::from_value(tx_proposal.get("fee_amount").unwrap().clone()).unwrap(); + + assert_eq!(fee_amount, Amount::new(Mob::MINIMUM_FEE, Mob::ID)); + + // Two destinations. + let payload_txos = tx_proposal.get("payload_txos").unwrap().as_array().unwrap(); + assert_eq!(payload_txos.len(), 2); + + let change_txos = tx_proposal.get("change_txos").unwrap().as_array().unwrap(); + assert_eq!(change_txos.len(), 1); + + // Get balances before submitting. + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "get_account_status", + "params": { + "account_id": alice_account_id, + } + }); + let res = dispatch(&client, body, &logger); + let result = res.get("result").unwrap(); + let balance_per_token = result.get("balance_per_token").unwrap(); + let balance_mob = balance_per_token.get(Mob::ID.to_string()).unwrap(); + let unspent = balance_mob["unspent"].as_str().unwrap(); + assert_eq!(unspent, "100000000000000"); + + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "get_account_status", + "params": { + "account_id": bob_account_id, + } + }); + let res = dispatch(&client, body, &logger); + let result = res.get("result").unwrap(); + let balance_per_token = result.get("balance_per_token").unwrap(); + let balance_mob = balance_per_token.get(Mob::ID.to_string()); + assert!(balance_mob.is_none()); + + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "get_account_status", + "params": { + "account_id": charlie_account_id, + } + }); + let res = dispatch(&client, body, &logger); + let result = res.get("result").unwrap(); + let balance_per_token = result.get("balance_per_token").unwrap(); + let balance_mob = balance_per_token.get(Mob::ID.to_string()); + assert!(balance_mob.is_none()); + + // Submit the tx_proposal + assert_eq!(ledger_db.num_blocks().unwrap(), 13); + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "submit_transaction", + "params": { + "tx_proposal": tx_proposal, + "account_id": alice_account_id, + } + }); + let res = dispatch(&client, body, &logger); + let result = res.get("result").unwrap(); + let transaction_id = result + .get("transaction_log") + .unwrap() + .get("id") + .unwrap() + .as_str() + .unwrap(); + + let json_tx_proposal: TxProposalJSON = serde_json::from_value(tx_proposal.clone()).unwrap(); + let payments_tx_proposal = TxProposal::try_from(&json_tx_proposal).unwrap(); + + // The MockBlockchainConnection does not write to the ledger_db + add_block_with_tx(&mut ledger_db, payments_tx_proposal.tx, &mut rng); + assert_eq!(ledger_db.num_blocks().unwrap(), 14); + + // Wait for accounts to sync. + manually_sync_account( + &ledger_db, + &db_ctx.get_db_instance(logger.clone()), + &AccountID(alice_account_id.to_string()), + &logger, + ); + manually_sync_account( + &ledger_db, + &db_ctx.get_db_instance(logger.clone()), + &AccountID(bob_account_id.to_string()), + &logger, + ); + manually_sync_account( + &ledger_db, + &db_ctx.get_db_instance(logger.clone()), + &AccountID(charlie_account_id.to_string()), + &logger, + ); + + // Get balances after submission + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "get_account_status", + "params": { + "account_id": alice_account_id, + } + }); + let res = dispatch(&client, body, &logger); + let result = res.get("result").unwrap(); + let balance_per_token = result.get("balance_per_token").unwrap(); + let balance_mob = balance_per_token.get(Mob::ID.to_string()).unwrap(); + let unspent = balance_mob["unspent"].as_str().unwrap(); + assert_eq!(unspent, &(15 * MOB - Mob::MINIMUM_FEE).to_string()); + + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "get_account_status", + "params": { + "account_id": bob_account_id, + } + }); + let res = dispatch(&client, body, &logger); + let result = res.get("result").unwrap(); + let balance_per_token = result.get("balance_per_token").unwrap(); + let balance_mob = balance_per_token.get(Mob::ID.to_string()).unwrap(); + let unspent = balance_mob["unspent"].as_str().unwrap(); + assert_eq!(unspent, "42000000000000"); + + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "get_account_status", + "params": { + "account_id": charlie_account_id, + } + }); + let res = dispatch(&client, body, &logger); + let result = res.get("result").unwrap(); + let balance_per_token = result.get("balance_per_token").unwrap(); + let balance_mob = balance_per_token.get(Mob::ID.to_string()).unwrap(); + let unspent = balance_mob["unspent"].as_str().unwrap(); + assert_eq!(unspent, "43000000000000"); + + // Get the transaction log and verify it contains what we expect + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "get_transaction_log", + "params": { + "transaction_log_id": transaction_id, + } + }); + let res = dispatch(&client, body, &logger); + let result = res.get("result").unwrap(); + let transaction_log = result.get("transaction_log").unwrap(); + + let value_map = transaction_log.get("value_map").unwrap(); + + let pmob_value = value_map.get("0").unwrap(); + assert_eq!(pmob_value.as_str().unwrap(), "85000000000000"); + + let mut output_addresses: Vec = transaction_log + .get("output_txos") + .unwrap() + .as_array() + .unwrap() + .iter() + .map(|t| { + t.get("recipient_public_address_b58") + .unwrap() + .as_str() + .unwrap() + .into() + }) + .collect(); + output_addresses.sort(); + let mut target_addresses = vec![bob_b58_public_address, charlie_b58_public_address]; + target_addresses.sort(); + assert_eq!(output_addresses, target_addresses); + + transaction_log.get("account_id").unwrap().as_str().unwrap(); + let fee_amount = transaction_log.get("fee_amount").unwrap(); + let fee_value = fee_amount.get("value").unwrap().as_str().unwrap(); + let fee_token_id = fee_amount.get("token_id").unwrap().as_str().unwrap(); + assert_eq!(fee_value, &Mob::MINIMUM_FEE.to_string()); + assert_eq!(fee_token_id, &Mob::ID.to_string()); + assert_eq!( + transaction_log.get("status").unwrap().as_str().unwrap(), + "succeeded" + ); + assert_eq!( + transaction_log + .get("submitted_block_index") + .unwrap() + .as_str() + .unwrap(), + "13" + ); + assert_eq!( + transaction_log.get("id").unwrap().as_str().unwrap(), + transaction_id + ); + } +} diff --git a/full-service/src/json_rpc/v2/e2e_tests/transaction/mod.rs b/full-service/src/json_rpc/v2/e2e_tests/transaction/mod.rs new file mode 100644 index 000000000..316941ae8 --- /dev/null +++ b/full-service/src/json_rpc/v2/e2e_tests/transaction/mod.rs @@ -0,0 +1,3 @@ +mod build_submit; +mod transaction_other; +mod transaction_txo; diff --git a/full-service/src/json_rpc/v2/e2e_tests/transaction/transaction_other.rs b/full-service/src/json_rpc/v2/e2e_tests/transaction/transaction_other.rs new file mode 100644 index 000000000..bd1df8b56 --- /dev/null +++ b/full-service/src/json_rpc/v2/e2e_tests/transaction/transaction_other.rs @@ -0,0 +1,337 @@ +// Copyright (c) 2020-2022 MobileCoin Inc. + +//! End-to-end tests for the Full Service Wallet API. + +#[cfg(test)] +mod e2e_transaction { + use crate::{ + db::account::AccountID, + json_rpc::v2::{ + api::test_utils::{dispatch, setup}, + models::tx_proposal::TxProposal as TxProposalJSON, + }, + service::models::tx_proposal::TxProposal, + test_utils::{add_block_to_ledger_db, add_block_with_tx, manually_sync_account, MOB}, + util::b58::b58_decode_public_address, + }; + + use mc_common::logger::{test_with_logger, Logger}; + use mc_crypto_rand::rand_core::RngCore; + use mc_ledger_db::Ledger; + use mc_transaction_core::{ring_signature::KeyImage, tokens::Mob, Token}; + use rand::{rngs::StdRng, SeedableRng}; + + use std::convert::TryFrom; + + #[test_with_logger] + fn test_tx_status_failed_when_tombstone_block_index_exceeded(logger: Logger) { + let mut rng: StdRng = SeedableRng::from_seed([20u8; 32]); + let (client, mut ledger_db, db_ctx, _network_state) = setup(&mut rng, logger.clone()); + + // Add an account + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "create_account", + "params": { + "name": "Alice Main Account", + } + }); + let res = dispatch(&client, body, &logger); + let result = res.get("result").unwrap(); + let account_obj = result.get("account").unwrap(); + let account_id = account_obj.get("id").unwrap().as_str().unwrap(); + let b58_public_address = account_obj.get("main_address").unwrap().as_str().unwrap(); + let public_address = b58_decode_public_address(b58_public_address).unwrap(); + + // Add a block with a txo for this address (note that value is smaller than + // MINIMUM_FEE, so it is a "dust" TxOut that should get opportunistically swept + // up when we construct the transaction) + add_block_to_ledger_db( + &mut ledger_db, + &vec![public_address.clone()], + 100, + &vec![KeyImage::from(rng.next_u64())], + &mut rng, + ); + + manually_sync_account( + &ledger_db, + &db_ctx.get_db_instance(logger.clone()), + &AccountID(account_id.to_string()), + &logger, + ); + assert_eq!(ledger_db.num_blocks().unwrap(), 13); + + // Add a block with significantly more MOB + add_block_to_ledger_db( + &mut ledger_db, + &vec![public_address.clone()], + 100000000000000, // 100.0 MOB + &vec![KeyImage::from(rng.next_u64())], + &mut rng, + ); + + manually_sync_account( + &ledger_db, + &db_ctx.get_db_instance(logger.clone()), + &AccountID(account_id.to_string()), + &logger, + ); + assert_eq!(ledger_db.num_blocks().unwrap(), 14); + + // Create a tx proposal to ourselves with a tombstone block of 1 + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "build_and_submit_transaction", + "params": { + "account_id": account_id, + "recipient_public_address": b58_public_address, + "amount": { "value": "42000000000000", "token_id": "0"}, // 42.0 MOB + "tombstone_block": "16", + } + }); + let res = dispatch(&client, body, &logger); + let result = res.get("result").unwrap(); + let tx_log = result.get("transaction_log").unwrap(); + let tx_log_status = tx_log.get("status").unwrap(); + let tx_log_id = tx_log.get("id").unwrap(); + + assert_eq!(tx_log_status, "pending"); + + // Add a block with 1 MOB + add_block_to_ledger_db( + &mut ledger_db, + &vec![public_address.clone()], + 1, // 100.0 MOB + &vec![KeyImage::from(rng.next_u64())], + &mut rng, + ); + + manually_sync_account( + &ledger_db, + &db_ctx.get_db_instance(logger.clone()), + &AccountID(account_id.to_string()), + &logger, + ); + + // Get balance after submission + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "get_account_status", + "params": { + "account_id": account_id, + } + }); + let res = dispatch(&client, body, &logger); + let result = res.get("result").unwrap(); + let balance_per_token = result.get("balance_per_token").unwrap(); + let balance_mob = balance_per_token.get(Mob::ID.to_string()).unwrap(); + let unspent = balance_mob["unspent"].as_str().unwrap(); + let pending = balance_mob["pending"].as_str().unwrap(); + assert_eq!(unspent, "1"); + assert_eq!(pending, "100000000000100"); + + // Add a block with 1 MOB to increment height 2 times, + // which should cause the previous transaction to + // become invalid and free up the TXO as well as mark + // the transaction log as TX_STATUS_FAILED + add_block_to_ledger_db( + &mut ledger_db, + &vec![public_address.clone()], + 1, // 100.0 MOB + &vec![KeyImage::from(rng.next_u64())], + &mut rng, + ); + add_block_to_ledger_db( + &mut ledger_db, + &vec![public_address.clone()], + 1, // 100.0 MOB + &vec![KeyImage::from(rng.next_u64())], + &mut rng, + ); + manually_sync_account( + &ledger_db, + &db_ctx.get_db_instance(logger.clone()), + &AccountID(account_id.to_string()), + &logger, + ); + + assert_eq!(ledger_db.num_blocks().unwrap(), 17); + + // Get tx log after syncing is finished + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "get_transaction_log", + "params": { + "transaction_log_id": tx_log_id, + } + }); + let res = dispatch(&client, body, &logger); + let result = res.get("result").unwrap(); + let tx_log = result.get("transaction_log").unwrap(); + let tx_log_status = tx_log.get("status").unwrap(); + + assert_eq!(tx_log_status, "failed"); + + // Get balance after submission + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "get_account_status", + "params": { + "account_id": account_id, + } + }); + let res = dispatch(&client, body, &logger); + let result = res.get("result").unwrap(); + let balance_per_token = result.get("balance_per_token").unwrap(); + let balance_mob = balance_per_token.get(Mob::ID.to_string()).unwrap(); + let unspent = balance_mob["unspent"].as_str().unwrap(); + let pending = balance_mob["pending"].as_str().unwrap(); + let spent = balance_mob["spent"].as_str().unwrap(); + assert_eq!(unspent, "100000000000103".to_string()); + assert_eq!(pending, "0"); + assert_eq!(spent, "0"); + } + + #[test_with_logger] + fn test_receipts(logger: Logger) { + let mut rng: StdRng = SeedableRng::from_seed([20u8; 32]); + let (client, mut ledger_db, db_ctx, _network_state) = setup(&mut rng, logger.clone()); + + // Add an account + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "create_account", + "params": { + "name": "Alice Main Account", + } + }); + let res = dispatch(&client, body, &logger); + let result = res.get("result").unwrap(); + let account_obj = result.get("account").unwrap(); + let alice_account_id = account_obj.get("id").unwrap().as_str().unwrap(); + let alice_b58_public_address = account_obj.get("main_address").unwrap().as_str().unwrap(); + let alice_public_address = b58_decode_public_address(alice_b58_public_address).unwrap(); + + // Add a block with a txo for this address + add_block_to_ledger_db( + &mut ledger_db, + &vec![alice_public_address], + 100 * MOB, + &vec![KeyImage::from(rng.next_u64())], + &mut rng, + ); + + manually_sync_account( + &ledger_db, + &db_ctx.get_db_instance(logger.clone()), + &AccountID(alice_account_id.to_string()), + &logger, + ); + + // Add Bob's account to our wallet + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "create_account", + "params": { + "name": "Bob Main Account", + } + }); + let res = dispatch(&client, body, &logger); + let result = res.get("result").unwrap(); + let bob_account_obj = result.get("account").unwrap(); + let bob_account_id = bob_account_obj.get("id").unwrap().as_str().unwrap(); + let bob_b58_public_address = bob_account_obj + .get("main_address") + .unwrap() + .as_str() + .unwrap(); + + // Construct a transaction proposal from Alice to Bob + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "build_transaction", + "params": { + "account_id": alice_account_id, + "recipient_public_address": bob_b58_public_address, + "amount": { "value": "42000000000000", "token_id": "0" }, // 42 MOB + } + }); + let res = dispatch(&client, body, &logger); + let result = res.get("result").unwrap(); + let tx_proposal = result.get("tx_proposal").unwrap(); + + // Get the receipts from the tx_proposal + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "create_receiver_receipts", + "params": { + "tx_proposal": tx_proposal + } + }); + let res = dispatch(&client, body, &logger); + let result = res.get("result").unwrap(); + let receipts = result["receiver_receipts"].as_array().unwrap(); + assert_eq!(receipts.len(), 1); + let receipt = &receipts[0]; + + // Bob checks status (should be pending before the block is added to the ledger) + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "check_receiver_receipt_status", + "params": { + "address": bob_b58_public_address, + "receiver_receipt": receipt, + } + }); + let res = dispatch(&client, body, &logger); + let result = res.get("result").unwrap(); + let status = result["receipt_transaction_status"].as_str().unwrap(); + assert_eq!(status, "TransactionPending"); + + // Add the block to the ledger with the tx proposal + let json_tx_proposal: TxProposalJSON = serde_json::from_value(tx_proposal.clone()).unwrap(); + let payments_tx_proposal = TxProposal::try_from(&json_tx_proposal).unwrap(); + + // The MockBlockchainConnection does not write to the ledger_db + add_block_with_tx(&mut ledger_db, payments_tx_proposal.tx, &mut rng); + + manually_sync_account( + &ledger_db, + &db_ctx.get_db_instance(logger.clone()), + &AccountID(alice_account_id.to_string()), + &logger, + ); + manually_sync_account( + &ledger_db, + &db_ctx.get_db_instance(logger.clone()), + &AccountID(bob_account_id.to_string()), + &logger, + ); + + // Bob checks status (should be successful after added to the ledger) + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "check_receiver_receipt_status", + "params": { + "address": bob_b58_public_address, + "receiver_receipt": receipt, + } + }); + let res = dispatch(&client, body, &logger); + let result = res.get("result").unwrap(); + let status = result["receipt_transaction_status"].as_str().unwrap(); + assert_eq!(status, "TransactionSuccess"); + } +} diff --git a/full-service/src/json_rpc/v2/e2e_tests/transaction/transaction_txo.rs b/full-service/src/json_rpc/v2/e2e_tests/transaction/transaction_txo.rs new file mode 100644 index 000000000..d01ca413b --- /dev/null +++ b/full-service/src/json_rpc/v2/e2e_tests/transaction/transaction_txo.rs @@ -0,0 +1,595 @@ +// Copyright (c) 2020-2022 MobileCoin Inc. + +//! End-to-end tests for the Full Service Wallet API. + +#[cfg(test)] +mod e2e_transaction { + use crate::{ + db::{account::AccountID, txo::TxoStatus}, + json_rpc::v2::{ + api::test_utils::{dispatch, setup}, + models::tx_proposal::TxProposal as TxProposalJSON, + }, + service::models::tx_proposal::TxProposal, + test_utils::{add_block_to_ledger_db, add_block_with_tx, manually_sync_account}, + util::b58::b58_decode_public_address, + }; + + use mc_common::logger::{test_with_logger, Logger}; + use mc_crypto_rand::rand_core::RngCore; + + use mc_transaction_core::{ring_signature::KeyImage, tokens::Mob, Token}; + use rand::{rngs::StdRng, SeedableRng}; + + use std::convert::TryFrom; + + #[test_with_logger] + fn test_send_txo_received_from_removed_account(logger: Logger) { + use crate::db::schema::txos; + use diesel::{dsl::count, prelude::*}; + + let mut rng: StdRng = SeedableRng::from_seed([20u8; 32]); + let (client, mut ledger_db, db_ctx, _network_state) = setup(&mut rng, logger.clone()); + + let wallet_db = db_ctx.get_db_instance(logger.clone()); + + // Add three accounts. + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "create_account", + "params": { + "name": "account 1", + } + }); + let res = dispatch(&client, body, &logger); + let result = res.get("result").unwrap(); + let account_obj = result.get("account").unwrap(); + let account_id_1 = account_obj.get("id").unwrap().as_str().unwrap(); + let b58_public_address_1 = account_obj.get("main_address").unwrap().as_str().unwrap(); + let public_address_1 = b58_decode_public_address(b58_public_address_1).unwrap(); + + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "create_account", + "params": { + "name": "account 2", + } + }); + let res = dispatch(&client, body, &logger); + let result = res.get("result").unwrap(); + let account_obj = result.get("account").unwrap(); + let account_id_2 = account_obj.get("id").unwrap().as_str().unwrap(); + let b58_public_address_2 = account_obj.get("main_address").unwrap().as_str().unwrap(); + + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "create_account", + "params": { + "name": "account 3", + } + }); + let res = dispatch(&client, body, &logger); + let result = res.get("result").unwrap(); + let account_obj = result.get("account").unwrap(); + let account_id_3 = account_obj.get("id").unwrap().as_str().unwrap(); + let b58_public_address_3 = account_obj.get("main_address").unwrap().as_str().unwrap(); + + // Add a block to fund account 1. + assert_eq!( + txos::table + .select(count(txos::id)) + .first::(&wallet_db.get_conn().unwrap()) + .unwrap(), + 0 + ); + add_block_to_ledger_db( + &mut ledger_db, + &vec![public_address_1], + 100000000000000, // 100.0 MOB + &vec![KeyImage::from(rng.next_u64())], + &mut rng, + ); + + manually_sync_account( + &ledger_db, + &wallet_db, + &AccountID(account_id_1.to_string()), + &logger, + ); + assert_eq!( + txos::table + .select(count(txos::id)) + .first::(&wallet_db.get_conn().unwrap()) + .unwrap(), + 1 + ); + + // Send some coins to account 2. + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "build_transaction", + "params": { + "account_id": account_id_1, + "recipient_public_address": b58_public_address_2, + "amount": {"value": "84000000000000", "token_id": "0"}, // 84.0 MOB + } + }); + let res = dispatch(&client, body, &logger); + let result = res.get("result").unwrap(); + let tx_proposal = result.get("tx_proposal").unwrap(); + + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "submit_transaction", + "params": { + "tx_proposal": tx_proposal, + "account_id": account_id_1, + } + }); + let res = dispatch(&client, body, &logger); + let result = res.get("result"); + assert!(result.is_some()); + + let json_tx_proposal: TxProposalJSON = serde_json::from_value(tx_proposal.clone()).unwrap(); + let payments_tx_proposal = TxProposal::try_from(&json_tx_proposal).unwrap(); + + add_block_with_tx(&mut ledger_db, payments_tx_proposal.tx, &mut rng); + + manually_sync_account( + &ledger_db, + &wallet_db, + &AccountID(account_id_2.to_string()), + &logger, + ); + assert_eq!( + txos::table + .select(count(txos::id)) + .first::(&wallet_db.get_conn().unwrap()) + .unwrap(), + 3 + ); + + // Remove account 1. + let body = json!({ + "jsonrpc": "2.0", + "id": 2, + "method": "remove_account", + "params": { + "account_id": account_id_1, + } + }); + let res = dispatch(&client, body, &logger); + let result = res.get("result").unwrap(); + assert_eq!(result["removed"].as_bool().unwrap(), true,); + assert_eq!( + txos::table + .select(count(txos::id)) + .first::(&wallet_db.get_conn().unwrap()) + .unwrap(), + 1 + ); + + // Send coins from account 2 to account 3. + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "build_transaction", + "params": { + "account_id": account_id_2, + "recipient_public_address": b58_public_address_3, + "amount": { "value": "42000000000000", "token_id": "0" }, // 42.0 MOB + } + }); + let res = dispatch(&client, body, &logger); + let result = res.get("result").unwrap(); + let tx_proposal = result.get("tx_proposal").unwrap(); + + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "submit_transaction", + "params": { + "tx_proposal": tx_proposal, + "account_id": account_id_2, + } + }); + let res = dispatch(&client, body, &logger); + let result = res.get("result"); + assert!(result.is_some()); + + let json_tx_proposal: TxProposalJSON = serde_json::from_value(tx_proposal.clone()).unwrap(); + let payments_tx_proposal = TxProposal::try_from(&json_tx_proposal).unwrap(); + + add_block_with_tx(&mut ledger_db, payments_tx_proposal.tx, &mut rng); + + manually_sync_account( + &ledger_db, + &wallet_db, + &AccountID(account_id_3.to_string()), + &logger, + ); + assert_eq!( + txos::table + .select(count(txos::id)) + .first::(&wallet_db.get_conn().unwrap()) + .unwrap(), + 3 + ); + + // Check that account 3 received its coins. + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "get_account_status", + "params": { + "account_id": account_id_3, + } + }); + let res = dispatch(&client, body, &logger); + let result = res.get("result").unwrap(); + let balance_per_token = result.get("balance_per_token").unwrap(); + let balance_mob = balance_per_token.get(Mob::ID.to_string()).unwrap(); + let unspent = balance_mob["unspent"].as_str().unwrap(); + assert_eq!(unspent, "42000000000000"); // 42.0 MOB + } + + /// This test is intended to make sure that when a subaddress is assigned + /// that it correctly generates and checks the key image against the ledger + /// db to see if the previously orphaned txo has been spent or not + #[test_with_logger] + fn test_mark_orphaned_txo_as_spent(logger: Logger) { + let mut rng: StdRng = SeedableRng::from_seed([20u8; 32]); + let (client, mut ledger_db, db_ctx, _network_state) = setup(&mut rng, logger.clone()); + + // Add an account + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "import_account", + "params": { + "mnemonic": "sheriff odor square mistake huge skate mouse shoot purity weapon proof stuff correct concert blanket neck own shift clay mistake air viable stick group", + "key_derivation_version": "2", + "name": "Alice Main Account", + } + }); + let res = dispatch(&client, body, &logger); + let result = res.get("result").unwrap(); + let account_obj = result.get("account").unwrap(); + let account_id = account_obj.get("id").unwrap().as_str().unwrap(); + + // Assign next subaddress for account. + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "assign_address_for_account", + "params": { + "account_id": account_id, + "metadata": "subaddress_index_2", + } + }); + let res = dispatch(&client, body, &logger); + let result = res.get("result").unwrap(); + let address = result.get("address").unwrap(); + let b58_public_address = address.get("public_address_b58").unwrap().as_str().unwrap(); + let public_address = b58_decode_public_address(b58_public_address).unwrap(); + + // Add a block to fund account at the new subaddress. + add_block_to_ledger_db( + &mut ledger_db, + &vec![public_address.clone()], + 100000000000000, // 100.0 MOB + &vec![KeyImage::from(rng.next_u64())], + &mut rng, + ); + + add_block_to_ledger_db( + &mut ledger_db, + &vec![public_address.clone()], + 500000000000000, // 500.0 MOB + &vec![KeyImage::from(rng.next_u64())], + &mut rng, + ); + + manually_sync_account( + &ledger_db, + &db_ctx.get_db_instance(logger.clone()), + &AccountID(account_id.to_string()), + &logger, + ); + + // Remove the account. + let body = json!({ + "jsonrpc": "2.0", + "id": 2, + "method": "remove_account", + "params": { + "account_id": *account_id, + } + }); + let res = dispatch(&client, body, &logger); + let result = res.get("result").unwrap(); + assert_eq!(result["removed"].as_bool().unwrap(), true,); + + // Add the same account back. + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "import_account", + "params": { + "mnemonic": "sheriff odor square mistake huge skate mouse shoot purity weapon proof stuff correct concert blanket neck own shift clay mistake air viable stick group", + "key_derivation_version": "2", + "name": "Alice Main Account", + } + }); + let res = dispatch(&client, body, &logger); + let result = res.get("result").unwrap(); + let account_obj = result.get("account").unwrap(); + let account_id = account_obj.get("id").unwrap().as_str().unwrap(); + + manually_sync_account( + &ledger_db, + &db_ctx.get_db_instance(logger.clone()), + &AccountID(account_id.to_string()), + &logger, + ); + + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "get_account_status", + "params": { + "account_id": *account_id, + } + }); + let res = dispatch(&client, body, &logger); + let result = res.get("result").unwrap(); + let balance_per_token = result.get("balance_per_token").unwrap(); + let balance_mob = balance_per_token.get(Mob::ID.to_string()).unwrap(); + assert_eq!(balance_mob.get("unspent").unwrap(), "0"); + assert_eq!(balance_mob.get("spent").unwrap(), "0"); + assert_eq!(balance_mob.get("orphaned").unwrap(), "600000000000000"); + + // Add back next subaddress. Txos are detected as unspent. + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "assign_address_for_account", + "params": { + "account_id": account_id, + "metadata": "subaddress_index_2", + } + }); + dispatch(&client, body, &logger); + + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "get_account_status", + "params": { + "account_id": *account_id, + } + }); + let res = dispatch(&client, body, &logger); + let result = res.get("result").unwrap(); + let balance_per_token = result.get("balance_per_token").unwrap(); + let balance_mob = balance_per_token.get(Mob::ID.to_string()).unwrap(); + assert_eq!(balance_mob.get("unspent").unwrap(), "600000000000000"); + assert_eq!(balance_mob.get("spent").unwrap(), "0"); + assert_eq!(balance_mob.get("orphaned").unwrap(), "0"); + + // Create a second account. + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "create_account", + "params": { + "name": "account 2", + } + }); + let res = dispatch(&client, body, &logger); + let result = res.get("result").unwrap(); + let account_obj = result.get("account").unwrap(); + let account_id_2 = account_obj.get("id").unwrap().as_str().unwrap(); + let b58_public_address_2 = account_obj.get("main_address").unwrap().as_str().unwrap(); + + // Remove the second Account + let body = json!({ + "jsonrpc": "2.0", + "id": 2, + "method": "remove_account", + "params": { + "account_id": *account_id_2, + } + }); + let res = dispatch(&client, body, &logger); + let result = res.get("result").unwrap(); + assert_eq!(result["removed"].as_bool().unwrap(), true,); + + // Send some coins to the removed second account. + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "build_transaction", + "params": { + "account_id": account_id, + "recipient_public_address": b58_public_address_2, + "amount": { "value": "50000000000000", "token_id": "0"}, // 50.0 MOB + } + }); + let res = dispatch(&client, body, &logger); + let result = res.get("result").unwrap(); + let tx_proposal = result.get("tx_proposal").unwrap(); + + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "submit_transaction", + "params": { + "tx_proposal": tx_proposal + } + }); + let res = dispatch(&client, body, &logger); + let result = res.get("result"); + assert!(result.is_some()); + + let json_tx_proposal: TxProposalJSON = serde_json::from_value(tx_proposal.clone()).unwrap(); + let payments_tx_proposal = TxProposal::try_from(&json_tx_proposal).unwrap(); + + add_block_with_tx(&mut ledger_db, payments_tx_proposal.tx, &mut rng); + + manually_sync_account( + &ledger_db, + &db_ctx.get_db_instance(logger.clone()), + &AccountID(account_id.to_string()), + &logger, + ); + + // The first account shows the coins are spent. + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "get_account_status", + "params": { + "account_id": *account_id, + } + }); + let res = dispatch(&client, body, &logger); + let result = res.get("result").unwrap(); + let balance_per_token = result.get("balance_per_token").unwrap(); + let balance_mob = balance_per_token.get(Mob::ID.to_string()).unwrap(); + assert_eq!(balance_mob.get("unspent").unwrap(), "549999600000000"); + assert_eq!(balance_mob.get("spent").unwrap(), "100000000000000"); + assert_eq!(balance_mob.get("orphaned").unwrap(), "0"); + + // Remove the first account and add it back again. + let body = json!({ + "jsonrpc": "2.0", + "id": 2, + "method": "remove_account", + "params": { + "account_id": *account_id, + } + }); + let res = dispatch(&client, body, &logger); + let result = res.get("result").unwrap(); + assert_eq!(result["removed"].as_bool().unwrap(), true,); + + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "import_account", + "params": { + "mnemonic": "sheriff odor square mistake huge skate mouse shoot purity weapon proof stuff correct concert blanket neck own shift clay mistake air viable stick group", + "key_derivation_version": "2", + "name": "Alice Main Account", + } + }); + let res = dispatch(&client, body, &logger); + let result = res.get("result").unwrap(); + let account_obj = result.get("account").unwrap(); + let account_id = account_obj.get("id").unwrap().as_str().unwrap(); + + manually_sync_account( + &ledger_db, + &db_ctx.get_db_instance(logger.clone()), + &AccountID(account_id.to_string()), + &logger, + ); + + // The unspent pmob shows what wasn't sent to the second account. + // The orphaned pmob are because we haven't added back the next subaddress. + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "get_account_status", + "params": { + "account_id": *account_id, + } + }); + let res = dispatch(&client, body, &logger); + let result = res.get("result").unwrap(); + let balance_per_token = result.get("balance_per_token").unwrap(); + let balance_mob = balance_per_token.get(Mob::ID.to_string()).unwrap(); + assert_eq!(balance_mob.get("unspent").unwrap(), "49999600000000"); + assert_eq!(balance_mob.get("spent").unwrap(), "0"); + assert_eq!(balance_mob.get("orphaned").unwrap(), "600000000000000"); + } + + #[test_with_logger] + fn test_get_txos(logger: Logger) { + let mut rng: StdRng = SeedableRng::from_seed([20u8; 32]); + let (client, mut ledger_db, db_ctx, _network_state) = setup(&mut rng, logger.clone()); + + // Add an account + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "create_account", + "params": { + "name": "Alice Main Account", + } + }); + let res = dispatch(&client, body, &logger); + let result = res.get("result").unwrap(); + let account_obj = result.get("account").unwrap(); + let account_id = account_obj.get("id").unwrap().as_str().unwrap(); + let b58_public_address = account_obj.get("main_address").unwrap().as_str().unwrap(); + let public_address = b58_decode_public_address(b58_public_address).unwrap(); + + // Add a block with a txo for this address + add_block_to_ledger_db( + &mut ledger_db, + &vec![public_address], + 100, + &vec![KeyImage::from(rng.next_u64())], + &mut rng, + ); + + manually_sync_account( + &ledger_db, + &db_ctx.get_db_instance(logger.clone()), + &AccountID(account_id.to_string()), + &logger, + ); + + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "get_txos", + "params": { + "account_id": account_id, + } + }); + let res = dispatch(&client, body, &logger); + let result = res.get("result").unwrap(); + let txos = result.get("txo_ids").unwrap().as_array().unwrap(); + assert_eq!(txos.len(), 1); + let txo_map = result.get("txo_map").unwrap().as_object().unwrap(); + let txo = txo_map.get(txos[0].as_str().unwrap()).unwrap(); + let txo_status = txo.get("status").unwrap().as_str().unwrap(); + assert_eq!(txo_status, TxoStatus::Unspent.to_string()); + let value = txo.get("value").unwrap().as_str().unwrap(); + assert_eq!(value, "100"); + + // Check the overall balance for the account + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "get_account_status", + "params": { + "account_id": account_id, + } + }); + let res = dispatch(&client, body, &logger); + let result = res.get("result").unwrap(); + let balance_per_token = result.get("balance_per_token").unwrap(); + let balance_mob = balance_per_token.get(Mob::ID.to_string()).unwrap(); + let unspent = balance_mob["unspent"].as_str().unwrap(); + assert_eq!(unspent, "100"); + } +} diff --git a/full-service/src/json_rpc/v2/mod.rs b/full-service/src/json_rpc/v2/mod.rs new file mode 100644 index 000000000..c2352b062 --- /dev/null +++ b/full-service/src/json_rpc/v2/mod.rs @@ -0,0 +1,5 @@ +pub mod api; +pub mod models; + +#[cfg(any(test))] +pub mod e2e_tests; diff --git a/full-service/src/json_rpc/v2/models/account.rs b/full-service/src/json_rpc/v2/models/account.rs new file mode 100644 index 000000000..497d97d7d --- /dev/null +++ b/full-service/src/json_rpc/v2/models/account.rs @@ -0,0 +1,88 @@ +// Copyright (c) 2020-2021 MobileCoin Inc. + +//! API definition for the Account object. + +use crate::{db, util::b58::b58_encode_public_address}; +use serde_derive::{Deserialize, Serialize}; +use std::collections::BTreeMap; + +#[derive(Deserialize, Serialize, Default, Debug, Clone)] +pub struct AccountMap(pub BTreeMap); + +/// An account in the wallet. +/// +/// An Account is associated with one AccountKey, containing a View keypair and +/// a Spend keypair. +#[derive(Deserialize, Serialize, Default, Debug, Clone)] +pub struct Account { + /// Unique identifier for the account. Constructed from the public key + /// materials of the account key. + pub id: String, + + /// Display name for the account. + pub name: String, + + /// Key Derivation Version + pub key_derivation_version: String, + + /// B58 Address Code for the account's main address. The main address is + /// determined by the seed subaddress. It is not assigned to a single + /// recipient, and should be consider a free-for-all address. + pub main_address: String, + + /// This index represents the next subaddress to be assigned as an address. + /// This is useful information in case the account is imported elsewhere. + pub next_subaddress_index: String, + + /// Index of the first block when this account may have received funds. + /// No transactions before this point will be synchronized. + pub first_block_index: String, + + /// Index of the next block this account needs to sync. + pub next_block_index: String, + + /// A flag that indicates this imported account is attempting to un-orphan + /// found TXOs. It is recommended to move all MOB to another account after + /// recovery if the user is unsure of the assigned addresses. + pub recovery_mode: bool, + + /// A flag that indicates if this account is FOG enabled, which means that + /// it will send any change to it's main subaddress (index 0) instead of + /// the default change subaddress (index 1). It also generates + /// PublicAddressB58's with fog credentials. + pub fog_enabled: bool, + + /// A flag that indicates if this account is a watch only account. + pub view_only: bool, +} + +impl Account { + pub fn new(src: &db::models::Account, next_subaddress_index: u64) -> Result { + let main_public_address = if src.view_only { + let account_key: mc_account_keys::ViewAccountKey = + mc_util_serial::decode(&src.account_key) + .map_err(|e| format!("Failed to decode view account key: {}", e))?; + account_key.default_subaddress() + } else { + let account_key: mc_account_keys::AccountKey = mc_util_serial::decode(&src.account_key) + .map_err(|e| format!("Failed to decode account key: {}", e))?; + account_key.default_subaddress() + }; + + let main_public_address_b58 = b58_encode_public_address(&main_public_address) + .map_err(|e| format!("Could not b58 encode public address {:?}", e))?; + + Ok(Account { + id: src.id.clone(), + key_derivation_version: src.key_derivation_version.to_string(), + name: src.name.clone(), + main_address: main_public_address_b58, + next_subaddress_index: next_subaddress_index.to_string(), + first_block_index: (src.first_block_index as u64).to_string(), + next_block_index: (src.next_block_index as u64).to_string(), + recovery_mode: false, + fog_enabled: src.fog_enabled, + view_only: src.view_only, + }) + } +} diff --git a/full-service/src/json_rpc/v2/models/account_key.rs b/full-service/src/json_rpc/v2/models/account_key.rs new file mode 100644 index 000000000..d7d114f65 --- /dev/null +++ b/full-service/src/json_rpc/v2/models/account_key.rs @@ -0,0 +1,137 @@ +// Copyright (c) 2020-2021 MobileCoin Inc. + +//! API definition for the Account Key object. + +use crate::util::encoding_helpers::{ + hex_to_ristretto, hex_to_ristretto_public, hex_to_vec, ristretto_public_to_hex, + ristretto_to_hex, vec_to_hex, +}; +use serde_derive::{Deserialize, Serialize}; +use std::convert::TryFrom; + +/// The AccountKey contains a View keypair and a Spend keypair, used to +/// construct and receive transactions. +#[derive(Deserialize, Serialize, Default, Debug, Clone)] +pub struct AccountKey { + /// Private key used for view-key matching, hex-encoded Ristretto bytes. + pub view_private_key: String, + + /// Private key used for spending, hex-encoded Ristretto bytes. + pub spend_private_key: String, + + /// Fog Report server url (if user has Fog service), empty string otherwise. + pub fog_report_url: String, + + /// Fog Report Key (if user has Fog service), empty otherwise + /// The key labelling the report to use, from among the several reports + /// which might be served by the fog report server. + pub fog_report_id: String, + + /// Fog Authority Subject Public Key Info (if user has Fog service), + /// empty string otherwise. + pub fog_authority_spki: String, +} + +impl From<&mc_account_keys::AccountKey> for AccountKey { + fn from(src: &mc_account_keys::AccountKey) -> AccountKey { + AccountKey { + view_private_key: ristretto_to_hex(src.view_private_key()), + spend_private_key: ristretto_to_hex(src.spend_private_key()), + fog_report_url: src.fog_report_url().unwrap_or("").to_string(), + fog_report_id: src.fog_report_id().unwrap_or("").to_string(), + fog_authority_spki: vec_to_hex(src.fog_authority_spki().unwrap_or(&[])), + } + } +} + +impl TryFrom<&AccountKey> for mc_account_keys::AccountKey { + type Error = String; + + fn try_from(src: &AccountKey) -> Result { + let view_private_key = hex_to_ristretto(&src.view_private_key)?; + let spend_private_key = hex_to_ristretto(&src.spend_private_key)?; + let fog_authority_spki = hex_to_vec(&src.fog_authority_spki)?; + + Ok(mc_account_keys::AccountKey::new_with_fog( + &spend_private_key, + &view_private_key, + src.fog_report_url.clone(), + src.fog_report_id.clone(), + fog_authority_spki, + )) + } +} + +/// The Fog Info contains the information needed to construct a Fog Report. +#[derive(Deserialize, Serialize, Default, Debug, Clone)] +pub struct FogInfo { + /// Fog Report server url (if user has Fog service), empty string otherwise. + pub report_url: String, + + /// Fog Report Key (if user has Fog service), empty otherwise + /// The key labelling the report to use, from among the several reports + /// which might be served by the fog report server. + pub report_id: String, + + /// Fog Authority Subject Public Key Info (if user has Fog service), + /// empty string otherwise. + pub authority_spki: String, +} + +/// The ViewAccountKey contains a View private key and a Spend public key +#[derive(Deserialize, Serialize, Default, Debug, Clone)] +pub struct ViewAccountKey { + /// String representing the object's type. Objects of the same type share + /// the same value. + pub object: String, + + /// Private key used for view-key matching, hex-encoded Ristretto bytes. + pub view_private_key: String, + + /// Public key, hex-encoded Ristretto bytes. + pub spend_public_key: String, +} + +impl From<&mc_account_keys::ViewAccountKey> for ViewAccountKey { + fn from(src: &mc_account_keys::ViewAccountKey) -> ViewAccountKey { + ViewAccountKey { + object: "view_account_key".to_string(), + view_private_key: ristretto_to_hex(src.view_private_key()), + spend_public_key: ristretto_public_to_hex(src.spend_public_key()), + } + } +} + +impl TryFrom<&ViewAccountKey> for mc_account_keys::ViewAccountKey { + type Error = String; + + fn try_from(src: &ViewAccountKey) -> Result { + let view_private_key = hex_to_ristretto(&src.view_private_key)?; + let spend_public_key = hex_to_ristretto_public(&src.spend_public_key)?; + + Ok(mc_account_keys::ViewAccountKey::new( + view_private_key, + spend_public_key, + )) + } +} + +#[cfg(test)] +mod account_key_tests { + use super::*; + use rand::{rngs::StdRng, SeedableRng}; + + #[test] + fn test_round_trip() { + let mut rng: StdRng = SeedableRng::from_seed([20u8; 32]); + + let account_key1 = mc_account_keys::AccountKey::random(&mut rng); + let json_rpc_account_key1 = AccountKey::try_from(&account_key1).unwrap(); + let json_account_key = serde_json::json!(json_rpc_account_key1); + + let json_rpc_account_key2: AccountKey = serde_json::from_value(json_account_key).unwrap(); + let account_key2 = mc_account_keys::AccountKey::try_from(&json_rpc_account_key2).unwrap(); + + assert_eq!(account_key1, account_key2); + } +} diff --git a/full-service/src/json_rpc/v2/models/account_secrets.rs b/full-service/src/json_rpc/v2/models/account_secrets.rs new file mode 100644 index 000000000..727fdd94e --- /dev/null +++ b/full-service/src/json_rpc/v2/models/account_secrets.rs @@ -0,0 +1,103 @@ +// Copyright (c) 2020-2021 MobileCoin Inc. + +//! API definition for the Account Secrets object. + +use crate::{ + db::models::Account, + json_rpc::v2::models::account_key::{AccountKey, ViewAccountKey}, +}; + +use bip39::{Language, Mnemonic}; +use serde_derive::{Deserialize, Serialize}; +use std::convert::TryFrom; + +/// The AccountSecrets contains the entropy and the account key derived from +/// that entropy. +#[derive(Deserialize, Serialize, Default, Debug, Clone)] +pub struct AccountSecrets { + /// The account ID for this account key in the wallet database. + pub account_id: String, + + /// The name of this account + pub name: String, + + /// The entropy from which this account key was derived, as a String + /// (version 1) + #[serde(skip_serializing_if = "Option::is_none")] + pub entropy: Option, + + /// The mnemonic from which this account key was derived, as a String + /// (version 2) + #[serde(skip_serializing_if = "Option::is_none")] + pub mnemonic: Option, + + /// The key derivation version that this mnemonic goes with + pub key_derivation_version: String, + + /// Private keys for receiving and spending MobileCoin. + #[serde(skip_serializing_if = "Option::is_none")] + pub account_key: Option, + + /// Private keys for receiving and spending MobileCoin. + #[serde(skip_serializing_if = "Option::is_none")] + pub view_account_key: Option, +} + +impl TryFrom<&Account> for AccountSecrets { + type Error = String; + + fn try_from(src: &Account) -> Result { + if src.view_only { + let view_account_key: mc_account_keys::ViewAccountKey = + mc_util_serial::decode(&src.account_key).map_err(|err| { + format!("Could not decode account key from database: {:?}", err) + })?; + + Ok(AccountSecrets { + account_id: src.id.clone(), + name: src.name.clone(), + entropy: None, + mnemonic: None, + key_derivation_version: src.key_derivation_version.to_string(), + account_key: None, + view_account_key: Some(ViewAccountKey::from(&view_account_key)), + }) + } else { + let account_key: mc_account_keys::AccountKey = mc_util_serial::decode(&src.account_key) + .map_err(|err| format!("Could not decode account key from database: {:?}", err))?; + + let entropy = match src.key_derivation_version { + 1 => Some(hex::encode(src.entropy.as_ref().ok_or("No entropy found")?)), + _ => None, + }; + + let mnemonic = match src.key_derivation_version { + 2 => Some( + Mnemonic::from_entropy( + src.entropy.as_ref().ok_or("No entropy found")?, + Language::English, + ) + .map_err(|err| format!("Could not create mnemonic: {:?}", err))? + .phrase() + .to_string(), + ), + _ => None, + }; + + Ok(AccountSecrets { + name: src.name.clone(), + account_id: src.id.clone(), + entropy, + mnemonic, + key_derivation_version: src.key_derivation_version.to_string(), + account_key: Some(AccountKey::try_from(&account_key).map_err(|err| { + format!( + "Could not convert account_key to json_rpc representation: {:?}", + err + ) + })?), + view_account_key: None, + }) + } + } +} diff --git a/full-service/src/json_rpc/v2/models/address.rs b/full-service/src/json_rpc/v2/models/address.rs new file mode 100644 index 000000000..d3b4e016a --- /dev/null +++ b/full-service/src/json_rpc/v2/models/address.rs @@ -0,0 +1,46 @@ +// Copyright (c) 2020-2021 MobileCoin Inc. + +//! API definition for the Address object. + +use std::collections::BTreeMap; + +use crate::db::models::AssignedSubaddress; +use serde_derive::{Deserialize, Serialize}; + +#[derive(Deserialize, Serialize, Default, Debug, Clone)] +pub struct AddressMap(pub BTreeMap); + +/// An address for an account in the wallet. +/// +/// An account may have many addresses. This wallet implementation assumes +/// that an address has been "assigned" to an intended sender. In this way +/// the wallet can make sense of the anonymous MobileCoin ledger, by +/// determining the likely sender of the Txo is whomever was given that +/// address to which to send. +#[derive(Deserialize, Serialize, Default, Debug, Clone)] +pub struct Address { + /// A b58 encoding of the public address materials. + /// + /// The public_address is the unique identifier for the address. + pub public_address_b58: String, + + /// The account which owns this address. + pub account_id: String, + + /// Additional data associated with this address. + pub metadata: String, + + /// The index of this address in the subaddress space for the account. + pub subaddress_index: String, +} + +impl From<&AssignedSubaddress> for Address { + fn from(src: &AssignedSubaddress) -> Address { + Address { + public_address_b58: src.public_address_b58.clone(), + account_id: src.account_id.clone(), + metadata: src.comment.clone(), + subaddress_index: (src.subaddress_index as u64).to_string(), + } + } +} diff --git a/full-service/src/json_rpc/v2/models/amount.rs b/full-service/src/json_rpc/v2/models/amount.rs new file mode 100644 index 000000000..23465f201 --- /dev/null +++ b/full-service/src/json_rpc/v2/models/amount.rs @@ -0,0 +1,50 @@ +// Copyright (c) 2020-2021 MobileCoin Inc. + +//! API definition for the Account object. + +use mc_transaction_core::TokenId; +use serde::{Deserialize, Serialize}; +use std::convert::TryFrom; + +/// The value and token_id of a txo. +#[derive(Deserialize, Serialize, Default, Debug, Clone, PartialEq, Eq)] +pub struct Amount { + /// The value of a Txo + pub value: String, + + /// The token_id of a Txo + pub token_id: String, +} + +impl Amount { + pub fn new(value: u64, token_id: TokenId) -> Self { + Self { + value: value.to_string(), + token_id: token_id.to_string(), + } + } +} + +impl From<&mc_transaction_core::Amount> for Amount { + fn from(src: &mc_transaction_core::Amount) -> Self { + Self::new(src.value, src.token_id) + } +} + +impl TryFrom<&Amount> for mc_transaction_core::Amount { + type Error = String; + + fn try_from(src: &Amount) -> Result { + Ok(Self { + value: src + .value + .parse::() + .map_err(|err| format!("Could not parse value u64: {:?}", err))?, + token_id: TokenId::from( + src.token_id + .parse::() + .map_err(|err| format!("Could not parse token_id u64: {:?}", err))?, + ), + }) + } +} diff --git a/full-service/src/json_rpc/v2/models/balance.rs b/full-service/src/json_rpc/v2/models/balance.rs new file mode 100644 index 000000000..663e1581e --- /dev/null +++ b/full-service/src/json_rpc/v2/models/balance.rs @@ -0,0 +1,60 @@ +// Copyright (c) 2020-2021 MobileCoin Inc. + +//! API definition for the Balance object. + +use std::collections::BTreeMap; + +use crate::service; + +use serde_derive::{Deserialize, Serialize}; + +#[derive(Deserialize, Serialize, Default, Debug, Clone)] +pub struct BalanceMap(pub BTreeMap); + +/// The balance for an account, as well as some information about syncing status +/// needed to interpret the balance correctly. +#[derive(Deserialize, Serialize, Default, Debug, Clone)] +pub struct Balance { + /// The max spendable amount in a single transaction. + pub max_spendable: String, + + /// Unverified pico MOB. The Unverified value represents the Txos which were + /// NOT view-key matched, but do have an assigned subaddress. + pub unverified: String, + + /// Unspent pico MOB for this account at the current account_block_height. + /// If the account is syncing, this value may change. + pub unspent: String, + + /// Pending, out-going pico MOB. The pending value will clear once the + /// ledger processes the outgoing txos. The available_pmob will reflect the + /// change. + pub pending: String, + + /// Spent pico MOB. This is the sum of all the Txos in the wallet which have + /// been spent. + pub spent: String, + + /// Secreted (minted) pico MOB. This is the sum of all the Txos which have + /// been created in the wallet for outgoing transactions. + pub secreted: String, + + /// Orphaned pico MOB. The orphaned value represents the Txos which were + /// view-key matched, but which can not be spent until their subaddress + /// index is recovered. + pub orphaned: String, +} + +impl From<&service::balance::Balance> for Balance { + fn from(src: &service::balance::Balance) -> Balance { + Balance { + max_spendable: src.max_spendable.to_string(), + unverified: src.unverified.to_string(), + unspent: src.unspent.to_string(), + pending: src.pending.to_string(), + spent: src.spent.to_string(), + secreted: src.secreted.to_string(), + orphaned: src.orphaned.to_string(), + } + } +} diff --git a/full-service/src/json_rpc/v2/models/block.rs b/full-service/src/json_rpc/v2/models/block.rs new file mode 100644 index 000000000..7185a67b7 --- /dev/null +++ b/full-service/src/json_rpc/v2/models/block.rs @@ -0,0 +1,59 @@ +// Copyright (c) 2020-2021 MobileCoin Inc. + +//! API definition for the Block object. + +use mc_mobilecoind_json::data_types::{JsonTxOut, JsonTxOutMembershipElement}; +use serde_derive::{Deserialize, Serialize}; + +#[derive(Deserialize, Serialize, Default, Debug)] +pub struct Block { + pub id: String, + pub version: String, + pub parent_id: String, + pub index: String, + pub cumulative_txo_count: String, + pub root_element: JsonTxOutMembershipElement, + pub contents_hash: String, +} + +impl Block { + pub fn new(block: &mc_blockchain_types::Block) -> Self { + let membership_element_proto = + mc_api::external::TxOutMembershipElement::from(&block.root_element); + Self { + id: hex::encode(block.id.clone()), + version: block.version.to_string(), + parent_id: hex::encode(block.parent_id.clone()), + index: block.index.to_string(), + cumulative_txo_count: block.cumulative_txo_count.to_string(), + root_element: JsonTxOutMembershipElement::from(&membership_element_proto), + contents_hash: hex::encode(block.contents_hash.0), + } + } +} + +#[derive(Deserialize, Serialize, Default, Debug)] +pub struct BlockContents { + pub key_images: Vec, + pub outputs: Vec, +} + +impl BlockContents { + pub fn new(block_contents: &mc_blockchain_types::BlockContents) -> Self { + Self { + key_images: block_contents + .key_images + .iter() + .map(|k| hex::encode(mc_util_serial::encode(k))) + .collect::>(), + outputs: block_contents + .outputs + .iter() + .map(|txo| { + let proto_txo = mc_api::external::TxOut::from(txo); + JsonTxOut::from(&proto_txo) + }) + .collect::>(), + } + } +} diff --git a/full-service/src/json_rpc/v2/models/confirmation_number.rs b/full-service/src/json_rpc/v2/models/confirmation_number.rs new file mode 100644 index 000000000..9da73c284 --- /dev/null +++ b/full-service/src/json_rpc/v2/models/confirmation_number.rs @@ -0,0 +1,34 @@ +// Copyright (c) 2020-2021 MobileCoin Inc. + +//! API definition for the Txo object. + +use crate::service; +use serde::{Deserialize, Serialize}; + +/// A confirmation number for a Txo in the wallet. +/// +/// A confirmation number allows a sender to provide evidence that they were +/// involved in the construction of an associated Txo. +#[derive(Deserialize, Serialize, Default, Debug, Clone)] +pub struct Confirmation { + /// Unique identifier for the Txo. + txo_id: String, + + /// The index of the Txo in the ledger. + txo_index: String, + + /// A string with a confirmation number that can be validated to confirm + /// that another party constructed or had knowledge of the construction + /// of the associated Txo. + confirmation: String, +} + +impl From<&service::confirmation_number::Confirmation> for Confirmation { + fn from(src: &service::confirmation_number::Confirmation) -> Confirmation { + Confirmation { + txo_id: src.txo_id.to_string(), + txo_index: src.txo_index.to_string(), + confirmation: hex::encode(mc_util_serial::encode(&src.confirmation)), + } + } +} diff --git a/full-service/src/json_rpc/v2/models/masked_amount.rs b/full-service/src/json_rpc/v2/models/masked_amount.rs new file mode 100644 index 000000000..1ed7a5fe8 --- /dev/null +++ b/full-service/src/json_rpc/v2/models/masked_amount.rs @@ -0,0 +1,97 @@ +// Copyright (c) 2020-2021 MobileCoin Inc. + +//! API definition for the Account object. + +use mc_crypto_keys::ReprBytes; +use serde::{Deserialize, Serialize}; +use std::convert::TryFrom; + +#[derive(Deserialize, Serialize, Debug, Clone)] +pub enum MaskedAmountVersion { + V1, + V2, +} + +impl Default for MaskedAmountVersion { + fn default() -> Self { + MaskedAmountVersion::V1 + } +} + +/// The encrypted amount of pMOB in a Txo. +#[derive(Deserialize, Serialize, Default, Debug, Clone)] +pub struct MaskedAmount { + /// A Pedersen commitment `v*G + s*H` + pub commitment: String, + + /// The masked value of pMOB in a Txo. + /// + /// The private view key is required to decrypt the amount, via: + /// `masked_value = value XOR_8 Blake2B("value_mask" || shared_secret)` + pub masked_value: String, + + /// `masked_token_id = token_id XOR_8 Blake2B(token_id_mask | + /// shared_secret)` 8 bytes long when used, 0 bytes for older amounts + /// that don't have this. + pub masked_token_id: String, + + /// The version of the masked amount. + pub version: MaskedAmountVersion, +} + +impl From<&mc_transaction_core::MaskedAmount> for MaskedAmount { + fn from(src: &mc_transaction_core::MaskedAmount) -> Self { + let version = match src { + mc_transaction_core::MaskedAmount::V1(_) => MaskedAmountVersion::V1, + mc_transaction_core::MaskedAmount::V2(_) => MaskedAmountVersion::V2, + }; + + Self { + commitment: hex::encode(src.commitment().to_bytes()), + masked_value: src.get_masked_value().to_string(), + masked_token_id: hex::encode(&src.masked_token_id()), + version, + } + } +} + +impl TryFrom<&MaskedAmount> for mc_transaction_core::MaskedAmount { + type Error = String; + + fn try_from(src: &MaskedAmount) -> Result { + let mut commitment_bytes = [0u8; 32]; + commitment_bytes[0..32].copy_from_slice( + &hex::decode(&src.commitment) + .map_err(|err| format!("Could not decode hex for amount commitment: {:?}", err))?, + ); + + let commitment = (&commitment_bytes).into(); + let masked_value = src + .masked_value + .parse::() + .map_err(|err| format!("Could not parse masked value u64: {:?}", err))?; + let masked_token_id = hex::decode(&src.masked_token_id) + .map_err(|err| format!("Could not decode hex for masked token id: {:?}", err))?; + + match src.version { + MaskedAmountVersion::V1 => { + let masked_amount = mc_transaction_core::MaskedAmountV1 { + commitment, + masked_value, + masked_token_id, + }; + + Ok(mc_transaction_core::MaskedAmount::V1(masked_amount)) + } + MaskedAmountVersion::V2 => { + let masked_amount = mc_transaction_core::MaskedAmountV2 { + commitment, + masked_value, + masked_token_id, + }; + + Ok(mc_transaction_core::MaskedAmount::V2(masked_amount)) + } + } + } +} diff --git a/full-service/src/json_rpc/v2/models/mod.rs b/full-service/src/json_rpc/v2/models/mod.rs new file mode 100644 index 000000000..2f23b3e2b --- /dev/null +++ b/full-service/src/json_rpc/v2/models/mod.rs @@ -0,0 +1,15 @@ +pub mod account; +pub mod account_key; +pub mod account_secrets; +pub mod address; +pub mod amount; +pub mod balance; +pub mod block; +pub mod confirmation_number; +pub mod masked_amount; +pub mod network_status; +pub mod receiver_receipt; +pub mod transaction_log; +pub mod tx_proposal; +pub mod txo; +pub mod wallet_status; diff --git a/full-service/src/json_rpc/v2/models/network_status.rs b/full-service/src/json_rpc/v2/models/network_status.rs new file mode 100644 index 000000000..a0b3deb74 --- /dev/null +++ b/full-service/src/json_rpc/v2/models/network_status.rs @@ -0,0 +1,41 @@ +// Copyright (c) 2020-2021 MobileCoin Inc. + +//! API definition for the Network Status object. + +use crate::service; + +use serde_derive::{Deserialize, Serialize}; +use std::{collections::BTreeMap, convert::TryFrom}; + +#[derive(Deserialize, Serialize, Default, Debug, Clone)] +pub struct NetworkStatus { + /// The block count of MobileCoin's distributed ledger. + pub network_block_height: String, + + /// The local block count downloaded from the ledger. The local database + /// is synced when the local_block_height reaches the network_block_height. + pub local_block_height: String, + + /// The current network fee per token_id. + pub fees: BTreeMap, + + /// The current block version + pub block_version: String, +} + +impl TryFrom<&service::balance::NetworkStatus> for NetworkStatus { + type Error = String; + + fn try_from(src: &service::balance::NetworkStatus) -> Result { + Ok(NetworkStatus { + network_block_height: src.network_block_height.to_string(), + local_block_height: src.local_block_height.to_string(), + fees: src + .fees + .iter() + .map(|(token_id, fee)| (token_id.to_string(), fee.to_string())) + .collect(), + block_version: src.block_version.to_string(), + }) + } +} diff --git a/full-service/src/json_rpc/v2/models/receiver_receipt.rs b/full-service/src/json_rpc/v2/models/receiver_receipt.rs new file mode 100644 index 000000000..c0f1f8561 --- /dev/null +++ b/full-service/src/json_rpc/v2/models/receiver_receipt.rs @@ -0,0 +1,128 @@ +// Copyright (c) 2020-2021 MobileCoin Inc. + +//! API definition for the ReceiverReceipt object. + +use crate::{json_rpc::v2::models::masked_amount::MaskedAmount, service}; +use mc_crypto_keys::CompressedRistrettoPublic; +use mc_transaction_core::tx::TxOutConfirmationNumber; +use serde_derive::{Deserialize, Serialize}; +use std::convert::TryFrom; + +/// An receipt provided from the sender of a transaction for the receiver to use +/// in order to check the status of a transaction. +/// +/// Note: This should stay in line wth the Receipt defined in external.proto +/// https://github.com/mobilecoinfoundation/mobilecoin/blob/master/api/proto/external.proto#L255 +#[derive(Deserialize, Serialize, Default, Debug, Clone)] +pub struct ReceiverReceipt { + /// The public key of the Txo sent to the recipient. + pub public_key: String, + + /// The confirmation proof for this Txo, which links the sender to this Txo. + pub confirmation: String, + + /// The tombstone block for the transaction. + pub tombstone_block: String, + + /// The amount of the Txo. + /// Note: This value is self-reported by the sender and is unverifiable. + pub amount: MaskedAmount, +} + +impl TryFrom<&service::receipt::ReceiverReceipt> for ReceiverReceipt { + type Error = String; + + fn try_from(src: &service::receipt::ReceiverReceipt) -> Result { + Ok(ReceiverReceipt { + public_key: hex::encode(&mc_util_serial::encode(&src.public_key)), + tombstone_block: src.tombstone_block.to_string(), + confirmation: hex::encode(&mc_util_serial::encode(&src.confirmation)), + amount: MaskedAmount::from(&src.amount), + }) + } +} + +impl TryFrom<&ReceiverReceipt> for service::receipt::ReceiverReceipt { + type Error = String; + + fn try_from(src: &ReceiverReceipt) -> Result { + let txo_public_key: CompressedRistrettoPublic = mc_util_serial::decode( + &hex::decode(&src.public_key) + .map_err(|err| format!("Could not decode hex for txo_public_key: {:?}", err))?, + ) + .map_err(|err| format!("Could not decode txo public key: {:?}", err))?; + + let proof: TxOutConfirmationNumber = mc_util_serial::decode( + &hex::decode(&src.confirmation) + .map_err(|err| format!("Could not decode hex for proof: {:?}", err))?, + ) + .map_err(|err| format!("Could not decode proof: {:?}", err))?; + + let amount = mc_transaction_core::MaskedAmount::try_from(&src.amount) + .map_err(|err| format!("Could not convert amount: {:?}", err))?; + + Ok(service::receipt::ReceiverReceipt { + public_key: txo_public_key, + tombstone_block: src + .tombstone_block + .parse::() + .map_err(|err| format!("Could not parse u64: {:?}", err))?, + confirmation: proof, + amount, + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use mc_account_keys::AccountKey; + use mc_crypto_keys::{RistrettoPrivate, RistrettoPublic}; + use mc_crypto_rand::RngCore; + use mc_transaction_core::{tokens::Mob, tx::TxOut, Amount, Token}; + use mc_transaction_types::BlockVersion; + use mc_util_from_random::FromRandom; + use rand::{rngs::StdRng, SeedableRng}; + + #[test] + fn test_rpc_receipt_round_trip() { + let mut rng: StdRng = SeedableRng::from_seed([20u8; 32]); + + let account_key = AccountKey::random(&mut rng); + let public_address = account_key.default_subaddress(); + let txo = TxOut::new( + BlockVersion::MAX, + Amount::new(rng.next_u64(), Mob::ID), + &public_address, + &RistrettoPrivate::from_random(&mut rng), + Default::default(), + ) + .expect("Could not make TxOut"); + let tombstone = rng.next_u64(); + let mut proof_bytes = [0u8; 32]; + rng.fill_bytes(&mut proof_bytes); + let confirmation_number = TxOutConfirmationNumber::from(proof_bytes); + let amount = mc_transaction_core::MaskedAmount::new( + BlockVersion::MAX, + Amount::new(rng.next_u64(), Mob::ID), + &RistrettoPublic::from_random(&mut rng), + ) + .expect("Could not create amount"); + + let service_receipt = service::receipt::ReceiverReceipt { + public_key: txo.public_key, + tombstone_block: tombstone, + confirmation: confirmation_number, + amount, + }; + + let json_rpc_receipt = ReceiverReceipt::try_from(&service_receipt) + .expect("Could not get json receipt from service receipt"); + + let service_receipt_from_json = + service::receipt::ReceiverReceipt::try_from(&json_rpc_receipt) + .expect("Could not get receipt from json"); + + assert_eq!(service_receipt, service_receipt_from_json); + } +} diff --git a/full-service/src/json_rpc/v2/models/transaction_log.rs b/full-service/src/json_rpc/v2/models/transaction_log.rs new file mode 100644 index 000000000..1dedf7876 --- /dev/null +++ b/full-service/src/json_rpc/v2/models/transaction_log.rs @@ -0,0 +1,147 @@ +// Copyright (c) 2020-2021 MobileCoin Inc. + +//! API definition for the TransactionLog object. + +use std::collections::BTreeMap; + +use mc_common::HashMap; +use serde::{Deserialize, Serialize}; + +use crate::{ + db, + db::transaction_log::{AssociatedTxos, TransactionLogModel, ValueMap}, +}; + +use super::amount::Amount; + +#[derive(Deserialize, Serialize, Default, Debug, Clone)] +pub struct TransactionLogMap(pub BTreeMap); + +/// A log of a transaction that occurred on the MobileCoin network, constructed +/// and/or submitted from an account in this wallet. +#[derive(Deserialize, Serialize, Default, Debug, Clone)] +pub struct TransactionLog { + /// Unique identifier for the transaction log. This value is not associated + /// to the ledger, but derived from the tx. + pub id: String, + + /// Unique identifier for the assigned associated account. If the + /// transaction is outgoing, this account is from whence the txo came. If + /// received, this is the receiving account. + pub account_id: String, + + /// A list of the Txos which were inputs to this transaction. + pub input_txos: Vec, + + /// A list of the Txos which were outputs from this transaction. + pub output_txos: Vec, + + /// A list of the Txos which were change in this transaction. + pub change_txos: Vec, + + pub value_map: HashMap, + + pub fee_amount: Amount, + + /// The block index of the highest block on the network at the time the + /// transaction was submitted. + pub submitted_block_index: Option, + + pub tombstone_block_index: Option, + + /// The scanned block block index in which this transaction occurred. + pub finalized_block_index: Option, + + /// String representing the transaction log status. On "sent", valid + /// statuses are "built", "pending", "succeeded", "failed". On "received", + /// the status is "succeeded". + pub status: String, + + /// Time at which sent transaction log was created. Only available if + /// direction is "sent". This value is null if "received" or if the sent + /// transactions were recovered from the ledger (is_sent_recovered = true). + pub sent_time: Option, + + /// An arbitrary string attached to the object. + pub comment: String, +} + +impl TransactionLog { + pub fn new( + transaction_log: &db::models::TransactionLog, + associated_txos: &AssociatedTxos, + value_map: &ValueMap, + ) -> Self { + let values = value_map + .0 + .iter() + .map(|(k, v)| (k.to_string(), v.to_string())) + .collect(); + + Self { + id: transaction_log.id.clone(), + account_id: transaction_log.account_id.clone(), + submitted_block_index: transaction_log + .submitted_block_index + .map(|b| (b as u64).to_string()), + tombstone_block_index: transaction_log + .tombstone_block_index + .map(|b| (b as u64).to_string()), + finalized_block_index: transaction_log + .finalized_block_index + .map(|b| (b as u64).to_string()), + status: transaction_log.status().to_string(), + input_txos: associated_txos.inputs.iter().map(InputTxo::new).collect(), + output_txos: associated_txos + .outputs + .iter() + .map(|(txo, recipient)| OutputTxo::new(txo, recipient.to_string())) + .collect(), + change_txos: associated_txos + .change + .iter() + .map(|(txo, recipient)| OutputTxo::new(txo, recipient.to_string())) + .collect(), + value_map: values, + fee_amount: Amount::from(&transaction_log.fee_amount()), + sent_time: None, + comment: transaction_log.comment.clone(), + } + } +} + +#[derive(Deserialize, Serialize, Default, Debug, Clone)] +pub struct InputTxo { + pub txo_id: String, + + /// Amount of this Txo + pub amount: Amount, +} + +impl InputTxo { + pub fn new(txo: &db::models::Txo) -> Self { + Self { + txo_id: txo.id.clone(), + amount: Amount::from(&txo.amount()), + } + } +} + +#[derive(Deserialize, Serialize, Default, Debug, Clone)] +pub struct OutputTxo { + pub txo_id_hex: String, + + pub amount: Amount, + + pub recipient_public_address_b58: String, +} + +impl OutputTxo { + pub fn new(txo: &db::models::Txo, recipient_public_address_b58: String) -> Self { + Self { + txo_id_hex: txo.id.clone(), + amount: Amount::from(&txo.amount()), + recipient_public_address_b58, + } + } +} diff --git a/full-service/src/json_rpc/v2/models/tx_proposal.rs b/full-service/src/json_rpc/v2/models/tx_proposal.rs new file mode 100644 index 000000000..84864809c --- /dev/null +++ b/full-service/src/json_rpc/v2/models/tx_proposal.rs @@ -0,0 +1,172 @@ +// Copyright (c) 2020-2021 MobileCoin Inc. + +//! API definition for the TxProposal object. + +use super::amount::Amount as AmountJSON; +use crate::util::b58::{b58_encode_public_address, B58Error}; + +use protobuf::Message; +use serde_derive::{Deserialize, Serialize}; +use std::convert::TryFrom; + +#[derive(Deserialize, Serialize, Default, Debug)] +pub struct UnsignedInputTxo { + pub tx_out_proto: String, + pub amount: AmountJSON, + pub subaddress_index: String, +} + +#[derive(Deserialize, Serialize, Default, Debug)] +pub struct InputTxo { + pub tx_out_proto: String, + pub amount: AmountJSON, + pub subaddress_index: String, + pub key_image: String, +} + +#[derive(Deserialize, Serialize, Default, Debug)] +pub struct OutputTxo { + pub tx_out_proto: String, + pub amount: AmountJSON, + pub recipient_public_address_b58: String, + pub confirmation_number: String, +} + +#[derive(Deserialize, Serialize, Debug)] +pub struct UnsignedTxProposal { + pub unsigned_tx_proto_bytes_hex: String, + pub unsigned_input_txos: Vec, + pub payload_txos: Vec, + pub change_txos: Vec, +} + +impl TryFrom for UnsignedTxProposal { + type Error = String; + + fn try_from( + src: crate::service::models::tx_proposal::UnsignedTxProposal, + ) -> Result { + let unsigned_input_txos = src + .unsigned_input_txos + .iter() + .map(|input_txo| UnsignedInputTxo { + tx_out_proto: hex::encode(mc_util_serial::encode(&input_txo.tx_out)), + amount: AmountJSON::from(&input_txo.amount), + subaddress_index: input_txo.subaddress_index.to_string(), + }) + .collect(); + + let payload_txos = src + .payload_txos + .iter() + .map(|output_txo| { + Ok(OutputTxo { + tx_out_proto: hex::encode(mc_util_serial::encode(&output_txo.tx_out)), + amount: AmountJSON::from(&output_txo.amount), + recipient_public_address_b58: b58_encode_public_address( + &output_txo.recipient_public_address, + )?, + confirmation_number: hex::encode(output_txo.confirmation_number.as_ref()), + }) + }) + .collect::, B58Error>>() + .map_err(|_| "Error".to_string())?; + + let change_txos = src + .change_txos + .iter() + .map(|output_txo| { + Ok(OutputTxo { + tx_out_proto: hex::encode(mc_util_serial::encode(&output_txo.tx_out)), + amount: AmountJSON::from(&output_txo.amount), + recipient_public_address_b58: b58_encode_public_address( + &output_txo.recipient_public_address, + )?, + confirmation_number: hex::encode(output_txo.confirmation_number.as_ref()), + }) + }) + .collect::, B58Error>>() + .map_err(|_| "Error".to_string())?; + + let unsigned_tx_external: mc_api::external::UnsignedTx = (&src.unsigned_tx).into(); + let unsigned_tx_proto_bytes = unsigned_tx_external + .write_to_bytes() + .map_err(|e| e.to_string())?; + let unsigned_tx_proto_bytes_hex = hex::encode(unsigned_tx_proto_bytes.as_slice()); + + Ok(Self { + unsigned_tx_proto_bytes_hex, + unsigned_input_txos, + payload_txos, + change_txos, + }) + } +} + +#[derive(Deserialize, Serialize, Default, Debug)] +pub struct TxProposal { + pub input_txos: Vec, + pub payload_txos: Vec, + pub change_txos: Vec, + pub fee_amount: AmountJSON, + pub tombstone_block_index: String, + pub tx_proto: String, +} + +impl TryFrom<&crate::service::models::tx_proposal::TxProposal> for TxProposal { + type Error = String; + + fn try_from(src: &crate::service::models::tx_proposal::TxProposal) -> Result { + let input_txos = src + .input_txos + .iter() + .map(|input_txo| InputTxo { + tx_out_proto: hex::encode(mc_util_serial::encode(&input_txo.tx_out)), + amount: AmountJSON::from(&input_txo.amount), + subaddress_index: input_txo.subaddress_index.to_string(), + key_image: hex::encode(&input_txo.key_image.as_bytes()), + }) + .collect(); + + let payload_txos = src + .payload_txos + .iter() + .map(|output_txo| { + Ok(OutputTxo { + tx_out_proto: hex::encode(mc_util_serial::encode(&output_txo.tx_out)), + amount: AmountJSON::from(&output_txo.amount), + recipient_public_address_b58: b58_encode_public_address( + &output_txo.recipient_public_address, + )?, + confirmation_number: hex::encode(output_txo.confirmation_number.as_ref()), + }) + }) + .collect::, B58Error>>() + .map_err(|_| "Error".to_string())?; + + let change_txos = src + .change_txos + .iter() + .map(|output_txo| { + Ok(OutputTxo { + tx_out_proto: hex::encode(mc_util_serial::encode(&output_txo.tx_out)), + amount: AmountJSON::from(&output_txo.amount), + recipient_public_address_b58: b58_encode_public_address( + &output_txo.recipient_public_address, + )?, + confirmation_number: hex::encode(output_txo.confirmation_number.as_ref()), + }) + }) + .collect::, B58Error>>() + .map_err(|_| "Error".to_string())?; + + Ok(Self { + input_txos, + payload_txos, + change_txos, + tx_proto: hex::encode(mc_util_serial::encode(&src.tx)), + fee_amount: AmountJSON::new(src.tx.prefix.fee, src.tx.prefix.fee_token_id.into()), + tombstone_block_index: src.tx.prefix.tombstone_block.to_string(), + }) + } +} diff --git a/full-service/src/json_rpc/v2/models/txo.rs b/full-service/src/json_rpc/v2/models/txo.rs new file mode 100644 index 000000000..b27a91b77 --- /dev/null +++ b/full-service/src/json_rpc/v2/models/txo.rs @@ -0,0 +1,139 @@ +// Copyright (c) 2020-2021 MobileCoin Inc. + +//! API definition for the Txo object. + +use std::collections::BTreeMap; + +use crate::{db, db::txo::TxoStatus}; +use serde_derive::{Deserialize, Serialize}; + +#[derive(Deserialize, Serialize, Default, Debug, Clone)] +pub struct TxoMap(pub BTreeMap); + +/// An Txo in the wallet. +/// +/// An Txo is associated with one or two accounts, and can be categorized with +/// different statuses and types in relation to those accounts. +#[derive(Deserialize, Serialize, Default, Debug, Clone)] +pub struct Txo { + /// Unique identifier for the Txo. Constructed from the contents of the + /// TxOut in the ledger representation. + pub id: String, + + /// the txo's value + pub value: String, + + /// the txo's token id + pub token_id: String, + + /// Block index in which the txo was received by an account. + pub received_block_index: Option, + + /// Block index in which the txo was spent by an account. + pub spent_block_index: Option, + + /// The account_id for the account which has received this TXO. This account + /// has spend authority. + pub account_id: Option, + + /// The status of this txo + pub status: String, + + /// A cryptographic key for this Txo. + pub target_key: String, + + /// The public key for this txo, can be used as an identifier to find the + /// txo in the ledger. + pub public_key: String, + + /// The encrypted fog hint for this Txo. + pub e_fog_hint: String, + + /// The assigned subaddress index for this Txo with respect to its received + /// account. + pub subaddress_index: Option, + + /// A fingerprint of the txo derived from your private spend key materials, + /// required to spend a Txo. + pub key_image: Option, + + /// A confirmation number that the sender of the Txo can provide to verify + /// that they participated in the construction of this Txo. + pub confirmation: Option, +} + +impl Txo { + pub fn new(txo: &db::models::Txo, status: &TxoStatus) -> Txo { + Txo { + id: txo.id.clone(), + value: (txo.value as u64).to_string(), + token_id: (txo.token_id as u64).to_string(), + received_block_index: txo.received_block_index.map(|x| (x as u64).to_string()), + spent_block_index: txo.spent_block_index.map(|x| (x as u64).to_string()), + account_id: txo.account_id.clone(), + status: status.to_string(), + target_key: hex::encode(&txo.target_key), + public_key: hex::encode(&txo.public_key), + e_fog_hint: hex::encode(&txo.e_fog_hint), + subaddress_index: txo.subaddress_index.map(|s| (s as u64).to_string()), + key_image: txo.key_image.as_ref().map(|k| hex::encode(&k)), + confirmation: txo.shared_secret.as_ref().map(hex::encode), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{ + db, + db::{account::AccountModel, models::Account, txo::TxoModel}, + test_utils::{create_test_received_txo, WalletDbTestContext, MOB}, + }; + use mc_account_keys::{AccountKey, RootIdentity}; + use mc_common::logger::{test_with_logger, Logger}; + use mc_transaction_core::{tokens::Mob, Amount, Token}; + use mc_util_from_random::FromRandom; + use rand::{rngs::StdRng, SeedableRng}; + + #[test_with_logger] + fn test_display_txo_in_origin(logger: Logger) { + let mut rng: StdRng = SeedableRng::from_seed([20u8; 32]); + + let db_test_context = WalletDbTestContext::default(); + let wallet_db = db_test_context.get_db_instance(logger); + + let root_id = RootIdentity::from_random(&mut rng); + let account_key = AccountKey::from(&root_id); + let (_account_id_hex, _public_address_b58) = Account::create_from_root_entropy( + &root_id.root_entropy, + Some(1), + None, + None, + "Alice's Main Account", + "".to_string(), + "".to_string(), + "".to_string(), + &wallet_db.get_conn().unwrap(), + ) + .unwrap(); + + // Amount in origin block TXO is 250_000_000 MOB / 16 + let (txo_hex, _txo, _key_image) = create_test_received_txo( + &account_key, + 0, + Amount::new(15_625_000 * MOB, Mob::ID), + 0, + &mut rng, + &wallet_db, + ); + + let txo_details = db::models::Txo::get(&txo_hex, &wallet_db.get_conn().unwrap()) + .expect("Could not get Txo"); + let status = txo_details.status(&wallet_db.get_conn().unwrap()).unwrap(); + assert_eq!(txo_details.value as u64, 15_625_000 * MOB as u64); + let json_txo = Txo::new(&txo_details, &status); + assert_eq!(json_txo.value, "15625000000000000000"); + assert_eq!(json_txo.token_id, "0"); + } +} diff --git a/full-service/src/json_rpc/v2/models/wallet_status.rs b/full-service/src/json_rpc/v2/models/wallet_status.rs new file mode 100644 index 000000000..d8cb366f4 --- /dev/null +++ b/full-service/src/json_rpc/v2/models/wallet_status.rs @@ -0,0 +1,48 @@ +// Copyright (c) 2020-2021 MobileCoin Inc. + +//! API definition for the Wallet Status object. + +use crate::{json_rpc::v2::models::balance::Balance, service}; + +use serde_derive::{Deserialize, Serialize}; +use std::{collections::BTreeMap, convert::TryFrom}; + +/// The status of the wallet, including the sum of the balances for all +/// accounts. +#[derive(Deserialize, Serialize, Default, Debug, Clone)] +pub struct WalletStatus { + /// The block count of MobileCoin's distributed ledger. + pub network_block_height: String, + + /// The local block count downloaded from the ledger. The local database + /// is synced when the local_block_height reaches the network_block_height. + /// The account_block_height can only sync up to local_block_height. + pub local_block_height: String, + + /// Whether ALL accounts are synced up to the network_block_height. Balances + /// may not appear correct if any account is still syncing. + pub is_synced_all: bool, + + /// The minimum synced block across all accounts + pub min_synced_block_index: String, + + pub balance_per_token: BTreeMap, +} + +impl TryFrom<&service::balance::WalletStatus> for WalletStatus { + type Error = String; + + fn try_from(src: &service::balance::WalletStatus) -> Result { + Ok(WalletStatus { + network_block_height: src.network_block_height.to_string(), + local_block_height: src.local_block_height.to_string(), + is_synced_all: src.min_synced_block_index + 1 >= src.network_block_height, + min_synced_block_index: src.min_synced_block_index.to_string(), + balance_per_token: src + .balance_per_token + .iter() + .map(|(k, v)| (k.to_string(), Balance::from(v))) + .collect(), + }) + } +} diff --git a/full-service/src/json_rpc/view_only_account.rs b/full-service/src/json_rpc/view_only_account.rs deleted file mode 100644 index 3c7188120..000000000 --- a/full-service/src/json_rpc/view_only_account.rs +++ /dev/null @@ -1,146 +0,0 @@ -// Copyright (c) 2020-2022 MobileCoin Inc. - -//! API definition for the View Only Account object. - -use crate::{ - db, - json_rpc::{ - json_rpc_request::{JsonCommandRequest, JsonRPCRequest}, - view_only_subaddress::ViewOnlySubaddressJSON, - }, - util::encoding_helpers::ristretto_to_hex, -}; -use serde_derive::{Deserialize, Serialize}; -use std::convert::TryFrom; - -/// An view-only-account in the wallet. -/// -/// A view only account is associated with one private view key -#[derive(Deserialize, Serialize, Default, Debug, Clone)] -pub struct ViewOnlyAccountJSON { - /// String representing the object's type. Objects of the same type share - /// the same value. - pub object: String, - - /// Display name for the account. - pub account_id: String, - - /// Display name for the account. - pub name: String, - - /// Index of the first block when this account may have received funds. - /// No transactions before this point will be synchronized. - pub first_block_index: String, - - /// Index of the next block this account needs to sync. - pub next_block_index: String, - - pub main_subaddress_index: String, - - pub change_subaddress_index: String, - - pub next_subaddress_index: String, -} - -impl From<&db::models::ViewOnlyAccount> for ViewOnlyAccountJSON { - fn from(src: &db::models::ViewOnlyAccount) -> ViewOnlyAccountJSON { - ViewOnlyAccountJSON { - object: "view_only_account".to_string(), - name: src.name.clone(), - account_id: src.account_id_hex.clone(), - first_block_index: (src.first_block_index as u64).to_string(), - next_block_index: (src.next_block_index as u64).to_string(), - main_subaddress_index: (src.main_subaddress_index as u64).to_string(), - change_subaddress_index: (src.change_subaddress_index as u64).to_string(), - next_subaddress_index: (src.next_subaddress_index as u64).to_string(), - } - } -} - -impl From<&db::models::Account> for ViewOnlyAccountJSON { - fn from(src: &db::models::Account) -> ViewOnlyAccountJSON { - ViewOnlyAccountJSON { - object: "view_only_account".to_string(), - name: src.name.clone(), - account_id: src.account_id_hex.clone(), - first_block_index: (src.first_block_index as u64).to_string(), - next_block_index: (src.next_block_index as u64).to_string(), - main_subaddress_index: (src.main_subaddress_index as u64).to_string(), - change_subaddress_index: (src.change_subaddress_index as u64).to_string(), - next_subaddress_index: (src.next_subaddress_index as u64).to_string(), - } - } -} - -/// private view key for the account -#[derive(Deserialize, Serialize, Default, Debug, Clone)] -pub struct ViewOnlyAccountSecretsJSON { - /// The private key used for viewing transactions for this account - pub object: String, - pub view_private_key: String, - pub account_id: String, -} - -impl TryFrom<&db::models::ViewOnlyAccount> for ViewOnlyAccountSecretsJSON { - type Error = String; - - fn try_from(src: &db::models::ViewOnlyAccount) -> Result { - Ok(ViewOnlyAccountSecretsJSON { - object: "view_only_account_secrets".to_string(), - account_id: src.account_id_hex.clone(), - view_private_key: hex::encode(src.view_private_key.as_slice()), - }) - } -} - -impl TryFrom<&db::models::Account> for ViewOnlyAccountSecretsJSON { - type Error = String; - - fn try_from(src: &db::models::Account) -> Result { - let account_key: mc_account_keys::AccountKey = mc_util_serial::decode(&src.account_key) - .map_err(|err| format!("Could not decode account key from database: {:?}", err))?; - - Ok(ViewOnlyAccountSecretsJSON { - object: "view_only_account_secrets".to_string(), - account_id: src.account_id_hex.clone(), - view_private_key: ristretto_to_hex(account_key.view_private_key()), - }) - } -} - -impl TryFrom<&db::account::ViewOnlyAccountImportPackage> for JsonRPCRequest { - type Error = String; - - fn try_from(src: &db::account::ViewOnlyAccountImportPackage) -> Result { - let account = ViewOnlyAccountJSON::from(&src.account); - let secrets = ViewOnlyAccountSecretsJSON::try_from(&src.account)?; - let subaddresses = src - .subaddresses - .iter() - .map(ViewOnlySubaddressJSON::from) - .collect(); - - let json_command_request = JsonCommandRequest::import_view_only_account { - account, - secrets, - subaddresses, - }; - - let src_json: serde_json::Value = serde_json::json!(json_command_request); - let method = src_json - .get("method") - .ok_or("missing method")? - .as_str() - .ok_or("could not cast to str")?; - let params = src_json.get("params").ok_or("missing params")?; - - let json_rpc_request = JsonRPCRequest { - method: method.to_string(), - params: Some(params.clone()), - jsonrpc: "2.0".to_string(), - id: serde_json::Value::Number(serde_json::Number::from(1)), - }; - - Ok(json_rpc_request) - } -} diff --git a/full-service/src/json_rpc/view_only_subaddress.rs b/full-service/src/json_rpc/view_only_subaddress.rs deleted file mode 100644 index afca2a2a6..000000000 --- a/full-service/src/json_rpc/view_only_subaddress.rs +++ /dev/null @@ -1,64 +0,0 @@ -// Copyright (c) 2020-2021 MobileCoin Inc. - -//! API definition for the Address object. - -use crate::db::models::{AssignedSubaddress, ViewOnlySubaddress}; -use serde_derive::{Deserialize, Serialize}; - -/// An address for an account in the wallet. -/// -/// An account may have many addresses. This wallet implementation assumes -/// that an address has been "assigned" to an intended sender. In this way -/// the wallet can make sense of the anonymous MobileCoin ledger, by -/// determining the likely sender of the Txo is whomever was given that -/// address to which to send. -#[derive(Deserialize, Serialize, Default, Debug, Clone)] -pub struct ViewOnlySubaddressJSON { - /// String representing the object's type. Objects of the same type share - /// the same value. - pub object: String, - - /// A b58 encoding of the public address materials. - /// - /// The public_address is the unique identifier for the address. - pub public_address: String, - - /// The account which owns this address. - pub account_id: String, - - /// Additional data associated with this address. - pub comment: String, - - /// The index of this address in the subaddress space for the account. - pub subaddress_index: String, - - pub public_spend_key: String, -} - -pub type ViewOnlySubaddressesJSON = Vec; - -impl From<&ViewOnlySubaddress> for ViewOnlySubaddressJSON { - fn from(src: &ViewOnlySubaddress) -> ViewOnlySubaddressJSON { - ViewOnlySubaddressJSON { - object: "address".to_string(), - public_address: src.public_address_b58.clone(), - account_id: src.view_only_account_id_hex.clone(), - comment: src.comment.clone(), - subaddress_index: (src.subaddress_index as u64).to_string(), - public_spend_key: hex::encode(src.public_spend_key.clone()), - } - } -} - -impl From<&AssignedSubaddress> for ViewOnlySubaddressJSON { - fn from(src: &AssignedSubaddress) -> ViewOnlySubaddressJSON { - ViewOnlySubaddressJSON { - object: "view_only_subaddress".to_string(), - public_address: src.assigned_subaddress_b58.clone(), - account_id: src.account_id_hex.clone(), - comment: src.comment.clone(), - subaddress_index: (src.subaddress_index as u64).to_string(), - public_spend_key: hex::encode(src.subaddress_spend_key.clone()), - } - } -} diff --git a/full-service/src/json_rpc/view_only_txo.rs b/full-service/src/json_rpc/view_only_txo.rs deleted file mode 100644 index 6d5567c51..000000000 --- a/full-service/src/json_rpc/view_only_txo.rs +++ /dev/null @@ -1,64 +0,0 @@ -// Copyright (c) 2020-2022 MobileCoin Inc. - -//! API definition for the Txo object. - -use crate::db; -use serde_derive::{Deserialize, Serialize}; - -/// An View Only Txo in the wallet. -#[derive(Deserialize, Serialize, Default, Debug, Clone)] -pub struct ViewOnlyTxo { - /// String representing the object's type. Objects of the same type share - /// the same value. - pub object: String, - - /// Unique identifier for the Txo. Constructed from the contents of the - /// TxOut in the ledger representation. - pub txo_id_hex: String, - - /// A fingerprint of the txo derived from your private spend key materials, - /// required to spend a Txo. - pub key_image: Option, - - pub subaddress_index: Option, - - /// Available pico MOB for this account at the current account_block_height. - /// If the account is syncing, this value may change. - pub value_pmob: String, - - /// The public key for this txo, can be used as an identifier to find the - /// txo in the ledger. - pub public_key: String, - - /// the view-only-account id for this txo - pub view_only_account_id_hex: String, - - pub submitted_block_index: Option, - - pub pending_tombstone_block_index: Option, - - pub received_block_index: Option, - - pub spent_block_index: Option, -} - -impl From<&db::models::ViewOnlyTxo> for ViewOnlyTxo { - fn from(txo: &db::models::ViewOnlyTxo) -> ViewOnlyTxo { - ViewOnlyTxo { - object: "view_only_txo".to_string(), - txo_id_hex: txo.txo_id_hex.clone(), - key_image: txo.key_image.as_ref().map(|k| hex::encode(&k)), - subaddress_index: txo.subaddress_index.as_ref().map(|i| i.to_string()), - value_pmob: (txo.value as u64).to_string(), - public_key: hex::encode(&txo.public_key), - view_only_account_id_hex: txo.view_only_account_id_hex.to_string(), - submitted_block_index: txo.submitted_block_index.as_ref().map(|i| i.to_string()), - pending_tombstone_block_index: txo - .pending_tombstone_block_index - .as_ref() - .map(|i| i.to_string()), - received_block_index: txo.received_block_index.as_ref().map(|i| i.to_string()), - spent_block_index: txo.spent_block_index.as_ref().map(|i| i.to_string()), - } - } -} diff --git a/full-service/src/json_rpc/wallet.rs b/full-service/src/json_rpc/wallet.rs index fc8cf19b7..55b4ca36b 100644 --- a/full-service/src/json_rpc/wallet.rs +++ b/full-service/src/json_rpc/wallet.rs @@ -3,65 +3,32 @@ //! Entrypoint for Wallet API. use crate::{ - db::{self, account::AccountID, transaction_log::TransactionID, txo::TxoID}, - json_rpc, json_rpc::{ - account_secrets::AccountSecrets, - address::Address, - balance::Balance, - block::{Block, BlockContents}, - confirmation_number::Confirmation, - gift_code::GiftCode, - json_rpc_request::{help_str, JsonCommandRequest, JsonRPCRequest}, - json_rpc_response::{ - format_error, format_invalid_request_error, JsonCommandResponse, JsonRPCError, - JsonRPCResponse, + json_rpc_request::JsonRPCRequest, + json_rpc_response::JsonRPCResponse, + v1::api::{ + request::help_str as help_str_v1, + response::JsonCommandResponse as JsonCommandResponse_v1, + wallet::generic_wallet_api as generic_wallet_api_v1, + }, + v2::api::{ + request::help_str as help_str_v2, + response::JsonCommandResponse as JsonCommandResponse_v2, + wallet::generic_wallet_api as generic_wallet_api_v2, }, - network_status::NetworkStatus, - receiver_receipt::ReceiverReceipt, - tx_proposal::TxProposal, - txo::Txo, - view_only_subaddress::ViewOnlySubaddressJSON, - view_only_txo::ViewOnlyTxo, - wallet_status::WalletStatus, - }, - service, - service::{ - account::AccountService, - address::AddressService, - balance::BalanceService, - confirmation_number::ConfirmationService, - gift_code::{EncodedGiftCode, GiftCodeService}, - ledger::LedgerService, - payment_request::PaymentRequestService, - receipt::ReceiptService, - transaction::TransactionService, - transaction_log::TransactionLogService, - txo::TxoService, - view_only_account::ViewOnlyAccountService, - view_only_txo::ViewOnlyTxoService, - WalletService, - }, - util::b58::{ - b58_decode_payment_request, b58_encode_public_address, b58_printable_wrapper_type, - PrintableWrapperType, }, + service::WalletService, }; -use mc_common::logger::global_log; use mc_connection::{ BlockchainConnection, HardcodedCredentialsProvider, ThickClient, UserTxConnection, }; -use mc_crypto_keys::{RistrettoPrivate, RistrettoPublic}; -use mc_fog_report_validation::{FogPubkeyResolver, FogResolver}; -use mc_mobilecoind_json::data_types::{JsonTx, JsonTxOut}; -use mc_transaction_core::ring_signature::KeyImage; +use mc_fog_report_resolver::FogResolver; +use mc_fog_report_validation::FogPubkeyResolver; use mc_validator_connection::ValidatorConnection; use rocket::{ self, get, http::Status, outcome::Outcome, post, request::FromRequest, routes, Request, State, }; use rocket_contrib::json::Json; -use serde_json::Map; -use std::{collections::HashMap, convert::TryFrom, iter::FromIterator}; /// State managed by rocket. pub struct WalletState< @@ -104,1176 +71,57 @@ impl<'a, 'r> FromRequest<'a, 'r> for ApiKeyGuard { } } -fn generic_wallet_api( - _api_key_guard: ApiKeyGuard, - state: rocket::State>, - command: Json, -) -> Result, String> -where - T: BlockchainConnection + UserTxConnection + 'static, - FPR: FogPubkeyResolver + Send + Sync + 'static, -{ - let req: JsonRPCRequest = command.0.clone(); - - let mut response = JsonRPCResponse { - method: Some(command.0.method), - result: None, - error: None, - jsonrpc: "2.0".to_string(), - id: command.0.id, - }; - - let request = match JsonCommandRequest::try_from(&req) { - Ok(request) => request, - Err(error) => { - response.error = Some(format_invalid_request_error(error)); - return Ok(Json(response)); - } - }; - - match wallet_api_inner(&state.service, request) { - Ok(command_response) => { - response.result = Some(command_response); - } - Err(rpc_error) => { - response.error = Some(rpc_error); - } - }; +#[get("/health")] +fn health() -> Result<(), ()> { + Ok(()) +} - Ok(Json(response)) +#[get("/wallet")] +fn wallet_help_v1() -> Result { + Ok(help_str_v1()) } /// The route for the Full Service Wallet API. #[post("/wallet", format = "json", data = "")] -pub fn consensus_backed_wallet_api( +fn consensus_backed_wallet_api_v1( _api_key_guard: ApiKeyGuard, state: rocket::State, FogResolver>>, command: Json, -) -> Result, String> { - generic_wallet_api(_api_key_guard, state, command) +) -> Result>, String> { + generic_wallet_api_v1(_api_key_guard, state, command) } #[post("/wallet", format = "json", data = "")] -pub fn validator_backed_wallet_api( +fn validator_backed_wallet_api_v1( _api_key_guard: ApiKeyGuard, state: rocket::State>, command: Json, -) -> Result, String> { - generic_wallet_api(_api_key_guard, state, command) -} - -/// The Wallet API inner method, which handles switching on the method enum. -/// -/// Note that this is structured this way so that the routes can be defined to -/// take explicit Rocket state, and then pass the service to the inner method. -/// This allows us to properly construct state with Mock Connection Objects in -/// tests. This also allows us to version the overall API easily. -pub fn wallet_api_inner( - service: &WalletService, - command: JsonCommandRequest, -) -> Result -where - T: BlockchainConnection + UserTxConnection + 'static, - FPR: FogPubkeyResolver + Send + Sync + 'static, -{ - global_log::trace!("Running command {:?}", command); - - let response = match command { - JsonCommandRequest::assign_address_for_account { - account_id, - metadata, - } => JsonCommandResponse::assign_address_for_account { - address: Address::from( - &service - .assign_address_for_account(&AccountID(account_id), metadata.as_deref()) - .map_err(format_error)?, - ), - }, - JsonCommandRequest::build_and_submit_transaction { - account_id, - addresses_and_values, - recipient_public_address, - value_pmob, - input_txo_ids, - fee, - tombstone_block, - max_spendable_value, - comment, - } => { - // The user can specify either a single address and a single value, or a list of - // addresses and values. - let mut addresses_and_values = addresses_and_values.unwrap_or_default(); - if let (Some(a), Some(v)) = (recipient_public_address, value_pmob) { - addresses_and_values.push((a, v)); - } - let (transaction_log, associated_txos, tx_proposal) = service - .build_and_submit( - &account_id, - &addresses_and_values, - input_txo_ids.as_ref(), - fee, - tombstone_block, - max_spendable_value, - comment, - ) - .map_err(format_error)?; - JsonCommandResponse::build_and_submit_transaction { - transaction_log: json_rpc::transaction_log::TransactionLog::new( - &transaction_log, - &associated_txos, - ), - tx_proposal: TxProposal::try_from(&tx_proposal).map_err(format_error)?, - } - } - JsonCommandRequest::build_gift_code { - account_id, - value_pmob, - memo, - input_txo_ids, - fee, - tombstone_block, - max_spendable_value, - } => { - let (tx_proposal, gift_code_b58) = service - .build_gift_code( - &AccountID(account_id), - value_pmob.parse::().map_err(format_error)?, - memo, - input_txo_ids.as_ref(), - fee.map(|f| f.parse::()) - .transpose() - .map_err(format_error)?, - tombstone_block - .map(|t| t.parse::()) - .transpose() - .map_err(format_error)?, - max_spendable_value - .map(|m| m.parse::()) - .transpose() - .map_err(format_error)?, - ) - .map_err(format_error)?; - JsonCommandResponse::build_gift_code { - tx_proposal: TxProposal::try_from(&tx_proposal).map_err(format_error)?, - gift_code_b58: gift_code_b58.to_string(), - } - } - JsonCommandRequest::build_split_txo_transaction { - txo_id, - output_values, - destination_subaddress_index, - fee, - tombstone_block, - } => { - let tx_proposal = service - .split_txo( - &TxoID(txo_id), - &output_values, - destination_subaddress_index - .map(|f| f.parse::()) - .transpose() - .map_err(format_error)?, - fee, - tombstone_block, - ) - .map_err(format_error)?; - JsonCommandResponse::build_split_txo_transaction { - tx_proposal: TxProposal::try_from(&tx_proposal).map_err(format_error)?, - transaction_log_id: TransactionID::from(&tx_proposal.tx).to_string(), - } - } - JsonCommandRequest::build_transaction { - account_id, - addresses_and_values, - recipient_public_address, - value_pmob, - input_txo_ids, - fee, - tombstone_block, - max_spendable_value, - log_tx_proposal, - } => { - // The user can specify a list of addresses and values, - // or a single address and a single value (deprecated). - let mut addresses_and_values = addresses_and_values.unwrap_or_default(); - if let (Some(a), Some(v)) = (recipient_public_address, value_pmob) { - addresses_and_values.push((a, v)); - } - let tx_proposal = service - .build_transaction( - &account_id, - &addresses_and_values, - input_txo_ids.as_ref(), - fee, - tombstone_block, - max_spendable_value, - log_tx_proposal, - ) - .map_err(format_error)?; - JsonCommandResponse::build_transaction { - tx_proposal: TxProposal::try_from(&tx_proposal).map_err(format_error)?, - transaction_log_id: TransactionID::from(&tx_proposal.tx).to_string(), - } - } - JsonCommandRequest::build_unsigned_transaction { - account_id, - recipient_public_address, - value_pmob, - fee, - tombstone_block, - } => { - let mut addresses_and_values: Vec<(String, String)> = Vec::new(); - if let (Some(a), Some(v)) = (recipient_public_address, value_pmob) { - addresses_and_values.push((a, v)); - } - let (unsigned_tx, fog_resolver) = service - .build_unsigned_transaction( - &account_id, - &addresses_and_values, - fee, - tombstone_block, - ) - .map_err(format_error)?; - JsonCommandResponse::build_unsigned_transaction { - account_id, - unsigned_tx, - fog_resolver, - } - } - JsonCommandRequest::check_b58_type { b58_code } => { - let b58_type = b58_printable_wrapper_type(b58_code.clone()).map_err(format_error)?; - let mut b58_data = HashMap::new(); - match b58_type { - PrintableWrapperType::PublicAddress => { - b58_data.insert("public_address_b58".to_string(), b58_code); - } - PrintableWrapperType::TransferPayload => {} - PrintableWrapperType::PaymentRequest => { - let payment_request = - b58_decode_payment_request(b58_code).map_err(format_error)?; - let public_address_b58 = - b58_encode_public_address(&payment_request.public_address) - .map_err(format_error)?; - b58_data.insert("public_address_b58".to_string(), public_address_b58); - b58_data.insert("value".to_string(), payment_request.value.to_string()); - b58_data.insert("memo".to_string(), payment_request.memo); - } - } - JsonCommandResponse::check_b58_type { - b58_type, - data: b58_data, - } - } - JsonCommandRequest::check_gift_code_status { gift_code_b58 } => { - let (status, value, memo) = service - .check_gift_code_status(&EncodedGiftCode(gift_code_b58)) - .map_err(format_error)?; - JsonCommandResponse::check_gift_code_status { - gift_code_status: status, - gift_code_value: value, - gift_code_memo: memo, - } - } - JsonCommandRequest::check_receiver_receipt_status { - address, - receiver_receipt, - } => { - let receipt = service::receipt::ReceiverReceipt::try_from(&receiver_receipt) - .map_err(format_error)?; - let (status, txo) = service - .check_receipt_status(&address, &receipt) - .map_err(format_error)?; - JsonCommandResponse::check_receiver_receipt_status { - receipt_transaction_status: status, - txo: txo.as_ref().map(Txo::from), - } - } - JsonCommandRequest::claim_gift_code { - gift_code_b58, - account_id, - address, - } => { - let tx = service - .claim_gift_code( - &EncodedGiftCode(gift_code_b58), - &AccountID(account_id), - address, - ) - .map_err(format_error)?; - JsonCommandResponse::claim_gift_code { - txo_id: TxoID::from(&tx.prefix.outputs[0]).to_string(), - } - } - JsonCommandRequest::create_account { - name, - fog_report_url, - fog_report_id, - fog_authority_spki, - } => { - let account: db::models::Account = service - .create_account( - name, - fog_report_url.unwrap_or_default(), - fog_report_id.unwrap_or_default(), - fog_authority_spki.unwrap_or_default(), - ) - .map_err(format_error)?; - - JsonCommandResponse::create_account { - account: json_rpc::account::Account::try_from(&account).map_err(|e| { - format_error(format!("Could not get RPC Account from DB Account {:?}", e)) - })?, - } - } - JsonCommandRequest::create_new_subaddresses_request { - account_id, - num_subaddresses_to_generate, - } => { - let account = service - .get_view_only_account(&account_id) - .map_err(format_error)?; - - JsonCommandResponse::create_new_subaddresses_request { - account_id, - next_subaddress_index: (account.next_subaddress_index as u64).to_string(), - num_subaddresses_to_generate, - } - } - JsonCommandRequest::create_payment_request { - account_id, - subaddress_index, - amount_pmob, - memo, - } => JsonCommandResponse::create_payment_request { - payment_request_b58: service - .create_payment_request(account_id, subaddress_index, amount_pmob, memo) - .map_err(format_error)?, - }, - JsonCommandRequest::create_receiver_receipts { tx_proposal } => { - let receipts = service - .create_receiver_receipts( - &mc_mobilecoind::payments::TxProposal::try_from(&tx_proposal) - .map_err(format_error)?, - ) - .map_err(format_error)?; - let json_receipts: Vec = receipts - .iter() - .map(ReceiverReceipt::try_from) - .collect::, String>>() - .map_err(format_error)?; - JsonCommandResponse::create_receiver_receipts { - receiver_receipts: json_receipts, - } - } - JsonCommandRequest::create_view_only_account_sync_request { account_id } => { - let incomplete_txos = service - .list_incomplete_view_only_txos(&account_id) - .map_err(format_error)?; - - let incomplete_txos_encoded: Vec = incomplete_txos - .iter() - .map(|txo| hex::encode(mc_util_serial::encode(txo))) - .collect(); - - JsonCommandResponse::create_view_only_account_sync_request { - account_id, - incomplete_txos_encoded, - } - } - JsonCommandRequest::export_account_secrets { account_id } => { - let account = service - .get_account(&AccountID(account_id)) - .map_err(format_error)?; - JsonCommandResponse::export_account_secrets { - account_secrets: AccountSecrets::try_from(&account).map_err(format_error)?, - } - } - JsonCommandRequest::export_spent_txo_ids { account_id } => { - let txos = service - .list_spent_txos(&AccountID(account_id)) - .map_err(format_error)?; - let spent_txo_ids: Vec = txos - .iter() - .map(|txo| txo.txo_id_hex.clone()) - .collect::>(); - - JsonCommandResponse::export_spent_txo_ids { spent_txo_ids } - } - JsonCommandRequest::export_view_only_account_package { account_id } => { - let package = service - .get_view_only_import_package(&AccountID(account_id)) - .map_err(format_error)?; - - let json_rpc_request = JsonRPCRequest::try_from(&package).map_err(format_error)?; - - JsonCommandResponse::export_view_only_account_package { json_rpc_request } - } - JsonCommandRequest::export_view_only_account_secrets { account_id } => { - let account = service - .get_view_only_account(&account_id) - .map_err(format_error)?; - JsonCommandResponse::export_view_only_account_secrets { - view_only_account_secrets: - json_rpc::view_only_account::ViewOnlyAccountSecretsJSON::try_from(&account) - .map_err(format_error)?, - } - } - JsonCommandRequest::get_account { account_id } => JsonCommandResponse::get_account { - account: json_rpc::account::Account::try_from( - &service - .get_account(&AccountID(account_id)) - .map_err(format_error)?, - ) - .map_err(format_error)?, - }, - JsonCommandRequest::get_account_status { account_id } => { - let account = json_rpc::account::Account::try_from( - &service - .get_account(&AccountID(account_id.clone())) - .map_err(format_error)?, - ) - .map_err(format_error)?; - let balance = Balance::from( - &service - .get_balance_for_account(&AccountID(account_id)) - .map_err(format_error)?, - ); - JsonCommandResponse::get_account_status { account, balance } - } - JsonCommandRequest::get_address_for_account { account_id, index } => { - let assigned_subaddress = service - .get_address_for_account(&AccountID(account_id), index) - .map_err(format_error)?; - JsonCommandResponse::get_address_for_account { - address: Address::from(&assigned_subaddress), - } - } - JsonCommandRequest::get_address_for_view_only_account { account_id, index } => { - let view_only_subaddress = service - .get_address_for_view_only_account(&AccountID(account_id), index as u64) - .map_err(format_error)?; - JsonCommandResponse::get_address_for_view_only_account { - address: ViewOnlySubaddressJSON::from(&view_only_subaddress), - } - } - JsonCommandRequest::get_addresses_for_account { - account_id, - offset, - limit, - } => { - let (o, l) = page_helper(offset, limit)?; - let addresses = service - .get_addresses_for_account(&AccountID(account_id), Some(o), Some(l)) - .map_err(format_error)?; - let address_map: Map = Map::from_iter( - addresses - .iter() - .map(|a| { - ( - a.assigned_subaddress_b58.clone(), - serde_json::to_value(&(Address::from(a))) - .expect("Could not get json value"), - ) - }) - .collect::>(), - ); - - JsonCommandResponse::get_addresses_for_account { - public_addresses: addresses - .iter() - .map(|a| a.assigned_subaddress_b58.clone()) - .collect(), - address_map, - } - } - JsonCommandRequest::get_addresses_for_view_only_account { - account_id, - offset, - limit, - } => { - let (o, l) = page_helper(offset, limit)?; - let addresses = service - .get_addresses_for_view_only_account(&AccountID(account_id), Some(o), Some(l)) - .map_err(format_error)?; - let address_map: Map = Map::from_iter( - addresses - .iter() - .map(|a| { - ( - a.public_address_b58.clone(), - serde_json::to_value(&(Address::from(a))) - .expect("Could not get json value"), - ) - }) - .collect::>(), - ); - - JsonCommandResponse::get_addresses_for_account { - public_addresses: addresses - .iter() - .map(|a| a.public_address_b58.clone()) - .collect(), - address_map, - } - } - JsonCommandRequest::get_all_accounts => { - let accounts = service.list_accounts().map_err(format_error)?; - let json_accounts: Vec<(String, serde_json::Value)> = accounts - .iter() - .map(|a| { - json_rpc::account::Account::try_from(a) - .map_err(format_error) - .and_then(|v| { - serde_json::to_value(v) - .map(|v| (a.account_id_hex.clone(), v)) - .map_err(format_error) - }) - }) - .collect::, JsonRPCError>>()?; - let account_map: Map = Map::from_iter(json_accounts); - JsonCommandResponse::get_all_accounts { - account_ids: accounts.iter().map(|a| a.account_id_hex.clone()).collect(), - account_map, - } - } - JsonCommandRequest::get_all_gift_codes {} => JsonCommandResponse::get_all_gift_codes { - gift_codes: service - .list_gift_codes() - .map_err(format_error)? - .iter() - .map(GiftCode::from) - .collect(), - }, - JsonCommandRequest::get_all_transaction_logs_for_block { block_index } => { - let transaction_logs_and_txos = service - .get_all_transaction_logs_for_block( - block_index.parse::().map_err(format_error)?, - ) - .map_err(format_error)?; - let transaction_log_map: Map = Map::from_iter( - transaction_logs_and_txos - .iter() - .map(|(t, a)| { - ( - t.transaction_id_hex.clone(), - serde_json::json!(json_rpc::transaction_log::TransactionLog::new(t, a)), - ) - }) - .collect::>(), - ); - - JsonCommandResponse::get_all_transaction_logs_for_block { - transaction_log_ids: transaction_logs_and_txos - .iter() - .map(|(t, _a)| t.transaction_id_hex.to_string()) - .collect(), - transaction_log_map, - } - } - JsonCommandRequest::get_all_transaction_logs_ordered_by_block => { - let transaction_logs_and_txos = service - .get_all_transaction_logs_ordered_by_block() - .map_err(format_error)?; - let transaction_log_map: Map = Map::from_iter( - transaction_logs_and_txos - .iter() - .map(|(t, a)| { - ( - t.transaction_id_hex.clone(), - serde_json::json!(json_rpc::transaction_log::TransactionLog::new(t, a)), - ) - }) - .collect::>(), - ); - - JsonCommandResponse::get_all_transaction_logs_ordered_by_block { - transaction_log_map, - } - } - JsonCommandRequest::get_all_txos_for_address { address } => { - let txos = service - .get_all_txos_for_address(&address) - .map_err(format_error)?; - let txo_map: Map = Map::from_iter( - txos.iter() - .map(|t| { - ( - t.txo_id_hex.clone(), - serde_json::to_value(Txo::from(t)).expect("Could not get json value"), - ) - }) - .collect::>(), - ); - - JsonCommandResponse::get_all_txos_for_address { - txo_ids: txos.iter().map(|t| t.txo_id_hex.clone()).collect(), - txo_map, - } - } - JsonCommandRequest::get_all_view_only_accounts => { - let accounts = service.list_view_only_accounts().map_err(format_error)?; - let json_accounts: Vec<(String, serde_json::Value)> = accounts - .iter() - .map(|a| { - json_rpc::view_only_account::ViewOnlyAccountJSON::try_from(a) - .map_err(format_error) - .and_then(|v| { - serde_json::to_value(v) - .map(|v| (a.account_id_hex.clone(), v)) - .map_err(format_error) - }) - }) - .collect::, JsonRPCError>>()?; - let account_map: Map = Map::from_iter(json_accounts); - JsonCommandResponse::get_all_view_only_accounts { - account_ids: accounts.iter().map(|a| a.account_id_hex.clone()).collect(), - account_map, - } - } - JsonCommandRequest::get_balance_for_account { account_id } => { - JsonCommandResponse::get_balance_for_account { - balance: Balance::from( - &service - .get_balance_for_account(&AccountID(account_id)) - .map_err(format_error)?, - ), - } - } - JsonCommandRequest::get_balance_for_address { address } => { - JsonCommandResponse::get_balance_for_address { - balance: Balance::from( - &service - .get_balance_for_address(&address) - .map_err(format_error)?, - ), - } - } - JsonCommandRequest::get_balance_for_view_only_account { account_id } => { - JsonCommandResponse::get_balance_for_view_only_account { - balance: Balance::from( - &service - .get_balance_for_view_only_account(&account_id) - .map_err(format_error)?, - ), - } - } - JsonCommandRequest::get_balance_for_view_only_address { address } => { - JsonCommandResponse::get_balance_for_view_only_address { - balance: Balance::from( - &service - .get_balance_for_view_only_address(&address) - .map_err(format_error)?, - ), - } - } - JsonCommandRequest::get_block { block_index } => { - let (block, block_contents) = service - .get_block_object(block_index.parse::().map_err(format_error)?) - .map_err(format_error)?; - JsonCommandResponse::get_block { - block: Block::new(&block), - block_contents: BlockContents::new(&block_contents), - } - } - JsonCommandRequest::get_confirmations { transaction_log_id } => { - JsonCommandResponse::get_confirmations { - confirmations: service - .get_confirmations(&transaction_log_id) - .map_err(format_error)? - .iter() - .map(Confirmation::from) - .collect(), - } - } - JsonCommandRequest::get_gift_code { gift_code_b58 } => JsonCommandResponse::get_gift_code { - gift_code: GiftCode::from( - &service - .get_gift_code(&EncodedGiftCode(gift_code_b58)) - .map_err(format_error)?, - ), - }, - JsonCommandRequest::get_mc_protocol_transaction { transaction_log_id } => { - let tx = service - .get_transaction_object(&transaction_log_id) - .map_err(format_error)?; - let proto_tx = mc_api::external::Tx::from(&tx); - let json_tx = JsonTx::from(&proto_tx); - JsonCommandResponse::get_mc_protocol_transaction { - transaction: json_tx, - } - } - JsonCommandRequest::get_mc_protocol_txo { txo_id } => { - let tx_out = service.get_txo_object(&txo_id).map_err(format_error)?; - let proto_txo = mc_api::external::TxOut::from(&tx_out); - let json_txo = JsonTxOut::from(&proto_txo); - JsonCommandResponse::get_mc_protocol_txo { txo: json_txo } - } - JsonCommandRequest::get_network_status => JsonCommandResponse::get_network_status { - network_status: NetworkStatus::try_from( - &service.get_network_status().map_err(format_error)?, - ) - .map_err(format_error)?, - }, - JsonCommandRequest::get_transaction_log { transaction_log_id } => { - let (transaction_log, associated_txos) = service - .get_transaction_log(&transaction_log_id) - .map_err(format_error)?; - JsonCommandResponse::get_transaction_log { - transaction_log: json_rpc::transaction_log::TransactionLog::new( - &transaction_log, - &associated_txos, - ), - } - } - JsonCommandRequest::get_transaction_logs_for_account { - account_id, - offset, - limit, - min_block_index, - max_block_index, - } => { - let (o, l) = page_helper(offset, limit)?; - - let min_block_index = min_block_index - .map(|i| i.parse::()) - .transpose() - .map_err(format_error)?; - - let max_block_index = max_block_index - .map(|i| i.parse::()) - .transpose() - .map_err(format_error)?; - - let transaction_logs_and_txos = service - .list_transaction_logs( - &AccountID(account_id), - Some(o), - Some(l), - min_block_index, - max_block_index, - ) - .map_err(format_error)?; - let transaction_log_map: Map = Map::from_iter( - transaction_logs_and_txos - .iter() - .map(|(t, a)| { - ( - t.transaction_id_hex.clone(), - serde_json::json!(json_rpc::transaction_log::TransactionLog::new(t, a)), - ) - }) - .collect::>(), - ); - - JsonCommandResponse::get_transaction_logs_for_account { - transaction_log_ids: transaction_logs_and_txos - .iter() - .map(|(t, _a)| t.transaction_id_hex.to_string()) - .collect(), - transaction_log_map, - } - } - JsonCommandRequest::get_txo { txo_id } => { - let result = service.get_txo(&TxoID(txo_id)).map_err(format_error)?; - JsonCommandResponse::get_txo { - txo: Txo::from(&result), - } - } - JsonCommandRequest::get_txos_for_account { - account_id, - status, - offset, - limit, - } => { - let (o, l) = page_helper(offset, limit)?; - let txos = service - .list_txos(&AccountID(account_id), status, Some(o), Some(l)) - .map_err(format_error)?; - let txo_map: Map = Map::from_iter( - txos.iter() - .map(|t| { - ( - t.txo_id_hex.clone(), - serde_json::to_value(Txo::from(t)).expect("Could not get json value"), - ) - }) - .collect::>(), - ); - - JsonCommandResponse::get_txos_for_account { - txo_ids: txos.iter().map(|t| t.txo_id_hex.clone()).collect(), - txo_map, - } - } - JsonCommandRequest::get_txos_for_view_only_account { - account_id, - offset, - limit, - } => { - let (o, l) = page_helper(offset, limit)?; - let txos = service - .list_view_only_txos(&account_id, Some(o), Some(l)) - .map_err(format_error)?; - let txo_map: Map = Map::from_iter( - txos.iter() - .map(|t| { - ( - t.txo_id_hex.clone(), - serde_json::to_value(ViewOnlyTxo::from(t)) - .expect("Could not get json value"), - ) - }) - .collect::>(), - ); - - JsonCommandResponse::get_txos_for_account { - txo_ids: txos.iter().map(|t| t.txo_id_hex.clone()).collect(), - txo_map, - } - } - JsonCommandRequest::get_wallet_status => JsonCommandResponse::get_wallet_status { - wallet_status: WalletStatus::try_from( - &service.get_wallet_status().map_err(format_error)?, - ) - .map_err(format_error)?, - }, - JsonCommandRequest::get_view_only_account { account_id } => { - JsonCommandResponse::get_view_only_account { - view_only_account: json_rpc::view_only_account::ViewOnlyAccountJSON::try_from( - &service - .get_view_only_account(&account_id) - .map_err(format_error)?, - ) - .map_err(format_error)?, - } - } - JsonCommandRequest::import_account { - mnemonic, - key_derivation_version, - name, - first_block_index, - next_subaddress_index, - fog_report_url, - fog_report_id, - fog_authority_spki, - } => { - let fb = first_block_index - .map(|fb| fb.parse::()) - .transpose() - .map_err(format_error)?; - let ns = next_subaddress_index - .map(|ns| ns.parse::()) - .transpose() - .map_err(format_error)?; - let kdv = key_derivation_version.parse::().map_err(format_error)?; - - JsonCommandResponse::import_account { - account: json_rpc::account::Account::try_from( - &service - .import_account( - mnemonic, - kdv, - name, - fb, - ns, - fog_report_url.unwrap_or_default(), - fog_report_id.unwrap_or_default(), - fog_authority_spki.unwrap_or_default(), - ) - .map_err(format_error)?, - ) - .map_err(format_error)?, - } - } - JsonCommandRequest::import_account_from_legacy_root_entropy { - entropy, - name, - first_block_index, - next_subaddress_index, - fog_report_url, - fog_report_id, - fog_authority_spki, - } => { - let fb = first_block_index - .map(|fb| fb.parse::()) - .transpose() - .map_err(format_error)?; - let ns = next_subaddress_index - .map(|ns| ns.parse::()) - .transpose() - .map_err(format_error)?; - - JsonCommandResponse::import_account { - account: json_rpc::account::Account::try_from( - &service - .import_account_from_legacy_root_entropy( - entropy, - name, - fb, - ns, - fog_report_url.unwrap_or_default(), - fog_report_id.unwrap_or_default(), - fog_authority_spki.unwrap_or_default(), - ) - .map_err(format_error)?, - ) - .map_err(format_error)?, - } - } - JsonCommandRequest::import_subaddresses_to_view_only_account { - account_id, - subaddresses, - } => { - let subaddresses_decoded = subaddresses - .iter() - .map(|s| { - let public_spend_key_bytes = - hex::decode(&s.public_spend_key).map_err(format_error)?; - let decoded_public_spend_key = - mc_util_serial::decode(&public_spend_key_bytes).map_err(format_error)?; - let subaddress_index = - s.subaddress_index.parse::().map_err(format_error)?; - Ok(( - s.public_address.clone(), - subaddress_index, - s.comment.clone(), - decoded_public_spend_key, - )) - }) - .collect::, _>>()?; - - let public_address_b58s = service - .import_subaddresses(&account_id, subaddresses_decoded) - .map_err(format_error)?; - - JsonCommandResponse::import_subaddresses_to_view_only_account { - public_address_b58s, - } - } - JsonCommandRequest::import_view_only_account { - account, - secrets, - subaddresses, - } => { - let decoded_key_bytes = hex::decode(&secrets.view_private_key).map_err(format_error)?; - let decoded_key: RistrettoPrivate = - mc_util_serial::decode(&decoded_key_bytes).map_err(format_error)?; - - let subaddresses_decoded = subaddresses - .iter() - .map(|s| { - let public_spend_key_bytes = hex::decode(&s.public_spend_key).unwrap(); - let decoded_public_spend_key: RistrettoPublic = - mc_util_serial::decode(&public_spend_key_bytes).map_err(format_error)?; - let subaddress_index = - s.subaddress_index.parse::().map_err(format_error)?; - Ok(( - s.public_address.clone(), - subaddress_index, - s.comment.clone(), - decoded_public_spend_key, - )) - }) - .collect::, _>>()?; - - let view_only_account = &service - .import_view_only_account( - &account.account_id, - &decoded_key, - account - .main_subaddress_index - .parse::() - .map_err(format_error)?, - account - .change_subaddress_index - .parse::() - .map_err(format_error)?, - account - .next_subaddress_index - .parse::() - .map_err(format_error)?, - &account.name, - subaddresses_decoded, - ) - .map_err(format_error)?; - - let view_only_account_json = - json_rpc::view_only_account::ViewOnlyAccountJSON::from(view_only_account); - - JsonCommandResponse::import_view_only_account { - view_only_account: view_only_account_json, - } - } - JsonCommandRequest::remove_account { account_id } => JsonCommandResponse::remove_account { - removed: service - .remove_account(&AccountID(account_id)) - .map_err(format_error)?, - }, - JsonCommandRequest::remove_gift_code { gift_code_b58 } => { - JsonCommandResponse::remove_gift_code { - removed: service - .remove_gift_code(&EncodedGiftCode(gift_code_b58)) - .map_err(format_error)?, - } - } - JsonCommandRequest::remove_view_only_account { account_id } => { - JsonCommandResponse::remove_view_only_account { - removed: service - .remove_view_only_account(&account_id) - .map_err(format_error)?, - } - } - JsonCommandRequest::submit_gift_code { - from_account_id, - gift_code_b58, - tx_proposal, - } => { - let gift_code = service - .submit_gift_code( - &AccountID(from_account_id), - &EncodedGiftCode(gift_code_b58), - &mc_mobilecoind::payments::TxProposal::try_from(&tx_proposal) - .map_err(format_error)?, - ) - .map_err(format_error)?; - JsonCommandResponse::submit_gift_code { - gift_code: GiftCode::from(&gift_code), - } - } - JsonCommandRequest::submit_transaction { - tx_proposal, - comment, - account_id, - } => { - let result: Option = service - .submit_transaction( - mc_mobilecoind::payments::TxProposal::try_from(&tx_proposal) - .map_err(format_error)?, - comment, - account_id, - ) - .map_err(format_error)? - .map(|(transaction_log, associated_txos)| { - json_rpc::transaction_log::TransactionLog::new( - &transaction_log, - &associated_txos, - ) - }); - JsonCommandResponse::submit_transaction { - transaction_log: result, - } - } - JsonCommandRequest::sync_view_only_account { - account_id, - completed_txos, - subaddresses, - } => { - let txo_ids_and_key_images: Vec<(String, KeyImage)> = completed_txos - .iter() - .map(|(txo_id, key_image_encoded)| { - let key_image_bytes = hex::decode(&key_image_encoded).map_err(format_error)?; - let key_image: KeyImage = - mc_util_serial::decode(&key_image_bytes).map_err(format_error)?; - Ok((txo_id.clone(), key_image)) - }) - .collect::, _>>()?; - - service - .set_view_only_txos_key_images(txo_ids_and_key_images) - .map_err(format_error)?; - - let subaddresses_decoded = subaddresses - .iter() - .map(|s| { - let public_spend_key_bytes = - hex::decode(&s.public_spend_key).map_err(format_error)?; - let decoded_public_spend_key = - mc_util_serial::decode(&public_spend_key_bytes).map_err(format_error)?; - let subaddress_index = - s.subaddress_index.parse::().map_err(format_error)?; - Ok(( - s.public_address.clone(), - subaddress_index, - s.comment.clone(), - decoded_public_spend_key, - )) - }) - .collect::, _>>()?; - - service - .import_subaddresses(&account_id, subaddresses_decoded) - .map_err(format_error)?; - - JsonCommandResponse::sync_view_only_account - } - JsonCommandRequest::update_account_name { account_id, name } => { - JsonCommandResponse::update_account_name { - account: json_rpc::account::Account::try_from( - &service - .update_account_name(&AccountID(account_id), name) - .map_err(format_error)?, - ) - .map_err(format_error)?, - } - } - JsonCommandRequest::update_view_only_account_name { account_id, name } => { - JsonCommandResponse::update_view_only_account_name { - view_only_account: json_rpc::view_only_account::ViewOnlyAccountJSON::try_from( - &service - .update_view_only_account_name(&account_id, &name) - .map_err(format_error)?, - ) - .map_err(format_error)?, - } - } - JsonCommandRequest::validate_confirmation { - account_id, - txo_id, - confirmation, - } => { - let result = service - .validate_confirmation(&AccountID(account_id), &TxoID(txo_id), &confirmation) - .map_err(format_error)?; - JsonCommandResponse::validate_confirmation { validated: result } - } - JsonCommandRequest::verify_address { address } => JsonCommandResponse::verify_address { - verified: service.verify_address(&address).map_err(format_error)?, - }, - JsonCommandRequest::version => JsonCommandResponse::version { - string: env!("CARGO_PKG_VERSION").to_string(), - number: ( - env!("CARGO_PKG_VERSION_MAJOR").to_string(), - env!("CARGO_PKG_VERSION_MINOR").to_string(), - env!("CARGO_PKG_VERSION_PATCH").to_string(), - env!("CARGO_PKG_VERSION_PRE").to_string(), - ), - commit: env!("VERGEN_GIT_SHA").to_string(), - }, - }; - - Ok(response) +) -> Result>, String> { + generic_wallet_api_v1(_api_key_guard, state, command) } -#[get("/wallet")] -fn wallet_help() -> Result { - Ok(help_str()) +#[get("/wallet/v2")] +fn wallet_help_v2() -> Result { + Ok(help_str_v2()) } -#[get("/health")] -fn health() -> Result<(), ()> { - Ok(()) +/// The route for the Full Service Wallet API. +#[post("/wallet/v2", format = "json", data = "")] +fn consensus_backed_wallet_api_v2( + _api_key_guard: ApiKeyGuard, + state: rocket::State, FogResolver>>, + command: Json, +) -> Result>, String> { + generic_wallet_api_v2(_api_key_guard, state, command) } -fn page_helper(offset: Option, limit: Option) -> Result<(u64, u64), JsonRPCError> { - let offset = match offset { - Some(o) => o.parse::().map_err(format_error)?, - None => 0, // Default offset is zero, at the start of the records. - }; - let limit = match limit { - Some(l) => l.parse::().map_err(format_error)?, - None => 100, // Default page size is one hundred records. - }; - Ok((offset, limit)) +#[post("/wallet/v2", format = "json", data = "")] +fn validator_backed_wallet_api_v2( + _api_key_guard: ApiKeyGuard, + state: rocket::State>, + command: Json, +) -> Result>, String> { + generic_wallet_api_v2(_api_key_guard, state, command) } /// Returns an instance of a Rocket server. @@ -1284,7 +132,13 @@ pub fn consensus_backed_rocket( rocket::custom(rocket_config) .mount( "/", - routes![consensus_backed_wallet_api, wallet_help, health], + routes![ + consensus_backed_wallet_api_v1, + consensus_backed_wallet_api_v2, + wallet_help_v1, + wallet_help_v2, + health + ], ) .manage(state) } @@ -1296,7 +150,13 @@ pub fn validator_backed_rocket( rocket::custom(rocket_config) .mount( "/", - routes![validator_backed_wallet_api, wallet_help, health], + routes![ + validator_backed_wallet_api_v1, + validator_backed_wallet_api_v2, + wallet_help_v1, + wallet_help_v2, + health + ], ) .manage(state) } diff --git a/full-service/src/lib.rs b/full-service/src/lib.rs index aa3514d67..863160624 100644 --- a/full-service/src/lib.rs +++ b/full-service/src/lib.rs @@ -8,10 +8,8 @@ pub mod check_host; pub mod config; pub mod db; mod error; -pub mod fog_resolver; pub mod json_rpc; pub mod service; -pub mod unsigned_tx; pub mod util; mod validator_ledger_sync; diff --git a/full-service/src/service/account.rs b/full-service/src/service/account.rs index 14efc40af..057c31940 100644 --- a/full-service/src/service/account.rs +++ b/full-service/src/service/account.rs @@ -4,30 +4,40 @@ use crate::{ db::{ - account::{AccountID, AccountModel, ViewOnlyAccountImportPackage}, + account::{AccountID, AccountModel}, assigned_subaddress::AssignedSubaddressModel, - models::{Account, AssignedSubaddress}, - transaction, WalletDbError, + models::{Account, AssignedSubaddress, Txo}, + transaction, + txo::TxoModel, + WalletDbError, }, + json_rpc::{json_rpc_request::JsonRPCRequest, v2::api::request::JsonCommandRequest}, service::{ ledger::{LedgerService, LedgerServiceError}, WalletService, }, - util::constants::MNEMONIC_KEY_DERIVATION_VERSION, + util::{ + constants::MNEMONIC_KEY_DERIVATION_VERSION, + encoding_helpers::{ + hex_to_ristretto, hex_to_ristretto_public, ristretto_public_to_hex, ristretto_to_hex, + }, + }, }; use base64; use bip39::{Language, Mnemonic, MnemonicType}; use displaydoc::Display; -use mc_account_keys::RootEntropy; +use mc_account_keys::{AccountKey, RootEntropy}; use mc_account_keys_slip10; use mc_common::logger::log; use mc_connection::{BlockchainConnection, UserTxConnection}; +use mc_crypto_keys::RistrettoPublic; use mc_fog_report_validation::FogPubkeyResolver; use mc_ledger_db::Ledger; +use mc_transaction_core::ring_signature::KeyImage; #[derive(Display, Debug)] pub enum AccountServiceError { - /// Error interacting with the database: {0} + /// Error interacting& with the database: {0} Database(WalletDbError), /// Error with LedgerDB: {0} @@ -53,6 +63,15 @@ pub enum AccountServiceError { /// Error decoding private view key: {0} DecodePrivateKeyError(String), + + /// Account is a view only account and shouldn't be + AccountIsViewOnly(AccountID), + + /// Account is not a view only account and should be + AccountIsNotViewOnly(AccountID), + + /// JSON Rpc Request was formatted incorrectly + InvalidJsonRPCRequest, } impl From for AccountServiceError { @@ -142,12 +161,36 @@ pub trait AccountService { fog_authority_spki: String, ) -> Result; + /// Import an existing account to the wallet using the mnemonic. + fn import_view_only_account( + &self, + view_private_key: String, + spend_public_key: String, + name: Option, + first_block_index: Option, + next_subaddress_index: Option, + ) -> Result; + + fn get_view_only_account_import_request( + &self, + account_id: &AccountID, + ) -> Result; + /// List accounts in the wallet. - fn list_accounts(&self) -> Result, AccountServiceError>; + fn list_accounts( + &self, + offset: Option, + limit: Option, + ) -> Result, AccountServiceError>; /// Get an account in the wallet. fn get_account(&self, account_id: &AccountID) -> Result; + fn get_next_subaddress_index_for_account( + &self, + account_id: &AccountID, + ) -> Result; + /// Update the name for an account. fn update_account_name( &self, @@ -155,10 +198,13 @@ pub trait AccountService { name: String, ) -> Result; - fn get_view_only_import_package( + /// complete a sync request for a view only account + fn sync_account( &self, account_id: &AccountID, - ) -> Result; + txo_ids_and_key_images: Vec<(String, String)>, + next_subaddress_index: u64, + ) -> Result<(), AccountServiceError>; /// Remove an account from the wallet. fn remove_account(&self, account_id: &AccountID) -> Result; @@ -200,7 +246,7 @@ where let first_block_index = network_block_height; // -1 +1 let import_block_index = local_block_height; // -1 +1 - let conn = self.wallet_db.get_conn()?; + let conn = self.get_conn()?; transaction(&conn, || { let (account_id, _public_address_b58) = Account::create_from_mnemonic( &mnemonic, @@ -211,7 +257,6 @@ where fog_report_url, fog_report_id, fog_authority_spki, - &self.ledger_db, &conn, )?; let account = Account::get(&account_id, &conn)?; @@ -257,7 +302,7 @@ where // start scanning. let import_block = self.ledger_db.num_blocks()? - 1; - let conn = self.wallet_db.get_conn()?; + let conn = self.get_conn()?; transaction(&conn, || { Ok(Account::import( &mnemonic, @@ -268,7 +313,6 @@ where fog_report_url, fog_report_id, fog_authority_spki, - &self.ledger_db, &conn, )?) }) @@ -298,7 +342,7 @@ where // start scanning. let import_block = self.ledger_db.num_blocks()? - 1; - let conn = self.wallet_db.get_conn()?; + let conn = self.get_conn()?; transaction(&conn, || { Ok(Account::import_legacy( &RootEntropy::from(&entropy_bytes), @@ -309,53 +353,157 @@ where fog_report_url, fog_report_id, fog_authority_spki, - &self.ledger_db, &conn, )?) }) } - fn list_accounts(&self) -> Result, AccountServiceError> { - let conn = self.wallet_db.get_conn()?; - Ok(Account::list_all(&conn)?) + fn import_view_only_account( + &self, + view_private_key: String, + spend_public_key: String, + name: Option, + first_block_index: Option, + next_subaddress_index: Option, + ) -> Result { + log::info!( + self.logger, + "Importing view only account {:?} with first block: {:?}", + name, + first_block_index, + ); + + let view_private_key = + hex_to_ristretto(&view_private_key).map_err(AccountServiceError::Base64DecodeError)?; + let spend_public_key = hex_to_ristretto_public(&spend_public_key) + .map_err(AccountServiceError::Base64DecodeError)?; + + let import_block_index = self.ledger_db.num_blocks()? - 1; + + let conn = self.get_conn()?; + transaction(&conn, || { + Ok(Account::import_view_only( + &view_private_key, + &spend_public_key, + name, + import_block_index, + first_block_index, + next_subaddress_index, + &conn, + )?) + }) + } + + fn get_view_only_account_import_request( + &self, + account_id: &AccountID, + ) -> Result { + let conn = self.get_conn()?; + let account = Account::get(account_id, &conn)?; + + if account.view_only { + return Err(AccountServiceError::AccountIsViewOnly(account_id.clone())); + } + + let account_key: AccountKey = mc_util_serial::decode(&account.account_key)?; + let view_private_key = account_key.view_private_key(); + let spend_public_key = RistrettoPublic::from(account_key.spend_private_key()); + + let json_command_request = JsonCommandRequest::import_view_only_account { + view_private_key: ristretto_to_hex(view_private_key), + spend_public_key: ristretto_public_to_hex(&spend_public_key), + name: Some(account.name.clone()), + first_block_index: Some(account.first_block_index.to_string()), + next_subaddress_index: Some(account.next_subaddress_index(&conn)?.to_string()), + }; + + let src_json: serde_json::Value = serde_json::json!(json_command_request); + let method = src_json + .get("method") + .ok_or(AccountServiceError::InvalidJsonRPCRequest)? + .as_str() + .ok_or(AccountServiceError::InvalidJsonRPCRequest)?; + let params = src_json + .get("params") + .ok_or(AccountServiceError::InvalidJsonRPCRequest)?; + + Ok(JsonRPCRequest { + method: method.to_string(), + params: Some(params.clone()), + jsonrpc: "2.0".to_string(), + id: serde_json::Value::Number(serde_json::Number::from(1)), + }) + } + + fn list_accounts( + &self, + offset: Option, + limit: Option, + ) -> Result, AccountServiceError> { + let conn = self.get_conn()?; + Ok(Account::list_all(&conn, offset, limit)?) } fn get_account(&self, account_id: &AccountID) -> Result { - let conn = self.wallet_db.get_conn()?; + let conn = self.get_conn()?; Ok(Account::get(account_id, &conn)?) } + fn get_next_subaddress_index_for_account( + &self, + account_id: &AccountID, + ) -> Result { + let conn = self.get_conn()?; + let account = Account::get(account_id, &conn)?; + Ok(account.next_subaddress_index(&conn)?) + } + fn update_account_name( &self, account_id: &AccountID, name: String, ) -> Result { - let conn = self.wallet_db.get_conn()?; + let conn = self.get_conn()?; Account::get(account_id, &conn)?.update_name(name, &conn)?; Ok(Account::get(account_id, &conn)?) } - fn get_view_only_import_package( + fn sync_account( &self, account_id: &AccountID, - ) -> Result { - let conn = self.wallet_db.get_conn()?; - + txo_ids_and_key_images: Vec<(String, String)>, + next_subaddress_index: u64, + ) -> Result<(), AccountServiceError> { + let conn = self.get_conn()?; let account = Account::get(account_id, &conn)?; - let subaddresses = - AssignedSubaddress::list_all(&account_id.to_string(), None, None, &conn)?; - let view_only_account_import_package = ViewOnlyAccountImportPackage { - account, - subaddresses, - }; + if !account.view_only { + return Err(AccountServiceError::AccountIsNotViewOnly( + account_id.clone(), + )); + } + + for (txo_id_hex, key_image_encoded) in txo_ids_and_key_images { + let key_image: KeyImage = mc_util_serial::decode(&hex::decode(key_image_encoded)?)?; + let spent_block_index = self.ledger_db.check_key_image(&key_image)?; + Txo::update_key_image(&txo_id_hex, &key_image, spent_block_index, &conn)?; + } + + for _ in account.next_subaddress_index(&conn)?..next_subaddress_index { + AssignedSubaddress::create_next_for_account( + &account_id.to_string(), + "Recovered In Account Sync", + &self.ledger_db, + &conn, + )?; + } - Ok(view_only_account_import_package) + Ok(()) } fn remove_account(&self, account_id: &AccountID) -> Result { log::info!(self.logger, "Deleting account {}", account_id,); - let conn = self.wallet_db.get_conn()?; + let conn = self.get_conn()?; transaction(&conn, || { let account = Account::get(account_id, &conn)?; account.delete(&conn)?; @@ -370,13 +518,17 @@ mod tests { use crate::{ db::{models::Txo, txo::TxoModel}, test_utils::{ - create_test_received_txo, get_empty_test_ledger, get_test_ledger, - manually_sync_account, setup_wallet_service, setup_wallet_service_offline, MOB, + add_block_to_ledger_db, create_test_received_txo, get_empty_test_ledger, + get_test_ledger, manually_sync_account, setup_wallet_service, + setup_wallet_service_offline, MOB, }, }; - use mc_account_keys::{AccountKey, PublicAddress}; + use mc_account_keys::{AccountKey, PublicAddress, ViewAccountKey}; use mc_common::logger::{test_with_logger, Logger}; + use mc_crypto_keys::RistrettoPrivate; + use mc_crypto_rand::RngCore; use mc_transaction_core::{tokens::Mob, Amount, Token}; + use mc_util_from_random::FromRandom; use rand::{rngs::StdRng, SeedableRng}; #[test_with_logger] @@ -387,7 +539,7 @@ mod tests { let ledger_db = get_test_ledger(5, &known_recipients, 12, &mut rng); let service = setup_wallet_service(ledger_db.clone(), logger.clone()); - let wallet_db = &service.wallet_db; + let wallet_db = &service.wallet_db.as_ref().unwrap(); // Create an account. let account = service @@ -412,7 +564,9 @@ mod tests { ); let txos = Txo::list_for_account( - &account.account_id_hex, + &account.id, + None, + None, None, None, None, @@ -423,12 +577,14 @@ mod tests { assert_eq!(txos.len(), 1); // Delete the account. The transaction status referring to it is also cleared. - let account_id = AccountID(account.account_id_hex.clone().to_string()); + let account_id = AccountID(account.id.clone().to_string()); let result = service.remove_account(&account_id); assert!(result.is_ok()); let txos = Txo::list_for_account( - &account.account_id_hex, + &account.id, + None, + None, None, None, None, @@ -465,8 +621,13 @@ mod tests { // Syncing the account does nothing to the block indices since there are no new // blocks. - let account_id = AccountID(account.account_id_hex); - manually_sync_account(&ledger_db, &service.wallet_db, &account_id, &logger); + let account_id = AccountID(account.id); + manually_sync_account( + &ledger_db, + &service.wallet_db.as_ref().unwrap(), + &account_id, + &logger, + ); let account = service.get_account(&account_id).unwrap(); assert_eq!(account.first_block_index, 12); assert_eq!(account.next_block_index, 12); @@ -497,11 +658,157 @@ mod tests { // Syncing the account does nothing to the block indices since there are no // blocks in the ledger. - let account_id = AccountID(account.account_id_hex); - manually_sync_account(&ledger_db, &service.wallet_db, &account_id, &logger); + let account_id = AccountID(account.id); + manually_sync_account( + &ledger_db, + &service.wallet_db.as_ref().unwrap(), + &account_id, + &logger, + ); let account = service.get_account(&account_id).unwrap(); assert_eq!(account.first_block_index, 0); assert_eq!(account.next_block_index, 0); assert_eq!(account.import_block_index, Some(0)); } + + #[test_with_logger] + fn test_sync_view_only_account(logger: Logger) { + let mut rng: StdRng = SeedableRng::from_seed([20u8; 32]); + + let known_recipients: Vec = Vec::new(); + let mut ledger_db = get_test_ledger(5, &known_recipients, 12, &mut rng); + + let service = setup_wallet_service(ledger_db.clone(), logger.clone()); + let wallet_db = &service.wallet_db.as_ref().unwrap(); + let conn = wallet_db.get_conn().unwrap(); + + let view_private_key = RistrettoPrivate::from_random(&mut rng); + let spend_private_key = RistrettoPrivate::from_random(&mut rng); + + let account_key = AccountKey::new(&spend_private_key, &view_private_key); + let view_account_key = ViewAccountKey::from(&account_key); + + let view_only_account = service + .import_view_only_account( + ristretto_to_hex(&view_account_key.view_private_key()), + ristretto_public_to_hex(&view_account_key.spend_public_key()), + None, + None, + None, + ) + .unwrap(); + + let account_id = AccountID(view_only_account.id.clone()); + + add_block_to_ledger_db( + &mut ledger_db, + &vec![ + view_account_key.default_subaddress(), + view_account_key.subaddress(2), + ], + 100 * MOB, + &vec![KeyImage::from(rng.next_u64())], + &mut rng, + ); + + manually_sync_account(&ledger_db, wallet_db, &account_id, &logger); + + let unverified_txos = Txo::list_unverified( + Some(&account_id.to_string()), + None, + None, + None, + None, + None, + None, + &wallet_db.get_conn().unwrap(), + ) + .unwrap(); + + assert_eq!(unverified_txos.len(), 1); + assert_eq!(unverified_txos[0].subaddress_index, Some(0)); + assert_eq!(unverified_txos[0].key_image, None); + + let orphaned_txos = Txo::list_orphaned( + Some(&account_id.to_string()), + None, + None, + None, + None, + None, + &wallet_db.get_conn().unwrap(), + ) + .unwrap(); + + assert_eq!(orphaned_txos.len(), 1); + assert_eq!(orphaned_txos[0].subaddress_index, None); + assert_eq!(orphaned_txos[0].key_image, None); + + let view_only_account = service.get_account(&account_id).unwrap(); + assert_eq!(view_only_account.next_subaddress_index(&conn).unwrap(), 2); + + let key_image_1 = KeyImage::from(rng.next_u64()); + let key_image_2 = KeyImage::from(rng.next_u64()); + + let key_image_1_hex = hex::encode(mc_util_serial::encode(&key_image_1)); + let key_image_2_hex = hex::encode(mc_util_serial::encode(&key_image_2)); + + let txo_id_hex_1 = unverified_txos[0].id.clone(); + let txo_id_hex_2 = orphaned_txos[0].id.clone(); + + service + .sync_account( + &account_id, + vec![ + (txo_id_hex_1, key_image_1_hex), + (txo_id_hex_2, key_image_2_hex), + ], + 3, + ) + .unwrap(); + + let view_only_account = service.get_account(&account_id).unwrap(); + assert_eq!(view_only_account.next_subaddress_index(&conn).unwrap(), 3); + + let unverified_txos = Txo::list_unverified( + Some(&account_id.to_string()), + None, + None, + None, + None, + None, + None, + &wallet_db.get_conn().unwrap(), + ) + .unwrap(); + + assert_eq!(unverified_txos.len(), 0); + + let orphaned_txos = Txo::list_orphaned( + Some(&account_id.to_string()), + None, + None, + None, + None, + None, + &wallet_db.get_conn().unwrap(), + ) + .unwrap(); + + assert_eq!(orphaned_txos.len(), 0); + + let unspent_txos = Txo::list_unspent( + Some(&account_id.to_string()), + None, + None, + None, + None, + None, + None, + &wallet_db.get_conn().unwrap(), + ) + .unwrap(); + + assert_eq!(unspent_txos.len(), 2); + } } diff --git a/full-service/src/service/address.rs b/full-service/src/service/address.rs index 1b28246b9..a863b7810 100644 --- a/full-service/src/service/address.rs +++ b/full-service/src/service/address.rs @@ -4,18 +4,12 @@ use crate::{ db::{ - account::{AccountID, AccountModel}, - assigned_subaddress::AssignedSubaddressModel, - models::{Account, AssignedSubaddress, ViewOnlySubaddress}, - transaction, - view_only_subaddress::ViewOnlySubaddressModel, - WalletDbError, + account::AccountID, assigned_subaddress::AssignedSubaddressModel, + models::AssignedSubaddress, transaction, WalletDbError, }, service::WalletService, util::b58::b58_decode_public_address, }; -use mc_account_keys::{AccountKey, CHANGE_SUBADDRESS_INDEX}; -use mc_common::logger::log; use mc_connection::{BlockchainConnection, UserTxConnection}; use mc_fog_report_validation::FogPubkeyResolver; @@ -55,37 +49,25 @@ pub trait AddressService { // FIXME: FS-32 - add "sync from block" ) -> Result; + /// Get an assigned subaddress, if it exists. + fn get_address(&self, address_b58: &str) -> Result; + fn get_address_for_account( &self, account_id: &AccountID, index: i64, ) -> Result; - /// Gets all the addresses for the given account. - fn get_addresses_for_account( + /// Gets all the addresses for an optionally given account. + fn get_addresses( &self, - account_id: &AccountID, + account_id: Option, offset: Option, limit: Option, ) -> Result, AddressServiceError>; - fn get_address_for_view_only_account( - &self, - account_id: &AccountID, - index: u64, - ) -> Result; - - fn get_addresses_for_view_only_account( - &self, - account_id: &AccountID, - offset: Option, - limit: Option, - ) -> Result, AddressServiceError>; - /// Verifies whether an address can be decoded from b58. fn verify_address(&self, public_address: &str) -> Result; - - fn assign_missing_reserved_subaddresses_for_accounts(&self) -> Result<(), AddressServiceError>; } impl AddressService for WalletService @@ -98,7 +80,7 @@ where account_id: &AccountID, metadata: Option<&str>, ) -> Result { - let conn = self.wallet_db.get_conn()?; + let conn = self.get_conn()?; transaction(&conn, || { let (public_address_b58, _subaddress_index) = AssignedSubaddress::create_next_for_account( @@ -111,12 +93,17 @@ where }) } + fn get_address(&self, address_b58: &str) -> Result { + let conn = self.get_conn()?; + Ok(AssignedSubaddress::get(address_b58, &conn)?) + } + fn get_address_for_account( &self, account_id: &AccountID, index: i64, ) -> Result { - let conn = self.wallet_db.get_conn()?; + let conn = self.get_conn()?; Ok(AssignedSubaddress::get_for_account_by_index( &account_id.to_string(), index, @@ -124,100 +111,23 @@ where )?) } - fn get_addresses_for_account( + fn get_addresses( &self, - account_id: &AccountID, + account_id: Option, offset: Option, limit: Option, ) -> Result, AddressServiceError> { - let conn = self.wallet_db.get_conn()?; + let conn = self.get_conn()?; Ok(AssignedSubaddress::list_all( - &account_id.to_string(), - offset, - limit, - &conn, - )?) - } - - fn get_address_for_view_only_account( - &self, - account_id: &AccountID, - index: u64, - ) -> Result { - let conn = self.wallet_db.get_conn()?; - Ok(ViewOnlySubaddress::get_for_account_by_index( - &account_id.to_string(), - index, - &conn, - )?) - } - - fn get_addresses_for_view_only_account( - &self, - account_id: &AccountID, - offset: Option, - limit: Option, - ) -> Result, AddressServiceError> { - let conn = self.wallet_db.get_conn()?; - Ok(ViewOnlySubaddress::list_all( - &account_id.to_string(), - offset, - limit, - &conn, + account_id, offset, limit, &conn, )?) } fn verify_address(&self, public_address: &str) -> Result { match b58_decode_public_address(public_address) { - Ok(a) => { - log::info!( - self.logger, - "Verified address:\n\t\t{}\n\t\t{}\n\t\t{}\n\t\t{:?}\n\t\t{}", - a.view_public_key(), - a.spend_public_key(), - a.fog_report_url().unwrap_or(""), - a.fog_authority_sig().unwrap_or_default(), - a.fog_report_id().unwrap_or(""), - ); - Ok(true) - } - Err(e) => { - log::info!( - self.logger, - "Address did not verify {:?}: {:?}", - public_address, - e - ); - Ok(false) - } - } - } - - fn assign_missing_reserved_subaddresses_for_accounts(&self) -> Result<(), AddressServiceError> { - let conn = self.wallet_db.get_conn()?; - let accounts = Account::list_all(&conn)?; - - for account in accounts.iter() { - if AssignedSubaddress::get_for_account_by_index( - &account.account_id_hex, - CHANGE_SUBADDRESS_INDEX as i64, - &conn, - ) - .is_err() - { - let account_key: AccountKey = mc_util_serial::decode(&account.account_key).unwrap(); - AssignedSubaddress::create( - &account_key, - None, - CHANGE_SUBADDRESS_INDEX, - "Change", - &self.ledger_db, - &conn, - )?; - }; + Ok(_) => Ok(true), + Err(_) => Ok(false), } - - Ok(()) } } @@ -225,14 +135,79 @@ where mod tests { use super::*; use crate::{ + db::account::AccountModel, + service::account::AccountService, test_utils::{get_test_ledger, setup_wallet_service}, - util::b58::b58_encode_public_address, + util::{ + b58::b58_encode_public_address, + encoding_helpers::{ristretto_public_to_hex, ristretto_to_hex}, + }, }; use mc_account_keys::{AccountKey, PublicAddress}; use mc_common::logger::{test_with_logger, Logger}; + use mc_crypto_keys::{RistrettoPrivate, RistrettoPublic}; use mc_crypto_rand::rand_core::RngCore; + use mc_util_from_random::FromRandom; use rand::{rngs::StdRng, SeedableRng}; + #[test_with_logger] + fn test_assign_address_for_account(logger: Logger) { + let mut rng: StdRng = SeedableRng::from_seed([20u8; 32]); + + let known_recipients: Vec = Vec::new(); + + let ledger_db = get_test_ledger(5, &known_recipients, 12, &mut rng); + let service = setup_wallet_service(ledger_db.clone(), logger.clone()); + let conn = service.get_conn().unwrap(); + + // Create an account. + let account = service + .create_account(None, "".to_string(), "".to_string(), "".to_string()) + .unwrap(); + assert_eq!(account.clone().next_subaddress_index(&conn).unwrap(), 2); + + let account_id = AccountID(account.id); + + service + .assign_address_for_account(&account_id, None) + .unwrap(); + + let account = service.get_account(&account_id).unwrap(); + assert_eq!(account.next_subaddress_index(&conn).unwrap(), 3); + } + + #[test_with_logger] + fn test_assign_address_for_view_only_account(logger: Logger) { + let mut rng: StdRng = SeedableRng::from_seed([20u8; 32]); + + let known_recipients: Vec = Vec::new(); + + let ledger_db = get_test_ledger(5, &known_recipients, 12, &mut rng); + let service = setup_wallet_service(ledger_db.clone(), logger.clone()); + let conn = service.get_conn().unwrap(); + + let view_private_key = RistrettoPrivate::from_random(&mut rng); + let spend_public_key = RistrettoPublic::from_random(&mut rng); + + let vpk_hex = ristretto_to_hex(&view_private_key); + let spk_hex = ristretto_public_to_hex(&spend_public_key); + + // Create an account. + let account = service + .import_view_only_account(vpk_hex, spk_hex, None, None, None) + .unwrap(); + assert_eq!(account.clone().next_subaddress_index(&conn).unwrap(), 2); + + let account_id = AccountID(account.id); + + service + .assign_address_for_account(&account_id, None) + .unwrap(); + + let account = service.get_account(&account_id).unwrap(); + assert_eq!(account.next_subaddress_index(&conn).unwrap(), 3); + } + // A properly encoded address should verify. #[test_with_logger] fn test_verify_address_succeeds(logger: Logger) { diff --git a/full-service/src/service/balance.rs b/full-service/src/service/balance.rs index 41223ba64..f2f48aa82 100644 --- a/full-service/src/service/balance.rs +++ b/full-service/src/service/balance.rs @@ -1,21 +1,18 @@ // Copyright (c) 2020-2021 MobileCoin Inc. //! Service for managing balances. +use std::collections::BTreeMap; use crate::{ db::{ account::{AccountID, AccountModel}, assigned_subaddress::AssignedSubaddressModel, - models::{ - Account, AssignedSubaddress, Txo, ViewOnlyAccount, ViewOnlySubaddress, ViewOnlyTxo, - }, + models::{Account, AssignedSubaddress, Txo}, txo::TxoModel, - view_only_account::ViewOnlyAccountModel, - view_only_subaddress::ViewOnlySubaddressModel, - view_only_txo::ViewOnlyTxoModel, Conn, WalletDbError, }, service::{ + account::{AccountService, AccountServiceError}, ledger::{LedgerService, LedgerServiceError}, WalletService, }, @@ -25,6 +22,7 @@ use mc_common::HashMap; use mc_connection::{BlockchainConnection, UserTxConnection}; use mc_fog_report_validation::FogPubkeyResolver; use mc_ledger_db::Ledger; +use mc_transaction_core::TokenId; /// Errors for the Address Service. #[derive(Display, Debug)] @@ -44,6 +42,9 @@ pub enum BalanceServiceError { /// Unexpected Account Txo Status: {0} UnexpectedAccountTxoStatus(String), + + /// AccountServiceError + AccountServiceError(AccountServiceError), } impl From for BalanceServiceError { @@ -70,20 +71,39 @@ impl From for BalanceServiceError { } } +impl From for BalanceServiceError { + fn from(src: AccountServiceError) -> Self { + Self::AccountServiceError(src) + } +} + /// The balance object returned by balance services. /// /// This must be a service object because there is no "Balance" table in our /// data model. +#[derive(Debug, Clone, PartialEq, Eq, Hash, Default)] pub struct Balance { + pub max_spendable: u128, + pub unverified: u128, pub unspent: u128, pub pending: u128, pub spent: u128, pub secreted: u128, pub orphaned: u128, - pub network_block_height: u64, - pub local_block_height: u64, - pub synced_blocks: u64, - pub max_spendable: u128, +} + +impl Default for &Balance { + fn default() -> &'static Balance { + &Balance { + max_spendable: 0, + unverified: 0, + unspent: 0, + pending: 0, + spent: 0, + secreted: 0, + orphaned: 0, + } + } } /// The Network Status object. @@ -91,7 +111,7 @@ pub struct Balance { pub struct NetworkStatus { pub network_block_height: u64, pub local_block_height: u64, - pub fee_pmob: u64, + pub fees: BTreeMap, pub block_version: u32, } @@ -103,18 +123,12 @@ pub struct NetworkStatus { /// It shares several fields with balance, but also returns details about the /// accounts in the wallet. pub struct WalletStatus { - pub unspent: u128, - pub pending: u128, - pub spent: u128, - pub secreted: u128, - pub orphaned: u128, + pub balance_per_token: BTreeMap, pub network_block_height: u64, pub local_block_height: u64, pub min_synced_block_index: u64, pub account_ids: Vec, pub account_map: HashMap, - pub view_only_account_ids: Vec, - pub view_only_account_map: HashMap, } /// Trait defining the ways in which the wallet can interact with and manage @@ -126,19 +140,12 @@ pub trait BalanceService { fn get_balance_for_account( &self, account_id: &AccountID, - ) -> Result; + ) -> Result, BalanceServiceError>; - fn get_balance_for_view_only_account( - &self, - account_id: &str, - ) -> Result; - - fn get_balance_for_address(&self, address: &str) -> Result; - - fn get_balance_for_view_only_address( + fn get_balance_for_address( &self, address: &str, - ) -> Result; + ) -> Result, BalanceServiceError>; fn get_network_status(&self) -> Result; @@ -153,116 +160,66 @@ where fn get_balance_for_account( &self, account_id: &AccountID, - ) -> Result { - let account_id_hex = &account_id.to_string(); - - let conn = self.wallet_db.get_conn()?; - let (unspent, max_spendable, pending, spent, secreted, orphaned) = - Self::get_balance_inner(account_id_hex, None, &conn)?; - - let network_block_height = self.get_network_block_height()?; - let local_block_height = self.ledger_db.num_blocks()?; - let account = Account::get(account_id, &conn)?; - - Ok(Balance { - unspent, - max_spendable, - pending, - spent, - secreted, - orphaned, - network_block_height, - local_block_height, - synced_blocks: account.next_block_index as u64, - }) - } - - fn get_balance_for_view_only_account( - &self, - account_id: &str, - ) -> Result { - let conn = self.wallet_db.get_conn()?; - - let (unspent, max_spendable, pending, spent, secreted, orphaned) = - Self::get_view_only_balance_inner(account_id, None, &conn)?; - - let network_block_height = self.get_network_block_height()?; - let local_block_height = self.ledger_db.num_blocks()?; - let account = ViewOnlyAccount::get(account_id, &conn)?; - - Ok(Balance { - unspent, - pending, - spent, - secreted, - orphaned, - network_block_height, - local_block_height, - synced_blocks: account.next_block_index as u64, - max_spendable, - }) + ) -> Result, BalanceServiceError> { + let conn = &self.get_conn()?; + let account = self.get_account(account_id)?; + let distinct_token_ids = account.get_token_ids(conn)?; + + let network_fees = self.get_network_fees()?; + + let balances = distinct_token_ids + .into_iter() + .map(|token_id| { + let default_token_fee = network_fees.get(&token_id).unwrap_or(&0); + let balance = Self::get_balance_inner( + Some(&account_id.to_string()), + None, + token_id, + default_token_fee, + conn, + )?; + Ok((token_id, balance)) + }) + .collect::, BalanceServiceError>>()?; + + Ok(balances) } - fn get_balance_for_address(&self, address: &str) -> Result { - let network_block_height = self.get_network_block_height()?; - let local_block_height = self.ledger_db.num_blocks()?; - - let conn = self.wallet_db.get_conn()?; - let assigned_address = AssignedSubaddress::get(address, &conn)?; - - let (unspent, max_spendable, pending, spent, secreted, orphaned) = - Self::get_balance_inner(&assigned_address.account_id_hex, Some(address), &conn)?; - - let account = Account::get(&AccountID(assigned_address.account_id_hex), &conn)?; - - Ok(Balance { - unspent, - max_spendable, - pending, - spent, - secreted, - orphaned, - network_block_height, - local_block_height, - synced_blocks: account.next_block_index as u64, - }) - } - - fn get_balance_for_view_only_address( + fn get_balance_for_address( &self, address: &str, - ) -> Result { - let conn = self.wallet_db.get_conn()?; - let view_only_subaddress = ViewOnlySubaddress::get(address, &conn)?; - let (unspent, max_spendable, pending, spent, secreted, orphaned) = - Self::get_view_only_balance_inner( - &view_only_subaddress.view_only_account_id_hex, - Some(address), - &conn, - )?; - - let network_block_height = self.get_network_block_height()?; - let local_block_height = self.ledger_db.num_blocks()?; - let account = ViewOnlyAccount::get(&view_only_subaddress.view_only_account_id_hex, &conn)?; - - Ok(Balance { - unspent, - max_spendable, - pending, - spent, - secreted, - orphaned, - network_block_height, - local_block_height, - synced_blocks: account.next_block_index as u64, - }) + ) -> Result, BalanceServiceError> { + let conn = &self.get_conn()?; + let assigned_address = AssignedSubaddress::get(address, conn)?; + let account_id = AccountID::from(assigned_address.account_id); + let account = self.get_account(&account_id)?; + let distinct_token_ids = account.get_token_ids(conn)?; + let network_fees = self.get_network_fees()?; + + let balances = distinct_token_ids + .into_iter() + .map(|token_id| { + let default_token_fee = network_fees.get(&token_id).unwrap_or(&0); + let balance = Self::get_balance_inner( + None, + Some(address), + token_id, + default_token_fee, + conn, + )?; + Ok((token_id, balance)) + }) + .collect::, BalanceServiceError>>()?; + + Ok(balances) } + fn get_network_status(&self) -> Result { Ok(NetworkStatus { network_block_height: self.get_network_block_height()?, local_block_height: self.ledger_db.num_blocks()?, - fee_pmob: self.get_network_fee(), - block_version: *self.get_network_block_version(), + fees: self.get_network_fees()?, + block_version: *self.get_network_block_version()?, }) } @@ -270,30 +227,43 @@ where fn get_wallet_status(&self) -> Result { let network_block_height = self.get_network_block_height()?; - let conn = self.wallet_db.get_conn()?; - let accounts = Account::list_all(&conn)?; + let conn = self.get_conn()?; + let accounts = Account::list_all(&conn, None, None)?; let mut account_map = HashMap::default(); - let view_only_accounts = ViewOnlyAccount::list_all(&conn)?; - let mut view_only_account_map = HashMap::default(); - let mut unspent: u128 = 0; - let mut pending: u128 = 0; - let mut spent: u128 = 0; - let mut secreted: u128 = 0; - let mut orphaned: u128 = 0; + let mut balance_per_token = BTreeMap::new(); - let mut min_synced_block_index = network_block_height - 1; + let mut min_synced_block_index = network_block_height.saturating_sub(1); let mut account_ids = Vec::new(); + let network_fees = self.get_network_fees()?; for account in accounts { - let account_id = AccountID(account.account_id_hex.clone()); - let balance = Self::get_balance_inner(&account_id.to_string(), None, &conn)?; + let account_id = AccountID(account.id.clone()); + let token_ids = account.clone().get_token_ids(&conn)?; + + for token_id in token_ids { + let default_token_fee = network_fees.get(&token_id).unwrap_or(&0); + let balance = Self::get_balance_inner( + Some(&account_id.to_string()), + None, + token_id, + default_token_fee, + &conn, + )?; + balance_per_token + .entry(token_id) + .and_modify(|b: &mut Balance| { + b.unverified += balance.unverified; + b.unspent += balance.unspent; + b.pending += balance.pending; + b.spent += balance.spent; + b.secreted += balance.secreted; + b.orphaned += balance.orphaned; + }) + .or_insert(balance); + } + account_map.insert(account_id.clone(), account.clone()); - unspent += balance.0; - pending += balance.2; - spent += balance.3; - secreted += balance.4; - orphaned += balance.5; // account.next_block_index is an index in range [0..ledger_db.num_blocks()] min_synced_block_index = std::cmp::min( @@ -303,126 +273,112 @@ where account_ids.push(account_id); } - let mut view_only_account_ids = Vec::new(); - for account in view_only_accounts { - let account_id = account.account_id_hex.clone(); - view_only_account_map.insert(account_id.clone(), account.clone()); - view_only_account_ids.push(account_id); - } - Ok(WalletStatus { - unspent, - pending, - spent, - secreted, - orphaned, + balance_per_token, network_block_height, local_block_height: self.ledger_db.num_blocks()?, - min_synced_block_index: min_synced_block_index as u64, + min_synced_block_index, account_ids, account_map, - view_only_account_ids, - view_only_account_map, }) } } +fn sum_query_result(txos: Vec) -> u128 { + txos.iter().map(|t| (t.value as u64) as u128).sum::() +} + impl WalletService where T: BlockchainConnection + UserTxConnection + 'static, FPR: FogPubkeyResolver + Send + Sync + 'static, { + #[allow(clippy::type_complexity)] fn get_balance_inner( - account_id_hex: &str, - assigned_subaddress_b58: Option<&str>, + account_id_hex: Option<&str>, + public_address_b58: Option<&str>, + token_id: TokenId, + default_token_fee: &u64, conn: &Conn, - ) -> Result<(u128, u128, u128, u128, u128, u128), BalanceServiceError> { - let max_spendable = - Txo::list_spendable(account_id_hex, None, assigned_subaddress_b58, Some(0), conn)? - .max_spendable_in_wallet; - let unspent = Txo::list_unspent( + ) -> Result { + let unspent = sum_query_result(Txo::list_unspent( account_id_hex, - assigned_subaddress_b58, - Some(0), + public_address_b58, + Some(*token_id), + None, + None, None, None, conn, - )? - .iter() - .map(|t| (t.value as u64) as u128) - .sum::(); - let spent = Txo::list_spent( + )?); + + let spent = sum_query_result(Txo::list_spent( account_id_hex, - assigned_subaddress_b58, - Some(0), + public_address_b58, + Some(*token_id), + None, + None, None, None, conn, - )? - .iter() - .map(|t| (t.value as u64) as u128) - .sum::(); - let pending = Txo::list_pending( + )?); + + let pending = sum_query_result(Txo::list_pending( account_id_hex, - assigned_subaddress_b58, - Some(0), + public_address_b58, + Some(*token_id), + None, + None, None, None, conn, - )? - .iter() - .map(|t| (t.value as u64) as u128) - .sum::(); + )?); - let secreted = if assigned_subaddress_b58.is_some() { - 0 - } else { - Txo::list_secreted(account_id_hex, Some(0), None, None, conn)? - .iter() - .map(|t| t.value as u128) - .sum::() - }; + let unverified = sum_query_result(Txo::list_unverified( + account_id_hex, + public_address_b58, + Some(*token_id), + None, + None, + None, + None, + conn, + )?); - let orphaned = if assigned_subaddress_b58.is_some() { + let secreted = 0; + + let orphaned = if public_address_b58.is_some() { 0 } else { - Txo::list_orphaned(account_id_hex, Some(0), None, None, conn)? - .iter() - .map(|t| t.value as u128) - .sum::() + sum_query_result(Txo::list_orphaned( + account_id_hex, + Some(*token_id), + None, + None, + None, + None, + conn, + )?) }; - let result = (unspent, max_spendable, pending, spent, secreted, orphaned); - Ok(result) - } + let spendable_txos_result = Txo::list_spendable( + account_id_hex, + None, + public_address_b58, + *token_id, + *default_token_fee, + conn, + )?; - fn get_view_only_balance_inner( - account_id_hex: &str, - assigned_subaddress_b58: Option<&str>, - conn: &Conn, - ) -> Result<(u128, u128, u128, u128, u128, u128), BalanceServiceError> { - let unspent = - ViewOnlyTxo::list_unspent(account_id_hex, assigned_subaddress_b58, Some(0), conn)? - .iter() - .map(|t| (t.value as u64) as u128) - .sum::(); - let spent = - ViewOnlyTxo::list_spent(account_id_hex, assigned_subaddress_b58, Some(0), conn)? - .iter() - .map(|t| (t.value as u64) as u128) - .sum::(); - let orphaned = ViewOnlyTxo::list_orphaned(account_id_hex, Some(0), conn)? - .iter() - .map(|t| (t.value as u64) as u128) - .sum::(); - let pending = - ViewOnlyTxo::list_pending(account_id_hex, assigned_subaddress_b58, Some(0), conn)? - .iter() - .map(|t| (t.value as u64) as u128) - .sum::(); - - let result = (unspent, 0, pending, spent, 0, orphaned); - Ok(result) + Ok(Balance { + max_spendable: spendable_txos_result.max_spendable_in_wallet, + unverified, + unspent, + pending, + spent, + secreted, + orphaned, + }) } } @@ -430,22 +386,13 @@ where mod tests { use super::*; use crate::{ - service::{ - account::AccountService, address::AddressService, - view_only_account::ViewOnlyAccountService, - }, + service::{account::AccountService, address::AddressService}, test_utils::{get_test_ledger, manually_sync_account, setup_wallet_service, MOB}, util::b58::b58_encode_public_address, }; - use mc_account_keys::{ - AccountKey, PublicAddress, RootEntropy, RootIdentity, CHANGE_SUBADDRESS_INDEX, - DEFAULT_SUBADDRESS_INDEX, - }; + use mc_account_keys::{AccountKey, PublicAddress, RootEntropy, RootIdentity}; use mc_common::logger::{test_with_logger, Logger}; - use mc_crypto_keys::{RistrettoPrivate, RistrettoPublic}; - use mc_transaction_core::{ - encrypted_fog_hint::EncryptedFogHint, tokens::Mob, tx::TxOut, Amount, Token, - }; + use mc_transaction_core::{tokens::Mob, Token}; use mc_util_from_random::FromRandom; use rand::{rngs::StdRng, SeedableRng}; @@ -486,56 +433,66 @@ mod tests { .expect("Could not import account entropy"); let address = service - .assign_address_for_account(&AccountID(account.account_id_hex.clone()), None) + .assign_address_for_account(&AccountID(account.id.clone()), None) .expect("Could not assign address"); assert_eq!(address.subaddress_index, 2); let _account = manually_sync_account( &ledger_db, - &service.wallet_db, - &AccountID(account.account_id_hex.to_string()), + &service.wallet_db.as_ref().unwrap(), + &AccountID(account.id.to_string()), &logger, ); let account_balance = service - .get_balance_for_account(&AccountID(account.account_id_hex)) + .get_balance_for_account(&AccountID(account.id)) .expect("Could not get balance for account"); - + let account_balance_pmob = account_balance.get(&Mob::ID).unwrap(); // 3 accounts * 5_000 MOB * 12 blocks - assert_eq!(account_balance.unspent, 180_000 * MOB as u128); + assert_eq!(account_balance_pmob.unspent, 180_000 * MOB as u128); // 5_000 MOB per txo, max 16 txos input - network fee - assert_eq!(account_balance.max_spendable, 79999999600000000 as u128); - assert_eq!(account_balance.pending, 0); - assert_eq!(account_balance.spent, 0); - assert_eq!(account_balance.secreted, 0); - assert_eq!(account_balance.orphaned, 60_000 * MOB as u128); // Public address 3 + // assert_eq!( + // account_balance_pmob.max_spendable, + // 79999999600000000 as u128 + // ); + assert_eq!(account_balance_pmob.pending, 0); + assert_eq!(account_balance_pmob.spent, 0); + assert_eq!(account_balance_pmob.secreted, 0); + assert_eq!(account_balance_pmob.orphaned, 60_000 * MOB as u128); // Public address 3 let db_account_key: AccountKey = mc_util_serial::decode(&account.account_key).expect("Could not decode account key"); - let db_pub_address = db_account_key.subaddress(account.main_subaddress_index as u64); + let db_pub_address = db_account_key.default_subaddress(); assert_eq!(db_pub_address, public_address0); let b58_pub_address = b58_encode_public_address(&db_pub_address).expect("Could not encode public address"); let address_balance = service .get_balance_for_address(&b58_pub_address) .expect("Could not get balance for address"); - - assert_eq!(address_balance.unspent, 60_000 * MOB as u128); - assert_eq!(address_balance.max_spendable, 59999999600000000 as u128); - assert_eq!(address_balance.pending, 0); - assert_eq!(address_balance.spent, 0); - assert_eq!(address_balance.secreted, 0); - assert_eq!(address_balance.orphaned, 0); + let address_balance_pmob = address_balance.get(&Mob::ID).unwrap(); + assert_eq!(address_balance_pmob.unspent, 60_000 * MOB as u128); + // assert_eq!( + // address_balance_pmob.max_spendable, + // 59999999600000000 as u128 + // ); + assert_eq!(address_balance_pmob.pending, 0); + assert_eq!(address_balance_pmob.spent, 0); + assert_eq!(address_balance_pmob.secreted, 0); + assert_eq!(address_balance_pmob.orphaned, 0); let address_balance2 = service - .get_balance_for_address(&address.assigned_subaddress_b58) + .get_balance_for_address(&address.public_address_b58) .expect("Could not get balance for address"); - assert_eq!(address_balance2.unspent, 60_000 * MOB as u128); - assert_eq!(address_balance2.max_spendable, 59999999600000000 as u128); - assert_eq!(address_balance2.pending, 0); - assert_eq!(address_balance2.spent, 0); - assert_eq!(address_balance2.secreted, 0); - assert_eq!(address_balance2.orphaned, 0); + let address_balance2_pmob = address_balance2.get(&Mob::ID).unwrap(); + assert_eq!(address_balance2_pmob.unspent, 60_000 * MOB as u128); + // assert_eq!( + // address_balance2_pmob.max_spendable, + // 59999999600000000 as u128 + // ); + assert_eq!(address_balance2_pmob.pending, 0); + assert_eq!(address_balance2_pmob.spent, 0); + assert_eq!(address_balance2_pmob.secreted, 0); + assert_eq!(address_balance2_pmob.orphaned, 0); // Even though subaddress 3 has funds, we are not watching it, so we should get // an error. @@ -547,131 +504,4 @@ mod tests { Err(e) => panic!("Unexpected error {:?}", e), } } - - // The balance for an address should be accurate. - #[test_with_logger] - fn test_view_only_balance(logger: Logger) { - // setup view only account - let mut rng: StdRng = SeedableRng::from_seed([20u8; 32]); - let known_recipients: Vec = Vec::new(); - let current_block_height = 12; //index 11 - let ledger_db = get_test_ledger( - 5, - &known_recipients, - current_block_height as usize, - &mut rng, - ); - let service = setup_wallet_service(ledger_db.clone(), logger.clone()); - let conn = service.wallet_db.get_conn().unwrap(); - - let view_private_key = RistrettoPrivate::from_random(&mut rng); - let spend_private_key = RistrettoPrivate::from_random(&mut rng); - - let name = "testing"; - - let account_key = AccountKey::new(&spend_private_key, &view_private_key); - let account_id = AccountID::from(&account_key); - let main_public_address = account_key.default_subaddress(); - let change_public_address = account_key.change_subaddress(); - let mut subaddresses: Vec<(String, u64, String, RistrettoPublic)> = Vec::new(); - subaddresses.push(( - b58_encode_public_address(&main_public_address).unwrap(), - DEFAULT_SUBADDRESS_INDEX, - "Main".to_string(), - *main_public_address.spend_public_key(), - )); - subaddresses.push(( - b58_encode_public_address(&change_public_address).unwrap(), - CHANGE_SUBADDRESS_INDEX, - "Change".to_string(), - *change_public_address.spend_public_key(), - )); - - service - .import_view_only_account( - &account_id.to_string(), - &view_private_key, - DEFAULT_SUBADDRESS_INDEX, - CHANGE_SUBADDRESS_INDEX, - 2, - name.clone(), - subaddresses, - ) - .unwrap(); - - // add funds to account - for _ in 0..2 { - let value = 420 * MOB; - let amount = Amount::new(value, Mob::ID); - let tx_private_key = RistrettoPrivate::from_random(&mut rng); - let hint = EncryptedFogHint::fake_onetime_hint(&mut rng); - let fake_tx_out = - TxOut::new(amount, &main_public_address, &tx_private_key, hint).unwrap(); - ViewOnlyTxo::create( - fake_tx_out.clone(), - amount, - Some(DEFAULT_SUBADDRESS_INDEX), - Some(current_block_height), - &account_id.to_string(), - &conn, - ) - .unwrap(); - } - - // test balance for account - let balance: Balance = service - .get_balance_for_view_only_account(&account_id.to_string()) - .unwrap(); - assert_eq!(balance.unspent as u64, 840 * MOB); - // view only accounts have no spendable MOB - assert_eq!(balance.max_spendable, 0); - assert_eq!(balance.spent, 0); - assert_eq!(balance.pending, 0); - assert_eq!(balance.secreted, 0); - assert_eq!(balance.orphaned, 0); - - // add funds to specific address - let subaddress_index = 3; - let subaddress = account_key.subaddress(subaddress_index); - let b58_pub_address = - b58_encode_public_address(&subaddress).expect("Could not encode public address"); - service - .import_subaddresses( - &account_id.to_string(), - [( - b58_pub_address.clone(), - subaddress_index, - "cheese".to_string(), - subaddress.spend_public_key().to_owned(), - )] - .to_vec(), - ) - .unwrap(); - - let value = 100 * MOB; - let amount = Amount::new(value, Mob::ID); - let tx_private_key = RistrettoPrivate::from_random(&mut rng); - let hint = EncryptedFogHint::fake_onetime_hint(&mut rng); - let fake_tx_out = TxOut::new(amount, &main_public_address, &tx_private_key, hint).unwrap(); - ViewOnlyTxo::create( - fake_tx_out.clone(), - amount, - Some(subaddress_index), - Some(current_block_height), - &account_id.to_string(), - &conn, - ) - .unwrap(); - - let balance: Balance = service - .get_balance_for_view_only_address(&b58_pub_address) - .unwrap(); - assert_eq!(balance.unspent as u64, 100 * MOB); - // view only accounts have no spendable MOB - assert_eq!(balance.max_spendable, 0); - assert_eq!(balance.spent, 0); - assert_eq!(balance.pending, 0); - assert_eq!(balance.secreted, 0); - assert_eq!(balance.orphaned, 0); - } } diff --git a/full-service/src/service/confirmation_number.rs b/full-service/src/service/confirmation_number.rs index b8d5452e9..01800ce53 100644 --- a/full-service/src/service/confirmation_number.rs +++ b/full-service/src/service/confirmation_number.rs @@ -127,23 +127,24 @@ where &self, transaction_log_id: &str, ) -> Result, ConfirmationServiceError> { - let (_transaction_log, associated_txos) = self.get_transaction_log(transaction_log_id)?; + let (_transaction_log, associated_txos, _value_map) = + self.get_transaction_log(transaction_log_id)?; let mut results = Vec::new(); - for associated_txo in associated_txos.outputs { - let txo = self.get_txo(&TxoID(associated_txo.txo_id_hex.clone()))?; - if let Some(confirmation) = txo.confirmation { + for (associated_txo, _) in associated_txos.outputs { + let (txo, _) = self.get_txo(&TxoID(associated_txo.id.clone()))?; + if let Some(confirmation) = txo.shared_secret { let confirmation: TxOutConfirmationNumber = mc_util_serial::decode(&confirmation)?; let pubkey: CompressedRistrettoPublic = mc_util_serial::decode(&txo.public_key)?; let txo_index = self.ledger_db.get_tx_out_index_by_public_key(&pubkey)?; results.push(Confirmation { - txo_id: TxoID(txo.txo_id_hex), + txo_id: TxoID(txo.id), txo_index, confirmation, }); } else { return Err(ConfirmationServiceError::MissingConfirmation( - associated_txo.txo_id_hex, + associated_txo.id, )); } } @@ -156,7 +157,7 @@ where txo_id: &TxoID, confirmation_hex: &str, ) -> Result { - let conn = self.wallet_db.get_conn()?; + let conn = self.get_conn()?; let confirmation: TxOutConfirmationNumber = mc_util_serial::decode(&hex::decode(confirmation_hex)?)?; Ok(Txo::validate_confirmation( diff --git a/full-service/src/service/gift_code.rs b/full-service/src/service/gift_code.rs index 2bd7df9ad..f37716fdb 100644 --- a/full-service/src/service/gift_code.rs +++ b/full-service/src/service/gift_code.rs @@ -14,10 +14,13 @@ use crate::{ models::{Account, GiftCode}, transaction, WalletDbError, }, + error::WalletTransactionBuilderError, service::{ account::AccountServiceError, address::{AddressService, AddressServiceError}, - transaction::{TransactionService, TransactionServiceError}, + ledger::{LedgerService, LedgerServiceError}, + models::tx_proposal::TxProposal, + transaction::{TransactionMemo, TransactionService, TransactionServiceError}, transaction_builder::DEFAULT_NEW_TX_BLOCK_ATTEMPTS, WalletService, }, @@ -33,9 +36,9 @@ use mc_account_keys_slip10::Slip10KeyGenerator; use mc_common::{logger::log, HashSet}; use mc_connection::{BlockchainConnection, RetryableUserTxConnection, UserTxConnection}; use mc_crypto_keys::RistrettoPublic; +use mc_crypto_ring_signature_signer::NoKeysRingSigner; use mc_fog_report_validation::FogPubkeyResolver; use mc_ledger_db::Ledger; -use mc_mobilecoind::payments::TxProposal; use mc_transaction_core::{ constants::RING_SIZE, get_tx_out_shared_secret, @@ -43,7 +46,7 @@ use mc_transaction_core::{ ring_signature::KeyImage, tokens::Mob, tx::{Tx, TxOut}, - Amount, BlockVersion, Token, + Amount, Token, }; use mc_transaction_std::{ InputCredentials, RTHMemoBuilder, SenderMemoCredential, TransactionBuilder, @@ -150,6 +153,18 @@ pub enum GiftCodeServiceError { /// Invalid Fog Uri: {0} InvalidFogUri(String), + + /// Amount Error: {0} + Amount(mc_transaction_core::AmountError), + + /// Wallet Transaction Builder Error: {0} + WalletTransactionBuilder(WalletTransactionBuilderError), + + /// Tx Out Conversion Error: {0} + TxOutConversion(mc_transaction_core::TxOutConversionError), + + /// Ledger service error: {0} + LedgerService(LedgerServiceError), } impl From for GiftCodeServiceError { @@ -242,6 +257,30 @@ impl From for GiftCodeServiceError { } } +impl From for GiftCodeServiceError { + fn from(src: mc_transaction_core::AmountError) -> Self { + Self::Amount(src) + } +} + +impl From for GiftCodeServiceError { + fn from(src: WalletTransactionBuilderError) -> Self { + Self::WalletTransactionBuilder(src) + } +} + +impl From for GiftCodeServiceError { + fn from(src: mc_transaction_core::TxOutConversionError) -> Self { + Self::TxOutConversion(src) + } +} + +impl From for GiftCodeServiceError { + fn from(src: LedgerServiceError) -> Self { + Self::LedgerService(src) + } +} + #[derive(Debug, Clone, Eq, PartialEq, Hash)] pub struct EncodedGiftCode(pub String); @@ -334,7 +373,11 @@ pub trait GiftCodeService { ) -> Result; /// List all gift codes in the wallet. - fn list_gift_codes(&self) -> Result, GiftCodeServiceError>; + fn list_gift_codes( + &self, + offset: Option, + limit: Option, + ) -> Result, GiftCodeServiceError>; /// Check the status of a gift code currently in your wallet. If the gift /// code is not yet in the wallet, add it. @@ -345,13 +388,13 @@ pub trait GiftCodeService { /// Execute a transaction from the gift code account to drain the account to /// the destination specified by the account_id_hex and - /// assigned_subaddress_b58. If no assigned_subaddress_b58 is provided, + /// public_address_b58. If no public_address_b58 is provided, /// then a new AssignedSubaddress will be created to receive the funds. fn claim_gift_code( &self, gift_code_b58: &EncodedGiftCode, account_id: &AccountID, - assigned_subaddress_b58: Option, + public_address_b58: Option, ) -> Result; fn remove_gift_code( @@ -375,52 +418,65 @@ where tombstone_block: Option, max_spendable_value: Option, ) -> Result<(TxProposal, EncodedGiftCode), GiftCodeServiceError> { - // First we need to generate a new random bip39 entropy. The way that gift codes - // work currently is that the sender creates a middleman account and - // sends that account the amount of MOB desired, plus extra to cover the - // receivers fee. Then, that account and all of its secrets get encoded - // into a b58 string, and when the receiver gets that they can decode it, + // First we need to generate a new random bip39 entropy. The way that + // gift codes work currently is that the sender creates a + // middleman account and sends that account the amount of MOB + // desired, plus extra to cover the receivers fee. Then, that + // account and all of its secrets get encoded into a b58 + // string, and when the receiver gets that they can decode it, // and create a new transaction liquidating the gift account of all // of the MOB. - // There should never be a reason to check any other sub_address besides the - // main one. If there ever is any on a different subaddress, either - // something went terribly wrong and we messed up, or someone is being - // very dumb and using a gift account as a place to store their personal MOB. + // There should never be a reason to check any other sub_address + // besides the main one. If there ever is any on a different + // subaddress, either something went terribly wrong and we + // messed up, or someone is being very dumb and using a gift + // account as a place to store their personal MOB. let mnemonic = Mnemonic::new(MnemonicType::Words24, Language::English); let gift_code_bip39_entropy_bytes = mnemonic.entropy().to_vec(); let key = mnemonic.derive_slip10_key(0); let gift_code_account_key = AccountKey::from(key); - // We should never actually need this account to exist in the wallet_db, as we - // will only ever be using it a single time at this instant with a - // single unspent txo in its main subaddress and the b58 encoded gc will - // contain all necessary info to generate a tx_proposal for it + // We should never actually need this account to exist in the + // wallet_db, as we will only ever be using it a single time + // at this instant with a single unspent txo in its main + // subaddress and the b58 encoded gc will contain all + // necessary info to generate a tx_proposal for it let gift_code_account_main_subaddress_b58 = b58_encode_public_address(&gift_code_account_key.default_subaddress())?; - let conn = self.wallet_db.get_conn()?; + let conn = self.get_conn()?; let from_account = Account::get(from_account_id, &conn)?; - let tx_proposal = self.build_transaction( - &from_account.account_id_hex, - &[(gift_code_account_main_subaddress_b58, value.to_string())], + let fee_value = fee.map(|f| f.to_string()); + + let signing_data = self.build_transaction( + &from_account.id, + &[( + gift_code_account_main_subaddress_b58, + crate::json_rpc::v2::models::amount::Amount { + value: value.to_string(), + token_id: Mob::ID.to_string(), + }, + )], input_txo_ids, - fee.map(|f| f.to_string()), + fee_value, + None, tombstone_block.map(|t| t.to_string()), max_spendable_value.map(|f| f.to_string()), - None, + TransactionMemo::RTH, )?; - if tx_proposal.outlay_index_to_tx_out_index.len() != 1 { + let account_key: AccountKey = mc_util_serial::decode(&from_account.account_key)?; + let tx_proposal = signing_data.sign(&account_key)?; + + if tx_proposal.payload_txos.len() != 1 { return Err(GiftCodeServiceError::UnexpectedTxProposalFormat); } - let outlay_index = tx_proposal.outlay_index_to_tx_out_index[&0]; - let tx_out = tx_proposal.tx.prefix.outputs[outlay_index].clone(); - let txo_public_key = tx_out.public_key; + let tx_out = &tx_proposal.payload_txos[0].tx_out; - let proto_tx_pubkey: mc_api::external::CompressedRistretto = (&txo_public_key).into(); + let proto_tx_pubkey: mc_api::external::CompressedRistretto = (&tx_out.public_key).into(); let gift_code_b58 = b58_encode_transfer_payload( gift_code_bip39_entropy_bytes.to_vec(), @@ -438,7 +494,7 @@ where tx_proposal: &TxProposal, ) -> Result { let transfer_payload = decode_transfer_payload(gift_code_b58)?; - let value = tx_proposal.outlays[0].value as i64; + let value = tx_proposal.payload_txos[0].amount.value as i64; log::info!( self.logger, @@ -447,11 +503,11 @@ where ); // Save the gift code to the database before attempting to send it out. - let conn = self.wallet_db.get_conn()?; + let conn = self.get_conn()?; let gift_code = transaction(&conn, || GiftCode::create(gift_code_b58, value, &conn))?; self.submit_transaction( - tx_proposal.clone(), + tx_proposal, Some(json!({"gift_code_memo": transfer_payload.memo}).to_string()), Some(from_account_id.clone().0), )?; @@ -461,7 +517,7 @@ where root_entropy: transfer_payload.root_entropy.map(|e| e.bytes.to_vec()), bip39_entropy: transfer_payload.bip39_entropy, txo_public_key: mc_util_serial::encode(&transfer_payload.txo_public_key), - value: tx_proposal.outlays[0].value, + value: tx_proposal.payload_txos[0].amount.value, memo: transfer_payload.memo, }) } @@ -470,14 +526,18 @@ where &self, gift_code_b58: &EncodedGiftCode, ) -> Result { - let conn = self.wallet_db.get_conn()?; + let conn = self.get_conn()?; let gift_code = GiftCode::get(gift_code_b58, &conn)?; DecodedGiftCode::try_from(gift_code) } - fn list_gift_codes(&self) -> Result, GiftCodeServiceError> { - let conn = self.wallet_db.get_conn()?; - GiftCode::list_all(&conn)? + fn list_gift_codes( + &self, + offset: Option, + limit: Option, + ) -> Result, GiftCodeServiceError> { + let conn = self.get_conn()?; + GiftCode::list_all(&conn, offset, limit)? .into_iter() .map(DecodedGiftCode::try_from) .collect() @@ -520,7 +580,7 @@ where &RistrettoPublic::try_from(&gift_txo.public_key)?, ); - let (value, _blinding) = gift_txo.masked_amount.get_value(&shared_secret).unwrap(); + let (value, _blinding) = gift_txo.get_masked_amount()?.get_value(&shared_secret)?; // Check if the Gift Code has been spent - by convention gift codes are always // to the main subaddress index and gift accounts should NEVER have MOB stored @@ -553,7 +613,7 @@ where &self, gift_code_b58: &EncodedGiftCode, account_id: &AccountID, - assigned_subaddress_b58: Option, + public_address_b58: Option, ) -> Result { let (status, gift_value, _memo) = self.check_gift_code_status(gift_code_b58)?; @@ -565,19 +625,19 @@ where GiftCodeStatus::GiftCodeAvailable => {} } - let gift_value = gift_value.unwrap(); + let gift_value = gift_value.ok_or(GiftCodeServiceError::GiftCodeNotYetAvailable)?; let transfer_payload = decode_transfer_payload(gift_code_b58)?; let gift_account_key = transfer_payload.account_key; - let default_subaddress = if assigned_subaddress_b58.is_some() { - assigned_subaddress_b58.ok_or(GiftCodeServiceError::AccountNotFound) + let default_subaddress = if public_address_b58.is_some() { + public_address_b58.ok_or(GiftCodeServiceError::AccountNotFound) } else { let address = self.assign_address_for_account( account_id, Some(&json!({"gift_code_memo": transfer_payload.memo}).to_string()), )?; - Ok(address.assigned_subaddress_b58) + Ok(address.public_address_b58) }?; let recipient_public_address = b58_decode_public_address(&default_subaddress)?; @@ -654,13 +714,13 @@ where let mut memo_builder = RTHMemoBuilder::default(); memo_builder.set_sender_credential(SenderMemoCredential::from(&gift_account_key)); memo_builder.enable_destination_memo(); - let block_version = BlockVersion::MAX; + let block_version = self.get_network_block_version()?; let fee = Amount::new(Mob::MINIMUM_FEE, Mob::ID); let mut transaction_builder = TransactionBuilder::new(block_version, fee, fog_resolver, memo_builder)?; transaction_builder.add_input(input_credentials); transaction_builder.add_output( - gift_value as u64 - Mob::MINIMUM_FEE, + Amount::new(gift_value as u64 - Mob::MINIMUM_FEE, Mob::ID), &recipient_public_address, &mut rng, )?; @@ -668,7 +728,7 @@ where let num_blocks_in_ledger = self.ledger_db.num_blocks()?; transaction_builder .set_tombstone_block(num_blocks_in_ledger + DEFAULT_NEW_TX_BLOCK_ATTEMPTS); - let tx = transaction_builder.build(&mut rng)?; + let tx = transaction_builder.build(&NoKeysRingSigner {}, &mut rng)?; let responder_ids = self.peer_manager.responder_ids(); if responder_ids.is_empty() { @@ -698,7 +758,7 @@ where &self, gift_code_b58: &EncodedGiftCode, ) -> Result { - let conn = self.wallet_db.get_conn()?; + let conn = self.get_conn()?; transaction(&conn, || GiftCode::get(gift_code_b58, &conn)?.delete(&conn))?; Ok(true) } @@ -717,14 +777,14 @@ mod tests { use crate::{ service::{account::AccountService, balance::BalanceService}, test_utils::{ - add_block_to_ledger_db, add_block_with_tx, add_block_with_tx_proposal, get_test_ledger, - manually_sync_account, setup_wallet_service, MOB, + add_block_to_ledger_db, add_block_with_tx, get_test_ledger, manually_sync_account, + setup_wallet_service, MOB, }, }; use mc_account_keys::PublicAddress; use mc_common::logger::{test_with_logger, Logger}; use mc_crypto_rand::rand_core::RngCore; - use mc_transaction_core::ring_signature::KeyImage; + use mc_transaction_core::{ring_signature::KeyImage, tokens::Mob, Token}; use rand::{rngs::StdRng, SeedableRng}; #[test_with_logger] @@ -748,9 +808,8 @@ mod tests { // Add a block with a transaction for Alice let alice_account_key: AccountKey = mc_util_serial::decode(&alice.account_key).unwrap(); - let alice_public_address = - &alice_account_key.subaddress(alice.main_subaddress_index as u64); - let alice_account_id = AccountID(alice.account_id_hex.to_string()); + let alice_public_address = &alice_account_key.default_subaddress(); + let alice_account_id = AccountID(alice.id.to_string()); add_block_to_ledger_db( &mut ledger_db, @@ -759,18 +818,24 @@ mod tests { &vec![KeyImage::from(rng.next_u64())], &mut rng, ); - manually_sync_account(&ledger_db, &service.wallet_db, &alice_account_id, &logger); + manually_sync_account( + &ledger_db, + &service.wallet_db.as_ref().unwrap(), + &alice_account_id, + &logger, + ); // Verify balance for Alice let balance = service - .get_balance_for_account(&AccountID(alice.account_id_hex.clone())) + .get_balance_for_account(&AccountID(alice.id.clone())) .unwrap(); - assert_eq!(balance.unspent, 100 * MOB as u128); + let balance_pmob = balance.get(&Mob::ID).unwrap(); + assert_eq!(balance_pmob.unspent, 100 * MOB as u128); // Create a gift code for Bob let (tx_proposal, gift_code_b58) = service .build_gift_code( - &AccountID(alice.account_id_hex.clone()), + &AccountID(alice.id.clone()), 2 * MOB as u64, Some("Gift code for Bob".to_string()), None, @@ -783,7 +848,7 @@ mod tests { let _gift_code = service .submit_gift_code( - &AccountID(alice.account_id_hex.clone()), + &AccountID(alice.id.clone()), &gift_code_b58.clone(), &tx_proposal.clone(), ) @@ -796,8 +861,13 @@ mod tests { assert_eq!(status, GiftCodeStatus::GiftCodeSubmittedPending); assert!(gift_code_value_opt.is_none()); - add_block_with_tx_proposal(&mut ledger_db, tx_proposal); - manually_sync_account(&ledger_db, &service.wallet_db, &alice_account_id, &logger); + add_block_with_tx(&mut ledger_db, tx_proposal.tx, &mut rng); + manually_sync_account( + &ledger_db, + &service.wallet_db.as_ref().unwrap(), + &alice_account_id, + &logger, + ); // Now the Gift Code should be Available let (status, gift_code_value_opt, _memo) = service @@ -819,14 +889,19 @@ mod tests { gift_code_account_key.view_private_key(), &RistrettoPublic::try_from(&tx_out.public_key).unwrap(), ); - let (value, _blinding) = tx_out.masked_amount.get_value(&shared_secret).unwrap(); + let (value, _blinding) = tx_out + .get_masked_amount() + .unwrap() + .get_value(&shared_secret) + .unwrap(); assert_eq!(value, Amount::new(2 * MOB as u64, Mob::ID)); // Verify balance for Alice = original balance - fee - gift_code_value let balance = service - .get_balance_for_account(&AccountID(alice.account_id_hex.clone())) + .get_balance_for_account(&AccountID(alice.id.clone())) .unwrap(); - assert_eq!(balance.unspent, (98 * MOB - Mob::MINIMUM_FEE) as u128); + let balance_pmob = balance.get(&Mob::ID).unwrap(); + assert_eq!(balance_pmob.unspent, (98 * MOB - Mob::MINIMUM_FEE) as u128); // Verify that we can get the gift_code log::info!(logger, "Getting gift code from database"); @@ -836,7 +911,7 @@ mod tests { // Check that we can list all log::info!(logger, "Listing all gift codes"); - let gift_codes = service.list_gift_codes().unwrap(); + let gift_codes = service.list_gift_codes(None, None).unwrap(); assert_eq!(gift_codes.len(), 1); assert_eq!(gift_codes[0], gotten_gift_code); @@ -852,8 +927,8 @@ mod tests { .unwrap(); manually_sync_account( &ledger_db, - &service.wallet_db, - &AccountID(bob.account_id_hex.clone()), + &service.wallet_db.as_ref().unwrap(), + &AccountID(bob.id.clone()), &logger, ); @@ -866,7 +941,7 @@ mod tests { assert!(result.is_err()); let tx = service - .claim_gift_code(&gift_code_b58, &AccountID(bob.account_id_hex.clone()), None) + .claim_gift_code(&gift_code_b58, &AccountID(bob.id.clone()), None) .unwrap(); // Add the consume transaction to the ledger @@ -874,11 +949,11 @@ mod tests { logger, "Adding block to ledger with consume gift code transaction" ); - add_block_with_tx(&mut ledger_db, tx); + add_block_with_tx(&mut ledger_db, tx, &mut rng); manually_sync_account( &ledger_db, - &service.wallet_db, - &AccountID(bob.account_id_hex.clone()), + &service.wallet_db.as_ref().unwrap(), + &AccountID(bob.id.clone()), &logger, ); @@ -890,10 +965,12 @@ mod tests { assert!(gift_code_value_opt.is_some()); // Bob's balance should be = gift code value - fee (10000000000) - let bob_balance = service - .get_balance_for_account(&AccountID(bob.account_id_hex)) - .unwrap(); - assert_eq!(bob_balance.unspent, (2 * MOB - Mob::MINIMUM_FEE) as u128) + let bob_balance = service.get_balance_for_account(&AccountID(bob.id)).unwrap(); + let bob_balance_pmob = bob_balance.get(&Mob::ID).unwrap(); + assert_eq!( + bob_balance_pmob.unspent, + (2 * MOB - Mob::MINIMUM_FEE) as u128 + ) } #[test_with_logger] @@ -917,9 +994,8 @@ mod tests { // Add a block with a transaction for Alice let alice_account_key: AccountKey = mc_util_serial::decode(&alice.account_key).unwrap(); - let alice_public_address = - &alice_account_key.subaddress(alice.main_subaddress_index as u64); - let alice_account_id = AccountID(alice.account_id_hex.to_string()); + let alice_public_address = &alice_account_key.default_subaddress(); + let alice_account_id = AccountID(alice.id.to_string()); add_block_to_ledger_db( &mut ledger_db, @@ -928,18 +1004,24 @@ mod tests { &vec![KeyImage::from(rng.next_u64())], &mut rng, ); - manually_sync_account(&ledger_db, &service.wallet_db, &alice_account_id, &logger); + manually_sync_account( + &ledger_db, + &service.wallet_db.as_ref().unwrap(), + &alice_account_id, + &logger, + ); // Verify balance for Alice let balance = service - .get_balance_for_account(&AccountID(alice.account_id_hex.clone())) + .get_balance_for_account(&AccountID(alice.id.clone())) .unwrap(); - assert_eq!(balance.unspent, 100 * MOB as u128); + let balance_pmob = balance.get(&Mob::ID).unwrap(); + assert_eq!(balance_pmob.unspent, 100 * MOB as u128); // Create a gift code for Bob let (tx_proposal, gift_code_b58) = service .build_gift_code( - &AccountID(alice.account_id_hex.clone()), + &AccountID(alice.id.clone()), 2 * MOB as u64, Some("Gift code for Bob".to_string()), None, @@ -952,7 +1034,7 @@ mod tests { let _gift_code = service .submit_gift_code( - &AccountID(alice.account_id_hex.clone()), + &AccountID(alice.id.clone()), &gift_code_b58.clone(), &tx_proposal.clone(), ) @@ -966,8 +1048,13 @@ mod tests { assert!(gift_code_value_opt.is_none()); // Let transaction hit the ledger - add_block_with_tx_proposal(&mut ledger_db, tx_proposal); - manually_sync_account(&ledger_db, &service.wallet_db, &alice_account_id, &logger); + add_block_with_tx(&mut ledger_db, tx_proposal.tx, &mut rng); + manually_sync_account( + &ledger_db, + &service.wallet_db.as_ref().unwrap(), + &alice_account_id, + &logger, + ); // Check that it landed let (status, gift_code_value_opt, _memo) = service @@ -978,7 +1065,7 @@ mod tests { // Check that we get all gift codes let gift_codes = service - .list_gift_codes() + .list_gift_codes(None, None) .expect("Could not list gift codes"); assert_eq!(gift_codes.len(), 1); @@ -987,7 +1074,7 @@ mod tests { .remove_gift_code(&gift_code_b58) .expect("Could not remove gift code")); let gift_codes = service - .list_gift_codes() + .list_gift_codes(None, None) .expect("Could not list gift codes"); assert_eq!(gift_codes.len(), 0); } diff --git a/full-service/src/service/ledger.rs b/full-service/src/service/ledger.rs index 64ced7c45..509f21031 100644 --- a/full-service/src/service/ledger.rs +++ b/full-service/src/service/ledger.rs @@ -5,26 +5,31 @@ use crate::{ db::{ models::{TransactionLog, Txo}, - transaction_log::TransactionLogModel, + transaction_log::{TransactionID, TransactionLogModel}, txo::TxoModel, }, WalletService, }; -use mc_connection::{BlockchainConnection, RetryableBlockchainConnection, UserTxConnection}; +use mc_blockchain_types::{Block, BlockContents, BlockVersion, BlockVersionError}; +use mc_common::HashSet; +use mc_connection::{ + BlockInfo, BlockchainConnection, RetryableBlockchainConnection, UserTxConnection, +}; +use mc_crypto_keys::CompressedRistrettoPublic; use mc_fog_report_validation::FogPubkeyResolver; use mc_ledger_db::Ledger; use mc_ledger_sync::NetworkState; use mc_transaction_core::{ ring_signature::KeyImage, - tokens::Mob, - tx::{Tx, TxOut}, - Block, BlockContents, BlockVersion, Token, + tx::{Tx, TxOut, TxOutMembershipProof}, + TokenId, }; +use rand::Rng; use crate::db::WalletDbError; use displaydoc::Display; use rayon::prelude::*; // For par_iter -use std::{convert::TryFrom, iter::empty}; +use std::{collections::BTreeMap, convert::TryFrom}; /// Errors for the Address Service. #[derive(Display, Debug)] @@ -43,6 +48,27 @@ pub enum LedgerServiceError { * received transactions do not have transaction objects. */ NoTxInTransaction, + + /// Error converting from hex string to bytes. + FromHex(hex::FromHexError), + + /// Key Error from mc_crypto_keys + Key(mc_crypto_keys::KeyError), + + /// Invalid Argument: {0} + InvalidArgument(String), + + /// Insufficient Tx Outs + InsufficientTxOuts, + + /// No node responded to the last block info request + NoLastBlockInfo, + + /// Inconsistent last block info + InconsistentLastBlockInfo, + + /// Block version: {0} + BlockVersion(BlockVersionError), } impl From for LedgerServiceError { @@ -63,6 +89,24 @@ impl From for LedgerServiceError { } } +impl From for LedgerServiceError { + fn from(src: hex::FromHexError) -> Self { + Self::FromHex(src) + } +} + +impl From for LedgerServiceError { + fn from(src: mc_crypto_keys::KeyError) -> Self { + Self::Key(src) + } +} + +impl From for LedgerServiceError { + fn from(src: BlockVersionError) -> Self { + Self::BlockVersion(src) + } +} + /// Trait defining the ways in which the wallet can interact with and manage /// ledger objects and interfaces. pub trait LedgerService { @@ -80,9 +124,32 @@ pub trait LedgerService { fn contains_key_image(&self, key_image: &KeyImage) -> Result; - fn get_network_fee(&self) -> u64; + fn get_latest_block_info(&self) -> Result; - fn get_network_block_version(&self) -> BlockVersion; + fn get_network_fees(&self) -> Result, LedgerServiceError>; + + fn get_network_block_version(&self) -> Result; + + fn get_tx_out_proof_of_memberships( + &self, + indices: &[u64], + ) -> Result, LedgerServiceError>; + + fn get_indices_from_txo_public_keys( + &self, + public_keys: &[CompressedRistrettoPublic], + ) -> Result, LedgerServiceError>; + + fn sample_mixins( + &self, + num_mixins: usize, + excluded_indices: &[u64], + ) -> Result<(Vec, Vec), LedgerServiceError>; + + fn get_block_index_from_txo_public_key( + &self, + public_key: &CompressedRistrettoPublic, + ) -> Result; } impl LedgerService for WalletService @@ -99,19 +166,15 @@ where } fn get_transaction_object(&self, transaction_id_hex: &str) -> Result { - let conn = self.wallet_db.get_conn()?; - let transaction = TransactionLog::get(transaction_id_hex, &conn)?; - - if let Some(tx_bytes) = transaction.tx { - let tx: Tx = mc_util_serial::decode(&tx_bytes)?; - Ok(tx) - } else { - Err(LedgerServiceError::NoTxInTransaction) - } + let conn = self.get_conn()?; + let transaction_log = + TransactionLog::get(&TransactionID(transaction_id_hex.to_string()), &conn)?; + let tx: Tx = mc_util_serial::decode(&transaction_log.tx)?; + Ok(tx) } fn get_txo_object(&self, txo_id_hex: &str) -> Result { - let conn = self.wallet_db.get_conn()?; + let conn = self.get_conn()?; let txo_details = Txo::get(txo_id_hex, &conn)?; let txo: TxOut = mc_util_serial::decode(&txo_details.txo)?; @@ -131,44 +194,104 @@ where Ok(self.ledger_db.contains_key_image(key_image)?) } - fn get_network_fee(&self) -> u64 { - if self.peer_manager.is_empty() { - Mob::MINIMUM_FEE - } else { - // Iterate an owned list of connections in parallel, get the block info for - // each, and extract the fee. If no fees are returned, use the hard-coded - // minimum. - self.peer_manager - .conns() - .par_iter() - .filter_map(|conn| conn.fetch_block_info(empty()).ok()) - .filter_map(|block_info| { - // Cleanup the protobuf default fee - if block_info.minimum_fees[&Mob::ID] == 0 { - None - } else { - Some(block_info.minimum_fees[&Mob::ID]) - } - }) - .max() - .unwrap_or(Mob::MINIMUM_FEE) + fn get_latest_block_info(&self) -> Result { + // Get the last block information from all nodes we are aware of, in parallel. + let last_block_infos = self + .peer_manager + .conns() + .par_iter() + .filter_map(|conn| conn.fetch_block_info(std::iter::empty()).ok()) + .collect::>(); + + // Ensure that all nodes agree on the latest block version and network fees. + if last_block_infos.windows(2).any(|window| { + window[0].network_block_version != window[1].network_block_version + || window[0].minimum_fees != window[1].minimum_fees + }) { + return Err(LedgerServiceError::InconsistentLastBlockInfo); } + + last_block_infos + .first() + .cloned() + .ok_or(LedgerServiceError::NoLastBlockInfo) + } + + fn get_network_fees(&self) -> Result, LedgerServiceError> { + Ok(self.get_latest_block_info()?.minimum_fees) + } + + fn get_network_block_version(&self) -> Result { + Ok(BlockVersion::try_from( + self.get_latest_block_info()?.network_block_version, + )?) + } + + fn get_tx_out_proof_of_memberships( + &self, + indices: &[u64], + ) -> Result, LedgerServiceError> { + Ok(self.ledger_db.get_tx_out_proof_of_memberships(indices)?) + } + + fn get_indices_from_txo_public_keys( + &self, + public_keys: &[CompressedRistrettoPublic], + ) -> Result, LedgerServiceError> { + let indices = public_keys + .iter() + .map(|public_key| self.ledger_db.get_tx_out_index_by_public_key(public_key)) + .collect::, _>>()?; + Ok(indices) } - fn get_network_block_version(&self) -> BlockVersion { - if self.peer_manager.is_empty() { - BlockVersion::MAX - } else { - let block_version = self - .peer_manager - .conns() - .par_iter() - .filter_map(|conn| conn.fetch_block_info(empty()).ok()) - .map(|block_info| block_info.network_block_version) - .max() - .unwrap_or(*BlockVersion::MAX); - - BlockVersion::try_from(block_version).unwrap_or(BlockVersion::MAX) + fn sample_mixins( + &self, + num_mixins: usize, + excluded_indices: &[u64], + ) -> Result<(Vec, Vec), LedgerServiceError> { + let num_txos = self.ledger_db.num_txos()?; + + // Check that the ledger contains enough tx outs. + if excluded_indices.len() as u64 > num_txos { + return Err(LedgerServiceError::InvalidArgument( + "excluded_tx_out_indices exceeds amount of tx outs in ledger".to_string(), + )); + } + + if num_mixins > (num_txos as usize - excluded_indices.len()) { + return Err(LedgerServiceError::InsufficientTxOuts); + } + + let mut rng = rand::thread_rng(); + let mut sampled_indices: HashSet = HashSet::default(); + while sampled_indices.len() < num_mixins { + let index = rng.gen_range(0..num_txos); + if excluded_indices.contains(&index) { + continue; + } + sampled_indices.insert(index); } + let sampled_indices_vec: Vec = sampled_indices.into_iter().collect(); + + // Get proofs for all of those indexes. + let proofs = self + .ledger_db + .get_tx_out_proof_of_memberships(&sampled_indices_vec)?; + + let tx_outs = sampled_indices_vec + .iter() + .map(|index| self.ledger_db.get_tx_out_by_index(*index)) + .collect::, _>>()?; + + Ok((tx_outs, proofs)) + } + + fn get_block_index_from_txo_public_key( + &self, + public_key: &CompressedRistrettoPublic, + ) -> Result { + let index = self.ledger_db.get_tx_out_index_by_public_key(public_key)?; + Ok(self.ledger_db.get_block_index_by_tx_out_index(index)?) } } diff --git a/full-service/src/service/mod.rs b/full-service/src/service/mod.rs index c3785e107..830c3796c 100644 --- a/full-service/src/service/mod.rs +++ b/full-service/src/service/mod.rs @@ -8,6 +8,7 @@ pub mod balance; pub mod confirmation_number; pub mod gift_code; pub mod ledger; +pub mod models; pub mod payment_request; pub mod receipt; pub mod sync; @@ -15,8 +16,6 @@ pub mod transaction; pub mod transaction_builder; pub mod transaction_log; pub mod txo; -pub mod view_only_account; -pub mod view_only_txo; mod wallet_service; pub use wallet_service::WalletService; diff --git a/full-service/src/service/models/mod.rs b/full-service/src/service/models/mod.rs new file mode 100644 index 000000000..66234999a --- /dev/null +++ b/full-service/src/service/models/mod.rs @@ -0,0 +1 @@ +pub mod tx_proposal; diff --git a/full-service/src/service/models/tx_proposal.rs b/full-service/src/service/models/tx_proposal.rs new file mode 100644 index 000000000..adc250611 --- /dev/null +++ b/full-service/src/service/models/tx_proposal.rs @@ -0,0 +1,372 @@ +use std::convert::{TryFrom, TryInto}; + +use mc_account_keys::{AccountKey, PublicAddress}; +use mc_api::ConversionError; +use mc_crypto_keys::RistrettoPublic; +use mc_crypto_ring_signature_signer::LocalRingSigner; +use mc_transaction_core::{ + onetime_keys::recover_onetime_private_key, + ring_signature::KeyImage, + tokens::Mob, + tx::{Tx, TxOut, TxOutConfirmationNumber}, + Amount, Token, +}; +use mc_transaction_std::UnsignedTx; +use protobuf::Message; + +use crate::{service::transaction::TransactionServiceError, util::b58::b58_decode_public_address}; + +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct InputTxo { + pub tx_out: TxOut, + pub subaddress_index: u64, + pub key_image: KeyImage, + pub amount: Amount, +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct UnsignedInputTxo { + pub tx_out: TxOut, + pub subaddress_index: u64, + pub amount: Amount, +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct OutputTxo { + pub tx_out: TxOut, + pub recipient_public_address: PublicAddress, + pub confirmation_number: TxOutConfirmationNumber, + pub amount: Amount, +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct TxProposal { + pub tx: Tx, + pub input_txos: Vec, + pub payload_txos: Vec, + pub change_txos: Vec, +} + +#[derive(Clone, Debug)] +pub struct UnsignedTxProposal { + pub unsigned_tx: UnsignedTx, + pub unsigned_input_txos: Vec, + pub payload_txos: Vec, + pub change_txos: Vec, +} + +impl UnsignedTxProposal { + pub fn sign(self, account_key: &AccountKey) -> Result { + let input_txos = self + .unsigned_input_txos + .iter() + .map(|txo| { + let tx_out_public_key = RistrettoPublic::try_from(&txo.tx_out.public_key)?; + let onetime_private_key = recover_onetime_private_key( + &tx_out_public_key, + account_key.view_private_key(), + &account_key.subaddress_spend_private(txo.subaddress_index), + ); + + let key_image = KeyImage::from(&onetime_private_key); + + Ok(InputTxo { + tx_out: txo.tx_out.clone(), + subaddress_index: txo.subaddress_index, + key_image, + amount: txo.amount, + }) + }) + .collect::, TransactionServiceError>>()?; + + let signer = LocalRingSigner::from(account_key); + let mut rng = rand::thread_rng(); + let tx = self.unsigned_tx.sign(&signer, &mut rng)?; + + Ok(TxProposal { + tx, + input_txos, + payload_txos: self.payload_txos, + change_txos: self.change_txos, + }) + } +} + +impl TryFrom for UnsignedTxProposal { + type Error = String; + + fn try_from( + src: crate::json_rpc::v2::models::tx_proposal::UnsignedTxProposal, + ) -> Result { + let unsigned_input_txos = src + .unsigned_input_txos + .iter() + .map(|input_txo| { + Ok(UnsignedInputTxo { + tx_out: mc_util_serial::decode( + hex::decode(&input_txo.tx_out_proto) + .map_err(|e| e.to_string())? + .as_slice(), + ) + .map_err(|e| e.to_string())?, + subaddress_index: input_txo + .subaddress_index + .parse::() + .map_err(|e| e.to_string())?, + amount: Amount::try_from(&input_txo.amount)?, + }) + }) + .collect::, String>>()?; + + let mut payload_txos = Vec::new(); + + for txo in src.payload_txos.iter() { + let confirmation_number_hex = + hex::decode(&txo.confirmation_number).map_err(|e| format!("{}", e))?; + let confirmation_number_bytes: [u8; 32] = + confirmation_number_hex.as_slice().try_into().map_err(|_| { + "confirmation number is not the right number of bytes (expecting 32)" + })?; + let confirmation_number = TxOutConfirmationNumber::from(confirmation_number_bytes); + + let txo_out_hex = hex::decode(&txo.tx_out_proto).map_err(|e| e.to_string())?; + let tx_out = + mc_util_serial::decode(txo_out_hex.as_slice()).map_err(|e| e.to_string())?; + let recipient_public_address = + b58_decode_public_address(&txo.recipient_public_address_b58) + .map_err(|e| e.to_string())?; + + let amount = Amount::try_from(&txo.amount)?; + + let output_txo = OutputTxo { + tx_out, + recipient_public_address, + confirmation_number, + amount, + }; + + payload_txos.push(output_txo); + } + + let mut change_txos = Vec::new(); + + for txo in src.change_txos.iter() { + let confirmation_number_hex = + hex::decode(&txo.confirmation_number).map_err(|e| format!("{}", e))?; + let confirmation_number_bytes: [u8; 32] = + confirmation_number_hex.as_slice().try_into().map_err(|_| { + "confirmation number is not the right number of bytes (expecting 32)" + })?; + let confirmation_number = TxOutConfirmationNumber::from(confirmation_number_bytes); + + let txo_out_hex = hex::decode(&txo.tx_out_proto).map_err(|e| e.to_string())?; + let tx_out = + mc_util_serial::decode(txo_out_hex.as_slice()).map_err(|e| e.to_string())?; + let recipient_public_address = + b58_decode_public_address(&txo.recipient_public_address_b58) + .map_err(|e| e.to_string())?; + + let amount = Amount::try_from(&txo.amount)?; + + let output_txo = OutputTxo { + tx_out, + recipient_public_address, + confirmation_number, + amount, + }; + + change_txos.push(output_txo); + } + + let proto_bytes = + hex::decode(&src.unsigned_tx_proto_bytes_hex).map_err(|e| e.to_string())?; + let unsigned_tx_external: mc_api::external::UnsignedTx = + Message::parse_from_bytes(proto_bytes.as_slice()).map_err(|e| e.to_string())?; + let unsigned_tx = (&unsigned_tx_external) + .try_into() + .map_err(|e: ConversionError| e.to_string())?; + + Ok(Self { + unsigned_tx, + unsigned_input_txos, + payload_txos, + change_txos, + }) + } +} + +impl TryFrom<&crate::json_rpc::v1::models::tx_proposal::TxProposal> for TxProposal { + type Error = String; + + fn try_from( + src: &crate::json_rpc::v1::models::tx_proposal::TxProposal, + ) -> Result { + let mc_api_tx = mc_api::external::Tx::try_from(&src.tx)?; + let tx = Tx::try_from(&mc_api_tx).map_err(|e| e.to_string())?; + + let input_txos = src + .input_list + .iter() + .map(|unspent_txo| { + let mc_api_tx_out = mc_api::external::TxOut::try_from(&unspent_txo.tx_out)?; + let tx_out = TxOut::try_from(&mc_api_tx_out).map_err(|e| e.to_string())?; + + let key_image_bytes = + hex::decode(unspent_txo.key_image.clone()).map_err(|e| e.to_string())?; + let key_image = + KeyImage::try_from(key_image_bytes.as_slice()).map_err(|e| e.to_string())?; + + Ok(InputTxo { + tx_out, + subaddress_index: unspent_txo + .subaddress_index + .parse::() + .map_err(|e| e.to_string())?, + key_image, + amount: Amount::new(unspent_txo.value, Mob::ID), + }) + }) + .collect::, String>>()?; + + let mut payload_txos = Vec::new(); + + for (outlay_index, tx_out_index) in src.outlay_index_to_tx_out_index.iter() { + let outlay_index = outlay_index.parse::().map_err(|e| e.to_string())?; + let outlay = &src.outlay_list[outlay_index]; + let tx_out_index = tx_out_index.parse::().map_err(|e| e.to_string())?; + let tx_out = tx.prefix.outputs[tx_out_index].clone(); + let confirmation_number_bytes: &[u8; 32] = src.outlay_confirmation_numbers + [outlay_index] + .as_slice() + .try_into() + .map_err(|_| { + "confirmation number is not the right number of bytes (expecting 32)" + .to_string() + })?; + + let confirmation_number = TxOutConfirmationNumber::from(confirmation_number_bytes); + + let mc_api_public_address = + mc_api::external::PublicAddress::try_from(&outlay.receiver)?; + let public_address = + PublicAddress::try_from(&mc_api_public_address).map_err(|e| e.to_string())?; + + let payload_txo = OutputTxo { + tx_out, + recipient_public_address: public_address, + confirmation_number, + amount: Amount::new(outlay.value.0, Mob::ID), + }; + + payload_txos.push(payload_txo); + } + + Ok(Self { + tx, + input_txos, + payload_txos, + change_txos: Vec::new(), + }) + } +} + +impl TryFrom<&crate::json_rpc::v2::models::tx_proposal::TxProposal> for TxProposal { + type Error = String; + + fn try_from( + src: &crate::json_rpc::v2::models::tx_proposal::TxProposal, + ) -> Result { + let tx_bytes = hex::decode(&src.tx_proto).map_err(|e| e.to_string())?; + let tx = mc_util_serial::decode(tx_bytes.as_slice()).map_err(|e| e.to_string())?; + let input_txos = src + .input_txos + .iter() + .map(|input_txo| { + let key_image_bytes = + hex::decode(&input_txo.key_image).map_err(|e| e.to_string())?; + Ok(InputTxo { + tx_out: mc_util_serial::decode( + hex::decode(&input_txo.tx_out_proto) + .map_err(|e| e.to_string())? + .as_slice(), + ) + .map_err(|e| e.to_string())?, + subaddress_index: input_txo + .subaddress_index + .parse::() + .map_err(|e| e.to_string())?, + key_image: KeyImage::try_from(key_image_bytes.as_slice()) + .map_err(|e| e.to_string())?, + amount: Amount::try_from(&input_txo.amount)?, + }) + }) + .collect::, String>>()?; + + let mut payload_txos = Vec::new(); + + for txo in src.payload_txos.iter() { + let confirmation_number_hex = + hex::decode(&txo.confirmation_number).map_err(|e| format!("{}", e))?; + let confirmation_number_bytes: [u8; 32] = + confirmation_number_hex.as_slice().try_into().map_err(|_| { + "confirmation number is not the right number of bytes (expecting 32)" + })?; + let confirmation_number = TxOutConfirmationNumber::from(confirmation_number_bytes); + + let txo_out_hex = hex::decode(&txo.tx_out_proto).map_err(|e| e.to_string())?; + let tx_out = + mc_util_serial::decode(txo_out_hex.as_slice()).map_err(|e| e.to_string())?; + let recipient_public_address = + b58_decode_public_address(&txo.recipient_public_address_b58) + .map_err(|e| e.to_string())?; + + let amount = Amount::try_from(&txo.amount)?; + + let output_txo = OutputTxo { + tx_out, + recipient_public_address, + confirmation_number, + amount, + }; + + payload_txos.push(output_txo); + } + + let mut change_txos = Vec::new(); + + for txo in src.change_txos.iter() { + let confirmation_number_hex = + hex::decode(&txo.confirmation_number).map_err(|e| format!("{}", e))?; + let confirmation_number_bytes: [u8; 32] = + confirmation_number_hex.as_slice().try_into().map_err(|_| { + "confirmation number is not the right number of bytes (expecting 32)" + })?; + let confirmation_number = TxOutConfirmationNumber::from(confirmation_number_bytes); + + let txo_out_hex = hex::decode(&txo.tx_out_proto).map_err(|e| e.to_string())?; + let tx_out = + mc_util_serial::decode(txo_out_hex.as_slice()).map_err(|e| e.to_string())?; + let recipient_public_address = + b58_decode_public_address(&txo.recipient_public_address_b58) + .map_err(|e| e.to_string())?; + + let amount = Amount::try_from(&txo.amount)?; + + let output_txo = OutputTxo { + tx_out, + recipient_public_address, + confirmation_number, + amount, + }; + + change_txos.push(output_txo); + } + + Ok(Self { + tx, + input_txos, + payload_txos, + change_txos, + }) + } +} diff --git a/full-service/src/service/payment_request.rs b/full-service/src/service/payment_request.rs index fd3cb42e3..7acf420c2 100644 --- a/full-service/src/service/payment_request.rs +++ b/full-service/src/service/payment_request.rs @@ -9,6 +9,7 @@ use crate::{ }; use mc_connection::{BlockchainConnection, UserTxConnection}; use mc_fog_report_validation::FogPubkeyResolver; +use mc_transaction_core::Amount; use crate::service::ledger::LedgerServiceError; use displaydoc::Display; @@ -82,7 +83,7 @@ pub trait PaymentRequestService { &self, account_id: String, subaddress_index: Option, - amount_pmob: u64, + amount: Amount, memo: Option, ) -> Result; } @@ -96,10 +97,10 @@ where &self, account_id: String, subaddress_index: Option, - amount_pmob: u64, + amount: Amount, memo: Option, ) -> Result { - let conn = self.wallet_db.get_conn()?; + let conn = self.get_conn()?; let assigned_subaddress = AssignedSubaddress::get_for_account_by_index( &account_id, @@ -107,12 +108,11 @@ where &conn, )?; - let public_address = - b58_decode_public_address(&assigned_subaddress.assigned_subaddress_b58)?; + let public_address = b58_decode_public_address(&assigned_subaddress.public_address_b58)?; let payment_request_b58 = b58_encode_payment_request( &public_address, - amount_pmob, + &amount, memo.unwrap_or_else(|| "".to_string()), )?; diff --git a/full-service/src/service/receipt.rs b/full-service/src/service/receipt.rs index 24f8223d3..4ab431eef 100644 --- a/full-service/src/service/receipt.rs +++ b/full-service/src/service/receipt.rs @@ -13,9 +13,10 @@ use crate::{ account::{AccountID, AccountModel}, assigned_subaddress::AssignedSubaddressModel, models::{Account, AssignedSubaddress, Txo}, - txo::TxoModel, + txo::{TxoModel, TxoStatus}, WalletDbError, }, + service::models::tx_proposal::TxProposal, WalletService, }; use displaydoc::Display; @@ -23,7 +24,6 @@ use mc_account_keys::AccountKey; use mc_connection::{BlockchainConnection, UserTxConnection}; use mc_crypto_keys::{CompressedRistrettoPublic, RistrettoPublic}; use mc_fog_report_validation::FogPubkeyResolver; -use mc_mobilecoind::payments::TxProposal; use mc_transaction_core::{get_tx_out_shared_secret, tx::TxOutConfirmationNumber, MaskedAmount}; use serde::{Deserialize, Serialize}; use std::convert::TryFrom; @@ -55,6 +55,9 @@ pub enum ReceiptServiceError { /// Error decoding from hex: {0} HexDecode(hex::FromHexError), + + /// Tx Out Conversion Error: {0} + TxOutConversion(mc_transaction_core::TxOutConversionError), } impl From for ReceiptServiceError { @@ -93,6 +96,12 @@ impl From for ReceiptServiceError { } } +impl From for ReceiptServiceError { + fn from(src: mc_transaction_core::TxOutConversionError) -> Self { + Self::TxOutConversion(src) + } +} + #[derive(Debug, Clone, Eq, PartialEq)] pub struct ReceiverReceipt { /// The public key of the Txo sent to the recipient. @@ -144,12 +153,19 @@ impl TryFrom<&mc_api::external::Receipt> for ReceiverReceipt { let public_key: CompressedRistrettoPublic = CompressedRistrettoPublic::try_from(src.get_public_key())?; let confirmation = TxOutConfirmationNumber::try_from(src.get_confirmation())?; - let amount = MaskedAmount::try_from(src.get_masked_amount())?; + + let one_of_masked_amount = src + .masked_amount + .as_ref() + .ok_or(ReceiptServiceError::ProtoConversionInfallible)?; + + let masked_amount = MaskedAmount::try_from(one_of_masked_amount)?; + Ok(ReceiverReceipt { public_key, confirmation, tombstone_block: src.get_tombstone_block(), - amount, + amount: masked_amount, }) } } @@ -164,7 +180,7 @@ pub trait ReceiptService { &self, address: &str, receiver_receipt: &ReceiverReceipt, - ) -> Result<(ReceiptTransactionStatus, Option), ReceiptServiceError>; + ) -> Result<(ReceiptTransactionStatus, Option<(Txo, TxoStatus)>), ReceiptServiceError>; /// Create a receipt from a given TxProposal fn create_receiver_receipts( @@ -182,10 +198,10 @@ where &self, address: &str, receiver_receipt: &ReceiverReceipt, - ) -> Result<(ReceiptTransactionStatus, Option), ReceiptServiceError> { - let conn = &self.wallet_db.get_conn()?; + ) -> Result<(ReceiptTransactionStatus, Option<(Txo, TxoStatus)>), ReceiptServiceError> { + let conn = &self.get_conn()?; let assigned_address = AssignedSubaddress::get(address, conn)?; - let account_id = AccountID(assigned_address.account_id_hex); + let account_id = AccountID(assigned_address.account_id); let account = Account::get(&account_id, conn)?; // Get the transaction from the database, with status. let txos = Txo::select_by_public_key(&[&receiver_receipt.public_key], conn)?; @@ -195,10 +211,13 @@ where return Ok((ReceiptTransactionStatus::TransactionPending, None)); } let txo = txos[0].clone(); + let txo_status = txo.status(conn)?; - // Return if the Txo from the receipt has a pending tombstone block index - if txo.pending_tombstone_block_index.is_some() { - return Ok((ReceiptTransactionStatus::TransactionPending, Some(txo))); + if txo_status == TxoStatus::Pending { + return Ok(( + ReceiptTransactionStatus::TransactionPending, + Some((txo, txo_status)), + )); } // Decrypt the amount to get the expected value @@ -207,7 +226,12 @@ where let shared_secret = get_tx_out_shared_secret(account_key.view_private_key(), &public_key); let expected_value = match receiver_receipt.amount.get_value(&shared_secret) { Ok((v, _blinding)) => v, - Err(_) => return Ok((ReceiptTransactionStatus::FailedAmountDecryption, Some(txo))), + Err(_) => { + return Ok(( + ReceiptTransactionStatus::FailedAmountDecryption, + Some((txo, txo_status)), + )) + } }; // Check that the value of the received Txo matches the expected value. if (txo.value as u64) != expected_value.value { @@ -216,7 +240,7 @@ where "Expected: {}, Got: {}", expected_value.value, txo.value )), - Some(txo), + Some((txo, txo_status)), )); } @@ -224,11 +248,17 @@ where let confirmation_hex = hex::encode(mc_util_serial::encode(&receiver_receipt.confirmation)); let confirmation: TxOutConfirmationNumber = mc_util_serial::decode(&hex::decode(confirmation_hex)?)?; - if !Txo::validate_confirmation(&account_id, &txo.txo_id_hex, &confirmation, conn)? { - return Ok((ReceiptTransactionStatus::InvalidConfirmation, Some(txo))); + if !Txo::validate_confirmation(&account_id, &txo.id, &confirmation, conn)? { + return Ok(( + ReceiptTransactionStatus::InvalidConfirmation, + Some((txo, txo_status)), + )); } - Ok((ReceiptTransactionStatus::TransactionSuccess, Some(txo))) + Ok(( + ReceiptTransactionStatus::TransactionSuccess, + Some((txo, txo_status)), + )) } fn create_receiver_receipts( @@ -236,20 +266,17 @@ where tx_proposal: &TxProposal, ) -> Result, ReceiptServiceError> { let receiver_tx_receipts: Vec = tx_proposal - .outlays + .payload_txos .iter() - .enumerate() - .map(|(outlay_index, _outlay)| { - let tx_out_index = tx_proposal.outlay_index_to_tx_out_index[&outlay_index]; - let tx_out = tx_proposal.tx.prefix.outputs[tx_out_index].clone(); - ReceiverReceipt { - public_key: tx_out.public_key, + .map(|output_txo| { + Ok(ReceiverReceipt { + public_key: output_txo.tx_out.public_key, tombstone_block: tx_proposal.tx.prefix.tombstone_block, - confirmation: tx_proposal.outlay_confirmation_numbers[outlay_index].clone(), - amount: tx_out.masked_amount, - } + confirmation: output_txo.confirmation_number.clone(), + amount: output_txo.tx_out.get_masked_amount()?.clone(), + }) }) - .collect::>(); + .collect::, ReceiptServiceError>>()?; Ok(receiver_tx_receipts) } } @@ -258,19 +285,19 @@ where mod tests { use super::*; use crate::{ - db::{ - account::AccountID, - models::{TransactionLog, TX_DIRECTION_SENT}, - transaction_log::{AssociatedTxos, TransactionLogModel}, - }, + db::{account::AccountID, models::TransactionLog, transaction_log::TransactionLogModel}, + json_rpc::v2::models::amount::Amount as AmountJSON, service::{ - account::AccountService, address::AddressService, - confirmation_number::ConfirmationService, transaction::TransactionService, - transaction_log::TransactionLogService, txo::TxoService, + account::AccountService, + address::AddressService, + confirmation_number::ConfirmationService, + transaction::{TransactionMemo, TransactionService}, + transaction_log::TransactionLogService, + txo::TxoService, }, test_utils::{ - add_block_to_ledger_db, add_block_with_tx_proposal, get_test_ledger, - manually_sync_account, setup_wallet_service, MOB, + add_block_to_ledger_db, add_block_with_tx, get_test_ledger, manually_sync_account, + setup_wallet_service, MOB, }, util::b58::b58_encode_public_address, }; @@ -279,6 +306,7 @@ mod tests { use mc_crypto_keys::{ReprBytes, RistrettoPrivate, RistrettoPublic}; use mc_crypto_rand::RngCore; use mc_transaction_core::{ring_signature::KeyImage, tokens::Mob, tx::TxOut, Amount, Token}; + use mc_transaction_types::BlockVersion; use mc_util_from_random::FromRandom; use rand::{rngs::StdRng, SeedableRng}; @@ -290,6 +318,7 @@ mod tests { let account_key = AccountKey::random(&mut rng); let public_address = account_key.default_subaddress(); let txo = TxOut::new( + BlockVersion::MAX, Amount::new(rng.next_u64(), Mob::ID), &public_address, &RistrettoPrivate::from_random(&mut rng), @@ -308,19 +337,26 @@ mod tests { proto_confirmation.set_hash(confirmation_number.to_vec()); proto_tx_receipt.set_confirmation(proto_confirmation); let mut proto_commitment = mc_api::external::CompressedRistretto::new(); - proto_commitment.set_data(txo.masked_amount.commitment.to_bytes().to_vec()); + proto_commitment.set_data( + txo.get_masked_amount() + .unwrap() + .commitment() + .to_bytes() + .to_vec(), + ); let mut proto_amount = mc_api::external::MaskedAmount::new(); proto_amount.set_commitment(proto_commitment); - proto_amount.set_masked_value(txo.masked_amount.masked_value); - proto_amount.set_masked_token_id(txo.masked_amount.masked_token_id.clone()); - proto_tx_receipt.set_masked_amount(proto_amount); + proto_amount.set_masked_value(*txo.get_masked_amount().unwrap().get_masked_value()); + proto_amount + .set_masked_token_id(txo.get_masked_amount().unwrap().masked_token_id().to_vec()); + proto_tx_receipt.set_masked_amount_v2(proto_amount); let tx_receipt = ReceiverReceipt::try_from(&proto_tx_receipt).expect("Could not convert tx receipt"); assert_eq!(txo.public_key, tx_receipt.public_key); assert_eq!(tombstone, tx_receipt.tombstone_block); assert_eq!(confirmation_number, tx_receipt.confirmation); - assert_eq!(txo.masked_amount, tx_receipt.amount); + assert_eq!(txo.get_masked_amount().unwrap(), &tx_receipt.amount); } #[test_with_logger] @@ -342,7 +378,7 @@ mod tests { // Fund Alice let alice_account_key: AccountKey = mc_util_serial::decode(&alice.account_key).unwrap(); - let alice_public_address = alice_account_key.subaddress(alice.main_subaddress_index as u64); + let alice_public_address = alice_account_key.default_subaddress(); add_block_to_ledger_db( &mut ledger_db, &vec![alice_public_address.clone()], @@ -352,8 +388,8 @@ mod tests { ); manually_sync_account( &ledger_db, - &service.wallet_db, - &AccountID(alice.account_id_hex.to_string()), + &service.wallet_db.as_ref().unwrap(), + &AccountID(alice.id.to_string()), &logger, ); @@ -366,20 +402,21 @@ mod tests { ) .unwrap(); let bob_addresses = service - .get_addresses_for_account(&AccountID(bob.account_id_hex.clone()), None, None) + .get_addresses(Some(bob.id.clone()), None, None) .expect("Could not get addresses for Bob"); - let bob_address = bob_addresses[0].assigned_subaddress_b58.clone(); + let bob_address = bob_addresses[0].public_address_b58.clone(); // Create a TxProposal to Bob let tx_proposal = service - .build_transaction( - &alice.account_id_hex, - &vec![(bob_address.to_string(), (24 * MOB).to_string())], + .build_and_sign_transaction( + &alice.id, + &vec![(bob_address.to_string(), AmountJSON::new(24 * MOB, Mob::ID))], None, None, None, None, None, + TransactionMemo::RTH, ) .expect("Could not build transaction"); @@ -393,63 +430,56 @@ mod tests { // else we will get a Unique constraint failed if we had already scanned // before logging submitted. TransactionLog::log_submitted( - tx_proposal.clone(), + &tx_proposal, 14, "".to_string(), - &alice.account_id_hex, - &service.wallet_db.get_conn().unwrap(), + &alice.id, + &service.get_conn().unwrap(), ) .expect("Could not log submitted"); // Add the txo to the ledger - add_block_with_tx_proposal(&mut ledger_db, tx_proposal); + add_block_with_tx(&mut ledger_db, tx_proposal.tx, &mut rng); manually_sync_account( &ledger_db, - &service.wallet_db, - &AccountID(alice.account_id_hex.to_string()), + &service.wallet_db.as_ref().unwrap(), + &AccountID(alice.id.to_string()), &logger, ); manually_sync_account( &ledger_db, - &service.wallet_db, - &AccountID(bob.account_id_hex.to_string()), + &service.wallet_db.as_ref().unwrap(), + &AccountID(bob.id.to_string()), &logger, ); // Get corresponding Txo for Bob - let txos = service - .list_txos(&AccountID(bob.account_id_hex), None, None, None) + let txos_and_statuses = service + .list_txos(Some(bob.id), None, None, None, None, None, None, None) .expect("Could not get Bob Txos"); - assert_eq!(txos.len(), 1); + assert_eq!(txos_and_statuses.len(), 1); // Get the corresponding TransactionLog for Alice's Account - only the sender // has the confirmation number. let transaction_logs = service - .list_transaction_logs(&AccountID(alice.account_id_hex), None, None, None, None) + .list_transaction_logs(Some(alice.id), None, None, None, None) .expect("Could not get transaction logs"); - // Alice should have two received (initial and change), and one sent - // TransactionLog. - assert_eq!(transaction_logs.len(), 3); - let sent_transaction_logs_and_associated_txos: Vec<&(TransactionLog, AssociatedTxos)> = - transaction_logs - .iter() - .filter(|t| t.0.direction == TX_DIRECTION_SENT) - .collect(); - assert_eq!(sent_transaction_logs_and_associated_txos.len(), 1); - let sent_transaction_log: TransactionLog = - sent_transaction_logs_and_associated_txos[0].0.clone(); + // Alice should have one sent tranasction log + assert_eq!(transaction_logs.len(), 1); + let sent_transaction_log: TransactionLog = transaction_logs[0].0.clone(); let confirmations = service - .get_confirmations(&sent_transaction_log.transaction_id_hex) + .get_confirmations(&sent_transaction_log.id) .expect("Could not get confirmations"); assert_eq!(confirmations.len(), 1); - let txo_pubkey = - mc_util_serial::decode(&txos[0].public_key).expect("Could not decode pubkey"); + let txo_pubkey = mc_util_serial::decode(&txos_and_statuses[0].0.public_key) + .expect("Could not decode pubkey"); assert_eq!(receipt.public_key, txo_pubkey); assert_eq!(receipt.tombstone_block, 23); // Ledger seeded with 12 blocks at tx construction, then one appended + 10 - let txo: TxOut = mc_util_serial::decode(&txos[0].txo).expect("Could not decode txo"); - assert_eq!(receipt.amount, txo.masked_amount); + let txo: TxOut = + mc_util_serial::decode(&txos_and_statuses[0].0.txo).expect("Could not decode txo"); + assert_eq!(&receipt.amount, txo.get_masked_amount().unwrap()); assert_eq!(receipt.confirmation, confirmations[0].confirmation); } @@ -474,7 +504,7 @@ mod tests { // Fund Alice let alice_account_key: AccountKey = mc_util_serial::decode(&alice.account_key).unwrap(); - let alice_public_address = alice_account_key.subaddress(alice.main_subaddress_index as u64); + let alice_public_address = alice_account_key.default_subaddress(); add_block_to_ledger_db( &mut ledger_db, &vec![alice_public_address.clone()], @@ -484,8 +514,8 @@ mod tests { ); manually_sync_account( &ledger_db, - &service.wallet_db, - &AccountID(alice.account_id_hex.to_string()), + &service.wallet_db.as_ref().unwrap(), + &AccountID(alice.id.to_string()), &logger, ); @@ -498,20 +528,21 @@ mod tests { ) .unwrap(); let bob_addresses = service - .get_addresses_for_account(&AccountID(bob.account_id_hex.clone()), None, None) + .get_addresses(Some(bob.id.clone()), None, None) .expect("Could not get addresses for Bob"); - let bob_address = &bob_addresses[0].assigned_subaddress_b58.clone(); + let bob_address = &bob_addresses[0].public_address_b58.clone(); // Create a TxProposal to Bob let tx_proposal = service - .build_transaction( - &alice.account_id_hex, - &vec![(bob_address.to_string(), (24 * MOB).to_string())], + .build_and_sign_transaction( + &alice.id, + &vec![(bob_address.to_string(), AmountJSON::new(24 * MOB, Mob::ID))], None, None, None, None, None, + TransactionMemo::RTH, ) .expect("Could not build transaction"); @@ -530,11 +561,11 @@ mod tests { // Land the Txo in the ledger - only sync for the sender TransactionLog::log_submitted( - tx_proposal.clone(), + &tx_proposal, 14, "".to_string(), - &alice.account_id_hex, - &service.wallet_db.get_conn().unwrap(), + &alice.id, + &service.get_conn().unwrap(), ) .expect("Could not log submitted"); @@ -546,17 +577,17 @@ mod tests { assert_eq!(status, ReceiptTransactionStatus::TransactionPending); // Add the txo to the ledger - add_block_with_tx_proposal(&mut ledger_db, tx_proposal); + add_block_with_tx(&mut ledger_db, tx_proposal.tx, &mut rng); manually_sync_account( &ledger_db, - &service.wallet_db, - &AccountID(alice.account_id_hex.to_string()), + &service.wallet_db.as_ref().unwrap(), + &AccountID(alice.id.to_string()), &logger, ); manually_sync_account( &ledger_db, - &service.wallet_db, - &AccountID(bob.account_id_hex.to_string()), + &service.wallet_db.as_ref().unwrap(), + &AccountID(bob.id.to_string()), &logger, ); @@ -595,7 +626,7 @@ mod tests { // Fund Alice let alice_account_key: AccountKey = mc_util_serial::decode(&alice.account_key).unwrap(); - let alice_public_address = alice_account_key.subaddress(alice.main_subaddress_index as u64); + let alice_public_address = alice_account_key.default_subaddress(); add_block_to_ledger_db( &mut ledger_db, &vec![alice_public_address.clone()], @@ -605,8 +636,8 @@ mod tests { ); manually_sync_account( &ledger_db, - &service.wallet_db, - &AccountID(alice.account_id_hex.to_string()), + &service.wallet_db.as_ref().unwrap(), + &AccountID(alice.id.to_string()), &logger, ); @@ -619,21 +650,22 @@ mod tests { ) .unwrap(); let bob_addresses = service - .get_addresses_for_account(&AccountID(bob.account_id_hex.clone()), None, None) + .get_addresses(Some(bob.id.clone()), None, None) .expect("Could not get addresses for Bob"); - let bob_address = &bob_addresses[0].assigned_subaddress_b58.clone(); - let bob_account_id = AccountID(bob.account_id_hex.to_string()); + let bob_address = &bob_addresses[0].public_address_b58.clone(); + let bob_account_id = AccountID(bob.id.to_string()); // Create a TxProposal to Bob let tx_proposal0 = service - .build_transaction( - &alice.account_id_hex, - &vec![(bob_address.to_string(), (24 * MOB).to_string())], + .build_and_sign_transaction( + &alice.id, + &vec![(bob_address.to_string(), AmountJSON::new(24 * MOB, Mob::ID))], None, None, None, None, None, + TransactionMemo::RTH, ) .expect("Could not build transaction"); @@ -644,25 +676,31 @@ mod tests { // Land the Txo in the ledger - only sync for the sender TransactionLog::log_submitted( - tx_proposal0.clone(), + &tx_proposal0, 14, "".to_string(), - &alice.account_id_hex, - &service.wallet_db.get_conn().unwrap(), + &alice.id, + &service.get_conn().unwrap(), ) .expect("Could not log submitted"); - add_block_with_tx_proposal(&mut ledger_db, tx_proposal0); + add_block_with_tx(&mut ledger_db, tx_proposal0.tx, &mut rng); manually_sync_account( &ledger_db, - &service.wallet_db, - &AccountID(alice.account_id_hex.to_string()), + &service.wallet_db.as_ref().unwrap(), + &AccountID(alice.id.to_string()), + &logger, + ); + manually_sync_account( + &ledger_db, + &service.wallet_db.as_ref().unwrap(), + &bob_account_id, &logger, ); - manually_sync_account(&ledger_db, &service.wallet_db, &bob_account_id, &logger); // Bob checks the status, and is expecting an incorrect value, from a // transaction with a different shared secret receipt0.amount = MaskedAmount::new( + BlockVersion::MAX, Amount::new(18 * MOB, Mob::ID), &RistrettoPublic::from_random(&mut rng), ) @@ -674,7 +712,7 @@ mod tests { // Now check status with a correct shared secret, but the wrong value let bob_account_key: AccountKey = mc_util_serial::decode( - &Account::get(&bob_account_id, &service.wallet_db.get_conn().unwrap()) + &Account::get(&bob_account_id, &service.get_conn().unwrap()) .expect("Could not get bob account") .account_key, ) @@ -683,8 +721,12 @@ mod tests { .expect("Could not get ristretto public from compressed"); let shared_secret = get_tx_out_shared_secret(bob_account_key.view_private_key(), &public_key); - receipt0.amount = MaskedAmount::new(Amount::new(18 * MOB, Mob::ID), &shared_secret) - .expect("Could not create Amount"); + receipt0.amount = MaskedAmount::new( + BlockVersion::MAX, + Amount::new(18 * MOB, Mob::ID), + &shared_secret, + ) + .expect("Could not create Amount"); let (status, _txo) = service .check_receipt_status(&bob_address, &receipt0) .expect("Could not check status of receipt"); @@ -724,7 +766,7 @@ mod tests { // Fund Alice let alice_account_key: AccountKey = mc_util_serial::decode(&alice.account_key).unwrap(); - let alice_public_address = alice_account_key.subaddress(alice.main_subaddress_index as u64); + let alice_public_address = alice_account_key.default_subaddress(); add_block_to_ledger_db( &mut ledger_db, &vec![alice_public_address.clone()], @@ -734,8 +776,8 @@ mod tests { ); manually_sync_account( &ledger_db, - &service.wallet_db, - &AccountID(alice.account_id_hex.to_string()), + &service.wallet_db.as_ref().unwrap(), + &AccountID(alice.id.to_string()), &logger, ); @@ -748,21 +790,22 @@ mod tests { ) .unwrap(); let bob_addresses = service - .get_addresses_for_account(&AccountID(bob.account_id_hex.clone()), None, None) + .get_addresses(Some(bob.id.clone()), None, None) .expect("Could not get addresses for Bob"); - let bob_address = &bob_addresses[0].assigned_subaddress_b58.clone(); - let bob_account_id = AccountID(bob.account_id_hex.to_string()); + let bob_address = &bob_addresses[0].public_address_b58.clone(); + let bob_account_id = AccountID(bob.id.to_string()); // Create a TxProposal to Bob let tx_proposal0 = service - .build_transaction( - &alice.account_id_hex, - &vec![(bob_address.to_string(), (24 * MOB).to_string())], + .build_and_sign_transaction( + &alice.id, + &vec![(bob_address.to_string(), AmountJSON::new(24 * MOB, Mob::ID))], None, None, None, None, None, + TransactionMemo::RTH, ) .expect("Could not build transaction"); @@ -773,21 +816,26 @@ mod tests { // Land the Txo in the ledger - only sync for the sender TransactionLog::log_submitted( - tx_proposal0.clone(), + &tx_proposal0, 14, "".to_string(), - &alice.account_id_hex, - &service.wallet_db.get_conn().unwrap(), + &alice.id, + &service.get_conn().unwrap(), ) .expect("Could not log submitted"); - add_block_with_tx_proposal(&mut ledger_db, tx_proposal0); + add_block_with_tx(&mut ledger_db, tx_proposal0.tx, &mut rng); + manually_sync_account( + &ledger_db, + &service.wallet_db.as_ref().unwrap(), + &AccountID(alice.id.to_string()), + &logger, + ); manually_sync_account( &ledger_db, - &service.wallet_db, - &AccountID(alice.account_id_hex.to_string()), + &service.wallet_db.as_ref().unwrap(), + &bob_account_id, &logger, ); - manually_sync_account(&ledger_db, &service.wallet_db, &bob_account_id, &logger); // Construct an invalid receipt with an incorrect confirmation number. let mut receipt = receipt0.clone(); diff --git a/full-service/src/service/sync.rs b/full-service/src/service/sync.rs index 9330ddee0..216135dc7 100644 --- a/full-service/src/service/sync.rs +++ b/full-service/src/service/sync.rs @@ -6,22 +6,15 @@ use crate::{ db::{ account::{AccountID, AccountModel}, assigned_subaddress::AssignedSubaddressModel, - models::{ - Account, AssignedSubaddress, TransactionLog, Txo, ViewOnlyAccount, ViewOnlySubaddress, - ViewOnlyTxo, - }, + models::{Account, AssignedSubaddress, TransactionLog, Txo}, transaction, transaction_log::TransactionLogModel, txo::TxoModel, - view_only_account::ViewOnlyAccountModel, - view_only_subaddress::ViewOnlySubaddressModel, - view_only_txo::ViewOnlyTxoModel, Conn, WalletDb, }, error::SyncError, - util::b58::b58_encode_public_address, }; -use mc_account_keys::AccountKey; +use mc_account_keys::{AccountKey, ViewAccountKey}; use mc_common::{ logger::{log, Logger}, HashMap, @@ -32,14 +25,13 @@ use mc_transaction_core::{ get_tx_out_shared_secret, onetime_keys::{recover_onetime_private_key, recover_public_subaddress_spend_key}, ring_signature::KeyImage, - tokens::Mob, tx::TxOut, - Amount, Token, + Amount, }; use rayon::prelude::*; use std::{ - convert::TryFrom, + convert::{TryFrom, TryInto}, sync::{ atomic::{AtomicBool, Ordering}, Arc, @@ -124,14 +116,7 @@ pub fn sync_all_accounts( let conn = &wallet_db .get_conn() .expect("Could not get connection to DB"); - Account::list_all(conn).expect("Failed getting accounts from database") - }; - - let view_only_accounts: Vec = { - let conn = &wallet_db - .get_conn() - .expect("Could not get connection to DB"); - ViewOnlyAccount::list_all(conn).expect("Failed getting view only accounts from database") + Account::list_all(conn, None, None).expect("Failed getting accounts from database") }; for account in accounts { @@ -139,15 +124,7 @@ pub fn sync_all_accounts( if account.next_block_index as u64 > num_blocks - 1 { continue; } - sync_account(ledger_db, wallet_db, &account.account_id_hex, logger)?; - } - - for account in view_only_accounts { - // If there are no new blocks for this account, don't do anything. - if account.next_block_index as u64 > num_blocks - 1 { - continue; - } - sync_view_only_account(ledger_db, wallet_db, &account.account_id_hex, logger)?; + sync_account(ledger_db, wallet_db, &account.id, logger)?; } Ok(()) @@ -159,150 +136,6 @@ enum SyncStatus { NoMoreBlocks, } -/// Sync a single view only account. -pub fn sync_view_only_account( - ledger_db: &LedgerDB, - wallet_db: &WalletDb, - account_id_hex: &str, - logger: &Logger, -) -> Result<(), SyncError> { - let conn = wallet_db.get_conn()?; - - while let SyncStatus::ChunkFinished = - sync_view_only_account_next_chunk(ledger_db, &conn, logger, account_id_hex)? - {} - - Ok(()) -} - -fn sync_view_only_account_next_chunk( - ledger_db: &LedgerDB, - conn: &Conn, - logger: &Logger, - account_id_hex: &str, -) -> Result { - transaction(conn, || { - // Get the account data. If it is no longer available, the account has been - // removed and we can simply return. - let view_only_account = ViewOnlyAccount::get(account_id_hex, conn)?; - let view_private_key: RistrettoPrivate = - mc_util_serial::decode(&view_only_account.view_private_key)?; - - // Load subaddresses for this account into a hash map. - let mut subaddress_keys: HashMap = HashMap::default(); - let subaddresses: Vec<_> = ViewOnlySubaddress::list_all(account_id_hex, None, None, conn)?; - for s in subaddresses { - let subaddress_key = RistrettoPublic::try_from(s.public_spend_key.as_slice())?; - subaddress_keys.insert(subaddress_key, s.subaddress_index as u64); - } - - let start_time = Instant::now(); - let start_block_index = view_only_account.next_block_index as u64; - let mut end_block_index = view_only_account.next_block_index as u64; - - // Load transaction outputs and key_images for this chunk. - let mut tx_outs: Vec<(u64, TxOut)> = Vec::new(); - let mut key_images: Vec<(u64, KeyImage)> = Vec::new(); - - let start = view_only_account.next_block_index as u64; - let end = start + BLOCKS_CHUNK_SIZE; - for block_index in start..end { - let block_index = block_index as u64; - let block_contents = match ledger_db.get_block_contents(block_index as u64) { - Ok(block_contents) => block_contents, - Err(mc_ledger_db::Error::NotFound) => { - break; - } - Err(err) => { - return Err(err.into()); - } - }; - end_block_index = block_index; - - for tx_out in block_contents.outputs { - tx_outs.push((block_index, tx_out)); - } - - for key_image in block_contents.key_images { - key_images.push((block_index, key_image)); - } - } - - // Attempt to decode each transaction as received by this account. - let received_txos: Vec<_> = tx_outs - .into_par_iter() - .filter_map(|(block_index, tx_out)| { - let amount = match decode_amount(&tx_out, &view_private_key) { - None => return None, - Some(a) => a, - }; - - let subaddress_index = - decode_subaddress_index(&tx_out, &view_private_key, &subaddress_keys); - Some((block_index, tx_out, amount, subaddress_index)) - }) - .collect(); - let num_received_txos = received_txos.len(); - - // Write received txos to db - for (block_index, tx_out, amount, subaddress_index) in received_txos { - ViewOnlyTxo::create( - tx_out.clone(), - amount, - subaddress_index, - Some(block_index), - account_id_hex, - conn, - )?; - } - - // Match key images to mark existing unspent transactions as spent. - let unspent_key_images: HashMap = - ViewOnlyTxo::list_unspent_with_key_images(account_id_hex, None, conn)?; - let spent_txos: Vec<(u64, String)> = key_images - .into_par_iter() - .filter_map(|(block_index, key_image)| { - unspent_key_images - .get(&key_image) - .map(|txo_id_hex| (block_index, txo_id_hex.clone())) - }) - .collect(); - - for (block_index, txo_id_hex) in &spent_txos { - ViewOnlyTxo::update_spent_block_index(txo_id_hex, *block_index, conn)?; - } - - ViewOnlyTxo::release_txos_with_expired_pending_tombstone_block_index( - account_id_hex, - end_block_index, - conn, - )?; - - // Done syncing this chunk. Mark these blocks as synced for this account. - view_only_account.update_next_block_index(end_block_index + 1, conn)?; - let num_blocks_synced = end_block_index - start_block_index + 1; - - let duration = start_time.elapsed(); - - log::debug!( - logger, - "Synced {} blocks ({}-{}) for view only account {} in {:?}. {} txos received.", - num_blocks_synced, - start_block_index, - end_block_index, - account_id_hex.chars().take(6).collect::(), - duration, - num_received_txos, - ); - - if num_blocks_synced < BLOCKS_CHUNK_SIZE { - Ok(SyncStatus::NoMoreBlocks) - } else { - Ok(SyncStatus::ChunkFinished) - } - }) -} - /// Sync a single account. pub fn sync_account( ledger_db: &LedgerDB, @@ -329,13 +162,13 @@ fn sync_account_next_chunk( // Get the account data. If it is no longer available, the account has been // removed and we can simply return. let account = Account::get(&AccountID(account_id_hex.to_string()), conn)?; - let account_key: AccountKey = mc_util_serial::decode(&account.account_key)?; // Load subaddresses for this account into a hash map. let mut subaddress_keys: HashMap = HashMap::default(); - let subaddresses: Vec<_> = AssignedSubaddress::list_all(account_id_hex, None, None, conn)?; + let subaddresses: Vec<_> = + AssignedSubaddress::list_all(Some(account_id_hex.to_string()), None, None, conn)?; for s in subaddresses { - let subaddress_key = mc_util_serial::decode(s.subaddress_spend_key.as_slice())?; + let subaddress_key: RistrettoPublic = s.spend_public_key.as_slice().try_into()?; subaddress_keys.insert(subaddress_key, s.subaddress_index as u64); } @@ -377,122 +210,156 @@ fn sync_account_next_chunk( } let end_block_index = end_block_index.unwrap(); - // Attempt to decode each transaction as received by this account. - let received_txos: Vec<_> = tx_outs - .into_par_iter() - .filter_map(|(block_index, tx_out)| { - let amount = match decode_amount(&tx_out, account_key.view_private_key()) { - None => return None, - Some(a) => a, - }; - let (subaddress_index, key_image) = - decode_subaddress_and_key_image(&tx_out, &account_key, &subaddress_keys); - Some((block_index, tx_out, amount, subaddress_index, key_image)) - }) - .collect(); - let num_received_txos = received_txos.len(); - - // Write received transactions to the database. - for (block_index, tx_out, amount, subaddress_index, key_image) in received_txos { - let txo_id = Txo::create_received( - tx_out.clone(), - subaddress_index, - key_image, - amount, - block_index, - account_id_hex, + if account.view_only { + let view_account_key: ViewAccountKey = mc_util_serial::decode(&account.account_key)?; + + // Attempt to decode each transaction as received by this account. + let received_txos: Vec<_> = tx_outs + .into_par_iter() + .filter_map(|(block_index, tx_out)| { + let amount = match decode_amount(&tx_out, view_account_key.view_private_key()) { + None => return None, + Some(a) => a, + }; + let subaddress_index = decode_subaddress_index( + &tx_out, + view_account_key.view_private_key(), + &subaddress_keys, + ); + Some((block_index, tx_out, amount, subaddress_index)) + }) + .collect(); + let num_received_txos = received_txos.len(); + + // Write received transactions to the database. + for (block_index, tx_out, amount, subaddress_index) in received_txos { + Txo::create_received( + tx_out.clone(), + subaddress_index, + None, + amount, + block_index, + account_id_hex, + conn, + )?; + } + + // Match key images to mark existing unspent transactions as spent. + let unspent_key_images: HashMap = + Txo::list_unspent_or_pending_key_images(account_id_hex, None, conn)?; + let spent_txos: Vec<(u64, String)> = key_images + .into_par_iter() + .filter_map(|(block_index, key_image)| { + unspent_key_images + .get(&key_image) + .map(|txo_id_hex| (block_index, txo_id_hex.clone())) + }) + .collect(); + let num_spent_txos = spent_txos.len(); + for (block_index, txo_id_hex) in &spent_txos { + Txo::update_spent_block_index(txo_id_hex, *block_index as u64, conn)?; + TransactionLog::update_pending_associated_with_txo_to_succeeded( + txo_id_hex, + *block_index, + conn, + )?; + } + + TransactionLog::update_pending_exceeding_tombstone_block_index_to_failed( + end_block_index + 1, conn, )?; - // TODO: What's the best way to get the assigned_subaddress_b58? - // Do we even care about saving this in the database at all? We - // should be able to look up any relevant information about the - // txo directly from the txo table. This will also hinder us - // from supporting recoverable transaction history in the case that - // there are txo's that go to multiple different subaddresses in the - // same transaction. - // My thoughts are to remove assigned_subaddress_b58 entirely from - // this table and use the TransactionTxoType table to look up info - // about each of the txo's independently, since each on could - // be at a different subaddress. - // In fact, do we even want to be creating a TransactionLog for - // individual txo's at all, since all of this information is - // derivable from the txo's table? The only thing that's necessary - // to store in the database WRT a transaction is when we send, - // because that requires extra meta data that isn't derivable - // from the ledger. - // - // TL;DR - // Reconsider creating a TransactionLog in favor of deriving the - // information from the txo's table when necessary, and only - // store information about sent transactions. - - let assigned_subaddress_b58: Option = match subaddress_index { - None => None, - Some(subaddress_index) => { - let subaddress = account_key.subaddress(subaddress_index); - let subaddress_b58 = b58_encode_public_address(&subaddress)?; - Some(subaddress_b58) - } - }; + // Done syncing this chunk. Mark these blocks as synced for this account. + account.update_next_block_index(end_block_index + 1, conn)?; - if amount.token_id == Mob::ID { - TransactionLog::log_received( - account_id_hex, - assigned_subaddress_b58.as_deref(), - txo_id.as_str(), + let num_blocks_synced = end_block_index - start_block_index + 1; + + let duration = start_time.elapsed(); + + log::debug!( + logger, + "Synced {} blocks ({}-{}) for account {} in {:?}. {} txos received, {}/{} txos spent.", + num_blocks_synced, + start_block_index, + end_block_index, + account_id_hex.chars().take(6).collect::(), + duration, + num_received_txos, + num_spent_txos, + unspent_key_images.len(), + ); + + if num_blocks_synced < BLOCKS_CHUNK_SIZE { + Ok(SyncStatus::NoMoreBlocks) + } else { + Ok(SyncStatus::ChunkFinished) + } + } else { + let account_key: AccountKey = mc_util_serial::decode(&account.account_key)?; + + // Attempt to decode each transaction as received by this account. + let received_txos: Vec<_> = tx_outs + .into_par_iter() + .filter_map(|(block_index, tx_out)| { + let amount = match decode_amount(&tx_out, account_key.view_private_key()) { + None => return None, + Some(a) => a, + }; + let (subaddress_index, key_image) = + decode_subaddress_and_key_image(&tx_out, &account_key, &subaddress_keys); + Some((block_index, tx_out, amount, subaddress_index, key_image)) + }) + .collect(); + let num_received_txos = received_txos.len(); + + // Write received transactions to the database. + for (block_index, tx_out, amount, subaddress_index, key_image) in received_txos { + Txo::create_received( + tx_out.clone(), + subaddress_index, + key_image, amount, - block_index as u64, + block_index, + account_id_hex, + conn, + )?; + } + + // Match key images to mark existing unspent transactions as spent. + let unspent_key_images: HashMap = + Txo::list_unspent_or_pending_key_images(account_id_hex, None, conn)?; + let spent_txos: Vec<(u64, String)> = key_images + .into_par_iter() + .filter_map(|(block_index, key_image)| { + unspent_key_images + .get(&key_image) + .map(|txo_id_hex| (block_index, txo_id_hex.clone())) + }) + .collect(); + let num_spent_txos = spent_txos.len(); + for (block_index, txo_id_hex) in &spent_txos { + Txo::update_spent_block_index(txo_id_hex, *block_index as u64, conn)?; + TransactionLog::update_pending_associated_with_txo_to_succeeded( + txo_id_hex, + *block_index, conn, )?; } - } - // Match key images to mark existing unspent transactions as spent. - let unspent_key_images: HashMap = - Txo::list_unspent_or_pending_key_images(account_id_hex, None, conn)?; - let spent_txos: Vec<(u64, String)> = key_images - .into_par_iter() - .filter_map(|(block_index, key_image)| { - unspent_key_images - .get(&key_image) - .map(|txo_id_hex| (block_index, txo_id_hex.clone())) - }) - .collect(); - let num_spent_txos = spent_txos.len(); - for (block_index, txo_id_hex) in &spent_txos { - Txo::update_to_spent(txo_id_hex, *block_index as u64, conn)?; - TransactionLog::update_tx_logs_associated_with_txo_to_succeeded( - txo_id_hex, - *block_index, + TransactionLog::update_pending_exceeding_tombstone_block_index_to_failed( + end_block_index + 1, conn, )?; - } - let txos_exceeding_pending_block_index = Txo::list_pending_exceeding_block_index( - account_id_hex, - end_block_index + 1, - None, - conn, - )?; - TransactionLog::update_tx_logs_associated_with_txos_to_failed( - &txos_exceeding_pending_block_index, - conn, - )?; + // Done syncing this chunk. Mark these blocks as synced for this account. + account.update_next_block_index(end_block_index + 1, conn)?; - Txo::update_txos_exceeding_pending_tombstone_block_index_to_unspent( - end_block_index + 1, - conn, - )?; + let num_blocks_synced = end_block_index - start_block_index + 1; - // Done syncing this chunk. Mark these blocks as synced for this account. - account.update_next_block_index(end_block_index + 1, conn)?; + let duration = start_time.elapsed(); - let num_blocks_synced = end_block_index - start_block_index + 1; - - let duration = start_time.elapsed(); - - log::debug!( + log::debug!( logger, "Synced {} blocks ({}-{}) for account {} in {:?}. {} txos received, {}/{} txos spent.", num_blocks_synced, @@ -505,10 +372,11 @@ fn sync_account_next_chunk( unspent_key_images.len(), ); - if num_blocks_synced < BLOCKS_CHUNK_SIZE { - Ok(SyncStatus::NoMoreBlocks) - } else { - Ok(SyncStatus::ChunkFinished) + if num_blocks_synced < BLOCKS_CHUNK_SIZE { + Ok(SyncStatus::NoMoreBlocks) + } else { + Ok(SyncStatus::ChunkFinished) + } } }) } @@ -521,7 +389,7 @@ pub fn decode_amount(tx_out: &TxOut, view_private_key: &RistrettoPrivate) -> Opt Ok(k) => k, }; let shared_secret = get_tx_out_shared_secret(view_private_key, &tx_public_key); - match tx_out.masked_amount.get_value(&shared_secret) { + match tx_out.get_masked_amount().ok()?.get_value(&shared_secret) { Ok((a, _)) => Some(a), Err(_) => None, } @@ -540,6 +408,7 @@ pub fn decode_subaddress_index( Ok(k) => k, Err(_) => return None, }; + let subaddress_spk: RistrettoPublic = recover_public_subaddress_spend_key(view_private_key, &tx_out_target_key, &tx_public_key); subaddress_keys.get(&subaddress_spk).copied() @@ -588,6 +457,7 @@ mod tests { }; use mc_account_keys::{AccountKey, RootEntropy, RootIdentity}; use mc_common::logger::{test_with_logger, Logger}; + use mc_transaction_core::{tokens::Mob, Token}; use mc_util_from_random::FromRandom; use rand::{rngs::StdRng, SeedableRng}; @@ -629,7 +499,7 @@ mod tests { ); let service = setup_wallet_service(ledger_db.clone(), logger.clone()); - let wallet_db = &service.wallet_db; + let wallet_db = &service.wallet_db.as_ref().unwrap(); // Import the account let _account = service @@ -654,11 +524,20 @@ mod tests { // There should now be 16 txos. Let's get each one and verify the amount let expected_value = 15_625_000 * MOB; - let txos = service - .list_txos(&AccountID::from(&account_key), None, None, None) + let txos_and_statuses = service + .list_txos( + Some(AccountID::from(&account_key).to_string()), + None, + None, + None, + None, + None, + None, + None, + ) .unwrap(); - for txo in txos { + for (txo, _) in txos_and_statuses { assert_eq!(txo.value as u64, expected_value); } @@ -666,7 +545,8 @@ mod tests { let balance = service .get_balance_for_account(&AccountID::from(&account_key)) .expect("Could not get balance"); - assert_eq!(balance.unspent, 250_000_000 * MOB as u128); + let balance_pmob = balance.get(&Mob::ID).unwrap(); + assert_eq!(balance_pmob.unspent, 250_000_000 * MOB as u128); } // #[test_with_logger] @@ -694,7 +574,8 @@ mod tests { // ); // let service = setup_wallet_service(ledger_db.clone(), - // logger.clone()); let wallet_db = &service.wallet_db; + // logger.clone()); let wallet_db = + // &service.wallet_db.as_ref().unwrap(); // // create view only account // let account = service diff --git a/full-service/src/service/transaction.rs b/full-service/src/service/transaction.rs index e2d35be48..e230554ca 100644 --- a/full-service/src/service/transaction.rs +++ b/full-service/src/service/transaction.rs @@ -5,35 +5,43 @@ use crate::{ db::{ account::{AccountID, AccountModel}, - models::{Account, TransactionLog, ViewOnlyAccount, ViewOnlyTxo}, + models::{Account, TransactionLog}, transaction, - transaction_log::{AssociatedTxos, TransactionLogModel}, - txo::TxoID, - view_only_account::ViewOnlyAccountModel, - view_only_txo::ViewOnlyTxoModel, + transaction_log::{AssociatedTxos, TransactionLogModel, ValueMap}, WalletDbError, }, error::WalletTransactionBuilderError, + json_rpc::v2::models::amount::Amount as AmountJSON, service::{ - ledger::LedgerService, transaction_builder::WalletTransactionBuilder, WalletService, + ledger::{LedgerService, LedgerServiceError}, + models::tx_proposal::TxProposal, + transaction_builder::WalletTransactionBuilder, + WalletService, }, util::b58::{b58_decode_public_address, B58Error}, }; +use mc_account_keys::AccountKey; use mc_common::logger::log; use mc_connection::{BlockchainConnection, RetryableUserTxConnection, UserTxConnection}; use mc_fog_report_validation::FogPubkeyResolver; -use mc_ledger_db::Ledger; -use mc_mobilecoind::payments::TxProposal; -use mc_transaction_core::constants::{MAX_INPUTS, MAX_OUTPUTS}; - -use crate::{ - fog_resolver::FullServiceFogResolver, - service::address::{AddressService, AddressServiceError}, - unsigned_tx::UnsignedTx, +use mc_transaction_core::{ + constants::{MAX_INPUTS, MAX_OUTPUTS}, + tokens::Mob, + Amount, Token, TokenId, }; +use mc_transaction_std::{ + BurnRedemptionMemo, BurnRedemptionMemoBuilder, EmptyMemoBuilder, MemoBuilder, RTHMemoBuilder, + SenderMemoCredential, +}; + +use crate::service::address::{AddressService, AddressServiceError}; use displaydoc::Display; +use serde::{Deserialize, Serialize}; +use serde_big_array::BigArray; use std::{convert::TryFrom, iter::empty, sync::atomic::Ordering}; +use super::models::tx_proposal::UnsignedTxProposal; + /// Errors for the Transaction Service. #[derive(Display, Debug)] #[allow(clippy::large_enum_variant)] @@ -84,6 +92,30 @@ pub enum TransactionServiceError { /// Ledger DB Error: {0} LedgerDB(mc_ledger_db::Error), + + /// Invalid Amount: {0} + InvalidAmount(String), + + /// No default fee found for token id: {0} + DefaultFeeNotFoundForToken(TokenId), + + /// Error decoding hex string + FromHex(hex::FromHexError), + + /// Invalid burn redemption memo: {0} + InvalidBurnRedemptionMemo(String), + + /// mc_util_serial decode error: {0} + Decode(mc_util_serial::DecodeError), + + /// Tx Builder Error: {0} + TxBuilder(mc_transaction_std::TxBuilderError), + + /// Ledger service error: {0} + LedgerService(LedgerServiceError), + + /// Key Error: {0} + Key(mc_crypto_keys::KeyError), } impl From for TransactionServiceError { @@ -140,50 +172,125 @@ impl From for TransactionServiceError { } } +impl From for TransactionServiceError { + fn from(src: hex::FromHexError) -> Self { + Self::FromHex(src) + } +} + +impl From for TransactionServiceError { + fn from(src: mc_util_serial::DecodeError) -> Self { + Self::Decode(src) + } +} + +impl From for TransactionServiceError { + fn from(src: mc_transaction_std::TxBuilderError) -> Self { + Self::TxBuilder(src) + } +} + +impl From for TransactionServiceError { + fn from(src: mc_crypto_keys::KeyError) -> Self { + Self::Key(src) + } +} + +impl From for TransactionServiceError { + fn from(src: LedgerServiceError) -> Self { + Self::LedgerService(src) + } +} + +#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] +pub enum TransactionMemo { + /// Empty Transaction Memo. + Empty, + + /// Recoverable Transaction History memo. + RTH, + + /// Burn Redemption memo, with an optional 64 byte redemption memo hex + /// string. + #[serde(with = "BigArray")] + BurnRedemption([u8; BurnRedemptionMemo::MEMO_DATA_LEN]), +} + +impl TransactionMemo { + pub fn memo_builder( + &self, + account_key: Option, + ) -> Result, WalletTransactionBuilderError> { + match self { + Self::Empty => Ok(Box::new(EmptyMemoBuilder::default())), + Self::RTH => { + let mut memo_builder = RTHMemoBuilder::default(); + memo_builder + .set_sender_credential(SenderMemoCredential::from(&account_key.ok_or( + WalletTransactionBuilderError::RTHUnavailableForViewOnlyAccounts, + )?)); + memo_builder.enable_destination_memo(); + Ok(Box::new(memo_builder)) + } + Self::BurnRedemption(memo_data) => { + let mut memo_builder = BurnRedemptionMemoBuilder::new(*memo_data); + memo_builder.enable_destination_memo(); + Ok(Box::new(memo_builder)) + } + } + } +} + /// Trait defining the ways in which the wallet can interact with and manage /// transactions. pub trait TransactionService { - fn build_unsigned_transaction( + #[allow(clippy::too_many_arguments)] + fn build_transaction( &self, account_id_hex: &str, - addresses_and_values: &[(String, String)], - fee: Option, + addresses_and_amounts: &[(String, AmountJSON)], + input_txo_ids: Option<&Vec>, + fee_value: Option, + fee_token_id: Option, tombstone_block: Option, - ) -> Result<(UnsignedTx, FullServiceFogResolver), TransactionServiceError>; + max_spendable_value: Option, + memo: TransactionMemo, + ) -> Result; - /// Builds a transaction from the given account to the specified recipients. #[allow(clippy::too_many_arguments)] - fn build_transaction( + fn build_and_sign_transaction( &self, account_id_hex: &str, - addresses_and_values: &[(String, String)], + addresses_and_amounts: &[(String, AmountJSON)], input_txo_ids: Option<&Vec>, - fee: Option, + fee_value: Option, + fee_token_id: Option, tombstone_block: Option, max_spendable_value: Option, - log_tx_proposal: Option, + memo: TransactionMemo, ) -> Result; /// Submits a pre-built TxProposal to the MobileCoin Consensus Network. fn submit_transaction( &self, - tx_proposal: TxProposal, + tx_proposal: &TxProposal, comment: Option, account_id_hex: Option, - ) -> Result, TransactionServiceError>; + ) -> Result, TransactionServiceError>; - /// Convenience method that builds and submits in one go. #[allow(clippy::too_many_arguments)] - fn build_and_submit( + fn build_sign_and_submit_transaction( &self, account_id_hex: &str, - addresses_and_values: &[(String, String)], + addresses_and_amounts: &[(String, AmountJSON)], input_txo_ids: Option<&Vec>, - fee: Option, + fee_value: Option, + fee_token_id: Option, tombstone_block: Option, max_spendable_value: Option, comment: Option, - ) -> Result<(TransactionLog, AssociatedTxos, TxProposal), TransactionServiceError>; + memo: TransactionMemo, + ) -> Result<(TransactionLog, AssociatedTxos, ValueMap, TxProposal), TransactionServiceError>; } impl TransactionService for WalletService @@ -191,81 +298,41 @@ where T: BlockchainConnection + UserTxConnection + 'static, FPR: FogPubkeyResolver + Send + Sync + 'static, { - fn build_unsigned_transaction( - &self, - account_id_hex: &str, - addresses_and_values: &[(String, String)], - fee: Option, - tombstone_block: Option, - ) -> Result<(UnsignedTx, FullServiceFogResolver), TransactionServiceError> { - validate_number_outputs(addresses_and_values.len() as u64)?; - - let conn = self.wallet_db.get_conn()?; - transaction(&conn, || { - let mut builder = WalletTransactionBuilder::new( - account_id_hex.to_string(), - self.ledger_db.clone(), - self.fog_resolver_factory.clone(), - self.logger.clone(), - ); - - for (recipient_public_address, value) in addresses_and_values { - if !self.verify_address(recipient_public_address)? { - return Err(TransactionServiceError::InvalidPublicAddress( - recipient_public_address.to_string(), - )); - }; - let recipient = b58_decode_public_address(recipient_public_address)?; - builder.add_recipient(recipient, value.parse::()?)?; - } - - if let Some(tombstone) = tombstone_block { - builder.set_tombstone(tombstone.parse::()?)?; - } else { - builder.set_tombstone(0)?; - } - - builder.set_fee(match fee { - Some(f) => f.parse()?, - None => self.get_network_fee(), - })?; - - let unsigned_tx = builder.build_unsigned(&conn)?; - let fog_resolver = builder.get_fs_fog_resolver(&conn)?; - - Ok((unsigned_tx, fog_resolver)) - }) - } fn build_transaction( &self, account_id_hex: &str, - addresses_and_values: &[(String, String)], + addresses_and_amounts: &[(String, AmountJSON)], input_txo_ids: Option<&Vec>, - fee: Option, + fee_value: Option, + fee_token_id: Option, tombstone_block: Option, max_spendable_value: Option, - log_tx_proposal: Option, - ) -> Result { + memo: TransactionMemo, + ) -> Result { validate_number_inputs(input_txo_ids.unwrap_or(&Vec::new()).len() as u64)?; - validate_number_outputs(addresses_and_values.len() as u64)?; + validate_number_outputs(addresses_and_amounts.len() as u64)?; - let conn = self.wallet_db.get_conn()?; + let conn = self.get_conn()?; transaction(&conn, || { let mut builder = WalletTransactionBuilder::new( account_id_hex.to_string(), self.ledger_db.clone(), self.fog_resolver_factory.clone(), - self.logger.clone(), ); - for (recipient_public_address, value) in addresses_and_values { + let mut default_fee_token_id = Mob::ID; + + for (recipient_public_address, amount) in addresses_and_amounts { if !self.verify_address(recipient_public_address)? { return Err(TransactionServiceError::InvalidPublicAddress( recipient_public_address.to_string(), )); }; let recipient = b58_decode_public_address(recipient_public_address)?; - builder.add_recipient(recipient, value.parse::()?)?; + let amount = + Amount::try_from(amount).map_err(TransactionServiceError::InvalidAmount)?; + builder.add_recipient(recipient, amount.value, amount.token_id)?; + default_fee_token_id = amount.token_id; } if let Some(tombstone) = tombstone_block { @@ -274,36 +341,67 @@ where builder.set_tombstone(0)?; } - builder.set_fee(match fee { - Some(f) => f.parse()?, - None => self.get_network_fee(), - })?; + let fee_token_id = match fee_token_id { + Some(t) => TokenId::from(t.parse::()?), + None => default_fee_token_id, + }; + + let fee_value = match fee_value { + Some(f) => f.parse::()?, + None => *self.get_network_fees()?.get(&fee_token_id).ok_or( + TransactionServiceError::DefaultFeeNotFoundForToken(fee_token_id), + )?, + }; - builder.set_block_version(self.get_network_block_version()); + builder.set_fee(fee_value, fee_token_id)?; + + builder.set_block_version(self.get_network_block_version()?); if let Some(inputs) = input_txo_ids { - builder.set_txos(&conn, inputs, log_tx_proposal.unwrap_or_default())?; + builder.set_txos(&conn, inputs)?; } else { let max_spendable = if let Some(msv) = max_spendable_value { Some(msv.parse::()?) } else { None }; - builder.select_txos(&conn, max_spendable, log_tx_proposal.unwrap_or_default())?; + builder.select_txos(&conn, max_spendable)?; } - let tx_proposal = builder.build(&conn)?; - - if log_tx_proposal.unwrap_or_default() { - let block_index = self.ledger_db.num_blocks()? - 1; - let _transaction_log = TransactionLog::log_submitted( - tx_proposal.clone(), - block_index, - "".to_string(), - account_id_hex, - &conn, - )?; - } + let unsigned_tx_proposal = builder.build(memo, &conn)?; + + Ok(unsigned_tx_proposal) + }) + } + + fn build_and_sign_transaction( + &self, + account_id_hex: &str, + addresses_and_amounts: &[(String, AmountJSON)], + input_txo_ids: Option<&Vec>, + fee_value: Option, + fee_token_id: Option, + tombstone_block: Option, + max_spendable_value: Option, + memo: TransactionMemo, + ) -> Result { + let unsigned_tx_proposal = self.build_transaction( + account_id_hex, + addresses_and_amounts, + input_txo_ids, + fee_value, + fee_token_id, + tombstone_block, + max_spendable_value, + memo, + )?; + let conn = self.get_conn()?; + transaction(&conn, || { + let account = Account::get(&AccountID(account_id_hex.to_string()), &conn)?; + let account_key: AccountKey = mc_util_serial::decode(&account.account_key)?; + let tx_proposal = unsigned_tx_proposal.sign(&account_key)?; + + TransactionLog::log_built(tx_proposal.clone(), "".to_string(), account_id_hex, &conn)?; Ok(tx_proposal) }) @@ -311,10 +409,10 @@ where fn submit_transaction( &self, - tx_proposal: TxProposal, + tx_proposal: &TxProposal, comment: Option, account_id_hex: Option, - ) -> Result, TransactionServiceError> { + ) -> Result, TransactionServiceError> { if self.offline { return Err(TransactionServiceError::Offline); } @@ -328,30 +426,22 @@ where let idx = self.submit_node_offset.fetch_add(1, Ordering::SeqCst); let responder_id = &responder_ids[idx % responder_ids.len()]; - // FIXME: WS-34 - would prefer not to convert to proto as intermediary - let tx_proposal_proto = mc_mobilecoind_api::TxProposal::try_from(&tx_proposal) - .map_err(|_| TransactionServiceError::ProtoConversionInfallible)?; - - // Try to submit. - let tx = mc_transaction_core::tx::Tx::try_from(tx_proposal_proto.get_tx()) - .map_err(|_| TransactionServiceError::ProtoConversionInfallible)?; - let block_index = self .peer_manager .conn(responder_id) .ok_or(TransactionServiceError::NodeNotFound)? - .propose_tx(&tx, empty()) + .propose_tx(&tx_proposal.tx, empty()) .map_err(TransactionServiceError::from)?; log::trace!( self.logger, "Tx {:?} submitted at block height {}", - tx, + tx_proposal.tx, block_index ); if let Some(account_id_hex) = account_id_hex { - let conn = self.wallet_db.get_conn()?; + let conn = self.get_conn()?; let account_id = AccountID(account_id_hex.to_string()); transaction(&conn, || { @@ -365,21 +455,9 @@ where )?; let associated_txos = transaction_log.get_associated_txos(&conn)?; + let value_map = transaction_log.value_map(&conn)?; - Ok(Some((transaction_log, associated_txos))) - } else if ViewOnlyAccount::get(&account_id_hex, &conn).is_ok() { - for utxo in tx_proposal.utxos { - let txo_id = TxoID::from(&utxo.tx_out); - ViewOnlyTxo::update_for_pending_transaction( - &txo_id.to_string(), - utxo.subaddress_index, - &utxo.key_image, - block_index, - tx_proposal.tx.prefix.tombstone_block, - &conn, - )?; - } - Ok(None) + Ok(Some((transaction_log, associated_txos, value_map))) } else { Err(TransactionServiceError::Database( WalletDbError::AccountNotFound(account_id_hex), @@ -391,33 +469,37 @@ where } } - fn build_and_submit( + fn build_sign_and_submit_transaction( &self, account_id_hex: &str, - addresses_and_values: &[(String, String)], + addresses_and_amounts: &[(String, AmountJSON)], input_txo_ids: Option<&Vec>, - fee: Option, + fee_value: Option, + fee_token_id: Option, tombstone_block: Option, max_spendable_value: Option, comment: Option, - ) -> Result<(TransactionLog, AssociatedTxos, TxProposal), TransactionServiceError> { - let tx_proposal = self.build_transaction( + memo: TransactionMemo, + ) -> Result<(TransactionLog, AssociatedTxos, ValueMap, TxProposal), TransactionServiceError> + { + let tx_proposal = self.build_and_sign_transaction( account_id_hex, - addresses_and_values, + addresses_and_amounts, input_txo_ids, - fee, + fee_value, + fee_token_id, tombstone_block, max_spendable_value, - None, + memo, )?; - if let Some(transaction_log_and_associated_txos) = self.submit_transaction( - tx_proposal.clone(), - comment, - Some(account_id_hex.to_string()), - )? { + + if let Some(transaction_log_and_associated_txos) = + self.submit_transaction(&tx_proposal, comment, Some(account_id_hex.to_string()))? + { Ok(( transaction_log_and_associated_txos.0, transaction_log_and_associated_txos.1, + transaction_log_and_associated_txos.2, tx_proposal, )) } else { @@ -489,10 +571,10 @@ mod tests { // Add a block with a transaction for Alice let alice_account_key: AccountKey = mc_util_serial::decode(&alice.account_key).unwrap(); let alice_account_id = AccountID::from(&alice_account_key); - let alice_public_address = alice_account_key.subaddress(alice.main_subaddress_index as u64); + let alice_public_address = alice_account_key.default_subaddress(); let tx_logs = service - .list_transaction_logs(&alice_account_id, None, None, None, None) + .list_transaction_logs(Some(alice_account_id.to_string()), None, None, None, None) .unwrap(); assert_eq!(0, tx_logs.len()); @@ -505,19 +587,25 @@ mod tests { &mut rng, ); - manually_sync_account(&ledger_db, &service.wallet_db, &alice_account_id, &logger); + manually_sync_account( + &ledger_db, + &service.wallet_db.as_ref().unwrap(), + &alice_account_id, + &logger, + ); let tx_logs = service - .list_transaction_logs(&alice_account_id, None, None, None, None) + .list_transaction_logs(Some(alice_account_id.to_string()), None, None, None, None) .unwrap(); - assert_eq!(1, tx_logs.len()); + assert_eq!(0, tx_logs.len()); // Verify balance for Alice let balance = service - .get_balance_for_account(&AccountID(alice.account_id_hex.clone())) + .get_balance_for_account(&AccountID(alice.id.clone())) .unwrap(); - assert_eq!(balance.unspent, 100 * MOB as u128); + let balance_pmob = balance.get(&Mob::ID).unwrap(); + assert_eq!(balance_pmob.unspent, 100 * MOB as u128); // Add an account for Bob let bob = service @@ -534,84 +622,87 @@ mod tests { // Create an assigned subaddress for Bob let bob_address_from_alice = service - .assign_address_for_account(&AccountID(bob.account_id_hex.clone()), Some("From Alice")) + .assign_address_for_account(&AccountID(bob.id.clone()), Some("From Alice")) .unwrap(); let _tx_proposal = service - .build_transaction( - &alice.account_id_hex, + .build_and_sign_transaction( + &alice.id, &[( - bob_address_from_alice.assigned_subaddress_b58, - (42 * MOB).to_string(), + bob_address_from_alice.public_address_b58, + AmountJSON::new(42 * MOB, Mob::ID), )], None, None, None, None, None, + TransactionMemo::RTH, ) .unwrap(); log::info!(logger, "Built transaction from Alice"); let tx_logs = service - .list_transaction_logs(&alice_account_id, None, None, None, None) + .list_transaction_logs(Some(alice_account_id.to_string()), None, None, None, None) .unwrap(); assert_eq!(1, tx_logs.len()); // Create an assigned subaddress for Bob let bob_address_from_alice_2 = service - .assign_address_for_account(&AccountID(bob.account_id_hex.clone()), Some("From Alice")) + .assign_address_for_account(&AccountID(bob.id.clone()), Some("From Alice")) .unwrap(); let _tx_proposal = service - .build_transaction( - &alice.account_id_hex, + .build_and_sign_transaction( + &alice.id, &[( - bob_address_from_alice_2.assigned_subaddress_b58, - (42 * MOB).to_string(), + bob_address_from_alice_2.public_address_b58, + AmountJSON::new(42 * MOB, Mob::ID), )], None, None, None, None, - Some(false), + None, + TransactionMemo::RTH, ) .unwrap(); log::info!(logger, "Built transaction from Alice"); let tx_logs = service - .list_transaction_logs(&alice_account_id, None, None, None, None) + .list_transaction_logs(Some(alice_account_id.to_string()), None, None, None, None) .unwrap(); - assert_eq!(1, tx_logs.len()); + assert_eq!(2, tx_logs.len()); // Create an assigned subaddress for Bob let bob_address_from_alice_3 = service - .assign_address_for_account(&AccountID(bob.account_id_hex.clone()), Some("From Alice")) + .assign_address_for_account(&AccountID(bob.id.clone()), Some("From Alice")) .unwrap(); let _tx_proposal = service - .build_transaction( - &alice.account_id_hex, + .build_and_sign_transaction( + &alice.id, &[( - bob_address_from_alice_3.clone().assigned_subaddress_b58, - (42 * MOB).to_string(), + bob_address_from_alice_3.clone().public_address_b58, + AmountJSON::new(42 * MOB, Mob::ID), )], None, None, None, None, - Some(true), + None, + TransactionMemo::RTH, ) .unwrap(); log::info!(logger, "Built transaction from Alice"); let tx_logs = service - .list_transaction_logs(&alice_account_id, None, None, None, None) + .list_transaction_logs(Some(alice_account_id.to_string()), None, None, None, None) .unwrap(); - assert_eq!(2, tx_logs.len()); + assert_eq!(3, tx_logs.len()); } // Test sending a transaction from Alice -> Bob, and then from Bob -> Alice @@ -637,7 +728,7 @@ mod tests { // Add a block with a transaction for Alice let alice_account_key: AccountKey = mc_util_serial::decode(&alice.account_key).unwrap(); let alice_account_id = AccountID::from(&alice_account_key); - let alice_public_address = alice_account_key.subaddress(alice.main_subaddress_index as u64); + let alice_public_address = alice_account_key.default_subaddress(); add_block_to_ledger_db( &mut ledger_db, &vec![alice_public_address.clone()], @@ -646,13 +737,19 @@ mod tests { &mut rng, ); - manually_sync_account(&ledger_db, &service.wallet_db, &alice_account_id, &logger); + manually_sync_account( + &ledger_db, + &service.wallet_db.as_ref().unwrap(), + &alice_account_id, + &logger, + ); // Verify balance for Alice let balance = service - .get_balance_for_account(&AccountID(alice.account_id_hex.clone())) + .get_balance_for_account(&AccountID(alice.id.clone())) .unwrap(); - assert_eq!(balance.unspent, 100 * MOB as u128); + let balance_pmob = balance.get(&Mob::ID).unwrap(); + assert_eq!(balance_pmob.unspent, 100 * MOB as u128); // Add an account for Bob let bob = service @@ -669,22 +766,24 @@ mod tests { // Create an assigned subaddress for Bob let bob_address_from_alice = service - .assign_address_for_account(&AccountID(bob.account_id_hex.clone()), Some("From Alice")) + .assign_address_for_account(&AccountID(bob.id.clone()), Some("From Alice")) .unwrap(); // Send a transaction from Alice to Bob - let (transaction_log, _associated_txos, _tx_proposal) = service - .build_and_submit( - &alice.account_id_hex, + let (transaction_log, _associated_txos, _value_map, _tx_proposal) = service + .build_sign_and_submit_transaction( + &alice.id, &[( - bob_address_from_alice.assigned_subaddress_b58, - (42 * MOB).to_string(), + bob_address_from_alice.public_address_b58, + AmountJSON::new(42 * MOB, Mob::ID), )], None, None, None, None, None, + None, + TransactionMemo::RTH, ) .unwrap(); log::info!(logger, "Built and submitted transaction from Alice"); @@ -694,21 +793,31 @@ mod tests { // workaround. { log::info!(logger, "Adding block from transaction log"); - let conn = service.wallet_db.get_conn().unwrap(); - add_block_from_transaction_log(&mut ledger_db, &conn, &transaction_log); + let conn = service.get_conn().unwrap(); + add_block_from_transaction_log(&mut ledger_db, &conn, &transaction_log, &mut rng); } - manually_sync_account(&ledger_db, &service.wallet_db, &alice_account_id, &logger); - manually_sync_account(&ledger_db, &service.wallet_db, &bob_account_id, &logger); + manually_sync_account( + &ledger_db, + &service.wallet_db.as_ref().unwrap(), + &alice_account_id, + &logger, + ); + manually_sync_account( + &ledger_db, + &service.wallet_db.as_ref().unwrap(), + &bob_account_id, + &logger, + ); // Get the Txos from the transaction log let transaction_txos = transaction_log - .get_associated_txos(&service.wallet_db.get_conn().unwrap()) + .get_associated_txos(&service.get_conn().unwrap()) .unwrap(); let secreted = transaction_txos .outputs .iter() - .map(|t| Txo::get(&t.txo_id_hex, &service.wallet_db.get_conn().unwrap()).unwrap()) + .map(|(t, _)| Txo::get(&t.id, &service.get_conn().unwrap()).unwrap()) .collect::>(); assert_eq!(secreted.len(), 1); assert_eq!(secreted[0].value as u64, 42 * MOB); @@ -716,7 +825,7 @@ mod tests { let change = transaction_txos .change .iter() - .map(|t| Txo::get(&t.txo_id_hex, &service.wallet_db.get_conn().unwrap()).unwrap()) + .map(|(t, _)| Txo::get(&t.id, &service.get_conn().unwrap()).unwrap()) .collect::>(); assert_eq!(change.len(), 1); assert_eq!(change[0].value as u64, 58 * MOB - Mob::MINIMUM_FEE); @@ -724,36 +833,40 @@ mod tests { let inputs = transaction_txos .inputs .iter() - .map(|t| Txo::get(&t.txo_id_hex, &service.wallet_db.get_conn().unwrap()).unwrap()) + .map(|t| Txo::get(&t.id, &service.get_conn().unwrap()).unwrap()) .collect::>(); assert_eq!(inputs.len(), 1); assert_eq!(inputs[0].value as u64, 100 * MOB); // Verify balance for Alice = original balance - fee - txo_value let balance = service - .get_balance_for_account(&AccountID(alice.account_id_hex.clone())) + .get_balance_for_account(&AccountID(alice.id.clone())) .unwrap(); - assert_eq!(balance.unspent, (58 * MOB - Mob::MINIMUM_FEE) as u128); + let balance_pmob = balance.get(&Mob::ID).unwrap(); + assert_eq!(balance_pmob.unspent, (58 * MOB - Mob::MINIMUM_FEE) as u128); // Bob's balance should be = output_txo_value let bob_balance = service - .get_balance_for_account(&AccountID(bob.account_id_hex.clone())) + .get_balance_for_account(&AccountID(bob.id.clone())) .unwrap(); - assert_eq!(bob_balance.unspent, 42000000000000); + let bob_balance_pmob = bob_balance.get(&Mob::ID).unwrap(); + assert_eq!(bob_balance_pmob.unspent, 42000000000000); // Bob should now be able to send to Alice - let (transaction_log, _associated_txos, _tx_proposal) = service - .build_and_submit( - &bob.account_id_hex, + let (transaction_log, _associated_txos, _value_map, _tx_proposal) = service + .build_sign_and_submit_transaction( + &bob.id, &[( b58_encode_public_address(&alice_public_address).unwrap(), - (8 * MOB).to_string(), + AmountJSON::new(8 * MOB, Mob::ID), )], None, None, None, None, None, + None, + TransactionMemo::RTH, ) .unwrap(); @@ -763,23 +876,39 @@ mod tests { { log::info!(logger, "Adding block from transaction log"); - let conn = service.wallet_db.get_conn().unwrap(); - add_block_from_transaction_log(&mut ledger_db, &conn, &transaction_log); + let conn = service.get_conn().unwrap(); + add_block_from_transaction_log(&mut ledger_db, &conn, &transaction_log, &mut rng); } - manually_sync_account(&ledger_db, &service.wallet_db, &alice_account_id, &logger); - manually_sync_account(&ledger_db, &service.wallet_db, &bob_account_id, &logger); + manually_sync_account( + &ledger_db, + &service.wallet_db.as_ref().unwrap(), + &alice_account_id, + &logger, + ); + manually_sync_account( + &ledger_db, + &service.wallet_db.as_ref().unwrap(), + &bob_account_id, + &logger, + ); let alice_balance = service - .get_balance_for_account(&AccountID(alice.account_id_hex)) + .get_balance_for_account(&AccountID(alice.id)) .unwrap(); - assert_eq!(alice_balance.unspent, (66 * MOB - Mob::MINIMUM_FEE) as u128); + let alice_balance_pmob = alice_balance.get(&Mob::ID).unwrap(); + assert_eq!( + alice_balance_pmob.unspent, + (66 * MOB - Mob::MINIMUM_FEE) as u128 + ); // Bob's balance should be = output_txo_value - let bob_balance = service - .get_balance_for_account(&AccountID(bob.account_id_hex)) - .unwrap(); - assert_eq!(bob_balance.unspent, (34 * MOB - Mob::MINIMUM_FEE) as u128); + let bob_balance = service.get_balance_for_account(&AccountID(bob.id)).unwrap(); + let bob_balance_pmob = bob_balance.get(&Mob::ID).unwrap(); + assert_eq!( + bob_balance_pmob.unspent, + (34 * MOB - Mob::MINIMUM_FEE) as u128 + ); } // Building a transaction for an invalid public address should fail. @@ -805,7 +934,7 @@ mod tests { // Add a block with a transaction for Alice let alice_account_key: AccountKey = mc_util_serial::decode(&alice.account_key).unwrap(); let alice_account_id = AccountID::from(&alice_account_key); - let alice_public_address = alice_account_key.subaddress(alice.main_subaddress_index as u64); + let alice_public_address = alice_account_key.default_subaddress(); add_block_to_ledger_db( &mut ledger_db, &vec![alice_public_address.clone()], @@ -814,16 +943,22 @@ mod tests { &mut rng, ); - manually_sync_account(&ledger_db, &service.wallet_db, &alice_account_id, &logger); + manually_sync_account( + &ledger_db, + &service.wallet_db.as_ref().unwrap(), + &alice_account_id, + &logger, + ); - match service.build_transaction( - &alice.account_id_hex, - &vec![("NOTB58".to_string(), (42 * MOB).to_string())], + match service.build_and_sign_transaction( + &alice.id, + &vec![("NOTB58".to_string(), AmountJSON::new(42 * MOB, Mob::ID))], None, None, None, None, None, + TransactionMemo::RTH, ) { Ok(_) => { panic!("Should not be able to build transaction to invalid b58 public address") @@ -855,7 +990,7 @@ mod tests { // Add a block with a transaction for Alice let alice_account_key: AccountKey = mc_util_serial::decode(&alice.account_key).unwrap(); let alice_account_id = AccountID::from(&alice_account_key); - let alice_public_address = alice_account_key.subaddress(alice.main_subaddress_index as u64); + let alice_public_address = alice_account_key.default_subaddress(); add_block_to_ledger_db( &mut ledger_db, &vec![alice_public_address.clone()], @@ -864,24 +999,30 @@ mod tests { &mut rng, ); - manually_sync_account(&ledger_db, &service.wallet_db, &alice_account_id, &logger); + manually_sync_account( + &ledger_db, + &service.wallet_db.as_ref().unwrap(), + &alice_account_id, + &logger, + ); // test ouputs let mut outputs = Vec::new(); for _ in 0..17 { outputs.push(( b58_encode_public_address(&alice_public_address).unwrap(), - (42 * MOB).to_string(), + AmountJSON::new(42 * MOB, Mob::ID), )); } - match service.build_transaction( - &alice.account_id_hex, + match service.build_and_sign_transaction( + &alice.id, &outputs, None, None, None, None, None, + TransactionMemo::RTH, ) { Ok(_) => { panic!("Should not be able to build transaction with too many ouputs") @@ -897,21 +1038,22 @@ mod tests { for _ in 0..2 { outputs.push(( b58_encode_public_address(&alice_public_address).unwrap(), - (42 * MOB).to_string(), + AmountJSON::new(42 * MOB, Mob::ID), )); } let mut inputs = Vec::new(); for _ in 0..17 { inputs.push("fake txo id".to_string()); } - match service.build_transaction( - &alice.account_id_hex, + match service.build_and_sign_transaction( + &alice.id, &outputs, Some(&inputs), None, None, None, None, + TransactionMemo::RTH, ) { Ok(_) => { panic!("Should not be able to build transaction with too many inputs") diff --git a/full-service/src/service/transaction_builder.rs b/full-service/src/service/transaction_builder.rs index d4452ebb7..7f76e420f 100644 --- a/full-service/src/service/transaction_builder.rs +++ b/full-service/src/service/transaction_builder.rs @@ -11,44 +11,35 @@ use crate::{ db::{ account::{AccountID, AccountModel}, - models::{Account, Txo, ViewOnlyAccount, ViewOnlyTxo}, + assigned_subaddress::AssignedSubaddressModel, + models::{Account, Txo}, txo::TxoModel, - view_only_account::ViewOnlyAccountModel, - view_only_txo::ViewOnlyTxoModel, Conn, }, error::WalletTransactionBuilderError, - fog_resolver::{FullServiceFogResolver, FullServiceFullyValidatedFogPubkey}, - unsigned_tx::UnsignedTx, - util::b58::b58_encode_public_address, + service::transaction::TransactionMemo, }; -use mc_account_keys::{AccountKey, PublicAddress}; -use mc_common::{ - logger::{log, Logger}, - HashMap, HashSet, -}; -use mc_crypto_keys::RistrettoPublic; +use mc_account_keys::PublicAddress; +use mc_common::HashSet; +use mc_crypto_ring_signature_signer::OneTimeKeyDeriveData; use mc_fog_report_validation::FogPubkeyResolver; use mc_ledger_db::{Ledger, LedgerDB}; -use mc_mobilecoind::{ - payments::{Outlay, TxProposal}, - UnspentTxOut, -}; use mc_transaction_core::{ constants::RING_SIZE, - onetime_keys::recover_onetime_private_key, - ring_signature::KeyImage, tokens::Mob, - tx::{TxIn, TxOut, TxOutMembershipProof}, - Amount, BlockVersion, Token, + tx::{TxOut, TxOutMembershipProof}, + Amount, BlockVersion, Token, TokenId, }; + use mc_transaction_std::{ - ChangeDestination, InputCredentials, RTHMemoBuilder, SenderMemoCredential, TransactionBuilder, + DefaultTxOutputsOrdering, InputCredentials, ReservedSubaddresses, TransactionBuilder, }; use mc_util_uri::FogUri; -use rand::Rng; -use std::{convert::TryFrom, str::FromStr, sync::Arc}; +use rand::{rngs::ThreadRng, Rng}; +use std::{collections::BTreeMap, str::FromStr, sync::Arc}; + +use super::models::tx_proposal::{OutputTxo, UnsignedInputTxo, UnsignedTxProposal}; /// Default number of blocks used for calculating transaction tombstone block /// number. @@ -68,13 +59,13 @@ pub struct WalletTransactionBuilder { /// Vector of (PublicAddress, Amounts) for the recipients of this /// transaction. - outlays: Vec<(PublicAddress, u64)>, + outlays: Vec<(PublicAddress, u64, TokenId)>, /// The block after which this transaction is invalid. tombstone: u64, /// The fee for the transaction. - fee: Option, + fee: Option<(u64, TokenId)>, /// The block version for the transaction block_version: Option, @@ -83,9 +74,6 @@ pub struct WalletTransactionBuilder { /// This is abstracted because in tests, we don't want to form grpc /// connections to fog. fog_resolver_factory: Arc Result + Send + Sync>, - - /// Logger. - logger: Logger, } impl WalletTransactionBuilder { @@ -93,7 +81,6 @@ impl WalletTransactionBuilder { account_id_hex: String, ledger_db: LedgerDB, fog_resolver_factory: Arc Result + Send + Sync + 'static>, - logger: Logger, ) -> Self { WalletTransactionBuilder { account_id_hex, @@ -104,7 +91,6 @@ impl WalletTransactionBuilder { fee: None, block_version: None, fog_resolver_factory, - logger, } } @@ -114,21 +100,12 @@ impl WalletTransactionBuilder { &mut self, conn: &Conn, input_txo_ids: &[String], - update_to_pending: bool, ) -> Result<(), WalletTransactionBuilderError> { - let pending_tombstone_block_index = if update_to_pending { - Some(self.tombstone) - } else { - None - }; - - let txos = Txo::select_by_id(input_txo_ids, pending_tombstone_block_index, conn)?; + let txos = Txo::select_by_id(input_txo_ids, conn)?; let unspent: Vec = txos .iter() - .filter(|txo| { - txo.pending_tombstone_block_index == None && txo.spent_block_index == None - }) + .filter(|txo| txo.spent_block_index == None) .cloned() .collect(); @@ -146,91 +123,84 @@ impl WalletTransactionBuilder { &mut self, conn: &Conn, max_spendable_value: Option, - update_to_pending: bool, ) -> Result<(), WalletTransactionBuilderError> { - let outlay_value_sum = self.outlays.iter().map(|(_r, v)| *v as u128).sum::(); - - let fee = self.fee.unwrap_or(Mob::MINIMUM_FEE); - if outlay_value_sum > u64::MAX as u128 || outlay_value_sum > u64::MAX as u128 - fee as u128 - { - return Err(WalletTransactionBuilderError::OutboundValueTooLarge); - } - log::info!( - self.logger, - "Selecting Txos for value {:?} with fee {:?}", - outlay_value_sum, - fee - ); - let total_value = outlay_value_sum as u64 + fee; - - let pending_tombstone_block_index = if update_to_pending { - Some(self.tombstone) - } else { - None - }; - - self.inputs = Txo::select_unspent_txos_for_value( - &self.account_id_hex, - total_value, - max_spendable_value, - pending_tombstone_block_index, - Some(0), - conn, - )?; - - Ok(()) - } + let mut outlay_value_sum_map: BTreeMap = + self.outlays + .iter() + .fold(BTreeMap::new(), |mut acc, (_, value, token_id)| { + acc.entry(*token_id) + .and_modify(|v| *v += *value as u128) + .or_insert(*value as u128); + acc + }); + + let (fee_value, fee_token_id) = self.fee.unwrap_or((Mob::MINIMUM_FEE, Mob::ID)); + outlay_value_sum_map + .entry(fee_token_id) + .and_modify(|v| *v += fee_value as u128) + .or_insert(fee_value as u128); + + for (token_id, target_value) in outlay_value_sum_map { + if target_value > u64::MAX as u128 { + return Err(WalletTransactionBuilderError::OutboundValueTooLarge); + } - /// Selects View Only Txos from the account. - fn select_view_only_txos( - &self, - conn: &Conn, - ) -> Result, WalletTransactionBuilderError> { - let outlay_value_sum = self.outlays.iter().map(|(_r, v)| *v as u128).sum::(); + let fee_value = if token_id == fee_token_id { + fee_value + } else { + 0 + }; - let fee = self.fee.unwrap_or(Mob::MINIMUM_FEE); - if outlay_value_sum > u64::MAX as u128 || outlay_value_sum > u64::MAX as u128 - fee as u128 - { - return Err(WalletTransactionBuilderError::OutboundValueTooLarge); + self.inputs = Txo::select_spendable_txos_for_value( + &self.account_id_hex, + target_value as u64, + max_spendable_value, + *token_id, + fee_value, + conn, + )?; } - log::info!( - self.logger, - "Selecting Txos for value {:?} with fee {:?}", - outlay_value_sum, - fee - ); - let total_value = outlay_value_sum as u64 + fee; - Ok(ViewOnlyTxo::select_unspent_view_only_txos_for_value( - &self.account_id_hex, - total_value, - Some(0), - conn, - )?) + Ok(()) } pub fn add_recipient( &mut self, recipient: PublicAddress, value: u64, + token_id: TokenId, ) -> Result<(), WalletTransactionBuilderError> { // Verify that the maximum output value of this transaction remains under - // u64::MAX - let cur_sum = self.outlays.iter().map(|(_r, v)| *v as u128).sum::(); + // u64::MAX for the given Token Id + let cur_sum = self + .outlays + .iter() + .filter_map(|(_r, v, t)| { + if *t == token_id { + Some(*v as u128) + } else { + None + } + }) + .sum::(); if cur_sum > u64::MAX as u128 { return Err(WalletTransactionBuilderError::OutboundValueTooLarge); } - self.outlays.push((recipient, value)); + self.outlays.push((recipient, value, token_id)); Ok(()) } - pub fn set_fee(&mut self, fee: u64) -> Result<(), WalletTransactionBuilderError> { + pub fn set_fee( + &mut self, + fee: u64, + token_id: TokenId, + ) -> Result<(), WalletTransactionBuilderError> { if fee < 1 { return Err(WalletTransactionBuilderError::InsufficientFee( "1".to_string(), )); } - self.fee = Some(fee); + self.fee = Some((fee, token_id)); Ok(()) } @@ -249,195 +219,59 @@ impl WalletTransactionBuilder { Ok(()) } - pub fn get_fs_fog_resolver( - &self, - conn: &Conn, - ) -> Result { - let account = ViewOnlyAccount::get(&self.account_id_hex, conn)?; - let change_public_address: PublicAddress = account.change_public_address(conn)?; + pub fn get_fog_resolver(&self, conn: &Conn) -> Result { + let account = Account::get(&AccountID(self.account_id_hex.clone()), conn)?; + let change_subaddress = account.change_subaddress(conn)?; + let change_public_address = change_subaddress.public_address()?; let fog_resolver = { let fog_uris = core::slice::from_ref(&change_public_address) .iter() - .chain(self.outlays.iter().map(|(receiver, _amount)| receiver)) + .chain(self.outlays.iter().map(|(receiver, _, _)| receiver)) .filter_map(|x| extract_fog_uri(x).transpose()) .collect::, _>>()?; (self.fog_resolver_factory)(&fog_uris) .map_err(WalletTransactionBuilderError::FogPubkeyResolver)? }; - let mut fully_validated_fog_pubkeys: HashMap = - HashMap::default(); - - for (public_address, _) in self.outlays.iter() { - let fog_pubkey = match fog_resolver.get_fog_pubkey(public_address) { - Ok(fog_pubkey) => Some(fog_pubkey), - Err(_) => None, - }; - - if let Some(fog_pubkey) = fog_pubkey { - let fs_fog_pubkey = FullServiceFullyValidatedFogPubkey::from(fog_pubkey); - let b58_public_address = b58_encode_public_address(public_address)?; - fully_validated_fog_pubkeys.insert(b58_public_address, fs_fog_pubkey); - } - } - - Ok(FullServiceFogResolver(fully_validated_fog_pubkeys)) + Ok(fog_resolver) } - pub fn build_unsigned(&self, conn: &Conn) -> Result { - if self.tombstone == 0 { - return Err(WalletTransactionBuilderError::TombstoneNotSet); - } - - // select inputs here - let view_only_inputs = self.select_view_only_txos(conn)?; - - // Get membership proofs for our inputs - let indexes = view_only_inputs - .iter() - .map(|utxo| { - let txo: TxOut = mc_util_serial::decode(&utxo.txo)?; - self.ledger_db.get_tx_out_index_by_hash(&txo.hash()) - }) - .collect::, mc_ledger_db::Error>>()?; - let proofs = self.ledger_db.get_tx_out_proof_of_memberships(&indexes)?; - - let inputs_and_proofs: Vec<(ViewOnlyTxo, TxOutMembershipProof)> = view_only_inputs - .into_iter() - .zip(proofs.into_iter()) - .collect(); - - let excluded_tx_out_indices: Vec = inputs_and_proofs - .iter() - .map(|(utxo, _membership_proof)| { - let txo: TxOut = mc_util_serial::decode(&utxo.txo)?; - self.ledger_db - .get_tx_out_index_by_hash(&txo.hash()) - .map_err(WalletTransactionBuilderError::LedgerDB) - }) - .collect::, WalletTransactionBuilderError>>()?; - - let rings = self.get_rings(inputs_and_proofs.len(), &excluded_tx_out_indices)?; - - if rings.len() != inputs_and_proofs.len() { - return Err(WalletTransactionBuilderError::RingSizeMismatch); - } - - if self.outlays.is_empty() { - return Err(WalletTransactionBuilderError::NoRecipient); - } - - // Unzip each vec of tuples into a tuple of vecs. - let mut rings_and_proofs: Vec<(Vec, Vec)> = rings - .into_iter() - .map(|tuples| tuples.into_iter().unzip()) - .collect(); - - let mut inputs_and_real_indices_and_subaddress_indices: Vec<(TxIn, u64, u64)> = Vec::new(); - - for (utxo, proof) in inputs_and_proofs.iter() { - let db_tx_out: TxOut = mc_util_serial::decode(&utxo.txo)?; - let (mut ring, mut membership_proofs) = rings_and_proofs - .pop() - .ok_or(WalletTransactionBuilderError::RingsAndProofsEmpty)?; - if ring.len() != membership_proofs.len() { - return Err(WalletTransactionBuilderError::RingSizeMismatch); - } - - // Add the input to the ring. - let position_opt = ring.iter().position(|txo| *txo == db_tx_out); - let real_index = match position_opt { - Some(position) => { - // The input is already present in the ring. - // This could happen if ring elements are sampled randomly from the - // ledger. - position - } - None => { - // The input is not already in the ring. - if ring.is_empty() { - // Append the input and its proof of membership. - ring.push(db_tx_out.clone()); - membership_proofs.push(proof.clone()); - } else { - // Replace the first element of the ring. - ring[0] = db_tx_out.clone(); - membership_proofs[0] = proof.clone(); - } - // The real input is always the first element. This is safe because - // TransactionBuilder sorts each ring. - 0 - } - }; + pub fn build( + &self, + memo: TransactionMemo, + conn: &Conn, + ) -> Result { + let mut rng = rand::thread_rng(); + let account = Account::get(&AccountID(self.account_id_hex.clone()), conn)?; - if ring.len() != membership_proofs.len() { - return Err(WalletTransactionBuilderError::RingSizeMismatch); - } + let view_account_key = account.view_account_key()?; + let view_private_key = account.view_private_key()?; + let reserved_subaddresses = ReservedSubaddresses::from(&view_account_key); - let tx_in = TxIn { - ring, - proofs: membership_proofs, - }; + let block_version = self.block_version.unwrap_or(BlockVersion::MAX); + let (fee, fee_token_id) = self.fee.unwrap_or((Mob::MINIMUM_FEE, Mob::ID)); + let fee_amount = Amount::new(fee, fee_token_id); + let fog_resolver = self.get_fog_resolver(conn)?; + let memo_builder = memo.memo_builder(account.account_key()?)?; + + let mut transaction_builder = TransactionBuilder::new_with_box( + block_version, + fee_amount, + fog_resolver, + memo_builder, + )?; - inputs_and_real_indices_and_subaddress_indices.push(( - tx_in, - real_index as u64, - utxo.subaddress_index.unwrap() as u64, - )); - } + transaction_builder.set_tombstone_block(self.tombstone); - let mut outlays_string: Vec<(String, u64)> = Vec::new(); - for (receiver, amount) in self.outlays.iter() { - let b58_address = b58_encode_public_address(receiver)?; - outlays_string.push((b58_address, *amount)); + if self.tombstone == 0 { + return Err(WalletTransactionBuilderError::TombstoneNotSet); } - Ok(UnsignedTx { - inputs_and_real_indices_and_subaddress_indices, - outlays: outlays_string, - fee: self.fee.unwrap_or(Mob::MINIMUM_FEE), - tombstone_block_index: self.tombstone, - block_version: self.block_version.unwrap_or(BlockVersion::MAX), - }) - } - - /// Consumes self - pub fn build(&self, conn: &Conn) -> Result { if self.inputs.is_empty() { return Err(WalletTransactionBuilderError::NoInputs); } - if self.tombstone == 0 { - return Err(WalletTransactionBuilderError::TombstoneNotSet); - } - - let account: Account = Account::get(&AccountID(self.account_id_hex.to_string()), conn)?; - let from_account_key: AccountKey = mc_util_serial::decode(&account.account_key)?; - - // Collect all required FogUris from public addresses, then pass to resolver - // factory - let fog_resolver = { - let change_address = - from_account_key.subaddress(account.change_subaddress_index as u64); - let fog_uris = core::slice::from_ref(&change_address) - .iter() - .chain(self.outlays.iter().map(|(receiver, _amount)| receiver)) - .filter_map(|x| extract_fog_uri(x).transpose()) - .collect::, _>>()?; - (self.fog_resolver_factory)(&fog_uris) - .map_err(WalletTransactionBuilderError::FogPubkeyResolver)? - }; - - // Create transaction builder. - let mut memo_builder = RTHMemoBuilder::default(); - memo_builder.set_sender_credential(SenderMemoCredential::from(&from_account_key)); - memo_builder.enable_destination_memo(); - let block_version = self.block_version.unwrap_or(BlockVersion::MAX); - let fee = Amount::new(self.fee.unwrap_or(Mob::MINIMUM_FEE), Mob::ID); - let mut transaction_builder = - TransactionBuilder::new(block_version, fee, fog_resolver, memo_builder)?; - // Get membership proofs for our inputs let indexes = self .inputs @@ -482,9 +316,14 @@ impl WalletTransactionBuilder { .map(|tuples| tuples.into_iter().unzip()) .collect(); - // Add inputs to the tx. + let mut unsigned_input_txos = Vec::new(); for (utxo, proof) in inputs_and_proofs.iter() { + let subaddress_index = utxo.subaddress_index.ok_or_else(|| { + WalletTransactionBuilderError::CannotUseOrphanedTxoAsInput(utxo.id.clone()) + })?; + let db_tx_out: TxOut = mc_util_serial::decode(&utxo.txo)?; + let (mut ring, mut membership_proofs) = rings_and_proofs .pop() .ok_or(WalletTransactionBuilderError::RingsAndProofsEmpty)?; @@ -494,11 +333,11 @@ impl WalletTransactionBuilder { // Add the input to the ring. let position_opt = ring.iter().position(|txo| *txo == db_tx_out); - let real_key_index = match position_opt { + let real_index = match position_opt { Some(position) => { // The input is already present in the ring. - // This could happen if ring elements are sampled randomly from the - // ledger. + // This could happen if ring elements are sampled + // randomly from the ledger. position } None => { @@ -512,8 +351,8 @@ impl WalletTransactionBuilder { ring[0] = db_tx_out.clone(); membership_proofs[0] = proof.clone(); } - // The real input is always the first element. This is safe because - // TransactionBuilder sorts each ring. + // The real input is always the first element. This is + // safe because TransactionBuilder sorts each ring. 0 } }; @@ -522,147 +361,99 @@ impl WalletTransactionBuilder { return Err(WalletTransactionBuilderError::RingSizeMismatch); } - let public_key = RistrettoPublic::try_from(&db_tx_out.public_key).unwrap(); + let onetime_key_derive_data = + OneTimeKeyDeriveData::SubaddressIndex(subaddress_index as u64); - let subaddress_index = if let Some(s) = utxo.subaddress_index { - s - } else { - return Err(WalletTransactionBuilderError::NullSubaddress( - utxo.txo_id_hex.to_string(), - )); + let unsigned_input_txo = UnsignedInputTxo { + tx_out: db_tx_out, + subaddress_index: subaddress_index as u64, + amount: Amount::new(utxo.value as u64, TokenId::from(utxo.token_id as u64)), }; + unsigned_input_txos.push(unsigned_input_txo); - let onetime_private_key = recover_onetime_private_key( - &public_key, - from_account_key.view_private_key(), - &from_account_key.subaddress_spend_private(subaddress_index as u64), - ); - - let key_image = KeyImage::from(&onetime_private_key); - log::debug!( - self.logger, - "Adding input: ring {:?}, utxo index {:?}, key image {:?}, pubkey {:?}", - ring, - real_key_index, - key_image, - public_key - ); - - transaction_builder.add_input(InputCredentials::new( + let input_credentials = InputCredentials::new( ring, membership_proofs, - real_key_index, - onetime_private_key, - *from_account_key.view_private_key(), - )?); - } - - // Add outputs to our destinations. - // Note that we make an assumption currently when logging submitted Txos that - // they were built with only one recip ient, and one change txo. - let mut total_value = 0; - let mut tx_out_to_outlay_index: HashMap = HashMap::default(); - let mut outlay_confirmation_numbers = Vec::default(); - let mut rng = rand::thread_rng(); - for (i, (recipient, out_value)) in self.outlays.iter().enumerate() { - let txo_context = transaction_builder.add_output(*out_value, recipient, &mut rng)?; - - tx_out_to_outlay_index.insert(txo_context.tx_out, i); - outlay_confirmation_numbers.push(txo_context.confirmation); + real_index, + onetime_key_derive_data, + view_private_key, + )?; - total_value += *out_value; + transaction_builder.add_input(input_credentials); } - // Figure out if we have change. - let input_value = inputs_and_proofs - .iter() - .fold(0, |acc, (utxo, _proof)| acc + utxo.value); - if (total_value + transaction_builder.get_fee().value) > input_value as u64 { - return Err(WalletTransactionBuilderError::InsufficientInputFunds( - format!( - "Total value required to send transaction {:?}, but only {:?} in inputs", - total_value + transaction_builder.get_fee().value, - input_value - ), - )); - } + let mut total_value_per_token = BTreeMap::new(); + total_value_per_token.insert(fee_token_id, fee); - let change = input_value as u64 - total_value - transaction_builder.get_fee().value; + let mut payload_txos = Vec::new(); + for (receiver, amount, token_id) in self.outlays.clone().into_iter() { + total_value_per_token + .entry(token_id) + .and_modify(|value| *value += amount) + .or_insert(amount); - let change_destination = ChangeDestination::from(&from_account_key); - transaction_builder.add_change_output(change, &change_destination, &mut rng)?; + let amount = Amount::new(amount, token_id); + let tx_out_context = transaction_builder.add_output(amount, &receiver, &mut rng)?; - // Set tombstone block. - transaction_builder.set_tombstone_block(self.tombstone); - - // Build tx. - let tx = transaction_builder.build(&mut rng)?; - - // Map each TxOut in the constructed transaction to its respective outlay. - let outlay_index_to_tx_out_index: HashMap = tx - .prefix - .outputs - .iter() - .enumerate() - .filter_map(|(tx_out_index, tx_out)| { - tx_out_to_outlay_index - .get(tx_out) - .map(|outlay_index| (*outlay_index, tx_out_index)) - }) - .collect(); + let payload_txo = OutputTxo { + tx_out: tx_out_context.tx_out, + recipient_public_address: receiver, + confirmation_number: tx_out_context.confirmation, + amount, + }; + payload_txos.push(payload_txo); + } - // Sanity check: All of our outlays should have a unique index in the map. - assert_eq!(outlay_index_to_tx_out_index.len(), self.outlays.len()); - let mut found_tx_out_indices: HashSet<&usize> = HashSet::default(); - for i in 0..self.outlays.len() { - let tx_out_index = outlay_index_to_tx_out_index - .get(&i) - .expect("index not in map"); - if !found_tx_out_indices.insert(tx_out_index) { - panic!("duplicate index {} found in map", tx_out_index); + let input_value_per_token = + inputs_and_proofs + .iter() + .fold(BTreeMap::new(), |mut acc, (utxo, _proof)| { + acc.entry(TokenId::from(utxo.token_id as u64)) + .and_modify(|value| *value += utxo.value as u64) + .or_insert(utxo.value as u64); + acc + }); + + let mut change_txos = Vec::new(); + for (token_id, input_value) in input_value_per_token { + let total_value = total_value_per_token.get(&token_id).ok_or_else(|| { + WalletTransactionBuilderError::MissingInputsForTokenId(token_id.to_string()) + })?; + + if *total_value > input_value { + return Err(WalletTransactionBuilderError::InsufficientInputFunds(format!( + "Total value required to send transaction {:?}, but only {:?} in inputs for token_id {:?}", + total_value, + input_value, + token_id.to_string(), + ))); } + + let change_value = input_value - *total_value; + let change_amount = Amount::new(change_value, token_id); + let tx_out_context = transaction_builder.add_change_output( + change_amount, + &reserved_subaddresses, + &mut rng, + )?; + + let change_txo = OutputTxo { + tx_out: tx_out_context.tx_out, + recipient_public_address: reserved_subaddresses.change_subaddress.clone(), + confirmation_number: tx_out_context.confirmation, + amount: change_amount, + }; + change_txos.push(change_txo); } - // Make the UnspentTxOut for each Txo - // FIXME: WS-27 - I would prefer to provide just the txo_id_hex per txout, but - // this at least preserves some interoperability between - // mobilecoind and wallet-service. However, this is - // pretty clunky and I would rather not expose a storage - // type from mobilecoind just to get around having to write a bunch of - // tedious json conversions. - // Return the TxProposal - let selected_utxos = inputs_and_proofs - .iter() - .map(|(utxo, _membership_proof)| { - let decoded_tx_out = mc_util_serial::decode(&utxo.txo).unwrap(); - let decoded_key_image = - mc_util_serial::decode(&utxo.key_image.clone().unwrap()).unwrap(); - - UnspentTxOut { - tx_out: decoded_tx_out, - subaddress_index: utxo.subaddress_index.unwrap() as u64, /* verified not null - * earlier */ - key_image: decoded_key_image, - value: utxo.value as u64, - attempted_spend_height: 0, // NOTE: these are null because not tracked here - attempted_spend_tombstone: 0, - token_id: *Mob::ID, - } - }) - .collect(); - Ok(TxProposal { - utxos: selected_utxos, - outlays: self - .outlays - .iter() - .map(|(recipient, value)| Outlay { - receiver: recipient.clone(), - value: *value, - }) - .collect::>(), - tx, - outlay_index_to_tx_out_index, - outlay_confirmation_numbers, + let unsigned_tx = + transaction_builder.build_unsigned::()?; + + Ok(UnsignedTxProposal { + unsigned_tx, + unsigned_input_txos, + payload_txos, + change_txos, }) } @@ -747,6 +538,7 @@ mod tests { WalletDbTestContext, MOB, }, }; + use mc_account_keys::AccountKey; use mc_common::logger::{test_with_logger, Logger}; use rand::{rngs::StdRng, SeedableRng}; @@ -773,21 +565,24 @@ mod tests { // Construct a transaction let conn = wallet_db.get_conn().unwrap(); let (recipient, mut builder) = - builder_for_random_recipient(&account_key, &ledger_db, &mut rng, &logger); + builder_for_random_recipient(&account_key, &ledger_db, &mut rng); // Send value specifically for your smallest Txo size. Should take 2 inputs // and also make change. let value = 11 * MOB; - builder.add_recipient(recipient.clone(), value).unwrap(); + builder + .add_recipient(recipient.clone(), value, Mob::ID) + .unwrap(); // Select the txos for the recipient - builder.select_txos(&conn, None, false).unwrap(); + builder.select_txos(&conn, None).unwrap(); builder.set_tombstone(0).unwrap(); - let proposal = builder.build(&conn).unwrap(); - assert_eq!(proposal.outlays.len(), 1); - assert_eq!(proposal.outlays[0].receiver, recipient); - assert_eq!(proposal.outlays[0].value, value); + let unsigned_tx_proposal = builder.build(TransactionMemo::RTH, &conn).unwrap(); + let proposal = unsigned_tx_proposal.sign(&account_key).unwrap(); + assert_eq!(proposal.payload_txos.len(), 1); + assert_eq!(proposal.payload_txos[0].recipient_public_address, recipient); + assert_eq!(proposal.payload_txos[0].amount.value, value); assert_eq!(proposal.tx.prefix.inputs.len(), 2); assert_eq!(proposal.tx.prefix.fee, Mob::MINIMUM_FEE); assert_eq!(proposal.tx.prefix.outputs.len(), 2); @@ -817,11 +612,13 @@ mod tests { // Check balance let unspent = Txo::list_unspent( - &AccountID::from(&account_key).to_string(), + Some(&AccountID::from(&account_key).to_string()), None, Some(0), None, None, + None, + None, &wallet_db.get_conn().unwrap(), ) .unwrap(); @@ -831,13 +628,15 @@ mod tests { // Now try to send a transaction with a value > u64::MAX let conn = wallet_db.get_conn().unwrap(); let (recipient, mut builder) = - builder_for_random_recipient(&account_key, &ledger_db, &mut rng, &logger); + builder_for_random_recipient(&account_key, &ledger_db, &mut rng); let value = u64::MAX; - builder.add_recipient(recipient.clone(), value).unwrap(); + builder + .add_recipient(recipient.clone(), value, Mob::ID) + .unwrap(); // Select the txos for the recipient - should error because > u64::MAX - match builder.select_txos(&conn, None, false) { + match builder.select_txos(&conn, None) { Ok(_) => panic!("Should not be allowed to construct outbound values > u64::MAX"), Err(WalletTransactionBuilderError::OutboundValueTooLarge) => {} Err(e) => panic!("Unexpected error {:?}", e), @@ -871,6 +670,8 @@ mod tests { None, None, None, + None, + None, Some(0), &wallet_db.get_conn().unwrap(), ) @@ -878,18 +679,16 @@ mod tests { let conn = wallet_db.get_conn().unwrap(); let (recipient, mut builder) = - builder_for_random_recipient(&account_key, &ledger_db, &mut rng, &logger); + builder_for_random_recipient(&account_key, &ledger_db, &mut rng); // Setting value to exactly the input will fail because you need funds for fee builder - .add_recipient(recipient.clone(), txos[0].value as u64) + .add_recipient(recipient.clone(), txos[0].value as u64, Mob::ID) .unwrap(); - builder - .set_txos(&conn, &vec![txos[0].txo_id_hex.clone()], false) - .unwrap(); + builder.set_txos(&conn, &vec![txos[0].id.clone()]).unwrap(); builder.set_tombstone(0).unwrap(); - match builder.build(&conn) { + match builder.build(TransactionMemo::RTH, &conn) { Ok(_) => { panic!("Should not be able to construct Tx with > inputs value as output value") } @@ -899,25 +698,25 @@ mod tests { // Now build, setting to multiple TXOs let (recipient, mut builder) = - builder_for_random_recipient(&account_key, &ledger_db, &mut rng, &logger); + builder_for_random_recipient(&account_key, &ledger_db, &mut rng); // Set value to just slightly more than what fits in the one TXO builder - .add_recipient(recipient.clone(), txos[0].value as u64 + 10) + .add_recipient(recipient.clone(), txos[0].value as u64 + 10, Mob::ID) .unwrap(); builder - .set_txos( - &conn, - &vec![txos[0].txo_id_hex.clone(), txos[1].txo_id_hex.clone()], - false, - ) + .set_txos(&conn, &vec![txos[0].id.clone(), txos[1].id.clone()]) .unwrap(); builder.set_tombstone(0).unwrap(); - let proposal = builder.build(&conn).unwrap(); - assert_eq!(proposal.outlays.len(), 1); - assert_eq!(proposal.outlays[0].receiver, recipient); - assert_eq!(proposal.outlays[0].value, txos[0].value as u64 + 10); + let unsigned_tx_proposal = builder.build(TransactionMemo::RTH, &conn).unwrap(); + let proposal = unsigned_tx_proposal.sign(&account_key).unwrap(); + assert_eq!(proposal.payload_txos.len(), 1); + assert_eq!(proposal.payload_txos[0].recipient_public_address, recipient); + assert_eq!( + proposal.payload_txos[0].amount.value, + txos[0].value as u64 + 10 + ); assert_eq!(proposal.tx.prefix.inputs.len(), 2); // need one more for fee assert_eq!(proposal.tx.prefix.fee, Mob::MINIMUM_FEE); assert_eq!(proposal.tx.prefix.outputs.len(), 2); // self and change @@ -946,13 +745,15 @@ mod tests { let conn = wallet_db.get_conn().unwrap(); let (recipient, mut builder) = - builder_for_random_recipient(&account_key, &ledger_db, &mut rng, &logger); + builder_for_random_recipient(&account_key, &ledger_db, &mut rng); // Setting value to exactly the input will fail because you need funds for fee - builder.add_recipient(recipient.clone(), 80 * MOB).unwrap(); + builder + .add_recipient(recipient.clone(), 80 * MOB, Mob::ID) + .unwrap(); // Test that selecting Txos with max_spendable < all our txo values fails - match builder.select_txos(&conn, Some(10), false) { + match builder.select_txos(&conn, Some(10)) { Ok(_) => panic!("Should not be able to construct tx when max_spendable < all txos"), Err(WalletTransactionBuilderError::WalletDb(WalletDbError::NoSpendableTxos)) => {} Err(e) => panic!("Unexpected error {:?}", e), @@ -960,7 +761,7 @@ mod tests { // We should be able to try again, with max_spendable at 70, but will not hit // our outlay target (80 * MOB) - match builder.select_txos(&conn, Some(70 * MOB), false) { + match builder.select_txos(&conn, Some(70 * MOB)) { Ok(_) => panic!("Should not be able to construct tx when max_spendable < all txos"), Err(WalletTransactionBuilderError::WalletDb( WalletDbError::InsufficientFundsUnderMaxSpendable(_), @@ -970,12 +771,13 @@ mod tests { // Now, we should succeed if we set max_spendable = 80 * MOB, because we will // pick up both 70 and 80 - builder.select_txos(&conn, Some(80 * MOB), false).unwrap(); + builder.select_txos(&conn, Some(80 * MOB)).unwrap(); builder.set_tombstone(0).unwrap(); - let proposal = builder.build(&conn).unwrap(); - assert_eq!(proposal.outlays.len(), 1); - assert_eq!(proposal.outlays[0].receiver, recipient); - assert_eq!(proposal.outlays[0].value, 80 * MOB); + let unsigned_tx_proposal = builder.build(TransactionMemo::RTH, &conn).unwrap(); + let proposal = unsigned_tx_proposal.sign(&account_key).unwrap(); + assert_eq!(proposal.payload_txos.len(), 1); + assert_eq!(proposal.payload_txos[0].recipient_public_address, recipient); + assert_eq!(proposal.payload_txos[0].amount.value, 80 * MOB); assert_eq!(proposal.tx.prefix.inputs.len(), 2); // uses both 70 and 80 assert_eq!(proposal.tx.prefix.fee, Mob::MINIMUM_FEE); assert_eq!(proposal.tx.prefix.outputs.len(), 2); // self and change @@ -1004,48 +806,56 @@ mod tests { ); let (recipient, mut builder) = - builder_for_random_recipient(&account_key, &ledger_db, &mut rng, &logger); + builder_for_random_recipient(&account_key, &ledger_db, &mut rng); - builder.add_recipient(recipient.clone(), 10 * MOB).unwrap(); - builder.select_txos(&conn, None, false).unwrap(); + builder + .add_recipient(recipient.clone(), 10 * MOB, Mob::ID) + .unwrap(); + builder.select_txos(&conn, None).unwrap(); // Sanity check that our ledger is the height we think it is assert_eq!(ledger_db.num_blocks().unwrap(), 13); // We must set tombstone block before building - match builder.build(&conn) { + match builder.build(TransactionMemo::RTH, &conn) { Ok(_) => panic!("Expected TombstoneNotSet error"), Err(WalletTransactionBuilderError::TombstoneNotSet) => {} Err(e) => panic!("Unexpected error {:?}", e), } let (recipient, mut builder) = - builder_for_random_recipient(&account_key, &ledger_db, &mut rng, &logger); + builder_for_random_recipient(&account_key, &ledger_db, &mut rng); - builder.add_recipient(recipient.clone(), 10 * MOB).unwrap(); - builder.select_txos(&conn, None, false).unwrap(); + builder + .add_recipient(recipient.clone(), 10 * MOB, Mob::ID) + .unwrap(); + builder.select_txos(&conn, None).unwrap(); // Set to default builder.set_tombstone(0).unwrap(); // Not setting the tombstone results in tombstone = 0. This is an acceptable // value, - let proposal = builder.build(&conn).unwrap(); + let unsigned_tx_proposal = builder.build(TransactionMemo::RTH, &conn).unwrap(); + let proposal = unsigned_tx_proposal.sign(&account_key).unwrap(); assert_eq!(proposal.tx.prefix.tombstone_block, 23); // Build a transaction and explicitly set tombstone let (recipient, mut builder) = - builder_for_random_recipient(&account_key, &ledger_db, &mut rng, &logger); + builder_for_random_recipient(&account_key, &ledger_db, &mut rng); - builder.add_recipient(recipient.clone(), 10 * MOB).unwrap(); - builder.select_txos(&conn, None, false).unwrap(); + builder + .add_recipient(recipient.clone(), 10 * MOB, Mob::ID) + .unwrap(); + builder.select_txos(&conn, None).unwrap(); // Set to default builder.set_tombstone(20).unwrap(); // Not setting the tombstone results in tombstone = 0. This is an acceptable // value, - let proposal = builder.build(&conn).unwrap(); + let unsigned_tx_proposal = builder.build(TransactionMemo::RTH, &conn).unwrap(); + let proposal = unsigned_tx_proposal.sign(&account_key).unwrap(); assert_eq!(proposal.tx.prefix.tombstone_block, 20); } @@ -1072,41 +882,49 @@ mod tests { let conn = wallet_db.get_conn().unwrap(); let (recipient, mut builder) = - builder_for_random_recipient(&account_key, &ledger_db, &mut rng, &logger); + builder_for_random_recipient(&account_key, &ledger_db, &mut rng); - builder.add_recipient(recipient.clone(), 10 * MOB).unwrap(); - builder.select_txos(&conn, None, false).unwrap(); + builder + .add_recipient(recipient.clone(), 10 * MOB, Mob::ID) + .unwrap(); + builder.select_txos(&conn, None).unwrap(); builder.set_tombstone(0).unwrap(); // Verify that not setting fee results in default fee - let proposal = builder.build(&conn).unwrap(); + let unsigned_tx_proposal = builder.build(TransactionMemo::RTH, &conn).unwrap(); + let proposal = unsigned_tx_proposal.sign(&account_key).unwrap(); assert_eq!(proposal.tx.prefix.fee, Mob::MINIMUM_FEE); // You cannot set fee to 0 let (recipient, mut builder) = - builder_for_random_recipient(&account_key, &ledger_db, &mut rng, &logger); + builder_for_random_recipient(&account_key, &ledger_db, &mut rng); - builder.add_recipient(recipient.clone(), 10 * MOB).unwrap(); - builder.select_txos(&conn, None, false).unwrap(); + builder + .add_recipient(recipient.clone(), 10 * MOB, Mob::ID) + .unwrap(); + builder.select_txos(&conn, None).unwrap(); builder.set_tombstone(0).unwrap(); - match builder.set_fee(0) { + match builder.set_fee(0, Mob::ID) { Ok(_) => panic!("Should not be able to set fee to 0"), Err(WalletTransactionBuilderError::InsufficientFee(_)) => {} Err(e) => panic!("Unexpected error {:?}", e), } // Verify that not setting fee results in default fee - let proposal = builder.build(&conn).unwrap(); + let unsigned_tx_proposal = builder.build(TransactionMemo::RTH, &conn).unwrap(); + let proposal = unsigned_tx_proposal.sign(&account_key).unwrap(); assert_eq!(proposal.tx.prefix.fee, Mob::MINIMUM_FEE); // Setting fee less than minimum fee should fail let (recipient, mut builder) = - builder_for_random_recipient(&account_key, &ledger_db, &mut rng, &logger); + builder_for_random_recipient(&account_key, &ledger_db, &mut rng); - builder.add_recipient(recipient.clone(), 10 * MOB).unwrap(); - builder.select_txos(&conn, None, false).unwrap(); + builder + .add_recipient(recipient.clone(), 10 * MOB, Mob::ID) + .unwrap(); + builder.select_txos(&conn, None).unwrap(); builder.set_tombstone(0).unwrap(); - match builder.set_fee(0) { + match builder.set_fee(0, Mob::ID) { Ok(_) => panic!("Should not be able to set fee to 0"), Err(WalletTransactionBuilderError::InsufficientFee(_)) => {} Err(e) => panic!("Unexpected error {:?}", e), @@ -1114,13 +932,16 @@ mod tests { // Setting fee greater than MINIMUM_FEE works let (recipient, mut builder) = - builder_for_random_recipient(&account_key, &ledger_db, &mut rng, &logger); + builder_for_random_recipient(&account_key, &ledger_db, &mut rng); - builder.add_recipient(recipient.clone(), 10 * MOB).unwrap(); - builder.select_txos(&conn, None, false).unwrap(); + builder + .add_recipient(recipient.clone(), 10 * MOB, Mob::ID) + .unwrap(); + builder.select_txos(&conn, None).unwrap(); builder.set_tombstone(0).unwrap(); - builder.set_fee(Mob::MINIMUM_FEE * 10).unwrap(); - let proposal = builder.build(&conn).unwrap(); + builder.set_fee(Mob::MINIMUM_FEE * 10, Mob::ID).unwrap(); + let unsigned_tx_proposal = builder.build(TransactionMemo::RTH, &conn).unwrap(); + let proposal = unsigned_tx_proposal.sign(&account_key).unwrap(); assert_eq!(proposal.tx.prefix.fee, Mob::MINIMUM_FEE * 10); } @@ -1147,20 +968,24 @@ mod tests { let conn = wallet_db.get_conn().unwrap(); let (recipient, mut builder) = - builder_for_random_recipient(&account_key, &ledger_db, &mut rng, &logger); + builder_for_random_recipient(&account_key, &ledger_db, &mut rng); // Set value to consume the whole TXO and not produce change let value = 70 * MOB - Mob::MINIMUM_FEE; - builder.add_recipient(recipient.clone(), value).unwrap(); - builder.select_txos(&conn, None, false).unwrap(); + builder + .add_recipient(recipient.clone(), value, Mob::ID) + .unwrap(); + builder.select_txos(&conn, None).unwrap(); builder.set_tombstone(0).unwrap(); // Verify that not setting fee results in default fee - let proposal = builder.build(&conn).unwrap(); + let unsigned_tx_proposal = builder.build(TransactionMemo::RTH, &conn).unwrap(); + let proposal = unsigned_tx_proposal.sign(&account_key).unwrap(); + assert_eq!(proposal.tx.prefix.fee, Mob::MINIMUM_FEE); - assert_eq!(proposal.outlays.len(), 1); - assert_eq!(proposal.outlays[0].receiver, recipient); - assert_eq!(proposal.outlays[0].value, value); + assert_eq!(proposal.payload_txos.len(), 1); + assert_eq!(proposal.payload_txos[0].recipient_public_address, recipient); + assert_eq!(proposal.payload_txos[0].amount.value, value); assert_eq!(proposal.tx.prefix.inputs.len(), 1); // uses just one input assert_eq!(proposal.tx.prefix.outputs.len(), 2); // two outputs to // self @@ -1190,28 +1015,37 @@ mod tests { let conn = wallet_db.get_conn().unwrap(); let (recipient, mut builder) = - builder_for_random_recipient(&account_key, &ledger_db, &mut rng, &logger); + builder_for_random_recipient(&account_key, &ledger_db, &mut rng); - builder.add_recipient(recipient.clone(), 10 * MOB).unwrap(); - builder.add_recipient(recipient.clone(), 20 * MOB).unwrap(); - builder.add_recipient(recipient.clone(), 30 * MOB).unwrap(); - builder.add_recipient(recipient.clone(), 40 * MOB).unwrap(); + builder + .add_recipient(recipient.clone(), 10 * MOB, Mob::ID) + .unwrap(); + builder + .add_recipient(recipient.clone(), 20 * MOB, Mob::ID) + .unwrap(); + builder + .add_recipient(recipient.clone(), 30 * MOB, Mob::ID) + .unwrap(); + builder + .add_recipient(recipient.clone(), 40 * MOB, Mob::ID) + .unwrap(); - builder.select_txos(&conn, None, false).unwrap(); + builder.select_txos(&conn, None).unwrap(); builder.set_tombstone(0).unwrap(); - // Verify that not setting fee results in default fee - let proposal = builder.build(&conn).unwrap(); + let unsigned_tx_proposal = builder.build(TransactionMemo::RTH, &conn).unwrap(); + let proposal = unsigned_tx_proposal.sign(&account_key).unwrap(); + assert_eq!(proposal.tx.prefix.fee, Mob::MINIMUM_FEE); - assert_eq!(proposal.outlays.len(), 4); - assert_eq!(proposal.outlays[0].receiver, recipient); - assert_eq!(proposal.outlays[0].value, 10 * MOB); - assert_eq!(proposal.outlays[1].receiver, recipient); - assert_eq!(proposal.outlays[1].value, 20 * MOB); - assert_eq!(proposal.outlays[2].receiver, recipient); - assert_eq!(proposal.outlays[2].value, 30 * MOB); - assert_eq!(proposal.outlays[3].receiver, recipient); - assert_eq!(proposal.outlays[3].value, 40 * MOB); + assert_eq!(proposal.payload_txos.len(), 4); + assert_eq!(proposal.payload_txos[0].recipient_public_address, recipient); + assert_eq!(proposal.payload_txos[0].amount.value, 10 * MOB); + assert_eq!(proposal.payload_txos[1].recipient_public_address, recipient); + assert_eq!(proposal.payload_txos[1].amount.value, 20 * MOB); + assert_eq!(proposal.payload_txos[2].recipient_public_address, recipient); + assert_eq!(proposal.payload_txos[2].amount.value, 30 * MOB); + assert_eq!(proposal.payload_txos[3].recipient_public_address, recipient); + assert_eq!(proposal.payload_txos[3].amount.value, 40 * MOB); assert_eq!(proposal.tx.prefix.inputs.len(), 2); assert_eq!(proposal.tx.prefix.outputs.len(), 5); // outlays + change } @@ -1243,19 +1077,19 @@ mod tests { ); let (recipient, mut builder) = - builder_for_random_recipient(&account_key, &ledger_db, &mut rng, &logger); + builder_for_random_recipient(&account_key, &ledger_db, &mut rng); builder - .add_recipient(recipient.clone(), 7_000_000 * MOB) + .add_recipient(recipient.clone(), 7_000_000 * MOB, Mob::ID) .unwrap(); builder - .add_recipient(recipient.clone(), 7_000_000 * MOB) + .add_recipient(recipient.clone(), 7_000_000 * MOB, Mob::ID) .unwrap(); builder - .add_recipient(recipient.clone(), 7_000_000 * MOB) + .add_recipient(recipient.clone(), 7_000_000 * MOB, Mob::ID) .unwrap(); - match builder.select_txos(&wallet_db.get_conn().unwrap(), None, false) { + match builder.select_txos(&wallet_db.get_conn().unwrap(), None) { Ok(_) => panic!("Should not be able to select txos with > u64::MAX output value"), Err(WalletTransactionBuilderError::OutboundValueTooLarge) => {} Err(e) => panic!("Unexpected error {:?}", e), @@ -1284,14 +1118,16 @@ mod tests { ); let (recipient, mut builder) = - builder_for_random_recipient(&account_key, &ledger_db, &mut rng, &logger); + builder_for_random_recipient(&account_key, &ledger_db, &mut rng); - builder.add_recipient(recipient.clone(), 10 * MOB).unwrap(); + builder + .add_recipient(recipient.clone(), 10 * MOB, Mob::ID) + .unwrap(); // Create a new recipient let second_recipient = AccountKey::random(&mut rng).subaddress(0); builder - .add_recipient(second_recipient.clone(), 40 * MOB) + .add_recipient(second_recipient.clone(), 40 * MOB, Mob::ID) .unwrap(); } } diff --git a/full-service/src/service/transaction_log.rs b/full-service/src/service/transaction_log.rs index e99c3e164..10c25174a 100644 --- a/full-service/src/service/transaction_log.rs +++ b/full-service/src/service/transaction_log.rs @@ -4,9 +4,8 @@ use crate::{ db::{ - account::AccountID, models::TransactionLog, - transaction_log::{AssociatedTxos, TransactionLogModel}, + transaction_log::{AssociatedTxos, TransactionID, TransactionLogModel, ValueMap}, WalletDbError, }, error::WalletServiceError, @@ -45,29 +44,29 @@ pub trait TransactionLogService { /// List all transactions associated with the given Account ID. fn list_transaction_logs( &self, - account_id: &AccountID, + account_id: Option, offset: Option, limit: Option, min_block_index: Option, max_block_index: Option, - ) -> Result, WalletServiceError>; + ) -> Result, WalletServiceError>; /// Get a specific transaction log. fn get_transaction_log( &self, transaction_id_hex: &str, - ) -> Result<(TransactionLog, AssociatedTxos), TransactionLogServiceError>; + ) -> Result<(TransactionLog, AssociatedTxos, ValueMap), TransactionLogServiceError>; /// Get all transaction logs for a given block. fn get_all_transaction_logs_for_block( &self, block_index: u64, - ) -> Result, WalletServiceError>; + ) -> Result, WalletServiceError>; - /// Get all transaction logs ordered by finalized_block_index. + /// Get all transaction logs ordered& by finalized_block_index. fn get_all_transaction_logs_ordered_by_block( &self, - ) -> Result, WalletServiceError>; + ) -> Result, WalletServiceError>; } impl TransactionLogService for WalletService @@ -77,15 +76,15 @@ where { fn list_transaction_logs( &self, - account_id: &AccountID, + account_id: Option, offset: Option, limit: Option, min_block_index: Option, max_block_index: Option, - ) -> Result, WalletServiceError> { - let conn = &self.wallet_db.get_conn()?; + ) -> Result, WalletServiceError> { + let conn = &self.get_conn()?; Ok(TransactionLog::list_all( - &account_id.to_string(), + account_id, offset, limit, min_block_index, @@ -97,25 +96,28 @@ where fn get_transaction_log( &self, transaction_id_hex: &str, - ) -> Result<(TransactionLog, AssociatedTxos), TransactionLogServiceError> { - let conn = self.wallet_db.get_conn()?; - let transaction_log = TransactionLog::get(transaction_id_hex, &conn)?; + ) -> Result<(TransactionLog, AssociatedTxos, ValueMap), TransactionLogServiceError> { + let conn = self.get_conn()?; + let transaction_log = + TransactionLog::get(&TransactionID(transaction_id_hex.to_string()), &conn)?; let associated = transaction_log.get_associated_txos(&conn)?; + let value_map = transaction_log.value_map(&conn)?; - Ok((transaction_log, associated)) + Ok((transaction_log, associated, value_map)) } fn get_all_transaction_logs_for_block( &self, block_index: u64, - ) -> Result, WalletServiceError> { - let conn = self.wallet_db.get_conn()?; + ) -> Result, WalletServiceError> { + let conn = self.get_conn()?; let transaction_logs = TransactionLog::get_all_for_block_index(block_index, &conn)?; - let mut res: Vec<(TransactionLog, AssociatedTxos)> = Vec::new(); + let mut res: Vec<(TransactionLog, AssociatedTxos, ValueMap)> = Vec::new(); for transaction_log in transaction_logs { res.push(( transaction_log.clone(), transaction_log.get_associated_txos(&conn)?, + transaction_log.value_map(&conn)?, )); } Ok(res) @@ -123,14 +125,15 @@ where fn get_all_transaction_logs_ordered_by_block( &self, - ) -> Result, WalletServiceError> { - let conn = self.wallet_db.get_conn()?; + ) -> Result, WalletServiceError> { + let conn = self.get_conn()?; let transaction_logs = TransactionLog::get_all_ordered_by_block_index(&conn)?; - let mut res: Vec<(TransactionLog, AssociatedTxos)> = Vec::new(); + let mut res: Vec<(TransactionLog, AssociatedTxos, ValueMap)> = Vec::new(); for transaction_log in transaction_logs { res.push(( transaction_log.clone(), transaction_log.get_associated_txos(&conn)?, + transaction_log.value_map(&conn)?, )); } Ok(res) @@ -141,16 +144,22 @@ where mod tests { use crate::{ db::account::AccountID, - service::{account::AccountService, transaction_log::TransactionLogService}, + json_rpc::v2::models::amount::Amount, + service::{ + account::AccountService, + address::AddressService, + transaction::{TransactionMemo, TransactionService}, + transaction_log::TransactionLogService, + }, test_utils::{ - add_block_to_ledger_db, get_test_ledger, manually_sync_account, setup_wallet_service, - MOB, + add_block_from_transaction_log, add_block_to_ledger_db, get_test_ledger, + manually_sync_account, setup_wallet_service, MOB, }, }; use mc_account_keys::{AccountKey, PublicAddress}; use mc_common::logger::{test_with_logger, Logger}; use mc_crypto_rand::rand_core::RngCore; - use mc_transaction_core::ring_signature::KeyImage; + use mc_transaction_core::{ring_signature::KeyImage, tokens::Mob, Token}; use rand::{rngs::StdRng, SeedableRng}; #[test_with_logger] @@ -174,81 +183,105 @@ mod tests { let alice_account_key: AccountKey = mc_util_serial::decode(&alice.account_key).unwrap(); let alice_account_id = AccountID::from(&alice_account_key); - let alice_public_address = alice_account_key.subaddress(alice.main_subaddress_index as u64); + let alice_public_address = alice_account_key.default_subaddress(); let tx_logs = service - .list_transaction_logs(&alice_account_id, None, None, None, None) + .list_transaction_logs(Some(alice_account_id.to_string()), None, None, None, None) .unwrap(); assert_eq!(0, tx_logs.len()); - // block_index 12 - add_block_to_ledger_db( - &mut ledger_db, - &vec![alice_public_address.clone()], - 100 * MOB, - &vec![KeyImage::from(rng.next_u64())], - &mut rng, - ); - - // block_index 13 - add_block_to_ledger_db( - &mut ledger_db, - &vec![alice_public_address.clone()], - 100 * MOB, - &vec![KeyImage::from(rng.next_u64())], - &mut rng, - ); - - // block_index 14 - add_block_to_ledger_db( - &mut ledger_db, - &vec![alice_public_address.clone()], - 100 * MOB, - &vec![KeyImage::from(rng.next_u64())], - &mut rng, - ); + // add 5 txos to alices account + for _ in 0..5 { + add_block_to_ledger_db( + &mut ledger_db, + &vec![alice_public_address.clone()], + 100 * MOB, + &vec![KeyImage::from(rng.next_u64())], + &mut rng, + ); + } - // block_index 15 - add_block_to_ledger_db( - &mut ledger_db, - &vec![alice_public_address.clone()], - 100 * MOB, - &vec![KeyImage::from(rng.next_u64())], - &mut rng, + manually_sync_account( + &ledger_db, + &service.wallet_db.as_ref().unwrap(), + &alice_account_id, + &logger, ); - // block_index 16 - add_block_to_ledger_db( - &mut ledger_db, - &vec![alice_public_address.clone()], - 100 * MOB, - &vec![KeyImage::from(rng.next_u64())], - &mut rng, - ); + let address = service + .assign_address_for_account(&alice_account_id, None) + .unwrap(); - manually_sync_account(&ledger_db, &service.wallet_db, &alice_account_id, &logger); + for _ in 0..5 { + let (transaction_log, _, _, _) = service + .build_sign_and_submit_transaction( + &alice_account_id.to_string(), + &[( + address.public_address_b58.clone(), + Amount::new(50 * MOB, Mob::ID), + )], + None, + None, + None, + None, + None, + None, + TransactionMemo::RTH, + ) + .unwrap(); + + { + let conn = service.get_conn().unwrap(); + add_block_from_transaction_log(&mut ledger_db, &conn, &transaction_log, &mut rng); + } + + manually_sync_account( + &ledger_db, + &service.wallet_db.as_ref().unwrap(), + &alice_account_id, + &logger, + ); + } let tx_logs = service - .list_transaction_logs(&alice_account_id, None, None, None, None) + .list_transaction_logs(Some(alice_account_id.to_string()), None, None, None, None) .unwrap(); assert_eq!(5, tx_logs.len()); let tx_logs = service - .list_transaction_logs(&alice_account_id, None, None, Some(15), None) + .list_transaction_logs( + Some(alice_account_id.to_string()), + None, + None, + Some(20), + None, + ) .unwrap(); assert_eq!(2, tx_logs.len()); let tx_logs = service - .list_transaction_logs(&alice_account_id, None, None, None, Some(13)) + .list_transaction_logs( + Some(alice_account_id.to_string()), + None, + None, + None, + Some(18), + ) .unwrap(); assert_eq!(2, tx_logs.len()); let tx_logs = service - .list_transaction_logs(&alice_account_id, None, None, Some(13), Some(15)) + .list_transaction_logs( + Some(alice_account_id.to_string()), + None, + None, + Some(18), + Some(20), + ) .unwrap(); assert_eq!(3, tx_logs.len()); diff --git a/full-service/src/service/txo.rs b/full-service/src/service/txo.rs index ac4d85b17..5d928844e 100644 --- a/full-service/src/service/txo.rs +++ b/full-service/src/service/txo.rs @@ -4,19 +4,24 @@ use crate::{ db::{ - account::AccountID, + account::{AccountID, AccountModel}, assigned_subaddress::AssignedSubaddressModel, - models::{AssignedSubaddress, Txo}, - txo::{TxoID, TxoModel}, + models::{Account, AssignedSubaddress, Txo}, + txo::{TxoID, TxoModel, TxoStatus}, WalletDbError, }, - service::transaction::{TransactionService, TransactionServiceError}, + error::WalletTransactionBuilderError, + json_rpc::v2::models::amount::Amount, + service::{ + models::tx_proposal::TxProposal, + transaction::{TransactionMemo, TransactionService, TransactionServiceError}, + }, WalletService, }; use displaydoc::Display; +use mc_account_keys::AccountKey; use mc_connection::{BlockchainConnection, UserTxConnection}; use mc_fog_report_validation::FogPubkeyResolver; -use mc_mobilecoind::payments::TxProposal; /// Errors for the Txo Service. #[derive(Display, Debug)] @@ -42,6 +47,24 @@ pub enum TxoServiceError { /// Txo Not Spendable TxoNotSpendable(String), + + /// Must query with either an account ID or a subaddress b58. + InvalidQuery(String), + + /// Error decoding + Decode(mc_util_serial::DecodeError), + + /// Wallet Transaction Builder Error: {0} + WalletTransactionBuilder(WalletTransactionBuilderError), + + /// Key Error + Key(mc_crypto_keys::KeyError), + + /// From String Error: {0} + From(String), + + /// TxBuilderError: {0} + TxBuilder(mc_transaction_std::TxBuilderError), } impl From for TxoServiceError { @@ -68,23 +91,55 @@ impl From for TxoServiceError { } } +impl From for TxoServiceError { + fn from(src: mc_util_serial::DecodeError) -> Self { + Self::Decode(src) + } +} + +impl From for TxoServiceError { + fn from(src: WalletTransactionBuilderError) -> Self { + Self::WalletTransactionBuilder(src) + } +} + +impl From for TxoServiceError { + fn from(src: mc_crypto_keys::KeyError) -> Self { + Self::Key(src) + } +} + +impl From for TxoServiceError { + fn from(src: String) -> Self { + Self::From(src) + } +} + +impl From for TxoServiceError { + fn from(src: mc_transaction_std::TxBuilderError) -> Self { + Self::TxBuilder(src) + } +} + /// Trait defining the ways in which the wallet can interact with and manage /// Txos. pub trait TxoService { /// List the Txos for a given account in the wallet. + #[allow(clippy::too_many_arguments)] fn list_txos( &self, - account_id: &AccountID, - status: Option, - limit: Option, + account_id: Option, + address: Option, + status: Option, + token_id: Option, + min_received_block_index: Option, + max_received_block_index: Option, offset: Option, - ) -> Result, TxoServiceError>; - - /// list all spent txos - fn list_spent_txos(&self, account_id: &AccountID) -> Result, TxoServiceError>; + limit: Option, + ) -> Result, TxoServiceError>; /// Get a Txo from the wallet. - fn get_txo(&self, txo_id: &TxoID) -> Result; + fn get_txo(&self, txo_id: &TxoID) -> Result<(Txo, TxoStatus), TxoServiceError>; /// Split a Txo fn split_txo( @@ -92,12 +147,10 @@ pub trait TxoService { txo_id: &TxoID, output_values: &[String], subaddress_index: Option, - fee: Option, + fee_value: Option, + fee_token_id: Option, tombstone_block: Option, ) -> Result; - - /// List the Txos for a given address for an account in the wallet. - fn get_all_txos_for_address(&self, address: &str) -> Result, TxoServiceError>; } impl TxoService for WalletService @@ -107,37 +160,69 @@ where { fn list_txos( &self, - account_id: &AccountID, - status: Option, - limit: Option, + account_id: Option, + address: Option, + status: Option, + token_id: Option, + min_received_block_index: Option, + max_received_block_index: Option, offset: Option, - ) -> Result, TxoServiceError> { - let conn = self.wallet_db.get_conn()?; - Ok(Txo::list_for_account( - &account_id.to_string(), - status, - limit, - offset, - Some(0), - &conn, - )?) - } + limit: Option, + ) -> Result, TxoServiceError> { + let conn = &self.get_conn()?; + + let txos; + + if let Some(address) = address { + txos = Txo::list_for_address( + &address, + status, + min_received_block_index, + max_received_block_index, + offset, + limit, + token_id, + conn, + )?; + } else if let Some(account_id) = account_id { + txos = Txo::list_for_account( + &account_id, + status, + min_received_block_index, + max_received_block_index, + offset, + limit, + token_id, + conn, + )?; + } else { + txos = Txo::list( + status, + min_received_block_index, + max_received_block_index, + offset, + limit, + token_id, + conn, + )?; + } - fn list_spent_txos(&self, account_id: &AccountID) -> Result, TxoServiceError> { - let conn = self.wallet_db.get_conn()?; - Ok(Txo::list_spent( - &account_id.to_string(), - None, - Some(0), - None, - None, - &conn, - )?) + let txos_and_statuses = txos + .into_iter() + .map(|txo| { + let status = txo.status(conn)?; + Ok((txo, status)) + }) + .collect::, TxoServiceError>>()?; + + Ok(txos_and_statuses) } - fn get_txo(&self, txo_id: &TxoID) -> Result { - let conn = self.wallet_db.get_conn()?; - Ok(Txo::get(&txo_id.to_string(), &conn)?) + fn get_txo(&self, txo_id: &TxoID) -> Result<(Txo, TxoStatus), TxoServiceError> { + let conn = self.get_conn()?; + let txo = Txo::get(&txo_id.to_string(), &conn)?; + let status = txo.status(&conn)?; + Ok((txo, status)) } fn split_txo( @@ -145,17 +230,18 @@ where txo_id: &TxoID, output_values: &[String], subaddress_index: Option, - fee: Option, + fee_value: Option, + fee_token_id: Option, tombstone_block: Option, ) -> Result { use crate::service::txo::TxoServiceError::TxoNotSpendableByAnyAccount; - let conn = self.wallet_db.get_conn()?; + let conn = self.get_conn()?; let txo_details = Txo::get(&txo_id.to_string(), &conn)?; let account_id_hex = txo_details - .received_account_id_hex - .ok_or(TxoNotSpendableByAnyAccount(txo_details.txo_id_hex))?; + .account_id + .ok_or(TxoNotSpendableByAnyAccount(txo_details.id))?; let address_to_split_into: AssignedSubaddress = AssignedSubaddress::get_for_account_by_index( @@ -164,28 +250,31 @@ where &conn, )?; - let mut addresses_and_values = Vec::new(); + let mut addresses_and_amounts = Vec::new(); for output_value in output_values.iter() { - addresses_and_values.push(( - address_to_split_into.assigned_subaddress_b58.clone(), - output_value.to_string(), + addresses_and_amounts.push(( + address_to_split_into.public_address_b58.clone(), + Amount { + value: output_value.to_string(), + token_id: txo_details.token_id.to_string(), + }, )) } - Ok(self.build_transaction( + let unsigned_transaction = self.build_transaction( &account_id_hex, - &addresses_and_values, + &addresses_and_amounts, Some(&[txo_id.to_string()].to_vec()), - fee, + fee_value, + fee_token_id, tombstone_block, None, - None, - )?) - } + TransactionMemo::RTH, + )?; - fn get_all_txos_for_address(&self, address: &str) -> Result, TxoServiceError> { - let conn = self.wallet_db.get_conn()?; - Ok(Txo::list_for_address(address, Some(0), &conn)?) + let account = Account::get(&AccountID(account_id_hex), &conn)?; + let account_key: AccountKey = mc_util_serial::decode(&account.account_key)?; + Ok(unsigned_transaction.sign(&account_key)?) } } @@ -193,6 +282,7 @@ where mod tests { use super::*; use crate::{ + db::account::AccountID, service::{ account::AccountService, balance::BalanceService, transaction::TransactionService, }, @@ -203,14 +293,10 @@ mod tests { util::b58::b58_encode_public_address, }; use mc_account_keys::{AccountKey, PublicAddress}; - use mc_common::{ - logger::{test_with_logger, Logger}, - HashSet, - }; + use mc_common::logger::{test_with_logger, Logger}; use mc_crypto_rand::RngCore; use mc_transaction_core::{ring_signature::KeyImage, tokens::Mob, Token}; use rand::{rngs::StdRng, SeedableRng}; - use std::iter::FromIterator; #[test_with_logger] fn test_txo_lifecycle(logger: Logger) { @@ -233,7 +319,7 @@ mod tests { // Add a block with a txo for this address let alice_account_key: AccountKey = mc_util_serial::decode(&alice.account_key).unwrap(); let alice_account_id = AccountID::from(&alice_account_key); - let alice_public_address = alice_account_key.subaddress(alice.main_subaddress_index as u64); + let alice_public_address = alice_account_key.default_subaddress(); add_block_to_ledger_db( &mut ledger_db, &vec![alice_public_address.clone()], @@ -242,16 +328,31 @@ mod tests { &mut rng, ); - manually_sync_account(&ledger_db, &service.wallet_db, &alice_account_id, &logger); + manually_sync_account( + &ledger_db, + &service.wallet_db.as_ref().unwrap(), + &alice_account_id, + &logger, + ); // Verify balance for Alice let balance = service.get_balance_for_account(&alice_account_id).unwrap(); + let balance_pmob = balance.get(&Mob::ID).unwrap(); - assert_eq!(balance.unspent, 100 * MOB as u128); + assert_eq!(balance_pmob.unspent, 100 * MOB as u128); // Verify that we have 1 txo let txos = service - .list_txos(&alice_account_id, None, None, None) + .list_txos( + Some(alice_account_id.to_string()), + None, + None, + None, + None, + None, + None, + None, + ) .unwrap(); assert_eq!(txos.len(), 1); @@ -268,70 +369,49 @@ mod tests { // Construct a new transaction to Bob let bob_account_key: AccountKey = mc_util_serial::decode(&bob.account_key).unwrap(); let tx_proposal = service - .build_transaction( - &alice.account_id_hex, + .build_and_sign_transaction( + &alice.id, &vec![( - b58_encode_public_address( - &bob_account_key.subaddress(bob.main_subaddress_index as u64), - ) - .unwrap(), - "42000000000000".to_string(), + b58_encode_public_address(&bob_account_key.default_subaddress()).unwrap(), + Amount::new(42 * MOB, Mob::ID), )], None, None, None, None, None, + TransactionMemo::RTH, ) .unwrap(); let _submitted = service - .submit_transaction(tx_proposal, None, Some(alice.account_id_hex.clone())) + .submit_transaction(&tx_proposal, None, Some(alice.id.clone())) .unwrap(); - // We should now have 3 txos - one pending, two minted (one of which will be - // change) - let txos = service - .list_txos(&AccountID(alice.account_id_hex.clone()), None, None, None) + let pending: Vec<(Txo, TxoStatus)> = service + .list_txos( + Some(alice.id.clone()), + None, + Some(TxoStatus::Pending), + None, + None, + None, + None, + None, + ) .unwrap(); - assert_eq!(txos.len(), 3); - assert_eq!( - txos[0].received_account_id_hex, - Some(alice.account_id_hex.clone()) - ); - assert_eq!( - txos[1].minted_account_id_hex, - Some(alice.account_id_hex.clone()) - ); - assert_eq!( - txos[2].minted_account_id_hex, - Some(alice.account_id_hex.clone()) - ); - let pending: Vec = txos - .iter() - .cloned() - .filter(|txo| txo.received_account_id_hex == Some(alice.account_id_hex.clone())) - .collect(); assert_eq!(pending.len(), 1); - assert_eq!(pending[0].value, 100000000000000); - - let minted: Vec = txos - .iter() - .cloned() - .filter(|txo| txo.minted_account_id_hex.is_some()) - .collect(); - assert_eq!(minted.len(), 2); - let minted_value_set = HashSet::from_iter(minted.iter().map(|m| m.value as u64)); - assert!(minted_value_set.contains(&(58 * MOB - Mob::MINIMUM_FEE))); - assert!(minted_value_set.contains(&(42 * MOB))); + assert_eq!(pending[0].0.value, 100000000000000); // Our balance should reflect the various statuses of our txos let balance = service - .get_balance_for_account(&AccountID(alice.account_id_hex)) + .get_balance_for_account(&AccountID(alice.id)) .unwrap(); - assert_eq!(balance.unspent, 0); - assert_eq!(balance.pending, 100 * MOB as u128); - assert_eq!(balance.spent, 0); - assert_eq!(balance.secreted, (100 * MOB - Mob::MINIMUM_FEE) as u128); - assert_eq!(balance.orphaned, 0); + let balance_pmob = balance.get(&Mob::ID).unwrap(); + + assert_eq!(balance_pmob.unverified, 0); + assert_eq!(balance_pmob.unspent, 0); + assert_eq!(balance_pmob.pending, 100 * MOB as u128); + assert_eq!(balance_pmob.spent, 0); + assert_eq!(balance_pmob.orphaned, 0); } } diff --git a/full-service/src/service/view_only_account.rs b/full-service/src/service/view_only_account.rs deleted file mode 100644 index 1ce27cc02..000000000 --- a/full-service/src/service/view_only_account.rs +++ /dev/null @@ -1,291 +0,0 @@ -// Copyright (c) 2020-2022 MobileCoin Inc. - -//! Service for managing view-only-accounts. - -use crate::{ - db::{ - models::{ViewOnlyAccount, ViewOnlySubaddress}, - transaction, - view_only_account::ViewOnlyAccountModel, - view_only_subaddress::ViewOnlySubaddressModel, - }, - service::{account::AccountServiceError, WalletService}, -}; -use mc_common::logger::log; -use mc_connection::{BlockchainConnection, UserTxConnection}; -use mc_crypto_keys::{RistrettoPrivate, RistrettoPublic}; -use mc_fog_report_validation::FogPubkeyResolver; -use mc_ledger_db::Ledger; - -/// Trait defining the ways in which the wallet can interact with and manage -/// view-only accounts. -pub trait ViewOnlyAccountService { - /// Import an existing view-only-account to the wallet using the mnemonic. - #[allow(clippy::too_many_arguments)] - fn import_view_only_account( - &self, - account_id_hex: &str, - view_private_key: &RistrettoPrivate, - main_subaddress_index: u64, - change_subaddress_index: u64, - next_subaddress_index: u64, - name: &str, - subaddresses: Vec<(String, u64, String, RistrettoPublic)>, - ) -> Result; - - fn import_subaddresses( - &self, - account_id_hex: &str, - subaddresses: Vec<(String, u64, String, RistrettoPublic)>, - ) -> Result, AccountServiceError>; - - /// Get a view only account by view private key - fn get_view_only_account( - &self, - account_id: &str, - ) -> Result; - - // List all view only accounts - fn list_view_only_accounts(&self) -> Result, AccountServiceError>; - - /// Update the name for a view only account. - fn update_view_only_account_name( - &self, - account_id: &str, - name: &str, - ) -> Result; - - /// Remove a view only account from the wallet. - fn remove_view_only_account(&self, account_id: &str) -> Result; -} - -impl ViewOnlyAccountService for WalletService -where - T: BlockchainConnection + UserTxConnection + 'static, - FPR: FogPubkeyResolver + Send + Sync + 'static, -{ - fn import_view_only_account( - &self, - account_id_hex: &str, - view_private_key: &RistrettoPrivate, - main_subaddress_index: u64, - change_subaddress_index: u64, - next_subaddress_index: u64, - name: &str, - subaddresses: Vec<(String, u64, String, RistrettoPublic)>, - ) -> Result { - let conn = &self.wallet_db.get_conn()?; - - let local_block_height = self.ledger_db.num_blocks()?; - let import_block_index = local_block_height; - - transaction(conn, || { - let view_only_account = ViewOnlyAccount::create( - account_id_hex, - view_private_key, - 0, - import_block_index, - main_subaddress_index, - change_subaddress_index, - next_subaddress_index, - name, - conn, - )?; - - for (public_address_b58, subaddress_index, comment, public_spend_key) in - subaddresses.iter() - { - ViewOnlySubaddress::create( - &view_only_account, - public_address_b58, - *subaddress_index, - comment, - public_spend_key, - conn, - )?; - } - - Ok(view_only_account) - }) - } - - fn import_subaddresses( - &self, - account_id_hex: &str, - subaddresses: Vec<(String, u64, String, RistrettoPublic)>, - ) -> Result, AccountServiceError> { - let conn = &self.wallet_db.get_conn()?; - - transaction(conn, || { - let account = ViewOnlyAccount::get(account_id_hex, conn)?; - - for (public_address_b58, subaddress_index, comment, public_spend_key) in - subaddresses.iter() - { - let existing = ViewOnlySubaddress::get(public_address_b58, conn); - if existing.is_err() { - ViewOnlySubaddress::create( - &account, - public_address_b58, - *subaddress_index, - comment, - public_spend_key, - conn, - )?; - } - } - - let next_subaddress_index = subaddresses - .iter() - .map(|(_, index, _, _)| *index) - .max() - .unwrap_or(0) - + 1; - - if next_subaddress_index > account.next_subaddress_index as u64 { - account.update_next_subaddress_index(next_subaddress_index, conn)?; - } - - Ok(subaddresses - .iter() - .map(|(public_address_b58, _, _, _)| public_address_b58.clone()) - .collect()) - }) - } - - fn get_view_only_account( - &self, - account_id: &str, - ) -> Result { - log::info!(self.logger, "fetching view-only-account {:?}", account_id); - - let conn = self.wallet_db.get_conn()?; - Ok(ViewOnlyAccount::get(account_id, &conn)?) - } - - fn list_view_only_accounts(&self) -> Result, AccountServiceError> { - let conn = self.wallet_db.get_conn()?; - Ok(ViewOnlyAccount::list_all(&conn)?) - } - - fn update_view_only_account_name( - &self, - account_id: &str, - name: &str, - ) -> Result { - let conn = self.wallet_db.get_conn()?; - ViewOnlyAccount::get(account_id, &conn)?.update_name(name, &conn)?; - Ok(ViewOnlyAccount::get(account_id, &conn)?) - } - - fn remove_view_only_account(&self, account_id: &str) -> Result { - log::info!(self.logger, "Deleting view only account {}", account_id,); - - let conn = self.wallet_db.get_conn()?; - let account = ViewOnlyAccount::get(account_id, &conn)?; - transaction(&conn, || { - account.delete(&conn)?; - Ok(true) - }) - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::{ - db::account::AccountID, - test_utils::{get_test_ledger, setup_wallet_service}, - util::{b58::b58_encode_public_address, encoding_helpers::ristretto_to_vec}, - }; - use mc_account_keys::{ - AccountKey, PublicAddress, CHANGE_SUBADDRESS_INDEX, DEFAULT_SUBADDRESS_INDEX, - }; - use mc_common::logger::{test_with_logger, Logger}; - use mc_crypto_keys::RistrettoPrivate; - use mc_util_from_random::FromRandom; - use rand::{rngs::StdRng, SeedableRng}; - - #[test_with_logger] - fn service_view_only_account_crud(logger: Logger) { - let mut rng: StdRng = SeedableRng::from_seed([20u8; 32]); - let known_recipients: Vec = Vec::new(); - let current_block_height = 12; //index 11 - let ledger_db = get_test_ledger( - 5, - &known_recipients, - current_block_height as usize, - &mut rng, - ); - let service = setup_wallet_service(ledger_db.clone(), logger.clone()); - - let view_private_key = RistrettoPrivate::from_random(&mut rng); - let spend_private_key = RistrettoPrivate::from_random(&mut rng); - - let name = "testing"; - - let account_key = AccountKey::new(&spend_private_key, &view_private_key); - let account_id = AccountID::from(&account_key); - let main_public_address = account_key.default_subaddress(); - let change_public_address = account_key.change_subaddress(); - let mut subaddresses: Vec<(String, u64, String, RistrettoPublic)> = Vec::new(); - subaddresses.push(( - b58_encode_public_address(&main_public_address).unwrap(), - DEFAULT_SUBADDRESS_INDEX, - "Main".to_string(), - *main_public_address.spend_public_key(), - )); - subaddresses.push(( - b58_encode_public_address(&change_public_address).unwrap(), - CHANGE_SUBADDRESS_INDEX, - "Change".to_string(), - *change_public_address.spend_public_key(), - )); - - service - .import_view_only_account( - &account_id.to_string(), - &view_private_key, - DEFAULT_SUBADDRESS_INDEX, - CHANGE_SUBADDRESS_INDEX, - 2, - name.clone(), - subaddresses, - ) - .unwrap(); - - // test get - let expected_account = ViewOnlyAccount { - id: 1, - account_id_hex: account_id.to_string(), - view_private_key: ristretto_to_vec(&view_private_key), - first_block_index: 0, - next_block_index: 0, - import_block_index: (current_block_height - 1 + 1) as i64, - name: name.to_string(), - main_subaddress_index: DEFAULT_SUBADDRESS_INDEX as i64, - change_subaddress_index: CHANGE_SUBADDRESS_INDEX as i64, - next_subaddress_index: 2, - }; - - let gotten_account = service - .get_view_only_account(&account_id.to_string()) - .unwrap(); - - assert_eq!(gotten_account, expected_account); - - // test update name - let new_name = "coinzzzz"; - let updated = service - .update_view_only_account_name(&account_id.to_string(), new_name) - .unwrap(); - assert_eq!(updated.name, new_name.to_string()); - - // test remove account - assert!(service - .remove_view_only_account(&account_id.to_string()) - .unwrap()); - let not_found = service.get_view_only_account(&account_id.to_string()); - assert!(not_found.is_err()); - } -} diff --git a/full-service/src/service/view_only_txo.rs b/full-service/src/service/view_only_txo.rs deleted file mode 100644 index d139b3bbd..000000000 --- a/full-service/src/service/view_only_txo.rs +++ /dev/null @@ -1,219 +0,0 @@ -// Copyright (c) 2020-2022 MobileCoin Inc. - -//! Service for managing view-only Txos. - -use crate::{ - db::{models::ViewOnlyTxo, transaction, view_only_txo::ViewOnlyTxoModel}, - service::txo::TxoServiceError, - WalletService, -}; -use mc_connection::{BlockchainConnection, UserTxConnection}; -use mc_fog_report_validation::FogPubkeyResolver; -use mc_ledger_db::Ledger; -use mc_transaction_core::{ring_signature::KeyImage, tx::TxOut}; - -/// Trait defining the ways in which the wallet can interact with and manage -/// view only Txos. -pub trait ViewOnlyTxoService { - /// List the Txos for a given account in the wallet. - fn list_view_only_txos( - &self, - account_id: &str, - limit: Option, - offset: Option, - ) -> Result, TxoServiceError>; - - /// update the key image for a list of txos - fn set_view_only_txos_key_images( - &self, - txo_ids_and_key_images: Vec<(String, KeyImage)>, - ) -> Result<(), TxoServiceError>; - - fn list_incomplete_view_only_txos( - &self, - account_id: &str, - ) -> Result, TxoServiceError>; -} - -impl ViewOnlyTxoService for WalletService -where - T: BlockchainConnection + UserTxConnection + 'static, - FPR: FogPubkeyResolver + Send + Sync + 'static, -{ - fn list_view_only_txos( - &self, - account_id: &str, - limit: Option, - offset: Option, - ) -> Result, TxoServiceError> { - let conn = self.wallet_db.get_conn()?; - Ok(ViewOnlyTxo::list_for_account( - account_id, - limit, - offset, - Some(0), - &conn, - )?) - } - - fn set_view_only_txos_key_images( - &self, - txo_ids_and_key_images: Vec<(String, KeyImage)>, - ) -> Result<(), TxoServiceError> { - let conn = self.wallet_db.get_conn()?; - - transaction(&conn, || { - for (txo_id, key_image) in txo_ids_and_key_images { - ViewOnlyTxo::update_key_image(&txo_id, &key_image, &conn)?; - - if let Some(block_index) = match self.ledger_db.check_key_image(&key_image) { - Ok(block_index) => block_index, - Err(_) => None, - } { - ViewOnlyTxo::update_spent_block_index(&txo_id.to_string(), block_index, &conn)?; - } - } - - Ok(()) - }) - } - - fn list_incomplete_view_only_txos( - &self, - account_id: &str, - ) -> Result, TxoServiceError> { - let conn = self.wallet_db.get_conn()?; - - Ok(ViewOnlyTxo::export_txouts_without_key_image_or_subaddress_index(account_id, &conn)?) - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::{ - db::account::AccountID, - service::view_only_account::ViewOnlyAccountService, - test_utils::{add_block_to_ledger_db, get_test_ledger, setup_wallet_service, MOB}, - util::b58::b58_encode_public_address, - }; - use mc_account_keys::{ - AccountKey, PublicAddress, CHANGE_SUBADDRESS_INDEX, DEFAULT_SUBADDRESS_INDEX, - }; - use mc_common::logger::{test_with_logger, Logger}; - use mc_crypto_keys::{RistrettoPrivate, RistrettoPublic}; - use mc_crypto_rand::RngCore; - use mc_transaction_core::{encrypted_fog_hint::EncryptedFogHint, tokens::Mob, Amount, Token}; - use mc_util_from_random::FromRandom; - use rand::{rngs::StdRng, SeedableRng}; - - #[test_with_logger] - fn test_view_only_txo_service(logger: Logger) { - let mut rng: StdRng = SeedableRng::from_seed([20u8; 32]); - let known_recipients: Vec = Vec::new(); - let current_block_height = 12; //index 11 - let mut ledger_db = get_test_ledger( - 5, - &known_recipients, - current_block_height as usize, - &mut rng, - ); - let service = setup_wallet_service(ledger_db.clone(), logger.clone()); - let conn = service.wallet_db.get_conn().unwrap(); - - let view_private_key = RistrettoPrivate::from_random(&mut rng); - let spend_private_key = RistrettoPrivate::from_random(&mut rng); - - let account_key = AccountKey::new(&spend_private_key, &view_private_key); - let account_id = AccountID::from(&account_key); - let main_public_address = account_key.default_subaddress(); - let change_public_address = account_key.change_subaddress(); - let mut subaddresses: Vec<(String, u64, String, RistrettoPublic)> = Vec::new(); - subaddresses.push(( - b58_encode_public_address(&main_public_address).unwrap(), - DEFAULT_SUBADDRESS_INDEX, - "Main".to_string(), - *main_public_address.spend_public_key(), - )); - subaddresses.push(( - b58_encode_public_address(&change_public_address).unwrap(), - CHANGE_SUBADDRESS_INDEX, - "Change".to_string(), - *change_public_address.spend_public_key(), - )); - - let account = service - .import_view_only_account( - &account_id.to_string(), - &view_private_key, - DEFAULT_SUBADDRESS_INDEX, - CHANGE_SUBADDRESS_INDEX, - 2, - "testing", - subaddresses, - ) - .unwrap(); - - for _ in 0..2 { - let value = 420; - let amount = Amount::new(value, Mob::ID); - let tx_private_key = RistrettoPrivate::from_random(&mut rng); - let hint = EncryptedFogHint::fake_onetime_hint(&mut rng); - let fake_tx_out = - TxOut::new(amount, &main_public_address, &tx_private_key, hint).unwrap(); - ViewOnlyTxo::create( - fake_tx_out.clone(), - amount, - Some(DEFAULT_SUBADDRESS_INDEX), - Some(11), - &account.account_id_hex, - &conn, - ) - .unwrap(); - } - - let txos = service - .list_view_only_txos(&account.account_id_hex, None, None) - .unwrap(); - - let txo_id_1 = txos[0].txo_id_hex.clone(); - let txo_id_2 = txos[1].txo_id_hex.clone(); - - for txo in &txos { - assert_eq!(txo.key_image, None); - assert_eq!(txo.subaddress_index, Some(DEFAULT_SUBADDRESS_INDEX as i64)); - assert_eq!(txo.received_block_index, Some(11)); - assert_eq!(txo.submitted_block_index, None); - assert_eq!(txo.pending_tombstone_block_index, None); - assert_eq!(txo.spent_block_index, None); - } - - let key_image_1 = KeyImage::from(rng.next_u64()); - let key_image_2 = KeyImage::from(rng.next_u64()); - - add_block_to_ledger_db( - &mut ledger_db, - &vec![main_public_address], - 42 * MOB, - &vec![key_image_1], - &mut rng, - ); - - let input_vec = [(txo_id_1, key_image_1), (txo_id_2, key_image_2)].to_vec(); - - service.set_view_only_txos_key_images(input_vec).unwrap(); - - let txos = service - .list_view_only_txos(&account.account_id_hex, None, None) - .unwrap(); - - for txo in txos { - assert!(txo.key_image.is_some()); - if txo.key_image.unwrap() == mc_util_serial::encode(&key_image_1) { - assert_eq!(txo.spent_block_index, Some(12)); - } else { - assert_eq!(txo.spent_block_index, None); - } - } - } -} diff --git a/full-service/src/service/wallet_service.rs b/full-service/src/service/wallet_service.rs index 1f55ae223..acf2994e3 100644 --- a/full-service/src/service/wallet_service.rs +++ b/full-service/src/service/wallet_service.rs @@ -2,7 +2,10 @@ //! The Wallet Service for interacting with the wallet. -use crate::{db::WalletDb, service::sync::SyncThread}; +use crate::{ + db::{Conn, WalletDb, WalletDbError}, + service::sync::SyncThread, +}; use mc_common::logger::{log, Logger}; use mc_connection::{ BlockchainConnection, ConnectionManager as McConnectionManager, UserTxConnection, @@ -24,7 +27,7 @@ pub struct WalletService< FPR: FogPubkeyResolver + Send + Sync + 'static, > { /// Wallet database handle. - pub wallet_db: WalletDb, + pub wallet_db: Option, /// Ledger database. pub ledger_db: LedgerDB, @@ -40,7 +43,7 @@ pub struct WalletService< pub fog_resolver_factory: Arc Result + Send + Sync>, /// Background ledger sync thread. - _sync_thread: SyncThread, + _sync_thread: Option, /// Monotonically increasing counter. This is used for node round-robin /// selection. @@ -60,7 +63,7 @@ impl< { #[allow(clippy::too_many_arguments)] pub fn new( - wallet_db: WalletDb, + wallet_db: Option, ledger_db: LedgerDB, peer_manager: McConnectionManager, network_state: Arc>>, @@ -68,8 +71,17 @@ impl< offline: bool, logger: Logger, ) -> Self { - log::info!(logger, "Starting Wallet TXO Sync Task Thread"); - let sync_thread = SyncThread::start(ledger_db.clone(), wallet_db.clone(), logger.clone()); + let sync_thread = if let Some(wallet_db) = wallet_db.clone() { + log::info!(logger, "Starting Wallet TXO Sync Task Thread"); + Some(SyncThread::start( + ledger_db.clone(), + wallet_db, + logger.clone(), + )) + } else { + None + }; + let mut rng = rand::thread_rng(); WalletService { wallet_db, @@ -83,4 +95,11 @@ impl< logger, } } + + pub fn get_conn(&self) -> Result { + self.wallet_db + .as_ref() + .ok_or(WalletDbError::WalletFunctionsDisabled)? + .get_conn() + } } diff --git a/full-service/src/test_utils.rs b/full-service/src/test_utils.rs index dfb25bbba..31d81b4ae 100644 --- a/full-service/src/test_utils.rs +++ b/full-service/src/test_utils.rs @@ -1,19 +1,16 @@ // Copyright (c) 2020-2021 MobileCoin Inc. - +#[cfg(test)] use crate::{ db::{ account::{AccountID, AccountModel}, - models::{ - Account, TransactionLog, Txo, ViewOnlyAccount, TXO_USED_AS_CHANGE, TXO_USED_AS_OUTPUT, - }, + models::{Account, TransactionLog, Txo}, transaction_log::TransactionLogModel, txo::TxoModel, - view_only_account::ViewOnlyAccountModel, WalletDb, WalletDbError, }, error::SyncError, service::{ - sync::{sync_account, sync_view_only_account}, + sync::sync_account, transaction::TransactionMemo, transaction_builder::WalletTransactionBuilder, }, WalletService, @@ -25,28 +22,31 @@ use diesel::{ use diesel_migrations::embed_migrations; use mc_account_keys::{AccountKey, PublicAddress, RootIdentity}; use mc_attest_verifier::Verifier; +use mc_blockchain_test_utils::make_block_metadata; +use mc_blockchain_types::{Block, BlockContents, BlockVersion}; use mc_common::logger::{log, Logger}; use mc_connection::{Connection, ConnectionManager, HardcodedCredentialsProvider, ThickClient}; use mc_connection_test_utils::{test_client_uri, MockBlockchainConnection}; +use mc_consensus_enclave_api::FeeMap; use mc_consensus_scp::QuorumSet; use mc_crypto_keys::{RistrettoPrivate, RistrettoPublic}; use mc_crypto_rand::{CryptoRng, RngCore}; use mc_fog_report_validation::{FullyValidatedFogPubkey, MockFogPubkeyResolver}; use mc_ledger_db::{Ledger, LedgerDB}; use mc_ledger_sync::PollingNetworkState; -use mc_mobilecoind::payments::TxProposal; use mc_transaction_core::{ encrypted_fog_hint::EncryptedFogHint, onetime_keys::{create_tx_out_target_key, recover_onetime_private_key}, ring_signature::KeyImage, tokens::Mob, tx::{Tx, TxOut}, - Amount, Block, BlockContents, Token, MAX_BLOCK_VERSION, + Amount, Token, TokenId, }; use mc_util_from_random::FromRandom; use mc_util_uri::{ConnectionUri, FogUri}; use rand::{distributions::Alphanumeric, rngs::StdRng, thread_rng, Rng, SeedableRng}; use std::{ + collections::BTreeMap, convert::TryFrom, env, path::PathBuf, @@ -172,7 +172,11 @@ pub fn generate_ledger_db(path: &str) -> LedgerDB { db } -fn append_test_block(ledger_db: &mut LedgerDB, block_contents: BlockContents) -> u64 { +fn append_test_block( + ledger_db: &mut LedgerDB, + block_contents: BlockContents, + mut rng: &mut (impl CryptoRng + RngCore), +) -> u64 { let num_blocks = ledger_db.num_blocks().expect("failed to get block height"); let new_block; @@ -181,7 +185,7 @@ fn append_test_block(ledger_db: &mut LedgerDB, block_contents: BlockContents) -> .get_block(num_blocks - 1) .expect("failed to get parent block"); new_block = Block::new_with_parent( - MAX_BLOCK_VERSION, + BlockVersion::MAX, &parent, &Default::default(), &block_contents, @@ -190,8 +194,10 @@ fn append_test_block(ledger_db: &mut LedgerDB, block_contents: BlockContents) -> new_block = Block::new_origin_block(&block_contents.outputs); } + let block_metadata = make_block_metadata(new_block.id.clone(), &mut rng); + ledger_db - .append_block(&new_block, &block_contents, None) + .append_block(&new_block, &block_contents, None, Some(&block_metadata)) .expect("failed writing initial transactions"); ledger_db.num_blocks().expect("failed to get block height") @@ -218,6 +224,7 @@ pub fn add_block_to_ledger_db( .map(|recipient| { TxOut::new( // TODO: allow for subaddress index! + BlockVersion::MAX, Amount::new(output_value, Mob::ID), recipient, &RistrettoPrivate::from_random(rng), @@ -233,33 +240,28 @@ pub fn add_block_to_ledger_db( validated_mint_config_txs: Vec::new(), mint_txs: Vec::new(), }; - append_test_block(ledger_db, block_contents) + append_test_block(ledger_db, block_contents, rng) } -pub fn add_block_with_tx_proposal(ledger_db: &mut LedgerDB, tx_proposal: TxProposal) -> u64 { - let block_contents = BlockContents { - key_images: tx_proposal.tx.key_images(), - outputs: tx_proposal.tx.prefix.outputs.clone(), - validated_mint_config_txs: Vec::new(), - mint_txs: Vec::new(), - }; - append_test_block(ledger_db, block_contents) -} - -pub fn add_block_with_tx(ledger_db: &mut LedgerDB, tx: Tx) -> u64 { +pub fn add_block_with_tx( + ledger_db: &mut LedgerDB, + tx: Tx, + rng: &mut (impl CryptoRng + RngCore), +) -> u64 { let block_contents = BlockContents { key_images: tx.key_images(), outputs: tx.prefix.outputs.clone(), validated_mint_config_txs: Vec::new(), mint_txs: Vec::new(), }; - append_test_block(ledger_db, block_contents) + append_test_block(ledger_db, block_contents, rng) } pub fn add_block_from_transaction_log( ledger_db: &mut LedgerDB, conn: &PooledConnection>, transaction_log: &TransactionLog, + rng: &mut (impl CryptoRng + RngCore), ) -> u64 { let associated_txos = transaction_log.get_associated_txos(conn).unwrap(); @@ -267,7 +269,7 @@ pub fn add_block_from_transaction_log( output_txos.append(&mut associated_txos.change.clone()); let outputs: Vec = output_txos .iter() - .map(|txo| mc_util_serial::decode(&txo.txo).unwrap()) + .map(|(txo, _)| mc_util_serial::decode(&txo.txo).unwrap()) .collect(); let input_txos: Vec = associated_txos.inputs.clone(); @@ -284,13 +286,14 @@ pub fn add_block_from_transaction_log( mint_txs: Vec::new(), }; - append_test_block(ledger_db, block_contents) + append_test_block(ledger_db, block_contents, rng) } pub fn add_block_with_tx_outs( ledger_db: &mut LedgerDB, outputs: &[TxOut], key_images: &[KeyImage], + rng: &mut (impl CryptoRng + RngCore), ) -> u64 { let block_contents = BlockContents { key_images: key_images.to_vec(), @@ -298,7 +301,7 @@ pub fn add_block_with_tx_outs( validated_mint_config_txs: Vec::new(), mint_txs: Vec::new(), }; - append_test_block(ledger_db, block_contents) + append_test_block(ledger_db, block_contents, rng) } pub fn setup_peer_manager_and_network_state( @@ -312,14 +315,25 @@ pub fn setup_peer_manager_and_network_state( let (peers, node_ids) = if offline { (vec![], vec![]) } else { - let peer1 = MockBlockchainConnection::new(test_client_uri(1), ledger_db.clone(), 0); - let peer2 = MockBlockchainConnection::new(test_client_uri(2), ledger_db.clone(), 0); + let mut minimum_fees = BTreeMap::new(); + minimum_fees.insert(Mob::ID, Mob::MINIMUM_FEE); + minimum_fees.insert(TokenId::from(1), 1024); + let fee_map = FeeMap::try_from(minimum_fees).unwrap(); + + let peer1 = MockBlockchainConnection::new( + test_client_uri(1), + ledger_db.clone(), + 0, + fee_map.clone(), + ); + let peer2 = + MockBlockchainConnection::new(test_client_uri(2), ledger_db.clone(), 0, fee_map); ( vec![peer1.clone(), peer2.clone()], vec![ - peer1.uri().responder_id().unwrap(), - peer2.uri().responder_id().unwrap(), + peer1.uri().host_and_port_responder_id().unwrap(), + peer2.uri().host_and_port_responder_id().unwrap(), ], ) }; @@ -346,6 +360,7 @@ pub fn add_block_with_db_txos( wallet_db: &WalletDb, output_txo_ids: &[String], key_images: &[KeyImage], + rng: &mut (impl CryptoRng + RngCore), ) -> u64 { let outputs: Vec = output_txo_ids .iter() @@ -359,7 +374,7 @@ pub fn add_block_with_db_txos( }) .collect(); - add_block_with_tx_outs(ledger_db, &outputs, key_images) + add_block_with_tx_outs(ledger_db, &outputs, key_images, rng) } // Sync account to most recent block @@ -389,34 +404,6 @@ pub fn manually_sync_account( account } -// Sync view-only-account to most recent block -pub fn manually_sync_view_only_account( - ledger_db: &LedgerDB, - wallet_db: &WalletDb, - view_only_account_id: &str, - logger: &Logger, -) -> ViewOnlyAccount { - let mut account: ViewOnlyAccount; - loop { - match sync_view_only_account(&ledger_db, &wallet_db, &view_only_account_id, &logger) { - Ok(_) => {} - Err(SyncError::Database(WalletDbError::Diesel( - diesel::result::Error::DatabaseError(_kind, info), - ))) if info.message() == "database is locked" => { - log::trace!(logger, "Database locked. Will retry"); - std::thread::sleep(Duration::from_millis(500)); - } - Err(e) => panic!("Could not sync account due to {:?}", e), - } - account = - ViewOnlyAccount::get(&view_only_account_id, &wallet_db.get_conn().unwrap()).unwrap(); - if account.next_block_index as u64 >= ledger_db.num_blocks().unwrap() { - break; - } - } - account -} - pub fn setup_grpc_peer_manager_and_network_state( logger: Logger, ) -> ( @@ -440,6 +427,7 @@ pub fn setup_grpc_peer_manager_and_network_state( .iter() .map(|client_uri| { ThickClient::new( + "local".to_string(), client_uri.clone(), verifier.clone(), grpc_env.clone(), @@ -477,7 +465,7 @@ pub fn create_test_txo_for_recipient( let recipient = recipient_account_key.subaddress(recipient_subaddress_index); let tx_private_key = RistrettoPrivate::from_random(rng); let hint = EncryptedFogHint::fake_onetime_hint(rng); - let tx_out = TxOut::new(amount, &recipient, &tx_private_key, hint).unwrap(); + let tx_out = TxOut::new(BlockVersion::MAX, amount, &recipient, &tx_private_key, hint).unwrap(); // Calculate KeyImage - note you cannot use KeyImage::from(tx_private_key) // because the calculation must be done with CryptoNote math (see @@ -529,8 +517,7 @@ pub fn create_test_minted_and_change_txos( value: u64, wallet_db: WalletDb, ledger_db: LedgerDB, - logger: Logger, -) -> ((String, u64), (String, u64)) { +) -> TransactionLog { let mut rng: StdRng = SeedableRng::from_seed([20u8; 32]); // Use the builder to create valid TxOuts for this account @@ -538,51 +525,26 @@ pub fn create_test_minted_and_change_txos( AccountID::from(&src_account_key).to_string(), ledger_db, get_resolver_factory(&mut rng).unwrap(), - logger, ); let conn = wallet_db.get_conn().unwrap(); - builder.add_recipient(recipient, value).unwrap(); - builder.select_txos(&conn, None, false).unwrap(); + builder.add_recipient(recipient, value, Mob::ID).unwrap(); + builder.select_txos(&conn, None).unwrap(); builder.set_tombstone(0).unwrap(); - let tx_proposal = builder.build(&conn).unwrap(); + let unsigned_tx_proposal = builder.build(TransactionMemo::RTH, &conn).unwrap(); + let tx_proposal = unsigned_tx_proposal.sign(&src_account_key).unwrap(); // There should be 2 outputs, one to dest and one change assert_eq!(tx_proposal.tx.prefix.outputs.len(), 2); - // Create minted for the destination output. - assert_eq!(tx_proposal.outlay_index_to_tx_out_index.len(), 1); - let outlay_txo_index = tx_proposal.outlay_index_to_tx_out_index[&0]; - let tx_out = tx_proposal.tx.prefix.outputs[outlay_txo_index].clone(); - let processed_output = Txo::create_minted( - &AccountID::from(&src_account_key).to_string(), - &tx_out, + TransactionLog::log_submitted( &tx_proposal, - outlay_txo_index, - &conn, - ) - .unwrap(); - assert!(processed_output.recipient.is_some()); - assert_eq!(processed_output.txo_type, TXO_USED_AS_OUTPUT); - - // Create minted for the change output. - let change_txo_index = if outlay_txo_index == 0 { 1 } else { 0 }; - let change_tx_out = tx_proposal.tx.prefix.outputs[change_txo_index].clone(); - let processed_change = Txo::create_minted( + 10, + "".to_string(), &AccountID::from(&src_account_key).to_string(), - &change_tx_out, - &tx_proposal, - change_txo_index, &conn, ) - .unwrap(); - assert_eq!(processed_change.recipient, None,); - // Change starts as an output, and is updated to change when scanned. - assert_eq!(processed_change.txo_type, TXO_USED_AS_CHANGE); - ( - (processed_output.txo_id_hex, processed_output.value as u64), - (processed_change.txo_id_hex, processed_change.value as u64), - ) + .unwrap() } // Seed a local account with some Txos in the ledger @@ -605,7 +567,6 @@ pub fn random_account_with_seed_values( "".to_string(), "".to_string(), "".to_string(), - &ledger_db, &wallet_db.get_conn().unwrap(), ) .unwrap(); @@ -636,6 +597,8 @@ pub fn random_account_with_seed_values( None, None, None, + None, + None, Some(0), &wallet_db.get_conn().unwrap(), ) @@ -652,7 +615,6 @@ pub fn builder_for_random_recipient( account_key: &AccountKey, ledger_db: &LedgerDB, mut rng: &mut StdRng, - logger: &Logger, ) -> ( PublicAddress, WalletTransactionBuilder, @@ -662,7 +624,6 @@ pub fn builder_for_random_recipient( AccountID::from(account_key).to_string(), ledger_db.clone(), get_resolver_factory(&mut rng).unwrap(), - logger.clone(), ); let recipient_account_key = AccountKey::random(&mut rng); @@ -682,7 +643,7 @@ pub fn get_resolver_factory( let pubkey = RistrettoPublic::from(&fog_private_key); fog_pubkey_resolver .expect_get_fog_pubkey() - .return_once(move |_recipient| { + .returning(move |_| { Ok(FullyValidatedFogPubkey { pubkey, pubkey_expiry: 10000, @@ -697,25 +658,36 @@ pub fn setup_wallet_service( ledger_db: LedgerDB, logger: Logger, ) -> WalletService, MockFogPubkeyResolver> { - setup_wallet_service_impl(ledger_db, logger, false) + setup_wallet_service_impl(ledger_db, logger, false, false) } pub fn setup_wallet_service_offline( ledger_db: LedgerDB, logger: Logger, ) -> WalletService, MockFogPubkeyResolver> { - setup_wallet_service_impl(ledger_db, logger, true) + setup_wallet_service_impl(ledger_db, logger, true, false) +} + +pub fn setup_wallet_service_no_wallet_db( + ledger_db: LedgerDB, + logger: Logger, +) -> WalletService, MockFogPubkeyResolver> { + setup_wallet_service_impl(ledger_db, logger, false, true) } fn setup_wallet_service_impl( ledger_db: LedgerDB, logger: Logger, offline: bool, + no_wallet_db: bool, ) -> WalletService, MockFogPubkeyResolver> { let mut rng: StdRng = SeedableRng::from_seed([20u8; 32]); let db_test_context = WalletDbTestContext::default(); - let wallet_db = db_test_context.get_db_instance(logger.clone()); + let wallet_db = match no_wallet_db { + true => None, + false => Some(db_test_context.get_db_instance(logger.clone())), + }; let (peer_manager, network_state) = setup_peer_manager_and_network_state(ledger_db.clone(), logger.clone(), offline); diff --git a/full-service/src/unsigned_tx.rs b/full-service/src/unsigned_tx.rs deleted file mode 100644 index 861a33f40..000000000 --- a/full-service/src/unsigned_tx.rs +++ /dev/null @@ -1,203 +0,0 @@ -use mc_account_keys::AccountKey; -use mc_common::HashMap; -use mc_crypto_keys::{RistrettoPrivate, RistrettoPublic}; -use mc_mobilecoind::{ - payments::{Outlay, TxProposal}, - UnspentTxOut, -}; -use mc_transaction_core::{ - get_tx_out_shared_secret, - onetime_keys::recover_onetime_private_key, - ring_signature::{KeyImage, Scalar}, - tokens::Mob, - tx::{TxIn, TxOut, TxOutConfirmationNumber}, - Amount, BlockVersion, Token, -}; -use mc_transaction_std::{ - ChangeDestination, InputCredentials, RTHMemoBuilder, SenderMemoCredential, TransactionBuilder, -}; -use rand::{CryptoRng, RngCore}; -use serde::{Deserialize, Serialize}; -use std::convert::TryFrom; - -use crate::{ - error::WalletTransactionBuilderError, fog_resolver::FullServiceFogResolver, - util::b58::b58_decode_public_address, -}; - -#[derive(Clone, Debug, Deserialize, Serialize)] -pub struct UnsignedTx { - /// The fully constructed input rings - pub inputs_and_real_indices_and_subaddress_indices: Vec<(TxIn, u64, u64)>, - - /// Vector of (PublicAddressB58, Amount) for the recipients of this - /// transaction. - pub outlays: Vec<(String, u64)>, - - /// The fee to be paid - pub fee: u64, - - /// The tombstone block index - pub tombstone_block_index: u64, - - /// The block version - pub block_version: BlockVersion, -} - -impl UnsignedTx { - pub fn sign( - self, - account_key: &AccountKey, - fog_resolver: FullServiceFogResolver, - ) -> Result { - let mut rng = rand::thread_rng(); - // Create transaction builder. - let mut memo_builder = RTHMemoBuilder::default(); - memo_builder.set_sender_credential(SenderMemoCredential::from(account_key)); - memo_builder.enable_destination_memo(); - let fee = Amount::new(self.fee, Mob::ID); - let mut transaction_builder = - TransactionBuilder::new(self.block_version, fee, fog_resolver, memo_builder)?; - - transaction_builder.set_tombstone_block(self.tombstone_block_index); - - let mut selected_utxos: Vec = Vec::new(); - - for (tx_in, real_index, subaddress_index) in - self.inputs_and_real_indices_and_subaddress_indices - { - let tx_out = &tx_in.ring[real_index as usize]; - let tx_public_key = RistrettoPublic::try_from(&tx_out.public_key)?; - - let onetime_private_key = recover_onetime_private_key( - &tx_public_key, - account_key.view_private_key(), - &account_key.subaddress_spend_private(subaddress_index), - ); - - let key_image = KeyImage::from(&onetime_private_key); - - let input_credentials = InputCredentials::new( - tx_in.ring.clone(), - tx_in.proofs.clone(), - real_index as usize, - onetime_private_key, - *account_key.view_private_key(), - )?; - - transaction_builder.add_input(input_credentials); - - let tx_out = &tx_in.ring[real_index as usize]; - let (amount, _) = decode_amount(tx_out, account_key.view_private_key())?; - - let utxo = UnspentTxOut { - tx_out: tx_out.clone(), - subaddress_index, - key_image, - value: amount.value, - attempted_spend_height: 0, - attempted_spend_tombstone: 0, - token_id: *Mob::ID, - }; - - selected_utxos.push(utxo); - } - - // Add the inputs and sum their values - let total_input_value = selected_utxos - .iter() - .map(|utxo| utxo.value as u128) - .sum::() as u64; - - let mut outlays_decoded: Vec = Vec::new(); - - for (public_address_b58, value) in self.outlays { - let receiver = b58_decode_public_address(&public_address_b58)?; - outlays_decoded.push(Outlay { receiver, value }); - } - - let (total_payload_value, tx_out_to_outlay_index, outlay_confirmation_numbers) = - add_payload_outputs(&outlays_decoded, &mut transaction_builder, &mut rng)?; - - add_change_output( - account_key, - total_input_value, - total_payload_value, - &mut transaction_builder, - &mut rng, - )?; - - let tx = transaction_builder.build(&mut rng)?; - - let outlay_index_to_tx_out_index: HashMap = tx - .prefix - .outputs - .iter() - .enumerate() - .filter_map(|(tx_out_index, tx_out)| { - tx_out_to_outlay_index - .get(tx_out) - .map(|outlay_index| (*outlay_index, tx_out_index)) - }) - .collect(); - - Ok(TxProposal { - utxos: selected_utxos, - outlays: outlays_decoded.to_vec(), - tx, - outlay_index_to_tx_out_index, - outlay_confirmation_numbers, - }) - } -} - -pub fn decode_amount( - tx_out: &TxOut, - view_private_key: &RistrettoPrivate, -) -> Result<(Amount, Scalar), WalletTransactionBuilderError> { - let tx_public_key = RistrettoPublic::try_from(&tx_out.public_key)?; - let shared_secret = get_tx_out_shared_secret(view_private_key, &tx_public_key); - Ok(tx_out.masked_amount.get_value(&shared_secret)?) -} - -#[allow(clippy::type_complexity)] -fn add_payload_outputs( - outlays: &[Outlay], - transaction_builder: &mut TransactionBuilder, - rng: &mut RNG, -) -> Result<(u64, HashMap, Vec), WalletTransactionBuilderError> -{ - // Add outputs to our destinations. - let mut total_value = 0; - let mut tx_out_to_outlay_index: HashMap = HashMap::default(); - let mut outlay_confirmation_numbers = Vec::default(); - for (i, outlay) in outlays.iter().enumerate() { - let txo_context = transaction_builder.add_output(outlay.value, &outlay.receiver, rng)?; - - tx_out_to_outlay_index.insert(txo_context.tx_out, i); - outlay_confirmation_numbers.push(txo_context.confirmation); - - total_value += outlay.value; - } - Ok(( - total_value, - tx_out_to_outlay_index, - outlay_confirmation_numbers, - )) -} - -fn add_change_output( - account_key: &AccountKey, - total_input_value: u64, - total_payload_value: u64, - transaction_builder: &mut TransactionBuilder, - rng: &mut RNG, -) -> Result<(), WalletTransactionBuilderError> { - let change_value = - total_input_value - total_payload_value - transaction_builder.get_fee().value; - - let change_destination = ChangeDestination::from(account_key); - transaction_builder.add_change_output(change_value, &change_destination, rng)?; - - Ok(()) -} diff --git a/full-service/src/util/b58/mod.rs b/full-service/src/util/b58/mod.rs index 8db56729f..9912d7a95 100644 --- a/full-service/src/util/b58/mod.rs +++ b/full-service/src/util/b58/mod.rs @@ -9,12 +9,14 @@ use mc_account_keys::{AccountKey, PublicAddress, RootEntropy, RootIdentity}; use mc_account_keys_slip10::Slip10KeyGenerator; use mc_api::printable::{PaymentRequest, PrintableWrapper, TransferPayload}; use mc_crypto_keys::CompressedRistrettoPublic; +use mc_transaction_core::Amount; use serde::{Deserialize, Serialize}; use std::convert::TryFrom; pub struct DecodedPaymentRequest { pub public_address: PublicAddress, pub value: u64, + pub token_id: u64, pub memo: String, } @@ -67,12 +69,13 @@ pub fn b58_decode_public_address(public_address_b58_code: &str) -> Result Result { let mut payment_request = PaymentRequest::new(); payment_request.set_public_address(public_address.into()); - payment_request.set_value(amount_pmob); + payment_request.set_value(amount.value); + payment_request.set_token_id(*amount.token_id); payment_request.set_memo(memo); let mut wrapper = PrintableWrapper::new(); @@ -92,12 +95,14 @@ pub fn b58_decode_payment_request( }; let public_address = PublicAddress::try_from(payment_request_message.get_public_address())?; - let value = payment_request_message.get_value() as u64; + let value = payment_request_message.get_value(); + let token_id = payment_request_message.get_token_id(); let memo = payment_request_message.get_memo().to_string(); Ok(DecodedPaymentRequest { public_address, value, + token_id, memo, }) } diff --git a/full-service/src/util/b58/tests.rs b/full-service/src/util/b58/tests.rs index 2e7c3d2a2..2a851797a 100644 --- a/full-service/src/util/b58/tests.rs +++ b/full-service/src/util/b58/tests.rs @@ -45,7 +45,7 @@ mod tests { let public_address = get_public_address(&mut rng); let _encoded = b58_encode_payment_request( &public_address, - 1_000_000_000_000, + &Amount::new(1_000_000_000_000, Mob::ID), "This is a memo".to_string(), ) .unwrap(); @@ -90,7 +90,7 @@ mod tests { let public_address = get_public_address(&mut rng); let encoded = b58_encode_payment_request( &public_address, - 1_000_000_000_000, + &Amount::new(1_000_000_000_000, Mob::ID), "This is a memo".to_string(), ) .unwrap(); @@ -188,7 +188,7 @@ mod tests { let public_address = get_public_address(&mut rng); let encoded = b58_encode_payment_request( &public_address, - 1_000_000_000_000, + &Amount::new(1_000_000_000_000, Mob::ID), "This is a memo".to_string(), ) .unwrap(); diff --git a/full-service/src/util/encoding_helpers.rs b/full-service/src/util/encoding_helpers.rs index 8ce727dcc..64aa3f232 100644 --- a/full-service/src/util/encoding_helpers.rs +++ b/full-service/src/util/encoding_helpers.rs @@ -1,9 +1,13 @@ -use mc_crypto_keys::RistrettoPrivate; +use mc_crypto_keys::{RistrettoPrivate, RistrettoPublic}; pub fn ristretto_to_vec(key: &RistrettoPrivate) -> Vec { mc_util_serial::encode(key) } +pub fn ristretto_public_to_vec(key: &RistrettoPublic) -> Vec { + mc_util_serial::encode(key) +} + pub fn vec_to_hex(key: &[u8]) -> String { hex::encode(key) } @@ -17,10 +21,23 @@ pub fn vec_to_ristretto(key: &[u8]) -> Result { .map_err(|err| format!("Could not decode vector to ristretto: {:?}", err)) } +pub fn vec_to_ristretto_public(key: &[u8]) -> Result { + mc_util_serial::decode(key) + .map_err(|err| format!("Could not decode vector to ristretto public: {:?}", err)) +} + pub fn hex_to_ristretto(key: &str) -> Result { vec_to_ristretto(&hex_to_vec(key)?) } +pub fn hex_to_ristretto_public(key: &str) -> Result { + vec_to_ristretto_public(&hex_to_vec(key)?) +} + pub fn ristretto_to_hex(key: &RistrettoPrivate) -> String { vec_to_hex(&ristretto_to_vec(key)) } + +pub fn ristretto_public_to_hex(key: &RistrettoPublic) -> String { + vec_to_hex(&ristretto_public_to_vec(key)) +} diff --git a/full-service/src/validator_ledger_sync.rs b/full-service/src/validator_ledger_sync.rs index 94c1190c1..336a4fac9 100644 --- a/full-service/src/validator_ledger_sync.rs +++ b/full-service/src/validator_ledger_sync.rs @@ -2,10 +2,10 @@ //! Ledger syncing via the Validator Service. +use mc_blockchain_types::BlockData; use mc_common::logger::{log, Logger}; use mc_ledger_db::{Ledger, LedgerDB}; use mc_ledger_sync::{NetworkState, PollingNetworkState}; -use mc_transaction_core::{Block, BlockContents}; use mc_validator_api::ValidatorUri; use mc_validator_connection::ValidatorConnection; use std::{ @@ -28,6 +28,7 @@ pub struct ValidatorLedgerSyncThread { impl ValidatorLedgerSyncThread { pub fn new( validator_uri: &ValidatorUri, + chain_id: String, poll_interval: Duration, ledger_db: LedgerDB, network_state: Arc>>, @@ -35,7 +36,7 @@ impl ValidatorLedgerSyncThread { ) -> Self { let stop_requested = Arc::new(AtomicBool::new(false)); - let validator_conn = ValidatorConnection::new(validator_uri, logger.clone()); + let validator_conn = ValidatorConnection::new(validator_uri, chain_id, logger.clone()); let thread_stop_requested = stop_requested.clone(); let join_handle = Some( @@ -83,17 +84,15 @@ impl ValidatorLedgerSyncThread { break; } - let blocks_and_contents = + let block_data = Self::get_next_blocks(&ledger_db, &validator_conn, &mut network_state, &logger); - if !blocks_and_contents.is_empty() { - Self::append_safe_blocks(&mut ledger_db, &blocks_and_contents, &logger); + if !block_data.is_empty() { + Self::append_safe_blocks(&mut ledger_db, &block_data, &logger); } // If we got no blocks, or less than the amount we asked for, sleep for a bit. // Getting less the amount we asked for indicates we are fully synced. - if blocks_and_contents.is_empty() - || blocks_and_contents.len() < MAX_BLOCKS_PER_SYNC_ITERATION as usize - { + if block_data.is_empty() || block_data.len() < MAX_BLOCKS_PER_SYNC_ITERATION as usize { thread::sleep(poll_interval); } } @@ -104,7 +103,7 @@ impl ValidatorLedgerSyncThread { validator_conn: &ValidatorConnection, network_state: &mut Arc>>, logger: &Logger, - ) -> Vec<(Block, BlockContents)> { + ) -> Vec { let num_blocks = ledger_db .num_blocks() .expect("Failed getting the number of blocks in ledger"); @@ -145,33 +144,33 @@ impl ValidatorLedgerSyncThread { } }; - let blocks_and_contents: Vec<(Block, BlockContents)> = blocks_data - .into_iter() - .map(|block_data| (block_data.block().clone(), block_data.contents().clone())) - .collect(); - - mc_ledger_sync::identify_safe_blocks(ledger_db, &blocks_and_contents, logger) + mc_ledger_sync::identify_safe_blocks(ledger_db, &blocks_data, logger) } - fn append_safe_blocks( - ledger_db: &mut LedgerDB, - blocks_and_contents: &[(Block, BlockContents)], - logger: &Logger, - ) { + fn append_safe_blocks(ledger_db: &mut LedgerDB, block_data: &[BlockData], logger: &Logger) { log::info!( logger, "Appending {} blocks to ledger, which currently has {} blocks", - blocks_and_contents.len(), + block_data.len(), ledger_db .num_blocks() .expect("failed getting number of blocks"), ); - for (block, contents) in blocks_and_contents { + for block_data in block_data { ledger_db - .append_block(block, contents, None) + .append_block( + block_data.block(), + block_data.contents(), + None, + block_data.metadata(), + ) .unwrap_or_else(|err| { - panic!("Failed appending block #{} to ledger: {}", block.index, err) + panic!( + "Failed appending block #{} to ledger: {}", + block_data.block().index, + err + ) }); } } diff --git a/mobilecoin b/mobilecoin index 300614575..d8a3f550a 160000 --- a/mobilecoin +++ b/mobilecoin @@ -1 +1 @@ -Subproject commit 300614575f7e0153dd2f42e667ba39d6b8bae4f8 +Subproject commit d8a3f550a02ceae6b1d2ee1ee7dca3a765af6648 diff --git a/renovate.json b/renovate.json new file mode 100644 index 000000000..49b6bd487 --- /dev/null +++ b/renovate.json @@ -0,0 +1,31 @@ +{ + "$schema": "https://docs.renovatebot.com/renovate-schema.json", + "extends": [ + "config:base" + ], + "automergeStrategy": "squash", + "cloneSubmodules": true, + "labels": [ + "dependencies" + ], + "packageRules": [ + { + "matchUpdateTypes": [ + "minor", + "patch", + "pin", + "digest" + ], + "automerge": true + }, + { + "matchDepTypes": [ + "devDependencies" + ], + "automerge": true + } + ], + "reviewers": [ + "team:ramps-eng" + ] +} \ No newline at end of file diff --git a/tools/build-fs.sh b/tools/build-fs.sh new file mode 100755 index 000000000..1c71efc51 --- /dev/null +++ b/tools/build-fs.sh @@ -0,0 +1,55 @@ +#!/bin/bash +# Copyright (c) 2018-2022 The MobileCoin Foundation + +# To use this script, run build-fs test or build-fs main +# If no network is specified, or a different network is specified, the env's +# version of the following variables are used +# - SGX_MODE +# - IAS_MODE +# - CONSENSUS_ENCLAVE_CSS + +# Net can be main/test/local +NET="$1" + +if [ "$NET" == "test" ]; then + NAMESPACE="test" + export SGX_MODE=HW + export IAS_MODE=PROD + CONSENSUS_SIGSTRUCT_URI=$(curl -s https://enclave-distribution.${NAMESPACE}.mobilecoin.com/production.json | grep consensus-enclave.css | awk '{print $2}' | tr -d \" | tr -d ,) +elif [ "$NET" == "main" ]; then + NAMESPACE="prod" + export SGX_MODE=HW + export IAS_MODE=PROD + CONSENSUS_SIGSTRUCT_URI=$(curl -s https://enclave-distribution.${NAMESPACE}.mobilecoin.com/production.json | grep consensus-enclave.css | awk '{print $2}' | tr -d \" | tr -d ,) +elif [ "$NET" == "alpha" ]; then + NAMESPACE="alpha" + export SGX_MODE=HW + export IAS_MODE=DEV + CONSENSUS_SIGSTRUCT_URI="" +else + echo "Using current environment's SGX_MODE, IAS_MODE, CONSENSUS_ENCLAVE_CSS" + CONSENSUS_SIGSTRUCT_URI="" + if [ "$NET" == "" ]; then + NET="default" + fi +fi + +WORK_DIR="$HOME/.mobilecoin/${NET}" +CONSENSUS_DOWNLOAD_LOCATION="$WORK_DIR/consensus-enclave.css" +mkdir -p ${WORK_DIR} + +if ! test -f "$CONSENSUS_DOWNLOAD_LOCATION" && [ "$CONSENSUS_SIGSTRUCT_URI" != "" ]; then + (cd ${WORK_DIR} && curl -O https://enclave-distribution.${NAMESPACE}.mobilecoin.com/${CONSENSUS_SIGSTRUCT_URI}) +fi + +if [ -z "$CONSENSUS_ENCLAVE_CSS" ]; then + export CONSENSUS_ENCLAVE_CSS=$CONSENSUS_DOWNLOAD_LOCATION +fi + +if ! test -f "$CONSENSUS_ENCLAVE_CSS"; then + echo "Missing consensus enclave at $CONSENSUS_ENCLAVE_CSS" + exit 1 +fi + +echo "building full service..." +cargo build --release diff --git a/tools/run-alphanet.sh b/tools/run-alphanet.sh deleted file mode 100755 index 896203c93..000000000 --- a/tools/run-alphanet.sh +++ /dev/null @@ -1,16 +0,0 @@ -NAMESPACE=alpha - -WORK_DIR="$HOME/.mobilecoin/${NAMESPACE}" -WALLET_DB_DIR="${WORK_DIR}/wallet-db" -LEDGER_DB_DIR="${WORK_DIR}/ledger-db" -mkdir -p ${WORK_DIR} - -mkdir -p ${WALLET_DB_DIR} -${WORK_DIR}/full-service \ - --wallet-db ${WALLET_DB_DIR}/wallet.db \ - --ledger-db ${LEDGER_DB_DIR} \ - --peer mc://node1.alpha.development.mobilecoin.com/ \ - --peer mc://node2.alpha.development.mobilecoin.com/ \ - --tx-source-url https://s3-eu-central-1.amazonaws.com/mobilecoin.eu.development.chain/node1.alpha.development.mobilecoin.com/ \ - --tx-source-url https://s3-eu-central-1.amazonaws.com/mobilecoin.eu.development.chain/node2.alpha.development.mobilecoin.com/ \ - --fog-ingest-enclave-css ${WORK_DIR}/ingest-enclave.css diff --git a/tools/run-fs.sh b/tools/run-fs.sh new file mode 100755 index 000000000..c7c471593 --- /dev/null +++ b/tools/run-fs.sh @@ -0,0 +1,66 @@ +#!/bin/bash +# Copyright (c) 2022 The MobileCoin Foundation + +NET="$1" + +if [ "$NET" == "main" ]; then + NAMESPACE="prod" + PEER_DOMAIN="prod.mobilecoinww.com/" + TX_SOURCE_URL="https://ledger.mobilecoinww.com" + INGEST_SIGSTRUCT_URI=$(curl -s https://enclave-distribution.${NAMESPACE}.mobilecoin.com/production.json | grep ingest-enclave.css | awk '{print $2}' | tr -d \" | tr -d ,) +elif [ "$NET" == "test" ]; then + NAMESPACE=$NET + PEER_DOMAIN="test.mobilecoin.com/" + TX_SOURCE_URL="https://s3-us-west-1.amazonaws.com/mobilecoin.chain" + INGEST_SIGSTRUCT_URI=$(curl -s https://enclave-distribution.${NAMESPACE}.mobilecoin.com/production.json | grep ingest-enclave.css | awk '{print $2}' | tr -d \" | tr -d ,) +elif [ "$NET" == "alpha" ]; then + NAMESPACE=$NET + PEER_DOMAIN="alpha.development.mobilecoin.com/" + TX_SOURCE_URL="https://s3-eu-central-1.amazonaws.com/mobilecoin.eu.development.chain" + INGEST_SIGSTRUCT_URI="" +else + # TODO: add support for local network + echo "Unknown network" + echo "Usage: run-fs.sh {main|test|alpha} [--no-build]" + exit 1 +fi + +WORK_DIR="$HOME/.mobilecoin/${NET}" +WALLET_DB_DIR="${WORK_DIR}/wallet-db" +LEDGER_DB_DIR="${WORK_DIR}/ledger-db" +INGEST_DOWNLOAD_LOCATION="$WORK_DIR/ingest-enclave.css" +mkdir -p ${WORK_DIR} + + +if ! test -f "$INGEST_DOWNLOAD_LOCATION" && [ "$INGEST_SIGSTRUCT_URI" != "" ]; then + (cd ${WORK_DIR} && curl -O https://enclave-distribution.${NAMESPACE}.mobilecoin.com/${INGEST_SIGSTRUCT_URI}) +fi + +if [ -z "$INGEST_ENCLAVE_CSS" ]; then + export INGEST_ENCLAVE_CSS=$INGEST_DOWNLOAD_LOCATION +fi + +if ! test -f "$INGEST_ENCLAVE_CSS"; then + echo "Missing ingest enclave at $INGEST_ENCLAVE_CSS" + exit 1 +fi + +# Pass "--no-build" if the user just wants to run what they have in +# WORK_DIR instead of building and copying over a new exectuable +if [ "$2" != "--no-build" ]; then + echo "Building" + SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) + $SCRIPT_DIR/build-fs.sh $NET + cp $SCRIPT_DIR/../target/release/full-service $WORK_DIR +fi + +mkdir -p ${WALLET_DB_DIR} +$WORK_DIR/full-service \ + --wallet-db ${WALLET_DB_DIR}/wallet.db \ + --ledger-db ${LEDGER_DB_DIR} \ + --peer mc://node1.${PEER_DOMAIN} \ + --peer mc://node2.${PEER_DOMAIN} \ + --tx-source-url ${TX_SOURCE_URL}/node1.${PEER_DOMAIN} \ + --tx-source-url ${TX_SOURCE_URL}/node2.${PEER_DOMAIN} \ + --fog-ingest-enclave-css $INGEST_ENCLAVE_CSS \ + --chain-id $NET diff --git a/tools/run-mainnet.sh b/tools/run-mainnet.sh deleted file mode 100644 index fb8194d39..000000000 --- a/tools/run-mainnet.sh +++ /dev/null @@ -1,22 +0,0 @@ -NAMESPACE=main - -WORK_DIR="$HOME/.mobilecoin/${NAMESPACE}" -WALLET_DB_DIR="${WORK_DIR}/wallet-db" -LEDGER_DB_DIR="${WORK_DIR}/ledger-db" -mkdir -p ${WORK_DIR} - -(cd ${WORK_DIR} && CONSENSUS_SIGSTRUCT_URI=$(curl -s https://enclave-distribution.${NAMESPACE}.mobilecoin.com/production.json | grep consensus-enclave.css | awk '{print $2}' | tr -d \" | tr -d ,) -curl -O https://enclave-distribution.${NAMESPACE}.mobilecoin.com/${CONSENSUS_SIGSTRUCT_URI}) - -(cd ${WORK_DIR} && INGEST_SIGSTRUCT_URI=$(curl -s https://enclave-distribution.${NAMESPACE}.mobilecoin.com/production.json | grep ingest-enclave.css | awk '{print $2}' | tr -d \" | tr -d ,) -curl -O https://enclave-distribution.${NAMESPACE}.mobilecoin.com/${INGEST_SIGSTRUCT_URI}) - -mkdir -p ${WALLET_DB_DIR} -${WORK_DIR}/full-service \ - --wallet-db ${WALLET_DB_DIR}/wallet.db \ - --ledger-db ${LEDGER_DB_DIR} \ - --peer mc://node1.prod.mobilecoinww.com/ \ - --peer mc://node2.prod.mobilecoinww.com/ \ - --tx-source-url https://ledger.mobilecoinww.com/node1.prod.mobilecoinww.com/ \ - --tx-source-url https://ledger.mobilecoinww.com/node2.prod.mobilecoinww.com/ \ - --fog-ingest-enclave-css $(pwd)/ingest-enclave.css diff --git a/tools/run-testnet.sh b/tools/run-testnet.sh deleted file mode 100755 index 48743c4ef..000000000 --- a/tools/run-testnet.sh +++ /dev/null @@ -1,22 +0,0 @@ -NAMESPACE=test - -WORK_DIR="$HOME/.mobilecoin/${NAMESPACE}" -WALLET_DB_DIR="${WORK_DIR}/wallet-db" -LEDGER_DB_DIR="${WORK_DIR}/ledger-db" -mkdir -p ${WORK_DIR} - -(cd ${WORK_DIR} && CONSENSUS_SIGSTRUCT_URI=$(curl -s https://enclave-distribution.${NAMESPACE}.mobilecoin.com/production.json | grep consensus-enclave.css | awk '{print $2}' | tr -d \" | tr -d ,) -curl -O https://enclave-distribution.${NAMESPACE}.mobilecoin.com/${CONSENSUS_SIGSTRUCT_URI}) - -(cd ${WORK_DIR} && INGEST_SIGSTRUCT_URI=$(curl -s https://enclave-distribution.${NAMESPACE}.mobilecoin.com/production.json | grep ingest-enclave.css | awk '{print $2}' | tr -d \" | tr -d ,) -curl -O https://enclave-distribution.${NAMESPACE}.mobilecoin.com/${INGEST_SIGSTRUCT_URI}) - -mkdir -p ${WALLET_DB_DIR} -${WORK_DIR}/full-service \ - --wallet-db ${WALLET_DB_DIR}/wallet.db \ - --ledger-db ${LEDGER_DB_DIR} \ - --peer mc://node1.test.mobilecoin.com/ \ - --peer mc://node2.test.mobilecoin.com/ \ - --tx-source-url https://s3-us-west-1.amazonaws.com/mobilecoin.chain/node1.test.mobilecoin.com/ \ - --tx-source-url https://s3-us-west-1.amazonaws.com/mobilecoin.chain/node2.test.mobilecoin.com/ \ - --fog-ingest-enclave-css ${WORK_DIR}/ingest-enclave.css diff --git a/tools/test.sh b/tools/test.sh index af3f030ca..3b145e95c 100755 --- a/tools/test.sh +++ b/tools/test.sh @@ -2,6 +2,8 @@ # Copyright (c) 2018-2020 MobileCoin Inc. +# RUSTFLAGS="-C instrument-coverage" \ + set -e if [[ ! -z "$1" ]]; then @@ -9,5 +11,7 @@ if [[ ! -z "$1" ]]; then fi echo "Testing in $PWD" -SGX_MODE=SW IAS_MODE=DEV CONSENSUS_ENCLAVE_CSS=$(pwd)/consensus-enclave.css cargo test +LLVM_PROFILE_FILE="json5format-%m.profraw" \ +SGX_MODE=SW IAS_MODE=DEV CONSENSUS_ENCLAVE_CSS=$(pwd)/consensus-enclave.css \ +cargo test -p mc-full-service echo "Testing in $PWD complete." diff --git a/transaction-signer/Cargo.toml b/transaction-signer/Cargo.toml new file mode 100644 index 000000000..04e51e18d --- /dev/null +++ b/transaction-signer/Cargo.toml @@ -0,0 +1,31 @@ +[package] +name = "mc-transaction-signer" +authors = ["MobileCoin"] +version = "2.0.0" +edition = "2021" + +[[bin]] +name = "transaction-signer" +path = "src/bin/main.rs" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +mc-account-keys = { path = "../mobilecoin/account-keys" } +mc-account-keys-slip10 = { path = "../mobilecoin/account-keys/slip10" } +mc-common = { path = "../mobilecoin/common", default-features = false, features = ["loggers"] } +mc-crypto-keys = { path = "../mobilecoin/crypto/keys", default-features = false } +mc-crypto-ring-signature-signer = { path = "../mobilecoin/crypto/ring-signature/signer" } +mc-transaction-core = { path = "../mobilecoin/transaction/core" } +mc-transaction-std = { path = "../mobilecoin/transaction/std" } +mc-util-serial = { path = "../mobilecoin/util/serial", default-features = false } + +mc-full-service = { path = "../full-service" } + +base64 = "0.13.0" +hex = {version = "0.4", default-features = false } +rand = { version = "0.8", default-features = false } +serde = { version = "1.0", default-features = false, features = ["alloc", "derive"] } +serde_json = { version = "1.0", features = ["preserve_order"] } +structopt = "0.3" +tiny-bip39 = "1.0" diff --git a/full-service/src/bin/transaction-signer.rs b/transaction-signer/src/bin/main.rs similarity index 64% rename from full-service/src/bin/transaction-signer.rs rename to transaction-signer/src/bin/main.rs index 0497cbe53..38b5d2a38 100644 --- a/full-service/src/bin/transaction-signer.rs +++ b/transaction-signer/src/bin/main.rs @@ -1,32 +1,34 @@ use bip39::{Language, Mnemonic, MnemonicType}; -use mc_account_keys::{AccountKey, CHANGE_SUBADDRESS_INDEX, DEFAULT_SUBADDRESS_INDEX}; +use mc_account_keys::AccountKey; use mc_account_keys_slip10::Slip10Key; -use mc_common::{HashMap, HashSet}; +use mc_common::HashMap; +use mc_crypto_keys::{RistrettoPrivate, RistrettoPublic}; use mc_full_service::{ db::{account::AccountID, txo::TxoID}, - fog_resolver::FullServiceFogResolver, json_rpc::{ - account_key::AccountKey as AccountKeyJSON, - account_secrets::AccountSecrets, - json_rpc_request::{JsonCommandRequest, JsonRPCRequest}, - tx_proposal::TxProposal, - view_only_account::{ViewOnlyAccountJSON, ViewOnlyAccountSecretsJSON}, - view_only_subaddress::ViewOnlySubaddressJSON, + json_rpc_request::JsonRPCRequest, + v2::{ + api::request::JsonCommandRequest, + models::{ + account_key::AccountKey as AccountKeyJSON, + account_secrets::AccountSecrets, + tx_proposal::{ + TxProposal as TxProposalJSON, UnsignedTxProposal as UnsignedTxProposalJSON, + }, + }, + }, }, - unsigned_tx::UnsignedTx, - util::b58, + service::models::tx_proposal::UnsignedTxProposal, + util::encoding_helpers::{ristretto_public_to_hex, ristretto_to_hex}, }; -use std::{convert::TryFrom, fs}; -use structopt::StructOpt; - -use mc_crypto_keys::{RistrettoPrivate, RistrettoPublic}; - use mc_transaction_core::{ get_tx_out_shared_secret, onetime_keys::{recover_onetime_private_key, recover_public_subaddress_spend_key}, ring_signature::KeyImage, tx::TxOut, }; +use std::{convert::TryFrom, fs}; +use structopt::StructOpt; #[derive(Clone, Debug, StructOpt)] #[structopt( @@ -38,16 +40,17 @@ enum Opts { #[structopt(short, long)] name: Option, }, + Import { + #[structopt(short, long)] + name: Option, + mnemonic: String, + }, r#Sync { secret_mnemonic: String, sync_request: String, #[structopt(short, long, default_value = "1000")] subaddresses: u64, }, - Subaddresses { - secret_mnemonic: String, - request: String, - }, Sign { secret_mnemonic: String, request: String, @@ -63,7 +66,11 @@ fn main() { match opts { Opts::Create { ref name } => { let name = name.clone().unwrap_or_else(|| "".into()); - create_account(&name); + create_account(&name, None); + } + Opts::Import { mnemonic, name } => { + let name = name.unwrap_or_else(|| "".into()); + create_account(&name, Some(&mnemonic)); } Opts::ViewOnlyImportPackage { ref secret_mnemonic, @@ -77,12 +84,6 @@ fn main() { } => { sync_txos(secret_mnemonic, sync_request, subaddresses); } - Opts::Subaddresses { - ref secret_mnemonic, - ref request, - } => { - generate_subaddresses(secret_mnemonic, request); - } Opts::Sign { ref secret_mnemonic, ref request, @@ -92,11 +93,13 @@ fn main() { } } -fn create_account(name: &str) { +fn create_account(name: &str, mnemonic: Option<&str>) { println!("Creating account {}", name); - // Generate new seed mnemonic. - let mnemonic = Mnemonic::new(MnemonicType::Words24, Language::English); + let mnemonic = match mnemonic { + Some(mnemonic) => Mnemonic::from_phrase(mnemonic, Language::English).unwrap(), + None => Mnemonic::new(MnemonicType::Words24, Language::English), + }; let fog_report_url = "".to_string(); let fog_report_id = "".to_string(); @@ -111,13 +114,13 @@ fn create_account(name: &str) { let account_id = AccountID::from(&account_key); let secrets = AccountSecrets { - object: "account_secrets".to_string(), account_id: account_id.to_string(), entropy: None, mnemonic: Some(mnemonic.phrase().to_string()), key_derivation_version: "2".to_string(), - account_key: AccountKeyJSON::from(&account_key), + account_key: Some(AccountKeyJSON::from(&account_key)), name: name.to_string(), + view_account_key: None, }; // Write secret mnemonic to file. @@ -140,34 +143,16 @@ fn generate_view_only_import_package(secret_mnemonic: &str) { let account_key = account_key_from_mnemonic_phrase(&account_secrets.mnemonic.unwrap()); let account_id = AccountID::from(&account_key); - // Package view private key. - let account_json = ViewOnlyAccountJSON { - object: "view_only_account".to_string(), - name: account_secrets.name, - account_id: account_id.to_string(), - first_block_index: 0.to_string(), - next_block_index: 0.to_string(), - main_subaddress_index: DEFAULT_SUBADDRESS_INDEX.to_string(), - change_subaddress_index: CHANGE_SUBADDRESS_INDEX.to_string(), - next_subaddress_index: 2.to_string(), - }; - - let account_secrets_json = ViewOnlyAccountSecretsJSON { - object: "view_only_account_secrets".to_string(), - view_private_key: hex::encode(mc_util_serial::encode(account_key.view_private_key())), - account_id: account_id.to_string(), - }; - - // Generate main and change subaddresses. - let initial_subaddresses = vec![ - subaddress_json(&account_key, DEFAULT_SUBADDRESS_INDEX, "Main"), - subaddress_json(&account_key, CHANGE_SUBADDRESS_INDEX, "Change"), - ]; + let view_private_key_hex = ristretto_to_hex(account_key.view_private_key()); + let spend_public_key = RistrettoPublic::from(account_key.spend_private_key()); + let spend_public_key_hex = ristretto_public_to_hex(&spend_public_key); let json_command_request = JsonCommandRequest::import_view_only_account { - account: account_json, - secrets: account_secrets_json, - subaddresses: initial_subaddresses, + view_private_key: view_private_key_hex, + spend_public_key: spend_public_key_hex, + name: None, + first_block_index: None, + next_subaddress_index: None, }; // Write view private key and associated info to file. @@ -217,12 +202,6 @@ fn sync_txos(secret_mnemonic: &str, sync_request: &str, num_subaddresses: u64) { let txos_and_key_images = get_key_images_for_txos(&input_txos, &account_key, &subaddress_spend_public_keys); - let subaddress_indices: HashSet = txos_and_key_images.iter().map(|(_, _, i)| *i).collect(); - let related_subaddresses: Vec<_> = subaddress_indices - .iter() - .map(|i| subaddress_json(&account_key, *i, "")) - .collect(); - let completed_txos: Vec<(String, String)> = txos_and_key_images .iter() .map(|(txo, key_image, _)| { @@ -236,7 +215,7 @@ fn sync_txos(secret_mnemonic: &str, sync_request: &str, num_subaddresses: u64) { let json_command_request = JsonCommandRequest::sync_view_only_account { account_id: account_id.to_string(), completed_txos, - subaddresses: related_subaddresses, + next_subaddress_index: "0".to_string(), }; // Write result to file. @@ -244,90 +223,48 @@ fn sync_txos(secret_mnemonic: &str, sync_request: &str, num_subaddresses: u64) { write_json_command_request_to_file(&json_command_request, &filename); } -fn generate_subaddresses(secret_mnemonic: &str, request: &str) { +fn sign_transaction(secret_mnemonic: &str, sign_request: &str) { // Load account key. let mnemonic_json = fs::read_to_string(secret_mnemonic).expect("Could not open secret mnemonic file."); let account_secrets: AccountSecrets = serde_json::from_str(&mnemonic_json).unwrap(); let account_key = account_key_from_mnemonic_phrase(&account_secrets.mnemonic.unwrap()); - // Load input txos. - let request_data = - fs::read_to_string(request).expect("Could not open generate subaddresses request file."); - let request_json: serde_json::Value = - serde_json::from_str(&request_data).expect("Malformed generate subaddresses request."); - let account_id = request_json.get("account_id").unwrap().as_str().unwrap(); - assert_eq!(account_secrets.account_id, account_id); + // let signer = LocalRingSigner::from(&account_key); + // let mut rng = rand::thread_rng(); - let next_subaddress_index = request_json - .get("next_subaddress_index") - .unwrap() - .as_str() - .unwrap() - .parse::() - .unwrap(); - - let num_subaddresses_to_generate = request_json - .get("num_subaddresses_to_generate") - .unwrap() - .as_str() - .unwrap() - .parse::() - .unwrap(); - - let mut subaddresses: Vec = Vec::new(); - for i in next_subaddress_index..next_subaddress_index + num_subaddresses_to_generate { - subaddresses.push(subaddress_json(&account_key, i, "")); - } - - let json_command_request = JsonCommandRequest::import_subaddresses_to_view_only_account { - account_id: account_id.to_string(), - subaddresses, - }; - let filename = format!("{}_completed.json", request.trim_end_matches(".json")); - write_json_command_request_to_file(&json_command_request, &filename); -} - -fn sign_transaction(secret_mnemonic: &str, request: &str) { - // Load account key. - let mnemonic_json = - fs::read_to_string(secret_mnemonic).expect("Could not open secret mnemonic file."); - let account_secrets: AccountSecrets = serde_json::from_str(&mnemonic_json).unwrap(); - let account_key = account_key_from_mnemonic_phrase(&account_secrets.mnemonic.unwrap()); - - // Load input txos. + // // Load input txos. let request_data = - fs::read_to_string(request).expect("Could not open generate subaddresses request file."); - let request_json: serde_json::Value = - serde_json::from_str(&request_data).expect("Malformed generate subaddresses request."); + fs::read_to_string(sign_request).expect("Could not open generate signing request file."); + let request_json: serde_json::Value = serde_json::from_str(&request_data).expect( + "Malformed generate signing + request.", + ); let account_id = request_json.get("account_id").unwrap().as_str().unwrap(); assert_eq!(account_secrets.account_id, account_id); - let unsigned_tx: UnsignedTx = serde_json::from_value( + let unsigned_tx_proposal_json: UnsignedTxProposalJSON = serde_json::from_value( request_json - .get("unsigned_tx") - .expect("Could not find \"unsigned_tx\".") + .get("unsigned_tx_proposal") + .expect("Could not find \"unsigned_tx_proposal\".") .clone(), ) .unwrap(); - let fog_resolver: FullServiceFogResolver = serde_json::from_value( - request_json - .get("fog_resolver") - .expect("Could not find \"fog_resolver\".") - .clone(), - ) - .unwrap(); + let unsigned_tx_proposal: UnsignedTxProposal = unsigned_tx_proposal_json.try_into().unwrap(); - let tx_proposal = unsigned_tx.sign(&account_key, fog_resolver).unwrap(); - let tx_proposal_json = TxProposal::try_from(&tx_proposal).unwrap(); + let tx_proposal = unsigned_tx_proposal.sign(&account_key).unwrap(); + let tx_proposal_json = TxProposalJSON::try_from(&tx_proposal).unwrap(); let json_command_request = JsonCommandRequest::submit_transaction { tx_proposal: tx_proposal_json, comment: None, account_id: Some(account_id.to_string()), }; - let filename = format!("{}_completed.json", request.trim_end_matches(".json")); + let filename = format!( + "{}_completed.json", + sign_request.trim_end_matches("_unsigned.json") + ); write_json_command_request_to_file(&json_command_request, &filename); } @@ -413,10 +350,12 @@ fn tx_out_belongs_to_account(tx_out: &TxOut, account_view_private_key: &Ristrett Err(_) => return false, Ok(k) => k, }; - let shared_secret = get_tx_out_shared_secret(account_view_private_key, &tx_out_public_key); - - tx_out.masked_amount.get_value(&shared_secret).is_ok() + tx_out + .get_masked_amount() + .unwrap() + .get_value(&shared_secret) + .is_ok() } fn generate_subaddress_spend_public_keys( @@ -425,7 +364,9 @@ fn generate_subaddress_spend_public_keys( ) -> HashMap { let mut subaddress_spend_public_keys = HashMap::default(); - for i in 0..number_to_generate { + let mut subaddresses: Vec = (0..number_to_generate).collect(); + subaddresses.push(mc_account_keys::CHANGE_SUBADDRESS_INDEX); + for i in subaddresses.into_iter() { let subaddress_spend_private_key = account_key.subaddress_spend_private(i); let subaddress_spend_public_key = RistrettoPublic::from(&subaddress_spend_private_key); subaddress_spend_public_keys.insert(subaddress_spend_public_key, i); @@ -433,16 +374,3 @@ fn generate_subaddress_spend_public_keys( subaddress_spend_public_keys } - -fn subaddress_json(account_key: &AccountKey, index: u64, comment: &str) -> ViewOnlySubaddressJSON { - let account_id = AccountID::from(account_key); - let subaddress = account_key.subaddress(index); - ViewOnlySubaddressJSON { - object: "view_only_subaddress".to_string(), - public_address: b58::b58_encode_public_address(&subaddress).unwrap(), - account_id: account_id.to_string(), - comment: comment.to_string(), - subaddress_index: index.to_string(), - public_spend_key: hex::encode(mc_util_serial::encode(subaddress.spend_public_key())), - } -} diff --git a/validator/api/Cargo.toml b/validator/api/Cargo.toml index 51d3d32a5..50bf4bb66 100644 --- a/validator/api/Cargo.toml +++ b/validator/api/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "mc-validator-api" -version = "1.0.0" +version = "2.0.0" authors = ["MobileCoin"] build = "build.rs" edition = "2018" @@ -13,8 +13,8 @@ mc-fog-report-api = { path = "../../mobilecoin/fog/report/api" } mc-util-uri = { path = "../../mobilecoin/util/uri" } futures = "0.3" -grpcio = "0.10.2" -protobuf = "2.22.1" +grpcio = "0.10.3" +protobuf = "2.28.0" [build-dependencies] mc-util-build-grpc = { path = "../../mobilecoin/util/build/grpc" } diff --git a/validator/connection/Cargo.toml b/validator/connection/Cargo.toml index d2e172101..5c2db44c2 100644 --- a/validator/connection/Cargo.toml +++ b/validator/connection/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "mc-validator-connection" -version = "1.0.0" +version = "2.0.0" authors = ["MobileCoin"] edition = "2018" @@ -8,14 +8,15 @@ edition = "2018" mc-validator-api = { path = "../api" } mc-api = { path = "../../mobilecoin/api" } +mc-blockchain-types = { path = "../../mobilecoin/blockchain/types" } mc-common = { path = "../../mobilecoin/common", features = ["log"] } mc-connection = { path = "../../mobilecoin/connection" } -mc-fog-report-validation = { path = "../../mobilecoin/fog/report/validation" } +mc-fog-report-types = { path = "../../mobilecoin/fog/report/types" } mc-transaction-core = { path = "../../mobilecoin/transaction/core" } mc-util-grpc = { path = "../../mobilecoin/util/grpc" } mc-util-uri = { path = "../../mobilecoin/util/uri" } displaydoc = {version = "0.2", default-features = false } futures = "0.3" -grpcio = "0.10.2" -protobuf = "2.22.1" +grpcio = "0.10.3" +protobuf = "2.28.0" diff --git a/validator/connection/src/lib.rs b/validator/connection/src/lib.rs index dd66994be..cf87fb70d 100644 --- a/validator/connection/src/lib.rs +++ b/validator/connection/src/lib.rs @@ -4,15 +4,16 @@ mod error; -use grpcio::{ChannelBuilder, EnvBuilder}; +use grpcio::{CallOption, ChannelBuilder, EnvBuilder, MetadataBuilder}; +use mc_blockchain_types::{Block, BlockData, BlockID, BlockIndex}; use mc_common::logger::{log, Logger}; use mc_connection::{ BlockInfo, BlockchainConnection, Connection, Error as ConnectionError, Result as ConnectionResult, UserTxConnection, }; -use mc_fog_report_validation::FogReportResponses; -use mc_transaction_core::{tx::Tx, Block, BlockData, BlockID, BlockIndex}; -use mc_util_grpc::ConnectionUriGrpcioChannel; +use mc_fog_report_types::FogReportResponses; +use mc_transaction_core::tx::Tx; +use mc_util_grpc::{ConnectionUriGrpcioChannel, CHAIN_ID_GRPC_HEADER}; use mc_util_uri::{ConnectionUri, FogUri}; use mc_validator_api::{ blockchain::ArchiveBlock, @@ -35,16 +36,33 @@ use std::{ pub use error::Error; +/// Helper which creates a grpcio CallOption with "common" headers attached +/// TODO copied from `mobilecoin/util/grpc/src/lib.rs`, should be removed +/// once upreved. +pub fn common_headers_call_option(chain_id: &str) -> CallOption { + let mut metadata_builder = MetadataBuilder::new(); + + // Add the chain id header if we have a chain id specified + if !chain_id.is_empty() { + metadata_builder + .add_str(CHAIN_ID_GRPC_HEADER, chain_id) + .expect("Could not add chain-id header"); + } + + CallOption::default().headers(metadata_builder.build()) +} + #[derive(Clone)] pub struct ValidatorConnection { uri: ValidatorUri, validator_api_client: ValidatorApiClient, blockchain_api_client: BlockchainApiClient, + chain_id: String, logger: Logger, } impl ValidatorConnection { - pub fn new(uri: &ValidatorUri, logger: Logger) -> Self { + pub fn new(uri: &ValidatorUri, chain_id: String, logger: Logger) -> Self { let env = Arc::new(EnvBuilder::new().name_prefix("ValidatorRPC").build()); let ch = ChannelBuilder::new(env) .max_receive_message_len(std::i32::MAX) @@ -58,6 +76,7 @@ impl ValidatorConnection { uri: uri.clone(), validator_api_client, blockchain_api_client, + chain_id, logger, } } @@ -69,7 +88,7 @@ impl ValidatorConnection { let response = self .validator_api_client - .get_archive_blocks(&request) + .get_archive_blocks_opt(&request, common_headers_call_option(&self.chain_id)) .map_err(|err| { log::warn!( self.logger, @@ -100,7 +119,7 @@ impl ValidatorConnection { let response = self .validator_api_client - .fetch_fog_report(&request) + .fetch_fog_report_opt(&request, common_headers_call_option(&self.chain_id)) .map_err(|err| { log::warn!( self.logger, @@ -200,7 +219,7 @@ impl BlockchainConnection for ValidatorConnection { fn fetch_block_height(&mut self) -> ConnectionResult { let response = self .blockchain_api_client - .get_last_block_info(&Empty::new()) + .get_last_block_info_opt(&Empty::new(), common_headers_call_option(&self.chain_id)) .map_err(|err| { log::warn!( self.logger, @@ -216,7 +235,7 @@ impl BlockchainConnection for ValidatorConnection { fn fetch_block_info(&mut self) -> ConnectionResult { let response = self .blockchain_api_client - .get_last_block_info(&Empty::new()) + .get_last_block_info_opt(&Empty::new(), common_headers_call_option(&self.chain_id)) .map_err(|err| { log::warn!( self.logger, @@ -233,7 +252,7 @@ impl UserTxConnection for ValidatorConnection { fn propose_tx(&mut self, tx: &Tx) -> ConnectionResult { let response = self .validator_api_client - .propose_tx(&tx.into()) + .propose_tx_opt(&tx.into(), common_headers_call_option(&self.chain_id)) .map_err(|err| { log::warn!(self.logger, "validator propose_tx RPC call failed: {}", err); err @@ -241,7 +260,10 @@ impl UserTxConnection for ValidatorConnection { if response.get_result() == ProposeTxResult::Ok { Ok(response.get_block_count()) } else { - Err(response.get_result().into()) + Err(ConnectionError::TransactionValidation( + response.get_result(), + response.get_err_msg().to_owned(), + )) } } } diff --git a/validator/service/Cargo.toml b/validator/service/Cargo.toml index d84618642..0e3c7c5d3 100644 --- a/validator/service/Cargo.toml +++ b/validator/service/Cargo.toml @@ -1,12 +1,12 @@ [package] name = "mc-validator-service" -version = "1.0.0" +version = "2.0.0" authors = ["MobileCoin"] edition = "2018" license = "GPL-3.0" [[bin]] -name = "mc-validator-service" +name = "validator-service" path = "src/bin/main.rs" [dependencies] @@ -25,6 +25,6 @@ mc-util-grpc = { path = "../../mobilecoin/util/grpc" } mc-util-parse = { path = "../../mobilecoin/util/parse" } mc-util-uri = { path = "../../mobilecoin/util/uri" } -grpcio = "0.10.2" +grpcio = "0.10.3" structopt = "0.3" rayon = "1.5" diff --git a/validator/service/src/bin/main.rs b/validator/service/src/bin/main.rs index 4c76411d1..a895fc840 100644 --- a/validator/service/src/bin/main.rs +++ b/validator/service/src/bin/main.rs @@ -85,7 +85,13 @@ fn main() { ); // Start GRPC service. - let _service = Service::new(&config.listen_uri, ledger_db, peer_manager, logger); + let _service = Service::new( + &config.listen_uri, + config.peers_config.chain_id, + ledger_db, + peer_manager, + logger, + ); // Sleep indefinitely. loop { diff --git a/validator/service/src/blockchain_api.rs b/validator/service/src/blockchain_api.rs index 3be14890c..56e1bff09 100644 --- a/validator/service/src/blockchain_api.rs +++ b/validator/service/src/blockchain_api.rs @@ -7,13 +7,14 @@ use mc_common::logger::Logger; use mc_connection::{BlockchainConnection, ConnectionManager, RetryableBlockchainConnection}; use mc_ledger_db::{Ledger, LedgerDB}; use mc_transaction_core::{tokens::Mob, Token}; -use mc_util_grpc::{rpc_database_err, rpc_logger, send_result}; +use mc_util_grpc::{rpc_database_err, rpc_logger, rpc_precondition_error, send_result}; use mc_validator_api::{ consensus_common::{BlocksRequest, BlocksResponse, LastBlockInfoResponse}, consensus_common_grpc::{create_blockchain_api, BlockchainApi as GrpcBlockchainApi}, empty::Empty, }; use rayon::prelude::*; // For par_iter +use std::{collections::HashMap, iter::FromIterator}; pub struct BlockchainApi { /// Ledger DB. @@ -53,26 +54,72 @@ impl BlockchainApi { &self, logger: &Logger, ) -> Result { - let num_blocks = self + let latest_local_block = self .ledger_db - .num_blocks() + .get_latest_block() .map_err(|err| rpc_database_err(err, logger))?; - let mut resp = LastBlockInfoResponse::new(); - resp.set_index(num_blocks - 1); - - // Iterate an owned list of connections in parallel, get the block info for - // each, and extract the fee. If no fees are returned, use the hard-coded - // minimum. - let minimum_fee = self + // Get the last block information from all nodes we are aware of, in parallel. + let last_block_infos = self .conn_manager .conns() .par_iter() .filter_map(|conn| conn.fetch_block_info(std::iter::empty()).ok()) - .filter_map(|block_info| block_info.minimum_fee_or_none(&Mob::ID)) - .max() - .unwrap_or(Mob::MINIMUM_FEE); - resp.set_mob_minimum_fee(minimum_fee); + .collect::>(); + + // Must have at least one node to get the last block info from. + let latest_network_block = last_block_infos.first().ok_or_else(|| { + rpc_precondition_error( + "last_block_infos", + "No last block information available", + logger, + ) + })?; + + // Ensure that all nodes agree on the minimum fee map. + if last_block_infos + .windows(2) + .any(|window| window[0].minimum_fees != window[1].minimum_fees) + { + return Err(rpc_precondition_error( + "minimum_fees", + "Some nodes do not agree on the minimum fees", + logger, + )); + } + + let mut resp = LastBlockInfoResponse::new(); + + // It's possible the network is at a higher block index than we are, but until + // we have fully synced to that block index, there is no point in + // reporting it to full-service since we won't have block data for these blocks + // untill we have caught up. + resp.set_index(latest_local_block.index); + + // In theory the network could be at a higher block version than we are, but + // this is an intermittent issue and will resolve itself once we are + // fully synced. The alternative would've been to try and get the block + // version from the last_block_infos array, but it is possible to run + // into an edgecase where not all nodes agree on the block version (which could + // happen at a very brief period when a new version is being enabled). + // Simply choosing the max block version will allow a malicious node to poison + // us, so we choose not to worry about any of that and instead use the + // local ledger as the source of truth. + resp.set_network_block_version(latest_local_block.version); + + // Use minimum fee information from the network (which we previously verified + // all nodes agree on). + resp.set_mob_minimum_fee( + latest_network_block + .minimum_fee_or_none(&Mob::ID) + .unwrap_or(Mob::MINIMUM_FEE), + ); + resp.set_minimum_fees(HashMap::from_iter( + latest_network_block + .minimum_fees + .iter() + .map(|(token_id, fee)| (**token_id, *fee)), + )); Ok(resp) } diff --git a/validator/service/src/service.rs b/validator/service/src/service.rs index f7bb440bd..f09304bcc 100644 --- a/validator/service/src/service.rs +++ b/validator/service/src/service.rs @@ -19,6 +19,7 @@ pub struct Service { impl Service { pub fn new( listen_uri: &ValidatorUri, + chain_id: String, ledger_db: LedgerDB, conn_manager: ConnectionManager, logger: Logger, @@ -30,9 +31,13 @@ impl Service { let health_service = HealthService::new(None, logger.clone()).into_service(); // Validator API service. - let validator_service = - ValidatorApi::new(ledger_db.clone(), conn_manager.clone(), logger.clone()) - .into_service(); + let validator_service = ValidatorApi::new( + chain_id, + ledger_db.clone(), + conn_manager.clone(), + logger.clone(), + ) + .into_service(); // Blockchain API service. let blockchain_service = diff --git a/validator/service/src/validator_api.rs b/validator/service/src/validator_api.rs index 48f632e0a..42ee4c23e 100644 --- a/validator/service/src/validator_api.rs +++ b/validator/service/src/validator_api.rs @@ -11,8 +11,8 @@ use mc_connection::{ use mc_fog_report_connection::{Error as FogConnectionError, GrpcFogReportConnection}; use mc_ledger_db::{Ledger, LedgerDB}; use mc_util_grpc::{ - rpc_database_err, rpc_internal_error, rpc_invalid_arg_error, rpc_logger, rpc_permissions_error, - send_result, + check_request_chain_id, rpc_database_err, rpc_internal_error, rpc_invalid_arg_error, + rpc_logger, rpc_permissions_error, send_result, }; use mc_util_uri::FogUri; use mc_validator_api::{ @@ -48,6 +48,9 @@ pub struct ValidatorApi { /// Fog report connection. fog_report_connection: GrpcFogReportConnection, + /// Chain id. + chain_id: String, + /// Logger. logger: Logger, } @@ -59,18 +62,25 @@ impl Clone for ValidatorApi { conn_manager: self.conn_manager.clone(), submit_node_offset: self.submit_node_offset.clone(), fog_report_connection: self.fog_report_connection.clone(), + chain_id: self.chain_id.clone(), logger: self.logger.clone(), } } } impl ValidatorApi { - pub fn new(ledger_db: LedgerDB, conn_manager: ConnectionManager, logger: Logger) -> Self { + pub fn new( + chain_id: String, + ledger_db: LedgerDB, + conn_manager: ConnectionManager, + logger: Logger, + ) -> Self { Self { ledger_db, conn_manager, submit_node_offset: Arc::new(AtomicUsize::new(0)), fog_report_connection: GrpcFogReportConnection::new( + chain_id.clone(), Arc::new( EnvBuilder::new() .name_prefix("FogReportGrpc".to_string()) @@ -78,6 +88,7 @@ impl ValidatorApi { ), logger.clone(), ), + chain_id, logger, } } @@ -157,8 +168,8 @@ impl ValidatorApi { Err(RetryError::Operation { error, .. }) => { match error { - ConnectionError::TransactionValidation(err) => { - result.set_result(err.into()); + ConnectionError::TransactionValidation(err, _) => { + result.set_result(err); Ok(()) } @@ -221,6 +232,15 @@ impl ValidatorApi { )), } } + + /// Check the chain-id, if available. + fn maybe_check_request_chain_id(&self, ctx: &RpcContext) -> Result<(), RpcStatus> { + if self.chain_id.is_empty() { + return Ok(()); + } + + check_request_chain_id(&self.chain_id, ctx) + } } impl GrpcValidatorApi for ValidatorApi { @@ -231,6 +251,10 @@ impl GrpcValidatorApi for ValidatorApi { sink: UnarySink, ) { mc_common::logger::scoped_global_logger(&rpc_logger(&ctx, &self.logger), |logger| { + if let Err(err) = self.maybe_check_request_chain_id(&ctx) { + return send_result(ctx, sink, Err(err), &self.logger); + } + send_result( ctx, sink, @@ -242,6 +266,10 @@ impl GrpcValidatorApi for ValidatorApi { fn propose_tx(&mut self, ctx: RpcContext, request: Tx, sink: UnarySink) { mc_common::logger::scoped_global_logger(&rpc_logger(&ctx, &self.logger), |logger| { + if let Err(err) = self.maybe_check_request_chain_id(&ctx) { + return send_result(ctx, sink, Err(err), &self.logger); + } + send_result(ctx, sink, self.propose_tx_impl(request, logger), logger) }) } @@ -253,6 +281,10 @@ impl GrpcValidatorApi for ValidatorApi { sink: UnarySink, ) { mc_common::logger::scoped_global_logger(&rpc_logger(&ctx, &self.logger), |logger| { + if let Err(err) = self.maybe_check_request_chain_id(&ctx) { + return send_result(ctx, sink, Err(err), &self.logger); + } + send_result( ctx, sink,