Compare commits

...

252 Commits

Author SHA1 Message Date
muzam1l
04e2274dbf some refactoring 2022-04-06 14:42:39 +05:30
muzam1l
842b8a5226 Set debug stream default account from transaction account 2022-04-04 21:29:08 +05:30
Vaclav Barta
234832138f fixes #155 2022-04-01 08:42:10 +02:00
Valtteri Karesto
28d94a1475 Merge pull request #152 from eqlabs/fix/cloud-upload-button
Fix/cloud upload button
2022-03-31 10:15:31 +03:00
Valtteri Karesto
594aee6cd2 Merge pull request #154 from eqlabs/feat/add-header-templates
Feat/add header templates
2022-03-30 12:31:08 +03:00
Valtteri Karesto
d75910972f Change order 2022-03-30 12:09:16 +03:00
Valtteri Karesto
589c604a12 Add header files as hard coded 2022-03-30 12:00:27 +03:00
Valtteri Karesto
8394a11705 Merge pull request #151 from eqlabs/fix/disable-delete-hook
Fixes issue #148
2022-03-29 23:34:12 +03:00
Valtteri Karesto
4ad329882c Add tooltips to navigation items 2022-03-29 16:52:48 +03:00
Valtteri Karesto
ee86b91e82 Add tooltip component 2022-03-29 16:52:39 +03:00
Valtteri Karesto
d2addf782e Add extra isDisabled style for button 2022-03-29 16:52:24 +03:00
Valtteri Karesto
51f7bd509b Add radix tooltip 2022-03-29 16:52:02 +03:00
Valtteri Karesto
e064251ff9 Merge pull request #150 from eqlabs/feat/fix-newlines
Fixes issue #86 again, regressed in #107 for Firefox
2022-03-29 15:29:51 +03:00
Valtteri Karesto
5aeed7c246 Few more changes to deleteHook function 2022-03-29 15:23:30 +03:00
Valtteri Karesto
8d03edc299 Fixes issue #148 2022-03-29 14:03:06 +03:00
Valtteri Karesto
95022ef121 Fixes issue #86 again, regressed in #107 for Firefox 2022-03-29 13:55:10 +03:00
Valtteri Karesto
4519906b78 hotfix/remove-console-log 2022-03-28 15:35:27 +03:00
Valtteri Karesto
88a47c49a4 Merge pull request #149 from eqlabs/fix/fix-hook-params
Convert hook params to hex blobs
2022-03-28 15:33:23 +03:00
Valtteri Karesto
1ab03f9bed Fix types 2022-03-28 15:22:10 +03:00
Valtteri Karesto
84ff667135 Convert hook params to hex blobs 2022-03-28 15:20:14 +03:00
Valtteri Karesto
0d10e782f3 Ask faucets account only once if you dont have one 2022-03-25 11:35:23 +02:00
Valtteri Karesto
84e6763495 hotfix/keep-accounts 2022-03-25 10:49:29 +02:00
Vaclav Barta
7ffcfd694d Feature/new hook doc (#144)
* added hooks-param-buf-len doc file

* showing help for hooks-param-buf-len

* added hooks-param-set-buf-len
2022-03-25 09:23:53 +01:00
Valtteri Karesto
77e4917d38 Merge pull request #145 from eqlabs/hotfix/fix-accounts
Fix problem with accounts
2022-03-25 10:21:47 +02:00
Valtteri Karesto
e4238a40cc Fix problem with accounts 2022-03-25 10:18:55 +02:00
Valtteri Karesto
42c0b20512 Merge pull request #136 from eqlabs/feat/hooks-v2-preparations
Feat/hooks v2 preparations
2022-03-25 09:48:19 +02:00
Valtteri Karesto
43154ff6d8 Updated hooks templates 2022-03-25 09:41:29 +02:00
Valtteri Karesto
8197b510f9 delete hook and delete account features, #140 #47 2022-03-24 22:18:59 +02:00
Valtteri Karesto
fc7652f48e Remove logging 2022-03-24 21:22:27 +02:00
Valtteri Karesto
bd32555617 improve toast message 2022-03-24 21:21:35 +02:00
Valtteri Karesto
fc6f420e1e Add funds feature added 2022-03-24 21:20:29 +02:00
Valtteri Karesto
d3c36765de Fix select option focus color 2022-03-24 17:36:31 +02:00
Valtteri Karesto
2628a12673 Merge branch 'main' of github.com:eqlabs/xrpl-hooks-ide into feat/hooks-v2-preparations 2022-03-24 17:32:10 +02:00
Valtteri Karesto
f6c1869b5d Bring back styled logs after debug round 2022-03-24 17:31:49 +02:00
Valtteri Karesto
62c8b4f217 Merge pull request #130 from eqlabs/fix/do-not-allow-special-chars
Prevent special characters on filename
2022-03-24 16:54:59 +02:00
Valtteri Karesto
8798e5a233 Comment out code temporarily 2022-03-24 16:09:05 +02:00
Valtteri Karesto
5f7d42843c temporarily print raw debug messages 2022-03-24 16:07:02 +02:00
Valtteri Karesto
302b36dde8 Give new ids to account requests 2022-03-24 16:06:37 +02:00
Valtteri Karesto
3e7c7b1969 Updated regex 2022-03-24 14:49:30 +02:00
Valtteri Karesto
936bbc503a Updated refetch time to 5 seconds 2022-03-24 14:40:58 +02:00
Valtteri Karesto
81890c8833 Added loading state to set hook modal 2022-03-23 13:28:59 +02:00
Valtteri Karesto
50fa20c39a Fix styling of the text 2022-03-23 12:41:14 +02:00
Valtteri Karesto
11f2cffc87 Loosen up filters a bit 2022-03-23 09:52:57 +02:00
Valtteri Karesto
bbd1d162f0 Fix styling issues 2022-03-22 17:36:10 +02:00
Valtteri Karesto
b301a860bf Fix overflow problems and other styling issues 2022-03-22 17:08:38 +02:00
Valtteri Karesto
ff697b96ea reverse hookon calculation logic 2022-03-22 17:08:19 +02:00
Valtteri Karesto
48e9898e31 Add smarter filtering for debugstream 2022-03-22 17:08:00 +02:00
Valtteri Karesto
2e25242ebe Update urls 2022-03-22 17:07:41 +02:00
Valtteri Karesto
e32e07f7fd Added xor 2022-03-22 17:07:00 +02:00
Valtteri Karesto
0d2a17008e Update envs 2022-03-22 17:06:50 +02:00
Valtteri Karesto
a87b3de6c4 Add few more fixes and styling to modals 2022-03-17 19:10:27 +02:00
Valtteri Karesto
23068ff477 Style select again to support multiselect 2022-03-17 19:10:02 +02:00
Vaclav Barta
a12a5dfbac Merge pull request #138 from eqlabs/bugfix/timeago-init
alternative TimeAgo setup - terminal problems unfortunately persist, but at least the warning is gone...
2022-03-17 14:08:20 +01:00
Vaclav Barta
5a598cb091 alternative TimeAgo setup 2022-03-17 08:50:40 +01:00
Valtteri Karesto
be39054a2f Minor updates to debugstream, related to v2 2022-03-16 19:01:13 +02:00
Valtteri Karesto
0add65dd1c Update hook deployment logic for v2 2022-03-16 19:01:01 +02:00
Valtteri Karesto
82170ad4f8 update set hook functionality 2022-03-16 19:00:30 +02:00
Valtteri Karesto
af49426eb0 Move fonts to correct location 2022-03-16 19:00:15 +02:00
Valtteri Karesto
48a86e3386 Fix problems with middleware 2022-03-16 18:59:48 +02:00
Valtteri Karesto
c82c35b5a1 Use new env variables 2022-03-16 18:59:37 +02:00
Valtteri Karesto
f849be1f80 export variables 2022-03-16 18:59:21 +02:00
Valtteri Karesto
694d07fa0e Add react hook form and new env variables 2022-03-16 18:59:11 +02:00
Valtteri Karesto
b9aa3e2adc Merge branch 'main' of github.com:eqlabs/xrpl-hooks-ide into feat/hooks-v2-preparations 2022-03-14 14:37:12 +02:00
Valtteri Karesto
5b573b2379 Merge pull request #129 from eqlabs/fix/sample-list-logic
Fix modal showing up bug, issue #99
2022-03-14 14:36:57 +02:00
Valtteri Karesto
23538b1502 Fix navigation 2022-03-11 16:08:27 +02:00
Valtteri Karesto
723602ebdc Make some v2 hooks api preparations 2022-03-11 16:08:16 +02:00
Valtteri Karesto
f8fdeaf9ce Update dependencies 2022-03-11 16:07:01 +02:00
Vaclav Barta
e75b971718 Merge pull request #133 from eqlabs/update-hook-doc
closes #131
2022-03-11 08:03:19 +01:00
Vaclav Barta
11a35a5932 added link 2022-03-10 10:48:56 +01:00
Vaclav Barta
611f875761 upgraded links to https://xrpl-hooks.readme.io/v2.0/, added more 2022-03-10 10:15:20 +01:00
Vaclav Barta
a7df50c194 updated docs 2022-03-09 16:41:28 +01:00
Valtteri Karesto
0c6c60ed29 Removed unused variable 2022-03-09 16:26:10 +02:00
Vaclav Barta
e82662647f started updating docs 2022-03-09 15:25:42 +01:00
Valtteri Karesto
5490e7205a Improved the filename validation 2022-03-09 16:24:23 +02:00
Valtteri Karesto
d8e218392a Merge pull request #132 from eqlabs/feat/add-docs-about-hover-messages
Improve readme
2022-03-09 16:23:47 +02:00
Valtteri Karesto
723722df58 Merge pull request #123 from eqlabs/fix/increment-test-sequence-number
Increment sequence on every transaction
2022-03-09 15:21:53 +02:00
Valtteri Karesto
2ff85ede06 Improve readme 2022-03-09 15:19:59 +02:00
Valtteri Karesto
052a1e5b60 Prevent special characters on filename 2022-03-09 15:01:46 +02:00
Valtteri Karesto
7f8f47cb14 Fix modal showing up bug, issue #99 2022-03-09 13:27:42 +02:00
Valtteri Karesto
ddb043c104 Merge pull request #128 from eqlabs/feat/fix-account-button
Fixes issue #101
2022-03-09 12:50:50 +02:00
Valtteri Karesto
d2ad6537d7 Remove console logs 2022-03-09 12:48:53 +02:00
Valtteri Karesto
8f004ee4da Fixes issue #101 2022-03-09 12:29:20 +02:00
Valtteri Karesto
b90bf67c20 Merge pull request #120 from eqlabs/feat/add-hooks-docs
Feat/add hooks docs
2022-03-09 12:06:06 +02:00
Valtteri Karesto
746112e637 fixes issue #115 2022-03-09 11:57:06 +02:00
Valtteri Karesto
13bfd42093 Decorations should now work correctly 2022-03-09 11:14:43 +02:00
Valtteri Karesto
c1f7d7d51c Make sure not all files are defined as C files 2022-03-09 11:14:14 +02:00
muzamil
38a097a8f9 Merge pull request #108 from eqlabs/feat/debug-prettify
Debug stream improvements.
2022-03-08 21:07:13 +05:30
muzam1l
db0ffe999e fix line wrap and timestamp font 2022-03-08 19:18:51 +05:30
Jani Anttonen
6d88c4e546 Merge pull request #100 from eqlabs/feat/persist-splits
Persist splits
2022-03-08 14:15:49 +02:00
Valtteri Karesto
09f58f18ae Increment sequence on every transaction 2022-03-08 13:16:45 +02:00
Valtteri Karesto
e11ddaffb0 Remove html from md now that the paragraph styling works 2022-03-08 11:36:43 +02:00
Valtteri Karesto
2e88f568b8 Add imports 2022-03-08 11:24:14 +02:00
Valtteri Karesto
237d504f17 Add better styling for markdown 2022-03-08 11:24:02 +02:00
Valtteri Karesto
197fc09e1d delete unused files 2022-03-08 11:23:51 +02:00
Valtteri Karesto
2c74a93aee Add better logic for markdown files 2022-03-08 11:22:58 +02:00
Valtteri Karesto
5209644780 Add raw-loader 2022-03-08 11:22:39 +02:00
Valtteri Karesto
ed37427da8 Add new markdown files 2022-03-08 09:56:07 +02:00
Valtteri Karesto
daee9de96c Update md script and readme 2022-03-08 09:55:40 +02:00
Valtteri Karesto
e4b10d12c2 Remove unused script 2022-03-08 09:54:47 +02:00
Valtteri Karesto
64eabb4502 Remove unused dependencies 2022-03-08 09:54:21 +02:00
Valtteri Karesto
12a24d3d86 Remove rst files 2022-03-08 09:54:11 +02:00
muzam1l
ce91182c7b fix and segrregate debug stream state. 2022-03-07 17:14:53 +05:30
muzam1l
2e3a0e557e Some vertical margin in enhanced logs 2022-03-04 20:37:07 +05:30
Valtteri Karesto
395e02343b Add logic to enrich the hover messages with rst file contents 2022-03-04 12:48:03 +02:00
Valtteri Karesto
3682dd4946 Add readme how to use the script 2022-03-04 12:38:39 +02:00
Valtteri Karesto
3070ed706e Add script that converts rst to json which contains md 2022-03-04 12:38:24 +02:00
Valtteri Karesto
4b73687779 Add lodash.uniqby 2022-03-04 12:37:51 +02:00
muzam1l
6b9a9ef978 better socket error mesage 2022-03-04 15:36:48 +05:30
muzam1l
bc5bb5be39 Fix timesatmp in logs 2022-03-04 15:22:05 +05:30
muzam1l
0fe83811b9 Clickable accounts in logs and formatting fixes 2022-03-04 15:13:56 +05:30
muzam1l
c6359aa853 Add error code to close event message 2022-03-04 14:19:23 +05:30
muzam1l
c9c818c8f3 json data is now collapsible! 2022-03-03 21:03:28 +05:30
muzam1l
c521246393 debug stream state as full global 2022-03-03 16:36:42 +05:30
muzam1l
8936b34361 separate messages for debug stream error and close evensts 2022-03-03 16:16:00 +05:30
muzam1l
5993d2762f Separately format time, json and message of debug stream log. 2022-03-01 21:36:24 +05:30
muzamil
0a44b5b5d1 Merge pull request #94 from eqlabs/feat/wasm-stats
Compiled wasm file stats
2022-03-01 15:14:14 +05:30
JaniAnttonen
810d3b2524 Add a todo 2022-02-25 16:34:09 +02:00
JaniAnttonen
a3393ded1e Fix build 2022-02-25 15:22:09 +02:00
JaniAnttonen
17ede265b1 Remove debug logging 2022-02-25 14:39:06 +02:00
JaniAnttonen
629070edad Save split state 2022-02-25 13:50:56 +02:00
Vaclav Barta
cc83924c27 Merge pull request #98 from eqlabs/bugfix/typos
fix for #97
2022-02-15 08:49:38 +01:00
Vaclav Barta
e3e964f72a fix for #97 2022-02-11 13:54:52 +01:00
muzam
0def1d30a6 compiled wasm file stats 2022-02-09 19:06:29 +05:30
Joni Juup
bdb2c0cf8f Merge pull request #93 from eqlabs/bugfix/monaco-popups
Fix monaco popup issues
2022-02-09 15:10:28 +02:00
muzamil
3e8dbc9793 Merge pull request #91 from eqlabs/fixes
Clearing some issues
2022-02-09 18:32:47 +05:30
muzam
d735cd3833 Merge branch 'main' into fixes 2022-02-09 18:31:35 +05:30
Joni Juup
eddf228283 more specific rule for the box itself 2022-02-09 13:14:17 +02:00
Joni Juup
f9d617efdc more specific css rule for hr 2022-02-09 13:09:36 +02:00
Joni Juup
43796021da fix monaco editor issues with popups 2022-02-09 13:04:41 +02:00
Joni Juup
241c21782d Merge pull request #89 from eqlabs/enhancement/small-ui-fixes
new XRPL Hooks logo
clicking on the logo won't open the template dialog (retain gist path)
clicking gist under the Hook name opens the gist in a new window
documentation button open documentation in a new window
added meta tags & favicons
adjusted color schemes, removed pink (now matches design)
2022-02-09 12:42:18 +02:00
muzam
1a3f5d144c change accept template to starter 2022-02-09 14:26:19 +05:30
Joni Juup
66fb68d52e update share image 2022-02-08 18:58:59 +02:00
Joni Juup
aaeb32a576 update favicons 2022-02-08 18:29:12 +02:00
Joni Juup
78b5dcceb6 adjusting colors and themes 2022-02-08 18:07:37 +02:00
muzam
ce81c11c29 Update hooks installed info in card 2022-02-08 19:56:30 +05:30
muzam
3a98b95e3d fix firewall template link 2022-02-08 18:59:40 +05:30
muzam
8bc36655e2 disallow illegal characters in filename 2022-02-08 15:26:22 +05:30
muzam
b6ab536a60 Clear log on compile 2022-02-08 15:08:38 +05:30
Joni Juup
37a3d2b207 add share image 2022-02-07 17:10:00 +02:00
Joni Juup
cd0c5f8a0d meta tag changes 2022-02-07 16:54:31 +02:00
Vaclav Barta
8dde89fa9a Merge pull request #88 from eqlabs/feature/optimization
remove hardcoded file compilation options
2022-02-07 15:35:33 +01:00
Joni Juup
ca3d60cfb8 small ui fixes 2022-02-07 16:23:39 +02:00
Vaclav Barta
1a4d53cfbc removed hardcoded file compilation options 2022-02-07 15:03:47 +01:00
Joni Juup
94e126782b Merge pull request #65 from eqlabs/feature/panel-resize
Added panel resizing to all views.
2022-02-02 15:41:07 +02:00
Joni Juup
cc03c64f0a added a fixed height for logbox header to logbox content box height can be calculated easily 2022-02-02 15:03:31 +02:00
Joni Juup
3647aa6274 adjusted gutter sizes and highlight style 2022-02-02 12:39:08 +02:00
Joni Juup
a2a58f0ba9 light mode support 2022-02-02 12:15:58 +02:00
Joni Juup
c544a03be4 fixed log overflow, resize sizing 2022-02-02 12:12:07 +02:00
Joni Juup
9a09da88ec add panel resizing to views 2022-02-01 16:44:51 +02:00
Joni Juup
5850551906 fixed merge conflicts 2022-02-01 15:47:24 +02:00
muzam
e35e520d24 minor fix 2022-02-01 19:07:29 +05:30
muzamil
8077fc5865 Merge pull request #66 from eqlabs/feat/tabs
Transaction tabs
2022-02-01 19:02:26 +05:30
muzam
bff01b4a9f Merge branch 'main' into feat/tabs 2022-02-01 19:00:46 +05:30
muzamil
de5380d6f3 Merge pull request #67 from eqlabs/feat/debug-stream
Per account debug stream
2022-02-01 18:51:20 +05:30
muzamil
eda2a9596a Merge pull request #52 from eqlabs/transactions
Implemented Transactions!
2022-02-01 18:49:33 +05:30
muzam
195d33b1db Merge branch 'test-page' into transactions 2022-02-01 18:46:10 +05:30
muzam
4f042ef3b7 File prefixed logs 2022-02-01 18:42:19 +05:30
muzam
17c67822a9 First draft of debug stream 2022-02-01 17:05:53 +05:30
muzam
e6f613ae0b Fix tabs overflow 2022-02-01 13:53:18 +05:30
muzam
9b822cfda4 Resuable tabs component and transaction tabs 2022-01-31 20:27:49 +05:30
muzam
b5b918d877 minor changes 2022-01-31 18:55:15 +05:30
Vaclav Barta
739918647d fixed tail match 2022-01-20 10:37:24 +01:00
Vaclav Barta
1f334d6253 proposed fix for #59 2022-01-20 10:18:44 +01:00
muzam
0f15a85c45 Added additional tx type 2022-01-18 14:41:11 +05:30
muzam
0c4330e329 Support json fields and better error handling 2022-01-12 14:51:02 +05:30
muzam
a9676288ea implement reset transaction state 2022-01-11 20:20:39 +05:30
muzam
7354474c70 Implemented transactions ❤️‍🔥 2022-01-11 20:16:44 +05:30
muzam
ce5b307a8b Implement send transaction, payment works, yaay. 2022-01-10 15:21:59 +05:30
muzam
b28bcfdd0a Merge branch 'main' into test-page 2022-01-05 16:32:30 +05:30
muzam
7f06876e3e Test page UI layout 2022-01-05 16:32:07 +05:30
muzamil
fd479d8671 Merge pull request #41 from eqlabs/feat/templates
Fetch templates and header files according to selection.
2022-01-04 15:32:10 +05:30
Valtteri Karesto
938b567256 Merge pull request #46 from eqlabs/feat/patch-ripple-binary-codec
Feat/patch ripple binary codec
2021-12-23 09:08:38 +02:00
Valtteri Karesto
779f5aab0a Fixed typos 2021-12-22 16:14:35 +02:00
Valtteri Karesto
02194d8a98 Remove logs 2021-12-22 16:08:02 +02:00
Valtteri Karesto
5677fe34dc Add comments to state 2021-12-22 16:07:45 +02:00
Valtteri Karesto
895da89325 Add new version of ripple-binary-codec patch 2021-12-22 16:07:21 +02:00
Valtteri Karesto
b138cc8d5b Update readme 2021-12-22 16:06:52 +02:00
muzam
027b2c8ed4 remove console log 2021-12-21 17:07:45 +05:30
Valtteri Karesto
d85cc71817 Merge pull request #43 from eqlabs/feat/temporary-fix-for-editor
Roll back file paths for now
2021-12-21 11:18:58 +02:00
Valtteri Karesto
bac3522078 Roll back file paths for now 2021-12-21 11:07:52 +02:00
Vaclav Barta
b2c6aa7871 Merge pull request #42 from eqlabs/feature/workspace-location
fix for issue #39
2021-12-20 13:38:46 +01:00
Vaclav Barta
81e2a3673d fix for issue #39 2021-12-20 13:02:42 +01:00
muzam
b4ca360661 Implement read-only editors for some headers. 2021-12-16 22:30:03 +05:30
muzam
ad947be0bc Only fetch extra headers on template files. 2021-12-16 18:52:16 +05:30
muzam
f739d4da34 Fetch templates and header files according to selection. 2021-12-16 18:26:58 +05:30
Valtteri Karesto
fdb1eb01a4 Merge pull request #34 from eqlabs/feat/zip
Implemented download as zip.
2021-12-15 15:02:23 +02:00
Valtteri Karesto
920d359966 Merge pull request #38 from eqlabs/feat/controlled-dialog
Filename Dialog fixes and improvements.
2021-12-15 13:16:49 +02:00
muzam
9e1dbc8765 minor fixes 2021-12-15 15:49:55 +05:30
muzam
10ea77fd8d Error and loading states in download as zip. 2021-12-15 15:38:19 +05:30
muzam
50de7ebf15 Merge branch 'feat/controlled-dialog' into feat/zip 2021-12-14 22:19:05 +05:30
muzam
7db07e3f92 Merge branch 'main' into feat/controlled-dialog 2021-12-14 21:29:31 +05:30
Valtteri Karesto
6ad7c67672 hotfix: fixes faucet url 2021-12-14 16:36:37 +02:00
Valtteri Karesto
10f279a6b4 Merge pull request #33 from eqlabs/feat/add-language-client
Add example of language server
2021-12-14 16:17:16 +02:00
muzam
792c093cfd Input filename validation and default extension. 2021-12-14 16:43:43 +05:30
muzam
a11a641608 New file dialog confirms on pressing Enter. 2021-12-14 15:33:39 +05:30
Valtteri Karesto
c3bf31d993 Fetch accounts only on client side 2021-12-13 23:06:13 +02:00
Valtteri Karesto
67d1b72331 fix eslintrc.json 2021-12-13 23:04:11 +02:00
Valtteri Karesto
35bc89cf99 Few cleanups to code 2021-12-13 23:02:03 +02:00
Valtteri Karesto
380e196db2 Added quick comments about code 2021-12-13 22:54:57 +02:00
Valtteri Karesto
d67613c0cf Add disabled state to button if no compiled code 2021-12-13 22:27:21 +02:00
Valtteri Karesto
4d4b96bede Extract actions to separate files 2021-12-13 22:23:37 +02:00
Valtteri Karesto
59637e32fe Add a lot of functionality to state 2021-12-13 17:26:37 +02:00
Valtteri Karesto
82d0c8c5ff Update navigation logic 2021-12-13 17:26:25 +02:00
Valtteri Karesto
b41ee2198b Separate editors for deploy and develop 2021-12-13 17:26:13 +02:00
Valtteri Karesto
09c5aff1da Add deploy footer 2021-12-13 17:25:58 +02:00
Valtteri Karesto
d806a46f13 Add account component 2021-12-13 17:25:49 +02:00
Valtteri Karesto
6bcbb5d6df Add patterns 2021-12-13 17:23:45 +02:00
Valtteri Karesto
0d7a4aae10 Remove footer 2021-12-13 17:23:36 +02:00
Valtteri Karesto
276dfff2ba Add link and log components 2021-12-13 17:23:09 +02:00
Valtteri Karesto
eddb870f85 Update deploy and develope pages 2021-12-13 17:22:51 +02:00
Valtteri Karesto
3707a215bb Add custom faucet api page to prevent cors problems 2021-12-13 17:22:36 +02:00
Valtteri Karesto
fad5e13430 Do not show modal on sign-in page 2021-12-13 17:22:18 +02:00
Valtteri Karesto
df47158f29 Some fixes to button 2021-12-13 17:21:59 +02:00
Valtteri Karesto
51e4fed345 Forgot to add yarn lock 2021-12-13 17:21:47 +02:00
Valtteri Karesto
e471e8d7ef Add colors to theme 2021-12-13 17:21:40 +02:00
Valtteri Karesto
adc268c3cd Added some env info to readme 2021-12-13 17:21:28 +02:00
Valtteri Karesto
69e08abbc9 Add postinstall patch script 2021-12-13 17:21:09 +02:00
Valtteri Karesto
b8596ec7ce Add some lint settings 2021-12-13 17:20:35 +02:00
Valtteri Karesto
4fc7098e78 Fix styles 2021-12-13 17:16:43 +02:00
Valtteri Karesto
69c7865491 Added some helper utils 2021-12-13 17:16:34 +02:00
muzam
8ac7e82221 Implemented download as zip 2021-12-13 16:21:28 +05:30
Valtteri Karesto
5eea51744e Monkeypatch ripple-binary-code to support SetHook 2021-12-07 15:31:10 +02:00
Valtteri Karesto
dcf0598852 Update file uris 2021-11-29 10:58:04 +02:00
Valtteri Karesto
a7d04a28e4 Add example of language server 2021-11-27 00:06:26 +02:00
Valtteri Karesto
a0303ecfa4 Merge pull request #32 from eqlabs/feat/fix-scrollbars
Fix scrollbars
2021-11-26 15:23:13 +02:00
Valtteri Karesto
5a79e07c2d Fix scrollbars 2021-11-26 15:20:30 +02:00
Valtteri Karesto
1107bb8196 Merge pull request #31 from eqlabs/feat/add-gist-integration
Feat/add gist integration
2021-11-26 00:11:24 +02:00
Valtteri Karesto
405aafed7e Removed unused code 2021-11-26 00:02:39 +02:00
Valtteri Karesto
03156474f3 Move build button and add loading states 2021-11-25 23:55:45 +02:00
Valtteri Karesto
7982209732 Move deploy button to footer 2021-11-25 23:55:21 +02:00
Valtteri Karesto
0f9963b972 Refactor global navigation and editor navigation 2021-11-25 23:55:12 +02:00
Valtteri Karesto
650243279a Add sync to gist issue #19 feature and other fixes to global state 2021-11-25 23:54:47 +02:00
Valtteri Karesto
6f183049d5 Add some safeguards to data and new button size 2021-11-25 23:54:03 +02:00
Valtteri Karesto
48706effc1 Style tweaks to button component styles 2021-11-25 23:32:09 +02:00
Valtteri Karesto
c9740b1e8a Minor tweaks to dropdown menu 2021-11-25 23:31:49 +02:00
Valtteri Karesto
166300b8d5 Custom alert dialog page 2021-11-25 23:31:33 +02:00
Valtteri Karesto
99968855e0 Add alert dialog from radix 2021-11-25 23:31:24 +02:00
Valtteri Karesto
a62a9c3700 Separate login page so we maintain the app state, related to issue #17 2021-11-25 23:31:10 +02:00
Valtteri Karesto
4e971ce119 Conditional chaining to map 2021-11-25 22:02:02 +02:00
Valtteri Karesto
72ba2072ec Uppercase variant for Heading 2021-11-25 22:01:27 +02:00
Valtteri Karesto
bd8d3c39c2 New size for input component 2021-11-25 22:01:13 +02:00
Valtteri Karesto
7142f5b5e2 Added buttongroup component 2021-11-25 22:00:56 +02:00
Valtteri Karesto
37516c602d Add some dependencies and theme style change 2021-11-25 17:30:11 +02:00
Valtteri Karesto
cfa7a3bd30 update dialog styles 2021-11-25 17:29:52 +02:00
Valtteri Karesto
266e4c3e6c Create panel box component 2021-11-25 17:29:40 +02:00
Valtteri Karesto
4cf6d376f0 Update themechanger styles 2021-11-25 17:29:31 +02:00
Valtteri Karesto
b2f49625db Add truncate string util 2021-11-25 17:29:20 +02:00
Valtteri Karesto
0b9fd172ce Add patterns to modal 2021-11-25 17:29:08 +02:00
Valtteri Karesto
2582981d85 Open modal if no gist id or files 2021-11-25 17:28:41 +02:00
Valtteri Karesto
a0eda59982 Added some tsconfigs 2021-11-25 17:28:26 +02:00
Valtteri Karesto
9ae9db984d Renamed pages 2021-11-25 17:28:15 +02:00
Valtteri Karesto
8f2c78b08b Merge pull request #30 from eqlabs/feat/button-loading-state
Feat/button loading state
2021-11-20 01:37:26 +02:00
145 changed files with 12086 additions and 2109 deletions

View File

@@ -1,3 +1,9 @@
NEXTAUTH_URL=https://example.com
GITHUB_SECRET=""
GITHUB_ID=""
GITHUB_ID=""
NEXT_PUBLIC_COMPILE_API_ENDPOINT="http://localhost:9000/api/build"
NEXT_PUBLIC_LANGUAGE_SERVER_API_ENDPOINT="ws://localhost:9000/language-server/c"
NEXT_PUBLIC_TESTNET_URL="hooks-testnet-v2.xrpl-labs.com"
NEXT_PUBLIC_DEBUG_STREAM_URL="hooks-testnet-v2-debugstream.xrpl-labs.com"
NEXT_PUBLIC_EXPLORER_URL="hooks-testnet-v2-explorer.xrpl-labs.com"
NEXT_PUBLIC_SITE_URL=http://localhost:3000

1
.prettierignore Normal file
View File

@@ -0,0 +1 @@
*.md

View File

@@ -8,7 +8,9 @@ This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next
## Getting Started
First, run the development server:
First, copy the `.env.example` to `.env.local` file, someone from the team can provide you your enviroment variables.
Then, run the development server:
```bash
npm run dev
@@ -24,6 +26,78 @@ You can start editing the page by modifying `pages/index.tsx`. The page auto-upd
The `pages/api` directory is mapped to `/api/*`. Files in this directory are treated as [API routes](https://nextjs.org/docs/api-routes/introduction) instead of React pages.
## Github Login
If you want to use your Github app to provide login, here's the guide to do that.
- First go to https://github.com/settings/profile -> Developer Settings -> OAuth Apps
- Click "New OAuth App" button
- Give application some name eg. Xrpl Hooks Development
- Give some homepage url eg. localhost:3000
- Give some optional description (these values will show up on the popup when you login)
- Authorization callback URL should be http://localhost:3000/api/auth/callback (if you're creating the app for local development)
- Click register application
- Then a page should open up where you can get client id and client secret values. Copy paste those to .env.local to use them:
```
GITHUB_SECRET="client-secret-here"
GITHUB_ID="client-id-here"
```
Login should now work through your own Github OAuth app.
## Styling and Theming
This project uses Stitches (https://stitches.dev) for theming and styling the components. You should be quite familiar with the API if you have used for example styled-components earlier. Stitches should provide better performance, near zero runtime.
For components we try to use Radix-UI (https://www.radix-ui.com/) as much as possible. It may not provide all the necessary components so you're free to use other components/libraries if those makes sense. For colors we're using Radix-UI Colors (https://radix-ui.com/colors).
Theme file can be found under `./stitches.config.ts` file. When you're creating new components remeber to import `styled` from that file and not `@stitches` directly. That way it will provide the correct theme for you automatically.
Example:
```tsx
// Use our stitches.config instead of @stitches/react
import { styled } from "../stitches.config";
const Box = styled("div", {
boxSizing: "border-box",
});
export default Box;
```
Custom components can be found from `./components` folder.
## Monaco Editor
Project is relying on Monaco editor heavily. Instead of using Monaco editor directly we're using `@monaco-editor/react` which provides little helpers to use Monaco editor.
On the Develop page we're using following loader for Monaco editor:
```js
loader.config({
paths: {
vs: "https://cdn.jsdelivr.net/npm/monaco-editor@0.30.1/min/vs",
},
});
```
By default `@monaco-editor/react` was using 0.29.? version of Monaco editor. @codingame/monaco-languageclient library (connects to clangd language server) was using API methods that were introduced in Monaco Editor 0.30 so that's why we're loading certain version of it.
Monaco Languageclient related stuff is found from `./utils/languageClient.ts`. Basically we're connecting the editor to clangd language server which lives on separate backend. That project can be found from https://github.com/eqlabs/xrpl-hooks-compiler/. If you need access to that project ask permissions from @vbar (Vaclav Barta) on GitHub.
### Language server hover messages
If you want to extend hover messages provided by language-server you can add extra docs to `xrpl-hooks-docs/md/` folder. Just make sure the filename is matching with the error code that comes from language server. So lets say you want to add extra documentation for `hooks-func-addr-taken` check create new file called `hooks-func-addr-taken.md` and then remember to import and export it on `docs.ts` file with same logic as the other files.
## Global state management
Global state management is handled with library called Valtio (https://github.com/pmndrs/valtio). Initial state can be found from `./state/index.ts` file. All the actions which updates the state is found under `./state/actions/` folder.
## Special notes
Since we are dealing with greenfield tech and one of the dependencies (ripple-binary-codec) doesn't yet support signing `SetHook` transactions we had to monkey patch the library with patch-package (https://www.npmjs.com/package/patch-package). We modified the definitions.json file of the ripple-binary-codec library and then ran `yarn patch-package ripple-binary-codec` which created `patches/ripple-binary-codec+1.2.0.patch` file to this project. This file contains the modifications to `ripple-binary-codec` library. package.json contains postinstall hook which runs patch-package, and it will add the patch on the file mentioned earlier. This happens automatically after running patch package.
## Learn More
To learn more about Next.js, take a look at the following resources:
@@ -32,9 +106,3 @@ To learn more about Next.js, take a look at the following resources:
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome!
## Deploy on Vercel
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details.

535
components/Accounts.tsx Normal file
View File

@@ -0,0 +1,535 @@
import toast from "react-hot-toast";
import { useSnapshot } from "valtio";
import { ArrowSquareOut, Copy, Trash, Wallet, X } from "phosphor-react";
import React, { useEffect, useState, FC } from "react";
import Dinero from "dinero.js";
import Button from "./Button";
import { addFaucetAccount, importAccount } from "../state/actions";
import state from "../state";
import Box from "./Box";
import { Container, Heading, Stack, Text, Flex } from ".";
import {
Dialog,
DialogContent,
DialogTitle,
DialogDescription,
DialogClose,
DialogTrigger,
} from "./Dialog";
import { css } from "../stitches.config";
import { Input } from "./Input";
import truncate from "../utils/truncate";
const labelStyle = css({
color: "$mauve10",
textTransform: "uppercase",
fontSize: "10px",
mb: "$0.5",
});
import transactionsData from "../content/transactions.json";
import { SetHookDialog } from "./SetHookDialog";
import { addFunds } from "../state/actions/addFaucetAccount";
import { deleteHook } from "../state/actions/deployHook";
export const AccountDialog = ({
activeAccountAddress,
setActiveAccountAddress,
}: {
activeAccountAddress: string | null;
setActiveAccountAddress: React.Dispatch<React.SetStateAction<string | null>>;
}) => {
const snap = useSnapshot(state);
const [showSecret, setShowSecret] = useState(false);
const activeAccount = snap.accounts.find(
(account) => account.address === activeAccountAddress
);
return (
<Dialog
open={Boolean(activeAccountAddress)}
onOpenChange={(open) => {
setShowSecret(false);
!open && setActiveAccountAddress(null);
}}
>
<DialogContent
css={{
backgroundColor: "$mauve1 !important",
border: "1px solid $mauve2",
".dark &": {
// backgroundColor: "$black !important",
},
p: "$3",
"&:before": {
content: " ",
position: "absolute",
top: 0,
right: 0,
bottom: 0,
left: 0,
opacity: 0.2,
".dark &": {
opacity: 1,
},
zIndex: 0,
pointerEvents: "none",
backgroundImage: `url('/pattern-dark.svg'), url('/pattern-dark-2.svg')`,
backgroundRepeat: "no-repeat",
backgroundPosition: "bottom left, top right",
},
}}
>
<DialogTitle
css={{
display: "flex",
width: "100%",
alignItems: "center",
borderBottom: "1px solid $mauve6",
pb: "$3",
gap: "$3",
fontSize: "$md",
}}
>
<Wallet size="15px" /> {activeAccount?.name}
<DialogClose asChild>
<Button
size="xs"
outline
css={{ ml: "auto", mr: "$9" }}
tabIndex={-1}
onClick={() => {
const index = state.accounts.findIndex(
(acc) => acc.address === activeAccount?.address
);
state.accounts.splice(index, 1);
}}
>
Delete Account <Trash size="15px" />
</Button>
</DialogClose>
</DialogTitle>
<DialogDescription as="div" css={{ fontFamily: "$monospace" }}>
<Stack css={{ display: "flex", flexDirection: "column", gap: "$3" }}>
<Flex css={{ alignItems: "center" }}>
<Flex css={{ flexDirection: "column" }}>
<Text className={labelStyle()}>Account Address</Text>
<Text
css={{
fontFamily: "$monospace",
}}
>
{activeAccount?.address}
</Text>
</Flex>
<Flex css={{ marginLeft: "auto", color: "$mauve12" }}>
<Button
size="sm"
ghost
css={{ mt: "$3" }}
onClick={() => {
navigator.clipboard.writeText(activeAccount?.address || "");
toast.success("Copied address to clipboard");
}}
>
<Copy size="15px" />
</Button>
</Flex>
</Flex>
<Flex css={{ alignItems: "center" }}>
<Flex css={{ flexDirection: "column" }}>
<Text className={labelStyle()}>Secret</Text>
<Text
as="div"
css={{
fontFamily: "$monospace",
display: "flex",
alignItems: "center",
}}
>
{showSecret
? activeAccount?.secret
: "•".repeat(activeAccount?.secret.length || 16)}{" "}
<Button
css={{
fontFamily: "$monospace",
lineHeight: 2,
mt: "2px",
ml: "$3",
}}
ghost
size="xs"
onClick={() => setShowSecret((curr) => !curr)}
>
{showSecret ? "Hide" : "Show"}
</Button>
</Text>
</Flex>
<Flex css={{ marginLeft: "auto", color: "$mauve12" }}>
<Button
size="sm"
ghost
onClick={() => {
navigator.clipboard.writeText(activeAccount?.secret || "");
toast.success("Copied secret to clipboard");
}}
css={{ mt: "$3" }}
>
<Copy size="15px" />
</Button>
</Flex>
</Flex>
<Flex css={{ alignItems: "center" }}>
<Flex css={{ flexDirection: "column" }}>
<Text className={labelStyle()}>Balances & Objects</Text>
<Text
css={{
fontFamily: "$monospace",
display: "flex",
alignItems: "center",
}}
>
{Dinero({
amount: Number(activeAccount?.xrp || "0"),
precision: 6,
})
.toUnit()
.toLocaleString(undefined, {
style: "currency",
currency: "XRP",
currencyDisplay: "name",
})}
<Button
css={{
fontFamily: "$monospace",
lineHeight: 2,
mt: "2px",
ml: "$3",
}}
ghost
size="xs"
onClick={() => {
addFunds(activeAccount?.address || "");
}}
>
Add Funds
</Button>
</Text>
</Flex>
<Flex css={{ marginLeft: "auto" }}>
<a
href={`https://${process.env.NEXT_PUBLIC_EXPLORER_URL}/${activeAccount?.address}`}
target="_blank"
rel="noreferrer noopener"
>
<Button
size="sm"
ghost
css={{ color: "$grass11 !important", mt: "$3" }}
>
<ArrowSquareOut size="15px" />
</Button>
</a>
</Flex>
</Flex>
<Flex css={{ alignItems: "center" }}>
<Flex css={{ flexDirection: "column" }}>
<Text className={labelStyle()}>Installed Hooks</Text>
<Text
css={{
fontFamily: "$monospace",
}}
>
{activeAccount && activeAccount.hooks.length > 0
? activeAccount.hooks.map((i) => truncate(i, 12)).join(",")
: ""}
</Text>
</Flex>
{activeAccount && activeAccount?.hooks?.length > 0 && (
<Flex css={{ marginLeft: "auto" }}>
<Button
size="xs"
outline
disabled={activeAccount.isLoading}
css={{ mt: "$3", mr: "$1", ml: "auto" }}
onClick={() => {
deleteHook(activeAccount);
}}
>
Delete Hook <Trash size="15px" />
</Button>
</Flex>
)}
</Flex>
</Stack>
</DialogDescription>
<DialogClose asChild>
<Box css={{ position: "absolute", top: "$3", right: "$3" }}>
<X size="20px" />
</Box>
</DialogClose>
</DialogContent>
</Dialog>
);
};
interface AccountProps {
card?: boolean;
hideDeployBtn?: boolean;
showHookStats?: boolean;
}
const Accounts: FC<AccountProps> = (props) => {
const snap = useSnapshot(state);
const [activeAccountAddress, setActiveAccountAddress] = useState<
string | null
>(null);
useEffect(() => {
const fetchAccInfo = async () => {
if (snap.clientStatus === "online") {
const requests = snap.accounts.map((acc) =>
snap.client?.send({
id: `hooks-builder-req-info-${acc.address}`,
command: "account_info",
account: acc.address,
})
);
const responses = await Promise.all(requests);
responses.forEach((res: any) => {
const address = res?.account_data?.Account as string;
const balance = res?.account_data?.Balance as string;
const sequence = res?.account_data?.Sequence as number;
const accountToUpdate = state.accounts.find(
(acc) => acc.address === address
);
if (accountToUpdate) {
accountToUpdate.xrp = balance;
accountToUpdate.sequence = sequence;
}
});
const objectRequests = snap.accounts.map((acc) => {
return snap.client?.send({
id: `hooks-builder-req-objects-${acc.address}`,
command: "account_objects",
account: acc.address,
});
});
const objectResponses = await Promise.all(objectRequests);
objectResponses.forEach((res: any) => {
const address = res?.account as string;
const accountToUpdate = state.accounts.find(
(acc) => acc.address === address
);
if (accountToUpdate) {
accountToUpdate.hooks =
res.account_objects
.find((ac: any) => ac?.LedgerEntryType === "Hook")
?.Hooks?.map((oo: any) => oo.Hook.HookHash) || [];
}
});
}
};
let fetchAccountInfoInterval: NodeJS.Timer;
if (snap.clientStatus === "online") {
fetchAccInfo();
fetchAccountInfoInterval = setInterval(() => fetchAccInfo(), 10000);
}
return () => {
if (snap.accounts.length > 0) {
if (fetchAccountInfoInterval) {
clearInterval(fetchAccountInfoInterval);
}
}
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [snap.accounts, snap.clientStatus]);
return (
<Box
as="div"
css={{
display: "flex",
backgroundColor: props.card ? "$deep" : "$mauve1",
position: "relative",
flex: "1",
height: "100%",
border: "1px solid $mauve6",
borderRadius: props.card ? "$md" : undefined,
}}
>
<Container css={{ p: 0, flexShrink: 1, height: "100%" }}>
<Flex
css={{
py: "$3",
borderBottom: props.card ? "1px solid $mauve6" : undefined,
}}
>
<Heading
as="h3"
css={{
fontWeight: 300,
m: 0,
fontSize: "11px",
color: "$mauve12",
px: "$3",
textTransform: "uppercase",
alignItems: "center",
display: "inline-flex",
gap: "$3",
}}
>
<Wallet size="15px" /> <Text css={{ lineHeight: 1 }}>Accounts</Text>
</Heading>
<Flex css={{ ml: "auto", gap: "$3", marginRight: "$3" }}>
<Button ghost size="sm" onClick={() => addFaucetAccount(true)}>
Create
</Button>
<ImportAccountDialog />
</Flex>
</Flex>
<Stack
css={{
flexDirection: "column",
width: "100%",
fontSize: "13px",
wordWrap: "break-word",
fontWeight: "$body",
gap: 0,
height: "calc(100% - 52px)",
flexWrap: "nowrap",
overflowY: "auto",
}}
>
{snap.accounts.map((account) => (
<Flex
column
key={account.address + account.name}
onClick={() => setActiveAccountAddress(account.address)}
css={{
px: "$3",
py: props.card ? "$3" : "$2",
cursor: "pointer",
borderBottom: props.card ? "1px solid $mauve6" : undefined,
"@hover": {
"&:hover": {
background: "$backgroundAlt",
},
},
}}
>
<Flex
row
css={{
justifyContent: "space-between",
}}
>
<Box>
<Text>{account.name} </Text>
<Text
css={{
color: "$textMuted",
wordBreak: "break-word",
}}
>
{account.address} (
{Dinero({
amount: Number(account?.xrp || "0"),
precision: 6,
})
.toUnit()
.toLocaleString(undefined, {
style: "currency",
currency: "XRP",
currencyDisplay: "name",
})}
)
</Text>
</Box>
{!props.hideDeployBtn && (
<div
className="hook-deploy-button"
onClick={(e) => {
e.stopPropagation();
}}
>
<SetHookDialog account={account} />
</div>
)}
</Flex>
{props.showHookStats && (
<Text muted small css={{ mt: "$2" }}>
{account.hooks.length} hook
{account.hooks.length === 1 ? "" : "s"} installed
</Text>
)}
</Flex>
))}
</Stack>
</Container>
<AccountDialog
activeAccountAddress={activeAccountAddress}
setActiveAccountAddress={setActiveAccountAddress}
/>
</Box>
);
};
export const transactionsOptions = transactionsData.map((tx) => ({
value: tx.TransactionType,
label: tx.TransactionType,
}));
const ImportAccountDialog = () => {
const [value, setValue] = useState("");
return (
<Dialog>
<DialogTrigger asChild>
<Button ghost size="sm">
Import
</Button>
</DialogTrigger>
<DialogContent>
<DialogTitle>Import account</DialogTitle>
<DialogDescription>
<label>Add account secret</label>
<Input
name="secret"
type="password"
value={value}
onChange={(e) => setValue(e.target.value)}
/>
</DialogDescription>
<Flex
css={{
marginTop: 25,
justifyContent: "flex-end",
gap: "$3",
}}
>
<DialogClose asChild>
<Button outline>Cancel</Button>
</DialogClose>
<DialogClose asChild>
<Button
variant="primary"
onClick={() => {
importAccount(value);
setValue("");
}}
>
Import account
</Button>
</DialogClose>
</Flex>
<DialogClose asChild>
<Box css={{ position: "absolute", top: "$3", right: "$3" }}>
<X size="20px" />
</Box>
</DialogClose>
</DialogContent>
</Dialog>
);
};
export default Accounts;

View File

@@ -0,0 +1,88 @@
import React from "react";
import { blackA } from "@radix-ui/colors";
import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog";
import { styled, keyframes } from "../stitches.config";
const overlayShow = keyframes({
"0%": { opacity: 0 },
"100%": { opacity: 1 },
});
const contentShow = keyframes({
"0%": { opacity: 0, transform: "translate(-50%, -48%) scale(.96)" },
"100%": { opacity: 1, transform: "translate(-50%, -50%) scale(1)" },
});
const StyledOverlay = styled(AlertDialogPrimitive.Overlay, {
zIndex: 1000,
backgroundColor: blackA.blackA9,
position: "fixed",
inset: 0,
"@media (prefers-reduced-motion: no-preference)": {
animation: `${overlayShow} 150ms cubic-bezier(0.16, 1, 0.3, 1)`,
},
".dark &": {
backgroundColor: blackA.blackA11,
},
});
const Root: React.FC<AlertDialogPrimitive.AlertDialogProps> = ({
children,
...rest
}) => {
return (
<AlertDialogPrimitive.Root {...rest}>
<StyledOverlay />
{children}
</AlertDialogPrimitive.Root>
);
};
const StyledContent = styled(AlertDialogPrimitive.Content, {
zIndex: 1000,
backgroundColor: "$mauve2",
color: "$mauve12",
borderRadius: "$md",
boxShadow:
"0px 10px 38px -5px rgba(22, 23, 24, 0.25), 0px 10px 20px -5px rgba(22, 23, 24, 0.2)",
position: "fixed",
top: "50%",
left: "50%",
transform: "translate(-50%, -50%)",
width: "90vw",
maxWidth: "450px",
maxHeight: "85vh",
padding: 25,
"@media (prefers-reduced-motion: no-preference)": {
animation: `${contentShow} 150ms cubic-bezier(0.16, 1, 0.3, 1)`,
},
"&:focus": { outline: "none" },
".dark &": {
backgroundColor: "$mauve5",
boxShadow:
"0px 10px 38px 0px rgba(0, 0, 0, 0.85), 0px 10px 20px 0px rgba(0, 0, 0, 0.6)",
},
});
const StyledTitle = styled(AlertDialogPrimitive.Title, {
margin: 0,
color: "$mauve12",
fontWeight: 500,
fontSize: "$lg",
});
const StyledDescription = styled(AlertDialogPrimitive.Description, {
marginBottom: 20,
color: "$mauve11",
lineHeight: 1.5,
fontSize: "$sm",
});
// Exports
export const AlertDialog = Root;
export const AlertDialogTrigger = AlertDialogPrimitive.Trigger;
export const AlertDialogContent = StyledContent;
export const AlertDialogTitle = StyledTitle;
export const AlertDialogDescription = StyledDescription;
export const AlertDialogAction = AlertDialogPrimitive.Action;
export const AlertDialogCancel = AlertDialogPrimitive.Cancel;

View File

@@ -1,13 +1,12 @@
import { CSS } from "@stitches/react/types/css-util";
import { StyledComponent } from "@stitches/react/types/styled-component";
import React from "react";
import { styled } from "../stitches.config";
import Flex from "./Flex";
import Spinner from "./Spinner";
const Button = styled("button", {
export const StyledButton = styled("button", {
// Reset
all: "unset",
position: "relative",
appereance: "none",
fontFamily: "$body",
alignItems: "center",
@@ -32,8 +31,8 @@ const Button = styled("button", {
fontSize: "$2",
fontWeight: 500,
fontVariantNumeric: "tabular-nums",
backgroundColor: "red",
cursor: "pointer",
width: "max-content",
"&:disabled": {
opacity: 0.6,
pointerEvents: "none",
@@ -41,6 +40,12 @@ const Button = styled("button", {
},
variants: {
size: {
xs: {
borderRadius: "$sm",
height: "$5",
px: "$2",
fontSize: "$xs",
},
sm: {
borderRadius: "$sm",
height: "$7",
@@ -61,6 +66,12 @@ const Button = styled("button", {
},
},
variant: {
link: {
textDecoration: "underline",
fontSize: "inherit",
color: "$textMuted",
textUnderlineOffset: "2px",
},
default: {
backgroundColor: "$mauve12",
boxShadow: "inset 0 0 0 1px $colors$mauve12",
@@ -77,7 +88,7 @@ const Button = styled("button", {
},
"&:focus": {
boxShadow:
"inset 0 0 0 1px $colors$mauve12, 0 0 0 1px $colors$mauve12",
"inset 0 0 0 1px $colors$mauve12, inset 0 0 0 2px $colors$mauve12",
},
'&[data-radix-popover-trigger][data-state="open"], &[data-radix-dropdown-menu-trigger][data-state="open"]':
{
@@ -86,28 +97,89 @@ const Button = styled("button", {
},
},
primary: {
backgroundColor: `$pink9`,
boxShadow: "inset 0 0 0 1px $colors$pink9",
backgroundColor: `$accent`,
boxShadow: "inset 0 0 0 1px $colors$purple9",
color: "$white",
"@hover": {
"&:hover": {
backgroundColor: "$pink10",
boxShadow: "inset 0 0 0 1px $colors$pink11",
backgroundColor: "$purple10",
boxShadow: "inset 0 0 0 1px $colors$purple11",
},
},
"&:active": {
backgroundColor: "$pink8",
boxShadow: "inset 0 0 0 1px $colors$pink8",
backgroundColor: "$purple8",
boxShadow: "inset 0 0 0 1px $colors$purple8",
},
"&:focus": {
boxShadow: "inset 0 0 0 1px $colors$pink8",
boxShadow: "inset 0 0 0 2px $colors$purple12",
},
'&[data-radix-popover-trigger][data-state="open"], &[data-radix-dropdown-menu-trigger][data-state="open"]':
{
backgroundColor: "$mauve4",
boxShadow: "inset 0 0 0 1px $colors$pink8",
boxShadow: "inset 0 0 0 1px $colors$purple8",
},
},
secondary: {
backgroundColor: `$purple9`,
boxShadow: "inset 0 0 0 1px $colors$purple9",
color: "$white",
"@hover": {
"&:hover": {
backgroundColor: "$purple10",
boxShadow: "inset 0 0 0 1px $colors$purple11",
},
},
"&:active": {
backgroundColor: "$purple8",
boxShadow: "inset 0 0 0 1px $colors$purple8",
},
"&:focus": {
boxShadow: "inset 0 0 0 2px $colors$purple12",
},
'&[data-radix-popover-trigger][data-state="open"], &[data-radix-dropdown-menu-trigger][data-state="open"]':
{
backgroundColor: "$mauve4",
boxShadow: "inset 0 0 0 1px $colors$purple8",
},
},
destroy: {
backgroundColor: `$red9`,
boxShadow: "inset 0 0 0 1px $colors$red9",
color: "$white",
"@hover": {
"&:hover": {
backgroundColor: "$red10",
boxShadow: "inset 0 0 0 1px $colors$red11",
},
},
"&:active": {
backgroundColor: "$red8",
boxShadow: "inset 0 0 0 1px $colors$red8",
},
"&:focus": {
boxShadow: "inset 0 0 0 2px $colors$red12",
},
'&[data-radix-popover-trigger][data-state="open"], &[data-radix-dropdown-menu-trigger][data-state="open"]':
{
backgroundColor: "$mauve4",
boxShadow: "inset 0 0 0 1px $colors$red8",
},
},
},
muted: {
true: {
color: "$textMuted",
},
},
isDisabled: {
true: {
opacity: 0.6,
// pointerEvents: "none",
cursor: "auto",
"&:hover": {
boxShadow: "inherit",
},
},
},
outline: {
true: {
@@ -119,6 +191,11 @@ const Button = styled("button", {
textTransform: "uppercase",
},
},
fullWidth: {
true: {
width: "100%",
},
},
ghost: {
true: {
boxShadow: "none",
@@ -174,6 +251,18 @@ const Button = styled("button", {
},
},
},
{
outline: true,
variant: "secondary",
css: {
background: "transparent",
color: "$mauve12",
"&:hover": {
color: "$mauve12",
background: "$mauve5",
},
},
},
],
defaultVariants: {
size: "md",
@@ -182,10 +271,10 @@ const Button = styled("button", {
});
const CustomButton: React.FC<
React.ComponentProps<typeof Button> & { as?: string }
React.ComponentProps<typeof StyledButton> & { as?: string }
> = React.forwardRef(({ children, as = "button", ...rest }, ref) => (
// @ts-expect-error
<Button {...rest} ref={ref} as={as}>
<StyledButton {...rest} ref={ref} as={as}>
<Flex
as="span"
css={{ gap: "$2", alignItems: "center" }}
@@ -194,7 +283,7 @@ const CustomButton: React.FC<
{children}
</Flex>
{rest.isLoading && <Spinner css={{ position: "absolute" }} />}
</Button>
</StyledButton>
));
CustomButton.displayName = "CustomButton";

View File

@@ -0,0 +1,29 @@
import { styled } from "../stitches.config";
import { StyledButton } from "./Button";
const ButtonGroup = styled("div", {
display: "flex",
marginLeft: "1px",
[`& ${StyledButton}`]: {
marginLeft: "-1px",
px: "$4",
zIndex: 2,
position: "relative",
"&:hover, &:focus": {
zIndex: 200,
},
},
[`& ${StyledButton}:not(:only-of-type):not(:first-child):not(:last-child)`]: {
borderRadius: 0,
},
[`& ${StyledButton}:first-child:not(:only-of-type)`]: {
borderBottomRightRadius: 0,
borderTopRightRadius: 0,
},
[`& ${StyledButton}:last-child:not(:only-of-type)`]: {
borderBottomLeftRadius: 0,
borderTopLeftRadius: 0,
},
});
export default ButtonGroup;

147
components/DebugStream.tsx Normal file
View File

@@ -0,0 +1,147 @@
import { useCallback, useEffect } from "react";
import { proxy, ref, useSnapshot } from "valtio";
import { Select } from ".";
import state, { ILog } from "../state";
import { extractJSON } from "../utils/json";
import LogBox from "./LogBox";
interface ISelect<T = string> {
label: string;
value: T;
}
export const streamState = proxy({
selectedAccount: null as ISelect | null,
logs: [] as ILog[],
socket: undefined as WebSocket | undefined,
});
const DebugStream = () => {
const { selectedAccount, logs, socket } = useSnapshot(streamState);
const { accounts } = useSnapshot(state);
const accountOptions = accounts.map((acc) => ({
label: acc.name,
value: acc.address,
}));
const renderNav = () => (
<>
<Select
instanceId="DSAccount"
placeholder="Select account"
options={accountOptions}
hideSelectedOptions
value={selectedAccount}
onChange={(acc) => (streamState.selectedAccount = acc as any)}
css={{ width: "100%" }}
/>
</>
);
const prepareLog = useCallback((str: any): ILog => {
if (typeof str !== "string") throw Error("Unrecognized debug log stream!");
const match = str.match(/([\s\S]+(?:UTC|ISO|GMT[+|-]\d+))\ ?([\s\S]*)/m);
const [_, tm, msg] = match || [];
const extracted = extractJSON(msg);
const timestamp = isNaN(Date.parse(tm || ""))
? tm
: new Date(tm).toLocaleTimeString();
const message = !extracted
? msg
: msg.slice(0, extracted.start) + msg.slice(extracted.end + 1);
const jsonData = extracted
? JSON.stringify(extracted.result, null, 2)
: undefined;
return {
type: "log",
message,
timestamp,
jsonData,
defaultCollapsed: true,
};
}, []);
useEffect(() => {
const account = selectedAccount?.value;
if (account && (!socket || !socket.url.endsWith(account))) {
socket?.close();
streamState.socket = ref(
new WebSocket(
`wss://${process.env.NEXT_PUBLIC_DEBUG_STREAM_URL}/${account}`
)
);
} else if (!account && socket) {
socket.close();
streamState.socket = undefined;
}
}, [selectedAccount?.value, socket]);
useEffect(() => {
const account = selectedAccount?.value;
const socket = streamState.socket;
if (!socket) return;
const onOpen = () => {
streamState.logs = [];
streamState.logs.push({
type: "success",
message: `Debug stream opened for account ${account}`,
});
};
const onError = () => {
streamState.logs.push({
type: "error",
message: "Something went wrong! Check your connection and try again.",
});
};
const onClose = (e: CloseEvent) => {
streamState.logs.push({
type: "error",
message: `Connection was closed. [code: ${e.code}]`,
});
streamState.selectedAccount = null;
};
const onMessage = (event: any) => {
if (!event.data) return;
const log = prepareLog(event.data);
// Filter out account_info and account_objects requests
try {
const parsed = JSON.parse(log.jsonData);
if (parsed?.id?._Request?.includes("hooks-builder-req")) {
return;
}
} catch (err) {
// Lets just skip if we cannot parse the message
}
return streamState.logs.push(log);
};
socket.addEventListener("open", onOpen);
socket.addEventListener("close", onClose);
socket.addEventListener("error", onError);
socket.addEventListener("message", onMessage);
return () => {
socket.removeEventListener("open", onOpen);
socket.removeEventListener("close", onClose);
socket.removeEventListener("message", onMessage);
socket.removeEventListener("error", onError);
};
}, [prepareLog, selectedAccount?.value, socket]);
return (
<LogBox
enhanced
renderNav={renderNav}
title="Debug stream"
logs={logs}
clearLog={() => (streamState.logs = [])}
/>
);
};
export default DebugStream;

151
components/DeployEditor.tsx Normal file
View File

@@ -0,0 +1,151 @@
import React, { useRef, useState } from "react";
import { useSnapshot, ref } from "valtio";
import Editor, { loader } from "@monaco-editor/react";
import type monaco from "monaco-editor";
import { useTheme } from "next-themes";
import { useRouter } from "next/router";
import NextLink from "next/link";
import ReactTimeAgo from "react-time-ago";
import filesize from "filesize";
import Box from "./Box";
import Container from "./Container";
import dark from "../theme/editor/amy.json";
import light from "../theme/editor/xcode_default.json";
import state from "../state";
import wat from "../utils/wat-highlight";
import EditorNavigation from "./EditorNavigation";
import { Button, Text, Link, Flex } from ".";
loader.config({
paths: {
vs: "https://cdn.jsdelivr.net/npm/monaco-editor@0.30.1/min/vs",
},
});
const FILESIZE_BREAKPOINTS: [number, number] = [2 * 1024, 5 * 1024];
const DeployEditor = () => {
const editorRef = useRef<monaco.editor.IStandaloneCodeEditor>();
const snap = useSnapshot(state);
const router = useRouter();
const { theme } = useTheme();
const [showContent, setShowContent] = useState(false);
const activeFile = snap.files[snap.active];
const compiledSize = activeFile?.compiledContent?.byteLength || 0;
const color =
compiledSize > FILESIZE_BREAKPOINTS[1]
? "$error"
: compiledSize > FILESIZE_BREAKPOINTS[0]
? "$warning"
: "$success";
const CompiledStatView = activeFile && (
<Flex
column
align="center"
css={{
fontSize: "$sm",
fontFamily: "$monospace",
textAlign: "center",
}}
>
<Flex row align="center">
<Text css={{ mr: "$1" }}>
Compiled {activeFile.name.split(".")[0] + ".wasm"}
</Text>
{activeFile?.lastCompiled && (
<ReactTimeAgo date={activeFile.lastCompiled} locale="en-US" />
)}
{activeFile.compiledContent?.byteLength && (
<Text css={{ ml: "$2", color }}>
({filesize(activeFile.compiledContent.byteLength)})
</Text>
)}
</Flex>
<Button variant="link" onClick={() => setShowContent(true)}>
View as WAT-file
</Button>
</Flex>
);
const NoContentView = !snap.loading && router.isReady && (
<Text
css={{
mt: "-60px",
fontSize: "$sm",
fontFamily: "$monospace",
maxWidth: "300px",
textAlign: "center",
}}
>
{`You haven't compiled any files yet, compile files on `}
<NextLink shallow href={`/develop/${router.query.slug}`} passHref>
<Link as="a">develop view</Link>
</NextLink>
</Text>
);
const isContent =
snap.files?.filter((file) => file.compiledWatContent).length > 0 &&
router.isReady;
return (
<Box
css={{
flex: 1,
display: "flex",
position: "relative",
flexDirection: "column",
backgroundColor: "$mauve2",
width: "100%",
}}
>
<EditorNavigation showWat />
<Container
css={{
display: "flex",
flex: 1,
justifyContent: "center",
alignItems: "center",
}}
>
{!isContent ? (
NoContentView
) : !showContent ? (
CompiledStatView
) : (
<Editor
className="hooks-editor"
defaultLanguage={"wat"}
language={"wat"}
path={`file://tmp/c/${snap.files?.[snap.active]?.name}.wat`}
value={snap.files?.[snap.active]?.compiledWatContent || ""}
beforeMount={(monaco) => {
monaco.languages.register({ id: "wat" });
monaco.languages.setLanguageConfiguration("wat", wat.config);
monaco.languages.setMonarchTokensProvider("wat", wat.tokens);
if (!state.editorCtx) {
state.editorCtx = ref(monaco.editor);
// @ts-expect-error
monaco.editor.defineTheme("dark", dark);
// @ts-expect-error
monaco.editor.defineTheme("light", light);
}
}}
onMount={(editor, monaco) => {
editorRef.current = editor;
editor.updateOptions({
glyphMargin: true,
readOnly: true,
});
}}
theme={theme === "dark" ? "dark" : "light"}
/>
)}
</Container>
</Box>
);
};
export default DeployEditor;

103
components/DeployFooter.tsx Normal file
View File

@@ -0,0 +1,103 @@
import React, { useRef, useLayoutEffect } from "react";
import { useSnapshot } from "valtio";
import { Play, Prohibit } from "phosphor-react";
import useStayScrolled from "react-stay-scrolled";
import Container from "./Container";
import Box from "./Box";
import LogText from "./LogText";
import { compileCode } from "../state/actions";
import state from "../state";
import Button from "./Button";
import Heading from "./Heading";
const Footer = () => {
const snap = useSnapshot(state);
const logRef = useRef<HTMLPreElement>(null);
const { stayScrolled /*, scrollBottom*/ } = useStayScrolled(logRef);
useLayoutEffect(() => {
stayScrolled();
}, [snap.logs, stayScrolled]);
return (
<Box
as="footer"
css={{
display: "flex",
borderTop: "1px solid $mauve6",
background: "$mauve1",
position: "relative",
}}
>
<Container css={{ py: "$3", flexShrink: 1 }}>
<Heading
as="h3"
css={{ fontWeight: 300, m: 0, fontSize: "11px", color: "$mauve9" }}
>
DEVELOPMENT LOG
</Heading>
<Button
ghost
size="xs"
css={{
position: "absolute",
right: "$3",
top: "$2",
color: "$mauve10",
}}
onClick={() => {
state.logs = [];
}}
>
<Prohibit size="14px" />
</Button>
<Box
as="pre"
ref={logRef}
css={{
display: "flex",
flexDirection: "column",
width: "100%",
height: "160px",
fontSize: "13px",
fontWeight: "$body",
fontFamily: "$monospace",
overflowY: "auto",
wordWrap: "break-word",
py: 3,
}}
>
{snap.logs?.map((log, index) => (
<Box as="span" key={log.type + index}>
<LogText capitalize variant={log.type}>
{log.type}:{" "}
</LogText>
<LogText>{log.message}</LogText>
</Box>
))}
</Box>
<Button
variant="primary"
uppercase
disabled={!snap.files.length}
isLoading={snap.compiling}
onClick={() => compileCode(snap.active)}
css={{
position: "absolute",
bottom: "$4",
left: "$4",
alignItems: "center",
display: "flex",
cursor: "pointer",
}}
>
<Play weight="bold" size="16px" />
Compile to Wasm
</Button>
</Container>
</Box>
);
};
export default Footer;

