diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..64d4c20 --- /dev/null +++ b/.gitignore @@ -0,0 +1,46 @@ +.idea + +# Compiled class file +*.class + +# Log file +*.log + +# BlueJ files +*.ctxt + +# Mobile Tools for Java (J2ME) +.mtj.tmp/ + +# Package Files # +*.jar +*.war +*.nar +*.ear +*.zip +*.tar.gz +*.rar + +# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml +hs_err_pid* + +# Files generated by build, execution +cli-examples/*.iml +java-examples/*.iml +java-examples/build +java-examples/.gradle +java-examples/src/main/proto/* +proto-downloader/*.iml +python-examples/*.iml +python-examples/bisq/api/__pycache__/ +python-examples/bisq/api/*.py +python-examples/bots/__pycache__ +python-examples/bots/events/__pycache__ +python-examples/proto/* +reference-doc-builder/*.iml +reference-doc-builder/build +reference-doc-builder/.gradle +scratch +*/out +index.html.md +测试文档.md diff --git a/README.md b/README.md new file mode 100644 index 0000000..d33a046 --- /dev/null +++ b/README.md @@ -0,0 +1,54 @@ +# Bisq API Reference Doc Generator + +[Produces Bisq API Reference Doc.](https://bisq-network.github.io/slate) + +## What is bisq-api-reference? + +This application script consumes Bisq .proto files (`pb.proto` and `grpc.proto`), and produces +`index.html.md` -- a [Slate](https://github.com/slatedocs/slate) compatible Markdown file. + +_Note: For Python proto generation to work properly, the .proto files must be copied to the project's proto +directory from the remote or local GitHub repo. They are not versioned in this project repo._ + +The Markdown file is then manually deployed to a developer's or bisq-network's fork of the +[Slate repository](https://github.com/slatedocs/slate), where the slate fork transforms it to a single html file +describing Bisq gRPC API services, with example code. + +## Usage + +1. [Fork Slate](https://github.com/slatedocs/slate) + + +2. Run `BisqApiDocMain`: + +```asciidoc +BisqApiDocMain --protosIn=/proto + \ --markdownOut=/source + \ --failOnMissingDocumentation=false +``` + +3. Commit changes in your local slate/source file (`index.html.md`) _to your forked slate repo_. + +```asciidoc +git commit -a -m "Update index.html.md" +``` + +4. Run slate's `deploy.sh` script + + +5. After a few minutes, see changes on in your forked slate's github pages site. For example,
+ if your `index.html.md` file was deployed to bisq-network, the URL will be + https://bisq-network.github.io/slate. + + +## Credits + +Credit to [Juntao Han](https://github.com/mstao) for making his [markdown4j](https://github.com/mstao/markdown4j) +API available for use in this project. His source code is included in this project, modified in ways to make +generated markdown blocks compatible with Slate/GitHub style markdown. + +Lombok annotations are also replaced by the implementations they would generate if that had worked in my +gradle development setup. + + + diff --git a/cli-examples/CancelOffer.sh b/cli-examples/CancelOffer.sh new file mode 100755 index 0000000..4d8ae2c --- /dev/null +++ b/cli-examples/CancelOffer.sh @@ -0,0 +1,4 @@ +#!/bin/bash +source "env.sh" +$BISQ_HOME/bisq-cli --password=xyz --port=9998 canceloffer \ + --offer-id=83e8b2e2-51b6-4f39-a748-3ebd29c22aea diff --git a/cli-examples/CloseTrade.sh b/cli-examples/CloseTrade.sh new file mode 100755 index 0000000..ed61ccc --- /dev/null +++ b/cli-examples/CloseTrade.sh @@ -0,0 +1,3 @@ +#!/bin/bash +source "env.sh" +$BISQ_HOME/bisq-cli --password=xyz --port=9998 closetrade --trade-id=83e8b2e2-51b6-4f39-a748-3ebd29c22aea diff --git a/cli-examples/ConfirmPaymentReceived.sh b/cli-examples/ConfirmPaymentReceived.sh new file mode 100755 index 0000000..f04e30d --- /dev/null +++ b/cli-examples/ConfirmPaymentReceived.sh @@ -0,0 +1,5 @@ +#!/bin/bash +source "env.sh" +# Send message to BTC buyer that payment has been received. +$BISQ_HOME/bisq-cli --password=xyz --port=9998 confirmpaymentreceived \ + --trade-id=83e8b2e2-51b6-4f39-a748-3ebd29c22aea diff --git a/cli-examples/ConfirmPaymentStarted.sh b/cli-examples/ConfirmPaymentStarted.sh new file mode 100755 index 0000000..9b30f87 --- /dev/null +++ b/cli-examples/ConfirmPaymentStarted.sh @@ -0,0 +1,5 @@ +#!/bin/bash +source "env.sh" +# Send message to BTC seller that payment has been sent. +$BISQ_HOME/bisq-cli --password=xyz --port=9998 confirmpaymentstarted \ + --trade-id=83e8b2e2-51b6-4f39-a748-3ebd29c22aea diff --git a/cli-examples/CreateBsqSwapOffer.sh b/cli-examples/CreateBsqSwapOffer.sh new file mode 100755 index 0000000..523df40 --- /dev/null +++ b/cli-examples/CreateBsqSwapOffer.sh @@ -0,0 +1,9 @@ +#!/bin/bash +source "env.sh" +$BISQ_HOME/bisq-cli --password=xyz --port=9998 createoffer \ + --swap=true \ + --direction=BUY \ + --currency-code=BSQ \ + --amount=0.50 \ + --min-amount=0.25 \ + --fixed-price=0.00005 diff --git a/cli-examples/CreateCryptoCurrencyPaymentAccount.sh b/cli-examples/CreateCryptoCurrencyPaymentAccount.sh new file mode 100755 index 0000000..eff94e8 --- /dev/null +++ b/cli-examples/CreateCryptoCurrencyPaymentAccount.sh @@ -0,0 +1,7 @@ +#!/bin/bash +source "env.sh" +$BISQ_HOME/bisq-cli --password=xyz --port=9998 createcryptopaymentacct \ + --account-name="My XMR Payment Account" \ + --currency-code=XMR \ + --address=4AsjtNXChh3Va58czCWHjn9S8ZFnsxggGZoSePauBHmSMr8vY5aBSqrPtQ9Y9M1iwkBHxcuTWXJsJ4NDATQjQJyKBXR7WP7 \ + --trade-instant=false diff --git a/cli-examples/CreateOffer.sh b/cli-examples/CreateOffer.sh new file mode 100755 index 0000000..e040991 --- /dev/null +++ b/cli-examples/CreateOffer.sh @@ -0,0 +1,32 @@ +#!/bin/bash +source "env.sh" + +# Create a fixed-price offer to buy BTC with EUR. +$BISQ_HOME/bisq-cli --password=xyz --port=9998 createoffer \ + --payment-account-id=f3c1ec8b-9761-458d-b13d-9039c6892413 \ + --direction=BUY \ + --currency-code=EUR \ + --amount=0.125 \ + --min-amount=0.0625 \ + --fixed-price=34500 \ + --security-deposit=15.0 \ + --fee-currency=BSQ + +# Create a market-price-margin based offer to sell BTC for JPY, at 0.5% above the current market JPY price. +$BISQ_HOME/bisq-cli --password=xyz --port=9998 createoffer \ + --payment-account-id=f3c1ec8b-9761-458d-b13d-9039c6892413 \ + --direction=SELL \ + --currency-code=JPY \ + --amount=0.125 \ + --min-amount=0.0625 \ + --market-price-margin=0.5 \ + --security-deposit=15.0 \ + --fee-currency=BSQ + +# Create an offer to swap 0.5 BTC for BSQ, at a price of 0.00005 BTC for 1 BSQ +$BISQ_HOME/bisq-cli --password=xyz --port=9998 createoffer \ + --swap=true \ + --direction=BUY \ + --currency-code=BSQ \ + --amount=0.5 \ + --fixed-price=0.00005 diff --git a/cli-examples/CreatePaymentAccount.sh b/cli-examples/CreatePaymentAccount.sh new file mode 100755 index 0000000..b60b98f --- /dev/null +++ b/cli-examples/CreatePaymentAccount.sh @@ -0,0 +1,4 @@ +#!/bin/bash +source "env.sh" +# Create a swift fiat payment account, providing details in a json form generated by getpaymentacctform. +$BISQ_HOME/bisq-cli --password=xyz --port=9998 createpaymentacct --payment-account-form=swift.json diff --git a/cli-examples/EditOffer.sh b/cli-examples/EditOffer.sh new file mode 100755 index 0000000..293d888 --- /dev/null +++ b/cli-examples/EditOffer.sh @@ -0,0 +1,44 @@ +#!/bin/bash +source "env.sh" + +# Warning: Editing an offer involves removing it from the offer book, then re-publishing +# it with the changes. This operation takes a few seconds and clients should not try +# to make rapid changes to the same offer; there must be a delay before each edit request +# for the same offer. + +# Disable an offer. +$BISQ_HOME/bisq-cli --password=xyz --port=9998 editoffer \ + --offer-id=83e8b2e2-51b6-4f39-a748-3ebd29c22aea \ + --enable=false + +# Enable an offer. +$BISQ_HOME/bisq-cli --password=xyz --port=9998 editoffer \ + --offer-id=83e8b2e2-51b6-4f39-a748-3ebd29c22aea \ + --enable=true + +# Edit the fixed-price, and/or change a market price margin based offer to a fixed-price offer. +$BISQ_HOME/bisq-cli --password=xyz --port=9998 editoffer \ + --offer-id=83e8b2e2-51b6-4f39-a748-3ebd29c22aea \ + --fixed-price=35000.5555 + +# Edit the price margin, and/or change a fixed-price offer to a market price margin based offer. +$BISQ_HOME/bisq-cli --password=xyz --port=9998 editoffer \ + --offer-id=83e8b2e2-51b6-4f39-a748-3ebd29c22aea \ + --market-price-margin=0.5 + +# Set the trigger price on a market price margin based offer. +# Note: trigger prices do not apply to fixed-price offers. +$BISQ_HOME/bisq-cli --password=xyz --port=9998 editoffer \ + --offer-id=83e8b2e2-51b6-4f39-a748-3ebd29c22aea \ + --trigger-price=3960000.0000 + +# Remove the trigger price from a market price margin based offer. +$BISQ_HOME/bisq-cli --password=xyz --port=9998 editoffer \ + --trigger-price=0 + +# Change a disabled fixed-price offer to a market price margin based offer, set a trigger price, and enable it. +$BISQ_HOME/bisq-cli --password=xyz --port=9998 editoffer \ + --offer-id=83e8b2e2-51b6-4f39-a748-3ebd29c22aea \ + --market-price-margin=0.5 \ + --trigger-price=33000.0000 \ + --enable=true diff --git a/cli-examples/FailTrade.sh b/cli-examples/FailTrade.sh new file mode 100755 index 0000000..ac99b1d --- /dev/null +++ b/cli-examples/FailTrade.sh @@ -0,0 +1,3 @@ +#!/bin/bash +source "env.sh" +$BISQ_HOME/bisq-cli --password=xyz --port=9998 failtrade --trade-id=83e8b2e2-51b6-4f39-a748-3ebd29c22aea diff --git a/cli-examples/GetAddressBalance.sh b/cli-examples/GetAddressBalance.sh new file mode 100755 index 0000000..95d5524 --- /dev/null +++ b/cli-examples/GetAddressBalance.sh @@ -0,0 +1,3 @@ +#!/bin/bash +source "env.sh" +$BISQ_HOME/bisq-cli --password=xyz --port=9998 getaddressbalance --address=bcrt1qygvsqmyt8jyhtp7l3zwqm7s7v3nar6vkc2luz3 diff --git a/cli-examples/GetBalances.sh b/cli-examples/GetBalances.sh new file mode 100755 index 0000000..b6fd38c --- /dev/null +++ b/cli-examples/GetBalances.sh @@ -0,0 +1,5 @@ +#!/bin/bash +source "env.sh" +$BISQ_HOME/bisq-cli --password=xyz --port=9998 getbalance +$BISQ_HOME/bisq-cli --password=xyz --port=9998 getbalance --currency-code=BSQ +$BISQ_HOME/bisq-cli --password=xyz --port=9998 getbalance --currency-code=BTC diff --git a/cli-examples/GetBsqSwapOffer.sh b/cli-examples/GetBsqSwapOffer.sh new file mode 100755 index 0000000..be4d0ee --- /dev/null +++ b/cli-examples/GetBsqSwapOffer.sh @@ -0,0 +1,4 @@ +#!/bin/bash +source "env.sh" +$BISQ_HOME/bisq-cli --password=xyz --port=9998 getoffer \ + --offer-id=83e8b2e2-51b6-4f39-a748-3ebd29c22aea diff --git a/cli-examples/GetBsqSwapOffers.sh b/cli-examples/GetBsqSwapOffers.sh new file mode 100755 index 0000000..b8d1304 --- /dev/null +++ b/cli-examples/GetBsqSwapOffers.sh @@ -0,0 +1,6 @@ +#!/bin/bash +source "env.sh" +# Get available BSQ swap offers to buy BTC with BSQ. +$BISQ_HOME/bisq-cli --password=xyz --port=9998 getoffers --direction=BUY --currency-code=BSQ +# Get available BSQ swap offers to sell BTC for BSQ. +$BISQ_HOME/bisq-cli --password=xyz --port=9998 getoffers --direction=SELL --currency-code=BSQ diff --git a/cli-examples/GetCryptoCurrencyPaymentMethods.sh b/cli-examples/GetCryptoCurrencyPaymentMethods.sh new file mode 100755 index 0000000..b904453 --- /dev/null +++ b/cli-examples/GetCryptoCurrencyPaymentMethods.sh @@ -0,0 +1,2 @@ +#!/bin/bash +# Not yet supported in CLI. API currently supports only BSQ, BSQ Swap, and XMR trading. diff --git a/cli-examples/GetFundingAddresses.sh b/cli-examples/GetFundingAddresses.sh new file mode 100755 index 0000000..360ba09 --- /dev/null +++ b/cli-examples/GetFundingAddresses.sh @@ -0,0 +1,3 @@ +#!/bin/bash +source "env.sh" +$BISQ_HOME/bisq-cli --password=xyz --port=9998 getfundingaddresses diff --git a/cli-examples/GetMarketPrice.sh b/cli-examples/GetMarketPrice.sh new file mode 100755 index 0000000..0038cf9 --- /dev/null +++ b/cli-examples/GetMarketPrice.sh @@ -0,0 +1,4 @@ +#!/bin/bash +source "env.sh" +# Get most recently available market price of XMR in BTC. +$BISQ_HOME/bisq-cli --password=xyz --port=9998 getbtcprice --currency-code=XMR diff --git a/cli-examples/GetMethodHelp.sh b/cli-examples/GetMethodHelp.sh new file mode 100755 index 0000000..ead1e2b --- /dev/null +++ b/cli-examples/GetMethodHelp.sh @@ -0,0 +1,3 @@ +#!/bin/bash +source "env.sh" +$BISQ_HOME/bisq-cli --password=xyz --port=9998 createoffer --help diff --git a/cli-examples/GetMyBsqSwapOffer.sh b/cli-examples/GetMyBsqSwapOffer.sh new file mode 100755 index 0000000..6309d2c --- /dev/null +++ b/cli-examples/GetMyBsqSwapOffer.sh @@ -0,0 +1,4 @@ +#!/bin/bash +source "env.sh" +$BISQ_HOME/bisq-cli --password=xyz --port=9998 getmyoffer \ + --offer-id=83e8b2e2-51b6-4f39-a748-3ebd29c22aea diff --git a/cli-examples/GetMyBsqSwapOffers.sh b/cli-examples/GetMyBsqSwapOffers.sh new file mode 100755 index 0000000..4716911 --- /dev/null +++ b/cli-examples/GetMyBsqSwapOffers.sh @@ -0,0 +1,6 @@ +#!/bin/bash +source "env.sh" +# Get my BSQ swap offers to buy BTC for BSQ. +$BISQ_HOME/bisq-cli --password=xyz --port=9998 getmyoffers --direction=BUY --currency-code=BSQ +# Get my BSQ swap offers to sell BTC for BSQ. +$BISQ_HOME/bisq-cli --password=xyz --port=9998 getmyoffers --direction=SELL --currency-code=BSQ diff --git a/cli-examples/GetMyOffer.sh b/cli-examples/GetMyOffer.sh new file mode 100755 index 0000000..6309d2c --- /dev/null +++ b/cli-examples/GetMyOffer.sh @@ -0,0 +1,4 @@ +#!/bin/bash +source "env.sh" +$BISQ_HOME/bisq-cli --password=xyz --port=9998 getmyoffer \ + --offer-id=83e8b2e2-51b6-4f39-a748-3ebd29c22aea diff --git a/cli-examples/GetMyOffers.sh b/cli-examples/GetMyOffers.sh new file mode 100755 index 0000000..e4a080e --- /dev/null +++ b/cli-examples/GetMyOffers.sh @@ -0,0 +1,6 @@ +#!/bin/bash +source "env.sh" +# Get my offers to buy BTC with EUR. +$BISQ_HOME/bisq-cli --password=xyz --port=9998 getmyoffers --direction=BUY --currency-code=EUR +# Get my offers to sell BTC for EUR. +$BISQ_HOME/bisq-cli --password=xyz --port=9998 getmyoffers --direction=SELL --currency-code=EUR diff --git a/cli-examples/GetOffer.sh b/cli-examples/GetOffer.sh new file mode 100755 index 0000000..be4d0ee --- /dev/null +++ b/cli-examples/GetOffer.sh @@ -0,0 +1,4 @@ +#!/bin/bash +source "env.sh" +$BISQ_HOME/bisq-cli --password=xyz --port=9998 getoffer \ + --offer-id=83e8b2e2-51b6-4f39-a748-3ebd29c22aea diff --git a/cli-examples/GetOfferCategory.sh b/cli-examples/GetOfferCategory.sh new file mode 100755 index 0000000..89dc980 --- /dev/null +++ b/cli-examples/GetOfferCategory.sh @@ -0,0 +1 @@ +# Used internally by CLI; there is no user CLI command 'getoffercategory'. \ No newline at end of file diff --git a/cli-examples/GetOffers.sh b/cli-examples/GetOffers.sh new file mode 100755 index 0000000..91bc541 --- /dev/null +++ b/cli-examples/GetOffers.sh @@ -0,0 +1,6 @@ +#!/bin/bash +source "env.sh" +# Get available offers to buy BTC with JPY. +$BISQ_HOME/bisq-cli --password=xyz --port=9998 getoffers --direction=BUY --currency-code=JPY +# Get available offers to sell BTC for JPY. +$BISQ_HOME/bisq-cli --password=xyz --port=9998 getoffers --direction=SELL --currency-code=JPY diff --git a/cli-examples/GetPaymentAccountForm.sh b/cli-examples/GetPaymentAccountForm.sh new file mode 100755 index 0000000..374099a --- /dev/null +++ b/cli-examples/GetPaymentAccountForm.sh @@ -0,0 +1,4 @@ +#!/bin/bash +source "env.sh" +# Get a blank SWIFT payment account form and save the json file in the current working directory. +$BISQ_HOME/bisq-cli --password=xyz --port=9998 getpaymentacctform --payment-method-id=SWIFT diff --git a/cli-examples/GetPaymentAccounts.sh b/cli-examples/GetPaymentAccounts.sh new file mode 100755 index 0000000..0276175 --- /dev/null +++ b/cli-examples/GetPaymentAccounts.sh @@ -0,0 +1,4 @@ +#!/bin/bash +source "env.sh" +# Get list of all saved payment accounts, including altcoin accounts. +$BISQ_HOME/bisq-cli --password=xyz --port=9998 getpaymentaccts diff --git a/cli-examples/GetPaymentMethods.sh b/cli-examples/GetPaymentMethods.sh new file mode 100755 index 0000000..0436913 --- /dev/null +++ b/cli-examples/GetPaymentMethods.sh @@ -0,0 +1,4 @@ +#!/bin/bash +source "env.sh" +# Get the ids of all supported Bisq payment methods. +$BISQ_HOME/bisq-cli --password=xyz --port=9998 getpaymentmethods diff --git a/cli-examples/GetTrade.sh b/cli-examples/GetTrade.sh new file mode 100755 index 0000000..656a784 --- /dev/null +++ b/cli-examples/GetTrade.sh @@ -0,0 +1,3 @@ +#!/bin/bash +source "env.sh" +$BISQ_HOME/bisq-cli --password=xyz --port=9998 gettrade --trade-id=83e8b2e2-51b6-4f39-a748-3ebd29c22aea diff --git a/cli-examples/GetTrades.sh b/cli-examples/GetTrades.sh new file mode 100755 index 0000000..b786719 --- /dev/null +++ b/cli-examples/GetTrades.sh @@ -0,0 +1,11 @@ +#!/bin/bash +source "env.sh" +# Get currently open trades. +$BISQ_HOME/bisq-cli --password=xyz --port=9998 gettrades +$BISQ_HOME/bisq-cli --password=xyz --port=9998 gettrades --category=open + +# Get completed trades. +$BISQ_HOME/bisq-cli --password=xyz --port=9998 gettrades --category=closed + +# Get failed trades. +$BISQ_HOME/bisq-cli --password=xyz --port=9998 gettrades --category=failed diff --git a/cli-examples/GetTransaction.sh b/cli-examples/GetTransaction.sh new file mode 100755 index 0000000..0f81778 --- /dev/null +++ b/cli-examples/GetTransaction.sh @@ -0,0 +1,4 @@ +#!/bin/bash +source "env.sh" +$BISQ_HOME/bisq-cli --password=xyz --port=9998 gettransaction \ + --transaction-id=fef206f2ada53e70fd8430d130e43bc3994ce075d50ac1f4fda8182c40ef6bdd diff --git a/cli-examples/GetTxFeeRate.sh b/cli-examples/GetTxFeeRate.sh new file mode 100755 index 0000000..5b388c9 --- /dev/null +++ b/cli-examples/GetTxFeeRate.sh @@ -0,0 +1,3 @@ +#!/bin/bash +source "env.sh" +$BISQ_HOME/bisq-cli --password=xyz --port=9998 gettxfeerate diff --git a/cli-examples/GetUnusedBsqAddress.sh b/cli-examples/GetUnusedBsqAddress.sh new file mode 100755 index 0000000..73c276f --- /dev/null +++ b/cli-examples/GetUnusedBsqAddress.sh @@ -0,0 +1,3 @@ +#!/bin/bash +source "env.sh" +$BISQ_HOME/bisq-cli --password=xyz --port=9998 getunusedbsqaddress diff --git a/cli-examples/GetVersion.sh b/cli-examples/GetVersion.sh new file mode 100755 index 0000000..8bfd5e3 --- /dev/null +++ b/cli-examples/GetVersion.sh @@ -0,0 +1,3 @@ +#!/bin/bash +source "env.sh" +$BISQ_HOME/bisq-cli --password=xyz --port=9998 getversion diff --git a/cli-examples/LockWallet.sh b/cli-examples/LockWallet.sh new file mode 100755 index 0000000..6226807 --- /dev/null +++ b/cli-examples/LockWallet.sh @@ -0,0 +1,3 @@ +#!/bin/bash +source "env.sh" +$BISQ_HOME/bisq-cli --password=xyz --port=9998 lockwallet diff --git a/cli-examples/RemoveWalletPassword.sh b/cli-examples/RemoveWalletPassword.sh new file mode 100755 index 0000000..52d5b0b --- /dev/null +++ b/cli-examples/RemoveWalletPassword.sh @@ -0,0 +1,4 @@ +#!/bin/bash +source "env.sh" +# Note: CLI command option parser expects a --wallet-password option, to differentiate it from an api daemon password. +$BISQ_HOME/bisq-cli --password=xyz --port=9998 removewalletpassword --wallet-password="abc" diff --git a/cli-examples/SendBsq.sh b/cli-examples/SendBsq.sh new file mode 100755 index 0000000..e25c83a --- /dev/null +++ b/cli-examples/SendBsq.sh @@ -0,0 +1,5 @@ +#!/bin/bash +source "env.sh" +$BISQ_HOME/bisq-cli --password=xyz --port=9998 sendbsq \ + --address=Bbcrt1q9elrmtxtzpwt25zq2pmeeu6qk8029w404ad0xn \ + --amount=1000.10 diff --git a/cli-examples/SendBtc.sh b/cli-examples/SendBtc.sh new file mode 100755 index 0000000..694291a --- /dev/null +++ b/cli-examples/SendBtc.sh @@ -0,0 +1,6 @@ +#!/bin/bash +source "env.sh" +$BISQ_HOME/bisq-cli --password=xyz --port=9998 sendbtc \ + --address=bcrt1qqau7ad7lf8xx08mnxl709h6cdv4ma9u3ace5k2 \ + --amount=0.006 \ + --tx-fee-rate=20 diff --git a/cli-examples/SetTxFeeRatePreference.sh b/cli-examples/SetTxFeeRatePreference.sh new file mode 100755 index 0000000..5f723ac --- /dev/null +++ b/cli-examples/SetTxFeeRatePreference.sh @@ -0,0 +1,3 @@ +#!/bin/bash +source "env.sh" +$BISQ_HOME/bisq-cli --password=xyz --port=9998 settxfeerate --tx-fee-rate=50 diff --git a/cli-examples/SetWalletPassword.sh b/cli-examples/SetWalletPassword.sh new file mode 100755 index 0000000..4f13c15 --- /dev/null +++ b/cli-examples/SetWalletPassword.sh @@ -0,0 +1,4 @@ +#!/bin/bash +source "env.sh" +# Note: CLI command option parser expects a --wallet-password option, to differentiate it from an api daemon password. +$BISQ_HOME/bisq-cli --password=xyz --port=9998 setwalletpassword --wallet-password="abc" diff --git a/cli-examples/Stop.sh b/cli-examples/Stop.sh new file mode 100755 index 0000000..4e5237e --- /dev/null +++ b/cli-examples/Stop.sh @@ -0,0 +1,3 @@ +#!/bin/bash +source "env.sh" +$BISQ_HOME/bisq-cli --password=xyz --port=9998 stop diff --git a/cli-examples/TakeOffer.sh b/cli-examples/TakeOffer.sh new file mode 100755 index 0000000..ff68e56 --- /dev/null +++ b/cli-examples/TakeOffer.sh @@ -0,0 +1,12 @@ +#!/bin/bash +source "env.sh" + +# Take a BSQ swap offer. +$BISQ_HOME/bisq-cli --password=xyz --port=9998 takeoffer --offer-id=8368b2e2-anb6-4ty9-ab09-3ebdk34f2aea + +# Take an offer that is not a BSQ swap offer. +# The payment-account-id param is required, the fee-currency param is optional. +$BISQ_HOME/bisq-cli --password=xyz --port=9998 takeoffer \ + --offer-id=83e8b2e2-51b6-4f39-a748-3ebd29c22aea \ + --payment-account-id=fe20cdbd-22be-4b8a-a4b6-d2608ff09d6e \ + --fee-currency=BTC diff --git a/cli-examples/UnFailTrade.sh b/cli-examples/UnFailTrade.sh new file mode 100755 index 0000000..fbaf629 --- /dev/null +++ b/cli-examples/UnFailTrade.sh @@ -0,0 +1,3 @@ +#!/bin/bash +source "env.sh" +$BISQ_HOME/bisq-cli --password=xyz --port=9998 unfailtrade --trade-id=83e8b2e2-51b6-4f39-a748-3ebd29c22aea diff --git a/cli-examples/UnlockWallet.sh b/cli-examples/UnlockWallet.sh new file mode 100755 index 0000000..0ca5782 --- /dev/null +++ b/cli-examples/UnlockWallet.sh @@ -0,0 +1,3 @@ +#!/bin/bash +source "env.sh" +$BISQ_HOME/bisq-cli --password=xyz --port=9998 unlockwallet --wallet-password=abc --timeout=30 diff --git a/cli-examples/UnsetTxFeeRatePreference.sh b/cli-examples/UnsetTxFeeRatePreference.sh new file mode 100755 index 0000000..f9b57aa --- /dev/null +++ b/cli-examples/UnsetTxFeeRatePreference.sh @@ -0,0 +1,3 @@ +#!/bin/bash +source "env.sh" +$BISQ_HOME/bisq-cli --password=xyz --port=9998 unsettxfeerate diff --git a/cli-examples/VerifyBsqSentToAddress.sh b/cli-examples/VerifyBsqSentToAddress.sh new file mode 100755 index 0000000..f8a72ec --- /dev/null +++ b/cli-examples/VerifyBsqSentToAddress.sh @@ -0,0 +1,5 @@ +#!/bin/bash +source "env.sh" +$BISQ_HOME/bisq-cli --password=xyz --port=9998 verifybsqsenttoaddress \ + --address=Bbcrt1q9elrmtxtzpwt25zq2pmeeu6qk8029w404ad0xn \ + --amount="50.50" diff --git a/cli-examples/WithdrawFunds.sh b/cli-examples/WithdrawFunds.sh new file mode 100755 index 0000000..0d52fd4 --- /dev/null +++ b/cli-examples/WithdrawFunds.sh @@ -0,0 +1,7 @@ +#!/bin/bash +source "env.sh" +# Withdraw BTC trade proceeds to external bitcoin wallet. +$BISQ_HOME/bisq-cli --password=xyz --port=9998 withdrawfunds \ + --trade-id=83e8b2e2-51b6-4f39-a748-3ebd29c22aea \ + --address=bcrt1qqau7ad7lf8xx08mnxl709h6cdv4ma9u3ace5k2 \ + --memo="Optional memo saved with transaction in Bisq wallet." diff --git a/cli-examples/cli-examples.iml b/cli-examples/cli-examples.iml new file mode 100644 index 0000000..c028faa --- /dev/null +++ b/cli-examples/cli-examples.iml @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/cli-examples/env.sh b/cli-examples/env.sh new file mode 100755 index 0000000..6df124a --- /dev/null +++ b/cli-examples/env.sh @@ -0,0 +1,6 @@ +#!/bin/bash + +# Absolute or relative directory path to the fully built Bisq source project, usually `master` or `bisq`. +# See https://github.com/bisq-network/bisq/blob/master/apitest/docs/api-beta-test-guide.md#clone-and-build-source-code +# for instructions on how to set up Bisq for running API daemon in regtest mode. +export BISQ_HOME="../../master-scratch" diff --git a/java-examples/build.gradle b/java-examples/build.gradle new file mode 100644 index 0000000..356f6e0 --- /dev/null +++ b/java-examples/build.gradle @@ -0,0 +1,62 @@ +plugins { + id 'java' + id 'com.google.protobuf' version '0.8.16' +} + +repositories { + mavenCentral() + maven { url 'https://jitpack.io' } +} + +group 'bisq' +version '0.0.1-SNAPSHOT' + +apply plugin: 'com.google.protobuf' + +dependencies { + compileOnly 'javax.annotation:javax.annotation-api:1.2' + implementation 'com.google.protobuf:protobuf-java:3.19.4' + implementation 'io.grpc:grpc-protobuf:1.42.1' + implementation 'io.grpc:grpc-core:1.42.1' + implementation 'io.grpc:grpc-stub:1.42.1' + implementation 'io.grpc:grpc-auth:1.42.1' + runtimeOnly 'io.grpc:grpc-netty-shaded:1.42.1' + + implementation 'net.sf.jopt-simple:jopt-simple:5.0.4' + implementation 'commons-io:commons-io:2.11.0' + implementation 'com.google.protobuf:protobuf-java:3.12.4' + implementation 'org.slf4j:slf4j-api:1.7.30' + implementation 'ch.qos.logback:logback-classic:1.1.11' + implementation 'ch.qos.logback:logback-core:1.1.11' + + annotationProcessor 'org.projectlombok:lombok:1.18.22' + testAnnotationProcessor 'org.projectlombok:lombok:1.18.22' + compileOnly 'org.projectlombok:lombok:1.18.22' + testCompileOnly 'org.projectlombok:lombok:1.18.22' + testImplementation 'org.junit.jupiter:junit-jupiter-api:5.7.2' + testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.7.2' +} + +sourceSets.main.java.srcDirs += [ + 'build/generated/source/main/java', + 'build/generated/source/main/grpc' +] + +protobuf { + protoc { + artifact = 'com.google.protobuf:protoc:3.19.4' + } + plugins { + grpc { + artifact = 'io.grpc:protoc-gen-grpc-java:1.41.2' + } + } + generateProtoTasks { + all()*.plugins { grpc {} } + } + generatedFilesBaseDir = "$projectDir/build/generated/source" +} + +test { + useJUnitPlatform() +} diff --git a/java-examples/gradle/wrapper/gradle-wrapper.properties b/java-examples/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..2e6e589 --- /dev/null +++ b/java-examples/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.3-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/java-examples/gradlew b/java-examples/gradlew new file mode 100755 index 0000000..744e882 --- /dev/null +++ b/java-examples/gradlew @@ -0,0 +1,185 @@ +#!/usr/bin/env sh + +# +# Copyright 2015 the original author or authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MSYS* | MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin or MSYS, switch paths to Windows format before running java +if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=`expr $i + 1` + done + case $i in + 0) set -- ;; + 1) set -- "$args0" ;; + 2) set -- "$args0" "$args1" ;; + 3) set -- "$args0" "$args1" "$args2" ;; + 4) set -- "$args0" "$args1" "$args2" "$args3" ;; + 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=`save "$@"` + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +exec "$JAVACMD" "$@" diff --git a/java-examples/gradlew.bat b/java-examples/gradlew.bat new file mode 100644 index 0000000..ac1b06f --- /dev/null +++ b/java-examples/gradlew.bat @@ -0,0 +1,89 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/java-examples/settings.gradle b/java-examples/settings.gradle new file mode 100644 index 0000000..b0d0c44 --- /dev/null +++ b/java-examples/settings.gradle @@ -0,0 +1,2 @@ +rootProject.name = 'java-examples' + diff --git a/java-examples/src/main/java/rpccalls/BaseJavaExample.java b/java-examples/src/main/java/rpccalls/BaseJavaExample.java new file mode 100644 index 0000000..add6340 --- /dev/null +++ b/java-examples/src/main/java/rpccalls/BaseJavaExample.java @@ -0,0 +1,68 @@ +package rpccalls; + +import io.grpc.CallCredentials; +import io.grpc.ManagedChannel; +import io.grpc.Metadata; +import io.grpc.StatusRuntimeException; + +import java.util.Scanner; +import java.util.concurrent.Executor; + +import static io.grpc.Metadata.ASCII_STRING_MARSHALLER; +import static io.grpc.Status.UNAUTHENTICATED; +import static java.lang.System.*; +import static java.util.concurrent.TimeUnit.SECONDS; + +public class BaseJavaExample { + + static void addChannelShutdownHook(ManagedChannel channel) { + Runtime.getRuntime().addShutdownHook(new Thread(() -> { + try { + channel.shutdown().awaitTermination(1, SECONDS); + } catch (InterruptedException ex) { + throw new IllegalStateException("Error shutting down gRPC channel.", ex); + } + })); + } + + static String getApiPassword() { + Scanner scanner = new Scanner(in); + out.println("Enter api password:"); + var apiPassword = "xyz"; // scanner.nextLine(); + scanner.close(); + return apiPassword; + } + + static CallCredentials buildCallCredentials(String apiPassword) { + return new CallCredentials() { + @Override + public void applyRequestMetadata(RequestInfo requestInfo, + Executor appExecutor, + MetadataApplier metadataApplier) { + appExecutor.execute(() -> { + try { + var headers = new Metadata(); + var passwordKey = Metadata.Key.of("password", ASCII_STRING_MARSHALLER); + headers.put(passwordKey, apiPassword); + metadataApplier.apply(headers); + } catch (Throwable ex) { + metadataApplier.fail(UNAUTHENTICATED.withCause(ex)); + } + }); + } + + @Override + public void thisUsesUnstableApi() { + } + }; + } + + static void handleError(Throwable t) { + if (t instanceof StatusRuntimeException) { + var grpcErrorStatus = ((StatusRuntimeException) t).getStatus(); + err.println(grpcErrorStatus.getCode() + ": " + grpcErrorStatus.getDescription()); + } else { + err.println("Error: " + t); + } + } +} \ No newline at end of file diff --git a/java-examples/src/main/java/rpccalls/CancelOffer.java b/java-examples/src/main/java/rpccalls/CancelOffer.java new file mode 100644 index 0000000..4e0b480 --- /dev/null +++ b/java-examples/src/main/java/rpccalls/CancelOffer.java @@ -0,0 +1,26 @@ +package rpccalls; + +import bisq.proto.grpc.CancelOfferRequest; +import bisq.proto.grpc.OffersGrpc; +import io.grpc.ManagedChannelBuilder; + +import static java.lang.System.out; + +public class CancelOffer extends BaseJavaExample { + + public static void main(String[] args) { + try { + var channel = ManagedChannelBuilder.forAddress("localhost", 9998).usePlaintext().build(); + addChannelShutdownHook(channel); + var credentials = buildCallCredentials(getApiPassword()); + var stub = OffersGrpc.newBlockingStub(channel).withCallCredentials(credentials); + var request = CancelOfferRequest.newBuilder() + .setId("83e8b2e2-51b6-4f39-a748-3ebd29c22aea") + .build(); + var response = stub.cancelOffer(request); + out.println(response); + } catch (Throwable t) { + handleError(t); + } + } +} diff --git a/java-examples/src/main/java/rpccalls/CloseTrade.java b/java-examples/src/main/java/rpccalls/CloseTrade.java new file mode 100644 index 0000000..0b3f219 --- /dev/null +++ b/java-examples/src/main/java/rpccalls/CloseTrade.java @@ -0,0 +1,26 @@ +package rpccalls; + +import bisq.proto.grpc.CloseTradeRequest; +import bisq.proto.grpc.TradesGrpc; +import io.grpc.ManagedChannelBuilder; + +import static java.lang.System.out; + +public class CloseTrade extends BaseJavaExample { + + public static void main(String[] args) { + try { + var channel = ManagedChannelBuilder.forAddress("localhost", 9998).usePlaintext().build(); + addChannelShutdownHook(channel); + var credentials = buildCallCredentials(getApiPassword()); + var stub = TradesGrpc.newBlockingStub(channel).withCallCredentials(credentials); + var request = CloseTradeRequest.newBuilder() + .setTradeId("83e8b2e2-51b6-4f39-a748-3ebd29c22aea") + .build(); + stub.closeTrade(request); + out.println("Open trade is closed."); + } catch (Throwable t) { + handleError(t); + } + } +} diff --git a/java-examples/src/main/java/rpccalls/ConfirmPaymentReceived.java b/java-examples/src/main/java/rpccalls/ConfirmPaymentReceived.java new file mode 100644 index 0000000..9d8e1a3 --- /dev/null +++ b/java-examples/src/main/java/rpccalls/ConfirmPaymentReceived.java @@ -0,0 +1,26 @@ +package rpccalls; + +import bisq.proto.grpc.ConfirmPaymentReceivedRequest; +import bisq.proto.grpc.TradesGrpc; +import io.grpc.ManagedChannelBuilder; + +import static java.lang.System.out; + +public class ConfirmPaymentReceived extends BaseJavaExample { + + public static void main(String[] args) { + try { + var channel = ManagedChannelBuilder.forAddress("localhost", 9998).usePlaintext().build(); + addChannelShutdownHook(channel); + var credentials = buildCallCredentials(getApiPassword()); + var stub = TradesGrpc.newBlockingStub(channel).withCallCredentials(credentials); + var request = ConfirmPaymentReceivedRequest.newBuilder() + .setTradeId("83e8b2e2-51b6-4f39-a748-3ebd29c22aea") + .build(); + stub.confirmPaymentReceived(request); + out.println("Payment receipt confirmation message has been sent to BTC buyer."); + } catch (Throwable t) { + handleError(t); + } + } +} diff --git a/java-examples/src/main/java/rpccalls/ConfirmPaymentStarted.java b/java-examples/src/main/java/rpccalls/ConfirmPaymentStarted.java new file mode 100644 index 0000000..84f2b3d --- /dev/null +++ b/java-examples/src/main/java/rpccalls/ConfirmPaymentStarted.java @@ -0,0 +1,26 @@ +package rpccalls; + +import bisq.proto.grpc.ConfirmPaymentStartedRequest; +import bisq.proto.grpc.TradesGrpc; +import io.grpc.ManagedChannelBuilder; + +import static java.lang.System.out; + +public class ConfirmPaymentStarted extends BaseJavaExample { + + public static void main(String[] args) { + try { + var channel = ManagedChannelBuilder.forAddress("localhost", 9998).usePlaintext().build(); + addChannelShutdownHook(channel); + var credentials = buildCallCredentials(getApiPassword()); + var stub = TradesGrpc.newBlockingStub(channel).withCallCredentials(credentials); + var request = ConfirmPaymentStartedRequest.newBuilder() + .setTradeId("83e8b2e2-51b6-4f39-a748-3ebd29c22aea") + .build(); + stub.confirmPaymentStarted(request); + out.println("Payment started message has been sent to BTC seller."); + } catch (Throwable t) { + handleError(t); + } + } +} diff --git a/java-examples/src/main/java/rpccalls/CreateBsqSwapOffer.java b/java-examples/src/main/java/rpccalls/CreateBsqSwapOffer.java new file mode 100644 index 0000000..c03e4f7 --- /dev/null +++ b/java-examples/src/main/java/rpccalls/CreateBsqSwapOffer.java @@ -0,0 +1,29 @@ +package rpccalls; + +import bisq.proto.grpc.CreateBsqSwapOfferRequest; +import bisq.proto.grpc.OffersGrpc; +import io.grpc.ManagedChannelBuilder; + +import static java.lang.System.out; + +public class CreateBsqSwapOffer extends BaseJavaExample { + + public static void main(String[] args) { + try { + var channel = ManagedChannelBuilder.forAddress("localhost", 9998).usePlaintext().build(); + addChannelShutdownHook(channel); + var credentials = buildCallCredentials(getApiPassword()); + var stub = OffersGrpc.newBlockingStub(channel).withCallCredentials(credentials); + var request = CreateBsqSwapOfferRequest.newBuilder() + .setDirection("BUY") // Buy BTC with BSQ + .setAmount(12500000) // Satoshis + .setMinAmount(6250000) // Satoshis + .setPrice("0.00005") // Price of 1 BSQ in BTC + .build(); + var response = stub.createBsqSwapOffer(request); + out.println(response); + } catch (Throwable t) { + handleError(t); + } + } +} diff --git a/java-examples/src/main/java/rpccalls/CreateCryptoCurrencyPaymentAccount.java b/java-examples/src/main/java/rpccalls/CreateCryptoCurrencyPaymentAccount.java new file mode 100644 index 0000000..5d52e89 --- /dev/null +++ b/java-examples/src/main/java/rpccalls/CreateCryptoCurrencyPaymentAccount.java @@ -0,0 +1,30 @@ +package rpccalls; + +import bisq.proto.grpc.CreateCryptoCurrencyPaymentAccountRequest; +import bisq.proto.grpc.PaymentAccountsGrpc; +import io.grpc.ManagedChannelBuilder; + +import static java.lang.System.out; + +public class CreateCryptoCurrencyPaymentAccount extends BaseJavaExample { + + public static void main(String[] args) { + try { + var channel = ManagedChannelBuilder.forAddress("localhost", 9998).usePlaintext().build(); + addChannelShutdownHook(channel); + var credentials = buildCallCredentials(getApiPassword()); + var stub = PaymentAccountsGrpc.newBlockingStub(channel).withCallCredentials(credentials); + var xmrAddress = "4AsjtNXChh3Va58czCWHjn9S8ZFnsxggGZoSePauBHmSMr8vY5aBSqrPtQ9Y9M1iwkBHxcuTWXJsJ4NDATQjQJyKBXR7WP7"; + var request = CreateCryptoCurrencyPaymentAccountRequest.newBuilder() + .setAccountName("My Instant XMR Payment Account") + .setCurrencyCode("XMR") + .setAddress(xmrAddress) + .setTradeInstant(true) + .build(); + var response = stub.createCryptoCurrencyPaymentAccount(request); + out.println("New XMR instant payment account: " + response.getPaymentAccount()); + } catch (Throwable t) { + handleError(t); + } + } +} diff --git a/java-examples/src/main/java/rpccalls/CreateOffer.java b/java-examples/src/main/java/rpccalls/CreateOffer.java new file mode 100644 index 0000000..0006bbb --- /dev/null +++ b/java-examples/src/main/java/rpccalls/CreateOffer.java @@ -0,0 +1,85 @@ +package rpccalls; + +import bisq.proto.grpc.CreateOfferRequest; +import bisq.proto.grpc.OffersGrpc; +import io.grpc.ManagedChannelBuilder; + +import static java.lang.System.out; + +public class CreateOffer extends BaseJavaExample { + + public static void main(String[] args) { + try { + var channel = ManagedChannelBuilder.forAddress("localhost", 9998).usePlaintext().build(); + addChannelShutdownHook(channel); + var credentials = buildCallCredentials(getApiPassword()); + var stub = OffersGrpc.newBlockingStub(channel).withCallCredentials(credentials); + + var fixedPriceOfferRequest = createFixedPriceEurOfferRequest(); + var fixedPriceOfferResponse = stub.createOffer(fixedPriceOfferRequest); + out.println(fixedPriceOfferResponse); + waitForOfferPreparation(5_000); + + var marketBasedPriceOfferRequest = createMarketBasedPriceEurOfferRequest(); + var marketBasedPriceOfferResponse = stub.createOffer(marketBasedPriceOfferRequest); + out.println(marketBasedPriceOfferResponse); + waitForOfferPreparation(5_000); + + var fixedPriceXmrOfferRequest = createFixedPriceXmrOfferRequest(); + var fixedPriceXmrOfferResponse = stub.createOffer(fixedPriceXmrOfferRequest); + out.println(fixedPriceXmrOfferResponse); + } catch (Throwable t) { + handleError(t); + } + } + + private static CreateOfferRequest createFixedPriceEurOfferRequest() { + return CreateOfferRequest.newBuilder() + .setCurrencyCode("EUR") + .setDirection("BUY") // Buy BTC with EUR + .setPrice("34500.00") + .setAmount(12500000) // Satoshis + .setMinAmount(6250000) // Satoshis + .setBuyerSecurityDepositPct(15.00) // 15% + .setPaymentAccountId("f3c1ec8b-9761-458d-b13d-9039c6892413") + .setMakerFeeCurrencyCode("BTC") // Pay Bisq trade fee in BTC + .build(); + } + + private static CreateOfferRequest createMarketBasedPriceEurOfferRequest() { + return CreateOfferRequest.newBuilder() + .setCurrencyCode("EUR") + .setDirection("SELL") // Sell BTC for EUR + .setUseMarketBasedPrice(true) + .setMarketPriceMarginPct(3.25) // Sell at 3.25% above market BTC price in EUR + .setAmount(12500000) // Satoshis + .setMinAmount(6250000) // Satoshis + .setBuyerSecurityDepositPct(15.00) // 15% + .setTriggerPrice("0") // No trigger price + .setPaymentAccountId("f3c1ec8b-9761-458d-b13d-9039c6892413") + .setMakerFeeCurrencyCode("BSQ") // Pay Bisq trade fee in BSQ + .build(); + } + + private static CreateOfferRequest createFixedPriceXmrOfferRequest() { + return CreateOfferRequest.newBuilder() + .setCurrencyCode("XMR") + .setDirection("BUY") // Buy BTC with XMR + .setPrice("0.005") // BTC price for 1 XMR + .setAmount(12500000) // Satoshis + .setMinAmount(6250000) // Satoshis + .setBuyerSecurityDepositPct(33.00) // 33% + .setPaymentAccountId("g3c8ec8b-9aa1-458d-b66d-9039c6892413") + .setMakerFeeCurrencyCode("BSQ") // Pay Bisq trade fee in BSQ + .build(); + } + + private static void waitForOfferPreparation(long ms) { + try { + // Wait new offer's preparation and wallet updates before attempting to create another offer. + Thread.sleep(5_000); + } catch (InterruptedException ex) { + // ignored + } + } +} diff --git a/java-examples/src/main/java/rpccalls/CreatePaymentAccount.java b/java-examples/src/main/java/rpccalls/CreatePaymentAccount.java new file mode 100644 index 0000000..fb3789f --- /dev/null +++ b/java-examples/src/main/java/rpccalls/CreatePaymentAccount.java @@ -0,0 +1,27 @@ +package rpccalls; + +import bisq.proto.grpc.CreatePaymentAccountRequest; +import bisq.proto.grpc.PaymentAccountsGrpc; +import io.grpc.ManagedChannelBuilder; + +import static java.lang.System.out; + +public class CreatePaymentAccount extends BaseJavaExample { + + public static void main(String[] args) { + try { + var channel = ManagedChannelBuilder.forAddress("localhost", 9998).usePlaintext().build(); + addChannelShutdownHook(channel); + var credentials = buildCallCredentials(getApiPassword()); + var stub = PaymentAccountsGrpc.newBlockingStub(channel).withCallCredentials(credentials); + // Fill in the swift payment account json form generated by GetPaymentAccountForm. + var request = CreatePaymentAccountRequest.newBuilder() + .setPaymentAccountForm("/path/to/swift.json") + .build(); + var response = stub.createPaymentAccount(request); + out.println("New swift payment account: " + response.getPaymentAccount()); + } catch (Throwable t) { + handleError(t); + } + } +} diff --git a/java-examples/src/main/java/rpccalls/EditOffer.java b/java-examples/src/main/java/rpccalls/EditOffer.java new file mode 100644 index 0000000..fab6ab0 --- /dev/null +++ b/java-examples/src/main/java/rpccalls/EditOffer.java @@ -0,0 +1,98 @@ +package rpccalls; + +import bisq.proto.grpc.EditOfferRequest; +import bisq.proto.grpc.OffersGrpc; +import io.grpc.ManagedChannelBuilder; + +import static bisq.proto.grpc.EditOfferRequest.EditType.*; +import static bisq.proto.grpc.EditOfferRequest.newBuilder; +import static java.lang.System.out; + +public class EditOffer extends BaseJavaExample { + + public static void main(String[] args) { + try { + var channel = ManagedChannelBuilder.forAddress("localhost", 9998).usePlaintext().build(); + addChannelShutdownHook(channel); + var credentials = buildCallCredentials(getApiPassword()); + var stub = OffersGrpc.newBlockingStub(channel).withCallCredentials(credentials); + + var disableOfferRequest = createDisableOfferRequest(); + stub.editOffer(disableOfferRequest); + out.println("Offer is disabled (removed from peers' offer book)."); + waitForOfferToBeRepublished(2_500); + + var editFixedPriceRequest = createEditFixedPriceRequest(); + stub.editOffer(editFixedPriceRequest); + out.println("Offer has been re-published with new fixed-price."); + waitForOfferToBeRepublished(2_500); + + var editFixedPriceAndEnableRequest = createEditFixedPriceAndEnableRequest(); + stub.editOffer(editFixedPriceAndEnableRequest); + out.println("Offer has been re-published with new fixed-price, and enabled."); + waitForOfferToBeRepublished(2_500); + + var editPriceMarginRequest = createEditPriceMarginRequest(); + stub.editOffer(editPriceMarginRequest); + out.println("Offer has been re-published with new price margin."); + waitForOfferToBeRepublished(2_500); + + var editTriggerPriceRequest = createEditTriggerPriceRequest(); + stub.editOffer(editTriggerPriceRequest); + out.println("Offer has been re-published with new trigger price."); + } catch (Throwable t) { + handleError(t); + } + } + + private static EditOfferRequest createDisableOfferRequest() { + return newBuilder() + .setId("83e8b2e2-51b6-4f39-a748-3ebd29c22aea") + .setEditType(ACTIVATION_STATE_ONLY) + .setEnable(0) // -1 = ignore this parameter, 0 = disable offer, 1 = enable offer + .build(); + } + + private static EditOfferRequest createEditFixedPriceRequest() { + return newBuilder() + .setId("83e8b2e2-51b6-4f39-a748-3ebd29c22aea") + .setEditType(FIXED_PRICE_ONLY) + .setPrice("30000.99") + .build(); + } + + private static EditOfferRequest createEditFixedPriceAndEnableRequest() { + return newBuilder() + .setId("83e8b2e2-51b6-4f39-a748-3ebd29c22aea") + .setEditType(FIXED_PRICE_AND_ACTIVATION_STATE) + .setPrice("30000.99") + .setEnable(1) + .build(); + } + + private static EditOfferRequest createEditPriceMarginRequest() { + return newBuilder() + .setId("83e8b2e2-51b6-4f39-a748-3ebd29c22aea") + .setEditType(MKT_PRICE_MARGIN_ONLY) + .setUseMarketBasedPrice(true) + .setMarketPriceMarginPct(2.00) // 2.00% + .build(); + } + + private static EditOfferRequest createEditTriggerPriceRequest() { + return newBuilder() + .setId("83e8b2e2-51b6-4f39-a748-3ebd29c22aea") + .setEditType(TRIGGER_PRICE_ONLY) + .setTriggerPrice("29000.00") // Trigger price is disabled when set to "0". + .build(); + } + + private static void waitForOfferToBeRepublished(long ms) { + try { + // Wait for edited offer to be removed from offer-book, edited, and re-published. + Thread.sleep(ms); + } catch (InterruptedException ex) { + // ignored + } + } +} diff --git a/java-examples/src/main/java/rpccalls/FailTrade.java b/java-examples/src/main/java/rpccalls/FailTrade.java new file mode 100644 index 0000000..4d64025 --- /dev/null +++ b/java-examples/src/main/java/rpccalls/FailTrade.java @@ -0,0 +1,26 @@ +package rpccalls; + +import bisq.proto.grpc.FailTradeRequest; +import bisq.proto.grpc.TradesGrpc; +import io.grpc.ManagedChannelBuilder; + +import static java.lang.System.out; + +public class FailTrade extends BaseJavaExample { + + public static void main(String[] args) { + try { + var channel = ManagedChannelBuilder.forAddress("localhost", 9998).usePlaintext().build(); + addChannelShutdownHook(channel); + var credentials = buildCallCredentials(getApiPassword()); + var stub = TradesGrpc.newBlockingStub(channel).withCallCredentials(credentials); + var request = FailTradeRequest.newBuilder() + .setTradeId("83e8b2e2-51b6-4f39-a748-3ebd29c22aea") + .build(); + stub.failTrade(request); + out.println("Open trade has been moved to failed trades list."); + } catch (Throwable t) { + handleError(t); + } + } +} diff --git a/java-examples/src/main/java/rpccalls/GetAddressBalance.java b/java-examples/src/main/java/rpccalls/GetAddressBalance.java new file mode 100644 index 0000000..acbabb2 --- /dev/null +++ b/java-examples/src/main/java/rpccalls/GetAddressBalance.java @@ -0,0 +1,26 @@ +package rpccalls; + +import bisq.proto.grpc.GetAddressBalanceRequest; +import bisq.proto.grpc.WalletsGrpc; +import io.grpc.ManagedChannelBuilder; + +import static java.lang.System.out; + +public class GetAddressBalance extends BaseJavaExample { + + public static void main(String[] args) { + try { + var channel = ManagedChannelBuilder.forAddress("localhost", 9998).usePlaintext().build(); + addChannelShutdownHook(channel); + var credentials = buildCallCredentials(getApiPassword()); + var stub = WalletsGrpc.newBlockingStub(channel).withCallCredentials(credentials); + var request = GetAddressBalanceRequest.newBuilder() + .setAddress("mwLYmweQf2dCgAqQCb3qU2UbxBycVBi2PW") + .build(); + var response = stub.getAddressBalance(request); + out.println(response); + } catch (Throwable t) { + handleError(t); + } + } +} diff --git a/java-examples/src/main/java/rpccalls/GetBalances.java b/java-examples/src/main/java/rpccalls/GetBalances.java new file mode 100644 index 0000000..c04d6bb --- /dev/null +++ b/java-examples/src/main/java/rpccalls/GetBalances.java @@ -0,0 +1,25 @@ +package rpccalls; + +import bisq.proto.grpc.GetBalancesRequest; +import bisq.proto.grpc.WalletsGrpc; +import io.grpc.ManagedChannelBuilder; + +import static java.lang.System.out; + +public class GetBalances extends BaseJavaExample { + + public static void main(String[] args) { + try { + var channel = ManagedChannelBuilder.forAddress("localhost", 9998).usePlaintext().build(); + addChannelShutdownHook(channel); + var credentials = buildCallCredentials(getApiPassword()); + var stub = WalletsGrpc.newBlockingStub(channel).withCallCredentials(credentials); + var request = GetBalancesRequest.newBuilder().build(); + var response = stub.getBalances(request); + out.println("BSQ " + response.getBalances().getBsq()); + out.println("BTC " + response.getBalances().getBtc()); + } catch (Throwable t) { + handleError(t); + } + } +} diff --git a/java-examples/src/main/java/rpccalls/GetBsqSwapOffer.java b/java-examples/src/main/java/rpccalls/GetBsqSwapOffer.java new file mode 100644 index 0000000..ff1598f --- /dev/null +++ b/java-examples/src/main/java/rpccalls/GetBsqSwapOffer.java @@ -0,0 +1,26 @@ +package rpccalls; + +import bisq.proto.grpc.GetOfferRequest; +import bisq.proto.grpc.OffersGrpc; +import io.grpc.ManagedChannelBuilder; + +import static java.lang.System.out; + +public class GetBsqSwapOffer extends BaseJavaExample { + + public static void main(String[] args) { + try { + var channel = ManagedChannelBuilder.forAddress("localhost", 9998).usePlaintext().build(); + addChannelShutdownHook(channel); + var credentials = buildCallCredentials(getApiPassword()); + var stub = OffersGrpc.newBlockingStub(channel).withCallCredentials(credentials); + var request = GetOfferRequest.newBuilder() + .setId("83e8b2e2-51b6-4f39-a748-3ebd29c22aea") + .build(); + var response = stub.getBsqSwapOffer(request); + out.println(response); + } catch (Throwable t) { + handleError(t); + } + } +} diff --git a/java-examples/src/main/java/rpccalls/GetBsqSwapOffers.java b/java-examples/src/main/java/rpccalls/GetBsqSwapOffers.java new file mode 100644 index 0000000..06fa736 --- /dev/null +++ b/java-examples/src/main/java/rpccalls/GetBsqSwapOffers.java @@ -0,0 +1,26 @@ +package rpccalls; + +import bisq.proto.grpc.GetBsqSwapOffersRequest; +import bisq.proto.grpc.OffersGrpc; +import io.grpc.ManagedChannelBuilder; + +import static java.lang.System.out; + +public class GetBsqSwapOffers extends BaseJavaExample { + + public static void main(String[] args) { + try { + var channel = ManagedChannelBuilder.forAddress("localhost", 9998).usePlaintext().build(); + addChannelShutdownHook(channel); + var credentials = buildCallCredentials(getApiPassword()); + var stub = OffersGrpc.newBlockingStub(channel).withCallCredentials(credentials); + var request = GetBsqSwapOffersRequest.newBuilder() + .setDirection("SELL") // Offers to sell (swap) BTC for BSQ. + .build(); + var response = stub.getBsqSwapOffers(request); + out.println(response); + } catch (Throwable t) { + handleError(t); + } + } +} diff --git a/java-examples/src/main/java/rpccalls/GetCryptoCurrencyPaymentMethods.java b/java-examples/src/main/java/rpccalls/GetCryptoCurrencyPaymentMethods.java new file mode 100644 index 0000000..26c4abe --- /dev/null +++ b/java-examples/src/main/java/rpccalls/GetCryptoCurrencyPaymentMethods.java @@ -0,0 +1,24 @@ +package rpccalls; + +import bisq.proto.grpc.GetCryptoCurrencyPaymentMethodsRequest; +import bisq.proto.grpc.PaymentAccountsGrpc; +import io.grpc.ManagedChannelBuilder; + +import static java.lang.System.out; + +public class GetCryptoCurrencyPaymentMethods extends BaseJavaExample { + // Note: API currently supports only BSQ, BSQ Swap, and XMR trading. + public static void main(String[] args) { + try { + var channel = ManagedChannelBuilder.forAddress("localhost", 9998).usePlaintext().build(); + addChannelShutdownHook(channel); + var credentials = buildCallCredentials(getApiPassword()); + var stub = PaymentAccountsGrpc.newBlockingStub(channel).withCallCredentials(credentials); + var request = GetCryptoCurrencyPaymentMethodsRequest.newBuilder().build(); + var response = stub.getCryptoCurrencyPaymentMethods(request); + out.println("Response: " + response.getPaymentMethodsList()); + } catch (Throwable t) { + handleError(t); + } + } +} diff --git a/java-examples/src/main/java/rpccalls/GetFundingAddresses.java b/java-examples/src/main/java/rpccalls/GetFundingAddresses.java new file mode 100644 index 0000000..d164ee3 --- /dev/null +++ b/java-examples/src/main/java/rpccalls/GetFundingAddresses.java @@ -0,0 +1,24 @@ +package rpccalls; + +import bisq.proto.grpc.GetFundingAddressesRequest; +import bisq.proto.grpc.WalletsGrpc; +import io.grpc.ManagedChannelBuilder; + +import static java.lang.System.out; + +public class GetFundingAddresses extends BaseJavaExample { + + public static void main(String[] args) { + try { + var channel = ManagedChannelBuilder.forAddress("localhost", 9998).usePlaintext().build(); + addChannelShutdownHook(channel); + var credentials = buildCallCredentials(getApiPassword()); + var stub = WalletsGrpc.newBlockingStub(channel).withCallCredentials(credentials); + var request = GetFundingAddressesRequest.newBuilder().build(); + var response = stub.getFundingAddresses(request); + out.println(response); + } catch (Throwable t) { + handleError(t); + } + } +} diff --git a/java-examples/src/main/java/rpccalls/GetMarketPrice.java b/java-examples/src/main/java/rpccalls/GetMarketPrice.java new file mode 100644 index 0000000..d537dfd --- /dev/null +++ b/java-examples/src/main/java/rpccalls/GetMarketPrice.java @@ -0,0 +1,26 @@ +package rpccalls; + +import bisq.proto.grpc.MarketPriceRequest; +import bisq.proto.grpc.PriceGrpc; +import io.grpc.ManagedChannelBuilder; + +import static java.lang.System.out; + +public class GetMarketPrice extends BaseJavaExample { + + public static void main(String[] args) { + try { + var channel = ManagedChannelBuilder.forAddress("localhost", 9998).usePlaintext().build(); + addChannelShutdownHook(channel); + var credentials = buildCallCredentials(getApiPassword()); + var stub = PriceGrpc.newBlockingStub(channel).withCallCredentials(credentials); + var request = MarketPriceRequest.newBuilder() + .setCurrencyCode("XMR") + .build(); + var response = stub.getMarketPrice(request); + out.println("Most recently available XMR market price: " + response.getPrice()); + } catch (Throwable t) { + handleError(t); + } + } +} diff --git a/java-examples/src/main/java/rpccalls/GetMethodHelp.java b/java-examples/src/main/java/rpccalls/GetMethodHelp.java new file mode 100644 index 0000000..b10067b --- /dev/null +++ b/java-examples/src/main/java/rpccalls/GetMethodHelp.java @@ -0,0 +1 @@ +package rpccalls;// Help Service is for CLI users. \ No newline at end of file diff --git a/java-examples/src/main/java/rpccalls/GetMyBsqSwapOffer.java b/java-examples/src/main/java/rpccalls/GetMyBsqSwapOffer.java new file mode 100644 index 0000000..a4d3f2d --- /dev/null +++ b/java-examples/src/main/java/rpccalls/GetMyBsqSwapOffer.java @@ -0,0 +1,26 @@ +package rpccalls; + +import bisq.proto.grpc.GetMyOfferRequest; +import bisq.proto.grpc.OffersGrpc; +import io.grpc.ManagedChannelBuilder; + +import static java.lang.System.out; + +public class GetMyBsqSwapOffer extends BaseJavaExample { + + public static void main(String[] args) { + try { + var channel = ManagedChannelBuilder.forAddress("localhost", 9998).usePlaintext().build(); + addChannelShutdownHook(channel); + var credentials = buildCallCredentials(getApiPassword()); + var stub = OffersGrpc.newBlockingStub(channel).withCallCredentials(credentials); + var request = GetMyOfferRequest.newBuilder() + .setId("83e8b2e2-51b6-4f39-a748-3ebd29c22aea") + .build(); + var response = stub.getMyBsqSwapOffer(request); + out.println(response); + } catch (Throwable t) { + handleError(t); + } + } +} diff --git a/java-examples/src/main/java/rpccalls/GetMyBsqSwapOffers.java b/java-examples/src/main/java/rpccalls/GetMyBsqSwapOffers.java new file mode 100644 index 0000000..29132f2 --- /dev/null +++ b/java-examples/src/main/java/rpccalls/GetMyBsqSwapOffers.java @@ -0,0 +1,26 @@ +package rpccalls; + +import bisq.proto.grpc.GetBsqSwapOffersRequest; +import bisq.proto.grpc.OffersGrpc; +import io.grpc.ManagedChannelBuilder; + +import static java.lang.System.out; + +public class GetMyBsqSwapOffers extends BaseJavaExample { + + public static void main(String[] args) { + try { + var channel = ManagedChannelBuilder.forAddress("localhost", 9998).usePlaintext().build(); + addChannelShutdownHook(channel); + var credentials = buildCallCredentials(getApiPassword()); + var stub = OffersGrpc.newBlockingStub(channel).withCallCredentials(credentials); + var request = GetBsqSwapOffersRequest.newBuilder() + .setDirection("BUY") // Offers to buy (swap) BTC for BSQ. + .build(); + var response = stub.getBsqSwapOffers(request); + out.println(response); + } catch (Throwable t) { + handleError(t); + } + } +} diff --git a/java-examples/src/main/java/rpccalls/GetMyOffer.java b/java-examples/src/main/java/rpccalls/GetMyOffer.java new file mode 100644 index 0000000..f8ed919 --- /dev/null +++ b/java-examples/src/main/java/rpccalls/GetMyOffer.java @@ -0,0 +1,26 @@ +package rpccalls; + +import bisq.proto.grpc.GetMyOfferRequest; +import bisq.proto.grpc.OffersGrpc; +import io.grpc.ManagedChannelBuilder; + +import static java.lang.System.out; + +public class GetMyOffer extends BaseJavaExample { + + public static void main(String[] args) { + try { + var channel = ManagedChannelBuilder.forAddress("localhost", 9998).usePlaintext().build(); + addChannelShutdownHook(channel); + var credentials = buildCallCredentials(getApiPassword()); + var stub = OffersGrpc.newBlockingStub(channel).withCallCredentials(credentials); + var request = GetMyOfferRequest.newBuilder() + .setId("83e8b2e2-51b6-4f39-a748-3ebd29c22aea") + .build(); + var response = stub.getMyOffer(request); + out.println(response); + } catch (Throwable t) { + handleError(t); + } + } +} diff --git a/java-examples/src/main/java/rpccalls/GetMyOffers.java b/java-examples/src/main/java/rpccalls/GetMyOffers.java new file mode 100644 index 0000000..019b2f4 --- /dev/null +++ b/java-examples/src/main/java/rpccalls/GetMyOffers.java @@ -0,0 +1,27 @@ +package rpccalls; + +import bisq.proto.grpc.GetMyOffersRequest; +import bisq.proto.grpc.OffersGrpc; +import io.grpc.ManagedChannelBuilder; + +import static java.lang.System.out; + +public class GetMyOffers extends BaseJavaExample { + + public static void main(String[] args) { + try { + var channel = ManagedChannelBuilder.forAddress("localhost", 9998).usePlaintext().build(); + addChannelShutdownHook(channel); + var credentials = buildCallCredentials(getApiPassword()); + var stub = OffersGrpc.newBlockingStub(channel).withCallCredentials(credentials); + var request = GetMyOffersRequest.newBuilder() + .setDirection("BUY") // Offers to buy BTC + .setCurrencyCode("USD") // with USD + .build(); + var response = stub.getMyOffers(request); + out.println(response); + } catch (Throwable t) { + handleError(t); + } + } +} diff --git a/java-examples/src/main/java/rpccalls/GetOffer.java b/java-examples/src/main/java/rpccalls/GetOffer.java new file mode 100644 index 0000000..49b0f66 --- /dev/null +++ b/java-examples/src/main/java/rpccalls/GetOffer.java @@ -0,0 +1,26 @@ +package rpccalls; + +import bisq.proto.grpc.GetOfferRequest; +import bisq.proto.grpc.OffersGrpc; +import io.grpc.ManagedChannelBuilder; + +import static java.lang.System.out; + +public class GetOffer extends BaseJavaExample { + + public static void main(String[] args) { + try { + var channel = ManagedChannelBuilder.forAddress("localhost", 9998).usePlaintext().build(); + addChannelShutdownHook(channel); + var credentials = buildCallCredentials(getApiPassword()); + var stub = OffersGrpc.newBlockingStub(channel).withCallCredentials(credentials); + var request = GetOfferRequest.newBuilder() + .setId("83e8b2e2-51b6-4f39-a748-3ebd29c22aea") + .build(); + var response = stub.getOffer(request); + out.println(response); + } catch (Throwable t) { + handleError(t); + } + } +} diff --git a/java-examples/src/main/java/rpccalls/GetOfferCategory.java b/java-examples/src/main/java/rpccalls/GetOfferCategory.java new file mode 100644 index 0000000..6450663 --- /dev/null +++ b/java-examples/src/main/java/rpccalls/GetOfferCategory.java @@ -0,0 +1,26 @@ +package rpccalls; + +import bisq.proto.grpc.GetOfferCategoryRequest; +import bisq.proto.grpc.OffersGrpc; +import io.grpc.ManagedChannelBuilder; + +import static java.lang.System.out; + +public class GetOfferCategory extends BaseJavaExample { + + public static void main(String[] args) { + try { + var channel = ManagedChannelBuilder.forAddress("localhost", 9998).usePlaintext().build(); + addChannelShutdownHook(channel); + var credentials = buildCallCredentials(getApiPassword()); + var stub = OffersGrpc.newBlockingStub(channel).withCallCredentials(credentials); + var request = GetOfferCategoryRequest.newBuilder() + .setId("83e8b2e2-51b6-4f39-a748-3ebd29c22aea") + .build(); + var response = stub.getOfferCategory(request); + out.println(response); + } catch (Throwable t) { + handleError(t); + } + } +} diff --git a/java-examples/src/main/java/rpccalls/GetOffers.java b/java-examples/src/main/java/rpccalls/GetOffers.java new file mode 100644 index 0000000..080f29d --- /dev/null +++ b/java-examples/src/main/java/rpccalls/GetOffers.java @@ -0,0 +1,27 @@ +package rpccalls; + +import bisq.proto.grpc.GetOffersRequest; +import bisq.proto.grpc.OffersGrpc; +import io.grpc.ManagedChannelBuilder; + +import static java.lang.System.out; + +public class GetOffers extends BaseJavaExample { + + public static void main(String[] args) { + try { + var channel = ManagedChannelBuilder.forAddress("localhost", 9998).usePlaintext().build(); + addChannelShutdownHook(channel); + var credentials = buildCallCredentials(getApiPassword()); + var stub = OffersGrpc.newBlockingStub(channel).withCallCredentials(credentials); + var request = GetOffersRequest.newBuilder() + .setDirection("SELL") // Available offers to sell BTC + .setCurrencyCode("JPY") // for JPY + .build(); + var response = stub.getOffers(request); + out.println(response); + } catch (Throwable t) { + handleError(t); + } + } +} diff --git a/java-examples/src/main/java/rpccalls/GetPaymentAccountForm.java b/java-examples/src/main/java/rpccalls/GetPaymentAccountForm.java new file mode 100644 index 0000000..c9870a1 --- /dev/null +++ b/java-examples/src/main/java/rpccalls/GetPaymentAccountForm.java @@ -0,0 +1,35 @@ +package rpccalls; + +import bisq.proto.grpc.GetPaymentAccountFormRequest; +import bisq.proto.grpc.PaymentAccountsGrpc; +import io.grpc.ManagedChannelBuilder; + +import java.io.BufferedWriter; +import java.io.File; +import java.io.FileWriter; + +import static java.lang.System.out; + +public class GetPaymentAccountForm extends BaseJavaExample { + + public static void main(String[] args) { + try { + var channel = ManagedChannelBuilder.forAddress("localhost", 9998).usePlaintext().build(); + addChannelShutdownHook(channel); + var credentials = buildCallCredentials(getApiPassword()); + var stub = PaymentAccountsGrpc.newBlockingStub(channel).withCallCredentials(credentials); + var request = GetPaymentAccountFormRequest.newBuilder() + .setPaymentMethodId("SWIFT") + .build(); + var response = stub.getPaymentAccountForm(request); + var paymentAccountFormJson = response.getPaymentAccountFormJson(); + File jsonFile = new File("/tmp/blank-swift-account-form.json"); + BufferedWriter writer = new BufferedWriter(new FileWriter(jsonFile)); + writer.write(paymentAccountFormJson); + writer.close(); + out.println("Swift payment account saved to " + jsonFile.getAbsolutePath()); + } catch (Throwable t) { + handleError(t); + } + } +} diff --git a/java-examples/src/main/java/rpccalls/GetPaymentAccounts.java b/java-examples/src/main/java/rpccalls/GetPaymentAccounts.java new file mode 100644 index 0000000..3704474 --- /dev/null +++ b/java-examples/src/main/java/rpccalls/GetPaymentAccounts.java @@ -0,0 +1,24 @@ +package rpccalls; + +import bisq.proto.grpc.GetPaymentAccountsRequest; +import bisq.proto.grpc.PaymentAccountsGrpc; +import io.grpc.ManagedChannelBuilder; + +import static java.lang.System.out; + +public class GetPaymentAccounts extends BaseJavaExample { + + public static void main(String[] args) { + try { + var channel = ManagedChannelBuilder.forAddress("localhost", 9998).usePlaintext().build(); + addChannelShutdownHook(channel); + var credentials = buildCallCredentials(getApiPassword()); + var stub = PaymentAccountsGrpc.newBlockingStub(channel).withCallCredentials(credentials); + var request = GetPaymentAccountsRequest.newBuilder().build(); + var response = stub.getPaymentAccounts(request); + out.println("Response: " + response.getPaymentAccountsList()); + } catch (Throwable t) { + handleError(t); + } + } +} diff --git a/java-examples/src/main/java/rpccalls/GetPaymentMethods.java b/java-examples/src/main/java/rpccalls/GetPaymentMethods.java new file mode 100644 index 0000000..f25fc3e --- /dev/null +++ b/java-examples/src/main/java/rpccalls/GetPaymentMethods.java @@ -0,0 +1,24 @@ +package rpccalls; + +import bisq.proto.grpc.GetPaymentMethodsRequest; +import bisq.proto.grpc.PaymentAccountsGrpc; +import io.grpc.ManagedChannelBuilder; + +import static java.lang.System.out; + +public class GetPaymentMethods extends BaseJavaExample { + + public static void main(String[] args) { + try { + var channel = ManagedChannelBuilder.forAddress("localhost", 9998).usePlaintext().build(); + addChannelShutdownHook(channel); + var credentials = buildCallCredentials(getApiPassword()); + var stub = PaymentAccountsGrpc.newBlockingStub(channel).withCallCredentials(credentials); + var request = GetPaymentMethodsRequest.newBuilder().build(); + var response = stub.getPaymentMethods(request); + out.println("Response: " + response.getPaymentMethodsList()); + } catch (Throwable t) { + handleError(t); + } + } +} diff --git a/java-examples/src/main/java/rpccalls/GetTrade.java b/java-examples/src/main/java/rpccalls/GetTrade.java new file mode 100644 index 0000000..f095503 --- /dev/null +++ b/java-examples/src/main/java/rpccalls/GetTrade.java @@ -0,0 +1,26 @@ +package rpccalls; + +import bisq.proto.grpc.GetTradeRequest; +import bisq.proto.grpc.TradesGrpc; +import io.grpc.ManagedChannelBuilder; + +import static java.lang.System.out; + +public class GetTrade extends BaseJavaExample { + + public static void main(String[] args) { + try { + var channel = ManagedChannelBuilder.forAddress("localhost", 9998).usePlaintext().build(); + addChannelShutdownHook(channel); + var credentials = buildCallCredentials(getApiPassword()); + var stub = TradesGrpc.newBlockingStub(channel).withCallCredentials(credentials); + var request = GetTradeRequest.newBuilder() + .setTradeId("83e8b2e2-51b6-4f39-a748-3ebd29c22aea") + .build(); + var response = stub.getTrade(request); + out.println("Trade: " + response.getTrade()); + } catch (Throwable t) { + handleError(t); + } + } +} diff --git a/java-examples/src/main/java/rpccalls/GetTrades.java b/java-examples/src/main/java/rpccalls/GetTrades.java new file mode 100644 index 0000000..a8c3f57 --- /dev/null +++ b/java-examples/src/main/java/rpccalls/GetTrades.java @@ -0,0 +1,27 @@ +package rpccalls; + +import bisq.proto.grpc.GetTradesRequest; +import bisq.proto.grpc.TradesGrpc; +import io.grpc.ManagedChannelBuilder; + +import static bisq.proto.grpc.GetTradesRequest.Category.CLOSED; +import static java.lang.System.out; + +public class GetTrades extends BaseJavaExample { + + public static void main(String[] args) { + try { + var channel = ManagedChannelBuilder.forAddress("localhost", 9998).usePlaintext().build(); + addChannelShutdownHook(channel); + var credentials = buildCallCredentials(getApiPassword()); + var stub = TradesGrpc.newBlockingStub(channel).withCallCredentials(credentials); + var request = GetTradesRequest.newBuilder() + .setCategory(CLOSED) // Or currently OPEN, or FAILED + .build(); + var response = stub.getTrades(request); + out.println("Open trades: " + response.getTradesList()); + } catch (Throwable t) { + handleError(t); + } + } +} diff --git a/java-examples/src/main/java/rpccalls/GetTransaction.java b/java-examples/src/main/java/rpccalls/GetTransaction.java new file mode 100644 index 0000000..a3b2291 --- /dev/null +++ b/java-examples/src/main/java/rpccalls/GetTransaction.java @@ -0,0 +1,26 @@ +package rpccalls; + +import bisq.proto.grpc.GetTransactionRequest; +import bisq.proto.grpc.WalletsGrpc; +import io.grpc.ManagedChannelBuilder; + +import static java.lang.System.out; + +public class GetTransaction extends BaseJavaExample { + + public static void main(String[] args) { + try { + var channel = ManagedChannelBuilder.forAddress("localhost", 9998).usePlaintext().build(); + addChannelShutdownHook(channel); + var credentials = buildCallCredentials(getApiPassword()); + var stub = WalletsGrpc.newBlockingStub(channel).withCallCredentials(credentials); + var request = GetTransactionRequest.newBuilder() + .setTxId("fef206f2ada53e70fd8430d130e43bc3994ce075d50ac1f4fda8182c40ef6bdd") + .build(); + var response = stub.getTransaction(request); + out.println(response); + } catch (Throwable t) { + handleError(t); + } + } +} diff --git a/java-examples/src/main/java/rpccalls/GetTxFeeRate.java b/java-examples/src/main/java/rpccalls/GetTxFeeRate.java new file mode 100644 index 0000000..859d476 --- /dev/null +++ b/java-examples/src/main/java/rpccalls/GetTxFeeRate.java @@ -0,0 +1,24 @@ +package rpccalls; + +import bisq.proto.grpc.GetTxFeeRateRequest; +import bisq.proto.grpc.WalletsGrpc; +import io.grpc.ManagedChannelBuilder; + +import static java.lang.System.out; + +public class GetTxFeeRate extends BaseJavaExample { + + public static void main(String[] args) { + try { + var channel = ManagedChannelBuilder.forAddress("localhost", 9998).usePlaintext().build(); + addChannelShutdownHook(channel); + var credentials = buildCallCredentials(getApiPassword()); + var stub = WalletsGrpc.newBlockingStub(channel).withCallCredentials(credentials); + var request = GetTxFeeRateRequest.newBuilder().build(); + var response = stub.getTxFeeRate(request); + out.println(response); + } catch (Throwable t) { + handleError(t); + } + } +} diff --git a/java-examples/src/main/java/rpccalls/GetUnusedBsqAddress.java b/java-examples/src/main/java/rpccalls/GetUnusedBsqAddress.java new file mode 100644 index 0000000..7b10fd7 --- /dev/null +++ b/java-examples/src/main/java/rpccalls/GetUnusedBsqAddress.java @@ -0,0 +1,24 @@ +package rpccalls; + +import bisq.proto.grpc.GetUnusedBsqAddressRequest; +import bisq.proto.grpc.WalletsGrpc; +import io.grpc.ManagedChannelBuilder; + +import static java.lang.System.out; + +public class GetUnusedBsqAddress extends BaseJavaExample { + + public static void main(String[] args) { + try { + var channel = ManagedChannelBuilder.forAddress("localhost", 9998).usePlaintext().build(); + addChannelShutdownHook(channel); + var credentials = buildCallCredentials(getApiPassword()); + var stub = WalletsGrpc.newBlockingStub(channel).withCallCredentials(credentials); + var request = GetUnusedBsqAddressRequest.newBuilder().build(); + var response = stub.getUnusedBsqAddress(request); + out.println(response); + } catch (Throwable t) { + handleError(t); + } + } +} diff --git a/java-examples/src/main/java/rpccalls/GetVersion.java b/java-examples/src/main/java/rpccalls/GetVersion.java new file mode 100644 index 0000000..6151615 --- /dev/null +++ b/java-examples/src/main/java/rpccalls/GetVersion.java @@ -0,0 +1,24 @@ +package rpccalls; + +import bisq.proto.grpc.GetVersionGrpc; +import bisq.proto.grpc.GetVersionRequest; +import io.grpc.ManagedChannelBuilder; + +import static java.lang.System.out; + +public class GetVersion extends BaseJavaExample { + + public static void main(String[] args) { + try { + var channel = ManagedChannelBuilder.forAddress("localhost", 9998).usePlaintext().build(); + addChannelShutdownHook(channel); + var credentials = buildCallCredentials(getApiPassword()); + var stub = GetVersionGrpc.newBlockingStub(channel).withCallCredentials(credentials); + var request = GetVersionRequest.newBuilder().build(); + var reply = stub.getVersion(request); + out.println(reply.getVersion()); + } catch (Throwable t) { + handleError(t); + } + } +} diff --git a/java-examples/src/main/java/rpccalls/LockWallet.java b/java-examples/src/main/java/rpccalls/LockWallet.java new file mode 100644 index 0000000..03ca6cd --- /dev/null +++ b/java-examples/src/main/java/rpccalls/LockWallet.java @@ -0,0 +1,24 @@ +package rpccalls; + +import bisq.proto.grpc.LockWalletRequest; +import bisq.proto.grpc.WalletsGrpc; +import io.grpc.ManagedChannelBuilder; + +import static java.lang.System.out; + +public class LockWallet extends BaseJavaExample { + + public static void main(String[] args) { + try { + var channel = ManagedChannelBuilder.forAddress("localhost", 9998).usePlaintext().build(); + addChannelShutdownHook(channel); + var credentials = buildCallCredentials(getApiPassword()); + var stub = WalletsGrpc.newBlockingStub(channel).withCallCredentials(credentials); + var request = LockWalletRequest.newBuilder().build(); + stub.lockWallet(request); + out.println("Wallet is locked."); + } catch (Throwable t) { + handleError(t); + } + } +} diff --git a/java-examples/src/main/java/rpccalls/RemoveWalletPassword.java b/java-examples/src/main/java/rpccalls/RemoveWalletPassword.java new file mode 100644 index 0000000..52f709f --- /dev/null +++ b/java-examples/src/main/java/rpccalls/RemoveWalletPassword.java @@ -0,0 +1,24 @@ +package rpccalls; + +import bisq.proto.grpc.RemoveWalletPasswordRequest; +import bisq.proto.grpc.WalletsGrpc; +import io.grpc.ManagedChannelBuilder; + +import static java.lang.System.out; + +public class RemoveWalletPassword extends BaseJavaExample { + + public static void main(String[] args) { + try { + var channel = ManagedChannelBuilder.forAddress("localhost", 9998).usePlaintext().build(); + addChannelShutdownHook(channel); + var credentials = buildCallCredentials(getApiPassword()); + var stub = WalletsGrpc.newBlockingStub(channel).withCallCredentials(credentials); + var request = RemoveWalletPasswordRequest.newBuilder().setPassword("abc").build(); + stub.removeWalletPassword(request); + out.println("Wallet encryption password is removed."); + } catch (Throwable t) { + handleError(t); + } + } +} diff --git a/java-examples/src/main/java/rpccalls/SendBsq.java b/java-examples/src/main/java/rpccalls/SendBsq.java new file mode 100644 index 0000000..0310f6e --- /dev/null +++ b/java-examples/src/main/java/rpccalls/SendBsq.java @@ -0,0 +1,28 @@ +package rpccalls; + +import bisq.proto.grpc.SendBsqRequest; +import bisq.proto.grpc.WalletsGrpc; +import io.grpc.ManagedChannelBuilder; + +import static java.lang.System.out; + +public class SendBsq extends BaseJavaExample { + + public static void main(String[] args) { + try { + var channel = ManagedChannelBuilder.forAddress("localhost", 9998).usePlaintext().build(); + addChannelShutdownHook(channel); + var credentials = buildCallCredentials(getApiPassword()); + var stub = WalletsGrpc.newBlockingStub(channel).withCallCredentials(credentials); + var request = SendBsqRequest.newBuilder() + .setAddress("Bbcrt1q9elrmtxtzpwt25zq2pmeeu6qk8029w404ad0xn") + .setAmount("50.50") + .setTxFeeRate("50") + .build(); + var response = stub.sendBsq(request); + out.println(response); + } catch (Throwable t) { + handleError(t); + } + } +} diff --git a/java-examples/src/main/java/rpccalls/SendBtc.java b/java-examples/src/main/java/rpccalls/SendBtc.java new file mode 100644 index 0000000..97fa1f4 --- /dev/null +++ b/java-examples/src/main/java/rpccalls/SendBtc.java @@ -0,0 +1,29 @@ +package rpccalls; + +import bisq.proto.grpc.SendBtcRequest; +import bisq.proto.grpc.WalletsGrpc; +import io.grpc.ManagedChannelBuilder; + +import static java.lang.System.out; + +public class SendBtc extends BaseJavaExample { + + public static void main(String[] args) { + try { + var channel = ManagedChannelBuilder.forAddress("localhost", 9998).usePlaintext().build(); + addChannelShutdownHook(channel); + var credentials = buildCallCredentials(getApiPassword()); + var stub = WalletsGrpc.newBlockingStub(channel).withCallCredentials(credentials); + var request = SendBtcRequest.newBuilder() + .setAddress("bcrt1qqau7ad7lf8xx08mnxl709h6cdv4ma9u3ace5k2") + .setAmount("0.005") + .setTxFeeRate("50") + .setMemo("Optional memo.") + .build(); + var response = stub.sendBtc(request); + out.println(response); + } catch (Throwable t) { + handleError(t); + } + } +} diff --git a/java-examples/src/main/java/rpccalls/SetTxFeeRatePreference.java b/java-examples/src/main/java/rpccalls/SetTxFeeRatePreference.java new file mode 100644 index 0000000..ef4ff44 --- /dev/null +++ b/java-examples/src/main/java/rpccalls/SetTxFeeRatePreference.java @@ -0,0 +1,26 @@ +package rpccalls; + +import bisq.proto.grpc.SetTxFeeRatePreferenceRequest; +import bisq.proto.grpc.WalletsGrpc; +import io.grpc.ManagedChannelBuilder; + +import static java.lang.System.out; + +public class SetTxFeeRatePreference extends BaseJavaExample { + + public static void main(String[] args) { + try { + var channel = ManagedChannelBuilder.forAddress("localhost", 9998).usePlaintext().build(); + addChannelShutdownHook(channel); + var credentials = buildCallCredentials(getApiPassword()); + var stub = WalletsGrpc.newBlockingStub(channel).withCallCredentials(credentials); + var request = SetTxFeeRatePreferenceRequest.newBuilder() + .setTxFeeRatePreference(25) + .build(); + var response = stub.setTxFeeRatePreference(request); + out.println(response); + } catch (Throwable t) { + handleError(t); + } + } +} diff --git a/java-examples/src/main/java/rpccalls/SetWalletPassword.java b/java-examples/src/main/java/rpccalls/SetWalletPassword.java new file mode 100644 index 0000000..0ff0b3b --- /dev/null +++ b/java-examples/src/main/java/rpccalls/SetWalletPassword.java @@ -0,0 +1,24 @@ +package rpccalls; + +import bisq.proto.grpc.SetWalletPasswordRequest; +import bisq.proto.grpc.WalletsGrpc; +import io.grpc.ManagedChannelBuilder; + +import static java.lang.System.out; + +public class SetWalletPassword extends BaseJavaExample { + + public static void main(String[] args) { + try { + var channel = ManagedChannelBuilder.forAddress("localhost", 9998).usePlaintext().build(); + addChannelShutdownHook(channel); + var credentials = buildCallCredentials(getApiPassword()); + var stub = WalletsGrpc.newBlockingStub(channel).withCallCredentials(credentials); + var request = SetWalletPasswordRequest.newBuilder().setPassword("abc").build(); + stub.setWalletPassword(request); + out.println("Wallet encryption password is set."); + } catch (Throwable t) { + handleError(t); + } + } +} diff --git a/java-examples/src/main/java/rpccalls/Stop.java b/java-examples/src/main/java/rpccalls/Stop.java new file mode 100644 index 0000000..2bb92c1 --- /dev/null +++ b/java-examples/src/main/java/rpccalls/Stop.java @@ -0,0 +1,24 @@ +package rpccalls; + +import bisq.proto.grpc.ShutdownServerGrpc; +import bisq.proto.grpc.StopRequest; +import io.grpc.ManagedChannelBuilder; + +import static java.lang.System.out; + +public class Stop extends BaseJavaExample { + + public static void main(String[] args) { + try { + var channel = ManagedChannelBuilder.forAddress("localhost", 9998).usePlaintext().build(); + addChannelShutdownHook(channel); + var credentials = buildCallCredentials(getApiPassword()); + var stub = ShutdownServerGrpc.newBlockingStub(channel).withCallCredentials(credentials); + var request = StopRequest.newBuilder().build(); + stub.stop(request); + out.println("Daemon is shutting down."); + } catch (Throwable t) { + handleError(t); + } + } +} diff --git a/java-examples/src/main/java/rpccalls/TakeOffer.java b/java-examples/src/main/java/rpccalls/TakeOffer.java new file mode 100644 index 0000000..9e3eaaf --- /dev/null +++ b/java-examples/src/main/java/rpccalls/TakeOffer.java @@ -0,0 +1,48 @@ +package rpccalls; + +import bisq.proto.grpc.GetOfferCategoryRequest; +import bisq.proto.grpc.OffersGrpc; +import bisq.proto.grpc.TakeOfferRequest; +import bisq.proto.grpc.TradesGrpc; +import io.grpc.ManagedChannelBuilder; + +import static bisq.proto.grpc.GetOfferCategoryReply.OfferCategory.BSQ_SWAP; +import static java.lang.System.err; +import static java.lang.System.out; + +public class TakeOffer extends BaseJavaExample { + + public static void main(String[] args) { + try { + var channel = ManagedChannelBuilder.forAddress("localhost", 9998).usePlaintext().build(); + addChannelShutdownHook(channel); + var credentials = buildCallCredentials(getApiPassword()); + var offersStub = OffersGrpc.newBlockingStub(channel).withCallCredentials(credentials); + var tradesStub = TradesGrpc.newBlockingStub(channel).withCallCredentials(credentials); + + // We need to send our payment account id, and the taker fee currency code if offer is + // not a BSQ swap offer. Find out by calling GetOfferCategory before taking the offer. + var getOfferCategoryRequest = GetOfferCategoryRequest.newBuilder() + .setId("83e8b2e2-51b6-4f39-a748-3ebd29c22aea") + .build(); + var offerCategory = offersStub.getOfferCategory(getOfferCategoryRequest); + // Create a takeoffer request builder with just the offerId parameter. + var takeOfferRequestBuilder = TakeOfferRequest.newBuilder() + .setOfferId("83e8b2e2-51b6-4f39-a748-3ebd29c22aea"); + // If offer is not a BSQ swap offer, add the paymentAccountId and takerFeeCurrencyCode parameters. + if (!offerCategory.equals(BSQ_SWAP)) + takeOfferRequestBuilder + .setPaymentAccountId("f3c1ec8b-9761-458d-b13d-9039c6892413") + .setTakerFeeCurrencyCode("BSQ"); + + var takeOfferRequest = takeOfferRequestBuilder.build(); + var takeOfferResponse = tradesStub.takeOffer(takeOfferRequest); + if (takeOfferResponse.hasFailureReason()) + err.println("Take offer failure reason: " + takeOfferResponse.getFailureReason()); + else + out.println("New trade: " + takeOfferResponse.getTrade()); + } catch (Throwable t) { + handleError(t); + } + } +} diff --git a/java-examples/src/main/java/rpccalls/UnFailTrade.java b/java-examples/src/main/java/rpccalls/UnFailTrade.java new file mode 100644 index 0000000..b200c79 --- /dev/null +++ b/java-examples/src/main/java/rpccalls/UnFailTrade.java @@ -0,0 +1,26 @@ +package rpccalls; + +import bisq.proto.grpc.TradesGrpc; +import bisq.proto.grpc.UnFailTradeRequest; +import io.grpc.ManagedChannelBuilder; + +import static java.lang.System.out; + +public class UnFailTrade extends BaseJavaExample { + + public static void main(String[] args) { + try { + var channel = ManagedChannelBuilder.forAddress("localhost", 9998).usePlaintext().build(); + addChannelShutdownHook(channel); + var credentials = buildCallCredentials(getApiPassword()); + var stub = TradesGrpc.newBlockingStub(channel).withCallCredentials(credentials); + var request = UnFailTradeRequest.newBuilder() + .setTradeId("83e8b2e2-51b6-4f39-a748-3ebd29c22aea") + .build(); + stub.unFailTrade(request); + out.println("Failed trade has been moved to open trades list."); + } catch (Throwable t) { + handleError(t); + } + } +} diff --git a/java-examples/src/main/java/rpccalls/UnlockWallet.java b/java-examples/src/main/java/rpccalls/UnlockWallet.java new file mode 100644 index 0000000..05b0d88 --- /dev/null +++ b/java-examples/src/main/java/rpccalls/UnlockWallet.java @@ -0,0 +1,27 @@ +package rpccalls; + +import bisq.proto.grpc.UnlockWalletRequest; +import bisq.proto.grpc.WalletsGrpc; +import io.grpc.ManagedChannelBuilder; + +import static java.lang.System.out; + +public class UnlockWallet extends BaseJavaExample { + + public static void main(String[] args) { + try { + var channel = ManagedChannelBuilder.forAddress("localhost", 9998).usePlaintext().build(); + addChannelShutdownHook(channel); + var credentials = buildCallCredentials(getApiPassword()); + var stub = WalletsGrpc.newBlockingStub(channel).withCallCredentials(credentials); + var request = UnlockWalletRequest.newBuilder() + .setPassword("abc") + .setTimeout(120) + .build(); + stub.unlockWallet(request); + out.println("Wallet is unlocked."); + } catch (Throwable t) { + handleError(t); + } + } +} diff --git a/java-examples/src/main/java/rpccalls/UnsetTxFeeRatePreference.java b/java-examples/src/main/java/rpccalls/UnsetTxFeeRatePreference.java new file mode 100644 index 0000000..f29c1bb --- /dev/null +++ b/java-examples/src/main/java/rpccalls/UnsetTxFeeRatePreference.java @@ -0,0 +1,24 @@ +package rpccalls; + +import bisq.proto.grpc.UnsetTxFeeRatePreferenceRequest; +import bisq.proto.grpc.WalletsGrpc; +import io.grpc.ManagedChannelBuilder; + +import static java.lang.System.out; + +public class UnsetTxFeeRatePreference extends BaseJavaExample { + + public static void main(String[] args) { + try { + var channel = ManagedChannelBuilder.forAddress("localhost", 9998).usePlaintext().build(); + addChannelShutdownHook(channel); + var credentials = buildCallCredentials(getApiPassword()); + var stub = WalletsGrpc.newBlockingStub(channel).withCallCredentials(credentials); + var request = UnsetTxFeeRatePreferenceRequest.newBuilder().build(); + var response = stub.unsetTxFeeRatePreference(request); + out.println(response); + } catch (Throwable t) { + handleError(t); + } + } +} diff --git a/java-examples/src/main/java/rpccalls/VerifyBsqSentToAddress.java b/java-examples/src/main/java/rpccalls/VerifyBsqSentToAddress.java new file mode 100644 index 0000000..859dc53 --- /dev/null +++ b/java-examples/src/main/java/rpccalls/VerifyBsqSentToAddress.java @@ -0,0 +1,27 @@ +package rpccalls; + +import bisq.proto.grpc.VerifyBsqSentToAddressRequest; +import bisq.proto.grpc.WalletsGrpc; +import io.grpc.ManagedChannelBuilder; + +import static java.lang.System.out; + +public class VerifyBsqSentToAddress extends BaseJavaExample { + + public static void main(String[] args) { + try { + var channel = ManagedChannelBuilder.forAddress("localhost", 9998).usePlaintext().build(); + addChannelShutdownHook(channel); + var credentials = buildCallCredentials(getApiPassword()); + var stub = WalletsGrpc.newBlockingStub(channel).withCallCredentials(credentials); + var request = VerifyBsqSentToAddressRequest.newBuilder() + .setAddress("Bbcrt1q9elrmtxtzpwt25zq2pmeeu6qk8029w404ad0xn") + .setAmount("50.50") + .build(); + var response = stub.verifyBsqSentToAddress(request); + out.println("Address did receive amount of BSQ: " + response.getIsAmountReceived()); + } catch (Throwable t) { + handleError(t); + } + } +} diff --git a/java-examples/src/main/java/rpccalls/WithdrawFunds.java b/java-examples/src/main/java/rpccalls/WithdrawFunds.java new file mode 100644 index 0000000..ac5b376 --- /dev/null +++ b/java-examples/src/main/java/rpccalls/WithdrawFunds.java @@ -0,0 +1,28 @@ +package rpccalls; + +import bisq.proto.grpc.TradesGrpc; +import bisq.proto.grpc.WithdrawFundsRequest; +import io.grpc.ManagedChannelBuilder; + +import static java.lang.System.out; + +public class WithdrawFunds extends BaseJavaExample { + + public static void main(String[] args) { + try { + var channel = ManagedChannelBuilder.forAddress("localhost", 9998).usePlaintext().build(); + addChannelShutdownHook(channel); + var credentials = buildCallCredentials(getApiPassword()); + var stub = TradesGrpc.newBlockingStub(channel).withCallCredentials(credentials); + var request = WithdrawFundsRequest.newBuilder() + .setTradeId("83e8b2e2-51b6-4f39-a748-3ebd29c22aea") + .setAddress("bcrt1qqau7ad7lf8xx08mnxl709h6cdv4ma9u3ace5k2") + .setMemo("Optional memo saved with transaction in Bisq wallet.") + .build(); + stub.withdrawFunds(request); + out.println("BTC trade proceeds have been sent to external bitcoin wallet."); + } catch (Throwable t) { + handleError(t); + } + } +} diff --git a/java-examples/src/main/resources/logback.xml b/java-examples/src/main/resources/logback.xml new file mode 100644 index 0000000..92a7893 --- /dev/null +++ b/java-examples/src/main/resources/logback.xml @@ -0,0 +1,14 @@ + + + + + %highlight(%d{MMM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{30}: %msg %xEx%n) + + + + + + + + + diff --git a/proto-downloader/download-bisq-protos.sh b/proto-downloader/download-bisq-protos.sh new file mode 100755 index 0000000..1558401 --- /dev/null +++ b/proto-downloader/download-bisq-protos.sh @@ -0,0 +1,12 @@ +#!/bin/bash + +echo "Downloading Bisq protobuf files from github to $PROTO_PATH directory." +echo "You may want to skip this step and copy your own local .proto files instead." + +set -x +curl -o ./pb.proto https://raw.githubusercontent.com/bisq-network/bisq/master/proto/src/main/proto/pb.proto +curl -o ./grpc.proto https://raw.githubusercontent.com/bisq-network/bisq/master/proto/src/main/proto/grpc.proto + +cp -v *.proto ../java-examples/src/main/proto +cp -v *.proto ../python-examples/proto +set +x \ No newline at end of file diff --git a/proto-downloader/grpc.proto b/proto-downloader/grpc.proto new file mode 100644 index 0000000..b0277a5 --- /dev/null +++ b/proto-downloader/grpc.proto @@ -0,0 +1,940 @@ +/* + * This file is part of Bisq. + * + * Bisq is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bisq is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bisq. If not, see . + */ + +syntax = "proto3"; +package io.bisq.protobuffer; +import "pb.proto"; +option java_package = "bisq.proto.grpc"; +option java_multiple_files = true; + +/* +* The DisputeAgents service is provided for development only; it can only be used when running in regtest mode. +*/ +service DisputeAgents { + // Register regtest / dev mode dispute agents. Does not work when running on mainnet. + rpc RegisterDisputeAgent (RegisterDisputeAgentRequest) returns (RegisterDisputeAgentReply) { + } +} + +message RegisterDisputeAgentRequest { + // One of "mediator" or "refundagent". Development / test arbitrators can only be registered in the UI. + string dispute_agent_type = 1; + // Private developer (only) registration key. + string registration_key = 2; +} + +message RegisterDisputeAgentReply { +} + +service Help { + // Returns a CLI command man page. + rpc GetMethodHelp (GetMethodHelpRequest) returns (GetMethodHelpReply) { + } +} + +message GetMethodHelpRequest { + string method_name = 1; // The CLI command name. +} + +message GetMethodHelpReply { + string method_help = 1; // The man page for the CLI command. +} + +/* +* The Offers service provides rpc methods for creating, editing, listing, and cancelling Bisq offers. +*/ +service Offers { + // Get an offer's category, one of FIAT, ALTCOIN, or BSQ_SWAP. This information is needed before an offer + // can be taken, and is used by a client to determine what kind of offer to take: a v1 FIAT or ALTCOIN offer, + // or a BSQ swap offer. V1 and BSQ swap trades are handled differently in the API daemon. + rpc GetOfferCategory (GetOfferCategoryRequest) returns (GetOfferCategoryReply) { + } + // Get the available BSQ swap offer with offer-id. + rpc GetBsqSwapOffer (GetOfferRequest) returns (GetBsqSwapOfferReply) { + } + // Get the v1 protocol offer with offer-id. + rpc GetOffer (GetOfferRequest) returns (GetOfferReply) { + } + // Get user's BSQ swap offer with offer-id. + rpc GetMyBsqSwapOffer (GetMyOfferRequest) returns (GetMyBsqSwapOfferReply) { + } + // Get my open v1 protocol offer with offer-id. Deprecated since 27-Dec-2021 (v1.8.0). Use GetOffer. + rpc GetMyOffer (GetMyOfferRequest) returns (GetMyOfferReply) { + } + // Get all available BSQ swap offers with a BUY (BTC) or SELL (BTC) direction. + rpc GetBsqSwapOffers (GetBsqSwapOffersRequest) returns (GetBsqSwapOffersReply) { + } + // Get all available v1 protocol offers with a BUY (BTC) or SELL (BTC) direction. + rpc GetOffers (GetOffersRequest) returns (GetOffersReply) { + } + // Get all user's BSQ swap offers with a BUY (BTC) or SELL (BTC) direction. + rpc GetMyBsqSwapOffers (GetBsqSwapOffersRequest) returns (GetMyBsqSwapOffersReply) { + } + // Get all user's open v1 protocol offers with a BUY (BTC) or SELL (BTC) direction. + rpc GetMyOffers (GetMyOffersRequest) returns (GetMyOffersReply) { + } + // Create a BSQ swap offer. + rpc CreateBsqSwapOffer (CreateBsqSwapOfferRequest) returns (CreateBsqSwapOfferReply) { + } + // Create a v1 protocol offer. + rpc CreateOffer (CreateOfferRequest) returns (CreateOfferReply) { + } + // Edit an open offer. + rpc EditOffer (EditOfferRequest) returns (EditOfferReply) { + } + // Cancel an open offer; remove it from the offer book. + rpc CancelOffer (CancelOfferRequest) returns (CancelOfferReply) { + } +} + +message GetOfferCategoryRequest { + string id = 1; // The offer's unique identifier. + bool is_my_offer = 2; // Whether the offer was created by the user or not. +} + +message GetOfferCategoryReply { + enum OfferCategory { + UNKNOWN = 0; // An invalid offer category probably indicates a software bug. + FIAT = 1; // Indicates offer is to BUY or SELL BTC with a fiat currency. + ALTCOIN = 2; // Indicates offer is to BUY or SELL BTC with an altcoin. + BSQ_SWAP = 3; // Indicates offer is to swap BTC for BSQ. + } + OfferCategory offer_category = 1; +} + +message GetBsqSwapOfferReply { + OfferInfo bsq_swap_offer = 1; // The returned BSQ swap offer. +} + +message GetOfferRequest { + string id = 1; // The offer's unique identifier. +} + +message GetOfferReply { + OfferInfo offer = 1; // The returned v1 protocol offer. +} + +message GetMyBsqSwapOfferReply { + OfferInfo bsq_swap_offer = 1; // The returned BSQ swap offer. +} + +// Deprecated with rpc method GetMyOffer since 27-Dec-2021 (v1.8.0). +message GetMyOfferRequest { + string id = 1; // The offer's unique identifier. +} + +// Deprecated with rpc method GetMyOffer since 27-Dec-2021 (v1.8.0). +message GetMyOfferReply { + OfferInfo offer = 1; // The returned v1 protocol offer. +} + +message GetOffersRequest { + string direction = 1; // The offer's BUY (BTC) or SELL (BTC) direction. + string currency_code = 2; // The offer's fiat or altcoin currency code. +} + +message GetOffersReply { + repeated OfferInfo offers = 1; // The returned list of available offers. +} + +message GetBsqSwapOffersRequest { + string direction = 1; // The BSQ swap offer's BUY (BTC) or SELL (BTC) direction. +} + +message GetBsqSwapOffersReply { + repeated OfferInfo bsq_swap_offers = 1; // The returned list of available BSQ swap offers. +} + +message GetMyOffersRequest { + string direction = 1; // The offers' BUY (BTC) or SELL (BTC) direction. + string currency_code = 2; // The offer's fiat or altcoin currency code. +} + +message GetMyOffersReply { + repeated OfferInfo offers = 1; // The returned list of user's open offers. +} + +message GetMyBsqSwapOffersReply { + repeated OfferInfo bsq_swap_offers = 1; // The returned list of user's open BSQ swap offers. +} + +message CreateBsqSwapOfferRequest { + // The new BSQ swap offer's BUY (BTC) or SELL (BTC) direction. + string direction = 1; + // The amount of BTC to be traded as a long representing satoshi units. + uint64 amount = 2; + // The minimum amount of BTC to be traded as a long representing satoshi units. + uint64 min_amount = 3; + // The fixed price of the offer as a string representing BTC units, e.g., "0.00005" or "0.00005000". + string price = 4; +} + +message CreateBsqSwapOfferReply { + OfferInfo bsq_swap_offer = 1; // The newly created BSQ swap offer. +} + +message CreateOfferRequest { + // The new offer's fiat or altcoin currency code. + string currency_code = 1; + // The new v1 protocol offer's BUY (BTC) or SELL (BTC) direction. + string direction = 2; + // For fiat offers: a string representing the rounded, fixed fiat price of the offer, e.g., "45000", not "45000". + // For altcoin offers: a string representing the fixed BTC price of the offer, e.g., "0.00005". + string price = 3; + // Whether the offer price is fixed, or market price margin based. + bool use_market_based_price = 4; + // The offer's market price margin as a percentage above or below the current market BTC price, e.g., 2.50 represents 2.5%. + double market_price_margin_pct = 5; + // The amount of BTC to be traded, in satoshis. + uint64 amount = 6; + // The minimum amount of BTC to be traded, in satoshis. + uint64 min_amount = 7; + // A BUY BTC offer maker's security deposit as a percentage of the BTC amount to be traded, e.g., 15.00 represents 15%. + double buyer_security_deposit_pct = 8; + // A market price margin based offer's trigger price is the market BTC price at which the offer is automatically disabled. + // Disabled offers are never automatically enabled, they must be manually re-enabled. + // A zero value indicates trigger price is not set. Trigger price does not apply to fixed price offers. + string trigger_price = 9; + // The unique identifier of the payment account used to create the new offer, and send or receive trade payment. + string payment_account_id = 10; + // The offer maker's trade fee currency: BTC or BSQ. + string maker_fee_currency_code = 11; +} + +message CreateOfferReply { + OfferInfo offer = 1; // The newly created v1 protocol offer. +} + +message EditOfferRequest { + // The edited offer's unique identifier. + string id = 1; + // For fiat offers: a string representing the new rounded, fixed fiat price of the offer, e.g., "45000", not "45000". + // For altcoin offers: a string representing the new fixed BTC price of the offer, e.g., "0.00005". + string price = 2; + // Whether the offer price is fixed, or market price margin based. + bool use_market_based_price = 3; + // An offer's new market price margin as a percentage above or below the current market BTC price. + double market_price_margin_pct = 4; + // A market price margin based offer's trigger price is the market BTC price at which the offer is automatically disabled. + // Disabled offers are never automatically enabled, they must be manually re-enabled. + // A zero value indicates trigger price is not set. Trigger price does not apply to fixed price offers. + string trigger_price = 5; + // Whether the offer's activation state should be changed (disable or enable), or left alone. + // Send a signed int, not a bool (with default=false). + // -1 = do not change activation state + // 0 = disable + // 1 = enable + sint32 enable = 6; + // The EditType determines and constricts what offer details can be modified by the request, simplifying param + // validation. (The CLI need to infer this detail from 'editoffer' command options, other clients do not.) + enum EditType { + // Edit only the offer's activation state (enabled or disabled). + ACTIVATION_STATE_ONLY = 0; + // Edit only the offer's fixed price. + FIXED_PRICE_ONLY = 1; + // Edit only the offer's fixed price and activation state. + FIXED_PRICE_AND_ACTIVATION_STATE = 2; + // Edit only the offer's market price margin. + MKT_PRICE_MARGIN_ONLY = 3; + // Edit only the offer's market price margin and activation state. + MKT_PRICE_MARGIN_AND_ACTIVATION_STATE = 4; + // Edit only the market price margin based offer's trigger price. + TRIGGER_PRICE_ONLY = 5; + // Edit only the market price margin based offer's trigger price and activation state. + TRIGGER_PRICE_AND_ACTIVATION_STATE = 6; + // Edit only the offer's market price margin and trigger price. + MKT_PRICE_MARGIN_AND_TRIGGER_PRICE = 7; + // Edit only the offer's market price margin, trigger price, and activation state. + MKT_PRICE_MARGIN_AND_TRIGGER_PRICE_AND_ACTIVATION_STATE = 8; + } + // Tell the daemon precisely what is being edited. + EditType edit_type = 7; +} + +message EditOfferReply { +} + +message CancelOfferRequest { + string id = 1; // The canceled offer's unique identifier. +} + +message CancelOfferReply { +} + +// OfferInfo describes an offer to a client. It is derived from the heavier +// Offer object in the daemon, which holds too much state to be sent to clients. +message OfferInfo { + // The offer's unique identifier. + string id = 1; + // The offer's BUY (BTC) or SELL (BTC) direction. + string direction = 2; + // For fiat offers: the fiat price for 1 BTC to 4 decimal places, e.g., 45000 EUR is "45000.0000". + // For altcoin offers: the altcoin price for 1 BTC to 8 decimal places, e.g., 0.00005 BTC is "0.00005000". + string price = 3; + // Whether the offer price is fixed, or market price margin based. + bool use_market_based_price = 4; + // The offer's market price margin above or below the current market BTC price, e.g., 5.00 represents 5%. + double market_price_margin_pct = 5; + // The offer's BTC amount in satoshis. Ten million satoshis is represented as 10000000. + uint64 amount = 6; + // The offer's minimum BTC amount in satoshis. One million satoshis is represented as 1000000. + uint64 min_amount = 7; + // The rounded volume of currency to be traded for BTC. + // Fiat volume is rounded to whole currency units (no cents). Altcoin volume is rounded to 2 decimal places. + string volume = 8; + // The rounded, minimum volume of currency to be traded for BTC. + // Fiat volume is rounded to whole currency units (no cents). Altcoin volume is rounded to 2 decimal places. + string min_volume = 9; + // A long representing the BTC buyer's security deposit in satoshis. + uint64 buyer_security_deposit = 10; + // A market price margin based offer's trigger price is the market BTC price at which the offer is automatically disabled. + // Disabled offers are never automatically enabled, they must be manually re-enabled. + // A zero value indicates trigger price is not set. Trigger price does not apply to fixed price offers. + string trigger_price = 11; + // Whether the offer maker paid the trading fee in BTC or not (BSQ). + bool is_currency_for_maker_fee_btc = 12; + // The unique identifier of the payment account used to create the offer. + string payment_account_id = 13; + // The unique identifier of the payment method used to create the offer. + string payment_method_id = 14; + // The short description of the payment method used to create the offer. + string payment_method_short_name = 15; + // For fiat offers, the baseCurrencyCode is BTC, and the counter_currency_code is the fiat currency code. + // For altcoin offers it is the opposite, the baseCurrencyCode is the altcoin code and the counter_currency_code is BTC. + string base_currency_code = 16; + // For fiat offers, the base_currency_code is BTC, and the counter_currency_code is the fiat currency code. + // For altcoin offers it is the opposite, the base_currency_code is the altcoin code and the counter_currency_code is BTC. + string counter_currency_code = 17; + // The creation date of the offer as a long: the number of milliseconds that have elapsed since January 1, 1970. + uint64 date = 18; + // The internal state of the offer, e.g., AVAILABLE, NOT_AVAILABLE, REMOVED, etc. + string state = 19; + // A long representing the BTC seller's security deposit in satoshis. + uint64 seller_security_deposit = 20; + // The bitcoin transaction id of the offer maker's fee payment. + string offer_fee_payment_tx_id = 21; + // The bitcoin transaction fee (amount) for the offer maker's fee payment transaction, in satoshis. + uint64 tx_fee = 22; + // The offer maker's Bisq trade fee amount in satoshis. + uint64 maker_fee = 23; + // Whether the offer is currently enabled or not. + bool is_activated = 24; + // Whether the offer was created by the user or not. + bool is_my_offer = 25; + // Whether the newly created offer was created by the user or not. + bool is_my_pending_offer = 26; + // Whether the offer is a BSQ swap offer or not (v1 protocol offer). + bool is_bsq_swap_offer = 27; + // The offer creator's Tor onion address. + string owner_node_address = 28; + // The offer creator's public key ring as a string. + string pub_key_ring = 29; + // The Bisq software version used to create the offer. + string version_nr = 30; + // The bitcoin protocol version used to create the offer. + int32 protocol_version = 31; +} + +// An offer's current availability status. +message AvailabilityResultWithDescription { + // An offer's current status as an eum. + AvailabilityResult availability_result = 1; + // A user friendly description of an offer's current availability status. + string description = 2; +} + +/* +* The PaymentAccounts service provides rpc methods for creating fiat and crypto currency payment accounts. +*/ +service PaymentAccounts { + // Create a fiat payment account, providing details in a json form generated by rpc method GetPaymentAccountForm. + rpc CreatePaymentAccount (CreatePaymentAccountRequest) returns (CreatePaymentAccountReply) { + } + // Get list of all saved fiat payment accounts. + rpc GetPaymentAccounts (GetPaymentAccountsRequest) returns (GetPaymentAccountsReply) { + } + // Get list of all supported Bisq payment methods. + rpc GetPaymentMethods (GetPaymentMethodsRequest) returns (GetPaymentMethodsReply) { + } + // Get a json template file for a supported Bisq payment method. Fill in the form and call rpc method CreatePaymentAccount. + rpc GetPaymentAccountForm (GetPaymentAccountFormRequest) returns (GetPaymentAccountFormReply) { + } + // Create a crypto currency (altcoin) payment account. + rpc CreateCryptoCurrencyPaymentAccount (CreateCryptoCurrencyPaymentAccountRequest) returns (CreateCryptoCurrencyPaymentAccountReply) { + } + // Get list of all supported Bisq crypto currency (altcoin) payment methods. + rpc GetCryptoCurrencyPaymentMethods (GetCryptoCurrencyPaymentMethodsRequest) returns (GetCryptoCurrencyPaymentMethodsReply) { + } +} + +message CreatePaymentAccountRequest { + string payment_account_form = 1; // File path of filled json payment account form. +} + +message CreatePaymentAccountReply { + PaymentAccount payment_account = 1; // The new payment account. +} + +message GetPaymentAccountsRequest { +} + +message GetPaymentAccountsReply { + repeated PaymentAccount payment_accounts = 1; // All user's saved payment accounts. +} + +message GetPaymentMethodsRequest { +} + +message GetPaymentMethodsReply { + repeated PaymentMethod payment_methods = 1; // Ids of all supported Bisq fiat payment methods. +} + +message GetPaymentAccountFormRequest { + string payment_method_id = 1; // Payment method id determining content of the requested payment account form. +} + +message GetPaymentAccountFormReply { + // An empty payment account json form to be filled out and passed to rpc method CreatePaymentAccount. + string payment_account_form_json = 1; +} + +message CreateCryptoCurrencyPaymentAccountRequest { + string account_name = 1; // The name of the altcoin payment account. Uniqueness is not enforced. + string currency_code = 2; // The altcoin currency code. + string address = 3; // The altcoin receiving address. + bool trade_instant = 4; // Whether the altcoin payment account is an instant account or not. +} + +message CreateCryptoCurrencyPaymentAccountReply { + PaymentAccount payment_account = 1; // The new altcoin payment account. +} + +message GetCryptoCurrencyPaymentMethodsRequest { +} + +message GetCryptoCurrencyPaymentMethodsReply { + repeated PaymentMethod payment_methods = 1; // Ids of all supported Bisq altcoin payment methods. +} + +service Price { + // Get the current market price for a crypto currency. + rpc GetMarketPrice (MarketPriceRequest) returns (MarketPriceReply) { + } +} + +message MarketPriceRequest { + string currency_code = 1; // The three letter currency code. +} + +message MarketPriceReply { + double price = 1; // The most recently available market price. +} + +service ShutdownServer { + // Shut down a local Bisq daemon. + rpc Stop (StopRequest) returns (StopReply) { + } +} + +message StopRequest { +} + +message StopReply { +} + +/* +* The Trades service provides rpc methods for taking, executing, and listing trades. +*/ +service Trades { + // Get a currently open trade. + rpc GetTrade (GetTradeRequest) returns (GetTradeReply) { + } + // Get currently open, or historical trades (closed or failed). + rpc GetTrades (GetTradesRequest) returns (GetTradesReply) { + } + // Take an open offer. + rpc TakeOffer (TakeOfferRequest) returns (TakeOfferReply) { + } + // Send a 'payment started' message to a trading peer (the BTC seller). + rpc ConfirmPaymentStarted (ConfirmPaymentStartedRequest) returns (ConfirmPaymentStartedReply) { + } + // Send a 'payment received' message to a trading peer (the BTC buyer). + rpc ConfirmPaymentReceived (ConfirmPaymentReceivedRequest) returns (ConfirmPaymentReceivedReply) { + } + // Close a completed trade; move it to trade history. + rpc CloseTrade (CloseTradeRequest) returns (CloseTradeReply) { + } + // Fail an open trade. + rpc FailTrade (FailTradeRequest) returns (FailTradeReply) { + } + // Unfail a failed trade. + rpc UnFailTrade (UnFailTradeRequest) returns (UnFailTradeReply) { + } + // Withdraw trade proceeds to an external bitcoin wallet address. + rpc WithdrawFunds (WithdrawFundsRequest) returns (WithdrawFundsReply) { + } +} + +message TakeOfferRequest { + string offer_id = 1; // The unique identifier of the offer being taken. + string payment_account_id = 2; // The unique identifier of the payment account used to take offer.. + string taker_fee_currency_code = 3; // The code of the currency (BSQ or BTC) used to pay the taker's Bisq trade fee. +} + +message TakeOfferReply { + TradeInfo trade = 1; // The new trade. + AvailabilityResultWithDescription failure_reason = 2; // The reason the offer could not be taken. +} + +message ConfirmPaymentStartedRequest { + string trade_id = 1; // The unique identifier of the open trade. +} + +message ConfirmPaymentStartedReply { +} + +message ConfirmPaymentReceivedRequest { + string trade_id = 1; // The unique identifier of the open trade. +} + +message ConfirmPaymentReceivedReply { +} + +message GetTradeRequest { + string trade_id = 1; // The unique identifier of the trade. +} + +message GetTradeReply { + TradeInfo trade = 1; // The unique identifier of the trade. +} + +message GetTradesRequest { + // Rpc method GetTrades parameter determining what category of trade list is is being requested. + enum Category { + OPEN = 0; // Get all currently open trades. + CLOSED = 1; // Get all completed trades. + FAILED = 2; // Get all failed trades. + } + Category category = 1; +} + +message GetTradesReply { + repeated TradeInfo trades = 1; // All trades for GetTradesRequest.Category. +} + +message CloseTradeRequest { + string trade_id = 1; // The unique identifier of the trade. +} + +message CloseTradeReply { +} + +message FailTradeRequest { + string trade_id = 1; // The unique identifier of the trade. +} + +message FailTradeReply { +} + +message UnFailTradeRequest { + string trade_id = 1; // The unique identifier of the trade. +} + +message UnFailTradeReply { +} + +message WithdrawFundsRequest { + string trade_id = 1; // The unique identifier of the trade. + string address = 2; // The receiver's bitcoin wallet address. + string memo = 3; // An optional memo saved with the sent btc transaction. +} + +message WithdrawFundsReply { +} + +// TODO Modify bisq-grpc-api-doc to include core Trade enums in API Reference. +message TradeInfo { + // The original offer. + OfferInfo offer = 1; + // The unique identifier of the trade. + string trade_id = 2; + // An abbreviation of unique identifier of the trade. It cannot be used as parameter to rpc methods GetTrade, + // ConfirmPaymentStarted, CloseTrade, etc., but it may be useful while interacting with support or trading peers. + string short_id = 3; + // The creation date of the trade as a long: the number of milliseconds that have elapsed since January 1, 1970. + uint64 date = 4; + // A brief description of the user's role in the trade, i.e., an offer maker or taker, a BTC buyer or seller. + string role = 5; + // Whether the offer taker's Bisq trade fee was paid in BTC or not (BSQ). + bool is_currency_for_taker_fee_btc = 6; + // The bitcoin miner transaction fee in satoshis. + uint64 tx_fee_as_long = 7; + // The offer taker's Bisq trade fee in satoshis. + uint64 taker_fee_as_long = 8; + // The bitcoin transaction id for offer taker's Bisq trade fee. + string taker_fee_tx_id = 9; + // The bitcoin transaction id for the offer taker's security deposit. + string deposit_tx_id = 10; + // The bitcoin transaction id for trade payout. + string payout_tx_id = 11; + // The trade payout amount in satoshis. + uint64 trade_amount_as_long = 12; + // For fiat trades: the fiat price for 1 BTC to 4 decimal places, e.g., 41000.50 EUR is "41000.5000". + // For altcoin trades: the altcoin price for 1 BTC to 8 decimal places, e.g., 0.5 BTC is "0.50000000". + string trade_price = 13; + // The trading peer's node address. + string trading_peer_node_address = 14; + // The internal state of the trade. (TODO bisq-grpc-api-doc -> include Trade.State in API Reference.) + string state = 15; + // The internal phase of the trade. (TODO bisq-grpc-api-doc -> include Trade.Phase in API Reference.) + string phase = 16; + // How much of the trade protocol's time limit has elapsed. (TODO bisq-grpc-api-doc -> include Trade.TradePeriodState in API Reference.) + string trade_period_state = 17; + // Whether the trade's security deposit bitcoin transaction has been broadcast, or not. + bool is_deposit_published = 18; + // Whether the trade's security deposit bitcoin transaction has been confirmed at least once, or not. + bool is_deposit_confirmed = 19; + // Whether the trade's 'start payment' message has been sent by the BTC buyer, or not. + bool is_payment_started_message_sent = 20; + // Whether the trade's 'payment received' message has been sent by the BTC seller, or not. + bool is_payment_received_message_sent = 21; + // Whether the trade's payout bitcoin transaction has been confirmed at least once, or not. + bool is_payout_published = 22; + // Whether the trade's payout has been completed and the trade is now closed, or not. + bool is_completed = 23; + // The entire trade contract as a json string. + string contract_as_json = 24; + // The summary of the trade contract. + ContractInfo contract = 25; + // The volume of currency traded for BTC. + string trade_volume = 26; + // The details specific to the BSQ swap trade. If the trade is not a BSQ swap, this field should be ignored. + BsqSwapTradeInfo bsq_swap_trade_info = 28; + // Needed by open/closed/failed trade list items. + string closing_status = 29; +} + +message ContractInfo { + string buyer_node_address = 1; // The BTC buyer peer's node address. + string seller_node_address = 2; // The BTC seller peer's node address. + string mediator_node_address = 3; // If the trade was disputed, the Bisq mediator's node address. + string refund_agent_node_address = 4; // If a trade refund was requested, the Bisq refund agent's node address. + bool is_buyer_maker_and_seller_taker = 5; // Whether the BTC buyer created the original offer, or not. + string maker_account_id = 6; // The offer maker's payment account id. + string taker_account_id = 7; // The offer taker's payment account id. + PaymentAccountPayloadInfo maker_payment_account_payload = 8; // A summary of the offer maker's payment account. + PaymentAccountPayloadInfo taker_payment_account_payload = 9; // A summary of the offer taker's payment account. + string maker_payout_address_string = 10; // The offer maker's BTC payout address. + string taker_payout_address_string = 11; // The offer taker's BTC payout address. + uint64 lock_time = 12; // The earliest time a transaction can be added to the block chain. +} + +/* +* BSQ Swap protocol specific fields not common to Bisq v1 trade protocol fields. +*/ +message BsqSwapTradeInfo { + string tx_id = 1; // The BSQ swap's bitcoin transaction id. + uint64 bsq_trade_amount = 2; // The amount of BSQ swapped in satoshis. + uint64 btc_trade_amount = 3; // The amount of BTC swapped in satoshis. + uint64 bsq_maker_trade_fee = 4; // The swap offer maker's BSQ trade fee. + uint64 bsq_taker_trade_fee = 5; // The swap offer taker's BSQ trade fee. + uint64 tx_fee_per_vbyte = 6; // The swap transaction's bitcoin transaction id. + string maker_bsq_address = 7; // The swap offer maker's BSQ wallet address. + string maker_btc_address = 8; // The swap offer maker's BTC wallet address. + string taker_bsq_address = 9; // The swap offer taker's BSQ wallet address. + string taker_btc_address = 10; // The swap offer taker's BTC wallet address. + uint64 num_confirmations = 11; // The confirmations count for the completed swap's bitcoin transaction. + string error_message = 12; // An explanation for a failure to complete the swap. + uint64 payout = 13; // The amount of the user's trade payout in satoshis. + uint64 swap_peer_payout = 14; // The amount of the peer's trade payout in satoshis. +} + +message PaymentAccountPayloadInfo { + string id = 1; // The unique identifier of the payment account. + string payment_method_id = 2; // The unique identifier of the payment method. + string address = 3; // The optional altcoin wallet address associated with the (altcoin) payment account. + string payment_details = 4; // The optional payment details, if available. +} + +message TxFeeRateInfo { + bool use_custom_tx_fee_rate = 1; // Whether the daemon's custom btc transaction fee rate preference is set, or not. + uint64 custom_tx_fee_rate = 2; // The daemon's custom btc transaction fee rate preference, in sats/byte. + uint64 fee_service_rate = 3; // The Bisq network's most recently available btc transaction fee rate, in sats/byte. + // The date of the most recent Bisq network fee rate request as a long: the number of milliseconds that have elapsed since January 1, 1970. + uint64 last_fee_service_request_ts = 4; + uint64 min_fee_service_rate = 5; // The Bisq network's minimum btc transaction fee rate, in sats/byte. +} + +message TxInfo { + string tx_id = 1; // The bitcoin transaction id. + uint64 input_sum = 2; // The sum of the bitcoin transaction's input values in satoshis. + uint64 output_sum = 3; // The sum of the bitcoin transaction's output values in satoshis. + uint64 fee = 4; // The bitcoin transaction's miner fee in satoshis. + int32 size = 5; // The bitcoin transaction's size in bytes. + bool is_pending = 6; // Whether the bitcoin transaction has been confirmed at least one time, or not. + string memo = 7; // An optional memo associated with the bitcoin transaction. +} + +/* +* The Wallets service provides rpc methods for basic wallet operations such as checking balances, +* sending BTC or BSQ to external wallets, checking transaction fee rates, setting or unsetting +* an encryption password on a a wallet, and unlocking / locking an encrypted wallet. +*/ +service Wallets { + // Get the Bisq wallet's current BSQ and BTC balances. + rpc GetBalances (GetBalancesRequest) returns (GetBalancesReply) { + } + // Get BTC balance for a wallet address. + rpc GetAddressBalance (GetAddressBalanceRequest) returns (GetAddressBalanceReply) { + } + // Get an unused BSQ wallet address. + rpc GetUnusedBsqAddress (GetUnusedBsqAddressRequest) returns (GetUnusedBsqAddressReply) { + } + // Send an amount of BSQ to an external address. + rpc SendBsq (SendBsqRequest) returns (SendBsqReply) { + } + // Send an amount of BTC to an external address. + rpc SendBtc (SendBtcRequest) returns (SendBtcReply) { + } + // Verify a specific amount of BSQ was received by a BSQ wallet address. + // This is a problematic way of verifying BSQ payment has been received for a v1 trade protocol BSQ-BTC trade, + // which has been solved by the introduction of BSQ swap trades, which use a different, unused BSQ address for each trade. + rpc VerifyBsqSentToAddress (VerifyBsqSentToAddressRequest) returns (VerifyBsqSentToAddressReply) { + } + // Get the Bisq network's most recently available bitcoin miner transaction fee rate, or custom fee rate if set. + rpc GetTxFeeRate (GetTxFeeRateRequest) returns (GetTxFeeRateReply) { + } + // Set the Bisq daemon's custom bitcoin miner transaction fee rate, in sats/byte.. + rpc SetTxFeeRatePreference (SetTxFeeRatePreferenceRequest) returns (SetTxFeeRatePreferenceReply) { + } + // Remove the custom bitcoin miner transaction fee rate; revert to the Bisq network's bitcoin miner transaction fee rate. + rpc UnsetTxFeeRatePreference (UnsetTxFeeRatePreferenceRequest) returns (UnsetTxFeeRatePreferenceReply) { + } + // Get a bitcoin transaction summary. + rpc GetTransaction (GetTransactionRequest) returns (GetTransactionReply) { + } + // Get all bitcoin receiving addresses in the Bisq BTC wallet. + rpc GetFundingAddresses (GetFundingAddressesRequest) returns (GetFundingAddressesReply) { + } + // Set the Bisq wallet's encryption password. + rpc SetWalletPassword (SetWalletPasswordRequest) returns (SetWalletPasswordReply) { + } + // Remove the encryption password from the Bisq wallet. + rpc RemoveWalletPassword (RemoveWalletPasswordRequest) returns (RemoveWalletPasswordReply) { + } + // Lock an encrypted Bisq wallet before the UnlockWallet rpc method's timeout period has expired. + rpc LockWallet (LockWalletRequest) returns (LockWalletReply) { + } + // Unlock a Bisq encrypted wallet before calling wallet sensitive rpc methods: CreateOffer, TakeOffer, GetBalances, + // etc., for a timeout period in seconds. An unlocked wallet will automatically lock itself after the timeout + // period has expired, or a LockWallet request has been made, whichever is first. An unlocked wallet's timeout + // setting can be overridden by subsequent UnlockWallet calls. + rpc UnlockWallet (UnlockWalletRequest) returns (UnlockWalletReply) { + } +} + +message GetBalancesRequest { + string currency_code = 1; // The Bisq wallet currency (BSQ or BTC) for the balances request. +} + +message GetBalancesReply { + BalancesInfo balances = 1; // The summary of Bisq wallet's BSQ and BTC balances. +} + +message GetAddressBalanceRequest { + string address = 1; // The BTC wallet address being queried. +} + +message GetAddressBalanceReply { + AddressBalanceInfo address_balance_info = 1; // The BTC wallet address with its balance summary. +} + +message GetUnusedBsqAddressRequest { +} + +message GetUnusedBsqAddressReply { + string address = 1; // The BSQ wallet's unused address. +} + +message SendBsqRequest { + // The external BSQ wallet address. + string address = 1; + // The amount being sent to the external BSQ wallet address, as a string in "#######,##" format. + string amount = 2; + // An optional bitcoin miner transaction fee rate, in sats/byte. If not defined, Bisq will revert + // to the custom transaction fee rate preference, if set, else the common Bisq network fee rate. + string tx_fee_rate = 3; +} + +message SendBsqReply { + // The summary of a bitcoin transaction. (BSQ is a colored coin, and transacted on the bitcoin blockchain.) + TxInfo tx_info = 1; +} + +message SendBtcRequest { + // The external bitcoin address. + string address = 1; + // The amount of BTC to send to the external address, as a string in "##.########" (BTC unit) format. + string amount = 2; + // An optional bitcoin miner transaction fee rate, in sats/byte. If not defined, Bisq will revert + // to the custom transaction fee rate preference, if set, else the common Bisq network fee rate. + string tx_fee_rate = 3; + // An optional memo associated with the bitcoin transaction. + string memo = 4; +} + +message SendBtcReply { + TxInfo tx_info = 1; // The summary of a bitcoin transaction. +} + +message VerifyBsqSentToAddressRequest { + string address = 1; // The internal BSQ wallet address. + string amount = 2; // The amount supposedly sent to the BSQ wallet address, as a string in "#######,##" format. +} + +message VerifyBsqSentToAddressReply { + // Whether a specific BSQ wallet address has received a specific amount of BSQ. If the same address has received + // the same amount of BSQ more than once, a true value does not indicate payment has been made for a v1 protocol + // BSQ-BTC trade. This BSQ payment verification problem is solved with BSQ swaps, which use a different BSQ + // address for each swap transaction. + bool is_amount_received = 1; +} + +message GetTxFeeRateRequest { +} + +message GetTxFeeRateReply { + TxFeeRateInfo tx_fee_rate_info = 1; // The summary of the most recently available bitcoin transaction fee rates. +} + +message SetTxFeeRatePreferenceRequest { + uint64 tx_fee_rate_preference = 1; +} + +message SetTxFeeRatePreferenceReply { + TxFeeRateInfo tx_fee_rate_info = 1; // The summary of the most recently available bitcoin transaction fee rates. +} + +message UnsetTxFeeRatePreferenceRequest { +} + +message UnsetTxFeeRatePreferenceReply { + TxFeeRateInfo tx_fee_rate_info = 1; // The summary of the most recently available bitcoin transaction fee rates. +} + +message GetTransactionRequest { + string tx_id = 1; +} + +message GetTransactionReply { + TxInfo tx_info = 1; // The summary of a bitcoin transaction. +} + +message GetFundingAddressesRequest { +} + +message GetFundingAddressesReply { + repeated AddressBalanceInfo address_balance_info = 1; // The list of BTC wallet addresses with their balances. +} + +message SetWalletPasswordRequest { + string password = 1; // The new password for encrypting an unencrypted Bisq wallet. + string new_password = 2; // The new password for encrypting an already encrypted Bisq wallet (a password override). +} + +message SetWalletPasswordReply { +} + +message RemoveWalletPasswordRequest { + string password = 1; // The Bisq wallet's current encryption password. +} + +message RemoveWalletPasswordReply { +} + +message LockWalletRequest { +} + +message LockWalletReply { +} + +message UnlockWalletRequest { + string password = 1; // The Bisq wallet's encryption password. + uint64 timeout = 2; // The Bisq wallet's unlock time period, in seconds. +} + +message UnlockWalletReply { +} + +message BalancesInfo { + BsqBalanceInfo bsq = 1; // BSQ wallet balance information. + BtcBalanceInfo btc = 2; // BTC wallet balance information. +} + +// TODO Thoroughly review field descriptions. +message BsqBalanceInfo { + // The BSQ amount currently available to send to other addresses at the user's discretion, in satoshis. + uint64 available_confirmed_balance = 1; + // The BSQ amount currently being used in send transactions, in satoshis. Unverified BSQ balances are + // not spendable until returned to the available_confirmed_balance when send transactions have been confirmed. + uint64 unverified_balance = 2; + // The BSQ transaction change amount tied up in unconfirmed transactions, remaining unspendable until transactions + // have been confirmed and the change returned to the available_confirmed_balance. + uint64 unconfirmed_change_balance = 3; + // The locked BSQ amount held by DAO voting transaction. + uint64 locked_for_voting_balance = 4; + // The locked BSQ amount held by DAO bonding transaction. + uint64 lockup_bonds_balance = 5; + // The BSQ bonding amount in unlocking state, awaiting a lockup transaction's lock time expiry before the funds + // can be spent in normal transactions. + uint64 unlocking_bonds_balance = 6; +} + +// TODO Thoroughly review field descriptions. +message BtcBalanceInfo { + // The BTC amount currently available to send to other addresses at the user's discretion, in satoshis. + uint64 available_balance = 1; + // The BTC amount currently reserved to cover open offers' security deposits, and BTC sellers' payout amounts, + // in satoshis. Reserved funds are not spendable, but are recoverable by users. When a user cancels an offer + // funds reserved for that offer are returned to the available_balance. + uint64 reserved_balance = 2; + // The sum of available_balance + reserved_balance, in satoshis. + uint64 total_available_balance = 3; + // The BTC amount being locked to cover the security deposits and BTC seller's pending trade payouts. Locked + // funds are not recoverable until a trade is completed, when security deposits are returned to the available_balance. + uint64 locked_balance = 4; +} + +message AddressBalanceInfo { + string address = 1; // The bitcoin wallet address. + int64 balance = 2; // The address' BTC balance in satoshis. + int64 num_confirmations = 3; // The number of confirmations for the most recent transaction referencing the output address. + bool is_address_unused = 4; // Whether the bitcoin address has ever been used, or not. +} + +service GetVersion { + // Get the current Bisq version number. + rpc GetVersion (GetVersionRequest) returns (GetVersionReply) { + } +} + +message GetVersionRequest { +} + +message GetVersionReply { + string version = 1; // The version of the Bisq software release. +} diff --git a/proto-downloader/pb.proto b/proto-downloader/pb.proto new file mode 100644 index 0000000..8a63d6a --- /dev/null +++ b/proto-downloader/pb.proto @@ -0,0 +1,2477 @@ +/* + * This file is part of Bisq. + * + * Bisq is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bisq is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bisq. If not, see . + */ + +syntax = "proto3"; +package io.bisq.protobuffer; +option java_package = "protobuf"; +option java_multiple_files = true; + +message NetworkEnvelope { + int32 message_version = 1; + oneof message { + PreliminaryGetDataRequest preliminary_get_data_request = 2; + GetDataResponse get_data_response = 3; + GetUpdatedDataRequest get_updated_data_request = 4; + + GetPeersRequest get_peers_request = 5; + GetPeersResponse get_peers_response = 6; + Ping ping = 7; + Pong pong = 8; + + OfferAvailabilityRequest offer_availability_request = 9; + OfferAvailabilityResponse offer_availability_response = 10; + RefreshOfferMessage refresh_offer_message = 11; + + AddDataMessage add_data_message = 12; + RemoveDataMessage remove_data_message = 13; + RemoveMailboxDataMessage remove_mailbox_data_message = 14; + + CloseConnectionMessage close_connection_message = 15; + PrefixedSealedAndSignedMessage prefixed_sealed_and_signed_message = 16; + + InputsForDepositTxRequest inputs_for_deposit_tx_request = 17; + InputsForDepositTxResponse inputs_for_deposit_tx_response = 18; + DepositTxMessage deposit_tx_message = 19; + CounterCurrencyTransferStartedMessage counter_currency_transfer_started_message = 20; + PayoutTxPublishedMessage payout_tx_published_message = 21; + + OpenNewDisputeMessage open_new_dispute_message = 22; + PeerOpenedDisputeMessage peer_opened_dispute_message = 23; + ChatMessage chat_message = 24; + DisputeResultMessage dispute_result_message = 25; + PeerPublishedDisputePayoutTxMessage peer_published_dispute_payout_tx_message = 26; + + PrivateNotificationMessage private_notification_message = 27; + + GetBlocksRequest get_blocks_request = 28; + GetBlocksResponse get_blocks_response = 29; + NewBlockBroadcastMessage new_block_broadcast_message = 30; + + AddPersistableNetworkPayloadMessage add_persistable_network_payload_message = 31; + AckMessage ack_message = 32; + RepublishGovernanceDataRequest republish_governance_data_request = 33; + NewDaoStateHashMessage new_dao_state_hash_message = 34; + GetDaoStateHashesRequest get_dao_state_hashes_request = 35; + GetDaoStateHashesResponse get_dao_state_hashes_response = 36; + NewProposalStateHashMessage new_proposal_state_hash_message = 37; + GetProposalStateHashesRequest get_proposal_state_hashes_request = 38; + GetProposalStateHashesResponse get_proposal_state_hashes_response = 39; + NewBlindVoteStateHashMessage new_blind_vote_state_hash_message = 40; + GetBlindVoteStateHashesRequest get_blind_vote_state_hashes_request = 41; + GetBlindVoteStateHashesResponse get_blind_vote_state_hashes_response = 42; + + BundleOfEnvelopes bundle_of_envelopes = 43; + MediatedPayoutTxSignatureMessage mediated_payout_tx_signature_message = 44; + MediatedPayoutTxPublishedMessage mediated_payout_tx_published_message = 45; + + DelayedPayoutTxSignatureRequest delayed_payout_tx_signature_request = 46; + DelayedPayoutTxSignatureResponse delayed_payout_tx_signature_response = 47; + DepositTxAndDelayedPayoutTxMessage deposit_tx_and_delayed_payout_tx_message = 48; + PeerPublishedDelayedPayoutTxMessage peer_published_delayed_payout_tx_message = 49; + + RefreshTradeStateRequest refresh_trade_state_request = 50 [deprecated = true]; + TraderSignedWitnessMessage trader_signed_witness_message = 51 [deprecated = true]; + + GetInventoryRequest get_inventory_request = 52; + GetInventoryResponse get_inventory_response = 53; + + ShareBuyerPaymentAccountMessage share_buyer_payment_account_message = 54; // Added at 1.7.0 + + SellersBsqSwapRequest sellers_bsq_swap_request = 55; + BuyersBsqSwapRequest buyers_bsq_swap_request = 56; + BsqSwapTxInputsMessage bsq_swap_tx_inputs_message = 57; + BsqSwapFinalizeTxRequest bsq_swap_finalize_tx_request = 58; + BsqSwapFinalizedTxMessage bsq_swap_finalized_tx_message = 59; + + FileTransferPart file_transfer_part = 60; + } +} + +message BundleOfEnvelopes { + repeated NetworkEnvelope envelopes = 1; +} + +message PreliminaryGetDataRequest { + int32 nonce = 21; // This was set to 21 instead of 1 in some old commit so we cannot change it. + repeated bytes excluded_keys = 2; + repeated int32 supported_capabilities = 3; + string version = 4; +} + +message GetDataResponse { + int32 request_nonce = 1; + bool is_get_updated_data_response = 2; + repeated StorageEntryWrapper data_set = 3; + repeated int32 supported_capabilities = 4; + repeated PersistableNetworkPayload persistable_network_payload_items = 5; +} + +message GetUpdatedDataRequest { + NodeAddress sender_node_address = 1; + int32 nonce = 2; + repeated bytes excluded_keys = 3; + string version = 4; +} + +message FileTransferPart { + NodeAddress sender_node_address = 1; + string uid = 2; + string trade_id = 3; + int32 trader_id = 4; + int64 seq_num_or_file_length = 5; + bytes message_data = 6; +} + +message GetPeersRequest { + NodeAddress sender_node_address = 1; + int32 nonce = 2; + repeated int32 supported_capabilities = 3; + repeated Peer reported_peers = 4; +} + +message GetPeersResponse { + int32 request_nonce = 1; + repeated Peer reported_peers = 2; + repeated int32 supported_capabilities = 3; +} + +message Ping { + int32 nonce = 1; + int32 last_round_trip_time = 2; +} + +message Pong { + int32 request_nonce = 1; +} + +message GetInventoryRequest { + string version = 1; +} + +message GetInventoryResponse { + map inventory = 1; +} + +message OfferAvailabilityRequest { + string offer_id = 1; + PubKeyRing pub_key_ring = 2; + int64 takers_trade_price = 3; + repeated int32 supported_capabilities = 4; + string uid = 5; + bool is_taker_api_user = 6; +} + +message OfferAvailabilityResponse { + string offer_id = 1; + AvailabilityResult availability_result = 2; + repeated int32 supported_capabilities = 3; + string uid = 4; + NodeAddress arbitrator = 5; + NodeAddress mediator = 6; + NodeAddress refund_agent = 7; +} + +message RefreshOfferMessage { + bytes hash_of_data_and_seq_nr = 1; + bytes signature = 2; + bytes hash_of_payload = 3; + int32 sequence_number = 4; +} + +message AddDataMessage { + StorageEntryWrapper entry = 1; +} + +message RemoveDataMessage { + ProtectedStorageEntry protected_storage_entry = 1; +} + +message RemoveMailboxDataMessage { + ProtectedMailboxStorageEntry protected_storage_entry = 1; +} + +message AddPersistableNetworkPayloadMessage { + PersistableNetworkPayload payload = 1; +} + +message CloseConnectionMessage { + string reason = 1; +} + +message AckMessage { + string uid = 1; + NodeAddress sender_node_address = 2; + string source_type = 3; // enum name. e.g. TradeMessage, DisputeMessage,... + string source_msg_class_name = 4; + string source_uid = 5; // uid of source (TradeMessage) + string source_id = 6; // id of source (tradeId, disputeId) + bool success = 7; // true if source message was processed successfully + string error_message = 8; // optional error message if source message processing failed +} + +message PrefixedSealedAndSignedMessage { + NodeAddress node_address = 1; + SealedAndSigned sealed_and_signed = 2; + bytes address_prefix_hash = 3; + string uid = 4; +} + +message InputsForDepositTxRequest { + string trade_id = 1; + NodeAddress sender_node_address = 2; + int64 trade_amount = 3; + int64 trade_price = 4; + int64 tx_fee = 5; + int64 taker_fee = 6; + bool is_currency_for_taker_fee_btc = 7; + repeated RawTransactionInput raw_transaction_inputs = 8; + int64 change_output_value = 9; + string change_output_address = 10; + bytes taker_multi_sig_pub_key = 11; + string taker_payout_address_string = 12; + PubKeyRing taker_pub_key_ring = 13; + // Not used anymore from 1.7.0 but kept for backward compatibility. + PaymentAccountPayload taker_payment_account_payload = 14; + string taker_account_id = 15; + string taker_fee_tx_id = 16; + repeated NodeAddress accepted_arbitrator_node_addresses = 17; + repeated NodeAddress accepted_mediator_node_addresses = 18; + NodeAddress arbitrator_node_address = 19; + NodeAddress mediator_node_address = 20; + string uid = 21; + bytes account_age_witness_signature_of_offer_id = 22; + int64 current_date = 23; + repeated NodeAddress accepted_refund_agent_node_addresses = 24; + NodeAddress refund_agent_node_address = 25; + bytes hash_of_takers_payment_account_payload = 26; + string takers_payout_method_id = 27; +} + +message InputsForDepositTxResponse { + string trade_id = 1; + // Not used anymore from 1.7.0 but kept for backward compatibility. + PaymentAccountPayload maker_payment_account_payload = 2; + string maker_account_id = 3; + string maker_contract_as_json = 4; + string maker_contract_signature = 5; + string maker_payout_address_string = 6; + bytes prepared_deposit_tx = 7; + repeated RawTransactionInput maker_inputs = 8; + bytes maker_multi_sig_pub_key = 9; + NodeAddress sender_node_address = 10; + string uid = 11; + bytes account_age_witness_signature_of_prepared_deposit_tx = 12; + int64 current_date = 13; + int64 lock_time = 14; + bytes hash_of_makers_payment_account_payload = 15; + string makers_payout_method_id = 16; +} + +message DelayedPayoutTxSignatureRequest { + string uid = 1; + string trade_id = 2; + NodeAddress sender_node_address = 3; + bytes delayed_payout_tx = 4; + bytes delayed_payout_tx_seller_signature = 5; +} + +message DelayedPayoutTxSignatureResponse { + string uid = 1; + string trade_id = 2; + NodeAddress sender_node_address = 3; + bytes delayed_payout_tx_buyer_signature = 4; + bytes deposit_tx = 5; +} + +message DepositTxAndDelayedPayoutTxMessage { + string uid = 1; + string trade_id = 2; + NodeAddress sender_node_address = 3; + bytes deposit_tx = 4; + bytes delayed_payout_tx = 5; + PaymentAccountPayload seller_payment_account_payload = 6; +} + +message ShareBuyerPaymentAccountMessage { + string uid = 1; + string trade_id = 2; + NodeAddress sender_node_address = 3; + PaymentAccountPayload buyer_payment_account_payload = 4; +} + +message DepositTxMessage { + string uid = 1; + string trade_id = 2; + NodeAddress sender_node_address = 3; + bytes deposit_tx_without_witnesses = 4; +} + +message PeerPublishedDelayedPayoutTxMessage { + string uid = 1; + string trade_id = 2; + NodeAddress sender_node_address = 3; +} + +message CounterCurrencyTransferStartedMessage { + string trade_id = 1; + string buyer_payout_address = 2; + NodeAddress sender_node_address = 3; + bytes buyer_signature = 4; + string counter_currency_tx_id = 5; + string uid = 6; + string counter_currency_extra_data = 7; +} + +message FinalizePayoutTxRequest { + string trade_id = 1; + bytes seller_signature = 2; + string seller_payout_address = 3; + NodeAddress sender_node_address = 4; + string uid = 5; +} + +message PayoutTxPublishedMessage { + string trade_id = 1; + bytes payout_tx = 2; + NodeAddress sender_node_address = 3; + string uid = 4; + SignedWitness signed_witness = 5; // Added in v1.4.0 +} + +message MediatedPayoutTxPublishedMessage { + string trade_id = 1; + bytes payout_tx = 2; + NodeAddress sender_node_address = 3; + string uid = 4; +} + +message MediatedPayoutTxSignatureMessage { + string uid = 1; + string trade_id = 3; + bytes tx_signature = 2; + NodeAddress sender_node_address = 4; +} + +/* Deprecated since 1.4.0. */ +message RefreshTradeStateRequest { + string uid = 1 [deprecated = true]; + string trade_id = 2 [deprecated = true]; + NodeAddress sender_node_address = 3 [deprecated = true]; +} + +/* Deprecated since 1.4.0. */ +message TraderSignedWitnessMessage { + string uid = 1 [deprecated = true]; + string trade_id = 2 [deprecated = true]; + NodeAddress sender_node_address = 3 [deprecated = true]; + SignedWitness signed_witness = 4 [deprecated = true]; +} + +message SellersBsqSwapRequest { + string uid = 1; + string trade_id = 2; + NodeAddress sender_node_address = 3; + PubKeyRing taker_pub_key_ring = 4; + int64 trade_amount = 5; + int64 tx_fee_per_vbyte = 6; + int64 maker_fee = 7; + int64 taker_fee = 8; + int64 trade_date = 9; +} + +message BuyersBsqSwapRequest { + string uid = 1; + string trade_id = 2; + NodeAddress sender_node_address = 3; + PubKeyRing taker_pub_key_ring = 4; + int64 trade_amount = 5; + int64 tx_fee_per_vbyte = 6; + int64 maker_fee = 7; + int64 taker_fee = 8; + int64 trade_date = 9; + repeated RawTransactionInput bsq_inputs = 10; + int64 bsq_change = 11; + string buyers_btc_payout_address = 12; + string buyers_bsq_change_address = 13; +} + +message BsqSwapTxInputsMessage { + string uid = 1; + string trade_id = 2; + NodeAddress sender_node_address = 3; + repeated RawTransactionInput bsq_inputs = 4; + int64 bsq_change = 5; + string buyers_btc_payout_address = 6; + string buyers_bsq_change_address = 7; +} + +message BsqSwapFinalizeTxRequest { + string uid = 1; + string trade_id = 2; + NodeAddress sender_node_address = 3; + bytes tx = 4; + repeated RawTransactionInput btc_inputs = 5; + int64 btc_change = 6; + string bsq_payout_address = 7; + string btc_change_address = 8; +} + +message BsqSwapFinalizedTxMessage { + string uid = 1; + string trade_id = 2; + NodeAddress sender_node_address = 3; + bytes tx = 4; +} + +/* Dispute support types. */ +enum SupportType { + ARBITRATION = 0; + MEDIATION = 1; + TRADE = 2; + REFUND = 3; +} + +message OpenNewDisputeMessage { + Dispute dispute = 1; + NodeAddress sender_node_address = 2; + string uid = 3; + SupportType type = 4; +} + +message PeerOpenedDisputeMessage { + Dispute dispute = 1; + NodeAddress sender_node_address = 2; + string uid = 3; + SupportType type = 4; +} + +message ChatMessage { + int64 date = 1; + string trade_id = 2; + int32 trader_id = 3; + bool sender_is_trader = 4; + string message = 5; + repeated Attachment attachments = 6; + bool arrived = 7; + bool stored_in_mailbox = 8; + bool is_system_message = 9; + NodeAddress sender_node_address = 10; + string uid = 11; + string send_message_error = 12; + bool acknowledged = 13; + string ack_error = 14; + SupportType type = 15; + bool was_displayed = 16; +} + +message DisputeResultMessage { + string uid = 1; + DisputeResult dispute_result = 2; + NodeAddress sender_node_address = 3; + SupportType type = 4; +} + +message PeerPublishedDisputePayoutTxMessage { + string uid = 1; + bytes transaction = 2; + string trade_id = 3; + NodeAddress sender_node_address = 4; + SupportType type = 5; +} + +message PrivateNotificationMessage { + string uid = 1; + NodeAddress sender_node_address = 2; + PrivateNotificationPayload private_notification_payload = 3; +} + +/* DAO blocks request. */ +message GetBlocksRequest { + int32 from_block_height = 1; + int32 nonce = 2; + NodeAddress sender_node_address = 3; + repeated int32 supported_capabilities = 4; +} + +/* DAO blocks response. */ +message GetBlocksResponse { + // Because of the way how PB implements inheritance we need to use the super class as type. + repeated BaseBlock raw_blocks = 1; + int32 request_nonce = 2; +} + +message NewBlockBroadcastMessage { + // Because of the way how PB implements inheritance we need to use the super class as type. + BaseBlock raw_block = 1; +} + +message RepublishGovernanceDataRequest { +} + +message NewDaoStateHashMessage { + DaoStateHash state_hash = 1; +} + +message NewProposalStateHashMessage { + ProposalStateHash state_hash = 1; +} + +message NewBlindVoteStateHashMessage { + BlindVoteStateHash state_hash = 1; +} + +message GetDaoStateHashesRequest { + int32 height = 1; + int32 nonce = 2; +} + +message GetProposalStateHashesRequest { + int32 height = 1; + int32 nonce = 2; +} + +message GetBlindVoteStateHashesRequest { + int32 height = 1; + int32 nonce = 2; +} + +message GetDaoStateHashesResponse { + repeated DaoStateHash state_hashes = 1; + int32 request_nonce = 2; +} + +message GetProposalStateHashesResponse { + repeated ProposalStateHash state_hashes = 1; + int32 request_nonce = 2; +} + +message GetBlindVoteStateHashesResponse { + repeated BlindVoteStateHash state_hashes = 1; + int32 request_nonce = 2; +} + +message NodeAddress { + string host_name = 1; + int32 port = 2; +} + +message Peer { + NodeAddress node_address = 1; + int64 date = 2; + repeated int32 supported_capabilities = 3; +} + +message PubKeyRing { + bytes signature_pub_key_bytes = 1; + bytes encryption_pub_key_bytes = 2; + reserved 3; // Formerly: string pgp_pub_key_as_pem = 3; +} + +message SealedAndSigned { + bytes encrypted_secret_key = 1; + bytes encrypted_payload_with_hmac = 2; + bytes signature = 3; + bytes sig_public_key_bytes = 4; +} + +message StoragePayload { + oneof message { + Alert alert = 1; + Arbitrator arbitrator = 2; + Mediator mediator = 3; + Filter filter = 4; + + // TradeStatistics trade_statistics = 5 [deprecated = true]; Removed in v.1.4.0 + + MailboxStoragePayload mailbox_storage_payload = 6; + OfferPayload offer_payload = 7; + TempProposalPayload temp_proposal_payload = 8; + RefundAgent refund_agent = 9; + BsqSwapOfferPayload bsq_swap_offer_payload = 10; + } +} + +message PersistableNetworkPayload { + oneof message { + AccountAgeWitness account_age_witness = 1; + TradeStatistics2 trade_statistics2 = 2 [deprecated = true]; + ProposalPayload proposal_payload = 3; + BlindVotePayload blind_vote_payload = 4; + SignedWitness signed_witness = 5; + TradeStatistics3 trade_statistics3 = 6; + } +} + +message ProtectedStorageEntry { + StoragePayload storagePayload = 1; + bytes owner_pub_key_bytes = 2; + int32 sequence_number = 3; + bytes signature = 4; + int64 creation_time_stamp = 5; +} + +message StorageEntryWrapper { + oneof message { + ProtectedStorageEntry protected_storage_entry = 1; + ProtectedMailboxStorageEntry protected_mailbox_storage_entry = 2; + } +} + +message ProtectedMailboxStorageEntry { + ProtectedStorageEntry entry = 1; + bytes receivers_pub_key_bytes = 2; +} + +message DataAndSeqNrPair { + StoragePayload payload = 1; + int32 sequence_number = 2; +} + +message MailboxMessageList { + repeated MailboxItem mailbox_item = 1; +} + +message RemovedPayloadsMap { + map date_by_hashes = 1; +} + +message IgnoredMailboxMap { + map data = 1; +} + +message MailboxItem { + ProtectedMailboxStorageEntry protected_mailbox_storage_entry = 1; + DecryptedMessageWithPubKey decrypted_message_with_pub_key = 2; +} + +message DecryptedMessageWithPubKey { + NetworkEnvelope network_envelope = 1; + bytes signature_pub_key_bytes = 2; +} + +message PrivateNotificationPayload { + string message = 1; + string signature_as_base64 = 2; + bytes sig_public_key_bytes = 3; +} + +message PaymentAccountFilter { + string payment_method_id = 1; + string get_method_name = 2; + string value = 3; +} + +message Alert { + string message = 1; + string version = 2; + bool is_update_info = 3; + string signature_as_base64 = 4; + bytes owner_pub_key_bytes = 5; + map extra_data = 6; + bool is_pre_release_info = 7; +} + +message Arbitrator { + NodeAddress node_address = 1; + repeated string language_codes = 2; + int64 registration_date = 3; + string registration_signature = 4; + bytes registration_pub_key = 5; + PubKeyRing pub_key_ring = 6; + bytes btc_pub_key = 7; + string btc_address = 8; + string email_address = 9; + string info = 10; + map extra_data = 11; +} + +message Mediator { + NodeAddress node_address = 1; + repeated string language_codes = 2; + int64 registration_date = 3; + string registration_signature = 4; + bytes registration_pub_key = 5; + PubKeyRing pub_key_ring = 6; + string email_address = 7; + string info = 8; + map extra_data = 9; +} + +message RefundAgent { + NodeAddress node_address = 1; + repeated string language_codes = 2; + int64 registration_date = 3; + string registration_signature = 4; + bytes registration_pub_key = 5; + PubKeyRing pub_key_ring = 6; + string email_address = 7; + string info = 8; + map extra_data = 9; +} + +message Filter { + repeated string node_addresses_banned_from_trading = 1; + repeated string banned_offer_ids = 2; + repeated PaymentAccountFilter banned_payment_accounts = 3; + string signature_as_base64 = 4; + bytes owner_pub_key_bytes = 5; + map extra_data = 6; + repeated string banned_currencies = 7; + repeated string banned_payment_methods = 8; + repeated string arbitrators = 9; + repeated string seed_nodes = 10; + repeated string price_relay_nodes = 11; + bool prevent_public_btc_network = 12; + repeated string btc_nodes = 13; + bool disable_dao = 14; + string disable_dao_below_version = 15; + string disable_trade_below_version = 16; + repeated string mediators = 17; + repeated string refundAgents = 18; + repeated string bannedSignerPubKeys = 19; + repeated string btc_fee_receiver_addresses = 20; + int64 creation_date = 21; + string signer_pub_key_as_hex = 22; + repeated string bannedPrivilegedDevPubKeys = 23; + bool disable_auto_conf = 24; + repeated string banned_auto_conf_explorers = 25; + repeated string node_addresses_banned_from_network = 26; + bool disable_api = 27; + bool disable_mempool_validation = 28; + bool disable_pow_message = 29; + double pow_difficulty = 30; + int64 maker_fee_btc = 31; + int64 taker_fee_btc = 32; + int64 maker_fee_bsq = 33; + int64 taker_fee_bsq = 34; + repeated int32 enabled_pow_versions = 35; +} + +/* Deprecated */ +message TradeStatistics2 { + string base_currency = 1 [deprecated = true]; + string counter_currency = 2 [deprecated = true]; + OfferDirection direction = 3 [deprecated = true]; + int64 trade_price = 4 [deprecated = true]; + int64 trade_amount = 5 [deprecated = true]; + int64 trade_date = 6 [deprecated = true]; + string payment_method_id = 7 [deprecated = true]; + int64 offer_date = 8 [deprecated = true]; + bool offer_use_market_based_price = 9 [deprecated = true]; + double offer_market_price_margin = 10 [deprecated = true]; + int64 offer_amount = 11 [deprecated = true]; + int64 offer_min_amount = 12 [deprecated = true]; + string offer_id = 13 [deprecated = true]; + string deposit_tx_id = 14 [deprecated = true]; + bytes hash = 15 [deprecated = true]; + map extra_data = 16 [deprecated = true]; +} + +message TradeStatistics3 { + string currency = 1; + int64 price = 2; + int64 amount = 3; + string payment_method = 4; + int64 date = 5; + string mediator = 6; + string refund_agent = 7; + bytes hash = 8; + map extra_data = 9; +} + +message MailboxStoragePayload { + PrefixedSealedAndSignedMessage prefixed_sealed_and_signed_message = 1; + bytes sender_pub_key_for_add_operation_bytes = 2; + bytes owner_pub_key_bytes = 3; + map extra_data = 4; +} + +message OfferPayload { + string id = 1; + int64 date = 2; + NodeAddress owner_node_address = 3; + PubKeyRing pub_key_ring = 4; + OfferDirection direction = 5; + int64 price = 6; + double market_price_margin = 7; + bool use_market_based_price = 8; + int64 amount = 9; + int64 min_amount = 10; + string base_currency_code = 11; + string counter_currency_code = 12; + // Not used anymore but still required as old clients check for nonNull. + repeated NodeAddress arbitrator_node_addresses = 13 [deprecated = true]; + // Not used anymore but still required as old clients check for nonNull. + repeated NodeAddress mediator_node_addresses = 14 [deprecated = true]; + string payment_method_id = 15; + string maker_payment_account_id = 16; + string offer_fee_payment_tx_id = 17; + string country_code = 18; + repeated string accepted_country_codes = 19; + string bank_id = 20; + repeated string accepted_bank_ids = 21; + string version_nr = 22; + int64 block_height_at_offer_creation = 23; + int64 tx_fee = 24; + int64 maker_fee = 25; + bool is_currency_for_maker_fee_btc = 26; + int64 buyer_security_deposit = 27; + int64 seller_security_deposit = 28; + int64 max_trade_limit = 29; + int64 max_trade_period = 30; + bool use_auto_close = 31; + bool use_re_open_after_auto_close = 32; + int64 lower_close_price = 33; + int64 upper_close_price = 34; + bool is_private_offer = 35; + string hash_of_challenge = 36; + map extra_data = 37; + int32 protocol_version = 38; +} + +enum OfferDirection { + OFFER_DIRECTION_ERROR = 0; + BUY = 1; + SELL = 2; +} + +message BsqSwapOfferPayload { + string id = 1; + int64 date = 2; + NodeAddress owner_node_address = 3; + PubKeyRing pub_key_ring = 4; + OfferDirection direction = 5; + int64 price = 6; + int64 amount = 7; + int64 min_amount = 8; + ProofOfWork proof_of_work = 9; + map extra_data = 10; + string version_nr = 11; + int32 protocol_version = 12; +} + +message ProofOfWork { + bytes payload = 1; + int64 counter = 2; + bytes challenge = 3; + double difficulty = 4; + int64 duration = 5; + bytes solution = 6; + int32 version = 7; +} + +message AccountAgeWitness { + bytes hash = 1; + int64 date = 2; +} + +message SignedWitness { + enum VerificationMethod { + PB_ERROR = 0; + ARBITRATOR = 1; + TRADE = 2; + } + + VerificationMethod verification_method = 1; + bytes account_age_witness_hash = 2; + bytes signature = 3; + bytes signer_pub_key = 4; + bytes witness_owner_pub_key = 5; + int64 date = 6; + int64 trade_amount = 7; +} + +message Dispute { + enum State { + NEEDS_UPGRADE = 0; + NEW = 1; + OPEN = 2; + REOPENED = 3; + CLOSED = 4; + } + string trade_id = 1; + string id = 2; + int32 trader_id = 3; + bool dispute_opener_is_buyer = 4; + bool dispute_opener_is_maker = 5; + int64 opening_date = 6; + PubKeyRing trader_pub_key_ring = 7; + int64 trade_date = 8; + Contract contract = 9; + bytes contract_hash = 10; + bytes deposit_tx_serialized = 11; + bytes payout_tx_serialized = 12; + string deposit_tx_id = 13; + string payout_tx_id = 14; + string contract_as_json = 15; + string maker_contract_signature = 16; + string taker_contract_signature = 17; + PubKeyRing agent_pub_key_ring = 18; + bool is_support_ticket = 19; + repeated ChatMessage chat_message = 20; + bool is_closed = 21; + DisputeResult dispute_result = 22; + string dispute_payout_tx_id = 23; + SupportType support_type = 24; + string mediators_dispute_result = 25; + string delayed_payout_tx_id = 26; + string donation_address_of_delayed_payout_tx = 27; + State state = 28; + int64 trade_period_end = 29; + map extra_data = 30; +} + +message Attachment { + string file_name = 1; + bytes bytes = 2; +} + +message DisputeResult { + enum Winner { + PB_ERROR_WINNER = 0; + BUYER = 1; + SELLER = 2; + } + + enum Reason { + PB_ERROR_REASON = 0; + OTHER = 1; + BUG = 2; + USABILITY = 3; + SCAM = 4; + PROTOCOL_VIOLATION = 5; + NO_REPLY = 6; + BANK_PROBLEMS = 7; + OPTION_TRADE = 8; + SELLER_NOT_RESPONDING = 9; + WRONG_SENDER_ACCOUNT = 10; + TRADE_ALREADY_SETTLED = 11; + PEER_WAS_LATE = 12; + } + + enum PayoutSuggestion { + CUSTOM_PAYOUT = 0; + BUYER_GETS_TRADE_AMOUNT = 1; + BUYER_GETS_TRADE_AMOUNT_PLUS_COMPENSATION = 2; + BUYER_GETS_TRADE_AMOUNT_MINUS_PENALTY = 3; + SELLER_GETS_TRADE_AMOUNT = 4; + SELLER_GETS_TRADE_AMOUNT_PLUS_COMPENSATION = 5; + SELLER_GETS_TRADE_AMOUNT_MINUS_PENALTY = 6; + } + + string trade_id = 1; + int32 trader_id = 2; + Winner winner = 3; + int32 reason_ordinal = 4; + bool tamper_proof_evidence = 5; + bool id_verification = 6; + bool screen_cast = 7; + string summary_notes = 8; + ChatMessage chat_message = 9; + bytes arbitrator_signature = 10; + int64 buyer_payout_amount = 11; + int64 seller_payout_amount = 12; + bytes arbitrator_pub_key = 13; + int64 close_date = 14; + bool is_loser_publisher = 15; + string payout_adjustment_percent = 16; + PayoutSuggestion payout_suggestion = 17; +} + +message Contract { + OfferPayload offer_payload = 1; + int64 trade_amount = 2; + int64 trade_price = 3; + string taker_fee_tx_id = 4; + reserved 5; // WAS: arbitrator_node_address + bool is_buyer_maker_and_seller_taker = 6; + string maker_account_id = 7; + string taker_account_id = 8; + // Not used anymore from 1.7.0 but kept for backward compatibility. + PaymentAccountPayload maker_payment_account_payload = 9; + // Not used anymore from 1.7.0 but kept for backward compatibility. + PaymentAccountPayload taker_payment_account_payload = 10; + PubKeyRing maker_pub_key_ring = 11; + PubKeyRing taker_pub_key_ring = 12; + NodeAddress buyer_node_address = 13; + NodeAddress seller_node_address = 14; + string maker_payout_address_string = 15; + string taker_payout_address_string = 16; + bytes maker_multi_sig_pub_key = 17; + bytes taker_multi_sig_pub_key = 18; + NodeAddress mediator_node_address = 19; + int64 lock_time = 20; + NodeAddress refund_agent_node_address = 21; + bytes hash_of_makers_payment_account_payload = 22; + bytes hash_of_takers_payment_account_payload = 23; + string maker_payment_method_id = 24; + string taker_payment_method_id = 25; +} + +message RawTransactionInput { + int64 index = 1; + bytes parent_transaction = 2; + int64 value = 3; + int32 script_type_id = 4; +} + +enum AvailabilityResult { + PB_ERROR = 0; + UNKNOWN_FAILURE = 1; + AVAILABLE = 2; + OFFER_TAKEN = 3; + PRICE_OUT_OF_TOLERANCE = 4; + MARKET_PRICE_NOT_AVAILABLE = 5; + NO_ARBITRATORS = 6; + NO_MEDIATORS = 7; + USER_IGNORED = 8; + MISSING_MANDATORY_CAPABILITY = 9; + NO_REFUND_AGENTS = 10; + UNCONF_TX_LIMIT_HIT = 11; + MAKER_DENIED_API_USER = 12; + PRICE_CHECK_FAILED = 13; +} + +message PaymentAccountPayload { + string id = 1; + string payment_method_id = 2; + // Not used anymore but we need to keep it in PB for backward compatibility. + int64 max_trade_period = 3 [deprecated = true]; + oneof message { + AliPayAccountPayload ali_pay_account_payload = 4; + ChaseQuickPayAccountPayload chase_quick_pay_account_payload = 5; + ClearXchangeAccountPayload clear_xchange_account_payload = 6; + CountryBasedPaymentAccountPayload country_based_payment_account_payload = 7; + CryptoCurrencyAccountPayload crypto_currency_account_payload = 8; + FasterPaymentsAccountPayload faster_payments_account_payload = 9; + InteracETransferAccountPayload interac_e_transfer_account_payload = 10; + OKPayAccountPayload o_k_pay_account_payload = 11 [deprecated = true]; + PerfectMoneyAccountPayload perfect_money_account_payload = 12; + SwishAccountPayload swish_account_payload = 13; + USPostalMoneyOrderAccountPayload u_s_postal_money_order_account_payload = 14; + UpholdAccountPayload uphold_account_payload = 16; + CashAppAccountPayload cash_app_account_payload = 17 [deprecated = true]; + MoneyBeamAccountPayload money_beam_account_payload = 18; + VenmoAccountPayload venmo_account_payload = 19 [deprecated = true]; + PopmoneyAccountPayload popmoney_account_payload = 20; + RevolutAccountPayload revolut_account_payload = 21; + WeChatPayAccountPayload we_chat_pay_account_payload = 22; + MoneyGramAccountPayload money_gram_account_payload = 23; + HalCashAccountPayload hal_cash_account_payload = 24; + PromptPayAccountPayload prompt_pay_account_payload = 25; + AdvancedCashAccountPayload advanced_cash_account_payload = 26; + InstantCryptoCurrencyAccountPayload instant_crypto_currency_account_payload = 27; + JapanBankAccountPayload japan_bank_account_payload = 28; + TransferwiseAccountPayload Transferwise_account_payload = 29; + AustraliaPayidPayload australia_payid_payload = 30; + AmazonGiftCardAccountPayload amazon_gift_card_account_payload = 31; + CashByMailAccountPayload cash_by_mail_account_payload = 32; + CapitualAccountPayload capitual_account_payload = 33; + PayseraAccountPayload Paysera_account_payload = 34; + PaxumAccountPayload Paxum_account_payload = 35; + SwiftAccountPayload swift_account_payload = 36; + CelPayAccountPayload cel_pay_account_payload = 37; + MoneseAccountPayload monese_account_payload = 38; + VerseAccountPayload verse_account_payload = 39; + BsqSwapAccountPayload bsq_swap_account_payload = 40; + } + map exclude_from_json_data = 15; +} + +message AliPayAccountPayload { + string account_nr = 1; +} + +message WeChatPayAccountPayload { + string account_nr = 1; +} + +message ChaseQuickPayAccountPayload { + string email = 1; + string holder_name = 2; +} + +message ClearXchangeAccountPayload { + string holder_name = 1; + string email_or_mobile_nr = 2; +} + +message CountryBasedPaymentAccountPayload { + string countryCode = 1; + oneof message { + BankAccountPayload bank_account_payload = 2; + CashDepositAccountPayload cash_deposit_account_payload = 3; + SepaAccountPayload sepa_account_payload = 4; + WesternUnionAccountPayload western_union_account_payload = 5; + SepaInstantAccountPayload sepa_instant_account_payload = 6; + F2FAccountPayload f2f_account_payload = 7; + UpiAccountPayload upi_account_payload = 9; + PaytmAccountPayload paytm_account_payload = 10; + IfscBasedAccountPayload ifsc_based_account_payload = 11; + NequiAccountPayload nequi_account_payload = 12; + BizumAccountPayload bizum_account_payload = 13; + PixAccountPayload pix_account_payload = 14; + SatispayAccountPayload satispay_account_payload = 15; + StrikeAccountPayload strike_account_payload = 16; + TikkieAccountPayload tikkie_account_payload = 17; + TransferwiseUsdAccountPayload transferwise_usd_account_payload = 18; + } +} + +message BankAccountPayload { + string holder_name = 1; + string bank_name = 2; + string bank_id = 3; + string branch_id = 4; + string account_nr = 5; + string account_type = 6; + string holder_tax_id = 7; + string email = 8 [deprecated = true]; + oneof message { + NationalBankAccountPayload national_bank_account_payload = 9; + SameBankAccountPayload same_bank_accont_payload = 10; + SpecificBanksAccountPayload specific_banks_account_payload = 11; + AchTransferAccountPayload ach_transfer_account_payload = 13; + DomesticWireTransferAccountPayload domestic_wire_transfer_account_payload = 14; + } + string national_account_id = 12; +} + +message AchTransferAccountPayload { + string holder_address = 1; +} + +message DomesticWireTransferAccountPayload { + string holder_address = 1; +} + +message NationalBankAccountPayload { +} + +message SameBankAccountPayload { +} + +message JapanBankAccountPayload { + string bank_name = 1; + string bank_code = 2; + string bank_branch_name = 3; + string bank_branch_code = 4; + string bank_account_type = 5; + string bank_account_name = 6; + string bank_account_number = 7; +} + +message AustraliaPayidPayload { + string bank_account_name = 1; + string payid = 2; +} + +message SpecificBanksAccountPayload { + repeated string accepted_banks = 1; +} + +message CashDepositAccountPayload { + string holder_name = 1; + string holder_email = 2; + string bank_name = 3; + string bank_id = 4; + string branch_id = 5; + string account_nr = 6; + string account_type = 7; + string requirements = 8; + string holder_tax_id = 9; + string national_account_id = 10; +} + +message MoneyGramAccountPayload { + string holder_name = 1; + string country_code = 2; + string state = 3; + string email = 4; +} + +message HalCashAccountPayload { + string mobile_nr = 1; +} + +message WesternUnionAccountPayload { + string holder_name = 1; + string city = 2; + string state = 3; + string email = 4; +} + +message AmazonGiftCardAccountPayload { + string email_or_mobile_nr = 1; + string country_code = 2; +} + +message SepaAccountPayload { + string holder_name = 1; + string iban = 2; + string bic = 3; + string email = 4 [deprecated = true]; + repeated string accepted_country_codes = 5; +} + +message SepaInstantAccountPayload { + string holder_name = 1; + string iban = 2; + string bic = 3; + repeated string accepted_country_codes = 4; +} + +message CryptoCurrencyAccountPayload { + string address = 1; +} + +message InstantCryptoCurrencyAccountPayload { + string address = 1; +} + +message BsqSwapAccountPayload { +} + +message FasterPaymentsAccountPayload { + string sort_code = 1; + string account_nr = 2; + string email = 3 [deprecated = true]; +} + +message InteracETransferAccountPayload { + string email = 1; + string holder_name = 2; + string question = 3; + string answer = 4; +} + +/* Deprecated, not used. */ +message OKPayAccountPayload { + string account_nr = 1; +} + +message UpholdAccountPayload { + string account_id = 1; + string account_owner = 2; +} + +/* Deprecated, not used. */ +message CashAppAccountPayload { + string cash_tag = 1; +} + +message MoneyBeamAccountPayload { + string account_id = 1; +} + +/* Deprecated, not used. */ +message VenmoAccountPayload { + string venmo_user_name = 1; + string holder_name = 2; +} + +message PopmoneyAccountPayload { + string account_id = 1; + string holder_name = 2; +} + +message RevolutAccountPayload { + string account_id = 1; + string user_name = 2; +} + +message PerfectMoneyAccountPayload { + string account_nr = 1; +} + +message SwishAccountPayload { + string mobile_nr = 1; + string holder_name = 2; +} + +message USPostalMoneyOrderAccountPayload { + string postal_address = 1; + string holder_name = 2; +} + +message F2FAccountPayload { + string contact = 1; + string city = 2; + string extra_info = 3; +} + +message IfscBasedAccountPayload { + string holder_name = 1; + string account_nr = 2; + string ifsc = 3; + oneof message { + NeftAccountPayload neft_account_payload = 4; + RtgsAccountPayload rtgs_account_payload = 5; + ImpsAccountPayload imps_account_payload = 6; + } +} + +message NeftAccountPayload { +} + +message RtgsAccountPayload { +} + +message ImpsAccountPayload { +} + +message UpiAccountPayload { + string virtual_payment_address = 1; +} + +message PaytmAccountPayload { + string email_or_mobile_nr = 1; +} + +message CashByMailAccountPayload { + string postal_address = 1; + string contact = 2; + string extra_info = 3; +} + +message PromptPayAccountPayload { + string prompt_pay_id = 1; +} + +message AdvancedCashAccountPayload { + string account_nr = 1; +} + +message TransferwiseAccountPayload { + string email = 1; +} + +message TransferwiseUsdAccountPayload { + string email = 1; + string holder_name = 2; + string beneficiary_address = 3; +} + +message PayseraAccountPayload { + string email = 1; +} + +message PaxumAccountPayload { + string email = 1; +} + +message CapitualAccountPayload { + string account_nr = 1; +} + +message CelPayAccountPayload { + string email = 1; +} + +message NequiAccountPayload { + string mobile_nr = 1; +} + +message BizumAccountPayload { + string mobile_nr = 1; +} + +message PixAccountPayload { + string pix_key = 1; +} + +message MoneseAccountPayload { + string mobile_nr = 1; + string holder_name = 2; +} + +message SatispayAccountPayload { + string mobile_nr = 1; + string holder_name = 2; +} + +message StrikeAccountPayload { + string holder_name = 1; +} + +message TikkieAccountPayload { + string iban = 1; +} + +message VerseAccountPayload { + string holder_name = 1; +} + +message SwiftAccountPayload { + string beneficiary_name = 1; + string beneficiary_account_nr = 2; + string beneficiary_address = 3; + string beneficiary_city = 4; + string beneficiary_phone = 5; + string special_instructions = 6; + + string bank_swift_code = 7; + string bank_country_code = 8; + string bank_name = 9; + string bank_branch = 10; + string bank_address = 11; + + string intermediary_swift_code = 12; + string intermediary_country_code = 13; + string intermediary_name = 14; + string intermediary_branch = 15; + string intermediary_address = 16; +} + +message PersistableEnvelope { + oneof message { + SequenceNumberMap sequence_number_map = 1; + PersistedEntryMap persisted_entry_map = 2 [deprecated = true]; + PeerList peer_list = 3; + AddressEntryList address_entry_list = 4; + NavigationPath navigation_path = 5; + + TradableList tradable_list = 6; + + // TradeStatisticsList trade_statistics_list = 7; Deprecated, Was used in pre v0.6.0 version. Not used anymore. + + ArbitrationDisputeList arbitration_dispute_list = 8; + + PreferencesPayload preferences_payload = 9; + UserPayload user_payload = 10; + PaymentAccountList payment_account_list = 11; + + // Deprecated. + // Not used but as other non-dao data have a higher index number we leave it to make clear that we + // cannot change following indexes. + // BsqState bsq_state = 12; + + AccountAgeWitnessStore account_age_witness_store = 13; + TradeStatistics2Store trade_statistics2_store = 14 [deprecated = true]; + + // PersistableNetworkPayloadList persistable_network_payload_list = 15; // long deprecated & migration away from it is already done. + + ProposalStore proposal_store = 16; + TempProposalStore temp_proposal_store = 17; + BlindVoteStore blind_vote_store = 18; + MyProposalList my_proposal_list = 19; + BallotList ballot_list = 20; + MyVoteList my_vote_list = 21; + MyBlindVoteList my_blind_vote_list = 22; + + // MeritList merit_list = 23; // was not used here, but its class used to implement PersistableEnvelope via its super. + + DaoStateStore dao_state_store = 24; + MyReputationList my_reputation_list = 25; + MyProofOfBurnList my_proof_of_burn_list = 26; + UnconfirmedBsqChangeOutputList unconfirmed_bsq_change_output_list = 27; + SignedWitnessStore signed_witness_store = 28; + MediationDisputeList mediation_dispute_list = 29; + RefundDisputeList refund_dispute_list = 30; + TradeStatistics3Store trade_statistics3_store = 31; + MailboxMessageList mailbox_message_list = 32; + IgnoredMailboxMap ignored_mailbox_map = 33; + RemovedPayloadsMap removed_payloads_map = 34; + BsqBlockStore bsq_block_store = 35; + } +} + +message SequenceNumberMap { + repeated SequenceNumberEntry sequence_number_entries = 1; +} + +message SequenceNumberEntry { + ByteArray bytes = 1; + MapValue map_value = 2; +} + +message ByteArray { + bytes bytes = 1; +} + +message MapValue { + int32 sequence_nr = 1; + int64 time_stamp = 2; +} + +/* Deprecated. Not used anymore. */ +message PersistedEntryMap { + map persisted_entry_map = 1; +} + +/* We use a list not a hash map to save disc space. The hash can be calculated from the payload anyway. */ +message AccountAgeWitnessStore { + repeated AccountAgeWitness items = 1; +} + +message SignedWitnessStore { + repeated SignedWitness items = 1; +} + +/* Deprecated. Not used anymore. +* We use a list not a hash map to save disc space. The hash can be calculated from the payload anyway. +*/ +message TradeStatistics2Store { + repeated TradeStatistics2 items = 1 [deprecated = true]; +} + +message TradeStatistics3Store { + repeated TradeStatistics3 items = 1; +} + +message PeerList { + repeated Peer peer = 1; +} + +message AddressEntryList { + repeated AddressEntry address_entry = 1; +} + +message AddressEntry { + enum Context { + PB_ERROR = 0; + ARBITRATOR = 1; + AVAILABLE = 2; + OFFER_FUNDING = 3; + RESERVED_FOR_TRADE = 4; + MULTI_SIG = 5; + TRADE_PAYOUT = 6; + } + + string offer_id = 7; + Context context = 8; + bytes pub_key = 9; + bytes pub_key_hash = 10; + int64 coin_locked_in_multi_sig = 11; + bool segwit = 12; +} + +message NavigationPath { + repeated string path = 1; +} + +message PaymentAccountList { + repeated PaymentAccount payment_account = 1; +} + +message TradableList { + repeated Tradable tradable = 1; +} + +message Offer { + enum State { + PB_ERROR = 0; + UNKNOWN = 1; + OFFER_FEE_PAID = 2; + AVAILABLE = 3; + NOT_AVAILABLE = 4; + REMOVED = 5; + MAKER_OFFLINE = 6; + } + + oneof message { + OfferPayload offer_payload = 1; + BsqSwapOfferPayload bsq_swap_offer_payload = 2; + } +} + +message OpenOffer { + enum State { + PB_ERROR = 0; + AVAILABLE = 1; + RESERVED = 2; + CLOSED = 3; + CANCELED = 4; + DEACTIVATED = 5; + } + + Offer offer = 1; + State state = 2; + NodeAddress arbitrator_node_address = 3; + NodeAddress mediator_node_address = 4; + NodeAddress refund_agent_node_address = 5; + int64 trigger_price = 6; +} + +message Tradable { + oneof message { + OpenOffer open_offer = 1; + BuyerAsMakerTrade buyer_as_maker_trade = 2; + BuyerAsTakerTrade buyer_as_taker_trade = 3; + SellerAsMakerTrade seller_as_maker_trade = 4; + SellerAsTakerTrade seller_as_taker_trade = 5; + BsqSwapBuyerAsMakerTrade bsq_swap_buyer_as_maker_trade = 6; + BsqSwapBuyerAsTakerTrade bsq_swap_buyer_as_taker_trade = 7; + BsqSwapSellerAsMakerTrade bsq_swap_seller_as_maker_trade = 8; + BsqSwapSellerAsTakerTrade bsq_swap_seller_as_taker_trade = 9; + } +} + +message Trade { + enum State { + PB_ERROR_STATE = 0; + PREPARATION = 1; + TAKER_PUBLISHED_TAKER_FEE_TX = 2; + MAKER_SENT_PUBLISH_DEPOSIT_TX_REQUEST = 3; + MAKER_SAW_ARRIVED_PUBLISH_DEPOSIT_TX_REQUEST = 4; + MAKER_STORED_IN_MAILBOX_PUBLISH_DEPOSIT_TX_REQUEST = 5; + MAKER_SEND_FAILED_PUBLISH_DEPOSIT_TX_REQUEST = 6; + TAKER_RECEIVED_PUBLISH_DEPOSIT_TX_REQUEST = 7; + SELLER_PUBLISHED_DEPOSIT_TX = 8; + SELLER_SENT_DEPOSIT_TX_PUBLISHED_MSG = 9 [deprecated = true]; + SELLER_SAW_ARRIVED_DEPOSIT_TX_PUBLISHED_MSG = 10 [deprecated = true]; + SELLER_STORED_IN_MAILBOX_DEPOSIT_TX_PUBLISHED_MSG = 11 [deprecated = true]; + SELLER_SEND_FAILED_DEPOSIT_TX_PUBLISHED_MSG = 12 [deprecated = true]; + BUYER_RECEIVED_DEPOSIT_TX_PUBLISHED_MSG = 13; + BUYER_SAW_DEPOSIT_TX_IN_NETWORK = 14; + DEPOSIT_CONFIRMED_IN_BLOCK_CHAIN = 15; + BUYER_CONFIRMED_IN_UI_FIAT_PAYMENT_INITIATED = 16; + BUYER_SENT_FIAT_PAYMENT_INITIATED_MSG = 17; + BUYER_SAW_ARRIVED_FIAT_PAYMENT_INITIATED_MSG = 18; + BUYER_STORED_IN_MAILBOX_FIAT_PAYMENT_INITIATED_MSG = 19; + BUYER_SEND_FAILED_FIAT_PAYMENT_INITIATED_MSG = 20; + SELLER_RECEIVED_FIAT_PAYMENT_INITIATED_MSG = 21; + SELLER_CONFIRMED_IN_UI_FIAT_PAYMENT_RECEIPT = 22; + SELLER_PUBLISHED_PAYOUT_TX = 23; + SELLER_SENT_PAYOUT_TX_PUBLISHED_MSG = 24; + SELLER_SAW_ARRIVED_PAYOUT_TX_PUBLISHED_MSG = 25; + SELLER_STORED_IN_MAILBOX_PAYOUT_TX_PUBLISHED_MSG = 26; + SELLER_SEND_FAILED_PAYOUT_TX_PUBLISHED_MSG = 27; + BUYER_RECEIVED_PAYOUT_TX_PUBLISHED_MSG = 28; + BUYER_SAW_PAYOUT_TX_IN_NETWORK = 29; + WITHDRAW_COMPLETED = 30; + } + + enum Phase { + PB_ERROR_PHASE = 0; + INIT = 1; + TAKER_FEE_PUBLISHED = 2; + DEPOSIT_PUBLISHED = 3; + DEPOSIT_CONFIRMED = 4; + FIAT_SENT = 5; + FIAT_RECEIVED = 6; + PAYOUT_PUBLISHED = 7; + WITHDRAWN = 8; + } + + enum DisputeState { + PB_ERROR_DISPUTE_STATE = 0; + NO_DISPUTE = 1; + // Arbitration requested. We use the enum name for resolving enums so it cannot be renamed. + DISPUTE_REQUESTED = 2; + // Arbitration requested. We use the enum name for resolving enums so it cannot be renamed. + DISPUTE_STARTED_BY_PEER = 3; + // Arbitration requested. We use the enum name for resolving enums so it cannot be renamed + DISPUTE_CLOSED = 4; + MEDIATION_REQUESTED = 5; + MEDIATION_STARTED_BY_PEER = 6; + MEDIATION_CLOSED = 7; + REFUND_REQUESTED = 8; + REFUND_REQUEST_STARTED_BY_PEER = 9; + REFUND_REQUEST_CLOSED = 10; + } + + enum TradePeriodState { + PB_ERROR_TRADE_PERIOD_STATE = 0; + FIRST_HALF = 1; + SECOND_HALF = 2; + TRADE_PERIOD_OVER = 3; + } + + Offer offer = 1; + ProcessModel process_model = 2; + string taker_fee_tx_id = 3; + string deposit_tx_id = 4; + string payout_tx_id = 5; + int64 trade_amount_as_long = 6; + int64 tx_fee_as_long = 7; + int64 taker_fee_as_long = 8; + int64 take_offer_date = 9; + bool is_currency_for_taker_fee_btc = 10; + int64 trade_price = 11; + NodeAddress trading_peer_node_address = 12; + State state = 13; + DisputeState dispute_state = 14; + TradePeriodState trade_period_state = 15; + Contract contract = 16; + string contract_as_json = 17; + bytes contract_hash = 18; + string taker_contract_signature = 19; + string maker_contract_signature = 20; + NodeAddress arbitrator_node_address = 21; + NodeAddress mediator_node_address = 22; + bytes arbitrator_btc_pub_key = 23; + string taker_payment_account_id = 24; + string error_message = 25; + PubKeyRing arbitrator_pub_key_ring = 26; + PubKeyRing mediator_pub_key_ring = 27; + string counter_currency_tx_id = 28; + repeated ChatMessage chat_message = 29; + MediationResultState mediation_result_state = 30; + int64 lock_time = 31; + bytes delayed_payout_tx_bytes = 32; + NodeAddress refund_agent_node_address = 33; + PubKeyRing refund_agent_pub_key_ring = 34; + RefundResultState refund_result_state = 35; + int64 last_refresh_request_date = 36 [deprecated = true]; + string counter_currency_extra_data = 37; + string asset_tx_proof_result = 38; // name of AssetTxProofResult enum + string uid = 39; +} + +message BuyerAsMakerTrade { + Trade trade = 1; +} + +message BuyerAsTakerTrade { + Trade trade = 1; +} + +message SellerAsMakerTrade { + Trade trade = 1; +} + +message SellerAsTakerTrade { + Trade trade = 1; +} + +message BsqSwapTrade { + enum State { + PB_ERROR_STATE = 0; + PREPARATION = 1; + COMPLETED = 2; + FAILED = 3; + } + + string uid = 1; + Offer offer = 2; + int64 amount = 3; + int64 take_offer_date = 4; + NodeAddress peer_node_address = 5; + int64 mining_fee_per_byte = 6; + int64 maker_fee = 7; + int64 taker_fee = 8; + BsqSwapProtocolModel bsq_swap_protocol_model = 9; + string tx_id = 10; + string error_message = 11; + State state = 12; +} + +message BsqSwapBuyerAsMakerTrade { + BsqSwapTrade bsq_swap_trade = 1; +} + +message BsqSwapBuyerAsTakerTrade { + BsqSwapTrade bsq_swap_trade = 1; +} + +message BsqSwapSellerAsMakerTrade { + BsqSwapTrade bsq_swap_trade = 1; +} + +message BsqSwapSellerAsTakerTrade { + BsqSwapTrade bsq_swap_trade = 1; +} + +message ProcessModel { + TradingPeer trading_peer = 1; + string offer_id = 2; + string account_id = 3; + PubKeyRing pub_key_ring = 4; + string take_offer_fee_tx_id = 5; + bytes payout_tx_signature = 6; + reserved 7; // Not used anymore. + reserved 8; // Not used anymore. + bytes prepared_deposit_tx = 9; + repeated RawTransactionInput raw_transaction_inputs = 10; + int64 change_output_value = 11; + string change_output_address = 12; + bool use_savings_wallet = 13; + int64 funds_needed_for_trade_as_long = 14; + bytes my_multi_sig_pub_key = 15; + NodeAddress temp_trading_peer_node_address = 16; + string payment_started_message_state = 17; + bytes mediated_payout_tx_signature = 18; + int64 buyer_payout_amount_from_mediation = 19; + int64 seller_payout_amount_from_mediation = 20; +} + +message TradingPeer { + string account_id = 1; + PaymentAccountPayload payment_account_payload = 2; + string payout_address_string = 3; + string contract_as_json = 4; + string contract_signature = 5; + bytes signature = 6; + PubKeyRing pub_key_ring = 7; + bytes multi_sig_pub_key = 8; + repeated RawTransactionInput raw_transaction_inputs = 9; + int64 change_output_value = 10; + string change_output_address = 11; + bytes account_age_witness_nonce = 12; + bytes account_age_witness_signature = 13; + int64 current_date = 14; + bytes mediated_payout_tx_signature = 15; + bytes hash_of_payment_account_payload = 16; +} + +message BsqSwapProtocolModel { + BsqSwapTradePeer trade_peer = 1; + PubKeyRing pub_key_ring = 2; + string btc_address = 3; + string bsq_address = 4; + repeated RawTransactionInput inputs = 5; + int64 change = 6; + int64 payout = 7; + bytes tx = 8; + int64 tx_fee = 9; +} + +message BsqSwapTradePeer { + PubKeyRing pub_key_ring = 1; + string btc_address = 2; + string bsq_address = 3; + repeated RawTransactionInput inputs = 4; + int64 change = 5; + int64 payout = 6; + bytes tx = 7; +} + +message ArbitrationDisputeList { + repeated Dispute dispute = 1; +} + +message MediationDisputeList { + repeated Dispute dispute = 1; +} + +message RefundDisputeList { + repeated Dispute dispute = 1; +} + +enum MediationResultState { + PB_ERROR_MEDIATION_RESULT = 0; + UNDEFINED_MEDIATION_RESULT = 1; + MEDIATION_RESULT_ACCEPTED = 2; + MEDIATION_RESULT_REJECTED = 3; + SIG_MSG_SENT = 4; + SIG_MSG_ARRIVED = 5; + SIG_MSG_IN_MAILBOX = 6; + SIG_MSG_SEND_FAILED = 7; + RECEIVED_SIG_MSG = 8; + PAYOUT_TX_PUBLISHED = 9; + PAYOUT_TX_PUBLISHED_MSG_SENT = 10; + PAYOUT_TX_PUBLISHED_MSG_ARRIVED = 11; + PAYOUT_TX_PUBLISHED_MSG_IN_MAILBOX = 12; + PAYOUT_TX_PUBLISHED_MSG_SEND_FAILED = 13; + RECEIVED_PAYOUT_TX_PUBLISHED_MSG = 14; + PAYOUT_TX_SEEN_IN_NETWORK = 15; +} + +enum RefundResultState { + PB_ERROR_REFUND_RESULT = 0; + UNDEFINED_REFUND_RESULT = 1; +} + +message PreferencesPayload { + string user_language = 1; + Country user_country = 2; + repeated TradeCurrency fiat_currencies = 3; + repeated TradeCurrency crypto_currencies = 4; + BlockChainExplorer block_chain_explorer_main_net = 5; + BlockChainExplorer block_chain_explorer_test_net = 6; + BlockChainExplorer bsq_block_chain_explorer = 7; + string backup_directory = 8; + bool auto_select_arbitrators = 9; + map dont_show_again_map = 10; + bool tac_accepted = 11; + bool use_tor_for_bitcoin_j = 12; + bool show_own_offers_in_offer_book = 13; + TradeCurrency preferred_trade_currency = 14; + int64 withdrawal_tx_fee_in_vbytes = 15; + bool use_custom_withdrawal_tx_fee = 16; + double max_price_distance_in_percent = 17; + string offer_book_chart_screen_currency_code = 18; + string trade_charts_screen_currency_code = 19; + string buy_screen_currency_code = 20; + string sell_screen_currency_code = 21; + int32 trade_statistics_tick_unit_index = 22; + bool resync_Spv_requested = 23; + bool sort_market_currencies_numerically = 24; + bool use_percentage_based_price = 25; + map peer_tag_map = 26; + string bitcoin_nodes = 27; + repeated string ignore_traders_list = 28; + string directory_chooser_path = 29; + // Superseded by buyerSecurityDepositAsPercent. + int64 buyer_security_deposit_as_long = 30 [deprecated = true]; + bool use_animations = 31; + PaymentAccount selectedPayment_account_for_createOffer = 32; + bool pay_fee_in_Btc = 33; + repeated string bridge_addresses = 34; + int32 bridge_option_ordinal = 35; + int32 tor_transport_ordinal = 36; + string custom_bridges = 37; + int32 bitcoin_nodes_option_ordinal = 38; + string referral_id = 39; + string phone_key_and_token = 40; + bool use_sound_for_mobile_notifications = 41; + bool use_trade_notifications = 42; + bool use_market_notifications = 43; + bool use_price_notifications = 44; + bool use_standby_mode = 45; + bool is_dao_full_node = 46; + string rpc_user = 47; + string rpc_pw = 48; + string take_offer_selected_payment_account_id = 49; + double buyer_security_deposit_as_percent = 50; + int32 ignore_dust_threshold = 51; + double buyer_security_deposit_as_percent_for_crypto = 52; + int32 block_notify_port = 53; + int32 css_theme = 54; + bool tac_accepted_v120 = 55; + repeated AutoConfirmSettings auto_confirm_settings = 56; + double bsq_average_trim_threshold = 57; + bool hide_non_account_payment_methods = 58; + bool show_offers_matching_my_accounts = 59; + bool deny_api_taker = 60; + bool notify_on_pre_release = 61; + bool use_full_mode_dao_monitor = 62; + int32 clear_data_after_days = 63; +} + +message AutoConfirmSettings { + bool enabled = 1; + int32 required_confirmations = 2; + int64 trade_limit = 3; + repeated string service_addresses = 4; + string currency_code = 5; +} + +message UserPayload { + string account_id = 1; + repeated PaymentAccount payment_accounts = 2; + PaymentAccount current_payment_account = 3; + repeated string accepted_language_locale_codes = 4; + Alert developers_alert = 5; + Alert displayed_alert = 6; + Filter developers_filter = 7; + repeated Arbitrator accepted_arbitrators = 8; + repeated Mediator accepted_mediators = 9; + Arbitrator registered_arbitrator = 10; + Mediator registered_mediator = 11; + PriceAlertFilter price_alert_filter = 12; + repeated MarketAlertFilter market_alert_filters = 13; + repeated RefundAgent accepted_refund_agents = 14; + RefundAgent registered_refund_agent = 15; + map cookie = 16; +} + +message BaseBlock { + int32 height = 1; + int64 time = 2; + string hash = 3; + string previous_block_hash = 4; + oneof message { + RawBlock raw_block = 5; + Block block = 6; + } +} + +message BsqBlockStore { + repeated BaseBlock blocks = 1; +} + +message RawBlock { + // Because of the way how PB implements inheritance we need to use the super class as type. + repeated BaseTx raw_txs = 1; +} + +message Block { + // Because of the way how PB implements inheritance we need to use the super class as type. + repeated BaseTx txs = 1; +} + +message BaseTx { + string tx_version = 1; + string id = 2; + int32 block_height = 3; + string block_hash = 4; + int64 time = 5; + repeated TxInput tx_inputs = 6; + oneof message { + RawTx raw_tx = 7; + Tx tx = 8; + } +} + +message RawTx { + // Because of the way how PB implements inheritance we need to use the super class as type. + repeated BaseTxOutput raw_tx_outputs = 1; +} + +message Tx { + // Because of the way how PB implements inheritance we need to use the super class as type. + repeated BaseTxOutput tx_outputs = 1; + TxType txType = 2; + int64 burnt_bsq = 3; +} + +enum TxType { + PB_ERROR_TX_TYPE = 0; + UNDEFINED_TX_TYPE = 1; + UNVERIFIED = 2; + INVALID = 3; + GENESIS = 4; + TRANSFER_BSQ = 5; + PAY_TRADE_FEE = 6; + PROPOSAL = 7; + COMPENSATION_REQUEST = 8; + REIMBURSEMENT_REQUEST = 9; + BLIND_VOTE = 10; + VOTE_REVEAL = 11; + LOCKUP = 12; + UNLOCK = 13; + ASSET_LISTING_FEE = 14; + PROOF_OF_BURN = 15; + IRREGULAR = 16; +} + +message TxInput { + string connected_tx_output_tx_id = 1; + int32 connected_tx_output_index = 2; + string pub_key = 3; +} + +message BaseTxOutput { + int32 index = 1; + int64 value = 2; + string tx_id = 3; + PubKeyScript pub_key_script = 4; + string address = 5; + bytes op_return_data = 6; + int32 block_height = 7; + oneof message { + RawTxOutput raw_tx_output = 8; + TxOutput tx_output = 9; + } +} + +message UnconfirmedTxOutput { + int32 index = 1; + int64 value = 2; + string tx_id = 3; +} + +message RawTxOutput { +} + +message TxOutput { + TxOutputType tx_output_type = 1; + int32 lock_time = 2; + int32 unlock_block_height = 3; +} + +enum TxOutputType { + PB_ERROR_TX_OUTPUT_TYPE = 0; + UNDEFINED_OUTPUT = 1; + GENESIS_OUTPUT = 2; + BSQ_OUTPUT = 3; + BTC_OUTPUT = 4; + PROPOSAL_OP_RETURN_OUTPUT = 5; + COMP_REQ_OP_RETURN_OUTPUT = 6; + REIMBURSEMENT_OP_RETURN_OUTPUT = 7; + CONFISCATE_BOND_OP_RETURN_OUTPUT = 8; + ISSUANCE_CANDIDATE_OUTPUT = 9; + BLIND_VOTE_LOCK_STAKE_OUTPUT = 10; + BLIND_VOTE_OP_RETURN_OUTPUT = 11; + VOTE_REVEAL_UNLOCK_STAKE_OUTPUT = 12; + VOTE_REVEAL_OP_RETURN_OUTPUT = 13; + ASSET_LISTING_FEE_OP_RETURN_OUTPUT = 14; + PROOF_OF_BURN_OP_RETURN_OUTPUT = 15; + LOCKUP_OUTPUT = 16; + LOCKUP_OP_RETURN_OUTPUT = 17; + UNLOCK_OUTPUT = 18; + INVALID_OUTPUT = 19; +} + +message SpentInfo { + int64 block_height = 1; + string tx_id = 2; + int32 input_index = 3; +} + +enum ScriptType { + PB_ERROR_SCRIPT_TYPES = 0; + PUB_KEY = 1; + PUB_KEY_HASH = 2; + SCRIPT_HASH = 3; + MULTISIG = 4; + NULL_DATA = 5; + WITNESS_V0_KEYHASH = 6; + WITNESS_V0_SCRIPTHASH = 7; + NONSTANDARD = 8; + WITNESS_UNKNOWN = 9; + WITNESS_V1_TAPROOT = 10; +} + +message PubKeyScript { + int32 req_sigs = 1; + ScriptType script_type = 2; + repeated string addresses = 3; + string asm = 4; + string hex = 5; +} + +message DaoPhase { + int32 phase_ordinal = 1; + int32 duration = 2; +} + +message Cycle { + int32 height_of_first_lock = 1; + repeated DaoPhase dao_phase = 2; +} + +message DaoState { + int32 chain_height = 1; + // Because of the way how PB implements inheritance we need to use the super class as type. + repeated BaseBlock blocks = 2; + repeated Cycle cycles = 3; + // Because of the way how PB implements inheritance we need to use the super class as type. + map unspent_tx_output_map = 4; + map issuance_map = 5; + repeated string confiscated_lockup_tx_list = 6; + map spent_info_map = 7; + repeated ParamChange param_change_list = 8; + repeated EvaluatedProposal evaluated_proposal_list = 9; + repeated DecryptedBallotsWithMerits decrypted_ballots_with_merits_list = 10; +} + +message Issuance { + string tx_id = 1; + int32 chain_height = 2; + int64 amount = 3; + string pub_key = 4; + string issuance_type = 5; +} + +message Proposal { + string name = 1; + string link = 2; + uint32 version = 3; + int64 creation_date = 4; + string tx_id = 5; + oneof message { + CompensationProposal compensation_proposal = 6; + ReimbursementProposal reimbursement_proposal = 7; + ChangeParamProposal change_param_proposal = 8; + RoleProposal role_proposal = 9; + ConfiscateBondProposal confiscate_bond_proposal = 10; + GenericProposal generic_proposal = 11; + RemoveAssetProposal remove_asset_proposal = 12; + } + // We leave some index space here in case we add more subclasses. + map extra_data = 20; +} + +message CompensationProposal { + int64 requested_bsq = 1; + string bsq_address = 2; +} + +message ReimbursementProposal { + int64 requested_bsq = 1; + string bsq_address = 2; +} + +message ChangeParamProposal { + string param = 1; // Name of enum. + string param_value = 2; +} + +message RoleProposal { + Role role = 1; + int64 required_bond_unit = 2; + int32 unlock_time = 3; +} + +message ConfiscateBondProposal { + string lockup_tx_id = 1; +} + +message GenericProposal { +} + +message RemoveAssetProposal { + string ticker_symbol = 1; +} + +message Role { + string uid = 1; + string name = 2; + string link = 3; + string bonded_role_type = 4; // Name of BondedRoleType enum. +} + +message MyReputation { + string uid = 1; + bytes salt = 2; +} + +message MyReputationList { + repeated MyReputation my_reputation = 1; +} + +message MyProofOfBurn { + string tx_id = 1; + string pre_image = 2; +} + +message MyProofOfBurnList { + repeated MyProofOfBurn my_proof_of_burn = 1; +} + +message UnconfirmedBsqChangeOutputList { + repeated UnconfirmedTxOutput unconfirmed_tx_output = 1; +} + +message TempProposalPayload { + Proposal proposal = 1; + bytes owner_pub_key_encoded = 2; + map extra_data = 3; +} + +message ProposalPayload { + Proposal proposal = 1; + bytes hash = 2; +} + +message ProposalStore { + repeated ProposalPayload items = 1; +} + +message TempProposalStore { + repeated ProtectedStorageEntry items = 1; +} + +message Ballot { + Proposal proposal = 1; + Vote vote = 2; +} + +message MyProposalList { + repeated Proposal proposal = 1; +} + +message BallotList { + repeated Ballot ballot = 1; +} + +message ParamChange { + string param_name = 1; + string param_value = 2; + int32 activation_height = 3; +} + +message ConfiscateBond { + string lockup_tx_id = 1; +} + +message MyVote { + int32 height = 1; + BallotList ballot_list = 2; + bytes secret_key_encoded = 3; + BlindVote blind_vote = 4; + int64 date = 5; + string reveal_tx_id = 6; +} + +message MyVoteList { + repeated MyVote my_vote = 1; +} + +message VoteWithProposalTxId { + string proposal_tx_id = 1; + Vote vote = 2; +} + +message VoteWithProposalTxIdList { + repeated VoteWithProposalTxId item = 1; +} + +message BlindVote { + bytes encrypted_votes = 1; + string tx_id = 2; + int64 stake = 3; + bytes encrypted_merit_list = 4; + int64 date = 5; + map extra_data = 6; +} + +message MyBlindVoteList { + repeated BlindVote blind_vote = 1; +} + +message BlindVoteStore { + repeated BlindVotePayload items = 1; +} + +message BlindVotePayload { + BlindVote blind_vote = 1; + bytes hash = 2; +} + +message Vote { + bool accepted = 1; +} + +message Merit { + Issuance issuance = 1; + bytes signature = 2; +} + +message MeritList { + repeated Merit merit = 1; +} + +message ProposalVoteResult { + Proposal proposal = 1; + int64 stake_of_Accepted_votes = 2; + int64 stake_of_Rejected_votes = 3; + int32 num_accepted_votes = 4; + int32 num_rejected_votes = 5; + int32 num_ignored_votes = 6; +} + +message EvaluatedProposal { + bool is_accepted = 1; + ProposalVoteResult proposal_vote_result = 2; +} + +message DecryptedBallotsWithMerits { + bytes hash_of_blind_vote_list = 1; + string blind_vote_tx_id = 2; + string vote_reveal_tx_id = 3; + int64 stake = 4; + BallotList ballot_list = 5; + MeritList merit_list = 6; +} + +message DaoStateStore { + DaoState dao_state = 1; + repeated DaoStateHash dao_state_hash = 2; +} + +message DaoStateHash { + int32 height = 1; + bytes hash = 2; + bytes prev_hash = 3 [deprecated = true]; + bool is_self_created = 4; +} + +message ProposalStateHash { + int32 height = 1; + bytes hash = 2; + bytes prev_hash = 3 [deprecated = true]; + int32 num_proposals = 4; +} + +message BlindVoteStateHash { + int32 height = 1; + bytes hash = 2; + bytes prev_hash = 3 [deprecated = true]; + int32 num_blind_votes = 4; +} + +message BlockChainExplorer { + string name = 1; + string tx_url = 2; + string address_url = 3; +} + +message PaymentAccount { + string id = 1; + int64 creation_date = 2; + PaymentMethod payment_method = 3; + string account_name = 4; + repeated TradeCurrency trade_currencies = 5; + TradeCurrency selected_trade_currency = 6; + PaymentAccountPayload payment_account_payload = 7; +} + +message PaymentMethod { + string id = 1; + int64 max_trade_period = 2; + int64 max_trade_limit = 3; +} + +message Currency { + string currency_code = 1; +} + +message TradeCurrency { + string code = 1; + string name = 2; + oneof message { + CryptoCurrency crypto_currency = 3; + FiatCurrency fiat_currency = 4; + } +} + +message CryptoCurrency { + bool is_asset = 1; +} + +message FiatCurrency { + Currency currency = 1; +} + +message Country { + string code = 1; + string name = 2; + Region region = 3; +} + +message Region { + string code = 1; + string name = 2; +} + +message PriceAlertFilter { + string currencyCode = 1; + int64 high = 2; + int64 low = 3; +} + +message MarketAlertFilter { + PaymentAccount payment_account = 1; + int32 trigger_value = 2; + bool is_buy_offer = 3; + repeated string alert_ids = 4; +} + +message MockMailboxPayload { + string message = 1; + NodeAddress sender_node_address = 2; + string uid = 3; +} + +message MockPayload { + string message_version = 1; + string message = 2; +} diff --git a/proto-downloader/proto-downloader.iml b/proto-downloader/proto-downloader.iml new file mode 100644 index 0000000..9a5cfce --- /dev/null +++ b/proto-downloader/proto-downloader.iml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/python-examples/bots/__init__.py b/python-examples/bots/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/python-examples/bots/assets/bisq_logo.png b/python-examples/bots/assets/bisq_logo.png new file mode 100644 index 0000000..d5c3d61 Binary files /dev/null and b/python-examples/bots/assets/bisq_logo.png differ diff --git a/python-examples/bots/assets/btc_amount_entry.png b/python-examples/bots/assets/btc_amount_entry.png new file mode 100644 index 0000000..df7de44 Binary files /dev/null and b/python-examples/bots/assets/btc_amount_entry.png differ diff --git a/python-examples/bots/assets/payment_received_button.png b/python-examples/bots/assets/payment_received_button.png new file mode 100644 index 0000000..f20abb7 Binary files /dev/null and b/python-examples/bots/assets/payment_received_button.png differ diff --git a/python-examples/bots/assets/payment_sent_button.png b/python-examples/bots/assets/payment_sent_button.png new file mode 100644 index 0000000..d46c2bd Binary files /dev/null and b/python-examples/bots/assets/payment_sent_button.png differ diff --git a/python-examples/bots/assets/reference_price_margin_entry.png b/python-examples/bots/assets/reference_price_margin_entry.png new file mode 100644 index 0000000..71b7b01 Binary files /dev/null and b/python-examples/bots/assets/reference_price_margin_entry.png differ diff --git a/python-examples/bots/assets/spread_entry.png b/python-examples/bots/assets/spread_entry.png new file mode 100644 index 0000000..5333f72 Binary files /dev/null and b/python-examples/bots/assets/spread_entry.png differ diff --git a/python-examples/bots/assets/start_button.png b/python-examples/bots/assets/start_button.png new file mode 100644 index 0000000..b5f5603 Binary files /dev/null and b/python-examples/bots/assets/start_button.png differ diff --git a/python-examples/bots/assets/stop_button.png b/python-examples/bots/assets/stop_button.png new file mode 100644 index 0000000..a61b146 Binary files /dev/null and b/python-examples/bots/assets/stop_button.png differ diff --git a/python-examples/bots/assets/window_icon.png b/python-examples/bots/assets/window_icon.png new file mode 100644 index 0000000..8f40138 Binary files /dev/null and b/python-examples/bots/assets/window_icon.png differ diff --git a/python-examples/bots/best_priced_offer_bot.ini b/python-examples/bots/best_priced_offer_bot.ini new file mode 100644 index 0000000..8ff242f --- /dev/null +++ b/python-examples/bots/best_priced_offer_bot.ini @@ -0,0 +1,18 @@ +[general] +check_price_interval_in_sec = 60 + +[offer type] +currency = XMR +direction = SELL + +[offers] +# List of comma separated offer ids to be synced. +offer_ids = piape-ef84138d-8204-4b6e-995c-49d3dd1ef37a-184 + +[safeguards] +# This should prevent the synchronization with offers that are out of bounds. +min_accepted_offer_amount_for_price_adaption = 1 +# Your minimum accepted price when trying to buy BTC (direction="BUY"). +min_accepted_price = 0.00300000 +# Your maximum accepted price when trying to sell BTC (direction="SELL"). +max_accepted_price = 0.00500000 diff --git a/python-examples/bots/best_priced_offer_bot.py b/python-examples/bots/best_priced_offer_bot.py new file mode 100644 index 0000000..755de3f --- /dev/null +++ b/python-examples/bots/best_priced_offer_bot.py @@ -0,0 +1,213 @@ +import configparser +import sys +import threading +import time +from decimal import Decimal + +from bisq_client import BisqClient +from logger import log + + +# To run file in Python console: main('localhost', 9998, 'xyz') +# To run from another Python script: +# import best_priced_offer_bot +# best_priced_offer_bot.main('localhost', 9998, 'xyz') + + +# noinspection PyInitNewSignature +class BestPricedOfferBot(BisqClient): + def __init__(self, host, port, api_password): + super().__init__(host, port, api_password) + self.config = configparser.ConfigParser() + self.config.read('best_priced_offer_bot.ini') + self.my_synced_offer_ids = [offer_id.strip() for offer_id in self.config.get('offers', 'offer_ids').split(',')] + if not self.my_synced_offer_ids[0]: + sys.exit('best_priced_offer_bot.ini\'s offer_ids value is not specified') + + def run(self): + # Before checking available offers for their best price, update my offers with my own most competitive price. + self.update_my_offers_with_my_most_competitive_price() + + # Poll available offers fpr their best price, and adjust my own offer prices to compete. + timer = threading.Timer(0, ()) + max_iterations = 100 + count = 0 + interval = 0 + try: + while not timer.finished.wait(interval): + self.check_best_available_price_and_update() + count = count + 1 + if count >= max_iterations: + timer.cancel() + else: + interval = self.check_price_interval_in_sec() + sys.exit(0) + except KeyboardInterrupt: + log.warning('Timer interrupted') + sys.exit(0) + + def update_my_offers_with_my_most_competitive_price(self): + log.info('Updating or recreating my synced %s %s offers with my most competitive price.', + self.offer_type_direction(), self.offer_type_currency()) + # Sort all my open offers by price to find the most competitive price. + my_offers = self.get_my_open_offers(self.offer_type_direction(), self.offer_type_currency()) + my_offers.sort(key=lambda x: x.price, reverse=self.offer_type_direction() == 'SELL') + if len(my_offers): + my_most_competitive_price = Decimal(my_offers[0].price) + else: + sys.exit('I have no {} {} offers to sync.' + .format(self.offer_type_direction(), self.offer_type_currency())) + + # Update (or recreate) any synced offers that do not have my most competitive price. + offer_filter = filter(lambda candidate_offer: (candidate_offer.id in self.my_synced_offer_ids), my_offers) + filtered_offers = list(offer_filter) + for offer in filtered_offers: + if Decimal(offer.price) != my_most_competitive_price: + self.update_my_offer(offer, my_most_competitive_price) + else: + log.info('My synced %s %s offer %s already has my most competitive price (%s)', + self.offer_type_direction(), self.offer_type_currency(), offer.id, offer.price) + + def update_my_offer(self, my_offer, competitive_price): + if my_offer.is_bsq_swap_offer: + new_offer = self.recreate_bsqswap_offer(my_offer, competitive_price) + log.info('Replaced old %s %s offer %s with new offer %s with fixed-price %s.', + self.offer_type_direction(), + self.offer_type_currency(), + my_offer.id, + new_offer.id, + new_offer.price) + else: + self.update_offer_fixed_price(my_offer, competitive_price) + + try: + time.sleep(5) # Wait for offer to be re-published with new fixed-price. + except KeyboardInterrupt: + log.warning('Interrupted while updating offer.') + sys.exit(0) + + updated_offer = self.get_my_offer(my_offer.id) + log.info('Updated %s %s offer %s with new fixed-price %s.', + self.offer_type_direction(), + self.offer_type_currency(), + updated_offer.id, + updated_offer.price) + + def recreate_bsqswap_offer(self, old_offer, competitive_price): + # Remove my_offer from offer book. + self.cancel_offer(old_offer.id) + # Get canceled offer's details for new offer. + old_offer_direction = self.offer_type_direction() + old_offer_amount = old_offer.amount + old_offer_min_amount = old_offer.min_amount + competitive_price_str = str(competitive_price) + new_offer = self.create_bsqswap_offer(old_offer_direction, + competitive_price_str, + old_offer_amount, + old_offer_min_amount) + self.replace_synced_offer_id(old_offer.id, new_offer.id) + return new_offer + + def update_offer_fixed_price(self, my_offer, competitive_price): + competitive_price_str = str(competitive_price) + self.edit_offer_fixed_price(my_offer, competitive_price_str) + return + + def replace_synced_offer_id(self, canceled_offer_id, new_offer_id): + # Remove my_offer.id from list of my_synced_offer_ids. + self.my_synced_offer_ids.remove(canceled_offer_id) + # Add new_offer.id to list of my_synced_offer_ids. + self.my_synced_offer_ids.append(new_offer_id) + + def get_best_available_price_for_acceptable_amount(self): + available_offers = self.get_available_offers(self.offer_type_direction(), self.offer_type_currency()) + if len(available_offers) == 0: + log.info('No available offers found.') + return None + + # Filter all available offers that are below 'min_accepted_offer_amount_for_price_adaption'. + min_amount = self.safeguard_min_accepted_amount() + offer_filter = filter(lambda offer: (Decimal(offer.volume) >= Decimal(min_amount)), available_offers) + filtered_offers = list(offer_filter) + # Sort the filtered, available offers by price. + filtered_offers.sort(key=lambda x: x.price, reverse=self.offer_type_direction() == 'SELL') + + # Find the best (most competitive) available offer price. + if len(filtered_offers): + current_best_price = Decimal(filtered_offers[0].price) + if self.offer_type_direction() == 'BUY': + if self.is_price_below_accepted_limit(current_best_price): + log.info('Best available price %s is too low, below safeguard_max_accepted_price %s.', + current_best_price, self.safeguard_min_accepted_price()) + return self.safeguard_min_accepted_price() + else: + log.info('Best available price is max of current_best_price, min_accepted_price (%s, %s)', + current_best_price, self.safeguard_min_accepted_price()) + return max(current_best_price, self.safeguard_min_accepted_price()) + else: + if self.is_price_above_accepted_limit(current_best_price): + log.info('Best available price %s is too high, above safeguard_max_accepted_price %s.', + current_best_price, self.safeguard_max_accepted_price()) + return self.safeguard_max_accepted_price() + else: + log.info('Best available price is min of current_best_price, max_accepted_price (%s, %s)', + current_best_price, self.safeguard_max_accepted_price()) + return min(current_best_price, self.safeguard_max_accepted_price()) + + def is_price_below_accepted_limit(self, price): + return price < self.safeguard_min_accepted_price() + + def is_price_above_accepted_limit(self, price): + return price > self.safeguard_max_accepted_price() + + def check_best_available_price_and_update(self): + log.info('Polling available offers for the best price...') + best_available_price = self.get_best_available_price_for_acceptable_amount() + if best_available_price is None: + log.warning('Could not find best available price.') + return + + for offer_id in self.my_synced_offer_ids: + offer = super().get_my_offer(offer_id) + if offer is None: + err_msg = 'You do not have an offer with id {}.\n'.format(offer_id) + err_msg += 'The offer may have been taken or canceled.\n' + err_msg += 'Update your best_priced_offer_bot.ini file and restart the bot.' + sys.exit(err_msg) + + if Decimal(offer.price) != best_available_price: + log.info('Update %s %s offer %s price (%s) with a competitive price (%s).', + self.offer_type_direction(), + self.offer_type_currency(), + offer.id, + offer.price, + best_available_price) + self.update_my_offer(offer, best_available_price) + else: + log.info('My %s offer %s (with price %s) already has the most competitive price (%s).', + offer.direction, offer.id, offer.price, best_available_price) + + def check_price_interval_in_sec(self): + return int(self.config.get('general', 'check_price_interval_in_sec')) + + def offer_type_currency(self): + return self.config.get('offer type', 'currency') + + def offer_type_direction(self): + return self.config.get('offer type', 'direction') + + def safeguard_min_accepted_amount(self): + return int(self.config.get('safeguards', 'min_accepted_offer_amount_for_price_adaption')) + + def safeguard_min_accepted_price(self): + return Decimal(self.config.get('safeguards', 'min_accepted_price')) + + def safeguard_max_accepted_price(self): + return Decimal(self.config.get('safeguards', 'max_accepted_price')) + + def __str__(self): + return 'BestPricedOfferBot: ' + 'host=' + self.host + ', port=' + str(self.port) + ', api_password=' + '*****' + + +def main(host, port, api_password): + BestPricedOfferBot(host, port, api_password).run() diff --git a/python-examples/bots/bisq_client.py b/python-examples/bots/bisq_client.py new file mode 100644 index 0000000..91c1c64 --- /dev/null +++ b/python-examples/bots/bisq_client.py @@ -0,0 +1,400 @@ +import operator +import time +from datetime import datetime, timezone +from decimal import * + +import grpc + +import bisq.api.grpc_pb2 as bisq_messages +import bisq.api.grpc_pb2_grpc as bisq_service +from logger import log + +# For more channel options, please see https://grpc.io/grpc/core/group__grpc__arg__keys.html +# And see https://www.cs.mcgill.ca/~mxia3/2019/02/23/Using-gRPC-in-Production +CHANNEL_OPTIONS = [('grpc.lb_policy_name', 'pick_first'), + ('grpc.enable_retries', 0), + ('grpc.keepalive_timeout_ms', 10000)] + + +class BisqClient(object): + def __init__(self, host, port, api_password): + self.host = host + self.port = port + self.api_password = api_password + self.grpc_channel = None + self.offers_stub = None + self.payment_accts_stub = None + self.trades_stub = None + self.version_stub = None + self.wallets_stub = None + self.open_channel() + + def open_channel(self): + self.grpc_channel = grpc.insecure_channel(self.host + ':' + str(self.port), options=CHANNEL_OPTIONS) + self.offers_stub = bisq_service.OffersStub(self.grpc_channel) + self.payment_accts_stub = bisq_service.PaymentAccountsStub(self.grpc_channel) + self.trades_stub = bisq_service.TradesStub(self.grpc_channel) + self.version_stub = bisq_service.GetVersionStub(self.grpc_channel) + self.wallets_stub = bisq_service.WalletsStub(self.grpc_channel) + + def close_channel(self): + log.info('Closing gRPC channel') + self.grpc_channel.close() + time.sleep(0.5) + self.grpc_channel = None + + def is_connected(self): + return self.grpc_channel is not None + + def create_margin_priced_offer(self, currency_code, + direction, + market_price_margin_pct, + amount, + min_amount, + payment_account_id): + try: + response = self.offers_stub.CreateOffer.with_call( + bisq_messages.CreateOfferRequest( + currency_code=currency_code, + direction=direction, + use_market_based_price=True, + market_price_margin_pct=market_price_margin_pct, + amount=amount, + min_amount=min_amount, + buyer_security_deposit_pct=15.00, + payment_account_id=payment_account_id), + metadata=[('password', self.api_password)]) + return response[0].offer + except grpc.RpcError as rpc_error: + print('gRPC API Exception: %s', rpc_error) + + def create_bsqswap_offer(self, direction, price_str, amount, min_amount): + try: + response = self.offers_stub.CreateBsqSwapOffer.with_call( + bisq_messages.CreateBsqSwapOfferRequest( + direction=direction, + price=price_str, + amount=amount, + min_amount=min_amount), + metadata=[('password', self.api_password)]) + return response[0].bsq_swap_offer + except grpc.RpcError as rpc_error: + print('gRPC API Exception: %s', rpc_error) + + def edit_offer_fixed_price(self, offer, fixed_price): + try: + self.offers_stub.EditOffer.with_call( + bisq_messages.EditOfferRequest( + id=offer.id, + price=fixed_price, + edit_type=bisq_messages.EditOfferRequest.EditType.FIXED_PRICE_ONLY, + enable=-1), # enable=-1 means offer activation state remains unchanged + metadata=[('password', self.api_password)]) + except grpc.RpcError as rpc_error: + print('gRPC API Exception: %s', rpc_error) + + def cancel_offer(self, offer_id): + try: + self.offers_stub.CancelOffer.with_call( + bisq_messages.CancelOfferRequest(id=offer_id), + metadata=[('password', self.api_password)]) + except grpc.RpcError as rpc_error: + print('gRPC API Exception: %s', rpc_error) + + def get_my_offer(self, offer_id): + try: + response = self.offers_stub.GetMyOffer.with_call( + bisq_messages.GetMyOfferRequest(id=offer_id), + metadata=[('password', self.api_password)]) + return response[0].offer + except grpc.RpcError as rpc_error: + print('gRPC API Exception: %s', rpc_error) + + def get_available_offers(self, direction, currency_code) -> list: + if currency_code == 'BSQ': + return self.get_available_bsqswap_offers(direction) + else: + return self.get_available_v1_offers(direction, currency_code) + + def get_available_v1_offers(self, direction, currency_code) -> list: + try: + response = self.offers_stub.GetOffers.with_call( + bisq_messages.GetOffersRequest(direction=direction, + currency_code=currency_code), + metadata=[('password', self.api_password)]) + return list(response[0].offers) + except grpc.RpcError as rpc_error: + print('gRPC API Exception: %s', rpc_error) + + def get_available_bsqswap_offers(self, direction) -> list: + try: + response = self.offers_stub.GetBsqSwapOffers.with_call( + bisq_messages.GetBsqSwapOffersRequest(direction=direction), + metadata=[('password', self.api_password)]) + return list(response[0].bsq_swap_offers) + except grpc.RpcError as rpc_error: + print('gRPC API Exception: %s', rpc_error) + + def get_my_open_offers(self, direction, currency_code) -> list: + if currency_code == 'BSQ': + return self.get_my_bsqswap_offers(direction) + else: + return self.get_my_offers(direction, currency_code) + + def get_my_bsqswap_offers(self, direction) -> list: + try: + response = self.offers_stub.GetMyBsqSwapOffers.with_call( + bisq_messages.GetBsqSwapOffersRequest(direction=direction), + metadata=[('password', self.api_password)]) + return list(response[0].bsq_swap_offers) + except grpc.RpcError as rpc_error: + print('gRPC API Exception: %s', rpc_error) + + def get_my_offers(self, direction, currency_code) -> list: + try: + response = self.offers_stub.GetMyOffers.with_call( + bisq_messages.GetMyOffersRequest(direction=direction, + currency_code=currency_code), + metadata=[('password', self.api_password)]) + return list(response[0].offers) + except grpc.RpcError as rpc_error: + print('gRPC API Exception: %s', rpc_error) + + def get_trades(self, category) -> list: + if category.casefold() == str(bisq_messages.GetTradesRequest.Category.CLOSED).casefold(): + return self.get_closed_trades() + elif category.casefold() == str(bisq_messages.GetTradesRequest.Category.FAILED).casefold(): + return self.get_failed_trades() + elif category.casefold() == str(bisq_messages.GetTradesRequest.Category.OPEN).casefold(): + return self.get_open_trades() + else: + raise Exception('Invalid trade category {0}, must be one of CLOSED | FAILED | OPEN'.format(category)) + + def get_closed_trades(self) -> list: + try: + response = self.trades_stub.GetTrades.with_call( + bisq_messages.GetTradesRequest( + category=bisq_messages.GetTradesRequest.Category.CLOSED), + metadata=[('password', self.api_password)]) + return list(response[0].trades) + except grpc.RpcError as rpc_error: + print('gRPC API Exception: %s', rpc_error) + + def get_failed_trades(self) -> list: + try: + response = self.trades_stub.GetTrades.with_call( + bisq_messages.GetTradesRequest( + category=bisq_messages.GetTradesRequest.Category.FAILED), + metadata=[('password', self.api_password)]) + return list(response[0].trades) + except grpc.RpcError as rpc_error: + print('gRPC API Exception: %s', rpc_error) + + def get_open_trades(self) -> list: + try: + response = self.trades_stub.GetTrades.with_call( + bisq_messages.GetTradesRequest( + category=bisq_messages.GetTradesRequest.Category.OPEN), + metadata=[('password', self.api_password)]) + return list(response[0].trades) + except grpc.RpcError as rpc_error: + print('gRPC API Exception: %s', rpc_error) + + def get_open_trade(self, trade_id): + open_trades = self.get_open_trades() + if len(open_trades): + trade_filter = filter(lambda candidate_trade: (candidate_trade.trade_id == trade_id), open_trades) + filtered_trades = list(trade_filter) + if len(filtered_trades): + return filtered_trades[0] + else: + return None + else: + return None + + def get_open_fiat_trades(self, currency_code, direction) -> list: + open_trades = self.get_open_trades() + if len(open_trades): + trade_filter = filter(lambda candidate_trade: + (candidate_trade.offer.counter_currency_code == currency_code + and candidate_trade.offer.direction == direction), + open_trades) + filtered_trades = list(trade_filter) + if len(filtered_trades): + # This sort is done on server !? + filtered_trades.sort(key=operator.attrgetter('date')) + return filtered_trades + else: + return [] + else: + return [] + + def get_oldest_open_fiat_trade(self, currency_code, direction): + open_trades = self.get_open_trades() + if len(open_trades): + trade_filter = filter(lambda candidate_trade: + (candidate_trade.offer.counter_currency_code == currency_code + and candidate_trade.offer.direction == direction), + open_trades) + filtered_trades = list(trade_filter) + if len(filtered_trades): + # This sort is done on server !? + filtered_trades.sort(key=operator.attrgetter('date')) + return filtered_trades[0] + else: + return None + else: + return None + + def get_version(self): + try: + response = self.version_stub.GetVersion.with_call( + bisq_messages.GetVersionRequest(), + metadata=[('password', self.api_password)]) + return response[0].version + except grpc.RpcError as rpc_error: + print('gRPC API Exception: %s', rpc_error) + + @staticmethod + def reference_price_offset(reference_price, spread_midpoint): + return round(reference_price * spread_midpoint, 8) + + @staticmethod + def reference_price_margin_offset(spread): + return round(spread / Decimal(2.00), 2) + + def calc_buy_offset_price_margin(self, reference_price_margin, target_spread): + offset = self.reference_price_margin_offset(target_spread) + return round(reference_price_margin - offset, 2) + + def calc_sell_offset_price_margin(self, reference_price_margin, target_spread): + offset = self.reference_price_margin_offset(target_spread) + return round(reference_price_margin + offset, 2) + + @staticmethod + def satoshis_to_btc_str(sats): + btc = Decimal(round(sats * 0.00000001, 8)) + return format(btc, '2.8f') + + @staticmethod + def get_my_offers_header(): + return '\t\t{0:<50} {1:<11} {2:<9} {3:>10} {4:>16} {5:>18} {6:<50}' \ + .format('OFFER_ID', + 'DIRECTION', + 'CURRENCY', + 'PRICE', + 'AMOUNT (BTC)', + 'BUYER COST (EUR)', + 'PAYMENT_ACCOUNT_ID') + + def get_offer_tbl(self, offer): + header = self.get_my_offers_header() + columns = '\t\t{0:<50} {1:<11} {2:<9} {3:>10} {4:>16} {5:>18} {6:<50}' \ + .format(offer.id, + offer.direction, + offer.counter_currency_code, + offer.price, + self.satoshis_to_btc_str(offer.amount), + offer.volume, + offer.payment_account_id) + return header + '\n' + columns + + @staticmethod + def get_my_bsqswap_offers_header(): + return '\t\t{0:<50} {1:<11} {2:<10} {3:>11} {4:>12} {5:>16}' \ + .format('OFFER_ID', + 'DIRECTION', + 'CURRENCY', + 'PRICE (BTC)', + 'AMOUNT (BTC)', + 'BUYER COST (BSQ)') + + def get_bsqswap_offer_tbl(self, offer): + btc_amount = round(offer.amount * Decimal(0.00000001), 8) + headers = self.get_my_bsqswap_offers_header() + columns = '\t\t{0:<50} {1:<11} {2:<10} {3:>11} {4:>12} {5:>16}' \ + .format(offer.id, + offer.direction + ' (BTC)', + offer.base_currency_code, + offer.price, + btc_amount, + offer.volume) + return headers + '\n' + columns + + def get_my_usd_offers_tbl(self): + headers = self.get_my_offers_header() + rows = [] + my_usd_offers = self.get_my_offers('BUY', 'USD') + for o in my_usd_offers: + columns = '\t\t{0:<50} {1:<12} {2:<12} {3:>12}'.format(o.id, o.direction, o.counter_currency_code, o.price) + rows.append(columns) + my_usd_offers = self.get_my_offers('SELL', 'USD') + for o in my_usd_offers: + columns = '\t\t{0:<50} {1:<12} {2:<12} {3:>12}'.format(o.id, o.direction, o.counter_currency_code, o.price) + rows.append(columns) + return self.get_tbl(headers, rows) + + @staticmethod + def get_trades_header(): + return '\t\t{0:<50} {1:<26} {2:<20} {3:>16} {4:>13} {5:>12}' \ + .format('TRADE_ID', + 'DATE', + 'ROLE', + 'PRICE', + 'AMOUNT (BTC)', + 'BUYER COST') + + def get_trades_tbl(self, trades): + headers = self.get_trades_header() + rows = [] + for trade in trades: + # For fiat offer the baseCurrencyCode is BTC and the counterCurrencyCode is the fiat currency. + # For altcoin offers it is the opposite: baseCurrencyCode is the altcoin and the counterCurrencyCode is BTC. + if trade.offer.base_currency_code == 'BTC': + currency_code = trade.offer.counter_currency_code + else: + currency_code = trade.offer.base_currency_code + columns = '\t\t{0:<50} {1:<26} {2:<20} {3:>16} {4:>13} {5:>12}' \ + .format(trade.trade_id, + datetime.fromtimestamp( + trade.date / Decimal(1000.0), + tz=timezone.utc).isoformat(), + trade.role, + trade.trade_price + ' ' + currency_code, + self.satoshis_to_btc_str(trade.trade_amount_as_long), + trade.trade_volume + ' ' + currency_code) + rows.append(columns) + return self.get_tbl(headers, rows) + + @staticmethod + def get_tbl(headers, rows): + tbl = headers + '\n' + for row in rows: + tbl = tbl + row + '\n' + return tbl + + # Return a multi-line string describing a trade + payment details. + def get_trade_payment_summary(self, trade): + currency_code = trade.offer.counter_currency_code + is_my_offer = trade.offer.is_my_offer + if is_my_offer is True: + payment_details = trade.contract.taker_payment_account_payload.payment_details + else: + payment_details = trade.contract.maker_payment_account_payload.payment_details + + return 'ID: {0}\nDate: {1}\nRole: {2}\nPrice: {3} {4}\nAmount: {5} BTC\nBuyer cost: {6} {7}\nPayment {8}' \ + .format(trade.trade_id, + datetime.fromtimestamp( + trade.date / Decimal(1000.0), + tz=timezone.utc).isoformat(), + trade.role, + trade.trade_price, + currency_code, + self.satoshis_to_btc_str( + trade.trade_amount_as_long), + trade.trade_volume, + currency_code, + payment_details) + + def __str__(self): + return 'host=' + self.host + ' port=' + self.port + ' api_password=' + '*****' diff --git a/python-examples/bots/bisqswap_mm_bot.ini b/python-examples/bots/bisqswap_mm_bot.ini new file mode 100644 index 0000000..631970f --- /dev/null +++ b/python-examples/bots/bisqswap_mm_bot.ini @@ -0,0 +1,23 @@ +# When offers are taken and trades executed, the bot replaces +# them with new offers so there is always 1 buy and 1 sell offer. +# +# Trades above the midpoint are buys and trades below the midpoint are sales. +# +# See https://en.wikipedia.org/wiki/Bid%E2%80%93ask_spread +# See https://kollider.medium.com/demystifying-market-making-in-cryptocurrency-trading-98efe4f709da +# + + +[general] +offers_poll_interval_in_sec = 30 + +# A hard-coded "market price" for BSQ. +reference_price = 0.00004000 + +# The target spread as a percent literal. 4.00 means 4% spread. +# For a 4% spread, a buy BTC offer price would be the reference price -2%, +# a sell BTC offer price would be the reference price +2%. +spread = 4.00 + +# The amount of BTC to be traded, in Satoshis. +amount = 10000000 diff --git a/python-examples/bots/bisqswap_mm_bot.py b/python-examples/bots/bisqswap_mm_bot.py new file mode 100644 index 0000000..c3adf0b --- /dev/null +++ b/python-examples/bots/bisqswap_mm_bot.py @@ -0,0 +1,142 @@ +import configparser +import sys +import threading +from decimal import * + +from bisq_client import BisqClient +from logger import log + + +# To run file in Python console: main('localhost', 9998, 'xyz') +# To run from another Python script: +# import bisqswap_mm_bot +# bisqswap_mm_bot.main('localhost', 9998, 'xyz') + + +# noinspection PyInitNewSignature +class BsqSWapMMBot(BisqClient): + def __init__(self, host, port, api_password): + super().__init__(host, port, api_password) + self.config = configparser.ConfigParser() + self.config.read('bisqswap_mm_bot.ini') + log.info('Starting %s', str(self)) + + def run(self): + self.print_configuration() + + # Poll the bot's offers. Replace taken offers with new offers so there is always 1 buy and 1 sell offer. + timer = threading.Timer(0, ()) + max_iterations = 100 + count = 0 + interval = 0 + try: + while not timer.finished.wait(interval): + self.make_market() + count = count + 1 + if count >= max_iterations: + timer.cancel() + else: + interval = self.offers_poll_interval_in_sec() + sys.exit(0) + except KeyboardInterrupt: + log.warning('Timer interrupted') + sys.exit(0) + + def make_market(self): + # Get or create mm buy offer. + buy_offer = self.get_my_buy_btc_offer() + if buy_offer is None: + log.info('\tNo buy BTC with BSQ offers.') + buy_offer = self.create_buy_btc_offer() + log.info('My new BUY BTC with BSQ offer:\n%s', self.get_bsqswap_offer_tbl(buy_offer)) + else: + log.info('My old BUY BTC with BSQ offer:\n%s', self.get_bsqswap_offer_tbl(buy_offer)) + + # Get or create mm sell offer. + sell_offer = self.get_my_sell_btc_offer() + if sell_offer is None: + sell_offer = self.create_sell_btc_offer() + log.info('My new SELL BTC for BSQ offer:\n%s', self.get_bsqswap_offer_tbl(sell_offer)) + else: + log.info('My old SELL BTC for BSQ offer:\n%s', self.get_bsqswap_offer_tbl(sell_offer)) + + all_closed_trades = self.get_closed_trades() + log.info('My Closed Trades:\n%s', self.get_trades_tbl(all_closed_trades)) + + log.info('Going to sleep for %d seconds', self.offers_poll_interval_in_sec()) + + def get_my_buy_btc_offer(self): + my_buy_offers = self.get_my_bsqswap_offers('BUY') + if len(my_buy_offers): + return my_buy_offers[0] + else: + return None + + def create_buy_btc_offer(self): + amount = self.amount() + buy_price = self.calc_buy_offset_price() + log.info('Creating BUY BTC offer with %.2f BSQ at price of %.8f BTC for 1 BSQ.', amount, buy_price) + log.warning('Remember, a buy offer is an offer to buy BTC!') + new_buy_offer = self.create_bsqswap_offer('BUY', str(buy_price), amount, amount) + return new_buy_offer + + def get_my_sell_btc_offer(self): + my_sell_offers = self.get_my_bsqswap_offers('SELL') + if len(my_sell_offers): + return my_sell_offers[0] + else: + return None + + def create_sell_btc_offer(self): + amount = self.amount() + sell_price = self.calc_sell_offset_price() + log.info('Creating SELL BTC for %.2f BSQ at price %.8f BTC for 1 BSQ.', amount, sell_price) + log.warning('Remember, a sell offer is an offer to sell BTC!') + new_sell_offer = self.create_bsqswap_offer('SELL', str(sell_price), amount, amount) + return new_sell_offer + + def calc_buy_offset_price(self): + # With a 4% spread, a buy BTC offer price would be market price -2%. + # 2% of X = 0.00000080 + offset = self.reference_price_offset(self.reference_price(), self.spread_midpoint()) + return round(self.reference_price() - offset, 8) + + def calc_sell_offset_price(self): + # With a 4% spread, a sell BTC offer price would be market price +2%. + offset = self.reference_price_offset(self.reference_price(), self.spread_midpoint()) + return round(self.reference_price() + offset, 8) + + def print_configuration(self): + offset = self.reference_price_offset(self.reference_price(), self.spread_midpoint()) + description = 'Bisq Reference Price = {0} BTC Spread = {1}% Reference Price Offset={2:.8f} BTC' \ + .format(self.reference_price(), + self.spread(), + offset) + log.info('Bot Configuration: %s', description) + + def offers_poll_interval_in_sec(self): + return int(self.config.get('general', 'offers_poll_interval_in_sec')) + + def reference_price(self): + return Decimal(self.config.get('general', 'reference_price')) + + def spread(self): + return Decimal(self.config.get('general', 'spread')) + + def spread_midpoint(self): + midpoint = self.spread() / Decimal(2) + return Decimal(round(midpoint * Decimal(0.01), 2)) + + def amount(self): + return int(self.config.get('general', 'amount')) + + def __str__(self): + description = 'BsqSWapMMBot: host={0}, port={1}, api_password={2}' \ + .format(self.host, + str(self.port), + '*****') + return description + + +def main(host, port, api_password): + BsqSWapMMBot(host, port, api_password).run() diff --git a/python-examples/bots/events/__init__.py b/python-examples/bots/events/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/python-examples/bots/events/event.py b/python-examples/bots/events/event.py new file mode 100644 index 0000000..5f9c1d7 --- /dev/null +++ b/python-examples/bots/events/event.py @@ -0,0 +1,26 @@ +# From https://expobrain.net/2010/07/31/simple-event-dispatcher-in-python +class Event(object): + """ + Generic event to use with EventDispatcher. + """ + + def __init__(self, event_type, data=None): + """ + The constructor accepts an event type as string and a custom data + """ + self._type = event_type + self._data = data + + @property + def type(self): + """ + Returns the event type + """ + return self._type + + @property + def data(self): + """ + Returns the data associated to the event + """ + return self._data diff --git a/python-examples/bots/events/event_dispatcher.py b/python-examples/bots/events/event_dispatcher.py new file mode 100644 index 0000000..5364fe1 --- /dev/null +++ b/python-examples/bots/events/event_dispatcher.py @@ -0,0 +1,65 @@ +# From https://expobrain.net/2010/07/31/simple-event-dispatcher-in-python +class EventDispatcher(object): + """ + Generic event dispatcher which listen and dispatch events + """ + + def __init__(self): + self._events = dict() + + def __del__(self): + """ + Remove all listener references at destruction time + """ + self._events = None + + def has_listener(self, event_type, listener): + """ + Return true if listener is register to event_type + """ + # Check for event type and for the listener + if event_type in self._events.keys(): + return listener in self._events[event_type] + else: + return False + + def dispatch_event(self, event): + """ + Dispatch an instance of Event class + """ + # Dispatch the event to all the associated listeners + if event.type in self._events.keys(): + listeners = self._events[event.type] + + for listener in listeners: + listener(event) + + def add_event_listener(self, event_type, listener): + """ + Add an event listener for an event type + """ + # Add listener to the event type + if not self.has_listener(event_type, listener): + listeners = self._events.get(event_type, []) + + listeners.append(listener) + + self._events[event_type] = listeners + + def remove_event_listener(self, event_type, listener): + """ + Remove event listener. + """ + # Remove the listener from the event type + if self.has_listener(event_type, listener): + listeners = self._events[event_type] + + if len(listeners) == 1: + # Only this listener remains so remove the key + del self._events[event_type] + + else: + # Update listeners chain + listeners.remove(listener) + + self._events[event_type] = listeners diff --git a/python-examples/bots/events/test_trade_event.py b/python-examples/bots/events/test_trade_event.py new file mode 100644 index 0000000..0866ae7 --- /dev/null +++ b/python-examples/bots/events/test_trade_event.py @@ -0,0 +1,11 @@ +# From https://expobrain.net/2010/07/31/simple-event-dispatcher-in-python +# Create and instance of event dispatcher +from event_dispatcher import EventDispatcher +from trade_event_listener import TradeEventListener +from trade_event_sender import TradeEventSender + +dispatcher = EventDispatcher() + +sender = TradeEventSender(dispatcher) +listener = TradeEventListener(dispatcher) +sender.main() diff --git a/python-examples/bots/events/trade_event.py b/python-examples/bots/events/trade_event.py new file mode 100644 index 0000000..9d06f3e --- /dev/null +++ b/python-examples/bots/events/trade_event.py @@ -0,0 +1,13 @@ +# From https://expobrain.net/2010/07/31/simple-event-dispatcher-in-python +from event import Event + + +class TradeEvent(Event): + """ + When subclassing Event class the only thing you must do is to define + a list of class level constants which defines the event types and the + string associated to them + """ + + MUST_SEND_PAYMENT_STARTED_MSG_EVENT = 'MUST_SEND_PAYMENT_STARTED_MSG_EVENT' + MUST_SEND_PAYMENT_RECEIVED_MSG_EVENT = 'MUST_SEND_PAYMENT_RECEIVED_MSG_EVENT' diff --git a/python-examples/bots/events/trade_event_data.py b/python-examples/bots/events/trade_event_data.py new file mode 100644 index 0000000..3a938a3 --- /dev/null +++ b/python-examples/bots/events/trade_event_data.py @@ -0,0 +1,4 @@ +class TradeEventData: + def __init__(self, trade, instructions): + self.trade = trade + self.instructions = instructions diff --git a/python-examples/bots/events/trade_event_listener.py b/python-examples/bots/events/trade_event_listener.py new file mode 100644 index 0000000..bca569e --- /dev/null +++ b/python-examples/bots/events/trade_event_listener.py @@ -0,0 +1,26 @@ +# From https://expobrain.net/2010/07/31/simple-event-dispatcher-in-python +from bots.logger import log +from trade_event import TradeEvent + + +class TradeEventListener(object): + """ + First class which ask who is listening to it + """ + + def __init__(self, event_dispatcher): + # Save a reference to the event dispatch + self.event_dispatcher = event_dispatcher + + # Listen for the MUST_SEND_PAYMENT_STARTED_MSG_EVENT event type + self.event_dispatcher.add_event_listener(TradeEvent.MUST_SEND_PAYMENT_STARTED_MSG_EVENT, + self.handle_trade_event) + + def handle_trade_event(self, event): + """ + Event handler for the MUST_SEND_PAYMENT_STARTED_MSG_EVENT event type + """ + log.info('Handling %s: %s', event.type, event.data) + data = event.data + log.info('trade: %s', str(data.trade)) + log.info('what_to_do: %s', data.instructions) diff --git a/python-examples/bots/events/trade_event_sender.py b/python-examples/bots/events/trade_event_sender.py new file mode 100644 index 0000000..b2546b6 --- /dev/null +++ b/python-examples/bots/events/trade_event_sender.py @@ -0,0 +1,22 @@ +# From https://expobrain.net/2010/07/31/simple-event-dispatcher-in-python +from trade_event import TradeEvent +from trade_event_data import TradeEventData + + +class TradeEventSender(object): + """ + First class which ask who is listening to it + """ + + def __init__(self, event_dispatcher): + # Save a reference to the event dispatch + self.event_dispatcher = event_dispatcher + + def main(self): + """ + Send a TradeEvent + """ + + self.event_dispatcher.dispatch_event( + TradeEvent(TradeEvent.MUST_SEND_PAYMENT_STARTED_MSG_EVENT, TradeEventData('trade', 'what to do...')) + ) diff --git a/python-examples/bots/logger.py b/python-examples/bots/logger.py new file mode 100644 index 0000000..20affd2 --- /dev/null +++ b/python-examples/bots/logger.py @@ -0,0 +1,7 @@ +import logging + +logging.basicConfig( + format='[%(asctime)s %(levelname)s %(threadName)s] %(message)s', + level=logging.INFO, + datefmt='%H:%M:%S') +log = logging.getLogger() diff --git a/python-examples/bots/protocol_step.py b/python-examples/bots/protocol_step.py new file mode 100644 index 0000000..1233079 --- /dev/null +++ b/python-examples/bots/protocol_step.py @@ -0,0 +1,168 @@ +import grpc + +import bisq.api.grpc_pb2 as bisq_messages +from logger import log + +WAIT_FOR_TRADE_DEPOSIT_CONFIRMATION = 'WAIT_FOR_TRADE_DEPOSIT_CONFIRMATION' +WAIT_FOR_PAYMENT_STARTED_MSG = 'WAIT_FOR_PAYMENT_STARTED_MSG' +WAIT_FOR_PAYMENT_RECEIVED_MSG = 'WAIT_FOR_PAYMENT_RECEIVED_MSG' +WAIT_FOR_PAYOUT_IS_PUBLISHED = 'WAIT_FOR_PAYOUT_IS_PUBLISHED' + +SEND_PAYMENT_STARTED_MSG = 'SEND_PAYMENT_STARTED_MSG' +SEND_PAYMENT_RECEIVED_MSG = 'SEND_PAYMENT_RECEIVED_MSG' +CLOSE_TRADE = 'CLOSE_TRADE' +STOP_BOT_OPEN_UI_CONTACT_SUPPORT = 'STOP_BOT_OPEN_UI_CONTACT_SUPPORT' + + +class ProtocolStep(): + def __init__(self, trades_stub, api_password, trade_id): + self.trades_stub = trades_stub + self.api_password = api_password + self.trade_id = trade_id + self.timer = None + + def run(self): + log.info('Execute automatic protocol step for trade %s.', self.trade_id) + trade = self.get_trade() + if self.i_am_buyer(trade) is True: + self.do_next_buy_step(trade) + else: + self.do_next_sell_step(trade) + + def can_execute(self): + trade = self.get_trade() + if self.i_am_buyer(trade) is True: + next_buy_step = self.get_next_buy_step(trade) + return next_buy_step != SEND_PAYMENT_STARTED_MSG + else: + next_sell_step = self.get_next_sell_step(trade) + return next_sell_step != SEND_PAYMENT_RECEIVED_MSG + + def do_next_buy_step(self, trade): + next_buy_step = self.get_next_buy_step(trade) + log.info('\tTrade %s: next buy step: %s', trade.trade_id, next_buy_step) + if next_buy_step == SEND_PAYMENT_STARTED_MSG: + log.warn('\tPayment must be sent manually, and payment sent msg must be sent from UI.') + elif next_buy_step == WAIT_FOR_PAYMENT_RECEIVED_MSG: + log.info('\tPayment sent, waiting for payment received msg ...') + elif next_buy_step == WAIT_FOR_PAYOUT_IS_PUBLISHED: + log.info('\tPayment received, waiting for payout tx to be published ...') + elif next_buy_step == CLOSE_TRADE: + log.info('\tPayment received, payout tx is published, closing trade ...') + self.close_trade() + elif next_buy_step == STOP_BOT_OPEN_UI_CONTACT_SUPPORT: + log.error('Something bad happened. You have to shutdown the bot,' + + ' start the desktop UI, and open a support ticked for trade %s', + self.trade_id) + + def do_next_sell_step(self, trade): + next_sell_step = self.get_next_sell_step(trade) + log.info('\tTrade %s: next sell step: %s', trade.trade_id, next_sell_step) + if next_sell_step == WAIT_FOR_PAYMENT_STARTED_MSG: + log.info('\tWaiting for buyer to start payment ...') + if next_sell_step == SEND_PAYMENT_RECEIVED_MSG: + log.warn('\tPayment receipt must be confirmed manually, and payment received' + + ' confirmation msg must be sent from UI.') + elif next_sell_step == WAIT_FOR_PAYOUT_IS_PUBLISHED: + log.info('\tPayment received, waiting for payout tx to be published ...') + elif next_sell_step == CLOSE_TRADE: + log.info('\tPayment received, payout tx is published, closing trade ...') + self.close_trade() + elif next_sell_step == STOP_BOT_OPEN_UI_CONTACT_SUPPORT: + log.error('Something bad happened. You have to shutdown the bot,' + + ' start the desktop UI, and open a support ticked for trade %s', + self.trade_id) + + @staticmethod + def get_next_buy_step(trade): + if trade.is_deposit_published is False: + return WAIT_FOR_TRADE_DEPOSIT_CONFIRMATION + elif trade.is_deposit_confirmed is False: + return WAIT_FOR_TRADE_DEPOSIT_CONFIRMATION + elif trade.is_payment_started_message_sent is False: + return SEND_PAYMENT_STARTED_MSG + elif trade.is_payment_started_message_sent is True and trade.is_payment_received_message_sent is False: + return WAIT_FOR_PAYMENT_RECEIVED_MSG + elif trade.is_payment_received_message_sent is True and trade.is_payout_published is False: + return WAIT_FOR_PAYOUT_IS_PUBLISHED + elif trade.is_payout_published is True: + return CLOSE_TRADE + else: + return STOP_BOT_OPEN_UI_CONTACT_SUPPORT + + @staticmethod + def get_next_sell_step(trade): + if trade.is_deposit_published is False: + return WAIT_FOR_TRADE_DEPOSIT_CONFIRMATION + elif trade.is_deposit_confirmed is False: + return WAIT_FOR_TRADE_DEPOSIT_CONFIRMATION + elif trade.is_payment_started_message_sent is False: + return WAIT_FOR_PAYMENT_STARTED_MSG + elif trade.is_payment_started_message_sent is True and trade.is_payment_received_message_sent is False: + return SEND_PAYMENT_RECEIVED_MSG + elif trade.is_payment_received_message_sent is True and trade.is_payout_published is False: + return WAIT_FOR_PAYOUT_IS_PUBLISHED + elif trade.is_payout_published is True: + return CLOSE_TRADE + else: + return STOP_BOT_OPEN_UI_CONTACT_SUPPORT + + @staticmethod + def i_am_buyer(trade): + offer = trade.offer + if offer.is_my_offer is True: + return offer.direction == 'BUY' + else: + return offer.direction == 'SELL' + + def send_payment_started_msg(self): + trade = self.get_trade() + next_buy_step = self.get_next_buy_step(trade) + if next_buy_step != SEND_PAYMENT_STARTED_MSG: + raise 'Trade {0} is not in proper state to send a payment started msg.' \ + + ' Next step should be {1}.'.format(trade.trade_id, next_buy_step) + log.info('Sending payment started msg for trade %s.', self.trade_id) + try: + self.trades_stub.ConfirmPaymentStarted.with_call( + bisq_messages.ConfirmPaymentStartedRequest(trade_id=self.trade_id), + metadata=[('password', self.api_password)]) + + except grpc.RpcError as rpc_error: + print('gRPC API Exception: %s', rpc_error) + + def send_payment_received_msg(self): + trade = self.get_trade() + next_sell_step = self.get_next_sell_step(trade) + if next_sell_step != SEND_PAYMENT_RECEIVED_MSG: + raise 'Trade {0} is not in proper state to send a payment received confirmation msg.' \ + + ' Next step should be {1}.'.format(trade.trade_id, next_sell_step) + log.info('Sending payment received confirmation msg for trade %s.', self.trade_id) + try: + self.trades_stub.ConfirmPaymentReceived.with_call( + bisq_messages.ConfirmPaymentReceivedRequest(trade_id=self.trade_id), + metadata=[('password', self.api_password)]) + except grpc.RpcError as rpc_error: + print('gRPC API Exception: %s', rpc_error) + + def close_trade(self): + try: + self.trades_stub.CloseTrade.with_call( + bisq_messages.CloseTradeRequest(trade_id=self.trade_id), + metadata=[('password', self.api_password)]) + except grpc.RpcError as rpc_error: + print('gRPC API Exception: %s', rpc_error) + + def get_trade(self): + try: + response = self.trades_stub.GetTrade.with_call( + bisq_messages.GetTradeRequest(trade_id=self.trade_id), + metadata=[('password', self.api_password)]) + return response[0].trade + except grpc.RpcError as rpc_error: + print('gRPC API Exception: %s', rpc_error) + + def __str__(self): + description = 'ProtocolStep: trades_stub={0}, trade_id={1}' \ + .format(self.trades_stub, + str(self.trade_id)) + return description diff --git a/python-examples/bots/run-main.py b/python-examples/bots/run-main.py new file mode 100644 index 0000000..fb38ac0 --- /dev/null +++ b/python-examples/bots/run-main.py @@ -0,0 +1,6 @@ +import best_priced_offer_bot +import bisqswap_mm_bot + +# best_priced_offer_bot.main('localhost', 9998, 'xyz') + +bisqswap_mm_bot.main('localhost', 9998, 'xyz') diff --git a/python-examples/bots/sepa_mm_bot.ini b/python-examples/bots/sepa_mm_bot.ini new file mode 100644 index 0000000..5b53359 --- /dev/null +++ b/python-examples/bots/sepa_mm_bot.ini @@ -0,0 +1,14 @@ +[general] +offers_poll_interval_in_sec = 30 + +# TODO Create sepa_mm_bot.py method: get_oldest_sepa_account +sepa_payment_account_id=f82c408a-b7b9-4db6-9e14-146cc4c9ee64 + +# TODO Explain why a negative reference_price_margin value makes the offer price more competitive. +reference_price_margin=0.00 + +# The target spread as a percent literal. Cannot be a negative value. +spread = 5.00 + +# The amount of BTC to be traded, in Satoshis. +amount = 10000000 diff --git a/python-examples/bots/sepa_mm_bot.py b/python-examples/bots/sepa_mm_bot.py new file mode 100644 index 0000000..56ba53c --- /dev/null +++ b/python-examples/bots/sepa_mm_bot.py @@ -0,0 +1,237 @@ +import sys +import threading +import time + +from bisq_client import BisqClient +from events.trade_event import TradeEvent +from events.trade_event_data import TradeEventData +from protocol_step import * + + +# noinspection PyInitNewSignature +class SepaMMBot(BisqClient): + def __init__(self, host, port, api_password, event_dispatcher): + super().__init__(host, port, api_password) + self.is_running = False + self.timer = None + self.event_dispatcher = event_dispatcher + log.info('Starting ' + str(self)) + + def start(self, offers_poll_interval_in_sec, + reference_price_margin, + target_spread, + amount_in_satoshis, + sepa_payment_account_id): + log.info('Starting SEPA market maker bot.') + log.info('Reference Price Margin = %.2f%s, Spread = %.2f%s, Amount = %d satoshis, SEPA Payment Account Id = %s', + reference_price_margin, '%', target_spread, '%', amount_in_satoshis, sepa_payment_account_id) + + if self.is_connected() is False: + self.open_channel() + + # Poll the bot's offers. Replace taken offers with new offers so there is always 1 buy and 1 sell offer. + self.timer = threading.Timer(0, ()) + max_iterations = 100 + count = 0 + interval = 0 + try: + self.is_running = True + while not self.timer.finished.wait(interval): + # Make sure there is 1 SEPA BUY BTC offer, and 1 SEPA SELL BTC offer. + self.make_market(reference_price_margin, + target_spread, + amount_in_satoshis, + sepa_payment_account_id) + time.sleep(1) + # Execute a single trade protocol step for each open SEPA trade. + self.process_open_trades(sepa_payment_account_id) + + log.info('Bot is sleeping for %d seconds.', offers_poll_interval_in_sec) + count = count + 1 + if count >= max_iterations: + self.timer.cancel() + else: + interval = offers_poll_interval_in_sec + sys.exit(0) + except KeyboardInterrupt: + log.info('Timer interrupted') + sys.exit(0) + + def shutdown(self): + log.info('Shutting down SEPA market maker bot.') + if self.timer is not None: + self.timer.cancel() + self.close_channel() + self.is_running = False + + def make_market(self, reference_price_margin, + target_spread, + amount_in_satoshis, + sepa_payment_account_id): + self.make_buy_offer(reference_price_margin, target_spread, amount_in_satoshis, sepa_payment_account_id) + self.make_sell_offer(reference_price_margin, target_spread, amount_in_satoshis, sepa_payment_account_id) + + def process_open_trades(self, sepa_payment_account_id): + self.process_buy_trades(sepa_payment_account_id) + self.process_sell_trades(sepa_payment_account_id) + + # Perform the next automatic trade protocol step for each open BUY trade. + # Manual steps trigger an event for the UI. + def process_buy_trades(self, sepa_payment_account_id): + open_trades = self.get_open_fiat_trades('EUR', 'BUY') + trade_filter = filter( + lambda candidate_trade: (candidate_trade.offer.payment_account_id == sepa_payment_account_id), + open_trades) + filtered_trades = list(trade_filter) + if len(filtered_trades): + log.info('Do one protocol step for %d open BUY trades.', len(filtered_trades)) + for trade in filtered_trades: + protocol_step = ProtocolStep(self.trades_stub, self.api_password, trade.trade_id) + if protocol_step.can_execute() is True: + protocol_step.run() + else: + log.warn('Next protocol step is manual (trade %s).', trade.trade_id) + next_buy_step = protocol_step.get_next_buy_step(trade) + if next_buy_step == SEND_PAYMENT_STARTED_MSG: + buy_trade_tbl = self.get_trades_tbl([trade]) + log.warn('Dispatching %s for trade:\n%s', + TradeEvent.MUST_SEND_PAYMENT_STARTED_MSG_EVENT, + buy_trade_tbl) + self.event_dispatcher.dispatch_event( + TradeEvent(TradeEvent.MUST_SEND_PAYMENT_STARTED_MSG_EVENT, + TradeEventData(trade, 'Send trade payment and click the button.'))) + else: + log.info('There are no open BUY trades at this time.') + + # Perform the next automatic trade protocol step for each open SELL trade. + # Manual steps trigger an event for the UI. + def process_sell_trades(self, sepa_payment_account_id): + open_trades = self.get_open_fiat_trades('EUR', 'SELL') + trade_filter = filter( + lambda candidate_trade: (candidate_trade.offer.payment_account_id == sepa_payment_account_id), + open_trades) + filtered_trades = list(trade_filter) + if len(filtered_trades): + log.info('Do one protocol step for %d open SELL trades.', len(filtered_trades)) + for trade in filtered_trades: + protocol_step = ProtocolStep(self.trades_stub, self.api_password, trade.trade_id) + if protocol_step.can_execute() is True: + protocol_step.run() + else: + log.warn('Next protocol step is manual (trade %s).', trade.trade_id) + next_sell_step = protocol_step.get_next_sell_step(trade) + if next_sell_step == SEND_PAYMENT_RECEIVED_MSG: + sell_trade_tbl = self.get_trades_tbl([trade]) + log.warn('Dispatching %s for trade:\n%s', + TradeEvent.MUST_SEND_PAYMENT_RECEIVED_MSG_EVENT, + sell_trade_tbl) + self.event_dispatcher.dispatch_event( + TradeEvent(TradeEvent.MUST_SEND_PAYMENT_RECEIVED_MSG_EVENT, + TradeEventData( + trade, + 'Confirm trade payment was received and click the button.'))) + else: + log.info('There are no open SELL trades at this time.') + + def make_buy_offer(self, reference_price_margin, + target_spread, + amount_in_satoshis, + sepa_payment_account_id): + # Get or create buy offer. + buy_offer = self.get_my_buy_btc_offer() + if buy_offer is None: + log.info('No buy BTC offers.') + buy_offer = self.create_buy_btc_offer(reference_price_margin, + target_spread, + amount_in_satoshis, + sepa_payment_account_id) + log.info('Created new BUY BTC offer:\n%s', self.get_offer_tbl(buy_offer)) + else: + log.info('My open BUY BTC offer:\n%s', self.get_offer_tbl(buy_offer)) + + def make_sell_offer(self, reference_price_margin, + target_spread, + amount_in_satoshis, + sepa_payment_account_id): + # Get or create sell offer. + sell_offer = self.get_my_sell_btc_offer() + if sell_offer is None: + log.info('No sell BTC offers.') + sell_offer = self.create_sell_btc_offer(reference_price_margin, + target_spread, + amount_in_satoshis, + sepa_payment_account_id) + log.info('Created new SELL BTC offer:\n%s', self.get_offer_tbl(sell_offer)) + else: + log.info('My open SELL BTC offer:\n%s', self.get_offer_tbl(sell_offer)) + + def create_buy_btc_offer(self, reference_price_margin, + target_spread, + amount_in_satoshis, + sepa_payment_account_id): + margin = self.calc_buy_offset_price_margin(reference_price_margin, target_spread) + return self.create_margin_priced_offer('EUR', + 'BUY', + margin, + amount_in_satoshis, + amount_in_satoshis, + sepa_payment_account_id) + + def create_sell_btc_offer(self, reference_price_margin, + target_spread, + amount_in_satoshis, + sepa_payment_account_id): + margin = self.calc_sell_offset_price_margin(reference_price_margin, target_spread) + return self.create_margin_priced_offer('EUR', + 'SELL', + margin, + amount_in_satoshis, + amount_in_satoshis, + sepa_payment_account_id) + + def get_my_buy_btc_offer(self): + my_eur_buy_offers = self.get_my_offers('BUY', 'EUR') + if my_eur_buy_offers is None: + return None + else: + offer_filter = filter(lambda candidate_offer: + (candidate_offer.payment_method_id == 'SEPA'), + my_eur_buy_offers) + filtered_offers = list(offer_filter) + if len(filtered_offers): + return filtered_offers[0] + else: + return None + + def get_my_sell_btc_offer(self): + my_eur_sell_offers = self.get_my_offers('SELL', 'EUR') + if len(my_eur_sell_offers): + offer_filter = filter(lambda candidate_offer: + (candidate_offer.payment_method_id == 'SEPA'), + my_eur_sell_offers) + filtered_offers = list(offer_filter) + if len(filtered_offers): + return filtered_offers[0] + else: + return None + else: + return None + + def send_payment_started_msg(self, trade_id): + protocol_step = ProtocolStep(self.trades_stub, self.api_password, trade_id) + protocol_step.send_payment_started_msg() + + def send_payment_received_msg(self, trade_id): + protocol_step = ProtocolStep(self.trades_stub, self.api_password, trade_id) + protocol_step.send_payment_received_msg() + + def __str__(self): + description = 'SepaMMBot: host={0}, port={1}, api_password={2}' \ + .format(self.host, + str(self.port), + '*****') + return description + + +def main(host, port, api_password): + SepaMMBot(host, port, api_password).start() diff --git a/python-examples/bots/sepa_mm_bot_ui.py b/python-examples/bots/sepa_mm_bot_ui.py new file mode 100644 index 0000000..0fffbdb --- /dev/null +++ b/python-examples/bots/sepa_mm_bot_ui.py @@ -0,0 +1,318 @@ +import configparser +import sys +import threading +from decimal import * +from pathlib import Path +from tkinter import * + +# Append the events pkg to the system path before importing the sepa_mm_bot. +# TODO Is there a better way to do this? +sys.path.append('events') + +import sepa_mm_bot + +from events.event_dispatcher import EventDispatcher +from events.trade_event import TradeEvent +from logger import log + +config = configparser.ConfigParser() +config.read('sepa_mm_bot.ini') + +OUTPUT_PATH = Path(__file__).parent +ASSETS_PATH = OUTPUT_PATH / Path('./assets') +WINDOW_WIDTH = 970 +WINDOW_HEIGHT = 575 + + +def relative_to_assets(path: str) -> Path: + return ASSETS_PATH / Path(path) + + +window = Tk() +window.title('Bisq API SEPA Market Maker Bot') +window_geometry = str(WINDOW_WIDTH) + 'x' + str(WINDOW_HEIGHT) + '+100+50' +window.geometry(window_geometry) +window.configure(bg='#F2F2F2') +window.iconphoto(False, PhotoImage(file=relative_to_assets('window_icon.png'))) + +# Initialize editable input fields from the .ini file. +price_margin_input = StringVar() +price_margin_input.set(Decimal(config.get('general', 'reference_price_margin'))) + +spread_input = StringVar() +spread_input.set(Decimal(config.get('general', 'spread'))) + +btc_amount_input = StringVar() +btc_amount_input.set(round(int(config.get('general', 'amount')) * 0.00000001, 8)) + +# Polling interval is not editable in UI. Edit the .init file and restart UI. +offers_poll_interval_in_sec = int(config.get('general', 'offers_poll_interval_in_sec')) + +# Payment account id is not editable in UI. +# If you want to change it, shut down the bot and edit the .ini file. +sepa_payment_account_id = config.get('general', 'sepa_payment_account_id') + +# Set up event dispatcher, but delay setting up listeners until bot and canvass are created. +event_dispatcher = EventDispatcher() + +# These globals are set when we handle a trade event from the event_dispatcher. +trade_id_for_start_payment_msg = None +trade_id_for_payment_received_msg = None + +# Create the bot instance this UI controls. +bot = sepa_mm_bot.SepaMMBot('localhost', 9998, 'xyz', event_dispatcher) + + +def start(): + start_thread = threading.Thread(name='Bot', target=start_bot, args=(), daemon=False) + start_thread.start() + + +def start_bot(): + disable_button(start_button) + enable_button(stop_button) + reference_price_margin = Decimal(price_margin_input.get()) + target_spread = Decimal(spread_input.get()) + bot.start(int(offers_poll_interval_in_sec), + reference_price_margin, + target_spread, + int(Decimal(btc_amount_input.get()) * 10000000), # Convert the BTC input amt to Satoshis. + sepa_payment_account_id) + + +def stop(): + shutdown_thread = threading.Thread(name='Shutdown', target=stop_bot, args=(), daemon=False) + shutdown_thread.start() + + +def stop_bot(): + global trade_id_for_start_payment_msg + trade_id_for_start_payment_msg = None + disable_button(payment_sent_button) + canvas.itemconfig(manual_buy_trade_instructions, text='') + + global trade_id_for_payment_received_msg + trade_id_for_payment_received_msg = None + disable_button(payment_received_button) + canvas.itemconfig(manual_sell_trade_instructions, text='') + + disable_button(stop_button) + bot.shutdown() + enable_button(start_button) + + +def send_payment_started_msg(): + log.info('payment_sent_button clicked') + disable_button(payment_sent_button) + global trade_id_for_start_payment_msg + bot.send_payment_started_msg(trade_id_for_start_payment_msg) + ui_update_text = 'Payment started message was sent for trade' + '\n' \ + + trade_id_for_start_payment_msg + canvas.itemconfig(manual_buy_trade_instructions, text=ui_update_text) + trade_id_for_start_payment_msg = None + + +def send_payment_received_msg(): + log.info('payment_received_button clicked') + disable_button(payment_received_button) + global trade_id_for_payment_received_msg + bot.send_payment_received_msg(trade_id_for_payment_received_msg) + ui_update_text = 'Payment received confirmation message was sent for trade' + '\n' \ + + trade_id_for_payment_received_msg + canvas.itemconfig(manual_sell_trade_instructions, text=ui_update_text) + trade_id_for_payment_received_msg = None + + +canvas = Canvas(window, + bg='#F2F2F2', + width=WINDOW_WIDTH - 12, + height=WINDOW_HEIGHT - 12, + bd=5, + highlightthickness=0, + relief='ridge') +canvas.place(x=0, y=0) +canvas.create_text( + 275.0, + 16.0, + anchor='nw', + text='SEPA EUR / BTC Market Maker Bot', + fill='#000000', + font=('IBMPlexSans Bold', 28 * -1)) + +bisq_logo_file = PhotoImage(file=relative_to_assets('bisq_logo.png')) +bisq_logo_image = canvas.create_image(116.0, 34.0, image=bisq_logo_file) + +canvas.create_text( + 39.0, + 97.0, + anchor='nw', + text='Reference Market Price Margin:', + fill='#000000', + font=('IBMPlexSans SemiBold', 14 * -1)) + +canvas.create_text( + 395.0, + 97.0, + anchor='nw', + text='Spread:', + fill='#000000', + font=('IBMPlexSans SemiBold', 13 * -1)) + +canvas.create_text( + 336.0, + 97.0, + anchor='nw', + text='%', + fill='#000000', + font=('IBMPlexSans SemiBold', 14 * -1)) + +canvas.create_text( + 506.0, + 97.0, + anchor='nw', + text='%', + fill='#000000', + font=('IBMPlexSans SemiBold', 14 * -1)) + +canvas.create_text( + 568.0, + 97.0, + anchor='nw', + text='BTC Amount:', + fill='#000000', + font=('IBMPlexSans SemiBold', 14 * -1)) + +manual_buy_trade_instructions = canvas.create_text( + 57.0, + 226.0, + anchor='nw', + text='Manual Buy BTC Instructions', + fill='#000000', + font=('IBMPlexSans SemiBold', 14 * -1)) + +manual_sell_trade_instructions = canvas.create_text( + 520.0, + 226.0, + anchor='nw', + text='Manual Sell BTC Instructions', + fill='#000000', + font=('IBMPlexSans SemiBold', 14 * -1)) + +# https://pythonguides.com/python-tkinter-button/ +# https://www.askpython.com/python-modules/tkinter/change-button-state +start_button_image = PhotoImage(file=relative_to_assets('start_button.png')) +start_button = Button( + state=NORMAL, + bg='#F2F2F2', + image=start_button_image, + borderwidth=0, + highlightthickness=0, + command=lambda: start(), + relief='flat') +start_button.place(x=140.0, y=135.0, width=150.0, height=45.0) + +stop_button_image = PhotoImage(file=relative_to_assets('stop_button.png')) +stop_button = Button( + state=DISABLED, + bg='#F2F2F2', + image=stop_button_image, + borderwidth=0, + highlightthickness=0, + command=lambda: stop(), + relief='flat') +stop_button.place(x=600.0, y=135.0, width=150.0, height=45.0) + +payment_sent_button_image = PhotoImage(file=relative_to_assets('payment_sent_button.png')) +payment_sent_button = Button( + state=DISABLED, + bg='#F2F2F2', + image=payment_sent_button_image, + borderwidth=0, + highlightthickness=0, + command=lambda: send_payment_started_msg(), + relief='flat') +payment_sent_button.place(x=140.0, y=470.0, width=150.0, height=45.0) + +payment_received_button__image = PhotoImage(file=relative_to_assets('payment_received_button.png')) +payment_received_button = Button( + state=DISABLED, + bg='#F2F2F2', + image=payment_received_button__image, + borderwidth=0, + highlightthickness=0, + command=lambda: send_payment_received_msg(), + relief='flat') +payment_received_button.place(x=600.0, y=470.0, width=150.0, height=45.0) + +reference_price_margin_image = PhotoImage(file=relative_to_assets('reference_price_margin_entry.png')) +reference_price_margin_bg = canvas.create_image(303.5, 106.5, image=reference_price_margin_image) +reference_price_margin_entry = Entry( + bd=0, + bg='#FFFFFF', + highlightthickness=0, + textvariable=price_margin_input, + justify='right') +reference_price_margin_entry.place(x=276.0, y=96.0, width=55.0, height=19.0) + +spread_image = PhotoImage(file=relative_to_assets('spread_entry.png')) +spread_bg = canvas.create_image(476.5, 104.5, image=spread_image) +spread_entry = Entry( + bd=0, + bg='#FFFFFF', + highlightthickness=0, + textvariable=spread_input, + justify='right' +) +spread_entry.place(x=452.0, y=94.0, width=49.0, height=19.0) + +btc_amount_image = PhotoImage(file=relative_to_assets('btc_amount_entry.png')) +btc_amount_bg = canvas.create_image(749.0, 104.5, image=btc_amount_image) +btc_amount_entry = Entry( + bd=0, + bg='#FFFFFF', + highlightthickness=0, + textvariable=btc_amount_input, + justify='right') +btc_amount_entry.place(x=671.0, y=94.0, width=156.0, height=19.0) + + +def disable_button(button): + button['state'] = DISABLED + + +def enable_button(button): + button['state'] = NORMAL + + +# Event handling method must be declared before listeners are set up. +def handle_trade_event(event): + log.info('\tHandling %s...', str(event.type)) + trade = event.data.trade + instructions = event.data.instructions + trade_summary = bot.get_trade_payment_summary(trade) + ui_update_text = instructions + '\n\n' + trade_summary + if event.type == TradeEvent.MUST_SEND_PAYMENT_STARTED_MSG_EVENT: + global trade_id_for_start_payment_msg + if trade_id_for_start_payment_msg is None: + canvas.itemconfig(manual_buy_trade_instructions, text=ui_update_text) + trade_id_for_start_payment_msg = trade.trade_id + enable_button(payment_sent_button) + else: + log.warning('Already waiting for manual step for trade %s', trade_id_for_start_payment_msg) + elif event.type == TradeEvent.MUST_SEND_PAYMENT_RECEIVED_MSG_EVENT: + global trade_id_for_payment_received_msg + if trade_id_for_payment_received_msg is None: + canvas.itemconfig(manual_sell_trade_instructions, text=ui_update_text) + trade_id_for_payment_received_msg = trade.trade_id + enable_button(payment_received_button) + else: + log.warning('Already waiting for manual step for trade %s', trade_id_for_payment_received_msg) + else: + raise 'Invalid event type ' + event.type + + +event_dispatcher.add_event_listener(TradeEvent.MUST_SEND_PAYMENT_STARTED_MSG_EVENT, handle_trade_event) +event_dispatcher.add_event_listener(TradeEvent.MUST_SEND_PAYMENT_RECEIVED_MSG_EVENT, handle_trade_event) + +window.resizable(False, False) +window.mainloop() diff --git a/python-examples/generate-python-protos.sh b/python-examples/generate-python-protos.sh new file mode 100755 index 0000000..5a9c0e4 --- /dev/null +++ b/python-examples/generate-python-protos.sh @@ -0,0 +1,24 @@ +#!/bin/bash +# +# Generates Bisq API protobufs from pb.proto and grpc.proto. +# +# Must be run from root project directory `bisq-grpc-api-doc`. (Set the IDE launcher's working directory). +# +# TODO Use requirement.txt for this step: +# Requirements: +# Install python plugins for protoc. +# python -m pip install grpcio grpcio-tools +# pip3 install mypy-protobuf + +source "env.sh" + +python3 -m grpc_tools.protoc \ + --proto_path=$PROTO_PATH \ + --python_out=$PYTHON_PROTO_OUT_PATH \ + --grpc_python_out=$PYTHON_PROTO_OUT_PATH $PROTO_PATH/*.proto +protoc --proto_path=$PROTO_PATH --python_out=$PYTHON_PROTO_OUT_PATH $PROTO_PATH/*.proto + +# Hack the internal import statements in the generated python to prepend the `bisq.api` package name. +# See why Google will not fix this: https://github.com/protocolbuffers/protobuf/issues/1491 +sed -i 's/import pb_pb2 as pb__pb2/import bisq.api.pb_pb2 as pb__pb2/g' $PYTHON_PROTO_OUT_PATH/grpc_pb2.py +sed -i 's/import grpc_pb2 as grpc__pb2/import bisq.api.grpc_pb2 as grpc__pb2/g' $PYTHON_PROTO_OUT_PATH/grpc_pb2_grpc.py diff --git a/python-examples/rpccalls/README.md b/python-examples/rpccalls/README.md new file mode 100644 index 0000000..55977a1 --- /dev/null +++ b/python-examples/rpccalls/README.md @@ -0,0 +1 @@ +This directory contains Python examples showing how clients can communicate with a local Bisq API daemon. \ No newline at end of file diff --git a/python-examples/rpccalls/__init__.py b/python-examples/rpccalls/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/python-examples/rpccalls/cancel_offer.py b/python-examples/rpccalls/cancel_offer.py new file mode 100644 index 0000000..437d2ea --- /dev/null +++ b/python-examples/rpccalls/cancel_offer.py @@ -0,0 +1,24 @@ +from builtins import print + +import grpc + +# from getpass import getpass +import bisq.api.grpc_pb2 as bisq_messages +import bisq.api.grpc_pb2_grpc as bisq_service + + +def main(): + grpc_channel = grpc.insecure_channel('localhost:9998') + grpc_service_stub = bisq_service.OffersStub(grpc_channel) + api_password: str = 'xyz' # getpass("Enter API password: ") + try: + response = grpc_service_stub.CancelOffer.with_call( + bisq_messages.CancelOfferRequest(id='ubwhog-fef370c3-3fe7-4ac5-b0d6-8de850916642-184'), + metadata=[('password', api_password)]) + print('Response: ' + str(response)) + except grpc.RpcError as rpc_error: + print('gRPC API Exception: %s', rpc_error) + + +if __name__ == '__main__': + main() diff --git a/python-examples/rpccalls/close_trade.py b/python-examples/rpccalls/close_trade.py new file mode 100644 index 0000000..c8c4d24 --- /dev/null +++ b/python-examples/rpccalls/close_trade.py @@ -0,0 +1,24 @@ +from builtins import print + +import grpc + +# from getpass import getpass +import bisq.api.grpc_pb2 as bisq_messages +import bisq.api.grpc_pb2_grpc as bisq_service + + +def main(): + grpc_channel = grpc.insecure_channel('localhost:9998') + grpc_service_stub = bisq_service.TradesStub(grpc_channel) + api_password: str = 'xyz' # getpass("Enter API password: ") + try: + response = grpc_service_stub.CloseTrade.with_call( + bisq_messages.CloseTradeRequest(trade_id='83e8b2e2-51b6-4f39-a748-3ebd29c22aea'), + metadata=[('password', api_password)]) + print('Response: ' + str(response)) + except grpc.RpcError as rpc_error: + print('gRPC API Exception: %s', rpc_error) + + +if __name__ == '__main__': + main() diff --git a/python-examples/rpccalls/confirm_payment_received.py b/python-examples/rpccalls/confirm_payment_received.py new file mode 100644 index 0000000..c6d1d59 --- /dev/null +++ b/python-examples/rpccalls/confirm_payment_received.py @@ -0,0 +1,24 @@ +from builtins import print + +import grpc + +# from getpass import getpass +import bisq.api.grpc_pb2 as bisq_messages +import bisq.api.grpc_pb2_grpc as bisq_service + + +def main(): + grpc_channel = grpc.insecure_channel('localhost:9998') + grpc_service_stub = bisq_service.TradesStub(grpc_channel) + api_password: str = 'xyz' # getpass("Enter API password: ") + try: + response = grpc_service_stub.ConfirmPaymentReceived.with_call( + bisq_messages.ConfirmPaymentReceivedRequest(trade_id='83e8b2e2-51b6-4f39-a748-3ebd29c22aea'), + metadata=[('password', api_password)]) + print('Response: ' + str(response)) + except grpc.RpcError as rpc_error: + print('gRPC API Exception: %s', rpc_error) + + +if __name__ == '__main__': + main() diff --git a/python-examples/rpccalls/confirm_payment_started.py b/python-examples/rpccalls/confirm_payment_started.py new file mode 100644 index 0000000..ea92dee --- /dev/null +++ b/python-examples/rpccalls/confirm_payment_started.py @@ -0,0 +1,24 @@ +from builtins import print + +import grpc + +# from getpass import getpass +import bisq.api.grpc_pb2 as bisq_messages +import bisq.api.grpc_pb2_grpc as bisq_service + + +def main(): + grpc_channel = grpc.insecure_channel('localhost:9998') + grpc_service_stub = bisq_service.TradesStub(grpc_channel) + api_password: str = 'xyz' # getpass("Enter API password: ") + try: + response = grpc_service_stub.ConfirmPaymentStarted.with_call( + bisq_messages.ConfirmPaymentStartedRequest(trade_id='83e8b2e2-51b6-4f39-a748-3ebd29c22aea'), + metadata=[('password', api_password)]) + print('Response: ' + str(response)) + except grpc.RpcError as rpc_error: + print('gRPC API Exception: %s', rpc_error) + + +if __name__ == '__main__': + main() diff --git a/python-examples/rpccalls/create_bsq_swap_offer.py b/python-examples/rpccalls/create_bsq_swap_offer.py new file mode 100644 index 0000000..d67057a --- /dev/null +++ b/python-examples/rpccalls/create_bsq_swap_offer.py @@ -0,0 +1,28 @@ +from builtins import print + +import grpc + +# from getpass import getpass +import bisq.api.grpc_pb2 as bisq_messages +import bisq.api.grpc_pb2_grpc as bisq_service + + +def main(): + grpc_channel = grpc.insecure_channel('localhost:9998') + grpc_service_stub = bisq_service.OffersStub(grpc_channel) + api_password: str = 'xyz' # getpass("Enter API password: ") + try: + response = grpc_service_stub.CreateBsqSwapOffer.with_call( + bisq_messages.CreateBsqSwapOfferRequest( + direction='SELL', # Buy BTC with BSQ + price='0.00005', # Price of 1 BSQ in BTC + amount=6250000, # Satoshis + min_amount=3125000), # Optional parameter cannot be 0 Satoshis. + metadata=[('password', api_password)]) + print('New BSQ swap offer: ' + str(response[0].bsq_swap_offer)) + except grpc.RpcError as rpc_error: + print('gRPC API Exception: %s', rpc_error) + + +if __name__ == '__main__': + main() diff --git a/python-examples/rpccalls/create_crypto_currency_payment_account.py b/python-examples/rpccalls/create_crypto_currency_payment_account.py new file mode 100644 index 0000000..848ebff --- /dev/null +++ b/python-examples/rpccalls/create_crypto_currency_payment_account.py @@ -0,0 +1,28 @@ +from builtins import print + +import grpc + +# from getpass import getpass +import bisq.api.grpc_pb2 as bisq_messages +import bisq.api.grpc_pb2_grpc as bisq_service + + +def main(): + grpc_channel = grpc.insecure_channel('localhost:9998') + grpc_service_stub = bisq_service.PaymentAccountsStub(grpc_channel) + api_password: str = 'xyz' # getpass("Enter API password: ") + try: + response = grpc_service_stub.CreateCryptoCurrencyPaymentAccount.with_call( + bisq_messages.CreateCryptoCurrencyPaymentAccountRequest( + account_name='name', + currency_code='XMR', + address='472CJ9TADoeVabhAe6byZQN4yqAFA4morKiyzb8DfLTj4hcQvsXNHxJUNYMw1JDmMALkQ3Bwmyn4aZYST7DzEw9nUeUTKVL', + trade_instant=False), + metadata=[('password', api_password)]) + print('Response: ' + str(response[0].payment_account)) + except grpc.RpcError as rpc_error: + print('gRPC API Exception: %s', rpc_error) + + +if __name__ == '__main__': + main() diff --git a/python-examples/rpccalls/create_offer.py b/python-examples/rpccalls/create_offer.py new file mode 100644 index 0000000..35061a1 --- /dev/null +++ b/python-examples/rpccalls/create_offer.py @@ -0,0 +1,75 @@ +# from getpass import getpass +import time +from builtins import print + +import grpc + +import bisq.api.grpc_pb2 as bisq_messages +import bisq.api.grpc_pb2_grpc as bisq_service + + +def main(): + grpc_channel = grpc.insecure_channel('localhost:9998') + grpc_service_stub = bisq_service.OffersStub(grpc_channel) + api_password: str = 'xyz' # getpass("Enter API password: ") + try: + create_offer_request = fixed_price_usd_request() + create_offer_response = grpc_service_stub.CreateOffer.with_call(create_offer_request, + metadata=[('password', api_password)]) + print('New fixed-price offer: ' + str(create_offer_response[0].offer)) + time.sleep(3) # Wait for new offer preparation and wallet updates before creating another offer. + + create_offer_request = market_based_price_usd_request() + create_offer_response = grpc_service_stub.CreateOffer.with_call(create_offer_request, + metadata=[('password', api_password)]) + print('New mkt price margin based offer: ' + str(create_offer_response[0].offer)) + time.sleep(3) # Wait for new offer preparation and wallet updates before creating another offer. + + create_offer_request = fixed_price_xmr_request() + create_offer_response = grpc_service_stub.CreateOffer.with_call(create_offer_request, + metadata=[('password', api_password)]) + print('New XMR offer: ' + str(create_offer_response[0].offer)) + + except grpc.RpcError as rpc_error: + print('gRPC API Exception: %s', rpc_error) + + +def fixed_price_usd_request(): + # Create an offer to buy BTC with USD at a fixed price. + return bisq_messages.CreateOfferRequest(direction='BUY', + currency_code='USD', + price='40000.00', + amount=12500000, + min_amount=6250000, + buyer_security_deposit_pct=20.00, + payment_account_id='af852e11-f2db-48bd-82f5-123047b41f0c', + maker_fee_currency_code='BSQ') + + +def market_based_price_usd_request(): + # Create an offer to sell BTC for USD at a moving, market price margin. + return bisq_messages.CreateOfferRequest(direction='SELL', + currency_code='USD', + use_market_based_price=True, + market_price_margin_pct=2.50, + amount=12500000, + min_amount=6250000, + buyer_security_deposit_pct=20.00, + payment_account_id='af852e11-f2db-48bd-82f5-123047b41f0c', + maker_fee_currency_code='BSQ') + + +def fixed_price_xmr_request(): + # Create an offer to buy BTC with XMR. + return bisq_messages.CreateOfferRequest(direction='BUY', # Buy BTC with XMR + currency_code='XMR', + price='0.005', # Price of 1 XMR in BTC + amount=12500000, + min_amount=6250000, + buyer_security_deposit_pct=20.00, + payment_account_id='7d52d9b6-e943-4625-a063-f53b09381bf2', + maker_fee_currency_code='BTC') + + +if __name__ == '__main__': + main() diff --git a/python-examples/rpccalls/create_payment_account.py b/python-examples/rpccalls/create_payment_account.py new file mode 100644 index 0000000..ca630b0 --- /dev/null +++ b/python-examples/rpccalls/create_payment_account.py @@ -0,0 +1,27 @@ +from builtins import print + +import grpc + +# from getpass import getpass +import bisq.api.grpc_pb2 as bisq_messages +import bisq.api.grpc_pb2_grpc as bisq_service + + +def main(): + grpc_channel = grpc.insecure_channel('localhost:9998') + grpc_service_stub = bisq_service.PaymentAccountsStub(grpc_channel) + api_password: str = 'xyz' # getpass("Enter API password: ") + try: + # Convert a .json form to a string and send it in the request. + with open('/tmp/sepa-alice.json', 'r') as file: + json = file.read() + response = grpc_service_stub.CreatePaymentAccount.with_call( + bisq_messages.CreatePaymentAccountRequest(payment_account_form=json), + metadata=[('password', api_password)]) + print('Response: ' + str(response[0].payment_account)) + except grpc.RpcError as rpc_error: + print('gRPC API Exception: %s', rpc_error) + + +if __name__ == '__main__': + main() diff --git a/python-examples/rpccalls/edit_offer.py b/python-examples/rpccalls/edit_offer.py new file mode 100644 index 0000000..8957d29 --- /dev/null +++ b/python-examples/rpccalls/edit_offer.py @@ -0,0 +1,107 @@ +# from getpass import getpass +import time +from builtins import print + +import grpc + +import bisq.api.grpc_pb2 as bisq_messages +import bisq.api.grpc_pb2_grpc as bisq_service + +EDITED_USD_OFFER_ID = '44736-16df6819-d98b-4f13-87dd-50087c464fac-184' + + +def main(): + grpc_channel = grpc.insecure_channel('localhost:9998') + grpc_service_stub = bisq_service.OffersStub(grpc_channel) + api_password: str = 'xyz' # getpass("Enter API password: ") + try: + edit_offer_request = disable_offer_request() + edit_offer_response = grpc_service_stub.EditOffer.with_call(edit_offer_request, + metadata=[('password', api_password)]) + print('Offer is disabled. Rpc response: ' + str(edit_offer_response)) + time.sleep(4) # Wait for new offer preparation and wallet updates before creating another offer. + + edit_offer_request = enable_offer_request() + edit_offer_response = grpc_service_stub.EditOffer.with_call(edit_offer_request, + metadata=[('password', api_password)]) + print('Offer is enabled. Rpc response: ' + str(edit_offer_response)) + time.sleep(4) # Wait for new offer preparation and wallet updates before creating another offer. + + edit_offer_request = edit_fixed_price_request() + edit_offer_response = grpc_service_stub.EditOffer.with_call(edit_offer_request, + metadata=[('password', api_password)]) + print('Offer fixed-price has been changed. Rpc response: ' + str(edit_offer_response)) + time.sleep(4) # Wait for new offer preparation and wallet updates before creating another offer. + + edit_offer_request = edit_fixed_price_and_enable_request() + edit_offer_response = grpc_service_stub.EditOffer.with_call(edit_offer_request, + metadata=[('password', api_password)]) + print('Offer fixed-price has been changed, and offer is enabled. Rpc response: ' + str(edit_offer_response)) + time.sleep(4) # Wait for new offer preparation and wallet updates before creating another offer. + + # Change the fixed-price offer to a mkt price margin based offer + edit_offer_request = edit_price_margin_request() + edit_offer_response = grpc_service_stub.EditOffer.with_call(edit_offer_request, + metadata=[('password', api_password)]) + print('Fixed-price offer is not a mkt price margin based offer. Rpc response: ' + str(edit_offer_response)) + time.sleep(4) # Wait for new offer preparation and wallet updates before creating another offer. + + # Set the trigger-price on a mkt price margin based offer + edit_offer_request = edit_trigger_price_request() + edit_offer_response = grpc_service_stub.EditOffer.with_call(edit_offer_request, + metadata=[('password', api_password)]) + print('Offer trigger price is set. Rpc response: ' + str(edit_offer_response)) + + except grpc.RpcError as rpc_error: + print('gRPC API Exception: %s', rpc_error) + + +def disable_offer_request(): + return bisq_messages.EditOfferRequest( + id=EDITED_USD_OFFER_ID, + edit_type=bisq_messages.EditOfferRequest.EditType.ACTIVATION_STATE_ONLY, + enable=0) # If enable=-1: ignore enable param, enable=0: disable offer, enable=1: enable offer + + +def enable_offer_request(): + return bisq_messages.EditOfferRequest( + id=EDITED_USD_OFFER_ID, + edit_type=bisq_messages.EditOfferRequest.EditType.ACTIVATION_STATE_ONLY, + enable=1) # If enable=-1: ignore enable param, enable=0: disable offer, enable=1: enable offer + + +def edit_fixed_price_request(): + return bisq_messages.EditOfferRequest( + id=EDITED_USD_OFFER_ID, + price='42000.50', + edit_type=bisq_messages.EditOfferRequest.EditType.FIXED_PRICE_ONLY, + enable=-1) # If enable=-1: ignore enable param, enable=0: disable offer, enable=1: enable offer + + +def edit_fixed_price_and_enable_request(): + return bisq_messages.EditOfferRequest( + id=EDITED_USD_OFFER_ID, + price='43000.50', + edit_type=bisq_messages.EditOfferRequest.EditType.FIXED_PRICE_AND_ACTIVATION_STATE, + enable=1) # If enable=-1: ignore enable param, enable=0: disable offer, enable=1: enable offer + + +def edit_price_margin_request(): + return bisq_messages.EditOfferRequest( + id=EDITED_USD_OFFER_ID, + use_market_based_price=True, + market_price_margin_pct=5.00, + edit_type=bisq_messages.EditOfferRequest.EditType.MKT_PRICE_MARGIN_ONLY, + enable=-1) # If enable=-1: ignore enable param, enable=0: disable offer, enable=1: enable offer + + +def edit_trigger_price_request(): + return bisq_messages.EditOfferRequest( + id=EDITED_USD_OFFER_ID, + trigger_price='40000.0000', + edit_type=bisq_messages.EditOfferRequest.EditType.TRIGGER_PRICE_ONLY, + enable=-1) # If enable=-1: ignore enable param, enable=0: disable offer, enable=1: enable offer + + +if __name__ == '__main__': + main() diff --git a/python-examples/rpccalls/fail_trade.py b/python-examples/rpccalls/fail_trade.py new file mode 100644 index 0000000..f7bea43 --- /dev/null +++ b/python-examples/rpccalls/fail_trade.py @@ -0,0 +1,24 @@ +from builtins import print + +import grpc + +# from getpass import getpass +import bisq.api.grpc_pb2 as bisq_messages +import bisq.api.grpc_pb2_grpc as bisq_service + + +def main(): + grpc_channel = grpc.insecure_channel('localhost:9998') + grpc_service_stub = bisq_service.TradesStub(grpc_channel) + api_password: str = 'xyz' # getpass("Enter API password: ") + try: + response = grpc_service_stub.FailTrade.with_call( + bisq_messages.FailTradeRequest(trade_id='HDCXKUR-1cfb39e9-68b9-4772-8ae0-abceb8339c90-184'), + metadata=[('password', api_password)]) + print('Response: ' + str(response)) + except grpc.RpcError as rpc_error: + print('gRPC API Exception: %s', rpc_error) + + +if __name__ == '__main__': + main() diff --git a/python-examples/rpccalls/get_address_balance.py b/python-examples/rpccalls/get_address_balance.py new file mode 100644 index 0000000..a4e5673 --- /dev/null +++ b/python-examples/rpccalls/get_address_balance.py @@ -0,0 +1,29 @@ +from builtins import print + +import grpc + +# from getpass import getpass +import bisq.api.grpc_pb2 as bisq_messages +import bisq.api.grpc_pb2_grpc as bisq_service + + +def main(): + grpc_channel = grpc.insecure_channel('localhost:9998') + grpc_service_stub = bisq_service.WalletsStub(grpc_channel) + api_password: str = 'xyz' # getpass("Enter API password: ") + try: + response = grpc_service_stub.GetAddressBalance.with_call( + bisq_messages.GetAddressBalanceRequest(address='mwLYmweQf2dCgAqQCb3qU2UbxBycVBi2PW'), + metadata=[('password', api_password)]) + address_balance_info = response[0].address_balance_info + print('Address = {0}\nAvailable Balance = {1} sats\nUnused? {2}\nNum confirmations of most recent tx = {3}' + .format(address_balance_info.address, + address_balance_info.balance, + address_balance_info.is_address_unused, + address_balance_info.num_confirmations)) + except grpc.RpcError as rpc_error: + print('gRPC API Exception: %s', rpc_error) + + +if __name__ == '__main__': + main() diff --git a/python-examples/rpccalls/get_balances.py b/python-examples/rpccalls/get_balances.py new file mode 100644 index 0000000..fce181d --- /dev/null +++ b/python-examples/rpccalls/get_balances.py @@ -0,0 +1,25 @@ +from builtins import print + +import grpc + +# from getpass import getpass +import bisq.api.grpc_pb2 as bisq_messages +import bisq.api.grpc_pb2_grpc as bisq_service + + +def main(): + grpc_channel = grpc.insecure_channel('localhost:9998') + grpc_service_stub = bisq_service.WalletsStub(grpc_channel) + api_password: str = 'xyz' # getpass("Enter API password: ") + try: + response = grpc_service_stub.GetBalances.with_call( + bisq_messages.GetBalancesRequest(), + metadata=[('password', api_password)]) + print('BTC Balances: ' + str(response[0].balances.bsq)) + print('BSQ Balances: ' + str(response[0].balances.btc)) + except grpc.RpcError as rpc_error: + print('gRPC API Exception: %s', rpc_error) + + +if __name__ == '__main__': + main() diff --git a/python-examples/rpccalls/get_bsq_swap_offer.py b/python-examples/rpccalls/get_bsq_swap_offer.py new file mode 100644 index 0000000..131ca5f --- /dev/null +++ b/python-examples/rpccalls/get_bsq_swap_offer.py @@ -0,0 +1,24 @@ +from builtins import print + +import grpc + +# from getpass import getpass +import bisq.api.grpc_pb2 as bisq_messages +import bisq.api.grpc_pb2_grpc as bisq_service + + +def main(): + grpc_channel = grpc.insecure_channel('localhost:9998') + grpc_service_stub = bisq_service.OffersStub(grpc_channel) + api_password: str = 'xyz' # getpass("Enter API password: ") + try: + response = grpc_service_stub.GetBsqSwapOffer.with_call( + bisq_messages.GetOfferRequest(id='VZLGFPV-e8dd2f8c-fc90-4509-8f30-e0bb95815b46-184'), + metadata=[('password', api_password)]) + print('Response: ' + str(response[0].bsq_swap_offer)) + except grpc.RpcError as rpc_error: + print('gRPC API Exception: %s', rpc_error) + + +if __name__ == '__main__': + main() diff --git a/python-examples/rpccalls/get_bsq_swap_offers.py b/python-examples/rpccalls/get_bsq_swap_offers.py new file mode 100644 index 0000000..f3067ca --- /dev/null +++ b/python-examples/rpccalls/get_bsq_swap_offers.py @@ -0,0 +1,24 @@ +from builtins import print + +import grpc + +# from getpass import getpass +import bisq.api.grpc_pb2 as bisq_messages +import bisq.api.grpc_pb2_grpc as bisq_service + + +def main(): + grpc_channel = grpc.insecure_channel('localhost:9998') + grpc_service_stub = bisq_service.OffersStub(grpc_channel) + api_password: str = 'xyz' # getpass("Enter API password: ") + try: + response = grpc_service_stub.GetBsqSwapOffers.with_call( + bisq_messages.GetBsqSwapOffersRequest(direction='SELL'), + metadata=[('password', api_password)]) + print('Response: ' + str(response[0].bsq_swap_offers)) + except grpc.RpcError as rpc_error: + print('gRPC API Exception: %s', rpc_error) + + +if __name__ == '__main__': + main() diff --git a/python-examples/rpccalls/get_crypto_currency_payment_methods.py b/python-examples/rpccalls/get_crypto_currency_payment_methods.py new file mode 100644 index 0000000..dcc7b95 --- /dev/null +++ b/python-examples/rpccalls/get_crypto_currency_payment_methods.py @@ -0,0 +1,27 @@ +from builtins import print + +import grpc + +# from getpass import getpass +import bisq.api.grpc_pb2 as bisq_messages +import bisq.api.grpc_pb2_grpc as bisq_service + + +def main(): + grpc_channel = grpc.insecure_channel('localhost:9998') + grpc_service_stub = bisq_service.PaymentAccountsStub(grpc_channel) + api_password: str = 'xyz' # getpass("Enter API password: ") + try: + response = grpc_service_stub.GetCryptoCurrencyPaymentMethods.with_call( + bisq_messages.GetCryptoCurrencyPaymentMethodsRequest(), + metadata=[('password', api_password)]) + payment_methods = list(response[0].payment_methods) + print('Response contains {0} payment methods: '.format(len(payment_methods))) + for payment_method in payment_methods: + print('\t{0}'.format(payment_method.id)) + except grpc.RpcError as rpc_error: + print('gRPC API Exception: %s', rpc_error) + + +if __name__ == '__main__': + main() diff --git a/python-examples/rpccalls/get_funding_addresses.py b/python-examples/rpccalls/get_funding_addresses.py new file mode 100644 index 0000000..ebdc8e1 --- /dev/null +++ b/python-examples/rpccalls/get_funding_addresses.py @@ -0,0 +1,31 @@ +from builtins import print + +import grpc + +# from getpass import getpass +import bisq.api.grpc_pb2 as bisq_messages +import bisq.api.grpc_pb2_grpc as bisq_service + + +def main(): + grpc_channel = grpc.insecure_channel('localhost:9998') + grpc_service_stub = bisq_service.WalletsStub(grpc_channel) + api_password: str = 'xyz' # getpass("Enter API password: ") + try: + response = grpc_service_stub.GetFundingAddresses.with_call( + bisq_messages.GetFundingAddressesRequest(), + metadata=[('password', api_password)]) + funding_addresses = list(response[0].address_balance_info) + print('Response contains {0} funding addresses:'.format(len(funding_addresses))) + for address_balance_info in funding_addresses: + print('Address = {0} Available Balance = {1} sats Unused? {2} Num confirmations of most recent tx = {3}' + .format(address_balance_info.address, + address_balance_info.balance, + address_balance_info.is_address_unused, + address_balance_info.num_confirmations)) + except grpc.RpcError as rpc_error: + print('gRPC API Exception: %s', rpc_error) + + +if __name__ == '__main__': + main() diff --git a/python-examples/rpccalls/get_market_price.py b/python-examples/rpccalls/get_market_price.py new file mode 100644 index 0000000..6cd87ba --- /dev/null +++ b/python-examples/rpccalls/get_market_price.py @@ -0,0 +1,24 @@ +from builtins import print + +import grpc + +# from getpass import getpass +import bisq.api.grpc_pb2 as bisq_messages +import bisq.api.grpc_pb2_grpc as bisq_service + + +def main(): + grpc_channel = grpc.insecure_channel('localhost:9998') + grpc_service_stub = bisq_service.PriceStub(grpc_channel) + api_password: str = 'xyz' # getpass("Enter API password: ") + try: + response = grpc_service_stub.GetMarketPrice.with_call( + bisq_messages.MarketPriceRequest(currency_code='USD'), + metadata=[('password', api_password)]) + print('Response: ' + str(response)) + except grpc.RpcError as rpc_error: + print('gRPC API Exception: %s', rpc_error) + + +if __name__ == '__main__': + main() diff --git a/python-examples/rpccalls/get_method_help.py b/python-examples/rpccalls/get_method_help.py new file mode 100644 index 0000000..623c2cf --- /dev/null +++ b/python-examples/rpccalls/get_method_help.py @@ -0,0 +1 @@ +# Help Service is for CLI users. diff --git a/python-examples/rpccalls/get_my_bsq_swap_offer.py b/python-examples/rpccalls/get_my_bsq_swap_offer.py new file mode 100644 index 0000000..10a6160 --- /dev/null +++ b/python-examples/rpccalls/get_my_bsq_swap_offer.py @@ -0,0 +1,24 @@ +from builtins import print + +import grpc + +# from getpass import getpass +import bisq.api.grpc_pb2 as bisq_messages +import bisq.api.grpc_pb2_grpc as bisq_service + + +def main(): + grpc_channel = grpc.insecure_channel('localhost:9998') + grpc_service_stub = bisq_service.OffersStub(grpc_channel) + api_password: str = 'xyz' # getpass("Enter API password: ") + try: + response = grpc_service_stub.GetMyBsqSwapOffer.with_call( + bisq_messages.GetMyOfferRequest(id='gzcvxum-63c23b0b-6acd-49ba-a956-55e406994da1-184'), + metadata=[('password', api_password)]) + print('Response: ' + str(response[0].bsq_swap_offer)) + except grpc.RpcError as rpc_error: + print('gRPC API Exception: %s', rpc_error) + + +if __name__ == '__main__': + main() diff --git a/python-examples/rpccalls/get_my_bsq_swap_offers.py b/python-examples/rpccalls/get_my_bsq_swap_offers.py new file mode 100644 index 0000000..876f3a2 --- /dev/null +++ b/python-examples/rpccalls/get_my_bsq_swap_offers.py @@ -0,0 +1,25 @@ +from builtins import print + +import grpc + +# from getpass import getpass +import bisq.api.grpc_pb2 as bisq_messages +import bisq.api.grpc_pb2_grpc as bisq_service + + +def main(): + grpc_channel = grpc.insecure_channel('localhost:9998') + grpc_service_stub = bisq_service.OffersStub(grpc_channel) + api_password: str = 'xyz' # getpass("Enter API password: ") + try: + response = grpc_service_stub.GetMyBsqSwapOffers.with_call( + bisq_messages.GetBsqSwapOffersRequest(direction='BUY'), # My buy BTC for BSQ swap offers + metadata=[('password', api_password)]) + offers = list(response[0].bsq_swap_offers) + print('Response: ' + str(offers)) + except grpc.RpcError as rpc_error: + print('gRPC API Exception: %s', rpc_error) + + +if __name__ == '__main__': + main() diff --git a/python-examples/rpccalls/get_my_offer.py b/python-examples/rpccalls/get_my_offer.py new file mode 100644 index 0000000..884bc2c --- /dev/null +++ b/python-examples/rpccalls/get_my_offer.py @@ -0,0 +1,24 @@ +from builtins import print + +import grpc + +# from getpass import getpass +import bisq.api.grpc_pb2 as bisq_messages +import bisq.api.grpc_pb2_grpc as bisq_service + + +def main(): + grpc_channel = grpc.insecure_channel('localhost:9998') + grpc_service_stub = bisq_service.OffersStub(grpc_channel) + api_password: str = 'xyz' # getpass("Enter API password: ") + try: + response = grpc_service_stub.GetMyOffer.with_call( + bisq_messages.GetMyOfferRequest(id='QusccrDV-47ae5521-bda1-4f3c-801b-5c193f957df7-184'), + metadata=[('password', api_password)]) + print('Response: ' + str(response[0].offer)) + except grpc.RpcError as rpc_error: + print('gRPC API Exception: %s', rpc_error) + + +if __name__ == '__main__': + main() diff --git a/python-examples/rpccalls/get_my_offers.py b/python-examples/rpccalls/get_my_offers.py new file mode 100644 index 0000000..f5f00f2 --- /dev/null +++ b/python-examples/rpccalls/get_my_offers.py @@ -0,0 +1,27 @@ +from builtins import print + +import grpc + +# from getpass import getpass +import bisq.api.grpc_pb2 as bisq_messages +import bisq.api.grpc_pb2_grpc as bisq_service + + +def main(): + grpc_channel = grpc.insecure_channel('localhost:9998') + grpc_service_stub = bisq_service.OffersStub(grpc_channel) + api_password: str = 'xyz' # getpass("Enter API password: ") + try: + response = grpc_service_stub.GetMyOffers.with_call( + bisq_messages.GetMyOffersRequest( + direction='BUY', + currency_code='USD'), + metadata=[('password', api_password)]) + offers = list(response[0].offers) + print('Response: ' + str(offers)) + except grpc.RpcError as rpc_error: + print('gRPC API Exception: %s', rpc_error) + + +if __name__ == '__main__': + main() diff --git a/python-examples/rpccalls/get_offer.py b/python-examples/rpccalls/get_offer.py new file mode 100644 index 0000000..df0a243 --- /dev/null +++ b/python-examples/rpccalls/get_offer.py @@ -0,0 +1,24 @@ +from builtins import print + +import grpc + +# from getpass import getpass +import bisq.api.grpc_pb2 as bisq_messages +import bisq.api.grpc_pb2_grpc as bisq_service + + +def main(): + grpc_channel = grpc.insecure_channel('localhost:9998') + grpc_service_stub = bisq_service.OffersStub(grpc_channel) + api_password: str = 'xyz' # getpass("Enter API password: ") + try: + response = grpc_service_stub.GetOffer.with_call( + bisq_messages.GetOfferRequest(id='VZLGFPV-e8dd2f8c-fc90-4509-8f30-e0bb95815b46-184'), + metadata=[('password', api_password)]) + print('Response: ' + str(response[0].offer)) + except grpc.RpcError as rpc_error: + print('gRPC API Exception: %s', rpc_error) + + +if __name__ == '__main__': + main() diff --git a/python-examples/rpccalls/get_offer_category.py b/python-examples/rpccalls/get_offer_category.py new file mode 100644 index 0000000..cf4fd5e --- /dev/null +++ b/python-examples/rpccalls/get_offer_category.py @@ -0,0 +1,26 @@ +# from getpass import getpass +from builtins import print + +import grpc + +import bisq.api.grpc_pb2 as bisq_messages +import bisq.api.grpc_pb2_grpc as bisq_service + + +def main(): + grpc_channel = grpc.insecure_channel('localhost:9998') + grpc_service_stub = bisq_service.OffersStub(grpc_channel) + api_password: str = 'xyz' # getpass("Enter API password: ") + try: + response = grpc_service_stub.GetOfferCategory.with_call( + bisq_messages.GetOfferCategoryRequest( + id='VZLGFPV-e8dd2f8c-fc90-4509-8f30-e0bb95815b46-184', + is_my_offer=False), + metadata=[('password', api_password)]) + print('Response: ' + str(response[0].offer_category)) + except grpc.RpcError as rpc_error: + print('gRPC API Exception: %s', rpc_error) + + +if __name__ == '__main__': + main() diff --git a/python-examples/rpccalls/get_offers.py b/python-examples/rpccalls/get_offers.py new file mode 100644 index 0000000..5853f35 --- /dev/null +++ b/python-examples/rpccalls/get_offers.py @@ -0,0 +1,27 @@ +from builtins import print + +import grpc + +# from getpass import getpass +import bisq.api.grpc_pb2 as bisq_messages +import bisq.api.grpc_pb2_grpc as bisq_service + + +def main(): + grpc_channel = grpc.insecure_channel('localhost:9998') + grpc_service_stub = bisq_service.OffersStub(grpc_channel) + api_password: str = 'xyz' # getpass("Enter API password: ") + try: + response = grpc_service_stub.GetOffers.with_call( + bisq_messages.GetOffersRequest( + direction='SELL', + currency_code='USD'), + metadata=[('password', api_password)]) + offers = list(response[0].offers) + print('Response: ' + str(offers)) + except grpc.RpcError as rpc_error: + print('gRPC API Exception: %s', rpc_error) + + +if __name__ == '__main__': + main() diff --git a/python-examples/rpccalls/get_payment_account_form.py b/python-examples/rpccalls/get_payment_account_form.py new file mode 100644 index 0000000..23d848c --- /dev/null +++ b/python-examples/rpccalls/get_payment_account_form.py @@ -0,0 +1,27 @@ +from builtins import print + +import grpc + +# from getpass import getpass +import bisq.api.grpc_pb2 as bisq_messages +import bisq.api.grpc_pb2_grpc as bisq_service + + +def main(): + grpc_channel = grpc.insecure_channel('localhost:9998') + grpc_service_stub = bisq_service.PaymentAccountsStub(grpc_channel) + api_password: str = 'xyz' # getpass("Enter API password: ") + try: + response = grpc_service_stub.GetPaymentAccountForm.with_call( + bisq_messages.GetPaymentAccountFormRequest(payment_method_id='SWIFT'), + metadata=[('password', api_password)]) + json_string = response[0].payment_account_form_json + print('Response: ' + json_string) + # The client should save this json string to file, manually fill in the form + # fields, then use it as shown in the create_payment_account.py example. + except grpc.RpcError as rpc_error: + print('gRPC API Exception: %s', rpc_error) + + +if __name__ == '__main__': + main() diff --git a/python-examples/rpccalls/get_payment_accounts.py b/python-examples/rpccalls/get_payment_accounts.py new file mode 100644 index 0000000..21fd75b --- /dev/null +++ b/python-examples/rpccalls/get_payment_accounts.py @@ -0,0 +1,32 @@ +from builtins import print + +import grpc + +# from getpass import getpass +import bisq.api.grpc_pb2 as bisq_messages +import bisq.api.grpc_pb2_grpc as bisq_service + + +def main(): + grpc_channel = grpc.insecure_channel('localhost:9998') + grpc_service_stub = bisq_service.PaymentAccountsStub(grpc_channel) + api_password: str = 'xyz' # getpass("Enter API password: ") + try: + response = grpc_service_stub.GetPaymentAccounts.with_call( + bisq_messages.GetPaymentAccountsRequest(), + metadata=[('password', api_password)]) + payment_accounts = list(response[0].payment_accounts) + print('Response: {0} payment accounts'.format(len(payment_accounts))) + print('\t\t{0:<40} {1:<24} {2:<20} {3:<8}'.format('ID', 'Name', 'Payment Method', 'Trade Currency')) + for payment_account in payment_accounts: + print('\t\t{0:<40} {1:<24} {2:<20} {3:<8}' + .format(payment_account.id, + payment_account.account_name, + payment_account.payment_method.id, + payment_account.selected_trade_currency.code)) + except grpc.RpcError as rpc_error: + print('gRPC API Exception: %s', rpc_error) + + +if __name__ == '__main__': + main() diff --git a/python-examples/rpccalls/get_payment_methods.py b/python-examples/rpccalls/get_payment_methods.py new file mode 100644 index 0000000..8a6dc36 --- /dev/null +++ b/python-examples/rpccalls/get_payment_methods.py @@ -0,0 +1,27 @@ +from builtins import print + +import grpc + +# from getpass import getpass +import bisq.api.grpc_pb2 as bisq_messages +import bisq.api.grpc_pb2_grpc as bisq_service + + +def main(): + grpc_channel = grpc.insecure_channel('localhost:9998') + grpc_service_stub = bisq_service.PaymentAccountsStub(grpc_channel) + api_password: str = 'xyz' # getpass("Enter API password: ") + try: + response = grpc_service_stub.GetPaymentMethods.with_call( + bisq_messages.GetPaymentMethodsRequest(), + metadata=[('password', api_password)]) + payment_methods = list(response[0].payment_methods) + print('Response: {0} payment methods'.format(len(payment_methods))) + for payment_method in payment_methods: + print('\t\t{0}'.format(payment_method.id)) + except grpc.RpcError as rpc_error: + print('gRPC API Exception: %s', rpc_error) + + +if __name__ == '__main__': + main() diff --git a/python-examples/rpccalls/get_trade.py b/python-examples/rpccalls/get_trade.py new file mode 100644 index 0000000..e594013 --- /dev/null +++ b/python-examples/rpccalls/get_trade.py @@ -0,0 +1,24 @@ +from builtins import print + +import grpc + +# from getpass import getpass +import bisq.api.grpc_pb2 as bisq_messages +import bisq.api.grpc_pb2_grpc as bisq_service + + +def main(): + grpc_channel = grpc.insecure_channel('localhost:9998') + grpc_service_stub = bisq_service.TradesStub(grpc_channel) + api_password: str = 'xyz' # getpass("Enter API password: ") + try: + response = grpc_service_stub.GetTrade.with_call( + bisq_messages.GetTradeRequest(trade_id='87533-abc12dcd-b12f-499d-8594-e6ee39630d50-184'), + metadata=[('password', api_password)]) + print('Response: ' + str(response[0].trade)) + except grpc.RpcError as rpc_error: + print('gRPC API Exception: %s', rpc_error) + + +if __name__ == '__main__': + main() diff --git a/python-examples/rpccalls/get_trades.py b/python-examples/rpccalls/get_trades.py new file mode 100644 index 0000000..b63ca87 --- /dev/null +++ b/python-examples/rpccalls/get_trades.py @@ -0,0 +1,26 @@ +from builtins import print + +import grpc + +# from getpass import getpass +import bisq.api.grpc_pb2 as bisq_messages +import bisq.api.grpc_pb2_grpc as bisq_service + + +def main(): + grpc_channel = grpc.insecure_channel('localhost:9998') + grpc_service_stub = bisq_service.TradesStub(grpc_channel) + api_password: str = 'xyz' # getpass("Enter API password: ") + try: + response = grpc_service_stub.GetTrades.with_call( + bisq_messages.GetTradesRequest( + category=bisq_messages.GetTradesRequest.Category.OPEN), + metadata=[('password', api_password)]) + trades = list(response[0].trades) + print('Response: ' + str(trades)) + except grpc.RpcError as rpc_error: + print('gRPC API Exception: %s', rpc_error) + + +if __name__ == '__main__': + main() diff --git a/python-examples/rpccalls/get_transaction.py b/python-examples/rpccalls/get_transaction.py new file mode 100644 index 0000000..2f790ce --- /dev/null +++ b/python-examples/rpccalls/get_transaction.py @@ -0,0 +1,25 @@ +from builtins import print + +import grpc + +# from getpass import getpass +import bisq.api.grpc_pb2 as bisq_messages +import bisq.api.grpc_pb2_grpc as bisq_service + + +def main(): + grpc_channel = grpc.insecure_channel('localhost:9998') + grpc_service_stub = bisq_service.WalletsStub(grpc_channel) + api_password: str = 'xyz' # getpass("Enter API password: ") + try: + response = grpc_service_stub.GetTransaction.with_call( + bisq_messages.GetTransactionRequest( + tx_id='907cf2b9ec2970653a9d7b5384b729037bfdf213d3fa38797704a7adb4c7217e'), + metadata=[('password', api_password)]) + print('Response: ' + str(response[0].tx_info)) + except grpc.RpcError as rpc_error: + print('gRPC API Exception: %s', rpc_error) + + +if __name__ == '__main__': + main() diff --git a/python-examples/rpccalls/get_tx_fee_rate.py b/python-examples/rpccalls/get_tx_fee_rate.py new file mode 100644 index 0000000..101e5b3 --- /dev/null +++ b/python-examples/rpccalls/get_tx_fee_rate.py @@ -0,0 +1,24 @@ +from builtins import print + +import grpc + +# from getpass import getpass +import bisq.api.grpc_pb2 as bisq_messages +import bisq.api.grpc_pb2_grpc as bisq_service + + +def main(): + grpc_channel = grpc.insecure_channel('localhost:9998') + grpc_service_stub = bisq_service.WalletsStub(grpc_channel) + api_password: str = 'xyz' # getpass("Enter API password: ") + try: + response = grpc_service_stub.GetTxFeeRate.with_call( + bisq_messages.GetTxFeeRateRequest(), + metadata=[('password', api_password)]) + print('Response: ' + str(response[0].tx_fee_rate_info)) + except grpc.RpcError as rpc_error: + print('gRPC API Exception: %s', rpc_error) + + +if __name__ == '__main__': + main() diff --git a/python-examples/rpccalls/get_unused_bsq_address.py b/python-examples/rpccalls/get_unused_bsq_address.py new file mode 100644 index 0000000..c36a49e --- /dev/null +++ b/python-examples/rpccalls/get_unused_bsq_address.py @@ -0,0 +1,24 @@ +from builtins import print + +import grpc + +# from getpass import getpass +import bisq.api.grpc_pb2 as bisq_messages +import bisq.api.grpc_pb2_grpc as bisq_service + + +def main(): + grpc_channel = grpc.insecure_channel('localhost:9998') + grpc_service_stub = bisq_service.WalletsStub(grpc_channel) + api_password: str = 'xyz' # getpass("Enter API password: ") + try: + response = grpc_service_stub.GetUnusedBsqAddress.with_call( + bisq_messages.GetUnusedBsqAddressRequest(), + metadata=[('password', api_password)]) + print('Response: ' + str(response[0].address)) + except grpc.RpcError as rpc_error: + print('gRPC API Exception: %s', rpc_error) + + +if __name__ == '__main__': + main() diff --git a/python-examples/rpccalls/get_version.py b/python-examples/rpccalls/get_version.py new file mode 100644 index 0000000..314d985 --- /dev/null +++ b/python-examples/rpccalls/get_version.py @@ -0,0 +1,24 @@ +from builtins import print + +import grpc + +# from getpass import getpass +import bisq.api.grpc_pb2 as bisq_messages +import bisq.api.grpc_pb2_grpc as bisq_service + + +def main(): + grpc_channel = grpc.insecure_channel('localhost:9998') + grpc_service_stub = bisq_service.GetVersionStub(grpc_channel) + api_password: str = 'xyz' # getpass("Enter API password: ") + try: + response = grpc_service_stub.GetVersion.with_call( + bisq_messages.GetVersionRequest(), + metadata=[('password', api_password)]) + print('Response: ' + response[0].version) + except grpc.RpcError as rpc_error: + print('gRPC API Exception: %s', rpc_error) + + +if __name__ == '__main__': + main() diff --git a/python-examples/rpccalls/lock_wallet.py b/python-examples/rpccalls/lock_wallet.py new file mode 100644 index 0000000..8580b7c --- /dev/null +++ b/python-examples/rpccalls/lock_wallet.py @@ -0,0 +1,24 @@ +from builtins import print + +import grpc + +# from getpass import getpass +import bisq.api.grpc_pb2 as bisq_messages +import bisq.api.grpc_pb2_grpc as bisq_service + + +def main(): + grpc_channel = grpc.insecure_channel('localhost:9998') + grpc_service_stub = bisq_service.WalletsStub(grpc_channel) + api_password: str = 'xyz' # getpass("Enter API password: ") + try: + response = grpc_service_stub.LockWallet.with_call( + bisq_messages.LockWalletRequest(), + metadata=[('password', api_password)]) + print('Response: ' + str(response)) + except grpc.RpcError as rpc_error: + print('gRPC API Exception: %s', rpc_error) + + +if __name__ == '__main__': + main() diff --git a/python-examples/rpccalls/remove_wallet_password.py b/python-examples/rpccalls/remove_wallet_password.py new file mode 100644 index 0000000..db7428e --- /dev/null +++ b/python-examples/rpccalls/remove_wallet_password.py @@ -0,0 +1,24 @@ +from builtins import print + +import grpc + +# from getpass import getpass +import bisq.api.grpc_pb2 as bisq_messages +import bisq.api.grpc_pb2_grpc as bisq_service + + +def main(): + grpc_channel = grpc.insecure_channel('localhost:9998') + grpc_service_stub = bisq_service.WalletsStub(grpc_channel) + api_password: str = 'xyz' # getpass("Enter API password: ") + try: + response = grpc_service_stub.RemoveWalletPassword.with_call( + bisq_messages.RemoveWalletPasswordRequest(password='abc'), + metadata=[('password', api_password)]) + print('Response: ' + str(response)) + except grpc.RpcError as rpc_error: + print('gRPC API Exception: %s', rpc_error) + + +if __name__ == '__main__': + main() diff --git a/python-examples/rpccalls/send_bsq.py b/python-examples/rpccalls/send_bsq.py new file mode 100644 index 0000000..cd07cd3 --- /dev/null +++ b/python-examples/rpccalls/send_bsq.py @@ -0,0 +1,27 @@ +from builtins import print + +import grpc + +# from getpass import getpass +import bisq.api.grpc_pb2 as bisq_messages +import bisq.api.grpc_pb2_grpc as bisq_service + + +def main(): + grpc_channel = grpc.insecure_channel('localhost:9998') + grpc_service_stub = bisq_service.WalletsStub(grpc_channel) + api_password: str = 'xyz' # getpass("Enter API password: ") + try: + response = grpc_service_stub.SendBsq.with_call( + bisq_messages.SendBsqRequest( + address='Bbcrt1q9elrmtxtzpwt25zq2pmeeu6qk8029w404ad0xn', + amount='10.00', + tx_fee_rate='10'), + metadata=[('password', api_password)]) + print('Response: ' + str(response[0].tx_info)) + except grpc.RpcError as rpc_error: + print('gRPC API Exception: %s', rpc_error) + + +if __name__ == '__main__': + main() diff --git a/python-examples/rpccalls/send_btc.py b/python-examples/rpccalls/send_btc.py new file mode 100644 index 0000000..419eb92 --- /dev/null +++ b/python-examples/rpccalls/send_btc.py @@ -0,0 +1,28 @@ +from builtins import print + +import grpc + +# from getpass import getpass +import bisq.api.grpc_pb2 as bisq_messages +import bisq.api.grpc_pb2_grpc as bisq_service + + +def main(): + grpc_channel = grpc.insecure_channel('localhost:9998') + grpc_service_stub = bisq_service.WalletsStub(grpc_channel) + api_password: str = 'xyz' # getpass("Enter API password: ") + try: + response = grpc_service_stub.SendBtc.with_call( + bisq_messages.SendBtcRequest( + address='bcrt1qr3tjm77z3qzkf4kstj9v3yw9ewhjqjefz3c48y', + amount='0.006', + tx_fee_rate='15', + memo='Optional memo saved with transaction in Bisq wallet.'), + metadata=[('password', api_password)]) + print('Response: ' + str(response[0].tx_info)) + except grpc.RpcError as rpc_error: + print('gRPC API Exception: %s', rpc_error) + + +if __name__ == '__main__': + main() diff --git a/python-examples/rpccalls/set_tx_fee_rate_preference.py b/python-examples/rpccalls/set_tx_fee_rate_preference.py new file mode 100644 index 0000000..1055f2a --- /dev/null +++ b/python-examples/rpccalls/set_tx_fee_rate_preference.py @@ -0,0 +1,24 @@ +# from getpass import getpass +from builtins import print + +import grpc + +import bisq.api.grpc_pb2 as bisq_messages +import bisq.api.grpc_pb2_grpc as bisq_service + + +def main(): + grpc_channel = grpc.insecure_channel('localhost:9998') + grpc_service_stub = bisq_service.WalletsStub(grpc_channel) + api_password: str = 'xyz' # getpass("Enter API password: ") + try: + response = grpc_service_stub.SetTxFeeRatePreference.with_call( + bisq_messages.SetTxFeeRatePreferenceRequest(tx_fee_rate_preference=20), + metadata=[('password', api_password)]) + print('Response: ' + str(response[0].tx_fee_rate_info)) + except grpc.RpcError as rpc_error: + print('gRPC API Exception: %s', rpc_error) + + +if __name__ == '__main__': + main() diff --git a/python-examples/rpccalls/set_wallet_password.py b/python-examples/rpccalls/set_wallet_password.py new file mode 100644 index 0000000..3abfeae --- /dev/null +++ b/python-examples/rpccalls/set_wallet_password.py @@ -0,0 +1,24 @@ +from builtins import print + +import grpc + +# from getpass import getpass +import bisq.api.grpc_pb2 as bisq_messages +import bisq.api.grpc_pb2_grpc as bisq_service + + +def main(): + grpc_channel = grpc.insecure_channel('localhost:9998') + grpc_service_stub = bisq_service.WalletsStub(grpc_channel) + api_password: str = 'xyz' # getpass("Enter API password: ") + try: + response = grpc_service_stub.SetWalletPassword.with_call( + bisq_messages.SetWalletPasswordRequest(password='abc'), + metadata=[('password', api_password)]) + print('Response: ' + str(response)) + except grpc.RpcError as rpc_error: + print('gRPC API Exception: %s', rpc_error) + + +if __name__ == '__main__': + main() diff --git a/python-examples/rpccalls/stop.py b/python-examples/rpccalls/stop.py new file mode 100644 index 0000000..f9b9c0a --- /dev/null +++ b/python-examples/rpccalls/stop.py @@ -0,0 +1,23 @@ +from builtins import print + +import grpc + +# from getpass import getpass +import bisq.api.grpc_pb2 as bisq_messages +import bisq.api.grpc_pb2_grpc as bisq_service + + +def main(): + grpc_channel = grpc.insecure_channel('localhost:9998') + grpc_service_stub = bisq_service.ShutdownServerStub(grpc_channel) + api_password: str = 'xyz' # getpass("Enter API password: ") + try: + response = grpc_service_stub.Stop.with_call(bisq_messages.StopRequest(), + metadata=[('password', api_password)]) + print('Response: ' + str(response)) + except grpc.RpcError as rpc_error: + print('gRPC API Exception: %s', rpc_error) + + +if __name__ == '__main__': + main() diff --git a/python-examples/rpccalls/take_offer.py b/python-examples/rpccalls/take_offer.py new file mode 100644 index 0000000..f262b39 --- /dev/null +++ b/python-examples/rpccalls/take_offer.py @@ -0,0 +1,36 @@ +# from getpass import getpass +from builtins import print + +import grpc + +import bisq.api.grpc_pb2 as bisq_messages +import bisq.api.grpc_pb2_grpc as bisq_service + + +def main(): + grpc_channel = grpc.insecure_channel('localhost:9999') + grpc_offers_service_stub = bisq_service.OffersStub(grpc_channel) + grpc_trades_service_stub = bisq_service.TradesStub(grpc_channel) + api_password: str = 'xyz' # getpass("Enter API password: ") + try: + # We need to send our payment account id and an (optional) taker fee currency code if offer + # is not a BSQ swap offer. Find out by calling GetOfferCategory before taking the offer. + get_offer_category_response = grpc_offers_service_stub.GetOfferCategory.with_call( + bisq_messages.GetOfferCategoryRequest(id='4940749-73a2e9c3-d5b9-440a-a05d-9feb8e8805f0-182'), + metadata=[('password', api_password)]) + offer_category = get_offer_category_response[0].offer_category + is_bsq_swap = offer_category == bisq_messages.GetOfferCategoryReply.BSQ_SWAP + take_offer_request = bisq_messages.TakeOfferRequest(offer_id='4940749-73a2e9c3-d5b9-440a-a05d-9feb8e8805f0-182') + if not is_bsq_swap: + take_offer_request.payment_account_id = '44838060-ddb5-4fa4-8b34-c128a655316e' + take_offer_request.taker_fee_currency_code = 'BSQ' + response = grpc_trades_service_stub.TakeOffer.with_call( + take_offer_request, + metadata=[('password', api_password)]) + print('Response: ' + str(response)) + except grpc.RpcError as rpc_error: + print('gRPC API Exception: %s', rpc_error) + + +if __name__ == '__main__': + main() diff --git a/python-examples/rpccalls/unfail_trade.py b/python-examples/rpccalls/unfail_trade.py new file mode 100644 index 0000000..d38fa0d --- /dev/null +++ b/python-examples/rpccalls/unfail_trade.py @@ -0,0 +1,25 @@ +from builtins import print + +import grpc + +# from getpass import getpass +import bisq.api.grpc_pb2 as bisq_messages +import bisq.api.grpc_pb2_grpc as bisq_service + + +def main(): + grpc_channel = grpc.insecure_channel('localhost:9998') + grpc_service_stub = bisq_service.TradesStub(grpc_channel) + api_password: str = 'xyz' # getpass("Enter API password: ") + try: + response = grpc_service_stub.UnFailTrade.with_call( + bisq_messages.UnFailTradeRequest( + trade_id='83e8b2e2-51b6-4f39-a748-3ebd29c22aea'), + metadata=[('password', api_password)]) + print('Response: ' + str(response)) + except grpc.RpcError as rpc_error: + print('gRPC API Exception: %s', rpc_error) + + +if __name__ == '__main__': + main() diff --git a/python-examples/rpccalls/unlock_wallet.py b/python-examples/rpccalls/unlock_wallet.py new file mode 100644 index 0000000..55d8da3 --- /dev/null +++ b/python-examples/rpccalls/unlock_wallet.py @@ -0,0 +1,26 @@ +from builtins import print + +import grpc + +# from getpass import getpass +import bisq.api.grpc_pb2 as bisq_messages +import bisq.api.grpc_pb2_grpc as bisq_service + + +def main(): + grpc_channel = grpc.insecure_channel('localhost:9998') + grpc_service_stub = bisq_service.WalletsStub(grpc_channel) + api_password: str = 'xyz' # getpass("Enter API password: ") + try: + response = grpc_service_stub.UnlockWallet.with_call( + bisq_messages.UnlockWalletRequest( + password='abc', + timeout=30), + metadata=[('password', api_password)]) + print('Response: ' + str(response)) + except grpc.RpcError as rpc_error: + print('gRPC API Exception: %s', rpc_error) + + +if __name__ == '__main__': + main() diff --git a/python-examples/rpccalls/unset_tx_fee_rate_preference.py b/python-examples/rpccalls/unset_tx_fee_rate_preference.py new file mode 100644 index 0000000..10c8693 --- /dev/null +++ b/python-examples/rpccalls/unset_tx_fee_rate_preference.py @@ -0,0 +1,24 @@ +from builtins import print + +import grpc + +# from getpass import getpass +import bisq.api.grpc_pb2 as bisq_messages +import bisq.api.grpc_pb2_grpc as bisq_service + + +def main(): + grpc_channel = grpc.insecure_channel('localhost:9998') + grpc_service_stub = bisq_service.WalletsStub(grpc_channel) + api_password: str = 'xyz' # getpass("Enter API password: ") + try: + response = grpc_service_stub.UnsetTxFeeRatePreference.with_call( + bisq_messages.UnsetTxFeeRatePreferenceRequest(), + metadata=[('password', api_password)]) + print('Response: ' + str(response[0].tx_fee_rate_info)) + except grpc.RpcError as rpc_error: + print('gRPC API Exception: %s', rpc_error) + + +if __name__ == '__main__': + main() diff --git a/python-examples/rpccalls/verify_bsq_sent_to_address.py b/python-examples/rpccalls/verify_bsq_sent_to_address.py new file mode 100644 index 0000000..4afe6a2 --- /dev/null +++ b/python-examples/rpccalls/verify_bsq_sent_to_address.py @@ -0,0 +1,26 @@ +from builtins import print + +import grpc + +# from getpass import getpass +import bisq.api.grpc_pb2 as bisq_messages +import bisq.api.grpc_pb2_grpc as bisq_service + + +def main(): + grpc_channel = grpc.insecure_channel('localhost:9998') + grpc_service_stub = bisq_service.WalletsStub(grpc_channel) + api_password: str = 'xyz' # getpass("Enter API password: ") + try: + response = grpc_service_stub.VerifyBsqSentToAddress.with_call( + bisq_messages.VerifyBsqSentToAddressRequest( + address='Bbcrt1q9elrmtxtzpwt25zq2pmeeu6qk8029w404ad0xn', + amount='10.00'), + metadata=[('password', api_password)]) + print('BSQ amount was received at address: ' + str(response[0].is_amount_received)) + except grpc.RpcError as rpc_error: + print('gRPC API Exception: %s', rpc_error) + + +if __name__ == '__main__': + main() diff --git a/python-examples/rpccalls/withdraw_funds.py b/python-examples/rpccalls/withdraw_funds.py new file mode 100644 index 0000000..e98c7ac --- /dev/null +++ b/python-examples/rpccalls/withdraw_funds.py @@ -0,0 +1,24 @@ +from builtins import print + +import grpc + +# from getpass import getpass +import bisq.api.grpc_pb2 as bisq_messages +import bisq.api.grpc_pb2_grpc as bisq_service + + +def main(): + grpc_channel = grpc.insecure_channel('localhost:9998') + grpc_service_stub = bisq_service.TradesStub(grpc_channel) + api_password: str = 'xyz' # getpass("Enter API password: ") + try: + response = grpc_service_stub.WithdrawFunds.with_call( + bisq_messages.WithdrawFundsRequest(trade_id='83e8b2e2-51b6-4f39-a748-3ebd29c22aea'), + metadata=[('password', api_password)]) + print('Response: ' + str(response)) + except grpc.RpcError as rpc_error: + print('gRPC API Exception: %s', rpc_error) + + +if __name__ == '__main__': + main() diff --git a/reference-doc-builder/build.gradle b/reference-doc-builder/build.gradle new file mode 100644 index 0000000..4df5b7b --- /dev/null +++ b/reference-doc-builder/build.gradle @@ -0,0 +1,33 @@ +plugins { + id 'java' +} + +repositories { + mavenCentral() + maven { url 'https://jitpack.io' } +} + +group 'bisq' +version '0.0.1-SNAPSHOT' + +dependencies { + compileOnly 'javax.annotation:javax.annotation-api:1.2' + + implementation 'net.sf.jopt-simple:jopt-simple:5.0.4' + implementation 'commons-io:commons-io:2.11.0' + implementation 'com.google.protobuf:protobuf-java:3.12.4' + implementation 'org.slf4j:slf4j-api:1.7.30' + implementation 'ch.qos.logback:logback-classic:1.1.11' + implementation 'ch.qos.logback:logback-core:1.1.11' + + annotationProcessor 'org.projectlombok:lombok:1.18.22' + testAnnotationProcessor 'org.projectlombok:lombok:1.18.22' + compileOnly 'org.projectlombok:lombok:1.18.22' + testCompileOnly 'org.projectlombok:lombok:1.18.22' + testImplementation 'org.junit.jupiter:junit-jupiter-api:5.7.2' + testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.7.2' +} + +test { + useJUnitPlatform() +} diff --git a/reference-doc-builder/gradle/wrapper/gradle-wrapper.properties b/reference-doc-builder/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..2e6e589 --- /dev/null +++ b/reference-doc-builder/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.3-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/reference-doc-builder/gradlew b/reference-doc-builder/gradlew new file mode 100755 index 0000000..744e882 --- /dev/null +++ b/reference-doc-builder/gradlew @@ -0,0 +1,185 @@ +#!/usr/bin/env sh + +# +# Copyright 2015 the original author or authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MSYS* | MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin or MSYS, switch paths to Windows format before running java +if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=`expr $i + 1` + done + case $i in + 0) set -- ;; + 1) set -- "$args0" ;; + 2) set -- "$args0" "$args1" ;; + 3) set -- "$args0" "$args1" "$args2" ;; + 4) set -- "$args0" "$args1" "$args2" "$args3" ;; + 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=`save "$@"` + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +exec "$JAVACMD" "$@" diff --git a/reference-doc-builder/gradlew.bat b/reference-doc-builder/gradlew.bat new file mode 100644 index 0000000..ac1b06f --- /dev/null +++ b/reference-doc-builder/gradlew.bat @@ -0,0 +1,89 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/reference-doc-builder/settings.gradle b/reference-doc-builder/settings.gradle new file mode 100644 index 0000000..d7dc0c3 --- /dev/null +++ b/reference-doc-builder/settings.gradle @@ -0,0 +1,2 @@ +rootProject.name = 'reference-doc-builder' + diff --git a/reference-doc-builder/src/main/java/bisq/apidoc/BisqApiDocMain.java b/reference-doc-builder/src/main/java/bisq/apidoc/BisqApiDocMain.java new file mode 100644 index 0000000..1e4c937 --- /dev/null +++ b/reference-doc-builder/src/main/java/bisq/apidoc/BisqApiDocMain.java @@ -0,0 +1,146 @@ +/* + * This file is part of Bisq. + * + * Bisq is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bisq is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bisq. If not, see . + */ +package bisq.apidoc; + + +import bisq.apidoc.markdown.ProtobufDefinitionParser; +import bisq.apidoc.protobuf.definition.TextBlockParser; +import bisq.apidoc.protobuf.text.ProtobufFileReader; +import bisq.apidoc.protobuf.text.TextBlock; +import fun.mingshan.markdown4j.Markdown; +import fun.mingshan.markdown4j.type.block.Block; +import fun.mingshan.markdown4j.writer.MdWriter; +import joptsimple.OptionParser; +import joptsimple.OptionSet; +import joptsimple.OptionSpec; +import lombok.extern.slf4j.Slf4j; + +import java.io.IOException; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.List; + +import static bisq.apidoc.protobuf.text.ProtobufTextBlockFactory.createTextBlocks; + +/** + * Converts Bisq .proto files into a single Slate style Markdown file: the Bisq gRPC API Reference document. + *

+ * The script generates the reference doc from .proto file in three steps: + *

+ * (1) Bisq's pb.proto and grpc.proto files are parsed into java objects representing chunks of the files' raw text, + * i.e., TextBlocks. Each TextBlock represents some protobuf definition, from an import statement, a comment, an + * enum, message, rpc-method, to a grpc service definition. + *

+ * (2) The list of TextBlocks produced by step 1 are parsed into java POJOs -- ProtobufDefinitions -- containing + * descriptions of the .proto definition. These descriptions are sourced from comments in the .proto files, and + * serve as the "descriptions" displayed in the final Bisq gRPC API Reference doc hosted on GitHub. + *

+ * (3) The list of ProtobufDefinitions produced by step 2 are parsed into the Markdown content blocks, and written + * in order to a single Markdown file: index.html.md. The file can be written directly to your forked slate repo's + * source directory, committed to your slate fork's remote repo, then deployed to your forked slate repo's github-pages + * site. + */ +@SuppressWarnings("ClassCanBeRecord") +@Slf4j +public class BisqApiDocMain { + + private final Path protobufsPath; + private final Path markdownPath; + private final boolean failOnMissingDocumentation; + + public BisqApiDocMain(Path protobufsPath, Path markdownPath, boolean failOnMissingDocumentation) { + this.protobufsPath = protobufsPath; + this.markdownPath = markdownPath; + this.failOnMissingDocumentation = failOnMissingDocumentation; + } + + public void generateMarkdown() { + // Transform .proto files into TextBlock objects identifying what they are: + // block comment, line comment, grpc service, rpc method, message, enum, etc. + List pbProtoTextBlocks = getParsedProtobufTextBlocks("pb.proto"); + List grpcProtoTextBlocks = getParsedProtobufTextBlocks("grpc.proto"); + + // Transform TextBlock objects into ProtobufDefinition maps + // (by name) used to generate the final API doc markdown file. + TextBlockParser textBlockParser = new TextBlockParser(pbProtoTextBlocks, + grpcProtoTextBlocks, + failOnMissingDocumentation); + textBlockParser.parse(); + + // Parse the mappings of names to protobuf service definitions into Markdown content blocks. + ProtobufDefinitionParser protobufDefinitionParser = new ProtobufDefinitionParser(textBlockParser, failOnMissingDocumentation); + protobufDefinitionParser.parse(); + + // Generate final markdown content blocks ProtobufDefinition by name mappings. + List mdBlocks = protobufDefinitionParser.getMdBlocks(); // createMdBlocks(textBlockParser, failOnMissingDocumentation); + try { + // Write the markdown content blocks to file. + Markdown.MarkdownBuilder markdownBuilder = Markdown.builder().name("index.html"); + for (Block mdBlock : mdBlocks) { + markdownBuilder.block(mdBlock); + } + Markdown markdown = markdownBuilder.build(); + + // Write to markdownPath (opt) dir. + MdWriter.write(markdownPath, markdown); + + // Write to project dir. + // MdWriter.write(markdown); + + log.debug("Final Markdown:\n{}", markdown); + } catch (IOException ex) { + throw new RuntimeException(ex); + } + } + + private List getParsedProtobufTextBlocks(String fileName) { + ProtobufFileReader protobufFileReader = null; + try { + protobufFileReader = new ProtobufFileReader(protobufsPath.resolve(fileName)); + return createTextBlocks(protobufFileReader); + } catch (Exception ex) { + throw new IllegalStateException("Fatal error parsing file " + fileName, ex); + } finally { + if (protobufFileReader != null) + protobufFileReader.close(); + } + } + + public static void main(String[] args) { + OptionParser optionParser = new OptionParser(); + + OptionSpec protosInOpt = optionParser.accepts("protosIn", "directory path to input protobuf files") + .withRequiredArg() + .defaultsTo("src/main/resources/proto"); + OptionSpec markdownOutOpt = optionParser.accepts("markdownOut", "directory path to output markdown file") + .withRequiredArg() + .defaultsTo("./"); + OptionSpec failOnMissingDocumentationOpt = optionParser.accepts("failOnMissingDocumentation", + "fail if needed .proto comment is missing") + .withOptionalArg() + .ofType(boolean.class) + .defaultsTo(Boolean.FALSE); + + OptionSet options = optionParser.parse(args); + Path protobufsPath = Paths.get(options.valueOf(protosInOpt)); + Path markdownPath = Paths.get(options.valueOf(markdownOutOpt)); + boolean failOnMissingDocumentation = options.valueOf(failOnMissingDocumentationOpt); + + BisqApiDocMain bisqApiDocMain = new BisqApiDocMain(protobufsPath, markdownPath, failOnMissingDocumentation); + bisqApiDocMain.generateMarkdown(); + } +} diff --git a/reference-doc-builder/src/main/java/bisq/apidoc/markdown/CodeExamples.java b/reference-doc-builder/src/main/java/bisq/apidoc/markdown/CodeExamples.java new file mode 100644 index 0000000..889a41b --- /dev/null +++ b/reference-doc-builder/src/main/java/bisq/apidoc/markdown/CodeExamples.java @@ -0,0 +1,167 @@ +/* + * This file is part of Bisq. + * + * Bisq is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bisq is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bisq. If not, see . + */ +package bisq.apidoc.markdown; + +import bisq.apidoc.protobuf.definition.RpcMethodDefinition; +import lombok.extern.slf4j.Slf4j; + +import java.io.File; +import java.util.function.BiFunction; +import java.util.function.Predicate; +import java.util.stream.Collectors; + +import static bisq.apidoc.protobuf.ProtoParserUtil.toText; + +/** + * Generates the slate Markdown String from the example source code + * files (shell, java and python), for a given rpc method definition. + *

+ * The content must be inserted into the encapsulating rpc method block's + * second line, after its variable substitution has been performed. + */ +@Slf4j +public class CodeExamples { + + // TODO Is hard-coding paths to example source files OK? + private static final String CLI_EXAMPLES_DIR = "cli-examples"; + private static final String JAVA_EXAMPLES_DIR = "java-examples/src/main/java/rpccalls"; + private static final String PYTHON_EXAMPLES_DIR = "python-examples/rpccalls"; + + private static final String START_SHELL_MD = "```shell"; + private static final String START_JAVA_MD = "```java"; + private static final String START_PYTHON_MD = "```python"; + private static final String END_SAMPLE_MD = "```"; + + private static final String SHELLSCRIPT_EXTENSION = ".sh"; + private static final String JAVA_EXTENSION = ".java"; + private static final String PYTHON_EXTENSION = ".py"; + + private final BiFunction toCliSourceFileName = (method, extension) -> { + if (!extension.equals(SHELLSCRIPT_EXTENSION)) + throw new IllegalArgumentException("Invalid CLI code example file extension: " + extension); + else + return CLI_EXAMPLES_DIR + File.separatorChar + method.name() + extension; + }; + + private final BiFunction toJavaSourceFileName = (method, extension) -> { + if (!extension.equals(JAVA_EXTENSION)) + throw new IllegalArgumentException("Invalid Java code example file extension: " + extension); + else + return JAVA_EXAMPLES_DIR + File.separatorChar + method.name() + extension; + }; + + private final BiFunction toPythonSourceFileName = (method, extension) -> { + if (!extension.equals(PYTHON_EXTENSION)) { + throw new IllegalArgumentException("Invalid Python code example file extension: " + extension); + } else { + String firstLetter = method.name().substring(0, 1).toLowerCase(); + String idiomaticSourceFileSuffix = firstLetter + method.name().substring(1) + .replaceAll("([A-Z])", "_$1") + .toLowerCase(); + return PYTHON_EXAMPLES_DIR + File.separatorChar + idiomaticSourceFileSuffix + extension; + } + }; + + private final BiFunction toSourceFile = (method, extension) -> { + if (extension.equals(SHELLSCRIPT_EXTENSION)) + return new File(toCliSourceFileName.apply(method, extension)); + else if (extension.equals(JAVA_EXTENSION)) + return new File(toJavaSourceFileName.apply(method, extension)); + else if (extension.equals(PYTHON_EXTENSION)) + return new File(toPythonSourceFileName.apply(method, extension)); + else + throw new IllegalArgumentException("Invalid code example file extension: " + extension); + }; + + private final Predicate hasCliExample = (method) -> toSourceFile.apply(method, SHELLSCRIPT_EXTENSION).exists(); + private final Predicate hasJavaExample = (method) -> toSourceFile.apply(method, JAVA_EXTENSION).exists(); + private final Predicate hasPythonExample = (method) -> toSourceFile.apply(method, PYTHON_EXTENSION).exists(); + + private final RpcMethodDefinition rpcMethodDefinition; + private static String javaBoilerplateSource; + + public CodeExamples(RpcMethodDefinition rpcMethodDefinition) { + this.rpcMethodDefinition = rpcMethodDefinition; + } + + public String getContent() { + StringBuilder examplesBuilder = new StringBuilder(); + + examplesBuilder.append(START_SHELL_MD).append("\n"); + if (hasCliExample.test(rpcMethodDefinition)) { + String rawSource = getRawContent(toSourceFile.apply(rpcMethodDefinition, SHELLSCRIPT_EXTENSION)); + rawSource.lines().collect(Collectors.toList()).forEach(l -> { + if (!l.startsWith("source ")) { + examplesBuilder.append(l.replace("$BISQ_HOME", ".")); + examplesBuilder.append("\n"); + } + }); + examplesBuilder.append("\n"); + } + examplesBuilder.append(END_SAMPLE_MD).append("\n").append("\n"); + + examplesBuilder.append(START_JAVA_MD).append("\n"); + if (hasJavaExample.test(rpcMethodDefinition)) { + File sourceFile = toSourceFile.apply(rpcMethodDefinition, JAVA_EXTENSION); + String displayedSource = getRawContent(sourceFile).trim(); + examplesBuilder.append(displayedSource); + examplesBuilder.append("\n"); + if (displayedSource.lines().collect(Collectors.toList()).size() > 1) { + examplesBuilder.append("\n"); + examplesBuilder.append("//////////////////").append("\n"); + examplesBuilder.append("// BaseJavaExample").append("\n"); + examplesBuilder.append("//////////////////").append("\n"); + examplesBuilder.append("\n"); + examplesBuilder.append(getJavaBoilerplateSource()); + examplesBuilder.append("\n"); + } + } + examplesBuilder.append(END_SAMPLE_MD).append("\n"); + + examplesBuilder.append(START_PYTHON_MD).append("\n"); + if (hasPythonExample.test(rpcMethodDefinition)) { + String pythonSource = getRawContent(toSourceFile.apply(rpcMethodDefinition, PYTHON_EXTENSION)); + examplesBuilder.append(pythonSource.trim()); + examplesBuilder.append("\n"); + } + examplesBuilder.append(END_SAMPLE_MD).append("\n"); + + return examplesBuilder.toString(); + } + + public boolean exist() { + var hasAnyExample = hasCliExample.test(rpcMethodDefinition) + || hasJavaExample.test(rpcMethodDefinition) + || hasPythonExample.test(rpcMethodDefinition); + return hasAnyExample; + } + + public static String getJavaBoilerplateSource() { + if (javaBoilerplateSource == null) { + String sourcePath = JAVA_EXAMPLES_DIR + File.separator + "BaseJavaExample.java"; + File sourceFile = new File(sourcePath); + StringBuilder displayedSource = new StringBuilder(getRawContent(sourceFile).trim()); + displayedSource.append("\n"); + javaBoilerplateSource = displayedSource.toString(); + } + return javaBoilerplateSource; + } + + private static String getRawContent(File sourceFile) { + return toText.apply(sourceFile.toPath()); + } +} diff --git a/reference-doc-builder/src/main/java/bisq/apidoc/markdown/FieldTableBlockBuilder.java b/reference-doc-builder/src/main/java/bisq/apidoc/markdown/FieldTableBlockBuilder.java new file mode 100644 index 0000000..cf916b8 --- /dev/null +++ b/reference-doc-builder/src/main/java/bisq/apidoc/markdown/FieldTableBlockBuilder.java @@ -0,0 +1,189 @@ +/* + * This file is part of Bisq. + * + * Bisq is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bisq is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bisq. If not, see . + */ +package bisq.apidoc.markdown; + +import bisq.apidoc.protobuf.definition.FieldDefinition; +import bisq.apidoc.protobuf.definition.MapFieldDefinition; +import bisq.apidoc.protobuf.definition.MessageDefinition; +import fun.mingshan.markdown4j.type.block.Block; +import fun.mingshan.markdown4j.type.block.SlateTableBlock; +import fun.mingshan.markdown4j.type.block.StringBlock; +import fun.mingshan.markdown4j.type.element.UrlElement; +import lombok.extern.slf4j.Slf4j; + +import java.util.*; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.function.Supplier; + +import static bisq.apidoc.protobuf.ProtoParserUtil.isScalarDataType; +import static java.util.Arrays.asList; +import static java.util.Comparator.comparing; + +@Slf4j +public class FieldTableBlockBuilder { + + private static final String BEGIN_ONE_OF_MESSAGE = "one of {"; + private static final String END_ONE_OF_MESSAGE = "}"; + private static final String DESCRIPTION = "Description"; + private static final String NAME = "Name"; + private static final String TYPE = "Type"; + + // Used for indentation in Markdown document. + private static final String NON_BREAKING_SPACE = " "; + private static final String FOUR_NBSP = new String(new char[4]).replace("\0", NON_BREAKING_SPACE); + + private final Supplier anotherRow = SlateTableBlock.TableRow::new; + private final Predicate shouldWriteInternalLink = (type) -> type != null && !isScalarDataType.test(type); + private final Function toDecoratedFieldType = (f) -> + f.isRepeated() ? "array " + f.type() : f.type(); + + private final MessageDefinition messageDefinition; + private final boolean failOnMissingDocumentation; + private final List rows; + + public FieldTableBlockBuilder(MessageDefinition messageDefinition, boolean failOnMissingDocumentation) { + this.messageDefinition = messageDefinition; + this.failOnMissingDocumentation = failOnMissingDocumentation; + this.rows = new ArrayList<>(); + } + + public Block build() { + if (messageDefinition.fields().size() == 0) + throw new IllegalArgumentException("Cannot build Field md table block from empty field map."); + + List fields = messageDefinition.fields().values().stream().toList(); + fields.forEach(f -> { + if (f.isMapField()) { + SlateTableBlock.TableRow row = anotherRow.get(); + List mapFieldColumns = getMapFieldColumns((MapFieldDefinition) f); + row.setColumns(mapFieldColumns); + rows.add(row); + } else if (f.isOneOfMessageField()) { + SlateTableBlock.TableRow startRow = anotherRow.get(); + startRow.setColumns(asList(BEGIN_ONE_OF_MESSAGE, + NON_BREAKING_SPACE, + "Field value will be one of the following.")); + rows.add(startRow); + + // TODO Append one of field rows, each name is indented by four nbsp; chars. + List> oneOfFieldColumns = getOneOfFieldColumns(f.oneOfFieldChoices()); + oneOfFieldColumns.forEach(row -> { + SlateTableBlock.TableRow choiceRow = anotherRow.get(); + choiceRow.setColumns(row); + rows.add(choiceRow); + }); + + SlateTableBlock.TableRow endRow = anotherRow.get(); + endRow.setColumns(asList(END_ONE_OF_MESSAGE, NON_BREAKING_SPACE, NON_BREAKING_SPACE)); + rows.add(endRow); + } else { + // If the field type is a global enum or message, generate an internal link to it + // and use that in the table row's 'Type' column. But do not generate a link if the + // field type is a (local) enum declared within the message definition because you + // cannot navigate to it. In Slate, only level 1 and 2 headers will appear in the + // table of contents. + boolean isLocalEnum = messageDefinition.enums().containsKey(f.type()); + Optional optionalInternalLink = isLocalEnum + ? Optional.empty() + : getInternalLink(f); + String type = optionalInternalLink.isPresent() + ? optionalInternalLink.get().toMd() + : toDecoratedFieldType.apply(f); + SlateTableBlock.TableRow row = anotherRow.get(); + row.setColumns(asList(f.name(), + type, + f.description())); + rows.add(row); + } + }); + SlateTableBlock tableBlock = SlateTableBlock.builder() + .titles(asList(NAME, TYPE, DESCRIPTION)) + .rows(rows) + .build(); + Map templateVars = new HashMap<>() {{ + put("field.tbl", tableBlock.toMd()); + }}; + Template template = new Template("message-fields.md", templateVars, failOnMissingDocumentation); + return StringBlock.builder().content(template.getContent()).build(); + } + + private List getMapFieldColumns(MapFieldDefinition mapField) { + String mapValueType = mapField.valueType(); + if (isScalarDataType.test(mapValueType)) { + // LT and GT symbols won't work in MD by simply escaping them; use the decimal unicode point. + String mapSpec = "map" + "<" + mapField.keyType() + ", " + mapValueType + ">"; + return asList(mapField.name(), + mapSpec, + mapField.description()); + } else { + throw new UnsupportedOperationException("TODO generate internal link for custom map value type: " + mapValueType); + } + } + + /** + * Returns a list of row columns for each oneof message field choice. + */ + private List> getOneOfFieldColumns(List choices) { + // Sort the choice byte type, ignoring field numbers. Should I? + choices.sort(comparing(FieldDefinition::type)); + List> rows = new ArrayList<>(); + choices.forEach(f -> { + // One Of Fields cannot be repeated, or map fields; do + // not need to decorate any type column values with "array". + Optional optionalInternalLink = getInternalLink(f); + String type = optionalInternalLink.isPresent() + ? optionalInternalLink.get().toMd() + : f.type(); + rows.add(asList(FOUR_NBSP + f.name(), type, f.description())); + }); + return rows; + } + + /** + * Returns an optional Markdown block for an internal link, if the + * field type refers to a custom protobuf type, else Optional.empty(). + */ + private Optional getInternalLink(FieldDefinition fieldDefinition) { + // A repeated field requires an "array" prefix on the type column value. + String decoratedType = toDecoratedFieldType.apply(fieldDefinition); + return shouldWriteInternalLink.test(fieldDefinition.type()) + ? Optional.of(UrlElement.builder() + .tips(decoratedType) + .url("#" + fieldDefinition.type().toLowerCase()) + .build() + .toBlock()) + : Optional.empty(); + + } + + /** + * Returns an optional Markdown block for an internal link, if the + * field type refers to a custom protobuf type, else Optional.empty(). + */ + @Deprecated + private Optional getInternalLinkDeprecated(String fieldType) { + return shouldWriteInternalLink.test(fieldType) + ? Optional.of(UrlElement.builder() + .tips(fieldType) + .url("#" + fieldType.toLowerCase()) + .build() + .toBlock()) + : Optional.empty(); + + } +} diff --git a/reference-doc-builder/src/main/java/bisq/apidoc/markdown/GrpcDependencyCache.java b/reference-doc-builder/src/main/java/bisq/apidoc/markdown/GrpcDependencyCache.java new file mode 100644 index 0000000..d9602b5 --- /dev/null +++ b/reference-doc-builder/src/main/java/bisq/apidoc/markdown/GrpcDependencyCache.java @@ -0,0 +1,218 @@ +/* + * This file is part of Bisq. + * + * Bisq is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bisq is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bisq. If not, see . + */ +package bisq.apidoc.markdown; + +import bisq.apidoc.protobuf.definition.*; +import lombok.extern.slf4j.Slf4j; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.function.BiPredicate; +import java.util.function.Predicate; +import java.util.stream.Collectors; + +import static bisq.apidoc.protobuf.ProtoParserUtil.isScalarDataType; +import static java.util.Comparator.comparing; + +/** + * This class caches protobuf message and enum definitions needed by internal navigational links in the API + * reference doc. It attempts to find all the required dependencies from the grpcServiceDefinitions constructor + * argument alone, but recursion is not used to crawl down to the deepest levels, and all bank account payload + * message types are cached in a hacky way to make sure all of those links work too. + */ +@Slf4j +public class GrpcDependencyCache { + + // Aids debugging: makes it easy to set break points. + @SuppressWarnings("unused") + private final BiPredicate isDebuggedField = (field, name) -> + field.type() != null + && !isScalarDataType.test(field.type()) + && field.type().equals(name); + + // Lists of message and enums to be displayed below the gRPC service blocks. + // Only enums and messages used by the gRPC services are needed, + // not all enums and messages parsed from the pb.proto file. + private final List grpcMessageDependencies; + private final List grpcEnumDependencies; + + // Maps of ProtobufDefinitions produced by TextBlockParser. + private final Map globalEnumDefinitions; + private final Map globalMessageDefinitions; + private final Map grpcServiceDefinitions; + + public GrpcDependencyCache(Map globalEnumDefinitions, + Map globalMessageDefinitions, + Map grpcServiceDefinitions) { + this.globalEnumDefinitions = globalEnumDefinitions; + this.globalMessageDefinitions = globalMessageDefinitions; + this.grpcServiceDefinitions = grpcServiceDefinitions; + this.grpcMessageDependencies = new ArrayList<>(); + this.grpcEnumDependencies = new ArrayList<>(); + } + + public GrpcDependencyCache load() { + // Cache the grpc message dependencies first. + loadMessages(); + // Now the enum dependencies references by the message dependencies just loaded. + loadEnums(); + + return this; + } + + public List getGrpcMessageDependencies() { + return grpcMessageDependencies; + } + + public List getGrpcEnumDependencies() { + return grpcEnumDependencies; + } + + private void loadAllBankAccountPayloadMessages() { + // Recursion cannot be used to update the dependency cache collection, and some grpc msg dependencies + // referenced more deeply inside .proto definition structures get skipped (not cached). This kludge ensures + // we get all the AccountPayload messages into the cache so internal links to them work. + Predicate isAccountPayloadMessage = (msgName) -> msgName != null && msgName.contains("AccountPayload"); + List accountPayloadMessages = globalMessageDefinitions.values().stream() + .filter(m -> isAccountPayloadMessage.test(m.name())) + .collect(Collectors.toList()); + accountPayloadMessages.forEach(this::cacheGrpcMessageDependency); + } + + private void loadMessages() { + loadAllBankAccountPayloadMessages(); + + grpcServiceDefinitions.values().forEach(serviceDefinition -> + serviceDefinition.rpcMethodDefinitions().values().forEach(rpcMethodDefinition -> { + MessageDefinition requestMessageDefinition = rpcMethodDefinition.requestMessageDefinition(); + cacheGrpMessageDefinition(requestMessageDefinition); + + MessageDefinition responseMessageDefinition = rpcMethodDefinition.responseMessageDefinition(); + cacheGrpMessageDefinition(responseMessageDefinition); + })); + grpcMessageDependencies.sort(comparing(MessageDefinition::name)); + } + + private void loadEnums() { + // Cache the grpc enum dependencies found in the grpc message dependencies. + grpcMessageDependencies.forEach(m -> + m.fields().values().forEach(f -> { + if (f.isOneOfMessageField()) { + log.warn("Dead code? {} has oneof message field type.", m.name()); // TODO + } else if (f.isMapField()) { + log.warn("Dead code? {} has map value type.", m.name()); // TODO + } else if (f.type() == null) { + log.warn("Dead code? {} has null field type.", m.name()); // TODO + } else if (globalEnumDefinitions.containsKey(f.type())) { + EnumDefinition enumDefinition = globalEnumDefinitions.get(f.type()); + if (!grpcEnumDependencies.contains(enumDefinition)) { + grpcEnumDependencies.add(enumDefinition); + } + } + })); + grpcEnumDependencies.sort(comparing(EnumDefinition::name)); + } + + private void cacheGrpMessageDefinition(MessageDefinition messageDefinition) { + messageDefinition.fields().values().forEach(field -> { + cacheFieldTypeAsMessage(field); + + // Now iterate the field message's fields and cache those that are messages. + Optional fieldTypeAsMessage = getMessageDefinition(field.type()); + // A field type can be a message, with its own child fields. + // (Deep, but we can't use recursion while updating a collection). + fieldTypeAsMessage.ifPresent(definition -> + definition.fields().values().stream() + .filter(child -> child.type() != null) + .filter(child -> !isScalarDataType.test(child.type())) + .forEach(this::cacheFieldTypeAsMessage)); + }); + } + + private void cacheFieldTypeAsMessage(FieldDefinition fieldDefinition) { + if (fieldDefinition.isMapField()) { + cacheMapFieldValueType((MapFieldDefinition) fieldDefinition); + } else { + Optional fieldAsMessage = getMessageDefinition(fieldDefinition.type()); + fieldAsMessage.ifPresent(message -> { + cacheGrpcMessageDependency(message); + if (message.hasOneOfField()) { + List choices = message.getOneOfFieldChoices(); + cacheFieldMessageTypeDependencies(choices); + } else if (message.fields().size() > 0) { + cacheFieldMessageTypeDependencies(message.fields().values().stream().toList()); + } + }); + } + } + + private void cacheFieldMessageTypeDependencies(List fields) { + fields.forEach(f -> { + if (!f.isMapField() && !f.isOneOfMessageField()) { + Optional fieldAsMessage = getMessageDefinition(f.type()); + fieldAsMessage.ifPresent(message -> { + cacheGrpcMessageDependency(message); + if (message.hasOneOfField()) { + cacheOneOfFields(message); + } + }); + } + }); + } + + private void cacheOneOfFields(MessageDefinition messageDefinitionWithOneOfField) { + List oneOfFieldChoices = messageDefinitionWithOneOfField.getOneOfFieldChoices(); + for (FieldDefinition choice : oneOfFieldChoices) { + Optional choiceTypeAsMessage = getMessageDefinition(choice.type()); + choiceTypeAsMessage.ifPresent(m -> { + List fields = m.fields().values().stream().toList(); + fields.forEach(f -> { + Optional oneOfMessage = getMessageDefinition(f.type()); + oneOfMessage.ifPresent(this::cacheGrpcMessageDependency); + }); + }); + } + } + + private void cacheMapFieldValueType(MapFieldDefinition mapFieldDefinition) { + String mapValueType = mapFieldDefinition.valueType(); + Optional mapValueTypeAsMessage = isScalarDataType.test(mapValueType) + ? Optional.empty() + : getMessageDefinition(mapValueType); + if (mapValueTypeAsMessage.isPresent()) { + log.debug("Cache map field value type as grpc msg dependency: {}", mapValueTypeAsMessage.get()); + cacheGrpcMessageDependency(mapValueTypeAsMessage.get()); + } + } + + private void cacheGrpcMessageDependency(MessageDefinition messageDefinition) { + if (!grpcMessageDependencies.contains(messageDefinition)) + grpcMessageDependencies.add(messageDefinition); + } + + private Optional getMessageDefinition(String fieldType) { + if (isScalarDataType.test(fieldType)) + return Optional.empty(); + + return globalMessageDefinitions.keySet().stream() + .filter(name -> name.equals(fieldType)) + .map(globalMessageDefinitions::get) + .findFirst(); + } +} diff --git a/reference-doc-builder/src/main/java/bisq/apidoc/markdown/ProtobufDefinitionParser.java b/reference-doc-builder/src/main/java/bisq/apidoc/markdown/ProtobufDefinitionParser.java new file mode 100644 index 0000000..b16c931 --- /dev/null +++ b/reference-doc-builder/src/main/java/bisq/apidoc/markdown/ProtobufDefinitionParser.java @@ -0,0 +1,434 @@ +/* + * This file is part of Bisq. + * + * Bisq is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bisq is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bisq. If not, see . + */ +package bisq.apidoc.markdown; + +import bisq.apidoc.protobuf.definition.*; +import fun.mingshan.markdown4j.type.block.Block; +import fun.mingshan.markdown4j.type.block.SlateTableBlock; +import fun.mingshan.markdown4j.type.block.StringBlock; +import lombok.extern.slf4j.Slf4j; + +import java.util.*; +import java.util.function.BiFunction; +import java.util.function.Predicate; +import java.util.stream.Collectors; + +import static java.lang.String.format; +import static java.util.Arrays.asList; +import static java.util.Comparator.comparing; + +/** + * Transforms maps of ProtobufDefinitions into Markdown blocks for the final API doc markdown file. + *

+ * A Markdown "Block" can be as small as a link, up to a large chunk of text describing a protobuf message + * with nested tables of enums and fields. What matters is that lists of blocks are written in proper order. + *

+ * Some notes about Slate Markdown style follows. + *

+ * Only level 1 and 2 headers will appear in the table of contents. + *

+ * Internal and External Links: + *

+ * This is an [internal link](#error-code-definitions) + * Put these in slate tbl 'type' columns: + * [PaymentAccount](#paymentaccount) + * [PaymentAccountPayload](#paymentaccountpayload) + *

+ * This is an [external link](http://google.com). + *

+ * There are minor differences between the fun.mingshan.markdown4j type style, and slate style. + * See SlateTableBlockEncoder. + */ +@Slf4j +public class ProtobufDefinitionParser { + + private static final String CONSTANT = "Constant"; + private static final String DESCRIPTION = "Description"; + private static final String REQUEST = "Request"; + private static final String RESPONSE = "Response"; + private static final String VALUE = "Value"; + + private final Predicate invalidRpcMessageType = (type) -> + type == null || !(type.equals(REQUEST) || type.equals(RESPONSE)); + + // Maps of ProtobufDefinitions produced by TextBlockParser. + private final Map globalEnumDefinitions; + private final Map globalMessageDefinitions; + private final Map grpcServiceDefinitions; + + // All API Markdown blocks in grpc-services, grpc-messages, grpc-enums order. + private final List mdBlocks; + + // A true value will for an exception when a ProtobufDefinition does not have a description. + private final boolean failOnMissingDocumentation; + + public ProtobufDefinitionParser(TextBlockParser textBlockParser, boolean failOnMissingDocumentation) { + this.failOnMissingDocumentation = failOnMissingDocumentation; + this.globalEnumDefinitions = textBlockParser.getGlobalEnumDefinitions(); + this.globalMessageDefinitions = textBlockParser.getGlobalMessageDefinitions(); + this.grpcServiceDefinitions = textBlockParser.getGrpcServiceDefinitions(); + this.mdBlocks = new ArrayList<>(); + } + + /** + * Transform ProtobufDefinition pojos into Markdown blocks. + *

+ * Get the generated md blocks by calling getMdBlocks(). + */ + public void parse() { + parseLeadingBlocks(); + parseGrpcServiceBlocks(); + parseGrpcDependencyBlocks(); + Template errorsTemplate = new Template("errors.md"); + Block errorsBlock = StringBlock.builder().content(errorsTemplate.getContent()).build(); + mdBlocks.add(errorsBlock); + } + + /** + * Returns the complete list of Markdown content Blocks produced by the parse() + * method in the following order: grpc-services, grpc-message, grpc-enums. + */ + public List getMdBlocks() { + return this.mdBlocks; + } + + /** + * Transforms gRPC service definitions into Markdown blocks. + */ + private void parseGrpcServiceBlocks() { + List serviceBlocks = new ArrayList<>(); + grpcServiceDefinitions.forEach((key, serviceDefinition) -> { + String serviceDescription = serviceDefinition.description(); + if (shouldFail(serviceDescription)) + throw new IllegalStateException( + format("Cannot generate markdown because %s service description is missing.", + serviceDefinition.name())); + + Block serviceBlock = getServiceDefinitionBlock(serviceDefinition); + serviceBlocks.add(serviceBlock); + List rpcMethodBlocks = getRpcMethodBlocks(serviceDefinition); + serviceBlocks.addAll(rpcMethodBlocks); + }); + mdBlocks.addAll(serviceBlocks); + } + + /** + * Returns a Markdown block for gRPC service declaration. + */ + private Block getServiceDefinitionBlock(GrpcServiceDefinition serviceDefinition) { + String name = serviceDefinition.name(); + Map serviceTemplateVars = new HashMap<>() {{ + put("service.name", name); + put("service.description", serviceDefinition.description()); + }}; + Template serviceTemplate = new Template("grpc-service.md", + serviceTemplateVars, + failOnMissingDocumentation); + return StringBlock.builder().content(serviceTemplate.getContent()).build(); + } + + /** + * Returns list of Markdown blocks for an rpc method definition pojo. + *

+ * An rpc method declaration is one line specifying the method name, plus the request & response message names. + * For example: + *

+     * rpc GetMethodHelp (GetMethodHelpRequest) returns (GetMethodHelpReply)
+     * The rpc method name is "GetMethodHelp".
+     * The request message name is "GetMethodHelpRequest".
+     * The response message name is "GetMethodHelpReply".
+     * 
+ * This method returns md blocks for the method name, the request message, all the request message's enums and + * fields, the response message, and all the response message's enums and fields, in that order. + */ + private List getRpcMethodBlocks(GrpcServiceDefinition serviceDefinition) { + List rpcMethodBlocks = new ArrayList<>(); + Map rpcMethodDefinitions = serviceDefinition.rpcMethodDefinitions(); + List sortedRpcMethodDefinitions = rpcMethodDefinitions.values().stream() + .sorted(comparing(RpcMethodDefinition::name)) + .collect(Collectors.toList()); + sortedRpcMethodDefinitions.forEach(m -> { + rpcMethodBlocks.add(getRpcMethodBlock(m)); + rpcMethodBlocks.addAll(getRpcMessageBlocks(m)); + }); + return rpcMethodBlocks; + } + + /** + * Returns a Markdown block for the given rpc method definition with optional code examples. + */ + private StringBlock getRpcMethodBlock(RpcMethodDefinition methodDefinition) { + Map methodTemplateVars = new HashMap<>() {{ + put("method.name", methodDefinition.name()); + put("method.description", methodDefinition.description()); + }}; + Template rpcMethodTemplate = new Template("rpc-method.md", + methodTemplateVars, + failOnMissingDocumentation); + Optional examplesBlock = getRpcMethodCodeExamplesBlock(methodDefinition); + return examplesBlock.isPresent() + ? toBlockWithCodeExamples.apply(rpcMethodTemplate, examplesBlock.get()) + : StringBlock.builder().content(rpcMethodTemplate.getContent()).build(); + } + + /** + * Transforms an rpc method definition Template into a StringBlock containing the optional + * code examples StringBlock's content, inserted at the second line of the original content. + */ + private final BiFunction toBlockWithCodeExamples = (rpcMethodTemplate, examplesBlock) -> + StringBlock.builder().content(rpcMethodTemplate.getContentWithCodeExamples(examplesBlock.toMd())).build(); + + /** + * Return the optional rpc method definition code examples block, if present. + */ + private Optional getRpcMethodCodeExamplesBlock(RpcMethodDefinition rpcMethodDefinition) { + // Use working code in the java/python/shell examples directories. + CodeExamples codeExamples = new CodeExamples(rpcMethodDefinition); + if (codeExamples.exist()) { + StringBlock examplesBlock = StringBlock.builder().content(codeExamples.getContent()).build(); + return Optional.of(examplesBlock); + } else { + return Optional.empty(); + } + } + + /** + * Returns a list of Markdown blocks for an rpc method's request message, all the request message's enums + * and fields, the response message, and all the response message's enums and fields, in that order. + */ + private List getRpcMessageBlocks(RpcMethodDefinition rpcMethodDefinition) { + // All rpc methods have a request message and a response message. + List messageBlocks = new ArrayList<>(); + + // Append top level request msg declaration table block. + MessageDefinition requestMessageDefinition = rpcMethodDefinition.requestMessageDefinition(); + Block requestBlock = getRpcMessageBlock(requestMessageDefinition, REQUEST); + messageBlocks.add(requestBlock); + + // Append request msg enum and field table blocks. + messageBlocks.addAll(getRpcEnumAndMessageBlocks(requestMessageDefinition)); + + // Append top level response msg declaration table block. + MessageDefinition responseMessageDefinition = rpcMethodDefinition.responseMessageDefinition(); + Block responseBlock = getRpcMessageBlock(responseMessageDefinition, RESPONSE); + messageBlocks.add(responseBlock); + + // Append response msg enum and field table blocks. + messageBlocks.addAll(getRpcEnumAndMessageBlocks(responseMessageDefinition)); + + return messageBlocks; + } + + /** + * Returns list of Markdown blocks for an rpc message's enums and fields. + */ + private List getRpcEnumAndMessageBlocks(MessageDefinition rpcMessageDefinition) { + List messageBlocks = new ArrayList<>(); + // Append msg enum table blocks here. + if (!rpcMessageDefinition.enums().isEmpty()) { + List enumBlocks = getMessageEnumBlocks(rpcMessageDefinition); + messageBlocks.addAll(enumBlocks); + } + // Append msg field table blocks here. + if (!rpcMessageDefinition.fields().isEmpty()) { + Block requestFieldBlock = getFieldTableBlock(rpcMessageDefinition); + messageBlocks.add(requestFieldBlock); + } + return messageBlocks; + } + + /** + * Returns a Markdown block for an rpc request or response message. + */ + private StringBlock getRpcMessageBlock(MessageDefinition rpcMessageDefinition, + String rpcMessageType) { + if (invalidRpcMessageType.test(rpcMessageType)) + throw new IllegalArgumentException( + format("Invalid rpcMessageType param value '%s', must be one of %s or %s", + rpcMessageType, + REQUEST, + RESPONSE)); + + boolean hasFields = rpcMessageDefinition.fields().size() > 0; + String description = hasFields + ? rpcMessageDefinition.description() + : format("%s%nThis %s has no parameters.", rpcMessageDefinition.description(), rpcMessageType); + Map templateVars = new HashMap<>() {{ + put("rpc.message.type", rpcMessageType); + put("rpc.message.name", rpcMessageDefinition.name()); + put("rpc.message.description", description); + }}; + Template template = new Template("rpc-message.md", templateVars, failOnMissingDocumentation); + return StringBlock.builder().content(template.getContent()).build(); + } + + /** + * Returns a Markdown block for a global enum definition. + */ + private Block getGlobalEnumBlock(EnumDefinition enumDefinition) { + return getEnumTableBlock(enumDefinition, true); + } + + /** + * Returns a list of Markdown blocks containing all the (local) enums declared within a message. + */ + private List getMessageEnumBlocks(MessageDefinition messageDefinition) { + List enumBlocks = new ArrayList<>(); + messageDefinition.enums().values().forEach(enumDefinition -> { + Block mdBlock = getEnumTableBlock(enumDefinition, false); + enumBlocks.add(mdBlock); + }); + return enumBlocks; + } + + /** + * Returns a Markdown block for an enum with an md table of constants. + */ + private Block getEnumTableBlock(EnumDefinition enumDefinition, boolean isGlobal) { + Block enumConstantBlock = getEnumConstantTableBlock(enumDefinition); + Map templateVars = new HashMap<>() {{ + put("enum.name", enumDefinition.name()); + put("enum.description", enumDefinition.description()); + put("enum.constant.tbl", enumConstantBlock.toMd()); + }}; + String templateFileName = isGlobal ? "global-enum.md" : "message-enum.md"; + Template template = new Template(templateFileName, templateVars, failOnMissingDocumentation); + return StringBlock.builder().content(template.getContent()).build(); + } + + /** + * Returns a Markdown block containing a table of fields (name, type, description). + */ + private Block getFieldTableBlock(MessageDefinition messageDefinition) { + if (messageDefinition.fields().size() == 0) + throw new IllegalArgumentException("Cannot build Field md table block from empty field map."); + + return new FieldTableBlockBuilder(messageDefinition, failOnMissingDocumentation).build(); + } + + /** + * Cache fields and enums with custom data types, then transform them into Markdown blocks, and + * append those md blocks to the end of the API doc under sections "gRPC Messages" and "gRPC Enums". + */ + private void parseGrpcDependencyBlocks() { + parseGrpcMessagesHeaderBlock(); + + // Make sure all ProtobufDefinition dependencies are cached in the grpcMessageDefinitions + // list before creating any md field blocks. + GrpcDependencyCache grpcDependencyCache = new GrpcDependencyCache(globalEnumDefinitions, + globalMessageDefinitions, + grpcServiceDefinitions) + .load(); + parseGrpcMessageDependencies(grpcDependencyCache); // msg deps have to be parsed first + parseGrpcEnumsHeaderBlock(); + parseGrpcEnumDependencies(grpcDependencyCache); // enum deps are found in the msg deps + } + + private void parseGrpcMessagesHeaderBlock() { + Template template = new Template("grpc-messages.md"); + Block sectionTitleBlock = StringBlock.builder().content(template.getContent()).build(); + mdBlocks.add(sectionTitleBlock); + } + + private void parseGrpcMessageDependencies(GrpcDependencyCache dependencyCache) { + dependencyCache.getGrpcMessageDependencies().forEach(m -> { + Block messageBlock = getMessageBlock(m); + mdBlocks.add(messageBlock); + if (!m.fields().isEmpty()) { + Block fieldsBlock = getFieldTableBlock(m); + mdBlocks.add(fieldsBlock); + } + }); + } + + private void parseGrpcEnumsHeaderBlock() { + Template template = new Template("grpc-enums.md"); + StringBlock mdBlock = StringBlock.builder().content(template.getContent()).build(); + mdBlocks.add(mdBlock); + } + + private void parseGrpcEnumDependencies(GrpcDependencyCache dependencyCache) { + dependencyCache.getGrpcEnumDependencies().forEach(e -> { + log.debug("Create md block for {}", e.name()); + Block enumBlock = getGlobalEnumBlock(e); + mdBlocks.add(enumBlock); + }); + } + + private StringBlock getMessageBlock(MessageDefinition messageDefinition) { + Map templateVars = new HashMap<>() {{ + put("message.name", messageDefinition.name()); + put("message.description", messageDefinition.description()); + }}; + Template template = new Template("message.md", templateVars, failOnMissingDocumentation); + return StringBlock.builder().content(template.getContent()).build(); + } + + private Block getEnumConstantTableBlock(EnumDefinition enumDefinition) { + if (enumDefinition.constants().size() == 0) + throw new IllegalArgumentException("Cannot build Enum md table block without enum constants."); + + List rows = new ArrayList<>(); + List constants = enumDefinition.constants().values().stream().toList(); + constants.forEach(c -> { + SlateTableBlock.TableRow row = new SlateTableBlock.TableRow(); + row.setColumns(asList(c.name(), + Integer.toString(c.value()), + c.description())); + rows.add(row); + }); + return SlateTableBlock.builder() + .titles(asList(CONSTANT, VALUE, DESCRIPTION)) + .rows(rows) + .build(); + } + + private void parseLeadingBlocks() { + Template headerTemplate = new Template("header.md"); + Block headerBlock = StringBlock.builder() + .content(headerTemplate.getContent()) + .build(); + mdBlocks.add(headerBlock); + + Template introTemplate = new Template("introduction.md"); + Block introBlock = StringBlock.builder() + .content(introTemplate.getContent()) + .build(); + mdBlocks.add(introBlock); + + Template examplesSetupTemplate = new Template("examples-setup.md"); + Block examplesSetupBlock = StringBlock.builder() + .content(examplesSetupTemplate.getContent()) + .build(); + mdBlocks.add(examplesSetupBlock); + + Template authenticationTemplate = new Template("authentication.md"); + Block authenticationBlock = StringBlock.builder() + .content(authenticationTemplate.getContent()) + .build(); + mdBlocks.add(authenticationBlock); + } + + /** + * Return true if failOnMissingDocumentation is true and description is not specified. + * TODO Fully implement this fail-on-missing-comments rule only after .proto commenting + * has reached an advanced stage. + */ + private boolean shouldFail(String description) { + return failOnMissingDocumentation && (description == null || description.isBlank()); + } +} diff --git a/reference-doc-builder/src/main/java/bisq/apidoc/markdown/Template.java b/reference-doc-builder/src/main/java/bisq/apidoc/markdown/Template.java new file mode 100644 index 0000000..ecbbdb1 --- /dev/null +++ b/reference-doc-builder/src/main/java/bisq/apidoc/markdown/Template.java @@ -0,0 +1,245 @@ +/* + * This file is part of Bisq. + * + * Bisq is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bisq is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bisq. If not, see . + */ +package bisq.apidoc.markdown; + +import java.io.File; +import java.net.URISyntaxException; +import java.net.URL; +import java.nio.file.Path; +import java.util.*; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Collectors; + +import static bisq.apidoc.protobuf.ProtoParserUtil.toText; +import static java.lang.String.format; + +/** + * Loads a template MD file from the resources folder. + * Template file content can be transformed to a String, as is, or + * template contents can be transformed via variable resolution. + */ +public class Template { + + private static final String START_VARIABLE_DELIMITER = "{{"; + private static final String END_VARIABLE_DELIMITER = "}}"; + private static final Pattern VARIABLE_DELIMITER_PATTERN = Pattern.compile("\\{\\{([^}]+)}}"); + + private final Path templatePath; + private final Map templateValuesByName; + private final String rawContent; + // A fatal exception will be thrown if failOnMissingTemplateValue = true and a template + // value is not provided, else an empty string "" will be substituted for missing values. + private final boolean failOnMissingTemplateValue; + + /** + * Constructor for templates with no variable tokens. + */ + public Template(String templateName) { + this(templateName, new HashMap<>(), false); + } + + public Template(String templateName, + Map templateValuesByName, + boolean failOnMissingTemplateValue) { + this.templatePath = getPath(templateName); + this.templateValuesByName = templateValuesByName; + this.rawContent = toText.apply(templatePath); + this.failOnMissingTemplateValue = failOnMissingTemplateValue; + validateNumExpectedVariables(); + validateVariableValues(); + } + + public Path getTemplatePath() { + return this.templatePath; + } + + /** + * Returns template content as a String if there are no template variables to + * resolve, else returns content transformed by variable substitution. + * + * @return String + */ + public String getContent() { + return templateValuesByName.isEmpty() ? rawContent : getResolvedContent(); + } + + /** + * Return content transformed by variable substitution. + */ + private String getResolvedContent() { + validateTemplateVariableNames(); + return doVariableSubstitution(); + } + + /** + * Returns this template's resolved content with the given code examples text + * inserted at the second line of the original content. The code examples + * argument is inserted as is; no variable substitution is attempted on + * code snippets. + */ + public String getContentWithCodeExamples(String codeExamples) { + if (codeExamples == null || codeExamples.isBlank()) + throw new IllegalArgumentException("Code examples text is null or empty."); + + // Any necessary variable substitution happens before insertion. + String originalContent = getContent(); + StringBuilder contentBuilder = new StringBuilder(originalContent); + int codeInsertionIdx = contentBuilder.indexOf("\n") + 1; + contentBuilder.insert(codeInsertionIdx, codeExamples); + contentBuilder.append("\n"); + return contentBuilder.toString(); + } + + /** + * Return transformed template content. Provided values are substituted for + * template variable tokens, and template variable delimiters are removed. + */ + private String doVariableSubstitution() { + String variableNamesRegex = String.join("|", templateValuesByName.keySet()); + Pattern pattern = Pattern.compile(variableNamesRegex); + Matcher matcher = pattern.matcher(rawContent); + StringBuilder resolvedContentBuilder = new StringBuilder(); + while (matcher.find()) + matcher.appendReplacement(resolvedContentBuilder, templateValuesByName.get(matcher.group())); + + matcher.appendTail(resolvedContentBuilder); // copy remainder of input sequence + return removedVariableDelimiters(resolvedContentBuilder); + } + + /** + * Returns transformed template content stripped of all '{{' and '}}' template variable delimiters. + */ + private String removedVariableDelimiters(StringBuilder resolvedContentBuilder) { + String halfClean = resolvedContentBuilder.toString().replaceAll("\\{\\{", ""); + String allClean = halfClean.replaceAll("}}", ""); + return allClean.trim() + "\n\n"; + } + + /** + * Throws an exception if the variable names passed into the Template constructor + * do not exactly match the variable names found in the template (.md) file. + */ + private void validateTemplateVariableNames() { + // The expected variables names are those provided in the Template constructor. + List expectedVariableNames = templateValuesByName.keySet().stream() + .sorted() + .collect(Collectors.toList()); + List templateVariableNames = findVariableNamesInTemplate(); + boolean isMatch = expectedVariableNames.equals(templateVariableNames); + if (!isMatch) + throw new IllegalStateException(format("Variable names passed into the Template" + + " constructor do not match variable names found in template file %s.%n" + + "Compare your template file content with the templateValuesByName argument.", + templatePath.getFileName().toString())); + } + + /** + * Return list of all variable names found in the template (.md) file. + */ + private List findVariableNamesInTemplate() { + List variables = new ArrayList<>(); + Matcher m = VARIABLE_DELIMITER_PATTERN.matcher(rawContent); + while (m.find()) + variables.add(m.group(1).trim()); + + Collections.sort(variables); + return variables; + } + + /** + * Throws an exception if the number of variables passed into the Template constructor + * do not exactly match the number of variables tokens found in the template (.md) file. + */ + private void validateNumExpectedVariables() { + int numStartVarTokens = getVariableDelimiterCount(START_VARIABLE_DELIMITER); + int numEndVarTokens = getVariableDelimiterCount(END_VARIABLE_DELIMITER); + if ((numStartVarTokens != templateValuesByName.size()) || (numEndVarTokens != templateValuesByName.size())) + throw new IllegalArgumentException( + format("Incorrect templateValuesByName argument passed to Template constructor," + + " or template %s content is malformed.%n" + + "# of Template constructor templateValuesByName elements = %d%n" + + "# of template file's start variable delimiters = %d%n" + + "# of template file's end variable delimiters = %d%n", + templatePath.getFileName().toString(), + templateValuesByName.size(), + numStartVarTokens, + numEndVarTokens)); + } + + /** + * If class level field failOnMissingTemplateValue == true, an exception is thrown if any variable value passed + * into the Template constructor is null or blank. + *

+ * Else, if class level field failOnMissingTemplateValue == false, an empty string will be substituted for any + * null or blank variable passed into the Template constructor. + */ + private void validateVariableValues() { + for (Map.Entry varEntry : templateValuesByName.entrySet()) { + String name = varEntry.getKey(); + String value = varEntry.getValue(); + if (value == null || value.isBlank()) { + if (failOnMissingTemplateValue) { + String errMsg = format("Required %s template variable '%s' not specified.\n\tTemplate Vars: %s", + templatePath.getFileName().toString(), + name, + templateValuesByName); + throw new IllegalArgumentException(errMsg); + } else { + // Do not fail; substitute the empty string for the missing template value. + varEntry.setValue(""); + } + } + } + } + + /** + * Return the number of occurrences a variable token delimiter + * ('{{' or '}}') is found in the template (.md) file's raw content. + */ + private int getVariableDelimiterCount(String delimiter) { + int count = 0, index = 0; + while ((index = rawContent.indexOf(delimiter, index)) != -1) { + count++; + index++; + } + return count; + } + + /** + * Returns a Path object for the given template file name. + */ + private Path getPath(String templateName) { + File file = getFileFromResource(templateName); + return file.toPath(); + } + + /** + * Returns a File object for the given template file name. + */ + private File getFileFromResource(String templateName) { + URL resource = getClass().getClassLoader().getResource("templates" + File.separatorChar + templateName); + if (resource == null) { + throw new IllegalArgumentException("Resource template " + templateName + " not found."); + } + try { + return new File(resource.toURI()); + } catch (URISyntaxException ex) { + throw new IllegalArgumentException("Resource template " + templateName + " has bad URI syntax.", ex); + } + } +} diff --git a/reference-doc-builder/src/main/java/bisq/apidoc/protobuf/ProtoParserUtil.java b/reference-doc-builder/src/main/java/bisq/apidoc/protobuf/ProtoParserUtil.java new file mode 100644 index 0000000..1798fce --- /dev/null +++ b/reference-doc-builder/src/main/java/bisq/apidoc/protobuf/ProtoParserUtil.java @@ -0,0 +1,280 @@ +/* + * This file is part of Bisq. + * + * Bisq is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bisq is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bisq. If not, see . + */ +package bisq.apidoc.protobuf; + +import bisq.apidoc.protobuf.text.TextBlock; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Arrays; +import java.util.Optional; +import java.util.function.BiFunction; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import static bisq.apidoc.protobuf.text.TextBlock.TEXT_BLOCK_TYPE.LINE_COMMENT; +import static java.lang.String.format; +import static java.nio.charset.StandardCharsets.UTF_8; +import static java.util.Arrays.stream; + +// Note: Line comments appended to protobuf declarations ending in '{' will not be supported. +// Note: Single line block comments appended to any protobuf declarations will not be supported. +// None existed in pb.proto as of 2022-02-05. + +public class ProtoParserUtil { + + private static final String DEPRECATION_OPTION = "[deprecated = true]"; + private static final String LINE_COMMENT_SLASHES = "//"; + private static final String OPENING_CURLY_BRACE = "{"; + private static final String CLOSING_CURLY_BRACE = "}"; + + public static final String ONEOF_MESSAGE = "oneof message"; + + // TextBlock content indentation is determined by bracesLevelStack.size(). + static final char INDENT_CHAR = '\t'; + + // Regex patterns // + + public static final Pattern BLOCK_COMMENT_PATTERN = Pattern.compile("/\\*([^}]+)\\*/"); + public static final Pattern LICENSE_PATTERN = Pattern.compile("GNU Affero General Public License"); + public static final Pattern MAP_KV_TYPES_PATTERN = Pattern.compile("<([^}]+)>"); + public static final Pattern MESSAGE_FIELD_PATTERN = Pattern.compile("=\\s+\\d+"); + public static final Pattern PARENTHESES_PATTERN = Pattern.compile("\\(([^}]+)\\)"); + + // .proto file syntax, package, options (at top of file) pattern check functions // + + public static final Predicate isImportStatement = (line) -> line.startsWith("import"); + public static final Predicate isOptionDeclaration = (line) -> line.startsWith("option "); + public static final Predicate isPackageDeclaration = (line) -> line.startsWith("package "); + public static final Predicate isSyntaxDeclaration = (line) -> line.startsWith("syntax"); + + // Comment and blank line pattern check functions // + + public static final Predicate isBlankLine = String::isBlank; + public static final Predicate isLineComment = (line) -> line.startsWith(LINE_COMMENT_SLASHES); + public static final Predicate isBlockCommentOpener = (line) -> line.startsWith("/*"); + public static final Predicate isBlockCommentCloser = (line) -> line.endsWith("*/"); + public static final Predicate isComment = (line) -> isLineComment.test(line) || isBlockCommentOpener.test(line); + public static final Predicate hasAppendedLineComment = (line) -> !isLineComment.test(line) && line.contains(LINE_COMMENT_SLASHES); + + // Comment extraction functions // + + @SuppressWarnings("IndexOfReplaceableByContains") + public static final Function toCleanSingleLineComment = (comment) -> + comment.indexOf("// ") >= 0 + ? comment.replaceFirst("// ", "").trim() + : comment.substring(LINE_COMMENT_SLASHES.length()).trim(); + + + public static final Function toCleanLineComment = (comment) -> { + if (comment.contains("///")) + return comment; // Don't bother trying to clean up comments like this: ///////////// + + Predicate isMultiLineComment = (c) -> c.split("\n").length > 1; + if (isMultiLineComment.test(comment)) { + StringBuilder cleanBuilder = new StringBuilder(); + String[] lines = comment.split("\n"); + for (String line : lines) + cleanBuilder.append(toCleanSingleLineComment.apply(line)).append("\n"); + + return cleanBuilder.toString(); + } else { + return toCleanSingleLineComment.apply(comment); + } + }; + + public static final Function toCleanBlockComment = (comment) -> + comment.replace("/*", "") + .replace("*/", "") + .replace("*", "") + .trim(); + + public static final BiFunction, String> removeAppendedLineComment = (line, previousTextBlock) -> { + if (previousTextBlock.isEmpty()) { + return line; + } else { + var commentBlock = previousTextBlock.get(); + if (commentBlock.getTextBlockType().equals(LINE_COMMENT)) { + String comment = commentBlock.getText(); + return line.substring(0, line.indexOf(comment)); + } else { + throw new IllegalArgumentException(format("%s is not a LINE_COMMENT.", commentBlock)); + } + } + }; + + public static Optional getAppendedLineCommentBlock(String line) { + // Appended block comments will not be supported. + if (hasAppendedLineComment.test(line)) { + int appendedCommentIdx = line.indexOf(LINE_COMMENT_SLASHES); + return Optional.of(new TextBlock(line.substring(appendedCommentIdx), LINE_COMMENT)); + } else { + return Optional.empty(); + } + } + + // Protobuf declaration open and close ('{' and '}') pattern check functions. // + // All declarations are enclosed in curly braces, and nothing else can be, exception comment text. // + + public static final Predicate isDeclaringProtobufDefinition = (line) -> line.endsWith(OPENING_CURLY_BRACE); + public static final Predicate isClosingProtobufDefinition = (line) -> line.startsWith(CLOSING_CURLY_BRACE); + + // Deprecated field pattern check functions // + + public static final Predicate hasDeprecationOption = (line) -> line.contains(DEPRECATION_OPTION); + public static final Predicate isDeprecated = (line) -> line.contains(DEPRECATION_OPTION); + + // Enum pattern check functions // + + public static final Predicate isEnumDeclaration = (line) -> line.startsWith("enum ") && line.endsWith(OPENING_CURLY_BRACE); + public static final Predicate isEnumConstantDeclaration = (line) -> { + try { + // If the line can be parsed without an exception, it is an enum constant declaration. + boolean isDeprecatedConstant = isDeprecated.test(line); + String[] parts = getEnumConstantParts(line, isDeprecatedConstant); + getEnumConstantOrdinal(parts, isDeprecatedConstant); + return true; + } catch (Exception e) { + return false; + } + }; + + // Message pattern check functions // + + public static final Predicate isMessageFieldDeclaration = (line) -> + !isEnumDeclaration.test(line) + && line.split(" ").length >= 3 + && hasPattern(line, MESSAGE_FIELD_PATTERN); + public static final Predicate isMessageMapFieldDeclaration = (line) -> line.startsWith("map") && hasPattern(line, MAP_KV_TYPES_PATTERN); + public static final Predicate isOneOfMessageFieldDeclaration = (line) -> line.startsWith(ONEOF_MESSAGE) && line.endsWith(OPENING_CURLY_BRACE); + public static final Predicate isMessageDeclaration = (line) -> line.startsWith("message ") && line.endsWith(OPENING_CURLY_BRACE); + public static final Predicate isReservedFieldDeclaration = (line) -> line.startsWith("reserved "); + + // Returns true if a protobuf field type is one of the standard protobuffer scalar types + // as defined in https://developers.google.com/protocol-buffers/docs/proto3#scalar. + public static final Predicate isScalarDataType = (dataType) -> + stream(ProtobufDataType.values()).anyMatch(t -> t.formalName.equals(dataType)); + + // gRPC service and rpc method pattern check functions // + + public static final Predicate isGrpcServiceDeclaration = (line) -> line.startsWith("service ") && line.endsWith(OPENING_CURLY_BRACE); + public static final Predicate isRpcMethodDeclaration = (line) -> line.startsWith("rpc ") && line.endsWith(OPENING_CURLY_BRACE); + + + // Pattern matching utils // + + public static boolean hasPattern(String string, Pattern pattern) { + return pattern.matcher(string).find(); + } + + public static Optional findFirstMatch(String string, Pattern pattern) { + Matcher m = pattern.matcher(string); + if (m.find()) { + return Optional.of(m.group(1)); + } else { + return Optional.empty(); + } + } + + // String manipulation utils // + + public static String indentedText(String trimmedText, int baseLevel) { + if (baseLevel == 0) + return trimmedText + "\n"; + + char[] baseIndentation = new char[baseLevel]; + Arrays.fill(baseIndentation, INDENT_CHAR); + String[] lines = trimmedText.split("\n"); + StringBuilder stringBuilder = new StringBuilder(); + Arrays.stream(lines).forEach(l -> { + stringBuilder.append(new String(baseIndentation)); + stringBuilder.append(l); + stringBuilder.append("\n"); + }); + return stringBuilder.toString(); + } + + public static String stripSemicolon(String line) { + int semicolonIndex = line.indexOf(";"); + return semicolonIndex >= 0 + ? line.substring(0, semicolonIndex) + : line; + } + + public static String stripDeprecatedOption(String line) { + int optionIndex = line.indexOf(DEPRECATION_OPTION); + // Note this will also remove a trailing semicolon if it exists. + return optionIndex >= 0 + ? line.substring(0, optionIndex) + : line; + } + + public static final Function toText = (filePath) -> { + try { + byte[] bytes = Files.readAllBytes(filePath); + return new String(bytes, UTF_8); + } catch (IOException ex) { + throw new IllegalArgumentException(format("Could not convert content of file %s to text.", filePath)); + } + }; + + // Protobuf definition text parsing and manipulation utils // + + public static String[] getEnumConstantParts(String line, boolean isDeprecated) { + return isDeprecated ? line.split(" ") : line.split("="); + } + + public static int getEnumConstantOrdinal(String[] parts, boolean isDeprecated) { + return isDeprecated + ? Integer.parseInt(parts[2].trim()) + : Integer.parseInt(stripSemicolon(parts[1].trim())); + } + + public static String[] getMapKeyValueTypes(String line) { + try { + Optional keyValueDataTypes = findFirstMatch(line, MAP_KV_TYPES_PATTERN); + if (keyValueDataTypes.isPresent()) { + String[] kvTypes = keyValueDataTypes.get().split(","); + String keyType = kvTypes[0].trim(); + String valueType = kvTypes[1].trim(); + return new String[]{keyType, valueType}; + } else { + throw new IllegalStateException(format("Did not find map field type declaration in line: %s", line)); + } + } catch (Exception ex) { + throw new IllegalStateException(format("Could not parse msg map field kv data types for line: %s", line)); + } + } + + public static String[] getMapFieldNameAndFieldNumberParts(String line, boolean isDeprecated) { + String rawNameAndNumber = line.substring(line.indexOf(">") + 1); + return isDeprecated + ? stripDeprecatedOption(rawNameAndNumber).split("=") + : stripSemicolon(rawNameAndNumber).split("="); + } + + @SuppressWarnings("OptionalUsedAsFieldOrParameterType") + public static String getCleanBlockText(String line, Optional appendedLineCommentTextBlock) { + // Returns the given line without an appended comment, if present. + return appendedLineCommentTextBlock.isPresent() + ? removeAppendedLineComment.apply(line, appendedLineCommentTextBlock) + : line; + } +} diff --git a/reference-doc-builder/src/main/java/bisq/apidoc/protobuf/ProtobufDataType.java b/reference-doc-builder/src/main/java/bisq/apidoc/protobuf/ProtobufDataType.java new file mode 100644 index 0000000..65ddc8b --- /dev/null +++ b/reference-doc-builder/src/main/java/bisq/apidoc/protobuf/ProtobufDataType.java @@ -0,0 +1,45 @@ +/* + * This file is part of Bisq. + * + * Bisq is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bisq is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bisq. If not, see . + */ +package bisq.apidoc.protobuf; + +/** + * Valid protobuf v3 field data type definitions from + * https://developers.google.com/protocol-buffers/docs/proto3#scalar. + */ +public enum ProtobufDataType { + + DOUBLE("double"), + FLOAT("float"), + INT32("int32"), + INT64("int64"), + UINT64("uint64"), + SINT32("sint32"), + SINT64("sint64"), + FIXED32("fixed32"), + FIXED64("fixed64"), + SFIXED32("sfixed32"), + SFIXED64("sfixed64"), + BOOL("bool"), + STRING("string"), + BYTES("bytes"); + + public final String formalName; + + ProtobufDataType(String formalName) { + this.formalName = formalName; + } +} diff --git a/reference-doc-builder/src/main/java/bisq/apidoc/protobuf/definition/EnumConstantDefinition.java b/reference-doc-builder/src/main/java/bisq/apidoc/protobuf/definition/EnumConstantDefinition.java new file mode 100644 index 0000000..a322610 --- /dev/null +++ b/reference-doc-builder/src/main/java/bisq/apidoc/protobuf/definition/EnumConstantDefinition.java @@ -0,0 +1,78 @@ +/* + * This file is part of Bisq. + * + * Bisq is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bisq is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bisq. If not, see . + */ +package bisq.apidoc.protobuf.definition; + +import java.util.Objects; + +public final class EnumConstantDefinition implements ProtobufDefinition { + + private final String name; + private final int value; + private final String description; + private final boolean isDeprecated; + + public EnumConstantDefinition(String name, + int value, + String description, + boolean isDeprecated) { + this.name = name; + this.value = value; + this.description = description; + this.isDeprecated = isDeprecated; + } + + public String name() { + return name; + } + + public int value() { + return value; + } + + public String description() { + return description; + } + + public boolean isDeprecated() { + return isDeprecated; + } + + @Override + public boolean equals(Object obj) { + if (obj == this) return true; + if (obj == null || obj.getClass() != this.getClass()) return false; + var that = (EnumConstantDefinition) obj; + return Objects.equals(this.name, that.name) && + this.value == that.value && + Objects.equals(this.description, that.description) && + this.isDeprecated == that.isDeprecated; + } + + @Override + public int hashCode() { + return Objects.hash(name, value, description, isDeprecated); + } + + @Override + public String toString() { + return "EnumConstantDefinition[" + + "name=" + name + ", " + + "value=" + value + ", " + + "description=" + description + ", " + + "isDeprecated=" + isDeprecated + ']'; + } +} diff --git a/reference-doc-builder/src/main/java/bisq/apidoc/protobuf/definition/EnumDefinition.java b/reference-doc-builder/src/main/java/bisq/apidoc/protobuf/definition/EnumDefinition.java new file mode 100644 index 0000000..8b0bdb1 --- /dev/null +++ b/reference-doc-builder/src/main/java/bisq/apidoc/protobuf/definition/EnumDefinition.java @@ -0,0 +1,80 @@ +/* + * This file is part of Bisq. + * + * Bisq is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bisq is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bisq. If not, see . + */ +package bisq.apidoc.protobuf.definition; + +import java.util.Map; +import java.util.Objects; + +@SuppressWarnings("ClassCanBeRecord") +public final class EnumDefinition implements ProtobufDefinition { + + private final String name; + private final Map constants; + private final String description; + private final boolean isGlobal; + + public EnumDefinition(String name, + Map constants, + String description, + boolean isGlobal) { + this.name = name; + this.constants = constants; + this.description = description; + this.isGlobal = isGlobal; + } + + public String name() { + return this.name; + } + + public Map constants() { + return this.constants; + } + + public String description() { + return this.description; + } + + public boolean isGlobal() { + return this.isGlobal; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof EnumDefinition that)) return false; + return isGlobal == that.isGlobal + && name.equals(that.name) + && constants.equals(that.constants) + && description.equals(that.description); + } + + @Override + public int hashCode() { + return Objects.hash(name, constants, description, isGlobal); + } + + @Override + public String toString() { + return "EnumDefinition{" + + "name='" + name + '\'' + "\n" + + ", constants=" + constants + "\n" + + ", description='" + description + '\'' + + ", isGlobal=" + isGlobal + + '}'; + } +} diff --git a/reference-doc-builder/src/main/java/bisq/apidoc/protobuf/definition/FieldDefinition.java b/reference-doc-builder/src/main/java/bisq/apidoc/protobuf/definition/FieldDefinition.java new file mode 100644 index 0000000..c25f50f --- /dev/null +++ b/reference-doc-builder/src/main/java/bisq/apidoc/protobuf/definition/FieldDefinition.java @@ -0,0 +1,154 @@ +/* + * This file is part of Bisq. + * + * Bisq is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bisq is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bisq. If not, see . + */ +package bisq.apidoc.protobuf.definition; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +public class FieldDefinition implements ProtobufDefinition { + + protected final boolean isRepeated; + protected final String type; + protected final String name; + protected final int fieldNumber; + protected final String description; + protected final boolean isDeprecated; // Only fields can be @Deprecated, not messages nor services. + protected boolean isReserved; + + // Oneof fields cannot be repeated: https://developers.google.com/protocol-buffers/docs/proto3#oneof + private final List oneOfFieldChoices; + + /** + * Constructor for "One Of" message fields. + */ + public FieldDefinition(List oneOfFieldChoices, + int fieldNumber, + String description, + boolean isDeprecated) { + this(false, null, null, fieldNumber, description, isDeprecated); + this.oneOfFieldChoices.addAll(oneOfFieldChoices); + } + + /** + * Constructor for reserved protobuf message fields, e.g., 'reserved 3;'. + */ + public FieldDefinition(int fieldNumber, + String description) { + this(false, null, null, fieldNumber, description, false); + this.isReserved = true; + } + + /** + * Constructor for standard fields, but may be used by the MapFieldDefinition subclass. + */ + public FieldDefinition(boolean isRepeated, + String type, + String name, + int fieldNumber, + String description, + boolean isDeprecated) { + this.isRepeated = isRepeated; + this.type = type; + this.name = name; + this.fieldNumber = fieldNumber; + this.description = description; + this.isDeprecated = isDeprecated; + this.oneOfFieldChoices = new ArrayList<>(); + } + + /////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // Getter method names are not prefixed by 'get' to conform to other implicate 'record' getters in this package. + /////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + public boolean isRepeated() { + return this.isRepeated; + } + + public String type() { + return this.type; + } + + public String name() { + return this.name; + } + + public int fieldNumber() { + return this.fieldNumber; + } + + public String description() { + return this.description; + } + + public boolean isDeprecated() { + return this.isDeprecated; + } + + public List oneOfFieldChoices() { + return this.oneOfFieldChoices; + } + + public boolean isReserved() { + return this.isReserved; + } + + public boolean isOneOfMessageField() { + return this.oneOfFieldChoices.size() > 0; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + FieldDefinition that = (FieldDefinition) o; + return isRepeated == that.isRepeated + && fieldNumber == that.fieldNumber + && isDeprecated == that.isDeprecated + && isReserved == that.isReserved + && type.equals(that.type) + && name.equals(that.name) + && description.equals(that.description) + && oneOfFieldChoices.equals(that.oneOfFieldChoices); + } + + @Override + public int hashCode() { + return Objects.hash(isRepeated, + type, + name, + fieldNumber, + description, + isDeprecated, + isReserved, + oneOfFieldChoices); + } + + @Override + public String toString() { + return "FieldDefinition{" + + "isRepeated=" + isRepeated + + ", type='" + type + '\'' + + ", name='" + name + '\'' + + ", fieldNumber=" + fieldNumber + + ", description='" + description + '\'' + + ", isDeprecated=" + isDeprecated + + ", isReserved=" + isReserved + + ", oneOfFieldChoices=" + oneOfFieldChoices + + '}'; + } +} diff --git a/reference-doc-builder/src/main/java/bisq/apidoc/protobuf/definition/GrpcServiceDefinition.java b/reference-doc-builder/src/main/java/bisq/apidoc/protobuf/definition/GrpcServiceDefinition.java new file mode 100644 index 0000000..2e4f2fe --- /dev/null +++ b/reference-doc-builder/src/main/java/bisq/apidoc/protobuf/definition/GrpcServiceDefinition.java @@ -0,0 +1,73 @@ +/* + * This file is part of Bisq. + * + * Bisq is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bisq is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bisq. If not, see . + */ +package bisq.apidoc.protobuf.definition; + +import java.util.Map; +import java.util.Objects; + +/** + * A GrpcServiceDefinition is a group of protobuf RpcServiceDefinitions. + */ +public final class GrpcServiceDefinition implements ProtobufDefinition { + + private final String name; + private final Map rpcMethodDefinitions; + private final String description; + + public GrpcServiceDefinition(String name, + Map rpcMethodDefinitions, + String description) { + this.name = name; + this.rpcMethodDefinitions = rpcMethodDefinitions; + this.description = description; + } + + public String name() { + return name; + } + + public Map rpcMethodDefinitions() { + return rpcMethodDefinitions; + } + + public String description() { + return description; + } + + @Override + public boolean equals(Object obj) { + if (obj == this) return true; + if (obj == null || obj.getClass() != this.getClass()) return false; + var that = (GrpcServiceDefinition) obj; + return Objects.equals(this.name, that.name) && + Objects.equals(this.rpcMethodDefinitions, that.rpcMethodDefinitions) && + Objects.equals(this.description, that.description); + } + + @Override + public int hashCode() { + return Objects.hash(name, rpcMethodDefinitions, description); + } + + @Override + public String toString() { + return "GrpcServiceDefinition[" + + "name=" + name + ", " + + "rpcMethodDefinitions=" + rpcMethodDefinitions + ", " + + "description=" + description + ']'; + } +} diff --git a/reference-doc-builder/src/main/java/bisq/apidoc/protobuf/definition/MapFieldDefinition.java b/reference-doc-builder/src/main/java/bisq/apidoc/protobuf/definition/MapFieldDefinition.java new file mode 100644 index 0000000..71816ef --- /dev/null +++ b/reference-doc-builder/src/main/java/bisq/apidoc/protobuf/definition/MapFieldDefinition.java @@ -0,0 +1,71 @@ +/* + * This file is part of Bisq. + * + * Bisq is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bisq is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bisq. If not, see . + */ +package bisq.apidoc.protobuf.definition; + +import java.util.Objects; + +public class MapFieldDefinition extends FieldDefinition implements ProtobufDefinition { + + // Map fields are not repeatable. + private final String keyType; + private final String valueType; + + public MapFieldDefinition(String keyType, + String valueType, + String name, + int fieldNumber, + String description, + boolean isDeprecated) { + super(false, null, name, fieldNumber, description, isDeprecated); + this.keyType = keyType; + this.valueType = valueType; + } + + public String keyType() { + return keyType; + } + + public String valueType() { + return valueType; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof MapFieldDefinition)) return false; + if (!super.equals(o)) return false; + MapFieldDefinition that = (MapFieldDefinition) o; + return keyType.equals(that.keyType) && valueType.equals(that.valueType); + } + + @Override + public int hashCode() { + return Objects.hash(super.hashCode(), keyType, valueType); + } + + @Override + public String toString() { + return "MapFieldDefinition{" + + "name='" + name + '\'' + + ", fieldNumber=" + fieldNumber + + ", description='" + description + '\'' + + ", isDeprecated=" + isDeprecated + + ", keyType='" + keyType + '\'' + + ", valueType='" + valueType + '\'' + + '}'; + } +} diff --git a/reference-doc-builder/src/main/java/bisq/apidoc/protobuf/definition/MessageDefinition.java b/reference-doc-builder/src/main/java/bisq/apidoc/protobuf/definition/MessageDefinition.java new file mode 100644 index 0000000..62c7306 --- /dev/null +++ b/reference-doc-builder/src/main/java/bisq/apidoc/protobuf/definition/MessageDefinition.java @@ -0,0 +1,96 @@ +/* + * This file is part of Bisq. + * + * Bisq is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bisq is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bisq. If not, see . + */ +package bisq.apidoc.protobuf.definition; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +public final class MessageDefinition implements ProtobufDefinition { + + private final String name; + private final Map enums; + private final Map fields; + private final String description; + + public MessageDefinition(String name, + Map enums, + Map fields, + String description) { + this.name = name; + this.enums = enums; + this.fields = fields; + this.description = description; + } + + public String name() { + return name; + } + + public Map enums() { + return enums; + } + + public Map fields() { + return fields; + } + + public String description() { + return description; + } + + public boolean hasOneOfField() { + return fields.values().stream().anyMatch(FieldDefinition::isOneOfMessageField); + } + + public List getOneOfFieldChoices() { + if (!hasOneOfField()) + return new ArrayList<>(); + + FieldDefinition oneOfField = fields.values().stream() + .filter(FieldDefinition::isOneOfMessageField) + .findFirst().get(); + return oneOfField.oneOfFieldChoices(); + } + + @Override + public boolean equals(Object obj) { + if (obj == this) return true; + if (obj == null || obj.getClass() != this.getClass()) return false; + var that = (MessageDefinition) obj; + return Objects.equals(this.name, that.name) && + Objects.equals(this.enums, that.enums) && + Objects.equals(this.fields, that.fields) && + Objects.equals(this.description, that.description); + } + + @Override + public int hashCode() { + return Objects.hash(name, enums, fields, description); + } + + @Override + public String toString() { + return "MessageDefinition[" + + "name=" + name + ", " + + "enums=" + enums + ", " + + "fields=" + fields + ", " + "\n" + + "hasOneOfField=" + hasOneOfField() + ", " + + "description=" + description + ']'; + } +} diff --git a/reference-doc-builder/src/main/java/bisq/apidoc/protobuf/definition/ProtobufDefinition.java b/reference-doc-builder/src/main/java/bisq/apidoc/protobuf/definition/ProtobufDefinition.java new file mode 100644 index 0000000..760dd89 --- /dev/null +++ b/reference-doc-builder/src/main/java/bisq/apidoc/protobuf/definition/ProtobufDefinition.java @@ -0,0 +1,43 @@ +/* + * This file is part of Bisq. + * + * Bisq is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bisq is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bisq. If not, see . + */ +package bisq.apidoc.protobuf.definition; + +/** + * Marker interface for pojos -- much easier to transform into markdown context than chunks of text. + */ +public interface ProtobufDefinition { + + String name(); + + String description(); + + default boolean hasDescription() { + return this.description() != null && !this.description().isBlank(); + } + + default boolean isEnum() { + return this instanceof EnumDefinition; + } + + default boolean isMessage() { + return this instanceof MessageDefinition; + } + + default boolean isMapField() { + return this instanceof MapFieldDefinition; + } +} diff --git a/reference-doc-builder/src/main/java/bisq/apidoc/protobuf/definition/RpcMethodDefinition.java b/reference-doc-builder/src/main/java/bisq/apidoc/protobuf/definition/RpcMethodDefinition.java new file mode 100644 index 0000000..29c97e6 --- /dev/null +++ b/reference-doc-builder/src/main/java/bisq/apidoc/protobuf/definition/RpcMethodDefinition.java @@ -0,0 +1,79 @@ +/* + * This file is part of Bisq. + * + * Bisq is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bisq is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bisq. If not, see . + */ +package bisq.apidoc.protobuf.definition; + +import java.util.Objects; + +@SuppressWarnings("ClassCanBeRecord") +public final class RpcMethodDefinition implements ProtobufDefinition { + + private final String name; + private final MessageDefinition requestMessageDefinition; + private final MessageDefinition responseMessageDefinition; + private final String description; + + public RpcMethodDefinition(String name, + MessageDefinition requestMessageDefinition, + MessageDefinition responseMessageDefinition, + String description) { + this.name = name; + this.requestMessageDefinition = requestMessageDefinition; + this.responseMessageDefinition = responseMessageDefinition; + this.description = description; + } + + public String name() { + return name; + } + + public MessageDefinition requestMessageDefinition() { + return requestMessageDefinition; + } + + public MessageDefinition responseMessageDefinition() { + return responseMessageDefinition; + } + + public String description() { + return description; + } + + @Override + public boolean equals(Object obj) { + if (obj == this) return true; + if (obj == null || obj.getClass() != this.getClass()) return false; + var that = (RpcMethodDefinition) obj; + return Objects.equals(this.name, that.name) && + Objects.equals(this.requestMessageDefinition, that.requestMessageDefinition) && + Objects.equals(this.responseMessageDefinition, that.responseMessageDefinition) && + Objects.equals(this.description, that.description); + } + + @Override + public int hashCode() { + return Objects.hash(name, requestMessageDefinition, responseMessageDefinition, description); + } + + @Override + public String toString() { + return "RpcMethodDefinition[" + + "name=" + name + ", " + + "requestMessageDefinition=" + requestMessageDefinition + ", " + + "responseMessageDefinition=" + responseMessageDefinition + ", " + + "description=" + description + ']'; + } +} diff --git a/reference-doc-builder/src/main/java/bisq/apidoc/protobuf/definition/TextBlockParser.java b/reference-doc-builder/src/main/java/bisq/apidoc/protobuf/definition/TextBlockParser.java new file mode 100644 index 0000000..1afb273 --- /dev/null +++ b/reference-doc-builder/src/main/java/bisq/apidoc/protobuf/definition/TextBlockParser.java @@ -0,0 +1,349 @@ +/* + * This file is part of Bisq. + * + * Bisq is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bisq is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bisq. If not, see . + */ +package bisq.apidoc.protobuf.definition; + +import bisq.apidoc.protobuf.text.TextBlock; +import lombok.extern.slf4j.Slf4j; + +import java.util.*; + +import static bisq.apidoc.protobuf.ProtoParserUtil.*; +import static bisq.apidoc.protobuf.text.TextBlock.TEXT_BLOCK_TYPE.*; +import static java.lang.String.format; + +/** + * Transforms lists of TextBlocks into ProtobufDefinition objects used to generate the final API doc markdown file. + */ +@SuppressWarnings({"rawtypes", "unchecked", "OptionalUsedAsFieldOrParameterType"}) +@Slf4j +public class TextBlockParser { + + private final Map globalEnumDefinitions = new TreeMap<>(); + private final Map globalMessageDefinitions = new TreeMap<>(); + private final Map grpcServiceDefinitions = new TreeMap<>(); + + private final List pbProtoTextBlocks; + private final List grpcProtoTextBlocks; + @SuppressWarnings("FieldCanBeLocal") + private final boolean failOnMissingDocumentation; + + public TextBlockParser(List pbProtoTextBlocks, List grpcProtoTextBlocks, boolean failOnMissingDocumentation) { + this.pbProtoTextBlocks = pbProtoTextBlocks; + this.grpcProtoTextBlocks = grpcProtoTextBlocks; + this.failOnMissingDocumentation = failOnMissingDocumentation; + } + + public void parse() { + // The pb.proto TextBlocks must be parsed first because grpc.proto definitions depend on them. + parse(pbProtoTextBlocks); + // The grpc.proto enum and message TextBlocks must be parsed before the grpc service TextBlocks. + parse(grpcProtoTextBlocks, false); + // The grpc.proto enum and message TextBlocks have been cached for lookup, now parse the grpc service blocks. + parse(grpcProtoTextBlocks, true); + } + + public Map getGlobalEnumDefinitions() { + return globalEnumDefinitions; + } + + public Map getGlobalMessageDefinitions() { + return globalMessageDefinitions; + } + + public Map getGrpcServiceDefinitions() { + return grpcServiceDefinitions; + } + + private void parse(List textBlocks) { + parse(textBlocks, false); + } + + private void parse(List textBlocks, boolean onlyParseGrpcServiceTextBlocks) { + for (int i = 0; i < textBlocks.size(); i++) { + TextBlock current = textBlocks.get(i); + + if (isHeaderTextBlock(current)) + continue; // skip header line + + Optional descriptionTextBlock; + + if (!onlyParseGrpcServiceTextBlocks && current.isEnumDeclaration()) { + descriptionTextBlock = getProtoDescriptionTextBlock(current, textBlocks.get(i - 1)); + parseEnumTextBlock(current, descriptionTextBlock); + } else if (!onlyParseGrpcServiceTextBlocks && current.isMessageDeclaration()) { + descriptionTextBlock = getProtoDescriptionTextBlock(current, textBlocks.get(i - 1)); + parseMessageTextBlock(current, descriptionTextBlock); + } else if (onlyParseGrpcServiceTextBlocks && current.isGrpcServiceDeclaration()) { + descriptionTextBlock = getProtoDescriptionTextBlock(current, textBlocks.get(i - 1)); + parseGrpcServiceTextBlock(current, descriptionTextBlock); + } + + // Else no-op: nested comments, rpc methods, messages, etc. + // are consumed by high level protobuf TextBlock parsers. + } + } + + private Optional getProtoDescriptionTextBlock(TextBlock current, TextBlock previous) { + return current.getTextBlockType().equals(PROTO_DEFINITION) && previous.isCommentBlock() + ? Optional.of(previous) + : Optional.empty(); + } + + private void parseEnumTextBlock(TextBlock enumTextBlock, + Optional descriptionTextBlock) { + EnumDefinition enumDefinition = getEnumDefinition(enumTextBlock, descriptionTextBlock, true); + globalEnumDefinitions.put(enumDefinition.name(), enumDefinition); + } + + private void parseMessageTextBlock(TextBlock messageTextBlock, + Optional descriptionTextBlock) { + String messageName = messageTextBlock.getProtobufDefinitionName(); + String description = getDescription(descriptionTextBlock); + Map enums = new LinkedHashMap(); + Map fields = new LinkedHashMap(); + + List children = messageTextBlock.getChildren(); + for (int i = 0; i < children.size(); i++) { + + ProtoAndDescriptionPair pair = new ProtoAndDescriptionPair(children, i); + // Bump children iteration idx if we just consumed two TextBlock list elements. + if (pair.optionalDescription.isPresent()) + i++; + + String fieldDescription = getDescription(pair.optionalDescription); + if (pair.proto.isEnumDeclaration()) { + EnumDefinition enumDefinition = getEnumDefinition(pair.proto, pair.optionalDescription, false); + enums.put(enumDefinition.name(), enumDefinition); + } else if (pair.proto.isMessageMapField()) { + MapFieldDefinition mapFieldDefinition = parseMapFieldDefinition(pair.proto.getText()); + fields.put(mapFieldDefinition.name(), mapFieldDefinition); + } else if (pair.proto.isOneOfMessageDeclaration()) { + FieldDefinition oneOfFieldDefinition = parseOneOfFieldDefinition(pair.proto, fieldDescription); + fields.put(ONEOF_MESSAGE, oneOfFieldDefinition); + } else if (pair.proto.isReservedField()) { + FieldDefinition fieldDefinition = parseReservedFieldDefinition(pair.proto.getText(), fieldDescription); + // TODO calculate and lookup reserved field "name" for markdown generation. + fields.put("reserved_field_number_" + fieldDefinition.fieldNumber(), fieldDefinition); + } else if (pair.proto.isMessageField()) { + FieldDefinition fieldDefinition = parseStandardFieldDefinition(pair.proto.getText(), fieldDescription); + fields.put(fieldDefinition.name(), fieldDefinition); + } + } + MessageDefinition messageDefinition = new MessageDefinition(messageName, enums, fields, description); + globalMessageDefinitions.put(messageName, messageDefinition); + } + + private void parseGrpcServiceTextBlock(TextBlock serviceTextBlock, + Optional descriptionTextBlock) { + String serviceName = serviceTextBlock.getProtobufDefinitionName(); + String description = getDescription(descriptionTextBlock); + Map rpcMethods = new LinkedHashMap(); + + List children = serviceTextBlock.getChildren(); + for (int i = 0; i < children.size(); i++) { + ProtoAndDescriptionPair pair = new ProtoAndDescriptionPair(children, i); + // Bump children iteration idx if we just consumed two TextBlock list elements. + if (pair.optionalDescription.isPresent()) + i++; + + String rpcMethodDescription = getDescription(pair.optionalDescription); + if (pair.proto.isRpcMethodDeclaration()) { + RpcMethodDefinition rpcMethodDefinition = getRpcMethodDefinition(pair.proto, rpcMethodDescription); + rpcMethods.put(rpcMethodDefinition.name(), rpcMethodDefinition); + } + } + GrpcServiceDefinition serviceDefinition = new GrpcServiceDefinition(serviceName, rpcMethods, description); + grpcServiceDefinitions.put(serviceName, serviceDefinition); + } + + private RpcMethodDefinition getRpcMethodDefinition(TextBlock rpcMethodTextBlock, String description) { + String methodName = rpcMethodTextBlock.getProtobufDefinitionName(); + String line = rpcMethodTextBlock.getText(); + String[] parts = rpcMethodTextBlock.getText().split(" "); + String requestName = findFirstMatch(parts[2], PARENTHESES_PATTERN).orElseThrow(() -> + new IllegalStateException(format("Could not parse rpc request method name from line: %s", line))); + MessageDefinition requestMessage = findMessageDefinition(requestName); + String responseName = findFirstMatch(parts[4], PARENTHESES_PATTERN).orElseThrow(() -> + new IllegalStateException(format("Could not parse rpc response method name from line: %s", line))); + MessageDefinition responseMessage = findMessageDefinition(responseName); + return new RpcMethodDefinition(methodName, requestMessage, responseMessage, description); + } + + private FieldDefinition parseOneOfFieldDefinition(TextBlock oneOfFieldTextBlock, String description) { + List fieldChoices = new ArrayList<>(); + List children = oneOfFieldTextBlock.getChildren(); + for (int i = 0; i < children.size(); i++) { + ProtoAndDescriptionPair pair = new ProtoAndDescriptionPair(children, i); + // Bump children iteration idx if we just consumed two TextBlock list elements. + if (pair.optionalDescription.isPresent()) + i++; + + if (!pair.proto.isMessageField()) + throw new IllegalStateException(format("Could not parse oneof msg field definition block: %s", pair.proto)); + + String fieldChoiceDescription = getDescription(pair.optionalDescription); + FieldDefinition fieldDefinition = parseStandardFieldDefinition(pair.proto.getText(), fieldChoiceDescription); + fieldChoices.add(fieldDefinition); + } + boolean isDeprecatedFieldChoice = hasDeprecationOption.test(oneOfFieldTextBlock.getText()); + return new FieldDefinition(fieldChoices, + -1, + description, + isDeprecatedFieldChoice); + } + + private MapFieldDefinition parseMapFieldDefinition(String line) { + try { + Optional keyValueDataTypes = findFirstMatch(line, MAP_KV_TYPES_PATTERN); + if (keyValueDataTypes.isPresent()) { + String[] kvTypes = keyValueDataTypes.get().split(","); + String keyType = kvTypes[0].trim(); + String valueType = kvTypes[1].trim(); + boolean isDeprecated = hasDeprecationOption.test(line); + String[] nameAndFieldNumberParts = getMapFieldNameAndFieldNumberParts(line, isDeprecated); + String name = nameAndFieldNumberParts[0].trim(); + int fieldNumber = Integer.parseInt(nameAndFieldNumberParts[1].trim()); + return new MapFieldDefinition(keyType, valueType, name, fieldNumber, "todo", isDeprecated); + } else { + throw new RuntimeException(format("Could not parse msg map field kv data types at line: %s", line)); + } + } catch (ArrayIndexOutOfBoundsException ex) { + throw new RuntimeException(format("Could not parse msg field definition at line: %s", line), ex); + } catch (NumberFormatException ex) { + throw new RuntimeException(format("Could not parse msg field number at line: %s", line), ex); + } + } + + private FieldDefinition parseReservedFieldDefinition(String line, String fieldDescription) { + if (!isReservedFieldDeclaration.test(line)) + throw new IllegalArgumentException("Line argument does not represent a reserved field declaration: " + line); + + // Reserved fields have no name, just the 'reserved' + // keyword followed by the field number being reserved. + String fieldNumberPart = stripSemicolon(line.split(" ")[1]).trim(); + int fieldNumber = Integer.parseInt(fieldNumberPart); + return new FieldDefinition(fieldNumber, fieldDescription); + } + + private FieldDefinition parseStandardFieldDefinition(String line, String fieldDescription) { + try { + String[] parts = stripSemicolon(line).split(" "); + boolean isRepeated = parts[0].trim().equals("repeated"); + String dataType = isRepeated ? parts[1] : parts[0]; + String name = isRepeated ? parts[2] : parts[1]; + int fieldNumber = isRepeated ? Integer.parseInt(parts[4]) : Integer.parseInt(parts[3]); + boolean isDeprecated = hasDeprecationOption.test(line); + return new FieldDefinition(isRepeated, dataType, name, fieldNumber, fieldDescription, isDeprecated); + } catch (ArrayIndexOutOfBoundsException ex) { + throw new RuntimeException(format("Could not parse msg field definition at line: %s", line), ex); + } catch (NumberFormatException ex) { + throw new RuntimeException(format("Could not parse msg field number at line: %s", line), ex); + } + } + + private EnumDefinition getEnumDefinition(TextBlock enumTextBlock, + Optional descriptionTextBlock, + boolean isGlobal) { + String enumName = enumTextBlock.getProtobufDefinitionName(); + String description = getDescription(descriptionTextBlock); + Map constants = new LinkedHashMap(); + List children = enumTextBlock.getChildren(); + for (int i = 0; i < children.size(); i++) { + ProtoAndDescriptionPair pair = new ProtoAndDescriptionPair(children, i); + // Bump children iteration idx if we just consumed two TextBlock list elements. + if (pair.optionalDescription.isPresent()) + i++; + + String constantDescription = getDescription(pair.optionalDescription); + EnumConstantDefinition constantDefinition = parseEnumConstantDefinition( + pair.proto.getText(), + constantDescription); + constants.put(constantDefinition.name(), constantDefinition); + } + return new EnumDefinition(enumName, constants, description, isGlobal); + } + + private EnumConstantDefinition parseEnumConstantDefinition(String line, String description) { + try { + boolean isDeprecated = hasDeprecationOption.test(line); + String[] parts = getEnumConstantParts(line, isDeprecated); + String constant = parts[0].trim(); + int ordinal = getEnumConstantOrdinal(parts, isDeprecated); + return new EnumConstantDefinition(constant, ordinal, description, isDeprecated); + } catch (ArrayIndexOutOfBoundsException ex) { + throw new IllegalStateException(format("Could not parse enum constant definition at line: %s", line), ex); + } catch (NumberFormatException ex) { + throw new IllegalStateException(format("Could not parse enum constant value at line: %s", line), ex); + } + } + + private String getDescription(Optional optionalDescription) { + if (optionalDescription.isPresent()) { + TextBlock textBlock = optionalDescription.get(); + String rawComment = textBlock.getText(); + return textBlock.getTextBlockType().equals(LINE_COMMENT) + ? toCleanLineComment.apply(rawComment) + : toCleanBlockComment.apply(rawComment); + } else { + return ""; + } + } + + private MessageDefinition findMessageDefinition(String name) { + if (globalMessageDefinitions.containsKey(name)) + return globalMessageDefinitions.get(name); + else + throw new IllegalStateException( + format("Could not find '%s' protobuf message in pb.proto or grpc.proto.", name)); + } + + @SuppressWarnings("unused") + private EnumDefinition findEnumDefinition(String name) { + if (globalEnumDefinitions.containsKey(name)) + return globalEnumDefinitions.get(name); + else + throw new IllegalStateException( + format("Could not find '%s' protobuf enum in pb.proto or grpc.proto.", name)); + } + + private boolean isHeaderTextBlock(TextBlock textBlock) { + return textBlock.isLicenseBlock() + || textBlock.getTextBlockType().equals(SYNTAX_DECLARATION) + || textBlock.getTextBlockType().equals(PACKAGE_DECLARATION) + || textBlock.getTextBlockType().equals(IMPORT_STATEMENT) + || textBlock.getTextBlockType().equals(OPTION_DECLARATION); + } + + /** + * Container class for protobuf definition TextBlock paired with its optional comment TextBlock. + */ + private static class ProtoAndDescriptionPair { + final List siblings; + final Optional optionalDescription; + final TextBlock proto; + + public ProtoAndDescriptionPair(List siblings, int currentSiblingIdx) { + this.siblings = siblings; + this.optionalDescription = siblings.get(currentSiblingIdx).isCommentBlock() + ? Optional.of(siblings.get(currentSiblingIdx)) + : Optional.empty(); + this.proto = optionalDescription.isPresent() + ? siblings.get(currentSiblingIdx + 1) + : siblings.get(currentSiblingIdx); + } + } +} diff --git a/reference-doc-builder/src/main/java/bisq/apidoc/protobuf/text/EnumTextBlockFactory.java b/reference-doc-builder/src/main/java/bisq/apidoc/protobuf/text/EnumTextBlockFactory.java new file mode 100644 index 0000000..fef5a85 --- /dev/null +++ b/reference-doc-builder/src/main/java/bisq/apidoc/protobuf/text/EnumTextBlockFactory.java @@ -0,0 +1,86 @@ +/* + * This file is part of Bisq. + * + * Bisq is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bisq is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bisq. If not, see . + */ +package bisq.apidoc.protobuf.text; + +import lombok.extern.slf4j.Slf4j; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +import static bisq.apidoc.protobuf.ProtoParserUtil.*; +import static bisq.apidoc.protobuf.text.TextBlock.PROTOBUF_DEF_TYPE.ENUM_CONSTANT; +import static bisq.apidoc.protobuf.text.TextBlock.PROTOBUF_DEF_TYPE.ENUM_DECLARATION; +import static java.lang.String.format; + +@Slf4j +public class EnumTextBlockFactory { + + public static TextBlock createEnumTextBlock(ProtobufFileReader fileReader, String firstLine) { + // Verify the firstLine is declaring a protobuf enum. + if (!isEnumDeclaration.test(firstLine)) + throw new IllegalArgumentException(format("First line is not an enum declaration: %s", firstLine)); + + StringBuilder enclosingEnumTextBuilder = new StringBuilder(indentedText(firstLine, 0)); + List childBlocks = new ArrayList<>(); + fileReader.pushBracesLevelStack(); + boolean done = false; + while (!done) { + String nextTrimmedLine = fileReader.getNextTrimmedLine(true); + Optional appendedLineCommentTextBlock = getAppendedLineCommentBlock(nextTrimmedLine); + if (appendedLineCommentTextBlock.isPresent()) { + childBlocks.add(appendedLineCommentTextBlock.get()); + enclosingEnumTextBuilder.append(indentedText(appendedLineCommentTextBlock.get().getText(), 1)); + } + if (isClosingProtobufDefinition.test(nextTrimmedLine)) { + enclosingEnumTextBuilder.append(indentedText(nextTrimmedLine, 0)); + fileReader.bracesLevelStack.pop(); + done = true; + } else if (isComment.test(nextTrimmedLine)) { + TextBlock commentBlock = fileReader.readCommentTextBlock(nextTrimmedLine); + enclosingEnumTextBuilder.append(indentedText(commentBlock.getText(), 1)); + childBlocks.add(commentBlock); + } else if (isEnumConstantDeclaration.test(nextTrimmedLine)) { + String blockText = getCleanBlockText(nextTrimmedLine, appendedLineCommentTextBlock); + TextBlock enumConstant = createEnumConstantTextBlock(blockText); + enclosingEnumTextBuilder.append(indentedText(enumConstant.getText(), 1)); + childBlocks.add(enumConstant); + } else { + throw new IllegalStateException(format("Invalid text found in protobuf enum definition: %s", firstLine)); + } + } + enclosingEnumTextBuilder.append("\n"); + var enumTextBlock = new TextBlock(enclosingEnumTextBuilder.toString(), ENUM_DECLARATION, childBlocks); + log.trace(">>>> Enum TextBlock Text:\n{}", enumTextBlock.getText()); + return enumTextBlock; + } + + private static TextBlock createEnumConstantTextBlock(String line) { + try { + // Sanity check. + boolean isDeprecatedEnumConstant = isDeprecated.test(line); + String[] parts = getEnumConstantParts(line, isDeprecatedEnumConstant); + int constantOrdinal = getEnumConstantOrdinal(parts, isDeprecatedEnumConstant); + if (constantOrdinal < 0) + throw new IllegalStateException("Malformed enum constant declaration in line: " + line); + + return new TextBlock(line, ENUM_CONSTANT); + } catch (Exception ex) { + throw new IllegalStateException("Could not read enum constant from line: " + line, ex); + } + } +} diff --git a/reference-doc-builder/src/main/java/bisq/apidoc/protobuf/text/GrpcServiceTextBlockFactory.java b/reference-doc-builder/src/main/java/bisq/apidoc/protobuf/text/GrpcServiceTextBlockFactory.java new file mode 100644 index 0000000..a7abc04 --- /dev/null +++ b/reference-doc-builder/src/main/java/bisq/apidoc/protobuf/text/GrpcServiceTextBlockFactory.java @@ -0,0 +1,95 @@ +/* + * This file is part of Bisq. + * + * Bisq is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bisq is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bisq. If not, see . + */ +package bisq.apidoc.protobuf.text; + +import lombok.extern.slf4j.Slf4j; + +import java.util.ArrayList; +import java.util.List; + +import static bisq.apidoc.protobuf.ProtoParserUtil.*; +import static bisq.apidoc.protobuf.text.TextBlock.PROTOBUF_DEF_TYPE.GRPC_SERVICE_DECLARATION; +import static bisq.apidoc.protobuf.text.TextBlock.PROTOBUF_DEF_TYPE.RPC_METHOD_DECLARATION; +import static java.lang.String.format; + +@Slf4j +public class GrpcServiceTextBlockFactory { + + public static TextBlock createGrpcServiceTextBlock(ProtobufFileReader fileReader, String firstLine) { + // Verify the firstLine is declaring a protobuf grpc service. + if (!isGrpcServiceDeclaration.test(firstLine)) + throw new IllegalArgumentException(format("First line is not a grpc service declaration: %s", firstLine)); + + StringBuilder enclosingServiceTextBuilder = new StringBuilder(firstLine).append("\n"); + List childBlocks = new ArrayList<>(); + fileReader.pushBracesLevelStack(); + while (!fileReader.isBracesStackEmpty()) { + String nextTrimmedLine = fileReader.getNextTrimmedLine(true); + + // Line comments appended 1st line of gRPC service and rpc method declarations will not be supported. + // Block and line comments describing Grpc services and rpc methods must be placed above them. + + if (isClosingProtobufDefinition.test(nextTrimmedLine)) { + enclosingServiceTextBuilder.append(nextTrimmedLine).append("\n"); + fileReader.popBracesLevelStack(); + } else if (isComment.test(nextTrimmedLine)) { + TextBlock commentBlock = fileReader.readCommentTextBlock(nextTrimmedLine); + enclosingServiceTextBuilder.append(indentedText(commentBlock.getText(), fileReader.getBracesStackSize())); + childBlocks.add(commentBlock); + } else if (isRpcMethodDeclaration.test(nextTrimmedLine)) { + TextBlock rpcMethodTextBlock = createRpcMethodTextBlock(fileReader, nextTrimmedLine); + enclosingServiceTextBuilder.append(indentedText(rpcMethodTextBlock.getText(), fileReader.getBracesStackSize())); + childBlocks.add(rpcMethodTextBlock); + } else { + throw new IllegalStateException(format("Invalid text found in protobuf grpc service definition: %s", firstLine)); + } + } + enclosingServiceTextBuilder.append("\n"); + var grpcServiceTextBlock = new TextBlock(enclosingServiceTextBuilder.toString(), GRPC_SERVICE_DECLARATION, childBlocks); + log.trace(">>>> gRPC service TextBlock Text:\n{}", grpcServiceTextBlock.getText()); + return grpcServiceTextBlock; + } + + private static TextBlock createRpcMethodTextBlock(ProtobufFileReader fileReader, String firstLine) { + try { + // Verify the firstLine is declaring a protobuf rpc method. + if (!isRpcMethodDeclaration.test(firstLine)) + throw new IllegalArgumentException(format("First line is not a rpc method declaration: %s", firstLine)); + + // Rpc methods are easy to parse: a single line to declare it, and a second line to close it. + // For example: + // rpc RegisterDisputeAgent (RegisterDisputeAgentRequest) returns (RegisterDisputeAgentReply) { + // } + // Line comments appended to line 1 will not be supported. + // Comments inside the rpc method declaration will not be supported. + // Comments on the rpc request and response message definitions themselves are supported. + + StringBuilder rpcTextBuilder = new StringBuilder(firstLine).append("\n"); + fileReader.pushBracesLevelStack(); + String enclosingLine = fileReader.getNextTrimmedLine(true); + if (!isClosingProtobufDefinition.test(enclosingLine)) + throw new IllegalStateException("Did not find enclosing curly brace for rpc method declaration: " + firstLine); + + fileReader.popBracesLevelStack(); + rpcTextBuilder.append(enclosingLine).append("\n"); + return new TextBlock(rpcTextBuilder.toString(), RPC_METHOD_DECLARATION); + } catch (Exception ex) { + throw new IllegalStateException("Could not read enum constant from line: " + firstLine, ex); + } + } + +} diff --git a/reference-doc-builder/src/main/java/bisq/apidoc/protobuf/text/MessageTextBlockFactory.java b/reference-doc-builder/src/main/java/bisq/apidoc/protobuf/text/MessageTextBlockFactory.java new file mode 100644 index 0000000..e195abf --- /dev/null +++ b/reference-doc-builder/src/main/java/bisq/apidoc/protobuf/text/MessageTextBlockFactory.java @@ -0,0 +1,146 @@ +/* + * This file is part of Bisq. + * + * Bisq is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bisq is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bisq. If not, see . + */ +package bisq.apidoc.protobuf.text; + +import lombok.extern.slf4j.Slf4j; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +import static bisq.apidoc.protobuf.ProtoParserUtil.*; +import static bisq.apidoc.protobuf.text.EnumTextBlockFactory.createEnumTextBlock; +import static bisq.apidoc.protobuf.text.TextBlock.PROTOBUF_DEF_TYPE.*; +import static java.lang.String.format; + +@Slf4j +public class MessageTextBlockFactory { + + public static TextBlock createMessageTextBlock(ProtobufFileReader fileReader, String firstLine) { + StringBuilder enclosingMessageTextBuilder = new StringBuilder(firstLine).append("\n"); + List childBlocks = new ArrayList<>(); + + fileReader.pushBracesLevelStack(); + while (!fileReader.isBracesStackEmpty()) { + String nextTrimmedLine = fileReader.getNextTrimmedLine(true); + Optional appendedLineCommentTextBlock = getAppendedLineCommentBlock(nextTrimmedLine); + + if (isClosingProtobufDefinition.test(nextTrimmedLine)) { + enclosingMessageTextBuilder.append(nextTrimmedLine).append("\n"); + fileReader.popBracesLevelStack(); + } else if (isComment.test(nextTrimmedLine)) { + TextBlock commentBlock = fileReader.readCommentTextBlock(nextTrimmedLine); + enclosingMessageTextBuilder.append(indentedText(commentBlock.getText(), fileReader.getBracesStackSize())); + childBlocks.add(commentBlock); + } else if (isEnumDeclaration.test(nextTrimmedLine)) { + TextBlock enumTextBlock = createEnumTextBlock(fileReader, nextTrimmedLine); + enclosingMessageTextBuilder.append(indentedText(enumTextBlock.getText(), fileReader.getBracesStackSize())); + childBlocks.add(enumTextBlock); + } else if (isReservedFieldDeclaration.test(nextTrimmedLine)) { + if (appendedLineCommentTextBlock.isPresent()) { + childBlocks.add(appendedLineCommentTextBlock.get()); + enclosingMessageTextBuilder.append(indentedText(appendedLineCommentTextBlock.get().getText(), fileReader.getBracesStackSize())); + } + String blockText = getCleanBlockText(nextTrimmedLine, appendedLineCommentTextBlock); + TextBlock reservedFieldTextBlock = new TextBlock(blockText, RESERVED_MESSAGE_FIELD); + enclosingMessageTextBuilder.append(indentedText(reservedFieldTextBlock.getText(), fileReader.getBracesStackSize())); + childBlocks.add(reservedFieldTextBlock); + } else if (isOneOfMessageFieldDeclaration.test(nextTrimmedLine)) { + // No map fields, no repeated fields, or repeated oneof blocks. + TextBlock oneOfFieldTextBlock = createOneOfFieldTextBlock(fileReader, nextTrimmedLine); + enclosingMessageTextBuilder.append(indentedText(oneOfFieldTextBlock.getText(), fileReader.getBracesStackSize())); + childBlocks.add(oneOfFieldTextBlock); + } else if (isMessageFieldDeclaration.test(nextTrimmedLine)) { + if (isDeprecated.test(nextTrimmedLine)) { + if (appendedLineCommentTextBlock.isPresent()) { + childBlocks.add(appendedLineCommentTextBlock.get()); + enclosingMessageTextBuilder.append(indentedText(appendedLineCommentTextBlock.get().getText(), fileReader.getBracesStackSize())); + } + String blockText = getCleanBlockText(nextTrimmedLine, appendedLineCommentTextBlock); + TextBlock deprecatedFieldTextBlock = new TextBlock(blockText, DEPRECATED_MESSAGE_FIELD); + enclosingMessageTextBuilder.append(indentedText(deprecatedFieldTextBlock.getText(), fileReader.getBracesStackSize())); + childBlocks.add(deprecatedFieldTextBlock); + } else if (isMessageMapFieldDeclaration.test(nextTrimmedLine)) { + // Do a check on map type declaration before returning the TextBlock. + // It will throw an exception if type declaration is not found in the line. + getMapKeyValueTypes(nextTrimmedLine); + if (appendedLineCommentTextBlock.isPresent()) { + childBlocks.add(appendedLineCommentTextBlock.get()); + enclosingMessageTextBuilder.append(indentedText(appendedLineCommentTextBlock.get().getText(), fileReader.getBracesStackSize())); + } + String blockText = getCleanBlockText(nextTrimmedLine, appendedLineCommentTextBlock); + TextBlock mapFieldTextBlock = new TextBlock(blockText, MESSAGE_MAP_FIELD); + enclosingMessageTextBuilder.append(indentedText(blockText, fileReader.getBracesStackSize())); + childBlocks.add(mapFieldTextBlock); + } else { + if (appendedLineCommentTextBlock.isPresent()) { + childBlocks.add(appendedLineCommentTextBlock.get()); + enclosingMessageTextBuilder.append(indentedText(appendedLineCommentTextBlock.get().getText(), fileReader.getBracesStackSize())); + } + String blockText = getCleanBlockText(nextTrimmedLine, appendedLineCommentTextBlock); + TextBlock fieldTextBlock = new TextBlock(blockText, MESSAGE_FIELD); + enclosingMessageTextBuilder.append(indentedText(blockText, fileReader.getBracesStackSize())); + childBlocks.add(fieldTextBlock); + } + } else { + throw new IllegalStateException(format("Invalid protobuf message component: %s", firstLine)); + } + } + var messageTextBlock = new TextBlock(enclosingMessageTextBuilder.toString(), MESSAGE_DECLARATION, childBlocks); + log.trace(">>>> Message TextBlock Text:\n{}", messageTextBlock.getText()); + return messageTextBlock; + } + + public static TextBlock createOneOfFieldTextBlock(ProtobufFileReader fileReader, String firstLine) { + // You can add fields of any type, except map fields and repeated fields. + // See https://developers.google.com/protocol-buffers/docs/proto3#using_oneof + + // Verify the firstLine is declaring a protobuf oneof message field. + if (!isOneOfMessageFieldDeclaration.test(firstLine)) + throw new IllegalArgumentException(format("First line is not oneof message declaration: %s", firstLine)); + + StringBuilder textBuilder = new StringBuilder(indentedText(firstLine, 0)); + List childBlocks = new ArrayList<>(); + boolean done = false; + while (!done) { + String nextTrimmedLine = fileReader.getNextTrimmedLine(true); + + Optional appendedLineCommentTextBlock = getAppendedLineCommentBlock(nextTrimmedLine); + if (appendedLineCommentTextBlock.isPresent()) { + childBlocks.add(appendedLineCommentTextBlock.get()); + textBuilder.append(indentedText(appendedLineCommentTextBlock.get().getText(), 1)); + } + + if (isClosingProtobufDefinition.test(nextTrimmedLine)) { + textBuilder.append(indentedText(nextTrimmedLine, 0)); + done = true; // Do not need curlies stack for oneof message case. + } else if (isComment.test(nextTrimmedLine)) { + TextBlock commentBlock = fileReader.readCommentTextBlock(nextTrimmedLine); + textBuilder.append(indentedText(commentBlock.getText(), 1)); + childBlocks.add(commentBlock); + } else if (isMessageFieldDeclaration.test(nextTrimmedLine)) { + String blockText = getCleanBlockText(nextTrimmedLine, appendedLineCommentTextBlock); + TextBlock fieldTextBlock = new TextBlock(blockText, MESSAGE_FIELD); + textBuilder.append(indentedText(blockText, 1)); + childBlocks.add(fieldTextBlock); + } else { + throw new IllegalStateException(format("Invalid text found in protobuf oneof message definition: %s", firstLine)); + } + } + return new TextBlock(textBuilder.toString(), ONEOF_MESSAGE_DECLARATION, childBlocks); + } +} diff --git a/reference-doc-builder/src/main/java/bisq/apidoc/protobuf/text/ProtobufFileReader.java b/reference-doc-builder/src/main/java/bisq/apidoc/protobuf/text/ProtobufFileReader.java new file mode 100644 index 0000000..f7b39e6 --- /dev/null +++ b/reference-doc-builder/src/main/java/bisq/apidoc/protobuf/text/ProtobufFileReader.java @@ -0,0 +1,205 @@ +/* + * This file is part of Bisq. + * + * Bisq is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bisq is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bisq. If not, see . + */ +package bisq.apidoc.protobuf.text; + +import lombok.extern.slf4j.Slf4j; + +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.RandomAccessFile; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; +import java.util.Stack; + +import static bisq.apidoc.protobuf.ProtoParserUtil.*; +import static bisq.apidoc.protobuf.text.TextBlock.TEXT_BLOCK_TYPE.*; +import static java.lang.String.format; + +@Slf4j +public class ProtobufFileReader { + + // Used to track nested curly braces in nested structures in .proto files. + @SuppressWarnings("rawtypes") + protected final Stack bracesLevelStack = new Stack(); + + protected final Path protobufPath; + protected final RandomAccessFile randomAccessFile; + + public ProtobufFileReader(Path protobufPath) { + this.protobufPath = protobufPath; + randomAccessFile = open(); + } + + @SuppressWarnings("BooleanMethodIsAlwaysInverted") + public boolean isBracesStackEmpty() { + return bracesLevelStack.empty(); + } + + public int getBracesStackSize() { + return bracesLevelStack.size(); + } + + public int popBracesLevelStack() { + return (int) bracesLevelStack.pop(); + } + + public void pushBracesLevelStack() { + //noinspection unchecked + bracesLevelStack.push(bracesLevelStack.size()); + } + + public Path getProtobufPath() { + return protobufPath; + } + + public String getNextTrimmedLine() { + return getNextTrimmedLine(false); + } + + public String getNextTrimmedLine(boolean skipBlankLines) { + try { + if (isEndOfFile()) + throw new IllegalStateException("Reached end of random access file."); + + String line = randomAccessFile.readLine(); + if (line == null) + return null; + + while (skipBlankLines && isBlankLine.test(line)) { + line = randomAccessFile.readLine(); + if (line == null) + return null; + } + + + return line.trim(); + } catch (IOException ex) { + throw new IllegalStateException("Error consuming blank lines in " + protobufPath, ex); + } + } + + public List readHeaderTextBlocks() { + try { + List textBlocks = new ArrayList<>(); + while (!isEndOfFile()) { + long lastFilePtr = getFilePointer(); + String line = getNextTrimmedLine(true); + if (isComment.test(line)) + textBlocks.add(readCommentTextBlock(line)); + else if (isSyntaxDeclaration.test(line)) + textBlocks.add(new TextBlock(line, SYNTAX_DECLARATION)); + else if (isPackageDeclaration.test(line)) + textBlocks.add(new TextBlock(line, PACKAGE_DECLARATION)); + else if (isImportStatement.test(line)) + textBlocks.add(new TextBlock(line, IMPORT_STATEMENT)); + else if (isOptionDeclaration.test(line)) + textBlocks.add(new TextBlock(line, OPTION_DECLARATION)); + else if (isDeclaringProtobufDefinition.test(line)) { + // The .proto file license, syntax, pkg, import, and option TextBlocks have been read. + // Move the file ptr back to where it was before reading the first line of the first + // protobuf declaration and break out of the loop. + randomAccessFile.seek(lastFilePtr); + break; + } + } + return textBlocks; + } catch (IOException ex) { + throw new IllegalStateException("Could not get random access file ptr.", ex); + } + } + + public TextBlock readCommentTextBlock(String firstLine) { + if (isBlockCommentOpener.test(firstLine)) + return readBlockCommentTextBlock(firstLine); + else if (isLineComment.test(firstLine)) + return readLineCommentTextBlock(firstLine); + else + throw new IllegalStateException(format("Line '%s' is not a comment.", firstLine)); + } + + public RandomAccessFile open() { + try { + return new RandomAccessFile(protobufPath.toFile(), "r"); + } catch (FileNotFoundException ex) { + throw new IllegalArgumentException(protobufPath.getFileName() + " not found.", ex); + } + } + + public boolean isEndOfFile() { + try { + return getFilePointer() == randomAccessFile.length(); + } catch (IOException ex) { + throw new IllegalStateException("Could not determine file has been fully read.", ex); + } + } + + public void close() { + try { + randomAccessFile.close(); + } catch (IOException ex) { + throw new IllegalStateException(protobufPath.getFileName() + " not closed.", ex); + } + } + + public long getFilePointer() { + try { + return randomAccessFile.getFilePointer(); + } catch (IOException ex) { + throw new IllegalStateException("Could not get random access file pointer.", ex); + } + } + + private TextBlock readBlockCommentTextBlock(String firstLine) { + /* A block comment can be single line. */ + if (isBlockCommentCloser.test(firstLine)) + return new TextBlock(firstLine, BLOCK_COMMENT); + + StringBuilder sb = new StringBuilder(firstLine).append("\n"); + while (!isEndOfFile()) { + String line = getNextTrimmedLine(); + sb.append(line).append("\n"); + if (isBlockCommentCloser.test(line)) + break; + } + if (isEndOfFile()) + throw new IllegalStateException( + "Reading the comment exhausted the file pointer, protobuf file must be malformed."); + + return new TextBlock(sb.toString(), BLOCK_COMMENT); + } + + private TextBlock readLineCommentTextBlock(String firstLine) { + try { + StringBuilder sb = new StringBuilder(firstLine).append("\n"); + while (true) { + long savedFilePointer = getFilePointer(); + String line = getNextTrimmedLine(true); + if (!isLineComment.test(line)) { + // Move file ptr back to non-comment line to it is not missed by next getNextTrimmedLine() call. + randomAccessFile.seek(savedFilePointer); + break; + } else { + sb.append(line).append("\n"); + } + } + return new TextBlock(sb.toString(), LINE_COMMENT); + } catch (IOException ex) { + throw new IllegalStateException("Could not read block comment.", ex); + } + } +} diff --git a/reference-doc-builder/src/main/java/bisq/apidoc/protobuf/text/ProtobufTextBlockFactory.java b/reference-doc-builder/src/main/java/bisq/apidoc/protobuf/text/ProtobufTextBlockFactory.java new file mode 100644 index 0000000..569beba --- /dev/null +++ b/reference-doc-builder/src/main/java/bisq/apidoc/protobuf/text/ProtobufTextBlockFactory.java @@ -0,0 +1,69 @@ +/* + * This file is part of Bisq. + * + * Bisq is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bisq is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bisq. If not, see . + */ +package bisq.apidoc.protobuf.text; + +import lombok.extern.slf4j.Slf4j; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +import static bisq.apidoc.protobuf.ProtoParserUtil.*; +import static bisq.apidoc.protobuf.text.EnumTextBlockFactory.createEnumTextBlock; +import static bisq.apidoc.protobuf.text.GrpcServiceTextBlockFactory.createGrpcServiceTextBlock; +import static bisq.apidoc.protobuf.text.MessageTextBlockFactory.createMessageTextBlock; +import static java.lang.String.format; + +@Slf4j +public class ProtobufTextBlockFactory { + + public static List createTextBlocks(ProtobufFileReader fileReader) { + List textBlocks = new ArrayList<>(fileReader.readHeaderTextBlocks()); + while (!fileReader.isEndOfFile()) { + Optional textBlock = createTextBlock(fileReader); + textBlock.ifPresent(textBlocks::add); + } + return textBlocks; + } + + private static Optional createTextBlock(ProtobufFileReader fileReader) { + try { + String line = fileReader.getNextTrimmedLine(true); + if (line == null) + return Optional.empty(); + else if (isComment.test(line)) + return Optional.of(fileReader.readCommentTextBlock(line)); + else if (isDeclaringProtobufDefinition.test(line)) + return Optional.of(createProtoDefinitionTextBlock(fileReader, line)); + else + throw new IllegalStateException("Invalid text found in .proto file " + fileReader.getProtobufPath()); + } catch (Exception ex) { + throw new IllegalStateException("Could not read TextBlock.", ex); + } + } + + private static TextBlock createProtoDefinitionTextBlock(ProtobufFileReader fileReader, String firstLine) { + if (isEnumDeclaration.test(firstLine)) + return createEnumTextBlock(fileReader, firstLine); + else if (isMessageDeclaration.test(firstLine)) + return createMessageTextBlock(fileReader, firstLine); + else if (isGrpcServiceDeclaration.test(firstLine)) + return createGrpcServiceTextBlock(fileReader, firstLine); + else + throw new IllegalStateException(format("Invalid protobuf declaration: %s", firstLine)); + } +} diff --git a/reference-doc-builder/src/main/java/bisq/apidoc/protobuf/text/TextBlock.java b/reference-doc-builder/src/main/java/bisq/apidoc/protobuf/text/TextBlock.java new file mode 100644 index 0000000..3364a37 --- /dev/null +++ b/reference-doc-builder/src/main/java/bisq/apidoc/protobuf/text/TextBlock.java @@ -0,0 +1,207 @@ +/* + * This file is part of Bisq. + * + * Bisq is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bisq is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bisq. If not, see . + */ +package bisq.apidoc.protobuf.text; + +import java.util.ArrayList; +import java.util.List; + +import static bisq.apidoc.protobuf.ProtoParserUtil.*; +import static bisq.apidoc.protobuf.text.TextBlock.PROTOBUF_DEF_TYPE.*; +import static bisq.apidoc.protobuf.text.TextBlock.TEXT_BLOCK_TYPE.*; +import static java.lang.String.format; + +/** + * A raw block of text read from a .proto file, defining a gRPC service, + * a rpc service, message, enum, block comment or line comment. + */ +public class TextBlock { + + public enum TEXT_BLOCK_TYPE { + BLOCK_COMMENT, + IMPORT_STATEMENT, + LINE_COMMENT, + OPTION_DECLARATION, + PACKAGE_DECLARATION, + PROTO_DEFINITION, + SYNTAX_DECLARATION + } + + public enum PROTOBUF_DEF_TYPE { + ENUM_DECLARATION, + ENUM_CONSTANT, + GRPC_SERVICE_DECLARATION, + MESSAGE_DECLARATION, + DEPRECATED_MESSAGE_FIELD, + ONEOF_MESSAGE_DECLARATION, + MESSAGE_FIELD, + MESSAGE_MAP_FIELD, + RESERVED_MESSAGE_FIELD, + RPC_METHOD_DECLARATION, + UNKNOWN + } + + private final String text; + private final TEXT_BLOCK_TYPE textBlockType; + private final PROTOBUF_DEF_TYPE protobufDefType; + private final List children; + + public TextBlock(String text, TEXT_BLOCK_TYPE textBlockType) { + this.text = text; + this.textBlockType = textBlockType; + this.protobufDefType = textBlockType.equals(PROTO_DEFINITION) ? UNKNOWN : null; + this.children = textBlockType.equals(PROTO_DEFINITION) ? new ArrayList<>() : null; + validateConstructorTextParam(); + } + + public TextBlock(String text, PROTOBUF_DEF_TYPE protobufDefType) { + this(text, protobufDefType, new ArrayList<>()); + } + + public TextBlock(String text, PROTOBUF_DEF_TYPE protobufDefType, List children) { + this.text = text; + this.textBlockType = PROTO_DEFINITION; + this.protobufDefType = protobufDefType; + this.children = children; + validateConstructorTextParam(); + } + + public String getText() { + return text; + } + + public TEXT_BLOCK_TYPE getTextBlockType() { + return this.textBlockType; + } + + public PROTOBUF_DEF_TYPE getProtobufDefType() { + return protobufDefType; + } + + public List getChildren() { + return children; + } + + public void addBlockCommentBlock(TextBlock child) { + if (!child.getTextBlockType().equals(BLOCK_COMMENT)) + throw new IllegalArgumentException(format("Child TextBlock is not a BLOCK_COMMENT: %s", text)); + + children.add(child); + } + + public void addLineCommentBlock(TextBlock child) { + if (!child.getTextBlockType().equals(LINE_COMMENT)) + throw new IllegalArgumentException(format("Child TextBlock is not a LINE_COMMENT: %s", text)); + + children.add(child); + } + + public void addProtoDefinitionBlock(TextBlock child) { + if (!child.getTextBlockType().equals(PROTO_DEFINITION)) + throw new IllegalArgumentException(format("Child TextBlock is not a PROTO_DEFINITION: %s", text)); + + children.add(child); + } + + public String getComment() { + if (!textBlockType.equals(BLOCK_COMMENT) && !textBlockType.equals(LINE_COMMENT)) + throw new IllegalStateException(format("Text block is not a comment: %s", text)); + + return textBlockType.equals(BLOCK_COMMENT) ? getBlockComment() : getLineComment(); + } + + public String getBlockComment() { + if (!textBlockType.equals(BLOCK_COMMENT)) + throw new IllegalStateException(format("Text block is not a block comment: %s", text)); + + return findFirstMatch(text, BLOCK_COMMENT_PATTERN).orElseThrow(() -> + new IllegalStateException(format("Could not comment from text block: %s", text))); + } + + public String getLineComment() { + if (!textBlockType.equals(LINE_COMMENT)) + throw new IllegalStateException(format("Text block is not a line comment: %s", text)); + + return text.substring(text.indexOf("//")); + } + + public String getProtobufDefinitionName() { + if (!textBlockType.equals(PROTO_DEFINITION)) + throw new IllegalStateException("TextBlock must be a PROTO_DEFINITION to have a protobuf name."); + + // All protobuf service, message, and enum definition names can be + // found in the second token of the declaration (first line). + return text.split(" ")[1].trim(); + } + + public boolean isLicenseBlock() { + return textBlockType.equals(BLOCK_COMMENT) && hasPattern(text, LICENSE_PATTERN); + } + + public boolean isCommentBlock() { + return textBlockType.equals(BLOCK_COMMENT) || textBlockType.equals(LINE_COMMENT); + } + + public boolean isEnumDeclaration() { + return textBlockType.equals(PROTO_DEFINITION) && protobufDefType.equals(ENUM_DECLARATION); + } + + public boolean isEnumConstant() { + return textBlockType.equals(PROTO_DEFINITION) && protobufDefType.equals(ENUM_CONSTANT); + } + + public boolean isGrpcServiceDeclaration() { + return textBlockType.equals(PROTO_DEFINITION) && protobufDefType.equals(GRPC_SERVICE_DECLARATION); + } + + public boolean isOneOfMessageDeclaration() { + return textBlockType.equals(PROTO_DEFINITION) && protobufDefType.equals(ONEOF_MESSAGE_DECLARATION); + } + + public boolean isMessageDeclaration() { + return textBlockType.equals(PROTO_DEFINITION) && protobufDefType.equals(MESSAGE_DECLARATION); + } + + public boolean isMessageField() { + return textBlockType.equals(PROTO_DEFINITION) && protobufDefType.equals(MESSAGE_FIELD); + } + + public boolean isMessageMapField() { + return textBlockType.equals(PROTO_DEFINITION) && protobufDefType.equals(MESSAGE_MAP_FIELD); + } + + public boolean isReservedField() { + return textBlockType.equals(PROTO_DEFINITION) && protobufDefType.equals(RESERVED_MESSAGE_FIELD); + } + + public boolean isRpcMethodDeclaration() { + return textBlockType.equals(PROTO_DEFINITION) && protobufDefType.equals(RPC_METHOD_DECLARATION); + } + + @Override + public String toString() { + return "TextBlock {" + "\n" + + " textBlockType=" + textBlockType + + ", protobufDefType=" + protobufDefType + "\n" + + ", " + text + "\n" + + '}'; + } + + private void validateConstructorTextParam() { + if (text == null || text.isBlank()) + throw new IllegalArgumentException("TextBlock constructor's text parameter cannot be null."); + } +} diff --git a/reference-doc-builder/src/main/java/fun/mingshan/markdown4j/Markdown.java b/reference-doc-builder/src/main/java/fun/mingshan/markdown4j/Markdown.java new file mode 100644 index 0000000..a554b81 --- /dev/null +++ b/reference-doc-builder/src/main/java/fun/mingshan/markdown4j/Markdown.java @@ -0,0 +1,95 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package fun.mingshan.markdown4j; + +import fun.mingshan.markdown4j.type.block.Block; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +/** + * Markdown文档 + * + * @author hanjuntao + * @date 2022/1/17 + */ +@SuppressWarnings("ALL") +public class Markdown { + + private final String name; + private final List blocks; + + public Markdown(String name, List blocks) { + this.name = name; + this.blocks = blocks; + } + + public String getName() { + return this.name; + } + + public List getBlocks() { + return this.blocks; + } + + public static MarkdownBuilder builder() { + return new MarkdownBuilder(); + } + + public static class MarkdownBuilder { + + private String name; + private List blocks; + + MarkdownBuilder() { + } + + public MarkdownBuilder name(String name) { + Objects.requireNonNull(name, "the name not be null"); + this.name = name; + return this; + } + + public MarkdownBuilder block(Block block) { + Objects.requireNonNull(block, "the block not be null"); + + if (blocks == null) { + blocks = new ArrayList<>(); + } + blocks.add(block); + + return this; + } + + public Markdown build() { + return new Markdown(this.name, this.blocks); + } + + } + + @Override + public String toString() { + StringBuilder result = new StringBuilder(); + + for (Block block : blocks) { + result.append(block.toMd()); + + // My attempt at appending newlines to MD looks like a mistake. Corrupts .md file? + // result.append("\n"); + } + + return result.toString(); + } +} diff --git a/reference-doc-builder/src/main/java/fun/mingshan/markdown4j/constant/FlagConstants.java b/reference-doc-builder/src/main/java/fun/mingshan/markdown4j/constant/FlagConstants.java new file mode 100644 index 0000000..9babc8a --- /dev/null +++ b/reference-doc-builder/src/main/java/fun/mingshan/markdown4j/constant/FlagConstants.java @@ -0,0 +1,34 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package fun.mingshan.markdown4j.constant; + +/** + * @author hanjuntao + * @date 2022/1/17 + */ +public class FlagConstants { + public static final String CODE_BLOCK_FLAG = "```"; + + public static final String TITLE_BLOCK_FLAG = "#"; + + public static final String REFERENCE_BLOCK_FLAG = ">"; + + public static final String INLINE_CODE_FLAG = "`"; + + public static final String SPACE = " "; + + public static final String HTML_LINE_BREAK = "
"; + + public static final String NEW_LINE_BREAK = "\n"; +} diff --git a/reference-doc-builder/src/main/java/fun/mingshan/markdown4j/encoder/block/BlockEncoder.java b/reference-doc-builder/src/main/java/fun/mingshan/markdown4j/encoder/block/BlockEncoder.java new file mode 100644 index 0000000..1d9023b --- /dev/null +++ b/reference-doc-builder/src/main/java/fun/mingshan/markdown4j/encoder/block/BlockEncoder.java @@ -0,0 +1,41 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package fun.mingshan.markdown4j.encoder.block; + +import fun.mingshan.markdown4j.type.block.Block; +import fun.mingshan.markdown4j.type.block.BlockType; + +/** + * Block块编码器,将java代码编译成markdown 语法 + * + * @author hanjuntao + * @date 2022/1/17 + */ +@SuppressWarnings("ALL") +public interface BlockEncoder { + /** + * 编译 + * + * @param block java代码 + * @return markdown语法字符串 + */ + String encode(Block block); + + /** + * 返回块类型 + * + * @return 块类型 + */ + BlockType getType(); +} diff --git a/reference-doc-builder/src/main/java/fun/mingshan/markdown4j/encoder/block/BlockEncoderFactory.java b/reference-doc-builder/src/main/java/fun/mingshan/markdown4j/encoder/block/BlockEncoderFactory.java new file mode 100644 index 0000000..ba71173 --- /dev/null +++ b/reference-doc-builder/src/main/java/fun/mingshan/markdown4j/encoder/block/BlockEncoderFactory.java @@ -0,0 +1,45 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package fun.mingshan.markdown4j.encoder.block; + +import fun.mingshan.markdown4j.type.block.BlockType; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + * @author hanjuntao + * @date 2022/1/18 + */ +@SuppressWarnings("ALL") +public class BlockEncoderFactory { + private static final Map ENCODER_MAP = new ConcurrentHashMap<>(); + + static { + ENCODER_MAP.put(BlockType.CODE.name(), new CodeBlockEncoder()); + ENCODER_MAP.put(BlockType.SLATE_TABLE.name(), new SlateTableBlockEncoder()); + ENCODER_MAP.put(BlockType.TABLE.name(), new TableBlockEncoder()); + ENCODER_MAP.put(BlockType.TITLE.name(), new TitleBlockEncoder()); + ENCODER_MAP.put(BlockType.REFERENCE.name(), new ReferenceBlockEncoder()); + } + + public static BlockEncoder getEncoder(BlockType blockType) { + BlockEncoder blockEncoder = ENCODER_MAP.get(blockType.name()); + if (blockEncoder == null) { + throw new IllegalStateException("Miss blockEncoder : " + blockType.name()); + } + + return blockEncoder; + } +} diff --git a/reference-doc-builder/src/main/java/fun/mingshan/markdown4j/encoder/block/CodeBlockEncoder.java b/reference-doc-builder/src/main/java/fun/mingshan/markdown4j/encoder/block/CodeBlockEncoder.java new file mode 100644 index 0000000..8fef175 --- /dev/null +++ b/reference-doc-builder/src/main/java/fun/mingshan/markdown4j/encoder/block/CodeBlockEncoder.java @@ -0,0 +1,56 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package fun.mingshan.markdown4j.encoder.block; + +import fun.mingshan.markdown4j.constant.FlagConstants; +import fun.mingshan.markdown4j.type.block.Block; +import fun.mingshan.markdown4j.type.block.BlockType; +import fun.mingshan.markdown4j.type.block.CodeBlock; + +/** + * 代码块编译 + * + * @author hanjuntao + * @date 2022/1/17 + */ +@SuppressWarnings("JavaDoc") +public class CodeBlockEncoder implements BlockEncoder { + @Override + public String encode(Block block) { + CodeBlock codeBlock = (CodeBlock) block; + String language = codeBlock.getLanguage(); + + String result = ""; + + if (language != null) { + result += FlagConstants.CODE_BLOCK_FLAG + language; + } else { + result += FlagConstants.CODE_BLOCK_FLAG; + } + + result += FlagConstants.HTML_LINE_BREAK; + + result += codeBlock.getContent(); + result += FlagConstants.HTML_LINE_BREAK; + result += FlagConstants.CODE_BLOCK_FLAG; + result += FlagConstants.HTML_LINE_BREAK; + + return result; + } + + @Override + public BlockType getType() { + return BlockType.CODE; + } +} diff --git a/reference-doc-builder/src/main/java/fun/mingshan/markdown4j/encoder/block/ReferenceBlockEncoder.java b/reference-doc-builder/src/main/java/fun/mingshan/markdown4j/encoder/block/ReferenceBlockEncoder.java new file mode 100644 index 0000000..8574d3b --- /dev/null +++ b/reference-doc-builder/src/main/java/fun/mingshan/markdown4j/encoder/block/ReferenceBlockEncoder.java @@ -0,0 +1,38 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package fun.mingshan.markdown4j.encoder.block; + +import fun.mingshan.markdown4j.constant.FlagConstants; +import fun.mingshan.markdown4j.type.block.Block; +import fun.mingshan.markdown4j.type.block.BlockType; +import fun.mingshan.markdown4j.type.block.ReferenceBlock; + +/** + * @author hanjuntao + * @date 2022/1/18 + */ +public class ReferenceBlockEncoder implements BlockEncoder { + @Override + public String encode(Block block) { + ReferenceBlock referenceBlock = (ReferenceBlock) block; + String content = referenceBlock.getContent(); + + return FlagConstants.REFERENCE_BLOCK_FLAG + FlagConstants.SPACE + content; + } + + @Override + public BlockType getType() { + return BlockType.REFERENCE; + } +} diff --git a/reference-doc-builder/src/main/java/fun/mingshan/markdown4j/encoder/block/SlateTableBlockEncoder.java b/reference-doc-builder/src/main/java/fun/mingshan/markdown4j/encoder/block/SlateTableBlockEncoder.java new file mode 100644 index 0000000..96ecd55 --- /dev/null +++ b/reference-doc-builder/src/main/java/fun/mingshan/markdown4j/encoder/block/SlateTableBlockEncoder.java @@ -0,0 +1,123 @@ +/* + * This file is part of Bisq. + * + * Bisq is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bisq is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bisq. If not, see . + */ +package fun.mingshan.markdown4j.encoder.block; + +import fun.mingshan.markdown4j.type.block.Block; +import fun.mingshan.markdown4j.type.block.BlockType; +import fun.mingshan.markdown4j.type.block.SlateTableBlock; + +import java.util.List; +import java.util.Objects; +import java.util.function.Predicate; + +import static fun.mingshan.markdown4j.constant.FlagConstants.*; +import static fun.mingshan.markdown4j.type.block.BlockType.SLATE_TABLE; + +/** + * Encode Slate compatible table block. + *

+ * Slate Tables use PHP Markdown Extra style tables: + *

+ *

+ * Table Header 1 | Table Header 2 | Table Header 3
+ * -------------- | -------------- | --------------
+ * Row 1 col 1 | Row 1 col 2 | Row 1 col 3
+ * Row 2 col 1 | Row 2 col 2 | Row 2 col 3
+ * 
+ * Note that the pipes do not need to line up with each other on each line. + *

+ * Note that Markdown does not support column spanning (nested, ugly html does). + */ +public class SlateTableBlockEncoder implements BlockEncoder { + + // + // Slate md table blocks must look line this: + // + // Parameter | Default | Description + // --------- | ------- | ----------- + // include_cats | false | If set to true, the result will also include cats. + // available | true | If set to false, the result will include kittens that have already been adopted. + // + // Not like this: + // | header1 | header2 | + // | ------- | ------- | + // | wqeq | qwq | + // + // The differences are: + // The omission of '|' SEP chars at the beginning and end of each line in hanjuntao's TableBlockEncoder, + // The substitution of newline (\n) characters for html line break tags (
). + // Multi-line column values need to be transformed: replace '\n' with '
'. + + private static final String SEP = "|"; // Do not prepend and end each line with SEP. + private static final String SPE2 = "-------------"; + + private final Predicate isMultilineColumnValue = (c) -> c.split(NEW_LINE_BREAK).length > 1; + + @Override + public String encode(Block block) { + SlateTableBlock tableBlock = (SlateTableBlock) block; + List titles = tableBlock.getTitles(); + StringBuilder result = new StringBuilder(); // Do not prepend the titles line with a SEP. + + // Write the titles. + for (int i = 0; i < titles.size(); i++) { + String title = titles.get(i); + result.append(SPACE).append(title).append(SPACE); + // Do not end each line with SEP. + if (i + 1 < titles.size()) + result.append(SEP); + } + result.append(NEW_LINE_BREAK); // Do I really want this
tag in the markdown? + + // Write the 2nd line, i.e., "--------- | ------- | -----------". + for (int i = 0; i < titles.size(); i++) { + result.append(SPACE).append(SPE2).append(SPACE); + // Do not end each line with SEP. + if (i + 1 < titles.size()) + result.append(SEP); + } + result.append(NEW_LINE_BREAK); + + // Write the rows. + List rows = tableBlock.getRows(); + for (SlateTableBlock.TableRow tableRow : rows) { + List columns = tableRow.getColumns(); + for (int i = 0; i < columns.size(); i++) { + String column = columns.get(i); + + // Multi-line column values separated by newline (\n) chars will mess up md table rendering. + // Replace newlines with html line break chars: (
. + if (isMultilineColumnValue.test(column)) + column = column.replaceAll(NEW_LINE_BREAK, HTML_LINE_BREAK); + + result.append(SPACE); + result.append(Objects.requireNonNullElse(column, SPACE)); + result.append(SPACE); + if (i + 1 < columns.size()) + result.append(SEP); + } + result.append(NEW_LINE_BREAK); + } + result.append(NEW_LINE_BREAK); + return result.toString(); + } + + @Override + public BlockType getType() { + return SLATE_TABLE; + } +} diff --git a/reference-doc-builder/src/main/java/fun/mingshan/markdown4j/encoder/block/TableBlockEncoder.java b/reference-doc-builder/src/main/java/fun/mingshan/markdown4j/encoder/block/TableBlockEncoder.java new file mode 100644 index 0000000..2b523e8 --- /dev/null +++ b/reference-doc-builder/src/main/java/fun/mingshan/markdown4j/encoder/block/TableBlockEncoder.java @@ -0,0 +1,79 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package fun.mingshan.markdown4j.encoder.block; + +import fun.mingshan.markdown4j.constant.FlagConstants; +import fun.mingshan.markdown4j.type.block.Block; +import fun.mingshan.markdown4j.type.block.BlockType; +import fun.mingshan.markdown4j.type.block.TableBlock; + +import java.util.List; +import java.util.Objects; + +/** + * 表格编码器 + * + * @author hanjuntao + * @date 2022/1/18 + */ +public class TableBlockEncoder implements BlockEncoder { + private static final String SEP = "|"; + private static final String SPE2 = "-------------"; + + @Override + public String encode(Block block) { + TableBlock tableBlock = (TableBlock) block; + List titles = tableBlock.getTitles(); + + StringBuilder result = new StringBuilder(SEP); + + // 拼接表头 + for (String title : titles) { + result.append(FlagConstants.SPACE).append(title).append(FlagConstants.SPACE).append(SEP); + } + + result.append(FlagConstants.HTML_LINE_BREAK); + + result.append(SEP); + // 拼接 表头与表内容分割 + for (int i = 0; i < titles.size(); i++) { + result.append(FlagConstants.SPACE).append(SPE2).append(SEP); + } + + result.append(FlagConstants.HTML_LINE_BREAK); + + // 拼接表格内容 + List rows = tableBlock.getRows(); + + for (TableBlock.TableRow tableRow : rows) { + List list = tableRow.getColumns(); + result.append(SEP); + for (String item : list) { + result.append(FlagConstants.SPACE); + result.append(Objects.requireNonNullElse(item, FlagConstants.SPACE)); + + result.append(SEP); + } + + result.append(FlagConstants.HTML_LINE_BREAK); + } + + return result.toString(); + } + + @Override + public BlockType getType() { + return BlockType.TABLE; + } +} diff --git a/reference-doc-builder/src/main/java/fun/mingshan/markdown4j/encoder/block/TitleBlockEncoder.java b/reference-doc-builder/src/main/java/fun/mingshan/markdown4j/encoder/block/TitleBlockEncoder.java new file mode 100644 index 0000000..9c37a0f --- /dev/null +++ b/reference-doc-builder/src/main/java/fun/mingshan/markdown4j/encoder/block/TitleBlockEncoder.java @@ -0,0 +1,59 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package fun.mingshan.markdown4j.encoder.block; + +import fun.mingshan.markdown4j.constant.FlagConstants; +import fun.mingshan.markdown4j.type.block.Block; +import fun.mingshan.markdown4j.type.block.BlockType; +import fun.mingshan.markdown4j.type.block.TitleBlock; + +/** + * 标题块解析器 + * + * @author hanjuntao + * @date 2022/1/17 + */ +public class TitleBlockEncoder implements BlockEncoder { + @Override + public String encode(Block block) { + TitleBlock titleBlock = (TitleBlock) block; + + String content = titleBlock.getContent(); + TitleBlock.Level level = titleBlock.getLevel(); + String result = ""; + + if (TitleBlock.Level.FIRST.equals(level)) { + result += FlagConstants.TITLE_BLOCK_FLAG; + } else if (TitleBlock.Level.SECOND.equals(level)) { + result += FlagConstants.TITLE_BLOCK_FLAG + FlagConstants.TITLE_BLOCK_FLAG; + } else if (TitleBlock.Level.THIRD.equals(level)) { + result += FlagConstants.TITLE_BLOCK_FLAG + FlagConstants.TITLE_BLOCK_FLAG + FlagConstants.TITLE_BLOCK_FLAG; + } else if (TitleBlock.Level.FOURTH.equals(level)) { + result += FlagConstants.TITLE_BLOCK_FLAG + FlagConstants.TITLE_BLOCK_FLAG + + FlagConstants.TITLE_BLOCK_FLAG + FlagConstants.TITLE_BLOCK_FLAG; + } else if (TitleBlock.Level.FIFTH.equals(level)) { + result += FlagConstants.TITLE_BLOCK_FLAG + FlagConstants.TITLE_BLOCK_FLAG + + FlagConstants.TITLE_BLOCK_FLAG + FlagConstants.TITLE_BLOCK_FLAG + FlagConstants.TITLE_BLOCK_FLAG; + } + + result += FlagConstants.SPACE + content; + + return result; + } + + @Override + public BlockType getType() { + return BlockType.TITLE; + } +} diff --git a/reference-doc-builder/src/main/java/fun/mingshan/markdown4j/encoder/element/BoldElementEncoder.java b/reference-doc-builder/src/main/java/fun/mingshan/markdown4j/encoder/element/BoldElementEncoder.java new file mode 100644 index 0000000..b441479 --- /dev/null +++ b/reference-doc-builder/src/main/java/fun/mingshan/markdown4j/encoder/element/BoldElementEncoder.java @@ -0,0 +1,37 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package fun.mingshan.markdown4j.encoder.element; + +import fun.mingshan.markdown4j.type.element.BoldElement; +import fun.mingshan.markdown4j.type.element.Element; +import fun.mingshan.markdown4j.type.element.ElementType; + +/** + * @author hanjuntao + * @date 2022/1/18 + */ +@SuppressWarnings("ALL") +public class BoldElementEncoder implements ElementEncoder { + @Override + public String encode(Element element) { + BoldElement boldElement = (BoldElement) element; + + return "**" + boldElement.getContent() + "**"; + } + + @Override + public ElementType getType() { + return ElementType.BOLD; + } +} diff --git a/reference-doc-builder/src/main/java/fun/mingshan/markdown4j/encoder/element/ElementEncoder.java b/reference-doc-builder/src/main/java/fun/mingshan/markdown4j/encoder/element/ElementEncoder.java new file mode 100644 index 0000000..80c44c1 --- /dev/null +++ b/reference-doc-builder/src/main/java/fun/mingshan/markdown4j/encoder/element/ElementEncoder.java @@ -0,0 +1,39 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package fun.mingshan.markdown4j.encoder.element; + +import fun.mingshan.markdown4j.type.element.Element; +import fun.mingshan.markdown4j.type.element.ElementType; + +/** + * @author hanjuntao + * @date 2022/1/18 + */ +@SuppressWarnings("ALL") +public interface ElementEncoder { + /** + * 编译 + * + * @param element java代码 + * @return markdown语法字符串 + */ + String encode(Element element); + + /** + * 返回元素类型 + * + * @return 元素类型 + */ + ElementType getType(); +} diff --git a/reference-doc-builder/src/main/java/fun/mingshan/markdown4j/encoder/element/ElementEncoderFactory.java b/reference-doc-builder/src/main/java/fun/mingshan/markdown4j/encoder/element/ElementEncoderFactory.java new file mode 100644 index 0000000..906caf8 --- /dev/null +++ b/reference-doc-builder/src/main/java/fun/mingshan/markdown4j/encoder/element/ElementEncoderFactory.java @@ -0,0 +1,45 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package fun.mingshan.markdown4j.encoder.element; + +import fun.mingshan.markdown4j.type.element.ElementType; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + * @author hanjuntao + * @date 2022/1/18 + */ +@SuppressWarnings("ALL") +public class ElementEncoderFactory { + private static final Map ENCODER_MAP = new ConcurrentHashMap<>(); + + static { + ENCODER_MAP.put(ElementType.BOLD.name(), new BoldElementEncoder()); + ENCODER_MAP.put(ElementType.INLINE_CODE.name(), new InlineCodeElementEncoder()); + ENCODER_MAP.put(ElementType.ITALIC.name(), new ItalicElementEncoder()); + ENCODER_MAP.put(ElementType.IMAGE.name(), new ImageElementEncoder()); + ENCODER_MAP.put(ElementType.URL.name(), new UrlElementEncoder()); + } + + public static ElementEncoder getEncoder(ElementType elementType) { + ElementEncoder elementEncoder = ENCODER_MAP.get(elementType.name()); + if (elementEncoder == null) { + throw new IllegalStateException("Miss elementEncoder : " + elementType.name()); + } + + return elementEncoder; + } +} diff --git a/reference-doc-builder/src/main/java/fun/mingshan/markdown4j/encoder/element/ImageElementEncoder.java b/reference-doc-builder/src/main/java/fun/mingshan/markdown4j/encoder/element/ImageElementEncoder.java new file mode 100644 index 0000000..bcb9776 --- /dev/null +++ b/reference-doc-builder/src/main/java/fun/mingshan/markdown4j/encoder/element/ImageElementEncoder.java @@ -0,0 +1,38 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package fun.mingshan.markdown4j.encoder.element; + +import fun.mingshan.markdown4j.type.element.Element; +import fun.mingshan.markdown4j.type.element.ElementType; +import fun.mingshan.markdown4j.type.element.ImageElement; + +/** + * @author hanjuntao + * @date 2022/1/18 + */ +@SuppressWarnings("JavaDoc") +public class ImageElementEncoder implements ElementEncoder { + @Override + public String encode(Element element) { + ImageElement imageElement = (ImageElement) element; + + String imageUrl = imageElement.getImageUrl(); + return "![](" + imageUrl + ")"; + } + + @Override + public ElementType getType() { + return ElementType.IMAGE; + } +} diff --git a/reference-doc-builder/src/main/java/fun/mingshan/markdown4j/encoder/element/InlineCodeElementEncoder.java b/reference-doc-builder/src/main/java/fun/mingshan/markdown4j/encoder/element/InlineCodeElementEncoder.java new file mode 100644 index 0000000..23fcbc9 --- /dev/null +++ b/reference-doc-builder/src/main/java/fun/mingshan/markdown4j/encoder/element/InlineCodeElementEncoder.java @@ -0,0 +1,35 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package fun.mingshan.markdown4j.encoder.element; + +import fun.mingshan.markdown4j.type.element.Element; +import fun.mingshan.markdown4j.type.element.ElementType; +import fun.mingshan.markdown4j.type.element.InlineCodeElement; + +/** + * @author hanjuntao + * @date 2022/1/18 + */ +public class InlineCodeElementEncoder implements ElementEncoder { + @Override + public String encode(Element element) { + InlineCodeElement inlineCodeElement = (InlineCodeElement) element; + return "`" + inlineCodeElement.getContent() + "`"; + } + + @Override + public ElementType getType() { + return ElementType.INLINE_CODE; + } +} diff --git a/reference-doc-builder/src/main/java/fun/mingshan/markdown4j/encoder/element/ItalicElementEncoder.java b/reference-doc-builder/src/main/java/fun/mingshan/markdown4j/encoder/element/ItalicElementEncoder.java new file mode 100644 index 0000000..5e9b8f6 --- /dev/null +++ b/reference-doc-builder/src/main/java/fun/mingshan/markdown4j/encoder/element/ItalicElementEncoder.java @@ -0,0 +1,35 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package fun.mingshan.markdown4j.encoder.element; + +import fun.mingshan.markdown4j.type.element.Element; +import fun.mingshan.markdown4j.type.element.ElementType; +import fun.mingshan.markdown4j.type.element.ItalicElement; + +/** + * @author hanjuntao + * @date 2022/1/18 + */ +public class ItalicElementEncoder implements ElementEncoder { + @Override + public String encode(Element element) { + ItalicElement italicElement = (ItalicElement) element; + return "_" + italicElement.getContent() + "_"; + } + + @Override + public ElementType getType() { + return ElementType.ITALIC; + } +} diff --git a/reference-doc-builder/src/main/java/fun/mingshan/markdown4j/encoder/element/UrlElementEncoder.java b/reference-doc-builder/src/main/java/fun/mingshan/markdown4j/encoder/element/UrlElementEncoder.java new file mode 100644 index 0000000..67d0c9d --- /dev/null +++ b/reference-doc-builder/src/main/java/fun/mingshan/markdown4j/encoder/element/UrlElementEncoder.java @@ -0,0 +1,46 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package fun.mingshan.markdown4j.encoder.element; + +import fun.mingshan.markdown4j.type.element.Element; +import fun.mingshan.markdown4j.type.element.ElementType; +import fun.mingshan.markdown4j.type.element.UrlElement; + +/** + * @author hanjuntao + * @date 2022/1/18 + */ +public class UrlElementEncoder implements ElementEncoder { + @Override + public String encode(Element element) { + UrlElement urlElement = (UrlElement) element; + + String url = urlElement.getUrl(); + String tips = urlElement.getTips(); + + String result = "["; + if (tips != null) { + result += tips; + } + + result += "](" + url + ")"; + + return result; + } + + @Override + public ElementType getType() { + return null; + } +} diff --git a/reference-doc-builder/src/main/java/fun/mingshan/markdown4j/type/block/Block.java b/reference-doc-builder/src/main/java/fun/mingshan/markdown4j/type/block/Block.java new file mode 100644 index 0000000..1dddb6a --- /dev/null +++ b/reference-doc-builder/src/main/java/fun/mingshan/markdown4j/type/block/Block.java @@ -0,0 +1,41 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package fun.mingshan.markdown4j.type.block; + +/** + * 块的概念,代表markdown 一块元素 + *

+ * 例如: + *

+ * 1. 代码块 + *

+ *     ```java
+ *     ccccc
+ *     ```
+ * 
+ *

+ * 2. 一个标题 + *

+ *     # 这是一个标题
+ * 
+ * + * @author hanjuntao + * @date 2022/1/17 + */ +@SuppressWarnings("JavaDoc") +public interface Block { + String toMd(); + + BlockType getType(); +} diff --git a/reference-doc-builder/src/main/java/fun/mingshan/markdown4j/type/block/BlockType.java b/reference-doc-builder/src/main/java/fun/mingshan/markdown4j/type/block/BlockType.java new file mode 100644 index 0000000..3c949f9 --- /dev/null +++ b/reference-doc-builder/src/main/java/fun/mingshan/markdown4j/type/block/BlockType.java @@ -0,0 +1,30 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package fun.mingshan.markdown4j.type.block; + +/** + * 块类型 + * + * @author hanjuntao + * @date 2022/1/17 + */ +@SuppressWarnings("JavaDoc") +public enum BlockType { + CODE, + REFERENCE, + SLATE_TABLE, + TABLE, + TITLE, + STRING +} diff --git a/reference-doc-builder/src/main/java/fun/mingshan/markdown4j/type/block/CodeBlock.java b/reference-doc-builder/src/main/java/fun/mingshan/markdown4j/type/block/CodeBlock.java new file mode 100644 index 0000000..0e6af95 --- /dev/null +++ b/reference-doc-builder/src/main/java/fun/mingshan/markdown4j/type/block/CodeBlock.java @@ -0,0 +1,118 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package fun.mingshan.markdown4j.type.block; + +import fun.mingshan.markdown4j.encoder.block.BlockEncoder; +import fun.mingshan.markdown4j.encoder.block.BlockEncoderFactory; + +import java.util.Objects; + +/** + * 代码块 + * + *
+ *     ```java
+ *     public class CodeBlock extends Block {
+ *     }
+ *     ```
+ * 
+ * + * @author hanjuntao + * @date 2022/1/17 + */ +@SuppressWarnings("ALL") +public class CodeBlock implements Block { + + private final String language; + private final String content; + + public CodeBlock(String language, String content) { + this.language = language; + this.content = content; + } + + public String getLanguage() { + return language; + } + + public String getContent() { + return content; + } + + @Override + public BlockType getType() { + return BlockType.CODE; + } + + @Override + public String toMd() { + BlockEncoder encoder = BlockEncoderFactory.getEncoder(BlockType.CODE); + return encoder.encode(this); + } + + /** + * 语言 + * + * @author hanjuntao + * @date 2022/1/17 + */ + public enum Language { + JAVA("Java"), + C("C"), + CPLUSPLUS("C++"), + JAVASCRIPT("Javascript"), + PYTHON("Python"), + CLI("Bisq CLI"); + + private final String description; + + Language(String description) { + this.description = description; + } + + public String description() { + return description; + } + } + + + public static CodeBlockBuilder builder() { + return new CodeBlockBuilder(); + } + + public static class CodeBlockBuilder { + private String language; + private String content; + + CodeBlockBuilder() { + } + + + public CodeBlockBuilder language(String language) { + Objects.requireNonNull(language, "language cannot be null"); + this.language = language; + return this; + } + + public CodeBlockBuilder content(String content) { + Objects.requireNonNull(content, "content cannot be null"); + this.content = content; + return this; + } + + public CodeBlock build() { + return new CodeBlock(this.language, this.content); + } + } +} diff --git a/reference-doc-builder/src/main/java/fun/mingshan/markdown4j/type/block/ReferenceBlock.java b/reference-doc-builder/src/main/java/fun/mingshan/markdown4j/type/block/ReferenceBlock.java new file mode 100644 index 0000000..40e97b7 --- /dev/null +++ b/reference-doc-builder/src/main/java/fun/mingshan/markdown4j/type/block/ReferenceBlock.java @@ -0,0 +1,75 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package fun.mingshan.markdown4j.type.block; + +import fun.mingshan.markdown4j.encoder.block.BlockEncoder; +import fun.mingshan.markdown4j.encoder.block.BlockEncoderFactory; + +import java.util.Objects; + +/** + * 引用块 + * + *
+ *     > aadadad
+ * 
+ * + * @author hanjuntao + * @date 2022/1/17 + */ +public class ReferenceBlock implements Block { + + private final String content; + + public ReferenceBlock(String content) { + this.content = content; + } + + public String getContent() { + return content; + } + + @Override + public BlockType getType() { + return BlockType.REFERENCE; + } + + @Override + public String toMd() { + BlockEncoder encoder = BlockEncoderFactory.getEncoder(BlockType.REFERENCE); + return encoder.encode(this); + } + + + public static ReferenceBlockBuilder builder() { + return new ReferenceBlockBuilder(); + } + + public static class ReferenceBlockBuilder { + private String content; + + ReferenceBlockBuilder() { + } + + public ReferenceBlockBuilder content(String content) { + Objects.requireNonNull(content, "content cannot be null"); + this.content = content; + return this; + } + + public ReferenceBlock build() { + return new ReferenceBlock(this.content); + } + } +} diff --git a/reference-doc-builder/src/main/java/fun/mingshan/markdown4j/type/block/SlateTableBlock.java b/reference-doc-builder/src/main/java/fun/mingshan/markdown4j/type/block/SlateTableBlock.java new file mode 100644 index 0000000..2442d28 --- /dev/null +++ b/reference-doc-builder/src/main/java/fun/mingshan/markdown4j/type/block/SlateTableBlock.java @@ -0,0 +1,114 @@ +/* + * This file is part of Bisq. + * + * Bisq is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bisq is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bisq. If not, see . + */ +package fun.mingshan.markdown4j.type.block; + +import fun.mingshan.markdown4j.encoder.block.BlockEncoder; +import fun.mingshan.markdown4j.encoder.block.BlockEncoderFactory; + +import java.util.List; +import java.util.Objects; + +/** + * Slate Tables use PHP Markdown Extra style tables: + *

+ *

+ * Table Header 1 | Table Header 2 | Table Header 3
+ * -------------- | -------------- | --------------
+ * Row 1 col 1 | Row 1 col 2 | Row 1 col 3
+ * Row 2 col 1 | Row 2 col 2 | Row 2 col 3
+ * 
+ * Note that the pipes do not need to line up with each other on each line. + *

+ */ +public class SlateTableBlock implements Block { + + private final List titles; + private final List rows; + + public SlateTableBlock(List titles, List rows) { + this.titles = titles; + this.rows = rows; + } + + public List getTitles() { + return titles; + } + + public List getRows() { + return rows; + } + + @Override + public BlockType getType() { + return BlockType.SLATE_TABLE; + } + + @Override + public String toMd() { + BlockEncoder encoder = BlockEncoderFactory.getEncoder(BlockType.SLATE_TABLE); + return encoder.encode(this); + } + + // @Data + public static class TableRow { + private List columns; + + public TableRow() { + } + + public TableRow(List columns) { + this.columns = columns; + } + + public List getColumns() { + return columns; + } + + public void setColumns(List columns) { + this.columns = columns; + } + } + + + public static SlateTableBlockBuilder builder() { + return new SlateTableBlockBuilder(); + } + + public static class SlateTableBlockBuilder { + private List titles; + private List rows; + + SlateTableBlockBuilder() { + } + + public SlateTableBlockBuilder titles(List titles) { + Objects.requireNonNull(titles, "titles cannot be null"); + this.titles = titles; + return this; + } + + public SlateTableBlockBuilder rows(List rows) { + Objects.requireNonNull(titles, "rows cannot be null"); + this.rows = rows; + return this; + } + + public SlateTableBlock build() { + return new SlateTableBlock(this.titles, this.rows); + } + } +} diff --git a/reference-doc-builder/src/main/java/fun/mingshan/markdown4j/type/block/StringBlock.java b/reference-doc-builder/src/main/java/fun/mingshan/markdown4j/type/block/StringBlock.java new file mode 100644 index 0000000..ee176bc --- /dev/null +++ b/reference-doc-builder/src/main/java/fun/mingshan/markdown4j/type/block/StringBlock.java @@ -0,0 +1,64 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package fun.mingshan.markdown4j.type.block; + +import java.util.Objects; + +/** + * @author hanjuntao + * @date 2022/1/18 + */ +public class StringBlock implements Block { + + private final String content; + + public StringBlock(String content) { + this.content = content; + } + + public String getContent() { + return this.content; + } + + @Override + public String toMd() { + return content; + } + + @Override + public BlockType getType() { + return BlockType.STRING; + } + + public static StringBlockBuilder builder() { + return new StringBlockBuilder(); + } + + public static class StringBlockBuilder { + private String content; + + StringBlockBuilder() { + } + + public StringBlockBuilder content(String content) { + Objects.requireNonNull(content, "content cannot be null"); + this.content = content; + return this; + } + + public StringBlock build() { + return new StringBlock(this.content); + } + } +} diff --git a/reference-doc-builder/src/main/java/fun/mingshan/markdown4j/type/block/TableBlock.java b/reference-doc-builder/src/main/java/fun/mingshan/markdown4j/type/block/TableBlock.java new file mode 100644 index 0000000..0b20c49 --- /dev/null +++ b/reference-doc-builder/src/main/java/fun/mingshan/markdown4j/type/block/TableBlock.java @@ -0,0 +1,111 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package fun.mingshan.markdown4j.type.block; + +import fun.mingshan.markdown4j.encoder.block.BlockEncoder; +import fun.mingshan.markdown4j.encoder.block.BlockEncoderFactory; + +import java.util.List; +import java.util.Objects; + +/** + * 表格块 + * + *

+ * | header1 | header2 |
+ * | ------- | ------- |
+ * |   wqeq  |  qwq    |
+ * 
+ * + * @author hanjuntao + * @date 2022/1/17 + */ +public class TableBlock implements Block { + + private final List titles; + private final List rows; + + public TableBlock(List titles, List rows) { + this.titles = titles; + this.rows = rows; + } + + public List getTitles() { + return titles; + } + + public List getRows() { + return rows; + } + + @Override + public BlockType getType() { + return BlockType.TABLE; + } + + @Override + public String toMd() { + BlockEncoder encoder = BlockEncoderFactory.getEncoder(BlockType.TABLE); + return encoder.encode(this); + } + + // @Data + public static class TableRow { + private List columns; + + public TableRow() { + } + + public TableRow(List columns) { + this.columns = columns; + } + + public List getColumns() { + return columns; + } + + public void setColumns(List columns) { + this.columns = columns; + } + } + + + public static TableBlockBuilder builder() { + return new TableBlockBuilder(); + } + + public static class TableBlockBuilder { + private List titles; + private List rows; + + TableBlockBuilder() { + } + + public TableBlockBuilder titles(List titles) { + Objects.requireNonNull(titles, "titles cannot be null"); + this.titles = titles; + return this; + } + + public TableBlockBuilder rows(List rows) { + Objects.requireNonNull(titles, "rows cannot be null"); + this.rows = rows; + return this; + } + + public TableBlock build() { + return new TableBlock(this.titles, this.rows); + } + } +} diff --git a/reference-doc-builder/src/main/java/fun/mingshan/markdown4j/type/block/TitleBlock.java b/reference-doc-builder/src/main/java/fun/mingshan/markdown4j/type/block/TitleBlock.java new file mode 100644 index 0000000..734b762 --- /dev/null +++ b/reference-doc-builder/src/main/java/fun/mingshan/markdown4j/type/block/TitleBlock.java @@ -0,0 +1,102 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package fun.mingshan.markdown4j.type.block; + +import fun.mingshan.markdown4j.encoder.block.BlockEncoder; +import fun.mingshan.markdown4j.encoder.block.BlockEncoderFactory; + +import java.util.Objects; + +/** + * 标题块 + * + *
+ *     # 一级标题
+ *     ## 二级标题
+ *     ### 三级标题
+ *     #### 四级标题
+ *     #####  五级标题
+ * 
+ * + * @author hanjuntao + * @date 2022/1/17 + */ +public class TitleBlock implements Block { + + private final Level level; + private final String content; + + public TitleBlock(Level level, String content) { + this.level = level; + this.content = content; + } + + public Level getLevel() { + return level; + } + + public String getContent() { + return content; + } + + @Override + public BlockType getType() { + return BlockType.TITLE; + } + + @Override + public String toMd() { + BlockEncoder encoder = BlockEncoderFactory.getEncoder(BlockType.TITLE); + return encoder.encode(this); + } + + /** + * 标题级别枚举 + */ + public enum Level { + FIRST, + SECOND, + THIRD, + FOURTH, + FIFTH + } + + public static TitleBlockBuilder builder() { + return new TitleBlockBuilder(); + } + + public static class TitleBlockBuilder { + private Level level; + private String content; + + TitleBlockBuilder() { + } + + public TitleBlockBuilder level(Level level) { + Objects.requireNonNull(level, "level cannot be null"); + this.level = level; + return this; + } + + public TitleBlockBuilder content(String content) { + Objects.requireNonNull(content, "content cannot be null"); + this.content = content; + return this; + } + + public TitleBlock build() { + return new TitleBlock(this.level, this.content); + } + } +} diff --git a/reference-doc-builder/src/main/java/fun/mingshan/markdown4j/type/element/BoldElement.java b/reference-doc-builder/src/main/java/fun/mingshan/markdown4j/type/element/BoldElement.java new file mode 100644 index 0000000..b62294a --- /dev/null +++ b/reference-doc-builder/src/main/java/fun/mingshan/markdown4j/type/element/BoldElement.java @@ -0,0 +1,70 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package fun.mingshan.markdown4j.type.element; + +import fun.mingshan.markdown4j.encoder.element.ElementEncoder; +import fun.mingshan.markdown4j.encoder.element.ElementEncoderFactory; + +import java.util.Objects; + +/** + * 加粗 + * + * @author hanjuntao + * @date 2022/1/18 + */ +@SuppressWarnings("ALL") +public class BoldElement implements Element { + private final String content; + + public BoldElement(String content) { + this.content = content; + } + + public String getContent() { + return content; + } + + @Override + public String toMd() { + ElementEncoder encoder = ElementEncoderFactory.getEncoder(ElementType.BOLD); + return encoder.encode(this); + } + + @Override + public ElementType getType() { + return ElementType.BOLD; + } + + public static BoldElementBuilder builder() { + return new BoldElementBuilder(); + } + + public static class BoldElementBuilder { + private String content; + + BoldElementBuilder() { + } + + public BoldElementBuilder content(String content) { + Objects.requireNonNull(content, "content cannot be null"); + this.content = content; + return this; + } + + public BoldElement build() { + return new BoldElement(this.content); + } + } +} diff --git a/reference-doc-builder/src/main/java/fun/mingshan/markdown4j/type/element/Element.java b/reference-doc-builder/src/main/java/fun/mingshan/markdown4j/type/element/Element.java new file mode 100644 index 0000000..8a49fb1 --- /dev/null +++ b/reference-doc-builder/src/main/java/fun/mingshan/markdown4j/type/element/Element.java @@ -0,0 +1,33 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package fun.mingshan.markdown4j.type.element; + +import fun.mingshan.markdown4j.type.block.StringBlock; + +/** + * 元素 + * + * @author hanjuntao + * @date 2022/1/17 + */ +@SuppressWarnings("ALL") +public interface Element { + String toMd(); + + ElementType getType(); + + default StringBlock toBlock() { + return StringBlock.builder().content(toMd()).build(); + } +} diff --git a/reference-doc-builder/src/main/java/fun/mingshan/markdown4j/type/element/ElementType.java b/reference-doc-builder/src/main/java/fun/mingshan/markdown4j/type/element/ElementType.java new file mode 100644 index 0000000..2470bdd --- /dev/null +++ b/reference-doc-builder/src/main/java/fun/mingshan/markdown4j/type/element/ElementType.java @@ -0,0 +1,30 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package fun.mingshan.markdown4j.type.element; + +/** + * 元素类型 + * + * @author hanjuntao + * @date 2022/1/17 + */ +@SuppressWarnings("JavaDoc") +public enum ElementType { + STRING, + URL, + IMAGE, + BOLD, + ITALIC, + INLINE_CODE +} diff --git a/reference-doc-builder/src/main/java/fun/mingshan/markdown4j/type/element/ImageElement.java b/reference-doc-builder/src/main/java/fun/mingshan/markdown4j/type/element/ImageElement.java new file mode 100644 index 0000000..7114254 --- /dev/null +++ b/reference-doc-builder/src/main/java/fun/mingshan/markdown4j/type/element/ImageElement.java @@ -0,0 +1,74 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package fun.mingshan.markdown4j.type.element; + +import fun.mingshan.markdown4j.encoder.element.ElementEncoder; +import fun.mingshan.markdown4j.encoder.element.ElementEncoderFactory; + +import java.util.Objects; + +/** + * 图片元素: + * + *
+ *     ![](https://pandao.github.io/editor.md/examples/images/4.jpg)
+ * 
+ * + * @author hanjuntao + * @date 2022/1/17 + */ +@SuppressWarnings({"ALL", "ClassCanBeRecord"}) +public class ImageElement implements Element { + private final String imageUrl; + + public ImageElement(String imageUrl) { + this.imageUrl = imageUrl; + } + + public String getImageUrl() { + return imageUrl; + } + + @Override + public String toMd() { + ElementEncoder encoder = ElementEncoderFactory.getEncoder(ElementType.IMAGE); + return encoder.encode(this); + } + + @Override + public ElementType getType() { + return ElementType.IMAGE; + } + + public static ImageElementBuilder builder() { + return new ImageElementBuilder(); + } + + public static class ImageElementBuilder { + private String imageUrl; + + ImageElementBuilder() { + } + + public ImageElementBuilder imageUrl(String imageUrl) { + Objects.requireNonNull(imageUrl, "imageUrl cannot be null"); + this.imageUrl = imageUrl; + return this; + } + + public ImageElement build() { + return new ImageElement(this.imageUrl); + } + } +} diff --git a/reference-doc-builder/src/main/java/fun/mingshan/markdown4j/type/element/InlineCodeElement.java b/reference-doc-builder/src/main/java/fun/mingshan/markdown4j/type/element/InlineCodeElement.java new file mode 100644 index 0000000..df17712 --- /dev/null +++ b/reference-doc-builder/src/main/java/fun/mingshan/markdown4j/type/element/InlineCodeElement.java @@ -0,0 +1,70 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package fun.mingshan.markdown4j.type.element; + +import fun.mingshan.markdown4j.encoder.element.ElementEncoder; +import fun.mingshan.markdown4j.encoder.element.ElementEncoderFactory; + +import java.util.Objects; + +/** + * @author hanjuntao + * @date 2022/1/18 + */ +@SuppressWarnings("ALL") +public class InlineCodeElement implements Element { + + private final String content; + + public InlineCodeElement(String content) { + this.content = content; + } + + public String getContent() { + return content; + } + + @Override + public String toMd() { + ElementEncoder encoder = ElementEncoderFactory.getEncoder(ElementType.INLINE_CODE); + return encoder.encode(this); + } + + @Override + public ElementType getType() { + return ElementType.INLINE_CODE; + } + + + public static InlineCodeElementBuilder builder() { + return new InlineCodeElementBuilder(); + } + + public static class InlineCodeElementBuilder { + private String content; + + InlineCodeElementBuilder() { + } + + public InlineCodeElementBuilder content(String content) { + Objects.requireNonNull(content, "content cannot be null"); + this.content = content; + return this; + } + + public InlineCodeElement build() { + return new InlineCodeElement(this.content); + } + } +} diff --git a/reference-doc-builder/src/main/java/fun/mingshan/markdown4j/type/element/ItalicElement.java b/reference-doc-builder/src/main/java/fun/mingshan/markdown4j/type/element/ItalicElement.java new file mode 100644 index 0000000..8701459 --- /dev/null +++ b/reference-doc-builder/src/main/java/fun/mingshan/markdown4j/type/element/ItalicElement.java @@ -0,0 +1,75 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package fun.mingshan.markdown4j.type.element; + +import fun.mingshan.markdown4j.encoder.element.ElementEncoder; +import fun.mingshan.markdown4j.encoder.element.ElementEncoderFactory; + +import java.util.Objects; + +/** + * 斜体 + * + *
+ *     _斜体字_
+ * 
+ * + * @author hanjuntao + * @date 2022/1/18 + */ +@SuppressWarnings({"JavaDoc", "ClassCanBeRecord"}) +public class ItalicElement implements Element { + private final String content; + + public ItalicElement(String content) { + this.content = content; + } + + public String getContent() { + return content; + } + + @Override + public String toMd() { + ElementEncoder encoder = ElementEncoderFactory.getEncoder(ElementType.ITALIC); + return encoder.encode(this); + } + + @Override + public ElementType getType() { + return ElementType.ITALIC; + } + + + public static ItalicElementBuilder builder() { + return new ItalicElementBuilder(); + } + + public static class ItalicElementBuilder { + private String content; + + ItalicElementBuilder() { + } + + public ItalicElementBuilder content(String content) { + Objects.requireNonNull(content, "content cannot be null"); + this.content = content; + return this; + } + + public ItalicElement build() { + return new ItalicElement(this.content); + } + } +} diff --git a/reference-doc-builder/src/main/java/fun/mingshan/markdown4j/type/element/StringElement.java b/reference-doc-builder/src/main/java/fun/mingshan/markdown4j/type/element/StringElement.java new file mode 100644 index 0000000..24917b0 --- /dev/null +++ b/reference-doc-builder/src/main/java/fun/mingshan/markdown4j/type/element/StringElement.java @@ -0,0 +1,65 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package fun.mingshan.markdown4j.type.element; + +import java.util.Objects; + +/** + * @author hanjuntao + * @date 2022/1/18 + */ +@SuppressWarnings("ALL") +public class StringElement implements Element { + + private final String content; + + public StringElement(String content) { + this.content = content; + } + + public String getContent() { + return content; + } + + @Override + public String toMd() { + return this.content; + } + + @Override + public ElementType getType() { + return ElementType.STRING; + } + + public static StringElementBuilder builder() { + return new StringElementBuilder(); + } + + public static class StringElementBuilder { + private String content; + + StringElementBuilder() { + } + + public StringElementBuilder content(String content) { + Objects.requireNonNull(content, "content cannot be null"); + this.content = content; + return this; + } + + public StringElement build() { + return new StringElement(this.content); + } + } +} diff --git a/reference-doc-builder/src/main/java/fun/mingshan/markdown4j/type/element/UrlElement.java b/reference-doc-builder/src/main/java/fun/mingshan/markdown4j/type/element/UrlElement.java new file mode 100644 index 0000000..85effd0 --- /dev/null +++ b/reference-doc-builder/src/main/java/fun/mingshan/markdown4j/type/element/UrlElement.java @@ -0,0 +1,86 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package fun.mingshan.markdown4j.type.element; + +import fun.mingshan.markdown4j.encoder.element.ElementEncoder; +import fun.mingshan.markdown4j.encoder.element.ElementEncoderFactory; + +import java.util.Objects; + +/** + *
+ *     [普通链接](http://localhost/)
+ * 
+ * + * @author hanjuntao + * @date 2022/1/17 + */ +@SuppressWarnings({"JavaDoc", "ClassCanBeRecord"}) +public class UrlElement implements Element { + + private final String tips; + private final String url; + + public UrlElement(String tips, String url) { + this.tips = tips; + this.url = url; + } + + public String getTips() { + return tips; + } + + public String getUrl() { + return url; + } + + @Override + public String toMd() { + ElementEncoder encoder = ElementEncoderFactory.getEncoder(ElementType.URL); + return encoder.encode(this); + } + + @Override + public ElementType getType() { + return ElementType.URL; + } + + public static UrlElementBuilder builder() { + return new UrlElementBuilder(); + } + + public static class UrlElementBuilder { + private String tips; + private String url; + + UrlElementBuilder() { + } + + public UrlElementBuilder tips(String tips) { + Objects.requireNonNull(tips, "tips cannot be null"); + this.tips = tips; + return this; + } + + public UrlElementBuilder url(String url) { + Objects.requireNonNull(url, "url cannot be null"); + this.url = url; + return this; + } + + public UrlElement build() { + return new UrlElement(this.tips, this.url); + } + } +} diff --git a/reference-doc-builder/src/main/java/fun/mingshan/markdown4j/writer/MdWriter.java b/reference-doc-builder/src/main/java/fun/mingshan/markdown4j/writer/MdWriter.java new file mode 100644 index 0000000..2ed2566 --- /dev/null +++ b/reference-doc-builder/src/main/java/fun/mingshan/markdown4j/writer/MdWriter.java @@ -0,0 +1,45 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package fun.mingshan.markdown4j.writer; + +import fun.mingshan.markdown4j.Markdown; + +import java.io.IOException; +import java.io.PrintWriter; +import java.nio.file.Path; +import java.nio.file.Paths; + +import static java.nio.charset.StandardCharsets.UTF_8; + +/** + * @author hanjuntao + * @date 2022/1/18 + */ +@SuppressWarnings("ALL") +public class MdWriter { + + public static void write(Markdown markdown) throws IOException { + write(Paths.get("./"), markdown); + } + + public static void write(Path dir, Markdown markdown) throws IOException { + String fileName = dir.resolve(markdown.getName() + ".md").toString(); + PrintWriter printWriter = new PrintWriter(fileName, UTF_8); + byte[] bytes = markdown.toString().getBytes(UTF_8); + String unicodeContent = new String(bytes, UTF_8); + printWriter.write(unicodeContent); + printWriter.flush(); + printWriter.close(); + } +} diff --git a/reference-doc-builder/src/main/resources/images/bisq-logo-svg-as.png b/reference-doc-builder/src/main/resources/images/bisq-logo-svg-as.png new file mode 100644 index 0000000..dea37f4 Binary files /dev/null and b/reference-doc-builder/src/main/resources/images/bisq-logo-svg-as.png differ diff --git a/reference-doc-builder/src/main/resources/images/favicon.ico b/reference-doc-builder/src/main/resources/images/favicon.ico new file mode 100644 index 0000000..30f589b Binary files /dev/null and b/reference-doc-builder/src/main/resources/images/favicon.ico differ diff --git a/reference-doc-builder/src/main/resources/logback.xml b/reference-doc-builder/src/main/resources/logback.xml new file mode 100644 index 0000000..92a7893 --- /dev/null +++ b/reference-doc-builder/src/main/resources/logback.xml @@ -0,0 +1,14 @@ + + + + + %highlight(%d{MMM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{30}: %msg %xEx%n) + + + + + + + + + diff --git a/reference-doc-builder/src/main/resources/templates/authentication.md b/reference-doc-builder/src/main/resources/templates/authentication.md new file mode 100644 index 0000000..fca8d8c --- /dev/null +++ b/reference-doc-builder/src/main/resources/templates/authentication.md @@ -0,0 +1,9 @@ +# Authentication + +An API password option `--apiPassword=` is passed in the daemon's start command. + +All API client requests must authenticate to the daemon with the api-password. + +* Each CLI command must include the `--password=` option. + +* API authentication from Java and Python requests are demonstrated in API usage examples. diff --git a/reference-doc-builder/src/main/resources/templates/errors.md b/reference-doc-builder/src/main/resources/templates/errors.md new file mode 100644 index 0000000..22cfb42 --- /dev/null +++ b/reference-doc-builder/src/main/resources/templates/errors.md @@ -0,0 +1,20 @@ +# Errors + +Errors sent from the Java-based gRPC daemon to gRPC clients are instances +of [StatusRuntimeException](https://github.com/grpc/grpc-java/blob/master/api/src/main/java/io/grpc/StatusRuntimeException.java) +. For non-Java gRPC clients, the equivelant gRPC error type is sent. Exceptions sent to gRPC clients contain +a [Status.Code](https://github.com/grpc/grpc-java/blob/master/api/src/main/java/io/grpc/Status.java) to aid client-side +error handling. The Bisq API daemon does not use all sixteen of the gRPC status codes, below are the currently used +status codes. + +Code | Value | Description + ------------- |-------| ------------- +UNKNOWN | 2 | An unexpected error occurred in the daemon, and it was not mapped to a meaningful status code. +INVALID_ARGUMENT | 3 | An invalid parameter value was sent to the daemon. +NOT_FOUND | 5 | A requested entity was not found. +ALREADY_EXISTS | 6 | An attempt to change some value or state in the daemon failed because the value or state already exists. +FAILED_PRECONDITION | 9 | An attempted operation failed because some pre-condition was not met. +UNIMPLEMENTED | 12 | An attempt was made to perform an unsupported operation. +UNAVAILABLE | 14 | Some resource is not available at the time requested. +UNAUTHENTICATED | 16 | The gRPC client did not correctly authenticate to the daemon. + diff --git a/reference-doc-builder/src/main/resources/templates/examples-setup.md b/reference-doc-builder/src/main/resources/templates/examples-setup.md new file mode 100644 index 0000000..6d68175 --- /dev/null +++ b/reference-doc-builder/src/main/resources/templates/examples-setup.md @@ -0,0 +1,133 @@ +# Running Example Code + +Examples should not be used to make calls to an API daemon connected to the bitcoin mainnet. There is a convenient way +to run a regtest bitcoin-core daemon, a Bisq seed node, an arbitration node, and two regtest API daemons called Alice ( +listening on port 9998), and Bob (listening on port 9999). The Bob and Alice daemons will have regtest wallets +containing 10 BTC. Bob's BSQ wallet will also be set up with 1500000 BSQ, Alice's with 1000000 BSQ. These two API +daemons can simulate trading over the local regtest network. + +See +the [Bisq API Beta Testing Guide](https://github.com/bisq-network/bisq/blob/master/apitest/docs/api-beta-test-guide.md) +for instructions on how to get the Bisq API test harness up and running. + +## CLI Examples + +The API CLI is included in Bisq; no protobuf code generation tasks are required to run the CLI examples in this +document. + +The only requirements are: + +- A running, local API daemon, preferably the test harness described in + the [Bisq API Beta Testing Guide](https://github.com/bisq-network/bisq/blob/master/apitest/docs/api-beta-test-guide.md) +- A terminal open in the Bisq source project's root directory + +## Java Examples + +Running Java examples requires: + +- A running, local API daemon, preferably the test harness described in + the [Bisq API Beta Testing Guide](https://github.com/bisq-network/bisq/blob/master/apitest/docs/api-beta-test-guide.md) +- Downloading Bisq protobuf definition files +- Generating protobuf and gRPC service stubs using the the [protoc](https://grpc.io/docs/protoc-installation/) compiler, + with the [protoc-gen-grpc-java](https://github.com/grpc/grpc-java) plugin. + +### Download the Bisq .proto files to your Java project + +If your Java source is located in a directory named `my-api-app/src/main`, open a terminal in your project root +directory (`my-api-app`), and the Bisq .proto files are located in a directory named `my-api-app/src/main/proto`: + + `$ export PROTO_PATH="src/main/proto"`
+ `$ curl -o $PROTO_PATH/pb.proto https://raw.githubusercontent.com/bisq-network/bisq/master/proto/src/main/proto/pb.proto`
+ `$ curl -o $PROTO_PATH/grpc.proto https://raw.githubusercontent.com/bisq-network/bisq/master/proto/src/main/proto/grpc.proto` + +### Generate Bisq API protobuf stubs using Gradle grpc-java plugin (recommended) + +You can generate the API stubs in a Gradle project using the [protoc-gen-grpc-java](https://github.com/grpc/grpc-java) +plugin. The [build.gradle](https://github.com/ghubstan/bisq-grpc-api-doc/blob/main/build.gradle) +file used in the project that builds this document could be modified to suit you. + +_Note: You can also generate stubs with [protoc-gen-grpc-java](https://github.com/grpc/grpc-java) in maven projects._ + +### Generate Bisq API protobuf stubs using grpc-java plugin from terminal + +If you prefer to generate the Java protos from a terminal, you can compile +the [protoc gen-java](https://github.com/grpc/grpc-java/blob/master/COMPILING.md) binary from source, or manually +download the [binary](https://repo1.maven.org/maven2/io/grpc/protoc-gen-grpc-java/) to your system `PATH`, and +run `protoc` with the appropriate options: + + `$ protoc --plugin=protoc-gen-grpc-java=$GEN_GRPC_JAVA_BINARY_PATH \`
+ `--grpc-java_out=$JAVA_PROTO_OUT_PATH \`
+ `--proto_path=$PROTO_PATH \`
+ `$PROTO_PATH/*.proto`
+ +_Note: My attempts to compile the protoc gen-java plugin on my own platform were unsuccessful. You may have better luck +or time to resolve platform specific build issues._ + +## Python Examples + +Running Python examples requires: + +- A running, local API daemon, preferably the test harness described in + the [Bisq API Beta Testing Guide](https://github.com/bisq-network/bisq/blob/master/apitest/docs/api-beta-test-guide.md) +- Downloading Bisq protobuf definition files +- Generating protobuf and gRPC service stubs using the `protoc` compiler, with two additional Python protobuf and grpc + plugins. + +Instructions for generating the Bisq API Python stubs follow. + +### Install python plugins for protoc compiler + + `$ python -m pip install grpcio grpcio-tools`
+ `$ pip3 install mypy-protobuf` + +### Download the Bisq .proto files to your Python project + +If your Python scripts are located in a directory named `my-api-scripts`, open a terminal in that directory and +download the Bisq .proto files to a directory named `my-api-scripts/proto`: + + `$ export PROTO_PATH="proto"`
+ `$ curl -o $PROTO_PATH/pb.proto https://raw.githubusercontent.com/bisq-network/bisq/master/proto/src/main/proto/pb.proto`
+ `$ curl -o $PROTO_PATH/grpc.proto https://raw.githubusercontent.com/bisq-network/bisq/master/proto/src/main/proto/grpc.proto` + +### Generate Bisq API gRPC Python stubs + +The location of Python code generated from downloaded Bisq .proto files _must_ be `/bisq/api`, or +this document's example Python scripts will not be able to import them at runtime. Put another way, if your Python +scripts are in a folder named `my-api-scripts`, the `protoc` compiler must generate Python stubs +in `my-api-scripts/bisq/api`. Each step of the setup is explained below. + +If + +- Python scripts are located in `my-api-scripts` +- Downloaded Bisq .proto files are located in `my-api-scripts/proto` +- A terminal is opened in directory `my-api-scripts` + +Generate API Python stubs in `my-api-scripts/bisq/api`, as follows: + + `$ export PROTO_PATH="proto"`
+ `$ export PYTHON_PROTO_OUT_PATH="bisq/api"`
+
+ `$ python3 -m grpc_tools.protoc \`
+ `--proto_path=$PROTO_PATH \`
+ `--python_out=$PYTHON_PROTO_OUT_PATH \`
+ `--grpc_python_out=$PYTHON_PROTO_OUT_PATH $PROTO_PATH/*.proto`
+
+ `$ protoc --proto_path=$PROTO_PATH --python_out=$PYTHON_PROTO_OUT_PATH $PROTO_PATH/*.proto`
+
+ `# Hack two internal import statements in the generated Python code to prepend the bisq.api package name.`
+ `# See why @ https://github.com/protocolbuffers/protobuf/issues/1491`
+
+ `sed -i 's/import pb_pb2 as pb__pb2/import bisq.api.pb_pb2 as pb__pb2/g' $PYTHON_PROTO_OUT_PATH/grpc_pb2.py`
+ `sed -i 's/import grpc_pb2 as grpc__pb2/import bisq.api.grpc_pb2 as grpc__pb2/g' $PYTHON_PROTO_OUT_PATH/grpc_pb2_grpc.py` + +Now you should be able to import the Bisq gRPC API stubs into scripts located in `my-api-scripts`, using the following +Python import statements: + + `import grpc`
+ `import bisq.api.grpc_pb2 as bisq_messages`
+ `import bisq.api.grpc_pb2_grpc as bisq_service` + +### _TODO Simplify these Python example setup sections..._ + +_if more experienced Python developers can demonstrate an easier, more flexible way of generating protobuf / grpc stubs +without having to manually hack internal import statements._ diff --git a/reference-doc-builder/src/main/resources/templates/global-enum.md b/reference-doc-builder/src/main/resources/templates/global-enum.md new file mode 100644 index 0000000..9c6dbe1 --- /dev/null +++ b/reference-doc-builder/src/main/resources/templates/global-enum.md @@ -0,0 +1,4 @@ +## {{enum.name}} +{{enum.description}} + +{{enum.constant.tbl}} diff --git a/reference-doc-builder/src/main/resources/templates/grpc-enums.md b/reference-doc-builder/src/main/resources/templates/grpc-enums.md new file mode 100644 index 0000000..2e70621 --- /dev/null +++ b/reference-doc-builder/src/main/resources/templates/grpc-enums.md @@ -0,0 +1 @@ +# gRPC Enums diff --git a/reference-doc-builder/src/main/resources/templates/grpc-messages.md b/reference-doc-builder/src/main/resources/templates/grpc-messages.md new file mode 100644 index 0000000..f0f825f --- /dev/null +++ b/reference-doc-builder/src/main/resources/templates/grpc-messages.md @@ -0,0 +1 @@ +# gRPC Messages diff --git a/reference-doc-builder/src/main/resources/templates/grpc-service.md b/reference-doc-builder/src/main/resources/templates/grpc-service.md new file mode 100644 index 0000000..1764617 --- /dev/null +++ b/reference-doc-builder/src/main/resources/templates/grpc-service.md @@ -0,0 +1,2 @@ +# Service {{service.name}} +{{service.description}} diff --git a/reference-doc-builder/src/main/resources/templates/header.md b/reference-doc-builder/src/main/resources/templates/header.md new file mode 100644 index 0000000..0df8838 --- /dev/null +++ b/reference-doc-builder/src/main/resources/templates/header.md @@ -0,0 +1,22 @@ +--- +title: Bisq gRPC API Reference + +language_tabs: + - shell: CLI + - java: Java + - python: Python + +toc_footers: + - Bisq Site + - Bisq Source Code + - Documentation Powered by Slate + +search: true + +code_clipboard: true + +meta: + - name: description + content: Documentation for the Bisq gRPC API +--- + diff --git a/reference-doc-builder/src/main/resources/templates/introduction.md b/reference-doc-builder/src/main/resources/templates/introduction.md new file mode 100644 index 0000000..8f67a6a --- /dev/null +++ b/reference-doc-builder/src/main/resources/templates/introduction.md @@ -0,0 +1,20 @@ +# Introduction + +Welcome to the Bisq gRPC API reference documentation for Bisq Daemon. + +You can use this API to access local Bisq daemon API endpoints, which provide a subset of the Bisq Desktop application's +feature set: check balances, transfer BTC and BSQ, create payment accounts, view offers, create and take offers, and +execute trades. + +The Bisq API is based on the gRPC framework, and any supported gRPC language binding can be used to call Bisq API +endpoints. This document provides code examples for language bindings in Java and Python, plus bisq-cli (CLI) command +examples. The code examples are viewable in the dark area to the right, and you can switch between programming language +examples with the tabs on the top right. + +The original *.proto files from which the gRPC documentation was generated can be found here: + +* [pb.proto](https://github.com/bisq-network/bisq/tree/master/proto/src/main/proto/pb.proto) +* [grpc.proto](https://github.com/bisq-network/bisq/tree/master/proto/src/main/proto/grpc.proto) + +This API documentation was created with [Slate](https://github.com/slatedocs/slate). + diff --git a/reference-doc-builder/src/main/resources/templates/message-enum.md b/reference-doc-builder/src/main/resources/templates/message-enum.md new file mode 100644 index 0000000..b1df908 --- /dev/null +++ b/reference-doc-builder/src/main/resources/templates/message-enum.md @@ -0,0 +1,4 @@ +### Enum: {{enum.name}} +{{enum.description}} + +{{enum.constant.tbl}} diff --git a/reference-doc-builder/src/main/resources/templates/message-fields.md b/reference-doc-builder/src/main/resources/templates/message-fields.md new file mode 100644 index 0000000..c919fc2 --- /dev/null +++ b/reference-doc-builder/src/main/resources/templates/message-fields.md @@ -0,0 +1 @@ +{{field.tbl}} diff --git a/reference-doc-builder/src/main/resources/templates/message.md b/reference-doc-builder/src/main/resources/templates/message.md new file mode 100644 index 0000000..e977d2b --- /dev/null +++ b/reference-doc-builder/src/main/resources/templates/message.md @@ -0,0 +1,3 @@ +## {{message.name}} +{{message.description}} + diff --git a/reference-doc-builder/src/main/resources/templates/rpc-message.md b/reference-doc-builder/src/main/resources/templates/rpc-message.md new file mode 100644 index 0000000..7b075bb --- /dev/null +++ b/reference-doc-builder/src/main/resources/templates/rpc-message.md @@ -0,0 +1,2 @@ +### gRPC {{rpc.message.type}}: {{rpc.message.name}} +{{rpc.message.description}} diff --git a/reference-doc-builder/src/main/resources/templates/rpc-method.md b/reference-doc-builder/src/main/resources/templates/rpc-method.md new file mode 100644 index 0000000..580725c --- /dev/null +++ b/reference-doc-builder/src/main/resources/templates/rpc-method.md @@ -0,0 +1,3 @@ +## RPC Method {{method.name}} +### Unary RPC +{{method.description}}