Compare commits

..

228 Commits

Author SHA1 Message Date
Juraj Bednar
bb8d10a0b9
Update README.md (#112)
@pavlenex said on btcpayserver chat that this is not maintained, probably a good idea to mention it in the documentation, since from docs it seems that it is alpha that is going to receive some care.
2024-10-21 15:51:03 +02:00
Kukks
b19665152d bump 2021-11-05 11:08:12 +01:00
Kukks
561c676758 use custom exchangesharp until they merge PR 2021-11-05 11:08:08 +01:00
Kukks
6699e73ed6 bump 2021-10-05 14:13:58 +02:00
Kukks
c5dfbf336e Bump ExchangeSharp 2021-10-05 14:13:38 +02:00
Kukks
e74a996424 bump 2021-07-12 08:44:52 +02:00
juandelcid
96c3747ad9 Send MarketSymbol on GetOrderDetailsAsync (#95)
Binance requires MarketSymbol when you query an order.
2021-07-12 08:44:00 +02:00
Kukks
c7fe58200f small fixes and adjustments around btcpay auth 2021-03-18 09:38:14 +01:00
Pavlenex
8e3051e61b
Merge pull request #83 from MaxHillebrand/patch-1
[typo] remove double word
2021-02-03 16:43:42 +01:00
Pavlenex
d84d4bdf7f
Merge pull request #72 from britttttk/readme-dca
Add DCA to readme
2021-02-03 16:43:31 +01:00
Max Hillebrand
003c261909
[typo] remove double word 2021-02-03 16:22:42 +01:00
Dennis Reimann
4dc82b81c5
Fix headline level (#80)
Noticed this while working on the docs search.
2020-12-04 10:26:52 +01:00
britttttk
d36ead30a7
fix case sensitive command and add link to FAQ (#71) 2020-10-13 09:00:54 +02:00
Britt Kelly
4cb2a23900 Add DCA to readme 2020-09-11 18:19:16 -06:00
ɹǝƃıǝ⅁ ɯo⊥
b3faa84cf3
Prevent model invalidation after sent email (#66)
Removed the confirmation message as it invalidates the model, preventing it from being saved.
2020-08-06 09:25:30 +02:00
Kukks
b413816797 set first user to admin even if from btcpay 2020-07-17 12:37:05 +02:00
Kukks
048b59c261 set default mode to list
closes #53
2020-07-17 12:36:49 +02:00
Kukks
7de6647e65 Refactor LoginWithBtcPay 2020-07-17 11:35:32 +02:00
Kukks
f4a837f36f fix login 2020-07-16 10:38:02 +02:00
Kukks
140d2c206a Login with BTCPay feature 2020-07-15 14:40:43 +02:00
Kukks
50c1651cba update 2020-06-21 14:07:47 +02:00
Kukks
a497084b34 fix build 2020-06-21 13:47:18 +02:00
Kukks
051a27fd86 bump version 2020-06-21 13:38:23 +02:00
Kukks
124982bd94 fix plenty of warnings 2020-06-21 13:37:51 +02:00
Kukks
97f682115c Add DCA guide 2020-06-21 13:28:03 +02:00
Kukks
39bd13bb25 use custom exchanegsharp until next release 2020-06-21 12:54:31 +02:00
Kukks
2ef1e353e7 Add DCA preset 2020-06-21 09:56:55 +02:00
Kukks
8334969f93 bump packages 2020-06-21 09:15:18 +02:00
Kukks
21ecf986f1 fix algolia autocomplete search dark theme color 2020-06-21 09:13:11 +02:00
Kukks
cb6fa66300 opt out telemetry 2020-06-21 08:52:08 +02:00
Dennis Reimann
32f21e5306
Fix links (#64)
Found these while working on a broken link check for the docs.
2020-06-18 07:28:11 +02:00
Dennis Reimann
5d0b771a36
Apply design guidelines (#62)
* Add design basics

* Minor footer improvement
2020-06-16 17:26:03 +02:00
Kukks
435ebf8909 bump packages 2020-05-27 09:17:49 +02:00
Kukks
323f7e8481 remove debug code 2020-05-27 08:46:52 +02:00
Kukks
2d7911bc95 bump 2020-05-26 12:08:58 +02:00
Kukks
ce7a70bb99 fix logs styling 2020-05-26 12:08:50 +02:00
Kukks
9b4fb02241 bump dynamic linq and set JObject type on receive webhook 2020-05-26 12:05:07 +02:00
Kukks
aa68a90224 Allow a webhook receive trigger to accept any http method 2020-05-26 12:04:36 +02:00
Kukks
236f416fc6 do not show individual actions or allow creation of them anymore 2020-05-26 10:46:12 +02:00
Kukks
36fbd57209 fix api stuff 2020-05-26 10:32:19 +02:00
Kukks
28c2bac049 fix badge styling 2020-05-26 09:57:56 +02:00
Kukks
2c6ace2a95 set basic auth to correct scheme 2020-05-25 09:13:51 +02:00
Kukks
a9d5a2fadb fix webhook receive 2020-05-24 15:54:17 +02:00
Kukks
215d311113 wip 2020-05-24 15:54:17 +02:00
Andrew Camilleri
e598cc10d2
Update config.yml 2020-05-14 17:40:26 +02:00
Dennis Reimann
25383e196b
Use relative asset links in docs (#61)
This way [Vuepress can handle them correctly](https://vuepress.vuejs.org/guide/assets.html#relative-urls) and they work on GitHub too.
2020-05-08 15:27:06 +02:00
Andrew Camilleri
d5c84fa491
Update config.yml 2020-04-30 20:03:13 +02:00
Andrew Camilleri
05b64186fb
Update config.yml 2020-04-30 19:15:11 +02:00
Andrew Camilleri
a75c275c10
test docs trigger 2020-04-30 19:14:00 +02:00
britttttk
1ca2119138
Add demo guide for Transmuter email receipt preset (#52)
* Add ticket demo guide

* Add Presets to index.md doc

* move new section to readme

Co-authored-by: Kukks <evilkukka@gmail.com>
2020-04-28 09:50:38 +02:00
Andrew Camilleri
0138d9712b
Merge pull request #58 from Eskyee/patch-1
fixed typo missing dot and whitespace
2020-04-26 13:20:26 +02:00
Esky33
19db0dff46
fixed typo missing dot and whitespace 2020-04-26 11:59:07 +01:00
Andrew Camilleri
f21a4ac701
Merge pull request #56 from 2pac1/master
Update README.md
2020-04-15 17:06:53 +02:00
2pac1
0295dd1e87
Update README.md 2020-04-15 16:54:32 +02:00
Kukks
1293f03182 Make basic auth always an api 2020-04-06 08:40:16 +02:00
Kukks
d56356d2f3 make the scheme use the same one as cookie 2020-04-05 16:53:05 +02:00
Kukks
ded2313463 recipe actions api 2020-04-05 10:28:14 +02:00
Kukks
e7c48e2db7 Make add action group a post action 2020-04-05 10:18:59 +02:00
Kukks
ddfc647300 newtonsoft json for mvc 2020-04-04 14:32:10 +02:00
Kukks
5902da4212 Add ReDoc and swagger 2020-04-04 14:27:27 +02:00
Kukks
774007e5c7 fix authorization for api 2020-04-04 14:05:13 +02:00
Kukks
c47efa0b43 Recipes API 2020-04-02 15:27:06 +02:00
Kukks
c8fee0e18f make cookie default scheme 2020-04-02 14:28:55 +02:00
Kukks
c3b73c7e4d fix auth selector 2020-04-02 13:43:40 +02:00
Kukks
0e7a7a5d70 Add role claims when authenticated 2020-04-02 12:05:14 +02:00
Kukks
08a3329eff Add Basic Auth option 2020-04-02 11:58:52 +02:00
Kukks
5b29c62fc6 Cleanup Identity pages 2020-04-02 11:58:46 +02:00
Kukks
8ad099bcce Add Blob to User 2020-04-02 10:51:10 +02:00
Kukks
2fba3da43e fixes #54 2020-03-10 12:35:13 +01:00
Kukks
9649294bbf fix issue with operators plugin 2020-03-02 12:56:08 +01:00
Kukks
c555df3054 bump 2020-02-27 11:38:05 +01:00
Kukks
78ca88b030 add fingerprint and acc key path to wallet gen helper 2020-02-27 11:37:07 +01:00
Kukks
7ffaa89f55 move and update docs 2020-02-27 11:13:54 +01:00
Kukks
d80adaada3 add qr code for 2fa
fixes #43
2020-02-27 09:09:14 +01:00
Andrew Camilleri
2c7f7ef780
Merge pull request #51 from britttttk/email-preset
Fix smtp dropdown
2020-02-26 20:29:34 +01:00
Britt Kelly
e1d82f0350 Fix smtp dropdown 2020-02-19 20:26:27 -07:00
Kukks
4dd0b4450c fix more ef core issues 2020-02-17 17:18:59 +01:00
Kukks
e258d6b19f fix dockerfiles 2020-02-14 12:23:25 +01:00
Kukks
be14704843 bump 2020-02-14 12:10:07 +01:00
Kukks
4744d574e4 fix #48 2020-02-14 12:06:20 +01:00
Kukks
a085305b4b fix #49 2020-02-14 12:04:41 +01:00
Kukks
f09094087c fix bug with logs 2020-02-14 11:48:54 +01:00
Kukks
9b950cf46d Make payment forwarder use nbx wallet generator 2020-02-14 11:24:38 +01:00
Kukks
bebefb5f48 Fix Wallet generator to use proper derived xpubs 2020-02-14 11:13:46 +01:00
Kukks
079b67f421 Merge branch '3.0' 2020-01-22 20:50:49 +01:00
Kukks
4efeddf080 fix docker 2020-01-22 17:22:19 +01:00
Kukks
951ae61b25 remove direct embedded resources! 2020-01-22 17:22:19 +01:00
Kukks
58946b1427 fix view loading 2020-01-22 17:22:19 +01:00
Kukks
a2eb4d0194 bump some more packages 2020-01-22 17:22:19 +01:00
Kukks
33fde4b51f switch to dotnetcoreplugins 2020-01-22 17:22:19 +01:00
Kukks
a82ea4221b fix some ef core breaking changes 2020-01-22 17:22:19 +01:00
Kukks
19858588db package bumps 2020-01-22 17:22:18 +01:00
Kukks
de1a6a0adc switch to correct dynamic.core lib 2020-01-22 17:22:18 +01:00
Kukks
f55e390859 consolidate docker builds for .net core 3.1 2020-01-22 17:22:18 +01:00
Kukks
ba945afb3a switching to 3.1 2020-01-22 17:22:18 +01:00
Kukks
9efb53b93a Use a base class for Ids in Entity models to make sure Id is autogenerated 2020-01-22 17:22:18 +01:00
Kukks
c108c4e9b2 fix presets plugin 2020-01-22 17:22:18 +01:00
Kukks
f705b2c187 final fixes 2020-01-22 17:22:18 +01:00
Kukks
875248fc23 fixes allaround 2020-01-22 17:22:18 +01:00
Kukks
d05a1a5c3c start migrating to net core 3.0 and 2.1 netstandard 2020-01-22 17:22:18 +01:00
Andrew Camilleri
a91aa41173 Merge pull request #47 from britttttk/preset-percent
Calculate percent before parse
2020-01-22 13:44:26 +01:00
Kukks
987b2d4169 make amount formula simpler 2020-01-20 14:10:19 +01:00
Kukks
41672156c4 bump version 2019-12-18 14:55:41 +01:00
Kukks
eb61e7c08f remove messy hack for json interpolation 2019-12-18 14:54:44 +01:00
Kukks
6ee85be74a fix forwarder preset + make the interpolation system modular 2019-12-17 17:08:49 +01:00
Kukks
7589e94202 bump 2019-12-13 15:18:09 +01:00
Kukks
df946a441a fix get payments from invoice by allowing any payment type option and not breaking when multiple same cryptocode payment types are avaialble 2019-12-13 15:08:37 +01:00
Kukks
80c4ad5189 make action view clearer of params 2019-12-13 15:07:45 +01:00
Kukks
9f64f17b49 fix and simplify fiat preset 2019-12-13 15:06:36 +01:00
Kukks
205151a83d Add tests for interpolation helper 2019-12-13 15:05:58 +01:00
Kukks
12c599e5cb bump to 45 2019-12-13 11:11:00 +01:00
Kukks
ba491bfccd fixes 2019-12-13 11:10:10 +01:00
Kukks
9d9c61c9f2 revert app name in data protection change 2019-12-13 10:48:22 +01:00
Kukks
b067106fcf Add create link 2019-12-13 10:48:08 +01:00
Kukks
fc6158db3b bump in app version 2019-12-12 16:43:06 +01:00
Kukks
6bc7024244 create fiat market sell preset 2019-12-11 17:38:32 +01:00
Kukks
67f39bf73d fix build 2019-12-10 13:21:47 +01:00
Kukks
cf06bb1a7c fix cookie and data protection 2019-12-10 13:17:04 +01:00
Kukks
d653113492 better logging and start handling some errors 2019-12-10 12:55:15 +01:00
Kukks
61a8daa1b5 fix preset text 2019-12-10 11:54:50 +01:00
Kukks
834d87e8ae add btcpay email receipt preset 2019-12-10 11:26:23 +01:00
Kukks
c85c03e8e7 fix onion link 2019-12-09 19:54:48 +01:00
Kukks
5464d508e4 support sub path app 2019-12-09 16:10:03 +01:00
Andrew Camilleri
037d280c6b
Merge pull request #45 from britttttk/patch-1
Improve export command
2019-12-02 08:00:15 +01:00
britttttk
739d303956
remove extra space 2019-12-01 13:18:38 -07:00
britttttk
b725d48860
Add transmuter to existing env vars in docs 2019-12-01 13:15:41 -07:00
britttttk
c357f67b46
Improve export command
Add instructions for adding transmuter to existing environment variables.
2019-11-30 18:28:15 -07:00
Andrew Camilleri
8dcc42d867
Merge pull request #44 from pavlenex/master
Add how to deploy instructions
2019-11-30 12:04:30 +01:00
pavlenex
6d7b95e089 Add how to deploy instructions 2019-11-30 11:08:29 +01:00
Kukks
85924bd786 bump nbitcoin & remove DerivFactoryProvider 2019-11-19 18:30:21 +01:00
Kukks
4a8c73e805 attempt fix for onion url 2019-11-19 17:08:24 +01:00
Kukks
5a2586e597 bump version 2019-11-11 06:44:29 +01:00
Kukks
a65ef7e340 bump exchangesharp 2019-11-11 06:43:31 +01:00
Kukks
0b58b61b07 remove docker layer caching because CircleCI business team sucks 2019-11-04 09:05:55 +01:00
Kukks
cd207bc0dc Move email tester logic to smtp service instead of send email action. 2019-11-04 08:53:17 +01:00
Kukks
9ef7b0b2b5 add test email for smtp
closes #42
2019-11-02 09:34:05 +01:00
Kukks
b725299b1d fix stray ascii 2019-09-30 13:23:08 +02:00
Kukks
12c1ea59b3 fix nbx wallet balance viewer 2019-09-30 11:22:50 +02:00
Kukks
62d6494cb0 bump 2019-09-29 18:34:31 +02:00
Kukks
2e46509b10 use correct nertwork 2019-09-29 17:45:37 +02:00
Kukks
b9979f7bfd fixes #29 2019-09-29 17:44:31 +02:00
Kukks
48b2084f54 fixes #38 2019-09-29 15:11:05 +02:00
Kukks
52f6f5c4ba add datetime picker ui 2019-09-29 15:05:31 +02:00
Kukks
240ff51c7e fixes #28 2019-09-27 18:46:28 +02:00
Kukks
ea6cfaab25 use xpub not xpriv fixes #32 2019-09-27 18:31:12 +02:00
Kukks
40d561f199 remove confusing addresses closes #35 2019-09-27 18:28:13 +02:00
Kukks
9d9b099a31 add different viewmode for recipe list
closes #39
2019-09-27 18:26:27 +02:00
Kukks
bb83f20fb2 fix viewing logs issue with previous recipe action 2019-09-27 16:05:23 +02:00
Kukks
6e8db6282a recipe logs should not block action deletion/changes 2019-09-27 15:32:02 +02:00
Kukks
1e3b66ca6d allow actions and action groups to be collapsed 2019-09-27 12:20:21 +02:00
Kukks
f3f9a07cb0 Tor hidden service extension
closes #40
2019-09-27 12:08:13 +02:00
Kukks
b20b149b1c discourage search engines, closes #31 2019-09-27 11:29:43 +02:00
Kukks
d9f672804c make sure to set ishtml in edit view on load 2019-08-20 15:04:48 +02:00
Kukks
b2e5b14dd6 bump 2019-08-19 17:05:12 +02:00
Kukks
6638105e69 fix db col encryption for dev env 2019-08-19 17:04:53 +02:00
Kukks
36878ec7c0 make send email able to send HTML body 2019-08-19 17:04:43 +02:00
Kukks
afa340ccb8 fix condition action 2019-08-19 17:04:10 +02:00
Kukks
0c8d9bd136 remove redundant validation summary 2019-08-19 16:18:31 +02:00
Kukks
88786b5fe5 update version not package version 2019-08-19 16:11:08 +02:00
Kukks
f9f1d2d2f3 bump v 2019-08-19 15:42:55 +02:00
Kukks
090bbd464a fix condition action edit view 2019-08-19 15:11:23 +02:00
Andrew Camilleri
061fab086b move fee submission to before destinations added 2019-06-18 12:22:41 +02:00
Andrew Camilleri
d5ec6f7e0d make trigger execution source not wait for full cycle to finish properly 2019-06-18 10:48:03 +02:00
Andrew Camilleri
dff978f556 make db encryption optional until we add more settings for it 2019-06-18 10:47:37 +02:00
Kukks
fda441854f add more logs to nbx wallet display 2019-06-13 21:24:50 +02:00
Kukks
c725444347 Allow configuration of fee in send transaction action 2019-06-13 21:23:21 +02:00
Kukks
a90a44cb19 fix get balance view model issue 2019-06-11 17:56:35 +02:00
Kukks
d0442693b2 add log for balance failure 2019-06-11 17:50:21 +02:00
Kukks
cfc6e95012 bump to 26 2019-06-10 21:04:05 +02:00
Kukks
58cbbfa5f8 fixes #25 2019-06-10 21:03:59 +02:00
Kukks
7a755d356b closes #26 2019-06-10 21:03:45 +02:00
Kukks
ee63b10f96 add alpha warning 2019-06-10 21:03:07 +02:00
Kukks
4e3b832395 allow cloning of recipe through UI and fix bug with cloning recipes that are linked to external services 2019-06-10 15:00:06 +02:00
Kukks
e44094ceb1 move clone recipe to manager instead of action to reuse in UI 2019-06-10 13:11:09 +02:00
Kukks
d6cd796ae5 add more logging toplace order action 2019-06-10 13:03:23 +02:00
Kukks
d043f0bf88 redo status change trigger to handle any usecase 2019-06-10 12:54:39 +02:00
Kukks
f66e971c0a Reduce microsoft specific logs 2019-06-10 09:29:42 +02:00
Kukks
a5a34f3681 make sure there is no conflict with older version 2019-06-08 20:59:07 +02:00
Kukks
f41f6efe4c fixes #24 2019-06-08 20:53:04 +02:00
Kukks
5adc78eede fixes #23 2019-06-08 20:30:02 +02:00
Kukks
5f3edaa0eb Verify Externalservice trigger matches the recipe trigger's
fixes #22
2019-06-08 20:05:44 +02:00
Kukks
1a9f972dab do not use p2sh in preset if crypto is not available 2019-05-26 19:32:30 +02:00
Kukks
c0c6083069 add dropdowns for exchange balances fixes #19 2019-05-26 19:14:26 +02:00
Kukks
afc65f0f15 make nbx view tidier 2019-05-26 18:49:28 +02:00
Kukks
5118869219 fix build 2019-05-26 15:50:50 +02:00
Kukks
bd937ae9e3 fix nbx wallet view 2019-05-26 15:03:35 +02:00
Kukks
2f5f53531e fix js bug 2019-05-26 14:34:06 +02:00
Kukks
1921f19e66 improve exchange service UX 2019-05-26 14:14:20 +02:00
Kukks
87dd78f3ab fixes #15 2019-05-25 17:08:12 +02:00
Kukks
8bad5e1105 fix tests 2019-05-25 16:45:58 +02:00
Kukks
a17697a011 closes #15 2019-05-24 15:03:13 +02:00
Kukks
dcd79d104b closes #14 2019-05-24 14:17:24 +02:00
Kukks
3b92f4b4ed closes #16 2019-05-24 13:14:53 +02:00
Kukks
b996143abc closes #20 2019-05-24 13:14:37 +02:00
Kukks
07e0ccd284 cleanup homepage 2019-05-24 13:09:58 +02:00
Kukks
0549ecd94c fix condition edit view 2019-05-24 13:06:11 +02:00
Kukks
cc6065d7df fix anime 2019-05-19 11:06:58 +02:00
Kukks
832eec1400 lighter anime js 2019-05-19 09:26:38 +02:00
Kukks
651b2bf181 small ux fixes for forwarder preset 2019-05-19 08:14:20 +02:00
Kukks
2e626e448b remove duplicate error message 2019-05-19 07:58:08 +02:00
Kukks
2b66b0b26b include jquery scripts 2019-05-19 07:52:32 +02:00
Kukks
dca7bd02c5 use new logo 2019-05-18 17:13:47 +02:00
Kukks
9bd4864e4a fix compile error 2019-05-18 16:15:43 +02:00
Kukks
94377e964c bump deps 2019-05-18 16:15:12 +02:00
Kukks
2cb8e9c02b protect preset endpoint 2019-05-08 13:45:31 +02:00
Kukks
d8e43af4a4 bump nbitcoin and lightning lib 2019-05-08 13:45:22 +02:00
Kukks
76d52a4be2 improve homepage 2019-05-07 15:39:01 +02:00
Kukks
51f7724f3c use local svgs 2019-05-07 13:27:27 +02:00
Kukks
fe37f28713 make homepage pretty 2019-05-07 13:23:50 +02:00
Kukks
17684a2a44 remove chmod in docker 2019-05-07 09:30:50 +02:00
Kukks
2b284733a6 add in payment forwarder preset 2019-05-07 09:12:07 +02:00
Kukks
19babb809b payment forwarder backend code 2019-05-06 22:18:30 +02:00
Kukks
624dd99654 start working on presets 2019-05-06 17:29:58 +02:00
Kukks
110d287e94 Allow extensions to define global json converters 2019-05-06 15:00:47 +02:00
Kukks
caee4b70f5 fix ToJson func 2019-05-06 14:31:17 +02:00
Kukks
104c2074df work on adding funcs to interpolator 2019-05-05 19:15:43 +02:00
Kukks
53ea6b9b76 fix ln node service info no populated 2019-05-05 15:47:53 +02:00
Kukks
6da6edd27f fix case specific bug in nbx summary checker 2019-05-05 15:45:43 +02:00
Kukks
0100da72f0 bump nbitcoin and nbx 2019-05-05 15:45:21 +02:00
Kukks
fb75d110f5 more tests 2019-05-05 13:55:39 +02:00
Kukks
e02637c850 18% coverage 2019-05-03 19:44:14 +02:00
Kukks
80d753db54 more base test code 2019-05-02 13:08:11 +02:00
Kukks
3340ca6cde more test code 2019-05-02 12:45:33 +02:00
Kukks
a7432b868a cleanup redundant code 2019-05-01 13:07:12 +02:00
Kukks
caf0f6b691 test coverage 17% coverage 2019-05-01 13:03:21 +02:00
Kukks
409f3a01c0 test coverage 9% - painful days ahead 2019-04-30 16:06:27 +02:00
Kukks
797863aa79 more test fx 2019-04-30 15:00:45 +02:00
Kukks
6314829956 fix failing tests and start creating test framework 2019-04-30 14:23:06 +02:00
446 changed files with 69014 additions and 9015 deletions

View File

@ -2,50 +2,61 @@ version: 2
jobs:
build:
machine:
docker_layer_caching: true
docker_layer_caching: false
steps:
- checkout
test:
machine:
docker_layer_caching: true
steps:
- checkout
- run:
command: |
# cd BTCPayServer.Tests
# docker-compose down --v
# docker-compose build
# docker-compose run tests
# publish jobs require $DOCKERHUB_REPO, $DOCKERHUB_USER, $DOCKERHUB_PASS defined
publish_docker_linuxamd64:
machine:
docker_layer_caching: true
docker_layer_caching: false
steps:
- checkout
- run:
command: |
LATEST_TAG=${CIRCLE_TAG:1} #trim v from tag
if [ -z "$LATEST_TAG" ]
then
LATEST_TAG="latest"
fi
#
sudo docker build --pull -t $DOCKERHUB_REPO:$LATEST_TAG-amd64 -f BtcTransmuter/Dockerfile.linuxamd64 .
sudo docker build --pull -t $DOCKERHUB_REPO:$LATEST_TAG-amd64 -f Dockerfiles/amd64.Dockerfile .
sudo docker login --username=$DOCKERHUB_USER --password=$DOCKERHUB_PASS
sudo docker push $DOCKERHUB_REPO:$LATEST_TAG-amd64
publish_docker_linuxarm:
publish_docker_linuxarm32:
machine:
docker_layer_caching: true
docker_layer_caching: false
steps:
- checkout
- run:
command: |
sudo docker run --rm --privileged multiarch/qemu-user-static:register --reset
LATEST_TAG=${CIRCLE_TAG:1} #trim v from tag
if [ -z "$LATEST_TAG" ]
then
LATEST_TAG="latest"
fi
#
sudo docker build --pull -t $DOCKERHUB_REPO:$LATEST_TAG-arm32v7 -f BtcTransmuter/Dockerfile.linuxarm32v7 .
sudo docker build --pull -t $DOCKERHUB_REPO:$LATEST_TAG-arm32v7 -f Dockerfiles/arm32v7.Dockerfile .
sudo docker login --username=$DOCKERHUB_USER --password=$DOCKERHUB_PASS
sudo docker push $DOCKERHUB_REPO:$LATEST_TAG-arm32v7
publish_docker_linuxarm64:
machine:
docker_layer_caching: false
steps:
- checkout
- run:
command: |
sudo docker run --rm --privileged multiarch/qemu-user-static:register --reset
LATEST_TAG=${CIRCLE_TAG:1} #trim v from tag
if [ -z "$LATEST_TAG" ]
then
LATEST_TAG="latest"
fi
#
sudo docker build --pull -t $DOCKERHUB_REPO:$LATEST_TAG-arm64v8 -f Dockerfiles/arm64v8.Dockerfile .
sudo docker login --username=$DOCKERHUB_USER --password=$DOCKERHUB_PASS
sudo docker push $DOCKERHUB_REPO:$LATEST_TAG-arm64v8
publish_docker_multiarch:
machine:
enabled: true
@ -60,39 +71,62 @@ jobs:
sudo docker login --username=$DOCKERHUB_USER --password=$DOCKERHUB_PASS
#
LATEST_TAG=${CIRCLE_TAG:1} #trim v from tag
sudo docker manifest create --amend $DOCKERHUB_REPO:$LATEST_TAG $DOCKERHUB_REPO:$LATEST_TAG-amd64 $DOCKERHUB_REPO:$LATEST_TAG-arm32v7
if [ -z "$LATEST_TAG" ]
then
LATEST_TAG="latest"
fi
sudo docker manifest create --amend $DOCKERHUB_REPO:$LATEST_TAG $DOCKERHUB_REPO:$LATEST_TAG-amd64 $DOCKERHUB_REPO:$LATEST_TAG-arm32v7 $DOCKERHUB_REPO:$LATEST_TAG-arm64v8
sudo docker manifest annotate $DOCKERHUB_REPO:$LATEST_TAG $DOCKERHUB_REPO:$LATEST_TAG-amd64 --os linux --arch amd64
sudo docker manifest annotate $DOCKERHUB_REPO:$LATEST_TAG $DOCKERHUB_REPO:$LATEST_TAG-arm32v7 --os linux --arch arm --variant v7
sudo docker manifest annotate $DOCKERHUB_REPO:$LATEST_TAG $DOCKERHUB_REPO:$LATEST_TAG-arm64v8 --os linux --arch arm64 --variant v8
sudo docker manifest push $DOCKERHUB_REPO:$LATEST_TAG -p
trigger_docs_build:
machine:
enabled: true
image: circleci/classic:201808-01
steps:
- run:
command: |
curl -X POST -H "Authorization: token $GH_PAT" -H "Accept: application/vnd.github.everest-preview+json" -H "Content-Type: application/json" https://api.github.com/repos/btcpayserver/btcpayserver-doc/dispatches --data '{"event_type": "build_docs"}'
workflows:
version: 2
build_and_test:
jobs:
- test
publish:
jobs:
- trigger_docs_build:
filters:
branches:
only: master
# only act on version tags
tags:
only: /v[0-9]+(\.[0-9]+)*/
- publish_docker_linuxamd64:
filters:
# ignore any commit on any branch by default
branches:
ignore: /.*/
only: master
# only act on version tags
tags:
only: /v[0-9]+(\.[0-9]+)*/
- publish_docker_linuxarm:
- publish_docker_linuxarm32:
filters:
branches:
ignore: /.*/
only: master
tags:
only: /v[0-9]+(\.[0-9]+)*/
- publish_docker_linuxarm64:
filters:
branches:
only: master
tags:
only: /v[0-9]+(\.[0-9]+)*/
- publish_docker_multiarch:
requires:
- publish_docker_linuxamd64
- publish_docker_linuxarm
- publish_docker_linuxarm32
- publish_docker_linuxarm64
filters:
branches:
ignore: /.*/
only: master
tags:
only: /v[0-9]+(\.[0-9]+)*/

1
.gitignore vendored
View File

@ -303,3 +303,4 @@ BtcTransmuter\.Extension\.BtcPayServer/obj/
BtcTransmuter/Extensions/
/BtcTransmuter/Extensions/
/BtcTransmuter/mydb.db_old
dest

View File

@ -0,0 +1,10 @@
<Project Sdk="Microsoft.NET.Sdk.Razor">
<PropertyGroup>
<TargetFramework>netcoreapp3.1</TargetFramework>
<AddRazorSupportForMvc>true</AddRazorSupportForMvc>
<OutputType>Library</OutputType>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\BtcTransmuter.Abstractions\BtcTransmuter.Abstractions.csproj" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,16 @@
using Microsoft.Extensions.Configuration;
namespace BTCTransmuter.Extension.Tor.Configuration
{
public class BTCTransmuterTorOptions
{
public string TorrcFile { get; }
public string TransmuterHiddenServiceName { get; }
public BTCTransmuterTorOptions(IConfiguration configuration)
{
TorrcFile = configuration.GetValue<string>(nameof(TorrcFile), null);
TransmuterHiddenServiceName = configuration.GetValue(nameof(TransmuterHiddenServiceName), "BTCTransmuter");
}
}
}

View File

@ -0,0 +1,44 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using BTCTransmuter.Extension.Tor.Configuration;
using BTCTransmuter.Extension.Tor.Services;
using Microsoft.Extensions.Hosting;
namespace BTCTransmuter.Extension.Tor.HostedServices
{
public class TorServicesHostedService : IHostedService
{
private readonly BTCTransmuterTorOptions _btcTransmuterTorOptions;
private readonly TorServices _torServices;
public TorServicesHostedService(BTCTransmuterTorOptions btcTransmuterTorOptions, TorServices torServices)
{
_btcTransmuterTorOptions = btcTransmuterTorOptions;
_torServices = torServices;
}
public Task StartAsync(CancellationToken cancellationToken)
{
if (!string.IsNullOrEmpty(_btcTransmuterTorOptions.TorrcFile))
{
_ = ContinuouslyMonitorTorServices(cancellationToken);
}
return Task.CompletedTask;
}
private async Task ContinuouslyMonitorTorServices(CancellationToken cancellationToken)
{
while (!cancellationToken.IsCancellationRequested)
{
_torServices.Refresh();
await Task.Delay(TimeSpan.FromMinutes(2), cancellationToken);
}
}
public Task StopAsync(CancellationToken cancellationToken)
{
return Task.CompletedTask;
}
}
}

View File

@ -0,0 +1,9 @@
namespace BTCTransmuter.Extension.Tor.Models
{
public class TorService
{
public string Name { get; set; }
public string OnionHost { get; set; }
public int VirtualPort { get; set; }
}
}

View File

@ -0,0 +1,91 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using BTCTransmuter.Extension.Tor.Configuration;
using BTCTransmuter.Extension.Tor.Models;
using Microsoft.Extensions.Logging;
namespace BTCTransmuter.Extension.Tor.Services
{
public class TorServices
{
private readonly BTCTransmuterTorOptions _transmuterTorOptions;
private readonly ILogger<TorServices> _logger;
public TorServices(BTCTransmuterTorOptions transmuterTorOptions, ILogger<TorServices> logger)
{
_transmuterTorOptions = transmuterTorOptions;
_logger = logger;
}
public TorService[] Services { get; internal set; } = Array.Empty<TorService>();
public TorService TransmuterTorService
{
get
{
if (string.IsNullOrEmpty(_transmuterTorOptions.TransmuterHiddenServiceName))
{
return null;
}
return Services.SingleOrDefault(service =>
service.Name.Equals(_transmuterTorOptions.TransmuterHiddenServiceName,
StringComparison.InvariantCultureIgnoreCase));
}
}
internal void Refresh()
{
if (string.IsNullOrEmpty(_transmuterTorOptions.TorrcFile) || !File.Exists(_transmuterTorOptions.TorrcFile))
{
if (!string.IsNullOrEmpty(_transmuterTorOptions.TorrcFile))
_logger.LogWarning("Torrc file is not found");
Services = Array.Empty<TorService>();
return;
}
List<TorService> result = new List<TorService>();
try
{
var torrcContent = File.ReadAllText(_transmuterTorOptions.TorrcFile);
if (!Torrc.TryParse(torrcContent, out var torrc))
{
_logger.LogWarning("Torrc file could not be parsed");
Services = Array.Empty<TorService>();
return;
}
var services = torrc.ServiceDirectories.SelectMany(d => d.ServicePorts.Select(p => (Directory: new DirectoryInfo(d.DirectoryPath), VirtualPort: p.VirtualPort)))
.Select(d => (ServiceName: d.Directory.Name,
ReadingLines: System.IO.File.ReadAllLines(Path.Combine(d.Directory.FullName, "hostname")),
VirtualPort: d.VirtualPort))
.ToArray();
foreach (var service in services)
{
try
{
var onionHost = (service.ReadingLines)[0].Trim();
var torService = new TorService()
{
Name = service.ServiceName,
OnionHost = $"http://{onionHost}",
VirtualPort = service.VirtualPort
};
result.Add(torService);
}
catch (Exception ex)
{
_logger.LogWarning(ex, $"Error while reading hidden service {service.ServiceName} configuration");
}
}
}
catch (Exception ex)
{
_logger.LogWarning(ex, $"Error while reading torrc file");
}
Services = result.ToArray();
}
}
}

View File

@ -0,0 +1,98 @@
using System;
using System.Collections.Generic;
using System.Net;
using System.Text;
namespace BTCTransmuter.Extension.Tor.Services
{
public class Torrc
{
public static bool TryParse(string str, out Torrc value)
{
value = null;
List<HiddenServiceDir> serviceDirectories = new List<HiddenServiceDir>();
var lines = str.Split(new char[] { '\n' });
HiddenServiceDir currentDirectory = null;
foreach (var line in lines)
{
if (HiddenServiceDir.TryParse(line, out var dir))
{
serviceDirectories.Add(dir);
currentDirectory = dir;
}
else if (HiddenServicePortDefinition.TryParse(line, out var portDef) && currentDirectory != null)
{
currentDirectory.ServicePorts.Add(portDef);
}
}
value = new Torrc() { ServiceDirectories = serviceDirectories };
return true;
}
public List<HiddenServiceDir> ServiceDirectories { get; set; } = new List<HiddenServiceDir>();
public override string ToString()
{
StringBuilder builder = new StringBuilder();
foreach(var serviceDir in ServiceDirectories)
{
builder.AppendLine(serviceDir.ToString());
foreach (var port in serviceDir.ServicePorts)
builder.AppendLine(port.ToString());
}
return builder.ToString();
}
}
public class HiddenServiceDir
{
public static bool TryParse(string str, out HiddenServiceDir serviceDir)
{
serviceDir = null;
if (!str.Trim().StartsWith("HiddenServiceDir ", StringComparison.OrdinalIgnoreCase))
return false;
var parts = str.Split(new char[] { ' ', '\t' }, StringSplitOptions.None);
if (parts.Length != 2)
return false;
serviceDir = new HiddenServiceDir() { DirectoryPath = parts[1].Trim() };
return true;
}
public string DirectoryPath { get; set; }
public List<HiddenServicePortDefinition> ServicePorts { get; set; } = new List<HiddenServicePortDefinition>();
public override string ToString()
{
return $"HiddenServiceDir {DirectoryPath}";
}
}
public class HiddenServicePortDefinition
{
public static bool TryParse(string str, out HiddenServicePortDefinition portDefinition)
{
portDefinition = null;
if (!str.Trim().StartsWith("HiddenServicePort ", StringComparison.OrdinalIgnoreCase))
return false;
var parts = str.Split(new char[] { ' ', '\t' }, StringSplitOptions.RemoveEmptyEntries);
if (parts.Length != 3)
return false;
if (!int.TryParse(parts[1].Trim(), out int virtualPort))
return false;
var addressPort = parts[2].Trim().Split(new []{':'}, StringSplitOptions.RemoveEmptyEntries);
if (addressPort.Length != 2)
return false;
if (!int.TryParse(addressPort[1].Trim(), out int port))
return false;
if (!IPAddress.TryParse(addressPort[0].Trim(), out IPAddress address))
return false;
portDefinition = new HiddenServicePortDefinition() { VirtualPort = virtualPort, Endpoint = new IPEndPoint(address, port) };
return true;
}
public int VirtualPort { get; set; }
public IPEndPoint Endpoint { get; set; }
public override string ToString()
{
return $"HiddenServicePort {VirtualPort} {Endpoint}";
}
}
}

View File

@ -0,0 +1,22 @@
using System;
using BtcTransmuter.Abstractions.Extensions;
using BTCTransmuter.Extension.Tor.Configuration;
using BTCTransmuter.Extension.Tor.Services;
using Microsoft.Extensions.DependencyInjection;
namespace BTCTransmuter.Extension.Tor
{
public class TorBtcTransmuterExtension : BtcTransmuterExtension
{
public override string Name => "Tor Plugin";
public override string Description => "Allows you to access Transmuter over Tor";
public override string MenuPartial => "TorOnionLinkPartial";
public override void Execute(IServiceCollection serviceCollection)
{
serviceCollection.AddSingleton<BTCTransmuterTorOptions>();
serviceCollection.AddSingleton<TorServices>();
base.Execute(serviceCollection);
}
}
}

View File

@ -0,0 +1,7 @@
@inject BTCTransmuter.Extension.Tor.Services.TorServices TorServices
@if (TorServices.TransmuterTorService != null)
{
<li class="nav-item">
<a href="@TorServices.TransmuterTorService.OnionHost" class="nav-link">Onion</a>
</li>
}

View File

@ -81,6 +81,8 @@ namespace BtcTransmuter.Abstractions.Actions
{
return BadRequest("what you tryin to pull bro");
}
modelResult.ToSave.ExternalService = externalService;
}
await _recipeManager.AddOrUpdateRecipeAction(modelResult.ToSave);

View File

@ -1,15 +1,18 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<TargetFramework>netcoreapp3.1</TargetFramework>
<AddRazorSupportForMvc>true</AddRazorSupportForMvc>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="ExtCore.Mvc.Infrastructure" Version="4.0.0" />
<PackageReference Include="Microsoft.Extensions.Identity.Stores" Version="2.2.0" />
<PackageReference Include="NetCore.AutoRegisterDi" Version="1.0.0" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.1" />
<PackageReference Include="wilx.System.Linq.Dynamic.Core" Version="2.1.0" />
<PackageReference Include="McMaster.NETCore.Plugins" Version="1.3.0" />
<PackageReference Include="McMaster.NETCore.Plugins.Mvc" Version="1.3.0" />
<PackageReference Include="Microsoft.Extensions.Identity.Stores" Version="3.1.4" />
<PackageReference Include="NetCore.AutoRegisterDi" Version="2.0.0" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
<PackageReference Include="System.Linq.Dynamic.Core" Version="1.1.1" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\BtcTransmuter.Data\BtcTransmuter.Data.csproj" />

View File

@ -3,7 +3,6 @@ namespace BtcTransmuter
public enum DatabaseType
{
Sqlite,
Postgres,
SqlServer
Postgres
}
}

View File

@ -0,0 +1,15 @@
using System;
namespace BtcTransmuter
{
public interface IBtcTransmuterOptions
{
string ExtensionsDir { get; set; }
string DatabaseConnectionString { get; set; }
string DataProtectionDir { get; set; }
DatabaseType DatabaseType { get; set; }
bool UseDatabaseColumnEncryption { get; set; }
bool DisableInternalAuth { get; set; }
Uri BTCPayAuthServer { get; set; }
}
}

View File

@ -4,17 +4,17 @@ using System.Linq;
using System.Reflection;
using BtcTransmuter.Abstractions.Actions;
using BtcTransmuter.Abstractions.ExternalServices;
using BtcTransmuter.Abstractions.Helpers;
using BtcTransmuter.Abstractions.Triggers;
using ExtCore.Infrastructure;
using ExtCore.Infrastructure.Actions;
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using NetCore.AutoRegisterDi;
using Newtonsoft.Json;
namespace BtcTransmuter.Abstractions.Extensions
{
public abstract class BtcTransmuterExtension: ExtensionBase, IExtension, IConfigureAction, IConfigureServicesAction
public abstract class BtcTransmuterExtension: IExtension
{
public abstract string Name { get; }
@ -25,6 +25,7 @@ namespace BtcTransmuter.Abstractions.Extensions
public virtual string Authors { get; } = string.Empty;
public virtual string HeaderPartial { get; }
public virtual string MenuPartial { get; }
public virtual IEnumerable<JsonConverter> JsonConverters { get; set; }
public IEnumerable<IActionDescriptor> Actions => GetInstancesOfTypeInOurAssembly<IActionDescriptor>();
public IEnumerable<ITriggerDescriptor> Triggers => GetInstancesOfTypeInOurAssembly<ITriggerDescriptor>();
@ -42,10 +43,6 @@ namespace BtcTransmuter.Abstractions.Extensions
Execute(serviceCollection);
}
int IConfigureServicesAction.Priority => 0;
int IConfigureAction.Priority=> 0;
public virtual void Execute(IServiceCollection serviceCollection)
{
RegisterInstances(serviceCollection, new Type[]
@ -55,6 +52,7 @@ namespace BtcTransmuter.Abstractions.Extensions
typeof(ITriggerDescriptor),
typeof(ITriggerHandler),
typeof(IExternalServiceDescriptor),
typeof(TransmuterInterpolationTypeProvider),
});
RegisterInstances(serviceCollection, new Type[]
{

View File

@ -0,0 +1,57 @@
using System.Collections.Generic;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
namespace BtcTransmuter.Abstractions.Extensions
{
public static class ControllerExtensions
{
public static bool IsApi(this Controller controller)
{
return controller.HttpContext.IsApi();
}
public static bool IsApi(this HttpContext context)
{
if (context.Items.TryGetValue("API", out var val))
{
return val is true;
}
return false;
}
public static void SetIsApi(this HttpContext context, bool val)
{
context.Items.TryAdd("API", val);
}
public static IActionResult ViewOrJson<T>(this Controller controller, string viewName, T payload)
{
if (controller.IsApi())
{
return controller.Json(payload);
}
return controller.View(viewName, payload);
}
public static IActionResult ViewOrJson<T>(this Controller controller, T payload)
{
if (controller.IsApi())
{
return controller.Json(payload);
}
return controller.View(payload);
}
public static IActionResult ViewOrBadRequest<T>(this Controller controller, T payload, bool usePayloadInBadRequest = false)
{
if (controller.IsApi())
{
return usePayloadInBadRequest ? controller.BadRequest(payload) : controller.BadRequest(controller.ModelState);
}
return controller.View(payload);
}
}
}

View File

@ -0,0 +1,14 @@
using System.Collections.Generic;
namespace BtcTransmuter.Abstractions.Extensions
{
public static class EnumerableExtensions
{
public static HashSet<T> ToHashSet<T>(
this IEnumerable<T> source,
IEqualityComparer<T> comparer = null)
{
return new HashSet<T>(source, comparer);
}
}
}

View File

@ -1,29 +1,15 @@
using System;
using System.Linq.Expressions;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Mvc.ViewFeatures.Internal;
namespace BtcTransmuter.Abstractions.Extensions
{
public static class ModelStateExtensions
{
public static void AddModelError<TModel, TProperty>(
this ModelStateDictionary modelState,
Expression<Func<TModel, TProperty>> ex,
string message
)
{
var key = ExpressionHelper.GetExpressionText(ex);
modelState.AddModelError(key, message);
}
public static void AddModelError<TModel, TProperty>(this TModel source,
Expression<Func<TModel, TProperty>> ex,
public static void AddModelError<TModel>(this TModel source,
string name,
string message,
ModelStateDictionary modelState)
{
var key = ExpressionHelper.GetExpressionText(ex);
modelState.AddModelError(key, message);
modelState.AddModelError(name, message);
}
}
}

View File

@ -0,0 +1,16 @@
using Microsoft.AspNetCore.Http;
namespace BtcTransmuter.Abstractions.Extensions
{
public static class RequestExtensions
{
public static string GetAbsoluteRoot(this HttpRequest request)
{
return string.Concat(
request.Scheme,
"://",
request.Host.ToUriComponent(),
request.PathBase.ToUriComponent());
}
}
}

View File

@ -19,15 +19,15 @@ namespace BtcTransmuter.Abstractions.ExternalServices
public abstract string Name { get; }
public abstract string Description { get; }
public abstract string ViewPartial { get; }
protected abstract string ControllerName { get; }
public abstract string ControllerName { get; }
public T GetData()
public virtual T GetData()
{
return _data.Get<T>();
}
public void SetData(T data)
public virtual void SetData(T data)
{
_data.Set(data);
}

View File

@ -35,7 +35,7 @@ namespace BtcTransmuter.Abstractions.ExternalServices
{
return result.Error;
}
return View(await BuildViewModel(result.Data));
}
@ -117,7 +117,7 @@ namespace BtcTransmuter.Abstractions.ExternalServices
protected async Task<bool> IsAdmin()
{
var user = await _userManager.GetUserAsync(User);
return await _userManager.IsInRoleAsync(user, "Admin");
return user!= null && await _userManager.IsInRoleAsync(user, "Admin");
}
}
}

View File

@ -1,13 +1,46 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Dynamic.Core;
using System.Linq.Dynamic.Core.CustomTypeProviders;
using System.Linq.Expressions;
using System.Text.RegularExpressions;
using BtcTransmuter.Abstractions.Extensions;
using Newtonsoft.Json;
namespace BtcTransmuter.Abstractions.Helpers
{
public class InterpolationHelper
public class TransmuterInterpolationTypeProvider : DefaultDynamicLinqCustomTypeProvider
{
private readonly Type[] _types;
public TransmuterInterpolationTypeProvider(params Type[] types)
{
_types = types;
}
public override HashSet<Type> GetCustomTypes()
{
return _types.ToHashSet();
}
}
public sealed class InterpolationTypeProvider : DefaultDynamicLinqCustomTypeProvider {
private readonly IEnumerable<DefaultDynamicLinqCustomTypeProvider> _typeProviders;
public InterpolationTypeProvider(IEnumerable<TransmuterInterpolationTypeProvider> typeProviders)
{
_typeProviders = typeProviders;
ParsingConfig.Default.CustomTypeProvider = this;
}
public override HashSet<Type> GetCustomTypes()
{
return _typeProviders.SelectMany(provider => provider.GetCustomTypes()).ToHashSet();
}
}
public static class InterpolationHelper
{
/// <summary>
/// https://dotnetfiddle.net/MoqJFk
/// </summary>
@ -15,15 +48,25 @@ namespace BtcTransmuter.Abstractions.Helpers
{
try
{
var parameterExpressions =
data.Select(pair => Expression.Parameter(pair.Value.GetType(), pair.Key)).ToList();
return Regex.Replace(value, @"{{(.+?)}}",
match =>
{
var parameterExpressions =
data.Select(pair => Expression.Parameter(pair.Value.GetType(), pair.Key)).ToArray();
var e = System.Linq.Dynamic.DynamicExpression.ParseLambda(parameterExpressions, null,
match.Groups[1].Value);
return (e.Compile().DynamicInvoke(data.Values.ToArray()) ?? "").ToString();
var processed = match.Groups[1].Value;
try
{
var e = DynamicExpressionParser.ParseLambda(parameterExpressions.ToArray(), null,
processed);
return (e.Compile().DynamicInvoke(data.Values.ToArray()) ?? "").ToString();
}
catch (Exception)
{
return processed;
}
});
}
catch (Exception)

View File

@ -1,4 +1,4 @@
using System.Collections;
using System.Collections;
using System.Collections.Generic;
using System.Threading.Tasks;
using BtcTransmuter.Data;
@ -24,6 +24,7 @@ namespace BtcTransmuter.Abstractions.Recipes
Task RemoveRecipeTrigger(string recipeTriggerId);
Task RemoveRecipeActionGroup(string recipeActionGroupId);
Task<string> GetRecipeName(string recipeId);
Task<Recipe> CloneRecipe(string recipeId, bool enable, string newName = null);
}
}

View File

@ -0,0 +1,10 @@
using System.Threading.Tasks;
namespace BtcTransmuter.Abstractions.Settings
{
public interface ISettingsManager
{
Task<T> GetSettings<T>(string key);
Task SaveSettings<T>(string key,T settings);
}
}

View File

@ -20,7 +20,7 @@ namespace BtcTransmuter.Abstractions.Triggers
public abstract string Name { get; }
public abstract string Description { get; }
public abstract string ViewPartial { get; }
protected abstract string ControllerName { get; }
public abstract string ControllerName { get; }
public Task<IActionResult> EditData(RecipeTrigger data)
{
@ -55,7 +55,7 @@ namespace BtcTransmuter.Abstractions.Triggers
var triggerData = await GetTriggerData(trigger);
if (typeof(TTriggerParameters).IsAssignableFrom(typeof(IUseExternalService)) &&
if (typeof(IUseExternalService).IsAssignableFrom(typeof(TTriggerData)) &&
((IUseExternalService) triggerData).ExternalServiceId != recipeTrigger.ExternalServiceId)
{
return false;

View File

@ -0,0 +1,23 @@
using System.Collections;
using System.ComponentModel.DataAnnotations;
namespace BtcTransmuter.Abstractions
{
public class EnsureMinimumElementsAttribute : ValidationAttribute
{
private readonly int _minElements;
public EnsureMinimumElementsAttribute(int minElements)
{
_minElements = minElements;
}
public override bool IsValid(object value)
{
if (value is IList list)
{
return list.Count >= _minElements;
}
return false;
}
}
}

View File

@ -1,21 +1,18 @@
<Project Sdk="Microsoft.NET.Sdk.Razor">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<TargetFramework>netcoreapp3.1</TargetFramework>
<AddRazorSupportForMvc>true</AddRazorSupportForMvc>
<LangVersion>latest</LangVersion>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Mvc" Version="2.2.0" />
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="2.2.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.2.2" />
<!--<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Design" Version="1.1.6" />-->
<PackageReference Include="Newtonsoft.Json" Version="12.0.1" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="2.2.2" />
</ItemGroup>
<ItemGroup>
<Folder Include="Migrations" />
<PackageReference Include="Microsoft.AspNetCore.DataProtection.Abstractions" Version="3.1.4" />
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="3.1.4" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="3.1.4" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="3.1.4" />
</ItemGroup>
</Project>

View File

@ -1,11 +1,11 @@
using System.ComponentModel.DataAnnotations.Schema;
using BtcTransmuter.Data.Encryption;
using BtcTransmuter.Data.Models;
namespace BtcTransmuter.Data.Entities
{
public abstract class BaseEntity : IHasJsonData
public abstract class BaseEntity :BaseIdEntity, IHasJsonData
{
public string Id { get; set; }
[Encrypted] public string DataJson { get; set; }
}
}

View File

@ -0,0 +1,10 @@
using System.ComponentModel.DataAnnotations.Schema;
namespace BtcTransmuter.Data.Entities
{
public abstract class BaseIdEntity
{
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public string Id { get; set; }
}
}

View File

@ -2,9 +2,8 @@ using System.Collections.Generic;
namespace BtcTransmuter.Data.Entities
{
public class Recipe
public class Recipe : BaseIdEntity
{
public string Id { get; set; }
public string UserId { get; set; }
public string Name { get; set; }
public bool Enabled { get; set; }
@ -13,7 +12,13 @@ namespace BtcTransmuter.Data.Entities
public RecipeTrigger RecipeTrigger { get; set; }
public List<RecipeAction> RecipeActions { get; set; } // executes all actions
public List<RecipeActionGroup> RecipeActionGroups { get; set; } // executes actions in groups. Action in a group are executed syncrhonously and depending if the previous action executes
public List<RecipeActionGroup>
RecipeActionGroups
{
get;
set;
} // executes actions in groups. Action in a group are executed syncrhonously and depending if the previous action executes
public List<RecipeInvocation> RecipeInvocations { get; set; } // log
}
}

View File

@ -13,8 +13,11 @@ namespace BtcTransmuter.Data.Entities
public Recipe Recipe { get; set; }
public ExternalServiceData ExternalService { get; set; }
public RecipeActionGroup RecipeActionGroup { get; set; }
public List<RecipeInvocation> RecipeInvocations { get; set; }
public int Order { get; set; } = 0;
public override string ToString()
{
return $"{ActionId} {(ExternalService == null ? string.Empty : $"using service {ExternalService.Name}")}";
}
}
}

View File

@ -1,10 +1,10 @@
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations.Schema;
namespace BtcTransmuter.Data.Entities
{
public class RecipeActionGroup
public class RecipeActionGroup : BaseIdEntity
{
public string Id { get; set; }
public string RecipeId { get; set; }
public Recipe Recipe { get; set; }

View File

@ -1,17 +1,17 @@
using System;
using System.ComponentModel.DataAnnotations;
namespace BtcTransmuter.Data.Entities
{
public class RecipeInvocation
public class RecipeInvocation: BaseIdEntity
{
public string Id { get; set; }
public string RecipeId { get; set; }
public string RecipeActionId { get; set; }
public string RecipeAction { get; set; }
public string TriggerDataJson { get; set; }
public string ActionResult { get; set; }
public DateTime Timestamp { get; set; }
public Recipe Recipe { get; set; }
public RecipeAction RecipeAction { get; set; }
}
}

View File

@ -0,0 +1,7 @@
namespace BtcTransmuter.Data.Entities
{
public class Settings: BaseEntity
{
public string Key { get; set; }
}
}

View File

@ -1,10 +1,26 @@
using System.Collections.Generic;
using BtcTransmuter.Data.Encryption;
using BtcTransmuter.Data.Models;
using Microsoft.AspNetCore.Identity;
namespace BtcTransmuter.Data.Entities
{
public class User : IdentityUser
public class User : IdentityUser, IHasJsonData
{
public List<Recipe> Recipes { get; set; }
[Encrypted] public string DataJson { get; set; }
}
public class UserBlob
{
public bool BasicAuth { get; set; }
public BTCPayAuthDetails BTCPayAuthDetails { get; set; } = new BTCPayAuthDetails();
}
public class BTCPayAuthDetails
{
public string UserId { get; set; }
public string AccessToken { get; set; }
}
}

View File

@ -0,0 +1,166 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Threading;
using System.Threading.Tasks;
using BtcTransmuter.Abstractions.ExternalServices;
using BtcTransmuter.Abstractions.Triggers;
using BtcTransmuter.Data.Entities;
using BtcTransmuter.Data.Models;
using BtcTransmuter.Extension.BtcPayServer.ExternalServices.BtcPayServer;
using BtcTransmuter.Extension.BtcPayServer.HostedServices;
using BtcTransmuter.Extension.BtcPayServer.Triggers.InvoiceStatusChanged;
using BtcTransmuter.Tests.Base;
using Moq;
using NBitcoin;
using NBitpayClient;
using Newtonsoft.Json;
using Xunit;
using Assert = Xunit.Assert;
namespace BtcTransmuter.Extension.BtcPayServer.Tests
{
public class BtcPayInvoiceWatcherHostedServiceTests : BaseTests
{
[Fact]
public async Task CanDetectInvoiceChanges()
{
var externalServiceManager = new Mock<IExternalServiceManager>();
var triggerDispatcher = new Mock<ITriggerDispatcher>();
var externalServices = new List<ExternalServiceData>()
{
new ExternalServiceData()
{
Type = BtcPayServerService.BtcPayServerServiceType,
Id = "A",
Name = "A",
DataJson = JsonConvert.SerializeObject(new BtcPayServerExternalServiceData())
}
};
var invoices = new List<BtcPayInvoice>()
{
new BtcPayInvoice()
{
Id = "A-A",
Status = Invoice.STATUS_NEW,
CurrentTime = DateTimeOffset.Now,
InvoiceTime = DateTimeOffset.Now,
ExpirationTime = DateTimeOffset.Now,
}
};
ITrigger dispatchedTrigger = null;
triggerDispatcher.Setup(dispatcher => dispatcher.DispatchTrigger(It.IsAny<ITrigger>()))
.Callback<ITrigger>((x) => { dispatchedTrigger = x; }).Returns(Task.CompletedTask);
var mockBitPay = new TestBitpay();
mockBitPay.Invoices = () => invoices.ToArray();
var btcpayserviceMock = new Mock<BtcPayServerService>();
btcpayserviceMock.Setup(serverService => serverService.ConstructClient())
.Returns(() => mockBitPay);
btcpayserviceMock.Setup(serverService => serverService.CheckAccess()).ReturnsAsync(() => true);
btcpayserviceMock.Setup(serverService => serverService.GetData()).Returns(() => externalServices[0].Get<BtcPayServerExternalServiceData>());
externalServiceManager
.Setup(manager => manager.GetExternalServicesData(It.IsAny<ExternalServicesDataQuery>()))
.ReturnsAsync(() => externalServices);
var flag = 0;
externalServiceManager
.Setup(manager => manager.UpdateInternalData(It.IsAny<string>(), It.IsAny<object>()))
.Callback<string, object>((key, data) =>
{
externalServices[0].Set(data);
flag++;
externalServiceManager.Raise(manager => manager.ExternalServiceDataUpdated+=null, new UpdatedItem<ExternalServiceData>()
{
Item = externalServices[0],
Action = UpdatedItem<ExternalServiceData>.UpdateAction.Updated
});
})
.Returns(Task.CompletedTask);
var service =
new TestBtcPayInvoiceWatcherHostedService(externalServiceManager.Object, triggerDispatcher.Object);
service.ConvertData = data => btcpayserviceMock.Object;
await service.StartAsync(CancellationToken.None);
while (flag == 0)
{
await Task.Delay(200);
}
externalServiceManager.Verify(
manager => manager.GetExternalServicesData(It.IsAny<ExternalServicesDataQuery>()), Times.Once);
triggerDispatcher.Verify(dispatcher => dispatcher.DispatchTrigger(It.IsAny<ITrigger>()), Times.Once);
var invoice = Assert.IsType<InvoiceStatusChangedTrigger>(dispatchedTrigger).Data.Invoice;
Assert.Equal(invoices[0].Id, invoice.Id);
Assert.Equal(invoices[0].Status, invoice.Status);
invoices[0].Status = Invoice.STATUS_PAID;
while (flag == 1)
{
await Task.Delay(200);
}
triggerDispatcher.Verify(dispatcher => dispatcher.DispatchTrigger(It.IsAny<ITrigger>()), Times.Exactly(2));
invoice = Assert.IsType<InvoiceStatusChangedTrigger>(dispatchedTrigger).Data.Invoice;
Assert.Equal(invoices[0].Id, invoice.Id);
Assert.Equal(invoices[0].Status, invoice.Status);
}
public class TestBtcPayInvoiceWatcherHostedService : BtcPayInvoiceWatcherHostedService
{
public TestBtcPayInvoiceWatcherHostedService(IExternalServiceManager externalServiceManager,
ITriggerDispatcher triggerDispatcher) : base(externalServiceManager, triggerDispatcher)
{
}
public Func<ExternalServiceData, BtcPayServerService> ConvertData = null;
protected override BtcPayServerService GetServiceFromData(ExternalServiceData externalServiceData)
{
return ConvertData == null
? base.GetServiceFromData(externalServiceData)
: ConvertData(externalServiceData);
}
protected override TimeSpan CheckInterval { get; } = TimeSpan.FromSeconds(2);
}
public class TestBitpay : Bitpay
{
public TestBitpay(Key ecKey, Uri envUrl) : base(ecKey, envUrl)
{
}
public TestBitpay(): base(new Key(), new Uri("https://gozo.com"))
{
}
public Func<Invoice[]> Invoices;
public override Task<T[]> GetInvoicesAsync<T>(DateTime? dateStart = null, DateTime? dateEnd = null)
{
if (Invoices != null)
{
return Task.FromResult(Invoices.Invoke().Select(invoice => (T) invoice).ToArray());
}
return base.GetInvoicesAsync<T>(dateStart, dateEnd);
}
}
}
}

View File

@ -0,0 +1,23 @@
using System;
using System.Linq;
using BtcTransmuter.Data.Entities;
using BtcTransmuter.Extension.BtcPayServer.ExternalServices.BtcPayServer;
using BtcTransmuter.Tests.Base;
using Xunit;
namespace BtcTransmuter.Extension.BtcPayServer.Tests
{
public class
BtcPayServerServiceTests : BaseExternalServiceTest<BtcPayServerService, BtcPayServerExternalServiceData>
{
protected override BtcPayServerService GetExternalService(params object[] setupArgs)
{
if (setupArgs?.Any() ?? false)
{
return new BtcPayServerService((ExternalServiceData) setupArgs.First());
}
return new BtcPayServerService();
}
}
}

View File

@ -0,0 +1,24 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netcoreapp3.1</TargetFramework>
<AddRazorSupportForMvc>true</AddRazorSupportForMvc>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="3.1.4" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.6.1" />
<PackageReference Include="Moq" Version="4.14.1" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.1" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\BtcTransmuter.Extension.BtcPayServer\BtcTransmuter.Extension.BtcPayServer.csproj" />
<ProjectReference Include="..\BtcTransmuter.Tests.Base\BtcTransmuter.Tests.Base.csproj" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,15 @@
using System.Threading.Tasks;
using BtcTransmuter.Extension.BtcPayServer.Actions.GetInvoice;
using BtcTransmuter.Extension.BtcPayServer.HostedServices;
using BtcTransmuter.Tests.Base;
using Xunit;
namespace BtcTransmuter.Extension.BtcPayServer.Tests
{
public class
GetInvoiceDataActionHandlerTests : BaseActionTest<GetInvoiceDataActionHandler, GetInvoiceData, BtcPayInvoice>
{
}
}

View File

@ -0,0 +1,12 @@
using System.Collections.Generic;
using BtcTransmuter.Extension.BtcPayServer.Actions.GetPaymentsFromInvoice;
using BtcTransmuter.Tests.Base;
using NBitpayClient;
namespace BtcTransmuter.Extension.BtcPayServer.Tests
{
public class
GetPaymentsFromInvoiceDataActionHandlerTests : BaseActionTest<GetPaymentsFromInvoiceDataActionHandler, GetPaymentsFromInvoiceData, List<InvoicePaymentInfo>>
{
}
}

View File

@ -0,0 +1,306 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using BtcTransmuter.Data.Entities;
using BtcTransmuter.Extension.BtcPayServer.HostedServices;
using BtcTransmuter.Extension.BtcPayServer.Triggers.InvoiceStatusChanged;
using BtcTransmuter.Tests.Base;
using NBitpayClient;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Xunit;
using Assert = Xunit.Assert;
namespace BtcTransmuter.Extension.BtcPayServer.Tests
{
public class
InvoiceStatusChangedTriggerHandlerTests : BaseTriggerTest<InvoiceStatusChangedTriggerHandler,
InvoiceStatusChangedTriggerData,
InvoiceStatusChangedTriggerParameters>
{
[Fact]
public async Task IsTriggered_ExecutesCorrectly()
{
var handler = GetTriggerHandlerInstance();
//any status combination
var parameters = new InvoiceStatusChangedTriggerParameters()
{
Conditions = InvoiceStatusChangedController.AllowedStatuses.Select(item => new InvoiceStatusChangeCondition()
{
Status = item.Value,
ExceptionStatuses = InvoiceStatusChangedController.AllowedExceptionStatus.Select(item2 => item2.Value).ToList()
}).ToList()
};
Assert.True(await handler.IsTriggered(new InvoiceStatusChangedTrigger()
{
Data = new InvoiceStatusChangedTriggerData()
{
ExternalServiceId = "A",
Invoice = new BtcPayInvoice()
{
Status = BtcPayInvoice.STATUS_NEW,
CurrentTime = DateTimeOffset.Now,
InvoiceTime = DateTimeOffset.Now,
ExpirationTime = DateTimeOffset.Now
}
}
},
new RecipeTrigger()
{
TriggerId = handler.TriggerId,
ExternalServiceId = "A",
DataJson = JsonConvert.SerializeObject(parameters)
}));
//the external service id is not the same, if it triggers you are in deep shit
Assert.False(await handler.IsTriggered(new InvoiceStatusChangedTrigger()
{
Data = new InvoiceStatusChangedTriggerData()
{
ExternalServiceId = "B",
Invoice = new BtcPayInvoice()
{
Status = BtcPayInvoice.STATUS_NEW,
CurrentTime = DateTimeOffset.Now,
InvoiceTime = DateTimeOffset.Now,
ExpirationTime = DateTimeOffset.Now
}
}
},
new RecipeTrigger()
{
TriggerId = handler.TriggerId,
ExternalServiceId = "A",
DataJson = JsonConvert.SerializeObject(parameters)
}));
Assert.True(await handler.IsTriggered(new InvoiceStatusChangedTrigger()
{
Data = new InvoiceStatusChangedTriggerData()
{
ExternalServiceId = "A",
Invoice = new BtcPayInvoice()
{
Status = BtcPayInvoice.STATUS_PAID,
CurrentTime = DateTimeOffset.Now,
InvoiceTime = DateTimeOffset.Now,
ExpirationTime = DateTimeOffset.Now
}
}
},
new RecipeTrigger()
{
TriggerId = handler.TriggerId,
ExternalServiceId = "A",
DataJson = JsonConvert.SerializeObject(parameters)
}));
Assert.True(await handler.IsTriggered(new InvoiceStatusChangedTrigger()
{
Data = new InvoiceStatusChangedTriggerData()
{
ExternalServiceId = "A",
Invoice = new BtcPayInvoice()
{
Status = BtcPayInvoice.STATUS_INVALID,
ExceptionStatus = new JValue(Invoice.EXSTATUS_PAID_PARTIAL),
CurrentTime = DateTimeOffset.Now,
InvoiceTime = DateTimeOffset.Now,
ExpirationTime = DateTimeOffset.Now
}
}
},
new RecipeTrigger()
{
TriggerId = handler.TriggerId,
ExternalServiceId = "A",
DataJson = JsonConvert.SerializeObject(parameters)
}));
//only paid status with no exception statuses
parameters.Conditions = new List<InvoiceStatusChangeCondition>()
{
new InvoiceStatusChangeCondition()
{
Status = Invoice.STATUS_PAID,
ExceptionStatuses = new List<string>()
{
Invoice.EXSTATUS_FALSE
}
}
};
Assert.True(await handler.IsTriggered(new InvoiceStatusChangedTrigger()
{
Data = new InvoiceStatusChangedTriggerData()
{
ExternalServiceId = "A",
Invoice = new BtcPayInvoice()
{
Status = BtcPayInvoice.STATUS_PAID,
CurrentTime = DateTimeOffset.Now,
InvoiceTime = DateTimeOffset.Now,
ExpirationTime = DateTimeOffset.Now
}
}
},
new RecipeTrigger()
{
TriggerId = handler.TriggerId,
ExternalServiceId = "A",
DataJson = JsonConvert.SerializeObject(parameters)
}));
Assert.False(await handler.IsTriggered(new InvoiceStatusChangedTrigger()
{
Data = new InvoiceStatusChangedTriggerData()
{
ExternalServiceId = "A",
Invoice = new BtcPayInvoice()
{
Status = BtcPayInvoice.STATUS_PAID,
ExceptionStatus = Invoice.EXSTATUS_PAID_PARTIAL,
CurrentTime = DateTimeOffset.Now,
InvoiceTime = DateTimeOffset.Now,
ExpirationTime = DateTimeOffset.Now
}
}
},
new RecipeTrigger()
{
TriggerId = handler.TriggerId,
ExternalServiceId = "A",
DataJson = JsonConvert.SerializeObject(parameters)
}));
Assert.False(await handler.IsTriggered(new InvoiceStatusChangedTrigger()
{
Data = new InvoiceStatusChangedTriggerData()
{
ExternalServiceId = "A",
Invoice = new BtcPayInvoice()
{
Status = BtcPayInvoice.STATUS_NEW,
CurrentTime = DateTimeOffset.Now,
InvoiceTime = DateTimeOffset.Now,
ExpirationTime = DateTimeOffset.Now
}
}
},
new RecipeTrigger()
{
TriggerId = handler.TriggerId,
ExternalServiceId = "A",
DataJson = JsonConvert.SerializeObject(parameters)
}));
parameters.Conditions = new List<InvoiceStatusChangeCondition>()
{
new InvoiceStatusChangeCondition()
{
Status = Invoice.STATUS_PAID,
ExceptionStatuses = new List<string>()
{
Invoice.EXSTATUS_FALSE
}
},
new InvoiceStatusChangeCondition()
{
Status = Invoice.STATUS_INVALID,
ExceptionStatuses = new List<string>()
{
"paidLate"
}
}
};
Assert.False(await handler.IsTriggered(new InvoiceStatusChangedTrigger()
{
Data = new InvoiceStatusChangedTriggerData()
{
ExternalServiceId = "A",
Invoice = new BtcPayInvoice()
{
Status = BtcPayInvoice.STATUS_INVALID,
CurrentTime = DateTimeOffset.Now,
InvoiceTime = DateTimeOffset.Now,
ExpirationTime = DateTimeOffset.Now
}
}
},
new RecipeTrigger()
{
TriggerId = handler.TriggerId,
ExternalServiceId = "A",
DataJson = JsonConvert.SerializeObject(parameters)
}));
Assert.False(await handler.IsTriggered(new InvoiceStatusChangedTrigger()
{
Data = new InvoiceStatusChangedTriggerData()
{
ExternalServiceId = "A",
Invoice = new BtcPayInvoice()
{
Status = BtcPayInvoice.STATUS_PAID,
ExceptionStatus = Invoice.EXSTATUS_PAID_OVER,
CurrentTime = DateTimeOffset.Now,
InvoiceTime = DateTimeOffset.Now,
ExpirationTime = DateTimeOffset.Now
}
}
},
new RecipeTrigger()
{
TriggerId = handler.TriggerId,
ExternalServiceId = "A",
DataJson = JsonConvert.SerializeObject(parameters)
}));
Assert.True(await handler.IsTriggered(new InvoiceStatusChangedTrigger()
{
Data = new InvoiceStatusChangedTriggerData()
{
ExternalServiceId = "A",
Invoice = new BtcPayInvoice()
{
Status = BtcPayInvoice.STATUS_PAID,
ExceptionStatus = Invoice.EXSTATUS_FALSE,
CurrentTime = DateTimeOffset.Now,
InvoiceTime = DateTimeOffset.Now,
ExpirationTime = DateTimeOffset.Now
}
}
},
new RecipeTrigger()
{
TriggerId = handler.TriggerId,
ExternalServiceId = "A",
DataJson = JsonConvert.SerializeObject(parameters)
}));
Assert.True(await handler.IsTriggered(new InvoiceStatusChangedTrigger()
{
Data = new InvoiceStatusChangedTriggerData()
{
ExternalServiceId = "A",
Invoice = new BtcPayInvoice()
{
Status = BtcPayInvoice.STATUS_INVALID,
ExceptionStatus = "paidLate",
CurrentTime = DateTimeOffset.Now,
InvoiceTime = DateTimeOffset.Now,
ExpirationTime = DateTimeOffset.Now
}
}
},
new RecipeTrigger()
{
TriggerId = handler.TriggerId,
ExternalServiceId = "A",
DataJson = JsonConvert.SerializeObject(parameters)
}));
}
}
}

View File

@ -22,8 +22,9 @@ namespace BtcTransmuter.Extension.BtcPayServer.Actions.GetPaymentsFromInvoice
{
private readonly IExternalServiceManager _externalServiceManager;
private static readonly SelectListItem[] PaymentTypes =
public static readonly SelectListItem[] PaymentTypes =
{
new SelectListItem("Any", ""),
new SelectListItem("On-Chain", "BTCLike"),
new SelectListItem("Off-Chain", "LightningLike")
};

View File

@ -35,11 +35,12 @@ namespace BtcTransmuter.Extension.BtcPayServer.Actions.GetPaymentsFromInvoice
var client = service.ConstructClient();
var invoice = await client.GetInvoiceAsync<BtcPayInvoice>(invoiceId);
var payments = invoice.CryptoInfo.SingleOrDefault(info => info.CryptoCode.Equals(actionData.CryptoCode))?
.Payments.Where(x =>
var payments = invoice.CryptoInfo.Where(info => info.CryptoCode.Equals(actionData.CryptoCode))
.SelectMany(info => info.Payments)
.Where(x =>
string.IsNullOrEmpty(actionData.PaymentType) ||
x.PaymentType.Equals(actionData.PaymentType))
.ToList() ?? new List<InvoicePaymentInfo>();
.ToList();
return new BtcPayServerActionHandlerResult<List<InvoicePaymentInfo>>()
{

View File

@ -1,24 +1,16 @@
<Project Sdk="Microsoft.NET.Sdk.Razor">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<TargetFramework>netcoreapp3.1</TargetFramework>
<AddRazorSupportForMvc>true</AddRazorSupportForMvc>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Mvc" Version="2.2.0" />
<ProjectReference Include="..\BtcTransmuter.Abstractions\BtcTransmuter.Abstractions.csproj" />
<ProjectReference Include="..\BtcTransmuter.Extension.DynamicServices\BtcTransmuter.Extension.DynamicServices.csproj" />
<ProjectReference Include="..\BtcTransmuter.Abstractions\BtcTransmuter.Abstractions.csproj" />
<ProjectReference Include="..\BtcTransmuter.Extension.DynamicServices\BtcTransmuter.Extension.DynamicServices.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="2.2.0" />
<PackageReference Include="NBitcoin" Version="4.1.1.99" />
<PackageReference Include="NBitpayClient" Version="1.0.0.34" />
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="3.1.4" />
<PackageReference Include="NBitcoin" Version="5.0.40" />
<PackageReference Include="NBitpayClient" Version="1.0.0.38" />
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="Styles\**;Views\**;Scripts\**" />
</ItemGroup>
</Project>

View File

@ -6,7 +6,8 @@ namespace BtcTransmuter.Extension.BtcPayServer.ExternalServices.BtcPayServer
{
public class BtcPayServerExternalServiceData
{
[Required][Display(Name = "BtcPay Host Url")] public Uri Server { get; set; }
[Required][Display(Name = "BtcPay Host Url")]
[Validation.Uri] public Uri Server { get; set; }
public string Seed { get; set; }

View File

@ -19,7 +19,7 @@ namespace BtcTransmuter.Extension.BtcPayServer.ExternalServices.BtcPayServer
public override string Name => "BtcPayServer External Service";
public override string Description => "BtcPayServer External Service to be able to interact with its services";
public override string ViewPartial => "ViewBtcPayServerExternalService";
protected override string ControllerName => "BtcPayServer";
public override string ControllerName => "BtcPayServer";
public BtcPayServerService() : base()
@ -30,7 +30,7 @@ namespace BtcTransmuter.Extension.BtcPayServer.ExternalServices.BtcPayServer
{
}
public Bitpay ConstructClient()
public virtual Bitpay ConstructClient()
{
try
{
@ -45,10 +45,17 @@ namespace BtcTransmuter.Extension.BtcPayServer.ExternalServices.BtcPayServer
}
}
public async Task<bool> CheckAccess()
public virtual async Task<bool> CheckAccess()
{
var client = ConstructClient();
return client != null && await client.TestAccessAsync(Facade.Merchant);
try
{
var client = ConstructClient();
return client != null && await client.TestAccessAsync(Facade.Merchant);
}
catch (Exception)
{
return false;
}
}
public async Task<string> GetPairingUrl(string label)
@ -69,7 +76,7 @@ namespace BtcTransmuter.Extension.BtcPayServer.ExternalServices.BtcPayServer
catch (Exception)
{
var data = GetData();
return new Uri(data.Server, "api-tokens").ToString();
return Uri.TryCreate(data.Server, "api-tokens", out var result) ? result.ToString() : null;
}
}
}

View File

@ -19,6 +19,7 @@ namespace BtcTransmuter.Extension.BtcPayServer.HostedServices
private readonly IExternalServiceManager _externalServiceManager;
private readonly ITriggerDispatcher _triggerDispatcher;
private ConcurrentDictionary<string, BtcPayServerService> _externalServices;
protected virtual TimeSpan CheckInterval { get; } = TimeSpan.FromMinutes(1);
public BtcPayInvoiceWatcherHostedService(IExternalServiceManager externalServiceManager,
ITriggerDispatcher triggerDispatcher)
@ -39,19 +40,24 @@ namespace BtcTransmuter.Extension.BtcPayServer.HostedServices
_externalServices = new ConcurrentDictionary<string, BtcPayServerService>(
externalServices
.Select(service =>
new KeyValuePair<string, BtcPayServerService>(service.Id, new BtcPayServerService(service))));
new KeyValuePair<string, BtcPayServerService>(service.Id, GetServiceFromData(service))));
_externalServiceManager.ExternalServiceDataUpdated += ExternalServiceManagerOnExternalServiceUpdated;
_ = Loop(cancellationToken);
}
protected virtual BtcPayServerService GetServiceFromData(ExternalServiceData externalServiceData)
{
return new BtcPayServerService(externalServiceData);
}
private async Task Loop(CancellationToken cancellationToken)
{
while (!cancellationToken.IsCancellationRequested)
{
var tasks = _externalServices.Select(CheckInvoiceChangeInService);
await Task.WhenAll(tasks);
await Task.Delay(TimeSpan.FromMinutes(1), cancellationToken);
await Task.Delay(CheckInterval, cancellationToken);
}
}
@ -75,7 +81,7 @@ namespace BtcTransmuter.Extension.BtcPayServer.HostedServices
var client = service.ConstructClient();
var invoices = await client.GetInvoicesAsync<BtcPayInvoice>(data.PairedDate);
var invoices = await client.GetInvoicesAsync<BtcPayInvoice>(data.PairedDate, null);
foreach (var invoice in invoices)
{
@ -86,7 +92,7 @@ namespace BtcTransmuter.Extension.BtcPayServer.HostedServices
{
if (data.MonitoredInvoiceStatuses[invoice.Id] != invoice.Status)
{
await _triggerDispatcher.DispatchTrigger(new InvoiceStatusChangedTrigger()
_ = _triggerDispatcher.DispatchTrigger(new InvoiceStatusChangedTrigger()
{
Data = new InvoiceStatusChangedTriggerData()
{
@ -98,7 +104,7 @@ namespace BtcTransmuter.Extension.BtcPayServer.HostedServices
}
else
{
await _triggerDispatcher.DispatchTrigger(new InvoiceStatusChangedTrigger()
_ = _triggerDispatcher.DispatchTrigger(new InvoiceStatusChangedTrigger()
{
Data = new InvoiceStatusChangedTriggerData()
{
@ -112,8 +118,8 @@ namespace BtcTransmuter.Extension.BtcPayServer.HostedServices
data.MonitoredInvoiceStatuses.AddOrReplace(invoice.Id, invoice.Status);
}
service.SetData(data);
await _externalServiceManager.UpdateInternalData(key, data);
}
catch (Exception e)
@ -137,13 +143,13 @@ namespace BtcTransmuter.Extension.BtcPayServer.HostedServices
switch (e.Action)
{
case UpdatedItem<ExternalServiceData>.UpdateAction.Added:
_externalServices.TryAdd(e.Item.Id, new BtcPayServerService(e.Item));
_externalServices.TryAdd(e.Item.Id, GetServiceFromData(e.Item));
break;
case UpdatedItem<ExternalServiceData>.UpdateAction.Removed:
_externalServices.TryRemove(e.Item.Id, out var _);
break;
case UpdatedItem<ExternalServiceData>.UpdateAction.Updated:
_externalServices.TryUpdate(e.Item.Id, new BtcPayServerService(e.Item), null);
_externalServices.TryUpdate(e.Item.Id, GetServiceFromData(e.Item), null);
break;
default:
throw new ArgumentOutOfRangeException();

View File

@ -1,5 +1,9 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Threading.Tasks;
using BtcTransmuter.Abstractions.Extensions;
using BtcTransmuter.Abstractions.ExternalServices;
using BtcTransmuter.Abstractions.Recipes;
using BtcTransmuter.Abstractions.Triggers;
@ -22,18 +26,34 @@ namespace BtcTransmuter.Extension.BtcPayServer.Triggers.InvoiceStatusChanged
{
private readonly IExternalServiceManager _externalServiceManager;
private readonly SelectListItem[] AllowedStatuses = new SelectListItem[]
public static readonly SelectListItem[] AllowedStatuses = new SelectListItem[]
{
new SelectListItem() {Text = "Any Status", Value = null},
new SelectListItem() {Text = "New", Value = Invoice.STATUS_NEW},
new SelectListItem() {Text = "Paid", Value = Invoice.STATUS_PAID},
new SelectListItem() {Text = "Invalid", Value = Invoice.STATUS_INVALID},
new SelectListItem() {Text = "Confirmed", Value = Invoice.STATUS_CONFIRMED},
new SelectListItem() {Text = "Complete", Value = Invoice.STATUS_COMPLETE}
new SelectListItem() {Text = "Complete", Value = Invoice.STATUS_COMPLETE},
new SelectListItem() {Text = "Expired", Value = "expired"},
};
public static SelectListItem[] AllowedExceptionStatus = new SelectListItem[]
{
new SelectListItem() {Text = "None", Value = Invoice.EXSTATUS_FALSE},
new SelectListItem() {Text = "Paid partially", Value = Invoice.EXSTATUS_PAID_PARTIAL},
new SelectListItem() {Text = "Paid over", Value = Invoice.EXSTATUS_PAID_OVER},
new SelectListItem() {Text = "Paid late", Value = "paidLate"},
new SelectListItem() {Text = "Marked", Value = "marked"},
};
public InvoiceStatusChangedController(IRecipeManager recipeManager, UserManager<User> userManager,
public static SelectListItem[] GetAvailableStatuses(string[] usedKeys)
{
return AllowedStatuses.Where(item => !usedKeys.Contains(item.Value)).ToArray();
}
public InvoiceStatusChangedController(IRecipeManager recipeManager, UserManager<User> userManager,
IMemoryCache memoryCache, IExternalServiceManager externalServiceManager) : base(recipeManager, userManager,
memoryCache, externalServiceManager)
{
@ -56,8 +76,7 @@ namespace BtcTransmuter.Extension.BtcPayServer.Triggers.InvoiceStatusChanged
nameof(ExternalServiceData.Name), data.ExternalServiceId),
RecipeId = data.RecipeId,
ExternalServiceId = data.ExternalServiceId,
Status = fromData.Status,
Statuses = new SelectList(AllowedStatuses, "Value", "Text", fromData.Status),
Conditions = fromData.Conditions,
};
}
@ -65,7 +84,7 @@ namespace BtcTransmuter.Extension.BtcPayServer.Triggers.InvoiceStatusChanged
BuildModel(
InvoiceStatusChangedTriggerViewModel viewModel, RecipeTrigger mainModel)
{
if (!ModelState.IsValid)
if (viewModel.Action != "EditData" || !ModelState.IsValid)
{
var btcPayServices = await _externalServiceManager.GetExternalServicesData(
new ExternalServicesDataQuery()
@ -73,12 +92,29 @@ namespace BtcTransmuter.Extension.BtcPayServer.Triggers.InvoiceStatusChanged
Type = new[] {BtcPayServerService.BtcPayServerServiceType},
UserId = GetUserId()
});
viewModel.Statuses = new SelectList(AllowedStatuses, "Value", "Text", viewModel.Status);
viewModel.ExternalServices = new SelectList(btcPayServices, nameof(ExternalServiceData.Id),
nameof(ExternalServiceData.Name), viewModel.ExternalServiceId);
return (null, viewModel);
if (viewModel.Action.StartsWith("add-condition", StringComparison.InvariantCultureIgnoreCase))
{
viewModel.Conditions.Add(new InvoiceStatusChangeCondition()
{
Status = GetAvailableStatuses(viewModel.Conditions.Select(condition => condition.Status).ToArray()).FirstOrDefault()?.Value?? Invoice.STATUS_NEW,
ExceptionStatuses = new List<string>()
{
Invoice.EXSTATUS_FALSE
}
});
}
if (viewModel.Action.StartsWith("remove-condition", StringComparison.InvariantCultureIgnoreCase))
{
var index = int.Parse(viewModel.Action.Substring(viewModel.Action.IndexOf(":", StringComparison.InvariantCultureIgnoreCase) + 1));
viewModel.Conditions.RemoveAt(index);
}
return (null, viewModel);
}
mainModel.ExternalServiceId = viewModel.ExternalServiceId;
@ -88,10 +124,10 @@ namespace BtcTransmuter.Extension.BtcPayServer.Triggers.InvoiceStatusChanged
public class InvoiceStatusChangedTriggerViewModel : InvoiceStatusChangedTriggerParameters
{
public string Action { get; set; }
public string RecipeId { get; set; }
public SelectList ExternalServices { get; set; }
[Required][Display(Name = "BtcPay Service")] public string ExternalServiceId { get; set; }
public SelectList Statuses { get; set; }
}
}
}

View File

@ -2,11 +2,13 @@ using System;
using System.Threading.Tasks;
using BtcTransmuter.Abstractions.Triggers;
using BtcTransmuter.Data.Entities;
using NBitpayClient;
using Newtonsoft.Json.Linq;
namespace BtcTransmuter.Extension.BtcPayServer.Triggers.InvoiceStatusChanged
{
public class InvoiceStatusChangedTriggerHandler : BaseTriggerHandler<InvoiceStatusChangedTriggerData,
InvoiceStatusChangedTriggerParameters>
InvoiceStatusChangedTriggerParameters>
{
public override string TriggerId => new InvoiceStatusChangedTrigger().Id;
public override string Name => "BtcPayServer Invoice Status Change";
@ -15,19 +17,30 @@ namespace BtcTransmuter.Extension.BtcPayServer.Triggers.InvoiceStatusChanged
"Trigger a recipe by detecting a status change of an invoice in a btcpay external service.";
public override string ViewPartial => "ViewInvoiceStatusChangedTrigger";
protected override string ControllerName => "InvoiceStatusChanged";
public override string ControllerName => "InvoiceStatusChanged";
protected override Task<bool> IsTriggered(ITrigger trigger, RecipeTrigger recipeTrigger,
InvoiceStatusChangedTriggerData triggerData,
InvoiceStatusChangedTriggerParameters parameters)
{
if (string.IsNullOrEmpty(parameters.Status))
var exceptionStatus =Invoice.EXSTATUS_FALSE;
if (triggerData.Invoice.ExceptionStatus.Type == JTokenType.String)
{
return Task.FromResult(true);
exceptionStatus = triggerData.Invoice.ExceptionStatus.Value<string>();
}
var status = triggerData.Invoice.Status;
foreach (var condition in parameters.Conditions)
{
if (condition.Status.Equals(status, StringComparison.InvariantCultureIgnoreCase) &&
condition.ExceptionStatuses.Contains(exceptionStatus))
{
return Task.FromResult(true);
}
}
return Task.FromResult(triggerData.Invoice.Status.Equals(parameters.Status,
StringComparison.InvariantCultureIgnoreCase));
return Task.FromResult(false);
}
}
}

View File

@ -1,9 +1,22 @@
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using BtcTransmuter.Abstractions;
namespace BtcTransmuter.Extension.BtcPayServer.Triggers.InvoiceStatusChanged
{
public class InvoiceStatusChangedTriggerParameters
{
public string Status { get; set; }
public List<InvoiceStatusChangeCondition> Conditions { get; set; } = new List<InvoiceStatusChangeCondition>();
}
public class InvoiceStatusChangeCondition
{
[Required]
public string Status { get; set; }
[Display(Name = "Additional Statuses")]
[Required]
[EnsureMinimumElements(1, ErrorMessage = "At least one additional status is required")]
public List<string> ExceptionStatuses { get; set; }
}
}

View File

@ -0,0 +1,22 @@
using System;
using System.ComponentModel.DataAnnotations;
using System.Globalization;
namespace BtcTransmuter.Extension.BtcPayServer.Validation
{
public class UriAttribute : ValidationAttribute
{
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
{
var str = value == null ? null : Convert.ToString(value, CultureInfo.InvariantCulture);
Uri uri;
bool valid = string.IsNullOrWhiteSpace(str) || Uri.TryCreate(str, UriKind.Absolute, out uri);
if (!valid)
{
return new ValidationResult(ErrorMessage);
}
return ValidationResult.Success;
}
}
}

View File

@ -20,14 +20,47 @@
<span class="text-danger">There are no BtcPay external services available to create this action. <a asp-action="CreateExternalService" asp-controller="ExternalServices">Create one</a></span>
}
</div>
<div class="form-group">
<label asp-for="Status" class="control-label"></label>
<select asp-for="Status" asp-items="Model.Statuses" class="form-control"></select>
<span asp-validation-for="Status" class="text-danger"></span>
<div class="list-group mb-2">
<div class="list-group-item ">
<h5 class="mb-1">Status Conditions</h5>
</div>
@for (var index = 0; index < Model.Conditions.Count; index++)
{
var statusCondition = Model.Conditions[index];
<div class="list-group-item justify-content-between align-items-center">
<div class="row">
<div class="form-group col-sm-12 col-md-6">
<label asp-for="Conditions[index].Status" class="control-label"></label>
<select asp-for="Conditions[index].Status"
asp-items="@(new SelectList(BtcTransmuter.Extension.BtcPayServer.Triggers.InvoiceStatusChanged.InvoiceStatusChangedController.AllowedStatuses, "Value", "Text", statusCondition.Status))" class="form-control">
</select>
<span asp-validation-for="Conditions[index].Status" class="text-danger"></span>
</div>
<div class="form-group col-sm-12 col-md-6">
<label asp-for="Conditions[index].ExceptionStatuses" class="control-label"></label>
<select asp-for="Conditions[index].ExceptionStatuses"
asp-items="@(new SelectList(BtcTransmuter.Extension.BtcPayServer.Triggers.InvoiceStatusChanged.InvoiceStatusChangedController.AllowedExceptionStatus, "Value", "Text", statusCondition.ExceptionStatuses))"
multiple="multiple"
class="form-control">
</select>
<span asp-validation-for="Conditions[index].ExceptionStatuses" class="text-danger"></span>
</div>
</div>
<div>
<button type="submit" name="action" value="@($"remove-condition:{index}")" class="btn btn-danger">Remove</button>
</div>
</div>
}
<div class="list-group-item ">
<button type="submit" name="action" value="add-condition" class="btn btn-secondary">Add</button>
</div>
</div>
<input type="hidden" asp-for="RecipeId" />
<div class="mt-2">
<button type="submit" class="btn btn-primary">Save</button>
<button type="submit" name="" value="" class="btn btn-primary">Save</button>
<a asp-action="EditRecipe" asp-controller="Recipes" class="btn btn-secondary" asp-route-id="@Model.RecipeId">Back to recipe</a>
</div>
</form>

View File

@ -5,5 +5,5 @@
var data = Model.Get<GetPaymentsFromInvoiceData>();
}
<div>
Get <kbd>@data.CryptoCode @data.PaymentType </kbd>of BtcPay invoice <kbd>@data.InvoiceId</kbd>
Get <kbd>@data.CryptoCode @data.PaymentType</kbd>payments of BTCPay invoice <kbd>@data.InvoiceId</kbd>
</div>

View File

@ -6,10 +6,15 @@
}
<div>
Btcpayserver Invoice Status Change
@if (!string.IsNullOrEmpty(data.Status))
{
<span>to @data.Status</span>
}
Btcpayserver Invoice Status Change to
<kbd>
@if (data.Conditions.Any())
{
@string.Join(" or ", data.Conditions.Select(condition => $"{condition.Status}({string.Join(" or ", condition.ExceptionStatuses)})"))
}
else
{
<span>Not configured</span>
}
</kbd>
</div>

View File

@ -0,0 +1,24 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netcoreapp3.1</TargetFramework>
<AddRazorSupportForMvc>true</AddRazorSupportForMvc>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="3.1.4" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.6.1" />
<PackageReference Include="Moq" Version="4.14.1" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.1" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\BtcTransmuter.Extension.DynamicServices\BtcTransmuter.Extension.DynamicServices.csproj" />
<ProjectReference Include="..\BtcTransmuter.Tests.Base\BtcTransmuter.Tests.Base.csproj" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,15 @@
using BtcTransmuter.Extension.Webhook.Triggers.DynamicServiceMarker;
using BtcTransmuter.Tests.Base;
namespace BtcTransmuter.Extension.DynamicServices.Tests
{
public class DynamicServiceMarkerTriggerHandlerTests : BaseTriggerTest<DynamicServiceMarkerTriggerHandler,
DynamicServiceMarkerTriggerData,
DynamicServiceMarkerTriggerParameters>
{
protected override DynamicServiceMarkerTriggerHandler GetTriggerHandlerInstance(params object[] setupArgs)
{
return new DynamicServiceMarkerTriggerHandler();
}
}
}

View File

@ -0,0 +1,58 @@
using System;
using System.Linq;
using System.Threading.Tasks;
using BtcTransmuter.Abstractions.Actions;
using BtcTransmuter.Abstractions.ExternalServices;
using BtcTransmuter.Abstractions.Helpers;
using BtcTransmuter.Abstractions.Recipes;
using BtcTransmuter.Data.Entities;
using BtcTransmuter.Extension.BtcPayServer.ExternalServices.BtcPayServer;
using BtcTransmuter.Extension.DynamicService.ExternalServices.DynamicService;
using BtcTransmuter.Extension.Lightning.ExternalServices.NBXplorerWallet;
using BtcTransmuter.Extension.NBXplorer.Services;
using BtcTransmuter.Tests.Base;
using Microsoft.Extensions.DependencyInjection;
using Xunit;
namespace BtcTransmuter.Extension.DynamicServices.Tests
{
public class
DynamicServiceServiceTests : BaseExternalServiceTest<DynamicServiceService, DynamicServiceExternalServiceData>
{
public override Task CanSerializeData()
{
ConfigureDependencyHelper();
using (var scope = DependencyHelper.ServiceScopeFactory.CreateScope())
{
var x = GetExternalService();
var instance = new DynamicServiceExternalServiceData();
var externalService = GetExternalService(new ExternalServiceData()
{
Type = x.ExternalServiceType
},
scope.ServiceProvider.GetRequiredService<IRecipeManager>(),
scope.ServiceProvider.GetRequiredService<IActionDispatcher>(),
scope.ServiceProvider.GetRequiredService<IExternalServiceManager>()
);
externalService.SetData(instance);
externalService.GetData();
}
return Task.CompletedTask;
}
protected override DynamicServiceService GetExternalService(params object[] setupArgs)
{
if (setupArgs?.Any()?? false)
{
return new DynamicServiceService(
(ExternalServiceData) setupArgs.First(o => o is ExternalServiceData),
(IRecipeManager) setupArgs.First(o => o is IRecipeManager),
(IActionDispatcher) setupArgs.First(o => o is IActionDispatcher),
(IExternalServiceManager) setupArgs.First(o => o is IExternalServiceManager));
}
return new DynamicServiceService();
}
}
}

View File

@ -1,19 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk.Razor">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<TargetFramework>netcoreapp3.1</TargetFramework>
<AddRazorSupportForMvc>true</AddRazorSupportForMvc>
<OutputType>Library</OutputType>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Mvc" Version="2.2.0" />
<ProjectReference Include="..\BtcTransmuter.Abstractions\BtcTransmuter.Abstractions.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="2.2.0" />
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="3.1.4" />
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="Styles\**;Views\**;Scripts\**" />
</ItemGroup>
</Project>

View File

@ -24,7 +24,7 @@ namespace BtcTransmuter.Extension.DynamicService.ExternalServices.DynamicService
"DynamicService External Service to be able to resolve other external services using actions";
public override string ViewPartial => "ViewDynamicServiceExternalService";
protected override string ControllerName => "DynamicService";
public override string ControllerName => "DynamicService";
public DynamicServiceService() : base()

View File

@ -15,7 +15,7 @@ namespace BtcTransmuter.Extension.Webhook.Triggers.DynamicServiceMarker
"Used to mark a recipe to be sued for a Dynamic External Service";
public override string ViewPartial => "ViewDynamicServiceMarkerTrigger";
protected override string ControllerName => "DynamicServiceMarker";
public override string ControllerName => "DynamicServiceMarker";
protected override Task<bool> IsTriggered(ITrigger trigger, RecipeTrigger recipeTrigger,

View File

@ -0,0 +1,24 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netcoreapp3.1</TargetFramework>
<AddRazorSupportForMvc>true</AddRazorSupportForMvc>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="3.1.4" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.6.1" />
<PackageReference Include="Moq" Version="4.14.1" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.1" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\BtcTransmuter.Extension.Email\BtcTransmuter.Extension.Email.csproj" />
<ProjectReference Include="..\BtcTransmuter.Tests.Base\BtcTransmuter.Tests.Base.csproj" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,20 @@
using System.Linq;
using BtcTransmuter.Data.Entities;
using BtcTransmuter.Extension.Email.ExternalServices.Imap;
using BtcTransmuter.Tests.Base;
using Xunit;
namespace BtcTransmuter.Extension.Email.Tests
{
public class ImapServiceTests: BaseExternalServiceTest<ImapService, ImapExternalServiceData>{
protected override ImapService GetExternalService(params object[] setupArgs)
{
if (setupArgs?.Any() ?? false)
{
return new ImapService((ExternalServiceData) setupArgs.First());
}
return new ImapService();
}
}
}

View File

@ -0,0 +1,19 @@
using System.Linq;
using BtcTransmuter.Data.Entities;
using BtcTransmuter.Extension.Email.ExternalServices.Pop3;
using BtcTransmuter.Tests.Base;
namespace BtcTransmuter.Extension.Email.Tests
{
public class Pop3ServiceTests: BaseExternalServiceTest<Pop3Service, Pop3ExternalServiceData>{
protected override Pop3Service GetExternalService(params object[] setupArgs)
{
if (setupArgs?.Any() ?? false)
{
return new Pop3Service((ExternalServiceData) setupArgs.First());
}
return new Pop3Service();
}
}
}

View File

@ -0,0 +1,15 @@
using BtcTransmuter.Extension.Email.Triggers.ReceivedEmail;
using BtcTransmuter.Tests.Base;
namespace BtcTransmuter.Extension.Email.Tests
{
public class ReceivedEmailTriggerHandlerTests : BaseTriggerTest<ReceivedEmailTriggerHandler,
ReceivedEmailTriggerData,
ReceivedEmailTriggerParameters>
{
protected override ReceivedEmailTriggerHandler GetTriggerHandlerInstance(params object[] setupArgs)
{
return new ReceivedEmailTriggerHandler();
}
}
}

View File

@ -0,0 +1,14 @@
using System;
using BtcTransmuter.Extension.Email.Actions.SendEmail;
using BtcTransmuter.Tests.Base;
namespace BtcTransmuter.Extension.Email.Tests
{
public class SendEmailDataActionHandlerTests : BaseActionTest<SendEmailDataActionHandler, SendEmailData, string>
{
protected override SendEmailDataActionHandler GetActionHandlerInstance(params object[] setupArgs)
{
return new SendEmailDataActionHandler();
}
}
}

View File

@ -0,0 +1,19 @@
using System.Linq;
using BtcTransmuter.Data.Entities;
using BtcTransmuter.Extension.Email.ExternalServices.Smtp;
using BtcTransmuter.Tests.Base;
namespace BtcTransmuter.Extension.Email.Tests
{
public class SmtpServiceTests: BaseExternalServiceTest<SmtpService, SmtpExternalServiceData>{
protected override SmtpService GetExternalService(params object[] setupArgs)
{
if (setupArgs?.Any() ?? false)
{
return new SmtpService((ExternalServiceData) setupArgs.First());
}
return new SmtpService();
}
}
}

View File

@ -1,3 +1,5 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Threading.Tasks;
using BtcTransmuter.Abstractions.Actions;
@ -11,6 +13,8 @@ using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.Extensions.Caching.Memory;
using MimeKit;
using MimeKit.Text;
namespace BtcTransmuter.Extension.Email.Actions.SendEmail
{
@ -44,15 +48,16 @@ namespace BtcTransmuter.Extension.Email.Actions.SendEmail
Subject = fromData.Subject,
To = fromData.To,
From = fromData.From,
IsHTML = fromData.IsHTML,
ExternalServices = new SelectList(services, nameof(ExternalServiceData.Id),
nameof(ExternalServiceData.Name), from.ExternalServiceId),
};
}
protected override async Task<(RecipeAction ToSave, SendEmailViewModel showViewModel)> BuildModel(
SendEmailViewModel viewModel, RecipeAction mainModel)
{
if (ModelState.IsValid)
if(ModelState.IsValid)
{
mainModel.ExternalServiceId = viewModel.ExternalServiceId;
mainModel.Set<SendEmailData>(viewModel);

View File

@ -10,5 +10,6 @@ namespace BtcTransmuter.Extension.Email.Actions.SendEmail
public string To { get; set; }
public string Body { get; set; }
public string Subject { get; set; }
public bool IsHTML { get; set; } = false;
}
}

View File

@ -34,7 +34,7 @@ namespace BtcTransmuter.Extension.Email.Actions.SendEmail
{
InternetAddress.Parse(InterpolateString(actionData.To, data))
}, InterpolateString(actionData.Subject, data),
new TextPart(TextFormat.Plain)
new TextPart(actionData.IsHTML ? TextFormat.Html : TextFormat.Plain)
{
Text = InterpolateString(actionData.Body, data)
});

View File

@ -1,22 +1,15 @@
<Project Sdk="Microsoft.NET.Sdk.Razor">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<TargetFramework>netcoreapp3.1</TargetFramework>
<AddRazorSupportForMvc>true</AddRazorSupportForMvc>
<OutputType>Library</OutputType>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Mvc" Version="2.2.0" />
<ProjectReference Include="..\BtcTransmuter.Abstractions\BtcTransmuter.Abstractions.csproj" />
<ProjectReference Include="..\BtcTransmuter.Extension.DynamicServices\BtcTransmuter.Extension.DynamicServices.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="MailKit" Version="2.1.4" />
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="2.2.0" />
<ProjectReference Include="..\BtcTransmuter.Abstractions\BtcTransmuter.Abstractions.csproj" />
<ProjectReference Include="..\BtcTransmuter.Extension.DynamicServices\BtcTransmuter.Extension.DynamicServices.csproj" />
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="Styles\**;Views\**;Scripts\**" />
<PackageReference Include="MailKit" Version="2.6.0" />
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="3.1.4" />
</ItemGroup>
</Project>

View File

@ -14,7 +14,7 @@ namespace BtcTransmuter.Extension.Email.ExternalServices.Imap
public override string Name => "Imap External Service";
public override string Description => "Imap External Service to be able to analyze incoming email as a trigger";
public override string ViewPartial => "ViewImapExternalService";
protected override string ControllerName => "Imap";
public override string ControllerName => "Imap";
public ImapService() : base()

View File

@ -14,8 +14,8 @@ namespace BtcTransmuter.Extension.Email.ExternalServices.Pop3
public override string Name => "Pop3 External Service";
public override string Description => "Pop3 External Service to be able to analyze incoming email as a trigger";
public override string ViewPartial => "ViewPop3ExternalService";
protected override string ControllerName => "Pop3";
public override string ControllerName => "Pop3";
public Pop3Service() : base()

View File

@ -1,3 +1,6 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Threading.Tasks;
using BtcTransmuter.Abstractions.ExternalServices;
using BtcTransmuter.Data.Entities;
@ -6,12 +9,14 @@ using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Caching.Memory;
using MimeKit;
using MimeKit.Text;
namespace BtcTransmuter.Extension.Email.ExternalServices.Smtp
{
[Route("email-plugin/external-services/smtp")]
[Authorize]
public class SmtpController : BaseExternalServiceController<SmtpExternalServiceData>
public class SmtpController : BaseExternalServiceController<EditSmtpExternalServiceViewModel>
{
public SmtpController(IExternalServiceManager externalServiceManager, UserManager<User> userManager,
IMemoryCache memoryCache) : base(externalServiceManager, userManager, memoryCache)
@ -20,23 +25,77 @@ namespace BtcTransmuter.Extension.Email.ExternalServices.Smtp
protected override string ExternalServiceType => SmtpService.SmtpExternalServiceType;
protected override Task<SmtpExternalServiceData> BuildViewModel(ExternalServiceData data)
protected override Task<EditSmtpExternalServiceViewModel> BuildViewModel(ExternalServiceData data)
{
return Task.FromResult(new SmtpService(data).GetData());
var serviceData = new SmtpService(data).GetData();
return Task.FromResult(new EditSmtpExternalServiceViewModel()
{
Password = serviceData.Password,
Port = serviceData.Port,
Server = serviceData.Server,
Username = serviceData.Username,
SSL = serviceData.SSL
});
}
protected override Task<(ExternalServiceData ToSave, SmtpExternalServiceData showViewModel)> BuildModel(
SmtpExternalServiceData viewModel, ExternalServiceData mainModel)
protected override async Task<(ExternalServiceData ToSave, EditSmtpExternalServiceViewModel showViewModel)>
BuildModel(
EditSmtpExternalServiceViewModel viewModel, ExternalServiceData mainModel)
{
if (ModelState.IsValid && !string.IsNullOrEmpty(viewModel.TestEmail))
{
var data = new ExternalServiceData();
data.Set(viewModel);
data.Type = ExternalServiceType;
var smtpService = new SmtpService(data);
var error = await SendTestEmail(smtpService, viewModel.TestEmail);
if (string.IsNullOrEmpty(error))
{
viewModel.TestEmail = string.Empty;
}
else
{
ModelState.AddModelError(nameof(viewModel.TestEmail),error);
}
}
if (!ModelState.IsValid)
{
return Task.FromResult<(ExternalServiceData ToSave, SmtpExternalServiceData showViewModel)>((null,
viewModel));
return (null, viewModel);
}
mainModel.Set(viewModel);
return Task.FromResult<(ExternalServiceData ToSave, SmtpExternalServiceData showViewModel)>((mainModel,
null));
return (mainModel, null);
}
private async Task<string> SendTestEmail(SmtpService service, string email)
{
try
{
await service.SendEmail(new MimeMessage(new List<InternetAddress>()
{
InternetAddress.Parse(email)
}, new List<InternetAddress>()
{
InternetAddress.Parse(email)
}, "BTCTransmuter Test Email", new TextPart(TextFormat.Plain)
{
Text = "Just testing your email setup for BTC Transmuter (ノ◕ヮ◕)ノ*:・゚✧"
}));
return null;
}
catch (Exception e)
{
return e.Message;
}
}
}
}
public class EditSmtpExternalServiceViewModel : SmtpExternalServiceData
{
[EmailAddress]
[Display(Name = "Send test email from and to this address to check if your settings are valid")]
public string TestEmail { get; set; }
}
}

View File

@ -14,7 +14,7 @@ namespace BtcTransmuter.Extension.Email.ExternalServices.Smtp
public override string Name => "SMTP External Service";
public override string Description => "SMTP External Service to be able to send emails as an action";
public override string ViewPartial => "ViewSmtpExternalService";
protected override string ControllerName => "Smtp";
public override string ControllerName => "Smtp";
public SmtpService() : base()

View File

@ -73,7 +73,7 @@ namespace BtcTransmuter.Extension.Email.HostedServices
await inbox.OpenAsync(FolderAccess.ReadOnly);
var emailIds =
await inbox.SearchAsync(new DateSearchQuery(SearchTerm.SentAfter,
await inbox.SearchAsync(new DateSearchQuery(SearchTerm.DeliveredAfter,
data.LastCheck.GetValueOrDefault(data.PairedDate)));
@ -90,7 +90,7 @@ namespace BtcTransmuter.Extension.Email.HostedServices
ExternalServiceId = service.Key
}
};
await _triggerDispatcher.DispatchTrigger(trigger);
_ = _triggerDispatcher.DispatchTrigger(trigger);
}
data.LastCheck = DateTime.Now;

View File

@ -87,7 +87,7 @@ namespace BtcTransmuter.Extension.Email.HostedServices
ExternalServiceId = service.Key
}
};
await _triggerDispatcher.DispatchTrigger(trigger);
_ = _triggerDispatcher.DispatchTrigger(trigger);
}
data.LastCheck = DateTime.Now;

View File

@ -21,7 +21,7 @@ namespace BtcTransmuter.Extension.Email.Triggers.ReceivedEmail
"Trigger a recipe by receiving a specifically formatted email through a pop3 or imap external service.";
public override string ViewPartial => "ViewReceivedEmailTrigger";
protected override string ControllerName => "ReceivedEmail";
public override string ControllerName => "ReceivedEmail";
protected override Task<bool> IsTriggered(ITrigger trigger, RecipeTrigger recipeTrigger,
ReceivedEmailTriggerData triggerData,

View File

@ -39,7 +39,11 @@
<textarea asp-for="Body" class="form-control"></textarea>
<span asp-validation-for="Body" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="IsHTML" class="control-label"></label>
<input type="checkbox" asp-for="IsHTML" class="form-check"/>
<span asp-validation-for="IsHTML" class="text-danger"></span>
</div>
<input type="hidden" asp-for="RecipeId"/>
<div class="mt-2">
<button type="submit" class="btn btn-primary">Save</button>

View File

@ -13,6 +13,6 @@
}
@if (!string.IsNullOrEmpty(data.Body))
{
<span>with body <kbd>@data.Body</kbd></span>
<span>with body <kbd>@data.Body</kbd>@(data.IsHTML? "(in HTML)": "")</span>
}
</div>

View File

@ -1,4 +1,4 @@
@model BtcTransmuter.Extension.Email.ExternalServices.Smtp.SmtpExternalServiceData
@model BtcTransmuter.Extension.Email.ExternalServices.Smtp.EditSmtpExternalServiceViewModel
@{
ViewData["Title"] = "Edit Smtp External Service Data";
}
@ -33,6 +33,11 @@
<input type="checkbox" asp-for="SSL" class="form-check"/>
<span asp-validation-for="SSL" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="TestEmail" class="control-label"></label>
<input type="text" asp-for="TestEmail" class="form-control"/>
<span asp-validation-for="TestEmail" class="text-danger"></span>
</div>
<div class="mt-2">
<button type="submit" class="btn btn-primary">Save</button>
<a asp-action="GetServices" asp-controller="ExternalServices" class="btn btn-secondary">Back to list</a>

View File

@ -1,19 +1,24 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netcoreapp2.2</TargetFramework>
<TargetFramework>netcoreapp3.1</TargetFramework>
<AddRazorSupportForMvc>true</AddRazorSupportForMvc>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.0.1" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="3.1.4" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.6.1" />
<PackageReference Include="Moq" Version="4.14.1" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.1" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\BtcTransmuter.Extension.Exchange\BtcTransmuter.Extension.Exchange.csproj" />
<ProjectReference Include="..\BtcTransmuter.Tests.Base\BtcTransmuter.Tests.Base.csproj" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,11 @@
using BtcTransmuter.Extension.Exchange.Triggers.CheckExchangeBalance;
using BtcTransmuter.Tests.Base;
namespace BtcTransmuter.Extension.Exchange.Tests
{
public class CheckExchangeBalanceTriggerHandlerTests :
BaseTriggerTest<CheckExchangeBalanceTriggerHandler, CheckExchangeBalanceTriggerData,
CheckExchangeBalanceTriggerParameters>
{
}
}

View File

@ -1,18 +1,21 @@
using System;
using BtcTransmuter.Data.Entities;
using BtcTransmuter.Data.Models;
using System.Linq;
using System.Threading.Tasks;
using BtcTransmuter.Extension.Exchange.ExternalServices.Exchange;
using Microsoft.EntityFrameworkCore.Internal;
using BtcTransmuter.Tests.Base;
using Xunit;
using Assert = Xunit.Assert;
namespace BtcTransmuter.Extension.Exchange.Tests
{
public class ExchangeServiceTests
public class ExchangeServiceTests:BaseExternalServiceTest<ExchangeService,ExchangeExternalServiceData >
{
[Fact]
public void ExchangeService_GetAvailableExchanges()
public async Task ExchangeService_GetAvailableExchanges()
{
Assert.True(ExchangeService.GetAvailableExchanges().Any());
Assert.True((await ExchangeService.GetAvailableExchanges()).Any());
}
[Fact]
@ -23,7 +26,7 @@ namespace BtcTransmuter.Extension.Exchange.Tests
Assert.Throws<ArgumentException>(() =>
{
_ = new ExchangeService(new ExternalServiceData()
_ = GetExternalService(new ExternalServiceData()
{
Type = "invalid"
});
@ -47,24 +50,44 @@ namespace BtcTransmuter.Extension.Exchange.Tests
}
[Fact]
public void ExchangeService_CanConstructClient()
public async Task ExchangeService_CanConstructClient()
{
var data = new ExchangeExternalServiceData()
var InvalidData = new ExchangeExternalServiceData()
{
PublicKey = "test",
PairedDate = DateTime.Now
PairedDate = DateTime.Now,
};
var externalServiceData = new ExternalServiceData()
{
Type = ExchangeService.ExchangeServiceType,
Name = "something"
};
externalServiceData.Set(data);
externalServiceData.Set(InvalidData);
var exchangeService = new ExchangeService(externalServiceData);
var exchangeService = GetExternalService(externalServiceData);
await Assert.ThrowsAnyAsync<Exception>(async () => await exchangeService.ConstructClient());
var validData = new ExchangeExternalServiceData()
{
PublicKey = "test",
PairedDate = DateTime.Now,
ExchangeName = "Binance",
PrivateKey = "aa"
};
externalServiceData.Set(validData);
Assert.NotNull(exchangeService.ConstructClient());
}
protected override ExchangeService GetExternalService(params object[] setupArgs)
{
if (setupArgs?.Any()?? false)
{
return new ExchangeService((ExternalServiceData) setupArgs.First());
}
return new ExchangeService();
}
}
}

View File

@ -0,0 +1,11 @@
using BtcTransmuter.Abstractions.Actions;
using BtcTransmuter.Extension.Exchange.Actions.GetExchangeBalance;
using BtcTransmuter.Tests.Base;
using Microsoft.AspNetCore.Mvc.Testing;
namespace BtcTransmuter.Extension.Exchange.Tests
{
public class GetExchangeBalanceDataActionHandlerTests :BaseActionTest<GetExchangeBalanceDataActionHandler, GetExchangeBalanceData, decimal>
{
}
}

View File

@ -0,0 +1,11 @@
using BtcTransmuter.Extension.Exchange.Actions.GetExchangeRate;
using BtcTransmuter.Tests.Base;
using ExchangeSharp;
namespace BtcTransmuter.Extension.Exchange.Tests
{
public class GetExchangeRateDataActionHandlerTests :
BaseActionTest<GetExchangeRateDataActionHandler, GetExchangeRateData, ExchangeTicker>
{
}
}

View File

@ -0,0 +1,11 @@
using BtcTransmuter.Extension.Exchange.Actions.PlaceOrder;
using BtcTransmuter.Tests.Base;
using ExchangeSharp;
namespace BtcTransmuter.Extension.Exchange.Tests
{
public class PlaceOrderDataActionHandlerTests :
BaseActionTest<PlaceOrderDataActionHandler,PlaceOrderData, ExchangeOrderResult>
{
}
}

View File

@ -30,6 +30,17 @@ namespace BtcTransmuter.Extension.Exchange.Actions.GetExchangeBalance
_externalServiceManager = externalServiceManager;
}
[HttpGet("symbols/{externalServiceId}")]
public async Task<string[]> GetAvailableMarketSymbols(string externalServiceId)
{
var serviceData =
await _externalServiceManager.GetExternalServiceData(externalServiceId, GetUserId());
var exchangeService = new ExchangeService(serviceData);
var symbols = await (await exchangeService.ConstructClient()).GetCurrenciesAsync();
return symbols.Keys.ToArray();
}
protected override async Task<GetExchangeBalanceViewModel> BuildViewModel(RecipeAction from)
{
var services = await _externalServiceManager.GetExternalServicesData(new ExternalServicesDataQuery()

View File

@ -1,11 +1,11 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using BtcTransmuter.Abstractions.Actions;
using BtcTransmuter.Data.Entities;
using BtcTransmuter.Extension.DynamicServices;
using BtcTransmuter.Extension.Exchange.ExternalServices.Exchange;
using ExchangeSharp;
namespace BtcTransmuter.Extension.Exchange.Actions.GetExchangeBalance
{
@ -28,10 +28,13 @@ namespace BtcTransmuter.Extension.Exchange.Actions.GetExchangeBalance
var externalService = await recipeAction.GetExternalService();
var exchangeService = new ExchangeService(externalService);
var client = exchangeService.ConstructClient();
var client = await exchangeService.ConstructClient();
var result = await client.GetAmountsAsync();
var amount = result.ContainsKey(actionData.Asset) ? result[actionData.Asset] : 0;
var matched = result
.FirstOrDefault(pair => pair.Key.Equals(actionData.Asset, StringComparison.InvariantCultureIgnoreCase));
var amount = matched.Value;
return new TypedActionHandlerResult<decimal>()
{

View File

@ -55,7 +55,7 @@ namespace BtcTransmuter.Extension.Exchange.Actions.GetExchangeRate
var serviceData =
await _externalServiceManager.GetExternalServiceData(viewModel.ExternalServiceId, GetUserId());
var exchangeService = new ExchangeService(serviceData);
var symbols = (await exchangeService.ConstructClient().GetMarketSymbolsAsync()).ToArray();
var symbols = (await (await exchangeService.ConstructClient()).GetMarketSymbolsAsync()).ToArray();
if (symbols.Contains(viewModel.MarketSymbol))
{
mainModel.ExternalServiceId = viewModel.ExternalServiceId;

View File

@ -26,7 +26,7 @@ namespace BtcTransmuter.Extension.Exchange.Actions.GetExchangeRate
{
var externalService = await recipeAction.GetExternalService();
var exchangeService = new ExchangeService(externalService);
var client = exchangeService.ConstructClient();
var client = await exchangeService.ConstructClient();
var result = await client.GetTickerAsync(actionData.MarketSymbol);

View File

@ -1,3 +1,5 @@
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Threading.Tasks;
@ -30,6 +32,15 @@ namespace BtcTransmuter.Extension.Exchange.Actions.PlaceOrder
_externalServiceManager = externalServiceManager;
}
[HttpGet("symbols/{externalServiceId}")]
public async Task<string[]> GetAvailableMarketSymbols(string externalServiceId)
{
var serviceData =
await _externalServiceManager.GetExternalServiceData(externalServiceId, GetUserId());
var exchangeService = new ExchangeService(serviceData);
return (await (await exchangeService.ConstructClient()).GetMarketSymbolsAsync()).ToArray();
}
protected override async Task<PlaceOrderViewModel> BuildViewModel(RecipeAction from)
{
var services = await _externalServiceManager.GetExternalServicesData(new ExternalServicesDataQuery()
@ -68,7 +79,7 @@ namespace BtcTransmuter.Extension.Exchange.Actions.PlaceOrder
var serviceData =
await _externalServiceManager.GetExternalServiceData(viewModel.ExternalServiceId, GetUserId());
var exchangeService = new ExchangeService(serviceData);
var symbols = (await exchangeService.ConstructClient().GetMarketSymbolsAsync()).ToArray();
var symbols = (await (await exchangeService.ConstructClient()).GetMarketSymbolsAsync()).ToArray();
if (symbols.Contains(viewModel.MarketSymbol))
{
mainModel.ExternalServiceId = viewModel.ExternalServiceId;

View File

@ -6,13 +6,14 @@ using BtcTransmuter.Data.Entities;
using BtcTransmuter.Extension.DynamicServices;
using BtcTransmuter.Extension.Exchange.ExternalServices.Exchange;
using ExchangeSharp;
using Newtonsoft.Json;
namespace BtcTransmuter.Extension.Exchange.Actions.PlaceOrder
{
public class PlaceOrderDataActionHandler : BaseActionHandler<PlaceOrderData, ExchangeOrderResult>
{
public override string ActionId => "PlaceOrder";
public override string Name => "Place Order";
public override string ActionId => "PlaceOrder";
public override string Name => "Place order on an Exchange";
public override string Description =>
"Place an order on an exchange";
@ -20,13 +21,12 @@ namespace BtcTransmuter.Extension.Exchange.Actions.PlaceOrder
public override string ViewPartial => "ViewPlaceOrderAction";
public override string ControllerName => "PlaceOrder";
protected override async Task<TypedActionHandlerResult<ExchangeOrderResult>> Execute(Dictionary<string, object> data, RecipeAction recipeAction,
PlaceOrderData actionData)
{
var externalService = await recipeAction.GetExternalService();
var exchangeService = new ExchangeService(externalService);
var client = exchangeService.ConstructClient();
var client = await exchangeService.ConstructClient();
var orderRequest = new ExchangeOrderRequest()
{
MarketSymbol = actionData.MarketSymbol,
@ -36,13 +36,14 @@ namespace BtcTransmuter.Extension.Exchange.Actions.PlaceOrder
StopPrice = Convert.ToDecimal(InterpolateString(actionData.StopPrice, data)),
IsBuy = actionData.IsBuy,
IsMargin = actionData.IsMargin,
ShouldRoundAmount = false
};
try
{
var result = await client.PlaceOrderAsync(orderRequest);
var result = await client.PlaceOrderAsync(orderRequest);
System.Threading.Thread.Sleep(500);
result = await client.GetOrderDetailsAsync(result.OrderId);
result = await client.GetOrderDetailsAsync(result.OrderId, orderRequest.MarketSymbol);
return new TypedActionHandlerResult<ExchangeOrderResult>()
{
Executed = true,
@ -57,9 +58,9 @@ namespace BtcTransmuter.Extension.Exchange.Actions.PlaceOrder
{
Executed = false,
Result =
$"Could not place order because {e.Message}"
$"Could not place order because {e.Message}. Order details: {JsonConvert.SerializeObject(orderRequest)}"
};
}
}
}
}
}

View File

@ -1,25 +1,17 @@
<Project Sdk="Microsoft.NET.Sdk.Razor">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<TargetFramework>netcoreapp3.1</TargetFramework>
<AddRazorSupportForMvc>true</AddRazorSupportForMvc>
<OutputType>Library</OutputType>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Mvc" Version="2.2.0" />
<PackageReference Include="DigitalRuby.ExchangeSharp" Version="0.6.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\BtcTransmuter.Abstractions\BtcTransmuter.Abstractions.csproj" />
<ProjectReference Include="..\BtcTransmuter.Extension.DynamicServices\BtcTransmuter.Extension.DynamicServices.csproj" />
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="Styles\**;Views\**;Scripts\**" />
</ItemGroup>
<ItemGroup>
<UpToDateCheckInput Remove="Views\SendEmail\EditData.cshtml" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Custom.DigitalRuby.ExchangeSharp" Version="0.9.0" />
</ItemGroup>
</Project>

View File

@ -25,10 +25,10 @@ namespace BtcTransmuter.Extension.Exchange.ExternalServices.Exchange
protected override string ExternalServiceType => ExchangeService.ExchangeServiceType;
protected override Task<EditExchangeExternalServiceDataViewModel> BuildViewModel(ExternalServiceData data)
protected override async Task<EditExchangeExternalServiceDataViewModel> BuildViewModel(ExternalServiceData data)
{
return Task.FromResult(new EditExchangeExternalServiceDataViewModel(new ExchangeService(data).GetData(),
ExchangeService.GetAvailableExchanges()));
return new EditExchangeExternalServiceDataViewModel(new ExchangeService(data).GetData(),
await ExchangeService.GetAvailableExchanges());
}
protected override async
@ -41,7 +41,7 @@ namespace BtcTransmuter.Extension.Exchange.ExternalServices.Exchange
if (!ModelState.IsValid)
{
return (null,
new EditExchangeExternalServiceDataViewModel(viewModel, ExchangeService.GetAvailableExchanges()));
new EditExchangeExternalServiceDataViewModel(viewModel, await ExchangeService.GetAvailableExchanges()));
}
//current External Service data
@ -50,11 +50,11 @@ namespace BtcTransmuter.Extension.Exchange.ExternalServices.Exchange
if (!await exchangeService.TestAccess())
{
ModelState.AddModelError(String.Empty, "Could not connect with current settings");
ModelState.AddModelError(String.Empty,
"Could not connect with current settings. Transmuter tests against fetching your balance amount from the exchange so you would need to enable that option if available");
return (null,
new EditExchangeExternalServiceDataViewModel(viewModel, ExchangeService.GetAvailableExchanges()));
new EditExchangeExternalServiceDataViewModel(viewModel, await ExchangeService.GetAvailableExchanges()));
}
return (externalServiceData, null);

View File

@ -14,7 +14,7 @@ namespace BtcTransmuter.Extension.Exchange.ExternalServices.Exchange
public override string Name => "Exchange External Service";
public override string Description => "Integrate from a wide variety of cryptocurrency exchanges";
public override string ViewPartial => "ViewExchangeExternalService";
protected override string ControllerName => "Exchange";
public override string ControllerName => "Exchange";
public ExchangeService() : base()
@ -25,17 +25,17 @@ namespace BtcTransmuter.Extension.Exchange.ExternalServices.Exchange
{
}
public static IExchangeAPI[] GetAvailableExchanges()
public static async Task<IExchangeAPI[]> GetAvailableExchanges()
{
return ExchangeAPI.GetExchangeAPIs();
return await ExchangeAPI.GetExchangeAPIsAsync();
}
public ExchangeAPI ConstructClient()
public async Task<ExchangeAPI> ConstructClient()
{
var data = GetData();
var result = ExchangeAPI.GetExchangeAPI(data.ExchangeName);
var result = await ExchangeAPI.GetExchangeAPIAsync(data.ExchangeName);
if (result is ExchangeAPI api)
{
if (!string.IsNullOrEmpty(data.OverrideUrl))
@ -52,7 +52,7 @@ namespace BtcTransmuter.Extension.Exchange.ExternalServices.Exchange
public async Task<bool> TestAccess()
{
var client = ConstructClient();
var client = await ConstructClient();
if (client == null)
{
return false;

View File

@ -52,11 +52,11 @@ namespace BtcTransmuter.Extension.NBXplorer.HostedServices
await Task.WhenAll(exchangeExternalServices.Select(async data =>
{
var exchangeService = new ExchangeService(data);
var client = exchangeService.ConstructClient();
var client = await exchangeService.ConstructClient();
var amounts = await client.GetAmountsAsync();
foreach (var keyValuePair in amounts)
{
await _triggerDispatcher.DispatchTrigger(new CheckExchangeBalanceTrigger()
_ = _triggerDispatcher.DispatchTrigger(new CheckExchangeBalanceTrigger()
{
Data = new CheckExchangeBalanceTriggerData()
{

View File

@ -1,4 +1,5 @@
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Threading.Tasks;
using BtcTransmuter.Abstractions.ExternalServices;
using BtcTransmuter.Abstractions.Recipes;
@ -30,6 +31,18 @@ namespace BtcTransmuter.Extension.Exchange.Triggers.CheckExchangeBalance
_externalServiceManager = externalServiceManager;
}
[HttpGet("symbols/{externalServiceId}")]
public async Task<string[]> GetAvailableMarketSymbols(string externalServiceId)
{
var serviceData =
await _externalServiceManager.GetExternalServiceData(externalServiceId, GetUserId());
var exchangeService = new ExchangeService(serviceData);
var symbols = await (await exchangeService.ConstructClient()).GetCurrenciesAsync();
return symbols.Keys.ToArray();
}
protected override async Task<CheckExchangeBalanceViewModel> BuildViewModel(RecipeTrigger data)
{
var innerData = data.Get<CheckExchangeBalanceTriggerParameters>();

View File

@ -15,7 +15,7 @@ namespace BtcTransmuter.Extension.Exchange.Triggers.CheckExchangeBalance
"Trigger a recipe by checking if the balance of an asset on an exchange ";
public override string ViewPartial => "ViewCheckExchangeBalanceTrigger";
protected override string ControllerName => "CheckExchangeBalance";
public override string ControllerName => "CheckExchangeBalance";
protected override Task<bool> IsTriggered(ITrigger trigger, RecipeTrigger recipeTrigger,

View File

@ -22,7 +22,7 @@
</div>
<div class="form-group">
<label asp-for="Asset" class="control-label"></label>
<input asp-for="Asset" class="form-control"/>
<input asp-for="Asset" class="form-control autocomplete" data-datasrc="availableMarketSymbols"/>
<span asp-validation-for="Asset" class="text-danger"></span>
</div>
<div class="form-group">
@ -41,4 +41,31 @@
<button type="submit" class="btn btn-primary">Save</button>
<a asp-action="EditRecipe" asp-controller="Recipes" class="btn btn-secondary" asp-route-id="@Model.RecipeId">Back to recipe</a>
</div>
</form>
</form>
<script>
var actionUrlMapping = @Json.Serialize(Model.ExternalServices.ToDictionary(item => item.Value, item => @Url.Action("GetAvailableMarketSymbols", new { ExternalServiceId = item.Value })));;
var availableMarketSymbols = [];
$(document).ready(function(){
$("#ExternalServiceId").on("input", populateAvailableMarketSymbols);
function populateAvailableMarketSymbols(){
var value = $("#ExternalServiceId").val();
if(!value){
availableMarketSymbols = [];
}else{
$.ajax({
url: actionUrlMapping[value],
success: function(response){
availableMarketSymbols = response;
},
error: function(){
availableMarketSymbols = [];
}
});
}
}
populateAvailableMarketSymbols();
})
</script>

View File

@ -22,12 +22,12 @@
</div>
<div class="form-group">
<label asp-for="Asset" class="control-label"></label>
<input asp-for="Asset" class="form-control"/>
<input asp-for="Asset" class="form-control autocomplete" data-datasrc="availableMarketSymbols" />
<span asp-validation-for="Asset" class="text-danger"></span>
</div>
<input type="hidden" asp-for="RecipeId"/>
<input type="hidden" asp-for="RecipeId" />
<div class="mt-2">
<button type="submit" class="btn btn-primary">Save</button>
<a asp-action="EditRecipe" asp-controller="Recipes" class="btn btn-secondary" asp-route-id="@Model.RecipeId">Back to recipe</a>
@ -35,6 +35,33 @@
</form>
@await Component.InvokeAsync("RecipeActionFooter", new
{
recipeId = @Model.RecipeId,
recipeId = @Model.RecipeId,
recipeActionIdInGroupBeforeThisOne = @Model.RecipeActionIdInGroupBeforeThisOne
})
})
<script>
var actionUrlMapping = @Json.Serialize(Model.ExternalServices.ToDictionary(item => item.Value, item => @Url.Action("GetAvailableMarketSymbols", new { ExternalServiceId = item.Value })));;
var availableMarketSymbols = [];
$(document).ready(function(){
$("#ExternalServiceId").on("input", populateAvailableMarketSymbols);
function populateAvailableMarketSymbols(){
var value = $("#ExternalServiceId").val();
if(!value){
availableMarketSymbols = [];
}else{
$.ajax({
url: actionUrlMapping[value],
success: function(response){
availableMarketSymbols = response;
},
error: function(){
availableMarketSymbols = [];
}
});
}
}
populateAvailableMarketSymbols();
})
</script>

Some files were not shown because too many files have changed in this diff Show More