View File

@@ -1,29 +1,36 @@
import React from "react";
import * as Stiches from "@stitches/react";
import { keyframes } from "@stitches/react";
import { violet, blackA, mauve, whiteA } from "@radix-ui/colors";
import { blackA } from "@radix-ui/colors";
import * as DialogPrimitive from "@radix-ui/react-dialog";
import { styled } from "../stitches.config";
const overlayShow = keyframes({
"0%": { opacity: 0 },
"0%": { opacity: 0.01 },
"100%": { opacity: 1 },
});
const contentShow = keyframes({
"0%": { opacity: 0, transform: "translate(-50%, -48%) scale(.96)" },
"100%": { opacity: 1, transform: "translate(-50%, -50%) scale(1)" },
"0%": { opacity: 0.01 },
"100%": { opacity: 1 },
});
const StyledOverlay = styled(DialogPrimitive.Overlay, {
zIndex: 1000,
zIndex: 9999,
backgroundColor: blackA.blackA9,
position: "fixed",
inset: 0,
top: 0,
left: 0,
right: 0,
bottom: 0,
display: "grid",
placeItems: "center",
overflowY: "auto",
"@media (prefers-reduced-motion: no-preference)": {
animation: `${overlayShow} 150ms cubic-bezier(0.16, 1, 0.3, 1)`,
animation: `${overlayShow} 250ms cubic-bezier(0.16, 1, 0.3, 1)`,
},
".dark &": {
backgroundColor: blackA.blackA9,
backgroundColor: blackA.blackA11,
},
});
@@ -32,15 +39,12 @@ const StyledContent = styled(DialogPrimitive.Content, {
backgroundColor: "$mauve2",
color: "$mauve12",
borderRadius: "$md",
position: "relative",
boxShadow:
"0px 10px 38px -5px rgba(22, 23, 24, 0.25), 0px 10px 20px -5px rgba(22, 23, 24, 0.2)",
position: "fixed",
top: "50%",
left: "50%",
transform: "translate(-50%, -50%)",
width: "90vw",
maxWidth: "450px",
maxHeight: "85vh",
// maxHeight: "85vh",
padding: 25,
"@media (prefers-reduced-motion: no-preference)": {
animation: `${contentShow} 150ms cubic-bezier(0.16, 1, 0.3, 1)`,
@@ -49,16 +53,15 @@ const StyledContent = styled(DialogPrimitive.Content, {
".dark &": {
backgroundColor: "$mauve5",
boxShadow:
"0px 10px 38px 0px rgba(22, 23, 24, 0.85), 0px 10px 20px 0px rgba(22, 23, 24, 0.6)",
"0px 10px 38px 0px rgba(0, 0, 0, 0.85), 0px 10px 20px 0px rgba(0, 0, 0, 0.6)",
},
});
const Content: React.FC<{ css?: Stiches.CSS }> = ({ css, children }) => {
return (
<div>
<StyledOverlay />
<StyledOverlay>
<StyledContent css={css}>{children}</StyledContent>
</div>
</StyledOverlay>
);
};
@@ -70,16 +73,17 @@ const StyledTitle = styled(DialogPrimitive.Title, {
});
const StyledDescription = styled(DialogPrimitive.Description, {
margin: "10px 0 20px",
margin: "10px 0 10px",
color: "$mauve11",
fontSize: 15,
lineHeight: 1.5,
});
// Exports
export const Dialog = DialogPrimitive.Root;
export const Dialog = styled(DialogPrimitive.Root);
export const DialogTrigger = DialogPrimitive.Trigger;
export const DialogContent = Content;
export const DialogTitle = StyledTitle;
export const DialogDescription = StyledDescription;
export const DialogClose = DialogPrimitive.Close;
export const DialogPortal = DialogPrimitive.Portal;

View File

@@ -61,6 +61,8 @@ const itemStyles = {
position: "relative",
paddingLeft: "5px",
userSelect: "none",
py: "$0.5",
pr: "$2",
gap: "$2",
"&[data-disabled]": {
@@ -69,7 +71,7 @@ const itemStyles = {
},
"&:focus": {
backgroundColor: "$pink9",
backgroundColor: "$purple9",
color: "$white",
},
};
@@ -83,8 +85,8 @@ const StyledRadioItem = styled(DropdownMenuPrimitive.RadioItem, {
});
const StyledTriggerItem = styled(DropdownMenuPrimitive.TriggerItem, {
'&[data-state="open"]': {
backgroundColor: "$pink9",
color: "$pink9",
backgroundColor: "$purple9",
color: "$purple9",
},
...itemStyles,
});
@@ -113,6 +115,9 @@ const StyledItemIndicator = styled(DropdownMenuPrimitive.ItemIndicator, {
const StyledArrow = styled(DropdownMenuPrimitive.Arrow, {
fill: "$mauve2",
".dark &": {
fill: "$mauve5",
},
});
// Exports

View File

@@ -1,8 +1,39 @@
import React, { useRef, useState } from "react";
import { Plus, Share, DownloadSimple, Gear, X } from "phosphor-react";
import { useTheme } from "next-themes";
import React, { useState, useEffect, useCallback } from "react";
import {
Plus,
Share,
DownloadSimple,
Gear,
X,
GithubLogo,
SignOut,
ArrowSquareOut,
CloudArrowUp,
CaretDown,
User,
FilePlus,
} from "phosphor-react";
import Image from "next/image";
import {
DropdownMenu,
DropdownMenuTrigger,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuArrow,
DropdownMenuSeparator,
} from "./DropdownMenu";
import NewWindow from "react-new-window";
import { signOut, useSession } from "next-auth/react";
import { useSnapshot } from "valtio";
import toast from "react-hot-toast";
import { createNewFile, state, updateEditorSettings } from "../state";
import {
createNewFile,
syncToGist,
updateEditorSettings,
downloadAsZip,
} from "../state/actions";
import state from "../state";
import Box from "./Box";
import Button from "./Button";
import Container from "./Container";
@@ -16,233 +47,522 @@ import {
} from "./Dialog";
import Flex from "./Flex";
import Stack from "./Stack";
import { useSnapshot } from "valtio";
import { useSession } from "next-auth/react";
import { useRouter } from "next/router";
import Input from "./Input";
import Text from "./Text";
import Tooltip from "./Tooltip";
import {
AlertDialog,
AlertDialogContent,
AlertDialogTitle,
AlertDialogDescription,
AlertDialogCancel,
AlertDialogAction,
} from "./AlertDialog";
import { styled } from "../stitches.config";
const EditorNavigation = () => {
const ErrorText = styled(Text, {
color: "$error",
mt: "$1",
display: "block",
});
const EditorNavigation = ({ showWat }: { showWat?: boolean }) => {
const snap = useSnapshot(state);
const [createNewAlertOpen, setCreateNewAlertOpen] = useState(false);
const [editorSettingsOpen, setEditorSettingsOpen] = useState(false);
const [isNewfileDialogOpen, setIsNewfileDialogOpen] = useState(false);
const [newfileError, setNewfileError] = useState<string | null>(null);
const [filename, setFilename] = useState("");
const { theme } = useTheme();
const { data: session, status } = useSession();
const router = useRouter();
const [popup, setPopUp] = useState(false);
const [editorSettings, setEditorSettings] = useState(snap.editorSettings);
useEffect(() => {
if (session && session.user && popup) {
setPopUp(false);
}
}, [session, popup]);
// when filename changes, reset error
useEffect(() => {
setNewfileError(null);
}, [filename, setNewfileError]);
const validateFilename = useCallback(
(filename: string): { error: string | null } => {
// check if filename already exists
if (!filename) {
return { error: "You need to add filename" };
}
if (snap.files.find((file) => file.name === filename)) {
return { error: "Filename already exists." };
}
if (!filename.includes(".") || filename[filename.length - 1] === ".") {
return { error: "Filename should include file extension" };
}
// check for illegal characters
const ALPHA_NUMERICAL_REGEX = /^[A-Za-z0-9_-]+[.][A-Za-z0-9]{1,4}$/g;
if (!filename.match(ALPHA_NUMERICAL_REGEX)) {
return {
error: `Filename can contain only characters from a-z, A-Z, 0-9, "_" and "-" and it needs to have file extension (e.g. ".c")`,
};
}
return { error: null };
},
[snap.files]
);
const handleConfirm = useCallback(() => {
// add default extension in case omitted
const chk = validateFilename(filename);
if (chk && chk.error) {
setNewfileError(`Error: ${chk.error}`);
return;
}
setIsNewfileDialogOpen(false);
createNewFile(filename);
setFilename("");
}, [filename, setIsNewfileDialogOpen, setFilename, validateFilename]);
const files = snap.files;
return (
<Flex css={{ flexShrink: 0, gap: "$0" }}>
<Flex css={{ overflowX: "scroll", py: "$3", flex: 1 }}>
<Flex
css={{
overflowX: "scroll",
py: "$3",
flex: 1,
"&::-webkit-scrollbar": {
height: 0,
background: "transparent",
},
}}
>
<Container css={{ flex: 1 }}>
<Stack css={{ gap: "$3", flex: 1, flexWrap: "nowrap" }}>
{state.loading && "loading"}
{snap.files &&
snap.files.length > 0 &&
snap.files?.map((file, index) => (
<Button
size="sm"
outline={snap.active !== index}
onClick={() => (state.active = index)}
key={file.name + index}
css={{
"&:hover": {
span: {
visibility: "visible",
},
},
}}
>
{file.name}
<Box
as="span"
<Stack
css={{
gap: "$3",
flex: 1,
flexWrap: "nowrap",
marginBottom: "-1px",
}}
>
{files &&
files.length > 0 &&
files.map((file, index) => {
if (!file.compiledContent && showWat) {
return null;
}
return (
<Button
size="sm"
outline={
showWat ? snap.activeWat !== index : snap.active !== index
}
onClick={() => (state.active = index)}
key={file.name + index}
css={{
display: "flex",
p: "2px",
borderRadius: "$full",
mr: "-4px",
"&:hover": {
// boxSizing: "0px 0px 1px",
backgroundColor: "$mauve2",
color: "$mauve12",
span: {
visibility: "visible",
},
},
}}
onClick={(ev: React.MouseEvent<HTMLElement>) => {
ev.stopPropagation();
// Remove file from state
state.files.splice(index, 1);
// Change active file state
// If deleted file is behind active tab
// we keep the current state otherwise
// select previous file on the list
state.active =
index > snap.active ? snap.active : snap.active - 1;
}}
>
<X size="9px" weight="bold" />
</Box>
</Button>
))}
<Dialog>
<DialogTrigger asChild>
<Button
ghost
size="sm"
css={{ alignItems: "center", px: "$2", mr: "$3" }}
>
<Plus size="16px" />{" "}
{snap.files.length === 0 && "Add new file"}
</Button>
</DialogTrigger>
<DialogContent>
<DialogTitle>Create new file</DialogTitle>
<DialogDescription>
<label>Filename</label>
<Input
value={filename}
onChange={(e) => setFilename(e.target.value)}
/>
</DialogDescription>
<Flex
css={{ marginTop: 25, justifyContent: "flex-end", gap: "$3" }}
>
<DialogClose asChild>
<Button outline>Cancel</Button>
</DialogClose>
<DialogClose asChild>
<Button
variant="primary"
onClick={() => {
createNewFile(filename);
// reset
setFilename("");
{file.name}
{showWat && ".wat"}
{!showWat && (
<Box
as="span"
css={{
display: "flex",
p: "2px",
borderRadius: "$full",
mr: "-4px",
"&:hover": {
// boxSizing: "0px 0px 1px",
backgroundColor: "$mauve2",
color: "$mauve12",
},
}}
onClick={(ev: React.MouseEvent<HTMLElement>) => {
ev.stopPropagation();
// Remove file from state
state.files.splice(index, 1);
// Change active file state
// If deleted file is behind active tab
// we keep the current state otherwise
// select previous file on the list
state.active =
index > snap.active ? snap.active : snap.active - 1;
}}
>
<X size="9px" weight="bold" />
</Box>
)}
</Button>
);
})}
{!showWat && (
<Dialog
open={isNewfileDialogOpen}
onOpenChange={setIsNewfileDialogOpen}
>
<DialogTrigger asChild>
<Button
ghost
size="sm"
css={{ alignItems: "center", px: "$2", mr: "$3" }}
>
<Plus size="16px" />{" "}
{snap.files.length === 0 && "Add new file"}
</Button>
</DialogTrigger>
<DialogContent>
<DialogTitle>Create new file</DialogTitle>
<DialogDescription>
<label>Filename</label>
<Input
value={filename}
onChange={(e) => setFilename(e.target.value)}
onKeyPress={(e) => {
if (e.key === "Enter") {
handleConfirm();
}
}}
>
/>
<ErrorText>{newfileError}</ErrorText>
</DialogDescription>
<Flex
css={{
marginTop: 25,
justifyContent: "flex-end",
gap: "$3",
}}
>
<DialogClose asChild>
<Button outline>Cancel</Button>
</DialogClose>
<Button variant="primary" onClick={handleConfirm}>
Create file
</Button>
</Flex>
<DialogClose asChild>
<Box css={{ position: "absolute", top: "$3", right: "$3" }}>
<X size="20px" />
</Box>
</DialogClose>
</Flex>
<DialogClose asChild>
<Box css={{ position: "absolute", top: "$3", right: "$3" }}>
<X size="20px" />
</Box>
</DialogClose>
</DialogContent>
</Dialog>
</DialogContent>
</Dialog>
)}
</Stack>
</Container>
</Flex>
<Flex
css={{
py: "$3",
backgroundColor: "$mauve3",
backgroundColor: "$mauve2",
zIndex: 1,
}}
>
<Container css={{ width: "unset" }}>
<Container
css={{ width: "unset", display: "flex", alignItems: "center" }}
>
{status === "authenticated" ? (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Box
css={{
display: "flex",
borderRadius: "$full",
overflow: "hidden",
width: "$6",
height: "$6",
boxShadow: "0px 0px 0px 1px $colors$mauve11",
position: "relative",
mr: "$3",
"@hover": {
"&:hover": {
cursor: "pointer",
boxShadow: "0px 0px 0px 1px $colors$mauve12",
},
},
}}
>
<Image
src={session?.user?.image || ""}
width="30px"
height="30px"
objectFit="cover"
alt="User avatar"
/>
</Box>
</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuItem disabled onClick={() => signOut()}>
<User size="16px" /> {session?.user?.username} (
{session?.user.name})
</DropdownMenuItem>
<DropdownMenuItem
onClick={() =>
window.open(
`http://gist.github.com/${session?.user.username}`
)
}
>
<ArrowSquareOut size="16px" />
Go to your Gist
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem onClick={() => signOut({ callbackUrl: "/" })}>
<SignOut size="16px" /> Log out
</DropdownMenuItem>
<DropdownMenuArrow offset={10} />
</DropdownMenuContent>
</DropdownMenu>
) : (
<Button
outline
size="sm"
css={{ mr: "$3" }}
onClick={() => setPopUp(true)}
>
<GithubLogo size="16px" /> Login
</Button>
)}
<Stack
css={{
display: "inline-flex",
marginLeft: "auto",
flexShrink: 0,
gap: "$0",
border: "1px solid $mauve10",
borderRadius: "$sm",
boxShadow: "inset 0px 0px 0px 1px $colors$mauve10",
zIndex: 9,
position: "relative",
overflow: "hidden",
button: {
borderRadius: "$0",
borderRadius: 0,
px: "$2",
alignSelf: "flex-start",
boxShadow: "none",
},
"button:not(:first-child):not(:last-child)": {
borderRight: 0,
borderLeft: 0,
},
"button:first-child": {
borderTopLeftRadius: "$sm",
borderBottomLeftRadius: "$sm",
},
"button:last-child": {
borderTopRightRadius: "$sm",
borderBottomRightRadius: "$sm",
boxShadow: "inset 0px 0px 0px 1px $colors$mauve10",
"&:hover": {
boxShadow: "inset 0px 0px 0px 1px $colors$mauve12",
},
},
}}
>
<Button ghost size="sm" css={{ alignItems: "center" }}>
<DownloadSimple size="16px" />
</Button>
<Dialog>
<DialogTrigger asChild>
<Button ghost size="sm" css={{ alignItems: "center" }}>
<Tooltip content="Download as ZIP">
<Button
isLoading={snap.zipLoading}
onClick={downloadAsZip}
outline
size="sm"
css={{ alignItems: "center" }}
>
<DownloadSimple size="16px" />
</Button>
</Tooltip>
<Tooltip content="Copy share link to clipboard">
<Button
outline
size="sm"
css={{ alignItems: "center" }}
onClick={() => {
navigator.clipboard.writeText(
`${window.location.origin}/develop/${snap.gistId}`
);
toast.success("Copied share link to clipboard!");
}}
>
<Share size="16px" />
</Button>
</Tooltip>
<Tooltip
content={
session && session.user
? snap.gistOwner === session?.user.username
? "Sync to Gist"
: "Create as a new Gist"
: "You need to be logged in to sync with Gist"
}
>
<Button
outline
size="sm"
isDisabled={!session || !session.user}
isLoading={snap.gistLoading}
css={{ alignItems: "center" }}
onClick={() => {
if (!session || !session.user) {
return;
}
if (snap.gistOwner === session?.user.username) {
syncToGist(session);
} else {
setCreateNewAlertOpen(true);
}
}}
>
{snap.gistOwner === session?.user.username ? (
<CloudArrowUp size="16px" />
) : (
<FilePlus size="16px" />
)}
</Button>
</Tooltip>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button outline size="sm">
<CaretDown size="16px" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuItem
disabled={snap.zipLoading}
onClick={downloadAsZip}
>
<DownloadSimple size="16px" /> Download as ZIP
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => {
navigator.clipboard.writeText(
`${window.location.origin}/develop/${snap.gistId}`
);
toast.success("Copied share link to clipboard!");
}}
>
<Share size="16px" />
</Button>
</DialogTrigger>
<DialogContent>
<DialogTitle>Share hook</DialogTitle>
<DialogDescription>
<span>
We will store your hook code in public GitHub Gist and
generate link to that
</span>
</DialogDescription>
<Flex
css={{ marginTop: 25, justifyContent: "flex-end", gap: "$3" }}
Copy share link to clipboard
</DropdownMenuItem>
<DropdownMenuItem
disabled={
session?.user.username !== snap.gistOwner || !snap.gistId
}
onClick={() => {
syncToGist(session);
}}
>
<DialogClose asChild>
<Button outline>Cancel</Button>
</DialogClose>
</Flex>
<DialogClose asChild>
<Box css={{ position: "absolute", top: "$3", right: "$3" }}>
<X size="20px" />
</Box>
</DialogClose>
</DialogContent>
</Dialog>
<Dialog>
<DialogTrigger asChild>
<Button ghost size="sm" css={{ alignItems: "center" }}>
<Gear size="16px" />
</Button>
</DialogTrigger>
<DialogContent>
<DialogTitle>Editor settings</DialogTitle>
<DialogDescription>
<label>Tab size</label>
<Input
type="number"
min="1"
value={editorSettings.tabSize}
onChange={(e) =>
setEditorSettings((curr) => ({
...curr,
tabSize: Number(e.target.value),
}))
}
/>
</DialogDescription>
<Flex
css={{ marginTop: 25, justifyContent: "flex-end", gap: "$3" }}
<CloudArrowUp size="16px" /> Push to Gist
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem
disabled={status !== "authenticated"}
onClick={() => {
setCreateNewAlertOpen(true);
}}
>
<DialogClose asChild>
<Button
outline
onClick={() => updateEditorSettings(editorSettings)}
>
Cancel
</Button>
</DialogClose>
<DialogClose asChild>
<Button
variant="primary"
onClick={() => updateEditorSettings(editorSettings)}
>
Save changes
</Button>
</DialogClose>
</Flex>
<DialogClose asChild>
<Box css={{ position: "absolute", top: "$3", right: "$3" }}>
<X size="20px" />
</Box>
</DialogClose>
</DialogContent>
</Dialog>
<FilePlus size="16px" /> Create as a new Gist
</DropdownMenuItem>
<DropdownMenuItem onClick={() => setEditorSettingsOpen(true)}>
<Gear size="16px" /> Editor Settings
</DropdownMenuItem>
<DropdownMenuArrow offset={10} />
</DropdownMenuContent>
</DropdownMenu>
</Stack>
{popup && !session ? (
<NewWindow center="parent" url="/sign-in" />
) : null}
</Container>
</Flex>
<AlertDialog
open={createNewAlertOpen}
onOpenChange={(value) => setCreateNewAlertOpen(value)}
>
<AlertDialogContent>
<AlertDialogTitle>Are you sure?</AlertDialogTitle>
<AlertDialogDescription>
This action will create new <strong>public</strong> Github Gist from
your current saved files. You can delete gist anytime from your
GitHub Gists page.
</AlertDialogDescription>
<Flex css={{ justifyContent: "flex-end", gap: "$3" }}>
<AlertDialogCancel asChild>
<Button outline>Cancel</Button>
</AlertDialogCancel>
<AlertDialogAction asChild>
<Button
variant="primary"
onClick={() => {
syncToGist(session, true);
}}
>
<FilePlus size="15px" /> Create new Gist
</Button>
</AlertDialogAction>
</Flex>
</AlertDialogContent>
</AlertDialog>
<Dialog open={editorSettingsOpen} onOpenChange={setEditorSettingsOpen}>
<DialogTrigger asChild>
{/* <Button outline size="sm" css={{ alignItems: "center" }}>
<Gear size="16px" />
</Button> */}
</DialogTrigger>
<DialogContent>
<DialogTitle>Editor settings</DialogTitle>
<DialogDescription>
<label>Tab size</label>
<Input
type="number"
min="1"
value={editorSettings.tabSize}
onChange={(e) =>
setEditorSettings((curr) => ({
...curr,
tabSize: Number(e.target.value),
}))
}
/>
</DialogDescription>
<Flex css={{ marginTop: 25, justifyContent: "flex-end", gap: "$3" }}>
<DialogClose asChild>
<Button
outline
onClick={() => updateEditorSettings(editorSettings)}
>
Cancel
</Button>
</DialogClose>
<DialogClose asChild>
<Button
variant="primary"
onClick={() => updateEditorSettings(editorSettings)}
>
Save changes
</Button>
</DialogClose>
</Flex>
<DialogClose asChild>
<Box css={{ position: "absolute", top: "$3", right: "$3" }}>
<X size="20px" />
</Box>
</DialogClose>
</DialogContent>
</Dialog>
</Flex>
);
};

View File

@@ -3,6 +3,51 @@ import Box from "./Box";
const Flex = styled(Box, {
display: "flex",
variants: {
row: {
true: {
flexDirection: "row",
},
},
column: {
true: {
flexDirection: "column",
},
},
fluid: {
true: {
width: "100%",
},
},
align: {
start: {
alignItems: "start",
},
center: {
alignItems: "center",
},
end: {
alignItems: "end",
},
},
justify: {
start: {
justifyContent: "start",
},
center: {
justifyContent: "center",
},
end: {
justifyContent: "end",
},
"space-between": {
justifyContent: "space-between",
},
"space-around": {
justifyContent: "space-around",
},
},
},
});
export default Flex;

View File

@@ -1,51 +0,0 @@
import { useSnapshot } from "valtio";
import Container from "./Container";
import Box from "./Box";
import LogText from "./LogText";
import { state } from "../state";
const Footer = () => {
const snap = useSnapshot(state);
return (
<Box
as="footer"
css={{
display: "flex",
borderTop: "1px solid $mauve6",
background: "$mauve1",
}}
>
<Container css={{ py: "$3", flexShrink: 1 }}>
<Box
as="pre"
css={{
display: "flex",
flexDirection: "column",
width: "100%",
height: "160px",
fontSize: "13px",
fontWeight: "$body",
fontFamily: "$monospace",
overflowY: "scroll",
wordWrap: "break-word",
py: 3,
px: 3,
m: 3,
}}
>
{snap.logs.map((log, index) => (
<Box as="span" key={log.type + index}>
<LogText capitalize variant={log.type}>
{log.type}:{" "}
</LogText>
<LogText>{log.message}</LogText>
</Box>
))}
</Box>
</Container>
</Box>
);
};
export default Footer;

View File

@@ -4,7 +4,13 @@ const Heading = styled("span", {
fontFamily: "$heading",
lineHeight: "$heading",
fontWeight: "$heading",
textTransform: "uppercase",
variants: {
uppercase: {
true: {
textTransform: "uppercase",
},
},
},
});
export default Heading;

View File

@@ -1,24 +1,128 @@
import React, { useEffect, useRef } from "react";
import { useSnapshot, ref } from "valtio";
import Editor from "@monaco-editor/react";
import Editor, { loader } from "@monaco-editor/react";
import type monaco from "monaco-editor";
import { ArrowBendLeftUp, Play } from "phosphor-react";
import { ArrowBendLeftUp } from "phosphor-react";
import { useTheme } from "next-themes";
import { useRouter } from "next/router";
import uniqBy from "lodash.uniqby";
import Box from "./Box";
import Button from "./Button";
import Container from "./Container";
import dark from "../theme/editor/amy.json";
import light from "../theme/editor/xcode_default.json";
import { compileCode, saveFile, state } from "../state";
import { saveFile } from "../state/actions";
import { apiHeaderFiles } from "../state/constants";
import state from "../state";
import EditorNavigation from "./EditorNavigation";
import Text from "./Text";
import { MonacoServices } from "@codingame/monaco-languageclient";
import { createLanguageClient, createWebSocket } from "../utils/languageClient";
import { listen } from "@codingame/monaco-jsonrpc";
import ReconnectingWebSocket from "reconnecting-websocket";
import docs from "../xrpl-hooks-docs/docs";
loader.config({
paths: {
vs: "https://cdn.jsdelivr.net/npm/monaco-editor@0.30.1/min/vs",
},
});
const validateWritability = (editor: monaco.editor.IStandaloneCodeEditor) => {
const currPath = editor.getModel()?.uri.path;
if (apiHeaderFiles.find((h) => currPath?.endsWith(h))) {
editor.updateOptions({ readOnly: true });
} else {
editor.updateOptions({ readOnly: false });
}
};
let decorations: { [key: string]: string[] } = {};
const setMarkers = (monacoE: typeof monaco) => {
// Get all the markers that are active at the moment,
// Also if same error is there twice, we can show the content
// only once (that's why we're using uniqBy)
const markers = uniqBy(
monacoE.editor
.getModelMarkers({})
// Filter out the markers that are hooks specific
.filter(
(marker) =>
typeof marker?.code === "string" &&
// Take only markers that starts with "hooks-"
marker?.code?.includes("hooks-")
),
"code"
);
// Get the active model (aka active file you're editing)
// const model = monacoE.editor?.getModel(
// monacoE.Uri.parse(`file:///work/c/${state.files?.[state.active]?.name}`)
// );
// console.log(state.active);
// Add decoration (aka extra hoverMessages) to markers in the
// exact same range (location) where the markers are
const models = monacoE.editor.getModels();
models.forEach((model) => {
decorations[model.id] = model?.deltaDecorations(
decorations?.[model.id] || [],
markers
.filter((marker) =>
marker?.resource.path
.split("/")
.includes(`${state.files?.[state.active]?.name}`)
)
.map((marker) => ({
range: new monacoE.Range(
marker.startLineNumber,
marker.startColumn,
marker.endLineNumber,
marker.endColumn
),
options: {
hoverMessage: {
value:
// Find the related hover message markdown from the
// /xrpl-hooks-docs/xrpl-hooks-docs-files.json file
// which was generated from rst files
(typeof marker.code === "string" &&
docs[marker?.code]?.toString()) ||
"",
supportHtml: true,
isTrusted: true,
},
},
}))
);
});
};
const HooksEditor = () => {
const editorRef = useRef<monaco.editor.IStandaloneCodeEditor>();
const monacoRef = useRef<typeof monaco>();
const subscriptionRef = useRef<ReconnectingWebSocket | null>(null);
const snap = useSnapshot(state);
const router = useRouter();
const { theme } = useTheme();
useEffect(() => {
if (editorRef.current) validateWritability(editorRef.current);
}, [snap.active]);
useEffect(() => {
return () => {
subscriptionRef?.current?.close();
};
}, []);
useEffect(() => {
if (monacoRef.current) {
setMarkers(monacoRef.current);
}
}, [snap.active]);
return (
<Box
css={{
@@ -27,18 +131,69 @@ const HooksEditor = () => {
display: "flex",
position: "relative",
flexDirection: "column",
backgroundColor: "$mauve3",
backgroundColor: "$mauve2",
width: "100%",
}}
>
<EditorNavigation />
{snap.files.length > 0 ? (
{snap.files.length > 0 && router.isReady ? (
<Editor
className="hooks-editor"
keepCurrentModel
defaultLanguage={snap.files?.[snap.active]?.language}
path={snap.files?.[snap.active]?.name}
language={snap.files?.[snap.active]?.language}
path={`file:///work/c/${snap.files?.[snap.active]?.name}`}
defaultValue={snap.files?.[snap.active]?.content}
beforeMount={(monaco) => {
if (!snap.editorCtx) {
snap.files.forEach((file) =>
monaco.editor.createModel(
file.content,
file.language,
monaco.Uri.parse(`file:///work/c/${file.name}`)
)
);
}
// create the web socket
if (!subscriptionRef.current) {
monaco.languages.register({
id: "c",
extensions: [".c", ".h"],
aliases: ["C", "c", "H", "h"],
mimetypes: ["text/plain"],
});
MonacoServices.install(monaco);
const webSocket = createWebSocket(
process.env.NEXT_PUBLIC_LANGUAGE_SERVER_API_ENDPOINT || ""
);
subscriptionRef.current = webSocket;
// listen when the web socket is opened
listen({
webSocket: webSocket as WebSocket,
onConnection: (connection) => {
// create and start the language client
const languageClient = createLanguageClient(connection);
const disposable = languageClient.start();
connection.onClose(() => {
try {
// disposable.stop();
disposable.dispose();
} catch (err) {
console.log("err", err);
}
});
},
});
}
// // hook editor to global state
// editor.updateOptions({
// minimap: {
// enabled: false,
// },
// ...snap.editorSettings,
// });
if (!state.editorCtx) {
state.editorCtx = ref(monaco.editor);
// @ts-expect-error
@@ -49,70 +204,63 @@ const HooksEditor = () => {
}}
onMount={(editor, monaco) => {
editorRef.current = editor;
// hook editor to global state
monacoRef.current = monaco;
editor.updateOptions({
minimap: {
enabled: false,
glyphMargin: true,
lightbulb: {
enabled: true,
},
...snap.editorSettings,
});
editor.addCommand(
monaco.KeyMod.CtrlCmd | monaco.KeyCode.KEY_S,
monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyS,
() => {
saveFile(editor.getValue());
saveFile();
}
);
// When the markers (errors/warnings from clangd language server) change
// Lets improve the markers by adding extra content to them from related
// md files
monaco.editor.onDidChangeMarkers(() => {
if (monacoRef.current) {
setMarkers(monacoRef.current);
}
});
validateWritability(editor);
}}
theme={theme === "dark" ? "dark" : "light"}
/>
) : (
<Container>
<Box
css={{
flexDirection: "row",
width: "$spaces$wide",
gap: "$3",
display: "inline-flex",
}}
>
<Box css={{ display: "inline-flex", pl: "35px" }}>
<ArrowBendLeftUp size={30} />
</Box>
{!snap.loading && router.isReady && (
<Box
css={{ pl: "0px", pt: "15px", flex: 1, display: "inline-flex" }}
css={{
flexDirection: "row",
width: "$spaces$wide",
gap: "$3",
display: "inline-flex",
}}
>
<Text
css={{
fontSize: "14px",
maxWidth: "220px",
fontFamily: "$monospace",
}}
<Box css={{ display: "inline-flex", pl: "35px" }}>
<ArrowBendLeftUp size={30} />
</Box>
<Box
css={{ pl: "0px", pt: "15px", flex: 1, display: "inline-flex" }}
>
Click the link above to create a your file (until we get the
fancy modal, where you can select example files 😊)
</Text>
<Text
css={{
fontSize: "14px",
maxWidth: "220px",
fontFamily: "$monospace",
}}
>
Click the link above to create your file
</Text>
</Box>
</Box>
</Box>
)}
</Container>
)}
<Button
variant="primary"
uppercase
disabled={!snap.files.length}
isLoading={snap.compiling}
onClick={() => compileCode(snap.active)}
css={{
position: "absolute",
bottom: "$4",
left: "$4",
alignItems: "center",
display: "flex",
cursor: "pointer",
}}
>
<Play weight="bold" size="16px" />
Compile to Wasm
</Button>
</Box>
);
};

View File

@@ -1,3 +1,4 @@
import React from "react";
import { styled } from "../stitches.config";
export const Input = styled("input", {
@@ -65,6 +66,14 @@ export const Input = styled("input", {
variants: {
size: {
sm: {
height: "$5",
fontSize: "$1",
lineHeight: "$sizes$4",
"&:-webkit-autofill::first-line": {
fontSize: "$1",
},
},
md: {
height: "$8",
fontSize: "$1",
@@ -102,20 +111,24 @@ export const Input = styled("input", {
backgroundColor: "transparent",
},
},
deep: {
backgroundColor: "$deep",
boxShadow: "none",
},
},
state: {
invalid: {
boxShadow: "inset 0 0 0 1px $colors$red7",
boxShadow: "inset 0 0 0 1px $colors$crimson7",
"&:focus": {
boxShadow:
"inset 0px 0px 0px 1px $colors$red8, 0px 0px 0px 1px $colors$red8",
"inset 0px 0px 0px 1px $colors$crimson8, 0px 0px 0px 1px $colors$crimson8",
},
},
valid: {
boxShadow: "inset 0 0 0 1px $colors$green7",
boxShadow: "inset 0 0 0 1px $colors$grass7",
"&:focus": {
boxShadow:
"inset 0px 0px 0px 1px $colors$green8, 0px 0px 0px 1px $colors$green8",
"inset 0px 0px 0px 1px $colors$grass8, 0px 0px 0px 1px $colors$grass8",
},
},
},
@@ -136,4 +149,10 @@ export const Input = styled("input", {
},
});
export default Input;
// eslint-disable-next-line react/display-name
const ReffedInput = React.forwardRef<
HTMLInputElement,
React.ComponentProps<typeof Input>
>((props, ref) => <Input {...props} ref={ref} />);
export default ReffedInput;

16
components/Link.tsx Normal file
View File

@@ -0,0 +1,16 @@
import { styled } from "../stitches.config";
const StyledLink = styled("a", {
color: "CurrentColor",
textDecoration: "underline",
cursor: 'pointer',
variants: {
highlighted: {
true: {
color: '$blue9'
}
}
}
});
export default StyledLink;

223
components/LogBox.tsx Normal file
View File

@@ -0,0 +1,223 @@
import {
useRef,
useLayoutEffect,
ReactNode,
FC,
useState,
useCallback,
} from "react";
import { Notepad, Prohibit } from "phosphor-react";
import useStayScrolled from "react-stay-scrolled";
import NextLink from "next/link";
import Container from "./Container";
import LogText from "./LogText";
import state, { ILog } from "../state";
import { Pre, Link, Heading, Button, Text, Flex, Box } from ".";
import regexifyString from "regexify-string";
import { useSnapshot } from "valtio";
import { AccountDialog } from "./Accounts";
interface ILogBox {
title: string;
clearLog?: () => void;
logs: ILog[];
renderNav?: () => ReactNode;
enhanced?: boolean;
}
const LogBox: FC<ILogBox> = ({
title,
clearLog,
logs,
children,
renderNav,
enhanced,
}) => {
const logRef = useRef<HTMLPreElement>(null);
const { stayScrolled /*, scrollBottom*/ } = useStayScrolled(logRef);
useLayoutEffect(() => {
stayScrolled();
}, [stayScrolled, logs]);
return (
<Flex
as="div"
css={{
display: "flex",
borderTop: "1px solid $mauve6",
background: "$mauve1",
position: "relative",
flex: 1,
height: "100%",
}}
>
<Container
css={{
px: 0,
height: "100%",
}}
>
<Flex
fluid
css={{
height: "48px",
alignItems: "center",
fontSize: "$sm",
fontWeight: 300,
}}
>
<Heading
as="h3"
css={{
fontWeight: 300,
m: 0,
fontSize: "11px",
color: "$mauve12",
px: "$3",
textTransform: "uppercase",
alignItems: "center",
display: "inline-flex",
gap: "$3",
}}
>
<Notepad size="15px" /> <Text css={{ lineHeight: 1 }}>{title}</Text>
</Heading>
<Flex
row
align="center"
css={{
width: "50%", // TODO make it max without breaking layout!
}}
>
{renderNav?.()}
</Flex>
<Flex css={{ ml: "auto", gap: "$3", marginRight: "$3" }}>
{clearLog && (
<Button ghost size="xs" onClick={clearLog}>
<Prohibit size="14px" />
</Button>
)}
</Flex>
</Flex>
<Box
as="pre"
ref={logRef}
css={{
margin: 0,
// display: "inline-block",
display: "flex",
flexDirection: "column",
width: "100%",
height: "calc(100% - 48px)", // 100% minus the logbox header height
overflowY: "auto",
fontSize: "13px",
fontWeight: "$body",
fontFamily: "$monospace",
px: "$3",
pb: "$2",
whiteSpace: "normal",
}}
>
{logs?.map((log, index) => (
<Box
as="span"
key={log.type + index}
css={{
"@hover": {
"&:hover": {
backgroundColor: enhanced ? "$backgroundAlt" : undefined,
},
},
p: enhanced ? "$1" : undefined,
my: enhanced ? "$1" : undefined,
}}
>
<Log {...log} />
</Box>
))}
{children}
</Box>
</Container>
</Flex>
);
};
export const Log: FC<ILog> = ({
type,
timestamp: timestamp,
message: _message,
link,
linkText,
defaultCollapsed,
jsonData: _jsonData,
}) => {
const [expanded, setExpanded] = useState(!defaultCollapsed);
const { accounts } = useSnapshot(state);
const [dialogAccount, setDialogAccount] = useState<string | null>(null);
const enrichAccounts = useCallback(
(str?: string): ReactNode => {
if (!str || !accounts.length) return null;
const pattern = `(${accounts.map((acc) => acc.address).join("|")})`;
const res = regexifyString({
pattern: new RegExp(pattern, "gim"),
decorator: (match, idx) => {
const name = accounts.find((acc) => acc.address === match)?.name;
return (
<Link
key={match + idx}
as="a"
onClick={() => setDialogAccount(match)}
title={match}
highlighted
>
{name || match}
</Link>
);
},
input: str,
});
return <>{res}</>;
},
[accounts]
);
_message = _message.trim().replace(/\n /gi, "\n");
const message = enrichAccounts(_message);
const jsonData = enrichAccounts(_jsonData);
return (
<>
<AccountDialog
setActiveAccountAddress={setDialogAccount}
activeAccountAddress={dialogAccount}
/>
<LogText variant={type}>
{timestamp && (
<Text muted monospace>
{timestamp}{" "}
</Text>
)}
<Pre>{message} </Pre>
{link && (
<NextLink href={link} shallow passHref>
<Link as="a">{linkText}</Link>
</NextLink>
)}
{jsonData && (
<Link onClick={() => setExpanded(!expanded)} as="a">
{expanded ? "Collapse" : "Expand"}
</Link>
)}
{expanded && jsonData && <Pre block>{jsonData}</Pre>}
</LogText>
<br />
</>
);
};
export default LogBox;

View File

@@ -4,16 +4,20 @@ const Text = styled("span", {
fontFamily: "$monospace",
lineHeight: "$body",
color: "$text",
wordWrap: "break-word",
variants: {
variant: {
log: {
color: "$text",
},
warning: {
color: "$yellow11",
color: "$warning",
},
error: {
color: "$red11",
color: "$error",
},
success: {
color: "$success",
},
},
capitalize: {

View File

@@ -1,10 +1,8 @@
import { useTheme } from "next-themes";
import { styled } from "../stitches.config";
const SVG = styled("svg", {
"& #path-one, & #path-two": {
fill: "$text",
"& #path": {
fill: "$accent",
},
});
function Logo({
@@ -16,21 +14,18 @@ function Logo({
}) {
return (
<SVG
width={width || "1em"}
height={height || "1em"}
viewBox="0 0 28 22"
width={width || "1.1em"}
height={height || "1.1em"}
viewBox="0 0 294 283"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
id="path-one"
d="M19.603 3.87h2.3l-4.786 4.747a4.466 4.466 0 01-6.276 0L6.054 3.871h2.3l3.636 3.605a2.828 2.828 0 003.975 0l3.638-3.605zM8.325 17.069h-2.3l4.816-4.776a4.466 4.466 0 016.276 0l4.816 4.776h-2.3l-3.665-3.635a2.828 2.828 0 00-3.975 0l-3.668 3.635z"
/>
<path
id="path-two"
fillRule="evenodd"
clipRule="evenodd"
d="M1.556 9.769h4.751v1.555H1.556v10.072H0V0h1.556v9.769zM26.444 9.769h-4.751v1.555h4.751v10.072H28V0h-1.556v9.769z"
id="path"
d="M265.827 235L172.416 141.589L265.005 49H226.822L147.732 128.089H53.5514L27.4824 155.089H147.732L227.643 235H265.827Z"
fill="#9D2DFF"
/>
</SVG>
);

View File

@@ -1,23 +1,9 @@
import React from "react";
import Link from "next/link";
import {
Gear,
GithubLogo,
SignOut,
User,
ArrowSquareOut,
} from "phosphor-react";
import { useSnapshot } from "valtio";
import Image from "next/image";
import { useSession, signIn, signOut } from "next-auth/react";
import {
DropdownMenu,
DropdownMenuTrigger,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuArrow,
DropdownMenuSeparator,
} from "./DropdownMenu";
import { useRouter } from "next/router";
import { FolderOpen, X, ArrowUpRight, BookOpen } from "phosphor-react";
import Stack from "./Stack";
import Logo from "./Logo";
@@ -26,47 +12,342 @@ import Flex from "./Flex";
import Container from "./Container";
import Box from "./Box";
import ThemeChanger from "./ThemeChanger";
import { useRouter } from "next/router";
import state from "../state";
import Heading from "./Heading";
import Text from "./Text";
import Spinner from "./Spinner";
import truncate from "../utils/truncate";
import ButtonGroup from "./ButtonGroup";
import {
Dialog,
DialogClose,
DialogContent,
DialogDescription,
DialogTitle,
DialogTrigger,
} from "./Dialog";
import PanelBox from "./PanelBox";
import { templateFileIds } from "../state/constants";
const Navigation = () => {
const { data: session, status } = useSession();
const router = useRouter();
const snap = useSnapshot(state);
const slug = router.query?.slug;
const gistId = Array.isArray(slug) ? slug[0] : null;
return (
<Box
as="nav"
css={{
display: "flex",
height: "60px",
backgroundColor: "$mauve1",
borderBottom: "1px solid $mauve6",
position: "relative",
zIndex: 2003,
height: "60px",
}}
>
<Container
css={{
display: "flex",
alignItems: "center",
py: "$2",
}}
>
<Link href="/" passHref>
<Box
as="a"
css={{
display: "flex",
alignItems: "center",
color: "$textColor",
}}
>
<Logo width="30px" height="30px" />
</Box>
</Link>
<Flex
css={{
flex: 1,
alignItems: "center",
borderRight: "1px solid $colors$mauve6",
py: "$3",
pr: "$4",
}}
>
<Link href={gistId ? `/develop/${gistId}` : "/develop"} passHref>
<Box
as="a"
css={{
display: "flex",
alignItems: "center",
color: "$textColor",
}}
>
<Logo width="32px" height="32px" />
</Box>
</Link>
<Flex
css={{
ml: "$5",
flexDirection: "column",
gap: "1px",
}}
>
{snap.loading ? (
<Spinner />
) : (
<>
<Heading css={{ lineHeight: 1 }}>
{snap.files?.[0]?.name || "XRPL Hooks"}
</Heading>
<Text
css={{ fontSize: "$xs", color: "$mauve10", lineHeight: 1 }}
>
{snap.files.length > 0 ? "Gist: " : "Playground"}
{snap.files.length > 0 && (
<Link
href={`https://gist.github.com/${snap.gistOwner || ""}/${
snap.gistId || ""
}`}
passHref
>
<Text
as="a"
target="_blank"
rel="noreferrer noopener"
css={{ color: "$mauve12" }}
>
{`${snap.gistOwner || "-"}/${truncate(
snap.gistId || ""
)}`}
</Text>
</Link>
)}
</Text>
</>
)}
</Flex>
{router.isReady && (
<ButtonGroup css={{ marginLeft: "auto" }}>
<Dialog
open={snap.mainModalOpen}
onOpenChange={(open) => (state.mainModalOpen = open)}
>
<DialogTrigger asChild>
<Button outline>
<FolderOpen size="15px" />
</Button>
</DialogTrigger>
<DialogContent
css={{
maxWidth: "1080px",
width: "80vw",
height: "80%",
backgroundColor: "$mauve1 !important",
overflowY: "auto",
p: 0,
}}
>
<Flex
css={{
flexDirection: "column",
flex: 1,
height: "auto",
"@md": {
flexDirection: "row",
height: "100%",
},
}}
>
<Flex
css={{
borderBottom: "1px solid $colors$mauve5",
width: "100%",
flexDirection: "column",
p: "$7",
height: "100%",
backgroundColor: "$mauve2",
"@md": {
width: "30%",
maxWidth: "300px",
borderBottom: "0px",
borderRight: "1px solid $colors$mauve6",
},
}}
>
<DialogTitle
css={{
textTransform: "uppercase",
display: "inline-flex",
alignItems: "center",
gap: "$3",
fontSize: "$xl",
lineHeight: "$one",
fontWeight: "$bold",
}}
>
<Logo width="48px" height="48px" /> XRPL Hooks Builder
</DialogTitle>
<DialogDescription as="div">
<Text
css={{
display: "inline-flex",
color: "inherit",
my: "$5",
mb: "$7",
}}
>
Hooks add smart contract functionality to the XRP
Ledger.
</Text>
<Flex
css={{ flexDirection: "column", gap: "$2", mt: "$2" }}
>
<Text
css={{
display: "inline-flex",
alignItems: "center",
gap: "$3",
color: "$purple10",
"&:hover": {
color: "$purple11",
},
"&:focus": {
outline: 0,
},
}}
as="a"
rel="noreferrer noopener"
target="_blank"
href="https://github.com/XRPL-Labs/xrpld-hooks"
>
<ArrowUpRight size="15px" /> Hooks Github
</Text>
<Text
css={{
display: "inline-flex",
alignItems: "center",
gap: "$3",
color: "$purple10",
"&:hover": {
color: "$purple11",
},
"&:focus": {
outline: 0,
},
}}
as="a"
rel="noreferrer noopener"
target="_blank"
href="https://xrpl-hooks.readme.io/v2.0/docs"
>
<ArrowUpRight size="15px" /> Hooks documentation
</Text>
<Text
css={{
display: "inline-flex",
alignItems: "center",
gap: "$3",
color: "$purple10",
"&:hover": {
color: "$purple11",
},
"&:focus": {
outline: 0,
},
}}
as="a"
rel="noreferrer noopener"
target="_blank"
href="https://xrpl.org/docs.html"
>
<ArrowUpRight size="15px" /> XRPL documentation
</Text>
</Flex>
</DialogDescription>
</Flex>
<div>
<Flex
css={{
display: "grid",
gridTemplateColumns: "1fr",
gridTemplateRows: "max-content",
flex: 1,
p: "$7",
gap: "$3",
alignItems: "normal",
flexWrap: "wrap",
backgroundColor: "$mauve1",
"@md": {
gridTemplateColumns: "1fr 1fr 1fr",
gridTemplateRows: "max-content",
},
}}
>
<PanelBox
as="a"
href={`/develop/${templateFileIds.starter}`}
>
<Heading>Starter</Heading>
<Text>
Just a basic starter with essential imports
</Text>
</PanelBox>
<PanelBox
as="a"
href={`/develop/${templateFileIds.firewall}`}
>
<Heading>Firewall</Heading>
<Text>
This Hook essentially checks a blacklist of accounts
</Text>
</PanelBox>
<PanelBox
as="a"
href={`/develop/${templateFileIds.notary}`}
>
<Heading>Notary</Heading>
<Text>
Collecting signatures for multi-sign transactions
</Text>
</PanelBox>
<PanelBox
as="a"
href={`/develop/${templateFileIds.carbon}`}
>
<Heading>Carbon</Heading>
<Text>Send a percentage of sum to an address</Text>
</PanelBox>
<PanelBox
as="a"
href={`/develop/${templateFileIds.peggy}`}
>
<Heading>Peggy</Heading>
<Text>An oracle based stable coin hook</Text>
</PanelBox>
</Flex>
</div>
</Flex>
<DialogClose asChild>
<Box
css={{
position: "absolute",
top: "$1",
right: "$1",
cursor: "pointer",
background: "$mauve1",
display: "flex",
borderRadius: "$full",
p: "$1",
}}
>
<X size="20px" />
</Box>
</DialogClose>
</DialogContent>
</Dialog>
<ThemeChanger />
</ButtonGroup>
)}
</Flex>
<Flex
css={{
flexWrap: "nowrap",
marginLeft: "$3",
marginLeft: "$4",
overflowX: "scroll",
"&::-webkit-scrollbar": {
height: 0,
background: "transparent",
},
}}
>
<Stack
@@ -75,117 +356,59 @@ const Navigation = () => {
gap: "$3",
flexWrap: "nowrap",
alignItems: "center",
pr: "$3",
marginLeft: "auto",
}}
>
<Link href="/develop" passHref shallow>
<Button
as="a"
outline={!router.pathname.includes("/develop")}
uppercase
<ButtonGroup>
<Link
href={gistId ? `/develop/${gistId}` : "/develop"}
passHref
shallow
>
Develop
</Button>
</Link>
<Link href="/deploy" passHref shallow>
<Button
as="a"
outline={!router.pathname.includes("/deploy")}
uppercase
<Button
as="a"
outline={!router.pathname.includes("/develop")}
uppercase
>
Develop
</Button>
</Link>
<Link
href={gistId ? `/deploy/${gistId}` : "/deploy"}
passHref
shallow
>
Deploy
</Button>
</Link>
<Link href="/test" passHref shallow>
<Button
as="a"
outline={!router.pathname.includes("/test")}
uppercase
<Button
as="a"
outline={!router.pathname.includes("/deploy")}
uppercase
>
Deploy
</Button>
</Link>
<Link
href={gistId ? `/test/${gistId}` : "/test"}
passHref
shallow
>
Test
</Button>
<Button
as="a"
outline={!router.pathname.includes("/test")}
uppercase
>
Test
</Button>
</Link>
</ButtonGroup>
<Link href="https://xrpl-hooks.readme.io/v2.0" passHref>
<a target="_blank" rel="noreferrer noopener">
<Button outline>
<BookOpen size="15px" />
</Button>
</a>
</Link>
<ThemeChanger />
</Stack>
</Flex>
<Stack
css={{
color: "text",
ml: "auto",
flexWrap: "nowrap",
marginLeft: "$3",
"@sm": {
marginLeft: "auto",
},
}}
>
{status === "authenticated" && (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Box
css={{
borderRadius: "$full",
overflow: "hidden",
width: "30px",
height: "30px",
position: "relative",
}}
>
<Image
src={session?.user?.image || ""}
width="30px"
height="30px"
objectFit="cover"
alt="User avatar"
/>
</Box>
</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuItem disabled onClick={() => signOut()}>
<User size="16px" /> {session?.user?.username} (
{session?.user.name})
</DropdownMenuItem>
<DropdownMenuItem
onClick={() =>
window.open(
`http://gist.github.com/${session?.user.username}`
)
}
>
<ArrowSquareOut size="16px" />
Go to your Gist
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem>
<Gear size="16px" /> Settings
</DropdownMenuItem>
<DropdownMenuItem onClick={() => signOut()}>
<SignOut size="16px" /> Log out
</DropdownMenuItem>
<DropdownMenuArrow offset={10} />
</DropdownMenuContent>
</DropdownMenu>
)}
{status !== "authenticated" && (
<Button
isLoading={status === "loading"}
outline
onClick={() => signIn("github")}
>
<GithubLogo size="16px" /> GitHub Login
</Button>
)}
{/* <Box
css={{
border: "1px solid",
borderRadius: "3px",
borderColor: "text",
p: 1,
}}
>
<BookOpen size="20px" />
</Box> */}
</Stack>
</Container>
</Box>
);

30
components/PanelBox.tsx Normal file
View File

@@ -0,0 +1,30 @@
import { styled } from "../stitches.config";
import Heading from "./Heading";
import Text from "./Text";
const PanelBox = styled("div", {
display: "flex",
flexDirection: "column",
border: "1px solid $colors$mauve6",
backgroundColor: "$mauve2",
padding: "$3",
borderRadius: "$sm",
fontWeight: "lighter",
height: "auto",
cursor: "pointer",
flex: "1 1 0px",
"&:hover": {
border: "1px solid $colors$mauve9",
},
[`& ${Heading}`]: {
fontWeight: "lighter",
mb: "$2",
},
[`& ${Text}`]: {
fontWeight: "lighter",
color: "$mauve10",
fontSize: "$sm",
},
});
export default PanelBox;

27
components/Pre.tsx Normal file
View File

@@ -0,0 +1,27 @@
import { styled } from "../stitches.config";
const Pre = styled("span", {
m: 0,
wordBreak: "break-all",
fontFamily: '$monospace',
whiteSpace: 'pre-wrap',
variants: {
fluid: {
true: {
width: "100%",
},
},
line: {
true: {
whiteSpace: 'pre-line'
}
},
block: {
true: {
display: 'block'
}
}
},
});
export default Pre;

152
components/Select.tsx Normal file
View File

@@ -0,0 +1,152 @@
import { forwardRef } from "react";
import { mauve, mauveDark, purple, purpleDark } from "@radix-ui/colors";
import { useTheme } from "next-themes";
import { styled } from "../stitches.config";
import dynamic from "next/dynamic";
import type { Props } from "react-select";
const SelectInput = dynamic(() => import("react-select"), { ssr: false });
// eslint-disable-next-line react/display-name
const Select = forwardRef<any, Props>((props, ref) => {
const { theme } = useTheme();
const isDark = theme === "dark";
const colors: any = {
// primary: pink.pink9,
active: isDark ? purpleDark.purple9 : purple.purple9,
activeLight: isDark ? purpleDark.purple5 : purple.purple5,
primary: isDark ? mauveDark.mauve4 : mauve.mauve4,
secondary: isDark ? mauveDark.mauve8 : mauve.mauve8,
background: isDark ? mauveDark.mauve4 : mauve.mauve4,
searchText: isDark ? mauveDark.mauve12 : mauve.mauve12,
bg: isDark ? mauveDark.mauve1 : mauve.mauve1,
dropDownBg: isDark ? mauveDark.mauve5 : mauve.mauve2,
mauve4: isDark ? mauveDark.mauve4 : mauve.mauve4,
mauve5: isDark ? mauveDark.mauve5 : mauve.mauve5,
mauve8: isDark ? mauveDark.mauve8 : mauve.mauve8,
mauve9: isDark ? mauveDark.mauve9 : mauve.mauve9,
mauve12: isDark ? mauveDark.mauve12 : mauve.mauve12,
border: isDark ? mauveDark.mauve10 : mauve.mauve10,
placeholder: isDark ? mauveDark.mauve11 : mauve.mauve11,
};
colors.outline = colors.background;
colors.selected = colors.secondary;
return (
<SelectInput
ref={ref}
menuPosition={props.menuPosition || "fixed"}
styles={{
container: (provided) => {
return {
...provided,
position: "relative",
};
},
singleValue: (provided) => ({
...provided,
color: colors.mauve12,
}),
menu: (provided) => ({
...provided,
backgroundColor: colors.dropDownBg,
}),
control: (provided, state) => {
return {
...provided,
border: "0px",
backgroundColor: colors.mauve4,
boxShadow: `0 0 0 1px ${
state.isFocused ? colors.border : colors.secondary
}`,
};
},
input: (provided) => {
return {
...provided,
color: "$text",
};
},
multiValue: (provided) => {
return {
...provided,
backgroundColor: colors.mauve8,
};
},
multiValueLabel: (provided) => {
return {
...provided,
color: colors.mauve12,
};
},
multiValueRemove: (provided) => {
return {
...provided,
":hover": {
background: colors.mauve9,
},
};
},
option: (provided, state) => {
return {
...provided,
color: colors.searchText,
backgroundColor:
state.isSelected || state.isFocused
? colors.activeLight
: colors.dropDownBg,
":hover": {
backgroundColor: colors.active,
color: "#ffffff",
},
":selected": {
backgroundColor: "red",
},
};
},
indicatorSeparator: (provided) => {
return {
...provided,
backgroundColor: colors.secondary,
};
},
dropdownIndicator: (provided, state) => {
return {
...provided,
color: state.isFocused ? colors.border : colors.secondary,
":hover": {
color: colors.border,
},
};
},
}}
// theme={(theme) => ({
// ...theme,
// spacing: {
// ...theme.spacing,
// controlHeight: 30,
// },
// colors: {
// primary: colors.selected,
// primary25: colors.active,
// primary50: colors.primary,
// primary75: colors.primary,
// danger: colors.primary,
// dangerLight: colors.primary,
// neutral0: colors.background,
// neutral5: colors.primary,
// neutral10: colors.primary,
// neutral20: colors.outline,
// neutral30: colors.primary,
// neutral40: colors.primary,
// neutral50: colors.placeholder,
// neutral60: colors.primary,
// neutral70: colors.primary,
// neutral80: colors.searchText,
// neutral90: colors.primary,
// },
// })}
{...props}
/>
);
});
export default styled(Select, {});

View File

@@ -0,0 +1,257 @@
import React, { useState } from "react";
import { Plus, Trash, X } from "phosphor-react";
import Button from "./Button";
import Box from "./Box";
import { Stack, Flex, Select } from ".";
import {
Dialog,
DialogContent,
DialogTitle,
DialogDescription,
DialogClose,
DialogTrigger,
} from "./Dialog";
import { Input } from "./Input";
import {
Controller,
SubmitHandler,
useFieldArray,
useForm,
} from "react-hook-form";
import { TTS, tts } from "../utils/hookOnCalculator";
import { deployHook } from "../state/actions";
import type { IAccount } from "../state";
import { useSnapshot } from "valtio";
import state from "../state";
import toast from "react-hot-toast";
const transactionOptions = Object.keys(tts).map((key) => ({
label: key,
value: key as keyof TTS,
}));
export type SetHookData = {
Invoke: {
value: keyof TTS;
label: string;
}[];
HookParameters: {
HookParameter: {
HookParameterName: string;
HookParameterValue: string;
};
}[];
// HookGrants: {
// HookGrant: {
// Authorize: string;
// HookHash: string;
// };
// }[];
};
export const SetHookDialog: React.FC<{ account: IAccount }> = ({ account }) => {
const snap = useSnapshot(state);
const [isSetHookDialogOpen, setIsSetHookDialogOpen] = useState(false);
const {
register,
handleSubmit,
control,
// formState: { errors },
} = useForm<SetHookData>();
const { fields, append, remove } = useFieldArray({
control,
name: "HookParameters", // unique name for your Field Array
});
// const {
// fields: grantFields,
// append: grantAppend,
// remove: grantRemove,
// } = useFieldArray({
// control,
// name: "HookGrants", // unique name for your Field Array
// });
if (!account) {
return null;
}
const onSubmit: SubmitHandler<SetHookData> = async (data) => {
const currAccount = state.accounts.find(
(acc) => acc.address === account.address
);
if (currAccount) currAccount.isLoading = true;
const res = await deployHook(account, data);
if (currAccount) currAccount.isLoading = false;
if (res && res.engine_result === "tesSUCCESS") {
toast.success("Transaction succeeded!");
return setIsSetHookDialogOpen(false);
}
toast.error(`Transaction failed! (${res?.engine_result_message})`);
};
return (
<Dialog open={isSetHookDialogOpen} onOpenChange={setIsSetHookDialogOpen}>
<DialogTrigger asChild>
<Button
ghost
size="xs"
uppercase
variant={"secondary"}
disabled={
account.isLoading ||
!snap.files.filter((file) => file.compiledWatContent).length
}
>
Set Hook
</Button>
</DialogTrigger>
<DialogContent>
<form onSubmit={handleSubmit(onSubmit)}>
<DialogTitle>Deploy configuration</DialogTitle>
<DialogDescription as="div">
<Stack css={{ width: "100%", flex: 1 }}>
<Box css={{ width: "100%" }}>
<label>Invoke on transactions</label>
<Controller
name="Invoke"
control={control}
defaultValue={transactionOptions.filter(
(to) => to.label === "ttPAYMENT"
)}
render={({ field }) => (
<Select
{...field}
closeMenuOnSelect={false}
isMulti
menuPosition="fixed"
options={transactionOptions}
/>
)}
/>
</Box>
<Box css={{ width: "100%" }}>
<label style={{ marginBottom: "10px", display: "block" }}>
Hook parameters
</label>
<Stack>
{fields.map((field, index) => (
<Stack key={field.id}>
<Input
// important to include key with field's id
placeholder="Parameter name"
{...register(
`HookParameters.${index}.HookParameter.HookParameterName`
)}
/>
<Input
placeholder="Parameter value"
{...register(
`HookParameters.${index}.HookParameter.HookParameterValue`
)}
/>
<Button onClick={() => remove(index)} variant="destroy">
<Trash weight="regular" size="16px" />
</Button>
</Stack>
))}
<Button
outline
fullWidth
type="button"
onClick={() =>
append({
HookParameter: {
HookParameterName: "",
HookParameterValue: "",
},
})
}
>
<Plus size="16px" />
Add Hook Parameter
</Button>
</Stack>
</Box>
{/* <Box css={{ width: "100%" }}>
<label style={{ marginBottom: "10px", display: "block" }}>
Hook Grants
</label>
<Stack>
{grantFields.map((field, index) => (
<Stack key={field.id}>
<Input
// important to include key with field's id
placeholder="Authorize"
{...register(
`HookGrants.${index}.HookGrant.Authorize`,
{ minLength: 5 }
)}
/>
<Input
placeholder="HookHash"
{...register(`HookGrants.${index}.HookGrant.HookHash`, {
minLength: 64,
maxLength: 64,
})}
/>
<Button
onClick={() => grantRemove(index)}
variant="destroy"
>
<Trash weight="regular" size="16px" />
</Button>
</Stack>
))}
<Button
outline
fullWidth
type="button"
onClick={() =>
grantAppend({
HookGrant: {
Authorize: "",
HookHash: "",
},
})
}
>
<Plus size="16px" />
Add Hook Grant
</Button>
</Stack>
</Box> */}
</Stack>
</DialogDescription>
<Flex
css={{
marginTop: 25,
justifyContent: "flex-end",
gap: "$3",
}}
>
<DialogClose asChild>
<Button outline>Cancel</Button>
</DialogClose>
{/* <DialogClose asChild> */}
<Button
variant="primary"
type="submit"
isLoading={account.isLoading}
>
Set Hook
</Button>
{/* </DialogClose> */}
</Flex>
<DialogClose asChild>
<Box css={{ position: "absolute", top: "$3", right: "$3" }}>
<X size="20px" />
</Box>
</DialogClose>
</form>
</DialogContent>
</Dialog>
);
};
export default SetHookDialog;

View File

@@ -1,8 +1,5 @@
import { Children } from "react";
import Box from "./Box";
import { styled } from "../stitches.config";
import type * as Stitches from "@stitches/react";
const StackComponent = styled(Box, {
display: "flex",

257
components/Tabs.tsx Normal file
View File

@@ -0,0 +1,257 @@
import React, {
useEffect,
useState,
Fragment,
isValidElement,
useCallback,
} from "react";
import type { ReactNode, ReactElement } from "react";
import { Box, Button, Flex, Input, Stack, Text } from ".";
import {
Dialog,
DialogTrigger,
DialogContent,
DialogTitle,
DialogDescription,
DialogClose,
} from "./Dialog";
import { Plus, X } from "phosphor-react";
import { styled } from "../stitches.config";
const ErrorText = styled(Text, {
color: "$error",
mt: "$1",
display: "block",
});
interface TabProps {
header?: string;
children: ReactNode;
}
// TODO customise strings shown
interface Props {
activeIndex?: number;
activeHeader?: string;
headless?: boolean;
children: ReactElement<TabProps>[];
keepAllAlive?: boolean;
defaultExtension?: string;
forceDefaultExtension?: boolean;
onCreateNewTab?: (name: string) => any;
onCloseTab?: (index: number, header?: string) => any;
}
export const Tab = (props: TabProps) => null;
export const Tabs = ({
children,
activeIndex,
activeHeader,
headless,
keepAllAlive = false,
onCreateNewTab,
onCloseTab,
defaultExtension = "",
forceDefaultExtension,
}: Props) => {
const [active, setActive] = useState(activeIndex || 0);
const tabs: TabProps[] = children.map((elem) => elem.props);
const [isNewtabDialogOpen, setIsNewtabDialogOpen] = useState(false);
const [tabname, setTabname] = useState("");
const [newtabError, setNewtabError] = useState<string | null>(null);
useEffect(() => {
if (activeIndex) setActive(activeIndex);
}, [activeIndex]);
useEffect(() => {
if (activeHeader) {
const idx = tabs.findIndex((tab) => tab.header === activeHeader);
setActive(idx);
}
}, [activeHeader, tabs]);
// when filename changes, reset error
useEffect(() => {
setNewtabError(null);
}, [tabname, setNewtabError]);
const validateTabname = useCallback(
(tabname: string): { error: string | null } => {
if (tabs.find((tab) => tab.header === tabname)) {
return { error: "Name already exists." };
}
return { error: null };
},
[tabs]
);
const handleCreateTab = useCallback(() => {
// add default extension in case omitted
let _tabname = tabname.includes(".") ? tabname : tabname + defaultExtension;
if (forceDefaultExtension && !_tabname.endsWith(defaultExtension)) {
_tabname = _tabname + defaultExtension;
}
const chk = validateTabname(_tabname);
if (chk.error) {
setNewtabError(`Error: ${chk.error}`);
return;
}
setIsNewtabDialogOpen(false);
setTabname("");
// switch to new tab?
setActive(tabs.length);
onCreateNewTab?.(_tabname);
}, [tabname, defaultExtension, validateTabname, onCreateNewTab, tabs.length]);
const handleCloseTab = useCallback(
(idx: number) => {
if (idx <= active && active !== 0) {
setActive(active - 1);
}
onCloseTab?.(idx, tabs[idx].header);
},
[active, onCloseTab, tabs]
);
return (
<>
{!headless && (
<Stack
css={{
gap: "$3",
flex: 1,
flexWrap: "nowrap",
marginBottom: "-1px",
width: "100%",
overflow: "auto",
}}
>
{tabs.map((tab, idx) => (
<Button
key={tab.header}
role="tab"
tabIndex={idx}
onClick={() => setActive(idx)}
onKeyPress={() => setActive(idx)}
outline={active !== idx}
size="sm"
css={{
"&:hover": {
span: {
visibility: "visible",
},
},
}}
>
{tab.header || idx}
{onCloseTab && (
<Box
as="span"
css={{
display: "flex",
p: "2px",
borderRadius: "$full",
mr: "-4px",
"&:hover": {
// boxSizing: "0px 0px 1px",
backgroundColor: "$mauve2",
color: "$mauve12",
},
}}
onClick={(ev: React.MouseEvent<HTMLElement>) => {
ev.stopPropagation();
handleCloseTab(idx);
}}
>
<X size="9px" weight="bold" />
</Box>
)}
</Button>
))}
{onCreateNewTab && (
<Dialog
open={isNewtabDialogOpen}
onOpenChange={setIsNewtabDialogOpen}
>
<DialogTrigger asChild>
<Button
ghost
size="sm"
css={{ alignItems: "center", px: "$2", mr: "$3" }}
>
<Plus size="16px" /> {tabs.length === 0 && "Add new tab"}
</Button>
</DialogTrigger>
<DialogContent>
<DialogTitle>Create new tab</DialogTitle>
<DialogDescription>
<label>Tabname</label>
<Input
value={tabname}
onChange={(e) => setTabname(e.target.value)}
onKeyPress={(e) => {
if (e.key === "Enter") {
handleCreateTab();
}
}}
/>
<ErrorText>{newtabError}</ErrorText>
</DialogDescription>
<Flex
css={{
marginTop: 25,
justifyContent: "flex-end",
gap: "$3",
}}
>
<DialogClose asChild>
<Button outline>Cancel</Button>
</DialogClose>
<Button variant="primary" onClick={handleCreateTab}>
Create
</Button>
</Flex>
<DialogClose asChild>
<Box css={{ position: "absolute", top: "$3", right: "$3" }}>
<X size="20px" />
</Box>
</DialogClose>
</DialogContent>
</Dialog>
)}
</Stack>
)}
{keepAllAlive ? (
tabs.map((tab, idx) => {
// TODO Maybe rule out fragments as children
if (!isValidElement(tab.children)) {
if (active !== idx) return null;
return tab.children;
}
let key = tab.children.key || tab.header || idx;
let { children } = tab;
let { style, ...props } = children.props;
return (
<children.type
key={key}
{...props}
style={{ ...style, display: active !== idx ? "none" : undefined }}
/>
);
})
) : (
<Fragment key={tabs[active].header || active}>
{tabs[active].children}
</Fragment>
)}
</>
);
};

View File

@@ -4,6 +4,23 @@ const Text = styled("span", {
fontFamily: "$body",
lineHeight: "$body",
color: "$text",
variants: {
small: {
true: {
fontSize: '$xs'
}
},
muted: {
true: {
color: '$mauve9'
}
},
monospace: {
true: {
fontFamily: '$monospace'
}
}
}
});
export default Text;

View File

@@ -2,7 +2,7 @@ import { useState, useEffect } from "react";
import { useTheme } from "next-themes";
import { Sun, Moon } from "phosphor-react";
import Box from "./Box";
import Button from "./Button";
const ThemeChanger = () => {
const { theme, setTheme } = useTheme();
@@ -12,7 +12,8 @@ const ThemeChanger = () => {
if (!mounted) return null;
return (
<Box
<Button
outline
onClick={() => {
setTheme(theme && theme === "light" ? "dark" : "light");
}}
@@ -25,12 +26,8 @@ const ThemeChanger = () => {
color: "$muted",
}}
>
{theme === "dark" ? (
<Sun weight="bold" size="16px" />
) : (
<Moon weight="bold" size="16px" />
)}
</Box>
{theme === "dark" ? <Sun size="15px" /> : <Moon size="15px" />}
</Button>
);
};

89
components/Tooltip.tsx Normal file
View File

@@ -0,0 +1,89 @@
import React from "react";
import { styled, keyframes } from "../stitches.config";
import * as TooltipPrimitive from "@radix-ui/react-tooltip";
const slideUpAndFade = keyframes({
"0%": { opacity: 0, transform: "translateY(2px)" },
"100%": { opacity: 1, transform: "translateY(0)" },
});
const slideRightAndFade = keyframes({
"0%": { opacity: 0, transform: "translateX(-2px)" },
"100%": { opacity: 1, transform: "translateX(0)" },
});
const slideDownAndFade = keyframes({
"0%": { opacity: 0, transform: "translateY(-2px)" },
"100%": { opacity: 1, transform: "translateY(0)" },
});
const slideLeftAndFade = keyframes({
"0%": { opacity: 0, transform: "translateX(2px)" },
"100%": { opacity: 1, transform: "translateX(0)" },
});
const StyledContent = styled(TooltipPrimitive.Content, {
borderRadius: 4,
padding: "$2 $3",
fontSize: 12,
lineHeight: 1,
color: "$text",
backgroundColor: "$background",
boxShadow:
"hsl(206 22% 7% / 35%) 0px 10px 38px -10px, hsl(206 22% 7% / 20%) 0px 10px 20px -15px",
"@media (prefers-reduced-motion: no-preference)": {
animationDuration: "400ms",
animationTimingFunction: "cubic-bezier(0.16, 1, 0.3, 1)",
animationFillMode: "forwards",
willChange: "transform, opacity",
'&[data-state="delayed-open"]': {
'&[data-side="top"]': { animationName: slideDownAndFade },
'&[data-side="right"]': { animationName: slideLeftAndFade },
'&[data-side="bottom"]': { animationName: slideUpAndFade },
'&[data-side="left"]': { animationName: slideRightAndFade },
},
},
".dark &": {
boxShadow:
"0px 0px 10px 2px rgba(255,255,255,.15), hsl(206 22% 7% / 35%) 0px 10px 38px -10px, hsl(206 22% 7% / 20%) 0px 10px 20px -15px",
},
".light &": {
boxShadow:
"0px 0px 10px 2px rgba(0,0,0,.15), hsl(206 22% 7% / 35%) 0px 10px 38px -10px, hsl(206 22% 7% / 20%) 0px 10px 20px -15px",
},
});
const StyledArrow = styled(TooltipPrimitive.Arrow, {
fill: "$background",
});
interface ITooltip {
content: string;
open?: boolean;
defaultOpen?: boolean;
onOpenChange?: (open: boolean) => void;
}
const Tooltip: React.FC<ITooltip> = ({
children,
content,
open,
defaultOpen = false,
onOpenChange,
}) => {
return (
<TooltipPrimitive.Root
open={open}
defaultOpen={defaultOpen}
onOpenChange={onOpenChange}
>
<TooltipPrimitive.Trigger asChild>{children}</TooltipPrimitive.Trigger>
<StyledContent side="bottom" align="center">
{content}
<StyledArrow offset={5} width={11} height={5} />
</StyledContent>
</TooltipPrimitive.Root>
);
};
export default Tooltip;

17
components/index.tsx Normal file
View File

@@ -0,0 +1,17 @@
export { default as Flex } from "./Flex";
export { default as Link } from "./Link";
export { default as Container } from "./Container";
export { default as Heading } from "./Heading";
export { default as Stack } from "./Stack";
export { default as Text } from "./Text";
export { default as Input } from "./Input";
export { default as Select } from "./Select";
export * from "./Tabs";
export * from "./AlertDialog";
export { default as Box } from "./Box";
export { default as Button } from "./Button";
export { default as Pre } from "./Pre";
export { default as ButtonGroup } from "./ButtonGroup";
export { default as DeployFooter } from "./DeployFooter";
export * from "./Dialog";
export * from "./DropdownMenu";

221
content/transactions.json Normal file
View File

@@ -0,0 +1,221 @@
[
{
"TransactionType": "AccountDelete",
"Account": "rWYkbWkCeg8dP6rXALnjgZSjjLyih5NXm",
"Destination": "rPT1Sjq2YGrBMTttX4GZHjKu9dyfzbpAYe",
"DestinationTag": 13,
"Fee": "2000000",
"Sequence": 2470665,
"Flags": 2147483648
},
{
"TransactionType": "AccountSet",
"Account": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn",
"Fee": "12",
"Sequence": 5,
"Domain": "6578616D706C652E636F6D",
"SetFlag": 5,
"MessageKey": "03AB40A0490F9B7ED8DF29D246BF2D6269820A0EE7742ACDD457BEA7C7D0931EDB"
},
{
"Account": "rUn84CUYbNjRoTQ6mSW7BVJPSVJNLb1QLo",
"TransactionType": "CheckCancel",
"CheckID": "49647F0D748DC3FE26BDACBC57F251AADEFFF391403EC9BF87C97F67E9977FB0",
"Fee": "12"
},
{
"Account": "rfkE1aSy9G8Upk4JssnwBxhEv5p4mn2KTy",
"TransactionType": "CheckCash",
"Amount": {
"value": "100",
"type": "currency"
},
"CheckID": "838766BA2B995C00744175F69A1B11E32C3DBC40E64801A4056FCBD657F57334",
"Fee": "12"
},
{
"TransactionType": "CheckCreate",
"Account": "rUn84CUYbNjRoTQ6mSW7BVJPSVJNLb1QLo",
"Destination": "rfkE1aSy9G8Upk4JssnwBxhEv5p4mn2KTy",
"SendMax": "100000000",
"Expiration": 570113521,
"InvoiceID": "6F1DFD1D0FE8A32E40E1F2C05CF1C15545BAB56B617F9C6C2D63A6B704BEF59B",
"DestinationTag": 1,
"Fee": "12"
},
{
"TransactionType": "DepositPreauth",
"Account": "rsUiUMpnrgxQp24dJYZDhmV4bE3aBtQyt8",
"Authorize": "rEhxGqkqPPSxQ3P25J66ft5TwpzV14k2de",
"Fee": "10",
"Flags": 2147483648,
"Sequence": 2
},
{
"Account": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn",
"TransactionType": "EscrowCancel",
"Owner": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn",
"OfferSequence": 7
},
{
"Account": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn",
"TransactionType": "EscrowCreate",
"Amount": {
"value": "100",
"type": "currency"
},
"Destination": "rsA2LpzuawewSBQXkiju3YQTMzW13pAAdW",
"CancelAfter": 533257958,
"FinishAfter": 533171558,
"Condition": "A0258020E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855810100",
"DestinationTag": 23480,
"SourceTag": 11747
},
{
"Account": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn",
"TransactionType": "EscrowFinish",
"Owner": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn",
"OfferSequence": 7,
"Condition": "A0258020E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855810100",
"Fulfillment": "A0028000"
},
{
"TransactionType": "NFTokenBurn",
"Account": "rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B",
"Fee": "10",
"TokenID": "000B013A95F14B0044F78A264E41713C64B5F89242540EE208C3098E00000D65"
},
{
"TransactionType": "NFTokenAcceptOffer",
"Fee": "10"
},
{
"TransactionType": "NFTokenCancelOffer",
"Account": "ra5nK24KXen9AHvsdFTKHSANinZseWnPcX",
"TokenIDs": "000100001E962F495F07A990F4ED55ACCFEEF365DBAA76B6A048C0A200000007"
},
{
"TransactionType": "NFTokenCreateOffer",
"Account": "rs8jBmmfpwgmrSPgwMsh7CvKRmRt1JTVSX",
"TokenID": "000100001E962F495F07A990F4ED55ACCFEEF365DBAA76B6A048C0A200000007",
"Amount": {
"value": "100",
"type": "currency"
},
"Flags": 1
},
{
"TransactionType": "OfferCancel",
"Account": "ra5nK24KXen9AHvsdFTKHSANinZseWnPcX",
"Fee": "12",
"Flags": 0,
"LastLedgerSequence": 7108629,
"OfferSequence": 6,
"Sequence": 7
},
{
"TransactionType": "OfferCreate",
"Account": "ra5nK24KXen9AHvsdFTKHSANinZseWnPcX",
"Fee": "12",
"Flags": 0,
"LastLedgerSequence": 7108682,
"Sequence": 8,
"TakerGets": "6000000",
"Amount": {
"value": "100",
"type": "currency"
}
},
{
"TransactionType": "Payment",
"Account": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn",
"Destination": "ra5nK24KXen9AHvsdFTKHSANinZseWnPcX",
"Amount": {
"value": "100",
"type": "currency"
},
"Fee": "12",
"Flags": 2147483648,
"Sequence": 2
},
{
"Account": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn",
"TransactionType": "PaymentChannelCreate",
"Amount": {
"value": "100",
"type": "currency"
},
"Destination": "rsA2LpzuawewSBQXkiju3YQTMzW13pAAdW",
"SettleDelay": 86400,
"PublicKey": "32D2471DB72B27E3310F355BB33E339BF26F8392D5A93D3BC0FC3B566612DA0F0A",
"CancelAfter": 533171558,
"DestinationTag": 23480,
"SourceTag": 11747
},
{
"Account": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn",
"TransactionType": "PaymentChannelFund",
"Channel": "C1AE6DDDEEC05CF2978C0BAD6FE302948E9533691DC749DCDD3B9E5992CA6198",
"Amount": {
"value": "200",
"type": "currency"
},
"Expiration": 543171558
},
{
"Flags": 0,
"TransactionType": "SetRegularKey",
"Account": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn",
"Fee": "12",
"RegularKey": "rAR8rR8sUkBoCZFawhkWzY4Y5YoyuznwD"
},
{
"Flags": 0,
"TransactionType": "SignerListSet",
"Account": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn",
"Fee": "12",
"SignerQuorum": 3,
"SignerEntries": {
"type": "json",
"value": [
{
"SignerEntry": {
"Account": "rsA2LpzuawewSBQXkiju3YQTMzW13pAAdW",
"SignerWeight": 2
}
},
{
"SignerEntry": {
"Account": "rUpy3eEg8rqjqfUoLeBnZkscbKbFsKXC3v",
"SignerWeight": 1
}
},
{
"SignerEntry": {
"Account": "raKEEVSGnKSD9Zyvxu4z6Pqpm4ABH8FS6n",
"SignerWeight": 1
}
}
]
}
},
{
"TransactionType": "TicketCreate",
"Account": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn",
"Fee": "10",
"Sequence": 381,
"TicketCount": 10
},
{
"TransactionType": "TrustSet",
"Account": "ra5nK24KXen9AHvsdFTKHSANinZseWnPcX",
"Fee": "12",
"Flags": 262144,
"LastLedgerSequence": 8007750,
"Amount": {
"value": "100",
"type": "currency"
},
"Sequence": 12
}
]

1
next-env.d.ts vendored
View File

@@ -1,5 +1,4 @@
/// <reference types="next" />
/// <reference types="next/types/global" />
/// <reference types="next/image-types/global" />
// NOTE: This file should not be edited

View File

@@ -2,6 +2,19 @@
module.exports = {
reactStrictMode: true,
images: {
domains: ['avatars.githubusercontent.com'],
domains: ["avatars.githubusercontent.com"],
},
}
webpack(config, { isServer }) {
config.resolve.alias["vscode"] = require.resolve(
"@codingame/monaco-languageclient/lib/vscode-compatibility"
);
if (!isServer) {
config.resolve.fallback.fs = false;
}
config.module.rules.push({
test: /\.md$/,
use: "raw-loader",
});
return config;
},
};

View File

@@ -6,30 +6,69 @@
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint"
"lint": "next lint",
"postinstall": "patch-package"
},
"dependencies": {
"@codingame/monaco-jsonrpc": "^0.3.1",
"@codingame/monaco-languageclient": "^0.17.0",
"@monaco-editor/react": "^4.3.1",
"@octokit/core": "^3.5.1",
"@radix-ui/colors": "^0.1.7",
"@radix-ui/react-alert-dialog": "^0.1.1",
"@radix-ui/react-dialog": "^0.1.1",
"@radix-ui/react-dropdown-menu": "^0.1.1",
"@stitches/react": "^1.2.5",
"monaco-editor": "^0.29.1",
"@radix-ui/react-id": "^0.1.1",
"@radix-ui/react-tooltip": "^0.1.7",
"@stitches/react": "^1.2.6-0",
"base64-js": "^1.5.1",
"dinero.js": "^1.9.1",
"file-saver": "^2.0.5",
"filesize": "^8.0.7",
"javascript-time-ago": "^2.3.11",
"jszip": "^3.7.1",
"lodash.uniqby": "^4.7.0",
"lodash.xor": "^4.5.0",
"monaco-editor": "^0.30.1",
"next": "^12.0.4",
"next-auth": "^4.0.0-beta.5",
"next-themes": "^0.0.15",
"normalize-url": "^7.0.2",
"octokit": "^1.7.0",
"pako": "^2.0.4",
"patch-package": "^6.4.7",
"phosphor-react": "^1.3.1",
"postinstall-postinstall": "^2.1.0",
"re-resizable": "^6.9.1",
"react": "17.0.2",
"react-dom": "17.0.2",
"react-hook-form": "^7.28.0",
"react-hot-keys": "^2.7.1",
"react-hot-toast": "^2.1.1",
"valtio": "^1.2.5"
"react-new-window": "^0.2.1",
"react-select": "^5.2.1",
"react-split": "^2.0.14",
"react-stay-scrolled": "^7.4.0",
"react-time-ago": "^7.1.9",
"reconnecting-websocket": "^4.4.0",
"regexify-string": "^1.0.17",
"valtio": "^1.2.5",
"vscode-languageserver": "^7.0.0",
"vscode-uri": "^3.0.2",
"wabt": "1.0.16",
"xrpl-accountlib": "^1.3.2",
"xrpl-client": "^1.9.4"
},
"devDependencies": {
"@types/dinero.js": "^1.9.0",
"@types/file-saver": "^2.0.4",
"@types/lodash.uniqby": "^4.7.6",
"@types/lodash.xor": "^4.5.6",
"@types/pako": "^1.0.2",
"@types/react": "17.0.31",
"eslint": "7.32.0",
"eslint-config-next": "11.1.2",
"raw-loader": "^4.0.2",
"typescript": "4.4.4"
}
}

View File

@@ -1,30 +1,148 @@
import { useEffect } from "react";
import "../styles/globals.css";
import type { AppProps } from "next/app";
import Head from "next/head";
import { SessionProvider } from "next-auth/react";
import { ThemeProvider } from "next-themes";
import { Toaster } from "react-hot-toast";
import { useRouter } from "next/router";
import { IdProvider } from "@radix-ui/react-id";
import { darkTheme } from "../stitches.config";
import { darkTheme, css } from "../stitches.config";
import Navigation from "../components/Navigation";
import { fetchFiles } from "../state/actions";
import state from "../state";
import TimeAgo from "javascript-time-ago";
import en from "javascript-time-ago/locale/en.json";
import { useSnapshot } from "valtio";
TimeAgo.setDefaultLocale(en.locale);
TimeAgo.addLocale(en)
function MyApp({ Component, pageProps: { session, ...pageProps } }: AppProps) {
const router = useRouter();
const slug = router.query?.slug;
const gistId = (Array.isArray(slug) && slug[0]) ?? null;
const origin = "https://xrpl-hooks-ide.vercel.app"; // TODO: Change when site is deployed
const shareImg = "/share-image.png";
const snap = useSnapshot(state);
useEffect(() => {
if (gistId && router.isReady) {
fetchFiles(gistId);
} else {
if (
!gistId &&
router.isReady &&
!router.pathname.includes("/sign-in") &&
!snap.files.length &&
!snap.mainModalShowed
) {
state.mainModalOpen = true;
state.mainModalShowed = true;
}
}
}, [
gistId,
router.isReady,
router.pathname,
snap.files,
snap.mainModalShowed,
]);
return (
<>
<SessionProvider session={session}>
<ThemeProvider
attribute="class"
defaultTheme="dark"
enableSystem={false}
value={{
light: "light",
dark: darkTheme.className,
}}
>
<Navigation />
<Component {...pageProps} />
<Toaster />
</ThemeProvider>
</SessionProvider>
<Head>
<meta charSet="utf-8" />
<meta name="viewport" content="initial-scale=1.0, width=device-width" />
<meta name="format-detection" content="telephone=no" />
<meta property="og:url" content={`${origin}${router.asPath}`} />
<title>XRPL Hooks Editor</title>
<meta property="og:title" content="XRPL Hooks Editor" />
<meta name="twitter:title" content="XRPL Hooks Editor" />
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:site" content="@xrpllabs" />
<meta
name="description"
content="Playground for buildings Hooks, that add smart contract functionality to the XRP Ledger."
/>
<meta
property="og:description"
content="Playground for buildings Hooks, that add smart contract functionality to the XRP Ledger."
/>
<meta
name="twitter:description"
content="Playground for buildings Hooks, that add smart contract functionality to the XRP Ledger.."
/>
<meta property="og:image" content={`${origin}${shareImg}`} />
<meta property="og:image:width" content="1200" />
<meta property="og:image:height" content="630" />
<meta name="twitter:image" content={`${origin}${shareImg}`} />
<link
rel="apple-touch-icon"
sizes="180x180"
href="/apple-touch-icon.png"
/>
<link
rel="icon"
type="image/png"
sizes="32x32"
href="/favicon-32x32.png"
/>
<link
rel="icon"
type="image/png"
sizes="16x16"
href="/favicon-16x16.png"
/>
<link rel="manifest" href="/site.webmanifest" />
<link rel="mask-icon" href="/safari-pinned-tab.svg" color="#161618" />
<meta name="application-name" content="XRPL Hooks Editor" />
<meta name="msapplication-TileColor" content="#c10ad0" />
<meta
name="theme-color"
content="#161618"
media="(prefers-color-scheme: dark)"
/>
<meta
name="theme-color"
content="#FDFCFD"
media="(prefers-color-scheme: light)"
/>
</Head>
<IdProvider>
<SessionProvider session={session}>
<ThemeProvider
attribute="class"
defaultTheme="dark"
enableSystem={false}
value={{
light: "light",
dark: darkTheme.className,
}}
>
<Navigation />
<Component {...pageProps} />
<Toaster
toastOptions={{
className: css({
backgroundColor: "$mauve1",
color: "$mauve10",
fontSize: "$sm",
zIndex: 9999,
".dark &": {
backgroundColor: "$mauve4",
color: "$mauve12",
},
})(),
}}
/>
</ThemeProvider>
</SessionProvider>
</IdProvider>
</>
);
}

View File

@@ -16,11 +16,14 @@ class MyDocument extends Document {
}
render() {
globalStyles();
return (
<Html>
<Head>
<meta name="description" content="Playground for XRPL Hooks" />
<link rel="icon" href="/favicon.ico" />
<style
id="stitches"
dangerouslySetInnerHTML={{ __html: getCssText() }}
/>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link
rel="preconnect"
@@ -31,10 +34,6 @@ class MyDocument extends Document {
href="https://fonts.googleapis.com/css2?family=Roboto+Mono:ital@0;1&family=Work+Sans:wght@400;600;700&display=swap"
rel="stylesheet"
/>
<style
id="stitches"
dangerouslySetInnerHTML={{ __html: getCssText() }}
/>
</Head>
<body>
<Main />

View File

@@ -1,11 +1,12 @@
import type { NextRequest, NextFetchEvent } from 'next/server';
import { NextResponse as Response } from 'next/server';
import { getToken } from "next-auth/jwt"
export default function middleware(req: NextRequest, ev: NextFetchEvent) {
if (req.nextUrl.pathname === "/") {
return Response.redirect("/develop");
const url = req.nextUrl.clone();
url.pathname = '/develop';
return Response.redirect(url);
}
}

View File

@@ -1,5 +1,4 @@
import NextAuth from "next-auth"
import GithubProvider from "next-auth/providers/github"
export default NextAuth({
// Configure one or more authentication providers
@@ -42,7 +41,6 @@ export default NextAuth({
},
async session({ session, token }) {
session.accessToken = token.accessToken as string;
const user = { ...session.user, username: token.username };
session['user']['username'] = token.username as string;
return session
}

43
pages/api/faucet.ts Normal file
View File

@@ -0,0 +1,43 @@
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
import type { NextApiRequest, NextApiResponse } from 'next'
interface ErrorResponse {
error: string
}
export interface Faucet {
address: string;
secret: string;
xrp: number;
hash: string;
code: string;
}
export default async function handler(
req: NextApiRequest,
res: NextApiResponse<Faucet | ErrorResponse>
) {
if (req.method !== 'POST') {
return res.status(405).json({ error: 'Method not allowed!' })
}
const { account } = req.query;
const ip = Array.isArray(req?.headers?.["x-real-ip"]) ? req?.headers?.["x-real-ip"][0] : req?.headers?.["x-real-ip"];
try {
const response = await fetch(`https://${process.env.NEXT_PUBLIC_TESTNET_URL}/newcreds?account=${account ? account : ''}`, {
method: 'POST',
headers: {
'x-forwarded-for': ip || '',
},
});
const json: Faucet | ErrorResponse = await response.json();
if ("error" in json) {
return res.status(429).json(json)
}
return res.status(200).json(json);
} catch (err) {
console.log(err)
return res.status(500).json({ error: 'Server error' })
}
return res.status(500).json({ error: 'Not able to create faucet, try again' })
}

View File

@@ -0,0 +1,63 @@
import dynamic from "next/dynamic";
import React from "react";
import Split from "react-split";
import { useSnapshot } from "valtio";
import state from "../../state";
import { getSplit, saveSplit } from "../../state/actions/persistSplits";
const DeployEditor = dynamic(() => import("../../components/DeployEditor"), {
ssr: false,
});
const Accounts = dynamic(() => import("../../components/Accounts"), {
ssr: false,
});
const LogBox = dynamic(() => import("../../components/LogBox"), {
ssr: false,
});
const Deploy = () => {
const { deployLogs } = useSnapshot(state);
return (
<Split
direction="vertical"
gutterSize={4}
gutterAlign="center"
sizes={getSplit("deployVertical") || [40, 60]}
style={{ height: "calc(100vh - 60px)" }}
onDragEnd={(e) => saveSplit("deployVertical", e)}
>
<main style={{ display: "flex", flex: 1, position: "relative" }}>
<DeployEditor />
</main>
<Split
direction="horizontal"
sizes={getSplit("deployHorizontal") || [50, 50]}
minSize={[320, 160]}
gutterSize={4}
gutterAlign="center"
style={{
display: "flex",
flexDirection: "row",
width: "100%",
height: "100%",
}}
onDragEnd={(e) => saveSplit("deployHorizontal", e)}
>
<div style={{ alignItems: "stretch", display: "flex" }}>
<Accounts />
</div>
<div>
<LogBox
title="Deploy Log"
logs={deployLogs}
clearLog={() => (state.deployLogs = [])}
/>
</div>
</Split>
</Split>
);
};
export default Deploy;

View File

@@ -1,9 +0,0 @@
import Container from "../../components/Container";
const Deploy = () => {
return (
<Container css={{ py: "$10" }}>This will be the deploy page</Container>
);
};
export default Deploy;

View File

@@ -0,0 +1,83 @@
import type { NextPage } from "next";
import dynamic from "next/dynamic";
import { Play } from "phosphor-react";
import Hotkeys from "react-hot-keys";
import Split from "react-split";
import { useSnapshot } from "valtio";
import Box from "../../components/Box";
import Button from "../../components/Button";
import state from "../../state";
import { compileCode } from "../../state/actions";
import { getSplit, saveSplit } from "../../state/actions/persistSplits";
const HooksEditor = dynamic(() => import("../../components/HooksEditor"), {
ssr: false,
});
const LogBox = dynamic(() => import("../../components/LogBox"), {
ssr: false,
});
const Home: NextPage = () => {
const snap = useSnapshot(state);
return (
<Split
direction="vertical"
sizes={getSplit("developVertical") || [70, 30]}
minSize={[100, 100]}
gutterAlign="center"
gutterSize={4}
style={{ height: "calc(100vh - 60px)" }}
onDragEnd={(e) => saveSplit("developVertical", e)}
>
<main style={{ display: "flex", flex: 1, position: "relative" }}>
<HooksEditor />
{snap.files[snap.active]?.name?.split(".")?.[1].toLowerCase() ===
"c" && (
<Hotkeys
keyName="command+b,ctrl+b"
onKeyDown={() =>
!snap.compiling && snap.files.length && compileCode(snap.active)
}
>
<Button
variant="primary"
uppercase
disabled={!snap.files.length}
isLoading={snap.compiling}
onClick={() => compileCode(snap.active)}
css={{
position: "absolute",
bottom: "$4",
left: "$4",
alignItems: "center",
display: "flex",
cursor: "pointer",
}}
>
<Play weight="bold" size="16px" />
Compile to Wasm
</Button>
</Hotkeys>
)}
</main>
<Box
css={{
display: "flex",
background: "$mauve1",
position: "relative",
}}
>
<LogBox
title="Development Log"
clearLog={() => (state.logs = [])}
logs={snap.logs}
/>
</Box>
</Split>
);
};
export default Home;

View File

@@ -1,27 +0,0 @@
import type { NextPage } from "next";
import Head from "next/head";
import dynamic from "next/dynamic";
const HooksEditor = dynamic(() => import("../../components/HooksEditor"), {
ssr: false,
});
const Footer = dynamic(() => import("../../components/Footer"), {
ssr: false,
});
const Home: NextPage = () => {
return (
<>
<Head>
<title>XRPL Hooks Playground</title>
</Head>
<main style={{ display: "flex", flex: 1 }}>
<HooksEditor />
</main>
<Footer />
</>
);
};
export default Home;

38
pages/sign-in.tsx Normal file
View File

@@ -0,0 +1,38 @@
import { useEffect } from "react";
import { signIn, useSession } from "next-auth/react";
import Box from "../components/Box";
import Spinner from "../components/Spinner";
const SignInPage = () => {
const { data: session, status } = useSession();
useEffect(() => {
if (status !== "loading" && !session)
void signIn("github", { redirect: false });
if (status !== "loading" && session) window.close();
}, [session, status]);
return (
<Box
css={{
display: "flex",
backgroundColor: "$mauve1",
position: "absolute",
top: 0,
right: 0,
bottom: 0,
left: 0,
zIndex: 9999,
textAlign: "center",
justifyContent: "center",
alignItems: "center",
gap: "$2",
}}
>
Logging in <Spinner />
</Box>
);
};
export default SignInPage;

474
pages/test/[[...slug]].tsx Normal file
View File

@@ -0,0 +1,474 @@
import dynamic from "next/dynamic";
import { Play } from "phosphor-react";
import { FC, useCallback, useEffect, useState } from "react";
import Split from "react-split";
import { useSnapshot } from "valtio";
import {
Box,
Button,
Container,
Flex,
Input,
Select,
Tab,
Tabs,
Text,
} from "../../components";
import transactionsData from "../../content/transactions.json";
import state from "../../state";
import { sendTransaction } from "../../state/actions";
import { getSplit, saveSplit } from "../../state/actions/persistSplits";
import { streamState } from "../../components/DebugStream";
const DebugStream = dynamic(() => import("../../components/DebugStream"), {
ssr: false,
});
const LogBox = dynamic(() => import("../../components/LogBox"), {
ssr: false,
});
const Accounts = dynamic(() => import("../../components/Accounts"), {
ssr: false,
});
// type SelectOption<T> = { value: T, label: string };
type TxFields = Omit<
typeof transactionsData[0],
"Account" | "Sequence" | "TransactionType"
>;
type OtherFields = (keyof Omit<TxFields, "Destination">)[];
interface AccountOption {
label: string;
value: string;
}
interface Props {
header?: string;
}
const Transaction: FC<Props> = ({ header, ...props }) => {
const snap = useSnapshot(state);
const transactionsOptions = transactionsData.map(tx => ({
value: tx.TransactionType,
label: tx.TransactionType,
}));
const [selectedTransaction, setSelectedTransaction] = useState<
typeof transactionsOptions[0] | null
>(null);
const accountOptions: AccountOption[] = snap.accounts.map(acc => ({
label: acc.name,
value: acc.address,
}));
const [selectedAccount, setSelectedAccount] = useState<AccountOption | null>(
null
);
const destAccountOptions: AccountOption[] = snap.accounts
.map(acc => ({
label: acc.name,
value: acc.address,
}))
.filter(acc => acc.value !== selectedAccount?.value);
const [selectedDestAccount, setSelectedDestAccount] =
useState<AccountOption | null>(null);
const [txIsLoading, setTxIsLoading] = useState(false);
const [txIsDisabled, setTxIsDisabled] = useState(false);
const [txFields, setTxFields] = useState<TxFields>({});
useEffect(() => {
const transactionType = selectedTransaction?.value;
const account = snap.accounts.find(
acc => acc.address === selectedAccount?.value
);
if (!account || !transactionType || txIsLoading) {
setTxIsDisabled(true);
} else {
setTxIsDisabled(false);
}
}, [txIsLoading, selectedTransaction, selectedAccount, snap.accounts]);
useEffect(() => {
let _txFields: TxFields | undefined = transactionsData.find(
tx => tx.TransactionType === selectedTransaction?.value
);
if (!_txFields) return setTxFields({});
_txFields = { ..._txFields } as TxFields;
setSelectedDestAccount(null);
// @ts-ignore
delete _txFields.TransactionType;
// @ts-ignore
delete _txFields.Account;
// @ts-ignore
delete _txFields.Sequence;
setTxFields(_txFields);
}, [selectedTransaction, setSelectedDestAccount]);
const submitTest = useCallback(async () => {
const account = snap.accounts.find(
acc => acc.address === selectedAccount?.value
);
const TransactionType = selectedTransaction?.value;
if (!account || !TransactionType || txIsDisabled) return;
setTxIsLoading(true);
// setTxIsError(null)
try {
let options = { ...txFields };
options.Destination = selectedDestAccount?.value;
(Object.keys(options) as (keyof TxFields)[]).forEach(field => {
let _value = options[field];
// convert currency
if (typeof _value === "object" && _value.type === "currency") {
if (+_value.value) {
options[field] = (+_value.value * 1000000 + "") as any;
} else {
options[field] = undefined; // 👇 💀
}
}
// handle type: `json`
if (typeof _value === "object" && _value.type === "json") {
if (typeof _value.value === "object") {
options[field] = _value.value as any;
} else {
try {
options[field] = JSON.parse(_value.value);
} catch (error) {
const message = `Input error for json field '${field}': ${
error instanceof Error ? error.message : ""
}`;
throw Error(message);
}
}
}
// delete unneccesary fields
if (!options[field]) {
delete options[field];
}
});
const logPrefix = header ? `${header.split(".")[0]}: ` : undefined;
await sendTransaction(
account,
{
TransactionType,
...options,
},
{ logPrefix }
);
} catch (error) {
console.error(error);
if (error instanceof Error) {
state.transactionLogs.push({ type: "error", message: error.message });
}
}
setTxIsLoading(false);
}, [
header,
selectedAccount?.value,
selectedDestAccount?.value,
selectedTransaction?.value,
snap.accounts,
txFields,
txIsDisabled,
]);
const resetState = useCallback(() => {
setSelectedAccount(null);
setSelectedDestAccount(null);
setSelectedTransaction(null);
setTxFields({});
setTxIsDisabled(false);
setTxIsLoading(false);
}, []);
const handleSetAccount = (acc: AccountOption) => {
setSelectedAccount(acc);
streamState.selectedAccount = acc;
};
const usualFields = ["TransactionType", "Amount", "Account", "Destination"];
const otherFields = Object.keys(txFields).filter(
k => !usualFields.includes(k)
) as OtherFields;
return (
<Box css={{ position: "relative", height: "calc(100% - 28px)" }} {...props}>
<Container
css={{
p: "$3 01",
fontSize: "$sm",
height: "calc(100% - 45px)",
}}
>
<Flex column fluid css={{ height: "100%", overflowY: "auto" }}>
<Flex
row
fluid
css={{
justifyContent: "flex-end",
alignItems: "center",
mb: "$3",
mt: "1px",
pr: "1px",
}}
>
<Text muted css={{ mr: "$3" }}>
Transaction type:{" "}
</Text>
<Select
instanceId="transactionsType"
placeholder="Select transaction type"
options={transactionsOptions}
hideSelectedOptions
css={{ width: "70%" }}
value={selectedTransaction}
onChange={(tt: any) => setSelectedTransaction(tt)}
/>
</Flex>
<Flex
row
fluid
css={{
justifyContent: "flex-end",
alignItems: "center",
mb: "$3",
pr: "1px",
}}
>
<Text muted css={{ mr: "$3" }}>
Account:{" "}
</Text>
<Select
instanceId="from-account"
placeholder="Select your account"
css={{ width: "70%" }}
options={accountOptions}
value={selectedAccount}
onChange={(acc: any) => handleSetAccount(acc)} // TODO make react-select have correct types for acc
/>
</Flex>
{txFields.Amount !== undefined && (
<Flex
row
fluid
css={{
justifyContent: "flex-end",
alignItems: "center",
mb: "$3",
pr: "1px",
}}
>
<Text muted css={{ mr: "$3" }}>
Amount (XRP):{" "}
</Text>
<Input
value={txFields.Amount.value}
onChange={e =>
setTxFields({
...txFields,
Amount: { type: "currency", value: e.target.value },
})
}
css={{ width: "70%", flex: "inherit", height: "$9" }}
/>
</Flex>
)}
{txFields.Destination !== undefined && (
<Flex
row
fluid
css={{
justifyContent: "flex-end",
alignItems: "center",
mb: "$3",
pr: "1px",
}}
>
<Text muted css={{ mr: "$3" }}>
Destination account:{" "}
</Text>
<Select
instanceId="to-account"
placeholder="Select the destination account"
css={{ width: "70%" }}
options={destAccountOptions}
value={selectedDestAccount}
isClearable
onChange={(acc: any) => setSelectedDestAccount(acc)}
/>
</Flex>
)}
{otherFields.map(field => {
let _value = txFields[field];
let value = typeof _value === "object" ? _value.value : _value;
value =
typeof value === "object"
? JSON.stringify(value)
: value?.toLocaleString();
let isCurrency =
typeof _value === "object" && _value.type === "currency";
return (
<Flex
key={field}
row
fluid
css={{
justifyContent: "flex-end",
alignItems: "center",
mb: "$3",
pr: "1px",
}}
>
<Text muted css={{ mr: "$3" }}>
{field + (isCurrency ? " (XRP)" : "")}:{" "}
</Text>
<Input
value={value}
onChange={e =>
setTxFields({
...txFields,
[field]:
typeof _value === "object"
? { ..._value, value: e.target.value }
: e.target.value,
})
}
css={{ width: "70%", flex: "inherit", height: "$9" }}
/>
</Flex>
);
})}
</Flex>
</Container>
<Flex
row
css={{
justifyContent: "space-between",
position: "absolute",
left: 0,
bottom: 0,
width: "100%",
}}
>
<Button outline>VIEW AS JSON</Button>
<Flex row>
<Button onClick={resetState} outline css={{ mr: "$3" }}>
RESET
</Button>
<Button
variant="primary"
onClick={submitTest}
isLoading={txIsLoading}
disabled={txIsDisabled}
>
<Play weight="bold" size="16px" />
RUN TEST
</Button>
</Flex>
</Flex>
</Box>
);
};
const Test = () => {
const { transactionLogs } = useSnapshot(state);
const [tabHeaders, setTabHeaders] = useState<string[]>(["test1.json"]);
return (
<Container css={{ px: 0 }}>
<Split
direction="vertical"
sizes={getSplit("testVertical") || [50, 50]}
gutterSize={4}
gutterAlign="center"
style={{ height: "calc(100vh - 60px)" }}
onDragEnd={e => saveSplit("testVertical", e)}
>
<Flex
row
fluid
css={{
justifyContent: "center",
p: "$3 $2",
}}
>
<Split
direction="horizontal"
sizes={getSplit("testHorizontal") || [50, 50]}
minSize={[180, 320]}
gutterSize={4}
gutterAlign="center"
style={{
display: "flex",
flexDirection: "row",
width: "100%",
height: "100%",
}}
onDragEnd={e => saveSplit("testHorizontal", e)}
>
<Box css={{ width: "55%", px: "$2" }}>
<Tabs
keepAllAlive
forceDefaultExtension
defaultExtension=".json"
onCreateNewTab={name => setTabHeaders(tabHeaders.concat(name))}
onCloseTab={index =>
setTabHeaders(tabHeaders.filter((_, idx) => idx !== index))
}
>
{tabHeaders.map(header => (
<Tab key={header} header={header}>
<Transaction header={header} />
</Tab>
))}
</Tabs>
</Box>
<Box css={{ width: "45%", mx: "$2", height: "100%" }}>
<Accounts card hideDeployBtn showHookStats />
</Box>
</Split>
</Flex>
<Flex row fluid>
<Split
direction="horizontal"
sizes={[50, 50]}
minSize={[320, 160]}
gutterSize={4}
gutterAlign="center"
style={{
display: "flex",
flexDirection: "row",
width: "100%",
height: "100%",
}}
>
<Box
css={{
borderRight: "1px solid $mauve8",
height: "100%",
}}
>
<LogBox
title="Development Log"
logs={transactionLogs}
clearLog={() => (state.transactionLogs = [])}
/>
</Box>
<Box css={{ height: "100%" }}>
<DebugStream />
</Box>
</Split>
</Flex>
</Split>
</Container>
);
};
export default Test;

View File

@@ -1,7 +0,0 @@
import Container from "../../components/Container";
const Test = () => {
return <Container css={{ py: "$10" }}>This will be the test page</Container>;
};
export default Test;

File diff suppressed because it is too large Load Diff

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

BIN
public/apple-touch-icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

9
public/browserconfig.xml Normal file
View File

@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<browserconfig>
<msapplication>
<tile>
<square150x150logo src="/mstile-150x150.png"/>
<TileColor>#161618</TileColor>
</tile>
</msapplication>
</browserconfig>

BIN
public/favicon-16x16.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 737 B

BIN
public/favicon-32x32.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 15 KiB

BIN
public/mstile-150x150.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

9
public/pattern-2.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 138 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 152 KiB

9
public/pattern-dark.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 153 KiB

9
public/pattern.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 134 KiB

View File

@@ -0,0 +1,18 @@
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
width="588.000000pt" height="588.000000pt" viewBox="0 0 588.000000 588.000000"
preserveAspectRatio="xMidYMid meet">
<metadata>
Created by potrace 1.14, written by Peter Selinger 2001-2017
</metadata>
<g transform="translate(0.000000,588.000000) scale(0.100000,-0.100000)"
fill="#000000" stroke="none">
<path d="M3843 4097 c-381 -380 -737 -736 -791 -790 l-99 -99 -940 1 -940 0
-204 -211 c-112 -116 -228 -235 -257 -265 -29 -30 -51 -57 -48 -60 2 -3 542
-5 1198 -5 l1193 0 799 -799 799 -799 380 0 c209 0 378 2 376 5 -2 2 -422 423
-933 934 l-928 929 921 920 c507 507 921 923 921 927 0 3 -170 5 -377 5 l-378
0 -692 -693z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 837 B

BIN
public/share-image.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 710 KiB

19
public/site.webmanifest Normal file
View File

@@ -0,0 +1,19 @@
{
"name": "Hooks Builder",
"short_name": "Hooks Builder",
"icons": [
{
"src": "/android-chrome-192x192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "/android-chrome-512x512.png",
"sizes": "512x512",
"type": "image/png"
}
],
"theme_color": "#161618",
"background_color": "#161618",
"display": "standalone"
}

View File

@@ -1,4 +0,0 @@
<svg width="283" height="64" viewBox="0 0 283 64" fill="none"
xmlns="http://www.w3.org/2000/svg">
<path d="M141.04 16c-11.04 0-19 7.2-19 18s8.96 18 20 18c6.67 0 12.55-2.64 16.19-7.09l-7.65-4.42c-2.02 2.21-5.09 3.5-8.54 3.5-4.79 0-8.86-2.5-10.37-6.5h28.02c.22-1.12.35-2.28.35-3.5 0-10.79-7.96-17.99-19-17.99zm-9.46 14.5c1.25-3.99 4.67-6.5 9.45-6.5 4.79 0 8.21 2.51 9.45 6.5h-18.9zM248.72 16c-11.04 0-19 7.2-19 18s8.96 18 20 18c6.67 0 12.55-2.64 16.19-7.09l-7.65-4.42c-2.02 2.21-5.09 3.5-8.54 3.5-4.79 0-8.86-2.5-10.37-6.5h28.02c.22-1.12.35-2.28.35-3.5 0-10.79-7.96-17.99-19-17.99zm-9.45 14.5c1.25-3.99 4.67-6.5 9.45-6.5 4.79 0 8.21 2.51 9.45 6.5h-18.9zM200.24 34c0 6 3.92 10 10 10 4.12 0 7.21-1.87 8.8-4.92l7.68 4.43c-3.18 5.3-9.14 8.49-16.48 8.49-11.05 0-19-7.2-19-18s7.96-18 19-18c7.34 0 13.29 3.19 16.48 8.49l-7.68 4.43c-1.59-3.05-4.68-4.92-8.8-4.92-6.07 0-10 4-10 10zm82.48-29v46h-9V5h9zM36.95 0L73.9 64H0L36.95 0zm92.38 5l-27.71 48L73.91 5H84.3l17.32 30 17.32-30h10.39zm58.91 12v9.69c-1-.29-2.06-.49-3.2-.49-5.81 0-10 4-10 10V51h-9V17h9v9.2c0-5.08 5.91-9.2 13.2-9.2z" fill="#000"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.1 KiB

4
raw-loader.d.ts vendored Normal file
View File

@@ -0,0 +1,4 @@
declare module "*.md" {
const content: string;
export default content;
};

172
state.ts
View File

@@ -1,172 +0,0 @@
import { proxy, subscribe } from 'valtio';
import { devtools } from 'valtio/utils';
import { Octokit } from '@octokit/core';
import type monaco from 'monaco-editor';
import toast from 'react-hot-toast';
const octokit = new Octokit();
interface File {
name: string;
language: string;
content: string;
}
interface IState {
files: File[],
active: number;
loading: boolean;
compiling: boolean;
logs: {
type: 'error' | 'warning' | 'log',
message: string;
}[];
editorCtx?: typeof monaco.editor;
editorSettings: {
tabSize: number;
}
}
let localStorageState: null | string = null;
let initialState = {
files: [],
active: 0,
loading: false,
compiling: false,
logs: [],
editorCtx: undefined,
editorSettings: {
tabSize: 2
}
}
// Check if there's a persited state in localStorage
if (typeof window !== 'undefined') {
try {
localStorageState = localStorage.getItem('hooksIdeState');
} catch (err) {
console.log(`localStorage state broken`);
localStorage.removeItem('hooksIdeState');
}
}
if (localStorageState) {
initialState = JSON.parse(localStorageState);
}
// Initialize state
export const state = proxy<IState>(initialState);
// Fetch content from Githug Gists
export const fetchFiles = (gistId: string) => {
if (gistId) {
state.logs.push({ type: 'log', message: `Fetching Gist with id: ${gistId}` });
octokit.request("GET /gists/{gist_id}", { gist_id: gistId }).then(res => {
if (res.data.files && Object.keys(res.data.files).length > 0) {
const files = Object.keys(res.data.files).map(filename => ({
name: res.data.files?.[filename]?.filename || 'noname.c',
language: res.data.files?.[filename]?.language?.toLowerCase() || '',
content: res.data.files?.[filename]?.content || ''
}))
state.loading = false;
if (files.length > 0) {
state.logs.push({ type: 'log', message: 'Fetched successfully ✅' })
state.files = files;
return
}
return
}
}).catch(err => {
state.loading = false;
state.logs.push({ type: 'error', message: `Couldn't find Gist with id: ${gistId}` })
return
})
return
}
state.loading = false;
// return state.files = initFiles
}
export const updateEditorSettings = (editorSettings: IState['editorSettings']) => {
state.editorCtx?.getModels().forEach(model => {
console.log(model.uri)
model.updateOptions({
...editorSettings
})
});
return state.editorSettings = editorSettings;
}
export const saveFile = (value: string) => {
const editorModels = state.editorCtx?.getModels();
const currentModel = editorModels?.find(editorModel => editorModel.uri.path === `/${state.files[state.active].name}`);
console.log(currentModel?.getValue())
if (state.files.length > 0) {
console.log('häää')
state.files[state.active].content = currentModel?.getValue() || '';
}
console.log(state.files[state.active])
toast.success('Saved successfully', { position: 'bottom-center' })
}
export const createNewFile = (name: string) => {
const emptyFile: File = { name, language: 'c', content: "" };
state.files.push(emptyFile)
state.active = state.files.length - 1;
}
export const compileCode = async (activeId: number) => {
if (!process.env.NEXT_PUBLIC_COMPILE_API_ENDPOINT) {
throw Error('Missing env!')
};
if (state.compiling) {
return;
}
state.compiling = true;
try {
const res = await fetch(process.env.NEXT_PUBLIC_COMPILE_API_ENDPOINT, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
"output": "wasm",
"compress": true,
"files": [
{
"type": "c",
"name": state.files[activeId].name,
"options": "-g -O3",
"src": state.files[activeId].content
}
]
})
});
const json = await res.json();
state.compiling = false;
if (!json.success) {
state.logs.push({ type: 'error', message: json.message })
if (json.tasks && json.tasks.length > 0) {
json.tasks.forEach((task: any) => {
if (!task.success) {
state.logs.push({ type: 'error', message: task?.console })
}
})
}
return toast.error(`Couldn't compile!`, { position: 'bottom-center' });
}
state.logs.push({ type: 'log', message: 'Compiled successfully ✅' })
toast.success('Compiled successfully!', { position: 'bottom-center' });
} catch (err) {
console.log(err)
state.logs.push({ type: 'error', message: 'Error occured while compiling!' })
state.compiling = false;
}
}
const unsub = devtools(state, 'Files State');
subscribe(state, () => {
const { editorCtx, ...storedState } = state;
localStorage.setItem('hooksIdeState', JSON.stringify(storedState))
});

View File

@@ -0,0 +1,96 @@
import toast from "react-hot-toast";
import state, { FaucetAccountRes } from '../index';
export const names = [
"Alice",
"Bob",
"Carol",
"Carlos",
"Charlie",
"Dan",
"Dave",
"David",
"Faythe",
"Frank",
"Grace",
"Heidi",
"Judy",
"Olive",
"Peggy",
"Walter",
];
/* This function adds faucet account to application global state.
* It calls the /api/faucet endpoint which in send a HTTP POST to
* https://hooks-testnet.xrpl-labs.com/newcreds and it returns
* new account with 10 000 XRP. Hooks Testnet /newcreds endpoint
* is protected with CORS so that's why we did our own endpoint
*/
export const addFaucetAccount = async (showToast: boolean = false) => {
// Lets limit the number of faucet accounts to 5 for now
if (state.accounts.length > 5) {
return toast.error("You can only have maximum 6 accounts");
}
if (typeof window !== 'undefined') {
const toastId = showToast ? toast.loading("Creating account") : "";
const res = await fetch(`${window.location.origin}/api/faucet`, {
method: "POST",
});
const json: FaucetAccountRes | { error: string } = await res.json();
if ("error" in json) {
if (showToast) {
return toast.error(json.error, { id: toastId });
} else {
return;
}
} else {
if (showToast) {
toast.success("New account created", { id: toastId });
}
const currNames = state.accounts.map(acc => acc.name);
state.accounts.push({
name: names.filter(name => !currNames.includes(name))[0],
xrp: (json.xrp || 0 * 1000000).toString(),
address: json.address,
secret: json.secret,
sequence: 1,
hooks: [],
isLoading: false,
version: '2'
});
}
}
};
// fetch initial faucets
(async function fetchFaucets() {
if (typeof window !== 'undefined') {
if (state.accounts.length === 0) {
await addFaucetAccount();
// setTimeout(() => {
// addFaucetAccount();
// }, 10000);
}
}
})();
export const addFunds = async (address: string) => {
const toastId = toast.loading("Requesting funds");
const res = await fetch(`${window.location.origin}/api/faucet?account=${address}`, {
method: "POST",
});
const json: FaucetAccountRes | { error: string } = await res.json();
if ("error" in json) {
return toast.error(json.error, { id: toastId });
} else {
toast.success(`Funds added (${json.xrp} XRP)`, { id: toastId });
const currAccount = state.accounts.find(acc => acc.address === address);
if (currAccount) {
currAccount.xrp = (Number(currAccount.xrp) + (json.xrp * 1000000)).toString();
}
}
}

View File

@@ -0,0 +1,91 @@
import toast from "react-hot-toast";
import Router from 'next/router';
import state from "../index";
import { saveFile } from "./saveFile";
import { decodeBinary } from "../../utils/decodeBinary";
import { ref } from "valtio";
/* compileCode sends the code of the active file to compile endpoint
* If all goes well you will get base64 encoded wasm file back with
* some extra logging information if we can provide it. This function
* also decodes the returned wasm and creates human readable WAT file
* out of it and store both in global state.
*/
export const compileCode = async (activeId: number) => {
// Save the file to global state
saveFile(false);
if (!process.env.NEXT_PUBLIC_COMPILE_API_ENDPOINT) {
throw Error("Missing env!");
}
// Bail out if we're already compiling
if (state.compiling) {
// if compiling is ongoing return
return;
}
// Set loading state to true
state.compiling = true;
state.logs = []
try {
const res = await fetch(process.env.NEXT_PUBLIC_COMPILE_API_ENDPOINT, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
output: "wasm",
compress: true,
files: [
{
type: "c",
name: state.files[activeId].name,
src: state.files[activeId].content,
},
],
}),
});
const json = await res.json();
state.compiling = false;
if (!json.success) {
state.logs.push({ type: "error", message: json.message });
if (json.tasks && json.tasks.length > 0) {
json.tasks.forEach((task: any) => {
if (!task.success) {
state.logs.push({ type: "error", message: task?.console });
}
});
}
return toast.error(`Couldn't compile!`, { position: "bottom-center" });
}
state.logs.push({
type: "success",
message: `File ${state.files?.[activeId]?.name} compiled successfully. Ready to deploy.`,
link: Router.asPath.replace("develop", "deploy"),
linkText: "Go to deploy",
});
// Decode base64 encoded wasm that is coming back from the endpoint
const bufferData = await decodeBinary(json.output);
state.files[state.active].compiledContent = ref(bufferData);
state.files[state.active].lastCompiled = new Date();
// Import wabt from and create human readable version of wasm file and
// put it into state
import("wabt").then((wabt) => {
const ww = wabt.default();
const myModule = ww.readWasm(new Uint8Array(bufferData), {
readDebugNames: true,
});
myModule.applyNames();
const wast = myModule.toText({ foldExprs: false, inlineExport: false });
state.files[state.active].compiledWatContent = wast;
toast.success("Compiled successfully!", { position: "bottom-center" });
});
} catch (err) {
console.log(err);
state.logs.push({
type: "error",
message: "Error occured while compiling!",
});
state.compiling = false;
}
};

View File

@@ -0,0 +1,17 @@
import state, { IFile } from '../index';
const languageMapping = {
'ts': 'typescript',
'js': 'javascript',
'md': 'markdown',
'c': 'c',
'h': 'c',
'other': ''
} /* Initializes empty file to global state */
export const createNewFile = (name: string) => {
const tempName = name.split('.');
const fileExt = tempName[tempName.length - 1] || 'other';
const emptyFile: IFile = { name, language: languageMapping[fileExt as 'ts' | 'js' | 'md' | 'c' | 'h' | 'other'], content: "" };
state.files.push(emptyFile);
state.active = state.files.length - 1;
};

223
state/actions/deployHook.ts Normal file
View File

@@ -0,0 +1,223 @@
import { derive, sign } from "xrpl-accountlib";
import toast from "react-hot-toast";
import state, { IAccount } from "../index";
import calculateHookOn, { TTS } from "../../utils/hookOnCalculator";
import { SetHookData } from "../../components/SetHookDialog";
const hash = async (string: string) => {
const utf8 = new TextEncoder().encode(string);
const hashBuffer = await crypto.subtle.digest('SHA-256', utf8);
const hashArray = Array.from(new Uint8Array(hashBuffer));
const hashHex = hashArray
.map((bytes) => bytes.toString(16).padStart(2, '0'))
.join('');
return hashHex;
}
function toHex(str: string) {
var result = '';
for (var i = 0; i < str.length; i++) {
result += str.charCodeAt(i).toString(16);
}
return result.toUpperCase();
}
function arrayBufferToHex(arrayBuffer?: ArrayBuffer | null) {
if (!arrayBuffer) {
return "";
}
if (
typeof arrayBuffer !== "object" ||
arrayBuffer === null ||
typeof arrayBuffer.byteLength !== "number"
) {
throw new TypeError("Expected input to be an ArrayBuffer");
}
var view = new Uint8Array(arrayBuffer);
var result = "";
var value;
for (var i = 0; i < view.length; i++) {
value = view[i].toString(16);
result += value.length === 1 ? "0" + value : value;
}
return result;
}
/* deployHook function turns the wasm binary into
* hex string, signs the transaction and deploys it to
* Hooks testnet.
*/
export const deployHook = async (account: IAccount & { name?: string }, data: SetHookData) => {
if (
!state.files ||
state.files.length === 0 ||
!state.files?.[state.active]?.compiledContent
) {
return;
}
if (!state.files?.[state.active]?.compiledContent) {
return;
}
if (!state.client) {
return;
}
const HookNamespace = await hash(arrayBufferToHex(
state.files?.[state.active]?.compiledContent
).toUpperCase());
const hookOnValues: (keyof TTS)[] = data.Invoke.map(tt => tt.value);
const { HookParameters } = data;
const filteredHookParameters = HookParameters.filter(hp => hp.HookParameter.HookParameterName && hp.HookParameter.HookParameterValue)?.map(aa => ({ HookParameter: { HookParameterName: toHex(aa.HookParameter.HookParameterName || ''), HookParameterValue: toHex(aa.HookParameter.HookParameterValue || '') } }));
// const filteredHookGrants = HookGrants.filter(hg => hg.HookGrant.Authorize || hg.HookGrant.HookHash).map(hg => {
// return {
// HookGrant: {
// ...(hg.HookGrant.Authorize && { Authorize: hg.HookGrant.Authorize }),
// // HookHash: hg.HookGrant.HookHash || undefined
// ...(hg.HookGrant.HookHash && { HookHash: hg.HookGrant.HookHash })
// }
// }
// });
if (typeof window !== "undefined") {
const tx = {
Account: account.address,
TransactionType: "SetHook",
Sequence: account.sequence,
Fee: "100000",
Hooks: [
{
Hook: {
CreateCode: arrayBufferToHex(
state.files?.[state.active]?.compiledContent
).toUpperCase(),
HookOn: calculateHookOn(hookOnValues),
HookNamespace,
HookApiVersion: 0,
Flags: 1,
// ...(filteredHookGrants.length > 0 && { HookGrants: filteredHookGrants }),
...(filteredHookParameters.length > 0 && { HookParameters: filteredHookParameters }),
}
}
]
};
const keypair = derive.familySeed(account.secret);
const { signedTransaction } = sign(tx, keypair);
const currentAccount = state.accounts.find(
(acc) => acc.address === account.address
);
if (currentAccount) {
currentAccount.isLoading = true;
}
let submitRes;
try {
submitRes = await state.client.send({
command: "submit",
tx_blob: signedTransaction,
});
if (submitRes.engine_result === "tesSUCCESS") {
state.deployLogs.push({
type: "success",
message: "Hook deployed successfully ✅",
});
state.deployLogs.push({
type: "success",
message: `[${submitRes.engine_result}] ${submitRes.engine_result_message} Validated ledger index: ${submitRes.validated_ledger_index}`,
});
} else {
state.deployLogs.push({
type: "error",
message: `[${submitRes.engine_result || submitRes.error}] ${submitRes.engine_result_message || submitRes.error_exception}`,
});
}
} catch (err) {
console.log(err);
state.deployLogs.push({
type: "error",
message: "Error occured while deploying",
});
}
if (currentAccount) {
currentAccount.isLoading = false;
}
return submitRes;
}
};
export const deleteHook = async (account: IAccount & { name?: string }) => {
if (!state.client) {
return;
}
const currentAccount = state.accounts.find(
(acc) => acc.address === account.address
);
if (currentAccount?.isLoading || !currentAccount?.hooks.length) {
return
}
if (typeof window !== "undefined") {
const tx = {
Account: account.address,
TransactionType: "SetHook",
Sequence: account.sequence,
Fee: "100000",
Hooks: [
{
Hook: {
CreateCode: "",
Flags: 1,
}
}
]
};
const keypair = derive.familySeed(account.secret);
const { signedTransaction } = sign(tx, keypair);
if (currentAccount) {
currentAccount.isLoading = true;
}
let submitRes;
const toastId = toast.loading("Deleting hook...");
try {
submitRes = await state.client.send({
command: "submit",
tx_blob: signedTransaction,
});
if (submitRes.engine_result === "tesSUCCESS") {
toast.success('Hook deleted successfully ✅', { id: toastId })
state.deployLogs.push({
type: "success",
message: "Hook deleted successfully ✅",
});
state.deployLogs.push({
type: "success",
message: `[${submitRes.engine_result}] ${submitRes.engine_result_message} Validated ledger index: ${submitRes.validated_ledger_index}`,
});
currentAccount.hooks = [];
} else {
toast.error(`${submitRes.engine_result_message || submitRes.error_exception}`, { id: toastId })
state.deployLogs.push({
type: "error",
message: `[${submitRes.engine_result || submitRes.error}] ${submitRes.engine_result_message || submitRes.error_exception}`,
});
}
} catch (err) {
console.log(err);
toast.error('Error occured while deleting hoook', { id: toastId })
state.deployLogs.push({
type: "error",
message: "Error occured while deleting hook",
});
}
if (currentAccount) {
currentAccount.isLoading = false;
}
return submitRes;
}
};

View File

@@ -0,0 +1,19 @@
import { createZip } from '../../utils/zip';
import { guessZipFileName } from '../../utils/helpers';
import state from '..'
import toast from 'react-hot-toast';
export const downloadAsZip = async () => {
try {
state.zipLoading = true
// TODO do something about file/gist loading state
const files = state.files.map(({ name, content }) => ({ name, content }));
const zipped = await createZip(files);
const zipFileName = guessZipFileName(files);
zipped.saveFile(zipFileName);
} catch (error) {
toast.error('Error occured while creating zip file, try again later')
} finally {
state.zipLoading = false
}
};

View File

@@ -0,0 +1,82 @@
import { Octokit } from "@octokit/core";
import Router from "next/router";
import state from '../index';
import { templateFileIds } from '../constants';
import { hookapiH, hookmacroH, sfcodesH } from '../constants/headerTemplates';
const octokit = new Octokit();
/* Fetches Gist files from Githug Gists based on
* gistId and stores the content in global state
*/
export const fetchFiles = (gistId: string) => {
state.loading = true;
if (gistId && !state.files.length) {
state.logs.push({
type: "log",
message: `Fetching Gist with id: ${gistId}`,
});
octokit
.request("GET /gists/{gist_id}", { gist_id: gistId })
.then(res => {
if (!Object.values(templateFileIds).includes(gistId)) {
return res
}
// in case of templates, fetch header file(s) and append to res
const files = {
...res.data.files,
'hookapi.h': res.data.files?.['hookapi.h'] || { filename: 'hookapi.h', content: hookapiH, language: 'C' },
'hookmacro.h': res.data.files?.['hookmacro.h'] || { filename: 'hookmacro.h', content: hookmacroH, language: 'C' },
'sfcodes.h': res.data.files?.['sfcodes.h'] || { filename: 'sfcodes.h', content: sfcodesH, language: 'C' },
};
res.data.files = files;
return res;
// If you want to load templates from GIST instad, uncomment the code below and comment the code above.
// return octokit.request("GET /gists/{gist_id}", { gist_id: templateFileIds.headers }).then(({ data: { files: headerFiles } }) => {
// const files = { ...res.data.files, ...headerFiles }
// console.log(headerFiles)
// res.data.files = files
// return res
// })
})
.then((res) => {
if (res.data.files && Object.keys(res.data.files).length > 0) {
const files = Object.keys(res.data.files).map((filename) => ({
name: res.data.files?.[filename]?.filename || "untitled.c",
language: res.data.files?.[filename]?.language?.toLowerCase() || "",
content: res.data.files?.[filename]?.content || "",
}));
state.loading = false;
if (files.length > 0) {
state.logs.push({
type: "success",
message: "Fetched successfully ✅",
});
state.files = files;
state.gistId = gistId;
state.gistName = Object.keys(res.data.files)?.[0] || "untitled";
state.gistOwner = res.data.owner?.login;
return;
} else {
// Open main modal if now files
state.mainModalOpen = true;
}
return Router.push({ pathname: "/develop" });
}
state.loading = false;
})
.catch((err) => {
// console.error(err)
state.loading = false;
state.logs.push({
type: "error",
message: `Couldn't find Gist with id: ${gistId}`,
});
return;
});
return;
}
state.loading = false;
};

View File

@@ -0,0 +1,30 @@
import toast from "react-hot-toast";
import { derive } from "xrpl-accountlib";
import state from '../index';
import { names } from './addFaucetAccount';
// Adds test account to global state with secret key
export const importAccount = (secret: string) => {
if (!secret) {
return toast.error("You need to add secret!");
}
if (state.accounts.find((acc) => acc.secret === secret)) {
return toast.error("Account already added!");
}
const account = derive.familySeed(secret);
if (!account.secret.familySeed) {
return toast.error(`Couldn't create account!`);
}
state.accounts.push({
name: names[state.accounts.length],
address: account.address || "",
secret: account.secret.familySeed || "",
xrp: "0",
sequence: 1,
hooks: [],
isLoading: false,
version: '2'
});
return toast.success("Account imported successfully!");
};

25
state/actions/index.ts Normal file
View File

@@ -0,0 +1,25 @@
import { addFaucetAccount } from "./addFaucetAccount";
import { compileCode } from "./compileCode";
import { createNewFile } from "./createNewFile";
import { deployHook } from "./deployHook";
import { fetchFiles } from "./fetchFiles";
import { importAccount } from "./importAccount";
import { saveFile } from "./saveFile";
import { syncToGist } from "./syncToGist";
import { updateEditorSettings } from "./updateEditorSettings";
import { downloadAsZip } from "./downloadAsZip";
import { sendTransaction } from "./sendTransaction";
export {
addFaucetAccount,
compileCode,
createNewFile,
deployHook,
fetchFiles,
importAccount,
saveFile,
syncToGist,
updateEditorSettings,
downloadAsZip,
sendTransaction
};

View File

@@ -0,0 +1,15 @@
import { snapshot } from "valtio"
import state from ".."
export type SplitSize = number[]
export const saveSplit = (splitId: string, event: SplitSize) => {
state.splits[splitId] = event
}
export const getSplit = (splitId: string): SplitSize | null => {
const { splits } = snapshot(state)
const split = splits[splitId]
return split ? split : null
}

17
state/actions/saveFile.ts Normal file
View File

@@ -0,0 +1,17 @@
import toast from "react-hot-toast";
import state from '../index';
// Saves the current editor content to global state
export const saveFile = (showToast: boolean = true) => {
const editorModels = state.editorCtx?.getModels();
const sought = '/' + state.files[state.active].name;
const currentModel = editorModels?.find((editorModel) => {
return editorModel.uri.path.endsWith(sought);
});
if (state.files.length > 0) {
state.files[state.active].content = currentModel?.getValue() || "";
}
if (showToast) {
toast.success("Saved successfully", { position: "bottom-center" });
}
};

View File

@@ -0,0 +1,57 @@
import { derive, sign } from "xrpl-accountlib";
import state from '..'
import type { IAccount } from "..";
interface TransactionOptions {
TransactionType: string,
Account?: string,
Fee?: string,
Destination?: string
[index: string]: any
}
interface OtherOptions {
logPrefix?: string
}
export const sendTransaction = async (account: IAccount, txOptions: TransactionOptions, options?: OtherOptions) => {
if (!state.client) throw Error('XRPL client not initalized')
const { Fee = "1000", ...opts } = txOptions
const tx: TransactionOptions = {
Account: account.address,
Sequence: account.sequence, // TODO auto-fillable
Fee, // TODO auto-fillable
...opts
};
const currAcc = state.accounts.find(acc => acc.address === account.address);
if (currAcc) {
currAcc.sequence = account.sequence + 1;
}
const { logPrefix = '' } = options || {}
try {
const signedAccount = derive.familySeed(account.secret);
const { signedTransaction } = sign(tx, signedAccount);
const response = await state.client.send({
command: "submit",
tx_blob: signedTransaction,
});
if (response.engine_result === "tesSUCCESS") {
state.transactionLogs.push({
type: 'success',
message: `${logPrefix}[${response.engine_result}] ${response.engine_result_message}`
})
} else {
state.transactionLogs.push({
type: "error",
message: `${logPrefix}[${response.error || response.engine_result}] ${response.error_exception || response.engine_result_message}`,
});
}
} catch (err) {
console.error(err);
state.transactionLogs.push({
type: "error",
message: err instanceof Error ? `${logPrefix}Error: ${err.message}` : `${logPrefix}Something went wrong, try again later`,
});
}
};

102
state/actions/syncToGist.ts Normal file
View File

@@ -0,0 +1,102 @@
import type { Session } from "next-auth";
import toast from "react-hot-toast";
import { Octokit } from "@octokit/core";
import Router from "next/router";
import state from '../index';
const octokit = new Octokit();
// Syncs the current files from the state to GitHub Gists.
export const syncToGist = async (
session?: Session | null,
createNewGist?: boolean
) => {
let files: Record<string, { filename: string; content: string }> = {};
state.gistLoading = true;
if (!session || !session.user) {
state.gistLoading = false;
return toast.error("You need to be logged in!");
}
const toastId = toast.loading("Pushing to Gist");
if (!state.files || !state.files.length) {
state.gistLoading = false;
return toast.error(`You need to create some files we can push to gist`, {
id: toastId,
});
}
if (
state.gistId &&
session?.user.username === state.gistOwner &&
!createNewGist
) {
// You can only remove files from Gist by updating file with empty contents
// So we need to fetch existing files and compare those to local state
// and then send empty content if we don't have matching files anymore
// on local state
const currentFilesRes = await octokit.request("GET /gists/{gist_id}", {
gist_id: state.gistId,
});
if (currentFilesRes.data.files) {
Object.keys(currentFilesRes?.data?.files).forEach((filename) => {
files[`${filename}`] = { filename, content: "" };
});
}
state.files.forEach((file) => {
files[`${file.name}`] = { filename: file.name, content: file.content };
});
// Update existing Gist
octokit
.request("PATCH /gists/{gist_id}", {
gist_id: state.gistId,
files,
headers: {
authorization: `token ${session?.accessToken || ""}`,
},
})
.then((res) => {
state.gistLoading = false;
return toast.success("Updated to gist successfully!", { id: toastId });
})
.catch((err) => {
console.log(err);
state.gistLoading = false;
return toast.error(`Could not update Gist, try again later!`, {
id: toastId,
});
});
} else {
// Not Gist of the current user or it isn't Gist yet
state.files.forEach((file) => {
files[`${file.name}`] = { filename: file.name, content: file.content };
});
octokit
.request("POST /gists", {
files,
public: true,
headers: {
authorization: `token ${session?.accessToken || ""}`,
},
})
.then((res) => {
state.gistLoading = false;
state.gistOwner = res.data.owner?.login;
state.gistId = res.data.id;
state.gistName = Array.isArray(res.data.files)
? Object.keys(res.data?.files)?.[0]
: "Untitled";
Router.push({ pathname: `/develop/${res.data.id}` });
return toast.success("Created new gist successfully!", { id: toastId });
})
.catch((err) => {
console.log(err);
state.gistLoading = false;
return toast.error(`Could not create Gist, try again later!`, {
id: toastId,
});
});
}
};
export default syncToGist;

View File

@@ -0,0 +1,14 @@
import state, { IState } from '../index';
// Updates editor settings and stores them
// in global state
export const updateEditorSettings = (
editorSettings: IState["editorSettings"]
) => {
state.editorCtx?.getModels().forEach((model) => {
model.updateOptions({
...editorSettings,
});
});
return (state.editorSettings = editorSettings);
};

File diff suppressed because it is too large Load Diff

1
state/constants/index.ts Normal file
View File

@@ -0,0 +1 @@
export * from './templates'

View File

@@ -0,0 +1,20 @@
// export const templateFileIds = {
// 'starter': '1d14e51e2e02dc0a508cb0733767a914', // TODO currently same as accept
// 'firewall': 'bcd6d0c0fcbe52545ddb802481ff9d26',
// 'notary': 'a789c75f591eeab7932fd702ed8cf9ea',
// 'carbon': '43925143fa19735d8c6505c34d3a6a47',
// 'peggy': 'ceaf352e2a65741341033ab7ef05c448',
// 'headers': '9b448e8a55fab11ef5d1274cb59f9cf3'
// }
export const templateFileIds = {
'starter': '1f7d2963d9e342ea092286115274f3e3',
'firewall': '70edec690f0de4dd315fad1f4f996d8c',
'notary': '3d5677768fe8a54c4f6317e185d9ba66',
'carbon': 'a9fbcaf1b816b198c7fc0f62962bebf2',
'doubler': '56b86174aeb70b2b48eee962bad3e355',
'peggy': 'd21298a37e1550b781682014762a567b',
'headers': '55f639bce59a49c58c45e663776b5138'
}
export const apiHeaderFiles = ['hookapi.h', 'sfcodes.h', 'hookmacro.h']

161
state/index.ts Normal file
View File

@@ -0,0 +1,161 @@
import type monaco from "monaco-editor";
import { proxy, ref, subscribe } from "valtio";
import { devtools } from 'valtio/utils';
import { XrplClient } from "xrpl-client";
import { SplitSize } from "./actions/persistSplits";
declare module "valtio" {
function useSnapshot<T extends object>(p: T): T;
function snapshot<T extends object>(p: T): T;
}
export interface IFile {
name: string;
language: string;
content: string;
compiledContent?: ArrayBuffer | null;
compiledWatContent?: string | null;
lastCompiled?: Date
}
export interface FaucetAccountRes {
address: string;
secret: string;
xrp: number;
hash: string;
code: string;
}
export interface IAccount {
name: string;
address: string;
secret: string;
xrp: string;
sequence: number;
hooks: string[];
isLoading: boolean;
version?: string;
}
export interface ILog {
type: "error" | "warning" | "log" | "success";
message: string;
jsonData?: any,
timestamp?: string;
link?: string;
linkText?: string;
defaultCollapsed?: boolean
}
export interface IState {
files: IFile[];
gistId?: string | null;
gistOwner?: string | null;
gistName?: string | null;
active: number;
activeWat: number;
loading: boolean;
gistLoading: boolean;
zipLoading: boolean;
compiling: boolean;
logs: ILog[];
deployLogs: ILog[];
transactionLogs: ILog[];
editorCtx?: typeof monaco.editor;
editorSettings: {
tabSize: number;
};
splits: {
[id: string]: SplitSize
};
client: XrplClient | null;
clientStatus: "offline" | "online";
mainModalOpen: boolean;
mainModalShowed: boolean;
accounts: IAccount[];
}
// let localStorageState: null | string = null;
let initialState: IState = {
files: [],
// active file index on the Develop page editor
active: 0,
// Active file index on the Deploy page editor
activeWat: 0,
loading: false,
compiling: false,
logs: [],
deployLogs: [],
transactionLogs: [],
editorCtx: undefined,
gistId: undefined,
gistOwner: undefined,
gistName: undefined,
gistLoading: false,
zipLoading: false,
editorSettings: {
tabSize: 2,
},
splits: {},
client: null,
clientStatus: "offline" as "offline",
mainModalOpen: false,
mainModalShowed: false,
accounts: [],
};
let localStorageAccounts: string | null = null;
let initialAccounts: IAccount[] = [];
// TODO: What exactly should we store in localStorage? editorSettings, splits, accounts?
// Check if there's a persited accounts in localStorage
if (typeof window !== "undefined") {
try {
localStorageAccounts = localStorage.getItem("hooksIdeAccounts");
} catch (err) {
console.log(`localStorage state broken`);
localStorage.removeItem("hooksIdeAccounts");
}
if (localStorageAccounts) {
initialAccounts = JSON.parse(localStorageAccounts);
}
// filter out old accounts (they do not have version property at all)
// initialAccounts = initialAccounts.filter(acc => acc.version === '2');
}
// Initialize state
const state = proxy<IState>({
...initialState,
accounts: initialAccounts.length > 0 ? initialAccounts : [],
logs: [],
});
// Initialize socket connection
const client = new XrplClient(`wss://${process.env.NEXT_PUBLIC_TESTNET_URL}`);
client.on("online", () => {
state.client = ref(client);
state.clientStatus = "online";
});
client.on("offline", () => {
state.clientStatus = "offline";
});
if (process.env.NODE_ENV !== "production") {
devtools(state, "Files State");
}
if (typeof window !== "undefined") {
subscribe(state, () => {
const { accounts, active } = state;
const accountsNoLoading = accounts.map(acc => ({ ...acc, isLoading: false }))
localStorage.setItem("hooksIdeAccounts", JSON.stringify(accountsNoLoading));
if (!state.files[active]?.compiledWatContent) {
state.activeWat = 0;
} else {
state.activeWat = active;
}
});
}
export default state

View File

@@ -1,26 +1,27 @@
// stitches.config.ts
import type Stitches from '@stitches/react';
import { createStitches } from '@stitches/react';
import type Stitches from "@stitches/react";
import { createStitches } from "@stitches/react";
import {
gray,
blue,
red,
green,
plum,
crimson,
grass,
slate,
mauve,
pink,
yellow,
amber,
purple,
grayDark,
blueDark,
redDark,
greenDark,
plumDark,
crimsonDark,
grassDark,
slateDark,
mauveDark,
pinkDark,
yellowDark,
} from '@radix-ui/colors';
amberDark,
purpleDark,
red,
redDark,
} from "@radix-ui/colors";
export const {
styled,
@@ -36,26 +37,33 @@ export const {
colors: {
...gray,
...blue,
...red,
...green,
...plum,
...crimson,
...grass,
...slate,
...mauve,
...pink,
...yellow,
...amber,
...purple,
...red,
accent: "#9D2DFF",
background: "$gray1",
backgroundAlt: "$gray4",
text: "$gray12",
textMuted: "$gray10",
primary: "$plum",
error: '$red9',
warning: '$amber11',
success: "$grass11",
white: "white",
black: "black"
black: "black",
deep: "rgb(244, 244, 244)",
},
fonts: {
body: 'Work Sans, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", sans-serif',
heading: 'Work Sans, sans-serif',
monospace: 'Roboto, monospace',
heading: "Work Sans, sans-serif",
monospace: "Roboto Mono, monospace",
},
fontSizes: {
xs: "0.75rem",
xs: "0.6875rem",
sm: "0.875rem",
md: "1rem",
lg: "1.125rem",
@@ -68,7 +76,7 @@ export const {
"7xl": "4.5rem",
"8xl": "6rem",
"9xl": "8rem",
default: '$md'
default: "$md",
},
space: {
px: "1px",
@@ -104,15 +112,15 @@ export const {
72: "18rem",
80: "20rem",
96: "24rem",
"widePlus": '2048px',
"wide": '1536px',
"layoutPlus": '1260px',
"layout": '1024px',
"copyUltra": '980px',
"copyPlus": '768px',
"copy": '680px',
"narrowPlus": '600px',
"narrow": '512px',
widePlus: "2048px",
wide: "1536px",
layoutPlus: "1260px",
layout: "1024px",
copyUltra: "980px",
copyPlus: "768px",
copy: "680px",
narrowPlus: "600px",
narrow: "512px",
xs: "20rem",
sm: "24rem",
md: "28rem",
@@ -191,7 +199,7 @@ export const {
},
fontWeights: {
body: 400,
heading: 400,
heading: 700,
bold: 700,
},
lineHeights: {
@@ -212,62 +220,112 @@ export const {
lg: "(min-width: 62em)",
xl: "(min-width: 80em)",
"2xl": "(min-width: 96em)",
hover: '(any-hover: hover)',
dark: '(prefers-color-scheme: dark)',
light: '(prefers-color-scheme: light)',
hover: "(any-hover: hover)",
dark: "(prefers-color-scheme: dark)",
light: "(prefers-color-scheme: light)",
},
utils: {
// Abbreviated margin properties
m: (value: Stitches.ScaleValue<'space'> | Stitches.PropertyValue<'margin'>) => ({
m: (
value: Stitches.ScaleValue<"space"> | Stitches.PropertyValue<"margin">
) => ({
margin: value,
}),
mt: (value: Stitches.ScaleValue<'space'> | Stitches.PropertyValue<'marginTop'>) => ({
mt: (
value: Stitches.ScaleValue<"space"> | Stitches.PropertyValue<"marginTop">
) => ({
marginTop: value,
}),
mr: (value: Stitches.ScaleValue<'space'> | Stitches.PropertyValue<'marginRight'>) => ({
mr: (
value:
| Stitches.ScaleValue<"space">
| Stitches.PropertyValue<"marginRight">
) => ({
marginRight: value,
}),
mb: (value: Stitches.ScaleValue<'space'> | Stitches.PropertyValue<'marginBottom'>) => ({
mb: (
value:
| Stitches.ScaleValue<"space">
| Stitches.PropertyValue<"marginBottom">
) => ({
marginBottom: value,
}),
ml: (value: Stitches.ScaleValue<'space'> | Stitches.PropertyValue<'marginLeft'>) => ({
ml: (
value: Stitches.ScaleValue<"space"> | Stitches.PropertyValue<"marginLeft">
) => ({
marginLeft: value,
}),
mx: (value: Stitches.ScaleValue<'space'> | Stitches.PropertyValue<'marginLeft' | 'marginRight'>) => ({
mx: (
value:
| Stitches.ScaleValue<"space">
| Stitches.PropertyValue<"marginLeft" | "marginRight">
) => ({
marginLeft: value,
marginRight: value,
}),
my: (value: Stitches.ScaleValue<'space'> | Stitches.PropertyValue<'marginTop' | 'marginBottom'>) => ({
my: (
value:
| Stitches.ScaleValue<"space">
| Stitches.PropertyValue<"marginTop" | "marginBottom">
) => ({
marginTop: value,
marginBottom: value,
}),
// Abbreviated margin properties
p: (value: Stitches.ScaleValue<'space'> | Stitches.PropertyValue<'padding'>) => ({
p: (
value: Stitches.ScaleValue<"space"> | Stitches.PropertyValue<"padding">
) => ({
padding: value,
}),
pt: (value: Stitches.ScaleValue<'space'> | Stitches.PropertyValue<'paddingTop'>) => ({
pt: (
value: Stitches.ScaleValue<"space"> | Stitches.PropertyValue<"paddingTop">
) => ({
paddingTop: value,
}),
pr: (value: Stitches.ScaleValue<'space'> | Stitches.PropertyValue<'paddingRight'>) => ({
pr: (
value:
| Stitches.ScaleValue<"space">
| Stitches.PropertyValue<"paddingRight">
) => ({
paddingRight: value,
}),
pb: (value: Stitches.ScaleValue<'space'> | Stitches.PropertyValue<'paddingBottom'>) => ({
pb: (
value:
| Stitches.ScaleValue<"space">
| Stitches.PropertyValue<"paddingBottom">
) => ({
paddingBottom: value,
}),
pl: (value: Stitches.ScaleValue<'space'> | Stitches.PropertyValue<'paddingLeft'>) => ({
pl: (
value:
| Stitches.ScaleValue<"space">
| Stitches.PropertyValue<"paddingLeft">
) => ({
paddingLeft: value,
}),
px: (value: Stitches.ScaleValue<'space'> | Stitches.PropertyValue<'paddingLeft' | 'paddingRight'>) => ({
px: (
value:
| Stitches.ScaleValue<"space">
| Stitches.PropertyValue<"paddingLeft" | "paddingRight">
) => ({
paddingLeft: value,
paddingRight: value,
}),
py: (value: Stitches.ScaleValue<'space'> | Stitches.PropertyValue<'paddingTop' | 'paddingBottom'>) => ({
py: (
value:
| Stitches.ScaleValue<"space">
| Stitches.PropertyValue<"paddingTop" | "paddingBottom">
) => ({
paddingTop: value,
paddingBottom: value,
}),
// A property for applying width/height together
size: (value: Stitches.ScaleValue<'space'> | Stitches.PropertyValue<'width' | 'height'>) => ({
size: (
value:
| Stitches.ScaleValue<"space">
| Stitches.PropertyValue<"width" | "height">
) => ({
width: value,
height: value,
}),
@@ -276,40 +334,45 @@ export const {
// }),
// A property to apply linear gradient
linearGradient: (value: Stitches.ScaleValue<'space'>) => ({
linearGradient: (value: Stitches.ScaleValue<"space">) => ({
backgroundImage: `linear-gradient(${value})`,
}),
// An abbreviated property for border-radius
br: (value: Stitches.ScaleValue<'space'>) => ({
br: (value: Stitches.ScaleValue<"space">) => ({
borderRadius: value,
}),
},
});
export const darkTheme = createTheme('dark', {
export const darkTheme = createTheme("dark", {
colors: {
...grayDark,
...blueDark,
...redDark,
...greenDark,
...plumDark,
...crimsonDark,
...grassDark,
...slateDark,
...mauveDark,
...pinkDark,
...yellowDark
...amberDark,
...purpleDark,
...redDark,
deep: "rgb(10, 10, 10)",
// backgroundA: transparentize(0.1, grayDark.gray1),
},
});
export const globalStyles = globalCss({
// body: { backgroundColor: '$background', color: '$text', fontFamily: 'Helvetica' },
'html, body': {
backgroundColor: '$gray1',
color: '$gray12',
fontFamily: '$body',
fontSize: '$md',
'-webkit-font-smoothing': 'antialiased',
'-moz-osx-font-smoothing': 'grayscale'
"html, body": {
backgroundColor: "$mauve2",
color: "$mauve12",
fontFamily: "$body",
fontSize: "$md",
"-webkit-font-smoothing": "antialiased",
"-moz-osx-font-smoothing": "grayscale",
},
a: {
color: "inherit",
textDecoration: "none",
},
});

View File

@@ -6,13 +6,51 @@ body,
min-height: 100vh;
display: flex;
flex-direction: column;
}
a {
color: inherit;
text-decoration: none;
/* overflow-y: hidden; */
}
* {
box-sizing: border-box;
}
.monaco-hover p {
margin-bottom: 10px !important;
}
.gutter {
position: relative;
transition: border-color 0.3s, background-color 0.3s;
}
.gutter-vertical {
margin-top: -4px;
}
.gutter-horizontal {
margin-left: -4px;
}
.gutter-vertical:hover {
cursor: row-resize;
background-color: rgba(255, 255, 255, 0.25);
}
html.light .gutter-vertical:hover {
background-color: rgba(0, 0, 0, 0.25);
}
.gutter-horizontal:hover {
cursor: col-resize;
background-color: rgba(255, 255, 255, 0.25);
}
html.light .gutter-horizontal:hover {
background-color: rgba(0, 0, 0, 0.25);
}
/* Adjust Monaco tooltip stylings */
.markdown-hover h3 {
margin: 0;
}
.monaco-editor .monaco-hover hr {
margin: 8px 0;
}
.monaco-editor .monaco-hover {
z-index: 9999;
}

View File

@@ -3,7 +3,7 @@
"inherit": true,
"rules": [
{
"background": "1a1d1e",
"background": "161618",
"token": ""
},
{
@@ -182,10 +182,10 @@
],
"colors": {
"editor.foreground": "#D0D0FF",
"editor.background": "#232326",
"editor.background": "#1C1C1F",
"editor.selectionBackground": "#ffffff30",
"editor.lineHighlightBackground": "#ffffff20",
"editorCursor.foreground": "#7070FF",
"editorWhitespace.foreground": "#BFBFBF"
}
}
}

View File

@@ -3,7 +3,7 @@
"inherit": true,
"rules": [
{
"background": "FFFFFF",
"background": "F4F2F4",
"token": ""
},
{
@@ -89,10 +89,10 @@
],
"colors": {
"editor.foreground": "#000000",
"editor.background": "#f4f2f4",
"editor.background": "#F9F8F9",
"editor.selectionBackground": "#B5D5FF",
"editor.lineHighlightBackground": "#00000012",
"editorCursor.foreground": "#000000",
"editorWhitespace.foreground": "#BFBFBF"
}
}
}

View File

@@ -17,7 +17,8 @@
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true
"incremental": true,
"noUnusedLocals": true
},
"include": [
"next-env.d.ts",
@@ -25,6 +26,7 @@
"**/*.tsx"
],
"exclude": [
"node_modules"
"node_modules",
"*.md"
]
}

15
utils/decodeBinary.ts Normal file
View File

@@ -0,0 +1,15 @@
import { decodeRestrictedBase64ToBytes } from "./decodeRestrictedBase64ToBytes";
import { isZlibData, decompressZlib } from "./zlib";
import { fromByteArray } from "base64-js";
export async function decodeBinary(input: string): Promise<ArrayBuffer> {
let data = decodeRestrictedBase64ToBytes(input);
if (isZlibData(data)) {
data = await decompressZlib(data);
}
return data.buffer as ArrayBuffer;
}
export function encodeBinary(input: ArrayBuffer): string {
return fromByteArray(new Uint8Array(input));
}

View File

@@ -0,0 +1,44 @@
const base64DecodeMap = [ // starts at 0x2B
62, 0, 0, 0, 63, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61,
0, 0, 0, 0, 0, 0, 0, // 0x3A-0x40
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18,
19, 20, 21, 22, 23, 24, 25, 0, 0, 0, 0, 0, 0, // 0x5B-0x0x60
26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43,
44, 45, 46, 47, 48, 49, 50, 51
];
const base64DecodeMapOffset = 0x2B;
const base64EOF = 0x3D;
export function decodeRestrictedBase64ToBytes(encoded: string) {
let ch: any;
let code: any;
let code2: any;
const len = encoded.length;
const padding = encoded.charAt(len - 2) === "=" ? 2 : encoded.charAt(len - 1) === "=" ? 1 : 0;
const decoded = new Uint8Array((encoded.length >> 2) * 3 - padding);
for (let i = 0, j = 0; i < encoded.length;) {
ch = encoded.charCodeAt(i++);
code = base64DecodeMap[ch - base64DecodeMapOffset];
ch = encoded.charCodeAt(i++);
code2 = base64DecodeMap[ch - base64DecodeMapOffset];
decoded[j++] = (code << 2) | ((code2 & 0x30) >> 4);
ch = encoded.charCodeAt(i++);
if (ch === base64EOF) {
return decoded;
}
code = base64DecodeMap[ch - base64DecodeMapOffset];
decoded[j++] = ((code2 & 0x0f) << 4) | ((code & 0x3c) >> 2);
ch = encoded.charCodeAt(i++);
if (ch === base64EOF) {
return decoded;
}
code2 = base64DecodeMap[ch - base64DecodeMapOffset];
decoded[j++] = ((code & 0x03) << 6) | code2;
}
return decoded;
}

9
utils/helpers.ts Normal file
View File

@@ -0,0 +1,9 @@
interface File {
name: string
}
export const guessZipFileName = (files: File[]) => {
let parts = (files.filter(f => f.name.endsWith('.c'))[0]?.name || 'hook').split('.')
parts = parts.length > 1 ? parts.slice(0, -1) : parts
return parts.join('')
}

41
utils/hookOnCalculator.ts Normal file
View File

@@ -0,0 +1,41 @@
export const tts = {
ttPAYMENT: 0,
ttESCROW_CREATE: 1,
ttESCROW_FINISH: 2,
ttACCOUNT_SET: 3,
ttESCROW_CANCEL: 4,
ttREGULAR_KEY_SET: 5,
ttOFFER_CREATE: 7,
ttOFFER_CANCEL: 8,
ttTICKET_CREATE: 10,
ttSIGNER_LIST_SET: 12,
ttPAYCHAN_CREATE: 13,
ttPAYCHAN_FUND: 14,
ttPAYCHAN_CLAIM: 15,
ttCHECK_CREATE: 16,
ttCHECK_CASH: 17,
ttCHECK_CANCEL: 18,
ttDEPOSIT_PREAUTH: 19,
ttTRUST_SET: 20,
ttACCOUNT_DELETE: 21,
ttHOOK_SET: 22
};
export type TTS = typeof tts;
const calculateHookOn = (arr: (keyof TTS)[]) => {
let start = '0x00000000003ff5bf';
arr.forEach(n => {
let v = BigInt(start);
v ^= (BigInt(1) << BigInt(tts[n as keyof TTS]));
let s = v.toString(16);
let l = s.length;
if (l < 16)
s = '0'.repeat(16 - l) + s;
s = '0x' + s;
start = s;
})
return start.substring(2);
}
export default calculateHookOn

21
utils/json.ts Normal file
View File

@@ -0,0 +1,21 @@
export const extractJSON = (str?: string) => {
if (!str) return
let firstOpen = 0, firstClose = 0, candidate = '';
firstOpen = str.indexOf('{', firstOpen + 1);
do {
firstClose = str.lastIndexOf('}');
if (firstClose <= firstOpen) {
return;
}
do {
candidate = str.substring(firstOpen, firstClose + 1);
try {
let result = JSON.parse(candidate);
return { result, start: firstOpen < 0 ? 0 : firstOpen, end: firstClose }
}
catch (e) { }
firstClose = str.substring(0, firstClose).lastIndexOf('}');
} while (firstClose > firstOpen);
firstOpen = str.indexOf('{', firstOpen + 1);
} while (firstOpen != -1);
}

50
utils/languageClient.ts Normal file
View File

@@ -0,0 +1,50 @@
import { MessageConnection } from "@codingame/monaco-jsonrpc";
import { MonacoLanguageClient, ErrorAction, CloseAction, createConnection } from "@codingame/monaco-languageclient";
import Router from "next/router";
import normalizeUrl from "normalize-url";
import ReconnectingWebSocket from "reconnecting-websocket";
export function createLanguageClient(connection: MessageConnection): MonacoLanguageClient {
return new MonacoLanguageClient({
name: "Clangd Language Client",
clientOptions: {
// use a language id as a document selector
documentSelector: ['c', 'h'],
// disable the default error handler
errorHandler: {
error: () => ErrorAction.Continue,
closed: () => {
if (Router.pathname.includes('/develop')) {
return CloseAction.Restart
} else {
return CloseAction.DoNotRestart
}
}
},
},
// create a language client connection from the JSON RPC connection on demand
connectionProvider: {
get: (errorHandler, closeHandler) => {
return Promise.resolve(createConnection(connection, errorHandler, closeHandler))
}
}
});
}
export function createUrl(path: string): string {
const protocol = location.protocol === 'https:' ? 'wss' : 'ws';
return normalizeUrl(`${protocol}://${location.host}${location.pathname}${path}`);
}
export function createWebSocket(url: string) {
const socketOptions = {
maxReconnectionDelay: 10000,
minReconnectionDelay: 1000,
reconnectionDelayGrowFactor: 1.3,
connectionTimeout: 10000,
maxRetries: Infinity,
debug: false
};
return new ReconnectingWebSocket(url, [], socketOptions);
}

59
utils/libwabt.js Normal file

File diff suppressed because one or more lines are too long

8
utils/truncate.ts Normal file
View File

@@ -0,0 +1,8 @@
const truncate = (str: string, max: number = 8) => {
const array = str.trim().split('');
const ellipsis = array.length > max ? '...' : '';
return array.slice(0, max).join('') + ellipsis;
};
export default truncate

714
utils/wat-highlight.ts Normal file
View File

@@ -0,0 +1,714 @@
// 'WebAssembly Text Format' Monarch language
import type monaco from 'monaco-editor';
const WebAssemblyTextLanguage: { config: monaco.languages.LanguageConfiguration, tokens: monaco.languages.IMonarchLanguage } = {
config: {
brackets: [
["(", ")"],
["if", "end"],
["loop", "end"],
["block", "end"],
],
autoClosingPairs: [
{ open: "(", close: ")" },
{ open: "if", close: "end" },
{ open: "loop", close: "end" },
{ open: "block", close: "end" },
],
surroundingPairs: [
{ open: "(", close: ")" },
{ open: "if", close: "end" },
{ open: "loop", close: "end" },
{ open: "block", close: "end" },
],
},
tokens: {
keywords: [
"module",
"import",
"export",
"memory",
"data",
"table",
"elem",
"start",
"func",
"tag",
"type",
"param",
"result",
"global",
"local",
"mut",
"struct",
"array",
"field",
],
types: [
"i8",
"i16",
"i32",
"i64",
"f32",
"f64",
"v128",
"i31ref",
"eqref",
"anyref",
"dataref",
"externref",
"funcref",
"exnref",
"extern",
"null",
"any",
"eq",
],
instructions: [
"pop",
"nop",
"drop",
"data.drop",
"elem.drop",
"local.get",
"local.set",
"local.tee",
"global.get",
"global.set",
"tuple.make",
"tuple.extract",
"select",
"v128.const",
"v128.and",
"v128.or",
"v128.xor",
"v128.not",
"v128.andnot",
"v128.bitselect",
"v128.load",
"v128.load8x8_s",
"v128.load8x8_u",
"v128.load16x4_s",
"v128.load16x4_u",
"v128.load32x2_s",
"v128.load32x2_u",
"v128.load8_lane",
"v128.load16_lane",
"v128.load32_lane",
"v128.load64_lane",
"v128.load8_splat",
"v128.load16_splat",
"v128.load32_splat",
"v128.load64_splat",
"v128.load32_zero",
"v128.load64_zero",
"v128.store",
"v128.store8_lane",
"v128.store16_lane",
"v128.store32_lane",
"v128.store64_lane",
"v128.any_true",
"i8x16.shuffle",
"i8x16.swizzle",
"i8x16.bitmask",
"i8x16.splat",
"i8x16.popcnt",
"i8x16.replace_lane",
"i8x16.extract_lane_s",
"i8x16.extract_lane_u",
"i8x16.all_true",
"i8x16.abs",
"i8x16.add",
"i8x16.add_sat_s",
"i8x16.add_sat_u",
"i8x16.sub",
"i8x16.sub_sat_s",
"i8x16.sub_sat_u",
"i8x16.mul",
"i8x16.neg",
"i8x16.shl",
"i8x16.shr_s",
"i8x16.shr_u",
"i8x16.eq",
"i8x16.ne",
"i8x16.lt_s",
"i8x16.lt_u",
"i8x16.le_s",
"i8x16.le_u",
"i8x16.gt_s",
"i8x16.gt_u",
"i8x16.ge_s",
"i8x16.ge_u",
"i8x16.min_s",
"i8x16.min_u",
"i8x16.max_s",
"i8x16.max_u",
"i8x16.avgr_u",
"i8x16.narrow_i16x8_s",
"i8x16.narrow_i16x8_u",
"i16x8.bitmask",
"i16x8.splat",
"i16x8.load_8x8_s",
"i16x8.load_8x8_u",
"i16x8.replace_lane",
"i16x8.extract_lane_s",
"i16x8.extract_lane_u",
"i16x8.extend_low_i8x16_s",
"i16x8.extend_high_i8x16_s",
"i16x8.extend_low_i8x16_u",
"i16x8.extend_high_i8x16_u",
"i16x8.all_true",
"i16x8.abs",
"i16x8.add",
"i16x8.add_sat_s",
"i16x8.add_sat_u",
"i16x8.extadd_pairwise_i8x16_s",
"i16x8.extadd_pairwise_i8x16_u",
"i16x8.sub",
"i16x8.sub_sat_s",
"i16x8.sub_sat_u",
"i16x8.q15mulr_sat_s",
"i16x8.mul",
"i16x8.extmul_low_i8x16_s",
"i16x8.extmul_high_i8x16_s",
"i16x8.extmul_low_i8x16_u",
"i16x8.extmul_high_i8x16_u",
"i16x8.neg",
"i16x8.shl",
"i16x8.shr_s",
"i16x8.shr_u",
"i16x8.eq",
"i16x8.ne",
"i16x8.lt_s",
"i16x8.lt_u",
"i16x8.le_s",
"i16x8.le_u",
"i16x8.gt_s",
"i16x8.gt_u",
"i16x8.ge_s",
"i16x8.ge_u",
"i16x8.min_s",
"i16x8.min_u",
"i16x8.max_s",
"i16x8.max_u",
"i16x8.avgr_u",
"i16x8.narrow_i32x4_s",
"i16x8.narrow_i32x4_u",
"i32x4.bitmask",
"i32x4.splat",
"i32x4.load_16x4_s",
"i32x4.load_16x4_u",
"i32x4.replace_lane",
"i32x4.extract_lane",
"i32x4.extend_low_i16x8_s",
"i32x4.extend_high_i16x8_s",
"i32x4.extend_low_i16x8_u",
"i32x4.extend_high_i16x8_u",
"i32x4.all_true",
"i32x4.abs",
"i32x4.add",
"i32x4.extadd_pairwise_i16x8_s",
"i32x4.extadd_pairwise_i16x8_u",
"i32x4.sub",
"i32x4.mul",
"i32x4.extmul_low_i16x8_s",
"i32x4.extmul_high_i16x8_s",
"i32x4.extmul_low_i16x8_u",
"i32x4.extmul_high_i16x8_u",
"i32x4.neg",
"i32x4.shl",
"i32x4.shr_s",
"i32x4.shr_u",
"i32x4.eq",
"i32x4.ne",
"i32x4.lt_s",
"i32x4.lt_u",
"i32x4.le_s",
"i32x4.le_u",
"i32x4.gt_s",
"i32x4.gt_u",
"i32x4.ge_s",
"i32x4.ge_u",
"i32x4.min_s",
"i32x4.min_u",
"i32x4.max_s",
"i32x4.max_u",
"i32x4.trunc_sat_f32x4_s",
"i32x4.trunc_sat_f32x4_u",
"i32x4.trunc_sat_f64x2_s_zero",
"i32x4.trunc_sat_f64x2_u_zero",
"i32x4.dot_i16x8_s",
"i64x2.bitmask",
"i64x2.splat",
"i64x2.load32x2_s",
"i64x2.load32x2_u",
"i64x2.replace_lane",
"i64x2.extract_lane",
"i64x2.extend_low_i32x4_s",
"i64x2.extend_high_i32x4_s",
"i64x2.extend_low_i32x4_u",
"i64x2.extend_high_i32x4_u",
"i64x2.all_true",
"i64x2.abs",
"i64x2.add",
"i64x2.sub",
"i64x2.mul",
"i64x2.neg",
"i64x2.shl",
"i64x2.shr_s",
"i64x2.shr_u",
"f32x4.splat",
"f32x4.replace_lane",
"f32x4.extract_lane",
"f32x4.add",
"f32x4.sub",
"f32x4.mul",
"i64x2.extmul_low_i32x4_s",
"i64x2.extmul_high_i32x4_s",
"i64x2.extmul_low_i32x4_u",
"i64x2.extmul_high_i32x4_u",
"i64x2.eq",
"i64x2.ne",
"i64x2.lt_s",
"i64x2.le_s",
"i64x2.gt_s",
"i64x2.ge_s",
"f32x4.neg",
"f32x4.eq",
"f32x4.ne",
"f32x4.lt",
"f32x4.le",
"f32x4.gt",
"f32x4.ge",
"f32x4.abs",
"f32x4.min",
"f32x4.pmin",
"f32x4.max",
"f32x4.pmax",
"f32x4.div",
"f32x4.sqrt",
"f32x4.ceil",
"f32x4.floor",
"f32x4.trunc",
"f32x4.nearest",
"f32x4.demote_f64x2_zero",
"f32x4.convert_i32x4_s",
"f32x4.convert_i32x4_u",
"f64x2.splat",
"f64x2.replace_lane",
"f64x2.extract_lane",
"f64x2.add",
"f64x2.sub",
"f64x2.mul",
"f64x2.neg",
"f64x2.eq",
"f64x2.ne",
"f64x2.lt",
"f64x2.le",
"f64x2.gt",
"f64x2.ge",
"f64x2.abs",
"f64x2.min",
"f64x2.max",
"f64x2.pmin",
"f64x2.pmax",
"f64x2.div",
"f64x2.sqrt",
"f64x2.ceil",
"f64x2.floor",
"f64x2.trunc",
"f64x2.nearest",
"f64x2.promote_low_f32x4",
"f64x2.convert_low_i32x4_s",
"f64x2.convert_low_i32x4_u",
"i32.atomic.load",
"i32.atomic.load8_u",
"i32.atomic.load16_u",
"i32.atomic.store",
"i32.atomic.store8",
"i32.atomic.store16",
"i32.atomic.rmw.add",
"i32.atomic.rmw.sub",
"i32.atomic.rmw.and",
"i32.atomic.rmw.or",
"i32.atomic.rmw.xor",
"i32.atomic.rmw.xchg",
"i32.atomic.rmw.cmpxchg",
"i32.atomic.rmw8.add_u",
"i32.atomic.rmw8.sub_u",
"i32.atomic.rmw8.and_u",
"i32.atomic.rmw8.or_u",
"i32.atomic.rmw8.xor_u",
"i32.atomic.rmw8.xchg_u",
"i32.atomic.rmw8.cmpxchg_u",
"i32.atomic.rmw16.add_u",
"i32.atomic.rmw16.sub_u",
"i32.atomic.rmw16.and_u",
"i32.atomic.rmw16.or_u",
"i32.atomic.rmw16.xor_u",
"i32.atomic.rmw16.xchg_u",
"i32.atomic.rmw16.cmpxchg_u",
"i64.atomic.load",
"i64.atomic.load8_u",
"i64.atomic.load16_u",
"i64.atomic.load32_u",
"i64.atomic.store",
"i64.atomic.store8",
"i64.atomic.store16",
"i64.atomic.store32",
"i64.atomic.rmw.add",
"i64.atomic.rmw.sub",
"i64.atomic.rmw.and",
"i64.atomic.rmw.or",
"i64.atomic.rmw.xor",
"i64.atomic.rmw.xchg",
"i64.atomic.rmw.cmpxchg",
"i64.atomic.rmw8.add_u",
"i64.atomic.rmw8.sub_u",
"i64.atomic.rmw8.and_u",
"i64.atomic.rmw8.or_u",
"i64.atomic.rmw8.xor_u",
"i64.atomic.rmw8.xchg_u",
"i64.atomic.rmw8.cmpxchg_u",
"i64.atomic.rmw16.add_u",
"i64.atomic.rmw16.sub_u",
"i64.atomic.rmw16.and_u",
"i64.atomic.rmw16.or_u",
"i64.atomic.rmw16.xor_u",
"i64.atomic.rmw16.xchg_u",
"i64.atomic.rmw16.cmpxchg_u",
"i64.atomic.rmw32.add_u",
"i64.atomic.rmw32.sub_u",
"i64.atomic.rmw32.and_u",
"i64.atomic.rmw32.or_u",
"i64.atomic.rmw32.xor_u",
"i64.atomic.rmw32.xchg_u",
"i64.atomic.rmw32.cmpxchg_u",
"atomic.fence",
"func.bind",
"ref",
"ref.eq",
"ref.null",
"ref.is_null",
"ref.is_func",
"ref.is_data",
"ref.is_i31",
"ref.as_func",
"ref.as_non_null",
"ref.as_data",
"ref.as_i31",
"ref.func",
"ref.cast",
"ref.cast_static",
"ref.test",
"ref.test_static",
"table.get",
"table.set",
"table.size",
"table.grow",
"table.fill",
"table.init",
"table.copy",
"throw",
"rethrow",
"i32.load",
"i32.load8_s",
"i32.load8_u",
"i32.load16_s",
"i32.load16_u",
"i32.store",
"i32.store8",
"i32.store16",
"i32.const",
"i32.eqz",
"i32.eq",
"i32.ne",
"i32.lt_s",
"i32.lt_u",
"i32.le_s",
"i32.le_u",
"i32.gt_s",
"i32.gt_u",
"i32.ge_s",
"i32.ge_u",
"i32.clz",
"i32.ctz",
"i32.popcnt",
"i32.add",
"i32.sub",
"i32.mul",
"i32.div_s",
"i32.div_u",
"i32.rem_s",
"i32.rem_u",
"i32.and",
"i32.or",
"i32.xor",
"i32.shl",
"i32.shr_s",
"i32.shr_u",
"i32.rotl",
"i32.rotr",
"i32.wrap_i64",
"i32.trunc_f32_s",
"i32.trunc_f32_u",
"i32.trunc_f64_s",
"i32.trunc_f64_u",
"i32.reinterpret_f32",
"i64.load",
"i64.load8_s",
"i64.load8_u",
"i64.load16_s",
"i64.load16_u",
"i64.load32_s",
"i64.load32_u",
"i64.store",
"i64.store8",
"i64.store16",
"i64.store32",
"i64.const",
"i64.eqz",
"i64.eq",
"i64.ne",
"i64.lt_s",
"i64.lt_u",
"i64.le_s",
"i64.le_u",
"i64.gt_s",
"i64.gt_u",
"i64.ge_s",
"i64.ge_u",
"i64.clz",
"i64.ctz",
"i64.popcnt",
"i64.add",
"i64.sub",
"i64.mul",
"i64.div_s",
"i64.div_u",
"i64.rem_s",
"i64.rem_u",
"i64.and",
"i64.or",
"i64.xor",
"i64.shl",
"i64.shr_s",
"i64.shr_u",
"i64.rotl",
"i64.rotr",
"i64.extend_i32_s",
"i64.extend_i32_u",
"i64.trunc_f32_s",
"i64.trunc_f32_u",
"i64.trunc_f64_s",
"i64.trunc_f64_u",
"i64.reinterpret_f64",
"f32.load",
"f32.store",
"f32.const",
"f32.eq",
"f32.ne",
"f32.lt",
"f32.le",
"f32.gt",
"f32.ge",
"f32.abs",
"f32.neg",
"f32.ceil",
"f32.floor",
"f32.trunc",
"f32.nearest",
"f32.sqrt",
"f32.add",
"f32.sub",
"f32.mul",
"f32.div",
"f32.min",
"f32.max",
"f32.copysign",
"f32.convert_i32_s",
"f32.convert_i32_u",
"f32.convert_i64_s",
"f32.convert_i64_u",
"f32.demote_f64",
"f32.reinterpret_i32",
"f64.load",
"f64.store",
"f64.const",
"f64.eq",
"f64.ne",
"f64.lt",
"f64.le",
"f64.gt",
"f64.ge",
"f64.abs",
"f64.neg",
"f64.ceil",
"f64.floor",
"f64.trunc",
"f64.nearest",
"f64.sqrt",
"f64.add",
"f64.sub",
"f64.mul",
"f64.div",
"f64.min",
"f64.max",
"f64.copysign",
"f64.convert_i32_s",
"f64.convert_i32_u",
"f64.convert_i64_s",
"f64.convert_i64_u",
"f64.promote_f32",
"f64.reinterpret_i64",
"i32.extend8_s",
"i32.extend16_s",
"i64.extend8_s",
"i64.extend16_s",
"i64.extend32_s",
"i32.trunc_sat_f32_s",
"i32.trunc_sat_f32_u",
"i32.trunc_sat_f64_s",
"i32.trunc_sat_f64_u",
"i64.trunc_sat_f32_s",
"i64.trunc_sat_f32_u",
"i64.trunc_sat_f64_s",
"i64.trunc_sat_f64_u",
"memory.size",
"memory.grow",
"memory.copy",
"memory.fill",
"memory.init",
"memory.atomic.notify",
"memory.atomic.wait32",
"memory.atomic.wait64",
"i31.new",
"i31.get_u",
"i31.get_s",
"array.new",
"array.new_default",
"array.init",
"array.init_static",
"array.get",
"array.get_s",
"array.get_u",
"array.set",
"array.len",
"array.copy",
"struct.new",
"struct.new_default",
"struct.new_with_rtt",
"struct.new_default_with_rtt",
"struct.get",
"struct.get_s",
"struct.get_u",
"struct.set",
"rtt.canon",
"rtt.sub",
"rtt.fresh_sub",
],
controlInstructions: [
"block",
"loop",
"if",
"else",
"then",
"end",
"do",
"let",
"br",
"br_if",
"br_table",
"br_on_exn",
"br_on_null",
"br_on_non_null",
"br_on_cast",
"br_on_cast_static",
"br_on_cast_fail",
"br_on_cast_static_fail",
"br_on_func",
"br_on_non_func",
"br_on_data",
"br_on_non_data",
"br_on_i31",
"br_on_non_i31",
"call",
"call_indirect",
"call_ref",
"return",
"return_call",
"return_call_indirect",
"return_call_ref",
"try",
"catch",
"catch_all",
"delegate",
"unreachable",
],
escapes:
/\\(?:[abfnrtv\\"']|x[0-9A-Fa-f]{1,4}|u[0-9A-Fa-f]{4}|U[0-9A-Fa-f]{8})/,
digits: /\d+(_+\d+)*/,
octaldigits: /[0-7]+(_+[0-7]+)*/,
binarydigits: /[0-1]+(_+[0-1]+)*/,
hexdigits: /[[0-9a-fA-F]+(_+[0-9a-fA-F]+)*/,
tokenizer: {
root: [
// whitespace
{ include: "@whitespace" },
// strings
[/"([^"\\]|\\.)*$/, "string.invalid"], // non-teminated string
[/"/, "string", "@string"],
// numbers (not all of these are generated, but here to be sure)
[/(@digits)[eE]([\-+]?(@digits))?[fFdD]?/, "number.float"],
[/(@digits)\.(@digits)([eE][\-+]?(@digits))?[fFdD]?/, "number.float"],
[/0[xX](@hexdigits)[Ll]?/, "number.hex"],
[/0(@octaldigits)[Ll]?/, "number.octal"],
[/0[bB](@binarydigits)[Ll]?/, "number.binary"],
[/(@digits)[fFdD]/, "number.float"],
[/(@digits)[lL]?/, "number"],
// variable names
[/\$[^\s\)]*/, { token: "identifier" }],
// instructions and types
[
/[a-z0-9_]+(?:\.[a-z0-9_]+)*/,
{
cases: {
"@types": { token: "type.$0" },
"@keywords": { token: "keyword.$0" },
"@controlInstructions": { token: "controlInstruction.$0" },
"@instructions": { token: "instruction.$0" },
"@default": "identifier",
},
},
],
],
string: [
[/[^\\"]+/, "string"],
[/@escapes/, "string.escape"],
[/\\./, "string.escape.invalid"],
[/"/, "string", "@pop"],
],
whitespace: [
[/[ \t\r\n]+/, ""],
[/(;; )(ERROR |FAILURE )([^\n]*)/, ["comment", "error", ""]],
[/(;; )(WARNING )([^\n]*)/, ["comment", "warning", ""]],
[/(;; )(INFO )([^\n]*)/, ["comment", "info", ""]],
[/(;; )(PEDANTIC )([^\n]*)/, ["comment", "pedantic", ""]],
[/(;; +)(~+|\^)$/, ["comment", "underline"]],
[/(;; )([^\n]*)/, ["comment", ""]],
[/;;[^\n]*/, "comment"],
],
},
},
};
export default WebAssemblyTextLanguage;

32
utils/zip.ts Normal file
View File

@@ -0,0 +1,32 @@
import JSZip, { JSZipFileOptions } from 'jszip'
import { saveAs } from 'file-saver'
interface File {
name: string
content: any
options?: JSZipFileOptions
}
interface Zipped {
saveFile: (filename: string) => void
data: Blob
}
export const createZip = async (files: File[]): Promise<Zipped> => {
const zip = new JSZip()
files.forEach(({ name, content, options }) => {
zip.file(name, content, options)
})
const data = await zip.generateAsync({ type: "blob" })
return {
saveFile: (filename: string) =>
saveAs(data, filename),
data
}
}

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